diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..d2c4fc09ca5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +target +.classpath +.project +.settings +*.iml +*.ipr +*.iws +.idea diff --git a/HEADER.txt b/HEADER.txt deleted file mode 100644 index 1c669de7240..00000000000 --- a/HEADER.txt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index d6456956733..883ab098f7f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -200,3 +200,65 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + +APACHE JACKRABBIT SUBCOMPONENTS + +Apache Jackrabbit includes parts with separate copyright notices and license +terms. Your use of these subcomponents is subject to the terms and conditions +of the following licenses: + + XPath 2.0/XQuery 1.0 Parser: + http://www.w3.org/2002/11/xquery-xpath-applets/xgrammar.zip + + Copyright (C) 2002 World Wide Web Consortium, (Massachusetts Institute of + Technology, European Research Consortium for Informatics and Mathematics, + Keio University). All Rights Reserved. + + This work is distributed under the W3C(R) Software License in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + W3C(R) SOFTWARE NOTICE AND LICENSE + http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + + This work (and included software, documentation such as READMEs, or + other related items) is being provided by the copyright holders under + the following license. By obtaining, using and/or copying this work, + you (the licensee) agree that you have read, understood, and will comply + with the following terms and conditions. + + Permission to copy, modify, and distribute this software and its + documentation, with or without modification, for any purpose and + without fee or royalty is hereby granted, provided that you include + the following on ALL copies of the software and documentation or + portions thereof, including modifications: + + 1. The full text of this NOTICE in a location viewable to users + of the redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, + or terms and conditions. If none exist, the W3C Software Short + Notice should be included (hypertext is preferred, text is + permitted) within the body of any redistributed or derivative code. + + 3. Notice of any changes or modifications to the files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT + HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR + DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, + TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and + any associated documentation will at all times remain with + copyright holders. diff --git a/NOTICE.txt b/NOTICE.txt index 1010997e172..0dc6f68c75a 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,15 +1,8 @@ -This product includes software developed by +Apache Jackrabbit +Copyright 2014 The Apache Software Foundation + +This product includes software developed at The Apache Software Foundation (http://www.apache.org/). Based on source code originally developed by -Day Software (http://www.day.com). - -This product contains a preliminary implementation of the -Content Repository for Java Technology API, as specified by - - http://www.jcp.org/en/jsr/detail?id=170 - -that is not in final form and will change in the future. -Implementations prior to final publication of JSR 170 -are not considered compliant with JSR 170 and cannot be -advertised as such. +Day Software (http://www.day.com/). diff --git a/README.txt b/README.txt index fbda05aac4f..2143f553530 100644 --- a/README.txt +++ b/README.txt @@ -1,110 +1,79 @@ -======================================================================= -Welcome to Apache Jackrabbit -======================================================================= +============================================================= +Welcome to Apache Jackrabbit +============================================================= -License (see also LICENSE.txt) -============================== - -Copyright 2004-2005 The Apache Software Foundation or its licensors, - as applicable. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - -Getting Started -=============== +Apache Jackrabbit is a fully conforming implementation of the +Content Repository for Java Technology API (JCR, specified in +JSR 170 and 283). -Apache Jackrabbit is an effort undergoing incubation at the -Apache Software Foundation. Incubation is required of all newly -accepted projects until a further review indicates that the -infrastructure, communications, and decision making process -have stabilized in a manner consistent with other successful -ASF projects. While incubation status is not necessarily a -reflection of the completeness or stability of the code, it -does indicate that the project has yet to be fully endorsed -by the ASF. The incubation status is recorded at +A content repository is a hierarchical content store with support +for structured and unstructured content, full text search, +versioning, transactions, observation, and more. - http://incubator.apache.org/projects/jackrabbit.html +Apache Jackrabbit is a project of the Apache Software Foundation. -Mailing Lists -------------- +Building Jackrabbit +=================== -To get involved with the Jackrabbit project, start by having a -look at our website (link at top of page) and join our mailing -lists by sending an empty message to +You can build Jackrabbit like this: - jackrabbit-dev-subscribe :at: incubator.apache.org -and - jackrabbit-commits-subscribe :at: incubator.apache.org + mvn clean install -and the dev mailing list archives can be found at +You need Maven 3 (or higher) with Java 8 (or higher) for the +build. For more instructions, please see the documentation at: - http://incubator.apache.org/mail/jackrabbit-dev/ + http://jackrabbit.apache.org/building-jackrabbit.html +License (see also LICENSE.txt) +============================== -Downloading ------------ +Collective work: Copyright 2014 The Apache Software Foundation. -The Jackrabbit source code is available via Subversion at +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at - https://svn.apache.org/repos/asf/incubator/jackrabbit/trunk + http://www.apache.org/licenses/LICENSE-2.0 -and anonymous access is available at +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. - http://svn.apache.org/repos/asf/incubator/jackrabbit/trunk +Mailing Lists +============= -or with viewcvs at +To get involved with the Apache Jackrabbit project, start by having a +look at our website and joining our mailing lists. For more details about +Jackrabbit mailing lists as well as links to list archives, please see: - http://svn.apache.org/viewcvs/incubator/jackrabbit/trunk/ + http://jackrabbit.apache.org/mailing-lists.html -Once you have a copy of the source code tree, you can use Apache Maven +Latest development +================== - http://maven.apache.org/ +The latest Jackrabbit source code is available via Subversion at -to build the project. After installing Maven 1.0, be sure to download the -latest release of the Ant plugin (1.8.1 or later) using a command like + https://svn.apache.org/repos/asf/jackrabbit/trunk/ - maven plugin:download -DgroupId=maven \ - -DartifactId=maven-ant-plugin -Dversion=1.8.1 +or with ViewVC at -before running one of the maven commands listed at + https://svn.apache.org/viewvc/jackrabbit/trunk/ - http://maven.apache.org/start/use.html +To checkout the main Jackrabbit source tree, run -to build the Jackrabbit project and/or documentation. + svn checkout https://svn.apache.org/repos/asf/jackrabbit/trunk jackrabbit -NOTE: JDK 1.5 users need to download xalan.jar and place it in -$MAVEN_HOME/lib/endorsed to build the Jackrabbit sources. The -reason for this workaround is explained in +If you use Git, you can clone Jackrabbit with - http://issues.apache.org/jira/browse/JCR-46 + git clone git://git.apache.org/jackrabbit.git Credits ======= -who what --------------------- ----------------------------------------------- -Roy Fielding incubation -Stefan Guggisberg core, data model, persistence, nodetypes, misc. -David Nuescheler architecture, api -Dominique Pfister transactions -Peeter Piegaze api -Tim Reilly mavenize -Marcel Reutegger observation, query -Tobias Strasser versioning - - -Changes -======= - -See +See http://jackrabbit.apache.org/jackrabbit-team.html for the list of +Jackrabbit committers and main contributors. diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt new file mode 100644 index 00000000000..75fe4c49954 --- /dev/null +++ b/RELEASE-NOTES.txt @@ -0,0 +1,98 @@ +Release Notes -- Apache Jackrabbit -- Version 2.17.4 + +Introduction +------------ + +This is Apache Jackrabbit(TM) 2.17.4, a fully compliant implementation of the +Content Repository for Java(TM) Technology API, version 2.0 (JCR 2.0) as +specified in the Java Specification Request 283 (JSR 283). + +Apache Jackrabbit 2.17.4 is an unstable release cut directly from +Jackrabbit trunk, with a focus on new features and other +improvements. For production use we recommend the latest stable 2.16.x +release. + +Changes in Jackrabbit 2.17.4 +---------------------------- + +Bug + + [JCR-4317] - davex remoting fails for non-ASCII characters in node names + [JCR-4324] - NPE on Version.getLinearPredecessor() implementation + +Improvement + + [JCR-3211] - Support HTTP proxy in SPI2DAV + +Task + + [JCR-4301] - get rid of JSR 305 dependency + [JCR-4302] - BTreeManager: fix Eclipse compiler error + [JCR-4304] - update Jetty to supported version 9.2.* + [JCR-4307] - Update animal-sniffer-maven-plugin to 1.16 + [JCR-4312] - set baseline comparisonVersion to latest stable (2.16.1) + [JCR-4316] - set baseline comparisonVersion to latest stable (2.16.2) + [JCR-4318] - Update failsafe and surefire plugin versions to 2.22.0 + [JCR-4320] - Update spotbugs plugin to 3.1.5 + [JCR-4321] - Update maven plugins from org.apache.maven.plugins + [JCR-4322] - Consistent use of log4j versions + [JCR-4323] - webapp: update Tomcat dependency to 8.5.32 + [JCR-4326] - Update aws java sdk version to 1.11.330 (consistent with Oak) + [JCR-4327] - Update httpcore dependency to 4.4.10 + [JCR-4331] - Update httpclient dependency to 4.5.6 + [JCR-4332] - Update httpmime dependency to 4.5.6 + [JCR-4333] - Update javax.transaction dependency to 1.3 + +Sub-task + + [JCR-4306] - switch to findbugs replacement that is still maintained (spotbugs) + + +In addition to the above-mentioned changes, this release contains +all the changes included up to the Apache Jackrabbit 2.16.x release. + +For more detailed information about all the changes in this and other +Jackrabbit releases, please see the Jackrabbit issue tracker at + + https://issues.apache.org/jira/browse/JCR + +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.txt file for instructions on how to build this release. + +The source archive is accompanied by SHA1 and SHA512 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://www.apache.org/dist/jackrabbit/KEYS. + +About Apache Jackrabbit +----------------------- + +Apache Jackrabbit is a fully conforming implementation of the Content +Repository for Java Technology API (JCR). A content repository is a +hierarchical content store with support for structured and unstructured +content, full text search, versioning, transactions, observation, and +more. + +For more information, visit http://jackrabbit.apache.org/ + +About The Apache Software Foundation +------------------------------------ + +Established in 1999, The Apache Software Foundation provides organizational, +legal, and financial support for more than 140 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 3,800+ contributors. + +For more information, visit http://www.apache.org/ + +Trademarks +---------- + +Apache Jackrabbit, Jackrabbit, Apache, the Apache feather logo, and the Apache +Jackrabbit project logo are trademarks of The Apache Software Foundation. diff --git a/ToDo.txt b/ToDo.txt deleted file mode 100644 index a305346829b..00000000000 --- a/ToDo.txt +++ /dev/null @@ -1,17 +0,0 @@ -open issues, features not yet implemented, improvements, etc. -------------------------------------------------------------- - -the following list is not yet categorized/prioritized, neither -is it complete :( - -- provide clean abstraction for persistence grouping (nodes & properties - that should be stored/loaded together in the persistence layer); -- HierarchyManager: cache Path objects (key: ItemId, value: Path[]); - update cache on hierarchy changes (move, order, remove, etc) -- inline @todo comments: resolve/implement -- javaDoc, javaDoc, javaDoc -- logging: use Commons Logging (JCL) or Universal and Generic Logging Interface (UGLI) - instead of log4j (patch supplied by manoj is available pending a JCL/UGLI decision) -- logging: remove unnecessary output, check log categories/verbosity, - use 'debug' whenever possible - \ No newline at end of file diff --git a/applications/test/jaas.config b/applications/test/jaas.config deleted file mode 100644 index 1fc3c365871..00000000000 --- a/applications/test/jaas.config +++ /dev/null @@ -1,3 +0,0 @@ -Jackrabbit { - org.apache.jackrabbit.core.security.SimpleLoginModule required anonymousId="anonymous"; -}; \ No newline at end of file diff --git a/applications/test/log4j.properties b/applications/test/log4j.properties deleted file mode 100644 index e03b04c00a1..00000000000 --- a/applications/test/log4j.properties +++ /dev/null @@ -1,21 +0,0 @@ -# Set root logger level to DEBUG and its only appender to A1. -log4j.rootLogger=INFO, file -#log4j.rootLogger=DEBUG, stdout, file -#log4j.rootLogger=ERROR, stdout, file - -log4j.logger.org.apache.jackrabbit.test=DEBUG - -# 'stdout' is set to be a ConsoleAppender. -log4j.appender.stdout=org.apache.log4j.ConsoleAppender - -# 'stdout' uses PatternLayout -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n - -# 'file' is set to be a FileAppender. -log4j.appender.file=org.apache.log4j.FileAppender -log4j.appender.file.File=jcr.log - -# 'file' uses PatternLayout. -log4j.appender.file.layout=org.apache.log4j.PatternLayout -log4j.appender.file.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n diff --git a/applications/test/repository.xml b/applications/test/repository.xml deleted file mode 100644 index cd35dc49e6d..00000000000 --- a/applications/test/repository.xml +++ /dev/null @@ -1,234 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/applications/test/repository/nodetypes/custom_nodetypes.xml b/applications/test/repository/nodetypes/custom_nodetypes.xml deleted file mode 100644 index 47d610ff5c0..00000000000 --- a/applications/test/repository/nodetypes/custom_nodetypes.xml +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - mix:versionable - nt:base - - - - - - - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - - - - mix:versionable - nt:base - - - - - - - - nt:base - - - - - - abc - def - ghi - - - - - abc - def - ghi - - - - - - - (,100) - - - - - (,100) - - - - - - - (1974-02-15T00:00:00.000Z,) - - - - - (,1974-02-15T00:00:00.000Z) - - - - - - - (100,) - - - - - (,100) - - - - - - - (100,) - - - - - (,100) - - - - - - - true - - - - - true - - - - - - - abc - - - - - abc - - - - - - - /abc - - - - - /abc - - - - - - - - nt:base - - - - nt:base - - - - - nt:base - - - - - - - - nt:base - mix:referenceable - - - - - - - - - - nt:base - - - - - nt:base - - - - - - diff --git a/applications/test/repositoryStubImpl.properties b/applications/test/repositoryStubImpl.properties deleted file mode 100644 index 5fe7be648f9..00000000000 --- a/applications/test/repositoryStubImpl.properties +++ /dev/null @@ -1,395 +0,0 @@ -# -# This is the configuration file for the jackrabbit repository test stub. -# - -# Stub implementation class -javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.core.JackrabbitRepositoryStub - -# repository specific configuration -org.apache.jackrabbit.repository.config=applications/test/repository.xml -org.apache.jackrabbit.repository.name=repo -org.apache.jackrabbit.repository.home=applications/test -org.apache.jackrabbit.repository.jaas.config=applications/test/jaas.config - -# credential configuration -javax.jcr.tck.superuser.name=superuser -javax.jcr.tck.superuser.pwd= -javax.jcr.tck.readwrite.name=user -javax.jcr.tck.readwrite.pwd= -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.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.workspacename=test - -# namespace configuration -javax.jcr.tck.namespaces=test -javax.jcr.tck.namespaces.test=http://www.apache.org/jackrabbit/test - -# 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: 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: 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:hierarchyNode - -# Test class: SessionTest -# Test method: testSaveContstraintViolationException -# nodetype that has a property that is mandatory but not autocreated -javax.jcr.tck.SessionTest.testSaveContstraintViolationException.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:base -# name of the mandatory node -javax.jcr.tck.NodeTest.testRemoveMandatoryNode.nodename3=jcr:content - -# Test class: NodeTest -# Test method: testSaveContstraintViolationException -# nodetype that has a property that is mandatory but not autocreated -javax.jcr.tck.NodeTest.testSaveContstraintViolationException.nodetype2=nt:file - -# 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:hierarchyNode - -# 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:sameNameSibsFalseChildNodeDef -javax.jcr.tck.WorkspaceCloneSameNameSibsTest.sameNameSibsTrueNodeType=nt:unstructured - -# Test class: WorkspaceCopyBetweenWorkspacesSameNameSibsTest -javax.jcr.tck.WorkspaceCopyBetweenWorkspacesSameNameSibsTest.sameNameSibsFalseNodeType=test:sameNameSibsFalseChildNodeDef -javax.jcr.tck.WorkspaceCopyBetweenWorkspacesSameNameSibsTest.sameNameSibsTrueNodeType=nt:unstructured - -# Test class: WorkspaceCopySameNameSibsTest -javax.jcr.tck.WorkspaceCopySameNameSibsTest.sameNameSibsFalseNodeType=test:sameNameSibsFalseChildNodeDef -javax.jcr.tck.WorkspaceCopySameNameSibsTest.sameNameSibsTrueNodeType=nt:unstructured - -# Test class: WorkspaceMoveSameNameSibsTest -javax.jcr.tck.WorkspaceMoveSameNameSibsTest.sameNameSibsFalseNodeType=test:sameNameSibsFalseChildNodeDef -javax.jcr.tck.WorkspaceMoveSameNameSibsTest.sameNameSibsTrueNodeType=nt:unstructured - -# Test class: RepositoryLoginTest -javax.jcr.tck.RepositoryLoginTest.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 - -# 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 - -# 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 class: ExportSysViewTest -javax.jcr.tck.ExportSysViewTest.testroot=/testdata - -# ============================================================================== -# 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: GetPropertyNamesTest -javax.jcr.tck.GetPropertyNamesTest.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 -# ============================================================================== - -# nodetye 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.propertyValue=aPropertyValue - -# 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 - -# 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 diff --git a/applications/test/workspaces/default/workspace.xml b/applications/test/workspaces/default/workspace.xml deleted file mode 100644 index eb5cc7fedf6..00000000000 --- a/applications/test/workspaces/default/workspace.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/applications/test/workspaces/test/workspace.xml b/applications/test/workspaces/test/workspace.xml deleted file mode 100644 index 6cd912eefc3..00000000000 --- a/applications/test/workspaces/test/workspace.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - 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/checkstyle-suppressions.xml b/checkstyle-suppressions.xml deleted file mode 100644 index 3876ca316f2..00000000000 --- a/checkstyle-suppressions.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/checkstyle.xml b/checkstyle.xml deleted file mode 100644 index 9a2d4485e83..00000000000 --- a/checkstyle.xml +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/examples/project.xml b/contrib/examples/project.xml deleted file mode 100644 index e14d9dba8bf..00000000000 --- a/contrib/examples/project.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - 3 - jackrabbit-examples - Jackrabbit Examples - 0.16.2-dev - - The Apache Software Foundation - http://incubator.apache.org/projects/jackrabbit.html - http://incubator.apache.org/images/apache-incubator-logo.png - - 2005 - org.apache.jackrabbit.examples* - /images/jackrabbitlogo.gif - - The Jackrabbit Examples package contains example code for illustrating - both general JCR API usage and specific Jackrabbit features. - - Example code for JCR and Jackrabbit - - scm:subversion:http://svn.apache.org/repos/asf/incubator/jackrabbit/trunk/contrib/examples - scm:subversion:https://svn.apache.org/repos/asf/incubator/jackrabbit/trunk/contrib/examples - http://svn.apache.org/viewcvs - - - - The Apache Software License, Version 2.0 - /LICENSE.txt - repo - - - - - - jackrabbit - 0.16.2-dev - - - jsr170 - jcr - 0.16.2 - http://www.day.com/maven/jsr170/jars/jcr-0.16.2.jar - - - - - src/java - - diff --git a/contrib/examples/src/java/org/apache/jackrabbit/examples/FirstSteps.java b/contrib/examples/src/java/org/apache/jackrabbit/examples/FirstSteps.java deleted file mode 100644 index ff0391e07ed..00000000000 --- a/contrib/examples/src/java/org/apache/jackrabbit/examples/FirstSteps.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.examples; - -import org.apache.jackrabbit.core.jndi.RegistryHelper; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Property; -import javax.jcr.PropertyIterator; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.SimpleCredentials; -import javax.jcr.StringValue; -import javax.jcr.Value; -import javax.naming.Context; -import javax.naming.InitialContext; -import java.io.FileInputStream; -import java.io.InputStream; -import java.util.Hashtable; - -/** - * The First Steps example class. - */ -public class FirstSteps { - - /** - * Run the First Steps example. - * - * @param args command line arguments (ignored) - */ - public static void main(String[] args) { - try { - Repository repository = getRepository(); - SimpleCredentials creds = new SimpleCredentials("anonymous", "".toCharArray()); - Session session = repository.login(creds); - Node root = session.getRootNode(); - - System.out.println(root.getPrimaryNodeType().getName()); - - if (!root.hasNode("testnode")) { - System.out.println("creating testnode"); - Node node = root.addNode("testnode", "nt:unstructured"); - node.setProperty("testprop", new StringValue("Hello, World.")); - session.save(); - } - - if (!root.hasNode("importxml")) { - System.out.println("importing xml"); - Node node = root.addNode("importxml", "nt:unstructured"); - InputStream xml = new FileInputStream("repotest/test.xml"); - session.importXML("/importxml", xml); - session.save(); - } - - dump(root); - } catch (Exception e) { - System.err.println(e); - } - } - - /** - * Creates a Repository instance to be used by the example class. - * - * @return repository instance - * @throws Exception on errors - */ - private static Repository getRepository() throws Exception { - String configFile = "repotest/repository.xml"; - String repHomeDir = "repotest"; - - Hashtable env = new Hashtable(); - env.put(Context.INITIAL_CONTEXT_FACTORY, - "org.apache.jackrabbit.core.jndi.provider.DummyInitialContextFactory"); - env.put(Context.PROVIDER_URL, "localhost"); - InitialContext ctx = new InitialContext(env); - - RegistryHelper.registerRepository(ctx, "repo", configFile, repHomeDir, true); - return (Repository) ctx.lookup("repo"); - } - - /** - * Dumps the contents of the given node to standard output. - * - * @param node the node to be dumped - * @throws RepositoryException on repository errors - */ - public static void dump(Node node) throws RepositoryException { - System.out.println(node.getPath()); - - PropertyIterator properties = node.getProperties(); - while (properties.hasNext()) { - Property property = properties.nextProperty(); - System.out.print(property.getPath() + "="); - if (property.getDefinition().isMultiple()) { - Value[] values = property.getValues(); - for (int i = 0; i < values.length; i++) { - if (i > 0) { - System.out.println(","); - } - System.out.println(values[i].getString()); - } - } else { - System.out.print(property.getString()); - } - System.out.println(); - } - - NodeIterator nodes = node.getNodes(); - while (nodes.hasNext()) { - Node child = nodes.nextNode(); - dump(child); - } - } - -} diff --git a/contrib/jcr-rmi/HEADER.txt b/contrib/jcr-rmi/HEADER.txt deleted file mode 100644 index 1c669de7240..00000000000 --- a/contrib/jcr-rmi/HEADER.txt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ \ No newline at end of file diff --git a/contrib/jcr-rmi/LICENSE.txt b/contrib/jcr-rmi/LICENSE.txt deleted file mode 100644 index d6456956733..00000000000 --- a/contrib/jcr-rmi/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/contrib/jcr-rmi/checkstyle.xml b/contrib/jcr-rmi/checkstyle.xml deleted file mode 100644 index edd0cd76c0f..00000000000 --- a/contrib/jcr-rmi/checkstyle.xml +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/jcr-rmi/maven.xml b/contrib/jcr-rmi/maven.xml deleted file mode 100644 index a5f045390b7..00000000000 --- a/contrib/jcr-rmi/maven.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - diff --git a/contrib/jcr-rmi/project.properties b/contrib/jcr-rmi/project.properties deleted file mode 100644 index b0669b2785f..00000000000 --- a/contrib/jcr-rmi/project.properties +++ /dev/null @@ -1,3 +0,0 @@ -maven.javadoc.links=http://java.sun.com/j2se/1.4.2/docs/api/,http://www.day.com/maven/jsr170/javadocs/jcr-0.16.1-pfd/ -maven.repo.remote=http://www.ibiblio.org/maven/,http://www.day.com/maven/ -maven.checkstyle.properties=checkstyle.xml diff --git a/contrib/jcr-rmi/project.xml b/contrib/jcr-rmi/project.xml deleted file mode 100644 index 3cf765933de..00000000000 --- a/contrib/jcr-rmi/project.xml +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - 2 - jcr-rmi - JCR-RMI - 0.16.2 - 2004 - org.apache.jackrabbit.rmi.* - - JCR-RMI is an independent subproject of the Jackrabbit project. It - provides a transparent Remote Method Invocation (RMI) layer for the - Content Repository for Java Technology API (JCR). The JCR-RMI layer - makes it possible to access JCR content repositories across virtual - machine boundaries. Although implemented as a contribution to the - Jackrabbit project, the JCR-RMI layer is independent of the underlying - JCR repository implementation. - - Transparent RMI layer for the JCR API. - - - - Jukka Zitting - jukkaz - jz@yukatan.fi - Yukatan - - Java Developer - - +2 - - - - - - The Apache Software License, Version 2.0 - /LICENSE.txt - repo - - - - - - jsr170 - jcr - 0.16.2 - http://www.day.com/maven/jsr170/jars/jcr-0.16.2.jar - - - xerces - xercesImpl - 2.6.2 - - - easymock - 1.1 - - - junit - 3.8.1 - - - - - src/java - src/test - - - **/*TestAll.java - - - - - diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientAdapterFactory.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientAdapterFactory.java deleted file mode 100644 index c96f8905a8d..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientAdapterFactory.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import javax.jcr.Item; -import javax.jcr.NamespaceRegistry; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.Session; -import javax.jcr.Workspace; -import javax.jcr.lock.Lock; -import javax.jcr.nodetype.ItemDef; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeManager; -import javax.jcr.nodetype.PropertyDef; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; -import javax.jcr.query.QueryResult; -import javax.jcr.query.Row; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; - -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteItemDef; -import org.apache.jackrabbit.rmi.remote.RemoteLock; -import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.RemoteQuery; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; -import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; -import org.apache.jackrabbit.rmi.remote.RemoteRepository; -import org.apache.jackrabbit.rmi.remote.RemoteRow; -import org.apache.jackrabbit.rmi.remote.RemoteSession; -import org.apache.jackrabbit.rmi.remote.RemoteVersion; -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; -import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; - -/** - * Default implementation of the - * {@link org.apache.jackrabbit.rmi.client.LocalAdapterFactory LocalAdapterFactory} - * interface. This factory uses the client adapters defined in this - * package as the default adapter implementations. Subclasses can - * easily override or extend the default adapters by implementing the - * corresponding factory methods. - * - * @author Jukka Zitting - * @author Philipp Koch - */ -public class ClientAdapterFactory implements LocalAdapterFactory { - - /** - * Creates and returns a {@link ClientRepository ClientRepository} - * instance. - * - * {@inheritDoc} - */ - public Repository getRepository(RemoteRepository remote) { - return new ClientRepository(remote, this); - } - - /** - * Creates and returns a {@link ClientSession ClientSession} instance. - * - * {@inheritDoc} - */ - public Session getSession(Repository repository, RemoteSession remote) { - return new ClientSession(repository, remote, this); - } - - /** - * Creates and returns a {@link ClientWorkspace ClientWorkspace} instance. - * - * {@inheritDoc} - */ - public Workspace getWorkspace(Session session, RemoteWorkspace remote) { - return new ClientWorkspace(session, remote, this); - } - - /** - * Creates and returns a - * {@link ClientNamespaceRegistry ClientClientNamespaceRegistry} instance. - * - * {@inheritDoc} - */ - public NamespaceRegistry getNamespaceRegistry( - RemoteNamespaceRegistry remote) { - return new ClientNamespaceRegistry(remote, this); - } - - /** - * Creates and returns a - * {@link ClientNodeTypeManager ClienNodeTypeManager} instance. - * - * {@inheritDoc} - */ - public NodeTypeManager getNodeTypeManager(RemoteNodeTypeManager remote) { - return new ClientNodeTypeManager(remote, this); - } - - /** - * Creates and returns a {@link ClientItem ClientItem} instance. - * - * {@inheritDoc} - */ - public Item getItem(Session session, RemoteItem remote) { - return new ClientItem(session, remote, this); - } - - /** - * Creates and returns a {@link ClientProperty ClientProperty} instance. - * - * {@inheritDoc} - */ - public Property getProperty(Session session, RemoteProperty remote) { - return new ClientProperty(session, remote, this); - } - - /** - * Creates and returns a {@link ClientNode ClientNode} instance. - * - * {@inheritDoc} - */ - public Node getNode(Session session, RemoteNode remote) { - return new ClientNode(session, remote, this); - } - - /** - * Creates and returns a {@link ClientVersion ClientVersion} instance. - * - * {@inheritDoc} - */ - public Version getVersion(Session session, RemoteVersion remote) { - return new ClientVersion(session, remote, this); - } - - /** - * Creates and returns a {@link ClientVersionHistory ClientVersionHistory} - * instance. - * - * {@inheritDoc} - */ - public VersionHistory getVersionHistory(Session session, RemoteVersionHistory remote) { - return new ClientVersionHistory(session, remote, this); - } - - /** - * Creates and returns a {@link ClientNodeType ClientNodeType} instance. - * - * {@inheritDoc} - */ - public NodeType getNodeType(RemoteNodeType remote) { - return new ClientNodeType(remote, this); - } - - /** - * Creates and returns a {@link ClientItemDef ClientItemDef} instance. - * - * {@inheritDoc} - */ - public ItemDef getItemDef(RemoteItemDef remote) { - return new ClientItemDef(remote, this); - } - - /** - * Creates and returns a {@link ClientNodeDef ClientNodeDef} instance. - * - * {@inheritDoc} - */ - public NodeDef getNodeDef(RemoteNodeDef remote) { - return new ClientNodeDef(remote, this); - } - - /** - * Creates and returns a {@link ClientPropertyDef ClientPropertyDef} - * instance. - * - * {@inheritDoc} - */ - public PropertyDef getPropertyDef(RemotePropertyDef remote) { - return new ClientPropertyDef(remote, this); - } - - /** - * Creates and returns a {@link ClientLock ClientLock} instance. - * - * {@inheritDoc} - */ - public Lock getLock(Node node, RemoteLock remote) { - return new ClientLock(node, remote); - } - - /** - * Creates and returns a {@link ClientQueryManager ClientQueryManager} instance. - * - * {@inheritDoc} - */ - public QueryManager getQueryManager( - Session session, RemoteQueryManager remote) { - return new ClientQueryManager(session, remote, this); - } - - /** - * Creates and returns a {@link ClientQuery ClientQuery} instance. - * - * {@inheritDoc} - */ - public Query getQuery(Session session, RemoteQuery remote) { - return new ClientQuery(session, remote, this); - } - - /** - * Creates and returns a {@link ClientQueryResult ClientQueryResult} instance. - * - * {@inheritDoc} - */ - public QueryResult getQueryResult( - Session session, RemoteQueryResult remote) { - return new ClientQueryResult(session, remote, this); - } - - /** - * Creates and returns a {@link ClientRow ClientRow} instance. - * - * {@inheritDoc} - */ - public Row getRow(RemoteRow remote) { - return new ClientRow(remote); - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientItemDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientItemDef.java deleted file mode 100644 index afa90742eea..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientItemDef.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.nodetype.ItemDef; -import javax.jcr.nodetype.NodeType; - -import org.apache.jackrabbit.rmi.remote.RemoteItemDef; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteItemDef RemoteItemDef} - * inteface. This class makes a remote item definition locally available using - * the JCR {@link javax.jcr.nodetype.ItemDef ItemDef} interface. Used mainly - * as the base class for the - * {@link org.apache.jackrabbit.rmi.client.ClientPropertyDef ClientPropertyDef} - * and - * {@link org.apache.jackrabbit.rmi.client.ClientNodeDef ClientNodeDef} adapters. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.ItemDef - * @see org.apache.jackrabbit.rmi.remote.RemoteItemDef - */ -public class ClientItemDef extends ClientObject implements ItemDef { - - /** The adapted remote item definition. */ - private RemoteItemDef remote; - - /** - * Creates a local adapter for the given remote item definition. - * - * @param remote remote item definition - * @param factory local adapter factory - */ - public ClientItemDef(RemoteItemDef remote, LocalAdapterFactory factory) { - super(factory); - this.remote = remote; - } - - /** {@inheritDoc} */ - public NodeType getDeclaringNodeType() { - try { - return getFactory().getNodeType(remote.getDeclaringNodeType()); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String getName() { - try { - return remote.getName(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isAutoCreate() { - try { - return remote.isAutoCreate(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isMandatory() { - try { - return remote.isMandatory(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public int getOnParentVersion() { - try { - return remote.getOnParentVersion(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isProtected() { - try { - return remote.isProtected(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientLock.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientLock.java deleted file mode 100644 index 2401da66b04..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientLock.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.Node; -import javax.jcr.RepositoryException; -import javax.jcr.lock.Lock; - -import org.apache.jackrabbit.rmi.remote.RemoteLock; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteLock RemoteLock} - * inteface. This class makes a remote lock locally available using - * the JCR {@link javax.jcr.lock.Lock Lock} interface. - * - * @author Jukka Zitting - * @see javax.jcr.lock.Lock - * @see org.apache.jackrabbit.rmi.remote.RemoteLock - */ -public class ClientLock implements Lock { - - /** The current node. */ - private Node node; - - /** The adapted remote lock. */ - private RemoteLock remote; - - /** - * Creates a local adapter for the given remote lock. - * - * @param node current node - * @param remote remote lock - */ - public ClientLock(Node node, RemoteLock remote) { - this.node = node; - this.remote = remote; - } - - /** - * Returns the owning node without contacting the remote lock. - * - * {@inheritDoc} - */ - public Node getNode() { - return node; - } - - /** {@inheritDoc} */ - public String getLockOwner() { - try { - return remote.getLockOwner(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isDeep() { - try { - return remote.isDeep(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String getLockToken() { - try { - return remote.getLockToken(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isLive() { - try { - return remote.isLive(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public void refresh() throws RepositoryException { - try { - remote.refresh(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNode.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNode.java deleted file mode 100644 index 46652d961ce..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNode.java +++ /dev/null @@ -1,595 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.io.InputStream; -import java.rmi.RemoteException; -import java.util.Calendar; - -import javax.jcr.BinaryValue; -import javax.jcr.BooleanValue; -import javax.jcr.DateValue; -import javax.jcr.DoubleValue; -import javax.jcr.Item; -import javax.jcr.ItemVisitor; -import javax.jcr.LongValue; -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Property; -import javax.jcr.PropertyIterator; -import javax.jcr.ReferenceValue; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.StringValue; -import javax.jcr.Value; -import javax.jcr.lock.Lock; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; - -import org.apache.jackrabbit.rmi.remote.RemoteLock; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.SerialValue; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteNode RemoteNode} - * inteface. This class makes a remote node locally available using - * the JCR {@link javax.jcr.Node Node} interface. - * - * @author Jukka Zitting - * @see javax.jcr.Node - * @see org.apache.jackrabbit.rmi.remote.RemoteNode - */ -public class ClientNode extends ClientItem implements Node { - - /** The adapted remote node. */ - private RemoteNode remote; - - /** - * Creates a local adapter for the given remote node. - * - * @param session current session - * @param remote remote node - * @param factory local adapter factory - */ - public ClientNode( - Session session, RemoteNode remote, LocalAdapterFactory factory) { - super(session, remote, factory); - this.remote = remote; - } - - /** - * Returns true without contacting the remote node. - * - * {@inheritDoc} - */ - public boolean isNode() { - return true; - } - - /** - * Calls the {@link ItemVisitor#visit(Node) ItemVisitor.visit(Node)} - * method of the given visitor. Does not contact the remote node, but - * the visitor may invoke other methods that do contact the remote node. - * - * {@inheritDoc} - */ - public void accept(ItemVisitor visitor) throws RepositoryException { - visitor.visit(this); - } - - /** {@inheritDoc} */ - public Node addNode(String path) throws RepositoryException { - try { - return getNode(getSession(), remote.addNode(path)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Node addNode(String path, String type) throws RepositoryException { - try { - RemoteNode node = remote.addNode(path, type); - return getNode(getSession(), node); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void orderBefore(String src, String dst) throws RepositoryException { - try { - remote.orderBefore(src, dst); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Property setProperty(String name, Value value) - throws RepositoryException { - try { - RemoteProperty property = - remote.setProperty(name, new SerialValue(value)); - return getFactory().getProperty(getSession(), property); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Property setProperty(String name, Value[] values) - throws RepositoryException { - try { - Value[] serials = SerialValue.makeSerialValueArray(values); - RemoteProperty property = remote.setProperty(name, serials); - return getFactory().getProperty(getSession(), property); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Property setProperty(String name, String[] strings) - throws RepositoryException { - Value[] values = new Value[strings.length]; - for (int i = 0; i < strings.length; i++) { - values[i] = new StringValue(strings[i]); - } - return setProperty(name, values); - } - - /** {@inheritDoc} */ - public Property setProperty(String name, String value) - throws RepositoryException { - return setProperty(name, new StringValue(value)); - } - - /** {@inheritDoc} */ - public Property setProperty(String name, InputStream value) - throws RepositoryException { - return setProperty(name, new BinaryValue(value)); - } - - /** {@inheritDoc} */ - public Property setProperty(String name, boolean value) - throws RepositoryException { - return setProperty(name, new BooleanValue(value)); - } - - /** {@inheritDoc} */ - public Property setProperty(String name, double value) - throws RepositoryException { - return setProperty(name, new DoubleValue(value)); - } - - /** {@inheritDoc} */ - public Property setProperty(String name, long value) - throws RepositoryException { - return setProperty(name, new LongValue(value)); - } - - /** {@inheritDoc} */ - public Property setProperty(String name, Calendar value) - throws RepositoryException { - return setProperty(name, new DateValue(value)); - } - - /** {@inheritDoc} */ - public Property setProperty(String name, Node value) - throws RepositoryException { - return setProperty(name, new ReferenceValue(value)); - } - - /** {@inheritDoc} */ - public Node getNode(String path) throws RepositoryException { - try { - return getNode(getSession(), remote.getNode(path)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeIterator getNodes() throws RepositoryException { - try { - return getNodeIterator(getSession(), remote.getNodes()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeIterator getNodes(String pattern) throws RepositoryException { - try { - return getNodeIterator(getSession(), remote.getNodes(pattern)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Property getProperty(String path) throws RepositoryException { - try { - RemoteProperty property = remote.getProperty(path); - return getFactory().getProperty(getSession(), property); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public PropertyIterator getProperties() throws RepositoryException { - try { - return getPropertyIterator(getSession(), remote.getProperties()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public PropertyIterator getProperties(String pattern) - throws RepositoryException { - try { - RemoteProperty[] properties = remote.getProperties(pattern); - return getPropertyIterator(getSession(), properties); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Item getPrimaryItem() throws RepositoryException { - try { - return getItem(getSession(), remote.getPrimaryItem()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String getUUID() throws RepositoryException { - try { - return remote.getUUID(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public PropertyIterator getReferences() throws RepositoryException { - try { - return getPropertyIterator(getSession(), remote.getReferences()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasNode(String path) throws RepositoryException { - try { - return remote.hasNode(path); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasProperty(String path) throws RepositoryException { - try { - return remote.hasProperty(path); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasNodes() throws RepositoryException { - try { - return remote.hasNodes(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasProperties() throws RepositoryException { - try { - return remote.hasProperties(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeType getPrimaryNodeType() throws RepositoryException { - try { - return getFactory().getNodeType(remote.getPrimaryNodeType()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeType[] getMixinNodeTypes() throws RepositoryException { - try { - return getNodeTypeArray(remote.getMixinNodeTypes()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isNodeType(String type) throws RepositoryException { - try { - return remote.isNodeType(type); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void addMixin(String name) throws RepositoryException { - try { - remote.addMixin(name); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void removeMixin(String name) throws RepositoryException { - try { - remote.removeMixin(name); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean canAddMixin(String name) throws RepositoryException { - try { - return remote.canAddMixin(name); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeDef getDefinition() throws RepositoryException { - try { - return getFactory().getNodeDef(remote.getDefinition()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Version checkin() throws RepositoryException { - try { - return getFactory().getVersion(getSession(), remote.checkin()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void checkout() throws RepositoryException { - try { - remote.checkout(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void update(String workspace) throws RepositoryException { - try { - remote.update(workspace); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void merge(String workspace, boolean bestEffort) - throws RepositoryException { - try { - remote.merge(workspace, bestEffort); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void cancelMerge(Version version) throws RepositoryException { - try { - remote.cancelMerge(version.getUUID()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void doneMerge(Version version) throws RepositoryException { - try { - remote.doneMerge(version.getUUID()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String getCorrespondingNodePath(String workspace) - throws RepositoryException { - try { - return remote.getCorrespondingNodePath(workspace); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public int getIndex() throws RepositoryException { - try { - return remote.getIndex(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restore(String version, boolean removeExisting) - throws RepositoryException { - try { - remote.restore(version, removeExisting); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restore(Version version, boolean removeExisting) - throws RepositoryException { - try { - remote.restoreByUUID(version.getUUID(), removeExisting); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restore(Version version, String path, boolean removeExisting) - throws RepositoryException { - try { - remote.restore(version.getUUID(), path, removeExisting); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restoreByLabel(String label, boolean removeExisting) - throws RepositoryException { - try { - remote.restoreByLabel(label, removeExisting); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Property setProperty(String name, String[] strings, int type) - throws RepositoryException { - Value[] values = new Value[strings.length]; - for (int i = 0; i < strings.length; i++) { - values[i] = new StringValue(strings[i]); - } - return setProperty(name, values, type); - } - - /** {@inheritDoc} */ - public Property setProperty(String name, Value[] values, int type) - throws RepositoryException { - try { - Value[] serials = SerialValue.makeSerialValueArray(values); - RemoteProperty property = remote.setProperty(name, serials, type); - return getFactory().getProperty(getSession(), property); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isCheckedOut() throws RepositoryException { - try { - return remote.isCheckedOut(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public VersionHistory getVersionHistory() throws RepositoryException { - try { - return getFactory().getVersionHistory(getSession(), remote.getVersionHistory()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Version getBaseVersion() throws RepositoryException { - try { - return getFactory().getVersion(getSession(), remote.getBaseVersion()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Lock lock(boolean isDeep, boolean isSessionScoped) - throws RepositoryException { - try { - RemoteLock lock = remote.lock(isDeep, isSessionScoped); - return getFactory().getLock(this, lock); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Lock getLock() throws RepositoryException { - try { - return getFactory().getLock(this, remote.getLock()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void unlock() throws RepositoryException { - try { - remote.unlock(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean holdsLock() throws RepositoryException { - try { - return remote.holdsLock(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isLocked() throws RepositoryException { - try { - return remote.isLocked(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeDef.java deleted file mode 100644 index c4aef585db3..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeDef.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; - -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeDef RemoteNodeDef} - * inteface. This class makes a remote node definition locally available using - * the JCR {@link javax.jcr.nodetype.NodeDef NodeDef} interface. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.NodeDef - * @see org.apache.jackrabbit.rmi.remote.RemoteNodeDef - */ -public class ClientNodeDef extends ClientItemDef implements NodeDef { - - /** The adapted remote node definition. */ - private RemoteNodeDef remote; - - /** - * Creates a local adapter for the given remote node definition. - * - * @param remote remote node definition - * @param factory local adapter factory - */ - public ClientNodeDef(RemoteNodeDef remote, LocalAdapterFactory factory) { - super(remote, factory); - this.remote = remote; - } - - /** {@inheritDoc} */ - public NodeType[] getRequiredPrimaryTypes() { - try { - return getNodeTypeArray(remote.getRequiredPrimaryTypes()); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public NodeType getDefaultPrimaryType() { - try { - return getFactory().getNodeType(remote.getDefaultPrimaryType()); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean allowSameNameSibs() { - try { - return remote.allowSameNameSibs(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeType.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeType.java deleted file mode 100644 index 7b8f07fb8c3..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeType.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.Value; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.PropertyDef; - -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.SerialValue; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} - * inteface. This class makes a remote node type locally available using - * the JCR {@link javax.jcr.nodetype.NodeType NodeType} interface. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.NodeType - * @see org.apache.jackrabbit.rmi.remote.RemoteNodeType - */ -public class ClientNodeType extends ClientObject implements NodeType { - - /** The adapted remote node type. */ - private RemoteNodeType remote; - - /** - * Creates a local adapter for the given remote node type. - * - * @param remote remote node type - * @param factory local adapter factory - */ - public ClientNodeType(RemoteNodeType remote, LocalAdapterFactory factory) { - super(factory); - this.remote = remote; - } - - /** {@inheritDoc} */ - public String getName() { - try { - return remote.getName(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isMixin() { - try { - return remote.isMixin(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasOrderableChildNodes() { - try { - return remote.hasOrderableChildNodes(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public NodeType[] getSupertypes() { - try { - return getNodeTypeArray(remote.getSupertypes()); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public NodeType[] getDeclaredSupertypes() { - try { - return getNodeTypeArray(remote.getDeclaredSupertypes()); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isNodeType(String type) { - try { - return remote.isNodeType(type); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public PropertyDef[] getPropertyDefs() { - try { - return getPropertyDefArray(remote.getPropertyDefs()); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public PropertyDef[] getDeclaredPropertyDefs() { - try { - return getPropertyDefArray(remote.getDeclaredPropertyDefs()); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public NodeDef[] getChildNodeDefs() { - try { - RemoteNodeDef[] defs = remote.getChildNodeDefs(); - return getNodeDefArray(defs); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public NodeDef[] getDeclaredChildNodeDefs() { - try { - RemoteNodeDef[] defs = remote.getDeclaredChildNodeDefs(); - return getNodeDefArray(defs); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean canSetProperty(String name, Value value) { - try { - return remote.canSetProperty(name, new SerialValue(value)); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean canSetProperty(String name, Value[] values) { - try { - Value[] serials = SerialValue.makeSerialValueArray(values); - return remote.canSetProperty(name, serials); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean canAddChildNode(String name) { - try { - return remote.canAddChildNode(name); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean canAddChildNode(String name, String type) { - try { - return remote.canAddChildNode(name, type); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean canRemoveItem(String name) { - try { - return remote.canRemoveItem(name); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String getPrimaryItemName() { - try { - return remote.getPrimaryItemName(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeTypeManager.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeTypeManager.java deleted file mode 100644 index 5604f4d442a..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNodeTypeManager.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeIterator; -import javax.jcr.nodetype.NodeTypeManager; - -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager RemoteNodeTypeManager} - * inteface. This class makes a remote node type manager locally available - * using the JCR {@link javax.jcr.nodetype.NodeTypeManager NodeTypeManager} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.NodeTypeManager - * @see org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager - */ -public class ClientNodeTypeManager extends ClientObject - implements NodeTypeManager { - - /** The adapted remote node type manager. */ - private RemoteNodeTypeManager remote; - - /** - * Creates a local adapter for the given remote node type manager. - * - * @param remote remote node type manager - * @param factory local adapter factory - */ - public ClientNodeTypeManager( - RemoteNodeTypeManager remote, LocalAdapterFactory factory) { - super(factory); - this.remote = remote; - } - - /** {@inheritDoc} */ - public NodeType getNodeType(String name) throws RepositoryException { - try { - return getFactory().getNodeType(remote.getNodeType(name)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeTypeIterator getAllNodeTypes() throws RepositoryException { - try { - return getNodeTypeIterator(remote.getAllNodeTypes()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeTypeIterator getPrimaryNodeTypes() throws RepositoryException { - try { - return getNodeTypeIterator(remote.getPrimaryNodeTypes()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeTypeIterator getMixinNodeTypes() throws RepositoryException { - try { - return getNodeTypeIterator(remote.getMixinNodeTypes()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientObject.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientObject.java deleted file mode 100644 index 1cbe0b0429a..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientObject.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import javax.jcr.Item; -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Property; -import javax.jcr.PropertyIterator; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeIterator; -import javax.jcr.nodetype.PropertyDef; -import javax.jcr.version.Version; -import javax.jcr.version.VersionIterator; - -import org.apache.jackrabbit.rmi.iterator.ArrayNodeIterator; -import org.apache.jackrabbit.rmi.iterator.ArrayNodeTypeIterator; -import org.apache.jackrabbit.rmi.iterator.ArrayPropertyIterator; -import org.apache.jackrabbit.rmi.iterator.ArrayVersionIterator; -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.RemoteVersion; -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; - -/** - * Base class for client adapter objects. The only purpose of - * this class is to centralize the handling of the - * local adapter factory used by the client adapters to - * instantiate new adapters. - * - * @author Jukka Zitting - */ -public class ClientObject { - - /** Local adapter factory. */ - private LocalAdapterFactory factory; - - /** - * Creates a basic client adapter that uses the given factory - * to create new adapters. - * - * @param factory local adapter factory - */ - protected ClientObject(LocalAdapterFactory factory) { - this.factory = factory; - } - - /** - * Returns the local adapter factory used to create new adapters. - * - * @return local adapter factory - */ - protected LocalAdapterFactory getFactory() { - return factory; - } - - /** - * Utility method to create a local adapter for a remote item. - * This method introspects the remote reference to determine - * whether to instantiate a {@link Property Property}, - * a {@link Node Node}, or an {@link Item Item} adapter using - * the local adapter factory. - *

- * If the remote item is a {@link RemoteNode}, this method delegates - * to {@link #getNode(Session, RemoteNode)}. - * - * @param session current session - * @param remote remote item - * @return local property, node, or item adapter - */ - protected Item getItem(Session session, RemoteItem remote) { - if (remote instanceof RemoteProperty) { - return factory.getProperty(session, (RemoteProperty) remote); - } else if (remote instanceof RemoteNode) { - return getNode(session, (RemoteNode) remote); - } else { - return factory.getItem(session, remote); - } - } - - /** - * Utility method to create a local adapter for a remote node. - * This method introspects the remote reference to determine - * whether to instantiate a {@link Node Node}, - * a {@link javax.jcr.version.VersionHistory VersionHistory}, or a - * {@link Version Version} adapter using - * the local adapter factory. - * - * @param session current session - * @param remote remote node - * @return local node, version, or version history adapter - */ - protected Node getNode(Session session, RemoteNode remote) { - if (remote instanceof RemoteVersion) { - return factory.getVersion(session, (RemoteVersion) remote); - } else if (remote instanceof RemoteVersionHistory) { - return factory.getVersionHistory(session, (RemoteVersionHistory) remote); - } else { - return factory.getNode(session, (RemoteNode) remote); - } - } - - /** - * Utility method for creating a property iterator for an array - * of remote properties. The properties in the returned iterator - * are created using the local adapter factory. - *

- * A null input is treated as an empty array. - * - * @param session current session - * @param remotes remote properties - * @return local property iterator - */ - protected PropertyIterator getPropertyIterator( - Session session, RemoteProperty[] remotes) { - if (remotes != null) { - Property[] properties = new Property[remotes.length]; - for (int i = 0; i < remotes.length; i++) { - properties[i] = factory.getProperty(session, remotes[i]); - } - return new ArrayPropertyIterator(properties); - } else { - return new ArrayPropertyIterator(new Property[0]); // for safety - } - } - - /** - * Utility method for creating a node iterator for an array - * of remote nodes. The nodes in the returned iterator - * are created using the local adapter factory. - *

- * A null input is treated as an empty array. - * - * @param session current session - * @param remotes remote nodes - * @return local node iterator - */ - protected NodeIterator getNodeIterator( - Session session, RemoteNode[] remotes) { - if (remotes != null) { - Node[] nodes = new Node[remotes.length]; - for (int i = 0; i < remotes.length; i++) { - nodes[i] = getNode(session, remotes[i]); - } - return new ArrayNodeIterator(nodes); - } else { - return new ArrayNodeIterator(new Node[0]); // for safety - } - } - - /** - * Utility method for creating a version array for an array - * of remote versions. The versions in the returned array - * are created using the local adapter factory. - *

- * A null input is treated as an empty array. - * - * @param session current session - * @param remotes remote versions - * @return local version array - */ - protected Version[] getVersionArray( - Session session, RemoteVersion[] remotes) { - if (remotes != null) { - Version[] versions = new Version[remotes.length]; - for (int i = 0; i < remotes.length; i++) { - versions[i] = factory.getVersion(session, remotes[i]); - } - return versions; - } else { - return new Version[0]; // for safety - } - } - - /** - * Utility method for creating a version iterator for an array - * of remote versions. The versions in the returned iterator - * are created using the local adapter factory. - *

- * A null input is treated as an empty array. - * - * @param session current session - * @param remotes remote versions - * @return local version iterator - */ - protected VersionIterator getVersionIterator( - Session session, RemoteVersion[] remotes) { - return new ArrayVersionIterator(getVersionArray(session, remotes)); - } - - /** - * Utility method for creating an array of local node type adapters - * for an array of remote node types. The node type adapters are created - * using the local adapter factory. - *

- * A null input is treated as an empty array. - * - * @param remotes remote node types - * @return local node type array - */ - protected NodeType[] getNodeTypeArray(RemoteNodeType[] remotes) { - if (remotes != null) { - NodeType[] types = new NodeType[remotes.length]; - for (int i = 0; i < remotes.length; i++) { - types[i] = factory.getNodeType(remotes[i]); - } - return types; - } else { - return new NodeType[0]; // for safety - } - } - - /** - * Utility method for creating an iterator of local node type adapters - * for an array of remote node types. The node type adapters are created - * using the local adapter factory. - *

- * A null input is treated as an empty array. - * - * @param remotes remote node types - * @return local node type iterator - */ - protected NodeTypeIterator getNodeTypeIterator(RemoteNodeType[] remotes) { - return new ArrayNodeTypeIterator(getNodeTypeArray(remotes)); - } - - /** - * Utility method for creating an array of local node definition - * adapters for an array of remote node definitions. The node - * definition adapters are created using the local adapter factory. - *

- * A null input is treated as an empty array. - * - * @param remotes remote node definitions - * @return local node definition array - */ - protected NodeDef[] getNodeDefArray(RemoteNodeDef[] remotes) { - if (remotes != null) { - NodeDef[] defs = new NodeDef[remotes.length]; - for (int i = 0; i < remotes.length; i++) { - defs[i] = factory.getNodeDef(remotes[i]); - } - return defs; - } else { - return new NodeDef[0]; // for safety - } - } - - /** - * Utility method for creating an array of local property definition - * adapters for an array of remote property definitions. The property - * definition adapters are created using the local adapter factory. - *

- * A null input is treated as an empty array. - * - * @param remotes remote property definitions - * @return local property definition array - */ - protected PropertyDef[] getPropertyDefArray(RemotePropertyDef[] remotes) { - if (remotes != null) { - PropertyDef[] defs = new PropertyDef[remotes.length]; - for (int i = 0; i < remotes.length; i++) { - defs[i] = factory.getPropertyDef(remotes[i]); - } - return defs; - } else { - return new PropertyDef[0]; // for safety - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientProperty.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientProperty.java deleted file mode 100644 index 127d97351cc..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientProperty.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.io.InputStream; -import java.rmi.RemoteException; -import java.util.Calendar; - -import javax.jcr.BinaryValue; -import javax.jcr.BooleanValue; -import javax.jcr.DateValue; -import javax.jcr.DoubleValue; -import javax.jcr.ItemVisitor; -import javax.jcr.LongValue; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.ReferenceValue; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.StringValue; -import javax.jcr.Value; -import javax.jcr.nodetype.PropertyDef; - -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.SerialValue; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteProperty RemoteProperty} - * inteface. This class makes a remote property locally available using - * the JCR {@link javax.jcr.Property Property} interface. - * - * @author Jukka Zitting - * @see javax.jcr.Property - * @see org.apache.jackrabbit.rmi.remote.RemoteProperty - */ -public class ClientProperty extends ClientItem implements Property { - - /** The adapted remote property. */ - private RemoteProperty remote; - - /** - * Creates a local adapter for the given remote property. - * - * @param session current session - * @param remote remote property - * @param factory local adapter factory - */ - public ClientProperty( - Session session, RemoteProperty remote, - LocalAdapterFactory factory) { - super(session, remote, factory); - this.remote = remote; - } - - /** - * Calls the {@link ItemVisitor#visit(Property) ItemVisitor.visit(Property} - * method of the given visitor. Does not contact the remote property, but - * the visitor may invoke other methods that do contact the remote property. - * - * {@inheritDoc} - */ - public void accept(ItemVisitor visitor) throws RepositoryException { - visitor.visit(this); - } - - /** - * Returns the boolean value of this property. Implemented as - * getValue().getBoolean(). - * - * {@inheritDoc} - */ - public boolean getBoolean() throws RepositoryException { - return getValue().getBoolean(); - } - - /** - * Returns the date value of this property. Implemented as - * getValue().getDate(). - * - * {@inheritDoc} - */ - public Calendar getDate() throws RepositoryException { - return getValue().getDate(); - } - - /** - * Returns the double value of this property. Implemented as - * getValue().getDouble(). - * - * {@inheritDoc} - */ - public double getDouble() throws RepositoryException { - return getValue().getDouble(); - } - - /** - * Returns the long value of this property. Implemented as - * getValue().getLong(). - * - * {@inheritDoc} - */ - public long getLong() throws RepositoryException { - return getValue().getLong(); - } - - /** - * Returns the binary value of this property. Implemented as - * getValue().getStream(). - * - * {@inheritDoc} - */ - public InputStream getStream() throws RepositoryException { - return getValue().getStream(); - } - - /** - * Returns the string value of this property. Implemented as - * getValue().getString(). - * - * {@inheritDoc} - */ - public String getString() throws RepositoryException { - return getValue().getString(); - } - - /** {@inheritDoc} */ - public Value getValue() throws RepositoryException { - try { - return remote.getValue(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Value[] getValues() throws RepositoryException { - try { - return remote.getValues(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** - * Sets the boolean value of this property. Implemented as - * setValue(new BooleanValue(value)). - * - * {@inheritDoc} - */ - public void setValue(boolean value) throws RepositoryException { - setValue(new BooleanValue(value)); - } - - /** - * Sets the date value of this property. Implemented as - * setValue(new DateValue(value)). - * - * {@inheritDoc} - */ - public void setValue(Calendar value) throws RepositoryException { - setValue(new DateValue(value)); - } - - /** - * Sets the double value of this property. Implemented as - * setValue(new DoubleValue(value)). - * - * {@inheritDoc} - */ - public void setValue(double value) throws RepositoryException { - setValue(new DoubleValue(value)); - } - - /** - * Sets the binary value of this property. Implemented as - * setValue(new BinaryValue(value)). - * - * {@inheritDoc} - */ - public void setValue(InputStream value) throws RepositoryException { - setValue(new BinaryValue(value)); - } - - /** - * Sets the long value of this property. Implemented as - * setValue(new LongValue(value)). - * - * {@inheritDoc} - */ - public void setValue(long value) throws RepositoryException { - setValue(new LongValue(value)); - } - - /** - * Sets the reference value of this property. Implemented as - * setValue(new ReferenceValue(value)). - * - * {@inheritDoc} - */ - public void setValue(Node value) throws RepositoryException { - setValue(new ReferenceValue(value)); - } - - /** - * Sets the string value of this property. Implemented as - * setValue(new StringValue(value)). - * - * {@inheritDoc} - */ - public void setValue(String value) throws RepositoryException { - setValue(new StringValue(value)); - } - - /** - * Sets the string values of this property. Implemented as - * setValue(new Value[] { new StringValue(strings[0]), ... }). - * - * {@inheritDoc} - */ - public void setValue(String[] strings) throws RepositoryException { - Value[] values = new Value[strings.length]; - for (int i = 0; i < strings.length; i++) { - values[i] = new StringValue(strings[i]); - } - setValue(values); - } - - /** {@inheritDoc} */ - public void setValue(Value value) throws RepositoryException { - try { - remote.setValue(new SerialValue(value)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void setValue(Value[] values) throws RepositoryException { - try { - remote.setValue(SerialValue.makeSerialValueArray(values)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** - * Returns the reference value of this property. Implemented by - * converting the reference value to an UUID string and using the - * current session to look up the referenced node. - * - * {@inheritDoc} - */ - public Node getNode() throws RepositoryException { - return getSession().getNodeByUUID(getString()); - } - - /** {@inheritDoc} */ - public long getLength() throws RepositoryException { - try { - return remote.getLength(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public long[] getLengths() throws RepositoryException { - try { - return remote.getLengths(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public PropertyDef getDefinition() throws RepositoryException { - try { - return getFactory().getPropertyDef(remote.getDefinition()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public int getType() throws RepositoryException { - try { - return remote.getType(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientPropertyDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientPropertyDef.java deleted file mode 100644 index 1af1b91477a..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientPropertyDef.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.Value; -import javax.jcr.nodetype.PropertyDef; - -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemotePropertyDef RemotePropertyDef} - * inteface. This class makes a remote property definition locally available - * using the JCR {@link javax.jcr.nodetype.PropertyDef PropertyDef} interface. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.PropertyDef - * @see org.apache.jackrabbit.rmi.remote.RemotePropertyDef - */ -public class ClientPropertyDef extends ClientItemDef implements PropertyDef { - - /** The adapted remote property. */ - private RemotePropertyDef remote; - - /** - * Creates a local adapter for the given remote property definition. - * - * @param remote remote property definition - * @param factory local adapter factory - */ - public ClientPropertyDef( - RemotePropertyDef remote, LocalAdapterFactory factory) { - super(remote, factory); - this.remote = remote; - } - - /** {@inheritDoc} */ - public int getRequiredType() { - try { - return remote.getRequiredType(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getValueConstraints() { - try { - return remote.getValueConstraints(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public Value[] getDefaultValues() { - try { - return remote.getDefaultValues(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isMultiple() { - try { - return remote.isMultiple(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQuery.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQuery.java deleted file mode 100644 index d698f85706f..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQuery.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.query.Query; -import javax.jcr.query.QueryResult; - -import org.apache.jackrabbit.rmi.remote.RemoteQuery; - -/** - * Local adapter for the JCR-RMI - * {@link RemoteQuery RemoteQuery} - * inteface. This class makes a remote query locally available using - * the JCR {@link Query Query} interface. - * - * @author Philipp Koch - * @see javax.jcr.query.Query Query - * @see org.apache.jackrabbit.rmi.remote.RemoteQuery - */ -public class ClientQuery extends ClientObject implements Query { - - /** The current session */ - private Session session; - - /** The adapted remote query manager. */ - private RemoteQuery remote; - - /** - * Creates a client adapter for the given query. - * - * @param session current session - * @param remote remote query - * @param factory adapter factory - */ - public ClientQuery( - Session session, RemoteQuery remote, LocalAdapterFactory factory) { - super(factory); - this.session = session; - this.remote = remote; - } - - /** {@inheritDoc} */ - public QueryResult execute() throws RepositoryException { - try { - return getFactory().getQueryResult(session, remote.execute()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String getStatement() { - try { - return remote.getStatement(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String getLanguage() { - try { - return remote.getLanguage(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String getPersistentQueryPath() throws RepositoryException { - try { - return remote.getPersistentQueryPath(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void save(String absPath) throws RepositoryException { - try { - remote.save(absPath); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQueryResult.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQueryResult.java deleted file mode 100644 index f9b3f618fbf..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQueryResult.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.query.QueryResult; -import javax.jcr.query.Row; -import javax.jcr.query.RowIterator; - -import org.apache.jackrabbit.rmi.iterator.ArrayNodeIterator; -import org.apache.jackrabbit.rmi.iterator.ArrayRowIterator; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; -import org.apache.jackrabbit.rmi.remote.RemoteRow; - -/** - * Local adapter for the JCR-RMI - * {@link RemoteQueryResult RemoteQueryResult} - * inteface. This class makes a remote query result locally available using - * the JCR {@link QueryResult QueryResult} interface. - * - * @author Philipp Koch - * @see javax.jcr.query.QueryResult QueryResult - * @see org.apache.jackrabbit.rmi.remote.RemoteQueryResult - */ -public class ClientQueryResult extends ClientObject implements QueryResult { - - /** The current session */ - private Session session; - - /** The adapted remote query result. */ - private RemoteQueryResult remote; - - /** - * Creates a client adapter for the given remote query result. - * - * @param session current session - * @param remote remote query result - * @param factory adapter factory - */ - public ClientQueryResult( - Session session, RemoteQueryResult remote, - LocalAdapterFactory factory) { - super(factory); - this.session = session; - this.remote = remote; - } - - /** {@inheritDoc} */ - public String[] getPropertyNames() throws RepositoryException { - try { - return remote.getPropertyNames(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RowIterator getRows() throws RepositoryException { - try { - RemoteRow[] remotes = remote.getRows(); - if (remotes != null) { - Row[] rows = new Row[remotes.length]; - for (int i = 0; i < rows.length; i++) { - rows[i] = getFactory().getRow(remotes[i]); - } - return new ArrayRowIterator(rows); - } else { - return new ArrayRowIterator(new Row[0]); - } - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeIterator getNodes() throws RepositoryException { - try { - RemoteNode[] remotes = remote.getNodes(); - if (remotes != null) { - Node[] nodes = new Node[remotes.length]; - for (int i = 0; i < nodes.length; i++) { - nodes[i] = getNode(session, remotes[i]); - } - return new ArrayNodeIterator(nodes); - } else { - return new ArrayNodeIterator(new Node[0]); - } - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRepository.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRepository.java deleted file mode 100644 index 1ce012cb145..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRepository.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.Credentials; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; - -import org.apache.jackrabbit.rmi.remote.RemoteRepository; -import org.apache.jackrabbit.rmi.remote.RemoteSession; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteRepository RemoteRepository} - * inteface. This class makes a remote repository locally available using - * the JCR {@link javax.jcr.Repository Repository} interface. - * - * @author Jukka Zitting - * @see javax.jcr.Repository - * @see org.apache.jackrabbit.rmi.remote.RemoteRepository - */ -public class ClientRepository extends ClientObject implements Repository { - - /** The adapted remote repository. */ - private RemoteRepository remote; - - /** - * Creates a client adapter for the given remote repository. - * - * @param remote remote repository - * @param factory local adapter factory - */ - public ClientRepository( - RemoteRepository remote, LocalAdapterFactory factory) { - super(factory); - this.remote = remote; - } - - /** {@inheritDoc} */ - public String getDescriptor(String name) { - try { - return remote.getDescriptor(name); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getDescriptorKeys() { - try { - return remote.getDescriptorKeys(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public Session login() throws RepositoryException { - try { - RemoteSession session = remote.login(); - return getFactory().getSession(this, session); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Session login(String workspace) throws RepositoryException { - try { - RemoteSession session = remote.login(workspace); - return getFactory().getSession(this, session); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Session login(Credentials credentials) throws RepositoryException { - try { - RemoteSession session = remote.login(credentials); - return getFactory().getSession(this, session); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Session login(Credentials credentials, String workspace) - throws RepositoryException { - try { - RemoteSession session = remote.login(credentials, workspace); - return getFactory().getSession(this, session); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRepositoryFactory.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRepositoryFactory.java deleted file mode 100644 index 6b920e48a46..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRepositoryFactory.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.net.MalformedURLException; -import java.rmi.Naming; -import java.rmi.NotBoundException; -import java.rmi.RemoteException; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Map; - -import javax.jcr.Repository; -import javax.naming.Context; -import javax.naming.Name; -import javax.naming.NamingException; -import javax.naming.RefAddr; -import javax.naming.Reference; -import javax.naming.spi.ObjectFactory; - -import org.apache.jackrabbit.rmi.remote.RemoteRepository; - -/** - * Object factory for JCR-RMI clients. This factory can be used either - * directly or as a JNDI object factory. - * - * @author Jukka Zitting - * @see ClientRepository - */ -public class ClientRepositoryFactory implements ObjectFactory { - - /** - * The JNDI parameter name for configuring the RMI URL of - * a remote repository. - */ - public static final String URL_PARAMETER = "url"; - - /** - * Cache for repository references. - */ - private Map repositories; - - /** - * Local adapter factory. - */ - private LocalAdapterFactory factory; - - /** - * Creates a JCR-RMI client factory with the default adapter factory. - */ - public ClientRepositoryFactory() { - this(new ClientAdapterFactory()); - } - - /** - * Creates a JCR-RMI client factory with the given adapter factory. - * - * @param factory local adapter factory - */ - public ClientRepositoryFactory(LocalAdapterFactory factory) { - this.repositories = new HashMap(); - this.factory = factory; - } - - /** - * Returns a client wrapper for a remote content repository. The remote - * repository is looked up from the RMI registry using the given URL and - * wrapped into a {@link ClientRepository ClientRepository} adapter. - *

- * The repository references are cached so that only one client instance - * (per factory) exists for each remote repository. - * - * @param url the RMI URL of the remote repository - * @return repository client - * @throws ClassCastException if the URL points to an unknown object - * @throws MalformedURLException if the URL is malformed - * @throws NotBoundException if the URL points to nowhere - * @throws RemoteException on RMI errors - */ - public synchronized Repository getRepository(String url) throws - ClassCastException, MalformedURLException, - NotBoundException, RemoteException { - Repository repository = (Repository) repositories.get(url); - if (repository == null) { - RemoteRepository remote = (RemoteRepository) Naming.lookup(url); - repository = factory.getRepository(remote); - repositories.put(url, repository); - } - return repository; - } - - /** - * Utility method for looking up the URL within the given RefAddr object. - * Feeds the content of the RefAddr object to - * {@link #getRepository(String) getRepository(String)} and wraps all - * errors to {@link NamingException NamingExceptions}. - *

- * Used by {@link #getObjectInstance(Object, Name, Context, Hashtable) getObjectInstance()}. - * - * @param url the URL reference - * @return repository client - * @throws NamingException on all errors - */ - private Repository getRepository(RefAddr url) throws NamingException { - try { - return getRepository((String) url.getContent()); - } catch (Exception ex) { - throw new NamingException(ex.getMessage()); - } - } - - /** - * JNDI factory method for creating JCR-RMI clients. Looks up a - * remote repository using the reference parameter "url" as the RMI URL - * and returns a client wrapper for the remote repository. - * - * @param object reference parameters - * @param name unused - * @param context unused - * @param environment unused - * @return repository client - * @throws NamingException on all errors - */ - public Object getObjectInstance( - Object object, Name name, Context context, Hashtable environment) - throws NamingException { - if (object instanceof Reference) { - Reference reference = (Reference) object; - if (Repository.class.getName().equals(reference.getClassName())) { - RefAddr url = reference.get(URL_PARAMETER); - if (url != null) { - return getRepository(url); - } - } - } - return null; - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRow.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRow.java deleted file mode 100644 index 212e395f7a0..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientRow.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.Value; -import javax.jcr.query.Row; - -import org.apache.jackrabbit.rmi.remote.RemoteRow; - -/** - * Local adapter for the JCR-RMI {@link RemoteRow RemoteRow} - * inteface. This class makes a remote query row locally available using - * the JCR {@link Row Row} interface. - * - * @author Philipp Koch - * @see javax.jcr.query.Row Row - * @see org.apache.jackrabbit.rmi.remote.RemoteRow - */ -public class ClientRow implements Row { - - /** The remote query row. */ - private RemoteRow remote; - - /** - * Creates a client adapter for the given remote query row. - * - * @param remote remote query row - */ - public ClientRow(RemoteRow remote) { - this.remote = remote; - } - - /** {@inheritDoc} */ - public Value[] getValues() throws RepositoryException { - try { - return remote.getValues(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Value getValue(String s) throws RepositoryException { - try { - return remote.getValue(s); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientSession.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientSession.java deleted file mode 100644 index c8ce8258ee7..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientSession.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.rmi.RemoteException; -import java.security.AccessControlException; - -import javax.jcr.Credentials; -import javax.jcr.Item; -import javax.jcr.Node; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.Workspace; -import javax.xml.transform.Result; -import javax.xml.transform.Source; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.sax.SAXResult; -import javax.xml.transform.stream.StreamSource; - -import org.apache.jackrabbit.rmi.remote.RemoteSession; -import org.apache.jackrabbit.rmi.xml.SessionImportContentHandler; -import org.xml.sax.ContentHandler; -import org.xml.sax.SAXException; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteSession RemoteSession} - * inteface. This class makes a remote session locally available using - * the JCR {@link javax.jcr.Session Session} interface. - * - * @author Jukka Zitting - * @see javax.jcr.Session - * @see org.apache.jackrabbit.rmi.remote.RemoteSession - */ -public class ClientSession extends ClientObject implements Session { - - /** The current repository. */ - private Repository repository; - - /** The adapted remote session. */ - private RemoteSession remote; - - /** - * Creates a client adapter for the given remote session. - * - * @param repository current repository - * @param remote remote repository - * @param factory local adapter factory - */ - public ClientSession(Repository repository, RemoteSession remote, - LocalAdapterFactory factory) { - super(factory); - this.repository = repository; - this.remote = remote; - } - - /** - * Returns the current repository without contacting the remote session. - * - * {@inheritDoc} - */ - public Repository getRepository() { - return repository; - } - - /** {@inheritDoc} */ - public String getUserId() { - try { - return remote.getUserId(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public Object getAttribute(String name) { - try { - return remote.getAttribute(name); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getAttributeNames() { - try { - return remote.getAttributeNames(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public Workspace getWorkspace() { - try { - return getFactory().getWorkspace(this, remote.getWorkspace()); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public Session impersonate(Credentials credentials) - throws RepositoryException { - try { - RemoteSession session = remote.impersonate(credentials); - return getFactory().getSession(repository, session); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Node getRootNode() throws RepositoryException { - try { - return getNode(this, remote.getRootNode()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Node getNodeByUUID(String uuid) throws RepositoryException { - try { - return getNode(this, remote.getNodeByUUID(uuid)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Item getItem(String path) throws RepositoryException { - try { - return getItem(this, remote.getItem(path)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean itemExists(String path) { - try { - return remote.itemExists(path); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public void move(String from, String to) throws RepositoryException { - try { - remote.move(from, to); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void save() throws RepositoryException { - try { - remote.save(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void refresh(boolean keepChanges) throws RepositoryException { - try { - remote.refresh(keepChanges); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasPendingChanges() throws RepositoryException { - try { - return remote.hasPendingChanges(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void checkPermission(String path, String actions) - throws AccessControlException { - try { - remote.checkPermission(path, actions); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public void importXML(String path, InputStream xml) - throws IOException, RepositoryException { - try { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - byte[] bytes = new byte[4096]; - for (int n = xml.read(bytes); n != -1; n = xml.read(bytes)) { - buffer.write(bytes, 0, n); - } - remote.importXML(path, buffer.toByteArray()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public ContentHandler getImportContentHandler(String path) - throws RepositoryException { - return new SessionImportContentHandler(this, path); - } - - /** {@inheritDoc} */ - public void setNamespacePrefix(String prefix, String uri) - throws RepositoryException { - try { - remote.setNamespacePrefix(prefix, uri); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getNamespacePrefixes() throws RepositoryException { - try { - return remote.getNamespacePrefixes(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String getNamespaceURI(String prefix) throws RepositoryException { - try { - return remote.getNamespaceURI(prefix); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String getNamespacePrefix(String uri) throws RepositoryException { - try { - return remote.getNamespacePrefix(uri); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void logout() { - try { - remote.logout(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public void addLockToken(String name) { - try { - remote.addLockToken(name); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getLockTokens() { - try { - return remote.getLockTokens(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public void removeLockToken(String name) { - try { - remote.removeLockToken(name); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** - * Exports the XML system view of the specified repository location - * to the given XML content handler. This method first requests the - * raw XML data from the remote session, and then uses an identity - * transformation to feed the data to the given XML content handler. - * Possible IO and transformer exceptions are thrown as SAXExceptions. - * - * {@inheritDoc} - */ - public void exportSysView( - String path, ContentHandler handler, - boolean binaryAsLink, boolean noRecurse) - throws SAXException, RepositoryException { - try { - byte[] xml = remote.exportSysView(path, binaryAsLink, noRecurse); - - Source source = new StreamSource(new ByteArrayInputStream(xml)); - Result result = new SAXResult(handler); - - TransformerFactory factory = TransformerFactory.newInstance(); - Transformer transformer = factory.newTransformer(); - transformer.transform(source, result); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } catch (IOException ex) { - throw new SAXException(ex); - } catch (TransformerConfigurationException ex) { - throw new SAXException(ex); - } catch (TransformerException ex) { - throw new SAXException(ex); - } - } - - /** - * Exports the XML system view of the specified repository location - * to the given output stream. This method first requests the - * raw XML data from the remote session, and then writes the data to - * the output stream. - * - * {@inheritDoc} - */ - public void exportSysView( - String path, OutputStream output, - boolean binaryAsLink, boolean noRecurse) - throws IOException, RepositoryException { - try { - byte[] xml = remote.exportSysView(path, binaryAsLink, noRecurse); - output.write(xml); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** - * Exports the XML document view of the specified repository location - * to the given XML content handler. This method first requests the - * raw XML data from the remote session, and then uses an identity - * transformation to feed the data to the given XML content handler. - * Possible IO and transformer exceptions are thrown as SAXExceptions. - * - * {@inheritDoc} - */ - public void exportDocView( - String path, ContentHandler handler, - boolean binaryAsLink, boolean noRecurse) - throws SAXException, RepositoryException { - try { - byte[] xml = remote.exportDocView(path, binaryAsLink, noRecurse); - - Source source = new StreamSource(new ByteArrayInputStream(xml)); - Result result = new SAXResult(handler); - - TransformerFactory factory = TransformerFactory.newInstance(); - Transformer transformer = factory.newTransformer(); - transformer.transform(source, result); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } catch (IOException ex) { - throw new SAXException(ex); - } catch (TransformerConfigurationException ex) { - throw new SAXException(ex); - } catch (TransformerException ex) { - throw new SAXException(ex); - } - } - - /** - * Exports the XML document view of the specified repository location - * to the given output stream. This method first requests the - * raw XML data from the remote session, and then writes the data to - * the output stream. - * - * {@inheritDoc} - */ - public void exportDocView( - String path, OutputStream output, - boolean binaryAsLink, boolean noRecurse) - throws IOException, RepositoryException { - try { - byte[] xml = remote.exportDocView(path, binaryAsLink, noRecurse); - output.write(xml); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientVersion.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientVersion.java deleted file mode 100644 index b5bed4e9254..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientVersion.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; -import java.util.Calendar; - -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.version.Version; - -import org.apache.jackrabbit.rmi.remote.RemoteVersion; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteVersion RemoteVersion} - * interface. This class makes a remote version locally available using - * the JCR {@link javax.jcr.version.Version Version} interface. - * - * @author Felix Meschberger - * @see javax.jcr.version.Version - * @see org.apache.jackrabbit.rmi.remote.RemoteVersion - */ -public class ClientVersion extends ClientNode implements Version { - - /** The adapted remote version. */ - private RemoteVersion remote; - - /** - * Creates a local adapter for the given remote version. - * - * @param session current session - * @param remote remote version - * @param factory local adapter factory - */ - public ClientVersion(Session session, RemoteVersion remote, - LocalAdapterFactory factory) { - super(session, remote, factory); - this.remote = remote; - } - - /** {@inheritDoc} */ - public Calendar getCreated() throws RepositoryException { - try { - return remote.getCreated(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Version[] getSuccessors() throws RepositoryException { - try { - return getVersionArray(getSession(), remote.getSuccessors()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Version[] getPredecessors() throws RepositoryException { - try { - return getVersionArray(getSession(), remote.getPredecessors()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } -} \ No newline at end of file diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientVersionHistory.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientVersionHistory.java deleted file mode 100644 index dea6927ee0a..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientVersionHistory.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.UnsupportedRepositoryOperationException; -import javax.jcr.version.Version; -import javax.jcr.version.VersionException; -import javax.jcr.version.VersionHistory; -import javax.jcr.version.VersionIterator; - -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; - -/** - * Local adapter for the JCR-RMI - * {@link org.apache.jackrabbit.rmi.remote.RemoteVersionHistory RemoteVersionHistory} - * interface. This class makes a remote version history locally available using - * the JCR {@link javax.jcr.version.VersionHistory VersionHistory} interface. - * - * @author Felix Meschberger - * @see javax.jcr.version.VersionHistory - * @see org.apache.jackrabbit.rmi.remote.RemoteVersionHistory - */ -public class ClientVersionHistory extends ClientNode implements VersionHistory { - - /** The adapted remote version history. */ - private RemoteVersionHistory remote; - - /** - * Creates a local adapter for the given remote version history. - * - * @param session current session - * @param remote remote version history - * @param factory local adapter factory - */ - public ClientVersionHistory(Session session, RemoteVersionHistory remote, - LocalAdapterFactory factory) { - super(session, remote, factory); - this.remote = remote; - } - - /** {@inheritDoc} */ - public Version getRootVersion() throws RepositoryException { - try { - return getFactory().getVersion(getSession(), remote.getRootVersion()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public VersionIterator getAllVersions() throws RepositoryException { - try { - return getVersionIterator(getSession(), remote.getAllVersions()); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Version getVersion(String versionName) throws VersionException, - RepositoryException { - try { - return getFactory().getVersion(getSession(), remote.getVersion(versionName)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Version getVersionByLabel(String label) throws RepositoryException { - try { - return getFactory().getVersion(getSession(), remote.getVersionByLabel(label)); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void addVersionLabel(String versionName, String label, - boolean moveLabel) throws VersionException, RepositoryException { - try { - remote.addVersionLabel(versionName, label, moveLabel); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void removeVersionLabel(String label) - throws VersionException, RepositoryException { - try { - remote.removeVersionLabel(label); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasVersionLabel(String label) { - try { - return remote.hasVersionLabel(label); - } catch (RemoteException ex) { - // grok the exception and assume label is missing - return false; - } - } - - /** {@inheritDoc} */ - public boolean hasVersionLabel(Version version, String label) - throws VersionException, RepositoryException { - try { - String versionUUID = version.getUUID(); - return remote.hasVersionLabel(versionUUID, label); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getVersionLabels() { - try { - return remote.getVersionLabels(); - } catch (RemoteException ex) { - // grok the exception and return an empty array - return new String[0]; - } - } - - /** {@inheritDoc} */ - public String[] getVersionLabels(Version version) - throws VersionException, RepositoryException { - try { - String versionUUID = version.getUUID(); - return remote.getVersionLabels(versionUUID); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void removeVersion(String versionName) - throws UnsupportedRepositoryOperationException, VersionException, - RepositoryException { - try { - remote.removeVersion(versionName); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientWorkspace.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientWorkspace.java deleted file mode 100644 index d6085677963..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientWorkspace.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.rmi.RemoteException; - -import javax.jcr.NamespaceRegistry; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.UnsupportedRepositoryOperationException; -import javax.jcr.Workspace; -import javax.jcr.nodetype.NodeTypeManager; -import javax.jcr.observation.ObservationManager; -import javax.jcr.query.QueryManager; -import javax.jcr.version.Version; - -import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; -import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; -import org.apache.jackrabbit.rmi.xml.WorkspaceImportContentHandler; -import org.xml.sax.ContentHandler; - -/** - * Local adapter for the JCR-RMI {@link RemoteWorkspace RemoteWorkspace} - * interface. This class makes a remote workspace locally available using - * the JCR {@link Workspace Workspace} interface. - * - * @author Jukka Zitting - * @author Philipp Koch - * @see javax.jcr.Workspace - * @see org.apache.jackrabbit.rmi.remote.RemoteWorkspace - */ -public class ClientWorkspace extends ClientObject implements Workspace { - - /** The current session. */ - private Session session; - - /** The adapted remote workspace. */ - private RemoteWorkspace remote; - - /** - * Creates a client adapter for the given remote workspace. - * - * @param session current session - * @param remote remote workspace - * @param factory local adapter factory - */ - public ClientWorkspace( - Session session, RemoteWorkspace remote, - LocalAdapterFactory factory) { - super(factory); - this.session = session; - this.remote = remote; - } - - /** - * Returns the current session without contacting the remote workspace. - * - * {@inheritDoc} - */ - public Session getSession() { - return session; - } - - /** {@inheritDoc} */ - public String getName() { - try { - return remote.getName(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - - /** {@inheritDoc} */ - public void copy(String from, String to) throws RepositoryException { - try { - remote.copy(from, to); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void copy(String workspace, String to, String from) - throws RepositoryException { - try { - remote.copy(workspace, from, to); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void move(String from, String to) throws RepositoryException { - try { - remote.move(from, to); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public QueryManager getQueryManager() throws RepositoryException { - try { - RemoteQueryManager manager = remote.getQueryManager(); - return getFactory().getQueryManager(session, manager); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NamespaceRegistry getNamespaceRegistry() throws RepositoryException { - try { - RemoteNamespaceRegistry registry = remote.getNamespaceRegistry(); - return getFactory().getNamespaceRegistry(registry); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public NodeTypeManager getNodeTypeManager() throws RepositoryException { - try { - RemoteNodeTypeManager manager = remote.getNodeTypeManager(); - return getFactory().getNodeTypeManager(manager); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public ObservationManager getObservationManager() - throws RepositoryException { - // TODO Auto-generated method stub - // return null; - throw new UnsupportedRepositoryOperationException(); - } - - /** {@inheritDoc} */ - public void clone( - String workspace, String src, String dst, boolean removeExisting) - throws RepositoryException { - try { - remote.clone(workspace, src, dst, removeExisting); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getAccessibleWorkspaceNames() throws RepositoryException { - try { - return remote.getAccessibleWorkspaceNames(); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public ContentHandler getImportContentHandler( - String path, int uuidBehaviour) - throws RepositoryException { - return new WorkspaceImportContentHandler(this, path, uuidBehaviour); - } - - /** {@inheritDoc} */ - public void importXML(String path, InputStream xml, int uuidBehaviour) - throws IOException, RepositoryException { - try { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - byte[] bytes = new byte[4096]; - for (int n = xml.read(bytes); n != -1; n = xml.read(bytes)) { - buffer.write(bytes, 0, n); - } - remote.importXML(path, buffer.toByteArray(), uuidBehaviour); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restore(Version[] versions, boolean removeExisting) - throws RepositoryException { - // TODO Auto-generated method stub - throw new UnsupportedRepositoryOperationException(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/LocalAdapterFactory.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/LocalAdapterFactory.java deleted file mode 100644 index 98c8abadfe5..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/LocalAdapterFactory.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import javax.jcr.Item; -import javax.jcr.NamespaceRegistry; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.Session; -import javax.jcr.Workspace; -import javax.jcr.lock.Lock; -import javax.jcr.nodetype.ItemDef; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeManager; -import javax.jcr.nodetype.PropertyDef; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; -import javax.jcr.query.QueryResult; -import javax.jcr.query.Row; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; - -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteItemDef; -import org.apache.jackrabbit.rmi.remote.RemoteLock; -import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.RemoteQuery; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; -import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; -import org.apache.jackrabbit.rmi.remote.RemoteRepository; -import org.apache.jackrabbit.rmi.remote.RemoteRow; -import org.apache.jackrabbit.rmi.remote.RemoteSession; -import org.apache.jackrabbit.rmi.remote.RemoteVersion; -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; -import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; - -/** - * Factory interface for creating local adapters for remote references. - * This interface defines how remote JCR-RMI references are adapted - * back to the normal JCR interfaces. The adaption mechanism can be - * modified (for example to add extra features) by changing the - * local adapter factory used by the repository client. - *

- * Note that the - * {@link org.apache.jackrabbit.rmi.client.ClientObject ClientObject} - * base class provides a number of utility methods designed to work with - * a local adapter factory. Adapter implementations may want to inherit - * that functionality by subclassing from ClientObject. - * - * @author Jukka Zitting - * @author Philipp Koch - * @see org.apache.jackrabbit.rmi.server.RemoteAdapterFactory - * @see org.apache.jackrabbit.rmi.client.ClientAdapterFactory - * @see org.apache.jackrabbit.rmi.client.ClientObject - */ -public interface LocalAdapterFactory { - - /** - * Factory method for creating a local adapter for a remote repository. - * - * @param remote remote repository - * @return local repository adapter - */ - Repository getRepository(RemoteRepository remote); - - /** - * Factory method for creating a local adapter for a remote session. - * - * @param repository current repository - * @param remote remote session - * @return local session adapter - */ - Session getSession(Repository repository, RemoteSession remote); - - /** - * Factory method for creating a local adapter for a remote workspace. - * - * @param session current session - * @param remote remote workspace - * @return local workspace adapter - */ - Workspace getWorkspace(Session session, RemoteWorkspace remote); - - /** - * Factory method for creating a local adapter for a remote namespace - * registry. - * - * @param remote remote namespace registry - * @return local namespace registry adapter - */ - NamespaceRegistry getNamespaceRegistry(RemoteNamespaceRegistry remote); - - /** - * Factory method for creating a local adapter for a remote node type - * manager. - * - * @param remote remote node type manager - * @return local node type manager adapter - */ - NodeTypeManager getNodeTypeManager(RemoteNodeTypeManager remote); - - /** - * Factory method for creating a local adapter for a remote item. - * Note that before calling this method, the client may want to - * introspect the remote item reference to determine whether to use the - * {@link #getNode(Session, RemoteNode) getNode} or - * {@link #getProperty(Session, RemoteProperty) getProperty} method - * instead, as the adapter returned by this method will only cover - * the basic {@link Item Item} interface. - * - * @param session current session - * @param remote remote item - * @return local item adapter - */ - Item getItem(Session session, RemoteItem remote); - - /** - * Factory method for creating a local adapter for a remote property. - * - * @param session current session - * @param remote remote property - * @return local property adapter - */ - Property getProperty(Session session, RemoteProperty remote); - - /** - * Factory method for creating a local adapter for a remote node. - * - * @param session current session - * @param remote remote node - * @return local node adapter - */ - Node getNode(Session session, RemoteNode remote); - - /** - * Factory method for creating a local adapter for a remote version. - * - * @param session current session - * @param remote remote version - * @return local version adapter - */ - Version getVersion(Session session, RemoteVersion remote); - - /** - * Factory method for creating a local adapter for a remote version history. - * - * @param session current session - * @param remote remote version history - * @return local version history adapter - */ - VersionHistory getVersionHistory(Session session, RemoteVersionHistory remote); - - /** - * Factory method for creating a local adapter for a remote node type. - * - * @param remote remote node type - * @return local node type adapter - */ - NodeType getNodeType(RemoteNodeType remote); - - /** - * Factory method for creating a local adapter for a remote item - * definition. Note that before calling this method, the client may want to - * introspect the remote item definition to determine whether to use the - * {@link #getNodeDef(RemoteNodeDef) getNodeDef} or - * {@link #getPropertyDef(RemotePropertyDef) getPropertyDef} method - * instead, as the adapter returned by this method will only cover - * the {@link ItemDef ItemDef} base interface. - * - * @param remote remote item definition - * @return local item definition adapter - */ - ItemDef getItemDef(RemoteItemDef remote); - - /** - * Factory method for creating a local adapter for a remote node - * definition. - * - * @param remote remote node definition - * @return local node definition adapter - */ - NodeDef getNodeDef(RemoteNodeDef remote); - - /** - * Factory method for creating a local adapter for a remote property - * definition. - * - * @param remote remote property definition - * @return local property definition adapter - */ - PropertyDef getPropertyDef(RemotePropertyDef remote); - - /** - * Factory method for creating a local adapter for a remote lock. - * - * @param node current node - * @param remote remote lock - * @return local lock adapter - */ - Lock getLock(Node node, RemoteLock remote); - - /** - * Factory method for creating a local adapter for a remote query manager. - * - * @param session current session - * @param remote remote query manager - * @return local query manager adapter - */ - QueryManager getQueryManager(Session session, RemoteQueryManager remote); - - /** - * Factory method for creating a local adapter for a remote query. - * - * @param session current session - * @param remote remote query - * @return local query adapter - */ - Query getQuery(Session session, RemoteQuery remote); - - /** - * Factory method for creating a local adapter for a remote query result. - * - * @param session current session - * @param remote remote query result - * @return local query result adapter - */ - QueryResult getQueryResult(Session session, RemoteQueryResult remote); - - /** - * Factory method for creating a local adapter for a remote query row. - * - * @param remote remote query row - * @return local query row adapter - */ - Row getRow(RemoteRow remote); - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/RemoteRepositoryException.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/RemoteRepositoryException.java deleted file mode 100644 index 0f7d2f32267..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/RemoteRepositoryException.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; - -/** - * JCR-RMI remote exception. Used by the JCR-RMI client to wrap RMI errors - * into RepositoryExceptions to avoid breaking the JCR interfaces. - *

- * Note that if a RemoteException is received by call with no declared - * exceptions, then the RemoteException is wrapped into a - * RemoteRuntimeException. - * - * @author Jukka Zitting - */ -public class RemoteRepositoryException extends RepositoryException { - - /** - * Creates a RemoteRepositoryException based on the given RemoteException. - * - * @param ex the remote exception - */ - public RemoteRepositoryException(RemoteException ex) { - super(ex); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/RemoteRuntimeException.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/RemoteRuntimeException.java deleted file mode 100644 index 80fbf3a011e..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/RemoteRuntimeException.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -/** - * JCR-RMI remote runtime exception. Used by the JCR-RMI client to wrap - * RMI errors into RuntimeExceptions to avoid breaking the JCR interfaces. - *

- * Note that if a RemoteException is received by call that declares to - * throw RepositoryExceptions, then the RemoteException is wrapped into - * a RemoteRepositoryException. - * - * @author Jukka Zitting - */ -public class RemoteRuntimeException extends RuntimeException { - - /** - * Creates a RemoteRuntimeException based on the given RemoteException. - * - * @param ex the remote exception - */ - public RemoteRuntimeException(RemoteException ex) { - super(ex); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/package.html b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/package.html deleted file mode 100644 index b343705eb0e..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/package.html +++ /dev/null @@ -1,60 +0,0 @@ - -Client implementation of the transparent JCR-RMI layer. -

-This package contains the default client implementation of the -transparent JCR-RMI layer. The classes in this package can be used -to make a remote JCR-RMI service seem like a local JCR repository. -

-The contents of this package is designed using two design patterns, -Factory and Adapter. All the ClientObject subclasses implement the -Adapter pattern to adapt a remote JCR-RMI reference to the corresponding -local JCR interface. The Factory pattern is used to centralize the -creation and configuration of all adapter instances. - -

Looking up a JCR-RMI client

-

-The ClientRepositoryFactory class provides a convenient mechanism for -looking up a remote JCR-RMI repository. The factory can be used either -directly or as a JNDI object factory. -

-The following example shows how to use the ClientRepositoryFactory -directly: - -

-    String name = ...; // The RMI URL of the repository
-    
-    ClientRepositoryFactory factory = new ClientRepositoryFactory();
-    Repository repository = factory.getRepository(name);
-
- -

-The ClientRepositoryFactory can also be used via JNDI. The following -example settings and code demonstrate how to configure and use the -transparent JCR-RMI layer in a Tomcat 5.5 web application: - -

-context.xml:
-    <Resource name="jcr/Repository" auth="Container"
-              type="javax.jcr.Repository"
-              factory="org.apache.jackrabbit.rmi.client.ClientRepositoryFactory"
-              url="..."/>
-              
-web.xml:
-    <resource-env-ref>
-      <description>The external content repository</description>
-      <resource-env-ref-name>jcr/Repository</resource-env-ref-name>
-      <resource-env-ref-type>javac.jcr.Repository</resource-env-ref-type>
-    </resource-env-ref>
-
-...SomeServlet.java:
-    Context initial = new InitialContext();
-    Context context = (Context) initial.lookup("java:comp/env");
-    Repository repository = (Repository) context.lookup("jcr/Repository");
-
- -

-Note that in the example above only the context.xml configuration file -contains a direct references to the JCR-RMI layer. All other parts of the -web application can be implemented using the standard JCR interfaces. - - diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayIterator.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayIterator.java deleted file mode 100644 index c956ce6862e..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayIterator.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.iterator; - -import javax.jcr.RangeIterator; - -/** - * Array implementation of the JCR - * {@link javax.jcr.RangeIterator RangeIterator} interface. This class - * implements the RangeIterator functionality for an underlying array - * of objects. Used as the base class for the type-specific iterator - * classes defined in this package. - * - * @author Jukka Zitting - */ -public class ArrayIterator implements RangeIterator { - - /** The current iterator position. */ - private int position; - - /** The underlying array of objects. */ - private Object[] array; - - /** - * Creates an iterator for the given array of objects. - * - * @param array the objects to iterate - */ - public ArrayIterator(Object[] array) { - this.position = 0; - this.array = array; - } - - /** {@inheritDoc} */ - public boolean hasNext() { - return (position < array.length); - } - - /** {@inheritDoc} */ - public Object next() { - return array[position++]; - } - - /** {@inheritDoc} */ - public void remove() { - throw new UnsupportedOperationException(); - } - - /** {@inheritDoc} */ - public void skip(long items) { - position += items; - } - - /** {@inheritDoc} */ - public long getSize() { - return array.length; - } - - /** {@inheritDoc} */ - public long getPos() { - return position; - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayNodeIterator.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayNodeIterator.java deleted file mode 100644 index 233bd860029..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayNodeIterator.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.iterator; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; - -/** - * Array implementation of the JCR - * {@link javax.jcr.NodeIterator NodeIterator} interface. - * This class is used by the JCR-RMI client adapters to convert - * node arrays to iterators. - * - * @author Jukka Zitting - */ -public class ArrayNodeIterator extends ArrayIterator implements NodeIterator { - - /** - * Creates an iterator for the given array of nodes. - * - * @param nodes the nodes to iterate - */ - public ArrayNodeIterator(Node[] nodes) { - super(nodes); - } - - /** {@inheritDoc} */ - public Node nextNode() { - return (Node) next(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayNodeTypeIterator.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayNodeTypeIterator.java deleted file mode 100644 index f41eea5cae3..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayNodeTypeIterator.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.iterator; - -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeIterator; - -/** - * Array implementation of the JCR - * {@link javax.jcr.NodeTypeIterator NodeTypeIterator} interface. - * This class is used by the JCR-RMI client adapters to convert - * node type arrays to iterators. - * - * @author Jukka Zitting - */ -public class ArrayNodeTypeIterator extends ArrayIterator implements - NodeTypeIterator { - - /** - * Creates an iterator for the given array of node types. - * - * @param types the node types to iterate - */ - public ArrayNodeTypeIterator(NodeType[] types) { - super(types); - } - - /** {@inheritDoc} */ - public NodeType nextNodeType() { - return (NodeType) next(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayPropertyIterator.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayPropertyIterator.java deleted file mode 100644 index f53e79dbde5..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayPropertyIterator.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.iterator; - -import javax.jcr.Property; -import javax.jcr.PropertyIterator; - -/** - * Array implementation of the JCR - * {@link javax.jcr.PropertyIterator PropertyIterator} interface. - * This class is used by the JCR-RMI client adapters to convert - * property arrays to iterators. - * - * @author Jukka Zitting - */ -public class ArrayPropertyIterator extends ArrayIterator - implements PropertyIterator { - - /** - * Creates an iterator for the given array of properties. - * - * @param properties the properties to iterate - */ - public ArrayPropertyIterator(Property[] properties) { - super(properties); - } - - /** {@inheritDoc} */ - public Property nextProperty() { - return (Property) next(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayRowIterator.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayRowIterator.java deleted file mode 100644 index d04d7ae0a94..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayRowIterator.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.iterator; - -import javax.jcr.query.Row; -import javax.jcr.query.RowIterator; - -/** - * Array implementation of the JCR - * {@link javax.jcr.query.RowIterator RowIterator} interface. - * This class is used by the JCR-RMI client adapters to convert - * node arrays to iterators. - * - * @author Philipp Koch - */ -public class ArrayRowIterator extends ArrayIterator implements RowIterator { - - /** - * Creates an iterator for the given array of rows. - * - * @param rows the rows to iterate - */ - public ArrayRowIterator(Row[] rows) { - super(rows); - } - - /** {@inheritDoc} */ - public Row nextRow() { - return (Row) next(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayVersionIterator.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayVersionIterator.java deleted file mode 100644 index f81cb989b08..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/ArrayVersionIterator.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.iterator; - -import javax.jcr.version.Version; -import javax.jcr.version.VersionIterator; - -/** - * Array implementation of the JCR - * {@link javax.jcr.version.VersionIterator VersionIterator} interface. - * This class is used by the JCR-RMI client adapters to convert - * version arrays to iterators. - * - * @author Felix Meschberger - */ -public class ArrayVersionIterator extends ArrayIterator implements VersionIterator { - - /** - * Creates an iterator for the given array of nodes. - * - * @param versions the versions to iterate - */ - public ArrayVersionIterator(Version[] versions) { - super(versions); - } - - /** {@inheritDoc} */ - public Version nextVersion() { - return (Version) next(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/package.html b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/package.html deleted file mode 100644 index fc702d1ed1e..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/iterator/package.html +++ /dev/null @@ -1,10 +0,0 @@ - -Utility classes for implementing JCR iterators based on static arrays. -

-This package contains array-based implementations of the JCR -{@link javax.jcr.RangeIterator RangeIterator} interfaces. -

-These utility classes were designed for the transparent JCR-RMI layer, -but can easily be used as a part of any JCR repository implementation. -This package depends only on the standard JCR and J2SE APIs. - diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteItemDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteItemDef.java deleted file mode 100644 index 0e9da275211..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteItemDef.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -/** - * Remote version of the JCR {@link javax.jcr.nodetype.ItemDef ItemDef} - * interface. Used by the - * {@link org.apache.jackrabbit.rmi.server.ServerItemDef ServerItemDef} and - * {@link org.apache.jackrabbit.rmi.client.ClientItemDef ClientItemDef} - * adapter base classes to provide transparent RMI access to remote item - * definitions. - *

- * The methods in this interface are documented only with a reference - * to a corresponding ItemDef method. The remote object will simply forward - * the method call to the underlying ItemDef instance. Argument and return - * values, as well as possible exceptions, are copied over the network. - * Compex {@link javax.jcr.nodetype.NodeType NodeType} return values - * are returned as remote references to the - * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} - * interface. RMI errors are signalled with RemoteExceptions. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.ItemDef - * @see org.apache.jackrabbit.rmi.client.ClientItemDef - * @see org.apache.jackrabbit.rmi.server.ServerItemDef - */ -public interface RemoteItemDef extends Remote { - - /** - * Remote version of the - * {@link javax.jcr.nodetype.ItemDef#getDeclaringNodeType() ItemDef.getDeclaringNodeType()} - * method. - * - * @return declaring node type - * @throws RemoteException on RMI errors - */ - RemoteNodeType getDeclaringNodeType() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.ItemDef#getName() ItemDef.getName()} method. - * - * @return item name - * @throws RemoteException on RMI errors - */ - String getName() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.ItemDef#isAutoCreate() ItemDef.isAutoCreate()} - * method. - * - * @return true if the item is automatically created, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean isAutoCreate() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.ItemDef#isMandatory() ItemDef.isMandatory()} - * method. - * - * @return true if the item is mandatory, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean isMandatory() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.ItemDef#getOnParentVersion() ItemDef.getOnParentVersion()} - * method. - * - * @return parent version behaviour - * @throws RemoteException on RMI errors - */ - int getOnParentVersion() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.ItemDef#isProtected() ItemDef.isProtected()} - * method. - * - * @return true if the item is protected, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean isProtected() throws RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteLock.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteLock.java deleted file mode 100644 index 015ffc3d630..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteLock.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; - -/** - * Remote version of the JCR {@link javax.jcr.lock.Lock} interface. - * Used by the {@link org.apache.jackrabbit.rmi.server.ServerLock ServerLock} - * and {@link org.apache.jackrabbit.rmi.client.ClientLock ClientLock} - * adapter classes to provide transparent RMI access to remote locks. - *

- * The methods in this interface are documented only with a reference - * to a corresponding Lock method. The remote object will simply forward - * the method call to the underlying Lock instance. Return values and - * possible exceptions are copied over the network. RMI errors are signalled - * with RemoteExceptions. - * - * @author Jukka Zitting - * @see javax.jcr.lock.Lock - * @see org.apache.jackrabbit.rmi.client.ClientLock - * @see org.apache.jackrabbit.rmi.server.ServerLock - */ -public interface RemoteLock extends Remote { - - /** - * Remote version of the - * {@link javax.jcr.lock.Lock#getLockOwner() Lock.getLockOwner()} method. - * - * @return lock owner - * @throws RemoteException on RMI errors - */ - String getLockOwner() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.lock.Lock#isDeep() Lock.isDeep()} method. - * - * @return true if the lock is deep, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean isDeep() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.lock.Lock#getLockToken() Lock.getLockToken()} method. - * - * @return lock token - * @throws RemoteException on RMI errors - */ - String getLockToken() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.lock.Lock#isLive() Lock.isLive()} method. - * - * @return true if the lock is live, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean isLive() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.lock.Lock#refresh() Lock.refresh()} method. - * - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - void refresh() throws RepositoryException, RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeDef.java deleted file mode 100644 index f774a0180f9..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeDef.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.RemoteException; - -/** - * Remote version of the JCR {@link javax.jcr.nodetype.NodeDef NodeDef} - * interface. Used by the - * {@link org.apache.jackrabbit.rmi.server.ServerNodeDef ServerNodeDef} and - * {@link org.apache.jackrabbit.rmi.client.ClientNodeDef ClientNodeDef} - * adapters to provide transparent RMI access to remote node definitions. - *

- * The methods in this interface are documented only with a reference - * to a corresponding NodeDef method. The remote object will simply forward - * the method call to the underlying NodeDef instance. Return values - * and possible exceptions are copied over the network. Complex - * {@link javax.jcr.nodetype.NodeType NodeType} return values - * are returned as remote references to the - * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} - * interface. RMI errors are signalled with RemoteExceptions. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.NodeDef - * @see org.apache.jackrabbit.rmi.client.ClientNodeDef - * @see org.apache.jackrabbit.rmi.server.ServerNodeDef - */ -public interface RemoteNodeDef extends RemoteItemDef { - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeDef#getRequiredPrimaryTypes() NodeDef.getRequiredPrimaryTypes()} - * method. - * - * @return required primary node types - * @throws RemoteException on RMI errors - */ - RemoteNodeType[] getRequiredPrimaryTypes() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeDef#getDefaultPrimaryType() NodeDef.getDefaultPrimaryType()} - * method. - * - * @return default primary node type - * @throws RemoteException on RMI errors - */ - RemoteNodeType getDefaultPrimaryType() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeDef#allowSameNameSibs() NodeDef.allowSameNameSibs()} - * method. - * - * @return true if same name siblings are allowed, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean allowSameNameSibs() throws RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeType.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeType.java deleted file mode 100644 index 0fa861d5873..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeType.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -import javax.jcr.Value; - -/** - * Remote version of the JCR {@link javax.jcr.nodetype.NodeType NodeType} - * interface. Used by the - * {@link org.apache.jackrabbit.rmi.server.ServerNodeType ServerNodeType} and - * {@link org.apache.jackrabbit.rmi.client.ClientNodeType ClientNodeType} - * adapters to provide transparent RMI access to remote node types. - *

- * The methods in this interface are documented only with a reference - * to a corresponding NodeType method. The remote object will simply forward - * the method call to the underlying NodeType instance. Return values - * and possible exceptions are copied over the network. Complex return - * values (like NodeTypes and PropertyDefs) are retunred as remote - * references to the corresponding remote interfaces. RMI errors are - * signalled with RemoteExceptions. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.NodeType - * @see org.apache.jackrabbit.rmi.client.ClientNodeType - * @see org.apache.jackrabbit.rmi.server.ServerNodeType - */ -public interface RemoteNodeType extends Remote { - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#getName() NodeType.getName()} method. - * - * @return node type name - * @throws RemoteException on RMI errors - */ - String getName() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#isMixin() NodeType.isMixin()} method. - * - * @return true if this is a mixin type, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean isMixin() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#hasOrderableChildNodes() NodeType.hasOrderableChildNodes()} - * method. - * - * @return true if nodes of this type has orderable - * child nodes, false otherwise - * @throws RemoteException on RMI errors - */ - boolean hasOrderableChildNodes() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#getSupertypes() NodeType.getSupertypes()} - * method. - * - * @return supertypes - * @throws RemoteException on RMI errors - */ - RemoteNodeType[] getSupertypes() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#getDeclaredSupertypes() NodeType.getDeclaredSupertypes()} - * method. - * - * @return declared supertypes - * @throws RemoteException on RMI errors - */ - RemoteNodeType[] getDeclaredSupertypes() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#isNodeType(String) NodeType.isNodeType(String)} - * method. - * - * @param type node type name - * @return true if this node type is or extends the - * given node type, false otherwise - * @throws RemoteException on RMI errors - */ - boolean isNodeType(String type) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#getPropertyDefs() NodeType.getPropertyDefs()} - * method. - * - * @return property definitions - * @throws RemoteException on RMI errors - */ - RemotePropertyDef[] getPropertyDefs() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#getDeclaredPropertyDefs() NodeType.getDeclaredPropertyDefs()} - * method. - * - * @return declared property definitions - * @throws RemoteException on RMI errors - */ - RemotePropertyDef[] getDeclaredPropertyDefs() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#getChildNodeDefs() NodeType.getChildNodeDefs()} - * method. - * - * @return child node definitions - * @throws RemoteException on RMI errors - */ - RemoteNodeDef[] getChildNodeDefs() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#getDeclaredChildNodeDefs() NodeType.getDeclaredChildNodeDefs()} - * method. - * - * @return declared child node definitions - * @throws RemoteException on RMI errors - */ - RemoteNodeDef[] getDeclaredChildNodeDefs() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#canSetProperty(String,Value) NodeType.canSetProperty(String,Value)} - * method. - * - * @param name property name - * @param value property value - * @return true if the property can be set, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean canSetProperty(String name, Value value) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#canSetProperty(String,Value[]) NodeType.canSetProperty(String,Value[])} - * method. - * - * @param name property name - * @param values property values - * @return true if the property can be set, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean canSetProperty(String name, Value[] values) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#canAddChildNode(String) NodeType.canAddChildNode(String)} - * method. - * - * @param name child node name - * @return true if the child node can be added, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean canAddChildNode(String name) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#canAddChildNode(String,String) NodeType.canAddChildNode(String,String)} - * method. - * - * @param name child node name - * @param type child node type - * @return true if the child node can be added, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean canAddChildNode(String name, String type) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#canRemoveItem(String) NodeType.canRemoveItem(String)} - * method. - * - * @param name item name - * @return true if the item can be removed, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean canRemoveItem(String name) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.NodeType#getPrimaryItemName() NodeType.getPrimaryItemName()} - * method. - * - * @return primary item name - * @throws RemoteException on RMI errors - */ - String getPrimaryItemName() throws RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemotePropertyDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemotePropertyDef.java deleted file mode 100644 index 8bb133a5fa1..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemotePropertyDef.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.RemoteException; - -import javax.jcr.Value; - -/** - * Remote version of the JCR {@link javax.jcr.nodetype.PropertyDef PropertyDef} - * interface. Used by the - * {@link org.apache.jackrabbit.rmi.server.ServerPropertyDef ServerPropertyDef} - * and - * {@link org.apache.jackrabbit.rmi.client.ClientPropertyDef ClientPropertyDef} - * adapters to provide transparent RMI access to remote property definitions. - *

- * The methods in this interface are documented only with a reference - * to a corresponding PropertyDef method. The remote object will simply - * forward the method call to the underlying PropertyDef instance. Return - * values and possible exceptions are copied over the network. RMI errors - * are signalled with RemoteExceptions. - *

- * Note that returned Value objects must be serializable and implemented - * using classes available on both the client and server side. The - * {@link org.apache.jackrabbit.rmi.remote.SerialValue SerialValue} - * decorator utility provides a convenient way to satisfy these - * requirements. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.PropertyDef - * @see org.apache.jackrabbit.rmi.client.ClientPropertyDef - * @see org.apache.jackrabbit.rmi.server.ServerPropertyDef - */ -public interface RemotePropertyDef extends RemoteItemDef { - - /** - * Remote version of the - * {@link javax.jcr.nodetype.PropertyDef#getRequiredType() PropertyDef.getRequiredType()} - * method. - * - * @return required type - * @throws RemoteException on RMI errors - */ - int getRequiredType() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.PropertyDef#getValueConstraints() PropertyDef.getValueConstraints()} - * method. - * - * @return value constraints - * @throws RemoteException on RMI errors - */ - String[] getValueConstraints() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.PropertyDef#getDefaultValues() PropertyDef.getDefaultValues()} - * method. - * - * @return default values - * @throws RemoteException on RMI errors - */ - Value[] getDefaultValues() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.nodetype.PropertyDef#isMultiple() PropertyDef.isMultiple()} - * method. - * - * @return true if the property is multi-valued, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean isMultiple() throws RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQuery.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQuery.java deleted file mode 100644 index dc0a61991ee..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQuery.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; - -/** - * Remote version of the JCR {@link javax.jcr.query.Query Query} interface. - * Used by the {@link org.apache.jackrabbit.rmi.server.ServerQuery ServerQuery} - * and {@link org.apache.jackrabbit.rmi.client.ClientQuery ClientQuery} - * adapter base classes to provide transparent RMI access to remote items. - *

- * RMI errors are signalled with RemoteExceptions. - * - * @author Philipp Koch - * @see javax.jcr.query.Query - * @see org.apache.jackrabbit.rmi.client.ClientQuery - * @see org.apache.jackrabbit.rmi.server.ServerQuery - */ -public interface RemoteQuery extends Remote { - - /** - * @see javax.jcr.query.Query#execute() - * - * @return a QueryResult - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteQueryResult execute() throws RepositoryException, RemoteException; - - /** - * @see javax.jcr.query.Query#getStatement() - * - * @return the query statement. - * @throws RemoteException on RMI errors - */ - String getStatement() throws RemoteException; - - /** - * @see javax.jcr.query.Query#getLanguage() - * - * @return the query language. - * @throws RemoteException on RMI errors - */ - String getLanguage() throws RemoteException; - - /** - * @see javax.jcr.query.Query#getPersistentQueryPath() - * - * @return path of the node representing this query. - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - String getPersistentQueryPath() throws RepositoryException, RemoteException; - - /** - * @see javax.jcr.query.Query#save(String) - * - * @param absPath path at which to persist this query. - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - void save(String absPath) throws RepositoryException, RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQueryResult.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQueryResult.java deleted file mode 100644 index c3bd4f2ce54..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQueryResult.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; - -/** - * Remote version of the JCR {@link javax.jcr.query.QueryResult QueryResult} interface. - * Used by the {@link org.apache.jackrabbit.rmi.server.ServerQueryResult ServerQueryResult} - * and {@link org.apache.jackrabbit.rmi.client.ClientQueryResult ClientQueryResult} - * adapter base classes to provide transparent RMI access to remote items. - *

- * RMI errors are signalled with RemoteExceptions. - * - * @author Philipp Koch - * @see javax.jcr.query.QueryResult - * @see org.apache.jackrabbit.rmi.client.ClientQueryResult - * @see org.apache.jackrabbit.rmi.server.ServerQueryResult - */ -public interface RemoteQueryResult extends Remote { - /** - * @see javax.jcr.query.QueryResult#getPropertyNames() - * - * @return a PropertyIterator - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - String[] getPropertyNames() throws RepositoryException, RemoteException; - - /** - * @see javax.jcr.query.QueryResult#getRows() - * - * @return a RowIterator - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteRow[] getRows() throws RepositoryException, RemoteException; - - /** - * @see javax.jcr.query.QueryResult#getNodes() - * - * @return a NodeIterator - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteNode[] getNodes() throws RepositoryException, RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteRepository.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteRepository.java deleted file mode 100644 index e8efeac0168..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteRepository.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -import javax.jcr.Credentials; -import javax.jcr.RepositoryException; - -/** - * Remote version of the JCR {@link javax.jcr.Repository Repository} interface. - * Used by the - * {@link org.apache.jackrabbit.rmi.server.ServerRepository ServerRepository} - * and - * {@link org.apache.jackrabbit.rmi.client.ClientRepository ClientRepository} - * adapters to provide transparent RMI access to remote repositories. -*

- * The methods in this interface are documented only with a reference - * to a corresponding Repository method. The remote object will simply - * forward the method call to the underlying Repository instance. - * {@link javax.jcr.Session Session} objects are returned as remote references - * to the {@link RemoteSession RemoteSession} interface. Simple return - * values and possible exceptions are copied over the network to the client. - * RMI errors are signalled with RemoteExceptions. - * - * @author Jukka Zitting - * @see javax.jcr.Repository - * @see org.apache.jackrabbit.rmi.client.ClientRepository - * @see org.apache.jackrabbit.rmi.server.ServerRepository - */ -public interface RemoteRepository extends Remote { - - /** - * Remote version of the - * {@link javax.jcr.Repository#getDescriptor(String) Repository.getDescriptor(String)} - * method. - * - * @param key descriptor key - * @return descriptor value - * @throws RemoteException on RMI errors - */ - String getDescriptor(String key) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Repository#getDescriptorKeys() Repository.getDescriptorKeys()} - * method. - * - * @return descriptor keys - * @throws RemoteException on RMI errors - */ - String[] getDescriptorKeys() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Repository#login() Repository.login(}} method. - * - * @return remote session - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteSession login() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Repository#login(String) Repository.login(String}} - * method. - * - * @param workspace workspace name - * @return remote session - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteSession login(String workspace) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Repository#login(Credentials) Repository.login(Credentials}} - * method. - * - * @param credentials client credentials - * @return remote session - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteSession login(Credentials credentials) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Repository#login(Credentials,String) Repository.login(Credentials,String}} - * method. - * - * @param credentials client credentials - * @param workspace workspace name - * @return remote session - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteSession login(Credentials credentials, String workspace) - throws RepositoryException, RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteRow.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteRow.java deleted file mode 100644 index 6c82e4a8fcb..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteRow.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.Value; - -/** - * Remote version of the JCR {@link javax.jcr.query.Row Row} interface. - * Used by the {@link org.apache.jackrabbit.rmi.server.ServerRow ServerRow} - * and {@link org.apache.jackrabbit.rmi.client.ClientRow ClientRow} - * adapter base classes to provide transparent RMI access to remote items. - *

- * RMI errors are signalled with RemoteExceptions. - * - * @author Philipp Koch - * @see javax.jcr.query.Row - * @see org.apache.jackrabbit.rmi.client.ClientRow - * @see org.apache.jackrabbit.rmi.server.ServerRow - */ -public interface RemoteRow extends Remote { - - /** - * @see javax.jcr.query.Row#getValues() - * - * @return row values - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - Value[] getValues() throws RepositoryException, RemoteException; - - /** - * @see javax.jcr.query.Row#getValue(String) - * - * @param propertyName property name - * @return identified value - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - Value getValue(String propertyName) - throws RepositoryException, RemoteException; -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteSession.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteSession.java deleted file mode 100644 index 8d133c214fb..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteSession.java +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.io.IOException; -import java.rmi.Remote; -import java.rmi.RemoteException; -import java.security.AccessControlException; - -import javax.jcr.Credentials; -import javax.jcr.RepositoryException; - -/** - * Remote version of the JCR {@link javax.jcr.Session Session} interface. - * Used by the - * {@link org.apache.jackrabbit.rmi.server.ServerSession ServerSession} - * and - * {@link org.apache.jackrabbit.rmi.client.ClientSession ClientSession} - * adapters to provide transparent RMI access to remote sessions. - *

- * Most of the methods in this interface are documented only with a reference - * to a corresponding Session method. In these cases the remote object - * will simply forward the method call to the underlying Session instance. - * Complex return values like workspaces and other objects are returned - * as remote references to the corresponding remote interface. Simple - * return values and possible exceptions are simply copied over the network - * to the client. RMI errors are signalled with RemoteExceptions. - * - * @author Jukka Zitting - * @see javax.jcr.Session - * @see org.apache.jackrabbit.rmi.client.ClientSession - * @see org.apache.jackrabbit.rmi.server.ServerSession - */ -public interface RemoteSession extends Remote { - - /** - * Remote version of the - * {@link javax.jcr.Session#getUserId() Session.getUserId()} method. - * - * @return user id - * @throws RemoteException on RMI errors - * @see javax.jcr.Session#getUserId() - */ - String getUserId() throws RemoteException; - - /** - * Returns the named attribute. Note that only serializable - * attribute values can be transmitted over the network and that - * the client should have (or be able to fetch) the object class - * to access the returned value. Failures to meet these conditions - * are signalled with RemoteExceptions. - * - * @param name attribute name - * @return attribute value - * @throws RemoteException on RMI errors - * @see javax.jcr.Session#getAttribute(java.lang.String) - */ - Object getAttribute(String name) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getAttributeNames() Session.getAttributeNames()} - * method. - * - * @return attribute names - * @throws RemoteException on RMI errors - */ - String[] getAttributeNames() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getWorkspace() Session.getWorkspace()} method. - * - * @return workspace - * @see javax.jcr.Session#getWorkspace() - * @throws RemoteException on RMI errors - */ - RemoteWorkspace getWorkspace() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#impersonate(Credentials) Session.impersonate(Credentials)} - * method. - * - * @param credentials credentials for the new session - * @return new session - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteSession impersonate(Credentials credentials) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getNodeByUUID(String) Session.getNodeByUUID(String)} - * method. - * - * @param uuid node uuid - * @return node - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteNode getNodeByUUID(String uuid) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getItem(String) Session.getItem(String)} - * method. - * - * @param path item path - * @return item - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteItem getItem(String path) throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#itemExists(String) Session.itemExists(String)} - * method. - * - * @param path item path - * @return true if the item exists, - * false otherwise - * @throws RemoteException on RMI errors - */ - boolean itemExists(String path) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#move(String,String) Session.move(String,String)} - * method. - * - * @param from source path - * @param to destination path - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - void move(String from, String to) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#save() Session.save()} method. - * - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - void save() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#refresh(boolean) Session.refresh(boolean)} - * method. - * - * @param keepChanges flag to keep transient changes - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - void refresh(boolean keepChanges) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#logout() Session.logout()} - * method. - * - * @throws RemoteException on RMI errors - */ - void logout() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getRootNode() Session.getRootNode()} method. - * - * @return root node - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteNode getRootNode() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#hasPendingChanges() Session.hasPendingChanges()} - * method. - * - * @return true if the session has pending changes, - * false otherwise - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - boolean hasPendingChanges() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#checkPermission(String,String) Session.checkPermission(String,String)} - * method. - * - * @param path item path - * @param actions actions - * @throws AccessControlException if permission is denied - * @throws RemoteException on RMI errors - */ - void checkPermission(String path, String actions) - throws AccessControlException, RemoteException; - - /** - * Imports the system or document view XML data into a subtree of - * the identified node. Note that the entire XML stream is transferred - * as a single byte array over the network. This may cause problems with - * large XML streams. The remote server will wrap the XML data into - * a {@link java.io.ByteArrayInputStream ByteArrayInputStream} and feed - * it to the normal importXML method. - * - * @param path node path - * @param xml imported XML document - * @throws IOException on IO errors - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - * @see javax.jcr.Session#importXML(java.lang.String, java.io.InputStream) - */ - void importXML(String path, byte[] xml) - throws IOException, RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#setNamespacePrefix(String,String) Session.setNamespacePrefix(String,String)} - * method. - * - * @param prefix namespace prefix - * @param uri namespace uri - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - void setNamespacePrefix(String prefix, String uri) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getNamespacePrefixes() Session.getNamespacePrefixes()} - * method. - * - * @return namespace prefixes - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - String[] getNamespacePrefixes() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getNamespaceURI(String) Session.getNamespaceURI(String)} - * method. - * - * @param prefix namespace prefix - * @return namespace uri - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - String getNamespaceURI(String prefix) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getNamespacePrefix(String) Session.getNamespacePrefix(String)} - * method. - * - * @param uri namespace uri - * @return namespace prefix - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - String getNamespacePrefix(String uri) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#addLockToken(String) Session.addLockToken(String)} - * method. - * - * @param name lock token - * @throws RemoteException on RMI errors - */ - void addLockToken(String name) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#getLockTokens() Session.getLockTokens()} - * method. - * - * @return lock tokens - * @throws RemoteException on RMI errors - */ - String[] getLockTokens() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.Session#removeLockToken(String) Session.removeLockToken(String)} - * method. - * - * @param name lock token - * @throws RemoteException on RMI errors - */ - void removeLockToken(String name) throws RemoteException; - - /** - * Exports the identified repository subtree as a system view XML - * stream. Note that the entire XML stream is transferred as a - * single byte array over the network. This may cause problems with - * large exports. The remote server uses a - * {@link java.io.ByteArrayOutputStream ByteArrayOutputStream} to capture - * the XML data written by the normal exportSysView method. - * - * @param path node path - * @param binaryAsLink TODO - * @param noRecurse TODO - * @return exported XML document - * @throws IOException on IO errors - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - * @see javax.jcr.Workspace#exportSysView(java.lang.String, java.io.OutputStream, boolean, boolean) - */ - byte[] exportSysView(String path, boolean binaryAsLink, boolean noRecurse) - throws IOException, RepositoryException, RemoteException; - - /** - * Exports the identified repository subtree as a document view XML - * stream. Note that the entire XML stream is transferred as a - * single byte array over the network. This may cause problems with - * large exports. The remote server uses a - * {@link java.io.ByteArrayOutputStream ByteArrayOutputStream} to capture - * the XML data written by the normal exportDocView method. - * - * @param path node path - * @param binaryAsLink TODO - * @param noRecurse TODO - * @return exported XML document - * @throws IOException on IO errors - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - * @see javax.jcr.Workspace#exportDocView(java.lang.String, java.io.OutputStream, boolean, boolean) - */ - byte[] exportDocView(String path, boolean binaryAsLink, boolean noRecurse) - throws IOException, RepositoryException, RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteVersion.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteVersion.java deleted file mode 100644 index d7acdd303c9..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteVersion.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.RemoteException; -import java.util.Calendar; - -import javax.jcr.RepositoryException; - - -/** - * Remote version of the JCR {@link javax.jcr.version.Version Version} interface. - * Used by the {@link org.apache.jackrabbit.rmi.server.ServerVersion ServerVersion} - * and {@link org.apache.jackrabbit.rmi.client.ClientVersion ClientVersion} - * adapters to provide transparent RMI access to remote versions. - *

- * The methods in this interface are documented only with a reference - * to a corresponding Version method. The remote object will simply forward - * the method call to the underlying Version instance. Argument and return - * values, as well as possible exceptions, are copied over the network. - * Complex return values (like Versions) are returned as remote - * references to the corresponding remote interfaces. Iterator values - * are transmitted as object arrays. RMI errors are signalled with - * RemoteExceptions. - * - * @author Felix Meschberger - * @see javax.jcr.version.Version - * @see org.apache.jackrabbit.rmi.client.ClientVersion - * @see org.apache.jackrabbit.rmi.server.ServerVersion - */ -public interface RemoteVersion extends RemoteNode { - - /** - * Remote version of the - * {@link javax.jcr.version.Version#getContainingHistory() Version.getContainingHistory()} method. - * - * @return a RemoteVersionHistory object. - * - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ -// RemoteVersionHistory getContainingHistory() throws RepositoryException; - - /** - * Remote version of the - * {@link javax.jcr.version.Version#getCreated() Version.getCreated()} method. - * - * @return a Calendar object. - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - Calendar getCreated() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.Version#getSuccessors() Version.getSuccessors()} method. - * - * @return a RemoteVersion array. - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteVersion[] getSuccessors() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.Version#getPredecessors() Version.getPredecessors()} method. - * - * @return a RemoteVersion array. - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteVersion[] getPredecessors() throws RepositoryException, RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteVersionHistory.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteVersionHistory.java deleted file mode 100644 index 839647c2b4c..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteVersionHistory.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; - -/** - * Remote version of the JC - * {@link javax.jcr.version.VersionHistory VersionHistory} interface. Used by - * the - * {@link org.apache.jackrabbit.rmi.server.ServerVersionHistory ServerVersionHistory} - * and - * {@link org.apache.jackrabbit.rmi.client.ClientVersionHistory ClientVersionHistory} - * adapters to provide transparent RMI access to remote version histories. - *

- * The methods in this interface are documented only with a reference - * to a corresponding VersionHistory method. The remote object will simply - * forward the method call to the underlying VersionHistory instance. Argument - * and return values, as well as possible exceptions, are copied over the - * network. Complex return values (like Versions) are returned as remote - * references to the corresponding remote interfaces. Iterator values - * are transmitted as object arrays. RMI errors are signalled with - * RemoteExceptions. - * - * @author Felix Meschberger - * @see javax.jcr.version.Version - * @see org.apache.jackrabbit.rmi.client.ClientVersionHistory - * @see org.apache.jackrabbit.rmi.server.ServerVersionHistory - */ -public interface RemoteVersionHistory extends RemoteNode { - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#getVersionableUUID() VersionHistory.getVersionableUUID()} - * method. - * - * @return the UUID of the versionable node for which this is the version history. - * @throws RepositoryException if an error occurs. - * @throws RemoteException on RMI errors - */ -// String getVersionableUUID() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#getRootVersion() VersionHistory.getRootVersion()} - * method. - * - * @return a Version object. - * @throws RepositoryException if an error occurs. - * @throws RemoteException on RMI errors - */ - RemoteVersion getRootVersion() throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#getAllVersions() VersionHistory.getAllVersions()} - * method. - * - * @return a VersionIterator object. - * @throws RepositoryException if an error occurs. - * @throws RemoteException on RMI errors - */ - RemoteVersion[] getAllVersions() - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#getVersion(String) VersionHistory.getVersion(String)} - * method. - * - * @param versionName a version name - * @return a Version object. - * @throws RepositoryException if an error occurs. - * @throws RemoteException on RMI errors - */ - RemoteVersion getVersion(String versionName) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#getVersionByLabel(String) VersionHistory.getVersionByLabel(String)} - * method. - * - * @param label a version label - * @return a Version object. - * @throws RepositoryException if an error occurs. - * @throws RemoteException on RMI errors - */ - RemoteVersion getVersionByLabel(String label) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean) - * VersionHistory.addVersionLabel(String, String, boolean)} - * method. - * - * @param versionName the name of the version to which the label is to be added. - * @param label the label to be added. - * @param moveLabel if true, then if label is already assigned to a version in - * this version history, it is moved to the new version specified; if false, then attempting - * to assign an already used label will throw a VersionException. - * - * @throws RepositoryException if another error occurs. - * @throws RemoteException on RMI errors - */ - void addVersionLabel(String versionName, String label, boolean moveLabel) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#removeVersionLabel(String) VersionHistory.removeVersionLabel(String)} - * method. - * - * @param label a version label - * @throws RepositoryException if another error occurs. - * @throws RemoteException on RMI errors - */ - void removeVersionLabel(String label) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#hasVersionLabel(String) VersionHistory.hasVersionLabel(String)} - * method. - * - * @param label a version label - * @return a boolean - * @throws RemoteException on RMI errors - */ - boolean hasVersionLabel(String label) throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#hasVersionLabel(RemoteVersion, String) hasVersionLabel(RemoteVersion, String)} - * method. - * - * @param versionUUID The UUID of the version whose labels are to be returned. - * @param label a version label - * @return a boolean. - * @throws RepositoryException if another error occurs. - * @throws RemoteException on RMI errors - */ - boolean hasVersionLabel(String versionUUID, String label) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#getVersionLabels() VersionHistory.getVersionLabels()} - * method. - * - * @return a String array containing all the labels of the version history - * @throws RemoteException on RMI errors - */ - String[] getVersionLabels() throws RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#getVersionLabels(RemoteVersion) VersionHistory.getVersionLabels(RemoteVersion)} - * method. - * - * @param versionUUID The UUID of the version whose labels are to be returned. - * @return a String array containing all the labels of the given version - * @throws RepositoryException if another error occurs. - * @throws RemoteException on RMI errors - */ - String[] getVersionLabels(String versionUUID) - throws RepositoryException, RemoteException; - - /** - * Remote version of the - * {@link javax.jcr.version.VersionHistory#removeVersion(String) VersionHistory.removeVersion(String)} - * method. - * - * @param versionName the name of a version in this version history. - * @throws RepositoryException if another error occurs. - * @throws RemoteException on RMI errors - */ - void removeVersion(String versionName) - throws RepositoryException, RemoteException; -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/SerialValue.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/SerialValue.java deleted file mode 100644 index 2b78989e886..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/SerialValue.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.util.Calendar; - -import javax.jcr.BinaryValue; -import javax.jcr.BooleanValue; -import javax.jcr.DateValue; -import javax.jcr.DoubleValue; -import javax.jcr.LongValue; -import javax.jcr.NameValue; -import javax.jcr.PathValue; -import javax.jcr.PropertyType; -import javax.jcr.ReferenceValue; -import javax.jcr.RepositoryException; -import javax.jcr.StringValue; -import javax.jcr.Value; -import javax.jcr.ValueFormatException; - -/** - * Serializable {@link Value Value} decorator. A SerialValue decorator - * makes it possible to serialize the contents of a Value object even - * if the object itself is not serializable. For example the standard - * JCR Value classes are not serializable. - *

- * Serialization is achieved by extracting and serializing the type and - * underlying data of the Value object. On deserialization the type and - * data information is used to create a standard JCR Value object as - * a copy of the original value. This makes it possible to copy even - * system-specific Value instances to a remote JVM that might not contain - * the implementation class of the original Value object. - *

- * The SerialValue decorator adds no other functionality to the Value - * interface. Normal method calls are simply forwarded to the decorated - * Value object. - *

- * Note that a decorator object keeps a reference to the underlying value - * object and uses the standard value access methods to perform serialization. - * Serialization therefore affects the internal state of the underlying value! - * On the other hand, the internal state of a value might interfere with the - * serialization decorator. The safest course of action is to only decorate - * and serialize fresh value objects and to discard them after serialization. - * - * @author Jukka Zitting - * @see javax.jcr.Value - * @see java.io.Serializable - */ -public class SerialValue implements Value, Serializable { - - /** Static serial version UID. */ - static final long serialVersionUID = 8070492457339121953L; - - /** The decorated value. */ - private Value value; - - /** - * Creates a serialization decorator for the given value. - * - * @param value the value to be decorated - */ - public SerialValue(Value value) { - this.value = value; - } - - /** - * Utility method for decorating an array of values. The - * returned array will contain SerialValue decorators for - * all the given values. Note that the contents of the - * original values will only be copied when the decorators - * are serialized. - *

- * If the given array is null, then an empty - * array is returned. - * - * @param values the values to be decorated - * @return array of decorated values - */ - public static Value[] makeSerialValueArray(Value[] values) { - if (values != null) { - Value[] serials = new Value[values.length]; - for (int i = 0; i < values.length; i++) { - serials[i] = new SerialValue(values[i]); - } - return serials; - } else { - return new Value[0]; - } - } - - /** - * Serializes the underlying Value object. Instead of using - * the normal serialization mechanism, the essential state - * of the Value object is extracted and written to the serialization - * stream as a type-value pair. - * - * @param out the serialization stream - * @throws IOException on IO errors - */ - private void writeObject(ObjectOutputStream out) throws IOException { - try { - int type = value.getType(); - out.writeInt(type); - switch (type) { - case PropertyType.BINARY: - InputStream data = value.getStream(); - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - byte[] bytes = new byte[4096]; - for (int n = data.read(bytes); n != -1; n = data.read(bytes)) { - buffer.write(bytes, 0, n); - } - out.writeInt(buffer.size()); - buffer.writeTo(out); - break; - case PropertyType.BOOLEAN: - out.writeBoolean(value.getBoolean()); - break; - case PropertyType.DATE: - out.writeObject(value.getDate()); - break; - case PropertyType.DOUBLE: - out.writeDouble(value.getDouble()); - break; - case PropertyType.LONG: - out.writeLong(value.getLong()); - break; - case PropertyType.NAME: - case PropertyType.PATH: - case PropertyType.REFERENCE: - case PropertyType.STRING: - out.writeUTF(value.getString()); - break; - default: - throw new IOException("Unknown value type"); - } - } catch (RepositoryException ex) { - throw new IOException(ex.getMessage()); - } - } - - /** - * Deserializes the underlying Value object. A new Value object - * is created based on the type and state data read fro the - * serialization stream. - * - * @param in the serialization stream - * @throws IOException on IO errors - */ - private void readObject(ObjectInputStream in) throws IOException { - try { - int type = in.readInt(); - switch (type) { - case PropertyType.BINARY: - byte[] bytes = new byte[in.readInt()]; - in.readFully(bytes); - value = new BinaryValue(bytes); - break; - case PropertyType.BOOLEAN: - value = new BooleanValue(in.readBoolean()); - break; - case PropertyType.DATE: - value = new DateValue((Calendar) in.readObject()); - break; - case PropertyType.DOUBLE: - value = new DoubleValue(in.readDouble()); - break; - case PropertyType.LONG: - value = new LongValue(in.readLong()); - break; - case PropertyType.NAME: - value = NameValue.valueOf(in.readUTF()); - break; - case PropertyType.PATH: - value = PathValue.valueOf(in.readUTF()); - break; - case PropertyType.REFERENCE: - value = ReferenceValue.valueOf(in.readUTF()); - break; - case PropertyType.STRING: - value = new StringValue(in.readUTF()); - break; - default: - throw new IllegalStateException("Illegal serial value type"); - } - } catch (ValueFormatException ex) { - throw new IOException(ex.getMessage()); - } catch (ClassNotFoundException ex) { - throw new IOException(ex.getMessage()); - } - } - - /** - * Forwards the method call to the decorated value. - * {@inheritDoc} - */ - public boolean getBoolean() throws ValueFormatException, - IllegalStateException, RepositoryException { - return value.getBoolean(); - } - - /** - * Forwards the method call to the decorated value. - * {@inheritDoc} - */ - public Calendar getDate() throws ValueFormatException, - IllegalStateException, RepositoryException { - return value.getDate(); - } - - /** - * Forwards the method call to the decorated value. - * {@inheritDoc} - */ - public double getDouble() throws ValueFormatException, - IllegalStateException, RepositoryException { - return value.getDouble(); - } - - /** - * Forwards the method call to the decorated value. - * {@inheritDoc} - */ - public long getLong() throws ValueFormatException, IllegalStateException, - RepositoryException { - return value.getLong(); - } - - /** - * Forwards the method call to the decorated value. - * {@inheritDoc} - */ - public InputStream getStream() throws ValueFormatException, - IllegalStateException, RepositoryException { - return value.getStream(); - } - - /** - * Forwards the method call to the decorated value. - * {@inheritDoc} - */ - public String getString() throws ValueFormatException, - IllegalStateException, RepositoryException { - return value.getString(); - } - - /** - * Forwards the method call to the decorated value. - * {@inheritDoc} - */ - public int getType() { - return value.getType(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/package.html b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/package.html deleted file mode 100644 index 53883a550fc..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/package.html +++ /dev/null @@ -1,19 +0,0 @@ - -Remote interfaces of the transparent JCR-RMI layer. -

-This package contains all the interfaces and classes used by both -the client and server sides of the transparent JCR-RMI layer. -The compiled contents of this package should thus be included -in both client and server installations. Note also that RMI stubs and -skeletons need to be generated for all the remote interfaces when using -old JDK versions (stubs for JDK < 1.5, skeletons for JDK < 1.2). -

-The interfaces in this package are remote versions of the -corresponding interfaces in the javax.jcr packages. They are used by -the adapter classes in the .rmi.client and .rmi.server packages. The server -classes adapt local JCR objects to the remote interfaces, and the client -classes adapt the resulting remote references back to the JCR interfaces. -

-The SerialValue class is a decorator utility used by both the client and -server classes to safely pass Value objects over the network. - diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/RemoteAdapterFactory.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/RemoteAdapterFactory.java deleted file mode 100644 index 6dd5923e355..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/RemoteAdapterFactory.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.Item; -import javax.jcr.NamespaceRegistry; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.Session; -import javax.jcr.Workspace; -import javax.jcr.lock.Lock; -import javax.jcr.nodetype.ItemDef; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeManager; -import javax.jcr.nodetype.PropertyDef; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; -import javax.jcr.query.QueryResult; -import javax.jcr.query.Row; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; - -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteItemDef; -import org.apache.jackrabbit.rmi.remote.RemoteLock; -import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.RemoteQuery; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; -import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; -import org.apache.jackrabbit.rmi.remote.RemoteRepository; -import org.apache.jackrabbit.rmi.remote.RemoteRow; -import org.apache.jackrabbit.rmi.remote.RemoteSession; -import org.apache.jackrabbit.rmi.remote.RemoteVersion; -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; -import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; - -/** - * Factory interface for creating remote adapters for local resources. - * This interface defines how the local JCR interfaces are adapted to - * remote JCR-RMI references. The adaption mechanism can be - * modified (for example to add extra features) by changing the - * remote adapter factory used by the repository server. - *

- * Note that the {@link ServerObject ServerObject} base class provides - * a number of utility methods designed to work with a remote adapter - * factory. Adapter implementations may want to inherit that functionality - * by subclassing from ServerObject. - * - * @author Jukka Zitting - * @author Philipp Koch - * @see org.apache.jackrabbit.rmi.client.LocalAdapterFactory - * @see org.apache.jackrabbit.rmi.server.ServerAdapterFactory - * @see org.apache.jackrabbit.rmi.server.ServerObject - */ -public interface RemoteAdapterFactory { - - /** - * Returns a remote adapter for the given local repository. - * - * @param repository local repository - * @return remote repository adapter - * @throws RemoteException on RMI errors - */ - RemoteRepository getRemoteRepository(Repository repository) - throws RemoteException; - - /** - * Returns a remote adapter for the given local session. - * - * @param session local session - * @return remote session adapter - * @throws RemoteException on RMI errors - */ - RemoteSession getRemoteSession(Session session) throws RemoteException; - - /** - * Returns a remote adapter for the given local workspace. - * - * @param workspace local workspace - * @return remote workspace adapter - * @throws RemoteException on RMI errors - */ - RemoteWorkspace getRemoteWorkspace(Workspace workspace) - throws RemoteException; - - /** - * Returns a remote adapter for the given local namespace registry. - * - * @param registry local namespace registry - * @return remote namespace registry adapter - * @throws RemoteException on RMI errors - */ - RemoteNamespaceRegistry getRemoteNamespaceRegistry( - NamespaceRegistry registry) throws RemoteException; - - /** - * Returns a remote adapter for the given local node type manager. - * - * @param manager local node type manager - * @return remote node type manager adapter - * @throws RemoteException on RMI errors - */ - RemoteNodeTypeManager getRemoteNodeTypeManager(NodeTypeManager manager) - throws RemoteException; - - /** - * Returns a remote adapter for the given local item. This method - * will return an adapter that implements only the - * {@link Item Item} interface. The caller may want to introspect - * the local item to determine whether to use either the - * {@link #getRemoteNode(Node) getRemoteNode} or the - * {@link #getRemoteProperty(Property) getRemoteProperty} method instead. - * - * @param item local item - * @return remote item adapter - * @throws RemoteException on RMI errors - */ - RemoteItem getRemoteItem(Item item) throws RemoteException; - - /** - * Returns a remote adapter for the given local property. - * - * @param property local property - * @return remote property adapter - * @throws RemoteException on RMI errors - */ - RemoteProperty getRemoteProperty(Property property) throws RemoteException; - - /** - * Returns a remote adapter for the given local node. - * - * @param node local node - * @return remote node adapter - * @throws RemoteException on RMI errors - */ - RemoteNode getRemoteNode(Node node) throws RemoteException; - - /** - * Returns a remote adapter for the given local version. - * - * @param version local version - * @return remote version adapter - * @throws RemoteException on RMI errors - */ - RemoteVersion getRemoteVersion(Version version) throws RemoteException; - - /** - * Returns a remote adapter for the given local version history. - * - * @param versionHistory local version history - * @return remote version history adapter - * @throws RemoteException on RMI errors - */ - RemoteVersionHistory getRemoteVersionHistory(VersionHistory versionHistory) - throws RemoteException; - - /** - * Returns a remote adapter for the given local node type. - * - * @param type local node type - * @return remote node type adapter - * @throws RemoteException on RMI errors - */ - RemoteNodeType getRemoteNodeType(NodeType type) throws RemoteException; - - /** - * Returns a remote adapter for the given local item definition. - * This method will return an adapter that implements only the - * {@link ItemDef ItemDef} interface. The caller may want to introspect - * the local item definition to determine whether to use either the - * {@link #getRemoteNodeDef(NodeDef) getRemoteNodeDef} or the - * {@link #getRemotePropertyDef(PropertyDef) getRemotePropertyDef} - * method instead. - * - * @param def local item definition - * @return remote item definition adapter - * @throws RemoteException on RMI errors - */ - RemoteItemDef getRemoteItemDef(ItemDef def) throws RemoteException; - - /** - * Returns a remote adapter for the given local node definition. - * - * @param def local node definition - * @return remote node definition adapter - * @throws RemoteException on RMI errors - */ - RemoteNodeDef getRemoteNodeDef(NodeDef def) throws RemoteException; - - /** - * Returns a remote adapter for the given local property definition. - * - * @param def local property definition - * @return remote property definition adapter - * @throws RemoteException on RMI errors - */ - RemotePropertyDef getRemotePropertyDef(PropertyDef def) - throws RemoteException; - - /** - * Returns a remote adapter for the given local lock. - * - * @param lock local lock - * @return remote lock adapter - * @throws RemoteException on RMI errors - */ - RemoteLock getRemoteLock(Lock lock) throws RemoteException; - - /** - * Returns a remote adapter for the given local query manager. - * - * @param manager local query manager - * @return remote query manager adapter - * @throws RemoteException on RMI errors - */ - RemoteQueryManager getRemoteQueryManager(QueryManager manager) - throws RemoteException; - - /** - * Returns a remote adapter for the given local query. - * - * @param query local query - * @return remote query adapter - * @throws RemoteException on RMI errors - */ - RemoteQuery getRemoteQuery(Query query) throws RemoteException; - - /** - * Returns a remote adapter for the given local query result. - * - * @param result local query result - * @return remote query result adapter - * @throws RemoteException on RMI errors - */ - RemoteQueryResult getRemoteQueryResult(QueryResult result) - throws RemoteException; - - /** - * Returns a remote adapter for the given local query row. - * - * @param row local query row - * @return remote query row adapter - * @throws RemoteException on RMI errors - */ - RemoteRow getRemoteRow(Row row) throws RemoteException; - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerAdapterFactory.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerAdapterFactory.java deleted file mode 100644 index 5a62804bf52..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerAdapterFactory.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.Item; -import javax.jcr.NamespaceRegistry; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.Session; -import javax.jcr.Workspace; -import javax.jcr.lock.Lock; -import javax.jcr.nodetype.ItemDef; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeManager; -import javax.jcr.nodetype.PropertyDef; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; -import javax.jcr.query.QueryResult; -import javax.jcr.query.Row; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; - -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteItemDef; -import org.apache.jackrabbit.rmi.remote.RemoteLock; -import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.RemoteQuery; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; -import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; -import org.apache.jackrabbit.rmi.remote.RemoteRepository; -import org.apache.jackrabbit.rmi.remote.RemoteRow; -import org.apache.jackrabbit.rmi.remote.RemoteSession; -import org.apache.jackrabbit.rmi.remote.RemoteVersion; -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; -import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; - -/** - * Default implementation of the - * {@link RemoteAdapterFactory RemoteAdapterFactory} interface. - * This factory uses the server adapters defined in this package as - * the default adapter implementations. Subclasses can override or extend - * the default adapters by implementing the corresponding factory methods. - * - * @author Jukka Zitting - * @author Philipp Koch - */ -public class ServerAdapterFactory implements RemoteAdapterFactory { - - /** - * Creates a {@link ServerRepository ServerRepository} instance. - * {@inheritDoc} - */ - public RemoteRepository getRemoteRepository(Repository repository) - throws RemoteException { - return new ServerRepository(repository, this); - } - - /** - * Creates a {@link ServerSession ServerSession} instance. - * {@inheritDoc} - */ - public RemoteSession getRemoteSession(Session session) - throws RemoteException { - return new ServerSession(session, this); - } - - /** - * Creates a {@link ServerWorkspace ServerWorkspace} instance. - * {@inheritDoc} - */ - public RemoteWorkspace getRemoteWorkspace(Workspace workspace) - throws RemoteException { - return new ServerWorkspace(workspace, this); - } - - /** - * Creates a {@link ServerNamespaceRegistry ServerNamespaceRegistry} - * instance. - * {@inheritDoc} - */ - public RemoteNamespaceRegistry getRemoteNamespaceRegistry( - NamespaceRegistry registry) - throws RemoteException { - return new ServerNamespaceRegistry(registry, this); - } - - /** - * Creates a {@link ServerNodeTypeManager ServerNodeTypeManager} instance. - * {@inheritDoc} - */ - public RemoteNodeTypeManager getRemoteNodeTypeManager( - NodeTypeManager manager) - throws RemoteException { - return new ServerNodeTypeManager(manager, this); - } - - /** - * Creates a {@link ServerItem ServerItem} instance. - * {@inheritDoc} - */ - public RemoteItem getRemoteItem(Item item) throws RemoteException { - return new ServerItem(item, this); - } - - /** - * Creates a {@link ServerProperty ServerProperty} instance. - * {@inheritDoc} - */ - public RemoteProperty getRemoteProperty(Property property) - throws RemoteException { - return new ServerProperty(property, this); - } - - /** - * Creates a {@link ServerNode ServerNode} instance. - * {@inheritDoc} - */ - public RemoteNode getRemoteNode(Node node) throws RemoteException { - return new ServerNode(node, this); - } - - /** - * Creates a {@link ServerVersion ServerVersion} instance. - * {@inheritDoc} - */ - public RemoteVersion getRemoteVersion(Version version) throws RemoteException { - return new ServerVersion(version, this); - } - - /** - * Creates a {@link ServerVersionHistory ServerVersionHistory} instance. - * {@inheritDoc} - */ - public RemoteVersionHistory getRemoteVersionHistory(VersionHistory versionHistory) - throws RemoteException { - return new ServerVersionHistory(versionHistory, this); - } - - /** - * Creates a {@link ServerNodeType ServerNodeType} instance. - * {@inheritDoc} - */ - public RemoteNodeType getRemoteNodeType(NodeType type) - throws RemoteException { - return new ServerNodeType(type, this); - } - - /** - * Creates a {@link ServerItemDef ServerItemDef} instance. - * {@inheritDoc} - */ - public RemoteItemDef getRemoteItemDef(ItemDef def) - throws RemoteException { - return new ServerItemDef(def, this); - } - - /** - * Creates a {@link ServerNodeDef ServerNodeDef} instance. - * {@inheritDoc} - */ - public RemoteNodeDef getRemoteNodeDef(NodeDef def) - throws RemoteException { - return new ServerNodeDef(def, this); - } - - /** - * Creates a {@link ServerPropertyDef ServerPropertyDef} instance. - * {@inheritDoc} - */ - public RemotePropertyDef getRemotePropertyDef(PropertyDef def) - throws RemoteException { - return new ServerPropertyDef(def, this); - } - - /** - * Creates a {@link ServerLock ServerLock} instance. - * {@inheritDoc} - */ - public RemoteLock getRemoteLock(Lock lock) throws RemoteException { - return new ServerLock(lock); - } - - /** - * Creates a {@link ServerQueryManager ServerQueryManager} instance. - * {@inheritDoc} - */ - public RemoteQueryManager getRemoteQueryManager(QueryManager manager) - throws RemoteException { - return new ServerQueryManager(manager, this); - } - - /** - * Creates a {@link ServerQuery ServerQuery} instance. - * {@inheritDoc} - */ - public RemoteQuery getRemoteQuery(Query query) throws RemoteException { - return new ServerQuery(query, this); - } - - /** - * Creates a {@link ServerQueryResult ServerQueryResult} instance. - * {@inheritDoc} - */ - public RemoteQueryResult getRemoteQueryResult(QueryResult result) - throws RemoteException { - return new ServerQueryResult(result, this); - } - - /** - * Creates a {@link ServerQueryResult ServerQueryResult} instance. - * {@inheritDoc} - */ - public RemoteRow getRemoteRow(Row row) throws RemoteException { - return new ServerRow(row, this); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerItemDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerItemDef.java deleted file mode 100644 index b5a4d9f6447..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerItemDef.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.nodetype.ItemDef; - -import org.apache.jackrabbit.rmi.remote.RemoteItemDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; - -/** - * Remote adapter for the JCR {@link javax.jcr.nodetype.ItemDef ItemDef} - * interface. This class makes a local item definition available as an - * RMI service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteItemDef RemoteItemDef} - * interface. Used mainly as the base class for the - * {@link org.apache.jackrabbit.rmi.server.ServerPropertyDef ServerPropertyDef} - * and - * {@link org.apache.jackrabbit.rmi.server.ServerNodeDef ServerNodeDef} - * adapters. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.ItemDef - * @see org.apache.jackrabbit.rmi.remote.RemoteItemDef - */ -public class ServerItemDef extends ServerObject implements RemoteItemDef { - - /** The adapted local item definition. */ - private ItemDef def; - - /** - * Creates a remote adapter for the given local item definition. - * - * @param def local item definition - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerItemDef(ItemDef def, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.def = def; - } - - /** {@inheritDoc} */ - public RemoteNodeType getDeclaringNodeType() throws RemoteException { - return getFactory().getRemoteNodeType(def.getDeclaringNodeType()); - } - - /** {@inheritDoc} */ - public String getName() throws RemoteException { - return def.getName(); - } - - /** {@inheritDoc} */ - public boolean isAutoCreate() throws RemoteException { - return def.isAutoCreate(); - } - - /** {@inheritDoc} */ - public boolean isMandatory() throws RemoteException { - return def.isMandatory(); - } - - /** {@inheritDoc} */ - public int getOnParentVersion() throws RemoteException { - return def.getOnParentVersion(); - } - - /** {@inheritDoc} */ - public boolean isProtected() throws RemoteException { - return def.isProtected(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerLock.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerLock.java deleted file mode 100644 index ede86cb751b..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerLock.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; -import java.rmi.server.UnicastRemoteObject; - -import javax.jcr.RepositoryException; -import javax.jcr.lock.Lock; - -import org.apache.jackrabbit.rmi.remote.RemoteLock; - -/** - * Remote adapter for the JCR {@link javax.jcr.lock.Lock Lock} interface. - * This class makes a local lock available as an RMI service using - * the {@link org.apache.jackrabbit.rmi.remote.RemoteLock RemoteLock} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.lock.Lock - * @see org.apache.jackrabbit.rmi.remote.RemoteLock - */ -public class ServerLock extends UnicastRemoteObject implements RemoteLock { - - /** The adapted local lock. */ - private Lock lock; - - /** - * Creates a remote adapter for the given local lock. - * - * @param lock local lock - * @throws RemoteException on RMI errors - */ - public ServerLock(Lock lock) throws RemoteException { - this.lock = lock; - } - - /** {@inheritDoc} */ - public String getLockOwner() throws RemoteException { - return lock.getLockOwner(); - } - - /** {@inheritDoc} */ - public boolean isDeep() throws RemoteException { - return lock.isDeep(); - } - - /** {@inheritDoc} */ - public String getLockToken() throws RemoteException { - return lock.getLockToken(); - } - - /** {@inheritDoc} */ - public boolean isLive() throws RemoteException { - return lock.isLive(); - } - - /** {@inheritDoc} */ - public void refresh() throws RepositoryException, RemoteException { - lock.refresh(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNode.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNode.java deleted file mode 100644 index 57bc6577451..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNode.java +++ /dev/null @@ -1,546 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.Value; -import javax.jcr.lock.Lock; -import javax.jcr.version.Version; - -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteLock; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemoteVersion; -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; - -/** - * Remote adapter for the JCR {@link javax.jcr.Node Node} interface. - * This class makes a local node available as an RMI service using - * the {@link org.apache.jackrabbit.rmi.remote.RemoteNode RemoteNode} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.Node - * @see org.apache.jackrabbit.rmi.remote.RemoteNode - */ -public class ServerNode extends ServerItem implements RemoteNode { - - /** The adapted local node. */ - private Node node; - - /** - * Creates a remote adapter for the given local node. - * - * @param node local node - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerNode(Node node, RemoteAdapterFactory factory) - throws RemoteException { - super(node, factory); - this.node = node; - } - - /** {@inheritDoc} */ - public RemoteNode addNode(String path) - throws RepositoryException, RemoteException { - try { - return getRemoteNode(node.addNode(path)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNode addNode(String path, String type) - throws RepositoryException, RemoteException { - try { - return getRemoteNode(node.addNode(path, type)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteProperty getProperty(String path) - throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteProperty(node.getProperty(path)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteProperty[] getProperties() - throws RepositoryException, RemoteException { - try { - return getRemotePropertyArray(node.getProperties()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteItem getPrimaryItem() - throws RepositoryException, RemoteException { - try { - return getRemoteItem(node.getPrimaryItem()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteProperty[] getProperties(String pattern) - throws RepositoryException, RemoteException { - try { - return getRemotePropertyArray(node.getProperties(pattern)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteProperty[] getReferences() - throws RepositoryException, RemoteException { - try { - return getRemotePropertyArray(node.getReferences()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String getUUID() throws RepositoryException, RemoteException { - try { - return node.getUUID(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasNodes() throws RepositoryException, RemoteException { - try { - return node.hasNodes(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasProperties() throws RepositoryException, RemoteException { - try { - return node.hasProperties(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasProperty(String path) - throws RepositoryException, RemoteException { - try { - return node.hasProperty(path); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNodeType[] getMixinNodeTypes() - throws RepositoryException, RemoteException { - try { - return getRemoteNodeTypeArray(node.getMixinNodeTypes()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNodeType getPrimaryNodeType() - throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteNodeType(node.getPrimaryNodeType()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isNodeType(String type) - throws RepositoryException, RemoteException { - try { - return node.isNodeType(type); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNode[] getNodes() throws RepositoryException, RemoteException { - try { - return getRemoteNodeArray(node.getNodes()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNode[] getNodes(String pattern) - throws RepositoryException, RemoteException { - try { - return getRemoteNodeArray(node.getNodes(pattern)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNode getNode(String path) - throws RepositoryException, RemoteException { - try { - return getRemoteNode(node.getNode(path)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasNode(String path) - throws RepositoryException, RemoteException { - try { - return node.hasNode(path); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteProperty setProperty(String name, Value value) - throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteProperty(node.setProperty(name, value)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void addMixin(String name) - throws RepositoryException, RemoteException { - try { - node.addMixin(name); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean canAddMixin(String name) - throws RepositoryException, RemoteException { - try { - return node.canAddMixin(name); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void removeMixin(String name) - throws RepositoryException, RemoteException { - try { - node.removeMixin(name); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void orderBefore(String src, String dst) - throws RepositoryException, RemoteException { - try { - node.orderBefore(src, dst); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteProperty setProperty(String name, Value[] values) - throws RepositoryException, RemoteException { - try { - Property property = node.setProperty(name, values); - return getFactory().getRemoteProperty(property); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNodeDef getDefinition() - throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteNodeDef(node.getDefinition()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteVersion checkin() throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteVersion(node.checkin()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void checkout() throws RepositoryException, RemoteException { - try { - node.checkout(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String getCorrespondingNodePath(String workspace) - throws RepositoryException, RemoteException { - try { - return node.getCorrespondingNodePath(workspace); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public int getIndex() throws RepositoryException, RemoteException { - try { - return node.getIndex(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void merge(String workspace, boolean bestEffort) - throws RepositoryException, RemoteException { - try { - node.merge(workspace, bestEffort); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void cancelMerge(String versionUUID) - throws RepositoryException, RemoteException { - try { - node.cancelMerge(getVersionByUUID(versionUUID)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void doneMerge(String versionUUID) - throws RepositoryException, RemoteException { - try { - node.doneMerge(getVersionByUUID(versionUUID)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restore(String version, boolean removeExisting) - throws RepositoryException, RemoteException { - try { - node.restore(version, removeExisting); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restoreByUUID(String versionUUID, boolean removeExisting) - throws RepositoryException, RemoteException { - try { - node.restore(getVersionByUUID(versionUUID), removeExisting); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restore(String versionUUID, String path, boolean removeExisting) - throws RepositoryException, RemoteException { - try { - node.restore(getVersionByUUID(versionUUID), path, removeExisting); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void restoreByLabel(String label, boolean removeExisting) - throws RepositoryException, RemoteException { - try { - node.restoreByLabel(label, removeExisting); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void update(String workspace) - throws RepositoryException, RemoteException { - try { - node.update(workspace); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean holdsLock() throws RepositoryException, RemoteException { - try { - return node.holdsLock(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isCheckedOut() throws RepositoryException, RemoteException { - try { - return node.isCheckedOut(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteVersionHistory getVersionHistory() - throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteVersionHistory(node.getVersionHistory()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteVersion getBaseVersion() - throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteVersion(node.getBaseVersion()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean isLocked() throws RepositoryException, RemoteException { - try { - return node.isLocked(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteProperty setProperty(String name, Value[] values, int type) - throws RepositoryException, RemoteException { - try { - Property property = node.setProperty(name, values, type); - return getFactory().getRemoteProperty(property); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void unlock() throws RepositoryException, RemoteException { - try { - node.unlock(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteLock getLock() throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteLock(node.getLock()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteLock lock(boolean isDeep, boolean isSessionScoped) - throws RepositoryException, RemoteException { - try { - Lock lock = node.lock(isDeep, isSessionScoped); - return getFactory().getRemoteLock(lock); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - //---------- Implementation helper ----------------------------------------- - - /** - * Returns the {@link Version} instance for the given UUID. - * - * @param versionUUID The UUID of the version. - * - * @return The version node. - * - * @throws RepositoryException if an error occurrs accessing the version - * node or if the UUID does not denote a version. - */ - protected Version getVersionByUUID(String versionUUID) - throws RepositoryException { - - // get the version node by its UUID from the version history's session - Session session = node.getSession(); - Node versionNode = session.getNodeByUUID(versionUUID); - - // check whether the node is a session, which it should be according - // to the spec (methods returning nodes should automatically return - // the correct type). - if (versionNode instanceof Version) { - return (Version) versionNode; - } - - // otherwise fail - throw new RepositoryException("Cannot find version " + versionUUID); - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeDef.java deleted file mode 100644 index 389f721d9b1..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeDef.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.nodetype.NodeDef; - -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; - -/** - * Remote adapter for the JCR {@link javax.jcr.nodetype.NodeDef NodeDef} - * interface. This class makes a local node definition available as an - * RMI service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeDef RemoteNodeDef} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.NodeDef - * @see org.apache.jackrabbit.rmi.remote.RemoteNodeDef - */ -public class ServerNodeDef extends ServerItemDef implements RemoteNodeDef { - - /** The adapted node definition. */ - private NodeDef def; - - /** - * Creates a remote adapter for the given local node definition. - * - * @param def local node definition - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerNodeDef(NodeDef def, RemoteAdapterFactory factory) - throws RemoteException { - super(def, factory); - this.def = def; - } - - /** {@inheritDoc} */ - public RemoteNodeType[] getRequiredPrimaryTypes() throws RemoteException { - return getRemoteNodeTypeArray(def.getRequiredPrimaryTypes()); - } - - /** {@inheritDoc} */ - public RemoteNodeType getDefaultPrimaryType() throws RemoteException { - return getFactory().getRemoteNodeType(def.getDefaultPrimaryType()); - } - - /** {@inheritDoc} */ - public boolean allowSameNameSibs() throws RemoteException { - return def.allowSameNameSibs(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeType.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeType.java deleted file mode 100644 index 313ef4104f1..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeType.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.Value; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.PropertyDef; - -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; - -/** - * Remote adapter for the JCR {@link javax.jcr.nodetype.NodeType NodeType} - * interface. This class makes a local node type available as an RMI service - * using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.NodeType - * @see org.apache.jackrabbit.rmi.remote.RemoteNodeType - */ -public class ServerNodeType extends ServerObject implements RemoteNodeType { - - /** The adapted local node type. */ - private NodeType type; - - /** - * Creates a remote adapter for the given local node type. - * - * @param type local node type - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerNodeType(NodeType type, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.type = type; - } - - /** {@inheritDoc} */ - public String getName() throws RemoteException { - return type.getName(); - } - - /** {@inheritDoc} */ - public boolean isMixin() throws RemoteException { - return type.isMixin(); - } - - /** {@inheritDoc} */ - public boolean hasOrderableChildNodes() throws RemoteException { - return type.hasOrderableChildNodes(); - } - - /** {@inheritDoc} */ - public RemoteNodeType[] getSupertypes() throws RemoteException { - return getRemoteNodeTypeArray(type.getSupertypes()); - } - - /** {@inheritDoc} */ - public RemoteNodeType[] getDeclaredSupertypes() throws RemoteException { - return getRemoteNodeTypeArray(type.getDeclaredSupertypes()); - } - - /** {@inheritDoc} */ - public boolean isNodeType(String type) throws RemoteException { - return this.type.isNodeType(type); - } - - /** {@inheritDoc} */ - public RemotePropertyDef[] getPropertyDefs() throws RemoteException { - PropertyDef[] defs = type.getPropertyDefs(); - return getRemotePropertyDefArray(defs); - } - - /** {@inheritDoc} */ - public RemotePropertyDef[] getDeclaredPropertyDefs() - throws RemoteException { - PropertyDef[] defs = type.getDeclaredPropertyDefs(); - return getRemotePropertyDefArray(defs); - } - - /** {@inheritDoc} */ - public RemoteNodeDef[] getChildNodeDefs() throws RemoteException { - return getRemoteNodeDefArray(type.getChildNodeDefs()); - } - - /** {@inheritDoc} */ - public RemoteNodeDef[] getDeclaredChildNodeDefs() throws RemoteException { - return getRemoteNodeDefArray(type.getDeclaredChildNodeDefs()); - } - - /** {@inheritDoc} */ - public boolean canSetProperty(String name, Value value) - throws RemoteException { - return type.canSetProperty(name, value); - } - - /** {@inheritDoc} */ - public boolean canSetProperty(String name, Value[] values) - throws RemoteException { - return type.canSetProperty(name, values); - } - - /** {@inheritDoc} */ - public boolean canAddChildNode(String name) throws RemoteException { - return type.canAddChildNode(name); - } - - /** {@inheritDoc} */ - public boolean canAddChildNode(String name, String type) - throws RemoteException { - return this.type.canAddChildNode(name, type); - } - - /** {@inheritDoc} */ - public boolean canRemoveItem(String name) throws RemoteException { - return type.canRemoveItem(name); - } - - /** {@inheritDoc} */ - public String getPrimaryItemName() throws RemoteException { - return type.getPrimaryItemName(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeTypeManager.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeTypeManager.java deleted file mode 100644 index f95d6aad53e..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNodeTypeManager.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.nodetype.NodeTypeManager; - -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; - -/** - * Remote adapter for the JCR - * {@link javax.jcr.nodetype.NodeTypeManager NodeTypeManager} - * interface. This class makes a local node type manager available as an - * RMI service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager RemoteNodeTypeManager} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.NodeTypeManager - * @see org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager - */ -public class ServerNodeTypeManager extends ServerObject - implements RemoteNodeTypeManager { - - /** The adapted local node type manager. */ - private NodeTypeManager manager; - - /** - * Creates a remote adapter for the given local node type manager. - * - * @param manager local node type manager - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerNodeTypeManager( - NodeTypeManager manager, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.manager = manager; - } - - /** {@inheritDoc} */ - public RemoteNodeType getNodeType(String name) - throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteNodeType(manager.getNodeType(name)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNodeType[] getAllNodeTypes() - throws RepositoryException, RemoteException { - try { - return getRemoteNodeTypeArray(manager.getAllNodeTypes()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNodeType[] getPrimaryNodeTypes() - throws RepositoryException, RemoteException { - try { - return getRemoteNodeTypeArray(manager.getPrimaryNodeTypes()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNodeType[] getMixinNodeTypes() - throws RepositoryException, RemoteException { - try { - return getRemoteNodeTypeArray(manager.getMixinNodeTypes()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerObject.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerObject.java deleted file mode 100644 index 07589e81bf2..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerObject.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; -import java.rmi.server.UnicastRemoteObject; - -import javax.jcr.AccessDeniedException; -import javax.jcr.InvalidItemStateException; -import javax.jcr.InvalidSerializedDataException; -import javax.jcr.Item; -import javax.jcr.ItemExistsException; -import javax.jcr.ItemNotFoundException; -import javax.jcr.LoginException; -import javax.jcr.MergeException; -import javax.jcr.NamespaceException; -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.ReferentialIntegrityException; -import javax.jcr.RepositoryException; -import javax.jcr.UnsupportedRepositoryOperationException; -import javax.jcr.ValueFormatException; -import javax.jcr.lock.LockException; -import javax.jcr.nodetype.ConstraintViolationException; -import javax.jcr.nodetype.NoSuchNodeTypeException; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeIterator; -import javax.jcr.nodetype.PropertyDef; -import javax.jcr.query.InvalidQueryException; -import javax.jcr.version.Version; -import javax.jcr.version.VersionException; -import javax.jcr.version.VersionHistory; -import javax.jcr.version.VersionIterator; - -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.RemoteVersion; - -/** - * Base class for remote adapters. The purpose of this class is to - * centralize the handling of the RemoteAdapterFactory instance used - * to instantiate new server adapters. - * - * @author Jukka Zitting - */ -public class ServerObject extends UnicastRemoteObject { - - /** Factory for creating server adapters. */ - private RemoteAdapterFactory factory; - - /** - * Creates a basic server adapter that uses the given factory - * to create new adapters. - * - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - protected ServerObject(RemoteAdapterFactory factory) - throws RemoteException { - this.factory = factory; - } - - /** - * Returns the remote adapter factory used to create new adapters. - * - * @return remote adapter factory - */ - protected RemoteAdapterFactory getFactory() { - return factory; - } - - /** - * Returns a cleaned version of the given exception. In some cases - * the underlying repository implementation may throw exceptions - * that are either unserializable, use exception subclasses that are - * only locally available, contain references to unserializable or - * only locally available classes. This method returns a cleaned - * version of such an exception. The returned exception contains only - * the message string from the original exception, and uses the public - * JCR exception class that most specifically matches the original - * exception. - * - * @param ex the original exception - * @return clean exception - */ - protected RepositoryException getRepositoryException( - RepositoryException ex) { - if (ex instanceof AccessDeniedException) { - return new AccessDeniedException(ex.getMessage()); - } else if (ex instanceof ConstraintViolationException) { - return new ConstraintViolationException(ex.getMessage()); - } else if (ex instanceof InvalidItemStateException) { - return new InvalidItemStateException(ex.getMessage()); - } else if (ex instanceof InvalidQueryException) { - return new InvalidQueryException(ex.getMessage()); - } else if (ex instanceof InvalidSerializedDataException) { - return new InvalidSerializedDataException(ex.getMessage()); - } else if (ex instanceof ItemExistsException) { - return new ItemExistsException(ex.getMessage()); - } else if (ex instanceof ItemNotFoundException) { - return new ItemNotFoundException(ex.getMessage()); - } else if (ex instanceof LockException) { - return new LockException(ex.getMessage()); - } else if (ex instanceof LoginException) { - return new LoginException(ex.getMessage()); - } else if (ex instanceof MergeException) { - return new MergeException(ex.getMessage()); - } else if (ex instanceof NamespaceException) { - return new NamespaceException(ex.getMessage()); - } else if (ex instanceof NoSuchNodeTypeException) { - return new NoSuchNodeTypeException(ex.getMessage()); - } else if (ex instanceof NoSuchWorkspaceException) { - return new NoSuchWorkspaceException(ex.getMessage()); - } else if (ex instanceof PathNotFoundException) { - return new PathNotFoundException(ex.getMessage()); - } else if (ex instanceof ReferentialIntegrityException) { - return new ReferentialIntegrityException(ex.getMessage()); - } else if (ex instanceof UnsupportedRepositoryOperationException) { - return new UnsupportedRepositoryOperationException(ex.getMessage()); - } else if (ex instanceof ValueFormatException) { - return new ValueFormatException(ex.getMessage()); - } else if (ex instanceof VersionException) { - return new VersionException(ex.getMessage()); - } else { - return new RepositoryException(ex.getMessage()); - } - } - - /** - * Utility method for creating a remote reference for a local item. - * Unlike the factory method for creating remote item references, this - * method introspects the type of the local item and returns the - * corresponding node, property, or item remote reference using the - * remote adapter factory. - *

- * If the item, this method calls the - * {@link #getRemoteNode(Node)} to return the correct remote type. - * - * @param item local node, property, or item - * @return remote node, property, or item reference - * @throws RemoteException on RMI errors - */ - protected RemoteItem getRemoteItem(Item item) throws RemoteException { - if (item instanceof Property) { - return factory.getRemoteProperty((Property) item); - } else if (item instanceof Node) { - return getRemoteNode((Node) item); - } else { - return factory.getRemoteItem(item); - } - } - - /** - * Utility method for creating a remote reference for a local node. - * Unlike the factory method for creating remote node references, this - * method introspects the type of the local node and returns the - * corresponding node, version, or version history remote reference using - * the remote adapter factory. - * - * @param node local version, versionhistory, or normal node - * @return remote node, property, or item reference - * @throws RemoteException on RMI errors - */ - protected RemoteNode getRemoteNode(Node node) throws RemoteException { - if (node instanceof Version) { - return factory.getRemoteVersion((Version) node); - } else if (node instanceof VersionHistory) { - return factory.getRemoteVersionHistory((VersionHistory) node); - } else { - return factory.getRemoteNode(node); - } - } - - /** - * Utility method for creating an array of remote references for - * local properties. The remote references are created using the - * remote adapter factory. - *

- * A null input is treated as an empty iterator. - * - * @param iterator local property iterator - * @return remote property array - * @throws RemoteException on RMI errors - */ - protected RemoteProperty[] getRemotePropertyArray(PropertyIterator iterator) - throws RemoteException { - if (iterator == null) { - return new RemoteProperty[0]; // for safety - } - - RemoteProperty[] remotes = new RemoteProperty[(int) iterator.getSize()]; - for (int i = 0; iterator.hasNext(); i++) { - remotes[i] = factory.getRemoteProperty(iterator.nextProperty()); - } - return remotes; - } - - /** - * Utility method for creating an array of remote references for - * local nodes. The remote references are created using the - * remote adapter factory. - *

- * A null input is treated as an empty iterator. - * - * @param iterator local node iterator - * @return remote node array - * @throws RemoteException on RMI errors - */ - protected RemoteNode[] getRemoteNodeArray(NodeIterator iterator) - throws RemoteException { - if (iterator != null) { - RemoteNode[] remotes = new RemoteNode[(int) iterator.getSize()]; - for (int i = 0; iterator.hasNext(); i++) { - remotes[i] = getRemoteNode(iterator.nextNode()); - } - return remotes; - } else { - return new RemoteNode[0]; // for safety - } - } - - /** - * Utility method for creating an array of remote references for - * local versions. The remote references are created using the - * remote adapter factory. - *

- * A null input is treated as an empty array. - * - * @param versions local version array - * @return remote version array - * @throws RemoteException on RMI errors - */ - protected RemoteVersion[] getRemoteVersionArray(Version[] versions) - throws RemoteException { - if (versions != null) { - RemoteVersion[] remotes = new RemoteVersion[versions.length]; - for (int i = 0; i < remotes.length; i++) { - remotes[i] = factory.getRemoteVersion(versions[i]); - } - return remotes; - } else { - return new RemoteVersion[0]; // for safety - } - } - - /** - * Utility method for creating an array of remote references for - * local versions. The remote references are created using the - * remote adapter factory. - *

- * A null input is treated as an empty iterator. - * - * @param iterator local version iterator - * @return remote version array - * @throws RemoteException on RMI errors - */ - protected RemoteVersion[] getRemoteVersionArray(VersionIterator iterator) - throws RemoteException { - if (iterator != null) { - RemoteVersion[] remotes = new RemoteVersion[(int) iterator.getSize()]; - for (int i = 0; iterator.hasNext(); i++) { - remotes[i] = factory.getRemoteVersion(iterator.nextVersion()); - } - return remotes; - } else { - return new RemoteVersion[0]; // for safety - } - } - - /** - * Utility method for creating an array of remote references for - * local node types. The remote references are created using the - * remote adapter factory. - *

- * A null input is treated as an empty array. - * - * @param types local node type array - * @return remote node type array - * @throws RemoteException on RMI errors - */ - protected RemoteNodeType[] getRemoteNodeTypeArray(NodeType[] types) - throws RemoteException { - if (types != null) { - RemoteNodeType[] remotes = new RemoteNodeType[types.length]; - for (int i = 0; i < types.length; i++) { - remotes[i] = factory.getRemoteNodeType(types[i]); - } - return remotes; - } else { - return new RemoteNodeType[0]; // for safety - } - } - - /** - * Utility method for creating an array of remote references for - * local node types. The remote references are created using the - * remote adapter factory. - *

- * A null input is treated as an empty iterator. - * - * @param iterator local node type iterator - * @return remote node type array - * @throws RemoteException on RMI errors - */ - protected RemoteNodeType[] getRemoteNodeTypeArray(NodeTypeIterator iterator) - throws RemoteException { - if (iterator != null) { - RemoteNodeType[] remotes = - new RemoteNodeType[(int) iterator.getSize()]; - for (int i = 0; iterator.hasNext(); i++) { - remotes[i] = factory.getRemoteNodeType(iterator.nextNodeType()); - } - return remotes; - } else { - return new RemoteNodeType[0]; // for safety - } - } - - /** - * Utility method for creating an array of remote references for - * local node definitions. The remote references are created using the - * remote adapter factory. - *

- * A null input is treated as an empty array. - * - * @param defs local node definition array - * @return remote node definition array - * @throws RemoteException on RMI errors - */ - protected RemoteNodeDef[] getRemoteNodeDefArray(NodeDef[] defs) - throws RemoteException { - if (defs != null) { - RemoteNodeDef[] remotes = new RemoteNodeDef[defs.length]; - for (int i = 0; i < defs.length; i++) { - remotes[i] = factory.getRemoteNodeDef(defs[i]); - } - return remotes; - } else { - return new RemoteNodeDef[0]; // for safety - } - } - - /** - * Utility method for creating an array of remote references for - * local property definitions. The remote references are created using the - * remote adapter factory. - *

- * A null input is treated as an empty array. - * - * @param defs local property definition array - * @return remote property definition array - * @throws RemoteException on RMI errors - */ - protected RemotePropertyDef[] getRemotePropertyDefArray(PropertyDef[] defs) - throws RemoteException { - if (defs != null) { - RemotePropertyDef[] remotes = new RemotePropertyDef[defs.length]; - for (int i = 0; i < defs.length; i++) { - remotes[i] = factory.getRemotePropertyDef(defs[i]); - } - return remotes; - } else { - return new RemotePropertyDef[0]; // for safety - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerPropertyDef.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerPropertyDef.java deleted file mode 100644 index 51d0d4a28e6..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerPropertyDef.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.Value; -import javax.jcr.nodetype.PropertyDef; - -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.SerialValue; - -/** - * Remote adapter for the JCR - * {@link javax.jcr.nodetype.PropertyDef PropertyDef} interface. This - * class makes a local property definition available as an RMI service - * using the - * {@link org.apache.jackrabbit.rmi.remote.RemotePropertyDef RemotePropertyDef} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.nodetype.PropertyDef - * @see org.apache.jackrabbit.rmi.remote.RemotePropertyDef - */ -public class ServerPropertyDef extends ServerItemDef - implements RemotePropertyDef { - - /** The adapted local property definition. */ - private PropertyDef def; - - /** - * Creates a remote adapter for the given local property definition. - * - * @param def local property definition - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerPropertyDef(PropertyDef def, RemoteAdapterFactory factory) - throws RemoteException { - super(def, factory); - this.def = def; - } - - /** {@inheritDoc} */ - public int getRequiredType() throws RemoteException { - return def.getRequiredType(); - } - - /** {@inheritDoc} */ - public String[] getValueConstraints() throws RemoteException { - return def.getValueConstraints(); - } - - /** {@inheritDoc} */ - public Value[] getDefaultValues() throws RemoteException { - return SerialValue.makeSerialValueArray(def.getDefaultValues()); - } - - /** {@inheritDoc} */ - public boolean isMultiple() throws RemoteException { - return def.isMultiple(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQuery.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQuery.java deleted file mode 100644 index 4510f5f115f..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQuery.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.query.Query; - -import org.apache.jackrabbit.rmi.remote.RemoteQuery; -import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; - -/** - * Remote adapter for the JCR {@link javax.jcr.query.Query Query} interface. - * This class makes a local session available as an RMI service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteQuery RemoteQuery} - * interface. - * - * @author Philipp Koch - * @see javax.jcr.query.Query - * @see org.apache.jackrabbit.rmi.remote.RemoteQuery - */ -public class ServerQuery extends ServerObject implements RemoteQuery { - - /** The adapted local query manager. */ - private Query query; - - /** - * Creates a remote adapter for the given local Query. - * - * @param query local Query - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerQuery(Query query, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.query = query; - } - - /** {@inheritDoc} */ - public RemoteQueryResult execute() - throws RepositoryException, RemoteException { - return new ServerQueryResult(query.execute(), getFactory()); - } - - /** {@inheritDoc} */ - public String getStatement() throws RemoteException { - return query.getStatement(); - } - - /** {@inheritDoc} */ - public String getLanguage() throws RemoteException { - return query.getLanguage(); - } - - /** {@inheritDoc} */ - public String getPersistentQueryPath() - throws RepositoryException, RemoteException { - return query.getPersistentQueryPath(); - } - - /** {@inheritDoc} */ - public void save(String absPath) - throws RepositoryException, RemoteException { - query.save(absPath); - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQueryManager.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQueryManager.java deleted file mode 100644 index 26ce7f032c7..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQueryManager.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.Node; -import javax.jcr.RepositoryException; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; - -import org.apache.jackrabbit.rmi.remote.RemoteQuery; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; - -/** - * Remote adapter for the JCR {@link javax.jcr.query.QueryManager QueryManager} - * interface. This class makes a local query manager available as an RMI - * service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteQueryManager RemoteQueryManager} - * interface. - * - * @author Philipp Koch - * @see javax.jcr.query.QueryManager - * @see org.apache.jackrabbit.rmi.remote.RemoteQueryManager - */ -public class ServerQueryManager extends ServerObject - implements RemoteQueryManager { - - /** The adapted local query manager. */ - private QueryManager manager; - - /** - * Creates a remote adapter for the given local query manager. - * - * @param manager local query manager - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerQueryManager( - QueryManager manager, ServerAdapterFactory factory) - throws RemoteException { - super(factory); - this.manager = manager; - } - - /** {@inheritDoc} */ - public RemoteQuery createQuery(String statement, String language) - throws RepositoryException, RemoteException { - try { - Query query = manager.createQuery(statement, language); - return getFactory().getRemoteQuery(query); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteQuery getQuery(String absPath) - throws RepositoryException, RemoteException { - try { - Node node = null; // TODO - return getFactory().getRemoteQuery(manager.getQuery(node)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getSupportedQueryLanguages() throws RemoteException { - return manager.getSupportedQueryLanguages(); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQueryResult.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQueryResult.java deleted file mode 100644 index 60616929934..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerQueryResult.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.NodeIterator; -import javax.jcr.RepositoryException; -import javax.jcr.query.QueryResult; -import javax.jcr.query.RowIterator; - -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; -import org.apache.jackrabbit.rmi.remote.RemoteRow; - -/** - * Remote adapter for the JCR {@link javax.jcr.query.QueryResult QueryResult} interface. - * This class makes a local session available as an RMI service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteQueryResult RemoteQueryResult} - * interface. - * - * @author Philipp Koch - * @see javax.jcr.query.QueryResult - * @see org.apache.jackrabbit.rmi.remote.RemoteQueryResult - */ -public class ServerQueryResult extends ServerObject - implements RemoteQueryResult { - - /** The adapted local query result. */ - private QueryResult result; - - /** - * Creates a remote adapter for the given local QueryResult. - * - * @param result local QueryResult - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerQueryResult(QueryResult result, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.result = result; - } - - /** {@inheritDoc} */ - public String[] getPropertyNames() - throws RepositoryException, RemoteException { - return result.getPropertyNames(); - } - - /** {@inheritDoc} */ - public RemoteRow[] getRows() throws RepositoryException, RemoteException { - RowIterator iterator = result.getRows(); - if (iterator != null) { - RemoteRow[] remotes = new RemoteRow[(int) iterator.getSize()]; - for (int i = 0; iterator.hasNext(); i++) { - remotes[i] = getFactory().getRemoteRow(iterator.nextRow()); - } - return remotes; - } else { - return new RemoteRow[0]; // for safety - } - } - - /** {@inheritDoc} */ - public RemoteNode[] getNodes() throws RepositoryException, RemoteException { - NodeIterator iterator = result.getNodes(); - if (iterator != null) { - RemoteNode[] remotes = new RemoteNode[(int) iterator.getSize()]; - for (int i = 0; iterator.hasNext(); i++) { - remotes[i] = getRemoteNode(iterator.nextNode()); - } - return remotes; - } else { - return new RemoteNode[0]; // for safety - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerRepository.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerRepository.java deleted file mode 100644 index abc1425ae34..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerRepository.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.Credentials; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; - -import org.apache.jackrabbit.rmi.remote.RemoteRepository; -import org.apache.jackrabbit.rmi.remote.RemoteSession; - -/** - * Remote adapter for the JCR {@link javax.jcr.Repository Repository} - * interface. This class makes a local repository available as an RMI service - * using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteRepository RemoteRepository} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.Repository - * @see org.apache.jackrabbit.rmi.remote.RemoteRepository - */ -public class ServerRepository extends ServerObject implements RemoteRepository { - - /** The adapted local repository. */ - private Repository repository; - - /** - * Creates a remote adapter for the given local repository. - * - * @param repository local repository - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerRepository( - Repository repository, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.repository = repository; - } - - /** {@inheritDoc} */ - public String getDescriptor(String name) throws RemoteException { - return repository.getDescriptor(name); - } - - /** {@inheritDoc} */ - public String[] getDescriptorKeys() throws RemoteException { - return repository.getDescriptorKeys(); - } - - /** {@inheritDoc} */ - public RemoteSession login() throws RepositoryException, RemoteException { - try { - Session session = repository.login(); - return getFactory().getRemoteSession(session); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteSession login(String workspace) - throws RepositoryException, RemoteException { - try { - Session session = repository.login(workspace); - return getFactory().getRemoteSession(session); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteSession login(Credentials credentials) - throws RepositoryException, RemoteException { - try { - Session session = repository.login(credentials); - return getFactory().getRemoteSession(session); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteSession login(Credentials credentials, String workspace) - throws RepositoryException, RemoteException { - try { - Session session = repository.login(credentials, workspace); - return getFactory().getRemoteSession(session); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerRow.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerRow.java deleted file mode 100644 index 99c2c913a33..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerRow.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.Value; -import javax.jcr.query.Row; - -import org.apache.jackrabbit.rmi.remote.RemoteRow; - -/** - * Remote adapter for the JCR {@link javax.jcr.query.Row Row} interface. - * This class makes a local session available as an RMI service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteRow RemoteRow} - * interface. - * - * @author Philipp Koch - * @see javax.jcr.query.Row - * @see org.apache.jackrabbit.rmi.remote.RemoteRow - */ -public class ServerRow extends ServerObject implements RemoteRow { - - /** The adapted local row. */ - private Row row; - - /** - * Creates a remote adapter for the given local query row. - * - * @param row local query row - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerRow(Row row, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.row = row; - } - - /** {@inheritDoc} */ - public Value[] getValues() throws RepositoryException, RemoteException { - return row.getValues(); - } - - /** {@inheritDoc} */ - public Value getValue(String propertyName) - throws RepositoryException, RemoteException { - return row.getValue(propertyName); - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerSession.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerSession.java deleted file mode 100644 index 72c327758fc..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerSession.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.rmi.RemoteException; -import java.security.AccessControlException; - -import javax.jcr.Credentials; -import javax.jcr.RepositoryException; -import javax.jcr.Session; - -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteSession; -import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; - -/** - * Remote adapter for the JCR {@link javax.jcr.Session Session} interface. - * This class makes a local session available as an RMI service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteSession RemoteSession} - * interface. - * - * @author Jukka Zitting - * @see javax.jcr.Session - * @see org.apache.jackrabbit.rmi.remote.RemoteSession - */ -public class ServerSession extends ServerObject implements RemoteSession { - - /** The adapted local session. */ - private Session session; - - /** - * Creates a remote adapter for the given local session. - * - * @param session local session - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerSession(Session session, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.session = session; - } - - /** {@inheritDoc} */ - public String getUserId() throws RemoteException { - return session.getUserId(); - } - - /** {@inheritDoc} */ - public Object getAttribute(String name) throws RemoteException { - return session.getAttribute(name); - } - - /** {@inheritDoc} */ - public String[] getAttributeNames() throws RemoteException { - return session.getAttributeNames(); - } - - /** {@inheritDoc} */ - public RemoteSession impersonate(Credentials credentials) - throws RepositoryException, RemoteException { - try { - Session newSession = session.impersonate(credentials); - return getFactory().getRemoteSession(newSession); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteWorkspace getWorkspace() throws RemoteException { - return getFactory().getRemoteWorkspace(session.getWorkspace()); - } - - /** {@inheritDoc} */ - public void checkPermission(String path, String actions) - throws AccessControlException, RemoteException { - session.checkPermission(path, actions); - } - - /** {@inheritDoc} */ - public String getNamespacePrefix(String uri) - throws RepositoryException, RemoteException { - try { - return session.getNamespacePrefix(uri); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getNamespacePrefixes() - throws RepositoryException, RemoteException { - try { - return session.getNamespacePrefixes(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String getNamespaceURI(String prefix) - throws RepositoryException, RemoteException { - try { - return session.getNamespaceURI(prefix); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void setNamespacePrefix(String prefix, String uri) - throws RepositoryException, RemoteException { - try { - session.setNamespacePrefix(prefix, uri); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean itemExists(String path) throws RemoteException { - return session.itemExists(path); - } - - /** {@inheritDoc} */ - public RemoteNode getNodeByUUID(String uuid) - throws RepositoryException, RemoteException { - try { - return getRemoteNode(session.getNodeByUUID(uuid)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNode getRootNode() - throws RepositoryException, RemoteException { - try { - return getRemoteNode(session.getRootNode()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteItem getItem(String path) - throws RepositoryException, RemoteException { - try { - return getRemoteItem(session.getItem(path)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasPendingChanges() - throws RepositoryException, RemoteException { - try { - return session.hasPendingChanges(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void move(String from, String to) - throws RepositoryException, RemoteException { - try { - session.move(from, to); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void save() throws RepositoryException, RemoteException { - try { - session.save(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void refresh(boolean keepChanges) - throws RepositoryException, RemoteException { - try { - session.refresh(keepChanges); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void logout() throws RemoteException { - session.logout(); - } - - /** {@inheritDoc} */ - public void importXML(String path, byte[] xml) - throws IOException, RepositoryException, RemoteException { - try { - session.importXML(path, new ByteArrayInputStream(xml)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void addLockToken(String token) throws RemoteException { - session.addLockToken(token); - } - - /** {@inheritDoc} */ - public String[] getLockTokens() throws RemoteException { - return session.getLockTokens(); - } - - /** {@inheritDoc} */ - public void removeLockToken(String token) throws RemoteException { - session.removeLockToken(token); - } - - /** {@inheritDoc} */ - public byte[] exportDocView( - String path, boolean binaryAsLink, boolean noRecurse) - throws IOException, RepositoryException, RemoteException { - try { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - session.exportDocView(path, buffer, binaryAsLink, noRecurse); - return buffer.toByteArray(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public byte[] exportSysView( - String path, boolean binaryAsLink, boolean noRecurse) - throws IOException, RepositoryException, RemoteException { - try { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - session.exportSysView(path, buffer, binaryAsLink, noRecurse); - return buffer.toByteArray(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerVersion.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerVersion.java deleted file mode 100644 index b2063b4ee30..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerVersion.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; -import java.util.Calendar; - -import javax.jcr.RepositoryException; -import javax.jcr.version.Version; - -import org.apache.jackrabbit.rmi.remote.RemoteVersion; - -/** - * Remote adapter for the JCR {@link javax.jcr.version.Version Version} interface. - * This class makes a local version available as an RMI service using - * the {@link org.apache.jackrabbit.rmi.remote.RemoteVersion RemoteVersion} - * interface. - * - * @author Felix Meschberger - * @see javax.jcr.version.Version - * @see org.apache.jackrabbit.rmi.remote.RemoteVersion - */ -public class ServerVersion extends ServerNode implements RemoteVersion { - - /** The adapted local version. */ - private Version version; - - /** - * Creates a remote adapter for the given local version. - * - * @param version local version - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerVersion(Version version, RemoteAdapterFactory factory) - throws RemoteException { - super(version, factory); - this.version = version; - } - -// This is only available after 0.16.2 -// /** {@inheritDoc} */ -// public RemoteVersionHistory getContainingHistory() throws RepositoryException { -// try { -// return getFactory().getRemoteVersionHistory(version.getContainingHistory()); -// } catch (RepositoryException ex) { -// throw getRepositoryException(ex); -// } -// } - - /** {@inheritDoc} */ - public Calendar getCreated() throws RepositoryException { - try { - return version.getCreated(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteVersion[] getSuccessors() throws RepositoryException, RemoteException { - try { - return getRemoteVersionArray(version.getSuccessors()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteVersion[] getPredecessors() throws RepositoryException, RemoteException { - try { - return getRemoteVersionArray(version.getPredecessors()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerVersionHistory.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerVersionHistory.java deleted file mode 100644 index 3d8d63c769c..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerVersionHistory.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; - -import org.apache.jackrabbit.rmi.remote.RemoteVersion; -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; - -/** - * Remote adapter for the JCR {@link javax.jcr.version.VersionHistory VersionHistory} - * interface. This class makes a local version history available as an RMI - * service using the - * {@link org.apache.jackrabbit.rmi.remote.RemoteVersionHistory RemoteVersionHistory} - * interface. - * - * @author Felix Meschberger - * @see javax.jcr.version.VersionHistory - * @see org.apache.jackrabbit.rmi.remote.RemoteVersionHistory - */ -public class ServerVersionHistory extends ServerNode - implements RemoteVersionHistory { - - /** The adapted local version history. */ - private VersionHistory versionHistory; - - /** - * Creates a remote adapter for the given local version history. - * - * @param versionHistory local version history - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerVersionHistory(VersionHistory versionHistory, - RemoteAdapterFactory factory) throws RemoteException { - super(versionHistory, factory); - this.versionHistory = versionHistory; - } - - /** {@inheritDoc} */ -// public String getVersionableUUID() -// throws RepositoryException, RemoteException { -// try { -// return versionHistory.getVersionableUUID(); -// } catch (RepositoryException ex) { -// throw getRepositoryException(ex); -// } -// } - - /** {@inheritDoc} */ - public RemoteVersion getRootVersion() - throws RepositoryException, RemoteException { - try { - return getFactory().getRemoteVersion(versionHistory.getRootVersion()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteVersion[] getAllVersions() - throws RepositoryException, RemoteException { - try { - return getRemoteVersionArray(versionHistory.getAllVersions()); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteVersion getVersion(String versionName) - throws RepositoryException, RemoteException { - try { - Version version = versionHistory.getVersion(versionName); - return getFactory().getRemoteVersion(version); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteVersion getVersionByLabel(String label) - throws RepositoryException, RemoteException { - try { - Version version = versionHistory.getVersionByLabel(label); - return getFactory().getRemoteVersion(version); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void addVersionLabel(String versionName, String label, - boolean moveLabel) throws RepositoryException, RemoteException { - try { - versionHistory.addVersionLabel(versionName, label, moveLabel); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void removeVersionLabel(String label) throws RepositoryException, - RemoteException { - try { - versionHistory.removeVersionLabel(label); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public boolean hasVersionLabel(String label) throws RemoteException { - return versionHistory.hasVersionLabel(label); - } - - /** {@inheritDoc} */ - public boolean hasVersionLabel(String versionUUID, String label) - throws RepositoryException, RemoteException { - try { - Version version = getVersionByUUID(versionUUID); - return versionHistory.hasVersionLabel(version, label); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getVersionLabels() throws RemoteException { - return versionHistory.getVersionLabels(); - } - - /** {@inheritDoc} */ - public String[] getVersionLabels(String versionUUID) - throws RepositoryException, RemoteException { - try { - Version version = getVersionByUUID(versionUUID); - return versionHistory.getVersionLabels(version); - } catch (ClassCastException cce) { - // we do not expect this here as nodes should be returned correctly - throw getRepositoryException(new RepositoryException(cce)); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void removeVersion(String versionName) - throws RepositoryException, RemoteException { - try { - versionHistory.removeVersion(versionName); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerWorkspace.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerWorkspace.java deleted file mode 100644 index 0aabda47186..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerWorkspace.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.server; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.rmi.RemoteException; - -import javax.jcr.NamespaceRegistry; -import javax.jcr.RepositoryException; -import javax.jcr.Workspace; -import javax.jcr.nodetype.NodeTypeManager; -import javax.jcr.query.QueryManager; - -import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; -import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; - -/** - * Remote adapter for the JCR {@link Workspace Workspace} interface. - * This class makes a local workspace available as an RMI service using the - * {@link RemoteWorkspace RemoteWorkspace} interface. - * - * @see Workspace - * @see RemoteWorkspace - */ -public class ServerWorkspace extends ServerObject implements RemoteWorkspace { - - /** The adapted local workspace. */ - private Workspace workspace; - - /** - * Creates a remote adapter for the given local workspace. - * - * @param workspace local workspace - * @param factory remote adapter factory - * @throws RemoteException on RMI errors - */ - public ServerWorkspace(Workspace workspace, RemoteAdapterFactory factory) - throws RemoteException { - super(factory); - this.workspace = workspace; - } - - /** {@inheritDoc} */ - public String getName() throws RemoteException { - return workspace.getName(); - } - - /** {@inheritDoc} */ - public void copy(String from, String to) - throws RepositoryException, RemoteException { - try { - workspace.copy(from, to); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void copy(String from, String to, String workspace) - throws RepositoryException, RemoteException { - try { - this.workspace.copy(from, to, workspace); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void clone( - String workspace, String from, String to, boolean removeExisting) - throws RepositoryException, RemoteException { - try { - this.workspace.clone(workspace, from, to, removeExisting); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void move(String from, String to) - throws RepositoryException, RemoteException { - try { - workspace.move(from, to); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNodeTypeManager getNodeTypeManager() - throws RepositoryException, RemoteException { - try { - NodeTypeManager manager = workspace.getNodeTypeManager(); - return getFactory().getRemoteNodeTypeManager(manager); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteNamespaceRegistry getNamespaceRegistry() - throws RepositoryException, RemoteException { - try { - NamespaceRegistry registry = workspace.getNamespaceRegistry(); - return getFactory().getRemoteNamespaceRegistry(registry); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public RemoteQueryManager getQueryManager() - throws RepositoryException, RemoteException { - try { - QueryManager queryManager = workspace.getQueryManager(); - return getFactory().getRemoteQueryManager(queryManager); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getAccessibleWorkspaceNames() - throws RepositoryException, RemoteException { - try { - return workspace.getAccessibleWorkspaceNames(); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public void importXML(String path, byte[] xml, int uuidBehaviour) - throws IOException, RepositoryException, RemoteException { - try { - workspace.importXML( - path, new ByteArrayInputStream(xml), uuidBehaviour); - } catch (RepositoryException ex) { - throw getRepositoryException(ex); - } - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/package.html b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/package.html deleted file mode 100644 index 9244e3fd966..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/package.html +++ /dev/null @@ -1,64 +0,0 @@ - -Server implementation of the transparent JCR-RMI layer. -

-This package contains the default server implementation of the -transparent JCR-RMI layer. The classes in this package can be used -to make a local JCR repository available as an RMI service. In addition, -this package offers a straightforward mechanism for extending or modifying -the behaviour of the server layer. -

-The contents of this package is designed using two design patterns, -Factory and Adapter. All the remotely accessible ServerObject subclasses -implement the Adapter pattern to adapt a local JCR interface to the -corresponding remote JCR-RMI interface. The Factory pattern is used -to centralize the creation and configuration of all adapter instances. - -

Setting up a JCR-RMI server

-

-Setting up the server part of the JCR-RMI layer is quite straightforward. -After instantiating a local JCR repository you need to wrap it into a -remote adapter and create an RMI binding for the repository. A variation -of the following code is usually all that is needed in addition to the -standard RMI setup (starting rmiregistry, etc.): - -

-    Repository repository = ...; // The local repository
-    String name = ...; // The RMI URL for the repository
-    
-    RemoteAdapterFactory factory = new ServerAdapterFactory();
-    RemoteRepository remote = factory.getRemoteRepository(repository);
-    Naming.bind(name, remote);  // Make the RMI binding using java.rmi.Naming
-
- -

Extending the JCR-RMI server

-

-The Factory pattern used by this package makes it easy to extend -the behaviour of the JCR-RMI server. Such changes in behaviour or policy -can be implemented by modifying or replacing the default -ServerAdapterFactory used in the example above. -

-The following example code adds transparent logging of all session logins -and logouts: - -

-    Repository repository = ...; // The local repository
-    String name = ...; // The RMI URL for the repository
-    
-    RemoteAdapterFactory factory = new ServerAdapterFactory() {
-        public RemoteSession getRemoteSession(Session session)
-                throws RemoteException {
-            System.out.println("LOGIN: " + session.getUserId());
-            return new ServerSession(session, this) {
-                public void logout() {
-                    System.out.println("LOGOUT: " + session.getUserId());
-                    super.logout();
-                }
-            };
-        }
-    };
-
-    RemoteRepository remote = factory.getRemoteRepository(repository);
-    Naming.bind(name, remote);  // Make the RMI binding using java.rmi.Naming
-
- - \ No newline at end of file diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/ImportContentHandler.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/ImportContentHandler.java deleted file mode 100644 index ef620f94e91..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/ImportContentHandler.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.xml; - -import java.io.ByteArrayOutputStream; - -import org.apache.xml.serialize.OutputFormat; -import org.apache.xml.serialize.XMLSerializer; -import org.xml.sax.Attributes; -import org.xml.sax.ContentHandler; -import org.xml.sax.Locator; -import org.xml.sax.SAXException; - -/** - * Base class for a SAX content handler for importing XML data. This - * class provides a general mechanism for converting a SAX event stream - * to raw XML data and feeding the received byte array into an import - * method. Subclasses can provide different import mechanisms simply by - * implementing the abstract {@link #importXML(byte[]) importXML(byte[])} - * method. - * - * @author Jukka Zitting - */ -public abstract class ImportContentHandler implements ContentHandler { - - /** Internal buffer for the XML byte stream. */ - private ByteArrayOutputStream buffer; - - /** The internal XML serializer. */ - private ContentHandler handler; - - /** - * Creates a SAX content handler for importing XML data. - */ - public ImportContentHandler() { - this.buffer = new ByteArrayOutputStream(); - this.handler = new XMLSerializer(buffer, new OutputFormat()); - } - - /** - * Imports the given XML data. This method is called by the - * {@link #endDocument() endDocument()} method after the received - * XML stream has been serialized. - *

- * Subclasses must implement this method to provide the actual - * import mechanism. - * - * @param xml the XML data to import - * @throws Exception on import errors - */ - protected abstract void importXML(byte[] xml) throws Exception; - - /** {@inheritDoc} */ - public void setDocumentLocator(Locator locator) { - handler.setDocumentLocator(locator); - } - - /** {@inheritDoc} */ - public void startDocument() throws SAXException { - handler.startDocument(); - } - - /** {@inheritDoc} */ - public void endDocument() throws SAXException { - handler.endDocument(); - try { - importXML(buffer.toByteArray()); - } catch (Exception ex) { - throw new SAXException(ex); - } - } - - /** {@inheritDoc} */ - public void startPrefixMapping(String prefix, String uri) - throws SAXException { - handler.startPrefixMapping(prefix, uri); - } - - /** {@inheritDoc} */ - public void endPrefixMapping(String prefix) throws SAXException { - handler.endPrefixMapping(prefix); - } - - /** {@inheritDoc} */ - public void startElement(String uri, String localName, String qName, - Attributes atts) throws SAXException { - handler.startElement(uri, localName, qName, atts); - } - - /** {@inheritDoc} */ - public void endElement(String uri, String localName, String qName) - throws SAXException { - handler.endElement(uri, localName, qName); - } - - /** {@inheritDoc} */ - public void characters(char[] ch, int start, int length) - throws SAXException { - handler.characters(ch, start, length); - } - - /** {@inheritDoc} */ - public void ignorableWhitespace(char[] ch, int start, int length) - throws SAXException { - handler.ignorableWhitespace(ch, start, length); - } - - /** {@inheritDoc} */ - public void processingInstruction(String target, String data) - throws SAXException { - handler.processingInstruction(target, data); - } - - /** {@inheritDoc} */ - public void skippedEntity(String name) throws SAXException { - handler.skippedEntity(name); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/SessionImportContentHandler.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/SessionImportContentHandler.java deleted file mode 100644 index 8f3c57307bb..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/SessionImportContentHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.xml; - -import java.io.ByteArrayInputStream; - -import javax.jcr.Session; - - -/** - * SAX content handler for importing XML data to a JCR {@link Session Session}. - * This utility class can be used to implement the - * {@link Session#getImportContentHandler(String) Session.getImportContentHandler(String)} - * method in terms of the - * {@link Session#importXML(String, java.io.InputStream) Session.importXML(String, InputStream)} - * method. - * - * @author Jukka Zitting - */ -public class SessionImportContentHandler extends ImportContentHandler { - - /** The repository session. */ - private Session session; - - /** The import content path. */ - private String path; - - /** - * Creates a SAX content handler for importing XML data to the given - * session and path. - * - * @param session repository session - * @param path import content path - */ - public SessionImportContentHandler(Session session, String path) { - this.session = session; - this.path = path; - } - - /** - * Imports the serialized XML stream using the standard - * {@link Session#importXML(String, java.io.InputStream) Session.importXML(String, InputStream)} - * method. - * - * {@inheritDoc} - */ - protected void importXML(byte[] xml) throws Exception { - session.importXML(path, new ByteArrayInputStream(xml)); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/WorkspaceImportContentHandler.java b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/WorkspaceImportContentHandler.java deleted file mode 100644 index 9fcfd428c85..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/WorkspaceImportContentHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.xml; - -import java.io.ByteArrayInputStream; - -import javax.jcr.Workspace; - - -/** - * SAX content handler for importing XML data to a JCR {@link Workspace Workspace}. - * This utility class can be used to implement the - * {@link Workspace#getImportContentHandler(String, int) Workspace.getImportContentHandler(String, int)} - * method in terms of the - * {@link Workspace#importXML(String, java.io.InputStream, int) Workspace.importXML(String, InputStream, int)} - * method. - * - * @author Jukka Zitting - */ -public class WorkspaceImportContentHandler extends ImportContentHandler { - - /** The repository workspace. */ - private Workspace workspace; - - /** The import content path. */ - private String path; - - /** The UUID behaviour. */ - private int uuidBehaviour; - - /** - * Creates a SAX content handler for importing XML data to the given - * workspace and path using the given UUID behaviour. - * - * @param workspace repository workspace - * @param path import content path - * @param uuidBehaviour UUID behaviour - */ - public WorkspaceImportContentHandler(Workspace workspace, String path, int uuidBehaviour) { - this.workspace = workspace; - this.path = path; - this.uuidBehaviour = uuidBehaviour; - } - - /** - * Imports the serialized XML stream using the standard - * {@link Workspace#importXML(String, java.io.InputStream, int) Workspace.importXML(String, InputStream, int)} - * method. - * - * {@inheritDoc} - */ - protected void importXML(byte[] xml) throws Exception { - workspace.importXML(path, new ByteArrayInputStream(xml), uuidBehaviour); - } - -} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/package.html b/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/package.html deleted file mode 100644 index 956e5ecb042..00000000000 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/xml/package.html +++ /dev/null @@ -1,15 +0,0 @@ - -Utility classes for importing SAX event streams. -

-The classes in this package can be used for to implement the JCR -{@link javax.jcr.Session#getImportContentHandler(java.lang.String) Session.getImportContentHandler(String)} -and -{@link javax.jcr.Workspace#getImportContentHandler(java.lang.String, int) Workspace.getImportContentHandler(String, int)} -methods in terms of the corresponding importXML() methods. -

-These utility classes were designed for the transparent JCR-RMI layer, -but can easily be used as a part of any JCR repository implementation. -The public interface of this package depends only on the standard JCR and -J2SE APIs. The implementation uses Xerces as an extra dependency to -serialize the SAX event streams. - diff --git a/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/RemoteAdapterTest.java b/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/RemoteAdapterTest.java deleted file mode 100644 index e54279bcc45..00000000000 --- a/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/RemoteAdapterTest.java +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.test.rmi; - -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import javax.jcr.Item; -import javax.jcr.NamespaceRegistry; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.Session; -import javax.jcr.Workspace; -import javax.jcr.lock.Lock; -import javax.jcr.nodetype.ItemDef; -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.NodeTypeManager; -import javax.jcr.nodetype.PropertyDef; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; -import javax.jcr.query.QueryResult; -import javax.jcr.query.Row; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; - -import junit.framework.TestCase; - -import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; -import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; -import org.apache.jackrabbit.rmi.remote.RemoteItem; -import org.apache.jackrabbit.rmi.remote.RemoteItemDef; -import org.apache.jackrabbit.rmi.remote.RemoteLock; -import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; -import org.apache.jackrabbit.rmi.remote.RemoteNode; -import org.apache.jackrabbit.rmi.remote.RemoteNodeDef; -import org.apache.jackrabbit.rmi.remote.RemoteNodeType; -import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; -import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.RemoteQuery; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; -import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; -import org.apache.jackrabbit.rmi.remote.RemoteRepository; -import org.apache.jackrabbit.rmi.remote.RemoteRow; -import org.apache.jackrabbit.rmi.remote.RemoteSession; -import org.apache.jackrabbit.rmi.remote.RemoteVersion; -import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; -import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; -import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; -import org.apache.jackrabbit.rmi.server.ServerAdapterFactory; -import org.easymock.MockControl; - -/** - * Tests for the adapter classes of JCR-RMI. These tests use reflection - * to invoke all methods of an adapter object and to verify that the - * corresponding methods of the underlying object get called by the adapter. - */ -public class RemoteAdapterTest extends TestCase { - - /** Factory for creating remote test adapters. */ - private RemoteAdapterFactory remoteFactory; - - /** Factory for creating local test adapters. */ - private LocalAdapterFactory localFactory; - - /** Map of method names to method descriptions. */ - private Map methods; - - /** Mock controller. */ - private MockControl control; - - /** Mock object. */ - private Object mock; - - /** - * Prepares the automated test suite to adapters of the given interface. - * - * @param iface adapter interface - * @throws Exception on errors - */ - private void prepareTests(Class iface) throws Exception { - remoteFactory = new ServerAdapterFactory(); - localFactory = new ClientAdapterFactory(); - - methods = new HashMap(); - Method[] m = iface.getDeclaredMethods(); - for (int i = 0; i < m.length; i++) { - methods.put(m[i].getName(), m[i]); - } - - control = MockControl.createControl(iface); - mock = control.getMock(); - } - - /** - * Removes the identified method from the automatic tests. - * - * @param name method name - */ - private void ignoreMethod(String name) { - methods.remove(name); - } - - /** - * Returns a parameter array for the given method. - * - * @param method method description - * @return parameter array - */ - private Object[] getParameters(Method method) { - Class[] types = method.getParameterTypes(); - Object[] parameters = new Object[types.length]; - for (int i = 0; i < types.length; i++) { - if (!types[i].isPrimitive()) { - parameters[i] = null; - } else if ("int".equals(types[i].getName())) { - parameters[i] = new Integer(0); - } else if ("boolean".equals(types[i].getName())) { - parameters[i] = new Boolean(false); - } else { - System.out.println(types[i].getName()); - parameters[i] = null; - } - } - return parameters; - } - - /** - * Sets the expected return value for the given method. - * - * @param method method description - * @param control mock controller - */ - private void setReturnValue(Method method, MockControl control) { - Class type = method.getReturnType(); - if (!type.isPrimitive()) { - control.setReturnValue(null); - } else if ("void".equals(type.getName())) { - control.setVoidCallable(); - } else if ("int".equals(type.getName())) { - control.setReturnValue((int) 0); - } else if ("long".equals(type.getName())) { - control.setReturnValue((long) 0); - } else if ("boolean".equals(type.getName())) { - control.setReturnValue(false); - } else { - System.out.println(type.getName()); - control.setReturnValue(null); - } - } - - /** - * Runs the automatic test suite on the given adapter instance. - * - * @param adapter adapter instance - * @throws Exception on errors - */ - private void runTests(Object adapter) throws Exception { - Iterator iterator = methods.values().iterator(); - while (iterator.hasNext()) { - Method method = (Method) iterator.next(); - Object[] parameters = getParameters(method); - - method.invoke(mock, parameters); - setReturnValue(method, control); - control.replay(); - - method.invoke(adapter, parameters); - control.verify(); - - control.reset(); - } - } - - /** - * Tests Repository adapters. - * - * @throws Exception on errors - */ - public void testRepository() throws Exception { - prepareTests(Repository.class); - - Repository repository = (Repository) mock; - RemoteRepository remote = remoteFactory.getRemoteRepository(repository); - Repository local = localFactory.getRepository(remote); - - runTests(local); - } - - /** - * Tests Session adapters. - * - * @throws Exception on errors - */ - public void testSession() throws Exception { - prepareTests(Session.class); - ignoreMethod("getRepository"); // implemented locally - ignoreMethod("importXML"); // wrapped stream - ignoreMethod("getImportContentHandler"); // implemented locally - ignoreMethod("exportSysView"); // multiple methods - ignoreMethod("exportDocView"); // multiple method - - Session session = (Session) mock; - RemoteSession remote = remoteFactory.getRemoteSession(session); - Session local = localFactory.getSession(null, remote); - - runTests(local); - } - - /** - * Tests Item adapters. - * - * @throws Exception on errors - */ - public void testItem() throws Exception { - prepareTests(Item.class); - ignoreMethod("accept"); // implemented in subclasses - ignoreMethod("getSession"); // implemented locally - ignoreMethod("isNode"); // implemented in subclasses - ignoreMethod("isSame"); // implemented locally - - Item item = (Item) mock; - RemoteItem remote = remoteFactory.getRemoteItem(item); - Item local = localFactory.getItem(null, remote); - - runTests(local); - } - - /** - * Tests Node adapters. - * - * @throws Exception on errors - */ - public void testNode() throws Exception { - prepareTests(Node.class); - ignoreMethod("cancelMerge"); // TODO - ignoreMethod("doneMerge"); // TODO - ignoreMethod("checkin"); // TODO - ignoreMethod("restore"); // multiple methods - ignoreMethod("getVersionHistory"); // TODO - ignoreMethod("getBaseVersion"); // TODO - ignoreMethod("setProperty"); // multiple methods - - Node node = (Node) mock; - RemoteNode remote = remoteFactory.getRemoteNode(node); - Node local = localFactory.getNode(null, remote); - - runTests(local); - } - - /** - * Tests Property adapters. - * - * @throws Exception on errors - */ - public void testProperty() throws Exception { - prepareTests(Property.class); - ignoreMethod("getBoolean"); // implemented locally - ignoreMethod("getLong"); // implemented locally - ignoreMethod("getDouble"); // implemented locally - ignoreMethod("getDate"); // implemented locally - ignoreMethod("getString"); // implemented locally - ignoreMethod("getStream"); // implemented locally - ignoreMethod("getNode"); // implemented locally - ignoreMethod("setValue"); // multiple methods - - Property property = (Property) mock; - RemoteProperty remote = remoteFactory.getRemoteProperty(property); - Property local = localFactory.getProperty(null, remote); - - runTests(local); - } - - /** - * Tests Lock adapters. - * - * @throws Exception on errors - */ - public void testLock() throws Exception { - prepareTests(Lock.class); - ignoreMethod("getNode"); // implemented locally - - Lock lock = (Lock) mock; - RemoteLock remote = remoteFactory.getRemoteLock(lock); - Lock local = localFactory.getLock(null, remote); - - runTests(local); - } - - /** - * Tests Workspace adapters. - * - * @throws Exception on errors - */ - public void testWorkspace() throws Exception { - prepareTests(Workspace.class); - ignoreMethod("getObservationManager"); // TODO - ignoreMethod("restore"); // TODO - ignoreMethod("getSession"); // implemented locally - ignoreMethod("copy"); // multiple methods - ignoreMethod("importXML"); // wrapped stream - ignoreMethod("getImportContentHandler"); // implemented locally - - Workspace workspace = (Workspace) mock; - RemoteWorkspace remote = remoteFactory.getRemoteWorkspace(workspace); - Workspace local = localFactory.getWorkspace(null, remote); - - runTests(local); - } - - /** - * Tests NamespaceRegistry adapters. - * - * @throws Exception on errors - */ - public void testNamespaceRegistry() throws Exception { - prepareTests(NamespaceRegistry.class); - - NamespaceRegistry registry = (NamespaceRegistry) mock; - RemoteNamespaceRegistry remote = - remoteFactory.getRemoteNamespaceRegistry(registry); - NamespaceRegistry local = localFactory.getNamespaceRegistry(remote); - - runTests(local); - } - - /** - * Tests NodeTypeManager adapters. - * - * @throws Exception on errors - */ - public void testNodeTypeManager() throws Exception { - prepareTests(NodeTypeManager.class); - - NodeTypeManager manager = (NodeTypeManager) mock; - RemoteNodeTypeManager remote = - remoteFactory.getRemoteNodeTypeManager(manager); - NodeTypeManager local = localFactory.getNodeTypeManager(remote); - - runTests(local); - } - - /** - * Tests NodeType adapters. - * - * @throws Exception on errors - */ - public void testNodeType() throws Exception { - prepareTests(NodeType.class); - ignoreMethod("canSetProperty"); // wrapped Value object - - NodeType type = (NodeType) mock; - RemoteNodeType remote = remoteFactory.getRemoteNodeType(type); - NodeType local = localFactory.getNodeType(remote); - - runTests(local); - } - - /** - * Tests ItemDef adapters. - * - * @throws Exception on errors - */ - public void testItemDef() throws Exception { - prepareTests(ItemDef.class); - - ItemDef def = (ItemDef) mock; - RemoteItemDef remote = remoteFactory.getRemoteItemDef(def); - ItemDef local = localFactory.getItemDef(remote); - - runTests(local); - } - - /** - * Tests NodeDef adapters. - * - * @throws Exception on errors - */ - public void testNodeDef() throws Exception { - prepareTests(NodeDef.class); - - NodeDef def = (NodeDef) mock; - RemoteNodeDef remote = remoteFactory.getRemoteNodeDef(def); - NodeDef local = localFactory.getNodeDef(remote); - - runTests(local); - } - - /** - * Tests PropertyDef adapters. - * - * @throws Exception on errors - */ - public void testPropertyDef() throws Exception { - prepareTests(PropertyDef.class); - - PropertyDef def = (PropertyDef) mock; - RemotePropertyDef remote = remoteFactory.getRemotePropertyDef(def); - PropertyDef local = localFactory.getPropertyDef(remote); - - runTests(local); - } - - /** - * Tests QueryManager adapters. - * - * @throws Exception on errors - */ - public void testQueryManager() throws Exception { - prepareTests(QueryManager.class); - ignoreMethod("getQuery"); // TODO - - QueryManager manager = (QueryManager) mock; - RemoteQueryManager remote = remoteFactory.getRemoteQueryManager(manager); - QueryManager local = localFactory.getQueryManager(null, remote); - - runTests(local); - } - - /** - * Tests Query adapters. - * - * @throws Exception on errors - */ - public void testQuery() throws Exception { - prepareTests(Query.class); - - Query query = (Query) mock; - RemoteQuery remote = remoteFactory.getRemoteQuery(query); - Query local = localFactory.getQuery(null, remote); - - runTests(local); - } - - /** - * Tests QueryResult adapters. - * - * @throws Exception on errors - */ - public void testQueryResult() throws Exception { - prepareTests(QueryResult.class); - - QueryResult result = (QueryResult) mock; - RemoteQueryResult remote = remoteFactory.getRemoteQueryResult(result); - QueryResult local = localFactory.getQueryResult(null, remote); - - runTests(local); - } - - /** - * Tests Row adapters. - * - * @throws Exception on errors - */ - public void testRow() throws Exception { - prepareTests(Row.class); - - Row row = (Row) mock; - RemoteRow remote = remoteFactory.getRemoteRow(row); - Row local = localFactory.getRow(remote); - - runTests(local); - } - - /** - * Tests Version adapters. - * - * @throws Exception on errors - */ - public void testVersion() throws Exception { - prepareTests(Version.class); - - Version version = (Version) mock; - RemoteVersion remote = remoteFactory.getRemoteVersion(version); - Version local = localFactory.getVersion(null, remote); - - runTests(local); - } - - /** - * Tests VersionHistory adapters. - * - * @throws Exception on errors - */ - public void testVersionHistory() throws Exception { - prepareTests(VersionHistory.class); - - VersionHistory history = (VersionHistory) mock; - RemoteVersionHistory remote = - remoteFactory.getRemoteVersionHistory(history); - VersionHistory local = localFactory.getVersionHistory(null, remote); - - runTests(local); - } - -} diff --git a/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/TestAll.java b/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/TestAll.java deleted file mode 100644 index 90c12a25dc3..00000000000 --- a/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/TestAll.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.test.rmi; - -import junit.framework.Test; -import junit.framework.TestSuite; - -/** - * Test suite that contains all JCR-RMI test cases. - */ -public class TestAll { - - /** This class cannot be instantiated. */ - private TestAll() { - } - - /** - * Returns the JCR-RMI test suite. - * - * @return JCR-RMI test suite - */ - public static Test suite() { - TestSuite suite = new TestSuite("Tests for org.apache.jackrabbit.rmi"); - //$JUnit-BEGIN$ - suite.addTestSuite(RemoteAdapterTest.class); - //$JUnit-END$ - return suite; - } - -} diff --git a/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/package.html b/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/package.html deleted file mode 100644 index 92594a1014d..00000000000 --- a/contrib/jcr-rmi/src/test/org/apache/jackrabbit/test/rmi/package.html +++ /dev/null @@ -1,6 +0,0 @@ - -Test cases for the JCR-RMI wrapper. The test cases use the EasyMock library -to generate mock objects that are then wrapped into the remote client-server -wrappers. The test cases then access the client wrappers and verify that the -corresponding operations are performed also on the underlying mock objects. - diff --git a/contrib/jcr-server/LICENSE.txt b/contrib/jcr-server/LICENSE.txt deleted file mode 100644 index d6456956733..00000000000 --- a/contrib/jcr-server/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/contrib/jcr-server/client/maven.xml b/contrib/jcr-server/client/maven.xml deleted file mode 100644 index ef564ff1229..00000000000 --- a/contrib/jcr-server/client/maven.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - diff --git a/contrib/jcr-server/client/project.properties b/contrib/jcr-server/client/project.properties deleted file mode 100644 index 0569e85c372..00000000000 --- a/contrib/jcr-server/client/project.properties +++ /dev/null @@ -1,2 +0,0 @@ -maven.javadoc.links=http://java.sun.com/j2se/1.4.2/docs/api/,http://www.day.com/maven/jsr170/javadocs/jcr-0.16.1-pfd/ -maven.repo.remote = http://www.ibiblio.org/maven/,http://www.day.com/maven/ diff --git a/contrib/jcr-server/client/project.xml b/contrib/jcr-server/client/project.xml deleted file mode 100644 index 856ed641a5e..00000000000 --- a/contrib/jcr-server/client/project.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - ${basedir}/../project.xml - jcr-client - jcr-server - jar - JCRWebdavServer Client Library - - - - - - - jcr-webdav - jcr-server - ${pom.currentVersion} - - - - jsr170 - jcr - 0.16.2 - http://www.day.com/maven/jsr170/jars/jcr-0.16.2.jar - - - jackrabbit - 0.16.2-dev - - - jdom - 1.0 - - - log4j - 1.2.8 - - - servletapi - 2.3 - - - jcr-rmi - 0.16.2 - - - - - - - - - - ${basedir}/src/java - - - src/java - - **/*.xml - **/*.xsd - **/*.properties - **/*.dtd - - - - - - diff --git a/contrib/jcr-server/client/src/java/org/apache/jackrabbit/client/RepositoryAccessServlet.java b/contrib/jcr-server/client/src/java/org/apache/jackrabbit/client/RepositoryAccessServlet.java deleted file mode 100644 index e450f91358e..00000000000 --- a/contrib/jcr-server/client/src/java/org/apache/jackrabbit/client/RepositoryAccessServlet.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.client; - -import org.apache.log4j.Logger; -import org.apache.log4j.PropertyConfigurator; -import org.apache.jackrabbit.rmi.client.ClientRepositoryFactory; -import org.apache.jackrabbit.core.util.Base64; - -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.ServletException; -import javax.jcr.*; -import javax.naming.InitialContext; -import javax.naming.NamingException; - -import java.util.Properties; -import java.util.Enumeration; -import java.io.IOException; -import java.io.InputStream; -import java.io.ByteArrayOutputStream; -import java.rmi.RemoteException; -import java.rmi.NotBoundException; -import java.net.MalformedURLException; - -/** - * This Class implements a servlet that is used as unified mechanism to retrieve - * a jcr repository either through JNID, RMI or JCRWebdavServer. - */ -public class RepositoryAccessServlet extends HttpServlet { - - /** default logger */ - private static Logger log; - - // todo: implement correctly - public final static String INIT_PARAM_LOG4J_CONFIG = "log4j-config"; - - /** the 'repository-name' init parameter */ - public final static String INIT_PARAM_REPOSITORY_NAME = "repository-name"; - - /** the 'rmi-uri' init parameter */ - public final static String INIT_PARAM_RMI_URI = "rmi-uri"; - - /** Authorization header name */ - private static final String HEADER_AUTHORIZATION = "Authorization"; - - /** the configured repository name */ - private static String repositoryName; - - private static String rmiURI; - - private static InitialContext jndiContext; - - private static Repository repository; - - /** - * Initializes this servlet - * - * @throws javax.servlet.ServletException - */ - public void init() throws ServletException { - initLog4J(); - log.info("RepositoryAccessServlet initializing..."); - initJNDI(); - initRMI(); - initRepository(); - log.info("RepositoryAccessServlet initialized."); - } - - private void initLog4J() throws ServletException { - // setup log4j - String log4jConfig = getServletConfig().getInitParameter(INIT_PARAM_LOG4J_CONFIG); - InputStream in =getServletContext().getResourceAsStream(log4jConfig); - if (in==null) { - // try normal init - PropertyConfigurator.configure(log4jConfig); - } else { - try { - Properties log4jProperties = new Properties(); - log4jProperties.load(in); - in.close(); - PropertyConfigurator.configure(log4jProperties); - } catch (IOException e) { - throw new ServletException("Unable to load log4jProperties: " + e.toString()); - } - } - log = Logger.getLogger(RepositoryAccessServlet.class); - } - - private void initJNDI() throws ServletException { - // setup repository name - repositoryName = getServletConfig().getInitParameter(INIT_PARAM_REPOSITORY_NAME); - if (repositoryName==null) { - repositoryName="default"; - } - log.info(" repository-name = " + repositoryName); - - // retrieve JNDI Context environment - try { - Properties env = new Properties(); - Enumeration names = getServletConfig().getInitParameterNames(); - while (names.hasMoreElements()) { - String name = (String) names.nextElement(); - if (name.startsWith("java.naming.")) { - env.put(name, getServletConfig().getInitParameter(name)); - log.info(" adding property to JNDI environment: " + name + "=" + env.getProperty(name)); - } - } - jndiContext = new InitialContext(env); - } catch (NamingException e) { - log.error("Create initial context: " + e.toString()); - throw new ServletException(e); - } - } - - private void initRMI() { - // setup repository name - rmiURI = getServletConfig().getInitParameter(INIT_PARAM_RMI_URI); - if (rmiURI != null) { - log.info(" rmi-uri = " + rmiURI); - } - } - - /** - * tries to retrieve the repository - */ - private void initRepository() throws ServletException { - getRepositoryByRMI(); - if (repository == null) { - getRepositoryByJNDI(); - } - if (repository == null) { - log.error("Unable to retrieve repository"); - throw new ServletException("Unable to retrieve repository"); - } - log.info(repository.getDescriptor(Repository.REP_NAME_DESC) + " " + repository.getDescriptor(Repository.REP_VERSION_DESC)); - } - - /** - * tries to retrieve the repository using RMI - */ - private void getRepositoryByJNDI() { - if (jndiContext != null) { - // acquire via JNDI - try { - repository = (Repository) jndiContext.lookup(repositoryName); - } catch (NamingException e) { - log.error("Error while retrieving repository using JNDI: " + e); - return; - } - log.info("Acquired repository via JNDI."); - } - } - - /** - * tries to retrieve the repository using RMI - */ - private void getRepositoryByRMI() { - if (rmiURI != null) { - // acquire via RMI - ClientFactoryDelegater cfd = null; - try { - Class clazz = Class.forName("org.apache.jackrabbit.client.RMIClientFactoryDelegater"); - cfd = (ClientFactoryDelegater) clazz.newInstance(); - } catch (NoClassDefFoundError e) { - log.error("Unable to locate RMI ClientRepositoryFactory. jcr-rmi.jar missing? " + e.toString()); - return; - } catch (Exception e) { - log.error("Unable to locate RMI ClientRepositoryFactory. jcr-rmi.jar missing?" + e.toString()); - return; - } - - try { - repository = cfd.getRepository(rmiURI); - } catch (Exception e) { - log.error("Error while retrieving repository using RMI: " + e); - return; - } - log.info("Acquired repository via RMI."); - } - } - - /** - * Returns the JSR170 repository - * - * @return a jsr170 repository - */ - public static Repository getRepository() { - return repository; - } - - /** - * Build a {@link Credentials} object for the given authorization header. - * The creds may be used to login to the repository. If the specified header - * string is null or not of the required format, null - * is returned. - * - * @param authHeader Authorization header as present in the Http request - * @return credentials or null. - * @throws ServletException If an IOException occured while decoding the - * Authorization header. - * @see #getRepository() - * @see #login(HttpServletRequest) - */ - public static Credentials getCredentialsFromHeader(String authHeader) - throws ServletException { - try { - if (authHeader != null) { - String[] authStr = authHeader.split(" "); - if (authStr.length >= 2 && authStr[0].equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - Base64.decode(authStr[1].toCharArray(), out); - String decAuthStr = out.toString("ISO-8859-1"); - int pos = decAuthStr.indexOf(':'); - String userid = decAuthStr.substring(0, pos); - String passwd = decAuthStr.substring(pos + 1); - return new SimpleCredentials(userid, passwd.toCharArray()); - } - } - return null; - } catch (IOException e) { - throw new ServletException("Unable to decode authorization: " + e.toString()); - } - } - - /** - * Simple login to the {@link Repository} accessed by this servlet using the - * Authorization header present in the given request.

- * Please note, that no workspace information is provided to the repository - * login ({@link Repository#login(javax.jcr.Credentials)}), thus the default - * workspace will be selected. In order to provide a specific workspace name, - * manual {@link Repository#login(Credentials, String) login} is required (see - * also {@link #getRepository()}). - * - * @param request - * @return Session object obtained upon {@link Repository#login(javax.jcr.Credentials)}. - * @throws ServletException - * @see #getRepository() in order to be able to login to a specific workspace. - * @see #getCredentialsFromHeader(String) for a utility method to retrieve - * credentials from the Authorization header string. - */ - public static Session login(HttpServletRequest request) throws ServletException { - String authHeader = request.getHeader(HEADER_AUTHORIZATION); - try { - return repository.login(getCredentialsFromHeader(authHeader)); - } catch (RepositoryException e) { - throw new ServletException("Failed to login to the repository: " + e.toString()); - } - } -} - -/** - * optional class for RMI, will only be used, if RMI client is present - */ -abstract class ClientFactoryDelegater { - - public abstract Repository getRepository(String uri) - throws RemoteException, MalformedURLException, NotBoundException; -} - -/** - * optional class for RMI, will only be used, if RMI server is present - */ -class RMIClientFactoryDelegater extends ClientFactoryDelegater { - - // only used to enforce linking upon Class.forName() - static String FactoryClassName = ClientRepositoryFactory.class.getName(); - - public Repository getRepository(String uri) - throws MalformedURLException, NotBoundException, RemoteException { - System.setProperty("java.rmi.server.useCodebaseOnly", "true"); - return new ClientRepositoryFactory().getRepository(uri); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/maven.xml b/contrib/jcr-server/maven.xml deleted file mode 100644 index 11f77929899..00000000000 --- a/contrib/jcr-server/maven.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/contrib/jcr-server/project.properties b/contrib/jcr-server/project.properties deleted file mode 100644 index 0569e85c372..00000000000 --- a/contrib/jcr-server/project.properties +++ /dev/null @@ -1,2 +0,0 @@ -maven.javadoc.links=http://java.sun.com/j2se/1.4.2/docs/api/,http://www.day.com/maven/jsr170/javadocs/jcr-0.16.1-pfd/ -maven.repo.remote = http://www.ibiblio.org/maven/,http://www.day.com/maven/ diff --git a/contrib/jcr-server/project.xml b/contrib/jcr-server/project.xml deleted file mode 100644 index 0b041dc4f0f..00000000000 --- a/contrib/jcr-server/project.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - 3 - jcr-server - - Jackrabbit-Server - 0.16.2 - 2005 - org.apache.jackrabbit.server.* - - WebDav based JCR client/server connection facility. - - JCRWebdav Server - - - - The Apache Software License, Version 2.0 - /LICENSE.txt - repo - - - diff --git a/contrib/jcr-server/server/maven.xml b/contrib/jcr-server/server/maven.xml deleted file mode 100644 index ef564ff1229..00000000000 --- a/contrib/jcr-server/server/maven.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - diff --git a/contrib/jcr-server/server/project.properties b/contrib/jcr-server/server/project.properties deleted file mode 100644 index 0569e85c372..00000000000 --- a/contrib/jcr-server/server/project.properties +++ /dev/null @@ -1,2 +0,0 @@ -maven.javadoc.links=http://java.sun.com/j2se/1.4.2/docs/api/,http://www.day.com/maven/jsr170/javadocs/jcr-0.16.1-pfd/ -maven.repo.remote = http://www.ibiblio.org/maven/,http://www.day.com/maven/ diff --git a/contrib/jcr-server/server/project.xml b/contrib/jcr-server/server/project.xml deleted file mode 100644 index 5dcbcd227d3..00000000000 --- a/contrib/jcr-server/server/project.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - ${basedir}/../project.xml - jcr-server - jcr-server - jar - JCRWebdavServer Server Library - - - - - - - jcr-webdav - jcr-server - ${pom.currentVersion} - - - jcr-client - jcr-server - ${pom.currentVersion} - - true - - - - - jsr170 - jcr - 0.16.2 - http://www.day.com/maven/jsr170/jars/jcr-0.16.2.jar - - - jackrabbit - 0.16.2-dev - - - jdom - 1.0 - - - log4j - 1.2.8 - - - servletapi - 2.3 - - - jcr-rmi - 0.16.2 - - - - - - - - - - ${basedir}/src/java - - - src/java - - **/*.xml - **/*.xsd - **/*.properties - **/*.dtd - - - - - - diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/AbstractWebdavServlet.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/AbstractWebdavServlet.java deleted file mode 100644 index 57722ec78b6..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/AbstractWebdavServlet.java +++ /dev/null @@ -1,823 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.observation.ObservationResource; -import org.apache.jackrabbit.webdav.observation.SubscriptionInfo; -import org.apache.jackrabbit.webdav.observation.Subscription; -import org.apache.jackrabbit.webdav.observation.EventDiscovery; -import org.apache.jackrabbit.webdav.ordering.OrderingResource; -import org.apache.jackrabbit.webdav.ordering.OrderPatch; -import org.apache.jackrabbit.webdav.transaction.TransactionInfo; -import org.apache.jackrabbit.webdav.transaction.TransactionResource; -import org.apache.jackrabbit.webdav.lock.LockInfo; -import org.apache.jackrabbit.webdav.lock.ActiveLock; -import org.apache.jackrabbit.webdav.version.*; -import org.apache.jackrabbit.webdav.version.report.ReportInfo; -import org.apache.jackrabbit.webdav.version.report.Report; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.search.SearchResource; -import org.apache.jackrabbit.webdav.search.SearchConstants; -import org.apache.jackrabbit.webdav.search.SearchRequest; - -import org.jdom.Document; - -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.List; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * AbstractWebdavServlet - * - * todo respect Position header - */ -abstract public class AbstractWebdavServlet extends HttpServlet implements DavConstants { - - /** default logger */ - private static Logger log = Logger.getLogger(AbstractWebdavServlet.class); - - /** - * Create a new DavResource - * - * @param locator - * @param request - * @param response - * @return resource for the given locator - * @throws DavException - */ - abstract protected DavResource createResource(DavResourceLocator locator, - WebdavRequest request, - WebdavResponse response) throws DavException; - - /** - * The OPTION method - * - * @param request - * @param response - * @param resource - */ - protected void doOptions(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException { - response.addHeader(DavConstants.HEADER_DAV, resource.getComplianceClass()); - response.addHeader("Allow", resource.getSupportedMethods()); - response.addHeader("MS-Author-Via", DavConstants.HEADER_DAV); - if (resource instanceof SearchResource) { - String[] langs = ((SearchResource)resource).getQueryGrammerSet().getQueryLanguages(); - for (int i = 0; i < langs.length; i++) { - response.addHeader(SearchConstants.HEADER_DASL, "<" + langs[i] + ">"); - } - } - // with DeltaV the OPTIONS request may contain a Xml body. - OptionsResponse oR = null; - OptionsInfo oInfo = request.getOptionsInfo(); - if (oInfo != null && resource instanceof DeltaVResource) { - oR = ((DeltaVResource)resource).getOptionResponse(oInfo); - } - if (oR == null) { - response.setStatus(DavServletResponse.SC_OK); - } else { - response.sendXmlResponse(oR.toXml(), DavServletResponse.SC_OK); - } - } - - /** - * The HEAD method - * - * @param request - * @param response - * @param resource - * @throws java.io.IOException - */ - protected void doHead(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException { - spoolResource(request, response, resource, false); - } - - /** - * The GET method - * - * @param request - * @param response - * @param resource - * @throws IOException - */ - protected void doGet(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException { - spoolResource(request, response, resource, true); - } - - /** - * @param request - * @param response - * @param resource - * @param sendContent - * @throws IOException - */ - private void spoolResource(WebdavRequest request, WebdavResponse response, - DavResource resource, boolean sendContent) - throws IOException { - - if (!resource.exists()) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - long modTime = resource.getModificationTime(); - if (modTime != DavResource.UNDEFINED_MODIFICATIONTIME && modTime <= request.getDateHeader("If-Modified-Since")) { - // resource has not been modified since the time indicated in the - // 'If-Modified-Since' header. - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - return; - } - - DavProperty lastMod = resource.getProperty(DavPropertyName.GETLASTMODIFIED); - if (lastMod != null) { - response.setHeader("Last-Modified", String.valueOf(lastMod.getValue())); - } - - DavProperty etag = resource.getProperty(DavPropertyName.GETETAG); - if (etag != null) { - response.setHeader("ETag", String.valueOf(etag.getValue())); - } - - DavProperty contentType = resource.getProperty(DavPropertyName.GETCONTENTTYPE); - if (contentType != null) { - response.setHeader("Content-Type", String.valueOf(contentType.getValue())); - } - - DavProperty contentLength = resource.getProperty(DavPropertyName.GETCONTENTLENGTH); - if (contentLength != null) { - try { - int length = Integer.parseInt(contentLength.getValue()+""); - if (length > 0) { - response.setIntHeader("Content-Length", length); - } - } catch (NumberFormatException e) { - log.error("Could not build content length from property value '" + contentLength.getValue() + "'"); - } - } - - // spool content in case of 'GET' request - if (sendContent) { - InputStream in = resource.getStream(); - if (in != null) { - OutputStream out = response.getOutputStream(); - byte[] buffer = new byte[8192]; - int read; - while ((read = in.read(buffer)) >= 0 ) { - out.write(buffer, 0, read); - } - in.close(); - } - } - response.flushBuffer(); - } - - /** - * The PROPFIND method - * - * @param request - * @param response - * @param resource - * @throws IOException - */ - protected void doPropFind(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException { - - if (!resource.exists()) { - response.sendError(DavServletResponse.SC_NOT_FOUND); - return; - } - - int depth = request.getDepth(DEPTH_INFINITY); - DavPropertyNameSet requestProperties = request.getPropFindProperties(); - int propfindType = request.getPropFindType(); - - MultiStatus mstatus = new MultiStatus(); - mstatus.addResourceProperties(resource, requestProperties, propfindType, depth); - response.sendMultiStatusResponse(mstatus); - } - - /** - * The PROPPATCH method - * - * @param request - * @param response - * @param resource - * @throws IOException - */ - protected void doPropPatch(WebdavRequest request, WebdavResponse response, - DavResource resource) - throws IOException, DavException { - - DavPropertySet setProperties = request.getPropPatchSetProperties(); - DavPropertyNameSet removeProperties = request.getPropPatchRemoveProperties(); - if (setProperties.isEmpty() && removeProperties.isEmpty()) { - response.sendError(DavServletResponse.SC_BAD_REQUEST); - return; - } - - // first resolve merge conflicts - // TODO: not correct resolution of merge conflicts are immediately perstisted - // TODO: rfc 2518 requires, that no changes must only be persisted if the complete proppatch-req succeeds - if (resource instanceof VersionControlledResource) { - ((VersionControlledResource)resource).resolveMergeConflict(setProperties, removeProperties); - } - - // complete any other property setting or removing - DavPropertyIterator setIter = setProperties.iterator(); - while (setIter.hasNext()) { - DavProperty prop = setIter.nextProperty(); - resource.setProperty(prop); - } - Iterator remNameIter = removeProperties.iterator(); - while (remNameIter.hasNext()) { - DavPropertyName propName = (DavPropertyName) remNameIter.next(); - resource.removeProperty(propName); - } - response.setStatus(DavServletResponse.SC_OK); - - // todo return multistatus response in case of failure - } - - /** - * The PUT method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doPut(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException, DavException { - - DavResource parentResource = resource.getCollection(); - if (parentResource == null || !parentResource.exists()) { - // parent does not exist - response.sendError(DavServletResponse.SC_CONFLICT); - return; - } - - int status; - // test if resource already exists - if (resource.exists()){ - status = DavServletResponse.SC_NO_CONTENT; - } else { - status = DavServletResponse.SC_CREATED; - } - - parentResource.addMember(resource, request.getInputStream()); - response.setStatus(status); - } - - /** - * The MKCOL method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doMkCol(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException, DavException { - - DavResource parentResource = resource.getCollection(); - if (parentResource == null || !parentResource.exists() || !parentResource.isCollection()) { - // parent does not exist or is not a collection - response.sendError(DavServletResponse.SC_CONFLICT); - return; - } - - if (request.getContentLength() > 0 || request.getHeader("Transfer-Encoding") != null) { - parentResource.addMember(resource, request.getInputStream()); - } else { - parentResource.addMember(resource); - } - response.setStatus(DavServletResponse.SC_CREATED); - } - - /** - * The DELETE method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doDelete(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException, DavException { - DavResource parent = resource.getCollection(); - if (parent != null) { - parent.removeMember(resource); - response.setStatus(DavServletResponse.SC_NO_CONTENT); - } else { - response.sendError(DavServletResponse.SC_FORBIDDEN, "Cannot remove the root resource."); - } - } - - /** - * The COPY method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doCopy(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException, DavException { - - // only depth 0 and infinity is allowed - int depth = request.getDepth(DEPTH_INFINITY); - if (!(depth == DEPTH_0 || depth == DEPTH_INFINITY)) { - response.sendError(DavServletResponse.SC_BAD_REQUEST); - return; - } - - DavResource destResource = createResource(request.getDestinationLocator(), request, response); - int status = validateDestination(destResource, request); - if (status > DavServletResponse.SC_NO_CONTENT) { - response.sendError(status); - return; - } - - resource.copy(destResource, depth == DEPTH_0); - response.setStatus(status); - } - - /** - * The MOVE method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doMove(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException, DavException { - - DavResource destResource = createResource(request.getDestinationLocator(), request, response); - int status = validateDestination(destResource, request); - if (status > DavServletResponse.SC_NO_CONTENT) { - response.sendError(status); - return; - } - - resource.move(destResource); - response.setStatus(status); - } - - /** - * Validate the given destination resource and return the proper status - * code: Any return value greater/equal than {@link DavServletResponse#SC_NO_CONTENT} - * indicates an error. - * - * @param destResource destination resource to be validated. - * @param request - * @return status code indicating whether the destination is valid. - */ - private int validateDestination(DavResource destResource, WebdavRequest request) { - - String destHeader = request.getHeader(HEADER_DESTINATION); - if (destHeader == null || "".equals(destHeader)){ - return DavServletResponse.SC_BAD_REQUEST; - } - if (destResource.getLocator().equals(request.getRequestLocator())) { - return DavServletResponse.SC_FORBIDDEN; - } - - int status; - if (destResource.exists()) { - if (request.isOverwrite()) { - // matching if-header required for existing resources - if (!request.matchesIfHeader(destResource)) { - return DavServletResponse.SC_PRECONDITION_FAILED; - } else { - // overwrite existing resource: its up to the webdavresource - // object to deal with any delete prior to the copy/move - status = DavServletResponse.SC_NO_CONTENT; - } - } else { - // cannot copy/move to an existing item, if overwrite is not forced - return DavServletResponse.SC_PRECONDITION_FAILED; - } - } else { - // destination does not exist >> copy/move can be performed - status = DavServletResponse.SC_CREATED; - } - return status; - } - - /** - * The LOCK method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doLock(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException, DavException { - - LockInfo lockInfo = request.getLockInfo(); - if (lockInfo.isRefreshLock()) { - // refresh any matching existing locks - ActiveLock[] activeLocks = resource.getLocks(); - List lList = new ArrayList(); - for (int i = 0; i < activeLocks.length; i++) { - if (request.matchesIfHeader(resource.getHref(), activeLocks[i].getToken(), "")) { - lList.add(resource.refreshLock(lockInfo, activeLocks[i].getToken())); - } - } - if (lList.isEmpty()) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } - ActiveLock[] refreshedLocks = (ActiveLock[]) lList.toArray(new ActiveLock[lList.size()]); - response.sendRefreshLockResponse(refreshedLocks); - } else { - // create a new lock - ActiveLock lock = resource.lock(lockInfo); - response.sendLockResponse(lock); - } - - // TODO multistatus in case of failure... - // NOTE: spec says 409 status, but example says 207 (multistatus) - } - - /** - * The UNLOCK method - * - * @param request - * @param response - * @param resource - * @throws DavException - */ - protected void doUnlock(WebdavRequest request, WebdavResponse response, - DavResource resource) throws DavException { - // get lock token from header - String lockToken = request.getLockToken(); - TransactionInfo tInfo = request.getTransactionInfo(); - if (tInfo != null) { - ((TransactionResource)resource).unlock(lockToken, tInfo); - } else { - resource.unlock(lockToken); - } - response.setStatus(DavServletResponse.SC_NO_CONTENT); - } - - /** - * The ORDERPATCH method - * - * @param request - * @param response - * @param resource - * @throws java.io.IOException - * @throws DavException - */ - protected void doOrderPatch(WebdavRequest request, - WebdavResponse response, - DavResource resource) - throws IOException, DavException { - - if (!(resource instanceof OrderingResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - - OrderPatch op = request.getOrderPatch(); - if (op == null) { - response.sendError(DavServletResponse.SC_BAD_REQUEST); - return; - } - // perform reordering of internal members - ((OrderingResource)resource).orderMembers(op); - response.setStatus(DavServletResponse.SC_OK); - - //TODO: in case of failure Multistatus is required... - } - - /** - * The SUBSCRIBE method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doSubscribe(WebdavRequest request, - WebdavResponse response, - DavResource resource) - throws IOException, DavException { - - if (!(resource instanceof ObservationResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - - SubscriptionInfo info = request.getSubscriptionInfo(); - if (info == null) { - response.sendError(DavServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); - return; - } - Subscription subs = ((ObservationResource)resource).subscribe(info, request.getSubscriptionId()); - response.sendSubscriptionResponse(subs); - } - - /** - * The UNSUBSCRIBE method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doUnsubscribe(WebdavRequest request, - WebdavResponse response, - DavResource resource) - throws IOException, DavException { - - if (!(resource instanceof ObservationResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - ((ObservationResource)resource).unsubscribe(request.getSubscriptionId()); - response.setStatus(DavServletResponse.SC_NO_CONTENT); - } - - /** - * The POLL method - * - * @param request - * @param response - * @param resource - * @throws IOException - * @throws DavException - */ - protected void doPoll(WebdavRequest request, - WebdavResponse response, - DavResource resource) - throws IOException, DavException { - - if (!(resource instanceof ObservationResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - EventDiscovery ed = ((ObservationResource)resource).poll(request.getSubscriptionId()); - response.sendPollResponse(ed); - } - - /** - * The VERSION-CONTROL method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doVersionControl(WebdavRequest request, WebdavResponse response, - DavResource resource) - throws DavException, IOException { - if (!(resource instanceof VersionableResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - ((VersionableResource)resource).addVersionControl(); - } - - /** - * The LABEL method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doLabel(WebdavRequest request, WebdavResponse response, - DavResource resource) - throws DavException, IOException { - - LabelInfo labelInfo = request.getLabelInfo(); - if (resource instanceof VersionResource) { - ((VersionResource)resource).label(labelInfo); - } else if (resource instanceof VersionControlledResource) { - ((VersionControlledResource)resource).label(labelInfo); - } else { - // any other resource type that does not support a LABEL request - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - } - - /** - * The REPORT method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doReport(WebdavRequest request, WebdavResponse response, - DavResource resource) - throws DavException, IOException { - if (!(resource instanceof DeltaVResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - ReportInfo info = request.getReportInfo(); - Report report = ((DeltaVResource)resource).getReport(info); - response.sendXmlResponse(report.toXml(), DavServletResponse.SC_OK); - } - - /** - * The CHECKIN method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doCheckin(WebdavRequest request, WebdavResponse response, - DavResource resource) - throws DavException, IOException { - - if (!(resource instanceof VersionControlledResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - String versionHref = ((VersionControlledResource)resource).checkin(); - response.setHeader(DeltaVConstants.HEADER_LOCATION, versionHref); - } - - /** - * The CHECKOUT method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doCheckout(WebdavRequest request, WebdavResponse response, - DavResource resource) - throws DavException, IOException { - if (!(resource instanceof VersionControlledResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - ((VersionControlledResource)resource).checkout(); - } - - /** - * The UNCHECKOUT method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doUncheckout(WebdavRequest request, WebdavResponse response, - DavResource resource) - throws DavException, IOException { - if (!(resource instanceof VersionControlledResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - ((VersionControlledResource)resource).uncheckout(); - } - - /** - * The MERGE method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doMerge(WebdavRequest request, WebdavResponse response, - DavResource resource) throws DavException, IOException { - - if (!(resource instanceof VersionControlledResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - MergeInfo info = request.getMergeInfo(); - MultiStatus ms = ((VersionControlledResource)resource).merge(info); - response.sendMultiStatusResponse(ms); - } - - /** - * The UPDATE method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doUpdate(WebdavRequest request, WebdavResponse response, - DavResource resource) throws DavException, IOException { - - if (!(resource instanceof VersionControlledResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - UpdateInfo info = request.getUpdateInfo(); - MultiStatus ms = ((VersionControlledResource)resource).update(info); - response.sendMultiStatusResponse(ms); - } - - /** - * The MKWORKSPACE method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doMkWorkspace(WebdavRequest request, WebdavResponse response, - DavResource resource) throws DavException, IOException { - if (resource.exists()) { - log.warn("Cannot create a new workspace. Resource already exists."); - response.sendError(DavServletResponse.SC_FORBIDDEN); - return; - } - - DavResource parentResource = resource.getCollection(); - if (parentResource == null || !parentResource.exists() || !parentResource.isCollection()) { - // parent does not exist or is not a collection - response.sendError(DavServletResponse.SC_CONFLICT); - return; - } - if (!(parentResource instanceof DeltaVResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - ((DeltaVResource)parentResource).addWorkspace(resource); - response.setStatus(DavServletResponse.SC_CREATED); - } - - /** - * The SEARCH method - * - * @param request - * @param response - * @param resource - * @throws DavException - * @throws IOException - */ - protected void doSearch(WebdavRequest request, WebdavResponse response, - DavResource resource) throws DavException, IOException { - - if (!(resource instanceof SearchResource)) { - response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); - return; - } - try { - Document doc = request.getRequestDocument(); - if (doc != null) { - SearchRequest sR = new SearchRequest(doc); - response.sendMultiStatusResponse(((SearchResource)resource).search(sR)); - } else { - // request without request body is valid if requested resource - // is a 'query' resource. - response.sendMultiStatusResponse(((SearchResource)resource).search(null)); - } - } catch (IllegalArgumentException e) { - response.sendError(DavServletResponse.SC_BAD_REQUEST); - return; - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/JCRWebdavServer.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/JCRWebdavServer.java deleted file mode 100644 index 438e3134a19..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/JCRWebdavServer.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.client.RepositoryAccessServlet; - -import javax.jcr.*; -import javax.servlet.ServletException; -import java.util.HashMap; -import java.util.HashSet; -/** - * JCRWebdavServer... - */ -public class JCRWebdavServer implements DavSessionProvider { - - /** the default logger */ - private static Logger log = Logger.getLogger(JCRWebdavServer.class); - - /** the session cache */ - private final SessionCache cache = new SessionCache(); - - /** the jcr repository */ - private final Repository repository; - - /** - * Creates a new JCRWebdavServer that operates on the given repository. - * - * @param repository - */ - public JCRWebdavServer(Repository repository) { - this.repository = repository; - } - - //---------------------------------------< DavSessionProvider interface >--- - /** - * Acquires a DavSession either from the session cache or creates a new - * one by login to the repository. - * Upon success, the WebdavRequest will reference that session. - * - * @param request - * @throws DavException if no session could be obtained. - * @see DavSessionProvider#acquireSession(org.apache.jackrabbit.webdav.WebdavRequest) - */ - public void acquireSession(WebdavRequest request) - throws DavException { - DavSession session = cache.get(request); - request.setDavSession(session); - } - - /** - * Releases the reference from the request to the session. If no further - * references to the session exist, the session will be removed from the - * cache. - * - * @param request - * @see DavSessionProvider#releaseSession(org.apache.jackrabbit.webdav.WebdavRequest) - */ - public void releaseSession(WebdavRequest request) { - DavSession session = request.getDavSession(); - if (session != null) { - session.removeReference(request); - } - // remove the session from the request - request.setDavSession(null); - } - - //-------------------------------------------------------------------------- - /** - * Private inner class implementing the DavSession interface. - */ - private class DavSessionImpl implements DavSession { - - /** the underlaying jcr session */ - private final Session session; - - /** - * Private constructor. - * - * @param request - * @throws DavException in case a {@link javax.jcr.LoginException} or {@link javax.jcr.RepositoryException} occurs. - */ - private DavSessionImpl(DavServletRequest request) throws DavException { - try { - String workspaceName = request.getRequestLocator().getWorkspaceName(); - Credentials creds = RepositoryAccessServlet.getCredentialsFromHeader(request.getHeader(DavConstants.HEADER_AUTHORIZATION)); - session = repository.login(creds, workspaceName); - } catch (RepositoryException e) { - // LoginException, RepositoryException both result in FORBIDDEN - throw new JcrDavException(e); - } catch (ServletException e) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); - } - } - - /** - * Add a reference to this DavSession. - * - * @see DavSession#addReference(Object) - */ - public void addReference(Object reference) { - cache.addReference(this, reference); - } - - /** - * Removes the reference from this DavSession. If no - * more references are present, this DavSession is removed - * from the internal cache and the underlaying session is released by calling - * {@link javax.jcr.Session#logout()}. - * - * @see DavSession#removeReference(Object) - */ - public void removeReference(Object reference) { - cache.removeReference(this, reference); - } - - /** - * @see DavSession#getRepositorySession() - */ - public Session getRepositorySession() { - return session; - } - - /** - * @see DavSession#addLockToken(String) - */ - public void addLockToken(String token) { - session.addLockToken(token); - } - - /** - * @see DavSession#getLockTokens() - */ - public String[] getLockTokens() { - return session.getLockTokens(); - } - - /** - * @see DavSession#removeLockToken(String) - */ - public void removeLockToken(String token) { - session.removeLockToken(token); - } - } - - /** - * Private inner class providing a cache for referenced session objects. - */ - private class SessionCache { - - private SessionMap sessionMap = new SessionMap(); - private HashMap referenceToSessionMap = new HashMap(); - - /** - * Try to retrieve DavSession if a TransactionId or - * SubscriptionId is present in the request header. If no cached session - * was found null is returned. - * - * @param request - * @return a cached DavSession or null. - * @throws DavException - */ - private DavSession get(WebdavRequest request) - throws DavException { - String txId = request.getTransactionId(); - String subscriptionId = request.getSubscriptionId(); - String lockToken = request.getLockToken(); - - if ((lockToken != null || txId != null) && subscriptionId != null) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Ambiguous headers: either TransactionId/Lock-Token or SubscriptionId can be present, not both."); - } - - DavSession session = null; - // try to retrieve a cached session - if (lockToken != null && containsReference(lockToken)) { - session = getSessionByReference(lockToken); - } else if (txId != null && containsReference(txId)) { - session = getSessionByReference(txId); - } else if (subscriptionId != null && containsReference(subscriptionId)) { - session = getSessionByReference(subscriptionId); - } - // no cached session present -> create new one. - if (session == null) { - session = new DavSessionImpl(request); - sessionMap.put(session, new HashSet()); - log.info("login: User '" + session.getRepositorySession().getUserId() + "' logged in."); - } else { - log.info("login: Retrieved cached session for user '" + session.getRepositorySession().getUserId() + "'"); - } - addReference(session, request); - return session; - } - - /** - * Add a references to the specified DavSession. - * - * @param session - * @param reference - */ - private void addReference(DavSession session, Object reference) { - HashSet referenceSet = sessionMap.get(session); - if (referenceSet != null) { - referenceSet.add(reference); - referenceToSessionMap.put(reference, session); - } else { - log.error("Failed to add reference to session. No entry in cache found."); - } - } - - /** - * Remove the given reference from the specified DavSession. - * - * @param session - * @param reference - */ - private void removeReference(DavSession session, Object reference) { - HashSet referenceSet = sessionMap.get(session); - if (referenceSet != null) { - if (referenceSet.remove(reference)) { - log.info("Removed reference " + reference + " to session " + session); - referenceToSessionMap.remove(reference); - } else { - log.warn("Failed to remove reference " + reference + " to session " + session); - } - if (referenceSet.isEmpty()) { - log.info("No more references present on webdav session -> clean up."); - sessionMap.remove(session); - log.info("Login: User '" + session.getRepositorySession().getUserId() + "' logged out"); - session.getRepositorySession().logout(); - } else { - log.debug(referenceSet.size() + " references remaining on webdav session " + session); - } - } else { - log.error("Failed to remove reference from session. No entry in cache found."); - } - } - - /** - * Returns true, if there exists a DavSession in the cache - * that is referenced by the specified object. - * - * @param reference - * @return true if a DavSession is referenced by the given - * object. - */ - private boolean containsReference(Object reference) { - return referenceToSessionMap.containsKey(reference); - } - - /** - * Returns the DavSession that is referenced by the - * specified reference object. - * - * @param reference - * @return DavSession that is referenced by this reference - * object. - * @see #containsReference(Object) - */ - private DavSession getSessionByReference(Object reference) { - return (DavSession) referenceToSessionMap.get(reference); - } - } - - /** - * Simple inner class extending the {@link HashMap}. - */ - private static class SessionMap extends HashMap { - - public HashSet get(DavSession key) { - return (HashSet) super.get(key); - } - - public HashSet put(DavSession key, HashSet value) { - return (HashSet) super.put(key, value); - } - - public HashSet remove(DavSession key) { - return (HashSet) super.remove(key); - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/JCRWebdavServerServlet.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/JCRWebdavServerServlet.java deleted file mode 100644 index deaf8dac469..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/JCRWebdavServerServlet.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.WebdavRequestImpl; -import org.apache.jackrabbit.webdav.observation.*; -import org.apache.jackrabbit.webdav.spi.*; -import org.apache.jackrabbit.webdav.spi.observation.SubscriptionManagerImpl; -import org.apache.jackrabbit.webdav.spi.transaction.TxLockManagerImpl; -import org.apache.jackrabbit.client.RepositoryAccessServlet; - -import javax.servlet.ServletException; -import javax.servlet.http.*; -import javax.jcr.Repository; -import java.io.IOException; - -/** - * JCRWebdavServerServlet provides request/response handling for the JCRWebdavServer. - */ -public class JCRWebdavServerServlet extends AbstractWebdavServlet implements DavConstants { - - /** the default logger */ - private static Logger log = Logger.getLogger(JCRWebdavServerServlet.class); - - /** Init parameter specifying the prefix used with the resource path. */ - public static final String INIT_PARAM_PREFIX = "resource-path-prefix"; - private static String pathPrefix; - - private JCRWebdavServer server; - private DavResourceFactory resourceFactory; - private DavLocatorFactory locatorFactory; - private TxLockManagerImpl txMgr; - private SubscriptionManager subscriptionMgr; - - /** - * Initializes the servlet set reads the following parameter from the - * servlet configuration: - *

    - *
  • resource-path-prefix: optional prefix for all resources.
  • - *
- * - * @throws ServletException - */ - public void init() throws ServletException { - super.init(); - - // set resource path prefix - pathPrefix = getInitParameter(INIT_PARAM_PREFIX); - log.debug(INIT_PARAM_PREFIX + " = " + pathPrefix); - - Repository repository = RepositoryAccessServlet.getRepository(); - if (repository == null) { - throw new ServletException("Repository could not be retrieved. Check config of 'RepositoryServlet'."); - } - server = new JCRWebdavServer(repository); - txMgr = new TxLockManagerImpl(); - subscriptionMgr = new SubscriptionManagerImpl(); - - // todo: ev. make configurable - resourceFactory = new DavResourceFactoryImpl(txMgr, subscriptionMgr); - locatorFactory = new DavLocatorFactoryImpl(pathPrefix); - } - - /** - * Returns the path prefix - * - * @return pathPrefix - * @see #INIT_PARAM_PREFIX - */ - public static String getPathPrefix() { - return pathPrefix; - } - - /** - * Service the request. - * - * @param request - * @param response - * @throws javax.servlet.ServletException - * @throws java.io.IOException - * @see HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) - */ - protected void service(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - WebdavRequest webdavRequest = new WebdavRequestImpl(request, locatorFactory); - WebdavResponse webdavResponse = new WebdavResponseImpl(response); - try { - // login to the server - server.acquireSession(webdavRequest); - - // create the resource and perform initial precondition tests - DavResource resource = createResource(webdavRequest.getRequestLocator(), webdavRequest, webdavResponse); - if (preconditionFailed(webdavRequest, resource)) { - webdavResponse.sendError(DavServletResponse.SC_PRECONDITION_FAILED); - return; - } - - // execute the requested method - int methodCode = DavMethods.getMethodCode(webdavRequest.getMethod()); - execute(webdavRequest, webdavResponse, methodCode, resource); - } catch (DavException e) { - webdavResponse.sendErrorResponse(e); - } finally { - // logout - server.releaseSession(webdavRequest); - } - } - - /** - * - * @param locator - * @param request - * @param response - * @return - */ - protected DavResource createResource(DavResourceLocator locator, WebdavRequest request, - WebdavResponse response) throws DavException { - return resourceFactory.createResource(locator, request, response); - } - - /** - * - * @param request - * @param resource - * @return - */ - private boolean preconditionFailed(WebdavRequest request, DavResource resource) { - // first check matching If header - if (!request.matchesIfHeader(resource)) { - return true; - } - - // test if the requested path matches to the existing session - // this may occur if the session was retrieved from the cache. - String wsName = request.getDavSession().getRepositorySession().getWorkspace().getName(); - boolean failed = !resource.getLocator().isSameWorkspace(wsName); - if (!failed) { - // make sure, the TransactionId header is valid - String txId = request.getTransactionId(); - if (txId != null && !txMgr.hasLock(txId, resource)) { - failed = true; - } - } - return failed; - } - - /** - * @param request - * @param response - * @param method - * @param resource - * @throws ServletException - * @throws IOException - * @throws DavException - */ - private void execute(WebdavRequest request, WebdavResponse response, - int method, DavResource resource) - throws ServletException, IOException, DavException { - - switch (method) { - case DavMethods.DAV_GET: - doGet(request, response, resource); - break; - case DavMethods.DAV_HEAD: - doHead(request, response, resource); - break; - case DavMethods.DAV_PROPFIND: - doPropFind(request, response, resource); - break; - case DavMethods.DAV_PROPPATCH: - doPropPatch(request, response, resource); - break; - case DavMethods.DAV_POST: - case DavMethods.DAV_PUT: - doPut(request, response, resource); - break; - case DavMethods.DAV_DELETE: - doDelete(request, response, resource); - break; - case DavMethods.DAV_COPY: - doCopy(request, response, resource); - break; - case DavMethods.DAV_MOVE: - doMove(request, response, resource); - break; - case DavMethods.DAV_MKCOL: - doMkCol(request, response, resource); - break; - case DavMethods.DAV_OPTIONS: - doOptions(request, response, resource); - break; - case DavMethods.DAV_LOCK: - doLock(request, response, resource); - break; - case DavMethods.DAV_UNLOCK: - doUnlock(request, response, resource); - break; - case DavMethods.DAV_ORDERPATCH: - doOrderPatch(request, response, resource); - break; - case DavMethods.DAV_SUBSCRIBE: - doSubscribe(request, response, resource); - break; - case DavMethods.DAV_UNSUBSCRIBE: - doUnsubscribe(request, response, resource); - break; - case DavMethods.DAV_POLL: - doPoll(request, response, resource); - break; - case DavMethods.DAV_SEARCH: - doSearch(request, response, resource); - break; - case DavMethods.DAV_VERSION_CONTROL: - doVersionControl(request, response, resource); - break; - case DavMethods.DAV_LABEL: - doLabel(request, response, resource); - break; - case DavMethods.DAV_REPORT: - doReport(request, response, resource); - break; - case DavMethods.DAV_CHECKIN: - doCheckin(request, response, resource); - break; - case DavMethods.DAV_CHECKOUT: - doCheckout(request, response, resource); - break; - case DavMethods.DAV_UNCHECKOUT: - doUncheckout(request, response, resource); - break; - case DavMethods.DAV_MERGE: - doMerge(request, response, resource); - break; - case DavMethods.DAV_UPDATE: - doUpdate(request, response, resource); - break; - case DavMethods.DAV_MKWORKSPACE: - doMkWorkspace(request, response, resource); - break; - default: - // any other method - super.service(request, response); - } - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/RepositoryStartupServlet.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/RepositoryStartupServlet.java deleted file mode 100644 index 182687b5393..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/RepositoryStartupServlet.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server; - -import org.apache.log4j.PropertyConfigurator; -import org.apache.log4j.Logger; -import org.apache.jackrabbit.core.config.RepositoryConfig; -import org.apache.jackrabbit.core.RepositoryImpl; -import org.apache.jackrabbit.rmi.server.ServerAdapterFactory; -import org.xml.sax.InputSource; - -import javax.servlet.http.HttpServlet; -import javax.servlet.ServletException; -import javax.jcr.*; -import javax.naming.InitialContext; -import javax.naming.NamingException; -import java.io.*; -import java.util.Properties; -import java.util.Enumeration; -import java.rmi.registry.Registry; -import java.rmi.registry.LocateRegistry; -import java.rmi.Naming; -import java.rmi.RemoteException; -import java.rmi.AlreadyBoundException; -import java.rmi.Remote; -import java.net.MalformedURLException; - -/** - * The RepositoryStartupServlet starts a jackrabbit repository and registers it - * to the JNDI environment and optional to the RMI registry. - */ -public class RepositoryStartupServlet extends HttpServlet { - - /** the default logger */ - private static Logger log; - - /** initial param name for the repository config location */ - public final static String INIT_PARAM_REPOSITORY_CONFIG = "repository-config"; - - /** initial param name for the repository home directory */ - public final static String INIT_PARAM_REPOSITORY_HOME = "repository-home"; - - /** initial param name for the repository name */ - public final static String INIT_PARAM_REPOSITORY_NAME = "repository-name"; - - /** initial param name for the rmi port */ - public final static String INIT_PARAM_RMI_PORT = "rmi-port"; - - /** initial param name for the log4j config properties */ - public final static String INIT_PARAM_LOG4J_CONFIG = "log4j-config"; - - /** the registered repository */ - private static RepositoryImpl repository; - - /** the name of the repository as configured */ - private static String repositoryName; - - /** the jndi context, created base on configuration */ - private static InitialContext jndiContext; - - /** the rmi uri, in the form of '//:${rmi-port}/${repository-name}' */ - private static String rmiURI; - - /** - * Initializes the servlet - * @throws ServletException - */ - public void init() throws ServletException { - super.init(); - initLog4J(); - log.info("RepositoryStartupServlet initializing..."); - initRepository(); - registerJNDI(); - registerRMI(); - log.info("RepositoryStartupServlet initialized."); - } - - /** - * destroy the servlet - */ - public void destroy() { - super.destroy(); - if (log == null) { - log("RepositoryStartupServlet shutting down..."); - } else { - log.info("RepositoryStartupServlet shutting down..."); - } - unregisterRMI(); - unregisterJNDI(); - log("RepositoryStartupServlet shut down."); - } - - /** - * Initializes Log4J - * @throws ServletException - */ - private void initLog4J() throws ServletException { - // setup log4j - String log4jConfig = getServletConfig().getInitParameter(INIT_PARAM_LOG4J_CONFIG); - InputStream in =getServletContext().getResourceAsStream(log4jConfig); - if (in==null) { - // try normal init - PropertyConfigurator.configure(log4jConfig); - } else { - try { - Properties log4jProperties = new Properties(); - log4jProperties.load(in); - in.close(); - PropertyConfigurator.configure(log4jProperties); - } catch (IOException e) { - throw new ServletException("Unable to load log4jProperties: " + e.toString()); - } - } - log = Logger.getLogger(RepositoryStartupServlet.class); - } - - /** - * Creates a new Repository based on configuration - * @throws ServletException - */ - private void initRepository() throws ServletException { - // setup home directory - String repHome = getServletConfig().getInitParameter(INIT_PARAM_REPOSITORY_HOME); - if (repHome==null) { - log.error(INIT_PARAM_REPOSITORY_HOME + " missing."); - throw new ServletException(INIT_PARAM_REPOSITORY_HOME + " missing."); - } - File repositoryHome; - try { - repositoryHome = new File(repHome).getCanonicalFile(); - } catch (IOException e) { - log.error(INIT_PARAM_REPOSITORY_HOME + " invalid." + e.toString()); - throw new ServletException(INIT_PARAM_REPOSITORY_HOME + " invalid." + e.toString()); - } - log.info(" repository-home = " + repositoryHome.getPath()); - - // get repository config - String repConfig = getServletConfig().getInitParameter(INIT_PARAM_REPOSITORY_CONFIG); - if (repConfig==null) { - log.error(INIT_PARAM_REPOSITORY_CONFIG + " missing."); - throw new ServletException(INIT_PARAM_REPOSITORY_CONFIG + " missing."); - } - log.info(" repository-config = " + repConfig); - - InputStream in = getServletContext().getResourceAsStream(repConfig); - if (in==null) { - try { - in = new FileInputStream(new File(repositoryHome, repConfig)); - } catch (FileNotFoundException e) { - log.error(INIT_PARAM_REPOSITORY_CONFIG + " invalid." + e.toString()); - throw new ServletException(INIT_PARAM_REPOSITORY_CONFIG + " invalid." + e.toString()); - } - } - - // get repository name - repositoryName = getServletConfig().getInitParameter(INIT_PARAM_REPOSITORY_NAME); - if (repositoryName==null) { - repositoryName="default"; - } - log.info(" repository-name = " + repositoryName); - - try { - InputSource is = new InputSource(in); - RepositoryConfig config = RepositoryConfig.create(is, repositoryHome.getAbsolutePath()); - repository = RepositoryImpl.create(config); - } catch (RepositoryException e) { - throw new ServletException("Error while creating repository", e); - } - } - - /** - * Registers the repository in the JNDI context - * @throws ServletException - */ - private void registerJNDI() throws ServletException { - // registering via jndi - Properties env = new Properties(); - Enumeration names = getServletConfig().getInitParameterNames(); - while (names.hasMoreElements()) { - String name = (String) names.nextElement(); - if (name.startsWith("java.naming.")) { - env.put(name, getServletConfig().getInitParameter(name)); - log.info(" adding property to JNDI environment: " + name + "=" + env.getProperty(name)); - } - } - try { - jndiContext = new InitialContext(env); - jndiContext.bind(repositoryName, repository); - } catch (NamingException e) { - throw new ServletException(e); - } - log.info("Repository bound to JNDI with name: " + repositoryName); - } - - /** - * Unregisters the repository from the JNDI context - */ - private void unregisterJNDI() { - if (jndiContext != null) { - try { - jndiContext.unbind(repositoryName); - } catch (NamingException e) { - log("Error while unbinding repository from JNDI: " + e); - } - } - } - - /** - * Registers the repositroy to the RMI registry - * @throws ServletException - */ - private void registerRMI() throws ServletException { - // check registering via RMI - String rmiPortStr = getServletConfig().getInitParameter(INIT_PARAM_RMI_PORT); - if (rmiPortStr != null) { - int rmiPort = 0; - try { - rmiPort = Integer.parseInt(rmiPortStr); - } catch (NumberFormatException e) { - log.warn("Invalid port in rmi-port param: " + e); - } - if (rmiPort == 0) { - rmiPort = Registry.REGISTRY_PORT; - } - - // try to create remote repository - Remote remote = null; - try { - Class clazz = Class.forName("org.apache.jackrabbit.server.RMIRemoteFactoryDelegater"); - RemoteFactoryDelegater rmf = (RemoteFactoryDelegater) clazz.newInstance(); - remote = rmf.createRemoteRepository(repository); - } catch (RemoteException e) { - throw new ServletException("Unable to create remote repository: " + e.toString(), e); - } catch (NoClassDefFoundError e) { - log.warn("Unable to create RMI repository. jcr-rmi.jar might be missing.: " + e.toString()); - return; - } catch (Exception e) { - log.warn("Unable to create RMI repository. jcr-rmi.jar might be missing.: " + e.toString()); - return; - } - - try { - System.setProperty("java.rmi.server.useCodebaseOnly", "true"); - try { - // start registry - LocateRegistry.createRegistry(rmiPort); - } catch (RemoteException e) { - // ignore - } - rmiURI = "//:" + rmiPort + "/" + repositoryName; - Naming.bind(rmiURI, remote); - - log.info("Repository bound via RMI with name: " + rmiURI); - } catch (MalformedURLException e) { - throw new ServletException("Unable to bind repository via RMI: " + e.toString(), e); - } catch (RemoteException e) { - throw new ServletException("Unable to bind repository via RMI: " + e.toString(), e); - } catch (AlreadyBoundException e) { - throw new ServletException("Unable to bind repository via RMI: " + e.toString(), e); - } - } - } - - /** - * Unregisters the repository from the RMI registry - */ - private void unregisterRMI() { - if (rmiURI != null) { - try { - Naming.unbind(rmiURI); - } catch (Exception e) { - log("Error while unbinding repository from JNDI: " + e); - } - } - } - -} - -/** - * optional class for RMI, will only be used, if RMI server is present - */ -abstract class RemoteFactoryDelegater { - - public abstract Remote createRemoteRepository(Repository repository) - throws RemoteException; -} -/** - * optional class for RMI, will only be used, if RMI server is present - */ -class RMIRemoteFactoryDelegater extends RemoteFactoryDelegater { - - // only used to enforce linking upon Class.forName() - static String FactoryClassName = ServerAdapterFactory.class.getName(); - - public Remote createRemoteRepository(Repository repository) - throws RemoteException { - return new ServerAdapterFactory().getRemoteRepository(repository); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/WebdavServlet.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/WebdavServlet.java deleted file mode 100644 index d9259539487..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/WebdavServlet.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server.simple; - -import org.apache.jackrabbit.server.simple.dav.lock.SimpleLockManager; -import org.apache.jackrabbit.server.simple.dav.ResourceFactoryImpl; -import org.apache.jackrabbit.server.simple.dav.LocatorFactoryImpl; -import org.apache.jackrabbit.server.simple.dav.DavSessionProviderImpl; - -import javax.servlet.http.*; -import javax.servlet.*; -import java.io.*; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.server.AbstractWebdavServlet; -import org.apache.jackrabbit.webdav.*; - -/** - * WebdavServlet provides webdav support (level 1 and 2 complient) for repository - * resources. - */ -public class WebdavServlet extends AbstractWebdavServlet { - - /** the default logger */ - private static final Logger log = Logger.getLogger(WebdavServlet.class); - - /** init param name of the repository prefix */ - public static final String INIT_PARAM_RESOURCE_PATH_PREFIX = "resource-path-prefix"; - - /** - * Map used to remember any webdav lock created without being reflected - * in the underlaying repository. - * This is needed because some clients rely on a successful locking - * mechanism in order to perform properly (e.g. mac OSX built-in dav client) - */ - private SimpleLockManager lockManager; - - /** the resource factory */ - private DavResourceFactory resourceFactory; - - /** the locator factory */ - private DavLocatorFactory locatorFactory; - - /** the session provider */ - private DavSessionProvider sessionProvider; - - /** the repository prefix retrieved from config */ - private static String resourcePathPrefix; - - /** - * Init this servlet - * - * @throws ServletException - */ - public void init() throws ServletException { - super.init(); - - resourcePathPrefix = getInitParameter(INIT_PARAM_RESOURCE_PATH_PREFIX); - if (resourcePathPrefix == null) { - log.debug("Missing path prefix > setting to empty string."); - resourcePathPrefix = ""; - } else if (resourcePathPrefix.endsWith("/")) { - log.debug("Path prefix ends with '/' > removing trailing slash."); - resourcePathPrefix = resourcePathPrefix.substring(0, resourcePathPrefix.length()-1); - } - log.info(INIT_PARAM_RESOURCE_PATH_PREFIX + " = '" + resourcePathPrefix + "'"); - - lockManager = new SimpleLockManager(); - resourceFactory = new ResourceFactoryImpl(lockManager); - locatorFactory = new LocatorFactoryImpl(resourcePathPrefix); - } - - /** - * Service the given request. - * - * @param request - * @param response - * @throws ServletException - * @throws IOException - */ - protected void service(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - try { - WebdavRequest webdavRequest = new WebdavRequestImpl(request, locatorFactory); - WebdavResponse webdavResponse = new WebdavResponseImpl(response); - - // make sure there is a authenticated user - getDavSessionProvider().acquireSession(webdavRequest); - if (webdavRequest.getDavSession() == null) { - return; - } - - // check matching if=header for lock-token relevant operations - DavResource resource = createResource(webdavRequest.getRequestLocator(), webdavRequest, webdavResponse); - if (resource.exists() && !webdavRequest.matchesIfHeader(resource)) { - webdavResponse.sendError(DavServletResponse.SC_PRECONDITION_FAILED); - return; - } - - /* set cache control headers in order to deal with non-dav complient - * http1.1 or http1.0 proxies. >> see RFC2518 9.4.5 */ - webdavResponse.addHeader("Pragma", "No-cache"); // http1.0 - webdavResponse.addHeader("Cache-Control", "no-cache"); // http1.1 - - int methodCode = DavMethods.getMethodCode(webdavRequest.getMethod()); - switch (methodCode) { - case DavMethods.DAV_HEAD: - case DavMethods.DAV_GET: - doGet(webdavRequest, webdavResponse, resource); - case DavMethods.DAV_OPTIONS: - doOptions(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_PROPFIND: - doPropFind(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_PROPPATCH: - doPropPatch(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_PUT: - case DavMethods.DAV_POST: - doPut(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_DELETE: - doDelete(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_COPY: - doCopy(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_MOVE: - doMove(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_MKCOL: - doMkCol(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_LOCK: - doLock(webdavRequest, webdavResponse, resource); - break; - case DavMethods.DAV_UNLOCK: - doUnlock(webdavRequest, webdavResponse, resource); - break; - default: - // GET, HEAD, TRACE...... - super.service(request, response); - } - getDavSessionProvider().releaseSession(webdavRequest); - - } catch (DavException e) { - response.sendError(e.getErrorCode()); - } - } - - /** - * The MKCOL method - * - * @throws IOException - */ - protected void doMkCol(WebdavRequest request, WebdavResponse response, - DavResource resource) throws IOException, DavException { - // mkcol request with request.body is not supported. - if (request.getContentLength()>0 || request.getHeader("Transfer-Encoding") != null) { - response.sendError(DavServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); - return; - } - super.doMkCol(request, response, resource); - } - - /** - * Build a DavResource from the given path.
- * Please note, that the resource may not have a corresponding element in - * the repository in which case, {@link DavResource#exists()} will return - * false. - * - * @see AbstractWebdavServlet#createResource(org.apache.jackrabbit.webdav.DavResourceLocator, org.apache.jackrabbit.webdav.WebdavRequest, org.apache.jackrabbit.webdav.WebdavResponse) - */ - protected DavResource createResource(DavResourceLocator locator, WebdavRequest request, WebdavResponse response) - throws DavException { - return resourceFactory.createResource(locator, request, response); - } - - /** - * Returns the configured path prefix - * - * @return resourcePathPrefix - * @see #INIT_PARAM_RESOURCE_PATH_PREFIX - */ - public static String getPathPrefix() { - return resourcePathPrefix; - } - - /** - * Returns the DavSessionProvider. If no session provider has - * been set or created a new instance of {@link DavSessionProviderImpl} is - * return. - * - * @return the session provider - */ - public DavSessionProvider getDavSessionProvider() { - if (sessionProvider == null) { - sessionProvider = new DavSessionProviderImpl(); - } - return sessionProvider; - } - - /** - * Set the session provider - * - * @param sessionProvider - */ - public void setDavSessionProvider(DavSessionProvider sessionProvider) { - this.sessionProvider = sessionProvider; - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavResourceImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavResourceImpl.java deleted file mode 100644 index 28097a3711c..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavResourceImpl.java +++ /dev/null @@ -1,659 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server.simple.dav; - -import javax.jcr.*; -import javax.jcr.lock.Lock; -import java.util.*; -import java.io.*; - -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.util.Text; -import org.apache.jackrabbit.webdav.spi.lock.JcrActiveLock; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.lock.*; -import org.apache.jackrabbit.webdav.property.*; - -/** - * DavResourceImpl imeplements a DavResource. - */ -public class DavResourceImpl implements DavResource { - - private DavResourceFactory factory; - private LockManager lockManager; - private DavSession session; - private Node node; - private DavResourceLocator locator; - - private DavPropertySet properties; - private boolean isCollection = true; - - /** is created on initProperties */ - private NodeResource nodeResource; - - /** - * Create a new {@link DavResource}. - * - * @param locator - * @param factory - * @param session - */ - public DavResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session) { - this.session = session; - this.factory = factory; - this.locator = locator; - if (locator != null && locator.getResourcePath() != null) { - try { - init(session.getRepositorySession().getItem(locator.getResourcePath())); - } catch (RepositoryException e) { - // ignore: exists field evaluates to false - } - } - } - - /** - * Init the webdav resource and retrieve the relevant property. - * - * @param repositoryItem - * @throws RepositoryException - */ - private void init(Item repositoryItem) throws RepositoryException { - if (repositoryItem == null || !repositoryItem.isNode()) { - return; - } - node = (Node)repositoryItem; - - // define what is a resource in webdav - if (node.isNodeType("nt:resource") || node.isNodeType("nt:file")) { - isCollection = false; - } - } - - /** - * @return DavResource#COMPLIANCE_CLASS - * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() - */ - public String getComplianceClass() { - return DavResource.COMPLIANCE_CLASS; - } - - /** - * @return DavResource#METHODS - * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() - */ - public String getSupportedMethods() { - return DavResource.METHODS; - } - - /** - * @see DavResource#exists() ) - */ - public boolean exists() { - return node != null; - } - - /** - * @see DavResource#isCollection() - */ - public boolean isCollection() { - return isCollection; - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getLocator() - */ - public DavResourceLocator getLocator() { - return locator; - } - - /** - * @see DavResource#getResourcePath() - */ - public String getResourcePath() { - return locator.getResourcePath(); - } - - /** - * @see DavResource#getHref() - */ - public String getHref() { - return locator.getHref(isCollection()); - } - - /** - * @see DavResource#getDisplayName() - */ - public String getDisplayName() { - String name = null; - if (exists()) { - try { - name = node.getName(); - } catch (RepositoryException e) { - // ignore - } - } - if (name == null && getResourcePath() != null) { - name = Text.getLabel(getResourcePath()); - } - return name; - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getModificationTime() - */ - public long getModificationTime() { - initProperties(); - return nodeResource == null ? 0 : nodeResource.getModificationTime(); - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getStream() - */ - public InputStream getStream() { - initProperties(); - return nodeResource == null ? null : nodeResource.getStream(); - } - - /** - * @see DavResource#getProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) - */ - public DavProperty getProperty(DavPropertyName name) { - initProperties(); - return properties.get(name); - } - - /** - * @see DavResource#getProperties() - */ - public DavPropertySet getProperties() { - initProperties(); - return properties; - } - - /** - * @see DavResource#getPropertyNames() - */ - public DavPropertyName[] getPropertyNames() { - return getProperties().getPropertyNames(); - } - - /** - * Fill the set of properties - */ - private void initProperties() { - if (properties == null && exists()) { - properties = new DavPropertySet(); - try { - nodeResource = new NodeResource(this, node); - properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, nodeResource.getContentLength()+"")); - properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, nodeResource.getCreationDate())); - properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, nodeResource.getLastModified())); - String contentType = nodeResource.getContentType(); - if (contentType != null) { - properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTTYPE, contentType)); - } - properties.add(new DefaultDavProperty(DavPropertyName.GETETAG, nodeResource.getETag())); - } catch (RepositoryException e) { - // should not occure.... - } - - if (getDisplayName() != null) { - properties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, getDisplayName())); - } - if (isCollection()) { - properties.add(new ResourceType(ResourceType.COLLECTION)); - // Windows XP support - properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, "1")); - } else { - properties.add(new ResourceType(ResourceType.DEFAULT_RESOURCE)); - // Windows XP support - properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, "0")); - } - - /* set current lock information. If no lock is set to this resource, - an empty lockdiscovery will be returned in the response. */ - properties.add(new LockDiscovery(getLock(Type.WRITE, Scope.EXCLUSIVE))); - - /* lock support information: all locks are lockable. */ - SupportedLock supportedLock = new SupportedLock(); - supportedLock.addEntry(Type.WRITE, Scope.EXCLUSIVE); - properties.add(supportedLock); - } - } - - /** - * @param property - * @throws DavException - * @see DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) - */ - public void setProperty(DavProperty property) throws DavException { - if (isLocked(this)) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - - /** - * @param propertyName - * @throws DavException - * @see DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) - */ - public void removeProperty(DavPropertyName propertyName) throws DavException { - if (isLocked(this)) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - - /** - * @see DavResource#getCollection() - */ - public DavResource getCollection() { - DavResource parent = null; - if (getResourcePath() != null && !getResourcePath().equals("/")) { - String parentPath = Text.getRelativeParent(getResourcePath(), 1); - if (parentPath.equals("")) { - parentPath="/"; - } - DavResourceLocator parentloc = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentPath); - try { - parent = factory.createResource(parentloc, session); - } catch (DavException e) { - // should not occur - } - } - return parent; - } - - /** - * @see DavResource#getMembers() - */ - public DavResourceIterator getMembers() { - ArrayList list = new ArrayList(); - if (exists() && isCollection()) { - try { - NodeIterator it = node.getNodes(); - while(it.hasNext()) { - list.add(buildResourceFromItem(it.nextNode())); - } - } catch (RepositoryException e) { - // should not occure - } catch (DavException e) { - // should not occure - } - } - return new DavResourceIteratorImpl(list); - } - - /** - * @see DavResource#addMember(DavResource, InputStream) - */ - public void addMember(DavResource member, InputStream in) throws DavException { - if (!exists() || in == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST); - } - if (isLocked(this)) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - - try { - String fileName = member.getDisplayName(); - Node file; - boolean makeVersionable = true; // todo: to be configurable somewhere - if (node.hasNode(fileName)) { - file = node.getNode(fileName); - if (file.hasNode("jcr:content")) { - // remove an existing repository entry for 'overwriting' is not possible - file.getNode("jcr:content").remove(); - } - } else { - file = node.addNode(fileName, "nt:file"); - if (makeVersionable) { - file.addMixin("mix:versionable"); - } - } - - if (fileName.endsWith(".xml")) { - importXml(file, in, "text/xml"); - } else { - // todo: retrieve proper mimetype from filename - importFile(file, in, "application/octet-stream"); - } - session.getRepositorySession().save(); - } catch (ItemExistsException e) { - // according to RFC 2518: MKCOL only possible on non-existing/deleted resource - throw new JcrDavException(e, DavServletResponse.SC_METHOD_NOT_ALLOWED); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } catch (IOException e) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); - } - } - - /** - * Imports a xml into the repository - * @param parentNode - * @param in - * @param contentType - * @throws RepositoryException - * @throws IOException - */ - private void importXml(Node parentNode, InputStream in, String contentType) - throws RepositoryException, IOException { - Node content = parentNode.addNode("jcr:content", "nt:unstructured"); - content.setProperty("jcr:mimeType", contentType); - content.setProperty("jcr:lastModified", Calendar.getInstance()); - session.getRepositorySession().importXML(content.getPath(), in); - } - - /** - * Imports a plain file to the repository - * @param parentNode - * @param in - * @param contentType - * @throws RepositoryException - */ - private void importFile(Node parentNode, InputStream in, String contentType) - throws RepositoryException { - Node content = parentNode.addNode("jcr:content", "nt:resource"); - content.setProperty("jcr:mimeType", contentType); - content.setProperty("jcr:encoding", ""); - content.setProperty("jcr:data", in); - content.setProperty("jcr:lastModified", Calendar.getInstance()); - } - - /** - * @see DavResource#addMember(DavResource) - */ - public void addMember(DavResource member) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_CONFLICT); - } - if (isLocked(this)) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - try { - node.addNode(member.getDisplayName(), "nt:folder"); - node.save(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * @see DavResource#removeMember(DavResource) - */ - public void removeMember(DavResource member) throws DavException { - if (!exists() || !member.exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (isLocked(this) || isLocked(member)) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - - try { - // make sure, non-jcr locks are removed. - if (!isJsrLockable()) { - ActiveLock lock = getLock(Type.WRITE, Scope.EXCLUSIVE); - if (lock != null) { - lockManager.releaseLock(lock.getToken(), member); - } - } - ActiveLock lock = getLock(Type.WRITE, Scope.EXCLUSIVE); - if (lock != null && lockManager.hasLock(lock.getToken(), member)) { - lockManager.releaseLock(lock.getToken(), member); - } - - Session s = session.getRepositorySession(); - Item memItem = s.getItem(member.getResourcePath()); - memItem.remove(); - s.save(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * @see DavResource#move(DavResource) - */ - public void move(DavResource destination) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (isLocked(this)) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - try { - session.getRepositorySession().getWorkspace().move(getResourcePath(), destination.getResourcePath()); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * @see DavResource#copy(DavResource, boolean) - */ - public void copy(DavResource destination, boolean shallow) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (isLocked(destination)) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - // TODO: support shallow and deep copy - if (shallow) { - throw new DavException(DavServletResponse.SC_FORBIDDEN, "Unable to perform shallow copy."); - } - try { - session.getRepositorySession().getWorkspace().copy(getResourcePath(), destination.getResourcePath()); - } catch (PathNotFoundException e) { - // according to rfc 2518: missing parent - throw new DavException(DavServletResponse.SC_CONFLICT, e.getMessage()); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * @param type - * @param scope - * @return true if type is {@link Type#WRITE} and scope is {@link Scope#EXCLUSIVE} - * @see DavResource#isLockable(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) - */ - public boolean isLockable(Type type, Scope scope) { - return Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope); - } - - /** - * @see DavResource#hasLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) - */ - public boolean hasLock(Type type, Scope scope) { - return getLock(type, scope) != null; - } - - /** - * @see DavResource#getLock(Type, Scope) - */ - public ActiveLock getLock(Type type, Scope scope) { - ActiveLock lock = null; - if (exists() && Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope)) { - // try to retrieve the repository lock information first - if (isJsrLockable()) { - try { - Lock jcrLock = node.getLock(); - if (jcrLock != null && jcrLock.isLive()) { - lock = new JcrActiveLock(jcrLock); - } - } catch (RepositoryException e) { - // LockException: no lock applies to this node >> ignore - // RepositoryException, AccessDeniedException or another error >> ignore - } - } else { - // not-jcr lockable >> check for webdav lock - lock = lockManager.getLock(type, scope, this); - } - } - return lock; - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getLocks() - */ - public ActiveLock[] getLocks() { - return new ActiveLock[] {getLock(Type.WRITE, Scope.EXCLUSIVE)}; - } - - /** - * @see DavResource#lock(LockInfo) - */ - public ActiveLock lock(LockInfo lockInfo) throws DavException { - ActiveLock lock = null; - if (isLockable(lockInfo.getType(), lockInfo.getScope())) { - if (isJsrLockable()) { - try { - // try to execute the lock operation - Lock jcrLock = node.lock(lockInfo.isDeep(), false); - if (jcrLock != null) { - lock = new JcrActiveLock(jcrLock); - } - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } else { - // create a new lock which creates a random lock token - lock = lockManager.createLock(lockInfo, this); - } - } else { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Unsupported lock type or scope."); - } - return lock; - } - - /** - * @see DavResource#refreshLock(LockInfo, String) - */ - public ActiveLock refreshLock(LockInfo lockInfo, String lockToken) throws DavException{ - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - /* since lock is always has infinite timeout >> no extra refresh needed - return a lockdiscovery with the lock-info and the default scope and type */ - ActiveLock lock = getLock(lockInfo.getType(), lockInfo.getScope()); - if (lock == null) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No lock present on resource " + getResourcePath()); - } else if (lock.isLockedByToken(lockToken)) { - if (lock instanceof JcrActiveLock) { - try { - // refresh JCR lock and return the original lock object. - node.getLock().refresh(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } else { - lock = lockManager.refreshLock(lockInfo, lockToken, this); - } - } else { - throw new DavException(DavServletResponse.SC_LOCKED); - } - return lock; - } - - /** - * @see DavResource#unlock(String) - */ - public void unlock(String lockToken) throws DavException { - ActiveLock lock = getLock(Type.WRITE, Scope.EXCLUSIVE); - if (lock == null) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } else if (lock.isLockedByToken(lockToken)) { - if (lock instanceof JcrActiveLock) { - try { - node.unlock(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } else { - lockManager.releaseLock(lockToken, this); - } - } else { - throw new DavException(DavServletResponse.SC_LOCKED); - } - } - - /** - * @see DavResource#addLockManager(org.apache.jackrabbit.webdav.lock.LockManager) - */ - public void addLockManager(LockManager lockMgr) { - this.lockManager = lockMgr; - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getFactory() - */ - public DavResourceFactory getFactory() { - return factory; - } - - /** - * Returns true, if this webdav resource allows for locking without checking - * its current lock status. - * - * @return true if this resource is lockable. - */ - private boolean isJsrLockable() { - boolean lockable = false; - if (exists()) { - try { - lockable = node.isNodeType("mix:lockable"); - } catch (RepositoryException e) { - // not jcr-lockable - } - } - return lockable; - } - - /** - * Return true if this resource cannot be modified due to a write lock - * that is not owned by the given session. - * - * @return true if this resource cannot be modified due to a write lock - */ - private boolean isLocked(DavResource res) { - ActiveLock lock = res.getLock(Type.WRITE, Scope.EXCLUSIVE); - if (lock == null) { - return false; - } else { - String[] sLockTokens = session.getLockTokens(); - for (int i = 0; i < sLockTokens.length; i++) { - if (sLockTokens[i].equals(lock.getToken())) { - return false; - } - } - return true; - } - } - - /** - * @param item - * @return - * @throws DavException - * @throws RepositoryException - */ - private DavResource buildResourceFromItem(Item item) throws DavException, RepositoryException { - DavResourceLocator parentloc = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), item.getPath()); - return factory.createResource(parentloc, session); - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavSessionImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavSessionImpl.java deleted file mode 100644 index c9b68142834..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavSessionImpl.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server.simple.dav; - -import javax.jcr.Session; -import java.util.HashSet; - -import org.apache.jackrabbit.webdav.DavSession; - -/** - * Simple implementation of the {@link DavSession} interface. Stores - * lock tokens but does not yet store references. - */ -public class DavSessionImpl implements DavSession { - - /** the underlaying jcr session */ - private final Session session; - - /** the lock tokens of this session */ - private final HashSet lockTokens = new HashSet(); - - /** - * Creates a new DavSession based on a jcr session - * @param session - */ - public DavSessionImpl(Session session) { - this.session = session; - } - - /** - * @see DavSession#addReference(Object) - */ - public void addReference(Object reference) { - throw new UnsupportedOperationException("No yet implemented."); - } - - /** - * @see DavSession#removeReference(Object) - */ - public void removeReference(Object reference) { - throw new UnsupportedOperationException("No yet implemented."); - } - - /** - * @see DavSession#getRepositorySession() - */ - public Session getRepositorySession() { - return session; - } - - /** - * @see DavSession#addLockToken(String) - */ - public void addLockToken(String token) { - lockTokens.add(token); - } - - /** - * @see DavSession#getLockTokens() - */ - public String[] getLockTokens() { - return (String[]) lockTokens.toArray(new String[lockTokens.size()]); - } - - /** - * @see DavSession#removeLockToken(String) - */ - public void removeLockToken(String token) { - lockTokens.remove(token); - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavSessionProviderImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavSessionProviderImpl.java deleted file mode 100644 index ae9297dce6e..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/DavSessionProviderImpl.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server.simple.dav; - -import javax.jcr.*; -import javax.servlet.ServletException; - -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.client.RepositoryAccessServlet; - -/** - * Simple implementation of the {@link DavSessionProvider} - * interface that uses the {@link RepositoryAccessServlet} to locate - * credentials in the request, log into the respository, and provide - * a {@link DavSession} to the request. - */ -public class DavSessionProviderImpl implements DavSessionProvider { - - /** - * Acquires a DavSession. Upon success, the WebdavRequest will - * reference that session. - * - * A session will not be available if credentials can not be found - * in the request (meaning that the request has not been - * authenticated). - * - * @param request - * @throws DavException if a problem occurred while obtaining the - * session - * @see DavSessionProvider#acquireSession(org.apache.jackrabbit.webdav.WebdavRequest) - */ - public void acquireSession(WebdavRequest request) throws DavException { - try { - Credentials creds = RepositoryAccessServlet.getCredentialsFromHeader(request.getHeader(DavConstants.HEADER_AUTHORIZATION)); - if (creds == null) { - // generate anonymous login to gain write access - creds = new SimpleCredentials("anonymous", "anonymous".toCharArray()); - } - Session repSession = RepositoryAccessServlet.getRepository().login(creds); - DavSession ds = new DavSessionImpl(repSession); - request.setDavSession(ds); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } catch (ServletException e) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); - } - } - - /** - * Only removes the DavSession object from the given request object. - * No further actions required, since DavSessionImpl does not - * allow to keep track of references to it. - * - * @param request - * @see DavSessionProvider#releaseSession(org.apache.jackrabbit.webdav.WebdavRequest) - */ - public void releaseSession(WebdavRequest request) { - request.setDavSession(null); - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/LocatorFactoryImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/LocatorFactoryImpl.java deleted file mode 100644 index 9cb98d1ea32..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/LocatorFactoryImpl.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.jackrabbit.server.simple.dav; - -import org.apache.jackrabbit.webdav.*; -import org.apache.log4j.Logger; - -/** - * ResourceFactoryImpl implements a simple DavLocatorFactory - * - * @todo improve special handling of root item.... - */ -public class LocatorFactoryImpl implements DavLocatorFactory { - - /** the default logger */ - private static final Logger log = Logger.getLogger(LocatorFactoryImpl.class); - - private final String repositoryPrefix; - - public LocatorFactoryImpl(String repositoryPrefix) { - this.repositoryPrefix = repositoryPrefix; - } - - public DavResourceLocator createResourceLocator(String prefix, String requestHandle) { - String rPrefix = prefix + repositoryPrefix; - String rHandle = requestHandle; - // remove the configured repository prefix from the path - if (rHandle != null && rHandle.startsWith(repositoryPrefix)) { - rHandle = rHandle.substring(repositoryPrefix.length()); - } - // special treatment for root item, that has no name but '/' path. - if (rHandle == null || "".equals(rHandle)) { - rHandle = "/"; - } - return new Locator(rPrefix, rHandle, this); - } - - public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) { - return new Locator(prefix, resourcePath, this); - } - - private class Locator implements DavResourceLocator { - - private final String prefix; - private final String itemPath; - private final DavLocatorFactory factory; - - private Locator(String prefix, String itemPath, DavLocatorFactory factory) { - this.prefix = prefix; - this.factory = factory; - // remove trailing '/' that is not part of the itemPath except for the root item. - if (itemPath.endsWith("/") && !"/".equals(itemPath)) { - itemPath = itemPath.substring(0, itemPath.length()-1); - } - this.itemPath = itemPath; - } - - public String getPrefix() { - return prefix; - } - - public String getResourcePath() { - return itemPath; - } - - public String getWorkspacePath() { - return ""; - } - - public String getWorkspaceName() { - return ""; - } - - public boolean isSameWorkspace(DavResourceLocator path) { - return isSameWorkspace(path.getWorkspaceName()); - } - - public boolean isSameWorkspace(String workspaceName) { - return getWorkspaceName().equals(workspaceName); - } - - public String getHref(boolean isCollection) { - // avoid doubled trainling '/' for the root item - String suffix = (isCollection && !isRootLocation()) ? "/" : ""; - return prefix + itemPath + suffix; - } - - public boolean isRootLocation() { - return "/".equals(itemPath); - } - - public DavLocatorFactory getFactory() { - return factory; - } - - /** - * Computes the hash code using the prefix and the itemPath - * - * @return the hash code - */ - public int hashCode() { - int hashCode = prefix.hashCode(); - if (itemPath != null) { - hashCode += itemPath.hashCode(); - } - return hashCode % Integer.MAX_VALUE; - } - - /** - * Equality of path is achieved if the specified object is a DavResourceLocator - * and the return values of the two getHref(boolean) methods are - * equal. - * - * @param obj the object to compare to - * @return true if the 2 objects are equal; - * false otherwise - */ - public boolean equals(Object obj) { - if (obj instanceof DavResourceLocator) { - DavResourceLocator path = (DavResourceLocator) obj; - this.getHref(true).equals(path.getHref(true)); - } - return false; - } - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/NodeResource.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/NodeResource.java deleted file mode 100644 index c5bcaf0c017..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/NodeResource.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server.simple.dav; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.util.Text; - -import javax.jcr.*; -import java.util.Date; -import java.util.Locale; -import java.io.*; -import java.text.SimpleDateFormat; - -/** - * The NodeResource class wraps a jcr item in order to respond - * to 'GET', 'HEAD', 'PROPFIND' or 'PROPPATCH' requests. If the item is a - * {@link javax.jcr.Node} its primary property is determined. The value of the - * primary property can be accessed by {@link #getStream()}. If possible other - * required information (last modification date, content type...) is retrieved - * from the property siblings.
- * If the requested item is a {@link javax.jcr.Property} it is treated accordingly. - */ -public class NodeResource { - - /** the default logger */ - private static final Logger log = Logger.getLogger(NodeResource.class); - - /** - * modificationDate date format per RFC 1123 - */ - public static SimpleDateFormat modificationDateFormat = - new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); - - /** - * Simple date format for the creation date ISO representation (partial). - */ - public static SimpleDateFormat creationDateFormat = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - - private static final String PROP_MIMETYPE = "jcr:mimeType"; - private static final String PROP_ENCODING = "jcr:encoding"; - private static final String PROP_LASTMODIFIED = "jcr:lastModified"; - private static final String PROP_CREATED = "jcr:created"; - - private long creationTime = 0; - private long modificationTime = new Date().getTime(); - private long contentLength = 0; - private String contentType = null; - private InputStream in = null; - - /** - * Create a new NodeResource that wraps a JSR170 item. - * - * @throws ItemNotFoundException - * @throws RepositoryException - * @throws IllegalArgumentException if the given item is null - */ - public NodeResource(DavResourceImpl davResource, Node node) throws ItemNotFoundException, RepositoryException { - try { - if (davResource.isCollection()) { - createDirListingContent(node); - } else { - if (node.hasProperty(PROP_CREATED)) { - creationTime = node.getProperty(PROP_CREATED).getValue().getLong(); - } - Node content = node.getPrimaryNodeType().getName().equals("nt:file") - ? node.getNode("jcr:content") - : node; - if (content.getPrimaryNodeType().getName().equals("nt:resource")) { - createPlainFileContent(content); - } else { - createDocViewContent(content); - } - } - } catch (IOException e) { - // ignore - } - } - - private void createPlainFileContent(Node content) throws IOException, RepositoryException { - if (content.hasProperty(PROP_LASTMODIFIED)) { - modificationTime = content.getProperty(PROP_LASTMODIFIED).getLong(); - } - if (content.hasProperty(PROP_MIMETYPE)) { - contentType = content.getProperty(PROP_MIMETYPE).getString(); - } - if (content.hasProperty(PROP_ENCODING)) { - String encoding = content.getProperty(PROP_ENCODING).getString(); - if (!encoding.equals("")) { - contentType+="; charset=\"" + encoding + "\""; - } - } - if (content.hasProperty("jcr:data")) { - Property p = content.getProperty("jcr:data"); - contentLength = p.getLength(); - in = p.getStream(); - } else { - contentLength = 0; - } - } - - private void createDocViewContent(Node node) throws IOException, RepositoryException { - File tmpfile = File.createTempFile("__webdav", ".xml"); - FileOutputStream out = new FileOutputStream(tmpfile); - node.getSession().exportDocView(node.getPath(), out, true, false); - out.close(); - in = new FileInputStream(tmpfile); - contentLength = tmpfile.length(); - modificationTime = tmpfile.lastModified(); - contentType = "text/xml"; - tmpfile.deleteOnExit(); - } - - private void createSysViewContent(Node node) throws IOException, RepositoryException { - File tmpfile = File.createTempFile("__webdav", ".xml"); - FileOutputStream out = new FileOutputStream(tmpfile); - node.getSession().exportSysView(node.getPath(), out, true, false); - out.close(); - in = new FileInputStream(tmpfile); - contentLength = tmpfile.length(); - modificationTime = tmpfile.lastModified(); - contentType = "text/xml"; - tmpfile.deleteOnExit(); - } - - private void createDirListingContent(Node node) throws IOException, RepositoryException { - File tmpfile = File.createTempFile("__webdav", ".xml"); - FileOutputStream out = new FileOutputStream(tmpfile); - - String repName = node.getSession().getRepository().getDescriptor(Repository.REP_NAME_DESC); - String repURL = node.getSession().getRepository().getDescriptor(Repository.REP_VENDOR_URL_DESC); - String repVersion = node.getSession().getRepository().getDescriptor(Repository.REP_VERSION_DESC); - PrintWriter writer = new PrintWriter(out); - writer.print(""); - writer.print(repName); - writer.print(" "); - writer.print(repVersion); - writer.print(" "); - writer.print(node.getPath()); - writer.print(""); - writer.print("

"); - writer.print(node.getPath()); - writer.print("

    "); - writer.print("
  • ..
  • "); - NodeIterator iter = node.getNodes(); - while (iter.hasNext()) { - Node child = iter.nextNode(); - String label = Text.getLabel(child.getPath()); - writer.print("
  • "); - writer.print(label); - writer.print("
  • "); - } - writer.print("

Powered by "); - writer.print(repName); - writer.print(" version "); - writer.print(repVersion); - writer.print(""); - - writer.close(); - out.close(); - in = new FileInputStream(tmpfile); - contentLength = tmpfile.length(); - modificationTime = tmpfile.lastModified(); - contentType = "text/html"; - tmpfile.deleteOnExit(); - } - - /** - * Return the content length or '0'. - * @return content Length or '0' if it could not be determined. - */ - public long getContentLength() { - return contentLength; - } - - /** - * Return the creation time or '0'. - * - * @return creation time or '0' if it could not be determined. - */ - public long getCreationTime() { - return creationTime; - } - - /** - * Return the last modification time. By default it is set to the current - * time. - * - * @return time of last modification or the current time, if it could not - * be determined. - */ - public long getModificationTime() { - return modificationTime; - } - - /** - * Return the last modification time as formatted string. - * - * @return last modification time as string. - * @see NodeResource#modificationDateFormat - */ - public String getLastModified() { - if (modificationTime >= 0) { - return modificationDateFormat.format(new Date(modificationTime)); - } else { - return null; - } - } - - /** - * Return the creation time as formatted string. - * - * @return creation time as string. - * @see NodeResource#creationDateFormat - */ - public String getCreationDate() { - if (creationTime >= 0) { - return creationDateFormat.format(new Date(creationTime)); - } else { - return null; - } - } - - /** - * Return the weak ETag - * - * @return weak ETag - */ - public String getETag() { - return "W/\"" + this.contentLength + "-" + this.modificationTime + "\""; - } - - /** - * Return the strong ETag or empty string if it cannot be determined. - * - * @return strong ETag - */ - public String getStrongETag() { - return ""; - } - - /** - * Return the content type or null if it could not be determined. - * - * @return content type - */ - public String getContentType() { - return contentType; - } - - /** - * Return a stream to the resource value. - * - * @return - */ - public InputStream getStream() { - return in; - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/ResourceFactoryImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/ResourceFactoryImpl.java deleted file mode 100644 index e7cdb6d6d15..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/ResourceFactoryImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.jackrabbit.server.simple.dav; - -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.lock.LockManager; - -/** - * ResourceFactoryImpl implements a simple DavResourceFactory - */ -public class ResourceFactoryImpl implements DavResourceFactory { - - private final LockManager lockMgr; - - public ResourceFactoryImpl(LockManager lockMgr) { - this.lockMgr = lockMgr; - } - - public DavResource createResource(DavResourceLocator locator, DavServletRequest request, - DavServletResponse response) throws DavException { - return createResource(locator, request.getDavSession()); - } - - public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { - DavResource res = new DavResourceImpl(locator, this, session); - res.addLockManager(lockMgr); - return res; - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/lock/SimpleLockManager.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/lock/SimpleLockManager.java deleted file mode 100644 index 02232ca1a4b..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/server/simple/dav/lock/SimpleLockManager.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.server.simple.dav.lock; - -import java.util.HashMap; -import java.util.Iterator; - -import org.apache.jackrabbit.webdav.lock.*; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.util.Text; - -/** - * Simple manager for webdav locks.
- * NOTE: the timeout requested is always replace by a infinite timeout and - * expiration of locks is not checked. - */ -public class SimpleLockManager implements LockManager { - - /** map of locks */ - private HashMap locks = new HashMap(); - - /** - * - * @param lockToken - * @param resource - * @return - * @see LockManager#hasLock(String, org.apache.jackrabbit.webdav.DavResource) - */ - public boolean hasLock(String lockToken, DavResource resource) { - ActiveLock lock = (ActiveLock) locks.get(resource.getResourcePath()); - if (lock != null && lock.getToken().equals(lockToken)) { - return true; - } - return false; - } - - /** - * Returns the lock applying to the given resource or null if - * no lock can be found. - * - * @param type - * @param scope - * @param resource - * @return lock that applies to the given resource or null. - */ - public ActiveLock getLock(Type type, Scope scope, DavResource resource) { - if (!(Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope))) { - return null; - } - String key = resource.getResourcePath(); - ActiveLock lock = (locks.containsKey(key)) ? (ActiveLock)locks.get(key) : null; - - // look for an inherited lock - if (lock == null) { - // cut path instead of retrieving the parent resource - String parentPath = Text.getRelativeParent(key, 1); - boolean found = false; - /* stop as soon as parent lock is found: - if the lock is deep or the parent is a collection the lock - applies to the given resource. */ - while (!"/".equals(parentPath) && !(found = locks.containsKey(parentPath))) { - parentPath = Text.getRelativeParent(parentPath, 1); - } - if (found) { - ActiveLock parentLock = (ActiveLock)locks.get(parentPath); - if (parentLock.isDeep()) { - lock = parentLock; - } - } - } - // since locks have infinite timeout, check for expired lock is omitted. - return lock; - } - - /** - * Adds the lock for the given resource, replacing any existing lock. - * - * @param lockInfo - * @param resource being the lock holder - */ - public synchronized ActiveLock createLock(LockInfo lockInfo, DavResource resource) - throws DavException { - if (lockInfo == null || resource == null) { - throw new IllegalArgumentException("Neither lockInfo nor resource must be null."); - } - - String resourcePath = resource.getResourcePath(); - // test if there is already a lock present on this resource - if (locks.containsKey(resourcePath)) { - throw new DavException(DavServletResponse.SC_LOCKED, "Resource '" + resource.getResourcePath() + "' already holds a lock."); - } - // test if the new lock would conflict with any lock inherited from the - // collection or with a lock present on any member resource. - Iterator it = locks.keySet().iterator(); - while (it.hasNext()) { - String key = (String) it.next(); - // TODO: is check for lock on internal-member correct? - if (Text.isDescendant(key, resourcePath)) { - ActiveLock l = (ActiveLock) locks.get(key); - if (l.isDeep() || (key.equals(Text.getRelativeParent(resourcePath, 1)) && !resource.isCollection())) { - throw new DavException(DavServletResponse.SC_LOCKED, "Resource '" + resource.getResourcePath() + "' already inherits a lock by its collection."); - } - } else if (Text.isDescendant(resourcePath, key)) { - if (lockInfo.isDeep() || isInternalMember(resource, key)) { - throw new DavException(DavServletResponse.SC_LOCKED, "Resource '" + resource.getResourcePath() + "' cannot be locked due to a lock present on a member resource '" + key + "'."); - } - - } - } - ActiveLock lock = new DefaultActiveLock(lockInfo); - // Lazy: reset the timeout to 'Infinite', in order to omit the tests for - // lock expiration. - lock.setTimeout(DavConstants.INFINITE_TIMEOUT); - locks.put(resource.getResourcePath(), lock); - return lock; - } - - /** - * - * @param lockInfo - * @param lockToken - * @param resource - * @return - * @throws DavException - * @see DavResource#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String) - */ - public ActiveLock refreshLock(LockInfo lockInfo, String lockToken, DavResource resource) - throws DavException { - // timeout is always infinite > no test for expiration or adjusting timeout needed. - ActiveLock lock = (ActiveLock)locks.get(resource.getResourcePath()); - if (lock == null) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } else if (!lock.getToken().equals(lockToken)) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - return lock; - } - - /** - * Remove the lock hold by the given resource. - * - * @param lockToken - * @param resource that is the lock holder - */ - public synchronized void releaseLock(String lockToken, DavResource resource) - throws DavException { - if (!locks.containsKey(resource.getResourcePath())) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } - // since locks have infinite timeout, check for expiration is omitted. - - ActiveLock lock = (ActiveLock) locks.get(resource.getResourcePath()); - if (lock.getToken().equals(lockToken)) { - locks.remove(resource.getResourcePath()); - } else { - throw new DavException(DavServletResponse.SC_LOCKED); - } - } - - /** - * Return true, if the resource with the given memberPath is a internal - * non-collection member of the given resource, thus affected by a - * non-deep lock present on the resource. - * - * @param resource - * @param memberPath - * @return - */ - private static boolean isInternalMember(DavResource resource, String memberPath) { - if (resource.getResourcePath().equals(Text.getRelativeParent(memberPath, 1))) { - // find the member with the given path - DavResourceIterator it = resource.getMembers(); - while (it.hasNext()) { - DavResource member = it.nextResource(); - if (member.getResourcePath().equals(memberPath)) { - // return true if that member is not a collection - return !member.isCollection(); - } - } - } - return false; - } -} - diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/AbstractItemResource.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/AbstractItemResource.java deleted file mode 100644 index c6201ccb9c2..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/AbstractItemResource.java +++ /dev/null @@ -1,363 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.spi.search.SearchResourceImpl; -import org.apache.jackrabbit.webdav.spi.version.report.NodeTypesReport; -import org.apache.jackrabbit.webdav.spi.version.report.ExportViewReport; -import org.apache.jackrabbit.webdav.spi.version.report.LocateByUuidReport; -import org.apache.jackrabbit.webdav.spi.version.report.RegisteredNamespacesReport; -import org.apache.jackrabbit.webdav.DavResource; -import org.apache.jackrabbit.webdav.transaction.TxLockEntry; -import org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty; -import org.apache.jackrabbit.webdav.version.report.ReportType; -import org.apache.jackrabbit.webdav.search.*; -import org.apache.jackrabbit.webdav.util.Text; - -import javax.jcr.*; - -/** - * AbstractItemResource covers common functionality for the various - * resources, that represent a repository item. - */ -abstract class AbstractItemResource extends AbstractResource implements - SearchResource, ItemResourceConstants { - - private static Logger log = Logger.getLogger(AbstractItemResource.class); - - protected final Item item; - - /** - * Create a new AbstractItemResource. - * - * @param locator - * @param session - */ - AbstractItemResource(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - super(locator, session, factory); - Item repositoryItem = null; - if (locator != null) { - try { - repositoryItem = getRepositorySession().getItem(locator.getResourcePath()); - } catch (RepositoryException e) { - // ignore: exists field evaluates to false - log.info(e.getMessage()); - } - } - item = repositoryItem; - } - - //----------------------------------------------< DavResource interface >--- - /** - * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() - */ - public String getComplianceClass() { - return ItemResourceConstants.COMPLIANCE_CLASS; - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() - */ - public String getSupportedMethods() { - return ItemResourceConstants.METHODS; - } - - /** - * Returns true if there exists a {@link Item repository item} with the given - * resource path, false otherwise. - * - * @see DavResource#exists() - */ - public boolean exists() { - return item != null; - } - - /** - * @see DavResource#getDisplayName() ) - */ - public String getDisplayName() { - String name = null; - if (exists()) { - try { - name = item.getName(); - } catch (RepositoryException e) { - // ignore: should not occure - log.warn(e.getMessage()); - } - } - String resPath = getResourcePath(); - if (name == null && resPath != null) { - int pos = resPath.lastIndexOf('/'); - if (pos>=0) { - name = resPath.substring(pos+1); - } else { - name = resPath; - } - // note: since index info is present only with existing resources - // there is no need to check for any '[index]' suffix. - } - return name; - } - - /** - * Returns the resource representing the parent item of the repository item - * represented by this resource. If this resoure represents the root item - * a {@link RootCollection} is returned. - * - * @return the collection this resource is internal member of. Except for the - * repository root, the returned collection always represent the parent - * repository node. - * @see DavResource#getCollection() - */ - public DavResource getCollection() { - DavResource collection = null; - - String resourcePath = getResourcePath(); - // No special treatment for the root-item needed, because this is - // covered by the RootItemCollection itself. - String parentResourcePath = Text.getRelativeParent(resourcePath, 1); - String parentWorkspacePath = getLocator().getWorkspacePath(); - - DavResourceLocator parentLoc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), parentWorkspacePath, parentResourcePath); - try { - collection = createResourceFromLocator(parentLoc); - } catch (DavException e) { - log.error("Unexpected error while retrieving collection: " + e.getMessage()); - } - - return collection; - } - - /** - * Moves the underlaying repository item to the indicated destination. - * - * @param destination - * @throws DavException - * @see DavResource#move(DavResource) - * @see Session#move(String, String) - */ - public void move(DavResource destination) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - DavResourceLocator destPath = destination.getLocator(); - if (!getLocator().isSameWorkspace(destPath)) { - throw new DavException(DavServletResponse.SC_FORBIDDEN); - } - - try { - getRepositorySession().move(getResourcePath(), destination.getResourcePath()); - complete(); - - } catch (PathNotFoundException e) { - // according to rfc 2518 - throw new DavException(DavServletResponse.SC_CONFLICT, e.getMessage()); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Copies the underlaying repository item to the indicated destination. If - * the locator of the specified destination resource indicates a different - * workspace, {@link Workspace#copy(String, String, String)} is used to perform - * the copy operation, {@link Workspace#copy(String, String)} otherwise. - *

- * Note, that this implementation does not support shallow copy. - * - * @param destination - * @param shallow - * @throws DavException - * @see DavResource#copy(DavResource, boolean) - * @see Workspace#copy(String, String) - * @see Workspace#copy(String, String, String) - */ - public void copy(DavResource destination, boolean shallow) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - // TODO: support shallow and deep copy is required by RFC 2518 - if (shallow) { - throw new DavException(DavServletResponse.SC_FORBIDDEN, "Unable to perform shallow copy."); - } - - if (!(destination instanceof AbstractItemResource)) { - throw new DavException(DavServletResponse.SC_FORBIDDEN, "Cannot copy a resource that does not represent a repository item."); - } - - try { - AbstractItemResource destResource = (AbstractItemResource) destination; - String destResourcePath = destResource.getResourcePath(); - Workspace workspace = getRepositorySession().getWorkspace(); - if (getLocator().isSameWorkspace(destination.getLocator())) { - workspace.copy(getResourcePath(), destResourcePath); - } else { - Workspace destWorkspace = destResource.getRepositorySession().getWorkspace(); - destWorkspace.copy(workspace.getName(), getResourcePath(), destResourcePath); - } - } catch (PathNotFoundException e) { - // according to RFC 2518, should not occur - throw new DavException(DavServletResponse.SC_NOT_FOUND, e.getMessage()); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - //-------------------------------------------< SearchResource interface >--- - /** - * @return - * @see org.apache.jackrabbit.webdav.search.SearchResource#getQueryGrammerSet() - */ - public QueryGrammerSet getQueryGrammerSet() { - return new SearchResourceImpl(getLocator(), getSession()).getQueryGrammerSet(); - } - - /** - * @param sRequest - * @return - * @throws DavException - * @see SearchResource#search(org.apache.jackrabbit.webdav.search.SearchRequest) - */ - public MultiStatus search(SearchRequest sRequest) throws DavException { - return new SearchResourceImpl(getLocator(), getSession()).search(sRequest); - } - - //-------------------------------------------------------------------------- - /** - * Initialize the {@link org.apache.jackrabbit.webdav.lock.SupportedLock} property - * with entries that are valid for any type item resources. - * - * @see org.apache.jackrabbit.webdav.lock.SupportedLock - * @see org.apache.jackrabbit.webdav.transaction.TxLockEntry - * @see AbstractResource#initLockSupport() - */ - protected void initLockSupport() { - if (exists()) { - // add supportedlock entries for local and eventually for global transaction locks - supportedLock.addEntry(new TxLockEntry(true)); - supportedLock.addEntry(new TxLockEntry(false)); - } - } - - /** - * Define the set of reports supported by this resource. - * - * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty - * @see AbstractResource#initSupportedReports() - */ - protected void initSupportedReports() { - if (exists()) { - supportedReports = new SupportedReportSetProperty(new ReportType[] { - ReportType.EXPAND_PROPERTY, - NodeTypesReport.NODETYPES_REPORT, - ExportViewReport.EXPORTVIEW_REPORT, - LocateByUuidReport.LOCATE_BY_UUID_REPORT, - RegisteredNamespacesReport.REGISTERED_NAMESPACES_REPORT - }); - } - } - - /** - * Fill the property set for this resource. - */ - protected void initProperties() { - super.initProperties(); - if (exists()) { - try { - properties.add(new DefaultDavProperty(JCR_NAME, item.getName())); - properties.add(new DefaultDavProperty(JCR_PATH, item.getPath())); - properties.add(new DefaultDavProperty(JCR_DEPTH, String.valueOf(item.getDepth()))); - } catch (RepositoryException e) { - log.error("Error while accessing jcr properties: " + e.getMessage()); - } - - // transaction resource additional protected properties - if (item.isNew()) { - properties.add(new DefaultDavProperty(JCR_ISNEW, null, true)); - } else if (item.isModified()) { - properties.add(new DefaultDavProperty(JCR_ISMODIFIED, null, true)); - } - } - } - - /** - * @return href of the workspace or null if this resource - * does not represent a repository item. - * - * @see AbstractResource#getWorkspaceHref() - */ - protected String getWorkspaceHref() { - String workspaceHref = null; - DavResourceLocator locator = getLocator(); - if (locator != null && locator.getWorkspaceName() != null) { - workspaceHref = locator.getHref(isCollection()); - if (locator.getResourcePath() != null) { - workspaceHref = workspaceHref.substring(workspaceHref.indexOf(locator.getResourcePath())); - } - } - log.info(workspaceHref); - return workspaceHref; - } - - /** - * If this resource exists but does not contain a transaction id, complete - * will try to persist any modifications prsent on the underlaying repository - * item. - * - * @throws DavException if calling {@link Item#save()} fails - */ - void complete() throws DavException { - if (exists() && getTransactionId() == null) { - try { - if (item.isModified()) { - item.save(); - } - } catch (RepositoryException e) { - // this includes LockException, ConstraintViolationException etc. not detected before - log.error("Error while completing request: " + e.getMessage() +" -> reverting changes."); - try { - item.refresh(false); - } catch (RepositoryException re) { - log.error("Error while reverting changes: " + re.getMessage()); - } - throw new JcrDavException(e); - } - } - } - - /** - * Build a new {@link DavResourceLocator} from the given repository item. - * - * @param repositoryItem - * @return a new locator for the specified item. - * @see #getLocatorFromResourcePath(String) - */ - protected DavResourceLocator getLocatorFromItem(Item repositoryItem) { - String itemPath = null; - try { - if (repositoryItem != null) { - itemPath = repositoryItem.getPath(); - } - } catch (RepositoryException e) { - // ignore: should not occur - log.warn(e.getMessage()); - } - return getLocatorFromResourcePath(itemPath); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/AbstractResource.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/AbstractResource.java deleted file mode 100644 index 903a4811ebd..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/AbstractResource.java +++ /dev/null @@ -1,648 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.transaction.TransactionResource; -import org.apache.jackrabbit.webdav.transaction.TransactionInfo; -import org.apache.jackrabbit.webdav.transaction.TransactionConstants; -import org.apache.jackrabbit.webdav.transaction.TxLockManager; -import org.apache.jackrabbit.webdav.spi.transaction.TxLockManagerImpl; -import org.apache.jackrabbit.webdav.observation.*; -import org.apache.jackrabbit.webdav.util.Text; -import org.apache.jackrabbit.webdav.version.*; -import org.apache.jackrabbit.webdav.version.report.Report; -import org.apache.jackrabbit.webdav.version.report.ReportInfo; -import org.apache.jackrabbit.webdav.version.report.ReportType; -import org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty; -import org.apache.jackrabbit.webdav.lock.*; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.property.ResourceType; - -import javax.jcr.Session; -import java.io.InputStream; -import java.util.*; - -/** - * AbstractResource provides functionality common to all - * resources. - */ -abstract class AbstractResource implements DavResource, ObservationResource, - TransactionResource, DeltaVResource { - - private static Logger log = Logger.getLogger(AbstractResource.class); - - private final DavResourceLocator locator; - private final DavSession session; - private final DavResourceFactory factory; - - private SubscriptionManager subsMgr; - private TxLockManagerImpl txMgr; - private String transactionId; - - private long modificationTime = DavResource.UNDEFINED_MODIFICATIONTIME; - - protected boolean initedProps; - protected DavPropertySet properties = new DavPropertySet(); - protected SupportedLock supportedLock = new SupportedLock(); - protected SupportedReportSetProperty supportedReports = new SupportedReportSetProperty(); - - /** - * Create a new AbstractResource - * - * @param locator - * @param session - */ - AbstractResource(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - if (session == null) { - throw new IllegalArgumentException("Creating AbstractItemResource: DavSession must not be null."); - } - - this.locator = locator; - this.session = session; - this.factory = factory; - - // initialize the supported locks and reports - initLockSupport(); - initSupportedReports(); - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getLocator() - */ - public DavResourceLocator getLocator() { - return locator; - } - - /** - * Returns the path of the underlaying repository item or the item to - * be created (PUT/MKCOL). If the resource exists but does not represent - * a repository item null is returned. - * - * @return path of the underlaying repository item. - * @see DavResource#getResourcePath() - * @see org.apache.jackrabbit.webdav.DavResourceLocator#getResourcePath() - */ - public String getResourcePath() { - return locator.getResourcePath(); - } - - /** - * @see DavResource#getHref() - * @see DavResourceLocator#getHref(boolean) - */ - public String getHref() { - return locator.getHref(true); - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getModificationTime() - */ - public long getModificationTime() { - return modificationTime; - } - - /** - * Set the modificationTime field and adds the {@link DavPropertyName.GETLASTMODIFIED} - * property to the set of properties. - * @param modificationTime - */ - void setModificationTime(long modificationTime) { - this.modificationTime = modificationTime; - if (this.modificationTime >= 0) { - properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, - DavConstants.modificationDateFormat.format(new Date(modificationTime)))); - } - } - - /** - * Returns null - * - * @return Always returns null - * @see org.apache.jackrabbit.webdav.DavResource#getStream() - */ - public InputStream getStream() { - return null; - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getPropertyNames() - */ - public DavPropertyName[] getPropertyNames() { - return getProperties().getPropertyNames(); - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) - */ - public DavProperty getProperty(DavPropertyName name) { - return getProperties().get(name); - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getProperties() - */ - public DavPropertySet getProperties() { - if (!initedProps) { - initProperties(); - } - return properties; - } - - /** - * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - * - * @param property - * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - * @see org.apache.jackrabbit.webdav.DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) - */ - public void setProperty(DavProperty property) throws DavException { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - - /** - * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - * - * @param propertyName - * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - * @see org.apache.jackrabbit.webdav.DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) - */ - public void removeProperty(DavPropertyName propertyName) throws DavException { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - - /** - * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - * - * @param destination - * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - * @see DavResource#move(org.apache.jackrabbit.webdav.DavResource) - */ - public void move(DavResource destination) throws DavException { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - - /** - * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - * - * @param destination - * @param shallow - * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - * @see DavResource#copy(org.apache.jackrabbit.webdav.DavResource, boolean) - */ - public void copy(DavResource destination, boolean shallow) throws DavException { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - - - /** - * Returns true, if the {@link SupportedLock} property contains an entry - * with the given type and scope. By default resources allow for {@link org.apache.jackrabbit.webdav.transaction.TransactionConstants.XML_TRANSACTION - * transaction} lock only. - * - * @param type - * @param scope - * @return true if this resource may be locked by the given type and scope. - * @see DavResource#isLockable(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) - */ - public boolean isLockable(Type type, Scope scope) { - return supportedLock.isSupportedLock(type, scope); - } - - /** - * Returns true if this resource has a lock applied with the given type and scope. - * - * @param type - * @param scope - * @return true if this resource has a lock applied with the given type and scope. - * @see DavResource#hasLock(Type, Scope) - */ - public boolean hasLock(Type type, Scope scope) { - return getLock(type, scope) != null; - } - - /** - * @see DavResource#getLock(Type, Scope) - */ - public ActiveLock getLock(Type type, Scope scope) { - ActiveLock lock = null; - if (TransactionConstants.TRANSACTION.equals(type)) { - lock = txMgr.getLock(type, scope, this); - } - return lock; - } - - /** - * @see DavResource#getLocks() - * todo improve.... - */ - public ActiveLock[] getLocks() { - List locks = new ArrayList(); - // tx locks - ActiveLock l = getLock(TransactionConstants.TRANSACTION, TransactionConstants.LOCAL); - if (l != null) { - locks.add(l); - } - l = getLock(TransactionConstants.TRANSACTION, TransactionConstants.GLOBAL); - if (l != null) { - locks.add(l); - } - // write lock (either exclusive or session-scoped). - l = getLock(Type.WRITE, Scope.EXCLUSIVE); - if (l != null) { - locks.add(l); - } else { - l = getLock(Type.WRITE, ItemResourceConstants.EXCLUSIVE_SESSION); - if (l != null) { - locks.add(l); - } - } - return (ActiveLock[]) locks.toArray(new ActiveLock[locks.size()]); - } - - /** - * @see DavResource#lock(org.apache.jackrabbit.webdav.lock.LockInfo) - */ - public ActiveLock lock(LockInfo reqLockInfo) throws DavException { - if (isLockable(reqLockInfo.getType(), reqLockInfo.getScope())) { - return txMgr.createLock(reqLockInfo, this); - } else { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } - } - - /** - * Only transaction lock may be available on this resource. - * - * @param info - * @param lockToken - * @throws DavException - * @see DavResource#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String) - */ - public ActiveLock refreshLock(LockInfo info, String lockToken) throws DavException { - return txMgr.refreshLock(info, lockToken, this); - } - - /** - * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} since only transaction - * locks may be present on this resource, that need to be released by calling - * {@link TransactionResource#unlock(String, org.apache.jackrabbit.webdav.transaction.TransactionInfo)}. - * - * @param lockToken - * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} - */ - public void unlock(String lockToken) throws DavException { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } - - /** - * @see DavResource#addLockManager(org.apache.jackrabbit.webdav.lock.LockManager) - */ - public void addLockManager(LockManager lockMgr) { - if (lockMgr instanceof TxLockManagerImpl) { - txMgr = (TxLockManagerImpl) lockMgr; - } - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getFactory() - */ - public DavResourceFactory getFactory() { - return factory; - } - - //-------------------------------------------------------------------------- - /** - * @see org.apache.jackrabbit.webdav.transaction.TransactionResource#getSession() - * @see org.apache.jackrabbit.webdav.observation.ObservationResource#getSession() - */ - public DavSession getSession() { - return session; - } - - //--------------------------------------< ObservationResource interface >--- - /** - * @see ObservationResource#init(SubscriptionManager) - */ - public void init(SubscriptionManager subsMgr) { - this.subsMgr = subsMgr; - } - - /** - * @see ObservationResource#subscribe(org.apache.jackrabbit.webdav.observation.SubscriptionInfo, String) - * @see SubscriptionManager#subscribe(org.apache.jackrabbit.webdav.observation.SubscriptionInfo, String, org.apache.jackrabbit.webdav.observation.ObservationResource) - */ - public Subscription subscribe(SubscriptionInfo info, String subscriptionId) - throws DavException { - return subsMgr.subscribe(info, subscriptionId, this); - } - - /** - * @see ObservationResource#unsubscribe(String) - * @see SubscriptionManager#unsubscribe(String, org.apache.jackrabbit.webdav.observation.ObservationResource) - */ - public void unsubscribe(String subscriptionId) throws DavException { - subsMgr.unsubscribe(subscriptionId, this); - } - - /** - * @see ObservationResource#poll(String) - * @see SubscriptionManager#poll(String, org.apache.jackrabbit.webdav.observation.ObservationResource) - */ - public EventDiscovery poll(String subscriptionId) throws DavException { - return subsMgr.poll(subscriptionId, this); - } - - //--------------------------------------< TransactionResource interface >--- - /** - * @see TransactionResource#init(TxLockManager, String) - */ - public void init(TxLockManager txMgr, String transactionId) { - this.txMgr = (TxLockManagerImpl) txMgr; - this.transactionId = transactionId; - } - - /** - * @see TransactionResource#unlock(String, org.apache.jackrabbit.webdav.transaction.TransactionInfo) - */ - public void unlock(String lockToken, TransactionInfo tInfo) throws DavException { - txMgr.releaseLock(tInfo, lockToken, this); - } - - /** - * @see TransactionResource#getTransactionId() - */ - public String getTransactionId() { - return transactionId; - } - - //-------------------------------------------< DeltaVResource interface >--- - /** - * @param optionsInfo - * @return object to be used in the OPTIONS response body or null - * @see DeltaVResource#getOptionResponse(org.apache.jackrabbit.webdav.version.OptionsInfo) - */ - public OptionsResponse getOptionResponse(OptionsInfo optionsInfo) { - OptionsResponse oR = null; - if (optionsInfo != null) { - oR = new OptionsResponse(); - // currently on DAV:version-history-collection-set and - // DAV:workspace-collection-set is supported. - if (optionsInfo.containsElement(DeltaVConstants.XML_VH_COLLECTION_SET, DeltaVConstants.NAMESPACE)) { - String[] hrefs = new String[] { getLocatorFromResourcePath(ItemResourceConstants.VERSIONSTORAGE_PATH).getHref(true)}; - oR.addEntry(DeltaVConstants.XML_VH_COLLECTION_SET, DeltaVConstants.NAMESPACE, hrefs); - } else if (optionsInfo.containsElement(DeltaVConstants.XML_WSP_COLLECTION_SET, DeltaVConstants.NAMESPACE)) { - // workspaces cannot be created anywhere. - oR.addEntry(DeltaVConstants.XML_WSP_COLLECTION_SET, DeltaVConstants.NAMESPACE, new String[0]); - } - } - return oR; - } - - /** - * @param reportInfo - * @return the requested report - * @throws DavException - * @see DeltaVResource#getReport(org.apache.jackrabbit.webdav.version.report.ReportInfo) - */ - public Report getReport(ReportInfo reportInfo) throws DavException { - if (reportInfo == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "A REPORT request must provide a valid XML request body."); - } - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - if (supportedReports.isSupportedReport(reportInfo)) { - try { - Report report = ReportType.getType(reportInfo).createReport(); - report.setInfo(reportInfo); - report.setResource(this); - return report; - } catch (IllegalArgumentException e) { - // should never occur. - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); - } - } else { - throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY, "Unkown report "+ reportInfo.getReportElement().getNamespacePrefix() + reportInfo.getReportElement().getName() +"requested."); - } - } - - /** - * The JCR api does not provide methods to create new workspaces. Calling - * addWorkspace on this resource will always fail. - * - * @param workspace - * @throws DavException Always throws. - * @see DeltaVResource#addWorkspace(org.apache.jackrabbit.webdav.DavResource) - */ - public void addWorkspace(DavResource workspace) throws DavException { - throw new DavException(DavServletResponse.SC_FORBIDDEN); - } - - /** - * Return an array of DavResource objects that are referenced - * by the property with the specified name. - * - * @param hrefPropertyName - * @return array of DavResources - * @throws DavException - * @see DeltaVResource#getReferenceResources(org.apache.jackrabbit.webdav.property.DavPropertyName) - */ - public DavResource[] getReferenceResources(DavPropertyName hrefPropertyName) throws DavException { - DavProperty prop = getProperty(hrefPropertyName); - if (prop == null || !(prop instanceof HrefProperty)) { - throw new DavException(DavServletResponse.SC_CONFLICT, "Unknown Href-Property '"+hrefPropertyName+"' on resource "+getResourcePath()); - } - - List hrefs = ((HrefProperty)prop).getHrefs(); - DavResource[] refResources = new DavResource[hrefs.size()]; - Iterator hrefIter = hrefs.iterator(); - int i = 0; - while (hrefIter.hasNext()) { - refResources[i] = getResourceFromHref((String)hrefIter.next()); - i++; - } - return refResources; - } - - /** - * Retrieve the DavResource object that is represented by - * the given href String. - * - * @param href - * @return DavResource object - */ - private DavResource getResourceFromHref(String href) throws DavException { - // build a new locator: remove trailing prefix - DavResourceLocator locator = getLocator(); - String prefix = locator.getPrefix(); - if (href.startsWith(prefix)) { - href = href.substring(prefix.length()); - } - DavResourceLocator loc = locator.getFactory().createResourceLocator(prefix, href); - - // create a new resource object - DavResource res; - if (getRepositorySession().itemExists(loc.getResourcePath())) { - res = createResourceFromLocator(loc); - } else { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - return res; - } - - //-------------------------------------------------------------------------- - /** - * Fill the set of default properties - */ - protected void initProperties() { - if (getDisplayName() != null) { - properties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, getDisplayName())); - } - if (isCollection()) { - properties.add(new ResourceType(ResourceType.COLLECTION)); - // Windows XP support - properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, "1")); - } else { - properties.add(new ResourceType(ResourceType.DEFAULT_RESOURCE)); - // Windows XP support - properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, "0")); - } - // todo: add etag - - // default last modified - setModificationTime(new Date().getTime()); - // default creation time - properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, DavConstants.creationDateFormat.format(new Date(0)))); - - // supported lock property - properties.add(supportedLock); - - // set current lock information. If no lock is applied to this resource, - // an empty lockdiscovery will be returned in the response. - properties.add(new LockDiscovery(getLocks())); - - // observation resource - SubscriptionDiscovery subsDiscovery = subsMgr.getSubscriptionDiscovery(this); - properties.add(subsDiscovery); - - properties.add(new SupportedMethodSetProperty(getSupportedMethods().split(",\\s"))); - - // DeltaV properties - properties.add(supportedReports); - // creator-displayname, comment: not value available from jcr - properties.add(new DefaultDavProperty(DeltaVConstants.CREATOR_DISPLAYNAME, null, true)); - properties.add(new DefaultDavProperty(DeltaVConstants.COMMENT, null, true)); - - // 'workspace' property as defined by RFC 3253 - String workspaceHref = getWorkspaceHref(); - if (workspaceHref != null) { - properties.add(new HrefProperty(DeltaVConstants.WORKSPACE, workspaceHref, true)); - } - // TODO: required supported-live-property-set - } - - /** - * Create a new DavResource from the given locator. - * @param loc - * @return new DavResource - */ - protected DavResource createResourceFromLocator(DavResourceLocator loc) - throws DavException { - DavResource res = factory.createResource(loc, session); - if (res instanceof AbstractResource) { - ((AbstractResource)res).transactionId = this.transactionId; - } - return res; - } - - /** - * Build a DavResourceLocator from the given resource path. - * - * @param resourcePath - * @return a new DavResourceLocator - * @see DavLocatorFactory#createResourceLocator(String, String, String) - */ - protected DavResourceLocator getLocatorFromResourcePath(String resourcePath) { - DavResourceLocator loc = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), resourcePath); - return loc; - } - - /** - * Retrieve the name/label of a repository item from the given href by - * splitting of the part after the last slash. If the removeIndex - * flag is set to true, any trailing index (e.g. '[1]') will be removed. - * - * @param resourceHref - * @param removeIndex - * @return the name of the item - */ - protected static String getResourceName(String resourceHref, boolean removeIndex) { - if (resourceHref == null) { - return resourceHref; - } - - // cut the extension - int pos = resourceHref.lastIndexOf('.'); - if (pos > 0) { - resourceHref = resourceHref.substring(pos+1); - } else if (resourceHref.endsWith("/")) { - resourceHref = resourceHref.substring(0, resourceHref.length()-1); - } - - // retrieve the last part of the path - String name = Text.getLabel(resourceHref); - // remove index - if (removeIndex) { - if (name.endsWith("]")) { - name = name.substring(0, name.lastIndexOf('[')); - } - } - return name; - } - - /** - * Shortcut for getSession().getRepositorySession() - * - * @return repository session present in the {@link #session}. - */ - protected Session getRepositorySession() { - return getSession().getRepositorySession(); - } - - /** - * Define the set of locks supported by this resource. - * - * @see org.apache.jackrabbit.webdav.lock.SupportedLock - */ - abstract protected void initLockSupport(); - - /** - * Define the set of reports supported by this resource. - * - * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty - */ - abstract protected void initSupportedReports(); - - /** - * Retrieve the href of the workspace the current session belongs to. - * - * @return href of the workspace - */ - abstract protected String getWorkspaceHref(); -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DavLocatorFactoryImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DavLocatorFactoryImpl.java deleted file mode 100644 index c32008f6275..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DavLocatorFactoryImpl.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavResourceLocator; -import org.apache.jackrabbit.webdav.DavLocatorFactory; - -/** - * DavLocatorFactoryImpl... - */ -public class DavLocatorFactoryImpl implements DavLocatorFactory { - - private static Logger log = Logger.getLogger(DavLocatorFactoryImpl.class); - - private final String pathPrefix; - - /** - * Create a new factory - * - * @param pathPrefix Prefix, that needs to be removed in order to retrieve - * the path of the repository item from a given DavResourceLocator. - */ - public DavLocatorFactoryImpl(String pathPrefix) { - this.pathPrefix = pathPrefix; - } - - /** - * Create a new DavResourceLocator. Any leading - * path-prefix (as defined with the constructor) and trailing '/' with - * the request handle is removed. The first label of the remaining handle is - * treated as workspace name. The remaining part of the given request handle - * is said to be the resource handle ("/" if an empty string remains). - * If the request handle does neither provide workspace name nor resource - * handle both values are set to null; the path object then - * represents the root resource that has no corresponding item in the JCR - * repository. - * - * @param prefix - * @param requestHandle - * @return a new DavResourceLocator - * @throws IllegalArgumentException if the request handle is null - */ - public DavResourceLocator createResourceLocator(String prefix, String requestHandle) { - if (requestHandle == null) { - throw new IllegalArgumentException("Request handle must not be null."); - } - - StringBuffer b = new StringBuffer(""); - if (prefix != null) { - b.append(prefix); - if (pathPrefix != null && !prefix.endsWith(pathPrefix)) { - b.append(pathPrefix); - } - } - String rlPrefix = b.toString(); - - // remove path-prefix defined with the servlet that may preceed the - // the requestHandle - if (pathPrefix != null && requestHandle.startsWith(pathPrefix)) { - requestHandle = requestHandle.substring(pathPrefix.length()); - } - - // remove trailing "/" that is present with collections - if (requestHandle.endsWith("/")) { - requestHandle = requestHandle.substring(0, requestHandle.length()-1); - } - - String resourcePath; - String workspacePath; - - // an empty requestHandle (after removal of the "/") signifies a request - // to the root that does not represent a repository item. - if ("".equals(requestHandle)) { - resourcePath = null; - workspacePath = null; - } else { - // look for the first slash ignoring the leading one - int pos = requestHandle.indexOf('/', 1); - if (pos == -1) { - // request to a 'workspace' resource that in the same time - // represent the root node of the repository. - workspacePath = requestHandle; - resourcePath = ItemResourceConstants.ROOT_ITEM_PATH; - } else { - // separate the workspace name from the path of the repository - // item. - workspacePath = requestHandle.substring(0, pos); - resourcePath = requestHandle.substring(pos); - } - } - - return new DavResourceLocatorImpl(rlPrefix, workspacePath, resourcePath, this); - } - - /** - * Create a new DavResourceLocator from the specified prefix, - * workspace path and resource path, whithout modifying the specified Strings. - * - * @param prefix - * @param workspacePath - * @param resourcePath - * @return a new DavResourceLocator - * @see DavLocatorFactory#createResourceLocator(String, String, String) - */ - public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) { - return new DavResourceLocatorImpl(prefix, workspacePath, resourcePath, this); - } - - /** - * Private inner class DavResourceLocatorImpl implementing - * the DavResourceLocator interface. - */ - private class DavResourceLocatorImpl implements DavResourceLocator { - - private final String prefix; - private final String workspacePath; - private final String resourcePath; - private final DavLocatorFactory factory; - - /** - * Create a new DavResourceLocatorImpl. - * - * @param prefix - * @param workspacePath - * @param resourcePath - */ - DavResourceLocatorImpl(String prefix, String workspacePath, String resourcePath, DavLocatorFactory factory) { - this.prefix = prefix; - this.workspacePath = workspacePath; - this.resourcePath = resourcePath; - this.factory = factory; - } - - /** - * Return the prefix used to build the href String. This includes the initial - * hrefPrefix as well a the path prefix. - * - * @return prefix String used to build the href. - */ - public String getPrefix() { - return prefix; - } - - /** - * Return the resource path of null if this locator object - * represents the '/' request handle. To a request handle specifying a - * workspace name only the '/' resource path is assigned, which represents - * the root node of the repository. - * - * @return resource path or null - * @see org.apache.jackrabbit.webdav.DavResourceLocator#getResourcePath() - */ - public String getResourcePath() { - return resourcePath; - } - - /** - * Return the workspace path or null if this locator object - * represents the '/' request handle. - * - * @return workspace path or null - * @see org.apache.jackrabbit.webdav.DavResourceLocator#getWorkspacePath() - */ - public String getWorkspacePath() { - return workspacePath; - } - - /** - * Return the workspace name or null if this locator object - * represents the '/' request handle. - * - * @return workspace name or null - * @see org.apache.jackrabbit.webdav.DavResourceLocator#getWorkspaceName() - */ - public String getWorkspaceName() { - if (workspacePath != null) { - return workspacePath.substring(1); - } - return null; - } - - /** - * Returns true if the specified locator object refers to a resource within - * the same workspace. - * - * @param locator - * @return true if the workspace name is equal to this workspace name. - * @see DavResourceLocator#isSameWorkspace(org.apache.jackrabbit.webdav.DavResourceLocator) - */ - public boolean isSameWorkspace(DavResourceLocator locator) { - return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName()); - } - - /** - * Returns true if the specified string equals to this workspace name or - * if this workspace name is null. - * - * @param workspaceName - * @return true if the workspace name is equal to this workspace name. - * @see DavResourceLocator#isSameWorkspace(String) - */ - public boolean isSameWorkspace(String workspaceName) { - if (getWorkspaceName() == null) { - return true; - } else { - return getWorkspaceName().equals(workspaceName); - } - } - - /** - * Builds the 'href' from the prefix, the workspace name and the - * resource path present and assures a trailing '/' in case the href - * is used for collection. - * - * @param isCollection - * @return href String representing the text of the href element - * @see org.apache.jackrabbit.webdav.DavConstants#XML_HREF - * @see DavResourceLocator#getHref(boolean) - */ - public String getHref(boolean isCollection) { - StringBuffer href = new StringBuffer(prefix); - if (workspacePath != null) { - href.append(workspacePath); - } - if (resourcePath != null) { - href.append(resourcePath); - } - if (isCollection && href.charAt(href.length()-1) != '/') { - href.append("/"); - } - return href.toString(); - } - - /** - * Returns true if the 'workspaceName' field is null. - * - * @return true if the 'workspaceName' field is null. - * @see org.apache.jackrabbit.webdav.DavResourceLocator#isRootLocation() - */ - public boolean isRootLocation() { - return workspacePath == null; - } - - /** - * Return the factory that created this locator. - * - * @return factory - * @see org.apache.jackrabbit.webdav.DavResourceLocator#getFactory() - */ - public DavLocatorFactory getFactory() { - return factory; - } - - /** - * Computes the hash code using the prefix, the workspace name and the - * resource path. - * - * @return the hash code - */ - public int hashCode() { - int hashCode = prefix.hashCode(); - if (workspacePath != null) { - hashCode += workspacePath.hashCode(); - } - if (resourcePath != null) { - hashCode += resourcePath.hashCode(); - } - return hashCode % Integer.MAX_VALUE; - } - - /** - * Equality of locators is achieved if prefix and resource path - * are equal. - * - * @param obj the object to compare to - * @return true if the 2 objects are equal; - * false otherwise - */ - public boolean equals(Object obj) { - if (obj instanceof DavResourceLocatorImpl) { - DavResourceLocatorImpl locator = (DavResourceLocatorImpl) obj; - boolean equalWsName = (workspacePath == null) ? locator.workspacePath == null : workspacePath.equals(locator.workspacePath); - boolean equalRPath = (resourcePath == null) ? locator.resourcePath == null : resourcePath.equals(locator.resourcePath); - - return prefix.equals(locator.prefix) && equalWsName && equalRPath; - } - return false; - } - } -} - diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DavResourceFactoryImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DavResourceFactoryImpl.java deleted file mode 100644 index 13907788470..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DavResourceFactoryImpl.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.transaction.TransactionResource; -import org.apache.jackrabbit.webdav.transaction.TransactionDavServletRequest; -import org.apache.jackrabbit.webdav.observation.SubscriptionManager; -import org.apache.jackrabbit.webdav.observation.ObservationResource; -import org.apache.jackrabbit.webdav.version.DeltaVServletRequest; -import org.apache.jackrabbit.webdav.version.VersionControlledResource; -import org.apache.jackrabbit.webdav.spi.version.VersionItemCollection; -import org.apache.jackrabbit.webdav.spi.version.VersionHistoryItemCollection; -import org.apache.jackrabbit.webdav.spi.transaction.TxLockManagerImpl; - -import javax.jcr.*; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; - -/** - * DavResourceFactoryImpl... - */ -public class DavResourceFactoryImpl implements DavResourceFactory { - - private static Logger log = Logger.getLogger(DavResourceFactoryImpl.class); - - private final TxLockManagerImpl txMgr; - private final SubscriptionManager subsMgr; - - /** - * Create a new DavResourceFactoryImpl. - * - * @param txMgr - * @param subsMgr - */ - public DavResourceFactoryImpl(TxLockManagerImpl txMgr, SubscriptionManager subsMgr) { - this.txMgr = txMgr; - this.subsMgr = subsMgr; - } - - /** - * Create a new DavResource from the specified locator and request - * objects. Note, that in contrast to - * {@link #createResource(DavResourceLocator, DavSession)} the locator may - * point to a non-existing resource. - *

- * If the request contains a {@link org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getLabel() - * Label header}, the resource is build from the indicated - * {@link org.apache.jackrabbit.webdav.version.VersionResource version} instead. - * - * @param locator - * @param request - * @param response - * @return - * @see DavResourceFactory#createResource(org.apache.jackrabbit.webdav.DavResourceLocator, org.apache.jackrabbit.webdav.DavServletRequest, org.apache.jackrabbit.webdav.DavServletResponse) - */ - public DavResource createResource(DavResourceLocator locator, - DavServletRequest request, - DavServletResponse response) throws DavException { - - DavResource resource = null; - DavSession session = request.getDavSession(); - - if (locator.isRootLocation()) { - resource = new RootCollection(locator, session, this); - } - - if (resource == null) { - try { - resource = createResourceForItem(locator, session); - } catch (RepositoryException e) { - // create the default resources if no such item exists - - // MKCOL request forces a collection-resource even if there already - // exists a repository-property with the given path. the MKCOL will - // in that particular case fail with a 405 (method not allowed). - if (DavMethods.getMethodCode(request.getMethod()) == DavMethods.DAV_MKCOL) { - resource = new VersionControlledItemCollection(locator, session, this); - } else { - resource = new DefaultItemResource(locator, session, this); - } - } - - // if the created resource is version-controlled and the request - // contains a Label header, the corresponding Version must be used - // instead. - if (request instanceof DeltaVServletRequest && isVersionControlled(resource)) { - String labelHeader = ((DeltaVServletRequest)request).getLabel(); - if (labelHeader != null && DavMethods.isMethodAffectedByLabel(request.getMethod())) { - try { - Item item = session.getRepositorySession().getItem(locator.getResourcePath()); - Version v = ((Node)item).getVersionHistory().getVersionByLabel(labelHeader); - DavResourceLocator vloc = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), v.getPath()); - resource = new VersionItemCollection(vloc, session, this); - } catch (RepositoryException e) { - log.error("Failed to build version resource from "+locator.getHref(true)+" and label "+labelHeader); - throw new JcrDavException(e); - } - } - } - } - - ((TransactionResource)resource).init(txMgr, ((TransactionDavServletRequest)request).getTransactionId()); - ((ObservationResource)resource).init(subsMgr); - - return resource; - } - - /** - * Create a new DavResource from the given locator and session. - * - * @param locator - * @param session - * @return DavResource representing either a repository item or the {@link RootCollection}. - * @throws DavException if the given locator does neither refer to a repository item - * nor does represent the {@link org.apache.jackrabbit.webdav.DavResourceLocator#isRootLocation() - * root location}. - */ - public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { - DavResource resource; - try { - resource = createResourceForItem(locator, session); - } catch (RepositoryException e) { - log.info("Creating resource for non-existing repository item ..."); - if (locator.isRootLocation()) { - resource = new RootCollection(locator, session, this); - } else { - // todo: is this correct? - resource = new VersionControlledItemCollection(locator, session, this); - } - } - - resource.addLockManager(txMgr); - ((ObservationResource)resource).init(subsMgr); - - return resource; - } - - /** - * Tries to retrieve the repository item defined by the locator's resource - * path and build the corresponding WebDAV resource. The following distinction - * is made between items: Version nodes, VersionHistory nodes, root node, - * unspecified nodes and finally property items. - * - * @param locator - * @param session - * @return DavResource representing a repository item. - * @throws RepositoryException if {@link Session#getItem(String)} fails. - */ - private DavResource createResourceForItem(DavResourceLocator locator, DavSession session) throws RepositoryException { - DavResource resource; - Item item = session.getRepositorySession().getItem(locator.getResourcePath()); - if (item.isNode()) { - // create special resources for Version and VersionHistory - if (item instanceof Version) { - resource = new VersionItemCollection(locator, session, this); - } else if (item instanceof VersionHistory) { - resource = new VersionHistoryItemCollection(locator, session, this); - } else if (ItemResourceConstants.ROOT_ITEM_PATH.equals(locator.getResourcePath())) { - resource = new RootItemCollection(locator, session, this); - } else{ - resource = new VersionControlledItemCollection(locator, session, this); - } - } else { - resource = new DefaultItemResource(locator, session, this); - } - return resource; - } - - /** - * Returns true, if the specified resource is a {@link VersionControlledResource} - * and has a version history. - * - * @param resource - * @return true if the specified resource is version-controlled. - */ - private boolean isVersionControlled(DavResource resource) { - boolean vc = false; - if (resource instanceof VersionControlledResource) { - try { - vc = ((VersionControlledResource)resource).getVersionHistory() != null; - } catch (DavException e) { - log.debug("Resource '" + resource.getHref() + "' is not version-controlled."); - } - } - return vc; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DefaultItemCollection.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DefaultItemCollection.java deleted file mode 100644 index 438865eac8d..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DefaultItemCollection.java +++ /dev/null @@ -1,729 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.spi.lock.JcrActiveLock; -import org.apache.jackrabbit.webdav.spi.nodetype.NodeTypeProperty; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.ordering.*; -import org.apache.jackrabbit.webdav.util.Text; -import org.apache.jackrabbit.webdav.lock.*; - -import javax.jcr.*; -import javax.jcr.lock.Lock; -import javax.jcr.nodetype.NodeType; -import java.util.*; -import java.io.*; - -/** - * DefaultItemCollection represents a JCR node item. - */ -public class DefaultItemCollection extends AbstractItemResource - implements OrderingResource { - - private static Logger log = Logger.getLogger(DefaultItemCollection.class); - - private InputStream in; - - /** - * Create a new DefaultItemCollection. - * - * @param locator - * @param session - */ - protected DefaultItemCollection(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - super(locator, session, factory); - if (exists() && !(item instanceof Node)) { - throw new IllegalArgumentException("A collection resource can not be constructed from a Property item."); - } - } - - //----------------------------------------------< DavResource interface >--- - /** - * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() - */ - public String getComplianceClass() { - StringBuffer sb = new StringBuffer(super.getComplianceClass()); - sb.append(", ").append(OrderingResource.COMPLIANCE_CLASS); - return sb.toString(); - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() - */ - public String getSupportedMethods() { - StringBuffer sb = new StringBuffer(super.getSupportedMethods()); - // Ordering - if (isOrderable()) { - sb.append(", ").append(OrderingResource.METHODS); - } - return sb.toString(); - } - - /** - * Always returns true - * - * @return true - * @see org.apache.jackrabbit.webdav.DavResource#isCollection() - */ - public boolean isCollection() { - return true; - } - - - /** - * Returns an {@link java.io.InputStream} to the content of this collection. - * - * @return - * @see org.apache.jackrabbit.webdav.DavResource#getStream() - */ - public InputStream getStream() { - initProperties(); - return in; - } - - /** - * This implementation of the DavResource does only allow - * to set the jcr:mixinnodetypes property. Please note that the existing list of - * mixin nodetypes will be completely replaces.
- * In order to add / set any other repository property on the underlaying - * {@link javax.jcr.Node} use addMember(DavResource) or - * addMember(DavResource, InputStream) or modify the value - * of the corresponding resource. - * - * @param property - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) - * @see #JCR_MIXINNODETYPES - * @todo undo incomplete modifications... - */ - public void setProperty(DavProperty property) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (property.getName().equals(JCR_MIXINNODETYPES)) { - try { - Node n = (Node)item; - NodeType[] existingMixin = n.getMixinNodeTypes(); - NodeTypeProperty mix = new NodeTypeProperty(property); - Set mixins = mix.getNodeTypeNames(); - - for (int i = 0; i < existingMixin.length; i++) { - String name = existingMixin[i].getName(); - if (mixins.contains(name)){ - // do not add existing mixins - mixins.remove(name); - } else { - // remove mixin that are not contained in the new list - n.removeMixin(name); - } - } - - // add the remaining mixing types that are not yet set - Iterator it = mixins.iterator(); - while (it.hasNext()) { - n.addMixin((String)it.next()); - } - complete(); - - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } else { - // all props except for mixinnodetypes are read-only - throw new DavException(DavServletResponse.SC_CONFLICT); - } - } - - /** - * This implementation of the DavResource does only allow - * to remove the jcr:mixinnodetypes property. - * - * @param propertyName - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) - * @see #JCR_MIXINNODETYPES - * @todo undo incomplete modifications... - */ - public void removeProperty(DavPropertyName propertyName) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (JCR_MIXINNODETYPES.equals(propertyName)) { - // remove all mixin nodetypes - try { - Node n = (Node)item; - NodeType[] mixins = n.getMixinNodeTypes(); - for (int i = 0; i < mixins.length; i++) { - n.removeMixin(mixins[i].getName()); - } - complete(); - - } catch (RepositoryException e) { - // NoSuchNodeTypeException, ConstraintViolationException should never occur... - throw new JcrDavException(e); - } - } else { - // all props except for mixinnodetypes are read-only - throw new DavException(DavServletResponse.SC_CONFLICT); - } - } - - /** - * If the specified resource represents a collection, a new node is {@link Node#addNode(String) - * added} to the item represented by this resource. If an input stream is specified - * together with a collection resource {@link Session#importXML(String, java.io.InputStream)} - * is called instead and this resource path is used as parentAbsPath argument. - *

- * However, if the specified resource is not of resource type collection a - * new {@link Property} is set or an existing one is changed by modifying its - * value.
- * NOTE: with the current implementation it is not possible to create or - * modify multivalue JCR properties.
- * NOTE: if the JCR property represented by the specified resource has an - * {@link PropertyType#UNDEFINED undefined} resource type, its value will be - * changed/set to type {@link PropertyType#BINARY binary}. - * - * @param resource - * @param in - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.DavResource#addMember(org.apache.jackrabbit.webdav.DavResource, java.io.InputStream) - * @see Node#addNode(String) - * @see Node#setProperty(String, java.io.InputStream) - */ - public void addMember(DavResource resource, InputStream in) - throws DavException { - - /* RFC 2815 states that all 'parents' must exist in order all addition of members */ - if (!exists()) { - throw new DavException(DavServletResponse.SC_CONFLICT); - } - - try { - Node n = (Node) item; - if (resource.isCollection()) { - if (in == null) { - // MKCOL without a request body, try if a default-primary-type is defined. - n.addNode(resource.getDisplayName()); - } else { - // MKCOL, which is not allowed for existing resources - getRepositorySession().importXML(getResourcePath(), in); - } - } else { - if (in == null) { - // PUT: not possible - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Cannot create a new non-collection resource without request body."); - } else { - // TODO: find a way to create non-binary and multivalue properties - // PUT : create new or overwrite existing property. - // NOTE: will fail for multivalue properties. - n.setProperty(getResourceName(resource.getResourcePath(), true), in); - } - } - complete(); - } catch (ItemExistsException e) { - // according to RFC 2518: MKCOL only possible on non-existing/deleted resource - throw new JcrDavException(e, DavServletResponse.SC_METHOD_NOT_ALLOWED); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } catch (IOException e) { - throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY, e.getMessage()); - } - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#addMember(org.apache.jackrabbit.webdav.DavResource) - */ - public void addMember(DavResource resource) throws DavException { - addMember(resource, resource.getStream()); - } - - /** - * @see org.apache.jackrabbit.webdav.DavResource#getMembers() - */ - public DavResourceIterator getMembers() { - ArrayList memberList = new ArrayList(); - if (exists()) { - try { - Node n = (Node)item; - // add all node members - NodeIterator it = n.getNodes(); - while (it.hasNext()) { - Node node = it.nextNode(); - DavResourceLocator loc = getLocatorFromItem(node); - memberList.add(createResourceFromLocator(loc)); - } - // add all property members - PropertyIterator propIt = n.getProperties(); - while (propIt.hasNext()) { - Property prop = propIt.nextProperty(); - DavResourceLocator loc = getLocatorFromItem(prop); - memberList.add(createResourceFromLocator(loc)); - } - } catch (RepositoryException e) { - // ignore - log.error(e.getMessage()); - } catch (DavException e) { - // should never occur. - log.error(e.getMessage()); - } - } - return new DavResourceIteratorImpl(memberList); - } - - /** - * Removes the repository item represented by the specified member - * resource. - * - * @throws DavException if this resource does not exist or if an error occurs - * while deleting the underlaying item. - * @see DavResource#removeMember(DavResource) - * @see javax.jcr.Item#remove() - */ - public void removeMember(DavResource member) throws DavException { - Session session = getRepositorySession(); - if (!exists() || !session.itemExists(member.getResourcePath())) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!getResourcePath().equals(Text.getRelativeParent(member.getResourcePath(), 1))) { - throw new DavException(DavServletResponse.SC_CONFLICT, member.getResourcePath() + "is not member of this resource (" + getResourcePath() + ")"); - } - try { - session.getItem(member.getResourcePath()).remove(); - complete(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * @param type - * @param scope - * @return true if a lock with the specified type and scope is present on - * this resource, false otherwise. If retrieving the corresponding information - * fails, false is returned. - * @see org.apache.jackrabbit.webdav.DavResource#hasLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) - */ - public boolean hasLock(Type type, Scope scope) { - if (isLockable(type, scope)) { - if (Type.WRITE.equals(type)) { - try { - return ((Node) item).isLocked(); - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - } else { - return super.hasLock(type, scope); - } - } - return false; - } - - /** - * Retrieve the lock with the specified type and scope. - * - * @param type - * @param scope - * @return lock with the specified type and scope is present on this - * resource or null. NOTE: If retrieving the write lock present - * on the underlaying repository item fails, null is return. - * @see org.apache.jackrabbit.webdav.DavResource#getLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) - * @see javax.jcr.Node#getLock() for the write locks. - */ - public ActiveLock getLock(Type type, Scope scope) { - ActiveLock lock = null; - if (isLockable(type, scope)) { - if (Type.WRITE.equals(type)) { - try { - if (!exists()) { - log.warn("Unable to retrieve lock: no item found at '" + getResourcePath() + "'"); - } else if (((Node) item).isLocked()) { - Lock jcrLock = ((Node) item).getLock(); - // TODO: find out whether this lock is session-scoped or not! - lock = new JcrActiveLock(jcrLock); - } - } catch (AccessDeniedException e) { - log.error("Error while accessing resource lock: "+e.getMessage()); - } catch (UnsupportedRepositoryOperationException e) { - log.error("Error while accessing resource lock: "+e.getMessage()); - } catch (RepositoryException e) { - log.error("Error while accessing resource lock: "+e.getMessage()); - } - } else { - lock = super.getLock(type, scope); - } - } - return lock; - } - - /** - * Creates a lock on this resource by locking the underlaying - * {@link javax.jcr.Node node}. Except for the {@link org.apache.jackrabbit.webdav.lock.LockInfo#isDeep()} } - * all information included in the LockInfo object is ignored. - * Lock timeout is defined by JCR implementation. - * - * @param reqLockInfo - * @return lock object representing the lock created on this resource. - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.DavResource#lock(org.apache.jackrabbit.webdav.lock.LockInfo) - * @see Node#lock(boolean, boolean) - */ - public ActiveLock lock(LockInfo reqLockInfo) throws DavException { - - if (!isLockable(reqLockInfo.getType(), reqLockInfo.getScope())) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } - - if (Type.WRITE.equals(reqLockInfo.getType())) { - if (!exists()) { - log.warn("Cannot create a write lock for non-existing JCR node (" + getResourcePath() + ")"); - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - try { - boolean sessionScoped = EXCLUSIVE_SESSION.equals(reqLockInfo.getScope()); - Lock jcrLock = ((Node)item).lock(reqLockInfo.isDeep(), sessionScoped); - return new JcrActiveLock(jcrLock, sessionScoped); - - } catch (RepositoryException e) { - // UnsupportedRepositoryOperationException should not occur... - throw new JcrDavException(e); - } - } else { - return super.lock(reqLockInfo); - } - } - - /** - * Refreshes the lock on this resource. With this implementation the - * {@link javax.jcr.lock lock} present on the underlaying {@link javax.jcr.Node node} - * is refreshed. The timeout indicated by the LockInfo - * object is ignored. - * - * @param reqLockInfo LockInfo as build from the request. - * @param lockToken - * @return the updated lock info object. - * @throws org.apache.jackrabbit.webdav.DavException in case the lock could not be refreshed. - * @see org.apache.jackrabbit.webdav.DavResource#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String) - * @see javax.jcr.lock.Lock#refresh() - */ - public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) - throws DavException { - - if (lockToken == null) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } - - ActiveLock lock = getWriteLock(); - if (lock != null && lockToken.equals(lock.getToken())) { - try { - Lock jcrLock = ((Node) item).getLock(); - jcrLock.refresh(); - return new JcrActiveLock(jcrLock, EXCLUSIVE_SESSION.equals(lock.getScope())); - } catch (RepositoryException e) { - /* - NOTE: LockException is only thrown by Lock.refresh() - the lock exception thrown by Node.getLock() was circumvented - by the init test if there is a lock applied... - NOTE: UnsupportedRepositoryOperationException should not occur - */ - throw new JcrDavException(e); - } - } else { - return super.refreshLock(reqLockInfo, lockToken); - } - } - - /** - * Remove the write lock from this resource by unlocking the underlaying - * {@link javax.jcr.Node node}. - * - * @param lockToken - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.DavResource#unlock(String) - * @see javax.jcr.Node#unlock() - */ - public void unlock(String lockToken) throws DavException { - ActiveLock lock = getWriteLock(); - if (lock != null && lockToken.equals(lock.getToken())) { - try { - ((Node) item).unlock(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } else { - super.unlock(lockToken); - } - } - - /** - * Returns the write lock present on this resource or null if - * no write lock exists. NOTE: that the scope of a write lock may either - * be {@link org.apache.jackrabbit.webdav.lock.Scope#EXCLUSIVE} or {@link #EXCLUSIVE_SESSION}. - * - * @return write lock or null - * @throws DavException if this resource does not represent a repository item. - */ - private ActiveLock getWriteLock() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND, "Unable to retrieve write lock for non existing repository item (" + getResourcePath() + ")"); - } - ActiveLock writeLock = getLock(Type.WRITE, Scope.EXCLUSIVE); - if (writeLock == null) { - writeLock = getLock(Type.WRITE, EXCLUSIVE_SESSION); - } - return writeLock; - } - - //-----------------------------------------< OrderingResource interface >--- - /** - * Returns true if this resource exists and the nodetype defining the - * underlaying repository node allow to reorder this nodes children. - * - * @return true if {@link #orderMembers(OrderPatch)} can be called on this - * resource. - * @see org.apache.jackrabbit.webdav.ordering.OrderingResource#isOrderable() - * @see javax.jcr.nodetype.NodeType#hasOrderableChildNodes() - */ - public boolean isOrderable() { - boolean orderable = false; - if (exists()) { - try { - orderable = ((Node) item).getPrimaryNodeType().hasOrderableChildNodes(); - } catch (RepositoryException e) { - log.warn(e.getMessage()); - } - } - return orderable; - } - - /** - * Reorder the child nodes of the repository item represented by this - * resource as indicated by the specified {@link OrderPatch} object. - * - * @param orderPatch - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.ordering.OrderingResource#orderMembers(org.apache.jackrabbit.webdav.ordering.OrderPatch) - * @see Node#orderBefore(String, String) - */ - public void orderMembers(OrderPatch orderPatch) throws DavException { - if (!isOrderable()) { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - // only custom ordering is allowed - if (!OrderingConstants.ORDERING_TYPE_CUSTOM.equalsIgnoreCase(orderPatch.getOrderingType())) { - throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY,"Only DAV:custom ordering type supported."); - } - - OrderPatch.Member[] instructions = orderPatch.getOrderInstructions(); - Node n = (Node)item; - try { - for (int i = 0; i < instructions.length; i++) { - String srcRelPath = getResourceName(n.getPath() + instructions[i].getMemberHandle(), false); - Position pos = instructions[i].getPosition(); - String destRelPath = getRelDestinationPath(pos, n.getNodes()); - - n.orderBefore(srcRelPath, destRelPath); - } - } catch (RepositoryException e) { - // UnsupportedRepositoryException should not occur - throw new JcrDavException(e); - } - } - - /** - * Retrieve the relative path of the child node that acts as destination. - * A null destination path is used to place the child node indicated - * by the source path at the end of the list. - * - * @param position - * @param childNodes - * @return the relative path of the child node used as destination or null - * if the source node should be placed at the last position. - * @throws javax.jcr.RepositoryException - */ - private String getRelDestinationPath(Position position, NodeIterator childNodes) - throws RepositoryException { - - String destRelPath = null; - if (position.getType() == Position.TYPE_FIRST) { - while (childNodes.hasNext()) { - Node firstChild = childNodes.nextNode(); - destRelPath = firstChild.getPath(); - } - // no child nodes available > reordering to 'first' position fails. - if (destRelPath == null) { - throw new ItemNotFoundException("No 'first' item found for reordering."); - } - } else if (position.getType() == Position.TYPE_AFTER) { - String afterRelPath = getResourceName(position.getSegment(), false); - boolean found = false; - while (childNodes.hasNext() && destRelPath == null) { - String childPath = childNodes.nextNode().getPath(); - if (found) { - destRelPath = childPath; - } else { - found = afterRelPath.equals(Text.getLabel(childPath)); - } - } - } else { - destRelPath = position.getSegment(); - } - - if (destRelPath != null) { - destRelPath = getResourceName(destRelPath, false); - } - - return destRelPath; - } - - //-------------------------------------------------------------------------- - /** - * Extend the general {@link #supportedLock} field by lock entries specific for this - * resource: write locks (exclusive or exclusive session-scoped) in case the underlaying - * node has the node type mix:lockable. - * - * @see #MIX_LOCKABLE - */ - protected void initLockSupport() { - super.initLockSupport(); - // add exclusive write lock if allowed for the given node - try { - if (exists() && ((Node)item).isNodeType(MIX_LOCKABLE)) { - supportedLock.addEntry(Type.WRITE, Scope.EXCLUSIVE); - // TODO: do session-scoped lock properly (including session caching and proper scope discovery) - //supportedLock.addEntry(new SessionScopedLockEntry()); - } - } catch (RepositoryException e) { - log.warn(e.getMessage()); - } - } - - /** - * Fill the property set for this resource. - */ - protected void initProperties() { - super.initProperties(); - if (exists()) { - try { - String prefix = "_tmp_" + item.getName(); - // create tmpFile in default system-tmp directory - File tmpfile = File.createTempFile(prefix, null, null); - tmpfile.deleteOnExit(); - FileOutputStream out = new FileOutputStream(tmpfile); - getSession().getRepositorySession().exportSysView(item.getPath(), out, false, true); - out.close(); - in = new FileInputStream(tmpfile); - - properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, new Long(tmpfile.length()))); - properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTTYPE, "text/xml")); - - } catch (IOException e) { - log.error("Error while property initialization: "+e.getMessage()); - } catch (RepositoryException e) { - log.error("Error while property initialization: "+e.getMessage()); - } - - Node n = (Node)item; - // overwrite the default modificationtime if possible - try { - if (n.hasProperty(PROP_LASTMODIFIED)) { - setModificationTime(n.getProperty(PROP_LASTMODIFIED).getLong()); - } - } catch (RepositoryException e) { - log.warn("Error while accessing jcr:lastModified property"); - } - // overwrite the default creation date if possible - try { - if (n.hasProperty(PROP_CREATED)) { - long creationTime = n.getProperty(PROP_CREATED).getValue().getLong(); - properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, - DavConstants.creationDateFormat.format(new Date(creationTime)))); - } - } catch (RepositoryException e) { - log.warn("Error while accessing jcr:created property"); - } - - // add node-specific resource properties - try { - properties.add(new NodeTypeProperty(JCR_PRIMARYNODETYPE, n.getPrimaryNodeType(), false)); - properties.add(new NodeTypeProperty(JCR_MIXINNODETYPES, n.getMixinNodeTypes(), false)); - properties.add(new DefaultDavProperty(JCR_INDEX, new Integer(n.getIndex()))); - addHrefProperty(JCR_REFERENCES, n.getReferences(), false); - } catch (RepositoryException e) { - log.error("Failed to retrieve primary nodetype property: " + e.getMessage()); - } - try { - Item primaryItem = n.getPrimaryItem(); - addHrefProperty(JCR_PRIMARYITEM, new Item[] {primaryItem}, false); - } catch (ItemNotFoundException e) { - log.info("No primary item present on this node '" + getResourcePath() + "'"); - } catch (RepositoryException e) { - log.error("Error while retrieving primary item: " + e.getMessage()); - } - - // property defined by RFC 3648: this resource always has custom ordering! - if (isOrderable()) { - properties.add(new OrderingType(OrderingConstants.ORDERING_TYPE_CUSTOM)); - } - } - } - - /** - * Add a {@link org.apache.jackrabbit.webdav.property.HrefProperty} with the - * specified property name and values. Each item present in the specified - * values array is referenced in the resulting property. - * - * @param name - * @param values - * @param isProtected - */ - protected void addHrefProperty(DavPropertyName name, Item[] values, boolean isProtected) { - if (values == null) { - return; - } - try { - String[] pHref = new String[values.length]; - for (int i = 0; i < values.length; i++) { - pHref[i] = this.getLocatorFromResourcePath(values[i].getPath()).getHref(true); - } - properties.add(new HrefProperty(name, pHref, isProtected)); - } catch (RepositoryException e) { - e.getMessage(); - } - } - - /** - * Add a new {@link HrefProperty href property} to the property set, where - * all items present in the specifed iterator are referenced in the - * resulting property. - * - * @param name - * @param itemIterator - * @param isProtected - * @see #addHrefProperty(DavPropertyName, Item[], boolean) - */ - protected void addHrefProperty(DavPropertyName name, Iterator itemIterator, - boolean isProtected) { - ArrayList l = new ArrayList(); - while (itemIterator.hasNext()) { - l.add(itemIterator.next()); - } - addHrefProperty(name, (Item[]) l.toArray(new Item[l.size()]), isProtected); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DefaultItemResource.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DefaultItemResource.java deleted file mode 100644 index 7a615991ad6..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/DefaultItemResource.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.DavResource; -import org.apache.jackrabbit.webdav.DavResourceIterator; -import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; -import org.apache.jackrabbit.webdav.spi.property.ValuesProperty; -import org.apache.jackrabbit.webdav.spi.property.LengthsProperty; -import org.apache.jackrabbit.webdav.lock.*; -import org.apache.jackrabbit.core.util.ValueHelper; - -import javax.jcr.*; -import java.io.*; -import java.util.*; - -/** - * DefaultItemResource represents JCR property item. - * - * @see Property - */ -public class DefaultItemResource extends AbstractItemResource { - - private static Logger log = Logger.getLogger(DefaultItemResource.class); - - /** - * Create a new DefaultItemResource. - * - * @param locator - * @param session - */ - public DefaultItemResource(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - super(locator, session, factory); - } - - //----------------------------------------------< DavResource interface >--- - /** - * Returns false. - * - * @return false - * @see DavResource#isCollection() - */ - public boolean isCollection() { - return false; - } - - /** - * In case an underlaying repository {@link Property property} exists the following - * logic is applyed to obtain the stream:

    - *
  • Property is not multivalue: Return the {@link javax.jcr.Value#getStream() - * stream representation} of the property value.
  • - *
  • Property is multivalue: Return stream that provides the system view of - * that item.
  • - *
- * - * @return - * @see DavResource#getStream() - */ - public InputStream getStream() { - InputStream in = null; - if (exists()) { - try { - // NOTE: stream cannot be obtained for multivalue properties - if (!isMultiple()) { - in = ((Property)item).getStream(); - } - } catch (ValueFormatException e) { - // should not occur - log.error("Cannot obtain stream from resource: " + e.getMessage()); - } catch (RepositoryException e) { - log.error("Cannot obtain stream from resource: " + e.getMessage()); - } - } - return in; - } - - /** - * Sets the given property. Note, that {@link #JCR_VALUE} and {@link #JCR_VALUES} - * are the only resource properties that are allowed to be modified. Any other - * property is read-only and will throw an exception ('Conflict'). - * - * @param property - * @throws DavException - * @see DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) - * @todo undo incomplete modifications... - */ - public void setProperty(DavProperty property) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - try { - Property prop = (Property) item; - int type = prop.getType(); - if (property.getName().equals(JCR_VALUE)) { - Value val = ValueHelper.convert(String.valueOf(property.getValue()), type); - prop.setValue(val); - } else if (property.getName().equals(JCR_VALUES)) { - prop.setValue(new ValuesProperty(property).getValues(prop.getType())); - } else { - throw new DavException(DavServletResponse.SC_CONFLICT); - } - complete(); - - } catch (IllegalArgumentException e) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, e.getMessage()); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Removing properties is not allowed, for a single-value JCR-property without - * a value does not exist. For multivalue properties an empty {@link Value values array} - * may be specified with by setting the {@link #JCR_VALUES 'values' webdav property}. - * - * @param propertyName - * @throws DavException - * @see org.apache.jackrabbit.webdav.DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) - */ - public void removeProperty(DavPropertyName propertyName) throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - throw new DavException(DavServletResponse.SC_FORBIDDEN); - } - - /** - * Method is not allowed. - * - * @see org.apache.jackrabbit.webdav.DavResource#addMember(org.apache.jackrabbit.webdav.DavResource, InputStream) - */ - public void addMember(DavResource resource, InputStream in) throws DavException { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED, "Cannot add members to a non-collection resource"); - } - - /** - * Method is not allowed. - * - * @see DavResource#addMember(DavResource) - */ - public void addMember(DavResource resource) throws DavException { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED, "Cannot add members to a non-collection resource"); - } - - /** - * Always returns an empty iterator for a non-collection resource might - * not have internal members. - * - * @return an empty iterator - * @see DavResource#getMembers() - */ - public DavResourceIterator getMembers() { - log.warn("A non-collection resource never has internal members."); - return new DavResourceIteratorImpl(new ArrayList(0)); - } - - /** - * Method is not allowed. - * - * @see DavResource#removeMember(DavResource) - */ - public void removeMember(DavResource member) throws DavException { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED, "Cannot remove members from a non-collection resource"); - } - - /** - * {@link javax.jcr.Property JCR properties} are locked if their - * parent node is locked; thus this method will always return the - * {@link ActiveLock lock} object from the collection this resource is - * internal member of. - * - * @param type - * @param scope - * @return lock present on this resource or null if this resource - * has no lock. - * @see DavResource#getLock(Type, Scope) - */ - public ActiveLock getLock(Type type, Scope scope) { - if (Type.WRITE.equals(type)) { - return getCollection().getLock(type, scope); - } else { - return super.getLock(type, scope); - } - } - - //-------------------------------------------------------------------------- - /** - * Add resource specific properties. - */ - protected void initProperties() { - super.initProperties(); - if (exists()) { - try { - Property prop = (Property)item; - int type = prop.getType(); - - // set the content type - String contentType; - if (!isMultiple()) { - contentType = (type == PropertyType.BINARY) ? "application/octet-stream" : "text/plain"; - properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTTYPE, contentType)); - } // else: no contenttype for multivalue properties - - // add jcr-specific resource properties - properties.add(new DefaultDavProperty(JCR_TYPE, PropertyType.nameFromValue(type))); - if (isMultiple()) { - properties.add(new ValuesProperty(prop.getValues())); - properties.add(new LengthsProperty(prop.getLengths())); - } else { - properties.add(new DefaultDavProperty(JCR_VALUE, prop.getString())); - properties.add(new DefaultDavProperty(JCR_LENGTH, String.valueOf(prop.getLength()))); - } - } catch (RepositoryException e) { - log.error("Failed to retrieve resource properties: "+e.getMessage()); - } - } - } - - /** - * Returns true if the JCR Property represented by this resource is a multi - * value property. Note: if this resource does not exist or if the definition - * could not be retrieved false is returned. - * - * @return true if the underlaying resource is a multi value property. - */ - private boolean isMultiple() { - try { - if (exists() && ((Property)item).getDefinition().isMultiple()) { - return true; - } - } catch (RepositoryException e) { - log.error("Error while retrieving property definition: " + e.getMessage()); - } - return false; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/ItemResourceConstants.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/ItemResourceConstants.java deleted file mode 100644 index ab61ba606a6..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/ItemResourceConstants.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.jdom.Namespace; -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.DavResource; -import org.apache.jackrabbit.webdav.version.DeltaVResource; -import org.apache.jackrabbit.webdav.lock.Scope; -import org.apache.jackrabbit.webdav.search.SearchResource; -import org.apache.jackrabbit.webdav.observation.ObservationResource; - -/** - * ItemResourceConstants provides constants for any resources - * representing repository items. - */ -public interface ItemResourceConstants { - - /** - * Complience classes common to all item resources. - */ - public static final String COMPLIANCE_CLASS = DavResource.COMPLIANCE_CLASS + ", " +ObservationResource.COMPLIANCE_CLASS + ", " + DeltaVResource.COMPLIANCE_CLASS; - - /** - * Methods common to all item resources. - */ - public static final String METHODS = DavResource.METHODS + ", " + ObservationResource.METHODS + ", " + SearchResource.METHODS + ", " +DeltaVResource.METHODS; - - /** - * The resource path of the root-item-resource. - */ - public static final String ROOT_ITEM_PATH = "/"; - - /** - * The version storage item resource path. - */ - public static final String VERSIONSTORAGE_PATH = "/jcr:system/jcr:versionStorage"; - - /** - * Constant for the mix:versionable node type name. - */ - public static final String MIX_VERSIONABLE = "mix:versionable"; - - /** - * Constant for the mix:lockable node type name. - */ - public static final String MIX_LOCKABLE = "mix:lockable"; - - /** - * The namespace for all jcr specific extensions. - */ - public static final Namespace NAMESPACE = Namespace.getNamespace("jcr", "http://www.day.com/jcr/webdav/1.0"); - - // xml element names - public static final String XML_PRIMARYNODETYPE = "primarynodetype"; - public static final String XML_VALUE = "value"; - public static final String XML_LENGTH = "length"; - public static final String XML_EXCLUSIVE_SESSION_SCOPED = "exclusive-session-scoped"; - - // xml elements used to reflect the workspaces ns-registry - // TODO: to be review... - public static final String XML_NAMESPACE = "namespace"; - public static final String XML_NSPREFIX = "nsPrefix"; - public static final String XML_NSURI = "nsURI"; - - /** - * Extension to the WebDAV 'exclusive' lock, that allows to distinguish - * the session-scoped and open-scoped locks on a JCR node. - * - * @see javax.jcr.Node#lock(boolean, boolean) - */ - public static final Scope EXCLUSIVE_SESSION = Scope.create(XML_EXCLUSIVE_SESSION_SCOPED, NAMESPACE); - - /** - * The 'removeexisting' element is not defined by RFC 3253. If it is present - * in the UPDATE request body, uuid conflicts should be solved by removing - * the existing nodes. - * - * @see javax.jcr.Node#restore(javax.jcr.version.Version, boolean) - * @see javax.jcr.Workspace#restore(javax.jcr.version.Version[], boolean) - * @see org.apache.jackrabbit.webdav.version.UpdateInfo - */ - public static final String XML_REMOVEEXISTING = "removeexisting"; - - /** - * The 'relpath' element is not defined by RFC 3253. If it is present - * in the UPDATE request body, the server is forced to used the text contained - * as 'relPath' argument for the {@link javax.jcr.Node#restore(javax.jcr.version.Version, String, boolean) - * Node.restore} call. - * - * @see javax.jcr.Node#restore(javax.jcr.version.Version, String, boolean) - * @see org.apache.jackrabbit.webdav.version.UpdateInfo - */ - public static final String XML_RELPATH = "relpath"; - - // general property names - public static final DavPropertyName JCR_NAME = DavPropertyName.create("name", NAMESPACE); - public static final DavPropertyName JCR_PATH = DavPropertyName.create("path", NAMESPACE); - public static final DavPropertyName JCR_DEPTH = DavPropertyName.create("depth", NAMESPACE); - public static final DavPropertyName JCR_ISNEW = DavPropertyName.create("isnew", NAMESPACE); - public static final DavPropertyName JCR_ISMODIFIED = DavPropertyName.create("ismodified", NAMESPACE); - - // property names used for resources representing jcr-nodes - public static final DavPropertyName JCR_PRIMARYNODETYPE = DavPropertyName.create(XML_PRIMARYNODETYPE, NAMESPACE); - public static final DavPropertyName JCR_MIXINNODETYPES = DavPropertyName.create("mixinnodetypes", NAMESPACE); - public static final DavPropertyName JCR_INDEX = DavPropertyName.create("index", NAMESPACE); - public static final DavPropertyName JCR_REFERENCES = DavPropertyName.create("references", NAMESPACE); - public static final DavPropertyName JCR_PRIMARYITEM = DavPropertyName.create("primaryitem", NAMESPACE); - - // property names used for resources representing jcr-properties - public static final DavPropertyName JCR_TYPE = DavPropertyName.create("type", NAMESPACE); - public static final DavPropertyName JCR_VALUE = DavPropertyName.create("value", NAMESPACE); - public static final DavPropertyName JCR_VALUES = DavPropertyName.create("values", NAMESPACE); - public static final DavPropertyName JCR_LENGTH = DavPropertyName.create("length", NAMESPACE); - public static final DavPropertyName JCR_LENGTHS = DavPropertyName.create("lengths", NAMESPACE); - - // property names used for resource representing a workspace - public static final DavPropertyName JCR_NAMESPACES = DavPropertyName.create("namespaces", NAMESPACE); - - /** - * Property name for the jcr:created property present on Version items. - */ - public static final DavPropertyName CREATED = DavPropertyName.create("created", ItemResourceConstants.NAMESPACE); - - // JCR property names - public static final String PROP_LASTMODIFIED = "jcr:lastModified"; - public static final String PROP_CREATED = "jcr:created"; - public static final String PROP_BASEVERSION = "jcr:baseVersion"; - public static final String PROP_PREDECESSORS = "jcr:predecessors"; - public static final String PROP_MERGEFAILED = "jcr:mergeFailed"; -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/JcrDavException.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/JcrDavException.java deleted file mode 100644 index fbaa9e82073..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/JcrDavException.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.DavServletResponse; -import org.jdom.Element; - -import javax.jcr.*; -import javax.jcr.query.InvalidQueryException; -import javax.jcr.lock.LockException; -import javax.jcr.version.VersionException; -import javax.jcr.nodetype.*; - -/** - * JcrDavException extends the {@link DavException} in order to - * wrap various repository exceptions. - */ -public class JcrDavException extends DavException { - - private static Logger log = Logger.getLogger(JcrDavException.class); - - private Class exceptionClass; - - public JcrDavException(Exception e, int errorCode) { - super(errorCode, e.getMessage()); - exceptionClass = e.getClass(); - } - - public JcrDavException(AccessDeniedException e) { - this(e, DavServletResponse.SC_FORBIDDEN); - } - - public JcrDavException(ConstraintViolationException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(InvalidItemStateException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(InvalidSerializedDataException e) { - this(e, DavServletResponse.SC_BAD_REQUEST); - } - - public JcrDavException(InvalidQueryException e) { - this(e, DavServletResponse.SC_BAD_REQUEST); - } - - public JcrDavException(ItemExistsException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(ItemNotFoundException e) { - this(e, DavServletResponse.SC_FORBIDDEN); - } - - public JcrDavException(LockException e) { - this(e, DavServletResponse.SC_LOCKED); - } - - public JcrDavException(MergeException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(NamespaceException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(NoSuchNodeTypeException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(NoSuchWorkspaceException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(PathNotFoundException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(ReferentialIntegrityException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(RepositoryException e) { - this(e, DavServletResponse.SC_FORBIDDEN); - } - - public JcrDavException(UnsupportedRepositoryOperationException e) { - this(e, DavServletResponse.SC_NOT_IMPLEMENTED); - } - - public JcrDavException(ValueFormatException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - public JcrDavException(VersionException e) { - this(e, DavServletResponse.SC_CONFLICT); - } - - /** - * Returns a DAV:error Xml element containing the exceptions class and the - * message as child elements. - * - * @return Xml representation of this exception. - */ - public Element getError() { - Element error = super.getError(); - Element excep = new Element("exception", ItemResourceConstants.NAMESPACE); - excep.addContent(new Element("class", ItemResourceConstants.NAMESPACE).setText(exceptionClass.getName())); - excep.addContent(new Element("message", ItemResourceConstants.NAMESPACE).setText(getMessage())); - error.addContent(excep); - return error; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/RootCollection.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/RootCollection.java deleted file mode 100644 index 4ee1ad8ec4b..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/RootCollection.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.spi.version.report.RegisteredNamespacesReport; -import org.apache.jackrabbit.webdav.spi.version.report.NodeTypesReport; -import org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty; -import org.apache.jackrabbit.webdav.version.report.ReportType; - -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import java.util.*; -import java.io.InputStream; - -/** - * RootCollection represent the WebDAV root resource that does not - * represent any repository item. A call to getMembers() returns a - * DavResourceIterator containing only RootItemCollection - * resources, thus revealing the names of the accessable workspaces. - */ -public class RootCollection extends AbstractResource implements DavResource { - - private static Logger log = Logger.getLogger(RootCollection.class); - - /** - * Create a new RootCollection. - * - * @param locator - * @param session - */ - protected RootCollection(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - super(locator, session, factory); - setModificationTime(new Date().getTime()); - } - - /** - * Returns a string listing the complieance classes for this resource as it - * is required for the DAV response header. - * - * @return string listing the compliance classes. - * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() - */ - public String getComplianceClass() { - return DavResource.COMPLIANCE_CLASS; - } - - /** - * Returns a string listing the METHODS for this resource as it - * is required for the "Allow" response header. - * - * @return string listing the METHODS allowed - * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() - */ - public String getSupportedMethods() { - StringBuffer sb = new StringBuffer(DavResource.METHODS); - return sb.toString(); - } - - /** - * Returns true - * - * @return true - * @see org.apache.jackrabbit.webdav.DavResource#exists() - */ - public boolean exists() { - return true; - } - - /** - * Returns true - * - * @return true - * @see org.apache.jackrabbit.webdav.DavResource#isCollection() - */ - public boolean isCollection() { - return true; - } - - /** - * Returns an empty string. - * - * @return empty string - * @see org.apache.jackrabbit.webdav.DavResource#getDisplayName() - */ - public String getDisplayName() { - return ""; - } - - /** - * Always returns null - * - * @return null for the root resource is not internal member - * of any resource. - * @see org.apache.jackrabbit.webdav.DavResource#getCollection() - */ - public DavResource getCollection() { - return null; - } - - /** - * Throws exception: 403 Forbidden. - * @see DavResource#addMember(DavResource, InputStream) - */ - public void addMember(DavResource resource, InputStream in) throws DavException { - throw new DavException(DavServletResponse.SC_FORBIDDEN); - } - - /** - * Throws exception: 403 Forbidden. - * @see DavResource#addMember(org.apache.jackrabbit.webdav.DavResource) - */ - public void addMember(DavResource resource) throws DavException { - throw new DavException(DavServletResponse.SC_FORBIDDEN); - } - - /** - * Returns an iterator over the member resources, which are all - * RootItemCollection resources, revealing - * the names of all available workspaces. - * - * @return members of this collection - * @see org.apache.jackrabbit.webdav.DavResource#getMembers() - */ - public DavResourceIterator getMembers() { - List memberList = new ArrayList(); - try { - String[] wsNames = getSession().getRepositorySession().getWorkspace().getAccessibleWorkspaceNames(); - for (int i = 0; i < wsNames.length; i++) { - DavResourceLocator childLoc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), "/"+wsNames[i], ItemResourceConstants.ROOT_ITEM_PATH); - memberList.add(createResourceFromLocator(childLoc)); - } - } catch (RepositoryException e) { - log.error(e.getMessage()); - } catch (DavException e) { - // should never occur - log.error(e.getMessage()); - } - return new DavResourceIteratorImpl(memberList); - } - - /** - * Throws exception: 403 Forbidden. - * @see DavResource#removeMember(org.apache.jackrabbit.webdav.DavResource) - */ - public void removeMember(DavResource member) throws DavException { - throw new DavException(DavServletResponse.SC_FORBIDDEN); - } - - //-------------------------------------------------------------------------- - /** - * @see AbstractResource#initLockSupport() - */ - protected void initLockSupport() { - // no locking supported - } - - /** - * @see AbstractResource#initSupportedReports() - */ - protected void initSupportedReports() { - if (exists()) { - supportedReports = new SupportedReportSetProperty(new ReportType[] { - ReportType.EXPAND_PROPERTY, - NodeTypesReport.NODETYPES_REPORT, - RegisteredNamespacesReport.REGISTERED_NAMESPACES_REPORT - }); - } - } - - /** - * Since the root resource does not represent a repository item and therefore - * is not member of a workspace resource, the workspace href is calculated - * from the workspace name retrieved from the underlaying repository session. - * - * @return workspace href build from workspace name. - * @see AbstractResource#getWorkspaceHref() - */ - protected String getWorkspaceHref() { - Session session = this.getRepositorySession(); - if (session != null) { - String workspaceName = session.getWorkspace().getName(); - DavResourceLocator loc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), "/"+workspaceName, ItemResourceConstants.ROOT_ITEM_PATH); - return loc.getHref(true); - } - return null; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/RootItemCollection.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/RootItemCollection.java deleted file mode 100644 index 30219571ae1..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/RootItemCollection.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.property.DefaultDavProperty; -import org.apache.jackrabbit.webdav.property.DavProperty; -import org.jdom.Element; - -import javax.jcr.NamespaceRegistry; -import javax.jcr.RepositoryException; - -/** - * RootItemCollection represents the root node of the underlaying - * repository. However, the display name the name of the workspace is returned - * the root node is located. - * - * @todo currently the jcr root node is the same for all workspace resources... this is wrong... - */ -public class RootItemCollection extends VersionControlledItemCollection { - - private static Logger log = Logger.getLogger(RootItemCollection.class); - - /** - * Create a new RootItemCollection. - * - * @param locator - * @param session - */ - protected RootItemCollection(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - super(locator, session, factory); - } - - //----------------------------------------------< DavResource interface >--- - /** - * Returns the name of the workspace the underlaying root item forms part of. - * - * @return The workspace name - * @see org.apache.jackrabbit.webdav.DavResource#getDisplayName() - * @see javax.jcr.Workspace#getName() - */ - public String getDisplayName() { - return getLocator().getWorkspaceName(); - } - - /** - * Retrieve the collection that has all root item / workspace collections - * as internal members. - * - * @see org.apache.jackrabbit.webdav.DavResource#getCollection() - */ - public DavResource getCollection() { - DavResource collection = null; - // create location with 'null' values for workspace-path and resource-path - DavResourceLocator parentLoc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), null, null); - try { - collection = createResourceFromLocator(parentLoc); - } catch (DavException e) { - log.error("Unexpected error while retrieving collection: " + e.getMessage()); - } - return collection; - } - - public void setProperty(DavProperty property) throws DavException { - if (ItemResourceConstants.JCR_NAMESPACES.equals(property.getName())) { - // todo: register and unregister namespaces - } else { - super.setProperty(property); - } - } - - //-------------------------------------------------------------------------- - protected void initProperties() { - super.initProperties(); - try { - // init workspace specific properties - NamespaceRegistry nsReg = getRepositorySession().getWorkspace().getNamespaceRegistry(); - String[] prefixes = nsReg.getPrefixes(); - Element[] nsElems = new Element[prefixes.length]; - for (int i = 0; i < prefixes.length; i++) { - Element elem = new Element(XML_NAMESPACE, NAMESPACE); - elem.addContent(new Element(XML_NSPREFIX).setText(prefixes[i])); - elem.addContent(new Element(XML_NSURI)).setText(nsReg.getURI(prefixes[i])); - nsElems[i] = elem; - } - properties.add(new DefaultDavProperty(ItemResourceConstants.JCR_NAMESPACES, nsElems, false)); - } catch (RepositoryException e) { - log.error("Failed to access NamespaceRegistry from the session/workspace: " + e.getMessage()); - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/VersionControlledItemCollection.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/VersionControlledItemCollection.java deleted file mode 100644 index 34f4ba11fa7..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/VersionControlledItemCollection.java +++ /dev/null @@ -1,582 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.version.*; -import org.apache.jackrabbit.webdav.version.report.*; - -import javax.jcr.*; -import javax.jcr.observation.*; -import javax.jcr.observation.EventListener; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; -import java.util.List; - -/** - * VersionControlledItemCollection represents a JCR node item and - * covers all functionality related to versioning of {@link Node}s. - * - * @see Node - */ -public class VersionControlledItemCollection extends DefaultItemCollection - implements VersionControlledResource { - - private static Logger log = Logger.getLogger(VersionControlledItemCollection.class); - - /** - * Create a new VersionControlledItemCollection. - * - * @param locator - * @param session - */ - public VersionControlledItemCollection(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - super(locator, session, factory); - if (exists() && !(item instanceof Node)) { - throw new IllegalArgumentException("A collection resource can not be constructed from a Property item."); - } - } - - //----------------------------------------------< DavResource interface >--- - /** - * Return a comma separated string listing the supported method names. - * - * @return the supported method names. - * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() - */ - public String getSupportedMethods() { - StringBuffer sb = new StringBuffer(super.getSupportedMethods()); - // Versioning support - sb.append(", ").append(VersionableResource.METHODS); - if (this.isVersionControlled()) { - try { - if (((Node)item).isCheckedOut()) { - sb.append(", ").append(VersionControlledResource.methods_checkedOut); - } else { - sb.append(", ").append(VersionControlledResource.methods_checkedIn); - } - } catch (RepositoryException e) { - // should not occur. - log.error(e.getMessage()); - } - } - return sb.toString(); - } - - //--------------------------------< VersionControlledResource interface >--- - /** - * Adds version control to this resource. If the resource is already under - * version control, this method has no effect. - * - * @throws org.apache.jackrabbit.webdav.DavException if this resource does not - * exist yet or if an error occurs while making the underlaying node versionable. - * @see org.apache.jackrabbit.webdav.version.VersionableResource#addVersionControl() - */ - public void addVersionControl() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!isVersionControlled()) { - try { - ((Node)item).addMixin(MIX_VERSIONABLE); - item.save(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } // else: is already version controlled -> ignore - } - - /** - * Calls {@link javax.jcr.Node#checkin()} on the underlaying repository node. - * - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkin() - */ - public String checkin() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!isVersionControlled()) { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - try { - Version v = ((Node) item).checkin(); - DavResourceLocator loc = getLocator(); - String versionHref = loc.getFactory().createResourceLocator(loc.getPrefix(), loc.getWorkspacePath(), v.getPath()).getHref(true); - return versionHref; - } catch (RepositoryException e) { - // UnsupportedRepositoryException should not occur - throw new JcrDavException(e); - } - } - - /** - * Calls {@link javax.jcr.Node#checkout()} on the underlaying repository node. - * - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkout() - */ - public void checkout() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!isVersionControlled()) { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - try { - ((Node) item).checkout(); - } catch (RepositoryException e) { - // UnsupportedRepositoryException should not occur - throw new JcrDavException(e); - } - } - - /** - * Not implemented. Always throws a DavException with error code - * {@link org.apache.jackrabbit.webdav.DavServletResponse#SC_NOT_IMPLEMENTED}. - * - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#uncheckout() - */ - public void uncheckout() throws DavException { - throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED); - } - - /** - * Perform an update on this resource. Depending on the format of the updateInfo - * this is translated to one of the following methods defined by the JCR API: - *
    - *
  • {@link Node#restore(javax.jcr.version.Version, boolean)}
  • - *
  • {@link Node#restore(javax.jcr.version.Version, String, boolean)}
  • - *
  • {@link Node#restoreByLabel(String, boolean)}
  • - *
  • {@link Workspace#restore(javax.jcr.version.Version[], boolean)}
  • - *
  • {@link Node#update(String)}
  • - *
- *

- * Limitation: note that the MultiStatus returned by this method - * will not list any nodes that have been removed due to an Uuid conflict. - * - * @param updateInfo - * @return - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#update(org.apache.jackrabbit.webdav.version.UpdateInfo) - * @todo with jcr the node must not be versionable in order to perform Node.update. - */ - public MultiStatus update(UpdateInfo updateInfo) throws DavException { - if (updateInfo == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid update request body required."); - } - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - MultiStatus ms = new MultiStatus(); - try { - Node node = (Node)item; - boolean removeExisting = updateInfo.getUpdateElement().getChild(XML_REMOVEEXISTING, NAMESPACE) != null; - - // register eventListener in order to be able to report the modified resources. - EventListener el = new EListener(updateInfo.getPropertyNameSet(), ms); - registerEventListener(el, node.getPath()); - - // perform the update/restore according to the update info - if (updateInfo.getVersionHref() != null) { - VersionHistory vh = node.getVersionHistory(); - String[] hrefs = updateInfo.getVersionHref(); - Version[] versions = new Version[hrefs.length]; - for (int i = 0; i < hrefs.length; i++) { - versions[i] = vh.getVersion(getResourceName(hrefs[i], true)); - } - if (versions.length == 1) { - String relPath = updateInfo.getUpdateElement().getChildText(XML_RELPATH, NAMESPACE); - if (relPath == null) { - node.restore(versions[0], removeExisting); - } else { - node.restore(versions[0], relPath, removeExisting); - } - } else { - getRepositorySession().getWorkspace().restore(versions, removeExisting); - } - } else if (updateInfo.getLabelName() != null) { - String[] labels = updateInfo.getLabelName(); - if (labels.length == 1) { - node.restoreByLabel(labels[0], removeExisting); - } else { - Version[] vs = new Version[labels.length]; - VersionHistory vh = node.getVersionHistory(); - for (int i = 0; i < labels.length; i++) { - vs[i] = vh.getVersionByLabel(labels[i]); - } - getRepositorySession().getWorkspace().restore(vs, removeExisting); - } - } else if (updateInfo.getWorkspaceHref() != null) { - String workspaceName = getResourceName(updateInfo.getWorkspaceHref(), true); - node.update(workspaceName); - } else { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid update request body."); - } - - // unregister the event listener again - unregisterEventListener(el); - - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - return ms; - } - - /** - * Merge the repository node represented by this resource according to the - * information present in the given {@link MergeInfo} object. - * - * @param mergeInfo - * @return MultiStatus reccording all repository items affected - * by this merge call. - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#merge(org.apache.jackrabbit.webdav.version.MergeInfo) - * @see Node#merge(String, boolean) - * @todo with jcr the node must not be versionable in order to perform Node.merge - */ - public MultiStatus merge(MergeInfo mergeInfo) throws DavException { - if (mergeInfo == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST); - } - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - MultiStatus ms = new MultiStatus(); - try { - Node node = (Node)item; - - // register eventListener in order to be able to report the modifications. - EventListener el = new EListener(mergeInfo.getPropertyNameSet(), ms); - registerEventListener(el, node.getPath()); - - String workspaceName = getResourceName(mergeInfo.getSourceHref(), true); - node.merge(workspaceName, !mergeInfo.isNoAutoMerge()); - - // unregister the event listener again - unregisterEventListener(el); - - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - - return ms; - } - - /** - * Resolve the merge conflicts according to the value of the {@link #AUTO_MERGE_SET DAV:auto-merge-set} - * property present in the specified DavPropertySets. - * - * @param setProperties - * @param removePropertyNames - * @throws org.apache.jackrabbit.webdav.DavException - * @see VersionControlledResource#resolveMergeConflict(DavPropertySet, DavPropertyNameSet) - * @see Node#doneMerge(Version) - * @see Node#cancelMerge(Version) - */ - public void resolveMergeConflict(DavPropertySet setProperties, - DavPropertyNameSet removePropertyNames) throws DavException { - - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!isVersionControlled()) { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - - try { - Node n = (Node)item; - if (removePropertyNames.contains(AUTO_MERGE_SET)) { - // retrieve the current jcr:mergeFailed property values - if (!((Node)item).hasProperty(PROP_MERGEFAILED)) { - throw new DavException(DavServletResponse.SC_CONFLICT, "Attempt to resolve non-existing merge conflicts."); - } - Value[] mergeFailed = ((Node)item).getProperty(PROP_MERGEFAILED).getValues(); - - // resolve all remaining merge conflicts with 'cancel' - for (int i = 0; i < mergeFailed.length; i++) { - n.cancelMerge((Version)getRepositorySession().getNodeByUUID(mergeFailed[i].getString())); - } - // adjust removeProperty set - removePropertyNames.remove(AUTO_MERGE_SET); - - } else if (setProperties.contains(AUTO_MERGE_SET) && setProperties.contains(PREDECESSOR_SET)){ - // retrieve the current jcr:mergeFailed property values - if (!((Node)item).hasProperty(PROP_MERGEFAILED)) { - throw new DavException(DavServletResponse.SC_CONFLICT, "Attempt to resolve non-existing merge conflicts."); - } - Value[] mergeFailed = ((Node)item).getProperty(PROP_MERGEFAILED).getValues(); - - - // check which mergeFailed entries have been removed from the - // auto-merge-set (cancelMerge) and have been moved over to the - // predecessor set (doneMerge) - List mergeset = new HrefProperty(setProperties.get(AUTO_MERGE_SET)).getHrefs(); - List predecSet = new HrefProperty(setProperties.get(PREDECESSOR_SET)).getHrefs(); - - Session session = getRepositorySession(); - for (int i = 0; i < mergeFailed.length; i++) { - // build version-href from each entry in the jcr:mergeFailed property - Version version = (Version) session.getNodeByUUID(mergeFailed[i].getString()); - String href = this.getLocatorFromItem(version).getHref(true); - - // Test if that version has been removed from the merge-set. - // thus indicating that the merge-conflict needs to be resolved. - if (!mergeset.contains(href)) { - // Test if the 'href' has been moved over to the - // predecessor-set (thus 'doneMerge' is appropriate) or - // if it is not present in the predecessor set and the - // the conflict is resolved by 'cancelMerge'. - if (predecSet.contains(href)) { - n.doneMerge(version); - } else { - n.cancelMerge(version); - } - } - } - // adjust setProperty set - setProperties.remove(AUTO_MERGE_SET); - setProperties.remove(PREDECESSOR_SET); - } else { - // setPropertySet and removePropertySet do not ask for resolving merge conflicts */ - log.debug("setProperties and removeProperties sets do not request for merge conflict resolution."); - } - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Modify the labels present with the versions of this resource. - * - * @param labelInfo - * @throws DavException - * @see VersionHistory#addVersionLabel(String, String, boolean) - * @see VersionHistory#removeVersionLabel(String) - */ - public void label(LabelInfo labelInfo) throws DavException { - if (labelInfo == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid label request body required."); - } - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - try { - if (!isVersionControlled() || ((Node)item).isCheckedOut()) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "A LABEL request may only be applied to a version-controlled, checked-in resource."); - } - DavResource[] resArr = this.getReferenceResources(CHECKED_IN); - if (resArr.length == 1 && resArr[0] instanceof VersionResource) { - ((VersionResource)resArr[0]).label(labelInfo); - } else { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "DAV:checked-in property on '" + getHref() + "' did not point to a single VersionResource."); - } - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Returns the {@link VersionHistory} associated with the repository node. - * If the node is not versionable an exception is thrown. - * - * @return the {@link VersionHistoryResource} associated with this resource. - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#getVersionHistory() - * @see javax.jcr.Node#getVersionHistory() - */ - public VersionHistoryResource getVersionHistory() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - try { - VersionHistory vh = ((Node)item).getVersionHistory(); - DavResourceLocator loc = getLocatorFromItem(vh); - return (VersionHistoryResource) createResourceFromLocator(loc); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - //-------------------------------------------------------------------------- - /** - * Define the set of reports supported by this resource. - * - * @see SupportedReportSetProperty - */ - protected void initSupportedReports() { - super.initSupportedReports(); - if (exists()) { - supportedReports.addReportType(ReportType.LOCATE_BY_HISTORY); - if (this.isVersionControlled()) { - supportedReports.addReportType(ReportType.VERSION_TREE); - } - } - } - - /** - * Fill the property set for this resource. - */ - protected void initProperties() { - super.initProperties(); - if (exists()) { - Node n = (Node)item; - // properties defined by RFC 3253 for version-controlled resources - if (isVersionControlled()) { - // workspace property already set in AbstractResource.initProperties() - try { - // DAV:version-history (computed) - String vhHref = getLocatorFromResourcePath(n.getVersionHistory().getPath()).getHref(true); - properties.add(new HrefProperty(VERSION_HISTORY, vhHref, true)); - - // DAV:auto-version property: there is no auto version, explicit CHECKOUT is required. - properties.add(new DefaultDavProperty(AUTO_VERSION, null, false)); - - String baseVHref = getLocatorFromResourcePath(n.getBaseVersion().getPath()).getHref(true); - if (n.isCheckedOut()) { - // DAV:checked-out property (protected) - properties.add(new HrefProperty(CHECKED_OUT, baseVHref, true)); - - // DAV:predecessors property - if (n.hasProperty(PROP_PREDECESSORS)) { - Value[] predec = n.getProperty(PROP_PREDECESSORS).getValues(); - addHrefProperty(PREDECESSOR_SET, predec, false); - } - // DAV:auto-merge-set property. NOTE: the DAV:merge-set - // never occurs, because merging without bestEffort flag - // being set results in an exception on failure. - if (n.hasProperty(PROP_MERGEFAILED)) { - ReferenceValue[] mergeFailed = (ReferenceValue[]) n.getProperty(PROP_MERGEFAILED).getValues(); - addHrefProperty(AUTO_MERGE_SET, mergeFailed, false); - } - // todo: checkout-fork, checkin-fork - } else { - // DAV:checked-in property (protected) - properties.add(new HrefProperty(CHECKED_IN, baseVHref, true)); - } - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - } - } - } - - /** - * Add a {@link org.apache.jackrabbit.webdav.property.HrefProperty} with the specified property name and values. - * - * @param name - * @param values Array of {@link ReferenceValue}s. - * @param isProtected - * @throws javax.jcr.ValueFormatException - * @throws IllegalStateException - * @throws javax.jcr.RepositoryException - */ - private void addHrefProperty(DavPropertyName name, Value[] values, - boolean isProtected) - throws ValueFormatException, IllegalStateException, RepositoryException { - Node[] nodes = new Node[values.length]; - for (int i = 0; i < values.length; i++) { - nodes[i] = getRepositorySession().getNodeByUUID(values[i].getString()); - } - addHrefProperty(name, nodes, isProtected); - } - - /** - * @return true, if this resource represents an existing repository node - * that has the mixin nodetype 'mix:versionable' set. - */ - private boolean isVersionControlled() { - boolean vc = false; - if (exists()) { - try { - vc = ((Node) item).isNodeType("mix:versionable"); - } catch (RepositoryException e) { - log.warn(e.getMessage()); - } - } - return vc; - } - - /** - * Register the specified event listener with the observation manager present - * the repository session. - * - * @param listener - * @param nodePath - * @throws javax.jcr.RepositoryException - */ - private void registerEventListener(EventListener listener, String nodePath) throws RepositoryException { - getRepositorySession().getWorkspace().getObservationManager().addEventListener(listener, EListener.ALL_EVENTS, nodePath, true, null, null, false); - } - - /** - * Unregister the specified event listener with the observation manager present - * the repository session. - * - * @param listener - * @throws javax.jcr.RepositoryException - */ - private void unregisterEventListener(EventListener listener) throws RepositoryException { - getRepositorySession().getWorkspace().getObservationManager().removeEventListener(listener); - } - - //------------------------------------------------------< inner classes >--- - /** - * Simple EventListener that creates a new {@link org.apache.jackrabbit.webdav.MultiStatusResponse} object - * for each event and adds it to the specified {@link org.apache.jackrabbit.webdav.MultiStatus}. - */ - private class EListener implements EventListener { - - private static final int ALL_EVENTS = Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED; - - private final DavPropertyNameSet propNameSet; - private MultiStatus ms; - - private EListener(DavPropertyNameSet propNameSet, MultiStatus ms) { - this.propNameSet = propNameSet; - this.ms = ms; - } - - /** - * @see EventListener#onEvent(javax.jcr.observation.EventIterator) - */ - public void onEvent(EventIterator events) { - while (events.hasNext()) { - try { - Event e = events.nextEvent(); - String itemPath = e.getPath(); - DavResourceLocator loc = getLocatorFromResourcePath(itemPath); - DavResource res = createResourceFromLocator(loc); - ms.addResponse(new MultiStatusResponse(res, propNameSet)); - - } catch (DavException e) { - // should not occur - log.error("Error while building MultiStatusResponse from Event: " + e.getMessage()); - } catch (RepositoryException e) { - // should not occur - log.error("Error while building MultiStatusResponse from Event: " + e.getMessage()); - } - } - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/lock/JcrActiveLock.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/lock/JcrActiveLock.java deleted file mode 100644 index 78e3fbb662d..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/lock/JcrActiveLock.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.lock; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavConstants; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.apache.jackrabbit.webdav.lock.*; - -import javax.jcr.lock.Lock; -import javax.jcr.Node; -import javax.jcr.RepositoryException; - -/** - * JcrActiveLock wraps a {@link Lock JCR lock} object. - */ -public class JcrActiveLock extends AbstractActiveLock implements ActiveLock, DavConstants { - - private static Logger log = Logger.getLogger(JcrActiveLock.class); - - private final Lock lock; - private final boolean sessionScoped; - - /** - * Create a new ActiveLock object with type '{@link Type#WRITE write}' - * and scope '{@link Scope#EXCLUSIVE exclusive}'. - * - * @param lock - */ - public JcrActiveLock(Lock lock) { - this (lock, false); - } - - /** - * Create a new ActiveLock object with type '{@link Type#WRITE write}' - * and scope '{@link Scope#EXCLUSIVE exclusive}'. - * - * @param lock - */ - public JcrActiveLock(Lock lock, boolean sessionScoped) { - if (lock == null) { - throw new IllegalArgumentException("Can not create a ActiveLock with a 'null' argument."); - } - this.lock = lock; - this.sessionScoped = sessionScoped; - } - - /** - * Return true if the given lock token equals the token holding that lock. - * - * @param lockToken - * @return true if the given lock token equals this locks token. - * @see org.apache.jackrabbit.webdav.lock.ActiveLock#isLockedByToken(String) - */ - public boolean isLockedByToken(String lockToken) { - if (lockToken != null && lockToken.equals(getToken())) { - return true; - } - return false; - } - - /** - * @see ActiveLock#isExpired() - */ - public boolean isExpired() { - return lock.isLive(); - } - - /** - * Return the lock token if the {@link javax.jcr.Session} that optained the lock - * is the lock token holder, null otherwise.
- * NOTE: currently the token generated by the underlaying JCR repository - * is not checked for compliance with RFC 2518 ("OpaqueLockToken-URI = "opaquelocktoken:" - * UUID [Extension] ; The UUID production is the string representation of a - * UUID, as defined in [ISO-11578]. Note that white space (LWS) is not allowed - * between elements of this production."). - * - * @see ActiveLock#getToken() - */ - public String getToken() { - return lock.getLockToken(); - } - - /** - * @see ActiveLock#getOwner() - */ - public String getOwner() { - return lock.getLockOwner(); - } - - /** - * @see ActiveLock#setOwner(String) - */ - public void setOwner(String owner) { - throw new UnsupportedOperationException("setOwner is not implemented"); - } - - /** - * Always returns {@link DavConstants#UNDEFINED_TIMEOUT} for the timeout - * cannot be retrieved from the JCR lock. - * - * @see ActiveLock#getTimeout() - * @see DavConstants#UNDEFINED_TIMEOUT - */ - public long getTimeout() { - return UNDEFINED_TIMEOUT; - } - - /** - * @see ActiveLock#setTimeout(long) - */ - public void setTimeout(long timeout) { - throw new UnsupportedOperationException("setTimeout is not implemented"); - } - - /** - * @see ActiveLock#isDeep() - */ - public boolean isDeep() { - boolean isDeep = true; - Node n = lock.getNode(); - try { - // find out about deepness. if node does not hold the lock its deep anyway - if (n.holdsLock() && n.hasProperty("jcr:lockIsDeep")) { - isDeep = n.getProperty("jcr:lockIsDeep").getBoolean(); - } - } catch (RepositoryException e) { - // ignore and keep default depth settings - } - return isDeep; - } - - /** - * @see ActiveLock#setIsDeep(boolean) - */ - public void setIsDeep(boolean isDeep) { - throw new UnsupportedOperationException("setIsDeep is not implemented"); - } - - /** - * Always returns {@link Type#WRITE}. - * - * @return {@link Type#WRITE} - * @see ActiveLock#getType() - */ - public Type getType() { - return Type.WRITE; - } - - /** - * @return The scope of this lock, which may either by an {@link Scope#EXCLUSIVE exclusive} - * or {@link ItemResourceConstants#EXCLUSIVE_SESSION exlusive session scoped} - * lock. - * @see ActiveLock#getScope() - */ - public Scope getScope() { - return (sessionScoped) ? ItemResourceConstants.EXCLUSIVE_SESSION : Scope.EXCLUSIVE; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/lock/SessionScopedLockEntry.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/lock/SessionScopedLockEntry.java deleted file mode 100644 index 03898e3d252..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/lock/SessionScopedLockEntry.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.lock; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.lock.LockEntry; -import org.apache.jackrabbit.webdav.lock.Scope; -import org.apache.jackrabbit.webdav.lock.Type; -import org.apache.jackrabbit.webdav.lock.AbstractLockEntry; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; - -/** - * SessionScopedLockEntry represents the 'session-scoped' write - * lock as defined by JCR. - */ -public class SessionScopedLockEntry extends AbstractLockEntry { - - private static Logger log = Logger.getLogger(SessionScopedLockEntry.class); - - /** - * @return always returns {@link Type#WRITE write}. - * @see LockEntry#getType() - */ - public Type getType() { - return Type.WRITE; - } - - /** - * @return returns {@link ItemResourceConstants#EXCLUSIVE_SESSION}. - * @see LockEntry#getScope() - */ - public Scope getScope() { - return ItemResourceConstants.EXCLUSIVE_SESSION; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/ItemDefImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/ItemDefImpl.java deleted file mode 100644 index f6962c5421d..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/ItemDefImpl.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2004 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.nodetype; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import javax.jcr.nodetype.ItemDef; -import javax.jcr.nodetype.NodeType; -import javax.jcr.version.OnParentVersionAction; - -/** - * ItemDefImpl... - */ -abstract public class ItemDefImpl implements ItemDef, NodeTypeConstants { - - private static Logger log = Logger.getLogger(ItemDefImpl.class); - - private final String name; - private NodeType declaringNodeType; - private final boolean isAutoCreated; - private final boolean isMandatory; - private final boolean isProtected; - private final int onParentVersion; - - ItemDefImpl(ItemDef definition) { - if (definition == null) { - throw new IllegalArgumentException("PropDef argument can not be null"); - } - - name = definition.getName(); - declaringNodeType = definition.getDeclaringNodeType(); - isAutoCreated = definition.isAutoCreate(); - isMandatory = definition.isMandatory(); - isProtected = definition.isProtected(); - onParentVersion = definition.getOnParentVersion(); - } - - public NodeType getDeclaringNodeType() { - return declaringNodeType; - } - - public String getName() { - return name; - } - - public boolean isAutoCreate() { - return isAutoCreated; - } - - public boolean isMandatory() { - return isMandatory; - } - - public int getOnParentVersion() { - return onParentVersion; - } - - public boolean isProtected() { - return isProtected; - } - - //-------------------------------------< implementation specific method >--- - /** - * Returns the Xml representation of a {@link ItemDef} object. - * - * @return Xml representation of the specified {@link ItemDef def}. - */ - public Element toXml() { - Element elem = new Element(getElementName()); - elem.setAttribute(ATTR_NAME, getName()); - elem.setAttribute(ATTR_AUTOCREATE, Boolean.toString(isAutoCreate())); - elem.setAttribute(ATTR_MANDATORY, Boolean.toString(isMandatory())); - elem.setAttribute(ATTR_ONPARENTVERSION, OnParentVersionAction.nameFromValue(getOnParentVersion())); - elem.setAttribute(ATTR_PROTECTED, Boolean.toString(isProtected())); - return elem; - } - - public abstract String getElementName(); -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeDefImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeDefImpl.java deleted file mode 100644 index 84f8585f380..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeDefImpl.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2004 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.nodetype; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import javax.jcr.nodetype.NodeDef; -import javax.jcr.nodetype.NodeType; - -/** - * NodeDefImpl... - */ -public final class NodeDefImpl extends ItemDefImpl implements NodeDef { - - private static Logger log = Logger.getLogger(NodeDefImpl.class); - - private final NodeType[] requiredPrimaryTypes; - private final NodeType defaultPrimaryType; - private final boolean allowSameNameSibs; - - private NodeDefImpl(NodeDef definition) { - super(definition); - - requiredPrimaryTypes = definition.getRequiredPrimaryTypes(); - defaultPrimaryType = definition.getDefaultPrimaryType(); - allowSameNameSibs = definition.allowSameNameSibs(); - } - - public static NodeDefImpl create(NodeDef definition) { - if (definition instanceof NodeDefImpl) { - return (NodeDefImpl) definition; - } else { - return new NodeDefImpl(definition); - } - } - - //--------------------------------------------------< NodeDef interface >--- - public NodeType[] getRequiredPrimaryTypes() { - return requiredPrimaryTypes; - } - - public NodeType getDefaultPrimaryType() { - return defaultPrimaryType; - } - - public boolean allowSameNameSibs() { - return allowSameNameSibs; - } - - //-------------------------------------< implementation specific method >--- - public Element toXml() { - Element elem = super.toXml(); - - elem.setAttribute(ATTR_SAMENAMESIBS, Boolean.toString(allowSameNameSibs())); - - // defaultPrimaryType can be 'null' - NodeType defaultPrimaryType = getDefaultPrimaryType(); - if (defaultPrimaryType != null) { - elem.setAttribute(ATTR_DEFAULTPRIMARYTYPE, defaultPrimaryType.getName()); - } - // reqPrimaryTypes: minimal set is nt:base. - NodeType[] nts = getRequiredPrimaryTypes(); - Element reqPrimaryTypes = new Element(XML_REQUIREDPRIMARYTYPES); - for (int i = 0; i < nts.length; i++) { - reqPrimaryTypes.addContent(new Element(XML_REQUIREDPRIMARYTYPE).setText(nts[i].getName())); - } - elem.addContent(reqPrimaryTypes); - - return elem; - } - - public String getElementName() { - return XML_CHILDNODEDEF; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeConstants.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeConstants.java deleted file mode 100644 index 2450e30db2e..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeConstants.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.nodetype; - -import org.jdom.Namespace; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; - -/** - * NodeTypeConstants used to represent nodetype definitions in - * Xml. - * - * @see javax.jcr.nodetype.NodeType - * @todo intercaps only for consistency with jackrabbit... webdav rfcs never use intercaps. - */ -public interface NodeTypeConstants { - - public static final Namespace NAMESPACE = ItemResourceConstants.NAMESPACE; - - public static final String XML_NODETYPENAME = "nodetypename"; - - public static final String XML_REPORT_ALLNODETYPES = "allnodetypes"; - public static final String XML_REPORT_MIXINNODETYPES = "mixinnodetypes"; - public static final String XML_REPORT_PRIMARYNODETYPES = "primarynodetypes"; - - // report response - /** Name of the node type definition root element. */ - public static final String XML_NODETYPES = "nodeTypes"; - - /** Name of the node type definition element. */ - public static final String XML_NODETYPE = "nodeType"; - - /** Name of the isMixin attribute. */ - public static final String ATTR_ISMIXIN = "isMixin"; - - /** Name of the hasOrderableChildNodes attribute. */ - public static final String ATTR_HASORDERABLECHILDNODES = "hasOrderableChildNodes"; - - /** Name of the primary item name attribute. */ - public static final String ATTR_PRIMARYITEMNAME = "primaryItemName"; - - /** Name of the supertypes element. */ - public static final String XML_SUPERTYPES = "supertypes"; - - /** Name of the supertype element. */ - public static final String XML_SUPERTYPE = "supertype"; - - /** Name of the child node definition element. */ - public static final String XML_CHILDNODEDEF = "childNodeDef"; - - /** Name of the property definition element. */ - public static final String XML_PROPERTYDEF = "propertyDef"; - - /** Name of the name attribute. */ - public static final String ATTR_NAME = "name"; - - /** Name of the autoCreate attribute. */ - public static final String ATTR_AUTOCREATE = "autoCreate"; - - /** Name of the mandatory attribute. */ - public static final String ATTR_MANDATORY = "mandatory"; - - /** Name of the onParentVersion attribute. */ - public static final String ATTR_ONPARENTVERSION = "onParentVersion"; - - /** Name of the protected attribute. */ - public static final String ATTR_PROTECTED = "protected"; - - /** Name of the required type attribute. */ - public static final String ATTR_REQUIREDTYPE = "requiredType"; - - /** Name of the value constraints element. */ - public static final String XML_VALUECONSTRAINTS = "valueConstraints"; - - /** Name of the value constraint element. */ - public static final String XML_VALUECONSTRAINT = "valueConstraint"; - - /** Name of the default values element. */ - public static final String XML_DEFAULTVALUES = "defaultValues"; - - /** Name of the default value element. */ - public static final String XML_DEFAULTVALUE = "defaultValue"; - - /** Name of the multiple attribute. */ - public static final String ATTR_MULTIPLE = "multiple"; - - /** Name of the required primary types element. */ - public static final String XML_REQUIREDPRIMARYTYPES = "requiredPrimaryTypes"; - - /** Name of the required primary type element. */ - public static final String XML_REQUIREDPRIMARYTYPE = "requiredPrimaryType"; - - /** Name of the default primary type attribute. */ - public static final String ATTR_DEFAULTPRIMARYTYPE = "defaultPrimaryType"; - - /** Name of the sameNameSibs attribute. */ - public static final String ATTR_SAMENAMESIBS = "sameNameSibs"; -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeElement.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeElement.java deleted file mode 100644 index a3a77ab5faa..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeElement.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.nodetype; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import javax.jcr.nodetype.NodeType; - -/** - * NodeTypeElement... - */ -public class NodeTypeElement extends Element implements NodeTypeConstants { - - private static Logger log = Logger.getLogger(NodeTypeElement.class); - - public NodeTypeElement(NodeType nt) { - super(XML_NODETYPE, NodeTypeConstants.NAMESPACE); - addNodeTypeNameElement(nt.getName()); - } - - public NodeTypeElement(Element ntElement) { - super(XML_NODETYPE, NodeTypeConstants.NAMESPACE); - if (!XML_NODETYPE.equals(ntElement.getName())) { - throw new IllegalArgumentException("jcr:nodetype element expected."); - } - addNodeTypeNameElement(ntElement.getChildText(XML_NODETYPENAME, NodeTypeConstants.NAMESPACE)); - } - - private void addNodeTypeNameElement(String nodeTypeName) { - if (nodeTypeName != null) { - addContent(new Element(XML_NODETYPENAME, NodeTypeConstants.NAMESPACE).setText(nodeTypeName)); - } - } - - public String getNodeTypeName() { - return getChildText(XML_NODETYPENAME, NodeTypeConstants.NAMESPACE); - } - - public static NodeTypeElement[] create(NodeType[] nts) { - NodeTypeElement[] elems = new NodeTypeElement[nts.length]; - for (int i = 0; i < nts.length; i++) { - elems[i] = new NodeTypeElement(nts[i]); - } - return elems; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeProperty.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeProperty.java deleted file mode 100644 index bbb4edbb5bd..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/NodeTypeProperty.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.nodetype; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.property.DavProperty; -import org.apache.jackrabbit.webdav.property.AbstractDavProperty; -import org.jdom.Element; - -import javax.jcr.nodetype.NodeType; -import java.util.*; - -/** - * NodeTypeProperty... - */ -public class NodeTypeProperty extends AbstractDavProperty { - - private static Logger log = Logger.getLogger(NodeTypeProperty.class); - - private final NodeTypeElement[] value; - - public NodeTypeProperty(DavPropertyName name, NodeType nodeType, boolean isProtected) { - super(name, isProtected); - value = new NodeTypeElement[] {new NodeTypeElement(nodeType)}; - } - - public NodeTypeProperty(DavPropertyName name, NodeType[] nodeTypes, boolean isProtected) { - super(name, isProtected); - value = NodeTypeElement.create(nodeTypes); - } - - /** - * Create a new NodeTypeProperty from the specified general - * DavProperty object. - * - * @param property - * @throws IllegalArgumentException if the content of the specified property - * contains elements other than {@link NodeTypeConstants#XML_NODETYPE}. - */ - public NodeTypeProperty(DavProperty property) { - super(property.getName(), property.isProtected()); - - if (property.getValue() instanceof List) { - List ntElemList = new ArrayList(); - Iterator it = ((List) property.getValue()).iterator(); - while (it.hasNext()) { - Object content = it.next(); - if (content instanceof Element) { - ntElemList.add(new NodeTypeElement((Element)content)); - } - } - value = (NodeTypeElement[]) ntElemList.toArray(new NodeTypeElement[ntElemList.size()]); - } else if (property.getValue() instanceof Element) { - NodeTypeElement ntElem = new NodeTypeElement((Element)property.getValue()); - value = new NodeTypeElement [] {ntElem}; - } else { - value = new NodeTypeElement[0]; - } - } - - /** - * Return a set of nodetype names present in this property. - * - * @return set of nodetype names - */ - public Set getNodeTypeNames() { - HashSet names = new HashSet(); - Object val = getValue(); - if (val instanceof NodeTypeElement[]) { - NodeTypeElement[] elems = (NodeTypeElement[])val; - for (int i = 0; i < elems.length; i++) { - String ntName = elems[i].getNodeTypeName(); - if (ntName != null) { - names.add(ntName); - } - } - } - return names; - } - - /** - * Returns the value of this property which is an array of {@link NodeTypeElement} - * objects. - * - * @return an array of {@link NodeTypeElement}s - */ - public Object getValue() { - return value; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/PropertyDefImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/PropertyDefImpl.java deleted file mode 100644 index c96bce48171..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/nodetype/PropertyDefImpl.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2004 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.nodetype; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import javax.jcr.nodetype.PropertyDef; -import javax.jcr.Value; -import javax.jcr.PropertyType; -import javax.jcr.RepositoryException; - -/** - * PropertyDefImpl... - */ -public final class PropertyDefImpl extends ItemDefImpl implements PropertyDef { - - private static Logger log = Logger.getLogger(PropertyDefImpl.class); - - private final int type; - private final String[] valueConstraints; - private final Value[] defaultValues; - private final boolean isMultiple; - - private PropertyDefImpl(PropertyDef definition) { - super(definition); - - type = definition.getRequiredType(); - valueConstraints = definition.getValueConstraints(); - defaultValues = definition.getDefaultValues(); - isMultiple = definition.isMultiple(); - } - - public static PropertyDefImpl create(PropertyDef definition) { - if (definition instanceof PropertyDefImpl) { - return (PropertyDefImpl)definition; - } else { - return new PropertyDefImpl(definition); - } - } - - public int getRequiredType() { - return type; - } - - public String[] getValueConstraints() { - return valueConstraints; - } - - public Value[] getDefaultValues() { - return defaultValues; - } - - public boolean isMultiple() { - return isMultiple; - } - - //-------------------------------------< implementation specific method >--- - public Element toXml() { - Element elem = super.toXml(); - - elem.setAttribute(ATTR_MULTIPLE, Boolean.toString(isMultiple())); - elem.setAttribute(ATTR_REQUIREDTYPE, PropertyType.nameFromValue(getRequiredType())); - - // default values may be 'null' - Value[] values = getDefaultValues(); - if (values != null) { - Element dvElement = new Element(XML_DEFAULTVALUES); - for (int i = 0; i < values.length; i++) { - try { - Element valElem = new Element(XML_DEFAULTVALUE).setText(values[i].getString()); - dvElement.addContent(valElem); - } catch (RepositoryException e) { - // should not occur - log.error(e.getMessage()); - } - } - elem.addContent(dvElement); - } - // value constraints array is never null. - Element constrElem = new Element(XML_VALUECONSTRAINTS); - String[] constraints = getValueConstraints(); - for (int i = 0; i < constraints.length; i++) { - constrElem.addContent(new Element(XML_VALUECONSTRAINT).setText(constraints[i])); - } - elem.addContent(constrElem); - - return elem; - } - - public String getElementName() { - return XML_PROPERTYDEF; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/observation/SubscriptionImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/observation/SubscriptionImpl.java deleted file mode 100644 index 7ceb4f474b1..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/observation/SubscriptionImpl.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.observation; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavResourceLocator; -import org.apache.jackrabbit.webdav.DavConstants; -import org.apache.jackrabbit.webdav.observation.*; -import org.apache.jackrabbit.webdav.util.XmlUtil; -import org.apache.jackrabbit.core.util.uuid.UUID; -import org.jdom.Element; - -import javax.jcr.observation.*; -import javax.jcr.observation.EventListener; -import javax.jcr.RepositoryException; -import java.util.*; - -/** - * The Subscription class encapsulates a single subscription with - * the following responsibilities:
    - *
  • Providing access to the subscription info,
  • - *
  • Recording events this subscription is interested in,
  • - *
  • Providing access to the events.
  • - *
- */ -public class SubscriptionImpl implements Subscription, ObservationConstants, EventListener { - - private static Logger log = Logger.getLogger(SubscriptionImpl.class); - private static final long DEFAULT_TIMEOUT = 300000; // 5 minutes - - private SubscriptionInfo info; - - private final DavResourceLocator locator; - private final String subscriptionId = UUID.randomUUID().toString(); - private final List eventBundles = new ArrayList(); - - /** - * Create a new Subscription with the given {@link SubscriptionInfo} - * and {@link org.apache.jackrabbit.webdav.observation.ObservationResource resource}. - * - * @param info - * @param resource - */ - public SubscriptionImpl(SubscriptionInfo info, ObservationResource resource) { - setInfo(info); - locator = resource.getLocator(); - } - - /** - * Returns the id of this subscription. - * - * @return subscriptionId - */ - public String getSubscriptionId() { - return subscriptionId; - } - - /** - * Return the Xml representation of this Subscription as required - * for the {@link org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery} webdav property that in included - * in the response body of a sucessful SUBSCRIBE request or as part of a - * PROPFIND response. - * - * @return Xml representation - */ - public Element toXml() { - Element subscr = new Element(XML_SUBSCRIPTION, NAMESPACE); - Element[] elems = info.toXml(); - for (int i = 0; i < elems.length; i++) { - subscr.addContent(elems[i]); - } - - Element id = new Element(XML_SUBSCRIPTIONID); - id.addContent(XmlUtil.hrefToXml(subscriptionId)); - subscr.addContent(id); - return subscr; - } - - /** - * Modify the {@link SubscriptionInfo} for this subscription. - * - * @param info - */ - void setInfo(SubscriptionInfo info) { - this.info = info; - // validate the timeout and adjust value, if it is invalid or missing - long timeout = info.getTimeOut(); - if (timeout == DavConstants.UNDEFINED_TIMEOUT) { - info.setTimeOut(DEFAULT_TIMEOUT); - } - } - - /** - * @return JCR compliant integer representation of the event types defined - * for this {@link SubscriptionInfo}. - */ - int getEventTypes() { - Iterator xmlTypes = info.getEventTypes().iterator(); - int eventTypes = 0; - while (xmlTypes.hasNext()) { - eventTypes |= nametoTypeConstant(((Element)xmlTypes.next()).getName()); - } - return eventTypes; - } - - /** - * @return a String array with size > 0 or null - */ - String[] getUuidFilters() { - return info.getFilters(XML_UUID); - } - - /** - * @return a String array with size > 0 or null - */ - String[] getNodetypeNameFilters() { - return info.getFilters(XML_NODETYPE_NAME); - } - - /** - * - * @return true if a {@link ObservationConstants#XML_NOLOCAL} element - * is present in the {@link SubscriptionInfo}. - */ - boolean isNoLocal() { - return info.isNoLocal(); - } - - /** - * @return true if this subscription is intended to be deep. - */ - boolean isDeep() { - return info.isDeep(); - } - - /** - * @return the locator of the {@link ObservationResource resource} this - * Subscription was requested for. - */ - DavResourceLocator getLocator() { - return locator; - } - - /** - * Returns true if this Subscription matches the given - * resource. - * - * @param resource - * @return true if this Subscription matches the given - * resource. - */ - boolean isSubscribedToResource(ObservationResource resource) { - return locator.equals(resource.getLocator()); - } - - /** - * Returns true if this Subscription is expired and therefore - * stopped recording events. - * - * @return true if this Subscription is expired - */ - boolean isExpired() { - return System.currentTimeMillis() > info.getTimeOut() + System.currentTimeMillis(); - } - - /** - * Returns a {@link org.apache.jackrabbit.webdav.observation.EventDiscovery} object listing all events that were - * recorded since the last call to this method and clears the list of event - * bundles. - * - * @return object listing all events that were recorded. - * @see #onEvent(EventIterator) - */ - synchronized EventDiscovery discoverEvents() { - EventDiscovery ed = new EventDiscovery(); - Iterator it = eventBundles.iterator(); - while (it.hasNext()) { - EventBundle eb = (EventBundle) it.next(); - ed.addEventBundle(eb.toXml()); - } - // clear list - eventBundles.clear(); - return ed; - } - - //--------------------------------------------< EventListener interface >--- - /** - * Records the events passed as a new event bundle in order to make them - * available with the next {@link #discoverEvents()} request. - * - * @param events to be recorded. - * @see EventListener#onEvent(EventIterator) - * @see #discoverEvents() - */ - public synchronized void onEvent(EventIterator events) { - // TODO: correct not to accept events after expiration? without unsubscribing? - if (!isExpired()) { - eventBundles.add(new EventBundle(events)); - } - } - - //-------------------------------------------------------------------------- - /** - * Static utility method in order to convert the types defined by - * {@link javax.jcr.observation.Event} into their Xml representation. - * - * @param jcrEventType - * @return Xml representation of the event type. - */ - static Element typeConstantToXml(int jcrEventType) { - String eventName; - switch (jcrEventType) { - case Event.NODE_ADDED: - eventName = EVENT_NODEADDED; - break; - case Event.NODE_REMOVED: - eventName = EVENT_NODEREMOVED; - break; - case Event.PROPERTY_ADDED: - eventName = EVENT_PROPERTYADDED; - break; - case Event.PROPERTY_CHANGED: - eventName = EVENT_PROPERTYCHANGED; - break; - default: - //Event.PROPERTY_REMOVED: - eventName = EVENT_PROPERTYREMOVED; - break; - } - return new Element(eventName, NAMESPACE); - } - - /** - * Static utility method to convert an event type name as present in the - * Xml request body into the corresponding constant defined by - * {@link javax.jcr.observation.Event}. - * - * @param eventTypeName - * @return event type as defined by {@link javax.jcr.observation.Event}. - * @throws IllegalArgumentException if the given element cannot be translated - * to any of the events defined by {@link javax.jcr.observation.Event}. - */ - static int nametoTypeConstant(String eventTypeName) { - int eType; - if (EVENT_NODEADDED.equals(eventTypeName)) { - eType = Event.NODE_ADDED; - } else if (EVENT_NODEREMOVED.equals(eventTypeName)) { - eType = Event.NODE_REMOVED; - } else if (EVENT_PROPERTYADDED.equals(eventTypeName)) { - eType = Event.PROPERTY_ADDED; - } else if (EVENT_PROPERTYCHANGED.equals(eventTypeName)) { - eType = Event.PROPERTY_CHANGED; - } else if (EVENT_PROPERTYREMOVED.equals(eventTypeName)) { - eType = Event.PROPERTY_REMOVED; - } else { - throw new IllegalArgumentException("Invalid event type: "+eventTypeName); - } - return eType; - } - - /** - * Inner class EventBundle encapsulats an event bundle as - * recorded {@link SubscriptionImpl#onEvent(EventIterator) on event} and - * provides the possibility to retrieve the Xml representation of the - * bundle and the events included in order to respond to a POLL request. - * - * @see SubscriptionImpl#discoverEvents() - */ - private class EventBundle { - - private final EventIterator events; - - private EventBundle(EventIterator events) { - this.events = events; - } - - private Element toXml() { - Element bundle = new Element(XML_EVENTBUNDLE, NAMESPACE); - while (events.hasNext()) { - Event event = events.nextEvent(); - - Element eventElem = new Element(XML_EVENT, NAMESPACE); - // href - String eHref = ""; - try { - boolean isCollection = (event.getType() == Event.NODE_ADDED || event.getType() == Event.NODE_REMOVED); - eHref = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), event.getPath()).getHref(isCollection); - } catch (RepositoryException e) { - // should not occur.... - log.error(e.getMessage()); - } - eventElem.addContent(XmlUtil.hrefToXml(eHref)); - // eventtype - Element eType = new Element(XML_EVENTTYPE, NAMESPACE).addContent(typeConstantToXml(event.getType())); - eventElem.addContent(eType); - // user id - Element eUserId = new Element(XML_EVENTUSERID, NAMESPACE).setText(event.getUserId()); - eventElem.addContent(eUserId); - } - return bundle; - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/observation/SubscriptionManagerImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/observation/SubscriptionManagerImpl.java deleted file mode 100644 index 5048c9894fc..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/observation/SubscriptionManagerImpl.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.observation; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.observation.*; - -import javax.jcr.RepositoryException; -import javax.jcr.observation.ObservationManager; -import java.util.*; - -/** - * SubscriptionManager collects all subscriptions requested, handles - * the subscription timeout and provides METHODS to discover subscriptions - * present on a given resource as well as events for an specific subscription. - * - * @todo make sure all expired subscriptions are removed! - */ -public class SubscriptionManagerImpl implements SubscriptionManager { - - private static Logger log = Logger.getLogger(SubscriptionManager.class); - - /** - * Map containing all {@link org.apache.jackrabbit.webdav.observation.Subscription subscriptions}. - */ - private final SubscriptionMap subscriptions = new SubscriptionMap(); - - /** - * Retrieve the {@link org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery} object for the given - * resource. Note, that the discovery object will be empty if there are - * no subscriptions present. - * - * @param resource - * @todo is it correct to return subscriptions made by another session? - */ - public SubscriptionDiscovery getSubscriptionDiscovery(ObservationResource resource) { - Subscription[] subsForResource = subscriptions.getByPath(resource.getLocator()); - return new SubscriptionDiscovery(subsForResource); - } - - /** - * Create a new Subscription or update an existing Subscription - * and add it as eventlistener to the {@link javax.jcr.observation.ObservationManager}. - * - * @param info - * @param subscriptionId - * @param resource - * @return Subscription that has been added to the {@link javax.jcr.observation.ObservationManager} - * @throws DavException if the subscription fails - */ - public Subscription subscribe(SubscriptionInfo info, String subscriptionId, - ObservationResource resource) - throws DavException { - - SubscriptionImpl subscription; - DavSession session = resource.getSession(); - if (subscriptionId == null) { - // new subscription - subscription = new SubscriptionImpl(info, resource); - registerSubscription(subscription, session); - - // ajust references to this subscription - subscriptions.put(subscription.getSubscriptionId(), subscription); - session.addReference(subscription.getSubscriptionId()); - } else { - // refresh/modify existing one - subscription = validate(subscriptionId, resource); - subscription.setInfo(info); - registerSubscription(subscription, session); - } - return subscription; - } - - /** - * Register the event listener defined by the given subscription to the - * repository's observation manager. - * - * @param subscription - * @param session - * @throws DavException - */ - private void registerSubscription(SubscriptionImpl subscription, DavSession session) - throws DavException { - try { - ObservationManager oMgr = session.getRepositorySession().getWorkspace().getObservationManager(); - oMgr.addEventListener(subscription, subscription.getEventTypes(), - subscription.getLocator().getResourcePath(), subscription.isDeep(), - subscription.getUuidFilters(), - subscription.getNodetypeNameFilters(), - subscription.isNoLocal()); - } catch (RepositoryException e) { - log.error("Unable to register eventlistener: "+e.getMessage()); - throw new JcrDavException(e); - } - } - - /** - * Unsubscribe the Subscription with the given id and remove it - * from the {@link javax.jcr.observation.ObservationManager} as well as - * from the internal map. - * - * @param subscriptionId - * @param resource - * @throws DavException - */ - public void unsubscribe(String subscriptionId, ObservationResource resource) - throws DavException { - - SubscriptionImpl subs = validate(subscriptionId, resource); - unregisterSubscription(subs, resource.getSession()); - } - - /** - * Remove the event listener defined by the specified subscription from - * the repository's observation manager. - * - * @param subscription - * @param session - * @throws DavException - */ - private void unregisterSubscription(SubscriptionImpl subscription, - DavSession session) throws DavException { - try { - session.getRepositorySession().getWorkspace().getObservationManager().removeEventListener(subscription); - String sId = subscription.getSubscriptionId(); - - // clean up any references - subscriptions.remove(sId); - session.removeReference(sId); - - } catch (RepositoryException e) { - log.error("Unable to remove eventlistener: "+e.getMessage()); - throw new JcrDavException(e); - } - } - - /** - * Retrieve all event bundles accumulated since for the subscription specified - * by the given id. - * - * @param subscriptionId - * @param resource - * @return object encapsulating the events. - */ - public EventDiscovery poll(String subscriptionId, ObservationResource resource) - throws DavException { - - SubscriptionImpl subs = validate(subscriptionId, resource); - return subs.discoverEvents(); - } - - /** - * Validate the given subscription id. The validation will fail under the following - * conditions:
    - *
  • The subscription with the given id does not exist,
  • - *
  • DavResource path does not match the subscription id,
  • - *
  • The subscription with the given id is already expired.
  • - *
- * - * @param subscriptionId - * @param resource - * @return Subscription with the given id. - * @throws DavException if an error occured while retrieving the Subscription - */ - private SubscriptionImpl validate(String subscriptionId, ObservationResource resource) - throws DavException { - - SubscriptionImpl subs; - if (subscriptions.contains(subscriptionId)) { - subs = (SubscriptionImpl) subscriptions.get(subscriptionId); - if (!subs.isSubscribedToResource(resource)) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to operate on subscription with invalid resource path."); - } - if (subs.isExpired()) { - unregisterSubscription(subs, resource.getSession()); - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to operate on expired subscription."); - } - return subs; - } else { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to modify or to poll for non-existing subscription."); - } - } - - /** - * Private inner class SubscriptionMap that allows for quick - * access by resource path as well as by subscription id. - */ - private class SubscriptionMap { - - private HashMap subscriptions = new HashMap(); - private HashMap paths = new HashMap(); - - private boolean contains(String subscriptionId) { - return subscriptions.containsKey(subscriptionId); - } - - private Subscription get(String subscriptionId) { - return (Subscription) subscriptions.get(subscriptionId); - } - - private void put(String subscriptionId, SubscriptionImpl subscription) { - subscriptions.put(subscriptionId, subscription); - DavResourceLocator key = subscription.getLocator(); - Set idSet; - if (paths.containsKey(key)) { - idSet = (Set) paths.get(key); - } else { - idSet = new HashSet(); - paths.put(key, idSet); - } - if (!idSet.contains(subscriptionId)) { - idSet.add(subscriptionId); - } - } - - private void remove(String subscriptionId) { - SubscriptionImpl sub = (SubscriptionImpl) subscriptions.remove(subscriptionId); - ((Set)paths.get(sub.getLocator())).remove(subscriptionId); - } - - private Subscription[] getByPath(DavResourceLocator locator) { - Set idSet = (Set) paths.get(locator); - if (idSet != null && !idSet.isEmpty()) { - Iterator idIterator = idSet.iterator(); - Subscription[] subsForResource = new Subscription[idSet.size()]; - int i = 0; - while (idIterator.hasNext()) { - subsForResource[i] = (Subscription) subscriptions.get(idIterator.next()); - } - return subsForResource; - } else { - return new Subscription[0]; - } - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/package.html b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/package.html deleted file mode 100644 index 17e97edbf40..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/package.html +++ /dev/null @@ -1,3 +0,0 @@ - -Contains JCR specific implementations. - \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/property/LengthsProperty.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/property/LengthsProperty.java deleted file mode 100644 index 7984f7cecb9..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/property/LengthsProperty.java +++ /dev/null @@ -1,56 +0,0 @@ -/* -* Copyright 2004 The Apache Software Foundation. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -package org.apache.jackrabbit.webdav.spi.property; - -import org.apache.jackrabbit.webdav.property.AbstractDavProperty; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.jdom.Element; - -/** - * LengthsProperty extends {@link org.apache.jackrabbit.webdav.property.DavProperty} providing - * utilities to handle the multiple lengths of the property item represented - * by this resource. - */ -public class LengthsProperty extends AbstractDavProperty implements ItemResourceConstants { - - private final Element[] value; - - /** - * Create a new LengthsProperty from the given long array. - * - * @param lengths as retrieved from the JCR property - */ - public LengthsProperty(long[] lengths) { - super(JCR_LENGTHS, false); - - Element[] elems = new Element[lengths.length]; - for (int i = 0; i < lengths.length; i++) { - elems[i] = new Element(XML_LENGTH, ItemResourceConstants.NAMESPACE); - elems[i].addContent(String.valueOf(lengths[i])); - } - this.value = elems; - } - - /** - * Returns an array of {@link Element}s representing the value of this - * property. - * - * @return an array of {@link Element}s - */ - public Object getValue() { - return value; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/property/ValuesProperty.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/property/ValuesProperty.java deleted file mode 100644 index c580b89bd1f..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/property/ValuesProperty.java +++ /dev/null @@ -1,120 +0,0 @@ -/* -* Copyright 2004 The Apache Software Foundation. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -package org.apache.jackrabbit.webdav.spi.property; - -import org.apache.jackrabbit.webdav.property.AbstractDavProperty; -import org.apache.jackrabbit.webdav.property.DavProperty; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.apache.jackrabbit.core.util.ValueHelper; -import org.jdom.Element; - -import javax.jcr.Value; -import javax.jcr.ValueFormatException; -import javax.jcr.RepositoryException; -import java.util.List; -import java.util.Iterator; -import java.util.ArrayList; - -/** - * ValuesProperty extends {@link org.apache.jackrabbit.webdav.property.DavProperty} providing - * utilities to handle the multiple values of the property item represented - * by this resource. - */ -public class ValuesProperty extends AbstractDavProperty implements ItemResourceConstants { - - private final Element[] value; - - /** - * Wrap the specified DavProperty in a new ValuesProperty. - * - * @param property - */ - public ValuesProperty(DavProperty property) { - super(JCR_VALUES, false); - - if (!JCR_VALUES.equals(property.getName())) { - throw new IllegalArgumentException("ValuesProperty may only be created with a property that has name="+JCR_VALUES.getName()); - } - - Element[] elems = new Element[0]; - if (property.getValue() instanceof List) { - Iterator elemIt = ((List)property.getValue()).iterator(); - ArrayList valueElements = new ArrayList(); - while (elemIt.hasNext()) { - Object el = elemIt.next(); - /* make sure, only Elements with name 'value' are used for - * the 'value' field. any other content (other elements, text, - * comment etc.) is ignored. NO bad-request/conflict error is - * thrown. - */ - if (el instanceof Element && "value".equals(((Element)el).getName())) { - valueElements.add(el); - } - } - /* fill the 'value' with the valid 'value' elements found before */ - elems = (Element[])valueElements.toArray(new Element[valueElements.size()]); - } else { - new IllegalArgumentException("ValuesProperty may only be created with a property that has a list of 'value' elements as content."); - } - // finally set the value to the DavProperty - value = elems; - } - - /** - * Create a new ValuesProperty from the given {@link javax.jcr.Value Value - * array}. - * - * @param values Array of Value objects as obtained from the JCR property. - */ - public ValuesProperty(Value[] values) throws ValueFormatException, RepositoryException { - super(JCR_VALUES, false); - - Element[] propValue = new Element[values.length]; - for (int i = 0; i < values.length; i++) { - propValue[i] = new Element(XML_VALUE, ItemResourceConstants.NAMESPACE); - propValue[i].addContent(values[i].getString()); - } - // finally set the value to the DavProperty - value = propValue; - } - - /** - * Converts the value of this property to a {@link javax.jcr.Value value array}. - * Please note, that the convertion is done by using the {@link org.apache.jackrabbit.core.util.ValueHelper} - * class that is not part of the JSR170 API. - * - * @return Array of Value objects - * @throws RepositoryException - */ - public Value[] getValues(int propertyType) throws ValueFormatException, RepositoryException { - Element[] propValue = (Element[])getValue(); - Value[] values = new Value[propValue.length]; - for (int i = 0; i < propValue.length; i++) { - values[i] = ValueHelper.convert(propValue[i].getText(), propertyType); - } - return values; - } - - /** - * Returns an array of {@link Element}s representing the value of this - * property. - * - * @return an array of {@link Element}s - */ - public Object getValue() { - return value; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/search/SearchResourceImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/search/SearchResourceImpl.java deleted file mode 100644 index c9c26228946..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/search/SearchResourceImpl.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.search; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.search.SearchResource; -import org.apache.jackrabbit.webdav.search.QueryGrammerSet; -import org.apache.jackrabbit.webdav.search.SearchRequest; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; - -import javax.jcr.*; -import javax.jcr.query.*; - -/** - * SearchResourceImpl... - */ -public class SearchResourceImpl implements SearchResource, ItemResourceConstants { - - private static Logger log = Logger.getLogger(SearchResourceImpl.class); - - private final DavSession session; - private final DavResourceLocator locator; - - public SearchResourceImpl(DavResourceLocator locator, DavSession session) { - this.session = session; - this.locator = locator; - } - - //-------------------------------------------< SearchResource interface >--- - /** - * @see SearchResource#getQueryGrammerSet() - */ - public QueryGrammerSet getQueryGrammerSet() { - QueryGrammerSet qgs; - try { - QueryManager qMgr = session.getRepositorySession().getWorkspace().getQueryManager(); - String[] langs = qMgr.getSupportedQueryLanguages(); - qgs = new QueryGrammerSet(langs); - } catch (RepositoryException e) { - qgs = new QueryGrammerSet(new String[0]); - } - return qgs; - } - - /** - * Execute the query defined by the given sRequest. - * - * @see SearchResource#search(org.apache.jackrabbit.webdav.search.SearchRequest) - */ - public MultiStatus search(SearchRequest sRequest) throws DavException { - try { - Query q = getQuery(sRequest); - QueryResult qR = q.execute(); - return queryResultToMultiStatus(qR); - - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Create a query from the information present in the sRequest - * object.
The following JCR specific logic is applied: - *
    - *
  • If the requested resource represents a node with nodetype nt:query, the - * request body is ignored and the query defined with the node is executed - * instead.
  • - *
  • If the requested resource does not represent an existing item, the - * specified query is persisted by calling {@link Query#save(String)}.
  • - *
- * @param sRequest defining the query to be executed - * @return Query object. - * @throws InvalidQueryException if the query defined by sRequest is invalid - * @throws RepositoryException the query manager cannot be accessed or if - * another error occurs. - * @throws DavException if sRequest is null and - * the underlaying repository item is not an nt:query node or if an error - * occurs when calling {@link Query#save(String)}/ - */ - private Query getQuery(SearchRequest sRequest) - throws InvalidQueryException, RepositoryException, DavException { - - Node rootNode = session.getRepositorySession().getRootNode(); - QueryManager qMgr = session.getRepositorySession().getWorkspace().getQueryManager(); - String resourcePath = locator.getResourcePath(); - - // test if query is defined by requested repository node - if (!rootNode.getPath().equals(resourcePath)) { - String qNodeRelPath = resourcePath.substring(1); - if (rootNode.hasNode(qNodeRelPath)) { - Node qNode = rootNode.getNode(qNodeRelPath); - if (qNode.isNodeType("nt:query")) { - return qMgr.getQuery(qNode); - } - } - } - - Query q; - if (sRequest != null) { - q = qMgr.createQuery(sRequest.getQuery(), sRequest.getLanguageName()); - } else { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, resourcePath + " is not a nt:query node -> searchRequest body required."); - } - - /* test if resource path does not exist -> thus indicating that - the query must be made persistent by calling Query.save(String) */ - if (!session.getRepositorySession().itemExists(resourcePath)) { - try { - q.save(resourcePath); - } catch (RepositoryException e) { - // ItemExistsException should never occur. - new JcrDavException(e); - } - } - return q; - } - - /** - * Build a MultiStatus object from the specified query result. - * - * @param qResult QueryResult as obtained from {@link javax.jcr.query.Query#execute()}. - * @return MultiStatus object listing the query result in - * Webdav compatible form. - * @throws RepositoryException - */ - private MultiStatus queryResultToMultiStatus(QueryResult qResult) - throws RepositoryException { - MultiStatus ms = new MultiStatus(); - - String[] propertyNames = qResult.getPropertyNames(); - RowIterator rowIter = qResult.getRows(); - while (rowIter.hasNext()) { - Row row = rowIter.nextRow(); - Value[] values = row.getValues(); - String nodePath = values[0].getString(); - - // create a new ms-response for each row of the result set - DavResourceLocator loc = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), nodePath); - String nodeHref = loc.getHref(true); - MultiStatusResponse resp = new MultiStatusResponse(nodeHref); - // add a search-result-property for each value column - for (int i = 0; i < values.length; i++) { - resp.add(new SearchResultProperty(propertyNames[i], values[i])); - } - ms.addResponse(resp); - } - return ms; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/search/SearchResultProperty.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/search/SearchResultProperty.java deleted file mode 100644 index a45b2fdc41f..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/search/SearchResultProperty.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.search; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.property.DefaultDavProperty; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.jdom.Element; - -import javax.jcr.Value; -import javax.jcr.RepositoryException; - -/** - * SearchResultProperty... - */ -public class SearchResultProperty extends DefaultDavProperty { - - private static Logger log = Logger.getLogger(SearchResultProperty.class); - - public static final DavPropertyName SEARCH_RESULT_PROPERTY = DavPropertyName.create("search-result-property", ItemResourceConstants.NAMESPACE); - - private final String propertyName; - - /** - * Creates a new WebDAV property with the given namespace, name and value. - * If the property is intended to be protected the isProtected flag must - * be set to true. - * - * @param propertyName JCR property name - * @param value String representation of the property - */ - public SearchResultProperty(String propertyName, Value value) { - super(SEARCH_RESULT_PROPERTY, value, true); - this.propertyName = propertyName; - } - - /** - * Return the Xml representation. - *
-     *
-     * new SearchResultProperty("bli", new StringValue("blivalue")).toXml()
-     *
-     * returns an element with the following form:
-     *
-     * <jcr:search-result-property>
-     *    <jcr:name>bli<jcr:name/>
-     *    <jcr:value>blivalue<jcr:value/>
-     * </jcr:search-result-property>
-     * 
- * - * @return the Xml representation - * @see org.apache.jackrabbit.webdav.property.DavProperty#toXml() - */ - public Element toXml() { - Element elem = getName().toXml(); - elem.addContent(ItemResourceConstants.JCR_NAME.toXml().setText(propertyName)); - String valueStr = ""; - if (getValue() != null) { - Value value = (Value) getValue(); - try { - valueStr = value.getString(); - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - } - elem.addContent(ItemResourceConstants.JCR_VALUE.toXml().setText(valueStr)); - return elem; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/transaction/TxLockManagerImpl.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/transaction/TxLockManagerImpl.java deleted file mode 100644 index 8e42d7afeac..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/transaction/TxLockManagerImpl.java +++ /dev/null @@ -1,666 +0,0 @@ -/* - * Copyright 2004 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.transaction; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.spi.*; -import org.apache.jackrabbit.webdav.transaction.*; -import org.apache.jackrabbit.webdav.util.Text; -import org.apache.jackrabbit.webdav.lock.*; -import org.apache.jackrabbit.core.XASession; - -import javax.jcr.*; -import javax.transaction.xa.XAResource; -import javax.transaction.xa.XAException; -import javax.transaction.xa.Xid; -import java.util.*; - -/** - * TxLockManagerImpl manages locks with locktype - * '{@link TransactionConstants#TRANSACTION jcr:transaction}'. - * - * todo: removing all expired locks - * todo: 'local' and 'global' are not accurate terms in the given context > replace - * todo: the usage of the 'global' transaction is not according to the JTA specification, - * which explicitely requires any transaction present on a servlet to be completed before - * the service method returns. Starting/completing transactions on the session object, - * which is possible with the jackrabbit implementation is a hack. - * todo: review of this transaction part is therefore required. Is there a use-case - * for those 'global' transactions at all... - */ -public class TxLockManagerImpl implements TxLockManager { - - private static Logger log = Logger.getLogger(TxLockManagerImpl.class); - - private TransactionMap map = new TransactionMap(); - - /** - * Create a new lock. - * - * @param lockInfo as present in the request body. - * @param resource - * @return the lock - * @throws DavException if the lock could not be obtained. - * @throws IllegalArgumentException if the resource is null or - * does not implement {@link TransactionResource} interface. - * @see LockManager#createLock(org.apache.jackrabbit.webdav.lock.LockInfo, org.apache.jackrabbit.webdav.DavResource) - */ - public ActiveLock createLock(LockInfo lockInfo, DavResource resource) - throws DavException { - if (resource == null || !(resource instanceof TransactionResource)) { - throw new IllegalArgumentException("Invalid resource"); - } - return createLock(lockInfo, (TransactionResource)resource); - } - - /** - * Create a new lock. - * - * @param lockInfo - * @param resource - * @return the lock - * @throws DavException if the request lock has the wrong lock type or if - * the lock could not be obtained for any reason. - */ - private synchronized ActiveLock createLock(LockInfo lockInfo, TransactionResource resource) - throws DavException { - if (!lockInfo.isDeep() || !TransactionConstants.TRANSACTION.equals(lockInfo.getType())) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); - } - - ActiveLock existing = getLock(lockInfo.getType(), lockInfo.getScope(), resource); - if (existing != null) { - throw new DavException(DavServletResponse.SC_LOCKED); - } - // TODO: check for locks on member resources is required as well for lock is always deep! - - Transaction tx = createTransaction(resource.getLocator(), lockInfo); - tx.start(resource); - - // keep references to this lock - addReferences(tx, getMap(resource), resource); - - return tx.getLock(); - } - - /** - * Build the transaction object associated by the lock. - * - * @param locator - * @param lockInfo - * @return - */ - private Transaction createTransaction(DavResourceLocator locator, LockInfo lockInfo) { - if (TransactionConstants.GLOBAL.equals(lockInfo.getScope())) { - return new GlobalTransaction(locator, new TxActiveLock(lockInfo)); - } else { - return new LocalTransaction(locator, new TxActiveLock(lockInfo)); - } - } - - /** - * Refresh the lock indentified by the given lock token. - * - * @param lockInfo - * @param lockToken - * @param resource - * @return the lock - * @throws DavException - * @throws IllegalArgumentException if the resource is null or - * does not implement {@link TransactionResource} interface. - * @see LockManager#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String, org.apache.jackrabbit.webdav.DavResource) - */ - public ActiveLock refreshLock(LockInfo lockInfo, String lockToken, - DavResource resource) throws DavException { - if (resource == null || !(resource instanceof TransactionResource)) { - throw new IllegalArgumentException("Invalid resource"); - } - return refreshLock(lockInfo, lockToken, (TransactionResource)resource); - } - - /** - * Reset the timeout of the lock identified by the given lock token. - * - * @param lockInfo - * @param lockToken - * @param resource - * @return - * @throws DavException if the lockdid not exist or is expired. - */ - private synchronized ActiveLock refreshLock(LockInfo lockInfo, String lockToken, - TransactionResource resource) throws DavException { - - TransactionMap responsibleMap = getMap(resource); - Transaction tx = responsibleMap.get(lockToken); - if (tx == null) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No valid transaction lock found for resource '" + resource.getResourcePath()+ "'"); - } else if (tx.getLock().isExpired()) { - removeExpired(tx, responsibleMap, resource); - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction lock for resource '" + resource.getResourcePath()+ "' was already expired."); - } else { - tx.getLock().setTimeout(lockInfo.getTimeout()); - } - return tx.getLock(); - } - - /** - * Throws UnsupportedOperationException. - * - * @param lockToken - * @param resource - * @throws DavException - * @see LockManager#releaseLock(String, org.apache.jackrabbit.webdav.DavResource) - */ - public void releaseLock(String lockToken, DavResource resource) - throws DavException { - throw new UnsupportedOperationException("A transaction lock can only be release with a TransactionInfo object and a lock token."); - } - - /** - * Release the lock identified by the given lock token. - * - * @param lockInfo - * @param lockToken - * @param resource - * @throws DavException - */ - public synchronized void releaseLock(TransactionInfo lockInfo, String lockToken, - TransactionResource resource) throws DavException { - if (resource == null) { - throw new IllegalArgumentException("Resource must not be null."); - } - TransactionMap responsibleMap = getMap(resource); - Transaction tx = responsibleMap.get(lockToken); - - if (tx == null) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No transaction lock found for resource '" + resource.getResourcePath()+ "'"); - } else if (tx.getLock().isExpired()) { - removeExpired(tx, responsibleMap, resource); - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction lock for resource '" + resource.getResourcePath()+ "' was already expired."); - } else { - if (TransactionConstants.XML_COMMIT.equals(lockInfo.getStatus())) { - tx.commit(resource); - } else { - tx.rollback(resource); - } - removeReferences(tx, responsibleMap, resource); - } - } - - /** - * Always returns null - * - * @param type - * @param scope - * @param resource - * @return null - * @see #getLock(Type, Scope, TransactionResource) - * @see LockManager#getLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope, org.apache.jackrabbit.webdav.DavResource) - */ - public ActiveLock getLock(Type type, Scope scope, DavResource resource) { - return null; - } - - /** - * Return the lock applied to the given resource or null - * - * @param type - * @param scope - * @param resource - * @return lock applied to the given resource or null - * @see LockManager#getLock(Type, Scope, DavResource) - * todo: is it correct to return one that specific lock, the current session is token-holder of? - */ - public ActiveLock getLock(Type type, Scope scope, TransactionResource resource) { - ActiveLock lock = null; - if (TransactionConstants.TRANSACTION.equals(type)) { - String[] sessionTokens = resource.getSession().getRepositorySession().getLockTokens(); - int i = 0; - while (lock == null && i < sessionTokens.length) { - String lockToken = sessionTokens[i]; - lock = getLock(lockToken, scope, resource); - i++; - } - } - return lock; - } - - /** - * @param lockToken - * @param resource - * @return - */ - private ActiveLock getLock(String lockToken, Scope scope, DavResource resource) { - if (!(resource instanceof TransactionResource)) { - log.info(""); - return null; - } - - ActiveLock lock = null; - Transaction tx = null; - TransactionMap m = map; - // check if main-map contains that txId - if (m.containsKey(lockToken)) { - tx = m.get(lockToken); - } else { - // look through all the nested tx-maps (i.e. global txs) for the given txId - Iterator it = m.values().iterator(); - while (it.hasNext() && tx == null) { - Transaction txMap = (Transaction) it.next(); - if (!txMap.isLocal()) { - m = ((TransactionMap)txMap); - if (m.containsKey(lockToken)) { - tx = ((TransactionMap)txMap).get(lockToken); - } - } - } - } - - if (tx != null) { - if (tx.getLock().isExpired()) { - removeExpired(tx, m, (TransactionResource)resource); - } else if (tx.appliesToResource(resource) && (scope == null || tx.getLock().getScope().equals(scope))) { - lock = tx.getLock(); - } - } - return lock; - } - - /** - * Returns true if the given lock token belongs to a lock that applies to - * the given resource, false otherwise. The token may either be retrieved - * from the {@link DavConstants#HEADER_LOCK_TOKEN Lock-Token header} or - * from the {@link TransactionConstants#HEADER_TRANSACTIONID TransactionId header}. - * - * @param token - * @param resource - * @return - * @see LockManager#hasLock(String token, DavResource resource) - */ - public boolean hasLock(String token, DavResource resource) { - return getLock(token, null, resource) != null; - } - - /** - * Return the map that may contain a transaction lock for the given resource. - * In case the resource provides a transactionId, the map must be a - * repository transaction that is identified by the given id and which in - * turn can act as map. - * - * @param resource - * @return responsible map. - * @throws DavException if no map could be retrieved. - */ - private TransactionMap getMap(TransactionResource resource) - throws DavException { - - String txKey = resource.getTransactionId(); - if (txKey == null) { - return map; - } else { - if (!map.containsKey(txKey)) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction map '" + map + " does not contain a transaction with TransactionId '" + txKey + "'."); - } - Transaction tx = map.get(txKey); - if (tx.isLocal()) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "TransactionId '" + txKey + "' points to a local transaction, that cannot act as transaction map"); - } else if (tx.getLock() != null && tx.getLock().isExpired()) { - removeExpired(tx, map, resource); - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to retrieve an expired global transaction."); - } - // tx is a global transaction that acts as map as well. - return (TransactionMap)tx; - } - } - - /** - * Rollbacks the specified transaction and releases the lock. This includes - * the removal of all references. - * - * @param tx - * @param responsibleMap - * @param resource - */ - private static void removeExpired(Transaction tx, TransactionMap responsibleMap, - TransactionResource resource) { - log.info("Removing expired transaction lock " + tx); - try { - tx.rollback(resource); - removeReferences(tx, responsibleMap, resource); - } catch (DavException e) { - log.error("Error while removing expired transaction lock: " + e.getMessage()); - } - } - - /** - * Create the required references to the new transaction specified by tx. - * - * @param tx - * @param responsibleMap - * @param resource - * @throws DavException - */ - private static void addReferences(Transaction tx, TransactionMap responsibleMap, - TransactionResource resource) throws DavException { - log.info("Adding transactionId '" + tx.getId() + "' as session lock token."); - resource.getSession().getRepositorySession().addLockToken(tx.getId()); - - responsibleMap.put(tx.getId(), tx); - resource.getSession().addReference(tx.getId()); - } - - /** - * Remove all references to the specified transaction. - * - * @param tx - * @param responsibleMap - * @param resource - */ - private static void removeReferences(Transaction tx, TransactionMap responsibleMap, - TransactionResource resource) { - log.info("Removing transactionId '" + tx.getId() + "' from session lock tokens."); - resource.getSession().getRepositorySession().removeLockToken(tx.getId()); - - responsibleMap.remove(tx.getId()); - resource.getSession().removeReference(tx.getId()); - } - //------------------------------------------< inner classes, interfaces >--- - /** - * Internal Transaction interface - */ - private interface Transaction { - - TxActiveLock getLock(); - - /** - * @return the id of this transaction. - */ - String getId(); - - /** - * @return path of the lock holding resource - */ - String getResourcePath(); - - /** - * @param resource - * @return true if the lock defined by this transaction applies to the - * given resource, either due to the resource holding that lock or due - * to a deep lock hold by any ancestor resource. - */ - boolean appliesToResource(DavResource resource); - - /** - * @return true if this transaction is used to allow for transient changes - * on the underlaying repository, that may be persisted with the final - * UNLOCK request only. - */ - boolean isLocal(); - - /** - * Start this transaction. - * - * @param resource - * @throws DavException if an error occurs. - */ - void start(TransactionResource resource) throws DavException ; - - /** - * Commit this transaction - * - * @param resource - * @throws DavException if an error occurs. - */ - void commit(TransactionResource resource) throws DavException ; - - /** - * Rollback this transaction. - * - * @param resource - * @throws DavException if an error occurs. - */ - void rollback(TransactionResource resource) throws DavException ; - } - - /** - * Abstract transaction covering functionally to both implementations. - */ - private abstract static class AbstractTransaction extends TransactionMap implements Transaction { - - private final DavResourceLocator locator; - private final TxActiveLock lock; - - private AbstractTransaction(DavResourceLocator locator, TxActiveLock lock) { - this.locator = locator; - this.lock = lock; - } - - /** - * @see #getLock() - */ - public TxActiveLock getLock() { - return lock; - } - - /** - * @see #getId() - */ - public String getId() { - return lock.getToken(); - } - - /** - * @see #getResourcePath() - */ - public String getResourcePath() { - return locator.getResourcePath(); - } - - /** - * @see #appliesToResource(DavResource) - */ - public boolean appliesToResource(DavResource resource) { - if (locator.isSameWorkspace(resource.getLocator())) { - String lockResourcePath = getResourcePath(); - String resPath = resource.getResourcePath(); - - while (!"".equals(resPath)) { - if (lockResourcePath.equals(resPath)) { - return true; - } - resPath = Text.getRelativeParent(resPath, 1); - } - } - return false; - } - } - - /** - * - */ - private final static class LocalTransaction extends AbstractTransaction { - - private LocalTransaction(DavResourceLocator locator, TxActiveLock lock) { - super(locator, lock); - } - - public boolean isLocal() { - return true; - } - - public void start(TransactionResource resource) throws DavException { - // make sure, the given resource represents an existing repository item - if (!resource.getSession().getRepositorySession().itemExists(getResourcePath())) { - throw new DavException(DavServletResponse.SC_CONFLICT, "Unable to start local transaction: no repository item present at "+getResourcePath()); - } - } - - public void commit(TransactionResource resource) throws DavException { - try { - DavSession session = resource.getSession(); - session.getRepositorySession().getItem(getResourcePath()).save(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - public void rollback(TransactionResource resource) throws DavException { - try { - DavSession session = resource.getSession(); - session.getRepositorySession().getItem(getResourcePath()).refresh(false); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - public Transaction put(String key, Transaction value) throws DavException { - throw new DavException(WebdavResponse.SC_PRECONDITION_FAILED, "Attempt to nest a new transaction into a local one."); - } - } - - /** - * - */ - private static class GlobalTransaction extends AbstractTransaction { - - private Xid xid; - - private GlobalTransaction(DavResourceLocator locator, TxActiveLock lock) { - super(locator, lock); - xid = new XidImpl(lock.getToken()); - } - - public boolean isLocal() { - return false; - } - - public void start(TransactionResource resource) throws DavException { - XAResource xaRes = getXAResource(resource); - try { - xaRes.setTransactionTimeout((int)getLock().getTimeout()/1000); - xaRes.start(xid, XAResource.TMNOFLAGS); - } catch (XAException e) { - throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage()); - } - } - - public void commit(TransactionResource resource) throws DavException { - XAResource xaRes = getXAResource(resource); - try { - xaRes.commit(xid, false); - removeLocalTxReferences(resource); - } catch (XAException e) { - throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage()); - } - } - - public void rollback(TransactionResource resource) throws DavException { - XAResource xaRes = getXAResource(resource); - try { - xaRes.rollback(xid); - removeLocalTxReferences(resource); - } catch (XAException e) { - throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage()); - } - } - - private XAResource getXAResource(TransactionResource resource) throws DavException { - Session session = resource.getSession().getRepositorySession(); - if (session instanceof XASession) { - return ((XASession)session).getXAResource(); - } else { - throw new DavException(DavServletResponse.SC_FORBIDDEN); - } - } - - private void removeLocalTxReferences(TransactionResource resource) { - Iterator it = values().iterator(); - while (it.hasNext()) { - Transaction tx = (Transaction) it.next(); - removeReferences(tx, this, resource); - } - } - - public Transaction put(String key, Transaction value) throws DavException { - if (!(value instanceof LocalTransaction)) { - throw new DavException(WebdavResponse.SC_PRECONDITION_FAILED, "Attempt to nest global transaction into a global one."); - } - return (Transaction) super.put(key, value); - } - } - - /** - * - */ - private static class TransactionMap extends HashMap { - - public Transaction get(String key) { - Transaction tx = null; - if (containsKey(key)) { - tx = (Transaction) super.get(key); - } - return tx; - } - - public Transaction put(String key, Transaction value) throws DavException { - // any global an local transactions allowed. - return (Transaction) super.put(key, value); - } - } - - /** - * Private class implementing Xid interface. - */ - private static class XidImpl implements Xid { - - private final String id; - - /** - * Create a new Xid - * - * @param id - */ - private XidImpl(String id) { - this.id = id; - } - - /** - * @return 1 - * @see javax.transaction.xa.Xid#getFormatId() - */ - public int getFormatId() { - // todo: define reasonable format id - return 1; - } - - /** - * @return an empty byte array. - * @see javax.transaction.xa.Xid#getBranchQualifier() - */ - public byte[] getBranchQualifier() { - return new byte[0]; - } - - /** - * @return id as byte array - * @see javax.transaction.xa.Xid#getGlobalTransactionId() - */ - public byte[] getGlobalTransactionId() { - return id.getBytes(); - } - } -} diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionControlledItemCollection.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionControlledItemCollection.java deleted file mode 100644 index a4c7c82b144..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionControlledItemCollection.java +++ /dev/null @@ -1,584 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.version; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.spi.DefaultItemCollection; -import org.apache.jackrabbit.webdav.version.*; -import org.apache.jackrabbit.webdav.version.report.*; - -import javax.jcr.*; -import javax.jcr.observation.*; -import javax.jcr.observation.EventListener; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; -import java.util.List; - -/** - * VersionControlledItemCollection represents a JCR node item and - * covers all functionality related to versioning of {@link Node}s. - * - * @see Node - */ -public class VersionControlledItemCollection extends DefaultItemCollection - implements VersionControlledResource { - - private static Logger log = Logger.getLogger(VersionControlledItemCollection.class); - - /** - * Create a new VersionControlledItemCollection. - * - * @param locator - * @param session - */ - public VersionControlledItemCollection(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - super(locator, session, factory); - if (exists() && !(item instanceof Node)) { - throw new IllegalArgumentException("A collection resource can not be constructed from a Property item."); - } - } - - //----------------------------------------------< DavResource interface >--- - /** - * Return a comma separated string listing the supported method names. - * - * @return the supported method names. - * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() - */ - public String getSupportedMethods() { - StringBuffer sb = new StringBuffer(super.getSupportedMethods()); - // Versioning support - sb.append(", ").append(VersionableResource.METHODS); - if (this.isVersionControlled()) { - try { - if (((Node)item).isCheckedOut()) { - sb.append(", ").append(VersionControlledResource.methods_checkedOut); - } else { - sb.append(", ").append(VersionControlledResource.methods_checkedIn); - } - } catch (RepositoryException e) { - // should not occur. - log.error(e.getMessage()); - } - } - return sb.toString(); - } - - //--------------------------------< VersionControlledResource interface >--- - /** - * Adds version control to this resource. If the resource is already under - * version control, this method has no effect. - * - * @throws org.apache.jackrabbit.webdav.DavException if this resource does not - * exist yet or if an error occurs while making the underlaying node versionable. - * @see org.apache.jackrabbit.webdav.version.VersionableResource#addVersionControl() - */ - public void addVersionControl() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!isVersionControlled()) { - try { - ((Node)item).addMixin(MIX_VERSIONABLE); - item.save(); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } // else: is already version controlled -> ignore - } - - /** - * Calls {@link javax.jcr.Node#checkin()} on the underlaying repository node. - * - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkin() - */ - public String checkin() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!isVersionControlled()) { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - try { - Version v = ((Node) item).checkin(); - DavResourceLocator loc = getLocator(); - String versionHref = loc.getFactory().createResourceLocator(loc.getPrefix(), loc.getWorkspacePath(), v.getPath()).getHref(true); - return versionHref; - } catch (RepositoryException e) { - // UnsupportedRepositoryException should not occur - throw new JcrDavException(e); - } - } - - /** - * Calls {@link javax.jcr.Node#checkout()} on the underlaying repository node. - * - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkout() - */ - public void checkout() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!isVersionControlled()) { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - try { - ((Node) item).checkout(); - } catch (RepositoryException e) { - // UnsupportedRepositoryException should not occur - throw new JcrDavException(e); - } - } - - /** - * Not implemented. Always throws a DavException with error code - * {@link org.apache.jackrabbit.webdav.DavServletResponse#SC_NOT_IMPLEMENTED}. - * - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#uncheckout() - */ - public void uncheckout() throws DavException { - throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED); - } - - /** - * Perform an update on this resource. Depending on the format of the updateInfo - * this is translated to one of the following methods defined by the JCR API: - *
    - *
  • {@link Node#restore(javax.jcr.version.Version, boolean)}
  • - *
  • {@link Node#restore(javax.jcr.version.Version, String, boolean)}
  • - *
  • {@link Node#restoreByLabel(String, boolean)}
  • - *
  • {@link Workspace#restore(javax.jcr.version.Version[], boolean)}
  • - *
  • {@link Node#update(String)}
  • - *
- *

- * Limitation: note that the MultiStatus returned by this method - * will not list any nodes that have been removed due to an Uuid conflict. - * - * @param updateInfo - * @return - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#update(org.apache.jackrabbit.webdav.version.UpdateInfo) - * @todo with jcr the node must not be versionable in order to perform Node.update. - */ - public MultiStatus update(UpdateInfo updateInfo) throws DavException { - if (updateInfo == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid update request body required."); - } - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - MultiStatus ms = new MultiStatus(); - try { - Node node = (Node)item; - boolean removeExisting = updateInfo.getUpdateElement().getChild(XML_REMOVEEXISTING, NAMESPACE) != null; - - // register eventListener in order to be able to report the modified resources. - EventListener el = new EListener(updateInfo.getPropertyNameSet(), ms); - registerEventListener(el, node.getPath()); - - // perform the update/restore according to the update info - if (updateInfo.getVersionHref() != null) { - VersionHistory vh = node.getVersionHistory(); - String[] hrefs = updateInfo.getVersionHref(); - Version[] versions = new Version[hrefs.length]; - for (int i = 0; i < hrefs.length; i++) { - versions[i] = vh.getVersion(getResourceName(hrefs[i], true)); - } - if (versions.length == 1) { - String relPath = updateInfo.getUpdateElement().getChildText(XML_RELPATH, NAMESPACE); - if (relPath == null) { - node.restore(versions[0], removeExisting); - } else { - node.restore(versions[0], relPath, removeExisting); - } - } else { - getRepositorySession().getWorkspace().restore(versions, removeExisting); - } - } else if (updateInfo.getLabelName() != null) { - String[] labels = updateInfo.getLabelName(); - if (labels.length == 1) { - node.restoreByLabel(labels[0], removeExisting); - } else { - Version[] vs = new Version[labels.length]; - VersionHistory vh = node.getVersionHistory(); - for (int i = 0; i < labels.length; i++) { - vs[i] = vh.getVersionByLabel(labels[i]); - } - getRepositorySession().getWorkspace().restore(vs, removeExisting); - } - } else if (updateInfo.getWorkspaceHref() != null) { - String workspaceName = getResourceName(updateInfo.getWorkspaceHref(), true); - node.update(workspaceName); - } else { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid update request body."); - } - - // unregister the event listener again - unregisterEventListener(el); - - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - return ms; - } - - /** - * Merge the repository node represented by this resource according to the - * information present in the given {@link MergeInfo} object. - * - * @param mergeInfo - * @return MultiStatus reccording all repository items affected - * by this merge call. - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#merge(org.apache.jackrabbit.webdav.version.MergeInfo) - * @see Node#merge(String, boolean) - * @todo with jcr the node must not be versionable in order to perform Node.merge - */ - public MultiStatus merge(MergeInfo mergeInfo) throws DavException { - if (mergeInfo == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST); - } - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - MultiStatus ms = new MultiStatus(); - try { - Node node = (Node)item; - - // register eventListener in order to be able to report the modifications. - EventListener el = new EListener(mergeInfo.getPropertyNameSet(), ms); - registerEventListener(el, node.getPath()); - - String workspaceName = getResourceName(mergeInfo.getSourceHref(), true); - node.merge(workspaceName, !mergeInfo.isNoAutoMerge()); - - // unregister the event listener again - unregisterEventListener(el); - - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - - return ms; - } - - /** - * Resolve the merge conflicts according to the value of the {@link #AUTO_MERGE_SET DAV:auto-merge-set} - * property present in the specified DavPropertySets. - * - * @param setProperties - * @param removePropertyNames - * @throws org.apache.jackrabbit.webdav.DavException - * @see VersionControlledResource#resolveMergeConflict(DavPropertySet, DavPropertyNameSet) - * @see Node#doneMerge(Version) - * @see Node#cancelMerge(Version) - */ - public void resolveMergeConflict(DavPropertySet setProperties, - DavPropertyNameSet removePropertyNames) throws DavException { - - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - if (!isVersionControlled()) { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - - try { - Node n = (Node)item; - if (removePropertyNames.contains(AUTO_MERGE_SET)) { - // retrieve the current jcr:mergeFailed property values - if (!((Node)item).hasProperty(PROP_MERGEFAILED)) { - throw new DavException(DavServletResponse.SC_CONFLICT, "Attempt to resolve non-existing merge conflicts."); - } - Value[] mergeFailed = ((Node)item).getProperty(PROP_MERGEFAILED).getValues(); - - // resolve all remaining merge conflicts with 'cancel' - for (int i = 0; i < mergeFailed.length; i++) { - n.cancelMerge((Version)getRepositorySession().getNodeByUUID(mergeFailed[i].getString())); - } - // adjust removeProperty set - removePropertyNames.remove(AUTO_MERGE_SET); - - } else if (setProperties.contains(AUTO_MERGE_SET) && setProperties.contains(PREDECESSOR_SET)){ - // retrieve the current jcr:mergeFailed property values - if (!((Node)item).hasProperty(PROP_MERGEFAILED)) { - throw new DavException(DavServletResponse.SC_CONFLICT, "Attempt to resolve non-existing merge conflicts."); - } - Value[] mergeFailed = ((Node)item).getProperty(PROP_MERGEFAILED).getValues(); - - - // check which mergeFailed entries have been removed from the - // auto-merge-set (cancelMerge) and have been moved over to the - // predecessor set (doneMerge) - List mergeset = new HrefProperty(setProperties.get(AUTO_MERGE_SET)).getHrefs(); - List predecSet = new HrefProperty(setProperties.get(PREDECESSOR_SET)).getHrefs(); - - Session session = getRepositorySession(); - for (int i = 0; i < mergeFailed.length; i++) { - // build version-href from each entry in the jcr:mergeFailed property - Version version = (Version) session.getNodeByUUID(mergeFailed[i].getString()); - String href = this.getLocatorFromItem(version).getHref(true); - - // Test if that version has been removed from the merge-set. - // thus indicating that the merge-conflict needs to be resolved. - if (!mergeset.contains(href)) { - // Test if the 'href' has been moved over to the - // predecessor-set (thus 'doneMerge' is appropriate) or - // if it is not present in the predecessor set and the - // the conflict is resolved by 'cancelMerge'. - if (predecSet.contains(href)) { - n.doneMerge(version); - } else { - n.cancelMerge(version); - } - } - } - // adjust setProperty set - setProperties.remove(AUTO_MERGE_SET); - setProperties.remove(PREDECESSOR_SET); - } else { - // setPropertySet and removePropertySet do not ask for resolving merge conflicts */ - log.debug("setProperties and removeProperties sets do not request for merge conflict resolution."); - } - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Modify the labels present with the versions of this resource. - * - * @param labelInfo - * @throws DavException - * @see VersionHistory#addVersionLabel(String, String, boolean) - * @see VersionHistory#removeVersionLabel(String) - */ - public void label(LabelInfo labelInfo) throws DavException { - if (labelInfo == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid label request body required."); - } - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - try { - if (!isVersionControlled() || ((Node)item).isCheckedOut()) { - throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "A LABEL request may only be applied to a version-controlled, checked-in resource."); - } - DavResource[] resArr = this.getReferenceResources(CHECKED_IN); - if (resArr.length == 1 && resArr[0] instanceof VersionResource) { - ((VersionResource)resArr[0]).label(labelInfo); - } else { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "DAV:checked-in property on '" + getHref() + "' did not point to a single VersionResource."); - } - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Returns the {@link VersionHistory} associated with the repository node. - * If the node is not versionable an exception is thrown. - * - * @return the {@link VersionHistoryResource} associated with this resource. - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#getVersionHistory() - * @see javax.jcr.Node#getVersionHistory() - */ - public VersionHistoryResource getVersionHistory() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - try { - VersionHistory vh = ((Node)item).getVersionHistory(); - DavResourceLocator loc = getLocatorFromItem(vh); - return (VersionHistoryResource) createResourceFromLocator(loc); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - //-------------------------------------------------------------------------- - /** - * Define the set of reports supported by this resource. - * - * @see SupportedReportSetProperty - */ - protected void initSupportedReports() { - super.initSupportedReports(); - if (exists()) { - supportedReports.addReportType(ReportType.LOCATE_BY_HISTORY); - if (this.isVersionControlled()) { - supportedReports.addReportType(ReportType.VERSION_TREE); - } - } - } - - /** - * Fill the property set for this resource. - */ - protected void initProperties() { - super.initProperties(); - if (exists()) { - Node n = (Node)item; - // properties defined by RFC 3253 for version-controlled resources - if (isVersionControlled()) { - // workspace property already set in AbstractResource.initProperties() - try { - // DAV:version-history (computed) - String vhHref = getLocatorFromResourcePath(n.getVersionHistory().getPath()).getHref(true); - properties.add(new HrefProperty(VERSION_HISTORY, vhHref, true)); - - // DAV:auto-version property: there is no auto version, explicit CHECKOUT is required. - properties.add(new DefaultDavProperty(AUTO_VERSION, null, false)); - - String baseVHref = getLocatorFromResourcePath(n.getBaseVersion().getPath()).getHref(true); - if (n.isCheckedOut()) { - // DAV:checked-out property (protected) - properties.add(new HrefProperty(CHECKED_OUT, baseVHref, true)); - - // DAV:predecessors property - if (n.hasProperty(PROP_PREDECESSORS)) { - Value[] predec = n.getProperty(PROP_PREDECESSORS).getValues(); - addHrefProperty(PREDECESSOR_SET, predec, false); - } - // DAV:auto-merge-set property. NOTE: the DAV:merge-set - // never occurs, because merging without bestEffort flag - // being set results in an exception on failure. - if (n.hasProperty(PROP_MERGEFAILED)) { - ReferenceValue[] mergeFailed = (ReferenceValue[]) n.getProperty(PROP_MERGEFAILED).getValues(); - addHrefProperty(AUTO_MERGE_SET, mergeFailed, false); - } - // todo: checkout-fork, checkin-fork - } else { - // DAV:checked-in property (protected) - properties.add(new HrefProperty(CHECKED_IN, baseVHref, true)); - } - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - } - } - } - - /** - * Add a {@link org.apache.jackrabbit.webdav.property.HrefProperty} with the specified property name and values. - * - * @param name - * @param values Array of {@link ReferenceValue}s. - * @param isProtected - * @throws javax.jcr.ValueFormatException - * @throws IllegalStateException - * @throws javax.jcr.RepositoryException - */ - private void addHrefProperty(DavPropertyName name, Value[] values, - boolean isProtected) - throws ValueFormatException, IllegalStateException, RepositoryException { - Node[] nodes = new Node[values.length]; - for (int i = 0; i < values.length; i++) { - nodes[i] = getRepositorySession().getNodeByUUID(values[i].getString()); - } - addHrefProperty(name, nodes, isProtected); - } - - /** - * @return true, if this resource represents an existing repository node - * that has the mixin nodetype 'mix:versionable' set. - */ - private boolean isVersionControlled() { - boolean vc = false; - if (exists()) { - try { - vc = ((Node) item).isNodeType("mix:versionable"); - } catch (RepositoryException e) { - log.warn(e.getMessage()); - } - } - return vc; - } - - /** - * Register the specified event listener with the observation manager present - * the repository session. - * - * @param listener - * @param nodePath - * @throws javax.jcr.RepositoryException - */ - private void registerEventListener(EventListener listener, String nodePath) throws RepositoryException { - getRepositorySession().getWorkspace().getObservationManager().addEventListener(listener, EListener.ALL_EVENTS, nodePath, true, null, null, false); - } - - /** - * Unregister the specified event listener with the observation manager present - * the repository session. - * - * @param listener - * @throws javax.jcr.RepositoryException - */ - private void unregisterEventListener(EventListener listener) throws RepositoryException { - getRepositorySession().getWorkspace().getObservationManager().removeEventListener(listener); - } - - //------------------------------------------------------< inner classes >--- - /** - * Simple EventListener that creates a new {@link org.apache.jackrabbit.webdav.MultiStatusResponse} object - * for each event and adds it to the specified {@link org.apache.jackrabbit.webdav.MultiStatus}. - */ - private class EListener implements EventListener { - - private static final int ALL_EVENTS = Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED; - - private final DavPropertyNameSet propNameSet; - private MultiStatus ms; - - private EListener(DavPropertyNameSet propNameSet, MultiStatus ms) { - this.propNameSet = propNameSet; - this.ms = ms; - } - - /** - * @see EventListener#onEvent(javax.jcr.observation.EventIterator) - */ - public void onEvent(EventIterator events) { - while (events.hasNext()) { - try { - Event e = events.nextEvent(); - String itemPath = e.getPath(); - DavResourceLocator loc = getLocatorFromResourcePath(itemPath); - DavResource res = createResourceFromLocator(loc); - ms.addResponse(new MultiStatusResponse(res, propNameSet)); - - } catch (DavException e) { - // should not occur - log.error("Error while building MultiStatusResponse from Event: " + e.getMessage()); - } catch (RepositoryException e) { - // should not occur - log.error("Error while building MultiStatusResponse from Event: " + e.getMessage()); - } - } - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionHistoryItemCollection.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionHistoryItemCollection.java deleted file mode 100644 index 3bb743be380..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionHistoryItemCollection.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.version; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.version.*; -import org.apache.jackrabbit.webdav.property.HrefProperty; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.spi.DefaultItemCollection; -import org.apache.jackrabbit.webdav.*; - -import javax.jcr.RepositoryException; -import javax.jcr.version.VersionHistory; -import javax.jcr.version.VersionIterator; -import java.util.ArrayList; - -/** - * VersionHistoryItemCollection represents a JCR version history. - * - * @see VersionHistory - */ -public class VersionHistoryItemCollection extends DefaultItemCollection - implements VersionHistoryResource { - - private static Logger log = Logger.getLogger(VersionHistoryItemCollection.class); - - /** - * Create a new VersionHistoryItemCollection resource. - * - * @param resourcePath - * @param session - * @param factory - */ - public VersionHistoryItemCollection(DavResourceLocator resourcePath, DavSession session, DavResourceFactory factory) { - super(resourcePath, session, factory); - if (item == null || !(item instanceof VersionHistory)) { - throw new IllegalArgumentException("VersionHistory item expected."); - } - } - - //----------------------------------------------< DavResource interface >--- - /** - * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() - */ - public String getSupportedMethods() { - StringBuffer sb = new StringBuffer(ItemResourceConstants.METHODS); - sb.append(", ").append(VersionHistoryResource.METHODS); - return sb.toString(); - } - - /** - * Removing a version resource is achieved by calling removeVersion - * on the versionhistory item this version belongs to. - * - * @throws DavException if the version does not exist or if an error occurs - * while deleting. - * @see DavResource#removeMember(org.apache.jackrabbit.webdav.DavResource) - */ - public void removeMember(DavResource member) throws DavException { - if (exists()) { - VersionHistory versionHistory = (VersionHistory) item; - try { - versionHistory.removeVersion(getResourceName(member.getHref(), true)); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } else { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - } - //-----------------------------------< VersionHistoryResource interface >--- - /** - * Return an array of {@link VersionResource}s representing all versions - * present in the underlaying JCR version history. - * - * @return array of {@link VersionResource}s representing all versions - * present in the underlaying JCR version history. - * @throws DavException - * @see org.apache.jackrabbit.webdav.version.VersionHistoryResource#getVersions() - */ - public VersionResource[] getVersions() throws DavException { - try { - VersionIterator vIter = ((VersionHistory)item).getAllVersions(); - ArrayList l = new ArrayList(); - while (vIter.hasNext()) { - DavResourceLocator versionLoc = getLocatorFromItem(vIter.nextVersion()); - DavResource vr = createResourceFromLocator(versionLoc); - l.add(vr); - } - return (VersionResource[]) l.toArray(new VersionResource[l.size()]); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - //-------------------------------------------------------------------------- - /** - * Fill the property set for this resource. - */ - protected void initProperties() { - super.initProperties(); - - // change resourcetype defined by default item collection - properties.add(new ResourceType(ResourceType.VERSION_HISTORY)); - - // required root-version property for version-history resource - try { - String rootVersionResourcePath = ((VersionHistory)item).getRootVersion().getPath(); - properties.add(new HrefProperty(VersionHistoryResource.ROOT_VERSION, getLocatorFromResourcePath(rootVersionResourcePath).getHref(true), true)); - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - - // required, protected version-set property for version-history resource - try { - VersionIterator vIter = ((VersionHistory)item).getAllVersions(); - addHrefProperty(VersionHistoryResource.VERSION_SET, vIter, true); - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionItemCollection.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionItemCollection.java deleted file mode 100644 index fba927dbbef..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/VersionItemCollection.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.version; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.apache.jackrabbit.webdav.spi.DefaultItemCollection; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.version.*; -import org.apache.jackrabbit.webdav.version.report.ReportType; -import org.jdom.Element; - -import javax.jcr.*; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; -import java.util.List; -import java.util.ArrayList; - -/** - * VersionItemCollection represents a JCR version. - * - * @see Version - */ -public class VersionItemCollection extends DefaultItemCollection - implements VersionResource { - - private static Logger log = Logger.getLogger(VersionItemCollection.class); - - /** - * Create a new VersionItemCollection. - * - * @param locator - * @param session - * @param factory - */ - public VersionItemCollection(DavResourceLocator locator, DavSession session, DavResourceFactory factory) { - super(locator, session, factory); - if (item == null || !(item instanceof Version)) { - throw new IllegalArgumentException("Version item expected."); - } - } - - //----------------------------------------------< DavResource interface >--- - /** - * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() - */ - public String getSupportedMethods() { - StringBuffer sb = new StringBuffer(ItemResourceConstants.METHODS); - sb.append(", ").append(VersionResource.METHODS); - return sb.toString(); - } - - //------------------------------------------< VersionResource interface >--- - /** - * Modify the labels defined for the underlaying repository version. - * - * @param labelInfo - * @throws DavException - * @see VersionResource#label(org.apache.jackrabbit.webdav.version.LabelInfo) - * @see VersionHistory#addVersionLabel(String, String, boolean) - * @see VersionHistory#removeVersionLabel(String) - */ - public void label(LabelInfo labelInfo) throws DavException { - if (labelInfo == null) { - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid label request body required."); - } - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - try { - VersionHistory vh = getVersionHistoryItem(); - if (labelInfo.getType() == LabelInfo.TYPE_REMOVE) { - vh.removeVersionLabel(labelInfo.getLabelName()); - } else if (labelInfo.getType() == LabelInfo.TYPE_ADD) { - // ADD: only add if not yet existing - vh.addVersionLabel(item.getName(), labelInfo.getLabelName(), false); - } else { - // SET: move label if already existing - vh.addVersionLabel(item.getName(), labelInfo.getLabelName(), true); - } - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Returns the {@link VersionHistory} associated with the repository version. - * Note: in contrast to a versionable node, the version history of a version - * item is always represented by its nearest ancestor. - * - * @return the {@link VersionHistoryResource} associated with this resource. - * @throws org.apache.jackrabbit.webdav.DavException - * @see org.apache.jackrabbit.webdav.version.VersionResource#getVersionHistory() - * @see javax.jcr.Item#getParent() - */ - public VersionHistoryResource getVersionHistory() throws DavException { - if (!exists()) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } - - try { - VersionHistory vh = getVersionHistoryItem(); - DavResourceLocator loc = getLocatorFromItem(vh); - return (VersionHistoryResource) createResourceFromLocator(loc); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Return the nearest ancestor of the underlaying repository item. - * - * @return nearest ancestor of the underlaying repository item. - * @throws RepositoryException - */ - private VersionHistory getVersionHistoryItem() throws RepositoryException { - return (VersionHistory) item.getParent(); - } - - //-------------------------------------------------------------------------- - /** - * Define the set of reports supported by this resource. - * - * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty - */ - protected void initSupportedReports() { - super.initSupportedReports(); - if (exists()) { - supportedReports.addReportType(ReportType.VERSION_TREE); - } - } - - /** - * Fill the property set for this resource. - */ - protected void initProperties() { - super.initProperties(); - - if (exists()) { - Version v = (Version)item; - // created and creationDate properties - try { - String creationDate = DavConstants.creationDateFormat.format(v.getCreated().getTime()); - // jcr specific 'created' property - properties.add(new DefaultDavProperty(CREATED, creationDate)); - // replace dummy creation date from default collection - properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, creationDate)); - - // required, protected DAV:version-name property - properties.add(new DefaultDavProperty(VERSION_NAME, v.getName(), true)); - - // required, protected DAV:label-name-set property - String[] labels = getVersionHistoryItem().getVersionLabels(v); - Element[] labelElems = new Element[labels.length]; - for (int i = 0; i < labels.length; i++) { - labelElems[i] = new Element(DeltaVConstants.XML_LABEL_NAME, NAMESPACE).setText(labels[i]); - } - properties.add(new DefaultDavProperty(LABEL_NAME_SET, labelElems, true)); - - // required DAV:predecessor-set (protected) and DAV:successor-set (computed) properties - addHrefProperty(VersionResource.PREDECESSOR_SET, v.getPredecessors(), true); - addHrefProperty(SUCCESSOR_SET, v.getSuccessors(), true); - - // required DAV:version-history (computed) property - String vhPath = getVersionHistoryItem().getPath(); - properties.add(new HrefProperty(VersionResource.VERSION_HISTORY, getLocatorFromResourcePath(vhPath).getHref(true), true)); - - // required DAV:checkout-set (computed) property - PropertyIterator it = v.getReferences(); - List nodeList = new ArrayList(); - while (it.hasNext()) { - Property p = it.nextProperty(); - if (PROP_BASEVERSION.equals(p.getName())) { - Node n = p.getParent(); - if (n.isCheckedOut()) { - nodeList.add(n); - } - } - } - addHrefProperty(CHECKOUT_SET, (Node[]) nodeList.toArray(new Node[nodeList.size()]), true); - - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/package.html b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/package.html deleted file mode 100644 index aeea3f616a8..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/package.html +++ /dev/null @@ -1,9 +0,0 @@ - -Contains JCR specific implementations for the following interfaces: -
    -
  • VersionableResource
  • -
  • VersionControlledResource
  • -
  • VersionResource
  • -
  • VersionHistoryResource
  • -
- \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/ExportViewReport.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/ExportViewReport.java deleted file mode 100644 index 6312a68bad8..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/ExportViewReport.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.version.report.*; -import org.apache.jackrabbit.webdav.version.DeltaVResource; -import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.DavSession; -import org.apache.jackrabbit.webdav.DavServletResponse; -import org.apache.jackrabbit.webdav.util.Text; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.JDOMException; -import org.jdom.input.SAXBuilder; - -import javax.jcr.Session; -import javax.jcr.RepositoryException; -import javax.jcr.PathNotFoundException; -import java.io.*; - -/** - * ExportViewReport handles REPORT requests for the 'exportview' - * report. The 'exportview' report is used to export - * {@link Session#exportDocView(String, java.io.OutputStream, boolean, boolean) DocView} - * and {@link Session#exportSysView(String, java.io.OutputStream, boolean, boolean) SysView} - * of the {@link javax.jcr.Item item} represented by the requested resource. - *

- * The request body must contain a jcr:exportview element: - *

- * <!ELEMENT exportview  ( (sysview | docview)?, skipbinary?, norecurse ) >
- * <!ELEMENT sysview EMPTY >
- * <!ELEMENT docview EMPTY >
- * <!ELEMENT skipbinary EMPTY >
- * <!ELEMENT norecurse EMPTY >
- * 
- * If no view type is specified the DocView is generated. - */ -public class ExportViewReport implements Report { - - private static Logger log = Logger.getLogger(ExportViewReport.class); - - private static final String REPORT_NAME = "exportview"; - - /** - * The exportview report type - */ - public static final ReportType EXPORTVIEW_REPORT = ReportType.register(REPORT_NAME, ItemResourceConstants.NAMESPACE, ExportViewReport.class); - - private String absPath; - private Session session; - private ReportInfo info; - - /** - * Returns {@link #EXPORTVIEW_REPORT} report type. - * - * @return {@link #EXPORTVIEW_REPORT} - * @see org.apache.jackrabbit.webdav.version.report.Report#getType() - */ - public ReportType getType() { - return EXPORTVIEW_REPORT; - } - - /** - * @param resource The resource this report is generated from. NOTE: the - * {@link org.apache.jackrabbit.webdav.DavResource#getResourcePath() resource path} - * of the resource is used as 'absPath' argument for exporting the specified - * view. - * @throws IllegalArgumentException if the resource is null or - * if the session object provided with the resource is null. - * @see Report#setResource(org.apache.jackrabbit.webdav.version.DeltaVResource) - */ - public void setResource(DeltaVResource resource) { - if (resource == null) { - throw new IllegalArgumentException("Resource must not be null."); - } - DavSession davSession = resource.getSession(); - if (davSession == null || davSession.getRepositorySession() == null) { - throw new IllegalArgumentException("The resource must provide a non-null session object in order to create the jcr:nodetypes report."); - } - session = davSession.getRepositorySession(); - absPath = resource.getResourcePath(); - } - - /** - * @param info - * @throws IllegalArgumentException if the specified {@link ReportInfo info} - * object does not contain a jcr:exportview element. - * @see Report#setInfo(org.apache.jackrabbit.webdav.version.report.ReportInfo) - */ - public void setInfo(ReportInfo info) { - if (info == null || !REPORT_NAME.equals(info.getReportElement().getName())) { - throw new IllegalArgumentException("jcr:exportview element expected."); - } - this.info = info; - } - - /** - * Creates a Xml document from the generated view. - * - * @return Xml document representing the output of the specified view. - * @throws DavException if the report document could not be created. - * @see org.apache.jackrabbit.webdav.version.report.Report#toXml() - */ - public Document toXml() throws DavException { - Element reportElem = info.getReportElement(); - boolean skipBinary = reportElem.getChild("skipbinary", ItemResourceConstants.NAMESPACE) != null; - boolean noRecurse = reportElem.getChild("norecurse", ItemResourceConstants.NAMESPACE) != null; - - try { - // create tmpFile in default system-tmp directory - String prefix = "_tmp_" + Text.getLabel(absPath); - File tmpfile = File.createTempFile(prefix, null, null); - tmpfile.deleteOnExit(); - FileOutputStream out = new FileOutputStream(tmpfile); - - if (reportElem.getChild("sysview", ItemResourceConstants.NAMESPACE) != null) { - session.exportSysView(absPath, out, skipBinary, noRecurse); - } else { - // default is docview - session.exportDocView(absPath, out, skipBinary, noRecurse); - } - out.close(); - - SAXBuilder builder = new SAXBuilder(false); - InputStream in = new FileInputStream(tmpfile); - return builder.build(in); - - } catch (FileNotFoundException e) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); - } catch (IOException e) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); - } catch (PathNotFoundException e) { - throw new DavException(DavServletResponse.SC_NOT_FOUND); - } catch (RepositoryException e) { - throw new JcrDavException(e); - } catch (JDOMException e) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/LocateByUuidReport.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/LocateByUuidReport.java deleted file mode 100644 index 08eeee149e9..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/LocateByUuidReport.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.version.report.*; -import org.apache.jackrabbit.webdav.version.DeltaVResource; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; -import org.jdom.Document; - -import javax.jcr.Node; -import javax.jcr.RepositoryException; - -/** - * LocateByUuidReport handles REPORT requests for the 'locate-by-uuid' - * report. - *

- * The request body must be a 'jcr:locate-by-uuid' XML element: - *

- * <!ELEMENT locate-by-uuid ( href , prop? ) >
- * 
- * The response to a successful report request will be a Multi-Status response. - */ -public class LocateByUuidReport implements Report { - - private static Logger log = Logger.getLogger(LocateByUuidReport.class); - - private static final String REPORT_NAME = "locate-by-uuid"; - - /** - * The exportview report type - */ - public static final ReportType LOCATE_BY_UUID_REPORT = ReportType.register(REPORT_NAME, ItemResourceConstants.NAMESPACE, LocateByUuidReport.class); - - private DeltaVResource resource; - private ReportInfo info; - - /** - * Returns {@link #LOCATE_BY_UUID_REPORT} report type. - * - * @return {@link #LOCATE_BY_UUID_REPORT} - * @see org.apache.jackrabbit.webdav.version.report.Report#getType() - */ - public ReportType getType() { - return LOCATE_BY_UUID_REPORT; - } - - /** - * @param resource - * @throws IllegalArgumentException if the resource is null or - * if the session object provided with the resource is null. - * @see Report#setResource(org.apache.jackrabbit.webdav.version.DeltaVResource) - */ - public void setResource(DeltaVResource resource) { - if (resource == null) { - throw new IllegalArgumentException("Resource must not be null."); - } - DavSession davSession = resource.getSession(); - if (davSession == null || davSession.getRepositorySession() == null) { - throw new IllegalArgumentException("The resource must provide a non-null session object in order to create the jcr:nodetypes report."); - } - this.resource = resource; - } - - /** - * @param info - * @throws IllegalArgumentException if the specified {@link ReportInfo info} - * object does not contain a jcr:exportview element. - * @see Report#setInfo(org.apache.jackrabbit.webdav.version.report.ReportInfo) - */ - public void setInfo(ReportInfo info) { - if (info == null || !REPORT_NAME.equals(info.getReportElement().getName())) { - throw new IllegalArgumentException("jcr:locate-by-uuid element expected."); - } - this.info = info; - } - - /** - * Creates a Xml document from the generated view. - * - * @return Xml document representing the output of the specified view. - * @throws DavException if the report document could not be created. - * @see org.apache.jackrabbit.webdav.version.report.Report#toXml() - */ - public Document toXml() throws DavException { - String uuid = info.getReportElement().getChildText(DavConstants.XML_HREF, DavConstants.NAMESPACE); - DavPropertyNameSet propNameSet = info.getPropertyNameSet(); - - try { - DavSession session = resource.getSession(); - DavResourceLocator resourceLoc = resource.getLocator(); - - Node n = session.getRepositorySession().getNodeByUUID(uuid); - - DavResourceLocator loc = resourceLoc.getFactory().createResourceLocator(resourceLoc.getPrefix(), resourceLoc.getWorkspacePath(), n.getPath()); - DavResource res = resource.getFactory().createResource(loc, session); - - MultiStatus ms = new MultiStatus(); - ms.addResourceProperties(res, propNameSet, 0); - return ms.toXml(); - - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/NodeTypesReport.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/NodeTypesReport.java deleted file mode 100644 index 1258a128aa0..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/NodeTypesReport.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.version.report.*; -import org.apache.jackrabbit.webdav.version.DeltaVResource; -import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.DavServletResponse; -import org.apache.jackrabbit.webdav.DavSession; -import org.apache.jackrabbit.webdav.spi.nodetype.NodeTypeConstants; -import org.apache.jackrabbit.webdav.spi.nodetype.PropertyDefImpl; -import org.apache.jackrabbit.webdav.spi.nodetype.NodeDefImpl; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.core.util.IteratorHelper; -import org.jdom.Document; -import org.jdom.Element; - -import javax.jcr.nodetype.*; -import javax.jcr.*; -import java.util.*; - -/** - * NodeTypesReport allows to retrieve the definition of a single - * or multiple node types. The request body must be a 'jcr:nodetypes' element: - *
- * <!ELEMENT nodetypes ( nodetype+ | all-nodetypes | mixin-nodetypes | primary-nodetypes ) >
- *
- * <!ELEMENT nodetype ( nodetype-name ) >
- * <!ELEMENT nodetype-name (#PCDATA) >
- *
- * <!ELEMENT all-nodetypes EMPTY >
- * <!ELEMENT mixin-nodetypes EMPTY >
- * <!ELEMENT primary-nodetypes EMPTY >
- * 
- * - * @todo currently the nodetype report is not consistent with the general way of representing nodetype names (with NodetypeElement) in order to be compatible with the jackrabbit nodetype registry... - * @todo for the same reason, not the complete nodetype-definition, but only the nodetype def as stored is represented. - * @todo no namespace definition with response (> jackrabbit)... and nodetype element has same name as the one used with dav-properties - */ -public class NodeTypesReport implements Report, NodeTypeConstants { - - private static Logger log = Logger.getLogger(NodeTypesReport.class); - - /** - * The registered type of this report. - */ - public static final ReportType NODETYPES_REPORT = ReportType.register("nodetypes", NodeTypeConstants.NAMESPACE, NodeTypesReport.class); - - private NodeTypeManager ntMgr; - private ReportInfo info; - - /** - * Returns {@link #NODETYPES_REPORT} type. - * @return {@link #NODETYPES_REPORT} - * @see org.apache.jackrabbit.webdav.version.report.Report#getType() - */ - public ReportType getType() { - return NODETYPES_REPORT; - } - - /** - * @param resource - * @throws IllegalArgumentException if the resource or the session retrieved - * from the specified resource is null - * @see Report#setResource(org.apache.jackrabbit.webdav.version.DeltaVResource) - */ - public void setResource(DeltaVResource resource) { - if (resource == null) { - throw new IllegalArgumentException("Resource must not be null."); - } - try { - DavSession session = resource.getSession(); - if (session == null || session.getRepositorySession() == null) { - throw new IllegalArgumentException("The resource must provide a non-null session object in order to create the jcr:nodetypes report."); - } - ntMgr = session.getRepositorySession().getWorkspace().getNodeTypeManager(); - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - } - - /** - * @param info - * @throws IllegalArgumentException if the specified info does not contain - * a jcr:nodetypes element. - * @see Report#setInfo(org.apache.jackrabbit.webdav.version.report.ReportInfo) - */ - public void setInfo(ReportInfo info) { - if (info == null || !"nodetypes".equals(info.getReportElement().getName())) { - throw new IllegalArgumentException("jcr:nodetypes element expected."); - } - this.info = info; - } - - /** - * Returns a Xml representation of the node type definition(s) according - * to the info object. - * - * @return Xml representation of the node type definition(s) - * @throws DavException if the specified nodetypes are not known or if another - * error occurs while retrieving the nodetype definitions. - * @see org.apache.jackrabbit.webdav.version.report.Report#toXml() - */ - public Document toXml() throws DavException { - if (info == null || ntMgr == null) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while running jcr:nodetypes report"); - } - try { - Element report = new Element(XML_NODETYPES); - NodeTypeIterator ntIter = getNodeTypes(); - while (ntIter.hasNext()) { - NodeType nt = ntIter.nextNodeType(); - Element ntDef = new Element(XML_NODETYPE); - ntDef.setAttribute(ATTR_NAME, nt.getName()); - ntDef.setAttribute(ATTR_ISMIXIN, Boolean.toString(nt.isMixin())); - ntDef.setAttribute(ATTR_HASORDERABLECHILDNODES, Boolean.toString(nt.hasOrderableChildNodes())); - - // declared supertypes - NodeType[] snts = nt.getDeclaredSupertypes(); - Element supertypes = new Element(XML_SUPERTYPES); - for (int i = 0; i < snts.length; i++) { - supertypes.addContent(new Element(XML_SUPERTYPE).setText(snts[i].getName())); - } - ntDef.addContent(supertypes); - - // declared childnode defs - NodeDef[] cnd = nt.getChildNodeDefs(); - for (int i = 0; i < cnd.length; i++) { - if (cnd[i].getDeclaringNodeType().getName().equals(nt.getName())) { - ntDef.addContent(NodeDefImpl.create(cnd[i]).toXml()); - } - } - - // declared propertyDefs - PropertyDef[] pd = nt.getPropertyDefs(); - for (int i = 0; i < pd.length; i++) { - if (pd[i].getDeclaringNodeType().getName().equals(nt.getName())) { - ntDef.addContent(PropertyDefImpl.create(pd[i]).toXml()); - } - } - - String primaryItemName = nt.getPrimaryItemName(); - if (primaryItemName != null) { - ntDef.setAttribute(ATTR_PRIMARYITEMNAME, primaryItemName); - } - report.addContent(ntDef); - } - - Document reportDoc = new Document(report); - return reportDoc; - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } - - /** - * Parse the Xml element in the info object an return an interator over - * the specified node types. - * - * @return - * @throws RepositoryException - * @throws DavException - */ - private NodeTypeIterator getNodeTypes() throws RepositoryException, DavException { - NodeTypeIterator ntIter = null; - Iterator it = info.getReportElement().getChildren().iterator(); - while (it.hasNext() && ntIter == null) { - Element elem = (Element) it.next(); - if (elem.getNamespace().equals(NAMESPACE)) { - String name = elem.getName(); - if (XML_REPORT_ALLNODETYPES.equals(name)) { - ntIter = ntMgr.getAllNodeTypes(); - } else if (XML_REPORT_MIXINNODETYPES.equals(name)) { - ntIter = ntMgr.getMixinNodeTypes(); - } else if (XML_REPORT_PRIMARYNODETYPES.equals(name)) { - ntIter = ntMgr.getPrimaryNodeTypes(); - } - } - } - // None of the simple types. test if a report for individual nodetypes - // was request. If not, the request body is not valid. - if (ntIter == null) { - List ntList = new ArrayList(); - List elemList = info.getReportElement().getChildren(XML_NODETYPE, NAMESPACE); - if (elemList.isEmpty()) { - // throw exception if the request body does not contain a single jcr:nodetype element - throw new DavException(DavServletResponse.SC_BAD_REQUEST, "NodeTypes report: request body has invalid format."); - } - Iterator elemIter = elemList.iterator(); - while (elemIter.hasNext()) { - String nodetypeName = ((Element)elemIter.next()).getChildText(XML_NODETYPENAME, NAMESPACE); - if (nodetypeName != null) { - ntList.add(ntMgr.getNodeType(nodetypeName)); - } - } - ntIter = new IteratorHelper(Collections.unmodifiableCollection(ntList)); - } - - return ntIter; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/RegisteredNamespacesReport.java b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/RegisteredNamespacesReport.java deleted file mode 100644 index 0ed8fee63f4..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/RegisteredNamespacesReport.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.spi.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.version.report.*; -import org.apache.jackrabbit.webdav.version.DeltaVResource; -import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.DavServletResponse; -import org.apache.jackrabbit.webdav.DavSession; -import org.apache.jackrabbit.webdav.spi.JcrDavException; -import org.apache.jackrabbit.webdav.spi.ItemResourceConstants; -import org.jdom.Document; -import org.jdom.Element; - -import javax.jcr.*; -import java.util.ArrayList; -import java.util.List; - -/** - * NodeTypesReport allows to retrieve the definition of a single - * or multiple node types. The request body must be a 'jcr:nodetypes' element: - *
- * <!ELEMENT nodetypes ( nodetype+ | all-nodetypes | mixin-nodetypes | primary-nodetypes ) >
- *
- * <!ELEMENT nodetype ( nodetype-name ) >
- * <!ELEMENT nodetype-name (#PCDATA) >
- *
- * <!ELEMENT all-nodetypes EMPTY >
- * <!ELEMENT mixin-nodetypes EMPTY >
- * <!ELEMENT primary-nodetypes EMPTY >
- * 
- */ -public class RegisteredNamespacesReport implements Report, ItemResourceConstants { - - private static Logger log = Logger.getLogger(RegisteredNamespacesReport.class); - - /** - * The registered type of this report. - */ - public static final ReportType REGISTERED_NAMESPACES_REPORT = ReportType.register("registerednamespaces", ItemResourceConstants.NAMESPACE, RegisteredNamespacesReport.class); - - private NamespaceRegistry nsReg; - private ReportInfo info; - - /** - * Returns {@link #REGISTERED_NAMESPACES_REPORT} type. - * @return {@link #REGISTERED_NAMESPACES_REPORT} - * @see org.apache.jackrabbit.webdav.version.report.Report#getType() - */ - public ReportType getType() { - return REGISTERED_NAMESPACES_REPORT; - } - - /** - * @param resource - * @throws IllegalArgumentException if the resource or the session retrieved - * from the specified resource is null - * @see org.apache.jackrabbit.webdav.version.report.Report#setResource(org.apache.jackrabbit.webdav.version.DeltaVResource) - */ - public void setResource(DeltaVResource resource) { - if (resource == null) { - throw new IllegalArgumentException("Resource must not be null."); - } - try { - DavSession session = resource.getSession(); - if (session == null || session.getRepositorySession() == null) { - throw new IllegalArgumentException("The resource must provide a non-null session object in order to create the jcr:nodetypes report."); - } - nsReg = session.getRepositorySession().getWorkspace().getNamespaceRegistry(); - } catch (RepositoryException e) { - log.error(e.getMessage()); - } - } - - /** - * @param info - * @throws IllegalArgumentException if the specified info does not contain - * a jcr:nodetypes element. - * @see org.apache.jackrabbit.webdav.version.report.Report#setInfo(org.apache.jackrabbit.webdav.version.report.ReportInfo) - */ - public void setInfo(ReportInfo info) { - if (info == null || !"registerednamespaces".equals(info.getReportElement().getName())) { - throw new IllegalArgumentException("jcr:registerednamespaces element expected."); - } - this.info = info; - } - - /** - * Returns a Xml representation of the node type definition(s) according - * to the info object. - * - * @return Xml representation of the node type definition(s) - * @throws org.apache.jackrabbit.webdav.DavException if the specified nodetypes are not known or if another - * error occurs while retrieving the nodetype definitions. - * @see org.apache.jackrabbit.webdav.version.report.Report#toXml() - */ - public Document toXml() throws DavException { - if (info == null || nsReg == null) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while running jcr:registerednamespaces report"); - } - try { - String[] prefixes = nsReg.getPrefixes(); - List namespaceList = new ArrayList(); - for (int i = 0; i < prefixes.length; i++) { - Element elem = new Element(XML_NAMESPACE, NAMESPACE); - elem.addContent(new Element(XML_NSPREFIX, NAMESPACE).setText(prefixes[i])); - elem.addContent(new Element(XML_NSURI, NAMESPACE).setText(nsReg.getURI(prefixes[i]))); - namespaceList.add(elem); - } - Element report = new Element("registerednamespaces-report", NAMESPACE).addContent(namespaceList); - Document reportDoc = new Document(report); - return reportDoc; - } catch (RepositoryException e) { - throw new JcrDavException(e); - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/package.html b/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/package.html deleted file mode 100644 index 92f14280488..00000000000 --- a/contrib/jcr-server/server/src/java/org/apache/jackrabbit/webdav/spi/version/report/package.html +++ /dev/null @@ -1,3 +0,0 @@ - -Contains JCR specific reports. - \ No newline at end of file diff --git a/contrib/jcr-server/todo.txt b/contrib/jcr-server/todo.txt deleted file mode 100644 index 46114e6ddf5..00000000000 --- a/contrib/jcr-server/todo.txt +++ /dev/null @@ -1,90 +0,0 @@ -------------------------------------------------------------------- -todo webdav package -------------------------------------------------------------------- - -- usage of jdom.... - -------------------------------------------------------------------- -todo webdav/version package -------------------------------------------------------------------- - -- review: compliance to deltaV -- reflecting feature-sets -- baseline/activity not respected yet. - -------------------------------------------------------------------- -todo webdav/transaction package -------------------------------------------------------------------- - -- review naming of the lock scopes. 'global','local' are not correct in - this context. -- repository transactions ('global') are only possible with jackrabbit, where - the session represents the XAResource itself. - since j2ee explicitely requires any usertransaction to be completed - upon the end of the servletes service method. - general review necessary.... - -------------------------------------------------------------------- -todo webdav/search package -------------------------------------------------------------------- - -- SearchResource should extend DavResource -- basicquery as defined by the internet draft not respected - currently. - -------------------------------------------------------------------- -todo spi / servlets -------------------------------------------------------------------- - -general - -- undo incomplete changes in case of exception -- review GET/PUT for JCR properties -- etag property on resources -- use strong etag for comparison in ifHeader! -- multistatus fuer lock, copy, move, delete, proppatch wherever required. -- DAV:supported-live-property-set -- timeout: remove expired locks/subscriptions -- improve definition methods/compliance-class -- methods/compliance-class auf der root resoure vs * -- OPTIONS to *-request-uri (according to RFC 2616) - -ordering - -- respect Position header with creation of new collection members by - PUT, COPY, MKCOL requests - -lock - -- implement session-scoped locks. this includes: - > uncommenting supported-locks entry - > build caching mechanism for session in case of session-scoped locks. - > retrieval of cached sessions (currently not possible from IfHeader). - > open issue in JCR: scope of lock cannot be retrieved. - -- JCR lock-token currently not checked for compliance with RFC2518. If the - token is modified accordingly, setting the lock-token to the subsequent - session (currently in the WebdavRequestImpl) must be aware of that change.... - -- transaction locks - - lock returned upon lock-discovery - - remove after timeout (>> releasing cached sessions) - - define reasonable timeout or make timeout configurable - - createLock must respect existing locks in the subtree, for lock is always deep. - -observation - -- make sure all expired subscriptions are removed. -- subscription: reasonable default/max timeout make it configurable... - -versioning - -- VersionItemResource. review regarding definition of a version resource - rfc3253: 'A "version resource", or simply "version", is a resource - that contains a copy of a particular state (content and dead properties) of a - version-controlled resource. A version is created by "checking in" a checked-out - resource. The server allocates a distinct new URL for each new version, and - this URL will never be used to identify any resource other than that version. - The content and dead properties of a version never change.' - -- Additional VERSION-CONTROL Semantics with workspace not implemented. diff --git a/contrib/jcr-server/webapp/maven.xml b/contrib/jcr-server/webapp/maven.xml deleted file mode 100644 index 85eef13d69c..00000000000 --- a/contrib/jcr-server/webapp/maven.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/contrib/jcr-server/webapp/project.properties b/contrib/jcr-server/webapp/project.properties deleted file mode 100644 index 4f50324c147..00000000000 --- a/contrib/jcr-server/webapp/project.properties +++ /dev/null @@ -1,3 +0,0 @@ -maven.javadoc.links=http://java.sun.com/j2se/1.4.2/docs/api/,http://www.day.com/maven/jsr170/javadocs/jcr-0.16.1-pfd/ -maven.repo.remote = http://www.ibiblio.org/maven/,http://www.day.com/maven/ -maven.multiproject.type = war \ No newline at end of file diff --git a/contrib/jcr-server/webapp/project.xml b/contrib/jcr-server/webapp/project.xml deleted file mode 100644 index 1c3ce5d6a51..00000000000 --- a/contrib/jcr-server/webapp/project.xml +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - - ${basedir}/../project.xml - jackrabbit-server - jcr-server - war - Jackrabbit-Server WebApplication - - - - - - - jcr-server - jcr-server - ${pom.currentVersion} - - true - - - - jcr-webdav - jcr-server - ${pom.currentVersion} - - true - - - - jcr-client - jcr-server - ${pom.currentVersion} - - true - - - - - jsr170 - jcr - 0.16.2 - http://www.day.com/maven/jsr170/jars/jcr-0.16.2.jar - - true - - - - jackrabbit - 0.16.2-dev - - true - - - - jdom - 1.0 - - true - - - - log4j - 1.2.8 - - true - - - - junit - 3.8.1 - - - jcr-rmi - 0.16.2 - - false - - - - - concurrent - 1.3.4 - - true - - - - commons-collections - 2.1 - - true - - - - geronimo-spec - geronimo-spec-jta - 1.0-M1 - - true - - - - lucene - lucene - 1.4.3 - - true - - - - xerces - xercesImpl - 2.6.2 - - true - - - - - cqfs - cqfs-jackrabbit - 3.5.6 - http://www.day.com/maven/cqfs/jars/cqfs-jackrabbit-3.5.6.jar - - true - - - - cqfs - cqfs - 3.5.6 - http://www.day.com/maven/cqfs/jars/cqfs-3.5.6.jar - - true - - - - commons-logging - 1.0 - - true - - - - - servletapi - 2.3 - - - - - - - - - ${basedir}/src/java - - - src/java - - **/*.xml - **/*.xsd - **/*.properties - **/*.dtd - - - - - - diff --git a/contrib/jcr-server/webapp/src/webapp/WEB-INF/repository/log4j.properties b/contrib/jcr-server/webapp/src/webapp/WEB-INF/repository/log4j.properties deleted file mode 100644 index 2728ce9be0d..00000000000 --- a/contrib/jcr-server/webapp/src/webapp/WEB-INF/repository/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -# Set root logger level to DEBUG and its only appender to A1. -log4j.rootLogger=INFO, stdout -#log4j.rootLogger=DEBUG, stdout, file -#log4j.rootLogger=ERROR, stdout, file - -# 'stdout' is set to be a ConsoleAppender. -log4j.appender.stdout=org.apache.log4j.ConsoleAppender - -# 'stdout' uses PatternLayout -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n - -# 'file' is set to be a FileAppender. -log4j.appender.file=org.apache.log4j.FileAppender -log4j.appender.file.File=jcr.log - -# 'file' uses PatternLayout. -log4j.appender.file.layout=org.apache.log4j.PatternLayout -log4j.appender.file.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n - diff --git a/contrib/jcr-server/webapp/src/webapp/WEB-INF/repository/repository.xml b/contrib/jcr-server/webapp/src/webapp/WEB-INF/repository/repository.xml deleted file mode 100644 index 1d8d7b97ceb..00000000000 --- a/contrib/jcr-server/webapp/src/webapp/WEB-INF/repository/repository.xml +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/jcr-server/webapp/src/webapp/WEB-INF/web.xml b/contrib/jcr-server/webapp/src/webapp/WEB-INF/web.xml deleted file mode 100644 index e93b92a5eb1..00000000000 --- a/contrib/jcr-server/webapp/src/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,169 +0,0 @@ - - - - - Jackrabbit JCR Server - - - - - - RepositoryStartup - - Repository servlet that starts the repository and registers it to JNDI ans RMI. - If you already have the repository registered in this appservers JNDI context, - or if its accessible via RMI, you do not need to use this servlet. - - org.apache.jackrabbit.server.RepositoryStartupServlet - - - log4j-config - /WEB-INF/repository/log4j.properties - initial log4j configuration - - - - repository-config - /WEB-INF/repository/repository.xml - the repository config location - - - - repository-home - jackrabbit/repository - the repository home - - - - repository-name - jackrabbit.repository - Repository Name under which the repository is registered via JNDI/RMI - - - - rmi-port - 0 - - The RMI port for registering the repository in the RMI Registry. - If equals 0, the default port is used. Omit this parameter, to - disable RMI server completely. - - - - - - java.naming.provider.url - http://www.apache.org/jackrabbit - - - java.naming.factory.initial - org.apache.jackrabbit.core.jndi.provider.DummyInitialContextFactory - - - 1 - - - - - - - - Repository - - This servlet provides other servlets and jsps a common way to access - the repository. The repository can be accessed via JNDI, RMI or Webdav. - - org.apache.jackrabbit.client.RepositoryAccessServlet - - - log4j-config - /WEB-INF/repository/log4j.properties - initial log4j configuration - - - - repository-name - jackrabbit.repository - Repository Name that is used to retrieve it via JNDI - - - - - java.naming.provider.url - http://www.apache.org/jackrabbit - - - java.naming.factory.initial - org.apache.jackrabbit.core.jndi.provider.DummyInitialContextFactory - - - - - rmi-uri - ///jackrabbit.repository - The URI for the RMI connection. - - - 2 - - - - - - - Webdav - - The webdav servlet that connects HTTP request to the repository. - - org.apache.jackrabbit.server.simple.WebdavServlet - - - resource-path-prefix - /repository - - defines the prefix for spooling resources out of the repository. - - - 3 - - - - - - - JCRWebdavServer - - The webdav servlet that connects HTTP request to the repository. - - org.apache.jackrabbit.server.JCRWebdavServerServlet - - - resource-path-prefix - /server - - defines the prefix for spooling resources out of the repository. - - - 4 - - - - - - - Webdav - /repository/* - - - JCRWebdavServer - /server/* - - \ No newline at end of file diff --git a/contrib/jcr-server/webapp/src/webapp/index.jsp b/contrib/jcr-server/webapp/src/webapp/index.jsp deleted file mode 100644 index b315999bf8e..00000000000 --- a/contrib/jcr-server/webapp/src/webapp/index.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page import="org.apache.jackrabbit.server.simple.WebdavServlet, - javax.jcr.Repository, - org.apache.jackrabbit.client.RepositoryAccessServlet"%><% -%> - -Jackrabbit Examples - - - -<% - Repository rep = RepositoryAccessServlet.getRepository(); - -%>
Powered by <%= rep.getDescriptor(Repository.REP_NAME_DESC)%> version <%= rep.getDescriptor(Repository.REP_VERSION_DESC) %>. - - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/maven.xml b/contrib/jcr-server/webdav/maven.xml deleted file mode 100644 index ef564ff1229..00000000000 --- a/contrib/jcr-server/webdav/maven.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - diff --git a/contrib/jcr-server/webdav/project.properties b/contrib/jcr-server/webdav/project.properties deleted file mode 100644 index 0569e85c372..00000000000 --- a/contrib/jcr-server/webdav/project.properties +++ /dev/null @@ -1,2 +0,0 @@ -maven.javadoc.links=http://java.sun.com/j2se/1.4.2/docs/api/,http://www.day.com/maven/jsr170/javadocs/jcr-0.16.1-pfd/ -maven.repo.remote = http://www.ibiblio.org/maven/,http://www.day.com/maven/ diff --git a/contrib/jcr-server/webdav/project.xml b/contrib/jcr-server/webdav/project.xml deleted file mode 100644 index 8c77de0bc07..00000000000 --- a/contrib/jcr-server/webdav/project.xml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - ${basedir}/../project.xml - jcr-webdav - jcr-server - jar - JCRWebdavServer Webdav Library - - - - - - - jsr170 - jcr - 0.16.2 - http://www.day.com/maven/jsr170/jars/jcr-0.16.2.jar - - - jackrabbit - 0.16.2-dev - - - jdom - 1.0 - - - log4j - 1.2.8 - - - servletapi - 2.3 - - - jcr-rmi - 0.16.2 - - - - - - - - - - ${basedir}/src/java - - - src/java - - **/*.xml - **/*.xsd - **/*.properties - **/*.dtd - - - - - - diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavConstants.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavConstants.java deleted file mode 100644 index 0017fb855d3..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavConstants.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.jdom.Namespace; - -import java.text.SimpleDateFormat; - -/** - * DavConstants provide constants for request and response - * headers, Xml elements and property names defined by - * RFC 2518. In addition - * common date formats (creation date and modification time) are included. - */ -public interface DavConstants { - - /** - * Request and response headers and some value constants - */ - //-------------------------------------------------------------- Headers --- - public static final String HEADER_DAV = "DAV"; - public static final String HEADER_DESTINATION = "Destination"; - public static final String HEADER_IF = "If"; - public static final String HEADER_AUTHORIZATION = "Authorization"; - - //---------------------------------------------------- Lock-Token header --- - public static final String HEADER_LOCK_TOKEN = "Lock-Token"; - public static final String OPAQUE_LOCK_TOKEN_PREFIX = "opaquelocktoken:"; - - //------------------------------------------------------- Timeout header --- - public static final String HEADER_TIMEOUT = "Timeout"; - public static final String TIMEOUT_INFINITE = "Infinite"; - public static final long INFINITE_TIMEOUT = Long.MAX_VALUE; - public static final long UNDEFINED_TIMEOUT = Long.MIN_VALUE; - - //----------------------------------------------------- Overwrite header --- - public static final String HEADER_OVERWRITE = "Overwrite"; - public static final String NO_OVERWRITE = "T"; - - //--------------------------------------------------------- Depth header --- - public static final String HEADER_DEPTH = "Depth"; - public static final String DEPTH_INFINITY_S = "infinity"; - public static final int DEPTH_INFINITY = Integer.MAX_VALUE; - public static final int DEPTH_0 = 0; - public static final int DEPTH_1 = 1; - - /** - * Default Namespace constant - */ - public static final Namespace NAMESPACE = Namespace.getNamespace("D", "DAV:"); - - /** - * Xml element names used for response and request body - */ - public static final String XML_ALLPROP = "allprop"; - public static final String XML_COLLECTION = "collection"; - public static final String XML_DST = "dst"; - public static final String XML_HREF = "href"; - public static final String XML_KEEPALIVE = "keepalive"; - public static final String XML_LINK = "link"; - public static final String XML_MULTISTATUS = "multistatus"; - public static final String XML_OMIT = "omit"; - public static final String XML_PROP = "prop"; - public static final String XML_PROPERTYBEHAVIOR = "propertybehavior"; - public static final String XML_PROPERTYUPDATE = "propertyupdate"; - public static final String XML_PROPFIND = "propfind"; - public static final String XML_PROPNAME = "propname"; - public static final String XML_PROPSTAT = "propstat"; - public static final String XML_REMOVE = "remove"; - public static final String XML_RESPONSE = "response"; - public static final String XML_RESPONSEDESCRIPTION = "responsedescription"; - public static final String XML_SET = "set"; - public static final String XML_SOURCE = "source"; - public static final String XML_STATUS = "status"; - - /** - * XML element names related to locking - */ - public static final String XML_ACTIVELOCK = "activelock"; - public static final String XML_DEPTH = "depth"; - public static final String XML_LOCKTOKEN = "locktoken"; - public static final String XML_TIMEOUT = "timeout"; - public static final String XML_LOCKSCOPE = "lockscope"; - public static final String XML_EXCLUSIVE = "exclusive"; - public static final String XML_SHARED = "shared"; - public static final String XML_LOCKENTRY = "lockentry"; - public static final String XML_LOCKINFO = "lockinfo"; - public static final String XML_LOCKTYPE = "locktype"; - public static final String XML_WRITE = "write"; - public static final String XML_OWNER = "owner"; - - /** - * Webdav property names as defined by RFC 2518
- * Note: Microsoft webdav clients as well as Webdrive request additional - * property (e.g. href, name, owner, isRootLocation, isCollection) within the - * default namespace, which are are ignored by this implementation, except - * for the 'isCollection' property, needed for XP built-in clients. - */ - public static final String PROPERTY_CREATIONDATE = "creationdate"; - public static final String PROPERTY_DISPLAYNAME = "displayname"; - public static final String PROPERTY_GETCONTENTLANGUAGE = "getcontentlanguage"; - public static final String PROPERTY_GETCONTENTLENGTH = "getcontentlength"; - public static final String PROPERTY_GETCONTENTTYPE = "getcontenttype"; - public static final String PROPERTY_GETETAG = "getetag"; - public static final String PROPERTY_GETLASTMODIFIED = "getlastmodified"; - public static final String PROPERTY_LOCKDISCOVERY = "lockdiscovery"; - public static final String PROPERTY_RESOURCETYPE = "resourcetype"; - public static final String PROPERTY_SOURCE = "source"; - public static final String PROPERTY_SUPPORTEDLOCK = "supportedlock"; - - //--------------------------------------------------- Propfind constants --- - public static final int PROPFIND_BY_PROPERTY = 0; - public static final int PROPFIND_ALL_PROP = 1; - public static final int PROPFIND_PROPERTY_NAMES = 2; - - //--------------------------------------------------------- date formats --- - /** - * modificationDate date format per RFC 1123 - */ - public static SimpleDateFormat modificationDateFormat = - new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); - - /** - * Simple date format for the creation date ISO representation (partial). - */ - public static SimpleDateFormat creationDateFormat = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavException.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavException.java deleted file mode 100644 index 7ebb01220b1..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavException.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import java.util.Properties; -import java.io.IOException; - -/** - * DavException extends the {@link Exception} class in order - * to simplify handling of exceptional situations occuring during processing - * of WebDAV requests and provides possibility to retrieve an Xml representation - * of the error. - */ -public class DavException extends Exception { - - private static Logger log = Logger.getLogger(DavException.class); - private static Properties statusPhrases = new Properties(); - static { - try { - statusPhrases.load(DavException.class.getResourceAsStream("statuscode.properties")); - } catch (IOException e) { - log.error("Failed to load status properties: "+ e.getMessage()); - } - } - - private static final String XML_ERROR = "error"; - - private int errorCode = DavServletResponse.SC_INTERNAL_SERVER_ERROR; - private Element conditionElement; - - /** - * Create a new DavException. - * - * @param errorCode integer specifying any of the status codes defined by - * {@link DavServletResponse}. - * @param message Human readable error message. - */ - public DavException(int errorCode, String message) { - super(message); - this.errorCode = errorCode; - log.debug("DavException: (" + errorCode + ") " + message); - } - - /** - * Create a new DavException. - * - * @param errorCode integer specifying any of the status codes defined by - * {@link DavServletResponse}. - */ - public DavException(int errorCode) { - this(errorCode, statusPhrases.getProperty(String.valueOf(errorCode))); - } - - /** - * Create a new DavException. - * - * @param errorCode integer specifying any of the status codes defined by - * {@link DavServletResponse}. - * @param message - * @param conditionElement - */ - public DavException(int errorCode, String message, Element conditionElement) { - this(errorCode, message); - this.conditionElement = conditionElement; - log.debug("DavException: (" + errorCode + ") " + conditionElement.toString()); - } - - /** - * Return the error code attached to this DavException. - * - * @return errorCode - */ - public int getErrorCode() { - return errorCode; - } - - /** - * Returns the Xml representation of this DavException. In case - * no {@link Element} has been passed to the constructor, an empty DAV:error - * element is returned. - * - * @return Xml representation of this exception. - */ - public Element getError() { - Element error = new Element(XML_ERROR, DavConstants.NAMESPACE); - if (conditionElement != null) { - error.addContent(conditionElement); - } - return error; - } - - /** - * Return the status phrase corresponding to the error code attached to - * this DavException. - * - * @return status phrase corresponding to the error code. - * @see #getErrorCode() - */ - public String getStatusPhrase() { - return getStatusPhrase(errorCode); - } - - /** - * Returns the status phrase for the given error code. - * - * @param errorCode - * @return status phrase corresponding to the given error code. - */ - public static String getStatusPhrase(int errorCode) { - return statusPhrases.getProperty(errorCode+""); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavLocatorFactory.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavLocatorFactory.java deleted file mode 100644 index fd1217045fa..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavLocatorFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -/** - * DavLocatorFactory... - */ -public interface DavLocatorFactory { - - /** - * Create a new DavResourceLocator. - * - * @param prefix - * @param requestHandle - * @return - */ - public DavResourceLocator createResourceLocator(String prefix, String requestHandle); - - /** - * Create a new DavResourceLocator. - * - * @param prefix - * @param workspacePath - * @param resourcePath - * @return - */ - public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavMethods.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavMethods.java deleted file mode 100644 index 2cc0e4ca831..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavMethods.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.log4j.Logger; - -import java.util.HashMap; - -/** - * DavMethods defines constants for the WebDAV METHODS. - */ -public class DavMethods { - - private static Logger log = Logger.getLogger(DavMethods.class); - - /** - * A hashmap of webdav METHODS - */ - private static HashMap methodMap = new HashMap(); - - /** - * An array of method codes that are affected by a Label header - * @see org.apache.jackrabbit.webdav.version.DeltaVConstants#HEADER_LABEL - */ - private static int[] labelMethods; - - /** - * The webdav OPTIONS method and public constant - */ - public static final int DAV_OPTIONS = 1; - public static final String METHOD_OPTIONS = "OPTIONS"; - - /** - * The webdav GET method and public constant - */ - public static final int DAV_GET = DAV_OPTIONS + 1; - public static final String METHOD_GET = "GET"; - - /** - * The webdav HEAD method and public constant - */ - public static final int DAV_HEAD = DAV_GET + 1; - public static final String METHOD_HEAD = "HEAD"; - - - /** - * The webdav POST method and public constant - */ - public static final int DAV_POST = DAV_HEAD + 1; - private static final String METHOD_POST = "POST"; - - - /** The webdav DELETE method and public constant */ - public static final int DAV_DELETE = DAV_POST + 1; - public static final String METHOD_DELETE = "DELETE"; - - - /** The webdav PUT method and public constant */ - public static final int DAV_PUT = DAV_DELETE + 1; - public static final String METHOD_PUT = "PUT"; - - - /** - * The webdav PROPFIND method and public constant as defined by - * RFC 2518. - */ - public static final int DAV_PROPFIND = DAV_PUT + 1; - public static final String METHOD_PROPFIND = "PROPFIND"; - - - /** - * The webdav PROPPATCH method and public constant as defined by - * RFC 2518 - */ - public static final int DAV_PROPPATCH = DAV_PROPFIND + 1; - public static final String METHOD_PROPPATCH = "PROPPATCH"; - - - /** - * The webdav MKCOL (make collection) method and public constant as defined by - * RFC 2518 - */ - public static final int DAV_MKCOL = DAV_PROPPATCH + 1; - public static final String METHOD_MKCOL = "MKCOL"; - - - /** - * The webdav COPY method and public constant as defined by - * RFC 2518 - */ - public static final int DAV_COPY = DAV_MKCOL + 1; - public static final String METHOD_COPY = "COPY"; - - - /** - * The webdav MOVE method and public constant as defined by - * RFC 2518 - */ - public static final int DAV_MOVE = DAV_COPY + 1; - public static final String METHOD_MOVE = "MOVE"; - - - /** - * The webdav LOCK method and public constant as defined by - * RFC 2518 - */ - public static final int DAV_LOCK = DAV_MOVE + 1; - public static final String METHOD_LOCK = "LOCK"; - - - /** - * The webdav UNLOCK method and public constant as defined by - * RFC 2518 - */ - public static final int DAV_UNLOCK = DAV_LOCK + 1; - public static final String METHOD_UNLOCK = "UNLOCK"; - - - /** - * The webdav ORDERPATCH method and public constant - * defined by RFC 3648. - */ - public static final int DAV_ORDERPATCH = DAV_UNLOCK + 1; - public static final String METHOD_ORDERPATCH = "ORDERPATCH"; - - - /** - * The webdav SUBSCRIBE method and public constant.
- * NOTE: This method is not defined by any of the Webdav RFCs - */ - public static final int DAV_SUBSCRIBE = DAV_ORDERPATCH + 1; - public static final String METHOD_SUBSCRIBE = "SUBSCRIBE"; - - - /** - * The webdav UNSUBSCRIBE method and public constant
- * NOTE: This method is not defined by any of the Webdav RFCs - */ - public static final int DAV_UNSUBSCRIBE = DAV_SUBSCRIBE + 1; - public static final String METHOD_UNSUBSCRIBE = "UNSUBSCRIBE"; - - - /** - * The webdav POLL method and public constant
- * NOTE: This method is not defined by any of the Webdav RFCs - */ - public static final int DAV_POLL = DAV_UNSUBSCRIBE + 1; - public static final String METHOD_POLL = "POLL"; - - - /** - * The webdav SEARCH method and public constant as defined by the - * Webdav Search internet draft. - */ - public static final int DAV_SEARCH = DAV_POLL + 1; - public static final String METHOD_SEARCH = "SEARCH"; - - - /** - * The webdav REPORT method and public constant defined by - * RFC 3253 - */ - public static final int DAV_REPORT = DAV_SEARCH + 1; - public static final String METHOD_REPORT = "REPORT"; - - - /** - * The webdav VERSION-CONTROL method and public constant defined by - * RFC 3253 - */ - public static final int DAV_VERSION_CONTROL = DAV_REPORT + 1; - public static final String METHOD_VERSION_CONTROL = "VERSION-CONTROL"; - - /** - * The webdav CHECKIN method and public constant defined by - * RFC 3253 - */ - public static final int DAV_CHECKIN = DAV_VERSION_CONTROL + 1; - public static final String METHOD_CHECKIN = "CHECKIN"; - - /** - * The webdav CHECKOUT method and public constant defined by - * RFC 3253 - */ - public static final int DAV_CHECKOUT = DAV_CHECKIN + 1; - public static final String METHOD_CHECKOUT = "CHECKOUT"; - - /** - * The webdav UNCHECKOUT method and public constant defined by - * RFC 3253 - */ - public static final int DAV_UNCHECKOUT = DAV_CHECKOUT + 1; - public static final String METHOD_UNCHECKOUT = "UNCHECKOUT"; - - /** - * The webdav LABEL method and public constant defined by - * RFC 3253 - */ - public static final int DAV_LABEL = DAV_UNCHECKOUT + 1; - public static final String METHOD_LABEL = "LABEL"; - - /** - * The webdav MERGE method and public constant defined by - * RFC 3253 - */ - public static final int DAV_MERGE = DAV_LABEL + 1; - public static final String METHOD_MERGE = "MERGE"; - - /** - * The webdav UPDATE method and public constant defined by - * RFC 3253 - */ - public static final int DAV_UPDATE = DAV_MERGE + 1; - public static final String METHOD_UPDATE = "UPDATE"; - - /** - * The webdav MKWORKSPACE method and public constant defined by - * RFC 3253 - */ - public static final int DAV_MKWORKSPACE = DAV_UPDATE + 1; - public static final String METHOD_MKWORKSPACE = "MKWORKSPACE"; - - /** - * Returns webdav method type code, error result <= 0 - * Valid type codes > 0 - */ - public static int getMethodCode(String method) { - Integer code = (Integer) methodMap.get(method.toUpperCase()); - if (code != null) { - return code.intValue(); - } - return 0; - } - - /** - * Static intializer for methodTable hashmap - */ - private static void addMethodCode(String method, int code) { - methodMap.put(method, new Integer(code)); - } - - /** - * Webdav Method table - */ - static { - addMethodCode(METHOD_OPTIONS, DAV_OPTIONS); - addMethodCode(METHOD_GET, DAV_GET); - addMethodCode(METHOD_HEAD, DAV_HEAD); - addMethodCode(METHOD_POST, DAV_POST); - addMethodCode(METHOD_PUT, DAV_PUT); - addMethodCode(METHOD_DELETE, DAV_DELETE); - addMethodCode(METHOD_PROPFIND, DAV_PROPFIND); - addMethodCode(METHOD_PROPPATCH, DAV_PROPPATCH); - addMethodCode(METHOD_MKCOL, DAV_MKCOL); - addMethodCode(METHOD_COPY, DAV_COPY); - addMethodCode(METHOD_MOVE, DAV_MOVE); - addMethodCode(METHOD_LOCK, DAV_LOCK); - addMethodCode(METHOD_UNLOCK, DAV_UNLOCK); - addMethodCode(METHOD_ORDERPATCH, DAV_ORDERPATCH); - addMethodCode(METHOD_SUBSCRIBE, DAV_SUBSCRIBE); - addMethodCode(METHOD_UNSUBSCRIBE, DAV_UNSUBSCRIBE); - addMethodCode(METHOD_POLL, DAV_POLL); - addMethodCode(METHOD_SEARCH, DAV_SEARCH); - addMethodCode(METHOD_REPORT, DAV_REPORT); - addMethodCode(METHOD_VERSION_CONTROL, DAV_VERSION_CONTROL); - addMethodCode(METHOD_CHECKIN, DAV_CHECKIN); - addMethodCode(METHOD_CHECKOUT, DAV_CHECKOUT); - addMethodCode(METHOD_UNCHECKOUT, DAV_UNCHECKOUT); - addMethodCode(METHOD_LABEL, DAV_LABEL); - addMethodCode(METHOD_MERGE, DAV_MERGE); - addMethodCode(METHOD_UPDATE, DAV_UPDATE); - addMethodCode(METHOD_MKWORKSPACE, DAV_MKWORKSPACE); - - labelMethods = new int[] { DAV_GET, DAV_HEAD, DAV_OPTIONS, DAV_PROPFIND, - DAV_LABEL, DAV_COPY }; - } - - public static boolean isMethodAffectedByLabel(String method) { - int code = getMethodCode(method); - for (int i = 0; i < labelMethods.length; i++) { - if (code == labelMethods[i]) { - return true; - } - } - return false; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResource.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResource.java deleted file mode 100644 index 6a3c2f084ac..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResource.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.lock.*; - -import java.io.InputStream; - -/** - * DavResource provides standard WebDAV functionality as specified - * by RFC 2518. - */ -public interface DavResource { - - /** - * Constant for WebDAV 1 and 2 compliance class as is represented by this - * resource. - */ - public static final String COMPLIANCE_CLASS = "1, 2"; - - /** - * String constant representing the WebDAV 1 and 2 method set. - */ - public static final String METHODS = "OPTIONS, GET, HEAD, POST, TRACE, PROPFIND, PROPPATCH, COPY, PUT, DELETE, MOVE, LOCK, UNLOCK"; - - /** - * Constant indicating the undefined modification time. - */ - public static final long UNDEFINED_MODIFICATIONTIME = -1; - - /** - * Returns a comma separted list of all compliance classes the given - * resource is fulfilling. - * - * @return compliance classes - */ - public String getComplianceClass(); - - /** - * Returns a comma separated list of all METHODS supported by the given - * resource. - * - * @return METHODS supported by this resource. - */ - public String getSupportedMethods(); - - /** - * Returns true if this webdav resource represents an existing repository item. - * - * @return true, if the resource represents an existing repository item. - */ - public boolean exists(); - - /** - * Returns true if this webdav resource has the resourcetype 'collection'. - * - * @return true if the resource represents a collection resource. - */ - public boolean isCollection(); - - /** - * Returns the display name of this resource. - * - * @return display name. - */ - public String getDisplayName(); - - /** - * Returns the {@link DavResourceLocator locator} object for this webdav resource, - * which encapsulates the information for building the complete 'href'. - * - * @return the locator for this resource. - * @see #getResourcePath() - * @see #getHref() - */ - public DavResourceLocator getLocator(); - - /** - * Returns the path of the hierarchy element defined by this DavResource. - * This method is a shortcut for DavResource.getLocator().getResourcePath(). - * - * @return path of the element defined by this DavResource. - */ - public String getResourcePath(); - - /** - * Returns the absolute href of this resource as returned in the - * multistatus response body. - * - * @return href - */ - public String getHref(); - - /** - * Return the time of the last modification or -1 if the modification time - * could not be retrieved. - * - * @return time of last modification or -1. - */ - public long getModificationTime(); - - /** - * Returns a stream to the resource content in order to respond to a 'GET' - * request. - * - * @return stream to the resource content. - */ - public InputStream getStream(); - - /** - * Returns an array of all {@link DavPropertyName property names} available - * on this resource. - * - * @return an array of property names. - */ - public DavPropertyName[] getPropertyNames(); - - /** - * Return the webdav property with the specified name. - * - * @param name name of the webdav property - * @return the {@link DavProperty} with the given name or null - * if the property does not exist. - */ - public DavProperty getProperty(DavPropertyName name); - - /** - * Returns all webdav properties present on this resource. - * - * @return a {@link DavPropertySet} containing all webdav property - * of this resource. - */ - public DavPropertySet getProperties(); - - /** - * Add/Set the specified property on this resource. - * - * @param property - * @throws DavException if an error occurs - */ - public void setProperty(DavProperty property) throws DavException; - - /** - * Remove the specified property from this resource. - * - * @param propertyName - * @throws DavException if an error occurs - */ - public void removeProperty(DavPropertyName propertyName) throws DavException; - - /** - * Retrieve the resource this resource is internal member of. - * - * @return resource this resource is an internal member of. In case this resource - * is the root null is returned. - */ - public DavResource getCollection(); - - /** - * Add the given resource as an internal member to this resource. - * - * @param resource {@link DavResource} to be added as internal member. - * @param in {@link java.io.InputStream} providing the content for the - * internal member. - * @throws DavException - */ - public void addMember(DavResource resource, InputStream in) - throws DavException; - - /** - * Add the given resource as an internal member to this resource. - * - * @param resource webdav resource to be added as member. - * @throws DavException - */ - public void addMember(DavResource resource) throws DavException; - - /** - * Returns an iterator over all internal members. - * - * @return a {@link DavResourceIterator) over all internal members. - */ - public DavResourceIterator getMembers(); - - /** - * Removes the specified member from this resource. - * - * @throws DavException - */ - public void removeMember(DavResource member) throws DavException; - - /** - * Move this DavResource to the given destination resource - * - * @param destination - * @throws DavException - */ - public void move(DavResource destination) throws DavException; - - /** - * Copy this DavResource to the given destination resource - * - * @param destination - * @param shallow - * @throws DavException - */ - public void copy(DavResource destination, boolean shallow) throws DavException; - - /** - * Returns true, if the this resource allows locking. NOTE, that this method - * does not define, whether a lock/unlock can be successfully executed. - * - * @return true, if this resource supports any locking. - * @param type - * @param scope - */ - public boolean isLockable(Type type, Scope scope); - - /** - * Returns true if a lock applies to this resource. This may be either a - * lock on this resource itself or a deep lock inherited from a collection - * above this resource.
- * Note, that true is returned whenever a lock applies to that resource even - * if the lock is expired or not effective due to the fact that the request - * provides the proper lock token. - * - * @return true if a lock applies to this resource. - * @param type - */ - public boolean hasLock(Type type, Scope scope); - - /** - * Return the lock present on this webdav resource or null - * if the resource is either not locked or not lockable at all. Note, that - * a resource may have a lock that is inherited by a deep lock inforced on - * one of its 'parent' resources. - * - * @return lock information of this resource or null if this - * resource has no lock applying it. If an error occurs while retrieving the - * lock information null is returned as well. - * @param type - */ - public ActiveLock getLock(Type type, Scope scope) ; - - /** - * Returns an array of all locks applied to the given resource. - * - * @return array of locks. The array is empty if there are no locks applied - * to this resource. - */ - public ActiveLock[] getLocks(); - - /** - * Lock this webdav resource with the information retrieve from the request - * and return the resulting lockdiscovery object. - * - * @param reqLockInfo lock info as retrieved from the request. - * @return lockdiscovery object to be returned in the response. If the lock - * could not be obtained a DavException is thrown. - * @throws DavException if the lock could not be obtained. - */ - public ActiveLock lock(LockInfo reqLockInfo) throws DavException; - - /** - * Refresh an existing lock by resetting the timeout. - * - * @param reqLockInfo lock info as retrieved from the request. - * @param lockToken identifying the lock to be refreshed. - * @return lockdiscovery object to be returned in the response body. If the lock - * could not be refreshed a DavException is thrown. - * @throws DavException if the lock could not be refreshed. - */ - public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) throws DavException; - - /** - * Remove the lock indentified by the included lock token from this resource. - * This method will return false if the unlocking did not succeed. - * - * @param lockToken indentifying the lock to be removed. - * @throws DavException if the lock could not be removed. - */ - public void unlock(String lockToken) throws DavException; - - /** - * Add an external {@link LockManager} to this resource. This method may - * throw {@link UnsupportedOperationException} if the resource does handle - * locking itself. - * - * @param lockmgr - * @see LockManager - */ - public void addLockManager(LockManager lockmgr); - - /** - * Return the DavResourceFactory that created this resource. - * - * @return the factory that created this resource. - */ - public DavResourceFactory getFactory(); -} - diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceFactory.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceFactory.java deleted file mode 100644 index 74f43b9bd16..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceFactory.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -/** - * DavResourceFactory interface defines a single method for creating - * {@link DavResource} objects. - */ -public interface DavResourceFactory { - - /** - * Create a {@link DavResource} object from the given locator, request and response - * objects. - * - * @param locator locator of the resource - * @param request - * @param response - * @return a new DavResource object. - * @throws DavException - */ - public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException; - - /** - * Create a new {@link DavResource} object from the given locator and session. - * - * @param locator - * @param session - * @return a new DavResource object. - * @throws DavException - */ - public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceIterator.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceIterator.java deleted file mode 100644 index 2d337b8816e..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceIterator.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import java.util.Iterator; - -/** - * DavResourceIterator extends the Iterator interface. Additional - * METHODS allow to retrieve the next {@link DavResource} from the iterator - * and the iterators size. - */ -public interface DavResourceIterator extends Iterator { - - /** - * Returns the next {@link DavResource} in the iterator - * @return the next {@link DavResource} - */ - public DavResource nextResource(); - - /** - * Return the number of {@link DavResource}s in the iterator. - * @return number of elements in the iterator. - */ - public int size(); -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceIteratorImpl.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceIteratorImpl.java deleted file mode 100644 index d99edb031fa..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceIteratorImpl.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.log4j.Logger; - -import java.util.Iterator; -import java.util.List; - -/** - * DavResourceIteratorImpl implementation of the {@link DavResourceIterator} - * interface.
- * NOTE: {@link #remove()} is not implemented. - */ -public class DavResourceIteratorImpl implements DavResourceIterator { - - private static Logger log = Logger.getLogger(DavResourceIteratorImpl.class); - - private Iterator it; - private int size; - - /** - * Create a new DavResourceIterator from the specified list. - * @param list - */ - public DavResourceIteratorImpl(List list) { - it = list.iterator(); - size = list.size(); - } - - /** - * @see DavResourceIterator#hasNext() - */ - public boolean hasNext() { - return it.hasNext(); - } - - /** - * @see DavResourceIterator#next() - */ - public Object next() { - return it.next(); - } - - /** - * @see DavResourceIterator#nextResource() - */ - public DavResource nextResource() { - return (DavResource) next(); - } - - /** - * Returns the size of the initial list. - * - * @see DavResourceIterator#size() - */ - public int size() { - return size; - } - - /** - * @see DavResourceIterator#remove() - */ - public void remove() { - throw new UnsupportedOperationException("Remove not allowed with DavResourceIteratorImpl"); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceLocator.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceLocator.java deleted file mode 100644 index a62bd00a500..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavResourceLocator.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -/** - * DavResourceLocator... - */ -public interface DavResourceLocator { - - /** - * Return the prefix used to build the complete href of the resource as - * required for the {@link DavConstants#XML_HREF href Xml} element. - * This includes scheme and host information as well as constant prefixes. - * However, this must not include workspace prefix. - * - * @return prefix needed in order to build the href from a resource path. - * @see #getResourcePath() - */ - public String getPrefix(); - - /** - * Return the resource path. - * - * @return resource path - */ - public String getResourcePath(); - - /** - * Return the path of the workspace the resource identified by this - * locator is member of. - * - * @return path of the workspace - */ - public String getWorkspacePath(); - - /** - * Return the name of the workspace the resource identified by this - * locator is member of. - * - * @return workspace name - */ - public String getWorkspaceName(); - - /** - * Returns true if the specified locator refers to a resource within the - * same workspace. - * - * @param locator - * @return true if both paths are in the same workspace. - */ - public boolean isSameWorkspace(DavResourceLocator locator); - - /** - * Returns true if the specified workspace name equals to the workspace - * name defined with this locator. - * - * @param workspaceName - * @return true if workspace names are equal. - */ - public boolean isSameWorkspace(String workspaceName); - - /** - * Return the 'href' representation of this locator object. - * - * @param isCollection - * @return 'href' representation of this path - * @see DavConstants#XML_HREF - * @see DavResource#getHref() - */ - public String getHref(boolean isCollection); - - /** - * Returns true if this DavResourceLocator represents the root - * locator that would be requested with 'hrefPrefix'+'pathPrefix' with or - * without a trailing '/'. - * - * @return true if this locator object belongs to the root resource. - */ - public boolean isRootLocation(); - - /** - * Return the locator factory that created this locator. - * - * @return the locator factory - */ - public DavLocatorFactory getFactory(); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavServletRequest.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavServletRequest.java deleted file mode 100644 index 5223d4e49f1..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavServletRequest.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.jackrabbit.webdav.property.DavPropertySet; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; -import org.apache.jackrabbit.webdav.lock.LockInfo; -import org.jdom.Document; - -import javax.servlet.http.HttpServletRequest; - -/** - * DavServletRequest extends the HttpServletRequest by Webdav - * specific METHODS. - */ -public interface DavServletRequest extends HttpServletRequest { - - /** - * Sets the DavSession to this request. - * - * @param session - */ - public void setDavSession(DavSession session); - - /** - * Returns the {@link DavSession} created for this request. - * - * @return session for this resource - */ - public DavSession getDavSession(); - - /** - * Return the locator of the requested {@link DavResource resource}. - * - * @return locator of the requested {@link DavResource resource}. - */ - public DavResourceLocator getRequestLocator(); - - /** - * Parse the {@link DavConstants#HEADER_DESTINATION Destination header} - * and return the locator of the corresponding {@link DavResource resource}. - * - * @return locator of the resource specified with the Destination header. - * @see DavConstants#HEADER_DESTINATION - */ - public DavResourceLocator getDestinationLocator(); - - /** - * Returns true if the {@link DavConstants#HEADER_OVERWRITE Overwrite header} - * is set to 'T' thus instructing the server to overwrite the state of a - * non-null destination resource during a COPY or MOVE. A Overwrite header - * value of 'F' will return false. - * - * @return true if the Overwrite header is set to 'T', false if it is set - * to 'F'. - * @see DavConstants#HEADER_OVERWRITE - */ - public boolean isOverwrite(); - - /** - * Return the integer representation of the given {@link DavConstants#HEADER_DEPTH - * Depth header}. 'Infinity' is represented by {@link DavConstants#DEPTH_INFINITY}. - * - * @return integer representation of the {@link DavConstants#HEADER_DEPTH - * Depth header}. - * @see DavConstants#HEADER_DEPTH - */ - public int getDepth(); - - /** - * Returns the integer representation of the {@link DavConstants#HEADER_DEPTH - * Depth header} or the given defaultValue, if the Depth header is missing. - * - * @param defaultValue to be returned if no Depth header is present. - * @return integer representation of the {@link DavConstants#HEADER_DEPTH - * Depth header} or the given defaultValue. - * @see DavConstants#HEADER_DEPTH - */ - public int getDepth(int defaultValue); - - /** - * Returns the token present in the {@link DavConstants#HEADER_LOCK_TOKEN - * Lock-Token Header} or null if no such header is available.
- * Note: The 'Lock-Token' header is sent with UNLOCK requests and with - * lock responses only. For any other request that may be affected by a lock - * the 'If' header field is responsible. - * - * @return the token present in the Lock-Token header. - * @see DavConstants#HEADER_LOCK_TOKEN - */ - public String getLockToken(); - - /** - * Return the timeout requested in the {@link DavConstants#HEADER_TIMEOUT - * Timeout header} as long. The representation of the - * 'Infinite' timeout is left to the implementation. - * - * @return long value representation of the Timeout header. - * @see DavConstants#HEADER_TIMEOUT - * @see DavConstants#TIMEOUT_INFINITE - */ - public long getTimeout(); - - /** - * Parse the Xml request body and return a {@link org.jdom.Document}. - * If the request body can not be parsed null is returned. - * - * @return Document representing the Xml request body or null. - */ - public Document getRequestDocument(); - - /** - * Return the type of PROPFIND request as indicated by the PROPFIND request - * body. - * - * @return type of PROPFIND request - * @see DavConstants#PROPFIND_ALL_PROP - * @see DavConstants#PROPFIND_BY_PROPERTY - * @see DavConstants#PROPFIND_PROPERTY_NAMES - */ - public int getPropFindType(); - - /** - * Return the set of properties the client requested with a PROPFIND request - * or an empty set if the type of PROPFIND request was {@link DavConstants#PROPFIND_ALL_PROP} - * or {@link DavConstants#PROPFIND_PROPERTY_NAMES}. - * - * @return set of properties the client requested with a PROPFIND request - */ - public DavPropertyNameSet getPropFindProperties(); - - /** - * Return the set of properties the client wanted to modify / create with a - * PROPPATCH request, i.e. all entries in the <propertyupdate> element - * of the request body with name <set>. - * - * @return set of properties the client wanted to modify / create with a - * PROPPATCH request. - */ - public DavPropertySet getPropPatchSetProperties(); - - /** - * Return the set of property names the client wanted to remove with a - * PROPPATCH request, i.e. all entries in the <propertyupdate> element - * of the request body with name <remove>.
- * Note, that in constrast to the <set> Xml element, all the XML - * elements in a prop XML element inside of a remove XML element must be - * empty, as only the names of properties to be removed are required. Therefore - * a DavPropertyNameSet is returned and not a DavPropertySet - * - * @return set of property names the client wanted to remove with a - * PROPPATCH request. - */ - public DavPropertyNameSet getPropPatchRemoveProperties(); - - /** - * Return the parsed 'lockinfo' request body, the {@link DavConstants#HEADER_TIMEOUT - * Timeout header} and the {@link DavConstants#HEADER_DEPTH Depth header} - * of a LOCK request as LockInfo object. - * - * @return LockInfo object encapsulating the information - * present in the LOCK request. - * @see DavConstants#HEADER_TIMEOUT - * @see DavConstants#HEADER_DEPTH - * @see DavConstants#XML_LOCKINFO - */ - public LockInfo getLockInfo(); - - /** - * Returns true, if the {@link DavConstants#HEADER_IF If header} present - * with the request matches the given resource. - * - * @param resource - * @return true, if the test is successful, false otherwise. - */ - public boolean matchesIfHeader(DavResource resource); - - /** - * Returns true, if the {@link DavConstants#HEADER_IF If header} present - * with the request matches to the given href, token and eTag. - * - * @param href - * @param token - * @param eTag - * @return true, if the test is successful, false otherwise. - */ - public boolean matchesIfHeader(String href, String token, String eTag); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavServletResponse.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavServletResponse.java deleted file mode 100644 index 5266e4dcd38..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavServletResponse.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.jackrabbit.webdav.lock.ActiveLock; -import org.jdom.Document; - -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * WebdavResponse extends the HttpServletResponse by - * Webdav specific status codes and METHODS. - */ -public interface DavServletResponse extends HttpServletResponse { - - /** - * The 102 (Processing) status code is an interim response used to - * inform the client that the server has accepted the complete request, - * but has not yet completed it. - */ - int SC_PROCESSING = 102; - - /** - * Status code (207) indicating that the response requires - * providing status for multiple independent operations. - */ - int SC_MULTI_STATUS = 207; - - /** - * Status code (422) indicating the entity body submitted with - * the PATCH method was not understood by the resource. - */ - int SC_UNPROCESSABLE_ENTITY = 422; - - /** - * Status code (423) indicating the destination resource of a - * method is locked, and either the request did not contain a - * valid Lock-Info header, or the Lock-Info header identifies - * a lock held by another principal. - */ - int SC_LOCKED = 423; - - /** - * Status code (424) incidating that the method could not be - * performed on the resource, because the requested action depended - * on another action which failed. - */ - int SC_FAILED_DEPENDENCY = 424; - - /** - * Status code (507) indicating that the resource does not have - * sufficient space to record the state of the resource after the - * execution of this method. - */ - int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 507; - - /** - * Send a response body given more detailed information about the error - * occured. - * - * @param error - * @throws IOException - */ - public void sendErrorResponse(DavException error) throws IOException; - - /** - * Send the multistatus response to the client. A multistatus response - * is returned in response to a successful PROPFIND and PROPPATCH request. - * In addition multistatus response is required response in case a COPY, - * MOVE, DELETE, LOCK or PROPPATCH request fails. - * - * @param multistatus - * @throws IOException - * @see #SC_MULTI_STATUS - */ - public void sendMultiStatusResponse(MultiStatus multistatus) throws IOException; - - /** - * Send the lock response for a successful LOCK request. The given ActiveLock - * object is included in the lockdiscovery property of the response - * body as required by RFC 2518. - * - * @param lock - * @throws IOException - * @see DavConstants#PROPERTY_LOCKDISCOVERY - */ - public void sendLockResponse(ActiveLock lock) throws IOException; - - /** - * Send the lock response for a successful LOCK request, that was intended - * to refresh an existing lock. The locks array must contain at least - * a single element; the ActiveLock objects are then - * included in the lockdiscovery property of the response body as required - * by RFC 2518. - * - * @param locks - * @throws IOException - * @see DavConstants#PROPERTY_LOCKDISCOVERY - */ - public void sendRefreshLockResponse(ActiveLock[] locks) throws IOException; - - /** - * Generic method to return an Xml response body. - * - * @param xmlDoc Xml document representing the response body. - * @param status Status code to be used with {@link #setStatus(int)}. - * @throws IOException - */ - public void sendXmlResponse(Document xmlDoc, int status) throws IOException; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavSession.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavSession.java deleted file mode 100644 index 78d8e672689..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavSession.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import javax.jcr.Session; - -/** - * DavSession wraps a {@link Session repository session} - * object, that is obtained on - * {@link javax.jcr.Repository#login(javax.jcr.Credentials, String) login} to - * the underlaying repository. - */ -public interface DavSession { - - /** - * Adds a reference to this DavSession indicating that - * the underlaying {@link Session} object is needed for actions spanning over - * multiple requests. - * - * @param reference to be added. - */ - public void addReference(Object reference); - - /** - * Releasing a reference to this DavSession. If no more - * references are present, the underlaying {@link Session} may be discarded - * (e.g by calling {@link Session#logout()}. - * - * @param reference to be removed. - */ - public void removeReference(Object reference); - - /** - * Unwrap the {@link Session repository session} object. - * - * @return the session object wrapped by this DavSession - */ - public Session getRepositorySession(); - - /** - * Adds a lock token to this DavSession. - * - * @param token - */ - public void addLockToken(String token); - - /** - * Returns the lock tokens of this DavSession. - * - * @return - */ - public String[] getLockTokens(); - - /** - * Removes a lock token from this DavSession. - * - * @param token - */ - public void removeLockToken(String token); - -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavSessionProvider.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavSessionProvider.java deleted file mode 100644 index 7d02360dcfe..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/DavSessionProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -/** - * DavSessionProvider is an interface for components that - * can initiate and complete {@link DavSession}s. A provider is - * responsible for supplying references from a {@link WebdavRequest} - * to a {@link DavSession} when acquired and removing the references - * when released. - - */ -public interface DavSessionProvider { - - /** - * Acquires a DavSession. Upon success, the WebdavRequest will - * reference that session. - * - * A session will not be available if credentials can not be found - * in the request (meaning that the request has not been - * authenticated). - * - * @param request - * @throws DavException if a problem occurred while obtaining the - * session - */ - public void acquireSession(WebdavRequest request) throws DavException; - - /** - * Releases the reference from the request to the session. - * - * @param request - */ - public void releaseSession(WebdavRequest request); -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/MultiStatus.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/MultiStatus.java deleted file mode 100644 index 861b6a0e525..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/MultiStatus.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.jdom.Document; -import org.jdom.Element; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; - -import java.util.ArrayList; -import java.util.Iterator; - -/** - * MultiStatus representing the content of a multistatus response body and - * allows to retrieve the Xml representation. - */ -public class MultiStatus { - - private ArrayList responses = new ArrayList(); - - /** - * Add response(s) to this multistatus, in order to build a multistatus for - * responding to a PROPFIND request. - * - * @param resource The resource to add property from - * @param propNameSet The requested property names of the PROPFIND request - * @param propFindType - * @param depth - */ - public void addResourceProperties(DavResource resource, DavPropertyNameSet propNameSet, - int propFindType, int depth) { - addResponse(new MultiStatusResponse(resource, propNameSet, propFindType)); - if (depth > 0) { - DavResourceIterator iter = resource.getMembers(); - while (iter.hasNext()) { - addResourceProperties(iter.nextResource(), propNameSet, propFindType, depth-1); - } - } - } - - /** - * Add response(s) to this multistatus, in order to build a multistatus e.g. - * in order to respond to a PROPFIND request. Please note, that in terms - * of PROPFIND, this method would correspond to a - * {@link DavConstants#PROPFIND_BY_PROPERTY} propfind type. - * - * @param resource The resource to add property from - * @param propNameSet The requested property names of the PROPFIND request - * @param depth - * @see #addResourceProperties(DavResource, DavPropertyNameSet, int, int) for - * the corresponding method that allows to specify the type explicitely. - */ - public void addResourceProperties(DavResource resource, DavPropertyNameSet propNameSet, - int depth) { - addResourceProperties(resource, propNameSet, DavConstants.PROPFIND_BY_PROPERTY, depth); - } - - /** - * Add response(s) to this multistatus, in order to build a multistatus - * as returned for failed COPY, MOVE, LOCK or DELETE requests - * - * @param resource - * @param status - * @param depth - */ - public void addResourceStatus(DavResource resource, int status, int depth) { - addResponse(new MultiStatusResponse(resource.getHref(), status)); - if (depth > 0) { - DavResourceIterator iter = resource.getMembers(); - while (iter.hasNext()) { - addResourceStatus(iter.nextResource(), status, depth-1); - } - } - } - - /** - * Add a MultiStatusResponse element to this MultiStatus - * - * @param response - */ - public void addResponse(MultiStatusResponse response) { - responses.add(response); - } - - /** - * Return the Xml representation of this MultiStatus. - * - * @return Xml document - */ - public Document toXml() { - Element multistatus = new Element(DavConstants.XML_MULTISTATUS, DavConstants.NAMESPACE); - Iterator it = responses.iterator(); - while(it.hasNext()) { - multistatus.addContent(((MultiStatusResponse)it.next()).toXml()); - } - return new Document(multistatus); - } -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/MultiStatusResponse.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/MultiStatusResponse.java deleted file mode 100644 index 601cb18cb68..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/MultiStatusResponse.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.jdom.Element; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.util.XmlUtil; - -import java.util.HashMap; -import java.util.Iterator; - -/** - * The WebdavMultistatusResponse class implements a structure that - * hold a WebDAV multistatus response. Properties can be added to this response - * with the respective error/status code. - */ -public class MultiStatusResponse implements DavConstants { - - /** - * The content the 'href' element for this response - */ - private final String href; - - /** - * The '200' set (since this is used very often) - */ - private final Element status200; - - /** - * The '404' set (since this is used very often) - */ - private final Element status404; - - /** - * Hashmap containing all status - */ - private final HashMap statusMap = new HashMap(); - - /** - * An optional response description. - */ - private String responseDescription; - - /** - * Constructs an empty WebDAV multistatus response - */ - public MultiStatusResponse(String href) { - this.href = href; - status200 = new Element(XML_PROP, NAMESPACE); - status404 = new Element(XML_PROP, NAMESPACE); - statusMap.put(new Integer(DavServletResponse.SC_OK), status200); - statusMap.put(new Integer(DavServletResponse.SC_NOT_FOUND), status404); - } - - /** - * Constucts a WebDAV multistatus response and retrieves the resource properties - * according to the given DavPropertyNameSet. - * - * @param resource - * @param propNameSet - */ - public MultiStatusResponse(DavResource resource, DavPropertyNameSet propNameSet) { - this(resource, propNameSet, PROPFIND_BY_PROPERTY); - } - - /** - * Constucts a WebDAV multistatus response and retrieves the resource properties - * according to the given DavPropertyNameSet. It adds all known - * property to the '200' set, while unknown properties are added to the '404' set. - *

- * Note, that the set of property names is ignored in case of a {@link #PROPFIND_ALL_PROP} - * and {@link #PROPFIND_PROPERTY_NAMES} propFindType. - * - * @param resource The resource to retrieve the property from - * @param propNameSet The property name set as obtained from the request body. - * @param propFindType any of the following values: {@link #PROPFIND_ALL_PROP}, - * {@link #PROPFIND_BY_PROPERTY}, {@link #PROPFIND_PROPERTY_NAMES} - */ - public MultiStatusResponse(DavResource resource, DavPropertyNameSet propNameSet, - int propFindType) { - this(resource.getHref()); - - // only property names requested - if (propFindType == PROPFIND_PROPERTY_NAMES) { - DavPropertyName[] propNames = resource.getPropertyNames(); - for (int i = 0; i < propNames.length; i++) { - status200.addContent(propNames[i].toXml()); - } - // all or a specified set of property and their values requested. - } else { - // clone set of property, since several resources could use this again - propNameSet = new DavPropertyNameSet(propNameSet); - // Add requested properties or all non-protected properties - DavPropertyIterator iter = resource.getProperties().iterator(); - while (iter.hasNext()) { - DavProperty wdp = iter.nextProperty(); - if ((propFindType == PROPFIND_ALL_PROP && !wdp.isProtected()) || propNameSet.remove(wdp.getName())) { - status200.addContent(wdp.toXml()); - } - } - - if (propFindType != PROPFIND_ALL_PROP) { - Iterator iter1 = propNameSet.iterator(); - while (iter1.hasNext()) { - DavPropertyName propName = (DavPropertyName) iter1.next(); - status404.addContent(propName.toXml()); - } - } - } - } - - /** - * Constructs an WebDAV multistatus response for a given resource. This - * would be used by COPY, MOVE, DELETE, LOCK, UNLOCK that require a multistatus - * in case of failure. - */ - public MultiStatusResponse(String href, int status) { - this(href); - statusMap.put(new Integer(status), new Element(null)); - } - - /** - * Adds a JDOM element to this response - * - * @param prop the property to add - * @param status the status of the response set to select - */ - private void add(Element prop, int status) { - Integer statusKey = new Integer(status); - Element propsContainer = (Element) statusMap.get(statusKey); - if (propsContainer == null) { - propsContainer = new Element(XML_PROP, NAMESPACE); - statusMap.put(statusKey, propsContainer); - } - propsContainer.addContent(prop); - } - - /** - * Adds a property to this response '200' propstat set. - * - * @param prop the property to add - */ - public void add(DavProperty prop) { - status200.addContent(prop.toXml()); - } - - /** - * Adds a property name to this response '200' propstat set. - * - * @param name the property name to add - */ - public void add(DavPropertyName name) { - status200.addContent(name.toXml()); - } - - /** - * Adds a property to this response - * - * @param prop the property to add - * @param status the status of the response set to select - */ - public void add(DavProperty prop, int status) { - add(prop.toXml(), status); - } - - /** - * Adds a property name to this response - * - * @param name the property name to add - * @param status the status of the response set to select - */ - public void add(DavPropertyName name, int status) { - add(name.toXml(), status); - } - - /** - * Set the content of the optional response description element, which is - * intended to contain a message that can be displayed to the user - * explaining the nature of this response. - * - * @param responseDescription - */ - public void setResponseDescription(String responseDescription) { - this.responseDescription = responseDescription; - } - - /** - * Creates the JDOM element for this reponse. - * - * @return A JDOM element of this response - */ - public Element toXml() { - // don't create empty 'href' responses - if ("".equals(href)) { - return null; - } - - Element response= new Element(XML_RESPONSE, NAMESPACE); - - // add '' - response.addContent(XmlUtil.hrefToXml(href)); - - // add '' elements or a single '' element - Iterator iter = statusMap.keySet().iterator(); - while (iter.hasNext()) { - Integer statusKey = (Integer) iter.next(); - Element prop = (Element) statusMap.get(statusKey); - if (prop != null) { - Element status = new Element(XML_STATUS, NAMESPACE); - status.setText("HTTP/1.1 " + statusKey + " " + DavException.getStatusPhrase(statusKey.intValue())); - - if (XML_PROP.equals(prop.getName())) { - // do not add empty propstat elements - if (prop.getContentSize() > 0) { - Element propstat = new Element(XML_PROPSTAT, NAMESPACE); - propstat.addContent(prop); - propstat.addContent(status); - response.addContent(propstat); - } - } else { - response.addContent(status); - } - } - } - // add the optional '' element - if (responseDescription != null) { - Element desc = new Element(XML_RESPONSEDESCRIPTION, NAMESPACE); - desc.setText(responseDescription); - response.addContent(desc); - } - return response; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavRequest.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavRequest.java deleted file mode 100644 index 9e5bba7163d..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavRequest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.jackrabbit.webdav.observation.ObservationDavServletRequest; -import org.apache.jackrabbit.webdav.ordering.OrderingDavServletRequest; -import org.apache.jackrabbit.webdav.transaction.TransactionDavServletRequest; -import org.apache.jackrabbit.webdav.version.DeltaVServletRequest; - -/** - * The empty WebdavRequest interface collects the functionality - * defined by {@link org.apache.jackrabbit.webdav.DavServletRequest} encapsulting - * the core Webdav specification (RFC 2518) as well as the various extensions - * used for observation and transaction support, ordering of collections, search - * and versioning. - */ -public interface WebdavRequest extends DavServletRequest, - ObservationDavServletRequest, OrderingDavServletRequest, - TransactionDavServletRequest, DeltaVServletRequest { -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavRequestImpl.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavRequestImpl.java deleted file mode 100644 index 1f7ec9df5c1..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavRequestImpl.java +++ /dev/null @@ -1,908 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.util.Text; -import org.apache.jackrabbit.webdav.lock.LockInfo; -import org.apache.jackrabbit.webdav.lock.Type; -import org.apache.jackrabbit.webdav.lock.Scope; -import org.apache.jackrabbit.webdav.property.*; -import org.apache.jackrabbit.webdav.transaction.*; -import org.apache.jackrabbit.webdav.observation.*; -import org.apache.jackrabbit.webdav.version.*; -import org.apache.jackrabbit.webdav.version.report.ReportInfo; -import org.apache.jackrabbit.webdav.ordering.*; -import org.apache.jackrabbit.webdav.header.DepthHeader; -import org.jdom.input.SAXBuilder; -import org.jdom.JDOMException; -import org.jdom.Document; -import org.jdom.Element; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpSession; -import javax.servlet.ServletInputStream; -import javax.servlet.RequestDispatcher; -import java.io.InputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.io.BufferedReader; -import java.util.*; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.Principal; - -/** - * WebdavRequestImpl... - */ -public class WebdavRequestImpl implements WebdavRequest, DavConstants { - - private static Logger log = Logger.getLogger(WebdavRequestImpl.class); - - private final HttpServletRequest httpRequest; - private final DavLocatorFactory factory; - private final IfHeader ifHeader; - private final String hrefPrefix; - - private DavSession session; - - private int propfindType = PROPFIND_ALL_PROP; - private DavPropertyNameSet propfindProps; - private DavPropertySet proppatchSet; - private DavPropertyNameSet proppatchRemove; - - /** - * Creates a new DavServletRequest with the given parameters. - * - * @param httpRequest - * @param factory - */ - public WebdavRequestImpl(HttpServletRequest httpRequest, DavLocatorFactory factory) { - this.httpRequest = httpRequest; - this.factory = factory; - this.ifHeader = new IfHeader(httpRequest); - - String host = getHeader("Host"); - String scheme = getScheme(); - hrefPrefix = scheme + "://" + host + getContextPath(); - } - - /** - * Sets the session field and adds all lock tokens present with either the - * Lock-Token header or the If header to the given session object. - * - * @param session - * @see DavServletRequest#setDavSession(DavSession) - */ - public void setDavSession(DavSession session) { - this.session = session; - // set lock-tokens from header to the current session - if (session != null && session.getRepositorySession() != null) { - String lt = getLockToken(); - if (lt != null) { - session.addLockToken(lt); - } - // add all token present in the the If header to the session as well. - Iterator it = ifHeader.getAllTokens(); - while (it.hasNext()) { - String ifHeaderToken = (String)it.next(); - session.addLockToken(ifHeaderToken); - } - } - } - - /** - * @see DavServletRequest#getDavSession() - */ - public DavSession getDavSession() { - return session; - } - - /** - * Return a DavResourceLocator representing the request handle. - * - * @return locator of the requested resource - * @see DavServletRequest#getRequestLocator() - */ - public DavResourceLocator getRequestLocator() { - String path = getPathInfo(); - if (path == null) { - path = getServletPath(); - } - return factory.createResourceLocator(hrefPrefix, path); - } - - /** - * Parse the destination header field and return the path of the destination - * resource. - * - * @return path of the destination resource. - * @see #HEADER_DESTINATION - * @see DavServletRequest#getDestinationLocator - */ - public DavResourceLocator getDestinationLocator() { - String destination = httpRequest.getHeader(HEADER_DESTINATION); - if (destination != null) { - try { - URI uri = new URI(destination); - if (uri.getAuthority().equals(httpRequest.getHeader("Host"))) { - destination = Text.unescape(uri.getPath()); - } - } catch (URISyntaxException e) { - log.debug("Destination is path is not a valid URI ("+e.getMessage()+"."); - int pos = destination.lastIndexOf(":"); - if (pos > 0) { - destination = destination.substring(destination.indexOf("/",pos)); - log.debug("Tried to retrieve resource destination path from invalid URI: "+destination); - } - } - - // cut off the context path - String contextPath = httpRequest.getContextPath(); - if (destination.startsWith(contextPath)) { - destination = destination.substring(contextPath.length()); - } - } - return factory.createResourceLocator(hrefPrefix, destination); - } - - /** - * Return true if the overwrite header does not inhibit overwriting. - * - * @return true if the overwrite header requests 'overwriting' - * @see #HEADER_OVERWRITE - * @see DavServletRequest#isOverwrite() - */ - public boolean isOverwrite() { - boolean doOverwrite = true; - String overwriteHeader = httpRequest.getHeader(HEADER_OVERWRITE); - if (overwriteHeader != null && !overwriteHeader.equalsIgnoreCase(NO_OVERWRITE)){ - doOverwrite = false; - } - return doOverwrite; - } - - /** - * @see DavServletRequest#getDepth(int) - */ - public int getDepth(int defaultValue) { - return DepthHeader.parse(httpRequest.getHeader(HEADER_DEPTH), defaultValue).getDepth(); - } - - /** - * @see DavServletRequest#getDepth() - */ - public int getDepth() { - return getDepth(DEPTH_INFINITY); - } - - /** - * Parse the request timeout header and convert the timeout value - * into a long indicating the number of milliseconds until expiration time - * is reached.
- * NOTE: If the requested timeout is 'infinite' {@link Long.MAX_VALUE} - * is returned. - * - * @return milliseconds the lock is requested to live. - * @see DavServletRequest#getTimeout() - */ - public long getTimeout() { - String timeoutStr = httpRequest.getHeader(HEADER_TIMEOUT); - long timeout = UNDEFINED_TIMEOUT; - if (timeoutStr != null && timeoutStr.length() > 0) { - int secondsInd = timeoutStr.indexOf("Second-"); - if (secondsInd >= 0) { - secondsInd += 7; // read over "Second-" - int i = secondsInd; - while (i < timeoutStr.length() && Character.isDigit(timeoutStr.charAt(i))) { - i++; - } - try { - timeout = 1000L * Long.parseLong(timeoutStr.substring(secondsInd, i)); - } catch (NumberFormatException ignore) { - // ignore an let the lock define the default timeout - log.error("Invalid timeout format: "+timeoutStr); - } - } else if (timeoutStr.equalsIgnoreCase(TIMEOUT_INFINITE)) { - timeout = INFINITE_TIMEOUT; - } - } - return timeout; - } - - /** - * Retrive the lock token from the 'Lock-Token' header. - * - * @return String representing the lock token sent in the Lock-Token header. - * @throws IllegalArgumentException If the value has not the correct format. - * @see #HEADER_LOCK_TOKEN - * @see DavServletRequest#getLockToken() - */ - public String getLockToken() { - return getCodedURLHeader(HEADER_LOCK_TOKEN); - } - - /** - * @return Xml document - * @see DavServletRequest#getRequestDocument() - */ - public Document getRequestDocument() { - Document requestDocument = null; - // try to parse the request body - try { - InputStream in = httpRequest.getInputStream(); - if (in != null) { - SAXBuilder builder = new SAXBuilder(false); - requestDocument = builder.build(in); - } - } catch (IOException e) { - log.warn("Error while reading the request body: " + e.getMessage()); - } catch (JDOMException e) { - log.warn("Error while building xml document from request body: " + e.getMessage()); - } - return requestDocument; - } - - /** - * Returns the type of PROPFIND as indicated by the request body. - * - * @return type of the PROPFIND request. Default value is {@link #PROPFIND_ALL_PROP allprops} - * @see DavServletRequest#getPropFindType() - */ - public int getPropFindType() { - if (propfindProps == null) { - parsePropFindRequest(); - } - return propfindType; - } - - /** - * Returns the set of properties requested by the PROPFIND body or an - * empty set if the {@link #getPropFindType type} is either 'allprop' or - * 'propname'. - * - * @return set of properties requested by the PROPFIND body or an empty set. - * @see DavServletRequest#getPropFindProperties() - */ - public DavPropertyNameSet getPropFindProperties() { - if (propfindProps == null) { - parsePropFindRequest(); - } - return propfindProps; - } - - /** - * Parse the propfind request body in order to determine the type of the propfind - * and the set of requested property. - * NOTE: An empty 'propfind' request body will be treated as request for all - * property according to the specification. - */ - private void parsePropFindRequest() { - - propfindProps = new DavPropertyNameSet(); - Document requestDocument = getRequestDocument(); - - // propfind httpRequest with empty body or invalid Xml >> retrieve all property - // TODO: spec requires a 'BAD REQUEST' error code - if (requestDocument == null) { - return; - } - - // propfind httpRequest with invalid body >> treat as if empty body - Element root = requestDocument.getRootElement(); - if (!root.getName().equals(XML_PROPFIND)) { - log.info("PropFind-Request has no tag."); - return; - } - - List childList = root.getChildren(); - for (int i = 0; i < childList.size(); i++) { - Element child = (Element) childList.get(i); - String nodeName = child.getName(); - if (XML_PROP.equals(nodeName)) { - propfindType = PROPFIND_BY_PROPERTY; - propfindProps = new DavPropertyNameSet(child); - break; - } else if (XML_PROPNAME.equals(nodeName)) { - propfindType = PROPFIND_PROPERTY_NAMES; - break; - } else if (XML_ALLPROP.equals(nodeName)) { - propfindType = PROPFIND_ALL_PROP; - break; - } - } - } - - /** - * Return the list of 'set' entries in the PROPPATCH request body. The list - * is empty if the request body could not be parsed or if the request body did - * not contain any 'set' elements. - * - * @return the list of 'set' entries in the PROPPATCH request body - * @see DavServletRequest#getPropPatchSetProperties() - */ - public DavPropertySet getPropPatchSetProperties() { - if (proppatchSet == null) { - parsePropPatchRequest(); - } - return proppatchSet; - } - - /** - * Return the list of 'remove' entries in the PROPPATCH request body. The list - * is empty if the request body could not be parsed or if the request body did - * not contain any 'remove' elements. - * - * @return the list of 'remove' entries in the PROPPATCH request body - * @see DavServletRequest#getPropPatchRemoveProperties() - */ - public DavPropertyNameSet getPropPatchRemoveProperties() { - if (proppatchRemove == null) { - parsePropPatchRequest(); - } - return proppatchRemove; - } - - /** - * Parse the PROPPATCH request body. - */ - private void parsePropPatchRequest() { - - proppatchSet = new DavPropertySet(); - proppatchRemove = new DavPropertyNameSet(); - Document requestDocument = getRequestDocument(); - - if (requestDocument == null) { - return; - } - - Element root = requestDocument.getRootElement(); - if (!root.getName().equals(XML_PROPERTYUPDATE)) { - // we should also check for correct namespace - log.warn("PropPatch-Request has no tag."); - return; - } - - List setList = root.getChildren(XML_SET, NAMESPACE); - if (!setList.isEmpty()) { - Iterator setIter = setList.iterator(); - while (setIter.hasNext()) { - Element propElem = ((Element) setIter.next()).getChild(XML_PROP, NAMESPACE); - Iterator it = propElem.getChildren().iterator(); - while (it.hasNext()) { - Element propertyElem = (Element) it.next(); - proppatchSet.add(DefaultDavProperty.createFromXml(propertyElem)); - } - } - } - - // get properties - List removeList = root.getChildren(XML_REMOVE, NAMESPACE); - if (!removeList.isEmpty()) { - Iterator removeIter = removeList.iterator(); - while (removeIter.hasNext()) { - Element propElem = ((Element) removeIter.next()).getChild(XML_PROP, NAMESPACE); - Iterator it = propElem.getChildren().iterator(); - while (it.hasNext()) { - Element propertyElem = (Element) it.next(); - proppatchRemove.add(DavPropertyName.createFromXml(propertyElem)); - } - } - } - } - - /** - * {@link LockInfo} object encapsulating the information passed with a LOCK - * request if the LOCK request body was valid. If the request body is - * missing a 'refresh lock' request is assumed. The {@link LockInfo} - * then only provides timeout and isDeep property and returns true on - * {@link org.apache.jackrabbit.webdav.lock.LockInfo#isRefreshLock()} - * - * @return lock info object or null if an error occured while - * parsing the request body. - * @see DavServletRequest#getLockInfo() - */ - public LockInfo getLockInfo() { - LockInfo lockInfo = null; - boolean isDeep = (getDepth(DEPTH_INFINITY) == DEPTH_INFINITY); - Document requestDocument = getRequestDocument(); - // check if XML request body is present. It SHOULD have one for - // 'create Lock' request and missing for a 'refresh Lock' request - if (requestDocument != null) { - Element root = requestDocument.getRootElement(); - if (root.getName().equals(XML_LOCKINFO)) { - lockInfo = new LockInfo(root, getTimeout(), isDeep); - } else { - log.debug("Lock-Request has no tag."); - } - } else { - lockInfo = new LockInfo(null, getTimeout(), isDeep); - } - return lockInfo; - } - - /** - * MTest if the if header matches the given resource. The comparison is - * made with the {@link DavResource#getHref() - * resource href} and the token returned from an exclusive write lock present on - * the resource. An empty strong ETag is currently assumed.
- * NOTE: If either the If header or the resource is null or if - * the resource has not applied an exclusive write lock the preconditions are met. - * If in contrast the lock applied to the given resource returns a - * null lock token (e.g. for security reasons) or a lock token - * that does not match, the method will return false. - * - * @param resource Webdav resources being operated on - * @return true if the test is successful and the preconditions for the - * request processing are fulfilled. - * @param resource - * @return - * @see DavServletRequest#matchesIfHeader(DavResource) - * @see IfHeader#matches(String, String, String) - * @see DavResource#hasLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) - * @see org.apache.jackrabbit.webdav.lock.ActiveLock#getToken() - */ - public boolean matchesIfHeader(DavResource resource) { - // no ifheader, no resource or no write lock on resource - // >> preconditions ok so far - if (ifHeader == null || resource == null || !resource.hasLock(Type.WRITE, Scope.EXCLUSIVE)) { - return true; - } - - boolean isMatching = false; - String lockToken = resource.getLock(Type.WRITE, Scope.EXCLUSIVE).getToken(); - if (lockToken != null) { - // TODO: strongETag is missing - isMatching = matchesIfHeader(resource.getHref(), lockToken, ""); - } // else: lockToken is null >> the if-header will not match. - - return isMatching; - } - - /** - * @see DavServletRequest#matchesIfHeader(String, String, String) - * @see IfHeader#matches(String, String, String) - */ - public boolean matchesIfHeader(String href, String token, String eTag) { - return ifHeader.matches(href, token, eTag); - } - - /** - * Retrieve the header with the given header name and parse the CodedURL - * value included. - * - * @param headerName - * @return token present in the CodedURL header or null if - * the header is not present. - */ - private String getCodedURLHeader(String headerName) { - String headerValue = null; - String header = httpRequest.getHeader(headerName); - if (header != null) { - int p1 = header.indexOf('<'); - if (p1<0) { - throw new IllegalArgumentException("Invalid CodedURL header value:"+header); - } - int p2 = header.indexOf('>', p1); - if (p2<0) { - throw new IllegalArgumentException("Invalid CodedURL header value:"+header); - } - headerValue = header.substring(p1+1, p2); - } - return headerValue; - } - - //-----------------------------< TransactionDavServletRequest Interface >--- - /** - * - * @return - * @see org.apache.jackrabbit.webdav.transaction.TransactionDavServletRequest#getTransactionId() - */ - public String getTransactionId() { - return getCodedURLHeader(TransactionConstants.HEADER_TRANSACTIONID); - } - - /** - * - * @return - * @see org.apache.jackrabbit.webdav.transaction.TransactionDavServletRequest#getTransactionInfo() - */ - public TransactionInfo getTransactionInfo() { - Document requestDocument = getRequestDocument(); - if (requestDocument != null) { - try { - return new TransactionInfo(requestDocument.getRootElement()); - } catch (IllegalArgumentException e) { - log.error(e.getMessage()); - } - } - return null; - } - - //-----------------------------< ObservationDavServletRequest Interface >--- - /** - * - * @return - * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletRequest#getSubscriptionId() - */ - public String getSubscriptionId() { - return getCodedURLHeader(ObservationConstants.HEADER_SUBSCRIPTIONID); - } - - /** - * - * @return - * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletRequest#getSubscriptionInfo() - */ - public SubscriptionInfo getSubscriptionInfo() { - Document requestDocument = getRequestDocument(); - if (requestDocument != null) { - Element root = requestDocument.getRootElement(); - if (ObservationConstants.XML_SUBSCRIPTIONINFO.equals(root.getName())) { - int depth = getDepth(DEPTH_0); - return new SubscriptionInfo(root, getTimeout(), depth == DEPTH_INFINITY); - } - } - return null; - } - - //--------------------------------< OrderingDavServletRequest Interface >--- - /** - * - * @return - * @see org.apache.jackrabbit.webdav.ordering.OrderingDavServletRequest#getOrderingType() - */ - public String getOrderingType() { - return getHeader(OrderingConstants.HEADER_ORDERING_TYPE); - } - - /** - * - * @return - * @see org.apache.jackrabbit.webdav.ordering.OrderingDavServletRequest#getPosition() - */ - public Position getPosition() { - String h = getHeader(OrderingConstants.HEADER_POSITION); - Position pos = null; - if (h != null) { - String[] typeNSegment = h.split("\\s"); - if (typeNSegment.length == 2) { - try { - pos = new Position(typeNSegment[0], typeNSegment[1]); - } catch (IllegalArgumentException e) { - log.error("Cannot parse Position header: "+e.getMessage()); - } - } - } - return pos; - } - - /** - * - * @return OrderPatch object representing the orderpatch request - * body or null if the - * @see org.apache.jackrabbit.webdav.ordering.OrderingDavServletRequest#getOrderPatch() - */ - public OrderPatch getOrderPatch() { - OrderPatch op = null; - Document requestDocument = getRequestDocument(); - if (requestDocument != null) { - Element root = requestDocument.getRootElement(); - if (!OrderingConstants.XML_ORDERPATCH.equals(root.getName()) || - root.getChild(OrderingConstants.XML_ORDERING_TYPE) == null) { - log.error("ORDERPATH request body must start with an 'orderpatch' element, which must contain an 'ordering-type' child element."); - return op; - } - - try { - op = new OrderPatch(root); - } catch (IllegalArgumentException e) { - log.error(e.getMessage()); - } - } else { - log.error("Error while building xml document from ORDERPATH request body."); - } - return op; - } - - //-------------------------------------< DeltaVServletRequest interface >--- - /** - * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getLabel() - */ - public String getLabel() { - String label = getHeader(DeltaVConstants.HEADER_LABEL); - if (label != null) { - label = Text.unescape(label); - } - return label; - } - - /** - * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getLabelInfo() - */ - public LabelInfo getLabelInfo() { - LabelInfo lInfo = null; - Document requestDocument = getRequestDocument(); - if (requestDocument != null) { - Element root = requestDocument.getRootElement(); - int depth = getDepth(DEPTH_0); - try { - lInfo = new LabelInfo(root, depth); - } catch (IllegalArgumentException e) { - log.error(e.getMessage()); - } - } - return lInfo; - } - - /** - * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getMergeInfo() - */ - public MergeInfo getMergeInfo() { - MergeInfo mInfo = null; - Document requestDocument = getRequestDocument(); - if (requestDocument != null) { - try { - mInfo = new MergeInfo(requestDocument.getRootElement()); - } catch (IllegalArgumentException e) { - log.error(e.getMessage()); - } - } - return mInfo; - } - - /** - * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getUpdateInfo() - */ - public UpdateInfo getUpdateInfo() { - UpdateInfo uInfo = null; - Document requestDocument = getRequestDocument(); - if (requestDocument != null) { - try { - uInfo = new UpdateInfo(requestDocument.getRootElement()); - } catch (IllegalArgumentException e) { - log.error(e.getMessage()); - } - } - return uInfo; - } - - /** - * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getReportInfo() - */ - public ReportInfo getReportInfo() { - ReportInfo rInfo = null; - Document requestDocument = getRequestDocument(); - if (requestDocument != null) { - rInfo = new ReportInfo(requestDocument.getRootElement(), getDepth(DEPTH_0)); - } - return rInfo; - } - - /** - * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getOptionsInfo() - */ - public OptionsInfo getOptionsInfo() { - OptionsInfo info = null; - Document requestDocument = getRequestDocument(); - if (requestDocument != null) { - info = new OptionsInfo(requestDocument.getRootElement()); - } - return info; - } - - //---------------------------------------< HttpServletRequest interface >--- - public String getAuthType() { - return httpRequest.getAuthType(); - } - - public Cookie[] getCookies() { - return httpRequest.getCookies(); - } - - public long getDateHeader(String s) { - return httpRequest.getDateHeader(s); - } - - public String getHeader(String s) { - return httpRequest.getHeader(s); - } - - public Enumeration getHeaders(String s) { - return httpRequest.getHeaders(s); - } - - public Enumeration getHeaderNames() { - return httpRequest.getHeaderNames(); - } - - public int getIntHeader(String s) { - return httpRequest.getIntHeader(s); - } - - public String getMethod() { - return httpRequest.getMethod(); - } - - public String getPathInfo() { - return httpRequest.getPathInfo(); - } - - public String getPathTranslated() { - return httpRequest.getPathTranslated(); - } - - public String getContextPath() { - return httpRequest.getContextPath(); - } - - public String getQueryString() { - return httpRequest.getQueryString(); - } - - public String getRemoteUser() { - return httpRequest.getRemoteUser(); - } - - public boolean isUserInRole(String s) { - return httpRequest.isUserInRole(s); - } - - public Principal getUserPrincipal() { - return httpRequest.getUserPrincipal(); - } - - public String getRequestedSessionId() { - return httpRequest.getRequestedSessionId(); - } - - public String getRequestURI() { - return httpRequest.getRequestURI(); - } - - public StringBuffer getRequestURL() { - return httpRequest.getRequestURL(); - } - - public String getServletPath() { - return httpRequest.getServletPath(); - } - - public HttpSession getSession(boolean b) { - return httpRequest.getSession(b); - } - - public HttpSession getSession() { - return httpRequest.getSession(); - } - - public boolean isRequestedSessionIdValid() { - return httpRequest.isRequestedSessionIdValid(); - } - - public boolean isRequestedSessionIdFromCookie() { - return httpRequest.isRequestedSessionIdFromCookie(); - } - - public boolean isRequestedSessionIdFromURL() { - return httpRequest.isRequestedSessionIdFromURL(); - } - - public boolean isRequestedSessionIdFromUrl() { - return httpRequest.isRequestedSessionIdFromUrl(); - } - - public Object getAttribute(String s) { - return httpRequest.getAttribute(s); - } - - public Enumeration getAttributeNames() { - return httpRequest.getAttributeNames(); - } - - public String getCharacterEncoding() { - return httpRequest.getCharacterEncoding(); - } - - public void setCharacterEncoding(String s) throws UnsupportedEncodingException { - httpRequest.setCharacterEncoding(s); - } - - public int getContentLength() { - return httpRequest.getContentLength(); - } - - public String getContentType() { - return httpRequest.getContentType(); - } - - public ServletInputStream getInputStream() throws IOException { - return httpRequest.getInputStream(); - } - - public String getParameter(String s) { - return httpRequest.getParameter(s); - } - - public Enumeration getParameterNames() { - return httpRequest.getParameterNames(); - } - - public String[] getParameterValues(String s) { - return httpRequest.getParameterValues(s); - } - - public Map getParameterMap() { - return httpRequest.getParameterMap(); - } - - public String getProtocol() { - return httpRequest.getProtocol(); - } - - public String getScheme() { - return httpRequest.getScheme(); - } - - public String getServerName() { - return httpRequest.getServerName(); - } - - public int getServerPort() { - return httpRequest.getServerPort(); - } - - public BufferedReader getReader() throws IOException { - return httpRequest.getReader(); - } - - public String getRemoteAddr() { - return httpRequest.getRemoteAddr(); - } - - public String getRemoteHost() { - return httpRequest.getRemoteHost(); - } - - public void setAttribute(String s, Object o) { - httpRequest.setAttribute(s, o); - } - - public void removeAttribute(String s) { - httpRequest.removeAttribute(s); - } - - public Locale getLocale() { - return httpRequest.getLocale(); - } - - public Enumeration getLocales() { - return httpRequest.getLocales(); - } - - public boolean isSecure() { - return httpRequest.isSecure(); - } - - public RequestDispatcher getRequestDispatcher(String s) { - return httpRequest.getRequestDispatcher(s); - } - - public String getRealPath(String s) { - return httpRequest.getRealPath(s); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavResponse.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavResponse.java deleted file mode 100644 index 815fb04892c..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.apache.jackrabbit.webdav.observation.ObservationDavServletResponse; - -/** - * The empty WebdavResponse interface collects the functionality - * defined by {@link org.apache.jackrabbit.webdav.DavServletResponse} encapsulting - * for the core WebDAV specification (RFC 2518) as well as the various extensions - * used for observation and transaction support, ordering of collections, search - * and versioning. - */ -public interface WebdavResponse extends DavServletResponse, - ObservationDavServletResponse { -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavResponseImpl.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavResponseImpl.java deleted file mode 100644 index 284e2cfdda8..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/WebdavResponseImpl.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav; - -import org.jdom.Element; -import org.jdom.Document; -import org.jdom.output.XMLOutputter; -import org.jdom.output.Format; -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.lock.*; -import org.apache.jackrabbit.webdav.observation.*; - -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.Cookie; -import javax.servlet.ServletOutputStream; -import java.io.IOException; -import java.io.ByteArrayOutputStream; -import java.io.PrintWriter; -import java.util.Locale; - -/** - * WebdavResponseImpl implements the WebdavResponse interface. - */ -public class WebdavResponseImpl implements WebdavResponse { - - private static Logger log = Logger.getLogger(WebdavResponseImpl.class); - - private HttpServletResponse httpResponse; - - /** - * Create a new WebdavResponse - * - * @param httpResponse - */ - public WebdavResponseImpl(HttpServletResponse httpResponse) { - this.httpResponse = httpResponse; - - /* set cache control headers in order to deal with non-webdav complient - * http1.1 or http1.0 proxies. >> see RFC2518 9.4.5 */ - addHeader("Pragma", "No-cache"); // http1.0 - addHeader("Cache-Control", "no-cache"); // http1.1 - } - - /** - * - * @param exception - * @throws IOException - * @see DavServletResponse#sendErrorResponse(org.apache.jackrabbit.webdav.DavException) - */ - public void sendErrorResponse(DavException exception) throws IOException { - Element errorElem = exception.getError(); - if (errorElem == null || errorElem.getChildren().size() == 0) { - httpResponse.sendError(exception.getErrorCode(), exception.getStatusPhrase()); - } else { - sendXmlResponse(new Document(exception.getError()), exception.getErrorCode()); - } - } - - /** - * Send a multistatus response. - * - * @param multistatus - * @throws IOException - * @see DavServletResponse#sendMultiStatusResponse(org.apache.jackrabbit.webdav.MultiStatus) - */ - public void sendMultiStatusResponse(MultiStatus multistatus) throws IOException { - sendXmlResponse(multistatus.toXml(), SC_MULTI_STATUS); - } - - /** - * Send response body for a lock request intended to create a new lock. - * - * @param lock - * @throws java.io.IOException - * @see DavServletResponse#sendLockResponse(org.apache.jackrabbit.webdav.lock.ActiveLock) - */ - public void sendLockResponse(ActiveLock lock) throws IOException { - httpResponse.setHeader(DavConstants.HEADER_LOCK_TOKEN, "<" + lock.getToken() + ">"); - - Element propElem = new Element(DavConstants.XML_PROP, DavConstants.NAMESPACE); - propElem.addContent(new LockDiscovery(lock).toXml()); - sendXmlResponse(new Document(propElem), SC_OK); - } - - /** - * Send response body for a lock request that was intended to refresh one - * or several locks. - * - * @param locks - * @throws java.io.IOException - * @see DavServletResponse#sendRefreshLockResponse(org.apache.jackrabbit.webdav.lock.ActiveLock[]) - */ - public void sendRefreshLockResponse(ActiveLock[] locks) throws IOException { - Element propElem = new Element(DavConstants.XML_PROP, DavConstants.NAMESPACE); - propElem.addContent(new LockDiscovery(locks).toXml()); - sendXmlResponse(new Document(propElem), SC_OK); - } - - /** - * Send Xml response body. - * - * @param xmlDoc - * @param status - * @throws IOException - * @see DavServletResponse#sendXmlResponse(Document, int); - */ - public void sendXmlResponse(Document xmlDoc, int status) throws IOException { - httpResponse.setStatus(status); - if (xmlDoc != null) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - // Write dom tree into byte array output stream - XMLOutputter xmli = new XMLOutputter(Format.getRawFormat()); - xmli.output(xmlDoc, out); - byte[] bytes = out.toByteArray(); - httpResponse.setContentType("text/xml; charset=UTF-8"); - httpResponse.setContentLength(bytes.length); - httpResponse.getOutputStream().write(bytes); - out.close(); - out.flush(); - } - } - - //----------------------------< ObservationDavServletResponse Interface >--- - /** - * - * @param subscription - * @throws IOException - * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletResponse#sendSubscriptionResponse(org.apache.jackrabbit.webdav.observation.Subscription) - */ - public void sendSubscriptionResponse(Subscription subscription) throws IOException { - Element propElem = new Element(DavConstants.XML_PROP, DavConstants.NAMESPACE); - propElem.addContent(new SubscriptionDiscovery(subscription).toXml()); - Document doc = new Document(propElem); - sendXmlResponse(doc, SC_OK); - } - - /** - * - * @param eventDiscovery - * @throws IOException - * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletResponse#sendPollResponse(org.apache.jackrabbit.webdav.observation.EventDiscovery) - */ - public void sendPollResponse(EventDiscovery eventDiscovery) throws IOException { - Document pollDoc = new Document(eventDiscovery.toXml()); - sendXmlResponse(pollDoc, SC_OK); - } - - //--------------------------------------< HttpServletResponse interface >--- - public void addCookie(Cookie cookie) { - httpResponse.addCookie(cookie); - } - - public boolean containsHeader(String s) { - return httpResponse.containsHeader(s); - } - - public String encodeURL(String s) { - return httpResponse.encodeRedirectURL(s); - } - - public String encodeRedirectURL(String s) { - return httpResponse.encodeRedirectURL(s); - } - - public String encodeUrl(String s) { - return httpResponse.encodeUrl(s); - } - - public String encodeRedirectUrl(String s) { - return httpResponse.encodeRedirectURL(s); - } - - public void sendError(int i, String s) throws IOException { - httpResponse.sendError(i, s); - } - - public void sendError(int i) throws IOException { - httpResponse.sendError(i); - } - - public void sendRedirect(String s) throws IOException { - httpResponse.sendRedirect(s); - } - - public void setDateHeader(String s, long l) { - httpResponse.setDateHeader(s, l); - } - - public void addDateHeader(String s, long l) { - httpResponse.addDateHeader(s, l); - } - - public void setHeader(String s, String s1) { - httpResponse.setHeader(s, s1); - } - - public void addHeader(String s, String s1) { - httpResponse.addHeader(s, s1); - } - - public void setIntHeader(String s, int i) { - httpResponse.setIntHeader(s, i); - } - - public void addIntHeader(String s, int i) { - httpResponse.addIntHeader(s, i); - } - - public void setStatus(int i) { - httpResponse.setStatus(i); - } - - public void setStatus(int i, String s) { - httpResponse.setStatus(i, s); - } - - public String getCharacterEncoding() { - return httpResponse.getCharacterEncoding(); - } - - public ServletOutputStream getOutputStream() throws IOException { - return httpResponse.getOutputStream(); - } - - public PrintWriter getWriter() throws IOException { - return httpResponse.getWriter(); - } - - public void setContentLength(int i) { - - } - - public void setContentType(String s) { - httpResponse.setContentType(s); - } - - public void setBufferSize(int i) { - httpResponse.setBufferSize(i); - } - - public int getBufferSize() { - return httpResponse.getBufferSize(); - } - - public void flushBuffer() throws IOException { - httpResponse.flushBuffer(); - } - - public void resetBuffer() { - httpResponse.resetBuffer(); - } - - public boolean isCommitted() { - return httpResponse.isCommitted(); - } - - public void reset() { - httpResponse.reset(); - } - - public void setLocale(Locale locale) { - httpResponse.setLocale(locale); - } - - public Locale getLocale() { - return httpResponse.getLocale(); - } -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/header/DepthHeader.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/header/DepthHeader.java deleted file mode 100644 index ad26d892c8a..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/header/DepthHeader.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2004 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.header; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavConstants; - -/** - * DepthHeader... - */ -public class DepthHeader implements DavConstants { - - private static Logger log = Logger.getLogger(DepthHeader.class); - - private final int depth; - - /** - * Create a new DepthHeader from the given integer. - * - * @param depth - */ - public DepthHeader(int depth) { - if (depth == DavConstants.DEPTH_0 || depth == DavConstants.DEPTH_1 || depth == DavConstants.DEPTH_INFINITY) { - this.depth = depth; - } else { - throw new IllegalArgumentException("Invalid depth: " + depth); - } - } - - /** - * @return integer representation of the depth indicated by the given header. - */ - public int getDepth() { - return depth; - } - - /** - * Return {@link DavConstants#HEADER_DEPTH Depth} - * - * @return {@link DavConstants#HEADER_DEPTH Depth} - * @see DavConstants#HEADER_DEPTH - */ - public String getHeaderName() { - return DavConstants.HEADER_DEPTH; - } - - /** - * Returns the header value. - * - * @return header value - */ - public String getHeaderValue() { - if (depth == DavConstants.DEPTH_0 || depth == DavConstants.DEPTH_1) { - return depth + ""; - } else { - return DavConstants.DEPTH_INFINITY_S; - } - } - - /** - * Parse the given header value or use the defaultValue if the header - * string is empty or null. - * - * @param headerValue - * @param defaultValue - * @return a new DepthHeader - */ - public static DepthHeader parse(String headerValue, int defaultValue) { - if (headerValue == null || "".equals(headerValue)) { - return new DepthHeader(defaultValue); - } else { - return new DepthHeader(depthToInt(headerValue)); - } - } - - /** - * Convert the String depth value to an integer. - * - * @param depth - * @return integer representation of the given depth String - * @throws IllegalArgumentException if the String does not represent a valid - * depth. - */ - private static int depthToInt(String depth) { - int d; - if (depth.equalsIgnoreCase(DavConstants.DEPTH_INFINITY_S)) { - d = DavConstants.DEPTH_INFINITY; - } else if (depth.equals(DavConstants.DEPTH_0+"")) { - d = DavConstants.DEPTH_0; - } else if (depth.equals(DavConstants.DEPTH_1+"")) { - d = DavConstants.DEPTH_1; - } else { - throw new IllegalArgumentException("Invalid depth value: " + depth); - } - return d; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/AbstractActiveLock.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/AbstractActiveLock.java deleted file mode 100644 index 48513015917..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/AbstractActiveLock.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.apache.jackrabbit.webdav.DavConstants; -import org.apache.jackrabbit.webdav.util.XmlUtil; -import org.jdom.Element; - -/** - * AbstractActiveLock... - */ -public abstract class AbstractActiveLock implements ActiveLock, DavConstants { - - /** - * Returns the default Xml representation of the 'activelock' element - * as defined by RFC 2518. - * - * @return Xml representation - */ - public Element toXml() { - Element activeLock = new Element(XML_ACTIVELOCK, NAMESPACE); - - // locktype property - Element property = new Element(XML_LOCKTYPE, NAMESPACE); - property.addContent(getType().toXml()); - activeLock.addContent(property); - - // lockscope property - property = new Element(XML_LOCKSCOPE, NAMESPACE); - property.addContent(getScope().toXml()); - activeLock.addContent(property); - - // depth - activeLock.addContent(XmlUtil.depthToXml(isDeep())); - // timeout - long timeout = getTimeout(); - if (!isExpired() && timeout != UNDEFINED_TIMEOUT) { - activeLock.addContent(XmlUtil.timeoutToXml(getTimeout())); - } - - // owner - if (getOwner() != null) { - property = new Element(XML_OWNER, NAMESPACE); - property.setText(getOwner()); - activeLock.addContent(property); - } - - // locktoken - if (getToken() != null) { - property = new Element(XML_LOCKTOKEN, NAMESPACE); - Element href = new Element(XML_HREF, NAMESPACE); - href.setText(getToken()); - property.addContent(href); - activeLock.addContent(property); - } - return activeLock; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/AbstractLockEntry.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/AbstractLockEntry.java deleted file mode 100644 index 20693cad536..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/AbstractLockEntry.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Element; - -/** - * AbstractLockEntry provides the generic {@link #toXml} method. - */ -public abstract class AbstractLockEntry implements LockEntry, DavConstants { - - private static Logger log = Logger.getLogger(AbstractLockEntry.class); - - /** - * Returns the Xml representation of this LockEntry. - * - * @return Xml representation - */ - public Element toXml() { - Element entry = new Element(XML_LOCKENTRY, NAMESPACE); - Element prop = new Element(XML_LOCKSCOPE, NAMESPACE); - prop.addContent(getScope().toXml()); - entry.addContent(prop); - prop = new Element(XML_LOCKTYPE, NAMESPACE); - prop.addContent(getType().toXml()); - entry.addContent(prop); - return entry; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/ActiveLock.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/ActiveLock.java deleted file mode 100644 index 08e1cd2ef4a..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/ActiveLock.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.jdom.Element; - -/** - * ActiveLock encapsulates the lock information for a - * {@link org.apache.jackrabbit.webdav.DavResource}. - */ -public interface ActiveLock { - - /** - * Return true, if the given token matches the lock token present in this - * lock thus indicating that any lock relevant operation should not fail - * due to a lock. - * - * @param lockToken to be checked - * @return true if the given lock token matches. - */ - public boolean isLockedByToken(String lockToken); - - /** - * Returns true, if this lock is already expired. - * - * @return true if the lock is expired - */ - public boolean isExpired(); - - /** - * Return the lock token. - * - * @return token string representing the lock token. - */ - public String getToken(); - - /** - * Return the name (or id) of the lock owner. - * - * @return name (or id) of the lock owner. - */ - public String getOwner(); - - /** - * Set the name (or id) of the lock owner - * - * @param owner - */ - public void setOwner(String owner); - - /** - * Return the number of milliseconds the lock will live until it is expired - * or -1 if the timeout is not available (or the client is not allowed - * to retrieve it). - * - * @return - */ - public long getTimeout(); - - /** - * Defines the number of milliseconds until the timeout is reached. - * - * @param timeout - */ - public void setTimeout(long timeout); - - /** - * Return true if the lock is deep. - * - * @return true if the lock is deep. - */ - public boolean isDeep(); - - /** - * Set the lock deepness. - * - * @param isDeep - */ - public void setIsDeep(boolean isDeep); - - /** - * Return the type of this lock. - * - * @return type - */ - public Type getType(); - - /** - * Return the scope of this lock. - * - * @return scope - */ - public Scope getScope(); - - /** - * Return the Xml representation of this lock. - * - * @return Xml representation of this lock - */ - public Element toXml(); -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockDiscovery.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockDiscovery.java deleted file mode 100644 index a9e2f57da67..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockDiscovery.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.jdom.Element; -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.property.AbstractDavProperty; - -import java.util.List; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * The LockDiscovery class encapsulates the webdav lock discovery - * property that is sent in the request body (PROPFIND and LOCK) and received - * in a LOCK response body. - */ -public class LockDiscovery extends AbstractDavProperty { - - /** - * Listing of existing locks applied to the resource this discovery was - * requested for. Each entry reveals, who has a lock, what type of lock he has, - * the timeout type and the time remaining on the timeout, and the lock-type. - * NOTE, that any of the information listed may be not availble for the - * server is free to withhold any or all of this information. - */ - private List activeLocks = new ArrayList(); - - /** - * Creates a new empty LockDiscovery property - */ - public LockDiscovery() { - super(DavPropertyName.LOCKDISCOVERY, false); - } - - /** - * Create a new LockDiscovery property - * - * @param lock - */ - public LockDiscovery(ActiveLock lock) { - super(DavPropertyName.LOCKDISCOVERY, false); - addActiveLock(lock); - } - - /** - * Create a new LockDiscovery property - * - * @param locks - */ - public LockDiscovery(ActiveLock[] locks) { - super(DavPropertyName.LOCKDISCOVERY, false); - for (int i = 0; i < locks.length; i++) { - addActiveLock(locks[i]); - } - } - - private void addActiveLock(ActiveLock lock) { - if (lock != null) { - activeLocks.add(lock); - } - } - - /** - * Creates a JDOM <lockdiscovery> element in order to respond to a LOCK - * request or to the lockdiscovery property of a PROPFIND request.
- * NOTE: if the {@link #activeLocks} list is empty an empty lockdiscovery - * property is created ( <lockdiscovery/>) - * @return A JDOM element of the <active> lock tag. - */ - public Element toXml() { - Element lockdiscovery = getName().toXml(); - Iterator it = activeLocks.iterator(); - while (it.hasNext()) { - ActiveLock lock = (ActiveLock) it.next(); - lockdiscovery.addContent(lock.toXml()); - } - return lockdiscovery; - } - - /** - * Returns the list of active locks. - * - * @return list of active locks - * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() - */ - public Object getValue() { - return activeLocks; - } -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockEntry.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockEntry.java deleted file mode 100644 index 49b90d6fb12..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockEntry.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.jdom.Element; - -/** - * LockEntry... - */ -public interface LockEntry { - - /** - * Returns the type of this lock entry - * - * @return type of this lock entry. - */ - public Type getType(); - - /** - * Returns the scope of this lock entry. - * - * @return scope of this lock entry. - */ - public Scope getScope(); - - /** - * Returns the Xml representation of this entry. - * - * @return Xml representation - */ - public Element toXml(); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockInfo.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockInfo.java deleted file mode 100644 index 6847d413c15..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockInfo.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Element; - -import java.util.List; -import java.util.Iterator; - -/** - * LockInfo is a simple utility class encapsulating the information - * passed with a LOCK request. It combines both the request body (which if present - * is required to by a 'lockinfo' Xml element) and the lock relevant request - * headers '{@link DavConstants#HEADER_TIMEOUT Timeout}' and - * '{@link DavConstants#HEADER_DEPTH Depth}'.
- * Note that is class is not intended to perform any validation of the information - * given, since this left to those objects responsible for the lock creation - * on the requested resource. - */ -public class LockInfo { - - private Type type; - private Scope scope; - private String owner; - private boolean isDeep; - private long timeout; - - private boolean isRefreshLock; - - /** - * Create a new LockInfo object from the given information. If - * liElement is null this lockinfo is assumed to - * be issued from a 'Refresh Lock' request. - * - * @param liElement 'lockinfo' element present in the request body of a LOCK request - * or null if the request was intended to refresh an existing lock. - * @param timeout Requested timespan until the lock should expire. A LOCK - * request MUST contain a '{@link DavConstants#HEADER_TIMEOUT Timeout}' - * according to RFC 2518. - * @param isDeep boolean value indicating whether the lock should be applied - * with depth infinity or only to the requested resource. - * @throws IllegalArgumentException if the liElement is not - * null but does not start with an 'lockinfo' element. - */ - public LockInfo(Element liElement, long timeout, boolean isDeep) { - this.timeout = timeout; - this.isDeep = isDeep; - - if (liElement != null) { - if (!DavConstants.XML_LOCKINFO.equals(liElement.getName())) { - throw new IllegalArgumentException("Element must have name 'lockinfo'."); - } - - List childList = liElement.getChildren(); - for (int i = 0; i < childList.size(); i++) { - Element child = (Element) childList.get(i); - String nodeName = child.getName(); - if (DavConstants.XML_LOCKTYPE.equals(nodeName)) { - Element typeElement = getFirstChildElement(child); - type = Type.create(typeElement); - } else if (DavConstants.XML_LOCKSCOPE.equals(nodeName)) { - Element scopeElement = getFirstChildElement(child); - scope = Scope.create(scopeElement); - } else if (DavConstants.XML_OWNER.equals(nodeName)) { - owner = child.getChildTextTrim(DavConstants.XML_HREF); - if (owner==null) { - // check if child is a text element - owner = child.getTextTrim(); - } - } - } - isRefreshLock = false; - } else { - isRefreshLock = true; - } - } - - /** - * Retrieve the first element from the content list of the specified Xml element. - * - * @param elem - * @return - */ - private static Element getFirstChildElement(Element elem) { - if (elem.getContentSize() > 0) { - Iterator it = elem.getContent().iterator(); - while (it.hasNext()) { - Object content = it.next(); - if (content instanceof Element) { - return (Element) content; - } - } - } - return null; - } - - /** - * Returns the lock type or null if no 'lockinfo' element was - * passed to the constructor or did not contain an 'type' element. - * - * @return type or null - */ - public Type getType() { - return type; - } - - /** - * Return the lock scope or null if no 'lockinfo' element was - * passed to the constructor or did not contain an 'scope' element. - * - * @return scope or null - */ - public Scope getScope() { - return scope; - } - - /** - * Return the owner indicated by the corresponding child element from the - * 'lockinfo' element or null if no 'lockinfo' element was - * passed to the constructor or did not contain an 'owner' element. - * - * @return owner or null - */ - public String getOwner() { - return owner; - } - - /** - * Returns true if the lock must be applied with depth infinity. - * - * @return true if a deep lock must be created. - */ - public boolean isDeep() { - return isDeep; - } - - /** - * Returns the time until the lock is requested to expire. - * - * @return time until the lock should expire. - */ - public long getTimeout() { - return timeout; - } - - /** - * Returns true if this LockInfo was created for a LOCK - * request intended to refresh an existing lock rather than creating a - * new one. - * - * @return true if the corresponding LOCK request was intended to refresh - * an existing lock. - */ - public boolean isRefreshLock() { - return isRefreshLock; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockManager.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockManager.java deleted file mode 100644 index 3f415af8784..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/LockManager.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.DavResource; - -/** - * The LockManager interface. - */ -public interface LockManager { - - /** - * Create a new lock for the given {@link org.apache.jackrabbit.webdav.DavResource resource}. - * - * @param lockInfo - * @param resource - * @return - * @throws DavException - */ - public ActiveLock createLock(LockInfo lockInfo, DavResource resource) - throws DavException; - - /** - * Refresh the lock identified by the given lockToken and initially created - * on the specified resouce. The update information can be retrieved from - * the lockInfo object passes. - * - * @param lockInfo - * @param lockToken - * @param resource - * @return - * @throws DavException - */ - public ActiveLock refreshLock(LockInfo lockInfo, String lockToken, DavResource resource) - throws DavException; - - /** - * Release the lock identified by the given lockToken and initially created - * on the specified resouce. - * - * @param lockToken - * @param resource - * @throws DavException - */ - public void releaseLock(String lockToken, DavResource resource) - throws DavException; - - /** - * Retrieve the lock with the given type and scope that is applied to the - * given resource. The lock may be either initially created on this resource - * or might be inherited from an ancestor resource that hold a deep lock. - * If no such lock applies to the given resource null must be - * returned. - * - * @param type - * @param scope - * @param resource - * @return lock with the given type and scope applied to the resource or - * null if no lock applies. - */ - public ActiveLock getLock(Type type, Scope scope, DavResource resource); - - /** - * Returns true, if the the manager contains a lock for the given - * resource, that is hold by the specified token. - * - * @param lockToken - * @param resource - * @return true if the resource is locked by the specified token. - */ - public boolean hasLock(String lockToken, DavResource resource); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/Scope.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/Scope.java deleted file mode 100644 index 0bd516ef624..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/Scope.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.jdom.Element; -import org.jdom.Namespace; -import org.apache.jackrabbit.webdav.DavConstants; - -import java.util.*; - -/** - * The Scope class abstracts the lock scope as defined by RFC 2518. - */ -public class Scope { - - private static final Map scopes = new HashMap(); - - public static final Scope EXCLUSIVE = Scope.create(DavConstants.XML_EXCLUSIVE, DavConstants.NAMESPACE); - public static final Scope SHARED = Scope.create(DavConstants.XML_SHARED, DavConstants.NAMESPACE); - - private final String name; - private final Namespace namespace; - - /** - * Private constructor - * - * @param name - * @param namespace - */ - private Scope(String name, Namespace namespace) { - this.name = name; - this.namespace = namespace; - } - - /** - * Return the Xml representation of the lock scope object as present in - * the LOCK request and response body and in the {@link LockDiscovery}. - * - * @return Xml representation - */ - public Element toXml() { - return new Element(name, namespace); - } - - /** - * Create a Scope object from the given Xml element. - * - * @param lockScope - * @return Scope object. - */ - public static Scope create(Element lockScope) { - if (lockScope == null) { - throw new IllegalArgumentException("'null' is not valid lock scope entry."); - } - return create(lockScope.getName(), lockScope.getNamespace()); - } - - /** - * Create a Scope object from the given name and namespace. - * - * @param name - * @param namespace - * @return Scope object. - */ - public static Scope create(String name, Namespace namespace) { - String key = "{" + namespace.getURI() + "}" + name; - if (scopes.containsKey(key)) { - return (Scope) scopes.get(key); - } else { - Scope scope = new Scope(name, namespace); - scopes.put(key, scope); - return scope; - } - } - - /** - * Returns true if this Scope is equal to the given one. - * - * @param obj - * @return - */ - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj instanceof Scope) { - Scope other = (Scope) obj; - return name.equals(other.name) && namespace.equals(other.namespace); - } - return false; - } - -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/Type.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/Type.java deleted file mode 100644 index 24d0d8892f4..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/Type.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.lock; - -import org.jdom.Element; -import org.jdom.Namespace; -import org.apache.jackrabbit.webdav.DavConstants; - -import java.util.*; - -/** - * The Type class encapsulates the lock type as defined by RFC 2518. - */ -public class Type { - - private static Map types = new HashMap(); - - public static final Type WRITE = Type.create(DavConstants.XML_WRITE, DavConstants.NAMESPACE); - - private final String name; - private final Namespace namespace; - - /** - * Private constructor. - * - * @param name - * @param namespace - */ - private Type(String name, Namespace namespace) { - this.name = name; - this.namespace = namespace; - } - - /** - * Returns the Xml representation of this lock Type. - * - * @return Xml representation - */ - public Element toXml() { - return new Element(name, namespace); - } - - /** - * Create a Type object from the given Xml element. - * - * @param lockType - * @return Type object. - */ - public static Type create(Element lockType) { - if (lockType == null) { - throw new IllegalArgumentException("'null' is not valid lock type entry."); - } - return create(lockType.getName(), lockType.getNamespace()); - } - - /** - * Create a Type object from the given name and namespace. - * - * @param name - * @param namespace - * @return Type object. - */ - public static Type create(String name, Namespace namespace) { - String key = "{" + namespace.getURI() + "}" + name; - if (types.containsKey(key)) { - return (Type) types.get(key); - } else { - Type type = new Type(name, namespace); - types.put(key, type); - return type; - } - } - - /** - * Returns true if this Type is equal to the given one. - * - * @param obj - * @return - */ - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj instanceof Type) { - Type other = (Type) obj; - return name.equals(other.name) && namespace.equals(other.namespace); - } - return false; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/package.html deleted file mode 100644 index b189cbfafd4..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/package.html +++ /dev/null @@ -1,3 +0,0 @@ - -Provides interfaces and classes for locking related issues. - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/EventDiscovery.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/EventDiscovery.java deleted file mode 100644 index cc08afaf24e..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/EventDiscovery.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.observation; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import java.util.List; -import java.util.ArrayList; - -/** - * EventDiscovery represents the request body of a successfull - * POLL request. It reveals all events that occured since the last POLL. The - * definition what events that particular subscription is interested in was - * specified with the initial SUBSCRIPTION that started the event listening. - */ -public class EventDiscovery implements ObservationConstants { - - private static Logger log = Logger.getLogger(EventDiscovery.class); - - private List bundles = new ArrayList(); - - /** - * Add the Xml representation of an single 'eventBundle' listing the - * events that resulted from a change in the server, filtered by the - * restrictions present in the corresponding subscription. - * - * @param eventBundle - * @see Subscription - */ - public void addEventBundle(Element eventBundle) { - if (eventBundle != null) { - bundles.add(eventBundle); - } - } - - /** - * Returns the Xml representation of this EventDiscovery as - * being present in the POLL response body. - * - * @return Xml representation - */ - public Element toXml() { - Element ed = new Element(XML_EVENTDISCOVERY, NAMESPACE); - ed.addContent(bundles); - return ed; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationConstants.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationConstants.java deleted file mode 100644 index b7db08f79b1..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationConstants.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.observation; - -import org.jdom.Namespace; -import org.apache.jackrabbit.webdav.property.DavPropertyName; - -/** - * ObservationConstants interface provide constants for request - * and response headers, Xml elements and property names used for handling - * observation over WebDAV. There exists no public standard for this - * functionality. - */ -public interface ObservationConstants { - - /** - * The namespace - */ - public static final Namespace NAMESPACE = Namespace.getNamespace("jcr", "http://www.day.com/jcr/webdav/1.0"); - - /** - * The SubscriptionId request header
- */ - public static final String HEADER_SUBSCRIPTIONID = "SubscriptionId"; - - /** - * subscription Xml element
- * Mandatory element inside the {@link #SUBSCRIPTIONDISCOVERY subscriptiondiscovery} - * property indicating the event listeners present for this session.
- * NOTE, that this will not reveal any subscription made by another session. - */ - public static final String XML_SUBSCRIPTION = "subscription"; - - /** - * Xml elements - */ - public static final String XML_SUBSCRIPTIONINFO = "subscriptioninfo"; - - public static final String XML_EVENTTYPE = "eventtype"; - public static final String XML_NOLOCAL = "nolocal"; - public static final String XML_FILTER = "filter"; - public static final String XML_SUBSCRIPTIONID = "subscriptionid"; - public static final String XML_UUID = "uuid"; - public static final String XML_NODETYPE_NAME = "nodetype-name"; - - public static final String XML_EVENTDISCOVERY = "eventdiscovery"; - public static final String XML_EVENTBUNDLE = "eventbundle"; - public static final String XML_EVENT = "event"; - public static final String XML_EVENTUSERID = "eventuserid"; - - /** - * Element representing the 'nodeadded' event type. - * @see javax.jcr.observation.Event#NODE_ADDED - */ - public static final String EVENT_NODEADDED = "nodeadded"; - - /** - * Element representing the 'noderemoved' event type. - * @see javax.jcr.observation.Event#NODE_REMOVED - */ - public static final String EVENT_NODEREMOVED = "noderemoved"; - - /** - * Element representing the 'propertyadded' event type. - * @see javax.jcr.observation.Event#PROPERTY_ADDED - */ - public static final String EVENT_PROPERTYADDED = "propertyadded"; - - /** - * Element representing the 'propertyremoved' event type. - * @see javax.jcr.observation.Event#PROPERTY_REMOVED - */ - public static final String EVENT_PROPERTYREMOVED = "propertyremoved"; - - /** - * Element representing the 'propertychanged' event type. - * @see javax.jcr.observation.Event#PROPERTY_CHANGED - */ - public static final String EVENT_PROPERTYCHANGED = "propertychanged"; - - /** - * The protected subscription discovery property is used to find out about - * existing subscriptions present on the specified resource. - */ - public static final DavPropertyName SUBSCRIPTIONDISCOVERY = DavPropertyName.create("subscriptiondiscovery", NAMESPACE); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletRequest.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletRequest.java deleted file mode 100644 index f6c5377623e..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletRequest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.observation; - -import org.apache.jackrabbit.webdav.DavServletRequest; - -/** - * ObservationDavServletRequest provides extensions to the - * {@link DavServletRequest} interface used for dealing with observation. - */ -public interface ObservationDavServletRequest extends DavServletRequest { - - /** - * Return the {@link ObservationConstants#HEADER_SUBSCRIPTIONID SubscriptionId header} - * or null if no such header is present. - * - * @return the {@link ObservationConstants#HEADER_SUBSCRIPTIONID SubscriptionId header} - */ - public String getSubscriptionId(); - - /** - * Return a {@link SubscriptionInfo} object representing the subscription - * info present in the SUBSCRIBE request body or null if - * retrieving the subscription info fails. - * - * @return subscription info object encapsulating the SUBSCRIBE request body - * or null if the subscription info cannot be built. - */ - public SubscriptionInfo getSubscriptionInfo(); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletResponse.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletResponse.java deleted file mode 100644 index 8a68b97a509..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.observation; - -import org.apache.jackrabbit.webdav.DavServletResponse; - -import java.io.IOException; - -/** - * ObservationDavServletResponse provides extensions to the - * {@link DavServletResponse} interface used for dealing with observation. - */ -public interface ObservationDavServletResponse extends DavServletResponse { - - /** - * Send the response to a successful SUBSCRIBE request. - * - * @param subsription that needs to be represented in the response body. - * @throws IOException - */ - public void sendSubscriptionResponse(Subscription subsription) throws IOException; - - /** - * Send the response to a sucessful POLL request. - * - * @param eventdiscovery {@link EventDiscovery} object to be returned in - * the response body. - * @throws IOException - */ - public void sendPollResponse(EventDiscovery eventdiscovery) throws IOException; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/Subscription.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/Subscription.java deleted file mode 100644 index 0b7debd7640..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/Subscription.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.observation; - -import org.jdom.Element; - -/** - * Subscription represents public representation of the event - * listener created (or modified) by a successful SUBSCRIBE request. - */ -public interface Subscription { - - /** - * Returns the id of this subscription, that must be used for unsubscribing - * as well as for event discovery later on. - * - * @return subscriptionId - */ - public String getSubscriptionId(); - - /** - * Return the Xml representation of this Subscription that is - * returned in the response to a successful SUBSCRIBE request as well - * as in a PROPFIND request. In both cases the subscription is packed into - * a {@link SubscriptionDiscovery} property object. - * - * @return Xml representation - */ - public Element toXml(); - -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionDiscovery.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionDiscovery.java deleted file mode 100644 index 58c9a51bc2b..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionDiscovery.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.observation; - -import org.apache.jackrabbit.webdav.property.AbstractDavProperty; -import org.jdom.Element; - -/** - * SubscriptionDiscovery encapsulates the 'subscriptiondiscovery' - * property of a webdav resource. - */ -public class SubscriptionDiscovery extends AbstractDavProperty { - - Subscription[] subscriptions = new Subscription[0]; - - /** - * Create a new SubscriptionDiscovery that lists the given - * subscriptions. - * - * @param subscriptions - */ - public SubscriptionDiscovery(Subscription[] subscriptions) { - super(ObservationConstants.SUBSCRIPTIONDISCOVERY, true); - if (subscriptions != null) { - this.subscriptions = subscriptions; - } - } - - /** - * Create a new SubscriptionDiscovery that contains a single - * subscription entry. - * - * @param subscription - */ - public SubscriptionDiscovery(Subscription subscription) { - super(ObservationConstants.SUBSCRIPTIONDISCOVERY, true); - if (subscription != null) { - this.subscriptions = new Subscription[]{subscription}; - } - } - - /** - * Returns the Xml representation of the subscription discovery. - * - * @return Xml representation - * @see org.apache.jackrabbit.webdav.property.DavProperty#toXml() - */ - public Element toXml() { - Element elem = getName().toXml(); - for (int i = 0; i < subscriptions.length; i++) { - elem.addContent(subscriptions[i].toXml()); - } - return elem; - } - - /** - * Returns an array of {@link Subscription}s. - * - * @return an array of {@link Subscription}s - * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() - */ - public Object getValue() { - return subscriptions; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionInfo.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionInfo.java deleted file mode 100644 index d9f1719c8b3..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionInfo.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.observation; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.util.XmlUtil; -import org.jdom.Element; - -import java.util.List; - -/** - * SubscriptionInfo class encapsulates the subscription info - * that forms the request body of a SUBSCRIBE request. - * @see ObservationConstants#XML_SUBSCRIPTIONINFO - */ -public class SubscriptionInfo implements ObservationConstants { - - private static Logger log = Logger.getLogger(SubscriptionInfo.class); - - private Element info; - private List eventTypes; - private long timeout; - private boolean isDeep; - - /** - * Create a new SubscriptionInfo - * - * @param reqInfo Xml element present in the request body. - * @param timeout as defined by the {@link org.apache.jackrabbit.webdav.DavConstants#HEADER_TIMEOUT timeout header}. - * @param isDeep as defined by the {@link org.apache.jackrabbit.webdav.DavConstants#HEADER_DEPTH depth header}. - * @throws IllegalArgumentException if the reqInfo element does not contain the mandatory elements. - */ - public SubscriptionInfo(Element reqInfo, long timeout, boolean isDeep) { - if (!XML_SUBSCRIPTIONINFO.equals(reqInfo.getName())) { - throw new IllegalArgumentException("Element with name 'subscriptioninfo' expected"); - } - if (reqInfo.getChild(XML_EVENTTYPE, NAMESPACE) == null ) { - throw new IllegalArgumentException("'subscriptioninfo' must contain an 'eventtype' child element."); - } - - eventTypes = reqInfo.getChild(XML_EVENTTYPE, NAMESPACE).getChildren(); - if (eventTypes.size() == 0) { - throw new IllegalArgumentException("'subscriptioninfo' must at least indicate a single event type."); - } - - // detach the request info, in order to remove the reference to the parent - this.info = (Element)reqInfo.detach(); - this.isDeep = isDeep; - setTimeOut(timeout); - } - - /** - * Return list of event types Xml elements present in the subscription info. - * NOTE: the elements need to be detached in order to be added as content - * to any other Xml element. - * - * @return List of Xml elements defining which events this subscription should - * listen to. - * - */ - public List getEventTypes() { - return eventTypes; - } - - /** - * Return array of filters with the specified name. - * - * @param name the filter elments must provide. - * @return array containing the text of the filter elements with the given - * name. - */ - public String[] getFilters(String name) { - String[] filters = null; - Element filter = info.getChild(XML_FILTER); - if (filter != null) { - List li = filter.getChildren(name); - if (!li.isEmpty()) { - filters = new String[li.size()]; - for (int i = 0; i < filters.length; i++) { - filters[i] = ((Element)li.get(i)).getText(); - } - } - } - return filters; - } - - /** - * Returns true if the {@link #XML_NOLOCAL} element is present in this - * subscription info. - * - * @return if {@link #XML_NOLOCAL} element is present. - */ - public boolean isNoLocal() { - return info.getChild(XML_NOLOCAL, NAMESPACE) != null; - } - - /** - * Returns true if the {@link org.apache.jackrabbit.webdav.DavConstants#HEADER_DEPTH - * depths header} defined a depth other than '0'. - * - * @return true if this subscription info was created with isDeep - * true. - */ - public boolean isDeep() { - return isDeep; - } - - /** - * Return the timeout as retrieved from the request. - * - * @return timeout. - */ - public long getTimeOut() { - return timeout; - } - - /** - * Set the timeout. NOTE: no validation is made. - * - * @param timeout as defined by the {@link org.apache.jackrabbit.webdav.DavConstants#HEADER_TIMEOUT}. - */ - public void setTimeOut(long timeout) { - this.timeout = timeout; - } - - /** - * Xml representation of this SubscriptionInfo. - * - * @return Xml representation - */ - public Element[] toXml() { - Element[] elems = { info, XmlUtil.depthToXml(isDeep), XmlUtil.timeoutToXml(timeout)}; - return elems; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionManager.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionManager.java deleted file mode 100644 index 71d0a4ea890..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/SubscriptionManager.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.observation; - -import org.apache.jackrabbit.webdav.DavException; - -/** - * SubscriptionManager interface. - */ -public interface SubscriptionManager { - - /** - * Retrieve the {@link org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery} object for the given - * resource. Note, that the discovery object will be empty if there are - * no subscriptions present. - * - * @param resource - */ - public SubscriptionDiscovery getSubscriptionDiscovery(ObservationResource resource); - - /** - * Create a new Subscription or update an existing Subscription.. - * - * @param info - * @param subscriptionId - * @param resource - * @return Subscription that has been created or updated - * @throws DavException if the subscription fails - */ - public Subscription subscribe(SubscriptionInfo info, String subscriptionId, - ObservationResource resource) - throws DavException; - - /** - * Unsubscribe the Subscription with the given id. - * - * @param subscriptionId - * @param resource - * @throws DavException - */ - public void unsubscribe(String subscriptionId, ObservationResource resource) - throws DavException; - - /** - * Retrieve the list of events that occured since the last poll. - * - * @param subscriptionId indentifier for the subscription - * @param resource - * @return - */ - public EventDiscovery poll(String subscriptionId, ObservationResource resource) - throws DavException; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/package.html deleted file mode 100644 index 7ab068095ba..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/package.html +++ /dev/null @@ -1,4 +0,0 @@ - -Contains interfaces and classes related to observation, which is not covered -by the WebDAV protocol. - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderPatch.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderPatch.java deleted file mode 100644 index 7f006697876..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderPatch.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.ordering; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Element; - -import java.util.List; -import java.util.Iterator; -import java.util.ArrayList; - -/** - * OrderPatch represents the mandatory request body of an - * ORDERPATCH request. RFC 3648 defines the following structure for it:
- *

- * <!ELEMENT orderpatch (ordering-type?, order-member*) >
- * <!ELEMENT order-member (segment, position) >
- * <!ELEMENT position (first | last | before | after) >
- * <!ELEMENT segment (#PCDATA) >
- * <!ELEMENT first EMPTY >
- * <!ELEMENT last EMPTY >
- * <!ELEMENT before segment >
- * <!ELEMENT after segment >
- * 
- */ -public class OrderPatch implements OrderingConstants{ - - private static Logger log = Logger.getLogger(OrderPatch.class); - - private Member[] instructions; - private String orderingType; - - /** - * Create a new OrderPath object. - * - * @param orderPatchElement - * @throws IllegalArgumentException if the specified Xml element was not valid. - */ - public OrderPatch(Element orderPatchElement) { - if (!OrderingConstants.XML_ORDERPATCH.equals(orderPatchElement.getName()) || - orderPatchElement.getChild(OrderingConstants.XML_ORDERING_TYPE) == null) { - throw new IllegalArgumentException("ORDERPATH request body must start with an 'orderpatch' element, which must contain an 'ordering-type' child element."); - } - // retrieve the orderingtype element - orderingType = orderPatchElement.getChild(OrderingConstants.XML_ORDERING_TYPE).getChildText(DavConstants.XML_HREF); - - // set build the list of ordering instructions - List oMembers = orderPatchElement.getChildren(OrderingConstants.XML_ORDER_MEMBER, DavConstants.NAMESPACE); - Iterator it = oMembers.iterator(); - int cnt = 0; - List tmpInst = new ArrayList(); - while (it.hasNext()) { - Element member = (Element) it.next(); - try { - String segment = member.getChildText(OrderingConstants.XML_SEGMENT); - Position pos = new Position(member.getChild(OrderingConstants.XML_POSITION)); - Member om = new Member(segment, pos); - tmpInst.add(om); - cnt++; - } catch (IllegalArgumentException e) { - log.error("Invalid element in 'orderpatch' request body: " + e.getMessage()); - } - } - instructions = (Member[]) tmpInst.toArray(new Member[cnt]); - } - - /** - * Create a new OrderPath object. - * - * @param orderingType - * @param instructions - */ - public OrderPatch(String orderingType, Member[] instructions) { - this.orderingType = orderingType; - this.instructions = instructions; - } - - /** - * Return the ordering type. - * - * @return ordering type - */ - public String getOrderingType() { - return orderingType; - } - - /** - * Return an array of {@link Member} objects defining the re-ordering - * instructions to be applied to the requested resource. - * - * @return ordering instructions. - */ - public Member[] getOrderInstructions() { - return instructions; - } - - //-------------------------------------------------------------------------- - /** - * Internal class Member represents the 'Order-Member' children - * elements of an 'OrderPatch' request body present in the ORDERPATCH request. - */ - public class Member { - - private String memberHandle; - private Position position; - - /** - * Create a new Member object. - * - * @param memberHandle - * @param position - */ - public Member(String memberHandle, Position position) { - this.memberHandle = memberHandle; - this.position = position; - } - - /** - * Return the handle of the internal member to be reordered. - * - * @return handle of the internal member. - */ - public String getMemberHandle() { - return memberHandle; - } - - /** - * Return the position where the internal member identified by the - * member handle should be placed. - * - * @return position for the member after the request. - * @see #getMemberHandle() - */ - public Position getPosition() { - return position; - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingType.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingType.java deleted file mode 100644 index 3f3af1ca1ea..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingType.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.ordering; - -import org.jdom.Element; -import org.apache.jackrabbit.webdav.property.DefaultDavProperty; -import org.apache.jackrabbit.webdav.util.XmlUtil; - -/** - * OrderingType represents the {@link #ORDERING_TYPE - * DAV:ordering-type} property as defined by - * RFC 3648. This property is - * protected cannot be set using PROPPATCH. Its value may only be set by - * including the Ordering-Type header with a MKCOL request or by submitting an - * ORDERPATCH request. - * - * @see org.apache.jackrabbit.webdav.property.DavProperty#isProtected() - */ -public class OrderingType extends DefaultDavProperty implements OrderingConstants { - - /** - * Create an OrderingType with the given ordering.
- * NOTE: the ordering-type property is defined to be protected. - * - * @param href - * @see org.apache.jackrabbit.webdav.property.DavProperty#isProtected() - */ - public OrderingType(String href) { - super(ORDERING_TYPE, href, true); - } - - /** - * Returns the Xml representation of this property. If the property has - * a null value, the default ({@link #ORDERING_TYPE_UNORDERED - * DAV:unordered}) is assumed. - * - * @return Xml representation - */ - public Element toXml() { - Element elem = getName().toXml(); - // spec requires that the default is 'DAV:unordered' - String href = (getValue() != null) ? getValue().toString() : ORDERING_TYPE_UNORDERED; - XmlUtil.hrefToXml(href); - return elem; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/Position.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/Position.java deleted file mode 100644 index 46833fcc85b..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/Position.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.ordering; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import java.util.HashMap; - -/** - * Position encapsulates the position in ordering information - * contained in a Webdav request. This includes both the - * {@link OrderingConstants#HEADER_POSITION Position header} and the position - * Xml element present in the request body of an ORDERPATCH request. - * - * @see OrderingConstants#HEADER_POSITION - * @see OrderingConstants#XML_POSITION - * @see OrderPatch - */ -public class Position implements OrderingConstants { - - private static Logger log = Logger.getLogger(Position.class); - - public static final int TYPE_FIRST = 1; - public static final int TYPE_LAST = 2; - public static final int TYPE_BEFORE = 4; - public static final int TYPE_AFTER = 8; - - private static final HashMap xmlTypeMap = new HashMap(4); - static { - xmlTypeMap.put(XML_FIRST, new Integer(TYPE_FIRST)); - xmlTypeMap.put(XML_LAST, new Integer(TYPE_LAST)); - xmlTypeMap.put(XML_BEFORE, new Integer(TYPE_BEFORE)); - xmlTypeMap.put(XML_AFTER, new Integer(TYPE_AFTER)); - } - - private int type; - private String segment; - - /** - * Create a new Position object with the specified type. - * Since any type except for {@link #XML_FIRST first} and {@link #XML_LAST last} - * must be combined with a segment, only the mentioned types are valid - * arguments. - * - * @param type {@link #XML_FIRST first} or {@link #XML_LAST last} - * @throws IllegalArgumentException if the given type is other than {@link #XML_FIRST} - * or {@link #XML_LAST}. - */ - public Position(String type) { - if (!(XML_FIRST.equals(type) || XML_LAST.equals(type))) { - throw new IllegalArgumentException("If type is other than 'first' or 'last' a segment must be specified"); - } - setType(type); - } - - /** - * Create a new Position object from the specified position - * element. The element must fulfill the following structure:
- *
-     * <!ELEMENT position (first | last | before | after) >
-     * <!ELEMENT segment (#PCDATA) >
-     * <!ELEMENT first EMPTY >
-     * <!ELEMENT last EMPTY >
-     * <!ELEMENT before segment >
-     * <!ELEMENT after segment >
-     * 
- * - * @param position Xml element defining the position. - * @throws IllegalArgumentException if the given Xml element is not valid. - */ - public Position(Element position) { - if (position.getChildren().size() != 1) { - throw new IllegalArgumentException("The 'position' element must contain exactly a single child indicating the type."); - } - Element typeElem = (Element)position.getChildren().get(0); - String type = typeElem.getName(); - String segmentText = null; - if (typeElem.getChildren().size() > 0) { - segmentText = typeElem.getChildText(XML_SEGMENT); - } - init(type, segmentText); - } - - /** - * Create a new Position object with the specified type and - * segment. - * - * @param type - * @param segment - * @throws IllegalArgumentException if the specified type and segment do not - * form a valid pair. - */ - public Position(String type, String segment) { - init(type, segment); - } - - /** - * Initialize the internal fields. - * - * @param type - * @param segment - */ - private void init(String type, String segment) { - if ((XML_AFTER.equals(type) || XML_BEFORE.equals(type)) && (segment == null || "".equals(segment))) { - throw new IllegalArgumentException("If type is other than 'first' or 'last' a segment must be specified"); - } - setType(type); - this.segment = segment; - } - - /** - * Return the type of this Position object, which may be any - * of the following valid types: {@link #XML_FIRST first}, - * {@link #XML_LAST last}, {@link #XML_AFTER after}, {@link #XML_BEFORE before} - * - * @return type - */ - public int getType() { - return type; - } - - /** - * Set the type. - * - * @param xmlType - */ - private void setType(String xmlType) { - type = ((Integer)xmlTypeMap.get(xmlType)).intValue(); - } - - /** - * Returns the segment used to create this Position object or - * null if no segment is present with the type. - * - * @return segment or null - * @see #getType() - */ - public String getSegment() { - return segment; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/package.html deleted file mode 100644 index d59cfbeb8d6..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/package.html +++ /dev/null @@ -1,5 +0,0 @@ - -Contains interfaces and classes used to cover the functionality defined by the -RFC 3648: Web Distributed Authoring -and Versioning (WebDAV) Ordered Collections Protocol . - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/AbstractDavProperty.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/AbstractDavProperty.java deleted file mode 100644 index 80da6498fd9..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/AbstractDavProperty.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import java.util.Arrays; -import java.util.List; - -/** - * AbstractDavProperty provides generic METHODS used by various - * implementations of the {@link DavProperty} interface. - */ -public abstract class AbstractDavProperty implements DavProperty { - - private static Logger log = Logger.getLogger(AbstractDavProperty.class); - - private final DavPropertyName name; - private final boolean isProtected; - - /** - * Create a new AbstractDavProperty with the given {@link DavPropertyName} - * and a boolean flag indicating whether this property is protected. - * - * @param name - * @param isProtected - */ - public AbstractDavProperty(DavPropertyName name, boolean isProtected) { - this.name = name; - this.isProtected = isProtected; - } - - /** - * Computes the hash code using this propertys name and value. - * - * @return the hash code - */ - public int hashCode() { - int hashCode = getName().hashCode(); - if (getValue() != null) { - hashCode += getValue().hashCode(); - } - return hashCode % Integer.MAX_VALUE; - } - - /** - * Checks if this property has the same {@link DavPropertyName name} - * and value as the given one. - * - * @param obj the object to compare to - * @return true if the 2 objects are equal; - * false otherwise - */ - public boolean equals(Object obj) { - if (obj instanceof DavProperty) { - DavProperty prop = (DavProperty) obj; - boolean equalName = getName().equals(prop.getName()); - boolean equalValue = (getValue() == null) ? prop.getValue() == null : getValue().equals(prop.getValue()); - return equalName && equalValue; - } - return false; - } - - - /** - * Return a JDOM element representation of this property. The value of the - * property will be added as text or as child element. - *
-     * new DavProperty("displayname", "WebDAV Directory").toXml()
-     * gives a element like:
-     * <D:displayname>WebDAV Directory</D:displayname>
-     *
-     * new DavProperty("resourcetype", new Element("collection")).toXml()
-     * gives a element like:
-     * <D:resourcetype><D:collection/></D:resourcetype>
-     *
-     * Element[] customVals = { new Element("bla", customNamespace), new Element("bli", customNamespace) };
-     * new DavProperty("custom-property", customVals, customNamespace).toXml()
-     * gives an element like
-     * <Z:custom-property>
-     *    <Z:bla/>
-     *    <Z:bli/>
-     * </Z:custom-property>
-     * 
- * - * @return a JDOM element of this property - * @see DavProperty#toXml() - */ - public Element toXml() { - Element elem = getName().toXml(); - Object value = getValue(); - if (value != null) { - if (value instanceof Element) { - elem.addContent((Element) value); - } else if (value instanceof Element[]) { - elem.addContent(Arrays.asList((Element[])value)); - } else if (value instanceof List) { - elem.addContent((List)value); - } else { - elem.setText(value.toString()); - } - } - return elem; - } - - /** - * Returns the name of this property. - * - * @return name - * @see DavProperty#getName() - */ - public DavPropertyName getName() { - return name; - } - - /** - * Returns true if this property is protected or computed. - * - * @return true if this is a protected (or computed) property. - * @see org.apache.jackrabbit.webdav.property.DavProperty#isProtected() - */ - public boolean isProtected() { - return isProtected; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavProperty.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavProperty.java deleted file mode 100644 index 700dc790feb..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavProperty.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import org.jdom.Element; -import org.apache.jackrabbit.webdav.DavConstants; - -/** - * The Property class represents a Property of a WebDAV - * resource. The {@link #hashCode()} and {@link #equals(Object)} METHODS are - * overridden in a way, that the name and value of the property are - * respected. this means, an property is equal to another, if the names - * and values are equal. - */ -public interface DavProperty extends DavConstants { - - /** - * Return a JDOM element representation of this property. The value of the - * property will be added as text or as child element. - *
-     * new DavProperty("displayname", "WebDAV Directory").toXml()
-     * gives a element like:
-     * <D:displayname>WebDAV Directory</D:displayname>
-     *
-     * new DavProperty("resourcetype", new Element("collection")).toXml()
-     * gives a element like:
-     * <D:resourcetype><D:collection/></D:resourcetype>
-     *
-     * Element[] customVals = { new Element("bla", customNamespace), new Element("bli", customNamespace) };
-     * new DavProperty("custom-property", customVals, customNamespace).toXml()
-     * gives an element like
-     * <Z:custom-property>
-     *    <Z:bla/>
-     *    <Z:bli/>
-     * </Z:custom-property>
-     * 
- * - * @return a JDOM element of this property - */ - public Element toXml(); - - /** - * Returns the name of this property - * - * @return the name of this property - */ - public DavPropertyName getName(); - - /** - * Returns the value of this property - * - * @return the value of this property - */ - public Object getValue(); - - /** - * Return true if this property is protected. A protected property - * will not be returned in a {@link DavConstants#PROPFIND_ALL_PROP DAV:allprop} - * PROPFIND request and cannot be set/removed with a PROPPATCH request. - * - * @return true, if this property is protected. - */ - public boolean isProtected(); -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyIterator.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyIterator.java deleted file mode 100644 index 042836220d7..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyIterator.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import java.util.Iterator; -import java.util.NoSuchElementException; - -/** - * The DavPropertyIterator extends the Iterator by - * a property specific next() method. - */ -public interface DavPropertyIterator extends Iterator { - /** - * Returns the next Property in the interation. - * - * @return the next Property in the iteration. - * @throws java.util.NoSuchElementException if iteration has no more elements. - */ - public DavProperty nextProperty() throws NoSuchElementException; -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyName.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyName.java deleted file mode 100644 index 97b36529c02..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyName.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import org.jdom.Namespace; -import org.jdom.Element; -import org.apache.jackrabbit.webdav.DavConstants; - -import java.util.HashMap; - -/** - * The DavPropertyName class reflects a Webdav property name. It - * holds together the actualy name of the property and its namespace. - */ -public class DavPropertyName implements DavConstants { - - /** internal 'cache' of created property names */ - private static final HashMap cache = new HashMap(); - - /* some standard webdav property (that have #PCDATA) */ - public static final DavPropertyName CREATIONDATE = DavPropertyName.create(PROPERTY_CREATIONDATE); - public static final DavPropertyName DISPLAYNAME = DavPropertyName.create(PROPERTY_DISPLAYNAME); - public static final DavPropertyName GETCONTENTLANGUAGE = DavPropertyName.create(PROPERTY_GETCONTENTLANGUAGE); - public static final DavPropertyName GETCONTENTLENGTH = DavPropertyName.create(PROPERTY_GETCONTENTLENGTH); - public static final DavPropertyName GETCONTENTTYPE = DavPropertyName.create(PROPERTY_GETCONTENTTYPE); - public static final DavPropertyName GETETAG = DavPropertyName.create(PROPERTY_GETETAG); - public static final DavPropertyName GETLASTMODIFIED = DavPropertyName.create(PROPERTY_GETLASTMODIFIED); - - /* some standard webdav property (that have other elements) */ - public static final DavPropertyName LOCKDISCOVERY = DavPropertyName.create(PROPERTY_LOCKDISCOVERY); - public static final DavPropertyName RESOURCETYPE = DavPropertyName.create(PROPERTY_RESOURCETYPE); - public static final DavPropertyName SOURCE = DavPropertyName.create(PROPERTY_SOURCE); - public static final DavPropertyName SUPPORTEDLOCK = DavPropertyName.create(PROPERTY_SUPPORTEDLOCK); - - /* property use by microsoft that are not specified in the RFC 2518 */ - public static final DavPropertyName ISCOLLECTION = DavPropertyName.create("iscollection"); - - /** the name of the property */ - private final String name; - - /** the namespace of the property */ - private final Namespace namespace; - - /** - * Creates a new DavPropertyName with the given name and - * Namespace. - * - * @param name The local name of the new property name - * @param namespace The namespace of the new property name - * - * @return The WebDAV property name - */ - public synchronized static DavPropertyName create(String name, Namespace namespace) { - - // get (or create) map for the given namespace - HashMap map = (HashMap) cache.get(namespace); - if (map == null) { - map = new HashMap(); - cache.put(namespace, map); - } - // get (or create) property name object - DavPropertyName ret = (DavPropertyName) map.get(name); - if (ret == null) { - if (namespace.equals(NAMESPACE)) { - // ensure prefix for default 'DAV:' namespace - namespace = NAMESPACE; - } - ret = new DavPropertyName(name, namespace); - map.put(name, ret); - } - return ret; - } - - /** - * Creates a new DavPropertyName with the given local name - * and the default WebDAV {@link DavConstants#NAMESPACE namespace}. - * - * @param name The local name of the new property name - * - * @return The WebDAV property name - */ - public synchronized static DavPropertyName create(String name) { - return create(name, NAMESPACE); - } - - /** - * Create a new DavPropertyName with the name and namespace - * of the given Xml element. - * - * @param nameElement - * @return DavPropertyName instance - */ - public synchronized static DavPropertyName createFromXml(Element nameElement) { - if (nameElement == null) { - throw new IllegalArgumentException("Cannot build DavPropertyName from a 'null' element."); - } - Namespace ns = nameElement.getNamespace(); - if (ns == null) { - return create(nameElement.getName()); - } else { - return create(nameElement.getName(), ns); - } - } - - /** - * Creates a new DavPropertyName with the given name and - * Namespace. - * - * @param name The local name of the new property name - * @param namespace The namespace of the new property name - */ - private DavPropertyName(String name, Namespace namespace) { - if (name == null || namespace == null) { - throw new IllegalArgumentException("Name and namespace must not be 'null' for a DavPropertyName."); - } - this.name = name; - this.namespace = namespace; - } - - /** - * Return the name of this DavPropertyName. - * - * @return name - */ - public String getName() { - return name; - } - - /** - * Return the namespace of this DavPropertyName. - * - * @return namespace - */ - public Namespace getNamespace() { - return namespace; - } - - - /** - * Computes the hash code using this propertys name and namespace. - * - * @return the hash code - */ - public int hashCode() { - return (name.hashCode() + namespace.hashCode()) % Integer.MAX_VALUE; - } - - /** - * Checks if this property has the same name and namespace as the - * given one. - * - * @param obj the object to compare to - * - * @return true if the 2 objects are equal; - * false otherwise - */ - public boolean equals(Object obj) { - if (obj instanceof DavPropertyName) { - DavPropertyName propName = (DavPropertyName) obj; - return name.equals(propName.name) && namespace.equals(propName.namespace); - } - return false; - } - - /** - * Returns a string representation of this property suitable for debugging - * - * @return a human readable string representation - */ - public String toString() { - return "{" + namespace.getURI() + "}" + name; - } - - /** - * Creates a JDOM element with the name and namespace of this - * DavPropertyName. - * - * @return A JDOM Element. - */ - public Element toXml() { - return new Element(name, namespace); - } -} - diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyNameSet.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyNameSet.java deleted file mode 100644 index 50e52fd4220..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertyNameSet.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Element; - -import java.util.HashSet; -import java.util.Collection; -import java.util.List; - -/** - * DavPropertyNameSet represents a Set of {@link DavPropertyName} - * objects. - */ -public class DavPropertyNameSet extends HashSet { - - private static Logger log = Logger.getLogger(DavPropertyNameSet.class); - - /** - * Create a new empty set. - * @see HashSet() - */ - public DavPropertyNameSet() { - super(); - } - - /** - * Create a new set from the given collection. - * @param c - * @see HashSet(Collection) - */ - public DavPropertyNameSet(Collection c) { - super(c); - } - - /** - * Create a new DavPropertyNameSet from the given DAV:prop - * element. - * - * @param propElement - * @throws IllegalArgumentException if the specified element is null - * or is not a DAV:prop element. - */ - public DavPropertyNameSet(Element propElement) { - super(); - if (propElement == null || !propElement.getName().equals(DavConstants.XML_PROP)) { - throw new IllegalArgumentException("'DAV:prop' element expected."); - } - - // fill the set - List props = propElement.getChildren(); - for (int j = 0; j < props.size(); j++) { - Element prop = (Element) props.get(j); - String propName = prop.getName(); - if (propName != null && !"".equals(propName)) { - add(DavPropertyName.create(propName, prop.getNamespace())); - } - } - } - - /** - * Adds the specified {@link DavPropertyName} object to this - * set if it is not already present. - * - * @param propertyName element to be added to this set. - * @return true if the set did not already contain the specified - * element. - */ - public boolean add(DavPropertyName propertyName) { - return super.add(propertyName); - } - - /** - * Add the given object to this set. In case the object is not a {@link DavPropertyName} - * this method returns false. - * - * @param o - * @return true if adding the object was successful. - * @see #add(DavPropertyName) - */ - public boolean add(Object o) { - if (o instanceof DavPropertyName) { - return add((DavPropertyName) o); - } else { - return false; - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertySet.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertySet.java deleted file mode 100644 index b48fa113ce2..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DavPropertySet.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import org.jdom.Namespace; -import org.apache.jackrabbit.webdav.DavConstants; - -import java.util.*; - -/** - * The DavPropertySet class represents a set of WebDAV - * property. - */ -public class DavPropertySet { - - /** - * the set of property - */ - private final HashMap map = new HashMap(); - - /** - * Adds a new property to this set. - * - * @param property The property to add - * - * @return The previously assigned property or null. - */ - public DavProperty add(DavProperty property) { - return (DavProperty) map.put(property.getName(), property); - } - - /** - * - * @param pset Properties to add - */ - public void addAll(DavPropertySet pset) { - map.putAll(pset.map); - } - - /** - * Retrieves the property with the specified name and the - * default WebDAV {@link org.apache.jackrabbit.webdav.DavConstants#NAMESPACE namespace}. - * - * @param name The name of the property to retrieve - * - * @return The desired property or null - */ - public DavProperty get(String name) { - return get(DavPropertyName.create(name)); - } - - /** - * Retrieves the property with the specified name and - * namespace. - * - * @param name The name of the property to retrieve - * @param namespace The namespace of the property to retrieve - * - * @return The desired property or null - */ - public DavProperty get(String name, Namespace namespace) { - return get(DavPropertyName.create(name, namespace)); - } - - /** - * Retrieves the property with the specified name - * - * @param name The webdav property name of the property to retrieve - * - * @return The desired property or null - */ - public DavProperty get(DavPropertyName name) { - return (DavProperty) map.get(name); - } - - - /** - * Removes the indicated property from this set. - * - * @param name The webdav property name to remove - * - * @return The removed property or null - */ - public DavProperty remove(DavPropertyName name) { - return (DavProperty) map.remove(name); - } - - /** - * Removes the property with the specified name and the - * default WebDAV {@link org.apache.jackrabbit.webdav.DavConstants#NAMESPACE namespace}. - * - * @param name The name of the property to remove - * - * @return The removed property or null - */ - public DavProperty remove(String name) { - return remove(DavPropertyName.create(name)); - } - - /** - * Removes the property with the specified name and - * namespace from this set. - * - * @param name The name of the property to remove - * @param namespace The namespace of the property to remove - * - * @return The removed property or null - */ - public DavProperty remove(String name, Namespace namespace) { - return remove(DavPropertyName.create(name, namespace)); - } - - /** - * Returns an iterator over all property in this set. - * - * @return An iterator over {@link DavProperty}. - */ - public DavPropertyIterator iterator() { - return new PropIter(); - } - - /** - * Returns an iterator over all those property in this set, that have the - * indicated namespace. - * - * @param namespace The namespace of the property in the iteration. - * - * @return An iterator over {@link DavProperty}. - */ - public DavPropertyIterator iterator(Namespace namespace) { - return new PropIter(namespace); - } - - /** - * Checks if this set contains the property with the specified name. - * - * @param name The name of the property - * - * @return true if this set contains the property; - * false otherwise. - */ - public boolean contains(DavPropertyName name) { - return map.containsKey(name); - } - - /** - * Checks if this set contains the property with the specified name and the - * default WebDAV {@link org.apache.jackrabbit.webdav.DavConstants#NAMESPACE namespace}. - * - * @param name The name of the property - * - * @return true if this set contains the property; - * false otherwise. - */ - public boolean contains(String name) { - return contains(DavPropertyName.create(name, DavConstants.NAMESPACE)); - } - - /** - * Return true if this property set is empty. - * - * @return true if the internal map contains no elements. - */ - public boolean isEmpty() { - return map.isEmpty(); - } - - /** - * Return the names of all properties present in this set. - * - * @return array of {@link DavPropertyName property names} present in this set. - */ - public DavPropertyName[] getPropertyNames() { - Set keySet = map.keySet(); - return (DavPropertyName[]) keySet.toArray(new DavPropertyName[keySet.size()]); - } - - //---------------------------------------------------------- Inner class --- - /** - * Implementation of a DavPropertyIterator that returns webdav property. - * Additionally, it can only return property with the given namespace. - */ - private class PropIter implements DavPropertyIterator { - - /** the namespace to match agains */ - private final Namespace namespace; - - /** the internal iterator */ - private final Iterator iterator; - - /** the next property to return */ - private DavProperty next; - - /** - * Creates a new property iterator. - */ - private PropIter() { - this(null); - } - - /** - * Creates a new iterator with the given namespace - * @param namespace The namespace to match against - */ - private PropIter(Namespace namespace) { - this.namespace = namespace; - iterator = map.values().iterator(); - seek(); - } - - /** - * @see DavPropertyIterator#nextProperty(); - */ - public DavProperty nextProperty() throws NoSuchElementException { - if (next==null) { - throw new NoSuchElementException(); - } - DavProperty ret = next; - seek(); - return ret; - } - - /** - * @see DavPropertyIterator#hasNext(); - */ - public boolean hasNext() { - return next!=null; - } - - /** - * @see DavPropertyIterator#next(); - */ - public Object next() { - return nextProperty(); - } - - /** - * @see DavPropertyIterator#remove(); - */ - public void remove() { - throw new UnsupportedOperationException(); - } - - /** - * Seeks for the next valid property - */ - private void seek() { - while (iterator.hasNext()) { - next = (DavProperty) iterator.next(); - if (namespace == null || namespace.equals(next.getName().getNamespace())) { - return; - } - } - next = null; - } - } -} - diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DefaultDavProperty.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DefaultDavProperty.java deleted file mode 100644 index ff533d5a741..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/DefaultDavProperty.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import org.apache.log4j.Logger; -import org.jdom.Namespace; -import org.jdom.Element; -import org.jdom.Content; -import org.jdom.Text; - -/** - * DefaultDavProperty... - */ -public class DefaultDavProperty extends AbstractDavProperty { - - private static Logger log = Logger.getLogger(DefaultDavProperty.class); - - /** - * the value of the property - */ - private final Object value; - - /** - * Creates a new WebDAV property with the given namespace, name and value. - * If the property is intended to be protected the isProtected flag must - * be set to true. - * - * @param name the name of the property - * @param value the value of the property - * @param namespace the namespace of the property - * @param isProtected A value of true, defines this property to be protected. - * It will not be returned in a {@link org.apache.jackrabbit.webdav.DavConstants#PROPFIND_ALL_PROP DAV:allprop} - * PROPFIND request and cannot be set/removed with a PROPPATCH request. - */ - public DefaultDavProperty(String name, Object value, Namespace namespace, boolean isProtected) { - super(DavPropertyName.create(name, namespace), isProtected); - this.value = value; - } - - /** - * Creates a new non-protected WebDAV property with the given namespace, name - * and value. - * - * @param name the name of the property - * @param value the value of the property - * @param namespace the namespace of the property - */ - public DefaultDavProperty(String name, Object value, Namespace namespace) { - this(name, value, namespace, false); - } - - /** - * Creates a new WebDAV property with the given DavPropertyName - * and value. If the property is meant to be protected the 'isProtected' - * flag must be set to true. - * - * @param name the name of the property - * @param value the value of the property - * @param isProtected A value of true, defines this property to be protected. - * It will not be returned in a {@link org.apache.jackrabbit.webdav.DavConstants#PROPFIND_ALL_PROP DAV:allprop} - * PROPFIND request and cannot be set/removed with a PROPPATCH request. - */ - public DefaultDavProperty(DavPropertyName name, Object value, boolean isProtected) { - super(name, isProtected); - this.value = value; - } - - /** - * Creates a new non- protected WebDAV property with the given - * DavPropertyName and value. - * - * @param name the name of the property - * @param value the value of the property - */ - public DefaultDavProperty(DavPropertyName name, Object value) { - this(name, value, false); - } - - /** - * Returns the value of this property - * - * @return the value of this property - */ - public Object getValue() { - return value; - } - - /** - * Create a new DefaultDavProperty instance from the given Xml - * element. Name and namespace of the element are building the {@link DavPropertyName}, - * while the element's content forms the property value. The following logic - * is applied: - *
-     * - empty Element           -> null value
-     * - single Text content     -> String value
-     * - single non-Text content -> Element.getContent(0) is used as value
-     * - other: List obtained from Element.getContent() is used as value
-     * 
- * - * @param propertyElement - * @return - */ - public static DefaultDavProperty createFromXml(Element propertyElement) { - if (propertyElement == null) { - throw new IllegalArgumentException("Cannot create a new DavProperty from a 'null' element."); - } - DavPropertyName name = DavPropertyName.createFromXml(propertyElement); - Object value; - int size = propertyElement.getContentSize(); - switch (size) { - case 0: - value = null; - break; - case 1: - Content c = propertyElement.getContent(0); - if (c instanceof Text) { - value = ((Text)c).getText(); - } else { - value = c; - } - break; - default: - value = propertyElement.getContent(); - } - return new DefaultDavProperty(name, value, false); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/HrefProperty.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/HrefProperty.java deleted file mode 100644 index ee9c5c5cbf4..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/HrefProperty.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.util.XmlUtil; -import org.jdom.Element; - -import java.util.List; -import java.util.Iterator; -import java.util.ArrayList; -import java.util.Arrays; - -/** - * HrefProperty is an extension to the common {@link DavProperty}. - * The String representation of the property value is always displayed as text - * inside an extra 'href' element. If the value is a String array each array - * element is added as text to a separate 'href' element. - * - * @see org.apache.jackrabbit.webdav.DavConstants#XML_HREF - * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() - */ -public class HrefProperty extends AbstractDavProperty { - - private static Logger log = Logger.getLogger(HrefProperty.class); - - private final String[] value; - - /** - * Creates a new WebDAV property with the given DavPropertyName - * - * @param name the name of the property - * @param value the value of the property - * @param isProtected A value of true, defines this property to be protected. - * It will not be returned in a {@link org.apache.jackrabbit.webdav.DavConstants#PROPFIND_ALL_PROP DAV:allprop} - * PROPFIND request and cannot be set/removed with a PROPPATCH request. - */ - public HrefProperty(DavPropertyName name, String value, boolean isProtected) { - super(name, isProtected); - this.value = new String[]{value}; - } - - /** - * Creates a new WebDAV property with the given DavPropertyName - * - * @param name the name of the property - * @param value the value of the property - * @param isProtected A value of true, defines this property to be protected. - * It will not be returned in a {@link org.apache.jackrabbit.webdav.DavConstants#PROPFIND_ALL_PROP DAV:allprop} - * PROPFIND request and cannot be set/removed with a PROPPATCH request. - */ - public HrefProperty(DavPropertyName name, String[] value, boolean isProtected) { - super(name, isProtected); - this.value = value; - } - - /** - * Create a new HrefProperty from the specified property. - * Please note, that the property must have a List value - * object, consisting of {@link #XML_HREF href} Element entries. - * - * @param prop - * @throws IllegalArgumentException if the property {@link DavProperty#getValue() value} - * is not a List. - */ - public HrefProperty(DavProperty prop) { - super(prop.getName(), prop.isProtected()); - if (! (prop.getValue() instanceof List)) { - throw new IllegalArgumentException("Expected a property with a List value object."); - } - Iterator it = ((List)prop.getValue()).iterator(); - ArrayList hrefList = new ArrayList(); - while (it.hasNext()) { - Object o = it.next(); - if (o instanceof Element) { - String href = ((Element)o).getChildText(XML_HREF, NAMESPACE); - if (href != null) { - hrefList.add(href); - } else { - log.warn("Valid DAV:href element expected instead of " + o.toString()); - } - } else { - log.warn("DAV: href element expected in the content of " + getName().toString()); - } - } - value = (String[]) hrefList.toArray(new String[hrefList.size()]); - } - - /** - * Returns an Xml element with the following form: - *
-     * <Z:name>
-     *    <DAV:href>value</DAV:href/>
-     * </Z:name>
-     * 
- * where Z: represents the prefix of the namespace defined with the initial - * webdav property name. - * - * @return Xml representation - * @see XmlUtil#hrefToXml(String) - */ - public Element toXml() { - Element elem = getName().toXml(); - Object value = getValue(); - if (value != null) { - if (value instanceof String[]) { - String[] hrefs = (String[]) value; - for (int i = 0; i < hrefs.length; i++) { - elem.addContent(XmlUtil.hrefToXml(hrefs[i])); - } - } else { - elem.addContent(XmlUtil.hrefToXml(value.toString())); - } - } - return elem; - } - - /** - * Returns an array of String. - * - * @return an array of String. - * @see DavProperty#getValue() - */ - public Object getValue() { - return value; - } - - /** - * Return an array of String containg the text of those DAV:href elements - * that would be returned as child elements of this property on {@link #toXml()} - * - * @return - */ - public List getHrefs() { - return (value != null) ? Arrays.asList(value) : new ArrayList(); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/ResourceType.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/ResourceType.java deleted file mode 100644 index 224e9943dde..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/ResourceType.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.property; - -import org.jdom.Element; - -/** - * The ResourceType class represents the webdav resource - * type property. Valid resource types are '{@link #COLLECTION collection}', - * {@link #DEFAULT_RESOURCE}. - */ -public class ResourceType extends AbstractDavProperty { - - /** - * The default resource type - */ - public static final int DEFAULT_RESOURCE = 0; - - /** - * The collection resource type - */ - public static final int COLLECTION = DEFAULT_RESOURCE + 1; - - private int resourceType = DEFAULT_RESOURCE; - - /** - * Create a resource type property - */ - public ResourceType(int resourceType) { - super(DavPropertyName.RESOURCETYPE, false); - if (!isValidResourceType(resourceType)) { - throw new IllegalArgumentException("Invalid resource type '"+ resourceType +"'."); - } - this.resourceType = resourceType; - } - - /** - * Return the JDOM element representation of this resource type - * - * @return a JDOM element - */ - public Element toXml() { - Element elem = getName().toXml(); - if (getValue() != null) { - elem.addContent((Element)getValue()); - } - return elem; - } - - /** - * Returns the Xml representation of this resource type. - * - * @return Xml representation of this resource type. - * @see DavProperty#getValue() - */ - public Object getValue() { - return (resourceType == COLLECTION) ? new Element(XML_COLLECTION, NAMESPACE) : null; - } - - /** - * Returns the resource type specified with the constructor. - * - * @return resourceType - */ - public int getResourceType() { - return resourceType; - } - - /** - * Validates the specified resourceType. - * - * @param resourceType - * @return true if the specified resourceType is valid. - */ - public boolean isValidResourceType(int resourceType) { - if (resourceType < DEFAULT_RESOURCE || resourceType > COLLECTION) { - return false; - } - return true; - } -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/package.html deleted file mode 100644 index 5794344d36d..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/property/package.html +++ /dev/null @@ -1,3 +0,0 @@ - -Interfaces and classes related to WebDAV properties. - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/QueryGrammerSet.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/QueryGrammerSet.java deleted file mode 100644 index 252d808b878..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/QueryGrammerSet.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.search; - -import org.apache.jackrabbit.webdav.property.DavProperty; -import org.apache.jackrabbit.webdav.property.AbstractDavProperty; -import org.jdom.Element; -import org.jdom.Namespace; - -import java.util.List; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * QueryGrammerSet is a {@link DavProperty} that - * encapsulates the 'supported-query-grammer-set' as defined by the - * Webdav SEARCH internet draft. - */ -public class QueryGrammerSet extends AbstractDavProperty implements SearchConstants { - - private List queryLanguages = new ArrayList(); - - /** - * Create a new QueryGrammerSet from the given query languages - * string array. The default {@link SearchConstants#NAMESPACE} is assumed. - * @param qLanguages - */ - public QueryGrammerSet(String[] qLanguages) { - super(QUERY_GRAMMER_SET, true); - if (qLanguages != null) { - for (int i = 0; i < qLanguages.length; i++) { - queryLanguages.add(new Element(qLanguages[i], SearchConstants.NAMESPACE)); - } - } - } - - /** - * Add another query language to this set. - * - * @param qLanguage - * @param namespace - */ - public void addQueryLanguage(String qLanguage, Namespace namespace) { - if (namespace == null) { - namespace = SearchConstants.NAMESPACE; - } - queryLanguages.add(new Element(qLanguage, namespace)); - } - - /** - * Return a String array containing the URIs of the query - * languages supported. - * - * @return names of the supported query languages - */ - public String[] getQueryLanguages() { - int size = queryLanguages.size(); - if (size > 0) { - String[] qLangStr = new String[size]; - Element[] elements = (Element[]) queryLanguages.toArray(new Element[size]); - for (int i = 0; i < elements.length; i++) { - qLangStr[i] = elements[i].getNamespaceURI() + elements[i].getName(); - } - return qLangStr; - } else { - return new String[0]; - } - } - - /** - * Return the Xml representation of this property according to the definition - * of the 'supported-query-grammer-set'. - * - * @return Xml representation - * @see SearchConstants#QUERY_GRAMMER_SET - * @see org.apache.jackrabbit.webdav.property.DavProperty#toXml() - */ - public Element toXml() { - Element elem = getName().toXml(); - Iterator qlIter = queryLanguages.iterator(); - while (qlIter.hasNext()) { - Element grammer = new Element(XML_GRAMMER, SearchConstants.NAMESPACE).addContent((Element)qlIter.next()); - Element sqg = new Element(XML_QUERY_GRAMMAR, SearchConstants.NAMESPACE).addContent(grammer); - elem.addContent(sqg); - } - return elem; - } - - /** - * Returns the list of supported query languages. - * - * @return list of supported query languages. - * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() - */ - public Object getValue() { - return queryLanguages; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchConstants.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchConstants.java deleted file mode 100644 index fe2ef5451fe..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchConstants.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.search; - -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Namespace; - -/** - * SearchConstants interface provide constants for request - * and response headers, Xml elements and property names used for WebDAV - * search. - */ -public interface SearchConstants { - - /** - * Namespace definition.
- * NOTE: For convenience reasons, the namespace is defined to be the default - * {@link DavConstants#NAMESPACE DAV:} namespace. This is not correct for the - * underlaying specification is still in a draft state. See also the editorial - * note inside the - * Internet Draft WebDAV Search - * document. - */ - public static final Namespace NAMESPACE = DavConstants.NAMESPACE; - - /** - * The DASL response header specifing the query languages supported by - * the requested resource. - */ - public static final String HEADER_DASL = "DASL"; - - /** - * Xml element name for a single query grammar element inside - * the {@link #QUERY_GRAMMER_SET supported-query-grammer-set property}. - */ - public static final String XML_QUERY_GRAMMAR = "supported-query-grammar"; - - /** - * Name constant for the 'DAV:grammar' element, which is used inside the - * {@link #XML_QUERY_GRAMMAR} element. - */ - public static final String XML_GRAMMER = "grammar"; - - /** - * Xml element name for the required request body of a SEARCH request. - * - * @see SearchRequest - * @see SearchResource#search(SearchRequest) - */ - public static final String XML_SEARCHREQUEST = "searchrequest"; - - /** - * Optional Xml element name used in the SEARCH request body instead of {@link XML_SEARCHREQUEST} - * in order to access a given query schema. - */ - public static final String XML_QUERY_SCHEMA_DISCOVERY = "query-schema-discovery"; - - - /** - * Predefined basic query grammer. - */ - public static final String BASICSEARCH = NAMESPACE.getPrefix()+"basicsearch"; - - /** - * Property indicating the set of query languages the given resource is - * able deal with. The property has the following definition:
- *
-     * <!ELEMENT supported-query-grammar-set (supported-query-grammar*)>
-     * <!ELEMENT supported-query-grammar grammar>
-     * <!ELEMENT grammar ANY>
-     * 
- */ - public static final DavPropertyName QUERY_GRAMMER_SET = DavPropertyName.create("supported-query-grammar-set", NAMESPACE); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchRequest.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchRequest.java deleted file mode 100644 index 1aa792fea07..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchRequest.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.search; - -import org.apache.log4j.Logger; -import org.jdom.*; - -/** - * SearchRequest parses the 'searchrequest' element of a SEARCH - * request body and performs basic validation. Both query language and the - * query itself can be access from the resulting object.
- * NOTE: The query is expected to be represented by the text contained in the - * Xml element specifying the query language, thus the 'basicsearch' defined - * by the Webdav Search Internet Draft is not supported by this implementation. - *

- * - * Example of a valid 'searchrequest' body - *

- * <d:searchrequest xmlns:d="DAV:" jcr:="http://www.day.com/jcr/webdav/1.0" >
- *    <jcr:xpath>//sv:node[@sv:name='myapp:paragraph'][1]</jcr:xpath>
- * </d:searchrequest>
- * 
- * - * Would return the following values: - *
- *    getLanguageName() -> xpath
- *    getQuery()    -> //sv:node[@sv:name='myapp:paragraph'][1]
- * 
- * - */ -public class SearchRequest implements SearchConstants { - - private static Logger log = Logger.getLogger(SearchRequest.class); - - private final Element language; - - /** - * Create a new SearchRequest from the specified element. - * - * @param searchRequest - * @throws IllegalArgumentException if the element's name is other than - * 'searchrequest' or if it does not contain a single child element specifying - * the query language to be used. - */ - public SearchRequest(Element searchRequest) { - if (searchRequest == null || !XML_SEARCHREQUEST.equals(searchRequest.getName())) { - throw new IllegalArgumentException("The root element must be 'searchrequest'."); - } else if (searchRequest.getChildren().size() != 1) { - throw new IllegalArgumentException("A single child element is expected with the 'searchrequest'."); - } - Element child = (Element)searchRequest.getChildren().get(0); - language = (Element) child.detach(); - } - - /** - * Create a new SearchRequest from the specifying document - * retrieved from the request body. - * - * @param searchDocument - * @see #SearchRequest(Element) - */ - public SearchRequest(Document searchDocument) { - this(searchDocument.getRootElement()); - } - - /** - * Returns the name of the query language to be used. - * - * @return name of the query language - */ - public String getLanguageName() { - return language.getName(); - } - - /** - * Returns the namespace of the language specified with the search request element. - * - * @return namespace of the requestes language. - */ - public Namespace getLanguageNameSpace() { - return language.getNamespace(); - } - - /** - * Return the query string. - * - * @return query string - */ - public String getQuery() { - return language.getText(); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchResource.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchResource.java deleted file mode 100644 index 1ac328f0210..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/SearchResource.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.search; - -import org.apache.jackrabbit.webdav.MultiStatus; -import org.apache.jackrabbit.webdav.DavException; - -/** - * SearchResource defines METHODS required in order to handle - * a SEARCH request. - */ -public interface SearchResource { - - /** - * No extra compliance class defined by the Webdav Search spec. - * Instead an extra DASL header is included. - */ - public String COMPLIANCE_CLASS = ""; - - /** - * The 'SEARCH' method - */ - public String METHODS = "SEARCH"; - - - /** - * Returns the protected DAV:supported-method-set property which is defined - * mandatory by RTF 3253. This method call is a shortcut for - * DavResource.getProperty(SearchConstants.QUERY_GRAMMER_SET). - * - * @return the DAV:supported-query-grammer-set - * @see SearchConstants#QUERY_GRAMMER_SET - */ - public QueryGrammerSet getQueryGrammerSet(); - - /** - * Runs a search with the language and query defined in the {@link SearchRequest} - * object specified and returns a {@link MultiStatus} object listing the - * results. - * - * @param sRequest SearchRequest element encapsulating the SEARCH - * request body. - * @return MultiStatus object listing the results. - * @throws DavException - */ - public MultiStatus search(SearchRequest sRequest) throws DavException; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/package.html deleted file mode 100644 index a42961cb3af..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/search/package.html +++ /dev/null @@ -1,5 +0,0 @@ - -Contains interfaces and classes used to cover the functionality defined by the -Internet -Draft WebDAV Search. - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/statuscode.properties b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/statuscode.properties deleted file mode 100644 index 94a5e6e40bc..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/statuscode.properties +++ /dev/null @@ -1,47 +0,0 @@ -100=Continue -101=Switching Protocols -102=Processing -200=OK -201=Created -202=Accepted -203=Non-Authoritative Information -204=No Content -205=Reset Content -206=Partial Content -207=Multi-Status -300=Multiple Choices -301=Moved Permanently -302=Found -303=See Other -304=Not Modified -305=Use Proxy -307=Temporary Redirect -400=Bad Request -401=Unauthorized -402=Payment Required -403=Forbidden -404=Not Found -405=Method Not Allowed -406=Not Acceptable -407=Proxy Authentication Required -408=Request Time-out -409=Conflict -410=Gone -411=Length Required -412=Precondition Failed -413=Request Entity Too Large -414=Request-URI Too Large -415=Unsupported Media Type -416=Requested range not satisfiable -417=Expectation Failed -420=Method Failure -422=Unprocessable Entity -423=Locked -424=Failed Dependency -500=Internal Server Error -501=Not Implemented -502=Bad Gateway -503=Service Unavailable -504=Gateway Time-out -505=HTTP Version not supported -507=Insufficient Storage diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionConstants.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionConstants.java deleted file mode 100644 index 5e0ea71d745..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionConstants.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.transaction; - -import org.apache.jackrabbit.webdav.DavConstants; -import org.apache.jackrabbit.webdav.lock.Type; -import org.apache.jackrabbit.webdav.lock.Scope; -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.jdom.Namespace; - -/** - * TransactionConstants interface provide constants for request - * and response headers, Xml elements and property names used for handling - * transactions over WebDAV. There exists no public standard for this functionality. - * - * todo: 'local' and 'global' are not accurate terms in the given context > replace - */ -public interface TransactionConstants { - - /** - * Namespace for transaction related xml elements - */ - public static final Namespace NAMESPACE = Namespace.getNamespace("jcr", "http://www.day.com/jcr/webdav/1.0"); - - /** - * TransactionId Header - */ - public static final String HEADER_TRANSACTIONID = "TransactionId"; - - /** - * transaction XML element
- * Used as element inside the {@link DavConstants#XML_LOCKTYPE locktype} - * element. - * @see DavConstants#XML_LOCKTYPE - */ - public static final String XML_TRANSACTION = "transaction"; - - /** - * global XML element
- * Used as element inside of the {@link DavConstants#XML_LOCKSCOPE lockscope} element. - * It indicates the transaction to be global (e.g. a JCR transaction). - * - * @see DavConstants#XML_LOCKSCOPE - */ - public static final String XML_GLOBAL = "global"; - - /** - * local XML element
- * Used as element inside of the {@link DavConstants#XML_LOCKSCOPE lockscope} element. - * It indicates the transaction to be local (e.g. transient changes to - * a repository). - * - * @see DavConstants#XML_LOCKSCOPE - */ - public static final String XML_LOCAL = "local"; - - /** - * transactioninfo XML element
- * Mandatory element of the UNLOCK request body, if the unlock request - * is intended to complete a transaction. - */ - public static final String XML_TRANSACTIONINFO = "transactioninfo"; - - /** - * transactionstatus XML element
- * Mandatory element inside the {@link #XML_TRANSACTIONINFO transactioninfo} - * element indicating how the transaction should be completed. - * @see #XML_TRANSACTIONINFO - */ - public static final String XML_TRANSACTIONSTATUS = "transactionstatus"; - - /** - * commit XML element
- * Used as element inside of the {@link #XML_TRANSACTIONSTATUS transactionstatus} - * element. It indicates a completion by committing the transaction. - * @see #XML_TRANSACTIONSTATUS - */ - public static final String XML_COMMIT = "commit"; - - /** - * rollback XML element
- * Used as element inside of the {@link #XML_TRANSACTIONSTATUS transactionstatus} - * element. It indicates a completion by roll backing the transaction. - * @see #XML_TRANSACTIONSTATUS - */ - public static final String XML_ROLLBACK = "rollback"; - - /** - * String defining the 'isnew' property, that identifies a {@link TransactionResource} - * to be new within the given local transaction, meaning that it exists only in - * transient storage. This property is not defined by any of the Webdav RTFs. - * @see javax.jcr.Item#isNew() - * @see #XML_LOCAL - */ - public static final DavPropertyName ISNEW = DavPropertyName.create("isnew", NAMESPACE); - - /** - * String defining the 'ismodified' property, that is present on any {@link TransactionResource} - * that has been modified whithout the corresponding local transaction - * being completed yet. This property is not defined by any of the Webdav RTFs. - * @see javax.jcr.Item#isModified() - * @see #XML_LOCAL - */ - public static final DavPropertyName ISMODIFIED = DavPropertyName.create("ismodified", NAMESPACE); - - /** - * "transaction" lock type constant. - * @see #XML_TRANSACTION - * @see Type#create(String, org.jdom.Namespace) - */ - public static final Type TRANSACTION = Type.create(XML_TRANSACTION, TransactionConstants.NAMESPACE); - - /** - * "local" lock scope constant. - * - * @see #XML_LOCAL - * @see Scope#create(String, org.jdom.Namespace) - */ - public static final Scope LOCAL = Scope.create(XML_LOCAL, TransactionConstants.NAMESPACE); - - /** - * "global" lock scope constant. - * - * @see #XML_GLOBAL - * @see Scope#create(String, org.jdom.Namespace) - */ - public static final Scope GLOBAL = Scope.create(XML_GLOBAL, TransactionConstants.NAMESPACE); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionDavServletRequest.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionDavServletRequest.java deleted file mode 100644 index 22cc8c68584..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionDavServletRequest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.transaction; - -import org.apache.jackrabbit.webdav.DavServletRequest; - -/** - * TransactionDavServletRequest provides extensions to the - * {@link DavServletRequest} interface used for dealing with transaction lock - * requests. - */ -public interface TransactionDavServletRequest extends DavServletRequest { - - /** - * Retrieve the 'transactioninfo' request body that must be included with - * the UNLOCK request of a transaction lock. If the request body is does not - * provide the information required (either because it is missing or the - * Xml is not valid) null is returned. - * - * @return TransactionInfo object encapsulating the 'transactioninfo' - * Xml element present in the request body or null if no - * body is present or if it could not be parsed. - */ - public TransactionInfo getTransactionInfo(); - - - /** - * Retrieve the transaction id from the - * {@link TransactionConstants#HEADER_TRANSACTIONID TransactionId header}. - * - * @return transaction id as present in the {@link TransactionConstants#HEADER_TRANSACTIONID TransactionId header} - * or null. - */ - public String getTransactionId(); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionInfo.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionInfo.java deleted file mode 100644 index d35a4b4d41a..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionInfo.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.transaction; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -/** - * TransactionInfo class encapsultes the information present - * in the {@link #XML_TRANSACTIONINFO} element that forms the request body of - * the UNLOCk request for a transaction lock. - * - * @see TransactionConstants#XML_TRANSACTIONINFO - * @see TransactionConstants#XML_TRANSACTION - */ -public class TransactionInfo implements TransactionConstants { - - private static Logger log = Logger.getLogger(TransactionInfo.class); - - private Element status; - - /** - * Creates a TransactionInfo object from the given 'transactionInfo' - * element. The 'transactionInfo' must have the following form: - *
-     *
-     *  <!ELEMENT transactioninfo (transactionstatus) >
-     *  <!ELEMENT transactionstatus ( commit | rollback ) >
-     *  <!ELEMENT commit EMPTY >
-     *  <!ELEMENT rollback EMPTY >
-     * 
- * @param transactionInfo as present in the UNLOCK request body. - * @throws IllegalArgumentException if the given transactionInfo element - * is not valid. - */ - public TransactionInfo(Element transactionInfo) { - if (transactionInfo == null || !XML_TRANSACTIONINFO.equals(transactionInfo.getName())) { - throw new IllegalArgumentException("transactionInfo element expected."); - } - Element tStatus = transactionInfo.getChild(XML_TRANSACTIONSTATUS, NAMESPACE); - if (tStatus == null) { - throw new IllegalArgumentException("transactionInfo must contain a single 'jcr:transactionstatus' element."); - } - - // retrieve status: commit or rollback - status = tStatus.getChild(XML_COMMIT, NAMESPACE); - if (status == null) { - status = tStatus.getChild(XML_ROLLBACK, NAMESPACE); - } - - if (status == null) { - throw new IllegalArgumentException("'jcr:transactionstatus' element must contain either a '" + XML_COMMIT + "' or a '" + XML_ROLLBACK + "' elements."); - } - } - - /** - * Returns either 'commit' or 'rollback' with are the only allowed status - * types. - * - * @return 'commit' or 'rollback' - * @see #XML_COMMIT - * @see #XML_ROLLBACK - */ - public String getStatus() { - return status.getName(); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxLockEntry.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxLockEntry.java deleted file mode 100644 index 0fb612b28e3..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxLockEntry.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.transaction; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.lock.Type; -import org.apache.jackrabbit.webdav.lock.Scope; -import org.apache.jackrabbit.webdav.lock.AbstractLockEntry; - -/** - * TxLockEntry represents the lock entry objects allowed for - * a transaction lock. - */ -public final class TxLockEntry extends AbstractLockEntry implements TransactionConstants { - - private static Logger log = Logger.getLogger(TxLockEntry.class); - - private final Scope scope; - - /** - * Create a lock entry that identifies transaction lock. - * - * @param isLocal boolean value indicating whether this is a local or a global - * lock entry. - */ - public TxLockEntry(boolean isLocal) { - if (isLocal) { - scope = LOCAL; - } else { - scope = GLOBAL; - } - } - - /** - * Returns the {@link #TRANSACTION 'transaction'} lock type. - * - * @return always returns the 'transaction' type. - * @see org.apache.jackrabbit.webdav.lock.LockEntry#getType() - * @see #TRANSACTION - */ - public Type getType() { - return TRANSACTION; - } - - /** - * Returns either {@link #LOCAL local} or {@link #GLOBAL global} scope - * depending on the initial construtor value. - * - * @return returns 'global' or 'local' scope. - * @see org.apache.jackrabbit.webdav.lock.LockEntry#getScope() - * @see #GLOBAL - * @see #LOCAL - */ - public Scope getScope() { - return scope; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxLockManager.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxLockManager.java deleted file mode 100644 index c9eff6a161b..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxLockManager.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2005 Your Corporation. All Rights Reserved. - */ -package org.apache.jackrabbit.webdav.transaction; - -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.lock.*; - -/** - * TxLockManager manages locks with locktype - * '{@link TransactionConstants#TRANSACTION jcr:transaction}'. - * - * todo: removing all expired locks - * todo: 'local' and 'global' are not accurate terms in the given context > replace - * todo: the usage of the 'global' transaction is not according to the JTA specification, - * which explicitely requires any transaction present on a servlet to be completed before - * the service method returns. Starting/completing transactions on the session object, - * which is possible with the jackrabbit implementation is a hack. - * todo: review of this transaction part is therefore required. Is there a use-case - * for those 'global' transactions at all... - */ -public interface TxLockManager extends LockManager { - - - /** - * Release the lock identified by the given lock token. - * - * @param lockInfo - * @param lockToken - * @param resource - * @throws org.apache.jackrabbit.webdav.DavException - */ - public void releaseLock(TransactionInfo lockInfo, String lockToken, - TransactionResource resource) throws DavException; - - - /** - * Return the lock applied to the given resource or null - * - * @param type - * @param scope - * @param resource - * @return lock applied to the given resource or null - * @see org.apache.jackrabbit.webdav.lock.LockManager#getLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope, org.apache.jackrabbit.webdav.DavResource) - */ - public ActiveLock getLock(Type type, Scope scope, TransactionResource resource); - - -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/package.html deleted file mode 100644 index a552dc6ab13..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/package.html +++ /dev/null @@ -1,3 +0,0 @@ - -Contains interfaces and classes related to transaction locks. - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/Text.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/Text.java deleted file mode 100644 index 4c7e07c2de2..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/Text.java +++ /dev/null @@ -1,2042 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.util; - -import java.io.ByteArrayOutputStream; -import java.io.UnsupportedEncodingException; -import java.math.BigDecimal; -import java.security.NoSuchAlgorithmException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; - -/** - * This class holds a collection of string utility operations. - */ -public final class Text extends org.apache.jackrabbit.core.util.Text { - - /** - * The default format pattern used in strftime() if no pattern - * parameter has been supplied. This is the default format used to format - * dates in Communiqué 2 - */ - public static final String DEFAULT_DATE_FORMAT_PATTERN = "dd.MM.yyyy HH:mm:ss"; - - /** - * Common used DateFormat implementation. When the supplied formatting - * pattern has been translated it is applied to this formatter and then - * executed. Both steps occur in a synchronized section, such that no - * two threads disturb each other. - *
    - *
  1. A single instance of the formatter is used to prevent the object - * creation overhead. But then how much is this ? - *
  2. Using one static formatter for each thread may prove to give even - * more overhead given all those synchronized blocks waiting for each - * other during real formatting. But then how knows ? - *
- *

- * This formatter must always be used synchronized as follows : - *

-	    synchronized (dateFormatter) {
-		dateFormatter.applyPattern( ... );
-		dateFormatter.setTimezone( ... );
-		String result = dateFormatter.format( ... );
-	    }
-     *	
- *

- * To parse date and time strings , the formatter is used as follows : - *

-	    synchronized (dateFormatter) {
-		dateFormatter.applyPattern( ... );
-		dateFormatter.setTimezone( ... );
-	        try {
-		    Date result = dateFormatter.parse(dateString);
-	        } catch (ParseException pe) {
-		    // handle exception
-	        }
-	    }
-     *	
- * - */ - private static final SimpleDateFormat dateFormatter = new SimpleDateFormat(); - - /** - * The UTC timezone - */ - public static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone("UTC"); - - /** format for RFC 1123 date string -- "Sun, 06 Nov 1994 08:49:37 GMT" */ - private final static SimpleDateFormat rfc1123Format = - new SimpleDateFormat("EEE, dd MMM yyyyy HH:mm:ss z", Locale.US); - - static { - rfc1123Format.setTimeZone(TIMEZONE_UTC); - } - - /** - * The local timezone - */ - public static final TimeZone TIMEZONE_LOCAL = TimeZone.getDefault(); - - /** - * Empty result - */ - private final static String[] empty = new String[0]; - - /** - * avoid instantiation - */ - private Text() { - } - - /** - * returns an array of strings decomposed of the original string, split at - * every occurance of 'ch'. if 2 'ch' follow each other with no intermediate - * characters, empty "" entries are avoided. - * - * @param str the string to decompose - * @param ch the character to use a split pattern - * @return an array of strings - */ - public static String[] explode(String str, int ch) { - return explode(str,ch,false); - } - - /** - * returns an array of strings decomposed of the original string, split at - * every occurance of 'ch'. - * @param str the string to decompose - * @param ch the character to use a split pattern - * @param respectEmpty if true, empty elements are generated - * @return an array of strings - */ - public static String[] explode(String str, int ch, boolean respectEmpty) { - if (str == null) { - return empty; - } - - Vector strings = new Vector(); - int pos = 0; - int lastpos = 0; - - // add snipples - while ((pos = str.indexOf(ch, lastpos)) >= 0) { - if (pos-lastpos>0 || respectEmpty) - strings.add(str.substring(lastpos, pos)); - lastpos = pos+1; - } - // add rest - if (lastpos < str.length()) { - strings.add(str.substring(lastpos)); - } else if (respectEmpty && lastpos==str.length()) { - strings.add(""); - } - - // return stringarray - return (String[]) strings.toArray(new String[strings.size()]); - } - - public static String implode(String[] arr, String delim) { - StringBuffer buf = new StringBuffer(); - for (int i=0; i0) { - buf.append(delim); - } - buf.append(arr[i]); - } - return buf.toString(); - } - - /** - * compares to handles lexigographically with one exception: the '/' - * character is always considered smaller than all other chars. this results - * in a ordering, in which the parent pages come first (it's about 6 times - * slower than the string impl. of compareTo). - *
    example (normal string compare): - *
  • /foo - *
  • /foo-bar - *
  • /foo/bar
  • - *
- *
    example (this handle compare): - *
  • /foo - *
  • /foo/bar
  • - *
  • /foo-bar - *
- * - * @param h1 the first handle - * @param h2 the second handle - * @return the return is positive, if the first handle is bigger than the - * second; negative, if the first handle is smaller than the second; - * and zero, if the two handles are equal. - */ - public static int comparePaths(String h1, String h2) { - char[] ca1=h1.toCharArray(); // this is faster, than a .charAt everytime - char[] ca2=h2.toCharArray(); - int n= ca1.length < ca2.length ? ca1.length : ca2.length; - int i=0; - while (iparent as parent directory rather than a base - * handle. if further respects full qualified uri's. - *
examples: - * - * parent | path | result - * ----------+----------+------------ - * "" | "" | / - * /foo | "" | /foo - * "" | /foo | /foo - * "." | foo | foo - * /foo/bar | bla | /foo/bar/bla - * /foo/bar | /bla | /bla - * /foo/bar | ../bla | /foo/bla - * /foo/bar | ./bla | /foo/bar/bla - * foo | bar | foo/bar - * c:/bla | gurk | c:/bla/gurk - * /foo | c:/bar | c:/bar - * - * - * @param parent the base handle - * @param path the path - */ - public static String fullFilePath(String parent, String path) { - if (parent==null) parent=""; - if (path==null) path=""; - - // compose source string - StringBuffer source; - if (path.equals("") || (path.charAt(0)!='/' && path.indexOf(':')<0)) { - // relative - source = new StringBuffer(parent); - if (!path.equals("")) { - source.append("/./"); - source.append(path); - } - } else { - // absolute - source = new StringBuffer(path==null?"":path); - } - return makeCanonicalPath(source); - } - - /** - * returns a full path. - * if base is empty, '/' is assumed - * if base and path are relative, a relative path will be generated. - *
examples: - *
-     * base      | path     | result
-     * ----------+----------+------------
-     * ""        | ""       | /
-     * /foo      | ""       | /foo
-     * ""        | /foo     | /foo
-     * "."       | foo      | foo
-     * /foo/bar  | bla      | /foo/bla
-     * /foo/bar  | /bla     | /bla
-     * /foo/bar  | ../bla   | /bla
-     * /foo/bar  | ./bla    | /foo/bla
-     * foo       | bar      | bar
-     * 
- * - * @param base the base handle - * @param path the path - */ - public static String fullPath(String base, String path) { - if (base==null) base=""; - if (path==null) path=""; - - // compose source string - StringBuffer source; - if (path.equals("") || path.charAt(0)!='/') { - // relative - source = new StringBuffer(base); - if (!path.equals("")) { - source.append("/../"); - source.append(path); - } - } else { - // absolute - source = new StringBuffer(path==null?"":path); - } - return makeCanonicalPath(source); - } - - /** - * Make a path canonical. This is a shortcut for - * - * Text.makeCanonicalPath(new StringBuffer(path)); - * - * @param path path to make canonical - */ - public static String makeCanonicalPath(String path) { - return makeCanonicalPath(new StringBuffer(path)); - } - - /** - * make a cannonical path. removes all /./ and /../ and multiple slashes. - * @param source the input source - * @return a string containing the cannonical path - */ - public static String makeCanonicalPath(StringBuffer source) { - // remove/resolve .. and . - int dst=0, pos=0, slash=0, dots=0, last=0, len=source.length(); - int[] slashes=new int[1024]; - slashes[0]=source.charAt(0)=='/' ? 0 : -1; - while (pos=0) source.setCharAt(dst, (char) (last=ch)); - dst++; - } - // check dots again - if (dots == 1) { - dst = slashes[slash]; - } else if (dots == 2) { - if (slash > 0) { - slash--; - } - dst = slashes[slash]; - } - - // truncate result - if (dst>0) source.setLength(dst); - return dst==0 ? "/" : source.toString(); - } - - /** - * Determines, if two handles are sister-pages, that meens, if they - * represent the same hierarchic level and share the same parent page. - * @param h1 first handle - * @param h2 second handle - * @return true if on same level, false otherwise - */ - public static boolean isSibling(String h1, String h2) { - int pos1 = h1.lastIndexOf('/'); - int pos2 = h2.lastIndexOf('/'); - return (pos1==pos2 && pos1>=0 && h1.regionMatches(0,h2,0,pos1)); - } - - /** - * Determines if the descendant handle is hierarchical a - * descendant of handle. - * - * /content/playground/en isDescendantOf /content/playground - * /content/playground/en isDescendantOf /content - * /content/playground/en isNOTDescendantOf /content/designground - * /content/playground/en isNOTDescendantOf /content/playground/en - * - * - * @param handle the current handle - * @param descendant the potential descendant - * @return true if the descendant is a descendant; - * false otherwise. - * @since gumbear - */ - public static boolean isDescendant(String handle, String descendant) { - return !handle.equals(descendant) && - descendant.startsWith(handle) && - descendant.charAt(handle.length())=='/'; - } - - /** - * Determines if the descendant handle is hierarchical a - * descendant of handle or equal to it. - * - * /content/playground/en isDescendantOrEqualOf /content/playground - * /content/playground/en isDescendantOrEqualOf /content - * /content/playground/en isDescendantOrEqualOf /content/playground/en - * /content/playground/en isNOTDescendantOrEqualOf /content/designground - * - * - * @param path the path to check - * @param descendant the potential descendant - * @return true if the descendant is a descendant - * or equal; false otherwise. - * @since gumbear - */ - public static boolean isDescendantOrEqual(String path, String descendant) { - if (path.equals(descendant)) { - return true; - } else { - String pattern = path.endsWith("/") ? path : path + "/"; - return descendant.startsWith(pattern); - } - } - - /** - * Returns the label of a handle - * @param handle the handle - * @return the label - */ - public static String getLabel(String handle) { - int pos=handle.lastIndexOf('/'); - return pos>=0 ? handle.substring(pos+1) : ""; - } - - /** - * Returns the label of a string - * @param handle the string - * @param delim the delimiter - * @return the label - */ - public static String getLabel(String handle, char delim) { - int pos=handle.lastIndexOf(delim); - return pos>=0 ? handle.substring(pos+1) : ""; - } - - /** - * Digest the plain string using the given algorithm. - * - * @param algorithm The alogrithm for the digest. This algorithm must be - * supported by the MessageDigest class. - * @param data The plain text String to be digested. - * - * @return The digested plain text String represented as Hex digits. - * - * @throws NoSuchAlgorithmException if the desired algorithm is not supported by - * the MessageDigest class. - * - * @deprecated since echidna, use {@link #digest(String, String, String)} - */ - public static String digest(String algorithm, String data) - throws NoSuchAlgorithmException { - - return digest(algorithm, data.getBytes()); - } - - /** - * Returns the nth relative parent of the handle, where n=level. - *

Example:
- * - * Text.getRelativeParent("/en/home/index/about", 1) == "/en/home/index" - * - * - * @param handle the handle of the page - * @param level the level of the parent - */ - public static String getRelativeParent(String handle, int level) { - int idx = handle.length(); - while (level > 0) { - idx = handle.lastIndexOf('/',idx-1); - if (idx < 0) { - return ""; - } - level--; - } - return (idx == 0) ? "/" : handle.substring(0,idx); - } - - /** - * Returns the nth absolute parent of the handle, where n=level. - *

Example:
- * - * Text.getAbsoluteParent("/en/home/index/about", 1) == "/en/home" - * - * - * @param handle the handle of the page - * @param level the level of the parent - */ - public static String getAbsoluteParent(String handle, int level) { - int idx = 0; - int len = handle.length(); - while (level >= 0 && idx=0 ? "" : handle.substring(0,idx); - } - - /** - * The list of characters that are not encoded by the escape() - * and unescape() METHODS. They contains the characters as - * defined 'unreserved' in section 2.3 of the RFC 2396 'URI genric syntax': - * - *

-     * unreserved  = alphanum | mark
-     * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
-     * 
- */ - public static BitSet URISave; - - /** - * Same as {@link #URISave} but also contains the '/' - */ - public static BitSet URISaveEx; - - static { - URISave = new BitSet(256); - int i; - for (i = 'a'; i <= 'z'; i++) { - URISave.set(i); - } - for (i = 'A'; i <= 'Z'; i++) { - URISave.set(i); - } - for (i = '0'; i <= '9'; i++) { - URISave.set(i); - } - URISave.set('-'); - URISave.set('_'); - URISave.set('.'); - URISave.set('!'); - URISave.set('~'); - URISave.set('*'); - URISave.set('\''); - URISave.set('('); - URISave.set(')'); - - URISaveEx = (BitSet) URISave.clone(); - URISaveEx.set('/'); - } - - /** - * Does an URL encoding of the string using the - * escape character. The characters that don't need encoding - * are those defined 'unreserved' in section 2.3 of the 'URI genric syntax' - * RFC 2396, but without the escape character. - * - * @param string the string to encode. - * @param escape the escape character. - * @return the escaped string - * - * @throws NullPointerException if string is null. - */ - public static String escape(String string, char escape) { - return escape(string, escape, false); - } - - /** - * Does an URL encoding of the string using the - * escape character. The characters that don't need encoding - * are those defined 'unreserved' in section 2.3 of the 'URI genric syntax' - * RFC 2396, but without the escape character. If isPath is - * true, additionally the slash '/' is ignored, too. - * - * @param string the string to encode. - * @param escape the escape character. - * @param isPath if true, the string is treated as path - * - * @return the escaped string - * - * @throws NullPointerException if string is null. - */ - public static String escape(String string, char escape, boolean isPath) { - try { - BitSet validChars = isPath ? URISaveEx : URISave; - byte[] bytes = string.getBytes("utf-8"); - StringBuffer out = new StringBuffer(bytes.length); - for (int i = 0; i < bytes.length; i++) { - int c = bytes[i]&0xff; - if (validChars.get(c) && c!=escape) { - out.append((char)c); - } else { - out.append(escape); - out.append(hexTable[(c>>4) & 0x0f]); - out.append(hexTable[(c ) & 0x0f]); - } - } - return out.toString(); - } catch (UnsupportedEncodingException e) { - throw new InternalError(e.toString()); - } - } - - /** - * Does a URL encoding of the string. The characters that - * don't need encoding are those defined 'unreserved' in section 2.3 of - * the 'URI genric syntax' RFC 2396. - * - * @param string the string to encode - * @return the escaped string - * - * @throws NullPointerException if string is null. - */ - public static String escape(String string) { - return escape(string, '%'); - } - - /** - * Does a URL encoding of the path. The characters that - * don't need encoding are those defined 'unreserved' in section 2.3 of - * the 'URI genric syntax' RFC 2396. In contrast to the - * {@link #escape(String)} method, not the entire path string is escaped, - * but every individual part (i.e. the slashes are not escaped). - * - * @param path the path to encode - * @return the escaped path - * - * @throws NullPointerException if path is null. - */ - public static String escapePath(String path) { - return escape(path, '%', true); - } - - /** - * Does a URL decoding of the string using the - * escape character. Please note that in opposite to the - * {@link java.net.URLDecoder} it does not transform the + into spaces. - * @param string the string to decode - * @param escape the escape character - * @return the decoded string - * - * @throws NullPointerException if string is null. - * @throws ArrayIndexOutOfBoundsException if not enough character follow an - * escape character - * @throws IllegalArgumentException if the 2 characters following the escape - * character do not represent a hex-number. - */ - public static String unescape(String string, char escape) { - ByteArrayOutputStream out = new ByteArrayOutputStream(string.length()); - for (int i=0; istring
. Please note that in - * opposite to the {@link java.net.URLDecoder} it does not transform the + - * into spaces. - * - * @param string the string to decode - * @return the decoded string - * - * @throws NullPointerException if string is null. - * @throws ArrayIndexOutOfBoundsException if not enough character follow an - * escape character - * @throws IllegalArgumentException if the 2 characters following the escape - * character do not represent a hex-number. - */ - public static String unescape(String string) { - return unescape(string, '%'); - } - - /** - * Returns a stringified date accoring to the date format specified in - * RTF1123. this is of the form: "Sun, 06 Nov 1994 08:49:37 GMT" - */ - public static String dateToRfc1123String(Date date) { - synchronized (rfc1123Format) { - return rfc1123Format.format(date); - } - } - - /** - * Implements a date formatting routine supporting (a subset) of the POSIX - * strftime() function. - * - * @param date The date value to be formatted - * @param formatPattern The pattern used to format the date. This pattern - * supports a subset of the pattern characters of the POSIX - * strftime() function. If this pattern is empty or - * null the default pattern - * dd.MM.yyyy HH:mm:ss is used. - * @param zone Defines for which time zone the date should be outputted. If - * this parameter is null, then the local time zone is taken. - * - * @return the formatted date as a String. - */ - public static final String strftime(Date date, String formatPattern, TimeZone zone) { - // Check whether to apply default format - if (formatPattern == null || formatPattern.length() == 0) { - formatPattern = DEFAULT_DATE_FORMAT_PATTERN; - } else { - formatPattern = convertFormat(formatPattern); - } - - // check zone - if (zone == null) zone = TIMEZONE_LOCAL; - - // Reuse the global SimpleDateFormat synchronizing on it to prevent - // multiple tasks from interfering with each other - synchronized(dateFormatter) { - dateFormatter.applyPattern(formatPattern); - dateFormatter.setTimeZone(zone); - return dateFormatter.format(date); - } - } - - /** - * Implements a date formatting routine supporting (a subset) of the POSIX - * strftime() function. - * - * @param date The date value to be formatted - * @param formatPattern The pattern used to format the date. This pattern - * supports a subset of the pattern characters of the POSIX - * strftime() function. If this pattern is empty or - * null the default pattern - * dd.MM.yyyy HH:mm:ss is used. - * @param asUTC Defines whether to interpret the date as belong to the UTC - * time zone or the local time zone. - */ - public static final String strftime(Date date, String formatPattern, boolean asUTC) { - return strftime(date, formatPattern, asUTC ? TIMEZONE_UTC : TIMEZONE_LOCAL); - } - - /** - * Implements a date formatting routine supporting (a subset) of the POSIX - * strftime() function. The default pattern - * dd.MM.yyyy HH:mm:ss is used to format the date. - * - * @param date The date value to be formatted - * @param asUTC Defines whether to interpret the date as belong to the UTC - * time zone or the local time zone. - */ - public static final String strftime(Date date, boolean asUTC) { - return strftime(date, null, asUTC); - } - - /** - * Implements a date formatting routine supporting (a subset) of the POSIX - * strftime() function. The default pattern - * dd.MM.yyyy HH:mm:ss is used to format the date, which is - * interpreted to be in the local time zone. - * - * @param date The date value to be formatted - */ - public static final String strftime(Date date) { - return strftime(date, null, false); - } - - /** - * Parses the date string based on the format pattern which is in the - * format used by the Java platfrom SimpleDateFormat class. - * - * @param dateString The date string to be parsed - * @param formatPattern the pattern to use for parsing. If null - * or empty, the same default pattern is used as with - * {@link #strftime(Date, String, boolean)}, namely - * dd.MM.yyyy HH:mm:ss. - * - * @throws ParseException if the date string cannot be parsed accordinung - * to the format pattern. - */ - public static final Date parseDate(String dateString, String formatPattern) - throws ParseException { - - return parseDate(dateString, formatPattern, false); - } - - /** - * Parses the date string based on the format pattern which is in the - * format used by the Java platfrom SimpleDateFormat class. - * - * @param dateString The date string to be parsed - * @param formatPattern the pattern to use for parsing. If null - * or empty, the same default pattern is used as with - * {@link #strftime(Date, String, boolean)}, namely - * dd.MM.yyyy HH:mm:ss. - * @param isUTC if true the date string is considered in UTC, - * otherwise the default timezone of the host is used. - * - * @throws ParseException if the date string cannot be parsed accordinung - * to the format pattern. - */ - public static final Date parseDate(String dateString, String formatPattern, - boolean isUTC) - throws ParseException { - - synchronized (dateFormatter) { - dateFormatter.applyPattern(formatPattern); - if (isUTC) { - dateFormatter.setTimeZone(TIMEZONE_UTC); - } else { - dateFormatter.setTimeZone(TimeZone.getDefault()); - } - return dateFormatter.parse(dateString); - } - } - - /** - * Parses the date string based on the format pattern which is in the - * default format dd.MM.yyyy HH:mm:ss. - * - * @param dateString The date string to be parsed - * - * @throws ParseException if the date string cannot be parsed accordinung - * to the format pattern. - */ - public static final Date parseDate(String dateString) throws ParseException { - return parseDate(dateString, DEFAULT_DATE_FORMAT_PATTERN, false); - } - - /** - * Parses the date string based on the format pattern which is in the - * default format dd.MM.yyyy HH:mm:ss. - * - * @param dateString The date string to be parsed - * @param isUTC if true the date string is considered in UTC, - * otherwise the default timezone of the host is used. - * - * - * @throws ParseException if the date string cannot be parsed accordinung - * to the format pattern. - */ - public static final Date parseDate(String dateString, boolean isUTC) - throws ParseException { - return parseDate(dateString, DEFAULT_DATE_FORMAT_PATTERN, isUTC); - } - - //--------- sprintf() formatting constants --------------------------------- - - /** left justified - '-' flag */ - private static final int FLAG_LJ = 1; - - /** always show sign - '+' flag */ - private static final int FLAG_SI = 2; - - /** space placeholder for omitted plus sign - ' ' flag, ignore if SI */ - private static final int FLAG_SP = 3; - - /** zero padded if right aligned - '0' flag, ignore if LJ */ - private static final int FLAG_ZE = 4; - - /** - * alternate format - '#' flag : - * SI - incr. precision to have zero as first char - * x - prefix '0x' - * X - prefix '0X' - * eEf - always show decimal point, omit trailing zeroes - * gG - always show decimal point, show trailing zeroes - */ - private static final int FLAG_AL = 5; - - /** interpret ints as short - 'h' size */ - private static final int FLAG_SHORT = 8; - - /** interpret ints as long - 'l' size */ - private static final int FLAG_LONG = 9; - - /** waiting for format */ - private static final int PARSE_STATE_NONE = 0; - - /** parsing flags */ - private static final int PARSE_STATE_FLAGS = 1; - - /** parsing wdth */ - private static final int PARSE_STATE_WIDTH = 2; - - /** parsing precision */ - private static final int PARSE_STATE_PRECISION = 3; - - /** parsing size */ - private static final int PARSE_STATE_SIZE = 4; - - /** parsing type */ - private static final int PARSE_STATE_TYPE = 5; - - /** parsing finished */ - private static final int PARSE_STATE_END = 6; - - /** incomplete pattern at end of format string, throw error */ - private static final int PARSE_STATE_ABORT = 7; - - /** end of format string during plain text */ - private static final int PARSE_STATE_TERM = 8; - - /** - * This method implements the famous and ubiquituous sprintf - * formatter in Java. The arguments to the method are the formatting string - * and the list of arguments defined by the formatting instructions - * contained in the formatting string. - *

- * Each element in the argument array is either a Number object - * or any other Object type. Whenever the formatting string - * stipulates the corresponding argument to be numeric, it is assumed this - * array element to be a Number. If this is not the case an - * IllegalArgumentException is thrown. If a String - * argument is stipulated by the formatting string, a simple call to the - * toString() method of the object yields the - * String required. - *

- * SPECIFICATION - *

- * sprintf accepts a series of arguments, applies to each a - * format specifier from format, and stores the formatted data - * to the result Strint. The method throws an - * IllegalArgumentException if either the format - * is incorrect, there are not enough arguments for the format - * or if any of the argument's type is wrong. sprintf returns - * when it reaches the end of the format string. If there are more - * arguments than the format requires, excess arguments are ignored. - *

- * format is a String containing two types of - * objects: ordinary characters (other than %), which - * are copied unchanged to the output, and conversion specifications, - * each of which is introduced by %. (To include - * % in the output, use %% in the format - * string.) A conversion specification has the following form: - *

- * - * [ flags ] [ width ] [ "." prec ] [ size ] - * type - * - *
- *

- * The fields of the conversion specification have the following meanings: - * - *

- *
flags - *
an optional sequence of characters which control output - * justification, numeric signs, decimal points, trailing zeroes, and - * octal and hex prefixes. The flag characters are minus (-), - * plus (+), space (" "), zero (0), - * and sharp (# ). They can appear in any combination. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
-The result of the conversion is left justified, and the right - * is padded with blanks. If you do not use this flag, the result - * is right justified, and padded on the left.
+The result of a signed conversion (as determined by type) - * will always begin with a plus or minus sign. (If you do not use - * this flag, positive values do not begin with a plus sign.)
" " (space)If the first character of a signed conversion specification is - * not a sign, or if a signed conversion results in no characters, - * the result will begin with a space. If the space - * ( ) flag and the plus (+) flag both - * appear, the space flag is ignored.
0If the type is d, i, - * o, u, x, X, - * e, E, f, g, - * or G: leading zeroes, are used to pad the field - * width (following any indication of sign or base); no spaces - * are used for padding. If the zero (0) and minus - * (-) flags both appear, the zero (0) - * flag will be ignored. For d, i, - * o, u, x, X - * conversions, if a precision prec is specified, the zero - * (0) flag is ignored. - *
- * Note that 0 is interpreted as a flag, not as the - * beginning of a field width.
#The result is to be converted to an alternative form, according - * to the type character: - *
- *
o - *
Increases precision to force the first digit of the result - * to be a zero. - * - *
x - *
A non-zero result will have a 0x prefix. - * - *
X - *
A non-zero result will have a 0X prefix. - * - *
e, E, or f - *
The result will always contain a decimal point even if no - * digits follow the point. (Normally, a decimal point appears - * only if a digit follows it.) Trailing zeroes are removed. - * - *
g or G - *
Same as e or E, but trailing - * zeroes arenot removed. - * - *
all others - *
Undefined. - *
- * - *
width - *
width is an optional minimum field width. You can either - * specify it directly as a decimal integer, or indirectly by using instead - * an asterisk (*), in which case an integral numeric argument - * is used as the field width. Negative field widths are not supported; if - * you attempt to specify a negative field width, it is interpreted as a - * minus (i) flag followed by a positive field width. - * - *
prec - *
an optional field; if present, it is introduced with - * `.' (a period). This field gives the maximum number of - * characters to print in a conversion; the minimum number of digits of an - * integer to print, for conversions with type d, - * i, o, u, x, and - * X; the maximum number of significant digits, for the - * g and G conversions; or the number of digits - * to print after the decimal point, for e, E, - * and f conversions. You can specify the precision either - * directly as a decimal integer or indirectly by using an asterisk - * (*), in which case an integral numeric argument is used as - * the precision. Supplying a negative precision is equivalent to - * omitting the precision. If only a period is specified the precision - * is zero. If a precision appears with any other conversion type - * than those listed here, the behavior is undefined. - * - *
size - *
h, l, and L are optional size - * characters which override the default way that sprintf - * interprets the data type of the corresponding argument. - * h forces the following d, i, - * o, u, x, or X - * conversion type to apply to a short or unsigned - * short. Similarily, an l forces the following - * d, i, o, u, - * x, or X conversion type to apply to - * a long or unsigned long. If an h - * or an l appears with another conversion specifier, the - * behavior is undefined. L forces a following - * e, E, f, g, or - * G conversion type to apply to a long - * double argument. If L appears with any other - * conversion type, the behavior is undefined. - * - *
type - *
type specifies what kind of conversion sprintf - * performs. Here is a table of these: - * - *
- *
% - *
prints the percent character (%) - * - *
c - *
prints arg as single character. That is the argument is - * converted to a String of which only the first - * character is printed. - * - *
s - *
Prints characters until precision is reached or the - * String ends; takes any Object whose - * toString() method is called to get the - * String to print. - * - *
d - *
prints a signed decimal integer; takes a Number (same - * as i) - * - *
d - *
prints a signed decimal integer; takes a Number (same - * as d) - * - *
d - *
prints a signed octal integer; takes a Number - * - *
u - *
prints a unsigned decimal integer; takes a Number. - * This conversion is not supported correctly by the Java - * implementation and is really the same as i. - * - *
x - *
prints an unsigned hexadecimal integer (using abcdef as - * digits beyond 9; takes a Number - * - *
X - *
prints an unsigned hexadecimal integer (using ABCDEF as - * digits beyond 9; takes a Number - * - *
f - *
prints a signed value of the form [-]9999.9999; takes a - * Number - * - *
e - *
prints a signed value of the form [-]9.9999e[+|-]999; takes - * a Number - * - *
E - *
prints the same way as e, but using E to - * introduce the exponent; takes a Number - * - *
g - *
prints a signed value in either f or e - * form, based on given value and precision &emdash; trailing zeros - * and the decimal point are printed only if necessary; takes a - * Number - * - *
G - *
prints the same way as g, but using E - * for the exponent if an exponent is needed; takes a - * Number - * - *
n - *
Not supported in the Java implementation, throws an - * IllegalArgumentException if used. - * - *
p - *
Not supported in the Java implementation, throws an - * IllegalArgumentException if used. - *
- * - *
- * - *

- * IMPLEMENTATION NOTES - *

- * Due to the nature of the Java programming language, neither pointer - * conversions, nor unsigned and long double - * conversions are supported. - *

- * Also the Java implementation only distinguishes between - * Number and other Object arguments. If a - * numeric argument is expected as per the format - * String, the current argument must be a Number - * object that is converted to a basic type as expected. If a - * String or char argument is expected, any - * Object is valid, whose toString() method is used to convert - * to a String. - * - * @param format The format string as known from the POSIX sprintf() - * C function. - * @param args The list of arguments to accomodate the format string. This - * argument list is supposed to contain at least as much entries as - * there are formatting options in the format string. If for a - * numeric option, the entry is not a number an - * IllegalArgumentException is thrown. - * - * @return The formatted String. An empty String - * is only returned if the format String - * is empty. A null value is never returned. - * - * @throws NullPointerException if the formatting string or any of the - * argument values is null. - * @throws IllegalArgumentException if the formatting string has wrong - * format tags, if the formatting string has an incomplete - * formatting pattern at the end of the string, if the argument - * vector has not enough values to satisfy the formatting string - * or if an argument's type does not match the requirements of the - * format string. - * - */ - public static String sprintf(String format, Object[] args) { - - // Return immediately if we have no arguments .... - if (format == null) { - throw new NullPointerException("format"); - } - - if (format.length() == 0) { - return ""; - } - - // Get the format string - char[] s = format.toCharArray(); - - // prepare the result, initial size has no sound basis - StringBuffer res = new StringBuffer( s.length * 3 / 2 ); - - for (int i=0,j=0, length=format.length(); i < length; ) { - - int parse_state = PARSE_STATE_NONE; - BitSet flags = new BitSet(16); - int width = 0; - int precision = -1; - char fmt = ' '; - - // find a start of a formatting ... - while (parse_state == PARSE_STATE_NONE) { - if (i >= length) parse_state = PARSE_STATE_TERM; - else if (s[i] == '%') { - if (i < length - 1) { - if (s[i + 1] == '%') { - res.append('%'); - i++; - } else { - parse_state = PARSE_STATE_FLAGS; - } - } else { - throw new java.lang.IllegalArgumentException( - "Incomplete format at end of format string"); - } - } else { - res.append(s[i]); - } - i++; - } - - // Get flags, if any - while (parse_state == PARSE_STATE_FLAGS) { - if (i >= length) parse_state = PARSE_STATE_ABORT; - else if (s[i] == ' ') flags.set(FLAG_SP); - else if (s[i] == '-') flags.set(FLAG_LJ); - else if (s[i] == '+') flags.set(FLAG_SI); - else if (s[i] == '0') flags.set(FLAG_ZE); - else if (s[i] == '#') flags.set(FLAG_AL); - else { - parse_state = PARSE_STATE_WIDTH; - i--; - } - i++; - } - - // Get width specification - while (parse_state == PARSE_STATE_WIDTH) { - if (i >= length) { - - parse_state = PARSE_STATE_ABORT; - - } else if ('0' <= s[i] && s[i] <= '9') { - - width = width * 10 + s[i] - '0'; - i++; - - } else { - // finished with digits or none at all - - // if width is a '*' take width from arg - if (s[i] == '*') { - // Check whether we have an argument - if (j >= args.length) { - throw new IllegalArgumentException("Missing " + - "argument for the width"); - } - try { - width = ((Number)(args[j++])).intValue(); - } catch (ClassCastException cce) { - // something wrong with the arg - throw new IllegalArgumentException("Width " + - "argument is not numeric"); - } - i++; - } - - // if next is a dot, then we have a precision, else a size - if (s[i] == '.') { - parse_state = PARSE_STATE_PRECISION; - precision = 0; - i++; - } else { - parse_state = PARSE_STATE_SIZE; - } - - } - } - - // Get precision - while (parse_state == PARSE_STATE_PRECISION) { - - if (i >= length) { - - parse_state = PARSE_STATE_ABORT; - - } else if ('0' <= s[i] && s[i] <= '9') { - - precision = precision * 10 + s[i] - '0'; - i++; - - } else { - // finished with digits or none at all - - // if width is a '*' take precision from arg - if (s[i] == '*') { - // Check whether we have an argument - if (j >= args.length) { - throw new IllegalArgumentException("Missing " + - "argument for the precision"); - } - try { - width = ((Number)(args[j++])).intValue(); - } catch (ClassCastException cce) { - // something wrong with the arg - throw new IllegalArgumentException("Precision " + - "argument is not numeric"); - } - i++; - } - - parse_state = PARSE_STATE_SIZE; - - } - - } - - // Get size character - if (parse_state == PARSE_STATE_SIZE) { - if (i >= length) parse_state = 6; - else { - if (s[i] == 'h') { - flags.set(FLAG_SHORT); - i++; - } else if (s[i] == 'l' || s[i] == 'L') { - flags.set(FLAG_LONG); - i++; - } - parse_state = PARSE_STATE_TYPE; - } - } - - // Get format character - if (parse_state == PARSE_STATE_TYPE) { - if (i >= length) parse_state = PARSE_STATE_ABORT; - else { - fmt = s[i]; - i++; - parse_state = PARSE_STATE_END; - } - } - - // Now that we have anything, format it .... - if (parse_state == PARSE_STATE_END) { - - // Check whether we have an argument - if (j >= args.length) { - throw new IllegalArgumentException("Not enough parameters for the format string"); - } - - try { - - // Convert the argument according to the flag - switch (fmt) { - case 'd': // decimal - fall through - case 'i': // integral - fall through - case 'x': // hexadecimal, lower case - fall through - case 'X': // hexadecimal, upper case - fall through - case 'o': // octal - fall through - case 'u': // unsigned (not really supported) - format(res, (Number)args[j], fmt, width, precision, - flags); - break; - - case 'f': // float - fall through - case 'e': // exponential, lower case - fall through - case 'E': // exponential, upper case - fall through - case 'g': // float or exp., lower case - fall through - case 'G': // float or exp., upper case - fall through - format(res, ((Number)args[j]).doubleValue(), fmt, - width, precision, flags); - break; - - case 'c': // character - precision = 1; - // fall through - - case 's': // string - - String val = args[j].toString(); - if (val.length() > precision && precision > 0) { - val = val.substring(0, precision); - } - - flags.clear(FLAG_ZE); - format(res, val, "", width, flags); - break; - - default : // unknown format - - throw new IllegalArgumentException("Unknown " + - "conversion type " + fmt); - - } - - } catch (ClassCastException cce) { - // something wrong with the arg - throw new IllegalArgumentException("sprintf: Argument #" + - j + " of type " + args[j].getClass().getName() + - " does not match format " + fmt); - } - - // goto the next argument - j++; - } - - // if the format string is not complete - if (parse_state == PARSE_STATE_ABORT) { - throw new java.lang.IllegalArgumentException( - "Incomplete format at end of format string"); - } - - } // while i - - return res.toString(); - } - - /** - * Formats a string according to format and argument. This method only - * supports string formats. See {@link #sprintf(String, Object[])} for details. - * - * @param format The format string - * @param a0 The single parameter - * - * @return the result from sprintf(format, new Object[]{ a0 }). - * - * @throws IllegalArgumentException from {@link #sprintf(String, Object[])}. - */ - public static String sprintf(String format, Object a0) { - return sprintf(format, new Object[]{a0}); - } - - /** - * Formats a string according to format and argument. This method only - * supports string formats. See {@link #sprintf(String, Object[])} for details. - * - * @param format The format string - * @param a0 The first parameter - * @param a1 The second parameter - * - * @return the result from sprintf(format, new Object[]{ ... }). - * - * @throws IllegalArgumentException from {@link #sprintf(String, Object[])}. - */ - public static String sprintf(String format, Object a0, Object a1) { - return sprintf(format, new Object[]{a0, a1}); - } - - /** - * Formats a string according to format and argument. This method only - * supports string formats. See {@link #sprintf(String, Object[])} for details. - * - * @param format The format string - * @param a0 The first parameter - * @param a1 The second parameter - * @param a2 The thrid parameter - * - * @return the result from sprintf(format, new Object[]{ ... }). - * - * @throws IllegalArgumentException from {@link #sprintf(String, Object[])}. - */ - public static String sprintf(String format, Object a0, Object a1, - Object a2) { - - return sprintf(format, new Object[]{a0, a1, a2}); - } - - /** - * Formats a string according to format and argument. This method only - * supports string formats. See {@link #sprintf(String, Object[])} for details. - * - * @param format The format string - * @param a0 The first parameter - * @param a1 The second parameter - * @param a2 The thrid parameter - * @param a3 The fourth parameter - * - * @return the result from sprintf(format, new Object[]{ ... }). - * - * @throws IllegalArgumentException from {@link #sprintf(String, Object[])}. - */ - public static String sprintf(String format, Object a0, Object a1, - Object a2, Object a3) { - - return sprintf(format, new Object[]{a0, a1, a2, a3}); - } - - /** - * Formats a string according to format and argument. This method only - * supports string formats. See {@link #sprintf(String, Object[])} for details. - * - * @param format The format string - * @param a0 The first parameter - * @param a1 The second parameter - * @param a2 The thrid parameter - * @param a3 The fourth parameter - * @param a4 The fifth parameter - * - * @return the result from sprintf(format, new Object[]{ ... }). - * - * @throws IllegalArgumentException from {@link #sprintf(String, Object[])}. - */ - public static String sprintf(String format, Object a0, Object a1, - Object a2, Object a3, Object a4) { - return sprintf(format, new Object[]{a0, a1, a2, a3, a4}); - } - - //---------- internal ------------------------------------------------------ - - /** - * Convert a Date formatting string in POSIX strftime() format to the - * pattern format used by the Java SimpleDateFormat class. - * - * These are the symbols used in SimpleDateFormat to which we convert - * our strftime() symbols. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
SymbolMeaningPresentationExample
Gera designator (Text) AD
yyear (Number) 1996
Mmonth in year (Text & Number)July & 07
dday in month (Number) 10
hhour in am/pm (1~12)(Number) 12
Hhour in day (0~23) (Number) 0
mminute in hour (Number) 30
ssecond in minute (Number) 55
Smillisecond (Number) 978
Eday in week (Text) Tuesday
Dday in year (Number) 189
Fday of week in month(Number) 2 (2nd Wed in July)
wweek in year (Number) 27
Wweek in month (Number) 2
aam/pm marker (Text) PM
khour in day (1~24) (Number) 24
Khour in am/pm (0~11)(Number) 0
ztime zone (Text) Pacific Standard Time
'escape for text (Delimiter) 
''single quote (Literal) '
- * - * @param posixFormat The formatting pattern in POSIX strftime() format. - * @return The Date formatting pattern in SimpleDateFormat pattern format. - * - * todo: Check for the more or less complete support of all pattern tags. - */ - private static String convertFormat(String posixFormat) { - char[] format = posixFormat.toCharArray(); - StringBuffer jFormat = new StringBuffer(format.length); - boolean inString = false; - - for (int i=0; isprintf()
method, that is, this method handles d, i, o, u, - * x, and X formatting characters. - * - * @param buf The formatted number is appended to this string buffer - * @param num The number object to format - * @param fmt The format character defining the radix of the number - * @param width The minimum field width for the number - * @param precision The minimum number of digits to print for the number, - * this does not include any signs or prefix characters - * @param flags The flags governing the formatting. This is a combination - * of the FLAG_* constants above. - * - * @return The formatted string - * - * @see sprintf() - */ - private static StringBuffer format(StringBuffer buf, Number num, - char fmt, int width, int precision, BitSet flags) { - - String numStr; - String prefStr = ""; - boolean toUpper = (fmt == 'X'); - - // Check precision and make default - if (precision >= 0) { - flags.clear(FLAG_ZE); - } else { - precision = 1; - } - - // Get the value and adjust size interpretation - long val; - long sizeMask; - if (flags.get(FLAG_SHORT)) { - val = num.shortValue(); - sizeMask = 0xffffL; - } else if (flags.get(FLAG_LONG)) { - val = num.longValue(); - sizeMask = 0xffffffffffffffffL; - } else { - val = num.intValue(); - sizeMask = 0xffffffffL; - } - - // check formatting type - if (fmt == 'x' || fmt == 'X') { - - numStr = Long.toHexString(val & sizeMask); - - if (toUpper) { - numStr = numStr.toUpperCase(); - } - - if (flags.get(FLAG_AL)) { - prefStr = toUpper ? "0X" : "0x"; - } - - } else if (fmt == 'o') { - - numStr = Long.toOctalString(val & sizeMask); - - if (flags.get(FLAG_AL) && val != 0 && precision <= numStr.length()) { - precision = numStr.length() + 1; - } - - } else { - - numStr = Long.toString(val); - - // move sign to prefix if negative, or set '+' - if (val < 0) { - prefStr = "-"; - numStr = numStr.substring(1); - } else if (flags.get(FLAG_SI)) { - prefStr = "+"; - } - - - } - - // prefix 0 for precision - if (precision > numStr.length()) { - StringBuffer tmp = new StringBuffer(precision); - for (precision -= numStr.length(); precision > 0; precision--) { - tmp.append('0'); - } - numStr = tmp.append(numStr).toString(); - } - - return format(buf, numStr, prefStr, width, flags); - } - - /** - * Implements the floating point number formatting part of the - * sprintf() method, that is, this method handles f, e, E, g, - * and G formatting characters. - * - * @param buf The formatted number is appended to this string buffer - * @param num The numeric value to format - * @param fmt The format character defining the floating point format - * @param width The minimum field width for the number - * @param precision Depending on fmt either the number of - * digits after the decimal point or the number of significant - * digits. - * @param flags The flags governing the formatting. This is a combination - * of the FLAG_* constants above. - * - * @return The formatted string - * - * @see sprintf() - */ - private static StringBuffer format(StringBuffer buf, double num, - char fmt, int width, int precision, BitSet flags) { - - BigDecimal val = new BigDecimal(num).abs(); - - // the exponent character, will be defined if exponent is needed - char expChar = 0; - - // the exponent value - int exp; - if (fmt != 'f') { - exp = val.unscaledValue().toString().length() - val.scale() - 1; - } else { - exp = 0; - } - - // force display of the decimal dot, if otherwise omitted - boolean needDot = (precision == 0 && flags.get(FLAG_AL)); - - // for fmt==g|G : treat trailing 0 and decimal dot specially - boolean checkTrails = false; - - // get a sensible precision value - if (precision < 0) { - precision = 6; - } - - switch (fmt) { - case 'G': // fall through - case 'g': - // decrement precision, to simulate significance - if (precision > 0) { - precision--; - } - - // we have to check trailing zeroes later - checkTrails = true; - - // exponent does not stipulate exp notation, break here - if (exp <= precision) { - precision -= exp; - break; - } - - // fall through for exponent handling - - case 'E': // fall through - case 'e': - // place the dot after the first decimal place - val = val.movePointLeft(exp); - - // define the exponent character - expChar = (fmt == 'e' || fmt == 'g') ? 'e' : 'E'; - - break; - } - - // only rescale if the precision is positive, may be negative - // for g|G - if (precision >= 0) { - val = val.setScale(precision, BigDecimal.ROUND_HALF_UP); - } - - // convert the number to a string - String numStr = val.toString(); - - // for g|G : check trailing zeroes - if (checkTrails) { - - if (flags.get(FLAG_AL)) { - - // need a dot, if not existing for alternative format - needDot |= (numStr.indexOf('.') < 0); - - } else { - // remove trailing dots and zeros - int dot = numStr.indexOf('.'); - if (dot >= 0) { - int i; - for (i=numStr.length()-1; i>=dot && numStr.charAt(i)=='0'; - i--); - // if stopped at dot, remove it - if (i > dot) { - i++; - } - numStr = numStr.substring(0, i); - } - } - } - - // Get a buffer with the number up to now - StringBuffer numBuf = new StringBuffer(numStr); - - // if we need a decimal dot, add it - if (needDot) { - numBuf.append('.'); - } - - // we have an exponent to add - if (expChar != 0) { - numBuf.append(expChar); - numBuf.append(exp < 0 ? '-' : '+'); - if (exp < 10) { - numBuf.append('0'); - } - numBuf.append(exp); - } - - // define the number's sign as the prefix for later formatting - String prefStr; - if (num < 0) { - prefStr = "-"; - } else if (flags.get(FLAG_SI)) { - prefStr = "+"; - } else { - prefStr = ""; - } - - // now format it and up we go - return format(buf, numBuf.toString(), prefStr, width, flags); - } - - /** - * Formats the String appending to the - * StringBuffer at least the String and - * justifying according to the flags. - *

- * The flags will be interpreted as follows :
- * * if {@link #FLAG_LJ} is set, append blanks for left justification
- * * else if {@link #FLAG_ZE} is set, insert zeroes between prefStr and str - * * else prepend blanks for right justification - * - * @param buf The StringBuffer to append the formatted result - * to. - * @param str The String to be appended with surrounding - * blanks, zeroes and prefStr depending on the - * flags. This is usually the real string to print - * like "ape" or "4.5E99". - * @param prefStr An optional prefix String to be appended - * in front of the String. This is usually the prefix - * string for numeric str values, for example - * "-", "0x", or "+". The reason for this separation is that the - * {@link #FLAG_ZE} flag will insert zeroes between the prefix and - * the string itself to fill the field to the width. - * @param width The minimal field width. If the field width is larger than - * the sum of the lengths of str and - * prefStr, blanks or zeroes will be prepended or - * appended according to the flags value. - * @param flags The flags indicating where blanks or zeroes will be filled. - * See above for the interpretation of flags. - * - * @throws NullPointerException if any of buf, - * str, prefStr or flags - * is null. - */ - private static StringBuffer format(StringBuffer buf, String str, - String prefStr, int width, BitSet flags) { - - int numFill = width - prefStr.length() - str.length(); - int preZero = 0; - int preBlank = 0; - int postBlank = 0; - - if (flags.get(FLAG_LJ)) { - postBlank = numFill; - } else if (flags.get(FLAG_ZE)) { - preZero = numFill; - } else { - preBlank = numFill; - } - - for ( ; preBlank > 0; preBlank--) buf.append(' '); - buf.append(prefStr); - for ( ; preZero > 0; preZero--) buf.append('0'); - buf.append(str); - for ( ; postBlank > 0; postBlank--) buf.append(' '); - - return buf; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/XmlUtil.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/XmlUtil.java deleted file mode 100644 index 56dc01bbe0d..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/XmlUtil.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.util; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Element; - -/** - * XmlUtil provides utility METHODS for building Xml representation. - */ -public class XmlUtil implements DavConstants { - - private static Logger log = Logger.getLogger(XmlUtil.class); - - /** - * Converts the given timeout (long value defining the number of milli- - * second until timeout is reached) to its Xml representation as defined - * by RTF 2518. - * - * @param timeout number of milli-seconds until timeout is reached. - * @return 'timeout' JDOM element - */ - public static Element timeoutToXml(long timeout) { - // TODO: check if 'infinite' would be better to return for infinite timeout. - String expString = "Second-"+ timeout/1000; - Element exp = new Element(XML_TIMEOUT, NAMESPACE); - exp.setText(expString); - return exp; - } - - /** - * Returns the Xml representation of a boolean isDeep, where false - * presents a depth value of '0', true a depth value of 'infinity'. - * - * @param isDeep - * @return Xml representation - */ - public static Element depthToXml(boolean isDeep) { - return depthToXml(isDeep? "infinity" : "0"); - } - - /** - * Returns the Xml representation of a depth String. Webdav defines the - * following valid values for depths: 0, 1, infinity - * - * @param depth - * @return 'deep' JDOM element - */ - public static Element depthToXml(String depth) { - Element dElem = new Element(XML_DEPTH, NAMESPACE); - dElem.setText(depth); - return dElem; - } - - /** - * Builds a 'href' Xml element from the given String - * - * @param href String representing the text of the 'href' Xml element - * @return Xml representation of a 'href' according to RFC 2518. - */ - public static Element hrefToXml(String href) { - return new Element(XML_HREF, NAMESPACE).setText(href); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/package.html deleted file mode 100644 index 8b8b2f978eb..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/util/package.html +++ /dev/null @@ -1,3 +0,0 @@ - -Utility classes used for Text handling and creation of common Xml elements. - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVConstants.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVConstants.java deleted file mode 100644 index 89cb94a235c..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVConstants.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Namespace; - -/** - * DeltaVConstants defines the following headers and properties - * required for any resource that is complient to - * RFC 3253:

- * - * Headers: - *

- * Label
- * 
- * - * Properties: - *
- * DAV:comment
- * DAV:creator-displayname
- * DAV:supported-method-set
- * DAV:supported-live-property-set
- * DAV:supported-report-set
- * DAV:workspace
- * 
- */ -public interface DeltaVConstants { - - /** - * The DAV: namespace. - */ - public static final Namespace NAMESPACE = DavConstants.NAMESPACE; - - /** - * For certain METHODS, if the request-URL identifies a version-controlled - * resource, a label can be specified in a LabelInfo request header to cause the - * method to be applied to the version selected by that label.
- * LabelInfo header MUST have no effect on a request whose request-URL does not - * identify a version-controlled resource. In particular, it MUST have no - * effect on a request whose request-URL identifies a version or a version - * history. - */ - public static final String HEADER_LABEL = "Label"; - - /** - * Location header as defined by - * RFC 2616. In the versioning - * context it is used to indicate the location of the new version created by a - * successful checkin in the response.

- * From RFC 2616:
- * The Location response-header field is used to redirect the recipient to a - * location other than the Request-URI for completion of the request or - * identification of a new resource.
- * For 201 (Created) responses, the Location is that of the new resource - * which was created by the request. - */ - public static final String HEADER_LOCATION = "Location"; - - /** - * The "DAV:comment" property is used to track a brief comment about a resource that is - * suitable for presentation to a user. The DAV:comment of a version can be - * used to indicate why that version was created. - */ - public static final DavPropertyName COMMENT = DavPropertyName.create("comment", NAMESPACE); - - /** - * The "DAV:creator-displayname" property contains a description of the creator of - * the resource that is suitable for presentation to a user. The - * DAV:creator-displayname of a version can be used to indicate who created - * that version. - */ - public static final DavPropertyName CREATOR_DISPLAYNAME = DavPropertyName.create("creator-displayname", NAMESPACE); - - /** - * Required protected live property for any resources being complient with - * RFC 3253. Clients should classify a resource by examing the values of the - * DAV:supported-method-set and DAV:supported-live-property-set - * properties of that resource.
- * Property structure: - *
-     * <!ELEMENT supported-method-set (supported-method*)>
-     * <!ELEMENT supported-method ANY>
-     * <!ATTLIST supported-method name NMTOKEN #REQUIRED>
-     * name value: a method name
-     * 
- * - * @see #SUPPORTED_LIVE_PROPERTY_SET - */ - public static final DavPropertyName SUPPORTED_METHOD_SET = DavPropertyName.create("supported-method-set", NAMESPACE); - - /** - * Required protected live property for any resources being complient with - * RFC 3253. Clients should classify a resource by examing the values of the - * DAV:supported-method-set and DAV:supported-live-property-set - * properties of that resource.
- * Property structure: - *
-     * <!ELEMENT supported-live-property-set (supported-live-property*)>
-     * <!ELEMENT supported-live-property name>
-     * <!ELEMENT prop ANY>
-     * ANY value: a property element type
-     * 
- * - * @see #SUPPORTED_METHOD_SET - */ - public static final DavPropertyName SUPPORTED_LIVE_PROPERTY_SET = DavPropertyName.create("supported-live-property-set", NAMESPACE); - - /** - * Protected "supported-report-set" property identifies the reports that are - * supported by the resource. - * - * @see #SUPPORTED_REPORT_SET - */ - public static final DavPropertyName SUPPORTED_REPORT_SET = DavPropertyName.create("supported-report-set", NAMESPACE); - - /** - * Protected "workspace" property indicating the workspace of a resource. - * - * @see #WORKSPACE - */ - public static final DavPropertyName WORKSPACE = DavPropertyName.create("workspace", NAMESPACE); - - - //-------------------------------------------------------------------------- - /** - * Xml elements - */ - public static final String XML_ACTIVITY = "activity"; - public static final String XML_BASELINE = "baseline"; - - public static final String XML_SUPPORTED_METHOD = "supported-method"; - public static final String XML_VERSION_HISTORY = "version-history"; - public static final String XML_VERSION = "version"; - public static final String XML_WORKSPACE = "workspace"; - - // options - /** - * If the OPTIONS request contains a body, i must start with an DAV:options - * element. - * - * @see OptionsInfo - * @see #XML_VH_COLLECTION_SET - * @see #XML_WSP_COLLECTION_SET - * @see #XML_ACTIVITY_COLLECTION_SET - */ - public static final String XML_OPTIONS = "options"; - - /** - * If an XML response body for a successful request is included, it must be - * a DAV:options-response XML element. - * - * @see OptionsResponse - */ - public static final String XML_OPTIONS_RESPONSE = "options-response"; - - /** - * A DAV:version-history-collection-set element may be included in the OPTIONS - * request body to identify collections that may contain version history - * resources.
- * The response body for a successful request must in consequence contain a - * DAV:version-history-collection-set element identifying collections that - * may contain version histories. An identified collection may be the root - * collection of a tree of collections, all of which may contain version - * histories. - * - *
-     * <!ELEMENT version-history-collection-set (href*)>
-     * 
- */ - public static final String XML_VH_COLLECTION_SET = "version-history-collection-set"; - - /** - * A DAV:workspace-collection-set element may be included in the OPTIONS request - * body to identify collections that may contain workspace resources.
- * The response body for a successful request must contain a - * DAV:workspace-collection-set element identifying collections that may - * contain workspaces. An identified collection may be the root collection - * of a tree of collections, all of which may contain workspaces. - * - *
-     * <!ELEMENT workspace-collection-set (href*)>
-     * 
- */ - public static final String XML_WSP_COLLECTION_SET = "workspace-collection-set"; - - /** - * A DAV:workspace-collection-set element may be included in the OPTIONS request - * body to identify collections that may contain activity resources.
- * The response body for a successful request must contain a - * DAV:workspace-collection-set element identifying collections that may - * contain activity resources. An identified collection may be the root collection - * of a tree of collections, all of which may contain activity resources. - * - *
-     * <!ELEMENT activity-collection-set (href*)>
-     * 
- */ - public static final String XML_ACTIVITY_COLLECTION_SET = "activity-collection-set"; - - /** - * Name of Xml element contained in the {@link #SUPPORTED_REPORT_SET} property. - * - * @see #SUPPORTED_REPORT_SET - * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty - */ - public static final String XML_SUPPORTED_REPORT = "supported-report"; - - /** - * Name of Xml child elements of {@link #XML_SUPPORTED_REPORT}. - * - * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty - */ - public static final String XML_REPORT = "report"; - - /** - * Top element for the 'DAV:version-tree' report - */ - public static final String XML_VERSION_TREE = "version-tree"; - - /** - * Top element for the 'DAV:expand-property' report - */ - public static final String XML_EXPAND_PROPERTY = "expand-property"; - - /** - * 'DAV:property' element to be used inside the 'DAV:expand-property' element. - * - * @see #XML_EXPAND_PROPERTY - */ - public static final String XML_PROPERTY = "property"; - - /** - * 'DAV:name' attribute for the property element - * - * @see #XML_PROPERTY - */ - public static final String ATTR_NAME = "name"; - - /** - * 'DAV:namespace' attribute for the property element - * - * @see #XML_PROPERTY - */ - public static final String ATTR_NAMESPACE = "namespace"; - - /** - * Top element for the 'DAV:locate-by-history' report - */ - public static final String XML_LOCATE_BY_HISTORY = "locate-by-history"; - - /** - * 'DAV:version-history-set' to be used inside the 'DAV:locate-by-history' - * element - * - * @see #XML_LOCATE_BY_HISTORY - */ - public static final String XML_VERSION_HISTORY_SET = "version-history-set"; - - - /** - * Xml element representing the mandatory root element of a LABEL request - * body. - * - * @see #XML_LABEL_NAME - * @see #XML_LABEL_ADD - * @see #XML_LABEL_REMOVE - * @see #XML_LABEL_SET - * @see LabelInfo - */ - public static final String XML_LABEL = "label"; - public static final String XML_LABEL_NAME = "label-name"; - public static final String XML_LABEL_ADD = "add"; - public static final String XML_LABEL_REMOVE = "remove"; - public static final String XML_LABEL_SET = "set"; - - /** - * Xml element defining the top element in the UPDATE request body. RFC 3253 - * defines the following structure for the 'update' element. - *
-     * <!ELEMENT update ANY>
-     * ANY value: A sequence of elements with at most one DAV:version element
-     * and at most one DAV:prop element.
-     * <!ELEMENT version (href)>
-     * prop: see RFC 2518, Section 12.11
-     * 
- */ - public static final String XML_UPDATE = "update"; - - // auto-version - public static final String XML_CHECKOUT_CHECKIN = "checkin-checkout"; - public static final String XML_CHECKOUT_UNLOCK_CHECKIN = "checkout-unlocked-checkin"; - public static final String XML_CHECKOUT = "checkout"; - public static final String XML_LOCKED_CHECKIN = "locked-checkout"; - - // merge - public static final String XML_MERGE = "merge"; - public static final String XML_N0_AUTO_MERGE = "no-auto-merge"; - public static final String XML_N0_CHECKOUT = "no-checkout"; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVServletRequest.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVServletRequest.java deleted file mode 100644 index 8a9f9f6ae4d..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVServletRequest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.jackrabbit.webdav.DavServletRequest; -import org.apache.jackrabbit.webdav.version.report.ReportInfo; - -/** - * DeltaVServletRequest provides extension useful for functionality - * related to RFC 3253. - */ -public interface DeltaVServletRequest extends DavServletRequest { - - /** - * Returns the Label header or null - * - * @return label header or null - * @see DeltaVConstants#HEADER_LABEL - */ - public String getLabel(); - - /** - * Return the request body as LabelInfo object or null - * if parsing the request body or the creation of the label info failed. - * - * @return LabelInfo object or null - */ - public LabelInfo getLabelInfo(); - - /** - * Return the request body as MergeInfo object or null - * if the creation failed due to invalid format. - * - * @return MergeInfo object or null - */ - public MergeInfo getMergeInfo(); - - /** - * Parses the UPDATE request body a build the corresponding UpdateInfo - * object. If the request body is missing or does not of the required format - * null is returned. - * - * @return the parsed update request body or null - */ - public UpdateInfo getUpdateInfo(); - - /** - * Returns the request body and the Depth header as ReportInfo - * object. The default depth, if no {@link org.apache.jackrabbit.webdav.DavConstants#HEADER_DEPTH - * Depth header}, is {@link org.apache.jackrabbit.webdav.DavConstants#DEPTH_0}. - * If the requuest body could not be parsed into an {@link org.jdom.Element} - * null is returned. - * - * @return ReportInfo or null - */ - public ReportInfo getReportInfo(); - - /** - * Returns the {@link OptionsInfo} present with the request or null. - * - * @return {@link OptionsInfo} or null - */ - public OptionsInfo getOptionsInfo(); -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/LabelInfo.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/LabelInfo.java deleted file mode 100644 index 8a1f6ca6974..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/LabelInfo.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -import java.util.Iterator; - -/** - * LabelInfo encapsulates the request body of a LABEL request - * used to add, set or remove a label from the requested version resource or - * from that version specified with the Label header in case the requested resource - * is a version-controlled resource.

- * The request body (thus the 'labelElement' passed to the constructore must be - * a DAV:label element: - *
- * <!ELEMENT label ANY>
- * ANY value: A sequence of elements with at most one DAV:add,
- * DAV:set, or DAV:remove element.
- * <!ELEMENT add (label-name)>
- * <!ELEMENT set (label-name)>
- * <!ELEMENT remove (label-name)>
- * <!ELEMENT label-name (#PCDATA)>
- * PCDATA value: string
- * 
- */ -public class LabelInfo implements DeltaVConstants { - - private static Logger log = Logger.getLogger(LabelInfo.class); - - public static final int TYPE_SET = 0; - public static final int TYPE_REMOVE = 1; - public static final int TYPE_ADD = 2; - - private final Element labelElement; - private final int depth; - - private int type; - private String labelName; - - /** - * Create a new LabelInfo from the given element and depth - * integer. If the specified Xml element does have a {@link DeltaVConstants#XML_LABEL} - * root element or no label name is specified with the action to perform - * the creation will fail. - * - * @param labelElement - * @param depth - * @throws IllegalArgumentException if the specified element does not - * start with a {@link DeltaVConstants#XML_LABEL} element or if the DAV:label - * element contains illegal instructions e.g. contains multiple DAV:add, DAV:set - * or DAV:remove elements. - */ - public LabelInfo(Element labelElement, int depth) { - if (labelElement == null || !labelElement.getName().equals(DeltaVConstants.XML_LABEL)) { - throw new IllegalArgumentException("label element expected"); - } - - this.labelElement = (Element) labelElement.detach(); - - Iterator childrenIter = labelElement.getChildren().iterator(); - while (childrenIter.hasNext()) { - Element child = (Element) childrenIter.next(); - if (!NAMESPACE.equals(child.getNamespace())) { - continue; - } - String name = child.getName(); - if (XML_LABEL_ADD.equals(name)) { - type = TYPE_ADD; - setLabelName(child); - } else if (XML_LABEL_REMOVE.equals(name)) { - type = TYPE_REMOVE; - setLabelName(child); - } else if (XML_LABEL_SET.equals(name)) { - type = TYPE_SET; - setLabelName(child); - } - } - this.depth = depth; - } - - /** - * Create a new LabelInfo from the given element. As depth - * the default value 0 is assumed. - * - * @param labelElement - * @throws IllegalArgumentException - * @see #LabelInfo(org.jdom.Element, int) - */ - public LabelInfo(Element labelElement) { - this(labelElement, 0); - } - - /** - * Return the 'label-name' or null - * - * @return 'label-name' or null - */ - public String getLabelName() { - return labelName; - } - - /** - * Retrieve the text of the 'label-name' child element of the specified - * parent element. - * - * @param parent the is intended to contain a valid 'label-name' child. - * @throws IllegalArgumentException if the labelName has been set before. - */ - private void setLabelName(Element parent) { - // test if any label name is present - if (labelName != null) { - throw new IllegalArgumentException("The DAV:label element may contain at most one DAV:add, DAV:set, or DAV:remove element"); - } - labelName = parent.getChildText(XML_LABEL_NAME, NAMESPACE); - } - - /** - * Return the type of the LABEL request. This might either be {@link #TYPE_SET}, - * {@link #TYPE_ADD} or {@link #TYPE_REMOVE}. - * - * @return type - */ - public int getType() { - return type; - } - - /** - * Return the depth - * - * @return depth - */ - public int getDepth() { - return depth; - } - - /** - * Return the DAV:label element - * - * @return the DAV:label element - */ - public Element getLabelElement() { - return labelElement; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/MergeInfo.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/MergeInfo.java deleted file mode 100644 index e9cb7b78f1e..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/MergeInfo.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.DavConstants; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; -import org.jdom.Element; - -/** - * MergeInfo encapsulates the information present in the DAV:merge - * element, that forms the mandatory request body of a MERGE request.
- * The DAV:merge element is specified to have the following form. - *
- * <!ELEMENT merge ANY>
- * ANY value: A sequence of elements with one DAV:source element, at most one
- * DAV:no-auto-merge element, at most one DAV:no-checkout element, at most one
- * DAV:prop element, and any legal set of elements that can occur in a DAV:checkout
- * element.
- * <!ELEMENT source (href+)>
- * <!ELEMENT no-auto-merge EMPTY>
- * <!ELEMENT no-checkout EMPTY>
- * prop: see RFC 2518, Section 12.11
- * 
- */ -public class MergeInfo implements DeltaVConstants { - - private static Logger log = Logger.getLogger(MergeInfo.class); - - private Element mergeElement; - - /** - * Create a new MergeInfo - * - * @param mergeElement - * @throws IllegalArgumentException if the mergeElement is null - * or not a DAV:merge element. - */ - public MergeInfo(Element mergeElement) { - if (mergeElement == null || !mergeElement.getName().equals(XML_MERGE)) { - throw new IllegalArgumentException("'DAV:merge' element expected"); - } - this.mergeElement = (Element) mergeElement.detach(); - } - - /** - * Returns the URL specified with the DAV:source element or null - * if no such child element is present in the DAV:merge element. - * - * @return href present in the DAV:source child element or null. - */ - public String getSourceHref() { - Element source = mergeElement.getChild(DavConstants.XML_SOURCE, DavConstants.NAMESPACE); - if (source != null) { - return source.getChildText(DavConstants.XML_HREF, DavConstants.NAMESPACE); - } - return null; - } - - /** - * Returns true if the DAV:merge element contains a DAV:no-auto-merge child element. - * - * @return true if the DAV:merge element contains a DAV:no-auto-merge child. - */ - public boolean isNoAutoMerge() { - return mergeElement.getChild(XML_N0_AUTO_MERGE, NAMESPACE) != null; - } - - /** - * Returns true if the DAV:merge element contains a DAV:no-checkout child element. - * - * @return true if the DAV:merge element contains a DAV:no-checkout child - */ - public boolean isNoCheckout() { - return mergeElement.getChild(XML_N0_CHECKOUT, NAMESPACE) != null; - } - - /** - * Returns a {@link DavPropertyNameSet}. If the DAV:merge element contains - * a DAV:prop child element the properties specified therein are included - * in the set. Otherwise an empty set is returned. - * - * @return set listing the properties specified in the DAV:prop element indicating - * those properties that must be reported in the response body. - */ - public DavPropertyNameSet getPropertyNameSet() { - Element propElement = mergeElement.getChild(DavConstants.XML_PROP, DavConstants.NAMESPACE); - if (propElement != null) { - return new DavPropertyNameSet(propElement); - } else { - return new DavPropertyNameSet(); - } - } - - /** - * Returns the DAV:merge element used to create this MergeInfo - * object. - * - * @return DAV:merge element - */ - public Element getMergeElement() { - return mergeElement; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/OptionsInfo.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/OptionsInfo.java deleted file mode 100644 index ca2c90d68a9..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/OptionsInfo.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.log4j.Logger; -import org.jdom.Element; -import org.jdom.Namespace; - -import java.util.List; - -/** - * OptionsInfo represents the Xml request body, that may be present - * with a OPTIONS request. - *
- * The DAV:options element is specified to have the following form. - * - *
- * <!ELEMENT options ANY>
- * ANY value: A sequence of elements each at most onces.
- * 
- * - * @see DeltaVConstants#XML_VH_COLLECTION_SET - * @see DeltaVConstants#XML_WSP_COLLECTION_SET - * @see DeltaVConstants#XML_ACTIVITY_COLLECTION_SET - */ -public class OptionsInfo { - - private static Logger log = Logger.getLogger(OptionsInfo.class); - - private final Element optionsElement; - - /** - * Create a new UpdateInfo object. - * - * @param optionsElement - * @throws IllegalArgumentException if the updateElement is null - * or not a DAV:update element or if the element does not match the required - * structure. - */ - public OptionsInfo(Element optionsElement) { - if (optionsElement == null || !optionsElement.getName().equals(DeltaVConstants.XML_OPTIONS)) { - throw new IllegalArgumentException("DAV:options element expected"); - } - this.optionsElement = (Element) optionsElement.detach(); - } - - /** - * Returns the set of elements present in the {@link DeltaVConstants#XML_OPTIONS DAV:options} - * element. These elements define the information the client wishes to retrieve - * the OPTIONS request. - * - * @return set of child elements - */ - public List getElements() { - return optionsElement.getChildren(); - } - - /** - * Returns true if a child element with the given name and namespace is present. - * - * @param name - * @param namespace - * @return true if such a child element exists in the options element. - */ - public boolean containsElement(String name, Namespace namespace) { - return optionsElement.getChild(name, namespace) != null; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/OptionsResponse.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/OptionsResponse.java deleted file mode 100644 index 1c49fc3549e..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/OptionsResponse.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.util.XmlUtil; -import org.jdom.Document; -import org.jdom.Element; -import org.jdom.Namespace; - -/** - * OptionsResponse encapsulates the DAV:options-response element - * present in the response body of a successful OPTIONS request (with body). - *
- * The DAV:options-response element is defined to have the following format. - * - *
- * <!ELEMENT options-response ANY>
- * ANY value: A sequence of elements
- * 
- */ -public class OptionsResponse implements DeltaVConstants { - - private static Logger log = Logger.getLogger(OptionsResponse.class); - - private final Element optionsResponse = new Element(XML_OPTIONS_RESPONSE, NAMESPACE); - - /** - * Add a new entry to this OptionsResponse - * - * @param elem - */ - public void addEntry(Element elem) { - optionsResponse.addContent(elem.detach()); - } - - /** - * Add a new entry to this OptionsResponse and make each - * href present in the String array being a separate {@link org.apache.jackrabbit.webdav.DavConstants#XML_HREF DAV:href} - * element within the entry. - * - * @param name - * @param namespace - * @param hrefs - */ - public void addEntry(String name, Namespace namespace, String[] hrefs) { - Element elem = new Element(name, namespace); - for (int i = 0; i < hrefs.length; i++) { - elem.addContent(XmlUtil.hrefToXml(hrefs[i])); - } - optionsResponse.addContent(elem); - } - - /** - * Return the Xml representation. - * - * @return Xml representation. - */ - public Document toXml() { - return new Document(optionsResponse); - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/ResourceType.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/ResourceType.java deleted file mode 100644 index 36d1aae4eb6..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/ResourceType.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.log4j.Logger; -import org.jdom.Element; - -/** - * The ResourceType extends the {@link org.apache.jackrabbit.webdav.property.ResourceType} - * by DeltaV specific types. - */ -public class ResourceType extends org.apache.jackrabbit.webdav.property.ResourceType - implements DeltaVConstants { - - private static Logger log = Logger.getLogger(ResourceType.class); - - /** - * The version-history resource type - */ - public static final int VERSION_HISTORY = COLLECTION + 1; - - /** - * The activity resource type - */ - public static final int ACTIVITY = VERSION_HISTORY + 1; - - /** - * The baseline resource type - */ - public static final int BASELINE = ACTIVITY + 1; - - /** - * Array containing all possible resourcetype elements - */ - private static final String[] ELEMENT_NAMES = { - null, - XML_COLLECTION, - XML_VERSION_HISTORY, - XML_ACTIVITY, - XML_BASELINE - }; - - - /** - * Create a resource type property - * - * @param resourceType - */ - public ResourceType(int resourceType) { - super(resourceType); - } - - /** - * Return the resource type as Xml element. - * - * @return Xml element representing the internal type or null - * if the resource has no element name assigned (default resource type). - */ - public Object getValue() { - String name = ELEMENT_NAMES[getResourceType()]; - return (name != null) ? new Element(name, DeltaVConstants.NAMESPACE) : null; - } - - /** - * Returns true if the given integer defines a valid resource type. - * - * @param resourceType to be validated. - * @return true if this is a known resource type. - */ - public boolean isValidResourceType(int resourceType) { - if (resourceType < DEFAULT_RESOURCE || resourceType > BASELINE) { - return false; - } - return true; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/SupportedMethodSetProperty.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/SupportedMethodSetProperty.java deleted file mode 100644 index 3a0dd817ff9..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/SupportedMethodSetProperty.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.DefaultDavProperty; -import org.jdom.Element; - -/** - * The SupportedMethodSetProperty - */ -public class SupportedMethodSetProperty extends DefaultDavProperty implements DeltaVConstants { - - private static Logger log = Logger.getLogger(SupportedMethodSetProperty.class); - - /** - * Create a new SupportedMethodSetProperty property. - * - * @param methods that are supported by the resource having this property. - */ - public SupportedMethodSetProperty(String[] methods) { - super(DeltaVConstants.SUPPORTED_METHOD_SET, new Element[methods.length], true); - - // fill the array with the proper elements - Element[] value = (Element[]) getValue(); - for (int i = 0; i < methods.length; i++) { - Element methodElem = new Element(DeltaVConstants.XML_SUPPORTED_METHOD, DeltaVConstants.NAMESPACE); - methodElem.setAttribute("name",methods[i], DeltaVConstants.NAMESPACE); - value[i] = methodElem; - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/UpdateInfo.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/UpdateInfo.java deleted file mode 100644 index a75e4f256f2..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/UpdateInfo.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Element; - -import java.util.List; -import java.util.Iterator; - -/** - * UpdateInfo encapsulates the request body of an UPDATE request. - * RFC 3253 defines the request body as follows: - *
- * <!ELEMENT update ANY>
- * ANY value: A sequence of elements with at most one DAV:version element and at
- * most one DAV:prop element.
- * <!ELEMENT version (href)>
- * prop: see RFC 2518, Section 12.11
- * 
- * - * In order to reflect the complete range of version restoring and updating - * of nodes defined by JSR170 the definition has been extended: - *
- * <!ELEMENT update ( (version+ | label-name | workspace ) , (prop)?, (removeExisting)? ) >
- * <!ELEMENT version (href) >
- * <!ELEMENT label-name (#PCDATA) >
- * <!ELEMENT workspace (href) >
- * <!ELEMENT prop ANY >
- * <!ELEMENT removeExisting EMPTY >
- * 
- */ -public class UpdateInfo implements DeltaVConstants { - - private static Logger log = Logger.getLogger(UpdateInfo.class); - - private final Element updateElement; - private String[] versionHref; - private String[] labelName; - private String workspaceHref; - - /** - * Create a new UpdateInfo object. - * - * @param updateElement - * @throws IllegalArgumentException if the updateElement is null - * or not a DAV:update element or if the element does not match the required - * structure. - */ - public UpdateInfo(Element updateElement) { - if (updateElement == null || !updateElement.getName().equals(DeltaVConstants.XML_UPDATE)) { - throw new IllegalArgumentException("DAV:update element expected"); - } - - List targetList; - if (!(targetList = updateElement.getChildren(XML_VERSION, NAMESPACE)).isEmpty()) { - Iterator it = targetList.iterator(); - versionHref = new String[targetList.size()]; - int i = 0; - while (it.hasNext()) { - Element versionElem = (Element) it.next(); - versionHref[i] = versionElem.getChildText(DavConstants.XML_HREF, NAMESPACE); - i++; - } - } else if (!(targetList = updateElement.getChildren(XML_LABEL_NAME, NAMESPACE)).isEmpty()) { - Iterator it = targetList.iterator(); - labelName = new String[targetList.size()]; - int i = 0; - while (it.hasNext()) { - Element labelNameElem = (Element) it.next(); - labelName[i] = labelNameElem.getText(); - i++; - } - } else if (updateElement.getChild(XML_WORKSPACE, NAMESPACE) != null) { - workspaceHref = updateElement.getChild(XML_WORKSPACE, NAMESPACE).getChildText(DavConstants.XML_HREF, NAMESPACE); - } else { - throw new IllegalArgumentException("DAV:update element must contain either DAV:version, DAV:label-name or DAV:workspace child element."); - } - - this.updateElement = (Element) updateElement.detach(); - } - - /** - * - * @return - */ - public String[] getVersionHref() { - return versionHref; - } - - /** - * - * @return - */ - public String[] getLabelName() { - return labelName; - } - - /** - * - * @return - */ - public String getWorkspaceHref() { - return workspaceHref; - } - - /** - * Returns a {@link DavPropertyNameSet}. If the DAV:update element contains - * a DAV:prop child element the properties specified therein are included - * in the set. Otherwise an empty set is returned. - * - * @return set listing the properties specified in the DAV:prop element indicating - * those properties that must be reported in the response body. - */ - public DavPropertyNameSet getPropertyNameSet() { - Element propElement = updateElement.getChild(DavConstants.XML_PROP, DavConstants.NAMESPACE); - if (propElement != null) { - return new DavPropertyNameSet(propElement); - } else { - return new DavPropertyNameSet(); - } - } - - /** - * - * @return - */ - public Element getUpdateElement() { - return updateElement; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/VersionControlledResource.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/VersionControlledResource.java deleted file mode 100644 index 0cc5e926f2d..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/VersionControlledResource.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.MultiStatus; -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.property.DavPropertySet; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; - -/** - * The VersionControlledResource represents in contrast to the - * VersionableResource a resource, that has already been put - * under version-control. This resource can be checked-in, checked-out and - * has its on {@link javax.jcr.version.VersionHistory version history}. - *

- * RFC 3253 defines the following required properties for a - * version-controlled resource (vc-resource): - *

    - *
  • DAV:auto-version
  • - *
  • DAV:version-history (version-history)
  • - *
  • DAV:workspace (workspace)
  • - *
  • DAV:version-controlled-configuration (baseline)
  • - *
  • all DeltaV-compliant resource properties.
  • - *
- * - * checked-in vc-resource: - *
    - *
  • DAV:checked-in
  • - *
- * - * checked-out vc-resource: - *
    - *
  • DAV:checked-out
  • - *
  • DAV:predecessor-set
  • - *
  • DAV:checkout-fork (in-place-checkout or working resource)
  • - *
  • DAV:checkin-fork (in-place-checkout or working resource)
  • - *
  • DAV:merge-set (merge)
  • - *
  • DAV:auto-merge-set (merge)
  • - *
  • DAV:unreserved (activity)
  • - *
  • DAV:activity-set (activity)
  • - *
- *

- * In addition a version-controlled resource must support the following METHODS: - *

    - *
  • VERSION-CONTROL
  • - *
  • MERGE (merge)
  • - *
  • all DeltaV-compliant resource METHODS.
  • - *
- * - * checked-in vc-resource: - *
    - *
  • CHECKOUT (checkout-in-place)
  • - *
  • UPDATE (update)
  • - *
  • all version-controlled resource METHODS.
  • - *
- * - * checked-out vc-resource: - *
    - *
  • CHECKIN (checkout-in-place or working-resource)
  • - *
  • UNCHECKOUT (checkout-in-place)
  • - *
  • all DeltaV-compliant resource METHODS.
  • - *
- * - * @see DeltaVResource - * @see VersionableResource - */ -public interface VersionControlledResource extends VersionableResource { - - /** - * Methods defined for a checked-in version-controlled resource: CHECKOUT, UNCHECHKOUT, UPDATE, MERGE, LABEL - */ - public String methods_checkedIn = "CHECKOUT, UNCHECHKOUT, UPDATE, MERGE, LABEL"; - /** - * Methods defined for a checked-out version-controlled resource: CHECKIN, MERGE - */ - public String methods_checkedOut = "CHECKIN, MERGE"; - - /** - * The DAV:auto-version property determines how it responds to a method that - * attempts to modify its content or dead properties. Possible responses - * include various combinations of automated checkout, write lock and checkin - * as well as failure until the resource is explicitely checked-out.
- * See RFC 3253 for a detailed - * description. - */ - public static final DavPropertyName AUTO_VERSION = DavPropertyName.create("auto-version", DeltaVConstants.NAMESPACE); - - /** - * The computed property DAV:version-history identifies the version history - * resource for the DAV:checked-in or DAV:checked-out version of this - * version-controlled resource.
- * The property is defined to have the following format: - *
-     * <!ELEMENT version-history (href)>
-     * 
- */ - public static final DavPropertyName VERSION_HISTORY = DavPropertyName.create("version-history", DeltaVConstants.NAMESPACE); - - /** - * The DAV:checked-in property appears on a checked-in version-controlled - * resource, and identifies the base version of this version-controlled - * resource. This property is removed when the resource is checked out, and - * then added back (identifying a new version) when the resource is checked - * back in.
- * This property is defined to have the following format: - *
-     * <!ELEMENT checked-in (href)>
-     * 
- */ - public static final DavPropertyName CHECKED_IN = DavPropertyName.create("checked-in", DeltaVConstants.NAMESPACE); - - /** - * The DAV:checked-out property identifies the base version of this resource. - * It is the same that was identified by the DAV:checked-in property at the - * time the resource was checked out. This property is removed when the - * resource is checked in.
- * This property is defined to have the following format: - *
-     * <!ELEMENT checked-out (href)>
-     * 
- * - * @see #CHECKED_IN - */ - public static final DavPropertyName CHECKED_OUT = DavPropertyName.create("checked-out", DeltaVConstants.NAMESPACE); - - /** - * The DAV:predecessor-set property of a version-controlled resource points - * to those version resources, that are scheduled to become the predecessors - * of this resource when it is back checked-in. This property is not - * protected, however a server may reject attempts to modify the - * DAV:predecessor-set of a version-controlled resource.
- * This property is defined to have the following format: - *
-     * <!ELEMENT predecessor-set (href+)>
-     * 
- * - * @see #checkin() - * @see VersionResource#PREDECESSOR_SET - */ - public static final DavPropertyName PREDECESSOR_SET = DavPropertyName.create("predecessor-set", DeltaVConstants.NAMESPACE); - - /** - * This property determines the DAV:checkin-fork property of the version - * that results from checking in this resource. - */ - public static final DavPropertyName CHECKIN_FORK = DavPropertyName.create("checkin-fork", DeltaVConstants.NAMESPACE); - - /** - * This property determines the DAV:checkout-fork property of the version - * that results from checking in this resource. - */ - public static final DavPropertyName CHECKOUT_FORK = DavPropertyName.create("checkout-fork", DeltaVConstants.NAMESPACE); - - /** - * This property identifies each version that is to be merged into this - * checked-out resource. This property is set, whenever a MERGE request - * with the DAV:no-auto-merge flag succeeded. The client then must confirm - * each single merge by removing the version from the DAV:merge-set or - * moving it the the versions DAV:predecessor-set.
- * This property is defined to have the following format: - *
-     * <!ELEMENT merge-set (href*)>
-     * 
- * - * @see #merge(MergeInfo) - * @see #resolveMergeConflict(DavPropertySet, DavPropertyNameSet) - */ - public static final DavPropertyName MERGE_SET = DavPropertyName.create("merge-set", DeltaVConstants.NAMESPACE); - - /** - * The DAV:auto-merge-set property identifies each version that the server - * has merged into this checked-out resource. The client should confirm that - * the merge has been performed correctly before moving a URL from the - * DAV:auto-merge-set to the DAV:predecessor-set of a checked-out resource.
- * This property is defined to have the following format: - *
-     * <!ELEMENT auto-merge-set (href*)>
-     * 
- * - * @see #merge(MergeInfo) - * @see #resolveMergeConflict(DavPropertySet, DavPropertyNameSet) - */ - public static final DavPropertyName AUTO_MERGE_SET = DavPropertyName.create("auto-merge-set", DeltaVConstants.NAMESPACE); - - /** - * Perform a checkin on the version controlled resource. - * - * @return String representing the location of the version created by the - * checkin. - * @throws DavException if an error occurs. - */ - public String checkin() throws DavException; - - /** - * Perform a checkout on the version controlled resource. - * - * @throws DavException - */ - public void checkout() throws DavException; - - /** - * Perform an uncheckout on the version controlled resource. - * - * @throws DavException - */ - public void uncheckout() throws DavException; - - /** - * Perform an update on this resource using the specified {@link UpdateInfo}. - * - * @param updateInfo - * @return MultiStatus containing the list of resources that - * have been modified by this update call. - * @throws DavException - */ - public MultiStatus update(UpdateInfo updateInfo) throws DavException; - - /** - * Perform a merge on this resource using the specified {@link MergeInfo}. - * - * @param mergeInfo - * @return MultiStatus containing the list of resources that - * have been modified. - * @throws DavException - */ - public MultiStatus merge(MergeInfo mergeInfo) throws DavException; - - /** - * Resolve one or multiple merge conflicts present on this resource. Please - * note that the 'setProperties' or 'removeProperties' set my contain additional - * resource properties, that need to be changed. Those properties are left - * untouched, whereas the {@link #AUTO_MERGE_SET}, {@link #MERGE_SET} and - * the {@link #PREDECESSOR_SET} are removed from the list upon successful - * resolution of a merge conflict.
- * If the removeProperties or setProperties set do not contain any of the - * mentioned resource properties or if the value of those properties do - * not allow for a resolution of an existing merge conflict, this METHODS - * returns silently. - * - * @param setProperties - * @param removePropertyNames - * @throws DavException the set or remove property sets attempt to resolve - * a non-existing merge conflict of if another error occurs while resolving - * an existing conflict. - */ - public void resolveMergeConflict(DavPropertySet setProperties, DavPropertyNameSet removePropertyNames) throws DavException; - - /** - * Modify the labels of the version referenced by the DAV:checked-in property - * of this checked-in version-controlled resource. If the resource is not - * checked-in the request must fail. - * - * @param labelInfo - * @throws org.apache.jackrabbit.webdav.DavException - * @see LabelInfo - * @see VersionResource#label(LabelInfo) for the pre- and postcondition of - * a successful LABEL request. - */ - public void label(LabelInfo labelInfo) throws DavException; - - /** - * Returns the VersionHistoryResource, that is referenced in the - * '{@link #VERSION_HISTORY version-history}' property. - * - * @return - * @throws DavException - */ - public VersionHistoryResource getVersionHistory() throws DavException; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/VersionResource.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/VersionResource.java deleted file mode 100644 index b1d7a964ef9..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/VersionResource.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version; - -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.DavException; - -/** - * VersionResource is a resource that contains a copy of a particular - * state of a version-controlled resource. A new version resource is created whenever - * a checked-out version-controlled resource is checked-in. The server allocates - * a distinct new URL for each new version, and this URL will never be used to - * identify any resource other than that version. The content and dead properties - * of a version never change. - *

- * RFC 3253 defines the following required properties for a version resource: - *

    - *
  • DAV:predecessor-set (protected)
  • - *
  • DAV:successor-set (computed)
  • - *
  • DAV:checkout-set
  • - *
  • DAV:version-name
  • - *
  • DAV:checkout-fork (in-place-checkout or working resource)
  • - *
  • DAV:checkin-fork (in-place-checkout or working resource)
  • - *
  • DAV:version-history (version-history)
  • - *
  • DAV:label-name-set (label)
  • - *
  • DAV:activity-set (activity)
  • - *
  • all DeltaV-compliant resource properties.
  • - *
- *

- * In addition a version resource must support the following METHODS: - *

    - *
  • LABEL (label)
  • - *
  • CHECKOUT (working-resource)
  • - *
  • all DeltaV-compliant resource METHODS.
  • - *
- * - * @see DeltaVResource - */ -public interface VersionResource extends DeltaVResource { - - /** - * The version resource defines one additional method LABEL. - * - * @see DeltaVResource#METHODS - * @see org.apache.jackrabbit.webdav.DavResource#METHODS - */ - public String METHODS = "LABEL"; - - /** - * Required protected property 'DAV:label-name-set' for a version of a webdav - * resource introduced with the 'LabelInfo' feature. - * This property contains the labels that currently select this version.
- * Property structure is defined as follows:
- *
-     * <!ELEMENT label-name-set (label-name*)>
-     * <!ELEMENT label-name (#PCDATA)>
-     * PCDATA value: string
-     * 
- */ - public static final DavPropertyName LABEL_NAME_SET = DavPropertyName.create("label-name-set", DeltaVConstants.NAMESPACE); - - /** - * The protected DAV:predecessor property identifies each predecessor of - * this version. Except for the root version, which has no predecessors, - * each version has at least one predecessor.
- * The property is defined to have the following format: - *
-     * <!ELEMENT predecessor-set (href*)>
-     * 
- */ - public static final DavPropertyName PREDECESSOR_SET = DavPropertyName.create("predecessor-set", DeltaVConstants.NAMESPACE); - - /** - * The computed property DAV:successor-set identifies each version whose - * DAV:predecessor-set identifies this version.
- * The property is defined to have the following format: - *
-     * <!ELEMENT successor-set (href*)>
-     * 
- * - */ - public static final DavPropertyName SUCCESSOR_SET = DavPropertyName.create("successor-set", DeltaVConstants.NAMESPACE); - - /** - * The computed property DAV:checkout-set identifies each checked-out - * resource whose DAV:checked-out property identifies this version.
- * The property is defined to have the following format: - *
-     * <!ELEMENT checkout-set (href*)>
-     * 
- * - * @see VersionControlledResource#CHECKED_OUT - */ - public static final DavPropertyName CHECKOUT_SET = DavPropertyName.create("checkout-set", DeltaVConstants.NAMESPACE); - - /** - * The protected property DAV:version-name defines a human readable id for - * this version. The id defined to be unique within the version-history this - * version belongs to.
- * The property is defined to have the following format: - *
-     * <!ELEMENT version-name (#PCDATA)>
-     * PCDATA value: string
-     * 
- */ - public static final DavPropertyName VERSION_NAME = DavPropertyName.create("version-name", DeltaVConstants.NAMESPACE); - - /** - * The computed property DAV:version-history identifies the version history - * that contains this version.
- * The property is defined to have the following format: - *
-     * <!ELEMENT version-history (href)>
-     * 
- */ - public static final DavPropertyName VERSION_HISTORY = DavPropertyName.create("version-history", DeltaVConstants.NAMESPACE); - - /** - * This property controls the behavior of CHECKOUT when a version already - * is checked out or has a successor. - */ - public static final DavPropertyName CHECKOUT_FORK = DavPropertyName.create("checkout-fork", DeltaVConstants.NAMESPACE); - - /** - * This property controls the behavior of CHECKIN when a version already - * has a successor. - */ - public static final DavPropertyName CHECKIN_FORK = DavPropertyName.create("checkin-fork", DeltaVConstants.NAMESPACE); - - /** - * Modify the labels of this version resource. The modifications (SET, ADD or - * REMOVE) are listed in the specified LabelInfo object.
- * The case of a label name must be preserved when it is stored and retrieved. - *
If the type of modification is ADD, then the label must not yet occur on - * any other version within the same version history. In contrast a SET - * modification will move the indicated label to this version, if it existed - * with another version before. After a successful LABEL request the label - * must not appear with any other version in the same version history. - * - * @param labelInfo - * @throws org.apache.jackrabbit.webdav.DavException - * @see LabelInfo - */ - public void label(LabelInfo labelInfo) throws DavException; - - /** - * Returns the VersionHistoryResource, that is referenced in the - * {@link #VERSION_HISTORY DAV:version-history} property. - * - * @return - * @throws DavException - */ - public VersionHistoryResource getVersionHistory() throws DavException; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/package.html deleted file mode 100644 index d384267c86c..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/package.html +++ /dev/null @@ -1,4 +0,0 @@ - -Interfaces and classes used to cover functionality defined by -RFC 3253: Versioning Extensions to WebDAV. - \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ExpandPropertyReport.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ExpandPropertyReport.java deleted file mode 100644 index d54c05a4d06..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ExpandPropertyReport.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.property.DavProperty; -import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.property.HrefProperty; -import org.apache.jackrabbit.webdav.property.AbstractDavProperty; -import org.apache.jackrabbit.webdav.version.DeltaVConstants; -import org.apache.jackrabbit.webdav.version.DeltaVResource; -import org.jdom.Element; -import org.jdom.Attribute; -import org.jdom.Namespace; -import org.jdom.Document; - -import java.util.*; - -/** - * ExpandPropertyReport encapsulates the DAV:expand-property report, - * that provides a mechanism for retrieving in one request the properties from - * the resources identified by those DAV:href elements. It should be supported by - * all resources that support the REPORT method. - *

- * RFC 3253 specifies the following required format for the request body: - *

- * <!ELEMENT expand-property (property*)>
- * <!ELEMENT property (property*)>
- * <!ATTLIST property name NMTOKEN #REQUIRED>
- * name value: a property element type
- * <!ATTLIST property namespace NMTOKEN "DAV:">
- * namespace value: an XML namespace
- * 
- * NOTE: any DAV:property elements defined in the request body, that does not - * represent {@link HrefProperty} is treated as in a common PROPFIND request. - * - * @see DeltaVConstants#XML_EXPAND_PROPERTY - * @see DeltaVConstants#XML_PROPERTY - */ -public class ExpandPropertyReport implements Report, DeltaVConstants { - - private static Logger log = Logger.getLogger(ExpandPropertyReport.class); - - private DeltaVResource resource; - private ReportInfo info; - private List properties; - - /** - * Returns {@link ReportType#EXPAND_PROPERTY}. - * - * @return - * @see Report#getType() - */ - public ReportType getType() { - return ReportType.EXPAND_PROPERTY; - } - - /** - * Set the target resource. - * - * @param resource - * @throws IllegalArgumentException if the specified resource is null - * @see Report#setResource(org.apache.jackrabbit.webdav.version.DeltaVResource) - */ - public void setResource(DeltaVResource resource) throws IllegalArgumentException { - if (resource == null) { - throw new IllegalArgumentException("The resource specified must not be null."); - } - this.resource = resource; - } - - /** - * Set the ReportInfo. - * - * @param info - * @throws IllegalArgumentException if the given ReportInfo - * does not contain a DAV:expand-property element. - * @see Report#setInfo(ReportInfo) - */ - public void setInfo(ReportInfo info) throws IllegalArgumentException { - if (info == null || !XML_EXPAND_PROPERTY.equals(info.getReportElement().getName())) { - throw new IllegalArgumentException("DAV:expand-property element expected."); - } - this.info = info; - properties = info.getReportElement().getChildren(XML_PROPERTY, NAMESPACE); - } - - /** - * Run the report - * - * @return Xml Document as defined by - * RFC 2518 - * @throws DavException - * @see Report#toXml() - */ - public Document toXml() throws DavException { - if (info == null || resource == null) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while running DAV:version-tree report"); - } - - MultiStatus ms = new MultiStatus(); - buildMultiStatus(resource, info.getDepth(), ms); - return ms.toXml(); - } - - /** - * Fills the specified MultiStatus object by generating a - * MultiStatusResponse for the given resource (and - * its member according to the depth value). - * - * @param res - * @param depth - * @param ms - * @throws DavException - * @see #getResponse(DavResource, List) - */ - private void buildMultiStatus(DavResource res, int depth, MultiStatus ms) - throws DavException { - MultiStatusResponse response = getResponse(res, properties); - ms.addResponse(response); - if (depth > 0) { - DavResourceIterator it = res.getMembers(); - while (it.hasNext()) { - buildMultiStatus(it.nextResource(), depth-1, ms); - } - } - } - - /** - * Builds a MultiStatusResponse for the given resource respecting - * the properties specified. Any property that represents a {@link HrefProperty} - * is expanded: It's name equals the name of a valid {@link HrefProperty}. - * However the value of that given property (consisting of one or multiple DAV:href elements) - * is replaced by the Xml representation of a separate - * {@link MultiStatusResponse multistatus responses} for the - * resource referenced by the given DAV:href elements. The responses may - * themselves have properties, which are defined by the separate list. - * - * @param res - * @param propertyList - * @return MultiStatusResponse for the given resource. - * @see ExpandProperty - */ - private MultiStatusResponse getResponse(DavResource res, List propertyList) { - MultiStatusResponse resp = new MultiStatusResponse(res.getHref()); - Iterator propIter = propertyList.iterator(); - while (propIter.hasNext()) { - Element propertyElem = (Element) propIter.next(); - Attribute nameAttr = propertyElem.getAttribute(ATTR_NAME); - if (nameAttr == null) { - // NOTE: this is not valid according to the DTD - continue; - } - Attribute namespaceAttr = propertyElem.getAttribute(ATTR_NAMESPACE); - - String name = nameAttr.getValue(); - Namespace namespace = (namespaceAttr != null) ? Namespace.getNamespace(namespaceAttr.getValue()) : NAMESPACE; - - DavPropertyName propName = DavPropertyName.create(name, namespace); - DavProperty p = res.getProperty(propName); - if (p != null) { - if (p instanceof HrefProperty && res instanceof DeltaVResource) { - resp.add(new ExpandProperty((DeltaVResource)res, (HrefProperty)p, propertyElem.getChildren(XML_PROPERTY, NAMESPACE))); - } else { - resp.add(p); - } - } else { - resp.add(propName, DavServletResponse.SC_NOT_FOUND); - } - } - return resp; - } - - //--------------------------------------------------------< inner class >--- - /** - * ExpandProperty extends DavProperty. It's name - * equals the name of a valid {@link HrefProperty}. However the value of - * that given property (consisting of one or multiple DAV:href elements) - * is replaced by the Xml representation of a separate - * {@link MultiStatusResponse multistatus responses} for the - * resource referenced to by the given DAV.:href elements. The responses may - * themselves have properties, which are defined by the separate list. - */ - private class ExpandProperty extends AbstractDavProperty { - - private List valueList = new ArrayList(); - - /** - * Create a new ExpandProperty. - * - * @param hrefProperty - * @param propertyList - */ - private ExpandProperty(DeltaVResource deltaVResource, HrefProperty hrefProperty, List propertyList) { - super(hrefProperty.getName(), hrefProperty.isProtected()); - try { - DavResource[] refResource = deltaVResource.getReferenceResources(hrefProperty.getName()); - for (int i = 0; i < refResource.length; i++) { - MultiStatusResponse resp = getResponse(refResource[i], propertyList); - valueList.add(resp.toXml()); - } - } catch (DavException e) { - // invalid references or unknown property - log.error(e.getMessage()); - } - } - - /** - * Returns - * @return - */ - public Object getValue() { - return valueList; - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/LocateByHistoryReport.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/LocateByHistoryReport.java deleted file mode 100644 index f10da0dc5c1..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/LocateByHistoryReport.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; -import org.apache.jackrabbit.webdav.version.DeltaVConstants; -import org.apache.jackrabbit.webdav.version.VersionControlledResource; -import org.apache.jackrabbit.webdav.version.VersionHistoryResource; -import org.apache.jackrabbit.webdav.version.DeltaVResource; -import org.jdom.Element; -import org.jdom.Document; - -import java.util.List; -import java.util.HashSet; -import java.util.Iterator; - -/** - * LocateByHistoryReport encapsulates the DAV:locate-by-hisotry - * report, that may be used to locate a version-controlled resource for that - * version history. The DAV:locate-by-history report can be applied to a collection - * to locate the collection member that is a version-controlled resource for a - * specified version history resource. - * - *
- * <!ELEMENT locate-by-history (version-history-set, prop)>
- * <!ELEMENT version-history-set (href+)>
- * 
- */ -public class LocateByHistoryReport implements Report, DeltaVConstants { - - private static Logger log = Logger.getLogger(LocateByHistoryReport.class); - - private ReportInfo info; - private HashSet vhHrefSet = new HashSet(); - private DeltaVResource resource; - - /** - * - * @return - * @see Report#getType() - */ - public ReportType getType() { - return ReportType.LOCATE_BY_HISTORY; - } - - /** - * Set the DeltaVResource. - * - * @param resource - * @throws IllegalArgumentException if the specified resource is not a {@link VersionControlledResource}. - * @see Report#setResource(org.apache.jackrabbit.webdav.version.DeltaVResource) - */ - public void setResource(DeltaVResource resource) throws IllegalArgumentException { - if (resource instanceof VersionControlledResource) { - this.resource = resource; - } else { - throw new IllegalArgumentException("DAV:version-tree report can only be created for version-controlled resources and version resources."); - } - } - - /** - * Set the ReportInfo - * - * @param info - * @throws IllegalArgumentException if the given ReportInfo - * does not contain a DAV:version-tree element. - * @see Report#setInfo(ReportInfo) - */ - public void setInfo(ReportInfo info) throws IllegalArgumentException { - if (info == null || !XML_LOCATE_BY_HISTORY.equals(info.getReportElement().getName())) { - throw new IllegalArgumentException("DAV:locate-by-history element expected."); - } - Element versionHistorySet = info.getReportElement().getChild(XML_VERSION_HISTORY_SET, NAMESPACE); - if (versionHistorySet == null) { - throw new IllegalArgumentException("The DAV:locate-by-history element must contain a DAV:version-history-set child."); - } - - List l = versionHistorySet.getChildren(DavConstants.XML_HREF, DavConstants.NAMESPACE); - if (l != null && !l.isEmpty()) { - Iterator it = l.iterator(); - while (it.hasNext()) { - String href = ((Element)it.next()).getText(); - if (href != null) { - vhHrefSet.add(href); - } - } - } - this.info = info; - } - - /** - * Run the report. - * - * @return Xml Document representing the report in the required - * format. - * @throws DavException - * @see Report#toXml() - */ - public Document toXml() throws DavException { - if (info == null || resource == null) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while running DAV:locate-by-history report"); - } - - MultiStatus ms = new MultiStatus(); - buildResponse(resource, info.getPropertyNameSet(), info.getDepth(), ms); - return ms.toXml(); - } - - /** - * Fill the MultiStatus with the MultiStatusResponses - * generated for the specified resource and its members according to the - * depth value. - * - * @param res - * @param propNameSet - * @param depth - * @param ms - * @throws DavException - */ - private void buildResponse(DavResource res, DavPropertyNameSet propNameSet, - int depth, MultiStatus ms) throws DavException { - // loop over members first, since this report only list members - DavResourceIterator it = res.getMembers(); - while (!vhHrefSet.isEmpty() && it.hasNext()) { - DavResource childRes = it.nextResource(); - if (childRes instanceof VersionControlledResource) { - try { - VersionHistoryResource vhr = ((VersionControlledResource)childRes).getVersionHistory(); - if (vhHrefSet.remove(vhr.getHref())) { - if (propNameSet.isEmpty()) { - ms.addResourceStatus(childRes, DavServletResponse.SC_OK, 0); - } else { - ms.addResourceProperties(childRes, propNameSet, 0); - } - } - } catch (DavException e) { - log.info(e.getMessage()); - } - } - // traverse subtree - if (depth > 0) { - buildResponse(it.nextResource(), propNameSet, depth-1, ms); - } - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/Report.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/Report.java deleted file mode 100644 index eaf3501095b..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/Report.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version.report; - -import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.version.DeltaVResource; -import org.jdom.Document; - -/** - * The Report interface defines METHODS needed in order to respond - * to a REPORT request. The REPORT method is a required feature to all - * DeltaV resources. - * - * @see DeltaVResource#getReport(ReportInfo) - */ -public interface Report { - - /** - * Returns the registered type of this report. - * - * @return the type of this report. - */ - public ReportType getType(); - - /** - * Set the DeltaVResource for which this report was requested. - * - * @param resource - */ - public void setResource(DeltaVResource resource); - - /** - * Set the ReportInfo as specified by the REPORT request body, - * that defines the details for this report. - * - * @param info providing in detail requirements for this report. - */ - public void setInfo(ReportInfo info); - - /** - * Returns the report {@link Document Xml document} defined by the this - * ReportType. The document will be returned in the response - * body. - * - * @return Xml Document object representing the generated report - * in the proper format. - * @throws DavException if an error occurs while running the report or - * creating the Document. - */ - public Document toXml() throws DavException; -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ReportInfo.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ReportInfo.java deleted file mode 100644 index 134274e9b81..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ReportInfo.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; -import org.apache.jackrabbit.webdav.DavConstants; -import org.jdom.Element; - -/** - * The ReportInfo class encapsulates the body of a REPORT request. - * RFC 3253 the top Xml element - * being the name of the requested report. In addition a Depth header may - * be present (default value: {@link DavConstants#DEPTH_0}). - */ -public class ReportInfo { - - private static Logger log = Logger.getLogger(ReportInfo.class); - - private final Element reportElement; - private final int depth; - - /** - * Create a new ReportInfo object. - * - * @param reportElement - * @param depth Depth value as retrieved from the {@link DavConstants#HEADER_DEPTH}. - */ - public ReportInfo(Element reportElement, int depth) { - this.reportElement = reportElement; - this.depth = depth; - } - - /** - * Returns the Xml element specifying the requested report. - * - * @return reportElement - */ - public Element getReportElement() { - return reportElement; - } - - /** - * Returns the depth field. The request must be applied separately to the - * collection itself and to all members of the collection that satisfy the - * depth value. - * - * @return depth - */ - public int getDepth() { - return depth; - } - - /** - * Returns a DavPropertyNameSet providing the property names present - * in an eventual {@link DavConstants#XML_PROP} child element. If no such - * child element is present an empty set is returned. - * - * @return {@link DavPropertyNameSet} providing the property names present - * in an eventual {@link DavConstants#XML_PROP DAV:prop} child element or an empty set. - */ - public DavPropertyNameSet getPropertyNameSet() { - Element propElement = reportElement.getChild(DavConstants.XML_PROP, DavConstants.NAMESPACE); - if (propElement != null) { - return new DavPropertyNameSet(propElement); - } else { - return new DavPropertyNameSet(); - } - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ReportType.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ReportType.java deleted file mode 100644 index 3d21bd948ea..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/ReportType.java +++ /dev/null @@ -1,169 +0,0 @@ -/* -* Copyright 2004 The Apache Software Foundation. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -package org.apache.jackrabbit.webdav.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.version.DeltaVConstants; -import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.DavServletResponse; -import org.jdom.Element; -import org.jdom.Namespace; - -import java.util.HashMap; - -/** - * ReportType... - */ -public class ReportType implements DeltaVConstants { - - private static Logger log = Logger.getLogger(ReportType.class); - - private static final HashMap types = new HashMap(); - - public static final ReportType VERSION_TREE = register(XML_VERSION_TREE, NAMESPACE, VersionTreeReport.class); - public static final ReportType EXPAND_PROPERTY = register(XML_EXPAND_PROPERTY, NAMESPACE, ExpandPropertyReport.class); - public static final ReportType LOCATE_BY_HISTORY = register(XML_LOCATE_BY_HISTORY, NAMESPACE, LocateByHistoryReport.class); - - private final String name; - private final Namespace namespace; - private final Class reportClass; - - /** - * Private constructor - * - * @see #register(String, Namespace, Class) - */ - private ReportType(String name, Namespace namespace, Class reportClass) { - this.name = name; - this.namespace = namespace; - this.reportClass = reportClass; - } - - /** - * Creates a new {@link Report} with this type. - * - * @return - * @throws DavException - */ - public Report createReport() throws DavException { - try { - return (Report) reportClass.getConstructor(new Class[0]).newInstance(new Object[0]); - } catch (Exception e) { - // should never occur - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Failed to register Report."); - } - } - - /** - * Returns an Xml element representing this report type - * - * @return Xml representation - */ - public Element toXml() { - return new Element(name, namespace); - } - - /** - * Returns true if this ReportType is requested by the given - * ReportInfo - * - * @param reqInfo - * @return - */ - public boolean isRequestedReportType(ReportInfo reqInfo) { - if (reqInfo != null) { - Element elem = reqInfo.getReportElement(); - if (elem != null) { - return name.equals(elem.getName()) && namespace.equals(elem.getNamespace()); - } - } - return false; - } - - /** - * Register the report type with the given name, namespace and class, that can - * run that report. - * - * @param name - * @param namespace - * @param reportClass - * @return - * @throws IllegalArgumentException if either parameter is null or - * if the given class does not implement the {@link Report} interface or if - * it does not provide an empty constructor. - */ - public static ReportType register(String name, Namespace namespace, Class reportClass) { - if (name == null || namespace == null || reportClass == null) { - throw new IllegalArgumentException("A ReportType cannot be registered with a null name, namespace or report class"); - } - - String key = buildKey(namespace, name); - if (types.containsKey(key)) { - return (ReportType) types.get(key); - } else { - // test if this report class has an empty constructor and implements Report interface - boolean isValidClass = false; - Class[] interfaces = reportClass.getInterfaces(); - for (int i = 0; i < interfaces.length && !isValidClass; i++) { - isValidClass = (interfaces[i] == Report.class); - } - if (!isValidClass) { - throw new IllegalArgumentException("The specified report class must implement the Report interface."); - } - - try { - reportClass.getConstructor(new Class[0]); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("The specified report class must provide a default constructor."); - } - - ReportType type = new ReportType(name, namespace, reportClass); - types.put(key, type); - return type; - } - } - - /** - * Return the ReportType requested by the given report info object. - * - * @param reportInfo - * @return the requested ReportType - * @throws IllegalArgumentException if the reportInfo is null or - * if the requested report type has not been registered yet. - */ - public static ReportType getType(ReportInfo reportInfo) { - if (reportInfo == null) { - throw new IllegalArgumentException("ReportInfo must not be null."); - } - String key = buildKey(reportInfo.getReportElement().getNamespace(), reportInfo.getReportElement().getName()); - if (types.containsKey(key)) { - return (ReportType) types.get(key); - } else { - throw new IllegalArgumentException("The request report '"+key+"' has not been registered yet."); - } - } - - /** - * Build the key from the given namespace and name. - * - * @param namespace - * @param name - * @return key identifying the report with the given namespace and name - */ - private static String buildKey(Namespace namespace, String name) { - return "{" + namespace.getURI() + "}" + name; - } -} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/SupportedReportSetProperty.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/SupportedReportSetProperty.java deleted file mode 100644 index edd4ab4b448..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/SupportedReportSetProperty.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.version.DeltaVConstants; -import org.apache.jackrabbit.webdav.property.AbstractDavProperty; -import org.jdom.Element; - -import java.util.HashSet; -import java.util.Iterator; - -/** - * SupportedReportSetProperty represents the DAV:supported-report-set - * property defined by RFC 3253. It identifies the reports that are supported by - * the given resource. - *
- * <!ELEMENT supported-report-set (supported-report*)>
- * <!ELEMENT supported-report report>
- * <!ELEMENT report ANY>
- * ANY value: a report element type
- * 
- */ -public class SupportedReportSetProperty extends AbstractDavProperty { - - private static Logger log = Logger.getLogger(SupportedReportSetProperty.class); - - private final HashSet reportTypes = new HashSet(); - - /** - * Create a new empty SupportedReportSetProperty. - */ - public SupportedReportSetProperty() { - super(DeltaVConstants.SUPPORTED_REPORT_SET, true); - } - - /** - * Create a new SupportedReportSetProperty property. - * - * @param reportTypes that are supported by the resource having this property. - */ - public SupportedReportSetProperty(ReportType[] reportTypes) { - super(DeltaVConstants.SUPPORTED_REPORT_SET, true); - for (int i = 0; i < reportTypes.length; i++) { - addReportType(reportTypes[i]); - } - } - - /** - * Add an additional report type to this property's value. - * - * @param reportType - */ - public void addReportType(ReportType reportType) { - reportTypes.add(reportType); - } - - /** - * Returns true if the report type indicated in the specified RequestInfo - * object is included in the supported reports. - * - * @param reqInfo - * @return true if the requested report is supported. - */ - public boolean isSupportedReport(ReportInfo reqInfo) { - Iterator it = reportTypes.iterator(); - while (it.hasNext()) { - ReportType rt = (ReportType)it.next(); - if (rt.isRequestedReportType(reqInfo)) { - return true; - } - } - return false; - } - - /** - * Returns the Xml representation of this property. - * - * @return Xml representation listing all supported reports - * @see org.apache.jackrabbit.webdav.property.DavProperty#toXml() - */ - public Element toXml() { - Element elem = getName().toXml(); - Iterator it = reportTypes.iterator(); - while (it.hasNext()) { - Element sr = new Element(DeltaVConstants.XML_SUPPORTED_REPORT, DeltaVConstants.NAMESPACE); - Element r = new Element(DeltaVConstants.XML_REPORT, DeltaVConstants.NAMESPACE); - elem.addContent(sr.addContent(r.addContent(((ReportType)it.next()).toXml()))); - } - return elem; - } - - /** - * Returns a set of report types. - * - * @return set of {@link ReportType}. - * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() - */ - public Object getValue() { - return reportTypes; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/VersionTreeReport.java b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/VersionTreeReport.java deleted file mode 100644 index 46e14a25f49..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/VersionTreeReport.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.webdav.version.report; - -import org.apache.log4j.Logger; -import org.apache.jackrabbit.webdav.*; -import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; -import org.apache.jackrabbit.webdav.version.*; -import org.jdom.Document; - -/** - * VersionTreeReport encapsulates the DAV:version-tree report. - * It describes the requested properties of all the versions in the version - * history of a version. The DAV:version-tree report must be supported by all - * version resources and all version-controlled resources. - */ -public class VersionTreeReport implements Report, DeltaVConstants { - - private static Logger log = Logger.getLogger(VersionTreeReport.class); - - private ReportInfo info; - private DeltaVResource resource; - - public ReportType getType() { - return ReportType.VERSION_TREE; - } - - /** - * Set the DeltaVResource used to register this report. - * - * @param resource - * @throws IllegalArgumentException if the given resource is neither - * {@link VersionControlledResource} nor {@link VersionResource}. - * @see Report#setResource(org.apache.jackrabbit.webdav.version.DeltaVResource) - */ - public void setResource(DeltaVResource resource) throws IllegalArgumentException { - if (resource instanceof VersionControlledResource || resource instanceof VersionResource) { - this.resource = resource; - } else { - throw new IllegalArgumentException("DAV:version-tree report can only be created for version-controlled resources and version resources."); - } - } - - /** - * Set the ReportInfo as specified by the REPORT request body, - * that defines the details for this report. - * - * @param info - * @throws IllegalArgumentException if the given ReportInfo - * does not contain a DAV:version-tree element. - * @see Report#setInfo(ReportInfo) - */ - public void setInfo(ReportInfo info) throws IllegalArgumentException { - if (info == null || !XML_VERSION_TREE.equals(info.getReportElement().getName())) { - throw new IllegalArgumentException("DAV:version-tree element expected."); - } - this.info = info; - } - - /** - * Runs the DAV:version-tree report. - * - * @return Xml Document representing the report in the required - * format. - * @throws DavException if the resource or the info field are null - * or if any other error occurs. - * @see Report#toXml() - */ - public Document toXml() throws DavException { - if (info == null || resource == null) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while running DAV:version-tree report"); - } - - MultiStatus ms = new MultiStatus(); - buildResponse(resource, info.getPropertyNameSet(), info.getDepth(), ms); - return ms.toXml(); - } - - /** - * - * @param res - * @param propNameSet - * @param depth - * @param ms - * @throws DavException - */ - private void buildResponse(DavResource res, DavPropertyNameSet propNameSet, - int depth, MultiStatus ms) throws DavException { - VersionResource[] versions = getVersions(res); - for (int i = 0; i < versions.length; i++) { - if (propNameSet.isEmpty()) { - ms.addResourceStatus(versions[i], DavServletResponse.SC_OK, 0); - } else { - ms.addResourceProperties(versions[i], propNameSet, 0); - } - } - if (depth > 0) { - DavResourceIterator it = res.getMembers(); - while (it.hasNext()) { - buildResponse(it.nextResource(), propNameSet, depth-1, ms); - } - } - } - - /** - * Retrieve all versions from the version history associated with the given - * resource. If the versions cannot be retrieved from the given resource - * an exception is thrown. - * - * @param res - * @return array of {@link VersionResource}s or an empty array if the versions - * could not be retrieved. - * @throws DavException if the version history could not be retrieved from - * the given resource or if an error occurs while accessing the versions - * from the version history resource. - */ - private static VersionResource[] getVersions(DavResource res) throws DavException { - VersionResource[] versions = new VersionResource[0]; - if (res instanceof VersionControlledResource) { - versions = ((VersionControlledResource)res).getVersionHistory().getVersions(); - } else if (res instanceof VersionResource) { - versions = ((VersionResource)res).getVersionHistory().getVersions(); - } - return versions; - } -} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/package.html b/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/package.html deleted file mode 100644 index 25f787c1ca7..00000000000 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/report/package.html +++ /dev/null @@ -1,3 +0,0 @@ - -Report interface and inplementation for default reports defined by RFC 3253. - \ No newline at end of file diff --git a/contrib/orm-persistence/HEADER.txt b/contrib/orm-persistence/HEADER.txt deleted file mode 100644 index 1c669de7240..00000000000 --- a/contrib/orm-persistence/HEADER.txt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ \ No newline at end of file diff --git a/contrib/orm-persistence/LICENSE.txt b/contrib/orm-persistence/LICENSE.txt deleted file mode 100644 index d6456956733..00000000000 --- a/contrib/orm-persistence/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT 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/contrib/orm-persistence/README.txt b/contrib/orm-persistence/README.txt deleted file mode 100644 index 04b663d5a3b..00000000000 --- a/contrib/orm-persistence/README.txt +++ /dev/null @@ -1,114 +0,0 @@ -ORM persistence managers for Jackrabbit README ----------------------------------------------- - -License (see also LICENSE.txt) ------------------------------- - -Copyright 2004-2005 The Apache Software Foundation or its licensors, - as applicable. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -Introduction ------------- - -This sub-project of Jackrabbit providers two ORM-based persistence manager -implementation. -- OJB (http://db.apache.org/ojb) -based persistence manager -- Hibernate (http://www.hibernate.org) -based persistence manager - -This also means that Jackrabbit can now store all content in a database. - -Requirements ------------- - -This project assumes that you have already successfully compiled and -installed the parent project Jackrabbit into your maven repository. If -this is not the case, go back to the root project and launch : - maven jar:install - -For running the full tests, you will require a rather large amount of -free memory, as the default setup with HSQLDB requires a lot of memory -when handling large BLOB files. - -Existing limitations --------------------- - -- Values (including multi-values) are stored in a single database column - in this implementation, limiting the length of values to the length - defines in the SQL schema and the mapping files (there is no hard-coded - limitation in the Java implementation though). - -Running -------- - -By default the package comes configured to run against an embedded HSQLDB -database. In order for the tests and the benchmark to run, you must first -run : - - maven clean - -The above resets the database, and clean the whole environment to run the -tests and/or the benchmark. You can then launch - - maven start.test.server - -This launches the database in server mode (full embedded mode was not -possible to implement because it doesn't finalize properly). Note that -your shell is now blocked while running the database (CTRL+C to stop it) -and you will need a second shell to run the tests/benchmarks - -From a second shell, you can now run - - maven - -This will run the tests. Please note that the tests may take a while -to run (1 minute on a P4 3.0Ghz). - -If you just want to build the project without the tests : - - maven -Dmaven.test.skip=true - -Recycling HSQLDB ----------------- - -If you want to restart a test session, here is the quick procedure to do so: - -1. In the shell that launched the database, press CTRL+C -2. In the other shell, type : maven clean (this resets the database to an - empty schema by copying a file, so don't do it while HSQLDB is running !) -3. Relaunch the database in it's shell by typing : maven start.test.server -That's it ! - -Using MySQL ------------ - -The ORM persistence managers have also been tested against a MySQL database. -In order to configure the persistence managers to use MySQL instead of HSQLDB -you need to change the following files, depending on whether you want to use -the OJB or Hibernate implementation : - -For OJB : applications/test/ojb/repository_database.xml -For Hibernate : applications/test/hibernate.properties - -This project also contains a database initialization script that contains the -DB schema : - - create_db_mysql.sql - -Also available is a batch script for Windows, that initializes a MySQL database -called "jackrabbit", and recycles it if it already exists : - - reset_db_mysql.bat - -Enjoy ! \ No newline at end of file diff --git a/contrib/orm-persistence/applications/test/OJB.properties b/contrib/orm-persistence/applications/test/OJB.properties deleted file mode 100644 index 8dd50980e99..00000000000 --- a/contrib/orm-persistence/applications/test/OJB.properties +++ /dev/null @@ -1,453 +0,0 @@ -# -# OJB.properties -- configuration of the OJB runtime environment -# Version: 1.0 -# (c) 2001, 2002, 2003 Apache Software Foundation -# Author: Thomas Mahler and many others -# @version $Id: OJB.properties,v 1.75 2004/06/27 23:36:23 arminw Exp $ -# -#---------------------------------------------------------------------------------------- -# repository file settings -#---------------------------------------------------------------------------------------- -# The repositoryFile entry tells OJB to use this file as as its standard mapping -# repository. The file is looked up from the classpath. -# -repositoryFile=ojb/repository.xml -# -# If the useSerializedRepository entry is set to true, OJB tries to load a -# serialized version of the repository for performance reasons. -# if set to false, OJB always loads the xml file. -# Setting this flag to true will accelerate the startup sequence of OJB. -# If set to true changes to the repository.xml file will only be detected -# after maually deleting the repository.xml.serialized file. -useSerializedRepository=false -# -# If Repository serialization is used the entry serializedRepositoryPath defines the -# directory where the Repository is written to and read from. -# this entry is used only when the useSerializedRepository flag is set to true -# -serializedRepositoryPath=. -# -#---------------------------------------------------------------------------------------- -# PersistenceBrokerFactory / PersistenceBroker -#---------------------------------------------------------------------------------------- -# The PersistenceBrokerFactoryClass entry decides which concrete -# PersistenceBrokerFactory implemention is to be used. -PersistenceBrokerFactoryClass=org.apache.ojb.broker.core.PersistenceBrokerFactoryDefaultImpl -# If in managed environment *only* the PB-api was used it's recommended to use this factory -# to enable the PersistenceBroker instances to participate in the JTA transaction. This makes -# e.g. PBStateListener work properly in managed environments. -#PersistenceBrokerFactoryClass=org.apache.ojb.broker.core.PersistenceBrokerFactorySyncImpl -# -# -# The PersistenceBrokerClass entry decides which concrete PersistenceBroker -# implementation is to be served by the PersistenceBrokerFactory. -# This is the singlevm implementation: -PersistenceBrokerClass=org.apache.ojb.broker.core.PersistenceBrokerImpl -# -# This is an implementation that uses Prevayler (prevayler.sf.net) as the persistent storage. -# Using this implementation OJB works as a simple OODBMS -#PersistenceBrokerClass=org.apache.ojb.broker.prevayler.PBPrevaylerImpl -# -#---------------------------------------------------------------------------------------- -# PersistenceBroker pool -#---------------------------------------------------------------------------------------- -# PersistenceBroker pool configuration -# This pool uses the jakarta-commons-pool api. -# There you can find things described in detail. -# -# maximum number of brokers that can be borrowed from the -# pool at one time. When non-positive, there is no limit. -maxActive=100 -# -# controls the maximum number of brokers that can sit idle in the -# pool (per key) at any time. When non-positive, there is no limit -maxIdle=-1 -# -# max time block to get broker instance from pool, after that exception is thrown. -# When non-positive, block till last judgement -maxWait=2000 -# -# indicates how long the eviction thread should sleep before "runs" of examining -# idle objects. When non-positive, no eviction thread will be launched. -timeBetweenEvictionRunsMillis=-1 -# -# specifies the minimum amount of time that an broker may sit idle -# in the pool before it is eligable for eviction due to idle time. -# When non-positive, no object will be dropped from the pool due -# to idle time alone (depends on timeBetweenEvictionRunsMillis > 0) -minEvictableIdleTimeMillis=1000000 -# -# specifies the behaviour of the pool when broker capacity is -# exhausted (see maxActive above) -# 0 - fail -# 1 - block -# 2 - grow -whenExhaustedAction=0 -# -# -#---------------------------------------------------------------------------------------- -# ConnectionFactory / Default ConnectionPool -#---------------------------------------------------------------------------------------- -# The ConnectionFactoryClass entry determines which kind of ConnectionFactory -# is to be used within org.apache.ojb as connection factory. -# A ConnectionFactory is responsible for creating -# JDBC Connections. Current version ships four implementations: -# -# 1. ConnectionFactoryNotPooledImpl -# No pooling, no playing around. -# Every connection request returns a new connection, -# every connection release close the connection. -# 2. ConnectionFactoryPooledImpl -# This implementation supports connection pooling. -# 3. ConnectionFactoryDBCPImpl -# Using the jakarta-DBCP api for connection management, support -# connection- and prepared statement-pooling, abandoned connection handling. -# 4. ConnectionFactoryManagedImpl -# Connection factory for use within managed environments - e.g. JBoss. -# Every obtained DataSource was wrapped within OJB (and ignore -# e.g. con.commit() calls within OJB). -# Use this implementation e.g if you use Datasources from an application server. -# -# Use the OJB performance tests to decide, which implementation is best for you. -# The proper way of obtaining a connection is configured in -# JDBCConnectionDescriptor entries in the repository.xml file. -# If want a more fine grained control of each connection pool used by OJB, -# take a look at the repository.dtd, there was a possibility to override -# this default connection factory entry in each JDBCConnectionDescriptor. -# -ConnectionFactoryClass=org.apache.ojb.broker.accesslayer.ConnectionFactoryPooledImpl -#ConnectionFactoryClass=org.apache.ojb.broker.accesslayer.ConnectionFactoryNotPooledImpl -#ConnectionFactoryClass=org.apache.ojb.broker.accesslayer.ConnectionFactoryManagedImpl -#ConnectionFactoryClass=org.apache.ojb.broker.accesslayer.ConnectionFactoryDBCPImpl -# -# -#---------------------------------------------------------------------------------------- -# ConnectionManager -#---------------------------------------------------------------------------------------- -# The ConnectionManagerClass entry defines the ConnectionManager implemementation to be used -ConnectionManagerClass=org.apache.ojb.broker.accesslayer.ConnectionManagerImpl -# -# -#---------------------------------------------------------------------------------------- -# SqlGenerator -#---------------------------------------------------------------------------------------- -# The SqlGeneratorClass entry defines the SqlGenerator implemementation to be used -SqlGeneratorClass=org.apache.ojb.broker.accesslayer.sql.SqlGeneratorDefaultImpl -# -# -#---------------------------------------------------------------------------------------- -# IndirectionHandler -#---------------------------------------------------------------------------------------- -# The IndirectionHandlerClass entry defines the class to be used by OJB's proxies to -# handle method invocations -# -IndirectionHandlerClass=org.apache.ojb.broker.core.proxy.IndirectionHandlerDefaultImpl -# -#---------------------------------------------------------------------------------------- -# ListProxy -#---------------------------------------------------------------------------------------- -# The ListProxyClass entry defines the proxy class to be used for collections that -# implement the java.util.List interface. -# -ListProxyClass=org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl -# -#---------------------------------------------------------------------------------------- -# SetProxy -#---------------------------------------------------------------------------------------- -# The SetProxyClass entry defines the proxy class to be used for collections that -# implement the java.util.Set interface. -# -SetProxyClass=org.apache.ojb.broker.core.proxy.SetProxyDefaultImpl -# -#---------------------------------------------------------------------------------------- -# CollectionProxy -#---------------------------------------------------------------------------------------- -# The CollectionProxyClass entry defines the proxy class to be used for collections that -# do not implement java.util.List or java.util.Set. -# -CollectionProxyClass=org.apache.ojb.broker.core.proxy.CollectionProxyDefaultImpl -# -#---------------------------------------------------------------------------------------- -# StatementManager -#---------------------------------------------------------------------------------------- -# The StatementManagerClass entry defines the StatementManager implemementation to be used -StatementManagerClass=org.apache.ojb.broker.accesslayer.StatementManager -# -# -#---------------------------------------------------------------------------------------- -# StatementsForClass -#---------------------------------------------------------------------------------------- -# The StatementsForClassClass entry defines the StatementsForClass implemementation to be used -# to implement cached statements. -StatementsForClassClass=org.apache.ojb.broker.accesslayer.StatementsForClassImpl -# -# -#---------------------------------------------------------------------------------------- -# JdbcAccess -#---------------------------------------------------------------------------------------- -# The JdbcAccessClass entry defines the JdbcAccess implemementation to be used -JdbcAccessClass=org.apache.ojb.broker.accesslayer.JdbcAccessImpl -# -# -#---------------------------------------------------------------------------------------- -# RowReader -#---------------------------------------------------------------------------------------- -# Set the standard RowReader implementation. It is also possible to specify the -# RowReader on class-descriptor level. -RowReaderDefaultClass=org.apache.ojb.broker.accesslayer.RowReaderDefaultImpl -# -#---------------------------------------------------------------------------------------- -# Object cache -#---------------------------------------------------------------------------------------- -# The ObjectCacheClass entry tells OJB which concrete ObjectCache -# implementation is to be used as standard cache. -# Its also possible to override this entry adding object-cache elements -# on jdbc-connection-descriptor level and -# per class-descriptor in repository file. More info see documentation. -# -ObjectCacheClass=org.apache.ojb.broker.cache.ObjectCacheDefaultImpl -#ObjectCacheClass=org.apache.ojb.broker.cache.ObjectCacheEmptyImpl -#ObjectCacheClass=org.apache.ojb.broker.cache.ObjectCachePerBrokerImpl -#ObjectCacheClass=org.apache.ojb.broker.cache.ObjectCacheJCSPerClassImpl -#ObjectCacheClass=org.apache.ojb.broker.cache.ObjectCachePerClassImpl -# -# -# This property is only relevant if the per class-descriptor object-cache -# declaration was used in conjunction with metadata runtime changes. -# If set 'flase' the class name of the object is used -# to find a per class ObjectCache implementation. -# If set 'true' the ObjectCacheDescriptor instance is used as key to -# find a per class ObjectCache, this enables to use different ObjectCache -# instances for the same class. -descriptorBasedCaches=false -# -# -# Use CacheFilters to do filter operations before caching methods were -# called. Build your own filter class by implementing org.apache.ojb.cache.CacheFilter. -# It is possible to use a arbitrary number of CacheFilters, but this slows -# down the performance of the cache, thus handle with care. -# -# - org.apache.ojb.broker.cache.CacheFilterClassImpl -# allows filtering of classes -# - org.apache.ojb.broker.cache.CacheFilterPackageImpl -# allows filtering of packages -# More info see Javadoc of the according classes. -# Set a comma separated list of CacheFilter. -#ObjectCacheFilter=org.apache.ojb.broker.cache.CacheFilterClassImpl,org.apache.ojb.broker.cache.CacheFilterPackageImpl -# -#---------------------------------------------------------------------------------------- -# Locking -#---------------------------------------------------------------------------------------- -# The LockManagerClass entry tells OJB which concrete LockManager -# implementation is to be used. -LockManagerClass=org.apache.ojb.odmg.locking.LockManagerDefaultImpl -# -# The LockMapClass entry tells OJB which concrete LockMap -# implementation is to be used. -# If OJB is running on multiple concurrent clients it is recommended -# to use the RemoteLockMapImpl. It guarantees to provide -# Lockmanagement across multiple JVMs. -# This Implemenation relies on a Servlet based Lockserver. To use it you have to -# deploy the ojb-lockserver.war into a Servlet engine. -# and you have to set the Property LockServletUrl to point to this servlet. -# (see LockServletUrl section below). -# If OJB is running in a single JVM (e.g. in a desktop app, or in a servlet -# engine) it is save to use the InMemoryLockMapImpl. Using it will result -# in a large performance gain. -#LockMapClass=org.apache.ojb.odmg.locking.RemoteLockMapImpl -LockMapClass=org.apache.ojb.odmg.locking.InMemoryLockMapImpl -# -# The LockTimeout entry defines the maximum time in milliseconds -# that a lock may be hold. Defaults to 60000 = 1 minute -LockTimeout=60000 -# -# The ImplicitLocking entry defines if implicit lock acquisition is -# to be used. If set to true OJB implicitely locks objects to ODMG -# transactions after performing OQL queries. -# If implicit locking is used locking objects is recursive, that is -# associated objects are also locked. -# If ImplicitLocking is set to false, no locks are obtained in OQL -# queries and there is also no recursive locking. -ImplicitLocking=true -#ImplicitLocking=false -# -# -# The LockServletUrl entry points to the Lockserver servlet. -# This Servlet is addressed by all distributed JVMs if the RemoteLockMapImpl -# is used. -LockServletUrl=http://127.0.0.1:8080/ojb-lockserver -# -# -# The LockAssociations entry defines the behaviour for the OJB -# implicit locking feature. If set to WRITE (default) acquiring a write- -# lock on a given object x implies write locks on all objects associated -# to x. If set to READ implicit read-locks are acquired. -# Acquiring a read-lock on x thus allways results in implicit read-locks -# on all associated objects. -#LockAssociations=READ -LockAssociations=WRITE -# -# -#---------------------------------------------------------------------------------------- -# OQL / SQL settings -#---------------------------------------------------------------------------------------- -# The OqlCollectionClass entry defines the collection type returned -# from OQL queries. By default this value is set to DListImpl. -# This will be good for most situations as DList allows maximum flexibility -# in a ODMG environment. See also section 'ODMG settings'. -# Using DLists for large resultsets may be bad for application performance. -# For these scenarios you can use ArrayLists or Vectors. -# Important note: the collections class to be used MUST implement the -# interface org.apache.ojb.broker.ManageableCollection. -# -OqlCollectionClass=org.apache.ojb.odmg.collections.DListImpl_2 -# OqlCollectionClass=org.apache.ojb.broker.util.collections.ManageableArrayList -# OqlCollectionClass=org.apache.ojb.broker.util.ManageableVector -# -# The SqlInLimit entry limits the number of values in IN-sql statement, -# -1 for no limits. This hint is used in Criteria. -SqlInLimit=200 -# -# -#---------------------------------------------------------------------------------------- -# ODMG settings -#---------------------------------------------------------------------------------------- -# Specify the used base class for ODMG API -# - ImplementationDefaultImpl is the default class -# - ImplementationJTAImpl is for use in managed environments like J2EE conform -# Application Server -# -ImplementationClass=org.apache.ojb.odmg.ImplementationImpl -#ImplementationClass=org.apache.ojb.odmg.ImplementationJTAImpl -# -# -# Specify the used tx handling. -# - LocalTxManager use if you want the transaction to be associated by a thread -# - JTATxManager use if you want the transaction to be associated via the Transaction -# manager that is in your application server. -# -OJBTxManagerClass=org.apache.ojb.odmg.LocalTxManager -#OJBTxManagerClass=org.apache.ojb.odmg.JTATxManager -# -# -# Used ODMG collection implementation classes -# (e.g. when do a Implementation#newDlist() call) -# -# org.odmg.DList implementation class -DListClass=org.apache.ojb.odmg.collections.DListImpl_2 -#DListClass=org.apache.ojb.odmg.collections.DListImpl -# -# org.odmg.DArray implementation class -DArrayClass=org.apache.ojb.odmg.collections.DListImpl_2 -#DArrayClass=org.apache.ojb.odmg.collections.DListImpl -# -# org.odmg.DMap implementation class -DMapClass=org.apache.ojb.odmg.collections.DMapImpl -# -# org.odmg.DBag implementation class -DBagClass=org.apache.ojb.odmg.collections.DBagImpl -# -# org.odmg.DSet implementation class -DSetClass=org.apache.ojb.odmg.collections.DSetImpl -# -# -#---------------------------------------------------------------------------------------- -# Meta data / mapping settings -#---------------------------------------------------------------------------------------- -# The PersistentFieldClass property defines the implementation class -# for PersistentField attributes used in the OJB MetaData layer. -# By default the best performing attribute/refection based implementation -# is selected (PersistentFieldDirectAccessImpl). -# -# - PersistentFieldDirectAccessImpl -# is a high-speed version of the access strategies. -# It does not cooperate with an AccessController, -# but accesses the fields directly. Persistent -# attributes don't need getters and setters -# and don't have to be declared public or protected -# - PersistentFieldPrivilegedImpl -# Same as above, but does cooperate with AccessController and do not -# suppress the java language access check (but is slow compared with direct access). -# - PersistentFieldIntrospectorImpl -# uses JavaBeans compliant calls only to access persistent attributes. -# No Reflection is needed. But for each attribute xxx there must be -# public getXxx() and setXxx() methods. -# - PersistentFieldDynaBeanAccessImpl -# implementation used to access a property from a -# org.apache.commons.beanutils.DynaBean. -# - PersistentFieldAutoProxyImpl -# for each field determines upon first access how to access this particular field -# (directly, as a bean, as a dyna bean) and then uses that strategy -# -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldDirectAccessImpl -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldPrivilegedImpl -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldIntrospectorImpl -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldDynaBeanAccessImpl -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldAutoProxyImpl -# -# Here are the new upcoming PersistentField implementations. These classes will replace the -# 'old' ones on next release. They pass the test-suite, but should be tested by community too. -# The new implementations are about 50 times faster in handling nested fields. -PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldDirectAccessImplNew -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldPrivilegedImplNew -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldIntrospectorImplNew -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldAutoProxyImpl -#PersistentFieldClass=org.apache.ojb.broker.metadata.fieldaccess.PersistentFieldDynaBeanImplNew -#(DynaBean implementation does not support nested fields) -# -#---------------------------------------------------------------------------------------- -# Component Intercepting for Profiling and Tracing -#---------------------------------------------------------------------------------------- -# By enabling an InterceptorClass all OJB components will use -# this Interceptor. Interceptors allow advanced tracing and Profiling -# of all component method calls. -# This is currently an experimental feature useful only for OJB kernel developers. -# -#InterceptorClass=org.apache.ojb.broker.util.interceptor.TracingInterceptor -# -#---------------------------------------------------------------------------------------- -# Transaction Management and assocation -#---------------------------------------------------------------------------------------- -# (optional, only used when OJB runs within managed environments) -# To praticipate in JTA transaction OJB needs access to the underlying transaction manager. -# The TransactionManager is acquired in different ways dependent on the application server. -# The JTATransactionManagerClass property allows you to specify the class that implements -# the proper behaviour for finding the transaction manager. Only use when OJBTxManagerClass -# is set to a factory that uses the application server transaction manager -# (org.apache.ojb.odmg.JTATxManager) -# -# JBoss Transaction Manager Factory -JTATransactionManagerClass=org.apache.ojb.broker.transaction.tm.JBossTransactionManagerFactory -# Weblogic Transaction Manager Factory -#JTATransactionManagerClass=org.apache.ojb.broker.transaction.tm.WeblogicTransactionManagerFactory -# WebSphere transaction manager factory -#JTATransactionManagerClass=org.apache.ojb.broker.transaction.tm.WebSphereTransactionManagerFactory -# Orion transaction manager factory -#JTATransactionManagerClass=org.apache.ojb.broker.transaction.tm.OrionTransactionManagerFactory -# SunOne transaction manager factory -#JTATransactionManagerClass=org.apache.ojb.broker.transaction.tm.SunOneTransactionManagerFactory -# JOnAs transaction manager factory -#JTATransactionManagerClass=org.apache.ojb.broker.transaction.tm.JOnASTransactionManagerFactory -# -# -#---------------------------------------------------------------------------------------- -# Logging settings are now in their own file, OJB-logging.properties -#---------------------------------------------------------------------------------------- -#---------------------------------------------------------------------------------------- -# End of OJB.properties file -#---------------------------------------------------------------------------------------- diff --git a/contrib/orm-persistence/applications/test/ehcache.xml b/contrib/orm-persistence/applications/test/ehcache.xml deleted file mode 100644 index ede25912550..00000000000 --- a/contrib/orm-persistence/applications/test/ehcache.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - - - - - - - - - - - --> - - - - - - - - diff --git a/contrib/orm-persistence/applications/test/hibernate.cfg.xml b/contrib/orm-persistence/applications/test/hibernate.cfg.xml deleted file mode 100644 index c68bc7a6b96..00000000000 --- a/contrib/orm-persistence/applications/test/hibernate.cfg.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - false - - - - - - - diff --git a/contrib/orm-persistence/applications/test/hibernate.properties b/contrib/orm-persistence/applications/test/hibernate.properties deleted file mode 100644 index 5794e81b19a..00000000000 --- a/contrib/orm-persistence/applications/test/hibernate.properties +++ /dev/null @@ -1,418 +0,0 @@ -# Copyright 2003-2005 The Apache Software Foundation or its licensors, -# as applicable -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -###################### -### Query Language ### -###################### - -## define query language constants / function names - -hibernate.query.substitutions true 1, false 0, yes 'Y', no 'N' - - - -################# -### Platforms ### -################# - -## JNDI Datasource - -#hibernate.connection.datasource jdbc/test -#hibernate.connection.username db2 -#hibernate.connection.password db2 - - -## HypersonicSQL - -hibernate.dialect net.sf.hibernate.dialect.HSQLDialect -hibernate.connection.driver_class org.hsqldb.jdbcDriver -hibernate.connection.username sa -hibernate.connection.password -hibernate.connection.url jdbc:hsqldb:hsql://localhost -#hibernate.connection.url jdbc:hsqldb:test -#hibernate.connection.url jdbc:hsqldb:. - - -## PostgreSQL - -#hibernate.dialect net.sf.hibernate.dialect.PostgreSQLDialect -#hibernate.connection.driver_class org.postgresql.Driver -#hibernate.connection.url jdbc:postgresql:template1 -#hibernate.connection.username pg -#hibernate.connection.password -#hibernate.query.substitutions yes 'Y', no 'N' - - -## DB2 - -#hibernate.dialect net.sf.hibernate.dialect.DB2Dialect -#hibernate.connection.driver_class COM.ibm.db2.jdbc.app.DB2Driver -#hibernate.connection.url jdbc:db2:test -#hibernate.connection.username db2 -#hibernate.connection.password db2 - - -## DB2/400 - -#hibernate.dialect net.sf.hibernate.dialect.DB2400Dialect -#hibernate.connection.username user -#hibernate.connection.password password - -## Native driver -#hibernate.connection.driver_class COM.ibm.db2.jdbc.app.DB2Driver -#hibernate.connection.url jdbc:db2://systemname - -## Toolbox driver -#hibernate.connection.driver_class com.ibm.as400.access.AS400JDBCDriver -#hibernate.connection.url jdbc:as400://systemname - - -## MySQL - -#hibernate.dialect net.sf.hibernate.dialect.MySQLDialect -#hibernate.connection.driver_class org.gjt.mm.mysql.Driver -#hibernate.connection.driver_class com.mysql.jdbc.Driver -#hibernate.connection.url jdbc:mysql:///jackrabbit -#hibernate.connection.username root -#hibernate.connection.password - - -## Oracle - -#hibernate.dialect net.sf.hibernate.dialect.Oracle9Dialect -#hibernate.dialect net.sf.hibernate.dialect.OracleDialect -#hibernate.connection.driver_class oracle.jdbc.driver.OracleDriver -#hibernate.connection.username ora -#hibernate.connection.password ora -#hibernate.connection.url jdbc:oracle:thin:@localhost:1521:test - - -## Sybase - -#hibernate.dialect net.sf.hibernate.dialect.SybaseDialect -#hibernate.connection.driver_class com.sybase.jdbc2.jdbc.SybDriver -#hibernate.connection.username sa -#hibernate.connection.password sasasa -#hibernate.connection.url jdbc:sybase:Tds:co3061835-a:5000/tempdb - - -## Mckoi SQL - -#hibernate.dialect net.sf.hibernate.dialect.MckoiDialect -#hibernate.connection.driver_class com.mckoi.JDBCDriver -#hibernate.connection.url jdbc:mckoi:/// -#hibernate.connection.url jdbc:mckoi:local://C:/mckoi1.00/db.conf -#hibernate.connection.username admin -#hibernate.connection.password nimda - - -## SAP DB - -#hibernate.dialect net.sf.hibernate.dialect.SAPDBDialect -#hibernate.connection.driver_class com.sap.dbtech.jdbc.DriverSapDB -#hibernate.connection.url jdbc:sapdb://localhost/TST -#hibernate.connection.username TEST -#hibernate.connection.password TEST -#hibernate.query.substitutions yes 'Y', no 'N' - - -## MS SQL Server - -#hibernate.dialect net.sf.hibernate.dialect.SQLServerDialect -#hibernate.connection.username sa -#hibernate.connection.password sa - -## JSQL Driver -#hibernate.connection.driver_class com.jnetdirect.jsql.JSQLDriver -#hibernate.connection.url jdbc:JSQLConnect://1E1/test - -## JTURBO Driver -#hibernate.connection.driver_class com.newatlanta.jturbo.driver.Driver -#hibernate.connection.url jdbc:JTurbo://1E1:1433/test - -## WebLogic Driver -#hibernate.connection.driver_class weblogic.jdbc.mssqlserver4.Driver -#hibernate.connection.url jdbc:weblogic:mssqlserver4:1E1:1433 - -## Microsoft Driver (not recommended!) -#hibernate.connection.driver_class com.microsoft.jdbc.sqlserver.SQLServerDriver -#hibernate.connection.url jdbc:microsoft:sqlserver://1E1;DatabaseName=test;SelectMethod=cursor - -## jTDS (since version 0.9) -#hibernate.connection.driver_class net.sourceforge.jtds.jdbc.Driver -#hibernate.connection.url jdbc:jtds:sqlserver://1E1/test - -## Interbase - -#hibernate.dialect net.sf.hibernate.dialect.InterbaseDialect -#hibernate.connection.username sysdba -#hibernate.connection.password masterkey - -## DO NOT specify hibernate.connection.sqlDialect - -## InterClient - -#hibernate.connection.driver_class interbase.interclient.Driver -#hibernate.connection.url jdbc:interbase://localhost:3060/C:/firebird/test.gdb - -## Pure Java - -#hibernate.connection.driver_class org.firebirdsql.jdbc.FBDriver -#hibernate.connection.url jdbc:firebirdsql:localhost/3050:/firebird/test.gdb - - -## Pointbase - -#hibernate.dialect net.sf.hibernate.dialect.PointbaseDialect -#hibernate.connection.driver_class com.pointbase.jdbc.jdbcUniversalDriver -#hibernate.connection.url jdbc:pointbase:embedded:sample -#hibernate.connection.username PBPUBLIC -#hibernate.connection.password PBPUBLIC - - - -################################# -### Hibernate Connection Pool ### -################################# - -hibernate.connection.pool_size 10 - - - -########################### -### C3P0 Connection Pool### -########################### - -#hibernate.c3p0.max_size 2 -#hibernate.c3p0.min_size 2 -#hibernate.c3p0.timeout 5000 -#hibernate.c3p0.max_statements 100 -#hibernate.c3p0.idle_test_period 3000 -#hibernate.c3p0.acquire_increment 2 -##hibernate.c3p0.validate false - - - -################################### -### Apache DBCP Connection Pool ### -################################### - -## connection pool - -hibernate.dbcp.maxActive 100 -hibernate.dbcp.whenExhaustedAction 1 -hibernate.dbcp.maxWait 120000 -hibernate.dbcp.maxIdle 10 - -## prepared statement cache - -hibernate.dbcp.ps.maxActive 100 -hibernate.dbcp.ps.whenExhaustedAction 1 -hibernate.dbcp.ps.maxWait 120000 -hibernate.dbcp.ps.maxIdle 10 - -## optional query to validate pooled connections: - -#hibernate.dbcp.validationQuery select 1 from dual -#hibernate.dbcp.testOnBorrow true -#hibernate.dbcp.testOnReturn false - - - -############################## -### Proxool Connection Pool### -############################## - -## Properties for external configuration of Proxool - -hibernate.proxool.pool_alias pool1 - -## Only need one of the following - -#hibernate.proxool.existing_pool true -#hibernate.proxool.xml proxool.xml -#hibernate.proxool.properties proxool.properties - - - -################################# -### Plugin ConnectionProvider ### -################################# - -## use a custom ConnectionProvider (if not set, Hibernate will choose a built-in ConnectionProvider using hueristics) - -#hibernate.connection.provider_class net.sf.hibernate.connection.DriverManagerConnectionProvider -#hibernate.connection.provider_class net.sf.hibernate.connection.DatasourceConnectionProvider -#hibernate.connection.provider_class net.sf.hibernate.connection.C3P0ConnectionProvider -hibernate.connection.provider_class net.sf.hibernate.connection.DBCPConnectionProvider -#hibernate.connection.provider_class net.sf.hibernate.connection.ProxoolConnectionProvider - - - -####################### -### Transaction API ### -####################### - -## the Transaction API abstracts application code from the underlying JTA or JDBC transactions - -#hibernate.transaction.factory_class net.sf.hibernate.transaction.JTATransactionFactory -hibernate.transaction.factory_class net.sf.hibernate.transaction.JDBCTransactionFactory - - -## to use JTATransactionFactory, Hibernate must be able to locate the UserTransaction in JNDI -## default is java:comp/UserTransaction -## you do NOT need this setting if you specify hibernate.transaction.manager_lookup_class - -#jta.UserTransaction jta/usertransaction -#jta.UserTransaction javax.transaction.UserTransaction -#jta.UserTransaction UserTransaction - - -## to use JCS caching with JTA, Hibernate must be able to obtain the JTA TransactionManager - -#hibernate.transaction.manager_lookup_class net.sf.hibernate.transaction.JBossTransactionManagerLookup -#hibernate.transaction.manager_lookup_class net.sf.hibernate.transaction.WeblogicTransactionManagerLookup -#hibernate.transaction.manager_lookup_class net.sf.hibernate.transaction.WebSphereTransactionManagerLookup -#hibernate.transaction.manager_lookup_class net.sf.hibernate.transaction.OrionTransactionManagerLookup -#hibernate.transaction.manager_lookup_class net.sf.hibernate.transaction.ResinTransactionManagerLookup - - - -############################## -### Miscellaneous Settings ### -############################## - -## print all generated SQL to the console - -#hibernate.show_sql true - - -## auto schema export - -#hibernate.hbm2ddl.auto create-drop -#hibernate.hbm2ddl.auto create -hibernate.hbm2ddl.auto update - - -## specify a JDBC isolation level - -#hibernate.connection.isolation 4 - - -## set the JDBC fetch size - -hibernate.jdbc.fetch_size 25 - - -## set the maximum JDBC 2 batch size (a nonzero value enables batching) - -hibernate.jdbc.batch_size 20 - -## use JDBC batching for versioned data - -hibernate.jdbc.batch_versioned_data true - -## enable use of JDBC 2 scrollable ResultSets (specifying a Dialect will cause Hibernate to use a sensible default) - -#hibernate.jdbc.use_scrollable_resultset true - - -## use streams when writing binary types to / from JDBC - -hibernate.jdbc.use_streams_for_binary true - - -## use JDBC 3 PreparedStatement.getGeneratedKeys to get the identifier of an inserted row - -#hibernate.jdbc.use_get_generated_keys true - - -## specify a default schema for unqualified tablenames - -#hibernate.default_schema test - - -## use a custom stylesheet for XML generation (if not specified, hibernate-default.xslt will be used) - -#hibernate.xml.output_stylesheet C:/Hibernate/net/sf/hibernate/hibernate-default.xslt - - -## enable outerjoin fetching (specifying a Dialect will cause Hibernate to use sensible default) - -#hibernate.use_outer_join false - - -## set the maximum depth of the outer join fetch tree - -hibernate.max_fetch_depth 1 - - -## enable CGLIB reflection optimizer (enabled by default) - -hibernate.cglib.use_reflection_optimizer true - - - -########################## -### Second-level Cache ### -########################## - -## optimize chache for minimal "puts" instead of minimal "gets" (good for clustered cache) - -#hibernate.cache.use_minimal_puts true - - -## set a prefix for cache region names - -hibernate.cache.region_prefix hibernate.test - - -## enable the query cache - -hibernate.cache.use_query_cache true - - -## choose a cache implementation - -hibernate.cache.provider_class net.sf.hibernate.cache.EhCacheProvider -#hibernate.cache.provider_class net.sf.hibernate.cache.EmptyCacheProvider -#hibernate.cache.provider_class net.sf.hibernate.cache.HashtableCacheProvider -#hibernate.cache.provider_class net.sf.hibernate.cache.TreeCacheProvider -#hibernate.cache.provider_class net.sf.hibernate.cache.OSCacheProvider -#hibernate.cache.provider_class net.sf.hibernate.cache.JCSCacheProvider -#hibernate.cache.provider_class net.sf.hibernate.cache.SwarmCacheProvider - - - -############ -### JNDI ### -############ - -## specify a JNDI name for the SessionFactory - -#hibernate.session_factory_name hibernate/session_factory - - -## Hibernate uses JNDI to bind a name to a SessionFactory and to look up the JTA UserTransaction; -## if hibernate.jndi.* are not specified, Hibernate will use the default InitialContext() which -## is the best approach in an application server - -#file system -#hibernate.jndi.class com.sun.jndi.fscontext.RefFSContextFactory -#hibernate.jndi.url file:/ - -#WebSphere -#hibernate.jndi.class com.ibm.websphere.naming.WsnInitialContextFactory -#hibernate.jndi.url iiop://localhost:900/ \ No newline at end of file diff --git a/contrib/orm-persistence/applications/test/hibernate/jackrabbit.hbm.xml b/contrib/orm-persistence/applications/test/hibernate/jackrabbit.hbm.xml deleted file mode 100644 index 7a6abb8dd60..00000000000 --- a/contrib/orm-persistence/applications/test/hibernate/jackrabbit.hbm.xml +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/applications/test/import.xml b/contrib/orm-persistence/applications/test/import.xml deleted file mode 100644 index befdf01c30b..00000000000 --- a/contrib/orm-persistence/applications/test/import.xml +++ /dev/null @@ -1,732 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - EHC/Hôpital de Morges/C. Vandenbulcke, Responsable hygiène/octobre 2003 - - - - - diff --git a/contrib/orm-persistence/applications/test/largeimport.xml b/contrib/orm-persistence/applications/test/largeimport.xml deleted file mode 100644 index 302a2f74fe6..00000000000 --- a/contrib/orm-persistence/applications/test/largeimport.xml +++ /dev/null @@ -1,8742 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Capgemini - - - - (version du 01/06/2004) - - - - Il est rappelé que la proposition définitive fournie après l -' - audition fera office de CCTP pour le projet. Elle doit donc prendre en compte toutes les remarques, observations ou compléments échangés pendant la phase d -' - évaluation. Elle doit lever les éventuelles ambiguïtés sur le périmètre de la prestation : il est donc nécessaire de supprimer les mentions à des prestations optionnelles qui ne sont pas prévues dans le cadre du marché. Supprimer systématiquement les formes conditionnelles dans l -' - offre (pourrait, serait) lorsque les termes de la condition ne sont pas explicites. Préférer des formes au présent ou au futur. Un effort, devra notamment être fait sur le cadre de réponse - - - Les réponses aux questions écrites transmises en mai devront être annexées à la nouvelle proposition commerciale. Si des éléments sont contradictoires avec la nouvelle proposition commerciale il convient alors de le mentionner. - - - - Les réponses aux questions posées lors de l’audition doivent apparaître clairement dans la nouvelle proposition commerciale. - - - - - - - - 1 - - - ère - - - partie : présentation du candidat et de son organisation, reformulation du projet et compréhension du contexte (1 heure). Questions / réponses entre les membres de la commission et le candidat (30 min.). - - - - - - - - Démarrage anticipé : est-il prévu de commencer les développements sur des plateformes mises à disposition par le prestataire dans ses locaux. - - - - - - - De T0 à T0+9 semaines, il est simplement demandé des postes de travail J2EE (windows, 1 Go de mémoire). - - - - Les travaux peuvent donc se réaliser dans les locaux de la DPMA dès le T0 sur ces postes de travail. - - - - - - - Les candidats doivent préciser quelles seront les ressources humaines que les éditeurs mettront à la disposition de l -' - intégrateur pendant la réalisation du projet. Ces ressources doivent avoir une expérience d’au moins un projet de gestion de contenu et de portail. L -' - implication des éditeurs tout au long du projet sera un plus pour le Minefi. - - - - - - Codeva est intégré à hauteur de 80 jours durant la phase de développement des éléments génériques au titre du développement collaboratif. - - - Clément Hegger, chef de projet Codeva, assiste Capgemini tout au long du projet. - - - - - - - Ressources allouées au projet : La réalisation du prototype a-t-elle permis de dégager de nouveaux éléments techniques ou fonctionnels qui pourraient être mis en commun entre les espaces : sites alize, - - - www.minefi.gouv.fr - - - et leurs sous-sites? - - - - - - Les éléments techniques et fonctionnels transverses aux différents sites sont soit présents dans Jahia, soit déjà prévus dans les développements collaboratifs. - - - - - - Les prestataires devront préciser des URL de deux projets réalisés sur Internet si - - possible par eux et/ou avec les outils proposés. Ces projets doivent être le plus - - représentatifs possibles des attentes du Minefi notamment au travers de la personnalisation des portails. - - - - - - - - - - - - - - - Adresse URL - - - - - Référence - - - - - Portail - - - - - Gestion de contenu - - - - - Personnalisation - - - - - Technologies - - - - - - - - - http://Unil.ch - - - - - - - Codeva - - - - - O - - - - - O - - - - - MyUnil (constitution d’un bureau personnalisé). - - - - - Jahia - - - - - - - - - http://www.edfentreprise.com - - - - - - - Capgemini - - - - - O - - - - - O - - - - - Espace client propre à son profil + données personnelles - - - - - BEA Portal / - - Documentum - - - - - - - - - - http://barcelona.com - - - - - - - Codeva - - - - - O - - - - - O - - - - - N - - - - - Jahia - - - - - - - - - http://monagence.edf.fr - - - - - - - Capgemini - - - - - O - - - - - O - - - - - O (données personnelles) - - - - - Weblogic - - - - - - - - - http://www.culture.fr - - - - - - - Capgemini - - - - - O - - - - - O - - - - - N - - - - - Zope/CPS - - - - - - - - - - Définition fonctionnelle - - - - - Dans la phase à dominante fonctionnelle (telle que le cadrage fonctionnel ou l’étude différentielle) est-il prévu de faire intervenir des personnes dont le profil est fonctionnel, c’est à dire des personnes qui ont l’habitude de gérer du contenu et de piloter des chantiers de portail, y compris dans leur dimension ergonomique et graphique ? - - - Ou les interventions prévues se limitent-elles à des profils de type « expert technique » (programmeur, analyste, …) ? - - - - - Le chef de projet pressenti, Sandrine Dalbègue, a travaillé sur différents portails : - - - - www.cci.fr - - - - , - - - - www.culture.fr - - - - , ArchipelRH (portail RH d’EDF). Sandrine a également participé aux différentes évaluations techniques et fonctionnelles de Jahia. - - - - - - - - Définition fonctionnelle des modules développés en mode collaboratif - - - - - Confirmer que les fonctionnalités développées en mode collaboratif seront étudiées de façon autonome par le prestataire sans que le Minefi soit sollicité pour établir les spécifications fonctionnelles et techniques. La DPMA souhaite uniquement être informée lors de l’avancement des travaux. Préciser les moyens d -' - études et les livrables intermédiaires prévus pour ces travaux. - - - Fournir des engagements clairs concernant le plan de développement du produit JAHIA indépendamment du projet du Ministère. - - - - - Sont financées actuellement les évolutions et devraient être disponibles cet été : - - - - - - gestion des champs obligatoires, - - - - - Support JSR 168. - - - - - - - Sont prévues (notamment en fonction du choix de la DPMA) : - - - - - - Support des métadonnées Dublin Core standard, - - - - - - Réplication multi-site, - - - - - - Gestion d’un statut archivé, - - - - - - Gestion multi LDAP. - - - - - - - Est réalisé par un client de Jahia (date non fournie) : - - - - - Gestion de formulaires. - - - - - - - - - 2 - - - ème - - - partie : présentation de l’offre technique et financière du candidat (1 heure 30). Questions / réponses entre les membres de la commission et le candidat (30 min.). - - - - - - - Les sociétés doivent s -' - engager dans la francisation des libellés, des messages, des commandes et la documentation de l -' - ensemble des outils nécessaires au fonctionnement et au paramétrage du système livré. Il est à noter que pour les utilisateurs (intra/internautes et gestionnaires de contenu déconcentrés) cette demande est indispensable ; pour les administrateurs (de contenu Alize/Sircom et système); cette demande est souhaitable, mais n -' - est probablement pas une contrainte. - - - - - - La documentation de la version 4 est en cours de francisation. - - - Le produit est déjà francisé. - - - - Capgemini et Codeva s’engagent sur la francisation des libellés, des messages, des commandes, de Lucene et la documentation de l -' - ensemble des outils nécessaires au fonctionnement et au paramétrage du système livré. - - - - - - - - Moteur de recherche (performance -& - intégration) - - - - - - Les moteurs de recherche proposés ne semblent pas atteindre les performances demandées dans le PFD. - - - On rappelle que les performances des moteurs demandées au PFD doivent : - - - - - offrir de meilleures performances (dictionnaire, synonymie, lemmatisation avancée, correcteur orthographique..) - - - - - offrir une meilleure concaténation des résultats entre les recherches effectuées sur les différents sites portées sur la plateforme et les recherches effectuées sur les sites externes au système. Y-aura-t-il des mises en commun des éventuels dictionnaires mis en œuvre ? - - - - - - Le Minefi a réalisé un inventaire à partir d’information disponible sur internet pour déterminer les fonctionnalités des moteurs de recherche. - - - - Syntaxe de recherche - - - - - - - - - - - - - - - - - Recherche floue - - - - - Phonétisation - - - - - Masque de recherche - - - - - Troncature - - - - - Correction orthographique - - - - - Lémmatisation - - - - - Opérateurs booléens - - - - - - - Lucène : interne (Cap Gemini) - - - - - oui - - - - - oui - - - - - Oui (*1) - - - - - Oui - - - - - Oui (*2) - - - - - oui - - - - - oui - - - - - - - Nutch : externe (Cap Gemini) - - - - - oui - - - - - oui - - - - - Oui (*1) - - - - - Oui - - - - - Oui (*2) - - - - - oui - - - - - oui - - - - - - - - Personnalisation - - - - - - - - - - - - Adjonction de thésaurus / dictionnaire - - - - - Recherche sur champs / corpus spécifiques - - - - - - - Lucène : interne (Cap Gemini) - - - - - Option (*3) - - - - - oui - - - - - - - Nutch : externe (Cap Gemini) - - - - - Même option (*3) - - - - - oui - - - - - - - Pondération des résultats - - - - - - - - - - - - - Adéquation requête / document - - - - - Surpondération (champ / terme) - - - - - Indice de popularité - - - - - - - Lucène : interne (Cap Gemini) - - - - - oui - - - - - oui - - - - - non - - - - - - - Nutch : externe (Cap Gemini) - - - - - oui - - - - - oui - - - - - non - - - - - - - - - - - - (*1) : Masque de recherche : - - - Lucene supporte les recherches par jokers : *, ? comme : - - - Test*, Te*t, Te ?t. - - - - - (*2) : Recherche orthographique. - - - Cette fonctionnalité permet lorsque l’utilisateur déclenche sa recherche de : - - - - - Déclencher le moteur de recherche avec la saisie de l’utilisateur - - - - - Présenter une correction orthographique de ce qui a été saisi pour permettre à l’utilisateur de re-lancer une recherche sur les termes corrigés. - - - - - - Ce scénario correspond exactement au mécanisme du moteur de recherche Google : - - - - - - - - Capgemini propose de réaliser cette fonctionnalité au travers du composant Open Source JTextCheck ( - - - - http://linux.org.mt/projects/jtextcheck/index.html - - - - ). - - - - - - - Corriger ce tableau, en fonction de ce qui sera disponible dans Jahia et/ou développer dans le cadre de la prestation. - - - - - - L’outil prend-il en compte l’intégration et gestion d’un thésaurus - - - - - - - gestion d’un thésaurus (création/modification/imports/exports), - - - - - d’utilisation du thésaurus lors de la recherche plein texte. - - - - - Si la réponse est négative, il convient de le prévoir (cf. paragraphe 6.5.3 du PFD). Si cette fonctionnalité engendre un surcoût, celui-ci doit être intégré dans les options -" - moteur de recherche, offre optionnelle -" - (total 13 et total 14). - - - - (*3) : Thésaurus. - - - Le thésaurus est un dictionnaire particulier réalisant des liens (de type spécialisation, généralisation) entre les mots. - - - - Cet outil est une aide à l’élaboration des critères de recherches : - - - - - L’utilisateur saisi ses mots de recherche - - - - - Pour chaque mot, - - le thésaurus lui présente les mots liés qu’il ajoute ou réfute - - - - - Le moteur de recherche est alors lancé avec l’ensemble de ces critères. - - - - - - - - - - Capgemini propose de développer ce thésaurus qui permet : - - - - - - L’import (resp. l’export) des mots du thésaurus depuis (resp. vers) un fichier texte conforme à la norme ISO 2788 et AFNOR NFZ-47100 - - - - - - Assister l’utilisateur dans la saisie de ses critères de recherche. - - - - - - L’alimentation du fichier est réalisée par la DPMA en fonction des concepts manipulés. - - - - - - - Prise en compte des meta-données - - - - - L’outil de gestion de contenu doit prendre en compte les meta-données tels que définis par le Minefi notamment au travers des formulaires de gestion des documents publiés. (cf. - - - En annexe le « Tableau de synthèse Méta-données - -  » - -   qui décrit les travaux en cours par le Minefi. - - - - Date de révision : pris en compte dans la solution. - - - - La date de fin de publication n’est pas une méta-données du document, puisque sa valeur peut être différentes en fonction de la rubrique où le document est publié. - - - - Capgemini propose de développer un workflow gérant cette date de fin de publication. - - - Le responsable de la rubrique avant la validation de la publication détermine à quelle date la page doit disparaître. - - - - Par contre, pour les documents saisis en mode edit/publish, la date de fin de publication ne peut être rattachée qu’au document. L’information est alors ‘dépubliée’ dans toutes les rubriques en même temps. - - - - - - - Consolidation des résultats par le moteur de recherche - - - - - Si un document est à la fois sur intranet, Internet et extranet, et si le moteur de recherche indexe ces trois espaces, est-ce qu’alors le document apparaît trois fois dans la liste des résultats lors d’une recherche sur ces trois espaces ? - - - Le comportement est-il identique si le document est publié dans plusieurs rubriques d -' - un même espace? - - - - Il est important que le document apparaisse (même si c’est le même) à chaque endroit où il est publié. Qu’il s’agisse d’un document publié sur différents portails, ou d’un document publié plusieurs fois sur le même portail. - - - Lorsqu’un utilisateur réalise une recherche sur un site, il s’attend à retrouver les documents publiés sur le site. - - - - Du point de vue ergonomique, si le portail propose une recherche multi-site, il est essentiel que l’utilisateur maîtrise la recherche qui est effectuée tout en gardant la simplicité d’utilisation. - - - Pour réaliser ce double objectif, la méthode la plus couramment utilisée est d’accompagner le champ de saisie d’un boite déroulante permettant à l’internaute de spécifier en plus des critères de recherche la cible : - - - - - - - - - Le traitement de la recherche peut également être réalisée au travers une fonction de recherche avancée. La fonctionnalité de recherche avancée présente les différents critères de recherches et cibles de recherches. - - - - - Dans le cas de cette recherche avancée, il est possible de solliciter les différents moteurs de recherche et de re-trier les informations rendues pour fusionner les différents résultats. Cette mise en œuvre nécessite : - - - - - - un coût d’intégration plus élevé, - - - - - une plus grande expertise des utilisateurs. - - - - - - A la demande de la DPMA, Capgemini peut intégrer ce module de recherche avancée. - - - - - - - Poids des pages html gérées par les outils - - - - - Les outils de portail introduisent-ils des éléments qui pourraient augmenter le poids des pages html ? - - - - - La notion de portlet commune à tous les portails induit implicitement des poids de page légèrement augmenté : il n’est plus possible d’optimiser la syntaxe HTML dans l’ensemble de la page. - - - - Il n’est pas possible réellement d’estimer ce surcoût. - - - - - - - Reprise des pages d -' - accueil Alizé et Minefi.gouv.fr. - - - - - Le candidat est-il en mesure de s -' - engager à ce que les pages d -' - accueil Internet et intranet soient reprises dans les portails proposés avec un niveau de complexité (graphique et ergonomique) similaire à celui des pages actuelles. Notamment il est nécessaire que ces pages d -' - accueil puissent adresser une dizaine d -' - espaces et une cinquantaine de liens. A titre d -' - exemple, il faudra valider que le système fourni pourra supporter les chartes graphiques et ergonomiques des pages d -' - accueil des sites Internet (www.minefi.gouv.fr) - - et intranet (alize) actuelles ainsi que la proposition en cours de réalisation pour Alize. - - - A contrario, le candidat peut-il indiquer des limites (ou des contraintes) à prendre en compte lors de l -' - élaboration de la charte graphique et ergonomique. Une liste d -' - exemples de difficultés rencontrées sur d -' - autres projets est satisfaisante. - - - - Capgemini préconise pour des aspects ergonomiques et techniques d’exclure les frames pour les documents gérés avec Jahia. - - - - Pour le reste, la solution de Capgemini supporte les pages d’accueil Alizé et Minefi.gouv.fr. - - - - - - - - - - - Accessibilité - - - - - Il est nécessaire que le système permette à l -' - administration de respecter au plus près les 55 critères du label Bronze accessiweb (voir - - - http://www.accessiweb.org/fr/Label_Accessibilite/criteres_accessiweb/55_accessiweb_bronze/ - - - ). Préciser quels sont les critères qui seront gérés par le système. A minima, il est souhaité que les champs d -' - information complémentaires (alt, title des liens…) soient saisis dans la gestion de contenu et correctement gérés par le système. Indiquer les solutions éventuelles pour gérer les attributs nécessaires à l -' - accessibilité des tableaux, des cadres, des formulaires. - - - - Capgemini s’engage à respecter les 55 critères du label Bronze Accessiweb. - - - - Le tableau ci-dessous regroupe les éléments automatisables pour ces critères, regroupés par catégories : - - - - Eléments graphiques : - - - - - - - - - - Critères - - - - - Niveau - - - - - Description - - - - - Solution - - - - - - - 1.1 - - - - - Bronze - - - - - Chaque élément graphique figurant dans une page web doit posséder une alternative textuelle. - - - - - Pour les éléments graphiques devant être ajouter ou modifier sur une page, nous ajouterons un champ obligatoire permettant de saisir l’alternative. - - - Pour les éléments graphiques ajoutés dans le « mini word », nous ajouterons automatiquement des alternatives vides, que l’utilisateur pourra renseigner par la suite en éditant le contenu html - - - - - - - 1.4 - - - - - Bronze - - - - - Pour chacun des éléments graphiques contenus dans la page, le texte de l’alternative doit faire un maximum de 60 caractères. - - - - - Avec le mécanisme décrit pour le critère 1.1, nous ajouterons une vérification de la longueur du texte. Si le texte est trop long, le contributeur en sera averti. - - - - - - - Cadres : - - - - - - - - - - Critères - - - - - Niveau - - - - - Description - - - - - Solution - - - - - - - 2.1 - - - - - Bronze - - - - - Chaque cadre composant une page web doit être nommé. - - - - - Les cadres sont créés au sein des fichiers jsp. La page contenant les cadres possédera un ensemble de champ (nom, alternative, titre) correspondant à chacun des cadres, ainsi qu’un champ de description définissant l’interaction entre les cadres, permettant ainsi la modification par un contributeur. - - - - - - - 2.3 - - - - - Bronze - - - - - Une alternative doit être proposée lorsqu’une page web est construite avec des cadres. - - - - - Même réponse que le point 2.1 pour l’alternative. - - - - - - - 2.5 - - - - - Bronze - - - - - En plus de posséder un nom explicite, chaque cadre doit être décrite de manière plus précise. - - - - - Même réponse que le point 2.1 pour le titre. - - - - - - - 2.9 - - - - - Bronze - - - - - Il doit y avoir un maximum de trois cadres dans la page. - - - - - La construction des cadres est défini au sein des templates jsp, et dépend de la charte graphique du site. - - - - - - - 2.10 - - - - - Bronze - - - - - Lorsqu’il y a des cadres, le défilement (« scrolling » en - - anglais) doit être automatique. - - - - - Cette propriété sera mise sur chaque cadre au sein des templates jsp. - - - - - - - Liens : - - - - - - - - - - Critères - - - - - Niveau - - - - - Description - - - - - Solution - - - - - - - 6.1 - - - - - Bronze - - - - - L’intitulé des liens doit faire un maximum de 80 caractères. - - - - - Nous ajouterons une vérification de la longueur des intitulés des liens dans les écrans de saisies de nouveaux liens, ainsi que dans le « mini Word ». - - - - - - - 6.3 - - - - - Bronze - - - - - Lorsque c’est nécessaire, un lien doit être commenté et ce commentaire doit comporter un maximum de 80 caractères. - - - - - Ce point concerne les liens de téléchargement de fichiers. Par conséquent, nous ajouterons un champ de saisie obligatoire permettant de décrire le fichier. - - - - - - - 6.5 - - - - - Bronze - - - - - Chaque intitulé de lien identique doit amener vers une seule et même destination. - - - - - ?????????? - - - - - - - Eléments obligatoires : - - - - - - - - - - Critères - - - - - Niveau - - - - - Description - - - - - Solution - - - - - - - 8.1 - - - - - Bronze - - - - - Le type du document électronique consulté dans un navigateur doit être spécifié. - - - - - Le type du document html étant défini par la balise DOCTYPE, nous ajouterons dans les templates cette balise afin de respecter ce critère. - - - - - - - 8.2 - - - - - Bronze - - - - - La langue utilisée dans un document électronique doit être clairement identifiée. - - - - - La langue utilisée dans les pages doit être mise, au sein du template, sur l’attribut « lang » de la balise -< - html -> - . Nous ajouterons cet attribut au sein des templates et en correspondance avec la langue en cours de visualisation. - - - - - - - 8.4 - - - - - Bronze - - - - - Le titre d’une page web doit être clairement identifié. - - - - - Le titre de la page sera présent dans l’ensemble des templates grâce à la balise -< - TITLE -> - . Le titre de la page peut être renseigné par le contributeur à partir des attributs de la page. - - - - - - - 8.6 - - - - - Bronze - - - - - Le titre d’une page web doit être unique - - - - - ???????????? - - - - - - - Présentation de l’information : - - - - - - - - - - Critères - - - - - Niveau - - - - - Description - - - - - Solution - - - - - - - 10.1 - - - - - Bronze - - - - - Le contenu d’une page web doit être séparé de sa présentation spécifique. - - - - - La mise en forme des pages (polices, couleurs, alignement,…) est externalisé dans des feuilles de style CSS. - - - - - - - Contenus accessibles : - - - - - - - - - - Critères - - - - - Niveau - - - - - Description - - - - - Solution - - - - - - - 13.2 - - - - - Bronze - - - - - Si une redirection automatique est présente, elle ne doit pas s’effectuer par l’intermédiaire d’un script. - - - - - L’ensemble des redirections automatiques sera réalisé sur le serveur. - - - - - - - 13.4 - - - - - Bronze - - - - - Une alternative équivalente au script qui déclenche l’ouverture de nouvelles fenêtres doit être prévue. - - - - - Au sein des templates, nous ajouterons des liens permettant d’accéder à la nouvelle fenêtre. - - - - - - - - - - - Cohérence des référentiels - - - - - Est-il prévu des traitements permettant de garantir la cohérence des référentiels de données entre le portail et la gestion de contenu ? - - - - - Les référentiels Portail et gestion de contenu sont communs dans Jahia. Il n’est pas nécessaire de prévoir des mécanismes d’intégrations particuliers. - - - - - - - - - Republication partielle ou totale de contenu par rapport à un ou deux sites : - - - - - L’administrateur des sites (webmestres) disposera-t-il de commandes lui permettant de lancer des synchros partielles ou totales de sites ? Ces commandes seront-elles déclenchées à partir d’un navigateur ? - - - Existera-il des commandes du type : - - - - - - relancer la synchro totale de tous les documents publiés sur l’Internet ? - - - - - - lancer une synchro partielle limitée aux seuls documents modifiés depuis 24 heures ? - - - - - synchroniser de manière immédiate les rubriques de la page d’accueil en priorité par rapport aux autres traitements de synchronisation actuellement en cours ou programmés ? - - - - - - - - Capgemini et Codeva proposent : - - - - - Un développement collaboratif permettant la réplication des éléments saisis depuis l’environnement Intranet (Back-Office) vers le site Internet. - - - - La réplication porte sur : - - - - - - Un ‘site virtuel’ (sous-site) : contenu, conteneurs d’un site, groupes jahia. - - - - - - L’arbre des catégories. - - - - - - Pour que la réplication fonctionne, tous les sites comprennent l’ensemble des templates Jahia. - - - - Cette fonctionnalité de réplication offre des fonctionnalités de : - - - - - Réplication totale – initialisation ou ré-initialisation totale, - - - - - - Réplication incrémentale – mise à jour par delta. A chaque export, les données validées depuis le dernier export sont mises en paquet. Le paquet est numéroté (numéro de version v) et envoyé vers le serveur cible. Le serveur cible reçoit ce paquet et vérifie qu’il a bien déjà importé le paquet v-1. Ce mécanisme permet de garantir les mises à jours incrémentales ont bien été réalisées dans le bon ordre. - - - - - - - Cette fonction de réplication peut être déclenchée, par site virtuel : - - - - - de façon automatique, - - - - - - manuellement via une interface Web pour l’utilisateur ‘root’, - - - - - - - - - Si certaines données (actualités de la page d’accueil) par exemple, doivent être mises à jour de façon régulières, il est possible  de déclencher manuellement la réplication du site virtuel ‘Internet’, ce qui permet de mettre en ligne tous les documents qui font parti du lot validé et notamment ceux qui doivent être publié en urgence. - - - - - - - - - - - Aspect architecture : en cours d’élaboration - - (nouvelle rubrique) - - - - - - - 1°) Intranet AdER - - - Au vu des éléments techniques montrés techniques dans les prototypes, le MINEFI propose l -' - architecture suivante : - - - - - - - la gestion de contenu de l -' - intranet AdER pourra être un site cloisonné logiquement sur la gestion de contenu Intranet. - - - - - le portail de publication de l -' - intranet AdER pourra être un site cloisonné logiquement sur la gestion de contenu Intranet. - - - - - - - Il n -' - est donc plus nécessaire d -' - isoler des éléments matériels ou logiciels sur le réseau AdER - - - - Pris en compte. - - - - 2°) optimisation du nombre de serveurs - - - a) Le candidat proposera une optimisation de son architecture actuelle en fonction du nouvel élément lié - - à AdER. - - - - - (nouveau) - - - Le Minefi prendra à sa charge l -' - acquisition des boîtiers alteon ou autres systèmes de répartition de charge préconisés par le candidat. Le candidat mettra en oeuvre ces outils. - - - - - - Pris en compte. - - - - Plusieurs optimisations ont été réalisées : - - - - - - - La fusion ADER/Intranet. - - - - - - Une étude des coûts détaillés nous a permis d’optimiser le coût de la plate-forme Internet. Les 2 quadri-processeurs sont remplacés de avantageusement par 5 bi-processeurs. - - - - - - - - - Au démarrage du projet, la DPMA indiquera à Capgemini si la solution retenue est un altéon ou un serveur sous LVS. - - - - b) De plus, le candidat proposera - - une architecture alternative basée sur des clusters et des SAN. - - - Le stockage intranet et le stockage Internet sera mutualisé sur des SAN (les SAN en ISCSI sont conseillés). - - - - Pris en compte. - - - - - Il devra s -' - engager à implémenter ses solutions sur les serveurs en cluster et l -' - archivage en SAN. - - - - Pris en compte. - - - - - Si une des deux solutions induit un surcoût celui-ci devra être proposé en variante. - - Le Minefi choisira au moment du lancement du projet l -' - architecture définitive parmi ces deux solutions. - - - - - Il n’y a pas de différence de coût, pour la mise en œuvre des sites, entre les 2 solutions. - - - - - - - Outils de partage de charge : Fournir et mettre en œuvre les outils de partage de charge (en cours d’élaboration) - - - - - - - - - Sécurité des accès et des données et respect des flux déterminés par le Minefi - - - - - Les sociétés doivent s’engager de manière explicite dans le CCTP à ce que: - - - - - - - aucune donnée de type intranet (contenu, index, login, …) ne soit disponible sur l’internet (sauf si publication volontaire) - - - - - les solutions techniques fonctionnent sans nécessiter de flux d’internet vers intranet. Dans le sens internet vers intranet les réseaux intranet et internet doivent être considérés comme physiquement séparés. - - - - - - - Les sociétés doivent s’engager à ce que leur solution respecte les flux admis par le Minefi entre l’Intranet et l’Internet (cf PFD paragraphe 10). - - - Si une de ces conditions n -' - était pas respectée, les candidats s’engagent à trouver une solution de contournement. - - - - - - Oui : aucune donnée de type intranet (contenu, index, login, …) ne soit disponible sur l’Internet (sauf si publication volontaire) - - - - - - Les solutions techniques fonctionnent sans nécessiter de flux d’Internet vers intranet. Dans le sens Internet vers intranet les réseaux intranet et Internet doivent être considérés comme physiquement séparés. - - - - - - - - - - Contrôle d’intégrité des données : - - - - - Le Minefi rappelle qu’il souhaite un contrôle de l’intégrité des données. La solution mise en œuvre doit au moins être du - - niveau de TripWire. Il convient d’intégrer cette demande dans la réponse. - - - - Le logiciel TripWire permet le contrôle - - d’intégrité des fichiers stockés sur les disques d’un serveur. - - - - Capgemini propose l’intégration de la version Open source de Tripwire ( - - - - http://www.tripwire.org/ - - - - ). L’intégration de TripWire permet de vérifier que le système de fichier n’est pas modifié. - - - - - - Les contenus publiés par Jahia ne sont pas stockés dans un système de fichier mais dans la base de données PostGres. Tripwire ne permet pas de vérifier l’intégrité de la base de données. - - - - - - De plus, la solution proposée par Capgemini positionne le serveur Apache derrière le pare-feu. Il s’agirait donc pour un ‘hacker’ de pénétrer le pare-feu puis d’accéder au serveur Apache, puis d’accéder au serveur d’application, puis d’accéder au serveur PostGres. Chaque accès (Serveur linux supportant Apache, Serveur linux supportant PostGres, Serveur Postgres) possède ses propres mécanismes d’authentification et de sécurité : - - - - - - - - - L’accès au serveur Linux supportant Apache est fortement restreint : durcissement du système d’exploitation. - - - - - - - L’accès au serveur Linux supportant le serveur d’application est restreint : durcissement du système d’exploitation , TcpWrapper (préconisé) n’autorisant les communications qu’avec le serveur Apache et le serveur de base de données. - - - - - - L’accès au serveur Linux supportant le serveur Postgres est restreint : durcissement du système d’exploitation, TcpWrapper (préconisé) n’autorisant les communications qu’avec le serveur d’application. - - - - - - L’accès au serveur Postgres est réalisé au travers d’un login/mot de passe crypté lors de l’échange. - - - - - - - - - - Enfin, si ces niveaux de sécurité ne sont pas suffisants, il est également possible d’ajouter un pare-feu supplémentaire entre le serveur Apache et le serveur d’application. Des solutions basées sur Linux permettent la mise en œuvre peu coûteuse de pare-feux. - - - - - Capgemini propose de réaliser un dossier sécurité durant la phase de conception. - - - - - - - - - Instance Jahia sur Internet : - - - - - Préciser si les outils d’administration des portails Jahia seront désactivés sur l’instance sur internet. Dans le cas contraire préciser les mécanismes qui permettent d’éviter les accès non désirés aux outils d’administration. - - - - - L’instance Jahia sur Internet ne possède pas les différentes engines - - - - - 1 - - - - - Fonction de type assistant pour Jahia. - - - - - - d’administration et de contribution. Si un utilisateur malveillant réussit à faire appel à ces fonctions, il obtiendra une erreur Java. - - - - - - De plus, Capgemini propose d’intégrer une étude sécurité permettant de lister tous les points mis en place (serveurs, réseaux, application) permettant de relever les enjeux de sécurité de la DPMA. - - - - - - - Utilisation du protocole WebDAV - - - - - Entre le client et Jahia : l’utilisation de WebDav est-elle obligatoire pour publier les documents. Si deux publieurs sont responsables d’une rubrique : l’un des deux peut-il utiliser webDav et l’autre pas ? - - - - L’utilisation de WebDav n’est pas obligatoire. - - - L’utilisation combinée pour une même rubrique est tout à fait possible. - - - - Entre les instances java intranet et internet : le protocole WebDav passe-t-il les firewall de la DPMA (pare-feux filtre de paquets et pare-feux applicatif) situés entre la zone intranet et la zone internet. Il est rappelé que pendant la réalisation des prototypes ce protocole n’était pas ouvert. Décrire les conditions qui permettront d’ouvrir ce protocole au travers des firewall (ouverture d’un port particulier, reconnaissance d’ordre http, …) ? - - - Si cela nécessite l -' - ajout d -' - un composant sur les pare-feux, la fourniture est à la charge du prestataire. - - - - - A priori, le protocole WebDav est construit pour passer les pare-feux. - - - - - Les équipes de DPMA/2C estime que webdav pourrait passer en https pour qu -' - il n -' - y ait pas analyse de protocole. Le HTTPS chiffre les données, avantage donc en termes de sécurité (les données confidentielles à publier ne passent pas en clair). L’inconvénient majeur pourrait alors porter sur le niveau des performances. - - - - - Le flux WebDav peut être encrypté. Il n’y a pas impact sur les performances au sens fonctionnel du terme, la phase de synchronisation entre le back office intranet et le front office Internet se découpe en 4 phases : - - - - 1/ Export des documents du back-office - - - 2/ Transport des informations vers le front office - - - 3/ Import des documents dans le front office. - - - - - Le front office est uniquement perturbé à la phase 3. Cette phase doit être limitée dans le temps : - - - - - - - - mise en place de la réplication incrémentale, - - - - - garantie de performance : 20000 pages par heure. - - - - - - - - - Le surcoût du transport n’a donc pas d’incidence sur l’objectif de performance qui porte sur les phases 3. - - - - - - - - Gestion de contenu multisite (spécifications) - - - - - Préciser le degré d‘engagement pour transformer Jahia en outil de gestion de contenu multi-sites. - - - Préciser les spécifications générales et l -' - architecture fonctionnelle du module de gestion de contenu dans sa version multisites. - - - - Introduction - - - Le module de gestion de contenu de Jahia permet, dès aujourd’hui, la mise en œuvre de 2 types de contribution : - - - - - « Edit/Publish » : dans se mode, le contributeur produit un document Web et le publie selon plusieurs axes (catégories). Le portail transforme ces catégories sous formes de rubriques. - - - - - - - « Edit in place » : dans ce mode le contributeur se positionne au sein du portail là où le document doit être affiché pour le saisir (ou le récupérer dans le cas ou le document est déjà publié). - - - - - - - Le mode ‘Edit/Publish’ - - - La contribution est réalisée dans des templates Jahia dédiés. Chaque document est associé à : - - - - - 1 fonction ‘back office’ réalisée par : - - - - - - - - - 1 page permettant de lister ou de rechercher 1 document de ce type depuis le site de contribution. - - - - - 1 page de saisie permettant la consultation, la saisie et la modification. - - - - - 1 à n catégories : l’arbre des catégories de Jahia. - - - - - - - - - 1 fonction ‘front office’ réalisée par : - - - - - - - - - La page de consultation précédente. - - - - - 1 page dédiée à la navigation au sein du portail (liste ou recherche). - - - - - - - - - Le site contient des rubriques construites sur l’extraction de documents rattachés à des catégories. - - - - - - - Pour un document, la validation est globale à toutes les catégories auxquelles il est rattaché. - - - - - - Le prototype ‘Concours’ présente ce modèle de contribution : - - - - - Une page permettant la recherche multi critères des concours en liste. - - - - - Une page permettant la saisie du document concours. - - - - - - - Publication multi-site : - - - - Dans ce mode, l’arbre des catégories est découpé en branches portant les catégories de chaque site (Alizé, Internet). Chaque portail présente les documents validés et attribués aux documents catégorie de son site. - - - - Le mode ‘Edit in place’ - - - La contribution est réalisée directement à l’endroit où le document est publié. - - - - - - Les fonctions ‘back office’ et ‘front office’ sont indifférenciées : - - - - - - - - - 1 page permet la consultation, la modification et la création d’un nouveau document. - - - - - au sein du portail. - - - - - - - - - - - - - - - - NB : Cela ne signifie pas que les modifications sont réalisées sur le site en ligne (Internet par exemple) mais bien sur une instance en ‘back office’. - - - - Publication multi-site : - - - - Dans le cadre du mode de contribution - - ‘edit in place’. La contribution multi-site est une simple extension du fonctionnement actuel qui permet à un même document d’être publié dans plusieurs rubriques. - - - - - - - Dans le mode edit in place – l’activité est centrée sur le portail : - - - - - - - - - Un document a été créé dans une page du portail Intranet (par exemple). Un workflow propre à la page a alors été déclenché. - - - - - Un contributeur ‘Internet’ décide de publier ce document. - - - - - - - - Il se positionne sur le portail Internet, choisit l’option d’inclure un contenu externe. - - - - - - - - - - - - - - - - - - Un assistant permet de recherche le contenu à inclure. Cet assistant permet plusieurs modes de recherche : - - - - Par catégorie, Auteur, par date - - En suivant le plan des sites - - - - - - - - - - - - - - - - - - - - - - - - - - Par recherche, résultat - - - - - - - - - - - - - - - - - - - - - - - - - - Par ‘recopie’ d’une URL du site. - - - - - - - - - - Synthèse - - - - La solution de Capgemini intègre ces 2 modes : - - - - - - - Le mode ‘edit/publish’ doit être utilisé pour les documents publiés sous forme de catalogue ou de listes qui sont généralement saisis par des contributeurs dédiés à ce type de document de type documentaliste. - - - - - - - C’est ainsi le cas pour les documents agenda, actualités, ressources documentaires, courrier du ministère, la liste des services en ligne, concours, les catalogues de l’action sociale, fiches de poste. - - - - - - - Le mode ‘edit in place’ doit être utilisé pour les documents de type communication et pour des contributeurs qui gèrent une information riche, avec une vision transverse du portail de type webmestre et responsable éditorial. - - - - - - - - Au sein du ‘back office’ on retrouve donc : - - - - - - - Le portail tel qu’il sera publié pour permettre la mise à jour du site ‘edit in place’. - - - - - Un menu particulier donnant l’accès à la production des documents ‘edit/publish’ - - - - - Le tout intégré du point de vue de la gestion des droits. - - - - - - - - - Préciser notamment la granularité des objets gérés par le système : une page peut-elle afficher plusieurs documents publiés séparément dans différentes rubriques (indépendance des documents par rapport à leurs instances de publication au sein des pages) - - - - - Oui : - - - - - - - Dans le mode ‘Edit/Publish’, - - c’est le portail qui décide (de façon programmée) quelles catégories de documents positionner dans ses pages. - - - - - - - Dans le mode ‘Edit in place’, l’utilisateur reste totalement libre de positionner tout contenu provenant de tout site sur sa page. - - - - - - - Un document pourra-t-il être rédigé et validé sans être publié ? - - - - Oui : - - - En fonction de ce que représentent du point de vue organisationnel les termes ‘rédigé’ et ‘validé’. - - - - Par défaut, Jahia gère 3 états : - - - - - - - - - En cours. - - - - - Terminé. - - - - - Publié. - - - - - - - - - - - Si l’état ‘terminé’ signifie : rédigé et validé par le contributeur. Il reste effectivement stocké dans Jahia sans qu’il ne soit publié. - - - - - - - Si par contre, la DPMA souhaite pour certains types de documents une validation par un tiers du document avant sa publication, Capgemini propose la mise en place d’un workflow à 4 états : - - - - - En cours - - - - - Rédigé - - - - - Validé rédaction - - - - - Publié. - - - - - - La proposition de Capgemini intègre la mise en œuvre de 3 workflows. - - - - - - Pourra-t-on avoir un document contenu à la fois dans l’outil Jahia et publié sur aucun site ? - - - Oui. - - - En complément de la question précédente. - - - Un document peut être dans le référentiel documentaire et pas encore publié. - - - - - Il peut également rester dans le référentiel et ne plus être publié : cette possibilité est simplement réalisée en utilisant la gestion des droits. Si l’on souhaite qu’un document dé-publié - - reste dans le référentiel documentaire, il faut simplement changer les droits d’accès au document. Un document publié sur un site est ouvert en lecture au profil ‘guest’. Dé-publié ce document revient à attribuer un profil de type contributeur pour qu’il ne soit plus accessible sur le site. - - - - - Préciser si tous les aspects d’une gestion de contenu multi-sites seront pris en compte dans les développements à venir : - - - - - contraintes liées aux documents, - - - - - contraintes sur le workflow (ex : un document est publié selon une règle de validation sur l’intranet et doit l -' - être selon une autre règle de validation sur l’internet) - - - - - contrainte sur le moteur de recherche - - - - - - Hypothèse : un document est créé à partir d’un site Internet Jahia (portail Internet) puis il est publié sur un site intranet. Que ce passe-t-il si le document est dé-publié du portail Internet. - - - - - Ce que l’on veut pour de mode « edit in place » : - - - - - - Scénario 1 : on souhaite qu’un document partagé ait le même cycle de vie entre l’Internet et l’intranet. - - - - - Il s’agit de rubriques partagées entre l’intranet et l’Internet (même si ces rubriques sont positionnées différemment sur chaque site). - - - - L’équipe éditoriale est la même : les rubriques sont les mêmes. - - - - Le document est géré dans un espace mixte entre l’Internet et l’intranet. - - - - - Il est validé, et dé-publié de façon partagée entre les 2 sites. - - - - - Scénario 2 : on souhaite qu’un document partagé ait des cycles de vies différentes entre L’Internet et l’intranet. - - - Il s’agit de rubriques différentes. - - - - L’équipe éditoriale de la rubrique intranet (resp. Internet) produit le document. Au travers d’un comité éditorial transverse ce document est identifié comme devant également être également publié sur Internet (resp. intranet). - - - - - Chaque rubrique possède son propre cycle de contribution, de validation, de péremption. - - - - - Repréciser clairement l’impact de l’utilisation des dossiers privés à un site et l’impact de l’utilisation des dossiers publics ? N’y a-t-il pas un risque que le choix d’un dossier privé implique des contraintes (voir des impossibilités) de publication vers d’autres portails ? - - - - - Les dossiers privés ne sont utilisés, normalement, que lors de son élaboration collaborative. Lorsqu’un contributeur veut publier un document privé, un message d’avertissement le prévient de cette incohérence potentielle. Si le contributeur le publie néanmoins, il peut : - - - - - - - - soit modifier les droits pour qu’il soit accessible de tous, - - - - - conserver ses droits. Il est alors visible de tous mais ne peut être récupéré que par les utilisateurs autorisés. - - - - - - - - - - Ergonomie - - - - - L -' - ergonomie actuelle de Jahia -" - edit in place -" - nécessite que le contributeur se positionne  dans un endroit d’un portail puis crée un document. Préciser si l’ergonomie suivante sera réalisée : création d’un document dans la gestion de contenu indépendamment de toute publication, puis publication vers un ou plusieurs portails. - - - - - (nouveau) - - - Préciser si dans le - - mode -" - edit in place -" - , il sera possible de publier à partir du site où l -' - on se trouve dans d’autres sites (intranet ou internet), sans avoir à se positionner sur chacun des sites cibles. Autrement dit le mode -" - edit in place -" - prend-il en compte la gestion en multi-sites attendue. - - - - Multi-site appliqué - - - Le mode « edit in place » permet la publication multi-site telle qu’attendue par la DPMA. - - - - Dans un premier temps il faut distinguer : - - - - - - - - les informations « chaudes » - informations mises à jours régulièrement (jour, semaine) – des articles par exemple. - - - - - - - des informations « froide » - informations mises à jours rarement (mois) – des dossiers par exemple. - - - - - - - - - - Les informations « chaudes » sont en général nombreuses et doivent être renseignées rapidement. Elles sont fortement candidates au mode « edit/publish » : des contributeurs sont dévolus à ces tâches répétitives. L’interface de contribution devient alors un outil de saisie basé sur des formulaires. L’alimentation du portail devient automatique. Les étapes de validation et de publication sont raccourcies au maximum. - - - - - - Les informations «  froides » sont en général - - plus complexes (plus riches), leur disposition dans le portail est moins prédictible, les étapes de construction, validation plus complexes. Ces informations doivent utiliser le mode « edit in place ». - - - - - - - - - - Il s’agit pour l’organisation éditoriale des informations froides d’accompagner l’organisation du site. De façon traditionnelle on retrouve 2 types de partages d’information entre l’intranet et l’Internet : - - - - - - - Partage de rubriques – un ensemble d’informations proposé sur un site se retrouve à l’identique sur un autre site. Il convient à chaque site de positionner cette rubrique au bon niveau dans sa propre arborescence. - - - - - - - Partages de contenus (dossiers) – une information particulière est partagée. - - - - - - - Partage de rubriques - - - Dans cette configuration, une équipe éditoriale anime la rubrique, qu’elle soit publiée sur l’intranet ou l’Internet. - - - - Cette rubrique est alors simplement réalisée en mode « edit in place » dans un espace mixte (site virtuel mixte) qui est publié à la fois sur l’intranet et l’Internet. - - - - - La validation et la publication sont alors communes à chaque site, les contributeurs/valideurs travaillent de façon unique au sein du site mixte. - - - - Chaque portail (intranet, Internet) : - - - - - - - intègre les rubriques mixtes au sein de son arborescence. - - - - - intègre ses gabarits (templates Jahia) permettant d’appliquer sa charte graphique au contenu des rubriques partagées. - - - - - - - Partage de contenus - - - - Dans cette configuration, les contenus sont publiés dans leurs espaces propres. L’équipe éditoriale d’une rubrique Internet (resp. intranet) identifie qu’un contenu publié sur l’intranet (resp. Internet) doit être repris. - - - - - Le mode « edit in place » permet à l’équipe Internet (resp. intranet) de récupérer ce contenu pour le publier. - - - - - - La validation et publication peuvent suivre des cycles totalement différents. Les contributeurs/valideurs travaillent de façons dissociées sur chaque site. - - - - - Cas d’utilisation - - - Contenu chaud ou de type catalogue - - - Problématique : - - - Un type de contenu est saisi en masse : soit souvent, soit en grande quantité - - - Souvent – contenus chauds : - - - Le cycle de vie du document est très raccourcis : contribution = -> - publication rapide. - - - Grande quantité – données structurée de type catalogues nécessitant des outils de recherche, de filtre développés sur le portail. - - - Des contributeurs spécialisés, dédiés à la production de ces contenus. - - - Solution : Edit/Publish - - - Impacts : - - - - - l’arbre des catégories est segmenté en zone Internet, intranet. - - - - - le cycle de vie du contenu est porté par le contenu indépendamment du site sur lequel il est présenté. - - - - - Contenu froid propre à un site - - - Problématique : - - - Il s’agit de faire simplement la mise à jour du site : contenu, rubriques, … - - - - Solution : Edit in place - - - Impacts : - - - - - aucun - - - - - - Contenu froid partagés par 2 sites réalisés dans des rubriques séparées - - - Problématique : - - - Des équipes éditoriales différentes travaillent sur des rubriques différentes de sites différents. - - - Un document peut néanmoins être partagé entre les 2 sites. - - - - Solution: Edit in place. - - Jahia proposera un assistant (« content sourcing ») permettant de récupérer un contenu existant pour le publier. - - - Impacts : - - - - - Chaque contribution correspond à un cycle de vie différent qui est fonction de la page dans laquelle le contenu est produit ou récupéré. - - - - - Tous les gabarits doivent intégrer les 2 chartes graphiques intranet et Internet. Pour pouvoir être intégré de façon harmonieuse tout contenu doit pouvoir être proposé sur l’intranet et l’Internet. - - - - - - Contenu froid partagés par 2 sites réalisés dans des rubriques mixtes - - - Problématique : - - - L’organisation éditoriale sur site Internet et du site intranet possèdent des similitudes : des rubriques sont partagées entre les 2 sites (même si ces rubriques ne sont pas disposées au même endroit. - - - Solution : Edit in place et site virtuel mixte. Un site mixte est mis en place (techniquement) permettant de gérer ces rubriques partagées entre le site Internet et le site intranet. - - - Impacts : - - - - - Chaque portail (Internet, intranet) référence ces rubriques. - - - - - Le cycle de vie du contenu est unique : contribution, notification, validation. - - - - - - - - - - - - - - - Une rubrique mixte devient propre à l’intranet (resp. l’Internet). - - - - - Problématique : - - - Une rubrique partagée se restreint à un site. - - - Solution immédiate (cas d’une restriction temporaire) : - - - Il suffit de retirer le lien sur le site Internet (resp. intranet) vers cette rubrique. - - - - Impact de la solution rapide : - - - - - Les contenus continuent à être répliqués vers le site Internet (resp. intranet) même s’ils ne sont plus accessibles. - - - - - De point de vue de la sécurité, ces documents étaient publiés juste avant la modification et gardent intrinsèquement leurs niveaux d’accessibilité ‘public’. - - - - - - Solution court terme : - - - Les contenus sont recopiés (manuellement) depuis le site mixte vers le site intranet (resp. Internet). Il s’agit bien ici des back-offices de chaque site. - - - - Impact de la solution court terme : - - - - - La recopie des contenus utilise la fonction de ‘content sourcing’ entre le site mixte et le site intranet (resp. Internet). - - - - - - Un contenu d’une rubrique mixte doit subir un embargo pour ne plus être accessible sur Internet. - - - Problématique : - - - Un contenu publié dans une rubrique mixte peut subir un embargo (temporaire ou définitif) - - - - - - - - - - - - - - - - - - - - Définir les outils (et les fonctions associées) permettant aux différents contributeurs d -' - administrer et de gérer la (les) base(s) de contenu indépendamment des accès via la navigation sur les sites de publication. (point 84 du cadre de réponse à détailler) - - - - - Jahia propose en standard un outil permettant d’explorer les contenus indépendamment des accès via la navigation sur les sites de publication. - - - - - Concernant les contenus réalisés en mode « edit/publish » une interface dédiée à la contribution et à la gestion est développée. - - - - - Concernant les contenus réalisés en mode « edit in place », Capgemini propose d’intégrer un agent de la DPMA (transfert de compétence) qui durant le projet aura la charge de spécifier et de développer ces outils. - - - - - - Concernant les contenus non publiés ou en cours d -' - édition, décrire les outils, pour permettre aux utilisateurs du système (contributeur, éditeur, superviseurs) de modifier, publier ou de piloter le travail d -' - édition. (La notion de corbeille sera-t-elle étendue aux documents en cours de préparation, si oui, comment ?) - - - - Jahia propose un outil permettant aux différents utilisateurs (contributeurs, valideurs, administrateurs) de suivre les contenus. - - - - - - - - Zone de commentaires. - - - - - - - - - - - - - - - - - - - - Contenu publié. - - - - - - - - - - - - - - - - - - - - Contenu terminé en attente de validation. - - - - - - - - - - - - - - - - - - - - Contenu en cours de rédaction. - - - - - - - - - - - - - - - - - - - Lors de la révision d -' - un document, est-ce que toutes ses instances de publication (internet et intranet) seront modifiées ? - - - - Les 2 cas sont possibles : - - - - - - - - Les contenus créés en mode «  edit /publish » et les contenus mixtes sont modifiés au même moment sur tous les sites. - - - - - - Les autres contenus ont des cycles de vies différents – chaque contenu doit être validé dans chaque rubrique de chaque site. - - - - - - - - - - Norme JSR 170 - - - - - Le développement proposé s -' - appuie en grande partie sur la norme JSR 170 en cours de validation. Préciser l -' - état d -' - avancement de la norme à ce jour, les acteurs industriels associés, les points restant à définir, les risques et les enjeux de cette norme. Donner des exemples de travaux ou de projets réalisés s -' - appuyant ou exploitant les prémices ou quelques caractéristiques de cette norme. Indiquer quelle est la responsabilité effective de Jahia dans l -' - élaboration de cette norme - - - Indiquer les éléments de la norme qui seront utilisés dans le projet et le rôle fonctionnel porté par chacun de ces éléments. - - - Dans le cas où cette norme ne serait pas finalisée avant le démarrage du projet, préciser les solutions alternatives qui seraient retenus pour y palier - - - - - Le draft publique de la JSR est publié. - - - - - Jahia attend que la spécification soit adoptée. Une étude a été réalisée par Codeva permettant de vérifier que tous les concepts de Jahia entrent dans les concepts de la JSR. - - - - - Le standard permet à Jahia à minima de standardiser l’export XML réalisé pour la synchro pour répondre à la structure CMS demandées par la JSR. - - - L’implémentation de la JSR 170 n’est pas indispensable pour le projet de la DPMA. - - - - - - - Poste de travail  - - - - - Indiquer si webdav n’est pas obligatoire pour ajouter des documents dans la gestion de contenu. Dans le cas où on publie sans Webdav, les postes de travail peuvent-ils être sous windows 95, ou windows 2000 ? - - - Un poste équipé d’Open Office 1.2 - - peut-il publier sans webdav ? - - - Un poste équipé d’Open Office 1.2 peut-il publier avec webdav ? - - - - L’utilisation de webdav n’est pas une obligation pour mettre à disposition des fichiers sur le site. Webdav permet de simplifier la publication de fichiers car il fonctionne, sur le poste du contributeur, de la même manière qu’un répertoire partagé. Ainsi, le contributeur peut ajouter très rapidement sur le site un grand nombre de fichiers, ou restructurer l’arborescence des répertoires. - - - Concernant les contributeurs ne disposant pas d’accès webdav, ils peuvent également « uploader » de fichiers sur le site en utilisant les fonctions disponibles sur le site. Dans ce cas, ils ne peuvent mettre à disposition qu’un seul fichier à la fois. - - - Les postes équipés d’Open Office, pourront publier sans webdav à partir du site web, comme décrit ci-dessus. Pour une publication avec webdav, Open Office ne supporte pas l’implémentation fournit par Microsoft, par conséquent, il est nécessaire d’utiliser un outil externe tel que WebDrive permettant de simuler un lecteur réseau. La fourniture d’un tel outil n’est pas incluse dans l’offre de Capgemini. - - - - - - - - - - Gestion des utilisateurs déclarés - - - - - Décrire les outils fournis pour administrer les utilisateurs qui sont déclarés pour bénéficier de la personnalisation (mesurer l -' - activité, identifier les centres d -' - intérêt déclarés, identifier les erreurs d -' - adresses courriel) - - - - Mesurer l’activité. - - - - Le suivi de l’activité peut être réalisé en utilisant Xiti et les logs produits par Apache et travaillés avec Awstat. - - - - Pour un suivi plus fin, Capgemini propose d’utiliser Log4J. Ce composant permet de lever des traces dans les templates Jahia, permettant de suivre : l’utilisateur/groupe, la page demandée. Ce suivi est réalisé dans un fichier (journal). - - - - La DPMA récupère les différents journaux sur les différents serveurs pour les analyser avec Excel par exemple. - - - - - Identifier les centres d’intérêt déclarés. - - - A la demande de la DPMA, Capgemini peut intégrer un développement spécifique permettant d’interroger les données de personnalisation et produire un fichier Excel. - - - - - Identifier les erreurs d’adresses courriel. - - - - - Le logiciel Sympa reçoit les mails en erreur lorsque l’adresse courriel est erronée. Le portail n’a pas d’action sur ces adresses. - - - - - - Par expérience, il est difficile de prévoir à priori les statistiques utiles au site. Il devient donc difficile de prévoir les bons points de mesures qui permettront de produire les statistiques. - - - - - La solution de Capgemini intègre une technologie permettant de réaliser ces points de mesures. Une première mise en œuvre est prévue qui permet de valider les principes et les fonctionnements. - - - - - Lors du transfert de compétence, ce point est notamment traité et permettra à la DPMA de positionner de nouveaux points de mesures et d’affiner les statistiques utiles. - - - - - - - - Moteur de Workflow Sensei - - - - - Sensei n -' - ayant pas été intégré dans le prototype, fournir une description détaillé de cet outil (règles de fonctionnement et d -' - administration, ergonomie fonctionnelle). - - - Sensei est-il inclus dans la proposition ?. - - - Dans le paragraphe 3.3.1, il est précisé que la livraison comprend trois workflow type. Le schéma qui suit ne prend pas en compte la gestion de la multi-publication. Il convient de préciser que les 3 workflow proposés prendront en compte la publication-multi-sites. - - - - - - - - Sensei est un - - moteur de gestion de process (BPMS - Business Process Management System) et d’orchestration de Webservices. - - - - - - Sensei va donc permettre de mettre en relation des systèmes hétérogènes par l’échange de messages standardisés (xml / soap) organisés selon un schéma (process) directeur défini lui aussi de façon standard (bpml). - - - - - - Sensei est un logiciel fondé sur la philosophie du développement collaboratif : - - - - - - Licence collaborative (vous ne payez pas les logiciels que vous contribuez à faire évoluer) - - - - - - - - Sources ouvertes - - - - - - - - Faible coût d’acquisition vs fonctionnalités - - - - - - - - Fédération des développements au sein de la communauté des utilisateurs - - - - - - - - Basé uniquement sur des standards : J2EE, JDO, Webservices, BPML - - - - - - - - - - Sensei est basé sur des standards ouverts : - - - - - - WSDL pour la définition des interfaces Soap / Webservices (Universalité applicative : J2EE / .NET / PHP) - - - - - BPML pour la définition des process - - - - - Exécution des process réalisés entièrement par Webservices (Soap et XML) mais pouvant être étendus par l’utilisation d’autres connecteurs (JMS,…) - - - - - Architecturé sur les technologies J2EE (Sun ©) via des composants Servlet. - - - - - - - - - Schéma de l -' - architecture : - - - - - - - - - - - Intégrant Sensei, la version « Enterprise » de Jahia va permettre l’ajout de processus complexes de publication définis par un fichier XML standardisé (BPML) en interaction totale avec Jahia (étapes de publication, notifications, verrouillage des objets de contenu, date de début de publication, etc…). - - - - Par ailleurs, Sensei va permettre, au-delà de la gestion des workflow complexes de publication, de mettre en place des applications métiers au travers des templates de Jahia ou dans le cadre d’une application web déployée dans Jahia. La Webapps métier utilisera alors Sensei pour gérer des process spécifiques, sans aucun lien avec le process de publication. - - - - - La gestion des workflow complexes de publication - - - - Jahia propose en version standard un workflow de publication à 2 étapes : l’auteur notifie une version de contenu sur lequel il travaille à un validateur qui acceptera ou refusera cette nouvelle version. Le contenu sera disponible en ligne, une fois validé. - - - - La version « Entreprise » de Jahia permet, grâce à Sensei, de définir des workflow complexes de publication à plusieurs étapes. - - - - Jahia va par ailleurs gérer les héritages de workflow : on peut à tout moment définir un process de publication différent de la page parente du contenu publié ou, inversement, avoir (option par défaut) le même workflow de publication que la page parente. - - - - Par exemple, l’utilisateur créée une page et sélectionne sur le bouton « modifier les propriétés » : - - - - - - - - - Il va pouvoir sélectionner dans l’onglet « workflow de publication », le type de workflow souhaité (hérité, standard, aucun workflow, externe) : - - - - - - - - En sélectionnant le workflow externe, il va pouvoir choisir parmi les process de publication importés dans Sensei. - - - - Par exemple, si l’utilisateur choisit le workflow à 3 étapes, il devra alors définir les personnes en charges de chacune des étapes : - - - - - - - - - Les actions possibles et leurs conséquences à chaque étapes sont gérées par Sensei selon le schéma directeur défini dans le fichier BPML : notification, choix à la disposition de la personne en charge de l’étape (par exemple définir ou non une date de début de publication), verrouillage des objets de contenus etc… - - - Application métier : Gestion de demandes - - - - - - L’application de gestion des demandes est une véritable application basée sur Jahia, livrée avec la version « Entreprise ». - - - - - Elle a été développé sur base du système de template de Jahia (une webapp / portlet déployée dans Jahia était une alternative) afin de bénéficier des fonctionnalités (indexation, personnalisation, gestion des fichiers) et de la souplesse de modifications des gabarits de Jahia. - - - - Elle a été développée à la demande du Parlement Européen et redistribuée à la communauté. - - - - Cette application permet à une personne de solliciter l’avis d’un groupe d’expert afin de répondre à une requête. Dans le cadre du projet du Parlement Européen, cette requête prend la forme d’une demande de support et de préconisation autour d’un projet informatique piloté par la Direction Informatique. - - - - La personne va donc créer une demande et solliciter un groupe d’expert ou un expert particulier (le « dispatcher ») si elle n’est pas certaine du domaine concerné par sa demande. - - - - Le groupe d’expert sera prévenu de cette demande et un membre du groupe devra prendre en charge cette demande sans quoi, sous 24h, le « dispatcher » sera prévenu par notification mail afin d’éventuellement désigner un responsable. - - - - Le responsable de la demande constituera alors sont groupe de travail. - - - Le groupe travaillera de façon collaborative sur la page de la demande (créée automatiquement par le système) en publiant des informations et documents publics (visibles par le demandeur) ou privés (visibles par le groupe de travail). - - - - La page de la demande présente donc l’historique complet du travail effectué et des évènements survenus lors du déroulement du processus de traitement de la requête. - - - - - Chaque demandeur et participant à une demande voit dans la liste personnalisée que le système présente de façon personnalisé, les demandes le concernant, son rôle et leur état d’avancement. - - - - - La composition du groupe de travail et le responsable de la demande sont modifiables à tous moment, Sensei gérant l’ensemble des conséquences issues de ces évènements. - - - - La publication de la réponse va mettre la demande en état « clôturé », personne ne pouvant plus y participer, à moins que le demandeur ré - ouvre sa requête, non satisfait de la réponse. - - - - Comme le montre le schéma ci-dessous, les actions issues du déroulement de l’application (création de la demande, création de la page présentant son historique, gestion des accès et des droits sur la demande, notifications mails etc…) sont gérés par Sensei selon le schéma défini en BPML en interaction avec deux Webservices : - - - - - - Jahia Management Webservice : en charge du contrôle de Jahia (création des pages, modifications des droits, etc…) - - - - - - - - Notifier Webservice : gestion des notifications, différentes selon les nombreux évènements provenant du déroulement du process vers le serveur SMTP - - - - - - - - - - - - - - Illustrons les fonctionnalités et la valeur ajoutée apportée par l’utilisation de Sensei au travers du scénario suivant : - - - - - - Une personne se connecte, le «  - - - - demandeur - - - - 1 » et - - - - va créer - - - - une - - - - demande - - - - destinée - - - - au - - - - domaine - - - «  - - - A » - - - de compétence. - - - - - - La - - - - demande - - - - apparaît - - - - dans - - - - le - - - - tablea - - - u - - - en - - - - état - - - - « - - - - Initialisation - - - - ». - - - - - - - - - - - En - - - - cliquan - - - t - - - - sur - - - - la - - - - de - - - m - - - ande, - - - - le - - - - demandeur - - - 1 - - - - peut - - - - vérifier - - - - les - - - - données - - - - de - - - - sa - - - - demande mais - - - - peut - - - - effectuer - - - - une - - - - seule - - - - a - - - c - - - tion - - - - : - - - - - - - - « - - - - Poster - - - - un - - - - commentaire - - - - public - - - - » - - - - - - - - - - - Il - - - - peut - - - - ain - - - s - - - i - - - - préciser - - - - s - - - a - - - - demande - - - - de - - - - faço - - - n - - - - plus - - - - complè - - - t - - - e, - - - - par - - - - exemple - - - - en - - - - mettant - - - - à dispo - - - s - - - ition - - - - un - - - - fichier - - - - de - - - - référence. - - - - - - Les - - - - membres - - - - du - - - - domaine - - - - A - - - - on - - - t - - - - reçu - - - - u - - - n - - - - mail - - - - d - - - e - - - - no - - - t - - - ificatio - - - n - - - - c - - - o - - - ncernant cette - - - - nouvelle - - - - demande. - - - - - - C - - - ette - - - - demande - - - - sera prise - - - - en - - - - charge - - - - par - - - - l’expert - - - - 1 - - - - de - - - - c - - - e - - - groupe - - - - de - - - - compétence - - - - A - - - - (expert1domA). - - - - - - L’expert1domA - - - - puis - - - - all - - - e - - - r - - - - sur - - - - la - - - - liste - - - - des - - - - demandes - - - - et - - - - cliq - - - u - - - er sur - - - - la - - - - demande - - - - (ou - - - - c - - - liquer - - - - sur - - - - le - - - - lien - - - - pré - - - s - - - enté - - - - dans - - - - le - - - - mail - - - - de - - - - notification - - - - et - - - - se - - - connecter avec ce profil). - - - - - - - L’expert1domA - - - - peut - - - - alors - - - - se - - - - désigne - - - r - - - - comme - - - - responsable - - - - de - - - - cette - - - - demande. - - - - - - - - - - - - - - L’expert1domA peut alors : - - - - - - - Modifier - - - - - son - - - - - groupe - - - - - de - - - - - travail - - - - - (gr - - - oupe - - - - - - - qu’il - - - - - - - constitue et destiné - - - - - - - à - - - - - - - l’aide - - - r - - - - - - - à répondre - - - - à - - - - cette - - - - demande) - - - - - - - - - - Ajouter - - - - un - - - - commentaire - - - - public - - - - ou - - - - privé - - - - - - - - - - Ajouter - - - - - une - - - - - réponse - - - - - afin - - - - - - de - - - - - clôturer - - - - - à - - - - - tout - - - - - m - - - oment - - - - - - - cette - - - - - - - demande - - - - - - - et répondre - - - - au - - - - demandeur. - - - - - - - - - - - - - - - - Not - - - e - - - : - - - - L’historique - - - - de - - - - la - - - - demande - - - - fai - - - t - - - - - - apparaître - - - - un - - - - bloc - - - - bleu - - - - (message - - - - public) - - - signifiant la date et l’évènement ( - - - « - - - - Un - - - - responsable - - - - s’est - - - - désigné - - - - ») - - - - - - - L’expert1domA - - - - va - - - - créer - - - - son - - - - groupe - - - - de - - - - trav - - - a - - - il - - - - en - - - - c - - - liquant - - - - sur - - - - le - - - - bouton - - - - - - « - - - Modifier - - - - le groupe - - - - de - - - - travail - - - - ». - - - - - - Il - - - - verra - - - - apparaître - - - - dans - - - - la - - - - c - - - olonne - - - - d - - - e - - - - gauche - - - - l’ensemble - - - - d - - - e - - - s - - - - utilisateurs - - - - du - - - groupe - - - - « - - - - demandes - - - - » et sélectionne les personnes susceptibles de l’aider à répondre à cette demande - - - - - - - - - - - - - - - Pour - - - - valider, - - - - cliquer - - - - sur - - - - « - - - - Soumettre - - - - la - - - - requête - - - - ». - - - - - - - Not - - - e - - - : - - - - - - - L’historique - - - - - - - de - - - - - - - la - - - - - - - demande - - - - - - - fai - - - t - - - - - - apparaître - - - - - - - un - - - - - - - blo - - - c - - - - - v - - - ert - - - - - - - (message - - - - - - - privé) - - - signifiant la date et l’évènement ( - - - « - - - - Le groupe de travail a été modifié - - - - ») - - - - - - Les personnes concernées reçoivent alors un - - - - mail - - - - leur notifiant - - - - le - - - - fait - - - - que - - - - le - - - - g - - - r - - - oupe - - - - de - - - - travail - - - - a - - - - été - - - - con - - - s - - - titu - - - é - - - - - - et - - - - qu’ils - - - - sont - - - - in - - - v - - - ités - - - - à - - - - y - - - participer. - - - - - - Chaque - - - - membre - - - - du - - - - groupe - - - - peut poster des commentaires - - - - privés - - - - et - - - - publics afin de faire avancer la réponse à la demande de façon collaborative. - - - - - - L’expert1domA - - - - poste - - - - la - - - - réponse à la demande dès qu’il estime que cette dernière est prête : - - - - - - - - - Le demandeur est alors prévenu et peut décider de ré-ouvrir la demande si il n’est pas satisfait de la réponse. Passé un délai de 1 mois, la demande passe en état « archive » : - - - - - - - - - - - Exemples d’interfaces - - - - Mise en action / en suspens des process déclarés vers l’application de destination : - - - - - - - - - - - - - Interface de monitoring - - - - - - - - - - Monitoring d’un process sélectionné  - - - - - - - - - - - - Export BPML - - - - - - - - - - - - - - - - - - - - - Gestion des formulaires sur les portails (type formulaire d’enquête) - - - - - Décrire précisément la solution qui sera retenue pour générer, paramétrer et publier des formulaires, y compris dans l -' - exploitation des données qui seront saisies par les utilisateurs. - - - La proposition de Capgemini n’intègre pas de composants permettant de gérer des formulaires. Néanmoins, cette fonctionnalité est prévue dans Jahia. Un client développe en mode collaboratif ce composant. - - - - - - - Gestion des droits des internautes sur le portail - - - - - Pourra-t-on auditer les droits (et éventuellement les accès) des utilisateurs déclarés sur le portail? - - - Capgemini intègre un export des droits d’un site (utilisateur/droits). - - - - - - - Administration des portails Jahia sur Internet - - - - - Le portail est administrable à travers le protocole http. - - - Néanmoins, l -' - instance installée sur Internet reste-t-elle administrable à partir de l’Internet ? Dans le prototype, l’accès aux outils d’administration du portail est réalisé à partir d’un contrôle de login / mot de passe. Ce type de contrôle est insuffisant en production. - - - Comment l’intégrateur est-il en mesure de garantir que les outils d’administration des outils ne seront pas attaqués ? Est-il prévu des contrôles autre que des accès protégés par login / mot de passe (accès par adresse et port TCP/IP séparée) ? - - - Il est - - demandé au prestataire de préciser les moyens empêchant l -' - accès via Internet aux interfaces d -' - administration. - - - - - Les fonctions d’administrations sont retirées de l’instance Jahia sur Internet (voir question 21). - - - - - - - - - - - - - - - - - - - - - - - - - - Gestion du portail Internet - - - - - La modification de structure est-elle faite par l’outil d’administration du portail Internet en s’adressant à la bonne instance de portail ou est-elle faite à partir du portail intranet ? - - - - La modification de structure n’est pas faite uniquement par l’outil d’administration. - - - - - Les modifications de structures qui sont réalisées sur le portail intranet sont répliquées sur le portail Internet automatiquement : nouvelle page, nouvelle rubrique, nouveau sous site, nouveau groupe, nouvelle catégorie … - - - - - - Les modifications de structures qui nécessitent la mise à jour du logiciel (corrections Java, nouveau template – JSP, nouvelle application Java) sont réalisées grâce à un processus traditionnel de livraison d’un patch en pré production puis en production. - - - - - Question complémentaire : si la modification de structure est faite à partir de l’outil d’administration du portail Internet et que le ‘rubriquage’ est en partie géré dans la gestion de contenu intranet, comment se fait la synchronisation entre la structure définie sur le portail Internet et la gestion de contenu qui se trouve sur intranet (sachant que les flux entrants de l’Internet sont interdits). - - - - Les modifications de structures ne sont pas faites à partir de l’outil d’administration du portail Internet. - - - - - - - - - - Flux IZ2 - - - - - - - - - Y a-t-il des flux directs pour gérer le portail ? - - - - - Lesquels ? pour faire quoi - - - - - - - - - Il convient de préciser s’il existe des flux dans la zone Internet sur le portail et pour faire quoi. - - - - Toutes les actions - - de gestion du portail Internet et de sites associés sont-elles réalisées à partir de l’instance Jahia intranet ? Si non préciser, les limites ? - - - - - Les actions de gestion du portail Internet sont réalisées depuis l’intranet à l’exception des : - - - - - - modifications de structures qui nécessitent la mise à jour du logiciel (corrections Java, nouveau template – JSP, nouvelle application Java) sont réalisées grâce à un processus traditionnel de livraison d’un patch en pré-production puis en production. - - - - - actions d’administrations (sauvegardes, purges). - - - - - - - - - - - - (nouveau) - - Préciser par rapport au calendrier du projet la date de début de la période de maintenance annuelle des progiciels et notamment par rapport à la VSR du projet. - - - - - - Effectivement les coûts de maintenances de Jahia sont prévus à l’issue de la VSR. - - - - Durant la phase de développement du projet, le support de Jahia est directement réalisé au travers de l’intégration au projet en temps que sous-traitant. - - - Le poste 12 utile au projet du bordereau de prix est modifié en conséquence. - - - - - - - (nouveau) - - - Le Minefi fournira des explications sur la définition des syntaxes de recherche (cf annexe) - - - - - - Cap Gemini indiquera s’il peut s -' - engager à accompagner le Minefi dans le domaine fonctionnel de la mise en place des moteurs de recherche. - - - - - Le moteur Lucerne accompagné du Thésaurus permet une grande souplesse de mise en œuvre. - - Capgemini propose qu’à l’issue de la mise en place des portails une enquête soit réalisée permettant de modifier le paramétrage du moteur de recherche en fonction des retours des utilisateurs. - - - - -   - - - - - - - (nouveau) - - - Le tableau des prix des unités d’œuvre du paragraphe 5.3.1.1 est modifié par l -' - ajout de 3 colonnes (10 pages, 50 pages, 200 pages). Il devient : - - - - - - - - - - - - - Reprise d’un site statique - - - - - - - - - - Coût unitaire (Euros HT) - - - - - - - - - Pour 10 pages - - - - - Pour 50 pages - - - - - Pour 200 pages - - - - - - - - - - - - Page simple : uniquement de l’information - - - - - - - - - - - - - - - - Page complexe : information, 8 images ou liens relatifs au maximum - - - - - - - - - - - - - - - - Page très complexe : information, plus de 8 images ou liens relatifs au maximum - - - - - - - - - - - - - - - Intégré. - - -    - - - - - - (nouveau) - - - décrire les outils de back office pour la gestion et/ ou les portails qui seront proposés (exemple : rechercher les documents qui ne publiés dans aucun site, rechercher les documents qui ont plus de deux ans. - - - - - - - - Capgemini propose d’intégrer un agent de la DPMA qui peut développer ces fonctionnalités d’exploration des contenus de Jahia. - - - - - - Annexes - - - - - - - - - Tableau de synthèse Méta-données - - - (document non validé) - - - - - - - - - - - Méta-données - - - - - Obligatoire/facultatif - - - - - commentaires - - - - - - - Date de création - - - - - - - Obligatoire - - - - - - - - - - Date de publication - - - - - Obligatoire - - - - - cette date sera affichée par défaut avec la date du jour, modifiable par le contributeur. Cette date pourra être utilisée comme date prévisionnelle de publication - - - - - - - Date de modification - - - - - Obligatoire - - - - - cette date sera renseignée par défaut par la date système. Elle devra être modifiable par le contributeur. Le fait d’afficher cette date sur la page devra être spécifiée (case à cocher) par le contributeur. - - - - - - - - Date de fin de publication - - - - - Facultatif/obligatoire - - - - - tout ce qui est événementielle comme par exemple « le printemps des poètes , la déclaration de l’impôt.. » auront une date de fin de publication facile à définir. - - - Pour toutes les autres rubriques , ce sera le responsable éditorial qui devra gérer cette fin de publication . - - - - - La - - date de fin de publication sera attachée non pas au document mais à la rubrique .En effet un document budgétaire peut être publié soit dans l’actualité (événement) soit dans une rubrique budget. En fonction de la rubrique, la date de fin de publication est donc différente. La date de fin de publication sera affichée par défaut modifiable par le responsable éditorial - - - - - - - - - Date de révision - - - - - Facultative/obligatoire - - - - - - - une date de prochaine révision - - sera obligatoire pour tous les documents sauf pour les événements . Elle sera - - affichée par défaut (même date suivant la typologie des documents). Cette date sera une alerte pour le responsable éditorial qui pourra consulter tous les documents devant être « révisés c’est à dire doit il rester en ligne ? si non mettre une date de fin de publication ) - - - - - - - - - - Date de soumission - - - - - - - - - Non retenue - - - - - - - - -title - - - - - obligatoire - - - - - - Il a été décidé de faire remplir directement par le rédacteur à partir du formulaire de l’outil de gestion de contenu, une zone -" - Title -" - différenciée de la rubrique titre du document. Une règle de gestion a été définie : la zone title affichera par défaut le titre du document préalablement saisi, la date de création du document et la typologie du document. - - - - - - - - Description - - - - - - obligatoire en fonction typologies des documents - - - - - - elle sera obligatoire en fonction de la typologie du document (une brève pas de résumé , un rapport obligatoire). - - - - - - - - Mots clés - - - - - Obligatoire - - - - - les thésaurus ne seront pas utilisés . les mots clés seront automatiquement créés à partir du plan de classement (cumul de toutes les arborescences) - - - - - - - - Typologie des documents - - - - - Obligatoire - - - - - Normalement une typologie - - correspond à un gabarit . En fonction de la liste où environ 50 typologies sont prévues , une étude sur la portée sémantique devra être réalisée pour regrouper plusieurs typologies à un formulaire de saisie. - - - - - - - Créateur/auteur - - - - - Facultatif - - - - - cela correspond au nom de la personne ou propriétaire à l’origine de l’information. Sur Internet le Sircom ne souhaite pas avoir cette information là. Sur Intranet, cette information pourrait être affichée dans la page - - - - - - - Méta langage - - - - - Obligatoire - - - - - Il est décidé de mettre par défaut « Français » avec possibilité de modifier la valeur. - - - - - - - - Méta droits - - - - - Obligatoire - - - - - - Pour tous les documents qu’ils soient sur Intranet/Internet, seront renseignés automatiquement avec un copy right. - - - - - - - - - Méta couverture - - - - - ? - - - - - Deux possibilités ont été présentées : - - - La saisie dans le formulaire de la couverture géographique - - sera renseignée - - mais il s’afficherait dans les métas - keyword - - - - - La saisie se ferait dans une zone méta-couverture spécifique - - - - - - - - Editeur - - - - - Facultatif - - - - - personne qui publie. Sur l’internet dans tous les cas il faudra renseigner par défaut : éditeur = minéfi-Sircom ; pour intranet cette information n’est pas utile - - - - - - - - - - Syntaxe d -' - interrogation - - - - - - - - Recherche approchée / recherche floue - - - - - Recherche élargie : -" - adminstration -" - cherche -" - Administration -" - - - - - - - Recherche phonétique - - - - -" - contrefasson -" - cherche -" - contrefaçon -" - - - - - - - Expressions exacte - - - - - - - - - - Masque de recherche - - - - -" - ele.*t cherche les mots commençant par ele et finissant par t - - - - - - - Troncature limitée / illimitée - - - - -" - Impo? -" - cherche -" - Impot -" - - -" - Cors* -" - cherche -" - Corse -" - , -" - Corsica -" - , -" - Corsaire -" - … - - - - - - - Correction orthographique - - - - - Proposition d -' - une orthographe alternative - - - - - - - Lemmatisation - - - - - Analyse de la racine des termes : -" - cheval -" - cherche -" - chevaux -" - - - - - - - Combinaison des fonctionnalités - - - - - Recherche phonétique approchée, Recherche par masque approchée, recherche par masque + troncature - - - - - - - Personnalisation - - - - - - - - Thesaurus / dictionnaires - - - - - Addition à la recherche plein texte - - - - - - - Recherche sur champs - - - - - Recherche sur des champs spécifiques (champs auteur, résumé, mots-clefs, titre…) - - - - - - - Pondération des résultats - - - - - - - - Adéquation requête / document - - - - - - - - - - - - - - - - - Opérateur de proximité par défaut - - : les documents qui contiennent les mots de la requête les plus proches les uns des autres sont favorisés - - - - - - - Les formes exactes sont favorisées par rapport aux formes lemmatisées - - - - - - - Définition d -' - un espacement maximum entre les mots de la requête - - - - - - - - - - Champs sur-pondérés - - - - - - - - - - - - - Les champs dont la pondération est supérieure à celle du texte sont paramétrables (par défaut : titre) - - - - - - - Indice de popularité (ranking) - - - - - - - - - - - - - Classement des documents en fonction du nombre de lien pointant vers la page. - - - - - - - diff --git a/contrib/orm-persistence/applications/test/log4j.xml b/contrib/orm-persistence/applications/test/log4j.xml deleted file mode 100644 index 89d2701d02d..00000000000 --- a/contrib/orm-persistence/applications/test/log4j.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/applications/test/ojb/repository.dtd b/contrib/orm-persistence/applications/test/ojb/repository.dtd deleted file mode 100644 index 6a2d4668417..00000000000 --- a/contrib/orm-persistence/applications/test/ojb/repository.dtd +++ /dev/null @@ -1,950 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/applications/test/ojb/repository.xml b/contrib/orm-persistence/applications/test/ojb/repository.xml deleted file mode 100644 index 43c4c535fe3..00000000000 --- a/contrib/orm-persistence/applications/test/ojb/repository.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - -]> - - - - - - &database; - - - &internal; - - - &user; - - diff --git a/contrib/orm-persistence/applications/test/ojb/repository_database.xml b/contrib/orm-persistence/applications/test/ojb/repository_database.xml deleted file mode 100644 index d7a1f19a148..00000000000 --- a/contrib/orm-persistence/applications/test/ojb/repository_database.xml +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/applications/test/ojb/repository_internal.xml b/contrib/orm-persistence/applications/test/ojb/repository_internal.xml deleted file mode 100644 index f7ff25a49fc..00000000000 --- a/contrib/orm-persistence/applications/test/ojb/repository_internal.xml +++ /dev/null @@ -1,362 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/applications/test/ojb/repository_user.xml b/contrib/orm-persistence/applications/test/ojb/repository_user.xml deleted file mode 100644 index 973d09db87d..00000000000 --- a/contrib/orm-persistence/applications/test/ojb/repository_user.xml +++ /dev/null @@ -1,285 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/applications/test/repository.xml b/contrib/orm-persistence/applications/test/repository.xml deleted file mode 100644 index 04d3cf4582d..00000000000 --- a/contrib/orm-persistence/applications/test/repository.xml +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/applications/test/repository/nodetypes/custom_nodetypes.xml b/contrib/orm-persistence/applications/test/repository/nodetypes/custom_nodetypes.xml deleted file mode 100644 index 430a328bd66..00000000000 --- a/contrib/orm-persistence/applications/test/repository/nodetypes/custom_nodetypes.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - mix:versionable - nt:base - - - - - - - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - nt:base - - - - - diff --git a/contrib/orm-persistence/applications/test/repositoryStubImpl.properties b/contrib/orm-persistence/applications/test/repositoryStubImpl.properties deleted file mode 100644 index 37b115e1460..00000000000 --- a/contrib/orm-persistence/applications/test/repositoryStubImpl.properties +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2003-2005 The Apache Software Foundation or its licensors, -# as applicable -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT 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 the configuration file for the jackrabbit repository test stub. -# - -# Stub implementation class -javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.test.JackrabbitRepositoryStub - -# credential configuration -javax.jcr.tck.superuser.name=superuser -javax.jcr.tck.superuser.pwd= -javax.jcr.tck.readwrite.name=user -javax.jcr.tck.readwrite.pwd= -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.nodename1=foo -javax.jcr.tck.nodename2=bar -javax.jcr.tck.nodename3=foobar -javax.jcr.tck.nodename4=myname -javax.jcr.tck.propertyname1=prop1 -javax.jcr.tck.propertyname2=prop2 -javax.jcr.tck.workspacename=test - -# namespace configuration -javax.jcr.tck.namespaces=test -javax.jcr.tck.namespaces.test=http://www.apache.org/jackrabbit/test - -# sample for per test case config overriding -# Test class: AddNodeText -# Test method: testName -javax.jcr.tck.AddNodeTest.testName.nodename1=myname - -# QUERY CONFIGURATION - -# Test class: SaveTest -# Test method: testConstraintViolationException -# Specified node type must not allow child nodes. -javax.jcr.tck.SaveTest.testConstraintViolationException.nodetype=nt:query - -# VERSIONING CONFIGURATION - -# nodetye 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.propertyValue=aPropertyValue - -# 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 - -# 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 - -# repository name -org.apache.jackrabbit.repository.config=applications/test/repository.xml -org.apache.jackrabbit.repository.name=repo -org.apache.jackrabbit.repository.home=applications/test diff --git a/contrib/orm-persistence/applications/test/workspaces/default/workspace.xml b/contrib/orm-persistence/applications/test/workspaces/default/workspace.xml deleted file mode 100644 index afc8fa0f64b..00000000000 --- a/contrib/orm-persistence/applications/test/workspaces/default/workspace.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/checkstyle.xml b/contrib/orm-persistence/checkstyle.xml deleted file mode 100644 index 9a2d4485e83..00000000000 --- a/contrib/orm-persistence/checkstyle.xml +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/create_db_hsqldb.sql b/contrib/orm-persistence/create_db_hsqldb.sql deleted file mode 100644 index 823c0b972aa..00000000000 --- a/contrib/orm-persistence/create_db_hsqldb.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TABLE JCR_NODE(UUID VARCHAR(36) NOT NULL,PARENT_UUID VARCHAR(36),DEFINITION_ID VARCHAR(100),NODE_TYPE VARCHAR(100),PRIMARY KEY(UUID)) -CREATE TABLE JCR_PROPERTY(ITEM_ID VARCHAR(200) NOT NULL,NAME VARCHAR(100),VALUE VARCHAR(100),PARENT_UUID VARCHAR(36),PROP_TYPE INTEGER,DEFINITION_ID VARCHAR(100),MULTI_VALUED CHAR(1),PRIMARY KEY (ITEM_ID)) -CREATE TABLE JCR_NODE_MIXIN_TYPE(NODE_UUID VARCHAR(36) NOT NULL,MIXIN_TYPE VARCHAR(100)) -CREATE TABLE JCR_NODE_PARENT(NODE_UUID VARCHAR(36) NOT NULL,PARENT_UUID VARCHAR(36)) -CREATE TABLE JCR_CHILD_NODE(PARENT_UUID VARCHAR(36),UUID VARCHAR(36),NAME VARCHAR(100),NODE_INDEX INTEGER) -CREATE TABLE JCR_NODE_PROPERTY(PARENT_UUID VARCHAR(36) NOT NULL,NAME VARCHAR(100)) -CREATE TABLE JCR_NODE_REF(NREF_ID IDENTITY,TARGET_UUID VARCHAR(36) NOT NULL,PROP_UUID VARCHAR(36),PROP_NAME VARCHAR(100)) -CREATE TABLE JCR_BLOB(BLOB_ID IDENTITY, PARENT_UUID VARCHAR(36), PROP_NAME VARCHAR(100), VALUE_INDEX INTEGER, BLOB_SIZE INTEGER, BLOB_VALUE LONGVARBINARY) -CREATE INDEX JCR_NODE_PROPERTY_INDEX1 ON JCR_NODE_PROPERTY(PARENT_UUID, NAME) -CREATE INDEX JCR_NODE_PROPERTY_INDEX2 ON JCR_NODE_PROPERTY(PARENT_UUID) -CREATE INDEX JCR_CHILD_NODE_INDEX1 ON JCR_CHILD_NODE (PARENT_UUID, UUID, NAME, NODE_INDEX) -CREATE INDEX JCR_CHILD_NODE_INDEX2 ON JCR_CHILD_NODE (PARENT_UUID) -CREATE INDEX JCR_NODE_MIXIN_TYPE_INDEX1 ON JCR_NODE_MIXIN_TYPE (NODE_UUID, MIXIN_TYPE) -CREATE INDEX JCR_NODE_MIXIN_TYPE_INDEX2 ON JCR_NODE_MIXIN_TYPE (NODE_UUID) -CREATE INDEX JCR_NODE_PARENT_INDEX1 ON JCR_NODE_PARENT (NODE_UUID, PARENT_UUID) -CREATE INDEX JCR_NODE_PARENT_INDEX2 ON JCR_NODE_PARENT (NODE_UUID) -CREATE INDEX JCR_NODE_REF_INDEX1 ON JCR_NODE_REF(TARGET_UUID, PROP_UUID, PROP_NAME) -CREATE INDEX JCR_NODE_REF_INDEX2 ON JCR_NODE_REF(TARGET_UUID) -CREATE INDEX JCR_BLOB_INDEX1 ON JCR_BLOB(PARENT_UUID, PROP_NAME, VALUE_INDEX) -CREATE USER SA PASSWORD "" ADMIN diff --git a/contrib/orm-persistence/create_db_mysql.sql b/contrib/orm-persistence/create_db_mysql.sql deleted file mode 100644 index d4f56657f09..00000000000 --- a/contrib/orm-persistence/create_db_mysql.sql +++ /dev/null @@ -1,82 +0,0 @@ -DROP DATABASE IF EXISTS jackrabbit ; - -CREATE DATABASE jackrabbit ; - -USE jackrabbit ; - -CREATE TABLE JCR_NODE( - UUID VARCHAR(36) NOT NULL, - PARENT_UUID VARCHAR(36), - DEFINITION_ID VARCHAR(100), - NODE_TYPE VARCHAR(100), - PRIMARY KEY (UUID) -) TYPE=InnoDB; - -CREATE TABLE JCR_PROPERTY( - ITEM_ID VARCHAR(200) NOT NULL, - NAME VARCHAR(100), - VALUE VARCHAR(100), - PARENT_UUID VARCHAR(36), - PROP_TYPE INTEGER, - DEFINITION_ID VARCHAR(100), - MULTI_VALUED CHAR(1), - PRIMARY KEY (ITEM_ID) -) TYPE=InnoDB; - -CREATE TABLE JCR_NODE_MIXIN_TYPE( - NODE_UUID VARCHAR(36) NOT NULL, - MIXIN_TYPE VARCHAR(100) -) TYPE=InnoDB; - -CREATE TABLE JCR_NODE_PARENT( - NODE_UUID VARCHAR(36) NOT NULL, - PARENT_UUID VARCHAR(36) -) TYPE=InnoDB; - -CREATE TABLE JCR_CHILD_NODE( - PARENT_UUID VARCHAR(36) NOT NULL, - UUID VARCHAR(36), - NAME VARCHAR(100), - NODE_INDEX INTEGER -) TYPE=InnoDB; - -CREATE TABLE JCR_NODE_PROPERTY( - PARENT_UUID VARCHAR(36) NOT NULL, - NAME VARCHAR(100) -) TYPE=InnoDB; - -CREATE TABLE JCR_NODE_REF( - NREF_ID INTEGER NOT NULL AUTO_INCREMENT, - TARGET_UUID VARCHAR(36) NOT NULL, - PROP_UUID VARCHAR(36), - PROP_NAME VARCHAR(100), - PRIMARY KEY (NREF_ID) -) TYPE=InnoDB; - -CREATE TABLE JCR_BLOB( - BLOB_ID INTEGER NOT NULL AUTO_INCREMENT, - PARENT_UUID VARCHAR(36), - PROP_NAME VARCHAR(100), - VALUE_INDEX INTEGER, - BLOB_SIZE BIGINT, - BLOB_VALUE BLOB, - PRIMARY KEY(BLOB_ID) -) TYPE=InnoDB; - - -CREATE INDEX JCR_NODE_PROPERTY_INDEX1 ON JCR_NODE_PROPERTY(PARENT_UUID, NAME); -CREATE INDEX JCR_NODE_PROPERTY_INDEX2 ON JCR_NODE_PROPERTY(PARENT_UUID); - -CREATE INDEX JCR_CHILD_NODE_INDEX1 ON JCR_CHILD_NODE (PARENT_UUID, UUID, NAME, NODE_INDEX); -CREATE INDEX JCR_CHILD_NODE_INDEX2 ON JCR_CHILD_NODE (PARENT_UUID); - -CREATE INDEX JCR_NODE_MIXIN_TYPE_INDEX1 ON JCR_NODE_MIXIN_TYPE (NODE_UUID, MIXIN_TYPE); -CREATE INDEX JCR_NODE_MIXIN_TYPE_INDEX2 ON JCR_NODE_MIXIN_TYPE (NODE_UUID); - -CREATE INDEX JCR_NODE_PARENT_INDEX1 ON JCR_NODE_PARENT (NODE_UUID, PARENT_UUID); -CREATE INDEX JCR_NODE_PARENT_INDEX2 ON JCR_NODE_PARENT (NODE_UUID); - -CREATE INDEX JCR_NODE_REF_INDEX1 ON JCR_NODE_REF(TARGET_UUID, PROP_UUID, PROP_NAME); -CREATE INDEX JCR_NODE_REF_INDEX2 ON JCR_NODE_REF(TARGET_UUID); - -CREATE INDEX JCR_BLOB_INDEX1 ON JCR_BLOB(PARENT_UUID, PROP_NAME, VALUE_INDEX); diff --git a/contrib/orm-persistence/maven.xml b/contrib/orm-persistence/maven.xml deleted file mode 100644 index 795352ee958..00000000000 --- a/contrib/orm-persistence/maven.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/orm-persistence/project.properties b/contrib/orm-persistence/project.properties deleted file mode 100644 index a7190113c82..00000000000 --- a/contrib/orm-persistence/project.properties +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2003-2005 The Apache Software Foundation or its licensors, -# as applicable -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -###################################################################### -# JUnit Testing -###################################################################### - -maven.test.failure = false -maven.junit.fork=true -maven.test.search.classdir=true -maven.junit.sysproperties=org.xml.sax.driver -maven.junit.jvmargs=-Xmx1024M -org.xml.sax.driver=org.apache.xerces.parsers.SAXParser - -###################################################################### -# JavaDoc -# -# javadoc urls can be added here, multiple urls are appended using a comma -# -# maven.javadoc.links = http://foo/bar/api,\ -# http://flim/flam/api/ -###################################################################### -maven.javadoc.links=http://java.sun.com/j2se/1.4.2/docs/api/,\ - http://incubator.apache.org/jackrabbit/apidocs/,\ - http://www.day.com/maven/jsr170/javadocs/jcr-0.16.1-pfd/ -maven.javadoc.author=false -maven.javadoc.version=false - -###################################################################### -# Checkstyle -###################################################################### -maven.checkstyle.properties= checkstyle.xml -maven.linkcheck.enable=false diff --git a/contrib/orm-persistence/project.xml b/contrib/orm-persistence/project.xml deleted file mode 100644 index f4627d9184e..00000000000 --- a/contrib/orm-persistence/project.xml +++ /dev/null @@ -1,235 +0,0 @@ - - - - 3 - jackrabbit-orm - jackrabbit - JackRabbit ORM Persistence Managers - 0.16.2-dev - - The Apache Software Foundation - http://incubator.apache.org/projects/jackrabbit.html - http://incubator.apache.org/images/apache-incubator-logo.png - - 2004 - org.apache.jackrabbit.* - /images/jackrabbitlogo.gif - - ORM implementation of Jackrabbit persistence managers. This sub-projects of Jackrabbit adds two - persistence manager implementations that use Object - Relational mapping technology : OJB - (http://db.apache.org/ojb) and Hibernate (http://www.hibernate.org). - - ORM persistence managers for Jackrabbit - - - - Serge Huber - - - - Java Developer - - +1 - - - - - - jackrabbit - jackrabbit - 0.16.2-dev - - - concurrent - 1.3.4 - - - commons-collections - 2.1 - - - jdom - 1.0 - - - geronimo-spec - geronimo-spec-jta - 1.0-M1 - - - jsr170 - jcr - 0.16.2 - http://www.day.com/maven/jsr170/jars/jcr-0.16.2.jar - - - log4j - 1.2.8 - - - lucene - lucene - 1.4.3 - - - xerces - xercesImpl - 2.6.2 - - - xerces - xmlParserAPIs - 2.6.2 - - - commons-logging - 1.0 - - - - - ojb - db-ojb - 1.0.1 - - - hsqldb - hsqldb - 1.7.1 - - - commons-lang - commons-lang - 2.0 - - - commons-pool - commons-pool - 1.2 - - - commons-dbcp - 1.2.1 - - - mysql - mysql-connector-java - 3.0.10-stable-bin - - - - - hibernate - hibernate - 2.1.7c - - - dom4j - dom4j - 1.4 - - - ehcache - ehcache - 0.9 - - - cglib - cglib-full - 2.0.2 - - - odmg - odmg - 3.0 - - - - - cqfs - cqfs-jackrabbit - 3.5.6 - http://www.day.com/maven/cqfs/jars/cqfs-jackrabbit-3.5.6.jar - - - cqfs - cqfs - 3.5.6 - http://www.day.com/maven/cqfs/jars/cqfs-3.5.6.jar - - - - - - src/java - src/test - - - **/*TestAll.class - - - - applications/test - - *.properties - *.xml - ojb/*.xml - ojb/*.dtd - hibernate/*.xml - hibernate/*.dtd - - - - - - - - applications/test - - *.properties - *.xml - ojb/*.xml - ojb/*.dtd - hibernate/*.xml - hibernate/*.dtd - - - - - - - maven-changelog-plugin - maven-changes-plugin - maven-checkstyle-plugin - - - - maven-javadoc-plugin - - maven-junit-report-plugin - maven-jxr-plugin - maven-license-plugin - - - maven-tasklist-plugin - - - diff --git a/contrib/orm-persistence/reset_db_mysql.bat b/contrib/orm-persistence/reset_db_mysql.bat deleted file mode 100644 index 0ad795adb28..00000000000 --- a/contrib/orm-persistence/reset_db_mysql.bat +++ /dev/null @@ -1,3 +0,0 @@ -mysqladmin -f -uroot drop jackrabbit -mysqladmin -uroot create jackrabbit -mysql -uroot jackrabbit < create_db_mysql.sql \ No newline at end of file diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMBlobValue.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMBlobValue.java deleted file mode 100644 index c797c74feb6..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMBlobValue.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm; - -import java.io.Serializable; - -/** - * BLOB value ORM object - */ -public class ORMBlobValue - implements Serializable { - - private Integer dbId; - private String parentUUID; - private String propertyName; - private Integer index; - private Long size; - private byte[] blobValue; - public ORMBlobValue() { - } - - public void setDbId(Integer dbId) { - this.dbId = dbId; - } - - public void setParentUUID(String parentUUID) { - this.parentUUID = parentUUID; - } - - public void setPropertyName(String propertyName) { - this.propertyName = propertyName; - } - - public void setIndex(Integer index) { - this.index = index; - } - - public void setSize(Long size) { - - this.size = size; - } - - public void setBlobValue(byte[] blobValue) { - - this.blobValue = blobValue; - } - - public Integer getDbId() { - return dbId; - } - - public String getParentUUID() { - return parentUUID; - } - - public String getPropertyName() { - return propertyName; - } - - public Integer getIndex() { - return index; - } - - public Long getSize() { - - return size; - } - - public byte[] getBlobValue() { - - return blobValue; - } - - public boolean equals(Object obj) { - if (!(obj instanceof ORMBlobValue)) { - return false; - } - ORMBlobValue right = (ORMBlobValue) obj; - if (dbId == null) { - if (right.getDbId() == null) { - return true; - } - // let's test other values. - if (parentUUID.equals(right.getParentUUID()) && - propertyName.equals(right.getPropertyName()) && - index.equals(right.getIndex())) { - return true; - } - return false; - } - if (dbId.equals(right.getDbId())) { - return true; - } else { - return false; - } - } - - public int hashCode() { - return getDbId().hashCode(); - } - -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMChildNodeEntry.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMChildNodeEntry.java deleted file mode 100644 index 0c9d6d1e73a..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMChildNodeEntry.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm; - -import java.io.Serializable; - -import org.apache.jackrabbit.core.state.NodeState.ChildNodeEntry; - -/** - *

This class represents a child node entry row in the ORM object graph.

- */ -public class ORMChildNodeEntry - implements Serializable, Comparable { - private String uuid; - private String parentUUID; - private String name; - private Integer index; - private Integer dbId; - private ORMNodeState parent; - public ORMChildNodeEntry() { - } - - public ORMChildNodeEntry(ORMNodeState parent, ChildNodeEntry childNodeEntry, String parentUUID) { - this.parent = parent; - uuid = childNodeEntry.getUUID(); - this.parentUUID = parentUUID; - name = childNodeEntry.getName().toString(); - index = new Integer(childNodeEntry.getIndex()); - } - - public void setUuid(String uuid) { - this.uuid = uuid; - } - - public void setParentUUID(String parentUUID) { - this.parentUUID = parentUUID; - } - - public void setName(String name) { - this.name = name; - } - - public void setIndex(Integer index) { - this.index = index; - } - - public void setDbId(Integer dbId) { - this.dbId = dbId; - } - - public void setParent(ORMNodeState parent) { - this.parent = parent; - } - - public String getUuid() { - return uuid; - } - - public String getParentUUID() { - return parentUUID; - } - - public String getName() { - return name; - } - - public Integer getIndex() { - return index; - } - - public Integer getDbId() { - return dbId; - } - - public ORMNodeState getParent() { - return parent; - } - - public boolean equals(Object obj) { - if (!(obj instanceof ORMChildNodeEntry)) { - return false; - } - ORMChildNodeEntry right = (ORMChildNodeEntry) obj; - if (getUuid().equals(right.getUuid()) && - getName().equals(right.getName()) && - (getIndex().equals(right.getIndex()))) { - return true; - } else { - return false; - } - } - - public int compareTo(Object obj) { - if (equals(obj)) { - return 0; - } - ORMChildNodeEntry right = (ORMChildNodeEntry) obj; - return (getUuid() + getName() + getIndex()).compareTo(right.getUuid() + right.getName() + right.getIndex()); - } - - public int hashCode() { - return (getUuid() + getName() + getIndex()).hashCode(); - } -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeMixinType.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeMixinType.java deleted file mode 100644 index cf0355dbaa4..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeMixinType.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm; - -import java.io.Serializable; - -/** - *

This class represents a single entry of a mixin type for a node.

- */ -public class ORMNodeMixinType - implements Serializable { - private String nodeUUID; - private String mixinTypeName; - private Integer dbId; - private ORMNodeState node; - public ORMNodeMixinType() { - } - - public ORMNodeMixinType(ORMNodeState node, String nodeUUID, String mixinTypeName) { - this.node = node; - this.nodeUUID = nodeUUID; - this.mixinTypeName = mixinTypeName; - } - - public void setNodeUUID(String nodeUUID) { - this.nodeUUID = nodeUUID; - } - - public void setMixinTypeName(String mixinTypeName) { - - this.mixinTypeName = mixinTypeName; - } - - public void setDbId(Integer dbId) { - this.dbId = dbId; - } - - public void setNode(ORMNodeState node) { - this.node = node; - } - - public String getNodeUUID() { - return nodeUUID; - } - - public String getMixinTypeName() { - - return mixinTypeName; - } - - public Integer getDbId() { - return dbId; - } - - public ORMNodeState getNode() { - return node; - } - - public boolean equals(Object obj) { - if (!(obj instanceof ORMChildNodeEntry)) { - return false; - } - ORMNodeMixinType right = (ORMNodeMixinType) obj; - if (getMixinTypeName().equals(right.getMixinTypeName())) { - return true; - } else { - return false; - } - } - - public int hashCode() { - return getMixinTypeName().hashCode(); - } - -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeParent.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeParent.java deleted file mode 100644 index 23e89659450..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeParent.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm; - -import java.io.Serializable; - -/** - *

This class represents a single entry of a node's parents.

- */ -public class ORMNodeParent - implements Serializable { - private String nodeUUID; - private String parentUUID; - private Integer dbId; - private ORMNodeState node; - public ORMNodeParent() { - } - - public ORMNodeParent(ORMNodeState node, String nodeUUID, String parentUUID) { - this.node = node; - this.nodeUUID = nodeUUID; - this.parentUUID = parentUUID; - } - - public void setNodeUUID(String nodeUUID) { - this.nodeUUID = nodeUUID; - } - - public void setParentUUID(String parentUUID) { - this.parentUUID = parentUUID; - } - - public void setDbId(Integer dbId) { - this.dbId = dbId; - } - - public void setNode(ORMNodeState node) { - this.node = node; - } - - public String getNodeUUID() { - return nodeUUID; - } - - public String getParentUUID() { - return parentUUID; - } - - public Integer getDbId() { - return dbId; - } - - public ORMNodeState getNode() { - return node; - } - - public boolean equals(Object obj) { - if (!(obj instanceof ORMNodeParent)) { - return false; - } - ORMNodeParent right = (ORMNodeParent) obj; - if (getParentUUID().equals(right.getParentUUID())) { - return true; - } else { - return false; - } - } - - public int hashCode() { - return getParentUUID().hashCode(); - } -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeReference.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeReference.java deleted file mode 100644 index a4f1dc9f002..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeReference.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm; - -import java.io.Serializable; - -/** - *

This class represents a node reference, that is to say a property that - * points to a specific node.

- */ -public class ORMNodeReference - implements Serializable { - private String targetId; - private String propertyParentUUID; - private String propertyName; - private Integer dbId; - public ORMNodeReference() { - } - - public ORMNodeReference(String targetId, String propertyParentUUID, String propertyName) { - this.targetId = targetId; - this.propertyParentUUID = propertyParentUUID; - this.propertyName = propertyName; - } - - public void setTargetId(String targetId) { - this.targetId = targetId; - } - - public void setPropertyParentUUID(String propertyParentUUID) { - this.propertyParentUUID = propertyParentUUID; - } - - public void setPropertyName(String propertyName) { - this.propertyName = propertyName; - } - - public void setDbId(Integer dbId) { - this.dbId = dbId; - } - - public String getTargetId() { - return targetId; - } - - public String getPropertyParentUUID() { - return propertyParentUUID; - } - - public String getPropertyName() { - return propertyName; - } - - public Integer getDbId() { - return dbId; - } - - public boolean equals(Object obj) { - if (!(obj instanceof ORMNodeParent)) { - return false; - } - ORMNodeReference right = (ORMNodeReference) obj; - if (getTargetId().equals(right.getTargetId()) && - getPropertyParentUUID().equals(right.getPropertyParentUUID()) && - getPropertyName().equals(right.getPropertyName()) ) { - return true; - } else { - return false; - } - } - - public int hashCode() { - return (getTargetId() + getPropertyParentUUID() + getPropertyName()).hashCode(); - } -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeState.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeState.java deleted file mode 100644 index 43d58c81a2b..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMNodeState.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import org.apache.jackrabbit.core.ItemId; -import org.apache.jackrabbit.core.QName; -import org.apache.jackrabbit.core.nodetype.NodeDefId; -import org.apache.jackrabbit.core.state.NodeState; -import org.apache.jackrabbit.core.state.NodeState.ChildNodeEntry; -import org.apache.jackrabbit.core.state.NodeState.PropertyEntry; -import org.apache.log4j.Logger; - -/** - *

This class represents an copy of Jackrabbit's node state, in an ORM - * compatible format.

- */ -public abstract class ORMNodeState implements Serializable { - - private static Logger log = Logger.getLogger(ORMNodeState.class); - - protected String uuid; - protected String parentUUID; - protected String nodeTypeName; - protected String definitionId; - - public ORMNodeState() { - - } - - public ORMNodeState(ItemId id) { - uuid = id.toString(); - } - - public ORMNodeState(NodeState state) { - fromPersistentNodeState(state); - } - - public void fromPersistentNodeState(NodeState state) { - getChildNodeEntries().clear(); - getPropertyEntries().clear(); - getMixinTypeNames().clear(); - getParentUUIDs().clear(); - uuid = state.getUUID(); - parentUUID = state.getParentUUID(); - if (state.getNodeTypeName() != null) { - nodeTypeName = state.getNodeTypeName().toString(); - } - if (state.getDefinitionId() != null) { - definitionId = state.getDefinitionId().toString(); - } - Iterator childNodeEntriesIter = state.getChildNodeEntries().iterator(); - while (childNodeEntriesIter.hasNext()) { - ChildNodeEntry curChildNodeEntry = (ChildNodeEntry) childNodeEntriesIter.next(); - log.debug("childNodeEntry " + curChildNodeEntry.getIndex() + " name=" + curChildNodeEntry.getName() + " uuid=" + curChildNodeEntry.getUUID()); - ORMChildNodeEntry childNode = new ORMChildNodeEntry(this, curChildNodeEntry, uuid); - getChildNodeEntries().add(childNode); - } - Iterator propertyEntryIter = state.getPropertyEntries().iterator(); - while (propertyEntryIter.hasNext()) { - PropertyEntry curPropertyEntry = (PropertyEntry) propertyEntryIter.next(); - log.debug("propertyEntry " + curPropertyEntry.getName()); - ORMPropertyEntry propertyEntry = new ORMPropertyEntry(this, curPropertyEntry, uuid); - getPropertyEntries().add(propertyEntry); - } - Iterator mixinTypeIter = state.getMixinTypeNames().iterator(); - while (mixinTypeIter.hasNext()) { - QName curName = (QName) mixinTypeIter.next(); - getMixinTypeNames().add(new ORMNodeMixinType(this, uuid, curName.toString())); - } - Iterator parentIter = state.getParentUUIDs().iterator(); - while (parentIter.hasNext()) { - String parentId = (String) parentIter.next(); - getParentUUIDs().add(new ORMNodeParent(this, uuid, parentId)); - } - } - - public String getUuid() { - return uuid; - } - - public String getParentUUID() { - return parentUUID; - } - - public String getNodeTypeName() { - return nodeTypeName; - } - - public String getDefinitionId() { - - return definitionId; - } - - public abstract Collection getChildNodeEntries(); - - public abstract Collection getPropertyEntries(); - - public abstract Collection getMixinTypeNames(); - - public abstract Collection getParentUUIDs(); - - public void setUuid(String uuid) { - this.uuid = uuid; - } - - public void setParentUUID(String parentUUID) { - this.parentUUID = parentUUID; - } - - public void setNodeTypeName(String nodeTypeName) { - this.nodeTypeName = nodeTypeName; - } - - public void setDefinitionId(String definitionId) { - - this.definitionId = definitionId; - } - - public abstract void setChildNodeEntries(Collection childNodeEntries); - - public abstract void setPropertyEntries(Collection propertyEntries); - - public abstract void setMixinTypeNames(Collection mixinTypeNames); - - public abstract void setParentUUIDs(Collection parentUUIDs); - - public void toPersistentNodeState(NodeState state) { - state.setDefinitionId(NodeDefId.valueOf(getDefinitionId())); - state.setNodeTypeName(QName.valueOf(getNodeTypeName())); - state.setParentUUID(getParentUUID()); - - Iterator childNodeEntryIter = getChildNodeEntries().iterator(); - while (childNodeEntryIter.hasNext()) { - ORMChildNodeEntry curChildNodeEntry = (ORMChildNodeEntry) childNodeEntryIter.next(); - log.debug(" Loaded child node " + QName.valueOf(curChildNodeEntry.getName()) + " uuid=" + curChildNodeEntry.getUuid()); - state.addChildNodeEntry(QName.valueOf(curChildNodeEntry.getName()), curChildNodeEntry.getUuid()); - } - Iterator propertyEntryIter = getPropertyEntries().iterator(); - while (propertyEntryIter.hasNext()) { - ORMPropertyEntry curPropertyEntry = (ORMPropertyEntry) propertyEntryIter.next(); - log.debug(" Loaded property " + QName.valueOf(curPropertyEntry.getName())); - state.addPropertyEntry(QName.valueOf(curPropertyEntry.getName())); - } - Iterator mixinTypeNameIter = getMixinTypeNames().iterator(); - Set mixinTypeQNames = new HashSet(); - while (mixinTypeNameIter.hasNext()) { - ORMNodeMixinType curMixinType = (ORMNodeMixinType) mixinTypeNameIter.next(); - mixinTypeQNames.add(QName.valueOf(curMixinType.getMixinTypeName())); - } - state.setMixinTypeNames(mixinTypeQNames); - Iterator parentUUIDIter = getParentUUIDs().iterator(); - List nParentUUIDs = new ArrayList(); - while (parentUUIDIter.hasNext()) { - ORMNodeParent curNodeParent = (ORMNodeParent) parentUUIDIter.next(); - nParentUUIDs.add(curNodeParent.getParentUUID()); - } - state.setParentUUIDs(nParentUUIDs); - } - - public boolean equals(Object obj) { - if (!(obj instanceof ORMNodeState)) { - return false; - } - ORMNodeState right = (ORMNodeState) obj; - if (getUuid().equals(right.getUuid())) { - return true; - } else { - return false; - } - } - - public int hashCode() { - return getUuid().hashCode(); - } -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMPropertyEntry.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMPropertyEntry.java deleted file mode 100644 index 86f26356b6b..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMPropertyEntry.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm; - -import java.io.Serializable; - -import org.apache.jackrabbit.core.state.NodeState.PropertyEntry; - -/** - *

This class represents a single entry of a property in a node. This - * class only stores the name of the property, the actual property and it's - * values are stored in the ORMPropertyState class.

- */ -public class ORMPropertyEntry - implements Serializable { - private String parentUUID; - private String name; - private Integer dbId; - private ORMNodeState parent; - public ORMPropertyEntry() { - } - - public ORMPropertyEntry(ORMNodeState parent, PropertyEntry propertyEntry, String parentUUID) { - this.parent = parent; - this.parentUUID = parentUUID; - this.name = propertyEntry.getName().toString(); - } - - public void setParentUUID(String parentUUID) { - this.parentUUID = parentUUID; - } - - public void setName(String name) { - this.name = name; - } - - public void setDbId(Integer dbId) { - - this.dbId = dbId; - } - - public void setParent(ORMNodeState parent) { - this.parent = parent; - } - - public String getParentUUID() { - return parentUUID; - } - - public String getName() { - return name; - } - - public Integer getDbId() { - - return dbId; - } - - public ORMNodeState getParent() { - return parent; - } - - public boolean equals(Object obj) { - if (!(obj instanceof ORMPropertyEntry)) { - return false; - } - ORMPropertyEntry right = (ORMPropertyEntry) obj; - if (getName().equals(right.getName())) { - return true; - } else { - return false; - } - } - - public int hashCode() { - return (getName()).hashCode(); - } - -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMPropertyState.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMPropertyState.java deleted file mode 100644 index 16bdab6bb10..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ORMPropertyState.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm; - -import java.io.Serializable; - -import org.apache.jackrabbit.core.InternalValue; -import org.apache.jackrabbit.core.ItemId; -import org.apache.jackrabbit.core.PropertyId; -import org.apache.jackrabbit.core.nodetype.PropDefId; -import org.apache.jackrabbit.core.state.ItemStateException; -import org.apache.jackrabbit.core.state.PropertyState; -import org.apache.jackrabbit.core.state.orm.ojb.ValuesToStringFieldConversion; -import org.apache.ojb.broker.accesslayer.conversions.ConversionException; -import javax.jcr.PropertyType; - -/** - *

This class represents a property state in an ORM-compatible format.

- */ -public class ORMPropertyState implements Serializable { - private String values; - private Integer type; - private String definitionId; - private Boolean multiValued; - private String itemId; - private String name; - private String parentUUID; - - public ORMPropertyState() { - } - - public ORMPropertyState(ItemId id) throws ItemStateException { - if (id instanceof PropertyId) { - PropertyId propId = (PropertyId) id; - this.itemId = propId.toString(); - name = propId.getName().toString(); - parentUUID = propId.getParentUUID(); - } else { - throw new ItemStateException("PropertyId expected, instead got " + id.getClass()); - } - } - - public ORMPropertyState(PropertyState state) { - fromPersistentPropertyState(state); - } - - public void fromPersistentPropertyState(PropertyState state) throws - ConversionException { - this.itemId = state.getId().toString(); - name = state.getName().toString(); - parentUUID = state.getParentUUID(); - values = (String) new ValuesToStringFieldConversion().javaToSql(state.getValues()); - type = new Integer(state.getType()); - if (state.getDefinitionId() != null) { - definitionId = state.getDefinitionId().toString(); - } - multiValued = new Boolean(state.isMultiValued()); - } - - public void setName(String name) { - this.name = name; - } - - public void setValues(String values) { - this.values = values; - } - - public void setParentUUID(String parentUUID) { - this.parentUUID = parentUUID; - } - - public void setType(Integer type) { - this.type = type; - } - - public void setDefinitionId(String definitionId) { - - this.definitionId = definitionId; - } - - public void setMultiValued(Boolean multiValued) { - this.multiValued = multiValued; - } - - public void setItemId(String itemId) { - - this.itemId = itemId; - } - - public String getName() { - return name; - } - - public String getValues() { - return values; - } - - public String getParentUUID() { - return parentUUID; - } - - public Integer getType() { - return type; - } - - public String getDefinitionId() { - - return definitionId; - } - - public Boolean getMultiValued() { - return multiValued; - } - - public String getItemId() { - - return itemId; - } - - public void toPersistentPropertyState(PropertyState state) { - if (getDefinitionId() != null) { - state.setDefinitionId(PropDefId.valueOf(getDefinitionId())); - } - if (getType() != null) { - state.setType(getType().intValue()); - } - if (getType().intValue() != PropertyType.BINARY) { - ValuesToStringFieldConversion vts = new - ValuesToStringFieldConversion(getType().intValue()); - InternalValue[] values = (InternalValue[]) vts.sqlToJava(getValues()); - if (values.length > 1) { - state.setMultiValued(true); - } else { - state.setMultiValued(multiValued.booleanValue()); - } - state.setValues(values); - } else { - state.setMultiValued(multiValued.booleanValue()); - } - } - - public boolean equals(Object obj) { - if (!(obj instanceof ORMPropertyState)) { - return false; - } - ORMPropertyState right = (ORMPropertyState) obj; - if (itemId.equals(right.getItemId())) { - return true; - } else { - return false; - } - } - - public int hashCode() { - return getItemId().hashCode(); - } -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/hibernate/HibernateNodeState.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/hibernate/HibernateNodeState.java deleted file mode 100644 index 61f4f47f74d..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/hibernate/HibernateNodeState.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm.hibernate; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import org.apache.jackrabbit.core.ItemId; -import org.apache.jackrabbit.core.state.NodeState; -import org.apache.jackrabbit.core.state.orm.ORMNodeState; - -/** - *

Hibernate-specific node state class. This is necessary because - * in this implementation we use a set to represent lists.

- */ -public class HibernateNodeState extends ORMNodeState { - - private Set setChildNodeEntries = new HashSet(); - private Set setParentUUIDs = new HashSet(); - private Set setMixinTypeNames = new HashSet(); - private Set setPropertyEntries = new HashSet(); - - public HibernateNodeState() { - } - public HibernateNodeState(ItemId id) { - super(id); - } - public HibernateNodeState(NodeState state) { - super(); - fromPersistentNodeState(state); - } - public Collection getChildNodeEntries() { - return setChildNodeEntries; - } - public void setChildNodeEntries(Collection childNodeEntries) { - this.setChildNodeEntries.clear(); - this.setChildNodeEntries.addAll(childNodeEntries); - } - - public Collection getPropertyEntries() { - return setPropertyEntries; - } - - public Collection getMixinTypeNames() { - return setMixinTypeNames; - } - - public Collection getParentUUIDs() { - return setParentUUIDs; - } - - public void setPropertyEntries(Collection propertyEntries) { - this.setPropertyEntries.clear(); - this.setPropertyEntries.addAll(propertyEntries); - } - - public void setMixinTypeNames(Collection mixinTypeNames) { - this.setMixinTypeNames.clear(); - this.setMixinTypeNames.addAll(mixinTypeNames); - } - - public void setParentUUIDs(Collection parentUUIDs) { - this.setParentUUIDs.clear(); - this.setParentUUIDs.addAll(parentUUIDs); - } - - public Set getSetChildNodeEntries() { - return setChildNodeEntries; - } - - public void setSetChildNodeEntries(Set setChildNodeEntries) { - this.setChildNodeEntries = setChildNodeEntries; - } - - public Set getSetPropertyEntries() { - return setPropertyEntries; - } - - public void setSetPropertyEntries(Set setPropertyEntries) { - this.setPropertyEntries = setPropertyEntries; - } - - public Set getSetMixinTypeNames() { - return setMixinTypeNames; - } - - public void setSetMixinTypeNames(Set setMixinTypeNames) { - this.setMixinTypeNames = setMixinTypeNames; - } - - public Set getSetParentUUIDs() { - return setParentUUIDs; - } - - public void setSetParentUUIDs(Set setParentUUIDs) { - this.setParentUUIDs = setParentUUIDs; - } - -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/hibernate/HibernatePersistenceManager.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/hibernate/HibernatePersistenceManager.java deleted file mode 100644 index 2cad00aeece..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/hibernate/HibernatePersistenceManager.java +++ /dev/null @@ -1,703 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm.hibernate; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import javax.jcr.PropertyType; - -import org.apache.jackrabbit.core.BLOBFileValue; -import org.apache.jackrabbit.core.InternalValue; -import org.apache.jackrabbit.core.NodeId; -import org.apache.jackrabbit.core.PropertyId; -import org.apache.jackrabbit.core.QName; -import org.apache.jackrabbit.core.state.AbstractPersistenceManager; -import org.apache.jackrabbit.core.state.ItemState; -import org.apache.jackrabbit.core.state.ItemStateException; -import org.apache.jackrabbit.core.state.NoSuchItemStateException; -import org.apache.jackrabbit.core.state.NodeReferences; -import org.apache.jackrabbit.core.state.NodeReferencesId; -import org.apache.jackrabbit.core.state.NodeState; -import org.apache.jackrabbit.core.state.PMContext; -import org.apache.jackrabbit.core.state.PropertyState; -import org.apache.jackrabbit.core.state.orm.ORMBlobValue; -import org.apache.jackrabbit.core.state.orm.ORMNodeReference; -import org.apache.jackrabbit.core.state.orm.ORMPropertyState; -import org.apache.log4j.Logger; -import net.sf.hibernate.Hibernate; -import net.sf.hibernate.HibernateException; -import net.sf.hibernate.ObjectNotFoundException; -import net.sf.hibernate.Session; -import net.sf.hibernate.SessionFactory; -import net.sf.hibernate.Transaction; -import net.sf.hibernate.cfg.Configuration; -import net.sf.hibernate.type.Type; - -/** - * Hibernate implementation of a Jackrabbit persistence manager. - */ -public class HibernatePersistenceManager extends AbstractPersistenceManager { - - private static Logger log = Logger.getLogger(HibernatePersistenceManager.class); - - private SessionFactory sessionFactory; - - public HibernatePersistenceManager() { - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#init - */ - public void init(PMContext context) throws Exception { - try { - // Create the SessionFactory - sessionFactory = new Configuration().configure(). - buildSessionFactory(); - } catch (Throwable ex) { - log.error("Initial SessionFactory creation failed.", ex); - throw new ExceptionInInitializerError(ex); - } - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#close - */ - public void close() throws Exception { - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#load(NodeId) - */ - public NodeState load(NodeId nodeId) throws NoSuchItemStateException, - ItemStateException { - log.debug("Request for " + nodeId.getUUID()); - - NodeState state = null; - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - List nodeList = session.find( - "from org.apache.jackrabbit.core.state.orm.hibernate.HibernateNodeState as node WHERE " + - "node.uuid = ?", - new Object[] {nodeId.getUUID()}, - new Type[] {Hibernate.STRING}); - - tx.commit(); - if (nodeList.size() != 1) { - throw new NoSuchItemStateException("Couldn't find unique node " + - nodeId.getUUID() + ", found " + - nodeList.size() + - " results instead"); - } - HibernateNodeState result = (HibernateNodeState) nodeList.get(0); - state = createNew(nodeId); - result.toPersistentNodeState(state); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException("Error loading " + nodeId.getUUID(), - he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - return state; - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#load(PropertyId) - */ - public PropertyState load(PropertyId propId) throws - NoSuchItemStateException, ItemStateException { - - PropertyState state = null; - Session session = null; - try { - session = sessionFactory.openSession(); - Transaction tx = session.beginTransaction(); - ORMPropertyState propState = null; - try { - - List propertyList = session.find( - "from org.apache.jackrabbit.core.state.orm.ORMPropertyState as prop WHERE " + - "prop.parentUUID = ? and prop.name = ?", - new Object[] {propId.getParentUUID(), - propId.getName().toString()}, - new Type[] {Hibernate.STRING, Hibernate.STRING}); - - tx.commit(); - if (propertyList.size() != 1) { - throw new NoSuchItemStateException( - "Couldn't find unique property " + propId + ", found " + - propertyList.size() + " results instead"); - } - propState = (ORMPropertyState) propertyList.get(0); - state = createNew(propId); - propState.toPersistentPropertyState(state); - if (propState.getType().intValue() == PropertyType.BINARY) { - // we must now load the binary values. - ArrayList internalValueList = new ArrayList(); - List blobValueList = session.find( - "from org.apache.jackrabbit.core.state.orm.ORMBlobValue as bv WHERE " + - "bv.parentUUID = ? and bv.propertyName = ?", - new Object[] {propId.getParentUUID(), - propId.getName().toString()}, - new Type[] {Hibernate.STRING, Hibernate.STRING}); - - Iterator resultIter = blobValueList.iterator(); - while (resultIter.hasNext()) { - ORMBlobValue ormBlobValue = (ORMBlobValue) resultIter. - next(); - ByteArrayInputStream in = new ByteArrayInputStream( - ormBlobValue.getBlobValue()); - try { - BLOBFileValue blobValue = new BLOBFileValue(in); - internalValueList.add(blobValue); - } catch (Throwable t) { - throw new ItemStateException( - "Error while trying to load blob value", t); - } - } - state.setValues( (InternalValue[]) internalValueList. - toArray(new - InternalValue[internalValueList. - size()])); - } - } catch (ObjectNotFoundException onfe) { - throw new NoSuchItemStateException("Couldn't find " + propId, - onfe); - } - } catch (HibernateException he) { - throw new ItemStateException("Error loading " + propId, he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - return state; - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#load(NodeReferencesId) - */ - public NodeReferences load(NodeReferencesId targetId) throws - NoSuchItemStateException, ItemStateException { - log.debug("Loading node references for targetId=" + - targetId.toString()); - NodeReferences refs = null; - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - Iterator nodeRefIter = session.iterate("from org.apache.jackrabbit.core.state.orm.ORMNodeReference as nf where nf.targetId='" + - targetId.toString() + - "'"); - refs = new NodeReferences(targetId); - while (nodeRefIter.hasNext()) { - ORMNodeReference curNodeReference = (ORMNodeReference) - nodeRefIter. - next(); - refs.addReference(new PropertyId(curNodeReference. - getPropertyParentUUID(), - QName. - valueOf(curNodeReference. - getPropertyName()))); - } - tx.commit(); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - log.error("Error while loading node reference for targetId=" + - targetId.toString(), he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - return refs; - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#exists(NodeId) - */ - public boolean exists(NodeId id) throws ItemStateException { - HibernateNodeState result = null; - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - List nodeList = session.find( - "from org.apache.jackrabbit.core.state.orm.hibernate.HibernateNodeState as node WHERE " + - "node.uuid = ?", - new Object[] {id.toString()}, - new Type[] {Hibernate.STRING}); - - tx.commit(); - if (nodeList.size() < 1) { - return false; - } else { - if (nodeList.size() > 1) { - log.warn("Node " + id + - " exists more than once in database !"); - } - return true; - } - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException("Error loading " + id, he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#exists(PropertyId) - */ - public boolean exists(PropertyId id) throws ItemStateException { - boolean result = false; - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - ORMPropertyState propState = null; - PropertyId propId = (PropertyId) id; - List propertyList = session.find( - "from org.apache.jackrabbit.core.state.orm.ORMPropertyState as prop WHERE " + - "prop.parentUUID = ? and prop.name = ?", - new Object[] {propId.getParentUUID(), - propId.getName().toString()}, - new Type[] {Hibernate.STRING, Hibernate.STRING}); - - tx.commit(); - if (propertyList.size() < 1) { - return false; - } else { - if (propertyList.size() > 1) { - log.warn("Property " + id + - " exists more than once in database !"); - } - return true; - } - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException("Error loading " + id, he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#exists(NodeReferencesId) - */ - public boolean exists(NodeReferencesId targetId) throws ItemStateException { - boolean result = false; - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - Iterator nodeRefIter = session.iterate("from org.apache.jackrabbit.core.state.orm.ORMNodeReference as nf where nf.targetId='" + - targetId.toString() + - "'"); - NodeReferences refs = new NodeReferences(targetId); - if (nodeRefIter.hasNext()) { - result = true; - } - tx.commit(); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException( - "Error while testing reference existence for targetId=" + - targetId, he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - return result; - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#store(NodeState) - */ - public void store(NodeState state) throws ItemStateException { - log.debug("Request to store " + state.getId()); - boolean isUpdate = true; - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - HibernateNodeState nodeState = new HibernateNodeState(state); - if (state.getStatus() == ItemState.STATUS_NEW) { - session.save(nodeState); - } else { - session.update(nodeState); - } - tx.commit(); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException("Error saving " + state.getId(), he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#store(PropertyState) - */ - public void store(PropertyState state) throws ItemStateException { - log.debug("Request to store " + state.getId()); - boolean isUpdate = true; - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - ORMPropertyState propState = new ORMPropertyState(state); - - InternalValue[] values = state.getValues(); - if (values != null) { - for (int i = 0; i < values.length; i++) { - - // first we delete any existing blob values (this is faster - // than trying to load and update each value seperately) - session.delete("from org.apache.jackrabbit.core.state.orm.ORMBlobValue as bv where bv.parentUUID=? AND bv.propertyName=?", - new Object[] {state.getParentUUID(), - state.getName().toString()}, - new Type[] {Hibernate.STRING, - Hibernate.STRING}); - - InternalValue val = values[i]; - if (val != null) { - if (state.getType() == PropertyType.BINARY) { - ORMBlobValue ormBlobValue = null; - ormBlobValue = new ORMBlobValue(); - ormBlobValue.setParentUUID(state.getParentUUID()); - ormBlobValue.setPropertyName(state.getName(). - toString()); - ormBlobValue.setIndex(new Integer(i)); - BLOBFileValue blobVal = (BLOBFileValue) val. - internalValue(); - propState.setValues(""); - ByteArrayOutputStream out = new - ByteArrayOutputStream(); - try { - blobVal.spool(out); - } catch (Throwable t) { - tx.rollback(); - session.close(); - throw new ItemStateException(t.getMessage(), t); - } - ormBlobValue.setSize(new Long(blobVal.getLength())); - ormBlobValue.setBlobValue(out.toByteArray()); - session.save(ormBlobValue); - } - } - } - } - - if (state.getStatus() == ItemState.STATUS_NEW) { - session.save(propState); - } else { - session.update(propState); - } - tx.commit(); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException("Error saving " + state.getId(), he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#store(NodeReferences) - */ - public void store(NodeReferences refs) throws ItemStateException { - Iterator nodeRefPropIdIter = refs.getReferences().iterator(); - log.debug("Request to store node references for targetId=" + - refs.getTargetId()); - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - int i = 0; - while (nodeRefPropIdIter.hasNext()) { - PropertyId curPropertyId = (PropertyId) nodeRefPropIdIter.next(); - ORMNodeReference curNodeReference = new ORMNodeReference(refs. - getTargetId().toString(), curPropertyId.getParentUUID(), - curPropertyId.getName().toString()); - session.save(curNodeReference); - i++; - if (i % 20 == 0) { - session.flush(); - session.clear(); - } - } - tx.commit(); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException( - "Error storing node references for targetId=" + - refs.getTargetId(), he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#destroy(NodeState) - */ - public void destroy(NodeState state) throws ItemStateException { - log.debug("Deleting node " + state.getUUID()); - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - HibernateNodeState nodeState = null; - try { - List nodeList = session.find( - "from org.apache.jackrabbit.core.state.orm.hibernate.HibernateNodeState as node WHERE " + - "node.uuid = ?", - new Object[] {state.getId().toString()}, - new Type[] {Hibernate.STRING}); - - if (nodeList.size() != 1) { - } else { - nodeState = (HibernateNodeState) nodeList.get(0); - session.delete(nodeState); - } - } catch (ObjectNotFoundException onfe) { - } - tx.commit(); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException("Error deleting " + state.getId(), he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#destroy(PropertyState) - */ - public void destroy(PropertyState state) throws ItemStateException { - log.debug("Deleting property " + state.getId()); - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - ORMPropertyState propState = null; - try { - List propertyList = session.find( - "from org.apache.jackrabbit.core.state.orm.ORMPropertyState as prop WHERE " + - "prop.itemId = ?", - new Object[] {state.getId().toString()}, - new Type[] {Hibernate.STRING}); - - if (propertyList.size() != 1) { - } else { - propState = (ORMPropertyState) propertyList.get(0); - session.delete(propState); - if (state.getType() == PropertyType.BINARY) { - session.delete("from org.apache.jackrabbit.core.state.orm.ORMBlobValue as bv where bv.parentUUID=? AND bv.propertyName=?", - new Object[] {state.getParentUUID(), - state.getName().toString()}, - new Type[] {Hibernate.STRING, - Hibernate.STRING}); - } - } - } catch (ObjectNotFoundException onfe) { - } - tx.commit(); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException("Error deleting " + state.getId(), he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#destroy(NodeReferences) - */ - public void destroy(NodeReferences refs) throws ItemStateException { - log.debug("Deleting node refences for targetId=" + - refs.getTargetId().toString()); - Session session = null; - Transaction tx = null; - try { - session = sessionFactory.openSession(); - tx = session.beginTransaction(); - session.delete("from org.apache.jackrabbit.core.state.orm.ORMNodeReference as nf where nf.targetId='" + - refs.getTargetId().toString() + - "'"); - refs.clearAllReferences(); - tx.commit(); - } catch (HibernateException he) { - try { - if (tx != null) - tx.rollback(); - } catch (HibernateException he2) { - log.error("Error while rolling back transaction", he2); - } - throw new ItemStateException( - "Error deleting node references for targetId=" + - refs.getTargetId().toString(), he); - } finally { - if (session != null) { - try { - session.close(); - } catch (HibernateException he) { - throw new ItemStateException( - "Error while closing hibernate session", he); - } - } - } - } - -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/OJBNodeState.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/OJBNodeState.java deleted file mode 100644 index 477eb4f053f..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/OJBNodeState.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm.ojb; - -import java.io.Serializable; -import java.util.Collection; -import java.util.List; - -import org.apache.jackrabbit.core.ItemId; -import org.apache.jackrabbit.core.state.NodeState; -import org.apache.jackrabbit.core.state.orm.ORMNodeState; -import org.apache.ojb.broker.util.collections.RemovalAwareList; - -/** - *

OJB specific node state. In order to properly track list - * modifications, we use an OJB specific list implementation.

- */ -public class OJBNodeState extends ORMNodeState implements Serializable { - - private List awareChildNodeEntries = new RemovalAwareList(); - private List awarePropertyEntries = new RemovalAwareList(); - private List awareMixinTypeNames = new RemovalAwareList(); - private List awareParentUUIDs = new RemovalAwareList(); - - public OJBNodeState() { - } - public OJBNodeState(ItemId id) { - super(id); - } - public OJBNodeState(NodeState state) { - super(); - fromPersistentNodeState(state); - } - public Collection getChildNodeEntries() { - return awareChildNodeEntries; - } - public void setChildNodeEntries(Collection childNodeEntries) { - this.awareChildNodeEntries.clear(); - this.awareChildNodeEntries.addAll(childNodeEntries); - } - - public List getAwareChildNodeEntries() { - return awareChildNodeEntries; - } - - public void setAwareChildNodeEntries(List awareChildNodeEntries) { - this.awareChildNodeEntries = awareChildNodeEntries; - } - - public Collection getPropertyEntries() { - return awarePropertyEntries; - } - - public Collection getMixinTypeNames() { - return awareMixinTypeNames; - } - - public Collection getParentUUIDs() { - return awareParentUUIDs; - } - - public void setPropertyEntries(Collection propertyEntries) { - this.awarePropertyEntries.clear(); - this.awarePropertyEntries.addAll(propertyEntries); - } - - public void setMixinTypeNames(Collection mixinTypeNames) { - this.awareMixinTypeNames.clear(); - this.awareMixinTypeNames.addAll(mixinTypeNames); - } - - public void setParentUUIDs(Collection parentUUIDs) { - this.awareParentUUIDs.clear(); - this.awareParentUUIDs.addAll(parentUUIDs); - } - -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/OJBPersistenceManager.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/OJBPersistenceManager.java deleted file mode 100644 index a2014968472..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/OJBPersistenceManager.java +++ /dev/null @@ -1,570 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm.ojb; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.Iterator; - -import javax.jcr.PropertyType; - -import org.apache.jackrabbit.core.BLOBFileValue; -import org.apache.jackrabbit.core.InternalValue; -import org.apache.jackrabbit.core.NodeId; -import org.apache.jackrabbit.core.PropertyId; -import org.apache.jackrabbit.core.QName; -import org.apache.jackrabbit.core.state.ChangeLog; -import org.apache.jackrabbit.core.state.ItemState; -import org.apache.jackrabbit.core.state.ItemStateException; -import org.apache.jackrabbit.core.state.NoSuchItemStateException; -import org.apache.jackrabbit.core.state.NodeReferences; -import org.apache.jackrabbit.core.state.NodeReferencesId; -import org.apache.jackrabbit.core.state.NodeState; -import org.apache.jackrabbit.core.state.PMContext; -import org.apache.jackrabbit.core.state.PersistenceManager; -import org.apache.jackrabbit.core.state.PropertyState; -import org.apache.jackrabbit.core.state.orm.ORMBlobValue; -import org.apache.jackrabbit.core.state.orm.ORMNodeReference; -import org.apache.jackrabbit.core.state.orm.ORMPropertyState; -import org.apache.log4j.Logger; -import org.apache.ojb.broker.PersistenceBroker; -import org.apache.ojb.broker.PersistenceBrokerException; -import org.apache.ojb.broker.PersistenceBrokerFactory; -import org.apache.ojb.broker.query.Criteria; -import org.apache.ojb.broker.query.QueryByCriteria; -import org.apache.ojb.broker.query.QueryByIdentity; - -/** - * OJB implementation of a Jackrabbit persistence manager. - */ -public class OJBPersistenceManager implements PersistenceManager -{ - - private static Logger log = Logger.getLogger(OJBPersistenceManager.class); - - private boolean initialized = false; - - public OJBPersistenceManager() - { - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#init - */ - public void init(PMContext context) throws Exception - { - // FIXME: A config param to set the broker name would be handy - if (initialized) - { - throw new IllegalStateException("already initialized"); - } - initialized = true; - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#close - */ - public void close() throws Exception - { - // Nothing to do - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#load(NodeId) - */ - public NodeState load(NodeId nodeId) throws NoSuchItemStateException, - ItemStateException - { - PersistenceBroker broker = PersistenceBrokerFactory - .defaultPersistenceBroker(); - try - { - log.debug("Request for " + nodeId.getUUID()); - OJBNodeState nodeState = new OJBNodeState(); - nodeState.setUuid(nodeId.getUUID()); - QueryByIdentity query = new QueryByIdentity(nodeState); - OJBNodeState result = (OJBNodeState) broker.getObjectByQuery(query); - if (result == null) - { - throw new NoSuchItemStateException(nodeId.getUUID()); - } - NodeState state = createNew(nodeId); - result.toPersistentNodeState(state); - return state; - } catch (PersistenceBrokerException e) - { - throw new ItemStateException(e); - } finally - { - if (broker != null) - broker.close(); - } - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#load(PropertyId) - */ - public PropertyState load(PropertyId propId) - throws NoSuchItemStateException, ItemStateException - { - PersistenceBroker broker = PersistenceBrokerFactory - .defaultPersistenceBroker(); - try - { - log.debug("Request for property " + propId); - ORMPropertyState propState = new ORMPropertyState(propId); - QueryByIdentity query = new QueryByIdentity(propState); - PropertyState state = createNew(propId); - ORMPropertyState result = (ORMPropertyState) broker - .getObjectByQuery(query); - if (result == null) - { - throw new NoSuchItemStateException("Couldn't find property " - + propId); - } - result.toPersistentPropertyState(state); - if (result.getType().intValue() == PropertyType.BINARY) - { - // we must now load the binary values. - ArrayList internalValueList = new ArrayList(); - Criteria criteria = new Criteria(); - criteria.addEqualTo("parentUUID", state.getParentUUID()); - criteria.addEqualTo("propertyName", state.getName().toString()); - QueryByCriteria blobQuery = new QueryByCriteria( - ORMBlobValue.class, criteria); - Iterator resultIter = broker.getCollectionByQuery(blobQuery) - .iterator(); - while (resultIter.hasNext()) - { - ORMBlobValue ormBlobValue = (ORMBlobValue) resultIter - .next(); - ByteArrayInputStream in = new ByteArrayInputStream( - ormBlobValue.getBlobValue()); - try - { - internalValueList.add(InternalValue.create(in)); - } catch (Throwable t) - { - throw new ItemStateException( - "Error while trying to load blob value", t); - } - } - state.setValues((InternalValue[]) internalValueList - .toArray(new InternalValue[internalValueList.size()])); - } - return state; - } catch (PersistenceBrokerException e) - { - throw new ItemStateException(e); - } finally - { - if (broker != null) - broker.close(); - } - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#load(NodeReferencesId) - */ - public NodeReferences load(NodeReferencesId targetId) - throws NoSuchItemStateException, ItemStateException - { - PersistenceBroker broker = PersistenceBrokerFactory - .defaultPersistenceBroker(); - try - { - ORMNodeReference nodeRef = new ORMNodeReference(); - nodeRef.setTargetId(targetId.toString()); - QueryByCriteria query = new QueryByCriteria(nodeRef); - Iterator resultIter = broker.getCollectionByQuery(query).iterator(); - NodeReferences refs = new NodeReferences(targetId); - while (resultIter.hasNext()) - { - ORMNodeReference curNodeReference = (ORMNodeReference) resultIter - .next(); - refs.addReference(new PropertyId(curNodeReference - .getPropertyParentUUID(), QName - .valueOf(curNodeReference.getPropertyName()))); - } - return refs; - } catch (PersistenceBrokerException e) - { - throw new ItemStateException(e); - } finally - { - if (broker != null) - broker.close(); - } - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#exists(NodeId) - */ - public boolean exists(NodeId id) throws ItemStateException - { - PersistenceBroker broker = PersistenceBrokerFactory - .defaultPersistenceBroker(); - try - { - OJBNodeState nodeState = new OJBNodeState(id); - QueryByIdentity query = new QueryByIdentity(nodeState); - OJBNodeState result = (OJBNodeState) broker.getObjectByQuery(query); - if (result == null) - { - return false; - } else - { - return true; - } - } catch (PersistenceBrokerException e) - { - throw new ItemStateException(e); - } finally - { - if (broker != null) - broker.close(); - } - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#exists(PropertyId) - */ - public boolean exists(PropertyId id) throws ItemStateException - { - PersistenceBroker broker = PersistenceBrokerFactory - .defaultPersistenceBroker(); - try - { - ORMPropertyState propState = new ORMPropertyState(id); - // QueryByCriteria query = new QueryByCriteria(propState); - QueryByIdentity query = new QueryByIdentity(propState); - ORMPropertyState result = (ORMPropertyState) broker - .getObjectByQuery(query); - if (result == null) - { - return false; - } else - { - return true; - } - } catch (PersistenceBrokerException e) - { - throw new ItemStateException(e); - } finally - { - if (broker != null) - broker.close(); - } - } - - /** - * @see org.apache.jackrabbit.core.state.PersistenceManager#exists(NodeReferencesId) - */ - public boolean exists(NodeReferencesId targetId) throws ItemStateException - { - PersistenceBroker broker = PersistenceBrokerFactory - .defaultPersistenceBroker(); - try - { - - ORMNodeReference nodeRef = new ORMNodeReference(); - nodeRef.setTargetId(targetId.toString()); - QueryByCriteria query = new QueryByCriteria(nodeRef); - Iterator resultIter = broker.getCollectionByQuery(query).iterator(); - NodeReferences refs = new NodeReferences(targetId); - if (resultIter.hasNext()) - { - return true; - } - return false; - } catch (PersistenceBrokerException e) - { - throw new ItemStateException(e); - } finally - { - if (broker != null) - broker.close(); - } - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#store(NodeState) - */ - private void store(NodeState state, PersistenceBroker broker) - throws ItemStateException - { - log.debug("Request to store node " + state.getId()); - OJBNodeState nodeState = new OJBNodeState(state.getId()); - QueryByIdentity query = new QueryByIdentity(nodeState); - OJBNodeState result = (OJBNodeState) broker.getObjectByQuery(query); - if (result == null) - { - result = new OJBNodeState(); - } - result.fromPersistentNodeState(state); - broker.store(result); - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#store(PropertyState) - */ - private void store(PropertyState state, PersistenceBroker broker) - throws ItemStateException - { - log.debug("Request to store property " + state.getId()); - ORMPropertyState propState = new ORMPropertyState(state.getId()); - QueryByIdentity query = new QueryByIdentity(propState); - ORMPropertyState result = (ORMPropertyState) broker - .getObjectByQuery(query); - if (result == null) - { - result = new ORMPropertyState(); - } - result.fromPersistentPropertyState(state); - - InternalValue[] values = state.getValues(); - if (values != null) - { - for (int i = 0; i < values.length; i++) - { - InternalValue val = values[i]; - if (val != null) - { - if (state.getType() == PropertyType.BINARY) - { - Criteria criteria = new Criteria(); - criteria - .addEqualTo("parentUUID", state.getParentUUID()); - criteria.addEqualTo("propertyName", state.getName() - .toString()); - criteria.addEqualTo("index", new Integer(i)); - QueryByCriteria blobQuery = new QueryByCriteria( - ORMBlobValue.class, criteria); - Iterator resultIter = broker.getCollectionByQuery( - blobQuery).iterator(); - ORMBlobValue ormBlobValue = null; - if (resultIter.hasNext()) - { - ormBlobValue = (ORMBlobValue) resultIter.next(); - } else - { - ormBlobValue = new ORMBlobValue(); - ormBlobValue.setParentUUID(state.getParentUUID()); - ormBlobValue.setPropertyName(state.getName() - .toString()); - ormBlobValue.setIndex(new Integer(i)); - } - BLOBFileValue blobVal = (BLOBFileValue) val - .internalValue(); - result.setValues(""); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try - { - blobVal.spool(out); - } catch (Throwable t) - { - // The caller is responsible of aborting the - // transaction - // broker.abortTransaction(); - throw new ItemStateException(t.getMessage(), t); - } - ormBlobValue.setSize(new Long(blobVal.getLength())); - ormBlobValue.setBlobValue(out.toByteArray()); - broker.store(ormBlobValue); - } - } - } - } - broker.store(result); - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#store(NodeReferences) - */ - private void store(NodeReferences refs, PersistenceBroker broker) - throws ItemStateException - { - // destroy all the references before saving - destroy(refs, broker); - - Iterator nodeRefPropIdIter = refs.getReferences().iterator(); - while (nodeRefPropIdIter.hasNext()) - { - PropertyId curPropertyId = (PropertyId) nodeRefPropIdIter.next(); - ORMNodeReference curNodeReference = new ORMNodeReference(refs - .getTargetId().toString(), curPropertyId.getParentUUID(), - curPropertyId.getName().toString()); - broker.store(curNodeReference); - } - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#destroy(NodeState) - */ - private void destroy(NodeState state, PersistenceBroker broker) - throws ItemStateException - { - log.debug("Deleting node " + state.getId()); - - // Destroy node - OJBNodeState nodeState = new OJBNodeState(state.getId()); - QueryByIdentity query = new QueryByIdentity(nodeState); - OJBNodeState result = (OJBNodeState) broker.getObjectByQuery(query); - if (result == null) - { - result = new OJBNodeState(); - } - broker.delete(result); - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#destroy(PropertyState) - */ - private void destroy(PropertyState state, PersistenceBroker broker) - throws ItemStateException - { - ORMPropertyState propState = new ORMPropertyState(state); - broker.delete(propState); - if (state.getType() == PropertyType.BINARY) - { - Criteria criteria = new Criteria(); - criteria.addEqualTo("parentUUID", state.getParentUUID()); - criteria.addEqualTo("propertyName", state.getName().toString()); - QueryByCriteria blobQuery = new QueryByCriteria(ORMBlobValue.class, - criteria); - broker.deleteByQuery(blobQuery); - } - } - - /** - * @see org.apache.jackrabbit.core.state.AbstractPersistenceManager#destroy(NodeReferences) - */ - private void destroy(NodeReferences refs, PersistenceBroker broker) - throws ItemStateException - { - ORMNodeReference nodeRef = new ORMNodeReference(); - nodeRef.setTargetId(refs.getTargetId().toString()); - QueryByCriteria query = new QueryByCriteria(nodeRef); - Iterator resultIter = broker.getCollectionByQuery(query).iterator(); - while (resultIter.hasNext()) - { - ORMNodeReference curNodeReference = (ORMNodeReference) resultIter - .next(); - broker.delete(curNodeReference); - } - } - - /** - * @see PersistenceManager#createNew - */ - public NodeState createNew(NodeId id) - { - return new NodeState(id.getUUID(), null, null, NodeState.STATUS_NEW, - false); - } - - /** - * @see PersistenceManager#createNew - */ - public PropertyState createNew(PropertyId id) - { - return new PropertyState(id.getName(), id.getParentUUID(), - PropertyState.STATUS_NEW, false); - } - - /** - * @see PersistenceManager#store(ChangeLog) - * - * This method ensures that changes are either written completely to the - * underlying persistence layer, or not at all. - */ - public void store(ChangeLog changeLog) throws ItemStateException - { - PersistenceBroker broker = PersistenceBrokerFactory - .defaultPersistenceBroker(); - try - { - broker.beginTransaction() ; - Iterator iter = changeLog.deletedStates(); - while (iter.hasNext()) - { - ItemState state = (ItemState) iter.next(); - if (state.isNode()) - { - destroy((NodeState) state, broker); - } else - { - destroy((PropertyState) state, broker); - } - } - iter = changeLog.addedStates(); - while (iter.hasNext()) - { - ItemState state = (ItemState) iter.next(); - if (state.isNode()) - { - store((NodeState) state, broker); - } else - { - store((PropertyState) state, broker); - } - } - iter = changeLog.modifiedStates(); - while (iter.hasNext()) - { - ItemState state = (ItemState) iter.next(); - if (state.isNode()) - { - store((NodeState) state, broker); - } else - { - store((PropertyState) state, broker); - } - } - iter = changeLog.modifiedRefs(); - while (iter.hasNext()) - { - NodeReferences refs = (NodeReferences) iter.next(); - if (refs.hasReferences()) - { - store(refs, broker); - } else - { - destroy(refs, broker); - } - } - broker.commitTransaction() ; - } catch (ItemStateException e) - { - if (broker != null) - broker.abortTransaction(); - throw e; - } catch (PersistenceBrokerException e) - { - if (broker != null) - broker.abortTransaction(); - throw new ItemStateException("Unable to store", e); - } finally - { - if (broker != null) - broker.close(); - } - - } - -} diff --git a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/ValuesToStringFieldConversion.java b/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/ValuesToStringFieldConversion.java deleted file mode 100644 index 93594c2e8be..00000000000 --- a/contrib/orm-persistence/src/java/org/apache/jackrabbit/core/state/orm/ojb/ValuesToStringFieldConversion.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.state.orm.ojb; - -import java.util.ArrayList; -import java.util.StringTokenizer; - -import org.apache.jackrabbit.core.InternalValue; -import org.apache.ojb.broker.accesslayer.conversions.ConversionException; -import org.apache.ojb.broker.accesslayer.conversions.FieldConversion; - -/** - *

Helper class to convert multi-valued properties into an encoded - * string stored in a single database column.

- */ -public class ValuesToStringFieldConversion - implements FieldConversion { - - private int type; - - public ValuesToStringFieldConversion() { - } - - public ValuesToStringFieldConversion(int type) { - this.type = type; - } - - public Object javaToSql(Object object) throws ConversionException { - InternalValue[] values = (InternalValue[]) object; - StringBuffer buffer = new StringBuffer(); - for (int i=0; i < values.length; i++) { - buffer.append(values[i].toString()); - if (i < values.length - 1) { - buffer.append(","); - } - } - return buffer.toString(); - } - - public Object sqlToJava(Object object) throws ConversionException { - ArrayList valueList = new ArrayList(); - StringTokenizer tokenizer = new StringTokenizer((String) object, ","); - while (tokenizer.hasMoreTokens()) { - InternalValue curValue = InternalValue.valueOf(tokenizer.nextToken(), type); - valueList.add(curValue); - } - return (InternalValue[]) valueList.toArray(new InternalValue[valueList.size()]); - } -} diff --git a/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/BlobTest.java b/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/BlobTest.java deleted file mode 100644 index 05ad50d2931..00000000000 --- a/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/BlobTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.test.orm; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import java.io.InputStream; -import javax.jcr.StringValue; -import java.io.ByteArrayInputStream; -import java.io.BufferedInputStream; - -import org.apache.jackrabbit.test.AbstractJCRTest; - -/** - * BLOB insertion/retrieval test case. - */ -public class BlobTest extends AbstractJCRTest { - - private final static int BLOB_SIZE = 1*1024*1024; - byte[] blobContent = new byte[BLOB_SIZE]; - - public BlobTest() { - } - - public void testBlob() throws Exception { - Node rn = superuser.getRootNode(); - - NodeIterator nodeIter = rn.getNodes(); - while (nodeIter.hasNext()) { - Node curNode = nodeIter.nextNode(); - } - - log.println("Creating BLOB data of " + BLOB_SIZE + " bytes..."); - for (int i=0; i < BLOB_SIZE; i++) { - blobContent[i] = (byte) (i % 256); - } - log.println("Adding BLOB node..."); - if (!rn.hasNode("blobnode")) { - ByteArrayInputStream inputStream = new ByteArrayInputStream(blobContent); - Node n = rn.addNode("blobnode", "nt:unstructured"); - n.setProperty("testprop", new StringValue("Hello, World.")); - n.setProperty("blobTest", inputStream); - superuser.save(); - } - log.println("Verifying BLOB node..."); - InputStream readInputStream = rn.getProperty("blobnode/blobTest"). - getStream(); - int ch = -1; - int i=0; - BufferedInputStream buf = new BufferedInputStream(readInputStream); - while ((ch = buf.read()) != -1) { - assertEquals((byte) ch, blobContent[i]); - i++; - } - log.println("Removing BLOB node..."); - rn.getNode("blobnode").remove(); - superuser.save(); - } - -} diff --git a/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/TestAll.java b/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/TestAll.java deleted file mode 100644 index fc03d699e01..00000000000 --- a/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/TestAll.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.test.orm; - -import junit.framework.TestCase; -import junit.framework.Test; -import junit.framework.TestSuite; - -/** - * Test suite that includes all testcases for the package - * javax.jcr. - */ -public class TestAll extends TestCase { - - /** - * Returns a Test suite that executes all tests inside this - * package. - * - * @return a Test suite that executes all tests inside this - * package. - */ - public static Test suite() { - TestSuite suite = new TestSuite("Jackrabbit ORM persistence tests"); - - suite.addTestSuite(BlobTest.class); - suite.addTestSuite(XMLImportSpeedTest.class); - - return suite; - } -} diff --git a/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/XMLImportSpeedTest.java b/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/XMLImportSpeedTest.java deleted file mode 100644 index 0d76713d605..00000000000 --- a/contrib/orm-persistence/src/test/org/apache/jackrabbit/test/orm/XMLImportSpeedTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.test.orm; - -import java.io.FileInputStream; -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Property; -import javax.jcr.PropertyIterator; -import javax.jcr.PropertyType; -import javax.jcr.RepositoryException; -import javax.jcr.Value; - -import org.apache.jackrabbit.test.AbstractJCRTest; -import org.dom4j.Document; -import org.dom4j.Element; -import org.dom4j.io.SAXReader; - -/** - * Test suite for XML import speed benchmarking. - */ -public class XMLImportSpeedTest extends AbstractJCRTest { - - private static final int BLOB_NODE_COUNT = 2; - private static final int XML_IMPORT_NODE_COUNT = 1; - - private final String XML_IMPORT_FILENAME = "applications/test/import.xml"; - - private int totalNodeCount = 0; - private int totalElementCount = 0; - private int totalAttributeCount = 0; - - public void treeWalk(Document document) { - treeWalk(document.getRootElement()); - } - - public void treeWalk(Element element) { - totalElementCount++; - totalAttributeCount += element.attributeCount(); - for (int i = 0, size = element.nodeCount(); i < size; i++) { - totalNodeCount++; - org.dom4j.Node node = element.node(i); - if (node instanceof Element) { - treeWalk( (Element) node); - } else { - // do something.... - } - } - } - - public void testXMLImportSpeed() throws Exception { - Node rn = superuser.getRootNode(); - - if (!rn.hasNode("importxml0")) { - log.println("importing xml"); - - SAXReader reader = new SAXReader(); - Document document = reader.read(new FileInputStream( - XML_IMPORT_FILENAME)); - treeWalk(document); - log.println("XML file " + XML_IMPORT_FILENAME + " has " + - totalElementCount + " elements, " + totalNodeCount + - " nodes and " + totalAttributeCount + - " attributes"); - - log.println("Now performing XML import " + - XML_IMPORT_NODE_COUNT + - " time(s)..."); - long xmlImportTestStart = System.currentTimeMillis(); - for (int i = 0; i < XML_IMPORT_NODE_COUNT; i++) { - Node nl = rn.addNode("importxml" + i, "nt:unstructured"); - superuser.importXML("/importxml" + i, - new FileInputStream( - XML_IMPORT_FILENAME)); - log.println("Saving..."); - superuser.save(); - } - long xmlImportTestTime = System.currentTimeMillis() - - xmlImportTestStart; - log.println("Imported XML " + XML_IMPORT_NODE_COUNT + - " time(s) in " + - xmlImportTestTime + "ms average=" + - xmlImportTestTime / XML_IMPORT_NODE_COUNT + - "ms/node"); - - log.println("Removing imported node(s)..."); - for (int i = 0; i < XML_IMPORT_NODE_COUNT; i++) { - Node curXMLImportNode = rn.getNode("importxml" + i); - curXMLImportNode.remove(); - } - superuser.save(); - } else { - log.println( - "XML import has already been run previously, not reimporting on same nodes"); - } - - // dump(rn); - - } - - public void dump(Node n) throws RepositoryException { - log.println(n.getPath()); - PropertyIterator pit = n.getProperties(); - while (pit.hasNext()) { - Property p = pit.nextProperty(); - if (!p.getDefinition().isMultiple()) { - Value curValue = p.getValue(); - if (curValue.getType() == PropertyType.BINARY) { - log.println(p.getPath() + "=BINARY[" + p.getLength() + "]"); - } else { - log.println(p.getPath() + "=" + p.getString()); - } - } else { - log.println("Multi-value for " + p.getPath()); - } - } - NodeIterator nit = n.getNodes(); - while (nit.hasNext()) { - Node cn = nit.nextNode(); - dump(cn); - } - } - -} diff --git a/contrib/tck-webapp/maven.xml b/contrib/tck-webapp/maven.xml deleted file mode 100644 index b9df7ca37aa..00000000000 --- a/contrib/tck-webapp/maven.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/tck-webapp/project.properties b/contrib/tck-webapp/project.properties deleted file mode 100644 index 502c15b8254..00000000000 --- a/contrib/tck-webapp/project.properties +++ /dev/null @@ -1,5 +0,0 @@ -# ------------------------------------------------------------------------ -maven.repo.remote = http://www.ibiblio.org/maven/,http://www.day.com/maven/ -maven.test.skip = true - - diff --git a/contrib/tck-webapp/project.xml b/contrib/tck-webapp/project.xml deleted file mode 100644 index d2742df27b9..00000000000 --- a/contrib/tck-webapp/project.xml +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - tck-webapp - - Day TCK web application - 0.1 - - - - - - - - - - concurrent - 1.3.4 - - true - - - - commons-collections - 2.1 - - true - - - - lucene - lucene - 1.4.3 - - true - - - - xerces - xercesImpl - 2.6.2 - - true - - - - - - jsr170 - jcr - 0.16.2 - http://www.day.com/maven/jsr170/jars/jcr-0.16.2.jar - - - - jackrabbit - 0.16.2-dev - - true - - - - - cqfs - cqfs-jackrabbit - 3.5.6 - http://www.day.com/maven/cqfs/jars/cqfs-jackrabbit-3.5.6.jar - - true - - - - - cqfs - 3.5.6 - - true - - - - commons-logging - 1.0 - - true - - - - - - - jdom - 1.0 - - true - - - - log4j - 1.2.8 - - true - - - - - - - servletapi - 2.3 - - - junit - 3.8.1 - - true - - - - - - - - - - ${basedir}/src/java - - - fakeClass - - **/test/observation/*.java - **/test/search/*.java - - - - - - src/java - - **/api/**/*.java - **/*.xml - **/*.xsd - **/*.properties - **/*.dtd - - - - - diff --git a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TckHelper.java b/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TckHelper.java deleted file mode 100644 index 33360848a71..00000000000 --- a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TckHelper.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.tck; - -/** - * Just a helper class - */ -public class TckHelper { - public static String getStatus(int state) { - String status = "UNDEFINED"; - - switch (state) { - case TestResult.SUCCESS: - status = "SUCCESS"; - break; - case TestResult.ERROR: - status = "ERROR"; - break; - case TestResult.FAILURE: - status = "FAILURE"; - } - return status; - } -} diff --git a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TckTestRunner.java b/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TckTestRunner.java deleted file mode 100644 index 821190eea70..00000000000 --- a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TckTestRunner.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.tck; - -import junit.framework.Test; -import junit.runner.BaseTestRunner; -import javax.servlet.jsp.JspWriter; -import java.io.IOException; -import java.io.Writer; -import java.util.Map; -import java.util.HashMap; -import java.text.MessageFormat; - -import org.apache.jackrabbit.test.NotExecutableException; - -/** - * The TckTestRunner class implements the TestListener interface. - */ -public class TckTestRunner extends BaseTestRunner { - /** Test state */ - int state; - - /** The test */ - Test test; - - /** the writer */ - Writer writer; - - /*** contains all results from all tests */ - Map results; - - /** test start time */ - long startTime; - - /** time that a test took */ - long testTime; - - /** Result of a test */ - TestResult result; - - /** String containing defined log output */ - private String logString; - - /** String containing defined interaction output */ - private String interactionString; - - /** current testclass */ - private String currentTestClass; - - /** new test identifier string */ - private String newTestString; - - /** - * The constructor inits the result map and sets the writer - * - * @param writer - */ - public TckTestRunner(JspWriter writer) { - this.writer = writer; - results = new HashMap(); - currentTestClass = ""; - } - - /** - * This method is called everytime a test is executed. - * The result object is "reset". the state is "reset" to its default value. - * The startTime is set. - * - * @param test The Test which will be executed - */ - public synchronized void startTest(Test test) { - result = new TestResult(); - state = TestResult.SUCCESS; - startTime = System.currentTimeMillis(); - } - - /** - * The test could not be started. This should not happen... - * - * @param message error message - */ - protected void runFailed(String message) { - String msg = "RUN FAILED:" + message; - write(msg, false); - } - - /** - * This method is called everytime a test is finished. it does not matter if the - * test was successful or not. The TestResult is added to the results list. - * - * @param test the current Test - */ - public synchronized void endTest(Test test) { - testTime = System.currentTimeMillis() - startTime; - result.setTest(test); - result.setTestTime(testTime); - result.setStatus(state); - results.put(test.toString(), result); - if (!currentTestClass.equals(test.getClass().getName())) { - currentTestClass = test.getClass().getName(); - write(test.toString(), true); - } else { - write(test.toString(), false); - } - } - - /** - * This method is called when a Test failed. - * The "error" code is passed: - *
  • - an error occured while testing - *
  • - the test failed - * And the Throwable object with the information why the test failed is passed as well. - * - * @param status "error" code - * @param test current Test - * @param t Throwable of error/failure - */ - public void testFailed(int status, Test test, Throwable t) { - if (t instanceof NotExecutableException) { - state = TestResult.NOT_EXECUTABLE; - } else { - state = status; - } - result.setErrorMsg(t.toString()); - } - - /** - * Writes test logging information to output - * - * @param msg - */ - private void write(String msg, boolean newTestClass) { - if (writer != null) { - try { - String html = ""; - if (logString!= null && !"".equals(logString)) { - html = MessageFormat.format(logString, new String[]{msg, TckHelper.getStatus(state)}); - writer.write(html); - } - - String color; - switch (state) { - case TestResult.SUCCESS: - color = "pass"; - break; - case TestResult.ERROR: - case TestResult.FAILURE: - color = "failure"; - break; - case TestResult.NOT_EXECUTABLE: - color = "error"; - break; - default: - color = "clear"; - } - - if (interactionString!= null && !"".equals(interactionString)) { - html = MessageFormat.format(interactionString, new String[]{msg, color, String.valueOf(testTime)}); - if (newTestClass) { - html += newTestString; - } - writer.write(html); - } - writer.flush(); - } catch (IOException e) { - // ignore - } - } else { - System.out.println(msg); - } - } - - /** - * Returns all results - * @return all test results - */ - public Map getResults() { - return results; - } - - public void setLogString(String logString) { - this.logString = logString; - } - - public void setInteractionString(String interactionString) { - this.interactionString = interactionString; - } - - public void setNewTestString(String newTestString) { - this.newTestString = newTestString; - } - - public void testStarted(String testName) { - } - - public void testEnded(String testName) { - } -} - diff --git a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TestFinder.java b/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TestFinder.java deleted file mode 100644 index 9239f263d34..00000000000 --- a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TestFinder.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.tck; - -import junit.framework.TestSuite; -import junit.framework.Test; -import java.io.*; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.*; - -/** - * The TestFinder class is responsible to find all TestCases which are - * in a jar. The information which jar has to be searched is passed in the constructor. - */ -public class TestFinder { - /** Jar file to search the test classes */ - private JarFile jarFile; - - /** all tests */ - private Map allTests; - - /** all test suites */ - private Map suites; - - /** - * The path where the jar containing the test classes and its sources is residing is passed here. - * - * @throws IOException - */ - public TestFinder() throws IOException { - allTests = new HashMap(); - suites = new HashMap(); - } - - /** - * This method searches all tests. - * - * @param exclude file(class) name (e.g. allTests) of tests which should be excluded. - * @param in InputStream of jar file containing the test class sources - * @throws IOException - * @throws ClassNotFoundException - */ - public void find(InputStream in, String exclude) throws IOException, ClassNotFoundException { - File tmpFile = null; - try { - // save to a temp file - tmpFile = File.createTempFile("tck-tests", "jar"); - OutputStream out = null; - try { - out = new FileOutputStream(tmpFile); - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } finally { - in.close(); - out.close(); - } - - jarFile = new JarFile(tmpFile); - - // go through all jar file entries and take the one we are interessted in. - Enumeration enum = jarFile.entries(); - - while (enum.hasMoreElements()) { - JarEntry entry = (JarEntry) enum.nextElement(); - String name = entry.getName(); - - if (!entry.isDirectory() && name.endsWith(".java")) { - // only source files are from interesst - String classname = name.replace('/','.').substring(0, name.lastIndexOf(".java")); - - Class testclass = Class.forName(classname); - - // check if class is really a testsuite - if (!isTestSuite(testclass)) { - continue; - } - - // check for files to be excluded - if (name.endsWith(exclude)) { - continue; - } - - // retrieve keyword from source file - String keyword = getKeyword(entry); - - // classify testsuite (level1, level2,...) - if (suites.containsKey(keyword)) { - TestSuite suite = (TestSuite) suites.get(keyword); - suite.addTestSuite(testclass); - } else { - TestSuite suite = new TestSuite(keyword); - suite.addTestSuite(testclass); - suites.put(keyword, suite); - } - - // memorize tests - allTests.put(classname, keyword); - } - } - } finally { - if (tmpFile != null) { - tmpFile.delete(); - } - } - } - - /** - * Check if the passed test class is a "real" test suite - * - * @param testclass class to check - * @return true if a test suite - */ - private boolean isTestSuite(Class testclass) { - TestSuite ts = new TestSuite(testclass); - if (ts.countTestCases() > 0) { - Test t = (Test) ts.tests().nextElement(); - if (t.toString().startsWith("warning")) { - return false; - } - } else { - return false; - } - return true; - } - - /** - * Reads the keyword from the java source. - * - * @param entry JarEntry to parse - * @return returns the existing keyword or "unspecified" - * @throws IOException - */ - private String getKeyword(JarEntry entry) throws IOException { - InputStream input = jarFile.getInputStream(entry); - InputStreamReader isr = new InputStreamReader(input); - BufferedReader reader = new BufferedReader(isr); - String line; - while ((line = reader.readLine()) != null) { - String keyword = null; - if ((keyword = parseLine(line)) != null) { - return keyword; - } - } - reader.close(); - return "unspecified"; - } - - /** - * Parses a line and checks for the "keywords" keyword - * - * @param line line to parse - * @return the kewyword string or null - */ - private String parseLine(String line) { - int pos = line.indexOf("keywords"); - int len = "keywords".length(); - String word = ""; - - if ( pos >= 0) { - char l[] = line.toCharArray(); - pos += len; - - while (pos < l.length) { - char c = l[pos]; - - switch (c) { - case 9: // '\t' - case 10: // '\n' - case 12: // '\f' - case 13: // '\r' - case 32: // ' ' - if (!"".equals(word)) { - return word; - } - break; - default: - word += c; - } - pos++; - } - } - return ("".equals(word)) ? null : word; - } - - /** - * Returns all tests categorized by it's keyword - * @return all tests - */ - public Map getTests() { - return allTests; - } - - /** - * Returns all built test suites. the suites are categorized: - *
  • - level1 - *
  • - level 2 - *
  • - optional... - * - * @return test suites - */ - public Map getSuites() { - return suites; - } -} \ No newline at end of file diff --git a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TestResult.java b/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TestResult.java deleted file mode 100644 index 9c3153532f8..00000000000 --- a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/TestResult.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.tck; - -import junit.framework.Test; - -/** - * The TestResult class is just a helper class to store test results. - */ -public class TestResult { - - /** Test succeeded */ - public static final int SUCCESS = 0; - - /** An error occured while testing */ - public static final int ERROR = 1; - - /** The test failed */ - public static final int FAILURE = 2; - - /** The test cannot be executed */ - public static final int NOT_EXECUTABLE = 4; - - /** The test */ - private Test test; - - /** Test status */ - private int status; - - /** Error message */ - private String errorMsg; - - /** Time that consumed while testing */ - private long testTime; - - public TestResult() { - // do nothing - } - - public long getTestTime() { - return testTime; - } - - public void setTestTime(long testTime) { - this.testTime = testTime; - } - - public Test getTest() { - return test; - } - - public void setTest(Test test) { - this.test = test; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getErrorMsg() { - return errorMsg; - } - - public void setErrorMsg(String errorMsg) { - this.errorMsg = errorMsg; - } -} diff --git a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/Tester.java b/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/Tester.java deleted file mode 100644 index 5646330012d..00000000000 --- a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/Tester.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.tck; - -import junit.framework.TestSuite; -import junit.framework.TestResult; -import javax.jcr.*; -import javax.jcr.nodetype.ConstraintViolationException; -import javax.servlet.jsp.JspWriter; -import java.util.*; -import java.io.IOException; -import java.text.MessageFormat; - -import org.apache.jackrabbit.test.AbstractJCRTest; -import org.apache.jackrabbit.test.RepositoryHelper; - -/** - * The Tester starts the all tests and saves all tests. - */ -public class Tester { - - /** Object containing all tests */ - TestFinder tests; - - /** all test results are stored here */ - Map results; - - /** the test listener */ - TckTestRunner runner; - - /** the jsp writer */ - JspWriter writer; - - /** The string to be writen after finishing a suite */ - String finishedSuiteString; - - /** - * The constructor... - * - * @param tests All tests found using TestFinder - */ - public Tester(TestFinder tests, TckTestRunner runner, JspWriter writer) { - this.tests = tests; - results = new HashMap(); - this.runner = runner; - this.writer = writer; - - // set the configuration for the to be tested repository - AbstractJCRTest.helper = new RepositoryHelper(WebAppTestConfig.getCurrentConfig()); - } - - /** - * Calling this method starts the testing. All test results will be stored in the - * results hashmap for further use. - * - * @return the result list (map) - */ - public Map run() { - - // get suites (level1, level2, ...) - Iterator suites = tests.getSuites().keySet().iterator(); - - while (suites.hasNext()) { - TestSuite suite = (TestSuite) tests.getSuites().get(suites.next()); - - TestResult result = new TestResult(); - result.addListener(runner); - try { - suite.run(result); - results.putAll(runner.getResults()); - write(suite); - } catch (Exception e) { - // ignore - } - } - return results; - } - - - public void setfinishedSuiteString(String line) { - finishedSuiteString = line; - } - - /** - * Writes a predefined string to the output after finishing a test suite - */ - private void write(TestSuite suite) throws IOException { - if (writer != null) { - writer.write(MessageFormat.format(finishedSuiteString, new String[]{suite.toString(), String.valueOf(suite.testCount())})); - } - } - - /** - * This method stores the result underneath the passed node in this structure: - *
    -     * node
    -     *  + suite name 1
    -     *    + test 1
    -     *    + test 2
    -     *    ...
    -     *  + suite name 2
    -     *    + test a
    -     *    + test b
    -     *    ...
    -     *  ....
    -     * 
    - * @param node parent Node for storage - * @throws RepositoryException - * @throws ConstraintViolationException - * @throws InvalidItemStateException - * @throws AccessDeniedException - */ - public void storeResults(Node node) throws RepositoryException, ConstraintViolationException, InvalidItemStateException, AccessDeniedException { - // create categories: level1, level2.... - Iterator keyItr = tests.getSuites().keySet().iterator(); - while (keyItr.hasNext()) { - node.addNode((String) keyItr.next()); - } - - // save test results here - Iterator itr = results.keySet().iterator(); - while (itr.hasNext()) { - String key = (String) itr.next(); - org.apache.jackrabbit.tck.TestResult tr = (org.apache.jackrabbit.tck.TestResult) results.get(key); - String className = tr.getTest().getClass().getName(); - String testName = key.substring(0, key.indexOf("(")); - String keyword = (String) tests.getTests().get(className); - if (keyword != null) { - Node testResNode = node.addNode(keyword + "/" + key); - testResNode.setProperty("name", testName); - testResNode.setProperty("status", tr.getStatus()); - if (tr.getErrorMsg() != null) { - testResNode.setProperty("errrormsg", tr.getErrorMsg()); - } - testResNode.setProperty("testtime", tr.getTestTime()); - } - } - node.save(); - } -} diff --git a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/WebAppTestConfig.java b/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/WebAppTestConfig.java deleted file mode 100644 index ac6fda65dee..00000000000 --- a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/WebAppTestConfig.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.tck; - -import org.apache.jackrabbit.tck.j2ee.RepositoryServlet; -import org.apache.jackrabbit.test.JNDIRepositoryStub; -import org.apache.jackrabbit.test.RepositoryStub; - -import javax.jcr.*; -import javax.servlet.http.HttpServletRequest; -import java.util.*; -import java.io.InputStream; -import java.io.IOException; - -import junit.framework.TestSuite; -import junit.framework.TestCase; - - -/** - * The WebAppTestConfig class reads and saves the config in the tck web app specific way. - */ -public class WebAppTestConfig { - /** default property names */ - public final static String[] propNames = {JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_WORKSPACE_NAME, - JNDIRepositoryStub.REPOSITORY_LOOKUP_PROP, - "java.naming.provider.url", - "java.naming.factory.initial", - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_SUPERUSER_NAME, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_SUPERUSER_PWD, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_READWRITE_NAME, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_READWRITE_PWD, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_READONLY_NAME, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_READONLY_PWD, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_TESTROOT, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_NODE_NAME1, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_NODE_NAME2, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_NODE_NAME3, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_NODE_NAME4, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_PROP_NAME1, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_PROP_NAME2, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_NAMESPACES, - JNDIRepositoryStub.PROP_PREFIX + "." + JNDIRepositoryStub.PROP_NODETYPE}; - - /** - * Reads the config entries from the repository - * - * @return test config - */ - public static Map getConfig() { - Map config = new HashMap(); - try { - Session repSession = RepositoryServlet.getSession(); - Node configNode = repSession.getRootNode().getNode("testconfig"); - PropertyIterator pitr = configNode.getProperties(); - - while (pitr.hasNext()) { - Property p = pitr.nextProperty(); - config.put(p.getName(), p.getString()); - } - } catch (RepositoryException e) { - return new HashMap(); - } - return config; - } - - /** - * Reads the original config from the property file. - * - * @return original read only config - */ - public static Map getOriConfig() { - Properties props = new Properties(); - InputStream is = WebAppTestConfig.class.getClassLoader().getResourceAsStream(JNDIRepositoryStub.STUB_IMPL_PROPS); - if (is != null) { - try { - props.load(is); - } catch (IOException e) { - // ignore - } - } - - // add additional props - props.put(JNDIRepositoryStub.REPOSITORY_LOOKUP_PROP, ""); - props.put("java.naming.provider.url", ""); - props.put("java.naming.factory.initial", ""); - - return props; - } - - /** - * Saves the configuration entries which needs to be set. - * - * @param request request with config changes - * @param repSession Session used to write config - * @throws RepositoryException - */ - public static void save(HttpServletRequest request, Session repSession) throws RepositoryException { - // create config node if not yet existing - Node testConfig; - if (repSession.getRootNode().hasNode("testconfig")) { - testConfig = repSession.getRootNode().getNode("testconfig"); - } else { - testConfig = repSession.getRootNode().addNode("testconfig", "nt:unstructured"); - repSession.getRootNode().save(); - } - - // save config entries - Iterator allPropNames = getCurrentConfig().keySet().iterator(); - - while (allPropNames.hasNext()) { - String pName = (String) allPropNames.next(); - setEntry(pName, request, testConfig); - } - - // save - testConfig.save(); - } - - /** - * This method saves a single property - * - * @param propName property name - * @param propValue property value - * @param repSession session - * @throws RepositoryException - */ - public static void saveProperty(String propName, String propValue, Session repSession) throws RepositoryException { - // create config node if not yet existing - Node testConfig; - if (repSession.getRootNode().hasNode("testconfig")) { - testConfig = repSession.getRootNode().getNode("testconfig"); - } else { - testConfig = repSession.getRootNode().addNode("testconfig", "nt:unstructured"); - repSession.getRootNode().save(); - } - - testConfig.setProperty(propName, propValue); - - // save - testConfig.save(); - } - - /** - * Set config entry - * - * @param propname config property name - * @param request request to read property value - * @param testConfig test config Node - * @throws RepositoryException - */ - private static void setEntry(String propname, HttpServletRequest request, Node testConfig) throws RepositoryException { - if (request.getParameter(propname) != null) { - testConfig.setProperty(propname, request.getParameter(propname)); - } - } - - /** - * Returns all test case specific configuration entries - * - * @param suite test suite - * @return all test case specific conf entries - */ - public static Map getTestCaseSpecificConfigs(TestSuite suite) { - Map currentConfig = getCurrentConfig(); - Map configs = new HashMap(); - - // check for "package" defined props - String pname = suite.getName(); - if ("versioning".equals(pname)) { - pname = "version"; - } - configs.putAll(getProperties(pname, currentConfig)); - - Enumeration allTestClasses = suite.tests(); - - while (allTestClasses.hasMoreElements()) { - TestSuite aTest = (TestSuite) allTestClasses.nextElement(); - String name = aTest.getName(); - name = name.substring(name.lastIndexOf(".") + 1); - - // check for class defined props - configs.putAll(getProperties(name, currentConfig)); - - // goto methods - Enumeration testMethods = aTest.tests(); - - while (testMethods.hasMoreElements()) { - TestCase tc = (TestCase) testMethods.nextElement(); - String methodname = tc.getName(); - String fullName = name + "." + methodname; - configs.putAll(getProperties(fullName, currentConfig)); - } - } - return configs; - } - - /** - * Returns the current configuration - * @return - */ - public static Map getCurrentConfig() { - Map conf = getOriConfig(); - conf.putAll(getConfig()); - return conf; - } - - /** - * Returns all properties which property name starts with "javax.jcr.tck." + name - * - * @param name property name "extension" - * @param config configuration - * @return all properties which apply the above mentioned rule - */ - private static Map getProperties(String name, Map config) { - Map props = new HashMap(); - - String pname = RepositoryStub.PROP_PREFIX + "." + name; - - Iterator itr = config.keySet().iterator(); - - while (itr.hasNext()) { - String key = (String) itr.next(); - if (key.startsWith(pname)) { - props.put(key, config.get(key)); - } - } - return props; - } - - /** - * Removes the custom config entries - * - * @throws RepositoryException - */ - public static void resetConfiguration() throws RepositoryException { - Session repSession = RepositoryServlet.getSession(); - if (repSession.getRootNode().hasNode("testconfig")) { - repSession.getRootNode().getNode("testconfig").remove(); - } - repSession.getRootNode().save(); - } -} diff --git a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/j2ee/RepositoryServlet.java b/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/j2ee/RepositoryServlet.java deleted file mode 100644 index 841e8de4eba..00000000000 --- a/contrib/tck-webapp/src/java/org/apache/jackrabbit/tck/j2ee/RepositoryServlet.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.tck.j2ee; - -import org.apache.log4j.PropertyConfigurator; -import org.apache.log4j.Logger; -import org.apache.jackrabbit.core.config.RepositoryConfig; -import org.apache.jackrabbit.core.RepositoryImpl; -import org.xml.sax.InputSource; - -import javax.servlet.http.HttpServlet; -import javax.servlet.ServletException; -import javax.jcr.*; -import java.io.*; -import java.util.Properties; -import javax.servlet.http.HttpServletRequest; - -/** - * The RepositoryServlet connects (starts) to a jsr170 repository and - * puts the reference into the application context - */ -public class RepositoryServlet extends HttpServlet { - - /** the logger */ - private static Logger log;// = Logger.getLogger(RepositoryServlet.class); - - /** repository configuration path */ - public final static String INIT_PARAM_REPOSITORY_CONFIG = "repository-config"; - - /** repository home */ - public final static String INIT_PARAM_REPOSITORY_HOME = "repository-home"; - - /** repository name */ - public final static String INIT_PARAM_REPOSITORY_NAME = "repository-name"; - - /** jaas configuration file path */ - public static final String JAAS_CONFIG_FILE = "jaas-config-file"; - - /** user id name */ - public static final String USER_ID = "jcr-userid"; - - /** user password name */ - public static final String USER_PASSWORD = "jcr-password"; - - /** log4j config */ - public final static String PARAM_LOG4J_CONFIG = "log4j-config"; - - /** the repository to read/write test results and config */ - private static RepositoryImpl repository; - - /** the user id */ - private static String uid; - - /** the password */ - private static String pw; - - /** - * The init method starts the repository to read/write test results and configuration, - * sets the jaas config and the user id and the user password - * - * @throws ServletException - */ - public void init() throws ServletException { - super.init(); - - try { - // setup log4j - // todo: do correctly - String log4jConfig = getServletConfig().getInitParameter(PARAM_LOG4J_CONFIG); - InputStream in = getServletContext().getResourceAsStream(log4jConfig); - if (in == null) { - // try normal init - PropertyConfigurator.configure(log4jConfig); - } else { - try { - Properties log4jProperties = new Properties(); - log4jProperties.load(in); - in.close(); - PropertyConfigurator.configure(log4jProperties); - } catch (IOException e) { - throw new ServletException("Unable to load log4jProperties: " + e.toString()); - } - } - log_info("RepositoryServlet initializing.."); - - // setup home directory - String repHome = getServletConfig().getInitParameter(INIT_PARAM_REPOSITORY_HOME); - if (repHome == null) { - log_info(INIT_PARAM_REPOSITORY_HOME + " missing."); - throw new ServletException(INIT_PARAM_REPOSITORY_HOME + " missing."); - } - File repositoryHome; - try { - repositoryHome = new File(repHome).getCanonicalFile(); - } catch (IOException e) { - log_info(INIT_PARAM_REPOSITORY_HOME + " invalid." + e.toString()); - throw new ServletException(INIT_PARAM_REPOSITORY_HOME + " invalid." + e.toString()); - } - log_info(" repository-home = " + repositoryHome.getPath()); - - // setup repository - String repConfig = getServletConfig().getInitParameter(INIT_PARAM_REPOSITORY_CONFIG); - if (repConfig == null) { - log_info(INIT_PARAM_REPOSITORY_CONFIG + " missing."); - throw new ServletException(INIT_PARAM_REPOSITORY_CONFIG + " missing."); - } - log_info(" repository-config = " + repConfig); - - in = getServletContext().getResourceAsStream(repConfig); - if (in == null) { - try { - in = new FileInputStream(new File(repositoryHome, repConfig)); - } catch (FileNotFoundException e) { - log_info(INIT_PARAM_REPOSITORY_CONFIG + " invalid." + e.toString()); - throw new ServletException(INIT_PARAM_REPOSITORY_CONFIG + " invalid." + e.toString()); - } - } - - // get repository name - String repositoryName = getServletConfig().getInitParameter(INIT_PARAM_REPOSITORY_NAME); - if (repositoryName == null) { - repositoryName = "default"; - } - log_info(" repository-name = " + repositoryName); - - InputSource is = new InputSource(in); - RepositoryConfig config = RepositoryConfig.create(is, repositoryHome.getPath()); - repository = RepositoryImpl.create(config); - - log_info("JSR170 RI Repository initialized."); - - // set jaas config file path - String jaasConfigFile = getServletConfig().getInitParameter(JAAS_CONFIG_FILE); - if (jaasConfigFile != null && !"".equals(jaasConfigFile)) { - System.setProperty("java.security.auth.login.config", jaasConfigFile); - log_info("JAAS config path set by the tck webapp. java.security.auth.login.config = " + jaasConfigFile); - } else { - log_info("No JAAS config path set by the tck webapp."); - } - - - // set user id and password to read/write test results and configuration - uid = getServletConfig().getInitParameter(USER_ID); - pw = getServletConfig().getInitParameter(USER_PASSWORD); - - } catch (RepositoryException e) { - log_info("Unable to initialize repository: " + e.toString(), e); - throw new ServletException("Unable to initialize repository: " + e.toString(), e); - } - - } - - public void destroy() { - super.destroy(); - log_info("RepositoryServlet shutting down..."); - } - - private void log_info(String msg) { - if (log != null) { - log.info(msg); - } else { - log(msg); - } - } - - private void log_info(String msg, Throwable t) { - if (log != null) { - log.info(msg, t); - } else { - log(msg, t); - } - } - /** - * Returns the JSR170 repository - * @return a jsr170 repository - */ - public static Repository getRepository() { - return repository; - } - - /** - * Returns the jcr session - * - * @return - */ - public static Session getSession() { - try { - return login(); - } catch (RepositoryException e) { - log.error("Unable to retrieve session: " + e.toString()); - } - return null; - } - - /** - * Logs in to the repository. The user to login is specified in the servlet config. - * @throws RepositoryException - */ - public static Session login() - throws RepositoryException { - - // login - Session repSession = repository.login(new SimpleCredentials(uid, pw.toCharArray()), null); - return repSession; - } - -} diff --git a/contrib/tck-webapp/src/webapp/WEB-INF/classes/repositoryStubImpl.properties b/contrib/tck-webapp/src/webapp/WEB-INF/classes/repositoryStubImpl.properties deleted file mode 100644 index 4887807befb..00000000000 --- a/contrib/tck-webapp/src/webapp/WEB-INF/classes/repositoryStubImpl.properties +++ /dev/null @@ -1,323 +0,0 @@ -# -# This is the configuration file for the jackrabbit repository test stub. -# - -# Stub implementation class -javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.test.JNDIRepositoryStub - -# repository specific configuration -org.apache.jackrabbit.repository.config=applications/test/repository.xml -org.apache.jackrabbit.repository.name=repo -org.apache.jackrabbit.repository.home=applications/test -org.apache.jackrabbit.repository.jaas.config=applications/test/jaas.config - -# credential configuration -javax.jcr.tck.superuser.name=superuser -javax.jcr.tck.superuser.pwd= -javax.jcr.tck.readwrite.name=user -javax.jcr.tck.readwrite.pwd= -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.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.workspacename=test - -# namespace configuration -javax.jcr.tck.namespaces=test -javax.jcr.tck.namespaces.test=http://www.apache.org/jackrabbit/test - -# 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: 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: 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:hierarchyNode - -# Test class: SessionTest -# Test method: testSaveContstraintViolationException -# nodetype that has a property that is mandatory but not autocreated -javax.jcr.tck.SessionTest.testSaveContstraintViolationException.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:base -# name of the mandatory node -javax.jcr.tck.NodeTest.testRemoveMandatoryNode.nodename3=jcr:content - -# Test class: NodeTest -# Test method: testSaveContstraintViolationException -# nodetype that has a property that is mandatory but not autocreated -javax.jcr.tck.NodeTest.testSaveContstraintViolationException.nodetype2=nt:file - -# 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:hierarchyNode - -# ------------------------------------------------------------------------------ -# observation configuration -# ------------------------------------------------------------------------------ - -# 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 - -# 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 - -# 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 - -# ============================================================================== -# 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: 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 - -# ============================================================================== -# JAVAX.JCR.VERSIONING CONFIGURATION -# ============================================================================== - -# nodetye 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.propertyValue=aPropertyValue - -# 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 - -# 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 diff --git a/contrib/tck-webapp/src/webapp/WEB-INF/content-repository/log4j.properties b/contrib/tck-webapp/src/webapp/WEB-INF/content-repository/log4j.properties deleted file mode 100644 index 2728ce9be0d..00000000000 --- a/contrib/tck-webapp/src/webapp/WEB-INF/content-repository/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -# Set root logger level to DEBUG and its only appender to A1. -log4j.rootLogger=INFO, stdout -#log4j.rootLogger=DEBUG, stdout, file -#log4j.rootLogger=ERROR, stdout, file - -# 'stdout' is set to be a ConsoleAppender. -log4j.appender.stdout=org.apache.log4j.ConsoleAppender - -# 'stdout' uses PatternLayout -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n - -# 'file' is set to be a FileAppender. -log4j.appender.file=org.apache.log4j.FileAppender -log4j.appender.file.File=jcr.log - -# 'file' uses PatternLayout. -log4j.appender.file.layout=org.apache.log4j.PatternLayout -log4j.appender.file.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n - diff --git a/contrib/tck-webapp/src/webapp/WEB-INF/content-repository/repository.xml b/contrib/tck-webapp/src/webapp/WEB-INF/content-repository/repository.xml deleted file mode 100644 index cd35dc49e6d..00000000000 --- a/contrib/tck-webapp/src/webapp/WEB-INF/content-repository/repository.xml +++ /dev/null @@ -1,234 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/tck-webapp/src/webapp/WEB-INF/tck/log4j.properties b/contrib/tck-webapp/src/webapp/WEB-INF/tck/log4j.properties deleted file mode 100644 index 057f2048a0a..00000000000 --- a/contrib/tck-webapp/src/webapp/WEB-INF/tck/log4j.properties +++ /dev/null @@ -1,27 +0,0 @@ -# Set root logger level to DEBUG and its only appender to A1. -log4j.rootLogger=INFO, stdout -#log4j.rootLogger=DEBUG, stdout, file -#log4j.rootLogger=ERROR, stdout, file -log4j.logger.org.apache.jackrabbit.test.testwebapp.j2ee.TesterServlet.login=INFO, login - -# 'stdout' is set to be a ConsoleAppender. -log4j.appender.stdout=org.apache.log4j.ConsoleAppender - -# 'stdout' uses PatternLayout -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n - -# 'file' is set to be a FileAppender. -log4j.appender.file=org.apache.log4j.FileAppender -log4j.appender.file.File=jcr.log - -# 'file' uses PatternLayout. -log4j.appender.file.layout=org.apache.log4j.PatternLayout -log4j.appender.file.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n - -# 'login' is set to be a FileAppender. -log4j.appender.login=org.apache.log4j.FileAppender -log4j.appender.login.File=tck/home/login.log -log4j.appender.login.layout=org.apache.log4j.PatternLayout -log4j.appender.login.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n - diff --git a/contrib/tck-webapp/src/webapp/WEB-INF/web.xml b/contrib/tck-webapp/src/webapp/WEB-INF/web.xml deleted file mode 100644 index 67bbdfb8a17..00000000000 --- a/contrib/tck-webapp/src/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - TCK web application - - - - - - Repository - repository servlet that starts the repository and registers it to JNDI. - org.apache.jackrabbit.tck.j2ee.RepositoryServlet - - - log4j-config - /WEB-INF/content-repository/log4j.properties - initial log4j configuration - - - - repository-config - /WEB-INF/content-repository/repository.xml - the repository config location - - - - repository-home - tck/content-repository - the repository home - - - - repository-name - tck.repository - Repository Name under which the repository is registered via JNDI - - - - jaas-config-file - etc/jaas.config - Path of the jaas config file - - - - jcr-userid - superuser - User to read/write from the repository where test results and config is stored - - - - jcr-password - superuser - User password - - - 1 - - \ No newline at end of file diff --git a/contrib/tck-webapp/src/webapp/config.jsp b/contrib/tck-webapp/src/webapp/config.jsp deleted file mode 100644 index 8d760d9b217..00000000000 --- a/contrib/tck-webapp/src/webapp/config.jsp +++ /dev/null @@ -1,168 +0,0 @@ -<%-- -Copyright 2004-2005 The Apache Software Foundation or its licensors, - as applicable. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---%><%@ page import="javax.jcr.Session, - javax.jcr.Node, - javax.jcr.NodeIterator, - java.text.SimpleDateFormat, - java.util.*, - org.apache.jackrabbit.tck.WebAppTestConfig, - org.apache.jackrabbit.test.JNDIRepositoryStub, - org.apache.jackrabbit.tck.WebAppTestConfig, - org.apache.jackrabbit.tck.j2ee.RepositoryServlet, - org.apache.jackrabbit.tck.TestFinder, - junit.framework.TestSuite" -%><%@page session="false" %><% - -// get path from jar where the test sources are stored -String TEST_JCR_PATH = "/WEB-INF/lib/tck-webapp-0.1.jar"; - -Session repSession = RepositoryServlet.getSession(); -if (repSession == null) { - return; -} - -String mode = request.getParameter("mode"); - -%> - - - - - - - - - - - -
    -
    - <% - if (mode == null || !mode.equals("view")) { - %>New TestView Results<% - } else { - %>New TestView Results<% - } - %> -
    -
    - <% - if (mode == null || !mode.equals("view")) { - // check for property additions - String newid = request.getParameter("newid"); - String newvalue = request.getParameter("newvalue"); - if (newvalue != null && !"".equals(newvalue) && newid != null && !"".equals(newid)) { - WebAppTestConfig.saveProperty(newid, newvalue, repSession); - } - - // reset to default configuration (from properties file) if requested - String resetConfig = request.getParameter("resetconfig"); - if (resetConfig != null && "yes".equals(resetConfig)) { - WebAppTestConfig.resetConfiguration(); - } - - // load current configuration - Map props = WebAppTestConfig.getCurrentConfig(); - %> -
    - - - <% - // display default config - for (int i = 0; i < WebAppTestConfig.propNames.length; i++) { - String name = WebAppTestConfig.propNames[i]; - String value = (String) props.get(name); - %><% - } - - // display test suite specific configs - TestFinder tf = new TestFinder(); - tf.find(getServletConfig().getServletContext().getResource(TEST_JCR_PATH).openStream(), - "TestAll.java"); - Iterator tests = tf.getSuites().keySet().iterator(); - tests = tf.getSuites().keySet().iterator(); - - while (tests.hasNext()) { - String key = (String) tests.next(); - TestSuite t = (TestSuite) tf.getSuites().get(key); - Map configs = WebAppTestConfig.getTestCaseSpecificConfigs(t); - if (configs.size() > 0) { - %><% - - Iterator citr = configs.keySet().iterator(); - while (citr.hasNext()) { - String ckey = (String) citr.next(); - // split title if too long - String title = (ckey.length() > 80) ? ckey.substring(0, 80) + " " + ckey.substring(81) : ckey; - %><% - } - %> - <% - } - } - %> -
    Default Configuration
    <%= name %>
    <%= key %>
    <%= title %>
    Set default configuration
    -
    - <% - } else { - %> -
    - - - <% - Node rootNode = repSession.getRootNode(); - - if (rootNode.hasNode("testing")) { - %> - - <% - } - %> -
    Select test to be viewed - - -
    -
    - <% - } - %> -
    - - \ No newline at end of file diff --git a/contrib/tck-webapp/src/webapp/docroot/imgs/banner_left.jpg b/contrib/tck-webapp/src/webapp/docroot/imgs/banner_left.jpg deleted file mode 100644 index f050a8db649..00000000000 Binary files a/contrib/tck-webapp/src/webapp/docroot/imgs/banner_left.jpg and /dev/null differ diff --git a/contrib/tck-webapp/src/webapp/docroot/imgs/banner_right.jpg b/contrib/tck-webapp/src/webapp/docroot/imgs/banner_right.jpg deleted file mode 100644 index c77b393048b..00000000000 Binary files a/contrib/tck-webapp/src/webapp/docroot/imgs/banner_right.jpg and /dev/null differ diff --git a/contrib/tck-webapp/src/webapp/docroot/imgs/clear.png b/contrib/tck-webapp/src/webapp/docroot/imgs/clear.png deleted file mode 100644 index 2b7c47df7fd..00000000000 Binary files a/contrib/tck-webapp/src/webapp/docroot/imgs/clear.png and /dev/null differ diff --git a/contrib/tck-webapp/src/webapp/docroot/imgs/error.png b/contrib/tck-webapp/src/webapp/docroot/imgs/error.png deleted file mode 100644 index 577366c6d29..00000000000 Binary files a/contrib/tck-webapp/src/webapp/docroot/imgs/error.png and /dev/null differ diff --git a/contrib/tck-webapp/src/webapp/docroot/imgs/failure.png b/contrib/tck-webapp/src/webapp/docroot/imgs/failure.png deleted file mode 100644 index 1c3413a21a2..00000000000 Binary files a/contrib/tck-webapp/src/webapp/docroot/imgs/failure.png and /dev/null differ diff --git a/contrib/tck-webapp/src/webapp/docroot/imgs/pass.png b/contrib/tck-webapp/src/webapp/docroot/imgs/pass.png deleted file mode 100644 index 11b20c212e9..00000000000 Binary files a/contrib/tck-webapp/src/webapp/docroot/imgs/pass.png and /dev/null differ diff --git a/contrib/tck-webapp/src/webapp/docroot/ui/default.css b/contrib/tck-webapp/src/webapp/docroot/ui/default.css deleted file mode 100644 index 8a1898d986f..00000000000 --- a/contrib/tck-webapp/src/webapp/docroot/ui/default.css +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* CSS Document */ - -body - { - font-family: Verdana, Arial, Helvetica, san-serif; - background-color: #ffffff; - padding: 0px; - margin: 0px; - margin-top: 30px; - vertical-align: top; - text-align: left; - } - -br - { - line-height:20px; - } - -table - { - text-align: left; - padding: 0px; - margin: 0px; - border: 0px solid #66dd44; - } - - -table.content - { - postion: absolute; - font-size: 10px; - line-height: 13px; - border: 0px solid #66dd44; - border-top: 1px solid #cccccc; - padding: 0px; - margin: 0px; - margin-top: 26px; - margin-bottom: 26px; - width: 718px; - text-align: left; - } - -td.content - { - color: #333333; - border: 0px solid #66dd44; - border-bottom: 1px solid #cccccc; - border-right: 0px solid #cccccc; - border-left: 0px solid #cccccc; - vertical-align: top; - padding: 3px; - padding-left: 10px; - } - -td.graph - { - font-size: 11px; - padding-left: 10px; - color: #333333; - } - - -td.disabled - { - color:#999999; - } - -th.content - { - color: #333333; - border: 0px solid #66dd44; - border-bottom: 1px solid #cccccc; - text-align: left; - padding: 5px; - padding-left:10px; - } - -th.container - { - color:#6181A9; - background-color:#f0f0f0; - } - -th.important - { - color:#B81833; - } - -.important - { - color:#B81833; - } - -#maintable - { - postion: absolute; - font-size: 10px; - line-height: 13px; - border: 1px solid #000000; - padding: 0px; - margin: 0px; - width: 960px; - } - - - -.toolcell - { - font-size: 10px; - color: #999999; - line-height: 10px; - background-color: #000000; - height: 18px; - width: 960px; - padding: 0px; - padding-bottom: 1px; - text-align: left; - } - -.toolcell A:link - { - color: #C5E2EE; - text-decoration: none; - } - -.toolcell A:visited - { - color: #C5E2EE; - text-decoration: none; - } - -.toolcell A:hover - { - color: #99FF33; - text-decoration: none; - border-bottom: 1px solid; - } - -.toolcell A:active - { - color: #FFFFFF; - text-decoration: none; - } - -.leadcell - { - margin: 0px; - padding: 0px; - border: 0px solid #000000; - width: 720px; - height: 100px; - background-image: url('../imgs/banner_left.jpg'); - vertical-align: top - } - -.leadcelltext - { - position: absolute; - border: 0px solid #000000; - font-size: 26px; - line-height: 32px; - margin-top: 5px; - margin-left: 8px; - color: #ffffff; - vertical-align: top; - } - -.logocell - { - border-left: 1px solid #000000; - width: 239px; - align:right - } - -#technavcell - { - font-size: 10px; - font-weight: bold; - border: 0px solid #000000; - border-top: 1px solid #000000; - height: 21px; - background-color: #6181A9; - } - -#technav - { - float: left; - display: block; - font-size: 10px; - line-height: 20px; - font-weight: bold; - color: #000000; - height: 21px; - border: 0px solid #000000; - padding: 0px; - background-color: #ffffff; - } -.technavat - { - cursor:default; - float: left; - display: block; - font-size: 10px; - line-height: 20px; - font-weight: bold; - color: #000000; - height: 21px; - border: 0px solid #000000; - border-right: 1px solid #000000; - padding: 0px 10px 0px 10px; - background-color: #B6CAE4; - } -#technav A:link - { - float: left; - display: block; - color: #ffffff; - border: 0px solid #000000; - border-right: 1px solid #000000; - height: 21px; - text-decoration: none; - padding: 0px 10px 0px 10px; - background-color: #6181A9; - } -#technav A:visited - { - float: left; - display: block; - color: #ffffff; - border-right: 1px solid #000000; - height: 21px; - text-decoration: none; - padding: 0px 10px 0px 10px; - background-color: #6181A9; - } -#technav A:hover - { - float: left; - display: block; - color: #ffffff; - border-right: 1px solid #000000; - height: 21px; - text-decoration: none; - padding: 0px 10px 0px 10px; - background-color: #000000; - } -#technav A:active - { - float: left; - display: block; - color: #ffffff; - border-right: 1px solid #000000; - height: 21px; - text-decoration: none; - padding: 0px 10px 0px 10px; - background-color: #6181A9; - } - -.techcontentcell - { - width: 718px; - border: 0px solid #000000; - border-top: 1px solid #000000; - } - -.relatedcell - { - border-left: 1px solid #000000; - border-top: 1px solid #000000; - width: 239px; - background-color: #EBF0F6; - } - -.claimcell - { - width: 715px; - font-size: 10px; - color: #ffffff; - line-height: 10px; - background-color: #000000; - height: 18px; - padding: 0px; - padding-bottom: 1px; - } - -.claimcell A:link - { - color: #C5E2EE; - text-decoration: none; - } - -.claimcell A:visited - { - color: #C5E2EE; - text-decoration: none; - } - -.claimcell A:hover - { - color: #99FF33; - text-decoration: none; - border-bottom: 1px solid; - } - -.claimcell A:active - { - color: #FFFFFF; - text-decoration: none; - } - -/* CENTRAL CONTENT AREA STYLING */ - -.content - { - position: relative; - margin: 25px; - font-size: 11px; - line-height: 16px; - color: #000000; - } - -.content A:link - { - color: #336600; - text-decoration: underline; - } - -.content A:visited - { - color: #666666; - text-decoration: underline; - } - -.content A:hover - { - color: #ffffff; - background-color: #336600; - text-decoration: none; - } - -.content A:active - { - color: #ffffff; - background-color: #000000; - text-decoration: none; - } - - - -/* TEXT STYLING */ - -h1, h2, h3, h4, h5, h6, p - - { - margin: 0px; - margin-bottom: 8px; - font-size: 11px; - line-height: 16px; - font-weight:bold; - } - - -h1 - { - color: #000000; - margin-top: 32px; - clear: left; - } - - -h2 - { - color: #336699; - } - - -h3 - { - color: #336699; - } - -p - { - font-size: 11px; - line-height: 16px; - font-weight: normal; - color: #000000; - } - -/* FORMS */ - -form - { - font-size: 9px; - border-top: 0px solid #000000; - border-bottom: 0px solid #000000; - border-left: 0px solid #000000; - border-right: 0px solid #000000; - margin: 0; - padding: 0; - clear: left; - } -select, textarea - { - font-family: Verdana, Arial, Helvetica, san-serif; - font-size: 9px; - font-weight: normal; - display: block; - float: left; - padding-top: 3px; - margin-bottom: 10px; - } -.input - { - font-family: Verdana, Arial, Helvetica, san-serif; - font-size: 10px; - font-weight: normal; - color: #184054; - background-color: #f0f0f0; - border: 1px solid #999999; - border-bottom: 1px solid #cccccc; - border-right: 1px solid #cccccc; - width:400px; - } - -.submit - { - cursor:default; - width:60px; - font-family: Verdana, Arial, Helvetica, san-serif; - font-size: 10px; - font-weight: bold; - background-color: #f0f0f0; - height: 20px; - padding: 0px; - padding-bottom: 1px; - margin: 0px; - border: 1px solid #cccccc; - border-bottom: 1px solid #666666; - border-right: 1px solid #666666; - } - -input.important - { - background-color:#B81833; - color:#ffffff; - } - -select - { - color: #184054; - background-color: #f0f0f0; - border: 0px solid #999999; - } -textarea - { - color: #184054; - background-color: #f0f0f0; - width: 234px; - height: 100px; - border: 1px solid #999999; - border-bottom: 1px solid #cccccc; - border-right: 1px solid #cccccc; - } -.clearleft - { - clear: left; - } -.clearboth - { - clear: both; - } - -.checkradio - { - background-color: #ffffff; - width: 20px; - padding: 0px; - padding-bottom: 10px; - margin: 0px; - margin-top: 2px; - border: 0px solid #999999; - } - -.checkradiotext - { - font-size: 9px; - font-weight: normal; - line-height: 11px; - width: 100px; - color: #000000; - padding: 0px; - padding-bottom: 10px; - margin: 0px; - margin-top: 4px; - border: 0px solid #999999; - } \ No newline at end of file diff --git a/contrib/tck-webapp/src/webapp/graph.jsp b/contrib/tck-webapp/src/webapp/graph.jsp deleted file mode 100644 index cb5b99382ba..00000000000 --- a/contrib/tck-webapp/src/webapp/graph.jsp +++ /dev/null @@ -1,249 +0,0 @@ -<%-- -Copyright 2004-2005 The Apache Software Foundation or its licensors, - as applicable. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---%><%@ page import="javax.jcr.Session, - javax.jcr.Node, - javax.jcr.NodeIterator, - junit.framework.Test, - junit.framework.TestSuite, - java.util.*, - junit.framework.TestCase, - javax.jcr.Property, - java.text.SimpleDateFormat, - org.apache.jackrabbit.tck.*, - org.apache.jackrabbit.tck.j2ee.RepositoryServlet" -%><%@page session="false" %><% - -// get the repository session for read(config and test results) and write (config and test results) access -Session repSession = RepositoryServlet.getSession(); -if (repSession == null) { - return; -} - -// get path from jar where the test sources are stored -String TEST_JCR_PATH = "/WEB-INF/lib/tck-webapp-0.1.jar"; - -// display mode: -// - testnow : new test -// - view: view results -// - null: blank view -String mode = request.getParameter("mode"); - -%> - - - - - - <% - // draw empty test grid - if (mode == null || (mode != null && mode.equals("testnow"))) { - // prepare test - TestFinder tf = new TestFinder(); - tf.find(getServletConfig().getServletContext().getResource(TEST_JCR_PATH).openStream(), - "TestAll.java"); - Iterator tests = tf.getSuites().keySet().iterator(); - - out.write(""); - tests = tf.getSuites().keySet().iterator(); - - while (tests.hasNext()) { - String key = (String) tests.next(); - TestSuite t = (TestSuite) tf.getSuites().get(key); - Enumeration members = t.tests(); - out.write(""); - - // list tests ordered by key (level1, level2, ....) - while (members.hasMoreElements()) { - TestSuite aTest = (TestSuite) members.nextElement(); - - out.write(""); - } - if (tests.hasNext()) { - out.write(""); - } - } - - out.write("
    " + t.toString() + "Config
    " + - aTest.toString() + " "); - - Enumeration testMethods = aTest.tests(); - while (testMethods.hasMoreElements()) { - TestCase tc = (TestCase) testMethods.nextElement(); - String methodname = tc.getName(); - - String id = methodname + "(" + aTest.getName() + ")"; - out.write(" "); - } - out.write("
     
    "); - } - - // start testing or show results - if (mode != null && mode.equals("testnow")) { - // read and save test configuration - WebAppTestConfig.save(request, repSession); - - // start testing - Node rootNode = repSession.getRootNode(); - Node testResNode = (rootNode.hasNode("testing")) ? - rootNode.getNode("testing") : rootNode.addNode("testing", "nt:unstructured"); - rootNode.save(); - - out.write(""); - TestFinder testfinder = new TestFinder(); - testfinder.find(getServletConfig().getServletContext().getResource(TEST_JCR_PATH).openStream(), - "TestAll.java"); - TckTestRunner runner = new TckTestRunner(out); - String logStr = ""; - runner.setLogString(logStr); - String interAStr = ""; - runner.setInteractionString(interAStr); - runner.setNewTestString(""); - Tester t = new Tester(testfinder, runner, out); - t.setfinishedSuiteString(""); - long startMillies = System.currentTimeMillis(); - t.run(); - Node results = testResNode.addNode(String.valueOf(startMillies)); - testResNode.save(); - t.storeResults(results); - out.write(""); - out.write(""); - out.write(""); - } else if (mode != null && mode.equals("view")) { - out.write(""); - - TestFinder tf = new TestFinder(); - tf.find(getServletConfig().getServletContext().getResource(TEST_JCR_PATH).openStream(), "TestAll.java"); - - // the test to be viewed is defined by the timestamp - String testTimeInMs = request.getParameter("test"); - - // load "test root" - Node testroot = repSession.getRootNode().getNode("testing/" + testTimeInMs); - - Calendar cal = Calendar.getInstance(); - cal.setTimeInMillis(Long.parseLong(testTimeInMs)); - SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy hh:mm:ss"); - String formatedTestTime = formatter.format(cal.getTime()); - - out.write(""); - out.write(""); - out.write("
    Test: " + formatedTestTime + "
    "); - - out.write(""); - out.write(""); - - // display the test result categorized by the test suites: - // - level1 - // - level2 - // - ..... - Iterator tests = tf.getSuites().keySet().iterator(); - - while (tests.hasNext()) { - String key = (String) tests.next(); - TestSuite t = (TestSuite) tf.getSuites().get(key); - Enumeration members = t.tests(); - out.write(""); - while (members.hasMoreElements()) { - TestSuite aTest = (TestSuite) members.nextElement(); - - out.write(""); - } - if (tests.hasNext()) { - out.write(""); - } - } - - out.write("
     
    " + t.toString() + "
    " + - aTest.toString() + " "); - - Enumeration testMethods = aTest.tests(); - while (testMethods.hasMoreElements()) { - TestCase tc = (TestCase) testMethods.nextElement(); - String methodname = tc.getName(); - - // test identifier - String keyname = methodname + "(" + aTest.getName() + ")"; - - // load node containig the test result for one test - Node testResultNode; - if (testroot.hasNode(key + "/" + keyname)) { - testResultNode = testroot.getNode(key + "/" + keyname); - } else { - continue; - } - - int status = new Long(testResultNode.getProperty("status").getLong()).intValue(); - String color; - switch (status) { - case TestResult.SUCCESS: - color = "pass"; - break; - case TestResult.ERROR: - case TestResult.FAILURE: - color = "failure"; - break; - case TestResult.NOT_EXECUTABLE: - color = "error"; - break; - default: - color = "clear"; - } - - String testTime = (testResultNode.hasProperty("testtime")) ? - String.valueOf(testResultNode.getProperty("testtime").getLong()) : "0"; - - String errorMsg = (testResultNode.hasProperty("errrormsg")) ? - "Error: " + testResultNode.getProperty("errrormsg").getString() : ""; - errorMsg = errorMsg.replaceAll("'"," "); - errorMsg = errorMsg.replaceAll("\""," "); - errorMsg = errorMsg.replaceAll("\n"," "); - errorMsg = errorMsg.replaceAll("\r"," "); - - String testInfo = "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" + - "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    " + - "Test name: " + methodname + "(" + aTest.getName() + ")
    " + - "Time: " + testTime + "ms
    " + errorMsg + "
    "; - - out.write(" "); - } - out.write("
     
    "); - out.write(""); - out.write(""); - } - %> - - \ No newline at end of file diff --git a/contrib/tck-webapp/src/webapp/index.jsp b/contrib/tck-webapp/src/webapp/index.jsp deleted file mode 100644 index cca418f7f40..00000000000 --- a/contrib/tck-webapp/src/webapp/index.jsp +++ /dev/null @@ -1,69 +0,0 @@ -<%-- -Copyright 2004-2005 The Apache Software Foundation or its licensors, - as applicable. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---%><%@ page import="javax.jcr.Session, - java.util.List, - java.util.ArrayList" -%><%@page session="false" %><% - - String parent = request.getRequestURI(); - if (parent.length() > 1) { - parent = parent.substring(0,parent.lastIndexOf('/')); - } - -%> - TCK for JSR170 - - - - -
    - - - - - - - - - - - - - - -
    TCK for JSR 170
    Content Repository Standard
    - -
    - -
    - -
    -
    - - \ No newline at end of file diff --git a/contrib/tck-webapp/src/webapp/status.jsp b/contrib/tck-webapp/src/webapp/status.jsp deleted file mode 100644 index a3ac44b59cf..00000000000 --- a/contrib/tck-webapp/src/webapp/status.jsp +++ /dev/null @@ -1,24 +0,0 @@ -<%-- -Copyright 2004-2005 The Apache Software Foundation or its licensors, - as applicable. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---%><%@page session="false" %><% - -%> - - - - - - diff --git a/contrib/vfs/05-03-03-repository.xml b/contrib/vfs/05-03-03-repository.xml deleted file mode 100644 index 05cca38b8ba..00000000000 --- a/contrib/vfs/05-03-03-repository.xml +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/vfs/README.txt b/contrib/vfs/README.txt deleted file mode 100644 index eb0e5d74409..00000000000 --- a/contrib/vfs/README.txt +++ /dev/null @@ -1,14 +0,0 @@ -For testing you can use tmp provider. This provider will delete -all the files and folders on repository shutdown. - -The providers configuration file (providers.xml) must be in the classpath. - -repository.xml Example: - - - - - - - - diff --git a/contrib/vfs/src/java/org/apache/jackrabbit/core/fs/vfs/VFSFileSystem.java b/contrib/vfs/src/java/org/apache/jackrabbit/core/fs/vfs/VFSFileSystem.java deleted file mode 100644 index 3298c7ff5a5..00000000000 --- a/contrib/vfs/src/java/org/apache/jackrabbit/core/fs/vfs/VFSFileSystem.java +++ /dev/null @@ -1,631 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.fs.vfs; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.commons.vfs.AllFileSelector; -import org.apache.commons.vfs.FileObject; -import org.apache.commons.vfs.FileSelector; -import org.apache.commons.vfs.FileSystemException; -import org.apache.commons.vfs.FileType; -import org.apache.commons.vfs.FileUtil; -import org.apache.commons.vfs.RandomAccessContent; -import org.apache.commons.vfs.cache.SoftRefFilesCache; -import org.apache.commons.vfs.impl.StandardFileSystemManager; -import org.apache.commons.vfs.util.RandomAccessMode; -import org.apache.jackrabbit.core.fs.FileSystem; -import org.apache.jackrabbit.core.fs.RandomAccessOutputStream; - -/** - * FileSystem backed by Commons VFS - * - * @author Edgar Poce - */ -public class VFSFileSystem implements FileSystem -{ - /** - * Logger - */ - private Log log = LogFactory.getLog(VFSFileSystem.class); - - /** - * File selector - */ - public final static FileSelector ALL = new AllFileSelector() ; - - /** - * VFS manager - */ - StandardFileSystemManager fsManager; - - /** - * Scheme - */ - private String prefix; - - /** - * Path - */ - private String path; - - /** - * The config file - */ - private String config; - - /** - * - */ - public VFSFileSystem() - { - super(); - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#init() - */ - public void init() throws org.apache.jackrabbit.core.fs.FileSystemException - { - - if (this.path == null) - { - String msg = "Path is not set"; - log.error(msg); - throw new org.apache.jackrabbit.core.fs.FileSystemException(msg); - } - - if (this.config == null) - { - String msg = "Configuration file name is not set (\"config\" parameter )."; - log.error(msg); - throw new org.apache.jackrabbit.core.fs.FileSystemException(msg); - } - - try - { - // Init file system - fsManager = new StandardFileSystemManager(); - - // Set class loader for resource retrieval - fsManager.setClassLoader(this.getClass().getClassLoader()); - - // Configuration file name - fsManager.setConfiguration(this.getClass().getClassLoader() - .getResource(this.config).toExternalForm()); - - // Set the logger - fsManager.setLogger(log); - - // Cache strategy - // FIXME: set through configuration - fsManager.setFilesCache(new SoftRefFilesCache()); - fsManager.init(); - - // Set the base folder - FileObject fo = fsManager - .resolveFile(this.prefix + ":" + this.path); - fsManager.setBaseFile(fo); - - } catch (FileSystemException e) - { - String msg = "Unable to init VFS FileSystem"; - log.error(msg, e); - throw new org.apache.jackrabbit.core.fs.FileSystemException(msg, e); - } - - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#close() - */ - public void close() - throws org.apache.jackrabbit.core.fs.FileSystemException - { - this.fsManager.close() ; - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#getInputStream(java.lang.String) - */ - public InputStream getInputStream(String filePath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject file = this.getFile(filePath); - this.validateFile(file); - return file.getContent().getInputStream(); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#getOutputStream(java.lang.String) - */ - public OutputStream getOutputStream(String filePath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject file = this.getFile(filePath); - if (!file.exists()) - { - file.createFile(); - } - this.validateFile(file); - return file.getContent().getOutputStream(); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#getRandomAccessOutputStream(java.lang.String) - */ - public RandomAccessOutputStream getRandomAccessOutputStream(String filePath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject file = this.getFile(filePath); - this.validateFile(file); - RandomAccessContent raf = file.getContent().getRandomAccessContent( - RandomAccessMode.READWRITE); - return new VFSRAFOutputStream(raf); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#createFolder(java.lang.String) - */ - public void createFolder(String folderPath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject folder = this.getFile(folderPath); - folder.createFolder(); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#exists(java.lang.String) - */ - public boolean exists(String path) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - return this.getFile(path).exists(); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#isFile(java.lang.String) - */ - public boolean isFile(String path) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - return this.getFile(path).getType().equals(FileType.FILE); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#isFolder(java.lang.String) - */ - public boolean isFolder(String path) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - return this.getFile(path).getType().equals(FileType.FOLDER); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#hasChildren(java.lang.String) - */ - public boolean hasChildren(String path) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - return this.getFile(path).getChildren().length > 0; - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#length(java.lang.String) - */ - public long length(String filePath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject file = this.getFile(filePath); - this.validateFile(file); - return file.getContent().getSize(); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#lastModified(java.lang.String) - */ - public long lastModified(String path) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - return this.getFile(path).getContent().getLastModifiedTime(); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#touch(java.lang.String) - */ - public void touch(String filePath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject file = this.getFile(filePath); - this.validateFile(file); - file.getContent().setLastModifiedTime( - Calendar.getInstance().getTimeInMillis()); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#list(java.lang.String) - */ - public String[] list(String folderPath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - return this.list(this.getFile(folderPath), null); - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#listFiles(java.lang.String) - */ - public String[] listFiles(String folderPath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - return this.list(this.getFile(folderPath), FileType.FILE); - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#listFolders(java.lang.String) - */ - public String[] listFolders(String folderPath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - return this.list(this.getFile(folderPath), FileType.FOLDER); - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#deleteFile(java.lang.String) - */ - public void deleteFile(String filePath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject file = this.getFile(filePath); - this.validateFile(file); - this.delete(file); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#deleteFolder(java.lang.String) - */ - public void deleteFolder(String folderPath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject folder = this.getFile(folderPath); - this.validateFolder(folder); - this.delete(folder); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#move(java.lang.String, - * java.lang.String) - */ - public void move(String srcPath, String destPath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject src = this.getFile(srcPath); - FileObject dest = this.getFile(destPath); - src.moveTo(dest); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.FileSystem#copy(java.lang.String, - * java.lang.String) - */ - public void copy(String srcPath, String destPath) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - FileObject src = this.getFile(srcPath); - FileObject dest = this.getFile(destPath); - FileUtil.copyContent(src, dest); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } catch (IOException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /** - * Gets the FileObject for the given path - * - * @param path - * @return FileSystem - * @throws FileSystemException - */ - private FileObject getFile(String path) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - if (path.startsWith("/")) { - path = path.substring(1, path.length()) ; - } - return fsManager.resolveFile(path); - } catch (FileSystemException e) - { - String msg = "Unable to get file " + path; - log.error(msg, e); - throw new org.apache.jackrabbit.core.fs.FileSystemException(msg, e); - } - } - - /** - * Validates Folder Type - * - * @param folder - * @throws FileSystemException - */ - private void validateFolder(FileObject folder) throws FileSystemException - { - if (!folder.getType().equals(FileType.FOLDER)) - { - String msg = folder.getName().getPath() - + " does not denote a folder"; - log.error(msg); - throw new FileSystemException(msg); - } - } - - /** - * Validates File Type - * - * @param folder - * @throws FileSystemException - */ - private void validateFile(FileObject file) throws FileSystemException - { - if (!file.getType().equals(FileType.FILE)) - { - String msg = file.getName().getPath() + " does not denote a file"; - log.error(msg); - throw new FileSystemException(msg); - } - } - - /** - * List the children for the given Type. - * - * @param path - * @param type - * @return - * @throws org.apache.jackrabbit.core.fs.FileSystemException - */ - private String[] list(FileObject folder, FileType type) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - this.validateFolder(folder); - FileObject[] fo = folder.getChildren(); - Collection c = new ArrayList(); - for (int i = 0; i < fo.length; i++) - { - if (type == null) - { - c.add(fo[i].getName().getBaseName()); - } else - { - if (fo[i].getType().equals(type)) - { - c.add(fo[i].getName().getBaseName()); - } - } - } - return (String[]) c.toArray(new String[c.size()]); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - /** - * Deletes the given File - */ - private void delete(FileObject file) - throws org.apache.jackrabbit.core.fs.FileSystemException - { - try - { - file.delete(ALL); - } catch (FileSystemException e) - { - throw new org.apache.jackrabbit.core.fs.FileSystemException(e); - } - } - - public String getPath() - { - return path; - } - - public void setPath(String path) - { - this.path = path; - } - - /** - * Makes a file canonical - */ - public static File getCanonicalFile(final File file) - { - try - { - return file.getCanonicalFile(); - } catch (IOException e) - { - return file.getAbsoluteFile(); - } - } - - public String getConfig() - { - return config; - } - - public void setConfig(String config) - { - this.config = config; - } - - /** - * @return Returns the scheme. - */ - public String getPrefix() - { - return prefix; - } - - /** - * @param scheme - * The scheme to set. - */ - public void setPrefix(String prefix) - { - this.prefix = prefix; - } -} \ No newline at end of file diff --git a/contrib/vfs/src/java/org/apache/jackrabbit/core/fs/vfs/VFSRAFOutputStream.java b/contrib/vfs/src/java/org/apache/jackrabbit/core/fs/vfs/VFSRAFOutputStream.java deleted file mode 100644 index ec06b5c239d..00000000000 --- a/contrib/vfs/src/java/org/apache/jackrabbit/core/fs/vfs/VFSRAFOutputStream.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.fs.vfs; - -import java.io.IOException; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.commons.vfs.RandomAccessContent; -import org.apache.jackrabbit.core.fs.RandomAccessOutputStream; - -/** - * Wrapper of VFS output stream on a random access file. - * - * @author Edgar Poce - */ -class VFSRAFOutputStream extends RandomAccessOutputStream -{ - - private Log log = LogFactory.getLog(VFSRAFOutputStream.class); - - /** - * The default size of the write buffer in bytes. - */ - static final int DEFAULT_BUFFER_SIZE = 1024; - - /** - * The write buffer. - */ - private final byte[] buffer; - - /** - * The underlying RandomAccessContent. - */ - protected RandomAccessContent rac; - - /** - * The starting position of the buffer in the code. - */ - private long bufferStart; - - /** - * The end of valid data in the buffer. - */ - private int bufferEnd; - - /** - * Dummy buffer for {@link #write(int)}. - */ - private byte[] one = new byte[1]; - - /** - * Constructor - */ - public VFSRAFOutputStream(RandomAccessContent rac, int size) - { - super(); - this.rac = rac; - this.buffer = new byte[size]; - try - { - bufferStart = rac.getFilePointer(); - } catch (IOException e) - { - log.error("Unable to get file pointer"); - } - } - - /** - * Constructor - */ - public VFSRAFOutputStream(RandomAccessContent rac) - { - this(rac, DEFAULT_BUFFER_SIZE); - } - - /* - * (non-Javadoc) - * - * @see org.apache.jackrabbit.core.fs.RandomAccessOutputStream#seek(long) - */ - public void seek(long position) throws IOException - { - flush(); - rac.seek(position); - bufferStart = position; - } - - /* - * (non-Javadoc) - * - * @see java.io.OutputStream#write(int) - */ - public void write(int b) throws IOException - { - one[0] = (byte) b; - write(one, 0, 1); - } - - public void close() throws IOException - { - flush(); - rac.close(); - rac = null; - } - - public void flush() throws IOException - { - rac.write(buffer, 0, bufferEnd); - bufferEnd = 0; - bufferStart = rac.getFilePointer(); - } - - public void write(byte b[], int off, int len) throws IOException - { - if (len > buffer.length - bufferEnd) - { - flush(); - rac.write(b, off, len); - } else - { - System.arraycopy(b, off, buffer, bufferEnd, len); - bufferEnd += len; - } - } - - public void write(byte[] b) throws IOException - { - write(b, 0, b.length); - } -} \ No newline at end of file diff --git a/contrib/vfs/src/java/providers.xml b/contrib/vfs/src/java/providers.xml deleted file mode 100644 index 8b57883d3ef..00000000000 --- a/contrib/vfs/src/java/providers.xml +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/contrib/vfs/src/test/org/apache/jackrabbit/core/fs/vfs/VFSFileSystemTest.java b/contrib/vfs/src/test/org/apache/jackrabbit/core/fs/vfs/VFSFileSystemTest.java deleted file mode 100644 index 5be1ff0f2f9..00000000000 --- a/contrib/vfs/src/test/org/apache/jackrabbit/core/fs/vfs/VFSFileSystemTest.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.core.fs.vfs; - -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Arrays; - -import org.apache.jackrabbit.core.fs.RandomAccessOutputStream; -import org.apache.jackrabbit.test.JUnitTest; - -/** - *

    - * FileSystem backed by Commons VFS Tests - *

    - * - *

    - * In order to run the following VM arguments must be set: - *

      - *
    • fs.prefix = [provider prefix]
    • - *
    • fs.path = [root path]
    • - *
    • fs.config = [providers config file]
    • - *
    - * - * @author Edgar Poce - */ -public class VFSFileSystemTest extends JUnitTest -{ - private static final String PREFIX = "fs.prefix"; - - private static final String PATH = "fs.path"; - - private static final String CONFIG = "fs.config"; - - private static final String TEST_FOLDER = "testFolder1"; - - private static final String TEST_FILE = "testFile1.txt"; - - private static final String TEST_FOLDER2 = TEST_FOLDER + "/testFolder2"; - - private static final String TEST_FILE2 = TEST_FOLDER2 + "/testFile2.txt"; - - private static final String TEST_FILE3 = TEST_FOLDER2 + "/testFile3.txt"; - - /** - * VFS fs - */ - private VFSFileSystem fs; - - /* - * (non-Javadoc) - * - * @see junit.framework.TestCase#setUp() - */ - protected void setUp() throws Exception - { - super.setUp(); - fs = new VFSFileSystem(); - fs.setPrefix(System.getProperty(PREFIX)); - fs.setPath(System.getProperty(PATH)); - fs.setConfig(System.getProperty(CONFIG)); - fs.init(); - } - - /* - * (non-Javadoc) - * - * @see junit.framework.TestCase#tearDown() - */ - protected void tearDown() throws Exception - { - super.tearDown(); - fs.close(); - fs = null; - } - - public void testFileSystem() throws Exception - { - /* - * Create folder - */ - // depth = 0 - fs.createFolder(TEST_FOLDER); - assertTrue(fs.exists(TEST_FOLDER)); - assertTrue(fs.isFolder(TEST_FOLDER)); - // depth = 1 - fs.createFolder(TEST_FOLDER2); - assertTrue(fs.exists(TEST_FOLDER2)); - assertTrue(fs.isFolder(TEST_FOLDER2)); - // Children - assertTrue(fs.hasChildren(TEST_FOLDER)); - assertTrue(fs.list(TEST_FOLDER)[0].equals(getName(TEST_FOLDER2))); - assertTrue(fs.listFiles(TEST_FOLDER).length == 0); - assertTrue(fs.listFolders(TEST_FOLDER)[0].equals(getName(TEST_FOLDER2))); - - /* - * Create file - */ - // depth = 1 - byte[] write = "hello world".getBytes(); - byte[] read = new byte[write.length]; - - OutputStream out = fs.getOutputStream(TEST_FILE); - out.write(write); - out.flush(); - out.close(); - assertTrue(fs.exists(TEST_FILE)); - fs.getInputStream(TEST_FILE).read(read); - assertTrue(Arrays.equals(write, read)); - // depth = 2 - out = fs.getOutputStream(TEST_FILE2); - out.write(write); - out.flush(); - out.close(); - assertTrue(fs.exists(TEST_FILE2)); - InputStream in = fs.getInputStream(TEST_FILE2); - in.read(read); - in.close(); - assertTrue(Arrays.equals(write, read)); - - /* - * Delete file - */ - fs.deleteFile(TEST_FILE2); - assertFalse(fs.exists(TEST_FILE2)); - - /* - * Delete folder - */ - fs.deleteFolder(TEST_FOLDER2); - assertFalse(fs.exists(TEST_FOLDER2)); - - /* - * Copy file - */ - fs.copy(TEST_FILE, TEST_FILE2); - assertTrue(fs.exists(TEST_FILE2)); - assertTrue(fs.isFile(TEST_FILE2)); - - /* - * Move file - */ - fs.move(TEST_FILE2, TEST_FILE3); - assertFalse(fs.exists(TEST_FILE2)); - assertTrue(fs.exists(TEST_FILE3)); - assertTrue(fs.isFile(TEST_FILE3)); - - /* Radom access content */ - RandomAccessOutputStream rout = fs - .getRandomAccessOutputStream(TEST_FILE); - rout.seek(100); - rout.write(10); - rout.flush(); - rout.close(); - - in = fs.getInputStream(TEST_FILE); - in.skip(100); - assertTrue(in.read() == 10); - in.close(); - - } - - /** - * Get the name - * - * @param path - * @return - */ - private String getName(String path) - { - return path.substring(path.lastIndexOf("/") + 1, path.length()); - } - -} \ No newline at end of file diff --git a/examples/jackrabbit-firsthops/pom.xml b/examples/jackrabbit-firsthops/pom.xml new file mode 100644 index 00000000000..aba9b44e573 --- /dev/null +++ b/examples/jackrabbit-firsthops/pom.xml @@ -0,0 +1,90 @@ + + + + 4.0.0 + + org.apache.jackrabbit + jackrabbit-firsthops + 0.1-SNAPSHOT + First Hops + First Hops Example Page + http://jackrabbit.apache.org/first-hops.html + + + + + + javax.jcr + jcr + 2.0 + + + + + org.apache.jackrabbit + jackrabbit-core + 2.12.1 + + + + + + + + + org.slf4j + slf4j-log4j12 + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.5 + 1.5 + 1.5 + true + false + true + false + + + + + + + + + diff --git a/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/FirstHop.java b/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/FirstHop.java new file mode 100644 index 00000000000..23ef4daed09 --- /dev/null +++ b/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/FirstHop.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.firsthops; + +import javax.jcr.GuestCredentials; +import javax.jcr.Repository; +import javax.jcr.Session; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * First hop example. Logs in to a content repository and prints a status + * message. + */ +public class FirstHop { + + /** + * The main entry point of the example application. + * + * @param args + * command line arguments (ignored) + * @throws Exception + * if an error occurs + */ + public static void main(String[] args) throws Exception { + Repository repository = JcrUtils.getRepository(); + Session session = repository.login(new GuestCredentials()); + try { + String user = session.getUserID(); + String name = repository.getDescriptor(Repository.REP_NAME_DESC); + System.out.println("Logged in as " + user + " to a " + name + + " repository."); + } finally { + session.logout(); + } + } +} diff --git a/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/SecondHop.java b/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/SecondHop.java new file mode 100644 index 00000000000..61a3bdeafaf --- /dev/null +++ b/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/SecondHop.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.firsthops; + +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Node; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Second hop example. Stores, retrieves, and removes example content. + */ +public class SecondHop { + + /** + * The main entry point of the example application. + * + * @param args + * command line arguments (ignored) + * @throws Exception + * if an error occurs + */ + public static void main(String[] args) throws Exception { + Repository repository = JcrUtils.getRepository(); + Session session = repository.login(new SimpleCredentials("admin", + "admin".toCharArray())); + try { + Node root = session.getRootNode(); + + // Store content + Node hello = root.addNode("hello"); + Node world = hello.addNode("world"); + world.setProperty("message", "Hello, World!"); + session.save(); + + // Retrieve content + Node node = root.getNode("hello/world"); + System.out.println(node.getPath()); + System.out.println(node.getProperty("message").getString()); + + // Remove content + root.getNode("hello").remove(); + session.save(); + } finally { + session.logout(); + } + } + +} diff --git a/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/ThirdHop.java b/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/ThirdHop.java new file mode 100644 index 00000000000..79b20f95929 --- /dev/null +++ b/examples/jackrabbit-firsthops/src/main/java/org/apache/jackrabbit/firsthops/ThirdHop.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.firsthops; + +import javax.jcr.*; +import java.io.FileInputStream; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Third Jackrabbit example application. Imports an example XML file and outputs + * the contents of the entire workspace. + */ +public class ThirdHop { + + /** + * The main entry point of the example application. + * + * @param args + * command line arguments (ignored) + * @throws Exception + * if an error occurs + */ + public static void main(String[] args) throws Exception { + Repository repository = JcrUtils.getRepository(); + Session session = repository.login(new SimpleCredentials("admin", + "admin".toCharArray())); + + FileInputStream xml = new FileInputStream("src/main/resources/test.xml"); + try { + Node root = session.getRootNode(); + + // Import the XML file unless already imported + if (!root.hasNode("importxml")) { + System.out.print("Importing xml... "); + // Create an unstructured node under which to import the XML + Node node = root.addNode("importxml", "nt:unstructured"); + // Import the file "test.xml" under the created node + session.importXML(node.getPath(), xml, + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + + session.save(); + System.out.println("done."); + } + + dump(root); + } finally { + session.logout(); + } + } + + /** Recursively outputs the contents of the given node. */ + private static void dump(Node node) throws RepositoryException { + // First output the node path + System.out.println(node.getPath()); + // Skip the virtual (and large!) jcr:system subtree + if (node.getName().equals("jcr:system")) { + return; + } + + // Then output the properties + PropertyIterator properties = node.getProperties(); + while (properties.hasNext()) { + Property property = properties.nextProperty(); + if (property.getDefinition().isMultiple()) { + // A multi-valued property, print all values + Value[] values = property.getValues(); + for (int i = 0; i < values.length; i++) { + System.out.println(property.getPath() + " = " + + values[i].getString()); + } + } else { + // A single-valued property + System.out.println(property.getPath() + " = " + + property.getString()); + } + } + + // Finally output all the child nodes recursively + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + dump(nodes.nextNode()); + } + } + +} diff --git a/examples/jackrabbit-firsthops/src/main/resources/log4j.properties b/examples/jackrabbit-firsthops/src/main/resources/log4j.properties new file mode 100644 index 00000000000..c8e9cb77d23 --- /dev/null +++ b/examples/jackrabbit-firsthops/src/main/resources/log4j.properties @@ -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. + +log4j.rootLogger=ERROR, Console + +log4j.appender.Console=org.apache.log4j.ConsoleAppender +log4j.appender.Console.layout=org.apache.log4j.PatternLayout +log4j.appender.Console.layout.ConversionPattern=%d %p %c - %m%n diff --git a/examples/jackrabbit-firsthops/src/main/resources/test.xml b/examples/jackrabbit-firsthops/src/main/resources/test.xml new file mode 100644 index 00000000000..fdc026199ee --- /dev/null +++ b/examples/jackrabbit-firsthops/src/main/resources/test.xml @@ -0,0 +1,68 @@ + + + + Three Namespaces + + + An Ellipse and a Rectangle + + + + + The equation for ellipses + + + + 1 + + + + + + + x + 2 + + + + a + 2 + + + + + + + y + 2 + + + + b + 2 + + + + + + + Last Modified January 10, 2002 + + diff --git a/jackrabbit-api/README.txt b/jackrabbit-api/README.txt new file mode 100644 index 00000000000..c5b53a35cf9 --- /dev/null +++ b/jackrabbit-api/README.txt @@ -0,0 +1,8 @@ +========================= +Welcome to Jackrabbit API +========================= + +This is the API component of the Apache Jackrabbit project. +This component contains the interface extensions that Apache +Jackrabbit supports in addition to the standard JCR API. You can +use these interfaces to access Jackrabbit-specific functionality. diff --git a/jackrabbit-api/pom.xml b/jackrabbit-api/pom.xml new file mode 100644 index 00000000000..e461edefbd5 --- /dev/null +++ b/jackrabbit-api/pom.xml @@ -0,0 +1,74 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-api + Apache Jackrabbit API + Jackrabbit-specific extensions to the JCR API + bundle + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.rat + apache-rat-plugin + + + .checkstyle + + + + + + + + + javax.jcr + jcr + + + org.osgi + org.osgi.annotation + provided + + + + + org.jetbrains + annotations + 16.0.2 + + + diff --git a/jackrabbit-api/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-api/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-api/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitNode.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitNode.java new file mode 100644 index 00000000000..0895f9cc667 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitNode.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.api; + +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +/** + * The Jackrabbit Node interface. This interface contains the + * Jackrabbit-specific extensions to the JCR {@link javax.jcr.Node} interface. + */ +public interface JackrabbitNode { + + /** + * + * @param newName + * @throws javax.jcr.RepositoryException + */ + void rename(String newName) throws RepositoryException; + + /** + * + * @param mixinNames + * @throws NoSuchNodeTypeException + * @throws VersionException + * @throws ConstraintViolationException + * @throws LockException + * @throws RepositoryException + */ + void setMixins(String[] mixinNames) + throws NoSuchNodeTypeException, VersionException, + ConstraintViolationException, LockException, RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitNodeTypeManager.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitNodeTypeManager.java new file mode 100644 index 00000000000..9a14a3ba880 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitNodeTypeManager.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.api; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * The Jackrabbit node type manager interface. This interface contains the + * Jackrabbit-specific extensions to the JCR {@link NodeTypeManager} interface. + *

    + * Currently Jackrabbit provides a mechanism to register new node types, but + * it is not possible to modify or remove existing node types. + * + * @deprecated Use standard JCR 2.0 API methods defined by + * {@link NodeTypeManager} instead. + */ +public interface JackrabbitNodeTypeManager extends NodeTypeManager { + + /** + * The standard XML content type to be used with XML-formatted + * node type streams. + */ + String TEXT_XML = "text/xml"; + + /** + * The experimental content type for the compact node type definition + * files. + */ + String TEXT_X_JCR_CND = "text/x-jcr-cnd"; + + /** + * Registers node types from the given node type XML stream. + * + * @param in node type XML stream + * @return registered node types + * @throws SAXException if the XML stream could not be read or parsed + * @throws RepositoryException if the node types are invalid or another + * repository error occurs + */ + NodeType[] registerNodeTypes(InputSource in) + throws SAXException, RepositoryException; + + /** + * Registers node types from the given input stream of the given type. + * + * @param in node type stream + * @param contentType type of the input stream + * @return registered node types + * @throws IOException if the input stream could not be read or parsed + * @throws RepositoryException if the node types are invalid or another + * repository error occurs + */ + NodeType[] registerNodeTypes(InputStream in, String contentType) + throws IOException, RepositoryException; + + /** + * Checks if a node type with the given name is registered. + * + * @param name node type name + * @return true if the named node type is registered + * false otherwise + * @throws RepositoryException if an error occurs + */ + boolean hasNodeType(String name) throws RepositoryException; + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitRepository.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitRepository.java new file mode 100644 index 00000000000..9a84715789b --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitRepository.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.api; + +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * The Jackrabbit repository interface. This interface contains the + * Jackrabbit-specific extensions to the JCR {@link Repository} interface. + */ +public interface JackrabbitRepository extends Repository { + + /** + * Key to a boolean descriptor. Returns true if + * and only if user management is supported. + */ + public static final String OPTION_USER_MANAGEMENT_SUPPORTED = "option.user.management.supported"; + + /** + * Key to a boolean descriptor. Returns true if + * and only if principal management is supported. + */ + public static final String OPTION_PRINCIPAL_MANAGEMENT_SUPPORTED = "option.principal.management.supported"; + + /** + * Key to a boolean descriptor. Returns true if + * and only if privilege management is supported. + */ + public static final String OPTION_PRIVILEGE_MANAGEMENT_SUPPORTED = "option.privilege.management.supported"; + + /** + * Equivalent to {@code login(credentials, workspaceName)} except that the returned + * Session instance contains the given extra session attributes in addition to any + * included in the given Credentials instance. Attribute names from the credentials + * and the attribute map must not overlap. In case of an overlap implementation + * may throw an RepositoryException. + *

    + * The attributes are implementation-specific and may affect the behavior of the returned + * session. Unlike credentials attributes, these separately passed session attributes + * are guaranteed not to affect the authentication of the client. + *

    + * An implementation that does not support a particular session attribute is expected + * to ignore it and not make it available through the returned session. A client that + * depends on specific behavior defined by a particular attribute can check whether + * the returned session contains that attribute to verify whether the underlying + * repository implementation supports that feature. + * + * @param credentials the credentials of the user + * @param workspaceName the name of a workspace + * @param attributes implementation-specific session attributes + * @return a valid session for the user to access the repository + * @throws LoginException if authentication or authorization for the specified workspace fails + * @throws NoSuchWorkspaceException if the specified workspace is not recognized + * @throws RepositoryException if another error occurs + */ + Session login(Credentials credentials, String workspaceName, Map attributes) + throws LoginException, NoSuchWorkspaceException, RepositoryException; + + /** + * Shuts down the repository. A Jackrabbit repository instance contains + * a acquired resources and cached data that needs to be released and + * persisted when the repository is no longer used. This method handles + * all these shutdown tasks and must therefore be called by the + * client application once the repository instance is no longer used. + *

    + * Possible errors are logged rather than thrown as exceptions as there + * is little that a client application could do in such a case. + */ + void shutdown(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitRepositoryFactory.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitRepositoryFactory.java new file mode 100644 index 00000000000..c96e5534dc9 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitRepositoryFactory.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.api; + +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import org.apache.jackrabbit.api.management.RepositoryManager; + +/** + * Classes that implement this interface additionally provide management features. + */ +public interface JackrabbitRepositoryFactory extends RepositoryFactory { + + /** + * Get the repository management component. Only the factory that created + * the given repository may retrieve the manager. + * + * @param repository the repository to manage + * @return the manager + */ + RepositoryManager getRepositoryManager(JackrabbitRepository repository) throws RepositoryException; + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitSession.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitSession.java new file mode 100644 index 00000000000..1fe1c1d4c8b --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitSession.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api; + +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Session; +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.jetbrains.annotations.NotNull; + +/** + * Jackrabbit specific extension of the JCR {@link javax.jcr.Session} interface. + */ +public interface JackrabbitSession extends Session { + + /** + * A constant representing the {@code add_property} action string, used to + * determine if this {@code Session} has permission to add a new property. + * + * @see #hasPermission(String, String...) + */ + String ACTION_ADD_PROPERTY = "add_property"; + + /** + * A constant representing the {@code modify_property} action string, used to + * determine if this {@code Session} has permission to modify a property. + * + * @see #hasPermission(String, String...) + */ + String ACTION_MODIFY_PROPERTY = "modify_property"; + + /** + * A constant representing the {@code remove_property} action string, used to + * determine if this {@code Session} has permission to remove a property. + * + * @see #hasPermission(String, String...) + */ + String ACTION_REMOVE_PROPERTY = "remove_property"; + + /** + * A constant representing the {@code remove_node} action string, used to + * determine if this {@code Session} has permission to remove a node. + * + * @see #hasPermission(String, String...) + */ + String ACTION_REMOVE_NODE = "remove_node"; + + /** + * A constant representing the {@code node_type_management} action string, + * used to determine if this {@code Session} has permission to write + * node type information of a node. + * + * @see #hasPermission(String, String...) + */ + String ACTION_NODE_TYPE_MANAGEMENT = "node_type_management"; + + /** + * A constant representing the {@code versioning} action string, + * used to determine if this {@code Session} has permission to perform + * version operations on a node. + * + * @see #hasPermission(String, String...) + */ + String ACTION_VERSIONING = "versioning"; + + /** + * A constant representing the {@code locking} action string, + * used to determine if this {@code Session} has permission to lock or + * unlock a node. + * + * @see #hasPermission(String, String...) + */ + String ACTION_LOCKING = "locking"; + + /** + * A constant representing the {@code read_access_control} action string, + * used to determine if this {@code Session} has permission to read + * access control content at the given path. + * + * @see #hasPermission(String, String...) + */ + String ACTION_READ_ACCESS_CONTROL = "read_access_control"; + + /** + * A constant representing the {@code modify_access_control} action string, + * used to determine if this {@code Session} has permission to modify + * access control content at the given path. + * + * @see #hasPermission(String, String...) + */ + String ACTION_MODIFY_ACCESS_CONTROL = "modify_access_control"; + + /** + * A constant representing the {@code user_management} action string, + * used to determine if this {@code Session} has permission to perform + * user management operations at the given path. + * + * @see #hasPermission(String, String...) + */ + String ACTION_USER_MANAGEMENT = "user_management"; + + /** + * Returns {@code true} if this {@code Session} has permission to + * perform the specified actions at the specified {@code absPath} and + * {@code false} otherwise. + *

    + * The {@code actions} parameter is a list of action strings. Apart + * from the actions defined on {@link Session}, this variant also allows + * to specify the following additional actions to provide better permission + * discovery: + *

      + *
    • {@link + * #ACTION_ADD_PROPERTY {@code add_property}}: If {@code hasPermission(path, + * "add_property")} returns {@code true}, then this {@code Session} has + * permission to add a new property at {@code path}.
    • + *
    • {@link #ACTION_MODIFY_PROPERTY {@code modify_property}}: If + * {@code hasPermission(path, "modify_property")} returns + * {@code true}, then this {@code Session} has permission to change + * a property at {@code path}.
    • + *
    • {@link + * #ACTION_REMOVE_PROPERTY {@code remove_property}}: If {@code hasPermission(path, + * "remove_property")} returns {@code true}, then this {@code Session} has + * permission to remove a property at {@code path}.
    • + *
    • {@link #ACTION_REMOVE_NODE {@code remove_node}}: If + * {@code hasPermission(path, "remove_node")} returns {@code true}, then + * this {@code Session} has permission to remove a node at {@code path}.
    • + *
    • {@link #ACTION_NODE_TYPE_MANAGEMENT {@code node_type_management}}: If + * {@code hasPermission(path, "node_type_management")} returns {@code true}, then + * this {@code Session} has permission to explicitly set or change the node type + * information associated with a node at {@code path}.
    • + *
    • {@link #ACTION_VERSIONING {@code versioning}}: If + * {@code hasPermission(path, "versioning")} returns {@code true}, then + * this {@code Session} has permission to perform version related operations + * on a node at {@code path}.
    • + *
    • {@link #ACTION_LOCKING {@code locking}}: If + * {@code hasPermission(path, "locking")} returns {@code true}, then + * this {@code Session} has permission to lock and unlock a node at {@code path}.
    • + *
    • {@link #ACTION_READ_ACCESS_CONTROL {@code read_access_control}}: If + * {@code hasPermission(path, "read_access_control")} returns {@code true}, then + * this {@code Session} has permission to read access control content stored + * at an item at {@code path}.
    • + *
    • {@link #ACTION_MODIFY_ACCESS_CONTROL {@code modify_access_control}}: If + * {@code hasPermission(path, "modify_access_control")} returns {@code true}, then + * this {@code Session} has permission to modify access control content + * at an item at {@code path}.
    • + *
    • {@link #ACTION_USER_MANAGEMENT {@code user_management}}: If + * {@code hasPermission(path, "user_management")} returns {@code true}, then + * this {@code Session} has permission to perform user management operations + * at an item at {@code path}.
    • + *
    + * + * When more than one action is specified, this method will only return + * {@code true} if this {@code Session} has permission to perform all + * of the listed actions at the specified path. + *

    + * The information returned through this method will only reflect the permission + * status (both JCR defined and implementation-specific) and not + * other restrictions that may exist, such as node type or other + * implementation enforced constraints. For example, even though + * {@code hasPermission} may indicate that a particular {@code Session} may + * add a property at {@code /A/B/C}, the node type of the node at {@code /A/B} + * may prevent the addition of a property called {@code C}. + * + * @param absPath an absolute path. + * @param actions one or serveral actions. + * @return {@code true} if this {@code Session} has permission to + * perform the specified actions at the specified + * {@code absPath}. + * @throws RepositoryException if an error occurs. + * @see Session#hasPermission(String, String) + */ + public boolean hasPermission(@NotNull String absPath, @NotNull String... actions) throws RepositoryException; + + /** + * Returns the PrincipalManager for the current Session. + * + * @return the PrincipalManager associated with this Session. + * @throws AccessDeniedException If the session lacks privileges to access + * the principal manager or principals in general. + * @throws UnsupportedRepositoryOperationException If principal management + * is not supported. + * @throws RepositoryException If another error occurs. + * @see PrincipalManager + */ + PrincipalManager getPrincipalManager() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Returns the UserManager for the current Session. + * + * @return the UserManager associated with this Session. + * @throws javax.jcr.AccessDeniedException If this session is not allowed to + * to access user data. + * @throws UnsupportedRepositoryOperationException If user management is + * not supported. + * @throws javax.jcr.RepositoryException If another error occurs. + * @see UserManager + */ + UserManager getUserManager() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Returns the node at the specified absolute path in the workspace. If no + * such node exists, then it returns the property at the specified path. + * If no such property exists, then it return {@code null}. + * + * @param absPath An absolute path. + * @return the specified {@code Item} or {@code null}. + * @throws RepositoryException if another error occurs. + * @since 2.11.1 + */ + Item getItemOrNull(final String absPath) throws RepositoryException; + + /** + * Returns the property at the specified absolute path in the workspace or + * {@code null} if no such node exists. + * + * @param absPath An absolute path. + * @return the specified {@code Property} or {@code null}. + * @throws RepositoryException if another error occurs. + * @since 2.11.1 + */ + Property getPropertyOrNull(final String absPath) throws RepositoryException; + + /** + * Returns the node at the specified absolute path in the workspace or + * {@code null} if no such node exists. + * + * @param absPath An absolute path. + * @return the specified {@code Node} or {@code null}. + * @throws RepositoryException If another error occurs. + * @since 2.11.1 + */ + Node getNodeOrNull(final String absPath) throws RepositoryException; + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitValue.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitValue.java new file mode 100644 index 00000000000..f458edec81f --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitValue.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.api; + +import javax.jcr.Value; + +/** + * Values returned by Jackrabbit may implement this interface. The interface + * defines optional features. An application should check if the returned value + * is of this type before casting as in: + *

    + * if (v instanceof JackrabbitValue) {
    + *     JackrabbitValue j = (JackrabbitValue) v;
    + *     ....
    + * }
    + * 
    + */ +public interface JackrabbitValue extends Value { + + /** + * Get a unique identifier of the content of this value. Usually this is a + * message digest of the content (a cryptographically secure one-way hash). + * This allows to avoid processing large binary values multiple times. + *

    + * This method returns null if the identifier is unknown. The identifier may + * not always be available, for example if the value has not yet been saved + * or processed. Once an identifier is available, it will never change + * because values are immutable. + *

    + * If two values have the same identifier, the content of the value is + * guaranteed to be the same. However it is not guaranteed that two values + * with the same content will return the same identifier. + *

    + * The identifier is opaque, meaning it can have any format and size, however + * it is at normally about 50 characters and at most 255 characters long. + * The string only contains Unicode code points from 32 to 127 (including). + * + * @return the unique identifier or null + */ + String getContentIdentity(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitValueFactory.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitValueFactory.java new file mode 100644 index 00000000000..e03209a3592 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitValueFactory.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.api; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Binary; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.api.binary.BinaryUpload; +import org.apache.jackrabbit.api.binary.BinaryDownload; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.osgi.annotation.versioning.ProviderType; + +/** + * Defines optional functionality that a {@link ValueFactory} may choose to + * provide. A {@link ValueFactory} may also implement this interface without + * supporting all of the capabilities in this interface. Each method of the + * interface describes the behavior of that method if the underlying capability + * is not available. + *

    + * Currently this interface defines the following optional features: + *

      + *
    • Direct Binary Access - enable a client to upload or download binaries + * directly to/from a storage location + *
    + *

    + * The features are described in more detail below. + * + *

    Direct Binary Access

    + *

    + * The Direct Binary Access feature provides the capability for a client to + * upload or download binaries directly to/from a storage location. For + * example, this might be a cloud storage providing high-bandwidth direct + * network access. This API allows for requests to be authenticated and for + * access permission checks to take place within the repository, but for clients + * to then access the storage location directly. + *

    + * The feature consists of two parts, direct binary upload and direct binary + * download. + *

    + * + *

    Direct Binary Upload

    + *

    + * This feature enables remote clients to upload binaries directly to a storage + * location. + *

    + * When adding binaries already present on the same JVM or server as Jackrabbit + * or Oak, for example because they were generated locally, please use the + * regular JCR API for {@link javax.jcr.Property#setValue(Binary) adding + * binaries through input streams} instead. This feature is solely designed for + * remote clients. + *

    + * The direct binary upload process is split into 3 phases: + *

      + *
    1. + * Initialize: A remote client makes request to the + * Jackrabbit-based application to request an upload, which calls {@link + * #initiateBinaryUpload(long, int)} and returns the resulting {@link + * BinaryUpload information} to the remote client. + *
    2. + *
    3. + * Upload: The remote client performs the actual binary upload + * directly to the binary storage provider. The {@link BinaryUpload} + * returned from the previous call to {@link + * #initiateBinaryUpload(long, int)} contains detailed instructions on + * how to complete the upload successfully. For more information, see + * the BinaryUpload documentation. + *
    4. + *
    5. + * Complete: The remote client notifies the Jackrabbit-based + * application that step 2 is complete. The upload token returned in + * the first step (obtained by calling {@link + * BinaryUpload#getUploadToken()} is passed by the client to {@link + * #completeBinaryUpload(String)}. This will provide the application + * with a regular {@link Binary JCR Binary} that can then be used to + * write JCR content including the binary (such as an nt:file structure) + * and {@link Session#save() persist} it. + *
    6. + *
    + *

    + *

    Direct Binary Download

    + *

    + * The direct binary download process is described in detail in {@link + * BinaryDownload}. + */ +@ProviderType +public interface JackrabbitValueFactory extends ValueFactory { + /** + * Initiate a transaction to upload binary data directly to a storage + * location. {@link IllegalArgumentException} will be thrown if an upload + * cannot be supported for the required parameters, or if the parameters are + * otherwise invalid. For example, if the value of {@code maxSize} exceeds + * the size limits for a single binary upload for the implementation or the + * service provider, or if the value of {@code maxSize} divided by {@code + * maxParts} exceeds the size limit for an upload or upload part of the + * implementation or the service provider, {@link IllegalArgumentException} + * may be thrown. + *

    + * Each service provider has specific limitations on upload sizes, + * multi-part upload support, part sizes, etc. which can result in {@link + * IllegalArgumentException} being thrown. You should consult the + * documentation for your underlying implementation and your service + * provider for details. + *

    + * If this call is successful, a {@link BinaryUpload} is returned + * which contains the information a client needs to successfully complete + * a direct upload. + * + * @param maxSize The expected maximum size of the binary to be uploaded by + * the client. If the actual size of the binary is known, this + * size should be used; otherwise, the client should make a best + * guess. If a client calls this method with one size and then + * later determines that the guess was too small, the transaction + * should be restarted by calling this method again with the correct + * size. + * @param maxURIs The maximum number of upload URIs that the client can + * accept. The implementation will ensure that an upload of size + * {@code maxSize} can be completed by splitting the value of {@code + * maxSize} into parts, such that the size of the largest part does + * not exceed any known implementation or service provider + * limitations on upload part size and such that the number of parts + * does not exceed the value of {@code maxURIs}. If this is not + * possible, {@link IllegalArgumentException} will be thrown. A + * client may specify -1 for this value, indicating that any number + * of URIs may be returned. + * @return A {@link BinaryUpload} that can be used by the client to complete + * the upload via a call to {@link #completeBinaryUpload(String)}, + * or {@code null} if the implementation does not support the direct + * upload feature. + * @throws IllegalArgumentException if the provided arguments are + * invalid or if a valid upload cannot be completed given the + * provided arguments. + * @throws AccessDeniedException if it is determined that insufficient + * permission exists to perform the upload. + */ + @Nullable + BinaryUpload initiateBinaryUpload(long maxSize, int maxURIs) + throws IllegalArgumentException, AccessDeniedException; + + /** + * Complete a transaction to upload binary data directly to a storage + * location. The client must provide a valid {@code uploadToken} that can + * only be obtained via a previous call to {@link + * #initiateBinaryUpload(long, int)}. If the {@code uploadToken} is + * unreadable or invalid, {@link IllegalArgumentException} will be thrown. + *

    + * Calling this method does not associate the returned {@link Binary} with + * any location in the repository. It is the responsibility of the client + * to do this if desired. + *

    + * The {@code uploadToken} can be obtained from the {@link + * BinaryUpload} returned from a prior call to {@link + * #initiateBinaryUpload(long, int)}. Clients should treat the {@code + * uploadToken} as an immutable string, and should expect that + * implementations will sign the string and verify the signature when this + * method is called. + * + * @param uploadToken A String that is used to identify the direct upload + * transaction. + * @return The uploaded {@link Binary}, or {@code null} if the + * implementation does not support the direct upload feature. + * @throws IllegalArgumentException if the {@code uploadToken} is + * unreadable or invalid. + * @throws RepositoryException if a repository access error occurs. + */ + @Nullable + Binary completeBinaryUpload(@NotNull String uploadToken) + throws IllegalArgumentException, RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitWorkspace.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitWorkspace.java new file mode 100644 index 00000000000..fae4eb3911a --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/JackrabbitWorkspace.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.api; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.xml.sax.InputSource; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; + +/** + * The Jackrabbit workspace interface. This interface contains the + * Jackrabbit-specific extensions to the JCR {@link Workspace} interface. + */ +public interface JackrabbitWorkspace extends Workspace { + + /** + * Creates a workspace with the given name. + * + * @param workspaceName name of the new workspace + * @throws AccessDeniedException if the current session is not allowed to + * create the workspace + * @throws RepositoryException if a workspace with the given name + * already exists or if another error occurs + * @see #getAccessibleWorkspaceNames() + */ + void createWorkspace(String workspaceName) + throws AccessDeniedException, RepositoryException; + + /** + * Creates a workspace with the given name and a workspace configuration + * template. + * + * @param workspaceName name of the new workspace + * @param workspaceTemplate the configuration template of the new workspace + * @throws AccessDeniedException if the current session is not allowed to + * create the workspace + * @throws RepositoryException if a workspace with the given name + * already exists or if another error occurs + * @see #getAccessibleWorkspaceNames() + */ + void createWorkspace(String workspaceName, InputSource workspaceTemplate) + throws AccessDeniedException, RepositoryException; + + /** + * Returns the privilege manager. + * + * @return the privilege manager. + * @throws RepositoryException If an error occurs. + */ + PrivilegeManager getPrivilegeManager() throws RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/ReferenceBinary.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/ReferenceBinary.java new file mode 100644 index 00000000000..b4378e2d343 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/ReferenceBinary.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.api; + +import javax.jcr.Binary; + +/** + * Referenceable binary. In addition to the normal JCR {@link Binary} + * functionality, implementations of this class contain a secure + * reference to the storage location of the binary stream. This + * reference can be used to efficiently copy binaries across servers as + * long as both the source and target servers use the same underlying + * storage for binaries. + */ +public interface ReferenceBinary extends Binary { + + /** + * Returns a secure reference to this binary, or {@code null} if such + * a reference is not available. + * + * @return binary reference, or {@code null} + */ + String getReference(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/ReferenceBinaryException.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/ReferenceBinaryException.java new file mode 100644 index 00000000000..4ae0f3b2f9c --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/ReferenceBinaryException.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.api; + +import javax.jcr.RepositoryException; + +public class ReferenceBinaryException extends RepositoryException { + + public ReferenceBinaryException(String message) { + super(message); + } + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/XASession.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/XASession.java new file mode 100644 index 00000000000..efd0b3e7b73 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/XASession.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.api; + +import javax.jcr.Session; +import javax.transaction.xa.XAResource; + +/** + * The XASession interface extends the capability of {@link Session} by adding + * access to a JCR repository's support for the Java Transaction API (JTA). + *

    + * This support takes the form of a {@link javax.transaction.xa.XAResource} + * object. The functionality of this object closely resembles that defined by + * the standard X/Open XA Resource interface. + *

    + * This interface is used by the transaction manager; an application does not + * use it directly. + * + * @since 1.4 + * @deprecated An XA-enabled session should directly implement the + * {@link javax.transaction.xa.XAResource} interface + */ +public interface XASession extends Session { + + /** + * Retrieves an {@link XAResource} object that the transaction manager + * will use to manage this XASession object's participation in + * a distributed transaction. + * + * @return the {@link XAResource} object. + */ + XAResource getXAResource(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryDownload.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryDownload.java new file mode 100644 index 00000000000..cfd629c566e --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryDownload.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.api.binary; + +import java.net.URI; + +import javax.jcr.Binary; +import javax.jcr.RepositoryException; + +import org.jetbrains.annotations.Nullable; +import org.osgi.annotation.versioning.ProviderType; + +/** + * This extension interface provides a mechanism whereby a client can download + * a {@link Binary} directly from a storage location. + */ +@ProviderType +public interface BinaryDownload extends Binary { + /** + * Get a URI for downloading a {@link Binary} directly from a storage + * location with the provided {@link BinaryDownloadOptions}. This is + * probably a signed URI with a short TTL (time to live), although the API + * does not require it to be so. + *

    + * The implementation will attempt to apply the specified {@code + * downloadOptions} to the subsequent download. For example, if the caller + * knows that the URI refers to a specific type of content, the caller can + * specify that content type by setting the internet media type and + * character encoding in the {@code downloadOptions}. The caller may also + * use a default instance obtained via {@link BinaryDownloadOptions#DEFAULT} + * in which case the caller is indicating that the default behavior of the + * service provider is acceptable. + * + * @param downloadOptions + * A {@link BinaryDownloadOptions} instance which is used to + * request specific options on the binary to be downloaded. + * {@link BinaryDownloadOptions#DEFAULT} should be used if the + * caller wishes to accept the service provider's default + * behavior. + * @return A URI for downloading the binary directly, or {@code null} if the + * binary cannot be downloaded directly or if the underlying + * implementation does not support this capability. + * @throws RepositoryException if an error occurs trying to locate the + * binary. + */ + @Nullable + URI getURI(BinaryDownloadOptions downloadOptions) + throws RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryDownloadOptions.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryDownloadOptions.java new file mode 100644 index 00000000000..5a3680e2aab --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryDownloadOptions.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.api.binary; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.osgi.annotation.versioning.ProviderType; + +/** + * Specifies the options to be used when obtaining a direct download URI via + * {@link BinaryDownload#getURI(BinaryDownloadOptions)}. Setting these options + * allows the caller to instruct the service provider that these options should + * be applied to the response to a request made with the URI returned. + *

    + * To specify download options, obtain a {@link BinaryDownloadOptionsBuilder} + * via the {@link #builder()} method, then specify the options desired and + * get the object via {@link BinaryDownloadOptionsBuilder#build()}. + *

    + * If no options are needed, use {@link BinaryDownloadOptions#DEFAULT} which + * instructs the implementation to use the service provider default behavior. + */ +@ProviderType +public final class BinaryDownloadOptions { + private final String mediaType; + private final String characterEncoding; + private final String fileName; + private final String dispositionType; + + private BinaryDownloadOptions(final String mediaType, + final String characterEncoding, + final String fileName, + final String dispositionType) { + this.mediaType = mediaType; + this.characterEncoding = characterEncoding; + this.fileName = fileName; + this.dispositionType = dispositionType; + } + + /** + * Provides a default instance of this class. Using the default instance + * indicates that the caller is willing to accept the service provider + * default behavior. + */ + public static final BinaryDownloadOptions DEFAULT = + BinaryDownloadOptions.builder().build(); + + /** + * Returns the internet media type that should be assumed for the binary that is to be + * downloaded. This value should be a valid {@code jcr:mimeType}. This + * value can be set by calling {@link + * BinaryDownloadOptionsBuilder#withMediaType(String)} when building an + * instance of this class. + * + * @return A String representation of the internet media type, or {@code null} if no + * type has been specified. + * @see + * JCR 2.0 Repository Model - jcr:mimeType + */ + @Nullable + public final String getMediaType() { + return mediaType; + } + + /** + * Returns the character encoding that should be assumed for the binary that + * is to be downloaded. This value should be a valid {@code jcr:encoding}. + * It can be set by calling {@link + * BinaryDownloadOptionsBuilder#withCharacterEncoding(String)} when building an + * instance of this class. + * + * @return The character encoding, or {@code null} if no + * encoding has been specified. + * @see + * JCR 2.0 Repository Model - jcr:encoding + */ + @Nullable + public final String getCharacterEncoding() { + return characterEncoding; + } + + /** + * Returns the filename that should be assumed for the binary that is to be + * downloaded. This value can be set by calling {@link + * BinaryDownloadOptionsBuilder#withFileName(String)} when building an + * instance of this class. + * + * @return The file name, or {@code null} if no + * file name has been specified. + */ + @Nullable + public final String getFileName() { + return fileName; + } + + /** + * Returns the disposition type that should be assumed for the binary that + * is to be downloaded. This value can be set by calling {@link + * BinaryDownloadOptionsBuilder#withDispositionTypeInline()} or {@link + * BinaryDownloadOptionsBuilder#withDispositionTypeAttachment()} when + * building an instance of this class. The default value of this setting is + * "inline". + * + * @return The disposition type. + * @see RFC 6266, Section 4.2 + */ + @NotNull + public final String getDispositionType() { + return dispositionType; + } + + /** + * Returns a {@link BinaryDownloadOptionsBuilder} instance to be used for + * creating an instance of this class. + * + * @return A builder instance. + */ + @NotNull + public static BinaryDownloadOptionsBuilder builder() { + return new BinaryDownloadOptionsBuilder(); + } + + /** + * Used to build an instance of {@link BinaryDownloadOptions} with the + * options set as desired by the caller. + */ + public static final class BinaryDownloadOptionsBuilder { + private String mediaType = null; + private String characterEncoding = null; + private String fileName = null; + private DispositionType dispositionType = DispositionType.INLINE; + + private BinaryDownloadOptionsBuilder() { } + + /** + * Sets the internet media type of the {@link BinaryDownloadOptions} object to be + * built. This value should be a valid {@code jcr:mimeType}. + *

    + * Calling this method has the effect of instructing the service + * provider to set {@code mediaType} as the internet media type + * in the {@code Content-Type} header field of the response to a request + * issued with a URI obtained by calling {@link + * BinaryDownload#getURI(BinaryDownloadOptions)}. This value can be + * later retrieved by calling {@link + * BinaryDownloadOptions#getMediaType()} on the instance returned from a + * call to {@link #build()}. + *

    + * Note that if the internet media type defines a "charset" parameter + * (as many textual types do), the caller may also wish to set the + * character encoding which is done separately. See {@link + * #withCharacterEncoding(String)}. + *

    + * The caller should ensure that the internet media type set is valid; the + * implementation does not perform any validation of this setting. + *

    + * If no internet media type is provided, no {@code Content-Type} header field will be + * specified to the service provider. + * + * @param mediaType The internet media type. + * @return The calling instance. + * @see + * JCR 2.0 Repository Model - jcr:mimeType + */ + @NotNull + public BinaryDownloadOptionsBuilder withMediaType(@NotNull String mediaType) { + this.mediaType = mediaType; + return this; + } + + /** + * Sets the character encoding of the {@link BinaryDownloadOptions} object to be + * built. This value should be a valid {@code jcr:encoding}. + *

    + * Calling this method has the effect of instructing the service + * provider to set {@code charecterEncoding} as the "charset" parameter + * of the content type in the {@code Content-Type} header field of the + * response to a request issued with a URI obtained by calling {@link + * BinaryDownload#getURI(BinaryDownloadOptions)}. This value can be + * later retrieved by calling {@link + * BinaryDownloadOptions#getCharacterEncoding()} on the instance returned by a + * call to {@link #build()}. + *

    + * Note that setting the character encoding only makes sense if the internet media type has + * also been set. See {@link + * #withMediaType(String)}. + *

    + * The caller should ensure that the proper character encoding has been set for + * the internet media type; the implementation does not perform any validation of + * these settings. + * + * @param characterEncoding A String representation of the jcr:encoding. + * @return The calling instance. + * @see + * JCR 2.0 Repository Model - jcr:encoding + */ + @NotNull + public BinaryDownloadOptionsBuilder withCharacterEncoding(@NotNull String characterEncoding) { + this.characterEncoding = characterEncoding; + return this; + } + + /** + * Sets the filename of the {@link BinaryDownloadOptions} object to be + * built. + *

    + * Calling this method has the effect of instructing the service + * provider to set {@code fileName} as the filename in the {@code + * Content-Disposition} header of the response to a request issued with + * a URI obtained by calling {@link + * BinaryDownload#getURI(BinaryDownloadOptions)}. This value can be + * later retrieved by calling {@link + * BinaryDownloadOptions#getFileName()} on the instance returned by a + * call to {@link #build()}. + *

    + * + * @param fileName The filename. + * @return The calling instance. + * @see RFC 6266, Section 4.3 + */ + @NotNull + public BinaryDownloadOptionsBuilder withFileName(@NotNull String fileName) { + this.fileName = fileName; + return this; + } + + /** + * Sets the disposition type of the {@link BinaryDownloadOptions} object + * to be built to {@code inline}. + *

    + * Calling this method has the effect of instructing the service + * provider to set the disposition type in the {@code + * Content-Disposition} header of the response to {@code inline}. This + * value can be later retrieved by calling {@link + * BinaryDownloadOptions#getDispositionType()} on the instance built by + * calling {@link #build()}. + *

    + * If this value is not set, the default value of {@code inline} + * will be used. + * + * @return The calling instance. + */ + @NotNull + public BinaryDownloadOptionsBuilder withDispositionTypeInline() { + dispositionType = DispositionType.INLINE; + return this; + } + + /** + * Sets the disposition type of the {@link BinaryDownloadOptions} object + * to be built to {@code attachment}. + *

    + * Calling this method has the effect of instructing the service + * provider to set the disposition type in the {@code + * Content-Disposition} header of the response to {@code attachment}. + * This value can later be retrieved by calling {@link + * BinaryDownloadOptions#getDispositionType()} on the instance built by + * calling {@link #build()}. + *

    + * If this value is not set, the default value of {@code inline} + * will be used. + * + * @return The calling instance. + */ + @NotNull + public BinaryDownloadOptionsBuilder withDispositionTypeAttachment() { + dispositionType = DispositionType.ATTACHMENT; + return this; + } + + /** + * Construct a {@link BinaryDownloadOptions} instance with the + * properties specified to the builder. + * + * @return A new {@link BinaryDownloadOptions} instance built with the + * properties specified to the builder. + */ + @NotNull + public BinaryDownloadOptions build() { + return new BinaryDownloadOptions(mediaType, + characterEncoding, + fileName, + null != dispositionType + ? dispositionType.toString() + : DispositionType.INLINE.toString() + ); + } + + private enum DispositionType { + INLINE("inline"), + ATTACHMENT("attachment"); + + private final String value; + + DispositionType(final String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + } +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryUpload.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryUpload.java new file mode 100644 index 00000000000..c6057806e3e --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/BinaryUpload.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.api.binary; + +import java.net.URI; + +import org.apache.jackrabbit.api.JackrabbitValueFactory; +import org.jetbrains.annotations.NotNull; +import org.osgi.annotation.versioning.ProviderType; + +/** + * This extension interface provides a mechanism whereby a client can upload a + * binary directly to a storage location. An object of this type can be + * created by a call to {@link + * JackrabbitValueFactory#initiateBinaryUpload(long, int)} which will return an + * object of this type if the underlying implementation supports direct upload + * functionality. When calling this method, the client indicates the expected + * size of the binary and the number of URIs that it is willing to accept. The + * implementation will attempt to create an instance of this class that is + * suited to enabling the client to complete the upload successfully. + *

    + * Using an instance of this class, a client can then use one or more of the + * included URIs for uploading the binary directly by calling {@link + * #getUploadURIs()} and iterating through the URIs returned. Multi-part + * uploads are supported by the interface, although they may not be supported + * by the underlying implementation. + *

    + * Once a client finishes uploading the binary data, the client must then call + * {@link JackrabbitValueFactory#completeBinaryUpload(String)} to complete the + * upload. This call requires an upload token which can be obtained from an + * instance of this class by calling {@link #getUploadToken()}. + *

    + * Below is the detailed direct binary upload algorithm for the remote client. + *

    + * In this example the following variables are used: + *

      + *
    • {@code fileSize}: the actual binary size (must be known at this + * point) + *
    • {@code minPartSize}: the value from {@link #getMinPartSize()} + *
    • {@code maxPartSize}: the value from {@link #getMaxPartSize()} + *
    • {@code numUploadURIs}: the number of entries in {@link + * #getUploadURIs()} + *
    • {@code uploadURIs}: the entries in {@link #getUploadURIs()} + *
    • {@code partSize}: the part size to be used in the upload (to be + * determined in the algorithm) + *
    + * + * Steps: + *
      + *
    1. If (fileSize divided by maxPartSize) is larger than numUploadURIs, + * then the client cannot proceed and will have to request a new set of URIs + * with the right fileSize as maxSize + *
    2. If fileSize is smaller than minPartSize, then take the first provided + * upload URI to upload the entire binary, with partSize = fileSize
    3. + *
    4. + * (optional) If the client has more information to optimize, the + * partSize can be chosen, under the condition that all of these are + * true for the partSize: + *
        + *
      1. larger than minPartSize + *
      2. smaller or equal than maxPartSize (unless it is -1 = + * unlimited) + *
      3. larger than fileSize divided by numUploadURIs + *
      + *
    5. + *
    6. Otherwise all part URIs are to be used and the partSize = fileSize + * divided by numUploadURIs (integer division, discard modulo which will be + * the last part) + *
    7. Upload: segment the binary into partSize, for each segment take the + * next URI from uploadURIs (strictly in order), proceed with a standard + * HTTP PUT for each (for "http(s)" URIs, otherwise currently unspecified), + * and for the last part use whatever segment size is left + *
    8. If a segment fails during upload, retry (up to a certain time out) + *
    9. After the upload has finished successfully, notify the application, + * for example through a complete request, passing the {@link + * #getUploadToken() upload token}, and the application will call {@link + * JackrabbitValueFactory#completeBinaryUpload(String)} with the token + *
    + * + *

    JSON view

    + * + * A JSON representation of this interface as passed back to a remote client + * might look like this: + *
    + * {
    + *     "uploadToken": "aaaa-bbbb-cccc-dddd-eeee-ffff-gggg-hhhh",
    + *     "minPartSize": 10485760,
    + *     "maxPartSize": 104857600,
    + *     "uploadURIs": [
    + *         "http://server.com/upload/1",
    + *         "http://server.com/upload/2",
    + *         "http://server.com/upload/3",
    + *         "http://server.com/upload/4"
    + *     ]
    + * }
    + * 
    + * */ +@ProviderType +public interface BinaryUpload { + /** + * Returns an Iterable of URIs that can be used for uploading binary data + * directly to a storage location. The first URI can be used for uploading + * binary data as a single entity, or multiple URIs can be used if the + * client wishes to do multi-part uploads. + *

    + * Clients are not necessarily required to use all of the URIs provided. A + * client may choose to use fewer, or even only one of the URIs. However, + * regardless of the number of URIs used, they must be consumed in sequence. + * For example, if a client wishes to upload a binary in three parts and + * there are five URIs returned, the client must use the first URI to + * upload the first part, the second URI to upload the second part, and + * the third URI to upload the third part. The client is not required to + * use the fourth and fifth URIs. However, using the second URI to upload + * the third part may result in either an upload failure or a corrupted + * upload; likewise, skipping the second URI to use subsequent URIs may + * result in either an upload failure or a corrupted upload. + *

    + * Clients should be aware that some storage providers have limitations on + * the minimum and maximum size of a binary payload for a single upload, so + * clients should take these limitations into account when deciding how many + * of the URIs to use. Underlying implementations may also choose to + * enforce their own limitations. + *

    + * While the API supports multi-part uploading via multiple upload URIs, + * implementations are not required to support multi-part uploading. If the + * underlying implementation does not support multi-part uploading, a single + * URI will be returned regardless of the size of the data being uploaded. + *

    + * Some storage providers also support multi-part uploads by reusing a + * single URI multiple times, in which case the implementation may also + * return a single URI regardless of the size of the data being uploaded. + *

    + * You should consult both the DataStore implementation documentation and + * the storage service provider documentation for details on such matters as + * multi-part upload support, upload minimum and maximum sizes, etc. + * + * @return Iterable of URIs that can be used for uploading directly to a + * storage location. + */ + @NotNull + Iterable getUploadURIs(); + + /** + * The smallest part size a client may upload for a multi-part upload, not + * counting the final part. This is usually either a service provider or + * implementation limitation. + *

    + * Note that the API offers no guarantees that uploading parts of this size + * can successfully complete the requested upload using the URIs provided + * via {@link #getUploadURIs()}. In other words, clients wishing to perform + * a multi-part upload must split the upload into parts of at least this + * size, but the sizes may need to be larger in order to successfully + * complete the upload. + * + * @return The smallest size acceptable for multi-part uploads. + */ + long getMinPartSize(); + + /** + * The largest part size a client may upload for a multi-part upload. This + * is usually either a service provider or implementation limitation. + *

    + * The API guarantees that a client can successfully complete a direct + * upload of the binary data of the requested size using the provided URIs + * by splitting the binary data into parts of the size returned by this + * method. + *

    + * The client is not required to use part sizes of this size; smaller sizes + * may be used so long as they are at least as large as the size returned by + * {@link #getMinPartSize()}. + *

    + * If the binary size specified by a client when calling {@link + * JackrabbitValueFactory#initiateBinaryUpload(long, int)} ends up being + * smaller than the actual size of the binary being uploaded, these API + * guarantees no longer apply, and it may not be possible to complete the + * upload using the URIs provided. In such cases, the client should restart + * the transaction using the correct size. + * + * @return The maximum size of an upload part for multi-part uploads. + */ + long getMaxPartSize(); + + /** + * Returns the upload token to be used in a subsequent call to {@link + * JackrabbitValueFactory#completeBinaryUpload(String)}. This upload token + * is used by the implementation to identify this upload. Clients should + * treat the upload token as an immutable string, as the underlying + * implementation may choose to implement techniques to detect tampering and + * reject the upload if the token is modified. + * + * @return This upload's unique upload token. + */ + @NotNull + String getUploadToken(); +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/package-info.java new file mode 100755 index 00000000000..62ebbfa00d0 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/binary/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. + */ + +/** + * Interfaces related to direct upload/download of binaries. + */ +@Version("1.0.0") +package org.apache.jackrabbit.api.binary; + +import org.osgi.annotation.versioning.Version; \ No newline at end of file diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/EventListenerMBean.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/EventListenerMBean.java new file mode 100644 index 00000000000..cee8df6dafc --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/EventListenerMBean.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.api.jmx; + +import javax.management.openmbean.CompositeData; + +/** + * MBean interface for exposing information about a registered observation + * listener. + * + * @see JCR-3608 + */ +public interface EventListenerMBean { + + /** Class name of the event listener */ + String getClassName(); + + /** toString of the event listener */ + String getToString(); + + /** Stack trace of where the listener was registered */ + String getInitStackTrace(); + + /** Event types of the listener registration */ + int getEventTypes(); + + /** Absolute path of the listener registration */ + String getAbsPath(); + + /** Whether the listener registration is deep */ + boolean isDeep(); + + /** UUIDs of the listener registration */ + String[] getUuid(); + + /** Node types of the listener registration */ + String[] getNodeTypeName(); + + /** Whether the listener registration is non-local */ + boolean isNoLocal(); + + /** Number of {@code onEvent()} calls made on the listener */ + long getEventDeliveries(); + + /** Average number of {@code onEvent()} calls per hour */ + long getEventDeliveriesPerHour(); + + /** Average time (in microseconds) taken per {@code onEvent()} call */ + long getMicrosecondsPerEventDelivery(); + + /** Number of individual events delivered to the listener */ + long getEventsDelivered(); + + /** Average number of individual events delivered per hour */ + long getEventsDeliveredPerHour(); + + /** Average time (in microseconds) taken per event delivered */ + long getMicrosecondsPerEventDelivered(); + + /** Ratio of time spent in event processing */ + double getRatioOfTimeSpentProcessingEvents(); + + /** Ratio of time spent in event listener vs. the overall event processing */ + double getEventConsumerTimeRatio(); + + /** Is user information accessed without checking if an event is external? */ + boolean isUserInfoAccessedWithoutExternalsCheck(); + + /** Is user information accessed from an external event? */ + boolean isUserInfoAccessedFromExternalEvent(); + + /** Is date information accessed without checking if an event is external? */ + boolean isDateAccessedWithoutExternalsCheck(); + + /** Is date information accessed from an external event? */ + boolean isDateAccessedFromExternalEvent(); + + /** + * The time difference between the current system time and the head (oldest) + * element in the queue in milliseconds. This method returns zero if the + * queue is empty. + */ + long getQueueBacklogMillis(); + + /** + * {@link org.apache.jackrabbit.api.stats.TimeSeries time series} of the number of + * items related to generating observation events that are currently queued by the + * system. The exact nature of these items is implementation specific and might not + * be in a one to one relation with the number of pending JCR events. + * @return time series of the queue length + */ + CompositeData getQueueLength(); + + /** + * @return time series of the number of JCR events + */ + CompositeData getEventCount(); + + /** + * @return time series of the time it took an event listener to process JCR events. + */ + CompositeData getEventConsumerTime(); + + /** + * @return time series of the time it took the system to produce JCR events. + */ + CompositeData getEventProducerTime(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/ManagedRepositoryMBean.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/ManagedRepositoryMBean.java new file mode 100644 index 00000000000..751667af16e --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/ManagedRepositoryMBean.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.api.jmx; + +import java.util.Map; + +import javax.jcr.RepositoryException; + +/** + * Interface for managing a JCR repository as a JMX MBean. + * + * @since Apache Jackrabbit 2.3 + */ +public interface ManagedRepositoryMBean { + + /** + * Returns the name of this repository implementation. + * + * @see javax.jcr.Repository#REP_NAME_DESC + * @return name of this repository implementation + */ + String getName(); + + /** + * Returns the version of this repository implementation. + * + * @see javax.jcr.Repository#REP_VERSION_DESC + * @return version of this repository implementation + */ + String getVersion(); + + /** + * Returns all the repository descriptors. + * + * @return repository descriptors + */ + Map getDescriptors(); + + /** + * Returns the names of all the workspaces in this repository. + * + * @return workspace names + */ + String[] getWorkspaceNames(); + + /** + * Creates a new workspace with the given name. + * + * @param name workspace name + * @throws RepositoryException if the workspace could not be created + */ + void createWorkspace(String name) throws RepositoryException; + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/QueryStatManagerMBean.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/QueryStatManagerMBean.java new file mode 100644 index 00000000000..8cbd23d25d0 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/QueryStatManagerMBean.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.api.jmx; + +import javax.management.openmbean.TabularData; + +import org.apache.jackrabbit.api.stats.QueryStat; + +/** + * JMX Bindings for {@link QueryStat}. + * + */ +public interface QueryStatManagerMBean { + + String NAME = "org.apache.jackrabbit:type=QueryStats"; + + /** + * @return a sorted array containing the top + * {@link #getSlowQueriesQueueSize()} slowest queries + */ + TabularData getSlowQueries(); + + /** + * @return a sorted array containing the + * {@link #getPopularQueriesQueueSize()} most popular queries + */ + TabularData getPopularQueries(); + + /** + * @return size of the Slow queue + */ + int getSlowQueriesQueueSize(); + + /** + * Change the size of the Slow queue + * + * @param size + * the new size + */ + void setSlowQueriesQueueSize(int size); + + /** + * clears the Slow queue + */ + void clearSlowQueriesQueue(); + + /** + * @return size of the Popular queue + */ + int getPopularQueriesQueueSize(); + + /** + * Change the size of the Popular queue + * + * @param size + * the new size + */ + void setPopularQueriesQueueSize(int size); + + /** + * clears the Popular queue + */ + void clearPopularQueriesQueue(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/package-info.java new file mode 100644 index 00000000000..c83550c8116 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/jmx/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. + */ + +/** + * JMX management interfaces for JCR. + */ +@org.osgi.annotation.versioning.Version("2.3.0") +package org.apache.jackrabbit.api.jmx; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/DataStoreGarbageCollector.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/DataStoreGarbageCollector.java new file mode 100644 index 00000000000..3b5061dca9a --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/DataStoreGarbageCollector.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.api.management; + +import javax.jcr.RepositoryException; + +/** + * Garbage collector for DataStore. This implementation iterates through all + * nodes and reads the binary properties. To detect nodes that are moved while + * the scan runs, event listeners are started. Like the well known garbage + * collection in Java, the items that are still in use are marked. Currently + * this is achieved by updating the modified date of the entries. Newly added + * entries are detected because the modified date is changed when they are + * added. + *

    + * Example code to run the data store garbage collection: + *

    + * JackrabbitRepositoryFactory jf = (JackrabbitRepositoryFactory) factory;
    + * RepositoryManager m = jf.getRepositoryManager((JackrabbitRepository) repository);
    + * GarbageCollector gc = m.createDataStoreGarbageCollector();
    + * gc.mark();
    + * gc.sweep();
    + * 
    + */ +public interface DataStoreGarbageCollector { + + /** + * Set the delay between scanning items. + * The main scan loop sleeps this many milliseconds after + * scanning a node. The default is 0, meaning the scan should run at full speed. + * + * @param millis the number of milliseconds to sleep + */ + void setSleepBetweenNodes(long millis); + + /** + * Get the delay between scanning items. + * + * @return the number of milliseconds to sleep + */ + long getSleepBetweenNodes(); + + /** + * Set the event listener. If set, the event listener will be called + * for each item that is scanned. This mechanism can be used + * to display the progress. + * + * @param callback if set, this is called while scanning + */ + void setMarkEventListener(MarkEventListener callback); + + /** + * Enable or disable using the IterablePersistenceManager interface + * to scan the items. This is important for clients that need + * the complete Node implementation in the ScanEventListener + * callback. + * + * @param allow true if using the IterablePersistenceManager interface is allowed + */ + void setPersistenceManagerScan(boolean allow); + + /** + * Check if using the IterablePersistenceManager interface is allowed. + * + * @return true if using IterablePersistenceManager is possible. + */ + boolean isPersistenceManagerScan(); + + /** + * Scan the repository. The garbage collector will iterate over all nodes in the repository + * and update the last modified date. If all persistence managers implement the + * IterablePersistenceManager interface, this mechanism is used; if not, the garbage + * collector scans the repository using the JCR API starting from the root node. + * + * @throws RepositoryException + */ + void mark() throws RepositoryException; + + /** + * Delete all unused items in the data store. + * + * @return the number of deleted items + * @throws RepositoryException + */ + int sweep() throws RepositoryException; + + /** + * Cleanup resources used internally by this instance. + */ + void close(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/MarkEventListener.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/MarkEventListener.java new file mode 100644 index 00000000000..5b5d2a7c9d2 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/MarkEventListener.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.api.management; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** + * The listener interface for receiving garbage collection scan events. + */ +public interface MarkEventListener { + + /** + * This method is called before a node is scanned. + * + * @param node node to be scanned + */ + void beforeScanning(Node node) throws RepositoryException; + +} \ No newline at end of file diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/RepositoryManager.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/RepositoryManager.java new file mode 100644 index 00000000000..2db260c4a9a --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/RepositoryManager.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.api.management; + +import javax.jcr.RepositoryException; + +/** + * The repository manager provides life-cycle management features for + * repositories. + * + * Not all implementations are required to implement all features, + * for example some implementations may not support starting a repository after + * is has been stopped. + */ +public interface RepositoryManager { + + /** + * Shuts down the repository. A Jackrabbit repository instance contains + * a acquired resources and cached data that needs to be released and + * persisted when the repository is no longer used. This method handles + * all these shutdown tasks and should therefore be called by the + * client application once the repository instance is no longer used. + *

    + * Possible errors are logged rather than thrown as exceptions as there + * is little that a client application could do in such a case. + */ + void stop(); + + /** + * Create a data store garbage collector for this repository. + * + * @return the data store garbage collector if the data store is enabled, null otherwise + */ + DataStoreGarbageCollector createDataStoreGarbageCollector() throws RepositoryException; + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/package-info.java new file mode 100644 index 00000000000..abfc83d03d3 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/management/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. + */ + +/** + * Interfaces for managing a Jackrabbit repository. + */ +@org.osgi.annotation.versioning.Version("2.3.1") +package org.apache.jackrabbit.api.management; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitEvent.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitEvent.java new file mode 100644 index 00000000000..a7e7a05b448 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitEvent.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.api.observation; + +import javax.jcr.observation.Event; + +/** + * This is an extension of the event interface which provides + * a method to detect whether the changes happened on locally + * or remotely in a clustered environment. + */ +public interface JackrabbitEvent extends Event { + + /** + * Return a flag indicating whether this is an externally generated event. + * + * @return true if this is an external event; + * false otherwise + */ + boolean isExternal(); +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitEventFilter.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitEventFilter.java new file mode 100644 index 00000000000..be63fbebcb4 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitEventFilter.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.api.observation; + +import static java.util.Arrays.copyOf; + +/** + * A storage object for event filter configuration. + *

    + * The parameters of the filter can then be set by chaining the set methods, + * since each method returns the same EventFilter with the indicated parameter set. + *

    + * Once the filter is configured, it and an {@link javax.jcr.observation.EventListener} object are + * passed to + * {@link org.apache.jackrabbit.api.observation.JackrabbitObservationManager#addEventListener(javax.jcr.observation.EventListener, JackrabbitEventFilter)}. + *

    + * The filter restricts which events are sent to the EventListener according to the + * following parameters. Note that the term associated parent node of an event means the + * parent node of the item at (or formerly at) the path returned by + * {@link javax.jcr.observation.Event#getPath}. + *

      + *
    • + * eventTypes: + * A bitwise OR of the event types to be listened to. See + * {@link javax.jcr.observation.Event} for details. + *
    • + *
    • + * absPath, absPaths, excludedPaths, + * isDeep: Only events whose associated parent node is at one + * of the paths in absPath or absPaths (or within + * its subgraph, if isDeep is true) will be received + * except if the associated parent node is at one of the paths in + * excludedPaths or its subgraph. + * It is permissible to register a listener for a path where no node currently + * exists. + *
    • + *
    • + * uuid: + * Only events whose associated parent node has one of + * the identifiers in this list will be received. If his parameter is + * null then no identifier-related restriction is placed on + * events received. Note that specifying an empty array instead of + * null would result in no nodes being listened to. The term + * "UUID" is used for compatibility with JCR 1.0. + *
    • + *
    • + * nodeTypeName: + * Only events whose associated parent node has + * one of the node types (or a subtype of one of the node types) in this + * list will be received. If his parameter is null then no node + * type-related restriction is placed on events received. Note that + * specifying an empty array instead of null would result in no + * nodes types being listened to. + *
    • + *
    • + * noLocal: if true, then events + * generated by the session through which the listener was registered are + * ignored. Otherwise, they are not ignored. + *
    • + *
    • + * noExternal: if true, then events + * from external cluster nodes are ignored. Otherwise, they are not ignored. + *
    • + *
    • + * noInternal: if true, then events + * from this cluster node are ignored. Otherwise, they are not ignored. + *
    • + *
    + * The restrictions are "ANDed" together. In other words, for a particular node to be "listened to" it + * must meet all the restrictions. + * + */ +public class JackrabbitEventFilter { // TODO extends EventFilter once JCR 2.1 is out + private int eventTypes; + private String absPath; + private boolean isDeep; + private String[] identifiers; + private String[] nodeTypeNames; + private boolean noLocal; + private String[] absPaths = new String[]{}; + private String[] excludedPaths = new String[]{}; + private boolean noExternal; + private boolean noInternal; + + /** + * Sets the eventTypes parameter of the filter. + * If left unset, this parameter defaults to 0. + * + * @param eventTypes an int. + * @return This EventFilter object with the eventTypes parameter set. + */ + public JackrabbitEventFilter setEventTypes(int eventTypes) { + this.eventTypes = eventTypes; + return this; + } + + /** + * Returns the eventTypes parameter of the filter. + * + * @return an int. + */ + public int getEventTypes() { + return eventTypes; + } + + /** + * Sets the absPath parameter of the filter. + * If left unset, this parameter defaults to null. + * + * @param absPath an absolute path String. + * @return This EventFilter object with the absPath parameter set. + */ + public JackrabbitEventFilter setAbsPath(String absPath) { + this.absPath = absPath; + return this; + } + + /** + * Returns the absPath parameter of the filter. + * + * @return a String. + */ + public String getAbsPath() { + return absPath; + } + + /** + * Sets the isDeep parameter of the filter. + * If left unset, this parameter defaults to false. + * + * @param isDeep a boolean. + * @return This EventFilter object with the isDeep parameter set. + */ + public JackrabbitEventFilter setIsDeep(boolean isDeep) { + this.isDeep = isDeep; + return this; + } + + /** + * Returns the isDeep parameter of the filter. + * + * @return a boolean. + */ + public boolean getIsDeep() { + return isDeep; + } + + /** + * Sets the identifiers parameter of the filter. + * If left unset, this parameter defaults to null. + * + * @param identifiers a String array. + * @return This EventFilter object with the identifiers parameter set. + */ + public JackrabbitEventFilter setIdentifiers(String[] identifiers) { + this.identifiers = copyOf(identifiers, identifiers.length); + return null; + } + + /** + * Returns the uuids parameter of the filter. + * + * @return a String array. + */ + public String[] getIdentifiers() { + return identifiers == null ? null : copyOf(identifiers, identifiers.length); + } + + /** + * Sets the nodeTypeNames parameter of the filter. + * If left unset, this parameter defaults to null. + * + * @param nodeTypeNames a String array. + * @return This EventFilter object with the nodeTypes parameter set. + */ + public JackrabbitEventFilter setNodeTypes(String[] nodeTypeNames) { + this.nodeTypeNames = copyOf(nodeTypeNames, nodeTypeNames.length); + return this; + } + + /** + * Returns the nodeTypeName parameter of the filter. + * + * @return a String array. + */ + public String[] getNodeTypes() { + return nodeTypeNames == null ? null : copyOf(nodeTypeNames, nodeTypeNames.length); + } + + /** + * Sets the noLocal parameter of the filter. + * If left unset, this parameter defaults to false. + * + * @param noLocal a boolean. + * @return This EventFilter object with the noLocal parameter set. + */ + public JackrabbitEventFilter setNoLocal(boolean noLocal) { + this.noLocal = noLocal; + return this; + } + + /** + * Returns the noLocal parameter of the filter. + * + * @return a boolean. + */ + public boolean getNoLocal() { + return noLocal; + } + + /** + * Sets the absPaths parameter of the filter. + * If left unset, this parameter defaults to an empty array. + * + * @param absPaths an absolute path String array. + * @return This EventFilter object with the absPaths parameter set. + */ + public JackrabbitEventFilter setAdditionalPaths(String... absPaths) { + this.absPaths = copyOf(absPaths, absPaths.length); + return this; + } + + /** + * Returns the absPaths parameter of the filter. + * + * @return a String array. + */ + public String[] getAdditionalPaths() { + return copyOf(absPaths, absPaths.length); + } + + /** + * Sets the excludedPaths parameter of the filter. + * If left unset, this parameter defaults to an empty array. + * + * @param excludedPaths an absolute path String array. + * @return This EventFilter object with the excludedPaths parameter set. + */ + public JackrabbitEventFilter setExcludedPaths(String... excludedPaths) { + this.excludedPaths = copyOf(excludedPaths, excludedPaths.length); + return this; + } + + /** + * Returns the excludedPaths parameter of the filter. + * + * @return a String array. + */ + public String[] getExcludedPaths() { + return copyOf(excludedPaths, excludedPaths.length); + } + + /** + * Sets the noExternal parameter of the filter. + * If left unset, this parameter defaults to false. + * + * @param noExternal a boolean. + * @return This EventFilter object with the noExternal parameter set. + */ + public JackrabbitEventFilter setNoExternal(boolean noExternal) { + this.noExternal = noExternal; + return this; + } + + /** + * Returns the noExternal parameter of the filter. + * + * @return a boolean. + */ + public boolean getNoExternal() { + return noExternal; + } + + /** + * Sets the noInternal parameter of the filter. + * If left unset, this parameter defaults to false. + * + * @param noInternal a boolean. + * @return This EventFilter object with the noExternal parameter set. + */ + public JackrabbitEventFilter setNoInternal(boolean noInternal) { + this.noInternal = noInternal; + return this; + } + + /** + * Returns the noInternal parameter of the filter. + * + * @return a boolean. + */ + public boolean getNoInternal() { + return noInternal; + } + +} \ No newline at end of file diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitObservationManager.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitObservationManager.java new file mode 100644 index 00000000000..122ad5aa8e9 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/JackrabbitObservationManager.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.api.observation; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; + +/** + * Jackrabbit specific extensions to {@link javax.jcr.observation.ObservationManager}. + */ +public interface JackrabbitObservationManager extends ObservationManager { + + /** + * Adds an event listener that listens for the events specified + * by the passed {@link JackrabbitEventFilter}. + *

    + * In addition to the EventFilter, the set of events reported + * will be further filtered by the access rights of the + * current Session. + *

    + * See {@link JackrabbitEventFilter} for a description of the filtering parameters available. + *

    + * The filter of an already-registered EventListener can be + * changed at runtime by re-registering the same EventListener + * object (i.e. the same actual Java object) with a new filter. + * The implementation must ensure that no events are lost during the changeover. + *

    + * In addition to the filters placed on a listener above, the scope of + * observation support, in terms of which parts of a workspace are observable, may also + * be subject to implementation-specific restrictions. For example, in some + * repositories observation of changes in the jcr:system + * subgraph may not be supported. + * + * @param listener an {@link EventListener} object. + * @param filter an {@link JackrabbitEventFilter} object. + * @throws RepositoryException If an error occurs. + */ + void addEventListener(EventListener listener, JackrabbitEventFilter filter) + throws RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/package-info.java new file mode 100644 index 00000000000..9e07701c96e --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/observation/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. + */ + +/** + * Jackrabbit extensions for JCR observation. + */ +@org.osgi.annotation.versioning.Version("2.3.1") +package org.apache.jackrabbit.api.observation; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/package-info.java new file mode 100644 index 00000000000..0c62b58306e --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/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. + */ + +/** + * Jackrabbit extensions for JCR core interfaces + */ +@org.osgi.annotation.versioning.Version("2.5.0") +package org.apache.jackrabbit.api; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/query/JackrabbitQueryResult.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/query/JackrabbitQueryResult.java new file mode 100644 index 00000000000..101c4e95456 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/query/JackrabbitQueryResult.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.api.query; + +import javax.jcr.query.QueryResult; + +/** + * The Jackrabbit query result interface. This interface contains the + * Jackrabbit-specific extensions to the JCR {@link QueryResult} interface. + * + * @since Jackrabbit 2.6 + */ +public interface JackrabbitQueryResult extends QueryResult { + + /** + * Returns the total number of hits. This is the number of results you + * would get without any limit or offset settings. This method may return + * -1 if the total size is unknown. + * + * @return the total number of hits, or -1 + */ + int getTotalSize(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/query/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/query/package-info.java new file mode 100755 index 00000000000..5395862e157 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/query/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.api.query; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlEntry.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlEntry.java new file mode 100644 index 00000000000..58654a80e80 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlEntry.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.api.security; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.security.AccessControlEntry; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * JackrabbitAccessControlEntry is a Jackrabbit specific extension + * of the AccessControlEntry interface. It represents an single + * entry of a {@link JackrabbitAccessControlList}. + */ +@ProviderType +public interface JackrabbitAccessControlEntry extends AccessControlEntry { + + /** + * @return true if this entry adds Privileges for the principal; + * false otherwise. + */ + boolean isAllow(); + + /** + * Return the names of the restrictions present with this access control entry. + * + * @return the names of the restrictions + * @throws RepositoryException if an error occurs. + */ + String[] getRestrictionNames() throws RepositoryException; + + /** + * Return the value of the restriction with the specified name or + * null if no such restriction exists. In case the restriction + * with the specified name contains multiple value this method will call + * {@code ValueFormatException}. + * + * @param restrictionName The of the restriction as obtained through + * {@link #getRestrictionNames()}. + * @return value of the restriction with the specified name or + * null if no such restriction exists. + * @throws ValueFormatException If the restriction with the specified name + * contains multiple values. + * @throws RepositoryException if an error occurs. + * @see #getRestrictions(String) + */ + Value getRestriction(String restrictionName) throws ValueFormatException, RepositoryException; + + /** + * Return the values of the restriction with the specified name or + * null if no such restriction exists. For restrictions that + * contain just a single value this method is expected to return an array + * with a single element even if the underlying implementation stored the + * restriction in single-value JCR property. + * + * @param restrictionName The of the restriction as obtained through + * {@link #getRestrictionNames()}. + * @return the values of the restriction with the specified name as an array + * or null if no such restriction exists. The array may contain + * zero, one or multiple values. + * @throws RepositoryException if an error occurs. + * @see #getRestriction(String) + */ + Value[] getRestrictions(String restrictionName) throws RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlList.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlList.java new file mode 100644 index 00000000000..7663518e2c7 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlList.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.api.security; + +import java.security.Principal; +import java.util.Map; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * JackrabbitAccessControlList is an extension of the AccessControlList. + * Similar to the latter any modifications made will not take effect, until it is + * {@link javax.jcr.security.AccessControlManager#setPolicy(String, AccessControlPolicy) + * written back} and {@link javax.jcr.Session#save() saved}. + */ +@ProviderType +public interface JackrabbitAccessControlList extends JackrabbitAccessControlPolicy, AccessControlList { + + /** + * Returns the names of the supported restrictions or an empty array + * if no restrictions are respected. + * + * @return the names of the supported restrictions or an empty array. + * @see #addEntry(Principal, Privilege[], boolean, Map) + * @throws RepositoryException If an error occurs. + */ + String[] getRestrictionNames() throws RepositoryException; + + /** + * Return the expected {@link javax.jcr.PropertyType property type} of the + * restriction with the specified restrictionName. + * + * @param restrictionName Any of the restriction names retrieved from + * {@link #getRestrictionNames()}. + * @return expected {@link javax.jcr.PropertyType property type}. + * @throws RepositoryException If an error occurs. + */ + int getRestrictionType(String restrictionName) throws RepositoryException; + + /** + * Returns true if the restriction is multivalued; false + * otherwise. If an given implementation doesn't support multivalued restrictions, + * this method always returns false. + * + * @param restrictionName Any of the restriction names retrieved from + * {@link #getRestrictionNames()}. + * @return true if the restriction is multivalued; false + * if the restriction with the given name is single value or if the implementation + * doesn't support multivalued restrictions, this method always returns false. + * @throws RepositoryException If an error occurs. + * @see #addEntry(Principal, Privilege[], boolean, Map, Map) + */ + boolean isMultiValueRestriction(String restrictionName) throws RepositoryException; + + /** + * Returns true if this policy does not yet define any + * entries. + * + * @return If no entries are present. + */ + boolean isEmpty(); + + /** + * Returns the number of entries or 0 if the policy {@link #isEmpty() is empty}. + * + * @return The number of entries present or 0 if the policy {@link #isEmpty() is empty}. + */ + int size(); + + /** + * Same as {@link #addEntry(Principal, Privilege[], boolean, Map)} using + * some implementation specific restrictions. + * + * @param principal the principal to add the entry for + * @param privileges the privileges to add + * @param isAllow if true if this is a positive (allow) entry + * @return true if this policy has changed by incorporating the given entry; + * false otherwise. + * @throws AccessControlException If any of the given parameter is invalid + * or cannot be handled by the implementation. + * @throws RepositoryException If another error occurs. + * @see AccessControlList#addAccessControlEntry(Principal, Privilege[]) + */ + boolean addEntry(Principal principal, Privilege[] privileges, boolean isAllow) + throws AccessControlException, RepositoryException; + + /** + * Adds an access control entry to this policy consisting of the specified + * principal, the specified privileges, the + * isAllow flag and an optional map containing additional + * restrictions. + *

    + * This method returns true if this policy was modified, + * false otherwise. + *

    + * An AccessControlException is thrown if any of the specified + * parameters is invalid or if some other access control related exception occurs. + * + * @param principal the principal to add the entry for + * @param privileges the privileges to add + * @param isAllow if true if this is a positive (allow) entry + * @param restrictions A map of additional restrictions used to narrow the + * effect of the entry to be created. The map must map JCR names to a single + * {@link javax.jcr.Value} object. + * @return true if this policy has changed by incorporating the given entry; + * false otherwise. + * @throws AccessControlException If any of the given parameter is invalid + * or cannot be handled by the implementation. + * @throws RepositoryException If another error occurs. + * @see AccessControlList#addAccessControlEntry(Principal, Privilege[]) + */ + boolean addEntry(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions) + throws AccessControlException, RepositoryException; + + /** + * Adds an access control entry to this policy consisting of the specified + * principal, the specified privileges, the + * isAllow flag and an optional map containing additional + * restrictions. + *

    + * This method returns true if this policy was modified, + * false otherwise. + *

    + * An AccessControlException is thrown if any of the specified + * parameters is invalid or if some other access control related exception occurs. + * + * @param principal the principal to add the entry for + * @param privileges the privileges to add + * @param isAllow if true if this is a positive (allow) entry + * @param restrictions A map of additional restrictions used to narrow the + * effect of the entry to be created. The map must map JCR names to a single + * {@link javax.jcr.Value} object. + * @param mvRestrictions A map of additional multivalued restrictions used to narrow the + * effect of the entry to be created. The map must map JCR names to a + * {@link javax.jcr.Value} array. + * @return true if this policy has changed by incorporating the given entry; + * false otherwise. + * @throws AccessControlException If any of the given parameter is invalid + * or cannot be handled by the implementation. + * @throws RepositoryException If another error occurs. + * @see AccessControlList#addAccessControlEntry(Principal, Privilege[]) + * @since 2.8 + */ + boolean addEntry(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions, + Map mvRestrictions) + throws AccessControlException, RepositoryException; + + /** + * If the AccessControlList implementation supports + * reordering of entries the specified srcEntry is inserted + * at the position of the specified destEntry. + *

    + * If destEntry is null the entry is moved to the + * end of the list. + *

    + * If srcEntry and destEntry are the same no + * changes are made. + * + * @param srcEntry The access control entry to be moved within the list. + * @param destEntry The entry before which the srcEntry will be moved. + * @throws AccessControlException If any of the given entries is invalid or + * cannot be handled by the implementation. + * @throws UnsupportedRepositoryOperationException If ordering is not supported. + * @throws RepositoryException If another error occurs. + */ + void orderBefore(AccessControlEntry srcEntry, AccessControlEntry destEntry) + throws AccessControlException, UnsupportedRepositoryOperationException, RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlManager.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlManager.java new file mode 100644 index 00000000000..a5fe13f8620 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlManager.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.api.security; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.PathNotFoundException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; + +import java.security.Principal; +import java.util.Set; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * JackrabbitAccessControlManager provides extensions to the + * AccessControlManager interface. + */ +@ProviderType +public interface JackrabbitAccessControlManager extends AccessControlManager { + + /** + * Returns the applicable policies for the specified principal + * or an empty array if no additional policies can be applied. + * + * @param principal A principal known to the editing session. + * @return array of policies for the specified principal. Note + * that the policy object returned must reveal the path of the node where + * they can be applied later on using {@link AccessControlManager#setPolicy(String, javax.jcr.security.AccessControlPolicy)}. + * @throws AccessDeniedException if the session lacks + * MODIFY_ACCESS_CONTROL privilege. + * @throws AccessControlException if the specified principal does not exist + * or if another access control related exception occurs. + * @throws UnsupportedRepositoryOperationException if editing access control + * policies by principal is not supported. + * @throws RepositoryException if another error occurs. + * @see JackrabbitAccessControlPolicy#getPath() + */ + JackrabbitAccessControlPolicy[] getApplicablePolicies(Principal principal) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Returns the AccessControlPolicy objects that have been set + * for the given principal or an empty array if no policy has + * been set. This method reflects the binding state, including transient + * policy modifications. + * + * @param principal A valid principal. + * @return The policies defined for the given principal or an empty array. + * @throws AccessDeniedException if the session lacks + * READ_ACCESS_CONTROL privilege. + * @throws AccessControlException if the specified principal does not exist + * or if another access control related exception occurs. + * @throws UnsupportedRepositoryOperationException if editing access control + * policies by principal is not supported. + * @throws RepositoryException If another error occurs. + */ + JackrabbitAccessControlPolicy[] getPolicies(Principal principal) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Returns the AccessControlPolicy objects that are in effect + * for the given Principals. This may be policies set through + * this API or some implementation specific (default) policies. + * + * @param principals A set of valid principals. + * @return The policies defined for the given principal or an empty array. + * @throws AccessDeniedException if the session lacks + * READ_ACCESS_CONTROL privilege. + * @throws AccessControlException if the specified principal does not exist + * or if another access control related exception occurs. + * @throws UnsupportedRepositoryOperationException if editing access control + * policies by principal is not supported. + * @throws RepositoryException If another error occurs. + */ + AccessControlPolicy[] getEffectivePolicies(Set principals) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Returns whether the given set of Principals has the specified + * privileges for absolute path absPath, which must be an + * existing node. + *

    + * Testing an aggregate privilege is equivalent to testing each non + * aggregate privilege among the set returned by calling + * Privilege.getAggregatePrivileges() for that privilege. + *

    + * The results reported by the this method reflect the net effect of + * the currently applied control mechanisms. It does not reflect unsaved + * access control policies or unsaved access control entries. Changes to + * access control status caused by these mechanisms only take effect on + * Session.save() and are only then reflected in the results of + * the privilege test methods. + *

    + * Since this method allows to view the privileges of principals other + * than included in the editing session, this method must throw + * AccessDeniedException if the session lacks + * READ_ACCESS_CONTROL privilege for the absPath + * node. + * + * @param absPath an absolute path. + * @param principals a set of Principals for which is the + * given privileges are tested. + * @param privileges an array of Privileges. + * @return true if the session has the specified privileges; + * false otherwise. + * @throws javax.jcr.PathNotFoundException if no node at absPath exists + * or the session does not have sufficient access to retrieve a node at that location. + * @throws AccessDeniedException if the session lacks + * READ_ACCESS_CONTROL privilege for the absPath node. + * @throws RepositoryException if another error occurs. + */ + public boolean hasPrivileges(String absPath, Set principals, Privilege[] privileges) + throws PathNotFoundException, AccessDeniedException, RepositoryException; + + /** + * Returns the privileges the given set of Principals has for + * absolute path absPath, which must be an existing node. + *

    + * The returned privileges are those for which {@link #hasPrivileges} would + * return true. + *

    + * The results reported by the this method reflect the net effect of + * the currently applied control mechanisms. It does not reflect unsaved + * access control policies or unsaved access control entries. Changes to + * access control status caused by these mechanisms only take effect on + * Session.save() and are only then reflected in the results of + * the privilege test methods. + *

    + * Since this method allows to view the privileges of principals other + * than included in the editing session, this method must throw + * AccessDeniedException if the session lacks + * READ_ACCESS_CONTROL privilege for the absPath + * node. + *

    + * Note that this method does not resolve any group membership, as this is + * the job of the user manager. nor does it augment the set with the + * "everyone" principal. + * + * @param absPath an absolute path. + * @param principals a set of Principals for which is the + * privileges are retrieved. + * @return an array of Privileges. + * @throws PathNotFoundException if no node at absPath exists + * or the session does not have sufficient access to retrieve a node at that + * location. + * @throws AccessDeniedException if the session lacks READ_ACCESS_CONTROL + * privilege for the absPath node. + * @throws RepositoryException if another error occurs. + */ + public Privilege[] getPrivileges(String absPath, Set principals) + throws PathNotFoundException, AccessDeniedException, RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlPolicy.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlPolicy.java new file mode 100644 index 00000000000..fb554b7ca03 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlPolicy.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.api.security; + +import javax.jcr.security.AccessControlPolicy; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * JackrabbitAccessControlPolicy is an extension of the + * AccessControlPolicy that exposes the path of the Node to + * which it can be applied using {@link javax.jcr.security.AccessControlManager#setPolicy(String, javax.jcr.security.AccessControlPolicy)}. + */ +@ProviderType +public interface JackrabbitAccessControlPolicy extends AccessControlPolicy { + + /** + * Returns the path of the node this policy has been created for. + * + * @return the path of the node this policy has been created for. + */ + String getPath(); +} \ No newline at end of file diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authentication/token/TokenCredentials.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authentication/token/TokenCredentials.java new file mode 100644 index 00000000000..0c360bd7529 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authentication/token/TokenCredentials.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.api.security.authentication.token; + +import javax.jcr.Credentials; +import java.util.HashMap; + +/** + * TokenCredentials implements the Credentials + * interface and represents single token credentials. Similar to + * {@link javax.jcr.SimpleCredentials} this credentials implementation allows + * to set additional attributes. + */ +public final class TokenCredentials implements Credentials { + + private final String token; + private final HashMap attributes = new HashMap(); + + /** + * Create a new instance. + * + * @param token A token string used to create this credentials instance. + * @throws IllegalArgumentException If the specified token is null + * or empty string. + */ + public TokenCredentials(String token) throws IllegalArgumentException { + if (token == null || token.length() == 0) { + throw new IllegalArgumentException("Invalid token '" + token + "'"); + } + this.token = token; + } + + /** + * Returns the token this credentials are built from. + * + * @return the token. + */ + public String getToken() { + return token; + } + + /** + * Stores an attribute in this credentials instance. + * + * @param name a String specifying the name of the attribute + * @param value the Object to be stored + */ + public void setAttribute(String name, String value) { + // name cannot be null + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + + // null value is the same as removeAttribute() + if (value == null) { + removeAttribute(name); + return; + } + + synchronized (attributes) { + attributes.put(name, value); + } + } + + /** + * Returns the value of the named attribute as an Object, or + * null if no attribute of the given name exists. + * + * @param name a String specifying the name of the attribute + * @return an Object containing the value of the attribute, or + * null if the attribute does not exist + */ + public String getAttribute(String name) { + synchronized (attributes) { + return (attributes.get(name)); + } + } + + /** + * Removes an attribute from this credentials instance. + * + * @param name a String specifying the name of the attribute to + * remove + */ + public void removeAttribute(String name) { + synchronized (attributes) { + attributes.remove(name); + } + } + + /** + * Returns the names of the attributes available to this credentials + * instance. This method returns an empty array if the credentials instance + * has no attributes available to it. + * + * @return a string array containing the names of the stored attributes + */ + public String[] getAttributeNames() { + synchronized (attributes) { + return attributes.keySet().toArray(new String[attributes.keySet().size()]); + } + } +} \ No newline at end of file diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authentication/token/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authentication/token/package-info.java new file mode 100644 index 00000000000..d0893aae59a --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authentication/token/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. + */ + +/** + * Token credentials. + */ +@org.osgi.annotation.versioning.Version("2.3") +package org.apache.jackrabbit.api.security.authentication.token; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/PrincipalSetPolicy.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/PrincipalSetPolicy.java new file mode 100644 index 00000000000..5843358f41a --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/PrincipalSetPolicy.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.api.security.authorization; + +import java.security.Principal; +import java.util.Set; + +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlPolicy; + +import org.jetbrains.annotations.NotNull; + +/** + * Extension of the JCR {@link javax.jcr.security.AccessControlPolicy AccessControlPolicy} + * intended to grant a set of {@code Principal}s the ability to perform certain + * actions. The scope of this policy (and thus the affected items) is an + * implementation detail; it may e.g. take effect on the tree defined by the + * {@link javax.jcr.Node}, where a given {@code PrincipalSetPolicy} is being + * applied. + * + *

    The very details on what actions are granted by a given {@code PrincipalSetPolicy} + * remains an implementation detail. Similarly a given permission model is + * in charge of defining the interactions and effects different + * {@link AccessControlPolicy policies} will have if used together in the same + * repository.

    + */ +public interface PrincipalSetPolicy extends AccessControlPolicy { + + /** + * Returns the set of {@code Principal}s that are allowed to perform + * implementation specific actions on the items affected by this policy. + * + * @return The set of {@code Principal}s that are allowed to perform + * implementation specific actions on the those items where this policy + * takes effect. + */ + @NotNull + Set getPrincipals(); + + /** + * Add {@code Principal}s that are allowed to perform some implementation + * specific actions on those items where this policy takes effect. + * + * @param principals The {@code Principal}s that are granted access. + * @return {@code true} if this policy was modified; {@code false} otherwise. + * @throws javax.jcr.security.AccessControlException If any of the specified + * principals is considered invalid or if another access control specific + * error occurs. + */ + boolean addPrincipals(@NotNull Principal... principals) throws AccessControlException; + + /** + * Remove the specified {@code Principal}s for the set of allowed principals + * thus revoking their ability to perform the implementation specific actions + * on items where this policy takes effect. + * + * @param principals The {@code Principal}s for which access should be revoked. + * @return {@code true} if this policy was modified; {@code false} otherwise. + * @throws javax.jcr.security.AccessControlException If any of the specified + * principals is considered invalid or if another access control specific + * error occurs. + */ + boolean removePrincipals(@NotNull Principal... principals) throws AccessControlException; +} \ No newline at end of file diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/PrivilegeManager.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/PrivilegeManager.java new file mode 100644 index 00000000000..c00a779f62b --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/PrivilegeManager.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.api.security.authorization; + +import javax.jcr.AccessDeniedException; +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +/** + * PrivilegeManager is a jackrabbit specific extensions to + * JCR access control management that allows to retrieve privileges known + * by this JCR implementation and to register new custom privileges according + * to implementation specific rules. + * + * @see javax.jcr.security.AccessControlManager#privilegeFromName(String) + */ +public interface PrivilegeManager { + + /** + * Returns all registered privileges. + * + * @return all registered privileges. + * @throws RepositoryException If an error occurs. + */ + Privilege[] getRegisteredPrivileges() throws RepositoryException; + + /** + * Returns the privilege with the specified privilegeName. + * + * @param privilegeName Name of the principal. + * @return the privilege with the specified privilegeName. + * @throws javax.jcr.security.AccessControlException If no privilege with the given name exists. + * @throws javax.jcr.RepositoryException If another error occurs. + */ + Privilege getPrivilege(String privilegeName) throws AccessControlException, RepositoryException; + + /** + * Creates and registers a new custom privilege with the specified + * characteristics and returns the new privilege. + *

    + * If the registration succeeds, the changes are immediately effective; + * there is no need to call save. + * + * @param privilegeName The name of the new custom privilege. + * @param isAbstract Boolean flag indicating if the privilege is abstract. + * @param declaredAggregateNames An array of privilege names referring to + * registered privileges being aggregated by this new custom privilege. + * In case of a non aggregate privilege an empty array should be passed. + * @return the new privilege. + * @throws AccessDeniedException If the session this manager has been created + * for is not allowed to register new privileges. + * @throws NamespaceException If any of the specified JCR names is illegal. + * @throws RepositoryException If the privilege could not be registered due + * to any implementation specific constraint violations or if persisting the + * custom privilege fails. + */ + Privilege registerPrivilege(String privilegeName, boolean isAbstract, + String[] declaredAggregateNames) + throws AccessDeniedException, NamespaceException, RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/package-info.java new file mode 100644 index 00000000000..cac7bfa550a --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/authorization/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. + */ + +/** + * Jackrabbit extensions for authorization. + */ +@org.osgi.annotation.versioning.Version("2.3.1") +package org.apache.jackrabbit.api.security.authorization; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/package-info.java new file mode 100644 index 00000000000..beb4d80851b --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/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. + */ + +/** + * Jackrabbit extensions for access control. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.api.security; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/GroupPrincipal.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/GroupPrincipal.java new file mode 100644 index 00000000000..17c0d44f957 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/GroupPrincipal.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.api.security.principal; + +import java.security.Principal; +import java.util.Enumeration; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * This interface is used to represent a group of principals. It is meant to + * replace the deprecated {@code java.security.acl.Group}. + */ +@ProviderType +public interface GroupPrincipal extends Principal { + + /** + * Returns true if the passed principal is a member of the group. + * This method does a recursive search, so if a principal belongs to a + * group which is a member of this group, true is returned. + * + * @param member the principal whose membership is to be checked. + * @return true if the principal is a member of this group, + * false otherwise. + */ + public boolean isMember(Principal member); + + /** + * Returns an enumeration of the members in the group. This includes both + * declared members and all principals that are indirect group members. The + * returned objects can be instances of either Principal or GroupPrincipal + * (which is a subclass of Principal). + * + * @return an enumeration of the group members. + */ + public Enumeration members(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/ItemBasedPrincipal.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/ItemBasedPrincipal.java new file mode 100644 index 00000000000..c7b57fc2617 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/ItemBasedPrincipal.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.api.security.principal; + +import javax.jcr.RepositoryException; + +/** + * ItemBasedPrincipal is a Principal having a + * corresponding item within the JCR repository. In addition to the methods + * inherited from the {@link java.security.Principal} interface it therefore + * provides a {@link #getPath()} method. + */ +public interface ItemBasedPrincipal extends JackrabbitPrincipal { + + /** + * Returns the JCR path of the item that corresponds to this + * Principal. + * + * @return the path of the {@link javax.jcr.Item} that corresponds to this + * Principal. + * @throws RepositoryException If an error occurs while retrieving the + * Item path. + */ + String getPath() throws RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/JackrabbitPrincipal.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/JackrabbitPrincipal.java new file mode 100644 index 00000000000..77ae1302cdf --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/JackrabbitPrincipal.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.api.security.principal; + +import java.security.Principal; + +/** + * JackrabbitPrincipal marks the principal to be the result of + * authentication against the repository. + */ +public interface JackrabbitPrincipal extends Principal { + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/PrincipalIterator.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/PrincipalIterator.java new file mode 100644 index 00000000000..5a843177f72 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/PrincipalIterator.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.api.security.principal; + +import javax.jcr.RangeIterator; +import java.security.Principal; + +/** + * A {@link RangeIterator} iterating over Principals.
    + */ +public interface PrincipalIterator extends RangeIterator { + + /** + * Returns the next principal. + * + * @return the next principal + */ + Principal nextPrincipal(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/PrincipalManager.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/PrincipalManager.java new file mode 100644 index 00000000000..626164d312c --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/PrincipalManager.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.api.security.principal; + +import java.security.Principal; + +/** + * This interface defines the principal manager which is the clients view on all + * principals known to the repository. Each principal manager is bound to a + * session and is restricted by the respective access control. The principal + * manager in addition provides basic search facilities. + *

    + * A {@link Principal} is an object used to connect to any kind + * of security mechanism. Example for this are the + * {@link javax.security.auth.spi.LoginModule login modules} that use principals + * to process the login procedure.
    + * A principal can be a member of a {@link GroupPrincipal}. A + * group is a principal itself and can therefore be a member of a group again. + *

    + * Please note the following security considerations that need to be respected + * when implementing the PrincipalManager: All principals returned by this + * manager as well as {@link GroupPrincipal#members()} must respect access + * restrictions that may be present for the Session this manager + * has been built for. The same applies for + * {@link #getGroupMembership(Principal)}. + *

    + * Since Jackrabbit 2.18, a new interface has been introduced to represent the + * concept of a group of principals: {@link GroupPrincipal}, alongside + * {@code java.security.acl.Group} which is deprecated to be deleted. Until the + * final deletion of {@code java.security.acl.Group}, the 2 interfaces will be + * used concurrently for backwards compatibility reasons. See JCR-4249 for more + * details. + */ +public interface PrincipalManager { + + /** + * Filter flag indicating that only Principals that do NOT + * represent a group should be searched and returned. + */ + int SEARCH_TYPE_NOT_GROUP = 1; + + /** + * Filter flag indicating that only Principals that represent + * a group of Principals should be searched and returned. + */ + int SEARCH_TYPE_GROUP = 2; + + /** + * Filter flag indicating that all Principals should be search + * irrespective whether they represent a group of Principals or not. + */ + int SEARCH_TYPE_ALL = 3; + + /** + * Checks if the principal with the given name is known to this manager + * (in respect to the sessions access rights). If this method returns + * true then the following expression evaluates to true + * as well: PrincipalManager.getPrincipal(name).getName().equals(name) + * + * @param principalName the name of the principal to check + * @return return true if the principal with this name is known + * to this manager; false otherwise. + */ + boolean hasPrincipal(String principalName); + + /** + * Returns the principal with the given name if is known to this manager + * (with respect to the sessions access rights). + * Please note that due to security reasons group principals will only + * reveal those members that are visible to the Session this + * PrincipalManager has been built for. + * + * @param principalName the name of the principal to retrieve + * @return return the requested principal or null if a + * principal with the given name does not exist or is not accessible + * for the editing session. + */ + Principal getPrincipal(String principalName); + + /** + * Gets the principals matching a simple filter expression applied against + * the {@link Principal#getName() principal name}. + * TODO: define the filter expression.
    + * An implementation may limit the number of principals returned. + * If there are no matching principals, an empty iterator is returned. + * + * @param simpleFilter + * @return a PrincipalIterator over the Principals + * matching the given filter. + */ + PrincipalIterator findPrincipals(String simpleFilter); + + /** + * Gets the principals matching a simple filter expression applied against + * the {@link Principal#getName() principal name} AND the specified search + * type. + * TODO: define the filter expression.
    + * An implementation may limit the number of principals returned. + * If there are no matching principals, an empty iterator is returned. + * + * @param simpleFilter + * @param searchType Any of the following constants: + *

      + *
    • {@link PrincipalManager#SEARCH_TYPE_ALL}
    • + *
    • {@link PrincipalManager#SEARCH_TYPE_GROUP}
    • + *
    • {@link PrincipalManager#SEARCH_TYPE_NOT_GROUP}
    • + *
    + * @return a PrincipalIterator over the Principals + * matching the given filter and search type. + */ + PrincipalIterator findPrincipals(String simpleFilter, int searchType); + + /** + * Returns all Principals matching the specified search type. + * + * @param searchType Any of the following constants: + *
      + *
    • {@link PrincipalManager#SEARCH_TYPE_ALL}
    • + *
    • {@link PrincipalManager#SEARCH_TYPE_GROUP}
    • + *
    • {@link PrincipalManager#SEARCH_TYPE_NOT_GROUP}
    • + *
    + * @return a PrincipalIterator over all the Principals + * matching the given search type. + */ + PrincipalIterator getPrincipals(int searchType); + + /** + * Returns an iterator over all group principals for which the given + * principal is either direct or indirect member of. + *

    + * Example:
    + * If Principal P is member of Group A, and Group A is member of + * Group B, this method will return Principal A and Principal B. + * + * @param principal the principal to return it's membership from. + * @return an iterator returning all groups the given principal is member of. + */ + PrincipalIterator getGroupMembership(Principal principal); + + /** + * Returns the Principal which is implicitly applied to + * every subject. + * + * @return the 'everyone' principal + */ + Principal getEveryone(); +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/package-info.java new file mode 100644 index 00000000000..0731f296338 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/principal/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. + */ + +/** + * Jackrabbit extensions for JAAS principals. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.api.security.principal; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Authorizable.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Authorizable.java new file mode 100644 index 00000000000..be0668d32bf --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Authorizable.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.api.security.user; + +import java.security.Principal; +import java.util.Iterator; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; + +/** + * The Authorizable is the common base interface for {@link User} and + * {@link Group}. It provides access to the Principals associated + * with an Authorizable (see below) and allow to access and + * modify additional properties such as e.g. full name, e-mail or address. + *

    + * Please note the difference between Authorizable and + * {@link java.security.Principal Principal}:
    + * An Authorizable is repository object that is neither associated + * with nor depending from a particular Session and thus independent + * of the login mechanisms creating Sessions.
    + *

    + * On the other hand Principals are representations of user + * identities. In other words: each Principal within the set + * associated with the Session's Subject upon login represents an identity for + * that user. An the set of Principals may differ between different + * login mechanisms.
    + *

    + * Consequently an one-to-many relationship exists between Authorizable + * and Principal (see also {@link #getPrincipal()}. + *

    + * The interfaces derived from Authorizable are defined as follows: + *

      + *
    • {@link User}: defined to be an Authorizable that can be authenticated + * (by using Credentials) and impersonated.
    • + *
    • {@link Group}: defined to be a collection of other + * Authorizables.
    • + *
    + * + * @see User + * @see Group + */ +public interface Authorizable { + + /** + * Return the implementation specific identifier for this + * Authorizable. It could e.g. be a UserID or simply the + * principal name. + * + * @return Name of this Authorizable. + * @throws RepositoryException if an error occurs. + */ + String getID() throws RepositoryException; + + /** + * @return if the current Authorizable is a {@link Group} + */ + boolean isGroup(); + + /** + * @return a representation as Principal. + * @throws RepositoryException If an error occurs. + */ + Principal getPrincipal() throws RepositoryException; + + /** + * @return all {@link Group}s, this Authorizable is declared member of. + * @throws RepositoryException If an error occurs. + */ + Iterator declaredMemberOf() throws RepositoryException; + + /** + * @return all {@link Group}s, this Authorizable is member of included + * indirect group membership. + * @throws RepositoryException If an error occurs. + */ + Iterator memberOf() throws RepositoryException; + + /** + * Removes this Authorizable, if the session has sufficient + * permissions. Note, that removing an Authorizable even + * if it listed as member of a Group or if still has members (this is + * a Group itself). + * + * @throws RepositoryException If an error occurred and the + * Authorizable could not be removed. + */ + void remove() throws RepositoryException; + + /** + * Returns the names of properties present with this + * Authorizable not taking possible relative paths into consideration. + * Same as {@link #getPropertyNames(String)} where the specified string + * is ".". + * + * @return names of properties. + * @throws RepositoryException If an error occurs. + * @see #getProperty(String) where the specified relative path is simply an + * name. + * @see #hasProperty(String) + */ + Iterator getPropertyNames() throws RepositoryException; + + /** + * Returns the names of properties present with this + * Authorizable at the specified relative path. + * + * @param relPath A relative path. + * @return names of properties. + * @throws RepositoryException If an error occurs. + * @see #getProperty(String) + * @see #hasProperty(String) + */ + Iterator getPropertyNames(String relPath) throws RepositoryException; + + /** + * Tests if a the property with specified name exists. + * + * @param relPath The relative path to the property to be tested. + * @return true if a property with the given name exists. + * @throws RepositoryException If an error occurs. + * @see #getProperty(String) + */ + boolean hasProperty(String relPath) throws RepositoryException; + + /** + * Set an arbitrary property to this Authorizable. + * + * @param relPath The relative path of the property to be added or modified. + * @param value The desired value. + * @throws RepositoryException If the specified property could not be set. + */ + void setProperty(String relPath, Value value) throws RepositoryException; + + /** + * Set an arbitrary property to this Authorizable. + * + * @param relPath The relative path of the property to be added or modified. + * @param value The desired property values. + * @throws RepositoryException If the specified property could not be set. + */ + void setProperty(String relPath, Value[] value) throws RepositoryException; + + /** + * Returns the values for the properties with the specified name or + * null. + * + * @param relPath Relative path of the property to be retrieved. + * @return value of the property with the given name or null + * if no such property exists. + * @throws RepositoryException If an error occurs. + */ + Value[] getProperty(String relPath) throws RepositoryException; + + /** + * Removes the property with the given name. + * + * @param relPath Relative path (or name) of the property to be removed. + * @return true If the property at the specified relPath was successfully + * removed; false if no such property was present. + * @throws RepositoryException If an error occurs. + */ + boolean removeProperty(String relPath) throws RepositoryException; + + /** + * Returns a JCR path if this authorizable instance is associated with an + * item that can be accessed by the editing Session. + *

    + * Throws UnsupportedRepositoryOperationException if this + * method is not supported or if there is no item associated with this + * authorizable that is accessible by the editing Session. + *

    + * Throws RepositoryException if another error occurs while + * retrieving the path. + * + * @return the path of the {@link javax.jcr.Item} that corresponds to this + * Authorizable. + * @throws UnsupportedRepositoryOperationException If this method is not + * supported or if there exists no accessible item associated with this + * Authorizable instance. + * @throws RepositoryException If an error occurs while retrieving the + * Item path. + */ + String getPath() throws UnsupportedRepositoryOperationException, RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/AuthorizableExistsException.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/AuthorizableExistsException.java new file mode 100644 index 00000000000..0832fccfd10 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/AuthorizableExistsException.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.api.security.user; + +import javax.jcr.RepositoryException; + +/** + * AuthorizableExistsException + */ +public class AuthorizableExistsException extends RepositoryException { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 7875416346848889564L; + + public AuthorizableExistsException(String msg) { + super(msg); + } + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/AuthorizableTypeException.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/AuthorizableTypeException.java new file mode 100644 index 00000000000..a70c707aa6f --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/AuthorizableTypeException.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.api.security.user; + +import javax.jcr.RepositoryException; + +/** + * The {@code AuthorizableTypeException} signals an {@link Authorizable} type mismatch. + */ +public class AuthorizableTypeException extends RepositoryException { + + public AuthorizableTypeException(String msg) { + super(msg); + } +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Group.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Group.java new file mode 100644 index 00000000000..81bc7bb081b --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Group.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.api.security.user; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import java.util.Iterator; +import java.util.Set; + +import org.jetbrains.annotations.NotNull; + +/** + * A Group is a collection of {@link #getMembers() Authorizable}s. + */ +public interface Group extends Authorizable { + + /** + * @return Iterator of Authorizables which are declared + * members of this Group. + * @throws RepositoryException If an error occurs. + */ + Iterator getDeclaredMembers() throws RepositoryException; + + /** + * @return Iterator of Authorizables which are members of + * this Group. This includes both declared members and all authorizables + * that are indirect group members. + * @throws RepositoryException If an error occurs. + */ + Iterator getMembers() throws RepositoryException; + + /** + * Test whether an {@link Authorizable} is a declared member of this group. + * @param authorizable The Authorizable to test. + * @return true if the Authorizable to test is a direct member + * @throws RepositoryException If an error occurs. + */ + boolean isDeclaredMember(Authorizable authorizable) throws RepositoryException; + + /** + * @param authorizable The Authorizable to test. + * @return true if the Authorizable to test is a direct or indirect member + * of this Group. + * @throws RepositoryException If an error occurs. + */ + boolean isMember(Authorizable authorizable) throws RepositoryException; + + /** + * Add a member to this Group. + * + * @param authorizable The Authorizable to be added as + * member to this group. + * @return true if the Authorizable has successfully been added + * to this Group, false otherwise (e.g. unknown implementation + * or if it already is a member or if the passed authorizable is this + * group itself or for some implementation specific constraint). + * @throws RepositoryException If an error occurs. + */ + boolean addMember(Authorizable authorizable) throws RepositoryException; + + /** + * Add one or more member(s) to this Group. Note, that an implementation may + * define circumstances under which this method allows to add non-existing + * {@code Authorizable}s as new members. Also an implementation may choose to + * (partially) postpone validation/verification util {@link Session#save()}. + * + * @param memberIds The {@code Id}s of the authorizables to be added as + * members to this group. + * @return a set of those {@code memberIds} that could not be added or an + * empty set of all ids have been successfully processed. The former may include + * those cases where a given id cannot be resolved to an existing authorizable, + * one that is already member or if adding the member would create a + * cyclic group membership. + * @throws RepositoryException If one of the specified memberIds is invalid or + * if some other error occurs. + */ + Set addMembers(@NotNull String... memberIds) throws RepositoryException; + + /** + * Remove a member from this Group. + * + * @param authorizable The Authorizable to be removed from + * the list of group members. + * @return true if the Authorizable was successfully removed. False otherwise. + * @throws RepositoryException If an error occurs. + */ + boolean removeMember(Authorizable authorizable) throws RepositoryException; + + /** + * Remove one or several members from this Group. Note, that an implementation + * may define circumstances under which this method allows to remove members + * that (no longer) exist. An implementation may choose to (partially) + * postpone validation/verification util {@link Session#save()}. + * + * @param memberIds The {@code Id}s of the authorizables to be removed + * from the members of this group. + * @return a set of those {@code memberIds} that could not be removed or an + * empty set if all ids have been successfully processed. The former may include + * those cases where a given id cannot be resolved to an existing authorizable. + * @throws RepositoryException If one of the specified memberIds is invalid + * or if some other error occurs. + */ + Set removeMembers(@NotNull String... memberIds) throws RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Impersonation.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Impersonation.java new file mode 100644 index 00000000000..e03a223ce4f --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Impersonation.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.api.security.user; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; + +import javax.jcr.RepositoryException; +import javax.security.auth.Subject; +import java.security.Principal; + +/** + * The Impersonation maintains Principals that are allowed to + * impersonate. Principals can be added or removed using + * {@link #grantImpersonation(Principal)} and + * {@link #revokeImpersonation(Principal)}, respectively. + * + * @see User#getImpersonation() + */ +public interface Impersonation { + + /** + * @return An iterator over the Principals that are allowed + * to impersonate the User this Impersonation + * object has been created for. + * @throws RepositoryException If an error occurs. + */ + PrincipalIterator getImpersonators() throws RepositoryException; + + /** + * @param principal The principal that should be allowed to impersonate + * the User this Impersonation has been built for. + * @return true if the specified Principal has not been allowed + * to impersonate before and if impersonation has been successfully + * granted to it, false otherwise. + * @throws RepositoryException If an error occurs. + */ + boolean grantImpersonation(Principal principal) throws RepositoryException; + + /** + * @param principal The principal that should no longer be allowed to + * impersonate. + * @return If the granted impersonation has been successfully revoked for + * the given principal; false otherwise. + * @throws RepositoryException If an error occurs. + */ + boolean revokeImpersonation(Principal principal) throws RepositoryException; + + /** + * Test if the given subject (i.e. any of the principals it contains) is + * allowed to impersonate. + * + * @param subject to impersonate. + * @return true if this Impersonation allows the specified + * Subject to impersonate. + * @throws RepositoryException If an error occurs. + */ + boolean allows(Subject subject) throws RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Query.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Query.java new file mode 100644 index 00000000000..65c2a194e67 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Query.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.api.security.user; + +/** + * A query to match {@link Authorizable}s. Pass an instance of this interface to + * {@link UserManager#findAuthorizables(Query)}. + * + * The following query finds all users named 'Bob' which have the word + * 'engineer' in its description and returns them in ascending order wrt. to + * the name. + * + *

    + *  Iterator<Authorizable> result = userMgr.findAuthorizables(new Query() {
    + *      public <T> void build(QueryBuilder<T> builder) {
    + *          builder.setCondition(builder.
    + *              and(builder.
    + *                  property("@name", RelationOp.EQ, valueFactory.createValue("Bob")), builder.
    + *                  contains("@description", "engineer")));
    + *
    + *          builder.setSortOrder("@name", Direction.ASCENDING);
    + *          builder.setSelector(Selector.USER);
    + *      }
    + *  });
    + * 
    + */ +public interface Query { + + /** + * Build the query using a {@link QueryBuilder}. + * @param builder A query builder for building the query. + * @param Opaque type of the query builder. + */ + void build(QueryBuilder builder); +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/QueryBuilder.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/QueryBuilder.java new file mode 100644 index 00000000000..6a49b882aa5 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/QueryBuilder.java @@ -0,0 +1,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.api.security.user; + +import javax.jcr.Value; + +public interface QueryBuilder { + + /** + * The sort order of the result set of a query. + */ + enum Direction { + ASCENDING("ascending"), + DESCENDING("descending"); + + private final String direction; + + Direction(String direction) { + this.direction = direction; + } + + public String getDirection() { + return direction; + } + } + + /** + * Set the selector for the query. The selector determines whether the query + * returns all {@link Authorizable}s or just {@link User}s respectively {@link Group}s. + * + * @param selector The selector for the query + */ + void setSelector(Class selector); + + /** + * Set the scope for the query. If set, the query will only return members of a specific group. + * + * @param groupName Name of the group to restrict the query to. + * @param declaredOnly If true only declared members of the groups are returned. + * Otherwise indirect memberships are also considered. + */ + void setScope(String groupName, boolean declaredOnly); + + /** + * Set the condition for the query. The query only includes {@link Authorizable}s + * for which this condition holds. + * + * @param condition Condition upon which Authorizables are included in the query result + */ + void setCondition(T condition); + + /** + * Set the sort order of the {@link Authorizable}s returned by the query. + * The format of the propertyName is the same as in XPath: + * @propertyName sorts on a property of the current node. + * relative/path/@propertyName sorts on a property of a + * descendant node. + * + * @param propertyName The name of the property to sort on + * @param direction Direction to sort. Either {@link Direction#ASCENDING} or {@link Direction#DESCENDING} + * @param ignoreCase Ignore character case in sort if true. Note: For false + * sorting is done lexicographically even for non string properties. + */ + void setSortOrder(String propertyName, Direction direction, boolean ignoreCase); + + /** + * Set the sort order of the {@link Authorizable}s returned by the query. + * The format of the propertyName is the same as in XPath: + * @propertyName sorts on a property of the current node. + * relative/path/@propertyName sorts on a property of a + * descendant node. Character case is taken into account for the sort order. + * + * @param propertyName The name of the property to sort on + * @param direction Direction to sort. Either {@link Direction#ASCENDING} or {@link Direction#DESCENDING} + */ + void setSortOrder(String propertyName, Direction direction); + + /** + * Set limits for the query. The limits consists of a bound and a maximal + * number of results. The bound refers to the value of the + * {@link #setSortOrder(String, Direction) sort order} property. The + * query returns at most maxCount {@link Authorizable}s whose + * values of the sort order property follow bound in the sort + * direction. This method has no effect if the sort order is not specified. + * + * @param bound Bound from where to start returning results. null + * for no bound + * @param maxCount Maximal number of results to return. -1 for no limit. + */ + void setLimit(Value bound, long maxCount); + + /** + * Set limits for the query. The limits consists of an offset and a maximal + * number of results. offset refers to the offset within the full + * result set at which the returned result set should start expressed in terms + * of the number of {@link Authorizable}s to skip. maxCount sets the + * maximum size of the result set expressed in terms of the number of authorizables + * to return. + * + * @param offset Offset from where to start returning results. 0 for no offset. + * @param maxCount Maximal number of results to return. -1 for no limit. + */ + void setLimit(long offset, long maxCount); + + /** + * Create a condition which holds if the name of the {@link Authorizable} + * matches a pattern. + * The percent character "%" represents any string of zero or more characters and the + * underscore character "_" represents any single character. Any literal use of these characters + * and the backslash character "\" must be escaped with a backslash character. + * The pattern is matched against the {@link Authorizable#getID() id} and the + * {@link Authorizable#getPrincipal() principal}. + * + * @param pattern Pattern to match the name of an authorizable. + * @return A condition + */ + T nameMatches(String pattern); + + /** + * Create a condition which holds if the node of an {@link Authorizable} has a + * property at relPath which is not equal to value. + * The format of the relPath argument is the same as in XPath: + * @attributeName for an attribute on this node and + * relative/path/@attributeName for an attribute of a descendant node. + * + * @param relPath Relative path from the authorizable's node to the property + * @param value Value to compare the property at relPath to + * @return A condition + */ + T neq(String relPath, Value value); + + /** + * Create a condition which holds if the node of an {@link Authorizable} has a + * property at relPath which is equal to value. + * The format of the relPath argument is the same as in XPath: + * @attributeName for an attribute on this node and + * relative/path/@attributeName for an attribute of a descendant node. + * + * @param relPath Relative path from the authorizable's node to the property + * @param value Value to compare the property at relPath to + * @return A condition + */ + T eq(String relPath, Value value); + + /** + * Create a condition which holds if the node of an {@link Authorizable} has a + * property at relPath which is smaller than value. + * The format of the relPath argument is the same as in XPath: + * @attributeName for an attribute on this node and + * relative/path/@attributeName for an attribute of a descendant node. + * + * @param relPath Relative path from the authorizable's node to the property + * @param value Value to compare the property at relPath to + * @return A condition + */ + T lt(String relPath, Value value); + + /** + * Create a condition which holds if the node of an {@link Authorizable} has a + * property at relPath which is smaller than or equal to value. + * The format of the relPath argument is the same as in XPath: + * @attributeName for an attribute on this node and + * relative/path/@attributeName for an attribute of a descendant node. + * + * @param relPath Relative path from the authorizable's node to the property + * @param value Value to compare the property at relPath to + * @return A condition + */ + T le(String relPath, Value value); + + /** + * Create a condition which holds if the node of an {@link Authorizable} has a + * property at relPath which is greater than value. + * The format of the relPath argument is the same as in XPath: + * @attributeName for an attribute on this node and + * relative/path/@attributeName for an attribute of a descendant node. + * + * @param relPath Relative path from the authorizable's node to the property + * @param value Value to compare the property at relPath to + * @return A condition + */ + T gt(String relPath, Value value); + + /** + * Create a condition which holds if the node of an {@link Authorizable} has a + * property at relPath which is greater than or equal to value. + * The format of the relPath argument is the same as in XPath: + * @attributeName for an attribute on this node and + * relative/path/@attributeName for an attribute of a descendant node. + * + * @param relPath Relative path from the authorizable's node to the property + * @param value Value to compare the property at relPath to + * @return A condition + */ + T ge(String relPath, Value value); + + /** + * Create a condition which holds if the node of an {@link Authorizable} has a + * property at relPath. + * The format of the relPath argument is the same as in XPath: + * @attributeName for an attribute on this node and + * relative/path/@attributeName for an attribute of a descendant node. + * + * @param relPath Relative path from the authorizable's node to the property + * @return A condition + */ + T exists(String relPath); + + /** + * Create a condition which holds if the node of an {@link Authorizable} has a + * property at relPath which matches the pattern in pattern. + * The percent character "%" represents any string of zero or more characters and the + * underscore character "_" represents any single character. Any literal use of these characters + * and the backslash character "\" must be escaped with a backslash character. + * The format of the relPath argument is the same as in XPath: + * @attributeName for an attribute on this node and + * relative/path/@attributeName for an attribute of a descendant node. + * + * @param relPath Relative path from the authorizable's node to the property + * @param pattern Pattern to match the property at relPath against + * @return A condition + */ + T like(String relPath, String pattern); + + /** + * Create a full text search condition. The condition holds if the node of an + * {@link Authorizable} has a property at relPath for which + * searchExpr yields results. + * The format of the relPath argument is the same as in XPath: + * . searches all properties of the current node, @attributeName + * searches the attributeName property of the current node, relative/path/. + * searches all properties of the descendant node at relative/path and + * relative/path/@attributeName searches the attributeName property + * of the descendant node at relative/path. + * The syntax of searchExpr is
    [-]value { [OR] [-]value }
    . + * + * @param relPath Relative path from the authorizable's node to the property + * @param searchExpr A full text search expression + * @return A condition + */ + T contains(String relPath, String searchExpr); + + /** + * Create a condition which holds for {@link Authorizable}s which can impersonate as + * name. + * + * @param name Name of an authorizable + * @return A condition + */ + T impersonates(String name); + + /** + * Return a condition which holds if condition does not hold. + * + * @param condition Condition to negate + * @return A condition + */ + T not(T condition); + + /** + * Return a condition which holds if both sub conditions hold. + * + * @param condition1 first sub condition + * @param condition2 second sub condition + * @return A condition + */ + T and(T condition1, T condition2); + + /** + * Return a condition which holds if any of the two sub conditions hold. + * + * @param condition1 first sub condition + * @param condition2 second sub condition + * @return A condition + */ + T or(T condition1, T condition2); +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/User.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/User.java new file mode 100644 index 00000000000..e02e3db9a4a --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/User.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.user; + +import javax.jcr.RepositoryException; +import javax.jcr.Credentials; + +/** + * User is a special {@link Authorizable} that can be authenticated and + * impersonated. + * + * @see #getCredentials() + * @see #getImpersonation() + */ +public interface User extends Authorizable { + + /** + * @return true if the current user represents the administrator. + */ + boolean isAdmin(); + + /** + * @return true if the current user represents a system user. + */ + boolean isSystemUser(); + + /** + * Returns the internal Credentials representation for this + * user. This method is expected to be used for validation during the + * login process. However, the return value should neither be usable nor + * used for {@link javax.jcr.Repository#login}. + * + * @return Credentials for this user. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + Credentials getCredentials() throws RepositoryException; + + /** + * @return Impersonation for this User. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + Impersonation getImpersonation() throws RepositoryException; + + /** + * Change the password of this user. + * + * @param password The new password. + * @throws RepositoryException If an error occurs. + */ + void changePassword(String password) throws RepositoryException; + + /** + * Change the password of this user. + * + * @param password The new password. + * @param oldPassword The old password. + * @throws RepositoryException If the old password doesn't match or if + * an error occurs. + */ + void changePassword(String password, String oldPassword) throws RepositoryException; + + /** + * Disable this user thus preventing future login if the reason + * is a non-null String.
    + * Note however, that this user will still be accessible by + * {@link UserManager#getAuthorizable}. + * + * @param reason String describing the reason for disable this user or + * null if the user account should be enabled again. + * @throws RepositoryException If an error occurs. + */ + void disable(String reason) throws RepositoryException; + + /** + * Returns true if this user is disabled, false + * otherwise. + * + * @return true if this user is disabled, false + * otherwise. + * @throws RepositoryException If an error occurs. + */ + boolean isDisabled() throws RepositoryException; + + /** + * Returns the String specified upon disabling this user or null + * if {@link #isDisabled()} returns false. + * + * @return The reason specified upon disabling this user or null + * if this user is not disabled. + * @throws RepositoryException If an error occurs. + */ + String getDisabledReason() throws RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/UserManager.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/UserManager.java new file mode 100644 index 00000000000..cc46db1a29e --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/UserManager.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.user; + +import java.security.Principal; +import java.util.Iterator; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * The UserManager provides access to and means to maintain + * {@link Authorizable authorizable objects} i.e. {@link User users} and + * {@link Group groups}. The UserManager is bound to a particular + * Session. + *

    + * Note that all create calls will modify the session associated + * with the {@linkplain UserManager} (whether this is the current session or not + * depends on the repository configuration). If the user manager is not + * in "autosave" mode (see {@link UserManager#isAutoSave()}), problems like + * overlapping creation of intermediate nodes may only surface upon a subsequent + * {@link Session#save()} operation; callers should be prepared to repeat them + * in case this happens. + */ +@ProviderType +public interface UserManager { + + /** + * Filter flag indicating that only Users should be searched + * and returned. + */ + int SEARCH_TYPE_USER = 1; + + /** + * Filter flag indicating that only Groups should be searched + * and returned. + */ + int SEARCH_TYPE_GROUP = 2; + + /** + * Filter flag indicating that all Authorizables should be + * searched. + */ + int SEARCH_TYPE_AUTHORIZABLE = 3; + + /** + * Get the Authorizable by its id. + * + * @param id The user or group id. + * @return Authorizable or null, if not present. + * @throws RepositoryException If an error occurs. + * @see Authorizable#getID() + */ + Authorizable getAuthorizable(String id) throws RepositoryException; + + /** + * Get the Authorizable of a specific type by its id. + * + * @param id the user or group id. + * @param authorizableClass the class of the type of Authorizable required; must not be null. + * @param the required Authorizable type. + * @return Authorizable or null, if not present. + * @throws AuthorizableTypeException If an authorizable exists but is not of the requested type. + * @throws RepositoryException If an error occurs + */ + T getAuthorizable(String id, Class authorizableClass) throws AuthorizableTypeException, RepositoryException; + + /** + * Get the Authorizable by its Principal. + * + * @param principal The principal of the authorizable to retrieve. + * @return Authorizable or null, if not present. + * @throws RepositoryException If an error occurs. + */ + Authorizable getAuthorizable(Principal principal) throws RepositoryException; + + /** + * In accordance to {@link org.apache.jackrabbit.api.security.user.Authorizable#getPath()} + * this method allows to retrieve an given authorizable by it's path. + * + * @param path The path to an authorizable. + * @return Authorizable or null, if not present. + * @throws UnsupportedRepositoryOperationException If this implementation does + * support to retrieve authorizables by path. + * @throws RepositoryException If another error occurs. + * @see org.apache.jackrabbit.api.security.user.Authorizable#getPath() + */ + Authorizable getAuthorizableByPath(String path) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Returns all Authorizables that have a + * {@link Authorizable#getProperty(String) property} with the given relative + * path (or name) that matches the specified value. + *

    + * If a relative path with more than one segment is specified only properties + * exactly matching that patch will be returned. If, however, a name is + * specified all properties that may be retrieved using + * {@link Authorizable#getProperty(String)} will be searched for a match. + * + * @param relPath A relative property path or name. + * @param value A string value to match. + * @return All Authorizables that have a property with the given + * name exactly matching the given value. + * @throws RepositoryException If an error occurs. + * @see Authorizable#getProperty(String) + */ + Iterator findAuthorizables(String relPath, String value) throws RepositoryException; + + /** + * Returns all Authorizables that have a + * {@link Authorizable#getProperty(String) property} with the given relative + * path (or name) that matches the specified value. In contrast to + * {@link #findAuthorizables(String, String)} the type of authorizable is + * respected while executing the search. + *

    + * If a relative path with more than one segment is specified only properties + * exactly matching that path will be returned. If, however, a name is + * specified all properties that may be retrieved using + * {@link Authorizable#getProperty(String)} will be searched for a match. + * + * @param relPath A relative property path or name. + * @param value A string value to match. + * @param searchType Any of the following constants: + *

      + *
    • {@link #SEARCH_TYPE_AUTHORIZABLE}
    • + *
    • {@link #SEARCH_TYPE_GROUP}
    • + *
    • {@link #SEARCH_TYPE_USER}
    • + *
    + * @return An iterator of Authorizable. + * @throws RepositoryException If an error occurs. + */ + Iterator findAuthorizables(String relPath, String value, int searchType) throws RepositoryException; + + /** + * Return {@link Authorizable}s that match a specific {@link Query}. + * + * @param query A query + * @return Iterator of authorizables witch match the query. + * @throws RepositoryException If an error occurs. + */ + Iterator findAuthorizables(Query query) throws RepositoryException; + + /** + * Creates an User for the given userID / password pair; neither of the + * specified parameters can be null.
    + * Same as {@link #createUser(String,String,Principal,String)} where + * the specified userID is equal to the principal name and the intermediate + * path is null. + * + * @param userID The ID of the new user. + * @param password The initial password of this user. + * @return The new User. + * @throws AuthorizableExistsException in case the given userID is already + * in use or another Authorizable with the same principal name exists. + * @throws RepositoryException If another error occurs. + */ + User createUser(String userID, String password) throws AuthorizableExistsException, RepositoryException; + + /** + * Creates an User for the given parameters. If the implementation is not + * able to deal with the intermediatePath that parameter should + * be ignored. + * Except for the intermediatePath, neither of the specified + * parameters can be null. + * + * @param userID The ID of the new user. + * @param password The initial password of the new user. + * @param principal The principal of the new user. + * @param intermediatePath An optional intermediate path used to create the + * new user. If the intermediate path is null an internal, + * implementation specific structure will be used. + * @return The new User. + * @throws AuthorizableExistsException in case the given userID is already + * in use or another Authorizable with the same principal name exists. + * @throws RepositoryException If the current Session is + * not allowed to create users or some another error occurs. + */ + User createUser(String userID, String password, Principal principal, + String intermediatePath) throws AuthorizableExistsException, RepositoryException; + + + /** + * Create a new system user for the specified {@code userID}. The new authorizable + * is required to have the following characteristics: + * + *
      + *
    • {@link org.apache.jackrabbit.api.security.user.User#isSystemUser()} returns {@code true}.
    • + *
    • The system user doesn't have a password set and doesn't allow change the password.
    • + *
    • The principal name is generated by the system; it may be the same as {@code userID}.
    • + *
    • A given implementation may choose to keep system users in a dedicated + * location and thus may impose restrictions on the {@code intermediatePath}.
    • + *
    + * + * @param userID A valid userID. + * @param intermediatePath An optional intermediate path to create the new + * system user. The implemenation may decide to reject intermediate paths + * if they violate an implementation specific requirement with respect to + * the location where systems users are being held. If the intermediate path + * is {@code null} an internal implementation specific structure will be used. + * @return The new system user. + * @throws AuthorizableExistsException if an Authorizable with this id already exists. + * @throws RepositoryException If another error occurs. + */ + User createSystemUser(String userID, String intermediatePath) throws AuthorizableExistsException, RepositoryException; + + /** + * Creates a Group for the given groupID, which must not be null. + *
    + * Same as {@link #createGroup(String, Principal,String)} where the specified + * groupID is the name of the Principal the intermediate path + * is null. + * + * @param groupID The ID of the new group; must not be null. + * @return The new Group. + * @throws AuthorizableExistsException in case the given groupID is already + * in use or another {@link Authorizable} with the same + * {@link Authorizable#getID() ID} or principal name already exists. + * @throws RepositoryException If another error occurs. + */ + Group createGroup(String groupID) throws AuthorizableExistsException, RepositoryException; + + /** + * Creates a new Group that is based on the given principal. + * Note that the group's ID is implementation specific. The implementation + * may take the principal name as ID hint but must in any case assert that + * it is unique among the IDs known to this manager. + * + * @param principal A non-null Principal + * @return The new Group. + * @throws AuthorizableExistsException in case the given principal is + * already in use with another Authorizable. + * @throws RepositoryException If another error occurs. + */ + Group createGroup(Principal principal) throws AuthorizableExistsException, RepositoryException; + + /** + * Same as {@link #createGroup(String, Principal, String)} where the + * name of the specified principal is used to create the group's ID. + * + * @param principal The principal associated with the new group. + * @param intermediatePath An optional intermediate path used to create the + * new group. If the intermediate path is null an internal, + * implementation specific structure will be used. + * @return The new Group. + * @throws AuthorizableExistsException in case the given principal is + * already in use with another Authorizable. + * @throws RepositoryException If another error occurs. + */ + Group createGroup(Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException; + + /** + * Creates a new Group that is based on the given id, principal + * and the specified intermediatePath hint. If the implementation + * is not able to deal with the intermediatePath this parameter + * should be ignored. + * + * @param groupID The ID of the new group. + * @param principal The principal of the new group. + * @param intermediatePath An optional intermediate path used to create the + * new group. If the intermediate path is null an internal, + * implementation specific structure will be used. + * @return The new Group. + * @throws AuthorizableExistsException in case the given principal is already + * in use with another Authorizable. + * @throws RepositoryException If another error occurs. + */ + Group createGroup(String groupID, Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException; + + /** + * If any write operations executed through the User API are automatically + * persisted this method returns true. In this case there are + * no pending transient changes left and there is no need to explicitly call + * {@link javax.jcr.Session#save()}. If this method returns false + * any changes must be completed by an extra save call on the + * Session associated with this UserManager. + * + * @return true if changes are automatically persisted; + * false if changes made through this API (including method + * calls on {@link Authorizable} and subclasses are only transient and + * must be persisted using {@link javax.jcr.Session#save()}. + * @see #autoSave(boolean) + */ + boolean isAutoSave(); + + /** + * Changes the auto save behavior of this UserManager. + *

    + * Note, that this shouldn't be allowed in cases where the associated session + * is different from the original session accessing the user manager. + * + * @param enable If true changes made through this API will + * be automatically saved; otherwise an explicit call to + * {@link javax.jcr.Session#save()} is required in order to persist changes. + * @throws UnsupportedRepositoryOperationException If the implementation + * does not allow to change the auto save behavior. + * @throws RepositoryException If some other error occurs. + */ + void autoSave(boolean enable) throws UnsupportedRepositoryOperationException, RepositoryException; +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/package-info.java new file mode 100644 index 00000000000..50e7742eeb0 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/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. + */ + +/** + * Jackrabbit extensions for user management. + */ +@org.osgi.annotation.versioning.Version("2.4.2") +package org.apache.jackrabbit.api.security.user; diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/QueryStat.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/QueryStat.java new file mode 100644 index 00000000000..13786db55ba --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/QueryStat.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.api.stats; + +/** + * Statistics on query operations + * + */ +public interface QueryStat { + + /** + * @return a sorted array containing the top + * {@link #getSlowQueriesQueueSize()} slowest queries + */ + QueryStatDto[] getSlowQueries(); + + /** + * @return size of the Slow queue + */ + int getSlowQueriesQueueSize(); + + /** + * Change the size of the Slow queue + * + * @param size + * the new size + */ + void setSlowQueriesQueueSize(int size); + + /** + * clears the Slow queue + */ + void clearSlowQueriesQueue(); + + /** + * @return a sorted array containing the + * {@link #getPopularQueriesQueueSize()} most popular queries + */ + QueryStatDto[] getPopularQueries(); + + /** + * @return size of the Popular queue + */ + int getPopularQueriesQueueSize(); + + /** + * Change the size of the Popular queue + * + * @param size + * the new size + */ + void setPopularQueriesQueueSize(int size); + + /** + * clears the Popular queue + */ + void clearPopularQueriesQueue(); + + /** -- GENERAL OPS -- **/ + + /** + * If this service is currently registering stats + * + * @return true if the service is enabled + */ + boolean isEnabled(); + + /** + * Enables/Disables the service + * + * @param enabled + */ + void setEnabled(boolean enabled); + + /** + * clears all data + */ + void reset(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/QueryStatDto.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/QueryStatDto.java new file mode 100644 index 00000000000..4f66fcada87 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/QueryStatDto.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.api.stats; + +import java.io.Serializable; + +/** + * Object that holds statistical info about a query. + * + */ +public interface QueryStatDto extends Serializable { + + long getDuration(); + + String getLanguage(); + + String getStatement(); + + String getCreationTime(); + + int getOccurrenceCount(); + + long getPosition(); + + void setPosition(long position); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/RepositoryStatistics.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/RepositoryStatistics.java new file mode 100644 index 00000000000..4d206262d91 --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/RepositoryStatistics.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.api.stats; + + +/** + * Statistics on core repository operations + * + */ +public interface RepositoryStatistics { + + /** + * The values of this enum determine the type of the time + * series returned by {@link #getTimeSeries(Type)} + * and link {@link #getTimeSeries(String, boolean)}. + */ + enum Type { + BUNDLE_READ_COUNTER(true), + BUNDLE_WRITE_COUNTER(true), + BUNDLE_WRITE_DURATION(true), + BUNDLE_WRITE_AVERAGE(false), + BUNDLE_CACHE_ACCESS_COUNTER(true), + BUNDLE_CACHE_SIZE_COUNTER(false), + BUNDLE_CACHE_MISS_COUNTER(true), + BUNDLE_CACHE_MISS_DURATION(true), + BUNDLE_CACHE_MISS_AVERAGE(false), + BUNDLE_COUNTER(true), + BUNDLE_WS_SIZE_COUNTER(true), + + /** + * Number of read accesses through any session. + */ + SESSION_READ_COUNTER(true), + + /** + * Total time spent reading from sessions in nano seconds. + */ + SESSION_READ_DURATION(true), + + /** + * Average time spent reading from sessions in nano seconds. + * This is the sum of all read durations divided by the number + * of reads in the respective time period. + */ + SESSION_READ_AVERAGE(false), + + /** + * Number of write accesses through any session. + */ + SESSION_WRITE_COUNTER(true), + + /** + * Total time spent writing to sessions in nano seconds. + */ + SESSION_WRITE_DURATION(true), + + /** + * Average time spent writing to sessions in nano seconds. + * This is the sum of all write durations divided by the number + * of writes in the respective time period. + */ + SESSION_WRITE_AVERAGE(false), + + /** + * Number of calls sessions that have been logged in. + */ + SESSION_LOGIN_COUNTER(true), + + /** + * Number of currently logged in sessions. + */ + SESSION_COUNT(false), + + /** + * Number of queries executed. + */ + QUERY_COUNT(true), + + /** + * Total time spent evaluating queries in milli seconds. + */ + QUERY_DURATION(true), + + /** + * Average time spent evaluating queries in milli seconds. + * This is the sum of all query durations divided by the number + * of queries in the respective time period. + */ + QUERY_AVERAGE(true), + + /** + * Total number of observation {@code Event} instances delivered + * to all observation listeners. + */ + OBSERVATION_EVENT_COUNTER(true), + + /** + * Total time spent processing observation events by all observation + * listeners in nano seconds. + */ + OBSERVATION_EVENT_DURATION(true), + + /** + * Average time spent processing observation events by all observation + * listeners in nano seconds. + * This is the sum of all observation durations divided by the number + * of observation events in the respective time period. + */ + OBSERVATION_EVENT_AVERAGE(true); + + private final boolean resetValueEachSecond; + + Type(final boolean resetValueEachSecond) { + this.resetValueEachSecond = resetValueEachSecond; + } + + public static Type getType(String type) { + Type realType = null; + try { + realType = valueOf(type); + } catch (IllegalArgumentException ignore) {} + return realType; + } + + public boolean isResetValueEachSecond() { + return resetValueEachSecond; + } + } + + TimeSeries getTimeSeries(Type type); + + TimeSeries getTimeSeries(String type, boolean resetValueEachSecond); +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/TimeSeries.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/TimeSeries.java new file mode 100755 index 00000000000..6c21a32265b --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/TimeSeries.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.api.stats; + +/** + * Interface for a time series of the measured values per + * second, minute, hour and day. The type of the value is arbitrary; it + * could be cache hits or misses, disk reads or writes, created sessions, + * completed transactions, or pretty much anything of interest. + * + * @since Apache Jackrabbit 2.3.2 + */ +public interface TimeSeries { + + /** + * Returns the measured value per second over the last minute. + * + * @return measured value per second, in chronological order + */ + long[] getValuePerSecond(); + + /** + * Returns the measured value per minute over the last hour. + * + * @return measured value per minute, in chronological order + */ + long[] getValuePerMinute(); + + /** + * Returns the measured value per hour over the last week. + * + * @return measured value per hour, in chronological order + */ + long[] getValuePerHour(); + + /** + * Returns the measured value per week over the last three years. + * + * @return measured value per week, in chronological order + */ + long[] getValuePerWeek(); + + /** + * The value used to encode missing values i.e. for a period where no value was recorded. + * + * @return default value + */ + long getMissingValue(); + +} diff --git a/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/package-info.java b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/package-info.java new file mode 100644 index 00000000000..dc88a2d541c --- /dev/null +++ b/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/stats/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. + */ + +/** + * Jackrabbit repository statistics + */ +@org.osgi.annotation.versioning.Version("1.2.0") +package org.apache.jackrabbit.api.stats; diff --git a/jackrabbit-api/src/main/javadoc/org/apache/jackrabbit/api/package.html b/jackrabbit-api/src/main/javadoc/org/apache/jackrabbit/api/package.html new file mode 100644 index 00000000000..135c2bfaea9 --- /dev/null +++ b/jackrabbit-api/src/main/javadoc/org/apache/jackrabbit/api/package.html @@ -0,0 +1,19 @@ + + +Contains set of interfaces that form the Jackrabbit-specific extensions to the JCR API + diff --git a/jackrabbit-aws-ext/README.txt b/jackrabbit-aws-ext/README.txt new file mode 100644 index 00000000000..864329c95e7 --- /dev/null +++ b/jackrabbit-aws-ext/README.txt @@ -0,0 +1,28 @@ +==================================================== +Welcome to Jackrabbit Amazon WebServices Extension +==================================================== + +This is the Amazon Webservices Extension component of the Apache Jackrabbit project. +This component contains S3 Datastore which stores binaries on Amazon S3 (http://aws.amazon.com/s3). + +==================================================== +Build Instructions +==================================================== +To build the latest SNAPSHOT versions of all the components +included here, run the following command with Maven 3: + + mvn clean install + +To run testcases which stores in S3 bucket, please pass aws config file via system property. For e.g. + + mvn clean install -DargLine="-Dconfig=/opt/cq/aws.properties" + +Sample aws properties located at src/test/resources/aws.properties + +==================================================== +Configuration Instructions +==================================================== +It require to configure aws.properties to configure S3 Datastore. + + + diff --git a/jackrabbit-aws-ext/pom.xml b/jackrabbit-aws-ext/pom.xml new file mode 100644 index 00000000000..07f13293fad --- /dev/null +++ b/jackrabbit-aws-ext/pom.xml @@ -0,0 +1,113 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-aws-ext + Jackrabbit AWS Extension + Jackrabbit extenstion to Amazon Webservices + bundle + + + + + + + javax.jcr + jcr + + + org.osgi + org.osgi.annotation + provided + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + com.amazonaws + aws-java-sdk-s3 + 1.11.330 + + + org.apache.jackrabbit + jackrabbit-data + ${project.version} + + + org.apache.jackrabbit + jackrabbit-data + ${project.version} + test-jar + + + org.slf4j + slf4j-api + + + + junit + junit + test + + + org.slf4j + slf4j-log4j12 + test + + + + + + maven-surefire-plugin + + + **/aws/**/TestAll.java + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.jackrabbit.aws.ext.ds + sun.io + + + + + org.apache.rat + apache-rat-plugin + + + .checkstyle + + + + + + diff --git a/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/S3Constants.java b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/S3Constants.java new file mode 100644 index 00000000000..29da427a06d --- /dev/null +++ b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/S3Constants.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.aws.ext; + +/** + * Defined Amazon S3 constants. + */ +public final class S3Constants { + + /** + * Amazon aws access key. + */ + public static final String ACCESS_KEY = "accessKey"; + + /** + * Amazon aws secret key. + */ + public static final String SECRET_KEY = "secretKey"; + + /** + * Amazon S3 Http connection timeout. + */ + public static final String S3_CONN_TIMEOUT = "connectionTimeout"; + + /** + * Amazon S3 socket timeout. + */ + public static final String S3_SOCK_TIMEOUT = "socketTimeout"; + + /** + * Amazon S3 maximum connections to be used. + */ + public static final String S3_MAX_CONNS = "maxConnections"; + + /** + * Amazon S3 maximum retries. + */ + public static final String S3_MAX_ERR_RETRY = "maxErrorRetry"; + + /** + * Amazon aws S3 bucket. + */ + public static final String S3_BUCKET = "s3Bucket"; + + /** + * Amazon aws S3 region. + */ + public static final String S3_REGION = "s3Region"; + + /** + * Amazon aws S3 region. + */ + public static final String S3_END_POINT = "s3EndPoint"; + + /** + * Constant for S3 Connector Protocol + */ + public static final String S3_CONN_PROTOCOL = "s3ConnProtocol"; + + /** + * Constant to rename keys + */ + public static final String S3_RENAME_KEYS = "s3RenameKeys"; + + /** + * Constant to rename keys + */ + public static final String S3_WRITE_THREADS = "writeThreads"; + + /** + * Constant to enable encryption in S3. + */ + public static final String S3_ENCRYPTION = "s3Encryption"; + + /** + * Constant for no encryption. it is default. + */ + public static final String S3_ENCRYPTION_NONE = "NONE"; + + /** + * Constant to set SSE_S3 encryption. + */ + public static final String S3_ENCRYPTION_SSE_S3 = "SSE_S3"; + + /** + * Constant to set proxy host. + */ + public static final String PROXY_HOST = "proxyHost"; + + /** + * Constant to set proxy port. + */ + public static final String PROXY_PORT = "proxyPort"; + + /** + * private constructor so that class cannot initialized from outside. + */ + private S3Constants() { + + } + +} diff --git a/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/S3RequestDecorator.java b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/S3RequestDecorator.java new file mode 100644 index 00000000000..2fcc2444ffe --- /dev/null +++ b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/S3RequestDecorator.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.aws.ext; + +import java.util.Properties; + +import com.amazonaws.services.s3.model.CopyObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; + +/** + * This class to sets encrption mode in S3 request. + * + */ +public class S3RequestDecorator { + DataEncryption dataEncryption = DataEncryption.NONE; + + public S3RequestDecorator(Properties props) { + if (props.getProperty(S3Constants.S3_ENCRYPTION) != null) { + this.dataEncryption = dataEncryption.valueOf(props.getProperty(S3Constants.S3_ENCRYPTION)); + } + } + + /** + * Set encryption in {@link PutObjectRequest} + */ + public PutObjectRequest decorate(PutObjectRequest request) { + switch (getDataEncryption()) { + case SSE_S3: + ObjectMetadata metadata = request.getMetadata() == null + ? new ObjectMetadata() + : request.getMetadata(); + metadata.setSSEAlgorithm(ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION); + request.setMetadata(metadata); + break; + case NONE: + break; + } + return request; + } + + /** + * Set encryption in {@link CopyObjectRequest} + */ + public CopyObjectRequest decorate(CopyObjectRequest request) { + switch (getDataEncryption()) { + case SSE_S3: + ObjectMetadata metadata = request.getNewObjectMetadata() == null + ? new ObjectMetadata() + : request.getNewObjectMetadata(); + metadata.setSSEAlgorithm(ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION); + request.setNewObjectMetadata(metadata); + break; + case NONE: + break; + } + return request; + } + + private DataEncryption getDataEncryption() { + return this.dataEncryption; + } + + /** + * Enum to indicate S3 encryption mode + * + */ + private enum DataEncryption { + SSE_S3, NONE; + } + +} diff --git a/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/Utils.java b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/Utils.java new file mode 100644 index 00000000000..88c0fc85f22 --- /dev/null +++ b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/Utils.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.aws.ext; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.ClientConfiguration; +import com.amazonaws.Protocol; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.Region; +import com.amazonaws.services.s3.model.S3ObjectSummary; +import com.amazonaws.util.StringUtils; + +/** + * Amazon S3 utilities. + */ +public final class Utils { + + private static final Logger LOG = LoggerFactory.getLogger(Utils.class); + + public static final String DEFAULT_CONFIG_FILE = "aws.properties"; + + private static final String DELETE_CONFIG_SUFFIX = ";burn"; + + /** + * The default value AWS bucket region. + */ + public static final String DEFAULT_AWS_BUCKET_REGION = "us-standard"; + + /** + * constants to define endpoint to various AWS region + */ + public static final String AWSDOTCOM = "amazonaws.com"; + + public static final String S3 = "s3"; + + public static final String DOT = "."; + + public static final String DASH = "-"; + + /** + * private constructor so that class cannot initialized from outside. + */ + private Utils() { + + } + + /** + * Create AmazonS3Client from properties. + * + * @param prop properties to configure @link {@link AmazonS3Client} + * @return {@link AmazonS3Client} + */ + public static AmazonS3Client openService(final Properties prop) { + String accessKey = prop.getProperty(S3Constants.ACCESS_KEY); + String secretKey = prop.getProperty(S3Constants.SECRET_KEY); + AmazonS3Client s3service = null; + if (StringUtils.isNullOrEmpty(accessKey) + || StringUtils.isNullOrEmpty(secretKey)) { + LOG.info("Configuring Amazon Client from environment"); + s3service = new AmazonS3Client(getClientConfiguration(prop)); + } else { + LOG.info("Configuring Amazon Client from property file."); + AWSCredentials credentials = new BasicAWSCredentials(accessKey, + secretKey); + s3service = new AmazonS3Client(credentials, + getClientConfiguration(prop)); + } + String region = prop.getProperty(S3Constants.S3_REGION); + String endpoint = null; + String propEndPoint = prop.getProperty(S3Constants.S3_END_POINT); + if ((propEndPoint != null) && !"".equals(propEndPoint)) { + endpoint = propEndPoint; + } else { + if (StringUtils.isNullOrEmpty(region)) { + com.amazonaws.regions.Region s3Region = Regions.getCurrentRegion(); + if (s3Region != null) { + region = s3Region.getName(); + } else { + throw new AmazonClientException( + "parameter [" + + S3Constants.S3_REGION + + "] not configured and cannot be derived from environment"); + } + } + if (DEFAULT_AWS_BUCKET_REGION.equals(region)) { + endpoint = S3 + DOT + AWSDOTCOM; + } else if (Region.EU_Ireland.toString().equals(region)) { + endpoint = "s3-eu-west-1" + DOT + AWSDOTCOM; + } else { + endpoint = S3 + DASH + region + DOT + AWSDOTCOM; + } + } + /* + * setting endpoint to remove latency of redirection. If endpoint is + * not set, invocation first goes us standard region, which + * redirects it to correct location. + */ + s3service.setEndpoint(endpoint); + LOG.info("S3 service endpoint [{}] ", endpoint); + return s3service; + } + + /** + * Delete S3 bucket. This method first deletes all objects from bucket and + * then delete empty bucket. + * + * @param bucketName the bucket name. + */ + public static void deleteBucket(final String bucketName) throws IOException { + Properties prop = readConfig(DEFAULT_CONFIG_FILE); + AmazonS3 s3service = openService(prop); + ObjectListing prevObjectListing = s3service.listObjects(bucketName); + while (true) { + for (S3ObjectSummary s3ObjSumm : prevObjectListing.getObjectSummaries()) { + s3service.deleteObject(bucketName, s3ObjSumm.getKey()); + } + if (!prevObjectListing.isTruncated()) { + break; + } + prevObjectListing = s3service.listNextBatchOfObjects(prevObjectListing); + } + s3service.deleteBucket(bucketName); + } + + /** + * Read a configuration properties file. If the file name ends with ";burn", + * the file is deleted after reading. + * + * @param fileName the properties file name + * @return the properties + * @throws IOException if the file doesn't exist + */ + public static Properties readConfig(String fileName) throws IOException { + boolean delete = false; + if (fileName.endsWith(DELETE_CONFIG_SUFFIX)) { + delete = true; + fileName = fileName.substring(0, fileName.length() + - DELETE_CONFIG_SUFFIX.length()); + } + if (!new File(fileName).exists()) { + throw new IOException("Config file not found: " + fileName); + } + Properties prop = new Properties(); + InputStream in = null; + try { + in = new FileInputStream(fileName); + prop.load(in); + } finally { + if (in != null) { + in.close(); + } + if (delete) { + deleteIfPossible(new File(fileName)); + } + } + return prop; + } + + private static void deleteIfPossible(final File file) { + boolean deleted = file.delete(); + if (!deleted) { + LOG.warn("Could not delete " + file.getAbsolutePath()); + } + } + + private static ClientConfiguration getClientConfiguration(Properties prop) { + int connectionTimeOut = Integer.parseInt(prop.getProperty(S3Constants.S3_CONN_TIMEOUT)); + int socketTimeOut = Integer.parseInt(prop.getProperty(S3Constants.S3_SOCK_TIMEOUT)); + int maxConnections = Integer.parseInt(prop.getProperty(S3Constants.S3_MAX_CONNS)); + int maxErrorRetry = Integer.parseInt(prop.getProperty(S3Constants.S3_MAX_ERR_RETRY)); + + String protocol = prop.getProperty(S3Constants.S3_CONN_PROTOCOL); + String proxyHost = prop.getProperty(S3Constants.PROXY_HOST); + String proxyPort = prop.getProperty(S3Constants.PROXY_PORT); + + ClientConfiguration cc = new ClientConfiguration(); + + if (protocol != null && protocol.equalsIgnoreCase("http")) { + cc.setProtocol(Protocol.HTTP); + } + + if (proxyHost != null && !proxyHost.isEmpty()) { + cc.setProxyHost(proxyHost); + } + + if (proxyPort != null && !proxyPort.isEmpty()) { + cc.setProxyPort(Integer.parseInt(proxyPort)); + } + + cc.setConnectionTimeout(connectionTimeOut); + cc.setSocketTimeout(socketTimeOut); + cc.setMaxConnections(maxConnections); + cc.setMaxErrorRetry(maxErrorRetry); + + return cc; + } + +} diff --git a/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/S3Backend.java b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/S3Backend.java new file mode 100644 index 00000000000..01e87ddb403 --- /dev/null +++ b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/S3Backend.java @@ -0,0 +1,921 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.aws.ext.ds; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.jackrabbit.aws.ext.S3Constants; +import org.apache.jackrabbit.aws.ext.S3RequestDecorator; +import org.apache.jackrabbit.aws.ext.Utils; +import org.apache.jackrabbit.core.data.AbstractBackend; +import org.apache.jackrabbit.core.data.AsyncTouchCallback; +import org.apache.jackrabbit.core.data.AsyncTouchResult; +import org.apache.jackrabbit.core.data.AsyncUploadCallback; +import org.apache.jackrabbit.core.data.AsyncUploadResult; +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.data.util.NamedThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.event.ProgressEvent; +import com.amazonaws.event.ProgressListener; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CopyObjectRequest; +import com.amazonaws.services.s3.model.DeleteObjectsRequest; +import com.amazonaws.services.s3.model.DeleteObjectsResult; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.Region; +import com.amazonaws.services.s3.model.S3Object; +import com.amazonaws.services.s3.model.S3ObjectSummary; +import com.amazonaws.services.s3.transfer.Copy; +import com.amazonaws.services.s3.transfer.TransferManager; +import com.amazonaws.services.s3.transfer.Upload; +import com.amazonaws.util.StringUtils; + +/** + * A data store backend that stores data on Amazon S3. + */ +public class S3Backend extends AbstractBackend { + + /** + * Logger instance. + */ + private static final Logger LOG = LoggerFactory.getLogger(S3Backend.class); + + private static final String KEY_PREFIX = "dataStore_"; + + private AmazonS3Client s3service; + + private String bucket; + + private TransferManager tmx; + + private Properties properties; + + private Date startTime; + + private S3RequestDecorator s3ReqDecorator; + + /** + * Initialize S3Backend. It creates AmazonS3Client and TransferManager from + * aws.properties. It creates S3 bucket if it doesn't pre-exist in S3. + */ + @Override + public void init(CachingDataStore store, String homeDir, String config) + throws DataStoreException { + super.init(store, homeDir, config); + Properties initProps = null; + //Check is configuration is already provided. That takes precedence + //over config provided via file based config + if(this.properties != null){ + initProps = this.properties; + } else { + if(config == null){ + config = Utils.DEFAULT_CONFIG_FILE; + } + try{ + initProps = Utils.readConfig(config); + }catch(IOException e){ + throw new DataStoreException("Could not initialize S3 from " + + config, e); + } + this.properties = initProps; + } + init(store, homeDir, initProps); + } + + public void init(CachingDataStore store, String homeDir, Properties prop) + throws DataStoreException { + + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + startTime = new Date(); + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + LOG.debug("init"); + setDataStore(store); + s3ReqDecorator = new S3RequestDecorator(prop); + + s3service = Utils.openService(prop); + if (bucket == null || "".equals(bucket.trim())) { + bucket = prop.getProperty(S3Constants.S3_BUCKET); + } + String region = prop.getProperty(S3Constants.S3_REGION); + Region s3Region = null; + if (StringUtils.isNullOrEmpty(region)) { + com.amazonaws.regions.Region ec2Region = Regions.getCurrentRegion(); + if (ec2Region != null) { + s3Region = Region.fromValue(ec2Region.getName()); + } else { + throw new AmazonClientException( + "parameter [" + + S3Constants.S3_REGION + + "] not configured and cannot be derived from environment"); + } + } else { + if (Utils.DEFAULT_AWS_BUCKET_REGION.equals(region)) { + s3Region = Region.US_Standard; + } else if (Region.EU_Ireland.toString().equals(region)) { + s3Region = Region.EU_Ireland; + } else { + s3Region = Region.fromValue(region); + } + } + + if (!s3service.doesBucketExist(bucket)) { + s3service.createBucket(bucket, s3Region); + LOG.info("Created bucket [{}] in [{}] ", bucket, region); + } else { + LOG.info("Using bucket [{}] in [{}] ", bucket, region); + } + + int writeThreads = 10; + String writeThreadsStr = prop.getProperty(S3Constants.S3_WRITE_THREADS); + if (writeThreadsStr != null) { + writeThreads = Integer.parseInt(writeThreadsStr); + } + LOG.info("Using thread pool of [{}] threads in S3 transfer manager.", writeThreads); + tmx = new TransferManager(s3service, + (ThreadPoolExecutor) Executors.newFixedThreadPool(writeThreads, + new NamedThreadFactory("s3-transfer-manager-worker"))); + + int asyncWritePoolSize = 10; + String maxConnsStr = prop.getProperty(S3Constants.S3_MAX_CONNS); + if (maxConnsStr != null) { + asyncWritePoolSize = Integer.parseInt(maxConnsStr) + - writeThreads; + } + setAsyncWritePoolSize(asyncWritePoolSize); + String renameKeyProp = prop.getProperty(S3Constants.S3_RENAME_KEYS); + boolean renameKeyBool = (renameKeyProp == null || "".equals(renameKeyProp)) + ? false + : Boolean.parseBoolean(renameKeyProp); + LOG.info("Rename keys [{}]", renameKeyBool); + if (renameKeyBool) { + renameKeys(); + } + LOG.debug("S3 Backend initialized in [{}] ms", + +(System.currentTimeMillis() - startTime.getTime())); + } catch (Exception e) { + LOG.debug(" error ", e); + throw new DataStoreException("Could not initialize S3 from " + + prop, e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + /** + * It uploads file to Amazon S3. If file size is greater than 5MB, this + * method uses parallel concurrent connections to upload. + */ + @Override + public void write(DataIdentifier identifier, File file) + throws DataStoreException { + this.write(identifier, file, false, null); + + } + + @Override + public void writeAsync(DataIdentifier identifier, File file, + AsyncUploadCallback callback) throws DataStoreException { + if (callback == null) { + throw new IllegalArgumentException( + "callback parameter cannot be null in asyncUpload"); + } + getAsyncWriteExecutor().execute(new AsyncUploadJob(identifier, file, + callback)); + } + + /** + * Check if record identified by identifier exists in Amazon S3. + */ + @Override + public boolean exists(DataIdentifier identifier) throws DataStoreException { + long start = System.currentTimeMillis(); + String key = getKeyName(identifier); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + ObjectMetadata objectMetaData = s3service.getObjectMetadata(bucket, + key); + if (objectMetaData != null) { + LOG.trace("exists [{}]: [true] took [{}] ms.", + identifier, (System.currentTimeMillis() - start) ); + return true; + } + return false; + } catch (AmazonServiceException e) { + if (e.getStatusCode() == 404 || e.getStatusCode() == 403) { + LOG.debug("exists [{}]: [false] took [{}] ms.", + identifier, (System.currentTimeMillis() - start) ); + return false; + } + throw new DataStoreException( + "Error occured to getObjectMetadata for key [" + + identifier.toString() + "]", e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + @Override + public boolean exists(DataIdentifier identifier, boolean touch) + throws DataStoreException { + long start = System.currentTimeMillis(); + String key = getKeyName(identifier); + ObjectMetadata objectMetaData = null; + boolean retVal = false; + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + objectMetaData = s3service.getObjectMetadata(bucket, key); + if (objectMetaData != null) { + retVal = true; + if (touch) { + CopyObjectRequest copReq = new CopyObjectRequest(bucket, + key, bucket, key); + copReq.setNewObjectMetadata(objectMetaData); + Copy copy = tmx.copy(s3ReqDecorator.decorate(copReq)); + copy.waitForCopyResult(); + LOG.debug("[{}] touched took [{}] ms. ", identifier, + (System.currentTimeMillis() - start)); + } + } else { + retVal = false; + } + + } catch (AmazonServiceException e) { + if (e.getStatusCode() == 404 || e.getStatusCode() == 403) { + retVal = false; + } else { + throw new DataStoreException( + "Error occured to find exists for key [" + + identifier.toString() + "]", e); + } + } catch (Exception e) { + throw new DataStoreException( + "Error occured to find exists for key " + + identifier.toString(), e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + LOG.debug("exists [{}]: [{}] took [{}] ms.", new Object[] { identifier, + retVal, (System.currentTimeMillis() - start) }); + return retVal; + } + + @Override + public void touchAsync(final DataIdentifier identifier, + final long minModifiedDate, final AsyncTouchCallback callback) + throws DataStoreException { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + if (callback == null) { + throw new IllegalArgumentException( + "callback parameter cannot be null in touchAsync"); + } + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + + getAsyncWriteExecutor().execute(new Runnable() { + @Override + public void run() { + try { + touch(identifier, minModifiedDate); + callback.onSuccess(new AsyncTouchResult(identifier)); + } catch (DataStoreException e) { + AsyncTouchResult result = new AsyncTouchResult( + identifier); + result.setException(e); + callback.onFailure(result); + } + } + }); + } catch (Exception e) { + callback.onAbort(new AsyncTouchResult(identifier)); + throw new DataStoreException("Cannot touch the record " + + identifier.toString(), e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + + } + + @Override + public void touch(DataIdentifier identifier, long minModifiedDate) + throws DataStoreException { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + final long start = System.currentTimeMillis(); + final String key = getKeyName(identifier); + if (minModifiedDate > 0 + && minModifiedDate > getLastModified(identifier)) { + CopyObjectRequest copReq = new CopyObjectRequest(bucket, key, + bucket, key); + copReq.setNewObjectMetadata(new ObjectMetadata()); + Copy copy = tmx.copy(s3ReqDecorator.decorate(copReq)); + copy.waitForCompletion(); + LOG.debug("[{}] touched. time taken [{}] ms ", new Object[] { + identifier, (System.currentTimeMillis() - start) }); + } else { + LOG.trace("[{}] touch not required. time taken [{}] ms ", + new Object[] { identifier, + (System.currentTimeMillis() - start) }); + } + + } catch (Exception e) { + throw new DataStoreException("Error occured in touching key [" + + identifier.toString() + "]", e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + @Override + public InputStream read(DataIdentifier identifier) + throws DataStoreException { + long start = System.currentTimeMillis(); + String key = getKeyName(identifier); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + S3Object object = s3service.getObject(bucket, key); + InputStream in = object.getObjectContent(); + LOG.debug("[{}] read took [{}]ms", identifier, + (System.currentTimeMillis() - start)); + return in; + } catch (AmazonServiceException e) { + throw new DataStoreException("Object not found: " + key, e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + @Override + public Iterator getAllIdentifiers() + throws DataStoreException { + long start = System.currentTimeMillis(); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + Set ids = new HashSet(); + ObjectListing prevObjectListing = s3service.listObjects(bucket); + while (true) { + for (S3ObjectSummary s3ObjSumm : prevObjectListing.getObjectSummaries()) { + String id = getIdentifierName(s3ObjSumm.getKey()); + if (id != null) { + ids.add(new DataIdentifier(id)); + } + } + if (!prevObjectListing.isTruncated()) break; + prevObjectListing = s3service.listNextBatchOfObjects(prevObjectListing); + } + LOG.debug("getAllIdentifiers returned size [{}] took [{}] ms.", + ids.size(), (System.currentTimeMillis() - start)); + return ids.iterator(); + } catch (AmazonServiceException e) { + throw new DataStoreException("Could not list objects", e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + @Override + public long getLastModified(DataIdentifier identifier) + throws DataStoreException { + long start = System.currentTimeMillis(); + String key = getKeyName(identifier); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + ObjectMetadata object = s3service.getObjectMetadata(bucket, key); + long lastModified = object.getLastModified().getTime(); + LOG.debug( + "Identifier [{}]'s lastModified = [{}] took [{}]ms.", + new Object[] { identifier, lastModified, + (System.currentTimeMillis() - start) }); + return lastModified; + } catch (AmazonServiceException e) { + if (e.getStatusCode() == 404 || e.getStatusCode() == 403) { + LOG.info( + "getLastModified:Identifier [{}] not found. Took [{}] ms.", + identifier, (System.currentTimeMillis() - start)); + } + throw new DataStoreException(e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + @Override + public long getLength(DataIdentifier identifier) throws DataStoreException { + long start = System.currentTimeMillis(); + String key = getKeyName(identifier); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + ObjectMetadata object = s3service.getObjectMetadata(bucket, key); + long length = object.getContentLength(); + LOG.debug("Identifier [{}]'s length = [{}] took [{}]ms.", + new Object[] { identifier, length, + (System.currentTimeMillis() - start) }); + return length; + } catch (AmazonServiceException e) { + throw new DataStoreException("Could not length of dataIdentifier " + + identifier, e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + @Override + public void deleteRecord(DataIdentifier identifier) + throws DataStoreException { + long start = System.currentTimeMillis(); + String key = getKeyName(identifier); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + s3service.deleteObject(bucket, key); + LOG.debug("Identifier [{}] deleted. It took [{}]ms.", new Object[] { + identifier, (System.currentTimeMillis() - start) }); + } catch (AmazonServiceException e) { + throw new DataStoreException( + "Could not getLastModified of dataIdentifier " + identifier, e); + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + @Override + public Set deleteAllOlderThan(long min) + throws DataStoreException { + long start = System.currentTimeMillis(); + // S3 stores lastModified to lower boundary of timestamp in ms. + // and hence min is reduced by 1000ms. + min = min - 1000; + Set deleteIdSet = new HashSet(30); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + ObjectListing prevObjectListing = s3service.listObjects(bucket); + while (true) { + List deleteList = new ArrayList(); + for (S3ObjectSummary s3ObjSumm : prevObjectListing.getObjectSummaries()) { + DataIdentifier identifier = new DataIdentifier( + getIdentifierName(s3ObjSumm.getKey())); + long lastModified = s3ObjSumm.getLastModified().getTime(); + LOG.debug("Identifier [{}]'s lastModified = [{}]", identifier, lastModified); + if (lastModified < min + && getDataStore().confirmDelete(identifier) + // confirm once more that record's lastModified < min + // order is important here + && s3service.getObjectMetadata(bucket, + s3ObjSumm.getKey()).getLastModified().getTime() < min) { + + getDataStore().deleteFromCache(identifier); + LOG.debug("add id [{}] to delete lists", + s3ObjSumm.getKey()); + deleteList.add(new DeleteObjectsRequest.KeyVersion( + s3ObjSumm.getKey())); + deleteIdSet.add(identifier); + } + } + if (deleteList.size() > 0) { + DeleteObjectsRequest delObjsReq = new DeleteObjectsRequest( + bucket); + delObjsReq.setKeys(deleteList); + DeleteObjectsResult dobjs = s3service.deleteObjects(delObjsReq); + if (dobjs.getDeletedObjects().size() != deleteList.size()) { + throw new DataStoreException( + "Incomplete delete object request. only " + + dobjs.getDeletedObjects().size() + " out of " + + deleteList.size() + " are deleted"); + } else { + LOG.debug("[{}] records deleted from datastore", + deleteList); + } + } + if (!prevObjectListing.isTruncated()) { + break; + } + prevObjectListing = s3service.listNextBatchOfObjects(prevObjectListing); + } + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + LOG.info( + "deleteAllOlderThan: min=[{}] exit. Deleted[{}] records. Number of records deleted [{}] took [{}]ms", + new Object[] { min, deleteIdSet, deleteIdSet.size(), + (System.currentTimeMillis() - start) }); + return deleteIdSet; + } + + @Override + public void close() throws DataStoreException { + super.close(); + // backend is closing. abort all mulitpart uploads from start. + if(s3service.doesBucketExist(bucket)) { + tmx.abortMultipartUploads(bucket, startTime); + } + tmx.shutdownNow(); + s3service.shutdown(); + LOG.info("S3Backend closed."); + } + + public String getBucket() { + return bucket; + } + + public void setBucket(String bucket) { + this.bucket = bucket; + } + + /** + * Properties used to configure the backend. If provided explicitly + * before init is invoked then these take precedence + * + * @param properties to configure S3Backend + */ + public void setProperties(Properties properties) { + this.properties = properties; + } + + private void write(DataIdentifier identifier, File file, + boolean asyncUpload, AsyncUploadCallback callback) + throws DataStoreException { + String key = getKeyName(identifier); + ObjectMetadata objectMetaData = null; + long start = System.currentTimeMillis(); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + // check if the same record already exists + try { + objectMetaData = s3service.getObjectMetadata(bucket, key); + } catch (AmazonServiceException ase) { + if (!(ase.getStatusCode() == 404 || ase.getStatusCode() == 403)) { + throw ase; + } + } + if (objectMetaData != null) { + long l = objectMetaData.getContentLength(); + if (l != file.length()) { + throw new DataStoreException("Collision: " + key + + " new length: " + file.length() + " old length: " + l); + } + LOG.debug("[{}]'s exists, lastmodified = [{}]", key, + objectMetaData.getLastModified().getTime()); + CopyObjectRequest copReq = new CopyObjectRequest(bucket, key, + bucket, key); + copReq.setNewObjectMetadata(objectMetaData); + Copy copy = tmx.copy(s3ReqDecorator.decorate(copReq)); + try { + copy.waitForCopyResult(); + LOG.debug("lastModified of [{}] updated successfully.", identifier); + if (callback != null) { + callback.onSuccess(new AsyncUploadResult(identifier, file)); + } + }catch (Exception e2) { + AsyncUploadResult asyncUpRes= new AsyncUploadResult(identifier, file); + asyncUpRes.setException(e2); + if (callback != null) { + callback.onAbort(asyncUpRes); + } + throw new DataStoreException("Could not upload " + key, e2); + } + } + + if (objectMetaData == null) { + try { + // start multipart parallel upload using amazon sdk + Upload up = tmx.upload(s3ReqDecorator.decorate(new PutObjectRequest( + bucket, key, file))); + // wait for upload to finish + if (asyncUpload) { + up.addProgressListener(new S3UploadProgressListener(up, + identifier, file, callback)); + LOG.debug( + "added upload progress listener to identifier [{}]", + identifier); + } else { + up.waitForUploadResult(); + LOG.debug("synchronous upload to identifier [{}] completed.", identifier); + if (callback != null) { + callback.onSuccess(new AsyncUploadResult( + identifier, file)); + } + } + } catch (Exception e2 ) { + AsyncUploadResult asyncUpRes= new AsyncUploadResult(identifier, file); + asyncUpRes.setException(e2); + if (callback != null) { + callback.onAbort(asyncUpRes); + } + throw new DataStoreException("Could not upload " + key, e2); + } + } + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + LOG.debug( + "write of [{}], length=[{}], in async mode [{}], in [{}]ms", + new Object[] { identifier, file.length(), asyncUpload, + (System.currentTimeMillis() - start) }); + } + + /** + * This method rename object keys in S3 concurrently. The number of + * concurrent threads is defined by 'maxConnections' property in + * aws.properties. As S3 doesn't have "move" command, this method simulate + * move as copy object object to new key and then delete older key. + */ + private void renameKeys() throws DataStoreException { + long startTime = System.currentTimeMillis(); + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + long count = 0; + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + ObjectListing prevObjectListing = s3service.listObjects(bucket); + List deleteList = new ArrayList(); + int nThreads = Integer.parseInt(properties.getProperty("maxConnections")); + ExecutorService executor = Executors.newFixedThreadPool(nThreads, + new NamedThreadFactory("s3-object-rename-worker")); + boolean taskAdded = false; + while (true) { + for (S3ObjectSummary s3ObjSumm : prevObjectListing.getObjectSummaries()) { + executor.execute(new KeyRenameThread(s3ObjSumm.getKey())); + taskAdded = true; + count++; + // delete the object if it follows old key name format + if( s3ObjSumm.getKey().startsWith(KEY_PREFIX)) { + deleteList.add(new DeleteObjectsRequest.KeyVersion( + s3ObjSumm.getKey())); + } + } + if (!prevObjectListing.isTruncated()) break; + prevObjectListing = s3service.listNextBatchOfObjects(prevObjectListing); + } + // This will make the executor accept no new threads + // and finish all existing threads in the queue + executor.shutdown(); + + try { + // Wait until all threads are finish + while (taskAdded + && !executor.awaitTermination(10, TimeUnit.SECONDS)) { + LOG.info("Rename S3 keys tasks timedout. Waiting again"); + } + } catch (InterruptedException ie) { + + } + LOG.info("Renamed [{}] keys, time taken [{}]sec", count, + ((System.currentTimeMillis() - startTime) / 1000)); + // Delete older keys. + if (deleteList.size() > 0) { + DeleteObjectsRequest delObjsReq = new DeleteObjectsRequest( + bucket); + int batchSize = 500, startIndex = 0, size = deleteList.size(); + int endIndex = batchSize < size ? batchSize : size; + while (endIndex <= size) { + delObjsReq.setKeys(Collections.unmodifiableList(deleteList.subList( + startIndex, endIndex))); + DeleteObjectsResult dobjs = s3service.deleteObjects(delObjsReq); + LOG.info( + "Records[{}] deleted in datastore from index [{}] to [{}]", + new Object[] { dobjs.getDeletedObjects().size(), + startIndex, (endIndex - 1) }); + if (endIndex == size) { + break; + } else { + startIndex = endIndex; + endIndex = (startIndex + batchSize) < size + ? (startIndex + batchSize) + : size; + } + } + } + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader(contextClassLoader); + } + } + } + + /** + * The method convert old key format to new format. For e.g. this method + * converts old key dataStore_004cb70c8f87d78f04da41e7547cb434094089ea to + * 004c-b70c8f87d78f04da41e7547cb434094089ea. + */ + private static String convertKey(String oldKey) + throws IllegalArgumentException { + if (!oldKey.startsWith(KEY_PREFIX)) { + return oldKey; + } + String key = oldKey.substring(KEY_PREFIX.length()); + return key.substring(0, 4) + Utils.DASH + key.substring(4); + } + + /** + * Get key from data identifier. Object is stored with key in S3. + */ + private static String getKeyName(DataIdentifier identifier) { + String key = identifier.toString(); + return key.substring(0, 4) + Utils.DASH + key.substring(4); + } + + /** + * Get data identifier from key. + */ + private static String getIdentifierName(String key) { + if (!key.contains(Utils.DASH)) { + return null; + } + return key.substring(0, 4) + key.substring(5); + } + + + /** + * The class renames object key in S3 in a thread. + */ + private class KeyRenameThread implements Runnable { + + private String oldKey; + + public void run() { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + String newS3Key = convertKey(oldKey); + CopyObjectRequest copReq = new CopyObjectRequest(bucket, + oldKey, bucket, newS3Key); + Copy copy = tmx.copy(s3ReqDecorator.decorate(copReq)); + try { + copy.waitForCopyResult(); + LOG.debug("[{}] renamed to [{}] ", oldKey, newS3Key); + } catch (InterruptedException ie) { + LOG.error(" Exception in renaming [{}] to [{}] ", + new Object[] { ie, oldKey, newS3Key }); + } + } finally { + if (contextClassLoader != null) { + Thread.currentThread().setContextClassLoader( + contextClassLoader); + } + } + } + + public KeyRenameThread(String oldKey) { + this.oldKey = oldKey; + } + } + + /** + * Listener which receives callback on status of S3 upload. + */ + private class S3UploadProgressListener implements ProgressListener { + + private File file; + + private DataIdentifier identifier; + + private AsyncUploadCallback callback; + + private Upload upload; + + public S3UploadProgressListener(Upload upload, DataIdentifier identifier, File file, + AsyncUploadCallback callback) { + super(); + this.identifier = identifier; + this.file = file; + this.callback = callback; + this.upload = upload; + } + + public void progressChanged(ProgressEvent progressEvent) { + switch (progressEvent.getEventCode()) { + case ProgressEvent.COMPLETED_EVENT_CODE: + callback.onSuccess(new AsyncUploadResult(identifier, file)); + break; + case ProgressEvent.FAILED_EVENT_CODE: + AsyncUploadResult result = new AsyncUploadResult( + identifier, file); + try { + AmazonClientException e = upload.waitForException(); + if (e != null) { + result.setException(e); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + callback.onFailure(result); + break; + default: + break; + } + } + } + + /** + * This class implements {@link Runnable} interface to upload {@link File} + * to S3 asynchronously. + */ + private class AsyncUploadJob implements Runnable { + + private DataIdentifier identifier; + + private File file; + + private AsyncUploadCallback callback; + + public AsyncUploadJob(DataIdentifier identifier, File file, + AsyncUploadCallback callback) { + super(); + this.identifier = identifier; + this.file = file; + this.callback = callback; + } + + public void run() { + try { + write(identifier, file, true, callback); + } catch (DataStoreException e) { + LOG.error("Could not upload [" + identifier + "], file[" + file + + "]", e); + } + + } + } +} diff --git a/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/S3DataStore.java b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/S3DataStore.java new file mode 100644 index 00000000000..8253572b244 --- /dev/null +++ b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/S3DataStore.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.aws.ext.ds; + +import java.util.Properties; + +import org.apache.jackrabbit.core.data.Backend; +import org.apache.jackrabbit.core.data.CachingDataStore; + +/** + * An Amazon S3 data store. + */ +public class S3DataStore extends CachingDataStore { + private Properties properties; + + @Override + protected Backend createBackend() { + S3Backend backend = new S3Backend(); + if(properties != null){ + backend.setProperties(properties); + } + return backend; + } + + @Override + protected String getMarkerFile() { + return "s3.init.done"; + } + + /** + * Properties required to configure the S3Backend + */ + public void setProperties(Properties properties) { + this.properties = properties; + } +} diff --git a/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/package-info.java b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/package-info.java new file mode 100755 index 00000000000..d82936c941a --- /dev/null +++ b/jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.aws.ext.ds; diff --git a/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/TestAll.java b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/TestAll.java new file mode 100644 index 00000000000..fb289ad99c5 --- /dev/null +++ b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/TestAll.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.aws.ext; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.apache.jackrabbit.aws.ext.ds.TestS3Ds; +import org.apache.jackrabbit.aws.ext.ds.TestS3DSAsyncTouch; +import org.apache.jackrabbit.aws.ext.ds.TestS3DsCacheOff; +import org.apache.jackrabbit.aws.ext.ds.TestS3DSWithSSES3; +import org.apache.jackrabbit.aws.ext.ds.TestS3DSWithSmallCache; +import org.apache.jackrabbit.core.data.TestCaseBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test suite that includes all test cases for the this module. + */ +public class TestAll extends TestCase { + + private static final Logger LOG = LoggerFactory.getLogger(TestAll.class); + + /** + * TestAll suite that executes all tests inside this module. To + * run test cases against Amazon S3 pass AWS configuration properties file as + * system property -Dconfig=/opt/cq/aws.properties. Sample aws properties + * located at src/test/resources/aws.properties. + */ + public static Test suite() { + TestSuite suite = new TestSuite("S3 tests"); + String config = System.getProperty(TestCaseBase.CONFIG); + LOG.info("config= " + config); + if (config != null && !"".equals(config.trim())) { + suite.addTestSuite(TestS3Ds.class); + suite.addTestSuite(TestS3DSAsyncTouch.class); + suite.addTestSuite(TestS3DSWithSmallCache.class); + suite.addTestSuite(TestS3DsCacheOff.class); + suite.addTestSuite(TestS3DSWithSSES3.class); + } + return suite; + } +} diff --git a/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/S3TestDataStore.java b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/S3TestDataStore.java new file mode 100644 index 00000000000..33dbba4d0ec --- /dev/null +++ b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/S3TestDataStore.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.aws.ext.ds; + +import java.util.Properties; + +import org.apache.jackrabbit.core.data.Backend; + +/** + * This class intialize {@link S3DataStore} with the give bucket. The other + * configuration are taken from configuration file. This class is implemented so + * that each test case run in its own bucket. It was required as deletions in + * bucket are not immediately reflected in the next test case. + */ +public class S3TestDataStore extends S3DataStore { + + Properties props; + + public S3TestDataStore() { + super(); + } + + public S3TestDataStore(Properties props) { + super(); + this.props = props; + } + + protected Backend createBackend() { + Backend backend = new S3Backend(); + ((S3Backend) backend).setProperties(props); + return backend; + } +} diff --git a/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSAsyncTouch.java b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSAsyncTouch.java new file mode 100644 index 00000000000..910d4229b4f --- /dev/null +++ b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSAsyncTouch.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.aws.ext.ds; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test {@link CachingDataStore} with + * {@link CachingDataStore#setTouchAsync(boolean) set to true. It requires to + * pass aws config file via system property. For e.g. + * -Dconfig=/opt/cq/aws.properties. Sample aws properties located at + * src/test/resources/aws.properties + */ +public class TestS3DSAsyncTouch extends TestS3Ds { + + protected static final Logger LOG = LoggerFactory.getLogger(TestS3DSAsyncTouch.class); + + public TestS3DSAsyncTouch() throws IOException { + + } + + @Override + protected CachingDataStore createDataStore() throws RepositoryException { + S3DataStore s3ds = new S3DataStore(); + s3ds.setProperties(props); + s3ds.setTouchAsync(true); + s3ds.setSecret("123456"); + s3ds.init(dataStoreDir); + s3ds.updateModifiedDateOnAccess(System.currentTimeMillis() + 50 * 1000); + sleep(1000); + return s3ds; + } +} diff --git a/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSWithSSES3.java b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSWithSSES3.java new file mode 100644 index 00000000000..45a29482494 --- /dev/null +++ b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSWithSSES3.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.aws.ext.ds; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.aws.ext.S3Constants; +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.apache.jackrabbit.core.data.DataRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test S3DataStore operation with SSE_S3 encryption. + */ +public class TestS3DSWithSSES3 extends TestS3Ds { + + protected static final Logger LOG = LoggerFactory.getLogger(TestS3DSWithSSES3.class); + + public TestS3DSWithSSES3() throws IOException { + } + + @Override + protected CachingDataStore createDataStore() throws RepositoryException { + props.setProperty(S3Constants.S3_ENCRYPTION, + S3Constants.S3_ENCRYPTION_SSE_S3); + S3DataStore s3ds = new S3DataStore(); + s3ds.setProperties(props); + s3ds.setSecret("123456"); + s3ds.init(dataStoreDir); + sleep(1000); + return s3ds; + } + + /** + * Test data migration enabling SSE_S3 encryption. + */ + public void testDataMigration() { + try { + String bucket = props.getProperty(S3Constants.S3_BUCKET); + S3DataStore s3ds = new S3DataStore(); + s3ds.setProperties(props); + s3ds.setCacheSize(0); + s3ds.init(dataStoreDir); + byte[] data = new byte[dataLength]; + randomGen.nextBytes(data); + DataRecord rec = s3ds.addRecord(new ByteArrayInputStream(data)); + assertEquals(data.length, rec.getLength()); + assertRecord(data, rec); + s3ds.close(); + + // turn encryption now. + props.setProperty(S3Constants.S3_BUCKET, bucket); + props.setProperty(S3Constants.S3_ENCRYPTION, + S3Constants.S3_ENCRYPTION_SSE_S3); + props.setProperty(S3Constants.S3_RENAME_KEYS, "true"); + s3ds = new S3DataStore(); + s3ds.setProperties(props); + s3ds.setCacheSize(0); + s3ds.init(dataStoreDir); + + rec = s3ds.getRecord(rec.getIdentifier()); + assertEquals(data.length, rec.getLength()); + assertRecord(data, rec); + + randomGen.nextBytes(data); + rec = s3ds.addRecord(new ByteArrayInputStream(data)); + s3ds.close(); + + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } + +} diff --git a/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSWithSmallCache.java b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSWithSmallCache.java new file mode 100644 index 00000000000..b63bf67f025 --- /dev/null +++ b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DSWithSmallCache.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.aws.ext.ds; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.apache.jackrabbit.core.data.LocalCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test {@link CachingDataStore} with S3Backend and with very small size (@link + * {@link LocalCache}. It requires to pass aws config file via system property. + * For e.g. -Dconfig=/opt/cq/aws.properties. Sample aws properties located at + * src/test/resources/aws.properties + */ +public class TestS3DSWithSmallCache extends TestS3Ds { + + protected static final Logger LOG = LoggerFactory.getLogger(TestS3DSWithSmallCache.class); + + public TestS3DSWithSmallCache() throws IOException { + } + + @Override + protected CachingDataStore createDataStore() throws RepositoryException { + S3DataStore s3ds = new S3DataStore(); + s3ds.setProperties(props); + s3ds.setCacheSize(dataLength * 10); + s3ds.setCachePurgeTrigFactor(0.5d); + s3ds.setCachePurgeResizeFactor(0.4d); + s3ds.setSecret("123456"); + s3ds.init(dataStoreDir); + sleep(1000); + return s3ds; + } +} diff --git a/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3Ds.java b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3Ds.java new file mode 100644 index 00000000000..36c256cddda --- /dev/null +++ b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3Ds.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.aws.ext.ds; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.aws.ext.S3Constants; +import org.apache.jackrabbit.aws.ext.Utils; +import org.apache.jackrabbit.core.data.Backend; +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.apache.jackrabbit.core.data.TestCaseBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.DeleteObjectsRequest; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.S3ObjectSummary; +import com.amazonaws.services.s3.transfer.TransferManager; + +/** + * Test {@link CachingDataStore} with S3Backend and local cache on. It requires + * to pass aws config file via system property. For e.g. + * -Dconfig=/opt/cq/aws.properties. Sample aws properties located at + * src/test/resources/aws.properties + */ +public class TestS3Ds extends TestCaseBase { + + protected static final Logger LOG = LoggerFactory.getLogger(TestS3Ds.class); + + private Date startTime = null; + + protected Properties props; + + protected String config; + + public TestS3Ds() throws IOException { + System.setProperty( + TestCaseBase.CONFIG, + "C:/src/apache/jackrabbit-encryp-changes/jackrabbit/jackrabbit-aws-ext/src/test/resources/aws.properties"); + config = System.getProperty(CONFIG); + props = Utils.readConfig(System.getProperty(CONFIG)); + } + + @Override + protected void setUp() throws Exception { + startTime = new Date(); + super.setUp(); + String bucket = String.valueOf(randomGen.nextInt(9999)) + "-" + + String.valueOf(randomGen.nextInt(9999)) + "-test"; + props.setProperty(S3Constants.S3_BUCKET, bucket); + // delete bucket if exists + deleteBucket(bucket); + } + + @Override + protected void tearDown() { + try { + deleteBucket(); + super.tearDown(); + } catch (Exception ignore) { + + } + } + + @Override + protected CachingDataStore createDataStore() throws RepositoryException { + S3DataStore s3ds = new S3DataStore(); + s3ds.setProperties(props); + s3ds.setSecret("123456"); + s3ds.init(dataStoreDir); + sleep(1000); + return s3ds; + } + + /** + * Cleaning of bucket after test run. + */ + /** + * Cleaning of bucket after test run. + */ + public void deleteBucket() throws Exception { + Backend backend = ((S3DataStore) ds).getBackend(); + String bucket = ((S3Backend) backend).getBucket(); + deleteBucket(bucket); + } + + public void deleteBucket(String bucket) throws Exception { + LOG.info("deleting bucket [" + bucket + "]"); + Properties props = Utils.readConfig(config); + AmazonS3Client s3service = Utils.openService(props); + TransferManager tmx = new TransferManager(s3service); + + if (s3service.doesBucketExist(bucket)) { + for (int i = 0; i < 4; i++) { + tmx.abortMultipartUploads(bucket, startTime); + ObjectListing prevObjectListing = s3service.listObjects(bucket); + while (prevObjectListing != null) { + List deleteList = new ArrayList(); + for (S3ObjectSummary s3ObjSumm : prevObjectListing.getObjectSummaries()) { + deleteList.add(new DeleteObjectsRequest.KeyVersion( + s3ObjSumm.getKey())); + } + if (deleteList.size() > 0) { + DeleteObjectsRequest delObjsReq = new DeleteObjectsRequest( + bucket); + delObjsReq.setKeys(deleteList); + s3service.deleteObjects(delObjsReq); + } + if (!prevObjectListing.isTruncated()) break; + prevObjectListing = s3service.listNextBatchOfObjects(prevObjectListing); + } + } + s3service.deleteBucket(bucket); + LOG.info("bucket [ " + bucket + "] deleted"); + + } else { + LOG.info("bucket [" + bucket + "] doesn't exists"); + } + tmx.shutdownNow(); + s3service.shutdown(); + } + +} diff --git a/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DsCacheOff.java b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DsCacheOff.java new file mode 100644 index 00000000000..14896b9da7b --- /dev/null +++ b/jackrabbit-aws-ext/src/test/java/org/apache/jackrabbit/aws/ext/ds/TestS3DsCacheOff.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.aws.ext.ds; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test {@link CachingDataStore} with S3Backend and local cache Off. It requires + * to pass aws config file via system property. For e.g. + * -Dconfig=/opt/cq/aws.properties. Sample aws properties located at + * src/test/resources/aws.properties + */ +public class TestS3DsCacheOff extends TestS3Ds { + + protected static final Logger LOG = LoggerFactory.getLogger(TestS3DsCacheOff.class); + + public TestS3DsCacheOff() throws IOException { + } + + @Override + protected CachingDataStore createDataStore() throws RepositoryException { + S3DataStore s3ds = new S3DataStore(); + s3ds.setProperties(props); + s3ds.setCacheSize(0); + s3ds.setSecret("123456"); + s3ds.init(dataStoreDir); + sleep(1000); + return s3ds; + } +} diff --git a/jackrabbit-aws-ext/src/test/resources/aws.properties b/jackrabbit-aws-ext/src/test/resources/aws.properties new file mode 100644 index 00000000000..04c752ae027 --- /dev/null +++ b/jackrabbit-aws-ext/src/test/resources/aws.properties @@ -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. +# + +# AWS account ID +accessKey= +# AWS secret key +secretKey= +# AWS bucket name +s3Bucket= +# AWS bucket region +# Mapping of S3 regions to their constants +# US Standard us-standard +# US West us-west-2 +# US West (Northern California) us-west-1 +# EU (Ireland) EU +# Asia Pacific (Singapore) ap-southeast-1 +# Asia Pacific (Sydney) ap-southeast-2 +# Asia Pacific (Tokyo) ap-northeast-1 +# South America (Sao Paulo) sa-east-1 +s3Region= +# S3 endpoint to be used. This parameter is optional +# and has a higher precedence over endpoint derived +# via S3 region. +s3EndPoint= +connectionTimeout=120000 +socketTimeout=120000 +maxConnections=20 +maxErrorRetry=10 +# maximum concurrent threads to write to S3. +writeThreads=10 +# proxy configurations (optional) +proxyHost= +proxyPort= \ No newline at end of file diff --git a/jackrabbit-aws-ext/src/test/resources/log4j.properties b/jackrabbit-aws-ext/src/test/resources/log4j.properties new file mode 100644 index 00000000000..ac7c3fc2057 --- /dev/null +++ b/jackrabbit-aws-ext/src/test/resources/log4j.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. +# + +# this is the log4j configuration for the JCR API tests +log4j.rootLogger=INFO, file +log4j.logger.org.apache.jackrabbit.core.data.CachingDataStore=ERROR +log4j.logger.org.apache.jackrabbit.aws.ext.ds=INFO + +#log4j.logger.org.apache.jackrabbit.test=DEBUG + +# 'file' is set to be a FileAppender. +log4j.appender.file=org.apache.log4j.FileAppender +log4j.appender.file.File=target/debug.log + +# 'file' uses PatternLayout. +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n diff --git a/jackrabbit-aws-ext/src/test/resources/repository_sample.xml b/jackrabbit-aws-ext/src/test/resources/repository_sample.xml new file mode 100644 index 00000000000..40a00ea064a --- /dev/null +++ b/jackrabbit-aws-ext/src/test/resources/repository_sample.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-bundle/pom.xml b/jackrabbit-bundle/pom.xml new file mode 100644 index 00000000000..5c5b4168c31 --- /dev/null +++ b/jackrabbit-bundle/pom.xml @@ -0,0 +1,135 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-bundle + bundle + Jackrabbit OSGi bundle + OSGi bundle of Apache Jackrabbit + + + + + org.apache.felix + maven-bundle-plugin + true + + + *;groupId=!org.osgi|javax.jcr + true + + org.apache.jackrabbit.api.*;version=${project.version} + + + + org.apache.jackrabbit.test;resolution:=optional, + + javax.servlet;resolution:=optional, + javax.servlet.http;resolution:=optional, + + org.apache.xpath;resolution:=optional, + org.apache.xpath.objects;resolution:=optional, + org.apache.xml.utils;resolution:=optional, + org.apache.xalan.serialize;resolution:=optional, + org.apache.xalan.templates;resolution:=optional, + org.apache.derby.impl.drda;resolution:=optional, + * + + + org.apache.jackrabbit.bundle.Activator + + + + + + + + + + org.osgi + org.osgi.core + 4.2.0 + provided + + + org.osgi + org.osgi.compendium + 4.2.0 + provided + + + javax.jcr + jcr + provided + + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + provided + + + javax.jcr + jcr + + + org.slf4j + slf4j-api + + + org.slf4j + jcl-over-slf4j + + + + + org.apache.jackrabbit + jackrabbit-jcr2dav + ${project.version} + provided + + + javax.jcr + jcr + + + org.slf4j + slf4j-api + + + org.slf4j + jcl-over-slf4j + + + + + + diff --git a/jackrabbit-bundle/src/main/java/org/apache/jackrabbit/bundle/Activator.java b/jackrabbit-bundle/src/main/java/org/apache/jackrabbit/bundle/Activator.java new file mode 100644 index 00000000000..f9f358aba3b --- /dev/null +++ b/jackrabbit-bundle/src/main/java/org/apache/jackrabbit/bundle/Activator.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.bundle; + +import java.io.File; +import java.util.Hashtable; + +import javax.jcr.Repository; + +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +public class Activator implements BundleActivator { + + private volatile RepositoryImpl repository; + + private volatile ServiceRegistration registration; + + public void start(BundleContext context) throws Exception { + repository = RepositoryImpl.create( + RepositoryConfig.install(new File("jackrabbit"))); + + Hashtable properties = new Hashtable(); + for (String key : repository.getDescriptorKeys()) { + String descriptor = repository.getDescriptor(key); + if (descriptor != null) { + properties.put(key, descriptor); + } + } + registration = context.registerService( + Repository.class.getName(), repository, properties); + } + + public void stop(BundleContext context) throws Exception { + registration.unregister(); + + repository.shutdown(); + } + +} diff --git a/jackrabbit-bundle/src/main/resources/OSGI-INF/jackrabbit-repository-factory.xml b/jackrabbit-bundle/src/main/resources/OSGI-INF/jackrabbit-repository-factory.xml new file mode 100644 index 00000000000..8c9efb48a58 --- /dev/null +++ b/jackrabbit-bundle/src/main/resources/OSGI-INF/jackrabbit-repository-factory.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/jackrabbit-core/HEADER.txt b/jackrabbit-core/HEADER.txt new file mode 100644 index 00000000000..ae6f28c4a1c --- /dev/null +++ b/jackrabbit-core/HEADER.txt @@ -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. + */ diff --git a/jackrabbit-core/README.txt b/jackrabbit-core/README.txt new file mode 100644 index 00000000000..21c486f43e5 --- /dev/null +++ b/jackrabbit-core/README.txt @@ -0,0 +1,7 @@ +========================== +Welcome to Jackrabbit Core +========================== + +This is the Core component of the Apache Jackrabbit project. +This component contains the core of the fully JSR-170 compliant +Apache Jackrabbit content repository implementation. diff --git a/jackrabbit-core/checkstyle-suppressions.xml b/jackrabbit-core/checkstyle-suppressions.xml new file mode 100644 index 00000000000..c0f0ab1833f --- /dev/null +++ b/jackrabbit-core/checkstyle-suppressions.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/jackrabbit-core/checkstyle.xml b/jackrabbit-core/checkstyle.xml new file mode 100644 index 00000000000..df1aedb4a14 --- /dev/null +++ b/jackrabbit-core/checkstyle.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/jdepend.properties b/jackrabbit-core/jdepend.properties new file mode 100644 index 00000000000..b7e12a58b41 --- /dev/null +++ b/jackrabbit-core/jdepend.properties @@ -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. +ignore.java=java.*,javax.*,org.w3c.*,org.xml.* +ignore.sun=sun.*,com.sun.* +ignore.jackrabbit=org.apache.jackrabbit.util +ignore.slf4j=org.slf4j +ignore.commons=org.apache.commons +ignore.concurrent=EDU.* diff --git a/jackrabbit-core/pom.xml b/jackrabbit-core/pom.xml new file mode 100644 index 00000000000..965e5c0c580 --- /dev/null +++ b/jackrabbit-core/pom.xml @@ -0,0 +1,582 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-core + Jackrabbit Core + Jackrabbit content repository implementation + + + false + -Xmx512m + + + + + + maven-antrun-plugin + + + process-test-resources + process-test-resources + + + + + + + + + + + + run + + + + + + ant + ant-optional + 1.5.3-1 + + + + + maven-surefire-plugin + + + **/*TestAll.java + + ${test.opts} + + + java.awt.headless + true + + + derby.system.durability + test + + + derby.storage.fileSyncTransactionLog + true + + + derby.stream.error.file + target/derby.log + + + org.apache.jackrabbit.repository.home + target/repository-factory-test + + + known.issues + +org.apache.jackrabbit.core.ConcurrentImportTest +org.apache.jackrabbit.core.xml.DocumentViewTest#testMultiValue +org.apache.jackrabbit.core.NodeImplTest#testReferentialIntegrityCorruptionGetPath +org.apache.jackrabbit.core.integration.ConcurrentQueryTest#testConcurrentQueryWithDeletes +org.apache.jackrabbit.test.api.ShareableNodeTest#testGetName +org.apache.jackrabbit.test.api.ShareableNodeTest#testGetNode +org.apache.jackrabbit.test.api.ShareableNodeTest#testGetNodes +org.apache.jackrabbit.test.api.ShareableNodeTest#testGetNodesByPattern +org.apache.jackrabbit.test.api.ShareableNodeTest#testMoveShareableNode +org.apache.jackrabbit.test.api.ShareableNodeTest#testTransientMoveShareableNode +org.apache.jackrabbit.test.api.lock.OpenScopedLockTest#testLockExpiration +org.apache.jackrabbit.test.api.lock.SessionScopedLockTest#testLockExpiration +org.apache.jackrabbit.test.api.observation.NodeReorderTest#testNodeReorderMove +org.apache.jackrabbit.core.data.ConcurrentGcTest#testDatabases +org.apache.jackrabbit.core.data.GarbageCollectorTest#testCloseSessionWhileRunningGc +org.apache.jackrabbit.core.data.GarbageCollectorTest#testSimultaneousRunGC +org.apache.jackrabbit.core.ReplacePropertyWhileOthersReadTest +org.apache.jackrabbit.core.security.user.MembershipCachePerfTest +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testStringLiteralInvalidName +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testPathLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testURILiteral + + + + org.apache.jackrabbit.test.integration + ${org.apache.jackrabbit.test.integration} + + + + + + do_test + integration-test + + + **/integration/*Test.java + + + + test + + + + + + org.apache.rat + apache-rat-plugin + + + src/main/javadoc/**/*.uxf + src/test/repository/** + src/test/resources/**/*.txt + src/test/resources/**/*.rtf + src/test/resources/**/*.cnd + src/test/compatibility/**/target/** + src/test/compatibility/**/.*/** + src/test/compatibility/repositories.zip + repository/** + *.log + + + + + org.apache.maven.plugins + maven-jar-plugin + + + logback-test.xml + + + + + + test-jar + + + + + + + + src/main/resources + + + src/main/resources-filtered + true + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.maven.plugins + maven-antrun-plugin + [1.6,) + + run + + + + + + + + + + + + + + + + + concurrent + concurrent + + + commons-collections + commons-collections + + + commons-io + commons-io + + + commons-dbcp + commons-dbcp + 1.3 + + + javax.jcr + jcr + + + org.apache.jackrabbit + jackrabbit-api + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-data + ${project.version} + + + org.apache.jackrabbit + jackrabbit-data + ${project.version} + test-jar + test + + + org.apache.jackrabbit + jackrabbit-spi-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-spi + ${project.version} + + + + org.apache.jackrabbit + jackrabbit-spi + ${project.version} + tests + test + + + org.apache.tika + tika-core + + + org.slf4j + slf4j-api + + + org.apache.lucene + lucene-core + + + org.apache.derby + derby + + + org.apache.jackrabbit + jackrabbit-jcr-tests + ${project.version} + true + + + junit + junit + test + + + org.apache.jackrabbit + jackrabbit-jcr-benchmark + test + + + org.apache.tika + tika-parsers + test + + + commons-logging + commons-logging + + + + + org.slf4j + jcl-over-slf4j + + + ch.qos.logback + logback-classic + test + + + javax.transaction + javax.transaction-api + test + + + com.h2database + h2 + 1.4.195 + test + + + org.mockito + mockito-core + test + + + + + + integrationTesting + + true + + + + + mysql + + jackrabbit + org.apache.jackrabbit.core.fs.db.DbFileSystem + org.apache.jackrabbit.core.data.db.DbDataStore + org.apache.jackrabbit.core.persistence.pool.MySqlPersistenceManager + org.apache.jackrabbit.core.journal.DatabaseJournal + mysql + select 1 + user + pwd + com.mysql.jdbc.Driver + jdbc:mysql://localhost:3306/${config.db.name}?autoReconnect=true + jdbc:mysql://localhost:3306/mysql?autoReconnect=true + drop database ${config.db.name} + create database ${config.db.name} + + + + mssql + + jackrabbit + org.apache.jackrabbit.core.fs.db.MSSqlFileSystem + org.apache.jackrabbit.core.data.db.DbDataStore + org.apache.jackrabbit.core.persistence.pool.MSSqlPersistenceManager + org.apache.jackrabbit.core.journal.MSSqlDatabaseJournal + mssql + select 1 + user + pwd + net.sourceforge.jtds.jdbc.Driver + jdbc:jtds:sqlserver://localhost:2433/${config.db.name} + jdbc:jtds:sqlserver://localhost:2433/master + drop database ${config.db.name} + create database ${config.db.name} + + + + oracle + + unused + org.apache.jackrabbit.core.fs.db.OracleFileSystem + org.apache.jackrabbit.core.data.db.DbDataStore + org.apache.jackrabbit.core.persistence.pool.OraclePersistenceManager + org.apache.jackrabbit.core.journal.OracleDatabaseJournal + oracle + select 'validationQuery' from dual + user + password + oracle.jdbc.driver.OracleDriver + jdbc:oracle:thin:@localhost:1521:xe + unused + unused + unused + + + + h2 + + jackrabbit + org.apache.jackrabbit.core.fs.db.DbFileSystem + org.apache.jackrabbit.core.data.db.DbDataStore + org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager + org.apache.jackrabbit.core.journal.DatabaseJournal + h2 + call 1 + sa + sa + org.h2.Driver + + jdbc:h2:~/jackrabbit2;MAX_LENGTH_INPLACE_LOB=10240;DB_CLOSE_ON_EXIT=FALSE + unused + drop all objects delete files + unused + + + + use-descriptor-overlay + + + + + org.codehaus.mojo + sql-maven-plugin + + + mysql + mysql-connector-java + 5.1.6 + jar + provided + + + net.sourceforge.jtds + jtds + 1.2.2 + provided + + + + ${config.db.driver} + ${config.db.metaurl} + ${config.db.user} + ${config.db.pwd} + sensibleKey + + + + drop-db + clean + + execute + + + true + ${config.db.dropcommand} + continue + + + + create-db + clean + + execute + + + true + ${config.db.createcommand} + + + + + + maven-antrun-plugin + + + overlay-repository-descriptors + process-test-resources + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + run + + + + + + ant + ant-optional + 1.5.3-1 + + + + + + + + mysql + mysql-connector-java + 5.1.6 + jar + test + + + net.sourceforge.jtds + jtds + 1.2.2 + test + + + com.oracle + ojdbc14 + 10.2.0.3.0 + test + + + + + diff --git a/jackrabbit-core/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-core/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-core/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/AbstractNodeData.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/AbstractNodeData.java new file mode 100644 index 00000000000..50954e086ee --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/AbstractNodeData.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.core; + +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.NodeState; + +/** + * Data object representing a node. + */ +public abstract class AbstractNodeData extends ItemData { + + /** Primary parent id of a shareable node. */ + private NodeId primaryParentId; + + /** + * Create a new instance of this class. + * + * @param state node state + * @param itemMgr item manager + */ + protected AbstractNodeData(NodeState state, ItemManager itemMgr) { + super(state, itemMgr); + + if (state.isShareable()) { + this.primaryParentId = state.getParentId(); + } + } + + /** + * Create a new instance of this class. + * + * @param id item id + */ + protected AbstractNodeData(ItemId id) { + super(id); + } + + /** + * Return the associated node state. + * + * @return node state + */ + public NodeState getNodeState() { + return (NodeState) getState(); + } + + /** + * Return the associated node definition. + * + * @return node definition + * @throws RepositoryException if the definition cannot be retrieved. + */ + public NodeDefinition getNodeDefinition() throws RepositoryException { + return (NodeDefinition) getDefinition(); + } + + /** + * Sets the associated node definition. + * + * @param definition new node definition + */ + public void setNodeDefinition(NodeDefinition definition) { + setDefinition(definition); + } + + /** + * Return the parent id of this node. Every shareable node in a shared set + * has a different parent. + * + * @return parent id + */ + @Override + public NodeId getParentId() { + if (primaryParentId != null) { + return primaryParentId; + } + return getState().getParentId(); + } + + /** + * Return the primary parent id of this node. Every shareable node in a + * shared set has a different primary parent. Returns null + * for nodes that are not shareable. + * + * @return primary parent id or null + */ + public NodeId getPrimaryParentId() { + return primaryParentId; + } + + /** + * Set the primary parent id of this node. + * + * @param primaryParentId primary parent id + */ + protected void setPrimaryParentId(NodeId primaryParentId) { + this.primaryParentId = primaryParentId; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isNode() { + return true; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/AddMixinOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/AddMixinOperation.java new file mode 100644 index 00000000000..f3049f894ce --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/AddMixinOperation.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.core; + +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CHECKED_OUT; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CONSTRAINTS; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_SIMPLE_VERSIONABLE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_VERSIONABLE; + +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionWriteOperation; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; + +/** + * Session operation for adding a mixin type to a node. + */ +class AddMixinOperation implements SessionWriteOperation { + + private final NodeImpl node; + + private final Name mixinName; + + public AddMixinOperation(NodeImpl node, Name mixinName) { + this.node = node; + this.mixinName = mixinName; + } + + public Object perform(SessionContext context) throws RepositoryException { + int permissions = Permission.NODE_TYPE_MNGMT; + // special handling of mix:(simple)versionable. since adding the + // mixin alters the version storage jcr:versionManagement privilege + // is required in addition. + if (MIX_VERSIONABLE.equals(mixinName) + || MIX_SIMPLE_VERSIONABLE.equals(mixinName)) { + permissions |= Permission.VERSION_MNGMT; + } + context.getItemValidator().checkModify( + node, + CHECK_LOCK | CHECK_CHECKED_OUT | CHECK_CONSTRAINTS | CHECK_HOLD, + permissions); + + NodeTypeManagerImpl ntMgr = context.getNodeTypeManager(); + NodeTypeImpl mixin = ntMgr.getNodeType(mixinName); + if (!mixin.isMixin()) { + throw new RepositoryException( + context.getJCRName(mixinName) + " is not a mixin node type"); + } + + Name primaryTypeName = node.getNodeState().getNodeTypeName(); + NodeTypeImpl primaryType = ntMgr.getNodeType(primaryTypeName); + if (primaryType.isDerivedFrom(mixinName)) { + // new mixin is already included in primary type + return this; + } + + // build effective node type of mixin's & primary type in order + // to detect conflicts + NodeTypeRegistry ntReg = context.getNodeTypeRegistry(); + EffectiveNodeType entExisting; + try { + // existing mixin's + Set mixins = new HashSet( + node.getNodeState().getMixinTypeNames()); + + // build effective node type representing primary type including + // existing mixin's + entExisting = ntReg.getEffectiveNodeType(primaryTypeName, mixins); + if (entExisting.includesNodeType(mixinName)) { + // new mixin is already included in existing mixin type(s) + return this; + } + + // add new mixin + mixins.add(mixinName); + // try to build new effective node type (will throw in case + // of conflicts) + ntReg.getEffectiveNodeType(primaryTypeName, mixins); + } catch (NodeTypeConflictException e) { + throw new ConstraintViolationException(e.getMessage(), e); + } + + // do the actual modifications implied by the new mixin; + // try to revert the changes in case an exception occurs + try { + // modify the state of this node + NodeState thisState = + (NodeState) node.getOrCreateTransientItemState(); + // add mixin name + Set mixins = new HashSet(thisState.getMixinTypeNames()); + mixins.add(mixinName); + thisState.setMixinTypeNames(mixins); + + // set jcr:mixinTypes property + node.setMixinTypesProperty(mixins); + + // add 'auto-create' properties defined in mixin type + for (PropertyDefinition aPda : mixin.getAutoCreatedPropertyDefinitions()) { + PropertyDefinitionImpl pd = (PropertyDefinitionImpl) aPda; + // make sure that the property is not already defined + // by primary type or existing mixin's + NodeTypeImpl declaringNT = + (NodeTypeImpl) pd.getDeclaringNodeType(); + if (!entExisting.includesNodeType(declaringNT.getQName())) { + node.createChildProperty( + pd.unwrap().getName(), pd.getRequiredType(), pd); + } + } + + // recursively add 'auto-create' child nodes defined in mixin type + for (NodeDefinition aNda : mixin.getAutoCreatedNodeDefinitions()) { + NodeDefinitionImpl nd = (NodeDefinitionImpl) aNda; + // make sure that the child node is not already defined + // by primary type or existing mixin's + NodeTypeImpl declaringNT = + (NodeTypeImpl) nd.getDeclaringNodeType(); + if (!entExisting.includesNodeType(declaringNT.getQName())) { + node.createChildNode( + nd.unwrap().getName(), + (NodeTypeImpl) nd.getDefaultPrimaryType(), + null); + } + } + } catch (RepositoryException re) { + // try to undo the modifications by removing the mixin + try { + node.removeMixin(mixinName); + } catch (RepositoryException re1) { + // silently ignore & fall through + } + throw re; + } + + return this; + } + + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "node.addMixin(" + mixinName + ")"; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.java new file mode 100644 index 00000000000..6ef89a90867 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/BatchedItemOperations.java @@ -0,0 +1,1904 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.retention.RetentionRegistry; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.UpdatableItemStateManager; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.version.VersionHistoryInfo; +import org.apache.jackrabbit.core.version.InternalVersionManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * BatchedItemOperations is an internal helper class that + * provides both high- and low-level operations directly on the + * ItemState level. + */ +public class BatchedItemOperations extends ItemValidator { + + private static Logger log = LoggerFactory.getLogger(BatchedItemOperations.class); + + // flags used by the copy(...) methods + protected static final int COPY = 0; + protected static final int CLONE = 1; + protected static final int CLONE_REMOVE_EXISTING = 2; + + /** + * wrapped item state manager + */ + protected final UpdatableItemStateManager stateMgr; + /** + * current session used for checking access rights + */ + protected final SessionImpl session; + + private final HierarchyManager hierMgr; + + /** + * Creates a new BatchedItemOperations instance. + * + * @param stateMgr item state manager + * @param sessionContext the session context + * @throws RepositoryException + */ + public BatchedItemOperations( + UpdatableItemStateManager stateMgr, SessionContext sessionContext) + throws RepositoryException { + super(sessionContext); + this.stateMgr = stateMgr; + this.session = sessionContext.getSessionImpl(); + this.hierMgr = sessionContext.getHierarchyManager(); + } + + //-----------------------------------------< controlling batch operations > + /** + * Starts an edit operation on the wrapped state manager. + * At the end of this operation, either {@link #update} or {@link #cancel} + * must be invoked. + * + * @throws IllegalStateException if the state manager is already in edit mode + */ + public void edit() throws IllegalStateException { + stateMgr.edit(); + } + + /** + * Store an item state. + * + * @param state item state that should be stored + * @throws IllegalStateException if the manager is not in edit mode. + */ + public void store(ItemState state) throws IllegalStateException { + stateMgr.store(state); + } + + /** + * Destroy an item state. + * + * @param state item state that should be destroyed + * @throws IllegalStateException if the manager is not in edit mode. + */ + public void destroy(ItemState state) throws IllegalStateException { + stateMgr.destroy(state); + } + + /** + * End an update operation. This will save all changes made since + * the last invocation of {@link #edit()}. If this operation fails, + * no item will have been saved. + * + * @throws RepositoryException if the update operation failed + * @throws IllegalStateException if the state manager is not in edit mode + */ + public void update() throws RepositoryException, IllegalStateException { + try { + stateMgr.update(); + } catch (ItemStateException ise) { + String msg = "update operation failed"; + log.debug(msg, ise); + throw new RepositoryException(msg, ise); + } + } + + /** + * Cancel an update operation. This will undo all changes made since + * the last invocation of {@link #edit()}. + * + * @throws IllegalStateException if the state manager is not in edit mode + */ + public void cancel() throws IllegalStateException { + stateMgr.cancel(); + } + + //-------------------------------------------< high-level item operations > + + /** + * Clones the subtree at the node srcAbsPath in to the new + * location at destAbsPath. This operation is only supported: + *
      + *
    • If the source element has the mixin mix:shareable (or some + * derived node type)
    • + *
    • If the parent node of destAbsPath has not already a shareable + * node in the same shared set as the node at srcPath.
    • + *
    + * + * @param srcPath source path + * @param destPath destination path + * @return the node id of the destination's parent + * + * @throws ConstraintViolationException if the operation would violate a + * node-type or other implementation-specific constraint. + * @throws VersionException if the parent node of destAbsPath is + * versionable and checked-in, or is non-versionable but its nearest versionable ancestor is + * checked-in. This exception will also be thrown if removeExisting is true, + * and a UUID conflict occurs that would require the moving and/or altering of a node that is checked-in. + * @throws AccessDeniedException if the current session does not have + * sufficient access rights to complete the operation. + * @throws PathNotFoundException if the node at srcAbsPath in + * srcWorkspace or the parent of destAbsPath in this workspace does not exist. + * @throws ItemExistsException if a property already exists at + * destAbsPath or a node already exist there, and same name + * siblings are not allowed or if removeExisting is false and a + * UUID conflict occurs. + * @throws LockException if a lock prevents the clone. + * @throws RepositoryException if the last element of destAbsPath + * has an index or if another error occurs. + */ + public NodeId clone(Path srcPath, Path destPath) + throws ConstraintViolationException, AccessDeniedException, + VersionException, PathNotFoundException, ItemExistsException, + LockException, RepositoryException, IllegalStateException { + + // check precondition + checkInEditMode(); + + // 1. check paths & retrieve state + NodeState srcState = getNodeState(srcPath); + + Path destParentPath = destPath.getAncestor(1); + NodeState destParentState = getNodeState(destParentPath); + int ind = destPath.getIndex(); + if (ind > 0) { + // subscript in name element + String msg = + "invalid destination path: " + safeGetJCRPath(destPath) + + " (subscript in name element is not allowed)"; + log.debug(msg); + throw new RepositoryException(msg); + } + + return clone(srcState, destParentState, destPath.getName()); + } + + /** + * Implementation of {@link #clone(Path, Path)} that has already determined + * the affected NodeStates. + * + * @param srcState source state + * @param destParentState destination parent state + * @param destName destination name + * @return the node id of the destination's parent + * + * @throws ConstraintViolationException if the operation would violate a + * node-type or other implementation-specific constraint. + * @throws VersionException if the parent node of destAbsPath is + * versionable and checked-in, or is non-versionable but its nearest versionable ancestor is + * checked-in. This exception will also be thrown if removeExisting is true, + * and a UUID conflict occurs that would require the moving and/or altering of a node that is checked-in. + * @throws AccessDeniedException if the current session does not have + * sufficient access rights to complete the operation. + * @throws PathNotFoundException if the node at srcAbsPath in + * srcWorkspace or the parent of destAbsPath in this workspace does not exist. + * @throws ItemExistsException if a property already exists at + * destAbsPath or a node already exist there, and same name + * siblings are not allowed or if removeExisting is false and a + * UUID conflict occurs. + * @throws LockException if a lock prevents the clone. + * @throws RepositoryException if the last element of destAbsPath + * has an index or if another error occurs. + * @see #clone(Path, Path) + */ + public NodeId clone(NodeState srcState, NodeState destParentState, Name destName) + throws ConstraintViolationException, AccessDeniedException, + VersionException, PathNotFoundException, ItemExistsException, + LockException, RepositoryException, IllegalStateException { + + + // 2. check access rights, lock status, node type constraints, etc. + checkAddNode(destParentState, destName, + srcState.getNodeTypeName(), CHECK_ACCESS | CHECK_LOCK + | CHECK_CHECKED_OUT | CHECK_CONSTRAINTS | CHECK_HOLD | CHECK_RETENTION); + + // 3. verify that source has mixin mix:shareable + if (!isShareable(srcState)) { + String msg = + "Cloning inside a workspace is only allowed for shareable" + + " nodes. Node with type " + srcState.getNodeTypeName() + + " is not shareable."; + log.debug(msg); + throw new RepositoryException(msg); + } + + // 4. detect share cycle + NodeId srcId = srcState.getNodeId(); + NodeId destParentId = destParentState.getNodeId(); + if (destParentId.equals(srcId) || hierMgr.isAncestor(srcId, destParentId)) { + String msg = + "Cloning Node with id " + srcId + + " to parent with id " + destParentId + + " would create a share cycle."; + log.debug(msg); + throw new RepositoryException(msg); + } + + // 5. do clone operation (modify and store affected states) + if (!srcState.addShare(destParentState.getNodeId())) { + String msg = + "Adding a shareable node with id (" + + destParentState.getNodeId() + + ") twice to the same parent is not supported."; + log.debug(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + destParentState.addChildNodeEntry(destName, srcState.getNodeId()); + + // store states + stateMgr.store(srcState); + stateMgr.store(destParentState); + return destParentState.getNodeId(); + } + + + /** + * Copies the tree at srcPath to the new location at + * destPath. Returns the id of the node at its new position. + *

    + * Precondition: the state manager needs to be in edit mode. + * + * @param srcPath + * @param destPath + * @param flag one of + *

      + *
    • COPY
    • + *
    • CLONE
    • + *
    • CLONE_REMOVE_EXISTING
    • + *
    + * @return the id of the node at its new position + * @throws RepositoryException if the copy operation fails + */ + public NodeId copy(Path srcPath, Path destPath, int flag) + throws RepositoryException { + return copy( + srcPath, stateMgr, hierMgr, context.getAccessManager(), + destPath, flag); + } + + /** + * Copies the tree at srcPath retrieved using the specified + * srcStateMgr to the new location at destPath. + * Returns the id of the node at its new position. + *

    + * Precondition: the state manager needs to be in edit mode. + * + * @param srcPath + * @param srcStateMgr + * @param srcHierMgr + * @param srcAccessMgr + * @param destPath + * @param flag one of + *

      + *
    • COPY
    • + *
    • CLONE
    • + *
    • CLONE_REMOVE_EXISTING
    • + *
    + * @return the id of the node at its new position + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws PathNotFoundException + * @throws ItemExistsException + * @throws LockException + * @throws RepositoryException + * @throws IllegalStateException if the state manager is not in edit mode. + */ + public NodeId copy(Path srcPath, + ItemStateManager srcStateMgr, + HierarchyManager srcHierMgr, + AccessManager srcAccessMgr, + Path destPath, + int flag) + throws ConstraintViolationException, AccessDeniedException, + VersionException, PathNotFoundException, ItemExistsException, + LockException, RepositoryException, IllegalStateException { + + // check precondition + checkInEditMode(); + + // 1. check paths & retrieve state + + NodeState srcState = getNodeState(srcStateMgr, srcHierMgr, srcPath); + + Path destParentPath = destPath.getAncestor(1); + NodeState destParentState = getNodeState(destParentPath); + int ind = destPath.getIndex(); + if (ind > 0) { + // subscript in name element + String msg = + "invalid copy destination path: " + safeGetJCRPath(destPath) + + " (subscript in name element is not allowed)"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // 2. check access rights, lock status, node type constraints, etc. + + // JCR-2269: store target node state in changelog early as a + // precautionary measure in order to isolate it from concurrent + // underlying changes while checking preconditions + stateMgr.store(destParentState); + checkAddNode(destParentState, destPath.getName(), + srcState.getNodeTypeName(), CHECK_ACCESS | CHECK_LOCK + | CHECK_CHECKED_OUT | CHECK_CONSTRAINTS | CHECK_HOLD | CHECK_RETENTION); + // check read access right on source node using source access manager + try { + if (!srcAccessMgr.isGranted(srcPath, Permission.READ)) { + throw new PathNotFoundException(safeGetJCRPath(srcPath)); + } + } catch (ItemNotFoundException infe) { + String msg = + "internal error: failed to check access rights for " + + safeGetJCRPath(srcPath); + log.debug(msg); + throw new RepositoryException(msg, infe); + } + + // 3. do copy operation (modify and store affected states) + + ReferenceChangeTracker refTracker = new ReferenceChangeTracker(); + + // create deep copy of source node state + NodeState newState = copyNodeState(srcState, srcPath, srcStateMgr, srcAccessMgr, + destParentState.getNodeId(), flag, refTracker); + + // add to new parent + destParentState.addChildNodeEntry(destPath.getName(), newState.getNodeId()); + + // adjust references that refer to uuid's which have been mapped to + // newly generated uuid's on copy/clone + Iterator iter = refTracker.getProcessedReferences(); + while (iter.hasNext()) { + PropertyState prop = (PropertyState) iter.next(); + // being paranoid... + if (prop.getType() != PropertyType.REFERENCE + && prop.getType() != PropertyType.WEAKREFERENCE) { + continue; + } + boolean modified = false; + InternalValue[] values = prop.getValues(); + InternalValue[] newVals = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + NodeId adjusted = refTracker.getMappedId(values[i].getNodeId()); + if (adjusted != null) { + boolean weak = prop.getType() == PropertyType.WEAKREFERENCE; + newVals[i] = InternalValue.create(adjusted, weak); + modified = true; + } else { + // reference doesn't need adjusting, just copy old value + newVals[i] = values[i]; + } + } + if (modified) { + prop.setValues(newVals); + stateMgr.store(prop); + } + } + refTracker.clear(); + + // store states + stateMgr.store(newState); + stateMgr.store(destParentState); + return newState.getNodeId(); + } + + /** + * Moves the tree at srcPath to the new location at + * destPath. Returns the id of the moved node. + *

    + * Precondition: the state manager needs to be in edit mode. + * + * @param srcPath + * @param destPath + * @return the id of the moved node + * @throws ConstraintViolationException + * @throws VersionException + * @throws AccessDeniedException + * @throws PathNotFoundException + * @throws ItemExistsException + * @throws LockException + * @throws RepositoryException + * @throws IllegalStateException if the state manager is not in edit mode + */ + public NodeId move(Path srcPath, Path destPath) + throws ConstraintViolationException, VersionException, + AccessDeniedException, PathNotFoundException, ItemExistsException, + LockException, RepositoryException, IllegalStateException { + + // check precondition + if (!stateMgr.inEditMode()) { + throw new IllegalStateException( + "cannot move path " + safeGetJCRPath(srcPath) + + " because manager is not in edit mode"); + } + + // 1. check paths & retrieve state + + try { + if (srcPath.isAncestorOf(destPath)) { + String msg = + safeGetJCRPath(destPath) + ": invalid destination path" + + " (cannot be descendant of source path)"; + log.debug(msg); + throw new RepositoryException(msg); + } + } catch (MalformedPathException mpe) { + String msg = "invalid path for move: " + safeGetJCRPath(destPath); + log.debug(msg); + throw new RepositoryException(msg, mpe); + } + + Path srcParentPath = srcPath.getAncestor(1); + NodeState target = getNodeState(srcPath); + NodeState srcParent = getNodeState(srcParentPath); + + Path destParentPath = destPath.getAncestor(1); + NodeState destParent = getNodeState(destParentPath); + + int ind = destPath.getIndex(); + if (ind > 0) { + // subscript in name element + String msg = + safeGetJCRPath(destPath) + ": invalid destination path" + + " (subscript in name element is not allowed)"; + log.debug(msg); + throw new RepositoryException(msg); + } + + HierarchyManagerImpl hierMgr = (HierarchyManagerImpl) this.hierMgr; + if (hierMgr.isShareAncestor(target.getNodeId(), destParent.getNodeId())) { + String msg = + safeGetJCRPath(destPath) + ": invalid destination path" + + " (share cycle detected)"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // 2. check if target state can be removed from old/added to new parent + + checkRemoveNode(target, srcParent.getNodeId(), + CHECK_ACCESS | CHECK_LOCK | CHECK_CHECKED_OUT | CHECK_CONSTRAINTS + | CHECK_HOLD | CHECK_RETENTION); + checkAddNode(destParent, destPath.getName(), + target.getNodeTypeName(), CHECK_ACCESS | CHECK_LOCK + | CHECK_CHECKED_OUT | CHECK_CONSTRAINTS | CHECK_HOLD | CHECK_RETENTION); + + // 3. do move operation (modify and store affected states) + boolean renameOnly = srcParent.getNodeId().equals(destParent.getNodeId()); + + int srcNameIndex = srcPath.getIndex(); + if (srcNameIndex == 0) { + srcNameIndex = 1; + } + + stateMgr.store(target); + if (renameOnly) { + stateMgr.store(srcParent); + // change child node entry + destParent.renameChildNodeEntry(srcPath.getName(), srcNameIndex, + destPath.getName()); + } else { + // check shareable case + if (target.isShareable()) { + String msg = + "Moving a shareable node (" + safeGetJCRPath(srcPath) + + ") is not supported."; + log.debug(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + + stateMgr.store(srcParent); + stateMgr.store(destParent); + + // do move: + // 1. remove child node entry from old parent + if (srcParent.removeChildNodeEntry(target.getNodeId())) { + // 2. re-parent target node + target.setParentId(destParent.getNodeId()); + // 3. add child node entry to new parent + destParent.addChildNodeEntry(destPath.getName(), target.getNodeId()); + } + } + + return target.getNodeId(); + } + + /** + * Removes the specified node, recursively removing its properties and + * child nodes. + *

    + * Precondition: the state manager needs to be in edit mode. + * + * @param nodePath + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ReferentialIntegrityException + * @throws RepositoryException + * @throws IllegalStateException + */ + public void removeNode(Path nodePath) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ReferentialIntegrityException, RepositoryException, + IllegalStateException { + + // check precondition + if (!stateMgr.inEditMode()) { + throw new IllegalStateException( + "cannot remove node (" + safeGetJCRPath(nodePath) + + ") because manager is not in edit mode"); + } + + // 1. retrieve affected state + NodeState target = getNodeState(nodePath); + NodeId parentId = target.getParentId(); + + // 2. check if target state can be removed from parent + checkRemoveNode(target, parentId, + CHECK_ACCESS | CHECK_LOCK | CHECK_CHECKED_OUT + | CHECK_CONSTRAINTS | CHECK_REFERENCES | CHECK_HOLD | CHECK_RETENTION); + + // 3. do remove operation + removeNodeState(target); + } + + //--------------------------------------< misc. high-level helper methods > + /** + * Checks if adding a child node called nodeName of node type + * nodeTypeName to the given parent node is allowed in the + * current context. + * + * @param parentState + * @param nodeName + * @param nodeTypeName + * @param options bit-wise OR'ed flags specifying the checks that should be + * performed; any combination of the following constants: + *

      + *
    • {@link #CHECK_ACCESS}: make sure + * current session is granted read & write access on + * parent node
    • + *
    • {@link #CHECK_LOCK}: make sure + * there's no foreign lock on parent node
    • + *
    • {@link #CHECK_CHECKED_OUT}: make sure + * parent node is checked-out
    • + *
    • {@link #CHECK_CONSTRAINTS}: + * make sure no node type constraints would be violated
    • + *
    • {@link #CHECK_HOLD}: check for effective holds preventing the add operation
    • + *
    • {@link #CHECK_RETENTION}: check for effective retention policy preventing the add operation
    • + *
    + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ItemExistsException + * @throws RepositoryException + */ + public void checkAddNode(NodeState parentState, Name nodeName, + Name nodeTypeName, int options) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ItemExistsException, RepositoryException { + + Path parentPath = hierMgr.getPath(parentState.getNodeId()); + + // 1. locking status + + if ((options & CHECK_LOCK) == CHECK_LOCK) { + // make sure there's no foreign lock on parent node + verifyUnlocked(parentPath); + } + + // 2. versioning status + + if ((options & CHECK_CHECKED_OUT) == CHECK_CHECKED_OUT) { + // make sure parent node is checked-out + verifyCheckedOut(parentPath); + } + + // 3. access rights + + if ((options & CHECK_ACCESS) == CHECK_ACCESS) { + AccessManager accessMgr = context.getAccessManager(); + // make sure current session is granted read access on parent node + if (!accessMgr.isGranted(parentPath, Permission.READ)) { + throw new ItemNotFoundException(safeGetJCRPath(parentState.getNodeId())); + } + // make sure current session is granted write access on parent node + if (!accessMgr.isGranted(parentPath, nodeName, Permission.ADD_NODE)) { + throw new AccessDeniedException(safeGetJCRPath(parentState.getNodeId()) + + ": not allowed to add child node"); + } + // make sure the editing session is allowed create nodes with a + // specified node type (and ev. mixins) + if (!accessMgr.isGranted(parentPath, nodeName, Permission.NODE_TYPE_MNGMT)) { + throw new AccessDeniedException(safeGetJCRPath(parentState.getNodeId()) + + ": not allowed to add child node"); + } + } + + // 4. node type constraints + + if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) { + QItemDefinition parentDef = + context.getItemManager().getDefinition(parentState).unwrap(); + // make sure parent node is not protected + if (parentDef.isProtected()) { + throw new ConstraintViolationException( + safeGetJCRPath(parentState.getNodeId()) + + ": cannot add child node to protected parent node"); + } + // make sure there's an applicable definition for new child node + EffectiveNodeType entParent = getEffectiveNodeType(parentState); + entParent.checkAddNodeConstraints( + nodeName, nodeTypeName, context.getNodeTypeRegistry()); + QNodeDefinition newNodeDef = + findApplicableNodeDefinition(nodeName, nodeTypeName, + parentState); + + // check for name collisions + if (parentState.hasChildNodeEntry(nodeName)) { + // there's already a node with that name... + + // get definition of existing conflicting node + ChildNodeEntry entry = parentState.getChildNodeEntry(nodeName, 1); + NodeState conflictingState; + NodeId conflictingId = entry.getId(); + try { + conflictingState = (NodeState) stateMgr.getItemState(conflictingId); + } catch (ItemStateException ise) { + String msg = + "internal error: failed to retrieve state of " + + safeGetJCRPath(conflictingId); + log.debug(msg); + throw new RepositoryException(msg, ise); + } + QNodeDefinition conflictingTargetDef = + context.getItemManager().getDefinition(conflictingState).unwrap(); + // check same-name sibling setting of both target and existing node + if (!conflictingTargetDef.allowsSameNameSiblings() + || !newNodeDef.allowsSameNameSiblings()) { + throw new ItemExistsException( + "cannot add child node '" + nodeName.getLocalName() + + "' to " + safeGetJCRPath(parentState.getNodeId()) + + ": colliding with same-named existing node"); + } + } + } + + RetentionRegistry retentionReg = + context.getSessionImpl().getRetentionRegistry(); + if ((options & CHECK_HOLD) == CHECK_HOLD) { + if (retentionReg.hasEffectiveHold(parentPath, false)) { + throw new RepositoryException("Unable to add node. Parent is affected by a hold."); + } + } + if ((options & CHECK_RETENTION) == CHECK_RETENTION) { + if (retentionReg.hasEffectiveRetention(parentPath, false)) { + throw new RepositoryException("Unable to add node. Parent is affected by a retention."); + } + } + } + + /** + * Checks if removing the given target node is allowed in the current context. + * + * @param targetState + * @param options bit-wise OR'ed flags specifying the checks that should be + * performed; any combination of the following constants: + *
      + *
    • {@link #CHECK_ACCESS}: make sure + * current session is granted read access on parent + * and remove privilege on target node
    • + *
    • {@link #CHECK_LOCK}: make sure + * there's no foreign lock on parent node
    • + *
    • {@link #CHECK_CHECKED_OUT}: make sure + * parent node is checked-out
    • + *
    • {@link #CHECK_CONSTRAINTS}: + * make sure no node type constraints would be violated
    • + *
    • {@link #CHECK_REFERENCES}: + * make sure no references exist on target node
    • + *
    • {@link #CHECK_HOLD}: check for effective holds preventing the add operation
    • + *
    • {@link #CHECK_RETENTION}: check for effective retention policy preventing the add operation
    • + *
    + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ReferentialIntegrityException + * @throws RepositoryException + */ + public void checkRemoveNode(NodeState targetState, int options) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ReferentialIntegrityException, RepositoryException { + checkRemoveNode(targetState, targetState.getParentId(), options); + } + + /** + * Checks if removing the given target node from the specifed parent + * is allowed in the current context. + * + * @param targetState + * @param parentId + * @param options bit-wise OR'ed flags specifying the checks that should be + * performed; any combination of the following constants: + *
      + *
    • {@link #CHECK_ACCESS}: make sure + * current session is granted read access on parent + * and remove privilege on target node
    • + *
    • {@link #CHECK_LOCK}: make sure + * there's no foreign lock on parent node
    • + *
    • {@link #CHECK_CHECKED_OUT}: make sure + * parent node is checked-out
    • + *
    • {@link #CHECK_CONSTRAINTS}: + * make sure no node type constraints would be violated
    • + *
    • {@link #CHECK_REFERENCES}: + * make sure no references exist on target node
    • + *
    • {@link #CHECK_HOLD}: check for effective holds preventing the add operation
    • + *
    • {@link #CHECK_RETENTION}: check for effective retention policy preventing the add operation
    • + *
    + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ReferentialIntegrityException + * @throws RepositoryException + */ + public void checkRemoveNode(NodeState targetState, NodeId parentId, + int options) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ReferentialIntegrityException, RepositoryException { + + if (targetState.getParentId() == null) { + // root or orphaned node + throw new ConstraintViolationException("cannot remove root node"); + } + Path targetPath = hierMgr.getPath(targetState.getNodeId()); + NodeState parentState = getNodeState(parentId); + Path parentPath = hierMgr.getPath(parentId); + + // 1. locking status + + if ((options & CHECK_LOCK) == CHECK_LOCK) { + // make sure there's no foreign lock on parent node + verifyUnlocked(parentPath); + } + + // 2. versioning status + + if ((options & CHECK_CHECKED_OUT) == CHECK_CHECKED_OUT) { + // make sure parent node is checked-out + verifyCheckedOut(parentPath); + } + + // 3. access rights + + if ((options & CHECK_ACCESS) == CHECK_ACCESS) { + try { + AccessManager accessMgr = context.getAccessManager(); + // make sure current session is granted read access on parent node + if (!accessMgr.isGranted(targetPath, Permission.READ)) { + throw new PathNotFoundException(safeGetJCRPath(targetPath)); + } + // make sure current session is allowed to remove target node + if (!accessMgr.isGranted(targetPath, Permission.REMOVE_NODE)) { + throw new AccessDeniedException(safeGetJCRPath(targetPath) + + ": not allowed to remove node"); + } + } catch (ItemNotFoundException infe) { + String msg = "internal error: failed to check access rights for " + + safeGetJCRPath(targetPath); + log.debug(msg); + throw new RepositoryException(msg, infe); + } + } + + // 4. node type constraints + + if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) { + QItemDefinition parentDef = + context.getItemManager().getDefinition(parentState).unwrap(); + if (parentDef.isProtected()) { + throw new ConstraintViolationException(safeGetJCRPath(parentId) + + ": cannot remove child node of protected parent node"); + } + QItemDefinition targetDef = + context.getItemManager().getDefinition(targetState).unwrap(); + if (targetDef.isMandatory()) { + throw new ConstraintViolationException(safeGetJCRPath(targetPath) + + ": cannot remove mandatory node"); + } + if (targetDef.isProtected()) { + throw new ConstraintViolationException(safeGetJCRPath(targetPath) + + ": cannot remove protected node"); + } + } + + // 5. referential integrity + + if ((options & CHECK_REFERENCES) == CHECK_REFERENCES) { + EffectiveNodeType ent = getEffectiveNodeType(targetState); + if (ent.includesNodeType(NameConstants.MIX_REFERENCEABLE)) { + NodeId targetId = targetState.getNodeId(); + if (stateMgr.hasNodeReferences(targetId)) { + try { + NodeReferences refs = stateMgr.getNodeReferences(targetId); + if (refs.hasReferences()) { + throw new ReferentialIntegrityException(safeGetJCRPath(targetPath) + + ": cannot remove node with references"); + } + } catch (ItemStateException ise) { + String msg = "internal error: failed to check references on " + + safeGetJCRPath(targetPath); + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + } + } + } + + RetentionRegistry retentionReg = + context.getSessionImpl().getRetentionRegistry(); + if ((options & CHECK_HOLD) == CHECK_HOLD) { + if (retentionReg.hasEffectiveHold(targetPath, true)) { + throw new RepositoryException("Unable to perform removal. Node is affected by a hold."); + } + } + if ((options & CHECK_RETENTION) == CHECK_RETENTION) { + if (retentionReg.hasEffectiveRetention(targetPath, true)) { + throw new RepositoryException("Unable to perform removal. Node is affected by a retention."); + } + } + } + + /** + * Verifies that the node at nodePath is writable. The + * following conditions must hold true: + *
      + *
    • the node must exist
    • + *
    • the current session must be granted read & write access on it
    • + *
    • the node must not be locked by another session
    • + *
    • the node must not be checked-in
    • + *
    • the node must not be protected
    • + *
    • the node must not be affected by a hold or a retention policy
    • + *
    + * + * @param nodePath path of node to check + * @throws PathNotFoundException if no node exists at + * nodePath of the current + * session is not granted read access + * to the specified path + * @throws AccessDeniedException if write access to the specified + * path is not allowed + * @throws ConstraintViolationException if the node at nodePath + * is protected + * @throws VersionException if the node at nodePath + * is checked-in + * @throws LockException if the node at nodePath + * is locked by another session + * @throws RepositoryException if another error occurs + */ + public void verifyCanWrite(Path nodePath) + throws PathNotFoundException, AccessDeniedException, + ConstraintViolationException, VersionException, LockException, + RepositoryException { + + NodeState node = getNodeState(nodePath); + + // access rights + // make sure current session is granted read access on node + AccessManager accessMgr = context.getAccessManager(); + if (!accessMgr.isGranted(nodePath, Permission.READ)) { + throw new PathNotFoundException(safeGetJCRPath(node.getNodeId())); + } + // TODO: removed check for 'WRITE' permission on node due to the fact, + // TODO: that add_node and set_property permission are granted on the + // TODO: items to be create/modified and not on their parent. + // in any case, the ability to add child-nodes and properties is checked + // while executing the corresponding operation. + + // locking status + verifyUnlocked(nodePath); + + // node type constraints + verifyNotProtected(nodePath); + + // versioning status + verifyCheckedOut(nodePath); + + RetentionRegistry retentionReg = + context.getSessionImpl().getRetentionRegistry(); + if (retentionReg.hasEffectiveHold(nodePath, false)) { + throw new RepositoryException("Unable to write. Node is affected by a hold."); + } + if (retentionReg.hasEffectiveRetention(nodePath, false)) { + throw new RepositoryException("Unable to write. Node is affected by a retention."); + } + } + + /** + * Verifies that the node at nodePath can be read. The + * following conditions must hold true: + *
      + *
    • the node must exist
    • + *
    • the current session must be granted read access on it
    • + *
    + * + * @param nodePath path of node to check + * @throws PathNotFoundException if no node exists at + * nodePath of the current + * session is not granted read access + * to the specified path + * @throws RepositoryException if another error occurs + */ + public void verifyCanRead(Path nodePath) + throws PathNotFoundException, RepositoryException { + // access rights + // make sure current session is granted read access on node + AccessManager accessMgr = context.getAccessManager(); + if (!accessMgr.isGranted(nodePath, Permission.READ)) { + throw new PathNotFoundException(safeGetJCRPath(nodePath)); + } + } + + //--------------------------------------------< low-level item operations > + /** + * Creates a new node. + *

    + * Note that access rights are not enforced! + *

    + * Precondition: the state manager needs to be in edit mode. + * + * @param parent + * @param nodeName + * @param nodeTypeName + * @param mixinNames + * @param id + * @return + * @throws ItemExistsException + * @throws ConstraintViolationException + * @throws RepositoryException + * @throws IllegalStateException if the state manager is not in edit mode. + */ + public NodeState createNodeState(NodeState parent, + Name nodeName, + Name nodeTypeName, + Name[] mixinNames, + NodeId id) + throws ItemExistsException, ConstraintViolationException, + RepositoryException, IllegalStateException { + + // check precondition + if (!stateMgr.inEditMode()) { + throw new IllegalStateException( + "cannot create node state for " + nodeName + + " because manager is not in edit mode"); + } + + QNodeDefinition def = findApplicableNodeDefinition(nodeName, nodeTypeName, parent); + return createNodeState(parent, nodeName, nodeTypeName, mixinNames, id, def); + } + + /** + * Creates a new node based on the given definition. + *

    + * Note that access rights are not enforced! + *

    + * Precondition: the state manager needs to be in edit mode. + * + * @param parent + * @param nodeName + * @param nodeTypeName + * @param mixinNames + * @param id + * @param def + * @return + * @throws ItemExistsException + * @throws ConstraintViolationException + * @throws RepositoryException + * @throws IllegalStateException + */ + public NodeState createNodeState(NodeState parent, + Name nodeName, + Name nodeTypeName, + Name[] mixinNames, + NodeId id, + QNodeDefinition def) + throws ItemExistsException, ConstraintViolationException, + RepositoryException, IllegalStateException { + + // check for name collisions with existing nodes + if (!def.allowsSameNameSiblings() && parent.hasChildNodeEntry(nodeName)) { + NodeId errorId = parent.getChildNodeEntry(nodeName, 1).getId(); + throw new ItemExistsException(safeGetJCRPath(errorId)); + } + if (nodeTypeName == null) { + // no primary node type specified, + // try default primary type from definition + nodeTypeName = def.getDefaultPrimaryType(); + if (nodeTypeName == null) { + String msg = + "an applicable node type could not be determined for " + + nodeName; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + NodeState node = stateMgr.createNew(id, nodeTypeName, parent.getNodeId()); + if (mixinNames != null && mixinNames.length > 0) { + node.setMixinTypeNames(new HashSet(Arrays.asList(mixinNames))); + } + + // now add new child node entry to parent + parent.addChildNodeEntry(nodeName, node.getNodeId()); + + EffectiveNodeType ent = getEffectiveNodeType(node); + + // check shareable + if (ent.includesNodeType(NameConstants.MIX_SHAREABLE)) { + node.addShare(parent.getNodeId()); + } + + if (!node.getMixinTypeNames().isEmpty()) { + // create jcr:mixinTypes property + QPropertyDefinition pd = ent.getApplicablePropertyDef(NameConstants.JCR_MIXINTYPES, + PropertyType.NAME, true); + createPropertyState(node, pd.getName(), pd.getRequiredType(), pd); + } + + // add 'auto-create' properties defined in node type + for (QPropertyDefinition pd : ent.getAutoCreatePropDefs()) { + createPropertyState(node, pd.getName(), pd.getRequiredType(), pd); + } + + // recursively add 'auto-create' child nodes defined in node type + for (QNodeDefinition nd : ent.getAutoCreateNodeDefs()) { + createNodeState(node, nd.getName(), nd.getDefaultPrimaryType(), + null, null, nd); + } + + // store node + stateMgr.store(node); + // store parent + stateMgr.store(parent); + + return node; + } + + /** + * Creates a new property. + *

    + * Note that access rights are not enforced! + *

    + * Precondition: the state manager needs to be in edit mode. + * + * @param parent + * @param propName + * @param type + * @param numValues + * @return + * @throws ItemExistsException + * @throws ConstraintViolationException + * @throws RepositoryException + * @throws IllegalStateException if the state manager is not in edit mode + */ + public PropertyState createPropertyState(NodeState parent, + Name propName, + int type, + int numValues) + throws ItemExistsException, ConstraintViolationException, + RepositoryException, IllegalStateException { + + // check precondition + if (!stateMgr.inEditMode()) { + throw new IllegalStateException( + "cannot create property state for " + propName + + " because manager is not in edit mode"); + } + + // find applicable definition + QPropertyDefinition def; + // multi- or single-valued property? + if (numValues == 1) { + // could be single- or multi-valued (n == 1) + try { + // try single-valued + def = findApplicablePropertyDefinition(propName, + type, false, parent); + } catch (ConstraintViolationException cve) { + // try multi-valued + def = findApplicablePropertyDefinition(propName, + type, true, parent); + } + } else { + // can only be multi-valued (n == 0 || n > 1) + def = findApplicablePropertyDefinition(propName, + type, true, parent); + } + return createPropertyState(parent, propName, type, def); + } + + /** + * Creates a new property based on the given definition. + *

    + * Note that access rights are not enforced! + *

    + * Precondition: the state manager needs to be in edit mode. + * + * @param parent + * @param propName + * @param type + * @param def + * @return + * @throws ItemExistsException + * @throws RepositoryException + */ + public PropertyState createPropertyState(NodeState parent, + Name propName, + int type, + QPropertyDefinition def) + throws ItemExistsException, RepositoryException { + + // check for name collisions with existing properties + if (parent.hasPropertyName(propName)) { + PropertyId errorId = new PropertyId(parent.getNodeId(), propName); + throw new ItemExistsException(safeGetJCRPath(errorId)); + } + + // create property + PropertyState prop = stateMgr.createNew(propName, parent.getNodeId()); + + if (def.getRequiredType() != PropertyType.UNDEFINED) { + prop.setType(def.getRequiredType()); + } else if (type != PropertyType.UNDEFINED) { + prop.setType(type); + } else { + prop.setType(PropertyType.STRING); + } + prop.setMultiValued(def.isMultiple()); + + // compute system generated values if necessary + new NodeTypeInstanceHandler(session.getUserID()).setDefaultValues( + prop, parent, def); + + // now add new property entry to parent + parent.addPropertyName(propName); + // store parent + stateMgr.store(parent); + + return prop; + } + + /** + * Unlinks the specified node state from its parent and recursively + * removes it including its properties and child nodes. + *

    + * Note that no checks (access rights etc.) are performed on the specified + * target node state. Those checks have to be performed beforehand by the + * caller. However, the (recursive) removal of target node's child nodes are + * subject to the following checks: access rights, locking, versioning. + * + * @param target + * @throws RepositoryException if an error occurs + */ + public void removeNodeState(NodeState target) + throws RepositoryException { + + NodeId parentId = target.getParentId(); + if (parentId == null) { + String msg = "root node cannot be removed"; + log.debug(msg); + throw new RepositoryException(msg); + } + // remove target + recursiveRemoveNodeState(target); + // remove child node entry from parent + NodeState parent = getNodeState(parentId); + parent.removeChildNodeEntry(target.getNodeId()); + // store parent + stateMgr.store(parent); + } + + /** + * Retrieves the state of the node at the given path. + *

    + * Note that access rights are not enforced! + * + * @param nodePath + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public NodeState getNodeState(Path nodePath) + throws PathNotFoundException, RepositoryException { + return getNodeState(stateMgr, hierMgr, nodePath); + } + + /** + * Retrieves the state of the node with the given id. + *

    + * Note that access rights are not enforced! + * + * @param id + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public NodeState getNodeState(NodeId id) + throws ItemNotFoundException, RepositoryException { + return (NodeState) getItemState(stateMgr, id); + } + + /** + * Retrieves the state of the property with the given id. + *

    + * Note that access rights are not enforced! + * + * @param id + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public PropertyState getPropertyState(PropertyId id) + throws ItemNotFoundException, RepositoryException { + return (PropertyState) getItemState(stateMgr, id); + } + + /** + * Retrieves the state of the item with the given id. + *

    + * Note that access rights are not enforced! + * + * @param id + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public ItemState getItemState(ItemId id) + throws ItemNotFoundException, RepositoryException { + return getItemState(stateMgr, id); + } + + //----------------------------------------------------< protected methods > + /** + * Verifies that the node at nodePath is checked-out; throws a + * VersionException if that's not the case. + *

    + * A node is considered checked-out if it is versionable and + * checked-out, or is non-versionable but its nearest versionable ancestor + * is checked-out, or is non-versionable and there are no versionable + * ancestors. + * + * @param nodePath + * @throws PathNotFoundException + * @throws VersionException + * @throws RepositoryException + */ + protected void verifyCheckedOut(Path nodePath) + throws PathNotFoundException, VersionException, RepositoryException { + // search nearest ancestor that is versionable, start with node at nodePath + /** + * FIXME should not only rely on existence of jcr:isCheckedOut property + * but also verify that node.isNodeType("mix:versionable")==true; + * this would have a negative impact on performance though... + */ + NodeState nodeState = getNodeState(nodePath); + while (!nodeState.hasPropertyName(NameConstants.JCR_ISCHECKEDOUT)) { + if (nodePath.denotesRoot()) { + return; + } + nodePath = nodePath.getAncestor(1); + nodeState = getNodeState(nodePath); + } + PropertyId propId = + new PropertyId(nodeState.getNodeId(), NameConstants.JCR_ISCHECKEDOUT); + PropertyState propState; + try { + propState = (PropertyState) stateMgr.getItemState(propId); + } catch (ItemStateException ise) { + String msg = "internal error: failed to retrieve state of " + + safeGetJCRPath(propId); + log.debug(msg); + throw new RepositoryException(msg, ise); + } + boolean checkedOut = propState.getValues()[0].getBoolean(); + if (!checkedOut) { + throw new VersionException(safeGetJCRPath(nodePath) + " is checked-in"); + } + } + + /** + * Verifies that the node at nodePath is not locked by + * somebody else than the current session. + * + * @param nodePath path of node to check + * @throws PathNotFoundException + * @throws LockException if write access to the specified path is not allowed + * @throws RepositoryException if another error occurs + */ + protected void verifyUnlocked(Path nodePath) + throws LockException, RepositoryException { + // make sure there's no foreign lock on node at nodePath + context.getWorkspace().getInternalLockManager().checkLock( + nodePath, session); + } + + /** + * Verifies that the node at nodePath is not protected. + * + * @param nodePath path of node to check + * @throws PathNotFoundException if no node exists at nodePath + * @throws ConstraintViolationException if write access to the specified + * path is not allowed + * @throws RepositoryException if another error occurs + */ + protected void verifyNotProtected(Path nodePath) + throws PathNotFoundException, ConstraintViolationException, + RepositoryException { + NodeState node = getNodeState(nodePath); + if (context.getItemManager().getDefinition(node).isProtected()) { + throw new ConstraintViolationException(safeGetJCRPath(nodePath) + + ": node is protected"); + } + } + + /** + * Retrieves the state of the node at nodePath using the given + * item state manager. + *

    + * Note that access rights are not enforced! + * + * @param srcStateMgr + * @param srcHierMgr + * @param nodePath + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + protected NodeState getNodeState(ItemStateManager srcStateMgr, + HierarchyManager srcHierMgr, + Path nodePath) + throws PathNotFoundException, RepositoryException { + try { + NodeId id = srcHierMgr.resolveNodePath(nodePath); + if (id == null) { + throw new PathNotFoundException(safeGetJCRPath(nodePath)); + } + return (NodeState) getItemState(srcStateMgr, id); + } catch (ItemNotFoundException infe) { + throw new PathNotFoundException(safeGetJCRPath(nodePath)); + } + } + + /** + * Retrieves the state of the item with the specified id using the given + * item state manager. + *

    + * Note that access rights are not enforced! + * + * @param srcStateMgr + * @param id + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + protected ItemState getItemState(ItemStateManager srcStateMgr, ItemId id) + throws ItemNotFoundException, RepositoryException { + try { + return srcStateMgr.getItemState(id); + } catch (NoSuchItemStateException nsise) { + throw new ItemNotFoundException(safeGetJCRPath(id)); + } catch (ItemStateException ise) { + String msg = "internal error: failed to retrieve state of " + + safeGetJCRPath(id); + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + //------------------------------------------------------< private methods > + + /** + * Recursively removes the given node state including its properties and + * child nodes. + *

    + * The removal of child nodes is subject to the following checks: + * access rights, locking & versioning status. Referential integrity + * (references) is checked on commit. + *

    + * Note that the child node entry refering to targetState is + * not automatically removed from targetState's + * parent. + * + * @param targetState + * @throws RepositoryException if an error occurs + */ + private void recursiveRemoveNodeState(NodeState targetState) + throws RepositoryException { + + if (targetState.hasChildNodeEntries()) { + // remove child nodes + // use temp array to avoid ConcurrentModificationException + ArrayList tmp = new ArrayList(targetState.getChildNodeEntries()); + // remove from tail to avoid problems with same-name siblings + for (int i = tmp.size() - 1; i >= 0; i--) { + ChildNodeEntry entry = tmp.get(i); + NodeId nodeId = entry.getId(); + try { + NodeState nodeState = (NodeState) stateMgr.getItemState(nodeId); + // check if child node can be removed + // (access rights, locking & versioning status as well + // as retention and hold); + // referential integrity (references) is checked + // on commit + checkRemoveNode(nodeState, targetState.getNodeId(), + CHECK_ACCESS + | CHECK_LOCK + | CHECK_CHECKED_OUT + | CHECK_HOLD + | CHECK_RETENTION + ); + // remove child node + recursiveRemoveNodeState(nodeState); + } catch (ItemStateException ise) { + String msg = "internal error: failed to retrieve state of " + + nodeId; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + // remove child node entry + targetState.removeChildNodeEntry(entry.getName(), entry.getIndex()); + } + } + + // remove properties + // use temp set to avoid ConcurrentModificationException + HashSet tmp = new HashSet(targetState.getPropertyNames()); + for (Name propName : tmp) { + PropertyId propId = + new PropertyId(targetState.getNodeId(), propName); + try { + PropertyState propState = + (PropertyState) stateMgr.getItemState(propId); + // remove property entry + targetState.removePropertyName(propId.getName()); + // destroy property state + stateMgr.destroy(propState); + } catch (ItemStateException ise) { + String msg = "internal error: failed to retrieve state of " + + propId; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + // now actually do unlink target state + targetState.setParentId(null); + // destroy target state (pass overlayed state since target state + // might have been modified during unlinking) + stateMgr.destroy(targetState.getOverlayedState()); + } + + /** + * Recursively copies the specified node state including its properties and + * child nodes. + * + * @param srcState + * @param srcPath + * @param srcStateMgr + * @param srcAccessMgr + * @param destParentId + * @param flag one of + *

      + *
    • COPY
    • + *
    • CLONE
    • + *
    • CLONE_REMOVE_EXISTING
    • + *
    + * @param refTracker tracks uuid mappings and processed reference properties + * @return a deep copy of the given node state and its children + * @throws RepositoryException if an error occurs + */ + private NodeState copyNodeState(NodeState srcState, + Path srcPath, + ItemStateManager srcStateMgr, + AccessManager srcAccessMgr, + NodeId destParentId, + int flag, + ReferenceChangeTracker refTracker) + throws RepositoryException { + + NodeState newState; + try { + NodeId id = null; + EffectiveNodeType ent = getEffectiveNodeType(srcState); + boolean referenceable = ent.includesNodeType(NameConstants.MIX_REFERENCEABLE); + boolean versionable = ent.includesNodeType(NameConstants.MIX_SIMPLE_VERSIONABLE); + boolean fullVersionable = ent.includesNodeType(NameConstants.MIX_VERSIONABLE); + boolean shareable = ent.includesNodeType(NameConstants.MIX_SHAREABLE); + switch (flag) { + case COPY: + /* if this node is shareable and another node in the same shared set + * has been already been copied and given a new uuid, use this one + * (see section 14.5 of the specification) + */ + if (shareable && refTracker.getMappedId(srcState.getNodeId()) != null) { + NodeId newId = refTracker.getMappedId(srcState.getNodeId()); + NodeState sharedState = (NodeState) stateMgr.getItemState(newId); + sharedState.addShare(destParentId); + return sharedState; + } + break; + case CLONE: + if (!referenceable) { + // non-referenceable node: always create new node id + break; + } + // use same uuid as source node + id = srcState.getNodeId(); + + if (stateMgr.hasItemState(id)) { + if (shareable) { + NodeState sharedState = (NodeState) stateMgr.getItemState(id); + sharedState.addShare(destParentId); + return sharedState; + } + // node with this uuid already exists + throw new ItemExistsException(safeGetJCRPath(id)); + } + break; + case CLONE_REMOVE_EXISTING: + if (!referenceable) { + // non-referenceable node: always create new node id + break; + } + // use same uuid as source node + id = srcState.getNodeId(); + if (stateMgr.hasItemState(id)) { + NodeState existingState = (NodeState) stateMgr.getItemState(id); + // make sure existing node is not the parent + // or an ancestor thereof + if (id.equals(destParentId) + || hierMgr.isAncestor(id, destParentId)) { + String msg = + "cannot remove node " + safeGetJCRPath(srcPath) + + " because it is an ancestor of the destination"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // check if existing can be removed + // (access rights, locking & versioning status, + // node type constraints and retention/hold) + checkRemoveNode(existingState, + CHECK_ACCESS + | CHECK_LOCK + | CHECK_CHECKED_OUT + | CHECK_CONSTRAINTS + | CHECK_HOLD + | CHECK_RETENTION); + // do remove existing + removeNodeState(existingState); + } + break; + default: + throw new IllegalArgumentException( + "unknown flag for copying node state: " + flag); + } + newState = stateMgr.createNew(id, srcState.getNodeTypeName(), destParentId); + id = newState.getNodeId(); + if (flag == COPY && referenceable) { + // remember uuid mapping + refTracker.mappedId(srcState.getNodeId(), id); + } + // copy node state + newState.setMixinTypeNames(srcState.getMixinTypeNames()); + if (shareable) { + // initialize shared set + newState.addShare(destParentId); + } + // copy child nodes + for (ChildNodeEntry entry : srcState.getChildNodeEntries()) { + Path srcChildPath = PathFactoryImpl.getInstance().create(srcPath, entry.getName(), true); + if (!srcAccessMgr.isGranted(srcChildPath, Permission.READ)) { + continue; + } + NodeId nodeId = entry.getId(); + NodeState srcChildState = (NodeState) srcStateMgr.getItemState(nodeId); + + /** + * special handling required for child nodes with special semantics + * (e.g. those defined by nt:version, et.al.) + * + * todo FIXME delegate to 'node type instance handler' + */ + + /** + * If child is shareble and its UUID has already been remapped, + * then simply add a reference to the state with that remapped + * UUID instead of copying the whole subtree. + */ + if (srcChildState.isShareable()) { + NodeId mappedId = refTracker.getMappedId(srcChildState.getNodeId()); + if (mappedId != null) { + if (stateMgr.hasItemState(mappedId)) { + NodeState destState = (NodeState) stateMgr.getItemState(mappedId); + if (!destState.isShareable()) { + String msg = + "Remapped child (" + safeGetJCRPath(srcPath) + + ") is not shareable."; + throw new ItemStateException(msg); + } + if (!destState.addShare(id)) { + String msg = "Unable to add share to node: " + id; + throw new ItemStateException(msg); + } + stateMgr.store(destState); + newState.addChildNodeEntry(entry.getName(), mappedId); + continue; + } + } + } + + // recursive copying of child node + NodeState newChildState = copyNodeState(srcChildState, srcChildPath, + srcStateMgr, srcAccessMgr, id, flag, refTracker); + // store new child node + stateMgr.store(newChildState); + // add new child node entry to new node + newState.addChildNodeEntry(entry.getName(), newChildState.getNodeId()); + } + // init version history if needed + VersionHistoryInfo history = null; + if (versionable && flag == COPY) { + NodeId copiedFrom = null; + if (fullVersionable) { + // base version of copied versionable node is reference value of + // the histories jcr:copiedFrom property + PropertyId propId = new PropertyId(srcState.getNodeId(), NameConstants.JCR_BASEVERSION); + PropertyState prop = (PropertyState) srcStateMgr.getItemState(propId); + copiedFrom = prop.getValues()[0].getNodeId(); + } + InternalVersionManager manager = session.getInternalVersionManager(); + history = manager.getVersionHistory(session, newState, copiedFrom); + } + // copy properties + for (Name propName : srcState.getPropertyNames()) { + Path propPath = PathFactoryImpl.getInstance().create(srcPath, propName, true); + PropertyId propId = new PropertyId(srcState.getNodeId(), propName); + if (!srcAccessMgr.canRead(propPath, propId)) { + continue; + } + PropertyState srcChildState = + (PropertyState) srcStateMgr.getItemState(propId); + + /** + * special handling required for properties with special semantics + * (e.g. those defined by mix:referenceable, mix:versionable, + * mix:lockable, et.al.) + * + * todo FIXME delegate to 'node type instance handler' + */ + QPropertyDefinition def = ent.getApplicablePropertyDef( + srcChildState.getName(), srcChildState.getType(), + srcChildState.isMultiValued()); + if (NameConstants.MIX_LOCKABLE.equals(def.getDeclaringNodeType())) { + // skip properties defined by mix:lockable + continue; + } + + PropertyState newChildState = + copyPropertyState(srcChildState, id, propName, def); + + if (history != null) { + if (fullVersionable) { + if (propName.equals(NameConstants.JCR_VERSIONHISTORY)) { + // jcr:versionHistory + InternalValue value = InternalValue.create( + history.getVersionHistoryId()); + newChildState.setValues(new InternalValue[] { value }); + } else if (propName.equals(NameConstants.JCR_BASEVERSION) + || propName.equals(NameConstants.JCR_PREDECESSORS)) { + // jcr:baseVersion or jcr:predecessors + InternalValue value = InternalValue.create( + history.getRootVersionId()); + newChildState.setValues(new InternalValue[] { value }); + } else if (propName.equals(NameConstants.JCR_ISCHECKEDOUT)) { + // jcr:isCheckedOut + newChildState.setValues(new InternalValue[]{InternalValue.create(true)}); + } + } else { + // for simple versionable, we just initialize the + // version history when we see the jcr:isCheckedOut + if (propName.equals(NameConstants.JCR_ISCHECKEDOUT)) { + // jcr:isCheckedOut + newChildState.setValues(new InternalValue[]{InternalValue.create(true)}); + } + } + } + + if (newChildState.getType() == PropertyType.REFERENCE + || newChildState.getType() == PropertyType.WEAKREFERENCE) { + refTracker.processedReference(newChildState); + } + // store new property + stateMgr.store(newChildState); + // add new property entry to new node + newState.addPropertyName(propName); + } + return newState; + } catch (ItemStateException ise) { + String msg = "internal error: failed to copy state of " + srcState.getNodeId(); + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + /** + * Copies the specified property state. + * + * @param srcState the property state to copy. + * @param parentId the id of the parent node. + * @param propName the name of the property. + * @param def the definition of the property. + * @return a copy of the property state. + * @throws RepositoryException if an error occurs while copying. + */ + private PropertyState copyPropertyState(PropertyState srcState, + NodeId parentId, + Name propName, + QPropertyDefinition def) + throws RepositoryException { + + PropertyState newState = stateMgr.createNew(propName, parentId); + + newState.setType(srcState.getType()); + newState.setMultiValued(srcState.isMultiValued()); + InternalValue[] values = srcState.getValues(); + if (values != null) { + /** + * special handling required for properties with special semantics + * (e.g. those defined by mix:referenceable, mix:versionable, + * mix:lockable, et.al.) + * + * todo FIXME delegate to 'node type instance handler' + */ + if (propName.equals(NameConstants.JCR_UUID) + && def.getDeclaringNodeType().equals(NameConstants.MIX_REFERENCEABLE)) { + // set correct value of jcr:uuid property + newState.setValues(new InternalValue[]{InternalValue.create(parentId.toString())}); + } else { + InternalValue[] newValues = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + newValues[i] = values[i].createCopy(); + } + newState.setValues(newValues); + } + } + return newState; + } + + /** + * Check that the updatable item state manager is in edit mode. + * + * @throws IllegalStateException if it isn't + */ + private void checkInEditMode() throws IllegalStateException { + if (!stateMgr.inEditMode()) { + throw new IllegalStateException("not in edit mode"); + } + } + + /** + * Determines whether the specified node is shareable, i.e. + * whether the mixin type mix:shareable is either + * directly assigned or indirectly inherited. + * + * @param state node state to check + * @return true if the specified node is shareable, false otherwise. + * @throws RepositoryException if an error occurs + */ + private boolean isShareable(NodeState state) throws RepositoryException { + // shortcut: check some wellknown built-in types first + Name primary = state.getNodeTypeName(); + Set mixins = state.getMixinTypeNames(); + if (mixins.contains(NameConstants.MIX_SHAREABLE)) { + return true; + } + + try { + NodeTypeRegistry registry = context.getNodeTypeRegistry(); + EffectiveNodeType type = + registry.getEffectiveNodeType(primary, mixins); + return type.includesNodeType(NameConstants.MIX_REFERENCEABLE); + } catch (NodeTypeConflictException ntce) { + String msg = "internal error: failed to build effective node type for node " + + state.getNodeId(); + log.debug(msg); + throw new RepositoryException(msg, ntce); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/CachingHierarchyManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/CachingHierarchyManager.java new file mode 100644 index 00000000000..0bc3500ea8c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/CachingHierarchyManager.java @@ -0,0 +1,1072 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.NodeStateListener; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of a HierarchyManager that caches paths of + * items. + */ +public class CachingHierarchyManager extends HierarchyManagerImpl + implements NodeStateListener { + + /** + * Default upper limit of cached states + */ + public static final int DEFAULT_UPPER_LIMIT = 10000; + + private static final int MAX_UPPER_LIMIT = + Integer.getInteger("org.apache.jackrabbit.core.CachingHierarchyManager.cacheSize", DEFAULT_UPPER_LIMIT); + + private static final int CACHE_STATISTICS_LOG_INTERVAL_MILLIS = + Integer.getInteger("org.apache.jackrabbit.core.CachingHierarchyManager.logInterval", 60000); + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(CachingHierarchyManager.class); + + /** + * Mapping of paths to children in the path map + */ + private final PathMap pathCache = new PathMap(); + + /** + * Mapping of item ids to LRUEntry in the path map + */ + private final ReferenceMap idCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.HARD); + + /** + * Cache monitor object + */ + private final Object cacheMonitor = new Object(); + + /** + * Upper limit + */ + private final int upperLimit; + + /** + * Object collecting and logging statistics about the idCache + */ + private final CacheStatistics idCacheStatistics; + + /** + * Head of LRU + */ + private LRUEntry head; + + /** + * Tail of LRU + */ + private LRUEntry tail; + + /** + * Flag indicating whether consistency checking is enabled. + */ + private boolean consistencyCheckEnabled; + + /** + * Log interval for item state exceptions. + */ + private static final int ITEM_STATE_EXCEPTION_LOG_INTERVAL_MILLIS = 60 * 1000; + + /** + * Last time-stamp item state exception was logged with a stacktrace. + */ + private long itemStateExceptionLogTimestamp = 0; + + /** + * Create a new instance of this class. + * + * @param rootNodeId root node id + * @param provider item state manager + */ + public CachingHierarchyManager(NodeId rootNodeId, + ItemStateManager provider) { + super(rootNodeId, provider); + upperLimit = MAX_UPPER_LIMIT; + idCacheStatistics = new CacheStatistics(); + if (log.isTraceEnabled()) { + log.trace("CachingHierarchyManager initialized. Max cache size = {}", upperLimit, new Exception()); + } else { + log.debug("CachingHierarchyManager initialized. Max cache size = {}", upperLimit); + } + } + + /** + * Enable or disable consistency checks in this instance. + * + * @param enable true to enable consistency checks; + * false to disable + */ + public void enableConsistencyChecks(boolean enable) { + this.consistencyCheckEnabled = enable; + } + + //-------------------------------------------------< base class overrides > + + /** + * {@inheritDoc} + */ + protected ItemId resolvePath(Path path, int typesAllowed) + throws RepositoryException { + + Path pathToNode = path; + if ((typesAllowed & RETURN_NODE) == 0) { + // if we must not return a node, pass parent path + // (since we only cache nodes) + pathToNode = path.getAncestor(1); + } + + PathMap.Element element = map(pathToNode); + if (element == null) { + // not even intermediate match: call base class + return super.resolvePath(path, typesAllowed); + } + + LRUEntry entry = element.get(); + if (element.hasPath(path)) { + // exact match: return answer + synchronized (cacheMonitor) { + entry.touch(); + } + return entry.getId(); + } + Path.Element[] elements = path.getElements(); + try { + return resolvePath(elements, element.getDepth() + 1, entry.getId(), typesAllowed); + } catch (ItemStateException e) { + String msg = "failed to retrieve state of intermediary node for entry: " + + entry.getId() + ", path: " + path.getString(); + logItemStateException(msg, e); + log.debug(msg); + // probably stale cache entry -> evict + evictAll(entry.getId(), true); + } + // JCR-3617: fall back to super class in case of ItemStateException + return super.resolvePath(path, typesAllowed); + } + + /** + * {@inheritDoc} + */ + protected void pathResolved(ItemId id, PathBuilder builder) + throws MalformedPathException { + + if (id.denotesNode()) { + cache((NodeId) id, builder.getPath()); + } + } + + /** + * {@inheritDoc} + *

    + * Overridden method tries to find a mapping for the intermediate item + * state and add its path elements to the builder currently + * being used. If no mapping is found, the item is cached instead after + * the base implementation has been invoked. + */ + protected void buildPath( + PathBuilder builder, ItemState state, CycleDetector detector) + throws ItemStateException, RepositoryException { + + if (state.isNode()) { + PathMap.Element element = get(state.getId()); + if (element != null) { + try { + Path.Element[] elements = element.getPath().getElements(); + for (int i = elements.length - 1; i >= 0; i--) { + builder.addFirst(elements[i]); + } + return; + } catch (MalformedPathException mpe) { + String msg = "Failed to build path of " + state.getId(); + log.debug(msg); + throw new RepositoryException(msg, mpe); + } + } + } + + super.buildPath(builder, state, detector); + + if (state.isNode()) { + try { + cache(((NodeState) state).getNodeId(), builder.getPath()); + } catch (MalformedPathException mpe) { + log.warn("Failed to build path of " + state.getId()); + } + } + } + + //-----------------------------------------------------< HierarchyManager > + + /** + * {@inheritDoc} + *

    + * Overridden method simply checks whether we have an item matching the id + * and returns its path, otherwise calls base implementation. + */ + public Path getPath(ItemId id) + throws ItemNotFoundException, RepositoryException { + + if (id.denotesNode()) { + PathMap.Element element = get(id); + if (element != null) { + try { + return element.getPath(); + } catch (MalformedPathException mpe) { + String msg = "Failed to build path of " + id; + log.debug(msg); + throw new RepositoryException(msg, mpe); + } + } + } + return super.getPath(id); + } + + /** + * {@inheritDoc} + */ + public Name getName(ItemId id) + throws ItemNotFoundException, RepositoryException { + + if (id.denotesNode()) { + PathMap.Element element = get(id); + if (element != null) { + return element.getName(); + } + } + return super.getName(id); + } + + /** + * {@inheritDoc} + */ + public int getDepth(ItemId id) + throws ItemNotFoundException, RepositoryException { + + if (id.denotesNode()) { + PathMap.Element element = get(id); + if (element != null) { + return element.getDepth(); + } + } + return super.getDepth(id); + } + + /** + * {@inheritDoc} + */ + public boolean isAncestor(NodeId nodeId, ItemId itemId) + throws ItemNotFoundException, RepositoryException { + + if (itemId.denotesNode()) { + PathMap.Element element = get(nodeId); + if (element != null) { + PathMap.Element child = get(itemId); + if (child != null) { + return element.isAncestorOf(child); + } + } + } + return super.isAncestor(nodeId, itemId); + } + + //----------------------------------------------------< ItemStateListener > + + /** + * {@inheritDoc} + */ + public void stateCreated(ItemState created) { + } + + /** + * {@inheritDoc} + */ + public void stateModified(ItemState modified) { + if (modified.isNode()) { + nodeModified((NodeState) modified); + } + } + + /** + * {@inheritDoc} + * + * If path information is cached for modified, this iterates + * over all child nodes in the path map, evicting the ones that do not + * (longer) exist in the underlying NodeState. + */ + public void nodeModified(NodeState modified) { + synchronized (cacheMonitor) { + for (PathMap.Element element + : getCachedPaths(modified.getNodeId())) { + for (PathMap.Element child : element.getChildren()) { + ChildNodeEntry cne = modified.getChildNodeEntry( + child.getName(), child.getNormalizedIndex()); + if (cne == null) { + // Item does not exist, remove + evict(child, true); + } else { + LRUEntry childEntry = child.get(); + if (childEntry != null + && !cne.getId().equals(childEntry.getId())) { + // Different child item, remove + evict(child, true); + } + } + } + } + checkConsistency(); + } + } + + private List> getCachedPaths(NodeId id) { + // JCR-2720: Handle the root path as a special case + if (rootNodeId.equals(id)) { + return Collections.singletonList(pathCache.map( + PathFactoryImpl.getInstance().getRootPath(), true)); + } + + LRUEntry entry = (LRUEntry) idCache.get(id); + if (entry != null) { + return Arrays.asList(entry.getElements()); + } else { + return Collections.emptyList(); + } + } + + /** + * {@inheritDoc} + */ + public void stateDestroyed(ItemState destroyed) { + evictAll(destroyed.getId(), true); + } + + /** + * {@inheritDoc} + */ + public void stateDiscarded(ItemState discarded) { + if (discarded.isTransient() && !discarded.hasOverlayedState() + && discarded.getStatus() == ItemState.STATUS_NEW) { + // a new node has been discarded -> remove from cache + evictAll(discarded.getId(), true); + } else if (provider.hasItemState(discarded.getId())) { + evictAll(discarded.getId(), false); + } else { + evictAll(discarded.getId(), true); + } + } + + /** + * {@inheritDoc} + */ + public void nodeAdded(NodeState state, Name name, int index, NodeId id) { + synchronized (cacheMonitor) { + if (idCache.containsKey(state.getNodeId())) { + // Optimization: ignore notifications for nodes that are not in the cache + try { + Path path = PathFactoryImpl.getInstance().create(getPath(state.getNodeId()), name, index, true); + nodeAdded(state, path, id); + checkConsistency(); + } catch (PathNotFoundException e) { + log.warn("Unable to get path of node " + state.getNodeId() + + ", event ignored."); + } catch (MalformedPathException e) { + log.warn("Unable to create path of " + id, e); + } catch (ItemNotFoundException e) { + log.warn("Unable to find item " + state.getNodeId(), e); + } catch (ItemStateException e) { + log.warn("Unable to find item " + id, e); + } catch (RepositoryException e) { + log.warn("Unable to get path of " + state.getNodeId(), e); + } + } else if (state.getParentId() == null && idCache.containsKey(id)) { + // A top level node was added + evictAll(id, true); + } + } + } + + /** + * {@inheritDoc} + *

    + * Iterate over all cached children of this state and verify each + * child's position. + */ + public void nodesReplaced(NodeState state) { + synchronized (cacheMonitor) { + LRUEntry entry = (LRUEntry) idCache.get(state.getNodeId()); + if (entry == null) { + return; + } + for (PathMap.Element parent : entry.getElements()) { + HashMap> newChildrenOrder = + new HashMap>(); + boolean orderChanged = false; + + for (PathMap.Element child : parent.getChildren()) { + LRUEntry childEntry = (LRUEntry) child.get(); + if (childEntry == null) { + // Child has no associated UUID information: we're + // therefore unable to determine if this child's + // position is still accurate and have to assume + // the worst and remove it. + evict(child, false); + } else { + NodeId childId = childEntry.getId(); + ChildNodeEntry cne = state.getChildNodeEntry(childId); + if (cne == null) { + // Child no longer in parent node, so remove it + evict(child, false); + } else { + // Put all children into map of new children order + // - regardless whether their position changed or + // not - as we might need to reorder them later on. + Path.Element newNameIndex = + PathFactoryImpl.getInstance().createElement( + cne.getName(), cne.getIndex()); + newChildrenOrder.put(newNameIndex, child); + + if (!newNameIndex.equals(child.getPathElement())) { + orderChanged = true; + } + } + } + } + + if (orderChanged) { + /* If at least one child changed its position, reorder */ + parent.setChildren(newChildrenOrder); + } + } + checkConsistency(); + } + } + + /** + * {@inheritDoc} + */ + public void nodeRemoved(NodeState state, Name name, int index, NodeId id) { + synchronized (cacheMonitor) { + if (idCache.containsKey(state.getNodeId())) { + // Optimization: ignore notifications for nodes that are not in the cache + try { + Path path = PathFactoryImpl.getInstance().create(getPath(state.getNodeId()), name, index, true); + nodeRemoved(state, path, id); + checkConsistency(); + } catch (PathNotFoundException e) { + log.warn("Unable to get path of node " + state.getNodeId() + + ", event ignored."); + } catch (MalformedPathException e) { + log.warn("Unable to create path of " + id, e); + } catch (ItemStateException e) { + log.warn("Unable to find item " + id, e); + } catch (ItemNotFoundException e) { + log.warn("Unable to get path of " + state.getNodeId(), e); + } catch (RepositoryException e) { + log.warn("Unable to get path of " + state.getNodeId(), e); + } + } else if (state.getParentId() == null && idCache.containsKey(id)) { + // A top level node was removed + evictAll(id, true); + } + } + } + + //------------------------------------------------------< private methods > + + /** + * Return the first cached path that is mapped to given id. + * + * @param id node id + * @return cached element, null if not found + */ + private PathMap.Element get(ItemId id) { + synchronized (cacheMonitor) { + LRUEntry entry = (LRUEntry) idCache.get(id); + if (entry != null) { + entry.touch(); + return entry.getElements()[0]; + } + return null; + } + } + + /** + * Return the nearest cached element in the path map, given a path. + * The returned element is guaranteed to have an associated object that + * is not null. + * + * @param path path + * @return cached element, null if not found + */ + private PathMap.Element map(Path path) { + synchronized (cacheMonitor) { + PathMap.Element element = pathCache.map(path, false); + while (element != null) { + LRUEntry entry = element.get(); + if (entry != null) { + entry.touch(); + return element; + } + element = element.getParent(); + } + return null; + } + } + + /** + * Cache an item in the hierarchy given its id and path. + * + * @param id node id + * @param path path to item + */ + private void cache(NodeId id, Path path) { + synchronized (cacheMonitor) { + if (isCached(id, path)) { + return; + } + if (idCache.size() >= upperLimit) { + + idCacheStatistics.log(); + + /** + * Remove least recently used item. Scans the LRU list from + * head to tail and removes the first item that has no children. + */ + LRUEntry entry = head; + while (entry != null) { + PathMap.Element[] elements = entry.getElements(); + int childrenCount = 0; + for (int i = 0; i < elements.length; i++) { + childrenCount += elements[i].getChildrenCount(); + } + if (childrenCount == 0) { + evictAll(entry.getId(), false); + return; + } + entry = entry.getNext(); + } + } + PathMap.Element element = pathCache.put(path); + if (element.get() != null) { + if (!id.equals(((LRUEntry) element.get()).getId())) { + log.debug("overwriting PathMap.Element"); + } + } + LRUEntry entry = (LRUEntry) idCache.get(id); + if (entry == null) { + entry = new LRUEntry(id, element); + idCache.put(id, entry); + } else { + entry.addElement(element); + } + element.set(entry); + + checkConsistency(); + } + } + + /** + * Return a flag indicating whether a certain node and/or path is cached. + * If path is null, check whether the item is + * cached at all. If path is not null, + * check whether the item is cached with that path. + * + * @param id item id + * @param path path, may be null + * @return true if the item is already cached; + * false otherwise + */ + boolean isCached(NodeId id, Path path) { + synchronized (cacheMonitor) { + LRUEntry entry = (LRUEntry) idCache.get(id); + if (entry == null) { + return false; + } + if (path == null) { + return true; + } + PathMap.Element[] elements = entry.getElements(); + for (int i = 0; i < elements.length; i++) { + if (elements[i].hasPath(path)) { + return true; + } + } + return false; + } + } + + /** + * Return a flag indicating whether a certain path is cached. + * + * @param path item path + * @return true if the item is already cached; + * false otherwise + */ + boolean isCached(Path path) { + synchronized (cacheMonitor) { + PathMap.Element element = pathCache.map(path, true); + if (element != null) { + return element.get() != null; + } + return false; + } + } + + /** + * Remove all path mapping for a given item id. Removes the associated + * LRUEntry and the PathMap.Element with it. + * Indexes of same name sibling elements are shifted! + * + * @param id item id + */ + private void evictAll(ItemId id, boolean shift) { + synchronized (cacheMonitor) { + LRUEntry entry = (LRUEntry) idCache.get(id); + if (entry != null) { + PathMap.Element[] elements = entry.getElements(); + for (int i = 0; i < elements.length; i++) { + evict(elements[i], shift); + } + } + checkConsistency(); + } + } + + /** + * Evict path map element from cache. This will traverse all children + * of this element and remove the objects associated with them. + * Index of same name sibling items are shifted! + * + * @param element path map element + */ + private void evict(PathMap.Element element, boolean shift) { + // assert: synchronized (cacheMonitor) + element.traverse(new PathMap.ElementVisitor() { + public void elementVisited(PathMap.Element element) { + LRUEntry entry = (LRUEntry) element.get(); + if (entry.removeElement(element) == 0) { + idCache.remove(entry.getId()); + entry.remove(); + } + } + }, false); + element.remove(shift); + } + + /** + * Invoked when a notification about a child node addition has been received. + * + * @param state node state where child was added + * @param path path to child node + * @param id child node id + * + * @throws PathNotFoundException if the path was not found + * @throws RepositoryException If the path's direct ancestor cannot be determined. + * @throws ItemStateException If the id cannot be resolved to a NodeState. + */ + private void nodeAdded(NodeState state, Path path, NodeId id) + throws RepositoryException, ItemStateException { + + // assert: synchronized (cacheMonitor) + PathMap.Element element = null; + + LRUEntry entry = (LRUEntry) idCache.get(id); + if (entry != null) { + // child node already cached: this can have the following + // reasons: + // 1) node was moved, cached path is outdated + // 2) node was cloned, cached path is still valid + NodeState child = null; + if (hasItemState(id)) { + child = (NodeState) getItemState(id); + } + if (child == null || !child.isShareable()) { + PathMap.Element[] elements = entry.getElements(); + element = elements[0]; + for (int i = 0; i < elements.length; i++) { + elements[i].remove(); + } + } + } + PathMap.Element parent = pathCache.map(path.getAncestor(1), true); + if (parent != null) { + parent.insert(path.getNameElement()); + } + if (element != null) { + // store remembered element at new position + pathCache.put(path, element); + } + } + + /** + * Invoked when a notification about a child node removal has been received. + * + * @param state node state + * @param path node path + * @param id node id + * + * @throws PathNotFoundException if the path was not found. + * @throws RepositoryException If the path's direct ancestor cannot be determined. + * @throws ItemStateException If the id cannot be resolved to a NodeState. + */ + private void nodeRemoved(NodeState state, Path path, NodeId id) + throws RepositoryException, ItemStateException { + + // assert: synchronized (cacheMonitor) + PathMap.Element parent = + pathCache.map(path.getAncestor(1), true); + if (parent == null) { + return; + } + PathMap.Element element = + parent.getDescendant(path.getLastElement(), true); + if (element != null) { + // with SNS, this might evict a child that is NOT the one + // having id, check first whether item has + // the id passed as argument + LRUEntry entry = (LRUEntry) element.get(); + if (entry != null && !entry.getId().equals(id)) { + return; + } + // if item is shareable, remove this path only, otherwise + // every path this item has been mapped to + NodeState child = null; + if (hasItemState(id)) { + child = (NodeState) getItemState(id); + } + if (child == null || !child.isShareable()) { + evictAll(id, true); + } else { + evict(element, true); + } + } else { + // element itself is not cached, but removal might cause SNS + // index shifting + parent.remove(path.getNameElement()); + } + } + + /** + * Dump contents of path map and elements included to a string. + */ + public String toString() { + final StringBuilder builder = new StringBuilder(); + synchronized (cacheMonitor) { + pathCache.traverse(new PathMap.ElementVisitor() { + public void elementVisited(PathMap.Element element) { + for (int i = 0; i < element.getDepth(); i++) { + builder.append("--"); + } + builder.append(element.getName()); + int index = element.getIndex(); + if (index != 0 && index != 1) { + builder.append('['); + builder.append(index); + builder.append(']'); + } + builder.append(" "); + builder.append(element.get()); + builder.append("\n"); + } + }, true); + } + return builder.toString(); + } + + /** + * Check consistency. + */ + private void checkConsistency() throws IllegalStateException { + // assert: synchronized (cacheMonitor) + if (!consistencyCheckEnabled) { + return; + } + + int elementsInCache = 0; + + Iterator iter = idCache.values().iterator(); + while (iter.hasNext()) { + LRUEntry entry = (LRUEntry) iter.next(); + elementsInCache += entry.getElements().length; + } + + class PathMapElementCounter implements PathMap.ElementVisitor { + int count; + public void elementVisited(PathMap.Element element) { + LRUEntry mappedEntry = (LRUEntry) element.get(); + LRUEntry cachedEntry = (LRUEntry) idCache.get(mappedEntry.getId()); + if (cachedEntry == null) { + String msg = "Path element (" + element + + " ) cached in path map, associated id (" + + mappedEntry.getId() + ") isn't."; + throw new IllegalStateException(msg); + } + if (cachedEntry != mappedEntry) { + String msg = "LRUEntry associated with element (" + element + + " ) in path map is not equal to cached LRUEntry (" + + cachedEntry.getId() + ")."; + throw new IllegalStateException(msg); + } + PathMap.Element[] elements = cachedEntry.getElements(); + for (int i = 0; i < elements.length; i++) { + if (elements[i] == element) { + count++; + return; + } + } + String msg = "Element (" + element + + ") cached in path map, but not in associated LRUEntry (" + + cachedEntry.getId() + ")."; + throw new IllegalStateException(msg); + } + } + + PathMapElementCounter counter = new PathMapElementCounter(); + pathCache.traverse(counter, false); + if (counter.count != elementsInCache) { + String msg = "PathMap element and cached element count don't match (" + + counter.count + " != " + elementsInCache + ")"; + throw new IllegalStateException(msg); + } + } + + /** + * Helper method to log item state exception with stack trace every so often. + * + * @param logMessage log message + * @param e item state exception + */ + private void logItemStateException(String logMessage, ItemStateException e) { + long now = System.currentTimeMillis(); + if ((now - itemStateExceptionLogTimestamp) >= ITEM_STATE_EXCEPTION_LOG_INTERVAL_MILLIS) { + itemStateExceptionLogTimestamp = now; + log.debug(logMessage, e); + } else { + log.debug(logMessage); + } + } + + /** + * Entry in the LRU list + */ + private class LRUEntry { + + /** + * Previous entry + */ + private LRUEntry previous; + + /** + * Next entry + */ + private LRUEntry next; + + /** + * Node id + */ + private final NodeId id; + + /** + * Elements in path map + */ + private PathMap.Element[] elements; + + /** + * Create a new instance of this class + * + * @param id node id + * @param element the path map element for this entry + */ + public LRUEntry(NodeId id, PathMap.Element element) { + this.id = id; + this.elements = new PathMap.Element[] { element }; + + append(); + } + + /** + * Append entry to end of LRU list + */ + public void append() { + if (tail == null) { + head = this; + tail = this; + } else { + previous = tail; + tail.next = this; + tail = this; + } + } + + /** + * Remove entry from LRU list + */ + public void remove() { + if (previous != null) { + previous.next = next; + } + if (next != null) { + next.previous = previous; + } + if (head == this) { + head = next; + } + if (tail == this) { + tail = previous; + } + previous = null; + next = null; + } + + /** + * Touch entry. Removes it from its current position in the LRU list + * and moves it to the end. + */ + public void touch() { + remove(); + append(); + } + + /** + * Return next LRU entry + * + * @return next LRU entry + */ + public LRUEntry getNext() { + return next; + } + + /** + * Return node ID + * + * @return node ID + */ + public NodeId getId() { + return id; + } + + /** + * Return elements in path map that are mapped to id. If + * this entry is a shareable node or one of its descendant, it can + * be reached by more than one path. + * + * @return element in path map + */ + public PathMap.Element[] getElements() { + return elements; + } + + /** + * Add a mapping to some element. + */ + public void addElement(PathMap.Element element) { + PathMap.Element[] tmp = + new PathMap.Element[elements.length + 1]; + System.arraycopy(elements, 0, tmp, 0, elements.length); + tmp[elements.length] = element; + elements = tmp; + } + + /** + * Remove a mapping to some element from this entry. + * + * @return number of mappings left + */ + public int removeElement(PathMap.Element element) { + boolean found = false; + for (int i = 0; i < elements.length; i++) { + if (found) { + elements[i - 1] = elements[i]; + } else if (elements[i] == element) { + found = true; + } + } + if (found) { + PathMap.Element[] tmp = + new PathMap.Element[elements.length - 1]; + System.arraycopy(elements, 0, tmp, 0, tmp.length); + elements = tmp; + } + return elements.length; + } + + /** + * {@inheritDoc} + */ + public String toString() { + return id.toString(); + } + } + + private final class CacheStatistics { + + private final String id; + + private final ReferenceMap cache; + + private long timeStamp = 0; + + public CacheStatistics() { + this.id = cacheMonitor.toString(); + this.cache = idCache; + } + + public void log() { + if (log.isDebugEnabled()) { + long now = System.currentTimeMillis(); + final String msg = "Cache id = {};size = {};max = {}"; + if (log.isTraceEnabled()) { + log.trace(msg, new Object[]{id, this.cache.size(), upperLimit}, new Exception()); + } else if (now > timeStamp + CACHE_STATISTICS_LOG_INTERVAL_MILLIS) { + timeStamp = now; + log.debug(msg, new Object[]{id, this.cache.size(), upperLimit}, new Exception()); + } + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/DefaultSecurityManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/DefaultSecurityManager.java new file mode 100644 index 00000000000..95dccb6a411 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/DefaultSecurityManager.java @@ -0,0 +1,681 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.security.Principal; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.security.AccessControlException; +import javax.security.auth.Subject; + +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.api.security.user.UserManager; +import org.apache.jackrabbit.core.config.AccessManagerConfig; +import org.apache.jackrabbit.core.config.LoginModuleConfig; +import org.apache.jackrabbit.core.config.SecurityConfig; +import org.apache.jackrabbit.core.config.SecurityManagerConfig; +import org.apache.jackrabbit.core.config.WorkspaceConfig; +import org.apache.jackrabbit.core.config.WorkspaceSecurityConfig; +import org.apache.jackrabbit.core.config.UserManagerConfig; +import org.apache.jackrabbit.core.security.AMContext; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.DefaultAccessManager; +import org.apache.jackrabbit.core.security.JackrabbitSecurityManager; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.SystemPrincipal; +import org.apache.jackrabbit.core.security.authentication.AuthContext; +import org.apache.jackrabbit.core.security.authentication.AuthContextProvider; +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.AccessControlProviderFactory; +import org.apache.jackrabbit.core.security.authorization.AccessControlProviderFactoryImpl; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; +import org.apache.jackrabbit.core.security.principal.AbstractPrincipalProvider; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; +import org.apache.jackrabbit.core.security.principal.DefaultPrincipalProvider; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.apache.jackrabbit.core.security.principal.PrincipalManagerImpl; +import org.apache.jackrabbit.core.security.principal.PrincipalProvider; +import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry; +import org.apache.jackrabbit.core.security.principal.ProviderRegistryImpl; +import org.apache.jackrabbit.core.security.user.MembershipCache; +import org.apache.jackrabbit.core.security.user.UserManagerImpl; +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The security manager acts as central managing class for all security related + * operations on a low-level non-protected level. It manages the + *

      + *
    • {@link PrincipalProvider}s + *
    • {@link AccessControlProvider}s + *
    • {@link WorkspaceAccessManager} + *
    • {@link UserManager} + *
    + */ +public class DefaultSecurityManager implements JackrabbitSecurityManager { + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(DefaultSecurityManager.class); + + /** + * Flag indicating if the security manager was properly initialized. + */ + private boolean initialized; + + /** + * the repository implementation + */ + private RepositoryImpl repository; + + /** + * System session. + */ + private SystemSession systemSession; + + /** + * System user manager. Implementation needed here for the DefaultPrincipalProvider. + */ + private UserManager systemUserManager; + + /** + * The user id of the administrator. The value is retrieved from + * configuration. If the config entry is missing a default id is used (see + * {@link SecurityConstants#ADMIN_ID}). + */ + protected String adminId; + + /** + * The user id of the anonymous user. The value is retrieved from + * configuration. If the config entry is missing a default id is used (see + * {@link SecurityConstants#ANONYMOUS_ID}). + */ + protected String anonymousId; + + /** + * Contains the access control providers per workspace. + * key = name of the workspace, + * value = {@link AccessControlProvider} + */ + private final Map acProviders = new HashMap(); + + /** + * the AccessControlProviderFactory + */ + private AccessControlProviderFactory acProviderFactory; + + /** + * the configured WorkspaceAccessManager + */ + private WorkspaceAccessManager workspaceAccessManager; + + /** + * the principal provider registry + */ + private PrincipalProviderRegistry principalProviderRegistry; + + /** + * factory for login-context {@see Repository#login()) + */ + private AuthContextProvider authContextProvider; + + //------------------------------------------< JackrabbitSecurityManager >--- + /** + * @see JackrabbitSecurityManager#init(Repository, Session) + */ + public synchronized void init(Repository repository, Session systemSession) throws RepositoryException { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + if (!(repository instanceof RepositoryImpl)) { + throw new RepositoryException("RepositoryImpl expected"); + } + if (!(systemSession instanceof SystemSession)) { + throw new RepositoryException("SystemSession expected"); + } + + this.systemSession = (SystemSession) systemSession; + this.repository = (RepositoryImpl) repository; + + SecurityConfig config = this.repository.getConfig().getSecurityConfig(); + LoginModuleConfig loginModConf = config.getLoginModuleConfig(); + + // build AuthContextProvider based on appName + optional LoginModuleConfig + authContextProvider = new AuthContextProvider(config.getAppName(), loginModConf); + if (authContextProvider.isLocal()) { + log.info("init: use Repository Login-Configuration for " + config.getAppName()); + } else if (authContextProvider.isJAAS()) { + log.info("init: use JAAS login-configuration for " + config.getAppName()); + } else { + String msg = "Neither JAAS nor RepositoryConfig contained a valid configuration for " + config.getAppName(); + log.error(msg); + throw new RepositoryException(msg); + } + + Properties[] moduleConfig = authContextProvider.getModuleConfig(); + + // retrieve default-ids (admin and anonymous) from login-module-configuration. + for (Properties props : moduleConfig) { + if (props.containsKey(LoginModuleConfig.PARAM_ADMIN_ID)) { + adminId = props.getProperty(LoginModuleConfig.PARAM_ADMIN_ID); + } + if (props.containsKey(LoginModuleConfig.PARAM_ANONYMOUS_ID)) { + anonymousId = props.getProperty(LoginModuleConfig.PARAM_ANONYMOUS_ID); + } + } + // fallback: + if (adminId == null) { + log.debug("No adminID defined in LoginModule/JAAS config -> using default."); + adminId = SecurityConstants.ADMIN_ID; + } + if (anonymousId == null) { + log.debug("No anonymousID defined in LoginModule/JAAS config -> using default."); + anonymousId = SecurityConstants.ANONYMOUS_ID; + } + + // create the system userManager and make sure the system-users exist. + systemUserManager = createUserManager(this.systemSession); + createSystemUsers(systemUserManager, this.systemSession, adminId, anonymousId); + + // init default ac-provider-factory + acProviderFactory = new AccessControlProviderFactoryImpl(); + acProviderFactory.init(this.systemSession); + + // create the workspace access manager + SecurityManagerConfig smc = config.getSecurityManagerConfig(); + if (smc != null && smc.getWorkspaceAccessConfig() != null) { + workspaceAccessManager = + smc.getWorkspaceAccessConfig().newInstance(WorkspaceAccessManager.class); + } else { + // fallback -> the default implementation + log.debug("No WorkspaceAccessManager configured; using default."); + workspaceAccessManager = createDefaultWorkspaceAccessManager(); + } + workspaceAccessManager.init(this.systemSession); + + // initialize principal-provider registry + // 1) create default + PrincipalProvider defaultPP = createDefaultPrincipalProvider(moduleConfig); + // 2) create registry instance + principalProviderRegistry = new ProviderRegistryImpl(defaultPP); + // 3) register all configured principal providers. + for (Properties props : moduleConfig) { + principalProviderRegistry.registerProvider(props); + } + + initialized = true; + } + + /** + * @see JackrabbitSecurityManager#dispose(String) + */ + public void dispose(String workspaceName) { + checkInitialized(); + synchronized (acProviders) { + AccessControlProvider prov = acProviders.remove(workspaceName); + if (prov != null) { + prov.close(); + } + } + } + + /** + * @see JackrabbitSecurityManager#close() + */ + public void close() { + checkInitialized(); + synchronized (acProviders) { + for (AccessControlProvider accessControlProvider : acProviders.values()) { + accessControlProvider.close(); + } + acProviders.clear(); + } + } + + /** + * @see JackrabbitSecurityManager#getAccessManager(Session,AMContext) + */ + public AccessManager getAccessManager(Session session, AMContext amContext) throws RepositoryException { + checkInitialized(); + AccessManagerConfig amConfig = repository.getConfig().getSecurityConfig().getAccessManagerConfig(); + try { + String wspName = session.getWorkspace().getName(); + AccessControlProvider pp = getAccessControlProvider(wspName); + AccessManager accessMgr; + if (amConfig == null) { + log.debug("No configuration entry for AccessManager. Using org.apache.jackrabbit.core.security.DefaultAccessManager"); + accessMgr = new DefaultAccessManager(); + } else { + accessMgr = amConfig.newInstance(AccessManager.class); + } + + accessMgr.init(amContext, pp, workspaceAccessManager); + return accessMgr; + } catch (AccessDeniedException e) { + // re-throw + throw e; + } catch (Exception e) { + // wrap in RepositoryException + String clsName = (amConfig == null) ? "-- missing access manager configuration --" : amConfig.getClassName(); + String msg = "Failed to instantiate AccessManager (" + clsName + ")"; + log.error(msg, e); + throw new RepositoryException(msg, e); + } + } + + /** + * @see JackrabbitSecurityManager#getPrincipalManager(Session) + */ + public PrincipalManager getPrincipalManager(Session session) throws RepositoryException { + checkInitialized(); + if (session instanceof SessionImpl) { + SessionImpl sImpl = (SessionImpl) session; + return createPrincipalManager(sImpl); + } else { + throw new RepositoryException("Internal error: SessionImpl expected."); + } + } + + /** + * @see JackrabbitSecurityManager#getUserManager(Session) + */ + public UserManager getUserManager(Session session) throws RepositoryException { + checkInitialized(); + if (session == systemSession) { + return systemUserManager; + } else if (session instanceof SessionImpl) { + String workspaceName = systemSession.getWorkspace().getName(); + try { + SessionImpl sImpl = (SessionImpl) session; + UserManagerImpl uMgr; + if (workspaceName.equals(sImpl.getWorkspace().getName())) { + uMgr = createUserManager(sImpl); + } else { + SessionImpl s = (SessionImpl) sImpl.createSession(workspaceName); + uMgr = createUserManager(s); + sImpl.addListener(uMgr); + } + return uMgr; + } catch (NoSuchWorkspaceException e) { + throw new AccessControlException("Cannot build UserManager for " + session.getUserID(), e); + } + } else { + throw new RepositoryException("Internal error: SessionImpl expected."); + } + } + + /** + * @see JackrabbitSecurityManager#getUserID(javax.security.auth.Subject, String) + */ + public String getUserID(Subject subject, String workspaceName) throws RepositoryException { + checkInitialized(); + + // shortcut if the subject contains the AdminPrincipal or + // SystemPrincipal in which cases the userID is already known. + if (!subject.getPrincipals(AdminPrincipal.class).isEmpty()) { + return adminId; + } else if (!subject.getPrincipals(SystemPrincipal.class).isEmpty()) { + // system session does not have a userId + return null; + } + + /* if there is a configure principal class that should be used to + determine the UserID -> try this one. */ + Class cl = getConfig().getUserIdClass(); + if (cl != null) { + Set s = subject.getPrincipals(cl); + if (!s.isEmpty()) { + for (Principal p : s) { + if (!GroupPrincipals.isGroup(p)) { + return p.getName(); + } + } + // all principals found with the given p-Class were Group principals + log.debug("Only Group principals found with class '" + cl.getName() + "' -> Not used for UserID."); + } else { + log.debug("No principal found with class '" + cl.getName() + "'."); + } + } + + /* + Fallback scenario to retrieve userID from the subject: + Since the subject may contain multiple principals and the principal + name may not be equals to the UserID, the id is retrieved by + searching for the corresponding authorizable and if this doesn't + succeed an attempt is made to obtained it from the login-credentials. + */ + String uid = null; + + // first try to retrieve an authorizable corresponding to + // a non-group principal. the first one present is used + // to determine the userID. + try { + UserManager umgr = getSystemUserManager(workspaceName); + for (Principal p : subject.getPrincipals()) { + if (!(p instanceof Group)) { + Authorizable authorz = umgr.getAuthorizable(p); + if (authorz != null && !authorz.isGroup()) { + uid = authorz.getID(); + break; + } + } + } + } catch (RepositoryException e) { + // failed to access userid via user manager -> use fallback 2. + log.error("Unexpected error while retrieving UserID.", e); + } + + // 2. if no matching user is found try simple access to userID over + // SimpleCredentials. + if (uid == null) { + Iterator creds = subject.getPublicCredentials( + SimpleCredentials.class).iterator(); + if (creds.hasNext()) { + SimpleCredentials sc = creds.next(); + uid = sc.getUserID(); + } + } + + return uid; + } + + /** + * Creates an AuthContext for the given {@link Credentials} and + * {@link Subject}. The workspace name is ignored and users are + * stored and retrieved from a specific (separate) workspace.
    + * This includes selection of application specific LoginModules and + * initialization with credentials and Session to System-Workspace + * + * @return an {@link AuthContext} for the given Credentials, Subject + * @throws RepositoryException in other exceptional repository states + */ + public AuthContext getAuthContext(Credentials creds, Subject subject, String workspaceName) + throws RepositoryException { + checkInitialized(); + return getAuthContextProvider().getAuthContext(creds, subject, systemSession, + getPrincipalProviderRegistry(), adminId, anonymousId); + } + + //----------------------------------------------------------< protected >--- + /** + * @return The SecurityManagerConfig configured for the + * repository this manager has been created for. + */ + protected SecurityManagerConfig getConfig() { + return repository.getConfig().getSecurityConfig().getSecurityManagerConfig(); + } + + /** + * @param workspaceName The name of the target workspace. + * @return The system user manager. Since this implementation stores users + * in a dedicated workspace the system user manager is the same for all + * sessions irrespective of the workspace. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + protected UserManager getSystemUserManager(String workspaceName) throws RepositoryException { + return systemUserManager; + } + + /** + * @param session The session for which to retrieve the membership cache. + * @return The membership cache. + * @throws RepositoryException If an error occurs. + */ + protected MembershipCache getMembershipCache(SessionImpl session) throws RepositoryException { + if (session == systemSession || session instanceof SystemSession) { + // force creation of the membership cache within the corresponding uMgr + return null; + } else { + return ((UserManagerImpl) getSystemUserManager(session.getWorkspace().getName())).getMembershipCache(); + } + } + + /** + * Creates a {@link UserManagerImpl} for the given session. May be overridden + * to return a custom implementation. + * + * @param session session + * @return user manager + * @throws RepositoryException if an error occurs + */ + protected UserManagerImpl createUserManager(SessionImpl session) throws RepositoryException { + UserManagerConfig umc = getConfig().getUserManagerConfig(); + UserManagerImpl um; + if (umc != null) { + Class[] paramTypes = new Class[] { + SessionImpl.class, + String.class, + Properties.class, + MembershipCache.class}; + um = (UserManagerImpl) umc.getUserManager(UserManagerImpl.class, + paramTypes, session, adminId, umc.getParameters(), getMembershipCache(session)); + } else { + um = new UserManagerImpl(session, adminId, null, getMembershipCache(session)); + } + + if (umc != null && !(session instanceof SystemSession)) { + AuthorizableAction[] actions = umc.getAuthorizableActions(); + um.setAuthorizableActions(actions); + } + return um; + } + + /** + * @param session The session used to create the principal manager. + * @return A new instance of PrincipalManagerImpl + * @throws javax.jcr.RepositoryException If an error occurs. + */ + protected PrincipalManager createPrincipalManager(SessionImpl session) throws RepositoryException { + return new PrincipalManagerImpl(session, getPrincipalProviderRegistry().getProviders()); + } + + /** + * @return A nwe instance of WorkspaceAccessManagerImpl to be used as + * default workspace access manager if the configuration doesn't specify one. + */ + protected WorkspaceAccessManager createDefaultWorkspaceAccessManager() { + return new WorkspaceAccessManagerImpl(); + } + + /** + * Creates the default principal provider used to create the + * {@link PrincipalProviderRegistry}. + * + * @return An new instance of DefaultPrincipalProvider. + * @throws RepositoryException If an error occurs. + */ + protected PrincipalProvider createDefaultPrincipalProvider(Properties[] moduleConfig) throws RepositoryException { + boolean initialized = false; + PrincipalProvider defaultPP = new DefaultPrincipalProvider(this.systemSession, (UserManagerImpl) systemUserManager); + for (Properties props : moduleConfig) { + //GRANITE-4470: apply config to DefaultPrincipalProvider if there is no explicit PrincipalProvider configured + if (!props.containsKey(LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS) && props.containsKey(AbstractPrincipalProvider.MAXSIZE_KEY)) { + defaultPP.init(props); + initialized = true; + break; + } + } + if (!initialized) { + defaultPP.init(new Properties()); + } + return defaultPP; + } + + /** + * @return The PrincipalProviderRegistry created during initialization. + */ + protected PrincipalProviderRegistry getPrincipalProviderRegistry() { + return principalProviderRegistry; + } + + /** + * @return The AuthContextProvider created during initialization. + */ + protected AuthContextProvider getAuthContextProvider() { + return authContextProvider; + } + + /** + * Throws IllegalStateException if this manager hasn't been + * initialized. + */ + protected void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("Not initialized"); + } + } + + /** + * @return The system session used to initialize this SecurityManager. + */ + protected Session getSystemSession() { + return systemSession; + } + + /** + * @return The repository used to initialize this SecurityManager. + */ + protected Repository getRepository() { + return repository; + } + //-------------------------------------------------------------------------- + /** + * Returns the access control provider for the specified + * workspaceName. + * + * @param workspaceName Name of the workspace. + * @return access control provider + * @throws NoSuchWorkspaceException If no workspace with 'workspaceName' exists. + * @throws RepositoryException + */ + private AccessControlProvider getAccessControlProvider(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + checkInitialized(); + AccessControlProvider provider = acProviders.get(workspaceName); + if (provider == null || !provider.isLive()) { + // mark this workspace as 'active' so the workspace does not + // get disposed by the workspace-janitor + // TODO: There should be a cleaner way to do this. + repository.markWorkspaceActive(workspaceName); + + WorkspaceSecurityConfig secConf = null; + WorkspaceConfig conf = + repository.getConfig().getWorkspaceConfig(workspaceName); + if (conf != null) { + secConf = conf.getSecurityConfig(); + } + + provider = acProviderFactory.createProvider( + repository.getSystemSession(workspaceName), secConf); + synchronized (acProviders) { + acProviders.put(workspaceName, provider); + } + } + return provider; + } + + /** + * Make sure the system users (admin and anonymous) exist. + * + * @param userManager Manager to create users/groups. + * @param session The editing session. + * @param adminId UserID of the administrator. + * @param anonymousId UserID of the anonymous user. + * @throws RepositoryException If an error occurs. + */ + static void createSystemUsers(UserManager userManager, + SystemSession session, + String adminId, + String anonymousId) throws RepositoryException { + + Authorizable admin; + if (adminId != null) { + admin = userManager.getAuthorizable(adminId); + if (admin == null) { + userManager.createUser(adminId, adminId); + if (!userManager.isAutoSave()) { + session.save(); + } + log.info("... created admin-user with id \'" + adminId + "\' ..."); + } + } + + if (anonymousId != null) { + Authorizable anonymous = userManager.getAuthorizable(anonymousId); + if (anonymous == null) { + try { + userManager.createUser(anonymousId, ""); + if (!userManager.isAutoSave()) { + session.save(); + } + log.info("... created anonymous user with id \'" + anonymousId + "\' ..."); + } catch (RepositoryException e) { + // exception while creating the anonymous user. + // log an error but don't abort the repository start-up + log.error("Failed to create anonymous user.", e); + } + } + } + } + + //------------------------------------------------------< inner classes >--- + /** + * WorkspaceAccessManager that upon {@link #grants(Set principals, String)} + * evaluates if access to the root node of a workspace with the specified + * name is granted. + */ + private final class WorkspaceAccessManagerImpl implements SecurityConstants, WorkspaceAccessManager { + + //-----------------------------------------< WorkspaceAccessManager >--- + /** + * {@inheritDoc} + */ + public void init(Session systemSession) throws RepositoryException { + // nothing to do here. + } + + /** + * {@inheritDoc} + */ + public void close() throws RepositoryException { + // nothing to do here. + } + + /** + * {@inheritDoc} + */ + public boolean grants(Set principals, String workspaceName) throws RepositoryException { + AccessControlProvider prov = getAccessControlProvider(workspaceName); + return prov.canAccessRoot(principals); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManager.java new file mode 100644 index 00000000000..7ba4245b5a0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManager.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * The HierarchyManager interface ... + */ +public interface HierarchyManager { + + /** + * Resolves a path into an item id. + *

    + * If there is both a node and a property at the specified path, this method + * will return the id of the node. + *

    + * Note that, for performance reasons, this method returns null + * rather than throwing a PathNotFoundException if there's no + * item to be found at path. + * + * @deprecated As of JSR 283, a Path doesn't anymore uniquely + * identify an Item, therefore {@link #resolveNodePath(Path)} and + * {@link #resolvePropertyPath(Path)} should be used instead. + * + * @param path path to resolve + * @return item id referred to by path or null + * if there's no item at path. + * @throws RepositoryException if an error occurs + */ + ItemId resolvePath(Path path) throws RepositoryException; + + /** + * Resolves a path into a node id. + *

    + * Note that, for performance reasons, this method returns null + * rather than throwing a PathNotFoundException if there's no + * node to be found at path. + * + * @param path path to resolve + * @return node id referred to by path or null + * if there's no node at path. + * @throws RepositoryException if an error occurs + */ + NodeId resolveNodePath(Path path) throws RepositoryException; + + /** + * Resolves a path into a property id. + *

    + * Note that, for performance reasons, this method returns null + * rather than throwing a PathNotFoundException if there's no + * property to be found at path. + * + * @param path path to resolve + * @return property id referred to by path or null + * if there's no property at path. + * @throws RepositoryException if an error occurs + */ + PropertyId resolvePropertyPath(Path path) throws RepositoryException; + + /** + * Returns the path to the given item. + * @param id + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + Path getPath(ItemId id) throws ItemNotFoundException, RepositoryException; + + /** + * Returns the name of the specified item. + * @param id id of item whose name should be returned + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + Name getName(ItemId id) throws ItemNotFoundException, RepositoryException; + + /** + * Returns the name of the specified item, with the given parent id. If the + * given item is not shareable, this is identical to {@link #getName(ItemId)}. + * + * @param id node id + * @param parentId parent node id + * @return name + * @throws ItemNotFoundException + * @throws RepositoryException + */ + Name getName(NodeId id, NodeId parentId) + throws ItemNotFoundException, RepositoryException; + + /** + * Returns the depth of the specified item which is equivalent to + * getPath(id).getAncestorCount(). The depth reflects the + * absolute hierarchy level. + * + * @param id item id + * @return the depth of the specified item + * @throws ItemNotFoundException if the specified id does not + * denote an existing item. + * @throws RepositoryException if another error occurs + */ + int getDepth(ItemId id) throws ItemNotFoundException, RepositoryException; + + /** + * Returns the depth of the specified descendant relative to the given + * ancestor. If ancestorId and descendantId + * denote the same item 0 is returned. If ancestorId does not + * denote an ancestor -1 is returned. + * + * @param ancestorId ancestor id + * @param descendantId descendant id + * @return the relative depth; -1 if ancestorId does not + * denote an ancestor of the item denoted by descendantId + * (or itself). + * @throws ItemNotFoundException if either of the specified id's does not + * denote an existing item. + * @throws RepositoryException if another error occurs + */ + int getRelativeDepth(NodeId ancestorId, ItemId descendantId) + throws ItemNotFoundException, RepositoryException; + + /** + * Determines whether the node with the specified nodeId + * is an ancestor of the item denoted by the given itemId. + * This is equivalent to + * getPath(nodeId).isAncestorOf(getPath(itemId)). + * + * @param nodeId node id + * @param itemId item id + * @return true if the node with the specified + * nodeId is an ancestor of the item denoted by the + * given itemId; false otherwise + * @throws ItemNotFoundException if any of the specified id's does not + * denote an existing item. + * @throws RepositoryException if another error occurs + */ + boolean isAncestor(NodeId nodeId, ItemId itemId) + throws ItemNotFoundException, RepositoryException; + + //------------------------------------------- operation with shareable nodes + + /** + * Determines whether the node with the specified ancestor + * is a share ancestor of the item denoted by the given descendant. + * This is true for two nodes A, B + * if either: + *

      + *
    • A is a (proper) ancestor of B
    • + *
    • there is a non-empty sequence of nodes N1,... + * ,Nk such that A= + * N1 and B=Nk + * and Ni is the parent or a share-parent of + * Ni+1 (for every i in 1 + * ...k-1.
    • + *
    + * + * @param ancestor node id + * @param descendant item id + * @return true if the node denoted by ancestor + * is a share ancestor of the item denoted by descendant, + * false otherwise + * @throws ItemNotFoundException if any of the specified id's does not + * denote an existing item. + * @throws RepositoryException if another error occurs + */ + boolean isShareAncestor(NodeId ancestor, NodeId descendant) + throws ItemNotFoundException, RepositoryException; + + /** + * Returns the depth of the specified share-descendant relative to the given + * share-ancestor. If ancestor and descendant + * denote the same item, 0 is returned. If ancestor + * does not denote an share-ancestor -1 is returned. + * + * @param ancestorId ancestor id + * @param descendantId descendant id + * @return the relative depth; -1 if ancestor does + * not denote a share-ancestor of the item denoted by descendant + * (or itself). + * @throws ItemNotFoundException if either of the specified id's does not + * denote an existing item. + * @throws RepositoryException if another error occurs + */ + int getShareRelativeDepth(NodeId ancestorId, ItemId descendantId) + throws ItemNotFoundException, RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManagerImpl.java new file mode 100644 index 00000000000..c4ccf3d97b8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/HierarchyManagerImpl.java @@ -0,0 +1,687 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * HierarchyManagerImpl ... + */ +public class HierarchyManagerImpl implements HierarchyManager { + + private static Logger log = LoggerFactory.getLogger(HierarchyManagerImpl.class); + + /** + * The parent name returned for orphaned or root nodes. + * TODO: Is it proper to use an invalid Name for this. + */ + private static final Name EMPTY_NAME = NameFactoryImpl.getInstance().create("", ""); + + protected final NodeId rootNodeId; + protected final ItemStateManager provider; + + /** + * Flags describing what items to return in {@link #resolvePath(Path, int)}. + */ + static final int RETURN_NODE = 1; + static final int RETURN_PROPERTY = 2; + static final int RETURN_ANY = (RETURN_NODE | RETURN_PROPERTY); + + public HierarchyManagerImpl(NodeId rootNodeId, + ItemStateManager provider) { + this.rootNodeId = rootNodeId; + this.provider = provider; + } + + public NodeId getRootNodeId() { + return rootNodeId; + } + + //-------------------------------------------------------< implementation > + + /** + * Internal implementation that iteratively resolves a path into an item. + * + * @param elements path elements + * @param next index of next item in elements to inspect + * @param id id of item at path elements[0]..elements[next - 1] + * @param typesAllowed one of RETURN_ANY, RETURN_NODE + * or RETURN_PROPERTY + * @return id or null + * @throws ItemStateException if an intermediate item state is not found + * @throws MalformedPathException if building an intermediate path fails + */ + protected ItemId resolvePath(Path.Element[] elements, int next, + ItemId id, int typesAllowed) + throws ItemStateException, MalformedPathException { + + PathBuilder builder = new PathBuilder(); + for (int i = 0; i < next; i++) { + builder.addLast(elements[i]); + } + for (int i = next; i < elements.length; i++) { + Path.Element elem = elements[i]; + NodeId parentId = (NodeId) id; + id = null; + + Name name = elem.getName(); + int index = elem.getIndex(); + if (index == 0) { + index = 1; + } + int typeExpected = typesAllowed; + if (i < elements.length - 1) { + // intermediate items must always be nodes + typeExpected = RETURN_NODE; + } + NodeState parentState = (NodeState) getItemState(parentId); + if ((typeExpected & RETURN_NODE) != 0) { + ChildNodeEntry nodeEntry = + getChildNodeEntry(parentState, name, index); + if (nodeEntry != null) { + id = nodeEntry.getId(); + } + } + if (id == null && (typeExpected & RETURN_PROPERTY) != 0) { + if (parentState.hasPropertyName(name) && (index <= 1)) { + // property + id = new PropertyId(parentState.getNodeId(), name); + } + } + if (id == null) { + break; + } + builder.addLast(elements[i]); + pathResolved(id, builder); + } + return id; + } + + //---------------------------------------------------------< overridables > + /** + * Return an item state, given its item id. + *

    + * Low-level hook provided for specialized derived classes. + * + * @param id item id + * @return item state + * @throws NoSuchItemStateException if the item does not exist + * @throws ItemStateException if an error occurs + * @see ZombieHierarchyManager#getItemState(ItemId) + */ + protected ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + return provider.getItemState(id); + } + + /** + * Determines whether an item state for a given item id exists. + *

    + * Low-level hook provided for specialized derived classes. + * + * @param id item id + * @return true if an item state exists, otherwise + * false + * @see ZombieHierarchyManager#hasItemState(ItemId) + */ + protected boolean hasItemState(ItemId id) { + return provider.hasItemState(id); + } + + /** + * Returns the parentUUID of the given item. + *

    + * Low-level hook provided for specialized derived classes. + * + * @param state item state + * @return parentUUID of the given item + * @see ZombieHierarchyManager#getParentId(ItemState) + */ + protected NodeId getParentId(ItemState state) { + return state.getParentId(); + } + + /** + * Return all parents of a node. A shareable node has possibly more than + * one parent. + * + * @param state item state + * @param useOverlayed whether to use overlayed state for shareable nodes + * @return set of parent NodeIds. If state has no parent, + * array has length 0. + */ + protected Set getParentIds(ItemState state, boolean useOverlayed) { + if (state.isNode()) { + // if this is a node, quickly check whether it is shareable and + // whether it contains more than one parent + NodeState ns = (NodeState) state; + if (ns.isShareable() && useOverlayed && ns.hasOverlayedState()) { + ns = (NodeState) ns.getOverlayedState(); + } + Set s = ns.getSharedSet(); + if (s.size() > 1) { + return s; + } + } + NodeId parentId = getParentId(state); + if (parentId != null) { + LinkedHashSet s = new LinkedHashSet(); + s.add(parentId); + return s; + } + return Collections.emptySet(); + } + + /** + * Returns the ChildNodeEntry of parent with the + * specified uuid or null if there's no such entry. + *

    + * Low-level hook provided for specialized derived classes. + * + * @param parent node state + * @param id id of child node entry + * @return the ChildNodeEntry of parent with + * the specified uuid or null if there's + * no such entry. + * @see ZombieHierarchyManager#getChildNodeEntry(NodeState, NodeId) + */ + protected ChildNodeEntry getChildNodeEntry(NodeState parent, + NodeId id) { + return parent.getChildNodeEntry(id); + } + + /** + * Returns the ChildNodeEntry of parent with the + * specified name and index or null + * if there's no such entry. + *

    + * Low-level hook provided for specialized derived classes. + * + * @param parent node state + * @param name name of child node entry + * @param index index of child node entry + * @return the ChildNodeEntry of parent with + * the specified name and index or + * null if there's no such entry. + * @see ZombieHierarchyManager#getChildNodeEntry(NodeState, Name, int) + */ + protected ChildNodeEntry getChildNodeEntry(NodeState parent, + Name name, + int index) { + return parent.getChildNodeEntry(name, index); + } + + /** + * Adds the path element of an item id to the path currently being built. + * Recursively invoked method that may be overridden by some subclass to + * either return cached responses or add response to cache. On exit, + * builder contains the path of state. + * + * @param builder builder currently being used + * @param state item to find path of + * @param detector path cycle detector + */ + protected void buildPath( + PathBuilder builder, ItemState state, CycleDetector detector) + throws ItemStateException, RepositoryException { + + // shortcut + if (state.getId().equals(rootNodeId)) { + builder.addRoot(); + return; + } + + NodeId parentId = getParentId(state); + if (parentId == null) { + String msg = "failed to build path of " + state.getId() + + ": orphaned item"; + log.debug(msg); + throw new ItemNotFoundException(msg); + } else if (detector.checkCycle(parentId)) { + throw new InvalidItemStateException( + "Path cycle detected: " + parentId); + } + + NodeState parent = (NodeState) getItemState(parentId); + // recursively build path of parent + buildPath(builder, parent, detector); + + if (state.isNode()) { + NodeState nodeState = (NodeState) state; + NodeId id = nodeState.getNodeId(); + ChildNodeEntry entry = getChildNodeEntry(parent, id); + if (entry == null) { + String msg = "failed to build path of " + state.getId() + ": " + + parent.getNodeId() + " has no child entry for " + + id; + log.debug(msg); + throw new ItemNotFoundException(msg); + } + // add to path + if (entry.getIndex() == 1) { + builder.addLast(entry.getName()); + } else { + builder.addLast(entry.getName(), entry.getIndex()); + } + } else { + PropertyState propState = (PropertyState) state; + Name name = propState.getName(); + // add to path + builder.addLast(name); + } + } + + /** + * Internal implementation of {@link #resolvePath(Path)} that will either + * resolve to a node or a property. Should be overridden by a subclass + * that can resolve an intermediate path into an ItemId. This + * subclass can then invoke {@link #resolvePath(org.apache.jackrabbit.spi.Path.Element[], int, ItemId, int)} + * with a value of next greater than 1. + * + * @param path path to resolve + * @param typesAllowed one of RETURN_ANY, RETURN_NODE + * or RETURN_PROPERTY + * @return id or null + * @throws RepositoryException if an error occurs + */ + protected ItemId resolvePath(Path path, int typesAllowed) + throws RepositoryException { + + Path.Element[] elements = path.getElements(); + ItemId id = rootNodeId; + + try { + return resolvePath(elements, 1, id, typesAllowed); + } catch (ItemStateException e) { + String msg = "failed to retrieve state of intermediary node"; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + /** + * Called by {@link #resolvePath(org.apache.jackrabbit.spi.Path.Element[], int, ItemId, int)}. + * May be overridden by some subclass to process/cache intermediate state. + * + * @param id id of resolved item + * @param builder path builder containing path resolved + * @throws MalformedPathException if the path contained in builder + * is malformed + */ + protected void pathResolved(ItemId id, PathBuilder builder) + throws MalformedPathException { + + // do nothing + } + + //-----------------------------------------------------< HierarchyManager > + + /** + * {@inheritDoc} + */ + public final ItemId resolvePath(Path path) throws RepositoryException { + // shortcut + if (path.denotesRoot()) { + return rootNodeId; + } + if (!path.isCanonical()) { + String msg = "path is not canonical"; + log.debug(msg); + throw new RepositoryException(msg); + } + return resolvePath(path, RETURN_ANY); + } + + /** + * {@inheritDoc} + */ + public NodeId resolveNodePath(Path path) throws RepositoryException { + return (NodeId) resolvePath(path, RETURN_NODE); + } + + /** + * {@inheritDoc} + */ + public PropertyId resolvePropertyPath(Path path) throws RepositoryException { + return (PropertyId) resolvePath(path, RETURN_PROPERTY); + } + + /** + * {@inheritDoc} + */ + public Path getPath(ItemId id) + throws ItemNotFoundException, RepositoryException { + // shortcut + if (id.equals(rootNodeId)) { + return PathFactoryImpl.getInstance().getRootPath(); + } + + PathBuilder builder = new PathBuilder(); + + try { + buildPath(builder, getItemState(id), new CycleDetector()); + return builder.getPath(); + } catch (NoSuchItemStateException nsise) { + String msg = "failed to build path of " + id; + log.debug(msg); + throw new ItemNotFoundException(msg, nsise); + } catch (ItemStateException ise) { + String msg = "failed to build path of " + id; + log.debug(msg); + throw new RepositoryException(msg, ise); + } catch (MalformedPathException mpe) { + String msg = "failed to build path of " + id; + log.debug(msg); + throw new RepositoryException(msg, mpe); + } + } + + /** + * {@inheritDoc} + */ + public Name getName(ItemId itemId) + throws ItemNotFoundException, RepositoryException { + if (itemId.denotesNode()) { + NodeId nodeId = (NodeId) itemId; + try { + NodeState nodeState = (NodeState) getItemState(nodeId); + NodeId parentId = getParentId(nodeState); + if (parentId == null) { + // this is the root or an orphaned node + // FIXME + return EMPTY_NAME; + } + return getName(nodeId, parentId); + } catch (NoSuchItemStateException nsis) { + String msg = "failed to resolve name of " + nodeId; + log.debug(msg); + throw new ItemNotFoundException(nodeId.toString()); + } catch (ItemStateException ise) { + String msg = "failed to resolve name of " + nodeId; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } else { + return ((PropertyId) itemId).getName(); + } + } + + /** + * {@inheritDoc} + */ + public Name getName(NodeId id, NodeId parentId) + throws ItemNotFoundException, RepositoryException { + + NodeState parentState; + + try { + parentState = (NodeState) getItemState(parentId); + } catch (NoSuchItemStateException nsis) { + String msg = "failed to resolve name of " + id; + log.debug(msg); + throw new ItemNotFoundException(id.toString()); + } catch (ItemStateException ise) { + String msg = "failed to resolve name of " + id; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + + ChildNodeEntry entry = + getChildNodeEntry(parentState, id); + if (entry == null) { + String msg = "failed to resolve name of " + id; + log.debug(msg); + throw new ItemNotFoundException(msg); + } + return entry.getName(); + } + + /** + * {@inheritDoc} + */ + public int getDepth(ItemId id) + throws ItemNotFoundException, RepositoryException { + // shortcut + if (id.equals(rootNodeId)) { + return 0; + } + try { + ItemState state = getItemState(id); + NodeId parentId = getParentId(state); + int depth = 0; + while (parentId != null) { + depth++; + state = getItemState(parentId); + parentId = getParentId(state); + } + return depth; + } catch (NoSuchItemStateException nsise) { + String msg = "failed to determine depth of " + id; + log.debug(msg); + throw new ItemNotFoundException(msg, nsise); + } catch (ItemStateException ise) { + String msg = "failed to determine depth of " + id; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + /** + * {@inheritDoc} + */ + public int getRelativeDepth(NodeId ancestorId, ItemId descendantId) + throws ItemNotFoundException, RepositoryException { + if (ancestorId.equals(descendantId)) { + return 0; + } + int depth = 1; + try { + ItemState state = getItemState(descendantId); + NodeId parentId = getParentId(state); + while (parentId != null) { + if (parentId.equals(ancestorId)) { + return depth; + } + depth++; + state = getItemState(parentId); + parentId = getParentId(state); + } + // not an ancestor + return -1; + } catch (NoSuchItemStateException nsise) { + String msg = "failed to determine depth of " + descendantId + + " relative to " + ancestorId; + log.debug(msg); + throw new ItemNotFoundException(msg, nsise); + } catch (ItemStateException ise) { + String msg = "failed to determine depth of " + descendantId + + " relative to " + ancestorId; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + /** + * {@inheritDoc} + */ + public boolean isAncestor(NodeId nodeId, ItemId itemId) + throws ItemNotFoundException, RepositoryException { + if (nodeId.equals(itemId)) { + // can't be ancestor of self + return false; + } + try { + ItemState state = getItemState(itemId); + NodeId parentId = getParentId(state); + while (parentId != null) { + if (parentId.equals(nodeId)) { + return true; + } + state = getItemState(parentId); + parentId = getParentId(state); + } + // not an ancestor + return false; + } catch (NoSuchItemStateException nsise) { + String msg = "failed to determine degree of relationship of " + + nodeId + " and " + itemId; + log.debug(msg); + throw new ItemNotFoundException(msg, nsise); + } catch (ItemStateException ise) { + String msg = "failed to determine degree of relationship of " + + nodeId + " and " + itemId; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + /** + * {@inheritDoc} + */ + public boolean isShareAncestor(NodeId ancestor, NodeId descendant) + throws ItemNotFoundException, RepositoryException { + if (ancestor.equals(descendant)) { + // can't be ancestor of self + return false; + } + try { + ItemState state = getItemState(descendant); + Set parentIds = getParentIds(state, false); + while (parentIds.size() > 0) { + if (parentIds.contains(ancestor)) { + return true; + } + Set grandparentIds = new LinkedHashSet(); + for (NodeId parentId : parentIds) { + grandparentIds.addAll(getParentIds(getItemState(parentId), false)); + } + parentIds = grandparentIds; + } + // not an ancestor + return false; + } catch (NoSuchItemStateException nsise) { + String msg = "failed to determine degree of relationship of " + + ancestor + " and " + descendant; + log.debug(msg); + throw new ItemNotFoundException(msg, nsise); + } catch (ItemStateException ise) { + String msg = "failed to determine degree of relationship of " + + ancestor + " and " + descendant; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + /** + * {@inheritDoc} + */ + public int getShareRelativeDepth(NodeId ancestor, ItemId descendant) + throws ItemNotFoundException, RepositoryException { + + if (ancestor.equals(descendant)) { + return 0; + } + int depth = 1; + try { + ItemState state = getItemState(descendant); + Set parentIds = getParentIds(state, true); + while (parentIds.size() > 0) { + if (parentIds.contains(ancestor)) { + return depth; + } + depth++; + Set grandparentIds = new LinkedHashSet(); + for (NodeId parentId : parentIds) { + state = getItemState(parentId); + grandparentIds.addAll(getParentIds(state, true)); + } + parentIds = grandparentIds; + } + // not an ancestor + return -1; + } catch (NoSuchItemStateException nsise) { + String msg = "failed to determine degree of relationship of " + + ancestor + " and " + descendant; + log.debug(msg); + throw new ItemNotFoundException(msg, nsise); + } catch (ItemStateException ise) { + String msg = "failed to determine degree of relationship of " + + ancestor + " and " + descendant; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + /** + * Utility class used to detect path cycles with as little overhead + * as possible. The {@link #checkCycle(ItemId)} method is called for + * each path element as the + * {@link HierarchyManagerImpl#buildPath(PathBuilder, ItemState, CycleDetector)} + * method walks up the hierarchy. At first, during the first fifteen + * path elements, the detector does nothing in order to avoid + * introducing any unnecessary overhead to normal paths that seldom + * are deeper than that. After that initial threshold all item + * identifiers along the path are tracked, and a cycle is reported + * if an identifier is encountered that already occurred along the + * same path. + */ + protected static class CycleDetector { + + private int count = 0; + + private Set ids; + + boolean checkCycle(ItemId id) throws InvalidItemStateException { + if (count++ >= 15) { + if (ids == null) { + ids = new HashSet(); + } else { + return !ids.add(id); + } + } + return false; + } + + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemData.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemData.java new file mode 100644 index 00000000000..e9815eaf6b1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemData.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.core; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; + +/** + * Data object referenced by different ItemImpl instances that + * all represent the same item, i.e. items having the same ItemId. + */ +public abstract class ItemData { + + /** Associated item id */ + private final ItemId id; + + /** Associated item state */ + private ItemState state; + + /** Associated item definition */ + private ItemDefinition definition; + + /** Status */ + private int status; + + /** The item manager */ + private ItemManager itemMgr; + + /** + * Create a new instance of this class. + * + * @param state item state + * @param itemMgr item manager + */ + protected ItemData(ItemState state, ItemManager itemMgr) { + this.id = state.getId(); + this.state = state; + this.itemMgr = itemMgr; + this.status = ItemImpl.STATUS_NORMAL; + } + + /** + * Create a new instance of this class. + * + * @param id item id + */ + protected ItemData(ItemId id) { + this.id = id; + this.status = ItemImpl.STATUS_NORMAL; + } + + /** + * Return the associated item state. + * + * @return item state + */ + public ItemState getState() { + return state; + } + + /** + * Set the associated item state. + * + * @param state item state + */ + protected void setState(ItemState state) { + this.state = state; + } + + /** + * Return the associated item definition. + * + * @return item definition + * @throws RepositoryException if the definition cannot be retrieved. + */ + public ItemDefinition getDefinition() throws RepositoryException { + if (definition == null && itemMgr != null) { + if (isNode()) { + definition = itemMgr.getDefinition((NodeState) state); + } else { + definition = itemMgr.getDefinition((PropertyState) state); + } + } + return definition; + } + + /** + * Set the associated item definition. + * + * @param definition item definition + */ + protected void setDefinition(ItemDefinition definition) { + this.definition = definition; + } + + /** + * Return the status. + * + * @return status + */ + public int getStatus() { + return status; + } + + /** + * Set the status. + * + * @param status + */ + protected void setStatus(int status) { + this.status = status; + } + + /** + * Return a flag indicating whether item is a node. + * + * @return true if this item is a node; + * false otherwise. + */ + public boolean isNode() { + return false; + } + + /** + * Return the id associated with this item. + * + * @return item id + */ + public ItemId getId() { + return id; + } + + /** + * Return the parent id of this item. + * + * @return parent id + */ + public NodeId getParentId() { + return getState().getParentId(); + } + + /** + * {@inheritDoc} + */ + public String toString() { + return getId().toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemImpl.java new file mode 100644 index 00000000000..858cf3db2ed --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemImpl.java @@ -0,0 +1,451 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.ItemVisitor; +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 org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.SessionItemStateManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.value.ValueHelper; + +/** + * ItemImpl implements the Item interface. + */ +public abstract class ItemImpl implements Item { + + protected static final int STATUS_NORMAL = 0; + protected static final int STATUS_MODIFIED = 1; + protected static final int STATUS_DESTROYED = 2; + protected static final int STATUS_INVALIDATED = 3; + + protected final ItemId id; + + /** + * The component context of the session to which this item is associated. + */ + protected final SessionContext sessionContext; + + /** + * Item data associated with this item. + */ + protected final ItemData data; + + /** + * ItemManager that created this Item + */ + protected final ItemManager itemMgr; + + /** + * SessionItemStateManager associated with this Item + */ + protected final SessionItemStateManager stateMgr; + + /** + * Package private constructor. + * + * @param itemMgr the ItemManager that created this Item + * @param sessionContext the component context of the associated session + * @param data ItemData of this Item + */ + ItemImpl(ItemManager itemMgr, SessionContext sessionContext, ItemData data) { + this.sessionContext = sessionContext; + this.stateMgr = sessionContext.getItemStateManager(); + this.id = data.getId(); + this.itemMgr = itemMgr; + this.data = data; + } + + protected T perform(final SessionOperation operation) + throws RepositoryException { + itemSanityCheck(); + return sessionContext.getSessionState().perform(operation); + } + + /** + * Performs a sanity check on this item and the associated session. + * + * @throws RepositoryException if this item has been rendered invalid for some reason + */ + protected void sanityCheck() throws RepositoryException { + // check session status + sessionContext.getSessionState().checkAlive(); + + // check status of this item for read operation + itemSanityCheck(); + } + + /** + * Checks the status of this item. + * + * @throws RepositoryException if this item no longer exists + */ + protected void itemSanityCheck() throws RepositoryException { + // check status of this item for read operation + final int status = data.getStatus(); + if (status == STATUS_DESTROYED || status == STATUS_INVALIDATED) { + throw new InvalidItemStateException( + "Item does not exist anymore: " + id); + } + } + + protected boolean isTransient() { + return getItemState().isTransient(); + } + + protected abstract ItemState getOrCreateTransientItemState() throws RepositoryException; + + protected abstract void makePersistent() throws RepositoryException; + + /** + * Marks this instance as 'removed' and notifies its listeners. + * The resulting state is either 'temporarily invalidated' or + * 'permanently invalidated', depending on the initial state. + * + * @throws RepositoryException if an error occurs + */ + protected void setRemoved() throws RepositoryException { + final int status = data.getStatus(); + if (status == STATUS_INVALIDATED || status == STATUS_DESTROYED) { + // this instance is already 'invalid', get outta here + return; + } + + ItemState transientState = getOrCreateTransientItemState(); + if (transientState.getStatus() == ItemState.STATUS_NEW) { + // this is a 'new' item, simply dispose the transient state + // (it is no longer used); this will indirectly (through + // stateDiscarded listener method) invalidate this instance permanently + stateMgr.disposeTransientItemState(transientState); + } else { + // this is an 'existing' item (i.e. it is backed by persistent + // state), mark it as 'removed' + transientState.setStatus(ItemState.STATUS_EXISTING_REMOVED); + // transfer the transient state to the attic + stateMgr.moveTransientItemStateToAttic(transientState); + + // set state of this instance to 'invalid' + data.setStatus(STATUS_INVALIDATED); + // notify the manager that this instance has been + // temporarily invalidated + itemMgr.itemInvalidated(id, data); + } + } + + /** + * Returns the item-state associated with this Item. + * + * @return state associated with this Item + */ + ItemState getItemState() { + return data.getState(); + } + + /** + * Return the id of this Item. + * + * @return the id of this Item + */ + public ItemId getId() { + return id; + } + + /** + * Returns the primary path to this Item. + * + * @return the primary path to this Item + */ + public Path getPrimaryPath() throws RepositoryException { + return sessionContext.getHierarchyManager().getPath(id); + } + + /** + * Failsafe mapping of internal id to JCR path for use in + * diagnostic output, error messages etc. + * + * @return JCR path or some fallback value + */ + public String safeGetJCRPath() { + return itemMgr.safeGetJCRPath(id); + } + + /** + * Same as {@link Item#getName()} except that + * this method returns a Name instead of a + * String. + * + * @return the name of this item as Name + * @throws RepositoryException if an error occurs. + */ + public abstract Name getQName() throws RepositoryException; + + /** + * Utility method that converts the given string into a qualified JCR name. + * + * @param name name string + * @return qualified name + * @throws RepositoryException if the given name is invalid + */ + protected Name getQName(String name) throws RepositoryException { + return sessionContext.getQName(name); + } + + /** + * Utility method that returns the value factory of this session. + * + * @return value factory + * @throws RepositoryException if the value factory is not available + */ + protected ValueFactory getValueFactory() throws RepositoryException { + return getSession().getValueFactory(); + } + + /** + * Utility method that converts the given strings into JCR values of the + * given type + * + * @param values value strings + * @param type value type + * @return JCR values + * @throws RepositoryException if the values can not be converted + */ + protected Value[] getValues(String[] values, int type) + throws RepositoryException { + if (values != null) { + return ValueHelper.convert(values, type, getValueFactory()); + } else { + return null; + } + } + + /** + * Utility method that returns the type of the first of the given values, + * or {@link PropertyType#UNDEFINED} when given no values. + * + * @param values given values, or null + * @return value type, or {@link PropertyType#UNDEFINED} + */ + protected int getType(Value[] values) { + if (values != null) { + for (Value value : values) { + if (value != null) { + return value.getType(); + } + } + } + return PropertyType.UNDEFINED; + } + + //-----------------------------------------------------------------< Item > + + /** + * {@inheritDoc} + */ + public abstract void accept(ItemVisitor visitor) + throws RepositoryException; + + /** + * {@inheritDoc} + */ + public abstract boolean isNode(); + + /** + * {@inheritDoc} + */ + public abstract String getName() throws RepositoryException; + + /** + * {@inheritDoc} + */ + public abstract Node getParent() + throws ItemNotFoundException, AccessDeniedException, RepositoryException; + + /** + * {@inheritDoc} + */ + public boolean isNew() { + final ItemState state = getItemState(); + return state.isTransient() && state.getOverlayedState() == null; + } + + /** + * checks if this item is new. running outside of transactions, this + * is the same as {@link #isNew()} but within a transaction an item can + * be saved but not yet persisted. + */ + protected boolean isTransactionalNew() { + final ItemState state = getItemState(); + return state.getStatus() == ItemState.STATUS_NEW; + } + + /** + * {@inheritDoc} + */ + public boolean isModified() { + final ItemState state = getItemState(); + return state.isTransient() && state.getOverlayedState() != null; + } + + /** + * {@inheritDoc} + */ + public void remove() throws RepositoryException { + perform(new ItemRemoveOperation(this, true)); + } + + /** + * {@inheritDoc} + */ + public void save() throws RepositoryException { + perform(new ItemSaveOperation(getItemState())); + } + + /** + * {@inheritDoc} + */ + public void refresh(boolean keepChanges) throws RepositoryException { + perform(new ItemRefreshOperation(getItemState(), keepChanges)); + } + + /** + * {@inheritDoc} + */ + public Item getAncestor(final int degree) throws RepositoryException { + return perform(new SessionOperation() { + public Item perform(SessionContext context) + throws RepositoryException { + if (degree == 0) { + return context.getItemManager().getRootNode(); + } + + try { + // Path.getAncestor requires relative degree, i.e. we need + // to convert absolute to relative ancestor degree + Path path = getPrimaryPath(); + int relDegree = path.getAncestorCount() - degree; + if (relDegree < 0) { + throw new ItemNotFoundException(); + } else if (relDegree == 0) { + return ItemImpl.this; // shortcut + } + Path ancestorPath = path.getAncestor(relDegree); + return context.getItemManager().getNode(ancestorPath); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException("Ancestor not found", e); + } + } + public String toString() { + return "item.getAncestor(" + degree + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public String getPath() throws RepositoryException { + return perform(new SessionOperation() { + public String perform(SessionContext context) + throws RepositoryException { + return context.getJCRPath(getPrimaryPath()); + } + public String toString() { + return "item.getPath()"; + } + }); + } + + /** + * {@inheritDoc} + */ + public int getDepth() throws RepositoryException { + return perform(new SessionOperation() { + public Integer perform(SessionContext context) + throws RepositoryException { + ItemState state = getItemState(); + if (state.getParentId() == null) { + return 0; // shortcut + } else { + return context.getHierarchyManager().getDepth(id); + } + } + public String toString() { + return "item.getDepth()"; + } + }); + } + + /** + * Returns the session associated with this item. + *

    + * Since Jackrabbit 1.4 it is safe to use this method regardless + * of item state. + * + * @see Issue JCR-911 + * @return current session + */ + public Session getSession() { + return sessionContext.getSessionImpl(); + } + + /** + * {@inheritDoc} + */ + public boolean isSame(Item otherItem) throws RepositoryException { + // check state of this instance + sanityCheck(); + + if (this == otherItem) { + return true; + } + if (otherItem instanceof ItemImpl) { + ItemImpl other = (ItemImpl) otherItem; + return id.equals(other.id) + && getSession().getWorkspace().getName().equals( + other.getSession().getWorkspace().getName()); + } + return false; + } + + //--------------------------------------------------------------< Object > + + /** + * Returns the({@link #safeGetJCRPath() safe}) path of this item for use + * in diagnostic output. + * + * @return "/path/to/item" + */ + public String toString() { + return safeGetJCRPath(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemLifeCycleListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemLifeCycleListener.java new file mode 100644 index 00000000000..9954ed3ef94 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemLifeCycleListener.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.core; + +import org.apache.jackrabbit.core.id.ItemId; + +/** + * The ItemLifeCycleListener interface allows an implementing + * object to be informed about changes on an Item instance. + */ +public interface ItemLifeCycleListener { + + /** + * Called when an ItemImpl instance has been created. + * + * @param item the instance which has been created + */ + void itemCreated(ItemImpl item); + + /** + * Called when an ItemImpl instance has been invalidated + * (i.e. it has been temporarily rendered 'invalid'). + *

    + * Note that most {@link javax.jcr.Item}, + * {@link javax.jcr.Node} and {@link javax.jcr.Property} + * methods will throw an InvalidItemStateException when called + * on an 'invalidated' item. + * + * @param id the id of the instance that has been discarded + * @param item the instance which has been discarded + */ + void itemInvalidated(ItemId id, ItemImpl item); + + /** + * Called when an ItemImpl instance has been destroyed + * (i.e. it has been permanently rendered 'invalid'). + *

    + * Note that most {@link javax.jcr.Item}, + * {@link javax.jcr.Node} and {@link javax.jcr.Property} + * methods will throw an InvalidItemStateException when called + * on a 'destroyed' item. + * + * @param id the id of the instance that has been destroyed + * @param item the instance which has been destroyed + */ + void itemDestroyed(ItemId id, ItemImpl item); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemManager.java new file mode 100644 index 00000000000..b226aa43840 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemManager.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.core; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NamespaceException; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateListener; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.SessionItemStateManager; +import org.apache.jackrabbit.core.version.VersionHistoryImpl; +import org.apache.jackrabbit.core.version.VersionImpl; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * There's one ItemManager instance per Session + * instance. It is the factory for Node and Property + * instances. + *

    + * The ItemManager's responsibilities are: + *

      + *
    • providing access to Item instances by ItemId + * whereas Node and Item are only providing relative access. + *
    • returning the instance of an existing Node or Property, + * given its absolute path. + *
    • creating the per-session instance of a Node + * or Property that doesn't exist yet and needs to be created first. + *
    • guaranteeing that there aren't multiple instances representing the same + * Node or Property associated with the same + * Session instance. + *
    • maintaining a cache of the item instances it created. + *
    • respecting access rights of associated Session in all methods. + *
    + *

    + * If the parent Session is an XASession, there is + * one ItemManager instance per started global transaction. + */ +public class ItemManager implements ItemStateListener { + + private static Logger log = LoggerFactory.getLogger(ItemManager.class); + + private final org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl rootNodeDef; + + /** + * Component context of the associated session. + */ + protected final SessionContext sessionContext; + + protected final SessionImpl session; + + private final SessionItemStateManager sism; + private final HierarchyManager hierMgr; + + /** + * A cache for item instances created by this ItemManager + */ + private final Map itemCache; + + /** + * Shareable node cache. + */ + private final ShareableNodesCache shareableNodesCache; + + /** + * Creates a new per-session instance ItemManager instance. + * + * @param sessionContext component context of the associated session + */ + @SuppressWarnings("unchecked") + protected ItemManager(SessionContext sessionContext) { + this.sism = sessionContext.getItemStateManager(); + this.hierMgr = sessionContext.getHierarchyManager(); + this.sessionContext = sessionContext; + this.session = sessionContext.getSessionImpl(); + this.rootNodeDef = sessionContext.getNodeTypeManager().getRootNodeDefinition(); + + // setup item cache with weak references to items + itemCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK); + + // setup shareable nodes cache + shareableNodesCache = new ShareableNodesCache(); + } + + /** + * Checks that this session is alive. + * + * @throws RepositoryException if the session has been closed + */ + private void sanityCheck() throws RepositoryException { + sessionContext.getSessionState().checkAlive(); + } + + /** + * Disposes this ItemManager and frees resources. + */ + void dispose() { + synchronized (itemCache) { + itemCache.clear(); + } + shareableNodesCache.clear(); + } + + NodeDefinitionImpl getDefinition(NodeState state) + throws RepositoryException { + if (state.getId().equals(sessionContext.getRootNodeId())) { + // special handling required for root node + return rootNodeDef; + } + + NodeId parentId = state.getParentId(); + if (parentId == null) { + // removed state has parentId set to null + // get from overlayed state + ItemState overlaid = state.getOverlayedState(); + if (overlaid != null) { + parentId = overlaid.getParentId(); + } else { + throw new InvalidItemStateException( + "Could not find parent of node " + state.getNodeId()); + } + } + NodeState parentState = null; + try { + // access the parent state circumventing permission check, since + // read permission on the parent isn't required in order to retrieve + // a node's definition. see also JCR-2418 + ItemData parentData = getItemData(parentId, null, false); + parentState = (NodeState) parentData.getState(); + if (state.getParentId() == null) { + // indicates state has been removed, must use + // overlayed state of parent, otherwise child node entry + // cannot be found. unless the parentState is new, which + // means it was recreated in place of a removed node + // that used to be the actual parent + if (parentState.getStatus() == ItemState.STATUS_NEW) { + // force getting parent from attic + parentState = null; + } else { + parentState = (NodeState) parentState.getOverlayedState(); + } + } + } catch (ItemNotFoundException e) { + // parent probably removed, get it from attic. see below + } + + if (parentState == null) { + try { + // use overlayed state if available + parentState = (NodeState) sism.getAttic().getItemState( + parentId).getOverlayedState(); + } catch (ItemStateException ex) { + throw new RepositoryException(ex); + } + } + + // get child node entry + ChildNodeEntry cne = parentState.getChildNodeEntry(state.getNodeId()); + if (cne == null) { + throw new InvalidItemStateException( + "Could not find child " + state.getNodeId() + + " of node " + parentState.getNodeId()); + } + + NodeTypeRegistry ntReg = sessionContext.getNodeTypeRegistry(); + try { + EffectiveNodeType ent = ntReg.getEffectiveNodeType( + parentState.getNodeTypeName(), parentState.getMixinTypeNames()); + QNodeDefinition def; + try { + def = ent.getApplicableChildNodeDef( + cne.getName(), state.getNodeTypeName(), ntReg); + } catch (ConstraintViolationException e) { + // fallback to child node definition of a nt:unstructured + ent = ntReg.getEffectiveNodeType(NameConstants.NT_UNSTRUCTURED); + def = ent.getApplicableChildNodeDef( + cne.getName(), state.getNodeTypeName(), ntReg); + log.warn("Fallback to nt:unstructured due to unknown child " + + "node definition for type '" + state.getNodeTypeName() + "'"); + } + return sessionContext.getNodeTypeManager().getNodeDefinition(def); + } catch (NodeTypeConflictException e) { + throw new RepositoryException(e); + } + } + + PropertyDefinitionImpl getDefinition(PropertyState state) + throws RepositoryException { + // this is a bit ugly + // there might be cases where otherwise protected items turn into + // non-protected items because a mixin has been removed from the parent + // node state. + // see also: JCR-2408 + if (state.getStatus() == ItemState.STATUS_EXISTING_REMOVED + && state.getName().equals(NameConstants.JCR_UUID)) { + NodeTypeRegistry ntReg = sessionContext.getNodeTypeRegistry(); + QPropertyDefinition def = ntReg.getEffectiveNodeType( + NameConstants.MIX_REFERENCEABLE).getApplicablePropertyDef( + state.getName(), state.getType()); + return sessionContext.getNodeTypeManager().getPropertyDefinition(def); + } + try { + // retrieve parent in 2 steps in order to avoid the check for + // read permissions on the parent which isn't required in order + // to read the property's definition. see also JCR-2418. + ItemData parentData = getItemData(state.getParentId(), null, false); + NodeImpl parent = (NodeImpl) createItemInstance(parentData); + return parent.getApplicablePropertyDefinition( + state.getName(), state.getType(), state.isMultiValued(), true); + } catch (ItemNotFoundException e) { + // parent probably removed, get it from attic + } + try { + NodeState parent = (NodeState) sism.getAttic().getItemState( + state.getParentId()).getOverlayedState(); + NodeTypeRegistry ntReg = sessionContext.getNodeTypeRegistry(); + EffectiveNodeType ent = ntReg.getEffectiveNodeType( + parent.getNodeTypeName(), parent.getMixinTypeNames()); + QPropertyDefinition def; + try { + def = ent.getApplicablePropertyDef( + state.getName(), state.getType(), state.isMultiValued()); + } catch (ConstraintViolationException e) { + ent = ntReg.getEffectiveNodeType(NameConstants.NT_UNSTRUCTURED); + def = ent.getApplicablePropertyDef(state.getName(), + state.getType(), state.isMultiValued()); + log.warn("Fallback to nt:unstructured due to unknown property " + + "definition for '" + state.getName() + "'"); + } + return sessionContext.getNodeTypeManager().getPropertyDefinition(def); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } catch (NodeTypeConflictException e) { + throw new RepositoryException(e); + } + } + + /** + * Common implementation for all variants of item/node/propertyExists + * with both itemId or path param. + * + * @param itemId The id of the item to test. + * @param path Path of the item to check if known or null. In + * the latter case the test for access permission is executed using the + * itemId. + * @return true if the item with the given itemId exists AND + * can be read by this session. + */ + private boolean itemExists(ItemId itemId, Path path) { + try { + sanityCheck(); + + // shortcut: check if state exists for the given item + if (!sism.hasItemState(itemId)) { + return false; + } + getItemData(itemId, path, true); + return true; + } catch (RepositoryException re) { + return false; + } + } + + /** + * Common implementation for all variants of getItem/getNode/getProperty + * with both itemId or path parameter. + * + * @param itemId + * @param path Path of the item to retrieve or null. In + * the latter case the test for access permission is executed using the + * itemId. + * @param permissionCheck + * @return The item identified by the given itemId. + * @throws ItemNotFoundException + * @throws AccessDeniedException + * @throws RepositoryException + */ + private ItemImpl getItem(ItemId itemId, Path path, boolean permissionCheck) throws ItemNotFoundException, AccessDeniedException, RepositoryException { + sanityCheck(); + + ItemData data = getItemData(itemId, path, permissionCheck); + return createItemInstance(data); + } + + /** + * Retrieves the data of the item with given id. If the + * specified item doesn't exist an ItemNotFoundException will + * be thrown. + * If the item exists but the current session is not granted read access an + * AccessDeniedException will be thrown. + * + * @param itemId id of item to be retrieved + * @return state state of said item + * @throws ItemNotFoundException if no item with given id exists + * @throws AccessDeniedException if the current session is not allowed to + * read the said item + * @throws RepositoryException if another error occurs + */ + private ItemData getItemData(ItemId itemId) + throws ItemNotFoundException, AccessDeniedException, + RepositoryException { + return getItemData(itemId, null, true); + } + + /** + * Retrieves the data of the item with given id. If the + * specified item doesn't exist an ItemNotFoundException will + * be thrown. + * If permissionCheck is true and the item exists + * but the current session is not granted read access an + * AccessDeniedException will be thrown. + * + * @param itemId id of item to be retrieved + * @param path The path of the item to retrieve the data for or + * null. In the latter case the id (instead of the path) is + * used to test if READ permission is granted. + * @param permissionCheck + * @return the ItemData for the item identified by the given itemId. + * @throws ItemNotFoundException if no item with given id exists + * @throws AccessDeniedException if the current session is not allowed to + * read the said item + * @throws RepositoryException if another error occurs + */ + ItemData getItemData(ItemId itemId, Path path, boolean permissionCheck) + throws ItemNotFoundException, AccessDeniedException, + RepositoryException { + ItemData data = retrieveItem(itemId); + if (data == null) { + // not yet in cache, need to create instance: + // - retrieve item state + // - create instance of item data + // NOTE: permission check & caching within createItemData + ItemState state; + try { + state = sism.getItemState(itemId); + } catch (NoSuchItemStateException nsise) { + throw new ItemNotFoundException(itemId.toString(), nsise); + } catch (ItemStateException ise) { + String msg = "failed to retrieve item state of item " + itemId; + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + // create item data including: perm check and caching. + data = createItemData(state, path, permissionCheck); + } else { + // already cached: if 'permissionCheck' is true, make sure read + // permission is granted. + if (permissionCheck && !canRead(data, path)) { + // item exists but read-perm has been revoked in the mean time. + // -> remove from cache + evictItems(itemId); + throw new AccessDeniedException("cannot read item " + data.getId()); + } + } + return data; + } + + /** + * @param data + * @param path Path to be used for the permission check or null + * in which case the itemId present with the specified data is used. + * @return true if the item with the given data can be read; + * false otherwise. + * @throws RepositoryException + */ + private boolean canRead(ItemData data, Path path) throws RepositoryException { + // JCR-1601: cached item may just have been invalidated + ItemState state = data.getState(); + if (state == null) { + throw new InvalidItemStateException(data.getId() + ": the item does not exist anymore"); + } + if (state.getStatus() == ItemState.STATUS_NEW) { + if (!data.getDefinition().isProtected()) { + /* + NEW items can always be read as long they have been added through + the API and NOT by the system (i.e. protected items). + */ + return true; + } else { + /* + NEW protected (system) item: + need use the path to evaluate the effective permissions. + */ + return (path == null) ? + sessionContext.getAccessManager().isGranted(data.getId(), AccessManager.READ) : + sessionContext.getAccessManager().isGranted(path, Permission.READ); + } + } else { + /* item is not NEW -> save to call acMgr.canRead(Path,ItemId) */ + return sessionContext.getAccessManager().canRead(path, data.getId()); + } + } + + /** + * @param parent The item data of the parent node. + * @param childId + * @return true if the item with the given childId can be read; + * false otherwise. + * @throws RepositoryException + */ + private boolean canRead(ItemData parent, ItemId childId) throws RepositoryException { + if (parent.getStatus() == ItemState.STATUS_EXISTING) { + // child item is for sure not NEW (because then the parent was modified). + // safe to use AccessManager#canRead(Path, ItemId). + return sessionContext.getAccessManager().canRead(null, childId); + } else { + // child could be NEW -> don't use AccessManager#canRead(Path, ItemId) + return sessionContext.getAccessManager().isGranted(childId, AccessManager.READ); + } + } + + //--------------------------------------------------< item access methods > + /** + * Checks whether an item exists at the specified path. + * + * @deprecated As of JSR 283, a Path doesn't anymore uniquely + * identify an Item, therefore {@link #nodeExists(Path)} and + * {@link #propertyExists(Path)} should be used instead. + * + * @param path path to the item to be checked + * @return true if the specified item exists + */ + public boolean itemExists(Path path) { + try { + sanityCheck(); + + ItemId id = hierMgr.resolvePath(path); + return (id != null) && itemExists(id, path); + } catch (RepositoryException re) { + return false; + } + } + + /** + * Checks whether a node exists at the specified path. + * + * @param path path to the node to be checked + * @return true if a node exists at the specified path + */ + public boolean nodeExists(Path path) { + try { + sanityCheck(); + + NodeId id = hierMgr.resolveNodePath(path); + return (id != null) && itemExists(id, path); + } catch (RepositoryException re) { + return false; + } + } + + /** + * Checks whether a property exists at the specified path. + * + * @param path path to the property to be checked + * @return true if a property exists at the specified path + */ + public boolean propertyExists(Path path) { + try { + sanityCheck(); + + PropertyId id = hierMgr.resolvePropertyPath(path); + return (id != null) && itemExists(id, path); + } catch (RepositoryException re) { + return false; + } + } + + /** + * Checks if the item with the given id exists. + * + * @param id id of the item to be checked + * @return true if the specified item exists + */ + public boolean itemExists(ItemId id) { + return itemExists(id, null); + } + + /** + * @return + * @throws RepositoryException + */ + NodeImpl getRootNode() throws RepositoryException { + return (NodeImpl) getItem(sessionContext.getRootNodeId()); + } + + /** + * Returns the node at the specified absolute path in the workspace. + * If no such node exists, then it returns the property at the specified path. + * If no such property exists a PathNotFoundException is thrown. + * + * @deprecated As of JSR 283, a Path doesn't anymore uniquely + * identify an Item, therefore {@link #getNode(Path)} and + * {@link #getProperty(Path)} should be used instead. + * @param path + * @return + * @throws PathNotFoundException + * @throws AccessDeniedException + * @throws RepositoryException + */ + public ItemImpl getItem(Path path) throws PathNotFoundException, + AccessDeniedException, RepositoryException { + ItemId id = hierMgr.resolvePath(path); + if (id == null) { + throw new PathNotFoundException(safeGetJCRPath(path)); + } + try { + ItemImpl item = getItem(id, path, true); + // Test, if this item is a shareable node. + if (item.isNode() && ((NodeImpl) item).isShareable()) { + return getNode(path); + } + return item; + } catch (ItemNotFoundException infe) { + throw new PathNotFoundException(safeGetJCRPath(path)); + } + } + + /** + * @param path + * @return + * @throws PathNotFoundException + * @throws AccessDeniedException + * @throws RepositoryException + */ + public NodeImpl getNode(Path path) throws PathNotFoundException, + AccessDeniedException, RepositoryException { + NodeId id = hierMgr.resolveNodePath(path); + if (id == null) { + throw new PathNotFoundException(safeGetJCRPath(path)); + } + NodeId parentId = null; + if (!path.denotesRoot()) { + parentId = hierMgr.resolveNodePath(path.getAncestor(1)); + } + try { + if (parentId == null) { + return (NodeImpl) getItem(id, path, true); + } + // if the node is shareable, it now returns the node with the right + // parent + return getNode(id, parentId); + } catch (ItemNotFoundException infe) { + throw new PathNotFoundException(safeGetJCRPath(path)); + } + } + + /** + * @param path + * @return + * @throws PathNotFoundException + * @throws AccessDeniedException + * @throws RepositoryException + */ + public PropertyImpl getProperty(Path path) + throws PathNotFoundException, AccessDeniedException, RepositoryException { + PropertyId id = hierMgr.resolvePropertyPath(path); + if (id == null) { + throw new PathNotFoundException(safeGetJCRPath(path)); + } + try { + return (PropertyImpl) getItem(id, path, true); + } catch (ItemNotFoundException infe) { + throw new PathNotFoundException(safeGetJCRPath(path)); + } + } + + /** + * @param id + * @return + * @throws RepositoryException + */ + public synchronized ItemImpl getItem(ItemId id) + throws ItemNotFoundException, AccessDeniedException, RepositoryException { + return getItem(id, null, true); + } + + /** + * @param id + * @return + * @throws RepositoryException + */ + synchronized ItemImpl getItem(ItemId id, boolean permissionCheck) + throws ItemNotFoundException, AccessDeniedException, RepositoryException { + return getItem(id, null, permissionCheck); + } + + /** + * Returns a node with a given id and parent id. If the indicated node is + * shareable, there might be multiple nodes associated with the same id, + * but there'is only one node with the given parent id. + * + * @param id node id + * @param parentId parent node id + * @return node + * @throws RepositoryException if an error occurs + */ + public synchronized NodeImpl getNode(NodeId id, NodeId parentId) + throws ItemNotFoundException, AccessDeniedException, RepositoryException { + return getNode(id, parentId, true); + } + + /** + * Returns a node with a given id and parent id. If the indicated node is + * shareable, there might be multiple nodes associated with the same id, + * but there'is only one node with the given parent id. + * + * @param id node id + * @param parentId parent node id + * @param permissionCheck Flag indicating if read permission must be check + * upon retrieving the node. + * @return node + * @throws RepositoryException if an error occurs + */ + synchronized NodeImpl getNode(NodeId id, NodeId parentId, boolean permissionCheck) + throws ItemNotFoundException, AccessDeniedException, RepositoryException { + if (parentId == null) { + return (NodeImpl) getItem(id); + } + AbstractNodeData data = retrieveItem(id, parentId); + if (data == null) { + data = (AbstractNodeData) getItemData(id, null, permissionCheck); + } else if (permissionCheck && !canRead(data, id)) { + // item exists but read-perm has been revoked in the mean time. + // -> remove from cache + evictItems(id); + throw new AccessDeniedException("cannot read item " + data.getId()); + } + if (!data.getParentId().equals(parentId)) { + // verify that parent actually appears in the shared set + if (!data.getNodeState().containsShare(parentId)) { + String msg = "Node with id '" + id + + "' does not have shared parent with id: " + parentId; + throw new ItemNotFoundException(msg); + } + // TODO: ev. need to check if read perm. is granted. + data = new NodeDataRef(data, parentId); + cacheItem(data); + } + return createNodeInstance(data); + } + + /** + * Create an item instance from an item state. This method creates a + * new ItemData instance without looking at the cache nor + * testing if the item can be read and returns a new item instance. + * + * @param state item state + * @return item instance + * @throws RepositoryException if an error occurs + */ + synchronized ItemImpl createItemInstance(ItemState state) + throws RepositoryException { + ItemData data = createItemData(state, null, false); + return createItemInstance(data); + } + + /** + * @param parentId + * @return + * @throws ItemNotFoundException + * @throws AccessDeniedException + * @throws RepositoryException + */ + synchronized boolean hasChildNodes(NodeId parentId) + throws ItemNotFoundException, AccessDeniedException, RepositoryException { + sanityCheck(); + + ItemData data = getItemData(parentId); + if (!data.isNode()) { + String msg = "can't list child nodes of property " + parentId; + log.debug(msg); + throw new RepositoryException(msg); + } + + NodeState state = (NodeState) data.getState(); + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + // make sure any of the properties can be read. + if (canRead(data, entry.getId())) { + return true; + } + } + return false; + } + + /** + * @param parentId + * @return + * @throws ItemNotFoundException + * @throws AccessDeniedException + * @throws RepositoryException + */ + synchronized NodeIterator getChildNodes(NodeId parentId) + throws ItemNotFoundException, AccessDeniedException, RepositoryException { + sanityCheck(); + + ItemData data = getItemData(parentId); + if (!data.isNode()) { + String msg = "can't list child nodes of property " + parentId; + log.debug(msg); + throw new RepositoryException(msg); + } + ArrayList childIds = new ArrayList(); + Iterator iter = ((NodeState) data.getState()).getChildNodeEntries().iterator(); + + while (iter.hasNext()) { + ChildNodeEntry entry = iter.next(); + // delay check for read-access until item is being built + // thus avoid duplicate check + childIds.add(entry.getId()); + } + + return new LazyItemIterator(sessionContext, childIds, parentId); + } + + /** + * @param parentId + * @return + * @throws ItemNotFoundException + * @throws AccessDeniedException + * @throws RepositoryException + */ + synchronized boolean hasChildProperties(NodeId parentId) + throws ItemNotFoundException, AccessDeniedException, RepositoryException { + sanityCheck(); + + ItemData data = getItemData(parentId); + if (!data.isNode()) { + String msg = "can't list child properties of property " + parentId; + log.debug(msg); + throw new RepositoryException(msg); + } + Iterator iter = ((NodeState) data.getState()).getPropertyNames().iterator(); + + while (iter.hasNext()) { + Name propName = iter.next(); + // make sure any of the properties can be read. + if (canRead(data, new PropertyId(parentId, propName))) { + return true; + } + } + + return false; + } + + /** + * @param parentId + * @return + * @throws ItemNotFoundException + * @throws AccessDeniedException + * @throws RepositoryException + */ + synchronized PropertyIterator getChildProperties(NodeId parentId) + throws ItemNotFoundException, AccessDeniedException, RepositoryException { + sanityCheck(); + + ItemData data = getItemData(parentId); + if (!data.isNode()) { + String msg = "can't list child properties of property " + parentId; + log.debug(msg); + throw new RepositoryException(msg); + } + ArrayList childIds = new ArrayList(); + Iterator iter = ((NodeState) data.getState()).getPropertyNames().iterator(); + + while (iter.hasNext()) { + Name propName = iter.next(); + PropertyId id = new PropertyId(parentId, propName); + // delay check for read-access until item is being built + // thus avoid duplicate check + childIds.add(id); + } + + return new LazyItemIterator(sessionContext, childIds); + } + + //-------------------------------------------------< item factory methods > + /** + * Builds the ItemData for the specified state. + * If permissionCheck is true, the access manager + * is used to determine if reading that item would be granted. If this is + * not the case an AccessDeniedException is thrown. + * Before returning the created ItemData it is put into the + * cache. In order to benefit from the cache + * {@link #getItemData(ItemId, Path, boolean)} should be called. + * + * @param state + * @return + * @throws RepositoryException + */ + private ItemData createItemData(ItemState state, Path path, boolean permissionCheck) throws RepositoryException { + ItemData data; + if (state.isNode()) { + NodeState nodeState = (NodeState) state; + data = new NodeData(nodeState, this); + } else { + PropertyState propertyState = (PropertyState) state; + data = new PropertyData(propertyState, this); + } + // make sure read-perm. is granted before returning the data. + if (permissionCheck && !canRead(data, path)) { + throw new AccessDeniedException("cannot read item " + state.getId()); + } + // before returning the data: put them into the cache. + cacheItem(data); + return data; + } + + private ItemImpl createItemInstance(ItemData data) { + if (data.isNode()) { + return createNodeInstance((AbstractNodeData) data); + } else { + return createPropertyInstance((PropertyData) data); + } + } + + private NodeImpl createNodeInstance(AbstractNodeData data) { + // check special nodes + final NodeState state = data.getNodeState(); + if (state.getNodeTypeName().equals(NameConstants.NT_VERSION)) { + return new VersionImpl(this, sessionContext, data); + } else if (state.getNodeTypeName().equals(NameConstants.NT_VERSIONHISTORY)) { + return new VersionHistoryImpl(this, sessionContext, data); + } else { + // create node object + return new NodeImpl(this, sessionContext, data); + } + } + + private PropertyImpl createPropertyInstance(PropertyData data) { + // check special nodes + return new PropertyImpl(this, sessionContext, data); + } + + //---------------------------------------------------< item cache methods > + + /** + * Returns an item reference from the cache. + * + * @param id id of the item that should be retrieved. + * @return the item reference stored in the corresponding cache entry + * or null if there's no corresponding cache entry. + */ + private ItemData retrieveItem(ItemId id) { + synchronized (itemCache) { + ItemData data = itemCache.get(id); + if (data == null && id.denotesNode()) { + data = shareableNodesCache.retrieveFirst((NodeId) id); + } + return data; + } + } + + /** + * Return a node from the cache. + * + * @param id id of the node that should be retrieved. + * @param parentId parent id of the node that should be retrieved + * @return reference stored in the corresponding cache entry + * or null if there's no corresponding cache entry. + */ + private AbstractNodeData retrieveItem(NodeId id, NodeId parentId) { + synchronized (itemCache) { + AbstractNodeData data = shareableNodesCache.retrieve(id, parentId); + if (data == null) { + data = (AbstractNodeData) itemCache.get(id); + } + return data; + } + } + + /** + * Puts the reference of an item in the cache with + * the item's path as the key. + * + * @param data the item data to cache + */ + private void cacheItem(ItemData data) { + synchronized (itemCache) { + if (data.isNode()) { + AbstractNodeData nd = (AbstractNodeData) data; + if (nd.getPrimaryParentId() != null) { + shareableNodesCache.cache(nd); + return; + } + } + ItemId id = data.getId(); + if (itemCache.containsKey(id)) { + log.debug("overwriting cached item " + id); + } + if (log.isDebugEnabled()) { + log.debug("caching item " + id); + } + itemCache.put(id, data); + } + } + + /** + * Removes all cache entries with the given item id. If the item is + * shareable, there might be more than one cache entry for this item. + * + * @param id id of the items to remove from the cache + */ + private void evictItems(ItemId id) { + if (log.isDebugEnabled()) { + log.debug("removing items " + id + " from cache"); + } + synchronized (itemCache) { + itemCache.remove(id); + if (id.denotesNode()) { + shareableNodesCache.evictAll((NodeId) id); + } + } + } + + /** + * Removes a cache entry for a specific item. + * + * @param data The item data to remove from the cache + */ + private void evictItem(ItemData data) { + if (log.isDebugEnabled()) { + log.debug("removing item " + data.getId() + " from cache"); + } + synchronized (itemCache) { + if (data.isNode()) { + shareableNodesCache.evict((AbstractNodeData) data); + } + ItemData cached = itemCache.get(data.getId()); + if (cached == data) { + itemCache.remove(data.getId()); + } + } + } + + + //-------------------------------------------------< misc. helper methods > + /** + * Failsafe conversion of internal Path to JCR path for use in + * error messages etc. + * + * @param path path to convert + * @return JCR path + */ + String safeGetJCRPath(Path path) { + try { + return session.getJCRPath(path); + } catch (NamespaceException e) { + log.error("failed to convert " + path.toString() + " to JCR path."); + // return string representation of internal path as a fallback + return path.toString(); + } + } + + /** + * Failsafe translation of internal ItemId to JCR path for use in + * error messages etc. + * + * @param id path to convert + * @return JCR path + */ + String safeGetJCRPath(ItemId id) { + try { + return safeGetJCRPath(hierMgr.getPath(id)); + } catch (RepositoryException re) { + log.error(id + ": failed to determine path to"); + // return string representation if id as a fallback + return id.toString(); + } + } + + //------------------------------------------------< ItemLifeCycleListener > + + /** + * {@inheritDoc} + */ + public void itemInvalidated(ItemId id, ItemData data) { + if (log.isDebugEnabled()) { + log.debug("invalidated item " + id); + } + evictItem(data); + } + + /** + * {@inheritDoc} + */ + public void itemDestroyed(ItemId id, ItemData data) { + if (log.isDebugEnabled()) { + log.debug("destroyed item " + id); + } + synchronized (itemCache) { + // remove instance from cache + evictItems(id); + } + } + + //--------------------------------------------------------------< Object > + /** + * {@inheritDoc} + */ + public synchronized String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ItemManager (" + super.toString() + ")\n"); + builder.append("Items in cache:\n"); + synchronized (itemCache) { + for (ItemId id : itemCache.keySet()) { + ItemData item = itemCache.get(id); + if (item.isNode()) { + builder.append("Node: "); + } else { + builder.append("Property: "); + } + if (item.getState().isTransient()) { + builder.append("transient "); + } else { + builder.append(" "); + } + builder.append(id + "\t" + safeGetJCRPath(id) + " (" + item + ")\n"); + } + } + return builder.toString(); + } + + //----------------------------------------------------< ItemStateListener > + + /** + * {@inheritDoc} + */ + public void stateCreated(ItemState created) { + ItemData data = retrieveItem(created.getId()); + if (data != null) { + data.setStatus(ItemImpl.STATUS_NORMAL); + } + } + + /** + * {@inheritDoc} + */ + public void stateModified(ItemState modified) { + ItemData data = retrieveItem(modified.getId()); + if (data != null && data.getState() == modified) { + data.setStatus(ItemImpl.STATUS_MODIFIED); + /* + if (modified.isNode()) { + NodeState state = (NodeState) modified; + if (state.isShareable()) { + //evictItem(modified.getId()); + NodeData nodeData = (NodeData) data; + NodeData shareSibling = new NodeData(nodeData, state.getParentId()); + shareableNodesCache.cache(shareSibling); + } + } + */ + } + } + + /** + * {@inheritDoc} + */ + public void stateDestroyed(ItemState destroyed) { + ItemData data = retrieveItem(destroyed.getId()); + if (data != null && data.getState() == destroyed) { + itemDestroyed(destroyed.getId(), data); + + data.setStatus(ItemImpl.STATUS_DESTROYED); + } + } + + /** + * {@inheritDoc} + */ + public void stateDiscarded(ItemState discarded) { + ItemData data = retrieveItem(discarded.getId()); + if (data != null && data.getState() == discarded) { + if (discarded.isTransient()) { + switch (discarded.getStatus()) { + /** + * persistent item that has been transiently removed + */ + case ItemState.STATUS_EXISTING_REMOVED: + case ItemState.STATUS_EXISTING_MODIFIED: + ItemState persistentState = discarded.getOverlayedState(); + // the state is a transient wrapper for the underlying + // persistent state, therefore restore the persistent state + // and resurrect this item instance if necessary + SessionItemStateManager stateMgr = + sessionContext.getItemStateManager(); + stateMgr.disconnectTransientItemState(discarded); + data.setState(persistentState); + return; + + /** + * persistent item that has been transiently modified or + * removed and the underlying persistent state has been + * externally destroyed since the transient + * modification/removal. + */ + case ItemState.STATUS_STALE_DESTROYED: + /** + * first notify the listeners that this instance has been + * permanently invalidated + */ + itemDestroyed(discarded.getId(), data); + // now set state of this instance to 'destroyed' + data.setStatus(ItemImpl.STATUS_DESTROYED); + data.setState(null); + return; + + /** + * new item that has been transiently added + */ + case ItemState.STATUS_NEW: + /** + * first notify the listeners that this instance has been + * permanently invalidated + */ + itemDestroyed(discarded.getId(), data); + // now set state of this instance to 'destroyed' + // finally dispose state + data.setStatus(ItemImpl.STATUS_DESTROYED); + data.setState(null); + return; + } + } + + /** + * first notify the listeners that this instance has been + * invalidated + */ + itemInvalidated(discarded.getId(), data); + // now render this instance 'invalid' + data.setStatus(ItemImpl.STATUS_INVALIDATED); + } + } + + /** + * Cache of shareable nodes. For performance reasons, methods are not + * synchronized and thread-safety must be guaranteed by caller. + */ + static class ShareableNodesCache { + + /** + * This cache is based on a reference map, that maps an item id to a map, + * which again maps a (hard-ref) parent id to a (weak-ref) shareable node. + */ + private final ReferenceMap cache; + + /** + * Create a new instance of this class. + */ + public ShareableNodesCache() { + cache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.HARD); + } + + /** + * Clear cache. + * + * @see ReferenceMap#clear() + */ + public void clear() { + cache.clear(); + } + + /** + * Return the first available node that maps to the given id. + * + * @param id node id + * @return node or null + */ + public AbstractNodeData retrieveFirst(NodeId id) { + ReferenceMap map = (ReferenceMap) cache.get(id); + if (map != null) { + Iterator iter = map.values().iterator(); + try { + while (iter.hasNext()) { + AbstractNodeData data = iter.next(); + if (data != null) { + return data; + } + } + } finally { + iter = null; + } + } + return null; + } + + /** + * Return the node with the given id and parent id. + * + * @param id node id + * @param parentId parent id + * @return node or null + */ + public AbstractNodeData retrieve(NodeId id, NodeId parentId) { + ReferenceMap map = (ReferenceMap) cache.get(id); + if (map != null) { + return (AbstractNodeData) map.get(parentId); + } + return null; + } + + /** + * Cache some node. + * + * @param data data to cache + */ + public void cache(AbstractNodeData data) { + NodeId id = data.getNodeState().getNodeId(); + ReferenceMap map = (ReferenceMap) cache.get(id); + if (map == null) { + map = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK); + cache.put(id, map); + } + Object old = map.put(data.getPrimaryParentId(), data); + if (old != null) { + log.debug("overwriting cached item: " + old); + } + } + + /** + * Evict some node from the cache. + * + * @param data data to evict + */ + public void evict(AbstractNodeData data) { + ReferenceMap map = (ReferenceMap) cache.get(data.getId()); + if (map != null) { + map.remove(data.getPrimaryParentId()); + } + } + + /** + * Evict all nodes with a given node id from the cache. + * + * @param id node id to evict + */ + public synchronized void evictAll(NodeId id) { + cache.remove(id); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemRefreshOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemRefreshOperation.java new file mode 100644 index 00000000000..b3c1fe98589 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemRefreshOperation.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.core; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.SessionItemStateManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ItemRefreshOperation implements SessionOperation { + + /** + * Logger instance. + */ + private static final Logger log = + LoggerFactory.getLogger(ItemRefreshOperation.class); + + private final ItemState state; + + private final boolean keepChanges; + + public ItemRefreshOperation(ItemState state, boolean keepChanges) { + this.state = state; + this.keepChanges = keepChanges; + } + + public Object perform(SessionContext context) throws RepositoryException { + if (keepChanges) { + // FIXME When keepChanges is true, should reset Item#status field + // to STATUS_NORMAL of all descendant non-transient instances; + // maybe also have to reset stale ItemState instances + return this; + } + + SessionItemStateManager stateMgr = context.getItemStateManager(); + + // Optimisation for the root node + if (state.getParentId() == null) { + stateMgr.disposeAllTransientItemStates(); + return this; + } + + // list of transient items that should be discarded + List transientStates = new ArrayList(); + + // check status of this item's state + if (state.isTransient()) { + switch (state.getStatus()) { + case ItemState.STATUS_STALE_DESTROYED: + // add this item's state to the list + transientStates.add(state); + break; + case ItemState.STATUS_EXISTING_MODIFIED: + if (!state.getParentId().equals( + state.getOverlayedState().getParentId())) { + throw new RepositoryException( + "Cannot refresh a moved item," + + " try refreshing the parent: " + this); + } + transientStates.add(state); + break; + case ItemState.STATUS_NEW: + throw new RepositoryException( + "Cannot refresh a new item: " + this); + default: + // log and ignore + log.warn("Unexpected item state status {} of {}", + state.getStatus(), this); + break; + } + } + + if (state.isNode()) { + // build list of 'new', 'modified' or 'stale' descendants + for (ItemState transientState + : stateMgr.getDescendantTransientItemStates(state.getId())) { + switch (transientState.getStatus()) { + case ItemState.STATUS_STALE_DESTROYED: + case ItemState.STATUS_NEW: + case ItemState.STATUS_EXISTING_MODIFIED: + // add new or modified state to the list + transientStates.add(transientState); + break; + + default: + // log and ignore + log.debug("unexpected state status ({})", + transientState.getStatus()); + break; + } + } + } + + // process list of 'new', 'modified' or 'stale' transient states + for (ItemState transientState : transientStates) { + // dispose the transient state, it is no longer used; + // this will indirectly (through stateDiscarded listener method) + // either restore or permanently invalidate the wrapping Item instances + stateMgr.disposeTransientItemState(transientState); + } + + if (state.isNode()) { + // discard all transient descendants in the attic (i.e. those marked + // as 'removed'); this will resurrect the removed items + for (ItemState descendant + : stateMgr.getDescendantTransientItemStatesInAttic(state.getId())) { + // dispose the transient state; this will indirectly + // (through stateDiscarded listener method) resurrect + // the wrapping Item instances + stateMgr.disposeTransientItemStateInAttic(descendant); + } + } + + return this; + } + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "item.refresh(" + keepChanges + ")"; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemRemoveOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemRemoveOperation.java new file mode 100644 index 00000000000..e2d3cd039b4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemRemoveOperation.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.core; + +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CHECKED_OUT; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CONSTRAINTS; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_RETENTION; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionWriteOperation; + +/** + * Session operation for removing a given item, optionally with constraint + * checks enabled. + */ +class ItemRemoveOperation implements SessionWriteOperation { + + /** + * The item to be removed. + */ + private final ItemImpl item; + + /** + * Flag to enabled constraint checks + */ + private final boolean checks; + + public ItemRemoveOperation(ItemImpl item, boolean checks) { + this.item = item; + this.checks = checks; + } + + public Object perform(SessionContext context) + throws RepositoryException { + // check if this is the root node + if (item.getDepth() == 0) { + throw new RepositoryException("Cannot remove the root node"); + } + + NodeImpl parentNode = (NodeImpl) item.getParent(); + if (checks) { + ItemValidator validator = context.getItemValidator(); + validator.checkRemove( + item, + CHECK_CONSTRAINTS | CHECK_HOLD | CHECK_RETENTION, + Permission.NONE); + + // Make sure the parent node is checked-out and + // neither protected nor locked. + validator.checkModify( + parentNode, + CHECK_LOCK | CHECK_CHECKED_OUT | CHECK_CONSTRAINTS, + Permission.NONE); + } + + // delegate the removal of the child item to the parent node + if (item.isNode()) { + parentNode.removeChildNode((NodeId) item.getId()); + } else { + parentNode.removeChildProperty(item.getPrimaryPath().getName()); + } + + return this; + } + + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "item.remove()"; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemSaveOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemSaveOperation.java new file mode 100644 index 00000000000..26a84e57ff4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemSaveOperation.java @@ -0,0 +1,939 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionWriteOperation; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.SessionItemStateManager; +import org.apache.jackrabbit.core.state.StaleItemStateException; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.version.InternalVersionManager; +import org.apache.jackrabbit.core.version.VersionHistoryInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The session operation triggered by {@link Item#save()}. + */ +class ItemSaveOperation implements SessionWriteOperation { + + /** + * Logger instance. + */ + private static final Logger log = + LoggerFactory.getLogger(ItemSaveOperation.class); + + private final ItemState state; + + public ItemSaveOperation(ItemState state) { + this.state = state; + } + + public Object perform(SessionContext context) throws RepositoryException { + SessionItemStateManager stateMgr = context.getItemStateManager(); + + /** + * build list of transient (i.e. new & modified) states that + * should be persisted + */ + Collection dirty; + try { + dirty = getTransientStates(context.getItemStateManager()); + } catch (ConcurrentModificationException e) { + String msg = "Concurrent modification; session is closed"; + log.error(msg, e); + context.getSessionImpl().logout(); + throw e; + } + if (dirty.size() == 0) { + // no transient items, nothing to do here + return this; + } + + /** + * build list of transient descendants in the attic + * (i.e. those marked as 'removed') + */ + Collection removed = + getRemovedStates(context.getItemStateManager()); + + // All affected item states. The keys are used to look up whether + // an item is affected, and the values are iterated through below + Map affected = + new HashMap(dirty.size() + removed.size()); + for (ItemState state : dirty) { + affected.put(state.getId(), state); + } + for (ItemState state : removed) { + affected.put(state.getId(), state); + } + + /** + * make sure that this save operation is totally 'self-contained' + * and independent; items within the scope of this save operation + * must not have 'external' dependencies; + * (e.g. moving a node requires that the target node including both + * old and new parents are saved) + */ + for (ItemState transientState : affected.values()) { + if (transientState.isNode()) { + NodeState nodeState = (NodeState) transientState; + Set dependentIDs = new HashSet(); + if (nodeState.hasOverlayedState()) { + NodeState overlayedState = + (NodeState) nodeState.getOverlayedState(); + NodeId oldParentId = overlayedState.getParentId(); + NodeId newParentId = nodeState.getParentId(); + if (oldParentId != null) { + if (newParentId == null) { + // node has been removed, add old parents + // to dependencies + if (overlayedState.isShareable()) { + dependentIDs.addAll(overlayedState.getSharedSet()); + } else { + dependentIDs.add(oldParentId); + } + } else { + if (!oldParentId.equals(newParentId)) { + // node has been moved to a new location, + // add old and new parent to dependencies + dependentIDs.add(oldParentId); + dependentIDs.add(newParentId); + } else { + // parent id hasn't changed, check whether + // the node has been renamed (JCR-1034) + if (!affected.containsKey(newParentId) + && stateMgr.hasTransientItemState(newParentId)) { + try { + NodeState parent = (NodeState) stateMgr.getTransientItemState(newParentId); + // check parent's renamed child node entries + for (ChildNodeEntry cne : parent.getRenamedChildNodeEntries()) { + if (cne.getId().equals(nodeState.getId())) { + // node has been renamed, + // add parent to dependencies + dependentIDs.add(newParentId); + } + } + } catch (ItemStateException ise) { + // should never get here + log.warn("failed to retrieve transient state: " + newParentId, ise); + } + } + } + } + } + } + + // removed child node entries + for (ChildNodeEntry cne : nodeState.getRemovedChildNodeEntries()) { + dependentIDs.add(cne.getId()); + } + // added child node entries + for (ChildNodeEntry cne : nodeState.getAddedChildNodeEntries()) { + dependentIDs.add(cne.getId()); + } + + // now walk through dependencies and check whether they + // are within the scope of this save operation + for (NodeId id : dependentIDs) { + if (!affected.containsKey(id)) { + // JCR-1359 workaround: check whether unresolved + // dependencies originate from 'this' session; + // otherwise ignore them + if (stateMgr.hasTransientItemState(id) + || stateMgr.hasTransientItemStateInAttic(id)) { + // need to save dependency as well + String msg = + context.getItemManager().safeGetJCRPath(id) + + " needs to be saved as well."; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + } + } + } + + // validate access and node type constraints + // (this will also validate child removals) + validateTransientItems(context, dirty, removed); + + // start the update operation + try { + stateMgr.edit(); + } catch (IllegalStateException e) { + throw new RepositoryException("Unable to start edit operation", e); + } + + boolean succeeded = false; + try { + // process transient items marked as 'removed' + removeTransientItems(context.getItemStateManager(), removed); + + // process transient items that have change in mixins + processShareableNodes( + context.getRepositoryContext().getNodeTypeRegistry(), + dirty); + + // initialize version histories for new nodes (might generate new transient state) + if (initVersionHistories(context, dirty)) { + // re-build the list of transient states because the previous call + // generated new transient state + dirty = getTransientStates(context.getItemStateManager()); + } + + // process 'new' or 'modified' transient states + persistTransientItems(context.getItemManager(), dirty); + + // dispose the transient states marked 'new' or 'modified' + // at this point item state data is pushed down one level, + // node instances are disconnected from the transient + // item state and connected to the 'overlayed' item state. + // transient item states must be removed now. otherwise + // the session item state provider will return an orphaned + // item state which is not referenced by any node instance. + for (ItemState transientState : dirty) { + // dispose the transient state, it is no longer used + stateMgr.disposeTransientItemState(transientState); + } + + // end update operation + stateMgr.update(); + // update operation succeeded + succeeded = true; + } catch (StaleItemStateException e) { + throw new InvalidItemStateException( + "Unable to update a stale item: " + this, e); + } catch (ItemStateException e) { + throw new RepositoryException( + "Unable to update item: " + this, e); + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + stateMgr.cancel(); + + // JCR-288: if an exception has been thrown during + // update() the transient changes have already been + // applied by persistTransientItems() and we need to + // restore transient state, i.e. undo the effect of + // persistTransientItems() + restoreTransientItems(context, dirty); + } + } + + // now it is safe to dispose the transient states: + // dispose the transient states marked 'removed'. + // item states in attic are removed after store, because + // the observation mechanism needs to build paths of removed + // items in store(). + for (ItemState transientState : removed) { + // dispose the transient state, it is no longer used + stateMgr.disposeTransientItemStateInAttic(transientState); + } + + return this; + } + + /** + * Builds a list of transient (i.e. new or modified) item states that are + * within the scope of this.{@link #perform(SessionContext)}. The collection + * returned is ordered depth-first, i.e. the item itself (if transient) + * comes last. + * + * @return list of transient item states + * @throws InvalidItemStateException + * @throws RepositoryException + */ + private Collection getTransientStates( + SessionItemStateManager sism) + throws InvalidItemStateException, RepositoryException { + // list of transient states that should be persisted + ArrayList dirty = new ArrayList(); + + if (state.isNode()) { + // build list of 'new' or 'modified' descendants + for (ItemState transientState + : sism.getDescendantTransientItemStates(state.getId())) { + // fail-fast test: check status of transient state + switch (transientState.getStatus()) { + case ItemState.STATUS_NEW: + case ItemState.STATUS_EXISTING_MODIFIED: + // add modified state to the list + dirty.add(transientState); + break; + + case ItemState.STATUS_STALE_DESTROYED: + throw new InvalidItemStateException( + "Item cannot be saved because it has been " + + "deleted externally: " + this); + + case ItemState.STATUS_UNDEFINED: + throw new InvalidItemStateException( + "Item cannot be saved; it seems to have been " + + "removed externally: " + this); + + default: + log.warn("Unexpected item state status: " + + transientState.getStatus() + " of " + this); + // ignore + break; + } + } + } + // fail-fast test: check status of this item's state + if (state.isTransient()) { + switch (state.getStatus()) { + case ItemState.STATUS_EXISTING_MODIFIED: + // add this item's state to the list + dirty.add(state); + break; + + case ItemState.STATUS_NEW: + throw new RepositoryException( + "Cannot save a new item: " + this); + + case ItemState.STATUS_STALE_DESTROYED: + throw new InvalidItemStateException( + "Item cannot be saved because it has been" + + " deleted externally:" + this); + + case ItemState.STATUS_UNDEFINED: + throw new InvalidItemStateException( + "Item cannot be saved; it seems to have been" + + " removed externally: " + this); + + default: + log.warn("Unexpected item state status:" + + state.getStatus() + " of " + this); + // ignore + break; + } + } + + return dirty; + } + + /** + * Builds a list of transient descendant item states in the attic + * (i.e. those marked as 'removed') that are within the scope of + * this.{@link #perform(SessionContext)}. + * + * @return list of transient item states + * @throws InvalidItemStateException + * @throws RepositoryException + */ + private Collection getRemovedStates( + SessionItemStateManager sism) + throws InvalidItemStateException, RepositoryException { + if (state.isNode()) { + ArrayList removed = new ArrayList(); + for (ItemState transientState + : sism.getDescendantTransientItemStatesInAttic(state.getId())) { + // check if stale + if (transientState.getStatus() == ItemState.STATUS_STALE_DESTROYED) { + throw new InvalidItemStateException( + "Item can't be removed because it has already" + + " been deleted externally: " + + transientState.getId()); + } + removed.add(transientState); + } + return removed; + } else { + return Collections.emptyList(); + } + } + + /** + * the following validations/checks are performed on transient items: + * + * for every transient item: + * - if it is 'modified' or 'new' check the corresponding write permission. + * - if it is 'removed' check the REMOVE permission + * + * for every transient node: + * - if it is 'new' check that its node type satisfies the + * 'required node type' constraint specified in its definition + * - check if 'mandatory' child items exist + * + * for every transient property: + * - check if the property value satisfies the value constraints + * specified in the property's definition + * + * note that the protected flag is checked in Node.addNode/Node.remove + * (for adding/removing child entries of a node), in + * Node.addMixin/removeMixin/setPrimaryType (for type changes on nodes) + * and in Property.setValue (for properties to be modified). + */ + private void validateTransientItems( + SessionContext context, + Iterable dirty, Iterable removed) + throws RepositoryException { + SessionImpl session = context.getSessionImpl(); + ItemManager itemMgr = context.getItemManager(); + SessionItemStateManager stateMgr = context.getItemStateManager(); + + AccessManager accessMgr = context.getAccessManager(); + NodeTypeManagerImpl ntMgr = context.getNodeTypeManager(); + // walk through list of dirty transient items and validate each + for (ItemState itemState : dirty) { + ItemDefinition def; + if (itemState.isNode()) { + def = itemMgr.getDefinition((NodeState) itemState); + } else { + def = itemMgr.getDefinition((PropertyState) itemState); + } + /* check permissions for non-protected items. protected items are + only added through API methods which need to assert that + permissions are not violated. + */ + if (!def.isProtected()) { + /* detect the effective set of modification: + - new added node -> add_node perm on the child + - new property added -> set_property permission + - property modified -> set_property permission + - modified nodes can be ignored for changes only included + child-item addition or removal or changes of protected + properties such as mixin-types which are covered separately + note: removed items are checked later on. + note: reordering of child nodes has been covered upfront as + this information isn't available here. + */ + Path path = stateMgr.getHierarchyMgr().getPath(itemState.getId()); + boolean isGranted = true; + if (itemState.isNode()) { + if (itemState.getStatus() == ItemState.STATUS_NEW) { + isGranted = accessMgr.isGranted(path, Permission.ADD_NODE); + } // else: modified node (see comment above) + } else { + // modified or new property: set_property permission + isGranted = accessMgr.isGranted(path, Permission.SET_PROPERTY); + } + + if (!isGranted) { + String msg = itemMgr.safeGetJCRPath(path) + ": not allowed to add or modify item"; + log.debug(msg); + throw new AccessDeniedException(msg); + } + } + + if (itemState.isNode()) { + // the transient item is a node + NodeState nodeState = (NodeState) itemState; + ItemId id = nodeState.getNodeId(); + NodeDefinition nodeDef = (NodeDefinition) def; + // primary type + NodeTypeImpl pnt = ntMgr.getNodeType(nodeState.getNodeTypeName()); + // effective node type (primary type incl. mixins) + EffectiveNodeType ent = getEffectiveNodeType( + context.getRepositoryContext().getNodeTypeRegistry(), + nodeState); + /** + * if the transient node was added (i.e. if it is 'new') or if + * its primary type has changed, check its node type against the + * required node type in its definition + */ + boolean primaryTypeChanged = + nodeState.getStatus() == ItemState.STATUS_NEW; + if (!primaryTypeChanged) { + NodeState overlaid = + (NodeState) nodeState.getOverlayedState(); + if (overlaid != null) { + Name newName = nodeState.getNodeTypeName(); + Name oldName = overlaid.getNodeTypeName(); + primaryTypeChanged = !newName.equals(oldName); + } + } + if (primaryTypeChanged) { + for (NodeType ntReq : nodeDef.getRequiredPrimaryTypes()) { + Name ntName = ((NodeTypeImpl) ntReq).getQName(); + if (!(pnt.getQName().equals(ntName) + || pnt.isDerivedFrom(ntName))) { + /** + * the transient node's primary node type does not + * satisfy the 'required primary types' constraint + */ + String msg = itemMgr.safeGetJCRPath(id) + + " must be of node type " + ntReq.getName(); + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + } + + // mandatory child properties + for (QPropertyDefinition pd : ent.getMandatoryPropDefs()) { + if (pd.getDeclaringNodeType().equals(NameConstants.MIX_VERSIONABLE) + || pd.getDeclaringNodeType().equals(NameConstants.MIX_SIMPLE_VERSIONABLE)) { + /** + * todo FIXME workaround for mix:versionable: + * the mandatory properties are initialized at a + * later stage and might not exist yet + */ + continue; + } + String msg = itemMgr.safeGetJCRPath(id) + + ": mandatory property " + pd.getName() + + " does not exist"; + if (!nodeState.hasPropertyName(pd.getName())) { + log.debug(msg); + throw new ConstraintViolationException(msg); + } else { + /* + there exists a property with the mandatory-name. + make sure the property really has the expected mandatory + property definition (and not another non-mandatory def, + such as e.g. multivalued residual instead of single-value + mandatory, named def). + */ + PropertyId pi = new PropertyId(nodeState.getNodeId(), pd.getName()); + ItemData childData = itemMgr.getItemData(pi, null, false); + if (!childData.getDefinition().isMandatory()) { + throw new ConstraintViolationException(msg); + } + } + } + // mandatory child nodes + for (QItemDefinition cnd : ent.getMandatoryNodeDefs()) { + String msg = itemMgr.safeGetJCRPath(id) + + ": mandatory child node " + cnd.getName() + + " does not exist"; + if (!nodeState.hasChildNodeEntry(cnd.getName())) { + log.debug(msg); + throw new ConstraintViolationException(msg); + } else { + /* + there exists a child node with the mandatory-name. + make sure the node really has the expected mandatory + node definition. + */ + boolean hasMandatoryChild = false; + for (ChildNodeEntry cne : nodeState.getChildNodeEntries(cnd.getName())) { + ItemData childData = itemMgr.getItemData(cne.getId(), null, false); + if (childData.getDefinition().isMandatory()) { + hasMandatoryChild = true; + break; + } + } + if (!hasMandatoryChild) { + throw new ConstraintViolationException(msg); + } + } + } + } else { + // the transient item is a property + PropertyState propState = (PropertyState) itemState; + ItemId propId = propState.getPropertyId(); + org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl propDef = (org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl) def; + + /** + * check value constraints + * (no need to check value constraints of protected properties + * as those are set by the implementation only, i.e. they + * cannot be set by the user through the api) + */ + if (!def.isProtected()) { + String[] constraints = propDef.getValueConstraints(); + if (constraints != null) { + InternalValue[] values = propState.getValues(); + try { + EffectiveNodeType.checkSetPropertyValueConstraints( + propDef.unwrap(), values); + } catch (RepositoryException e) { + // repack exception for providing more verbose error message + String msg = itemMgr.safeGetJCRPath(propId) + ": " + e.getMessage(); + log.debug(msg); + throw new ConstraintViolationException(msg); + } + + /** + * need to manually check REFERENCE value constraints + * as this requires a session (target node needs to + * be checked) + */ + if (constraints.length > 0 + && (propDef.getRequiredType() == PropertyType.REFERENCE + || propDef.getRequiredType() == PropertyType.WEAKREFERENCE)) { + for (InternalValue internalV : values) { + boolean satisfied = false; + String constraintViolationMsg = null; + try { + NodeId targetId = internalV.getNodeId(); + if (propDef.getRequiredType() == PropertyType.WEAKREFERENCE + && !itemMgr.itemExists(targetId)) { + // target of weakref doesn;t exist, skip + continue; + } + Node targetNode = session.getNodeById(targetId); + /** + * constraints are OR-ed, i.e. at least one + * has to be satisfied + */ + for (String constrNtName : constraints) { + /** + * a [WEAK]REFERENCE value constraint specifies + * the name of the required node type of + * the target node + */ + if (targetNode.isNodeType(constrNtName)) { + satisfied = true; + break; + } + } + if (!satisfied) { + NodeType[] mixinNodeTypes = targetNode.getMixinNodeTypes(); + String[] targetMixins = new String[mixinNodeTypes.length]; + for (int j = 0; j < mixinNodeTypes.length; j++) { + targetMixins[j] = mixinNodeTypes[j].getName(); + } + String targetMixinsString = Text.implode(targetMixins, ", "); + String constraintsString = Text.implode(constraints, ", "); + constraintViolationMsg = itemMgr.safeGetJCRPath(propId) + + ": is constraint to [" + + constraintsString + + "] but references [primaryType=" + + targetNode.getPrimaryNodeType().getName() + + ", mixins=" + + targetMixinsString + "]"; + } + } catch (RepositoryException re) { + String msg = itemMgr.safeGetJCRPath(propId) + + ": failed to check " + + ((propDef.getRequiredType() == PropertyType.REFERENCE) ? "REFERENCE" : "WEAKREFERENCE") + + " value constraint"; + log.debug(msg); + throw new ConstraintViolationException(msg, re); + } + if (!satisfied) { + log.debug(constraintViolationMsg); + throw new ConstraintViolationException(constraintViolationMsg); + } + } + } + } + } + + /** + * no need to check the protected flag as this is checked + * in PropertyImpl.setValue(Value) + */ + } + } + + // walk through list of removed transient items and check REMOVE permission + for (ItemState itemState : removed) { + QItemDefinition def; + try { + if (itemState.isNode()) { + def = itemMgr.getDefinition((NodeState) itemState).unwrap(); + } else { + def = itemMgr.getDefinition((PropertyState) itemState).unwrap(); + } + } catch (ConstraintViolationException e) { + // since identifier of assigned definition is not stored anymore + // with item state (see JCR-2170), correct definition cannot be + // determined for items which have been removed due to removal + // of a mixin (see also JCR-2130 & JCR-2408) + continue; + } + if (!def.isProtected()) { + Path path = stateMgr.getAtticAwareHierarchyMgr().getPath(itemState.getId()); + // check REMOVE permission + int permission = (itemState.isNode()) ? Permission.REMOVE_NODE : Permission.REMOVE_PROPERTY; + if (!accessMgr.isGranted(path, permission)) { + String msg = itemMgr.safeGetJCRPath(path) + + ": not allowed to remove item"; + log.debug(msg); + throw new AccessDeniedException(msg); + } + } + } + } + + /** + * walk through list of transient items marked 'removed' and + * definitively remove each one + */ + private void removeTransientItems( + SessionItemStateManager sism, Iterable states) throws StaleItemStateException { + for (ItemState transientState : states) { + ItemState persistentState = transientState.getOverlayedState(); + // remove persistent state + // this will indirectly (through stateDestroyed listener method) + // permanently invalidate all Item instances wrapping it + assert persistentState != null; + if (transientState.getModCount() != persistentState.getModCount()) { + throw new StaleItemStateException(transientState.getId() + " has been modified externally"); + } + sism.destroy(persistentState); + } + } + + /** + * Process all items given in iterator and check whether mix:shareable + * or (some derived node type) has been added or removed: + *
      + *
    • If the mixin mix:shareable (or some derived node type), + * then initialize the shared set inside the state.
    • + *
    • If the mixin mix:shareable (or some derived node type) + * has been removed, throw.
    • + *
    + */ + private void processShareableNodes( + NodeTypeRegistry registry, Iterable states) + throws RepositoryException { + for (ItemState is : states) { + if (is.isNode()) { + NodeState ns = (NodeState) is; + boolean wasShareable = false; + if (ns.hasOverlayedState()) { + NodeState old = (NodeState) ns.getOverlayedState(); + EffectiveNodeType ntOld = getEffectiveNodeType(registry, old); + wasShareable = ntOld.includesNodeType(NameConstants.MIX_SHAREABLE); + } + EffectiveNodeType ntNew = getEffectiveNodeType(registry, ns); + boolean isShareable = ntNew.includesNodeType(NameConstants.MIX_SHAREABLE); + + if (!wasShareable && isShareable) { + // mix:shareable has been added + ns.addShare(ns.getParentId()); + + } else if (wasShareable && !isShareable) { + // mix:shareable has been removed: not supported + String msg = "Removing mix:shareable is not supported."; + log.debug(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + } + } + } + + /** + * Initialises the version history of all new nodes of node type + * mix:versionable. + * + * @param states + * @return true if this call generated new transient state; otherwise false + * @throws RepositoryException + */ + private boolean initVersionHistories( + SessionContext context, Iterable states) + throws RepositoryException { + SessionImpl session = context.getSessionImpl(); + ItemManager itemMgr = context.getItemManager(); + + // walk through list of transient items and search for new versionable nodes + boolean createdTransientState = false; + for (ItemState itemState : states) { + if (itemState.isNode()) { + NodeState nodeState = (NodeState) itemState; + EffectiveNodeType nt = getEffectiveNodeType( + context.getRepositoryContext().getNodeTypeRegistry(), + nodeState); + if (nt.includesNodeType(NameConstants.MIX_VERSIONABLE)) { + if (!nodeState.hasPropertyName(NameConstants.JCR_VERSIONHISTORY)) { + NodeImpl node = (NodeImpl) itemMgr.getItem(itemState.getId(), false); + InternalVersionManager vMgr = session.getInternalVersionManager(); + /** + * check if there's already a version history for that + * node; this would e.g. be the case if a versionable + * node had been exported, removed and re-imported with + * either IMPORT_UUID_COLLISION_REMOVE_EXISTING or + * IMPORT_UUID_COLLISION_REPLACE_EXISTING; + * otherwise create a new version history + */ + VersionHistoryInfo history = + vMgr.getVersionHistory(session, nodeState, null); + InternalValue historyId = InternalValue.create( + history.getVersionHistoryId()); + InternalValue versionId = InternalValue.create( + history.getRootVersionId()); + node.internalSetProperty( + NameConstants.JCR_VERSIONHISTORY, historyId); + node.internalSetProperty( + NameConstants.JCR_BASEVERSION, versionId); + node.internalSetProperty( + NameConstants.JCR_ISCHECKEDOUT, + InternalValue.create(true)); + node.internalSetProperty( + NameConstants.JCR_PREDECESSORS, + new InternalValue[] { versionId }); + createdTransientState = true; + } + } else if (nt.includesNodeType(NameConstants.MIX_SIMPLE_VERSIONABLE)) { + // we need to check the version manager for an existing + // version history, since simple versioning does not + // expose it's reference in a property + InternalVersionManager vMgr = session.getInternalVersionManager(); + vMgr.getVersionHistory(session, nodeState, null); + + // create isCheckedOutProperty if not already exists + NodeImpl node = (NodeImpl) itemMgr.getItem(itemState.getId(), false); + if (!nodeState.hasPropertyName(NameConstants.JCR_ISCHECKEDOUT)) { + node.internalSetProperty( + NameConstants.JCR_ISCHECKEDOUT, + InternalValue.create(true)); + createdTransientState = true; + } + } + } + } + return createdTransientState; + } + + /** + * walk through list of transient items and persist each one + */ + private void persistTransientItems( + ItemManager itemMgr, Iterable states) + throws RepositoryException { + for (ItemState state : states) { + // persist state of transient item + itemMgr.getItem(state.getId(), false).makePersistent(); + } + } + + /** + * walk through list of transient states and re-apply transient changes + */ + private void restoreTransientItems( + SessionContext context, Iterable items) { + ItemManager itemMgr = context.getItemManager(); + SessionItemStateManager stateMgr = context.getItemStateManager(); + + for (ItemState itemState : items) { + ItemId id = itemState.getId(); + ItemImpl item; + + try { + if (stateMgr.isItemStateInAttic(id)) { + // If an item has been removed and then again created, the + // item is lost after persistTransientItems() and the + // TransientItemStateManager will bark because of a deleted + // state in its attic. We therefore have to forge a new item + // instance ourself. + item = itemMgr.createItemInstance(itemState); + itemState.setStatus(ItemState.STATUS_NEW); + } else { + try { + item = itemMgr.getItem(id, false); + } catch (ItemNotFoundException infe) { + // itemState probably represents a 'new' item and the + // ItemImpl instance wrapping it has already been gc'ed; + // we have to re-create the ItemImpl instance + item = itemMgr.createItemInstance(itemState); + itemState.setStatus(ItemState.STATUS_NEW); + } + } + // re-apply transient changes + // for persistent nodes undo effect of item.makePersistent() + if (item.isNode()) { + NodeImpl node = (NodeImpl) item; + node.restoreTransient((NodeState) itemState); + } else { + PropertyImpl prop = (PropertyImpl) item; + prop.restoreTransient((PropertyState) itemState); + } + } catch (RepositoryException re) { + // something went wrong, log exception and carry on + String msg = itemMgr.safeGetJCRPath(id) + + ": failed to restore transient state"; + if (log.isDebugEnabled()) { + log.warn(msg, re); + } else { + log.warn(msg); + } + } + } + } + + /** + * Helper method that builds the effective (i.e. merged and resolved) + * node type representation of the specified node's primary and mixin + * node types. + * + * @param state + * @return the effective node type + * @throws RepositoryException + */ + private EffectiveNodeType getEffectiveNodeType( + NodeTypeRegistry registry, NodeState state) + throws RepositoryException { + try { + return registry.getEffectiveNodeType( + state.getNodeTypeName(), state.getMixinTypeNames()); + } catch (NodeTypeConflictException e) { + throw new RepositoryException( + "Failed to build effective node type of node state " + + state.getId(), e); + } + } + + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "item.save()"; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemValidator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemValidator.java new file mode 100644 index 00000000000..eb08c56812c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ItemValidator.java @@ -0,0 +1,547 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for validating an item against constraints + * specified by its definition. + */ +public class ItemValidator { + + /** + * check access permissions + */ + public static final int CHECK_ACCESS = 1; + + /** + * option to check lock status + */ + public static final int CHECK_LOCK = 2; + + /** + * option to check checked-out status + */ + public static final int CHECK_CHECKED_OUT = 4; + + /** + * check for referential integrity upon removal + */ + public static final int CHECK_REFERENCES = 8; + + /** + * option to check if the item is protected by it's nt definition + */ + public static final int CHECK_CONSTRAINTS = 16; + + /** + * option to check for pending changes on the session + */ + public static final int CHECK_PENDING_CHANGES = 32; + + /** + * option to check for pending changes on the specified node + */ + public static final int CHECK_PENDING_CHANGES_ON_NODE = 64; + + /** + * option to check for effective holds + */ + public static final int CHECK_HOLD = 128; + + /** + * option to check for effective retention policies + */ + public static final int CHECK_RETENTION = 256; + + /** + * Logger instance for this class + */ + private static Logger log = LoggerFactory.getLogger(ItemValidator.class); + + /** + * Component context of the associated session. + */ + protected final SessionContext context; + + /** + * A bit mask of the checks that are currently enabled. All access to + * this mask must be synchronized to ensure that only the thread that + * uses the {@link #performRelaxed(SessionOperation, int)} method will + * experience the effect of the relaxed set of checks. + */ + private int enabledChecks = ~0; + + /** + * Creates a new ItemValidator instance. + * + * @param context component context of this session + */ + public ItemValidator(SessionContext context) { + this.context = context; + } + + /** + * Performs the given session operation with the specified checks disabled. + * + * @param operation the session operation to be performed + * @param checksToDisable bit mask of checks to be disabled + * @return return value of the session operation + * @throws RepositoryException if the operation could not be performed + */ + public synchronized T performRelaxed( + SessionOperation operation, int checksToDisable) + throws RepositoryException { + int previousChecks = enabledChecks; + try { + enabledChecks &= ~checksToDisable; + log.debug("Performing {} with checks [{}] disabled", + operation, Integer.toBinaryString(~enabledChecks)); + return operation.perform(context); + } finally { + enabledChecks = previousChecks; + } + } + + /** + * Checks whether the given node state satisfies the constraints specified + * by its primary and mixin node types. The following validations/checks are + * performed: + *
      + *
    • check if its node type satisfies the 'required node types' constraint + * specified in its definition
    • + *
    • check if all 'mandatory' child items exist
    • + *
    • for every property: check if the property value satisfies the + * value constraints specified in the property's definition
    • + *
    + * + * @param nodeState state of node to be validated + * @throws ConstraintViolationException if any of the validations fail + * @throws RepositoryException if another error occurs + */ + public void validate(NodeState nodeState) + throws ConstraintViolationException, RepositoryException { + // effective primary node type + NodeTypeRegistry registry = context.getNodeTypeRegistry(); + EffectiveNodeType entPrimary = + registry.getEffectiveNodeType(nodeState.getNodeTypeName()); + // effective node type (primary type incl. mixins) + EffectiveNodeType entPrimaryAndMixins = getEffectiveNodeType(nodeState); + QNodeDefinition def = + context.getItemManager().getDefinition(nodeState).unwrap(); + + // check if primary type satisfies the 'required node types' constraint + for (Name requiredPrimaryType : def.getRequiredPrimaryTypes()) { + if (!entPrimary.includesNodeType(requiredPrimaryType)) { + String msg = safeGetJCRPath(nodeState.getNodeId()) + + ": missing required primary type " + + requiredPrimaryType; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + // mandatory properties + for (QPropertyDefinition pd : entPrimaryAndMixins.getMandatoryPropDefs()) { + if (!nodeState.hasPropertyName(pd.getName())) { + String msg = safeGetJCRPath(nodeState.getNodeId()) + + ": mandatory property " + pd.getName() + + " does not exist"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + // mandatory child nodes + for (QItemDefinition cnd : entPrimaryAndMixins.getMandatoryNodeDefs()) { + if (!nodeState.hasChildNodeEntry(cnd.getName())) { + String msg = safeGetJCRPath(nodeState.getNodeId()) + + ": mandatory child node " + cnd.getName() + + " does not exist"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + } + + /** + * Checks whether the given property state satisfies the constraints + * specified by its definition. The following validations/checks are + * performed: + *
      + *
    • check if the type of the property values does comply with the + * requiredType specified in the property's definition
    • + *
    • check if the property values satisfy the value constraints + * specified in the property's definition
    • + *
    + * + * @param propState state of property to be validated + * @throws ConstraintViolationException if any of the validations fail + * @throws RepositoryException if another error occurs + */ + public void validate(PropertyState propState) + throws ConstraintViolationException, RepositoryException { + QPropertyDefinition def = + context.getItemManager().getDefinition(propState).unwrap(); + InternalValue[] values = propState.getValues(); + int type = PropertyType.UNDEFINED; + for (InternalValue value : values) { + if (type == PropertyType.UNDEFINED) { + type = value.getType(); + } else if (type != value.getType()) { + throw new ConstraintViolationException(safeGetJCRPath(propState.getPropertyId()) + + ": inconsistent value types"); + } + if (def.getRequiredType() != PropertyType.UNDEFINED + && def.getRequiredType() != type) { + throw new ConstraintViolationException(safeGetJCRPath(propState.getPropertyId()) + + ": requiredType constraint is not satisfied"); + } + } + EffectiveNodeType.checkSetPropertyValueConstraints(def, values); + } + + public synchronized void checkModify( + ItemImpl item, int options, int permissions) + throws RepositoryException { + checkCondition(item, options & enabledChecks, permissions, false); + } + + public synchronized void checkRemove( + ItemImpl item, int options, int permissions) + throws RepositoryException { + checkCondition(item, options & enabledChecks, permissions, true); + } + + private void checkCondition(ItemImpl item, int options, int permissions, boolean isRemoval) throws RepositoryException { + if ((options & CHECK_PENDING_CHANGES) == CHECK_PENDING_CHANGES) { + if (item.getSession().hasPendingChanges()) { + String msg = "Unable to perform operation. Session has pending changes."; + log.debug(msg); + throw new InvalidItemStateException(msg); + } + } + if ((options & CHECK_PENDING_CHANGES_ON_NODE) == CHECK_PENDING_CHANGES_ON_NODE) { + if (item.isNode() && ((NodeImpl) item).hasPendingChanges()) { + String msg = "Unable to perform operation. Session has pending changes."; + log.debug(msg); + throw new InvalidItemStateException(msg); + } + } + if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) { + if (isProtected(item)) { + String msg = "Unable to perform operation. Node is protected."; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + if ((options & CHECK_CHECKED_OUT) == CHECK_CHECKED_OUT) { + NodeImpl node = (item.isNode()) ? (NodeImpl) item : (NodeImpl) item.getParent(); + if (!node.isCheckedOut()) { + String msg = "Unable to perform operation. Node is checked-in."; + log.debug(msg); + throw new VersionException(msg); + } + } + if ((options & CHECK_LOCK) == CHECK_LOCK) { + checkLock(item); + } + + if (permissions > Permission.NONE) { + Path path = item.getPrimaryPath(); + context.getAccessManager().checkPermission(path, permissions); + } + if ((options & CHECK_HOLD) == CHECK_HOLD) { + if (hasHold(item, isRemoval)) { + throw new RepositoryException("Unable to perform operation. Node is affected by a hold."); + } + } + if ((options & CHECK_RETENTION) == CHECK_RETENTION) { + if (hasRetention(item, isRemoval)) { + throw new RepositoryException("Unable to perform operation. Node is affected by a retention."); + } + } + } + + public synchronized boolean canModify( + ItemImpl item, int options, int permissions) + throws RepositoryException { + return hasCondition(item, options & enabledChecks, permissions, false); + } + + private boolean hasCondition(ItemImpl item, int options, int permissions, boolean isRemoval) throws RepositoryException { + if ((options & CHECK_PENDING_CHANGES) == CHECK_PENDING_CHANGES) { + if (item.getSession().hasPendingChanges()) { + return false; + } + } + if ((options & CHECK_PENDING_CHANGES_ON_NODE) == CHECK_PENDING_CHANGES_ON_NODE) { + if (item.isNode() && ((NodeImpl) item).hasPendingChanges()) { + return false; + } + } + if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) { + if (isProtected(item)) { + return false; + } + } + if ((options & CHECK_CHECKED_OUT) == CHECK_CHECKED_OUT) { + NodeImpl node = (item.isNode()) ? (NodeImpl) item : (NodeImpl) item.getParent(); + if (!node.isCheckedOut()) { + return false; + } + } + if ((options & CHECK_LOCK) == CHECK_LOCK) { + try { + checkLock(item); + } catch (LockException e) { + return false; + } + } + if (permissions > Permission.NONE) { + Path path = item.getPrimaryPath(); + if (!context.getAccessManager().isGranted(path, permissions)) { + return false; + } + } + if ((options & CHECK_HOLD) == CHECK_HOLD) { + if (hasHold(item, isRemoval)) { + return false; + } + } + if ((options & CHECK_RETENTION) == CHECK_RETENTION) { + if (hasRetention(item, isRemoval)) { + return false; + } + } + return true; + } + + private void checkLock(ItemImpl item) throws LockException, RepositoryException { + if (item.isNew()) { + // a new item needs no check + return; + } + NodeImpl node = (item.isNode()) ? (NodeImpl) item : (NodeImpl) item.getParent(); + context.getWorkspace().getInternalLockManager().checkLock(node); + } + + private boolean isProtected(ItemImpl item) throws RepositoryException { + ItemDefinition def; + if (item.isNode()) { + def = ((Node) item).getDefinition(); + } else { + def = ((Property) item).getDefinition(); + } + return def.isProtected(); + } + + private boolean hasHold(ItemImpl item, boolean isRemoval) throws RepositoryException { + if (item.isNew()) { + return false; + } + Path path = item.getPrimaryPath(); + if (!item.isNode()) { + path = path.getAncestor(1); + } + boolean checkParent = (item.isNode() && isRemoval); + return context.getSessionImpl().getRetentionRegistry().hasEffectiveHold(path, checkParent); + } + + private boolean hasRetention(ItemImpl item, boolean isRemoval) throws RepositoryException { + if (item.isNew()) { + return false; + } + Path path = item.getPrimaryPath(); + if (!item.isNode()) { + path = path.getAncestor(1); + } + boolean checkParent = (item.isNode() && isRemoval); + return context.getSessionImpl().getRetentionRegistry().hasEffectiveRetention(path, checkParent); + } + + + + //-------------------------------------------------< misc. helper methods > + /** + * Helper method that builds the effective (i.e. merged and resolved) + * node type representation of the specified node's primary and mixin + * node types. + * + * @param state + * @return the effective node type + * @throws RepositoryException + */ + public EffectiveNodeType getEffectiveNodeType(NodeState state) + throws RepositoryException { + try { + return context.getNodeTypeRegistry().getEffectiveNodeType( + state.getNodeTypeName(), state.getMixinTypeNames()); + } catch (NodeTypeConflictException ntce) { + String msg = "internal error: failed to build effective node type for node " + + safeGetJCRPath(state.getNodeId()); + log.debug(msg); + throw new RepositoryException(msg, ntce); + } + } + + /** + * Helper method that finds the applicable definition for a child node with + * the given name and node type in the parent node's node type and + * mixin types. + * + * @param name + * @param nodeTypeName + * @param parentState + * @return a QNodeDefinition + * @throws ConstraintViolationException if no applicable child node definition + * could be found + * @throws RepositoryException if another error occurs + */ + public QNodeDefinition findApplicableNodeDefinition(Name name, + Name nodeTypeName, + NodeState parentState) + throws RepositoryException, ConstraintViolationException { + EffectiveNodeType entParent = getEffectiveNodeType(parentState); + return entParent.getApplicableChildNodeDef( + name, nodeTypeName, context.getNodeTypeRegistry()); + } + + /** + * Helper method that finds the applicable definition for a property with + * the given name, type and multiValued characteristic in the parent node's + * node type and mixin types. If there more than one applicable definitions + * then the following rules are applied: + *
      + *
    • named definitions are preferred to residual definitions
    • + *
    • definitions with specific required type are preferred to definitions + * with required type UNDEFINED
    • + *
    + * + * @param name + * @param type + * @param multiValued + * @param parentState + * @return a QPropertyDefinition + * @throws ConstraintViolationException if no applicable property definition + * could be found + * @throws RepositoryException if another error occurs + */ + public QPropertyDefinition findApplicablePropertyDefinition(Name name, + int type, + boolean multiValued, + NodeState parentState) + throws RepositoryException, ConstraintViolationException { + EffectiveNodeType entParent = getEffectiveNodeType(parentState); + return entParent.getApplicablePropertyDef(name, type, multiValued); + } + + /** + * Helper method that finds the applicable definition for a property with + * the given name, type in the parent node's node type and mixin types. + * Other than {@link #findApplicablePropertyDefinition(Name, int, boolean, NodeState)} + * this method does not take the multiValued flag into account in the + * selection algorithm. If there more than one applicable definitions then + * the following rules are applied: + *
      + *
    • named definitions are preferred to residual definitions
    • + *
    • definitions with specific required type are preferred to definitions + * with required type UNDEFINED
    • + *
    • single-value definitions are preferred to multiple-value definitions
    • + *
    + * + * @param name + * @param type + * @param parentState + * @return a QPropertyDefinition + * @throws ConstraintViolationException if no applicable property definition + * could be found + * @throws RepositoryException if another error occurs + */ + public QPropertyDefinition findApplicablePropertyDefinition(Name name, + int type, + NodeState parentState) + throws RepositoryException, ConstraintViolationException { + EffectiveNodeType entParent = getEffectiveNodeType(parentState); + return entParent.getApplicablePropertyDef(name, type); + } + + /** + * Failsafe conversion of internal Path to JCR path for use in + * error messages etc. + * + * @param path path to convert + * @return JCR path + */ + public String safeGetJCRPath(Path path) { + try { + return context.getJCRPath(path); + } catch (NamespaceException e) { + log.error("failed to convert {} to a JCR path", path); + // return string representation of internal path as a fallback + return path.toString(); + } + } + + /** + * Failsafe translation of internal ItemId to JCR path for use + * in error messages etc. + * + * @param id id to translate + * @return JCR path + */ + public String safeGetJCRPath(ItemId id) { + try { + return safeGetJCRPath( + context.getHierarchyManager().getPath(id)); + } catch (ItemNotFoundException e) { + // return string representation of id as a fallback + return id.toString(); + } catch (RepositoryException e) { + log.error(id + ": failed to build path"); + // return string representation of id as a fallback + return id.toString(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/JackrabbitRepositoryStub.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/JackrabbitRepositoryStub.java new file mode 100644 index 00000000000..cfbd63cb012 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/JackrabbitRepositoryStub.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; +import org.apache.jackrabbit.test.RepositoryStubException; + +/** + * RepositoryStub implementation for Apache Jackrabbit. + * + * @since Apache Jackrabbit 1.6 + */ +public class JackrabbitRepositoryStub extends RepositoryStub { + + /** + * Property for the repository configuration file. Defaults to + * <repository home>/repository.xml if not specified. + */ + public static final String PROP_REPOSITORY_CONFIG = + "org.apache.jackrabbit.repository.config"; + + /** + * Property for the repository home directory. Defaults to + * target/repository for convenience in Maven builds. + */ + public static final String PROP_REPOSITORY_HOME = + "org.apache.jackrabbit.repository.home"; + + /** + * Repository settings. + */ + private final Properties settings; + + /** + * Map of repository instances. Key = repository home, value = repository + * instance. + */ + private static final Map REPOSITORY_INSTANCES = new HashMap(); + + static { + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + public void run() { + synchronized (REPOSITORY_INSTANCES) { + for (Repository repo : REPOSITORY_INSTANCES.values()) { + if (repo instanceof RepositoryImpl) { + ((RepositoryImpl) repo).shutdown(); + } + } + } + } + })); + } + + public static RepositoryContext getRepositoryContext( + Repository repository) { + synchronized (REPOSITORY_INSTANCES) { + for (Repository r : REPOSITORY_INSTANCES.values()) { + if (r == repository) { + return ((RepositoryImpl) r).context; + } + } + } + throw new RuntimeException("Not a test repository: " + repository); + } + + private static Properties getStaticProperties() { + Properties properties = new Properties(); + try { + InputStream stream = + getResource("JackrabbitRepositoryStub.properties"); + try { + properties.load(stream); + } finally { + stream.close(); + } + } catch (IOException e) { + // TODO: Log warning + } + return properties; + } + + private static InputStream getResource(String name) { + return JackrabbitRepositoryStub.class.getResourceAsStream(name); + } + + /** + * Constructor as required by the JCR TCK. + * + * @param settings repository settings + */ + public JackrabbitRepositoryStub(Properties settings) { + super(getStaticProperties()); + // set some attributes on the sessions + superuser.setAttribute("jackrabbit", "jackrabbit"); + readwrite.setAttribute("jackrabbit", "jackrabbit"); + readonly.setAttribute("jackrabbit", "jackrabbit"); + + // Repository settings + this.settings = settings; + } + + /** + * Returns the configured repository instance. + * + * @return the configured repository instance. + * @throws RepositoryStubException if an error occurs while + * obtaining the repository instance. + */ + public synchronized Repository getRepository() + throws RepositoryStubException { + try { + String dir = settings.getProperty(PROP_REPOSITORY_HOME); + if (dir == null) { + dir = new File("target", "repository").getAbsolutePath(); + } else { + dir = new File(dir).getAbsolutePath(); + } + + String xml = settings.getProperty(PROP_REPOSITORY_CONFIG); + if (xml == null) { + xml = new File(dir, "repository.xml").getPath(); + } + + return getOrCreateRepository(dir, xml); + } catch (Exception e) { + throw new RepositoryStubException("Failed to start repository", e); + } + } + + protected Repository createRepository(String dir, String xml) + throws Exception { + new File(dir).mkdirs(); + + if (!new File(xml).exists()) { + InputStream input = getResource("repository.xml"); + try { + OutputStream output = new FileOutputStream(xml); + try { + IOUtils.copy(input, output); + } finally { + output.close(); + } + } finally { + input.close(); + } + } + + RepositoryConfig config = RepositoryConfig.create(xml, dir); + return RepositoryImpl.create(config); + } + + protected Repository getOrCreateRepository(String dir, String xml) + throws Exception { + synchronized (REPOSITORY_INSTANCES) { + Repository repo = REPOSITORY_INSTANCES.get(dir); + if (repo == null) { + repo = createRepository(dir, xml); + Session session = repo.login(superuser); + try { + TestContentLoader loader = new TestContentLoader(); + loader.loadTestContent(session); + } finally { + session.logout(); + } + + REPOSITORY_INSTANCES.put(dir, repo); + } + return repo; + } + } + + @Override + public Principal getKnownPrincipal(Session session) throws RepositoryException { + + Principal knownPrincipal = null; + + if (session instanceof SessionImpl) { + for (Principal p : ((SessionImpl)session).getSubject().getPrincipals()) { + if (!GroupPrincipals.isGroup(p)) { + knownPrincipal = p; + } + } + } + + if (knownPrincipal != null) { + return knownPrincipal; + } + else { + throw new RepositoryException("no applicable principal found"); + } + } + + private static Principal UNKNOWN_PRINCIPAL = new Principal() { + public String getName() { + return "an_unknown_user"; + } + }; + + @Override + public Principal getUnknownPrincipal(Session session) throws RepositoryException, NotExecutableException { + return UNKNOWN_PRINCIPAL; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/JackrabbitThreadPool.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/JackrabbitThreadPool.java new file mode 100644 index 00000000000..77e491374db --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/JackrabbitThreadPool.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.core; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Thread pool used by the repository. + */ +class JackrabbitThreadPool extends ScheduledThreadPoolExecutor { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory + .getLogger(JackrabbitThreadPool.class); + + /** + * Size of the per-repository thread pool. + */ + private static final int size = + Runtime.getRuntime().availableProcessors() * 2; + + /** + * The classloader used as the context classloader of threads in the pool. + */ + private static final ClassLoader loader = + JackrabbitThreadPool.class.getClassLoader(); + + /** + * Thread counter for generating unique names for the threads in the pool. + */ + private static final AtomicInteger counter = new AtomicInteger(1); + + /** + * Thread factory for creating the threads in the pool + */ + private static final ThreadFactory factory = new ThreadFactory() { + public Thread newThread(Runnable runnable) { + int count = counter.getAndIncrement(); + String name = "jackrabbit-pool-" + count; + Thread thread = new Thread(runnable, name); + thread.setDaemon(true); + if (thread.getPriority() != Thread.NORM_PRIORITY) { + thread.setPriority(Thread.NORM_PRIORITY); + } + thread.setContextClassLoader(loader); + return thread; + } + }; + + /** + * Handler for tasks for which no free thread is found within the pool. + */ + private static final RejectedExecutionHandler handler = new CallerRunsPolicy(); + + /** + * Property to control the value at which the thread pool starts to schedule + * the {@link LowPriorityTask} tasks for later execution. + * + * Set to 0 to disable the check + * + * Default value is 0 (check is disabled). + * + */ + public static final String MAX_LOAD_FOR_LOW_PRIORITY_TASKS_PROPERTY = "org.apache.jackrabbit.core.JackrabbitThreadPool.maxLoadForLowPriorityTasks"; + + /** + * @see #MAX_LOAD_FOR_LOW_PRIORITY_TASKS_PROPERTY + */ + private final static Integer maxLoadForLowPriorityTasks = getMaxLoadForLowPriorityTasks(); + + private static int getMaxLoadForLowPriorityTasks() { + final int defaultMaxLoad = 75; + int max = Integer.getInteger(MAX_LOAD_FOR_LOW_PRIORITY_TASKS_PROPERTY, + defaultMaxLoad); + if (max < 0 || max > 100) { + return defaultMaxLoad; + } + return max; + } + + /** + * Queue where all the {@link LowPriorityTask} tasks go for later execution + */ + private final BlockingQueue lowPriorityTasksQueue = new LinkedBlockingQueue(); + + /** + * Tasks that handles the scheduling and the execution of + * {@link LowPriorityTask} tasks + */ + private final RetryLowPriorityTask retryTask; + + /** + * Creates a new thread pool. + */ + public JackrabbitThreadPool() { + super(size, factory, handler); + retryTask = new RetryLowPriorityTask(this, lowPriorityTasksQueue); + } + + @Override + public void execute(Runnable command) { + if (command instanceof LowPriorityTask) { + scheduleLowPriority(command); + return; + } + super.execute(command); + } + + private void scheduleLowPriority(Runnable command) { + if (isOverDefinedMaxLoad()) { + lowPriorityTasksQueue.add(command); + retryTask.retryLater(); + return; + } + super.execute(command); + } + + /** + * compares the current load of the executor with the defined + * {@link #maxLoadForLowPriorityTasks} parameter. + * + * Used to determine if the executor can handle additional + * {@link LowPriorityTask} tasks. + * + * @return true if the load is under the + * {@link #maxLoadForLowPriorityTasks} parameter + */ + private boolean isOverDefinedMaxLoad() { + if (maxLoadForLowPriorityTasks == 0) { + return false; + } + double currentLoad = ((double) getActiveCount()) / getPoolSize() * 100; + return currentLoad > maxLoadForLowPriorityTasks; + } + + /** + * TEST ONLY + * + * @return the number of low priority tasks that are waiting in the queue + */ + int getPendingLowPriorityTaskCount() { + return lowPriorityTasksQueue.size(); + } + + private static final class RetryLowPriorityTask implements Runnable { + + /** + * schedule interval in ms for delayed tasks + */ + private static final int LATER_MS = 50; + + private final JackrabbitThreadPool executor; + private final BlockingQueue lowPriorityTasksQueue; + + /** + * flag to indicate that another execute has been scheduled or is + * currently running. + */ + private final AtomicBoolean retryPending; + + public RetryLowPriorityTask(JackrabbitThreadPool executor, + BlockingQueue lowPriorityTasksQueue) { + this.executor = executor; + this.lowPriorityTasksQueue = lowPriorityTasksQueue; + this.retryPending = new AtomicBoolean(false); + } + + public void retryLater() { + if (!retryPending.getAndSet(true)) { + executor.schedule(this, LATER_MS, TimeUnit.MILLISECONDS); + } + } + + public void run() { + int count = 0; + while (!executor.isOverDefinedMaxLoad()) { + Runnable r = lowPriorityTasksQueue.poll(); + if (r == null) { + log.debug("Executed {} low priority tasks.", count); + break; + } + count++; + executor.execute(r); + } + retryPending.set(false); + if (!lowPriorityTasksQueue.isEmpty()) { + log.debug( + "Executor is under load, will schedule {} remaining tasks for {} ms later", + lowPriorityTasksQueue.size(), LATER_MS); + retryLater(); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/LazyItemIterator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/LazyItemIterator.java new file mode 100644 index 00000000000..21b584380dd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/LazyItemIterator.java @@ -0,0 +1,269 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * LazyItemIterator is an id-based iterator that instantiates + * the Items only when they are requested. + *

    + * Important: Items that appear to be nonexistent + * for some reason (e.g. because of insufficient access rights or because they + * have been removed since the iterator has been retrieved) are silently + * skipped. As a result the size of the iterator as reported by + * {@link #getSize()} might appear to be shrinking while iterating over the + * items. + * todo should getSize() better always return -1? + * + * @see #getSize() + */ +public class LazyItemIterator implements NodeIterator, PropertyIterator { + + /** Logger instance for this class */ + private static Logger log = LoggerFactory.getLogger(LazyItemIterator.class); + + /** + * The session context used to access the repository. + */ + private final SessionContext sessionContext; + + /** the item manager that is used to lazily fetch the items */ + private final ItemManager itemMgr; + + /** the list of item ids */ + private final List idList; + + /** parent node id (when returning children nodes) or null */ + private final NodeId parentId; + + /** the position of the next item */ + private int pos; + + /** prefetched item to be returned on {@link #next()} */ + private Item next; + + /** + * Creates a new LazyItemIterator instance. + * + * @param sessionContext session context + * @param idList list of item id's + */ + public LazyItemIterator(SessionContext sessionContext, List< ? extends ItemId> idList) { + this(sessionContext, idList, null); + } + + /** + * Creates a new LazyItemIterator instance, additionally taking + * a parent id as parameter. This version should be invoked to strictly return + * children nodes of a node. + * + * @param sessionContext session context + * @param idList list of item id's + * @param parentId parent id. + */ + public LazyItemIterator(SessionContext sessionContext, List< ? extends ItemId> idList, NodeId parentId) { + this.sessionContext = sessionContext; + this.itemMgr = sessionContext.getSessionImpl().getItemManager(); + this.idList = new ArrayList(idList); + this.parentId = parentId; + // prefetch first item + pos = 0; + prefetchNext(); + } + + /** + * Prefetches next item. + *

    + * {@link #next} is set to the next available item in this iterator or to + * null in case there are no more items. + */ + private void prefetchNext() { + // reset + next = null; + while (next == null && pos < idList.size()) { + ItemId id = idList.get(pos); + try { + if (parentId != null) { + next = itemMgr.getNode((NodeId) id, parentId); + } else { + next = itemMgr.getItem(id); + } + } catch (ItemNotFoundException e) { + log.debug("ignoring nonexistent item " + id); + // remove invalid id + idList.remove(pos); + + // maybe fix the root cause + if (parentId != null && sessionContext.getSessionImpl().autoFixCorruptions()) { + try { + // it might be an access right problem + // we need to check if the item doesn't exist in the ism + ItemStateManager ism = sessionContext.getItemStateManager(); + if (!ism.hasItemState(id)) { + NodeImpl p = (NodeImpl) itemMgr.getItem(parentId); + p.removeChildNode((NodeId) id); + p.save(); + } + } catch (RepositoryException e2) { + log.error("could not fix repository inconsistency", e); + // ignore + } + } + + // try next + } catch (AccessDeniedException e) { + log.debug("ignoring nonexistent item " + id); + // remove invalid id + idList.remove(pos); + // try next + } catch (RepositoryException e) { + log.error("failed to fetch item " + id + ", skipping...", e); + // remove invalid id + idList.remove(pos); + // try next + } + } + } + + //---------------------------------------------------------< NodeIterator > + /** + * {@inheritDoc} + */ + public Node nextNode() { + return (Node) next(); + } + + //-----------------------------------------------------< PropertyIterator > + /** + * {@inheritDoc} + */ + public Property nextProperty() { + return (Property) next(); + } + + //--------------------------------------------------------< RangeIterator > + /** + * {@inheritDoc} + */ + public long getPosition() { + return pos; + } + + /** + * {@inheritDoc} + *

    + * Note that the size of the iterator as reported by {@link #getSize()} + * might appear to be shrinking while iterating because items that for + * some reason cannot be retrieved through this iterator are silently + * skipped, thus reducing the size of this iterator. + * + * todo better to always return -1? + */ + public long getSize() { + return idList.size(); + } + + /** + * {@inheritDoc} + */ + public void skip(long skipNum) { + if (skipNum < 0) { + throw new IllegalArgumentException("skipNum must not be negative"); + } + if (skipNum == 0) { + return; + } + if (next == null) { + throw new NoSuchElementException(); + } + + // reset + next = null; + // skip the first (skipNum - 1) items without actually retrieving them + while (--skipNum > 0) { + pos++; + if (pos >= idList.size()) { + // skipped past last item + throw new NoSuchElementException(); + } + ItemId id = idList.get(pos); + // eliminate invalid items from this iterator + while (!itemMgr.itemExists(id)) { + log.debug("ignoring nonexistent item " + id); + // remove invalid id + idList.remove(pos); + if (pos >= idList.size()) { + // skipped past last item + throw new NoSuchElementException(); + } + id = idList.get(pos); + } + } + // prefetch final item (the one to be returned on next()) + pos++; + prefetchNext(); + } + + //-------------------------------------------------------------< Iterator > + /** + * {@inheritDoc} + */ + public boolean hasNext() { + return next != null; + } + + /** + * {@inheritDoc} + */ + public Object next() { + if (next == null) { + throw new NoSuchElementException(); + } + Item item = next; + pos++; + prefetchNext(); + return item; + } + + /** + * {@inheritDoc} + * + * @throws UnsupportedOperationException always since not implemented + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/LowPriorityTask.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/LowPriorityTask.java new file mode 100644 index 00000000000..caf0c7ab219 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/LowPriorityTask.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.core; + +/** + * Interface for low priority tasks (like text extraction) that can be scheduled + * later based on the extractor's load + * + * @see JCR-3146. + */ +public interface LowPriorityTask extends Runnable { +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NamespaceRegistryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NamespaceRegistryImpl.java new file mode 100644 index 00000000000..f76fcbd482d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NamespaceRegistryImpl.java @@ -0,0 +1,526 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import org.apache.jackrabbit.core.cluster.NamespaceEventChannel; +import org.apache.jackrabbit.core.cluster.NamespaceEventListener; +import org.apache.jackrabbit.core.fs.BasedFileSystem; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.util.StringIndex; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.util.XMLChar; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Properties; + +import javax.jcr.AccessDeniedException; +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +/** + * A NamespaceRegistryImpl ... + */ +public class NamespaceRegistryImpl implements + NamespaceRegistry, NamespaceEventListener, StringIndex { + + private static Logger log = LoggerFactory.getLogger(NamespaceRegistryImpl.class); + + /** + * Special property key string to be used instead of an empty key to + * avoid problems with Java implementations that have problems with + * empty keys in property files. The selected value ({@value}) would be + * invalid as either a namespace prefix or a URI, so there's little fear + * of accidental collisions. + * + * @see JCR-888 + */ + private static final String EMPTY_KEY = ".empty.key"; + + private static final String NS_REG_RESOURCE = "ns_reg.properties"; + private static final String NS_IDX_RESOURCE = "ns_idx.properties"; + + private static final HashSet reservedPrefixes = new HashSet(); + private static final HashSet reservedURIs = new HashSet(); + + static { + // reserved prefixes + reservedPrefixes.add(Name.NS_XML_PREFIX); + reservedPrefixes.add(Name.NS_XMLNS_PREFIX); + // predefined (e.g. built-in) prefixes + reservedPrefixes.add(Name.NS_REP_PREFIX); + reservedPrefixes.add(Name.NS_JCR_PREFIX); + reservedPrefixes.add(Name.NS_NT_PREFIX); + reservedPrefixes.add(Name.NS_MIX_PREFIX); + reservedPrefixes.add(Name.NS_SV_PREFIX); + // reserved namespace URI's + reservedURIs.add(Name.NS_XML_URI); + reservedURIs.add(Name.NS_XMLNS_URI); + // predefined (e.g. built-in) namespace URI's + reservedURIs.add(Name.NS_REP_URI); + reservedURIs.add(Name.NS_JCR_URI); + reservedURIs.add(Name.NS_NT_URI); + reservedURIs.add(Name.NS_MIX_URI); + reservedURIs.add(Name.NS_SV_URI); + } + + private HashMap prefixToURI = new HashMap(); + private HashMap uriToPrefix = new HashMap(); + + private HashMap indexToURI = new HashMap(); + private HashMap uriToIndex = new HashMap(); + + private final FileSystem nsRegStore; + + /** + * Namespace event channel. + */ + private NamespaceEventChannel eventChannel; + + /** + * Protected constructor: Constructs a new instance of this class. + * + * @param fs repository file system + * @throws RepositoryException + */ + public NamespaceRegistryImpl(FileSystem fs) throws RepositoryException { + this.nsRegStore = new BasedFileSystem(fs, "/namespaces"); + load(); + } + + /** + * Clears all mappings. + */ + private void clear() { + prefixToURI.clear(); + uriToPrefix.clear(); + indexToURI.clear(); + uriToIndex.clear(); + } + + /** + * Adds a new mapping and automatically assigns a new index. + * + * @param prefix the namespace prefix + * @param uri the namespace uri + */ + private void map(String prefix, String uri) { + map(prefix, uri, null); + } + + /** + * Adds a new mapping and uses the given index if specified. + * + * @param prefix the namespace prefix + * @param uri the namespace uri + * @param idx the index or null. + */ + private void map(String prefix, String uri, Integer idx) { + prefixToURI.put(prefix, uri); + uriToPrefix.put(uri, prefix); + if (!uriToIndex.containsKey(uri)) { + if (idx == null) { + // Need to use only 24 bits, since that's what + // the BundleBinding class stores in bundles + idx = uri.hashCode() & 0x00ffffff; + while (indexToURI.containsKey(idx)) { + idx = (idx + 1) & 0x00ffffff; + } + } + indexToURI.put(idx, uri); + uriToIndex.put(uri, idx); + } + } + + private void load() throws RepositoryException { + FileSystemResource propFile = + new FileSystemResource(nsRegStore, NS_REG_RESOURCE); + FileSystemResource idxFile = + new FileSystemResource(nsRegStore, NS_IDX_RESOURCE); + try { + if (!propFile.exists()) { + // clear existing mappings + clear(); + + // default namespace (if no prefix is specified) + map(Name.NS_EMPTY_PREFIX, Name.NS_DEFAULT_URI); + + // declare the predefined mappings + // rep: + map(Name.NS_REP_PREFIX, Name.NS_REP_URI); + // jcr: + map(Name.NS_JCR_PREFIX, Name.NS_JCR_URI); + // nt: + map(Name.NS_NT_PREFIX, Name.NS_NT_URI); + // mix: + map(Name.NS_MIX_PREFIX, Name.NS_MIX_URI); + // sv: + map(Name.NS_SV_PREFIX, Name.NS_SV_URI); + // xml: + map(Name.NS_XML_PREFIX, Name.NS_XML_URI); + + // persist mappings + store(); + return; + } + + // check if index file exists + Properties indexes = new Properties(); + if (idxFile.exists()) { + InputStream in = idxFile.getInputStream(); + try { + indexes.load(in); + } finally { + in.close(); + } + } + + InputStream in = propFile.getInputStream(); + try { + Properties props = new Properties(); + props.load(in); + + // clear existing mappings + clear(); + + // read mappings from properties + for (Object p : props.keySet()) { + String prefix = (String) p; + String uri = props.getProperty(prefix); + String idx = indexes.getProperty(escapePropertyKey(uri)); + // JCR-888: Backwards compatibility check + if (idx == null && uri.equals("")) { + idx = indexes.getProperty(uri); + } + if (idx != null) { + map(unescapePropertyKey(prefix), uri, Integer.decode(idx)); + } else { + map(unescapePropertyKey(prefix), uri); + } + } + } finally { + in.close(); + } + if (!idxFile.exists()) { + store(); + } + } catch (Exception e) { + String msg = "failed to load namespace registry"; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + private void store() throws RepositoryException { + FileSystemResource propFile = + new FileSystemResource(nsRegStore, NS_REG_RESOURCE); + try { + propFile.makeParentDirs(); + OutputStream os = propFile.getOutputStream(); + Properties props = new Properties(); + + // store mappings in properties + for (String prefix : prefixToURI.keySet()) { + String uri = prefixToURI.get(prefix); + props.setProperty(escapePropertyKey(prefix), uri); + } + + try { + props.store(os, null); + } finally { + // make sure stream is closed + os.close(); + } + } catch (Exception e) { + String msg = "failed to persist namespace registry"; + log.debug(msg); + throw new RepositoryException(msg, e); + } + + FileSystemResource indexFile = + new FileSystemResource(nsRegStore, NS_IDX_RESOURCE); + try { + indexFile.makeParentDirs(); + OutputStream os = indexFile.getOutputStream(); + Properties props = new Properties(); + + // store mappings in properties + for (String uri : uriToIndex.keySet()) { + String index = uriToIndex.get(uri).toString(); + props.setProperty(escapePropertyKey(uri), index); + } + + try { + props.store(os, null); + } finally { + // make sure stream is closed + os.close(); + } + } catch (Exception e) { + String msg = "failed to persist namespace registry index."; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + /** + * Replaces an empty string with the special {@link #EMPTY_KEY} value. + * + * @see #unescapePropertyKey(String) + * @param key property key + * @return escaped property key + */ + private String escapePropertyKey(String key) { + if (key.equals("")) { + return EMPTY_KEY; + } else { + return key; + } + } + + /** + * Converts the special {@link #EMPTY_KEY} value back to an empty string. + * + * @see #escapePropertyKey(String) + * @param key property key + * @return escaped property key + */ + private String unescapePropertyKey(String key) { + if (key.equals(EMPTY_KEY)) { + return ""; + } else { + return key; + } + } + + /** + * Set an event channel to inform about changes. + * + * @param eventChannel event channel + */ + public void setEventChannel(NamespaceEventChannel eventChannel) { + this.eventChannel = eventChannel; + eventChannel.setListener(this); + } + + /** + * Returns true if the specified uri is one of the reserved + * URIs defined in this registry. + * + * @param uri The URI to test. + * @return true if the specified uri is reserved; + * false otherwise. + */ + public boolean isReservedURI(String uri) { + return reservedURIs.contains(uri); + } + + //-------------------------------------------------------< StringIndex >-- + + /** + * Returns the index (i.e. stable prefix) for the given namespace URI. + * + * @param uri namespace URI + * @return namespace index + * @throws IllegalArgumentException if the namespace is not registered + */ + public int stringToIndex(String uri) { + Integer idx = uriToIndex.get(uri); + if (idx == null) { + throw new IllegalArgumentException("Namespace not registered: " + uri); + } + return idx; + } + + /** + * Returns the namespace URI for a given index (i.e. stable prefix). + * + * @param idx namespace index + * @return namespace URI + * @throws IllegalArgumentException if the given index is invalid + */ + public String indexToString(int idx) { + String uri = indexToURI.get(idx); + if (uri == null) { + throw new IllegalArgumentException("Invalid namespace index: " + idx); + } + return uri; + } + + //----------------------------------------------------< NamespaceRegistry > + /** + * {@inheritDoc} + */ + public synchronized void registerNamespace(String prefix, String uri) + throws NamespaceException, UnsupportedRepositoryOperationException, + AccessDeniedException, RepositoryException { + if (prefix == null || uri == null) { + throw new IllegalArgumentException("prefix/uri can not be null"); + } + if (Name.NS_EMPTY_PREFIX.equals(prefix) || Name.NS_DEFAULT_URI.equals(uri)) { + throw new NamespaceException("default namespace is reserved and can not be changed"); + } + if (reservedURIs.contains(uri)) { + throw new NamespaceException("failed to register namespace " + + prefix + " -> " + uri + ": reserved URI"); + } + if (reservedPrefixes.contains(prefix)) { + throw new NamespaceException("failed to register namespace " + + prefix + " -> " + uri + ": reserved prefix"); + } + // special case: prefixes xml* + if (prefix.toLowerCase().startsWith(Name.NS_XML_PREFIX)) { + throw new NamespaceException("failed to register namespace " + + prefix + " -> " + uri + ": reserved prefix"); + } + // check if the prefix is a valid XML prefix + if (!XMLChar.isValidNCName(prefix)) { + throw new NamespaceException("failed to register namespace " + + prefix + " -> " + uri + ": invalid prefix"); + } + + // check existing mappings + String oldPrefix = uriToPrefix.get(uri); + if (prefix.equals(oldPrefix)) { + throw new NamespaceException("failed to register namespace " + + prefix + " -> " + uri + ": mapping already exists"); + } + if (prefixToURI.containsKey(prefix)) { + /** + * prevent remapping of existing prefixes because this would in effect + * remove the previously assigned namespace; + * as we can't guarantee that there are no references to this namespace + * (in names of nodes/properties/node types etc.) we simply don't allow it. + */ + throw new NamespaceException("failed to register namespace " + + prefix + " -> " + uri + + ": remapping existing prefixes is not supported."); + } + + if (oldPrefix != null) { + // remove old prefix mapping + prefixToURI.remove(oldPrefix); + uriToPrefix.remove(uri); + } + + // add new prefix mapping + map(prefix, uri); + + if (eventChannel != null) { + eventChannel.remapped(oldPrefix, prefix, uri); + } + + // persist mappings + store(); + } + + /** + * {@inheritDoc} + */ + public void unregisterNamespace(String prefix) + throws NamespaceException, UnsupportedRepositoryOperationException, + AccessDeniedException, RepositoryException { + if (reservedPrefixes.contains(prefix)) { + throw new NamespaceException("reserved prefix: " + prefix); + } + if (!prefixToURI.containsKey(prefix)) { + throw new NamespaceException("unknown prefix: " + prefix); + } + /** + * as we can't guarantee that there are no references to the specified + * namespace (in names of nodes/properties/node types etc.) we simply + * don't allow it. + */ + throw new NamespaceException("unregistering namespaces is not supported."); + } + + /** + * {@inheritDoc} + */ + public String[] getPrefixes() throws RepositoryException { + return prefixToURI.keySet().toArray(new String[prefixToURI.keySet().size()]); + } + + /** + * {@inheritDoc} + */ + public String[] getURIs() throws RepositoryException { + return uriToPrefix.keySet().toArray(new String[uriToPrefix.keySet().size()]); + } + + /** + * {@inheritDoc} + */ + public String getURI(String prefix) throws NamespaceException { + String uri = prefixToURI.get(prefix); + if (uri == null) { + throw new NamespaceException(prefix + + ": is not a registered namespace prefix."); + } + return uri; + } + + /** + * {@inheritDoc} + */ + public String getPrefix(String uri) throws NamespaceException { + String prefix = uriToPrefix.get(uri); + if (prefix == null) { + throw new NamespaceException(uri + + ": is not a registered namespace uri."); + } + return prefix; + } + + //-----------------------------------------------< NamespaceEventListener > + + /** + * {@inheritDoc} + */ + public void externalRemap(String oldPrefix, String newPrefix, String uri) + throws RepositoryException { + + if (newPrefix == null) { + /** + * as we can't guarantee that there are no references to the specified + * namespace (in names of nodes/properties/node types etc.) we simply + * don't allow it. + */ + throw new NamespaceException("unregistering namespaces is not supported."); + } + + if (oldPrefix != null) { + // remove old prefix mapping + prefixToURI.remove(oldPrefix); + uriToPrefix.remove(uri); + } + + // add new prefix mapping + map(newPrefix, uri); + + // persist mappings + store(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeData.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeData.java new file mode 100644 index 00000000000..f5fa9f85c41 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeData.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.core; + +import org.apache.jackrabbit.core.state.NodeState; + +/** + * Data object representing a node. Used for non-shareable nodes or for the + * first node in a shared set. For every share-sibling, NodeDataRef + * is used instead. + */ +class NodeData extends AbstractNodeData { + + /** + * Create a new instance of this class. + * + * @param state node state + * @param itemMgr item manager + */ + NodeData(NodeState state, ItemManager itemMgr) { + super(state, itemMgr); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeDataRef.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeDataRef.java new file mode 100644 index 00000000000..e87fb7f28e6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeDataRef.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.core; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ItemState; + +/** + * Data object representing a node. Used for share-siblings of a shareable node + * that is already loaded. + */ +class NodeDataRef extends AbstractNodeData { + + /** Referenced data object */ + private final AbstractNodeData data; + + /** + * Create a new instance of this class. + * + * @param data data to reference + * @param primaryParentId primary parent id + */ + protected NodeDataRef(AbstractNodeData data, NodeId primaryParentId) { + super(data.getId()); + + this.data = data; + + setPrimaryParentId(primaryParentId); + } + + /** + * {@inheritDoc} + * + * This implementation returns the state of the referenced data object. + */ + public ItemState getState() { + return data.getState(); + } + + /** + * {@inheritDoc} + * + * This implementation sets the state of the referenced data object. + */ + protected void setState(ItemState state) { + data.setState(state); + } + + /** + * {@inheritDoc} + * + * This implementation returns the definition of the referenced data object. + * @throws RepositoryException if the definition cannot be retrieved. + */ + public ItemDefinition getDefinition() throws RepositoryException { + return data.getDefinition(); + } + + /** + * {@inheritDoc} + * + * This implementation sets the definition of the referenced data object. + */ + protected void setDefinition(ItemDefinition definition) { + data.setDefinition(definition); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeImpl.java new file mode 100644 index 00000000000..3519d9211d6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeImpl.java @@ -0,0 +1,3917 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import static javax.jcr.PropertyType.STRING; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_CURRENT_LIFECYCLE_STATE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_ISCHECKEDOUT; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_LIFECYCLE_POLICY; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_LIFECYCLE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_REFERENCEABLE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_SIMPLE_VERSIONABLE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_VERSIONABLE; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Calendar; +import java.util.Collection; +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 java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Binary; +import javax.jcr.InvalidItemStateException; +import javax.jcr.InvalidLifecycleTransitionException; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.ItemVisitor; +import javax.jcr.NamespaceException; +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.lock.LockException; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.api.JackrabbitNode; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.query.QueryManagerImpl; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.AddNodeOperation; +import org.apache.jackrabbit.core.session.NodeNameNormalizer; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.core.session.SessionWriteOperation; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; +import org.apache.jackrabbit.util.ChildrenCollectorFilter; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.value.ValueHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NodeImpl implements the Node interface. + */ +public class NodeImpl extends ItemImpl implements Node, JackrabbitNode { + + private static Logger log = LoggerFactory.getLogger(NodeImpl.class); + + // flag set in status passed to getOrCreateProperty if property was created + protected static final short CREATED = 0; + + /** node data (avoids casting ItemImpl.data) */ + private final AbstractNodeData data; + + /** + * Protected constructor. + * + * @param itemMgr the ItemManager that created this Node instance + * @param sessionContext the component context of the associated session + * @param data the node data + */ + protected NodeImpl( + ItemManager itemMgr, SessionContext sessionContext, + AbstractNodeData data) { + super(itemMgr, sessionContext, data); + this.data = data; + // paranoid sanity check + NodeTypeRegistry ntReg = sessionContext.getNodeTypeRegistry(); + final NodeState state = data.getNodeState(); + if (!ntReg.isRegistered(state.getNodeTypeName())) { + /** + * todo need proper way of handling inconsistent/corrupt node type references + * e.g. 'flag' nodes that refer to non-registered node types + */ + log.warn("Fallback to nt:unstructured due to unknown node type '" + + state.getNodeTypeName() + "' of " + this); + data.getNodeState().setNodeTypeName(NameConstants.NT_UNSTRUCTURED); + } + List unknown = null; + for (Name mixinName : state.getMixinTypeNames()) { + if (!ntReg.isRegistered(mixinName)) { + if (unknown == null) { + unknown = new ArrayList(); + } + unknown.add(mixinName); + log.warn("Ignoring unknown mixin type '" + mixinName + + "' of " + this); + } + } + if (unknown != null) { + // ignore unknown mixin type names + Set known = new HashSet(state.getMixinTypeNames()); + known.removeAll(unknown); + state.setMixinTypeNames(known); + } + } + + /** + * Returns the node-state associated with this node. + * + * @return state associated with this node + */ + NodeState getNodeState() { + return data.getNodeState(); + } + + /** + * Returns the id of the property at relPath or null + * if no property exists at relPath. + *

    + * Note that access rights are not checked. + * + * @param relPath relative path of a (possible) property + * @return the id of the property at relPath or + * null if no property exists at relPath + * @throws RepositoryException if relPath is not a valid + * relative path + */ + protected PropertyId resolveRelativePropertyPath(String relPath) + throws RepositoryException { + Path p = resolveRelativePath(relPath); + return getPropertyId(p); + } + + /** + * Returns the id of the node at relPath or null + * if no node exists at relPath. + *

    + * Note that access rights are not checked. + * + * @param relPath relative path of a (possible) node + * @return the id of the node at relPath or + * null if no node exists at relPath + * @throws RepositoryException if relPath is not a valid + * relative path + */ + protected NodeId resolveRelativeNodePath(String relPath) + throws RepositoryException { + + Path p = resolveRelativePath(relPath); + return getNodeId(p); + } + + /** + * Resolve a relative path given as string into a Path. If + * a NameException occurs, it will be rethrown embedded + * into a RepositoryException + * + * @param relPath relative path + * @return Path object + * @throws RepositoryException if an error occurs + */ + private Path resolveRelativePath(String relPath) throws RepositoryException { + try { + return sessionContext.getQPath(relPath); + } catch (NameException e) { + throw new RepositoryException( + "Failed to resolve path " + relPath + + " relative to " + this, e); + } + } + + /** + * Returns the id of the node at p or null + * if no node exists at p. + *

    + * Note that access rights are not checked. + * + * @param p relative path of a (possible) node + * @return the id of the node at p or + * null if no node exists at p + * @throws RepositoryException if relPath is not a valid + * relative path + */ + private NodeId getNodeId(Path p) throws RepositoryException { + if (p.getLength() == 1 && p.denotesName()) { + // check if node entry exists + ChildNodeEntry cne = data.getNodeState().getChildNodeEntry( + p.getName(), p.getNormalizedIndex()); + if (cne != null) { + return cne.getId(); + } else { + return null; // there's no child node with that name + } + } else { + // build and resolve absolute path + try { + p = PathFactoryImpl.getInstance().create( + getPrimaryPath(), p, true); + } catch (RepositoryException re) { + // failed to build canonical path + return null; + } + return sessionContext.getHierarchyManager().resolveNodePath(p); + } + } + + /** + * Returns the id of the property at p or null + * if no node exists at p. + *

    + * Note that access rights are not checked. + * + * @param p relative path of a (possible) node + * @return the id of the node at p or + * null if no node exists at p + * @throws RepositoryException if relPath is not a valid + * relative path + */ + private PropertyId getPropertyId(Path p) throws RepositoryException { + if (p.getLength() == 1 && p.denotesName()) { + // check if property entry exists + NodeState thisState = data.getNodeState(); + if (p.getIndex() == Path.INDEX_UNDEFINED + && thisState.hasPropertyName(p.getName())) { + return new PropertyId(thisState.getNodeId(), p.getName()); + } else { + return null; // there's no property with that name + } + } else { + // build and resolve absolute path + try { + p = PathFactoryImpl.getInstance().create( + getPrimaryPath(), p, true); + } catch (RepositoryException re) { + // failed to build canonical path + return null; + } + return sessionContext.getHierarchyManager().resolvePropertyPath(p); + } + } + + /** + * Determines if there are pending unsaved changes either on this + * node or on any node or property in the subtree below it. + * + * @return true if there are pending unsaved changes, + * false otherwise. + * @throws RepositoryException if an error occurred + */ + protected boolean hasPendingChanges() throws RepositoryException { + if (isTransient()) { + return true; + } + return !stateMgr.getDescendantTransientItemStates(id).isEmpty(); + } + + @Override + protected synchronized ItemState getOrCreateTransientItemState() + throws RepositoryException { + + synchronized (data) { + if (!isTransient()) { + try { + // make transient (copy-on-write) + NodeState transientState = + stateMgr.createTransientNodeState( + (NodeState) stateMgr.getItemState(getId()), ItemState.STATUS_EXISTING_MODIFIED); + // replace persistent with transient state + data.setState(transientState); + } catch (ItemStateException ise) { + String msg = "failed to create transient state"; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + return getItemState(); + } + } + + /** + * @param name + * @param type + * @param multiValued + * @param exactTypeMatch + * @param status + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found + * @throws RepositoryException if another error occurs + */ + protected PropertyImpl getOrCreateProperty(String name, int type, + boolean multiValued, + boolean exactTypeMatch, + BitSet status) + throws ConstraintViolationException, RepositoryException { + try { + return getOrCreateProperty( + sessionContext.getQName(name), type, + multiValued, exactTypeMatch, status); + } catch (NameException e) { + throw new RepositoryException("invalid property name: " + name, e); + } + } + + /** + * @param name + * @param type + * @param multiValued + * @param exactTypeMatch + * @param status + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found + * @throws RepositoryException if another error occurs + */ + protected synchronized PropertyImpl getOrCreateProperty(Name name, int type, + boolean multiValued, + boolean exactTypeMatch, + BitSet status) + throws ConstraintViolationException, RepositoryException { + status.clear(); + + if (isNew() && !hasProperty(name)) { + // this is a new node and the property does not exist yet + // -> no need to check item manager + PropertyDefinitionImpl def = getApplicablePropertyDefinition( + name, type, multiValued, exactTypeMatch); + PropertyImpl prop = createChildProperty(name, type, def); + status.set(CREATED); + return prop; + } + + + /* + * Please note, that this implementation does not win a price for beauty + * or speed. It's never a good idea to use exceptions for semantical + * control flow. + * However, compared to the previous version, this one is thread save + * and makes the test/get block atomic in respect to transactional + * commits. the test/set can still fail. + * + * Old Version: + + NodeState thisState = (NodeState) state; + if (thisState.hasPropertyName(name)) { + /** + * the following call will throw ItemNotFoundException if the + * current session doesn't have read access + / + return getProperty(name); + } + [...create block...] + + */ + PropertyId propId = new PropertyId(getNodeId(), name); + try { + return (PropertyImpl) itemMgr.getItem(propId); + } catch (AccessDeniedException ade) { + throw new ItemNotFoundException(name.toString()); + } catch (ItemNotFoundException e) { + // does not exist yet or has been removed transiently: + // find definition for the specified property and (re-)create property + PropertyDefinitionImpl def = getApplicablePropertyDefinition( + name, type, multiValued, exactTypeMatch); + PropertyImpl prop; + if (stateMgr.hasTransientItemStateInAttic(propId)) { + // remove from attic + try { + stateMgr.disposeTransientItemStateInAttic(stateMgr.getAttic().getItemState(propId)); + } catch (ItemStateException ise) { + // shouldn't happen because we checked if it is in the attic + throw new RepositoryException(ise); + } + prop = (PropertyImpl) itemMgr.getItem(propId); + PropertyState state = (PropertyState) prop.getOrCreateTransientItemState(); + state.setMultiValued(multiValued); + state.setType(type); + getNodeState().addPropertyName(name); + } else { + prop = createChildProperty(name, type, def); + } + status.set(CREATED); + return prop; + } + } + + /** + * Creates a new property with the given name and type hint and + * property definition. If the given property definition is not of type + * UNDEFINED, then it takes precedence over the + * type hint. + * + * @param name the name of the property to create. + * @param type the type hint. + * @param def the associated property definition. + * @return the property instance. + * @throws RepositoryException if the property cannot be created. + */ + protected synchronized PropertyImpl createChildProperty(Name name, int type, + PropertyDefinitionImpl def) + throws RepositoryException { + + // create a new property state + PropertyState propState; + try { + QPropertyDefinition propDef = def.unwrap(); + if (def.getRequiredType() != PropertyType.UNDEFINED) { + type = def.getRequiredType(); + } + propState = + stateMgr.createTransientPropertyState(getNodeId(), name, + ItemState.STATUS_NEW); + propState.setType(type); + propState.setMultiValued(propDef.isMultiple()); + // compute system generated values if necessary + String userId = sessionContext.getSessionImpl().getUserID(); + new NodeTypeInstanceHandler(userId).setDefaultValues( + propState, data.getNodeState(), propDef); + } catch (ItemStateException ise) { + String msg = "failed to add property " + name + " to " + this; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + + // create Property instance wrapping new property state + // NOTE: since the property is not yet connected to its parent, avoid + // calling ItemManager#getItem(ItemId) which may include a permission + // check (with subsequent usage of the hierarachy-mgr -> error). + // just let the mgr create the new property that is known to exist and + // which has not been accessed before. + PropertyImpl prop = (PropertyImpl) itemMgr.createItemInstance(propState); + + // modify the state of 'this', i.e. the parent node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + // add new property entry + thisState.addPropertyName(name); + + return prop; + } + + protected synchronized NodeImpl createChildNode(Name name, + NodeTypeImpl nodeType, + NodeId id) + throws RepositoryException { + // create a new node state + NodeState nodeState = stateMgr.createTransientNodeState( + id, nodeType.getQName(), getNodeId(), ItemState.STATUS_NEW); + + // create Node instance wrapping new node state + NodeImpl node; + try { + // NOTE: since the node is not yet connected to its parent, avoid + // calling ItemManager#getItem(ItemId) which may include a permission + // check (with subsequent usage of the hierarachy-mgr -> error). + // just let the mgr create the new node that is known to exist and + // which has not been accessed before. + node = (NodeImpl) itemMgr.createItemInstance(nodeState); + } catch (RepositoryException re) { + // something went wrong + stateMgr.disposeTransientItemState(nodeState); + // re-throw + throw re; + } + + // modify the state of 'this', i.e. the parent node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + // add new child node entry + thisState.addChildNodeEntry(name, nodeState.getNodeId()); + + // add 'auto-create' properties defined in node type + for (PropertyDefinition aPda : nodeType.getAutoCreatedPropertyDefinitions()) { + PropertyDefinitionImpl pd = (PropertyDefinitionImpl) aPda; + node.createChildProperty(pd.unwrap().getName(), pd.getRequiredType(), pd); + } + + // recursively add 'auto-create' child nodes defined in node type + for (NodeDefinition aNda : nodeType.getAutoCreatedNodeDefinitions()) { + NodeDefinitionImpl nd = (NodeDefinitionImpl) aNda; + node.createChildNode(nd.unwrap().getName(), (NodeTypeImpl) nd.getDefaultPrimaryType(), null); + } + + return node; + } + + /** + * + * @param oldName + * @param index + * @param id + * @param newName + * @throws RepositoryException + * @deprecated use #renameChildNode(NodeId, Name, boolean) + */ + protected void renameChildNode(Name oldName, int index, NodeId id, + Name newName) + throws RepositoryException { + renameChildNode(id, newName, false); + } + + /** + * + * @param id + * @param newName + * @param replace + * @throws RepositoryException + */ + protected void renameChildNode(NodeId id, Name newName, boolean replace) + throws RepositoryException { + // modify the state of 'this', i.e. the parent node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + if (replace) { + // rename the specified child node by replacing the old + // child node entry with a new one at the same relative position + thisState.replaceChildNodeEntry(id, newName, id); + } else { + // rename the specified child node by removing the old and adding + // a new child node entry. + thisState.renameChildNodeEntry(id, newName); + } + } + + protected void removeChildProperty(Name propName) throws RepositoryException { + // modify the state of 'this', i.e. the parent node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + + // remove the property entry + if (!thisState.removePropertyName(propName)) { + String msg = "failed to remove property " + propName + " of " + this; + log.debug(msg); + throw new RepositoryException(msg); + } + + // remove property + PropertyId propId = new PropertyId(thisState.getNodeId(), propName); + itemMgr.getItem(propId).setRemoved(); + } + + protected void removeChildNode(NodeId childId) throws RepositoryException { + // modify the state of 'this', i.e. the parent node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + ChildNodeEntry entry = thisState.getChildNodeEntry(childId); + if (entry == null) { + String msg = "failed to remove child " + childId + " of " + this; + log.debug(msg); + throw new RepositoryException(msg); + } + + // notify target of removal + try { + NodeImpl childNode = itemMgr.getNode(childId, getNodeId()); + childNode.onRemove(getNodeId()); + } catch (ItemNotFoundException e) { + boolean ignoreError = false; + if (sessionContext.getSessionImpl().autoFixCorruptions()) { + // it might be an access right problem + // we need to check if the item doesn't exist in the ism + ItemStateManager ism = sessionContext.getItemStateManager(); + if (!ism.hasItemState(childId)) { + log.warn("Node " + childId + " not found, ignore", e); + ignoreError = true; + } + } + if (!ignoreError) { + throw e; + } + } + + // remove the child node entry + if (!thisState.removeChildNodeEntry(childId)) { + String msg = "failed to remove child " + childId + " of " + this; + log.debug(msg); + throw new RepositoryException(msg); + } + } + + protected void onRedefine(QNodeDefinition def) throws RepositoryException { + NodeDefinitionImpl newDef = + sessionContext.getNodeTypeManager().getNodeDefinition(def); + // modify the state of 'this', i.e. the target node + getOrCreateTransientItemState(); + // set new definition + data.setDefinition(newDef); + } + + protected void onRemove(NodeId parentId) throws RepositoryException { + // modify the state of 'this', i.e. the target node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + + // remove this node from its shared set + if (thisState.isShareable()) { + if (thisState.removeShare(parentId) > 0) { + // this state is still connected to some parents, so + // leave the child node entries and properties + + // set state of this instance to 'invalid' + data.setStatus(STATUS_INVALIDATED); + // notify the item manager that this instance has been + // temporarily invalidated + itemMgr.itemInvalidated(id, data); + return; + } + } + + if (thisState.hasChildNodeEntries()) { + // remove child nodes + // use temp array to avoid ConcurrentModificationException + ArrayList tmp = new ArrayList(thisState.getChildNodeEntries()); + // remove from tail to avoid problems with same-name siblings + for (int i = tmp.size() - 1; i >= 0; i--) { + ChildNodeEntry entry = + tmp.get(i); + // recursively remove child node + NodeId childId = entry.getId(); + //NodeImpl childNode = (NodeImpl) itemMgr.getItem(childId); + try { + /* omit the read-permission check upon retrieving the + child item as this is an internal call to remove the + subtree which may contain (protected) child items which + are not visible to the caller of the removal. the actual + validation of the remove permission however is only + executed during Item.save(). + */ + NodeImpl childNode = itemMgr.getNode(childId, getNodeId(), false); + childNode.onRemove(thisState.getNodeId()); + // remove the child node entry + } catch (ItemNotFoundException e) { + boolean ignoreError = false; + if (parentId != null && sessionContext.getSessionImpl().autoFixCorruptions()) { + // it might be an access right problem + // we need to check if the item doesn't exist in the ism + ItemStateManager ism = sessionContext.getItemStateManager(); + if (!ism.hasItemState(childId)) { + log.warn("Child named " + entry.getName() + " (index " + entry.getIndex() + ", " + + "node id " + childId + ") " + + "not found when trying to remove " + getPath() + " " + + "(node id " + getNodeId() + ") - ignored", e); + ignoreError = true; + } + } + if (!ignoreError) { + throw e; + } + } + thisState.removeChildNodeEntry(childId); + } + } + + // remove properties + // use temp set to avoid ConcurrentModificationException + HashSet tmp = new HashSet(thisState.getPropertyNames()); + for (Name propName : tmp) { + // remove the property entry + thisState.removePropertyName(propName); + // remove property + PropertyId propId = new PropertyId(thisState.getNodeId(), propName); + /* omit the read-permission check upon retrieving the + child item as this is an internal call to remove the + subtree which may contain (protected) child items which + are not visible to the caller of the removal. the actual + validation of the remove permission however is only + executed during Item.save(). + */ + itemMgr.getItem(propId, false).setRemoved(); + } + + // finally remove this node + thisState.setParentId(null); + setRemoved(); + } + + void setMixinTypesProperty(Set mixinNames) throws RepositoryException { + NodeState thisState = data.getNodeState(); + // get or create jcr:mixinTypes property + PropertyImpl prop; + if (thisState.hasPropertyName(NameConstants.JCR_MIXINTYPES)) { + prop = (PropertyImpl) itemMgr.getItem(new PropertyId(thisState.getNodeId(), NameConstants.JCR_MIXINTYPES)); + } else { + // find definition for the jcr:mixinTypes property and create property + PropertyDefinitionImpl def = getApplicablePropertyDefinition( + NameConstants.JCR_MIXINTYPES, PropertyType.NAME, true, true); + prop = createChildProperty(NameConstants.JCR_MIXINTYPES, PropertyType.NAME, def); + } + + if (mixinNames.isEmpty()) { + // purge empty jcr:mixinTypes property + removeChildProperty(NameConstants.JCR_MIXINTYPES); + return; + } + + // call internalSetValue for setting the jcr:mixinTypes property + // to avoid checking of the 'protected' flag + InternalValue[] vals = new InternalValue[mixinNames.size()]; + Iterator iter = mixinNames.iterator(); + int cnt = 0; + while (iter.hasNext()) { + vals[cnt++] = InternalValue.create(iter.next()); + } + prop.internalSetValue(vals, PropertyType.NAME); + } + + /** + * Returns the Names of this node's mixin types. + * + * @return a set of the Names of this node's mixin types. + */ + public Set getMixinTypeNames() { + return data.getNodeState().getMixinTypeNames(); + } + + /** + * Returns the effective (i.e. merged and resolved) node type representation + * of this node's primary and mixin node types. + * + * @return the effective node type + * @throws RepositoryException if an error occurs + */ + public EffectiveNodeType getEffectiveNodeType() throws RepositoryException { + try { + return sessionContext.getNodeTypeRegistry().getEffectiveNodeType( + data.getNodeState().getNodeTypeName(), + data.getNodeState().getMixinTypeNames()); + } catch (NodeTypeConflictException ntce) { + String msg = "Failed to build effective node type for " + this; + log.debug(msg); + throw new RepositoryException(msg, ntce); + } + } + + /** + * Returns the applicable child node definition for a child node with the + * specified name and node type. + * + * @param nodeName + * @param nodeTypeName + * @return + * @throws ConstraintViolationException if no applicable child node definition + * could be found + * @throws RepositoryException if another error occurs + */ + protected NodeDefinitionImpl getApplicableChildNodeDefinition(Name nodeName, + Name nodeTypeName) + throws ConstraintViolationException, RepositoryException { + NodeTypeManagerImpl ntMgr = sessionContext.getNodeTypeManager(); + QNodeDefinition cnd = getEffectiveNodeType().getApplicableChildNodeDef( + nodeName, nodeTypeName, sessionContext.getNodeTypeRegistry()); + return ntMgr.getNodeDefinition(cnd); + } + + /** + * Returns the applicable property definition for a property with the + * specified name and type. + * + * @param propertyName + * @param type + * @param multiValued + * @param exactTypeMatch + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found + * @throws RepositoryException if another error occurs + */ + protected PropertyDefinitionImpl getApplicablePropertyDefinition(Name propertyName, + int type, + boolean multiValued, + boolean exactTypeMatch) + throws ConstraintViolationException, RepositoryException { + QPropertyDefinition pd; + if (exactTypeMatch || type == PropertyType.UNDEFINED) { + pd = getEffectiveNodeType().getApplicablePropertyDef( + propertyName, type, multiValued); + } else { + try { + // try to find a definition with matching type first + pd = getEffectiveNodeType().getApplicablePropertyDef( + propertyName, type, multiValued); + } catch (ConstraintViolationException cve) { + // none found, now try by ignoring the type + pd = getEffectiveNodeType().getApplicablePropertyDef( + propertyName, PropertyType.UNDEFINED, multiValued); + } + } + return sessionContext.getNodeTypeManager().getPropertyDefinition(pd); + } + + @Override + protected void makePersistent() throws RepositoryException { + if (!isTransient()) { + log.debug(this + " (" + id + "): there's no transient state to persist"); + return; + } + + NodeState transientState = data.getNodeState(); + NodeState localState = stateMgr.makePersistent(transientState); + + // swap transient state with local state + data.setState(localState); + // reset status + data.setStatus(STATUS_NORMAL); + + if (isShareable() && data.getPrimaryParentId() == null) { + data.setPrimaryParentId(localState.getParentId()); + } + } + + protected void restoreTransient(NodeState transientState) + throws RepositoryException { + NodeState thisState = null; + + if (!isTransient()) { + thisState = (NodeState) getOrCreateTransientItemState(); + if (transientState.getStatus() == ItemState.STATUS_NEW + && thisState.getStatus() != ItemState.STATUS_NEW) { + thisState.setStatus(ItemState.STATUS_NEW); + stateMgr.disconnectTransientItemState(thisState); + } + thisState.setParentId(transientState.getParentId()); + thisState.setNodeTypeName(transientState.getNodeTypeName()); + } else { + // JCR-2503: Re-create transient state in the state manager, + // because it was removed + synchronized (data) { + thisState = stateMgr.createTransientNodeState( + (NodeId) transientState.getId(), + transientState.getNodeTypeName(), + transientState.getParentId(), + NodeState.STATUS_NEW); + data.setState(thisState); + } + } + + // re-apply transient changes + thisState.setMixinTypeNames(transientState.getMixinTypeNames()); + thisState.setChildNodeEntries(transientState.getChildNodeEntries()); + thisState.setPropertyNames(transientState.getPropertyNames()); + thisState.setSharedSet(transientState.getSharedSet()); + thisState.setModCount(transientState.getModCount()); + } + + /** + * Same as {@link Node#addMixin(String)} except that it takes a + * Name instead of a String. + * + * @see Node#addMixin(String) + */ + public void addMixin(Name mixinName) throws RepositoryException { + perform(new AddMixinOperation(this, mixinName)); + } + + /** + * Same as {@link Node#removeMixin(String)} except that it takes a + * Name instead of a String. + * + * @see Node#removeMixin(String) + */ + public void removeMixin(Name mixinName) throws RepositoryException { + perform(new RemoveMixinOperation(this, mixinName)); + } + + /** + * Same as {@link Node#isNodeType(String)} except that it takes a + * Name instead of a String. + * + * @param ntName name of node type + * @return true if this node is of the specified node type; + * otherwise false + */ + public boolean isNodeType(Name ntName) throws RepositoryException { + // first do trivial checks without using type hierarchy + Name primary = data.getNodeState().getNodeTypeName(); + if (ntName.equals(primary)) { + return true; + } + Set mixins = data.getNodeState().getMixinTypeNames(); + if (mixins.contains(ntName)) { + return true; + } + + // check effective node type + try { + NodeTypeRegistry registry = sessionContext.getNodeTypeRegistry(); + EffectiveNodeType type = + registry.getEffectiveNodeType(primary, mixins); + return type.includesNodeType(ntName); + } catch (NodeTypeConflictException e) { + String msg = "Failed to build effective node type for " + this; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + /** + * Checks various pre-conditions that are common to all + * setProperty() methods. The checks performed are: + *

      + *
    • this node must be checked-out
    • + *
    • this node must not be locked by somebody else
    • + *
    + * Note that certain checks are performed by the respective + * Property.setValue() methods. + * + * @throws VersionException if this node is not checked-out + * @throws LockException if this node is locked by somebody else + * @throws RepositoryException if another error occurs + * @see javax.jcr.Node#setProperty + */ + protected void checkSetProperty() + throws VersionException, LockException, RepositoryException { + // make sure this node is checked-out and is not locked + int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT; + sessionContext.getItemValidator().checkModify(this, options, Permission.NONE); + } + + /** + * Sets the internal value of a property without checking any constraints. + *

    + * Note that no type conversion is being performed, i.e. it's the caller's + * responsibility to make sure that the type of the given value is compatible + * with the specified property's definition. + * @param name + * @param value + * @return + * @throws ValueFormatException + * @throws RepositoryException + */ + protected Property internalSetProperty(Name name, InternalValue value) + throws ValueFormatException, RepositoryException { + int type; + if (value == null) { + type = PropertyType.UNDEFINED; + } else { + type = value.getType(); + } + + BitSet status = new BitSet(); + PropertyImpl prop = getOrCreateProperty(name, type, false, true, status); + try { + if (value == null) { + prop.internalSetValue(null, type); + } else { + prop.internalSetValue(new InternalValue[]{value}, type); + } + } catch (RepositoryException re) { + if (status.get(CREATED)) { + // setting value failed, get rid of newly created property + removeChildProperty(name); + } + // rethrow + throw re; + } + return prop; + } + + /** + * Sets the internal value of a property without checking any constraints. + *

    + * Note that no type conversion is being performed, i.e. it's the caller's + * responsibility to make sure that the type of the given values is compatible + * with the specified property's definition. + * + * @param name + * @param values + * @return + * @throws ValueFormatException + * @throws RepositoryException + */ + protected Property internalSetProperty(Name name, InternalValue[] values) + throws ValueFormatException, 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); + } + + /** + * Sets the internal value of a property without checking any constraints. + *

    + * Note that no type conversion is being performed, i.e. it's the caller's + * responsibility to make sure that the type of the given values is compatible + * with the specified property's definition. + * + * @param name + * @param values + * @param type + * @return + * @throws ValueFormatException + * @throws RepositoryException + */ + protected Property internalSetProperty(Name name, InternalValue[] values, + int type) + throws ValueFormatException, RepositoryException { + BitSet status = new BitSet(); + PropertyImpl prop = getOrCreateProperty(name, type, true, true, status); + try { + prop.internalSetValue(values, type); + } catch (RepositoryException re) { + if (status.get(CREATED)) { + // setting value failed, get rid of newly created property + removeChildProperty(name); + } + // rethrow + throw re; + } + return prop; + } + + /** + * Returns the child node of this node with the specified + * name. + * + * @param name The name of the child node to retrieve. + * @return The child node with the specified name. + * @throws ItemNotFoundException If no child node exists with the + * specified name. + * @throws RepositoryException If another error occurs. + */ + public NodeImpl getNode(Name name) throws ItemNotFoundException, RepositoryException { + return getNode(name, 1); + } + + /** + * Returns the child node of this node with the specified + * name. + * + * @param name The name of the child node to retrieve. + * @param index The index of the child node to retrieve (in the case of same-name siblings). + * @return The child node with the specified name. + * @throws ItemNotFoundException If no child node exists with the + * specified name. + * @throws RepositoryException If another error occurs. + */ + public NodeImpl getNode(final Name name, final int index) + throws ItemNotFoundException, RepositoryException { + return perform(new SessionOperation() { + public NodeImpl perform(SessionContext context) + throws RepositoryException { + ChildNodeEntry cne = data.getNodeState().getChildNodeEntry( + name, index != 0 ? index : 1); + if (cne != null) { + try { + return context.getItemManager().getNode( + cne.getId(), getNodeId()); + } catch (AccessDeniedException e) { + throw new ItemNotFoundException(); + } + } else { + throw new ItemNotFoundException(); + } + } + public String toString() { + return "node.getNode(" + name + "[" + index + "])"; + } + }); + } + + /** + * Indicates whether a child node with the specified name exists. + * Returns true if the child node exists and false + * otherwise. + * + * @param name The name of the child node. + * @return true if the child node exists; false otherwise. + * @throws RepositoryException If an unspecified error occurs. + */ + public boolean hasNode(Name name) throws RepositoryException { + return hasNode(name, 1); + } + + /** + * Indicates whether a child node with the specified name exists. + * Returns true if the child node exists and false + * otherwise. + * + * @param name The name of the child node. + * @param index The index of the child node (in the case of same-name siblings). + * @return true if the child node exists; false otherwise. + * @throws RepositoryException If an unspecified error occurs. + */ + public boolean hasNode(final Name name, final int index) + throws RepositoryException { + return perform(new SessionOperation() { + public Boolean perform(SessionContext context) + throws RepositoryException { + ChildNodeEntry cne = data.getNodeState().getChildNodeEntry( + name, index != 0 ? index : 1); + return cne != null + && context.getItemManager().itemExists(cne.getId()); + } + public String toString() { + return "node.hasNode(" + name + "[" + index + "])"; + } + }); + } + + /** + * Returns the property of this node with the specified + * name. + * + * @param name The name of the property to retrieve. + * @return The property with the specified name. + * @throws ItemNotFoundException If no property exists with the + * specified name. + * @throws RepositoryException If another error occurs. + */ + public PropertyImpl getProperty(final Name name) + throws ItemNotFoundException, RepositoryException { + return perform(new SessionOperation() { + public PropertyImpl perform(SessionContext context) + throws RepositoryException { + try { + return (PropertyImpl) context.getItemManager().getItem( + new PropertyId(getNodeId(), name)); + } catch (AccessDeniedException ade) { + String n = context.getJCRName(name); + throw new ItemNotFoundException( + "Property " + n + " not found"); + } + } + public String toString() { + return "node.getProperty(" + name + ")"; + } + }); + } + + /** + * Indicates whether a property with the specified name exists. + * Returns true if the property exists and false + * otherwise. + * + * @param name The name of the property. + * @return true if the property exists; false otherwise. + * @throws RepositoryException If an unspecified error occurs. + */ + public boolean hasProperty(final Name name) throws RepositoryException { + return perform(new SessionOperation() { + public Boolean perform(SessionContext context) + throws RepositoryException { + return data.getNodeState().hasPropertyName(name) + && context.getItemManager().itemExists( + new PropertyId(getNodeId(), name)); + } + public String toString() { + return "node.hasProperty(" + name + ")"; + } + }); + } + + /** + * Same as {@link Node#addNode(String, String)} except that + * this method takes Name arguments instead of + * Strings and has an additional uuid argument. + *

    + * Important Notice: This method is for internal use only! Passing + * already assigned uuid's might lead to unexpected results and + * data corruption in the worst case. + * + * @param nodeName name of the new node + * @param nodeTypeName name of the new node's node type or null + * if it should be determined automatically + * @param id id of the new node or null if a new + * id should be assigned + * @return the newly added node + * @throws RepositoryException if the node can not added + */ + // FIXME: This method should not be public + public synchronized NodeImpl addNode( + Name nodeName, Name nodeTypeName, NodeId id) + throws RepositoryException { + // check state of this instance + sanityCheck(); + + Path nodePath = PathFactoryImpl.getInstance().create( + getPrimaryPath(), nodeName, true); + + // Check the explicitly specified node type (if any) + NodeTypeImpl nt = null; + if (nodeTypeName != null) { + nt = sessionContext.getNodeTypeManager().getNodeType(nodeTypeName); + if (nt.isMixin()) { + throw new ConstraintViolationException( + "Unable to add a node with a mixin node type: " + + sessionContext.getJCRName(nodeTypeName)); + } else if (nt.isAbstract()) { + throw new ConstraintViolationException( + "Unable to add a node with an abstract node type: " + + sessionContext.getJCRName(nodeTypeName)); + } else { + // adding a node with explicit specifying the node type name + // requires the editing session to have nt_management privilege. + sessionContext.getAccessManager().checkPermission( + nodePath, Permission.NODE_TYPE_MNGMT); + } + } + + // Get the applicable child node definition for this node. + NodeDefinitionImpl def; + try { + def = getApplicableChildNodeDefinition(nodeName, nodeTypeName); + } catch (RepositoryException e) { + throw new ConstraintViolationException( + "No child node definition for " + + sessionContext.getJCRName(nodeName) + " found in " + this, e); + } + + // Use default node type from child node definition if needed + if (nt == null) { + nt = (NodeTypeImpl) def.getDefaultPrimaryType(); + } + + // check the new name + NodeNameNormalizer.check(nodeName); + + // check for name collisions + NodeState thisState = data.getNodeState(); + ChildNodeEntry cne = thisState.getChildNodeEntry(nodeName, 1); + if (cne != null) { + // there's already a child node entry with that name; + // check same-name sibling setting of new node + if (!def.allowsSameNameSiblings()) { + throw new ItemExistsException( + "This node already exists: " + + itemMgr.safeGetJCRPath(nodePath)); + } + // check same-name sibling setting of existing node + NodeImpl existing = itemMgr.getNode(cne.getId(), getNodeId()); + if (!existing.getDefinition().allowsSameNameSiblings()) { + throw new ItemExistsException( + "Same-name siblings not allowed for " + existing); + } + } + + // check protected flag of parent (i.e. this) node and retention/hold + // make sure this node is checked-out and not locked by another session. + int options = + ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT + | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD + | ItemValidator.CHECK_RETENTION; + sessionContext.getItemValidator().checkModify(this, options, Permission.NONE); + + // now do create the child node + return createChildNode(nodeName, nt, id); + } + + /** + * Same as {@link Node#setProperty(String, Value[], int)} except + * that this method takes a Name name argument instead of a + * String. + * + * @param name + * @param values + * @param type + * @return + * @throws ValueFormatException + * @throws VersionException + * @throws LockException + * @throws ConstraintViolationException + * @throws RepositoryException + */ + public PropertyImpl setProperty(Name name, Value[] values, int type) + throws ValueFormatException, VersionException, LockException, + ConstraintViolationException, RepositoryException { + return setProperty(name, values, type, true); + } + + /** + * Same as {@link Node#setProperty(String, Value)} except that + * this method takes a Name name argument instead of a + * String. + */ + public PropertyImpl setProperty(Name name, Value value) + throws RepositoryException { + return sessionContext.getSessionState().perform( + new SetPropertyOperation(name, value, false)); + } + + /** + * @see ItemImpl#getQName() + */ + @Override + public Name getQName() throws RepositoryException { + HierarchyManager hierMgr = sessionContext.getHierarchyManager(); + Name name; + + if (!isShareable()) { + name = hierMgr.getName(id); + } else { + name = hierMgr.getName(getNodeId(), getParentId()); + } + return name; + } + + /** + * Returns the identifier of this Node. + * + * @return the id of this Node + */ + public NodeId getNodeId() { + return (NodeId) id; + } + + /** + * Returns the name of the primary node type as exposed on the node state + * without retrieving the node type. + * + * @return the name of the primary node type. + */ + public Name getPrimaryNodeTypeName() { + return data.getNodeState().getNodeTypeName(); + } + + /** + * Test if this node is access controlled. The node is access controlled if + * it is of node type + * {@link org.apache.jackrabbit.core.security.authorization.AccessControlConstants#NT_REP_ACCESS_CONTROLLABLE "rep:AccessControllable"} + * and if it has a child node named + * {@link org.apache.jackrabbit.core.security.authorization.AccessControlConstants#N_POLICY}. + * + * @return true if this node is access controlled and has a + * rep:policy child; false otherwise. + * @throws RepositoryException if an error occurs + */ + public boolean isAccessControllable() throws RepositoryException { + return data.getNodeState().hasChildNodeEntry(NameConstants.REP_POLICY, 1) + && isNodeType(NameConstants.REP_ACCESS_CONTROLLABLE); + } + + /** + * Same as {@link Node#orderBefore(String, String)} except that + * this method takes a Path.Element arguments instead of + * Strings. + * + * @param srcName + * @param dstName + * @throws UnsupportedRepositoryOperationException + * @throws VersionException + * @throws ConstraintViolationException + * @throws ItemNotFoundException + * @throws LockException + * @throws RepositoryException + */ + public synchronized void orderBefore(Path.Element srcName, + Path.Element dstName) + throws UnsupportedRepositoryOperationException, VersionException, + ConstraintViolationException, ItemNotFoundException, LockException, + RepositoryException { + + // check state of this instance + sanityCheck(); + + if (!getPrimaryNodeType().hasOrderableChildNodes()) { + throw new UnsupportedRepositoryOperationException( + "child node ordering not supported on " + this); + } + + // check arguments + if (srcName.equals(dstName)) { + // there's nothing to do + return; + } + + // check existence + if (!hasNode(srcName.getName(), srcName.getIndex())) { + String name; + try { + Path.Element[] path = new Path.Element[] { srcName }; + name = sessionContext.getJCRPath(new PathBuilder(path).getPath()); + } catch (NameException e) { + name = srcName.toString(); + } catch (NamespaceException e) { + name = srcName.toString(); + } + throw new ItemNotFoundException( + this + " has no child node with name " + name); + } + + if (dstName != null && !hasNode(dstName.getName(), dstName.getIndex())) { + String name; + try { + Path.Element[] path = new Path.Element[] { dstName }; + name = sessionContext.getJCRPath(new PathBuilder(path).getPath()); + } catch (NameException e) { + name = dstName.toString(); + } catch (NamespaceException e) { + name = dstName.toString(); + } + throw new ItemNotFoundException( + this + " has no child node with name " + name); + } + + // make sure this node is checked-out and neither protected nor locked + int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT + | ItemValidator.CHECK_CONSTRAINTS; + sessionContext.getItemValidator().checkModify(this, options, Permission.NONE); + + /* + make sure the session is allowed to reorder child nodes. + since there is no specific privilege for reordering child nodes, + test if the the node to be reordered can be removed and added, + i.e. treating reorder similar to a move. + TODO: properly deal with sns in which case the index would change upon reorder. + */ + AccessManager acMgr = sessionContext.getAccessManager(); + PathBuilder pb = new PathBuilder(getPrimaryPath()); + pb.addLast(srcName.getName(), srcName.getIndex()); + Path childPath = pb.getPath(); + if (!acMgr.isGranted(childPath, Permission.MODIFY_CHILD_NODE_COLLECTION)) { + String msg = "Not allowed to reorder child node " + sessionContext.getJCRPath(childPath) + "."; + log.debug(msg); + throw new AccessDeniedException(msg); + } + + ArrayList list = new ArrayList(data.getNodeState().getChildNodeEntries()); + int srcInd = -1, destInd = -1; + for (int i = 0; i < list.size(); i++) { + ChildNodeEntry entry = list.get(i); + if (srcInd == -1) { + if (entry.getName().equals(srcName.getName()) + && (entry.getIndex() == srcName.getIndex() + || srcName.getIndex() == 0 && entry.getIndex() == 1)) { + srcInd = i; + } + } + if (destInd == -1 && dstName != null) { + if (entry.getName().equals(dstName.getName()) + && (entry.getIndex() == dstName.getIndex() + || dstName.getIndex() == 0 && entry.getIndex() == 1)) { + destInd = i; + if (srcInd != -1) { + break; + } + } + } else { + if (srcInd != -1) { + break; + } + } + } + + // check if resulting order would be different to current order + if (destInd == -1) { + if (srcInd == list.size() - 1) { + // no change, we're done + return; + } + } else { + if ((destInd - srcInd) == 1) { + // no change, we're done + return; + } + } + + // reorder list + if (destInd == -1) { + list.add(list.remove(srcInd)); + } else { + if (srcInd < destInd) { + list.add(destInd, list.get(srcInd)); + list.remove(srcInd); + } else { + list.add(destInd, list.remove(srcInd)); + } + } + + // modify the state of 'this', i.e. the parent node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + thisState.setChildNodeEntries(list); + } + + /** + * Replaces the child node with the specified id + * by a new child node with the same id and specified nodeName, + * nodeTypeName and mixinNames. + * + * @param id id of the child node to be replaced + * @param nodeName name of the new node + * @param nodeTypeName name of the new node's node type + * @param mixinNames name of the new node's mixin types + * + * @return the new child node replacing the existing child + * @throws ItemNotFoundException + * @throws NoSuchNodeTypeException + * @throws VersionException + * @throws ConstraintViolationException + * @throws LockException + * @throws RepositoryException + */ + public synchronized NodeImpl replaceChildNode(NodeId id, Name nodeName, + Name nodeTypeName, + Name[] mixinNames) + throws ItemNotFoundException, NoSuchNodeTypeException, VersionException, + ConstraintViolationException, LockException, RepositoryException { + // check state of this instance + sanityCheck(); + + Node existing = (Node) itemMgr.getItem(id); + + // 'replace' is actually a 'remove existing/add new' operation; + // this unfortunately changes the order of this node's + // child node entries (JCR-1055); + // => backup list of child node entries beforehand in order + // to restore it afterwards + NodeState state = data.getNodeState(); + ChildNodeEntry cneExisting = state.getChildNodeEntry(id); + if (cneExisting == null) { + throw new ItemNotFoundException( + this + ": no child node entry with id " + id); + } + List cneList = new ArrayList(state.getChildNodeEntries()); + + // remove existing + existing.remove(); + + // create new child node + NodeImpl node = addNode(nodeName, nodeTypeName, id); + if (mixinNames != null) { + for (Name mixinName : mixinNames) { + node.addMixin(mixinName); + } + } + + // fetch state again, as it changed while removing child + state = data.getNodeState(); + + // restore list of child node entries (JCR-1055) + if (cneExisting.getName().equals(nodeName)) { + // restore original child node list + state.setChildNodeEntries(cneList); + } else { + // replace child node entry with different name + // but preserving original position + state.removeAllChildNodeEntries(); + for (ChildNodeEntry cne : cneList) { + if (cne.getId().equals(id)) { + // replace entry with different name + state.addChildNodeEntry(nodeName, id); + } else { + state.addChildNodeEntry(cne.getName(), cne.getId()); + } + } + } + + return node; + } + + /** + * Create a child node that is a clone of a shareable node. + * + * @param src shareable source node + * @param name name of new node + * @return child node + * @throws ItemExistsException if there already is a child node with the + * name given and the definition does not allow creating another one + * @throws VersionException if this node is not checked out + * @throws ConstraintViolationException if no definition is found in this + * node that would allow creating the child node + * @throws LockException if this node is locked + * @throws RepositoryException if some other error occurs + */ + public synchronized NodeImpl clone(NodeImpl src, Name name) + throws ItemExistsException, VersionException, + ConstraintViolationException, LockException, + RepositoryException { + + Path nodePath; + try { + nodePath = PathFactoryImpl.getInstance().create(getPrimaryPath(), name, true); + } catch (MalformedPathException e) { + // should never happen + String msg = "internal error: invalid path " + this; + log.debug(msg); + throw new RepositoryException(msg, e); + } + + // (1) make sure that parent node is checked-out + // (2) check lock status + // (3) check protected flag of parent (i.e. this) node + int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT | ItemValidator.CHECK_CONSTRAINTS; + sessionContext.getItemValidator().checkModify(this, options, Permission.NONE); + + // (4) check for name collisions + NodeDefinitionImpl def; + try { + def = getApplicableChildNodeDefinition(name, null); + } catch (RepositoryException re) { + String msg = "no definition found in parent node's node type for new node"; + log.debug(msg); + throw new ConstraintViolationException(msg, re); + } + NodeState thisState = data.getNodeState(); + ChildNodeEntry cne = thisState.getChildNodeEntry(name, 1); + if (cne != null) { + // there's already a child node entry with that name; + // check same-name sibling setting of new node + if (!def.allowsSameNameSiblings()) { + throw new ItemExistsException(itemMgr.safeGetJCRPath(nodePath)); + } + // check same-name sibling setting of existing node + NodeId newId = cne.getId(); + if (!((NodeImpl) itemMgr.getItem(newId)).getDefinition().allowsSameNameSiblings()) { + throw new ItemExistsException(itemMgr.safeGetJCRPath(nodePath)); + } + } + + // (5) do clone operation + NodeId parentId = getNodeId(); + src.addShareParent(parentId); + + // (6) modify the state of 'this', i.e. the parent node + NodeId srcId = src.getNodeId(); + thisState = (NodeState) getOrCreateTransientItemState(); + // add new child node entry + thisState.addChildNodeEntry(name, srcId); + + return itemMgr.getNode(srcId, parentId); + } + + // -----------------------------------------------------------------< Item > + /** + * {@inheritDoc} + */ + @Override + public boolean isNode() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() throws RepositoryException { + return perform(new SessionOperation() { + public String perform(SessionContext context) + throws RepositoryException { + NodeId parentId = data.getNodeState().getParentId(); + if (parentId == null) { + return ""; // this is the root node + } + + Name name; + if (!isShareable()) { + name = context.getHierarchyManager().getName(id); + } else { + name = context.getHierarchyManager().getName( + getNodeId(), parentId); + } + return context.getJCRName(name); + } + public String toString() { + return "node.getName()"; + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public void accept(ItemVisitor visitor) throws RepositoryException { + // check state of this instance + sanityCheck(); + + visitor.visit(this); + } + + /** + * {@inheritDoc} + */ + @Override + public Node getParent() throws RepositoryException { + return perform(new SessionOperation() { + public Node perform(SessionContext context) + throws RepositoryException { + NodeId parentId = getParentId(); + if (parentId != null) { + return (Node) context.getItemManager().getItem(parentId); + } else { + throw new ItemNotFoundException( + "Root node doesn't have a parent"); + } + } + public String toString() { + return "node.getParent()"; + } + }); + } + + //----------------------------------------------------------------< Node > + + /** + * {@inheritDoc} + */ + public Node addNode(String relPath) throws RepositoryException { + return addNodeWithUuid(relPath, null, null); + } + + /** + * {@inheritDoc} + */ + public Node addNode(String relPath, String nodeTypeName) + throws RepositoryException { + return addNodeWithUuid(relPath, nodeTypeName, null); + } + + /** + * Adds a node with the given UUID. You can only add a node with a UUID + * that is not already assigned to another node in this workspace. + * + * @since Apache Jackrabbit 1.6 + * @see JCR-1972 + * @see Node#addNode(String) + * @param relPath path of the new node + * @param uuid UUID of the new node, + * or null for a random new UUID + * @return the newly added node + * @throws RepositoryException if the node can not be added + */ + public Node addNodeWithUuid(String relPath, String uuid) + throws RepositoryException { + return addNodeWithUuid(relPath, null, uuid); + } + + /** + * Adds a node with the given node type and UUID. You can only add a node + * with a UUID that is not already assigned to another node in this + * workspace. + * + * @since Apache Jackrabbit 1.6 + * @see JCR-1972 + * @see Node#addNode(String, String) + * @param relPath path of the new node + * @param nodeTypeName name of the new node's node type, + * or null for automatic type assignment + * @param uuid UUID of the new node, + * or null for a random new UUID + * @return the newly added node + * @throws RepositoryException if the node can not be added + */ + public Node addNodeWithUuid( + String relPath, String nodeTypeName, String uuid) + throws RepositoryException { + return perform(new AddNodeOperation(this, relPath, nodeTypeName, uuid)); + } + + /** + * {@inheritDoc} + */ + public void orderBefore(String srcName, String destName) + throws UnsupportedRepositoryOperationException, VersionException, + ConstraintViolationException, ItemNotFoundException, LockException, + RepositoryException { + + Path.Element insertName; + try { + Path p = sessionContext.getQPath(srcName); + // p must be a relative path of length==depth==1 (to eliminate e.g. "..") + if (p.isAbsolute() || p.getLength() != 1 || p.getDepth() != 1) { + throw new RepositoryException("invalid name: " + srcName); + } + insertName = p.getNameElement(); + } catch (NameException e) { + String msg = "invalid name: " + srcName; + log.debug(msg); + throw new RepositoryException(msg, e); + } + + Path.Element beforeName; + if (destName != null) { + try { + Path p = sessionContext.getQPath(destName); + // p must be a relative path of length==depth==1 (to eliminate e.g. "..") + if (p.isAbsolute() || p.getLength() != 1 || p.getDepth() != 1) { + throw new RepositoryException("invalid name: " + destName); + } + beforeName = p.getNameElement(); + } catch (NameException e) { + String msg = "invalid name: " + destName; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } else { + beforeName = null; + } + + orderBefore(insertName, beforeName); + } + + /** Wrapper around {@link #setProperty(Name, Value[], int, boolean)} */ + public Property setProperty(String name, Value[] values) + throws RepositoryException { + return setProperty(getQName(name), values, getType(values), false); + } + + /** Wrapper around {@link #setProperty(Name, Value[], int, boolean)} */ + public Property setProperty(String name, Value[] values, int type) + throws RepositoryException { + return setProperty(getQName(name), values, type, true); + } + + /** Wrapper around {@link #setProperty(Name, Value[], int, boolean)} */ + public Property setProperty(String name, String[] strings) + throws RepositoryException { + Value[] values = getValues(strings, STRING); + return setProperty(getQName(name), values, STRING, false); + } + + /** Wrapper around {@link #setProperty(Name, Value[], int, boolean)} */ + public Property setProperty(String name, String[] values, int type) + throws RepositoryException { + Value[] converted = getValues(values, type); + return setProperty(sessionContext.getQName(name), converted, type, true); + } + + /** Wrapper around {@link #setProperty(String, Value)} */ + public Property setProperty(String name, String value) + throws RepositoryException { + if (value != null) { + return setProperty(name, getValueFactory().createValue(value)); + } else { + return setProperty(name, (Value) null); + } + } + + /** Wrapper around {@link #setProperty(String, Value, int)} */ + public Property setProperty(String name, String value, int type) + throws RepositoryException { + if (value != null) { + return setProperty( + name, getValueFactory().createValue(value, type), type); + } else { + return setProperty(name, (Value) null, type); + } + } + + /** Wrapper around {@link SetPropertyOperation} */ + public Property setProperty(String name, Value value, int type) + throws RepositoryException { + if (value != null && value.getType() != type) { + value = ValueHelper.convert(value, type, getValueFactory()); + } + return sessionContext.getSessionState().perform( + new SetPropertyOperation(sessionContext.getQName(name), value, true)); + } + + /** Wrapper around {@link SetPropertyOperation} */ + public Property setProperty(String name, Value value) + throws RepositoryException { + return sessionContext.getSessionState().perform( + new SetPropertyOperation(sessionContext.getQName(name), value, false)); + } + + /** Wrapper around {@link #setProperty(String, Value)} */ + public Property setProperty(String name, InputStream value) + throws RepositoryException { + if (value != null) { + Binary binary = getValueFactory().createBinary(value); + try { + return setProperty(name, getValueFactory().createValue(binary)); + } finally { + binary.dispose(); + } + } else { + return setProperty(name, (Value) null); + } + } + + /** Wrapper around {@link #setProperty(String, Value)} */ + public Property setProperty(String name, boolean value) + throws RepositoryException { + return setProperty(name, getValueFactory().createValue(value)); + } + + /** Wrapper around {@link #setProperty(String, Value)} */ + public Property setProperty(String name, double value) + throws RepositoryException { + return setProperty(name, getValueFactory().createValue(value)); + } + + /** Wrapper around {@link #setProperty(String, Value)} */ + public Property setProperty(String name, long value) + throws RepositoryException { + return setProperty(name, getValueFactory().createValue(value)); + } + + /** Wrapper around {@link #setProperty(String, Value)} */ + public Property setProperty(String name, Calendar value) + throws RepositoryException { + if (value != null) { + try { + return setProperty(name, getValueFactory().createValue(value)); + } catch (IllegalArgumentException e) { + throw new ValueFormatException( + "Value is not an ISO8601 date: " + value, e); + } + } else { + return setProperty(name, (Value) null); + } + } + + /** Wrapper around {@link #setProperty(String, Value)} */ + public Property setProperty(String name, Node value) + throws RepositoryException { + if (value != null) { + try { + return setProperty(name, getValueFactory().createValue(value)); + } catch (UnsupportedRepositoryOperationException e) { + throw new ValueFormatException( + "Node is not referenceable: " + value, e); + } + } else { + return setProperty(name, (Value) null); + } + } + + /** + * Implementation for setProperty() using a single {@link + * Value}. The type of the returned property is enforced based on the + * enforceType flag. If set to true, the returned + * property is of the passed type if it didn't exist before. If set to + * false, then the returned property may be of some other type, + * but still must be based on an existing property definition for the given + * name and single-valued flag. The resulting type is taken from that + * definition and the implementation tries to convert the passed value to + * that type. If that fails, then a {@link ValueFormatException} is thrown. + */ + private class SetPropertyOperation implements SessionWriteOperation { + + private final Name name; + + private final Value value; + + private final boolean enforceType; + + /** + * @param name property name + * @param value new value of the property, + * or null to remove the property + * @param enforceType true to enforce the value type + */ + public SetPropertyOperation( + Name name, Value value, boolean enforceType) { + this.name = name; + this.value = value; + this.enforceType = enforceType; + } + + /** + * @return the Property object set, + * or null if this operation was used to remove + * a property (by setting its value to null) + * @throws ValueFormatException if value cannot be + * converted to the specified type or + * if the property already exists and + * is multi-valued. + * @throws VersionException if this node is read-only due to a + * checked-in node and this implementation + * performs this validation immediately. + * @throws LockException if a lock prevents the setting of + * the property and this implementation + * performs this validation immediately. + * @throws ConstraintViolationException if the change would violate a + * node-type or other constraint and + * this implementation performs this + * validation immediately. + * @throws RepositoryException if another error occurs. + */ + public PropertyImpl perform(SessionContext context) + throws RepositoryException { + itemSanityCheck(); + // check pre-conditions for setting property + checkSetProperty(); + + int type = PropertyType.UNDEFINED; + if (value != null) { + type = value.getType(); + } + + BitSet status = new BitSet(); + PropertyImpl property = + getOrCreateProperty(name, type, false, enforceType, status); + try { + property.setValue(value); + } catch (RepositoryException e) { + if (status.get(CREATED)) { + // setting value failed, get rid of newly created property + removeChildProperty(name); + } + throw e; // rethrow + } catch (RuntimeException e) { + if (status.get(CREATED)) { + // setting value failed, get rid of newly created property + removeChildProperty(name); + } + throw e; // rethrow + } catch (Error e) { + if (status.get(CREATED)) { + // setting value failed, get rid of newly created property + removeChildProperty(name); + } + throw e; // rethrow + } + return property; + } + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "node.setProperty(" + name + ", " + value + ")"; + } + + } + + /** + * Implementation for setProperty() using a {@link Value} + * array. The type of the returned property is enforced based on the + * enforceType flag. If set to true, the returned + * property is of the passed type if it didn't exist before. If set to + * false, then the returned property may be of some other type, + * but still must be based on an existing property definition for the given + * name and multi-valued flag. The resulting type is taken from that + * definition and the implementation tries to convert the passed values to + * that type. If that fails, then a {@link ValueFormatException} is thrown. + * + * @param name the name of the property to set. + * @param values the values to set. If null the property + * is removed. + * @param type the target type of the values to set. + * @param enforceType if the target type is enforced. + * @return the Property object set, or null if + * this method was used to remove a property (by setting its value + * to null). + * @throws ValueFormatException if a value cannot be converted to + * the specified type or if the + * property already exists and is not + * multi-valued. + * @throws VersionException if this node is read-only due to a + * checked-in node and this implementation + * performs this validation immediately. + * @throws LockException if a lock prevents the setting of + * the property and this implementation + * performs this validation immediately. + * @throws ConstraintViolationException if the change would violate a + * node-type or other constraint and + * this implementation performs this + * validation immediately. + * @throws RepositoryException if another error occurs. + */ + protected PropertyImpl setProperty( + final Name name, final Value[] values, final int type, + final boolean enforceType) throws RepositoryException { + return perform(new SessionOperation() { + public PropertyImpl perform(SessionContext context) + throws RepositoryException { + // check pre-conditions for setting property + checkSetProperty(); + + BitSet status = new BitSet(); + PropertyImpl prop = getOrCreateProperty( + name, type, true, enforceType, status); + try { + prop.setValue(values, type); + } catch (RepositoryException re) { + if (status.get(CREATED)) { + // setting value failed, get rid of newly created property + removeChildProperty(name); + } + // rethrow + throw re; + } + return prop; + } + public String toString() { + return "node.setProperty(...)"; + } + }); + } + + /** + * {@inheritDoc} + */ + public Node getNode(final String relPath) throws RepositoryException { + return perform(new SessionOperation() { + public Node perform(SessionContext context) + throws RepositoryException { + Path p = resolveRelativePath(relPath); + NodeId id = getNodeId(p); + if (id == null) { + throw new PathNotFoundException(relPath); + } + + // determine parent as mandated by path + NodeId parentId = null; + if (!p.denotesRoot()) { + parentId = getNodeId(p.getAncestor(1)); + } + try { + // if the node is shareable, it now returns the node + // with the right parent + if (parentId != null) { + return itemMgr.getNode(id, parentId); + } else { + return (NodeImpl) itemMgr.getItem(id); + } + } catch (AccessDeniedException e) { + throw new PathNotFoundException(relPath); + } catch (ItemNotFoundException e) { + throw new PathNotFoundException(relPath); + } + } + public String toString() { + return "node.getNode(" + relPath + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public NodeIterator getNodes() throws RepositoryException { + // IMPORTANT: an implementation of Node.getNodes() must not use + // a class derived from TraversingElementVisitor to traverse the + // hierarchy because this would lead to an infinite recursion! + return perform(new SessionOperation() { + public NodeIterator perform(SessionContext context) + throws RepositoryException { + try { + return itemMgr.getChildNodes((NodeId) id); + } catch (ItemNotFoundException e) { + throw new RepositoryException( + "Failed to list child nodes of " + NodeImpl.this, e); + } catch (AccessDeniedException e) { + throw new RepositoryException( + "Failed to list child nodes of " + NodeImpl.this, e); + } + } + public String toString() { + return "node.getNodes()"; + } + }); + } + + /** + * {@inheritDoc} + */ + public PropertyIterator getProperties() throws RepositoryException { + // IMPORTANT: an implementation of Node.getProperties() must not use + // a class derived from TraversingElementVisitor to traverse the + // hierarchy because this would lead to an infinite recursion! + return perform(new SessionOperation() { + public PropertyIterator perform(SessionContext context) + throws RepositoryException { + try { + return itemMgr.getChildProperties((NodeId) id); + } catch (ItemNotFoundException e) { + throw new RepositoryException( + "Failed to list properties of " + NodeImpl.this, e); + } catch (AccessDeniedException e) { + throw new RepositoryException( + "Failed to list properties of " + NodeImpl.this, e); + } + } + public String toString() { + return "node.getProperties()"; + } + }); + } + + /** + * {@inheritDoc} + */ + public Property getProperty(final String relPath) + throws PathNotFoundException, RepositoryException { + return perform(new SessionOperation() { + public Property perform(SessionContext context) + throws RepositoryException { + PropertyId id = resolveRelativePropertyPath(relPath); + if (id != null) { + try { + return (Property) itemMgr.getItem(id); + } catch (ItemNotFoundException e) { + throw new PathNotFoundException(relPath); + } catch (AccessDeniedException e) { + throw new PathNotFoundException(relPath); + } + } else { + throw new PathNotFoundException(relPath); + } + } + public String toString() { + return "node.getProperty(" + relPath + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public boolean hasNode(String relPath) throws RepositoryException { + // check state of this instance + sanityCheck(); + + NodeId id = resolveRelativeNodePath(relPath); + if (id != null) { + return itemMgr.itemExists(id); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public boolean hasNodes() throws RepositoryException { + // check state of this instance + sanityCheck(); + + /** + * hasNodes respects the access rights + * of this node's session, i.e. it will + * return false if child nodes exist + * but the session is not granted read-access + */ + return itemMgr.hasChildNodes((NodeId) id); + } + + /** + * {@inheritDoc} + */ + public boolean hasProperties() throws RepositoryException { + // check state of this instance + sanityCheck(); + + /** + * hasProperties respects the access rights + * of this node's session, i.e. it will + * return false if properties exist + * but the session is not granted read-access + */ + return itemMgr.hasChildProperties((NodeId) id); + } + + /** + * {@inheritDoc} + */ + public boolean isNodeType(String nodeTypeName) throws RepositoryException { + // check state of this instance + sanityCheck(); + + try { + return isNodeType(sessionContext.getQName(nodeTypeName)); + } catch (NameException e) { + throw new RepositoryException( + "invalid node type name: " + nodeTypeName, e); + } + } + + /** + * {@inheritDoc} + */ + public NodeType getPrimaryNodeType() throws RepositoryException { + // check state of this instance + sanityCheck(); + + return sessionContext.getNodeTypeManager().getNodeType( + data.getNodeState().getNodeTypeName()); + } + + /** + * {@inheritDoc} + */ + public NodeType[] getMixinNodeTypes() throws RepositoryException { + // check state of this instance + sanityCheck(); + + Set mixinNames = data.getNodeState().getMixinTypeNames(); + if (mixinNames.isEmpty()) { + return new NodeType[0]; + } + NodeType[] nta = new NodeType[mixinNames.size()]; + Iterator iter = mixinNames.iterator(); + int i = 0; + while (iter.hasNext()) { + nta[i++] = sessionContext.getNodeTypeManager().getNodeType(iter.next()); + } + return nta; + } + + /** Wrapper around {@link #addMixin(Name)}. */ + public void addMixin(String mixinName) throws RepositoryException { + try { + addMixin(sessionContext.getQName(mixinName)); + } catch (NameException e) { + throw new RepositoryException( + "Invalid mixin type name: " + mixinName, e); + } + } + + /** Wrapper around {@link #removeMixin(Name)}. */ + public void removeMixin(String mixinName) throws RepositoryException { + try { + removeMixin(sessionContext.getQName(mixinName)); + } catch (NameException e) { + throw new RepositoryException( + "Invalid mixin type name: " + mixinName, e); + } + } + + /** + * {@inheritDoc} + */ + public boolean canAddMixin(String mixinName) + throws NoSuchNodeTypeException, RepositoryException { + // check state of this instance + sanityCheck(); + + Name ntName = sessionContext.getQName(mixinName); + NodeTypeManagerImpl ntMgr = sessionContext.getNodeTypeManager(); + NodeTypeImpl mixin = ntMgr.getNodeType(ntName); + if (!mixin.isMixin()) { + return false; + } + + int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT + | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD; + int permissions = Permission.NODE_TYPE_MNGMT; + // special handling of mix:(simple)versionable. since adding the mixin alters + // the version storage jcr:versionManagement privilege is required + // in addition. + if (NameConstants.MIX_VERSIONABLE.equals(ntName) + || NameConstants.MIX_SIMPLE_VERSIONABLE.equals(ntName)) { + permissions |= Permission.VERSION_MNGMT; + } + if (!sessionContext.getItemValidator().canModify(this, options, permissions)) { + return false; + } + + final Name primaryTypeName = data.getNodeState().getNodeTypeName(); + + NodeTypeImpl primaryType = ntMgr.getNodeType(primaryTypeName); + if (primaryType.isDerivedFrom(ntName)) { + // mixin already inherited -> addMixin is allowed but has no effect. + return true; + } + + // build effective node type of mixins & primary type + // in order to detect conflicts + NodeTypeRegistry ntReg = ntMgr.getNodeTypeRegistry(); + EffectiveNodeType entExisting; + try { + // existing mixin's + Set mixins = new HashSet(data.getNodeState().getMixinTypeNames()); + + // build effective node type representing primary type including existing mixin's + entExisting = ntReg.getEffectiveNodeType(primaryTypeName, mixins); + if (entExisting.includesNodeType(ntName)) { + // the existing mixins already include the mixin to be added. + // addMixin would succeed without modifying the node. + return true; + } + + // add new mixin + mixins.add(ntName); + // try to build new effective node type (will throw in case of conflicts) + ntReg.getEffectiveNodeType(primaryTypeName, mixins); + } catch (NodeTypeConflictException ntce) { + return false; + } + + return true; + } + + /** + * {@inheritDoc} + */ + public boolean hasProperty(String relPath) throws RepositoryException { + // check state of this instance + sanityCheck(); + + PropertyId id = resolveRelativePropertyPath(relPath); + if (id != null) { + return itemMgr.itemExists(id); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public PropertyIterator getReferences() throws RepositoryException { + return getReferences(null); + } + + /** + * {@inheritDoc} + */ + public NodeDefinition getDefinition() throws RepositoryException { + // check state of this instance + sanityCheck(); + + return data.getNodeDefinition(); + } + + /** + * {@inheritDoc} + */ + public NodeIterator getNodes(String namePattern) throws RepositoryException { + // check state of this instance + sanityCheck(); + + return ChildrenCollectorFilter.collectChildNodes(this, namePattern); + } + + /** + * {@inheritDoc} + */ + public PropertyIterator getProperties(String namePattern) + throws RepositoryException { + // check state of this instance + sanityCheck(); + + return ChildrenCollectorFilter.collectProperties(this, namePattern); + } + + /** + * {@inheritDoc} + */ + public Item getPrimaryItem() + throws ItemNotFoundException, RepositoryException { + // check state of this instance + sanityCheck(); + + String name = getPrimaryNodeType().getPrimaryItemName(); + if (name == null) { + throw new ItemNotFoundException(); + } + if (hasProperty(name)) { + return getProperty(name); + } else if (hasNode(name)) { + return getNode(name); + } else { + throw new ItemNotFoundException(); + } + } + + /** + * {@inheritDoc} + */ + public String getUUID() + throws UnsupportedRepositoryOperationException, RepositoryException { + // check state of this instance + sanityCheck(); + + if (!isNodeType(NameConstants.MIX_REFERENCEABLE)) { + throw new UnsupportedRepositoryOperationException(); + } + + return getNodeId().toString(); + } + + /** + * {@inheritDoc} + */ + public String getCorrespondingNodePath(String workspaceName) + throws ItemNotFoundException, NoSuchWorkspaceException, + AccessDeniedException, RepositoryException { + // check state of this instance + sanityCheck(); + + SessionImpl srcSession = null; + try { + // create session on other workspace for current subject + // (may throw NoSuchWorkspaceException and AccessDeniedException) + RepositoryImpl rep = (RepositoryImpl) getSession().getRepository(); + srcSession = rep.createSession( + sessionContext.getSessionImpl().getSubject(), workspaceName); + + // search nearest ancestor that is referenceable + NodeImpl m1 = this; + while (m1.getDepth() != 0 && !m1.isNodeType(NameConstants.MIX_REFERENCEABLE)) { + m1 = (NodeImpl) m1.getParent(); + } + + // if root is common ancestor, corresponding path is same as ours + if (m1.getDepth() == 0) { + // check existence + if (!srcSession.getItemManager().nodeExists(getPrimaryPath())) { + throw new ItemNotFoundException("Node not found: " + this); + } else { + return getPath(); + } + } + + // get corresponding ancestor + Node m2 = srcSession.getNodeByUUID(m1.getUUID()); + + // return path of m2, if m1 == n1 + if (m1 == this) { + return m2.getPath(); + } + + String relPath; + try { + Path p = m1.getPrimaryPath().computeRelativePath(getPrimaryPath()); + // use prefix mappings of srcSession + relPath = sessionContext.getJCRPath(p); + } catch (NameException be) { + // should never get here... + String msg = "internal error: failed to determine relative path"; + log.error(msg, be); + throw new RepositoryException(msg, be); + } + + if (!m2.hasNode(relPath)) { + throw new ItemNotFoundException(); + } else { + return m2.getNode(relPath).getPath(); + } + } finally { + if (srcSession != null) { + // we don't need the other session anymore, logout + srcSession.logout(); + } + } + } + + /** + * {@inheritDoc} + */ + public int getIndex() throws RepositoryException { + // check state of this instance + sanityCheck(); + + NodeId parentId = getParentId(); + if (parentId == null) { + // the root node cannot have same-name siblings; always return 1 + return 1; + } + + try { + NodeState parent = + (NodeState) stateMgr.getItemState(parentId); + ChildNodeEntry parentEntry = + parent.getChildNodeEntry(getNodeId()); + return parentEntry.getIndex(); + } catch (ItemStateException ise) { + // should never get here... + String msg = "internal error: failed to determine index"; + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + } + + //-------------------------------------------------------< shareable nodes > + + /** + * Returns an iterator over all nodes that are in the shared set of this + * node. If this node is not shared then the returned iterator contains + * only this node. + * + * @return a NodeIterator + * @throws RepositoryException if an error occurs. + * @since JCR 2.0 + */ + public NodeIterator getSharedSet() throws RepositoryException { + // check state of this instance + sanityCheck(); + + ArrayList list = new ArrayList(); + + if (!isShareable()) { + list.add(this); + } else { + NodeState state = data.getNodeState(); + for (NodeId parentId : state.getSharedSet()) { + list.add(itemMgr.getNode(getNodeId(), parentId)); + } + } + return new NodeIteratorAdapter(list); + } + + /** + * A special kind of remove() that removes this node and every + * other node in the shared set of this node. + *

    + * This removal must be done atomically, i.e., if one of the nodes cannot be + * removed, the function throws the exception remove() would + * have thrown in that case, and none of the nodes are removed. + *

    + * If this node is not shared this method removes only this node. + * + * @throws VersionException + * @throws LockException + * @throws ConstraintViolationException + * @throws RepositoryException + * @see #removeShare() + * @see Item#remove() + * @since JCR 2.0 + */ + public void removeSharedSet() throws VersionException, LockException, + ConstraintViolationException, RepositoryException { + + // check state of this instance + sanityCheck(); + + NodeIterator iter = getSharedSet(); + while (iter.hasNext()) { + iter.nextNode().removeShare(); + } + } + + /** + * A special kind of remove() that removes this node, but does + * not remove any other node in the shared set of this node. + *

    + * All of the exceptions defined for remove() apply to this + * function. In addition, a RepositoryException is thrown if + * this node cannot be removed without removing another node in the shared + * set of this node. + *

    + * If this node is not shared this method removes only this node. + * + * @throws VersionException + * @throws LockException + * @throws ConstraintViolationException + * @throws RepositoryException + * @see #removeSharedSet() + * @see Item#remove() + * @since JCR 2.0 + */ + public void removeShare() throws VersionException, LockException, + ConstraintViolationException, RepositoryException { + + // check state of this instance + sanityCheck(); + + // Standard remove() will remove just this node + remove(); + } + + /** + * Helper method, returning a flag that indicates whether this node is + * shareable. + * + * @return true if this node is shareable; + * false otherwise. + * @see NodeState#isShareable() + */ + boolean isShareable() { + return data.getNodeState().isShareable(); + } + + /** + * Helper method, returning the parent id this node is attached to. If this + * node is shareable, it returns the primary parent id (which remains + * fixed since shareable nodes are not moveable). Otherwise returns the + * underlying state's parent id. + * + * @return parent id + */ + public NodeId getParentId() { + return data.getParentId(); + } + + /** + * Helper method, returning a flag indicating whether this node has + * the given share-parent. + * + * @param parentId parent id + * @return true if the node has the given shared parent; + * false otherwise. + */ + boolean hasShareParent(NodeId parentId) { + return data.getNodeState().containsShare(parentId); + } + + /** + * Add a share-parent to this node. This method checks, whether: + *

      + *
    • this node is shareable
    • + *
    • adding the given would create a share cycle
    • + *
    • the given parent is already a share-parent
    • + *
    + * @param parentId parent to add to the shared set + * @throws RepositoryException if an error occurs + */ + void addShareParent(NodeId parentId) throws RepositoryException { + // verify that we're shareable + if (!isShareable()) { + String msg = this + " is not shareable."; + log.debug(msg); + throw new RepositoryException(msg); + } + + // detect share cycle + NodeId srcId = getNodeId(); + HierarchyManager hierMgr = sessionContext.getHierarchyManager(); + if (parentId.equals(srcId) || hierMgr.isAncestor(srcId, parentId)) { + String msg = "This would create a share cycle."; + log.debug(msg); + throw new RepositoryException(msg); + } + + // quickly verify whether the share is already contained before creating + // a transient state in vain + NodeState state = data.getNodeState(); + if (!state.containsShare(parentId)) { + state = (NodeState) getOrCreateTransientItemState(); + if (state.addShare(parentId)) { + return; + } + } + String msg = "Adding a shareable node twice to the same parent is not supported."; + log.debug(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + + /** + * {@inheritDoc} + * + * Overridden to return a different path for shareable nodes. + * + * TODO SN: copies functionality in that is already available in + * HierarchyManagerImpl, namely composing a path by + * concatenating the parent path + this node's name and index: + * rather use hierarchy manager to do this + */ + @Override + public Path getPrimaryPath() throws RepositoryException { + if (!isShareable()) { + return super.getPrimaryPath(); + } + + NodeId parentId = getParentId(); + NodeImpl parentNode = (NodeImpl) getParent(); + Path parentPath = parentNode.getPrimaryPath(); + PathBuilder builder = new PathBuilder(parentPath); + + ChildNodeEntry entry = + parentNode.getNodeState().getChildNodeEntry(getNodeId()); + if (entry == null) { + String msg = "failed to build path of " + id + ": " + + parentId + " has no child entry for " + + id; + log.debug(msg); + throw new ItemNotFoundException(msg); + } + // add to path + if (entry.getIndex() == 1) { + builder.addLast(entry.getName()); + } else { + builder.addLast(entry.getName(), entry.getIndex()); + } + return builder.getPath(); + } + + //------------------------------< versioning support: public Node methods > + + /** + * {@inheritDoc} + */ + public boolean isCheckedOut() throws RepositoryException { + // check state of this instance + sanityCheck(); + + // try shortcut first: + // if current node is 'new' we can safely consider it checked-out since + // otherwise it would had been impossible to add it in the first place + if (isNew()) { + return true; + } + + // search nearest ancestor that is versionable + // FIXME should not only rely on existence of jcr:isCheckedOut property + // but also verify that node.isNodeType("mix:versionable")==true; + // this would have a negative impact on performance though... + try { + NodeState state = getNodeState(); + while (!state.hasPropertyName(JCR_ISCHECKEDOUT)) { + ItemId parentId = state.getParentId(); + if (parentId == null) { + // root reached or out of hierarchy + return true; + } + state = (NodeState) + sessionContext.getItemStateManager().getItemState(parentId); + } + PropertyId id = new PropertyId(state.getNodeId(), JCR_ISCHECKEDOUT); + PropertyState ps = + (PropertyState) sessionContext.getItemStateManager().getItemState(id); + InternalValue[] values = ps.getValues(); + if (values == null || values.length != 1) { + // the property is not fully set, or it is a multi-valued property + // in which case it's probably not mix:versionable + return true; + } + return values[0].getBoolean(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * Returns the version manager of this workspace. + */ + private VersionManagerImpl getVersionManagerImpl() { + return sessionContext.getWorkspace().getVersionManagerImpl(); + } + + /** + * {@inheritDoc} + */ + public void update(String srcWorkspaceName) throws RepositoryException { + getVersionManagerImpl().update(this, srcWorkspaceName); + } + + /** + * Use {@link VersionManager#checkin(String)} instead + */ + @Deprecated + public Version checkin() throws RepositoryException { + return getVersionManagerImpl().checkin(getPath()); + } + + /** + * Use {@link VersionManagerImpl#checkin(String, Calendar)} instead + * + * @since Apache Jackrabbit 1.6 + * @see JCR-1972 + */ + @Deprecated + public Version checkin(Calendar created) throws RepositoryException { + return getVersionManagerImpl().checkin(getPath(), created); + } + + /** + * Use {@link VersionManager#checkout(String)} instead + */ + @Deprecated + public void checkout() throws RepositoryException { + getVersionManagerImpl().checkout(getPath()); + } + + /** + * Use {@link VersionManager#merge(String, String, boolean)} instead + */ + @Deprecated + public NodeIterator merge(String srcWorkspace, boolean bestEffort) + throws RepositoryException { + return getVersionManagerImpl().merge( + getPath(), srcWorkspace, bestEffort); + } + + /** + * Use {@link VersionManager#cancelMerge(String, Version)} instead + */ + @Deprecated + public void cancelMerge(Version version) throws RepositoryException { + getVersionManagerImpl().cancelMerge(getPath(), version); + } + + /** + * Use {@link VersionManager#doneMerge(String, Version)} instead + */ + @Deprecated + public void doneMerge(Version version) throws RepositoryException { + getVersionManagerImpl().doneMerge(getPath(), version); + } + + /** + * Use {@link VersionManager#restore(String, String, boolean)} instead + */ + @Deprecated + public void restore(String versionName, boolean removeExisting) + throws RepositoryException { + getVersionManagerImpl().restore(getPath(), versionName, removeExisting); + } + + /** + * Use {@link VersionManager#restore(String, Version, boolean)} instead + */ + @Deprecated + public void restore(Version version, boolean removeExisting) + throws RepositoryException { + getVersionManagerImpl().restore(this, version, removeExisting); + } + + /** + * Use {@link VersionManager#restore(String, Version, boolean)} instead + */ + @Deprecated + public void restore(Version version, String relPath, boolean removeExisting) + throws RepositoryException { + if (hasNode(relPath)) { + getVersionManagerImpl().restore((NodeImpl) getNode(relPath), version, removeExisting); + } else { + getVersionManagerImpl().restore( + getPath() + "/" + relPath, version, removeExisting); + } + } + + /** + * Use {@link VersionManager#restoreByLabel(String, String, boolean)} + * instead + */ + @Deprecated + public void restoreByLabel(String versionLabel, boolean removeExisting) + throws RepositoryException { + getVersionManagerImpl().restoreByLabel( + getPath(), versionLabel, removeExisting); + } + + /** + * Use {@link VersionManager#getVersionHistory(String)} instead + */ + @Deprecated + public VersionHistory getVersionHistory() throws RepositoryException { + return getVersionManagerImpl().getVersionHistory(getPath()); + } + + /** + * Use {@link VersionManager#getBaseVersion(String)} instead + */ + @Deprecated + public Version getBaseVersion() throws RepositoryException { + return getVersionManagerImpl().getBaseVersion(getPath()); + } + + //------------------------------------------------------< locking support > + /** + * {@inheritDoc} + */ + public Lock lock(boolean isDeep, boolean isSessionScoped) + throws UnsupportedRepositoryOperationException, LockException, + AccessDeniedException, InvalidItemStateException, + RepositoryException { + // check state of this instance + sanityCheck(); + LockManager lockMgr = getSession().getWorkspace().getLockManager(); + return lockMgr.lock(getPath(), isDeep, isSessionScoped, + sessionContext.getWorkspace().getConfig().getDefaultLockTimeout(), null); + } + + /** + * {@inheritDoc} + */ + public Lock getLock() + throws UnsupportedRepositoryOperationException, LockException, + AccessDeniedException, RepositoryException { + // check state of this instance + sanityCheck(); + LockManager lockMgr = getSession().getWorkspace().getLockManager(); + return lockMgr.getLock(getPath()); + } + + /** + * {@inheritDoc} + */ + public void unlock() + throws UnsupportedRepositoryOperationException, LockException, + AccessDeniedException, InvalidItemStateException, + RepositoryException { + // check state of this instance + sanityCheck(); + LockManager lockMgr = getSession().getWorkspace().getLockManager(); + lockMgr.unlock(getPath()); + } + + /** + * {@inheritDoc} + */ + public boolean holdsLock() throws RepositoryException { + // check state of this instance + sanityCheck(); + LockManager lockMgr = getSession().getWorkspace().getLockManager(); + return lockMgr.holdsLock(getPath()); + } + + /** + * {@inheritDoc} + */ + public boolean isLocked() throws RepositoryException { + // check state of this instance + sanityCheck(); + LockManager lockMgr = getSession().getWorkspace().getLockManager(); + return lockMgr.isLocked(getPath()); + } + + /** + * Check whether this node is locked by somebody else. + * + * @throws LockException if this node is locked by somebody else + * @throws RepositoryException if some other error occurs + * @deprecated + */ + protected void checkLock() throws LockException, RepositoryException { + if (isNew()) { + // a new node needs no check + return; + } + sessionContext.getWorkspace().getInternalLockManager().checkLock(this); + } + + //--------------------------------------------------< new JSR 283 methods > + /** + * {@inheritDoc} + */ + public String getIdentifier() throws RepositoryException { + return id.toString(); + } + + /** + * {@inheritDoc} + */ + public PropertyIterator getReferences(String name) + throws RepositoryException { + // check state of this instance + sanityCheck(); + + try { + if (stateMgr.hasNodeReferences(getNodeId())) { + NodeReferences refs = stateMgr.getNodeReferences(getNodeId()); + // refs.getReferences() returns a list of PropertyId's + List idList = refs.getReferences(); + if (name != null) { + Name qName; + try { + qName = sessionContext.getQName(name); + } catch (NameException e) { + throw new RepositoryException("invalid property name: " + name, e); + } + ArrayList filteredList = new ArrayList(idList.size()); + for (PropertyId propId : idList) { + if (propId.getName().equals(qName)) { + filteredList.add(propId); + } + } + idList = filteredList; + } + return new LazyItemIterator(sessionContext, idList); + } else { + // there are no references, return empty iterator + return PropertyIteratorAdapter.EMPTY; + } + } catch (ItemStateException e) { + String msg = "Unable to retrieve REFERENCE properties that refer to " + id; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public PropertyIterator getWeakReferences() throws RepositoryException { + // check state of this instance + sanityCheck(); + + // shortcut if node isn't referenceable + if (!isNodeType(NameConstants.MIX_REFERENCEABLE)) { + return PropertyIteratorAdapter.EMPTY; + } + + Value ref = getSession().getValueFactory().createValue(this, true); + List props = new ArrayList(); + QueryManagerImpl qm = (QueryManagerImpl) getSession().getWorkspace().getQueryManager(); + for (Node n : qm.getWeaklyReferringNodes(this)) { + for (PropertyIterator it = n.getProperties(); it.hasNext(); ) { + Property p = it.nextProperty(); + if (p.getType() == PropertyType.WEAKREFERENCE) { + Collection refs; + if (p.isMultiple()) { + refs = Arrays.asList(p.getValues()); + } else { + refs = Collections.singleton(p.getValue()); + } + if (refs.contains(ref)) { + props.add(p); + } + } + } + } + return new PropertyIteratorAdapter(props); + } + + /** + * {@inheritDoc} + */ + public PropertyIterator getWeakReferences(String name) throws RepositoryException { + if (name == null) { + return getWeakReferences(); + } + + // check state of this instance + sanityCheck(); + + // shortcut if node isn't referenceable + if (!isNodeType(NameConstants.MIX_REFERENCEABLE)) { + return PropertyIteratorAdapter.EMPTY; + } + + try { + StringBuilder stmt = new StringBuilder(); + stmt.append("//*[@").append(ISO9075.encode(name)); + stmt.append(" = '").append(data.getId()).append("']"); + Query q = getSession().getWorkspace().getQueryManager().createQuery( + stmt.toString(), Query.XPATH); + QueryResult result = q.execute(); + ArrayList l = new ArrayList(); + for (NodeIterator nit = result.getNodes(); nit.hasNext();) { + Node n = nit.nextNode(); + l.add(n.getProperty(name)); + } + if (l.isEmpty()) { + return PropertyIteratorAdapter.EMPTY; + } else { + return new PropertyIteratorAdapter(l); + } + } catch (RepositoryException e) { + String msg = "Unable to retrieve WEAKREFERENCE properties that refer to " + id; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public NodeIterator getNodes(String[] nameGlobs) + throws RepositoryException { + // check state of this instance + sanityCheck(); + + return ChildrenCollectorFilter.collectChildNodes(this, nameGlobs); + } + + /** + * {@inheritDoc} + */ + public PropertyIterator getProperties(String[] nameGlobs) + throws RepositoryException { + // check state of this instance + sanityCheck(); + + return ChildrenCollectorFilter.collectProperties(this, nameGlobs); + } + + /** + * {@inheritDoc} + */ + public void setPrimaryType(String nodeTypeName) + throws NoSuchNodeTypeException, VersionException, + ConstraintViolationException, LockException, RepositoryException { + // check state of this instance + sanityCheck(); + + // make sure this node is checked-out, neither protected nor locked and + // the editing session has sufficient permission to change the primary type. + int options = ItemValidator.CHECK_CHECKED_OUT | ItemValidator.CHECK_LOCK + | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD; + sessionContext.getItemValidator().checkModify(this, options, Permission.NODE_TYPE_MNGMT); + + final NodeState state = data.getNodeState(); + if (state.getParentId() == null) { + String msg = "changing the primary type of the root node is not supported"; + log.debug(msg); + throw new RepositoryException(msg); + } + + Name ntName = sessionContext.getQName(nodeTypeName); + if (ntName.equals(state.getNodeTypeName())) { + log.debug("Node already has " + nodeTypeName + " as primary node type."); + return; + } + + NodeTypeManagerImpl ntMgr = sessionContext.getNodeTypeManager(); + NodeType nt = ntMgr.getNodeType(ntName); + if (nt.isMixin()) { + throw new ConstraintViolationException(nodeTypeName + ": not a primary node type."); + } else if (nt.isAbstract()) { + throw new ConstraintViolationException(nodeTypeName + ": is an abstract node type."); + } + + // build effective node type of new primary type & existing mixin's + // in order to detect conflicts + NodeTypeRegistry ntReg = ntMgr.getNodeTypeRegistry(); + EffectiveNodeType entNew, entOld, entAll; + try { + entNew = ntReg.getEffectiveNodeType(ntName); + entOld = ntReg.getEffectiveNodeType(state.getNodeTypeName()); + + // try to build new effective node type (will throw in case of conflicts) + entAll = ntReg.getEffectiveNodeType(ntName, state.getMixinTypeNames()); + } catch (NodeTypeConflictException ntce) { + throw new ConstraintViolationException(ntce.getMessage()); + } + + // get applicable definition for this node using new primary type + QNodeDefinition nodeDef; + try { + NodeImpl parent = (NodeImpl) getParent(); + nodeDef = parent.getApplicableChildNodeDefinition(getQName(), ntName).unwrap(); + } catch (RepositoryException re) { + String msg = this + ": no applicable definition found in parent node's node type"; + log.debug(msg); + throw new ConstraintViolationException(msg, re); + } + + if (!nodeDef.equals(itemMgr.getDefinition(state).unwrap())) { + onRedefine(nodeDef); + } + + Set oldDefs = new HashSet(Arrays.asList(entOld.getAllItemDefs())); + Set newDefs = new HashSet(Arrays.asList(entNew.getAllItemDefs())); + Set allDefs = new HashSet(Arrays.asList(entAll.getAllItemDefs())); + + // added child item definitions + Set addedDefs = new HashSet(newDefs); + addedDefs.removeAll(oldDefs); + + // referential integrity check + boolean referenceableOld = entOld.includesNodeType(NameConstants.MIX_REFERENCEABLE); + boolean referenceableNew = entNew.includesNodeType(NameConstants.MIX_REFERENCEABLE); + if (referenceableOld && !referenceableNew) { + // node would become non-referenceable; + // make sure no references exist + PropertyIterator iter = getReferences(); + if (iter.hasNext()) { + throw new ConstraintViolationException( + "the new primary type cannot be set as it would render " + + "this node 'non-referenceable' while it is still being " + + "referenced through at least one property of type REFERENCE"); + } + } + + // do the actual modifications in content as mandated by the new primary type + + // modify the state of this node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + thisState.setNodeTypeName(ntName); + + // set jcr:primaryType property + internalSetProperty(NameConstants.JCR_PRIMARYTYPE, InternalValue.create(ntName)); + + // walk through properties and child nodes and change definition as necessary + + // use temp set to avoid ConcurrentModificationException + HashSet set = new HashSet(thisState.getPropertyNames()); + for (Name propName : set) { + try { + PropertyState propState = + (PropertyState) stateMgr.getItemState( + new PropertyId(thisState.getNodeId(), propName)); + if (!allDefs.contains(itemMgr.getDefinition(propState).unwrap())) { + // try to find new applicable definition first and + // redefine property if possible + try { + PropertyImpl prop = (PropertyImpl) itemMgr.getItem(propState.getId()); + if (prop.getDefinition().isProtected()) { + // remove 'orphaned' protected properties immediately + removeChildProperty(propName); + continue; + } + PropertyDefinitionImpl pdi = getApplicablePropertyDefinition( + propName, propState.getType(), + propState.isMultiValued(), false); + if (pdi.getRequiredType() != PropertyType.UNDEFINED + && pdi.getRequiredType() != propState.getType()) { + // value conversion required + if (propState.isMultiValued()) { + // convert value + Value[] values = + ValueHelper.convert( + prop.getValues(), + pdi.getRequiredType(), + getSession().getValueFactory()); + // redefine property + prop.onRedefine(pdi.unwrap()); + // set converted values + prop.setValue(values); + } else { + // convert value + Value value = + ValueHelper.convert( + prop.getValue(), + pdi.getRequiredType(), + getSession().getValueFactory()); + // redefine property + prop.onRedefine(pdi.unwrap()); + // set converted values + prop.setValue(value); + } + } else { + // redefine property + prop.onRedefine(pdi.unwrap()); + } + // update collection of added definitions + addedDefs.remove(pdi.unwrap()); + } catch (ValueFormatException vfe) { + // value conversion failed, remove it + removeChildProperty(propName); + } catch (ConstraintViolationException cve) { + // no suitable definition found for this property, + // remove it + removeChildProperty(propName); + } + } + } catch (ItemStateException ise) { + String msg = propName + ": failed to retrieve property state"; + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + } + + // use temp array to avoid ConcurrentModificationException + ArrayList list = new ArrayList(thisState.getChildNodeEntries()); + // start from tail to avoid problems with same-name siblings + for (int i = list.size() - 1; i >= 0; i--) { + ChildNodeEntry entry = list.get(i); + try { + NodeState nodeState = (NodeState) stateMgr.getItemState(entry.getId()); + if (!allDefs.contains(itemMgr.getDefinition(nodeState).unwrap())) { + // try to find new applicable definition first and + // redefine node if possible + try { + NodeImpl node = (NodeImpl) itemMgr.getItem(nodeState.getId()); + if (node.getDefinition().isProtected()) { + // remove 'orphaned' protected child node immediately + removeChildNode(entry.getId()); + continue; + } + NodeDefinitionImpl ndi = getApplicableChildNodeDefinition( + entry.getName(), + nodeState.getNodeTypeName()); + // redefine node + node.onRedefine(ndi.unwrap()); + // update collection of added definitions + addedDefs.remove(ndi.unwrap()); + } catch (ConstraintViolationException cve) { + // no suitable definition found for this child node, + // remove it + removeChildNode(entry.getId()); + } + } + } catch (ItemStateException ise) { + String msg = entry.getName() + ": failed to retrieve node state"; + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + } + + // create items that are defined as auto-created by the new primary node + // type and at the same time were not present with the old nt + for (QItemDefinition def : addedDefs) { + if (def.isAutoCreated()) { + if (def.definesNode()) { + NodeDefinitionImpl ndi = ntMgr.getNodeDefinition((QNodeDefinition) def); + createChildNode(def.getName(), (NodeTypeImpl) ndi.getDefaultPrimaryType(), null); + } else { + PropertyDefinitionImpl pdi = ntMgr.getPropertyDefinition((QPropertyDefinition) def); + createChildProperty(pdi.unwrap().getName(), pdi.getRequiredType(), pdi); + } + } + } + } + + /** + * {@inheritDoc} + */ + public Property setProperty(String name, BigDecimal value) + throws ValueFormatException, VersionException, LockException, + ConstraintViolationException, RepositoryException { + Value v = null; + if (value != null) { + v = getSession().getValueFactory().createValue(value); + } + return setProperty(name, v); + } + + /** + * {@inheritDoc} + */ + public Property setProperty(String name, Binary value) + throws ValueFormatException, VersionException, LockException, + ConstraintViolationException, RepositoryException { + Value v = null; + if (value != null) { + v = getSession().getValueFactory().createValue(value); + } + return setProperty(name, v); + } + + /** + * Returns all allowed transitions from the current lifecycle state of + * this node. + *

    + * The lifecycle policy node referenced by the "jcr:lifecyclePolicy" + * property is expected to contain a "transitions" node with a list of + * child nodes, one for each transition. These transition nodes must + * have single-valued string "from" and "to" properties that identify + * the allowed source and target states of each transition. + *

    + * Note that future versions of Apache Jackrabbit may well use different + * lifecycle policy implementations. + * + * @since Apache Jackrabbit 2.0 + * @return allowed transitions for the current lifecycle state of this node + * @throws UnsupportedRepositoryOperationException + * if this node does not have the mix:lifecycle mixin node type + * @throws RepositoryException if a repository error occurs + */ + public String[] getAllowedLifecycleTransistions() + throws UnsupportedRepositoryOperationException, RepositoryException { + if (isNodeType(NameConstants.MIX_LIFECYCLE)) { + Node policy = getProperty(JCR_LIFECYCLE_POLICY).getNode(); + String state = getProperty(JCR_CURRENT_LIFECYCLE_STATE).getString(); + + List targetStates = new ArrayList(); + if (policy.hasNode("transitions")) { + Node transitions = policy.getNode("transitions"); + for (Node transition : JcrUtils.getChildNodes(transitions)) { + String from = transition.getProperty("from").getString(); + if (from.equals(state)) { + String to = transition.getProperty("to").getString(); + targetStates.add(to); + } + } + } + + return targetStates.toArray(new String[targetStates.size()]); + } else { + throw new UnsupportedRepositoryOperationException( + "Only nodes with mixin node type mix:lifecycle" + + " may participate in a lifecycle: " + this); + } + } + + /** + * Transitions this node through its lifecycle to the given target state. + * + * @since Apache Jackrabbit 2.0 + * @see #getAllowedLifecycleTransistions() + * @param transition target lifecycle state + * @throws UnsupportedRepositoryOperationException + * if this node does not have the mix:lifecycle mixin node type + * @throws InvalidLifecycleTransitionException + * if the given target state is not among the allowed + * transitions from the current lifecycle state of this node + * @throws RepositoryException if a repository error occurs + */ + public void followLifecycleTransition(String transition) + throws UnsupportedRepositoryOperationException, + InvalidLifecycleTransitionException, RepositoryException { + // getAllowedLifecycleTransitions checks for the mix:lifecycle mixin + for (String target : getAllowedLifecycleTransistions()) { + if (target.equals(transition)) { + PropertyImpl property = getProperty(JCR_CURRENT_LIFECYCLE_STATE); + property.internalSetValue( + new InternalValue[] { InternalValue.create(target) }, + PropertyType.STRING); + property.save(); + return; + } + } + + // No valid transition found + throw new InvalidLifecycleTransitionException( + "Invalid lifecycle transition \"" + + transition + "\" for " + this); + } + + /** + * Assigns the given lifecycle policy to this node and sets the + * current state to the one given. + *

    + * Note that currently no special checks are made against the given + * arguments, and that you will need to explicitly persist these changes + * by calling save(). + *

    + * Note that future versions of Apache Jackrabbit may well use different + * lifecycle policy implementations. + * + * @param policy lifecycle policy node + * @param state current lifecycle state + * @throws RepositoryException if a repository error occurs + */ + public void assignLifecyclePolicy(Node policy, String state) + throws RepositoryException { + if (!(policy instanceof NodeImpl) + || !((NodeImpl) policy).isNodeType(MIX_REFERENCEABLE)) { + throw new RepositoryException( + policy + " is not referenceable, so it can not be" + + " used as a lifecycle policy"); + } + + addMixin(MIX_LIFECYCLE); + internalSetProperty( + JCR_LIFECYCLE_POLICY, + InternalValue.create(((NodeImpl) policy).getNodeId())); + internalSetProperty( + JCR_CURRENT_LIFECYCLE_STATE, + InternalValue.create(state)); + } + + //-------------------------------------------------------< JackrabbitNode > + + /** + * {@inheritDoc} + */ + public void rename(String newName) throws RepositoryException { + // check if this is the root node + if (getDepth() == 0) { + throw new RepositoryException("Cannot rename the root node"); + } + + Name qName; + try { + qName = sessionContext.getQName(newName); + } catch (NameException e) { + throw new RepositoryException("invalid node name: " + newName, e); + } + + NodeImpl parent = (NodeImpl) getParent(); + + // check for name collisions + NodeImpl existing = null; + try { + existing = parent.getNode(qName); + // there's already a node with that name: + // check same-name sibling setting of existing node + if (!existing.getDefinition().allowsSameNameSiblings()) { + throw new ItemExistsException( + "Same name siblings are not allowed: " + existing); + } + } catch (AccessDeniedException ade) { + // FIXME by throwing ItemExistsException we're disclosing too much information + throw new ItemExistsException(); + } catch (ItemNotFoundException infe) { + // no name collision, fall through + } + + // verify that parent node + // - is checked-out + // - is not protected neither by node type constraints nor by retention/hold + int options = ItemValidator.CHECK_CHECKED_OUT | ItemValidator.CHECK_LOCK | + ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD | ItemValidator.CHECK_RETENTION; + sessionContext.getItemValidator().checkRemove(parent, options, Permission.NONE); + sessionContext.getItemValidator().checkModify(parent, options, Permission.NONE); + + // check constraints + // get applicable definition of renamed target node + NodeTypeImpl nt = (NodeTypeImpl) getPrimaryNodeType(); + org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl newTargetDef; + try { + newTargetDef = parent.getApplicableChildNodeDefinition(qName, nt.getQName()); + } catch (RepositoryException re) { + String msg = safeGetJCRPath() + ": no definition found in parent node's node type for renamed node"; + log.debug(msg); + throw new ConstraintViolationException(msg, re); + } + // if there's already a node with that name also check same-name sibling + // setting of new node; just checking same-name sibling setting on + // existing node is not sufficient since same-name sibling nodes don't + // necessarily have identical definitions + if (existing != null && !newTargetDef.allowsSameNameSiblings()) { + throw new ItemExistsException( + "Same name siblings not allowed: " + existing); + } + + // check permissions: + // 1. on the parent node the session must have permission to manipulate the child-entries + AccessManager acMgr = sessionContext.getAccessManager(); + if (!acMgr.isGranted(parent.getPrimaryPath(), qName, Permission.MODIFY_CHILD_NODE_COLLECTION)) { + String msg = "Not allowed to rename node " + safeGetJCRPath() + " to " + newName; + log.debug(msg); + throw new AccessDeniedException(msg); + } + // 2. in case of nt-changes the session must have permission to change + // the primary node type on this node itself. + if (!nt.getName().equals(newTargetDef.getName()) && !(acMgr.isGranted(getPrimaryPath(), Permission.NODE_TYPE_MNGMT))) { + String msg = "Not allowed to rename node " + safeGetJCRPath() + " to " + newName; + log.debug(msg); + throw new AccessDeniedException(msg); + } + + // change definition + onRedefine(newTargetDef.unwrap()); + + // delegate to parent + parent.renameChildNode(getNodeId(), qName, true); + } + + /** + * {@inheritDoc} + */ + public void setMixins(String[] mixinNames) + throws NoSuchNodeTypeException, VersionException, + ConstraintViolationException, LockException, RepositoryException { + + // check state of this instance + sanityCheck(); + + NodeTypeManagerImpl ntMgr = sessionContext.getNodeTypeManager(); + + Set newMixins = new HashSet(); + for (String name : mixinNames) { + Name qName = sessionContext.getQName(name); + if (! ntMgr.getNodeType(qName).isMixin()) { + throw new RepositoryException( + sessionContext.getJCRName(qName) + " is not a mixin node type"); + } + newMixins.add(qName); + } + + // make sure this node is checked-out, neither protected nor locked and + // the editing session has sufficient permission to change the mixin types. + + // special handling of mix:(simple)versionable. since adding the + // mixin alters the version storage jcr:versionManagement privilege + // is required in addition. + int permissions = Permission.NODE_TYPE_MNGMT; + if (newMixins.contains(MIX_VERSIONABLE) + || newMixins.contains(MIX_SIMPLE_VERSIONABLE)) { + permissions |= Permission.VERSION_MNGMT; + } + int options = ItemValidator.CHECK_CHECKED_OUT | ItemValidator.CHECK_LOCK + | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD; + sessionContext.getItemValidator().checkModify(this, options, permissions); + + final NodeState state = data.getNodeState(); + + // build effective node type of primary type & new mixin's + // in order to detect conflicts + NodeTypeRegistry ntReg = ntMgr.getNodeTypeRegistry(); + EffectiveNodeType entNew, entOld, entAll; + try { + entNew = ntReg.getEffectiveNodeType(newMixins); + entOld = ntReg.getEffectiveNodeType(state.getMixinTypeNames()); + + // try to build new effective node type (will throw in case of conflicts) + entAll = ntReg.getEffectiveNodeType(state.getNodeTypeName(), newMixins); + } catch (NodeTypeConflictException ntce) { + throw new ConstraintViolationException(ntce.getMessage()); + } + + // added child item definitions + Set addedDefs = new HashSet(Arrays.asList(entNew.getAllItemDefs())); + addedDefs.removeAll(Arrays.asList(entOld.getAllItemDefs())); + + // referential integrity check + boolean referenceableOld = getEffectiveNodeType().includesNodeType(NameConstants.MIX_REFERENCEABLE); + boolean referenceableNew = entAll.includesNodeType(NameConstants.MIX_REFERENCEABLE); + if (referenceableOld && !referenceableNew) { + // node would become non-referenceable; + // make sure no references exist + PropertyIterator iter = getReferences(); + if (iter.hasNext()) { + throw new ConstraintViolationException( + "the new mixin types cannot be set as it would render " + + "this node 'non-referenceable' while it is still being " + + "referenced through at least one property of type REFERENCE"); + } + } + + // gather currently assigned definitions *before* doing actual modifications + Map oldDefs = new HashMap(); + for (Name name : getNodeState().getPropertyNames()) { + PropertyId id = new PropertyId(getNodeId(), name); + try { + PropertyState propState = (PropertyState) stateMgr.getItemState(id); + oldDefs.put(id, itemMgr.getDefinition(propState)); + } catch (ItemStateException ise) { + String msg = name + ": failed to retrieve property state"; + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + } + for (ChildNodeEntry cne : getNodeState().getChildNodeEntries()) { + try { + NodeState nodeState = (NodeState) stateMgr.getItemState(cne.getId()); + oldDefs.put(cne.getId(), itemMgr.getDefinition(nodeState)); + } catch (ItemStateException ise) { + String msg = cne + ": failed to retrieve node state"; + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + } + + // now do the actual modifications in content as mandated by the new mixins + + // modify the state of this node + NodeState thisState = (NodeState) getOrCreateTransientItemState(); + thisState.setMixinTypeNames(newMixins); + + // set jcr:mixinTypes property + setMixinTypesProperty(newMixins); + + // walk through properties and child nodes and change definition as necessary + + // use temp set to avoid ConcurrentModificationException + HashSet set = new HashSet(thisState.getPropertyNames()); + for (Name propName : set) { + PropertyState propState = null; + try { + propState = (PropertyState) stateMgr.getItemState( + new PropertyId(thisState.getNodeId(), propName)); + // the following call triggers ConstraintViolationException + // if there isn't any suitable definition anymore + itemMgr.getDefinition(propState); + } catch (ConstraintViolationException cve) { + // no suitable definition found for this property + // try to find new applicable definition first and + // redefine property if possible + try { + if (oldDefs.get(propState.getId()).isProtected()) { + // remove 'orphaned' protected properties immediately + removeChildProperty(propName); + continue; + } + PropertyDefinitionImpl pdi = getApplicablePropertyDefinition( + propName, propState.getType(), + propState.isMultiValued(), false); + PropertyImpl prop = (PropertyImpl) itemMgr.getItem(propState.getId()); + if (pdi.getRequiredType() != PropertyType.UNDEFINED + && pdi.getRequiredType() != propState.getType()) { + // value conversion required + if (propState.isMultiValued()) { + // convert value + Value[] values = + ValueHelper.convert( + prop.getValues(), + pdi.getRequiredType(), + getSession().getValueFactory()); + // redefine property + prop.onRedefine(pdi.unwrap()); + // set converted values + prop.setValue(values); + } else { + // convert value + Value value = + ValueHelper.convert( + prop.getValue(), + pdi.getRequiredType(), + getSession().getValueFactory()); + // redefine property + prop.onRedefine(pdi.unwrap()); + // set converted values + prop.setValue(value); + } + } else { + // redefine property + prop.onRedefine(pdi.unwrap()); + } + // update collection of added definitions + addedDefs.remove(pdi.unwrap()); + } catch (ValueFormatException vfe) { + // value conversion failed, remove it + removeChildProperty(propName); + } catch (ConstraintViolationException cve1) { + // no suitable definition found for this property, + // remove it + removeChildProperty(propName); + } + } catch (ItemStateException ise) { + String msg = propName + ": failed to retrieve property state"; + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + } + + // use temp array to avoid ConcurrentModificationException + ArrayList list = new ArrayList(thisState.getChildNodeEntries()); + // start from tail to avoid problems with same-name siblings + for (int i = list.size() - 1; i >= 0; i--) { + ChildNodeEntry entry = list.get(i); + NodeState nodeState = null; + try { + nodeState = (NodeState) stateMgr.getItemState(entry.getId()); + // the following call triggers ConstraintViolationException + // if there isn't any suitable definition anymore + itemMgr.getDefinition(nodeState); + } catch (ConstraintViolationException cve) { + // no suitable definition found for this child node + // try to find new applicable definition first and + // redefine node if possible + try { + if (oldDefs.get(nodeState.getId()).isProtected()) { + // remove 'orphaned' protected child node immediately + removeChildNode(entry.getId()); + continue; + } + NodeDefinitionImpl ndi = getApplicableChildNodeDefinition( + entry.getName(), + nodeState.getNodeTypeName()); + NodeImpl node = (NodeImpl) itemMgr.getItem(nodeState.getId()); + // redefine node + node.onRedefine(ndi.unwrap()); + // update collection of added definitions + addedDefs.remove(ndi.unwrap()); + } catch (ConstraintViolationException cve1) { + // no suitable definition found for this child node, + // remove it + removeChildNode(entry.getId()); + } + } catch (ItemStateException ise) { + String msg = entry + ": failed to retrieve node state"; + log.error(msg, ise); + throw new RepositoryException(msg, ise); + } + } + + // create items that are defined as auto-created by the new mixins + // and at the same time were not present with the old mixins + for (QItemDefinition def : addedDefs) { + if (def.isAutoCreated()) { + if (def.definesNode()) { + NodeDefinitionImpl ndi = ntMgr.getNodeDefinition((QNodeDefinition) def); + createChildNode(def.getName(), (NodeTypeImpl) ndi.getDefaultPrimaryType(), null); + } else { + PropertyDefinitionImpl pdi = ntMgr.getPropertyDefinition((QPropertyDefinition) def); + createChildProperty(pdi.unwrap().getName(), pdi.getRequiredType(), pdi); + } + } + } + } + + //--------------------------------------------------------------< Object > + + /** + * Return a string representation of this node for diagnostic purposes. + * + * @return "node /path/to/item" + */ + public String toString() { + return "node " + super.toString(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeTypeInstanceHandler.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeTypeInstanceHandler.java new file mode 100644 index 00000000000..c82125430f7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/NodeTypeInstanceHandler.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.core; + +import java.util.Calendar; +import java.util.Set; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * The NodeTypeInstanceHandler is used to provide or initialize + * system protected properties (or child nodes). + * + */ +public class NodeTypeInstanceHandler { + + /** + * Default user id in the case where the creating user cannot be determined. + */ + public static final String DEFAULT_USERID = "system"; + + /** + * userid to use for the "*By" autocreated properties + */ + private final String userId; + + /** + * Creates a new node type instance handler. + * @param userId the user id. if null, {@value #DEFAULT_USERID} is used. + */ + public NodeTypeInstanceHandler(String userId) { + this.userId = userId == null + ? DEFAULT_USERID + : userId; + } + + /** + * Sets the system-generated or node type -specified default values + * of the given property. If such values are not specified, then the + * property is not modified. + * + * @param property property state + * @param parent parent node state + * @param def property definition + * @throws RepositoryException if the default values could not be created + */ + public void setDefaultValues( + PropertyState property, NodeState parent, QPropertyDefinition def) + throws RepositoryException { + InternalValue[] values = + computeSystemGeneratedPropertyValues(parent, def); + if (values == null && def.getDefaultValues() != null) { + values = InternalValue.create(def.getDefaultValues()); + } + if (values != null) { + property.setValues(values); + } + } + + /** + * Computes the values of well-known system (i.e. protected) properties. + * + * @param parent the parent node state + * @param def the definition of the property to compute + * @return the computed values + */ + public InternalValue[] computeSystemGeneratedPropertyValues(NodeState parent, + QPropertyDefinition def) { + + InternalValue[] genValues = null; + + Name name = def.getName(); + Name declaringNT = def.getDeclaringNodeType(); + + if (NameConstants.JCR_UUID.equals(name)) { + // jcr:uuid property of the mix:referenceable node type + if (NameConstants.MIX_REFERENCEABLE.equals(declaringNT)) { + genValues = new InternalValue[]{InternalValue.create(parent.getNodeId().toString())}; + } + } else if (NameConstants.JCR_PRIMARYTYPE.equals(name)) { + // jcr:primaryType property (of any node type) + genValues = new InternalValue[]{InternalValue.create(parent.getNodeTypeName())}; + } else if (NameConstants.JCR_MIXINTYPES.equals(name)) { + // jcr:mixinTypes property (of any node type) + Set mixins = parent.getMixinTypeNames(); + genValues = new InternalValue[mixins.size()]; + int i = 0; + for (Name n : mixins) { + genValues[i++] = InternalValue.create(n); + } + } else if (NameConstants.JCR_CREATED.equals(name)) { + // jcr:created property of a version or a mix:created + if (NameConstants.MIX_CREATED.equals(declaringNT) + || NameConstants.NT_VERSION.equals(declaringNT)) { + genValues = new InternalValue[]{InternalValue.create(Calendar.getInstance())}; + } + } else if (NameConstants.JCR_CREATEDBY.equals(name)) { + // jcr:createdBy property of a mix:created + if (NameConstants.MIX_CREATED.equals(declaringNT)) { + genValues = new InternalValue[]{InternalValue.create(userId)}; + } + } else if (NameConstants.JCR_LASTMODIFIED.equals(name)) { + // jcr:lastModified property of a mix:lastModified + if (NameConstants.MIX_LASTMODIFIED.equals(declaringNT)) { + genValues = new InternalValue[]{InternalValue.create(Calendar.getInstance())}; + } + } else if (NameConstants.JCR_LASTMODIFIEDBY.equals(name)) { + // jcr:lastModifiedBy property of a mix:lastModified + if (NameConstants.MIX_LASTMODIFIED.equals(declaringNT)) { + genValues = new InternalValue[]{InternalValue.create(userId)}; + } + } else if (NameConstants.JCR_ETAG.equals(name)) { + // jcr:etag property of a mix:etag + if (NameConstants.MIX_ETAG.equals(declaringNT)) { + // TODO: provide real implementation + genValues = new InternalValue[]{InternalValue.create("")}; + } + } + return genValues; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/PropertyData.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/PropertyData.java new file mode 100644 index 00000000000..30e38c07f9e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/PropertyData.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.core; + +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.state.PropertyState; + +/** + * Data object representing a property. + */ +public class PropertyData extends ItemData { + + /** + * Create a new instance of this class. + * + * @param state associated property state + * @param itemMgr item manager + */ + PropertyData(PropertyState state, ItemManager itemMgr) { + super(state, itemMgr); + } + + /** + * Return the associated property state. + * + * @return property state + */ + public PropertyState getPropertyState() { + return (PropertyState) getState(); + } + + /** + * Return the associated property definition. + * + * @return property definition + * @throws RepositoryException if the definition cannot be retrieved. + */ + public PropertyDefinition getPropertyDefinition() throws RepositoryException { + return (PropertyDefinition) getDefinition(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/PropertyImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/PropertyImpl.java new file mode 100644 index 00000000000..e89f2453ce2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/PropertyImpl.java @@ -0,0 +1,926 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import static javax.jcr.PropertyType.BINARY; +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.UNDEFINED; +import static javax.jcr.PropertyType.WEAKREFERENCE; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CHECKED_OUT; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CONSTRAINTS; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_RETENTION; + +import java.io.InputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Calendar; + +import javax.jcr.Binary; +import javax.jcr.InvalidItemStateException; +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.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; +import org.apache.jackrabbit.value.ValueHelper; +import org.apache.commons.io.input.AutoCloseInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * PropertyImpl implements the Property interface. + */ +public class PropertyImpl extends ItemImpl implements Property { + + private static Logger log = LoggerFactory.getLogger(PropertyImpl.class); + + /** property data (avoids casting ItemImpl.data) */ + private final PropertyData data; + + /** + * Package private constructor. + * + * @param itemMgr the ItemManager that created this Property + * @param sessionContext the component context of the associated session + * @param data the property data + */ + PropertyImpl( + ItemManager itemMgr, SessionContext sessionContext, + PropertyData data) { + super(itemMgr, sessionContext, data); + this.data = data; + // value will be read on demand + } + + /** + * Checks that this property is valid (session not closed, property not + * removed, etc.) and returns the underlying property state if all is OK. + * + * @return property state + * @throws RepositoryException if the property is not valid + */ + private PropertyState getPropertyState() throws RepositoryException { + // JCR-1272: Need to get the state reference now so it + // doesn't get invalidated after the sanity check + ItemState state = getItemState(); + sanityCheck(); + return (PropertyState) state; + } + + @Override + protected synchronized ItemState getOrCreateTransientItemState() + throws RepositoryException { + + synchronized (data) { + if (!isTransient()) { + // make transient (copy-on-write) + try { + PropertyState transientState = + stateMgr.createTransientPropertyState( + data.getPropertyState(), ItemState.STATUS_EXISTING_MODIFIED); + // swap persistent with transient state + data.setState(transientState); + } catch (ItemStateException ise) { + String msg = "failed to create transient state"; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + return getItemState(); + } + } + + @Override + protected void makePersistent() throws InvalidItemStateException { + if (!isTransient()) { + log.debug(this + " (" + id + "): there's no transient state to persist"); + return; + } + + PropertyState transientState = data.getPropertyState(); + PropertyState persistentState = (PropertyState) transientState.getOverlayedState(); + if (persistentState == null) { + // this property is 'new' + try { + persistentState = stateMgr.createNew(transientState); + } catch (ItemStateException e) { + throw new InvalidItemStateException(e); + } + } + + synchronized (persistentState) { + // check staleness of transient state first + if (transientState.isStale()) { + String msg = + this + ": the property cannot be saved because it has" + + " been modified externally."; + log.debug(msg); + throw new InvalidItemStateException(msg); + } + // copy state from transient state + persistentState.setType(transientState.getType()); + persistentState.setMultiValued(transientState.isMultiValued()); + persistentState.setValues(transientState.getValues()); + // make state persistent + stateMgr.store(persistentState); + } + + // tell state manager to disconnect item state + stateMgr.disconnectTransientItemState(transientState); + // swap transient state with persistent state + data.setState(persistentState); + // reset status + data.setStatus(STATUS_NORMAL); + } + + protected void restoreTransient(PropertyState transientState) + throws RepositoryException { + PropertyState thisState = null; + + if (!isTransient()) { + thisState = (PropertyState) getOrCreateTransientItemState(); + if (transientState.getStatus() == ItemState.STATUS_NEW + && thisState.getStatus() != ItemState.STATUS_NEW) { + thisState.setStatus(ItemState.STATUS_NEW); + stateMgr.disconnectTransientItemState(thisState); + } + } else { + // JCR-2503: Re-create transient state in the state manager, + // because it was removed + synchronized (data) { + try { + thisState = stateMgr.createTransientPropertyState( + transientState.getParentId(), + transientState.getName(), + PropertyState.STATUS_NEW); + data.setState(thisState); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + } + + // reapply transient changes + thisState.setType(transientState.getType()); + thisState.setMultiValued(transientState.isMultiValued()); + thisState.setValues(transientState.getValues()); + thisState.setModCount(transientState.getModCount()); + } + + protected void onRedefine(QPropertyDefinition def) throws RepositoryException { + PropertyDefinitionImpl newDef = + sessionContext.getNodeTypeManager().getPropertyDefinition(def); + data.setDefinition(newDef); + } + + /** + * Determines the length of the given value. + * + * @param value value whose length should be determined + * @return the length of the given value + * @throws RepositoryException if an error occurs + * @see javax.jcr.Property#getLength() + * @see javax.jcr.Property#getLengths() + */ + protected long getLength(InternalValue value) throws RepositoryException { + long length; + switch (value.getType()) { + case NAME: + case PATH: + String str = ValueFormat.getJCRString(value, sessionContext); + length = str.length(); + break; + default: + length = value.getLength(); + break; + } + return length; + } + + /** + * Checks various pre-conditions that are common to all + * setValue() methods. The checks performed are: + *

      + *
    • parent node must be checked-out
    • + *
    • property must not be protected
    • + *
    • parent node must not be locked by somebody else
    • + *
    • property must be multi-valued when set to an array of values + * (and vice versa)
    • + *
    + * + * @param multipleValues flag indicating whether the property is about to + * be set to an array of values + * @throws ValueFormatException if a single-valued property is set to an + * array of values (and vice versa) + * @throws VersionException if the parent node is not checked-out + * @throws LockException if the parent node is locked by somebody else + * @throws ConstraintViolationException if the property is protected + * @throws RepositoryException if another error occurs + * @see javax.jcr.Property#setValue + */ + protected void checkSetValue(boolean multipleValues) + throws ValueFormatException, VersionException, + LockException, ConstraintViolationException, + RepositoryException { + NodeImpl parent = (NodeImpl) getParent(false); + // check multi-value flag + if (multipleValues != isMultiple()) { + String msg = (multipleValues) ? + "Single-valued property can not be set to an array of values:" : + "Multivalued property can not be set to a single value (an array of length one is OK): "; + throw new ValueFormatException(msg + this); + } + + // check protected flag and for retention/hold + sessionContext.getItemValidator().checkModify( + this, CHECK_CONSTRAINTS, Permission.NONE); + + // make sure the parent is checked-out and neither locked nor under retention + sessionContext.getItemValidator().checkModify( + parent, + CHECK_CHECKED_OUT | CHECK_LOCK | CHECK_HOLD | CHECK_RETENTION, + Permission.NONE); + } + + /** + * @param values + * @param type + * @throws ConstraintViolationException + * @throws RepositoryException + */ + protected void internalSetValue(InternalValue[] values, int type) + throws ConstraintViolationException, RepositoryException { + // check for null value + if (values == null) { + // setting a property to null removes it automatically + ((NodeImpl) getParent()).removeChildProperty(((PropertyId) id).getName()); + return; + } + ArrayList list = new ArrayList(); + // compact array (purge null entries) + for (InternalValue v : values) { + if (v != null) { + list.add(v); + } + } + values = list.toArray(new InternalValue[list.size()]); + + // modify the state of this property + PropertyState thisState = (PropertyState) getOrCreateTransientItemState(); + + // free old values as necessary + InternalValue[] oldValues = thisState.getValues(); + if (oldValues != null) { + for (InternalValue old : oldValues) { + if (old != null && old.getType() == BINARY) { + // make sure temporarily allocated data is discarded + // before overwriting it + old.discard(); + } + } + } + + // set new values + thisState.setValues(values); + // set type + if (type == UNDEFINED) { + // fallback to default type + type = STRING; + } + thisState.setType(type); + } + + protected Node getParent(boolean checkPermission) throws RepositoryException { + return (Node) itemMgr.getItem(getPropertyState().getParentId(), checkPermission); + } + + /** + * Same as {@link Property#setValue(String)} except that + * this method takes a Name instead of a String + * value. + * + * @param name + * @throws ValueFormatException + * @throws VersionException + * @throws LockException + * @throws ConstraintViolationException + * @throws RepositoryException + */ + public void setValue(Name name) + throws ValueFormatException, VersionException, + LockException, ConstraintViolationException, + RepositoryException { + // check state of this instance + sanityCheck(); + + // check pre-conditions for setting property value + checkSetValue(false); + + // check type according to definition of this property + final PropertyDefinition definition = data.getPropertyDefinition(); + int reqType = definition.getRequiredType(); + if (reqType == UNDEFINED) { + reqType = NAME; + } + + if (name == null) { + internalSetValue(null, reqType); + return; + } + + InternalValue internalValue; + if (reqType != NAME) { + // type conversion required + Value targetValue = ValueHelper.convert( + ValueFormat.getJCRValue(InternalValue.create(name), sessionContext, getSession().getValueFactory()), + reqType, getSession().getValueFactory()); + internalValue = InternalValue.create( + targetValue, sessionContext, sessionContext.getDataStore()); + } else { + // no type conversion required + internalValue = InternalValue.create(name); + } + + internalSetValue(new InternalValue[]{internalValue}, reqType); + } + + /** + * Same as {@link Property#setValue(String[])} except that + * this method takes an array of Name instead of + * String values. + * + * @param names + * @throws ValueFormatException + * @throws VersionException + * @throws LockException + * @throws ConstraintViolationException + * @throws RepositoryException + */ + public void setValue(Name[] names) + throws ValueFormatException, VersionException, + LockException, ConstraintViolationException, + RepositoryException { + // check state of this instance + sanityCheck(); + + // check pre-conditions for setting property value + checkSetValue(true); + + // check type according to definition of this property + final PropertyDefinition definition = data.getPropertyDefinition(); + int reqType = definition.getRequiredType(); + if (reqType == UNDEFINED) { + reqType = NAME; + } + + InternalValue[] internalValues = null; + // convert to internal values of correct type + if (names != null) { + internalValues = new InternalValue[names.length]; + for (int i = 0; i < names.length; i++) { + Name name = names[i]; + InternalValue internalValue = null; + if (name != null) { + if (reqType != NAME) { + // type conversion required + Value targetValue = ValueHelper.convert( + ValueFormat.getJCRValue(InternalValue.create(name), sessionContext, getSession().getValueFactory()), + reqType, getSession().getValueFactory()); + internalValue = InternalValue.create( + targetValue, sessionContext, + sessionContext.getDataStore()); + } else { + // no type conversion required + internalValue = InternalValue.create(name); + } + } + internalValues[i] = internalValue; + } + } + + internalSetValue(internalValues, reqType); + } + + /** + * {@inheritDoc} + */ + @Override + public Name getQName() { + return ((PropertyId) id).getName(); + } + + /** + * Returns the internal values of a multi-valued property. + * + * @return array of values + * @throws ValueFormatException if this property is not multi-valued + * @throws RepositoryException + */ + public InternalValue[] internalGetValues() throws RepositoryException { + final PropertyDefinition definition = data.getPropertyDefinition(); + if (isMultiple()) { + return getPropertyState().getValues(); + } else { + throw new ValueFormatException( + this + " is a single-valued property," + + " so it's value can not be retrieved as an array"); + } + + } + + /** + * Returns the internal value of a single-valued property. + * + * @return value + * @throws ValueFormatException if this property is not single-valued + * @throws RepositoryException + */ + public InternalValue internalGetValue() throws RepositoryException { + if (isMultiple()) { + throw new ValueFormatException( + this + " is a multi-valued property," + + " so it's values can only be retrieved as an array"); + } else { + InternalValue[] values = getPropertyState().getValues(); + if (values.length > 0) { + return values[0]; + } else { + // should never be the case, but being a little paranoid can't hurt... + throw new RepositoryException(this + ": single-valued property with no value"); + } + } + } + + //-------------------------------------------------------------< Property > + + public Value[] getValues() throws RepositoryException { + InternalValue[] internals = internalGetValues(); + Value[] values = new Value[internals.length]; + for (int i = 0; i < internals.length; i++) { + values[i] = ValueFormat.getJCRValue(internals[i], sessionContext, getSession().getValueFactory()); + } + return values; + } + + public Value getValue() throws RepositoryException { + try { + return ValueFormat.getJCRValue(internalGetValue(), sessionContext, getSession().getValueFactory()); + } catch (RuntimeException e) { + String msg = "Internal error while retrieving value of " + this; + log.error(msg, e); + throw new RepositoryException(msg, e); + } + } + + /** Wrapper around {@link #getValue()} */ + public String getString() throws RepositoryException { + return getValue().getString(); + } + + /** Wrapper around {@link #getValue()} */ + public InputStream getStream() throws RepositoryException { + final Binary binary = getValue().getBinary(); + // make sure binary is disposed after stream had been consumed + return new AutoCloseInputStream(binary.getStream()) { + @Override + public void close() throws IOException { + super.close(); + binary.dispose(); + } + }; + } + + /** Wrapper around {@link #getValue()} */ + public long getLong() throws RepositoryException { + return getValue().getLong(); + } + + /** Wrapper around {@link #getValue()} */ + public double getDouble() throws RepositoryException { + return getValue().getDouble(); + } + + /** Wrapper around {@link #getValue()} */ + public Calendar getDate() throws RepositoryException { + return getValue().getDate(); + } + + /** Wrapper around {@link #getValue()} */ + public boolean getBoolean() throws RepositoryException { + return getValue().getBoolean(); + } + + public Node getNode() throws ValueFormatException, RepositoryException { + Session session = getSession(); + Value value = getValue(); + int type = value.getType(); + switch (type) { + case REFERENCE: + case WEAKREFERENCE: + return session.getNodeByUUID(value.getString()); + + case PATH: + case NAME: + String path = value.getString(); + Path p = sessionContext.getQPath(path); + boolean absolute = p.isAbsolute(); + try { + return (absolute) ? session.getNode(path) : getParent().getNode(path); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(path); + } + + case STRING: + try { + Value refValue = ValueHelper.convert(value, REFERENCE, session.getValueFactory()); + return session.getNodeByUUID(refValue.getString()); + } catch (RepositoryException e) { + // try if STRING value can be interpreted as PATH value + Value pathValue = ValueHelper.convert(value, PATH, session.getValueFactory()); + p = sessionContext.getQPath(pathValue.getString()); + absolute = p.isAbsolute(); + try { + return (absolute) ? session.getNode(pathValue.getString()) : getParent().getNode(pathValue.getString()); + } catch (PathNotFoundException e1) { + throw new ItemNotFoundException(pathValue.getString()); + } + } + + default: + throw new ValueFormatException("Property value cannot be converted to a PATH, REFERENCE or WEAKREFERENCE"); + } + } + + public Property getProperty() throws RepositoryException { + Value value = getValue(); + Value pathValue = ValueHelper.convert(value, PATH, getSession().getValueFactory()); + String path = pathValue.getString(); + boolean absolute; + try { + Path p = sessionContext.getQPath(path); + absolute = p.isAbsolute(); + } catch (RepositoryException e) { + throw new ValueFormatException("Property value cannot be converted to a PATH"); + } + try { + return (absolute) ? getSession().getProperty(path) : getParent().getProperty(path); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(path); + } + } + + /** Wrapper around {@link #getValue()} */ + public BigDecimal getDecimal() throws RepositoryException { + return getValue().getDecimal(); + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(BigDecimal value) throws RepositoryException { + if (value != null) { + setValue(getValueFactory().createValue(value)); + } else { + setValue((Value) null); + } + } + + /** Wrapper around {@link #getValue()} */ + public Binary getBinary() throws RepositoryException { + return getValue().getBinary(); + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(Binary value) throws RepositoryException { + if (value != null) { + setValue(getValueFactory().createValue(value)); + } else { + setValue((Value) null); + } + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(Calendar value) throws RepositoryException { + if (value != null) { + try { + setValue(getSession().getValueFactory().createValue(value)); + } catch (IllegalArgumentException e) { + throw new ValueFormatException( + "Value is not an ISO8601 date: " + value, e); + } + } else { + setValue((Value) null); + } + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(double value) throws RepositoryException { + setValue(getValueFactory().createValue(value)); + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(InputStream value) throws RepositoryException { + if (value != null) { + Binary binary = getValueFactory().createBinary(value); + try { + setValue(getValueFactory().createValue(binary)); + } finally { + binary.dispose(); + } + } else { + setValue((Value) null); + } + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(String value) throws RepositoryException { + if (value != null) { + setValue(getValueFactory().createValue(value)); + } else { + setValue((Value) null); + } + } + + /** Wrapper around {@link #setValue(Value[])} */ + public void setValue(String[] strings) throws RepositoryException { + if (strings != null) { + setValue(getValues(strings, STRING)); + } else { + setValue((Value[]) null); + } + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(boolean value) throws RepositoryException { + setValue(getValueFactory().createValue(value)); + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(Node value) throws RepositoryException { + if (value != null) { + try { + setValue(getValueFactory().createValue(value)); + } catch (UnsupportedRepositoryOperationException e) { + throw new ValueFormatException( + "Node is not referenceable: " + value, e); + } + } else { + setValue((Value) null); + } + } + + /** Wrapper around {@link #setValue(Value)} */ + public void setValue(long value) throws RepositoryException { + setValue(getValueFactory().createValue(value)); + } + + public synchronized void setValue(Value value) + throws ValueFormatException, VersionException, + LockException, ConstraintViolationException, + RepositoryException { + // check state of this instance + sanityCheck(); + + // check pre-conditions for setting property value + checkSetValue(false); + + // check type according to definition of this property + final PropertyDefinition definition = data.getPropertyDefinition(); + int reqType = definition.getRequiredType(); + if (reqType == UNDEFINED) { + if (value != null) { + reqType = value.getType(); + } else { + reqType = STRING; + } + } + + if (value == null) { + internalSetValue(null, reqType); + return; + } + + InternalValue internalValue; + if (reqType != value.getType()) { + // type conversion required + Value targetVal = ValueHelper.convert( + value, reqType, getSession().getValueFactory()); + internalValue = InternalValue.create( + targetVal, sessionContext, sessionContext.getDataStore()); + } else { + // no type conversion required + internalValue = InternalValue.create( + value, sessionContext, sessionContext.getDataStore()); + } + internalSetValue(new InternalValue[]{internalValue}, reqType); + } + + /** + * {@inheritDoc} + */ + public void setValue(Value[] values) throws RepositoryException { + setValue(values, UNDEFINED); + } + + /** + * Sets the values of this property. + * + * @param values property values (possibly null) + * @param valueType default value type if not set in the node type, + * may be {@link PropertyType#UNDEFINED} + * @throws RepositoryException if the property values could not be set + */ + public void setValue(Value[] values, int valueType) + throws RepositoryException { + // check state of this instance + sanityCheck(); + + // check pre-conditions for setting property value + checkSetValue(true); + + if (values != null) { + // check type of values + int firstValueType = UNDEFINED; + for (Value value : values) { + if (value != null) { + if (firstValueType == UNDEFINED) { + firstValueType = value.getType(); + } else if (firstValueType != value.getType()) { + throw new ValueFormatException( + "inhomogeneous type of values"); + } + } + } + } + + final PropertyDefinition definition = data.getPropertyDefinition(); + int reqType = definition.getRequiredType(); + if (reqType == UNDEFINED) { + reqType = valueType; // use the given type as property type + } + + InternalValue[] internalValues = null; + // convert to internal values of correct type + if (values != null) { + internalValues = new InternalValue[values.length]; + + // check type of values + for (int i = 0; i < values.length; i++) { + Value value = values[i]; + if (value != null) { + if (reqType == UNDEFINED) { + // Use the type of the fist value as the type + reqType = value.getType(); + } + if (reqType != value.getType()) { + value = ValueHelper.convert( + value, reqType, getSession().getValueFactory()); + } + internalValues[i] = InternalValue.create( + value, sessionContext, sessionContext.getDataStore()); + } else { + internalValues[i] = null; + } + } + } + + internalSetValue(internalValues, reqType); + } + + /** + * {@inheritDoc} + */ + public long getLength() throws RepositoryException { + return getLength(internalGetValue()); + } + + /** + * {@inheritDoc} + */ + public long[] getLengths() throws RepositoryException { + InternalValue[] values = internalGetValues(); + long[] lengths = new long[values.length]; + for (int i = 0; i < values.length; i++) { + lengths[i] = getLength(values[i]); + } + return lengths; + } + + /** + * {@inheritDoc} + */ + public PropertyDefinition getDefinition() throws RepositoryException { + // check state of this instance + sanityCheck(); + + return data.getPropertyDefinition(); + } + + /** + * {@inheritDoc} + */ + public int getType() throws RepositoryException { + return getPropertyState().getType(); + } + + /** + * {@inheritDoc} + */ + public boolean isMultiple() throws RepositoryException { + // check state of this instance + sanityCheck(); + + return getPropertyState().isMultiValued(); + } + + //-----------------------------------------------------------------< Item > + /** + * {@inheritDoc} + */ + @Override + public boolean isNode() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() throws RepositoryException { + // check state of this instance + sanityCheck(); + return sessionContext.getJCRName(((PropertyId) id).getName()); + } + + /** + * {@inheritDoc} + */ + @Override + public void accept(ItemVisitor visitor) throws RepositoryException { + // check state of this instance + sanityCheck(); + + visitor.visit(this); + } + + /** + * {@inheritDoc} + */ + @Override + public Node getParent() throws RepositoryException { + return getParent(true); + } + + //--------------------------------------------------------------< Object > + + /** + * Return a string representation of this property for diagnostic purposes. + * + * @return "property /path/to/item" + */ + public String toString() { + return "property " + super.toString(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ProtectedItemModifier.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ProtectedItemModifier.java new file mode 100644 index 00000000000..2da032f71fd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ProtectedItemModifier.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.retention.RetentionManagerImpl; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.authentication.token.TokenProvider; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.acl.ACLEditor; +import org.apache.jackrabbit.core.security.user.UserManagerImpl; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * ProtectedItemModifier: An abstract helper class to allow classes + * residing outside of the core package to modify and remove protected items. + * The protected item definitions are required in order not to have security + * relevant content being changed through common item operations but forcing + * the usage of the corresponding APIs, which assert that implementation + * specific constraints are not violated. + */ +public abstract class ProtectedItemModifier { + + private static final int DEFAULT_PERM_CHECK = -1; + private final int permission; + + protected ProtectedItemModifier() { + this(DEFAULT_PERM_CHECK); + } + + protected ProtectedItemModifier(int permission) { + Class cl = getClass(); + if (!(UserManagerImpl.class.isAssignableFrom(cl) || + RetentionManagerImpl.class.isAssignableFrom(cl) || + ACLEditor.class.isAssignableFrom(cl) || + TokenProvider.class.isAssignableFrom(cl) || + org.apache.jackrabbit.core.security.authorization.principalbased.ACLEditor.class.isAssignableFrom(cl))) { + throw new IllegalArgumentException("Only UserManagerImpl, RetentionManagerImpl and ACLEditor may extend from the ProtectedItemModifier"); + } + this.permission = permission; + } + + protected NodeImpl addNode(NodeImpl parentImpl, Name name, Name ntName) throws RepositoryException { + return addNode(parentImpl, name, ntName, null); + } + + protected NodeImpl addNode(NodeImpl parentImpl, Name name, Name ntName, NodeId nodeId) throws RepositoryException { + checkPermission(parentImpl, name, getPermission(true, false)); + // validation: make sure Node is not locked or checked-in. + parentImpl.checkSetProperty(); + + NodeTypeImpl nodeType = parentImpl.sessionContext.getNodeTypeManager().getNodeType(ntName); + org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl def = parentImpl.getApplicableChildNodeDefinition(name, ntName); + + // check for name collisions + // TODO: improve. copied from NodeImpl + NodeState thisState = parentImpl.getNodeState(); + ChildNodeEntry cne = thisState.getChildNodeEntry(name, 1); + if (cne != null) { + // there's already a child node entry with that name; + // check same-name sibling setting of new node + if (!def.allowsSameNameSiblings()) { + throw new ItemExistsException(); + } + // check same-name sibling setting of existing node + NodeId newId = cne.getId(); + NodeImpl n = (NodeImpl) parentImpl.sessionContext.getItemManager().getItem(newId); + if (!n.getDefinition().allowsSameNameSiblings()) { + throw new ItemExistsException(); + } + } + + return parentImpl.createChildNode(name, nodeType, nodeId); + } + + protected Property setProperty(NodeImpl parentImpl, Name name, Value value) throws RepositoryException { + return setProperty(parentImpl, name, value, false); + } + + protected Property setProperty(NodeImpl parentImpl, Name name, Value value, boolean ignorePermissions) throws RepositoryException { + if (!ignorePermissions) { + checkPermission(parentImpl, name, getPermission(false, false)); + } + // validation: make sure Node is not locked or checked-in. + parentImpl.checkSetProperty(); + InternalValue intVs = InternalValue.create(value, parentImpl.sessionContext); + return parentImpl.internalSetProperty(name, intVs); + } + + protected Property setProperty(NodeImpl parentImpl, Name name, Value[] values) throws RepositoryException { + checkPermission(parentImpl, name, getPermission(false, false)); + // validation: make sure Node is not locked or checked-in. + parentImpl.checkSetProperty(); + InternalValue[] intVs = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + intVs[i] = InternalValue.create(values[i], parentImpl.sessionContext); + } + return parentImpl.internalSetProperty(name, intVs); + } + + protected Property setProperty(NodeImpl parentImpl, Name name, Value[] values, int type) throws RepositoryException { + checkPermission(parentImpl, name, getPermission(false, false)); + // validation: make sure Node is not locked or checked-in. + parentImpl.checkSetProperty(); + InternalValue[] intVs = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + intVs[i] = InternalValue.create(values[i], parentImpl.sessionContext); + } + return parentImpl.internalSetProperty(name, intVs, type); + } + + protected void removeItem(ItemImpl itemImpl) throws RepositoryException { + NodeImpl n; + if (itemImpl.isNode()) { + n = (NodeImpl) itemImpl; + } else { + n = (NodeImpl) itemImpl.getParent(); + } + checkPermission(itemImpl, getPermission(itemImpl.isNode(), true)); + // validation: make sure Node is not locked or checked-in. + n.checkSetProperty(); + itemImpl.perform(new ItemRemoveOperation(itemImpl, false)); + } + + protected void markModified(NodeImpl parentImpl) throws RepositoryException { + parentImpl.getOrCreateTransientItemState(); + } + + protected T performProtected(SessionImpl session, SessionOperation operation) throws RepositoryException { + ItemValidator itemValidator = session.context.getItemValidator(); + return itemValidator.performRelaxed(operation, ItemValidator.CHECK_CONSTRAINTS); + } + + private void checkPermission(ItemImpl item, int perm) throws RepositoryException { + if (perm > Permission.NONE) { + SessionImpl sImpl = (SessionImpl) item.getSession(); + AccessManager acMgr = sImpl.getAccessManager(); + + Path path = item.getPrimaryPath(); + acMgr.checkPermission(path, perm); + } + } + + private void checkPermission(NodeImpl node, Name childName, int perm) throws RepositoryException { + if (perm > Permission.NONE) { + SessionImpl sImpl = (SessionImpl) node.getSession(); + AccessManager acMgr = sImpl.getAccessManager(); + + boolean isGranted = acMgr.isGranted(node.getPrimaryPath(), childName, perm); + if (!isGranted) { + throw new AccessDeniedException("Permission denied."); + } + } + } + + private int getPermission(boolean isNode, boolean isRemove) { + if (permission < Permission.NONE) { + if (isNode) { + return (isRemove) ? Permission.REMOVE_NODE : Permission.ADD_NODE; + } else { + return (isRemove) ? Permission.REMOVE_PROPERTY : Permission.SET_PROPERTY; + } + } else { + return permission; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RemoveMixinOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RemoveMixinOperation.java new file mode 100644 index 00000000000..ea45517cecf --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RemoveMixinOperation.java @@ -0,0 +1,305 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CHECKED_OUT; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CONSTRAINTS; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_REFERENCEABLE; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionWriteOperation; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.SessionItemStateManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; +import org.apache.jackrabbit.value.ValueHelper; + +/** + * Session operation for removing a mixin type from a node. + */ +class RemoveMixinOperation implements SessionWriteOperation { + + private final NodeImpl node; + + private final Name mixinName; + + public RemoveMixinOperation(NodeImpl node, Name mixinName) { + this.node = node; + this.mixinName = mixinName; + } + + public Object perform(SessionContext context) throws RepositoryException { + SessionImpl session = context.getSessionImpl(); + ItemManager itemMgr = context.getItemManager(); + SessionItemStateManager stateMgr = context.getItemStateManager(); + + context.getItemValidator().checkModify( + node, + CHECK_LOCK | CHECK_CHECKED_OUT | CHECK_CONSTRAINTS | CHECK_HOLD, + Permission.NODE_TYPE_MNGMT); + + // check if mixin is assigned + NodeState state = node.getNodeState(); + if (!state.getMixinTypeNames().contains(mixinName)) { + throw new NoSuchNodeTypeException( + "Mixin " + context.getJCRName(mixinName) + + " not included in " + node); + } + + NodeTypeManagerImpl ntMgr = context.getNodeTypeManager(); + NodeTypeRegistry ntReg = context.getNodeTypeRegistry(); + + // build effective node type of remaining mixin's & primary type + Set remainingMixins = new HashSet(state.getMixinTypeNames()); + // remove name of target mixin + remainingMixins.remove(mixinName); + EffectiveNodeType entResulting; + try { + // build effective node type representing primary type + // including remaining mixin's + entResulting = ntReg.getEffectiveNodeType( + state.getNodeTypeName(), remainingMixins); + } catch (NodeTypeConflictException e) { + throw new ConstraintViolationException(e.getMessage(), e); + } + + // mix:referenceable needs special handling because it has + // special semantics: + // it can only be removed if there no more references to this node + NodeTypeImpl mixin = ntMgr.getNodeType(mixinName); + if (isReferenceable(mixin) + && !entResulting.includesNodeType(MIX_REFERENCEABLE)) { + if (node.getReferences().hasNext()) { + throw new ConstraintViolationException( + mixinName + " can not be removed:" + + " the node is being referenced through at least" + + " one property of type REFERENCE"); + } + } + + // mix:lockable: the mixin cannot be removed if the node is + // currently locked even if the editing session is the lock holder. + if ((NameConstants.MIX_LOCKABLE.equals(mixinName) + || mixin.isDerivedFrom(NameConstants.MIX_LOCKABLE)) + && !entResulting.includesNodeType(NameConstants.MIX_LOCKABLE) + && node.isLocked()) { + throw new ConstraintViolationException( + mixinName + " can not be removed: the node is locked."); + } + + NodeState thisState = (NodeState) node.getOrCreateTransientItemState(); + + // collect information about properties and nodes which require further + // action as a result of the mixin removal; we need to do this *before* + // actually changing the assigned mixin types, otherwise we wouldn't + // be able to retrieve the current definition of an item. + Map affectedProps = + new HashMap(); + Map affectedNodes = + new HashMap(); + try { + Set names = thisState.getPropertyNames(); + for (Name propName : names) { + PropertyId propId = + new PropertyId(thisState.getNodeId(), propName); + PropertyState propState = + (PropertyState) stateMgr.getItemState(propId); + PropertyDefinition oldDef = itemMgr.getDefinition(propState); + // check if property has been defined by mixin type + // (or one of its supertypes) + NodeTypeImpl declaringNT = + (NodeTypeImpl) oldDef.getDeclaringNodeType(); + if (!entResulting.includesNodeType(declaringNT.getQName())) { + // the resulting effective node type doesn't include the + // node type that declared this property + affectedProps.put(propId, oldDef); + } + } + + List entries = thisState.getChildNodeEntries(); + for (ChildNodeEntry entry : entries) { + NodeState nodeState = + (NodeState) stateMgr.getItemState(entry.getId()); + NodeDefinition oldDef = itemMgr.getDefinition(nodeState); + // check if node has been defined by mixin type + // (or one of its supertypes) + NodeTypeImpl declaringNT = + (NodeTypeImpl) oldDef.getDeclaringNodeType(); + if (!entResulting.includesNodeType(declaringNT.getQName())) { + // the resulting effective node type doesn't include the + // node type that declared this child node + affectedNodes.put(entry, oldDef); + } + } + } catch (ItemStateException e) { + throw new RepositoryException( + "Failed to determine effect of removing mixin " + + context.getJCRName(mixinName), e); + } + + // modify the state of this node + thisState.setMixinTypeNames(remainingMixins); + // set jcr:mixinTypes property + node.setMixinTypesProperty(remainingMixins); + + // process affected nodes & properties: + // 1. try to redefine item based on the resulting + // new effective node type (see JCR-2130) + // 2. remove item if 1. fails + boolean success = false; + try { + for (Map.Entry entry : affectedProps.entrySet()) { + PropertyId id = entry.getKey(); + PropertyImpl prop = (PropertyImpl) itemMgr.getItem(id); + PropertyDefinition oldDef = entry.getValue(); + + if (oldDef.isProtected()) { + // remove 'orphaned' protected properties immediately + node.removeChildProperty(id.getName()); + continue; + } + // try to find new applicable definition first and + // redefine property if possible (JCR-2130) + try { + PropertyDefinitionImpl newDef = + node.getApplicablePropertyDefinition( + id.getName(), prop.getType(), + oldDef.isMultiple(), false); + if (newDef.getRequiredType() != PropertyType.UNDEFINED + && newDef.getRequiredType() != prop.getType()) { + // value conversion required + if (oldDef.isMultiple()) { + // convert value + Value[] values = + ValueHelper.convert( + prop.getValues(), + newDef.getRequiredType(), + session.getValueFactory()); + // redefine property + prop.onRedefine(newDef.unwrap()); + // set converted values + prop.setValue(values); + } else { + // convert value + Value value = + ValueHelper.convert( + prop.getValue(), + newDef.getRequiredType(), + session.getValueFactory()); + // redefine property + prop.onRedefine(newDef.unwrap()); + // set converted values + prop.setValue(value); + } + } else { + // redefine property + prop.onRedefine(newDef.unwrap()); + } + } catch (ValueFormatException vfe) { + // value conversion failed, remove it + node.removeChildProperty(id.getName()); + } catch (ConstraintViolationException cve) { + // no suitable definition found for this property, + // remove it + node.removeChildProperty(id.getName()); + } + } + + for (ChildNodeEntry entry : affectedNodes.keySet()) { + NodeState nodeState = (NodeState) stateMgr.getItemState(entry.getId()); + NodeImpl childNode = (NodeImpl) itemMgr.getItem(entry.getId()); + NodeDefinition oldDef = affectedNodes.get(entry); + + if (oldDef.isProtected()) { + // remove 'orphaned' protected child node immediately + node.removeChildNode(entry.getId()); + continue; + } + + // try to find new applicable definition first and + // redefine node if possible (JCR-2130) + try { + NodeDefinitionImpl newDef = + node.getApplicableChildNodeDefinition( + entry.getName(), + nodeState.getNodeTypeName()); + // redefine node + childNode.onRedefine(newDef.unwrap()); + } catch (ConstraintViolationException cve) { + // no suitable definition found for this child node, + // remove it + node.removeChildNode(entry.getId()); + } + } + success = true; + } catch (ItemStateException e) { + throw new RepositoryException( + "Failed to clean up child items defined by removed mixin " + + context.getJCRName(mixinName), e); + } finally { + if (!success) { + // TODO JCR-1914: revert any changes made so far + } + } + + return this; + } + + private boolean isReferenceable(NodeTypeImpl mixin) { + return MIX_REFERENCEABLE.equals(mixinName) + || mixin.isDerivedFrom(MIX_REFERENCEABLE); + } + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "node.removeMixin(" + mixinName + ")"; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryChecker.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryChecker.java new file mode 100644 index 00000000000..91749271f8b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryChecker.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.core; + +import static org.apache.jackrabbit.core.RepositoryImpl.SYSTEM_ROOT_NODE_ID; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_BASEVERSION; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_ISCHECKEDOUT; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_PREDECESSORS; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_ROOTVERSION; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_VERSIONHISTORY; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_VERSIONABLE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_REFERENCEABLE; + +import java.util.Calendar; +import java.util.HashSet; +import java.util.Set; +import java.util.TimeZone; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.version.InconsistentVersioningState; +import org.apache.jackrabbit.core.version.InternalVersion; +import org.apache.jackrabbit.core.version.InternalVersionHistory; +import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; +import org.apache.jackrabbit.core.version.VersionHistoryInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tool for checking for and optionally fixing consistency issues in a + * repository. Currently this class only contains a simple versioning + * recovery feature for + * JCR-2551. + */ +class RepositoryChecker { + + /** + * Logger instance. + */ + private static final Logger log = + LoggerFactory.getLogger(RepositoryChecker.class); + + private final PersistenceManager workspace; + + private final ChangeLog workspaceChanges; + + private final ChangeLog vworkspaceChanges; + + private final InternalVersionManagerImpl versionManager; + + // maximum size of changelog when running in "fixImmediately" mode + private final static long CHUNKSIZE = 256; + + // number of nodes affected by pending changes + private long dirtyNodes = 0; + + // total nodes checked, with problems + private long totalNodes = 0; + private long brokenNodes = 0; + + // start time + private long startTime; + + public RepositoryChecker(PersistenceManager workspace, + InternalVersionManagerImpl versionManager) { + this.workspace = workspace; + this.workspaceChanges = new ChangeLog(); + this.vworkspaceChanges = new ChangeLog(); + this.versionManager = versionManager; + } + + public void check(NodeId id, boolean recurse, boolean fixImmediately) + throws RepositoryException { + + log.info("Starting RepositoryChecker"); + + startTime = System.currentTimeMillis(); + + internalCheck(id, recurse, fixImmediately); + + if (fixImmediately) { + internalFix(true); + } + + log.info("RepositoryChecker finished; checked " + totalNodes + + " nodes in " + (System.currentTimeMillis() - startTime) + + "ms, problems found: " + brokenNodes); + } + + private void internalCheck(NodeId id, boolean recurse, + boolean fixImmediately) throws RepositoryException { + try { + log.debug("Checking consistency of node {}", id); + totalNodes += 1; + + NodeState state = workspace.load(id); + checkVersionHistory(state); + + if (fixImmediately && dirtyNodes > CHUNKSIZE) { + internalFix(false); + } + + if (recurse) { + for (ChildNodeEntry child : state.getChildNodeEntries()) { + if (!SYSTEM_ROOT_NODE_ID.equals(child.getId())) { + internalCheck(child.getId(), recurse, fixImmediately); + } + } + } + } catch (ItemStateException e) { + throw new RepositoryException("Unable to access node " + id, e); + } + } + + private void fix(PersistenceManager pm, ChangeLog changes, String store, + boolean verbose) throws RepositoryException { + if (changes.hasUpdates()) { + if (log.isWarnEnabled()) { + log.warn("Fixing " + store + " inconsistencies: " + + changes.toString()); + } + try { + pm.store(changes); + changes.reset(); + } catch (ItemStateException e) { + String message = "Failed to fix " + store + + " inconsistencies (aborting)"; + log.error(message, e); + throw new RepositoryException(message, e); + } + } else { + if (verbose) { + log.info("No " + store + " inconsistencies found"); + } + } + } + + public void fix() throws RepositoryException { + internalFix(true); + } + + private void internalFix(boolean verbose) throws RepositoryException { + fix(workspace, workspaceChanges, "workspace", verbose); + fix(versionManager.getPersistenceManager(), vworkspaceChanges, + "versioning workspace", verbose); + dirtyNodes = 0; + } + + private void checkVersionHistory(NodeState node) { + + String message = null; + NodeId nid = node.getNodeId(); + boolean isVersioned = node.hasPropertyName(JCR_VERSIONHISTORY); + + NodeId vhid = null; + + try { + String type = isVersioned ? "in-use" : "candidate"; + + log.debug("Checking " + type + " version history of node {}", nid); + + String intro = "Removing references to an inconsistent " + type + + " version history of node " + nid; + + message = intro + " (getting the VersionInfo)"; + VersionHistoryInfo vhi = versionManager.getVersionHistoryInfoForNode(node); + if (vhi != null) { + // get the version history's node ID as early as possible + // so we can attempt a fixup even when the next call fails + vhid = vhi.getVersionHistoryId(); + } + + message = intro + " (getting the InternalVersionHistory)"; + + InternalVersionHistory vh = null; + + try { + vh = versionManager.getVersionHistoryOfNode(nid); + } + catch (ItemNotFoundException ex) { + // it's ok if we get here if the node didn't claim to be versioned + if (isVersioned) { + throw ex; + } + } + + if (vh == null) { + if (isVersioned) { + message = intro + "getVersionHistoryOfNode returned null"; + throw new InconsistentVersioningState(message); + } + } else { + vhid = vh.getId(); + + // additional checks, see JCR-3101 + + message = intro + " (getting the version names failed)"; + Name[] versionNames = vh.getVersionNames(); + boolean seenRoot = false; + + for (Name versionName : versionNames) { + seenRoot |= JCR_ROOTVERSION.equals(versionName); + + log.debug("Checking version history of node {}, version {}", nid, versionName); + + message = intro + " (getting version " + versionName + " failed)"; + InternalVersion v = vh.getVersion(versionName); + + message = intro + "(frozen node of root version " + v.getId() + " missing)"; + if (null == v.getFrozenNode()) { + throw new InconsistentVersioningState(message); + } + } + + if (!seenRoot) { + message = intro + " (root version is missing)"; + throw new InconsistentVersioningState(message); + } + } + } catch (InconsistentVersioningState e) { + log.info(message, e); + NodeId nvhid = e.getVersionHistoryNodeId(); + if (nvhid != null) { + if (vhid != null && !nvhid.equals(vhid)) { + log.error("vhrid returned with InconsistentVersioningState does not match the id we already had: " + + vhid + " vs " + nvhid); + } + vhid = nvhid; + } + removeVersionHistoryReferences(node, vhid); + } catch (Exception e) { + log.info(message, e); + removeVersionHistoryReferences(node, vhid); + } + } + + // un-versions the node, and potentially moves the version history away + private void removeVersionHistoryReferences(NodeState node, NodeId vhid) { + + dirtyNodes += 1; + brokenNodes += 1; + + NodeState modified = + new NodeState(node, NodeState.STATUS_EXISTING_MODIFIED, true); + + Set mixins = new HashSet(node.getMixinTypeNames()); + if (mixins.remove(MIX_VERSIONABLE)) { + // we are keeping jcr:uuid, so we need to make sure the type info stays valid + mixins.add(MIX_REFERENCEABLE); + modified.setMixinTypeNames(mixins); + } + + removeProperty(modified, JCR_VERSIONHISTORY); + removeProperty(modified, JCR_BASEVERSION); + removeProperty(modified, JCR_PREDECESSORS); + removeProperty(modified, JCR_ISCHECKEDOUT); + + workspaceChanges.modified(modified); + + if (vhid != null) { + // attempt to rename the version history, so it doesn't interfere with + // a future attempt to put the node under version control again + // (see JCR-3115) + + log.info("trying to rename version history of node " + node.getId()); + + NameFactory nf = NameFactoryImpl.getInstance(); + + // Name of VHR in parent folder is ID of versionable node + Name vhrname = nf.create(Name.NS_DEFAULT_URI, node.getId().toString()); + + try { + NodeState vhrState = versionManager.getPersistenceManager().load(vhid); + NodeState vhrParentState = versionManager.getPersistenceManager().load(vhrState.getParentId()); + + if (vhrParentState.hasChildNodeEntry(vhrname)) { + NodeState modifiedParent = (NodeState) vworkspaceChanges.get(vhrState.getParentId()); + if (modifiedParent == null) { + modifiedParent = new NodeState(vhrParentState, NodeState.STATUS_EXISTING_MODIFIED, true); + } + + Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + String appendme = String.format(" (disconnected by RepositoryChecker on %04d%02d%02dT%02d%02d%02dZ)", + now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, now.get(Calendar.DAY_OF_MONTH), + now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), now.get(Calendar.SECOND)); + modifiedParent.renameChildNodeEntry(vhid, + nf.create(vhrname.getNamespaceURI(), vhrname.getLocalName() + appendme)); + + vworkspaceChanges.modified(modifiedParent); + } + else { + log.info("child node entry " + vhrname + " for version history not found inside parent folder."); + } + } catch (Exception ex) { + log.error("while trying to rename the version history", ex); + } + } + } + + private void removeProperty(NodeState node, Name name) { + if (node.hasPropertyName(name)) { + node.removePropertyName(name); + try { + workspaceChanges.deleted(workspace.load( + new PropertyId(node.getNodeId(), name))); + } catch (ItemStateException ignoe) { + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryContext.java new file mode 100644 index 00000000000..bbc43ad0170 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryContext.java @@ -0,0 +1,476 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ScheduledExecutorService; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.RepositoryImpl.WorkspaceInfo; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.security.JackrabbitSecurityManager; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.core.state.ItemStateCacheFactory; +import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; +import org.apache.jackrabbit.core.stats.StatManager; +import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; + +/** + * Internal component context of a Jackrabbit content repository. + * A repository context consists of the internal repository-level + * components and resources like the namespace and node type + * registries. Access to these resources is available only to objects + * with a reference to the context object. + */ +public class RepositoryContext { + + /** + * The repository instance to which this context is associated. + */ + private final RepositoryImpl repository; + + /** + * The namespace registry of this repository. + */ + private NamespaceRegistryImpl namespaceRegistry; + + /** + * The node type registry of this repository. + */ + private NodeTypeRegistry nodeTypeRegistry; + + /** + * The privilege registry for this repository. + */ + private PrivilegeRegistry privilegeRegistry; + + /** + * The internal version manager of this repository. + */ + private InternalVersionManagerImpl internalVersionManager; + + /** + * The root node identifier of this repository. + */ + private NodeId rootNodeId; + + /** + * The repository file system. + */ + private FileSystem fileSystem; + + /** + * The data store of this repository, or null. + */ + private DataStore dataStore; + + /** + * The cluster node instance of this repository, or null. + */ + private ClusterNode clusterNode; + + /** + * Workspace manager of this repository. + */ + private WorkspaceManager workspaceManager; + + /** + * Security manager of this repository; + */ + private JackrabbitSecurityManager securityManager; + + /** + * Item state cache factory of this repository. + */ + private ItemStateCacheFactory itemStateCacheFactory; + + private NodeIdFactory nodeIdFactory; + + /** + * Thread pool of this repository. + */ + private final ScheduledExecutorService executor = + new JackrabbitThreadPool(); + + /** + * Repository statistics collector. + */ + private final RepositoryStatisticsImpl statistics; + + /** + * The Statistics manager, handles statistics + */ + private StatManager statManager; + + /** + * flag to indicate if GC is running + */ + private volatile boolean gcRunning; + + /** + * Creates a component context for the given repository. + * + * @param repository repository instance + */ + RepositoryContext(RepositoryImpl repository) { + assert repository != null; + this.repository = repository; + this.statistics = new RepositoryStatisticsImpl(executor); + this.statManager = new StatManager(); + } + + /** + * Starts a repository with the given configuration and returns + * the internal component context of the started repository. + * + * @since Apache Jackrabbit 2.3.1 + * @param config repository configuration + * @return component context of the repository + * @throws RepositoryException if the repository could not be started + */ + public static RepositoryContext create(RepositoryConfig config) + throws RepositoryException { + RepositoryImpl repository = RepositoryImpl.create(config); + return repository.getRepositoryContext(); + } + + /** + * Starts a repository in the given directory and returns the + * internal component context of the started repository. If needed, + * the directory is created and a default repository configuration + * is installed inside it. + * + * @since Apache Jackrabbit 2.3.1 + * @see RepositoryConfig#install(File) + * @param dir repository directory + * @return component context of the repository + * @throws RepositoryException if the repository could not be started + * @throws IOException if the directory could not be initialized + */ + public static RepositoryContext install(File dir) + throws RepositoryException, IOException { + return create(RepositoryConfig.install(dir)); + } + + public RepositoryConfig getRepositoryConfig() { + return repository.getConfig(); + } + + /** + * Returns the repository instance to which this context is associated. + * + * @return repository instance + */ + public RepositoryImpl getRepository() { + return repository; + } + + /** + * Returns the thread pool of this repository. + * + * @return repository thread pool + */ + public ScheduledExecutorService getExecutor() { + return executor; + } + + /** + * Returns the namespace registry of this repository. + * + * @return namespace registry + */ + public NamespaceRegistryImpl getNamespaceRegistry() { + assert namespaceRegistry != null; + return namespaceRegistry; + } + + /** + * Sets the namespace registry of this repository. + * + * @param namespaceRegistry namespace registry + */ + void setNamespaceRegistry(NamespaceRegistryImpl namespaceRegistry) { + assert namespaceRegistry != null; + this.namespaceRegistry = namespaceRegistry; + } + + /** + * Returns the namespace registry of this repository. + * + * @return node type registry + */ + public NodeTypeRegistry getNodeTypeRegistry() { + assert nodeTypeRegistry != null; + return nodeTypeRegistry; + } + + /** + * Sets the node type registry of this repository. + * + * @param nodeTypeRegistry node type registry + */ + void setNodeTypeRegistry(NodeTypeRegistry nodeTypeRegistry) { + assert nodeTypeRegistry != null; + this.nodeTypeRegistry = nodeTypeRegistry; + } + + /** + * Returns the privilege registry of this repository. + * + * @return the privilege registry of this repository. + */ + public PrivilegeRegistry getPrivilegeRegistry() { + return privilegeRegistry; + } + + /** + * Sets the privilege registry of this repository. + * + * @param privilegeRegistry + */ + void setPrivilegeRegistry(PrivilegeRegistry privilegeRegistry) { + assert privilegeRegistry != null; + this.privilegeRegistry = privilegeRegistry; + } + + /** + * Returns the internal version manager of this repository. + * + * @return internal version manager + */ + public InternalVersionManagerImpl getInternalVersionManager() { + return internalVersionManager; + } + + /** + * Sets the internal version manager of this repository. + * + * @param internalVersionManager internal version manager + */ + void setInternalVersionManager( + InternalVersionManagerImpl internalVersionManager) { + assert internalVersionManager != null; + this.internalVersionManager = internalVersionManager; + } + + /** + * Returns the root node identifier of this repository. + * + * @return root node identifier + */ + public NodeId getRootNodeId() { + assert rootNodeId != null; + return rootNodeId; + } + + /** + * Sets the root node identifier of this repository. + * + * @param rootNodeId root node identifier + */ + void setRootNodeId(NodeId rootNodeId) { + assert rootNodeId != null; + this.rootNodeId = rootNodeId; + } + + /** + * Returns the repository file system. + * + * @return repository file system + */ + public FileSystem getFileSystem() { + assert fileSystem != null; + return fileSystem; + } + + /** + * Sets the repository file system. + * + * @param fileSystem repository file system + */ + void setFileSystem(FileSystem fileSystem) { + assert fileSystem != null; + this.fileSystem = fileSystem; + } + + /** + * Returns the data store of this repository, or null + * if a data store is not configured. + * + * @return data store, or null + */ + public DataStore getDataStore() { + return dataStore; + } + + /** + * Sets the data store of this repository. + * + * @param dataStore data store + */ + void setDataStore(DataStore dataStore) { + assert dataStore != null; + this.dataStore = dataStore; + } + + /** + * Returns the cluster node instance of this repository, or + * null if clustering is not enabled. + * + * @return cluster node + */ + public ClusterNode getClusterNode() { + return clusterNode; + } + + /** + * Sets the cluster node instance of this repository. + * + * @param clusterNode cluster node + */ + void setClusterNode(ClusterNode clusterNode) { + assert clusterNode != null; + this.clusterNode = clusterNode; + } + + /** + * Returns the workspace manager of this repository. + * + * @return workspace manager + */ + public WorkspaceManager getWorkspaceManager() { + assert workspaceManager != null; + return workspaceManager; + } + + /** + * Sets the workspace manager of this repository. + * + * @param workspaceManager workspace manager + */ + void setWorkspaceManager(WorkspaceManager workspaceManager) { + assert workspaceManager != null; + this.workspaceManager = workspaceManager; + } + + /** + * Returns the {@link WorkspaceInfo} for the named workspace. + * + * @param workspaceName The name of the workspace whose {@link WorkspaceInfo} + * is to be returned. This must not be null. + * @return The {@link WorkspaceInfo} for the named workspace. This will + * never be null. + * @throws NoSuchWorkspaceException If the named workspace does not exist. + * @throws RepositoryException If this repository has been shut down. + */ + public WorkspaceInfo getWorkspaceInfo(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + return repository.getWorkspaceInfo(workspaceName); + } + + /** + * Returns the security manager of this repository. + * + * @return security manager + */ + public JackrabbitSecurityManager getSecurityManager() { + assert securityManager != null; + return securityManager; + } + + /** + * Sets the security manager of this repository. + * + * @param securityManager security manager + */ + void setSecurityManager(JackrabbitSecurityManager securityManager) { + assert securityManager != null; + this.securityManager = securityManager; + } + + /** + * Returns the item state cache factory of this repository. + * + * @return item state cache factory + */ + public ItemStateCacheFactory getItemStateCacheFactory() { + assert itemStateCacheFactory != null; + return itemStateCacheFactory; + } + + /** + * Sets the item state cache factory of this repository. + * + * @param itemStateCacheFactory item state cache factory + */ + void setItemStateCacheFactory(ItemStateCacheFactory itemStateCacheFactory) { + assert itemStateCacheFactory != null; + this.itemStateCacheFactory = itemStateCacheFactory; + } + + public void setNodeIdFactory(NodeIdFactory nodeIdFactory) { + this.nodeIdFactory = nodeIdFactory; + } + + public NodeIdFactory getNodeIdFactory() { + return nodeIdFactory; + } + + /** + * Returns the repository statistics collector. + * + * @return repository statistics collector + */ + public RepositoryStatisticsImpl getRepositoryStatistics() { + return statistics; + } + + /** + * @return the statistics manager object + */ + public StatManager getStatManager() { + return statManager; + } + + /** + * + * @return gcRunning status + */ + public synchronized boolean isGcRunning() { + return gcRunning; + } + + /** + * set gcRunnign status + * @param gcRunning + */ + public synchronized void setGcRunning(boolean gcRunning) { + this.gcRunning = gcRunning; + } + + + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryCopier.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryCopier.java new file mode 100644 index 00000000000..db0eb68fc38 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryCopier.java @@ -0,0 +1,287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.lock.LockManagerImpl; +import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.persistence.PersistenceCopier; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tool for backing up or migrating the entire contents (workspaces, + * version histories, namespaces, node types, etc.) of a repository to + * a new repository. The target repository (if it exists) is overwritten. + *

    + * No cluster journal records are written in the target repository. If the + * target repository is clustered, it should be the only node in the cluster. + *

    + * The target repository needs to be fully reindexed after the copy operation. + * The static copy() methods will remove the target search index folders from + * their default locations to trigger automatic reindexing when the repository + * is next started. + * + * @since Apache Jackrabbit 1.6 + */ +public class RepositoryCopier { + + /** + * Logger instance + */ + private static final Logger logger = + LoggerFactory.getLogger(RepositoryCopier.class); + + /** + * Source repository context. + */ + private final RepositoryContext source; + + /** + * Target repository context. + */ + private final RepositoryContext target; + + /** + * Copies the contents of the repository in the given source directory + * to a repository in the given target directory. + * + * @param source source repository directory + * @param target target repository directory + * @throws RepositoryException if the copy operation fails + * @throws IOException if the target repository can not be initialized + */ + public static void copy(File source, File target) + throws RepositoryException, IOException { + copy(RepositoryConfig.create(source), RepositoryConfig.install(target)); + } + + /** + * Copies the contents of the repository with the given configuration + * to a repository in the given target directory. + * + * @param source source repository configuration + * @param target target repository directory + * @throws RepositoryException if the copy operation fails + * @throws IOException if the target repository can not be initialized + */ + public static void copy(RepositoryConfig source, File target) + throws RepositoryException, IOException { + copy(source, RepositoryConfig.install(target)); + } + + /** + * Copies the contents of the source repository with the given + * configuration to a target repository with the given configuration. + * + * @param source source repository configuration + * @param target target repository directory + * @throws RepositoryException if the copy operation fails + */ + public static void copy(RepositoryConfig source, RepositoryConfig target) + throws RepositoryException { + RepositoryImpl repository = RepositoryImpl.create(source); + try { + copy(repository, target); + } finally { + repository.shutdown(); + } + } + + /** + * Copies the contents of the given source repository to a repository in + * the given target directory. + *

    + * The source repository must not be modified while + * the copy operation is running to avoid an inconsistent copy. + * + * @param source source repository directory + * @param target target repository directory + * @throws RepositoryException if the copy operation fails + * @throws IOException if the target repository can not be initialized + */ + public static void copy(RepositoryImpl source, File target) + throws RepositoryException, IOException { + copy(source, RepositoryConfig.install(target)); + } + + /** + * Copies the contents of the given source repository to a target + * repository with the given configuration. + *

    + * The source repository must not be modified while + * the copy operation is running to avoid an inconsistent copy. + * + * @param source source repository directory + * @param target target repository directory + * @throws RepositoryException if the copy operation fails + */ + public static void copy(RepositoryImpl source, RepositoryConfig target) + throws RepositoryException { + RepositoryImpl repository = RepositoryImpl.create(target); + try { + new RepositoryCopier(source, repository).copy(); + } finally { + repository.shutdown(); + } + + // Remove index directories to force re-indexing on next startup + // TODO: There should be a cleaner way to do this + File targetDir = new File(target.getHomeDir()); + File repoDir = new File(targetDir, "repository"); + FileUtils.deleteQuietly(new File(repoDir, "index")); + File[] workspaces = new File(targetDir, "workspaces").listFiles(); + if (workspaces != null) { + for (File workspace : workspaces) { + FileUtils.deleteQuietly(new File(workspace, "index")); + } + } + } + + /** + * Creates a tool for copying the full contents of the source repository + * to the given target repository. Any existing content in the target + * repository will be overwritten. + * + * @param source source repository + * @param target target repository + */ + public RepositoryCopier(RepositoryImpl source, RepositoryImpl target) { + // TODO: It would be better if we were given the RepositoryContext + // instances directly. Perhaps we should use something like + // RepositoryImpl.getRepositoryCopier(RepositoryImpl target) + // instead of this public constructor to achieve that. + this.source = source.getRepositoryContext(); + this.target = target.getRepositoryContext(); + } + + /** + * Copies the full content from the source to the target repository. + *

    + * The source repository must not be modified while + * the copy operation is running to avoid an inconsistent copy. + *

    + * This method leaves the search indexes of the target repository in + * an + * Note that both the source and the target repository must be closed + * during the copy operation as this method requires exclusive access + * to the repositories. + * + * @throws RepositoryException if the copy operation fails + */ + public void copy() throws RepositoryException { + logger.info( + "Copying repository content from {} to {}", + source.getRepository().repConfig.getHomeDir(), + target.getRepository().repConfig.getHomeDir()); + try { + copyNamespaces(); + copyNodeTypes(); + copyVersionStore(); + copyWorkspaces(); + } catch (Exception e) { + throw new RepositoryException("Failed to copy content", e); + } + } + + private void copyNamespaces() throws RepositoryException { + NamespaceRegistry sourceRegistry = source.getNamespaceRegistry(); + NamespaceRegistry targetRegistry = target.getNamespaceRegistry(); + + logger.info("Copying registered namespaces"); + Collection existing = Arrays.asList(targetRegistry.getURIs()); + for (String uri : sourceRegistry.getURIs()) { + if (!existing.contains(uri)) { + // TODO: what if the prefix is already taken? + targetRegistry.registerNamespace( + sourceRegistry.getPrefix(uri), uri); + } + } + } + + private void copyNodeTypes() throws RepositoryException { + NodeTypeRegistry sourceRegistry = source.getNodeTypeRegistry(); + NodeTypeRegistry targetRegistry = target.getNodeTypeRegistry(); + + logger.info("Copying registered node types"); + Collection existing = + Arrays.asList(targetRegistry.getRegisteredNodeTypes()); + Collection register = new ArrayList(); + for (Name name : sourceRegistry.getRegisteredNodeTypes()) { + // TODO: what about modified node types? + if (!existing.contains(name)) { + register.add(sourceRegistry.getNodeTypeDef(name)); + } + } + try { + targetRegistry.registerNodeTypes(register); + } catch (InvalidNodeTypeDefException e) { + throw new RepositoryException("Unable to copy node types", e); + } + } + + private void copyVersionStore() throws RepositoryException { + logger.info("Copying version histories"); + PersistenceCopier copier = new PersistenceCopier( + source.getInternalVersionManager().getPersistenceManager(), + target.getInternalVersionManager().getPersistenceManager(), + target.getDataStore()); + copier.copy(RepositoryImpl.VERSION_STORAGE_NODE_ID); + copier.copy(RepositoryImpl.ACTIVITIES_NODE_ID); + } + + private void copyWorkspaces() throws RepositoryException { + Collection existing = + Arrays.asList(target.getRepository().getWorkspaceNames()); + for (String name : source.getRepository().getWorkspaceNames()) { + logger.info("Copying workspace {}" , name); + + if (!existing.contains(name)) { + target.getRepository().createWorkspace(name); + } + + // Copy all the workspace content + PersistenceCopier copier = new PersistenceCopier( + source.getRepository().getWorkspaceInfo(name).getPersistenceManager(), + target.getRepository().getWorkspaceInfo(name).getPersistenceManager(), + target.getDataStore()); + copier.excludeNode(RepositoryImpl.SYSTEM_ROOT_NODE_ID); + copier.copy(RepositoryImpl.ROOT_NODE_ID); + + // Copy all the active open-scoped locks + LockManagerImpl sourceLockManager = + source.getRepository().getLockManager(name); + LockManagerImpl targetLockManager = + target.getRepository().getLockManager(name); + targetLockManager.copyOpenScopedLocksFrom(sourceLockManager); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryFactoryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryFactoryImpl.java new file mode 100644 index 00000000000..3d75e7c2ec6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryFactoryImpl.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.core; + +import static org.apache.jackrabbit.core.config.RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.api.JackrabbitRepositoryFactory; +import org.apache.jackrabbit.api.management.RepositoryManager; +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * RepositoryFactoryImpl implements a repository factory that + * creates a {@link TransientRepository} on {@link #getRepository(Map)}. + */ +public class RepositoryFactoryImpl implements JackrabbitRepositoryFactory { + + /** + * Name of the repository home parameter. + */ + public static final String REPOSITORY_HOME + = "org.apache.jackrabbit.repository.home"; + + /** + * Name of the repository configuration parameter. + */ + public static final String REPOSITORY_CONF + = "org.apache.jackrabbit.repository.conf"; + + /** + * Map of repository instances. + * Key = repository parameters, value = repository instance. + */ + private static final Map REPOSITORIES = + new HashMap(); + + /** + * The repository instances that were created by this factory. + */ + private final Set ownRepositories = + new HashSet(); + + public Repository getRepository(Map parameters) throws RepositoryException { + if (parameters == null) { + return getRepository(null, Collections.emptyMap()); + } else if (parameters.containsKey(REPOSITORY_HOME)) { + String home = parameters.get(REPOSITORY_HOME).toString(); + return getRepository(home, parameters); + } else if (parameters.containsKey(JcrUtils.REPOSITORY_URI)) { + Object parameter = parameters.get(JcrUtils.REPOSITORY_URI); + try { + URI uri = new URI(parameter.toString().trim()); + String scheme = uri.getScheme(); + if (("file".equalsIgnoreCase(scheme) + || "jcr-jackrabbit".equalsIgnoreCase(scheme)) + && uri.getAuthority() == null) { + File file = new File(uri.getPath()); + if (file.isFile()) { + return null; // Not a (possibly missing) directory + } else { + return getRepository(file.getPath(), parameters); + } + } else { + return null; // not a file: or jcr-jackrabbit: URI + } + } catch (URISyntaxException e) { + return null; // not a valid URI + } + } else { + return null; // unknown or insufficient parameters + } + } + + private Repository getRepository(String home, Map parameters) + throws RepositoryException { + TransientRepository repository = + getOrCreateRepository(home, parameters); + ownRepositories.add(repository); + return repository; + } + + /** + * Either returns a cached repository or creates a repository instance and + * puts it into the {@link #REPOSITORIES} cache. + * + * @param home path to the repository home. + * @return the repository instance. + * @throws RepositoryException if an error occurs while creating the + * repository instance. + */ + private static synchronized TransientRepository getOrCreateRepository( + String home, Map parameters) throws RepositoryException { + // Prepare the repository properties + Properties properties = new Properties(System.getProperties()); + for (Map.Entry entry : parameters.entrySet()) { + Object key = entry.getKey(); + if (key != null) { + Object value = entry.getValue(); + if (value != null) { + properties.setProperty( + key.toString(), value.toString()); + } else { + properties.remove(key.toString()); + } + } + } + if (home != null) { + properties.put(REPOSITORY_HOME_VARIABLE, home); + } + + TransientRepository repository = REPOSITORIES.get(properties); + if (repository == null) { + try { + TransientRepository tr; + if (home == null) { + tr = new TransientRepository(properties); + // also remember this instance as the default repository + REPOSITORIES.put(null, tr); + } else { + tr = new TransientRepository(properties); + } + REPOSITORIES.put(properties, tr); + repository = tr; + } catch (IOException e) { + throw new RepositoryException( + "Failed to install repository configuration", e); + } + } + return repository; + } + + public RepositoryManager getRepositoryManager(JackrabbitRepository repo) throws RepositoryException { + if (!(repo instanceof TransientRepository)) { + throw new RepositoryException("The repository was not created in this factory"); + } + if (!ownRepositories.contains(repo)) { + throw new RepositoryException("The repository was not created in this factory"); + } + return new RepositoryManagerImpl((TransientRepository) repo); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryImpl.java new file mode 100644 index 00000000000..7eef0774aec --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryImpl.java @@ -0,0 +1,2481 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.observation.Event; +import javax.jcr.observation.ObservationManager; +import javax.security.auth.Subject; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.api.management.RepositoryManager; +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.commons.AbstractRepository; +import org.apache.jackrabbit.core.cache.CacheManager; +import org.apache.jackrabbit.core.cluster.ClusterContext; +import org.apache.jackrabbit.core.cluster.ClusterException; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.cluster.LockEventChannel; +import org.apache.jackrabbit.core.cluster.UpdateEventChannel; +import org.apache.jackrabbit.core.cluster.UpdateEventListener; +import org.apache.jackrabbit.core.cluster.WorkspaceEventChannel; +import org.apache.jackrabbit.core.cluster.WorkspaceListener; +import org.apache.jackrabbit.core.config.ClusterConfig; +import org.apache.jackrabbit.core.config.PersistenceManagerConfig; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.config.SecurityManagerConfig; +import org.apache.jackrabbit.core.config.VersioningConfig; +import org.apache.jackrabbit.core.config.WorkspaceConfig; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.gc.GarbageCollector; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.lock.LockManager; +import org.apache.jackrabbit.core.lock.LockManagerImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.nodetype.virtual.VirtualNodeTypeStateManager; +import org.apache.jackrabbit.core.observation.DelegatingObservationDispatcher; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.observation.EventStateCollection; +import org.apache.jackrabbit.core.observation.ObservationDispatcher; +import org.apache.jackrabbit.core.persistence.IterablePersistenceManager; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.persistence.check.ConsistencyChecker; +import org.apache.jackrabbit.core.retention.RetentionRegistry; +import org.apache.jackrabbit.core.retention.RetentionRegistryImpl; +import org.apache.jackrabbit.core.security.JackrabbitSecurityManager; +import org.apache.jackrabbit.core.security.authentication.AuthContext; +import org.apache.jackrabbit.core.security.authentication.token.TokenBasedAuthentication; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.core.security.simple.SimpleSecurityManager; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ISMLocking; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ManagedMLRUItemStateCacheFactory; +import org.apache.jackrabbit.core.state.SharedItemStateManager; +import org.apache.jackrabbit.core.util.RepositoryLockMechanism; +import org.apache.jackrabbit.core.version.InternalVersionManager; +import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; +import org.apache.jackrabbit.core.xml.ClonedInputSource; +import org.apache.jackrabbit.data.core.TransactionException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.RegistryNamespaceResolver; +import org.apache.jackrabbit.value.ValueFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.InputSource; + +import EDU.oswego.cs.dl.util.concurrent.Mutex; +import EDU.oswego.cs.dl.util.concurrent.ReadWriteLock; +import EDU.oswego.cs.dl.util.concurrent.ReentrantWriterPreferenceReadWriteLock; +import EDU.oswego.cs.dl.util.concurrent.WriterPreferenceReadWriteLock; + +/** + * A RepositoryImpl ... + */ +public class RepositoryImpl extends AbstractRepository + implements javax.jcr.Repository, JackrabbitRepository, SessionListener, WorkspaceListener { + + private static Logger log = LoggerFactory.getLogger(RepositoryImpl.class); + + /** + * hardcoded id of the repository root node + */ + public static final NodeId ROOT_NODE_ID = NodeId.valueOf("cafebabe-cafe-babe-cafe-babecafebabe"); + + /** + * hardcoded id of the "/jcr:system" node + */ + public static final NodeId SYSTEM_ROOT_NODE_ID = NodeId.valueOf("deadbeef-cafe-babe-cafe-babecafebabe"); + + /** + * hardcoded id of the "/jcr:system/jcr:versionStorage" node + */ + public static final NodeId VERSION_STORAGE_NODE_ID = NodeId.valueOf("deadbeef-face-babe-cafe-babecafebabe"); + + /** + * hardcoded id of the "/jcr:system/jcr:activities" node + */ + public static final NodeId ACTIVITIES_NODE_ID = NodeId.valueOf("deadbeef-face-babe-ac71-babecafebabe"); + + /** + * hardcoded id of the "/jcr:system/jcr:configurations" node + */ + public static final NodeId CONFIGURATIONS_NODE_ID = NodeId.valueOf("deadbeef-face-babe-c04f-babecafebabe"); + + /** + * hardcoded id of the "/jcr:system/jcr:nodeTypes" node + */ + public static final NodeId NODETYPES_NODE_ID = NodeId.valueOf("deadbeef-cafe-cafe-cafe-babecafebabe"); + + /** + * the name of the resource containing customized descriptors of the repository. + */ + private static final String PROPERTIES_RESOURCE = "repository.properties"; + + /** + * Key to a string descriptor. Returns the repository cluster id if + * and only if clustering is enabled. + */ + public static final String JACKRABBIT_CLUSTER_ID = "jackrabbit.cluster.id"; + + /** + * the repository descriptors, maps String keys to Value/Value[] objects + */ + private final Map repDescriptors = new HashMap(); + + protected final RepositoryContext context = new RepositoryContext(this); + + private final VirtualNodeTypeStateManager virtNTMgr; + + /** + * Security manager + */ + private JackrabbitSecurityManager securityMgr; + + /** + * Search manager for the jcr:system tree. May be null if + * none is configured. + */ + private SearchManager systemSearchMgr; + + // configuration of the repository + protected final RepositoryConfig repConfig; + + protected NodeIdFactory nodeIdFactory; + + /** + * the delegating observation dispatcher for all workspaces + */ + private final DelegatingObservationDispatcher delegatingDispatcher = + new DelegatingObservationDispatcher(); + + /** + * map of workspace names and WorkspaceInfos. + */ + private final HashMap wspInfos = new HashMap(); + + /** + * active sessions (weak references) + */ + private final Map activeSessions = + new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK); + + // flag indicating if repository has been shut down + private boolean disposed; + + /** + * The repository lock mechanism ensures that a repository is only instantiated once. + */ + private RepositoryLockMechanism repLock; + + /** + * Shutdown lock for guaranteeing that no new sessions are started during + * repository shutdown and that a repository shutdown is not initiated + * during a login. Each session login acquires a read lock while the + * repository shutdown requires a write lock. This guarantees that there + * can be multiple concurrent logins when the repository is not shutting + * down, but that only a single shutdown and no concurrent logins can + * happen simultaneously. + */ + private final ReadWriteLock shutdownLock = new WriterPreferenceReadWriteLock(); + + /** + * There is one cache manager per repository that manages the sizes of the caches used. + */ + private final CacheManager cacheMgr = new CacheManager(); + + /** + * Chanel for posting create workspace messages. + */ + private WorkspaceEventChannel createWorkspaceEventChannel; + + /** + * Protected constructor. + * + * @param repConfig the repository configuration. + * @throws RepositoryException if there is already another repository + * instance running on the given configuration + * or another error occurs. + */ + protected RepositoryImpl(RepositoryConfig repConfig) throws RepositoryException { + // Acquire a lock on the repository home + repLock = repConfig.getRepositoryLockMechanism(); + repLock.init(repConfig.getHomeDir()); + repLock.acquire(); + + long t0 = System.currentTimeMillis(); + log.info("Starting repository..."); + + boolean succeeded = false; + try { + this.repConfig = repConfig; + + context.setFileSystem(repConfig.getFileSystem()); + + // Load root node identifier + context.setRootNodeId(loadRootNodeId()); + + // initialize repository descriptors + initRepositoryDescriptors(); + + // create registries + context.setNamespaceRegistry(createNamespaceRegistry()); + context.setNodeTypeRegistry(createNodeTypeRegistry()); + context.setPrivilegeRegistry(new PrivilegeRegistry(context.getNamespaceRegistry(), context.getFileSystem())); + + // Create item state cache manager + context.setItemStateCacheFactory( + new ManagedMLRUItemStateCacheFactory(cacheMgr)); + + DataStore dataStore = repConfig.getDataStore(); + if (dataStore != null) { + context.setDataStore(dataStore); + } + + nodeIdFactory = new NodeIdFactory(repConfig.getHomeDir()); + nodeIdFactory.open(); + context.setNodeIdFactory(nodeIdFactory); + + context.setWorkspaceManager(new WorkspaceManager(this)); + + // init workspace configs + for (WorkspaceConfig config : repConfig.getWorkspaceConfigs()) { + WorkspaceInfo info = createWorkspaceInfo(config); + wspInfos.put(config.getName(), info); + } + + // initialize optional clustering before setting up any other + // external event source that a cluster node will be interested in + ClusterNode clusterNode = null; + if (repConfig.getClusterConfig() != null) { + clusterNode = createClusterNode(); + context.setClusterNode(clusterNode); + context.getNamespaceRegistry().setEventChannel(clusterNode); + context.getNodeTypeRegistry().setEventChannel(clusterNode); + context.getPrivilegeRegistry().setEventChannel(clusterNode); + + createWorkspaceEventChannel = clusterNode; + clusterNode.setListener(this); + } + + // init version manager + InternalVersionManagerImpl vMgr = createVersionManager( + repConfig.getVersioningConfig(), delegatingDispatcher); + context.setInternalVersionManager(vMgr); + if (clusterNode != null) { + vMgr.setEventChannel(clusterNode.createUpdateChannel(null)); + } + + // init virtual node type manager + virtNTMgr = new VirtualNodeTypeStateManager( + context.getNodeTypeRegistry(), + delegatingDispatcher, NODETYPES_NODE_ID, SYSTEM_ROOT_NODE_ID); + + // initialize startup workspaces + initStartupWorkspaces(); + + // initialize system search manager + getSystemSearchManager(repConfig.getDefaultWorkspaceName()); + + // Initialise the security manager; + initSecurityManager(); + + // after the workspace is initialized we pass a system session to + // the virtual node type manager + + // todo FIXME the *global* virtual node type manager is using a session that is bound to a single specific workspace... + virtNTMgr.setSession(getSystemSession(repConfig.getDefaultWorkspaceName())); + + // now start cluster node as last step + if (clusterNode != null) { + setDescriptor(JACKRABBIT_CLUSTER_ID, repConfig.getClusterConfig().getId()); + try { + clusterNode.start(); + } catch (ClusterException e) { + String msg = "Unable to start clustered node, forcing shutdown..."; + log.error(msg, e); + shutdown(); + throw new RepositoryException(msg, e); + } + } + + // amount of time in seconds before an idle workspace is automatically + // shut down + int maxIdleTime = repConfig.getWorkspaceMaxIdleTime(); + if (maxIdleTime != 0) { + // start workspace janitor thread + Thread wspJanitor = new Thread(new WorkspaceJanitor(maxIdleTime * 1000)); + wspJanitor.setName("WorkspaceJanitor"); + wspJanitor.setPriority(Thread.MIN_PRIORITY); + wspJanitor.setDaemon(true); + wspJanitor.start(); + } + + succeeded = true; + log.info("Repository started (" + (System.currentTimeMillis() - t0) + "ms)"); + } catch (RepositoryException e) { + log.error("failed to start Repository: " + e.getMessage(), e); + throw e; + } finally { + if (!succeeded) { + try { + // repository startup failed, clean up... + shutdown(); + } catch (Throwable t) { + // ensure this exception does not overlay the original + // startup exception and only log it + log.error("In addition to startup fail, another unexpected problem " + + "occurred while shutting down the repository again.", t); + // Clear the repository lock if it was left in place + repLock.release(); + } + } + } + } + + /** + * Protected factory method for creating the namespace registry. + * Called by the constructor after the repository file system has + * been initialised. + * + * @return namespace registry + * @throws RepositoryException if the namespace registry can not be created + */ + protected NamespaceRegistryImpl createNamespaceRegistry() + throws RepositoryException { + return new NamespaceRegistryImpl(context.getFileSystem()); + } + + /** + * Protected factory method for creating the node type registry. + * Called by the constructor after the repository file system and + * namespace registry have been initialised. + * + * @return node type registry + * @throws RepositoryException if the node type registry can not be created + */ + protected NodeTypeRegistry createNodeTypeRegistry() + throws RepositoryException { + return new NodeTypeRegistry( + context.getNamespaceRegistry(), context.getFileSystem()); + } + + /** + * Returns the internal component context of this repository. + * This package-private method should only be used when there is + * no other reasonable way to access the repository context. + * A better design would be to access the repository instance + * through the repository context. + * + * @return repository context + */ + RepositoryContext getRepositoryContext() { // TODO: Get rid of this method + return context; + } + + /** + * Get the cache manager of this repository, useful + * for setting its memory parameters. + * + * @return the cache manager + * @since 1.3 + */ + public CacheManager getCacheManager() { + return cacheMgr; + } + + /** + * Creates the {@link org.apache.jackrabbit.core.security.JackrabbitSecurityManager SecurityManager} + * of this Repository and adds it to the repository context. + * + * @throws RepositoryException if an error occurs. + */ + private synchronized void initSecurityManager() throws RepositoryException { + SecurityManagerConfig smc = + getConfig().getSecurityConfig().getSecurityManagerConfig(); + if (smc == null) { + log.debug("No configuration entry for SecurityManager. Using org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"); + securityMgr = new SimpleSecurityManager(); + } else { + securityMgr = smc.newInstance(JackrabbitSecurityManager.class); + } + + log.info("SecurityManager = " + securityMgr.getClass()); + + context.setSecurityManager(securityMgr); + + String workspaceName = getConfig().getDefaultWorkspaceName(); + if (smc != null && smc.getWorkspaceName() != null) { + workspaceName = smc.getWorkspaceName(); + } + + // mark the workspace as 'active' for that it does not get disposed + // by the workspace-janitor + // TODO: There should be a cleaner way to do this + markWorkspaceActive(workspaceName); + + // FIXME: Note that this call must be done *after* the security + // manager has been added to the repository context, since the + // initialisation code may invoke code that depends on the presence + // of a security manager. It would be better if this was not the case. + SystemSession systemSession = getSystemSession(workspaceName); + securityMgr.init(this, systemSession); + + // initial security specific repository descriptors that are defined + // by JackrabbitRepository + ValueFactory vf = ValueFactoryImpl.getInstance(); + boolean hasUserMgt; + try { + securityMgr.getUserManager(systemSession); + hasUserMgt = true; + } catch (RepositoryException e) { + hasUserMgt = false; + } + setDescriptor(JackrabbitRepository.OPTION_USER_MANAGEMENT_SUPPORTED, vf.createValue(hasUserMgt)); + + boolean hasPrincipalMgt; + try { + securityMgr.getPrincipalManager(systemSession); + hasPrincipalMgt = true; + } catch (RepositoryException e) { + hasPrincipalMgt = false; + } + setDescriptor(JackrabbitRepository.OPTION_PRINCIPAL_MANAGEMENT_SUPPORTED, vf.createValue(hasPrincipalMgt)); + setDescriptor(JackrabbitRepository.OPTION_PRIVILEGE_MANAGEMENT_SUPPORTED, vf.createValue(true)); + + } + + /** + * Creates the version manager. + * + * @param vConfig the versioning config + * @return the newly created version manager + * @throws RepositoryException if an error occurs + */ + protected InternalVersionManagerImpl createVersionManager(VersioningConfig vConfig, + DelegatingObservationDispatcher delegatingDispatcher) + throws RepositoryException { + + + FileSystem fs = vConfig.getFileSystem(); + PersistenceManager pm = createPersistenceManager( + vConfig.getHomeDir(), fs, + vConfig.getPersistenceManagerConfig()); + + ISMLocking ismLocking = vConfig.getISMLocking(); + + return new InternalVersionManagerImpl( + pm, fs, context.getNodeTypeRegistry(), delegatingDispatcher, + SYSTEM_ROOT_NODE_ID, + VERSION_STORAGE_NODE_ID, + ACTIVITIES_NODE_ID, + context.getItemStateCacheFactory(), + ismLocking, + context.getNodeIdFactory()); + } + + /** + * Initialize startup workspaces. Base implementation will initialize the + * default workspace. Derived classes may initialize their own startup + * workspaces after having called the base implementation. + * + * @throws RepositoryException if an error occurs + */ + protected void initStartupWorkspaces() throws RepositoryException { + String wspName = repConfig.getDefaultWorkspaceName(); + String secWspName = null; + SecurityManagerConfig smc = repConfig.getSecurityConfig().getSecurityManagerConfig(); + if (smc != null) { + secWspName = smc.getWorkspaceName(); + } + try { + (wspInfos.get(wspName)).initialize(); + if (secWspName != null && !wspInfos.containsKey(secWspName)) { + createWorkspace(secWspName); + log.info("created system workspace: {}", secWspName); + } + } catch (RepositoryException e) { + // if default workspace failed to initialize, shutdown again + log.error("Failed to initialize workspace '" + wspName + "'", e); + log.error("Unable to start repository, forcing shutdown..."); + shutdown(); + throw e; + } + } + + /** + * Returns the root node identifier. The identifier is loaded from + * the meta/rootUUID file within the repository file system. + * If such a file does not yet exist, the hardcoded default root node + * identifier ({@link #ROOT_NODE_ID}) is used and written to that file. + *

    + * This utility method should only be used by the constructor after the + * repository file system has been initialised. + * + * @return root node identifier + * @throws RepositoryException if the identifier can not be loaded or saved + */ + private NodeId loadRootNodeId() throws RepositoryException { + try { + FileSystemResource uuidFile = new FileSystemResource( + context.getFileSystem(), "/meta/rootUUID"); + if (uuidFile.exists()) { + // Load uuid of the repository's root node. It is stored in + // text format (36 characters) for better readability. + InputStream in = uuidFile.getInputStream(); + try { + return NodeId.valueOf(IOUtils.toString(in, "US-ASCII")); + } finally { + IOUtils.closeQuietly(in); + } + } else { + // Use hard-coded uuid for root node rather than generating + // a different uuid per repository instance; using a + // hard-coded uuid makes it easier to copy/move entire + // workspaces from one repository instance to another. + uuidFile.makeParentDirs(); + OutputStream out = uuidFile.getOutputStream(); + try { + out.write(ROOT_NODE_ID.toString().getBytes(StandardCharsets.US_ASCII)); + return ROOT_NODE_ID; + } finally { + IOUtils.closeQuietly(out); + } + } + } catch (IOException e) { + throw new RepositoryException( + "Failed to load or persist the root node identifier", e); + } catch (FileSystemException fse) { + throw new RepositoryException( + "Failed to access the root node identifier", fse); + } + } + + /** + * Creates a new RepositoryImpl instance. + *

    + * + * @param config the configuration of the repository + * @return a new RepositoryImpl instance + * @throws RepositoryException If an error occurs + */ + public static RepositoryImpl create(RepositoryConfig config) + throws RepositoryException { + return new RepositoryImpl(config); + } + + /** + * Performs a sanity check on this repository instance. + * + * @throws RepositoryException if this repository has been rendered + * invalid for some reason (e.g. if it has been shut down) + */ + protected void sanityCheck() throws RepositoryException { + // check repository status + if (disposed) { + throw new RepositoryException( + "This repository instance has been shut down."); + } + } + + /** + * Returns the system search manager or null if none is + * configured. + */ + protected SearchManager getSystemSearchManager(String wspName) + throws RepositoryException { + if (systemSearchMgr == null) { + if (repConfig.isSearchEnabled()) { + systemSearchMgr = new SearchManager( + null, context, + repConfig, + getWorkspaceInfo(wspName).itemStateMgr, + context.getInternalVersionManager().getPersistenceManager(), + SYSTEM_ROOT_NODE_ID, null, null); + + SystemSession defSysSession = getSystemSession(wspName); + ObservationManager obsMgr = defSysSession.getWorkspace().getObservationManager(); + obsMgr.addEventListener(systemSearchMgr, Event.NODE_ADDED + | Event.NODE_REMOVED | Event.PROPERTY_ADDED + | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED, + "/" + defSysSession.getJCRName(NameConstants.JCR_SYSTEM), + true, null, null, false); + } + } + return systemSearchMgr; + } + + /** + * Creates the cluster node. + * + * @return clustered node + */ + protected ClusterNode createClusterNode() throws RepositoryException { + try { + ClusterNode clusterNode = new ClusterNode(); + clusterNode.init(new ExternalEventListener()); + return clusterNode; + } catch (Exception e) { + throw new RepositoryException(e); + } + } + + /** + * Returns the names of all workspaces in this repository. + * + * @return the names of all workspaces in this repository. + * @see javax.jcr.Workspace#getAccessibleWorkspaceNames() + */ + protected String[] getWorkspaceNames() { + synchronized (wspInfos) { + return wspInfos.keySet().toArray(new String[wspInfos.keySet().size()]); + } + } + + /** + * Returns the {@link WorkspaceInfo} for the named workspace. + * + * @param workspaceName The name of the workspace whose {@link WorkspaceInfo} + * is to be returned. This must not be null. + * @return The {@link WorkspaceInfo} for the named workspace. This will + * never be null. + * @throws NoSuchWorkspaceException If the named workspace does not exist. + * @throws RepositoryException If this repository has been shut down. + */ + protected WorkspaceInfo getWorkspaceInfo(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + // check sanity of this instance + sanityCheck(); + + WorkspaceInfo wspInfo; + synchronized (wspInfos) { + wspInfo = wspInfos.get(workspaceName); + if (wspInfo == null) { + throw new NoSuchWorkspaceException(workspaceName); + } + } + + try { + wspInfo.initialize(); + } catch (RepositoryException e) { + log.error("Unable to initialize workspace '" + workspaceName + "'", e); + throw new NoSuchWorkspaceException(workspaceName); + } + return wspInfo; + } + + /** + * Creates a workspace with the given name. + * + * @param workspaceName name of the new workspace + * @throws RepositoryException if a workspace with the given name + * already exists or if another error occurs + * @see WorkspaceImpl#createWorkspace(String) + */ + protected void createWorkspace(String workspaceName) + throws RepositoryException { + synchronized (wspInfos) { + if (wspInfos.containsKey(workspaceName)) { + throw new RepositoryException("workspace '" + + workspaceName + "' already exists."); + } + + // needed to get newly created workspace config file content when + // running in clustered environment + StringBuffer workspaceConfigContent = null; + if (context.getClusterNode() != null) { + workspaceConfigContent = new StringBuffer(); + } + + // create the workspace configuration + WorkspaceConfig config = repConfig.createWorkspaceConfig(workspaceName, workspaceConfigContent); + WorkspaceInfo info = createWorkspaceInfo(config); + wspInfos.put(workspaceName, info); + + if (workspaceConfigContent != null && createWorkspaceEventChannel != null) { + // notify other cluster node that workspace has been created + InputSource s = new InputSource(new StringReader(workspaceConfigContent.toString())); + createWorkspaceEventChannel.workspaceCreated(workspaceName, new ClonedInputSource(s)); + } + } + } + + public void externalWorkspaceCreated(String workspaceName, + InputSource configTemplate) throws RepositoryException { + + createWorkspaceInternal(workspaceName, configTemplate); + } + + /** + * Creates a workspace with the given name and given workspace configuration + * template. + * + * The difference between this method and {@link #createWorkspace(String, InputSource)} + * is that the later notifies the other cluster node that workspace has been created + * whereas this method only creates the workspace. + * + * @param workspaceName name of the new workspace + * @param configTemplate the workspace configuration template of the new + * workspace + * @throws RepositoryException if a workspace with the given name already + * exists or if another error occurs + * @see WorkspaceImpl#createWorkspace(String,InputSource) + */ + private void createWorkspaceInternal(String workspaceName, + InputSource configTemplate) + throws RepositoryException { + synchronized (wspInfos) { + if (wspInfos.containsKey(workspaceName)) { + throw new RepositoryException("workspace '" + + workspaceName + "' already exists."); + } + + // create the workspace configuration + WorkspaceConfig config = repConfig.createWorkspaceConfig(workspaceName, configTemplate); + WorkspaceInfo info = createWorkspaceInfo(config); + wspInfos.put(workspaceName, info); + } + } + + /** + * Creates a workspace with the given name and given workspace configuration + * template. + * + * @param workspaceName name of the new workspace + * @param configTemplate the workspace configuration template of the new + * workspace + * @throws RepositoryException if a workspace with the given name already + * exists or if another error occurs + * @see WorkspaceImpl#createWorkspace(String,InputSource) + */ + protected void createWorkspace(String workspaceName, + InputSource configTemplate) + throws RepositoryException { + + if (createWorkspaceEventChannel == null) { + createWorkspaceInternal(workspaceName, configTemplate); + } else { + ClonedInputSource template = new ClonedInputSource(configTemplate); + createWorkspaceInternal(workspaceName, template.cloneInputSource()); + createWorkspaceEventChannel.workspaceCreated(workspaceName, template); + } + } + + SharedItemStateManager getWorkspaceStateManager(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + // check sanity of this instance + sanityCheck(); + + return getWorkspaceInfo(workspaceName).getItemStateProvider(); + } + + /** + * Enables or disables referential integrity checking for given workspace. + * Disabling referential integrity checks can result in a corrupted + * workspace, and thus this feature is only available to customized + * implementations that subclass RepositoryImpl. + * + * @see Issue JCR-954 + * @param workspace name of the workspace + * @param enabled true to enable integrity checking (default), + * false to disable it + * @throws RepositoryException if an error occurs + */ + protected void setReferentialIntegrityChecking( + String workspace, boolean enabled) throws RepositoryException { + SharedItemStateManager manager = getWorkspaceStateManager(workspace); + manager.setCheckReferences(enabled); + } + + ObservationDispatcher getObservationDispatcher(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + // check sanity of this instance + sanityCheck(); + + return getWorkspaceInfo(workspaceName).getObservationDispatcher(); + } + + /** + * Returns the {@link SearchManager} for the workspace with name + * workspaceName. + * + * @param workspaceName the name of the workspace. + * @return the SearchManager for the workspace, or + * null if the workspace does not have a + * SearchManager configured. + * @throws NoSuchWorkspaceException if there is no workspace with name + * workspaceName. + * @throws RepositoryException if an error occurs while opening the + * search index. + */ + SearchManager getSearchManager(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + // check sanity of this instance + sanityCheck(); + + return getWorkspaceInfo(workspaceName).getSearchManager(); + } + + /** + * Returns the {@link LockManager} for the workspace with name + * workspaceName + * + * @param workspaceName workspace name + * @return LockManager for the workspace + * @throws NoSuchWorkspaceException if such a workspace does not exist + * @throws RepositoryException if some other error occurs + */ + LockManagerImpl getLockManager(String workspaceName) throws + NoSuchWorkspaceException, RepositoryException { + // check sanity of this instance + sanityCheck(); + + return getWorkspaceInfo(workspaceName).getLockManager(); + } + + /** + * Returns the {@link org.apache.jackrabbit.core.retention.RetentionRegistry} for the workspace with name + * workspaceName + * + * @param workspaceName workspace name + * @return RetentionEvaluator for the workspace + * @throws NoSuchWorkspaceException if such a workspace does not exist + * @throws RepositoryException if some other error occurs + */ + RetentionRegistry getRetentionRegistry(String workspaceName) throws NoSuchWorkspaceException, RepositoryException { + // check sanity of this instance + sanityCheck(); + return getWorkspaceInfo(workspaceName).getRetentionRegistry(); + } + + /** + * Returns the {@link SystemSession} for the workspace with name + * workspaceName + * + * @param workspaceName workspace name + * @return system session of the specified workspace + * @throws NoSuchWorkspaceException if such a workspace does not exist + * @throws RepositoryException if some other error occurs + */ + SystemSession getSystemSession(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + // check sanity of this instance + sanityCheck(); + + return getWorkspaceInfo(workspaceName).getSystemSession(); + } + + /** + * Marks the specified workspace as "active", so that the workspace + * janitor won't attempt to dispose it. This is used by features like + * security managers and the data store garbage collector to prevent + * workspaces from disappearing from below them. + *

    + * FIXME: There should be a cleaner way to do this. + * + * @param workspaceName workspace name + * @throws RepositoryException if the workspace can not be accessed + */ + void markWorkspaceActive(String workspaceName) throws RepositoryException { + getWorkspaceInfo(workspaceName).setActive(true); + } + + /** + * Creates a new repository session on the specified workspace for the + * authenticated subject of the given login context and + * adds it to the active sessions. + *

    + * Calls {@link #createSessionInstance(AuthContext, WorkspaceConfig)} to + * create the actual SessionImpl instance. + * + * @param loginContext login context with authenticated subject + * @param workspaceName workspace name + * @return a new session + * @throws NoSuchWorkspaceException if the specified workspace does not exist + * @throws AccessDeniedException if the subject of the given login context + * is not granted access to the specified + * workspace + * @throws RepositoryException if another error occurs + */ + protected final SessionImpl createSession(AuthContext loginContext, + String workspaceName) + throws NoSuchWorkspaceException, AccessDeniedException, + RepositoryException { + WorkspaceInfo wspInfo = getWorkspaceInfo(workspaceName); + SessionImpl ses = createSessionInstance(loginContext, wspInfo.getConfig()); + onSessionCreated(ses); + // reset idle timestamp + wspInfo.setIdleTimestamp(0); + return ses; + } + + /** + * Creates a new repository session on the specified workspace for the given + * authenticated subject and adds it to the active + * sessions. + *

    + * Calls {@link #createSessionInstance(Subject, WorkspaceConfig)} to + * create the actual SessionImpl instance. + * + * @param subject authenticated subject + * @param workspaceName workspace name + * @return a new session + * @throws NoSuchWorkspaceException if the specified workspace does not exist + * @throws AccessDeniedException if the subject of the given login context + * is not granted access to the specified + * workspace + * @throws RepositoryException if another error occurs + */ + protected final SessionImpl createSession(Subject subject, + String workspaceName) + throws NoSuchWorkspaceException, AccessDeniedException, + RepositoryException { + WorkspaceInfo wspInfo = getWorkspaceInfo(workspaceName); + SessionImpl ses = createSessionInstance(subject, wspInfo.getConfig()); + onSessionCreated(ses); + // reset idle timestamp + wspInfo.setIdleTimestamp(0); + return ses; + } + + /** + * Adds the given session to the list of active sessions and registers this + * repository as listener. + * + * @param session the session to register + */ + protected void onSessionCreated(SessionImpl session) { + synchronized (activeSessions) { + session.addListener(this); + activeSessions.put(session, session); + } + } + + /** + * Tries to add Principals to a given subject: + * First Access the Subject from the current AccessControlContext, + * If Subject is found the LoginContext is evoked for it, in order + * to possibly allow for extension of preauthenticated Subject.
    + * In contrast to a login with Credentials, a Session is created, even if the + * Authentication failed.
    + * If the {@link Subject} is marked to be unmodificable or if the + * authentication of the the Subject failed Session is build for unchanged + * Subject. + * + * @param workspaceName must not be null + * @return if a Subject is exsting null else + * @throws RepositoryException + * @throws AccessDeniedException + */ + private Session extendAuthentication(String workspaceName) + throws RepositoryException, AccessDeniedException { + + Subject subject = null; + try { + AccessControlContext acc = AccessController.getContext(); + subject = Subject.getSubject(acc); + } catch (SecurityException e) { + log.warn("Can't check for preauthentication. Reason: {}", e.getMessage()); + } + if (subject == null) { + log.debug("No preauthenticated subject found -> return null."); + return null; + } + + Session s; + if (subject.isReadOnly()) { + log.debug("Preauthenticated Subject is read-only -> create Session"); + s = createSession(subject, workspaceName); + } else { + log.debug("Found preauthenticated Subject, try to extend authentication"); + // login either using JAAS or custom LoginModule + AuthContext authCtx = context.getSecurityManager().getAuthContext( + null, subject, workspaceName); + try { + authCtx.login(); + s = createSession(authCtx, workspaceName); + } catch (javax.security.auth.login.LoginException e) { + // subject could not be extended + log.debug("Preauthentication could not be extended"); + s = createSession(subject, workspaceName); + } + } + return s; + } + + //-------------------------------------------------< JackrabbitRepository > + + /** + * Shuts down this repository. The shutdown is guarded by a shutdown lock + * that prevents any new sessions from being started simultaneously. + */ + public void shutdown() { + try { + shutdownLock.writeLock().acquire(); + } catch (InterruptedException e) { + // TODO: Should this be a checked exception? + throw new RuntimeException("Shutdown lock could not be acquired", e); + } + + try { + // check status of this instance + if (!disposed) { + doShutdown(); + } + } finally { + shutdownLock.writeLock().release(); + } + } + + /** + * Protected method that performs the actual shutdown after the shutdown + * lock has been acquired by the {@link #shutdown()} method. + */ + protected synchronized void doShutdown() { + log.info("Shutting down repository..."); + + // stop optional cluster node + ClusterNode clusterNode = context.getClusterNode(); + if (clusterNode != null) { + clusterNode.stop(); + } + + if (securityMgr != null) { + securityMgr.close(); + } + + // close active user sessions + // (copy sessions to array to avoid ConcurrentModificationException; + // manually copy entries rather than calling ReferenceMap#toArray() in + // order to work around http://issues.apache.org/bugzilla/show_bug.cgi?id=25551) + List sa; + synchronized (activeSessions) { + sa = new ArrayList(activeSessions.size()); + for (Session session : activeSessions.values()) { + sa.add(session); + } + } + for (Session session : sa) { + if (session != null) { + session.logout(); + } + } + + // shutdown system search manager if there is one + if (systemSearchMgr != null) { + systemSearchMgr.close(); + } + + // shut down workspaces + synchronized (wspInfos) { + for (WorkspaceInfo wspInfo : wspInfos.values()) { + wspInfo.dispose(); + } + } + + try { + InternalVersionManager m = context.getInternalVersionManager(); + if (m != null) { + m.close(); + } + } catch (Exception e) { + log.error("Error while closing Version Manager.", e); + } + + repDescriptors.clear(); + + DataStore dataStore = context.getDataStore(); + if (dataStore != null) { + try { + // close the datastore + dataStore.close(); + } catch (DataStoreException e) { + log.error("error while closing datastore", e); + } + } + + try { + // close repository file system + context.getFileSystem().close(); + } catch (FileSystemException e) { + log.error("error while closing repository file system", e); + } + + try { + nodeIdFactory.close(); + } catch (RepositoryException e) { + log.error("error while closing repository file system", e); + } + + // make sure this instance is not used anymore + disposed = true; + + // wake up threads waiting on this instance's monitor (e.g. workspace janitor) + notifyAll(); + + // Shut down the executor service + ScheduledExecutorService executor = context.getExecutor(); + executor.shutdown(); + try { + // Wait for all remaining background threads to terminate + if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { + log.warn("Attempting to forcibly shutdown runaway threads"); + executor.shutdownNow(); + } + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for background threads", e); + } + + repConfig.getConnectionFactory().close(); + + // finally release repository lock + if (repLock != null) { + try { + repLock.release(); + } catch (RepositoryException e) { + log.error("failed to release the repository lock", e); + } + } + + log.info("Repository has been shutdown"); + } + + /** + * Returns the configuration of this repository. + * @return repository configuration + */ + public RepositoryConfig getConfig() { + return repConfig; + } + + /** + * Initializes the repository descriptors by executing the following steps: + *

      + *
    • Sets standard descriptors
    • + *
    • {@link #getCustomRepositoryDescriptors()} is called + * afterwards in order to add custom/overwrite standard repository descriptors.
    • + *
    + * + * @throws RepositoryException + */ + protected void initRepositoryDescriptors() throws RepositoryException { + + ValueFactory valFactory = ValueFactoryImpl.getInstance(); + Value valTrue = valFactory.createValue(true); + Value valFalse = valFactory.createValue(false); + + setDescriptor(Repository.REP_NAME_DESC, "Jackrabbit"); + setDescriptor(Repository.REP_VENDOR_DESC, "Apache Software Foundation"); + setDescriptor(Repository.REP_VENDOR_URL_DESC, "http://jackrabbit.apache.org/"); + setDescriptor(Repository.SPEC_NAME_DESC, "Content Repository API for Java(TM) Technology Specification"); + setDescriptor(Repository.SPEC_VERSION_DESC, "2.0"); + + setDescriptor(Repository.IDENTIFIER_STABILITY, Repository.IDENTIFIER_STABILITY_INDEFINITE_DURATION); + setDescriptor(Repository.LEVEL_1_SUPPORTED, valTrue); + setDescriptor(Repository.LEVEL_2_SUPPORTED, valTrue); + setDescriptor(Repository.WRITE_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED, valTrue); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED, valTrue); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_INHERITANCE, Repository.NODE_TYPE_MANAGEMENT_INHERITANCE_MULTIPLE); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED, valTrue); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED, valTrue); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED, valTrue); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED, valFalse); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED, valTrue); + + Value[] types = new Value[] { + valFactory.createValue(PropertyType.BINARY), + valFactory.createValue(PropertyType.BOOLEAN), + valFactory.createValue(PropertyType.DATE), + valFactory.createValue(PropertyType.DECIMAL), + valFactory.createValue(PropertyType.DOUBLE), + valFactory.createValue(PropertyType.LONG), + valFactory.createValue(PropertyType.NAME), + valFactory.createValue(PropertyType.PATH), + valFactory.createValue(PropertyType.REFERENCE), + valFactory.createValue(PropertyType.STRING), + valFactory.createValue(PropertyType.URI), + valFactory.createValue(PropertyType.WEAKREFERENCE), + valFactory.createValue(PropertyType.UNDEFINED) + }; + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_PROPERTY_TYPES, types); + + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED, valTrue); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED, valTrue); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED, valTrue); + setDescriptor(Repository.NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPORTED, valFalse); + setDescriptor(Repository.OPTION_ACCESS_CONTROL_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_LIFECYCLE_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_LOCKING_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_QUERY_SQL_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_RETENTION_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_SHAREABLE_NODES_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_SIMPLE_VERSIONING_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_TRANSACTIONS_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_UNFILED_CONTENT_SUPPORTED, valFalse); + setDescriptor(Repository.OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_VERSIONING_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_XML_EXPORT_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_XML_IMPORT_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_ACTIVITIES_SUPPORTED, valTrue); + setDescriptor(Repository.OPTION_BASELINES_SUPPORTED, valTrue); + + setDescriptor(Repository.QUERY_FULL_TEXT_SEARCH_SUPPORTED, valTrue); + setDescriptor(Repository.QUERY_JOINS, Repository.QUERY_JOINS_INNER_OUTER); + + Value[] languages = new Value[] { + valFactory.createValue("javax.jcr.query.JCR-JQOM"), + valFactory.createValue("javax.jcr.query.JCR-SQL2") + }; + setDescriptor(Repository.QUERY_LANGUAGES, languages); + + setDescriptor(Repository.QUERY_STORED_QUERIES_SUPPORTED, valTrue); + setDescriptor(Repository.QUERY_XPATH_POS_INDEX, valTrue); + // Disabled since in default configuration document order is not supported. + // See https://issues.apache.org/jira/browse/JCR-1237 for details + setDescriptor(Repository.QUERY_XPATH_DOC_ORDER, valFalse); + + // now set customized repository descriptor values (if any exist) + Properties props = getCustomRepositoryDescriptors(); + if (props != null) { + for (Object o : props.keySet()) { + String key = (String) o; + setDescriptor(key, props.getProperty(key)); + } + } + } + + /** + * Returns a Properties object containing custom repository + * descriptors or null if none exist. + *

    + * Overridable to allow subclasses to add custom descriptors or to + * override standard descriptor values. + *

    + * Note that the properties entries will be set as single-valued STRING + * descriptor values. + *

    + * This method tries to load the Properties from the + * org/apache/jackrabbit/core/repository.properties resource + * found in the class path. + * + * @throws RepositoryException if the properties can not be loaded + */ + protected Properties getCustomRepositoryDescriptors() throws RepositoryException { + InputStream in = RepositoryImpl.class.getResourceAsStream(PROPERTIES_RESOURCE); + if (in != null) { + try { + Properties props = new Properties(); + props.load(in); + return props; + } catch (IOException e) { + String msg = "Failed to load customized repository properties: " + e.toString(); + log.error(msg); + throw new RepositoryException(msg, e); + } finally { + IOUtils.closeQuietly(in); + } + } else { + return null; + } + } + + protected void setDescriptor(String desc, String value) { + setDescriptor(desc, ValueFactoryImpl.getInstance().createValue(value)); + } + + protected void setDescriptor(String desc, Value value) { + repDescriptors.put(desc, new DescriptorValue(value)); + } + + protected void setDescriptor(String desc, Value[] values) { + repDescriptors.put(desc, new DescriptorValue(values)); + } + + /** + * Creates a workspace persistence manager based on the given + * configuration. The persistence manager is instantiated using + * information in the given persistence manager configuration and + * initialized with a persistence manager context containing the other + * arguments. + * + * @return the created workspace persistence manager + * @throws RepositoryException if the persistence manager could + * not be instantiated/initialized + */ + private PersistenceManager createPersistenceManager( + File homeDir, FileSystem fs, PersistenceManagerConfig pmConfig) + throws RepositoryException { + try { + PersistenceManager pm = pmConfig + .newInstance(PersistenceManager.class); + PMContext pmContext = new PMContext( + homeDir, fs, + context.getRootNodeId(), + context.getNamespaceRegistry(), + context.getNodeTypeRegistry(), + context.getDataStore(), + context.getRepositoryStatistics()); + pm.init(pmContext); + return pm; + } catch (Exception e) { + String msg = "Cannot instantiate persistence manager " + pmConfig.getClassName(); + throw new RepositoryException(msg, e); + } + } + + /** + * Creates a SharedItemStateManager or derivative. + * + * @param persistMgr persistence manager + * @param usesReferences true if the item state manager should use + * node references to verify integrity of its reference properties; + * false otherwise + * @return item state manager + * @throws ItemStateException if an error occurs + */ + protected SharedItemStateManager createItemStateManager( + PersistenceManager persistMgr, boolean usesReferences, + ISMLocking locking) throws ItemStateException { + return new SharedItemStateManager( + persistMgr, + context.getRootNodeId(), + context.getNodeTypeRegistry(), + true, + context.getItemStateCacheFactory(), + locking, + context.getNodeIdFactory()); + } + + /** + * Creates a data store garbage collector for this repository. + *

    + * Note that you should use the {@link RepositoryManager} interface + * to access this functionality. This RepositoryImpl method may be + * removed in future Jackrabbit versions. + */ + public GarbageCollector createDataStoreGarbageCollector() + throws RepositoryException { + ArrayList pmList = new ArrayList(); + InternalVersionManagerImpl vm = context.getInternalVersionManager(); + PersistenceManager pm = vm.getPersistenceManager(); + pmList.add(pm); + String[] wspNames = getWorkspaceNames(); + SessionImpl[] sessions = new SessionImpl[wspNames.length]; + for (int i = 0; i < wspNames.length; i++) { + String wspName = wspNames[i]; + WorkspaceInfo wspInfo = getWorkspaceInfo(wspName); + + // this will initialize the workspace if required + SessionImpl systemSession = + SystemSession.create(context, wspInfo.getConfig()); + + // mark the workspace as 'active' so it does not get disposed by + // the workspace-janitor until the garbage collector is done + wspInfo.setActive(true); + + // the workspace could be disposed, so re-initialize if required + // afterwards it will not be disposed because it was marked active + wspInfo.initialize(); + + sessions[i] = systemSession; + pm = wspInfo.getPersistenceManager(); + pmList.add(pm); + } + IterablePersistenceManager[] ipmList = + new IterablePersistenceManager[pmList.size()]; + for (int i = 0; i < pmList.size(); i++) { + pm = pmList.get(i); + if (!(pm instanceof IterablePersistenceManager)) { + ipmList = null; + break; + } + ipmList[i] = (IterablePersistenceManager) pm; + } + GarbageCollector gc = new GarbageCollector(context, context.getDataStore(), ipmList, sessions); + synchronized (this) { + if (context.isGcRunning()) { + throw new RepositoryException("Cannot create GC. GC already running"); + } + context.setGcRunning(true); + } + return gc; + } + + //-----------------------------------------------------------< Repository > + /** + * {@inheritDoc} + */ + public Session login(Credentials credentials, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException { + try { + shutdownLock.readLock().acquire(); + } catch (InterruptedException e) { + throw new RepositoryException("Login lock could not be acquired", e); + } + + try { + // check sanity of this instance + sanityCheck(); + + if (workspaceName == null) { + workspaceName = repConfig.getDefaultWorkspaceName(); + } + + // check if workspace exists (will throw NoSuchWorkspaceException if not) + getWorkspaceInfo(workspaceName); + + if (credentials == null) { + // try to obtain the identity of the already authenticated + // subject from access control context + Session session = extendAuthentication(workspaceName); + if (session != null) { + // successful extended authentication + return session; + } else { + log.debug("Attempt to login without Credentials and Subject -> try login with null credentials."); + } + } + // not preauthenticated -> try login with credentials + AuthContext authCtx = context.getSecurityManager().getAuthContext( + credentials, new Subject(), workspaceName); + authCtx.login(); + + // create session, and add SimpleCredentials attributes (JCR-1932) + SessionImpl session = createSession(authCtx, workspaceName); + if (credentials instanceof SimpleCredentials) { + SimpleCredentials sc = (SimpleCredentials) credentials; + for (String name : sc.getAttributeNames()) { + if (!TokenBasedAuthentication.isMandatoryAttribute(name)) { + session.setAttribute(name, sc.getAttribute(name)); + } + } + } + Set tokenCreds = session.getSubject().getPublicCredentials(TokenCredentials.class); + if (!tokenCreds.isEmpty()) { + TokenCredentials tc = tokenCreds.iterator().next(); + for (String name : tc.getAttributeNames()) { + if (!TokenBasedAuthentication.isMandatoryAttribute(name)) { + session.setAttribute(name, tc.getAttribute(name)); + } + } + } + + log.debug("User {} logged in to workspace {}", + session.getUserID(), workspaceName); + return session; + } catch (SecurityException se) { + throw new LoginException("Unable to access authentication information", se); + } catch (javax.security.auth.login.LoginException le) { + throw new LoginException(le.getMessage(), le); + } catch (AccessDeniedException ade) { + // authenticated subject is not authorized for the specified workspace + throw new LoginException("Workspace access denied", ade); + } finally { + shutdownLock.readLock().release(); + } + } + + /** + * {@inheritDoc} + */ + public String getDescriptor(String key) { + Value v = getDescriptorValue(key); + try { + return (v == null) ? null : v.getString(); + } catch (RepositoryException e) { + log.error("corrupt descriptor value: " + key, e); + return null; + } + } + + /** + * {@inheritDoc} + */ + public String[] getDescriptorKeys() { + String[] keys = repDescriptors.keySet().toArray(new String[repDescriptors.keySet().size()]); + Arrays.sort(keys); + return keys; + } + + /** + * {@inheritDoc} + */ + public Value getDescriptorValue(String key) { + DescriptorValue descVal = repDescriptors.get(key); + return (descVal != null) ? descVal.getValue() : null; + } + + /** + * {@inheritDoc} + */ + public Value[] getDescriptorValues(String key) { + DescriptorValue descVal = repDescriptors.get(key); + return (descVal != null) ? descVal.getValues() : null; + } + + /** + * {@inheritDoc} + */ + public boolean isSingleValueDescriptor(String key) { + DescriptorValue descVal = repDescriptors.get(key); + return (descVal != null && descVal.getValue() != null); + } + + //------------------------------------------------------< SessionListener > + /** + * {@inheritDoc} + */ + public void loggingOut(SessionImpl session) { + } + + /** + * {@inheritDoc} + */ + public void loggedOut(SessionImpl session) { + synchronized (activeSessions) { + // remove session from active sessions + activeSessions.remove(session); + } + } + + //------------------------------------------< overridable factory methods > + /** + * Creates an instance of the {@link SessionImpl} class representing a + * user authenticated by the loginContext instance attached + * to the workspace configured by the wspConfig. + * + * @throws AccessDeniedException if the subject of the given login context + * is not granted access to the specified + * workspace + * @throws RepositoryException If any other error occurs creating the + * session. + */ + protected SessionImpl createSessionInstance(AuthContext loginContext, + WorkspaceConfig wspConfig) + throws AccessDeniedException, RepositoryException { + return new XASessionImpl(context, loginContext, wspConfig); + } + + /** + * Creates an instance of the {@link SessionImpl} class representing a + * user represented by the subject instance attached + * to the workspace configured by the wspConfig. + * + * @throws AccessDeniedException if the subject of the given login context + * is not granted access to the specified + * workspace + * @throws RepositoryException If any other error occurs creating the + * session. + */ + protected SessionImpl createSessionInstance(Subject subject, + WorkspaceConfig wspConfig) + throws AccessDeniedException, RepositoryException { + return new XASessionImpl(context, subject, wspConfig); + } + + /** + * Creates a new {@link RepositoryImpl.WorkspaceInfo} instance for + * wspConfig. + * + * @param wspConfig the workspace configuration. + * @return a new WorkspaceInfo instance. + */ + protected WorkspaceInfo createWorkspaceInfo(WorkspaceConfig wspConfig) { + return new WorkspaceInfo(wspConfig); + } + + //--------------------------------------------------------< inner classes > + /** + * WorkspaceInfo holds the objects that are shared + * among multiple per-session WorkspaceImpl instances + * representing the same named workspace, i.e. the same physical + * storage. + */ + public class WorkspaceInfo implements UpdateEventListener { + + /** + * workspace configuration (passed in constructor) + */ + private final WorkspaceConfig config; + + /** + * file system (instantiated on init) + */ + private FileSystem fs; + + /** + * persistence manager (instantiated on init) + */ + private PersistenceManager persistMgr; + + /** + * item state provider (instantiated on init) + */ + private SharedItemStateManager itemStateMgr; + + /** + * observation dispatcher (instantiated on init) + */ + private ObservationDispatcher dispatcher; + + /** + * system session (lazily instantiated) + */ + private SystemSession systemSession; + + /** + * search manager (lazily instantiated) + */ + private SearchManager searchMgr; + + /** + * lock manager (lazily instantiated) + */ + private LockManagerImpl lockMgr; + + /** + * internal manager for evaluation of effective retention policies + * and holds + */ + private RetentionRegistryImpl retentionReg; + + /** + * flag indicating whether this instance has been initialized. + */ + private boolean initialized; + + /** + * Flag used to mark this as an "active" workspace that should not + * get automatically disposed by the workspace janitor. + */ + private boolean active; + + /** + * lock that guards the initialization of this instance + */ + private final ReadWriteLock initLock = + new ReentrantWriterPreferenceReadWriteLock(); + + /** + * timestamp when the workspace has been determined being idle + */ + private long idleTimestamp; + + /** + * mutex for this workspace, used for locking transactions + */ + private final Mutex xaLock = new Mutex(); + + /** + * Update event channel, used in clustered environment. + */ + private UpdateEventChannel updateChannel; + + /** + * Lock event channel, used in clustered environment. + */ + private LockEventChannel lockChannel; + + /** + * Creates a new WorkspaceInfo based on the given + * config. + * + * @param config workspace configuration + */ + protected WorkspaceInfo(WorkspaceConfig config) { + this.config = config; + idleTimestamp = 0; + initialized = false; + } + + /** + * Returns the workspace name. + * + * @return the workspace name + */ + protected String getName() { + return config.getName(); + } + + /** + * Returns the workspace configuration. + * + * @return the workspace configuration + */ + public WorkspaceConfig getConfig() { + return config; + } + + /** + * Returns the timestamp when the workspace has become idle or zero + * if the workspace is currently not idle. + * + * @return the timestamp when the workspace has become idle or zero if + * the workspace is not idle. + */ + final long getIdleTimestamp() { + return idleTimestamp; + } + + /** + * Sets the timestamp when the workspace has become idle. if + * ts == 0 the workspace is marked as being currently + * active. + * + * @param ts timestamp when workspace has become idle. + */ + final void setIdleTimestamp(long ts) { + idleTimestamp = ts; + } + + /** + * Returns true if this workspace info is initialized, + * otherwise returns false. + * + * @return true if this workspace info is initialized. + */ + protected final boolean isInitialized() { + try { + if (!initLock.readLock().attempt(0)) { + return false; + } + } catch (InterruptedException e) { + return false; + } + // can't use 'finally' pattern here + boolean ret = initialized; + initLock.readLock().release(); + return ret; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + /** + * Returns the workspace file system. + * + * @return the workspace file system + */ + protected FileSystem getFileSystem() { + if (!isInitialized()) { + throw new IllegalStateException("workspace '" + getName() + + "' not initialized"); + } + + return fs; + } + + /** + * Returns the workspace persistence manager. + * + * @return the workspace persistence manager + * @throws RepositoryException if the persistence manager could not be + * instantiated/initialized + */ + public PersistenceManager getPersistenceManager() + throws RepositoryException { + if (!isInitialized()) { + throw new IllegalStateException("workspace '" + getName() + + "' not initialized"); + } + + return persistMgr; + } + + /** + * Returns the workspace item state provider + * + * @return the workspace item state provider + * @throws RepositoryException if the workspace item state provider + * could not be created + */ + protected SharedItemStateManager getItemStateProvider() + throws RepositoryException { + if (!isInitialized()) { + throw new IllegalStateException("workspace '" + getName() + + "' not initialized"); + } + + return itemStateMgr; + } + + /** + * Returns the observation dispatcher for this workspace + * + * @return the observation dispatcher for this workspace + */ + protected ObservationDispatcher getObservationDispatcher() { + if (!isInitialized()) { + throw new IllegalStateException("workspace '" + getName() + + "' not initialized"); + } + + return dispatcher; + } + + /** + * Returns the search manager for this workspace. + * + * @return the search manager for this workspace, or null + * if no SearchManager + * @throws RepositoryException if the search manager could not be created + */ + protected SearchManager getSearchManager() throws RepositoryException { + if (!isInitialized()) { + throw new IllegalStateException("workspace '" + getName() + + "' not initialized"); + } + + synchronized (this) { + if (searchMgr == null && config.isSearchEnabled()) { + // search manager is lazily instantiated in order to avoid + // 'chicken & egg' bootstrap problems + searchMgr = new SearchManager( + getName(), + context, + config, + itemStateMgr, persistMgr, + context.getRootNodeId(), + getSystemSearchManager(getName()), + SYSTEM_ROOT_NODE_ID); + } + return searchMgr; + } + } + + /** + * Returns the lock manager for this workspace. + * + * @return the lock manager for this workspace + * @throws RepositoryException if the lock manager could not be created + */ + protected LockManagerImpl getLockManager() throws RepositoryException { + if (!isInitialized()) { + throw new IllegalStateException("workspace '" + getName() + + "' not initialized"); + } + + synchronized (this) { + // lock manager is lazily instantiated in order to avoid + // 'chicken & egg' bootstrap problems + if (lockMgr == null) { + lockMgr = createLockManager(); + ClusterNode clusterNode = context.getClusterNode(); + if (clusterNode != null && config.isClustered()) { + lockChannel = clusterNode.createLockChannel(getName()); + lockMgr.setEventChannel(lockChannel); + } + } + return lockMgr; + } + } + + /** + * Create a new lock manager. This method is only called once within + * getLockManager(). + * + * @return the lock manager + */ + protected LockManagerImpl createLockManager() throws RepositoryException { + return new LockManagerImpl( + getSystemSession(), fs, context.getExecutor()); + } + + /** + * Return manager used for evaluating effect retention and holds. + * + * @return + * @throws RepositoryException + */ + protected RetentionRegistry getRetentionRegistry() throws RepositoryException { + if (!isInitialized()) { + throw new IllegalStateException("workspace '" + getName() + "' not initialized"); + } + synchronized (this) { + if (retentionReg == null) { + retentionReg = new RetentionRegistryImpl(getSystemSession(), fs); + } + return retentionReg; + } + } + + /** + * Returns the system session for this workspace. + * + * @return the system session for this workspace + * @throws RepositoryException if the system session could not be created + */ + protected SystemSession getSystemSession() throws RepositoryException { + if (!isInitialized()) { + throw new IllegalStateException("workspace '" + getName() + + "' not initialized"); + } + + synchronized (this) { + // system session is lazily instantiated in order to avoid + // 'chicken & egg' bootstrap problems + if (systemSession == null) { + systemSession = SystemSession.create(context, config); + } + return systemSession; + } + } + + /** + * Initializes this workspace info. The following components are + * initialized immediately: + *

      + *
    • file system
    • + *
    • persistence manager
    • + *
    • shared item state manager
    • + *
    • observation manager factory
    • + *
    + * The following components are initialized lazily (i.e. on demand) + * in order to save resources and to avoid 'chicken & egg' bootstrap + * problems: + *
      + *
    • system session
    • + *
    • lock manager
    • + *
    • search manager
    • + *
    + * @return true if this instance has been successfully + * initialized, false if it is already initialized. + * @throws RepositoryException if an error occurred during the initialization + */ + final boolean initialize() throws RepositoryException { + // check initialize status + try { + initLock.readLock().acquire(); + } catch (InterruptedException e) { + throw new RepositoryException("Unable to aquire read lock.", e); + } + try { + if (initialized) { + // already initialized, we're done + return false; + } + } finally { + initLock.readLock().release(); + } + + // workspace info was not initialized, now check again with write lock + try { + initLock.writeLock().acquire(); + } catch (InterruptedException e) { + throw new RepositoryException("Unable to aquire write lock.", e); + } + try { + if (initialized) { + // already initialized, some other thread was quicker, we're done + return false; + } + log.info("initializing workspace '" + getName() + "'..."); + doInitialize(); + initialized = true; + doPostInitialize(); + log.info("workspace '" + getName() + "' initialized"); + return true; + } finally { + initLock.writeLock().release(); + } + } + + /** + * Does the actual initialization work. assumes holding write lock. + * @throws RepositoryException if an error occurs. + */ + protected void doInitialize() throws RepositoryException { + fs = config.getFileSystem(); + + persistMgr = createPersistenceManager( + new File(config.getHomeDir()), fs, + config.getPersistenceManagerConfig()); + + doVersionRecovery(); + + ISMLocking ismLocking = config.getISMLocking(); + + // create item state manager + try { + itemStateMgr = + createItemStateManager(persistMgr, true, ismLocking); + try { + itemStateMgr.addVirtualItemStateProvider( + context.getInternalVersionManager().getVirtualItemStateProvider()); + itemStateMgr.addVirtualItemStateProvider( + virtNTMgr.getVirtualItemStateProvider()); + } catch (Exception e) { + log.error("Unable to add vmgr: " + e.toString(), e); + } + ClusterNode clusterNode = context.getClusterNode(); + if (clusterNode != null && config.isClustered()) { + updateChannel = clusterNode.createUpdateChannel(getName()); + itemStateMgr.setEventChannel(updateChannel); + updateChannel.setListener(this); + if (persistMgr instanceof ConsistencyChecker) { + ((ConsistencyChecker) persistMgr).setEventChannel(updateChannel); + } + } + } catch (ItemStateException ise) { + String msg = "failed to instantiate shared item state manager"; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + + dispatcher = new ObservationDispatcher(); + + // register the observation factory of that workspace + delegatingDispatcher.addDispatcher(dispatcher); + } + + /** + * If necessary, recover from a lost version history. + */ + protected void doVersionRecovery() throws RepositoryException { + // JCR-2551: Recovery from a lost version history + if (Boolean.getBoolean("org.apache.jackrabbit.version.recovery")) { + RepositoryChecker checker = new RepositoryChecker( + persistMgr, context.getInternalVersionManager()); + checker.check(ROOT_NODE_ID, true, true); + } + } + + /** + * Initializes the search manager of this workspace info. This method + * is called while still holding the write lock on this workspace + * info, but {@link #initialized} is already set to true. + * + * @throws RepositoryException if the search manager could not be created + */ + protected void doPostInitialize() + throws RepositoryException { + // get system Workspace instance + WorkspaceImpl wsp = (WorkspaceImpl) getSystemSession().getWorkspace(); + + /** + * todo implement 'System' workspace + * FIXME + * - there should be one 'System' workspace per repository + * - the 'System' workspace should have the /jcr:system node + * - versions, version history and node types should be reflected in + * this system workspace as content under /jcr:system + * - all other workspaces should be dynamic workspaces based on + * this 'read-only' system workspace + * + * for now, the jcr:system node is created in + * {@link org.apache.jackrabbit.core.state.SharedItemStateManager#createRootNodeState} + */ + + log.debug("initializing SearchManager..."); + long t0 = System.currentTimeMillis(); + // register SearchManager as event listener + SearchManager searchMgr = getSearchManager(); + if (searchMgr != null) { + wsp.getObservationManager().addEventListener(searchMgr, + Event.NODE_ADDED | Event.NODE_REMOVED + | Event.PROPERTY_ADDED | Event.PROPERTY_REMOVED + | Event.PROPERTY_CHANGED, + "/", true, null, null, false); + } + log.debug("SearchManager initialized (" + (System.currentTimeMillis() - t0) + "ms)"); + } + + /** + * Disposes this WorkspaceInfo if it has been idle for more + * than maxIdleTime milliseconds. + * + * @param maxIdleTime amount of time in mmilliseconds before an idle + * workspace is automatically shutdown. + */ + final void disposeIfIdle(long maxIdleTime) { + try { + initLock.readLock().acquire(); + } catch (InterruptedException e) { + return; + } + try { + if (!initialized || active) { + return; + } + long currentTS = System.currentTimeMillis(); + if (idleTimestamp == 0) { + // set idle timestamp + idleTimestamp = currentTS; + } else { + if ((currentTS - idleTimestamp) > maxIdleTime) { + // temporarily shutdown workspace + log.info("disposing workspace '" + getName() + + "' which has been idle for " + + (currentTS - idleTimestamp) + " ms"); + dispose(); + } + } + } finally { + initLock.readLock().release(); + } + } + + /** + * Disposes all objects this WorkspaceInfo is holding. + */ + final void dispose() { + try { + initLock.writeLock().acquire(); + } catch (InterruptedException e) { + throw new IllegalStateException("Unable to aquire write lock."); + } + try { + if (!initialized) { + // nothing to dispose of, we're already done + return; + } + + log.info("shutting down workspace '" + getName() + "'..."); + doDispose(); + // reset idle timestamp + idleTimestamp = 0; + + active = false; + initialized = false; + log.info("workspace '" + getName() + "' has been shutdown"); + } finally { + initLock.writeLock().release(); + } + } + + /** + * Does the actual disposal. assumes holding write lock. + */ + protected void doDispose() { + // inform cluster node about disposal + if (updateChannel != null) { + updateChannel.setListener(null); + } + if (lockChannel != null) { + lockChannel.setListener(null); + } + + // deregister the observation factory of that workspace + delegatingDispatcher.removeDispatcher(dispatcher); + + // dispose observation manager factory + dispatcher.dispose(); + dispatcher = null; + + // shutdown search managers + if (searchMgr != null) { + searchMgr.close(); + searchMgr = null; + } + + // deregister + if (securityMgr != null) { + securityMgr.dispose(getName()); + } + + + // close system session + if (systemSession != null) { + systemSession.removeListener(RepositoryImpl.this); + systemSession.logout(); + systemSession = null; + } + + // dispose shared item state manager + itemStateMgr.dispose(); + itemStateMgr = null; + + // close persistence manager + try { + persistMgr.close(); + } catch (Exception e) { + log.error("error while closing persistence manager of workspace " + + config.getName(), e); + } + persistMgr = null; + + // close lock manager + if (lockMgr != null) { + lockMgr.close(); + lockMgr = null; + } + + // close retention registry + if (retentionReg != null) { + retentionReg.close(); + retentionReg = null; + } + + // close workspace file system + try { + fs.close(); + } catch (FileSystemException fse) { + log.error("error while closing file system of workspace " + config.getName(), fse); + } + fs = null; + } + + /** + * Locks this workspace info. This is used (and only should be) by + * the {@link XASessionImpl} in order to lock all internal resources + * during a commit. + * + * @throws TransactionException + */ + void lockAcquire() throws TransactionException { + try { + xaLock.acquire(); + } catch (InterruptedException e) { + throw new TransactionException("Error while acquiering lock", e); + } + + } + + /** + * Unlocks this workspace info. This is used (and only should be) by + * the {@link XASessionImpl} in order to lock all internal resources + * during a commit. + */ + void lockRelease() { + xaLock.release(); + } + + //----------------------------------------------< UpdateEventListener > + + /** + * {@inheritDoc} + */ + public void externalUpdate(ChangeLog external, + List events, + long timestamp, + String userData) throws RepositoryException { + try { + EventStateCollection esc = new EventStateCollection( + getObservationDispatcher(), null, null); + esc.setUserData(userData); + esc.addAll(events); + esc.setTimestamp(timestamp); + + getItemStateProvider().externalUpdate(external, esc); + } catch (IllegalStateException e) { + String msg = "Unable to deliver events: " + e.getMessage(); + throw new RepositoryException(msg, e); + } + } + + } + + /** + * The workspace janitor thread that will shutdown workspaces that have + * been idle for a certain amount of time. + */ + private class WorkspaceJanitor implements Runnable { + + /** + * amount of time in milliseconds before an idle workspace is + * automatically shutdown. + */ + private long maxIdleTime; + + /** + * interval in milliseconds between checks for idle workspaces. + */ + private long checkInterval; + + /** + * Creates a new WorkspaceJanitor instance responsible for + * shutting down idle workspaces. + * + * @param maxIdleTime amount of time in milliseconds before an idle + * workspace is automatically shutdown. + */ + WorkspaceJanitor(long maxIdleTime) { + this.maxIdleTime = maxIdleTime; + // compute check interval as 10% of maxIdleTime + checkInterval = (long) (0.1 * maxIdleTime); + } + + /** + * {@inheritDoc} + *

    + * Performs the following tasks in a while (true) loop: + *

      + *
    1. wait for checkInterval milliseconds
    2. + *
    3. build list of initialized but currently inactive workspaces + * (excluding the default workspace)
    4. + *
    5. shutdown those workspaces that have been idle for at least + * maxIdleTime milliseconds
    6. + *
    + */ + public void run() { + while (true) { + synchronized (RepositoryImpl.this) { + try { + RepositoryImpl.this.wait(checkInterval); + } catch (InterruptedException e) { + // ignore + } + if (disposed) { + return; + } + } + // get names of workspaces + Set wspNames; + synchronized (wspInfos) { + wspNames = new HashSet(wspInfos.keySet()); + } + // remove default workspace (will never be shutdown when idle) + wspNames.remove(repConfig.getDefaultWorkspaceName()); + + synchronized (activeSessions) { + // remove workspaces with active sessions + for (Session ses : activeSessions.values()) { + wspNames.remove(ses.getWorkspace().getName()); + } + } + + // remaining names denote workspaces which currently have not + // active sessions + for (String wspName : wspNames) { + WorkspaceInfo wspInfo; + synchronized (wspInfos) { + wspInfo = wspInfos.get(wspName); + } + wspInfo.disposeIfIdle(maxIdleTime); + } + } + } + } + + /** + * Cluster context passed to a ClusterNode. + */ + class ExternalEventListener implements ClusterContext { + + /** + * {@inheritDoc} + */ + public ClusterConfig getClusterConfig() { + return getConfig().getClusterConfig(); + } + + /** + * {@inheritDoc} + */ + public File getRepositoryHome() { + return new File(getConfig().getHomeDir()); + } + + /** + * {@inheritDoc} + */ + public NamespaceResolver getNamespaceResolver() { + return new RegistryNamespaceResolver(context.getNamespaceRegistry()); + } + + /** + * {@inheritDoc} + */ + public void updateEventsReady(String workspace) throws RepositoryException { + // toggle the initialization of some workspace + getWorkspaceInfo(workspace); + } + + /** + * {@inheritDoc} + */ + public void lockEventsReady(String workspace) throws RepositoryException { + // toggle the initialization of some workspace's lock manager + getWorkspaceInfo(workspace).getLockManager(); + } + + } + + /** + * Represents a Repository Descriptor Value (either Value or Value[]) + */ + protected static final class DescriptorValue { + + private Value val; + private Value[] vals; + + protected DescriptorValue(Value val) { + this.val = val; + } + + protected DescriptorValue(Value[] vals) { + this.vals = vals; + } + + protected Value getValue() { + return val; + } + + protected Value[] getValues() { + return vals != null ? vals : new Value[] {val}; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryManagerImpl.java new file mode 100644 index 00000000000..a51a78dded3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/RepositoryManagerImpl.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.core; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.management.DataStoreGarbageCollector; +import org.apache.jackrabbit.api.management.RepositoryManager; + +/** + * The repository manager implementation. + */ +public class RepositoryManagerImpl implements RepositoryManager { + + private final TransientRepository tr; + + RepositoryManagerImpl(TransientRepository tr) { + this.tr = tr; + } + + public DataStoreGarbageCollector createDataStoreGarbageCollector() throws RepositoryException { + RepositoryImpl rep = tr.getRepository(); + if (rep != null) { + return rep.createDataStoreGarbageCollector(); + } else { + throw new RepositoryException("Repository is stopped"); + } + } + + public void stop() { + tr.shutdown(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SearchManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SearchManager.java new file mode 100644 index 00000000000..05cd8fc4099 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SearchManager.java @@ -0,0 +1,461 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.io.IOException; +import java.util.ArrayList; +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.NamespaceException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.qom.QueryObjectModel; + +import org.apache.jackrabbit.core.config.SearchConfig; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.observation.EventImpl; +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.query.AbstractQueryImpl; +import org.apache.jackrabbit.core.query.QueryHandler; +import org.apache.jackrabbit.core.query.QueryHandlerContext; +import org.apache.jackrabbit.core.query.QueryHandlerFactory; +import org.apache.jackrabbit.core.query.QueryObjectModelImpl; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.SharedItemStateManager; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.query.qom.QueryObjectModelTree; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Acts as a global entry point to execute queries and index nodes. + */ +public class SearchManager implements SynchronousEventListener { + + /** + * Logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(SearchManager.class); + + /** + * Namespace URI for xpath functions + */ + private static final String NS_FN_PREFIX = "fn"; + public static final String NS_FN_URI = "http://www.w3.org/2005/xpath-functions"; + + /** + * Deprecated namespace URI for xpath functions + */ + private static final String NS_FN_OLD_PREFIX = "fn_old"; + public static final String NS_FN_OLD_URI = "http://www.w3.org/2004/10/xpath-functions"; + + /** + * Namespace URI for XML schema + */ + private static final String NS_XS_PREFIX = "xs"; + public static final String NS_XS_URI = "http://www.w3.org/2001/XMLSchema"; + + /** + * The shared item state manager instance for the workspace. + */ + private final SharedItemStateManager itemMgr; + + /** + * QueryHandler where query execution is delegated to + */ + private QueryHandler handler; + + /** + * QueryHandler of the parent search manager or null if there + * is none. + */ + private final QueryHandler parentHandler; + + /** + * The namespace registry of the repository. + */ + private final NamespaceRegistryImpl nsReg; + + /** + * Path that will be excluded from indexing. + */ + private Path excludePath; + + /** + * Creates a new SearchManager. + * + * @param workspace the workspace name + * @param repositoryContext the repository context + * @param qhf the query handler factory + * @param itemMgr the shared item state manager. + * @param pm the underlying persistence manager. + * @param rootNodeId the id of the root node. + * @param parentMgr the parent search manager or null if + * there is no parent search manager. + * @param excludedNodeId id of the node that should be excluded from + * indexing. Any descendant of that node will also be + * excluded from indexing. + * @throws RepositoryException if the search manager cannot be initialized + */ + public SearchManager( + String workspace, + RepositoryContext repositoryContext, + QueryHandlerFactory qhf, + SharedItemStateManager itemMgr, + PersistenceManager pm, + NodeId rootNodeId, + SearchManager parentMgr, + NodeId excludedNodeId) throws RepositoryException { + this.nsReg = repositoryContext.getNamespaceRegistry(); + this.itemMgr = itemMgr; + this.parentHandler = (parentMgr != null) ? parentMgr.handler : null; + + // register namespaces + safeRegisterNamespace(NS_XS_PREFIX, NS_XS_URI); + try { + if (nsReg.getPrefix(NS_FN_OLD_URI).equals(NS_FN_PREFIX)) { + // old uri is mapped to 'fn' prefix -> re-map + String prefix = NS_FN_OLD_PREFIX; + try { + // Find a free prefix + for (int i = 2; true; i++) { + nsReg.getURI(prefix); + prefix = NS_FN_OLD_PREFIX + i; + } + } catch (NamespaceException e) { + // Re-map the old fn URI to that prefix + nsReg.registerNamespace(prefix, NS_FN_OLD_URI); + } + } + } catch (NamespaceException e) { + // does not yet exist + safeRegisterNamespace(NS_FN_OLD_PREFIX, NS_FN_OLD_URI); + } + // at this point the 'fn' prefix shouldn't be assigned anymore + safeRegisterNamespace(NS_FN_PREFIX, NS_FN_URI); + + if (excludedNodeId != null) { + HierarchyManagerImpl hmgr = + new HierarchyManagerImpl(rootNodeId, itemMgr); + excludePath = hmgr.getPath(excludedNodeId); + } + + // initialize query handler + this.handler = qhf.getQueryHandler(new QueryHandlerContext(workspace, + repositoryContext, itemMgr, pm, rootNodeId, parentHandler, + excludedNodeId)); + } + + /** + * Registers a namespace using the given prefix hint. Does nothing + * if the namespace is already registered. If the given prefix hint + * is not yet registered as a prefix, then it is used as the prefix + * of the registered namespace. Otherwise a unique prefix is generated + * based on the given hint. + * + * @param prefixHint the prefix hint + * @param uri the namespace URI + * @throws NamespaceException if an illegal attempt is made to register + * a mapping + * @throws RepositoryException if an unexpected error occurs + * @see javax.jcr.NamespaceRegistry#registerNamespace(String, String) + */ + private void safeRegisterNamespace(String prefixHint, String uri) + throws NamespaceException, RepositoryException { + try { + // Check if the namespace is already registered + nsReg.getPrefix(uri); + // ... it is, so do nothing. + } catch (NamespaceException e1) { + // ... it is not, try to find a unique prefix. + String prefix = prefixHint; + try { + for (int suffix = 2; true; suffix++) { + // Is this prefix already registered? + nsReg.getURI(prefix); + // ... it is, generate a new prefix and try again. + prefix = prefixHint + suffix; + } + } catch (NamespaceException e2) { + // ... it is not, register the namespace with this prefix. + nsReg.registerNamespace(prefix, uri); + } + } + } + + /** + * Closes this SearchManager and also closes the + * {@link FileSystem} configured in {@link SearchConfig}. + */ + public void close() { + try { + shutdownQueryHandler(); + } catch (IOException e) { + log.error("Exception closing QueryHandler.", e); + } + } + + /** + * Creates a query object that can be executed on the workspace. + * + * @param sessionContext component context of the current session + * @param statement the actual query statement. + * @param language the syntax of the query statement. + * @param node a nt:query node where the query was read from or + * null if it is not a stored query. + * @return a Query instance to execute. + * @throws InvalidQueryException if the query is malformed or the + * language is unknown. + * @throws RepositoryException if any other error occurs. + */ + public Query createQuery( + SessionContext sessionContext, + String statement, String language, Node node) + throws InvalidQueryException, RepositoryException { + AbstractQueryImpl query = createQueryInstance(); + query.init(sessionContext, handler, statement, language, node); + return query; + } + + /** + * Creates a query object model that can be executed on the workspace. + * + * @param sessionContext component context of the current session + * @param qomTree the query object model tree, representing the query. + * @param langugage the original language of the query statement. + * @param node a nt:query node where the query was read from or + * null if it is not a stored query. + * @return the query object model for the query. + * @throws InvalidQueryException the the query object model tree is + * considered invalid by the query handler + * implementation. + * @throws RepositoryException if any other error occurs. + */ + public QueryObjectModel createQueryObjectModel( + SessionContext sessionContext, QueryObjectModelTree qomTree, + String langugage, Node node) + throws InvalidQueryException, RepositoryException { + QueryObjectModelImpl qom = new QueryObjectModelImpl(); + qom.init(sessionContext, handler, qomTree, langugage, node); + return qom; + } + + /** + * Returns the ids of the nodes that refer to the node with id + * by weak references. + * + * @param id the id of the target node. + * @return the ids of the referring nodes. + * @throws RepositoryException if an error occurs. + * @throws IOException if an error occurs while reading from the + * index. + */ + public Iterable getWeaklyReferringNodes(NodeId id) + throws RepositoryException, IOException { + return handler.getWeaklyReferringNodes(id); + } + + /** + * Checks if the given event should be excluded based on the + * {@link #excludePath} setting. + * + * @param event observation event + * @return true if the event should be excluded, + * false otherwise + */ + private boolean isExcluded(EventImpl event) { + try { + return excludePath != null + && excludePath.isAncestorOf(event.getQPath()); + } catch (MalformedPathException ex) { + log.error("Error filtering events.", ex); + return false; + } catch (RepositoryException ex) { + log.error("Error filtering events.", ex); + return false; + } + + } + + //------------------------< for testing only >------------------------------ + + /** + * @return the query handler implementation. + */ + public QueryHandler getQueryHandler() { + return handler; + } + + //---------------< EventListener interface >-------------------------------- + + public void onEvent(EventIterator events) { + log.debug("onEvent: indexing started"); + long time = System.currentTimeMillis(); + + // nodes that need to be removed from the index. + final Set removedNodes = new HashSet(); + // nodes that need to be added to the index. + final Map addedNodes = new HashMap(); + // property events + List propEvents = new ArrayList(); + + while (events.hasNext()) { + EventImpl e = (EventImpl) events.nextEvent(); + if (!isExcluded(e)) { + long type = e.getType(); + if (type == Event.NODE_ADDED) { + addedNodes.put(e.getChildId(), e); + if (e.isShareableChildNode()) { + // simply re-index shareable nodes + removedNodes.add(e.getChildId()); + } + } else if (type == Event.NODE_REMOVED) { + removedNodes.add(e.getChildId()); + if (e.isShareableChildNode()) { + // check if there is a node remaining in the shared set + if (itemMgr.hasItemState(e.getChildId())) { + addedNodes.put(e.getChildId(), e); + } + } + } else { + propEvents.add(e); + } + } + } + + // sort out property events + for (EventImpl e : propEvents) { + NodeId nodeId = e.getParentId(); + if (e.getType() == Event.PROPERTY_ADDED) { + if (addedNodes.put(nodeId, e) == null) { + // only property added + // need to re-index + removedNodes.add(nodeId); + } else { + // the node where this prop belongs to is also new + } + } else if (e.getType() == Event.PROPERTY_CHANGED) { + // need to re-index + addedNodes.put(nodeId, e); + removedNodes.add(nodeId); + } else { + // property removed event is only generated when node still exists + addedNodes.put(nodeId, e); + removedNodes.add(nodeId); + } + } + + Iterator addedStates = new Iterator() { + private final Iterator iter = addedNodes.keySet().iterator(); + + public void remove() { + throw new UnsupportedOperationException(); + } + + public boolean hasNext() { + return iter.hasNext(); + } + + public NodeState next() { + NodeState item = null; + NodeId id = (NodeId) iter.next(); + try { + item = (NodeState) itemMgr.getItemState(id); + } catch (ItemStateException ise) { + // check whether this item state change originated from + // an external event + EventImpl e = addedNodes.get(id); + if (e == null || !e.isExternal()) { + log.error("Unable to index node " + id + ": does not exist"); + } else { + log.info("Node no longer available " + id + ", skipped."); + } + } + return item; + } + }; + Iterator removedIds = removedNodes.iterator(); + + if (removedNodes.size() > 0 || addedNodes.size() > 0) { + try { + handler.updateNodes(removedIds, addedStates); + } catch (RepositoryException e) { + log.error("Error indexing node.", e); + } catch (IOException e) { + log.error("Error indexing node.", e); + } + } + + if (log.isDebugEnabled()) { + log.debug("onEvent: indexing finished in " + + String.valueOf(System.currentTimeMillis() - time) + + " ms."); + } + } + + /** + * Creates a new instance of an {@link AbstractQueryImpl} which is not + * initialized. + * + * @return an new query instance. + * @throws RepositoryException if an error occurs while creating a new query + * instance. + */ + protected AbstractQueryImpl createQueryInstance() throws RepositoryException { + try { + String queryImplClassName = handler.getQueryClass(); + Object obj = Class.forName(queryImplClassName).newInstance(); + if (obj instanceof AbstractQueryImpl) { + return (AbstractQueryImpl) obj; + } else { + throw new IllegalArgumentException(queryImplClassName + + " is not of type " + AbstractQueryImpl.class.getName()); + } + } catch (Throwable t) { + throw new RepositoryException("Unable to create query: " + t.toString(), t); + } + } + + //------------------------< internal >-------------------------------------- + + /** + * Shuts down the query handler. If the query handler is already shut down + * this method does nothing. + * + * @throws IOException if an error occurs while shutting down the query + * handler. + */ + private void shutdownQueryHandler() throws IOException { + if (handler != null) { + handler.close(); + handler = null; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionFactory.java new file mode 100644 index 00000000000..8a4e4db43f2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionFactory.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.core; + +import java.security.Principal; +import java.util.Collections; + +import javax.jcr.RepositoryException; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; + +public class SessionFactory { + + private final RepositoryContext context; + + public SessionFactory(RepositoryContext context) { + this.context = context; + } + + public SessionImpl createAdminSession(String workspace) + throws RepositoryException { + Principal admin = new AdminPrincipal(SecurityConstants.ADMIN_ID); + Subject subject = new Subject( + true, Collections.singleton(admin), + Collections.emptySet(), Collections.emptySet()); + return context.getRepository().createSession(subject, workspace); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java new file mode 100644 index 00000000000..65c3359bac6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionImpl.java @@ -0,0 +1,1378 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CHECKED_OUT; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_CONSTRAINTS; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_RETENTION; + +import java.io.File; +import java.security.AccessControlException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.NamespaceException; +import javax.jcr.NoSuchWorkspaceException; +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.SimpleCredentials; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFactory; +import javax.jcr.Workspace; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; +import javax.jcr.retention.RetentionManager; +import javax.jcr.security.AccessControlManager; +import javax.jcr.version.VersionException; +import javax.security.auth.Subject; + +import org.apache.commons.collections.IteratorUtils; +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +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.commons.AbstractSession; +import org.apache.jackrabbit.core.config.WorkspaceConfig; +import org.apache.jackrabbit.core.gc.GarbageCollector; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.core.observation.ObservationManagerImpl; +import org.apache.jackrabbit.core.retention.RetentionManagerImpl; +import org.apache.jackrabbit.core.retention.RetentionRegistry; +import org.apache.jackrabbit.core.security.AMContext; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.SystemPrincipal; +import org.apache.jackrabbit.core.security.authentication.AuthContext; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionItemOperation; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.core.session.SessionRefreshOperation; +import org.apache.jackrabbit.core.session.SessionSaveOperation; +import org.apache.jackrabbit.core.state.SessionItemStateManager; +import org.apache.jackrabbit.core.version.InternalVersionManager; +import org.apache.jackrabbit.core.xml.ImportHandler; +import org.apache.jackrabbit.core.xml.SessionImporter; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.SessionExtensions; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.IdentifierResolver; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; + +/** + * A SessionImpl ... + */ +public class SessionImpl extends AbstractSession + implements JackrabbitSession, SessionExtensions, NamespaceResolver, NamePathResolver, IdentifierResolver { + + /** + * Name of the session attribute that controls whether the + * {@link #refresh(boolean)} method will cause the repository to + * synchronize itself to changes in other cluster nodes. This cluster + * synchronization is enabled by default, unless an attribute with this + * name is set (any non-null value) for this session. + * + * @since Apache Jackrabbit 1.6 + * @see JCR-1753 + */ + public static final String DISABLE_CLUSTER_SYNC_ON_REFRESH = + "org.apache.jackrabbit.disableClusterSyncOnRefresh"; + + /** + * Name of the session attribute that controls whether repository + * inconsistencies should be automatically fixed when traversing over child + * nodes, when trying to add a child node, or removing a child node. + * + * @since Apache Jackrabbit 2.2 + * @see JCR-2740 + */ + public static final String AUTO_FIX_CORRUPTIONS = + "org.apache.jackrabbit.autoFixCorruptions"; + + private static Logger log = LoggerFactory.getLogger(SessionImpl.class); + + /** + * Session counter. Used to generate unique internal session names. + */ + private static final AtomicLong SESSION_COUNTER = new AtomicLong(); + + /** + * The component context of this session. + */ + protected final SessionContext context; + + /** + * The component context of the repository that issued this session. + */ + protected final RepositoryContext repositoryContext; + + /** + * the AuthContext of this session (can be null if this + * session was not instantiated through a login process) + */ + protected AuthContext loginContext; + + /** + * the Subject of this session + */ + protected final Subject subject; + + /** + * the user ID that was used to acquire this session + */ + protected final String userId; + + /** + * Unique internal name of this session. Returned by the + * {@link #toString()} method for use in logging and debugging. + */ + private final String sessionName; + + /** + * the attributes of this session + */ + protected final Map attributes = + new HashMap(); + + /** + * Name and Path resolver + */ + protected NamePathResolver namePathResolver; + + /** + * The version manager for this session + */ + protected final InternalVersionManager versionMgr; + + /** + * Listeners (weak references) + */ + @SuppressWarnings("unchecked") + protected final Map listeners = + new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK); + + /** + * Principal Manager + */ + private PrincipalManager principalManager; + + /** + * User Manager + */ + private UserManager userManager; + + /** + * Retention and Hold Manager + */ + private RetentionManager retentionManager; + + /** + * The stack trace knows who opened this session. It is logged + * if the session is finalized, but Session.logout() was never called. + */ + private final Exception openStackTrace; + + /** + * Protected constructor. + * + * @param repositoryContext repository context + * @param loginContext + * @param wspConfig + * @throws AccessDeniedException if the subject of the given login context + * is not granted access to the specified + * workspace + * @throws RepositoryException if another error occurs + */ + protected SessionImpl( + RepositoryContext repositoryContext, AuthContext loginContext, + WorkspaceConfig wspConfig) + throws AccessDeniedException, RepositoryException { + this(repositoryContext, loginContext.getSubject(), wspConfig); + this.loginContext = loginContext; + } + + /** + * Protected constructor. + * + * @param repositoryContext repository context + * @param subject + * @param wspConfig + * @throws AccessDeniedException if the given subject is not granted access + * to the specified workspace + * @throws RepositoryException if another error occurs + */ + protected SessionImpl( + RepositoryContext repositoryContext, Subject subject, + WorkspaceConfig wspConfig) + throws AccessDeniedException, RepositoryException { + this.context = new SessionContext(repositoryContext, this, wspConfig); + this.repositoryContext = repositoryContext; + this.subject = subject; + + this.userId = retrieveUserId(subject, wspConfig.getName()); + long count = SESSION_COUNTER.incrementAndGet(); + if (userId != null) { + String user = Text.escapeIllegalJcrChars(userId); + this.sessionName = "session-" + user + "-" + count; + } else { + this.sessionName = "session-" + count; + } + + this.namePathResolver = new DefaultNamePathResolver(this, this, true); + this.context.setItemStateManager(createSessionItemStateManager()); + this.context.setItemManager(createItemManager()); + this.context.setAccessManager(createAccessManager(subject)); + this.context.setObservationManager(createObservationManager(wspConfig.getName())); + + this.versionMgr = createVersionManager(); + + // avoid building the stack trace when it won't be used anyway + this.openStackTrace = log.isWarnEnabled() ? new Exception("Stack Trace") : null; + } + + /** + * Retrieve the userID from the specified subject. + * + * @return the userID. + */ + protected String retrieveUserId(Subject subject, String workspaceName) throws RepositoryException { + return repositoryContext.getSecurityManager().getUserID( + subject, workspaceName); + } + + /** + * Create the session item state manager. + * + * @return session item state manager + */ + protected SessionItemStateManager createSessionItemStateManager() { + SessionItemStateManager mgr = new SessionItemStateManager( + context.getRootNodeId(), + context.getWorkspace().getItemStateManager()); + context.getWorkspace().getItemStateManager().addListener(mgr); + return mgr; + } + + /** + * Create the item manager. + * @return item manager + */ + protected ItemManager createItemManager() { + ItemManager mgr = new ItemManager(context); + context.getItemStateManager().addListener(mgr); + return mgr; + } + + protected ObservationManagerImpl createObservationManager(String wspName) + throws RepositoryException { + try { + return new ObservationManagerImpl( + context.getRepository().getObservationDispatcher(wspName), + this, context.getRepositoryContext().getClusterNode()); + } catch (NoSuchWorkspaceException e) { + // should never get here + throw new RepositoryException( + "Internal error: failed to create observation manager", e); + } + } + /** + * Create the version manager. If we are not using XA, we may safely use + * the repository version manager. + * @return version manager + */ + protected InternalVersionManager createVersionManager() + throws RepositoryException { + return context.getRepositoryContext().getInternalVersionManager(); + } + + /** + * Create the access manager. + * + * @param subject + * @return access manager + * @throws AccessDeniedException if the current subject is not granted access + * to the current workspace + * @throws RepositoryException if the access manager cannot be instantiated + */ + protected AccessManager createAccessManager(Subject subject) + throws AccessDeniedException, RepositoryException { + String wspName = getWorkspace().getName(); + AMContext ctx = new AMContext( + new File(context.getRepository().getConfig().getHomeDir()), + context.getRepositoryContext().getFileSystem(), + this, + subject, + context.getHierarchyManager(), + context.getPrivilegeManager(), + this, + wspName); + return repositoryContext.getSecurityManager().getAccessManager(this, ctx); + } + + private T perform(SessionOperation operation) + throws RepositoryException { + return context.getSessionState().perform(operation); + } + + /** + * Performs a sanity check on this session. + * + * @throws RepositoryException if this session has been rendered invalid + * for some reason (e.g. if this session has + * been closed explicitly or if it has expired) + */ + private void sanityCheck() throws RepositoryException { + context.getSessionState().checkAlive(); + } + + /** + * Returns a read only copy of the Subject associated with this + * session. + * + * @return a read only copy of Subject associated with this session + */ + public Subject getSubject() { + Subject readOnly = new Subject(true, subject.getPrincipals(), subject.getPublicCredentials(), subject.getPrivateCredentials()); + return readOnly; + } + + /** + * Returns true if the subject contains a + * SystemPrincipal; false otherwise. + * + * @return true if this is an system session. + */ + public boolean isSystem() { + // NOTE: for backwards compatibility evaluate subject for containing SystemPrincipal + // TODO: Q: shouldn't 'isSystem' rather be covered by instances of SystemSession only? + return (subject != null && !subject.getPrincipals(SystemPrincipal.class).isEmpty()); + } + + /** + * Returns true if this session has been created for the + * administrator. False otherwise. + * + * @return true if this is an admin session. + */ + public boolean isAdmin() { + // NOTE: don't replace by getUserManager() + if (userManager != null) { + try { + Authorizable a = userManager.getAuthorizable(userId); + if (a != null && !a.isGroup()) { + return ((User) a).isAdmin(); + } + } catch (RepositoryException e) { + // no user management -> use fallback + } + + } + // fallback: user manager not yet initialized or user mgt not supported + // -> check for AdminPrincipal being present in the subject. + return (subject != null && !subject.getPrincipals(AdminPrincipal.class).isEmpty()); + } + + /** + * Creates a new session with the same subject as this sessions but to a + * different workspace. The returned session is a newly logged in session, + * with the same subject but a different workspace. Even if the given + * workspace is the same as this sessions one, the implementation must + * return a new session object. + * + * @param workspaceName name of the workspace to acquire a session for. + * @return A session to the requested workspace for the same authenticated + * subject. + * @throws AccessDeniedException in case the current Subject is not allowed + * to access the requested Workspace + * @throws NoSuchWorkspaceException If the named workspace does not exist. + * @throws RepositoryException in any other exceptional state + */ + public Session createSession(String workspaceName) + throws AccessDeniedException, NoSuchWorkspaceException, RepositoryException { + if (workspaceName == null) { + workspaceName = + repositoryContext.getWorkspaceManager().getDefaultWorkspaceName(); + } + Subject newSubject = new Subject(subject.isReadOnly(), subject.getPrincipals(), subject.getPublicCredentials(), subject.getPrivateCredentials()); + return repositoryContext.getWorkspaceManager().createSession( + newSubject, workspaceName); + } + + /** + * Returns the AccessManager associated with this session. + * + * @return the AccessManager associated with this session + */ + public AccessManager getAccessManager() { + return context.getAccessManager(); + } + + /** + * Returns the NodeTypeManager. + * + * @return the NodeTypeManager + */ + public NodeTypeManagerImpl getNodeTypeManager() { + return context.getNodeTypeManager(); + } + + /** + * Returns the ItemManager of this session. + * + * @return the ItemManager + */ + public ItemManager getItemManager() { + return context.getItemManager(); + } + + /** + * Returns the HierarchyManager associated with this session. + * + * @return the HierarchyManager associated with this session + */ + public HierarchyManager getHierarchyManager() { + return context.getHierarchyManager(); + } + + /** + * Returns the InternalVersionManager associated with this session. + * + * @return the InternalVersionManager associated with this session + */ + public InternalVersionManager getInternalVersionManager() { + return versionMgr; + } + + + /** + * Returns the internal retention manager used for evaluation of effective + * retention policies and holds. + * + * @return internal retention manager + * @throws RepositoryException + */ + protected RetentionRegistry getRetentionRegistry() throws RepositoryException { + return context.getWorkspace().getRetentionRegistry(); + } + + /** + * Sets the named attribute. If the value is null, then + * the named attribute is removed. + * + * @see JCR-1932 + * @param name attribute name + * @param value attribute value + * @since Apache Jackrabbit 1.6 + */ + public void setAttribute(String name, Object value) { + if (value != null) { + attributes.put(name, value); + } else { + attributes.remove(name); + } + } + + /** + * Retrieves the Node with the given id. + * + * @param id id of node to be retrieved + * @return node with the given NodeId. + * @throws ItemNotFoundException if no such node exists or if this + * Session does not have permission to access the node. + * @throws RepositoryException if another error occurs. + */ + public NodeImpl getNodeById(NodeId id) throws ItemNotFoundException, RepositoryException { + // check sanity of this session + sanityCheck(); + + try { + return (NodeImpl) getItemManager().getItem(id); + } catch (AccessDeniedException ade) { + throw new ItemNotFoundException(id.toString()); + } + } + + /** + * Notify the listeners that this session is about to be closed. + */ + protected void notifyLoggingOut() { + // copy listeners to array to avoid ConcurrentModificationException + List copy = + new ArrayList(listeners.values()); + for (SessionListener listener : copy) { + if (listener != null) { + listener.loggingOut(this); + } + } + } + + /** + * Notify the listeners that this session has been closed. + */ + protected void notifyLoggedOut() { + // copy listeners to array to avoid ConcurrentModificationException + List copy = + new ArrayList(listeners.values()); + for (SessionListener listener : copy) { + if (listener != null) { + listener.loggedOut(this); + } + } + } + + /** + * Add a SessionListener + * + * @param listener the new listener to be informed on modifications + */ + public void addListener(SessionListener listener) { + if (!listeners.containsKey(listener)) { + listeners.put(listener, listener); + } + } + + /** + * Remove a SessionListener + * + * @param listener an existing listener + */ + public void removeListener(SessionListener listener) { + listeners.remove(listener); + } + + /** + * Create a data store garbage collector for this repository. + * + * @throws RepositoryException + */ + public GarbageCollector createDataStoreGarbageCollector() throws RepositoryException { + final GarbageCollector gc = + repositoryContext.getRepository().createDataStoreGarbageCollector(); + // Auto-close if the main session logs out + addListener(new SessionListener() { + public void loggedOut(SessionImpl session) { + } + public void loggingOut(SessionImpl session) { + gc.close(); + } + }); + return gc; + } + + //---------------------------------------------------< NamespaceResolver > + + public String getPrefix(String uri) throws NamespaceException { + try { + return getNamespacePrefix(uri); + } catch (NamespaceException e) { + throw e; + } catch (RepositoryException e) { + throw new NamespaceException("Namespace not found: " + uri, e); + } + } + + public String getURI(String prefix) throws NamespaceException { + try { + return getNamespaceURI(prefix); + } catch (NamespaceException e) { + throw e; + } catch (RepositoryException e) { + throw new NamespaceException("Namespace not found: " + prefix, e); + } + } + + //--------------------------------------------------------< NameResolver > + + public String getJCRName(Name name) throws NamespaceException { + return namePathResolver.getJCRName(name); + } + + public Name getQName(String name) throws IllegalNameException, NamespaceException { + return namePathResolver.getQName(name); + } + + //--------------------------------------------------------< PathResolver > + + public String getJCRPath(Path path) throws NamespaceException { + return namePathResolver.getJCRPath(path); + } + + public Path getQPath(String path) throws MalformedPathException, IllegalNameException, NamespaceException { + return namePathResolver.getQPath(path); + } + + public Path getQPath(String path, boolean normalizeIdentifier) throws MalformedPathException, IllegalNameException, NamespaceException { + return namePathResolver.getQPath(path, normalizeIdentifier); + } + + //---------------------------------------------------< IdentifierResolver > + /** + * @see IdentifierResolver#getPath(String) + */ + public Path getPath(String identifier) throws MalformedPathException { + try { + return context.getHierarchyManager().getPath(NodeId.valueOf(identifier)); + } catch (RepositoryException e) { + throw new MalformedPathException("Identifier '" + identifier + "' cannot be resolved."); + } + } + + /** + * @see IdentifierResolver#checkFormat(String) + */ + public void checkFormat(String identifier) throws MalformedPathException { + try { + NodeId.valueOf(identifier); + } catch (IllegalArgumentException e) { + throw new MalformedPathException("Invalid identifier: " + identifier); + } + } + + //----------------------------------------------------< JackrabbitSession > + /** + * @see JackrabbitSession#hasPermission(String, String...) + */ + @Override + public boolean hasPermission(String absPath, String... actions) throws RepositoryException { + return hasPermission(absPath, Text.implode(actions, ",")); + } + + /** + * @see JackrabbitSession#getPrincipalManager() + */ + public PrincipalManager getPrincipalManager() throws RepositoryException, AccessDeniedException { + if (principalManager == null) { + principalManager = + repositoryContext.getSecurityManager().getPrincipalManager(this); + } + return principalManager; + } + + /** + * @see JackrabbitSession#getUserManager() + */ + public UserManager getUserManager() throws AccessDeniedException, RepositoryException { + if (userManager == null) { + userManager = + repositoryContext.getSecurityManager().getUserManager(this); + } + return userManager; + } + + @Override + public Item getItemOrNull(String absPath) throws RepositoryException { + // TODO optimise, reduce to a single read operation + if (itemExists(absPath)) { + return getItem(absPath); + } else { + return null; + } + } + + @Override + public Property getPropertyOrNull(String absPath) throws RepositoryException { + // TODO optimise, reduce to a single read operation + if (propertyExists(absPath)) { + return getProperty(absPath); + } else { + return null; + } + } + + @Override + public Node getNodeOrNull(String absPath) throws RepositoryException { + // TODO optimise, reduce to a single read operation + if (nodeExists(absPath)) { + return getNode(absPath); + } else { + return null; + } + } + + //--------------------------------------------------------------< Session > + /** + * {@inheritDoc} + */ + public void checkPermission(String absPath, String actions) + throws AccessControlException, RepositoryException { + if (!hasPermission(absPath, actions)) { + throw new AccessControlException(actions); + } + } + + /** + * {@inheritDoc} + */ + public Workspace getWorkspace() { + return context.getWorkspace(); + } + + /** + * {@inheritDoc} + */ + @Override + public Session impersonate(Credentials otherCredentials) + throws LoginException, RepositoryException { + // check sanity of this session + sanityCheck(); + + if (!(otherCredentials instanceof SimpleCredentials)) { + String msg = "impersonate failed: incompatible credentials, SimpleCredentials expected"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // set IMPERSONATOR_ATTRIBUTE attribute of given credentials + // with subject of current session + SimpleCredentials creds = (SimpleCredentials) otherCredentials; + creds.setAttribute(SecurityConstants.IMPERSONATOR_ATTRIBUTE, subject); + + try { + return getRepository().login( + otherCredentials, getWorkspace().getName()); + } catch (NoSuchWorkspaceException nswe) { + // should never get here... + String msg = "impersonate failed"; + log.error(msg, nswe); + throw new RepositoryException(msg, nswe); + } finally { + // make sure IMPERSONATOR_ATTRIBUTE is removed + creds.removeAttribute(SecurityConstants.IMPERSONATOR_ATTRIBUTE); + } + } + + /** + * {@inheritDoc} + */ + public Node getRootNode() throws RepositoryException { + // check sanity of this session + sanityCheck(); + + return getItemManager().getRootNode(); + } + + /** + * {@inheritDoc} + */ + public Node getNodeByUUID(String uuid) throws ItemNotFoundException, RepositoryException { + try { + NodeImpl node = getNodeById(new NodeId(uuid)); + if (node.isNodeType(NameConstants.MIX_REFERENCEABLE)) { + return node; + } else { + // there is a node with that uuid but the node does not expose it + throw new ItemNotFoundException(uuid); + } + } catch (IllegalArgumentException e) { + // Assuming the exception is from UUID.fromString() + throw new RepositoryException("Invalid UUID: " + uuid, e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Item getItem(String absPath) throws RepositoryException { + return perform(SessionItemOperation.getItem(absPath)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean itemExists(String absPath) throws RepositoryException { + if (absPath != null && absPath.startsWith("[") && absPath.endsWith("]")) { + // an identifier segment has been specified (JCR-3014) + try { + NodeId id = NodeId.valueOf(absPath.substring(1, absPath.length() - 1)); + return getItemManager().itemExists(id); + } catch (IllegalArgumentException e) { + throw new MalformedPathException(absPath); + } + } + return perform(SessionItemOperation.itemExists(absPath)); + } + + /** + * {@inheritDoc} + */ + public void save() throws RepositoryException { + // JCR-3131: no need to perform save op when there's nothing to save... + if (context.getItemStateManager().hasAnyTransientItemStates()) { + perform(new SessionSaveOperation()); + } + } + + /** + * {@inheritDoc} + */ + public void refresh(boolean keepChanges) throws RepositoryException { + perform(new SessionRefreshOperation( + keepChanges, clusterSyncOnRefresh())); + } + + /** + * Checks whether the {@link #refresh(boolean)} method should cause + * cluster synchronization. + *

    + * Subclasses can override this method to implement alternative + * rules on when cluster synchronization should be done. + * + * @return true if the {@link #DISABLE_CLUSTER_SYNC_ON_REFRESH} + * attribute is not set, false otherwise + * @since Apache Jackrabbit 1.6 + * @see JCR-1753 + */ + protected boolean clusterSyncOnRefresh() { + return getAttribute(DISABLE_CLUSTER_SYNC_ON_REFRESH) == null; + } + + /** + * Checks whether repository inconsistencies should be automatically fixed + * when traversing over child nodes, when trying to add a child node, or + * when removing a child node. + * + * @return true if the {@link #AUTO_FIX_CORRUPTIONS} + * attribute is set, false otherwise + * @since Apache Jackrabbit 2.2 + * @see JCR-2740 + */ + protected boolean autoFixCorruptions() { + return getAttribute(AUTO_FIX_CORRUPTIONS) != null; + } + + /** + * {@inheritDoc} + */ + public boolean hasPendingChanges() throws RepositoryException { + // check sanity of this session + sanityCheck(); + + return context.getItemStateManager().hasAnyTransientItemStates(); + } + + /** + * {@inheritDoc} + */ + public void move(String srcAbsPath, String destAbsPath) + throws RepositoryException { + perform(new SessionMoveOperation(this, srcAbsPath, destAbsPath)); + } + + /** + * {@inheritDoc} + */ + public ContentHandler getImportContentHandler(String parentAbsPath, + int uuidBehavior) + throws PathNotFoundException, ConstraintViolationException, + VersionException, LockException, RepositoryException { + // check sanity of this session + sanityCheck(); + + NodeImpl parent; + try { + Path p = getQPath(parentAbsPath).getNormalizedPath(); + if (!p.isAbsolute()) { + throw new RepositoryException("not an absolute path: " + parentAbsPath); + } + parent = getItemManager().getNode(p); + } catch (NameException e) { + String msg = parentAbsPath + ": invalid path"; + log.debug(msg); + throw new RepositoryException(msg, e); + } catch (AccessDeniedException ade) { + throw new PathNotFoundException(parentAbsPath); + } + + // verify that parent node is checked-out, not locked and not protected + // by either node type constraints nor by some retention or hold. + int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT | + ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD | ItemValidator.CHECK_RETENTION; + context.getItemValidator().checkModify(parent, options, Permission.NONE); + + SessionImporter importer = new SessionImporter( + parent, this, uuidBehavior, + context.getWorkspace().getConfig().getImportConfig()); + return new ImportHandler(importer, this); + } + + /** + * {@inheritDoc} + */ + public boolean isLive() { + return context.getSessionState().isAlive(); + } + + /** + * Utility method that removes all registered event listeners. + */ + private void removeRegisteredEventListeners() { + try { + ObservationManager manager = getWorkspace().getObservationManager(); + // Use a copy to avoid modifying the set of registered listeners + // while iterating over it + Collection listeners = + IteratorUtils.toList(manager.getRegisteredEventListeners()); + for (EventListener listener : listeners) { + try { + manager.removeEventListener(listener); + } catch (RepositoryException e) { + log.warn("Error removing event listener: " + listener, e); + } + } + } catch (RepositoryException e) { + log.warn("Error removing event listeners", e); + } + } + + /** + * Invalidates this session and releases all associated resources. + */ + @Override + public void logout() { + if (context.getSessionState().close()) { + // JCR-798: Remove all registered event listeners to avoid concurrent + // access to session internals by the event delivery or even listeners + removeRegisteredEventListeners(); + + // discard any pending changes first as those might + // interfere with subsequent operations + context.getItemStateManager().disposeAllTransientItemStates(); + + // notify listeners that session is about to be closed + notifyLoggingOut(); + + context.getPrivilegeManager().dispose(); + context.getNodeTypeManager().dispose(); + // dispose session item state manager + context.getItemStateManager().dispose(); + // dispose item manager + context.getItemManager().dispose(); + // dispose workspace + context.getWorkspace().dispose(); + + // logout JAAS subject + if (loginContext != null) { + try { + loginContext.logout(); + } catch (javax.security.auth.login.LoginException le) { + log.warn("failed to logout current subject: " + le.getMessage()); + } + loginContext = null; + } + + try { + context.getAccessManager().close(); + } catch (Exception e) { + log.warn("error while closing AccessManager", e); + } + + // finally notify listeners that session has been closed + notifyLoggedOut(); + } + } + + + /** + * {@inheritDoc} + */ + public Repository getRepository() { + return repositoryContext.getRepository(); + } + + /** + * {@inheritDoc} + */ + public ValueFactory getValueFactory() { + return context.getValueFactory(); + } + + /** + * {@inheritDoc} + */ + public String getUserID() { + return userId; + } + + /** + * {@inheritDoc} + */ + public Object getAttribute(String name) { + return attributes.get(name); + } + + /** + * {@inheritDoc} + */ + public String[] getAttributeNames() { + return attributes.keySet().toArray(new String[attributes.size()]); + } + + /** + * {@inheritDoc} + */ + @Override + public void setNamespacePrefix(String prefix, String uri) + throws NamespaceException, RepositoryException { + super.setNamespacePrefix(prefix, uri); + // Clear name and path caches + namePathResolver = new DefaultNamePathResolver(this, true); + } + + + //------------------------------------------------------< locking support > + /** + * {@inheritDoc} + */ + public void addLockToken(String lt) { + try { + getWorkspace().getLockManager().addLockToken(lt); + } catch (RepositoryException e) { + log.debug("Error while adding lock token."); + } + } + + /** + * {@inheritDoc} + */ + public String[] getLockTokens() { + try { + return getWorkspace().getLockManager().getLockTokens(); + } catch (RepositoryException e) { + log.debug("Error while accessing lock tokens."); + return new String[0]; + } + } + + /** + * {@inheritDoc} + */ + public void removeLockToken(String lt) { + try { + getWorkspace().getLockManager().removeLockToken(lt); + } catch (RepositoryException e) { + log.debug("Error while removing lock token."); + } + } + + /** + * Returns all locks owned by this session. + * + * @return an array of Locks + */ + public Lock[] getLocks() { + // check sanity of this session + //sanityCheck(); + if (!isLive()) { + log.error("failed to retrieve locks: session has been closed"); + return new Lock[0]; + } + + try { + return context.getWorkspace().getInternalLockManager().getLocks(this); + } catch (RepositoryException e) { + log.error("Lock manager not available.", e); + return new Lock[0]; + } + } + + //--------------------------------------------------< new JSR 283 methods > + /** + * @see javax.jcr.Session#getNodeByIdentifier(String) + * @since JCR 2.0 + */ + public Node getNodeByIdentifier(String id) + throws ItemNotFoundException, RepositoryException { + NodeId nodeId; + try { + nodeId = NodeId.valueOf(id); + } catch (IllegalArgumentException iae) { + throw new RepositoryException("invalid identifier: " + id,iae); + } + return getNodeById(nodeId); + } + + /** + * @see javax.jcr.Session#getNode(String) + * @since JCR 2.0 + */ + @Override + public Node getNode(String absPath) throws RepositoryException { + return perform(SessionItemOperation.getNode(absPath)); + } + + /** + * @see javax.jcr.Session#getProperty(String) + * @since JCR 2.0 + */ + @Override + public Property getProperty(String absPath) throws RepositoryException { + return perform(SessionItemOperation.getProperty(absPath)); + } + + /** + * @see javax.jcr.Session#nodeExists(String) + * @since JCR 2.0 + */ + @Override + public boolean nodeExists(String absPath) throws RepositoryException { + if (absPath != null && absPath.startsWith("[") && absPath.endsWith("]")) { + // an identifier segment has been specified (JCR-3014) + try { + NodeId id = NodeId.valueOf(absPath.substring(1, absPath.length() - 1)); + return getItemManager().itemExists(id); + } catch (IllegalArgumentException e) { + throw new MalformedPathException(absPath); + } + } + return perform(SessionItemOperation.nodeExists(absPath)); + } + + /** + * @see javax.jcr.Session#propertyExists(String) + * @since JCR 2.0 + */ + @Override + public boolean propertyExists(String absPath) throws RepositoryException { + return perform(SessionItemOperation.propertyExists(absPath)); + } + + /** + * @see javax.jcr.Session#removeItem(String) + * @since JCR 2.0 + */ + @Override + public void removeItem(String absPath) throws RepositoryException { + perform(SessionItemOperation.remove(absPath)); + } + + /** + * @see javax.jcr.Session#hasPermission(String, String) + * @since 2.0 + */ + public boolean hasPermission(String absPath, String actions) throws RepositoryException { + // check sanity of this session + sanityCheck(); + Path path = getQPath(absPath).getNormalizedPath(); + // test if path is absolute + if (!path.isAbsolute()) { + throw new RepositoryException("Absolute path expected. Was:" + absPath); + } + + Set s = new HashSet(Arrays.asList(actions.split(","))); + int permissions = 0; + if (s.remove(ACTION_READ)) { + permissions |= Permission.READ; + } + if (s.remove(ACTION_ADD_NODE)) { + permissions |= Permission.ADD_NODE; + } + if (s.remove(ACTION_SET_PROPERTY)) { + permissions |= Permission.SET_PROPERTY; + } + if (s.remove(ACTION_REMOVE)) { + if (nodeExists(absPath)) { + permissions |= (propertyExists(absPath)) ? + (Permission.REMOVE_NODE | Permission.REMOVE_PROPERTY) : + Permission.REMOVE_NODE; + } else if (propertyExists(absPath)) { + permissions |= Permission.REMOVE_PROPERTY; + } else { + // item doesn't exist -> check both permissions + permissions = Permission.REMOVE_NODE | Permission.REMOVE_PROPERTY; + } + } + if (!s.isEmpty()) { + throw new IllegalArgumentException("Unknown actions: " + s); + } + try { + return context.getAccessManager().isGranted(path, permissions); + } catch (AccessDeniedException e) { + return false; + } + } + + /** + * @see javax.jcr.Session#hasCapability(String, Object, Object[]) + * @since JCR 2.0 + */ + public boolean hasCapability(String methodName, Object target, Object[] arguments) + throws RepositoryException { + // value of this method (as currently spec'ed) to jcr api clients + // is rather limited... + + // here's therefore a minimal rather than best effort implementation; + // returning true is always fine according to the spec... + ItemValidator validator = context.getItemValidator(); + int options = + CHECK_CHECKED_OUT | CHECK_LOCK | CHECK_CONSTRAINTS + | CHECK_HOLD | CHECK_RETENTION; + if (target instanceof Node) { + if (methodName.equals("addNode") + || methodName.equals("addMixin") + || methodName.equals("orderBefore") + || methodName.equals("removeMixin") + || methodName.equals("removeShare") + || methodName.equals("removeSharedSet") + || methodName.equals("setPrimaryType") + || methodName.equals("setProperty") + || methodName.equals("update")) { + return validator.canModify((ItemImpl) target, options, Permission.NONE); + } else if (methodName.equals("remove")) { + try { + validator.checkRemove((ItemImpl) target, options, Permission.NONE); + } catch (RepositoryException e) { + return false; + } + } + } else if (target instanceof Property) { + if (methodName.equals("setValue") + || methodName.equals("save")) { + return validator.canModify((ItemImpl) target, options, Permission.NONE); + } else if (methodName.equals("remove")) { + try { + validator.checkRemove((ItemImpl) target, options, Permission.NONE); + } catch (RepositoryException e) { + return false; + } + } +// TODO: Add minimal, best effort checks for Workspace and Session operations +// } else if (target instanceof Workspace) { +// if (methodName.equals("clone") +// || methodName.equals("copy") +// || methodName.equals("createWorkspace") +// || methodName.equals("deleteWorkspace") +// || methodName.equals("getImportContentHandler") +// || methodName.equals("importXML") +// || methodName.equals("move")) { +// // TODO minimal, best effort checks (e.g. permissions for write methods etc) +// } +// } else if (target instanceof Session) { +// if (methodName.equals("clone") +// || methodName.equals("removeItem") +// || methodName.equals("getImportContentHandler") +// || methodName.equals("importXML") +// || methodName.equals("save")) { +// // TODO minimal, best effort checks (e.g. permissions for write methods etc) +// } + } + + // we're unable to evaluate capability, return true (staying on the safe side) + return true; + } + + /** + * @see javax.jcr.Session#getAccessControlManager() + * @since JCR 2.0 + */ + public AccessControlManager getAccessControlManager() + throws UnsupportedRepositoryOperationException, RepositoryException { + AccessManager accessMgr = context.getAccessManager(); + if (accessMgr instanceof AccessControlManager) { + return (AccessControlManager) accessMgr; + } else { + throw new UnsupportedRepositoryOperationException( + "Access control discovery is not supported."); + } + } + + /** + * @see javax.jcr.Session#getRetentionManager() + * @since JCR 2.0 + */ + public RetentionManager getRetentionManager() + throws UnsupportedRepositoryOperationException, RepositoryException { + // check sanity of this session + sanityCheck(); + if (retentionManager == null) { + // make sure the internal retention manager exists. + getRetentionRegistry(); + // create the api level retention manager. + retentionManager = new RetentionManagerImpl(this); + } + return retentionManager; + } + + //--------------------------------------------------------------< Object > + + /** + * Returns the unique internal name of this session. The returned name + * is especially useful for debugging and logging purposes. + * + * @see #sessionName + * @return session name + */ + @Override + public String toString() { + return sessionName; + } + + /** + * Finalize the session. If the application doesn't call Session.logout(), + * the session is closed automatically; however a warning is written to the log file, + * together with the stack trace of where the session was opened. + */ + @Override + public void finalize() { + if (isLive()) { + if (openStackTrace != null) { + // Log a warning if and only if openStackTrace is not null + // indicating that the warn level is enabled and the session has + // been fully created + log.warn("Unclosed session detected. The session was opened here: ", openStackTrace); + } + logout(); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionListener.java new file mode 100644 index 00000000000..8c6ad01eb84 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionListener.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.core; + +/** + * The SessionListener interface allows an implementing + * object to be informed about changes on a Session. + * + * @see SessionImpl#addListener + */ +public interface SessionListener { + + /** + * Called when a Session is about to be 'closed' by + * calling {@link javax.jcr.Session#logout()}. At this + * moment the session is still valid. + * + * @param session the Session that is about to be 'closed' + */ + void loggingOut(SessionImpl session); + + /** + * Called when a Session has been 'closed' by + * calling {@link javax.jcr.Session#logout()}. + * + * @param session the Session that has been 'closed' + */ + void loggedOut(SessionImpl session); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionMoveOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionMoveOperation.java new file mode 100644 index 00000000000..314ca02130c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SessionMoveOperation.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.core; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionWriteOperation; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SessionMoveOperation implements SessionWriteOperation { + + private final Logger log = + LoggerFactory.getLogger(SessionMoveOperation.class); + + private final String srcAbsPath; + + private final Path srcPath; + + private final String destAbsPath; + + private final Path destPath; + + public SessionMoveOperation( + PathResolver resolver, String srcAbsPath, String destAbsPath) + throws RepositoryException { + this.srcAbsPath = srcAbsPath; + this.srcPath = getAbsolutePath(resolver, srcAbsPath); + + this.destAbsPath = destAbsPath; + this.destPath = getAbsolutePath(resolver, destAbsPath); + if (destPath.getIndex() != Path.INDEX_UNDEFINED) { + // subscript in name element + String msg = destAbsPath + ": invalid destination path (subscript in name element is not allowed)"; + log.debug(msg); + throw new RepositoryException(msg); + } + + + if (srcPath.isAncestorOf(destPath)) { + throw new RepositoryException( + "Destination path " + destAbsPath + + " cannot be descendant of source path " + srcAbsPath + + " in a move operation."); + } + } + + private Path getAbsolutePath(PathResolver resolver, String path) + throws RepositoryException { + try { + Path qpath = resolver.getQPath(path).getNormalizedPath(); + if (!qpath.isAbsolute()) { + throw new RepositoryException("Path is not absolute: " + path); + } + return qpath; + } catch (NameException e) { + throw new RepositoryException("Path is invalid: " + path, e); + } + } + + private NodeImpl getNode( + SessionContext context, Path path, String absPath) + throws RepositoryException { + try { + return context.getItemManager().getNode(path); + } catch (AccessDeniedException e) { + throw new PathNotFoundException("Path not found: " + absPath); + } + } + + public Object perform(SessionContext context) throws RepositoryException { + // Get node instances + NodeImpl targetNode = getNode(context, srcPath, srcAbsPath); + NodeImpl srcParentNode = + getNode(context, srcPath.getAncestor(1), srcAbsPath); + NodeImpl destParentNode = + getNode(context, destPath.getAncestor(1), destAbsPath); + + if (context.getHierarchyManager().isShareAncestor( + targetNode.getNodeId(), destParentNode.getNodeId())) { + throw new RepositoryException( + "Move not possible because of a share cycle between " + + srcAbsPath + " and " + destAbsPath); + } + + // check for name collisions + NodeImpl existing = null; + try { + existing = context.getItemManager().getNode(destPath); + // there's already a node with that name: + // check same-name sibling setting of existing node + if (!existing.getDefinition().allowsSameNameSiblings()) { + throw new ItemExistsException( + "Same name siblings are not allowed: " + existing); + } + } catch (AccessDeniedException ade) { + // FIXME by throwing ItemExistsException we're disclosing too much information + throw new ItemExistsException(destAbsPath); + } catch (PathNotFoundException pnfe) { + // no name collision, fall through + } + + // verify that the targetNode can be removed + int options = ItemValidator.CHECK_HOLD | ItemValidator.CHECK_RETENTION; + context.getItemValidator().checkRemove(targetNode, options, Permission.NONE); + + // verify for both source and destination parent nodes that + // - they are checked-out + // - are not protected neither by node type constraints nor by retention/hold + options = ItemValidator.CHECK_CHECKED_OUT | ItemValidator.CHECK_LOCK | + ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD | ItemValidator.CHECK_RETENTION; + context.getItemValidator().checkModify(srcParentNode, options, Permission.NONE); + context.getItemValidator().checkModify(destParentNode, options, Permission.NONE); + + // check constraints + // get applicable definition of target node at new location + NodeTypeImpl nt = (NodeTypeImpl) targetNode.getPrimaryNodeType(); + org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl newTargetDef; + try { + newTargetDef = destParentNode.getApplicableChildNodeDefinition(destPath.getName(), nt.getQName()); + } catch (RepositoryException re) { + String msg = destAbsPath + ": no definition found in parent node's node type for new node"; + log.debug(msg); + throw new ConstraintViolationException(msg, re); + } + // if there's already a node with that name also check same-name sibling + // setting of new node; just checking same-name sibling setting on + // existing node is not sufficient since same-name sibling nodes don't + // necessarily have identical definitions + if (existing != null && !newTargetDef.allowsSameNameSiblings()) { + throw new ItemExistsException( + "Same name siblings not allowed: " + existing); + } + + NodeId targetId = targetNode.getNodeId(); + + // check permissions + AccessManager acMgr = context.getAccessManager(); + if (!(acMgr.isGranted(srcPath, Permission.REMOVE_NODE) && + acMgr.isGranted(destPath, Permission.ADD_NODE | Permission.NODE_TYPE_MNGMT))) { + String msg = "Not allowed to move node " + srcAbsPath + " to " + destAbsPath; + log.debug(msg); + throw new AccessDeniedException(msg); + } + + if (srcParentNode.isSame(destParentNode)) { + // change definition of target + targetNode.onRedefine(newTargetDef.unwrap()); + // do rename + destParentNode.renameChildNode(targetId, destPath.getName(), false); + } else { + // check shareable case + if (targetNode.getNodeState().isShareable()) { + String msg = "Moving a shareable node is not supported."; + log.debug(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + // change definition of target + targetNode.onRedefine(newTargetDef.unwrap()); + + // Get the transient states + NodeState srcParentState = + (NodeState) srcParentNode.getOrCreateTransientItemState(); + NodeState targetState = + (NodeState) targetNode.getOrCreateTransientItemState(); + NodeState destParentState = + (NodeState) destParentNode.getOrCreateTransientItemState(); + + // do move: + // 1. remove child node entry from old parent + if (srcParentState.removeChildNodeEntry(targetId)) { + // 2. re-parent target node + targetState.setParentId(destParentNode.getNodeId()); + // 3. add child node entry to new parent + destParentState.addChildNodeEntry(destPath.getName(), targetId); + } + } + + return this; + } + + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "session.move(" + srcAbsPath + ", " + destAbsPath + ")"; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SystemSession.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SystemSession.java new file mode 100644 index 00000000000..b181d814f94 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/SystemSession.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.Collections; +import java.util.Set; +import java.security.Principal; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.core.config.WorkspaceConfig; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.security.AMContext; +import org.apache.jackrabbit.core.security.AbstractAccessControlManager; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.SystemPrincipal; +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * A SystemSession ... + */ +class SystemSession extends SessionImpl { + + /** + * Package private factory method + * + * @param repositoryContext The repository context + * @param wspConfig The workspace configuration + * @return A new instance of SystemSession + * @throws RepositoryException If an error occurs + */ + static SystemSession create( + RepositoryContext repositoryContext, WorkspaceConfig wspConfig) + throws RepositoryException { + // create subject with SystemPrincipal + Set principals = Collections.singleton(new SystemPrincipal()); + Subject subject = new Subject(true, principals, Collections.emptySet(), Collections.emptySet()); + return new SystemSession(repositoryContext, subject, wspConfig); + } + + /** + * private constructor + * + * @param repositoryContext repository context + * @param subject The subject + * @param wspConfig The workspace configuration + * @throws javax.jcr.RepositoryException If an error occurs. + */ + private SystemSession( + RepositoryContext repositoryContext, Subject subject, + WorkspaceConfig wspConfig) throws RepositoryException { + super(repositoryContext, subject, wspConfig); + } + + /** + * Always returns the name of the SystemPrincipal. + * + * @return the name of SystemPrincipal. + */ + @Override + protected String retrieveUserId(Subject subject, String workspaceName) throws RepositoryException { + return new SystemPrincipal().getName(); + } + + /** + * {@inheritDoc} + *

    + * Overridden in order to create custom access manager + * + * @return access manager for system session + */ + @Override + protected AccessManager createAccessManager(Subject subject) { + // use own AccessManager implementation rather than relying on + // configurable AccessManager to handle SystemPrincipal privileges + // correctly + return new SystemAccessManager(); + } + + /** + * Always returns true. + * + * @return true as this is an system session instance. + */ + @Override + public boolean isSystem() { + return true; + } + + /** + * Always returns false. + * + * @return false as this is an system session instance. + */ + @Override + public boolean isAdmin() { + return false; + } + + //--------------------------------------------------------< inner classes > + /** + * An access manager that grants access to everything. + */ + private class SystemAccessManager extends AbstractAccessControlManager implements AccessManager { + + SystemAccessManager() { + } + + //----------------------------------------------------< AccessManager > + /** + * {@inheritDoc} + * + * @throws AccessDeniedException is never thrown + * @throws Exception is never thrown + */ + public void init(AMContext context) + throws AccessDeniedException, Exception { + // nop + } + + public void init(AMContext context, AccessControlProvider acProvider, WorkspaceAccessManager wspAccessMgr) throws AccessDeniedException, Exception { + // nop + } + + /** + * {@inheritDoc} + */ + public void close() throws Exception { + // nop + } + + /** + * {@inheritDoc} + * + * @throws AccessDeniedException is never thrown + * @throws RepositoryException is never thrown + */ + public void checkPermission(ItemId id, int permissions) + throws AccessDeniedException, RepositoryException { + // allow everything + } + + /** + * {@inheritDoc} + */ + public void checkPermission(Path absPath, int permissions) throws AccessDeniedException, RepositoryException { + // allow everything + } + + /** + * {@inheritDoc} + */ + public void checkRepositoryPermission(int permissions) throws AccessDeniedException, RepositoryException { + // allow everything + } + + /** + * {@inheritDoc} + * + * @return always true + * @throws RepositoryException is never thrown + */ + public boolean isGranted(ItemId id, int permissions) throws RepositoryException { + // allow everything + return true; + } + + /** + * Always returns true. + * + * @see AccessManager#isGranted(Path, int) + */ + public boolean isGranted(Path absPath, int permissions) throws RepositoryException { + // allow everything + return true; + } + + /** + * Always returns true. + * + * @see AccessManager#isGranted(Path, Name, int) + */ + public boolean isGranted(Path parentPath, Name childName, int permissions) throws RepositoryException { + // allow everything + return true; + } + + /** + * Always returns true. + * + * @see AccessManager#canRead(org.apache.jackrabbit.spi.Path,org.apache.jackrabbit.core.id.ItemId) + */ + public boolean canRead(Path itemPath, ItemId itemId) throws RepositoryException { + return true; + } + + /** + * {@inheritDoc} + * + * @return always true + * @throws RepositoryException is never thrown + */ + public boolean canAccess(String workspaceName) throws RepositoryException { + return true; + } + + //-----------------------------------< AbstractAccessControlManager >--- + /** + * @see AbstractAccessControlManager#checkInitialized() + */ + @Override + protected void checkInitialized() throws IllegalStateException { + // nop + } + + /** + * @see AbstractAccessControlManager#checkPermission(String,int) + */ + @Override + protected void checkPermission(String absPath, int permission) throws + AccessDeniedException, PathNotFoundException, RepositoryException { + // allow everything + } + + /** + * @see AbstractAccessControlManager#getPrivilegeManager() + */ + @Override + protected PrivilegeManager getPrivilegeManager() throws RepositoryException { + return context.getPrivilegeManager(); + } + + /** + * @see AbstractAccessControlManager#checkValidNodePath(String) + */ + @Override + protected void checkValidNodePath(String absPath) + throws PathNotFoundException, RepositoryException { + if (absPath != null) { + Path p = getQPath(absPath); + if (!p.isAbsolute()) { + throw new RepositoryException("Absolute path expected."); + } + if (context.getHierarchyManager().resolveNodePath(p) == null) { + throw new PathNotFoundException("No such node " + absPath); + } + } + } + + //-------------------------------------------< AccessControlManager >--- + /** + * @see javax.jcr.security.AccessControlManager#hasPrivileges(String, Privilege[]) + */ + public boolean hasPrivileges(String absPath, Privilege[] privileges) + throws PathNotFoundException, RepositoryException { + checkValidNodePath(absPath); + // allow everything + return true; + } + + /** + * @see javax.jcr.security.AccessControlManager#getPrivileges(String) + */ + public Privilege[] getPrivileges(String absPath) + throws PathNotFoundException, RepositoryException { + checkValidNodePath(absPath); + return new Privilege[] {privilegeFromName(Privilege.JCR_ALL)}; + } + + /** + * @see javax.jcr.security.AccessControlManager#getEffectivePolicies(String) + */ + public AccessControlPolicy[] getEffectivePolicies(String absPath) throws + PathNotFoundException, AccessDeniedException, RepositoryException { + // cannot determine the effective policies for the system session. + return new AccessControlPolicy[0]; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getEffectivePolicies(Set) + */ + public AccessControlPolicy[] getEffectivePolicies(Set principal) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException { + // cannot determine the effective policies for the system session. + return new AccessControlPolicy[0]; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#hasPrivileges(String, Set, Privilege[]) + */ + public boolean hasPrivileges(String absPath, Set principals, Privilege[] privileges) throws PathNotFoundException, RepositoryException { + throw new UnsupportedOperationException("not implemented"); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getPrivileges(String, Set) + */ + public Privilege[] getPrivileges(String absPath, Set principals) throws PathNotFoundException, RepositoryException { + throw new UnsupportedOperationException("not implemented"); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/TestContentLoader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/TestContentLoader.java new file mode 100644 index 00000000000..3cf7f50db33 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/TestContentLoader.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.core; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +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.NodeType; +import javax.jcr.retention.RetentionPolicy; + +import org.apache.jackrabbit.api.JackrabbitNodeTypeManager; +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.retention.RetentionPolicyImpl; + +/** + * Test Content Loader. + */ +public class TestContentLoader { + + /** + * The encoding of the test resources. + */ + private static final Charset ENCODING = StandardCharsets.UTF_8; + + public void loadTestContent(Session session) throws RepositoryException, IOException { + JackrabbitWorkspace workspace = + (JackrabbitWorkspace) session.getWorkspace(); + Collection workspaces = + Arrays.asList(workspace.getAccessibleWorkspaceNames()); + if (!workspaces.contains("test")) { + workspace.createWorkspace("test"); + } + + JackrabbitNodeTypeManager manager = + (JackrabbitNodeTypeManager) workspace.getNodeTypeManager(); + if (!manager.hasNodeType("test:versionable")) { + InputStream xml = + TestContentLoader.class.getResourceAsStream("test-nodetypes.xml"); + try { + manager.registerNodeTypes(xml, JackrabbitNodeTypeManager.TEXT_XML); + } finally { + xml.close(); + } + } + + Node data = getOrAddNode(session.getRootNode(), "testdata"); + addPropertyTestData(getOrAddNode(data, "property")); + addQueryTestData(getOrAddNode(data, "query")); + addNodeTestData(getOrAddNode(data, "node")); + addLifecycleTestData(getOrAddNode(data, "lifecycle")); + addExportTestData(getOrAddNode(data, "docViewTest")); + + Node conf = getOrAddNode(session.getRootNode(), "testconf"); + addRetentionTestData(getOrAddNode(conf, "retentionTest")); + + session.save(); + } + + + + private 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 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" }); + } + + /** + * 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 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 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.name()); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty( + "jcr:data", + new ByteArrayInputStream("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(StandardCharsets.UTF_8))); + } + + /** + * 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 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 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.name()); + 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.name()); + resource.setProperty("jcr:mimeType", "text/plain"); + byte[] bytes = "Hello w\u00F6rld.".getBytes(ENCODING); + resource.setProperty(name, new ByteArrayInputStream(bytes)); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/TransientRepository.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/TransientRepository.java new file mode 100644 index 00000000000..8f60577353c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/TransientRepository.java @@ -0,0 +1,451 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.commons.AbstractRepository; +import org.apache.jackrabbit.core.config.ConfigurationException; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A repository proxy that automatically initializes and shuts down the + * underlying repository instance when the first session is opened + * or the last one closed. As long as all sessions are properly closed + * when no longer used, this class can be used to avoid having to explicitly + * shut down the repository. + */ +public class TransientRepository extends AbstractRepository + implements JackrabbitRepository, SessionListener { + + /** + * The logger instance used to log the repository and session lifecycles. + */ + private static final Logger logger = + LoggerFactory.getLogger(TransientRepository.class); + + /** + * Name of the repository configuration file property. + */ + private static final String CONF_PROPERTY = + "org.apache.jackrabbit.repository.conf"; + + /** + * Default value of the repository configuration file property. + */ + private static final String CONF_DEFAULT = "repository.xml"; + + /** + * Name of the repository home directory property. + */ + private static final String HOME_PROPERTY = + "org.apache.jackrabbit.repository.home"; + + /** + * Default value of the repository home directory property. + */ + private static final String HOME_DEFAULT = "repository"; + + /** + * Factory interface for creating {@link RepositoryImpl} instances. + * Used to give greater control of the repository initialization process + * to users of the TransientRepository class. + */ + public interface RepositoryFactory { + + /** + * Creates and initializes a repository instance. The returned instance + * will be used and finally shut down by the caller of this method. + * + * @return initialized repository instance + * @throws RepositoryException if an instance can not be created + */ + RepositoryImpl getRepository() throws RepositoryException; + + } + + /** + * The repository configuration. Set in the constructor and used to + * initialize the repository instance when the first session is opened. + */ + private final RepositoryFactory factory; + + /** + * The initialized repository instance. Set when the first session is + * opened and cleared when the last one is closed. + */ + private RepositoryImpl repository; + + /** + * The set of open sessions. When no more open sessions remain, the + * repository instance is automatically shut down until a new session + * is opened. + */ + private final Map sessions = + new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK); + + /** + * The static repository descriptors. The default {@link RepositoryImpl} + * descriptors are loaded as the static descriptors and used whenever a + * live repository instance is not available (no open sessions). + */ + private final Properties descriptors; + + /** + * The path to the repository home directory. + */ + private final String home; + + /** + * Creates a transient repository proxy that will use the given repository + * factory to initialize the underlying repository instances. + * + * @param factory repository factory + * @param home the path to the repository home directory. + */ + public TransientRepository(RepositoryFactory factory, String home) { + this.factory = factory; + this.home = home; + this.repository = null; + this.descriptors = new Properties(); + + // FIXME: The current RepositoryImpl class does not allow static + // access to the repository descriptors, so we need to load them + // directly from the underlying property file. + try { + InputStream in = RepositoryImpl.class.getResourceAsStream( + "repository.properties"); + try { + descriptors.load(in); + } finally { + in.close(); + } + } catch (IOException e) { + logger.warn("Unable to load static repository descriptors", e); + } + } + + /** + * Creates a transient repository proxy that will use the repository + * configuration file and home directory specified in system properties + * org.apache.jackrabbit.repository.conf and + * org.apache.jackrabbit.repository.home. If these properties + * are not found, then the default values "repository.xml" + * and "repository" are used. + */ + public TransientRepository() { + this(System.getProperty(CONF_PROPERTY, CONF_DEFAULT), + System.getProperty(HOME_PROPERTY, HOME_DEFAULT)); + } + + /** + * Creates a transient repository proxy that will use a copy of the given + * repository configuration to initialize the underlying repository + * instance. + * + * @param config repository configuration + */ + public TransientRepository(final RepositoryConfig config) { + this(new RepositoryFactory() { + public RepositoryImpl getRepository() throws RepositoryException { + return RepositoryImpl.create(RepositoryConfig.create(config)); + } + }, config.getHomeDir()); + } + + /** + * Creates a transient repository proxy that will use the given repository + * configuration file and home directory paths to initialize the underlying + * repository instances. + * + * @see #TransientRepository(File, File) + * @param config repository configuration file + * @param home repository home directory + */ + public TransientRepository(String config, String home) { + this(new File(config), new File(home)); + } + + /** + * Creates a transient repository proxy based on the given repository + * home directory and the repository configuration file "repository.xml" + * contained in that directory. + * + * @since Apache Jackrabbit 1.6 + * @param dir repository home directory + */ + public TransientRepository(File dir) { + this(new File(dir, "repository.xml"), dir); + } + + /** + * Creates a transient repository proxy that will use the given repository + * configuration file and home directory paths to initialize the underlying + * repository instances. The repository configuration file is reloaded + * whenever the repository is restarted, so it is safe to modify the + * configuration when all sessions have been closed. + *

    + * If the given repository configuration file does not exist, then a + * default configuration file is copied to the given location when the + * first session starts. Similarly, if the given repository home + * directory does not exist, it is automatically created when the first + * session starts. This is a convenience feature designed to reduce the + * need for manual configuration. + * + * @since Apache Jackrabbit 1.6 + * @param xml repository configuration file + * @param dir repository home directory + */ + public TransientRepository(final File xml, final File dir) { + this(new RepositoryFactory() { + public RepositoryImpl getRepository() throws RepositoryException { + try { + return RepositoryImpl.create( + RepositoryConfig.install(xml, dir)); + } catch (IOException e) { + throw new RepositoryException( + "Automatic repository configuration failed", e); + } catch (ConfigurationException e) { + throw new RepositoryException( + "Invalid repository configuration file: " + xml, e); + } + } + }, dir.getAbsolutePath()); + } + + public TransientRepository(final Properties properties) + throws ConfigurationException, IOException { + this(new RepositoryFactory() { + public RepositoryImpl getRepository() throws RepositoryException { + try { + return RepositoryImpl.create( + RepositoryConfig.install(properties)); + } catch (IOException e) { + throw new RepositoryException( + "Automatic repository configuration failed: " + + properties, e); + } catch (ConfigurationException e) { + throw new RepositoryException( + "Invalid repository configuration: " + + properties, e); + } + } + }, RepositoryConfig.getRepositoryHome(properties).getAbsolutePath()); + } + + /** + * @return the path to the repository home directory. + */ + public String getHomeDir() { + return home; + } + + /** + * Starts the underlying repository. + * + * @throws RepositoryException if the repository cannot be started + */ + private synchronized void startRepository() throws RepositoryException { + assert repository == null && sessions.isEmpty(); + logger.debug("Initializing transient repository"); + repository = factory.getRepository(); + logger.info("Transient repository initialized"); + } + + /** + * Stops the underlying repository. + */ + private synchronized void stopRepository() { + assert repository != null && sessions.isEmpty(); + logger.debug("Shutting down transient repository"); + repository.shutdown(); + logger.info("Transient repository shut down"); + repository = null; + } + + //------------------------------------------------------------ + + /** + * Returns the available descriptor keys. If the underlying repository + * is initialized, then the call is proxied to it, otherwise the static + * descriptor keys are returned. + * + * @return descriptor keys + */ + public synchronized String[] getDescriptorKeys() { + if (repository != null) { + return repository.getDescriptorKeys(); + } else { + String[] keys = Collections.list( + descriptors.propertyNames()).toArray(new String[0]); + Arrays.sort(keys); + return keys; + } + } + + /** + * Returns the identified repository descriptor. If the underlying + * repository is initialized, then the call is proxied to it, otherwise + * the static descriptors are used. + * + * @param key descriptor key + * @return descriptor value + * @see javax.jcr.Repository#getDescriptor(String) + */ + public synchronized String getDescriptor(String key) { + if (repository != null) { + return repository.getDescriptor(key); + } else { + return descriptors.getProperty(key); + } + } + + public Value getDescriptorValue(String key) { + if (repository != null) { + return repository.getDescriptorValue(key); + } else { + throw new UnsupportedOperationException( + "not implemented yet - see JCR-2062"); + } + } + + public Value[] getDescriptorValues(String key) { + if (repository != null) { + return repository.getDescriptorValues(key); + } else { + throw new UnsupportedOperationException( + "not implemented yet - see JCR-2062"); + } + } + + public boolean isSingleValueDescriptor(String key) { + if (repository != null) { + return repository.isSingleValueDescriptor(key); + } else { + throw new UnsupportedOperationException( + "not implemented yet - see JCR-2062"); + } + } + + /** + * Logs in to the content repository. Initializes the underlying repository + * instance if needed. The opened session is added to the set of open + * sessions and a session listener is added to track when the session gets + * closed. + * + * @param credentials login credentials + * @param workspaceName workspace name + * @return new session + * @throws RepositoryException if the session could not be created + * @see javax.jcr.Repository#login(Credentials,String) + */ + public synchronized Session login( + Credentials credentials, String workspaceName) + throws RepositoryException { + // Start the repository if this is the first login + if (repository == null) { + startRepository(); + } + + try { + logger.debug("Opening a new session"); + SessionImpl session = (SessionImpl) repository.login( + credentials, workspaceName); + sessions.put(session, session); + session.addListener(this); + logger.info("Session opened"); + + return session; + } finally { + // Stop the repository if the login failed + // and no other sessions are active + if (sessions.isEmpty()) { + stopRepository(); + } + } + } + + //-------------------------------------------------- + + /** + * Forces all active sessions to logout. Once the last session has logged + * out, the underlying repository instance will automatically be shut down. + * + * @see Session#logout() + */ + public synchronized void shutdown() { + Session[] copy = sessions.keySet().toArray(new Session[0]); + for (Session session : copy) { + session.logout(); + } + } + + //------------------------------------------------------- + + /** + * Removes the given session from the set of open sessions. If no open + * sessions remain, then the underlying repository instance is shut down. + * + * @param session closed session + * @see SessionListener#loggedOut(SessionImpl) + */ + public synchronized void loggedOut(SessionImpl session) { + assert sessions.containsKey(session); + sessions.remove(session); + logger.info("Session closed"); + if (sessions.isEmpty()) { + // FIXME: This is an ugly hack to avoid an infinite loop when + // RepositoryImpl.shutdown() repeatedly calls logout() on all + // remaining active sessions including the one that just emitted + // the loggedOut() message to us! + repository.loggedOut(session); + + stopRepository(); + } + } + + /** + * Ignored. {@inheritDoc} + */ + public void loggingOut(SessionImpl session) { + } + + /** + * Get the current repository. + * + * @return the repository + */ + RepositoryImpl getRepository() { + return repository; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/UserPerWorkspaceSecurityManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/UserPerWorkspaceSecurityManager.java new file mode 100644 index 00000000000..1df5eabe264 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/UserPerWorkspaceSecurityManager.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.core; + +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.config.LoginModuleConfig; +import org.apache.jackrabbit.core.config.UserManagerConfig; +import org.apache.jackrabbit.core.security.authentication.AuthContext; +import org.apache.jackrabbit.core.security.authentication.AuthContextProvider; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; +import org.apache.jackrabbit.core.security.principal.AbstractPrincipalProvider; +import org.apache.jackrabbit.core.security.principal.DefaultPrincipalProvider; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.apache.jackrabbit.core.security.principal.PrincipalManagerImpl; +import org.apache.jackrabbit.core.security.principal.PrincipalProvider; +import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry; +import org.apache.jackrabbit.core.security.simple.SimpleWorkspaceAccessManager; +import org.apache.jackrabbit.core.security.user.MembershipCache; +import org.apache.jackrabbit.core.security.user.UserPerWorkspaceUserManager; +import org.apache.jackrabbit.core.security.user.UserManagerImpl; +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Derived security manager implementation that expects that users information + * is present in each workspace instead of having a single, dedicated + * "security-workspace" that provides user information. Consequently, the + * UserManager used to retrieve and manipulate user content is always + * bound to the Session passed to {@link #getUserManager(Session)}. + *

    In addition the default (user-based) principal provider created by + * {@link org.apache.jackrabbit.core.DefaultSecurityManager} + * cannot be used to retrieve principals. Instead this implementation keeps + * a distinct pp-registry for each workspace. + *

    + * NOTE: While this security manager asserts that a minimal set of system + * users (admin and anonymous) is present in each workspace + * it doesn't make any attempt to set or define the access permissions on the + * tree containing user related information. + */ +public class UserPerWorkspaceSecurityManager extends DefaultSecurityManager { + + private final Map ppRegistries = new HashMap(); + private final Object monitor = new Object(); + + /** + * List of workspace names for which {@link #createSystemUsers} has already + * been called. + */ + private final List systemUsersInitialized = new ArrayList(); + + private PrincipalProviderRegistry getPrincipalProviderRegistry(SessionImpl s) throws RepositoryException { + String wspName = s.getWorkspace().getName(); + synchronized (monitor) { + PrincipalProviderRegistry p = ppRegistries.get(wspName); + if (p == null) { + SystemSession systemSession; + if (s instanceof SystemSession) { + systemSession = (SystemSession) s; + } else { + RepositoryImpl repo = (RepositoryImpl) getRepository(); + systemSession = repo.getSystemSession(wspName); + // TODO: review again... this workaround is used in several places. + repo.markWorkspaceActive(wspName); + } + + Properties[] moduleConfig = new AuthContextProvider("", ((RepositoryImpl) getRepository()).getConfig().getSecurityConfig().getLoginModuleConfig()).getModuleConfig(); + + PrincipalProvider defaultPP = new DefaultPrincipalProvider(systemSession, (UserManagerImpl) getUserManager(systemSession)); + + boolean initialized = false; + for (Properties props : moduleConfig) { + //GRANITE-4470: apply config to DefaultPrincipalProvider if there is no explicit PrincipalProvider configured + if (!props.containsKey(LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS) && props.containsKey(AbstractPrincipalProvider.MAXSIZE_KEY)) { + defaultPP.init(props); + initialized = true; + break; + } + } + if (!initialized) { + defaultPP.init(new Properties()); + } + + p = new WorkspaceBasedPrincipalProviderRegistry(defaultPP); + ppRegistries.put(wspName, p); + } + return p; + } + } + + //------------------------------------------< JackrabbitSecurityManager >--- + /** + * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#init(Repository, Session) + */ + @Override + public void init(Repository repository, Session systemSession) throws RepositoryException { + super.init(repository, systemSession); + + systemUsersInitialized.add(systemSession.getWorkspace().getName()); + } + + /** + * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#dispose(String) + */ + @Override + public void dispose(String workspaceName) { + super.dispose(workspaceName); + synchronized (monitor) { + PrincipalProviderRegistry reg = ppRegistries.remove(workspaceName); + if (reg != null) { + reg.getDefault().close(); + } + } + } + + /** + * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#close() + */ + @Override + public void close() { + super.close(); + synchronized (monitor) { + for (PrincipalProviderRegistry registry : ppRegistries.values()) { + registry.getDefault().close(); + } + ppRegistries.clear(); + } + } + + /** + * As this implementation expects that users information in present in + * every workspace, the UserManager is always created with the given + * session. + * + * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#getUserManager(javax.jcr.Session) + */ + @Override + public UserManager getUserManager(Session session) throws RepositoryException { + checkInitialized(); + if (session == getSystemSession()) { + return super.getUserManager(session); + } else if (session instanceof SessionImpl) { + UserManager uMgr = createUserManager((SessionImpl) session); + // Since users are not stored in a dedicated security workspace: + // make sure the system users are present. this is always the case + // for the configured security-workspace (or if missing the default + // workspace) but not for other workspaces. + // However, the check is only executed if the given session is a + // SystemSession (see also #getPrincipalProviderRegistry(Session) + // that initializes a SystemSession based UserManager for each workspace). + String wspName = session.getWorkspace().getName(); + if (session instanceof SystemSession && !systemUsersInitialized.contains(wspName)) { + createSystemUsers(uMgr, (SystemSession) session, adminId, anonymousId); + systemUsersInitialized.add(wspName); + } + return uMgr; + } else { + throw new RepositoryException("Internal error: SessionImpl expected."); + } + } + + /** + * Creates an AuthContext for the given {@link javax.jcr.Credentials} and + * {@link javax.security.auth.Subject}.
    + * This includes selection of application specific LoginModules and + * initialization with credentials and Session to System-Workspace + * + * @return an {@link org.apache.jackrabbit.core.security.authentication.AuthContext} for the given Credentials, Subject + * @throws javax.jcr.RepositoryException in other exceptional repository states + */ + @Override + public AuthContext getAuthContext(Credentials creds, Subject subject, String workspaceName) + throws RepositoryException { + checkInitialized(); + SystemSession systemSession = ((RepositoryImpl) getRepository()).getSystemSession(workspaceName); + return getAuthContextProvider().getAuthContext(creds, subject, systemSession, + getPrincipalProviderRegistry(systemSession), adminId, anonymousId); + } + + //-------------------------------------------------------------------------- + /** + * Always returns null. The default principal provider is + * workspace depending as users are expected to exist in every workspace. + * + * @return null + * @throws RepositoryException + */ + @Override + protected PrincipalProvider createDefaultPrincipalProvider(Properties[] moduleConfig) throws RepositoryException { + return null; + } + + @Override + protected UserManager getSystemUserManager(String workspaceName) throws RepositoryException { + if (workspaceName.equals(getSystemSession().getWorkspace().getName())) { + return super.getSystemUserManager(workspaceName); + } else { + return ((RepositoryImpl) getRepository()).getWorkspaceInfo(workspaceName).getSystemSession().getUserManager(); + } + } + + /** + * Creates a new instanceof TransientChangeUserManagerImpl. + * + * @param session session + * @return an instanceof TransientChangeUserManagerImpl + * @throws RepositoryException + */ + @Override + protected UserManagerImpl createUserManager(SessionImpl session) throws RepositoryException { + UserManagerConfig umc = getConfig().getUserManagerConfig(); + UserManagerImpl umgr; + // in contrast to the DefaultSecurityManager users are not retrieved + // from a dedicated workspace: the system session of each workspace must + // get a system user manager that asserts the existence of the admin user. + if (umc != null) { + Class[] paramTypes = new Class[] { + SessionImpl.class, + String.class, + Properties.class, + MembershipCache.class}; + umgr = (UserPerWorkspaceUserManager) umc.getUserManager(UserPerWorkspaceUserManager.class, + paramTypes, session, adminId, umc.getParameters(), getMembershipCache(session)); + } else { + umgr = new UserPerWorkspaceUserManager(session, adminId, null, getMembershipCache(session)); + } + + if (umc != null && !(session instanceof SystemSession)) { + AuthorizableAction[] actions = umc.getAuthorizableActions(); + umgr.setAuthorizableActions(actions); + } + return umgr; + } + + /** + * @param session Session for the principal manager must be created. + * @return A new instance of PrincipalManagerImpl. Note that this implementation + * uses a workspace specific principal provider registry, that retrieves + * the configured providers from the registry obtained through + * {@link #getPrincipalProviderRegistry()} but has a workspace specific + * default provider. + * @throws RepositoryException + */ + @Override + protected PrincipalManager createPrincipalManager(SessionImpl session) throws RepositoryException { + return new PrincipalManagerImpl(session, getPrincipalProviderRegistry(session).getProviders()); + } + + /** + * Returns a new instance of SimpleWorkspaceAccessManager, since + * with the DefaultLoginModule the existence of the user + * is checked in order to successfully complete the login. Since with this + * SecurityManager users are stored separately in each workspace, a user + * may only login to a workspace if the corresponding user node exists. + * Consequently a lazy workspace access manager is sufficient. + *

    + * If this SecurityManager is used with a distinct LoginModule + * implementation, the {@link org.apache.jackrabbit.core.config.SecurityManagerConfig#getWorkspaceAccessConfig() configuration} + * for WorkspaceAccessManager should be adjusted as well. + * + * @return An new instance of {@link SimpleWorkspaceAccessManager}. + */ + @Override + protected WorkspaceAccessManager createDefaultWorkspaceAccessManager() { + return new WorkspaceAccessManagerImpl(); + } + + //-------------------------------------------------------------------------- + /** + * Workaround to get a default (user-based) principal provider depending + * on the workspace being accessed. This is required for this security + * manager as users aren't stored in a single, dedicated workspace. + */ + private final class WorkspaceBasedPrincipalProviderRegistry implements PrincipalProviderRegistry { + + private final PrincipalProvider defaultPrincipalProvider; + + public WorkspaceBasedPrincipalProviderRegistry(PrincipalProvider defaultPrincipalProvider) { + this.defaultPrincipalProvider = defaultPrincipalProvider; + } + + public PrincipalProvider registerProvider(Properties configuration) throws RepositoryException { + return getPrincipalProviderRegistry().registerProvider(configuration); + } + + public PrincipalProvider getDefault() { + return defaultPrincipalProvider; + } + + public PrincipalProvider getProvider(String className) { + PrincipalProvider p = getPrincipalProviderRegistry().getProvider(className); + if (p == null && defaultPrincipalProvider.getClass().getName().equals(className)) { + p = defaultPrincipalProvider; + } + return p; + } + + public PrincipalProvider[] getProviders() { + List l = new ArrayList(); + l.addAll(Arrays.asList(getPrincipalProviderRegistry().getProviders())); + l.add(defaultPrincipalProvider); + return l.toArray(new PrincipalProvider[l.size()]); + } + } + + private final class WorkspaceAccessManagerImpl implements WorkspaceAccessManager { + /** + * Does nothing. + * @see WorkspaceAccessManager#init(javax.jcr.Session) + */ + public void init(Session systemSession) throws RepositoryException { + // nothing to do. + } + + /** + * Does nothing. + * @see org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager#close() + */ + public void close() throws RepositoryException { + // nothing to do. + } + + /** + * Returns true if a workspace with the given + * workspaceName exists and if that workspace defines a + * user that matches any of the given principals; + * false otherwise. + * + * @see WorkspaceAccessManager#grants(java.util.Set, String) + */ + public boolean grants(Set principals, String workspaceName) throws RepositoryException { + if (!(Arrays.asList(((RepositoryImpl) getRepository()).getWorkspaceNames())).contains(workspaceName)) { + return false; + } else { + UserManager umgr = UserPerWorkspaceSecurityManager.this.getSystemUserManager(workspaceName); + for (Principal principal : principals) { + if (!GroupPrincipals.isGroup(principal)) { + // check if the workspace identified by the given workspace + // name contains a user with this principal + if (umgr.getAuthorizable(principal) != null) { + return true; + } + } + } + } + return false; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/VersionManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/VersionManagerImpl.java new file mode 100644 index 00000000000..b5af9b8f2fc --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/VersionManagerImpl.java @@ -0,0 +1,649 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_PENDING_CHANGES; +import static org.apache.jackrabbit.core.ItemValidator.CHECK_PENDING_CHANGES_ON_NODE; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.core.session.SessionWriteOperation; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.UpdatableItemStateManager; +import org.apache.jackrabbit.core.version.InconsistentVersioningState; +import org.apache.jackrabbit.core.version.InternalActivity; +import org.apache.jackrabbit.core.version.InternalBaseline; +import org.apache.jackrabbit.core.version.InternalVersion; +import org.apache.jackrabbit.core.version.InternalVersionHistory; +import org.apache.jackrabbit.core.version.NodeStateEx; +import org.apache.jackrabbit.core.version.VersionImpl; +import org.apache.jackrabbit.core.version.VersionManagerImplConfig; +import org.apache.jackrabbit.core.version.VersionSet; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the {@link javax.jcr.version.VersionManager}. + *

    + * This class implements the JCR Version Manager interface but most of the + * operations are performed in the super classes. this is only cosmetic to + * avoid huge source files. + */ +public class VersionManagerImpl extends VersionManagerImplConfig + implements VersionManager { + + /** + * default logger + */ + private static final Logger log = LoggerFactory.getLogger(VersionManagerImpl.class); + + /** + * Creates a new version manager + * + * @param context component context of the current session + * @param stateMgr the underlying state manager + * @param hierMgr local hierarchy manager + */ + public VersionManagerImpl( + SessionContext context, UpdatableItemStateManager stateMgr, + HierarchyManager hierMgr) { + super(context, stateMgr, hierMgr); + } + + private T perform(SessionOperation operation) + throws RepositoryException { + return context.getSessionState().perform(operation); + } + + /** Wrapper around {@link #checkin(String, Calendar)}. */ + public Version checkin(String absPath) throws RepositoryException { + return checkin(absPath, null); + } + + /** + * Creates a new version of the node at the given path. + * + * @param absPath node path + * @param created create time of the new version, + * or null for the current time + * @return new version + * @throws RepositoryException if the version can not be created + */ + public Version checkin(final String absPath, final Calendar created) + throws RepositoryException { + return perform(new SessionWriteOperation () { + public Version perform(SessionContext context) + throws RepositoryException { + NodeStateEx state = getNodeState( + absPath, + CHECK_LOCK | CHECK_HOLD | CHECK_PENDING_CHANGES_ON_NODE, + Permission.VERSION_MNGMT); + NodeId baseId = checkoutCheckin(state, true, false, created); + return (Version) session.getNodeById(baseId); + } + public String toString() { + return "versionManager.checkin(" + absPath + ", " + created + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public void checkout(final String absPath) throws RepositoryException { + perform(new SessionWriteOperation () { + public NodeId perform(SessionContext context) + throws RepositoryException { + NodeStateEx state = getNodeState( + absPath, + CHECK_LOCK | CHECK_HOLD, + Permission.VERSION_MNGMT); + return checkoutCheckin(state, false, true, null); + } + public String toString() { + return "versionManager.checkout(" + absPath + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public Version checkpoint(final String absPath) throws RepositoryException { + return perform(new SessionWriteOperation () { + public Version perform(SessionContext context) + throws RepositoryException { + NodeStateEx state = getNodeState( + absPath, + CHECK_LOCK | CHECK_HOLD | CHECK_PENDING_CHANGES_ON_NODE, + Permission.VERSION_MNGMT); + NodeId baseId = checkoutCheckin(state, true, true, null); + return (Version) session.getNodeById(baseId); + } + public String toString() { + return "versionManager.checkpoint(" + absPath + ")"; + } + }); + } + + /** Wrapper around {@link Node#isCheckedOut()}. */ + public boolean isCheckedOut(String absPath) throws RepositoryException { + return session.getNode(absPath).isCheckedOut(); + } + + /** + * {@inheritDoc} + */ + public VersionHistory getVersionHistory(final String absPath) + throws RepositoryException { + return perform(new SessionOperation () { + public VersionHistory perform(SessionContext context) + throws RepositoryException { + NodeStateEx state = getNodeState(absPath); + InternalVersionHistory vh = getVersionHistory(state); + if (vh == null) { + throw new InconsistentVersioningState("Couldn't get version history for node " + state.getNodeId()); + } + return (VersionHistory) session.getNodeById(vh.getId()); + } + public String toString() { + return "versionManager.getVersionHistory(" + absPath + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public Version getBaseVersion(final String absPath) + throws RepositoryException { + return perform(new SessionOperation () { + public Version perform(SessionContext context) + throws RepositoryException { + NodeStateEx state = getNodeState(absPath); + InternalVersion v = getBaseVersion(state); + return (Version) session.getNodeById(v.getId()); + } + public String toString() { + return "versionManager.getBaseVersion(" + absPath + ")"; + } + }); + } + + /** Wrapper around {@link #restore(Version[], boolean)}. */ + public void restore(Version version, boolean removeExisting) + throws RepositoryException { + restore(new Version[]{version}, removeExisting); + } + + /** + * {@inheritDoc} + */ + public void restore(final Version[] versions, final boolean removeExisting) + throws RepositoryException { + perform(new SessionWriteOperation () { + public Object perform(SessionContext context) + throws RepositoryException { + // check for pending changes + if (session.hasPendingChanges()) { + throw new InvalidItemStateException( + "Unable to restore version. Session has pending changes."); + } + + // add all versions to map of versions to restore + Map toRestore = + new HashMap(); + for (Version version : versions) { + InternalVersion v = + vMgr.getVersion(((VersionImpl) version).getNodeId()); + // check for collision + NodeId historyId = v.getVersionHistory().getId(); + if (toRestore.containsKey(historyId)) { + throw new VersionException( + "Unable to restore. Two or more versions have same version history."); + } + toRestore.put(historyId, v); + } + + WriteOperation ops = startWriteOperation(); + try { + internalRestore( + new VersionSet(toRestore, true), removeExisting); + ops.save(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + + return this; + } + public String toString() { + return "versionManager.restore(versions, " + removeExisting + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public void restore( + final String absPath, final String versionName, + final boolean removeExisting) throws RepositoryException { + perform(new SessionWriteOperation () { + public Object perform(SessionContext context) + throws RepositoryException { + NodeStateEx state = getNodeState( + absPath, + CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD, + Permission.NONE); + restore(state, context.getQName(versionName), removeExisting); + return this; + } + public String toString() { + return "versionManager.restore(" + + absPath + ", " + versionName + ", " + + removeExisting + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public void restore( + final String absPath, final Version version, final boolean removeExisting) + throws RepositoryException { + perform(new SessionWriteOperation () { + public Object perform(SessionContext context) + throws RepositoryException { + // first check if node exists + if (session.nodeExists(absPath)) { + throw new VersionException( + "VersionManager.restore(String, Version, boolean)" + + " not allowed on existing nodes; use" + + " VersionManager.restore(Version, boolean) instead: " + + absPath); + } else { + // parent has to exist + Path path = context.getQPath(absPath); + Path parentPath = path.getAncestor(1); + Name name = path.getName(); + NodeImpl parent = context.getItemManager().getNode(parentPath); + + NodeStateEx state = getNodeState( + parent, + CHECK_PENDING_CHANGES | ItemValidator.CHECK_LOCK | CHECK_HOLD, + Permission.NONE); + + // check if given version is a baseline + InternalVersion v = getVersion(version); + if (v instanceof InternalBaseline) { + restore(state, name, (InternalBaseline) v); + } else { + restore(state, name, v, removeExisting); + } + } + return this; + } + public String toString() { + return "versionManager.restore(" + + absPath + ", version, " + removeExisting + ")"; + } + }); + } + + /** + * Same as {@link #restore(String, String, boolean)} but to ensure + * backward compatibility for Node.restore(Version, boolean). + * + * @param node the node to restore + * @param version the version to restore + * @param removeExisting the remove existing flag + * @throws RepositoryException if an error occurs + */ + protected void restore(NodeImpl node, Version version, boolean removeExisting) + throws RepositoryException { + NodeStateEx state = getNodeState( + node.getPath(), + CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD, + Permission.NONE); + InternalVersion v = getVersion(version); + restore(state, v, removeExisting); + } + + /** + * {@inheritDoc} + */ + public void restoreByLabel( + final String absPath, final String versionLabel, + final boolean removeExisting) throws RepositoryException { + perform(new SessionWriteOperation () { + public Object perform(SessionContext context) + throws RepositoryException { + NodeStateEx state = getNodeState( + absPath, + CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD, + Permission.NONE); + restoreByLabel( + state, context.getQName(versionLabel), removeExisting); + return this; + } + public String toString() { + return "versionManager.restoreByLabel(" + + absPath + ", " + versionLabel + ", " + + removeExisting + ")"; + } + }); + } + + /** + * Does an update. + * @see javax.jcr.Node#update(String) + * + * @param node the node to update + * @param srcWorkspaceName the source workspace name + * @throws RepositoryException if an error occurs + */ + public void update(NodeImpl node, String srcWorkspaceName) + throws RepositoryException { + NodeStateEx state = getNodeState(node, + ItemValidator.CHECK_PENDING_CHANGES, + Permission.VERSION_MNGMT); + mergeOrUpdate(state, srcWorkspaceName, null, false, false); + } + + /** Wrapper around {@link #merge(String, String, boolean, boolean)}. */ + public NodeIterator merge( + String absPath, String srcWorkspace, boolean bestEffort) + throws RepositoryException { + return merge(absPath, srcWorkspace, bestEffort, false); + } + + /** + * {@inheritDoc} + */ + public NodeIterator merge( + final String absPath, final String srcWorkspaceName, + final boolean bestEffort, final boolean isShallow) + throws RepositoryException { + return perform(new SessionWriteOperation () { + public NodeIterator perform(SessionContext context) + throws RepositoryException { + NodeStateEx state = getNodeState( + absPath, + CHECK_PENDING_CHANGES, + Permission.VERSION_MNGMT); + List failedIds = new LinkedList(); + mergeOrUpdate(state, srcWorkspaceName, failedIds, bestEffort, isShallow); + return new LazyItemIterator(context, failedIds); + } + public String toString() { + return "versionManager.merge(" + + absPath + ", " + srcWorkspaceName + ", " + + bestEffort + ", " + isShallow + ")"; + } + }); + } + + /** + * Combines merge and update method + * @param state the state to merge or update + * @param srcWorkspaceName source workspace name + * @param failedIds list that will contain the failed ids. + * if null and update will be performed. + * @param bestEffort best effort flag + * @param isShallow is shallow flag + * @throws RepositoryException if an error occurs + */ + private void mergeOrUpdate(NodeStateEx state, String srcWorkspaceName, + List failedIds, boolean bestEffort, + boolean isShallow) + throws RepositoryException { + // if same workspace, ignore + if (!srcWorkspaceName.equals(session.getWorkspace().getName())) { + // check authorization for specified workspace + if (!session.getAccessManager().canAccess(srcWorkspaceName)) { + String msg = "not authorized to access " + srcWorkspaceName; + log.error(msg); + throw new AccessDeniedException(msg); + } + // get root node of src workspace + SessionImpl srcSession = null; + try { + // create session on other workspace for current subject + // (may throw NoSuchWorkspaceException and AccessDeniedException) + srcSession = ((RepositoryImpl) session.getRepository()) + .createSession(session.getSubject(), srcWorkspaceName); + WorkspaceImpl srcWsp = (WorkspaceImpl) srcSession.getWorkspace(); + NodeId rootNodeId = ((NodeImpl) srcSession.getRootNode()).getNodeId(); + NodeStateEx srcRoot = new NodeStateEx( + srcWsp.getItemStateManager(), + ntReg, + rootNodeId); + merge(state, srcRoot, failedIds, bestEffort, isShallow); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + if (srcSession != null) { + // we don't need the other session anymore, logout + srcSession.logout(); + } + } + } + } + + /** + * {@inheritDoc} + */ + public void doneMerge(String absPath, Version version) + throws RepositoryException { + NodeStateEx state = getNodeState(absPath, + ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD, + Permission.VERSION_MNGMT); + finishMerge(state, version, false); + } + + /** + * {@inheritDoc} + */ + public void cancelMerge(String absPath, Version version) + throws RepositoryException { + NodeStateEx state = getNodeState(absPath, + ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD, + Permission.VERSION_MNGMT); + finishMerge(state, version, true); + } + + /** + * {@inheritDoc} + */ + public Node createConfiguration(String absPath) throws RepositoryException { + if (session.nodeExists(absPath)) { + NodeStateEx state = getNodeState(absPath, + ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD, + Permission.VERSION_MNGMT); + // check versionable + if (!checkVersionable(state)) { + throw new UnsupportedRepositoryOperationException("Node not full versionable: " + absPath); + } + if (state.getPropertyValue(NameConstants.JCR_CONFIGURATION) != null) { + String msg = "Node is already a configuration root: " + absPath; + log.error(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + + NodeId configId = createConfiguration(state); + return session.getNodeById(configId); + } else { + String msg = "Create configuration node must exist: " + absPath; + log.error(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + } + + /** + * {@inheritDoc} + */ + public Node setActivity(Node activity) throws RepositoryException { + Node oldActivity = getActivity(); + if (activity == null) { + currentActivity = null; + } else { + NodeImpl actNode = (NodeImpl) activity; + if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) { + String msg = "Given node is not an activity: " + actNode.safeGetJCRPath(); + log.error(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + currentActivity = actNode.getNodeId(); + } + return oldActivity; + } + + /** + * {@inheritDoc} + */ + public Node getActivity() throws RepositoryException { + if (currentActivity == null) { + return null; + } else { + return session.getNodeById(currentActivity); + } + } + + /** + * {@inheritDoc} + */ + public Node createActivity(String title) throws RepositoryException { + NodeId id = vMgr.createActivity(session, title); + return session.getNodeById(id); + } + + /** + * {@inheritDoc} + */ + public void removeActivity(Node node) throws RepositoryException { + NodeImpl actNode = (NodeImpl) node; + if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) { + String msg = "Given node is not an activity: " + actNode.safeGetJCRPath(); + log.error(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + NodeId actId = actNode.getNodeId(); + vMgr.removeActivity(session, actId); + if (actId.equals(currentActivity)) { + currentActivity = null; + } + } + + /** + * {@inheritDoc} + */ + public NodeIterator merge(Node activityNode) throws RepositoryException { + NodeImpl actNode = (NodeImpl) activityNode; + if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) { + String msg = "Given node is not an activity: " + actNode.safeGetJCRPath(); + log.error(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + InternalActivity activity = vMgr.getActivity(actNode.getNodeId()); + if (activity == null) { + String msg = "Given activity not found in version storage."; + log.error(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + List failedIds = new ArrayList(); + merge(activity, failedIds); + return new LazyItemIterator(context, failedIds); + } + + /** + * returns the node state for the given path + * @param path path of the node + * @throws RepositoryException if an error occurs + * @return the node state + */ + private NodeStateEx getNodeState(String path) throws RepositoryException { + return getNodeState(path, 0, 0); + } + + /** + * checks the permissions for the given path and returns the node state + * @param path path of the node + * @param options options to check + * @param permissions permissions to check + * @throws RepositoryException if an error occurs + * @return the node state + */ + private NodeStateEx getNodeState(String path, int options, int permissions) + throws RepositoryException { + return getNodeState((NodeImpl) session.getNode(path), options, permissions); + } + + /** + * checks the permissions for the given path and returns the node state + * @param node the node + * @param options options to check + * @param permissions permissions to check + * @throws RepositoryException if an error occurs + * @return the node state + */ + private NodeStateEx getNodeState(NodeImpl node, int options, int permissions) + throws RepositoryException { + try { + if (options > 0 || permissions > 0) { + context.getItemValidator().checkModify(node, options, permissions); + } + return new NodeStateEx( + stateMgr, + ntReg, + (NodeState) stateMgr.getItemState(node.getNodeId()), + node.getQName()); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/WorkspaceImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/WorkspaceImpl.java new file mode 100644 index 00000000000..c1c6b5fe093 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/WorkspaceImpl.java @@ -0,0 +1,924 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.observation.ObservationManager; +import javax.jcr.query.QueryManager; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.commons.AbstractWorkspace; +import org.apache.jackrabbit.core.config.WorkspaceConfig; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.lock.LockManager; +import org.apache.jackrabbit.core.lock.LockManagerImpl; +import org.apache.jackrabbit.core.lock.SessionLockManager; +import org.apache.jackrabbit.core.lock.XALockManager; +import org.apache.jackrabbit.core.observation.EventStateCollection; +import org.apache.jackrabbit.core.observation.EventStateCollectionFactory; +import org.apache.jackrabbit.core.observation.ObservationManagerImpl; +import org.apache.jackrabbit.core.query.QueryManagerImpl; +import org.apache.jackrabbit.core.retention.RetentionRegistry; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ItemStateCacheFactory; +import org.apache.jackrabbit.core.state.LocalItemStateManager; +import org.apache.jackrabbit.core.state.SharedItemStateManager; +import org.apache.jackrabbit.core.state.XAItemStateManager; +import org.apache.jackrabbit.core.xml.ImportHandler; +import org.apache.jackrabbit.core.xml.Importer; +import org.apache.jackrabbit.core.xml.WorkspaceImporter; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; + +/** + * A WorkspaceImpl ... + */ +public class WorkspaceImpl extends AbstractWorkspace + implements JackrabbitWorkspace, javax.jcr.Workspace, + EventStateCollectionFactory { + + private static Logger log = LoggerFactory.getLogger(WorkspaceImpl.class); + + /** + * The component context of this session. + */ + protected final SessionContext context; + + /** + * The configuration of this Workspace + */ + protected final WorkspaceConfig wspConfig; + + /** + * The persistent state mgr associated with the workspace represented by this + * Workspace instance. + */ + protected final LocalItemStateManager stateMgr; + + /** + * The hierarchy mgr that reflects persistent state only + * (i.e. that is isolated from transient changes made through + * the session). + */ + protected final CachingHierarchyManager hierMgr; + + /** + * The ObservationManager instance for this session. + */ + protected ObservationManagerImpl obsMgr; + + /** + * The QueryManager for this Workspace. + */ + protected QueryManagerImpl queryManager; + + /** + * the session that was used to acquire this Workspace + */ + protected final SessionImpl session; + + /** + * The LockManager for this Workspace + */ + protected LockManager lockMgr; + + /** + * The API LockManager for this workspace, used to create and release + * locks and determine the lock status. + */ + private javax.jcr.lock.LockManager jcr283LockManager; + + /** + * The API Version manager for this workspace + */ + protected VersionManagerImpl versionMgr; + + /** + * The internal manager used to evaluate effective retention policies and + * holds. + */ + private RetentionRegistry retentionRegistry; + + /** + * Creates a new workspace instance + * + * @param context component context of this session + * @param wspConfig The workspace configuration + * @throws RepositoryException if the workspace can not be accessed + */ + public WorkspaceImpl( + SessionContext context, WorkspaceConfig wspConfig) + throws RepositoryException { + this.context = context; + this.wspConfig = wspConfig; + this.stateMgr = createItemStateManager(); + this.hierMgr = new CachingHierarchyManager( + context.getRootNodeId(), this.stateMgr); + this.stateMgr.addListener(hierMgr); + this.session = context.getSessionImpl(); + } + + /** + * The hierarchy manager that reflects workspace state only + * (i.e. that is isolated from transient changes made through + * the session) + * + * @return the hierarchy manager of this workspace + */ + public HierarchyManager getHierarchyManager() { + return hierMgr; + } + + /** + * Returns the item state manager associated with the workspace + * represented by this WorkspaceImpl instance. + * + * @return the item state manager of this workspace + */ + public LocalItemStateManager getItemStateManager() { + return stateMgr; + } + + /** + * Disposes this WorkspaceImpl and frees resources. + */ + void dispose() { + if (obsMgr != null) { + obsMgr.dispose(); + obsMgr = null; + } + // remove hierarchy manager as listener to avoid + // unnecessary work during stateMgr.dispose() + stateMgr.removeListener(hierMgr); + stateMgr.dispose(); + } + + /** + * Performs a sanity check on this workspace and the associated session. + * + * @throws RepositoryException if this workspace has been rendered invalid + * for some reason + */ + public void sanityCheck() throws RepositoryException { + context.getSessionState().checkAlive(); + } + + //--------------------------------------------------< new JSR 283 methods > + /** + * {@inheritDoc} + */ + public void createWorkspace(String name, String srcWorkspace) + throws AccessDeniedException, RepositoryException { + // check state of this instance + sanityCheck(); + context.getAccessManager().checkRepositoryPermission(Permission.WORKSPACE_MNGMT); + + WorkspaceManager manager = context.getRepositoryContext().getWorkspaceManager(); + manager.createWorkspace(name); + + SessionImpl tmpSession = null; + try { + // create a temporary session on new workspace for current subject + tmpSession = manager.createSession(session.getSubject(), name); + WorkspaceImpl newWsp = (WorkspaceImpl) tmpSession.getWorkspace(); + + // Workspace#clone(String, String, String, boolean) doesn't + // allow to clone to "/"... + //newWsp.clone(srcWorkspace, "/", "/", false); + Node root = getSession().getRootNode(); + for (NodeIterator it = root.getNodes(); it.hasNext(); ) { + Node child = it.nextNode(); + // skip nodes that already exist in newly created workspace + if (!tmpSession.nodeExists(child.getPath())) { + newWsp.clone(srcWorkspace, child.getPath(), child.getPath(), false); + } + } + } finally { + if (tmpSession != null) { + // we don't need the temporary session anymore, logout + tmpSession.logout(); + } + } + } + + /** + * {@inheritDoc} + *

    + * Always throws UnsupportedRepositoryOperationException since + * removal of workspaces is currently not supported. + */ + public void deleteWorkspace(String name) throws AccessDeniedException, + UnsupportedRepositoryOperationException, RepositoryException { + // check if workspace exists (will throw NoSuchWorkspaceException if not) + context.getRepository().getWorkspaceInfo(name); + context.getAccessManager().checkRepositoryPermission(Permission.WORKSPACE_MNGMT); + + // todo implement deleteWorkspace + throw new UnsupportedRepositoryOperationException("not yet implemented"); + } + + /** + * @see javax.jcr.Workspace#getLockManager() + * @see javax.jcr.lock.LockManager + */ + public javax.jcr.lock.LockManager getLockManager() throws UnsupportedRepositoryOperationException, RepositoryException { + if (jcr283LockManager == null) { + jcr283LockManager = + new SessionLockManager(context, getInternalLockManager()); + } + return jcr283LockManager; + } + + /** + * @see javax.jcr.Workspace#getVersionManager() + */ + public VersionManager getVersionManager() { + return getVersionManagerImpl(); + } + + VersionManagerImpl getVersionManagerImpl() { + if (versionMgr == null) { + versionMgr = new VersionManagerImpl(context, stateMgr, hierMgr); + } + return versionMgr; + } + + //-------------------------------< JackrabbitWorkspace/new JSR 283 method > + + /** + * Creates a new Workspace with the specified + * name. The new workspace is empty, meaning it contains only + * root node. + *

    + * The new workspace can be accessed through a login + * specifying its name. + *

    + * Throws an AccessDeniedException if the session through which + * this Workspace object was acquired does not have permission + * to create the new workspace. + *

    + * Throws an UnsupportedRepositoryOperationException if the repository does + * not support the creation of workspaces. + *

    + * A RepositoryException is thrown if another error occurs. + * + * @param name A String, the name of the new workspace. + * @throws AccessDeniedException if the session through which + * this Workspace object was acquired does not have permission + * to create the new workspace. + * @throws RepositoryException if another error occurs. + * @since JCR 2.0 + */ + public void createWorkspace(String name) + throws AccessDeniedException, RepositoryException { + // check state of this instance + sanityCheck(); + context.getAccessManager().checkRepositoryPermission(Permission.WORKSPACE_MNGMT); + + context.getRepositoryContext().getWorkspaceManager().createWorkspace(name); + } + + //--------------------------------------------------< JackrabbitWorkspace > + /** + * Creates a workspace with the given name and a workspace configuration + * template. + * + * @param workspaceName name of the new workspace + * @param configTemplate the configuration template of the new workspace + * @throws AccessDeniedException if the current session is not allowed to + * create the workspace + * @throws RepositoryException if a workspace with the given name + * already exists or if another error occurs + * @see #getAccessibleWorkspaceNames() + */ + public void createWorkspace( + String workspaceName, InputSource configTemplate) + throws AccessDeniedException, RepositoryException { + // check state of this instance + sanityCheck(); + context.getAccessManager().checkRepositoryPermission(Permission.WORKSPACE_MNGMT); + context.getRepositoryContext().getWorkspaceManager().createWorkspace( + workspaceName, configTemplate); + } + + /** + * Return the PrivilegeManager. + * + * @return + * @throws RepositoryException + * @see org.apache.jackrabbit.api.JackrabbitWorkspace#getPrivilegeManager() + */ + public PrivilegeManager getPrivilegeManager() throws RepositoryException { + sanityCheck(); + return context.getPrivilegeManager(); + } + + + /** + * Returns the configuration of this workspace. + * @return the workspace configuration + */ + public WorkspaceConfig getConfig() { + return wspConfig; + } + + /** + * @param srcAbsPath + * @param srcWsp + * @param destAbsPath + * @param flag one of + *

      + *
    • COPY
    • + *
    • CLONE
    • + *
    • CLONE_REMOVE_EXISTING
    • + *
    + * @return the path of the node at its new position + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws PathNotFoundException + * @throws ItemExistsException + * @throws LockException + * @throws RepositoryException + */ + private String internalCopy(String srcAbsPath, + WorkspaceImpl srcWsp, + String destAbsPath, + int flag) + throws ConstraintViolationException, AccessDeniedException, + VersionException, PathNotFoundException, ItemExistsException, + LockException, RepositoryException { + + Path srcPath; + try { + srcPath = context.getQPath(srcAbsPath).getNormalizedPath(); + } catch (NameException e) { + String msg = "invalid path: " + srcAbsPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + if (!srcPath.isAbsolute()) { + throw new RepositoryException("not an absolute path: " + srcAbsPath); + } + + Path destPath; + try { + destPath = context.getQPath(destAbsPath).getNormalizedPath(); + } catch (NameException e) { + String msg = "invalid path: " + destAbsPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + if (!destPath.isAbsolute()) { + throw new RepositoryException("not an absolute path: " + destAbsPath); + } + + BatchedItemOperations ops = + new BatchedItemOperations(stateMgr, context); + + try { + ops.edit(); + } catch (IllegalStateException e) { + String msg = "unable to start edit operation"; + log.debug(msg); + throw new RepositoryException(msg, e); + } + + boolean succeeded = false; + + try { + NodeId id = ops.copy(srcPath, srcWsp.getItemStateManager(), + srcWsp.getHierarchyManager(), + srcWsp.context.getAccessManager(), + destPath, flag); + ops.update(); + succeeded = true; + return context.getJCRPath(hierMgr.getPath(id)); + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + ops.cancel(); + } + } + } + + /** + * Handles a clone inside the same workspace, which is supported with + * shareable nodes. + * + * @see {@link #clone()} + * + * @param srcAbsPath source path + * @param destAbsPath destination path + * @return the path of the node at its new position + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws PathNotFoundException + * @throws ItemExistsException + * @throws LockException + * @throws RepositoryException + */ + private String internalClone(String srcAbsPath, String destAbsPath) + throws ConstraintViolationException, AccessDeniedException, + VersionException, PathNotFoundException, ItemExistsException, + LockException, RepositoryException { + + Path srcPath; + try { + srcPath = context.getQPath(srcAbsPath).getNormalizedPath(); + } catch (NameException e) { + String msg = "invalid path: " + srcAbsPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + if (!srcPath.isAbsolute()) { + throw new RepositoryException("not an absolute path: " + srcAbsPath); + } + + Path destPath; + try { + destPath = context.getQPath(destAbsPath).getNormalizedPath(); + } catch (NameException e) { + String msg = "invalid path: " + destAbsPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + if (!destPath.isAbsolute()) { + throw new RepositoryException("not an absolute path: " + destAbsPath); + } + + BatchedItemOperations ops = + new BatchedItemOperations(stateMgr, context); + + try { + ops.edit(); + } catch (IllegalStateException e) { + String msg = "unable to start edit operation"; + log.debug(msg); + throw new RepositoryException(msg, e); + } + + boolean succeeded = false; + + try { + ItemId id = ops.clone(srcPath, destPath); + ops.update(); + succeeded = true; + return context.getJCRPath(hierMgr.getPath(id)); + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + ops.cancel(); + } + } + } + + /** + * Return the lock manager for this workspace. If not already done, creates + * a new instance. + * + * @return lock manager for this workspace + * @throws RepositoryException if an error occurs + */ + public synchronized org.apache.jackrabbit.core.lock.LockManager getInternalLockManager() throws RepositoryException { + + // check state of this instance + sanityCheck(); + + if (lockMgr == null) { + lockMgr = + context.getRepository().getLockManager(wspConfig.getName()); + // FIXME Shouldn't need to use instanceof here + if (context.getSessionImpl() instanceof XASessionImpl) { + lockMgr = new XALockManager((LockManagerImpl) lockMgr); + } + } + return lockMgr; + } + + /** + * Return the internal effective retention/hold manager for this workspace. + * If not already done, creates a new instance. + * + * @return effective retention/hold manager for this workspace + * @throws RepositoryException if an error occurs + */ + synchronized RetentionRegistry getRetentionRegistry() throws RepositoryException { + // check state of this instance + sanityCheck(); + if (retentionRegistry == null) { + retentionRegistry = + context.getRepository().getRetentionRegistry(wspConfig.getName()); + } + return retentionRegistry; + } + + //------------------------------------------------------------< Workspace > + /** + * {@inheritDoc} + */ + public String getName() { + return wspConfig.getName(); + } + + /** + * {@inheritDoc} + */ + public Session getSession() { + return session; + } + + /** + * {@inheritDoc} + */ + public NamespaceRegistry getNamespaceRegistry() throws RepositoryException { + return context.getNamespaceRegistry(); + } + + /** + * {@inheritDoc} + */ + public NodeTypeManager getNodeTypeManager() throws RepositoryException { + // check state of this instance + sanityCheck(); + + return context.getNodeTypeManager(); + } + + /** + * {@inheritDoc} + */ + public void clone(String srcWorkspace, String srcAbsPath, + String destAbsPath, boolean removeExisting) + throws NoSuchWorkspaceException, ConstraintViolationException, + VersionException, AccessDeniedException, PathNotFoundException, + ItemExistsException, LockException, RepositoryException { + // check state of this instance + sanityCheck(); + + // check workspace name + if (getName().equals(srcWorkspace)) { + // clone to same workspace is allowed for mix:shareable nodes, + // but only if removeExisting is false + if (!removeExisting) { + internalClone(srcAbsPath, destAbsPath); + return; + } + // same as current workspace + String msg = srcWorkspace + ": illegal workspace (same as current)"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // check authorization for specified workspace + if (!context.getAccessManager().canAccess(srcWorkspace)) { + throw new AccessDeniedException("not authorized to access " + srcWorkspace); + } + + // clone (i.e. pull) subtree at srcAbsPath from srcWorkspace + // to 'this' workspace at destAbsPath + + SessionImpl srcSession = null; + try { + // create session on other workspace for current subject + // (may throw NoSuchWorkspaceException and AccessDeniedException) + WorkspaceManager manager = + context.getRepositoryContext().getWorkspaceManager(); + srcSession = manager.createSession(session.getSubject(), srcWorkspace); + WorkspaceImpl srcWsp = (WorkspaceImpl) srcSession.getWorkspace(); + + // do cross-workspace copy + int mode = BatchedItemOperations.CLONE; + if (removeExisting) { + mode = BatchedItemOperations.CLONE_REMOVE_EXISTING; + } + internalCopy(srcAbsPath, srcWsp, destAbsPath, mode); + } finally { + if (srcSession != null) { + // we don't need the other session anymore, logout + srcSession.logout(); + } + } + } + + /** + * {@inheritDoc} + */ + public void copy(String srcAbsPath, String destAbsPath) + throws ConstraintViolationException, VersionException, + AccessDeniedException, PathNotFoundException, ItemExistsException, + LockException, RepositoryException { + // check state of this instance + sanityCheck(); + + // do intra-workspace copy + internalCopy(srcAbsPath, this, destAbsPath, BatchedItemOperations.COPY); + } + + /** + * {@inheritDoc} + */ + public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) + throws NoSuchWorkspaceException, ConstraintViolationException, + VersionException, AccessDeniedException, PathNotFoundException, + ItemExistsException, LockException, RepositoryException { + + // check state of this instance + sanityCheck(); + + // check workspace name + if (getName().equals(srcWorkspace)) { + // same as current workspace, delegate to intra-workspace copy method + copy(srcAbsPath, destAbsPath); + return; + } + + // check authorization for specified workspace + if (!context.getAccessManager().canAccess(srcWorkspace)) { + throw new AccessDeniedException("not authorized to access " + srcWorkspace); + } + + // copy (i.e. pull) subtree at srcAbsPath from srcWorkspace + // to 'this' workspace at destAbsPath + + SessionImpl srcSession = null; + try { + // create session on other workspace for current subject + // (may throw NoSuchWorkspaceException and AccessDeniedException) + WorkspaceManager manager = + context.getRepositoryContext().getWorkspaceManager(); + srcSession = manager.createSession(session.getSubject(), srcWorkspace); + WorkspaceImpl srcWsp = (WorkspaceImpl) srcSession.getWorkspace(); + + // do cross-workspace copy + internalCopy(srcAbsPath, srcWsp, destAbsPath, BatchedItemOperations.COPY); + } finally { + if (srcSession != null) { + // we don't need the other session anymore, logout + srcSession.logout(); + } + } + } + + /** + * {@inheritDoc} + */ + public void move(String srcAbsPath, String destAbsPath) + throws ConstraintViolationException, VersionException, + AccessDeniedException, PathNotFoundException, ItemExistsException, + LockException, RepositoryException { + // check state of this instance + sanityCheck(); + + // intra-workspace move... + + Path srcPath; + try { + srcPath = context.getQPath(srcAbsPath).getNormalizedPath(); + } catch (NameException e) { + String msg = "invalid path: " + srcAbsPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + if (!srcPath.isAbsolute()) { + throw new RepositoryException("not an absolute path: " + srcAbsPath); + } + + Path destPath; + try { + destPath = context.getQPath(destAbsPath).getNormalizedPath(); + } catch (NameException e) { + String msg = "invalid path: " + destAbsPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + if (!destPath.isAbsolute()) { + throw new RepositoryException("not an absolute path: " + destAbsPath); + } + + BatchedItemOperations ops = + new BatchedItemOperations(stateMgr, context); + + try { + ops.edit(); + } catch (IllegalStateException e) { + String msg = "unable to start edit operation"; + log.debug(msg); + throw new RepositoryException(msg, e); + } + + boolean succeeded = false; + + try { + ops.move(srcPath, destPath); + ops.update(); + succeeded = true; + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + ops.cancel(); + } + } + } + + /** + * Returns the observation manager of this session. + * + * @return the observation manager of this session + */ + public ObservationManager getObservationManager() { + return context.getObservationManager(); + } + + /** + * {@inheritDoc} + */ + public synchronized QueryManager getQueryManager() throws RepositoryException { + + // check state of this instance + sanityCheck(); + + if (queryManager == null) { + SearchManager searchManager; + try { + searchManager = + context.getRepository().getSearchManager(wspConfig.getName()); + if (searchManager == null) { + String msg = "no search manager configured for this workspace"; + log.debug(msg); + throw new RepositoryException(msg); + } + } catch (NoSuchWorkspaceException nswe) { + // should never get here + String msg = "internal error: failed to instantiate query manager"; + log.debug(msg); + throw new RepositoryException(msg, nswe); + } + queryManager = new QueryManagerImpl(context, searchManager); + } + return queryManager; + } + + /** + * {@inheritDoc} + */ + @Deprecated + public void restore(Version[] versions, boolean removeExisting) + throws ItemExistsException, UnsupportedRepositoryOperationException, + VersionException, LockException, InvalidItemStateException, + RepositoryException { + // check state of this instance + sanityCheck(); + getVersionManager().restore(versions, removeExisting); + } + + /** + * Returns the names of all workspaces of this repository with respect of the + * access rights of this session. + * + * @return the names of all accessible workspaces + * @throws RepositoryException if an error occurs + */ + public String[] getAccessibleWorkspaceNames() throws RepositoryException { + // check state of this instance + sanityCheck(); + + // filter workspaces according to access rights + List names = new ArrayList(); + WorkspaceManager manager = + context.getRepositoryContext().getWorkspaceManager(); + for (String name : manager.getWorkspaceNames()) { + try { + if (context.getAccessManager().canAccess(name)) { + names.add(name); + } + } catch (NoSuchWorkspaceException e) { + log.warn("Workspace disappeared unexpectedly: " + name, e); + } + } + return names.toArray(new String[names.size()]); + } + + + /** + * {@inheritDoc} + */ + public ContentHandler getImportContentHandler(String parentAbsPath, + int uuidBehavior) + throws PathNotFoundException, ConstraintViolationException, + VersionException, LockException, RepositoryException { + + // check state of this instance + sanityCheck(); + + Path parentPath; + try { + parentPath = context.getQPath(parentAbsPath).getNormalizedPath(); + } catch (NameException e) { + String msg = "invalid path: " + parentAbsPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + if (!parentPath.isAbsolute()) { + throw new RepositoryException("not an absolute path: " + parentAbsPath); + } + + Importer importer = new WorkspaceImporter( + parentPath, this, context, + uuidBehavior, wspConfig.getImportConfig()); + return new ImportHandler(importer, getSession()); + } + + /** + * Returns the shared item state manager of this workspace. + * + * @return shared item state manager + * @throws RepositoryException if the workspace can not be accessed + */ + protected SharedItemStateManager getSharedItemStateManager() + throws RepositoryException { + WorkspaceManager manager = + context.getRepositoryContext().getWorkspaceManager(); + return manager.getWorkspaceStateManager(getName()); + } + + /** + * Create the persistent item state manager on top of the shared item + * state manager. + * + * @return local item state manager + * @throws RepositoryException if the workspace can not be accessed + */ + protected LocalItemStateManager createItemStateManager() + throws RepositoryException { + SharedItemStateManager sism = getSharedItemStateManager(); + ItemStateCacheFactory iscf = + context.getRepositoryContext().getItemStateCacheFactory(); + + // FIXME We should be able to avoid the instanceof operator here + if (context.getSessionImpl() instanceof XASessionImpl) { + return XAItemStateManager.createInstance( + sism, this, null, iscf); + } else { + return LocalItemStateManager.createInstance( + sism, this, iscf); + } + + } + + //------------------------------------------< EventStateCollectionFactory > + /** + * {@inheritDoc} + *

    + * Implemented in this object and forwarded rather than {@link #obsMgr} + * since creation of the latter is lazy. + */ + public EventStateCollection createEventStateCollection() + throws RepositoryException { + + return ((ObservationManagerImpl) getObservationManager()).createEventStateCollection(); + } +} + + diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/WorkspaceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/WorkspaceManager.java new file mode 100644 index 00000000000..e95d18397c2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/WorkspaceManager.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.core; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.RepositoryException; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.core.observation.ObservationDispatcher; +import org.apache.jackrabbit.core.state.SharedItemStateManager; +import org.xml.sax.InputSource; + +/** + * Utility class that decouples {@link SessionImpl} from the internal + * workspace handling details of {@link RepositoryImpl}. + */ +public class WorkspaceManager { + + private final RepositoryImpl repository; + + WorkspaceManager(RepositoryImpl repository) { + this.repository = repository; + } + + /** + * Returns the name of the default workspace. + * + * @return default workspace name + */ + public String getDefaultWorkspaceName() { + return repository.getConfig().getDefaultWorkspaceName(); + } + + /** + * Returns the names of all the available workspaces. + * + * @return workspace names + */ + public String[] getWorkspaceNames() { + return repository.getWorkspaceNames(); + } + + /** + * Creates a workspace with the given name. + * + * @param workspaceName name of the new workspace + * @throws RepositoryException if a workspace with the given name + * already exists or if another error occurs + */ + public void createWorkspace(String workspaceName) + throws RepositoryException { + repository.createWorkspace(workspaceName); + } + + /** + * Creates a workspace with the given name and a workspace configuration + * template. + * + * @param workspaceName name of the new workspace + * @param configTemplate the configuration template of the new workspace + * @throws RepositoryException if a workspace with the given name already + * exists or if another error occurs + */ + public void createWorkspace( + String workspaceName, InputSource configTemplate) + throws RepositoryException { + repository.createWorkspace(workspaceName, configTemplate); + } + + // FIXME: This is a too low-level method. Refactor... + public SharedItemStateManager getWorkspaceStateManager(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + return repository.getWorkspaceStateManager(workspaceName); + } + + // FIXME: This is a too low-level method. Refactor... + public ObservationDispatcher getObservationDispatcher(String workspaceName) + throws NoSuchWorkspaceException, RepositoryException { + return repository.getObservationDispatcher(workspaceName); + } + + // FIXME: There should be a better place for this. Refactor... + public SessionImpl createSession(Subject subject, String workspaceName) + throws RepositoryException { + return repository.createSession(subject, workspaceName); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/XASessionImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/XASessionImpl.java new file mode 100644 index 00000000000..f2bcaa35779 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/XASessionImpl.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.core; + +import org.apache.jackrabbit.api.XASession; +import org.apache.jackrabbit.core.config.WorkspaceConfig; +import org.apache.jackrabbit.core.lock.XALockManager; +import org.apache.jackrabbit.core.security.authentication.AuthContext; +import org.apache.jackrabbit.core.state.XAItemStateManager; +import org.apache.jackrabbit.core.version.InternalVersionManager; +import org.apache.jackrabbit.core.version.InternalXAVersionManager; +import org.apache.jackrabbit.data.core.InternalXAResource; +import org.apache.jackrabbit.data.core.TransactionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.security.auth.Subject; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Session extension that provides XA support. + */ +@SuppressWarnings("deprecation") +public class XASessionImpl extends SessionImpl + implements XASession, XAResource { + + /** + * Logger instance + */ + private static final Logger log = LoggerFactory.getLogger(XASessionImpl.class); + + /** + * Global transactions + */ + private static final Map txGlobal = + Collections.synchronizedMap(new HashMap()); + + /** + * System property specifying the default Transaction Timeout + */ + public static final String SYSTEM_PROPERTY_DEFAULT_TRANSACTION_TIMEOUT = "org.apache.jackrabbit.core.defaultTransactionTimeout"; + + /** + * Default transaction timeout, in seconds. + * Either it is specified by the System Property {@link XASessionImpl#SYSTEM_PROPERTY_DEFAULT_TRANSACTION_TIMEOUT} or + * it is per default 5 seconds if it is not set by the TransactionManager at runtime + */ + private static final int DEFAULT_TX_TIMEOUT = Integer.parseInt(System.getProperty(SYSTEM_PROPERTY_DEFAULT_TRANSACTION_TIMEOUT, "5")); + + /** + * Currently associated transaction + */ + private TransactionContext tx; + + /** + * Transaction timeout, in seconds + */ + private int txTimeout; + + /** + * List of transactional resources. + */ + private InternalXAResource[] txResources; + + /** + * Create a new instance of this class. + * + * @param repositoryContext repository context + * @param loginContext login context containing authenticated subject + * @param wspConfig workspace configuration + * @throws AccessDeniedException if the subject of the given login context + * is not granted access to the specified + * workspace + * @throws RepositoryException if another error occurs + */ + protected XASessionImpl( + RepositoryContext repositoryContext, AuthContext loginContext, + WorkspaceConfig wspConfig) + throws AccessDeniedException, RepositoryException { + super(repositoryContext, loginContext, wspConfig); + init(); + } + + /** + * Create a new instance of this class. + * + * @param repositoryContext repository context + * @param subject authenticated subject + * @param wspConfig workspace configuration + * @throws AccessDeniedException if the given subject is not granted access + * to the specified workspace + * @throws RepositoryException if another error occurs + */ + protected XASessionImpl( + RepositoryContext repositoryContext, Subject subject, + WorkspaceConfig wspConfig) + throws AccessDeniedException, RepositoryException { + super(repositoryContext, subject, wspConfig); + init(); + } + + /** + * Initialize this object. + */ + private void init() throws RepositoryException { + WorkspaceImpl workspace = context.getWorkspace(); + XAItemStateManager stateMgr = + (XAItemStateManager) workspace.getItemStateManager(); + XALockManager lockMgr = + (XALockManager) workspace.getInternalLockManager(); + InternalXAVersionManager versionMgr = + (InternalXAVersionManager) getInternalVersionManager(); + + /** + * Create array that contains all resources that participate in this + * transactions. Some resources depend on each other, therefore you + * should only change the sequence if you know what you are doing! + * + * There are two artificial resources on the version manager (begin and + * end), which handle locking of the version manager. The begin resource + * acquires the write lock on the version manager in its prepare method, + * while the end resource releases the write lock in either commit or + * rollback. Please note that the write lock is only acquired if there + * is something to commit by the version manager. + * For further information see JCR-335 and JCR-962. + */ + txResources = new InternalXAResource[] { + versionMgr.getXAResourceBegin(), + versionMgr, stateMgr, lockMgr, + versionMgr.getXAResourceEnd() + }; + stateMgr.setVirtualProvider(versionMgr); + } + + /** + * {@inheritDoc} + */ + @Override + protected InternalVersionManager createVersionManager() + throws RepositoryException { + return new InternalXAVersionManager( + repositoryContext.getInternalVersionManager(), + repositoryContext.getNodeTypeRegistry(), + this, + repositoryContext.getItemStateCacheFactory()); + } + + //-------------------------------------------------------------< XASession > + /** + * {@inheritDoc} + */ + public XAResource getXAResource() { + return this; + } + + //------------------------------------------------------------< XAResource > + /** + * {@inheritDoc} + */ + public int getTransactionTimeout() { + return txTimeout == 0 ? DEFAULT_TX_TIMEOUT : txTimeout; + } + + /** + * {@inheritDoc} + */ + public boolean setTransactionTimeout(int seconds) { + txTimeout = seconds; + return true; + } + + /** + * {@inheritDoc} + *

    + * Two resources belong to the same resource manager if both connections + * (i.e. sessions) have the same credentials. + */ + public boolean isSameRM(XAResource xares) throws XAException { + if (xares instanceof XASessionImpl) { + XASessionImpl xases = (XASessionImpl) xares; + return stringsEqual(userId, xases.userId); + } + return false; + } + + /** + * {@inheritDoc} + *

    + * If TMNOFLAGS is specified, we create a new transaction + * context and associate it with this resource. + * If TMJOIN is specified, this resource should use the + * same transaction context as another, already known transaction. + * If TMRESUME is specified, we should resume work on + * a transaction context that was suspended earlier. + * All other flags generate an XAException of type + * XAER_INVAL + */ + public void start(Xid xid, int flags) throws XAException { + if (isAssociated()) { + log.error("Resource already associated with a transaction."); + throw new XAException(XAException.XAER_PROTO); + } + TransactionContext tx = txGlobal.get(xid); + if (flags == TMNOFLAGS) { + if (tx != null) { + throw new XAException(XAException.XAER_DUPID); + } + tx = createTransaction(xid); + } else if (flags == TMJOIN) { + if (tx == null) { + throw new XAException(XAException.XAER_NOTA); + } + } else if (flags == TMRESUME) { + if (tx == null) { + throw new XAException(XAException.XAER_NOTA); + } + if (!tx.isSuspended()) { + log.error("Unable to resume: transaction not suspended."); + throw new XAException(XAException.XAER_PROTO); + } + tx.setSuspended(false); + } else { + throw new XAException(XAException.XAER_INVAL); + } + + associate(tx); + } + + /** + * Create a new transaction context. + * @param xid xid of global transaction. + * @return transaction context + */ + private TransactionContext createTransaction(Xid xid) { + TransactionContext tx = new TransactionContext(xid, txResources); + txGlobal.put(xid, tx); + return tx; + } + + /** + * {@inheritDoc} + *

    + * If TMSUCCESS is specified, we disassociate this session + * from the transaction specified. + * If TMFAIL is specified, we disassociate this session from + * the transaction specified and mark the transaction rollback only. + * If TMSUSPEND is specified, we disassociate this session + * from the transaction specified. + * All other flags generate an XAException of type + * XAER_INVAL + *

    + * It is legal for a transaction association to be suspended and then + * ended (either with TMSUCCESS or TMFAIL) + * without having been resumed again. + */ + public void end(Xid xid, int flags) throws XAException { + TransactionContext tx = txGlobal.get(xid); + if (tx == null) { + throw new XAException(XAException.XAER_NOTA); + } + if (flags == TMSUSPEND) { + if (!isAssociated()) { + log.error("Resource not associated with a transaction."); + throw new XAException(XAException.XAER_PROTO); + } + associate(null); + tx.setSuspended(true); + } else if (flags == TMFAIL || flags == TMSUCCESS) { + if (!tx.isSuspended()) { + if (!isAssociated()) { + log.error("Resource not associated with a transaction."); + throw new XAException(XAException.XAER_PROTO); + } + associate(null); + } else { + tx.setSuspended(false); + } + } else { + throw new XAException(XAException.XAER_INVAL); + } + } + + /** + * {@inheritDoc} + */ + public int prepare(Xid xid) throws XAException { + TransactionContext tx = txGlobal.get(xid); + if (tx == null) { + throw new XAException(XAException.XAER_NOTA); + } + tx.prepare(); + return XA_OK; + } + + /** + * {@inheritDoc} + */ + public void commit(Xid xid, boolean onePhase) throws XAException { + TransactionContext tx = txGlobal.get(xid); + if (tx == null) { + throw new XAException(XAException.XAER_NOTA); + } + try { + if (onePhase) { + tx.prepare(); + } + tx.commit(); + } finally { + txGlobal.remove(xid); + } + } + + /** + * {@inheritDoc} + */ + public void rollback(Xid xid) throws XAException { + TransactionContext tx = txGlobal.get(xid); + if (tx == null) { + throw new XAException(XAException.XAER_NOTA); + } + try { + tx.rollback(); + } finally { + txGlobal.remove(xid); + } + } + + /** + * {@inheritDoc} + *

    + * No recovery support yet. + */ + public Xid[] recover(int flags) throws XAException { + return new Xid[0]; + } + + /** + * {@inheritDoc} + *

    + * No recovery support yet. + */ + public void forget(Xid xid) throws XAException { + } + + /** + * Associate this session with a global transaction. Internally, set + * the transaction containing all transaction-local objects to be + * used when performing item retrieval and store. + */ + public synchronized void associate(TransactionContext tx) { + this.tx = tx; + for (InternalXAResource txResource : txResources) { + txResource.associate(tx); + } + } + + /** + * Return a flag indicating whether this resource is associated + * with a transaction. + * + * @return true if this resource is associated + * with a transaction; otherwise false + */ + private boolean isAssociated() { + return tx != null; + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void logout() { + super.logout(); + // dispose the caches + try { + ((InternalXAVersionManager) versionMgr).close(); + } catch (Exception e) { + log.warn("error while closing InternalXAVersionManager", e); + } + } + + /** + * Compare two strings for equality. If both are null, this + * is also considered to be equal. + */ + private static boolean stringsEqual(String s1, String s2) { + if (s1 == null) { + return s2 == null; + } else { + return s1.equals(s2); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ZombieHierarchyManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ZombieHierarchyManager.java new file mode 100644 index 00000000000..b0d14e57da0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/ZombieHierarchyManager.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.core; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Name; + +/** + * HierarchyManager implementation that is also able to + * build/resolve paths of those items that have been moved or removed + * (i.e. moved to the attic). + *

    + * todo make use of path caching + */ +public class ZombieHierarchyManager extends HierarchyManagerImpl { + + /** + * the attic + */ + protected ItemStateManager attic; + + public ZombieHierarchyManager(HierarchyManagerImpl parent, + ItemStateManager provider, + ItemStateManager attic) { + super(parent.getRootNodeId(), provider); + this.attic = attic; + } + + /** + * {@inheritDoc} + *

    + * Delivers state from attic if such exists, otherwise calls base class. + */ + protected ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + // always check attic first + if (attic.hasItemState(id)) { + return attic.getItemState(id); + } + // delegate to base class + return super.getItemState(id); + } + + /** + * {@inheritDoc} + *

    + * Returns true if there's state on the attic for the + * requested item; otherwise delegates to base class. + */ + protected boolean hasItemState(ItemId id) { + // always check attic first + if (attic.hasItemState(id)) { + return true; + } + // delegate to base class + return super.hasItemState(id); + } + + /** + * {@inheritDoc} + *

    + * Also allows for removed items. + */ + protected NodeId getParentId(ItemState state) { + if (state.hasOverlayedState()) { + // use 'old' parent in case item has been removed + return state.getOverlayedState().getParentId(); + } + // delegate to base class + return super.getParentId(state); + } + + /** + * {@inheritDoc} + *

    + * Also allows for removed/renamed child node entries. + */ + protected ChildNodeEntry getChildNodeEntry(NodeState parent, + Name name, + int index) { + // first look for the entry in the current child node entry list + ChildNodeEntry entry = super.getChildNodeEntry(parent, name, index); + if (entry == null) { + // if not found, we need to look for a removed child node entry + for (ChildNodeEntry candidate : parent.getRemovedChildNodeEntries()) { + if (candidate.getName().equals(name) + && candidate.getIndex() == index) { + entry = candidate; + break; + } + } + } + return entry; + } + + /** + * {@inheritDoc} + *

    + * Also allows for removed child node entries. + */ + protected ChildNodeEntry getChildNodeEntry(NodeState parent, + NodeId id) { + // first look for the entry in the current child node entry list + ChildNodeEntry entry = super.getChildNodeEntry(parent, id); + if (entry == null) { + // if not found, we need to look for a removed child node entry + for (ChildNodeEntry candidate : parent.getRemovedChildNodeEntries()) { + if (candidate.getId().equals(id)) { + entry = candidate; + break; + } + } + } + return entry; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/AbstractCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/AbstractCache.java new file mode 100644 index 00000000000..8f96659c268 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/AbstractCache.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cache; + +import static org.apache.jackrabbit.core.cache.CacheAccessListener.ACCESS_INTERVAL; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Abstract base class for managed {@link Cache}s. This class uses atomic + * variables to track the current and maximum size of the cache, the cache + * access count and a possible {@link CacheAccessListener} instance. + *

    + * A subclass should call the protected {@link #recordCacheAccess()} method + * whenever the cache is accessed (even cache misses should be reported). + * The subclass should also use the {@link #recordSizeChange(long)} method + * to record all changes in the cache size, and automatically evict excess + * items when the {@link #isTooBig()} method returns true. + */ +public abstract class AbstractCache implements Cache { + + /** + * The estimated amount of memory currently used by this cache. The + * current value is returned by the {@link #getMemoryUsed()} method + * and can be updated by a subclass using the protected + * {@link #recordSizeChange(long)} method. + */ + private final AtomicLong memoryUsed = new AtomicLong(); + + /** + * The allocated maximum size of this cache. A {@link CacheManager} uses + * the {@link #getMaxMemorySize()} and {@link #setMaxMemorySize(long)} + * methods to control the target size of a cache. Subclasses can use the + * protected {@link #isTooBig()} method to determine whether the current + * size of the cache exceeds this size limit. + */ + private final AtomicLong maxMemorySize = new AtomicLong(); + + /** + * Cache access counter. Used to fire periodic + * {@link CacheAccessListener#cacheAccessed()} events once every + * {@link CacheAccessListener#ACCESS_INTERVAL} calls to the protected + * {@link #recordCacheAccess()} method. + *

    + * A long counter is used to prevent integer overflow. Even if the cache + * was accessed once every nanosecond, an overflow would only occur in + * about 300 years. See + * JCR-3013. + */ + private final AtomicLong accessCount = new AtomicLong(); + + /** + * Cache access counter. Unike his counterpart {@link #accessCount}, this + * does not get reset. + * + * It is used in the cases where a cache listener needs to call + * {@link Cache#resetAccessCount()}, but also needs a total access count. If + * you are sure that nobody calls reset, you can just use + * {@link #accessCount}. + */ + private final AtomicLong totalAccessCount = new AtomicLong(); + + /** + * Cache miss counter. + */ + private final AtomicLong missCount = new AtomicLong(); + + /** + * Cache access listener. Set in the + * {@link #setAccessListener(CacheAccessListener)} method and accessed + * by periodically by the {@link #recordCacheAccess()} method. + */ + private final AtomicReference accessListener = + new AtomicReference(); + + /** + * Checks whether the current estimate of the amount of memory used + * by this cache exceeds the allocated maximum amount of memory. + * + * @return true if the cache size is too big, + * false otherwise + */ + protected boolean isTooBig() { + return memoryUsed.get() > maxMemorySize.get(); + } + + /** + * Updates the current memory use estimate of this cache. + * + * @param delta number of bytes added or removed + */ + protected void recordSizeChange(long delta) { + memoryUsed.addAndGet(delta); // ignore the return value + } + + /** + * Records a single cache access and calls the configured + * {@link CacheAccessListener} (if any) whenever the constant access + * interval has passed since the previous listener call. + */ + protected void recordCacheAccess() { + totalAccessCount.incrementAndGet(); + long count = accessCount.incrementAndGet(); + if (count % ACCESS_INTERVAL == 0) { + CacheAccessListener listener = accessListener.get(); + if (listener != null) { + listener.cacheAccessed(ACCESS_INTERVAL); + } + } + } + + protected void recordCacheMiss() { + missCount.incrementAndGet(); + } + + public long getAccessCount() { + return accessCount.get(); + } + + public void resetAccessCount() { + accessCount.set(0); + } + + public long getTotalAccessCount(){ + return totalAccessCount.get(); + } + + public long getMissCount() { + return missCount.get(); + } + + public void resetMissCount() { + missCount.set(0); + } + + public long getMemoryUsed() { + return memoryUsed.get(); + } + + public long getMaxMemorySize() { + return maxMemorySize.get(); + } + + public void setMaxMemorySize(long size) { + maxMemorySize.set(size); + } + + /** + * Set the cache access listener. Only one listener per cache is supported. + * + * @param listener the new listener + */ + public void setAccessListener(CacheAccessListener listener) { + accessListener.set(listener); + } + + /** + * {@inheritDoc} + */ + public void dispose() { + CacheAccessListener listener = accessListener.get(); + if (listener != null) { + listener.disposeCache(this); + } + } + + /** + * {@inheritDoc} + */ + public String getCacheInfoAsString() { + long u = getMemoryUsed() / 1024; + long m = getMaxMemorySize() / 1024; + StringBuilder c = new StringBuilder(); + c.append("cachename="); + c.append(this.toString()); + c.append(", elements="); + c.append(getElementCount()); + c.append(", usedmemorykb="); + c.append(u); + c.append(", maxmemorykb="); + c.append(m); + c.append(", access="); + c.append(getTotalAccessCount()); + c.append(", miss="); + c.append(getMissCount()); + return c.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/Cache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/Cache.java new file mode 100644 index 00000000000..58d9006b703 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/Cache.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.core.cache; + +/** + * A Cache object + * A cache must call CacheManager.getInstance().add(this) + * to take part in the dynamic memory distribution. + */ +public interface Cache { + + /** + * Set the new memory limit. + * @param size the size in bytes + */ + void setMaxMemorySize(long size); + + /** + * Get the current limit. + * @return the size in bytes + */ + long getMaxMemorySize(); + + /** + * Get the amount of used memory. + * @return the size in bytes + */ + long getMemoryUsed(); + + /** + * Get the number of accesses (get or set) until resetAccessCount was called. + * @return the count + */ + long getAccessCount(); + + /** + * Reset the access counter. + */ + void resetAccessCount(); + + /** + * Get the total number of cache accesses. + * @return the number of hits + */ + long getTotalAccessCount(); + + /** + * Get the number of cache misses. + * + * @return the number of misses + */ + long getMissCount(); + + /** + * Reset the cache miss counter. + */ + void resetMissCount(); + + /** + * Get the number of elements/objects in the cache. + * @return the number of elements + */ + long getElementCount(); + + /** + * Add a listener to this cache that is informed after a number of accesses. + */ + void setAccessListener(CacheAccessListener listener); + + /** + * Gathers the stats of the cache for logging. + */ + String getCacheInfoAsString(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/CacheAccessListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/CacheAccessListener.java new file mode 100644 index 00000000000..93392b96576 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/CacheAccessListener.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.core.cache; + +/** + * The cache access listener can be registered to a class. + * From time to time, the method cacheAccess is called. + */ +public interface CacheAccessListener { + + /** + * The access listener is only called each x accesses. + */ + int ACCESS_INTERVAL = 127; + + /** + * The cache calls this method after a number of cache accesses.
    + * + * For statistical purposes, the cache access count since the last call is + * included. In normal circumstances this is equal to + * {@link CacheAccessListener#ACCESS_INTERVAL} + * + * @param accessCount + * number of cache accesses since the last call + * + */ + void cacheAccessed(long accessCount); + + /** + * Called after the cache is no longer used. + */ + void disposeCache(Cache cache); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/CacheManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/CacheManager.java new file mode 100644 index 00000000000..9073645a188 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/CacheManager.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.core.cache; + +import java.util.ArrayList; +import java.util.List; +import java.util.WeakHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class manages the size of the caches used in Jackrabbit. The combined + * size of all caches must be limited to avoid out of memory problems. The + * available memory is dynamically distributed across the caches each second. + * This class tries to calculates the best cache sizes by comparing the access + * counts of each cache, and the used memory. The idea is, the more a cache is + * accessed, the more memory it should get, while the cache should not shrink + * too quickly. A minimum and maximum size per cache is defined as well. After + * distributing the memory in this way, there might be some unused memory (if + * one or more caches did not use some of the allocated memory). This unused + * memory is distributed evenly across the full caches. + */ +public class CacheManager implements CacheAccessListener { + + /** The logger instance. */ + private static Logger log = LoggerFactory.getLogger(CacheManager.class); + + /** The default maximum amount of memory to distribute across the caches. */ + private static final long DEFAULT_MAX_MEMORY = 16 * 1024 * 1024; + + /** The default minimum size of a cache. */ + private static final long DEFAULT_MIN_MEMORY_PER_CACHE = 128 * 1024; + + /** The default maximum memory per cache. */ + private static final long DEFAULT_MAX_MEMORY_PER_CACHE = 4 * 1024 * 1024; + + /** The set of caches (weakly referenced). */ + private WeakHashMap caches = new WeakHashMap(); + + /** The default minimum resize interval (in ms). */ + private static final int DEFAULT_MIN_RESIZE_INTERVAL = 1000; + + /** The default minimum stats logging interval (in ms). */ + private static final int DEFAULT_LOG_STATS_INTERVAL = 60 * 1000; + + /** The size of a big object, to detect if a cache is full or not. */ + private static final int BIG_OBJECT_SIZE = 16 * 1024; + + /** The amount of memory to distribute across the caches. */ + private long maxMemory = Long.getLong( + "org.apache.jackrabbit.maxCacheMemory", + DEFAULT_MAX_MEMORY); + + /** The minimum size of a cache. */ + private long minMemoryPerCache = Long.getLong( + "org.apache.jackrabbit.minMemoryPerCache", + DEFAULT_MIN_MEMORY_PER_CACHE); + + /** The maximum memory per cache (unless, there is some unused memory). */ + private long maxMemoryPerCache = Long.getLong( + "org.apache.jackrabbit.maxMemoryPerCache", + DEFAULT_MAX_MEMORY_PER_CACHE); + + /** The minimum resize interval time */ + private long minResizeInterval = Long.getLong( + "org.apache.jackrabbit.cacheResizeInterval", + DEFAULT_MIN_RESIZE_INTERVAL); + + /** The minimum interval time between stats are logged */ + private long minLogStatsInterval = Long.getLong( + "org.apache.jackrabbit.cacheLogStatsInterval", + DEFAULT_LOG_STATS_INTERVAL); + + /** The last time the caches where resized. */ + private volatile long nextResize = + System.currentTimeMillis() + DEFAULT_MIN_RESIZE_INTERVAL; + + + /** The last time the cache stats were logged. */ + private volatile long nextLogStats = + System.currentTimeMillis() + DEFAULT_LOG_STATS_INTERVAL; + + + public long getMaxMemory() { + return maxMemory; + } + + public void setMaxMemory(final long maxMemory) { + this.maxMemory = maxMemory; + } + + public long getMaxMemoryPerCache() { + return maxMemoryPerCache; + } + + public void setMaxMemoryPerCache(final long maxMemoryPerCache) { + this.maxMemoryPerCache = maxMemoryPerCache; + } + + public long getMinMemoryPerCache() { + return minMemoryPerCache; + } + + public void setMinMemoryPerCache(final long minMemoryPerCache) { + this.minMemoryPerCache = minMemoryPerCache; + } + + public long getMinResizeInterval() { + return minResizeInterval; + } + + public void setMinResizeInterval(long minResizeInterval) { + this.minResizeInterval = minResizeInterval; + } + + /** + * After one of the caches is accessed a number of times, this method is called. + * Resize the caches if required. + */ + public void cacheAccessed(long accessCount) { + + logCacheStats(); + + long now = System.currentTimeMillis(); + if (now < nextResize) { + return; + } + synchronized (this) { + // the previous test was not synchronized (for speed) + // so we need another synchronized test + if (now < nextResize) { + return; + } + nextResize = now + minResizeInterval; + resizeAll(); + nextResize = System.currentTimeMillis() + minResizeInterval; + } + } + + /** + * Log info about the caches. + */ + private void logCacheStats() { + if (log.isDebugEnabled()) { + long now = System.currentTimeMillis(); + if (now < nextLogStats) { + return; + } + // JCR-3194 avoid ConcurrentModificationException + List list = new ArrayList(); + synchronized (caches) { + list.addAll(caches.keySet()); + } + for (Cache cache : list) { + log.debug(cache.getCacheInfoAsString()); + } + nextLogStats = now + minLogStatsInterval; + } + } + + /** + * Re-calculate the maximum memory for each cache, and set the new limits. + */ + private void resizeAll() { + if (log.isTraceEnabled()) { + log.trace("resizeAll size=" + caches.size()); + } + // get strong references + // entries in a weak hash map may disappear any time + // so can't use size() / keySet() directly + // only using the iterator guarantees that we don't get null references + List list = new ArrayList(); + synchronized (caches) { + list.addAll(caches.keySet()); + } + if (list.size() == 0) { + // nothing to do + return; + } + CacheInfo[] infos = new CacheInfo[list.size()]; + for (int i = 0; i < list.size(); i++) { + infos[i] = new CacheInfo((Cache) list.get(i)); + } + // calculate the total access count and memory used + long totalAccessCount = 0; + long totalMemoryUsed = 0; + for (CacheInfo info : infos) { + totalAccessCount += info.getAccessCount(); + totalMemoryUsed += info.getMemoryUsed(); + } + // try to distribute the memory based on the access count + // and memory used (higher numbers - more memory) + // and find out how many caches are full + // 50% is distributed according to access count, + // and 50% according to memory used + double memoryPerAccess = (double) maxMemory / 2. + / Math.max(1., (double) totalAccessCount); + double memoryPerUsed = (double) maxMemory / 2. + / Math.max(1., (double) totalMemoryUsed); + int fullCacheCount = 0; + for (CacheInfo info : infos) { + long mem = (long) (memoryPerAccess * info.getAccessCount()); + mem += (long) (memoryPerUsed * info.getMemoryUsed()); + mem = Math.min(mem, maxMemoryPerCache); + if (info.wasFull()) { + fullCacheCount++; + } else { + mem = Math.min(mem, info.getMemoryUsed()); + } + mem = Math.min(mem, maxMemoryPerCache); + mem = Math.max(mem, minMemoryPerCache); + info.setMemory(mem); + } + // calculate the unused memory + long unusedMemory = maxMemory; + for (CacheInfo info : infos) { + unusedMemory -= info.getMemory(); + } + // distribute the remaining memory evenly across the full caches + if (unusedMemory > 0 && fullCacheCount > 0) { + for (CacheInfo info : infos) { + if (info.wasFull()) { + info.setMemory(info.getMemory() + unusedMemory + / fullCacheCount); + } + } + } + // set the new limit + for (CacheInfo info : infos) { + Cache cache = info.getCache(); + if (log.isTraceEnabled()) { + log.trace(cache + " now:" + cache.getMaxMemorySize() + " used:" + + info.getMemoryUsed() + " access:" + info.getAccessCount() + + " new:" + info.getMemory()); + } + cache.setMaxMemorySize(info.getMemory()); + } + } + + /** + * Add a new cache to the list. + * This call does not trigger recalculating the cache sizes. + * + * @param cache the cache to add + */ + public void add(Cache cache) { + synchronized (caches) { + caches.put(cache, null); + } + } + + /** + * Remove a cache. As this class only has a weak reference to each cache, + * calling this method is not strictly required. + * This call does not trigger recalculating the cache sizes. + * + * @param cache + * the cache to remove + */ + public void remove(Cache cache) { + synchronized (caches) { + caches.remove(cache); + } + } + + /** + * Internal copy of the cache information. + */ + public static class CacheInfo { + private Cache cache; + + private long accessCount; + + private long memory; + + private long memoryUsed; + + private boolean wasFull; + + CacheInfo(Cache cache) { + this.cache = cache; + // copy the data as this runs in a different thread + // the exact values are not important, but it is important that the + // values don't change + this.memory = cache.getMaxMemorySize(); + this.memoryUsed = cache.getMemoryUsed(); + this.accessCount = cache.getAccessCount(); + // reset the access count, so that concurrent cache access is not lost + cache.resetAccessCount(); + // if the memory used plus one large object is smaller than the + // allocated memory, + // then the memory was not fully used + wasFull = (memoryUsed + BIG_OBJECT_SIZE) >= memory; + } + + boolean wasFull() { + return wasFull; + } + + long getAccessCount() { + return accessCount; + } + + long getMemoryUsed() { + return memoryUsed; + } + + void setMemory(long mem) { + this.memory = mem; + } + + long getMemory() { + return memory; + } + + Cache getCache() { + return cache; + } + + } + + public void disposeCache(Cache cache) { + remove(cache); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/ConcurrentCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/ConcurrentCache.java new file mode 100644 index 00000000000..b03f9bbb680 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/ConcurrentCache.java @@ -0,0 +1,269 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cache; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Concurrent cache implementation that uses cache segments to minimize + * the chance of lock contention. The LRU algorithm is used to evict excess + * entries from each cache segment separately, which makes the combined + * eviction algorithm similar but not exactly the same as LRU. None of the + * methods of this class are synchronized, but they are all thread-safe. + */ +public class ConcurrentCache extends AbstractCache { + + /** + * Default number of cache segments to use. Use the number of available + * processors (even if that might change during runtime!) as a reasonable + * approximation of the amount of parallelism we should expect in the + * worst case. + *

    + * One reason for this value being a constant is that the + * {@link Runtime#availableProcessors()} call is somewhat expensive at + * least in some environments. + */ + private static int DEFAULT_NUMBER_OF_SEGMENTS = + Runtime.getRuntime().availableProcessors(); + + private static class E { + + private final V value; + + private final long size; + + public E(V value, long size) { + this.value = value; + this.size = size; + } + + } + + private final String name; + private final Map>[] segments; + + @SuppressWarnings({ "unchecked", "serial" }) + public ConcurrentCache(String name, int numberOfSegments) { + this.name = name; + this.segments = new Map[numberOfSegments]; + for (int i = 0; i < segments.length; i++) { + segments[i] = new LinkedHashMap>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + if (isTooBig()) { + recordSizeChange(-eldest.getValue().size); + return true; + } else { + return false; + } + } + }; + } + } + + public ConcurrentCache(String name) { + this(name, DEFAULT_NUMBER_OF_SEGMENTS); + } + + /** + * Returns the cache segment for the given entry key. The segment is + * selected based on the hash code of the key, after a transformation + * to prevent interfering with the optimal performance of the segment + * hash map. + * + * @param key entry key + * @return cache segment + */ + private Map> getSegment(K key) { + // Unsigned shift right to prevent negative indexes and to + // prevent too similar keys to all get stored in the same segment + return segments[(key.hashCode() >>> 1) % segments.length]; + } + + /** + * Checks if the identified entry is cached. + * + * @param key entry key + * @return true if the entry is cached, + * false otherwise + */ + public boolean containsKey(K key) { + Map> segment = getSegment(key); + synchronized (segment) { + return segment.containsKey(key); + } + } + + /** + * Returns the identified cache entry. + * + * @param key entry key + * @return entry value, or null if not found + */ + public V get(K key) { + recordCacheAccess(); + + Map> segment = getSegment(key); + synchronized (segment) { + E entry = segment.get(key); + if (entry != null) { + return entry.value; + } + } + recordCacheMiss(); + return null; + } + + /** + * Returns all values in the cache. Note that this method is not + * synchronized over the entire cache, so it is only guaranteed to + * return accurate results when there are no concurrent threads modifying + * the cache. + * + * @return cached values + */ + public List values() { + List values = new ArrayList(); + for (int i = 0; i < segments.length; i++) { + synchronized (segments[i]) { + for (E entry : segments[i].values()) { + values.add(entry.value); + } + } + } + return values; + } + + /** + * Adds the given entry to the cache. + * + * @param key entry key + * @param value entry value + * @param size entry size + * @return the previous value, or null + */ + public V put(K key, V value, long size) { + E previous; + + Map> segment = getSegment(key); + synchronized (segment) { + recordSizeChange(size); + previous = segment.put(key, new E(value, size)); + } + + if (previous != null) { + recordSizeChange(-previous.size); + shrinkIfNeeded(); + return previous.value; + } else { + shrinkIfNeeded(); + return null; + } + } + + /** + * Removes the identified entry from the cache. + * + * @param key entry key + * @return removed entry, or null if not found + */ + public V remove(K key) { + Map> segment = getSegment(key); + synchronized (segment) { + E entry = segment.remove(key); + if (entry != null) { + recordSizeChange(-entry.size); + return entry.value; + } else { + return null; + } + } + } + + /** + * Clears all segments of the cache. Note that even this method is not + * synchronized over the entire cache, so it needs to explicitly count + * the cache size changes and may return with a non-empty cache if + * other threads have concurrently been adding new entries. + */ + public void clear() { + for (int i = 0; i < segments.length; i++) { + synchronized (segments[i]) { + for (E entry : segments[i].values()) { + recordSizeChange(-entry.size); + } + segments[i].clear(); + } + } + } + + /** + * Checks if the cache size is zero. + */ + public boolean isEmpty() { + return getMemoryUsed() == 0; + } + + /** + * Sets the maximum size of the cache and evicts any excess items until + * the current size falls within the given limit. + */ + @Override + public void setMaxMemorySize(long size) { + super.setMaxMemorySize(size); + shrinkIfNeeded(); + } + + /** + * Removes old entries from the cache until the cache is small enough. + */ + private void shrinkIfNeeded() { + // Semi-random start index to prevent bias against the first segments + int start = (int) Math.abs(getAccessCount() % segments.length); + for (int i = start; isTooBig(); i = (i + 1) % segments.length) { + synchronized (segments[i]) { + Iterator>> iterator = + segments[i].entrySet().iterator(); + if (iterator.hasNext()) { + // Removing and re-adding the first entry will + // evict the last entry if the cache is too big + Map.Entry> entry = iterator.next(); + segments[i].remove(entry.getKey()); + segments[i].put(entry.getKey(), entry.getValue()); + } + } + } + } + + public long getElementCount() { + long count = 0; + for (int i = 0; i < segments.length; i++) { + count += segments[i].size(); + } + return count; + } + + @Override + public String toString() { + return name + "[" + getClass().getSimpleName() + "@" + + Integer.toHexString(hashCode()) + "]"; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/GrowingLRUMap.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/GrowingLRUMap.java new file mode 100644 index 00000000000..e94ede59ac3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cache/GrowingLRUMap.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.core.cache; + +import org.apache.commons.collections.map.LRUMap; + +/** + * GrowingLRUMap extends the LRUMap such that it can grow from + * the specified initialSize to the specified maxSize; + */ +public class GrowingLRUMap extends LRUMap { + + private final int maxSize; + + public GrowingLRUMap(int initialSize, int maxSize) { + super(initialSize); + this.maxSize = maxSize; + } + + @Override + protected boolean removeLRU(LinkEntry entry) { + return size() > maxSize; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ChangeLogRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ChangeLogRecord.java new file mode 100644 index 00000000000..41d9d96b661 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ChangeLogRecord.java @@ -0,0 +1,538 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Map; +import java.util.HashMap; + +import javax.jcr.Session; +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import javax.jcr.observation.Event; + +/** + * Cluster record representing a workspace or version update. + */ +public class ChangeLogRecord extends ClusterRecord { + + /** + * Identifier: DATE + */ + static final char DATE_IDENTIFIER = 'D'; + + /** + * Identifier: NODE. + */ + static final char NODE_IDENTIFIER = 'N'; + + /** + * Identifier: PROPERTY. + */ + static final char PROPERTY_IDENTIFIER = 'P'; + + /** + * Identifier: EVENT. + */ + static final char EVENT_IDENTIFIER = 'E'; + + /** + * Identifier: USER DATA. + */ + static final char USER_DATA_IDENTIFIER = 'U'; + + /** + * Operation type: added. + */ + private static final int ADDED = 1; + + /** + * Operation type: modified. + */ + private static final int MODIFIED = 2; + + /** + * Operation type: deleted. + */ + private static final int DELETED = 3; + + /** + * Changes. + */ + private ChangeLog changes; + + /** + * The time when the changes happened. Milliseconds since January 1 1970 UTC. + */ + private long timestamp = System.currentTimeMillis(); + + /** + * List of EventStates. + */ + private List events; + + /** + * The user data. + */ + private String userData; + + /** + * First identifier read. + */ + private int identifier; + + /** + * Last used session for event sources. + */ + private ClusterSession lastSession; + + /** + * Create a new instance of this class. Used when serializing. + * + * @param changes changes + * @param events list of EventStates + * @param record record + * @param workspace workspace + * @param timestamp when the changes for this record were persisted. + * @param userData the user data associated with these changes. + */ + public ChangeLogRecord(ChangeLog changes, List events, + Record record, String workspace, + long timestamp, String userData) { + super(record, workspace); + + this.changes = changes; + this.events = events; + this.timestamp = timestamp; + this.userData = userData; + } + + /** + * Create a new instance of this class. Used when deserializing. + * + * @param identifier first identifier read + * @param record record + * @param workspace workspace + */ + ChangeLogRecord(int identifier, Record record, String workspace) { + super(record, workspace); + + this.identifier = identifier; + this.changes = new ChangeLog(); + this.events = new ArrayList(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void doRead() throws JournalException { + int identifier = this.identifier; + + while (identifier != END_MARKER) { + switch (identifier) { + case DATE_IDENTIFIER: + readTimestampRecord(); + break; + case USER_DATA_IDENTIFIER: + readUserDataRecord(); + break; + case NODE_IDENTIFIER: + readNodeRecord(); + break; + case PROPERTY_IDENTIFIER: + readPropertyRecord(); + break; + case EVENT_IDENTIFIER: + readEventRecord(); + break; + default: + String msg = "Unknown identifier: " + identifier; + throw new JournalException(msg); + } + identifier = record.readChar(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void readEndMarker() throws JournalException { + // This record type uses the end marker itself to indicate that + // no more node/property/event records are available, so + // do not try read it twice + } + + /** + * Reads the timestamp record. + * + * @throws JournalException if an error occurs. + */ + private void readTimestampRecord() throws JournalException { + timestamp = record.readLong(); + } + + /** + * Reads the user data record. + * + * @throws JournalException if an error occurs. + */ + private void readUserDataRecord() throws JournalException { + userData = record.readString(); + } + + /** + * Read a node record. + * + * @throws JournalException if an error occurs + */ + private void readNodeRecord() throws JournalException { + int operation = record.readByte(); + NodeState state = new NodeState(record.readNodeId(), null, null, + ItemState.STATUS_NEW, false); + + apply(operation, state); + } + + /** + * Read a property record. + * + * @throws JournalException if an error occurs + */ + private void readPropertyRecord() throws JournalException { + int operation = record.readByte(); + PropertyState state = new PropertyState(record.readPropertyId(), + ItemState.STATUS_NEW, false); + + apply(operation, state); + } + + /** + * Apply an item state to the internal change log. + * + * @param operation operation + * @param state item state + * @throws JournalException if an error occurs + */ + private void apply(int operation, ItemState state) throws JournalException { + switch (operation) { + case ADDED: + state.setStatus(ItemState.STATUS_EXISTING); + changes.added(state); + break; + case DELETED: + state.setStatus(ItemState.STATUS_EXISTING_REMOVED); + changes.deleted(state); + break; + case MODIFIED: + state.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + changes.modified(state); + break; + default: + String msg = "Unknown item operation: " + operation; + throw new JournalException(msg); + } + } + + /** + * Read an event record. + * + * @throws JournalException if an error occurs + */ + private void readEventRecord() throws JournalException { + int type = record.readByte(); + NodeId parentId = record.readNodeId(); + Path parentPath = record.readPath(); + NodeId childId = record.readNodeId(); + Path childRelPath = record.readPathElement(); + Name ntName = record.readQName(); + + Set mixins = new HashSet(); + int mixinCount = record.readInt(); + for (int i = 0; i < mixinCount; i++) { + mixins.add(record.readQName()); + } + String userId = record.readString(); + + Map info = null; + if (type == Event.NODE_MOVED) { + info = new HashMap(); + // read info map + int infoSize = record.readInt(); + for (int i = 0; i < infoSize; i++) { + String key = record.readString(); + int propType = record.readInt(); + InternalValue value; + if (propType == PropertyType.UNDEFINED) { + // indicates null value + value = null; + } else { + value = InternalValue.valueOf(record.readString(), propType); + } + info.put(key, value); + } + } + + EventState es = createEventState(type, parentId, parentPath, childId, + childRelPath, ntName, mixins, userId); + if (info != null) { + es.setInfo(info); + } + events.add(es); + } + + /** + * Create an event state. + * + * @param type event type + * @param parentId parent id + * @param parentPath parent path + * @param childId child id + * @param childRelPath child relative path + * @param ntName node type name + * @param mixins mixins + * @param userId user id + * @return event state + */ + private EventState createEventState(int type, NodeId parentId, Path parentPath, + NodeId childId, Path childRelPath, + Name ntName, Set mixins, String userId) { + switch (type) { + case Event.NODE_ADDED: + return EventState.childNodeAdded(parentId, parentPath, childId, childRelPath, + ntName, mixins, getOrCreateSession(userId), true); + case Event.NODE_MOVED: + return EventState.nodeMoved(parentId, parentPath, childId, childRelPath, + ntName, mixins, getOrCreateSession(userId), true); + case Event.NODE_REMOVED: + return EventState.childNodeRemoved(parentId, parentPath, childId, childRelPath, + ntName, mixins, getOrCreateSession(userId), true); + case Event.PROPERTY_ADDED: + return EventState.propertyAdded(parentId, parentPath, childRelPath, + ntName, mixins, getOrCreateSession(userId), true); + case Event.PROPERTY_CHANGED: + return EventState.propertyChanged(parentId, parentPath, childRelPath, + ntName, mixins, getOrCreateSession(userId), true); + case Event.PROPERTY_REMOVED: + return EventState.propertyRemoved(parentId, parentPath, childRelPath, + ntName, mixins, getOrCreateSession(userId), true); + default: + String msg = "Unexpected event type: " + type; + throw new IllegalArgumentException(msg); + } + } + + /** + * Return a session matching a certain user id. + * + * @param userId user id + * @return session + */ + private Session getOrCreateSession(String userId) { + if (lastSession == null || !lastSession.isUserId(userId)) { + lastSession = new ClusterSession(userId); + } + return lastSession; + } + + /** + * {@inheritDoc} + */ + @Override + protected void doWrite() throws JournalException { + writeTimestampRecord(); + writeUserDataRecord(); + for (ItemState state : changes.deletedStates()) { + if (state.isNode()) { + writeNodeRecord(DELETED, (NodeState) state); + } else { + writePropertyRecord(DELETED, (PropertyState) state); + } + } + for (ItemState state : changes.modifiedStates()) { + if (state.isNode()) { + writeNodeRecord(MODIFIED, (NodeState) state); + } else { + writePropertyRecord(MODIFIED, (PropertyState) state); + } + } + for (ItemState state : changes.addedStates()) { + if (state.isNode()) { + writeNodeRecord(ADDED, (NodeState) state); + } else { + writePropertyRecord(ADDED, (PropertyState) state); + } + } + + for (EventState event : events) { + writeEventRecord(event); + } + } + + /** + * Writes the timestamp record. + * + * @throws JournalException if an error occurs. + */ + private void writeTimestampRecord() throws JournalException { + record.writeChar(DATE_IDENTIFIER); + record.writeLong(timestamp); + } + + /** + * Writes the user data record. + * + * @throws JournalException if an error occurs. + */ + private void writeUserDataRecord() throws JournalException { + if (userData != null) { + record.writeChar(USER_DATA_IDENTIFIER); + record.writeString(userData); + } + } + + /** + * Write a node record + * + * @param operation operation + * @param state node state + * @throws JournalException if an error occurs + */ + private void writeNodeRecord(int operation, NodeState state) + throws JournalException { + + record.writeChar(NODE_IDENTIFIER); + record.writeByte(operation); + record.writeNodeId(state.getNodeId()); + } + + /** + * Write a property record + * + * @param operation operation + * @param state property state + * @throws JournalException if an error occurs + */ + private void writePropertyRecord(int operation, PropertyState state) + throws JournalException { + + record.writeChar(PROPERTY_IDENTIFIER); + record.writeByte(operation); + record.writePropertyId(state.getPropertyId()); + } + + /** + * Write an event record + * + * @param event event state + * @throws JournalException if an error occurs + */ + private void writeEventRecord(EventState event) throws JournalException { + record.writeChar(EVENT_IDENTIFIER); + record.writeByte(event.getType()); + record.writeNodeId(event.getParentId()); + record.writePath(event.getParentPath()); + record.writeNodeId(event.getChildId()); + record.writePathElement(event.getChildRelPath()); + record.writeQName(event.getNodeType()); + + Set mixins = event.getMixinNames(); + record.writeInt(mixins.size()); + for (Name mixin : mixins) { + record.writeQName(mixin); + } + record.writeString(event.getUserId()); + + if (event.getType() == Event.NODE_MOVED) { + // write info map + Map info = event.getInfo(); + record.writeInt(info.size()); + for (Map.Entry entry : info.entrySet()) { + String key = entry.getKey(); + InternalValue value = entry.getValue(); + record.writeString(key); + if (value == null) { + // use undefined for null value + record.writeInt(PropertyType.UNDEFINED); + } else { + record.writeInt(value.getType()); + record.writeString(value.toString()); + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void process(ClusterRecordProcessor processor) { + processor.process(this); + } + + /** + * Return the changes. + * + * @return changes + */ + public ChangeLog getChanges() { + return changes; + } + + /** + * Return the events. + * + * @return events + */ + public List getEvents() { + return Collections.unmodifiableList(events); + } + + /** + * Returns the timestamp. + * + * @return the timestamp. + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Returns the user data. + * + * @return the user data. + */ + public String getUserData() { + return userData; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterContext.java new file mode 100644 index 00000000000..2239f37540c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterContext.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.core.cluster; + +import java.io.File; + +import org.apache.jackrabbit.core.config.ClusterConfig; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.RepositoryException; + +/** + * Initial interface passed to a ClusterNode. + */ +public interface ClusterContext { + + /** + * Return the cluster configuration. + * + * @return cluster configuration + */ + ClusterConfig getClusterConfig(); + + /** + * Return the repository home directory. + * + * @return repository home directory + */ + File getRepositoryHome(); + + /** + * Return a namespace resolver to map prefixes to URIs and vice-versa + * + * @return namespace resolver + */ + NamespaceResolver getNamespaceResolver(); + + /** + * Notifies the cluster context that some workspace update events are available + * and that it should start up a listener to receive them. + * + * @param workspace workspace name + * @throws RepositoryException if the context is unable to provide the listener + */ + void updateEventsReady(String workspace) throws RepositoryException; + + /** + * Notifies the cluster context that some workspace lock events are available + * and that it should start up a listener to receive them. + * + * @param workspace workspace name + * @throws RepositoryException if the context is unable to provide the listener + */ + void lockEventsReady(String workspace) throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterException.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterException.java new file mode 100644 index 00000000000..d195668dc13 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterException.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.core.cluster; + +/** + * The ClusterException signals an error within a cluster operation. + */ +public class ClusterException extends Exception { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public ClusterException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public ClusterException(String message, Throwable rootCause) { + super(message, rootCause); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterNode.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterNode.java new file mode 100644 index 00000000000..1fdadc95784 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterNode.java @@ -0,0 +1,1130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.cluster.WorkspaceRecord.CreateWorkspaceAction; +import org.apache.jackrabbit.core.config.ClusterConfig; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.journal.AbstractJournal; +import org.apache.jackrabbit.core.journal.InstanceRevision; +import org.apache.jackrabbit.core.journal.Journal; +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; +import org.apache.jackrabbit.core.journal.RecordConsumer; +import org.apache.jackrabbit.core.journal.RecordProducer; +import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; +import org.apache.jackrabbit.core.xml.ClonedInputSource; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import EDU.oswego.cs.dl.util.concurrent.Latch; +import EDU.oswego.cs.dl.util.concurrent.Mutex; + +/** + * Default clustered node implementation. + */ +public class ClusterNode implements Runnable, + NamespaceEventChannel, NodeTypeEventChannel, RecordConsumer, + ClusterRecordProcessor, WorkspaceEventChannel, PrivilegeEventChannel { + + /** + * System property specifying a node id to use. + */ + public static final String SYSTEM_PROPERTY_NODE_ID = "org.apache.jackrabbit.core.cluster.node_id"; + + /** + * Producer identifier. + */ + private static final String PRODUCER_ID = "JR"; + + /** + * Status constant. + */ + private static final int NONE = 0; + + /** + * Status constant. + */ + private static final int STARTED = 1; + + /** + * Status constant. + */ + private static final int STOPPED = 2; + + /** + * Audit logger. + */ + private static Logger auditLogger = LoggerFactory.getLogger("org.apache.jackrabbit.core.audit"); + + /** + * Default Logger. + */ + private static Logger log = LoggerFactory.getLogger(ClusterNode.class); + + /** + * Cluster context. + */ + private ClusterContext clusterContext; + + /** + * Cluster node id. + */ + private String clusterNodeId; + + /** + * Synchronization delay, in milliseconds. + */ + private long syncDelay; + + /** + * Stop delay, in milliseconds. + */ + private long stopDelay; + + /** + * Journal used. + */ + private Journal journal; + + /** + * Synchronization thread. + */ + private Thread syncThread; + + /** + * Mutex used when syncing. + */ + private final Mutex syncLock = new Mutex(); + + /** + * Update counter, used in displaying the number of updates in audit log. + */ + private final AtomicInteger updateCount = new AtomicInteger(); + + /** + * Latch used to communicate a stop request to the synchronization thread. + */ + private final Latch stopLatch = new Latch(); + + /** + * Sync counter, used to avoid repeated sync() calls from piling up. + * Only updated within the critical section guarded by {@link #syncLock}. + * + * @since Apache Jackrabbit 1.6 + * @see JCR-1753 + */ + private AtomicInteger syncCount = new AtomicInteger(); + + /** + * Status flag, one of {@link #NONE}, {@link #STARTED} or {@link #STOPPED}. + */ + private int status; + + /** + * Map of available lock listeners, indexed by workspace name. + */ + private final Map wspLockListeners = new HashMap(); + + /** + * Map of available update listeners, indexed by workspace name. + */ + private final Map wspUpdateListeners = new HashMap(); + + /** + * Versioning update listener. + */ + private UpdateEventListener versionUpdateListener; + + /** + * Namespace listener. + */ + private NamespaceEventListener namespaceListener; + + /** + * Create workspace listener + */ + private WorkspaceListener createWorkspaceListener; + + /** + * Node type listener. + */ + private NodeTypeEventListener nodeTypeListener; + + /** + * Privilege listener. + */ + private PrivilegeEventListener privilegeListener; + + /** + * Instance revision manager. + */ + private InstanceRevision instanceRevision; + + /** + * Our record producer. + */ + private RecordProducer producer; + + /** + * Record deserializer. + */ + private ClusterRecordDeserializer deserializer = new ClusterRecordDeserializer(); + + /** + * Flag indicating whether sync is manual. + */ + private boolean disableAutoSync; + + /** + * Initialize this cluster node. + * + * @param clusterContext The cluster context. + * @throws ClusterException if an error occurs + */ + public void init(ClusterContext clusterContext) throws ClusterException { + this.clusterContext = clusterContext; + + init(); + } + + /** + * Initialize this cluster node (overridable). + * + * @throws ClusterException if an error occurs + */ + protected void init() throws ClusterException { + ClusterConfig cc = clusterContext.getClusterConfig(); + clusterNodeId = cc.getId(); + syncDelay = cc.getSyncDelay(); + stopDelay = cc.getStopDelay(); + + try { + journal = cc.getJournal(clusterContext.getNamespaceResolver()); + instanceRevision = journal.getInstanceRevision(); + journal.register(this); + producer = journal.getProducer(PRODUCER_ID); + } catch (RepositoryException e) { + throw new ClusterException( + "Cluster initialization failed: " + this, e); + } catch (JournalException e) { + throw new ClusterException( + "Journal initialization failed: " + this, e); + } + } + + /** + * Set the stop delay, i.e. number of millseconds to wait for the + * synchronization thread to stop. + * + * @param stopDelay stop delay in milliseconds + */ + public void setStopDelay(long stopDelay) { + this.stopDelay = stopDelay; + } + + /** + * Return the stop delay. + * + * @return stop delay + * @see #setStopDelay(long) + */ + public long getStopDelay() { + return stopDelay; + } + + /** + * Disable periodic background synchronization. Used for testing purposes, only. + */ + protected void disableAutoSync() { + disableAutoSync = true; + } + + /** + * Starts this cluster node. + * + * @throws ClusterException if an error occurs + */ + public synchronized void start() throws ClusterException { + if (status == NONE) { + syncOnStartup(); + + if (!disableAutoSync) { + Thread t = new Thread(this, "ClusterNode-" + clusterNodeId); + t.setDaemon(true); + t.start(); + syncThread = t; + } + status = STARTED; + } + } + + /** + * Run loop that will sync this node after some delay. + */ + public void run() { + for (;;) { + try { + if (stopLatch.attempt(syncDelay)) { + break; + } + } catch (InterruptedException e) { + String msg = "Interrupted while waiting for stop latch."; + log.warn(msg); + } + try { + sync(); + } catch (ClusterException e) { + String msg = "Periodic sync of journal failed: " + e.getMessage(); + log.error(msg, e); + } catch (Exception e) { + String msg = "Unexpected exception while syncing of journal: " + e.getMessage(); + log.error(msg, e); + } catch (Error e) { + String msg = "Unexpected error while syncing of journal: " + e.getMessage(); + log.error(msg, e); + throw e; + } + } + } + + /** + * Synchronize contents from journal. + * + * @param startup indicates if the cluster node is syncing on startup + * or does a normal sync. + * @throws ClusterException if an error occurs + */ + private void internalSync(boolean startup) throws ClusterException { + int count = syncCount.get(); + + try { + syncLock.acquire(); + } catch (InterruptedException e) { + String msg = "Interrupted while waiting for mutex."; + throw new ClusterException(msg); + } + + try { + // JCR-1753: Only synchronize if no other thread already did so + // while we were waiting to acquire the syncLock. + if (count == syncCount.get()) { + syncCount.incrementAndGet(); + journal.sync(startup); + } + } catch (JournalException e) { + throw new ClusterException(e.getMessage(), e.getCause()); + } finally { + syncLock.release(); + } + + } + + /** + * Synchronize contents from journal. + * + * @throws ClusterException if an error occurs + */ + public void sync() throws ClusterException { + internalSync(false); + } + + /** + * Synchronize contents from journal when a {@link ClusterNode} starts up. + * + * @throws ClusterException if an error occurs + */ + public void syncOnStartup() throws ClusterException { + internalSync(true); + } + + /** + * Stops this cluster node. + */ + public synchronized void stop() { + if (status != STOPPED) { + status = STOPPED; + + stopLatch.release(); + + // Give synchronization thread some time to finish properly before + // closing down the journal (see JCR-1553) + if (syncThread != null) { + try { + syncThread.join(stopDelay); + } catch (InterruptedException e) { + String msg = "Interrupted while joining synchronization thread."; + log.warn(msg); + } + } + if (journal != null) { + journal.close(); + } + if (instanceRevision != null) { + instanceRevision.close(); + } + } + } + + /** + * Create an {@link UpdateEventChannel} for some workspace. + * + * @param workspace workspace name + * @return lock event channel + */ + public UpdateEventChannel createUpdateChannel(String workspace) { + return new WorkspaceUpdateChannel(workspace); + } + + /** + * Create a {@link LockEventChannel} for some workspace. + * + * @param workspace workspace name + * @return lock event channel + */ + public LockEventChannel createLockChannel(String workspace) { + return new WorkspaceLockChannel(workspace); + } + + /** + * Return the journal created by this cluster node. + * + * @return journal + */ + public Journal getJournal() { + return journal; + } + + //-----------------------------------------------< NamespaceEventListener > + + /** + * {@inheritDoc} + */ + public void remapped(String oldPrefix, String newPrefix, String uri) { + if (status != STARTED) { + log.info("not started: namespace operation ignored."); + return; + } + ClusterRecord record = null; + boolean succeeded = false; + + try { + record = new NamespaceRecord(oldPrefix, newPrefix, uri, producer.append()); + record.write(); + record.update(); + setRevision(record.getRevision()); + succeeded = true; + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + } finally { + if (!succeeded && record != null) { + record.cancelUpdate(); + } + } + } + + public void setListener(NamespaceEventListener listener) { + namespaceListener = listener; + } + + //------------------------------------------------< NodeTypeEventListener > + + /** + * {@inheritDoc} + */ + public void registered(Collection ntDefs) { + if (status != STARTED) { + log.info("not started: nodetype operation ignored."); + return; + } + ClusterRecord record = null; + boolean succeeded = false; + + try { + record = new NodeTypeRecord(ntDefs, true, producer.append()); + record.write(); + record.update(); + setRevision(record.getRevision()); + succeeded = true; + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + } finally { + if (!succeeded && record != null) { + record.cancelUpdate(); + } + } + } + + /** + * {@inheritDoc} + */ + public void reregistered(QNodeTypeDefinition ntDef) { + if (status != STARTED) { + log.info("not started: nodetype operation ignored."); + return; + } + ClusterRecord record = null; + boolean succeeded = false; + + try { + record = new NodeTypeRecord(ntDef, producer.append()); + record.write(); + record.update(); + setRevision(record.getRevision()); + succeeded = true; + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + } finally { + if (!succeeded && record != null) { + record.cancelUpdate(); + } + } + } + + /** + * {@inheritDoc} + */ + public void unregistered(Collection qnames) { + if (status != STARTED) { + log.info("not started: nodetype operation ignored."); + return; + } + ClusterRecord record = null; + boolean succeeded = false; + + try { + record = new NodeTypeRecord(qnames, false, producer.append()); + record.write(); + record.update(); + setRevision(record.getRevision()); + succeeded = true; + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + } finally { + if (!succeeded && record != null) { + record.cancelUpdate(); + } + } + } + + /** + * {@inheritDoc} + */ + public void setListener(NodeTypeEventListener listener) { + nodeTypeListener = listener; + } + + //----------------------------------------------< PrivilegeEventChannel >--- + /** + * {@inheritDoc} + * @see PrivilegeEventChannel#registeredPrivileges(java.util.Collection) + */ + public void registeredPrivileges(Collection definitions) { + if (status != STARTED) { + log.info("not started: nodetype operation ignored."); + return; + } + ClusterRecord record = null; + boolean succeeded = false; + + try { + record = new PrivilegeRecord(definitions, producer.append()); + record.write(); + record.update(); + setRevision(record.getRevision()); + succeeded = true; + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + } finally { + if (!succeeded && record != null) { + record.cancelUpdate(); + } + } + } + + public void setListener(PrivilegeEventListener listener) { + privilegeListener = listener; + } + + //-------------------------------------------------------------------------- + /** + * Workspace update channel. + */ + class WorkspaceUpdateChannel implements UpdateEventChannel { + + /** + * Attribute name used to store record. + */ + private static final String ATTRIBUTE_RECORD = "record"; + + /** + * Attribute name used to store the size of the update. + */ + private static final String ATTRIBUTE_UPDATE_SIZE = "updateSize"; + + /** + * Workspace name. + */ + private final String workspace; + + /** + * Create a new instance of this class. + * + * @param workspace workspace name + */ + public WorkspaceUpdateChannel(String workspace) { + this.workspace = workspace; + } + + /** + * {@inheritDoc} + */ + public void updateCreated(Update update) throws ClusterException { + if (status != STARTED) { + log.info("not started: update create ignored."); + return; + } + try { + Record record = producer.append(); + update.setAttribute(ATTRIBUTE_RECORD, record); + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + throw new ClusterException(msg, e); + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry: " + + e.getMessage(); + throw new ClusterException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void updatePrepared(Update update) throws ClusterException { + if (status != STARTED) { + log.info("not started: update prepare ignored."); + return; + } + Record record = (Record) update.getAttribute(ATTRIBUTE_RECORD); + if (record == null) { + String msg = "No record created."; + log.warn(msg); + return; + } + + List events = update.getEvents(); + ChangeLog changes = update.getChanges(); + boolean succeeded = false; + + try { + ChangeLogRecord clr = new ChangeLogRecord(changes, events, + record, workspace, update.getTimestamp(), + update.getUserData()); + clr.write(); + succeeded = true; + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + throw new ClusterException(msg, e); + } catch (Throwable e) { + String msg = "Unexpected error while preparing log entry."; + throw new ClusterException(msg, e); + } finally { + if (!succeeded) { + record.cancelUpdate(); + update.setAttribute(ATTRIBUTE_RECORD, null); + } + } + } + + /** + * {@inheritDoc} + */ + public void updateCommitted(Update update, String path) { + Record record = (Record) update.getAttribute(ATTRIBUTE_RECORD); + if (record == null) { + if (status == STARTED) { + log.warn("No record prepared."); + } else { + log.info("not started: update commit ignored."); + } + return; + } + try { + + long recordRevision = record.getRevision(); + setRevision(recordRevision); + + long journalUpdateSize = record.update(); + + log.debug("Stored record '{}' to Journal ({})", recordRevision, journalUpdateSize); + + Object updateSizeValue = update.getAttribute(ATTRIBUTE_UPDATE_SIZE); + long updateSize = updateSizeValue != null? (Long)updateSizeValue : 0; + updateCount.compareAndSet(Integer.MAX_VALUE, 0); + + auditLogger.info("[{}] {} {} ({})", new Object[]{updateCount.incrementAndGet(), + record.getRevision(), path, updateSize}); + + } catch (JournalException e) { + String msg = "Unable to commit log entry."; + log.error(msg, e); + } catch (Throwable e) { + String msg = "Unexpected error while committing log entry."; + log.error(msg, e); + } finally { + update.setAttribute(ATTRIBUTE_RECORD, null); + } + } + + /** + * {@inheritDoc} + */ + public void updateCancelled(Update update) { + Record record = (Record) update.getAttribute(ATTRIBUTE_RECORD); + if (record != null) { + record.cancelUpdate(); + update.setAttribute(ATTRIBUTE_RECORD, null); + } + } + + /** + * {@inheritDoc} + */ + public void setListener(UpdateEventListener listener) { + if (workspace == null) { + versionUpdateListener = listener; + if (journal instanceof AbstractJournal && + versionUpdateListener instanceof InternalVersionManagerImpl) { + ((AbstractJournal) journal).setInternalVersionManager( + (InternalVersionManagerImpl) versionUpdateListener); + } + } else { + wspUpdateListeners.remove(workspace); + if (listener != null) { + wspUpdateListeners.put(workspace, listener); + } + } + } + } + + /** + * Workspace lock channel. + */ + class WorkspaceLockChannel implements LockEventChannel { + + /** + * Workspace name. + */ + private final String workspace; + + /** + * Create a new instance of this class. + * + * @param workspace workspace name + */ + public WorkspaceLockChannel(String workspace) { + this.workspace = workspace; + } + + /** + * {@inheritDoc} + */ + public ClusterOperation create(NodeId nodeId, boolean deep, String owner) { + if (status != STARTED) { + log.info("not started: lock operation ignored."); + return null; + } + try { + ClusterRecord record = new LockRecord(nodeId, deep, owner, + producer.append(), workspace); + return new DefaultClusterOperation(ClusterNode.this, record); + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + return null; + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + return null; + } + } + + /** + * {@inheritDoc} + */ + public ClusterOperation create(NodeId nodeId) { + if (status != STARTED) { + log.info("not started: unlock operation ignored."); + return null; + } + try { + ClusterRecord record = new LockRecord(nodeId, producer.append(), + workspace); + return new DefaultClusterOperation(ClusterNode.this, record); + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + return null; + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + return null; + } + } + + /** + * {@inheritDoc} + */ + public void setListener(LockEventListener listener) { + wspLockListeners.remove(workspace); + if (listener != null) { + wspLockListeners.put(workspace, listener); + } + } + } + + //-------------------------------------------------------< RecordConsumer > + + /** + * {@inheritDoc} + */ + public String getId() { + return PRODUCER_ID; + } + + /** + * {@inheritDoc} + */ + public long getRevision() { + try { + return instanceRevision.get(); + } catch (JournalException e) { + log.warn("Unable to return current revision.", e); + return Long.MAX_VALUE; + } + } + + /** + * {@inheritDoc} + */ + public void consume(Record record) { + log.info("Processing revision: " + record.getRevision()); + + try { + deserializer.deserialize(record).process(this); + } catch (JournalException e) { + String msg = "Unable to read revision '" + record.getRevision() + "'."; + log.error(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void setRevision(long revision) { + try { + instanceRevision.set(revision); + } catch (JournalException e) { + log.warn("Unable to set current revision to " + revision + ".", e); + } + } + + //--------------------------------------------------- ClusterRecordProcessor + + /** + * {@inheritDoc} + */ + public void process(ChangeLogRecord record) { + String workspace = record.getWorkspace(); + + UpdateEventListener listener = null; + if (workspace != null) { + listener = wspUpdateListeners.get(workspace); + if (listener == null) { + try { + clusterContext.updateEventsReady(workspace); + } catch (RepositoryException e) { + String msg = "Error making update listener for workspace " + + workspace + " online: " + e.getMessage(); + log.warn(msg); + } + listener = wspUpdateListeners.get(workspace); + if (listener == null) { + String msg = "Update listener unavailable for workspace: " + workspace; + log.error(msg); + return; + } + } + } else { + if (versionUpdateListener != null) { + listener = versionUpdateListener; + } else { + String msg = "Version update listener unavailable."; + log.error(msg); + return; + } + } + try { + List eventStates = record.getEvents(); + + String path = getFirstUserId(eventStates) + + "@" + workspace + + ":" + EventState.getCommonPath(eventStates, null); + + updateCount.compareAndSet(Integer.MAX_VALUE, 0); + auditLogger.info("[{}] {} {}", new Object[]{updateCount.incrementAndGet(), + record.getRevision(), path}); + + listener.externalUpdate(record.getChanges(), eventStates, + record.getTimestamp(), record.getUserData()); + } catch (RepositoryException e) { + String msg = "Unable to deliver update events: " + e.getMessage(); + log.error(msg); + if (e.getCause() instanceof IllegalStateException) { + throw (IllegalStateException) e.getCause(); + } + } + } + + /** + * {@inheritDoc} + */ + public void process(LockRecord record) { + String workspace = record.getWorkspace(); + + LockEventListener listener = wspLockListeners.get(workspace); + if (listener == null) { + try { + clusterContext.lockEventsReady(workspace); + } catch (RepositoryException e) { + String msg = "Unable to make lock listener for workspace " + + workspace + " online: " + e.getMessage(); + log.warn(msg); + } + listener = wspLockListeners.get(workspace); + if (listener == null) { + String msg = "Lock channel unavailable for workspace: " + workspace; + log.error(msg); + return; + } + } + try { + if (record.isLock()) { + listener.externalLock(record.getNodeId(), record.isDeep(), + record.getOwner()); + } else { + listener.externalUnlock(record.getNodeId()); + } + } catch (RepositoryException e) { + String msg = "Unable to deliver lock event: " + e.getMessage(); + log.error(msg); + if (e.getCause() instanceof IllegalStateException) { + throw (IllegalStateException) e.getCause(); + } + } + } + + /** + * {@inheritDoc} + */ + public void process(NamespaceRecord record) { + if (namespaceListener == null) { + String msg = "Namespace listener unavailable."; + log.error(msg); + return; + } + try { + namespaceListener.externalRemap(record.getOldPrefix(), + record.getNewPrefix(), record.getUri()); + } catch (RepositoryException e) { + String msg = "Unable to deliver namespace operation: " + e.getMessage(); + log.error(msg); + } + } + + /** + * {@inheritDoc} + */ + public void process(NodeTypeRecord record) { + if (nodeTypeListener == null) { + String msg = "NodeType listener unavailable."; + log.error(msg); + return; + } + Collection coll = record.getCollection(); + try { + switch (record.getOperation()) { + case NodeTypeRecord.REGISTER: + nodeTypeListener.externalRegistered(coll); + break; + case NodeTypeRecord.UNREGISTER: + nodeTypeListener.externalUnregistered(coll); + break; + case NodeTypeRecord.REREGISTER: + QNodeTypeDefinition ntd = (QNodeTypeDefinition) coll.iterator().next(); + nodeTypeListener.externalReregistered(ntd); + break; + } + } catch (InvalidNodeTypeDefException e) { + String msg = "Unable to deliver node type operation: " + e.getMessage(); + log.error(msg); + } catch (RepositoryException e) { + String msg = "Unable to deliver node type operation: " + e.getMessage(); + log.error(msg); + } + } + + public void process(PrivilegeRecord record) { + if (privilegeListener == null) { + String msg = "Privilege listener unavailable."; + log.error(msg); + return; + } + try { + privilegeListener.externalRegisteredPrivileges(record.getDefinitions()); + } catch (RepositoryException e) { + String msg = "Unable to deliver privilege registration operation: " + e.getMessage(); + log.error(msg); + } + } + + public void process(WorkspaceRecord record) { + if (createWorkspaceListener == null) { + String msg = "Create Workspace listener unavailable."; + log.error(msg); + return; + } + try { + if (record.getActionType() == WorkspaceRecord.CREATE_WORKSPACE_ACTION_TYPE) { + CreateWorkspaceAction action = record.getCreateWorkspaceAction(); + createWorkspaceListener.externalWorkspaceCreated(record.getWorkspace(), action.getInputSource()); + } + } catch (RepositoryException e) { + String msg = "Unable to create workspace: " + + e.getMessage(); + log.error(msg); + } + } + + // -----------------------------------------------< CreateWorkspaceChannel > + + public void setListener(WorkspaceListener listener) { + createWorkspaceListener = listener; + } + + public void workspaceCreated(String workspaceName, + ClonedInputSource inputSource) { + if (status != STARTED) { + log.info("not started: namespace operation ignored."); + return; + } + ClusterRecord record = null; + boolean succeeded = false; + + try { + record = new WorkspaceRecord(workspaceName, inputSource, producer.append()); + record.write(); + record.update(); + setRevision(record.getRevision()); + succeeded = true; + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + } finally { + if (!succeeded && record != null) { + record.cancelUpdate(); + } + } + } + + /** + * Invoked when a cluster operation has ended. If successful, + * attempts to fill the journal record and update it, otherwise cancels + * the update. + * + * @param operation cluster operation + * @param successful true if the operation was successful and + * the journal record should be updated; + * false to revoke changes + */ + public void ended(DefaultClusterOperation operation, boolean successful) { + ClusterRecord record = operation.getRecord(); + boolean succeeded = false; + + try { + if (successful) { + record.write(); + record.update(); + setRevision(record.getRevision()); + succeeded = true; + } + } catch (JournalException e) { + String msg = "Unable to create log entry: " + e.getMessage(); + log.error(msg); + } catch (Throwable e) { + String msg = "Unexpected error while creating log entry."; + log.error(msg, e); + } finally { + if (!succeeded) { + record.cancelUpdate(); + } + } + } + + private String getFirstUserId(List eventStates) { + if (eventStates == null || eventStates.isEmpty()) { + return ""; + } + return eventStates.get(0).getUserId(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterOperation.java new file mode 100644 index 00000000000..798a0112aaf --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterOperation.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.core.cluster; + +/** + * Cluster operation that will be written to the cluster's journal and + * ultimately processed by other instances. + */ +public interface ClusterOperation { + + /** + * Called when the operation has been finished. The passed flag indicates + * whether or not the operation was successful. + * + * @param successful true if the operation ended successfully; + * false otherwise + */ + void ended(boolean successful); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecord.java new file mode 100644 index 00000000000..96ffbdec886 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecord.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; + +/** + * Base cluster record. Used to serialize and deserialize cluster operations + * using journal records. + */ +public abstract class ClusterRecord { + + /** + * End marker. + */ + protected static final char END_MARKER = '\0'; + + /** + * Journal record. + */ + protected final Record record; + + /** + * Workspace name. + */ + protected String workspace; + + /** + * Create a new instance of this class. + * + * @param record journal record + * @param workspace workspace + */ + protected ClusterRecord(Record record, String workspace) { + this.record = record; + this.workspace = workspace; + } + + /** + * Create a new instance of this class. Used for records that do not + * have a workspace name. + * + * @param record journal record + */ + protected ClusterRecord(Record record) { + this(record, null); + } + + /** + * Deserialize this record. + * + * @throws JournalException if an error occurs + */ + public final void read() throws JournalException { + doRead(); + + readEndMarker(); + } + + /** + * Deserialize this record. Subclass responsibility. + * + * @throws JournalException if an error occurs + */ + protected abstract void doRead() throws JournalException; + + /** + * Serialize this record. + * + * @throws JournalException if an error occurs + */ + public final void write() throws JournalException { + record.writeString(workspace); + + doWrite(); + + record.writeChar(END_MARKER); + } + + /** + * Serialize this record. Subclass responsibility. + * + * @throws JournalException if an error occurs + */ + protected abstract void doWrite() throws JournalException; + + /** + * Read end marker. + * + * @throws JournalException if an error occurs + */ + protected void readEndMarker() throws JournalException { + char c = record.readChar(); + if (c != END_MARKER) { + String msg = "Expected end marker, found: " + c; + throw new JournalException(msg); + } + } + + /** + * Process this record, calling the appropriate process + * method. + * + * @param processor processor + */ + public abstract void process(ClusterRecordProcessor processor); + + /** + * Update the record. + * + * @throws JournalException if an error occurs + * @see Record#update() + */ + public void update() throws JournalException { + record.update(); + } + + /** + * Cancel updating the record. + * + * @see Record#cancelUpdate() + */ + public void cancelUpdate() { + record.cancelUpdate(); + } + + /** + * Return the record revision. + * + * @return record revision + * @see Record#getRevision() + */ + public long getRevision() { + return record.getRevision(); + } + + /** + * Return the workspace name. + * + * @return workspace name + */ + public String getWorkspace() { + return workspace; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecordDeserializer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecordDeserializer.java new file mode 100644 index 00000000000..5b5ebe649ef --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecordDeserializer.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.core.cluster; + +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; + +/** + * Deserialize a record written by a ClusterNode. + */ +public class ClusterRecordDeserializer { + + /** + * Deserialize a cluster record. + * + * @param record basic record containing a cluster record + * @return deserialized cluster record + * @throws JournalException if an error occurs + */ + public ClusterRecord deserialize(Record record) throws JournalException { + ClusterRecord clusterRecord; + + String workspace = record.readString(); + char c = record.readChar(); + switch (c) { + case ChangeLogRecord.NODE_IDENTIFIER: + case ChangeLogRecord.PROPERTY_IDENTIFIER: + case ChangeLogRecord.EVENT_IDENTIFIER: + case ChangeLogRecord.DATE_IDENTIFIER: + clusterRecord = new ChangeLogRecord(c, record, workspace); + clusterRecord.read(); + break; + case LockRecord.IDENTIFIER: + clusterRecord = new LockRecord(record, workspace); + clusterRecord.read(); + break; + case NamespaceRecord.IDENTIFIER: + clusterRecord = new NamespaceRecord(record); + clusterRecord.read(); + break; + case NodeTypeRecord.IDENTIFIER: + clusterRecord = new NodeTypeRecord(record); + clusterRecord.read(); + break; + case WorkspaceRecord.IDENTIFIER: + clusterRecord = new WorkspaceRecord(record); + clusterRecord.read(); + break; + case PrivilegeRecord.IDENTIFIER: + clusterRecord = new PrivilegeRecord(record); + clusterRecord.read(); + break; + case ClusterRecord.END_MARKER: + // JCR-1813: Invalid journal records during XATransactions + // Some journal records may be empty due to JCR-1813 and other + // issues. We handle such cases with this dummy sentinel record. + clusterRecord = new ClusterRecord(record) { + @Override + protected void doRead() { + } + @Override + protected void doWrite() { + } + @Override + public void process(ClusterRecordProcessor processor) { + } + }; + break; + default: + throw new JournalException("Unknown record identifier: " + c); + } + return clusterRecord; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecordProcessor.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecordProcessor.java new file mode 100644 index 00000000000..2efe8f8f623 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterRecordProcessor.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.core.cluster; + +/** + * Cluster record processor. Pass an implementation of this interface to a + * ClusterRecord and it will call back the appropriate + * process method. + * + * @see ClusterRecord#process(ClusterRecordProcessor) + */ +public interface ClusterRecordProcessor { + + /** + * Process a change log record. + * + * @param record change log record + */ + void process(ChangeLogRecord record); + + /** + * Process a lock record. + * + * @param record lock record + */ + void process(LockRecord record); + + /** + * Process a namespace record. + * + * @param record namespace record + */ + void process(NamespaceRecord record); + + /** + * Process a node type record + * + * @param record node type record + */ + void process(NodeTypeRecord record); + + /** + * Process a privilege record + * + * @param record privilege record + */ + void process(PrivilegeRecord record); + + /** + * Process a workspace record + * @param record workspace record + */ + void process(WorkspaceRecord record); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterSession.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterSession.java new file mode 100644 index 00000000000..2c9365355c5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/ClusterSession.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.io.InputStream; +import java.io.OutputStream; + +import javax.jcr.Credentials; +import javax.jcr.Item; +import javax.jcr.Node; +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 org.xml.sax.ContentHandler; + +/** + * Represents the session that has made some changes on another node in the + * cluster. The only method currently implemented is {@link #getUserID()}. + */ +class ClusterSession implements Session { + + /** + * User id to represent. + */ + private final String userId; + + /** + * Create a new instance of this class. + * + * @param userId user id + */ + public ClusterSession(String userId) { + this.userId = userId; + } + + /** + * Returns true if the given userId is the same as + * the {@link #userId} of this session. + * + * @param userId the user id or null. + * @return true if they are the same; false + * otherwise. + */ + boolean isUserId(String userId) { + if (userId == null) { + return this.userId == null; + } else { + return userId.equals(this.userId); + } + } + + /** + * {@inheritDoc} + */ + public String getUserID() { + return userId; + } + + /** + * {@inheritDoc} + */ + public Repository getRepository() { + return null; + } + + /** + * {@inheritDoc} + */ + public Object getAttribute(String s) { + return null; + } + + /** + * {@inheritDoc} + */ + public String[] getAttributeNames() { + return new String[0]; + } + + /** + * {@inheritDoc} + */ + public Workspace getWorkspace() { + return null; + } + + /** + * {@inheritDoc} + */ + public Session impersonate(Credentials credentials) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public Node getRootNode() throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public Node getNodeByUUID(String s) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public Item getItem(String s) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean itemExists(String s) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void move(String s, String s1) throws UnsupportedRepositoryOperationException { + + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void save() throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void refresh(boolean b) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean hasPendingChanges() throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public ValueFactory getValueFactory() throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void checkPermission(String s, String s1) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public ContentHandler getImportContentHandler(String s, int i) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void importXML(String s, InputStream inputStream, int i) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void exportSystemView(String s, ContentHandler contentHandler, boolean b, boolean b1) + throws UnsupportedRepositoryOperationException { + + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void exportSystemView(String s, OutputStream outputStream, boolean b, boolean b1) + throws UnsupportedRepositoryOperationException { + + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void exportDocumentView(String s, ContentHandler contentHandler, boolean b, boolean b1) + throws UnsupportedRepositoryOperationException { + + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void exportDocumentView(String s, OutputStream outputStream, boolean b, boolean b1) + throws UnsupportedRepositoryOperationException { + + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setNamespacePrefix(String s, String s1) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public String[] getNamespacePrefixes() throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public String getNamespaceURI(String s) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public String getNamespacePrefix(String s) throws UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * {@inheritDoc} + */ + public void logout() { + } + + /** + * {@inheritDoc} + */ + public boolean isLive() { + return true; + } + + /** + * {@inheritDoc} + */ + public void addLockToken(String s) { + } + + /** + * {@inheritDoc} + */ + public String[] getLockTokens() { + return new String[0]; + } + + /** + * {@inheritDoc} + */ + public void removeLockToken(String s) { + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof ClusterSession) { + ClusterSession other = (ClusterSession) obj; + return isUserId(other.userId); + } + return false; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return userId.hashCode(); + } + + public AccessControlManager getAccessControlManager() + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public Node getNode(String path) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public Node getNodeByIdentifier(String identifier) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public Property getProperty(String arg0) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public RetentionManager getRetentionManager() + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public boolean hasCapability(String arg0, Object arg1, Object[] arg2) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public boolean hasPermission(String arg0, String arg1) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public boolean nodeExists(String path) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public boolean propertyExists(String path) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public void removeItem(String path) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/DefaultClusterOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/DefaultClusterOperation.java new file mode 100644 index 00000000000..0e348e3ddc1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/DefaultClusterOperation.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.core.cluster; + +/** + * Default cluster operation implementation. + */ +public class DefaultClusterOperation implements ClusterOperation { + + /** + * Cluster node. + */ + private final ClusterNode clusterNode; + + /** + * Cluster record. + */ + private final ClusterRecord record; + + /** + * Create an instance of this class. + * + * @param clusterNode cluster node + * @param record cluster record + */ + public DefaultClusterOperation(ClusterNode clusterNode, + ClusterRecord record) { + + this.clusterNode = clusterNode; + this.record = record; + } + + /** + * {@inheritDoc} + */ + public void ended(boolean successful) { + clusterNode.ended(this, successful); + } + + /** + * Return the record. + * + * @return the record + */ + public ClusterRecord getRecord() { + return record; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockEventChannel.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockEventChannel.java new file mode 100644 index 00000000000..79fbe277302 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockEventChannel.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.core.cluster; + +import org.apache.jackrabbit.core.id.NodeId; + +/** + * Event channel used to transmit lock events. + */ +public interface LockEventChannel { + + /** + * Create a new cluster operation that should be used to inform other + * instances in the cluster. Called when a node is about to be + * locked. + * + * @param nodeId node id + * @param deep flag indicating whether lock is deep + * @param owner lock owner + * @return cluster operation or null if the cluster node + * is not started or some error occurs + */ + ClusterOperation create(NodeId nodeId, boolean deep, String owner); + + /** + * Create a new cluster operation that should be used to inform other + * instances in the cluster. Called when a node has been unlocked. + * + * @param nodeId node id + * @return cluster operation or null if the cluster node + * is not started or some error occurs + */ + ClusterOperation create(NodeId nodeId); + + /** + * Set listener that will receive information about incoming, external lock events. + * + * @param listener lock event listener + */ + void setListener(LockEventListener listener); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockEventListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockEventListener.java new file mode 100644 index 00000000000..6d809eb12e4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockEventListener.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.core.cluster; + +import org.apache.jackrabbit.core.id.NodeId; + +import javax.jcr.RepositoryException; + +/** + * Interface used to receive information about incoming, external lock events. + */ +public interface LockEventListener { + + /** + * Handle an external lock operation. + * + * @param nodeId node id + * @param isDeep true if the lock is deep; + * false otherwise + * @param lockOwner lock owner + * @throws RepositoryException if the lock cannot be processed + */ + void externalLock(NodeId nodeId, boolean isDeep, String lockOwner) throws RepositoryException; + + /** + * Handle an external unlock operation. + * + * @param nodeId node id + * @throws RepositoryException if the unlock cannot be processed + */ + void externalUnlock(NodeId nodeId) throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockRecord.java new file mode 100644 index 00000000000..e29c3cf7f96 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/LockRecord.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.core.cluster; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; + +/** + * Cluster record representing a lock or unlock operation. + */ +public class LockRecord extends ClusterRecord { + + /** + * Identifier: LOCK. + */ + static final char IDENTIFIER = 'L'; + + /** + * Node id. + */ + private NodeId nodeId; + + /** + * Flag indicating whether this is a lock or an unlock. + */ + private boolean isLock; + + /** + * Flag indicating whether the lock is deep. + */ + private boolean isDeep; + + /** + * Lock owner. + */ + private String lockOwner; + + /** + * Create a new instance of this class. Used when a lock operation should + * be serialized. + * + * @param nodeId node id + * @param isDeep flag indicating whether the lock is deep + * @param lockOwner the name of the lock owner. + * @param record journal record + * @param workspace workspace + */ + public LockRecord(NodeId nodeId, boolean isDeep, String lockOwner, + Record record, String workspace) { + super(record, workspace); + + this.nodeId = nodeId; + this.isLock = true; + this.isDeep = isDeep; + this.lockOwner = lockOwner; + } + + /** + * Create a new instance of this class. Used when an unlock operation should + * be serialized. + * + * @param nodeId node id + * @param record journal record + * @param workspace workspace + */ + public LockRecord(NodeId nodeId, Record record, String workspace) { + super(record, workspace); + + this.nodeId = nodeId; + this.isLock = false; + } + + /** + * Create a new instance of this class. Used when a deserializing either a + * lock or an unlock operation. + * + * @param record journal record + * @param workspace workspace + */ + LockRecord(Record record, String workspace) { + super(record, workspace); + } + + /** + * {@inheritDoc} + */ + @Override + protected void doRead() throws JournalException { + nodeId = record.readNodeId(); + isLock = record.readBoolean(); + if (isLock) { + isDeep = record.readBoolean(); + lockOwner = record.readString(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void doWrite() throws JournalException { + record.writeChar(IDENTIFIER); + record.writeNodeId(nodeId); + record.writeBoolean(isLock); + if (isLock) { + record.writeBoolean(isDeep); + record.writeString(lockOwner); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void process(ClusterRecordProcessor processor) { + processor.process(this); + } + + /** + * Return the node id. + * + * @return node id + */ + public NodeId getNodeId() { + return nodeId; + } + + /** + * Return a flag indicating whether this is a lock or an unlock operation. + * + * @return true if this is a lock operation; + * false if this is an unlock operation + */ + public boolean isLock() { + return isLock; + } + + /** + * Return a flag indicating whether the lock is deep. + * + * @return true if the lock is deep; + * false otherwise + */ + public boolean isDeep() { + return isDeep; + } + + /** + * Return the user id associated with the lock operation. + * + * @return user id + * @deprecated User {@link #getOwner()} instead. + */ + public String getUserId() { + return lockOwner; + } + + /** + * Return the lock owner associated with the lock operation. + * + * @return lock owner associated with the lock operation. + */ + public String getOwner() { + return lockOwner; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceEventChannel.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceEventChannel.java new file mode 100644 index 00000000000..1eebe0f5341 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceEventChannel.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.core.cluster; + +/** + * Event channel used to transmit namespace registry operations. + */ +public interface NamespaceEventChannel { + + /** + * Called when a namespace has been remapped. + * + * @param oldPrefix old prefix. if null this is a fresh mapping + * @param newPrefix new prefix. if null this is an unmap operation + * @param uri uri to map prefix to + */ + void remapped(String oldPrefix, String newPrefix, String uri); + + /** + * Set listener that will receive information about incoming, external namespace events. + * + * @param listener namespace event listener + */ + void setListener(NamespaceEventListener listener); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceEventListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceEventListener.java new file mode 100644 index 00000000000..0b40842343b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceEventListener.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.core.cluster; + +import javax.jcr.RepositoryException; + +/** + * Interface used to receive information about incoming, external namespace registry events. + */ +public interface NamespaceEventListener { + + /** + * Called when a namespace has been externally remapped. + * + * @param oldPrefix old prefix. if null this is a fresh mapping + * @param newPrefix new prefix. if null this is an unmap operation + * @param uri uri to map prefix to + * @throws RepositoryException if an error occurs + */ + void externalRemap(String oldPrefix, String newPrefix, String uri) + throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceRecord.java new file mode 100644 index 00000000000..335c3030482 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NamespaceRecord.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.core.cluster; + +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; + +/** + * Cluster record representing a namespace registration, reregistration or + * unregistration. + */ +public class NamespaceRecord extends ClusterRecord { + + /** + * Identifier: NAMESPACE. + */ + static final char IDENTIFIER = 'S'; + + /** + * Old prefix. + */ + private String oldPrefix; + + /** + * New prefix. + */ + private String newPrefix; + + /** + * URI. + */ + private String uri; + + /** + * Create a new instance of this class. Used when serializing a namespace + * operation. + * + * @param oldPrefix old prefix + * @param newPrefix new prefix + * @param uri URI + * @param record journal record + */ + public NamespaceRecord(String oldPrefix, String newPrefix, String uri, + Record record) { + super(record); + + this.oldPrefix = oldPrefix; + this.newPrefix = newPrefix; + this.uri = uri; + } + + /** + * Create a new instance of this class. Used when deserializing. + * + * @param record journal record + */ + NamespaceRecord(Record record) { + super(record); + } + + /** + * {@inheritDoc} + */ + @Override + protected void doRead() throws JournalException { + oldPrefix = record.readString(); + newPrefix = record.readString(); + uri = record.readString(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void doWrite() throws JournalException { + record.writeChar(IDENTIFIER); + record.writeString(oldPrefix); + record.writeString(newPrefix); + record.writeString(uri); + } + + /** + * {@inheritDoc} + */ + @Override + public void process(ClusterRecordProcessor processor) { + processor.process(this); + } + + /** + * Return the old prefix. + * + * @return old prefix + */ + public String getOldPrefix() { + return oldPrefix; + } + + /** + * Return the new prefix. + * + * @return new prefix + */ + public String getNewPrefix() { + return newPrefix; + } + + /** + * Return the URI. + * @return URI + */ + public String getUri() { + return uri; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeEventChannel.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeEventChannel.java new file mode 100644 index 00000000000..f45bc9077c4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeEventChannel.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.core.cluster; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +import java.util.Collection; + +/** + * Event channel used to transmit nodetype registry operations. + */ +public interface NodeTypeEventChannel { + + /** + * Called when one or more node types have been registered. + * + * @param ntDefs collection of node type definitions + */ + void registered(Collection ntDefs); + + /** + * Called when a node types has been re-registered. + * + * @param ntDef node type definition + */ + void reregistered(QNodeTypeDefinition ntDef); + + /** + * Called when one or more node types have been unregistered. + * + * @param ntNames collection of node type qnames + */ + void unregistered(Collection ntNames); + + /** + * Set listener that will receive information about incoming, external node type events. + * + * @param listener node type event listener + */ + void setListener(NodeTypeEventListener listener); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeEventListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeEventListener.java new file mode 100644 index 00000000000..18ac63269cd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeEventListener.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.core.cluster; + +import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import java.util.Collection; + +/** + * Interface used to receive information about incoming, external node type registry events. + */ +public interface NodeTypeEventListener { + + /** + * Called when one or more node types have been externally registered. + * + * @param ntDefs node type definitions + * @throws RepositoryException if an error occurs + * @throws InvalidNodeTypeDefException if the node type definition is invalid + */ + void externalRegistered(Collection ntDefs) + throws RepositoryException, InvalidNodeTypeDefException; + + /** + * Called when a node type has been externally re-registered. + * + * @param ntDef node type definition + * @throws RepositoryException if an error occurs + * @throws NoSuchNodeTypeException if the node type had not yet been registered + * @throws InvalidNodeTypeDefException if the node type definition is invalid + */ + void externalReregistered(QNodeTypeDefinition ntDef) + throws NoSuchNodeTypeException, InvalidNodeTypeDefException, RepositoryException; + + /** + * Called when one or more node types have been externally unregistered. + * + * @param ntNames node type qnames + * @throws RepositoryException if an error occurs + * @throws NoSuchNodeTypeException if a node type is already unregistered + */ + void externalUnregistered(Collection ntNames) + throws RepositoryException, NoSuchNodeTypeException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeRecord.java new file mode 100644 index 00000000000..1fef58ec173 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/NodeTypeRecord.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.core.cluster; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; + +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +/** + * Cluster record representing a node type registration, re-registration or + * unregistration. + */ +public class NodeTypeRecord extends ClusterRecord { + + /** + * Operation type: registration. + */ + public static final int REGISTER = 1; + + /** + * Operation type: re-registration. + */ + public static final int REREGISTER = 2; + + /** + * Operation type: unregistration. + */ + public static final int UNREGISTER = 3; + + /** + * Identifier: NODETYPE. + */ + static final char IDENTIFIER = 'T'; + + /** + * Bit indicating this is a registration operation. + */ + private static final int NTREG_REGISTER = 0; + + /** + * Bit indicating this is a reregistration operation. + */ + private static final int NTREG_REREGISTER = (1 << 30); + + /** + * Bit indicating this is an unregistration operation. + */ + private static final int NTREG_UNREGISTER = (1 << 31); + + /** + * Mask used in node type registration operations. + */ + private static final int NTREG_MASK = (NTREG_REREGISTER | NTREG_UNREGISTER); + + /** + * Operation type. + */ + private int operation; + + /** + * Collection of node type definitions or node type names. + */ + private Collection collection; + + /** + * Create a new instance of this class. Used when serializing a node type + * registration or unregistration. + * + * @param collection collection of node types definitions or node type names + * @param isRegister true if this is a registration; + * false if this is a unregistration + * @param record journal record + */ + public NodeTypeRecord(Collection collection, boolean isRegister, Record record) { + super(record); + + this.collection = collection; + this.operation = isRegister ? REGISTER : UNREGISTER; + } + + /** + * Create a new instance of this class. Used when serializing a node type + * re-registration. + * + * @param ntDef node type definition + * @param record journal record + */ + public NodeTypeRecord(QNodeTypeDefinition ntDef, Record record) { + super(record); + + this.collection = new ArrayList(); + this.collection.add(ntDef); + this.operation = REREGISTER; + } + + /** + * Create a new instance of this class. Used when deseralizing a node type + * registration, re-registration or unregistration. + * + * @param record journal record + */ + NodeTypeRecord(Record record) { + super(record); + } + + /** + * {@inheritDoc} + */ + @Override + protected void doRead() throws JournalException { + int size = record.readInt(); + int opcode = size & NTREG_MASK; + size &= ~NTREG_MASK; + + switch (opcode) { + case NTREG_REGISTER: + operation = REGISTER; + collection = new HashSet(); + for (int i = 0; i < size; i++) { + collection.add(record.readNodeTypeDef()); + } + break; + case NTREG_REREGISTER: + operation = REREGISTER; + collection = new HashSet(); + collection.add(record.readNodeTypeDef()); + break; + case NTREG_UNREGISTER: + operation = UNREGISTER; + collection = new HashSet(); + for (int i = 0; i < size; i++) { + collection.add(record.readQName()); + } + break; + default: + String msg = "Unknown opcode: " + opcode; + throw new JournalException(msg); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void doWrite() throws JournalException { + record.writeChar(IDENTIFIER); + + int size = collection.size(); + size |= getBitMask(); + record.writeInt(size); + + Iterator iter = collection.iterator(); + while (iter.hasNext()) { + if (operation == UNREGISTER) { + record.writeQName((Name) iter.next()); + } else { + record.writeNodeTypeDef((QNodeTypeDefinition) iter.next()); + } + } + } + + /** + * Return the bit mask associated with an operation type. + * + * @return bit mask + */ + private int getBitMask() { + switch (operation) { + case REGISTER: + return NTREG_REGISTER; + case UNREGISTER: + return NTREG_UNREGISTER; + case REREGISTER: + return NTREG_REREGISTER; + } + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public void process(ClusterRecordProcessor processor) { + processor.process(this); + } + + /** + * Return the operation type. + * @return REGISTER, REREGISTER or + * UNREGISTER + */ + public int getOperation() { + return operation; + } + + /** + * Return the collection of node type definitions or node type names. + * + * @return unmodifiable collection + */ + public Collection getCollection() { + return Collections.unmodifiableCollection(collection); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeEventChannel.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeEventChannel.java new file mode 100644 index 00000000000..63cb1572088 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeEventChannel.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.core.cluster; + +import org.apache.jackrabbit.spi.PrivilegeDefinition; + +import java.util.Collection; + +/** + * PrivilegeEventChannel... + */ +public interface PrivilegeEventChannel { + + void registeredPrivileges(Collection definitions); + + void setListener(PrivilegeEventListener listener); +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeEventListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeEventListener.java new file mode 100644 index 00000000000..d00a8ca09d4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeEventListener.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.core.cluster; + +import org.apache.jackrabbit.spi.PrivilegeDefinition; + +import javax.jcr.RepositoryException; +import java.util.Collection; + +/** + * PrivilegeEventListener... + */ +public interface PrivilegeEventListener { + + /** + * Called when one or more privilege definitions have been externally registered. + * + * @param definitions privilege definitions + * @throws RepositoryException if an error occurs + */ + void externalRegisteredPrivileges(Collection definitions) throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeRecord.java new file mode 100644 index 00000000000..39ac7e85f8c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/PrivilegeRecord.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.core.cluster; + +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; +import org.apache.jackrabbit.spi.PrivilegeDefinition; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +/** + * PrivilegeRecord... + */ +public class PrivilegeRecord extends ClusterRecord { + + /** + * Identifier: PRIVILEGES. + */ + static final char IDENTIFIER = 'A'; + + /** + * Collection of privilege definitions. + */ + private Collection definitions; + + protected PrivilegeRecord(Record record) { + super(record); + } + + protected PrivilegeRecord(Collection definitions, Record record) { + super(record); + + this.definitions = definitions; + } + + @Override + protected void doRead() throws JournalException { + int size = record.readInt(); + definitions = new HashSet(); + for (int i = 0; i < size; i++) { + definitions.add(record.readPrivilegeDef()); + } + } + + @Override + protected void doWrite() throws JournalException { + record.writeChar(IDENTIFIER); + record.writeInt(definitions.size()); + + for (PrivilegeDefinition def : definitions) { + record.writePrivilegeDef(def); + } + } + + @Override + public void process(ClusterRecordProcessor processor) { + processor.process(this); + } + + /** + * Return the collection of privilege definitions. + * + * @return unmodifiable collection + */ + public Collection getDefinitions() { + return Collections.unmodifiableCollection(definitions); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/Update.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/Update.java new file mode 100644 index 00000000000..5c26e072e36 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/Update.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.core.cluster; + +import java.util.List; + +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.state.ChangeLog; + +/** + * Update operation passed in UpdateEventChannel. + */ +public interface Update { + + /** + * Set an attribute of this update operation. Can be used + * to remember some setting for a later notification. + * + * @param name attribute name + * @param value attribute value + */ + void setAttribute(String name, Object value); + + /** + * Return an attribute of this update operation. + * + * @param name attribute name + * @return attribute value or null + */ + Object getAttribute(String name); + + /** + * Return the local changes of this update operation. + * + * @return local changes + */ + ChangeLog getChanges(); + + /** + * Return the collection of events this update operation will + * generate. + * + * @return collection of EventStates + */ + List getEvents(); + + /** + * Returns the timestamp whe this update occurred. + * + * @return the timestamp whe this update occurred. + */ + long getTimestamp(); + + /** + * Returns the user data associated with this update. + * + * @return the user data associated with this update. + */ + String getUserData(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/UpdateEventChannel.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/UpdateEventChannel.java new file mode 100644 index 00000000000..d415be79406 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/UpdateEventChannel.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.core.cluster; + +/** + * Event channel used to transmit update operations. + */ +public interface UpdateEventChannel { + + /** + * Called when an a update operation has been created. + * + * @param update update operation + * @throws ClusterException if an error occurs writing to the event channel. + */ + void updateCreated(Update update) throws ClusterException; + + /** + * Called when an a update operation has been prepared. + * + * @param update update operation + * @throws ClusterException if an error occurs writing to the event channel. + */ + void updatePrepared(Update update) throws ClusterException; + + /** + * Called when an a update operation has been committed. + * + * @param update update operation + * @param path the change path + */ + void updateCommitted(Update update, String path); + + /** + * Called when an a update operation has been cancelled. + * + * @param update update operation + */ + void updateCancelled(Update update); + + /** + * Set listener that will receive information about incoming, external update events. + * + * @param listener update event listener + */ + void setListener(UpdateEventListener listener); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/UpdateEventListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/UpdateEventListener.java new file mode 100644 index 00000000000..f4ff8aa1085 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/UpdateEventListener.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.core.cluster; + +import java.util.List; + +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.state.ChangeLog; +import javax.jcr.RepositoryException; + +/** + * Interface used to receive information about incoming, external update events. + */ +public interface UpdateEventListener { + + /** + * Handle an external update. + * + * @param changes external changes containing only node and property ids. + * @param events events to deliver + * @param timestamp when the change occurred. + * @param userData the user data associated with this update. + * @throws RepositoryException if the update cannot be processed + */ + void externalUpdate(ChangeLog changes, List events, long timestamp, String userData) + throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceEventChannel.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceEventChannel.java new file mode 100644 index 00000000000..e4489d7f382 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceEventChannel.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.core.cluster; + +import org.apache.jackrabbit.core.xml.ClonedInputSource; + +/** + * Event channel for reporting workspace change events. + */ +public interface WorkspaceEventChannel { + + void workspaceCreated(String workspaceName, ClonedInputSource inputSource); + + void setListener(WorkspaceListener listener); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceListener.java new file mode 100644 index 00000000000..4275eab53ad --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceListener.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.core.cluster; + +import javax.jcr.RepositoryException; + +import org.xml.sax.InputSource; + +/** + * Listener for external workspace changes. + */ +public interface WorkspaceListener { + + /** + * Workspace created on another cluster node. + * + * @param workspaceName + * @param configTemplate + * @throws RepositoryException + */ + void externalWorkspaceCreated(String workspaceName, + InputSource configTemplate) throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceRecord.java new file mode 100644 index 00000000000..6c28f43a036 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/cluster/WorkspaceRecord.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.io.ByteArrayInputStream; +import java.io.CharArrayReader; + +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; +import org.apache.jackrabbit.core.xml.ClonedInputSource; +import org.xml.sax.InputSource; + +/** + * Record for propagating workspace modifications across the cluster. Currently + * only workspace creation is propagated because workspace deletion is not yet + * implemented. + */ +public class WorkspaceRecord extends ClusterRecord { + + /** + * Identifier: NAMESPACE. + */ + static final char IDENTIFIER = 'W'; + + /** + * Subtype for determining workspace action. + */ + public static final int CREATE_WORKSPACE_ACTION_TYPE = 1; + + /** + * Base workspace action + */ + public abstract static class Action { + abstract int getType(); + + abstract void write(Record record) throws JournalException; + + abstract void read(Record record) throws JournalException; + } + + /** + * Action for workspace creation. + */ + static final class CreateWorkspaceAction extends Action { + private InputSource inputSource; + private char[] charArray; + private byte[] byteArray; + + @Override + int getType() { + return CREATE_WORKSPACE_ACTION_TYPE; + } + + CreateWorkspaceAction() { + + } + + CreateWorkspaceAction(ClonedInputSource inputSource) { + this.inputSource = inputSource; + this.charArray = inputSource.getCharacterArray(); + this.byteArray = inputSource.getByteArray(); + } + + @Override + void write(Record record) throws JournalException { + // store the input source + record.writeString(inputSource.getEncoding()); + record.writeString(inputSource.getPublicId()); + record.writeString(inputSource.getSystemId()); + + // save character array if present + if (charArray != null) { + record.writeBoolean(true); + record.writeString(new String(charArray)); + } else { + record.writeBoolean(false); + } + + // save the bytearray if present + if (byteArray != null) { + record.writeBoolean(true); + record.writeInt(byteArray.length); + record.write(byteArray); + } else { + record.writeBoolean(false); + } + } + + @Override + void read(Record record) throws JournalException { + // restore the input source + inputSource = new InputSource(); + inputSource.setEncoding(record.readString()); + inputSource.setPublicId(record.readString()); + inputSource.setSystemId(record.readString()); + + if (record.readBoolean()) { + charArray = record.readString().toCharArray(); + inputSource.setCharacterStream(new CharArrayReader(charArray)); + } + if (record.readBoolean()) { + final int size = record.readInt(); + byteArray = new byte[size]; + record.readFully(byteArray); + inputSource.setByteStream(new ByteArrayInputStream(byteArray)); + } + } + + public InputSource getInputSource() { + return inputSource; + } + } + + // current action + private Action action; + + /** + * Creates a new {@link WorkspaceRecord} for create workspace action. + * + * @param workspace + * workspace name + * @param inputSource + * input source with configuration for the workspace + * @param record + * journal record + */ + protected WorkspaceRecord(String workspace, ClonedInputSource inputSource, + Record record) { + super(record, workspace); + + action = new CreateWorkspaceAction(inputSource); + } + + /** + * Creates a new empty {@link WorkspaceRecord}. + * + * @param record + */ + protected WorkspaceRecord(Record record) { + super(record); + } + + @Override + protected void doRead() throws JournalException { + + workspace = record.readString(); + + // determine type + int action = record.readInt(); + + if (action == CREATE_WORKSPACE_ACTION_TYPE) { + this.action = new CreateWorkspaceAction(); + } + + if (this.action != null) { + this.action.read(record); + } else { + throw new JournalException("Unknown workspace action type"); + } + } + + @Override + protected void doWrite() throws JournalException { + + record.writeChar(IDENTIFIER); + + record.writeString(workspace); + + // store the type + record.writeInt(getActionType()); + + if (action != null) { + action.write(record); + } else { + throw new JournalException("Can not write empty workspace action"); + } + } + + public int getActionType() { + return action != null ? action.getType() : -1; + } + + public CreateWorkspaceAction getCreateWorkspaceAction() { + return (CreateWorkspaceAction) action; + } + + @Override + public void process(ClusterRecordProcessor processor) { + processor.process(this); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/AccessManagerConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/AccessManagerConfig.java new file mode 100644 index 00000000000..cbd3af53c27 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/AccessManagerConfig.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.core.config; + +/** + * Access manager configuration. This bean configuration class + * is used to create configured access manager objects. + *

    + * This class is currently only used to assign a static type to + * more generic bean configuration information. + * + * @see SecurityConfig#getAccessManagerConfig() + */ +public class AccessManagerConfig extends BeanConfig { + + /** + * Creates an access manager configuration object from the + * given bean configuration. + * + * @param config bean configuration + */ + public AccessManagerConfig(BeanConfig config) { + super(config); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanConfig.java new file mode 100644 index 00000000000..f29382e2919 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanConfig.java @@ -0,0 +1,343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.config; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.core.util.db.DatabaseAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Bean configuration class. BeanConfig instances contain the class name + * and property information required to instantiate a class that conforms + * with the JavaBean conventions. + */ +public class BeanConfig { + + private static Logger log = LoggerFactory.getLogger(BeanConfig.class); + + private static final Map DEPRECATIONS; + + static { + try { + Map temp = new HashMap(); + Properties props = new Properties(); + InputStream in = BeanConfig.class.getResourceAsStream("deprecated-classes.properties"); + try { + props.load(in); + } finally { + in.close(); + } + for (Map.Entry entry : props.entrySet()) { + temp.put(entry.getKey().toString(), entry.getValue().toString()); + } + DEPRECATIONS = Collections.unmodifiableMap(temp); + } catch (IOException e) { + throw new InternalError("failed to read deprecated classes"); + } + } + + /** The default class loader used by all instances of this class */ + private static ClassLoader defaultClassLoader = + BeanConfig.class.getClassLoader(); + + /** + * Factory to create instance from Bean className + */ + private BeanFactory instanceFactory = new SimpleBeanFactory(); + + /** + * The current class loader used by this instance to create instances of + * configured classes. + */ + private ClassLoader classLoader = getDefaultClassLoader(); + + /** + * The class name of the configured bean. + */ + private final String className; + + /** + * The initial properties of the configured bean. + */ + private final Properties properties; + + /** + * The repositories {@link ConnectionFactory}. + */ + private ConnectionFactory connectionFactory = null; + + /** + * Flag to validate the configured bean property names against + * the configured bean class. By default this is true + * to prevent incorrect property names. However, in some cases this + * validation should not be performed as client classes may access + * the configuration parameters directly through the + * {@link #getParameters()} method. + * + * @see JCR-1920 + */ + private boolean validate = true; + + /** + * Creates a bean configuration. Note that a copy of the given + * bean properties is stored as a part of the created configuration + * object. Thus the caller is free to modify the given properties + * once the configuration object has been created. + * + * @param className class name of the bean + * @param properties initial properties of the bean + */ + public BeanConfig(String className, Properties properties) { + if (DEPRECATIONS.containsKey(className)) { + String replacement = DEPRECATIONS.get(className); + log.info("{} is deprecated. Please use {} instead", className, replacement); + className = replacement; + } + this.className = className; + this.properties = (Properties) properties.clone(); + } + + /** + * Copies a bean configuration. + * + * @param config the configuration to be copied + */ + public BeanConfig(BeanConfig config) { + this(config.getClassName(), config.getParameters()); + setConnectionFactory(config.connectionFactory); + } + + /** + * Allows subclasses to control whether the configured bean property + * names should be validated. + * + * @param validate flag to validate the configured property names + */ + protected void setValidate(boolean validate) { + this.validate = validate; + } + + /** + * @param connectionFactory the {@link ConnectionFactory} to inject (if possible) in the + * {@link #newInstance(Class)} method + */ + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * + * @param instanceFactory the {@link BeanFactory} to use to create bean instance + */ + public void setInstanceFactory(BeanFactory instanceFactory) { + this.instanceFactory = instanceFactory; + } + + /** + * Returns the class name of the configured bean. + * + * @return class name of the bean + */ + public String getClassName() { + return className; + } + + /** + * Returns the initial properties of the configured bean. + * + * @return initial properties of the bean + */ + public Properties getParameters() { + return properties; + } + + /** + * Creates a new instance of the configured bean class. + * + * @return new bean instance + * @throws ConfigurationException on bean configuration errors + */ + @SuppressWarnings("unchecked") + public T newInstance(Class klass) throws ConfigurationException { + String cname = getClassName(); + // Instantiate the object using the default constructor + Object instance = instanceFactory.newInstance(klass,this); + Class objectClass = instance.getClass(); + + // Set all configured bean properties + Map setters = getSetters(objectClass); + Enumeration enumeration = properties.propertyNames(); + while (enumeration.hasMoreElements()) { + String name = enumeration.nextElement().toString(); + Method setter = setters.get(name); + if (setter != null) { + if (setter.getAnnotation(Deprecated.class) != null) { + log.warn("Parameter {} of {} has been deprecated", + name, cname); + } + String value = properties.getProperty(name); + setProperty(instance, name, setter, value); + } else if (validate) { + throw new ConfigurationException( + "Configured class " + cname + + " does not contain a property named " + name); + } + } + + if (instance instanceof DatabaseAware) { + ((DatabaseAware) instance).setConnectionFactory(connectionFactory); + } + + return (T) instance; + } + + private Map getSetters(Class klass) { + Map methods = new HashMap(); + for (Method method : klass.getMethods()) { + String name = method.getName(); + if (name.startsWith("set") && name.length() > 3 + && Modifier.isPublic(method.getModifiers()) + && !Modifier.isStatic(method.getModifiers()) + && Void.TYPE.equals(method.getReturnType()) + && method.getParameterTypes().length == 1) { + methods.put( + name.substring(3, 4).toLowerCase(Locale.ENGLISH) + name.substring(4), + method); + } + } + return methods; + } + + private void setProperty( + Object instance, String name, Method setter, String value) + throws ConfigurationException { + Class type = setter.getParameterTypes()[0]; + try { + if (type.isAssignableFrom(String.class) + || type.isAssignableFrom(Object.class)) { + setter.invoke(instance, value); + } else if (type.isAssignableFrom(Boolean.TYPE) + || type.isAssignableFrom(Boolean.class)) { + setter.invoke(instance, Boolean.valueOf(value)); + } else if (type.isAssignableFrom(Integer.TYPE) + || type.isAssignableFrom(Integer.class)) { + setter.invoke(instance, Integer.valueOf(value)); + } else if (type.isAssignableFrom(Long.TYPE) + || type.isAssignableFrom(Long.class)) { + setter.invoke(instance, Long.valueOf(value)); + } else if (type.isAssignableFrom(Double.TYPE) + || type.isAssignableFrom(Double.class)) { + setter.invoke(instance, Double.valueOf(value)); + } else { + throw new ConfigurationException( + "The type (" + type.getName() + + ") of property " + name + " of class " + + getClassName() + " is not supported"); + } + } catch (NumberFormatException e) { + throw new ConfigurationException( + "Invalid number format (" + value + ") for property " + + name + " of class " + getClassName(), e); + } catch (InvocationTargetException e) { + throw new ConfigurationException( + "Property " + name + " of class " + + getClassName() + " can not be set to \"" + value + "\"", + e); + } catch (IllegalAccessException e) { + throw new ConfigurationException( + "The setter of property " + name + + " of class " + getClassName() + " can not be accessed", + e); + } catch (IllegalArgumentException e) { + throw new ConfigurationException( + "Unable to call the setter of property " + + name + " of class " + getClassName(), e); + } + } + + //---------- Configurable class loader support ---------------------------- + + /** + * Returns the current ClassLoader used to instantiate objects + * in the {@link #newInstance(Class)} method. + * + * @see #setClassLoader(ClassLoader) + * @see #getDefaultClassLoader() + * @see #setDefaultClassLoader(ClassLoader) + */ + public ClassLoader getClassLoader() { + return classLoader; + } + + /** + * Sets the ClassLoader used to instantiate objects in the + * {@link #newInstance(Class)} method. + * + * @param classLoader The class loader to set on this instance. If this is + * null the system class loader will be used, which may + * lead to unexpected class loading failures. + * + * @see #getClassLoader() + * @see #getDefaultClassLoader() + * @see #setDefaultClassLoader(ClassLoader) + */ + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + /** + * Returns the current ClassLoader used for new instances of + * this class as the loader used to instantiate objects in the + * {@link #newInstance(Class)} method. + * + * @see #getClassLoader() + * @see #setClassLoader(ClassLoader) + * @see #setDefaultClassLoader(ClassLoader) + */ + public static ClassLoader getDefaultClassLoader() { + return defaultClassLoader; + } + + /** + * Sets the ClassLoader used for new instances of this class as + * the loader to instantiate objects in the {@link #newInstance(Class)} method. + * + * @param classLoader The class loader to set as the default class loader. + * If this is null the system class loader will be used, + * which may lead to unexpected class loading failures. + * + * @see #getClassLoader() + * @see #setClassLoader(ClassLoader) + * @see #getDefaultClassLoader() + */ + public static void setDefaultClassLoader(ClassLoader classLoader) { + defaultClassLoader = classLoader; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanConfigVisitor.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanConfigVisitor.java new file mode 100644 index 00000000000..2591c742b87 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanConfigVisitor.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.core.config; + +/** + * A BeanConfig visitor which is invoked upon creation of BeanConfig before any + * instance is created from that bean configuration + */ +public interface BeanConfigVisitor { + void visit(BeanConfig config); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanFactory.java new file mode 100644 index 00000000000..90a10bbb197 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/BeanFactory.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. + */ +package org.apache.jackrabbit.core.config; + + +public interface BeanFactory { + Object newInstance(Class klass, BeanConfig config) throws ConfigurationException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ClusterConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ClusterConfig.java new file mode 100644 index 00000000000..95d8410c120 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ClusterConfig.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.core.config; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.journal.Journal; +import org.apache.jackrabbit.core.journal.JournalFactory; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +/** + * Cluster configuration. + */ +public class ClusterConfig implements JournalFactory { + + /** + * Identifier. + */ + private final String id; + + /** + * Sync delay. + */ + private final long syncDelay; + + /** + * Stop delay. + */ + private final long stopDelay; + + /** + * Journal factory. + */ + private final JournalFactory jf; + + /** + * Creates a new cluster configuration. + * + * @param id custom cluster node id + * @param syncDelay syncDelay, in milliseconds + * @param jf journal factory + */ + public ClusterConfig(String id, long syncDelay, JournalFactory jf) { + this(id, syncDelay, -1, jf); + } + + /** + * Creates a new cluster configuration. + * + * @param id custom cluster node id + * @param syncDelay syncDelay, in milliseconds + * @param stopDelay stopDelay in milliseconds + * @param jf journal factory + */ + public ClusterConfig(String id, long syncDelay, + long stopDelay, JournalFactory jf) { + this.id = id; + this.syncDelay = syncDelay; + this.stopDelay = stopDelay < 0 ? syncDelay * 10 : stopDelay; + this.jf = jf; + } + + /** + * Return the id configuration attribute value. + * + * @return id attribute value + */ + public String getId() { + return id; + } + + /** + * Return the syncDelay configuration attribute value. + * + * @return syncDelay + */ + public long getSyncDelay() { + return syncDelay; + } + + /** + * @return stopDelay the stopDelay configuration attribute value. + */ + public long getStopDelay() { + return stopDelay; + } + + /** + * Returns an initialized journal instance. + * + * @param resolver namespace resolver + * @return initialized journal + * @throws RepositoryException + * @throws RepositoryException if the journal can not be created + */ + public Journal getJournal(NamespaceResolver resolver) + throws RepositoryException { + return jf.getJournal(resolver); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationEntityResolver.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationEntityResolver.java new file mode 100644 index 00000000000..612c218a81c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationEntityResolver.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.config; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Entity resolver for Jackrabbit configuration files. + * This simple resolver contains mappings for the following + * public identifiers used for the Jackrabbit configuration files: + *

      + *
    • -//The Apache Software Foundation//DTD Jackrabbit 2.6//EN
    • + *
    • -//The Apache Software Foundation//DTD Jackrabbit 2.4//EN
    • + *
    • -//The Apache Software Foundation//DTD Jackrabbit 2.0//EN
    • + *
    • -//The Apache Software Foundation//DTD Jackrabbit 1.6//EN
    • + *
    • -//The Apache Software Foundation//DTD Jackrabbit 1.5//EN
    • + *
    • -//The Apache Software Foundation//DTD Jackrabbit 1.4//EN
    • + *
    • -//The Apache Software Foundation//DTD Jackrabbit 1.2//EN
    • + *
    • -//The Apache Software Foundation//DTD Jackrabbit 1.0//EN
    • + *
    + *

    + * Also the following system identifiers are mapped to local resources: + *

      + *
    • http://jackrabbit.apache.org/dtd/repository-2.6.dtd
    • + *
    • http://jackrabbit.apache.org/dtd/repository-2.4.dtd
    • + *
    • http://jackrabbit.apache.org/dtd/repository-2.0.dtd
    • + *
    • http://jackrabbit.apache.org/dtd/repository-1.6.dtd
    • + *
    • http://jackrabbit.apache.org/dtd/repository-1.5.dtd
    • + *
    • http://jackrabbit.apache.org/dtd/repository-1.4.dtd
    • + *
    • http://jackrabbit.apache.org/dtd/repository-1.2.dtd
    • + *
    • http://jackrabbit.apache.org/dtd/repository-1.0.dtd
    • + *
    + *

    + * The public identifiers are mapped to document type definition + * files included in the Jackrabbit jar archive. + */ +public class ConfigurationEntityResolver implements EntityResolver { + + /** + * The singleton instance of this class. + */ + public static final EntityResolver INSTANCE = + new ConfigurationEntityResolver(); + + /** + * Public identifiers. + */ + private final Map publicIds = new HashMap(); + + /** + * System identifiers. + */ + private final Map systemIds = new HashMap(); + + /** + * Creates the singleton instance of this class. + */ + private ConfigurationEntityResolver() { + // Apache Jackrabbit 2.6 DTD + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 2.6//EN", + "repository-2.6.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-2.6.dtd", + "repository-2.6.dtd"); + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 2.6 Elements//EN", + "repository-2.6-elements.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-2.6-elements.dtd", + "repository-2.6-elements.dtd"); + + + // Apache Jackrabbit 2.4 DTD + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 2.4//EN", + "repository-2.4.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-2.4.dtd", + "repository-2.4.dtd"); + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 2.4 Elements//EN", + "repository-2.4-elements.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-2.4-elements.dtd", + "repository-2.4-elements.dtd"); + + // Apache Jackrabbit 2.0 DTD + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 2.0//EN", + "repository-2.0.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-2.0.dtd", + "repository-2.0.dtd"); + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 2.0 Elements//EN", + "repository-2.0-elements.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-2.0-elements.dtd", + "repository-2.0-elements.dtd"); + + // Apache Jackrabbit 1.6 DTD + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN", + "repository-1.6.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-1.6.dtd", + "repository-1.6.dtd"); + + // Apache Jackrabbit 1.5 DTD + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 1.5//EN", + "repository-1.5.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-1.5.dtd", + "repository-1.5.dtd"); + + // Apache Jackrabbit 1.4 DTD + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 1.4//EN", + "repository-1.4.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-1.4.dtd", + "repository-1.4.dtd"); + + // Apache Jackrabbit 1.2 DTD + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 1.2//EN", + "repository-1.2.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-1.2.dtd", + "repository-1.2.dtd"); + + // Apache Jackrabbit 1.0 DTD + publicIds.put( + "-//The Apache Software Foundation//DTD Jackrabbit 1.0//EN", + "repository-1.0.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/repository-1.0.dtd", + "repository-1.0.dtd"); + } + + /** + * Resolves an entity to the corresponding input source. + * + * @param publicId public identifier + * @param systemId system identifier + * @return resolved entity source + * @throws SAXException on SAX errors + * @throws IOException on IO errors + */ + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + String name; + + name = publicIds.get(publicId); + if (name != null) { + InputStream stream = getClass().getResourceAsStream(name); + if (stream != null) { + return new InputSource(stream); + } + } + + name = systemIds.get(systemId); + if (name != null) { + InputStream stream = getClass().getResourceAsStream(name); + if (stream != null) { + return new InputSource(stream); + } + } + + return null; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationErrorHandler.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationErrorHandler.java new file mode 100644 index 00000000000..be192af67d1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationErrorHandler.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.core.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Error handler for errors in the repository or workspace configuration. + */ +public class ConfigurationErrorHandler implements ErrorHandler { + + private static Logger log = LoggerFactory.getLogger(ConfigurationErrorHandler.class); + + /** + * This method is called when there is an error parsing the configuration file. + * The relevant information is written to the log file. + */ + public void error(SAXParseException exception) throws SAXException { + log("Warning", exception); + } + + private void log(String type, SAXParseException exception) { + log.warn(type + " parsing the configuration at line " + exception.getLineNumber() + " using system id " + exception.getSystemId() + ": " + exception.toString()); + } + + /** + * This method is called when there is a fatal error parsing the configuration file. + * The relevant information is written to the log file. + */ + public void fatalError(SAXParseException exception) throws SAXException { + log("Fatal error", exception); + throw exception; + } + + /** + * This method is called when there is a warning parsing the configuration file. + * The relevant information is written to the log file. + */ + public void warning(SAXParseException exception) throws SAXException { + log("Warning", exception); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationParser.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationParser.java new file mode 100644 index 00000000000..a010c5c88c2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ConfigurationParser.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.core.config; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.apache.jackrabbit.util.Text; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * Configuration parser base class. This class provides the basic + * functionality for parsing Jackrabbit configuration files. Subclasses + * extend this functionality with knowledge of the exact structure of the + * different configuration files. Each configuration parser instance + * contains a set of parser variables that are used for variable replacement + * in the configuration file. + */ +public class ConfigurationParser { + + /** Name of the bean parameter configuration element. */ + public static final String PARAM_ELEMENT = "param"; + + /** Name of the bean implementation class configuration attribute. */ + public static final String CLASS_ATTRIBUTE = "class"; + + /** Name of the bean parameter name configuration attribute. */ + public static final String NAME_ATTRIBUTE = "name"; + + /** Name of the bean parameter value configuration attribute. */ + public static final String VALUE_ATTRIBUTE = "value"; + + /** + * The configuration parser variables. These name-value pairs + * are used to substitute ${...} variable references + * with context-dependent values in the configuration. + * + * @see #replaceVariables(String) + */ + private final Properties variables; + + /** + * Creates a new configuration parser with the given parser variables. + * + * @param variables parser variables + */ + public ConfigurationParser(Properties variables) { + this.variables = variables; + } + + /** + * Returns the variables. + * @return the variables. + */ + public Properties getVariables() { + return variables; + } + + /** + * Parses a named bean configuration from the given element. + * Bean configuration uses the following format: + *

    +     *   <BeanName class="...">
    +     *     <param name="..." value="..."/>
    +     *     ...
    +     *   </BeanName>
    +     * 
    + *

    + * The returned bean configuration object contains the configured + * class name and configuration parameters. Variable replacement + * is performed on the parameter values. + * + * @param parent parent element + * @param name name of the bean configuration element + * @return bean configuration, + * @throws ConfigurationException if the configuration element does not + * exist or is broken + */ + protected BeanConfig parseBeanConfig(Element parent, String name) + throws ConfigurationException { + // Bean configuration element + Element element = getElement(parent, name); + + return parseBeanConfig(element); + } + + /** + * Parses a named bean configuration from the given element. + * Bean configuration uses the following format: + *

    +     *   <BeanName class="...">
    +     *     <param name="..." value="..."/>
    +     *     ...
    +     *   </BeanName>
    +     * 
    + *

    + * The returned bean configuration object contains the configured + * class name and configuration parameters. Variable replacement + * is performed on the parameter values. + * + * @param element + * @return bean configuration, + * @throws ConfigurationException if the configuration element does not + * exist or is broken + */ + protected BeanConfig parseBeanConfig(Element element) + throws ConfigurationException { + // Bean implementation class + String className = getAttribute(element, CLASS_ATTRIBUTE); + + // Bean properties + Properties properties = parseParameters(element); + + return new BeanConfig(className, properties); + } + + /** + * Parses the configuration parameters of the given element. + * Parameters are stored as + * <param name="..." value="..."/> + * child elements. This method parses all param elements, + * performs {@link #replaceVariables(String) variable replacement} + * on parameter values, and returns the resulting name-value pairs. + * + * @param element configuration element + * @return configuration parameters + * @throws ConfigurationException if a param element does + * not contain the name and + * value attributes + */ + protected Properties parseParameters(Element element) + throws ConfigurationException { + Properties parameters = new Properties(); + + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && PARAM_ELEMENT.equals(child.getNodeName())) { + Element parameter = (Element) child; + Attr name = parameter.getAttributeNode(NAME_ATTRIBUTE); + if (name == null) { + throw new ConfigurationException("Parameter name not set"); + } + Attr value = parameter.getAttributeNode(VALUE_ATTRIBUTE); + if (value == null) { + throw new ConfigurationException("Parameter value not set"); + } + parameters.put( + name.getValue().trim(), + replaceVariables(value.getValue())); + } + } + + return parameters; + } + + /** + * Performs variable replacement on the given string value. + * Each ${...} sequence within the given value is replaced + * with the value of the named parser variable. The replacement is not + * done if the named variable does not exist. + * + * @param value original value + * @return value after variable replacements + * @throws ConfigurationException if the replacement of a referenced + * variable is not found + */ + protected String replaceVariables(String value) + throws ConfigurationException { + try { + return Text.replaceVariables(variables, value, false); + } catch (IllegalArgumentException e) { + throw new ConfigurationException(e.getMessage(), e); + } + } + + /** + * Parses the given XML document and returns the DOM root element. + * A custom entity resolver is used to make the included configuration + * file DTD available using the specified public identifiers. + * This implementation does not validate the XML. + * + * @see ConfigurationEntityResolver + * @param xml xml document + * @return root element + * @throws ConfigurationException if the configuration document could + * not be read or parsed + */ + protected Element parseXML(InputSource xml) throws ConfigurationException { + return parseXML(xml, false); + } + + /** + * Returns the error handler to be used when parsing configuration + * documents. Subclasses can override this method to provide custom + * error handling. + * + * @since Apache Jackrabbit 2.0 + * @return error handler + */ + protected ErrorHandler getErrorHandler() { + return new ConfigurationErrorHandler(); + } + + /** + * Returns the entity resolver to be used when parsing configuration + * documents. Subclasses can override this method to provide custom + * entity resolution rules. + * + * @since Apache Jackrabbit 2.0 + * @return error handler + */ + protected EntityResolver getEntityResolver() { + return ConfigurationEntityResolver.INSTANCE; + } + + /** + * A post-processing hook for the parsed repository or workspace + * configuration documents. This hook makes it possible to make custom + * DOM modifications for backwards-compatibility or other reasons. + * + * @since Apache Jackrabbit 2.0 + * @param document the parsed configuration document + * @return the configuration document after any modifications + */ + protected Document postParseModificationHook(Document document) { + return document; + } + + /** + * Parses the given XML document and returns the DOM root element. + * A custom entity resolver is used to make the included configuration + * file DTD available using the specified public identifiers. + * + * @see ConfigurationEntityResolver + * @param xml xml document + * @param validate whether the XML should be validated + * @return root element + * @throws ConfigurationException if the configuration document could + * not be read or parsed + */ + protected Element parseXML(InputSource xml, boolean validate) throws ConfigurationException { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(validate); + DocumentBuilder builder = factory.newDocumentBuilder(); + if (validate) { + builder.setErrorHandler(getErrorHandler()); + } + builder.setEntityResolver(getEntityResolver()); + Document document = builder.parse(xml); + return postParseModificationHook(document).getDocumentElement(); + } catch (ParserConfigurationException e) { + throw new ConfigurationException( + "Unable to create configuration XML parser", e); + } catch (SAXParseException e) { + throw new ConfigurationException( + "Configuration file syntax error. (Line: " + e.getLineNumber() + " Column: " + e.getColumnNumber() + ")", e); + } catch (SAXException e) { + throw new ConfigurationException( + "Configuration file syntax error. ", e); + } catch (IOException e) { + throw new ConfigurationException( + "Configuration file could not be read.", e); + } + } + + /** + * Returns the named child of the given parent element. + * + * @param parent parent element + * @param name name of the child element + * @return named child element + * @throws ConfigurationException + * @throws ConfigurationException if the child element is not found + */ + protected Element getElement(Element parent, String name) + throws ConfigurationException { + return getElement(parent, name, true); + } + + /** + * Returns the named child of the given parent element. + * + * @param parent parent element + * @param name name of the child element + * @param required indicates if the child element is required + * @return named child element, or null if not found and + * required is false. + * @throws ConfigurationException if the child element is not found and + * required is true; + * or if more than one element with this name exists. + */ + protected Element getElement(Element parent, String name, boolean required) + throws ConfigurationException { + NodeList children = parent.getChildNodes(); + Element found = null; + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && name.equals(child.getNodeName())) { + if (found != null) { + throw new ConfigurationException( + "Duplicate configuration element " + name + " in " + + parent.getNodeName() + "."); + } + found = (Element) child; + } + } + if (required && found == null) { + throw new ConfigurationException( + "Configuration element " + name + " not found in " + + parent.getNodeName() + "."); + } + return found; + } + + /** + * Returns the named child of the given parent element. + * + * @param parent parent element + * @param name name of the child element + * @param required indicates if the child element is required + * @return named child element, or null if not found and + * required is false. + * @throws ConfigurationException if the child element is not found and + * required is true; + * or if more than one element with this name exists. + */ + protected Element[] getElements(Element parent, String name, boolean required) + throws ConfigurationException { + NodeList children = parent.getChildNodes(); + List found = new ArrayList(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && name.equals(child.getNodeName())) { + found.add((Element) child); + } + } + if (required && found.isEmpty()) { + throw new ConfigurationException( + "Configuration element " + name + " not found in " + + parent.getNodeName() + "."); + } + return found.toArray(new Element[found.size()]); + } + + /** + * Returns the value of the named attribute of the given element. + * + * @param element element + * @param name attribute name + * @return attribute value + * @throws ConfigurationException if the attribute is not found + */ + protected String getAttribute(Element element, String name) + throws ConfigurationException { + Attr attribute = element.getAttributeNode(name); + if (attribute != null) { + return attribute.getValue(); + } else { + throw new ConfigurationException( + "Configuration attribute " + name + " not found in " + + element.getNodeName() + "."); + } + } + + /** + * Returns the value of the named attribute of the given element. + * If the attribute is not found, then the given default value is returned. + * + * @param element element + * @param name attribute name + * @param def default value + * @return attribute value, or the default value + */ + protected String getAttribute(Element element, String name, String def) { + Attr attribute = element.getAttributeNode(name); + if (attribute != null) { + return attribute.getValue(); + } else { + return def; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ImportConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ImportConfig.java new file mode 100644 index 00000000000..e8daf0c80fd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/ImportConfig.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.core.config; + +import org.apache.jackrabbit.core.xml.ProtectedItemImporter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * XmlImportConfig... + */ +public class ImportConfig { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ImportConfig.class); + + private final List protectedItemImporters; + + public ImportConfig() { + protectedItemImporters = Collections.emptyList(); + } + + public ImportConfig(List protectedItemImporters) { + this.protectedItemImporters = protectedItemImporters; + } + + public List getProtectedItemImporters() { + List piis = new ArrayList(); + for (BeanConfig bc : protectedItemImporters) { + try { + piis.add(bc.newInstance(ProtectedItemImporter.class)); + } catch (ConfigurationException e) { + log.warn(e.getMessage()); + } + } + return piis; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/LoginModuleConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/LoginModuleConfig.java new file mode 100644 index 00000000000..6986e163e2a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/LoginModuleConfig.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.core.config; + +import javax.security.auth.spi.LoginModule; + +/** + * LoginModule configuration. This bean configuration class is used to + * create login module objects. + *

    + * Login module is an optional configuration that allows to use JackRabbit + * in a non-JAAS environment. + * + * @see RepositoryConfig#getLoginModuleConfig() + */ +public class LoginModuleConfig extends BeanConfig { + + /** + * UserId of the anonymous user. Optional parameter in the LoginModule + * configuration. + */ + public static final String PARAM_ANONYMOUS_ID = "anonymousId"; + + /** + * UserId of the administrator. Optional parameter in the LoginModule + * configuration. + */ + public static final String PARAM_ADMIN_ID = "adminId"; + + /** + * Property-Key for the fully qualified class name of the implementation of + * {@link org.apache.jackrabbit.core.security.principal.PrincipalProvider} + * to be used with the LoginModule. + */ + public static final String PARAM_PRINCIPAL_PROVIDER_CLASS = "principalProvider"; + + /** + * Same as {@link LoginModuleConfig#PARAM_PRINCIPAL_PROVIDER_CLASS}. + * Introduced for compatibility reasons. + * + * @see JCR-2629 + */ + public static final String COMPAT_PRINCIPAL_PROVIDER_CLASS = "principal_provider.class"; + + /** + * Property-Key if the PrincipalProvider configured with + * {@link LoginModuleConfig#PARAM_PRINCIPAL_PROVIDER_CLASS} be registered using the + * specified name instead of the class name which is used by default if the + * name parameter is missing. + * + * @see JCR-2629 + */ + public static final String COMPAT_PRINCIPAL_PROVIDER_NAME = "principal_provider.name"; + + /** + * Creates an access manager configuration object from the + * given bean configuration. + * + * @param config bean configuration + */ + public LoginModuleConfig(BeanConfig config) { + super(config); + setValidate(false); // JCR-1920 + } + + public LoginModule getLoginModule() throws ConfigurationException { + return newInstance(LoginModule.class); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/NoOpConfigVisitor.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/NoOpConfigVisitor.java new file mode 100644 index 00000000000..995bc7c431e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/NoOpConfigVisitor.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.config; + +public class NoOpConfigVisitor implements BeanConfigVisitor{ + @Override + public void visit(BeanConfig config) { + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/PersistenceManagerConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/PersistenceManagerConfig.java new file mode 100644 index 00000000000..b34423a589b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/PersistenceManagerConfig.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.core.config; + +/** + * Persistence manager configuration. This bean configuration class + * is used to create configured persistence manager objects. + *

    + * This class is currently only used to assign a static type to + * more generic bean configuration information. + * + * @see WorkspaceConfig#getPersistenceManagerConfig() + */ +public class PersistenceManagerConfig extends BeanConfig { + + /** + * Creates a persistence manager configuration object from the + * given bean configuration. + * + * @param config bean configuration + */ + public PersistenceManagerConfig(BeanConfig config) { + super(config); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfig.java new file mode 100644 index 00000000000..7801c01dacd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfig.java @@ -0,0 +1,1098 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.config; + +import static org.apache.jackrabbit.core.config.RepositoryConfigurationParser.REPOSITORY_CONF_VARIABLE; +import static org.apache.jackrabbit.core.config.RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.RepositoryFactoryImpl; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.data.DataStoreFactory; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemFactory; +import org.apache.jackrabbit.core.fs.FileSystemPathUtil; +import org.apache.jackrabbit.core.query.QueryHandler; +import org.apache.jackrabbit.core.query.QueryHandlerContext; +import org.apache.jackrabbit.core.query.QueryHandlerFactory; +import org.apache.jackrabbit.core.util.RepositoryLockMechanism; +import org.apache.jackrabbit.core.util.RepositoryLockMechanismFactory; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; + +import javax.jcr.RepositoryException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URI; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Repository configuration. This configuration class is used to + * create configured repository objects. + *

    + * The contained configuration information are: the home directory and name + * of the repository, the access manager, file system and versioning + * configuration, repository index configuration, the workspace directory, + * the default workspace name, and the workspace configuration template. In + * addition the workspace configuration object keeps track of all configured + * workspaces. + */ +public class RepositoryConfig + implements FileSystemFactory, DataStoreFactory, QueryHandlerFactory { + + /** the default logger */ + private static Logger log = LoggerFactory.getLogger(RepositoryConfig.class); + + /** Name of the default repository configuration file. */ + private static final String REPOSITORY_XML = "repository.xml"; + + /** Name of the workspace configuration file. */ + private static final String WORKSPACE_XML = "workspace.xml"; + + /** + * Returns the configuration of a repository in a given repository + * directory. The repository configuration is read from a "repository.xml" + * file inside the repository directory. + *

    + * The directory is created if it does not exist. If the repository + * configuration file does not exist, then it is created using the + * default Jackrabbit configuration settings. + * + * @param dir repository home directory + * @return repository configuration + * @throws ConfigurationException on configuration errors + * @throws java.io.IOException If an error occurs. + * @since Apache Jackrabbit 1.6 + */ + public static RepositoryConfig install(File dir) + throws IOException, ConfigurationException { + return install(new File(dir, REPOSITORY_XML), dir); + } + + /** + * Returns the configuration of a repository with the home directory, + * configuration file, and other options as specified in the given + * configuration parser variables. + *

    + * The directory is created if it does not exist. If the repository + * configuration file does not exist, then it is created using the + * default Jackrabbit configuration settings. + * + * @param variables parser variables + * @return repository configuration + * @throws ConfigurationException on configuration errors + * @throws java.io.IOException If an error occurs. + * @since Apache Jackrabbit 2.1 + */ + public static RepositoryConfig install(Properties variables) + throws IOException, ConfigurationException { + Properties copy = new Properties(variables); + + String home = copy.getProperty(REPOSITORY_HOME_VARIABLE); + if (home == null) { + home = copy.getProperty( + RepositoryFactoryImpl.REPOSITORY_HOME, "jackrabbit"); + copy.setProperty(REPOSITORY_HOME_VARIABLE, home); + } + File dir = new File(home); + + String conf = copy.getProperty(REPOSITORY_CONF_VARIABLE); + if (conf == null) { + conf = copy.getProperty(RepositoryFactoryImpl.REPOSITORY_CONF); + } + + URL resource = RepositoryImpl.class.getResource(REPOSITORY_XML); + if (conf == null) { + conf = new File(dir, REPOSITORY_XML).getPath(); + } else if (conf.startsWith("classpath:")) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + if (loader == null) { + loader = RepositoryImpl.class.getClassLoader(); + } + resource = loader.getResource(conf.substring("classpath:".length())); + conf = new File(dir, REPOSITORY_XML).getPath(); + } + + File xml = new File(conf); + installRepositorySkeleton(dir, xml, resource); + return create(new InputSource(xml.toURI().toString()), copy); + } + + public static File getRepositoryHome(Properties variables) { + String home = variables.getProperty(REPOSITORY_HOME_VARIABLE); + if (home == null) { + home = variables.getProperty( + RepositoryFactoryImpl.REPOSITORY_HOME, "jackrabbit"); + } + return new File(home); + } + + /** + * Returns the configuration of a repository with the given configuration + * file and repository home directory. + *

    + * The directory is created if it does not exist. If the repository + * configuration file does not exist, then it is created using the + * default Jackrabbit configuration settings. + * + * @param xml the configuration file. + * @param dir repository home directory + * @return repository configuration + * @throws ConfigurationException on configuration errors + * @throws java.io.IOException If another error occurs. + * @since Apache Jackrabbit 1.6 + */ + public static RepositoryConfig install(File xml, File dir) + throws IOException, ConfigurationException { + installRepositorySkeleton( + dir, xml, RepositoryImpl.class.getResource(REPOSITORY_XML)); + return create(xml, dir); + } + + private static void installRepositorySkeleton( + File dir, File xml, URL resource) + throws IOException, ConfigurationException { + if (!dir.exists()) { + log.info("Creating repository directory {}", dir); + boolean dirCreated = dir.mkdirs(); + if (!dirCreated) { + throw new ConfigurationException("Cannot create repository directory " + dir); + } + } + + if (!xml.exists()) { + log.info("Copying configuration from {} to {}", resource, xml); + OutputStream output = new FileOutputStream(xml); + try { + InputStream input = resource.openStream(); + try { + IOUtils.copy(input, output); + } finally { + input.close(); + } + } finally { + output.close(); + } + } + } + + /** + * Returns the configuration of a repository in a given repository + * directory. The repository configuration is read from a "repository.xml" + * file inside the repository directory. + *

    + * An exception is thrown if the directory does not exist or if + * the repository configuration file can not be read. + * + * @since Apache Jackrabbit 1.6 + * @param dir repository home directory + * @return repository configuration + * @throws ConfigurationException on configuration errors + */ + public static RepositoryConfig create(File dir) + throws ConfigurationException { + return create(new File(dir, REPOSITORY_XML), dir); + } + + /** + * Returns the configuration of a repository with the given configuration + * file and repository home directory. + *

    + * An exception is thrown if the directory does not exist or if + * the repository configuration file can not be read. + * + * @param xml The configuration file. + * @param dir repository home directory + * @return repository configuration + * @throws ConfigurationException on configuration errors + * @since Apache Jackrabbit 1.6 + */ + public static RepositoryConfig create(File xml, File dir) + throws ConfigurationException { + if (!dir.isDirectory()) { + throw new ConfigurationException( + "Repository directory " + dir + " does not exist"); + } + + if (!xml.isFile()) { + throw new ConfigurationException( + "Repository configuration file " + xml + " does not exist"); + } + + return create(new InputSource(xml.toURI().toString()), dir.getPath()); + } + + /** + * Convenience method that wraps the configuration file name into an + * {@link InputSource} and invokes the + * {@link #create(InputSource, String)} method. + * + * @param file repository configuration file name + * @param home repository home directory + * @return repository configuration + * @throws ConfigurationException on configuration errors + * @see #create(InputSource, String) + */ + public static RepositoryConfig create(String file, String home) + throws ConfigurationException { + URI uri = new File(file).toURI(); + return create(new InputSource(uri.toString()), home); + } + + /** + * Convenience method that wraps the configuration URI into an + * {@link InputSource} and invokes the + * {@link #create(InputSource, String)} method. + * + * @param uri repository configuration URI + * @param home repository home directory + * @return repository configuration + * @throws ConfigurationException on configuration errors + * @see #create(InputSource, String) + */ + public static RepositoryConfig create(URI uri, String home) + throws ConfigurationException { + return create(new InputSource(uri.toString()), home); + } + + /** + * Convenience method that wraps the configuration input stream into an + * {@link InputSource} and invokes the + * {@link #create(InputSource, String)} method. + * + * @param input repository configuration input stream + * @param home repository home directory + * @return repository configuration + * @throws ConfigurationException on configuration errors + * @see #create(InputSource, String) + */ + public static RepositoryConfig create(InputStream input, String home) + throws ConfigurationException { + return create(new InputSource(input), home); + } + + /** + * Convenience method that invokes the + * {@link #create(InputSource, Properties)} method with the given + * repository home home directory path set as the ${rep.home} parser + * variable. Also all system properties are used as parser variables. + * + * @param xml repository configuration document + * @param home repository home directory + * @return repository configuration + * @throws ConfigurationException on configuration errors + */ + public static RepositoryConfig create(InputSource xml, String home) + throws ConfigurationException { + Properties variables = new Properties(System.getProperties()); + variables.setProperty(REPOSITORY_HOME_VARIABLE, home); + return create(xml, variables); + } + + /** + * Parses the given repository configuration document using the given + * parser variables. Note that the ${rep.home} variable should be set + * by the caller! + *

    + * Note that in addition to parsing the repository configuration, this + * method also initializes the configuration (creates the configured + * directories, etc.). The {@link ConfigurationParser} class should be + * used directly to just parse the configuration. + * + * @since Apache Jackrabbit 2.1 + * @param xml repository configuration document + * @param variables parser variables + * @return repository configuration + * @throws ConfigurationException on configuration errors + */ + public static RepositoryConfig create(InputSource xml, Properties variables) + throws ConfigurationException { + RepositoryConfigurationParser parser = + new RepositoryConfigurationParser(variables, new ConnectionFactory()); + + RepositoryConfig config = parser.parseRepositoryConfig(xml); + config.init(); + + return config; + } + + /** + * Creates a repository configuration object based on an existing configuration. The factories + * contained within the configuration will be newly initialized, but all other information + * will be the same. + * + * @param config repository configuration to create the new instance from + * @return repository configuration + * @throws ConfigurationException on configuration errors + */ + public static RepositoryConfig create(RepositoryConfig config) throws ConfigurationException + { + RepositoryConfig copiedConfig = new RepositoryConfig(config.home, config.sec, config.fsf, + config.workspaceDirectory, config.workspaceConfigDirectory, config.defaultWorkspace, + config.workspaceMaxIdleTime, config.template, config.vc, config.qhf, config.cc, + config.dsf, config.rlf, config.dsc, new ConnectionFactory(), config.parser); + copiedConfig.init(); + return copiedConfig; + } + + /** + * map of workspace names and workspace configurations + */ + private Map workspaces; + + /** + * Repository home directory. + */ + private final String home; + + /** + * The security config. + */ + private final SecurityConfig sec; + + /** + * Repository file system factory. + */ + private final FileSystemFactory fsf; + + /** + * Name of the default workspace. + */ + private final String defaultWorkspace; + + /** + * the default parser + */ + private final RepositoryConfigurationParser parser; + + /** + * Workspace physical root directory. This directory contains a subdirectory + * for each workspace in this repository, i.e. the physical workspace home + * directory. Each workspace is configured by a workspace configuration file + * either contained in the workspace home directory or, optionally, located + * in a subdirectory of {@link #workspaceConfigDirectory} within the + * repository file system if such has been specified. + */ + private final String workspaceDirectory; + + /** + * Path to workspace configuration root directory within the + * repository file system or null if none was specified. + */ + private final String workspaceConfigDirectory; + + /** + * Amount of time in seconds after which an idle workspace is automatically + * shutdown. + */ + private final int workspaceMaxIdleTime; + + /** + * The workspace configuration template. Used in creating new workspace + * configuration files. + */ + private final Element template; + + /** + * Repository versioning configuration. + */ + private final VersioningConfig vc; + + /** + * Query handler factory, or null if not configured. + */ + private final QueryHandlerFactory qhf; + + /** + * Optional cluster configuration. + */ + private final ClusterConfig cc; + + /** + * The data store factory. + */ + private final DataStoreFactory dsf; + + /** + * The repository lock mechanism factory. + */ + private final RepositoryLockMechanismFactory rlf; + + /** + * The configuration for the used DataSources. + */ + private final DataSourceConfig dsc; + + /** + * The {@link ConnectionFactory} + */ + private final ConnectionFactory cf; + + /** + * Creates a repository configuration object. + * + * @param home repository home directory + * @param sec the security configuration + * @param fsf file system factory + * @param workspaceDirectory workspace root directory + * @param workspaceConfigDirectory optional workspace configuration directory + * @param defaultWorkspace name of the default workspace + * @param workspaceMaxIdleTime maximum workspace idle time in seconds + * @param template workspace configuration template + * @param vc versioning configuration + * @param qhf query handler factory for the system search manager + * @param cc optional cluster configuration + * @param dsf data store factory + * @param rlf the RepositoryLockMechanismFactory + * @param dsc the DataSource configuration + * @param cf the ConnectionFactory for all DatabasAware beans + * @param parser configuration parser + */ + public RepositoryConfig( + String home, SecurityConfig sec, FileSystemFactory fsf, + String workspaceDirectory, String workspaceConfigDirectory, + String defaultWorkspace, int workspaceMaxIdleTime, + Element template, VersioningConfig vc, QueryHandlerFactory qhf, + ClusterConfig cc, DataStoreFactory dsf, + RepositoryLockMechanismFactory rlf, + DataSourceConfig dsc, + ConnectionFactory cf, + RepositoryConfigurationParser parser) { + workspaces = new HashMap(); + this.home = home; + this.sec = sec; + this.fsf = fsf; + this.workspaceDirectory = workspaceDirectory; + this.workspaceConfigDirectory = workspaceConfigDirectory; + this.workspaceMaxIdleTime = workspaceMaxIdleTime; + this.defaultWorkspace = defaultWorkspace; + this.template = template; + this.vc = vc; + this.qhf = qhf; + this.cc = cc; + this.dsf = dsf; + this.rlf = rlf; + this.dsc = dsc; + this.cf = cf; + this.parser = parser; + } + + /** + * Initializes the repository configuration. This method loads the + * configurations for all available workspaces. + * + * @throws ConfigurationException on initialization errors + * @throws IllegalStateException if the repository configuration has already + * been initialized + */ + public void init() throws ConfigurationException, IllegalStateException { + + // This needs to be done here and not by clients (e.g., RepositoryImpl ctor) because + // fsf is used below and this might be a DatabaseAware FileSystem + try { + cf.registerDataSources(dsc); + } catch (RepositoryException e) { + throw new ConfigurationException("failed to register data sources", e); + } + + if (!workspaces.isEmpty()) { + throw new IllegalStateException( + "Repository configuration has already been initialized."); + } + + // Get the physical workspace root directory (create it if not found) + File directory = new File(workspaceDirectory); + if (!directory.exists()) { + boolean directoryCreated = directory.mkdirs(); + if (!directoryCreated) { + throw new ConfigurationException("Cannot create workspace root directory " + directory); + } + } + + // Get all workspace subdirectories + if (workspaceConfigDirectory != null) { + // a configuration directory had been specified; search for + // workspace configurations in virtual repository file system + // rather than in physical workspace root directory on disk + try { + FileSystem fs = fsf.getFileSystem(); + try { + if (!fs.exists(workspaceConfigDirectory)) { + fs.createFolder(workspaceConfigDirectory); + } else { + String[] dirNames = fs.listFolders(workspaceConfigDirectory); + for (String dir : dirNames) { + String configDir = workspaceConfigDirectory + + FileSystem.SEPARATOR + dir; + WorkspaceConfig wc = loadWorkspaceConfig(fs, configDir); + if (wc != null) { + addWorkspaceConfig(wc); + } + } + + } + } finally { + fs.close(); + } + } catch (Exception e) { + throw new ConfigurationException( + "error while loading workspace configurations from path " + + workspaceConfigDirectory, e); + } + } else { + // search for workspace configurations in physical workspace root + // directory on disk + File[] files = directory.listFiles(); + if (files == null) { + throw new ConfigurationException( + "Invalid workspace root directory: " + workspaceDirectory); + } + + for (File file: files) { + WorkspaceConfig wc = loadWorkspaceConfig(file); + if (wc != null) { + addWorkspaceConfig(wc); + } + } + } + if (!workspaces.containsKey(defaultWorkspace)) { + if (!workspaces.isEmpty()) { + log.warn("Potential misconfiguration. No configuration found " + + "for default workspace: " + defaultWorkspace); + } + // create initial default workspace + createWorkspaceConfig(defaultWorkspace, (StringBuffer)null); + } + } + + /** + * Attempts to load a workspace configuration from the given physical + * workspace subdirectory. If the directory contains a valid workspace + * configuration file, then the configuration is parsed and returned as a + * workspace configuration object. The returned configuration object has not + * been initialized. + *

    + * This method returns null, if the given directory does + * not exist or does not contain a workspace configuration file. If an + * invalid configuration file is found, then a + * {@link ConfigurationException ConfigurationException} is thrown. + * + * @param directory physical workspace configuration directory on disk + * @return workspace configuration + * @throws ConfigurationException if the workspace configuration is invalid + */ + private WorkspaceConfig loadWorkspaceConfig(File directory) + throws ConfigurationException { + FileInputStream fin = null; + try { + File file = new File(directory, WORKSPACE_XML); + fin = new FileInputStream(file); + InputSource xml = new InputSource(fin); + xml.setSystemId(file.toURI().toString()); + + Properties variables = new Properties(); + variables.setProperty( + RepositoryConfigurationParser.WORKSPACE_HOME_VARIABLE, + directory.getPath()); + RepositoryConfigurationParser localParser = + parser.createSubParser(variables); + return localParser.parseWorkspaceConfig(xml); + } catch (FileNotFoundException e) { + return null; + } finally { + IOUtils.closeQuietly(fin); + } + } + + /** + * Attempts to load a workspace configuration from the given workspace + * subdirectory within the repository file system. If the directory contains + * a valid workspace configuration file, then the configuration is parsed + * and returned as a workspace configuration object. The returned + * configuration object has not been initialized. + *

    + * This method returns null, if the given directory does + * not exist or does not contain a workspace configuration file. If an + * invalid configuration file is found, then a + * {@link ConfigurationException ConfigurationException} is thrown. + * + * @param fs virtual file system where to look for the configuration file + * @param configDir workspace configuration directory in virtual file system + * @return workspace configuration + * @throws ConfigurationException if the workspace configuration is invalid + */ + private WorkspaceConfig loadWorkspaceConfig(FileSystem fs, String configDir) + throws ConfigurationException { + Reader configReader = null; + try { + String configPath = configDir + FileSystem.SEPARATOR + WORKSPACE_XML; + if (!fs.exists(configPath)) { + // no configuration file in this directory + return null; + } + + configReader = new InputStreamReader(fs.getInputStream(configPath)); + InputSource xml = new InputSource(configReader); + xml.setSystemId(configPath); + + // the physical workspace home directory (TODO encode name?) + File homeDir = new File( + workspaceDirectory, FileSystemPathUtil.getName(configDir)); + if (!homeDir.exists()) { + homeDir.mkdir(); + } + Properties variables = new Properties(parser.getVariables()); + variables.setProperty( + RepositoryConfigurationParser.WORKSPACE_HOME_VARIABLE, + homeDir.getPath()); + RepositoryConfigurationParser localParser = + parser.createSubParser(variables); + return localParser.parseWorkspaceConfig(xml); + } catch (FileSystemException e) { + throw new ConfigurationException("Failed to load workspace configuration", e); + } finally { + IOUtils.closeQuietly(configReader); + } + } + + /** + * Adds the given workspace configuration to the repository. + * + * @param wc workspace configuration + * @throws ConfigurationException if a workspace with the same name + * already exists + */ + private void addWorkspaceConfig(WorkspaceConfig wc) + throws ConfigurationException { + String name = wc.getName(); + if (!workspaces.containsKey(name)) { + workspaces.put(name, wc); + } else { + throw new ConfigurationException( + "Duplicate workspace configuration: " + name); + } + } + + /** + * Creates a new workspace configuration with the specified name and the + * specified workspace template + * This method creates a workspace configuration subdirectory, + * copies the workspace configuration template into it, and finally + * adds the created workspace configuration to the repository. + * The initialized workspace configuration object is returned to + * the caller. + * + * @param name workspace name + * @param template the workspace template + * @param configContent optional stringbuffer that will have the content + * of workspace configuration file written in + * @return created workspace configuration + * @throws ConfigurationException if creating the workspace configuration + * failed + */ + private synchronized WorkspaceConfig internalCreateWorkspaceConfig(String name, + Element template, + StringBuffer configContent) + throws ConfigurationException { + + // The physical workspace home directory on disk (TODO encode name?) + File directory = new File(workspaceDirectory, name); + + // Create the physical workspace directory, fail if it exists + // or cannot be created + if (!directory.mkdir()) { + if (directory.exists()) { + throw new ConfigurationException( + "Workspace directory already exists: " + name); + } else { + throw new ConfigurationException( + "Failed to create workspace directory: " + name); + } + } + + FileSystem virtualFS; + if (workspaceConfigDirectory != null) { + // a configuration directoy had been specified; + // workspace configurations are maintained in + // virtual repository file system + try { + virtualFS = fsf.getFileSystem(); + } catch (RepositoryException e) { + throw new ConfigurationException("File system configuration error", e); + } + } else { + // workspace configurations are maintained on disk + virtualFS = null; + } + try { + Writer configWriter; + + // get a writer for the workspace configuration file + if (virtualFS != null) { + // a configuration directoy had been specified; create workspace + // configuration in virtual repository file system rather than + // on disk + String configDir = workspaceConfigDirectory + + FileSystem.SEPARATOR + name; + String configFile = configDir + FileSystem.SEPARATOR + WORKSPACE_XML; + try { + // Create the directory + virtualFS.createFolder(configDir); + + configWriter = new OutputStreamWriter( + virtualFS.getOutputStream(configFile)); + } catch (FileSystemException e) { + throw new ConfigurationException( + "failed to create workspace configuration at path " + + configFile, e); + } + } else { + File file = new File(directory, WORKSPACE_XML); + try { + configWriter = new FileWriter(file); + } catch (IOException e) { + throw new ConfigurationException( + "failed to create workspace configuration at path " + + file.getPath(), e); + } + } + + // Create the workspace.xml file using the configuration template and + // the configuration writer. + try { + template.setAttribute("name", name); + + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + + if (configContent == null) { + transformer.transform( + new DOMSource(template), new StreamResult(configWriter)); + } else { + StringWriter writer = new StringWriter(); + transformer.transform( + new DOMSource(template), new StreamResult(writer)); + + String s = writer.getBuffer().toString(); + configWriter.write(s); + configContent.append(s); + } + } catch (IOException e) { + throw new ConfigurationException( + "Cannot create a workspace configuration file", e); + } catch (TransformerConfigurationException e) { + throw new ConfigurationException( + "Cannot create a workspace configuration writer", e); + } catch (TransformerException e) { + throw new ConfigurationException( + "Cannot create a workspace configuration file", e); + } finally { + IOUtils.closeQuietly(configWriter); + } + + // Load the created workspace configuration. + WorkspaceConfig wc; + if (virtualFS != null) { + String configDir = workspaceConfigDirectory + + FileSystem.SEPARATOR + name; + wc = loadWorkspaceConfig(virtualFS, configDir); + } else { + wc = loadWorkspaceConfig(directory); + } + if (wc != null) { + addWorkspaceConfig(wc); + return wc; + } else { + throw new ConfigurationException( + "Failed to load the created configuration for workspace " + + name + "."); + } + } finally { + try { + if (virtualFS != null) { + virtualFS.close(); + } + } catch (FileSystemException ignore) { + } + } + } + + /** + * Creates a new workspace configuration with the specified name. + * This method creates a workspace configuration subdirectory, + * copies the workspace configuration template into it, and finally + * adds the created workspace configuration to the repository. + * The initialized workspace configuration object is returned to + * the caller. + * + * @param name workspace name + * @param configContent optional StringBuffer that will have the content + * of workspace configuration file written in + * @return created workspace configuration + * @throws ConfigurationException if creating the workspace configuration + * failed + */ + public WorkspaceConfig createWorkspaceConfig(String name, StringBuffer configContent) + throws ConfigurationException { + // use workspace template from repository.xml + return internalCreateWorkspaceConfig(name, template, configContent); + } + + /** + * Creates a new workspace configuration with the specified name. This + * method uses the provided workspace template to create the + * repository config instead of the template that is present in the + * repository configuration. + *

    + * This method creates a workspace configuration subdirectory, + * copies the workspace configuration template into it, and finally + * adds the created workspace configuration to the repository. + * The initialized workspace configuration object is returned to + * the caller. + * + * @param name workspace name + * @param template the workspace template + * @return created workspace configuration + * @throws ConfigurationException if creating the workspace configuration + * failed + */ + public WorkspaceConfig createWorkspaceConfig(String name, + InputSource template) + throws ConfigurationException { + ConfigurationParser parser = new ConfigurationParser(new Properties()); + Element workspaceTemplate = parser.parseXML(template); + return internalCreateWorkspaceConfig(name, workspaceTemplate, null); + } + + /** + * Returns the repository home directory. + * + * @return repository home directory + */ + public String getHomeDir() { + return home; + } + + /** + * Creates and returns the configured repository file system. + * + * @return the configured {@link FileSystem} + * @throws RepositoryException if the file system can not be created + */ + public FileSystem getFileSystem() throws RepositoryException { + return fsf.getFileSystem(); + } + + /** + * Returns the repository name. The repository name can be used for + * JAAS app-entry configuration. + * + * @return repository name + * @deprecated Use {@link SecurityConfig#getAppName()} instead. + */ + public String getAppName() { + return sec.getAppName(); + } + + /** + * Returns the repository access manager configuration. + * + * @return access manager configuration + * @deprecated Use {@link SecurityConfig#getAccessManagerConfig()} instead. + */ + public AccessManagerConfig getAccessManagerConfig() { + return sec.getAccessManagerConfig(); + } + + /** + * Returns the repository login module configuration. + * + * @return login module configuration, or null if standard + * JAAS mechanism should be used. + * @deprecated Use {@link SecurityConfig#getLoginModuleConfig()} instead. + */ + public LoginModuleConfig getLoginModuleConfig() { + return sec.getLoginModuleConfig(); + } + + /** + * Returns the repository security configuration. + * + * @return security configuration + */ + public SecurityConfig getSecurityConfig() { + return sec; + } + + /** + * Returns the workspace root directory. + * + * @return workspace root directory + */ + public String getWorkspacesConfigRootDir() { + return workspaceDirectory; + } + + /** + * Returns the name of the default workspace. + * + * @return name of the default workspace + */ + public String getDefaultWorkspaceName() { + return defaultWorkspace; + } + + /** + * Returns the amount of time in seconds after which an idle workspace is + * automatically shutdown. If zero then idle workspaces will never be + * automatically shutdown. + * + * @return maximum workspace idle time in seconds + */ + public int getWorkspaceMaxIdleTime() { + return workspaceMaxIdleTime; + } + + /** + * Returns all workspace configurations. + * + * @return workspace configurations + */ + public Collection getWorkspaceConfigs() { + return workspaces.values(); + } + + /** + * Returns the configuration of the specified workspace. + * + * @param name workspace name + * @return workspace configuration, or null if the named + * workspace does not exist + */ + public WorkspaceConfig getWorkspaceConfig(String name) { + return workspaces.get(name); + } + + /** + * Returns the repository versioning configuration. + * + * @return versioning configuration + */ + public VersioningConfig getVersioningConfig() { + return vc; + } + + /** + * Checks whether search configuration is present. + * + * @return true if search is configured, + * false otherwise + */ + public boolean isSearchEnabled() { + return qhf != null; + } + + /** + * Returns the initialized query handler, or null if one + * has not been configured. + * + * @return initialized query handler, or null + */ + public QueryHandler getQueryHandler(QueryHandlerContext context) + throws RepositoryException { + if (qhf != null) { + return qhf.getQueryHandler(context); + } else { + return null; + } + } + + /** + * Returns the cluster configuration. Returns null if clustering + * has not been configured. + * + * @return the cluster configuration or null if clustering + * has not been configured. + */ + public ClusterConfig getClusterConfig() { + return cc; + } + + /** + * Returns the {@link ConnectionFactory} for this repository. + * Please note that it must be closed explicitly. + * + * @return The connection factory configured for this repository. + */ + public ConnectionFactory getConnectionFactory() { + return cf; + } + + /** + * Creates and returns the configured data store. Returns + * null if a data store has not been configured. + * + * @return the configured data store, or null + * @throws RepositoryException if the data store can not be created + */ + public DataStore getDataStore() throws RepositoryException { + return dsf.getDataStore(); + } + + /** + * Creates and returns the configured repository lock mechanism. This method + * returns the default repository lock mechanism if no other mechanism is + * configured. + * + * @return the repository lock mechanism (never null) + * @throws RepositoryException if the repository lock mechanism can not be created + */ + public RepositoryLockMechanism getRepositoryLockMechanism() throws RepositoryException { + return rlf.getRepositoryLockMechanism(); + } + +} + diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java new file mode 100644 index 00000000000..6b815e3d580 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java @@ -0,0 +1,1182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.config; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.UUID; + +import javax.jcr.RepositoryException; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.data.DataStoreFactory; +import org.apache.jackrabbit.core.data.MultiDataStore; +import org.apache.jackrabbit.core.data.MultiDataStoreAware; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemFactory; +import org.apache.jackrabbit.core.journal.AbstractJournal; +import org.apache.jackrabbit.core.journal.Journal; +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.JournalFactory; +import org.apache.jackrabbit.core.query.QueryHandler; +import org.apache.jackrabbit.core.query.QueryHandlerContext; +import org.apache.jackrabbit.core.query.QueryHandlerFactory; +import org.apache.jackrabbit.core.state.DefaultISMLocking; +import org.apache.jackrabbit.core.state.ISMLocking; +import org.apache.jackrabbit.core.state.ISMLockingFactory; +import org.apache.jackrabbit.core.util.RepositoryLock; +import org.apache.jackrabbit.core.util.RepositoryLockMechanism; +import org.apache.jackrabbit.core.util.RepositoryLockMechanismFactory; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +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; + +/** + * Configuration parser. This class is used to parse the repository and + * workspace configuration files. + *

    + * The following code sample outlines the usage of this class: + *

    + *     Properties variables = ...; // parser variables
    + *     RepositoryConfigurationParser parser =
    + *         new RepositoryConfigurationParser(variables);
    + *     RepositoryConfig rc = parser.parseRepositoryConfig(...);
    + *     WorkspaceConfig wc = parser.parseWorkspaceConfig(...);
    + * 
    + *

    + * Note that the configuration objects returned by this parser are not + * initialized. The caller needs to initialize the configuration objects + * before using them. + */ +public class RepositoryConfigurationParser extends ConfigurationParser { + + /** Name of the repository home directory parser variable. */ + public static final String REPOSITORY_HOME_VARIABLE = "rep.home"; + + /** Name of the repository configuration file parser variable. */ + public static final String REPOSITORY_CONF_VARIABLE = "rep.conf"; + + /** Name of the workspace home directory parser variable. */ + public static final String WORKSPACE_HOME_VARIABLE = "wsp.home"; + + /** Name of the repository name parser variable. */ + public static final String WORKSPACE_NAME_VARIABLE = "wsp.name"; + + /** Name of the security configuration element. */ + public static final String SECURITY_ELEMENT = "Security"; + + /** Name of the security manager configuration element. */ + public static final String SECURITY_MANAGER_ELEMENT = "SecurityManager"; + + /** Name of the access manager configuration element. */ + public static final String ACCESS_MANAGER_ELEMENT = "AccessManager"; + + /** Name of the login module configuration element. */ + public static final String LOGIN_MODULE_ELEMENT = "LoginModule"; + + /** + * Name of the optional WorkspaceAccessManager element defining which + * implementation of WorkspaceAccessManager to be used. + */ + private static final String WORKSPACE_ACCESS_ELEMENT = "WorkspaceAccessManager"; + + /** + * Name of the optional UserManagerConfig element that defines the + * configuration options for the user manager. + */ + private static final String USER_MANAGER_ELEMENT = "UserManager"; + + /** Name of the general workspace configuration element. */ + public static final String WORKSPACES_ELEMENT = "Workspaces"; + + /** Name of the workspace configuration element. */ + public static final String WORKSPACE_ELEMENT = "Workspace"; + + /** Name of the versioning configuration element. */ + public static final String VERSIONING_ELEMENT = "Versioning"; + + /** Name of the file system configuration element. */ + public static final String FILE_SYSTEM_ELEMENT = "FileSystem"; + + /** Name of the cluster configuration element. */ + public static final String CLUSTER_ELEMENT = "Cluster"; + + /** Name of the data source configuration element. */ + public static final String DATASOURCES_ELEMENT = "DataSources"; + + /** Name of the data source configuration element. */ + public static final String DATASOURCE_ELEMENT = "DataSource"; + + /** Name of the journal configuration element. */ + public static final String JOURNAL_ELEMENT = "Journal"; + + /** Name of the data store configuration element. */ + public static final String DATA_STORE_ELEMENT = "DataStore"; + + /** Name of the repository lock mechanism configuration element. */ + public static final String REPOSITORY_LOCK_MECHANISM_ELEMENT = + "RepositoryLockMechanism"; + + /** Name of the persistence manager configuration element. */ + public static final String PERSISTENCE_MANAGER_ELEMENT = + "PersistenceManager"; + + /** Name of the search index configuration element. */ + public static final String SEARCH_INDEX_ELEMENT = "SearchIndex"; + + /** Name of the ism locking configuration element. */ + public static final String ISM_LOCKING_ELEMENT = "ISMLocking"; + + /** Name of the application name configuration attribute. */ + public static final String APP_NAME_ATTRIBUTE = "appName"; + + /** Name of the workspace containing security data. */ + public static final String WSP_NAME_ATTRIBUTE = "workspaceName"; + + /** Name of the root path configuration attribute. */ + public static final String ROOT_PATH_ATTRIBUTE = "rootPath"; + + /** Name of the config root path configuration attribute. */ + public static final String CONFIG_ROOT_PATH_ATTRIBUTE = "configRootPath"; + + /** Name of the maximum idle time configuration attribute. */ + public static final String MAX_IDLE_TIME_ATTRIBUTE = "maxIdleTime"; + + /** Name of the default workspace configuration attribute. */ + public static final String DEFAULT_WORKSPACE_ATTRIBUTE = + "defaultWorkspace"; + + /** Name of the id configuration attribute. */ + public static final String ID_ATTRIBUTE = "id"; + + /** Name of the syncDelay configuration attribute. */ + public static final String SYNC_DELAY_ATTRIBUTE = "syncDelay"; + + /** Name of the stopDelay configuration attribute. */ + public static final String STOP_DELAY_ATTRIBUTE = "stopDelay"; + + /** Name of the default search index implementation class. */ + public static final String DEFAULT_QUERY_HANDLER = + "org.apache.jackrabbit.core.query.lucene.SearchIndex"; + + /** Name of the clustered configuration attribute. */ + public static final String CLUSTERED_ATTRIBUTE = "clustered"; + + /** Name of the primary DataStore class attribute. */ + public static final String PRIMARY_DATASTORE_ATTRIBUTE = "primary"; + + /** Name of the archive DataStore class attribute. */ + public static final String ARCHIVE_DATASTORE_ATTRIBUTE = "archive"; + + /** Default synchronization delay, in milliseconds. */ + public static final String DEFAULT_SYNC_DELAY = "5000"; + + /** + * Default stop delay, in milliseconds or -1 if the default is derived + * from the sync delay. + */ + public static final String DEFAULT_STOP_DELAY = "-1"; + + /** Name of the workspace specific security configuration element */ + private static final String WSP_SECURITY_ELEMENT = "WorkspaceSecurity"; + + /** + * Name of the optional AccessControlProvider element defining which + * implementation of AccessControlProvider should be used. + */ + private static final String AC_PROVIDER_ELEMENT = "AccessControlProvider"; + + /** + * Optional configuration elements with the user manager configuration. + * @see org.apache.jackrabbit.core.security.user.action.AuthorizableAction + */ + private static final String AUTHORIZABLE_ACTION = "AuthorizableAction"; + + /** + * The repositories {@link ConnectionFactory}. + */ + protected final ConnectionFactory connectionFactory; + + protected BeanFactory beanFactory = new SimpleBeanFactory(); + + protected BeanConfigVisitor configVisitor = new NoOpConfigVisitor(); + + /** + * Element specifying the class of principals used to retrieve the userID + * in the 'class' attribute. + */ + private static final String USERID_CLASS_ELEMENT = "UserIdClass"; + + /** + * Name of the optional XmlImport config entry inside the workspace configuration. + */ + private static final String IMPORT_ELEMENT = "Import"; + private static final String IMPORT_PII_ELEMENT = "ProtectedItemImporter"; + private static final String IMPORT_PNI_ELEMENT = "ProtectedNodeImporter"; + private static final String IMPORT_PPI_ELEMENT = "ProtectedPropertyImporter"; + + /** + * Name of the cluster node id file. + */ + private static final String CLUSTER_NODE_ID_FILE = "cluster_node.id"; + + /** + * Creates a new configuration parser with the given parser variables + * and connection factory. + * + * @param variables parser variables + * @param connectionFactory connection factory + */ + protected RepositoryConfigurationParser( + Properties variables, ConnectionFactory connectionFactory) { + super(variables); + this.connectionFactory = connectionFactory; + } + + /** + * Creates a new configuration parser with the given parser variables. + * + * @param variables parser variables + */ + public RepositoryConfigurationParser(Properties variables) { + this(variables, new ConnectionFactory()); + } + + /** + * Parses repository configuration. Repository configuration uses the + * following format: + *

    +     *   <Repository>
    +     *     <FileSystem ...>
    +     *     <Security appName="...">
    +     *       <SecurityManager ...>
    +     *       <AccessManager ...>
    +     *       <LoginModule ... (optional)>
    +     *     </Security>
    +     *     <Workspaces rootPath="..." defaultWorkspace="..."/>
    +     *     <Workspace ...>
    +     *     <Versioning ...>
    +     *   </Repository>
    +     * 
    + *

    + * The FileSystem element is a + * {@link #parseBeanConfig(Element,String) bean configuration} element, + * that specifies the file system implementation for storing global + * repository information. The Security element contains + * an AccessManager bean configuration element and the + * JAAS name of the repository application. The Workspaces + * element contains general workspace parameters, and the + * Workspace element is a template for the individual + * workspace configuration files. The Versioning element + * contains + * {@link #parseVersioningConfig(Element) versioning configuration} for + * the repository. + *

    + * In addition to the configured information, the returned repository + * configuration object also contains the repository home directory path + * that is given as the ${rep.home} parser variable. Note that the + * variable must be available for the configuration document to + * be correctly parsed. + *

    + * {@link #replaceVariables(String) Variable replacement} is performed + * on the security application name attribute, the general workspace + * configuration attributes, and on the file system, access manager, + * and versioning configuration information. + *

    + * Note that the returned repository configuration object has not been + * initialized. + * + * @param xml repository configuration document + * @return repository configuration + * @throws ConfigurationException if the configuration is broken + * @see #parseBeanConfig(Element, String) + * @see #parseVersioningConfig(Element) + */ + public RepositoryConfig parseRepositoryConfig(InputSource xml) + throws ConfigurationException { + Element root = parseXML(xml, true); + + // Repository home directory + String home = getVariables().getProperty(REPOSITORY_HOME_VARIABLE); + + // File system implementation + FileSystemFactory fsf = getFileSystemFactory(root, FILE_SYSTEM_ELEMENT); + + // Security configuration and access manager implementation + Element security = getElement(root, SECURITY_ELEMENT); + SecurityConfig securityConfig = parseSecurityConfig(security); + + // General workspace configuration + Element workspaces = getElement(root, WORKSPACES_ELEMENT); + String workspaceDirectory = replaceVariables( + getAttribute(workspaces, ROOT_PATH_ATTRIBUTE)); + + String workspaceConfigDirectory = + getAttribute(workspaces, CONFIG_ROOT_PATH_ATTRIBUTE, null); + + String defaultWorkspace = replaceVariables( + getAttribute(workspaces, DEFAULT_WORKSPACE_ATTRIBUTE)); + + int maxIdleTime = Integer.parseInt( + getAttribute(workspaces, MAX_IDLE_TIME_ATTRIBUTE, "0")); + + // Workspace configuration template + Element template = getElement(root, WORKSPACE_ELEMENT); + + // Versioning configuration + VersioningConfig vc = parseVersioningConfig(root); + + // Query handler implementation + QueryHandlerFactory qhf = getQueryHandlerFactory(root); + + // Optional journal configuration + ClusterConfig cc = parseClusterConfig(root, new File(home)); + + // Optional data store factory + DataStoreFactory dsf = getDataStoreFactory(root, home); + + RepositoryLockMechanismFactory rlf = getRepositoryLockMechanismFactory(root); + + // Optional data source configuration + DataSourceConfig dsc = parseDataSourceConfig(root); + + return new RepositoryConfig(home, securityConfig, fsf, + workspaceDirectory, workspaceConfigDirectory, defaultWorkspace, + maxIdleTime, template, vc, qhf, cc, dsf, rlf, dsc, connectionFactory, this); + } + + + /** + * {@inheritDoc} + */ + @Override + protected BeanConfig parseBeanConfig(Element parent, String name) throws ConfigurationException { + BeanConfig cfg = super.parseBeanConfig(parent, name); + cfg.setConnectionFactory(connectionFactory); + cfg.setInstanceFactory(beanFactory); + configVisitor.visit(cfg); + return cfg; + } + + /** + * {@inheritDoc} + */ + @Override + protected BeanConfig parseBeanConfig(Element element) throws ConfigurationException { + BeanConfig cfg = super.parseBeanConfig(element); + cfg.setConnectionFactory(connectionFactory); + cfg.setInstanceFactory(beanFactory); + configVisitor.visit(cfg); + return cfg; + } + + /** + * Parses security configuration. Security configuration + * uses the following format: + *

    +     *   <Security appName="...">
    +     *     <SecurityManager ...>
    +     *     <AccessManager ...>
    +     *     <LoginModule ... (optional)>
    +     *   </Security>
    +     * 
    + *

    + * The SecurityManager, the AccessManager + * and LoginModule are all + * {@link #parseBeanConfig(Element,String) bean configuration} + * elements. + *

    + * The login module is an optional feature of repository configuration. + * + * @param security the <security> element. + * @return the security configuration. + * @throws ConfigurationException if the configuration is broken + */ + public SecurityConfig parseSecurityConfig(Element security) + throws ConfigurationException { + String appName = getAttribute(security, APP_NAME_ATTRIBUTE); + + SecurityManagerConfig smc = parseSecurityManagerConfig(security); + AccessManagerConfig amc = parseAccessManagerConfig(security); + LoginModuleConfig lmc = parseLoginModuleConfig(security); + + return new SecurityConfig(appName, smc, amc, lmc); + } + + /** + * Parses the security manager configuration. + * + * @param security the <security> element. + * @return the security manager configuration or null. + * @throws ConfigurationException if the configuration is broken + */ + public SecurityManagerConfig parseSecurityManagerConfig(Element security) + throws ConfigurationException { + // Optional security manager config entry + Element smElement = getElement(security, SECURITY_MANAGER_ELEMENT, false); + if (smElement != null) { + BeanConfig bc = parseBeanConfig(smElement); + String wspAttr = getAttribute(smElement, WSP_NAME_ATTRIBUTE, null); + + BeanConfig wac = null; + Element element = getElement(smElement, WORKSPACE_ACCESS_ELEMENT, false); + if (element != null) { + wac = parseBeanConfig(smElement, WORKSPACE_ACCESS_ELEMENT); + } + + UserManagerConfig umc = null; + element = getElement(smElement, USER_MANAGER_ELEMENT, false); + if (element != null) { + Element[] acElements = getElements(element, AUTHORIZABLE_ACTION, false); + BeanConfig[] aaConfig = new BeanConfig[acElements.length]; + for (int i = 0; i < acElements.length; i++) { + aaConfig[i] = parseBeanConfig(acElements[i]); + } + umc = new UserManagerConfig(parseBeanConfig(element), aaConfig); + } + + BeanConfig uidcc = null; + element = getElement(smElement, USERID_CLASS_ELEMENT, false); + if (element != null) { + uidcc = parseBeanConfig(element); + } + + return new SecurityManagerConfig(bc, wspAttr, wac, umc, uidcc); + } else { + return null; + } + } + + /** + * Parses the access manager configuration. + * + * @param security the <security> element. + * @return the access manager configuration or null. + * @throws ConfigurationException if the configuration is broken + */ + public AccessManagerConfig parseAccessManagerConfig(Element security) + throws ConfigurationException { + // Optional access manager config entry + Element accessMgr = getElement(security, ACCESS_MANAGER_ELEMENT, false); + if (accessMgr != null) { + return new AccessManagerConfig(parseBeanConfig(accessMgr)); + } else { + return null; + } + } + + /** + * Parses the login module configuration. + * + * @param security the <security> element. + * @return the login module configuration or null. + * @throws ConfigurationException if the configuration is broken + */ + public LoginModuleConfig parseLoginModuleConfig(Element security) + throws ConfigurationException { + // Optional login module + Element loginModule = getElement(security, LOGIN_MODULE_ELEMENT, false); + + if (loginModule != null) { + return new LoginModuleConfig(parseBeanConfig(security, LOGIN_MODULE_ELEMENT)); + } else { + return null; + } + } + + /** + * Parses workspace configuration. Workspace configuration uses the + * following format: + *

    +     *   <Workspace name="...">
    +     *     <FileSystem ...>
    +     *     <PersistenceManager ...>
    +     *     <SearchIndex ...>
    +     *     <ISMLocking ...>
    +     *     <WorkspaceSecurity ...>
    +     *     <ISMLocking ...>
    +     *   </Workspace>
    +     * 
    + *

    + * All the child elements (FileSystem, + * PersistenceManager, and SearchIndex) are + * {@link #parseBeanConfig(Element,String) bean configuration} elements. + * In addition to bean configuration, the + * search element also contains + * configuration for the search file system. + *

    + * In addition to the configured information, the returned workspace + * configuration object also contains the workspace home directory path + * that is given as the ${wsp.home} parser variable. Note that the + * variable must be available for the configuration document to + * be correctly parsed. + *

    + * Variable replacement is performed on the optional workspace name + * attribute. If the name is not given, then the name of the workspace + * home directory is used as the workspace name. Once the name has been + * determined, it will be added as the ${wsp.name} variable in a temporary + * configuration parser that is used to parse the contained configuration + * elements. + *

    + * The search index configuration element is optional. If it is not given, + * then the workspace will not have search capabilities. + *

    + * The ism locking configuration element is optional. If it is not given, + * then a default implementation is used. + *

    + * Note that the returned workspace configuration object has not been + * initialized. + * + * @param xml workspace configuration document + * @return workspace configuration + * @throws ConfigurationException if the configuration is broken + * @see #parseBeanConfig(Element, String) + * @see #parseWorkspaceSecurityConfig(Element) + */ + public WorkspaceConfig parseWorkspaceConfig(InputSource xml) + throws ConfigurationException { + + Element root = parseXML(xml); + return parseWorkspaceConfig(root); + } + + /** + * Parse workspace config. + * + * @param root root element of the workspace configuration + * @return The workspace configuration + * @throws ConfigurationException + * @see #parseWorkspaceConfig(InputSource) + */ + protected WorkspaceConfig parseWorkspaceConfig(Element root) + throws ConfigurationException { + + // Workspace home directory + String home = getVariables().getProperty(WORKSPACE_HOME_VARIABLE); + + // Workspace name + String name = getAttribute(root, "name", new File(home).getName()); + + // Clustered attribute + boolean clustered = Boolean.valueOf( + getAttribute(root, CLUSTERED_ATTRIBUTE, "true")); + + // Create a temporary parser that contains the ${wsp.name} variable + Properties tmpVariables = (Properties) getVariables().clone(); + tmpVariables.put(WORKSPACE_NAME_VARIABLE, name); + RepositoryConfigurationParser tmpParser = createSubParser(tmpVariables); + + // File system implementation + FileSystemFactory fsf = + tmpParser.getFileSystemFactory(root, FILE_SYSTEM_ELEMENT); + + // Persistence manager implementation + PersistenceManagerConfig pmc = tmpParser.parsePersistenceManagerConfig(root); + + // Query handler implementation + QueryHandlerFactory qhf = tmpParser.getQueryHandlerFactory(root); + + // Item state manager locking configuration (optional) + ISMLockingFactory ismLockingFactory = + tmpParser.getISMLockingFactory(root); + + // workspace specific security configuration + WorkspaceSecurityConfig workspaceSecurityConfig = tmpParser.parseWorkspaceSecurityConfig(root); + + // optional config for import handling + ImportConfig importConfig = tmpParser.parseImportConfig(root); + + // default lock timeout + String to = getAttribute(root, "defaultLockTimeout", new Long(Long.MAX_VALUE).toString()); + long defaultLockTimeout; + try { + defaultLockTimeout = Long.parseLong(to); + } + catch (NumberFormatException ex) { + throw new ConfigurationException("defaultLockTimeout must be an integer value", ex); + } + + return new WorkspaceConfig( + home, name, clustered, fsf, pmc, qhf, + ismLockingFactory, workspaceSecurityConfig, importConfig, defaultLockTimeout); + } + + /** + * Parses search index configuration. Search index configuration + * uses the following format: + *

    +     *   <SearchIndex class="...">
    +     *     <param name="..." value="...">
    +     *     ...
    +     *     <FileSystem ...>
    +     *   </Search>
    +     * 
    + *

    + * Both the SearchIndex and FileSystem + * elements are {@link #parseBeanConfig(Element,String) bean configuration} + * elements. If the search implementation class is not given, then + * a default implementation is used. + *

    + * The search index is an optional feature of workspace configuration. + * If the search configuration element is not found, then this method + * returns null. + *

    + * The FileSystem element in a search index configuration is optional. + * However some implementations may require a FileSystem. + * + * @param parent parent of the SearchIndex element + * @return query handler factory + */ + protected QueryHandlerFactory getQueryHandlerFactory(final Element parent) { + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + final Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && SEARCH_INDEX_ELEMENT.equals(child.getNodeName())) { + return new QueryHandlerFactory() { + public QueryHandler getQueryHandler(QueryHandlerContext context) + throws RepositoryException { + Element element = (Element) child; + + // Optional file system implementation + FileSystem fs = null; + if (getElement(element, FILE_SYSTEM_ELEMENT, false) != null) { + fs = getFileSystemFactory( + element, FILE_SYSTEM_ELEMENT).getFileSystem(); + } + + // Search implementation class + String className = getAttribute( + element, CLASS_ATTRIBUTE, DEFAULT_QUERY_HANDLER); + BeanConfig config = new BeanConfig( + className, parseParameters(element)); + + QueryHandler handler = + config.newInstance(QueryHandler.class); + try { + handler.init(fs, context); + return handler; + } catch (IOException e) { + throw new RepositoryException( + "Unable to initialize query handler: " + handler, e); + } + } + }; + } + } + return null; + } + + + /** + * Read the optional WorkspaceSecurity Element of Workspace's configuration. + * It uses the following format: + *

    +     *   <WorkspaceSecurity>
    +     *     <AccessControlProvider class="..." (optional)>
    +     *   </WorkspaceSecurity>
    +     * 
    + * + * @param parent Workspace-Root-Element + * @return a new WorkspaceSecurityConfig or null + * if none is configured. + * @throws ConfigurationException + */ + public WorkspaceSecurityConfig parseWorkspaceSecurityConfig(Element parent) + throws ConfigurationException { + + BeanConfig acProviderConfig = null; + Element element = getElement(parent, WSP_SECURITY_ELEMENT, false); + if (element != null) { + Element provFact = getElement(element, AC_PROVIDER_ELEMENT, false); + if (provFact != null) { + acProviderConfig = parseBeanConfig(element, AC_PROVIDER_ELEMENT); + acProviderConfig.setValidate(false); // JCR-1920 + } + return new WorkspaceSecurityConfig(acProviderConfig); + } + return null; + } + + /** + * Read the optional Import Element of Workspace's configuration. It uses + * the following format: + *
    +     *   <Import>
    +     *     <ProtectedNodeImporter class="..." (optional)>
    +     *     <ProtectedNodeImporter class="..." (optional)>
    +     *     ...
    +     *     <ProtectedPropertyImporter class="..." (optional)>
    +     *   </Import>
    +     * 
    + * + * @param parent Workspace-Root-Element + * @return a new ImportConfig or null if none is + * configured. + * @throws ConfigurationException + */ + public ImportConfig parseImportConfig(Element parent) throws ConfigurationException { + List protectedItemImporters = new ArrayList(); + Element element = getElement(parent, IMPORT_ELEMENT, false); + if (element != null) { + NodeList children = element.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE) { + if (IMPORT_PNI_ELEMENT.equals(child.getNodeName()) || + IMPORT_PPI_ELEMENT.equals(child.getNodeName()) || + IMPORT_PII_ELEMENT.equals(child.getNodeName())) { + BeanConfig bc = parseBeanConfig((Element) child); + bc.setValidate(false); + protectedItemImporters.add(bc); + } // else: some other entry -> ignore. + } + } + return new ImportConfig(protectedItemImporters); + } + return null; + } + + /** + * Returns an ISM locking factory that creates {@link ISMLocking} instances + * based on the given configuration. ISM locking configuration uses the + * following format: + *
    +     *   <ISMLocking class="...">
    +     *     <param name="..." value="...">
    +     *     ...
    +     *   </ISMLocking>
    +     * 
    + *

    + * The ISMLocking is a + * {@link #parseBeanConfig(Element,String) bean configuration} element. + *

    + * ISM locking is an optional part of the workspace configuration. If + * the ISM locking element is not found, then the returned factory will + * create instances of the {@link DefaultISMLocking} class. + * + * @param parent parent of the ISMLocking element + * @return ISM locking factory + */ + protected ISMLockingFactory getISMLockingFactory(final Element parent) { + return new ISMLockingFactory() { + public ISMLocking getISMLocking() throws RepositoryException { + Element element = getElement(parent, ISM_LOCKING_ELEMENT, false); + if (element != null) { + return parseBeanConfig(element).newInstance(ISMLocking.class); + } else { + return new DefaultISMLocking(); + } + } + }; + } + + /** + * Parses versioning configuration. Versioning configuration uses the + * following format: + *

    +     *   <Versioning rootPath="...">
    +     *     <FileSystem ...>
    +     *     <PersistenceManager ...>
    +     *   </Versioning>
    +     * 
    + *

    + * Both the FileSystem and PersistenceManager + * elements are {@link #parseBeanConfig(Element,String) bean configuration} + * elements. In addition to the bean parameter values, + * {@link #replaceVariables(String) variable replacement} is performed + * also on the versioning root path attribute. + * + * @param parent parent of the Versioning element + * @return versioning configuration + * @throws ConfigurationException if the configuration is broken + */ + protected VersioningConfig parseVersioningConfig(Element parent) + throws ConfigurationException { + Element element = getElement(parent, VERSIONING_ELEMENT); + + // Versioning home directory + String home = + replaceVariables(getAttribute(element, ROOT_PATH_ATTRIBUTE)); + + // File system implementation + FileSystemFactory fsf = + getFileSystemFactory(element, FILE_SYSTEM_ELEMENT); + + // Persistence manager implementation + PersistenceManagerConfig pmc = parsePersistenceManagerConfig(element); + + // Item state manager locking configuration (optional) + ISMLockingFactory ismLockingFactory = + getISMLockingFactory(element); + + return new VersioningConfig(home, fsf, pmc, ismLockingFactory); + } + + /** + * Parses cluster configuration. Cluster configuration uses the following format: + *

    +     *   <Cluster>
    +     *     <Journal ...>
    +     *   </Journal>
    +     * 
    + *

    + * Cluster is a {@link #parseBeanConfig(Element,String) bean configuration} + * element. + *

    + * Clustering is an optional feature. If the cluster element is not found, then this + * method returns null. + * + * @param parent parent of the Journal element + * @param home repository home directory + * @return cluster configuration, or null + * @throws ConfigurationException if the configuration is broken + */ + protected ClusterConfig parseClusterConfig(Element parent, File home) + throws ConfigurationException { + + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && CLUSTER_ELEMENT.equals(child.getNodeName())) { + Element element = (Element) child; + + // Find the cluster node id + String id = + System.getProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID); + String value = getAttribute(element, ID_ATTRIBUTE, null); + if (value != null) { + id = replaceVariables(value); + } else if (id == null) { + File file = new File(home, CLUSTER_NODE_ID_FILE); + try { + if (file.exists() && file.canRead()) { + id = FileUtils.readFileToString(file).trim(); + } else { + id = UUID.randomUUID().toString(); + FileUtils.writeStringToFile(file, id); + } + } catch (IOException e) { + throw new ConfigurationException( + "Failed to access cluster node id: " + file, e); + } + } + + long syncDelay = Long.parseLong(replaceVariables(getAttribute( + element, SYNC_DELAY_ATTRIBUTE, DEFAULT_SYNC_DELAY))); + long stopDelay = Long.parseLong(replaceVariables(getAttribute( + element, STOP_DELAY_ATTRIBUTE, "-1"))); + + JournalFactory jf = getJournalFactory(element, home, id); + return new ClusterConfig(id, syncDelay, stopDelay, jf); + } + } + return null; + } + + /** + * Parses journal configuration. Journal configuration uses the following format: + *

    +     *   <Journal class="...">
    +     *     <param name="..." value="...">
    +     *     ...
    +     *   </Journal>
    +     * 
    + *

    + * Journal is a {@link #parseBeanConfig(Element,String) bean configuration} + * element. + * + * @param cluster parent cluster element + * @param home repository home directory + * @param id cluster node id + * @return journal factory + * @throws ConfigurationException if the configuration is broken + */ + protected JournalFactory getJournalFactory( + final Element cluster, final File home, final String id) + throws ConfigurationException { + return new JournalFactory() { + public Journal getJournal(NamespaceResolver resolver) + throws RepositoryException { + BeanConfig config = parseBeanConfig(cluster, JOURNAL_ELEMENT); + Journal journal = config.newInstance(Journal.class); + if (journal instanceof AbstractJournal) { + ((AbstractJournal) journal).setRepositoryHome(home); + } + try { + journal.init(id, resolver); + } catch (JournalException e) { + // TODO: Should JournalException extend RepositoryException? + throw new RepositoryException( + "Journal initialization failed: " + journal, e); + } + return journal; + } + }; + } + + /** + * Parses the DataSources configuration under the given parent. It has the following format: + *

    +     *   <DataSources>
    +     *     <DataSource name="...">
    +     *       <param name="..." value="...">
    +     *       ...
    +     *     </DataSource>
    +     *     <DataSource name="...">
    +     *       <param name="..." value="...">
    +     *       ...
    +     *     </DataSource>
    +     *   </DataSources>
    +     * 
    + *

    + * @param parent the parent of the DataSources element + * @return a {@link DataSourceConfig} for the repository + * @throws ConfigurationException on error + */ + protected DataSourceConfig parseDataSourceConfig(Element parent) + throws ConfigurationException { + DataSourceConfig dsc = new DataSourceConfig(); + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && DATASOURCES_ELEMENT.equals(child.getNodeName())) { + Element element = (Element) child; + NodeList children2 = element.getChildNodes(); + // Process the DataSource entries: + for (int j = 0; j < children2.getLength(); j++) { + Node child2 = children2.item(j); + if (child2.getNodeType() == Node.ELEMENT_NODE + && DATASOURCE_ELEMENT.equals(child2.getNodeName())) { + Element dsdef = (Element) child2; + String logicalName = getAttribute(dsdef, "name"); + Properties props = parseParameters(dsdef); + dsc.addDataSourceDefinition(logicalName, props); + } + } + } + } + return dsc; + } + + /** + * Parses data store configuration. Data store configuration uses the following format: + *

    +     *   <DataStore class="...">
    +     *     <param name="..." value="...">
    +     *     ...
    +     *   </DataStore>
    +     * 
    + * Its also possible to configure a multi data store. The configuration uses following format: + *
    +     *   <DataStore class="org.apache.jackrabbit.core.data.MultiDataStore">
    +     *     <param name="primary" value="org.apache.jackrabbit.core.data.db.XXDataStore">
    +     *         <param name="..." value="...">
    +     *         ...
    +     *     </param>
    +     *     <param name="archive" value="org.apache.jackrabbit.core.data.db.XXDataStore">
    +     *         <param name="..." value="...">
    +     *         ...
    +     *     </param>
    +     *   </DataStore>
    +     * 
    + *

    + * DataStore is a {@link #parseBeanConfig(Element,String) bean configuration} + * element. + * + * @param parent configuration element + * @param directory the repository directory + * @return data store factory + * @throws ConfigurationException if the configuration is broken + */ + protected DataStoreFactory getDataStoreFactory( + final Element parent, final String directory) + throws ConfigurationException { + return new DataStoreFactory() { + public DataStore getDataStore() throws RepositoryException { + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && DATA_STORE_ELEMENT.equals(child.getNodeName())) { + BeanConfig bc = parseBeanConfig(parent, DATA_STORE_ELEMENT); + bc.setValidate(false); + DataStore store = bc.newInstance(DataStore.class); + if (store instanceof MultiDataStore) { + DataStore primary = null; + DataStore archive = null; + NodeList subParamNodes = child.getChildNodes(); + for (int x = 0; x < subParamNodes.getLength(); x++) { + Node paramNode = subParamNodes.item(x); + if (paramNode.getNodeType() == Node.ELEMENT_NODE + && (PRIMARY_DATASTORE_ATTRIBUTE.equals(paramNode.getAttributes().getNamedItem("name").getNodeValue()) + || ARCHIVE_DATASTORE_ATTRIBUTE.equals(paramNode.getAttributes().getNamedItem("name").getNodeValue()))) { + try { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + Element newParent = document.createElement("parent"); + document.appendChild(newParent); + Element datastoreElement = document.createElement(DATA_STORE_ELEMENT); + newParent.appendChild(datastoreElement); + NodeList childNodes = paramNode.getChildNodes(); + for (int y = 0; childNodes.getLength() > y; y++) { + datastoreElement.appendChild(document.importNode(childNodes.item(y), true)); + } + NamedNodeMap attributes = paramNode.getAttributes(); + for (int z = 0; attributes.getLength() > z; z++) { + Node item = attributes.item(z); + datastoreElement.setAttribute(CLASS_ATTRIBUTE, item.getNodeValue()); + } + DataStore subDataStore = getDataStoreFactory(newParent, directory).getDataStore(); + if (!MultiDataStoreAware.class.isAssignableFrom(subDataStore.getClass())) { + throw new ConfigurationException("Only MultiDataStoreAware datastore's can be used within a MultiDataStore."); + } + String type = getAttribute((Element) paramNode, NAME_ATTRIBUTE); + if (PRIMARY_DATASTORE_ATTRIBUTE.equals(type)) { + primary = subDataStore; + } else if (ARCHIVE_DATASTORE_ATTRIBUTE.equals(type)) { + archive = subDataStore; + } + } catch (Exception e) { + throw new ConfigurationException("Failed to parse the MultiDataStore element.", e); + } + } + } + if (primary == null || archive == null) { + throw new ConfigurationException("A MultiDataStore must have configured a primary and archive datastore"); + } + ((MultiDataStore) store).setPrimaryDataStore(primary); + ((MultiDataStore) store).setArchiveDataStore(archive); + } + store.init(directory); + return store; + } + } + return null; + } + }; + } + + /** + * Parses repository lock mechanism configuration. Repository lock mechanism + * configuration uses the following format: + *

    +     *   <RepositoryLockMechanism class="..." >
    +     *     <param name="..." value="...">
    +     *     ...
    +     *   </RepositoryLockMechanism>
    +     * 
    + *

    + * RepositoryLockMechanism is a + * {@link #parseBeanConfig(Element,String) bean configuration} element. + * + * @param root the root configuration element + * @return repository lock mechanism factory + */ + protected RepositoryLockMechanismFactory getRepositoryLockMechanismFactory(final Element root) { + return new RepositoryLockMechanismFactory() { + public RepositoryLockMechanism getRepositoryLockMechanism() throws RepositoryException { + NodeList children = root.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && REPOSITORY_LOCK_MECHANISM_ELEMENT.equals(child.getNodeName())) { + BeanConfig bc = + parseBeanConfig(root, REPOSITORY_LOCK_MECHANISM_ELEMENT); + return bc.newInstance(RepositoryLockMechanism.class); + } + } + return new RepositoryLock(); + } + }; + } + + /** + * Parses the PersistenceManager config. + * + * @param parent parent of the PersistenceManager element + * @return persistence manager configuration + * @throws ConfigurationException if the configuration is broken + */ + protected PersistenceManagerConfig parsePersistenceManagerConfig( + Element parent) throws ConfigurationException { + return new PersistenceManagerConfig( + parseBeanConfig(parent, PERSISTENCE_MANAGER_ELEMENT)); + } + + /** + * Creates a new instance of a configuration parser but with overlayed + * variables and the same connection factory as this parser. + * + * @param variables the variables overlay + * @return a new configuration parser instance + */ + protected RepositoryConfigurationParser createSubParser(Properties variables) { + // overlay the properties + Properties props = new Properties(getVariables()); + props.putAll(variables); + return new RepositoryConfigurationParser(props, connectionFactory); + } + + /** + * Creates and returns a factory object that creates {@link FileSystem} + * instances based on the bean configuration at the named element. + * + * @param parent parent element + * @param name name of the bean configuration element + * @return file system factory + * @throws ConfigurationException if the bean configuration is invalid + */ + protected FileSystemFactory getFileSystemFactory(Element parent, String name) + throws ConfigurationException { + final BeanConfig config = parseBeanConfig(parent, name); + return new FileSystemFactory() { + public FileSystem getFileSystem() throws RepositoryException { + try { + FileSystem fs = config.newInstance(FileSystem.class); + fs.init(); + return fs; + } catch (FileSystemException e) { + throw new RepositoryException( + "File system initialization failure.", e); + } + } + }; + } + + + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + public void setConfigVisitor(BeanConfigVisitor configVisitor) { + this.configVisitor = configVisitor; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SearchConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SearchConfig.java new file mode 100644 index 00000000000..2f6bb22d67b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SearchConfig.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.core.config; + +import java.util.Properties; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemFactory; + +/** + * Search index configuration. This bean configuration class + * is used to create configured search index objects. + *

    + * In addition to generic bean configuration information, this + * class also contains an optional file system configuration + * used by the search index. + */ +public class SearchConfig extends BeanConfig implements FileSystemFactory { + + /** + * The (optional) factory for creating the configured search file system. + */ + private final FileSystemFactory fsf; + + /** + * Creates a search index configuration object. + * + * @param className search index implementation class + * @param properties search index properties + * @param fsf configured search index file system factory, or null + */ + public SearchConfig( + String className, Properties properties, FileSystemFactory fsf) { + super(className, properties); + this.fsf = fsf; + } + + /** + * Creates and returns the configured search file system, or returns + * null if a search file system has not been configured. + * + * @return the configured {@link FileSystem}, or null + * @throws RepositoryException if the file system can not be created + */ + public FileSystem getFileSystem() throws RepositoryException { + if (fsf != null) { + return fsf.getFileSystem(); + } else { + return null; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityConfig.java new file mode 100644 index 00000000000..3d3ded50ea8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityConfig.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.core.config; + +/** + * Security configuration. This encapsulates the security related sub + * configurations {@link AccessManagerConfig} and {@link LoginModuleConfig}. + */ +public class SecurityConfig { + + /** + * Repository name for a JAAS app-entry configuration. + */ + private final String name; + + /** + * Repository security manager configuration; + */ + private final SecurityManagerConfig smc; + + /** + * Repository access manager configuration; + */ + private final AccessManagerConfig amc; + + /** + * Repository login module configuration. Optional, can be null + */ + private final LoginModuleConfig lmc; + + /** + * Creates a new security configuration. + * + * @param name repository name for a JAAS app-entry configuration + * @param smc security manager configuration + * @param amc access manager configuration + * @param lmc login module configuration (can be null) + */ + public SecurityConfig( + String name, + SecurityManagerConfig smc, + AccessManagerConfig amc, LoginModuleConfig lmc) { + this.name = name; + this.smc = smc; + this.amc = amc; + this.lmc = lmc; + } + + /** + * Returns the repository name. The repository name can be used for + * JAAS app-entry configuration. + * + * @return repository name + */ + public String getAppName() { + return name; + } + + /** + * Returns the repository security manager configuration. + * + * @return access manager configuration + */ + public SecurityManagerConfig getSecurityManagerConfig() { + return smc; + } + + /** + * Returns the repository access manager configuration. + * + * @return access manager configuration + */ + public AccessManagerConfig getAccessManagerConfig() { + return amc; + } + + /** + * Returns the repository login module configuration. + * + * @return login module configuration, or null if standard + * JAAS mechanism should be used. + */ + public LoginModuleConfig getLoginModuleConfig() { + return lmc; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityManagerConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityManagerConfig.java new file mode 100644 index 00000000000..1260803a8b3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SecurityManagerConfig.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.core.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Security manager configuration. This bean configuration class + * is used to create configured security manager objects. + *

    + * This class is currently only used to assign a static type to + * more generic bean configuration information. + * + * @see SecurityConfig#getSecurityManagerConfig() + */ +public class SecurityManagerConfig extends BeanConfig { + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(SecurityManagerConfig.class); + + private final String workspaceName; + private final BeanConfig workspaceAccessConfig; + private final UserManagerConfig userManagerConfig; + + /** + * Optional class used to retrieve userID from the subject. + */ + private final Class uidClass; + + /** + * Creates an security manager configuration object from the + * given bean configuration. + * + * @param config bean configuration + * @param workspaceName the security workspace name + * @param workspaceAccessConfig the configuration for the workspace access. + */ + public SecurityManagerConfig(BeanConfig config, String workspaceName, + BeanConfig workspaceAccessConfig) { + this(config, workspaceName, workspaceAccessConfig, null, null); + } + + /** + * Creates an security manager configuration object from the + * given bean configuration. + * + * @param config bean configuration + * @param workspaceName the security workspace name + * @param workspaceAccessConfig the configuration for the workspace access. + * @param userManagerConfig Configuration options for the user manager. + */ + public SecurityManagerConfig(BeanConfig config, String workspaceName, + BeanConfig workspaceAccessConfig, + UserManagerConfig userManagerConfig, + BeanConfig uidClassConfig) { + super(config); + this.workspaceName = workspaceName; + this.workspaceAccessConfig = workspaceAccessConfig; + this.userManagerConfig = userManagerConfig; + Class cl = null; + if (uidClassConfig != null) { + try { + cl = Class.forName(uidClassConfig.getClassName(), true, uidClassConfig.getClassLoader()); + } catch (ClassNotFoundException e) { + log.error("Configured bean implementation class " + uidClassConfig.getClassName() + " was not found -> Ignoring UserIdClass element.", e); + } + } + this.uidClass = cl; + } + + /** + * Returns the name of the 'workspaceName' attribute or null + * if the SecurityManager does not require an extra workspace. + * + * @return + */ + public String getWorkspaceName() { + return workspaceName; + } + + /** + * @return the configuration for the WorkspaceAccessManager. + * May be null if the configuration entry is missing (i.e. + * the system default should be used). + */ + public BeanConfig getWorkspaceAccessConfig() { + return workspaceAccessConfig; + } + + /** + * @return the configuration for the user manager. + * May be null if the configuration entry is missing (i.e. + * the system default should be used). + */ + public UserManagerConfig getUserManagerConfig() { + return userManagerConfig; + } + + /** + * @return Class which is used to retrieve the UserID from the Subject. + * @see org.apache.jackrabbit.core.security.JackrabbitSecurityManager#getUserID(javax.security.auth.Subject, String) + * @see javax.security.auth.Subject#getPrincipals(Class) + */ + public Class getUserIdClass() { + return uidClass; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SimpleBeanFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SimpleBeanFactory.java new file mode 100644 index 00000000000..d81230003f1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/SimpleBeanFactory.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.core.config; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SimpleBeanFactory implements BeanFactory { + private Logger log = LoggerFactory.getLogger(getClass()); + + @Override + public Object newInstance(Class klass, BeanConfig config) throws ConfigurationException{ + String cname = config.getClassName(); + try { + Class objectClass = Class.forName(cname, true, config.getClassLoader()); + if (!klass.isAssignableFrom(objectClass)) { + throw new ConfigurationException( + "Configured class " + cname + + " does not implement " + klass.getName() + + ". Please fix the repository configuration."); + } + if (objectClass.getAnnotation(Deprecated.class) != null) { + log.warn("{} has been deprecated", cname); + } + + // Instantiate the object using the default constructor + return objectClass.newInstance(); + } catch (ClassNotFoundException e) { + throw new ConfigurationException( + "Configured bean implementation class " + cname + + " was not found.", e); + } catch (InstantiationException e) { + throw new ConfigurationException( + "Configured bean implementation class " + cname + + " can not be instantiated.", e); + } catch (IllegalAccessException e) { + throw new ConfigurationException( + "Configured bean implementation class " + cname + + " is protected.", e); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/UserManagerConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/UserManagerConfig.java new file mode 100644 index 00000000000..e359e14e4a3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/UserManagerConfig.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.core.config; + +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.api.security.user.UserManager; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +/** + * User manager configuration. This bean configuration class is used to + * create user manager objects. + *

    + * This configuration is an optional part of the SecurityManager configuration. + * + * @see org.apache.jackrabbit.core.config.SecurityManagerConfig#getUserManagerConfig() + */ +public class UserManagerConfig extends BeanConfig { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(UserManagerConfig.class); + + private Constructor constr; + + private final BeanConfig[] actionConfig; + + public UserManagerConfig(BeanConfig config) { + this(config, null); + } + + public UserManagerConfig(BeanConfig config, BeanConfig[] actionConfig) { + super(config); + setValidate(false); // omit validation of the config properties + this.actionConfig = actionConfig; + } + + /** + * Build a new UserManager instance based on this configuration. + * Since the initial requirement for the User Management was to allow for + * implementations that don't store and retrieve user information from + * repository content, the otherwise used init interface method + * has intentionally be omitted. This method attempts to retrieve a + * constructor matching the given parameterTypes and creates + * an new instance from the initArgs matching the + * parameterTypes. + * + * @param assignableFrom An UserManager class from which the configured + * implementation must be assignable. + * @param parameterTypes Array of classes used to lookup the constructor. + * @param initArgs The arguments to create the new user manager instance + * matching the parameterTypes. + * @return A new instance of UserManager that is assignable from + * the class passed as assignableFrom. + * @throws ConfigurationException If the configured user manager implementation + * is not assignable from the given UserManager class, does not provide + * a constructor matching parameterTypes or creating the instance + * fails. + */ + public UserManager getUserManager(Class assignableFrom, Class[] parameterTypes, Object... initArgs) throws ConfigurationException { + if (constr == null) { + String msg = "Invalid UserManager implementation '" + getClassName() + "'."; + try { + Class umgrCl = Class.forName(getClassName(), true, getClassLoader()); + if (assignableFrom.isAssignableFrom(umgrCl)) { + constr = umgrCl.getConstructor(parameterTypes); + } else { + throw new ConfigurationException("Configured UserManager '" + getClassName() + "' is not assignable from " + assignableFrom); + } + } catch (ClassNotFoundException e) { + throw new ConfigurationException(msg, e); + } catch (NoSuchMethodException e) { + throw new ConfigurationException(msg, e); + } + } + + try { + return (UserManager) constr.newInstance(initArgs); + } catch (Exception e) { + throw new ConfigurationException("Invalid UserManager implementation '" + getClassName() + "'.", e); + } + } + + public AuthorizableAction[] getAuthorizableActions() throws ConfigurationException { + if (actionConfig == null || actionConfig.length == 0) { + return new AuthorizableAction[0]; + } else { + List actions = new ArrayList(actionConfig.length); + for (BeanConfig c : actionConfig) { + AuthorizableAction action = c.newInstance(AuthorizableAction.class); + actions.add(action); + } + return actions.toArray(new AuthorizableAction[actions.size()]); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/VersioningConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/VersioningConfig.java new file mode 100644 index 00000000000..d0ea5b5962b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/VersioningConfig.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.core.config; + +import java.io.File; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemFactory; +import org.apache.jackrabbit.core.state.ISMLocking; +import org.apache.jackrabbit.core.state.ISMLockingFactory; + +/** + * Versioning configuration. This configuration class is used to + * create configured versioning objects. + *

    + * The contained configuration information are: the home directory, + * the file system implementation, and the persistence manager + * implementation. + * + * @see RepositoryConfig#getVersioningConfig() + */ +public class VersioningConfig implements FileSystemFactory, ISMLockingFactory { + + /** + * Versioning home directory. + */ + private final String home; + + /** + * Versioning file system factory. + */ + private final FileSystemFactory fsf; + + /** + * Versioning persistence manager configuration. + */ + private final PersistenceManagerConfig pmc; + + /** + * ISM locking factory + */ + private final ISMLockingFactory ismLockingFactory; + + /** + * Creates a versioning configuration object. + * + * @param home home directory + * @param fsf file system factory + * @param pmc persistence manager configuration + * @param ismLockingFactory the item state manager locking factory + */ + public VersioningConfig(String home, + FileSystemFactory fsf, + PersistenceManagerConfig pmc, + ISMLockingFactory ismLockingFactory) { + this.home = home; + this.fsf = fsf; + this.pmc = pmc; + this.ismLockingFactory = ismLockingFactory; + } + + /** + * Returns the versioning home directory. + * + * @return versioning home directory + */ + public File getHomeDir() { + return new File(home); + } + + /** + * Creates and returns the configured versioning file system. + * + * @return the configured {@link FileSystem} + * @throws RepositoryException if the file system can not be created + */ + public FileSystem getFileSystem() throws RepositoryException { + return fsf.getFileSystem(); + } + + /** + * Returns the versioning persistence manager configuration. + * + * @return persistence manager configuration + */ + public PersistenceManagerConfig getPersistenceManagerConfig() { + return pmc; + } + + /** + * Creates and returns the configured versioning locking strategy. + * + * @return the configured {@link ISMLocking} + * @throws RepositoryException if the locking strategy can not be created + */ + public ISMLocking getISMLocking() throws RepositoryException { + return ismLockingFactory.getISMLocking(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/WorkspaceConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/WorkspaceConfig.java new file mode 100644 index 00000000000..eb91948b76b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/WorkspaceConfig.java @@ -0,0 +1,270 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.config; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemFactory; +import org.apache.jackrabbit.core.query.QueryHandler; +import org.apache.jackrabbit.core.query.QueryHandlerContext; +import org.apache.jackrabbit.core.query.QueryHandlerFactory; +import org.apache.jackrabbit.core.state.ISMLocking; +import org.apache.jackrabbit.core.state.ISMLockingFactory; + +/** + * Workspace configuration. This configuration class is used to create + * configured workspace objects. + *

    + * The contained configuration information are: the home directory and name of + * the workspace, the file system, the persistence manager, the search index and + * the item state manager locking configuration. The search index and the item + * state manager locking and the security config are optional parts. + */ +public class WorkspaceConfig + implements FileSystemFactory, ISMLockingFactory, QueryHandlerFactory { + + /** + * Workspace home directory. + */ + private final String home; + + /** + * Workspace name. + */ + private final String name; + + /** + * Flag indicating whether this workspace participates in a cluster. + */ + private final boolean clustered; + + /** + * Workspace file system factory. + */ + private FileSystemFactory fsf; + + /** + * Workspace persistence manager configuration. + */ + private PersistenceManagerConfig pmc; + + /** + * Query handler factory, or null if search is not configured. + */ + private QueryHandlerFactory qhf; + + /** + * The item state manager locking factory. + */ + private ISMLockingFactory ismLockingFactory; + + /** + * Workspace security configuration. Can be null. + */ + private final WorkspaceSecurityConfig workspaceSecurityConfig; + + /** + * Optional configuration for the xml import behavior. Up to now this consists + * of a single configuration point: the treatment + * of protected nodes and properties that is defined by a set of classes + * implementing {@link org.apache.jackrabbit.core.xml.ProtectedNodeImporter} + * or {@link org.apache.jackrabbit.core.xml.ProtectedPropertyImporter}. + */ + private final ImportConfig importConfig; + + /** + * Default lock timeout in seconds. + */ + private final long defaultLockTimeout; + + /** + * Creates a workspace configuration object. + * + * @param home home directory + * @param name workspace name + * @param clustered + * @param fsf file system factory + * @param pmc persistence manager configuration + * @param qhf query handler factory, or null if not configured + * @param ismLockingFactory the item state manager locking factory + * @param workspaceSecurityConfig the workspace specific security configuration. + */ + public WorkspaceConfig(String home, String name, boolean clustered, + FileSystemFactory fsf, PersistenceManagerConfig pmc, + QueryHandlerFactory qhf, + ISMLockingFactory ismLockingFactory, + WorkspaceSecurityConfig workspaceSecurityConfig) { + this(home, name, clustered, fsf, pmc, qhf, ismLockingFactory, workspaceSecurityConfig, null, Long.MAX_VALUE); + } + + /** + * Creates a workspace configuration object. + * + * @param home home directory + * @param name workspace name + * @param clustered + * @param fsf file system factory + * @param pmc persistence manager configuration + * @param qhf query handler factory, or null if not configured + * @param ismLockingFactory the item state manager locking factory + * @param workspaceSecurityConfig the workspace specific security configuration. + */ + public WorkspaceConfig(String home, String name, boolean clustered, + FileSystemFactory fsf, PersistenceManagerConfig pmc, + QueryHandlerFactory qhf, + ISMLockingFactory ismLockingFactory, + WorkspaceSecurityConfig workspaceSecurityConfig, + ImportConfig importConfig) { + this(home, name, clustered, fsf, pmc, qhf, ismLockingFactory, workspaceSecurityConfig, importConfig, Long.MAX_VALUE); + } + + /** + * Creates a workspace configuration object. + * + * @param home home directory + * @param name workspace name + * @param clustered + * @param fsf file system factory + * @param pmc persistence manager configuration + * @param qhf query handler factory, or null if not configured + * @param ismLockingFactory the item state manager locking factory + * @param workspaceSecurityConfig the workspace specific security configuration. + * @param defaultLockTimeout default timeout for locks (in seconds) + */ + public WorkspaceConfig(String home, String name, boolean clustered, FileSystemFactory fsf, + PersistenceManagerConfig pmc, QueryHandlerFactory qhf, ISMLockingFactory ismLockingFactory, + WorkspaceSecurityConfig workspaceSecurityConfig, ImportConfig importConfig, long defaultLockTimeout) { + this.home = home; + this.name = name; + this.clustered = clustered; + this.fsf = fsf; + this.pmc = pmc; + this.qhf = qhf; + this.ismLockingFactory = ismLockingFactory; + this.workspaceSecurityConfig = workspaceSecurityConfig; + this.importConfig = importConfig; + this.defaultLockTimeout = defaultLockTimeout; + } + + /** + * Returns the workspace home directory. + * + * @return workspace home directory + */ + public String getHomeDir() { + return home; + } + + /** + * Returns the workspace name. + * + * @return the workspace name + */ + public String getName() { + return name; + } + + /** + * Returns a flag indicating whether this workspace participates in a cluster. + * + * @return true if this workspace participates in a cluster; + * false otherwise + */ + public boolean isClustered() { + return clustered; + } + + /** + * Returns the default lock timeout in number of seconds or + * Long.MAX_VALUE when not specified. + * + * @return default lock timeout in number of seconds or + * Long.MAX_VALUE when not specified + */ + public long getDefaultLockTimeout() { + return defaultLockTimeout; + } + + /** + * Creates and returns the configured workspace locking strategy. + * + * @return the configured {@link ISMLocking} + * @throws RepositoryException if the locking strategy can not be created + */ + public ISMLocking getISMLocking() throws RepositoryException { + return ismLockingFactory.getISMLocking(); + } + + /** + * Creates and returns the configured workspace file system. + * + * @return the configured {@link FileSystem} + * @throws RepositoryException if the file system can not be created + */ + public FileSystem getFileSystem() throws RepositoryException { + return fsf.getFileSystem(); + } + + /** + * Returns the workspace persistence manager configuration. + * + * @return persistence manager configuration + */ + public PersistenceManagerConfig getPersistenceManagerConfig() { + return pmc; + } + + /** + * Checks whether search configuration is present. + * + * @return true if search is configured, + * false otherwise + */ + public boolean isSearchEnabled() { + return qhf != null; + } + + /** + * Returns an initialized query handler, or null if one + * was not configured. + * + * @return initialized query handler, or null + */ + public QueryHandler getQueryHandler(QueryHandlerContext context) + throws RepositoryException { + if (qhf != null) { + return qhf.getQueryHandler(context); + } else { + return null; + } + } + /** + * @return workspace-specific security settings. + * @see WorkspaceSecurityConfig + */ + public WorkspaceSecurityConfig getSecurityConfig() { + return workspaceSecurityConfig; + } + + /** + * @return xml import settings + */ + public ImportConfig getImportConfig() { + return importConfig; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/WorkspaceSecurityConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/WorkspaceSecurityConfig.java new file mode 100644 index 00000000000..174d0150078 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/config/WorkspaceSecurityConfig.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.core.config; + +/** + * Representation of workspace specific security settings. + */ +public class WorkspaceSecurityConfig { + + /** configuration for AccessControlProvider */ + private final BeanConfig accessControlProviderConfig; + + /** + * @param accessControlProviderConfig + */ + public WorkspaceSecurityConfig(BeanConfig accessControlProviderConfig) { + this.accessControlProviderConfig = accessControlProviderConfig; + } + + /** + * @return Configuration for the AccessControlProvider or + * null, if this optional config entry is missing + */ + public BeanConfig getAccessControlProviderConfig() { + return accessControlProviderConfig; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DB2FileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DB2FileSystem.java new file mode 100644 index 00000000000..c689503a99a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DB2FileSystem.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.core.fs.db; + +/** + * DB2FileSystem is a JDBC-based FileSystem + * implementation for Jackrabbit that persists file system entries in a + * DB2 database. + *

    + * It is configured through the following properties: + *

      + *
    • driver: the FQN name of the JDBC driver class + * (default: "com.ibm.db2.jcc.DB2Driver")
    • + *
    • schema: type of schema to be used + * (default: "db2")
    • + *
    • url: the database url (e.g. + * "jdbc:db2:[database]")
    • + *
    • user: the database user
    • + *
    • password: the user's password
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    + * See also {@link DbFileSystem}. + *

    + * The following is a fragment from a sample configuration: + *

    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.DB2FileSystem">
    + *       <param name="url" value="jdbc:db2:test"/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *   </FileSystem>
    + * 
    + */ +public class DB2FileSystem extends DbFileSystem { + + /** + * Creates a new DB2FileSystem instance. + */ + public DB2FileSystem() { + // preset some attributes to reasonable defaults + schema = "db2"; + driver = "com.ibm.db2.jcc.DB2Driver"; + } + + //-----------------------------------------< DatabaseFileSystem overrides > + /** + * {@inheritDoc} + *

    + * Since DB2 requires parameter markers within the select clause to be + * explicitly typed using cast(? as type_name) some statements + * had to be changed accordingly. + */ + protected void buildSQLStatements() { + super.buildSQLStatements(); + + copyFileSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "select cast(? as varchar(745)), cast(? as varchar(255)), FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null"; + + copyFilesSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "select cast(? as varchar(745)), FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_DATA is not null"; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DatabaseFileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DatabaseFileSystem.java new file mode 100644 index 00000000000..d62f52fb100 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DatabaseFileSystem.java @@ -0,0 +1,933 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.fs.db; + +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemPathUtil; +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DbUtility; +import org.apache.jackrabbit.core.util.db.StreamWrapper; +import org.apache.jackrabbit.util.TransientFileFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.FileInputStream; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; + +/** + * Base class for database file systems. This class contains common + * functionality for database file system subclasses that normally differ only + * in the way the database connection is acquired. + *

    + * See the {@link DbFileSystem} for a detailed description of the available + * configuration options and database behaviour. + */ +public abstract class DatabaseFileSystem implements FileSystem { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(DatabaseFileSystem.class); + + protected boolean initialized; + + protected String schema; + protected String schemaObjectPrefix; + + // initial size of buffer used to serialize objects + protected static final int INITIAL_BUFFER_SIZE = 8192; + + /** + * Whether the schema check must be done during initialization. + */ + private boolean schemaCheckEnabled = true; + + /** the {@link ConnectionHelper} set in the {@link #init()} method */ + protected ConnectionHelper conHelper; + + // SQL statements + protected String selectExistSQL; + protected String selectFileExistSQL; + protected String selectFolderExistSQL; + protected String selectChildCountSQL; + protected String selectDataSQL; + protected String selectLastModifiedSQL; + protected String selectLengthSQL; + protected String selectFileNamesSQL; + protected String selectFolderNamesSQL; + protected String selectFileAndFolderNamesSQL; + protected String deleteFileSQL; + protected String deleteFolderSQL; + protected String insertFileSQL; + protected String insertFolderSQL; + protected String updateDataSQL; + protected String updateLastModifiedSQL; + protected String copyFileSQL; + protected String copyFilesSQL; + + /** + * Default constructor + */ + public DatabaseFileSystem() { + schema = "default"; + schemaObjectPrefix = ""; + initialized = false; + } + + //----------------------------------------------------< setters & getters > + public String getSchemaObjectPrefix() { + return schemaObjectPrefix; + } + + public void setSchemaObjectPrefix(String schemaObjectPrefix) { + // make sure prefix is all uppercase + this.schemaObjectPrefix = schemaObjectPrefix.toUpperCase(); + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + /** + * @return whether the schema check is enabled + */ + public final boolean isSchemaCheckEnabled() { + return schemaCheckEnabled; + } + + /** + * @param enabled set whether the schema check is enabled + */ + public final void setSchemaCheckEnabled(boolean enabled) { + schemaCheckEnabled = enabled; + } + + //-------------------------------------------< java.lang.Object overrides > + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof DatabaseFileSystem) { + DatabaseFileSystem other = (DatabaseFileSystem) obj; + return equals(schema, other.schema) + && equals(schemaObjectPrefix, other.schemaObjectPrefix); + } else { + return false; + } + } + + private static boolean equals(Object a, Object b) { + if (a == null && b == null) { + return true; + } else if (a == null || b == null) { + return false; + } else { + return a.equals(b); + } + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //-----------------------------------------------------------< FileSystem > + + /** + * {@inheritDoc} + */ + public void init() throws FileSystemException { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + + try { + conHelper = createConnectionHelper(getDataSource()); + + // make sure schemaObjectPrefix consists of legal name characters only + schemaObjectPrefix = conHelper.prepareDbIdentifier(schemaObjectPrefix); + + // check if schema objects exist and create them if necessary + if (isSchemaCheckEnabled()) { + createCheckSchemaOperation().run(); + } + + // build sql statements + buildSQLStatements(); + + // finally verify that there's a file system root entry + verifyRootExists(); + + initialized = true; + } catch (Exception e) { + String msg = "failed to initialize file system"; + log.error(msg, e); + throw new FileSystemException(msg, e); + } + } + + /** + * @return + * @throws Exception + */ + protected abstract DataSource getDataSource() throws Exception; + + /** + * This method is called from the {@link #init()} method of this class and returns a + * {@link ConnectionHelper} instance which is assigned to the {@code conHelper} field. Subclasses may + * override it to return a specialized connection helper. + * + * @param dataSrc the {@link DataSource} of this persistence manager + * @return a {@link ConnectionHelper} + * @throws Exception on error + */ + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + return new ConnectionHelper(dataSrc, false); + } + + /** + * This method is called from {@link #init()} after the + * {@link #createConnectionHelper(DataSource)} method, and returns a default {@link CheckSchemaOperation}. + * Subclasses can overrride this implementation to get a customized implementation. + * + * @return a new {@link CheckSchemaOperation} instance + */ + protected CheckSchemaOperation createCheckSchemaOperation() { + InputStream in = DatabaseFileSystem.class.getResourceAsStream(getSchema() + ".ddl"); + return new CheckSchemaOperation(conHelper, in, schemaObjectPrefix + "FSENTRY").addVariableReplacement( + CheckSchemaOperation.SCHEMA_OBJECT_PREFIX_VARIABLE, schemaObjectPrefix); + } + + /** + * {@inheritDoc} + */ + public void close() throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + } + + /** + * {@inheritDoc} + */ + public void createFolder(String folderPath) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(folderPath); + + if (!exists(folderPath)) { + createDeepFolder(folderPath); + } else { + throw new FileSystemException("file system entry already exists: " + folderPath); + } + } + + /** + * {@inheritDoc} + */ + public void deleteFile(String filePath) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(filePath); + + String parentDir = FileSystemPathUtil.getParentDir(filePath); + String name = FileSystemPathUtil.getName(filePath); + + int count = 0; + synchronized (deleteFileSQL) { + try { + count = conHelper.update( + deleteFileSQL, new Object[]{parentDir, name}); + } catch (SQLException e) { + String msg = "failed to delete file: " + filePath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } + } + + if (count == 0) { + throw new FileSystemException("no such file: " + filePath); + } + } + + /** + * {@inheritDoc} + */ + public void deleteFolder(String folderPath) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(folderPath); + + if (folderPath.equals(FileSystem.SEPARATOR)) { + throw new FileSystemException("cannot delete root"); + } + + String parentDir = FileSystemPathUtil.getParentDir(folderPath); + String name = FileSystemPathUtil.getName(folderPath); + + int count = 0; + synchronized (deleteFolderSQL) { + try { + count = conHelper.update(deleteFolderSQL, new Object[]{ + parentDir, + name, + folderPath, + folderPath + FileSystem.SEPARATOR + "%"}); + } catch (SQLException e) { + String msg = "failed to delete folder: " + folderPath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } + } + + if (count == 0) { + throw new FileSystemException("no such folder: " + folderPath); + } + } + + /** + * {@inheritDoc} + */ + public boolean exists(String path) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(path); + + String parentDir = FileSystemPathUtil.getParentDir(path); + String name = FileSystemPathUtil.getName(path); + + synchronized (selectExistSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectExistSQL, new Object[]{parentDir, name}, false, 0); + + // a file system entry exists if the result set + // has at least one entry + return rs.next(); + } catch (SQLException e) { + String msg = "failed to check existence of file system entry: " + path; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public boolean isFile(String path) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(path); + + String parentDir = FileSystemPathUtil.getParentDir(path); + String name = FileSystemPathUtil.getName(path); + + synchronized (selectFileExistSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectFileExistSQL, new Object[]{parentDir, name}, false, 0); + + // a file exists if the result set has at least one entry + return rs.next(); + } catch (SQLException e) { + String msg = "failed to check existence of file: " + path; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public boolean isFolder(String path) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(path); + + String parentDir = FileSystemPathUtil.getParentDir(path); + String name = FileSystemPathUtil.getName(path); + + synchronized (selectFolderExistSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectFolderExistSQL, new Object[]{parentDir, name}, false, 0); + + // a folder exists if the result set has at least one entry + return rs.next(); + } catch (SQLException e) { + String msg = "failed to check existence of folder: " + path; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public long lastModified(String path) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(path); + + String parentDir = FileSystemPathUtil.getParentDir(path); + String name = FileSystemPathUtil.getName(path); + + synchronized (selectLastModifiedSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectLastModifiedSQL, new Object[]{parentDir, name}, false, 0); + if (!rs.next()) { + throw new FileSystemException("no such file system entry: " + path); + } + return rs.getLong(1); + } catch (SQLException e) { + String msg = "failed to determine lastModified of file system entry: " + path; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public long length(String filePath) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(filePath); + + String parentDir = FileSystemPathUtil.getParentDir(filePath); + String name = FileSystemPathUtil.getName(filePath); + + synchronized (selectLengthSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectLengthSQL, new Object[]{parentDir, name}, false, 0); + if (!rs.next()) { + throw new FileSystemException("no such file: " + filePath); + } + return rs.getLong(1); + } catch (SQLException e) { + String msg = "failed to determine length of file: " + filePath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public boolean hasChildren(String path) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(path); + + if (!exists(path)) { + throw new FileSystemException("no such file system entry: " + path); + } + + synchronized (selectChildCountSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec(selectChildCountSQL, new Object[]{path}, false, 0); + if (!rs.next()) { + return false; + } + int count = rs.getInt(1); + if (FileSystemPathUtil.denotesRoot(path)) { + // ingore file system root entry + count--; + } + return (count > 0); + } catch (SQLException e) { + String msg = "failed to determine child count of file system entry: " + path; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public String[] list(String folderPath) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(folderPath); + + if (!isFolder(folderPath)) { + throw new FileSystemException("no such folder: " + folderPath); + } + + synchronized (selectFileAndFolderNamesSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectFileAndFolderNamesSQL, new Object[]{folderPath}, false, 0); + ArrayList names = new ArrayList(); + while (rs.next()) { + String name = rs.getString(1); + if (name.length() == 0 + && FileSystemPathUtil.denotesRoot(folderPath)) { + // this is the file system root entry, skip... + continue; + } + names.add(name); + } + return names.toArray(new String[names.size()]); + } catch (SQLException e) { + String msg = "failed to list child entries of folder: " + folderPath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public String[] listFiles(String folderPath) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(folderPath); + + if (!isFolder(folderPath)) { + throw new FileSystemException("no such folder: " + folderPath); + } + + synchronized (selectFileNamesSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectFileNamesSQL, new Object[]{folderPath}, false, 0); + ArrayList names = new ArrayList(); + while (rs.next()) { + names.add(rs.getString(1)); + } + return names.toArray(new String[names.size()]); + } catch (SQLException e) { + String msg = "failed to list file entries of folder: " + folderPath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public String[] listFolders(String folderPath) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(folderPath); + + if (!isFolder(folderPath)) { + throw new FileSystemException("no such folder: " + folderPath); + } + + synchronized (selectFolderNamesSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectFolderNamesSQL, new Object[]{folderPath}, false, 0); + ArrayList names = new ArrayList(); + while (rs.next()) { + String name = rs.getString(1); + if (name.length() == 0 + && FileSystemPathUtil.denotesRoot(folderPath)) { + // this is the file system root entry, skip... + continue; + } + names.add(name); + } + return (String[]) names.toArray(new String[names.size()]); + } catch (SQLException e) { + String msg = "failed to list folder entries of folder: " + folderPath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public InputStream getInputStream(String filePath) throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(filePath); + + String parentDir = FileSystemPathUtil.getParentDir(filePath); + String name = FileSystemPathUtil.getName(filePath); + + synchronized (selectDataSQL) { + try { + final ResultSet rs = conHelper.exec( + selectDataSQL, new Object[]{parentDir, name}, false, 0); + + if (!rs.next()) { + throw new FileSystemException("no such file: " + filePath); + } + InputStream in = rs.getBinaryStream(1); + /** + * return an InputStream wrapper in order to + * close the ResultSet when the stream is closed + */ + return new FilterInputStream(in) { + public void close() throws IOException { + super.close(); + // close ResultSet + DbUtility.close(rs); + } + }; + } catch (SQLException e) { + String msg = "failed to retrieve data of file: " + filePath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } + } + } + + /** + * {@inheritDoc} + */ + public OutputStream getOutputStream(final String filePath) + throws FileSystemException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + FileSystemPathUtil.checkFormat(filePath); + + final String parentDir = FileSystemPathUtil.getParentDir(filePath); + final String name = FileSystemPathUtil.getName(filePath); + + if (!isFolder(parentDir)) { + throw new FileSystemException("path not found: " + parentDir); + } + + if (isFolder(filePath)) { + throw new FileSystemException("path denotes folder: " + filePath); + } + + try { + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + final File tmpFile = fileFactory.createTransientFile("bin", null, null); + + return new FilterOutputStream(new FileOutputStream(tmpFile)) { + + public void write(byte[] bytes, int off, int len) throws IOException { + out.write(bytes, off, len); + } + + public void close() throws IOException { + out.flush(); + ((FileOutputStream) out).getFD().sync(); + out.close(); + + InputStream in = null; + try { + if (isFile(filePath)) { + synchronized (updateDataSQL) { + long length = tmpFile.length(); + in = new FileInputStream(tmpFile); + conHelper.exec(updateDataSQL, + new Object[]{ + new StreamWrapper(in, length), + new Long(System.currentTimeMillis()), + new Long(length), + parentDir, + name + }); + } + } else { + synchronized (insertFileSQL) { + long length = tmpFile.length(); + in = new FileInputStream(tmpFile); + conHelper.exec(insertFileSQL, + new Object[]{ + parentDir, + name, + new StreamWrapper(in, length), + new Long(System.currentTimeMillis()), + new Long(length) + }); + } + } + + } catch (Exception e) { + IOException ioe = new IOException(e.getMessage()); + ioe.initCause(e); + throw ioe; + } finally { + if (in != null) { + in.close(); + } + // temp file can now safely be removed + tmpFile.delete(); + } + } + }; + } catch (Exception e) { + String msg = "failed to open output stream to file: " + filePath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } + } + + //----------------------------------< misc. helper methods & overridables > + + /** + * Builds the SQL statements + */ + protected void buildSQLStatements() { + insertFileSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "values (?, ?, ?, ?, ?)"; + + insertFolderSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "values (?, ?, ?, 0)"; + + updateDataSQL = "update " + + schemaObjectPrefix + "FSENTRY " + + "set FSENTRY_DATA = ?, FSENTRY_LASTMOD = ?, FSENTRY_LENGTH = ? " + + "where FSENTRY_PATH = ? and FSENTRY_NAME = ? " + + "and FSENTRY_DATA is not null"; + + updateLastModifiedSQL = "update " + + schemaObjectPrefix + "FSENTRY set FSENTRY_LASTMOD = ? " + + "where FSENTRY_PATH = ? and FSENTRY_NAME = ? " + + "and FSENTRY_DATA is not null"; + + selectExistSQL = "select 1 from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ?"; + + selectFileExistSQL = "select 1 from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null"; + + selectFolderExistSQL = "select 1 from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_DATA is null"; + + selectFileNamesSQL = "select FSENTRY_NAME from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_DATA is not null"; + + selectFolderNamesSQL = "select FSENTRY_NAME from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_DATA is null"; + + selectFileAndFolderNamesSQL = "select FSENTRY_NAME from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ?"; + + selectChildCountSQL = "select count(FSENTRY_NAME) from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "; + + selectDataSQL = "select FSENTRY_DATA from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null"; + + selectLastModifiedSQL = "select FSENTRY_LASTMOD from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ?"; + + selectLengthSQL = "select FSENTRY_LENGTH from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null"; + + deleteFileSQL = "delete from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null"; + + deleteFolderSQL = "delete from " + + schemaObjectPrefix + "FSENTRY where " + + "(FSENTRY_PATH = ? and FSENTRY_NAME = ? and FSENTRY_DATA is null) " + + "or (FSENTRY_PATH = ?) " + + "or (FSENTRY_PATH like ?) "; + + copyFileSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "select ?, ?, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null"; + + copyFilesSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "select ?, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_DATA is not null"; + } + + /** + * Verifies that the root file system entry exists. If it doesn't exist yet + * it will be automatically created. + * + * @throws Exception if an error occurs + */ + protected void verifyRootExists() throws Exception { + // check if root file system entry exists + synchronized (selectFolderExistSQL) { + ResultSet rs = null; + try { + rs = conHelper.exec( + selectFolderExistSQL, + new Object[]{FileSystem.SEPARATOR, ""}, false, 0); + + if (rs.next()) { + // root entry exists + return; + } + } catch (SQLException e) { + String msg = "failed to check existence of file system root entry"; + log.error(msg, e); + throw new FileSystemException(msg, e); + } finally { + DbUtility.close(rs); + } + } + + // the root entry doesn't exist yet, create it... + createDeepFolder(FileSystem.SEPARATOR); + } + + /** + * Creates the specified files system folder entry, recursively creating + * any non-existing intermediate folder entries. + * + * @param folderPath folder entry to create + * @throws FileSystemException if an error occurs + */ + protected void createDeepFolder(String folderPath) + throws FileSystemException { + String parentDir = FileSystemPathUtil.getParentDir(folderPath); + String name = FileSystemPathUtil.getName(folderPath); + + if (!FileSystemPathUtil.denotesRoot(folderPath)) { + if (!exists(parentDir)) { + createDeepFolder(parentDir); + } + } + + synchronized (insertFolderSQL) { + try { + conHelper.exec( + insertFolderSQL, + new Object[]{ + parentDir, + name, + new Long(System.currentTimeMillis())}); + } catch (SQLException e) { + String msg = "failed to create folder entry: " + folderPath; + log.error(msg, e); + throw new FileSystemException(msg, e); + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DbFileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DbFileSystem.java new file mode 100644 index 00000000000..815bdca5a6c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DbFileSystem.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.fs.db; + +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.core.util.db.DatabaseAware; + +import javax.sql.DataSource; + +/** + * DbFileSystem is a generic JDBC-based FileSystem + * implementation for Jackrabbit that persists file system entries in a + * database table. + *

    + * It is configured through the following properties: + *

      + *
    • driver: the FQN name of the JDBC driver class
    • + *
    • url: the database url of the form jdbc:subprotocol:subname
    • + *
    • user: the database user
    • + *
    • password: the user's password
    • + *
    • schema: type of schema to be used + * (e.g. mysql, mssql, etc.);
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    + * The required schema objects are automatically created by executing the DDL + * statements read from the [schema].ddl file. The .ddl file is read from the + * resources by calling getClass().getResourceAsStream(schema + ".ddl"). + * Every line in the specified .ddl file is executed separatly by calling + * java.sql.Statement.execute(String) where every occurence of the + * the string "${schemaObjectPrefix}" has been replaced with the + * value of the property schemaObjectPrefix. + *

    + * The following is a fragment from a sample configuration using MySQL: + *

    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
    + *       <param name="driver" value="com.mysql.jdbc.Driver"/>
    + *       <param name="url" value="jdbc:mysql:///test?autoReconnect=true"/>
    + *       <param name="schema" value="mysql"/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *   </FileSystem>
    + * 
    + * The following is a fragment from a sample configuration using Daffodil One$DB Embedded: + *
    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
    + *       <param name="driver" value="in.co.daffodil.db.jdbc.DaffodilDBDriver"/>
    + *       <param name="url" value="jdbc:daffodilDB_embedded:rep;path=${rep.home}/databases;create=true"/>
    + *       <param name="user" value="daffodil"/>
    + *       <param name="password" value="daffodil"/>
    + *       <param name="schema" value="daffodil"/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *   </FileSystem>
    + * 
    + * The following is a fragment from a sample configuration using MSSQL: + *
    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
    + *       <param name="driver" value="com.microsoft.jdbc.sqlserver.SQLServerDriver"/>
    + *       <param name="url" value="jdbc:microsoft:sqlserver://localhost:1433;;DatabaseName=test;SelectMethod=Cursor;"/>
    + *       <param name="schema" value="mssql"/>
    + *       <param name="user" value="sa"/>
    + *       <param name="password" value=""/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *   </FileSystem>
    + * 
    + * The following is a fragment from a sample configuration using Ingres: + *
    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
    + *       <param name="driver" value="com.ingres.jdbc.IngresDriver"/>
    + *       <param name="url" value="jdbc:ingres://localhost:II7/test"/>
    + *       <param name="schema" value="ingres"/>
    + *       <param name="user" value="ingres"/>
    + *       <param name="password" value="ingres"/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *   </FileSystem>
    + * 
    + * The following is a fragment from a sample configuration using PostgreSQL: + *
    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
    + *       <param name="driver" value="org.postgresql.Driver"/>
    + *       <param name="url" value="jdbc:postgresql://localhost/test"/>
    + *       <param name="schema" value="postgresql"/>
    + *       <param name="user" value="postgres"/>
    + *       <param name="password" value="postgres"/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *   </FileSystem>
    + * 
    + * JNDI can be used to get the connection. In this case, use the javax.naming.InitialContext as the driver, + * and the JNDI name as the URL. If the user and password are configured in the JNDI resource, + * they should not be configured here. Example JNDI settings: + *
    + * <param name="driver" value="javax.naming.InitialContext" />
    + * <param name="url" value="java:comp/env/jdbc/Test" />
    + * 
    + * See also {@link DerbyFileSystem}, {@link DB2FileSystem}, {@link OracleFileSystem}. + */ +public class DbFileSystem extends DatabaseFileSystem implements DatabaseAware { + + /** + * the full qualified JDBC driver name + */ + protected String driver; + + /** + * the JDBC connection URL + */ + protected String url; + + /** + * the JDBC connection user + */ + protected String user; + + /** + * the JDBC connection password + */ + protected String password; + + protected String dataSourceName; + + /** + * The repositories {@link ConnectionFactory}. + */ + private ConnectionFactory connectionFactory; + + /** + * {@inheritDoc} + */ + public void setConnectionFactory(ConnectionFactory connnectionFactory) { + this.connectionFactory = connnectionFactory; + } + + //----------------------------------------------------< setters & getters > + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getDriver() { + return driver; + } + + public void setDriver(String driver) { + this.driver = driver; + } + + public String getDataSourceName() { + return dataSourceName; + } + + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + //-------------------------------------------< java.lang.Object overrides > + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof DbFileSystem) { + DbFileSystem other = (DbFileSystem) obj; + if (((driver != null) ? driver.equals(other.driver) : other.driver == null) + && ((url != null) ? url.equals(other.url) : other.url == null) + && ((user != null) ? user.equals(other.user) : other.user == null) + && ((password != null) ? password.equals(other.password) : other.password == null) + && super.equals(other)) { + return true; + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //--------------------------------------------------< DatabaseFileSystem > + + /** + * {@inheritDoc} + */ + @Override + protected final DataSource getDataSource() throws Exception { + if (getDataSourceName() == null || "".equals(getDataSourceName())) { + return connectionFactory.getDataSource(getDriver(), getUrl(), getUser(), getPassword()); + } else { + String dbType = connectionFactory.getDataBaseType(dataSourceName); + if (DatabaseFileSystem.class.getResourceAsStream(dbType + ".ddl") != null) { + setSchema(dbType); + } + return connectionFactory.getDataSource(dataSourceName); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DerbyFileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DerbyFileSystem.java new file mode 100644 index 00000000000..8a9f66cef10 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/DerbyFileSystem.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.core.fs.db; + +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DerbyConnectionHelper; +import java.sql.SQLException; + +import javax.sql.DataSource; + +/** + * DerbyFileSystem is a JDBC-based FileSystem + * implementation for Jackrabbit that persists file system entries in an + * embedded Derby database. + *

    + * It is configured through the following properties: + *

      + *
    • url: the database url of the form + * "jdbc:derby:[db];[attributes]"
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    • driver: the FQN name of the JDBC driver class + * (default: "org.apache.derby.jdbc.EmbeddedDriver")
    • + *
    • schema: type of schema to be used + * (default: "derby")
    • + *
    • user: the database user (default: null)
    • + *
    • password: the user's password (default: null)
    • + *
    • shutdownOnClose: if true (the default) the + * database is shutdown when the last connection is closed; + * set this to false when using a standalone database
    • + *
    + * See also {@link DbFileSystem}. + *

    + * The following is a fragment from a sample configuration: + *

    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.DerbyFileSystem">
    + *       <param name="url" value="jdbc:derby:${rep.home}/db;create=true"/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *  </FileSystem>
    + * 
    + */ +public class DerbyFileSystem extends DbFileSystem { + + /** + * Flag indicating whether this derby database should be shutdown on close. + */ + protected boolean shutdownOnClose; + + /** + * Creates a new DerbyFileSystem instance. + */ + public DerbyFileSystem() { + // preset some attributes to reasonable defaults + schema = "derby"; + driver = "org.apache.derby.jdbc.EmbeddedDriver"; + shutdownOnClose = true; + initialized = false; + } + + //----------------------------------------------------< setters & getters > + + public boolean getShutdownOnClose() { + return shutdownOnClose; + } + + public void setShutdownOnClose(boolean shutdownOnClose) { + this.shutdownOnClose = shutdownOnClose; + } + + //-----------------------------------------------< DbFileSystem overrides > + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + return new DerbyConnectionHelper(dataSrc, false); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws FileSystemException { + super.close(); + if (shutdownOnClose) { + try { + ((DerbyConnectionHelper) conHelper).shutDown(driver); + } catch (SQLException e) { + throw new FileSystemException("failed to shutdown Derby", e); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/JNDIDatabaseFileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/JNDIDatabaseFileSystem.java new file mode 100644 index 00000000000..7d21a066fe5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/JNDIDatabaseFileSystem.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.core.fs.db; + +import javax.naming.InitialContext; +import javax.sql.DataSource; + +/** + * @deprecated + * This class should not be used because it is not database vendor specific. + * Each DatabaseFileSystem now supports getting the connection via JNDI + * by setting the driver to javax.naming.InitialContext + * and the URL to the JNDI name. + *

    + * Database file system that uses JNDI to acquire the database connection. + * The JNDI location of the {@link DataSource} to be used in given as + * the dataSourceLocation configuration property. See the + * {@link DbFileSystem} for more configuration details. + *

    + * WARNING: The acquired database connection is kept + * for the entire lifetime of the file system instance. The configured data + * source should be prepared for this. + */ +public class JNDIDatabaseFileSystem extends DatabaseFileSystem { + + /** + * JNDI location of the data source used to acquire database connections. + */ + private String dataSourceLocation; + + //----------------------------------------------------< setters & getters > + + /** + * Returns the JNDI location of the data source. + * + * @return data source location + */ + public String getDataSourceLocation() { + return dataSourceLocation; + } + + /** + * Sets the JNDI location of the data source. + * + * @param dataSourceLocation data source location + */ + public void setDataSourceLocation(String dataSourceLocation) { + this.dataSourceLocation = dataSourceLocation; + } + + //--------------------------------------------------< DatabaseFileSystem > + + /** + * {@inheritDoc} + */ + @Override + protected DataSource getDataSource() throws Exception { + InitialContext ic = new InitialContext(); + return (DataSource) ic.lookup(dataSourceLocation); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/MSSqlFileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/MSSqlFileSystem.java new file mode 100644 index 00000000000..d3d31871830 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/MSSqlFileSystem.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.core.fs.db; + +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; + +/** + * MSSqlFileSystem is a JDBC-based FileSystem + * implementation for Jackrabbit that persists file system entries in an + * MS SQL database. + *

    + * It is configured through the following properties: + *

      + *
    • driver: the FQN name of the JDBC driver class + * (default: "com.microsoft.sqlserver.jdbc.SQLServerDriver")
    • + *
    • schema: type of schema to be used + * (default: "mssql")
    • + *
    • url: the database url (e.g. + * "jdbc:sqlserver://[host]:[port];<params>")
    • + *
    • user: the database user
    • + *
    • password: the user's password
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    • tableSpace: the tablespace to use
    • + *
    + * See also {@link DbFileSystem}. + *

    + * The following is a fragment from a sample configuration: + *

    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.MSSqlFileSystem">
    + *       <param name="url" value="jdbc:sqlserver://localhost:1433"/>
    + *       <param name="user" value="padv25"/>
    + *       <param name="password" value="padv25"/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *       <param name="tableSpace" value="default"/>
    + *  </FileSystem>
    + * 
    + */ +public class MSSqlFileSystem extends DbFileSystem { + + /** the variable for the MS SQL table space */ + public static final String TABLE_SPACE_VARIABLE = "${tableSpace}"; + + /** the MS SQL table space to use */ + protected String tableSpace = ""; + + /** + * Returns the configured MS SQL table space. + * @return the configured MS SQL table space. + */ + public String getTableSpace() { + return tableSpace; + } + + /** + * Sets the MS SQL table space. + * @param tableSpace the MS SQL table space. + */ + public void setTableSpace(String tableSpace) { + if (tableSpace != null && tableSpace.length() > 0) { + this.tableSpace = "on " + tableSpace.trim(); + } else { + this.tableSpace = ""; + } + } + + /** + * Creates a new MSSqlFileSystem instance. + */ + public MSSqlFileSystem() { + // preset some attributes to reasonable defaults + schema = "mssql"; + driver = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + } + + /** + * {@inheritDoc} + */ + @Override + protected CheckSchemaOperation createCheckSchemaOperation() { + return super.createCheckSchemaOperation().addVariableReplacement( + CheckSchemaOperation.TABLE_SPACE_VARIABLE, tableSpace); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/Oracle9FileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/Oracle9FileSystem.java new file mode 100644 index 00000000000..7a97b5170b3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/Oracle9FileSystem.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.core.fs.db; + +import javax.sql.DataSource; + +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.Oracle10R1ConnectionHelper; + +/** + * This class adds special blob handling which is needed for Oracle DBs up to version 10R1. + */ +public class Oracle9FileSystem extends OracleFileSystem { + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + Oracle10R1ConnectionHelper helper = new Oracle10R1ConnectionHelper(dataSrc, false); + helper.init(); + return helper; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/OracleFileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/OracleFileSystem.java new file mode 100644 index 00000000000..029c07a432f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/db/OracleFileSystem.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.fs.db; + +import javax.sql.DataSource; + +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.OracleConnectionHelper; + +/** + * OracleFileSystem is a JDBC-based FileSystem + * implementation for Jackrabbit that persists file system entries in an + * Oracle database. + *

    + * It is configured through the following properties: + *

      + *
    • driver: the FQN name of the JDBC driver class + * (default: "oracle.jdbc.OracleDriver")
    • + *
    • schema: type of schema to be used + * (default: "oracle")
    • + *
    • url: the database url (e.g. + * "jdbc:oracle:thin:@[host]:[port]:[sid]")
    • + *
    • user: the database user
    • + *
    • password: the user's password
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    • tablespace: the tablespace to use for tables (also used for indexes if indexTablespace is omitted)
    • + *
    • indexTablespace: the tablespace to use for indexes
    • + *
    + * See also {@link DbFileSystem}. + *

    + * The following is a fragment from a sample configuration: + *

    + *   <FileSystem class="org.apache.jackrabbit.core.fs.db.OracleFileSystem">
    + *       <param name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:orcl"/>
    + *       <param name="user" value="scott"/>
    + *       <param name="password" value="tiger"/>
    + *       <param name="schemaObjectPrefix" value="rep_"/>
    + *       <param name="tablespace" value="user"/>
    + *       <param name="indexTablespace" value="user"/>
    + *  </FileSystem>
    + * 
    + */ +public class OracleFileSystem extends DbFileSystem { + /** + * The default tablespace clause used when {@link #tablespace} or {@link #indexTablespace} + * are not specified. + */ + protected static final String DEFAULT_TABLESPACE_CLAUSE = ""; + + /** + * Name of the replacement variable in the DDL for {@link #tablespace}. + */ + protected static final String TABLESPACE_VARIABLE = "${tablespace}"; + + /** + * Name of the replacement variable in the DDL for {@link #indexTablespace}. + */ + protected static final String INDEX_TABLESPACE_VARIABLE = "${indexTablespace}"; + + /** The Oracle tablespace to use for tables */ + protected String tablespace; + + /** The Oracle tablespace to use for indexes */ + protected String indexTablespace; + + /** + * Creates a new OracleFileSystem instance. + */ + public OracleFileSystem() { + // preset some attributes to reasonable defaults + schema = "oracle"; + driver = "oracle.jdbc.OracleDriver"; + schemaObjectPrefix = ""; + tablespace = DEFAULT_TABLESPACE_CLAUSE; + indexTablespace = DEFAULT_TABLESPACE_CLAUSE; + initialized = false; + } + + /** + * Returns the configured Oracle tablespace for tables. + * @return the configured Oracle tablespace for tables. + */ + public String getTablespace() { + return tablespace; + } + + /** + * Sets the Oracle tablespace for tables. + * @param tablespaceName the Oracle tablespace for tables. + */ + public void setTablespace(String tablespaceName) { + this.tablespace = this.buildTablespaceClause(tablespaceName); + } + + /** + * Returns the configured Oracle tablespace for indexes. + * @return the configured Oracle tablespace for indexes. + */ + public String getIndexTablespace() { + return indexTablespace; + } + + /** + * Sets the Oracle tablespace for indexes. + * @param tablespaceName the Oracle tablespace for indexes. + */ + public void setIndexTablespace(String tablespaceName) { + this.indexTablespace = this.buildTablespaceClause(tablespaceName); + } + + /** + * Constructs the tablespace <tbs name> clause from + * the supplied tablespace name. If the name is empty, {@link #DEFAULT_TABLESPACE_CLAUSE} + * is returned instead. + * + * @param tablespaceName A tablespace name + * @return A tablespace clause using the supplied name or + * {@value #DEFAULT_TABLESPACE_CLAUSE} if the name is empty + */ + private String buildTablespaceClause(String tablespaceName) { + if (tablespaceName == null || tablespaceName.trim().length() == 0) { + return DEFAULT_TABLESPACE_CLAUSE; + } else { + return "tablespace " + tablespaceName.trim(); + } + } + + //-----------------------------------------< DatabaseFileSystem overrides > + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + OracleConnectionHelper helper = new OracleConnectionHelper(dataSrc, false); + helper.init(); + return helper; + } + + /** + * {@inheritDoc} + */ + @Override + protected CheckSchemaOperation createCheckSchemaOperation() { + if (DEFAULT_TABLESPACE_CLAUSE.equals(indexTablespace) && !DEFAULT_TABLESPACE_CLAUSE.equals(tablespace)) { + // tablespace was set but not indexTablespace : use the same for both + indexTablespace = tablespace; + } + return super.createCheckSchemaOperation() + .addVariableReplacement(TABLESPACE_VARIABLE, tablespace) + .addVariableReplacement(INDEX_TABLESPACE_VARIABLE, indexTablespace); + } + + //-----------------------------------------< DatabaseFileSystem overrides > + + /** + * Builds the SQL statements + *

    + * Since Oracle treats emtpy strings and BLOBs as null values the SQL + * statements had to be adapated accordingly. The following changes were + * necessary: + *

      + *
    • The distinction between file and folder entries is based on + * FSENTRY_LENGTH being null/not null rather than FSENTRY_DATA being + * null/not null because FSENTRY_DATA of a 0-length (i.e. empty) file is + * null in Oracle.
    • + *
    • Folder entries: Since the root folder has an empty name (which would + * be null in Oracle), an empty name is automatically converted and treated + * as " ".
    • + *
    + */ + protected void buildSQLStatements() { + insertFileSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "values (?, ?, ?, ?, ?)"; + + insertFolderSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "values (?, nvl(?, ' '), ?, null)"; + + updateDataSQL = "update " + + schemaObjectPrefix + "FSENTRY " + + "set FSENTRY_DATA = ?, FSENTRY_LASTMOD = ?, FSENTRY_LENGTH = ? " + + "where FSENTRY_PATH = ? and FSENTRY_NAME = ? " + + "and FSENTRY_LENGTH is not null"; + + updateLastModifiedSQL = "update " + + schemaObjectPrefix + "FSENTRY set FSENTRY_LASTMOD = ? " + + "where FSENTRY_PATH = ? and FSENTRY_NAME = ? " + + "and FSENTRY_LENGTH is not null"; + + selectExistSQL = "select 1 from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = nvl(?, ' ')"; + + selectFileExistSQL = "select 1 from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_LENGTH is not null"; + + selectFolderExistSQL = "select 1 from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = nvl(?, ' ') and FSENTRY_LENGTH is null"; + + selectFileNamesSQL = "select FSENTRY_NAME from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_LENGTH is not null"; + + selectFolderNamesSQL = "select FSENTRY_NAME from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME != ' ' " + + "and FSENTRY_LENGTH is null"; + + selectFileAndFolderNamesSQL = "select FSENTRY_NAME from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME != ' '"; + + selectChildCountSQL = "select count(FSENTRY_NAME) from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME != ' '"; + + selectDataSQL = "select nvl(FSENTRY_DATA, empty_blob()) from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_LENGTH is not null"; + + selectLastModifiedSQL = "select FSENTRY_LASTMOD from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = nvl(?, ' ')"; + + selectLengthSQL = "select nvl(FSENTRY_LENGTH, 0) from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_LENGTH is not null"; + + deleteFileSQL = "delete from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_LENGTH is not null"; + + deleteFolderSQL = "delete from " + + schemaObjectPrefix + "FSENTRY where " + + "(FSENTRY_PATH = ? and FSENTRY_NAME = nvl(?, ' ') and FSENTRY_LENGTH is null) " + + "or (FSENTRY_PATH = ?) " + + "or (FSENTRY_PATH like ?) "; + + copyFileSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "select ?, ?, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_NAME = ? and FSENTRY_LENGTH is not null"; + + copyFilesSQL = "insert into " + + schemaObjectPrefix + "FSENTRY " + + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH) " + + "select ?, FSENTRY_NAME, FSENTRY_DATA, " + + "FSENTRY_LASTMOD, FSENTRY_LENGTH from " + + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + + "and FSENTRY_LENGTH is not null"; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFile.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFile.java new file mode 100644 index 00000000000..7392673db37 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFile.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.core.fs.mem; + +/** + * An in-memory file. + */ +public class MemoryFile extends MemoryFileSystemEntry { + + private byte[] data = new byte[0]; + + public boolean isFolder() { + return false; + } + + protected byte[] getData() { + return data; + } + + protected void setData(byte[] data) { + this.data = data; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystem.java new file mode 100644 index 00000000000..d5e17772371 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystem.java @@ -0,0 +1,221 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.fs.mem; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; + +/** + * An in-memory file system implementation. + */ +public class MemoryFileSystem implements FileSystem { + + private Map entries = new HashMap(); + + public void close() { + } + + private MemoryFile getFile(String filePath) throws FileSystemException { + MemoryFileSystemEntry entry = getEntry(filePath); + assertIsFile(filePath); + return (MemoryFile) entry; + } + + public void createFolder(String folderPath) throws FileSystemException { + if (exists(folderPath)) { + throw new FileSystemException("Folder or file " + folderPath + + " already exists"); + } + if (!exists(FileSystem.SEPARATOR)) { + createFolderInternal("/"); + } + String relativePath = folderPath.substring(1); + String[] pathElements = relativePath.split(FileSystem.SEPARATOR); + String currentFolderPath = ""; + for (int i = 0; i < pathElements.length; i++) { + String pathElement = pathElements[i]; + currentFolderPath += "/" + pathElement; + createFolderInternal(currentFolderPath); + } + } + + private void createFolderInternal(String folderPath) { + MemoryFolder folder = new MemoryFolder(); + entries.put(folderPath, folder); + } + + public void deleteFile(String filePath) throws FileSystemException { + assertExistence(filePath); + entries.remove(filePath); + } + + public void deleteFolder(String folderPath) throws FileSystemException { + assertIsFolder(folderPath); + Set selectedNames = new HashSet(); + for (String name : entries.keySet()) { + if (name.equals(folderPath) || name.startsWith(folderPath + SEPARATOR)) { + selectedNames.add(name); + } + } + for (String name : selectedNames) { + entries.remove(name); + } + } + + public boolean exists(String path) throws FileSystemException { + return entries.containsKey(path); + } + + public InputStream getInputStream(String filePath) + throws FileSystemException { + assertExistence(filePath); + assertIsFile(filePath); + + MemoryFile file = getFile(filePath); + return new ByteArrayInputStream(file.getData()); + } + + private void assertIsFolder(String folderPath) throws FileSystemException { + assertExistence(folderPath); + if (!getEntry(folderPath).isFolder()) { + throw new FileSystemException("Folder " + folderPath + + " does not exist"); + } + } + + private void assertIsFile(String filePath) throws FileSystemException { + if (!isFile(filePath)) { + throw new FileSystemException(filePath + " is a folder"); + } + } + + public OutputStream getOutputStream(String filePath) + throws FileSystemException { + if (isFolder(filePath)) { + throw new FileSystemException("path denotes folder: " + filePath); + } + + String folderPath = filePath; + if (filePath.lastIndexOf(FileSystem.SEPARATOR) > 0) { + folderPath = filePath.substring(0, filePath.lastIndexOf("/")); + } else { + folderPath = "/"; + } + assertIsFolder(folderPath); + + final MemoryFile file = new MemoryFile(); + entries.put(filePath, file); + return new FilterOutputStream(new ByteArrayOutputStream()) { + public void write(byte[] bytes, int off, int len) throws IOException { + out.write(bytes, off, len); + } + + public void close() throws IOException { + out.close(); + file.setData(((ByteArrayOutputStream) out).toByteArray()); + } + }; + } + + public boolean hasChildren(String path) throws FileSystemException { + assertIsFolder(path); + return list(path).length > 0; + } + + public void init() { + createFolderInternal("/"); + } + + public boolean isFile(String path) throws FileSystemException { + return exists(path) && !getEntry(path).isFolder(); + } + + private MemoryFileSystemEntry getEntry(String path) { + return entries.get(path); + } + + private void assertExistence(String path) throws FileSystemException { + if (!exists(path)) { + throw new FileSystemException("no such file " + path); + } + } + + public boolean isFolder(String path) throws FileSystemException { + if (path.equals("/")) { + return true; + } else { + return exists(path) && getEntry(path).isFolder(); + } + } + + public long lastModified(String path) throws FileSystemException { + assertExistence(path); + return getEntry(path).getLastModified(); + } + + public long length(String filePath) throws FileSystemException { + assertIsFile(filePath); + return getFile(filePath).getData().length; + } + + public String[] list(String folderPath) { + if (folderPath.equals("/")) { + folderPath = ""; + } + Set selectedNames = new HashSet(); + for (String name : entries.keySet()) { + if (name.matches(folderPath + "/[^/]*") && !name.equals("/")) { + selectedNames.add(name.substring(folderPath.length() + 1)); + } + } + return selectedNames.toArray(new String[selectedNames.size()]); + } + + public String[] listFiles(String folderPath) { + return listInternal(folderPath, false); + } + + public String[] listFolders(String folderPath) { + return listInternal(folderPath, true); + } + + private String[] listInternal(String folderPath, boolean isFolder) { + String[] names = list(folderPath); + if (folderPath.equals("/")) { + folderPath = ""; + } + Set result = new HashSet(); + for (String n : names) { + if (getEntry(folderPath + "/" + n).isFolder() == isFolder) { + result.add(n); + } + } + return result.toArray(new String[result.size()]); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystemEntry.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystemEntry.java new file mode 100644 index 00000000000..6d55eb6c247 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystemEntry.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.core.fs.mem; + +/** + * An in-memory file or folder. + */ +public abstract class MemoryFileSystemEntry { + + private long lastModified; + + public abstract boolean isFolder(); + + public MemoryFileSystemEntry() { + lastModified = System.currentTimeMillis(); + } + + public long getLastModified() { + return lastModified; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFolder.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFolder.java new file mode 100644 index 00000000000..317ee720aba --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/fs/mem/MemoryFolder.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.core.fs.mem; + +/** + * An in-memory folder. + */ +public class MemoryFolder extends MemoryFileSystemEntry { + + public boolean isFolder() { + return true; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/gc/GarbageCollector.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/gc/GarbageCollector.java new file mode 100644 index 00000000000..634938f6306 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/gc/GarbageCollector.java @@ -0,0 +1,605 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.gc; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +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.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Workspace; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.ObservationManager; + +import org.apache.jackrabbit.api.management.DataStoreGarbageCollector; +import org.apache.jackrabbit.api.management.MarkEventListener; +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.core.persistence.IterablePersistenceManager; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.persistence.util.NodeInfo; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Garbage collector for DataStore. This implementation iterates through all + * nodes and reads the binary properties. To detect nodes that are moved while + * the scan runs, event listeners are started. Like the well known garbage + * collection in Java, the items that are still in use are marked. Currently + * this is achieved by updating the modified date of the entries. Newly added + * entries are detected because the modified date is changed when they are + * added. + *

    + * Example code to run the data store garbage collection: + *

    + * JackrabbitRepositoryFactory jf = (JackrabbitRepositoryFactory) factory;
    + * RepositoryManager m = jf.getRepositoryManager((JackrabbitRepository) repository);
    + * GarbageCollector gc = m.createDataStoreGarbageCollector();
    + * try {
    + *     gc.mark();
    + *     gc.sweep();
    + * } finally {
    + *     gc.close();
    + * }
    + * 
    + */ +public class GarbageCollector implements DataStoreGarbageCollector { + + private class ScanNodeIdListTask implements Callable { + + private int split; + private List nodeList; + private PersistenceManager pm; + private int pmCount; + + public ScanNodeIdListTask(int split, List nodeList, PersistenceManager pm, int pmCount) { + this.split = split; + this.nodeList = nodeList; + this.pm = pm; + this.pmCount = pmCount; + } + + public Void call() throws Exception { + scanNodeIdList(split, nodeList, pm, pmCount); + return null; + } + + } + + /** logger instance */ + static final Logger LOG = LoggerFactory.getLogger(GarbageCollector.class); + + /** + * The number of nodes to fetch at once from the persistence manager. Defaults to 8kb + */ + private static final int NODESATONCE = Integer.getInteger("org.apache.jackrabbit.garbagecollector.nodesatonce", 1024 * 8); + + /** + * Set this System Property to true to speed up the node traversing in a binary focused repository. + * See JCR-3708 + */ + private static final boolean NODE_ID_SCAN = Boolean.getBoolean("org.apache.jackrabbit.garbagecollector.node_id.scan"); + + private MarkEventListener callback; + + private long sleepBetweenNodes; + + private long minSplitSize = 100000; + + private int concurrentThreadSize = 3; + + protected int testDelay; + + private final DataStore store; + + private long startScanTimestamp; + + private final ArrayList listeners = new ArrayList(); + + private final IterablePersistenceManager[] pmList; + + private final SessionImpl[] sessionList; + + private final AtomicBoolean closed = new AtomicBoolean(); + + private final RepositoryContext context; + + private boolean persistenceManagerScan; + + private volatile RepositoryException observationException; + + /** + * Create a new garbage collector. + * This method is usually not called by the application, it is called + * by SessionImpl.createDataStoreGarbageCollector(). + * + * @param context repository context + * @param dataStore the data store to be garbage-collected + * @param list the persistence managers + * @param sessionList the sessions to access the workspaces + */ + + public GarbageCollector(RepositoryContext context, + DataStore dataStore, IterablePersistenceManager[] list, + SessionImpl[] sessionList) { + this.context = context; + this.store = dataStore; + this.pmList = list; + this.persistenceManagerScan = list != null; + this.sessionList = sessionList; + } + + public void setSleepBetweenNodes(long millis) { + this.sleepBetweenNodes = millis; + } + + public long getSleepBetweenNodes() { + return sleepBetweenNodes; + } + + public long getMinSplitSize() { + return minSplitSize; + } + + public void setMinSplitSize(long minSplitSize) { + this.minSplitSize = minSplitSize; + } + + public int getConcurrentThreadSize() { + return concurrentThreadSize; + } + + public void setConcurrentThreadSize(int concurrentThreadSize) { + this.concurrentThreadSize = concurrentThreadSize; + } + + /** + * When testing the garbage collection, a delay is used instead of simulating concurrent access. + * + * @param testDelay the delay in milliseconds + */ + public void setTestDelay(int testDelay) { + this.testDelay = testDelay; + } + + public void setMarkEventListener(MarkEventListener callback) { + this.callback = callback; + } + + public void mark() throws RepositoryException { + if (store == null) { + throw new RepositoryException("No DataStore configured."); + } + long now = System.currentTimeMillis(); + if (startScanTimestamp == 0) { + startScanTimestamp = now; + store.updateModifiedDateOnAccess(startScanTimestamp); + } + + if (pmList == null || !persistenceManagerScan) { + for (SessionImpl s : sessionList) { + scanNodes(s); + } + } else { + try { + if (!NODE_ID_SCAN) { + scanPersistenceManagersByNodeInfos(); + } else { + scanPersistenceManagersByNodeIds(); + } + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + } + + private void scanNodes(SessionImpl session) throws RepositoryException { + + // add a listener to get 'moved' nodes + Session clonedSession = session.createSession(session.getWorkspace().getName()); + listeners.add(new Listener(this, clonedSession)); + + // adding a link to a BLOB updates the modified date + // reading usually doesn't, but when scanning, it does + recurse(session.getRootNode(), sleepBetweenNodes); + } + + public void setPersistenceManagerScan(boolean allow) { + persistenceManagerScan = allow; + } + + public boolean isPersistenceManagerScan() { + return persistenceManagerScan; + } + + private void scanPersistenceManagersByNodeInfos() throws RepositoryException, ItemStateException { + int pmCount = 0; + for (IterablePersistenceManager pm : pmList) { + pmCount++; + int count = 0; + Map batch = pm.getAllNodeInfos(null, NODESATONCE); + while (!batch.isEmpty()) { + NodeId lastId = null; + for (NodeInfo info : batch.values()) { + count++; + if (count % 1000 == 0) { + LOG.debug(pm.toString() + " ("+pmCount + "/" + pmList.length + "): analyzed " + count + " nodes..."); + } + lastId = info.getId(); + if (callback != null) { + callback.beforeScanning(null); + } + if (info.hasBlobsInDataStore()) { + try { + NodeState state = pm.load(info.getId()); + Set propertyNames = state.getPropertyNames(); + for (Name name : propertyNames) { + PropertyId pid = new PropertyId(info.getId(), name); + PropertyState ps = pm.load(pid); + if (ps.getType() == PropertyType.BINARY) { + for (InternalValue v : ps.getValues()) { + // getLength will update the last modified date + // if the persistence manager scan is running + v.getLength(); + } + } + } + } catch (NoSuchItemStateException ignored) { + // the node may have been deleted in the meantime + } + } + } + batch = pm.getAllNodeInfos(lastId, NODESATONCE); + } + } + NodeInfo.clearPool(); + } + + private void scanPersistenceManagersByNodeIds() throws RepositoryException, ItemStateException { + int pmCount = 0; + for (IterablePersistenceManager pm : pmList) { + pmCount++; + List allNodeIds = pm.getAllNodeIds(null, 0); + int overAllCount = allNodeIds.size(); + if (overAllCount > minSplitSize) { + final int splits = getConcurrentThreadSize(); + ExecutorService executorService = Executors.newFixedThreadPool(splits); + try { + Set> futures = new HashSet>(); + List> lists = splitIntoParts(allNodeIds, splits); + LOG.debug(splits + " concurrent Threads will be started. Split Size: " + lists.get(0).size()+" Total Size: " + overAllCount); + for (int i = 0; i < splits; i++) { + List subList = lists.get(i); + futures.add(executorService.submit(new ScanNodeIdListTask(i + 1, subList, pm, pmCount))); + } + for (Future future : futures) { + future.get(); + } + } catch (Exception e) { + throw new RepositoryException(e); + } finally { + executorService.shutdown(); + } + } else { + scanNodeIdList(0, allNodeIds, pm, pmCount); + } + } + } + + private void scanNodeIdList(int split, List nodeList, PersistenceManager pm, int pmCount) throws RepositoryException, ItemStateException { + int count = 0; + for (NodeId id : nodeList) { + count++; + if (count % 1000 == 0) { + if (split > 0) { + LOG.debug("[Split " + split + "] " + pm.toString() + " (" + pmCount + "/" + pmList.length + "): analyzed " + count + " nodes [" + nodeList.size() + "]..."); + } else { + LOG.debug(pm.toString() + " (" + pmCount + "/" + pmList.length + "): analyzed " + count + " nodes [" + nodeList.size() + "]..."); + } + } + if (callback != null) { + callback.beforeScanning(null); + } + try { + NodeState state = pm.load(id); + Set propertyNames = state.getPropertyNames(); + for (Name name : propertyNames) { + PropertyId pid = new PropertyId(id, name); + PropertyState ps = pm.load(pid); + if (ps.getType() == PropertyType.BINARY) { + for (InternalValue v : ps.getValues()) { + // getLength will update the last modified date + // if the persistence manager scan is running + v.getLength(); + } + } + } + } catch (NoSuchItemStateException e) { + // the node may have been deleted or moved in the meantime + // ignore it + } + } + } + + private List> splitIntoParts(List ls, int parts) { + final List> listParts = new ArrayList>(); + final int chunkSize = ls.size() / parts; + int leftOver = ls.size() % parts; + int iTake = chunkSize; + + for (int i = 0, iT = ls.size(); i < iT; i += iTake) { + if (leftOver > 0) { + leftOver--; + iTake = chunkSize + 1; + } else { + iTake = chunkSize; + } + listParts.add(new ArrayList(ls.subList(i, Math.min(iT, i + iTake)))); + } + return listParts; + } + + /** + * Reset modifiedDateOnAccess to 0 and stop the observation + * listener if any are installed. + */ + public void stopScan() throws RepositoryException { + // reset updateModifiedDateOnAccess to OL + store.updateModifiedDateOnAccess(0L); + + if (listeners.size() > 0) { + for (Listener listener : listeners) { + listener.stop(); + } + listeners.clear(); + } + checkObservationException(); + context.setGcRunning(false); + } + + public int sweep() throws RepositoryException { + if (startScanTimestamp == 0) { + throw new RepositoryException("scan must be called first"); + } + stopScan(); + return store.deleteAllOlderThan(startScanTimestamp); + } + + /** + * Get the data store if one is used. + * + * @return the data store, or null + */ + public DataStore getDataStore() { + return store; + } + + void recurse(final Node n, long sleep) throws RepositoryException { + if (sleep > 0) { + try { + Thread.sleep(sleep); + } catch (InterruptedException e) { + // ignore + } + } + if (callback != null) { + callback.beforeScanning(n); + } + try { + for (PropertyIterator it = n.getProperties(); it.hasNext();) { + Property p = it.nextProperty(); + try { + if (p.getType() == PropertyType.BINARY) { + if (n.hasProperty("jcr:uuid")) { + rememberNode(n.getProperty("jcr:uuid").getString()); + } else { + rememberNode(n.getPath()); + } + if (p.isMultiple()) { + checkLengths(p.getLengths()); + } else { + checkLengths(p.getLength()); + } + } + } catch (InvalidItemStateException e) { + LOG.debug("Property removed concurrently - ignoring", e); + } + } + } catch (InvalidItemStateException e) { + LOG.debug("Node removed concurrently - ignoring", e); + } + try { + for (NodeIterator it = n.getNodes(); it.hasNext();) { + recurse(it.nextNode(), sleep); + } + } catch (InvalidItemStateException e) { + LOG.debug("Node removed concurrently - ignoring", e); + } + checkObservationException(); + } + + private void rememberNode(String path) { + // Do nothing at the moment + // TODO It may be possible to delete some items early + /* + * To delete files early in the garbage collection scan, we could do + * this: + * + * A) If garbage collection was run before, see if there a file with the + * list of UUIDs ('uuids.txt'). + * + * B) If yes, and if the checksum is ok, read all those nodes first (if + * not so many). This updates the modified date of all old files that + * are still in use. Afterwards, delete all files with an older modified + * date than the last scan! Newer files, and files that are read have a + * newer modification date. + * + * C) Delete the 'uuids.txt' file (in any case). + * + * D) Iterate (recurse) through all nodes and properties like now. If a + * node has a binary property, store the UUID of the node in the file + * ('uuids.txt'). Also store the time when the scan started. + * + * E) Checksum and close the file. + * + * F) Like now, delete files with an older modification date than this + * scan. + * + * We can't use node path for this, UUIDs are required as nodes could be + * moved around. + * + * This mechanism requires that all data stores update the last modified + * date when calling addRecord and that record already exists. + * + */ + } + + private static void checkLengths(long... lengths) throws RepositoryException { + for (long length : lengths) { + if (length == -1) { + throw new RepositoryException("mark failed to access a property"); + } + } + } + + public void close() { + if (!closed.getAndSet(true)) { + try { + stopScan(); + } catch (RepositoryException e) { + LOG.warn("An error occured when stopping the event listener", e); + } + for (Session s : sessionList) { + s.logout(); + } + } + } + + private void checkObservationException() throws RepositoryException { + RepositoryException e = observationException; + if (e != null) { + observationException = null; + String message = "Exception while processing concurrent events"; + LOG.warn(message, e); + e = new RepositoryException(message, e); + } + } + + void onObservationException(Exception e) { + if (e instanceof RepositoryException) { + observationException = (RepositoryException) e; + } else { + observationException = new RepositoryException(e); + } + } + + /** + * Auto-close in case the application didn't call it explicitly. + */ + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + + /** + * Event listener to detect moved nodes. + * A SynchronousEventListener is used to make sure this method is called before the main iteration ends. + */ + class Listener implements SynchronousEventListener { + + private final GarbageCollector gc; + private final Session session; + private final ObservationManager manager; + + Listener(GarbageCollector gc, Session session) + throws UnsupportedRepositoryOperationException, + RepositoryException { + this.gc = gc; + this.session = session; + Workspace ws = session.getWorkspace(); + manager = ws.getObservationManager(); + manager.addEventListener(this, Event.NODE_MOVED, "/", true, null, + null, false); + } + + void stop() throws RepositoryException { + manager.removeEventListener(this); + session.logout(); + } + + public void onEvent(EventIterator events) { + if (testDelay > 0) { + try { + Thread.sleep(testDelay); + } catch (InterruptedException e) { + // ignore + } + } + while (events.hasNext()) { + Event event = events.nextEvent(); + try { + String path = event.getPath(); + try { + Item item = session.getItem(path); + if (item.isNode()) { + Node n = (Node) item; + recurse(n, testDelay); + } + } catch (PathNotFoundException e) { + // ignore + } + } catch (Exception e) { + gc.onObservationException(e); + try { + stop(); + } catch (RepositoryException e2) { + LOG.warn("Exception removing the observation listener - ignored", e2); + } + } + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/ItemId.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/ItemId.java new file mode 100644 index 00000000000..37db8d5d613 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/ItemId.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.core.id; + +import java.io.Serializable; + +/** + * Node or property identifier. All content items in a Jackrabbit repository + * have an identifier that uniquely identifies the item in a workspace. + *

    + * This interface is implemented by both the concrete node and property + * identifier classes in order to allow client code to determine whether + * an identifier refers to a node or a property. + */ +public interface ItemId extends Serializable { + + /** + * Checks whether this identifier denotes a node item. + * + * @return true if this identifier denotes a node, + * false if a property + * @see PropertyId + * @see NodeId + */ + boolean denotesNode(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/NodeId.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/NodeId.java new file mode 100644 index 00000000000..87898f12c8f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/NodeId.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.id; + +import java.util.Random; +import java.util.UUID; + +/** + * Node identifier, i.e. an immutable 128 bit UUID. + */ +public class NodeId implements ItemId, Comparable { + + /** + * The serial version UID. + */ + private static final long serialVersionUID = 5773949574212570258L; + + /** + * Chars in a UUID String. + */ + public static final int UUID_FORMATTED_LENGTH = 36; + + /** + * Number of bytes in a UUID (16). + */ + public static final int UUID_BYTE_LENGTH = 16; + + /** + * Returns a node identifier that is represented by the given UUID string. + * + * @param uuid the UUID string + * @return the node identifier + * @throws IllegalArgumentException if the given string is null + * or not a valid UUID + */ + public static NodeId valueOf(String uuid) throws IllegalArgumentException { + if (uuid != null) { + return new NodeId(uuid); + } else { + throw new IllegalArgumentException("NodeId.valueOf(null)"); + } + } + + /** + * The most significant 64 bits (bytes 0-7) of the UUID. + */ + private final long msb; + + /** + * The least significant 64 bits (bytes 8-15) of the UUID. + */ + private final long lsb; + + /** + * Creates a node identifier from the given 128 bits. + * + * @param msb most significant 64 bits + * @param lsb least significant 64 bits + */ + public NodeId(long msb, long lsb) { + this.msb = msb; + this.lsb = lsb; + } + + /** + * Creates a node identifier from the given 16 bytes. + * + * @param bytes array of 16 bytes + * @throws NullPointerException if the given array is null + * @throws ArrayIndexOutOfBoundsException + * if the given array is less than 16 bytes long + */ + public NodeId(byte[] bytes) + throws NullPointerException, ArrayIndexOutOfBoundsException { + this( // Most significant 64 bits + ((((long) bytes[0]) & 0xFF) << 56) + + ((((long) bytes[1]) & 0xFF) << 48) + + ((((long) bytes[2]) & 0xFF) << 40) + + ((((long) bytes[3]) & 0xFF) << 32) + + ((((long) bytes[4]) & 0xFF) << 24) + + ((((long) bytes[5]) & 0xFF) << 16) + + ((((long) bytes[6]) & 0xFF) << 8) + + ((((long) bytes[7]) & 0xFF)), + // Least significant 64 bits + ((((long) bytes[8]) & 0xFF) << 56) + + ((((long) bytes[9]) & 0xFF) << 48) + + ((((long) bytes[10]) & 0xFF) << 40) + + ((((long) bytes[11]) & 0xFF) << 32) + + ((((long) bytes[12]) & 0xFF) << 24) + + ((((long) bytes[13]) & 0xFF) << 16) + + ((((long) bytes[14]) & 0xFF) << 8) + + ((((long) bytes[15]) & 0xFF))); + } + + /** + * Creates a node identifier from the given UUID. + * + * @param uuid UUID + */ + public NodeId(UUID uuid) { + this(uuid.getMostSignificantBits(), uuid.getLeastSignificantBits()); + } + + /** + * Creates a node identifier from the given UUID string. + * + * @param uuidString UUID string + * @throws IllegalArgumentException if the UUID string is invalid + */ + public NodeId(String uuidString) throws IllegalArgumentException { + // e.g. f81d4fae-7dec-11d0-a765-00a0c91e6bf6 + // 012345678901234567890123456789012345 + if (uuidString.length() != UUID_FORMATTED_LENGTH) { + throw new IllegalArgumentException(uuidString); + } + long m = 0, x = 0; + for (int i = 0; i < UUID_FORMATTED_LENGTH; i++) { + int c = uuidString.charAt(i); + switch (i) { + case 18: + m = x; + x = 0; + // fall through + case 8: + case 13: + case 23: + if (c != '-') { + throw new IllegalArgumentException(uuidString); + } + break; + default: + if (c >= '0' && c <= '9') { + x = (x << 4) | (c - '0'); + } else if (c >= 'a' && c <= 'f') { + x = (x << 4) | (c - 'a' + 0xa); + } else if (c >= 'A' && c <= 'F') { + x = (x << 4) | (c - 'A' + 0xa); + } else { + throw new IllegalArgumentException(uuidString); + } + } + } + this.msb = m; + this.lsb = x; + } + + /** + * Creates a random node identifier using a secure random number generator. + */ + public static NodeId randomId() { + Random random = SeededSecureRandom.getInstance(); + return new NodeId( + // Most significant 64 bits, with version field set to 4 + random.nextLong() & 0xFfffFfffFfff0fffL | 0x0000000000004000L, + // Least significant 64 bits, with variant field set to IETF + random.nextLong() & 0x3fffFfffFfffFfffL | 0x8000000000000000L + ); + } + + /** + * Returns the 64 most significant bits of this identifier. + * + * @return 64 most significant bits + */ + public long getMostSignificantBits() { + return msb; + } + + /** + * Returns the 64 least significant bits of this identifier. + * + * @return 64 least significant bits + */ + public long getLeastSignificantBits() { + return lsb; + } + + /** + * Returns the 16 bytes of this identifier. + * + * @return newly allocated array of 16 bytes + */ + public byte[] getRawBytes() { + return new byte[] { + (byte) (msb >> 56), (byte) (msb >> 48), (byte) (msb >> 40), + (byte) (msb >> 32), (byte) (msb >> 24), (byte) (msb >> 16), + (byte) (msb >> 8), (byte) msb, + (byte) (lsb >> 56), (byte) (lsb >> 48), (byte) (lsb >> 40), + (byte) (lsb >> 32), (byte) (lsb >> 24), (byte) (lsb >> 16), + (byte) (lsb >> 8), (byte) lsb + }; + } + + //--------------------------------------------------------------< ItemId > + + /** + * Returns true to indicate that this is a node identifier. + * + * @return always true + */ + public boolean denotesNode() { + return true; + } + + //----------------------------------------------------------< Comparable > + + /** + * Compares this identifier to the given other one. + * + * @param that other identifier + * @return -1, 0 or +1 if this identifier is less than, equal to, + * or greater than the given other identifier + */ + public int compareTo(NodeId that) { + // This is not a 128 bit integer comparison! See also JCR-687. + if (msb < that.msb) { + return -1; + } else if (msb > that.msb) { + return 1; + } else if (lsb < that.lsb) { + return -1; + } else if (lsb > that.lsb) { + return 1; + } else { + return 0; + } + } + + //--------------------------------------------------------------< Object > + + /** + * Returns the UUID string representation of this identifier. + * + * @see UUID#toString() + * @return UUID string + */ + public String toString() { + char[] retval = new char[36]; + hex4(retval, 0, msb >>> 48); + hex4(retval, 4, msb >>> 32); + retval[8] = '-'; + hex4(retval, 9, msb >>> 16); + retval[13] = '-'; + hex4(retval, 14, msb); + retval[18] = '-'; + hex4(retval, 19, lsb >>> 48); + retval[23] = '-'; + hex4(retval, 24, lsb >>> 32); + hex4(retval, 28, lsb >>> 16); + hex4(retval, 32, lsb); + return new String(retval); + } + + private static final void hex4(char[] c, int index, long value) { + for (int i = 0; i < 4; i++) { + long v = (value >>> (12 - i * 4)) & 0xf; + if (v < 10) { + c[index + i] = (char) (v + '0'); + } else { + c[index + i] = (char) (v - 10 + 'a'); + } + } + } + + /** + * Compares two UUID for equality. + * + * @see Object#equals(Object) + */ + public boolean equals(Object that) { + return that instanceof NodeId + && msb == ((NodeId) that).msb + && lsb == ((NodeId) that).lsb; + } + + /** + * Returns a hash code of this identifier. + * + * @return hash code + */ + public int hashCode() { + return (int) ((msb >>> 32) ^ msb ^ (lsb >>> 32) ^ lsb); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/NodeIdFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/NodeIdFactory.java new file mode 100644 index 00000000000..b124e7168f5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/NodeIdFactory.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.core.id; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.UUID; +import javax.jcr.RepositoryException; + +/** + * A factory for creating new node ids. + */ +public class NodeIdFactory { + + public final static String SEQUENTIAL_NODE_ID = "jackrabbit.sequentialNodeId"; + + private final static String NODE_ID_FILE = "nodeId.properties"; + private final static String NODE_ID_FILE_TEMP = "nodeId.properties.temp"; + private final static String MSB = "msb"; + private final static String NEXT_LSB = "nextLsb"; + private final static int DEFAULT_CACHE_SIZE = 128; + + private final String repositoryHome; + + private boolean createRandom; + private long msb; + private long nextLsb; + private long storedLsb; + private int cacheSize = DEFAULT_CACHE_SIZE; + + public NodeIdFactory(String repositoryHome) { + this.repositoryHome = repositoryHome; + } + + public void setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + } + + public void open() throws RepositoryException { + String seq = System.getProperty(SEQUENTIAL_NODE_ID); + if (seq == null) { + createRandom = true; + return; + } + try { + File n = new File(repositoryHome, NODE_ID_FILE); + if (!n.exists()) { + File temp = new File(repositoryHome, NODE_ID_FILE_TEMP); + if (temp.exists()) { + temp.renameTo(n); + } else { + n.getParentFile().mkdirs(); + n.createNewFile(); + } + } + Properties p = new Properties(); + FileInputStream in = new FileInputStream(n); + try { + p.load(in); + } finally { + in.close(); + } + String defaultMsb = "", defaultLsb = "0"; + int index = seq.indexOf("/"); + if (index >= 0) { + defaultMsb = seq.substring(0, index); + defaultLsb = seq.substring(index + 1); + } + String m = p.getProperty(MSB, defaultMsb); + if (m.length() == 0) { + msb = UUID.randomUUID().getMostSignificantBits(); + // ensure it doesn't conflict with version 1-5 UUIDs + msb &= ~0xf000; + } else { + if (m.length() == 16) { + msb = (Long.parseLong(m.substring(0, 8), 16) << 32) | + Long.parseLong(m.substring(8), 16); + } else { + msb = Long.parseLong(m, 16); + } + } + storedLsb = nextLsb = Long.parseLong(p.getProperty(NEXT_LSB, defaultLsb), 16); + } catch (Exception e) { + throw new RepositoryException("Could not open node id factory", e); + } + } + + public void close() throws RepositoryException { + if (!createRandom) { + store(nextLsb); + } + } + + private void store(long lsb) throws RepositoryException { + this.storedLsb = lsb; + Properties p = new Properties(); + p.setProperty(MSB, Long.toHexString(msb)); + p.setProperty(NEXT_LSB, Long.toHexString(lsb)); + try { + File temp = new File(repositoryHome, NODE_ID_FILE_TEMP); + FileOutputStream out = new FileOutputStream(temp); + try { + p.store(out, null); + } finally { + out.close(); + } + File n = new File(repositoryHome, NODE_ID_FILE); + n.delete(); + temp.renameTo(n); + } catch (IOException e) { + throw new RepositoryException("Could not store next node id", e); + } + } + + public NodeId newNodeId() throws RepositoryException { + if (createRandom) { + return NodeId.randomId(); + } + long lsb = nextLsb++; + if (lsb >= storedLsb) { + store(lsb + cacheSize); + } + return new NodeId(msb, lsb); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/PropertyId.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/PropertyId.java new file mode 100644 index 00000000000..88cb58530e6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/PropertyId.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.core.id; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; + +/** + * Property identifier. An instance of this class identifies a single + * property using the UUID of the parent node and the name of + * the property. Once created a property identifier instance is immutable. + */ +public class PropertyId implements ItemId { + + /** Serial version UID of this class. */ + static final long serialVersionUID = 1118783735407446009L; + + /** id of the parent node. */ + private final NodeId parentId; + + /** Name of the property. */ + private final Name propName; + + /** the precalculated hash code */ + private final int hashCode; + + /** + * Creates a property identifier instance for the identified property. + * + * @param parentId the id of the parent node + * @param propName Name of the property + */ + public PropertyId(NodeId parentId, Name propName) { + if (parentId == null) { + throw new IllegalArgumentException("parentId can not be null"); + } + if (propName == null) { + throw new IllegalArgumentException("propName can not be null"); + } + this.parentId = parentId; + this.propName = propName; + + int h = 17; + h = 37 * h + parentId.hashCode(); + h = 37 * h + propName.hashCode(); + this.hashCode = h; + } + + /** + * Returns false as this class represents a property + * identifier, not a node identifier. + * + * @return always false + * @see ItemId#denotesNode() + */ + public boolean denotesNode() { + return false; + } + + /** + * Returns the identifier of the parent node. + * + * @return id of parent node + */ + public NodeId getParentId() { + return parentId; + } + + /** + * Returns the Name of the property. + * + * @return Name of the property. + */ + public Name getName() { + return propName; + } + + /** + * Returns a property identifier instance holding the value of the + * specified string. The string must be in the format returned by the + * {@link #toString() toString()} method of this class. + * + * @param s a String containing the PropertyId + * representation to be parsed. + * @return the PropertyId represented by the argument + * @throws IllegalArgumentException if the specified string can not be parsed + * as a PropertyId. + * @see #toString() + */ + public static PropertyId valueOf(String s) throws IllegalArgumentException { + if (s == null) { + throw new IllegalArgumentException("invalid PropertyId literal"); + } + int i = s.indexOf('/'); + if (i == -1) { + throw new IllegalArgumentException("invalid PropertyId literal"); + } + String uuid = s.substring(0, i); + Name name = NameFactoryImpl.getInstance().create(s.substring(i + 1)); + + return new PropertyId(NodeId.valueOf(uuid), name); + } + + //-------------------------------------------< java.lang.Object overrides > + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof PropertyId) { + PropertyId other = (PropertyId) obj; + return parentId.equals(other.parentId) + && propName.equals(other.propName); + } + return false; + } + + /** + * {@inheritDoc} + * + * Returns the same as this.getParentId() + "/" + this.getName() + */ + public String toString() { + return parentId + "/" + propName; + } + + /** + * {@inheritDoc} + * + * Returns the hash code of this property identifier. The hash code + * is computed from the parent node id and the property name. + */ + public int hashCode() { + return hashCode; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/SeededSecureRandom.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/SeededSecureRandom.java new file mode 100644 index 00000000000..38fd2c9ec06 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/id/SeededSecureRandom.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.core.id; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * Automatically seeded singleton secure random number generator. + * + * @see JCR-1206: + * UUID generation: SecureRandom should be used by default + */ +class SeededSecureRandom extends SecureRandom implements Runnable { + + /** + * Maximum number of milliseconds to wait for the seeding. + */ + private static final int MAX_SEED_TIME = 1000; + + /** + * Singleton instance of this class. Initialized when first accessed. + */ + private static volatile Random instance = null; + + /** + * Returns the singleton instance of this class. The instance is + * created and seeded when this method is first called. + * + * @return seeded secure random number generator + */ + public static Random getInstance() { + if (instance == null) { + synchronized (SeededSecureRandom.class) { + if (instance == null) { + instance = new SeededSecureRandom(); + } + } + } + return instance; + } + + /** + * Flag to indicate whether seeding is complete. + */ + private volatile boolean seeded = false; + + /** + * Creates and seeds a secure random number generator. + */ + private SeededSecureRandom() { + // Can not do that in a static initializer block, because + // threads are not started after the initializer block exits + Thread thread = new Thread(this, "SeededSecureRandom"); + thread.setDaemon(true); + thread.start(); + try { + thread.join(MAX_SEED_TIME); + } catch (InterruptedException e) { + // ignore + } + + if (!seeded) { + // Alternative seed algorithm if the default is very slow + setSeed(System.currentTimeMillis()); + setSeed(System.nanoTime()); + setSeed(new Object().hashCode()); + Runtime runtime = Runtime.getRuntime(); + setSeed(runtime.freeMemory()); + setSeed(runtime.maxMemory()); + setSeed(runtime.totalMemory()); + setSeed(System.getProperties().toString().hashCode()); + + // Thread timing (a second thread is already running) + for (int j = 0; j < 16; j++) { + int i = 0; + long start = System.currentTimeMillis(); + while (start == System.currentTimeMillis()) { + i++; + } + // Supplement the existing seed + setSeed(i); + } + } + } + + /** + * Seeds this random number generator with 32 bytes of random data. + * Run in an initializer thread as this may be slow on some systems, see + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6202721. + */ + public void run() { + setSeed(generateSeed(32)); + seeded = true; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/BindableRepository.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/BindableRepository.java new file mode 100644 index 00000000000..8108b2ef111 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/BindableRepository.java @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.jndi; + +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.commons.AbstractRepository; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.naming.Reference; +import javax.naming.Referenceable; + +/** + * A referenceable and serializable content repository proxy. + * This class implements the Proxy design pattern (GoF) for the + * Jackrabbit Repository implementation. The proxy implementation + * delays the instantiation of the actual Repository instance and + * implements serialization and JNDI referenceability by keeping + * track of the repository configuration parameters. + *

    + * A BindableRepository instance contains the configuration file + * and home directory paths of a Jackrabbit repository. The separate + * {@link #init() init()} method is used to create a transient + * {@link RepositoryImpl RepositoryImpl} instance to which all the + * JCR API calls are delegated. + *

    + * An instance of this class is normally always also initialized. + * The uninitialized state is only used briefly during the static + * construction, deserialization, and JNDI "referenciation". + *

    + * A JVM shutdown hook is used to make sure that the initialized + * repository is properly closed when the JVM shuts down. The + * {@link RegistryHelper#unregisterRepository(javax.naming.Context, String)} + * method should be used to explicitly close the repository if + * needed. + */ +public class BindableRepository extends AbstractRepository + implements javax.jcr.Repository, JackrabbitRepository, Referenceable, Serializable { + + /** + * The serialization UID of this class. + */ + private static final long serialVersionUID = 8864716577016297651L; + + /** + * type of configFilePath reference address + * @see Reference#get(String) + */ + public static final String CONFIGFILEPATH_ADDRTYPE = "configFilePath"; + + /** + * type of repHomeDir reference address + * @see Reference#get(String) + */ + public static final String REPHOMEDIR_ADDRTYPE = "repHomeDir"; + + /** + * The repository reference + */ + private final Reference reference; + + /** + * The delegate repository instance. Created by {@link #init() init}. + */ + private transient JackrabbitRepository repository; + + /** + * Thread that is registered as shutdown hook after {@link #init} has been + * called. + */ + private transient Thread hook; + + /** + * Creates a BindableRepository instance with the configuration + * information in the given JNDI reference. + * + * @param reference JNDI reference + * @throws RepositoryException if the repository can not be started + */ + public BindableRepository(Reference reference) throws RepositoryException { + this.reference = reference; + init(); + } + + /** + * Creates the underlying repository instance. A shutdown hook is + * registered to make sure that the initialized repository gets closed + * when the JVM shuts down. + * + * @throws RepositoryException if the repository cannot be created + */ + private void init() throws RepositoryException { + repository = createRepository(); + hook = new Thread() { + public void run() { + shutdown(); + } + }; + Runtime.getRuntime().addShutdownHook(hook); + } + + /** + * Creates a repository instance based on the contained JNDI reference. + * Can be overridden by subclasses to return different repositories. + * A subclass can access the JNDI reference through the + * {@link #getReference()} method. The default implementation + * returns a {@link RepositoryImpl} instance. + * + * @return repository instance + * @throws RepositoryException if the repository could not be created + */ + protected JackrabbitRepository createRepository() + throws RepositoryException { + RepositoryConfig config = RepositoryConfig.create( + reference.get(CONFIGFILEPATH_ADDRTYPE).getContent().toString(), + reference.get(REPHOMEDIR_ADDRTYPE).getContent().toString()); + return RepositoryImpl.create(config); + } + + /** + * Returns the underlying repository instance. Can be used by subclasses + * to access the repository instance. + * + * @return repository instance + */ + protected JackrabbitRepository getRepository() { + return repository; + } + + //-----------------------------------------------------------< Repository > + + /** + * Delegated to the underlying repository instance. + * {@inheritDoc} + */ + public Session login(Credentials credentials, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException { + return repository.login(credentials, workspaceName); + } + + /** + * Delegated to the underlying repository instance. + * {@inheritDoc} + */ + public String getDescriptor(String key) { + return repository.getDescriptor(key); + } + + /** + * Delegated to the underlying repository instance. + * {@inheritDoc} + */ + public String[] getDescriptorKeys() { + return repository.getDescriptorKeys(); + } + + /** + * Delegated to the underlying repository instance. + * {@inheritDoc} + */ + public Value getDescriptorValue(String key) { + return repository.getDescriptorValue(key); + } + + /** + * Delegated to the underlying repository instance. + * {@inheritDoc} + */ + public Value[] getDescriptorValues(String key) { + return repository.getDescriptorValues(key); + } + + /** + * Delegated to the underlying repository instance. + * {@inheritDoc} + */ + public boolean isSingleValueDescriptor(String key) { + return repository.isSingleValueDescriptor(key); + } + + /** + * Delegated to the underlying repository instance. + * {@inheritDoc} + */ + public boolean isStandardDescriptor(String key) { + return repository.isStandardDescriptor(key); + } + + //--------------------------------------------------------< Referenceable > + + /** + * Returns the JNDI reference for this content repository. The returned + * reference holds the configuration information required to create a + * copy of this instance. + * + * @return the JNDI reference + */ + public Reference getReference() { + return reference; + } + + //-------------------------------------------------< Serializable support > + + /** + * Deserializes a repository instance. The repository configuration + * is deserialized using the standard deserialization mechanism, and + * the underlying delegate repository is created using the + * {@link #init() init} method. + * + * @param in the serialization stream + * @throws IOException if configuration information cannot be deserialized + * or if the configured repository cannot be created + * @throws ClassNotFoundException on deserialization errors + */ + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + // delegate deserialization to default implementation + in.defaultReadObject(); + // initialize reconstructed instance + try { + init(); + } catch (RepositoryException e) { + // failed to reinstantiate repository + IOException exception = new IOException(e.getMessage()); + exception.initCause(e); + throw exception; + } + } + + /** + * Delegated to the underlying repository instance. + */ + public void shutdown() { + BindableRepositoryFactory.removeReference(reference); + repository.shutdown(); + try { + Runtime.getRuntime().removeShutdownHook(hook); + } catch (IllegalStateException e) { + // ignore. exception is thrown when hook itself calls shutdown + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/BindableRepositoryFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/BindableRepositoryFactory.java new file mode 100644 index 00000000000..3943111020d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/BindableRepositoryFactory.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.core.jndi; + +import java.util.Hashtable; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.commons.collections.map.ReferenceMap; + +/** + * BindableRepositoryFactory is an object factory that when given + * a reference for a BindableRepository object, will create an + * instance of the corresponding BindableRepository. + */ +public class BindableRepositoryFactory implements ObjectFactory { + + /** + * cache using java.naming.Reference objects as keys and + * storing soft references to BindableRepository instances + */ + private static final Map cache = new ReferenceMap(); + + /** + * {@inheritDoc} + */ + public Object getObjectInstance( + Object obj, Name name, Context nameCtx, Hashtable environment) + throws RepositoryException { + synchronized (cache) { + Object instance = cache.get(obj); + if (instance == null && obj instanceof Reference) { + instance = new BindableRepository((Reference) obj); + cache.put(obj, instance); + } + return instance; + } + } + + + /** + * Invalidates the given reference in this factory's cache. Called by + * {@link BindableRepository#shutdown()} to remove the old reference. + * + * @param reference repository reference + */ + static void removeReference(Reference reference) { + synchronized (cache) { + cache.remove(reference); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/RegistryHelper.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/RegistryHelper.java new file mode 100644 index 00000000000..5a8e406ce17 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/RegistryHelper.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.core.jndi; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.Reference; +import javax.naming.StringRefAddr; + +import org.apache.jackrabbit.api.JackrabbitRepository; + +/** + * JNDI helper functionality. This class contains static utility + * methods for binding and unbinding Jackrabbit repositories to and + * from a JNDI context. + */ +public class RegistryHelper { + + /** + * hidden constructor + */ + private RegistryHelper() { + } + + /** + * Binds a configured repository to the given JNDI context. + * This method creates a {@link BindableRepository BindableRepository} + * instance using the given configuration information, and binds + * it to the given JNDI context. + * + * @param ctx context where the repository should be registered (i.e. bound) + * @param name the name to register the repository with + * @param configFilePath path to the configuration file of the repository + * @param repHomeDir repository home directory + * @param overwrite if true, any existing binding with the given + * name will be overwritten; otherwise a NamingException will + * be thrown if the name is already bound + * @throws RepositoryException if the repository cannot be created + * @throws NamingException if the repository cannot be registered in JNDI + */ + public static void registerRepository(Context ctx, String name, + String configFilePath, + String repHomeDir, + boolean overwrite) + throws NamingException, RepositoryException { + Reference reference = new Reference( + Repository.class.getName(), + BindableRepositoryFactory.class.getName(), + null); // no classpath defined + reference.add(new StringRefAddr( + BindableRepository.CONFIGFILEPATH_ADDRTYPE, configFilePath)); + reference.add(new StringRefAddr( + BindableRepository.REPHOMEDIR_ADDRTYPE, repHomeDir)); + + // always create instance by using BindableRepositoryFactory + // which maintains an instance cache; + // see http://issues.apache.org/jira/browse/JCR-411 for details + Object obj = new BindableRepositoryFactory().getObjectInstance( + reference, null, null, null); + if (overwrite) { + ctx.rebind(name, obj); + } else { + ctx.bind(name, obj); + } + } + + /** + * This method shutdowns a {@link BindableRepository BindableRepository} + * instance using the given configuration information, and unbinds + * it from the given JNDI context. + * + * @param ctx context where the repository should be unregistered (i.e. unbound) + * @param name the name of the repository to unregister + * @throws NamingException on JNDI errors + */ + public static void unregisterRepository(Context ctx, String name) + throws NamingException { + ((JackrabbitRepository) ctx.lookup(name)).shutdown(); + ctx.unbind(name); + } + +} diff --git a/src/java/org/apache/jackrabbit/core/jndi/provider/DummyContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/provider/DummyContext.java similarity index 95% rename from src/java/org/apache/jackrabbit/core/jndi/provider/DummyContext.java rename to jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/provider/DummyContext.java index ceec4716ce5..8f9651fe7e8 100644 --- a/src/java/org/apache/jackrabbit/core/jndi/provider/DummyContext.java +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/provider/DummyContext.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -35,7 +35,7 @@ import java.util.Properties; /** - * DummyContext is a simple service provider that + * DummyContext is a simple service provider that * implements a flat namespace in memory. It is intended to be used for * testing purposes only. */ diff --git a/src/java/org/apache/jackrabbit/core/jndi/provider/DummyInitialContextFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/provider/DummyInitialContextFactory.java similarity index 78% rename from src/java/org/apache/jackrabbit/core/jndi/provider/DummyInitialContextFactory.java rename to jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/provider/DummyInitialContextFactory.java index ac587c005aa..913394d6813 100644 --- a/src/java/org/apache/jackrabbit/core/jndi/provider/DummyInitialContextFactory.java +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/jndi/provider/DummyInitialContextFactory.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AbstractJournal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AbstractJournal.java new file mode 100644 index 00000000000..7670faa697f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AbstractJournal.java @@ -0,0 +1,466 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import java.io.File; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.jackrabbit.core.util.XAReentrantWriterPreferenceReadWriteLock; +import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; +import org.apache.jackrabbit.core.version.VersioningLock; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base journal implementation. + */ +public abstract class AbstractJournal implements Journal { + + /** + * Logger. + */ + private static Logger log = LoggerFactory.getLogger(AbstractJournal.class); + + /** + * Journal id. + */ + private String id; + + /** + * Namespace resolver. + */ + private NamespaceResolver resolver; + + /** + * NamePathResolver + */ + private NamePathResolver npResolver; + + /** + * Map of registered consumers. + */ + private final Map consumers = new HashMap(); + + /** + * Map of registered producers. + */ + private final Map producers = new HashMap(); + + /** + * Journal lock, allowing multiple readers (synchronizing their contents) + * but only one writer (appending a new entry). + */ + private final XAReentrantWriterPreferenceReadWriteLock rwLock = new XAReentrantWriterPreferenceReadWriteLock(); + + /** + * The path of the local revision file on disk. Configurable through the repository.xml. + * + * Note: this field is not located in the FileJournal class for upgrade reasons (before + * JCR-1087 had been fixed all Journals used a revision file on the local file system. + * Also see {@link DatabaseJournal#initInstanceRevisionAndJanitor()}). + */ + private String revision; + + /** + * Repository home. + */ + private File repHome; + + /** + * Internal version manager. + */ + private InternalVersionManagerImpl internalVersionManager; + + /** + * {@inheritDoc} + */ + public void init(String id, NamespaceResolver resolver) throws JournalException { + this.id = id; + this.resolver = resolver; + this.npResolver = new DefaultNamePathResolver(resolver, true); + } + + /** + * {@inheritDoc} + */ + public void register(RecordConsumer consumer) throws JournalException { + synchronized (consumers) { + String consumerId = consumer.getId(); + if (consumers.containsKey(consumerId)) { + String msg = "Record consumer with identifier '" + + consumerId + "' already registered."; + throw new JournalException(msg); + } + consumers.put(consumerId, consumer); + } + } + + /** + * {@inheritDoc} + */ + public boolean unregister(RecordConsumer consumer) { + synchronized (consumers) { + String consumerId = consumer.getId(); + return consumers.remove(consumerId) != null; + } + } + + /** + * Return the consumer given its identifier. + * + * @param identifier identifier + * @return consumer associated with identifier; + * null if no consumer is associated with identifier + */ + public RecordConsumer getConsumer(String identifier) { + synchronized (consumers) { + return consumers.get(identifier); + } + } + + /** + * {@inheritDoc} + */ + public RecordProducer getProducer(String identifier) { + synchronized (producers) { + RecordProducer producer = producers.get(identifier); + if (producer == null) { + producer = createProducer(identifier); + producers.put(identifier, producer); + } + return producer; + } + } + + /** + * Create the record producer for a given identifier. May be overridden + * by subclasses. + * + * @param identifier producer identifier + */ + protected RecordProducer createProducer(String identifier) { + return new DefaultRecordProducer(this, identifier); + } + + /** + * Return the minimal revision of all registered consumers. + */ + private long getMinimalRevision() { + long minimalRevision = Long.MAX_VALUE; + + synchronized (consumers) { + for (RecordConsumer consumer : consumers.values()) { + if (consumer.getRevision() < minimalRevision) { + minimalRevision = consumer.getRevision(); + } + } + } + return minimalRevision; + } + + + /** + * {@inheritDoc} + */ + public void sync(boolean startup) throws JournalException { + log.debug("Synchronize to the latest change. Startup: " + startup); + for (;;) { + if (internalVersionManager != null) { + VersioningLock.ReadLock lock = + internalVersionManager.acquireReadLock(); + try { + internalSync(startup); + } finally { + lock.release(); + } + } else { + internalSync(startup); + } + // startup sync already done, don't do it again + startup = false; + if (syncAgainOnNewRecords()) { + // sync again if there are more records available + RecordIterator it = getRecords(getMinimalRevision()); + try { + if (it.hasNext()) { + continue; + } + } finally { + it.close(); + } + } + break; + } + } + + private void internalSync(boolean startup) throws JournalException { + try { + rwLock.readLock().acquire(); + } catch (InterruptedException e) { + String msg = "Unable to acquire read lock."; + throw new JournalException(msg, e); + } + try { + doSync(getMinimalRevision(), startup); + } finally { + rwLock.readLock().release(); + } + } + + + protected void doSync(long startRevision, boolean startup) throws JournalException { + // by default ignore startup parameter for backwards compatibility + // only needed for persistence backend that need special treatment on startup. + doSync(startRevision); + } + + /** + * + * Synchronize contents from journal. May be overridden by subclasses. + * + * @param startRevision start point (exclusive) + * @throws JournalException if an error occurs + */ + protected void doSync(long startRevision) throws JournalException { + log.debug("Synchronize contents from journal. StartRevision: " + startRevision); + RecordIterator iterator = getRecords(startRevision); + long stopRevision = Long.MIN_VALUE; + + try { + while (iterator.hasNext()) { + Record record = iterator.nextRecord(); + if (record.getJournalId().equals(id)) { + log.debug("Record with revision '" + record.getRevision() + + "' created by this journal, skipped."); + } else { + RecordConsumer consumer = getConsumer(record.getProducerId()); + if (consumer != null) { + consumer.consume(record); + } + } + stopRevision = record.getRevision(); + } + } catch (IllegalStateException e) { + log.error("Could not synchronize to revision: " + (stopRevision + 1) + " due illegal state of RecordConsumer."); + } finally { + iterator.close(); + } + + if (stopRevision > 0) { + for (RecordConsumer consumer : consumers.values()) { + consumer.setRevision(stopRevision); + } + log.debug("Synchronized from revision " + startRevision + " to revision: " + stopRevision); + } + } + + /** + * Return a flag indicating whether synchronization should continue + * in a loop until no more new records are found. Subclass overridable. + * + * @return true if synchronization should continue; + * false otherwise + */ + protected boolean syncAgainOnNewRecords() { + return false; + } + + /** + * Lock the journal revision, disallowing changes from other sources until + * {@link #unlock} has been called, and synchronizes to the latest change. + * + * @throws JournalException if an error occurs + */ + public void lockAndSync() throws JournalException { + log.debug("Lock the journal revision and synchronize to the latest change."); + if (internalVersionManager != null) { + VersioningLock.ReadLock lock = + internalVersionManager.acquireReadLock(); + try { + internalLockAndSync(); + } finally { + lock.release(); + } + } else { + internalLockAndSync(); + } + } + + private void internalLockAndSync() throws JournalException { + try { + rwLock.writeLock().acquire(); + } catch (InterruptedException e) { + String msg = "Unable to acquire write lock."; + throw new JournalException(msg, e); + } + + boolean succeeded = false; + + try { + // lock + log.debug("internalLockAndSync.doLock()"); + doLock(); + try { + // and sync + doSync(getMinimalRevision()); + succeeded = true; + } finally { + if (!succeeded) { + log.debug("internalLockAndSync.doUnlock(false)"); + doUnlock(false); + } + } + } finally { + if (!succeeded) { + rwLock.writeLock().release(); + } + } + } + + /** + * Unlock the journal revision. + * + * @param successful flag indicating whether the update process was + * successful + */ + public void unlock(boolean successful) { + log.debug("Unlock the journal revision. Successful: " + successful); + try { + doUnlock(successful); + } finally { + //Should not happen that a RuntimeException will be thrown in subCode, but it's safer + //to release the rwLock in finally block. + rwLock.writeLock().release(); + } + } + + /** + * Lock the journal revision. Subclass responsibility. + * + * @throws JournalException if an error occurs + */ + protected abstract void doLock() throws JournalException; + + /** + * Notification method called by an appended record at creation time. + * May be overridden by subclasses to save some context information + * inside the appended record. + * + * @param record record that was appended + */ + protected void appending(AppendRecord record) { + // nothing to be done here + } + + /** + * Append a record backed by a file. On exit, the new revision must have + * been set inside the appended record. Subclass responsibility. + * + * @param record record to append + * @param in input stream + * @param length number of bytes in input stream + * + * @throws JournalException if an error occurs + */ + protected abstract void append(AppendRecord record, InputStream in, int length) + throws JournalException; + + /** + * Unlock the journal revision. Subclass responsibility. + * + * @param successful flag indicating whether the update process was + * successful + */ + protected abstract void doUnlock(boolean successful); + + /** + * Return this journal's identifier. + * + * @return journal identifier + */ + public String getId() { + return id; + } + + /** + * Return this journal's namespace resolver. + * + * @return namespace resolver + */ + public NamespaceResolver getResolver() { + return resolver; + } + + /** + * Return this journal's NamePathResolver. + * + * @return name and path resolver + */ + public NamePathResolver getNamePathResolver() { + return npResolver; + } + + /** + * Set the repository home. + * + * @param repHome repository home + * @since JR 1.5 + */ + public void setRepositoryHome(File repHome) { + this.repHome = repHome; + } + + /** + * Set the version manager. + */ + public void setInternalVersionManager(InternalVersionManagerImpl internalVersionManager) { + this.internalVersionManager = internalVersionManager; + } + + /** + * Return the repository home. + * + * @return the repository home + * @since JR 1.5 + */ + public File getRepositoryHome() { + return repHome; + } + + /* + * Bean getters and setters. + */ + + /** + * @return the path of the cluster node's local revision file + */ + public String getRevision() { + return revision; + } + + /** + * @param revision the path of the cluster node's local revision file to set + */ + public void setRevision(String revision) { + this.revision = revision; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AbstractRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AbstractRecord.java new file mode 100644 index 00000000000..b904a304987 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AbstractRecord.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.core.journal; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.NamespaceException; + +import org.apache.commons.collections.BidiMap; +import org.apache.commons.collections.bidimap.DualHashBidiMap; +import org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefReader; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.nodetype.QDefinitionBuilderFactory; +import org.apache.jackrabbit.spi.commons.nodetype.compact.CompactNodeTypeDefWriter; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionReader; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionWriter; + +/** + * Base implementation for a record. + */ +public abstract class AbstractRecord implements Record { + + /** + * Indicator for a literal UUID. + */ + private static final byte UUID_LITERAL = 'L'; + + /** + * Indicator for a UUID index. + */ + private static final byte UUID_INDEX = 'I'; + + /** + * Maps NodeId to Integer index. + */ + private final BidiMap nodeIdIndex = new DualHashBidiMap(); + + /** + * Namespace resolver. + */ + protected final NamespaceResolver nsResolver; + + /** + * Name and Path resolver. + */ + protected final NamePathResolver resolver; + + /** + * Create a new instance of this class. + * @param nsResolver the namespace resolver + * @param resolver the name-path resolver + */ + public AbstractRecord(NamespaceResolver nsResolver, NamePathResolver resolver) { + this.nsResolver = nsResolver; + this.resolver = resolver; + } + + /** + * {@inheritDoc} + */ + public void writeQName(Name name) throws JournalException { + try { + writeString(resolver.getJCRName(name)); + } catch (NamespaceException e) { + String msg = "Undeclared prefix error while writing name."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void writePathElement(Path path) throws JournalException { + writeQName(path.getName()); + writeInt(path.getIndex()); + } + + /** + * {@inheritDoc} + */ + public void writePath(Path path) throws JournalException { + try { + writeString(resolver.getJCRPath(path)); + } catch (NamespaceException e) { + String msg = "Undeclared prefix error while writing path."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void writeNodeId(NodeId nodeId) throws JournalException { + if (nodeId == null) { + writeByte(UUID_INDEX); + writeInt(-1); + } else { + int index = getOrCreateIndex(nodeId); + if (index != -1) { + writeByte(UUID_INDEX); + writeInt(index); + } else { + writeByte(UUID_LITERAL); + write(nodeId.getRawBytes()); + } + } + } + + /** + * {@inheritDoc} + */ + public void writePropertyId(PropertyId propertyId) throws JournalException { + writeNodeId(propertyId.getParentId()); + writeQName(propertyId.getName()); + } + + /** + * {@inheritDoc} + */ + public void writeNodeTypeDef(QNodeTypeDefinition ntd) throws JournalException { + try { + StringWriter sw = new StringWriter(); + CompactNodeTypeDefWriter writer = new CompactNodeTypeDefWriter(sw, nsResolver, resolver); + writer.write(ntd); + writer.close(); + + writeString(sw.toString()); + } catch (IOException e) { + String msg = "I/O error while writing node type definition."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void writePrivilegeDef(PrivilegeDefinition privilegeDefinition) throws JournalException { + try { + Map nsMapping = new HashMap(); + String uri = privilegeDefinition.getName().getNamespaceURI(); + nsMapping.put(nsResolver.getPrefix(uri), uri); + for (Name n : privilegeDefinition.getDeclaredAggregateNames()) { + nsMapping.put(nsResolver.getPrefix(n.getNamespaceURI()), n.getNamespaceURI()); + } + + StringWriter sw = new StringWriter(); + PrivilegeDefinitionWriter writer = new PrivilegeDefinitionWriter("text/xml"); + writer.writeDefinitions(sw, new PrivilegeDefinition[] {privilegeDefinition}, nsMapping); + sw.close(); + + writeString(sw.toString()); + + } catch (IOException e) { + String msg = "I/O error while writing privilege definition."; + throw new JournalException(msg, e); + } catch (NamespaceException e) { + String msg = "NamespaceException error while writing privilege definition."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public Name readQName() throws JournalException { + try { + return resolver.getQName(readString()); + } catch (NameException e) { + String msg = "Unknown prefix error while reading name."; + throw new JournalException(msg, e); + } catch (NamespaceException e) { + String msg = "Illegal name error while reading name."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public Path readPathElement() throws JournalException { + try { + Name name = resolver.getQName(readString()); + int index = readInt(); + if (index != 0) { + return PathFactoryImpl.getInstance().create(name, index); + } else { + return PathFactoryImpl.getInstance().create(name); + } + } catch (NameException e) { + String msg = "Unknown prefix error while reading path element."; + throw new JournalException(msg, e); + } catch (NamespaceException e) { + String msg = "Illegal name error while reading path element."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public Path readPath() throws JournalException { + try { + return resolver.getQPath(readString()); + } catch (MalformedPathException e) { + String msg = "Malformed path error while reading path."; + throw new JournalException(msg, e); + } catch (NamespaceException e) { + String msg = "Malformed path error while reading path."; + throw new JournalException(msg, e); + } catch (NameException e) { + String msg = "Malformed path error while reading path."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public NodeId readNodeId() throws JournalException { + byte uuidType = readByte(); + if (uuidType == UUID_INDEX) { + int index = readInt(); + if (index == -1) { + return null; + } else { + return (NodeId) nodeIdIndex.getKey(index); + } + } else if (uuidType == UUID_LITERAL) { + byte[] b = new byte[NodeId.UUID_BYTE_LENGTH]; + readFully(b); + NodeId nodeId = new NodeId(b); + nodeIdIndex.put(nodeId, nodeIdIndex.size()); + return nodeId; + } else { + String msg = "Unknown UUID type found: " + uuidType; + throw new JournalException(msg); + } + } + + /** + * {@inheritDoc} + */ + public PropertyId readPropertyId() throws JournalException { + return new PropertyId(readNodeId(), readQName()); + } + + /** + * {@inheritDoc} + */ + public QNodeTypeDefinition readNodeTypeDef() throws JournalException { + try { + StringReader sr = new StringReader(readString()); + + CompactNodeTypeDefReader reader = + new CompactNodeTypeDefReader( + sr, "(internal)", new NamespaceMapping(nsResolver), + new QDefinitionBuilderFactory()); + + Collection ntds = reader.getNodeTypeDefinitions(); + if (ntds.size() != 1) { + throw new JournalException("Expected one node type definition: got " + ntds.size()); + } + return ntds.iterator().next(); + } catch (ParseException e) { + String msg = "Parse error while reading node type definition."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public PrivilegeDefinition readPrivilegeDef() throws JournalException { + try { + StringReader sr = new StringReader(readString()); + PrivilegeDefinitionReader reader = new PrivilegeDefinitionReader(sr, "text/xml"); + PrivilegeDefinition[] defs = reader.getPrivilegeDefinitions(); + + if (defs.length != 1) { + throw new JournalException("Expected one privilege definition: got " + defs.length); + } + return defs[0]; + + } catch (org.apache.jackrabbit.spi.commons.privilege.ParseException e) { + String msg = "Parse error while reading privilege definition."; + throw new JournalException(msg, e); + } + } + + /** + * Get a NodeId's existing cache index, creating a new entry + * if necessary. + * + * @param nodeId nodeId to lookup + * @return cache index of existing entry or -1 to indicate the entry was added + */ + private int getOrCreateIndex(NodeId nodeId) { + Integer index = (Integer) nodeIdIndex.get(nodeId); + if (index == null) { + nodeIdIndex.put(nodeId, nodeIdIndex.size()); + return -1; + } else { + return index; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AppendRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AppendRecord.java new file mode 100644 index 00000000000..1231ffb57e7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/AppendRecord.java @@ -0,0 +1,421 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.jackrabbit.core.data.db.ResettableTempFileInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default temporary record used for appending to some journal. + */ +public class AppendRecord extends AbstractRecord { + + /** + * Logger. + */ + private static Logger log = LoggerFactory.getLogger(AppendRecord.class); + + /** + * Default prefix for appended records in the file system. + */ + private static final String DEFAULT_PREFIX = "journal"; + + /** + * Default extension for appended records in the file system. + */ + private static final String DEFAULT_EXT = ".tmp"; + + /** + * Default size for in-memory records. + */ + private static final int DEFAULT_IN_MEMORY_SIZE = 1024; + + /** + * Maximum size for in-memory records. + */ + private static final int MAXIMUM_IN_MEMORY_SIZE = 65536; + + /** + * Journal where record is being appended. + */ + private final AbstractJournal journal; + + /** + * Producer identifier. + */ + private final String producerId; + + /** + * This record's revision. + */ + private long revision; + + /** + * Underlying data output. + */ + private DataOutputStream dataOut; + + /** + * Underlying byte output. + */ + private ByteArrayOutputStream byteOut; + + /** + * Underlying file. + */ + private File file; + + /** + * Underlying file output. + */ + private FileOutputStream fileOut; + + /** + * Flag indicating whether the output is closed. + */ + private boolean outputClosed; + + /** + * Create a new instance of this class. + * + * @param journal journal where record is being appended + * @param producerId producer identifier + */ + public AppendRecord(AbstractJournal journal, String producerId) { + super(journal.getResolver(), journal.getNamePathResolver()); + + this.journal = journal; + this.producerId = producerId; + this.revision = 0L; + + byteOut = new ByteArrayOutputStream(DEFAULT_IN_MEMORY_SIZE); + dataOut = new DataOutputStream(byteOut); + } + + /** + * {@inheritDoc} + */ + public String getJournalId() { + return journal.getId(); + } + + /** + * {@inheritDoc} + */ + public String getProducerId() { + return producerId; + } + + /** + * {@inheritDoc} + */ + public long getRevision() { + return revision; + } + + /** + * Set the revision this record represents. + * + * @param revision revision + */ + public void setRevision(long revision) { + this.revision = revision; + } + + /** + * {@inheritDoc} + */ + public void writeByte(int n) throws JournalException { + checkOutput(); + + try { + dataOut.writeByte(n); + } catch (IOException e) { + String msg = "I/O error while writing byte."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void writeChar(char c) throws JournalException { + checkOutput(); + + try { + dataOut.writeChar(c); + } catch (IOException e) { + String msg = "I/O error while writing character."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void writeBoolean(boolean b) throws JournalException { + checkOutput(); + + try { + dataOut.writeBoolean(b); + } catch (IOException e) { + String msg = "I/O error while writing boolean."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void writeInt(int n) throws JournalException { + checkOutput(); + + try { + dataOut.writeInt(n); + } catch (IOException e) { + String msg = "I/O error while writing integer."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void writeLong(long n) throws JournalException { + checkOutput(); + + try { + dataOut.writeLong(n); + } catch (IOException e) { + String msg = "I/O error while writing long."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void writeString(String s) throws JournalException { + checkOutput(); + + try { + if (s == null) { + dataOut.writeBoolean(true); + } else { + dataOut.writeBoolean(false); + dataOut.writeUTF(s); + } + } catch (IOException e) { + String msg = "I/O error while writing string."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void write(byte[] b) throws JournalException { + checkOutput(); + + try { + dataOut.write(b); + } catch (IOException e) { + String msg = "I/O error while writing a byte array."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public long update() throws JournalException { + boolean succeeded = false; + + try { + int length = dataOut.size(); + closeOutput(); + + InputStream in = openInput(); + + try { + journal.append(this, in, length); + succeeded = true; + return length; + } finally { + try { + in.close(); + } catch (IOException e) { + String msg = "I/O error while closing stream."; + log.warn(msg, e); + } + } + } finally { + dispose(); + + journal.unlock(succeeded); + } + } + + /** + * {@inheritDoc} + */ + public void cancelUpdate() { + if (!outputClosed) { + dispose(); + + journal.unlock(false); + } + } + + /** + * Open input on record written. + */ + private InputStream openInput() throws JournalException { + if (file != null) { + try { + return new ResettableTempFileInputStream(file); + } catch (IOException e) { + String msg = "Unable to open file input on: " + file.getPath(); + throw new JournalException(msg, e); + } + } else { + return new ByteArrayInputStream(byteOut.toByteArray()); + } + } + + /** + * Check output size and eventually switch to file output. + * + * @throws JournalException + */ + private void checkOutput() throws JournalException { + if (outputClosed) { + throw new IllegalStateException("Output closed."); + } + if (fileOut == null && byteOut.size() >= MAXIMUM_IN_MEMORY_SIZE) { + try { + file = File.createTempFile(DEFAULT_PREFIX, DEFAULT_EXT); + } catch (IOException e) { + String msg = "Unable to create temporary file."; + throw new JournalException(msg, e); + } + try { + fileOut = new FileOutputStream(file); + } catch (FileNotFoundException e) { + String msg = "Unable to open output stream on: " + file.getPath(); + throw new JournalException(msg, e); + } + dataOut = new DataOutputStream(new BufferedOutputStream(fileOut)); + + try { + dataOut.write(byteOut.toByteArray()); + } catch (IOException e) { + String msg = "Unable to write in-memory record to file."; + throw new JournalException(msg, e); + } + } + } + + /** + * Close output, keeping the underlying file. + * + * @throws JournalException if an error occurs + */ + private void closeOutput() throws JournalException { + if (!outputClosed) { + try { + if (fileOut != null) { + dataOut.flush(); + fileOut.getFD().sync(); + dataOut.close(); + } + } catch (IOException e) { + String msg = "I/O error while closing stream."; + throw new JournalException(msg, e); + } finally { + outputClosed = true; + } + } + } + + /** + * Dispose this record, deleting the underlying file. + */ + private void dispose() { + if (!outputClosed) { + try { + dataOut.close(); + } catch (IOException e) { + String msg = "I/O error while closing stream."; + log.warn(msg, e); + } finally { + outputClosed = true; + } + } + if (file != null) { + file.delete(); + file = null; + } + } + + /** + * Unsupported methods when appending. + */ + public byte readByte() throws JournalException { + throw unsupported(); + } + + public char readChar() throws JournalException { + throw unsupported(); + } + + public boolean readBoolean() throws JournalException { + throw unsupported(); + } + + public int readInt() throws JournalException { + throw unsupported(); + } + + public long readLong() throws JournalException { + throw unsupported(); + } + + public String readString() throws JournalException { + throw unsupported(); + } + + public void readFully(byte[] b) throws JournalException { + throw unsupported(); + } + + private JournalException unsupported() { + String msg = "Reading from an appended record is not supported."; + return new JournalException(msg); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DatabaseJournal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DatabaseJournal.java new file mode 100644 index 00000000000..786c92c1e60 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DatabaseJournal.java @@ -0,0 +1,900 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DatabaseAware; +import org.apache.jackrabbit.core.util.db.DbUtility; +import org.apache.jackrabbit.core.util.db.StreamWrapper; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Calendar; + +import javax.jcr.RepositoryException; +import javax.sql.DataSource; + +/** + * Database-based journal implementation. Stores records inside a database table named + * JOURNAL, whereas the table GLOBAL_REVISION contains the + * highest available revision number. These tables are located inside the schema specified + * in schemaObjectPrefix. + *

    + * It is configured through the following properties: + *

      + *
    • driver: the JDBC driver class name to use; this is a required + * property with no default value
    • + *
    • url: the JDBC connection url; this is a required property with + * no default value
    • + *
    • databaseType: the database type to be used; if not specified, this is the + * second field inside the JDBC connection url, delimited by colons
    • + *
    • schemaObjectPrefix: the schema object prefix to be used; + * defaults to an empty string
    • + *
    • user: username to specify when connecting
    • + *
    • password: password to specify when connecting
    • + *
    • reconnectDelayMs: number of milliseconds to wait before + * trying to reconnect to the database.
    • + *
    • janitorEnabled: specifies whether the clean-up thread for the + * journal table is enabled (default = false)
    • + *
    • janitorSleep: specifies the sleep time of the clean-up thread + * in seconds (only useful when the clean-up thread is enabled, default = 24 * 60 * 60, + * which equals 24 hours)
    • + *
    • janitorFirstRunHourOfDay: specifies the hour at which the clean-up + * thread initiates its first run (default = 3 which means 3:00 at night)
    • + *
    • schemaCheckEnabled: whether the schema check during initialization is enabled + * (default = true)
    • + *
    + *

    + * JNDI can be used to get the connection. In this case, use the javax.naming.InitialContext as the driver, + * and the JNDI name as the URL. If the user and password are configured in the JNDI resource, + * they should not be configured here. Example JNDI settings: + *

    + * <param name="driver" value="javax.naming.InitialContext" />
    + * <param name="url" value="java:comp/env/jdbc/Test" />
    + * 
    + */ +public class DatabaseJournal extends AbstractJournal implements DatabaseAware { + + /** + * Default journal table name, used to check schema completeness. + */ + private static final String DEFAULT_JOURNAL_TABLE = "JOURNAL"; + + /** + * Local revisions table name, used to check schema completeness. + */ + private static final String LOCAL_REVISIONS_TABLE = "LOCAL_REVISIONS"; + + /** + * Logger. + */ + static Logger log = LoggerFactory.getLogger(DatabaseJournal.class); + + /** + * Driver name, bean property. + */ + private String driver; + + /** + * Connection URL, bean property. + */ + private String url; + + /** + * Database type, bean property. + */ + private String databaseType; + + /** + * User name, bean property. + */ + private String user; + + /** + * Password, bean property. + */ + private String password; + + /** + * DataSource logical name, bean property. + */ + private String dataSourceName; + + /** + * The connection helper + */ + ConnectionHelper conHelper; + + /** + * Auto commit level. + */ + private int lockLevel; + + /** + * Locked revision. + */ + private long lockedRevision; + + /** + * Whether the revision table janitor thread is enabled. + */ + private boolean janitorEnabled = false; + + /** + * The sleep time of the revision table janitor in seconds, 1 day default. + */ + int janitorSleep = 60 * 60 * 24; + + /** + * Indicates when the next run of the janitor is scheduled. + * The first run is scheduled by default at 03:00 hours. + */ + Calendar janitorNextRun = Calendar.getInstance(); + + { + if (janitorNextRun.get(Calendar.HOUR_OF_DAY) >= 3) { + janitorNextRun.add(Calendar.DAY_OF_MONTH, 1); + } + janitorNextRun.set(Calendar.HOUR_OF_DAY, 3); + janitorNextRun.set(Calendar.MINUTE, 0); + janitorNextRun.set(Calendar.SECOND, 0); + janitorNextRun.set(Calendar.MILLISECOND, 0); + } + + private Thread janitorThread; + + /** + * Whether the schema check must be done during initialization. + */ + private boolean schemaCheckEnabled = true; + + /** + * The instance that manages the local revision. + */ + private DatabaseRevision databaseRevision; + + /** + * SQL statement returning all revisions within a range. + */ + protected String selectRevisionsStmtSQL; + + /** + * SQL statement updating the global revision. + */ + protected String updateGlobalStmtSQL; + + /** + * SQL statement returning the global revision. + */ + protected String selectGlobalStmtSQL; + + /** + * SQL statement appending a new record. + */ + protected String insertRevisionStmtSQL; + + /** + * SQL statement returning the minimum of the local revisions. + */ + protected String selectMinLocalRevisionStmtSQL; + + /** + * SQL statement removing a set of revisions with from the journal table. + */ + protected String cleanRevisionStmtSQL; + + /** + * SQL statement returning the local revision of this cluster node. + */ + protected String getLocalRevisionStmtSQL; + + /** + * SQL statement for inserting the local revision of this cluster node. + */ + protected String insertLocalRevisionStmtSQL; + + /** + * SQL statement for updating the local revision of this cluster node. + */ + protected String updateLocalRevisionStmtSQL; + + /** + * Schema object prefix, bean property. + */ + protected String schemaObjectPrefix; + + /** + * The repositories {@link ConnectionFactory}. + */ + private ConnectionFactory connectionFactory; + + public DatabaseJournal() { + databaseType = "default"; + schemaObjectPrefix = ""; + } + + /** + * {@inheritDoc} + */ + public void setConnectionFactory(ConnectionFactory connnectionFactory) { + this.connectionFactory = connnectionFactory; + } + + /** + * {@inheritDoc} + */ + public void init(String id, NamespaceResolver resolver) + throws JournalException { + + super.init(id, resolver); + + init(); + + try { + conHelper = createConnectionHelper(getDataSource()); + + // make sure schemaObjectPrefix consists of legal name characters only + schemaObjectPrefix = conHelper.prepareDbIdentifier(schemaObjectPrefix); + + // check if schema objects exist and create them if necessary + if (isSchemaCheckEnabled()) { + createCheckSchemaOperation().run(); + } + + // Make sure that the LOCAL_REVISIONS table exists (see JCR-1087) + if (isSchemaCheckEnabled()) { + checkLocalRevisionSchema(); + } + + buildSQLStatements(); + initInstanceRevisionAndJanitor(); + } catch (Exception e) { + String msg = "Unable to create connection."; + throw new JournalException(msg, e); + } + log.info("DatabaseJournal initialized."); + } + + private DataSource getDataSource() throws Exception { + if (getDataSourceName() == null || "".equals(getDataSourceName())) { + return connectionFactory.getDataSource(getDriver(), getUrl(), getUser(), getPassword()); + } else { + return connectionFactory.getDataSource(dataSourceName); + } + } + + /** + * This method is called from the {@link #init(String, NamespaceResolver)} method of this class and + * returns a {@link ConnectionHelper} instance which is assigned to the {@code conHelper} field. + * Subclasses may override it to return a specialized connection helper. + * + * @param dataSrc the {@link DataSource} of this persistence manager + * @return a {@link ConnectionHelper} + * @throws Exception on error + */ + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + return new ConnectionHelper(dataSrc, false); + } + + /** + * This method is called from {@link #init(String, NamespaceResolver)} after the + * {@link #createConnectionHelper(DataSource)} method, and returns a default {@link CheckSchemaOperation}. + * Subclasses can override this implementation to get a customized implementation. + * + * @return a new {@link CheckSchemaOperation} instance + */ + protected CheckSchemaOperation createCheckSchemaOperation() { + InputStream in = DatabaseJournal.class.getResourceAsStream(databaseType + ".ddl"); + return new CheckSchemaOperation(conHelper, in, schemaObjectPrefix + DEFAULT_JOURNAL_TABLE).addVariableReplacement( + CheckSchemaOperation.SCHEMA_OBJECT_PREFIX_VARIABLE, schemaObjectPrefix); + } + + /** + * Completes initialization of this database journal. Base implementation + * checks whether the required bean properties driver and + * url have been specified and optionally deduces a valid + * database type. Should be overridden by subclasses that use a different way to + * create a connection and therefore require other arguments. + * + * @throws JournalException if initialization fails + */ + protected void init() throws JournalException { + if (driver == null && dataSourceName == null) { + String msg = "Driver not specified."; + throw new JournalException(msg); + } + if (url == null && dataSourceName == null) { + String msg = "Connection URL not specified."; + throw new JournalException(msg); + } + if (dataSourceName != null) { + try { + String configuredDatabaseType = connectionFactory.getDataBaseType(dataSourceName); + if (DatabaseJournal.class.getResourceAsStream(configuredDatabaseType + ".ddl") != null) { + setDatabaseType(configuredDatabaseType); + } + } catch (RepositoryException e) { + throw new JournalException("failed to get database type", e); + } + } + if (databaseType == null) { + try { + databaseType = getDatabaseTypeFromURL(url); + } catch (IllegalArgumentException e) { + String msg = "Unable to derive database type from URL: " + e.getMessage(); + throw new JournalException(msg); + } + } + } + + /** + * Initialize the instance revision manager and the janitor thread. + * + * @throws JournalException on error + */ + protected void initInstanceRevisionAndJanitor() throws Exception { + databaseRevision = new DatabaseRevision(); + + // Get the local file revision from disk (upgrade; see JCR-1087) + long localFileRevision = 0L; + if (getRevision() != null) { + InstanceRevision currentFileRevision = new FileRevision(new File(getRevision()), true); + localFileRevision = currentFileRevision.get(); + currentFileRevision.close(); + } + + // Now write the localFileRevision (or 0 if it does not exist) to the LOCAL_REVISIONS + // table, but only if the LOCAL_REVISIONS table has no entry yet for this cluster node + long localRevision = databaseRevision.init(localFileRevision); + log.info("Initialized local revision to " + localRevision); + + // Start the clean-up thread if necessary. + if (janitorEnabled) { + janitorThread = new Thread(new RevisionTableJanitor(), "Jackrabbit-ClusterRevisionJanitor"); + janitorThread.setDaemon(true); + janitorThread.start(); + log.info("Cluster revision janitor thread started; first run scheduled at " + janitorNextRun.getTime()); + } else { + log.info("Cluster revision janitor thread not started"); + } + } + + /* (non-Javadoc) + * @see org.apache.jackrabbit.core.journal.Journal#getInstanceRevision() + */ + public InstanceRevision getInstanceRevision() throws JournalException { + return databaseRevision; + } + + /** + * Derive a database type from a JDBC connection URL. This simply treats the given URL + * as delimeted by colons and takes the 2nd field. + * + * @param url JDBC connection URL + * @return the database type + * @throws IllegalArgumentException if the JDBC connection URL is invalid + */ + private static String getDatabaseTypeFromURL(String url) throws IllegalArgumentException { + int start = url.indexOf(':'); + if (start != -1) { + int end = url.indexOf(':', start + 1); + if (end != -1) { + return url.substring(start + 1, end); + } + } + throw new IllegalArgumentException(url); + } + + /** + * {@inheritDoc} + */ + public RecordIterator getRecords(long startRevision) throws JournalException { + try { + return new DatabaseRecordIterator(conHelper.exec(selectRevisionsStmtSQL, new Object[]{new Long( + startRevision)}, false, 0), getResolver(), getNamePathResolver()); + } catch (SQLException e) { + throw new JournalException("Unable to return record iterator.", e); + } + } + + /** + * {@inheritDoc} + */ + public RecordIterator getRecords() throws JournalException { + try { + return new DatabaseRecordIterator(conHelper.exec(selectRevisionsStmtSQL, new Object[]{new Long( + Long.MIN_VALUE)}, false, 0), getResolver(), getNamePathResolver()); + } catch (SQLException e) { + throw new JournalException("Unable to return record iterator.", e); + } + } + + /** + * Synchronize contents from journal. May be overridden by subclasses. + * Do the initial sync in batchMode, since some databases (PSQL) when + * not in transactional mode, load all results in memory which causes + * out of memory. See JCR-2832 + * + * @param startRevision start point (exclusive) + * @param startup indicates if the cluster node is syncing on startup + * or does a normal sync. + * @throws JournalException if an error occurs + */ + @Override + protected void doSync(long startRevision, boolean startup) throws JournalException { + if (!startup) { + // if the cluster node is not starting do a normal sync + doSync(startRevision); + } else { + try { + startBatch(); + try { + doSync(startRevision); + } finally { + endBatch(true); + } + } catch (SQLException e) { + throw new JournalException("Couldn't sync the cluster node", e); + } + } + } + + /** + * {@inheritDoc} + *

    + * This journal is locked by incrementing the current value in the table + * named GLOBAL_REVISION, which effectively write-locks this + * table. The updated value is then saved away and remembered in the + * appended record, because a save may entail multiple appends (JCR-884). + */ + protected void doLock() throws JournalException { + ResultSet rs = null; + boolean succeeded = false; + + try { + startBatch(); + } catch (SQLException e) { + throw new JournalException("Unable to set autocommit to false.", e); + } + + try { + conHelper.exec(updateGlobalStmtSQL); + rs = conHelper.exec(selectGlobalStmtSQL, null, false, 0); + if (!rs.next()) { + throw new JournalException("No revision available."); + } + lockedRevision = rs.getLong(1); + succeeded = true; + } catch (SQLException e) { + throw new JournalException("Unable to lock global revision table.", e); + } finally { + DbUtility.close(rs); + if (!succeeded) { + log.debug("doLock.doUnlock(false)"); + doUnlock(false); + } + } + } + + /** + * {@inheritDoc} + */ + protected void doUnlock(boolean successful) { + endBatch(successful); + } + + private void startBatch() throws SQLException { + if (lockLevel++ == 0) { + conHelper.startBatch(); + } + } + + private void endBatch(boolean successful) { + if (--lockLevel == 0) { + try { + conHelper.endBatch(successful); + } catch (SQLException e) { + log.error("failed to end batch", e); + } + } + } + + /** + * {@inheritDoc} + *

    + * Save away the locked revision inside the newly appended record. + */ + protected void appending(AppendRecord record) { + record.setRevision(lockedRevision); + } + + /** + * {@inheritDoc} + *

    + * We have already saved away the revision for this record. + */ + protected void append(AppendRecord record, InputStream in, int length) + throws JournalException { + + try { + conHelper.exec(insertRevisionStmtSQL, record.getRevision(), getId(), record.getProducerId(), + new StreamWrapper(in, length)); + + } catch (SQLException e) { + String msg = "Unable to append revision " + lockedRevision + "."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void close() { + if (janitorThread != null) { + janitorThread.interrupt(); + } + } + + /** + * Checks if the local revision schema objects exist and creates them if they + * don't exist yet. + * + * @throws Exception if an error occurs + */ + private void checkLocalRevisionSchema() throws Exception { + InputStream localRevisionDDLStream = null; + InputStream in = DatabaseJournal.class.getResourceAsStream(databaseType + ".ddl"); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String sql = reader.readLine(); + while (sql != null) { + // Skip comments and empty lines, and select only the statement to create the LOCAL_REVISIONS + // table. + if (!sql.startsWith("#") && sql.length() > 0 && sql.indexOf(LOCAL_REVISIONS_TABLE) != -1) { + localRevisionDDLStream = new ByteArrayInputStream(sql.getBytes()); + break; + } + // read next sql stmt + sql = reader.readLine(); + } + } finally { + IOUtils.closeQuietly(in); + } + // Run the schema check for the single table + new CheckSchemaOperation(conHelper, localRevisionDDLStream, schemaObjectPrefix + + LOCAL_REVISIONS_TABLE).addVariableReplacement( + CheckSchemaOperation.SCHEMA_OBJECT_PREFIX_VARIABLE, schemaObjectPrefix).run(); + } + + /** + * Builds the SQL statements. May be overridden by subclasses to allow + * different table and/or column names. + */ + protected void buildSQLStatements() { + selectRevisionsStmtSQL = + "select REVISION_ID, JOURNAL_ID, PRODUCER_ID, REVISION_DATA from " + + schemaObjectPrefix + "JOURNAL where REVISION_ID > ? order by REVISION_ID"; + updateGlobalStmtSQL = + "update " + schemaObjectPrefix + "GLOBAL_REVISION" + + " set REVISION_ID = REVISION_ID + 1"; + selectGlobalStmtSQL = + "select REVISION_ID from " + + schemaObjectPrefix + "GLOBAL_REVISION"; + insertRevisionStmtSQL = + "insert into " + schemaObjectPrefix + "JOURNAL" + + " (REVISION_ID, JOURNAL_ID, PRODUCER_ID, REVISION_DATA) " + + "values (?,?,?,?)"; + selectMinLocalRevisionStmtSQL = + "select MIN(REVISION_ID) from " + schemaObjectPrefix + "LOCAL_REVISIONS"; + cleanRevisionStmtSQL = + "delete from " + schemaObjectPrefix + "JOURNAL " + "where REVISION_ID < ?"; + getLocalRevisionStmtSQL = + "select REVISION_ID from " + schemaObjectPrefix + "LOCAL_REVISIONS " + + "where JOURNAL_ID = ?"; + insertLocalRevisionStmtSQL = + "insert into " + schemaObjectPrefix + "LOCAL_REVISIONS " + + "(REVISION_ID, JOURNAL_ID) values (?,?)"; + updateLocalRevisionStmtSQL = + "update " + schemaObjectPrefix + "LOCAL_REVISIONS " + + "set REVISION_ID = ? where JOURNAL_ID = ?"; + } + + /** + * Bean getters + */ + public String getDriver() { + return driver; + } + + public String getUrl() { + return url; + } + + /** + * Get the database type. + * + * @return the database type + */ + public String getDatabaseType() { + return databaseType; + } + + /** + * Get the database type. + * @deprecated + * This method is deprecated; {@link #getDatabaseType} should be used instead. + * + * @return the database type + */ + public String getSchema() { + return databaseType; + } + + public String getSchemaObjectPrefix() { + return schemaObjectPrefix; + } + + public String getUser() { + return user; + } + + public String getPassword() { + return password; + } + + public boolean getJanitorEnabled() { + return janitorEnabled; + } + + public int getJanitorSleep() { + return janitorSleep; + } + + public int getJanitorFirstRunHourOfDay() { + return janitorNextRun.get(Calendar.HOUR_OF_DAY); + } + + /** + * Bean setters + */ + public void setDriver(String driver) { + this.driver = driver; + } + + public void setUrl(String url) { + this.url = url; + } + + /** + * Set the database type. + * + * @param databaseType the database type + */ + public void setDatabaseType(String databaseType) { + this.databaseType = databaseType; + } + + /** + * Set the database type. + * @deprecated + * This method is deprecated; {@link #getDatabaseType} should be used instead. + * + * @param databaseType the database type + */ + public void setSchema(String databaseType) { + this.databaseType = databaseType; + } + + public void setSchemaObjectPrefix(String schemaObjectPrefix) { + this.schemaObjectPrefix = schemaObjectPrefix.toUpperCase(); + } + + public void setUser(String user) { + this.user = user; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setJanitorEnabled(boolean enabled) { + this.janitorEnabled = enabled; + } + + public void setJanitorSleep(int sleep) { + this.janitorSleep = sleep; + } + + public void setJanitorFirstRunHourOfDay(int hourOfDay) { + janitorNextRun = Calendar.getInstance(); + if (janitorNextRun.get(Calendar.HOUR_OF_DAY) >= hourOfDay) { + janitorNextRun.add(Calendar.DAY_OF_MONTH, 1); + } + janitorNextRun.set(Calendar.HOUR_OF_DAY, hourOfDay); + janitorNextRun.set(Calendar.MINUTE, 0); + janitorNextRun.set(Calendar.SECOND, 0); + janitorNextRun.set(Calendar.MILLISECOND, 0); + } + + public String getDataSourceName() { + return dataSourceName; + } + + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + /** + * @return whether the schema check is enabled + */ + public final boolean isSchemaCheckEnabled() { + return schemaCheckEnabled; + } + + /** + * @param enabled set whether the schema check is enabled + */ + public final void setSchemaCheckEnabled(boolean enabled) { + schemaCheckEnabled = enabled; + } + + /** + * This class manages the local revision of the cluster node. It + * persists the local revision in the LOCAL_REVISIONS table in the + * clustering database. + */ + public class DatabaseRevision implements InstanceRevision { + + /** + * The cached local revision of this cluster node. + */ + private long localRevision; + + /** + * Indicates whether the init method has been called. + */ + private boolean initialized = false; + + /** + * Checks whether there's a local revision value in the database for this + * cluster node. If not, it writes the given default revision to the database. + * + * @param revision the default value for the local revision counter + * @return the local revision + * @throws JournalException on error + */ + protected synchronized long init(long revision) throws JournalException { + ResultSet rs = null; + try { + // Check whether there is an entry in the database. + rs = conHelper.exec(getLocalRevisionStmtSQL, new Object[]{getId()}, false, 0); + boolean exists = rs.next(); + if (exists) { + revision = rs.getLong(1); + } + + // Insert the given revision in the database + if (!exists) { + conHelper.exec(insertLocalRevisionStmtSQL, revision, getId()); + } + + // Set the cached local revision and return + localRevision = revision; + initialized = true; + return revision; + + } catch (SQLException e) { + log.warn("Failed to initialize local revision.", e); + throw new JournalException("Failed to initialize local revision", e); + } finally { + DbUtility.close(rs); + } + } + + public synchronized long get() { + if (!initialized) { + throw new IllegalStateException("instance has not yet been initialized"); + } + return localRevision; + } + + public synchronized void set(long localRevision) throws JournalException { + + if (!initialized) { + throw new IllegalStateException("instance has not yet been initialized"); + } + + // Update the cached value and the table with local revisions. + try { + conHelper.exec(updateLocalRevisionStmtSQL, localRevision, getId()); + this.localRevision = localRevision; + } catch (SQLException e) { + log.warn("Failed to update local revision.", e); + throw new JournalException("Failed to update local revision.", e); + } + } + + public void close() { + // nothing to do + } + } + + /** + * Class for maintaining the revision table. This is only useful if all + * JR information except the search index is in the database (i.e., node types + * etc). In that case, revision data can safely be thrown away from the JOURNAL table. + */ + public class RevisionTableJanitor implements Runnable { + + /** + * {@inheritDoc} + */ + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + log.info("Next clean-up run scheduled at " + janitorNextRun.getTime()); + long sleepTime = janitorNextRun.getTimeInMillis() - System.currentTimeMillis(); + if (sleepTime > 0) { + Thread.sleep(sleepTime); + } + cleanUpOldRevisions(); + janitorNextRun.add(Calendar.SECOND, janitorSleep); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + log.info("Interrupted: stopping clean-up task."); + } + + /** + * Cleans old revisions from the clustering table. + */ + protected void cleanUpOldRevisions() { + ResultSet rs = null; + try { + long minRevision = 0; + rs = conHelper.exec(selectMinLocalRevisionStmtSQL, null, false, 0); + boolean cleanUp = rs.next(); + if (cleanUp) { + minRevision = rs.getLong(1); + } + + // Clean up if necessary: + if (cleanUp) { + conHelper.exec(cleanRevisionStmtSQL, minRevision); + log.info("Cleaned old revisions up to revision " + minRevision + "."); + } + + } catch (Exception e) { + log.warn("Failed to clean up old revisions.", e); + } finally { + DbUtility.close(rs); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DatabaseRecordIterator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DatabaseRecordIterator.java new file mode 100644 index 00000000000..cae0f09a6f8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DatabaseRecordIterator.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.core.journal; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.DataInputStream; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.NoSuchElementException; + +/** + * RecordIterator interface. + */ +class DatabaseRecordIterator implements RecordIterator { + + /** + * Logger. + */ + private static Logger log = LoggerFactory.getLogger(DatabaseRecordIterator.class); + + /** + * Underlying result set. + */ + private final ResultSet rs; + + /** + * Namespace resolver. + */ + private final NamespaceResolver resolver; + + /** + * Name and Path resolver. + */ + private final NamePathResolver npResolver; + + /** + * Current record. + */ + private ReadRecord record; + + /** + * Last record returned. + */ + private ReadRecord lastRecord; + + /** + * Flag indicating whether EOF was reached. + */ + private boolean isEOF; + + /** + * Create a new instance of this class. + */ + public DatabaseRecordIterator(ResultSet rs, NamespaceResolver resolver, NamePathResolver npResolver) { + this.rs = rs; + this.resolver = resolver; + this.npResolver = npResolver; + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + try { + if (!isEOF && record == null) { + fetchRecord(); + } + return !isEOF; + } catch (SQLException e) { + String msg = "Error while moving to next record."; + log.error(msg, e); + return false; + } + } + + /** + * Return the next record. If there are no more records, throws + * a NoSuchElementException. If an error occurs, + * throws a JournalException. + * + * @return next record + * @throws java.util.NoSuchElementException if there are no more records + * @throws JournalException if another error occurs + */ + public Record nextRecord() throws NoSuchElementException, JournalException { + if (!hasNext()) { + String msg = "No current record."; + throw new NoSuchElementException(msg); + } + close(lastRecord); + lastRecord = record; + record = null; + + return lastRecord; + } + + /** + * {@inheritDoc} + */ + public void close() { + if (lastRecord != null) { + close(lastRecord); + lastRecord = null; + } + try { + rs.close(); + } catch (SQLException e) { + String msg = "Error while closing result set: " + e.getMessage(); + log.warn(msg); + } + } + + /** + * Fetch the next record. + */ + private void fetchRecord() throws SQLException { + if (rs.next()) { + long revision = rs.getLong(1); + String journalId = rs.getString(2); + String producerId = rs.getString(3); + DataInputStream dataIn = new DataInputStream(rs.getBinaryStream(4)); + record = new ReadRecord(journalId, producerId, revision, dataIn, 0, resolver, npResolver); + } else { + isEOF = true; + } + } + + /** + * Close a record. + * + * @param record record + */ + private static void close(ReadRecord record) { + if (record != null) { + try { + record.close(); + } catch (IOException e) { + String msg = "Error while closing record."; + log.warn(msg, e); + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DefaultRecordProducer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DefaultRecordProducer.java new file mode 100644 index 00000000000..b617d934900 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/DefaultRecordProducer.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.core.journal; + +/** + * Produces new records that can be appended to the journal. + */ +public class DefaultRecordProducer implements RecordProducer { + + /** + * Journal. + */ + private final AbstractJournal journal; + + /** + * Producer identifier. + */ + private String id; + + /** + * Create a new instance of this class. + * + * @param journal journal + * @param id producer id + */ + public DefaultRecordProducer(AbstractJournal journal, String id) { + this.journal = journal; + this.id = id; + } + + /** + * {@inheritDoc} + */ + public Record append() throws JournalException { + AppendRecord record = null; + + journal.lockAndSync(); + + try { + record = createRecord(); + journal.appending(record); + return record; + } finally { + if (record == null) { + journal.unlock(false); + } + } + } + + /** + * Create a new record. May be overridden by subclasses. + * + * @throws JournalException if an error occurs + */ + protected AppendRecord createRecord() throws JournalException { + return new AppendRecord(journal, id); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileJournal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileJournal.java new file mode 100644 index 00000000000..ab4f06e35b7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileJournal.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * File-based journal implementation that appends journal records to a single + * file. + *

    + * It is configured through the following properties: + *

      + *
    • revision: the filename where the parent cluster node's revision + * file should be written to; this is a required property with no default value
    • + *
    • directory: the directory where to keep the journal file as + * well as the rotated files; this is a required property with no default value
    • + *
    • basename: the basename of journal files; the default + * value is {@link #DEFAULT_BASENAME}
    • + *
    • maximumSize: the maximum size of an active journal file + * before rotating it: the default value is {@link #DEFAULT_MAXSIZE}
    • + *
    + */ +public class FileJournal extends AbstractJournal { + + /** + * Default instance revision file name. + */ + public static final String DEFAULT_INSTANCE_FILE_NAME = "revision.log"; + + /** + * Global revision counter name, located in the journal directory. + */ + private static final String REVISION_NAME = "revision"; + + /** + * Log extension. + */ + private static final String LOG_EXTENSION = "log"; + + /** + * Default base name for journal files. + */ + private static final String DEFAULT_BASENAME = "journal"; + + /** + * Default max size of a journal file (1MB). + */ + private static final int DEFAULT_MAXSIZE = 1048576; + + /** + * Logger. + */ + private static Logger log = LoggerFactory.getLogger(FileJournal.class); + + /** + * Directory name, bean property. + */ + private String directory; + + /** + * Journal file base name, bean property. + */ + private String basename; + + /** + * Maximum size of a journal file before a rotation takes place, bean property. + */ + private int maximumSize; + + /** + * Journal root directory. + */ + private File rootDirectory; + + /** + * Journal file. + */ + private File journalFile; + + /** + * Global revision counter. + */ + private LockableFileRevision globalRevision; + + /** + * {@inheritDoc} + */ + public void init(String id, NamespaceResolver resolver) throws JournalException { + super.init(id, resolver); + + if (getRevision() == null) { + File repHome = getRepositoryHome(); + if (repHome == null) { + String msg = "Revision not specified."; + throw new JournalException(msg); + } + String revision = new File(repHome, DEFAULT_INSTANCE_FILE_NAME).getPath(); + log.info("Revision not specified, using: " + revision); + setRevision(revision); + } + if (directory == null) { + String msg = "Directory not specified."; + throw new JournalException(msg); + } + if (basename == null) { + basename = DEFAULT_BASENAME; + } + if (maximumSize == 0) { + maximumSize = DEFAULT_MAXSIZE; + } + rootDirectory = new File(directory); + + // JCR-1341: Cluster Journal directory should be created automatically + rootDirectory.mkdirs(); + + if (!rootDirectory.exists() || !rootDirectory.isDirectory()) { + String msg = "Directory specified does either not exist " + + "or is not a directory: " + directory; + throw new JournalException(msg); + } + + journalFile = new File(rootDirectory, basename + "." + LOG_EXTENSION); + globalRevision = new LockableFileRevision(new File(rootDirectory, REVISION_NAME)); + + log.info("FileJournal initialized at path: " + directory); + } + + /** + * {@inheritDoc} + */ + protected long getGlobalRevision() throws JournalException { + return globalRevision.get(); + } + + /** + * {@inheritDoc} + */ + public RecordIterator getRecords(long startRevision) + throws JournalException { + + long stopRevision = getGlobalRevision(); + + File[] files = null; + if (startRevision < stopRevision) { + RotatingLogFile[] logFiles = RotatingLogFile.listFiles(rootDirectory, basename); + files = new File[logFiles.length]; + for (int i = 0; i < files.length; i++) { + files[i] = logFiles[i].getFile(); + } + } + return new FileRecordIterator(files, startRevision, stopRevision, + getResolver(), getNamePathResolver()); + } + + /** + * {@inheritDoc} + */ + public RecordIterator getRecords() throws JournalException { + long stopRevision = getGlobalRevision(); + long startRevision = 0; + + RotatingLogFile[] logFiles = RotatingLogFile.listFiles(rootDirectory, basename); + File[] files = new File[logFiles.length]; + for (int i = 0; i < files.length; i++) { + files[i] = logFiles[i].getFile(); + if (i == 0) { + try { + FileRecordLog log = new FileRecordLog(files[i]); + startRevision = log.getPreviousRevision(); + } catch (IOException e) { + String msg = "Unable to read startRevision from first " + + "record log file"; + throw new JournalException(msg, e); + } + } + } + return new FileRecordIterator(files, startRevision, stopRevision, + getResolver(), getNamePathResolver()); + } + + /** + * {@inheritDoc} + */ + protected void doLock() throws JournalException { + globalRevision.lock(false); + } + + /** + * {@inheritDoc} + */ + protected void append(AppendRecord record, InputStream in, int length) + throws JournalException { + + try { + FileRecordLog recordLog = new FileRecordLog(journalFile); + if (recordLog.exceeds(maximumSize)) { + rotateLogs(); + recordLog = new FileRecordLog(journalFile); + } + if (recordLog.isNew()) { + recordLog.init(globalRevision.get()); + } + long revision = recordLog.append(getId(), + record.getProducerId(), in, length); + globalRevision.set(revision); + record.setRevision(revision); + + } catch (IOException e) { + String msg = "Unable to append new record to journal '" + journalFile + "'."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void doUnlock(boolean successful) { + globalRevision.unlock(); + } + + /** + * {@inheritDoc} + */ + public void close() { + } + + /** + * {@inheritDoc} + */ + public InstanceRevision getInstanceRevision() throws JournalException { + return new FileRevision(new File(getRevision()), true); + } + + /** + * Bean getters + */ + public String getDirectory() { + return directory; + } + + public String getBasename() { + return basename; + } + + public int getMaximumSize() { + return maximumSize; + } + + /** + * Bean setters + */ + public void setDirectory(String directory) { + this.directory = directory; + } + + public void setBasename(String basename) { + this.basename = basename; + } + + public void setMaximumSize(int maximumSize) { + this.maximumSize = maximumSize; + } + + /** + * Move away current journal file (and all other files), incrementing their + * version counter. A file named journal.N.log gets renamed to + * journal.(N+1).log, whereas the main journal file gets renamed + * to journal.1.log. + */ + private void rotateLogs() { + RotatingLogFile[] logFiles = RotatingLogFile.listFiles(rootDirectory, basename); + for (int i = 0; i < logFiles.length; i++) { + logFiles[i].rotate(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRecordIterator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRecordIterator.java new file mode 100644 index 00000000000..12d34636833 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRecordIterator.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import java.io.File; +import java.io.IOException; +import java.util.NoSuchElementException; + +/** + * Record cursor that returns unseen revisions in ascending order on every + * iteration. + */ +public class FileRecordIterator implements RecordIterator { + + /** + * Log files to scan for revisions. + */ + private File[] logFiles; + + /** + * Current revision being visited. + */ + private long revision; + + /** + * Last revision to visit. + */ + private long stopRevision; + + /** + * Namespace resolver. + */ + private NamespaceResolver resolver; + + /** + * Name and Path resolver. + */ + private NamePathResolver npResolver; + + /** + * Current record log, containing file records. + */ + private FileRecordLog recordLog; + + /** + * Current record. + */ + private ReadRecord record; + + /** + * Creates a new instance of this class. + * + * @param logFiles available log files, sorted ascending by age + * @param startRevision start point (exclusive) + * @param stopRevision stop point (inclusive) + */ + public FileRecordIterator(File[] logFiles, long startRevision, long stopRevision, + NamespaceResolver resolver, NamePathResolver npResolver) { + this.logFiles = logFiles; + this.revision = startRevision; + this.stopRevision = stopRevision; + this.resolver = resolver; + this.npResolver = npResolver; + } + + + /** + * Return a flag indicating whether there are next records. + */ + public boolean hasNext() { + return revision < stopRevision; + } + + /** + * {@inheritDoc} + */ + public Record nextRecord() throws NoSuchElementException, JournalException { + if (!hasNext()) { + String msg = "No next revision."; + throw new NoSuchElementException(msg); + } + try { + if (record != null) { + record.close(); + record = null; + } + } catch (IOException e) { + close(); + String msg = "Unable to skip over record."; + throw new JournalException(msg, e); + } + + if (recordLog != null) { + if (!recordLog.contains(revision)) { + recordLog.close(); + recordLog = null; + } + } + + try { + if (recordLog == null) { + recordLog = getRecordLog(revision); + } + } catch (IOException e) { + String msg = "Unable to open record log with revision: " + revision; + throw new JournalException(msg, e); + } + + try { + record = recordLog.read(resolver, npResolver); + revision = record.getRevision(); + return record; + } catch (IOException e) { + String msg = "Unable to read record with revision: " + revision; + throw new JournalException(msg, e); + } + } + + /** + * Close this cursor, releasing its resources. + */ + public void close() { + if (recordLog != null) { + recordLog.close(); + } + } + + /** + * Return record log containing a given revision. + * + * @param revision revision to locate + * @return record log containing that revision + * @throws IOException if an I/O error occurs + */ + private FileRecordLog getRecordLog(long revision) throws IOException { + for (int i = 0; i < logFiles.length; i++) { + FileRecordLog recordLog = new FileRecordLog(logFiles[i]); + if (recordLog.contains(revision)) { + recordLog.seek(revision); + return recordLog; + } + } + String msg = "No log file found containing revision: " + revision; + throw new IOException(msg); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRecordLog.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRecordLog.java new file mode 100644 index 00000000000..8737084e6a8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRecordLog.java @@ -0,0 +1,459 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +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.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; + +/** + * A file record log is a file containing {@link Record}s. Every file record + * log contains a header with the following physical layout: + * + *
    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Byte 1Byte 2Byte 3Byte 4
    'J''L''O''G'
    MAJORMINOR
    START REVISION
    + *
    + * + * After this header, zero or more ReadRecords follow. + */ +public class FileRecordLog { + + /** + * Logger. + */ + private static Logger log = LoggerFactory.getLogger(FileRecordLog.class); + + /** + * Record log signature. + */ + private static final byte[] SIGNATURE = { 'J', 'L', 'O', 'G' }; + + /** + * Known major version. + */ + private static final short MAJOR_VERSION = 2; + + /** + * Known minor version. + */ + private static final short MINOR_VERSION = 0; + + /** + * Header size. This is the size of {@link #SIGNATURE}, {@link #MAJOR_VERSION}, + * {@link #MINOR_VERSION} and first revision (8 bytes). + */ + private static final int HEADER_SIZE = 4 + 2 + 2 + 8; + + /** + * Underlying file. + */ + private File logFile; + + /** + * Flag indicating whether this is a new log. + */ + private boolean isNew; + + /** + * Input stream used when seeking a specific record. + */ + private DataInputStream in; + + /** + * Last revision that is not in this log. + */ + private long previousRevision; + + /** + * Relative position inside this log. + */ + private long position; + + /** + * Last revision that is available in this log. + */ + private long lastRevision; + + /** + * Major version found in record log. + */ + private short major; + + /** + * Minor version found in record log. + */ + private short minor; + + /** + * Create a new instance of this class. Opens a record log in read-only mode. + * + * @param logFile file containing record log + * @throws java.io.IOException if an I/O error occurs + */ + public FileRecordLog(File logFile) throws IOException { + this.logFile = logFile; + + if (logFile.exists()) { + DataInputStream in = new DataInputStream( + new BufferedInputStream(new FileInputStream(logFile), 128)); + + try { + readHeader(in); + previousRevision = in.readLong(); + lastRevision = previousRevision + logFile.length() - HEADER_SIZE; + } finally { + close(in); + } + } else { + isNew = true; + } + } + + /** + * Initialize this record log by writing a header containing the + * previous revision. + */ + public void init(long previousRevision) throws IOException { + if (isNew) { + DataOutputStream out = new DataOutputStream( + new BufferedOutputStream(new FileOutputStream(logFile), 128)); + + try { + writeHeader(out); + out.writeLong(previousRevision); + } finally { + close(out); + } + + this.previousRevision = previousRevision; + this.lastRevision = previousRevision; + isNew = false; + } + } + + /** + * Return a flag indicating whether this record log contains a certain revision. + * + * @param revision revision to look for + * @return true if this record log contain a certain revision; + * false otherwise + */ + public boolean contains(long revision) { + return (revision >= previousRevision && revision < lastRevision); + } + + /** + * Return a flag indicating whether this record log is new. + * + * @return true if this record log is new; + * false otherwise + */ + public boolean isNew() { + return isNew; + } + + /** + * Return a flag indicating whether this record log exceeds a given size. + */ + public boolean exceeds(long size) { + return (lastRevision - previousRevision) > size; + } + + /** + * Seek an entry. This is an operation that allows the underlying input stream + * to be sequentially scanned and must therefore not be called twice. + * + * @param revision revision to seek + * @throws java.io.IOException if an I/O error occurs + */ + public void seek(long revision) throws IOException { + if (in != null) { + String msg = "Stream already open: seek() only allowed once."; + throw new IllegalStateException(msg); + } + in = new DataInputStream(new BufferedInputStream( + new FileInputStream(logFile))); + skip(revision - previousRevision + HEADER_SIZE); + position = revision - previousRevision; + } + + /** + * Skip exactly n bytes. Throws if less bytes are skipped. + * + * @param n bytes to skip + * @throws java.io.IOException if an I/O error occurs, or less that n bytes + * were skipped. + */ + private void skip(long n) throws IOException { + long skiplen = n; + while (skiplen > 0) { + long skipped = in.skip(skiplen); + if (skipped <= 0) { + break; + } + skiplen -= skipped; + } + if (skiplen != 0) { + String msg = "Unable to skip remaining bytes."; + throw new IOException(msg); + } + } + + /** + * Read the file record at the current seek position. + * + * @param resolver namespace resolver + * @return file record + * @throws java.io.IOException if an I/O error occurs + */ + public ReadRecord read(NamespaceResolver resolver, NamePathResolver npResolver) throws IOException { + String journalId = in.readUTF(); + String producerId = in.readUTF(); + int length = in.readInt(); + + position += + 2 + utfLength(journalId) + 2 + utfLength(producerId) + 4 + length; + + long revision = previousRevision + position; + return new ReadRecord(journalId, producerId, revision, in, length, resolver, npResolver); + } + + /** + * Append a record to this log. Returns the revision following this record. + * + * @param journalId journal identifier + * @param producerId producer identifier + * @param in record to add + * @param length record length + * @throws java.io.IOException if an I/O error occurs + */ + public long append(String journalId, String producerId, InputStream in, int length) + throws IOException { + + OutputStream out = new FileOutputStream(logFile, true); + + try { + DataBuffer buffer = new DataBuffer(); + buffer.writeUTF(journalId); + buffer.writeUTF(producerId); + buffer.writeInt(length); + buffer.copy(out); + + IOUtils.copy(in, out); + out.flush(); + + lastRevision += + 2 + utfLength(journalId) + 2 + utfLength(producerId) + + 4 + length; + return lastRevision; + } finally { + close(out); + } + } + + /** + * Return the previous revision. This is the last revision preceding the + * first revision in this log. + * + * @return previous revision + */ + public long getPreviousRevision() { + return previousRevision; + } + + /** + * Return the last revision. This is the last revision in this log. + * + * @return last revision + */ + public long getLastRevision() { + return lastRevision; + } + + /** + * Close this log. + */ + public void close() { + try { + if (in != null) { + in.close(); + } + } catch (IOException e) { + String msg = "Error while closing record log: " + e.getMessage(); + log.warn(msg); + } + } + + /** + * Read signature and major/minor version of file and verify. + * + * @param in input stream + * @throws java.io.IOException if an I/O error occurs or the file does + * not have a valid header. + */ + private void readHeader(DataInputStream in) throws IOException { + byte[] signature = new byte[SIGNATURE.length]; + in.readFully(signature); + + for (int i = 0; i < SIGNATURE.length; i++) { + if (signature[i] != SIGNATURE[i]) { + String msg = "Record log '" + logFile.getPath() + + "' has wrong signature: " + toHexString(signature); + throw new IOException(msg); + } + } + + major = in.readShort(); + if (major != MAJOR_VERSION) { + String msg = "Record log '" + logFile.getPath() + + "' has incompatible major version: " + major; + throw new IOException(msg); + } + minor = in.readShort(); + } + + /** + * Write signature and major/minor. + * + * @param out input stream + * @throws java.io.IOException if an I/O error occurs. + */ + private void writeHeader(DataOutputStream out) throws IOException { + out.write(SIGNATURE); + out.writeShort(MAJOR_VERSION); + out.writeShort(MINOR_VERSION); + } + + /** + * Close an input stream, logging a warning if an error occurs. + */ + private static void close(InputStream in) { + try { + in.close(); + } catch (IOException e) { + String msg = "I/O error while closing input stream."; + log.warn(msg, e); + } + } + + /** + * Close an output stream, logging a warning if an error occurs. + */ + private static void close(OutputStream out) { + try { + out.close(); + } catch (IOException e) { + String msg = "I/O error while closing input stream."; + log.warn(msg, e); + } + } + + /** + * Convert a byte array to its hexadecimal string representation. + */ + private static String toHexString(byte[] b) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < b.length; i++) { + String s = Integer.toHexString(b[i] & 0xff).toUpperCase(); + if (s.length() == 1) { + buf.append('0'); + } + buf.append(s); + } + return buf.toString(); + } + + /** + * Return the length of a string when converted to its Java modified + * UTF-8 encoding, as used by DataInput.readUTF and + * DataOutput.writeUTF. + */ + private static int utfLength(String s) { + char[] ac = s.toCharArray(); + int utflen = 0; + + for (int i = 0; i < ac.length; i++) { + char c = ac[i]; + if ((c >= 0x0001) && (c <= 0x007F)) { + utflen++; + } else if (c > 0x07FF) { + utflen += 3; + } else { + utflen += 2; + } + } + return utflen; + } + + /** + * A simple helper class that writes to a buffer. The current buffer can + * be {@link #copy copied} to an output stream. + */ + private static final class DataBuffer extends DataOutputStream { + + public DataBuffer() { + super(new ByteArrayOutputStream()); + } + + /** + * Copies the bytes the are currently held in the buffer to the given + * output stream. + * + * @param out the output stream where the buffered data is written. + * @throws IOException if an error occurs while writing data to + * out. + */ + public void copy(OutputStream out) throws IOException { + byte[] buffer = ((ByteArrayOutputStream) super.out).toByteArray(); + out.write(buffer); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRevision.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRevision.java new file mode 100644 index 00000000000..ce873b139d4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/FileRevision.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.core.journal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * Maintains a file-based revision counter with locking, assuring uniqueness. + */ +public class FileRevision implements InstanceRevision { + + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(FileRevision.class); + + /** + * Underlying random access file. + */ + protected final RandomAccessFile raf; + + /** + * Flag indicating whether to sync the file on every write. + */ + protected final boolean sync; + + /** + * Cached value. + */ + protected long value; + + /** + * Flag indicating whether this revision file is closed. + */ + protected boolean closed; + + /** + * Creates a new file based revision counter. + * + * @param file holding global counter + * @param sync whether to sync the file on every write + * + * @throws JournalException if some error occurs + */ + public FileRevision(File file, boolean sync) throws JournalException { + this.sync = sync; + + try { + if (!file.exists()) { + file.createNewFile(); + } + raf = new RandomAccessFile(file, "rw"); + if (raf.length() == 0) { + set(0); + } + } catch (IOException e) { + String msg = "I/O error while attempting to create new file '" + file + "'."; + throw new JournalException(msg, e); + } + } + + /** + * Return current counter value. + * + * @return counter value + * @throws JournalException if some error occurs + */ + public synchronized long get() throws JournalException { + try { + if (closed) { + throw new JournalException("Revision file closed."); + } + raf.seek(0L); + value = raf.readLong(); + return value; + } catch (IOException e) { + throw new JournalException("I/O error occurred.", e); + } + } + + /** + * Set current counter value. + * + * @param value new counter value + * @throws JournalException if some error occurs + */ + public synchronized void set(long value) throws JournalException { + try { + if (closed) { + throw new JournalException("Revision file closed."); + } + raf.seek(0L); + raf.writeLong(value); + if (sync) { + raf.getFD().sync(); + } + this.value = value; + } catch (IOException e) { + throw new JournalException("I/O error occurred.", e); + } + } + + /** + * Close file revision. Closes underlying random access file. + */ + public synchronized void close() { + try { + raf.close(); + closed = true; + } catch (IOException e) { + log.warn("I/O error closing revision file.", e); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/InstanceRevision.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/InstanceRevision.java new file mode 100644 index 00000000000..83bbe6bf5ca --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/InstanceRevision.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.core.journal; + +/** + * + */ +public interface InstanceRevision { + + /** + * Return current instance revision. + * + * @return instance revision + * @throws JournalException if some error occurs + */ + long get() throws JournalException; + + /** + * Set current instance revision. + * + * @param value new instance revision + * @throws JournalException if some error occurs + */ + void set(long value) throws JournalException; + + /** + * Closes the instance revision. + */ + void close(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JNDIDatabaseJournal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JNDIDatabaseJournal.java new file mode 100644 index 00000000000..fa4a0d24743 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JNDIDatabaseJournal.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.core.journal; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +/** + * @deprecated + * This class should not be used because it is not database vendor specific. + * Each DatabaseJournal now supports getting the connection via JNDI + * by setting the driver to javax.naming.InitialContext + * and the URL to the JNDI name. + *

    + * Database journal that uses JNDI to acquire the database connection. + * The JNDI location of the {@link DataSource} to be used in given as + * the dataSourceLocation configuration property. + *

    + * WARNING: The acquired database connection is kept + * for the entire lifetime of the journal instance. The configured data + * source should be prepared for this. + */ +public class JNDIDatabaseJournal extends DatabaseJournal { + + /** + * JNDI location of the data source used to acquire database connections. + */ + private String dataSourceLocation; + + //----------------------------------------------------< setters & getters > + + /** + * Returns the JNDI location of the data source. + * + * @return data source location + */ + public String getDataSourceLocation() { + return dataSourceLocation; + } + + /** + * Sets the JNDI location of the data source. + * + * @param dataSourceLocation data source location + */ + public void setDataSourceLocation(String dataSourceLocation) { + this.dataSourceLocation = dataSourceLocation; + } + + //-----------------------------------------------------< DatabaseJournal > + + /** + * Returns a JDBC connection from a {@link DataSource} acquired from JNDI + * with the configured data source location. + * + * @return new database connection + * @throws SQLException if a database access error occurs + * @see DataSource#getConnection() + */ + protected Connection getConnection() throws SQLException { + try { + InitialContext ic = new InitialContext(); + DataSource dataSource = (DataSource) ic.lookup(dataSourceLocation); + return dataSource.getConnection(); + } catch (NamingException e) { + SQLException exception = new SQLException( + "DataSource not found: " + dataSourceLocation); + exception.initCause(e); + throw exception; + } + } + + /** + * Overridden to avoid the driver and url checks in DatabaseJournal. + */ + protected void init() throws JournalException { + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/Journal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/Journal.java new file mode 100644 index 00000000000..3a2d759a4cd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/Journal.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.core.journal; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +/** + * Generic journal interface. + */ +public interface Journal { + + /** + * Initialize journal. + * + * @param id id this journal should use to write its own records + * @param resolver resolver used when reading/writing records + * @throws JournalException if an error occurs + */ + void init(String id, NamespaceResolver resolver) throws JournalException; + + /** + * Register a record consumer. + * + * @param consumer record consumer + * @throws JournalException if an error occurs + */ + void register(RecordConsumer consumer) throws JournalException; + + /** + * Unregister a record processor. + * + * @param consumer record processor to unregister + * @return true if the consumer was previously registered; + * false otherwise + */ + boolean unregister(RecordConsumer consumer); + + /** + * Synchronize contents from journal. This will compare the journal's + * revision with the revisions of all registered consumers and invoke + * their {@link RecordConsumer#consume} method when their identifier + * matches the one found in the records. + * The startup flag allow for a separate treatment of the initial sync + * when the cluster nodes starts up. This might be needed for example + * when there are a lot of old revisions in a database. + * + * @param startup indicates if the cluster node is syncing on startup + * or does a normal sync. + * @throws JournalException if an error occurs + */ + void sync(boolean startup) throws JournalException; + + /** + * Return the record producer for a given identifier. + * + * @param identifier identifier + * @return the record producer for a given identifier. + * @throws JournalException if an error occurs + */ + RecordProducer getProducer(String identifier) throws JournalException; + + /** + * Close this journal. This should release any resources still held by this journal. + */ + void close(); + + /** + * Gets the instance that manages the cluster node's local revision. + * + * @return the InstanceRevision manager + * @throws JournalException on error + */ + InstanceRevision getInstanceRevision() throws JournalException; + + /** + * Return an iterator over all records after the specified revision. + * + * @param startRevision start point (exlusive) + * @return an iterator over all records after the specified revision. + * @throws JournalException if an error occurs + */ + RecordIterator getRecords(long startRevision) + throws JournalException; + + /** + * Return an iterator over all available records in the journal. + * + * @return an iterator over all records. + * @throws JournalException if an error occurs + */ + RecordIterator getRecords() throws JournalException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JournalException.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JournalException.java new file mode 100644 index 00000000000..d7d95d0ac72 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JournalException.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.core.journal; + +/** + * The JournalException signals an error within a journal operation. + */ +public class JournalException extends Exception { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public JournalException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public JournalException(String message, Throwable rootCause) { + super(message, rootCause); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JournalFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JournalFactory.java new file mode 100644 index 00000000000..e66158c175b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/JournalFactory.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.core.journal; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +/** + * Factory interface for creating {@link Journal} instances. Used + * to decouple the repository internals from the repository configuration + * mechanism. + */ +public interface JournalFactory { + + /** + * Creates, initializes, and returns a {@link Journal} instance + * for use by the repository. + * + * @param resolver namespace resolver + * @return initialized journal + * @throws RepositoryException if the journal can not be created + */ + Journal getJournal(NamespaceResolver resolver) throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/LockableFileRevision.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/LockableFileRevision.java new file mode 100644 index 00000000000..c5840d4e301 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/LockableFileRevision.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.core.journal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileLock; + +/** + * Maintains a file-based revision counter with locking, assuring uniqueness. + */ +class LockableFileRevision { + + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(LockableFileRevision.class); + + /** + * Underlying file. + */ + private final File file; + + /** + * Underlying random access file. + */ + private RandomAccessFile raf; + + /** + * File lock. + */ + private FileLock lock; + + /** + * Current lock count. + */ + private int locks; + + /** + * Creates a new file based revision counter. + * + * @param file holding global counter + */ + public LockableFileRevision(File file) { + this.file = file; + + try { + if (!file.exists()) { + file.createNewFile(); + } + } catch (IOException e) { + String msg = "I/O error while attempting to create new file '" + file + "': " + e.getMessage(); + log.warn(msg); + } + } + + /** + * Lock underlying file. + * + * @param shared whether to allow other readers or not + */ + public synchronized void lock(boolean shared) throws JournalException { + if (lock == null) { + try { + raf = new RandomAccessFile(file, shared ? "r" : "rw"); + lock = raf.getChannel().lock(0L, Long.MAX_VALUE, shared); + } catch (IOException e) { + String msg = "I/O error occurred."; + throw new JournalException(msg, e); + } finally { + if (lock == null && raf != null) { + try { + raf.close(); + } catch (IOException e) { + String msg = "I/O error while closing file " + file.getPath() + ": " + e.getMessage(); + log.warn(msg); + } + raf = null; + } + } + } + locks++; + } + + /** + * Unlock underlying file. + */ + public synchronized void unlock() { + if (lock != null && --locks == 0) { + try { + lock.release(); + } catch (IOException e) { + String msg = "I/O error while releasing lock: " + e.getMessage(); + log.warn(msg); + } + lock = null; + + if (raf != null) { + try { + raf.close(); + } catch (IOException e) { + String msg = "I/O error while closing file: " + e.getMessage(); + log.warn(msg); + } + } + raf = null; + } + } + + /** + * Return current counter value. + * + * @return counter value + * @throws JournalException if some error occurs + */ + public long get() throws JournalException { + lock(true); + + try { + long value = 0L; + if (raf.length() > 0) { + raf.seek(0L); + value = raf.readLong(); + } + return value; + + } catch (IOException e) { + throw new JournalException("I/O error occurred: ", e); + } finally { + unlock(); + } + } + + /** + * Set current counter value. + * + * @param value new counter value + * @throws JournalException if some error occurs + */ + public void set(long value) throws JournalException { + lock(false); + + try { + raf.seek(0L); + raf.writeLong(value); + } catch (IOException e) { + throw new JournalException("I/O error occurred.", e); + } finally { + unlock(); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MSSqlDatabaseJournal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MSSqlDatabaseJournal.java new file mode 100644 index 00000000000..4299520542f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MSSqlDatabaseJournal.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.core.journal; + +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; + +/** + * It has the following property in addition to those of the DatabaseJournal: + *

      + *
    • tableSpace: the MS SQL tablespace to use
    • + *
    + */ +public class MSSqlDatabaseJournal extends DatabaseJournal { + + /** the MS SQL table space to use */ + protected String tableSpace = ""; + + /** + * Initialize this instance with the default schema and + * driver values. + */ + public MSSqlDatabaseJournal() { + setDriver("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + setDatabaseType("mssql"); + } + + /** + * {@inheritDoc} + */ + @Override + protected CheckSchemaOperation createCheckSchemaOperation() { + return super.createCheckSchemaOperation().addVariableReplacement( + CheckSchemaOperation.TABLE_SPACE_VARIABLE, tableSpace); + } + + /** + * Returns the configured MS SQL table space. + * @return the configured MS SQL table space. + */ + public String getTableSpace() { + return tableSpace; + } + + /** + * Sets the MS SQL table space. + * @param tableSpace the MS SQL table space. + */ + public void setTableSpace(String tableSpace) { + if (tableSpace != null && tableSpace.length() > 0) { + this.tableSpace = "on " + tableSpace.trim(); + } else { + this.tableSpace = ""; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MemoryJournal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MemoryJournal.java new file mode 100644 index 00000000000..b896080cdaa --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MemoryJournal.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.NoSuchElementException; +import java.util.List; +import java.util.Collections; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Memory-based journal, useful for testing purposes only. + */ +public class MemoryJournal extends AbstractJournal { + + /** + * Default read delay: none. + */ + private static final long DEFAULT_READ_DELAY = 0; + + /** + * Default write delay: none. + */ + private static final long DEFAULT_WRITE_DELAY = 0; + + /** + * Logger. + */ + private static Logger log = LoggerFactory.getLogger(MemoryJournal.class); + + /** + * Revision. + */ + private InstanceRevision revision = new MemoryRevision(); + + /** + * List of records. + */ + private List records = Collections.synchronizedList(new ArrayList()); + + /** + * Set the read delay, i.e. the time in ms to wait before returning + * a record. + */ + private long readDelay = DEFAULT_READ_DELAY; + + /** + * Set the write delay, i.e. the time in ms to wait before appending + * a record. + */ + private long writeDelay = DEFAULT_WRITE_DELAY; + + /** + * Flag indicating whether this journal is closed. + */ + private boolean closed; + + /** + * {@inheritDoc} + */ + public InstanceRevision getInstanceRevision() throws JournalException { + return revision; + } + + /** + * {@inheritDoc} + */ + public void init(String id, NamespaceResolver resolver) + throws JournalException { + + super.init(id, resolver); + } + + /** + * {@inheritDoc} + */ + protected void doLock() throws JournalException { + checkState(); + } + + @Override + protected void appending(AppendRecord record) { + record.setRevision(records.size()+1); + } + + /** + * {@inheritDoc} + */ + protected void append(AppendRecord record, InputStream in, int length) + throws JournalException { + + checkState(); + + byte[] data = new byte[length]; + int off = 0; + + while (off < data.length) { + try { + int len = in.read(data, off, data.length - off); + if (len < 0) { + String msg = "Unexpected end of record after " + off + " bytes."; + throw new JournalException(msg); + } + off += len; + } catch (IOException e) { + String msg = "I/O error after " + off + " bytes."; + throw new JournalException(msg, e); + } + } + try { + Thread.sleep(writeDelay); + } catch (InterruptedException e) { + throw new JournalException("Interrupted in append()."); + } + records.add(new MemoryRecord(getId(), record.getProducerId(), data)); + } + + /** + * {@inheritDoc} + */ + protected void doUnlock(boolean successful) { + try { + checkState(); + } catch (JournalException e) { + log.warn("Journal already closed while unlocking."); + } + } + + /** + * {@inheritDoc} + */ + public RecordIterator getRecords(long startRevision) + throws JournalException { + + checkState(); + + startRevision = Math.max(startRevision, 0); + long stopRevision = records.size(); + + return new MemoryRecordIterator(startRevision, stopRevision); + } + + /** + * {@inheritDoc} + */ + public RecordIterator getRecords() throws JournalException { + return new MemoryRecordIterator(0, records.size()); + } + + /** + * Set records. Used to share records between two journal implementations. + * + * @param records array list that should back up this memory journal + */ + public void setRecords(List records) { + this.records = records; + } + + /** + * Return the read delay in milliseconds. + * + * @return read delay + */ + public long getReadDelay() { + return readDelay; + } + + /** + * Set the read delay in milliseconds. + * + * @param readDelay read delay + */ + public void setReadDelay(long readDelay) { + this.readDelay = readDelay; + } + + /** + * Return the write delay in milliseconds. + * + * @return write delay + */ + public long getWriteDelay() { + return writeDelay; + } + + /** + * Set the write delay in milliseconds. + * + * @param writeDelay write delay + */ + public void setWriteDelay(long writeDelay) { + this.writeDelay = writeDelay; + } + + /** + * {@inheritDoc} + */ + public void close() { + closed = true; + } + + /** + * Check state of this journal. + */ + private void checkState() throws JournalException { + if (closed) { + throw new JournalException("Journal closed."); + } + } + + /** + * Memory record. + */ + public static class MemoryRecord { + + /** + * Journal id. + */ + private String journalId; + + /** + * Producer id. + */ + private String producerId; + + /** + * Data. + */ + private byte[] data; + + /** + * Create a new instance of this class + * + * @param journalId journal id + * @param producerId producer id + * @param data data + */ + public MemoryRecord(String journalId, String producerId, byte[] data) { + this.journalId = journalId; + this.producerId = producerId; + this.data = data; + } + + /** + * Return the journal id. + * + * @return the journal id + */ + public String getJournalId() { + return journalId; + } + + /** + * Return the producer id. + * + * @return the producer id + */ + public String getProducerId() { + return producerId; + } + + /** + * Return the data. + * + * @return data + */ + public byte[] getData() { + return data; + } + } + + /** + * Record iterator implementation. + */ + public class MemoryRecordIterator implements RecordIterator { + + /** + * Current revision. + */ + private long revision; + + /** + * Last revision. + */ + private final long stopRevision; + + /** + * Create a new instance of this class. + * + * @param startRevision start revision + * @param stopRevision stop revision + */ + public MemoryRecordIterator(long startRevision, long stopRevision) { + this.revision = startRevision; + this.stopRevision = stopRevision; + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + return revision < stopRevision; + } + + /** + * {@inheritDoc} + */ + public Record nextRecord() throws NoSuchElementException, + JournalException { + + int index = (int) revision; + MemoryRecord record = records.get(index); + + checkState(); + + byte[] data = record.getData(); + DataInputStream dataIn = new DataInputStream( + new ByteArrayInputStream(data)); + + try { + Thread.sleep(readDelay); + } catch (InterruptedException e) { + throw new JournalException("Interrupted in read()."); + } + + return new ReadRecord(record.getJournalId(), record.getProducerId(), + ++revision, dataIn, data.length, + getResolver(), getNamePathResolver()); + } + + /** + * {@inheritDoc} + */ + public void close() { + // nothing to be done here + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MemoryRevision.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MemoryRevision.java new file mode 100644 index 00000000000..54b691f318b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/MemoryRevision.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.core.journal; + +/** + * Memory-based revision, useful for testing purposes only. + */ +public class MemoryRevision implements InstanceRevision { + + /** + * Revision. + */ + private long revision; + + /** + * {@inheritDoc} + */ + public long get() throws JournalException { + return revision; + } + + /** + * {@inheritDoc} + */ + public void set(long value) throws JournalException { + this.revision = value; + } + + /** + * {@inheritDoc} + */ + public void close() { + // nothing to be done here + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/OracleDatabaseJournal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/OracleDatabaseJournal.java new file mode 100644 index 00000000000..43a4f00b9cc --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/OracleDatabaseJournal.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.core.journal; + +import javax.sql.DataSource; + +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.OracleConnectionHelper; + +/** + * It has the following property in addition to those of the DatabaseJournal: + *
      + *
    • tablespace: the tablespace to use for tables
    • + *
    • indexTablespace: the tablespace to use for indexes
    • + *
    + */ +public class OracleDatabaseJournal extends DatabaseJournal { + /** + * The default tablespace clause used when {@link #tablespace} or {@link #indexTablespace} + * are not specified. + */ + protected static final String DEFAULT_TABLESPACE_CLAUSE = ""; + + /** + * Name of the replacement variable in the DDL for {@link #tablespace}. + */ + protected static final String TABLESPACE_VARIABLE = "${tablespace}"; + + /** + * Name of the replacement variable in the DDL for {@link #indexTablespace}. + */ + protected static final String INDEX_TABLESPACE_VARIABLE = "${indexTablespace}"; + + /** The Oracle tablespace to use for tables */ + protected String tablespace; + + /** The Oracle tablespace to use for indexes */ + protected String indexTablespace; + + public OracleDatabaseJournal() { + setDatabaseType("oracle"); + setDriver("oracle.jdbc.OracleDriver"); + setSchemaObjectPrefix(""); + tablespace = DEFAULT_TABLESPACE_CLAUSE; + indexTablespace = DEFAULT_TABLESPACE_CLAUSE; + } + + /** + * Returns the configured Oracle tablespace for tables. + * @return the configured Oracle tablespace for tables. + */ + public String getTablespace() { + return tablespace; + } + + /** + * Sets the Oracle tablespace for tables. + * @param tablespaceName the Oracle tablespace for tables. + */ + public void setTablespace(String tablespaceName) { + this.tablespace = this.buildTablespaceClause(tablespaceName); + } + + /** + * Returns the configured Oracle tablespace for indexes. + * @return the configured Oracle tablespace for indexes. + */ + public String getIndexTablespace() { + return indexTablespace; + } + + /** + * Sets the Oracle tablespace for indexes. + * @param tablespaceName the Oracle tablespace for indexes. + */ + public void setIndexTablespace(String tablespaceName) { + this.indexTablespace = this.buildTablespaceClause(tablespaceName); + } + + /** + * Constructs the tablespace <tbs name> clause from + * the supplied tablespace name. If the name is empty, {@link #DEFAULT_TABLESPACE_CLAUSE} + * is returned instead. + * + * @param tablespaceName A tablespace name + * @return A tablespace clause using the supplied name or + * {@value #DEFAULT_TABLESPACE_CLAUSE} if the name is empty + */ + private String buildTablespaceClause(String tablespaceName) { + if (tablespaceName == null || tablespaceName.trim().length() == 0) { + return DEFAULT_TABLESPACE_CLAUSE; + } else { + return "tablespace " + tablespaceName.trim(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + OracleConnectionHelper helper = new OracleConnectionHelper(dataSrc, false); + helper.init(); + return helper; + } + + /** + * {@inheritDoc} + */ + @Override + protected CheckSchemaOperation createCheckSchemaOperation() { + if (DEFAULT_TABLESPACE_CLAUSE.equals(indexTablespace) && !DEFAULT_TABLESPACE_CLAUSE.equals(tablespace)) { + // tablespace was set but not indexTablespace : use the same for both + indexTablespace = tablespace; + } + return super.createCheckSchemaOperation() + .addVariableReplacement(TABLESPACE_VARIABLE, tablespace) + .addVariableReplacement(INDEX_TABLESPACE_VARIABLE, indexTablespace); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/ReadRecord.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/ReadRecord.java new file mode 100644 index 00000000000..84a55e4ba0f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/ReadRecord.java @@ -0,0 +1,285 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.Name; + +import java.io.DataInputStream; +import java.io.IOException; + +/** + * Record used for reading. + */ +public class ReadRecord extends AbstractRecord { + + /** + * This record's journal id. + */ + private final String journalId; + + /** + * This record's producer id. + */ + private final String producerId; + + /** + * This record's revision. + */ + private final long revision; + + /** + * Underlying data input. + */ + private final DataInputStream dataIn; + + /** + * This record's length. + */ + private final int length; + + /** + * Flag indicating whether this record was consumed. + */ + private boolean consumed; + + /** + * Create a new instance of this class. + */ + public ReadRecord(String journalId, String producerId, + long revision, DataInputStream dataIn, int length, + NamespaceResolver resolver, NamePathResolver npResolver) { + + super(resolver, npResolver); + + this.journalId = journalId; + this.producerId = producerId; + this.revision = revision; + this.dataIn = dataIn; + this.length = length; + } + + /** + * {@inheritDoc} + */ + public String getJournalId() { + return journalId; + } + + /** + * {@inheritDoc} + */ + public String getProducerId() { + return producerId; + } + + /** + * {@inheritDoc} + */ + public long getRevision() { + return revision; + } + + /** + * {@inheritDoc} + */ + public byte readByte() throws JournalException { + consumed = true; + + try { + return dataIn.readByte(); + } catch (IOException e) { + String msg = "I/O error while reading byte."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public char readChar() throws JournalException { + consumed = true; + + try { + return dataIn.readChar(); + } catch (IOException e) { + String msg = "I/O error while reading character."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public boolean readBoolean() throws JournalException { + consumed = true; + + try { + return dataIn.readBoolean(); + } catch (IOException e) { + String msg = "I/O error while reading boolean."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public int readInt() throws JournalException { + consumed = true; + + try { + return dataIn.readInt(); + } catch (IOException e) { + String msg = "I/O error while reading integer."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public long readLong() throws JournalException { + consumed = true; + + try { + return dataIn.readLong(); + } catch (IOException e) { + String msg = "I/O error while reading long."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public String readString() throws JournalException { + consumed = true; + + try { + boolean isNull = dataIn.readBoolean(); + if (isNull) { + return null; + } else { + return dataIn.readUTF(); + } + } catch (IOException e) { + String msg = "I/O error while reading string."; + throw new JournalException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public void readFully(byte[] b) throws JournalException { + consumed = true; + + try { + dataIn.readFully(b); + } catch (IOException e) { + String msg = "I/O error while reading byte array."; + throw new JournalException(msg, e); + } + } + + /** + * Close this record, eventually skipping unconsumed bytes. + * + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + if (length != 0) { + if (!consumed) { + skip(length); + } + } else { + dataIn.close(); + } + } + + /** + * Skip exactly n bytes. Throws if less bytes are skipped. + * + * @param n bytes to skip + * @throws IOException if an I/O error occurs, or less than + * n bytes were skipped. + */ + private void skip(long n) throws IOException { + long skiplen = n; + while (skiplen > 0) { + long skipped = dataIn.skip(skiplen); + if (skipped <= 0) { + break; + } + skiplen -= skipped; + } + if (skiplen != 0) { + String msg = "Should have skipped " + n + + " bytes, only " + (n - skiplen) + " skipped."; + throw new IOException(msg); + } + } + + /** + * Unsupported methods when appending. + */ + public void writeByte(int n) throws JournalException { + throw unsupported(); + } + + public void writeChar(char c) throws JournalException { + throw unsupported(); + } + + public void writeBoolean(boolean b) throws JournalException { + throw unsupported(); + } + + public void writeInt(int n) throws JournalException { + throw unsupported(); + } + + public void writeLong(long n) throws JournalException { + throw unsupported(); + } + + public void writeString(String s) throws JournalException { + throw unsupported(); + } + + public void writeQName(Name name) throws JournalException { + throw unsupported(); + } + + public void write(byte[] b) throws JournalException { + throw unsupported(); + } + + public long update() throws JournalException { + throw unsupported(); + } + + public void cancelUpdate() { + } + + private JournalException unsupported() { + String msg = "Record has been opened read-only."; + return new JournalException(msg); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/Record.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/Record.java new file mode 100644 index 00000000000..02ff9265611 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/Record.java @@ -0,0 +1,291 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.PrivilegeDefinition; + +/** + * Record interface. + */ +public interface Record { + + /** + * Returns the revision this record represents. + * + * @return revision + */ + long getRevision(); + + /** + * Return this record's journal identifier. + * + * @return journal identifier + */ + String getJournalId(); + + /** + * Return this record's producer identifier. + * + * @return producer identifier + */ + String getProducerId(); + + /** + * Read a byte from the underlying stream. + * + * @return byte + * @throws JournalException if an error occurs + */ + byte readByte() throws JournalException; + + /** + * Read a character from the underlying stream. + * + * @return character + * @throws JournalException if an error occurs + */ + char readChar() throws JournalException; + + /** + * Read a boolean from the underlying stream. + * + * @return boolean + * @throws JournalException if an error occurs + */ + boolean readBoolean() throws JournalException; + + /** + * Read an integer from the underlying stream. + * + * @return integer + * @throws JournalException if an error occurs + */ + int readInt() throws JournalException; + + /** + * Read a long from the underlying stream. + * + * @return long value. + * @throws JournalException if an error occurs + */ + long readLong() throws JournalException; + + /** + * Read a string from the underlying stream. + * + * @return string or null + * @throws JournalException if an error occurs + */ + String readString() throws JournalException; + + /** + * Fully read an array of bytes from the underlying stream. + * + * @param b byte array + * @throws JournalException if an error occurs + */ + void readFully(byte[] b) throws JournalException; + + /** + * Read a Name frmo the underlying stream. + * + * @return name name + * @throws JournalException if an error occurs + */ + Name readQName() throws JournalException; + + /** + * Read a named path element from the underlying stream. + * + * @return path element + * @throws JournalException if an error occurs + */ + Path readPathElement() throws JournalException; + + /** + * Read a Path from the underlying stream. + * + * @return path + * @throws JournalException if an error occurs + */ + Path readPath() throws JournalException; + + /** + * Read a NodeId from the underlying stream. + * + * @return node id + * @throws JournalException if an error occurs + */ + NodeId readNodeId() throws JournalException; + + /** + * Read a PropertyId from the underlying stream. + * + * @return property id + * @throws JournalException if an error occurs + */ + PropertyId readPropertyId() throws JournalException; + + /** + * Read a NodeTypeDef from the underlying stream. + * + * @return node type definition + * @throws JournalException if an error occurs + */ + QNodeTypeDefinition readNodeTypeDef() throws JournalException; + + /** + * Read a PrivilegeDefinition from the underlying stream. + * + * @return privilege definition + * @throws JournalException if an error occurs + */ + PrivilegeDefinition readPrivilegeDef() throws JournalException; + + /** + * Write a byte to the underlying stream. + * + * @param n byte + * @throws JournalException if an error occurs + */ + void writeByte(int n) throws JournalException; + + /** + * Write a character to the underlying stream. + * + * @param c character + * @throws JournalException if an error occurs + */ + void writeChar(char c) throws JournalException; + + /** + * Write a boolean from the underlying stream. + * + * @param b boolean + * @throws JournalException if an error occurs + */ + void writeBoolean(boolean b) throws JournalException; + + /** + * Write an integer to the underlying stream. + * + * @param n integer + * @throws JournalException if an error occurs + */ + void writeInt(int n) throws JournalException; + + /** + * Write a long to the underlying stream. + * + * @param n long + * @throws JournalException if an error occurs + */ + void writeLong(long n) throws JournalException; + + /** + * Write a string to the underlying stream. + * + * @param s string, may be null + * @throws JournalException if an error occurs + */ + void writeString(String s) throws JournalException; + + /** + * Write an array of bytes to the underlying stream. + * + * @param b byte array + * @throws JournalException if an error occurs + */ + void write(byte[] b) throws JournalException; + + /** + * Write a Name to the underlying stream. + * + * @param name name + * @throws JournalException if an error occurs + */ + void writeQName(Name name) throws JournalException; + + /** + * Write a Path.Element to the underlying stream. + * + * @param element path element + * @throws JournalException if an error occurs + */ + void writePathElement(Path element) throws JournalException; + + /** + * Write a Path to the underlying stream. + * + * @param path path + * @throws JournalException if an error occurs + */ + void writePath(Path path) throws JournalException; + + /** + * Write a NodeId to the underlying stream. + * + * @param nodeId node id + * @throws JournalException if an error occurs + */ + void writeNodeId(NodeId nodeId) throws JournalException; + + /** + * Write a PropertyId to the underlying stream. + * + * @param propertyId property id + * @throws JournalException if an error occurs + */ + void writePropertyId(PropertyId propertyId) throws JournalException; + + /** + * Write a NodeTypeDef to the underlying stream. + * + * @param ntd node type definition + * @throws JournalException if an error occurs + */ + void writeNodeTypeDef(QNodeTypeDefinition ntd) throws JournalException; + + /** + * Write a PrivilegeDefinition to the underlying stream. + * + * @param privilegeDefinition privilege definition + * @throws JournalException if an error occurs + */ + void writePrivilegeDef(PrivilegeDefinition privilegeDefinition) throws JournalException; + + /** + * Update the changes made to an appended record. This will also update + * this record's revision. + * + * @return The update size in bytes. + * @throws JournalException if this record has not been appended, + * or if another error occurs + */ + long update() throws JournalException; + + /** + * Cancel the changes made to an appended record. + */ + void cancelUpdate(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordConsumer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordConsumer.java new file mode 100644 index 00000000000..41b2fc67457 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordConsumer.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.core.journal; + +/** + * Listener interface on a journal that gets called back for records that should be consumed. + */ +public interface RecordConsumer { + + /** + * Return the unique identifier of the records this consumer + * will be able to handle. + * + * @return unique identifier + */ + String getId(); + + /** + * Return the revision this consumer has last seen. + * + * @return revision + */ + long getRevision(); + + /** + * Consume a record. + * + * @param record record to consume + */ + void consume(Record record); + + /** + * Set the revision this consumer has last seen. + * + * @param revision revision + */ + void setRevision(long revision); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordIterator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordIterator.java new file mode 100644 index 00000000000..e563f74d9a7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordIterator.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.core.journal; + +import java.util.NoSuchElementException; + +/** + * RecordIterator interface. + */ +public interface RecordIterator { + + /** + * Return a flag indicating whether there are more records. + * + * @return true if there are more records; + * false otherwise + */ + boolean hasNext(); + + /** + * Return the next record. If there are no more records, throws + * a NoSuchElementException. If an error occurs, + * throws a JournalException. + * + * @return next record + * @throws NoSuchElementException if there are no more records + * @throws JournalException if another error occurs + */ + Record nextRecord() throws NoSuchElementException, JournalException; + + /** + * Close this iterator. Releases all associated resources. + */ + void close(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordProducer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordProducer.java new file mode 100644 index 00000000000..9ec14f80e97 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RecordProducer.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.core.journal; + +/** + * Produces new records that can be appended to the journal. + */ +public interface RecordProducer { + + /** + * Append a record. This operation implicitly locks the journal revision + * and must be followed by either {@link Record#update} or {@link Record#cancelUpdate}. + * on the record returned. + * + * @return appended record + * @throws JournalException if an error occurs + */ + Record append() throws JournalException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RotatingLogFile.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RotatingLogFile.java new file mode 100644 index 00000000000..604b63af056 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/journal/RotatingLogFile.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.core.journal; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents a log file that can be rotated. + */ +public class RotatingLogFile implements Comparable { + + /** + * Logger. + */ + private static Logger log = LoggerFactory.getLogger(RotatingLogFile.class); + + /** + * Log extension. + */ + private static final String LOG_EXTENSION = "log"; + + /** + * Parent directory. + */ + private final File directory; + + /** + * Basename. + */ + private final String basename; + + /** + * Backing file. + */ + private final File file; + + /** + * Version number. + */ + private int version; + + /** + * Create a new instance of this class. + * + * @param file file itself + * @throws IllegalArgumentException if the filename is malformed + */ + private RotatingLogFile(File directory, String basename, File file) + throws IllegalArgumentException { + + this.directory = directory; + this.basename = basename; + this.file = file; + + parseName(); + } + + /** + * Parse the file name, ensuring that the file is actually made up + * of the components we expect. + * + * @throws IllegalArgumentException if the name is malformed + */ + private void parseName() throws IllegalArgumentException { + String name = file.getName(); + int sep1 = name.indexOf('.'); + if (sep1 == -1) { + throw new IllegalArgumentException("no dot in filename."); + } + if (!basename.equals(name.substring(0, sep1))) { + throw new IllegalArgumentException("name does not start with " + + basename + "."); + } + int sep2 = name.indexOf('.', sep1 + 1); + if (sep2 == -1) { + sep2 = name.length(); + } + if (!LOG_EXTENSION.equals(name.substring(sep1 + 1, sep2))) { + throw new IllegalArgumentException("name does not contain " + + LOG_EXTENSION + "."); + } + if (sep2 < name.length()) { + String versionS = name.substring(sep2 + 1); + try { + version = Integer.parseInt(versionS); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "extension is not a number: " + versionS); + } + } + } + + /** + * Return the backing file. + */ + public File getFile() { + return file; + } + + /** + * Rotate this file. + */ + public void rotate() { + String newName = basename + + "." + LOG_EXTENSION + + "." + String.valueOf(version + 1); + file.renameTo(new File(directory, newName)); + } + + /** + * Compares this log file to another file. It will return + * a negative number if this log file has a smaller version, + * a positive number if this log file a bigger version + * and 0 if they have the same version. + */ + public int compareTo(RotatingLogFile o) { + return version - o.version; + } + + /** + * List all log files inside some directory. The list returned is + * guaranteed to be in descending order, i.e. it is safe to rotate + * every file in turn without accidentally overwriting another one. + * + * @param directory parent directory + * @param basename basename expected + * @return array of log files found + */ + public static RotatingLogFile[] listFiles(File directory, final String basename) { + File[] files = directory.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.startsWith(basename + "."); + } + }); + + ArrayList l = new ArrayList(); + for (int i = 0; i < files.length; i++) { + File file = files[i]; + try { + l.add(new RotatingLogFile(directory, basename, file)); + } catch (IllegalArgumentException e) { + log.warn("Bogusly named journal file, skipped: " + files[i] + + ", reason: " + e.getMessage()); + } + } + RotatingLogFile[] logFiles = new RotatingLogFile[l.size()]; + l.toArray(logFiles); + + Arrays.sort(logFiles, new Comparator() { + public int compare(RotatingLogFile o1, RotatingLogFile o2) { + return o2.compareTo(o1); + } + }); + return logFiles; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockImpl.java new file mode 100644 index 00000000000..7fe701ed3d1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockImpl.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.lock; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.security.authorization.Permission; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; + +/** + * Implementation of a Lock that gets returned to clients asking + * for a lock. + */ +class LockImpl implements javax.jcr.lock.Lock { + + /** + * Lock info containing latest information + */ + protected final LockInfo info; + + /** + * Node holding lock + */ + protected final NodeImpl node; + + /** + * Create a new instance of this class. + * + * @param info lock information + * @param node node holding lock + */ + public LockImpl(LockInfo info, NodeImpl node) { + this.info = info; + this.node = node; + } + + //-----------------------------------------------------------------< Lock > + + /** + * {@inheritDoc} + */ + public String getLockOwner() { + return info.getLockOwner(); + } + + /** + * {@inheritDoc} + */ + public boolean isDeep() { + return info.isDeep(); + } + + /** + * {@inheritDoc} + */ + public Node getNode() { + return node; + } + + /** + * {@inheritDoc} + */ + public String getLockToken() { + if (!info.isSessionScoped() && (info.isLockHolder(node.getSession()) || isAdminUser(node.getSession()))) { + return info.getLockToken(); + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + public boolean isLive() throws RepositoryException { + return info.isLive(); + } + + /** + * {@inheritDoc} + */ + public boolean isSessionScoped() { + return info.isSessionScoped(); + } + + /** + * {@inheritDoc} + * @throws LockException if this Session is not the lock holder + * or if this Lock is not alive. + */ + public void refresh() throws LockException, RepositoryException { + if (!isLive()) { + info.throwLockException( + "Lock is not live any more", + (SessionImpl) node.getSession()); + } else if (!isLockOwningSession()) { + info.throwLockException( + "Session does not hold lock.", + (SessionImpl) node.getSession()); + } else { + // make sure the current session has sufficient privileges to refresh + // the lock. + SessionImpl session = (SessionImpl) node.getSession(); + session.getAccessManager().checkPermission( + node.getPrimaryPath(), Permission.LOCK_MNGMT); + + // Update the lock timeout + info.updateTimeoutTime(); + } + } + + //--------------------------------------------------< new JSR 283 methods > + + /** {@inheritDoc} */ + public long getSecondsRemaining() { + if (!info.isLive()) { + return -1; + } + + // TODO JCR-1590: Disabled until locks get unlocked when they timeout + long timeout = info.getTimeoutTime(); + if (timeout == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + + long remainingSeconds = (timeout - System.currentTimeMillis()) / 1000; + + return Math.max(remainingSeconds, 1); // must always be positive + } + + /** + * @see javax.jcr.lock.Lock#isLockOwningSession() + */ + public boolean isLockOwningSession() { + return info.isLockHolder(node.getSession()); + } + + /** + * Check whether a session belongs to an administrative user. + */ + private boolean isAdminUser(Session session) { + if (session instanceof SessionImpl) { + return ((SessionImpl) session).isAdmin(); + } else { + // fallback. use hardcoded default admin ID + return "admin".equals(session.getUserID()); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockInfo.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockInfo.java new file mode 100644 index 00000000000..0fd9844f300 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockInfo.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.lock; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; + +/** + * Internal lock information. + */ +public abstract class LockInfo { + + /** + * The biggest possible timeout hint value (in seconds), used to avoid + * overflows when calculating the timeout. 100 years should be plenty + * enough for anyone... + */ + private static final long MAXIMUM_TIMEOUT = 100L * 365L * 24L * 60L * 60L; + + /** + * Lock holder node id. Used also as the lock token. + */ + private final NodeId id; + + /** + * Flag indicating whether lock is session scoped + */ + private final boolean sessionScoped; + + /** + * Flag indicating whether lock is deep + */ + private final boolean deep; + + /** + * Lock owner, determined on creation time + */ + private final String lockOwner; + + /** + * Lock timeout hint (in seconds) given when the lock was created. + * Guaranteed to be between 0 and {@link #MAXIMUM_TIMEOUT}. If the value + * is 0, then this lock will not timeout. + */ + private final long timeoutHint; + + /** + * Time (in milliseconds since the epoch) when this lock will timeout. Set to + * {@link Long#MAX_VALUE} if this lock will not timeout. + */ + private long timeoutTime; + + /** + * Flag indicating whether this lock is live. See also {@link #timeoutTime}. + */ + private boolean live; + + /** + * Session currently holding lock + */ + private SessionImpl lockHolder; + + /** + * Create a new instance of this class. + * + * @param id lock holder node id + * @param sessionScoped whether lock token is session scoped + * @param deep whether lock is deep + * @param lockOwner owner of lock + * @param timeoutHint lock timeout hint in seconds + */ + protected LockInfo( + NodeId id, boolean sessionScoped, boolean deep, + String lockOwner, long timeoutHint) { + this.id = id; + this.sessionScoped = sessionScoped; + this.deep = deep; + this.lockOwner = lockOwner; + this.timeoutHint = timeoutHint; + + updateTimeoutTime(); + } + + protected LockInfo(LockInfo that) { + this.id = that.id; + this.sessionScoped = that.sessionScoped; + this.deep = that.deep; + this.lockOwner = that.lockOwner; + this.timeoutHint = that.timeoutHint; + this.timeoutTime = that.timeoutTime; + } + + /** + * Return the lock token associated with this lock. + * + * @return lock token + */ + public String getLockToken() { + String uuid = id.toString(); + return uuid + "-" + getLockTokenCheckDigit(uuid); + } + + /** + * Return the ID of the lock holding node + * @return the id + */ + public NodeId getId() { + return id; + } + + /** + * Return the lock owner. + * + * @return lock owner + */ + public String getLockOwner() { + return lockOwner; + } + + /** + * Return a flag indicating whether the lock is deep. + * + * @return true if the lock is deep; + * false otherwise + */ + public boolean isDeep() { + return deep; + } + + /** + * Return a flag indicating whether the session given is lock holder. + * + * @param session session to compare with + */ + public boolean isLockHolder(Session session) { + return lockHolder == session; + } + + /** + * Return the session currently holding the lock + * + * @return session currently holding the lock + */ + public SessionImpl getLockHolder() { + return lockHolder; + } + + /** + * Set the session currently holding the lock + * + * @param lockHolder session currently holding the lock + */ + public void setLockHolder(SessionImpl lockHolder) { + this.lockHolder = lockHolder; + } + + /** + * Return a flag indicating whether the lock is live + * + * @return true if the lock is live; otherwise false + */ + public boolean isLive() { + return live; + } + + /** + * Set the live flag + * + * @param live live flag + */ + public void setLive(boolean live) { + this.live = live; + } + + /** + * Return a flag indicating whether the lock information may still change. + * + * @return true if the lock is still alive. + */ + public boolean mayChange() { + return live; + } + + /** + * Return a flag indicating whether the lock is session-scoped + * + * @return true if the lock is session-scoped; + * otherwise false + */ + public boolean isSessionScoped() { + return sessionScoped; + } + + /** + * Returns the timeout hint given when the lock was created. + * + * @return timeout hint (in seconds) + */ + public long getTimeoutHint() { + return timeoutHint; + } + + /** + * Returns the time when this lock will expire. + * + * @return timeout time in milliseconds since the epoch + */ + public long getTimeoutTime() { + return timeoutTime; + } + + public boolean isExpired() { + return timeoutTime != Long.MAX_VALUE + && System.currentTimeMillis() > timeoutTime; + } + + /** + * Updates the timeout time of this lock based on current time and + * the timeout hint specified for this lock. The timeout time is always + * rounded up. + */ + public void updateTimeoutTime() { + if (timeoutHint > 0 && timeoutHint <= MAXIMUM_TIMEOUT) { + long now = System.currentTimeMillis(); + this.timeoutTime = now + timeoutHint * 1000; + } else { + this.timeoutTime = Long.MAX_VALUE; + } + } + + /** + * Utility method that throws a {@link LockException} with the + * "failure node path" set to the path of the node that holds this lock. + * The given session is used to resolve the path of the lock holder node. + * + * @param message exception message + * @param session session that the user was using for the failing operation + * @throws LockException always thrown, unless another error occurs + * @throws RepositoryException if the path of this lock can not be resolved + */ + public void throwLockException(String message, SessionImpl session) + throws LockException, RepositoryException { + String path; + try { + path = session.getJCRPath( + session.getHierarchyManager().getPath(id)); + } catch (RepositoryException ignored) { + path = null; + } + if (path != null) { + throw new LockException( + message + " (lock held by node " + path + ")", null, path); + } else { + throw new LockException(message); + } + } + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuilder buffer = new StringBuilder(); + buffer.append('('); + if (deep) { + buffer.append("deep "); + } + if (sessionScoped) { + buffer.append("session "); + } + buffer.append("holder:"); + if (lockHolder != null) { + buffer.append(lockHolder.getUserID()).append(' '); + } else { + buffer.append("none "); + } + buffer.append("owner:").append(lockOwner); + buffer.append(')'); + return buffer.toString(); + } + + /** + * Parse a lock token string representation and return the lock + * holder node id. + * + * @param token string representation of lock token + * @return lock holder node id + * @throws IllegalArgumentException if some field is illegal + */ + public static NodeId parseLockToken(String token) + throws IllegalArgumentException { + int sep = token.lastIndexOf('-'); + if (sep == -1 || sep == token.length() - 1) { + throw new IllegalArgumentException("Separator not found. Token [" + token + "]"); + } + String uuid = token.substring(0, sep); + if (getLockTokenCheckDigit(uuid) != token.charAt(token.length() - 1)) { + throw new IllegalArgumentException("Bad check digit. Token [" + token + "]"); + } + return NodeId.valueOf(uuid); + } + + /** + * Return the check digit for a lock token, given by its UUID + * @param uuid uuid + * @return check digit + */ + private static char getLockTokenCheckDigit(String uuid) { + int result = 0; + + int multiplier = 36; + for (int i = 0; i < uuid.length(); i++) { + char c = uuid.charAt(i); + if (c >= '0' && c <= '9') { + int num = c - '0'; + result += multiplier * num; + multiplier--; + } else if (c >= 'A' && c <= 'F') { + int num = c - 'A' + 10; + result += multiplier * num; + multiplier--; + } else if (c >= 'a' && c <= 'f') { + int num = c - 'a' + 10; + result += multiplier * num; + multiplier--; + } + } + + int rem = result % 37; + if (rem != 0) { + rem = 37 - rem; + } + if (rem >= 0 && rem <= 9) { + return (char) ('0' + rem); + } else if (rem >= 10 && rem <= 35) { + return (char) ('A' + rem - 10); + } else { + return '+'; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockManager.java new file mode 100644 index 00000000000..1099240fc31 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockManager.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.core.lock; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +/** + * Defines the functionality needed for locking and unlocking nodes. + */ +public interface LockManager { + + /** + * Lock a node. Checks whether the node is not locked and then + * returns a lock object for this node. + * @param node node + * @param isDeep whether the lock applies to this node only + * @param isSessionScoped whether the lock is session scoped + * @return lock object + * @throws LockException if this node already is locked, or some descendant + * node is locked and isDeep is true + * @see javax.jcr.Node#lock + */ + Lock lock(NodeImpl node, boolean isDeep, boolean isSessionScoped) + throws LockException, RepositoryException; + + /** + * Lock a node. Checks whether the node is not locked and then + * returns a lock object for this node. + * + * @param node Node to create the lock for. + * @param isDeep whether the lock applies to this node only + * @param isSessionScoped whether the lock is session scoped + * @param timeoutHint Desired lock timeout in seconds. + * @param ownerInfo Optional string acting as information about the owner. + * @return the lock. + * @throws LockException if this node already is locked, or some descendant + * node is locked and isDeep is true + * @see javax.jcr.lock.LockManager#lock(String, boolean, boolean, long, String) + * @throws RepositoryException + */ + Lock lock(NodeImpl node, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerInfo) + throws LockException, RepositoryException; + + /** + * Returns the Lock object that applies to a node. This may be either a lock + * on this node itself or a deep lock on a node above this node. + * @param node node + * @return lock object + * @throws LockException if this node is not locked + * @see javax.jcr.Node#getLock + */ + Lock getLock(NodeImpl node) throws LockException, RepositoryException; + + /** + * Returns all locks owned by the specified session. + * @param session session + * @return an array of lock objects + * @throws RepositoryException if an error occurs + * @see SessionImpl#getLocks + */ + Lock[] getLocks(SessionImpl session) throws RepositoryException; + + /** + * Removes the lock on a node given by its path. + * @param node node + * @throws LockException if this node is not locked or the session + * does not have the correct lock token + * @see javax.jcr.Node#unlock + */ + void unlock(NodeImpl node) throws LockException, RepositoryException; + + /** + * Returns true if the node given holds a lock; + * otherwise returns false. + * @param node node + * @return true if the node given holds a lock; + * otherwise returns false + * @see javax.jcr.Node#holdsLock + * @throws javax.jcr.RepositoryException If an exception occurs. + */ + boolean holdsLock(NodeImpl node) throws RepositoryException; + + /** + * Returns true if this node is locked either as a result + * of a lock held by this node or by a deep lock on a node above this + * node; otherwise returns false + * @param node node + * @return true if this node is locked either as a result + * of a lock held by this node or by a deep lock on a node above this + * node; otherwise returns false + * @see javax.jcr.Node#isLocked + * @throws javax.jcr.RepositoryException If an exception occurs. + */ + boolean isLocked(NodeImpl node) throws RepositoryException; + + /** + * Check whether the node given is locked by somebody else than the + * current session. Access is allowed if the node is not locked or + * if the session itself holds the lock to this node, i.e. the session + * contains the lock token for the lock. + * @param node node to check + * @throws LockException if write access to the specified node is not allowed + * @throws RepositoryException if some other error occurs + */ + void checkLock(NodeImpl node) + throws LockException, RepositoryException; + + /** + * Check whether the path given is locked by somebody else than the + * session described. Access is allowed if the node is not locked or + * if the session itself holds the lock to this node, i.e. the session + * contains the lock token for the lock. + * @param path path to check + * @param session session + * @throws LockException if write access to the specified path is not allowed + * @throws RepositoryException if some other error occurs + */ + void checkLock(Path path, Session session) + throws LockException, RepositoryException; + + /** + * Check whether a session is allowed to unlock a node. + * + * @throws LockException if unlocking is denied + * @throws RepositoryException if some other error occurs + */ + void checkUnlock(Session session, NodeImpl node) throws LockException, + RepositoryException; + + /** + * Invoked by a session to inform that a lock token has been added. + * + * @param session session that has a added lock token + * @param lt added lock token + * @throws LockException + * @throws RepositoryException + */ + void addLockToken(SessionImpl session, String lt) throws LockException, RepositoryException; + + /** + * Invoked by a session to inform that a lock token has been removed. + * + * @param session session that has a removed lock token + * @param lt removed lock token + * @throws LockException + * @throws RepositoryException + */ + void removeLockToken(SessionImpl session, String lt) throws LockException, RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockManagerImpl.java new file mode 100644 index 00000000000..e4e14718b29 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/LockManagerImpl.java @@ -0,0 +1,1415 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.lock; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; + +import org.apache.commons.collections.map.LinkedMap; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.SessionListener; +import org.apache.jackrabbit.core.WorkspaceImpl; +import org.apache.jackrabbit.core.cluster.ClusterOperation; +import org.apache.jackrabbit.core.cluster.LockEventChannel; +import org.apache.jackrabbit.core.cluster.LockEventListener; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.observation.EventImpl; +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.UpdatableItemStateManager; +import org.apache.jackrabbit.core.util.XAReentrantLock; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides the functionality needed for locking and unlocking nodes. + */ +public class LockManagerImpl + implements LockManager, SynchronousEventListener, LockEventListener { + + /** + * Logger + */ + private static final Logger log = LoggerFactory.getLogger(LockManagerImpl.class); + + /** + * Name of the lock file + */ + private static final String LOCKS_FILE = "locks"; + + /** + * Path map containing all locks at the leaves. + */ + private final PathMap lockMap = new PathMap(); + + /** + * XA/Thread aware lock to path map. + */ + private final XAReentrantLock lockMapLock = new XAReentrantLock(); + + /** + * XA/Thread aware lock for lock properties + */ + private XAReentrantLock lockPropertiesLock = new XAReentrantLock(); + + /** + * The periodically invoked lock timeout handler. + */ + private final ScheduledFuture timeoutHandler; + + /** + * System session + */ + private final SessionImpl sysSession; + + /** + * Locks file + */ + private final FileSystemResource locksFile; + + /** + * Flag indicating whether automatic saving is disabled. + */ + private boolean savingDisabled; + + /** + * Lock event channel. + */ + private LockEventChannel eventChannel; + + /** + * Create a new instance of this class. + * + * @param session system session + * @param fs file system for persisting locks + * @param executor scheduled executor service for handling lock timeouts + * @throws RepositoryException if an error occurs + */ + public LockManagerImpl( + SessionImpl session, FileSystem fs, + ScheduledExecutorService executor) throws RepositoryException { + + this.sysSession = session; + this.locksFile = new FileSystemResource(fs, FileSystem.SEPARATOR + LOCKS_FILE); + + session.getWorkspace().getObservationManager(). + addEventListener(this, Event.NODE_ADDED | Event.NODE_REMOVED, + "/", true, null, null, true); + + try { + if (locksFile.exists()) { + load(); + } + } catch (FileSystemException e) { + throw new RepositoryException("I/O error while reading locks from '" + + locksFile.getPath() + "'", e); + } + + timeoutHandler = executor.scheduleWithFixedDelay( + new TimeoutHandler(), 1, 1, TimeUnit.SECONDS); + } + + /** + * Close this lock manager. Writes back all changes. + */ + public void close() { + timeoutHandler.cancel(false); + save(); + } + + /** + * Periodically (at one second delay) invoked timeout handler. Traverses + * all locks and unlocks those that have expired. + * + * @see JCR-1590: + * JSR 283: Locking + */ + private class TimeoutHandler implements Runnable { + private final TimeoutHandlerVisitor visitor = new TimeoutHandlerVisitor(); + + public void run() { + lockMap.traverse(visitor, false); + } + } + + private class TimeoutHandlerVisitor implements + PathMap.ElementVisitor { + public void elementVisited(PathMap.Element element) { + LockInfo info = element.get(); + if (info != null && info.isLive() && info.isExpired()) { + NodeId id = info.getId(); + SessionImpl holder = info.getLockHolder(); + if (holder == null) { + info.setLockHolder(sysSession); + holder = sysSession; + } + try { + // FIXME: This session access is not thread-safe! + log.debug("Try to unlock expired lock. NodeId {}", id); + unlock(holder.getNodeById(id)); + } catch (RepositoryException e) { + log.warn("Unable to expire the lock. NodeId " + id, e); + } + } + } + } + + /** + * Read locks from locks file and populate path map + */ + private void load() throws FileSystemException { + BufferedReader reader = null; + + try { + reader = new BufferedReader( + new InputStreamReader(locksFile.getInputStream())); + while (true) { + String s = reader.readLine(); + if (s == null || s.equals("")) { + break; + } + reapplyLock(s); + } + } catch (IOException e) { + throw new FileSystemException("error while reading locks file", e); + } finally { + IOUtils.closeQuietly(reader); + } + } + + /** + * Reaply a lock given a lock token that was read from the locks file + * + * @param lockTokenLine lock token to apply + */ + private void reapplyLock(String lockTokenLine) { + String[] parts = lockTokenLine.split(","); + String token = parts[0]; + long timeoutHint = Long.MAX_VALUE; + if (parts.length > 1) { + try { + timeoutHint = Long.parseLong(parts[1]); + } catch (NumberFormatException e) { + log.warn("Unexpected timeout hint " + + parts[1] + " for lock token " + token, e); + } + } + + try { + acquire(); + + NodeId id = LockInfo.parseLockToken(parts[0]); + NodeImpl node = (NodeImpl) sysSession.getItemManager().getItem(id); + Path path = getPath(sysSession, id); + + InternalLockInfo info = new InternalLockInfo( + id, false, + node.getProperty(NameConstants.JCR_LOCKISDEEP).getBoolean(), + node.getProperty(NameConstants.JCR_LOCKOWNER).getString(), + timeoutHint); + info.setLive(true); + lockMap.put(path, info); + } catch (RepositoryException e) { + log.warn("Unable to recreate lock '" + token + "': " + e.getMessage()); + log.debug("Root cause: ", e); + } finally { + release(); + } + } + + /** + * Write locks to locks file + */ + private void save() { + if (savingDisabled) { + return; + } + + final ArrayList list = new ArrayList(); + + lockMap.traverse(new PathMap.ElementVisitor() { + public void elementVisited(PathMap.Element element) { + LockInfo info = element.get(); + if (!info.isSessionScoped()) { + list.add(info); + } + } + }, false); + + + BufferedWriter writer = null; + + try { + writer = new BufferedWriter( + new OutputStreamWriter(locksFile.getOutputStream())); + for (LockInfo info : list) { + writer.write(info.getLockToken()); + + // Store the timeout hint, if one is specified + if (info.getTimeoutHint() != Long.MAX_VALUE) { + writer.write(','); + writer.write(Long.toString(info.getTimeoutHint())); + } + + writer.newLine(); + } + } catch (FileSystemException fse) { + log.warn("I/O error while saving locks to '" + + locksFile.getPath() + "': " + fse.getMessage()); + log.debug("Root cause: ", fse); + } catch (IOException ioe) { + log.warn("I/O error while saving locks to '" + + locksFile.getPath() + "': " + ioe.getMessage()); + log.debug("Root cause: ", ioe); + } finally { + IOUtils.closeQuietly(writer); + } + } + + static SessionLockManager getSessionLockManager(SessionImpl session) throws RepositoryException { + Workspace wsp = session.getWorkspace(); + return (SessionLockManager) wsp.getLockManager(); + } + + /** + * Internal lock implementation that takes the same parameters + * as the public method. + * + * @param node node to lock + * @param isDeep whether the lock applies to this node only + * @param isSessionScoped whether the lock is session scoped + * @param timeoutHint + * @param ownerInfo + * @return lock + * @throws LockException if the node is already locked + * @throws RepositoryException if another error occurs + */ + LockInfo internalLock( + NodeImpl node, boolean isDeep, boolean isSessionScoped, + long timeoutHint, String ownerInfo) + throws LockException, RepositoryException { + + SessionImpl session = (SessionImpl) node.getSession(); + String lockOwner = (ownerInfo != null) ? ownerInfo : session.getUserID(); + InternalLockInfo info = new InternalLockInfo( + node.getNodeId(), isSessionScoped, isDeep, lockOwner, timeoutHint); + + ClusterOperation operation = null; + boolean successful = false; + + // Cluster is only informed about open-scoped locks + if (eventChannel != null && !isSessionScoped) { + operation = eventChannel.create(node.getNodeId(), isDeep, lockOwner); + } + + acquire(); + + try { + // check whether node is already locked + Path path = getPath(session, node.getId()); + PathMap.Element element = lockMap.map(path, false); + + LockInfo other = element.get(); + if (other != null) { + if (element.hasPath(path)) { + other.throwLockException( + "Node already locked: " + node, session); + } else if (other.isDeep()) { + other.throwLockException( + "Parent node has a deep lock: " + node, session); + } + } + if (info.isDeep() && element.hasPath(path) + && element.getChildrenCount() > 0) { + info.throwLockException("Some child node is locked", session); + } + + // create lock token + info.setLockHolder(session); + info.setLive(true); + session.addListener(info); + if (!info.isSessionScoped()) { + getSessionLockManager(session).lockTokenAdded(info.getLockToken()); + } + lockMap.put(path, info); + + if (!info.isSessionScoped()) { + save(); + successful = true; + } + return info; + + } finally { + release(); + if (operation != null) { + operation.ended(successful); + } + } + } + + /** + * Unlock a node (internal implementation) + * @param node node to unlock + * @throws LockException if the node can not be unlocked + * @throws RepositoryException if another error occurs + */ + boolean internalUnlock(NodeImpl node) + throws LockException, RepositoryException { + + ClusterOperation operation = null; + boolean successful = false; + + if (eventChannel != null) { + operation = eventChannel.create(node.getNodeId()); + } + + acquire(); + + try { + SessionImpl session = (SessionImpl) node.getSession(); + // check whether node is locked by this session + PathMap.Element element = + lockMap.map(getPath(session, node.getId()), true); + if (element == null) { + throw new LockException("Node not locked: " + node); + } + LockInfo info = element.get(); + if (info == null) { + throw new LockException("Node not locked: " + node); + } + checkUnlock(info, session); + + getSessionLockManager(session).lockTokenRemoved(info.getLockToken()); + + element.set(null); + info.setLive(false); + + if (!info.isSessionScoped()) { + save(); + successful = true; + } + return true; + } finally { + release(); + + if (operation != null) { + operation.ended(successful); + } + } + } + + /** + * Package-private low-level helper method returning all locks + * associated with the specified session. + * @param session session + * @return an array of AbstractLockInfos + */ + LockInfo[] getLockInfos(final SessionImpl session) { + final ArrayList infos = new ArrayList(); + lockMap.traverse(new PathMap.ElementVisitor() { + public void elementVisited(PathMap.Element element) { + LockInfo info = element.get(); + if (info.isLive() && info.isLockHolder(session)) { + infos.add(info); + } + } + }, false); + return infos.toArray(new LockInfo[infos.size()]); + } + + /** + * Helper method that copies all the active open-scoped locks from the + * given source to this lock manager. This method is used when backing + * up repositories, and only works correctly when the source lock manager + * belongs to the original copy of the workspace being backed up. + * + * @see org.apache.jackrabbit.core.RepositoryCopier + * @param source source lock manager + */ + public void copyOpenScopedLocksFrom(LockManagerImpl source) { + source.lockMap.traverse(new PathMap.ElementVisitor() { + public void elementVisited(PathMap.Element element) { + LockInfo info = element.get(); + if (info.isLive() && !info.isSessionScoped()) { + try { + lockMap.put(element.getPath(), info); + } catch (MalformedPathException e) { + log.warn("Ignoring invalid lock path: " + info, e); + } + } + } + }, false); + } + + /** + * Return the most appropriate lock information for a node. This is either + * the lock info for the node itself, if it is locked, or a lock info for one + * of its parents, if that is deep locked. + * @return lock info or null if node is not locked + * @throws RepositoryException if an error occurs + */ + public LockInfo getLockInfo(NodeId id) throws RepositoryException { + Path path; + try { + path = getPath(sysSession, id); + } catch (ItemNotFoundException e) { + return null; + } + + acquire(); + try { + PathMap.Element element = lockMap.map(path, false); + LockInfo info = element.get(); + if (info != null) { + if (element.hasPath(path) || info.isDeep()) { + return info; + } + } + return null; + } finally { + release(); + } + } + + //----------------------------------------------------------< LockManager > + + /** + * {@inheritDoc} + */ + public Lock lock(NodeImpl node, boolean isDeep, boolean isSessionScoped) + throws LockException, RepositoryException { + return lock(node, isDeep, isSessionScoped, Long.MAX_VALUE, null); + } + + public Lock lock(NodeImpl node, boolean isDeep, boolean isSessionScoped, long timoutHint, String ownerInfo) + throws LockException, RepositoryException { + LockInfo info = internalLock(node, isDeep, isSessionScoped, timoutHint, ownerInfo); + writeLockProperties(node, info.getLockOwner(), info.isDeep()); + + return new LockImpl(info, node); + } + + /** + * {@inheritDoc} + */ + public Lock getLock(NodeImpl node) + throws LockException, RepositoryException { + + acquire(); + + try { + SessionImpl session = (SessionImpl) node.getSession(); + Path path = getPath(session, node.getId()); + + PathMap.Element element = lockMap.map(path, false); + LockInfo info = element.get(); + if (info != null && (element.hasPath(path) || info.isDeep())) { + NodeImpl lockHolder = (NodeImpl) + session.getItemManager().getItem(info.getId()); + return new LockImpl(info, lockHolder); + } else { + throw new LockException("Node not locked: " + node); + } + } catch (ItemNotFoundException e) { + throw new LockException("Node not locked: " + node); + } finally { + release(); + } + } + + /** + * {@inheritDoc} + */ + public Lock[] getLocks(SessionImpl session) throws RepositoryException { + + acquire(); + + LockInfo[] infos = getLockInfos(session); + + try { + Lock[] locks = new Lock[infos.length]; + for (int i = 0; i < infos.length; i++) { + NodeImpl holder = (NodeImpl) + session.getItemManager().getItem(infos[i].getId()); + locks[i] = new LockImpl(infos[i], holder); + } + return locks; + } finally { + release(); + } + } + + /** + * {@inheritDoc} + *

    + * In order to prevent deadlocks from within the synchronous dispatching of + * events, content modifications should not be made from within code + * sections that hold monitors. (see #JCR-194) + */ + public void unlock(NodeImpl node) throws LockException, RepositoryException { + removeLockProperties(node); + internalUnlock(node); + } + + /** + * {@inheritDoc} + */ + public boolean holdsLock(NodeImpl node) throws RepositoryException { + acquire(); + + try { + SessionImpl session = (SessionImpl) node.getSession(); + PathMap.Element element = + lockMap.map(getPath(session, node.getId()), true); + if (element == null) { + return false; + } + return element.get() != null; + } catch (ItemNotFoundException e) { + return false; + } finally { + release(); + } + } + + /** + * {@inheritDoc} + */ + public boolean isLocked(NodeImpl node) throws RepositoryException { + acquire(); + + try { + SessionImpl session = (SessionImpl) node.getSession(); + Path path = getPath(session, node.getId()); + + PathMap.Element element = lockMap.map(path, false); + LockInfo info = element.get(); + if (info == null) { + return false; + } + if (element.hasPath(path)) { + return true; + } else { + return info.isDeep(); + } + } catch (ItemNotFoundException e) { + return false; + } finally { + release(); + } + } + + /** + * {@inheritDoc} + */ + public void checkLock(NodeImpl node) + throws LockException, RepositoryException { + + SessionImpl session = (SessionImpl) node.getSession(); + checkLock(getPath(session, node.getId()), session); + } + + /** + * {@inheritDoc} + */ + public void checkLock(Path path, Session session) + throws LockException, RepositoryException { + + acquire(); + try { + PathMap.Element element = lockMap.map(path, false); + LockInfo info = element.get(); + if (info != null) { + if (element.hasPath(path) || info.isDeep()) { + checkLock(info, session); + } + } + } finally { + release(); + } + } + + /** + * Check whether a lock info allows access to a session. May be overridden + * by subclasses to allow access to nodes for sessions other than the + * lock holder itself. + *

    + * Default implementation allows access to the lock holder only. + * + * @param info info to check + * @param session session + * @throws LockException if write access to the specified path is not allowed + * @throws RepositoryException if some other error occurs + */ + protected void checkLock(LockInfo info, Session session) + throws LockException, RepositoryException { + + if (!info.isLockHolder(session)) { + throw new LockException("Node locked."); + } + } + + /** + * {@inheritDoc} + */ + public void checkUnlock(Session session, NodeImpl node) + throws LockException, RepositoryException { + acquire(); + + try { + // check whether node is locked by this session + PathMap.Element element = + lockMap.map(getPath((SessionImpl) session, node.getId()), true); + if (element == null) { + throw new LockException("Node not locked: " + node); + } + LockInfo info = element.get(); + if (info == null) { + throw new LockException("Node not locked: " + node); + } + checkUnlock(info, session); + } finally { + release(); + } + } + + /** + * Check whether a session is allowed to unlock a node. May be overridden + * by subclasses to allow this to sessions other than the lock holder + * itself. + *

    + * Default implementation allows unlocking to the lock holder only. + * + * @param info info to check + * @param session session + * @throws LockException if unlocking is denied + * @throws RepositoryException if some other error occurs + */ + protected void checkUnlock(LockInfo info, Session session) + throws LockException, RepositoryException { + + if (!info.isLockHolder(session)) { + throw new LockException("Node not locked by session: " + + info.getId()); + } + } + + /** + * {@inheritDoc} + */ + public void addLockToken(SessionImpl session, String lt) throws LockException, RepositoryException { + try { + acquire(); + + NodeId id = LockInfo.parseLockToken(lt); + + NodeImpl node = (NodeImpl) sysSession.getItemManager().getItem(id); + Path path = node.getPrimaryPath(); + PathMap.Element element = lockMap.map(path, true); + if (element != null) { + LockInfo info = element.get(); + if (info != null && !info.isLockHolder(session)) { + if (info.getLockHolder() == null) { + info.setLockHolder(session); + if (info instanceof InternalLockInfo) { + session.addListener((InternalLockInfo) info); + } + } else { + String msg = "Cannot add lock token: lock already held by other session."; + log.warn(msg); + info.throwLockException(msg, session); + } + } + } + // inform SessionLockManager + getSessionLockManager(session).lockTokenAdded(lt); + } catch (IllegalArgumentException e) { + String msg = "Bad lock token: " + e.getMessage(); + log.warn(msg); + throw new LockException(msg); + } finally { + release(); + } + } + + /** + * {@inheritDoc} + */ + public void removeLockToken(SessionImpl session, String lt) + throws LockException, RepositoryException { + + try { + acquire(); + + NodeId id = LockInfo.parseLockToken(lt); + + NodeImpl node = (NodeImpl) sysSession.getItemManager().getItem(id); + PathMap.Element element = + lockMap.map(node.getPrimaryPath(), true); + if (element != null) { + LockInfo info = element.get(); + if (info != null) { + if (info.isLockHolder(session)) { + info.setLockHolder(null); + } else if (info.getLockHolder() != null) { + String msg = "Cannot remove lock token: lock held by other session."; + log.warn(msg); + info.throwLockException(msg, session); + } + } + } + // inform SessionLockManager + getSessionLockManager(session).lockTokenRemoved(lt); + } catch (IllegalArgumentException e) { + String msg = "Bad lock token: " + e.getMessage(); + log.warn(msg); + throw new LockException(msg); + } finally { + release(); + } + } + + /** + * Return the path of an item given its id. This method will lookup the + * item inside the system session. + */ + private Path getPath(SessionImpl session, ItemId id) throws RepositoryException { + return session.getHierarchyManager().getPath(id); + } + + /** + * Acquire lock on the lock map. + */ + private void acquire() { + for (;;) { + try { + lockMapLock.acquire(); + break; + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Release lock on the lock map. + */ + private void release() { + lockMapLock.release(); + } + + /** + * Acquire lock for modifying lock properties + */ + private void acquireLockPropertiesLock() { + for (;;) { + try { + lockPropertiesLock.acquire(); + break; + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Release lock on the lockPropertiesLock. + */ + private void releaseLockPropertiesLock() { + lockPropertiesLock.release(); + } + + /** + * Start an update operation. This will acquire the lock on the lock map + * and disable saving the lock map file. + */ + public void beginUpdate() { + acquire(); + savingDisabled = true; + } + + /** + * End an update operation. This will save the lock map file and release + * the lock on the lock map. + */ + public void endUpdate() { + savingDisabled = false; + save(); + release(); + } + + /** + * Cancel an update operation. This will release the lock on the lock map. + */ + public void cancelUpdate() { + savingDisabled = false; + release(); + } + + /** + * Add the lock related properties to the target node. + * + * @param node + * @param lockOwner + * @param isDeep + */ + protected void writeLockProperties(NodeImpl node, String lockOwner, boolean isDeep) throws RepositoryException { + boolean success = false; + + SessionImpl editingSession = (SessionImpl) node.getSession(); + WorkspaceImpl wsp = (WorkspaceImpl) editingSession.getWorkspace(); + UpdatableItemStateManager stateMgr = wsp.getItemStateManager(); + + try { + acquireLockPropertiesLock(); + + if (stateMgr.inEditMode()) { + throw new RepositoryException("Unable to write lock properties."); + } + stateMgr.edit(); + try { + // add properties to content + NodeId nodeId = node.getNodeId(); + NodeState nodeState = (NodeState) stateMgr.getItemState(nodeId); + + PropertyState propState; + if (!nodeState.hasPropertyName(NameConstants.JCR_LOCKOWNER)) { + propState = stateMgr.createNew(NameConstants.JCR_LOCKOWNER, nodeId); + propState.setType(PropertyType.STRING); + propState.setMultiValued(false); + } else { + propState = (PropertyState) stateMgr.getItemState(new PropertyId(nodeId, NameConstants.JCR_LOCKOWNER)); + } + propState.setValues(new InternalValue[] { InternalValue.create(lockOwner) }); + nodeState.addPropertyName(NameConstants.JCR_LOCKOWNER); + stateMgr.store(nodeState); + + if (!nodeState.hasPropertyName(NameConstants.JCR_LOCKISDEEP)) { + propState = stateMgr.createNew(NameConstants.JCR_LOCKISDEEP, nodeId); + propState.setType(PropertyType.BOOLEAN); + propState.setMultiValued(false); + } else { + propState = (PropertyState) stateMgr.getItemState(new PropertyId(nodeId, NameConstants.JCR_LOCKISDEEP)); + } + propState.setValues(new InternalValue[] { InternalValue.create(isDeep) }); + nodeState.addPropertyName(NameConstants.JCR_LOCKISDEEP); + stateMgr.store(nodeState); + + stateMgr.update(); + success = true; + } catch (ItemStateException e) { + throw new RepositoryException("Error while creating lock.", e); + } finally { + if (!success) { + // failed to set lock meta-data content, cleanup + stateMgr.cancel(); + try { + unlock(node); + } catch (RepositoryException e) { + // cleanup failed + log.error("error while cleaning up after failed lock attempt", e); + } + } + } + } finally { + releaseLockPropertiesLock(); + } + } + + /** + * + * @param node + * @throws RepositoryException + */ + protected void removeLockProperties(NodeImpl node) throws RepositoryException { + boolean success = false; + + SessionImpl editingSession = (SessionImpl) node.getSession(); + WorkspaceImpl wsp = (WorkspaceImpl) editingSession.getWorkspace(); + UpdatableItemStateManager stateMgr = wsp.getItemStateManager(); + + try { + acquireLockPropertiesLock(); + + // add properties to content + if (stateMgr.inEditMode()) { + throw new RepositoryException("Unable to remove lock properties."); + } + stateMgr.edit(); + try { + NodeId nodeId = node.getNodeId(); + NodeState nodeState = (NodeState) stateMgr.getItemState(nodeId); + if (nodeState.hasPropertyName(NameConstants.JCR_LOCKOWNER)) { + PropertyState propState = (PropertyState) stateMgr.getItemState(new PropertyId(nodeId, NameConstants.JCR_LOCKOWNER)); + nodeState.removePropertyName(NameConstants.JCR_LOCKOWNER); + stateMgr.destroy(propState); + stateMgr.store(nodeState); + } + + if (nodeState.hasPropertyName(NameConstants.JCR_LOCKISDEEP)) { + PropertyState propState = (PropertyState) stateMgr.getItemState(new PropertyId(nodeId, NameConstants.JCR_LOCKISDEEP)); + nodeState.removePropertyName(NameConstants.JCR_LOCKISDEEP); + stateMgr.destroy(propState); + stateMgr.store(nodeState); + } + + stateMgr.update(); + success = true; + } catch (ItemStateException e) { + throw new RepositoryException("Error while removing lock.", e); + } finally { + if (!success) { + // failed to set lock meta-data content, cleanup + stateMgr.cancel(); + } + } + } finally { + releaseLockPropertiesLock(); + } + } + + //----------------------------------------------< SynchronousEventListener > + + /** + * Internal event class that holds old and new paths for moved nodes + */ + private static class HierarchyEvent { + + /** + * ID recorded in event + */ + private final NodeId id; + + /** + * Path recorded in event + */ + private final Path path; + + /** + * Old path in move operation + */ + private Path oldPath; + + /** + * New path in move operation + */ + private Path newPath; + + /** + * Event type, may be {@link Event#NODE_ADDED}, + * {@link Event#NODE_REMOVED} or a combination of both + */ + private int type; + + /** + * Create a new instance of this class. + * + * @param id id + * @param path path + * @param type event type + */ + public HierarchyEvent(NodeId id, Path path, int type) { + this.id = id; + this.path = path; + this.type = type; + } + + /** + * Merge this event with another event. The result will be stored in + * this event + * + * @param event other event to merge with + */ + public void merge(HierarchyEvent event) { + type |= event.type; + if (event.type == Event.NODE_ADDED) { + newPath = event.path; + oldPath = path; + } else { + oldPath = event.path; + newPath = path; + } + } + + /** + * Return the old path if this is a move operation + * + * @return old path + */ + public Path getOldPath() { + return oldPath; + } + + /** + * Return the new path if this is a move operation + * + * @return new path + */ + public Path getNewPath() { + return newPath; + } + } + + /** + * {@inheritDoc} + */ + public void onEvent(EventIterator events) { + Iterator iter = consolidateEvents(events); + while (iter.hasNext()) { + HierarchyEvent event = iter.next(); + if (event.type == Event.NODE_ADDED) { + nodeAdded(event.path); + } else if (event.type == Event.NODE_REMOVED) { + nodeRemoved(event.path); + } else if (event.type == (Event.NODE_ADDED | Event.NODE_REMOVED)) { + nodeMoved(event.getOldPath(), event.getNewPath()); + } + } + } + + /** + * Consolidate an event iterator obtained from observation, merging + * add and remove operations on nodes with the same UUID into a move + * operation. + */ + @SuppressWarnings("unchecked") + private Iterator consolidateEvents(EventIterator events) { + LinkedMap eventMap = new LinkedMap(); + + while (events.hasNext()) { + EventImpl event = (EventImpl) events.nextEvent(); + HierarchyEvent he; + + try { + he = new HierarchyEvent(event.getChildId(), + sysSession.getQPath(event.getPath()).getNormalizedPath(), + event.getType()); + } catch (MalformedPathException e) { + log.info("Unable to get event's path: " + e.getMessage()); + continue; + } catch (RepositoryException e) { + log.info("Unable to get event's path: " + e.getMessage()); + continue; + } + + HierarchyEvent heExisting = (HierarchyEvent) eventMap.get(he.id); + if (heExisting != null) { + heExisting.merge(he); + } else { + eventMap.put(he.id, he); + } + } + return eventMap.values().iterator(); + } + + /** + * Refresh a non-empty path element whose children might have changed + * its position. + */ + private void refresh(PathMap.Element element) { + final ArrayList infos = new ArrayList(); + boolean needsSave = false; + + // save away non-empty children + element.traverse(new PathMap.ElementVisitor() { + public void elementVisited(PathMap.Element element) { + infos.add(element.get()); + } + }, false); + + // remove all children + element.removeAll(); + + // now re-insert at appropriate location or throw away if node + // does no longer exist + for (int i = 0; i < infos.size(); i++) { + LockInfo info = infos.get(i); + try { + acquire(); + + NodeImpl node = (NodeImpl) sysSession.getItemManager().getItem( + info.getId()); + lockMap.put(node.getPrimaryPath(), info); + } catch (RepositoryException e) { + info.setLive(false); + if (!info.isSessionScoped()) { + needsSave = true; + } + } finally { + release(); + } + } + + // save if required + if (needsSave) { + save(); + } + } + + /** + * Invoked when some node has been added. If the parent of that node + * exists, shift all name siblings of the new node having an index greater + * or equal. + * + * @param path path of added node + */ + private void nodeAdded(Path path) { + acquire(); + + try { + PathMap.Element parent = + lockMap.map(path.getAncestor(1), true); + if (parent != null) { + refresh(parent); + } + } catch (RepositoryException e) { + log.warn("Unable to determine path of added node's parent.", e); + } finally { + release(); + } + } + + /** + * Invoked when some node has been moved. Relink the child inside our + * map to the new parent. + * + * @param oldPath old path + * @param newPath new path + */ + private void nodeMoved(Path oldPath, Path newPath) { + acquire(); + + try { + PathMap.Element parent = + lockMap.map(oldPath.getAncestor(1), true); + if (parent != null) { + refresh(parent); + } + } catch (RepositoryException e) { + log.warn("Unable to determine path of moved node's parent.", e); + } finally { + release(); + } + } + + /** + * Invoked when some node has been removed. Remove the child from our + * path map. Disable all locks contained in that subtree. + * + * @param path path of removed node + */ + private void nodeRemoved(Path path) { + acquire(); + + try { + PathMap.Element parent = + lockMap.map(path.getAncestor(1), true); + if (parent != null) { + refresh(parent); + } + } catch (RepositoryException e) { + log.warn("Unable to determine path of removed node's parent.", e); + } finally { + release(); + } + } + + /** + * Contains information about a lock and gets placed inside the child + * information of a {@link org.apache.jackrabbit.spi.commons.name.PathMap}. + */ + class InternalLockInfo extends LockInfo implements SessionListener { + + /** + * Create a new instance of this class. + * + * @param lockToken lock token + * @param sessionScoped whether lock token is session scoped + * @param deep whether lock is deep + * @param lockOwner owner of lock + * @param timeoutHint + */ + public InternalLockInfo(NodeId lockToken, boolean sessionScoped, + boolean deep, String lockOwner, long timeoutHint) { + super(lockToken, sessionScoped, deep, lockOwner, timeoutHint); + } + + /** + * {@inheritDoc} + *

    + * When the owning session is logging out, we have to perform some + * operations depending on the lock type. + * (1) If the lock was session-scoped, we unlock the node. + * (2) If the lock was open-scoped, we remove the lock token + * from the session and set the lockHolder field to null. + */ + public void loggingOut(SessionImpl session) { + if (isLive()) { + if (isSessionScoped()) { + // if no session currently holds lock, reassign + SessionImpl lockHolder = getLockHolder(); + if (lockHolder == null) { + setLockHolder(session); + } + try { + NodeImpl node = (NodeImpl) session.getItemManager().getItem(getId()); + node.unlock(); + } catch (RepositoryException e) { + // Session is not allowed/able to unlock. + // Use system session present with lock-mgr as fallback + // in order to make sure, that session-scoped locks are + // properly cleaned. + SessionImpl systemSession = LockManagerImpl.this.sysSession; + setLockHolder(systemSession); + try { + NodeImpl node = (NodeImpl) systemSession.getItemManager().getItem(getId()); + node.unlock(); + } catch (RepositoryException re) { + log.warn("Unable to remove session-scoped lock on node '" + getLockToken() + "': " + e.getMessage()); + log.debug("Root cause: ", e); + } + } + } else if (isLockHolder(session)) { + session.removeLockToken(getLockToken()); + setLockHolder(null); + } + } + } + + /** + * {@inheritDoc} + */ + public void loggedOut(SessionImpl session) { + } + } + + //----------------------------------------------------< LockEventListener > + + /** + * Set a lock event channel + * + * @param eventChannel lock event channel + */ + public void setEventChannel(LockEventChannel eventChannel) { + this.eventChannel = eventChannel; + eventChannel.setListener(this); + } + + /** + * {@inheritDoc} + */ + public void externalLock(NodeId nodeId, boolean isDeep, String lockOwner) throws RepositoryException { + acquire(); + + try { + Path path = getPath(sysSession, nodeId); + + // create lock token + InternalLockInfo info = new InternalLockInfo( + nodeId, false, isDeep, lockOwner, Long.MAX_VALUE); + info.setLive(true); + lockMap.put(path, info); + + save(); + } finally { + release(); + } + } + + /** + * {@inheritDoc} + */ + public void externalUnlock(NodeId nodeId) throws RepositoryException { + acquire(); + + try { + Path path = getPath(sysSession, nodeId); + PathMap.Element element = lockMap.map(path, true); + if (element == null) { + throw new LockException("Node not locked: " + path.toString()); + } + LockInfo info = element.get(); + if (info == null) { + throw new LockException("Node not locked: " + path.toString()); + } + element.set(null); + info.setLive(false); + + save(); + + } finally { + release(); + } + } + + /** + * Dump contents of path map and elements included to a string. + */ + public String toString() { + final StringBuilder builder = new StringBuilder(); + lockMap.traverse(new PathMap.ElementVisitor() { + public void elementVisited(PathMap.Element element) { + for (int i = 0; i < element.getDepth(); i++) { + builder.append("--"); + } + builder.append(element.getName()); + int index = element.getIndex(); + if (index != 0 && index != 1) { + builder.append('['); + builder.append(index); + builder.append(']'); + } + builder.append(" "); + builder.append(element.get()); + builder.append("\n"); + } + }, true); + return builder.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/SessionLockManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/SessionLockManager.java new file mode 100644 index 00000000000..f72eb7e537b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/SessionLockManager.java @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.lock; + +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.core.ItemValidator; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SessionLockManager implements the + * {@link javax.jcr.lock.LockManager}. In contrast + * to the internal {@link LockManager} interface that is created once + * for each WorkspaceInfo, the JSR 283 LockManager + * is associated with a single Session and its + * Workspace. + * + * @see javax.jcr.Workspace#getLockManager() + */ +public class SessionLockManager implements javax.jcr.lock.LockManager { + + private static Logger log = LoggerFactory.getLogger(SessionLockManager.class); + + /** + * Component context of the current session + */ + private final SessionContext context; + + /** + * Current session + */ + private final SessionImpl session; + + private final LockManager systemLockMgr; + + private final Set lockTokens = new HashSet(); + + /** + * Creates a lock manager. + * + * @param context component context of the current session + * @param systemLockMgr internal lock manager + */ + public SessionLockManager( + SessionContext context, LockManager systemLockMgr) { + this.context = context; + this.session = context.getSessionImpl(); + this.systemLockMgr = systemLockMgr; + } + + /** + * @see javax.jcr.lock.LockManager#getLockTokens() + */ + public String[] getLockTokens() throws RepositoryException { + synchronized (lockTokens) { + String[] result = new String[lockTokens.size()]; + lockTokens.toArray(result); + return result; + } + } + + /** + * @see javax.jcr.lock.LockManager#addLockToken(String) + */ + public void addLockToken(String lockToken) throws LockException, RepositoryException { + if (!lockTokens.contains(lockToken)) { + systemLockMgr.addLockToken(session, lockToken); + } else { + log.debug("Lock token already present with session -> no effect."); + } + } + + /** + * @see javax.jcr.lock.LockManager#removeLockToken(String) + */ + public void removeLockToken(String lockToken) throws LockException, RepositoryException { + if (lockTokens.contains(lockToken)) { + systemLockMgr.removeLockToken(session, lockToken); + } else { + throw new LockException("Lock token " + lockToken + " not present with session."); + } + } + + /** + * @see javax.jcr.lock.LockManager#isLocked(String) + */ + public boolean isLocked(String absPath) throws RepositoryException { + NodeImpl node = (NodeImpl) session.getNode(absPath); + /* + NOTE: with JSR 283 a transient node below a deep lock will report + islocked = true. therefore, the shortcut for NEW nodes that was + present with NodeImpl.isLocked before cannot be applied any more. + */ + if (node.isNew()) { + while (node.isNew()) { + node = (NodeImpl) node.getParent(); + } + return systemLockMgr.isLocked(node) && systemLockMgr.getLock(node).isDeep(); + } else { + return systemLockMgr.isLocked(node); + } + } + + /** + * @see javax.jcr.lock.LockManager#getLock(String) + */ + public Lock getLock(String absPath) throws + UnsupportedRepositoryOperationException, LockException, + AccessDeniedException, RepositoryException { + NodeImpl node = (NodeImpl) session.getNode(absPath); + if (node.isNew()) { + while (node.isNew()) { + node = (NodeImpl) node.getParent(); + } + Lock l = systemLockMgr.getLock(node); + if (l.isDeep()) { + return l; + } else { + throw new LockException("Node not locked: " + node); + } + } else { + return systemLockMgr.getLock(node); + } + } + + /** + * @see javax.jcr.lock.LockManager#holdsLock(String) + */ + public boolean holdsLock(String absPath) throws RepositoryException { + NodeImpl node = (NodeImpl) session.getNode(absPath); + /* Shortcut: + New nodes never hold or not-lockable nodes never hold a lock. */ + if (node.isNew() || !node.isNodeType(NameConstants.MIX_LOCKABLE)) { + return false; + } else { + return systemLockMgr.holdsLock(node); + } + } + + /** + * @see javax.jcr.lock.LockManager#lock(String, boolean, boolean, long, String) + */ + public Lock lock(String absPath, boolean isDeep, boolean isSessionScoped, + long timeoutHint, String ownerInfo) throws RepositoryException { + NodeImpl node = (NodeImpl) session.getNode(absPath); + + int options = ItemValidator.CHECK_HOLD | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE; + context.getItemValidator().checkModify(node, options, Permission.LOCK_MNGMT); + checkLockable(node); + + synchronized (systemLockMgr) { + return systemLockMgr.lock(node, isDeep, isSessionScoped, timeoutHint, ownerInfo); + } + } + + /** + * @see javax.jcr.lock.LockManager#unlock(String) + */ + public void unlock(String absPath) throws + UnsupportedRepositoryOperationException, LockException, + AccessDeniedException, InvalidItemStateException, + RepositoryException { + + NodeImpl node = (NodeImpl) session.getNode(absPath); + int options = ItemValidator.CHECK_HOLD | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE; + context.getItemValidator().checkModify(node, options, Permission.LOCK_MNGMT); + checkLockable(node); + + synchronized (systemLockMgr) { + // basic checks if unlock can be called on the node. + if (!systemLockMgr.holdsLock(node)) { + throw new LockException("Node not locked: " + node); + } + systemLockMgr.checkUnlock(session, node); + systemLockMgr.unlock(node); + } + } + + //-------------------------------------------------------------------------- + /** + * + * @param lockToken + * @return true if the token was successfully added to the set. + */ + boolean lockTokenAdded(String lockToken) { + synchronized (lockTokens) { + return lockTokens.add(lockToken); + } + } + + /** + * + * @param lockToken + * @return true if the token was successfully removed from the set. + */ + boolean lockTokenRemoved(String lockToken) { + synchronized (lockTokens) { + return lockTokens.remove(lockToken); + } + } + + /** + * Checks if the given node is lockable, i.e. has 'mix:lockable'. + * + * @param node + * @throws LockException if this node is not lockable + * @throws RepositoryException if another error occurs + */ + private static void checkLockable(NodeImpl node) throws LockException, RepositoryException { + if (!node.isNodeType(NameConstants.MIX_LOCKABLE)) { + String msg = "Unable to perform a locking operation on a non-lockable node: " + node.safeGetJCRPath(); + log.debug(msg); + throw new LockException(msg); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XAEnvironment.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XAEnvironment.java new file mode 100755 index 00000000000..9542408c416 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XAEnvironment.java @@ -0,0 +1,528 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.lock; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.WorkspaceImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.data.core.TransactionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import javax.jcr.lock.LockException; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; + +/** + * Encapsulates operations that happen in an XA environment. + */ +class XAEnvironment { + + /** + * Logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(XAEnvironment.class); + + private static final int STATUS_PREPARING = 1; + private static final int STATUS_PREPARED = 2; + private static final int STATUS_COMMITTING = 3; + private static final int STATUS_COMMITTED = 4; + private static final int STATUS_ROLLING_BACK = 5; + private static final int STATUS_ROLLED_BACK = 6; + + /** + * Global lock manager. + */ + private final LockManagerImpl lockMgr; + + /** + * Map of locked nodes, indexed by their (internal) id. + */ + private final Map lockedNodesMap = + new HashMap(); + + /** + * Map of unlocked nodes, indexed by their (internal) id. + */ + private final Map unlockedNodesMap = + new HashMap(); + + /** + * List of lock/unlock operations. + */ + private final List operations = new ArrayList(); + + /** + * Operation index. + */ + private int opIndex; + + /** + * Current status. + */ + private int status; + + /** + * Create a new instance of this class. + * @param lockMgr global lock manager + */ + public XAEnvironment(LockManagerImpl lockMgr) { + this.lockMgr = lockMgr; + } + + /** + * Reset this environment. + */ + public void reset() { + lockedNodesMap.clear(); + unlockedNodesMap.clear(); + operations.clear(); + opIndex = 0; + } + + /** + * Lock some node. + * @param node node to lock + * @param isDeep true to deep lock this node; + * false otherwise + * @param isSessionScoped true if lock should be session scoped; + * false otherwise + * @throws LockException if node is already locked + * @throws RepositoryException if an error occurs + */ + public LockInfo lock(NodeImpl node, boolean isDeep, boolean isSessionScoped) + throws LockException, RepositoryException { + return lock(node, isDeep, isSessionScoped, Long.MAX_VALUE, null); + } + + /** + * Lock some node. + * @param node node to lock + * @param isDeep true to deep lock this node; + * false otherwise + * @param isSessionScoped true if lock should be session scoped; + * false otherwise + * @param timeoutHint + * @param ownerInfo + * @throws LockException if node is already locked + * @throws RepositoryException if an error occurs + */ + public LockInfo lock(NodeImpl node, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerInfo) + throws LockException, RepositoryException { + + NodeId id = node.getNodeId(); + + // check negative set first + XALockInfo info = unlockedNodesMap.get(id); + if (info != null) { + // if settings are compatible, this is effectively a no-op + if (info.isDeep() == isDeep && info.isSessionScoped() == isSessionScoped) { + unlockedNodesMap.remove(id); + operations.remove(info); + return lockMgr.getLockInfo(id); + } + } + + // verify node is not already locked. + if (isLocked(node)) { + throw new LockException("Node locked."); + } + + // create a new lock info for this node + String lockOwner = (ownerInfo != null) ? ownerInfo : node.getSession().getUserID(); + info = new XALockInfo(node, isSessionScoped, isDeep, timeoutHint, lockOwner); + SessionImpl session = (SessionImpl) node.getSession(); + info.setLockHolder(session); + info.setLive(true); + + LockManagerImpl.getSessionLockManager(session).lockTokenAdded(info.getLockToken()); + lockedNodesMap.put(id, info); + operations.add(info); + + return info; + } + + /** + * Unlock some node. + * @param node node to unlock + * @throws LockException if the node is not locked + * @throws RepositoryException if an error occurs + */ + public void unlock(NodeImpl node) throws LockException, RepositoryException { + NodeId id = node.getNodeId(); + + // check positive set first + LockInfo info = lockedNodesMap.get(id); + if (info != null) { + lockedNodesMap.remove(id); + operations.remove(info); + info.setLive(false); + } else { + info = getLockInfo(node); + if (info == null || !info.getId().equals(id)) { + throw new LockException("Node not locked."); + } else if (!info.isLockHolder(node.getSession())) { + throw new LockException("Node not locked by this session."); + } + XALockInfo xaInfo = new XALockInfo(node, info); + unlockedNodesMap.put(id, xaInfo); + operations.add(xaInfo); + } + + } + + /** + * Return a flag indicating whether the specified node is locked. + * @return true if this node is locked; + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean isLocked(NodeImpl node) throws RepositoryException { + return getLockInfo(node) != null; + } + + /** + * Return the most appropriate lock information for a node. This is either + * the lock info for the node itself, if it is locked, or a lock info for + * one of its parents, if that one is deep locked. + * @param node node + * @return LockInfo lock info or null if node is not locked + * @throws RepositoryException if an error occurs + */ + public LockInfo getLockInfo(NodeImpl node) throws RepositoryException { + NodeId id = node.getNodeId(); + + // check negative set + if (unlockedNodesMap.containsKey(id)) { + return null; + } + + // check positive set, iteratively ascending in hierarchy + if (!lockedNodesMap.isEmpty()) { + NodeImpl current = node; + for (;;) { + XALockInfo info = lockedNodesMap.get(current.getId()); + if (info != null) { + if (info.getId().equals(id) || info.isDeep()) { + return info; + } + break; + } + if (current.getDepth() == 0) { + break; + } + current = (NodeImpl) current.getParent(); + } + } + // ask parent + return lockMgr.getLockInfo(id); + } + + /** + * Returns all locks associated with the specified session. + * @param session session + * @return locks associated with the session + * @throws RepositoryException if an error occurs + */ + public LockInfo[] getLockInfos(SessionImpl session) + throws RepositoryException { + ArrayList result = new ArrayList(); + + // get lock information from global lock manager first + for (LockInfo info : lockMgr.getLockInfos(session)) { + // check negative set + if (!unlockedNodesMap.containsKey(info.getId())) { + result.add(info); + } + } + + // add 'uncommitted' lock information + result.addAll(lockedNodesMap.values()); + + return result.toArray(new LockInfo[result.size()]); + } + + /** + * Add lock token to this environment. + * @param session + * @param lt lock token + * @throws RepositoryException + */ + public void addLockToken(SessionImpl session, String lt) throws RepositoryException { + try { + NodeId id = LockInfo.parseLockToken(lt); + NodeImpl node = (NodeImpl) session.getItemManager().getItem(id); + LockInfo info = getLockInfo(node); + if (info != null && !info.isLockHolder(session)) { + if (info.getLockHolder() == null) { + info.setLockHolder(session); + } else { + String msg = "Cannot add lock token: lock already held by other session."; + log.warn(msg); + throw new LockException(msg); + } + } + // inform SessionLockManager + getSessionLockManager(session).lockTokenAdded(lt); + } catch (IllegalArgumentException e) { + String msg = "Bad lock token: " + e.getMessage(); + log.warn(msg); + throw new LockException(msg); + } + } + + /** + * Remove lock token from this environment. + * @param session + * @param lt lock token + * @throws RepositoryException + */ + public void removeLockToken(SessionImpl session, String lt) throws RepositoryException { + try { + NodeId id = LockInfo.parseLockToken(lt); + + NodeImpl node = (NodeImpl) session.getItemManager().getItem(id); + LockInfo info = getLockInfo(node); + if (info != null) { + if (info.isLockHolder(session)) { + info.setLockHolder(null); + } else if (info.getLockHolder() != null) { + String msg = "Cannot remove lock token: lock held by other session."; + log.warn(msg); + throw new LockException(msg); + } + } + // inform SessionLockManager + getSessionLockManager(session).lockTokenRemoved(lt); + } catch (IllegalArgumentException e) { + String msg = "Bad lock token: " + e.getMessage(); + log.warn(msg); + throw new LockException(msg); + } + } + + static SessionLockManager getSessionLockManager(SessionImpl session) throws RepositoryException { + Workspace wsp = session.getWorkspace(); + return (SessionLockManager) wsp.getLockManager(); + } + + /** + * Prepare update. Locks global lock manager and feeds all lock/ + * unlock operations. + */ + public void prepare() throws TransactionException { + status = STATUS_PREPARING; + if (!operations.isEmpty()) { + lockMgr.beginUpdate(); + + try { + while (opIndex < operations.size()) { + try { + XALockInfo info = operations.get(opIndex); + info.update(); + } catch (RepositoryException e) { + throw new TransactionException("Unable to update.", e); + } + opIndex++; + } + } finally { + if (opIndex < operations.size()) { + while (opIndex > 0) { + try { + XALockInfo info = operations.get(opIndex - 1); + info.undo(); + } catch (RepositoryException e) { + log.error("Unable to undo lock operation.", e); + } + opIndex--; + } + lockMgr.cancelUpdate(); + } + } + } + status = STATUS_PREPARED; + } + + /** + * Commit changes. This will finish the update and unlock the + * global lock manager. + */ + public void commit() { + int oldStatus = status; + + status = STATUS_COMMITTING; + if (oldStatus == STATUS_PREPARED) { + if (!operations.isEmpty()) { + lockMgr.endUpdate(); + reset(); + } + } + status = STATUS_COMMITTED; + } + + /** + * Rollback changes. This will undo all updates and unlock the + * global lock manager. + */ + public void rollback() { + int oldStatus = status; + + status = STATUS_ROLLING_BACK; + if (oldStatus == STATUS_PREPARED) { + if (!operations.isEmpty()) { + while (opIndex > 0) { + try { + XALockInfo info = operations.get(opIndex - 1); + info.undo(); + } catch (RepositoryException e) { + log.error("Unable to undo lock operation.", e); + } + opIndex--; + } + lockMgr.cancelUpdate(); + reset(); + } + } + status = STATUS_ROLLED_BACK; + } + + /** + * Return a flag indicating whether a lock info belongs to a different + * XA environment. + */ + public boolean differentXAEnv(LockInfo info) { + if (info instanceof XALockInfo) { + XALockInfo lockInfo = (XALockInfo) info; + return lockInfo.getXAEnv() != this; + } + return true; + } + + /** + * Information about a lock used inside transactions. + */ + class XALockInfo extends LockInfo { + + /** + * Node being locked/unlocked. + */ + private final NodeImpl node; + + /** + * Flag indicating whether this info belongs to a unlock operation. + */ + private boolean isUnlock; + + /** + * Create a new instance of this class. + * @param sessionScoped whether lock token is session scoped + * @param deep whether lock is deep + * @param lockOwner owner of lock + */ + public XALockInfo( + NodeImpl node, + boolean sessionScoped, boolean deep, long timeoutHint, String lockOwner) { + super(node.getNodeId(), sessionScoped, deep, lockOwner, timeoutHint); + this.node = node; + } + + /** + * Create a new instance of this class. Used to signal an + * unlock operation on some existing lock information. + */ + public XALockInfo(NodeImpl node, LockInfo info) { + super(info); + this.node = node; + this.isUnlock = true; + } + + /** + * Return a flag indicating whether this info belongs to a unlock operation. + * @return true if this info belongs to an unlock operation; + * otherwise false + */ + public boolean isUnlock() { + return isUnlock; + } + + /** + * Do operation. + */ + public void update() throws LockException, RepositoryException { + if (isUnlock) { + // Only if we have a valid ItemState try to unlock + // JCR-2332 + if (((WorkspaceImpl) node.getSession().getWorkspace()).getItemStateManager().hasItemState(node.getId())) { + lockMgr.internalUnlock(node); + } + } else { + LockInfo internalLock = lockMgr.internalLock( + node, isDeep(), isSessionScoped(), + getTimeoutHint(), +// getTimeoutTime(), + getLockOwner()); + LockInfo xaEnvLock = getLockInfo(node); + // Check if the lockToken has been removed in the transaction ... + if (xaEnvLock != null && xaEnvLock.getLockHolder() == null) { + //Remove lockToken from SessionLockManager + getSessionLockManager(internalLock.getLockHolder()).lockTokenRemoved(internalLock.getLockToken()); + internalLock.setLockHolder(null); + } + } + } + + /** + * Undo operation. + */ + public void undo() throws LockException, RepositoryException { + if (isUnlock) { + lockMgr.internalLock( + node, isDeep(), isSessionScoped(), + getTimeoutHint(), getLockOwner()); + } else { + lockMgr.internalUnlock(node); + } + } + + /** + * Return parent environment. + */ + public XAEnvironment getXAEnv() { + return XAEnvironment.this; + } + + /** + * {@inheritDoc} + *

    + * As long as the XA environment is neither committed nor rolled back, + * associated lock information is subject to change. + */ + @Override + public boolean mayChange() { + if (status != STATUS_COMMITTED + && status != STATUS_ROLLED_BACK) { + return true; + } + return super.mayChange(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XALockImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XALockImpl.java new file mode 100755 index 00000000000..fd696ba61f9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XALockImpl.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.core.lock; + +import org.apache.jackrabbit.core.NodeImpl; + +import javax.jcr.RepositoryException; + +/** + * Extension to standard lock implementation that works in XA environment. + */ +class XALockImpl extends LockImpl { + + /** + * XA lock manager. + */ + private final XALockManager lockMgr; + + /** + * The underlying lock info. + */ + private final LockInfo info; + + /** + * Create a new instance of this class. + * @param info lock information + * @param node node holding lock + */ + public XALockImpl( + XALockManager lockMgr, LockInfo info, NodeImpl node) { + super(info, node); + + this.info = info; + this.lockMgr = lockMgr; + } + + /** + * {@inheritDoc} + *

    + * Refresh lock information if XA environment has changed. + */ + @Override + public boolean isLive() throws RepositoryException { + if (info.mayChange()) { + if (lockMgr.differentXAEnv(info)) { + return lockMgr.holdsLock(node); + } + } + return super.isLive(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XALockManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XALockManager.java new file mode 100755 index 00000000000..7a9589d0e75 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/lock/XALockManager.java @@ -0,0 +1,315 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.lock; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.data.core.InternalXAResource; +import org.apache.jackrabbit.data.core.TransactionContext; +import org.apache.jackrabbit.data.core.TransactionException; +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +/** + * Session-local lock manager that implements the semantical changes inside + * transactions. This manager validates lock/unlock operations inside its + * view of the locking space. + */ +public class XALockManager implements LockManager, InternalXAResource { + + /** + * Attribute name for XA Environment. + */ + private static final String XA_ENV_ATTRIBUTE_NAME = "XALockManager.XAEnv"; + + /** + * Global lock manager. + */ + private final LockManagerImpl lockMgr; + + /** + * Current XA environment. + */ + private XAEnvironment xaEnv; + + /** + * Create a new instance of this class. + * @param lockMgr lockMgr global lock manager + */ + public XALockManager(LockManagerImpl lockMgr) { + this.lockMgr = lockMgr; + } + + //----------------------------------------------------------< LockManager > + + /** + * {@inheritDoc} + */ + public Lock lock(NodeImpl node, boolean isDeep, boolean isSessionScoped) + throws LockException, RepositoryException { + return lock(node, isDeep, isSessionScoped, Long.MAX_VALUE, null); + } + + /** + * @see LockManager#lock(NodeImpl, boolean, boolean, long, String) + */ + public Lock lock(NodeImpl node, boolean isDeep, boolean isSessionScoped, long timoutHint, String ownerInfo) + throws LockException, RepositoryException { + LockInfo info; + if (isInXA()) { + info = xaEnv.lock(node, isDeep, isSessionScoped, timoutHint, ownerInfo); + } else { + info = lockMgr.internalLock(node, isDeep, isSessionScoped, timoutHint, ownerInfo); + } + lockMgr.writeLockProperties(node, info.getLockOwner(), info.isDeep()); + return new XALockImpl(this, info, node); + } + + /** + * {@inheritDoc} + */ + public Lock getLock(NodeImpl node) throws LockException, RepositoryException { + LockInfo info; + if (isInXA()) { + info = xaEnv.getLockInfo(node); + } else { + info = lockMgr.getLockInfo(node.getNodeId()); + } + if (info == null) { + throw new LockException("Node not locked: " + node); + } + SessionImpl session = (SessionImpl) node.getSession(); + NodeImpl holder = (NodeImpl) session.getItemManager().getItem(info.getId()); + return new XALockImpl(this, info, holder); + } + + /** + * {@inheritDoc} + */ + public Lock[] getLocks(SessionImpl session) throws RepositoryException { + LockInfo[] infos; + if (isInXA()) { + infos = xaEnv.getLockInfos(session); + } else { + infos = lockMgr.getLockInfos(session); + } + + XALockImpl[] locks = new XALockImpl[infos.length]; + + for (int i = 0; i < infos.length; i++) { + LockInfo info = infos[i]; + NodeImpl holder = (NodeImpl) session.getItemManager().getItem(info.getId()); + locks[i] = new XALockImpl(this, info, holder); + } + return locks; + } + + /** + * {@inheritDoc} + */ + public void unlock(NodeImpl node) throws LockException, RepositoryException { + lockMgr.removeLockProperties(node); + if (isInXA()) { + xaEnv.unlock(node); + } else { + lockMgr.internalUnlock(node); + } + } + + /** + * {@inheritDoc} + */ + public boolean holdsLock(NodeImpl node) throws RepositoryException { + LockInfo info; + if (isInXA()) { + info = xaEnv.getLockInfo(node); + } else { + info = lockMgr.getLockInfo(node.getNodeId()); + } + return info != null && info.getId().equals(node.getId()); + } + + /** + * {@inheritDoc} + */ + public boolean isLocked(NodeImpl node) throws RepositoryException { + LockInfo info; + if (isInXA()) { + info = xaEnv.getLockInfo(node); + } else { + info = lockMgr.getLockInfo(node.getNodeId()); + } + return info != null; + } + + /** + * {@inheritDoc} + */ + public void checkLock(NodeImpl node) throws LockException, RepositoryException { + LockInfo info; + if (isInXA()) { + info = xaEnv.getLockInfo(node); + if (info != null && !info.isLockHolder(node.getSession())) { + throw new LockException("Node locked."); + } + } else { + lockMgr.checkLock(node); + } + } + + /** + * {@inheritDoc} + */ + public void checkLock(Path path, Session session) + throws LockException, RepositoryException { + + if (isInXA()) { + SessionImpl sessionImpl = (SessionImpl) session; + checkLock(sessionImpl.getItemManager().getNode(path)); + } else { + lockMgr.checkLock(path, session); + } + } + + /** + * {@inheritDoc} + */ + public void checkUnlock(Session session, NodeImpl node) + throws LockException, RepositoryException { + + if (isInXA()) { + LockInfo info = xaEnv.getLockInfo(node); + if (info == null || !info.getId().equals(node.getId())) { + throw new LockException("Node not locked: " + node); + } + if (!info.isLockHolder(session)) { + throw new LockException("Node not locked by session: " + node); + } + } else { + lockMgr.checkUnlock(session, node); + } + } + + /** + * {@inheritDoc} + */ + public void addLockToken(SessionImpl session, String lt) throws RepositoryException { + if (isInXA()) { + xaEnv.addLockToken(session, lt); + } else { + lockMgr.addLockToken(session, lt); + } + } + + /** + * {@inheritDoc} + */ + public void removeLockToken(SessionImpl session, String lt) throws RepositoryException { + if (isInXA()) { + xaEnv.removeLockToken(session, lt); + } else { + lockMgr.removeLockToken(session, lt); + } + } + + //-----------------------------------------------------------< transaction > + + /** + * {@inheritDoc} + */ + public void associate(TransactionContext tx) { + XAEnvironment xaEnv = null; + if (tx != null) { + xaEnv = (XAEnvironment) tx.getAttribute(XA_ENV_ATTRIBUTE_NAME); + if (xaEnv == null) { + xaEnv = new XAEnvironment(lockMgr); + tx.setAttribute(XA_ENV_ATTRIBUTE_NAME, xaEnv); + } + } + this.xaEnv = xaEnv; + } + + /** + * {@inheritDoc} + */ + public void beforeOperation(TransactionContext tx) { + } + + /** + * {@inheritDoc} + */ + public void prepare(TransactionContext tx) throws TransactionException { + XAEnvironment xaEnv = (XAEnvironment) tx.getAttribute(XA_ENV_ATTRIBUTE_NAME); + if (xaEnv != null) { + xaEnv.prepare(); + } + } + + /** + * {@inheritDoc} + *

    + * This will finish the update and unlock the shared lock manager. + */ + public void commit(TransactionContext tx) { + XAEnvironment xaEnv = (XAEnvironment) tx.getAttribute(XA_ENV_ATTRIBUTE_NAME); + if (xaEnv != null) { + xaEnv.commit(); + } + } + + /** + * {@inheritDoc} + *

    + * This will undo all updates and unlock the shared lock manager. + */ + public void rollback(TransactionContext tx) { + XAEnvironment xaEnv = (XAEnvironment) tx.getAttribute(XA_ENV_ATTRIBUTE_NAME); + if (xaEnv != null) { + xaEnv.rollback(); + } + } + + /** + * {@inheritDoc} + */ + public void afterOperation(TransactionContext tx) { + } + + /** + * Return a flag indicating whether a lock info belongs to a different + * XA environment. + */ + public boolean differentXAEnv(LockInfo info) { + if (isInXA()) { + return xaEnv.differentXAEnv(info); + } else { + return info instanceof XAEnvironment.XALockInfo; + } + } + + /** + * Return a flag indicating whether this version manager is currently + * associated with an XA transaction. + */ + private boolean isInXA() { + return xaEnv != null; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/BitSetENTCacheImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/BitSetENTCacheImpl.java new file mode 100644 index 00000000000..267da10b2f7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/BitSetENTCacheImpl.java @@ -0,0 +1,491 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype; + +import org.apache.jackrabbit.core.nodetype.EffectiveNodeTypeCache.Key; +import org.apache.jackrabbit.spi.Name; + +import java.util.TreeSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.ArrayList; + +import EDU.oswego.cs.dl.util.concurrent.ConcurrentReaderHashMap; + +/** + * Implements an effective node type cache that uses a bit set for storing the + * information about participating node types in a set. + */ +public class BitSetENTCacheImpl implements EffectiveNodeTypeCache { + + /** + * constant for bits-per-word + */ + private static final int BPW = 64; + + /** + * OR mask for bit set + */ + private static final long[] OR_MASK = new long[BPW]; + static { + for (int i = 0; i < BPW; i++) { + OR_MASK[i] = 1L << i; + } + } + + /** + * An ordered set of the keys. This is used for {@link #findBest(Key)}. + */ + private final TreeSet sortedKeys; + + /** + * cache of pre-built aggregations of node types + */ + private final HashMap aggregates; + + /** + * A lookup table for bit numbers for a given name. + * + * Note: further performance improvements could be made if this index would + * be stored in the node type registry since only registered node type names + * are allowed in the keys. + */ + private final ConcurrentReaderHashMap nameIndex = new ConcurrentReaderHashMap(); + + /** + * The reverse lookup table for bit numbers to names + */ + private Name[] names = new Name[1024]; + + /** + * Creates a new bitset effective node type cache + */ + BitSetENTCacheImpl() { + sortedKeys = new TreeSet(); + aggregates = new HashMap(); + } + + /** + * {@inheritDoc} + */ + public Key getKey(Name[] ntNames) { + return new BitSetKey(ntNames, nameIndex.size() + ntNames.length); + } + + /** + * {@inheritDoc} + */ + public void put(EffectiveNodeType ent) { + put(getKey(ent.getMergedNodeTypes()), ent); + } + + /** + * {@inheritDoc} + */ + public void put(Key key, EffectiveNodeType ent) { + aggregates.put(key, ent); + sortedKeys.add(key); + } + + /** + * {@inheritDoc} + */ + public Key findBest(Key key) { + // quick check for already cached key + if (contains(key)) { + return key; + } + // clone TreeSet first to prevent ConcurrentModificationException + TreeSet keys = (TreeSet) sortedKeys.clone(); + Iterator iter = keys.iterator(); + while (iter.hasNext()) { + Key k = iter.next(); + if (key.contains(k)) { + return k; + } + } + return null; + } + + /** + * {@inheritDoc} + */ + public void invalidate(Name name) { + /** + * remove all affected effective node types from aggregates cache + * (copy keys first to prevent ConcurrentModificationException) + */ + ArrayList keys = new ArrayList(aggregates.keySet()); + for (Iterator keysIter = keys.iterator(); keysIter.hasNext();) { + Key k = keysIter.next(); + EffectiveNodeType ent = get(k); + if (ent.includesNodeType(name)) { + remove(k); + } + } + } + + /** + * {@inheritDoc} + */ + public boolean contains(Key key) { + return aggregates.containsKey(key); + } + + /** + * {@inheritDoc} + */ + public EffectiveNodeType get(Key key) { + return aggregates.get(key); + } + + /** + * Returns the bit number for the given name. If the name does not exist + * a new new bit number for that name is created. + * + * @param name the name to lookup + * @return the bit number for the given name + */ + private int getBitNumber(Name name) { + Integer i = (Integer) nameIndex.get(name); + if (i == null) { + synchronized (nameIndex) { + i = (Integer) nameIndex.get(name); + if (i == null) { + int idx = nameIndex.size(); + i = new Integer(idx); + nameIndex.put(name, i); + if (idx >= names.length) { + Name[] newNames = new Name[names.length * 2]; + System.arraycopy(names, 0, newNames, 0, names.length); + names = newNames; + } + names[idx] = name; + } + } + } + return i.intValue(); + } + + /** + * Returns the node type name for a given bit number. + * @param n the bit number to lookup + * @return the node type name + */ + private Name getName(int n) { + return names[n]; + } + + /** + * Removes the effective node type for the given key from the cache. + * + * @param key the key of the effective node type to remove + * @return the removed effective node type or null if it was + * never cached. + */ + private EffectiveNodeType remove(Key key) { + EffectiveNodeType removed = aggregates.remove(key); + if (removed != null) { + // other than the original implementation, the weights in the + // treeset are now the same as in the given keys. so we can use + // the normal remove method + sortedKeys.remove(key); + } + return removed; + } + + /** + * {@inheritDoc} + */ + public Object clone() { + BitSetENTCacheImpl clone = new BitSetENTCacheImpl(); + clone.sortedKeys.addAll(sortedKeys); + clone.aggregates.putAll(aggregates); + clone.names = new Name[names.length]; + System.arraycopy(names, 0, clone.names, 0, names.length); + clone.nameIndex.putAll(nameIndex); + return clone; + } + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("BitSetENTCacheImpl (" + super.toString() + ")\n"); + builder.append("EffectiveNodeTypes in cache:\n"); + for (Key key : sortedKeys) { + builder.append(key); + builder.append("\n"); + } + return builder.toString(); + } + + /** + * Implements a {@link Key} by storing the node type aggregate information + * in a bit set. We do not use the {@link java.util.BitSet} because it + * does not suit all our requirements. Every node type is represented by a bit + * in the set. This key is immutable. + */ + private class BitSetKey implements Key { + + /** + * The names of the node types that form this key. + */ + private final Name[] names; + + /** + * The array of longs that hold the bit information. + */ + private final long[] bits; + + /** + * the hash code, only calculated once + */ + private final int hashCode; + + /** + * Creates a new bit set key. + * @param names the node type names + * @param maxBit the approximative number of the greatest bit + */ + public BitSetKey(Name[] names, int maxBit) { + this.names = names; + bits = new long[maxBit / BPW + 1]; + + for (int i = 0; i < names.length; i++) { + int n = getBitNumber(names[i]); + bits[n / BPW] |= OR_MASK[n % BPW]; + } + hashCode = calcHashCode(); + } + + /** + * Creates a new bit set key. + * @param bits the array of bits + * @param numBits the number of bits that are '1' in the given bits + */ + private BitSetKey(long[] bits, int numBits) { + this.bits = bits; + names = new Name[numBits]; + int i = nextSetBit(0); + int j = 0; + while (i >= 0) { + names[j++] = BitSetENTCacheImpl.this.getName(i); + i = nextSetBit(i + 1); + } + hashCode = calcHashCode(); + } + + /** + * {@inheritDoc} + */ + public Name[] getNames() { + return names; + } + + /** + * {@inheritDoc} + */ + public boolean contains(Key otherKey) { + /* + * 0 - 0 => 0 + * 0 - 1 => 1 + * 1 - 0 => 0 + * 1 - 1 => 0 + * !a and b + */ + BitSetKey other = (BitSetKey) otherKey; + int len = Math.max(bits.length, other.bits.length); + for (int i = 0; i < len; i++) { + long w1 = i < bits.length ? bits[i] : 0; + long w2 = i < other.bits.length ? other.bits[i] : 0; + long r = ~w1 & w2; + if (r != 0) { + return false; + } + } + return true; + } + + /** + * {@inheritDoc} + */ + public Key subtract(Key otherKey) { + /* + * 0 - 0 => 0 + * 0 - 1 => 0 + * 1 - 0 => 1 + * 1 - 1 => 0 + * a and !b + */ + BitSetKey other = (BitSetKey) otherKey; + int len = Math.max(bits.length, other.bits.length); + long[] newBits = new long[len]; + int numBits = 0; + for (int i = 0; i < len; i++) { + long w1 = i < bits.length ? bits[i] : 0; + long w2 = i < other.bits.length ? other.bits[i] : 0; + newBits[i] = w1 & ~w2; + numBits += bitCount(newBits[i]); + } + return new BitSetKey(newBits, numBits); + } + + /** + * Returns the bit number of the next bit that is set, starting at + * fromIndex inclusive. + * + * @param fromIndex the bit position to start the search + * @return the bit position of the bit or -1 if none found. + */ + private int nextSetBit(int fromIndex) { + int addr = fromIndex / BPW; + int off = fromIndex % BPW; + while (addr < bits.length) { + if (bits[addr] != 0) { + while (off < BPW) { + if ((bits[addr] & OR_MASK[off]) != 0) { + return addr * BPW + off; + } + off++; + } + off = 0; + } + addr++; + } + return -1; + } + + /** + * Returns the number of bits set in val. + * For a derivation of this algorithm, see + * "Algorithms and data structures with applications to + * graphics and geometry", by Jurg Nievergelt and Klaus Hinrichs, + * Prentice Hall, 1993. + * + * @param val the value to calculate the bit count for + * @return the number of '1' bits in the value + */ + private int bitCount(long val) { + val -= (val & 0xaaaaaaaaaaaaaaaaL) >>> 1; + val = (val & 0x3333333333333333L) + ((val >>> 2) & 0x3333333333333333L); + val = (val + (val >>> 4)) & 0x0f0f0f0f0f0f0f0fL; + val += val >>> 8; + val += val >>> 16; + return ((int) (val) + (int) (val >>> 32)) & 0xff; + } + + + /** + * {@inheritDoc} + * + * This compares 1. the cardinality (number of set bits) and 2. the + * numeric value of the bit sets in descending order. + */ + public int compareTo(Key other) { + BitSetKey o = (BitSetKey) other; + int res = o.names.length - names.length; + if (res == 0) { + int adr = Math.max(bits.length, o.bits.length) - 1; + while (adr >= 0) { + long w1 = adr < bits.length ? bits[adr] : 0; + long w2 = adr < o.bits.length ? o.bits[adr] : 0; + if (w1 != w2) { + // some signed arithmetic here + long h1 = w1 >>> 32; + long h2 = w2 >>> 32; + if (h1 == h2) { + h1 = w1 & 0x0ffffffffL; + h2 = w2 & 0x0ffffffffL; + } + return Long.signum(h2 - h1); + } + adr--; + } + } + return res; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BitSetKey) { + BitSetKey o = (BitSetKey) obj; + if (names.length != o.names.length) { + return false; + } + int adr = Math.max(bits.length, o.bits.length) - 1; + while (adr >= 0) { + long w1 = adr < bits.length ? bits[adr] : 0; + long w2 = adr < o.bits.length ? o.bits[adr] : 0; + if (w1 != w2) { + return false; + } + adr--; + } + return true; + } + return false; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return hashCode; + } + + /** + * Calculates the hash code. + * @return the calculated hash code + */ + private int calcHashCode() { + long h = 1234; + int addr = bits.length - 1; + while (addr >= 0 && bits[addr] == 0) { + addr--; + } + while (addr >= 0) { + h ^= bits[addr] * (addr + 1); + addr--; + } + return (int) ((h >> 32) ^ h); + } + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuilder buf = new StringBuilder("w="); + buf.append(names.length); + int i = nextSetBit(0); + while (i >= 0) { + buf.append(", ").append(i).append("="); + buf.append(BitSetENTCacheImpl.this.getName(i)); + i = nextSetBit(i + 1); + } + return buf.toString(); + } + + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeType.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeType.java new file mode 100644 index 00000000000..4d30d76b55d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeType.java @@ -0,0 +1,1164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype; + +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; +import java.util.Set; +import java.util.HashSet; + +/** + * An EffectiveNodeType represents one or more + * NodeTypes as one 'effective' node type where inheritance + * is resolved. + *

    + * Instances of EffectiveNodeType are immutable. + */ +public class EffectiveNodeType implements Cloneable { + private static Logger log = LoggerFactory.getLogger(EffectiveNodeType.class); + + // list of explicitly aggregated {i.e. merged) node types + private final TreeSet mergedNodeTypes; + // list of implicitly aggregated {through inheritance) node types + private final TreeSet inheritedNodeTypes; + // list of all either explicitly (through aggregation) or implicitly + // (through inheritance) included node types. + private final TreeSet allNodeTypes; + // map of named item definitions (maps name to list of definitions) + private final HashMap> namedItemDefs; + // list of unnamed item definitions (i.e. residual definitions) + private final ArrayList unnamedItemDefs; + + // flag indicating whether any included node type supports orderable child nodes + private boolean orderableChildNodes; + + private Name primaryItemName; + + /** + * private constructor. + */ + private EffectiveNodeType() { + mergedNodeTypes = new TreeSet(); + inheritedNodeTypes = new TreeSet(); + allNodeTypes = new TreeSet(); + namedItemDefs = new HashMap>(); + unnamedItemDefs = new ArrayList(); + orderableChildNodes = false; + primaryItemName = null; + } + + /** + * Package private factory method. + *

    + * Creates an effective node type representation of a node type definition. + * Note that the definitions of all referenced node types must be contained + * in ntdCache. + * + * @param ntd node type definition + * @param entCache cache of already-built effective node types + * @param ntdCache cache of node type definitions, used to resolve dependencies + * @return an effective node type representation of the given node type definition. + * @throws NodeTypeConflictException if the node type definition is invalid, + * e.g. due to ambiguous child definitions. + * @throws NoSuchNodeTypeException if a node type reference (e.g. a supertype) + * could not be resolved. + */ + static EffectiveNodeType create(QNodeTypeDefinition ntd, + EffectiveNodeTypeCache entCache, + Map ntdCache) + throws NodeTypeConflictException, NoSuchNodeTypeException { + // create empty effective node type instance + EffectiveNodeType ent = new EffectiveNodeType(); + Name ntName = ntd.getName(); + + // prepare new instance + ent.mergedNodeTypes.add(ntName); + ent.allNodeTypes.add(ntName); + + // map of all item definitions (maps id to definition) + // used to effectively detect ambiguous child definitions where + // ambiguity is defined in terms of definition identity + Set itemDefs = new HashSet(); + + QNodeDefinition[] cnda = ntd.getChildNodeDefs(); + for (QNodeDefinition aCnda : cnda) { + // check if child node definition would be ambiguous within + // this node type definition + if (itemDefs.contains(aCnda)) { + // conflict + String msg; + if (aCnda.definesResidual()) { + msg = ntName + " contains ambiguous residual child node definitions"; + } else { + msg = ntName + " contains ambiguous definitions for child node named " + + aCnda.getName(); + } + log.debug(msg); + throw new NodeTypeConflictException(msg); + } else { + itemDefs.add(aCnda); + } + if (aCnda.definesResidual()) { + // residual node definition + ent.unnamedItemDefs.add(aCnda); + } else { + // named node definition + Name name = aCnda.getName(); + List defs = ent.namedItemDefs.get(name); + if (defs == null) { + defs = new ArrayList(); + ent.namedItemDefs.put(name, defs); + } + if (defs.size() > 0) { + /** + * there already exists at least one definition with that + * name; make sure none of them is auto-create + */ + for (QItemDefinition def : defs) { + if (aCnda.isAutoCreated() || def.isAutoCreated()) { + // conflict + String msg = "There are more than one 'auto-create' item definitions for '" + + name + "' in node type '" + ntName + "'"; + log.debug(msg); + throw new NodeTypeConflictException(msg); + } + } + } + defs.add(aCnda); + } + } + QPropertyDefinition[] pda = ntd.getPropertyDefs(); + for (QPropertyDefinition aPda : pda) { + // check if property definition would be ambiguous within + // this node type definition + if (itemDefs.contains(aPda)) { + // conflict + String msg; + if (aPda.definesResidual()) { + msg = ntName + " contains ambiguous residual property definitions"; + } else { + msg = ntName + " contains ambiguous definitions for property named " + + aPda.getName(); + } + log.debug(msg); + throw new NodeTypeConflictException(msg); + } else { + itemDefs.add(aPda); + } + if (aPda.definesResidual()) { + // residual property definition + ent.unnamedItemDefs.add(aPda); + } else { + // named property definition + Name name = aPda.getName(); + List defs = ent.namedItemDefs.get(name); + if (defs == null) { + defs = new ArrayList(); + ent.namedItemDefs.put(name, defs); + } + if (defs.size() > 0) { + /** + * there already exists at least one definition with that + * name; make sure none of them is auto-create + */ + for (QItemDefinition def : defs) { + if (aPda.isAutoCreated() || def.isAutoCreated()) { + // conflict + String msg = "There are more than one 'auto-create' item definitions for '" + + name + "' in node type '" + ntName + "'"; + log.debug(msg); + throw new NodeTypeConflictException(msg); + } + } + } + defs.add(aPda); + } + } + + // resolve supertypes recursively + Name[] supertypes = ntd.getSupertypes(); + if (supertypes.length > 0) { + EffectiveNodeType base = + NodeTypeRegistry.getEffectiveNodeType(supertypes, entCache, ntdCache); + ent.internalMerge(base, true); + } + + // resolve 'orderable child nodes' attribute value (JCR-1947) + if (ntd.hasOrderableChildNodes()) { + ent.orderableChildNodes = true; + } else { + Name[] nta = ent.getInheritedNodeTypes(); + for (Name aNta : nta) { + QNodeTypeDefinition def = ntdCache.get(aNta); + if (def.hasOrderableChildNodes()) { + ent.orderableChildNodes = true; + break; + } + } + } + + // resolve 'primary item' attribute value (JCR-1947) + if (ntd.getPrimaryItemName() != null) { + ent.primaryItemName = ntd.getPrimaryItemName(); + } else { + Name[] nta = ent.getInheritedNodeTypes(); + for (Name aNta : nta) { + QNodeTypeDefinition def = ntdCache.get(aNta); + if (def.getPrimaryItemName() != null) { + ent.primaryItemName = def.getPrimaryItemName(); + break; + } + } + } + + // we're done + return ent; + } + + /** + * Package private factory method for creating a new 'empty' effective + * node type instance. + * + * @return an 'empty' effective node type instance. + */ + static EffectiveNodeType create() { + return new EffectiveNodeType(); + } + + /** + * Returns true if any of the included node types supports + * 'orderable child nodes'; returns false otherwise. + * @return true if this effective node type has orderable child nodes + */ + public boolean hasOrderableChildNodes() { + return orderableChildNodes; + } + + public Name getPrimaryItemName() { + return primaryItemName; + } + + public Name[] getMergedNodeTypes() { + return mergedNodeTypes.toArray(new Name[mergedNodeTypes.size()]); + } + + public Name[] getInheritedNodeTypes() { + return inheritedNodeTypes.toArray(new Name[inheritedNodeTypes.size()]); + } + + public Name[] getAllNodeTypes() { + return allNodeTypes.toArray(new Name[allNodeTypes.size()]); + } + + public QItemDefinition[] getAllItemDefs() { + if (namedItemDefs.size() == 0 && unnamedItemDefs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size() + unnamedItemDefs.size()); + for (List itemDefs : namedItemDefs.values()) { + defs.addAll(itemDefs); + } + defs.addAll(unnamedItemDefs); + if (defs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QItemDefinition[defs.size()]); + } + + public QItemDefinition[] getNamedItemDefs() { + if (namedItemDefs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List itemDefs : namedItemDefs.values()) { + defs.addAll(itemDefs); + } + if (defs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QItemDefinition[defs.size()]); + } + + public QItemDefinition[] getUnnamedItemDefs() { + if (unnamedItemDefs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + return unnamedItemDefs.toArray(new QItemDefinition[unnamedItemDefs.size()]); + } + + public boolean hasNamedItemDef(Name name) { + return namedItemDefs.containsKey(name); + } + + public QItemDefinition[] getNamedItemDefs(Name name) { + List defs = namedItemDefs.get(name); + if (defs == null || defs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QItemDefinition[defs.size()]); + } + + public QNodeDefinition[] getAllNodeDefs() { + if (namedItemDefs.size() == 0 && unnamedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size() + unnamedItemDefs.size()); + for (QItemDefinition def : unnamedItemDefs) { + if (def.definesNode()) { + defs.add((QNodeDefinition) def); + } + } + for (List list: namedItemDefs.values()) { + for (QItemDefinition def : list) { + if (def.definesNode()) { + defs.add((QNodeDefinition) def); + } + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + public QNodeDefinition[] getNamedNodeDefs() { + if (namedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition def : list) { + if (def.definesNode()) { + defs.add((QNodeDefinition) def); + } + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + public QNodeDefinition[] getNamedNodeDefs(Name name) { + List list = namedItemDefs.get(name); + if (list == null || list.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(list.size()); + for (QItemDefinition def : list) { + if (def.definesNode()) { + defs.add((QNodeDefinition) def); + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + public QNodeDefinition[] getUnnamedNodeDefs() { + if (unnamedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(unnamedItemDefs.size()); + for (QItemDefinition def : unnamedItemDefs) { + if (def.definesNode()) { + defs.add((QNodeDefinition) def); + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + public QNodeDefinition[] getAutoCreateNodeDefs() { + // since auto-create items must have a name, + // we're only searching the named item definitions + if (namedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition def : list) { + if (def.definesNode() && def.isAutoCreated()) { + defs.add((QNodeDefinition) def); + } + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + public QPropertyDefinition[] getAllPropDefs() { + if (namedItemDefs.size() == 0 && unnamedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size() + unnamedItemDefs.size()); + for (QItemDefinition def : unnamedItemDefs) { + if (!def.definesNode()) { + defs.add((QPropertyDefinition) def); + } + } + for (List list: namedItemDefs.values()) { + for (QItemDefinition def : list) { + if (!def.definesNode()) { + defs.add((QPropertyDefinition) def); + } + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + public QPropertyDefinition[] getNamedPropDefs() { + if (namedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition def : list) { + if (!def.definesNode()) { + defs.add((QPropertyDefinition) def); + } + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + public QPropertyDefinition[] getNamedPropDefs(Name name) { + List list = namedItemDefs.get(name); + if (list == null || list.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(list.size()); + for (QItemDefinition def : list) { + if (!def.definesNode()) { + defs.add((QPropertyDefinition) def); + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + public QPropertyDefinition[] getUnnamedPropDefs() { + if (unnamedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(unnamedItemDefs.size()); + for (QItemDefinition def : unnamedItemDefs) { + if (!def.definesNode()) { + defs.add((QPropertyDefinition) def); + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + public QPropertyDefinition[] getAutoCreatePropDefs() { + // since auto-create items must have a name, + // we're only searching the named item definitions + if (namedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition def : list) { + if (!def.definesNode() && def.isAutoCreated()) { + defs.add((QPropertyDefinition) def); + } + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + public QPropertyDefinition[] getMandatoryPropDefs() { + // since mandatory items must have a name, + // we're only searching the named item definitions + if (namedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition def : list) { + if (!def.definesNode() && def.isMandatory()) { + defs.add((QPropertyDefinition) def); + } + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + public QNodeDefinition[] getMandatoryNodeDefs() { + // since mandatory items must have a name, + // we're only searching the named item definitions + if (namedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition def : list) { + if (def.definesNode() && def.isMandatory()) { + defs.add((QNodeDefinition) def); + } + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + /** + * Determines whether this effective node type representation includes + * (either through inheritance or aggregation) the given node type. + * + * @param nodeTypeName name of node type + * @return true if the given node type is included, otherwise + * false + */ + public boolean includesNodeType(Name nodeTypeName) { + return allNodeTypes.contains(nodeTypeName); + } + + /** + * Determines whether this effective node type representation includes + * (either through inheritance or aggregation) all of the given node types. + * + * @param nodeTypeNames array of node type names + * @return true if all of the given node types are included, + * otherwise false + */ + public boolean includesNodeTypes(Name[] nodeTypeNames) { + return allNodeTypes.containsAll(Arrays.asList(nodeTypeNames)); + } + + /** + * Tests if the value constraints defined in the property definition + * pd are satisfied by the the specified values. + *

    + * Note that the protected flag is not checked. Also note that no + * type conversions are attempted if the type of the given values does not + * match the required type as specified in the given definition. + * + * @param pd The definiton of the property + * @param values An array of InternalValue objects. + * @throws ConstraintViolationException if the value constraints defined in + * the property definition are satisfied + * by the the specified values + * @throws RepositoryException if another error occurs + */ + public static void checkSetPropertyValueConstraints(QPropertyDefinition pd, + InternalValue[] values) + throws ConstraintViolationException, RepositoryException { + // check multi-value flag + if (!pd.isMultiple() && values != null && values.length > 1) { + throw new ConstraintViolationException("the property is not multi-valued"); + } + + QValueConstraint[] constraints = pd.getValueConstraints(); + if (constraints == null || constraints.length == 0) { + // no constraints to check + return; + } + if (values != null && values.length > 0) { + // check value constraints on every value + for (InternalValue value : values) { + // constraints are OR-ed together + boolean satisfied = false; + ConstraintViolationException cve = null; + for (QValueConstraint constraint : constraints) { + try { + constraint.check(value); + satisfied = true; + break; + } catch (ConstraintViolationException e) { + cve = e; + } + } + if (!satisfied) { + // re-throw last exception we encountered + throw cve; + } + } + } + } + + /** + * @param name + * @throws ConstraintViolationException + */ + public void checkAddNodeConstraints(Name name) + throws ConstraintViolationException { + try { + getApplicableChildNodeDef(name, null, null); + } catch (NoSuchNodeTypeException nsnte) { + String msg = "internal error: inconsistent node type"; + log.debug(msg); + throw new ConstraintViolationException(msg, nsnte); + } + } + + /** + * @param name + * @param nodeTypeName + * @param ntReg + * @throws ConstraintViolationException + * @throws NoSuchNodeTypeException + */ + public void checkAddNodeConstraints(Name name, Name nodeTypeName, + NodeTypeRegistry ntReg) + throws ConstraintViolationException, NoSuchNodeTypeException { + if (nodeTypeName != null) { + QNodeTypeDefinition ntDef = ntReg.getNodeTypeDef(nodeTypeName); + if (ntDef.isAbstract()) { + throw new ConstraintViolationException(nodeTypeName + " is abstract."); + } + if (ntDef.isMixin()) { + throw new ConstraintViolationException(nodeTypeName + " is mixin."); + } + } + QItemDefinition nd = getApplicableChildNodeDef(name, nodeTypeName, ntReg); + if (nd.isProtected()) { + throw new ConstraintViolationException(name + " is protected"); + } + if (nd.isAutoCreated()) { + throw new ConstraintViolationException(name + " is auto-created and can not be manually added"); + } + } + + /** + * Returns the applicable child node definition for a child node with the + * specified name and node type. If there are multiple applicable definitions + * named definitions will take precedence over residual definitions. + * + * @param name + * @param nodeTypeName + * @param ntReg + * @return + * @throws NoSuchNodeTypeException + * @throws ConstraintViolationException if no applicable child node definition + * could be found + */ + public QNodeDefinition getApplicableChildNodeDef(Name name, Name nodeTypeName, + NodeTypeRegistry ntReg) + throws NoSuchNodeTypeException, ConstraintViolationException { + EffectiveNodeType entTarget; + if (nodeTypeName != null) { + entTarget = ntReg.getEffectiveNodeType(nodeTypeName); + } else { + entTarget = null; + } + + // try named node definitions first + QItemDefinition[] defs = getNamedItemDefs(name); + for (QItemDefinition def : defs) { + if (def.definesNode()) { + QNodeDefinition nd = (QNodeDefinition) def; + Name[] types = nd.getRequiredPrimaryTypes(); + // node definition with that name exists + if (entTarget != null && types != null) { + // check 'required primary types' constraint + if (entTarget.includesNodeTypes(types)) { + // found named node definition + return nd; + } + } else if (nd.getDefaultPrimaryType() != null) { + // found node definition with default node type + return nd; + } + } + } + + // no item with that name defined; + // try residual node definitions + QNodeDefinition[] nda = getUnnamedNodeDefs(); + for (QNodeDefinition nd : nda) { + if (entTarget != null && nd.getRequiredPrimaryTypes() != null) { + // check 'required primary types' constraint + if (!entTarget.includesNodeTypes(nd.getRequiredPrimaryTypes())) { + continue; + } + // found residual node definition + return nd; + } else { + // since no node type has been specified for the new node, + // it must be determined from the default node type; + if (nd.getDefaultPrimaryType() != null) { + // found residual node definition with default node type + return nd; + } + } + } + + // no applicable definition found + throw new ConstraintViolationException("no matching child node definition found for " + name); + } + + /** + * Returns the applicable property definition for a property with the + * specified name, type and multiValued characteristic. If there are + * multiple applicable definitions the following rules will be applied: + *

      + *
    • named definitions are preferred to residual definitions
    • + *
    • definitions with specific required type are preferred to definitions + * with required type UNDEFINED
    • + *
    + * + * @param name + * @param type + * @param multiValued + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found + */ + public QPropertyDefinition getApplicablePropertyDef(Name name, int type, + boolean multiValued) + throws ConstraintViolationException { + // try named property definitions first + QPropertyDefinition match = + getMatchingPropDef(getNamedPropDefs(name), type, multiValued); + if (match != null) { + return match; + } + + // no item with that name defined; + // try residual property definitions + match = getMatchingPropDef(getUnnamedPropDefs(), type, multiValued); + if (match != null) { + return match; + } + + // no applicable definition found + throw new ConstraintViolationException("no matching property definition found for " + name); + } + + /** + * Returns the applicable property definition for a property with the + * specified name and type. The multiValued flag is not taken into account + * in the selection algorithm. Other than + * {@link #getApplicablePropertyDef(Name, int, boolean)} + * this method does not take the multiValued flag into account in the + * selection algorithm. If there more than one applicable definitions then + * the following rules are applied: + *
      + *
    • named definitions are preferred to residual definitions
    • + *
    • definitions with specific required type are preferred to definitions + * with required type UNDEFINED
    • + *
    • single-value definitions are preferred to multiple-value definitions
    • + *
    + * + * @param name + * @param type + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found + */ + public QPropertyDefinition getApplicablePropertyDef(Name name, int type) + throws ConstraintViolationException { + // try named property definitions first + QPropertyDefinition match = getMatchingPropDef(getNamedPropDefs(name), type); + if (match != null) { + return match; + } + + // no item with that name defined; + // try residual property definitions + match = getMatchingPropDef(getUnnamedPropDefs(), type); + if (match != null) { + return match; + } + + // no applicable definition found + throw new ConstraintViolationException("no matching property definition found for " + name); + } + + private QPropertyDefinition getMatchingPropDef(QPropertyDefinition[] defs, int type) { + QPropertyDefinition match = null; + for (QPropertyDefinition pd : defs) { + int reqType = pd.getRequiredType(); + // match type + if (reqType == PropertyType.UNDEFINED + || type == PropertyType.UNDEFINED + || reqType == type) { + if (match == null) { + match = pd; + } else { + // check if this definition is a better match than + // the one we've already got + if (match.getRequiredType() != pd.getRequiredType()) { + if (match.getRequiredType() == PropertyType.UNDEFINED) { + // found better match + match = pd; + } + } else { + if (match.isMultiple() && !pd.isMultiple()) { + // found better match + match = pd; + } + } + } + if (match.getRequiredType() != PropertyType.UNDEFINED + && !match.isMultiple()) { + // found best possible match, get outta here + return match; + } + } + } + return match; + } + + private QPropertyDefinition getMatchingPropDef(QPropertyDefinition[] defs, int type, + boolean multiValued) { + QPropertyDefinition match = null; + for (QPropertyDefinition pd : defs) { + int reqType = pd.getRequiredType(); + // match type + if (reqType == PropertyType.UNDEFINED + || type == PropertyType.UNDEFINED + || reqType == type) { + // match multiValued flag + if (multiValued == pd.isMultiple()) { + // found match + if (pd.getRequiredType() != PropertyType.UNDEFINED) { + // found best possible match, get outta here + return pd; + } else { + if (match == null) { + match = pd; + } + } + } + } + } + return match; + } + + /** + * @param name + * @throws ConstraintViolationException + */ + public void checkRemoveItemConstraints(Name name) throws ConstraintViolationException { + /** + * as there might be multiple definitions with the same name and we + * don't know which one is applicable, we check all of them + */ + QItemDefinition[] defs = getNamedItemDefs(name); + if (defs != null) { + for (QItemDefinition def : defs) { + if (def.isMandatory()) { + throw new ConstraintViolationException("can't remove mandatory item"); + } + if (def.isProtected()) { + throw new ConstraintViolationException("can't remove protected item"); + } + } + } + } + + /** + * @param name + * @throws ConstraintViolationException + */ + public void checkRemoveNodeConstraints(Name name) throws ConstraintViolationException { + /** + * as there might be multiple definitions with the same name and we + * don't know which one is applicable, we check all of them + */ + QNodeDefinition[] defs = getNamedNodeDefs(name); + if (defs != null) { + for (QNodeDefinition def : defs) { + if (def.isMandatory()) { + throw new ConstraintViolationException("can't remove mandatory node"); + } + if (def.isProtected()) { + throw new ConstraintViolationException("can't remove protected node"); + } + } + } + } + + /** + * @param name + * @throws ConstraintViolationException + */ + public void checkRemovePropertyConstraints(Name name) throws ConstraintViolationException { + /** + * as there might be multiple definitions with the same name and we + * don't know which one is applicable, we check all of them + */ + QItemDefinition[] defs = getNamedPropDefs(name); + if (defs != null) { + for (QItemDefinition def : defs) { + if (def.isMandatory()) { + throw new ConstraintViolationException("can't remove mandatory property"); + } + if (def.isProtected()) { + throw new ConstraintViolationException("can't remove protected property"); + } + } + } + } + + /** + * Merges another EffectiveNodeType with this one. + * Checks for merge conflicts. + * + * @param other + * @return + * @throws NodeTypeConflictException + */ + EffectiveNodeType merge(EffectiveNodeType other) + throws NodeTypeConflictException { + // create a clone of this instance and perform the merge on + // the 'clone' to avoid a potentially inconsistent state + // of this instance if an exception is thrown during + // the merge. + EffectiveNodeType copy = (EffectiveNodeType) clone(); + copy.internalMerge(other, false); + return copy; + } + + /** + * Internal helper method which merges another EffectiveNodeType + * instance with this instance. + *

    + * Warning: This instance might be in an inconsistent state if an exception + * is thrown. + * + * @param other + * @param supertype true if the merge is a result of inheritance, i.e. other + * represents one or more supertypes of this instance; otherwise false, i.e. + * the merge is the result of an explicit aggregation + * @throws NodeTypeConflictException + */ + private synchronized void internalMerge(EffectiveNodeType other, boolean supertype) + throws NodeTypeConflictException { + Name[] nta = other.getAllNodeTypes(); + int includedCount = 0; + for (Name aNta : nta) { + if (includesNodeType(aNta)) { + // redundant node type + log.debug("node type '" + aNta + "' is already contained."); + includedCount++; + } + } + if (includedCount == nta.length) { + // total overlap, ignore + return; + } + + // named item definitions + QItemDefinition[] defs = other.getNamedItemDefs(); + for (QItemDefinition def : defs) { + if (includesNodeType(def.getDeclaringNodeType())) { + // ignore redundant definitions + continue; + } + Name name = def.getName(); + List existingDefs = namedItemDefs.get(name); + if (existingDefs != null) { + if (existingDefs.size() > 0) { + // there already exists at least one definition with that name + for (QItemDefinition existingDef : existingDefs) { + // make sure none of them is auto-create + if (def.isAutoCreated() || existingDef.isAutoCreated()) { + // conflict + String msg = "The item definition for '" + name + + "' in node type '" + + def.getDeclaringNodeType() + + "' conflicts with node type '" + + existingDef.getDeclaringNodeType() + + "': name collision with auto-create definition"; + log.debug(msg); + throw new NodeTypeConflictException(msg); + } + // check ambiguous definitions + if (def.definesNode() == existingDef.definesNode()) { + if (!def.definesNode()) { + // property definition + QPropertyDefinition pd = (QPropertyDefinition) def; + QPropertyDefinition epd = (QPropertyDefinition) existingDef; + // compare type & multiValued flag + if (pd.getRequiredType() == epd.getRequiredType() + && pd.isMultiple() == epd.isMultiple()) { + // conflict + String msg = "The property definition for '" + + name + "' in node type '" + + def.getDeclaringNodeType() + + "' conflicts with node type '" + + existingDef.getDeclaringNodeType() + + "': ambiguous property definition"; + log.debug(msg); + throw new NodeTypeConflictException(msg); + } + } else { + // child node definition + // conflict + String msg = "The child node definition for '" + + name + "' in node type '" + + def.getDeclaringNodeType() + + "' conflicts with node type '" + + existingDef.getDeclaringNodeType() + + "': ambiguous child node definition"; + log.debug(msg); + throw new NodeTypeConflictException(msg); + } + } + } + } + } else { + existingDefs = new ArrayList(); + namedItemDefs.put(name, existingDefs); + } + existingDefs.add(def); + } + + // residual item definitions + defs = other.getUnnamedItemDefs(); + for (QItemDefinition def : defs) { + if (includesNodeType(def.getDeclaringNodeType())) { + // ignore redundant definitions + continue; + } + for (QItemDefinition existing : unnamedItemDefs) { + // compare with existing definition + if (def.definesNode() == existing.definesNode()) { + if (!def.definesNode()) { + // property definition + QPropertyDefinition pd = (QPropertyDefinition) def; + QPropertyDefinition epd = (QPropertyDefinition) existing; + // compare type & multiValued flag + if (pd.getRequiredType() == epd.getRequiredType() + && pd.isMultiple() == epd.isMultiple()) { + // conflict + String msg = "A property definition in node type '" + + def.getDeclaringNodeType() + + "' conflicts with node type '" + + existing.getDeclaringNodeType() + + "': ambiguous residual property definition"; + log.debug(msg); + throw new NodeTypeConflictException(msg); + } + } else { + // child node definition + QNodeDefinition nd = (QNodeDefinition) def; + QNodeDefinition end = (QNodeDefinition) existing; + // compare required & default primary types + if (Arrays.equals(nd.getRequiredPrimaryTypes(), end.getRequiredPrimaryTypes()) + && (nd.getDefaultPrimaryType() == null + ? end.getDefaultPrimaryType() == null + : nd.getDefaultPrimaryType().equals(end.getDefaultPrimaryType()))) { + // conflict + String msg = "A child node definition in node type '" + + def.getDeclaringNodeType() + + "' conflicts with node type '" + + existing.getDeclaringNodeType() + + "': ambiguous residual child node definition"; + log.debug(msg); + throw new NodeTypeConflictException(msg); + } + } + } + } + unnamedItemDefs.add(def); + } + allNodeTypes.addAll(Arrays.asList(nta)); + + if (supertype) { + // implicit merge as result of inheritance + + // add other merged node types as supertypes + nta = other.getMergedNodeTypes(); + inheritedNodeTypes.addAll(Arrays.asList(nta)); + // add supertypes of other merged node types as supertypes + nta = other.getInheritedNodeTypes(); + inheritedNodeTypes.addAll(Arrays.asList(nta)); + } else { + // explicit merge + + // merge with other merged node types + nta = other.getMergedNodeTypes(); + mergedNodeTypes.addAll(Arrays.asList(nta)); + // add supertypes of other merged node types as supertypes + nta = other.getInheritedNodeTypes(); + inheritedNodeTypes.addAll(Arrays.asList(nta)); + } + + // update 'orderable child nodes' attribute value (JCR-1947) + if (other.hasOrderableChildNodes()) { + orderableChildNodes = true; + } + + // update 'primary item' attribute value (JCR-1947) + if (primaryItemName == null && other.getPrimaryItemName() != null) { + primaryItemName = other.getPrimaryItemName(); + } + } + + @Override + protected Object clone() { + EffectiveNodeType clone = new EffectiveNodeType(); + + clone.mergedNodeTypes.addAll(mergedNodeTypes); + clone.inheritedNodeTypes.addAll(inheritedNodeTypes); + clone.allNodeTypes.addAll(allNodeTypes); + for (Name name : namedItemDefs.keySet()) { + List list = namedItemDefs.get(name); + clone.namedItemDefs.put(name, new ArrayList(list)); + } + clone.unnamedItemDefs.addAll(unnamedItemDefs); + clone.orderableChildNodes = orderableChildNodes; + clone.primaryItemName = primaryItemName; + return clone; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeTypeCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeTypeCache.java new file mode 100644 index 00000000000..e09ef413a63 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeTypeCache.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.core.nodetype; + +import org.apache.jackrabbit.spi.Name; + +/** + * EffectiveNodeTypeCache defines the interface for a cache for + * effective node types. Effective node types are addressed by {@link Key}s. + */ +public interface EffectiveNodeTypeCache extends Cloneable { + + /** + * Puts an effective node type to the cache. The key is internally generated + * from the set of merged node types. + * @param ent the effective node type to put to the cache + */ + void put(EffectiveNodeType ent); + + /** + * Puts an effective node type to the cache for the given key. + * @param key the key for the effective node type + * @param ent the effective node type to put to the cache + */ + void put(Key key, EffectiveNodeType ent); + + /** + * Checks if the effective node type for the given key exists. + * @param key the key to check + * @return true if the effective node type is cached; + * false otherwise. + */ + boolean contains(Key key); + + /** + * Returns the effective node type for the given key or null if + * the desired node type is not cached. + * @param key the key for the effective node type. + * @return the effective node type or null + */ + EffectiveNodeType get(Key key); + + /** + * Returns a key for an effective node type that consists of the given + * node type names. + * @param ntNames the array of node type names for the effective node type + * @return the key to an effective node type. + */ + Key getKey(Name[] ntNames); + + /** + * Removes all effective node types that are aggregated with the node type + * of the given name. + * @param name the name of the node type. + */ + void invalidate(Name name); + + /** + * {@inheritDoc} + */ + Object clone(); + + /** + * Searches the best key k for which the given key is a super + * set, i.e. for which {@link Key#contains(Key)}} returns + * true. If an already cached effective node type matches the + * key it is returned. + * + * @param key the key for which the subkey is to be searched + * @return the best key or null if no key could be found. + */ + Key findBest(Key key); + + /** + * An ENTKey uniquely identifies + * a combination (i.e. an aggregation) of one or more node types. + */ + interface Key extends Comparable { + + /** + * Returns the node type names of this key. + * @return the node type names of this key. + */ + Name[] getNames(); + + /** + * Checks if the otherKey is contained in this one. I.e. if + * this key contains all node type names of the other key. + * @param otherKey the other key to check + * @return true if this key contains the other key; + * false otherwise. + */ + boolean contains(Key otherKey); + + /** + * Creates a new key as a result of a subtract operation. i.e. removes all + * node type names that from the other key. + *

    + * Please note that no exception is thrown if the other key has node type + * names that are not contained in this key (i.e. {@link #contains(Key)} + * returns false). + * + * @param otherKey the other key to subtract + * @return the new key of the subtraction operation. + */ + Key subtract(Key otherKey); + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeTypeCacheImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeTypeCacheImpl.java new file mode 100644 index 00000000000..1043d32c9b4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/EffectiveNodeTypeCacheImpl.java @@ -0,0 +1,366 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype; + +import org.apache.jackrabbit.spi.Name; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; +import java.util.ArrayList; + +/** + * EffectiveNodeTypeCache implementation that uses an array of + * node type names as key for caching the effective node types. + */ +public class EffectiveNodeTypeCacheImpl implements EffectiveNodeTypeCache { + + /** + * ordered set of keys + */ + private final TreeSet sortedKeys; + + /** + * cache of pre-built aggregations of node types + */ + private final HashMap aggregates; + + /** + * Creates a new effective node type cache. + */ + EffectiveNodeTypeCacheImpl() { + sortedKeys = new TreeSet(); + aggregates = new HashMap(); + } + + /** + * {@inheritDoc} + */ + public Key getKey(Name[] ntNames) { + return new WeightedKey(ntNames); + } + + /** + * {@inheritDoc} + */ + public void put(EffectiveNodeType ent) { + // we define the weight as the total number of included node types + // (through aggregation and inheritance) + int weight = ent.getMergedNodeTypes().length; + // the effective node type is identified by the list of merged + // (i.e. aggregated) node types + WeightedKey k = new WeightedKey(ent.getMergedNodeTypes(), weight); + put(k, ent); + } + + /** + * {@inheritDoc} + */ + public void put(Key key, EffectiveNodeType ent) { + aggregates.put(key, ent); + sortedKeys.add(key); + } + + /** + * {@inheritDoc} + */ + public boolean contains(Key key) { + return aggregates.containsKey(key); + } + + /** + * {@inheritDoc} + */ + public EffectiveNodeType get(Key key) { + return aggregates.get(key); + } + + /** + * Removes the effective node type for the given key from the cache. + * @param key the key of the effective node type to remove + * @return the removed effective node type or null if it was + * never cached. + */ + private EffectiveNodeType remove(Key key) { + EffectiveNodeType removed = aggregates.remove(key); + if (removed != null) { + // remove index entry + + // FIXME: can't simply call TreeSet.remove(key) because the entry + // in sortedKeys might have a different weight and would thus + // not be found + Iterator iter = sortedKeys.iterator(); + while (iter.hasNext()) { + Key k = iter.next(); + // WeightedKey.equals(Object) ignores the weight + if (key.equals(k)) { + sortedKeys.remove(k); + break; + } + } + } + return removed; + } + + /** + * {@inheritDoc} + */ + public void invalidate(Name name) { + // remove all affected effective node types from aggregates cache + // (copy keys first to prevent ConcurrentModificationException) + ArrayList keys = new ArrayList(sortedKeys); + for (Iterator keysIter = keys.iterator(); keysIter.hasNext();) { + Key k = keysIter.next(); + EffectiveNodeType ent = get(k); + if (ent.includesNodeType(name)) { + remove(k); + } + } + } + + /** + * {@inheritDoc} + */ + public Key findBest(Key key) { + // quick check for already cached key + if (contains(key)) { + return key; + } + Iterator iter = sortedKeys.iterator(); + while (iter.hasNext()) { + Key k = iter.next(); + // check if the existing aggregate is a 'subset' of the one we're + // looking for + if (key.contains(k)) { + return k; + } + } + return null; + } + + //-------------------------------------------< java.lang.Object overrides > + + /** + * {@inheritDoc} + */ + @Override + public Object clone() { + EffectiveNodeTypeCacheImpl clone = new EffectiveNodeTypeCacheImpl(); + clone.sortedKeys.addAll(sortedKeys); + clone.aggregates.putAll(aggregates); + return clone; + } + + //--------------------------------------------------------------< Object > + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("EffectiveNodeTypeCache (" + super.toString() + ")\n"); + builder.append("EffectiveNodeTypes in cache:\n"); + for (Key key : sortedKeys) { + builder.append(key); + builder.append("\n"); + } + return builder.toString(); + } + + //--------------------------------------------------------< inner classes > + /** + * A WeightedKey uniquely identifies + * a combination (i.e. an aggregation) of one or more node types. + * The weight is an indicator for the cost involved in building such an + * aggregate (e.g. an aggregation of multiple complex node types with deep + * inheritance trees is more costly to build/validate than an aggregation + * of two very simple node types with just one property definition each). + *

    + * A very simple (and not very accurate) approximation of the weight would + * be the number of explicitly aggregated node types (ignoring inheritance + * and complexity of each involved node type). A better approximation would + * be the number of all, explicitly and implicitly (note that + * inheritance is also an aggregation) aggregated node types. + *

    + * The more accurate the weight definition, the more efficient is the + * the building of new aggregates. + *

    + * It is important to note that the weight is not part of the key value, + * i.e. it is not considered by the hashCode() and + * equals(Object) methods. It does however affect the order + * of WeightedKey instances. See + * {@link #compareTo(Object)} for more information. + *

    + * Let's assume we have an aggregation of node types named "b", "a" and "c". + * Its key would be "[a, b, c]" and the weight 3 (using the simple + * approximation). + */ + private static class WeightedKey implements Key { + + /** + * array of node type names, sorted in ascending order + */ + private final Name[] names; + + /** + * the weight of this key + */ + private final int weight; + + /** + * @param ntNames + */ + WeightedKey(Name[] ntNames) { + this(ntNames, ntNames.length); + } + + /** + * @param ntNames + * @param weight + */ + WeightedKey(Name[] ntNames, int weight) { + this.weight = weight; + names = new Name[ntNames.length]; + System.arraycopy(ntNames, 0, names, 0, names.length); + Arrays.sort(names); + } + + /** + * @param ntNames + */ + WeightedKey(Collection ntNames) { + this(ntNames, ntNames.size()); + } + + /** + * @param ntNames + * @param weight + */ + WeightedKey(Collection ntNames, int weight) { + this((Name[]) ntNames.toArray(new Name[ntNames.size()]), weight); + } + + /** + * @return the node type names of this key + */ + public Name[] getNames() { + return names; + } + + /** + * {@inheritDoc} + */ + public boolean contains(Key otherKey) { + WeightedKey key = (WeightedKey) otherKey; + Set tmp = new HashSet(Arrays.asList(names)); + for (int i = 0; i < key.names.length; i++) { + if (!tmp.contains(key.names[i])) { + return false; + } + } + return true; + } + + /** + * {@inheritDoc} + */ + public Key subtract(Key otherKey) { + WeightedKey key = (WeightedKey) otherKey; + Set tmp = new HashSet(Arrays.asList(names)); + tmp.removeAll(Arrays.asList(key.names)); + return new WeightedKey(tmp); + + } + + //-------------------------------------------------------< Comparable > + /** + * The resulting sort-order is: 1. descending weight, 2. ascending key + * (i.e. string representation of this sorted set). + * + * @param o the other key to compare + * @return the result of the comparison + */ + public int compareTo(Key o) { + WeightedKey other = (WeightedKey) o; + + // compare weights + if (weight > other.weight) { + return -1; + } else if (weight < other.weight) { + return 1; + } + + // compare arrays of names + int len1 = names.length; + int len2 = other.names.length; + int len = Math.min(len1, len2); + + for (int i = 0; i < len; i++) { + Name name1 = names[i]; + Name name2 = other.names[i]; + int result = name1.compareTo(name2); + if (result != 0) { + return result; + } + } + return len1 - len2; + } + + //---------------------------------------< java.lang.Object overrides > + + /** + * {@inheritDoc} + */ + public int hashCode() { + int h = 17; + // ignore weight + for (Name name : names) { + h *= 37; + h += name.hashCode(); + } + return h; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof WeightedKey) { + WeightedKey other = (WeightedKey) obj; + // ignore weight + return Arrays.equals(names, other.names); + } + return false; + } + + /** + * {@inheritDoc} + */ + public String toString() { + return Arrays.asList(names).toString() + " (" + weight + ")"; + } + + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/InvalidNodeTypeDefException.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/InvalidNodeTypeDefException.java new file mode 100644 index 00000000000..65fa64f2c2a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/InvalidNodeTypeDefException.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.core.nodetype; + +/** + * The NodeTypeConflictException ... + */ +public class InvalidNodeTypeDefException extends Exception { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public InvalidNodeTypeDefException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public InvalidNodeTypeDefException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeConflictException.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeConflictException.java new file mode 100644 index 00000000000..71fd17131c8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeConflictException.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.core.nodetype; + +/** + * The NodeTypeConflictException ... + */ +public class NodeTypeConflictException extends Exception { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public NodeTypeConflictException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public NodeTypeConflictException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeDefStore.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeDefStore.java new file mode 100644 index 00000000000..81ebf9c5c17 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeDefStore.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.core.nodetype; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefReader; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.core.nodetype.xml.NodeTypeReader; +import org.apache.jackrabbit.core.nodetype.xml.NodeTypeWriter; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping; +import org.apache.jackrabbit.spi.commons.nodetype.QDefinitionBuilderFactory; + +/** + * NodeTypeDefStore ... + */ +public class NodeTypeDefStore { + + /** Map of node type names to node type definitions. */ + private final Map ntDefs; + + /** + * Empty default constructor. + */ + public NodeTypeDefStore() throws RepositoryException { + ntDefs = new HashMap(); + } + + /** + * @param in + * @throws IOException + * @throws InvalidNodeTypeDefException + */ + public void load(InputStream in) + throws IOException, InvalidNodeTypeDefException, + RepositoryException { + QNodeTypeDefinition[] types = NodeTypeReader.read(in); + for (QNodeTypeDefinition type : types) { + add(type); + } + } + + /** + * Loads node types from a CND stream. + * + * @param in reader containing the nodetype definitions + * @param systemId optional name of the stream + * + * @throws IOException if an I/O error during reading occurs + * @throws InvalidNodeTypeDefException if the CND cannot be parsed + */ + public void loadCND(Reader in, String systemId) + throws IOException, InvalidNodeTypeDefException { + try { + CompactNodeTypeDefReader r = + new CompactNodeTypeDefReader( + in, systemId, new QDefinitionBuilderFactory()); + + for (QNodeTypeDefinition qdef: r.getNodeTypeDefinitions()) { + add(qdef); + } + } catch (ParseException e) { + throw new InvalidNodeTypeDefException("Unable to parse CND stream.", e); + } + } + + /** + * @param out + * @param registry + * @throws IOException + * @throws RepositoryException + */ + public void store(OutputStream out, NamespaceRegistry registry) + throws IOException, RepositoryException { + QNodeTypeDefinition[] types = ntDefs.values().toArray(new QNodeTypeDefinition[ntDefs.size()]); + NodeTypeWriter.write(out, types, registry); + } + + /** + * @param ntd + */ + public void add(QNodeTypeDefinition ntd) { + ntDefs.put(ntd.getName(), ntd); + } + + /** + * @param name + * @return + */ + public boolean remove(Name name) { + return (ntDefs.remove(name) != null); + } + + /** + * + */ + public void removeAll() { + ntDefs.clear(); + } + + /** + * @param name + * @return + */ + public boolean contains(Name name) { + return ntDefs.containsKey(name); + } + + /** + * @param name + * @return + */ + public QNodeTypeDefinition get(Name name) { + return ntDefs.get(name); + } + + /** + * @return + */ + public Collection all() { + return Collections.unmodifiableCollection(ntDefs.values()); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeDefinitionImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeDefinitionImpl.java new file mode 100644 index 00000000000..817bcc8fca8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeDefinitionImpl.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.core.nodetype; + +import javax.jcr.nodetype.NodeTypeDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.NamespaceException; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.PropertyDefinition; + +/** + * This class implements the NodeTypeDefinition interface. + * All method calls are delegated to the wrapped {@link QNodeTypeDefinition}, + * performing the translation from Names to JCR names + * (and vice versa) where necessary. + */ +public class NodeTypeDefinitionImpl implements NodeTypeDefinition { + + private static Logger log = LoggerFactory.getLogger(NodeTypeDefinitionImpl.class); + + private final QNodeTypeDefinition ntd; + // resolver used to translate Names to JCR name strings. + private final NamePathResolver resolver; + private final ValueFactory valueFactory; + + public NodeTypeDefinitionImpl(QNodeTypeDefinition ntd, NamePathResolver resolver, ValueFactory valueFactory) { + this.ntd = ntd; + this.resolver = resolver; + this.valueFactory = valueFactory; + } + + //---------------------------------------------------< NodeTypeDefinition > + /** + * {@inheritDoc} + */ + public String getName() { + try { + return resolver.getJCRName(ntd.getName()); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in node type name", e); + return ntd.getName().toString(); + } + } + + /** + * Returns the names of the supertypes actually declared in this node type. + *

    + * In implementations that support node type registration, if this + * NodeTypeDefinition object is actually a newly-created empty + * NodeTypeTemplate, then this method will return an array + * containing a single string indicating the node type + * nt:base. + * + * @return an array of Strings + * @since JCR 2.0 + */ + public String[] getDeclaredSupertypeNames() { + Name[] ntNames = ntd.getSupertypes(); + String[] supertypes = new String[ntNames.length]; + for (int i = 0; i < ntNames.length; i++) { + try { + supertypes[i] = resolver.getJCRName(ntNames[i]); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in node type name", e); + supertypes[i] = ntNames[i].toString(); + } + } + return supertypes; + } + + /** + * Returns true if this is an abstract node type; returns + * false otherwise. + *

    + * An abstract node type is one that cannot be assigned as the primary or + * mixin type of a node but can be used in the definitions of other node + * types as a superclass. + *

    + * In implementations that support node type registration, if this + * NodeTypeDefinition object is actually a newly-created empty + * NodeTypeTemplate, then this method will return + * false. + * + * @return a boolean + * @since JCR 2.0 + */ + public boolean isAbstract() { + return ntd.isAbstract(); + } + + /** + * Returns true if the node type is queryable, meaning that + * the available-query-operators, full-text-searchable + * and query-orderable attributes of its property definitions take effect. See + * {@link javax.jcr.nodetype.PropertyDefinition#getAvailableQueryOperators()}, + * {@link javax.jcr.nodetype.PropertyDefinition#isFullTextSearchable()} and + * {@link javax.jcr.nodetype.PropertyDefinition#isQueryOrderable()}. + *

    + * If a node type is declared non-queryable then these attributes of its property + * definitions have no effect. + * + * @since JCR 2.0 + * @return a boolean + */ + public boolean isQueryable() { + return ntd.isQueryable(); + } + + /** + * {@inheritDoc} + */ + public boolean isMixin() { + return ntd.isMixin(); + } + + /** + * {@inheritDoc} + */ + public boolean hasOrderableChildNodes() { + return ntd.hasOrderableChildNodes(); + } + + /** + * {@inheritDoc} + */ + public String getPrimaryItemName() { + try { + Name piName = ntd.getPrimaryItemName(); + if (piName != null) { + return resolver.getJCRName(piName); + } else { + return null; + } + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in name of primary item", e); + return ntd.getName().toString(); + } + } + + /** + * {@inheritDoc} + */ + public NodeDefinition[] getDeclaredChildNodeDefinitions() { + QItemDefinition[] cnda = ntd.getChildNodeDefs(); + NodeDefinition[] nodeDefs = new NodeDefinition[cnda.length]; + for (int i = 0; i < cnda.length; i++) { + nodeDefs[i] = new NodeDefinitionImpl(cnda[i], null, resolver); + } + return nodeDefs; + } + + /** + * {@inheritDoc} + */ + public PropertyDefinition[] getDeclaredPropertyDefinitions() { + QPropertyDefinition[] pda = ntd.getPropertyDefs(); + PropertyDefinition[] propDefs = new PropertyDefinition[pda.length]; + for (int i = 0; i < pda.length; i++) { + propDefs[i] = new PropertyDefinitionImpl(pda[i], null, resolver, valueFactory); + } + return propDefs; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeImpl.java new file mode 100644 index 00000000000..567069dd2c8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeImpl.java @@ -0,0 +1,505 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.List; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.nodetype.AbstractNodeType; +import org.apache.jackrabbit.value.ValueHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A NodeTypeImpl ... + */ +public class NodeTypeImpl extends AbstractNodeType implements NodeType, NodeTypeDefinition { + + private static Logger log = LoggerFactory.getLogger(NodeTypeImpl.class); + + private final EffectiveNodeType ent; + private final NodeTypeManagerImpl ntMgr; + // value factory used for type conversion + private final ValueFactory valueFactory; + private final DataStore store; + + /** + * Package private constructor + *

    + * Creates a valid node type instance. We assume that the node type + * definition is valid and all referenced node types (supertypes, required + * node types etc.) do exist and are valid. + * + * @param ent the effective (i.e. merged and resolved) node type + * representation + * @param ntd the definition of this node type + * @param ntMgr the node type manager associated with this node type + * @param resolver the name path resolver of the session. + * @param valueFactory the value factory of the session. + * @param store the data store or null if none is + * configured. + */ + NodeTypeImpl(EffectiveNodeType ent, + QNodeTypeDefinition ntd, + NodeTypeManagerImpl ntMgr, + NamePathResolver resolver, + ValueFactory valueFactory, + DataStore store) { + super(ntd, ntMgr, resolver); + this.ent = ent; + this.ntMgr = ntMgr; + this.valueFactory = valueFactory; + this.store = store; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isNodeType(Name nodeTypeName) { + return ent.includesNodeType(nodeTypeName); + } + + /** + * Checks if this node type is directly or indirectly derived from the + * specified node type. + * + * @param nodeTypeName the name of a node type. + * @return true if this node type is directly or indirectly derived from the + * specified node type, otherwise false. + */ + public boolean isDerivedFrom(Name nodeTypeName) { + return !nodeTypeName.equals(ntd.getName()) && ent.includesNodeType(nodeTypeName); + } + + /** + * Returns an array containing only those child node definitions of this + * node type (including the child node definitions inherited from supertypes + * of this node type) where {@link NodeDefinition#isAutoCreated()} + * returns true. + * + * @return an array of child node definitions. + * @see NodeDefinition#isAutoCreated + */ + public NodeDefinition[] getAutoCreatedNodeDefinitions() { + QNodeDefinition[] cnda = ent.getAutoCreateNodeDefs(); + NodeDefinition[] nodeDefs = new NodeDefinition[cnda.length]; + for (int i = 0; i < cnda.length; i++) { + nodeDefs[i] = ntMgr.getNodeDefinition(cnda[i]); + } + return nodeDefs; + } + + /** + * Returns an array containing only those property definitions of this + * node type (including the property definitions inherited from supertypes + * of this node type) where {@link PropertyDefinition#isAutoCreated()} + * returns true. + * + * @return an array of property definitions. + * @see PropertyDefinition#isAutoCreated + */ + public PropertyDefinition[] getAutoCreatedPropertyDefinitions() { + QPropertyDefinition[] pda = ent.getAutoCreatePropDefs(); + PropertyDefinition[] propDefs = new PropertyDefinition[pda.length]; + for (int i = 0; i < pda.length; i++) { + propDefs[i] = ntMgr.getPropertyDefinition(pda[i]); + } + return propDefs; + } + + /** + * Returns an array containing only those property definitions of this + * node type (including the property definitions inherited from supertypes + * of this node type) where {@link PropertyDefinition#isMandatory()} + * returns true. + * + * @return an array of property definitions. + * @see PropertyDefinition#isMandatory + */ + public PropertyDefinition[] getMandatoryPropertyDefinitions() { + QPropertyDefinition[] pda = ent.getMandatoryPropDefs(); + PropertyDefinition[] propDefs = new PropertyDefinition[pda.length]; + for (int i = 0; i < pda.length; i++) { + propDefs[i] = ntMgr.getPropertyDefinition(pda[i]); + } + return propDefs; + } + + /** + * Returns an array containing only those child node definitions of this + * node type (including the child node definitions inherited from supertypes + * of this node type) where {@link NodeDefinition#isMandatory()} + * returns true. + * + * @return an array of child node definitions. + * @see NodeDefinition#isMandatory + */ + public NodeDefinition[] getMandatoryNodeDefinitions() { + QNodeDefinition[] cnda = ent.getMandatoryNodeDefs(); + NodeDefinition[] nodeDefs = new NodeDefinition[cnda.length]; + for (int i = 0; i < cnda.length; i++) { + nodeDefs[i] = ntMgr.getNodeDefinition(cnda[i]); + } + return nodeDefs; + } + + /** + * Returns the Name of this node type. + * + * @return the name + */ + public Name getQName() { + return ntd.getName(); + } + + /** + * Returns all inherited supertypes of this node type. + * + * @return an array of NodeType objects. + * @see #getSupertypes + * @see #getDeclaredSupertypes + */ + public NodeType[] getInheritedSupertypes() { + // declared supertypes + Name[] ntNames = ntd.getSupertypes(); + Set declared = new HashSet(); + for (Name ntName : ntNames) { + declared.add(ntName); + } + // all supertypes + ntNames = ent.getInheritedNodeTypes(); + + // filter from all supertypes those that are not declared + List inherited = new ArrayList(); + for (Name ntName : ntNames) { + if (!declared.contains(ntName)) { + try { + inherited.add(ntMgr.getNodeType(ntName)); + } catch (NoSuchNodeTypeException e) { + // should never get here + log.error("undefined supertype", e); + return new NodeType[0]; + } + } + } + + return inherited.toArray(new NodeType[inherited.size()]); + } + + + //---------------------------------------------------< NodeTypeDefinition > + + /** + * {@inheritDoc} + */ + public boolean hasOrderableChildNodes() { + return ent.hasOrderableChildNodes(); + } + + //-------------------------------------------------------------< NodeType > + + /** + * {@inheritDoc} + */ + public NodeType[] getSupertypes() { + Name[] ntNames = ent.getInheritedNodeTypes(); + NodeType[] supertypes = new NodeType[ntNames.length]; + for (int i = 0; i < ntNames.length; i++) { + try { + supertypes[i] = ntMgr.getNodeType(ntNames[i]); + } catch (NoSuchNodeTypeException e) { + // should never get here + log.error("undefined supertype", e); + return new NodeType[0]; + } + } + return supertypes; + } + + /** + * {@inheritDoc} + */ + public NodeDefinition[] getChildNodeDefinitions() { + QNodeDefinition[] cnda = ent.getAllNodeDefs(); + NodeDefinition[] nodeDefs = new NodeDefinition[cnda.length]; + for (int i = 0; i < cnda.length; i++) { + nodeDefs[i] = ntMgr.getNodeDefinition(cnda[i]); + } + return nodeDefs; + } + + /** + * {@inheritDoc} + */ + public PropertyDefinition[] getPropertyDefinitions() { + QPropertyDefinition[] pda = ent.getAllPropDefs(); + PropertyDefinition[] propDefs = new PropertyDefinition[pda.length]; + for (int i = 0; i < pda.length; i++) { + propDefs[i] = ntMgr.getPropertyDefinition(pda[i]); + } + return propDefs; + } + + /** + * {@inheritDoc} + */ + public boolean canSetProperty(String propertyName, Value value) { + if (value == null) { + // setting a property to null is equivalent of removing it + return canRemoveItem(propertyName); + } + try { + Name name = resolver.getQName(propertyName); + QPropertyDefinition def; + try { + // try to get definition that matches the given value type + def = ent.getApplicablePropertyDef(name, value.getType(), false); + } catch (ConstraintViolationException cve) { + // fallback: ignore type + def = ent.getApplicablePropertyDef(name, PropertyType.UNDEFINED, false); + } + if (def.isProtected()) { + return false; + } + if (def.isMultiple()) { + return false; + } + int targetType; + if (def.getRequiredType() != PropertyType.UNDEFINED + && def.getRequiredType() != value.getType()) { + // type conversion required + targetType = def.getRequiredType(); + } else { + // no type conversion required + targetType = value.getType(); + } + // perform type conversion as necessary and create InternalValue + // from (converted) Value + InternalValue internalValue; + if (targetType != value.getType()) { + // type conversion required + Value targetVal = ValueHelper.convert( + value, targetType, + valueFactory); + internalValue = InternalValue.create(targetVal, resolver, store); + } else { + // no type conversion required + internalValue = InternalValue.create(value, resolver, store); + } + EffectiveNodeType.checkSetPropertyValueConstraints( + def, new InternalValue[]{internalValue}); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean canSetProperty(String propertyName, Value[] values) { + if (values == null) { + // setting a property to null is equivalent of removing it + return canRemoveItem(propertyName); + } + try { + Name name = resolver.getQName(propertyName); + // determine type of values + int type = PropertyType.UNDEFINED; + for (Value value : values) { + if (value == null) { + // skip null values as those would be purged + continue; + } + if (type == PropertyType.UNDEFINED) { + type = value.getType(); + } else if (type != value.getType()) { + // inhomogeneous types + return false; + } + } + QPropertyDefinition def; + try { + // try to get definition that matches the given value type + def = ent.getApplicablePropertyDef(name, type, true); + } catch (ConstraintViolationException cve) { + // fallback: ignore type + def = ent.getApplicablePropertyDef(name, PropertyType.UNDEFINED, true); + } + + if (def.isProtected()) { + return false; + } + if (!def.isMultiple()) { + return false; + } + // determine target type + int targetType; + if (def.getRequiredType() != PropertyType.UNDEFINED + && def.getRequiredType() != type) { + // type conversion required + targetType = def.getRequiredType(); + } else { + // no type conversion required + targetType = type; + } + + List list = new ArrayList(); + // convert values and compact array (purge null entries) + for (Value value : values) { + if (value != null) { + // perform type conversion as necessary and create InternalValue + // from (converted) Value + InternalValue internalValue; + if (targetType != type) { + // type conversion required + Value targetVal = ValueHelper.convert(value, targetType, valueFactory); + internalValue = InternalValue.create(targetVal, resolver, store); + } else { + // no type conversion required + internalValue = InternalValue.create(value, resolver, store); + } + list.add(internalValue); + } + } + InternalValue[] internalValues = list.toArray(new InternalValue[list.size()]); + EffectiveNodeType.checkSetPropertyValueConstraints(def, internalValues); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean canAddChildNode(String childNodeName) { + try { + ent.checkAddNodeConstraints(resolver.getQName(childNodeName)); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean canAddChildNode(String childNodeName, String nodeTypeName) { + try { + ent.checkAddNodeConstraints( + resolver.getQName(childNodeName), + resolver.getQName(nodeTypeName), + ntMgr.getNodeTypeRegistry()); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean canRemoveItem(String itemName) { + try { + ent.checkRemoveItemConstraints(resolver.getQName(itemName)); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + //--------------------------------------------------< new JSR 283 methods > + /** + * Returns true if removing the child node called + * nodeName is allowed by this node type. Returns + * false otherwise. + * + * @param nodeName The name of the child node + * @return a boolean + * @since JCR 2.0 + */ + public boolean canRemoveNode(String nodeName) { + try { + ent.checkRemoveNodeConstraints(resolver.getQName(nodeName)); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * Returns true if removing the property called + * propertyName is allowed by this node type. Returns + * false otherwise. + * + * @param propertyName The name of the property + * @return a boolean + * @since JCR 2.0 + */ + public boolean canRemoveProperty(String propertyName) { + try { + ent.checkRemovePropertyConstraints(resolver.getQName(propertyName)); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeManagerImpl.java new file mode 100644 index 00000000000..19c34205849 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeManagerImpl.java @@ -0,0 +1,663 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.nodetype.NodeTypeIterator; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.api.JackrabbitNodeTypeManager; +import org.apache.jackrabbit.commons.NamespaceHelper; +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.core.nodetype.xml.NodeTypeReader; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping; +import org.apache.jackrabbit.spi.commons.nodetype.AbstractNodeTypeManager; +import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; +import org.apache.jackrabbit.spi.commons.nodetype.QDefinitionBuilderFactory; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * A NodeTypeManagerImpl implements a session dependant + * NodeTypeManager. + */ +public class NodeTypeManagerImpl extends AbstractNodeTypeManager + implements JackrabbitNodeTypeManager, NodeTypeRegistryListener { + + /** + * Component context of the current session. + */ + private final SessionContext context; + + /** + * The root node definition. + */ + private final NodeDefinitionImpl rootNodeDef; + + /** + * A cache for NodeType instances created by this + * NodeTypeManager + */ + private final Map ntCache; + + /** + * A cache for PropertyDefinition instances created by this + * NodeTypeManager + */ + private final Map pdCache; + + /** + * A cache for NodeDefinition instances created by this + * NodeTypeManager + */ + private final Map ndCache; + + /** + * Creates a new NodeTypeManagerImpl instance. + * + * @param context the session context + */ + @SuppressWarnings("unchecked") + public NodeTypeManagerImpl(SessionContext context) { + this.context = context; + + // setup caches with soft references to node type + // & item definition instances + ntCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT); + pdCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT); + ndCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT); + + NodeTypeRegistry registry = context.getNodeTypeRegistry(); + + rootNodeDef = new NodeDefinitionImpl( + registry.getRootNodeDef(), this, context); + ndCache.put(rootNodeDef.unwrap(), rootNodeDef); + + registry.addListener(this); + } + + /** + * Disposes this node type manager. + */ + public void dispose() { + context.getNodeTypeRegistry().removeListener(this); + } + + /** + * @return the root node definition + */ + public NodeDefinitionImpl getRootNodeDefinition() { + return rootNodeDef; + } + + /** + * @param def the QNodeDefinition + * @return the node definition + */ + @Override + public NodeDefinitionImpl getNodeDefinition(QNodeDefinition def) { + synchronized (ndCache) { + NodeDefinitionImpl ndi = ndCache.get(def); + if (ndi == null) { + ndi = new NodeDefinitionImpl(def, this, context); + ndCache.put(def, ndi); + } + return ndi; + } + } + + /** + * @param def prop def + * @return the property definition + */ + @Override + public PropertyDefinitionImpl getPropertyDefinition(QPropertyDefinition def) { + synchronized (pdCache) { + PropertyDefinitionImpl pdi = pdCache.get(def); + if (pdi == null) { + pdi = new PropertyDefinitionImpl( + def, this, context, context.getValueFactory()); + pdCache.put(def, pdi); + } + return pdi; + } + } + + /** + * @param name node type name + * @return node type + * @throws NoSuchNodeTypeException if the nodetype does not exit + */ + @Override + public NodeTypeImpl getNodeType(Name name) throws NoSuchNodeTypeException { + synchronized (ntCache) { + NodeTypeImpl nt = ntCache.get(name); + if (nt == null) { + NodeTypeRegistry registry = context.getNodeTypeRegistry(); + EffectiveNodeType ent = registry.getEffectiveNodeType(name); + QNodeTypeDefinition def = registry.getNodeTypeDef(name); + nt = new NodeTypeImpl( + ent, def, this, context, + context.getValueFactory(), context.getDataStore()); + ntCache.put(name, nt); + } + return nt; + } + } + + /** + * @see org.apache.jackrabbit.spi.commons.nodetype.AbstractNodeTypeManager#getNamePathResolver() + */ + @Override + public NamePathResolver getNamePathResolver() { + return context; + } + + /** + * @return the node type registry + */ + public NodeTypeRegistry getNodeTypeRegistry() { + return context.getNodeTypeRegistry(); + } + + /** + * Registers the node types defined in the given input stream depending + * on the content type specified for the stream. This will also register + * any namespaces identified in the input stream if they have not already + * been registered. + * + * @param in node type XML stream + * @param contentType type of the input stream + * @param reregisterExisting flag indicating whether node types should be + * reregistered if they already exist + * @return registered node types + * @throws IOException if the input stream could not be read or parsed + * @throws RepositoryException if the node types are invalid or another + * repository error occurs + */ + public NodeType[] registerNodeTypes(InputStream in, String contentType, + boolean reregisterExisting) + throws IOException, RepositoryException { + + // make sure the editing session is allowed to register node types. + context.getAccessManager().checkRepositoryPermission(Permission.NODE_TYPE_DEF_MNGMT); + + try { + Map namespaceMap = new HashMap(); + List nodeTypeDefs = new ArrayList(); + + if (contentType.equalsIgnoreCase(TEXT_XML) + || contentType.equalsIgnoreCase(APPLICATION_XML)) { + try { + NodeTypeReader ntr = new NodeTypeReader(in); + + Properties namespaces = ntr.getNamespaces(); + if (namespaces != null) { + Enumeration prefixes = namespaces.propertyNames(); + while (prefixes.hasMoreElements()) { + String prefix = (String) prefixes.nextElement(); + String uri = namespaces.getProperty(prefix); + namespaceMap.put(prefix, uri); + } + } + + QNodeTypeDefinition[] defs = ntr.getNodeTypeDefs(); + nodeTypeDefs.addAll(Arrays.asList(defs)); + } catch (NameException e) { + throw new RepositoryException("Illegal JCR name", e); + } + } else if (contentType.equalsIgnoreCase(TEXT_X_JCR_CND)) { + try { + NamespaceMapping mapping = new NamespaceMapping(context.getSessionImpl()); + + CompactNodeTypeDefReader reader = + new CompactNodeTypeDefReader( + new InputStreamReader(in), "cnd input stream", mapping, + new QDefinitionBuilderFactory()); + + namespaceMap.putAll(mapping.getPrefixToURIMapping()); + for (QNodeTypeDefinition ntDef: reader.getNodeTypeDefinitions()) { + nodeTypeDefs.add(ntDef); + } + } catch (ParseException e) { + IOException e2 = new IOException(e.getMessage()); + e2.initCause(e); + throw e2; + } + } else { + throw new UnsupportedRepositoryOperationException( + "Unsupported content type: " + contentType); + } + + new NamespaceHelper(context.getSessionImpl()).registerNamespaces(namespaceMap); + + if (reregisterExisting) { + NodeTypeRegistry registry = context.getNodeTypeRegistry(); + // split the node types into new and already registered node types. + // this way we can register new node types together with already + // registered node types which make circular dependencies possible + List newNodeTypeDefs = new ArrayList(); + List registeredNodeTypeDefs = new ArrayList(); + for (QNodeTypeDefinition nodeTypeDef: nodeTypeDefs) { + if (registry.isRegistered(nodeTypeDef.getName())) { + registeredNodeTypeDefs.add(nodeTypeDef); + } else { + newNodeTypeDefs.add(nodeTypeDef); + } + } + + ArrayList nodeTypes = new ArrayList(); + + // register new node types + nodeTypes.addAll(registerNodeTypes(newNodeTypeDefs)); + + // re-register already existing node types + for (QNodeTypeDefinition nodeTypeDef: registeredNodeTypeDefs) { + registry.reregisterNodeType(nodeTypeDef); + nodeTypes.add(getNodeType(nodeTypeDef.getName())); + } + return nodeTypes.toArray(new NodeType[nodeTypes.size()]); + } else { + Collection types = registerNodeTypes(nodeTypeDefs); + return types.toArray(new NodeType[types.size()]); + } + + } catch (InvalidNodeTypeDefException e) { + throw new RepositoryException("Invalid node type definition", e); + } + } + + //---------------------------------------------< NodeTypeRegistryListener > + /** + * {@inheritDoc} + */ + public void nodeTypeRegistered(Name ntName) { + // not interested, ignore + } + + /** + * {@inheritDoc} + */ + public void nodeTypeReRegistered(Name ntName) { + // flush all affected cache entries + ntCache.remove(ntName); + synchronized (pdCache) { + Iterator iter = pdCache.values().iterator(); + while (iter.hasNext()) { + PropertyDefinitionImpl pd = iter.next(); + if (ntName.equals(pd.unwrap().getDeclaringNodeType())) { + iter.remove(); + } + } + } + synchronized (ndCache) { + Iterator iter = ndCache.values().iterator(); + while (iter.hasNext()) { + NodeDefinitionImpl nd = iter.next(); + if (ntName.equals(nd.unwrap().getDeclaringNodeType())) { + iter.remove(); + } + } + } + } + + /** + * {@inheritDoc} + */ + public void nodeTypesUnregistered(Collection names) { + // flush all affected cache entries + for (Name name : names) { + ntCache.remove(name); + } + synchronized (pdCache) { + Iterator iter = pdCache.values().iterator(); + while (iter.hasNext()) { + PropertyDefinitionImpl pd = iter.next(); + if (names.contains(pd.unwrap().getDeclaringNodeType())) { + iter.remove(); + } + } + } + synchronized (ndCache) { + Iterator iter = ndCache.values().iterator(); + while (iter.hasNext()) { + NodeDefinitionImpl nd = iter.next(); + if (names.contains(nd.unwrap().getDeclaringNodeType())) { + iter.remove(); + } + } + } + } + + //------------------------------------------------------< NodeTypeManager > + /** + * {@inheritDoc} + */ + public NodeTypeIterator getAllNodeTypes() throws RepositoryException { + Name[] ntNames = context.getNodeTypeRegistry().getRegisteredNodeTypes(); + Arrays.sort(ntNames); + ArrayList list = new ArrayList(ntNames.length); + for (Name ntName : ntNames) { + list.add(getNodeType(ntName)); + } + return new NodeTypeIteratorAdapter(list); + } + + /** + * {@inheritDoc} + */ + public NodeTypeIterator getPrimaryNodeTypes() throws RepositoryException { + Name[] ntNames = context.getNodeTypeRegistry().getRegisteredNodeTypes(); + Arrays.sort(ntNames); + ArrayList list = new ArrayList(ntNames.length); + for (Name ntName : ntNames) { + NodeType nt = getNodeType(ntName); + if (!nt.isMixin()) { + list.add(nt); + } + } + return new NodeTypeIteratorAdapter(list); + } + + /** + * {@inheritDoc} + */ + public NodeTypeIterator getMixinNodeTypes() throws RepositoryException { + Name[] ntNames = context.getNodeTypeRegistry().getRegisteredNodeTypes(); + Arrays.sort(ntNames); + ArrayList list = new ArrayList(ntNames.length); + for (Name ntName : ntNames) { + NodeType nt = getNodeType(ntName); + if (nt.isMixin()) { + list.add(nt); + } + } + return new NodeTypeIteratorAdapter(list); + } + + /** + * {@inheritDoc} + */ + public NodeType getNodeType(String nodeTypeName) + throws NoSuchNodeTypeException { + try { + return getNodeType(context.getQName(nodeTypeName)); + } catch (NameException e) { + throw new NoSuchNodeTypeException(nodeTypeName, e); + } catch (NamespaceException e) { + throw new NoSuchNodeTypeException(nodeTypeName, e); + } + } + + //--------------------------------------------< JackrabbitNodeTypeManager > + + /** + * Internal helper method for registering a list of node type definitions. + * Returns a collection containing the registered node types. + * + * @param defs a collection of QNodeTypeDefinition objects + * @return registered node types + * @throws InvalidNodeTypeDefException if a nodetype is invalid + * @throws RepositoryException if an error occurs + */ + private Collection registerNodeTypes(List defs) + throws InvalidNodeTypeDefException, RepositoryException { + context.getNodeTypeRegistry().registerNodeTypes(defs); + + Set types = new HashSet(); + for (QNodeTypeDefinition def : defs) { + try { + types.add(getNodeType(def.getName())); + } catch (NoSuchNodeTypeException e) { + // ignore + } + } + return types; + } + + /** + * Registers the node types defined in the given XML stream. This + * is a trivial implementation that just invokes the existing + * {@link NodeTypeReader} and {@link NodeTypeRegistry} methods and + * heuristically creates the returned node type array. It will also + * register any namespaces defined in the input source that have not + * already been registered. + * + * {@inheritDoc} + */ + public NodeType[] registerNodeTypes(InputSource in) + throws SAXException, RepositoryException { + try { + return registerNodeTypes(in.getByteStream(), TEXT_XML); + } catch (IOException e) { + throw new SAXException("Error reading node type stream", e); + } + } + + private static final String APPLICATION_XML = "application/xml"; + + /** + * Registers the node types defined in the given input stream depending + * on the content type specified for the stream. This will also register + * any namespaces identified in the input stream if they have not already + * been registered. + * + * {@inheritDoc} + */ + public NodeType[] registerNodeTypes(InputStream in, String contentType) + throws IOException, RepositoryException { + return registerNodeTypes(in, contentType, false); + } + + /** + * Checks whether a node type with the given name exists. + * + * @param name node type name + * @return true if the named node type exists, + * false otherwise + * @throws RepositoryException if the name format is invalid + */ + public boolean hasNodeType(String name) throws RepositoryException { + try { + Name qname = context.getQName(name); + return getNodeTypeRegistry().isRegistered(qname); + } catch (NamespaceException e) { + return false; + } catch (NameException e) { + throw new RepositoryException("Invalid name: " + name, e); + } + } + + //--------------------------------------------------< new JSR 283 methods > + /** + * Registers or updates the specified Collection of + * NodeTypeDefinition objects. This method is used to register + * or update a set of node types with mutual dependencies. Returns an + * iterator over the resulting NodeType objects. + *

    + * The effect of the method is "all or nothing"; if an error occurs, no node + * types are registered or updated. + *

    + * Throws an InvalidNodeTypeDefinitionException if a + * NodeTypeDefinition within the Collection is + * invalid or if the Collection contains an object of a type + * other than NodeTypeDefinition. + *

    + * Throws a NodeTypeExistsException if allowUpdate + * is false and a NodeTypeDefinition within the + * Collection specifies a node type name that is already + * registered. + *

    + * Throws an UnsupportedRepositoryOperationException if this + * implementation does not support node type registration. + * + * @param definitions a collection of NodeTypeDefinitions + * @param allowUpdate a boolean + * @return the registered node types. + * @throws InvalidNodeTypeDefinitionException if a + * NodeTypeDefinition within the Collection is + * invalid or if the Collection contains an object of a type + * other than NodeTypeDefinition. + * @throws NodeTypeExistsException if allowUpdate is + * false and a NodeTypeDefinition within the + * Collection specifies a node type name that is already + * registered. + * @throws UnsupportedRepositoryOperationException if this implementation + * does not support node type registration. + * @throws RepositoryException if another error occurs. + * @since JCR 2.0 + */ + public NodeTypeIterator registerNodeTypes( + NodeTypeDefinition[] definitions, boolean allowUpdate) + throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, + UnsupportedRepositoryOperationException, RepositoryException { + + // make sure the editing session is allowed to register node types. + context.getAccessManager().checkRepositoryPermission(Permission.NODE_TYPE_DEF_MNGMT); + + NodeTypeRegistry registry = context.getNodeTypeRegistry(); + + // split the node types into new and already registered node types. + // this way we can register new node types together with already + // registered node types which make circular dependencies possible + List addedDefs = new ArrayList(); + List modifiedDefs = new ArrayList(); + for (NodeTypeDefinition definition : definitions) { + // convert to QNodeTypeDefinition + QNodeTypeDefinition def = toNodeTypeDef(definition); + if (registry.isRegistered(def.getName())) { + if (allowUpdate) { + modifiedDefs.add(def); + } else { + throw new NodeTypeExistsException(definition.getName()); + } + } else { + addedDefs.add(def); + } + } + + try { + ArrayList result = new ArrayList(); + + // register new node types + result.addAll(registerNodeTypes(addedDefs)); + + // re-register already existing node types + for (QNodeTypeDefinition nodeTypeDef: modifiedDefs) { + registry.reregisterNodeType(nodeTypeDef); + result.add(getNodeType(nodeTypeDef.getName())); + } + + return new NodeTypeIteratorAdapter(result); + } catch (InvalidNodeTypeDefException e) { + throw new InvalidNodeTypeDefinitionException(e.getMessage(), e); + } + } + + /** + * Unregisters the specified set of node types. Used to unregister a set of node types with mutual dependencies. + *

    + * Throws a NoSuchNodeTypeException if one of the names listed is not a registered node type. + *

    + * Throws an UnsupportedRepositoryOperationException + * if this implementation does not support node type registration. + * + * @param names a String array + * @throws UnsupportedRepositoryOperationException if this implementation does not support node type registration. + * @throws NoSuchNodeTypeException if one of the names listed is not a registered node type. + * @throws RepositoryException if another error occurs. + * @since JCR 2.0 + */ + public void unregisterNodeTypes(String[] names) + throws UnsupportedRepositoryOperationException, + NoSuchNodeTypeException, RepositoryException { + + // make sure the editing session is allowed to un-register node types. + context.getAccessManager().checkRepositoryPermission(Permission.NODE_TYPE_DEF_MNGMT); + + Set ntNames = new HashSet(); + for (String name : names) { + try { + ntNames.add(context.getQName(name)); + } catch (NamespaceException e) { + throw new RepositoryException("Invalid name: " + name, e); + } catch (NameException e) { + throw new RepositoryException("Invalid name: " + name, e); + } + } + getNodeTypeRegistry().unregisterNodeTypes(ntNames); + } + + /** + * Internal helper method for converting a NodeTypeDefinition + * (using prefixed JCR names) to a NodeTypeDef (using + * namespace-qualified names). + * + * @param definition the definition + * @return a NodeTypeDef + * @throws RepositoryException if a repository error occurs + */ + private QNodeTypeDefinition toNodeTypeDef(NodeTypeDefinition definition) + throws RepositoryException { + return new QNodeTypeDefinitionImpl(definition, context, QValueFactoryImpl.getInstance()); + } + + //--------------------------------------------------------------< Object > + + /** + * {@inheritDoc} + */ + public String toString() { + return "NodeTypeManager(" + super.toString() + ")\n" + + context.getNodeTypeRegistry(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeRegistry.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeRegistry.java new file mode 100644 index 00000000000..cb914227f9c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeRegistry.java @@ -0,0 +1,1879 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.TreeSet; +import java.util.List; +import java.util.ArrayList; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.cluster.NodeTypeEventChannel; +import org.apache.jackrabbit.core.cluster.NodeTypeEventListener; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeDefDiff; +import org.apache.jackrabbit.spi.commons.nodetype.QNodeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import EDU.oswego.cs.dl.util.concurrent.ConcurrentReaderHashMap; + +/** + * A NodeTypeRegistry ... + */ +public class NodeTypeRegistry implements NodeTypeEventListener { + + private static Logger log = LoggerFactory.getLogger(NodeTypeRegistry.class); + + private static final String BUILTIN_NODETYPES_RESOURCE_PATH = + "org/apache/jackrabbit/core/nodetype/builtin_nodetypes.cnd"; + + private static final String CUSTOM_NODETYPES_RESOURCE_NAME = + "/nodetypes/custom_nodetypes.xml"; + + /** + * Feature flag for the unfortunate behavior in Jackrabbit 2.1 and 2.2 + * where the exception from {@link #checkForReferencesInContent(Name)} + * was never thrown because of a mistaken commit for + * JCR-2587. + * Setting this flag to true (the default value comes from + * the "disableCheckForReferencesInContentException" system property) + * will disable the exception thrown by default by the method. + * + * @see JCR-3223 + */ + public static volatile boolean disableCheckForReferencesInContentException = + Boolean.getBoolean("disableCheckForReferencesInContentException"); + + /** + * resource holding custom node type definitions which are represented as + * nodes in the repository; it is needed in order to make the registrations + * persistent. + */ + private final FileSystemResource customNodeTypesResource; + + // persistent node type definitions of built-in & custom node types + private final NodeTypeDefStore builtInNTDefs; + private final NodeTypeDefStore customNTDefs; + + // cache of pre-built aggregations of node types + private EffectiveNodeTypeCache entCache; + + // map of node type names and node type definitions + private final Map registeredNTDefs; + + // definition of the root node + private final QNodeDefinition rootNodeDef; + + /** + * namespace registry for resolving prefixes and namespace URI's; + * used for (de)serializing node type definitions + */ + private final NamespaceRegistry nsReg; + + /** + * Listeners (weak references) + */ + @SuppressWarnings("unchecked") + private final Map listeners = + Collections.synchronizedMap(new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK)); + + /** + * Node type event channel. + */ + private NodeTypeEventChannel eventChannel; + + //----------------------------------------< public NodeTypeRegistry 'api' > + /** + * Returns the names of all registered node types. That includes primary + * and mixin node types. + * + * @return the names of all registered node types. + */ + public Name[] getRegisteredNodeTypes() { + return registeredNTDefs.keySet().toArray(new Name[registeredNTDefs.size()]); + } + + /** + * Validates the NodeTypeDef and returns + * an EffectiveNodeType object representing the newly + * registered node type. + *

    + * The validation includes the following checks: + *

      + *
    • Supertypes must exist and be registered
    • + *
    • Inheritance graph must not be circular
    • + *
    • Aggregation of supertypes must not result in name conflicts, + * ambiguities, etc.
    • + *
    • Definitions of auto-created properties must specify a name
    • + *
    • Default values in property definitions must satisfy value constraints + * specified in the same property definition
    • + *
    • Definitions of auto-created child-nodes must specify a name
    • + *
    • Default node type in child-node definitions must exist and be + * registered
    • + *
    • The aggregation of the default node types in child-node definitions + * must not result in name conflicts, ambiguities, etc.
    • + *
    • Definitions of auto-created child-nodes must not specify default + * node types which would lead to infinite child node creation + * (e.g. node type 'A' defines auto-created child node with default + * node type 'A' ...)
    • + *
    • Node types specified as constraints in child-node definitions + * must exist and be registered
    • + *
    • The aggregation of the node types specified as constraints in + * child-node definitions must not result in name conflicts, ambiguities, + * etc.
    • + *
    • Default node types in child-node definitions must satisfy + * node type constraints specified in the same child-node definition
    • + *
    + * + * @param ntd the definition of the new node type + * @return an EffectiveNodeType instance + * @throws InvalidNodeTypeDefException if the given node type definition is invalid. + * @throws RepositoryException if a repository error occurs. + */ + public EffectiveNodeType registerNodeType(QNodeTypeDefinition ntd) + throws InvalidNodeTypeDefException, RepositoryException { + + EffectiveNodeType ent; + + synchronized (this) { + + // validate and register new node type definition + ent = internalRegister(ntd); + + // persist new node type definition + customNTDefs.add(ntd); + persistCustomNodeTypeDefs(customNTDefs); + + // notify listeners + notifyRegistered(ntd.getName()); + + } + + if (eventChannel != null) { + Set ntDefs = new HashSet(); + ntDefs.add(ntd); + eventChannel.registered(ntDefs); + } + + return ent; + } + + /** + * Same as {@link #registerNodeType(QNodeTypeDefinition)} except + * that a collection of NodeTypeDefs is registered instead of + * just one. + *

    + * This method can be used to register a set of node types that have + * dependencies on each other. + * + * @param ntDefs a collection of QNodeTypeDefinition objects + * @throws InvalidNodeTypeDefException if the given node type definition is invalid. + * @throws RepositoryException if a repository error occurs. + */ + public void registerNodeTypes(Collection ntDefs) + throws InvalidNodeTypeDefException, RepositoryException { + + registerNodeTypes(ntDefs, false); + } + + /** + * Internal implementation of {@link #registerNodeTypes(Collection)} + * + * @param ntDefs a collection of QNodeTypeDefinition objects + * @param external whether this invocation should be considered external + * @throws InvalidNodeTypeDefException if the given node type definition is invalid. + * @throws RepositoryException if a repository error occurs. + */ + private void registerNodeTypes(Collection ntDefs, + boolean external) + throws InvalidNodeTypeDefException, RepositoryException { + + synchronized (this) { + + // validate and register new node type definitions + internalRegister(ntDefs, external); + // persist new node type definitions + for (QNodeTypeDefinition ntDef: ntDefs) { + customNTDefs.add(ntDef); + } + persistCustomNodeTypeDefs(customNTDefs); + + // notify listeners + for (QNodeTypeDefinition ntDef : ntDefs) { + notifyRegistered(ntDef.getName()); + } + + } + + // inform cluster if this is not an external invocation + if (!external && eventChannel != null) { + eventChannel.registered(ntDefs); + } + + } + + /** + * Same as {@link #unregisterNodeType(Name)} except + * that a set of node types is unregistered instead of just one. + *

    + * This method can be used to unregister a set of node types that depend on + * each other. + * + * @param ntNames a collection of Name objects denoting the + * node types to be unregistered + * @throws NoSuchNodeTypeException if any of the specified names does not + * denote a registered node type. + * @throws RepositoryException if another error occurs + * @see #unregisterNodeType(Name) + */ + public void unregisterNodeTypes(Set ntNames) + throws NoSuchNodeTypeException, RepositoryException { + unregisterNodeTypes(ntNames, false); + } + + /** + * Internal implementation of {@link #unregisterNodeTypes(Set)} + * + * @param ntNames a collection of Name objects denoting the + * node types to be unregistered + * @param external whether this invocation should be considered external + * @throws NoSuchNodeTypeException if any of the specified names does not + * denote a registered node type. + * @throws RepositoryException if another error occurs + */ + private void unregisterNodeTypes( + Collection ntNames, boolean external) + throws NoSuchNodeTypeException, RepositoryException { + + synchronized (this) { + + // do some preliminary checks + for (Name ntName: ntNames) { + if (!registeredNTDefs.containsKey(ntName)) { + throw new NoSuchNodeTypeException(ntName.toString()); + } + if (builtInNTDefs.contains(ntName)) { + throw new RepositoryException(ntName.toString() + + ": can't unregister built-in node type."); + } + // check for node types other than those to be unregistered + // that depend on the given node types + Set dependents = getDependentNodeTypes(ntName); + dependents.removeAll(ntNames); + if (dependents.size() > 0) { + StringBuilder msg = new StringBuilder(); + msg.append(ntName).append(" can not be removed because the following node types depend on it: "); + for (Name dependent : dependents) { + msg.append(dependent); + msg.append(" "); + } + throw new RepositoryException(msg.toString()); + } + } + + // make sure node types are not currently in use + for (Name ntName : ntNames) { + checkForReferencesInContent(ntName); + } + + // all preconditions are met, node types can now safely be unregistered + internalUnregister(ntNames); + + // persist removal of node type definitions & notify listeners + for (Name ntName : ntNames) { + customNTDefs.remove(ntName); + } + notifyUnregistered(ntNames); + + persistCustomNodeTypeDefs(customNTDefs); + + } + + // inform cluster if this is not an external invocation + if (!external && eventChannel != null) { + eventChannel.unregistered(ntNames); + } + + } + + /** + * Unregisters the specified node type. In order for a node type to be + * successfully unregistered it must meet the following conditions: + *

      + *
    1. the node type must obviously be registered.
    2. + *
    3. a built-in node type can not be unregistered.
    4. + *
    5. the node type must not have dependents, i.e. other node types that + * are referencing it.
    6. + *
    7. the node type must not be currently used by any workspace.
    8. + *
    + * + * @param ntName name of the node type to be unregistered + * @throws NoSuchNodeTypeException if ntName does not + * denote a registered node type. + * @throws RepositoryException if another error occurs. + * @see #unregisterNodeTypes(Collection, boolean) + */ + public void unregisterNodeType(Name ntName) + throws NoSuchNodeTypeException, RepositoryException { + HashSet ntNames = new HashSet(); + ntNames.add(ntName); + unregisterNodeTypes(ntNames); + } + + /** + * Reregister a node type. + * @param ntd node type definition + * @return the new effective node type + * @throws NoSuchNodeTypeException if ntd refers to an + * unknown node type + * @throws InvalidNodeTypeDefException if the node type definition + * is invalid + * @throws RepositoryException if another error occurs + */ + public EffectiveNodeType reregisterNodeType(QNodeTypeDefinition ntd) + throws NoSuchNodeTypeException, InvalidNodeTypeDefException, + RepositoryException { + + return reregisterNodeType(ntd, false); + } + + /** + * Internal implementation of {@link #reregisterNodeType(QNodeTypeDefinition)}. + * + * @param ntd node type definition + * @param external whether this invocation should be considered external + * @return the new effective node type + * @throws NoSuchNodeTypeException if ntd refers to an + * unknown node type + * @throws InvalidNodeTypeDefException if the node type definition + * is invalid + * @throws RepositoryException if another error occurs + */ + private EffectiveNodeType reregisterNodeType(QNodeTypeDefinition ntd, + boolean external) + throws NoSuchNodeTypeException, InvalidNodeTypeDefException, + RepositoryException { + + EffectiveNodeType entNew; + + synchronized (this) { + + Name name = ntd.getName(); + if (!registeredNTDefs.containsKey(name)) { + throw new NoSuchNodeTypeException(name.toString()); + } + if (builtInNTDefs.contains(name)) { + throw new RepositoryException(name.toString() + + ": can't reregister built-in node type."); + } + + /** + * validate new node type definition + */ + ntd = checkNtBaseSubtyping(ntd, registeredNTDefs); + validateNodeTypeDef(ntd, entCache, registeredNTDefs, nsReg, false); + + /** + * build diff of current and new definition and determine type of change + */ + QNodeTypeDefinition ntdOld = registeredNTDefs.get(name); + NodeTypeDefDiff diff = NodeTypeDefDiff.create(ntdOld, ntd); + if (!diff.isModified()) { + // the definition has not been modified, there's nothing to do here... + return getEffectiveNodeType(name); + } + + // make sure existing content would not conflict + // with new node type definition + checkForConflictingContent(ntd, diff); + + /** + * re-register node type definition and update caches & + * notify listeners on re-registration + */ + internalUnregister(name); + // remove old node type definition from store + customNTDefs.remove(name); + + entNew = internalRegister(ntd); + + // add new node type definition to store + customNTDefs.add(ntd); + // persist node type definitions + persistCustomNodeTypeDefs(customNTDefs); + + // notify listeners + notifyReRegistered(name); + + } + + // inform cluster if this is not an external invocation + if (!external && eventChannel != null) { + eventChannel.reregistered(ntd); + } + + return entNew; + + } + + /** + * @param ntName name + * @return effective node type + * @throws NoSuchNodeTypeException if node type does not exist + */ + public EffectiveNodeType getEffectiveNodeType(Name ntName) + throws NoSuchNodeTypeException { + return getEffectiveNodeType(ntName, entCache, registeredNTDefs); + } + + /** + * Returns the effective node type of a node with the given primary + * and mixin types. + * + * @param primary primary type of the node + * @param mixins mixin types of the node (set of {@link Name names}); + * @return effective node type + * @throws NodeTypeConflictException if the given types are conflicting + * @throws NoSuchNodeTypeException if one of the given types is not found + */ + public EffectiveNodeType getEffectiveNodeType(Name primary, Set mixins) + throws NodeTypeConflictException, NoSuchNodeTypeException { + if (mixins.isEmpty()) { + return getEffectiveNodeType(primary); + } else { + Name[] names = new Name[mixins.size() + 1]; + mixins.toArray(names); + names[names.length - 1] = primary; + return getEffectiveNodeType(names, entCache, registeredNTDefs); + } + } + + /** + * Returns the effective node type representation of the given node types. + * + * @param mixins mixin types of the node (set of {@link Name names}); + * @return effective node type + * @throws NodeTypeConflictException if the given types are conflicting + * @throws NoSuchNodeTypeException if one of the given types is not found + */ + public EffectiveNodeType getEffectiveNodeType(Set mixins) + throws NodeTypeConflictException, NoSuchNodeTypeException { + Name[] names = new Name[mixins.size()]; + mixins.toArray(names); + return getEffectiveNodeType(names, entCache, registeredNTDefs); + } + + /** + * Returns the names of those registered node types that have + * dependencies on the given node type. + * + * @param nodeTypeName node type name + * @return a set of node type Names + * @throws NoSuchNodeTypeException if node type does not exist + */ + public Set getDependentNodeTypes(Name nodeTypeName) + throws NoSuchNodeTypeException { + if (!registeredNTDefs.containsKey(nodeTypeName)) { + throw new NoSuchNodeTypeException(nodeTypeName.toString()); + } + + /** + * collect names of those node types that have dependencies on the given + * node type + */ + HashSet names = new HashSet(); + for (QNodeTypeDefinition ntd : registeredNTDefs.values()) { + if (ntd.getDependencies().contains(nodeTypeName)) { + names.add(ntd.getName()); + } + } + return names; + } + + /** + * Returns the node type definition of the node type with the given name. + * + * @param nodeTypeName name of node type whose definition should be returned. + * @return the node type definition of the node type with the given name. + * @throws NoSuchNodeTypeException if a node type with the given name + * does not exist + */ + public QNodeTypeDefinition getNodeTypeDef(Name nodeTypeName) + throws NoSuchNodeTypeException { + QNodeTypeDefinition def = registeredNTDefs.get(nodeTypeName); + if (def == null) { + throw new NoSuchNodeTypeException(nodeTypeName.toString()); + } + return def; + } + + /** + * @param nodeTypeName node type name + * @return true if the specified node type is registered; + * false otherwise. + */ + public boolean isRegistered(Name nodeTypeName) { + return registeredNTDefs.containsKey(nodeTypeName); + } + + /** + * @param nodeTypeName node type name + * @return true if the specified node type is built-in; + * false otherwise. + */ + public boolean isBuiltIn(Name nodeTypeName) { + return builtInNTDefs.contains(nodeTypeName); + } + + /** + * Add a NodeTypeRegistryListener + * + * @param listener the new listener to be informed on (un)registration + * of node types + */ + public void addListener(NodeTypeRegistryListener listener) { + if (!listeners.containsKey(listener)) { + listeners.put(listener, listener); + } + } + + /** + * Remove a NodeTypeRegistryListener + * + * @param listener an existing listener + */ + public void removeListener(NodeTypeRegistryListener listener) { + listeners.remove(listener); + } + + //--------------------------------------------------------------< Object > + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("NodeTypeRegistry (" + super.toString() + ")\n"); + builder.append("Registered NodeTypes:\n"); + for (QNodeTypeDefinition ntd : registeredNTDefs.values()) { + builder.append(ntd.getName()); + builder.append("\n"); + builder.append( + "\tSupertypes: " + + Arrays.toString(ntd.getSupertypes()) + "\n"); + builder.append("\tMixin\t" + ntd.isMixin() + "\n"); + builder.append("\tOrderableChildNodes\t" + ntd.hasOrderableChildNodes() + "\n"); + builder.append("\tPrimaryItemName\t" + (ntd.getPrimaryItemName() == null ? "" : ntd.getPrimaryItemName().toString()) + "\n"); + QPropertyDefinition[] pd = ntd.getPropertyDefs(); + for (QPropertyDefinition aPd : pd) { + builder.append("\tPropertyDefinition\n"); + builder.append(" (declared in " + aPd.getDeclaringNodeType() + ")\n"); + builder.append("\t\tName\t\t" + (aPd.definesResidual() ? "*" : aPd.getName().toString()) + "\n"); + String type = aPd.getRequiredType() == 0 ? "null" : PropertyType.nameFromValue(aPd.getRequiredType()); + builder.append("\t\tRequiredType\t" + type + "\n"); + QValueConstraint[] vca = aPd.getValueConstraints(); + StringBuilder constraints = new StringBuilder(); + if (vca == null) { + constraints.append(""); + } else { + for (QValueConstraint aVca : vca) { + if (constraints.length() > 0) { + constraints.append(", "); + } + constraints.append(aVca.getString()); + } + } + builder.append("\t\tValueConstraints\t" + constraints + "\n"); + QValue[] defVals = aPd.getDefaultValues(); + StringBuilder defaultValues = new StringBuilder(); + if (defVals == null) { + defaultValues.append(""); + } else { + for (QValue defVal : defVals) { + if (defaultValues.length() > 0) { + defaultValues.append(", "); + } + defaultValues.append(defVal.toString()); + } + } + builder.append("\t\tDefaultValue\t" + defaultValues + "\n"); + builder.append("\t\tAutoCreated\t" + aPd.isAutoCreated() + "\n"); + builder.append("\t\tMandatory\t" + aPd.isMandatory() + "\n"); + builder.append("\t\tOnVersion\t" + OnParentVersionAction.nameFromValue(aPd.getOnParentVersion()) + "\n"); + builder.append("\t\tProtected\t" + aPd.isProtected() + "\n"); + builder.append("\t\tMultiple\t" + aPd.isMultiple() + "\n"); + } + QNodeDefinition[] nd = ntd.getChildNodeDefs(); + for (QNodeDefinition aNd : nd) { + builder.append("\tNodeDefinition\\n"); + builder.append(" (declared in " + aNd.getDeclaringNodeType() + ")\\n"); + builder.append("\t\tName\t\t" + (aNd.definesResidual() ? "*" : aNd.getName().toString()) + "\n"); + Name[] reqPrimaryTypes = aNd.getRequiredPrimaryTypes(); + if (reqPrimaryTypes != null && reqPrimaryTypes.length > 0) { + for (Name reqPrimaryType : reqPrimaryTypes) { + builder.append("\t\tRequiredPrimaryType\t" + reqPrimaryType + "\n"); + } + } + Name defPrimaryType = aNd.getDefaultPrimaryType(); + if (defPrimaryType != null) { + builder.append("\n\t\tDefaultPrimaryType\t" + defPrimaryType + "\n"); + } + builder.append("\n\t\tAutoCreated\t" + aNd.isAutoCreated() + "\n"); + builder.append("\t\tMandatory\t" + aNd.isMandatory() + "\n"); + builder.append("\t\tOnVersion\t" + OnParentVersionAction.nameFromValue(aNd.getOnParentVersion()) + "\n"); + builder.append("\t\tProtected\t" + aNd.isProtected() + "\n"); + builder.append("\t\tAllowsSameNameSiblings\t" + aNd.allowsSameNameSiblings() + "\n"); + } + } + builder.append(entCache); + return builder.toString(); + } + + //------------------------------------------------< NodeTypeEventListener > + + /** + * {@inheritDoc} + */ + public void externalRegistered(Collection ntDefs) + throws RepositoryException, InvalidNodeTypeDefException { + + registerNodeTypes(ntDefs, true); + } + + /** + * {@inheritDoc} + */ + public void externalReregistered(QNodeTypeDefinition ntDef) + throws NoSuchNodeTypeException, InvalidNodeTypeDefException, + RepositoryException { + + reregisterNodeType(ntDef, true); + } + + /** + * {@inheritDoc} + */ + public void externalUnregistered(Collection ntNames) + throws RepositoryException, NoSuchNodeTypeException { + unregisterNodeTypes(ntNames, true); + } + + //---------------------------------------------------------< overridables > + /** + * Constructor + * + * @param nsReg name space registry + * @param fs repository file system + * @throws RepositoryException if an error occurs + */ + @SuppressWarnings("unchecked") + public NodeTypeRegistry(NamespaceRegistry nsReg, FileSystem fs) + throws RepositoryException { + this.nsReg = nsReg; + customNodeTypesResource = + new FileSystemResource(fs, CUSTOM_NODETYPES_RESOURCE_NAME); + try { + // make sure path to resource exists + if (!customNodeTypesResource.exists()) { + customNodeTypesResource.makeParentDirs(); + } + } catch (FileSystemException fse) { + String error = "internal error: invalid resource: " + + customNodeTypesResource.getPath(); + log.debug(error); + throw new RepositoryException(error, fse); + } + + // use the improved node type cache + // (replace with: entCache = new EffectiveNodeTypeCacheImpl(); + // for the old one) + entCache = new BitSetENTCacheImpl(); + registeredNTDefs = new ConcurrentReaderHashMap(); + + // setup definition of root node + rootNodeDef = createRootNodeDef(); + + // load and register pre-defined (i.e. built-in) node types + builtInNTDefs = new NodeTypeDefStore(); + try { + // load built-in node type definitions + loadBuiltInNodeTypeDefs(builtInNTDefs); + + // register built-in node types + internalRegister(builtInNTDefs.all(), false, true); + } catch (InvalidNodeTypeDefException intde) { + String error = + "internal error: invalid built-in node type definition stored in " + + BUILTIN_NODETYPES_RESOURCE_PATH; + log.debug(error); + throw new RepositoryException(error, intde); + } + + // load and register custom node types + customNTDefs = new NodeTypeDefStore(); + + // load custom node type definitions + loadCustomNodeTypeDefs(customNTDefs); + + // validate & register custom node types + try { + internalRegister(customNTDefs.all(), false); + } catch (InvalidNodeTypeDefException intde) { + String error = + "internal error: invalid custom node type definition stored in " + + customNodeTypesResource.getPath(); + log.debug(error); + throw new RepositoryException(error, intde); + } + } + + /** + * Loads the built-in node type definitions into the given store. + *

    + * This method may be overridden by extensions of this class; It must + * only be called once and only from within the constructor though. + * + * @param store The {@link NodeTypeDefStore} into which the node type + * definitions are loaded. + * @throws RepositoryException If an error occurs while loading the + * built-in node type definitions. + */ + protected void loadBuiltInNodeTypeDefs(NodeTypeDefStore store) + throws RepositoryException { + InputStream in = null; + try { + in = getClass().getClassLoader().getResourceAsStream(BUILTIN_NODETYPES_RESOURCE_PATH); + if (in != null) { + Reader r = new InputStreamReader(in, StandardCharsets.UTF_8); + store.loadCND(r, BUILTIN_NODETYPES_RESOURCE_PATH); + } + } catch (IOException ioe) { + String error = + "internal error: failed to read built-in node type definitions stored in " + + BUILTIN_NODETYPES_RESOURCE_PATH; + log.debug(error); + throw new RepositoryException(error, ioe); + } catch (InvalidNodeTypeDefException intde) { + String error = + "internal error: invalid built-in node type definition stored in " + + BUILTIN_NODETYPES_RESOURCE_PATH; + log.debug(error); + throw new RepositoryException(error, intde); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Loads the custom node type definitions into the given store. + *

    + * This method may be overridden by extensions of this class; It must + * only be called once and only from within the constructor though. + * + * @param store The {@link NodeTypeDefStore} into which the node type + * definitions are loaded. + * @throws RepositoryException If an error occurs while loading the + * custom node type definitions. + */ + protected void loadCustomNodeTypeDefs(NodeTypeDefStore store) + throws RepositoryException { + + InputStream in = null; + try { + if (customNodeTypesResource.exists()) { + in = customNodeTypesResource.getInputStream(); + } + } catch (FileSystemException fse) { + String error = + "internal error: failed to access custom node type definitions stored in " + + customNodeTypesResource.getPath(); + log.debug(error); + throw new RepositoryException(error, fse); + } + + if (in == null) { + log.info("no custom node type definitions found"); + } else { + try { + store.load(in); + } catch (IOException ioe) { + String error = + "internal error: failed to read custom node type definitions stored in " + + customNodeTypesResource.getPath(); + log.debug(error); + throw new RepositoryException(error, ioe); + } catch (InvalidNodeTypeDefException intde) { + String error = + "internal error: invalid custom node type definition stored in " + + customNodeTypesResource.getPath(); + log.debug(error); + throw new RepositoryException(error, intde); + } finally { + try { + in.close(); + } catch (IOException ioe) { + // ignore + } + } + } + } + + /** + * Persists the custom node type definitions contained in the given + * store. + * + * @param store The {@link NodeTypeDefStore} containing the definitions to + * be persisted. + * @throws RepositoryException If an error occurs while persisting the + * custom node type definitions. + */ + protected void persistCustomNodeTypeDefs(NodeTypeDefStore store) + throws RepositoryException { + try { + OutputStream out = customNodeTypesResource.getOutputStream(); + try { + store.store(out, nsReg); + } finally { + out.close(); + } + } catch (IOException ioe) { + String error = + "internal error: failed to persist custom node type definitions to " + + customNodeTypesResource.getPath(); + log.debug(error); + throw new RepositoryException(error, ioe); + } catch (FileSystemException fse) { + String error = + "internal error: failed to persist custom node type definitions to " + + customNodeTypesResource.getPath(); + log.debug(error); + throw new RepositoryException(error, fse); + } + } + + /** + * Checks whether there is existing content that would conflict with the + * given node type definition. + *

    + * This method is not implemented yet and always throws a + * RepositoryException. + *

    + * TODO + *

      + *
    1. apply deep locks on root nodes in every workspace or alternatively + * put repository in 'exclusive' or 'single-user' mode + *
    2. check if the given node type (or any node type that has + * dependencies on this node type) is currently referenced by nodes + * in the repository. + *
    3. check if applying the changed definitions to the affected items would + * violate existing node type constraints + *
    4. apply and persist changes to affected nodes (e.g. update + * definition id's, etc.) + *
    + *

    + * the above checks/actions are absolutely necessary in order to + * guarantee integrity of repository content. + * + * + * @param ntd The node type definition replacing the former node type + * definition of the same name. + * @param diff + * @throws RepositoryException If there is conflicting content or if the + * check failed for some other reason. + */ + protected void checkForConflictingContent(QNodeTypeDefinition ntd, final NodeTypeDefDiff diff) + throws RepositoryException { + + if (!diff.isTrivial()) { + /** + * collect names of node types that have dependencies on the given + * node type + */ + //Set dependentNTs = getDependentNodeTypes(ntd.getName()); + + String message = + "The following node type change contains non-trivial changes." + + "Up until now only trivial changes are supported." + + " (see javadoc for " + + NodeTypeDefDiff.class.getName() + + "):\n" + diff.toString(); + throw new RepositoryException(message); + } + + /** + * the change is trivial and has no effect on current content + * (e.g. that would be the case when non-mandatory properties had + * been added); + */ + } + + /** + * Checks whether there is existing content that directly or indirectly + * refers to the specified node type. + *

    + * This method is not implemented yet and always throws a + * RepositoryException. + *

    + * TODO: + *

      + *
    1. apply deep locks on root nodes in every workspace or alternatively + * put repository in 'single-user' mode + *
    2. check if the given node type is currently referenced by nodes + * in the repository. + *
    3. remove the node type if it is not currently referenced, otherwise + * throw exception + *
    + *

    + * the above checks are absolutely necessary in order to guarantee + * integrity of repository content. + * + * @param nodeTypeName The name of the node type to be checked. + * @throws RepositoryException If the specified node type is currently + * being referenced or if the check failed for + * some other reason. + */ + protected void checkForReferencesInContent(Name nodeTypeName) + throws RepositoryException { + if (!disableCheckForReferencesInContentException) { + throw new RepositoryException( + "The check for the existence of content using the" + + " given node type is not yet implemented, so to" + + " guarantee repository consistency the request to" + + " unregister the type is denied. Contributions to" + + " implement this feature would be welcome! To restore" + + " the broken behavior of previous Jackrabbit versions" + + " where this check was simply skipped, please set the" + + " disableCheckForReferencesInContentException system" + + " property to true."); + } + } + + //-------------------------------------------------------< implementation > + /** + * @return the definition of the root node + */ + public QNodeDefinition getRootNodeDef() { + return rootNodeDef; + } + + /** + * Set an event channel to inform about changes. + * + * @param eventChannel event channel + */ + public void setEventChannel(NodeTypeEventChannel eventChannel) { + this.eventChannel = eventChannel; + eventChannel.setListener(this); + } + + /** + * @param ntName node type name + * @param entCache cache of already-built effective node types + * @param ntdCache cache of node type definitions + * @return the effective node type + * @throws NoSuchNodeTypeException if a node type reference (e.g. a supertype) + * could not be resolved. + */ + static EffectiveNodeType getEffectiveNodeType(Name ntName, + EffectiveNodeTypeCache entCache, + Map ntdCache) + throws NoSuchNodeTypeException { + // 1. check if effective node type has already been built + EffectiveNodeTypeCache.Key key = entCache.getKey(new Name[]{ntName}); + EffectiveNodeType ent = entCache.get(key); + if (ent != null) { + return ent; + } + + // 2. make sure we've got the definition of the specified node type + QNodeTypeDefinition ntd = ntdCache.get(ntName); + if (ntd == null) { + throw new NoSuchNodeTypeException(ntName.toString()); + } + + // 3. build effective node type + synchronized (entCache) { + try { + ent = EffectiveNodeType.create(ntd, entCache, ntdCache); + // store new effective node type + entCache.put(ent); + return ent; + } catch (NodeTypeConflictException ntce) { + // should never get here as all known node types should be valid! + String msg = "internal error: encountered invalid registered node type " + ntName; + log.debug(msg); + throw new NoSuchNodeTypeException(msg, ntce); + } + } + } + + /** + * Returns an effective node type representation of the given node types. + * + * @param ntNames array of node type names + * @param entCache cache of already-built effective node types + * @param ntdCache cache of node type definitions + * @return the desired effective node type + * @throws NodeTypeConflictException if the effective node type representation + * could not be built due to conflicting + * node type definitions. + * @throws NoSuchNodeTypeException if a node type reference (e.g. a supertype) + * could not be resolved. + */ + static EffectiveNodeType getEffectiveNodeType(Name[] ntNames, + EffectiveNodeTypeCache entCache, + Map ntdCache) + throws NodeTypeConflictException, NoSuchNodeTypeException { + + EffectiveNodeTypeCache.Key key = entCache.getKey(ntNames); + + // 1. check if aggregate has already been built + if (entCache.contains(key)) { + return entCache.get(key); + } + + // 2. make sure we've got the definitions of the specified node types + for (Name ntName : ntNames) { + if (!ntdCache.containsKey(ntName)) { + throw new NoSuchNodeTypeException(ntName.toString()); + } + } + + // 3. build aggregate + EffectiveNodeTypeCache.Key requested = key; + EffectiveNodeType result = null; + synchronized (entCache) { + // build list of 'best' existing sub-aggregates + while (key.getNames().length > 0) { + // find the (sub) key that matches the current key the best + EffectiveNodeTypeCache.Key subKey = entCache.findBest(key); + if (subKey != null) { + EffectiveNodeType ent = entCache.get(subKey); + if (result == null) { + result = ent; + } else { + result = result.merge(ent); + // store intermediate result + entCache.put(result); + } + // subtract the result from the temporary key + key = key.subtract(subKey); + } else { + /** + * no matching sub-aggregates found: + * build aggregate of remaining node types through iteration + */ + Name[] remainder = key.getNames(); + for (Name aRemainder : remainder) { + QNodeTypeDefinition ntd = ntdCache.get(aRemainder); + EffectiveNodeType ent = + EffectiveNodeType.create(ntd, entCache, ntdCache); + // store new effective node type + entCache.put(ent); + if (result == null) { + result = ent; + } else { + result = result.merge(ent); + // store intermediate result (sub-aggregate) + entCache.put(result); + } + } + break; + } + } + } + // also put the requested key, since the merge could have removed some + // the redundant nodetypes + if (!entCache.contains(requested)) { + entCache.put(requested, result); + } + // we're done + return result; + } + + static void checkForCircularInheritance(Name[] supertypes, + Stack inheritanceChain, + Map ntDefCache) + throws InvalidNodeTypeDefException, RepositoryException { + for (Name nt : supertypes) { + int pos = inheritanceChain.lastIndexOf(nt); + if (pos >= 0) { + StringBuilder buf = new StringBuilder(); + for (int j = 0; j < inheritanceChain.size(); j++) { + if (j == pos) { + buf.append("--> "); + } + buf.append(inheritanceChain.get(j)); + buf.append(" extends "); + } + buf.append("--> "); + buf.append(nt); + throw new InvalidNodeTypeDefException("circular inheritance detected: " + buf.toString()); + } + + try { + QNodeTypeDefinition ntd = ntDefCache.get(nt); + Name[] sta = ntd.getSupertypes(); + if (sta.length > 0) { + // check recursively + inheritanceChain.push(nt); + checkForCircularInheritance(sta, inheritanceChain, ntDefCache); + inheritanceChain.pop(); + } + } catch (NoSuchNodeTypeException nsnte) { + String msg = "unknown supertype: " + nt; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, nsnte); + } + } + } + + static void checkForCircularNodeAutoCreation(EffectiveNodeType childNodeENT, + Stack definingParentNTs, + EffectiveNodeTypeCache anEntCache, + Map ntDefCache) + throws InvalidNodeTypeDefException { + // check for circularity through default node types of auto-created child nodes + // (node type 'a' defines auto-created child node with default node type 'a') + Name[] childNodeNTs = childNodeENT.getAllNodeTypes(); + for (Name nt : childNodeNTs) { + int pos = definingParentNTs.lastIndexOf(nt); + if (pos >= 0) { + StringBuilder buf = new StringBuilder(); + for (int j = 0; j < definingParentNTs.size(); j++) { + if (j == pos) { + buf.append("--> "); + } + buf.append("node type "); + buf.append(definingParentNTs.get(j)); + buf.append(" defines auto-created child node with default "); + } + buf.append("--> "); + buf.append("node type "); + buf.append(nt); + throw new InvalidNodeTypeDefException("circular node auto-creation detected: " + + buf.toString()); + } + } + + QNodeDefinition[] nodeDefs = childNodeENT.getAutoCreateNodeDefs(); + for (QNodeDefinition nodeDef : nodeDefs) { + Name dnt = nodeDef.getDefaultPrimaryType(); + Name definingNT = nodeDef.getDeclaringNodeType(); + try { + if (dnt != null) { + // check recursively + definingParentNTs.push(definingNT); + checkForCircularNodeAutoCreation(getEffectiveNodeType(dnt, anEntCache, ntDefCache), + definingParentNTs, anEntCache, ntDefCache); + definingParentNTs.pop(); + } + } catch (NoSuchNodeTypeException nsnte) { + String msg = definingNT + + " defines invalid default node type for child node " + nodeDef.getName(); + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, nsnte); + } + } + } + + private EffectiveNodeType internalRegister(QNodeTypeDefinition ntd) + throws InvalidNodeTypeDefException, RepositoryException { + Name name = ntd.getName(); + if (name != null && registeredNTDefs.containsKey(name)) { + String msg = name + " already exists"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + + ntd = checkNtBaseSubtyping(ntd, registeredNTDefs); + EffectiveNodeType ent = + validateNodeTypeDef(ntd, entCache, registeredNTDefs, nsReg, false); + + // store new effective node type instance + entCache.put(ent); + + registeredNTDefs.put(name, ntd); + + return ent; + } + + /** + * Validates and registers the specified collection of NodeTypeDef + * objects. An InvalidNodeTypeDefException is thrown if the + * validation of any of the contained NodeTypeDef objects fails. + *

    + * Note that in the case an exception is thrown no node type will be + * eventually registered. + * + * @param ntDefs collection of NodeTypeDef objects + * @throws InvalidNodeTypeDefException if the node type is not valid + * @throws RepositoryException if an error occurs + * @see #registerNodeType + */ + private void internalRegister(Collection ntDefs, boolean external) + throws InvalidNodeTypeDefException, RepositoryException { + internalRegister(ntDefs, external, false); + } + + /** + * Same as {@link #internalRegister(java.util.Collection, boolean)} except for the + * additional lenient parameter which governs whether + * validation can be lenient (e.g. for built-in node types) or has to be + * strict (such as in the case of custom node types). This differentiation + * is unfortunately required as there are e.g. properties defined in built-in + * node types which are auto-created but don't have a fixed default value + * that can be exposed in a property definition because it is + * system-generated (such as jcr:primaryType in nt:base). + */ + private void internalRegister(Collection ntDefs, boolean external, boolean lenient) + throws InvalidNodeTypeDefException, RepositoryException { + + // need a list/collection that can be modified + List defs = new ArrayList(ntDefs); + + // map of node type names and node type definitions + Map tmpNTDefCache = new HashMap(registeredNTDefs); + + // temporarily register the node type definition + // and do some preliminary checks + for (QNodeTypeDefinition ntd : defs) { + Name name = ntd.getName(); + if (!external && name != null && tmpNTDefCache.containsKey(name)) { + String msg = name + " already exists locally"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + // add definition to temporary cache + tmpNTDefCache.put(ntd.getName(), ntd); + } + + // check if all node type defs have proper nt:base subtyping + for (int i = 0; i < defs.size(); i++) { + QNodeTypeDefinition ntd = defs.get(i); + QNodeTypeDefinition mod = checkNtBaseSubtyping(ntd, tmpNTDefCache); + if (mod != ntd) { + // check fixed subtyping + // -> update cache and list of defs + tmpNTDefCache.put(mod.getName(), mod); + defs.set(i, mod); + } + } + + // create working copies of current ent & ntd caches: + // cache of pre-built aggregations of node types + EffectiveNodeTypeCache tmpENTCache = (EffectiveNodeTypeCache) entCache.clone(); + for (QNodeTypeDefinition ntd : defs) { + EffectiveNodeType ent = validateNodeTypeDef(ntd, tmpENTCache, + tmpNTDefCache, nsReg, lenient); + + // store new effective node type instance + tmpENTCache.put(ent); + } + + // since no exception was thrown so far the definitions are assumed to + // be valid + for (QNodeTypeDefinition ntd : defs) { + registeredNTDefs.put(ntd.getName(), ntd); + } + + // finally add newly created effective node types to entCache + entCache = tmpENTCache; + } + + private void internalUnregister(Name name) throws NoSuchNodeTypeException { + QNodeTypeDefinition ntd = registeredNTDefs.get(name); + if (ntd == null) { + throw new NoSuchNodeTypeException(name.toString()); + } + registeredNTDefs.remove(name); + entCache.invalidate(name); + } + + private void internalUnregister(Collection ntNames) + throws NoSuchNodeTypeException { + for (Name name : ntNames) { + internalUnregister(name); + } + } + + /** + * Utility method for verifying that the namespace of a Name + * is registered; a null argument is silently ignored. + * + * @param name name whose namespace is to be checked + * @param nsReg namespace registry to be used for checking + * @throws RepositoryException if the namespace of the given name is not + * registered or if an unspecified error occured + */ + private static void checkNamespace(Name name, NamespaceRegistry nsReg) + throws RepositoryException { + if (name != null) { + // make sure namespace uri denotes a registered namespace + nsReg.getPrefix(name.getNamespaceURI()); + } + } + + /** + * Checks if the given node type def has the correct supertypes in respect + * to nt:base. all mixin nodetypes must not have a nt:base, the primary + * ones only if they don't inherit it from another supertype. + * + * @param ntd the node type def to check + * @param ntdCache cache for lookup + * @return the node type definition that was given to check or a new + * instance if it had to be fixed up. + */ + private static QNodeTypeDefinition checkNtBaseSubtyping(QNodeTypeDefinition ntd, Map ntdCache) { + if (NameConstants.NT_BASE.equals(ntd.getName())) { + return ntd; + } + Set supertypes = new TreeSet(Arrays.asList(ntd.getSupertypes())); + if (supertypes.isEmpty()) { + return ntd; + } + boolean modified; + if (ntd.isMixin()) { + // if mixin, remove possible nt:base supertype + modified = supertypes.remove(NameConstants.NT_BASE); + } else { + // check if all supertypes (except nt:base) are mixins + boolean allMixins = true; + for (Name name: supertypes) { + if (!name.equals(NameConstants.NT_BASE)) { + QNodeTypeDefinition def = ntdCache.get(name); + if (def != null && !def.isMixin()) { + allMixins = false; + break; + } + } + } + if (allMixins) { + // ntd is a primary node type and has only mixins as supertypes, + // so it needs a nt:base + modified = supertypes.add(NameConstants.NT_BASE); + } else { + // ntd is a primary node type and at least one of the supertypes + // is too, so ensure that no nt:base is added. note that the + // trivial case, where there would be no supertype left is handled + // in the QNodeTypeDefinition directly + modified = supertypes.remove(NameConstants.NT_BASE); + } + } + if (modified) { + ntd = new QNodeTypeDefinitionImpl(ntd.getName(), + supertypes.toArray(new Name[supertypes.size()]), + ntd.getSupportedMixinTypes(), ntd.isMixin(), + ntd.isAbstract(), ntd.isQueryable(), + ntd.hasOrderableChildNodes(), ntd.getPrimaryItemName(), + ntd.getPropertyDefs(), ntd.getChildNodeDefs()); + } + return ntd; + } + + /** + * Validates the specified NodeTypeDef within the context of + * the two other given collections and returns an EffectiveNodeType. + * + * @param ntd node type definition + * @param entCache effective node type cache + * @param ntdCache cache of 'known' node type definitions, used to resolve dependencies + * @param nsReg namespace registry used for validatingatch names + * @param lenient flag governing whether validation can be lenient or has to be strict + * @return an effective node type representation of the specified QNodeTypeDefinition + * @throws InvalidNodeTypeDefException if the node type is not valid + * @throws RepositoryException if another error occurs + */ + private static EffectiveNodeType validateNodeTypeDef(QNodeTypeDefinition ntd, + EffectiveNodeTypeCache entCache, + Map ntdCache, + NamespaceRegistry nsReg, + boolean lenient) + throws InvalidNodeTypeDefException, RepositoryException { + + /** + * the effective (i.e. merged and resolved) node type resulting from + * the specified node type definition; + * the effective node type will finally be created after the definition + * has been verified and checked for conflicts etc.; in some cases it + * will be created already at an earlier stage during the validation + * of child node definitions + */ + EffectiveNodeType ent = null; + + Name name = ntd.getName(); + if (name == null) { + String msg = "no name specified"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + checkNamespace(name, nsReg); + + // validate supertypes + Name[] supertypes = ntd.getSupertypes(); + if (supertypes.length > 0) { + for (Name supertype : supertypes) { + checkNamespace(supertype, nsReg); + /** + * simple check for infinite recursion + * (won't trap recursion on a deeper inheritance level) + */ + if (name.equals(supertype)) { + String msg = "[" + name + "] invalid supertype: " + + supertype + " (infinite recursion))"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + if (!ntdCache.containsKey(supertype)) { + String msg = "[" + name + "] invalid supertype: " + + supertype; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + } + + /** + * check for circularity in inheritance chain + * ('a' extends 'b' extends 'a') + */ + Stack inheritanceChain = new Stack(); + inheritanceChain.push(name); + checkForCircularInheritance(supertypes, inheritanceChain, ntdCache); + } + + /** + * note that infinite recursion through inheritance is automatically + * being checked by the following call to getEffectiveNodeType(...) + * as it's impossible to register a node type definition which + * references a supertype that isn't registered yet... + */ + + /** + * build effective (i.e. merged and resolved) node type from supertypes + * and check for conflicts + */ + if (supertypes.length > 0) { + try { + EffectiveNodeType est = getEffectiveNodeType(supertypes, entCache, ntdCache); + // check whether specified node type definition overrides + // a supertypes's primaryItem -> illegal (JCR-1947) + if (ntd.getPrimaryItemName() != null + && est.getPrimaryItemName() != null) { + String msg = "[" + name + "] primaryItemName is already specified by a supertype and must therefore not be overridden."; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + + } + } catch (NodeTypeConflictException ntce) { + String msg = "[" + name + "] failed to validate supertypes"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, ntce); + } catch (NoSuchNodeTypeException nsnte) { + String msg = "[" + name + "] failed to validate supertypes"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, nsnte); + } + } + + checkNamespace(ntd.getPrimaryItemName(), nsReg); + + // validate property definitions + QPropertyDefinition[] pda = ntd.getPropertyDefs(); + for (QPropertyDefinition pd : pda) { + /** + * sanity check: + * make sure declaring node type matches name of node type definition + */ + if (!name.equals(pd.getDeclaringNodeType())) { + String msg = "[" + name + "#" + pd.getName() + + "] invalid declaring node type specified"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + checkNamespace(pd.getName(), nsReg); + // check that auto-created properties specify a name + if (pd.definesResidual() && pd.isAutoCreated()) { + String msg = "[" + name + "#" + pd.getName() + + "] auto-created properties must specify a name"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + // check that auto-created properties specify a type + if (pd.getRequiredType() == PropertyType.UNDEFINED + && pd.isAutoCreated()) { + String msg = "[" + name + "#" + pd.getName() + + "] auto-created properties must specify a type"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + /** + * check default values: + * make sure type of value is consistent with required property type + */ + QValue[] defVals = pd.getDefaultValues(); + if (defVals != null && defVals.length != 0) { + int reqType = pd.getRequiredType(); + for (QValue defVal : defVals) { + if (reqType == PropertyType.UNDEFINED) { + reqType = defVal.getType(); + } else { + if (defVal.getType() != reqType) { + String msg = "[" + name + "#" + pd.getName() + + "] type of default value(s) is not consistent with required property type"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + } + } + } else { + // no default values specified + if (!lenient) { + // auto-created properties must have a default value + if (pd.isAutoCreated()) { + String msg = "[" + name + "#" + pd.getName() + + "] auto-created property must have a default value"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + } + } + + // check that default values satisfy value constraints + QValueConstraint[] constraints = pd.getValueConstraints(); + if (constraints != null && constraints.length > 0) { + if (defVals != null && defVals.length > 0) { + // check value constraints on every value + for (QValue defVal : defVals) { + // constraints are OR-ed together + boolean satisfied = false; + ConstraintViolationException cve = null; + for (QValueConstraint constraint : constraints) { + try { + constraint.check(defVal); + // at least one constraint is satisfied + satisfied = true; + break; + } catch (ConstraintViolationException e) { + cve = e; + } + } + if (!satisfied) { + // report last exception we encountered + String msg = "[" + name + "#" + pd.getName() + + "] default value does not satisfy value constraint"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, cve); + } + } + } + + /** + * ReferenceConstraint: + * the specified node type must be registered, with one notable + * exception: the node type just being registered + */ + if (pd.getRequiredType() == PropertyType.REFERENCE + || pd.getRequiredType() == PropertyType.WEAKREFERENCE) { + for (QValueConstraint constraint : constraints) { + Name ntName = NameFactoryImpl.getInstance().create(constraint.getString()); + if (!name.equals(ntName) && !ntdCache.containsKey(ntName)) { + String msg = "[" + name + "#" + pd.getName() + + "] invalid " + + (pd.getRequiredType() == PropertyType.REFERENCE ? "REFERENCE" : "WEAKREFERENCE") + + " value constraint '" + + ntName + "' (unknown node type)"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + } + } + } + } + + // validate child-node definitions + QNodeDefinition[] cnda = ntd.getChildNodeDefs(); + for (QNodeDefinition cnd : cnda) { + /** + * sanity check: + * make sure declaring node type matches name of node type definition + */ + if (!name.equals(cnd.getDeclaringNodeType())) { + String msg = "[" + name + "#" + cnd.getName() + + "] invalid declaring node type specified"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + checkNamespace(cnd.getName(), nsReg); + // check that auto-created child-nodes specify a name + if (cnd.definesResidual() && cnd.isAutoCreated()) { + String msg = "[" + name + "#" + cnd.getName() + + "] auto-created child-nodes must specify a name"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + // check that auto-created child-nodes specify a default primary type + if (cnd.getDefaultPrimaryType() == null + && cnd.isAutoCreated()) { + String msg = "[" + name + "#" + cnd.getName() + + "] auto-created child-nodes must specify a default primary type"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + // check default primary type + Name dpt = cnd.getDefaultPrimaryType(); + checkNamespace(dpt, nsReg); + boolean referenceToSelf = false; + EffectiveNodeType defaultENT = null; + if (dpt != null) { + // check if this node type specifies itself as default primary type + if (name.equals(dpt)) { + referenceToSelf = true; + } + /** + * the default primary type must be registered, with one notable + * exception: the node type just being registered + */ + if (!name.equals(dpt) && !ntdCache.containsKey(dpt)) { + String msg = "[" + name + "#" + cnd.getName() + + "] invalid default primary type '" + dpt + "'"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + /** + * build effective (i.e. merged and resolved) node type from + * default primary type and check for conflicts + */ + try { + if (!referenceToSelf) { + defaultENT = getEffectiveNodeType(dpt, entCache, ntdCache); + } else { + /** + * the default primary type is identical with the node + * type just being registered; we have to instantiate it + * 'manually' + */ + ent = EffectiveNodeType.create(ntd, entCache, ntdCache); + defaultENT = ent; + } + if (cnd.isAutoCreated()) { + /** + * check for circularity through default primary types + * of auto-created child nodes (node type 'a' defines + * auto-created child node with default primary type 'a') + */ + Stack definingNTs = new Stack(); + definingNTs.push(name); + checkForCircularNodeAutoCreation(defaultENT, definingNTs, entCache, ntdCache); + } + } catch (NodeTypeConflictException ntce) { + String msg = "[" + name + "#" + cnd.getName() + + "] failed to validate default primary type"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, ntce); + } catch (NoSuchNodeTypeException nsnte) { + String msg = "[" + name + "#" + cnd.getName() + + "] failed to validate default primary type"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, nsnte); + } + } + + // check required primary types + Name[] reqTypes = cnd.getRequiredPrimaryTypes(); + if (reqTypes != null && reqTypes.length > 0) { + for (Name rpt : reqTypes) { + // skip nt:base required types + if (NameConstants.NT_BASE.equals(rpt)) { + continue; + } + checkNamespace(rpt, nsReg); + referenceToSelf = false; + /** + * check if this node type specifies itself as required + * primary type + */ + if (name.equals(rpt)) { + referenceToSelf = true; + } + /** + * the required primary type must be registered, with one + * notable exception: the node type just being registered + */ + if (!name.equals(rpt) && !ntdCache.containsKey(rpt)) { + String msg = "[" + name + "#" + cnd.getName() + + "] invalid required primary type: " + rpt; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + /** + * check if default primary type satisfies the required + * primary type constraint + */ + if (defaultENT != null && !defaultENT.includesNodeType(rpt)) { + String msg = "[" + name + "#" + cnd.getName() + + "] default primary type does not satisfy required primary type constraint " + + rpt; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg); + } + /** + * build effective (i.e. merged and resolved) node type from + * required primary type constraint and check for conflicts + */ + try { + if (!referenceToSelf) { + getEffectiveNodeType(rpt, entCache, ntdCache); + } else { + /** + * the required primary type is identical with the + * node type just being registered; we have to + * instantiate it 'manually' + */ + if (ent == null) { + ent = EffectiveNodeType.create(ntd, entCache, ntdCache); + } + } + } catch (NodeTypeConflictException ntce) { + String msg = "[" + name + "#" + cnd.getName() + + "] failed to validate required primary type constraint"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, ntce); + } catch (NoSuchNodeTypeException nsnte) { + String msg = "[" + name + "#" + cnd.getName() + + "] failed to validate required primary type constraint"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, nsnte); + } + } + } + } + + /** + * now build effective (i.e. merged and resolved) node type from + * this node type definition; this will potentially detect more + * conflicts or problems + */ + if (ent == null) { + try { + ent = EffectiveNodeType.create(ntd, entCache, ntdCache); + } catch (NodeTypeConflictException ntce) { + String msg = "[" + name + "] failed to resolve node type definition"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, ntce); + } catch (NoSuchNodeTypeException nsnte) { + String msg = "[" + name + "] failed to resolve node type definition"; + log.debug(msg); + throw new InvalidNodeTypeDefException(msg, nsnte); + } + } + return ent; + } + + private static QNodeDefinition createRootNodeDef() { + QNodeDefinitionBuilder def = new QNodeDefinitionBuilder(); + + // FIXME need a fake declaring node type: + // rep:root is not quite correct but better than a non-existing node type + def.setDeclaringNodeType(NameConstants.REP_ROOT); + def.setRequiredPrimaryTypes(new Name[]{NameConstants.REP_ROOT}); + def.setDefaultPrimaryType(NameConstants.REP_ROOT); + def.setMandatory(true); + def.setProtected(false); + def.setOnParentVersion(OnParentVersionAction.VERSION); + def.setAllowsSameNameSiblings(false); + def.setAutoCreated(true); + return def.build(); + } + + /** + * Notify the listeners that a node type ntName has been registered. + * @param ntName node type name + */ + private void notifyRegistered(Name ntName) { + // copy listeners to array to avoid ConcurrentModificationException + NodeTypeRegistryListener[] la = listeners.values().toArray( + new NodeTypeRegistryListener[listeners.size()]); + for (NodeTypeRegistryListener aLa : la) { + if (aLa != null) { + aLa.nodeTypeRegistered(ntName); + } + } + } + + /** + * Notify the listeners that a node type ntName has been re-registered. + * @param ntName node type name + */ + private void notifyReRegistered(Name ntName) { + // copy listeners to array to avoid ConcurrentModificationException + NodeTypeRegistryListener[] la = listeners.values().toArray( + new NodeTypeRegistryListener[listeners.size()]); + for (NodeTypeRegistryListener aLa : la) { + if (aLa != null) { + aLa.nodeTypeReRegistered(ntName); + } + } + } + + /** + * Notify the listeners that oone or more node types have been unregistered. + * @param names node type names + */ + private void notifyUnregistered(Collection names) { + // copy listeners to array to avoid ConcurrentModificationException + NodeTypeRegistryListener[] la = listeners.values().toArray( + new NodeTypeRegistryListener[listeners.size()]); + for (NodeTypeRegistryListener aLa : la) { + if (aLa != null) { + aLa.nodeTypesUnregistered(names); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeRegistryListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeRegistryListener.java new file mode 100644 index 00000000000..16cceb5c4c5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/NodeTypeRegistryListener.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.core.nodetype; + +import java.util.Collection; + +import org.apache.jackrabbit.spi.Name; + +/** + * The NodeTypeRegistryListener interface allows an implementing + * object to be informed about node type (un)registration. + * + * @see NodeTypeRegistry#addListener(NodeTypeRegistryListener) + * @see NodeTypeRegistry#removeListener(NodeTypeRegistryListener) + */ +public interface NodeTypeRegistryListener { + + /** + * Called when a node type has been registered. + * + * @param ntName name of the node type that has been registered + */ + void nodeTypeRegistered(Name ntName); + + /** + * Called when a node type has been re-registered. + * + * @param ntName name of the node type that has been registered + */ + void nodeTypeReRegistered(Name ntName); + + /** + * Called when a set of node types has been unregistered. + * + * @param names names of the types that have been unregistered + */ + void nodeTypesUnregistered(Collection names); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/virtual/VirtualNodeTypeStateManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/virtual/VirtualNodeTypeStateManager.java new file mode 100644 index 00000000000..5a911559413 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/virtual/VirtualNodeTypeStateManager.java @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype.virtual; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import javax.jcr.NodeIterator; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistryListener; +import org.apache.jackrabbit.core.observation.DelegatingObservationDispatcher; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This Class implements a workaround helper for populating observation + * events for the virtual node states of the jcr:nodeTypes upon nodetype + * registry changes. + */ +public class VirtualNodeTypeStateManager implements NodeTypeRegistryListener { + + /** + * the default logger + */ + private static Logger log = LoggerFactory.getLogger(VirtualNodeTypeStateManager.class); + + /** + * Path: /jcr:system/jcr:nodeTypes + */ + private static final Path NODE_TYPES_PATH; + + static { + try { + PathBuilder builder = new PathBuilder(); + builder.addRoot(); + builder.addLast(NameConstants.JCR_SYSTEM); + builder.addLast(NameConstants.JCR_NODETYPES); + NODE_TYPES_PATH = builder.getPath(); + } catch (MalformedPathException e) { + // will not happen. path is always valid + throw new InternalError("Cannot initialize path"); + } + } + + /** + * an item state provider for the virtual nodetype states + */ + private VirtualNodeTypeStateProvider virtProvider; + + /** + * the node type registry + */ + private final NodeTypeRegistry ntReg; + + /** + * the root node id (usually the id of /jcr:system/jcr:nodeTypes) + */ + private final NodeId rootNodeId; + + /** + * the id of the roots parent (usually id of /jcr:system) + */ + private final NodeId parentId; + + /** + * the system session to generate the observation events + */ + private SessionImpl systemSession; + + /** + * the delegtating observation manager, that dispatches the events to + * all underlying ones. + */ + private DelegatingObservationDispatcher obsDispatcher; + + /** + * Creates a new virtual node type state manager + * + * @param ntReg + * @param obs + * @param rootNodeId + * @param parentId + */ + public VirtualNodeTypeStateManager( + NodeTypeRegistry ntReg, DelegatingObservationDispatcher obs, + NodeId rootNodeId, NodeId parentId) { + this.ntReg = ntReg; + this.obsDispatcher = obs; + this.rootNodeId = rootNodeId; + this.parentId = parentId; + ntReg.addListener(this); + } + + /** + * returns the virtual node state provider for the node type states. + * @return the virtual item state provider + */ + public synchronized VirtualItemStateProvider getVirtualItemStateProvider() { + if (virtProvider == null) { + virtProvider = new VirtualNodeTypeStateProvider(ntReg, rootNodeId, parentId); + } + return virtProvider; + } + + /** + * Sets the system session. This is needed, since the session should be + * set, after the workspaces are initialized. + * + * @param systemSession + */ + public void setSession(SessionImpl systemSession) { + this.systemSession = systemSession; + } + + /** + * {@inheritDoc} + */ + public void nodeTypeRegistered(Name ntName) { + try { + if (virtProvider != null) { + // allow provider to update + virtProvider.onNodeTypeAdded(ntName); + } + if (systemSession != null) { + // generate observation events + NodeImpl root = (NodeImpl) systemSession.getItemManager().getItem(rootNodeId); + NodeImpl child = root.getNode(ntName); + List events = new ArrayList(); + recursiveAdd(events, root, child); + obsDispatcher.dispatch(events, systemSession, + NODE_TYPES_PATH, null); + } + } catch (RepositoryException e) { + log.error("Unable to index new nodetype: " + e.toString()); + } + } + + /** + * {@inheritDoc} + */ + public void nodeTypeReRegistered(Name ntName) { + // lazy implementation + nodeTypesUnregistered(Collections.singleton(ntName)); + nodeTypeRegistered(ntName); + } + + /** + * {@inheritDoc} + */ + public void nodeTypesUnregistered(Collection names) { + try { + if (systemSession != null) { + // generated observation events + List events = new ArrayList(); + + NodeImpl root = (NodeImpl) + systemSession.getItemManager().getItem(rootNodeId); + for (Name name : names) { + NodeImpl child = root.getNode(name); + recursiveRemove(events, root, child); + } + + obsDispatcher.dispatch(events, systemSession, + NODE_TYPES_PATH, null); + } + if (virtProvider != null) { + // allow provider to update + virtProvider.onNodeTypesRemoved(names); + } + } catch (RepositoryException e) { + log.error("Unable to index removed nodetypes: " + names, e); + } + } + + /** + * Adds a subtree of itemstates as 'added' to a list of events + * + * @param events + * @param parent + * @param node + * @throws RepositoryException + */ + private void recursiveAdd( + List events, NodeImpl parent, NodeImpl node) + throws RepositoryException { + + events.add(EventState.childNodeAdded( + parent.getNodeId(), + parent.getPrimaryPath(), + node.getNodeId(), + node.getPrimaryPath().getLastElement(), + ((NodeTypeImpl) parent.getPrimaryNodeType()).getQName(), + parent.getMixinTypeNames(), + node.getSession() + )); + + PropertyIterator iter = node.getProperties(); + while (iter.hasNext()) { + PropertyImpl prop = (PropertyImpl) iter.nextProperty(); + events.add(EventState.propertyAdded( + (NodeId) node.getId(), + node.getPrimaryPath(), + prop.getPrimaryPath().getLastElement(), + ((NodeTypeImpl) node.getPrimaryNodeType()).getQName(), + node.getMixinTypeNames(), + node.getSession() + )); + } + NodeIterator niter = node.getNodes(); + while (niter.hasNext()) { + NodeImpl n = (NodeImpl) niter.nextNode(); + recursiveAdd(events, node, n); + } + } + + /** + * Adds a subtree of itemstates as 'removed' to a list of events + * + * @param events + * @param parent + * @param node + * @throws RepositoryException + */ + private void recursiveRemove( + List events, NodeImpl parent, NodeImpl node) + throws RepositoryException { + + events.add(EventState.childNodeRemoved( + parent.getNodeId(), + parent.getPrimaryPath(), + node.getNodeId(), + node.getPrimaryPath().getLastElement(), + ((NodeTypeImpl) parent.getPrimaryNodeType()).getQName(), + parent.getMixinTypeNames(), + node.getSession() + )); + NodeIterator niter = node.getNodes(); + while (niter.hasNext()) { + NodeImpl n = (NodeImpl) niter.nextNode(); + recursiveRemove(events, node, n); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/virtual/VirtualNodeTypeStateProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/virtual/VirtualNodeTypeStateProvider.java new file mode 100644 index 00000000000..3c399141fdb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/virtual/VirtualNodeTypeStateProvider.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype.virtual; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.virtual.AbstractVISProvider; +import org.apache.jackrabbit.core.virtual.VirtualNodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * This Class implements a virtual item state provider that exposes the + * registered nodetypes. + */ +public class VirtualNodeTypeStateProvider extends AbstractVISProvider { + + /** + * the parent id + */ + private final NodeId parentId; + + /** + * @param ntReg + * @param rootNodeId + * @param parentId + */ + public VirtualNodeTypeStateProvider(NodeTypeRegistry ntReg, + NodeId rootNodeId, NodeId parentId) { + super(ntReg, rootNodeId); + this.parentId = parentId; + try { + getRootState(); + } catch (ItemStateException e) { + // ignore + } + } + + /** + * {@inheritDoc} + *

    + * currently we have no dynamic ones, we just recreate the entire nodetypes tree + */ + protected VirtualNodeState createRootNodeState() throws RepositoryException { + VirtualNodeState root = new VirtualNodeState(this, parentId, rootNodeId, NameConstants.REP_NODETYPES, null); + Name[] ntNames = ntReg.getRegisteredNodeTypes(); + for (int i = 0; i < ntNames.length; i++) { + QNodeTypeDefinition ntDef = ntReg.getNodeTypeDef(ntNames[i]); + VirtualNodeState ntState = createNodeTypeState(root, ntDef); + root.addChildNodeEntry(ntNames[i], ntState.getNodeId()); + // add as hard reference + root.addStateReference(ntState); + } + return root; + } + + /** + * {@inheritDoc} + */ + protected boolean internalHasNodeState(NodeId id) { + return false; + } + + /** + * {@inheritDoc} + */ + protected VirtualNodeState internalGetNodeState(NodeId id) throws NoSuchItemStateException, ItemStateException { + return null; + } + + public void onNodeTypeAdded(Name ntName) { + discardAll(); // TODO: More efficient reloading + } + + public void onNodeTypeModified(Name ntName) { + discardAll(); // TODO: More efficient reloading + } + + public void onNodeTypesRemoved(Collection names) { + discardAll(); // TODO: More efficient reloading + } + + /** + * Creates a node type state + * + * @param parent + * @param ntDef + * @return + * @throws RepositoryException + */ + private VirtualNodeState createNodeTypeState(VirtualNodeState parent, + QNodeTypeDefinition ntDef) + throws RepositoryException { + NodeId id = calculateStableId(ntDef.getName().toString()); + VirtualNodeState ntState = createNodeState(parent, ntDef.getName(), id, NameConstants.NT_NODETYPE); + + // add properties + ntState.setPropertyValue(NameConstants.JCR_NODETYPENAME, InternalValue.create(ntDef.getName())); + ntState.setPropertyValues(NameConstants.JCR_SUPERTYPES, PropertyType.NAME, InternalValue.create(ntDef.getSupertypes())); + ntState.setPropertyValue(NameConstants.JCR_ISMIXIN, InternalValue.create(ntDef.isMixin())); + ntState.setPropertyValue(NameConstants.JCR_HASORDERABLECHILDNODES, InternalValue.create(ntDef.hasOrderableChildNodes())); + if (ntDef.getPrimaryItemName() != null) { + ntState.setPropertyValue(NameConstants.JCR_PRIMARYITEMNAME, InternalValue.create(ntDef.getPrimaryItemName())); + } + + // add property defs + QPropertyDefinition[] propDefs = ntDef.getPropertyDefs(); + for (int i = 0; i < propDefs.length; i++) { + VirtualNodeState pdState = createPropertyDefState(ntState, propDefs[i], ntDef, i); + ntState.addChildNodeEntry(NameConstants.JCR_PROPERTYDEFINITION, pdState.getNodeId()); + // add as hard reference + ntState.addStateReference(pdState); + } + + // add child node defs + QNodeDefinition[] cnDefs = ntDef.getChildNodeDefs(); + for (int i = 0; i < cnDefs.length; i++) { + VirtualNodeState cnState = createChildNodeDefState(ntState, cnDefs[i], ntDef, i); + ntState.addChildNodeEntry(NameConstants.JCR_CHILDNODEDEFINITION, cnState.getNodeId()); + // add as hard reference + ntState.addStateReference(cnState); + } + + return ntState; + } + + /** + * creates a node state for the given property def + * + * @param parent + * @param propDef + * @return + * @throws RepositoryException + */ + private VirtualNodeState createPropertyDefState(VirtualNodeState parent, + QPropertyDefinition propDef, + QNodeTypeDefinition ntDef, int n) + throws RepositoryException { + NodeId id = calculateStableId( + ntDef.getName().toString() + "/" + NameConstants.JCR_PROPERTYDEFINITION.toString() + "/" + n); + VirtualNodeState pState = createNodeState( + parent, NameConstants.JCR_PROPERTYDEFINITION, id, + NameConstants.NT_PROPERTYDEFINITION); + // add properties + if (!propDef.definesResidual()) { + pState.setPropertyValue(NameConstants.JCR_NAME, InternalValue.create(propDef.getName())); + } + pState.setPropertyValue(NameConstants.JCR_AUTOCREATED, InternalValue.create(propDef.isAutoCreated())); + pState.setPropertyValue(NameConstants.JCR_MANDATORY, InternalValue.create(propDef.isMandatory())); + pState.setPropertyValue(NameConstants.JCR_ONPARENTVERSION, + InternalValue.create(OnParentVersionAction.nameFromValue(propDef.getOnParentVersion()))); + pState.setPropertyValue(NameConstants.JCR_PROTECTED, InternalValue.create(propDef.isProtected())); + pState.setPropertyValue(NameConstants.JCR_MULTIPLE, InternalValue.create(propDef.isMultiple())); + pState.setPropertyValue( + NameConstants.JCR_REQUIREDTYPE, + InternalValue.create(PropertyType.nameFromValue(propDef.getRequiredType()).toUpperCase())); + InternalValue[] defVals = InternalValue.create(propDef.getDefaultValues()); + // retrieve the property type from the first default value present with + // the property definition. in case no default values are defined, + // fallback to PropertyType.STRING in order to avoid creating a property + // with type UNDEFINED which is illegal. + int defValsType = PropertyType.STRING; + if (defVals != null && defVals.length > 0) { + defValsType = defVals[0].getType(); + } + if (defVals != null) { + pState.setPropertyValues(NameConstants.JCR_DEFAULTVALUES, defValsType, defVals); + } + QValueConstraint[] vc = propDef.getValueConstraints(); + InternalValue[] vals = new InternalValue[vc.length]; + for (int i = 0; i < vc.length; i++) { + vals[i] = InternalValue.create(vc[i].getString()); + } + pState.setPropertyValues(NameConstants.JCR_VALUECONSTRAINTS, PropertyType.STRING, vals); + return pState; + } + + /** + * creates a node state for the given child node def + * + * @param parent + * @param cnDef + * @return + * @throws RepositoryException + */ + private VirtualNodeState createChildNodeDefState(VirtualNodeState parent, + QNodeDefinition cnDef, + QNodeTypeDefinition ntDef, int n) + throws RepositoryException { + NodeId id = calculateStableId( + ntDef.getName().toString() + "/" + NameConstants.JCR_CHILDNODEDEFINITION.toString() + "/" + n); + VirtualNodeState pState = createNodeState( + parent, NameConstants.JCR_CHILDNODEDEFINITION, id, NameConstants.NT_CHILDNODEDEFINITION); + // add properties + if (!cnDef.definesResidual()) { + pState.setPropertyValue(NameConstants.JCR_NAME, InternalValue.create(cnDef.getName())); + } + pState.setPropertyValue(NameConstants.JCR_AUTOCREATED, InternalValue.create(cnDef.isAutoCreated())); + pState.setPropertyValue(NameConstants.JCR_MANDATORY, InternalValue.create(cnDef.isMandatory())); + pState.setPropertyValue(NameConstants.JCR_ONPARENTVERSION, + InternalValue.create(OnParentVersionAction.nameFromValue(cnDef.getOnParentVersion()))); + pState.setPropertyValue(NameConstants.JCR_PROTECTED, InternalValue.create(cnDef.isProtected())); + pState.setPropertyValues(NameConstants.JCR_REQUIREDPRIMARYTYPES, + PropertyType.NAME, InternalValue.create(cnDef.getRequiredPrimaryTypes())); + if (cnDef.getDefaultPrimaryType() != null) { + pState.setPropertyValue(NameConstants.JCR_DEFAULTPRIMARYTYPE, InternalValue.create(cnDef.getDefaultPrimaryType())); + } + pState.setPropertyValue(NameConstants.JCR_SAMENAMESIBLINGS, InternalValue.create(cnDef.allowsSameNameSiblings())); + return pState; + } + + /** + * Calculates a stable identifier out of the given string. The algorithm + * does a MD5 digest from the string an converts it into the UUID format. + * + * @param name + * @return + * @throws RepositoryException + */ + private static NodeId calculateStableId(String name) throws RepositoryException { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(name.getBytes(StandardCharsets.UTF_8)); + return new NodeId(digest); + } catch (NoSuchAlgorithmException e) { + throw new RepositoryException(e); + } + } + + /** + * {@inheritDoc} + */ + public boolean setNodeReferences(ChangeLog references) { + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/AdditionalNamespaceResolver.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/AdditionalNamespaceResolver.java new file mode 100644 index 00000000000..28b07db8edb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/AdditionalNamespaceResolver.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.core.nodetype.xml; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import java.util.Enumeration; +import java.util.Properties; + +/** + * A simple namespace resolver implementation, that uses the additional + * namespaces declared in an XML element. + */ +public class AdditionalNamespaceResolver implements NamespaceResolver { + + /** Map from namespace prefixes to namespace URIs. */ + private final Properties prefixToURI = new Properties(); + + /** Map from namespace URIs to namespace prefixes. */ + private final Properties uriToPrefix = new Properties(); + + /** + * Creates a namespace resolver using the namespaces defined in + * the given prefix-to-URI property set. + * + * @param namespaces namespace properties + */ + public AdditionalNamespaceResolver(Properties namespaces) { + Enumeration prefixes = namespaces.propertyNames(); + while (prefixes.hasMoreElements()) { + String prefix = (String) prefixes.nextElement(); + addNamespace(prefix, namespaces.getProperty(prefix)); + } + addNamespace("", ""); + } + + /** + * Creates a namespace resolver using the namespaces declared + * in the given namespace registry. + * + * @param registry namespace registry + * @throws RepositoryException on repository errors + */ + public AdditionalNamespaceResolver(NamespaceRegistry registry) + throws RepositoryException { + String[] prefixes = registry.getPrefixes(); + for (int i = 0; i < prefixes.length; i++) { + addNamespace(prefixes[i], registry.getURI(prefixes[i])); + } + } + + /** + * Adds the given namespace declaration to this resolver. + * + * @param prefix namespace prefix + * @param uri namespace URI + */ + private void addNamespace(String prefix, String uri) { + prefixToURI.put(prefix, uri); + uriToPrefix.put(uri, prefix); + } + + /** {@inheritDoc} */ + public String getURI(String prefix) throws NamespaceException { + String uri = prefixToURI.getProperty(prefix); + if (uri != null) { + return uri; + } else { + throw new NamespaceException( + "Unknown namespace prefix " + prefix + "."); + } + } + + /** {@inheritDoc} */ + public String getPrefix(String uri) throws NamespaceException { + String prefix = uriToPrefix.getProperty(uri); + if (prefix != null) { + return prefix; + } else { + throw new NamespaceException( + "Unknown namespace URI " + uri + "."); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/Constants.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/Constants.java new file mode 100644 index 00000000000..ffb36c2c433 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/Constants.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.core.nodetype.xml; + +/** + * Name constants for the node type XML elements and attributes. + */ +public interface Constants { + + /** Name of the node type definition root element. */ + String NODETYPES_ELEMENT = "nodeTypes"; + + /** Name of the node type definition element. */ + String NODETYPE_ELEMENT = "nodeType"; + + /** Name of the child node definition element. */ + String CHILDNODEDEFINITION_ELEMENT = "childNodeDefinition"; + + /** Name of the property definition element. */ + String PROPERTYDEFINITION_ELEMENT = "propertyDefinition"; + + /** Name of the isMixin attribute. */ + String ISMIXIN_ATTRIBUTE = "isMixin"; + + /** Name of the isQueryable attribute. */ + String ISQUERYABLE_ATTRIBUTE = "isQueryable"; + + /** Name of the isAbstract attribute. */ + String ISABSTRACT_ATTRIBUTE = "isAbstract"; + + /** Name of the hasOrderableChildNodes attribute. */ + String HASORDERABLECHILDNODES_ATTRIBUTE = "hasOrderableChildNodes"; + + /** Name of the primary item name attribute. */ + String PRIMARYITEMNAME_ATTRIBUTE = "primaryItemName"; + + /** Name of the supertypes element. */ + String SUPERTYPES_ELEMENT = "supertypes"; + + /** Name of the supertype element. */ + String SUPERTYPE_ELEMENT = "supertype"; + + /** Name of the name attribute. */ + String NAME_ATTRIBUTE = "name"; + + /** Name of the autoCreated attribute. */ + String AUTOCREATED_ATTRIBUTE = "autoCreated"; + + /** Name of the mandatory attribute. */ + String MANDATORY_ATTRIBUTE = "mandatory"; + + /** Name of the onParentVersion attribute. */ + String ONPARENTVERSION_ATTRIBUTE = "onParentVersion"; + + /** Name of the protected attribute. */ + String PROTECTED_ATTRIBUTE = "protected"; + + /** Name of the required type attribute. */ + String REQUIREDTYPE_ATTRIBUTE = "requiredType"; + + /** Name of the value constraints element. */ + String VALUECONSTRAINTS_ELEMENT = "valueConstraints"; + + /** Name of the value constraint element. */ + String VALUECONSTRAINT_ELEMENT = "valueConstraint"; + + /** Name of the default values element. */ + String DEFAULTVALUES_ELEMENT = "defaultValues"; + + /** Name of the default value element. */ + String DEFAULTVALUE_ELEMENT = "defaultValue"; + + /** Name of the isQueryOrderable attribute. */ + String ISQUERYORDERABLE_ATTRIBUTE = "isQueryOrderable"; + + /** Name of the isFullTextSearchable attribute. */ + String ISFULLTEXTSEARCHABLE_ATTRIBUTE = "isFullTextSearchable"; + + /** Name of the availableQueryOperators attribute. */ + String AVAILABLEQUERYOPERATORS_ATTRIBUTE = "availableQueryOperators"; + + String EQ_ENTITY = "OP_EQ"; + String NE_ENTITY = "OP_NE"; + String LT_ENTITY = "OP_LT"; + String LE_ENTITY = "OP_LE"; + String GT_ENTITY = "OP_GT"; + String GE_ENTITY = "OP_GE"; + String LIKE_ENTITY = "OP_LIKE"; + + /** Name of the multiple attribute. */ + String MULTIPLE_ATTRIBUTE = "multiple"; + + /** Name of the required primary types element. */ + String REQUIREDPRIMARYTYPES_ELEMENT = "requiredPrimaryTypes"; + + /** Name of the required primary type element. */ + String REQUIREDPRIMARYTYPE_ELEMENT = "requiredPrimaryType"; + + /** Name of the default primary type attribute. */ + String DEFAULTPRIMARYTYPE_ATTRIBUTE = "defaultPrimaryType"; + + /** Name of the sameNameSiblings attribute. */ + String SAMENAMESIBLINGS_ATTRIBUTE = "sameNameSiblings"; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/NodeTypeReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/NodeTypeReader.java new file mode 100644 index 00000000000..c9c70e096ce --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/NodeTypeReader.java @@ -0,0 +1,369 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype.xml; + +import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException; +import org.apache.jackrabbit.core.util.DOMWalker; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.value.InternalValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; +import org.apache.jackrabbit.spi.commons.nodetype.QNodeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.QPropertyDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.QNodeTypeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.value.ValueHelper; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.NamespaceException; +import javax.jcr.ValueFactory; +import javax.jcr.Value; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.version.OnParentVersionAction; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.util.List; +import java.util.ArrayList; + +/** + * Node type definition reader. This class is used to read the + * persistent node type definition files used by Jackrabbit. + */ +public class NodeTypeReader { + + /** + * Reads a node type definition file. The file contents are read from + * the given input stream and the parsed node type definitions are + * returned. + * + * @param xml XML input stream + * @return node type definitions + * @throws IOException if the node type definitions + * cannot be read + * @throws InvalidNodeTypeDefException if the node type definition + * format is invalid + */ + public static QNodeTypeDefinition[] read(InputStream xml) + throws IOException, InvalidNodeTypeDefException { + try { + NodeTypeReader reader = new NodeTypeReader(xml); + return reader.getNodeTypeDefs(); + } catch (NameException e) { + throw new InvalidNodeTypeDefException( + "Invalid namespace reference in a node type definition", e); + } catch (NamespaceException e) { + throw new InvalidNodeTypeDefException( + "Invalid namespace reference in a node type definition", e); + } + } + + /** The node type document walker. */ + private final DOMWalker walker; + + /** The namespaces associated with the node type XML document. */ + private final Properties namespaces; + + /** The name, path resolver. */ + private final NamePathResolver resolver; + + private final ValueFactory valueFactory; + + private final QValueFactory qValueFactory = InternalValueFactory.getInstance(); + + /** + * Creates a node type definition file reader. + * + * @param xml node type definition file + * @throws IOException if the node type definition file cannot be read + */ + public NodeTypeReader(InputStream xml) throws IOException { + walker = new DOMWalker(xml); + namespaces = walker.getNamespaces(); + NamespaceResolver nsResolver = new AdditionalNamespaceResolver(namespaces); + resolver = new DefaultNamePathResolver(nsResolver); + valueFactory = new ValueFactoryQImpl(qValueFactory, resolver); + } + + /** + * Returns the namespaces declared in the node type definition + * file. + * @return the namespaces + */ + public Properties getNamespaces() { + return namespaces; + } + + /** + * Returns all node type definitions specified by node type elements + * under the current element. + * + * @return node type definitions + * @throws InvalidNodeTypeDefException if a definition is invalid + * @throws NameException if a definition contains an + * illegal name + * @throws NamespaceException if a namespace is not defined + */ + public QNodeTypeDefinition[] getNodeTypeDefs() + throws InvalidNodeTypeDefException, NameException, NamespaceException { + List defs = new ArrayList(); + while (walker.iterateElements(Constants.NODETYPE_ELEMENT)) { + defs.add(getNodeTypeDef()); + } + return defs.toArray(new QNodeTypeDefinition[defs.size()]); + } + + /** + * Returns the node type definition specified by the current element. + * + * @return node type definition + * @throws InvalidNodeTypeDefException if the definition is invalid + * @throws NameException if the definition contains an + * illegal name + * @throws NamespaceException if a namespace is not defined + */ + private QNodeTypeDefinition getNodeTypeDef() + throws InvalidNodeTypeDefException, NameException, NamespaceException { + QNodeTypeDefinitionBuilder type = new QNodeTypeDefinitionBuilder(); + + type.setName(resolver.getQName( + walker.getAttribute(Constants.NAME_ATTRIBUTE))); + type.setMixin(Boolean.valueOf( + walker.getAttribute(Constants.ISMIXIN_ATTRIBUTE))); + type.setOrderableChildNodes(Boolean.valueOf( + walker.getAttribute(Constants.HASORDERABLECHILDNODES_ATTRIBUTE))); + type.setAbstract(Boolean.valueOf( + walker.getAttribute(Constants.ISABSTRACT_ATTRIBUTE))); + if (walker.getAttribute(Constants.ISQUERYABLE_ATTRIBUTE) != null) { + type.setQueryable(Boolean.valueOf( + walker.getAttribute(Constants.ISQUERYABLE_ATTRIBUTE))); + } + String primaryItemName = + walker.getAttribute(Constants.PRIMARYITEMNAME_ATTRIBUTE); + if (primaryItemName != null && primaryItemName.length() > 0) { + type.setPrimaryItemName( + resolver.getQName(primaryItemName)); + } + + // supertype declarations + if (walker.enterElement(Constants.SUPERTYPES_ELEMENT)) { + List supertypes = new ArrayList(); + while (walker.iterateElements(Constants.SUPERTYPE_ELEMENT)) { + supertypes.add( + resolver.getQName(walker.getContent())); + } + type.setSupertypes(supertypes.toArray(new Name[supertypes.size()])); + walker.leaveElement(); + } + + // property definitions + List properties = new ArrayList(); + while (walker.iterateElements(Constants.PROPERTYDEFINITION_ELEMENT)) { + QPropertyDefinitionBuilder def = getPropDef(); + def.setDeclaringNodeType(type.getName()); + properties.add(def.build()); + } + type.setPropertyDefs(properties.toArray(new QPropertyDefinition[properties.size()])); + + // child node definitions + List nodes = new ArrayList(); + while (walker.iterateElements(Constants.CHILDNODEDEFINITION_ELEMENT)) { + QNodeDefinitionBuilder def = getChildNodeDef(); + def.setDeclaringNodeType(type.getName()); + nodes.add(def.build()); + } + type.setChildNodeDefs(nodes.toArray(new QNodeDefinition[nodes.size()])); + + return type.build(); + } + + /** + * Returns the property definition specified by the current element. + * + * @return property definition + * @throws InvalidNodeTypeDefException if the definition is invalid + * @throws NameException if the definition contains an + * illegal name + * @throws NamespaceException if a namespace is not defined + */ + private QPropertyDefinitionBuilder getPropDef() + throws InvalidNodeTypeDefException, NameException, NamespaceException { + QPropertyDefinitionBuilder def = new QPropertyDefinitionBuilder(); + String name = walker.getAttribute(Constants.NAME_ATTRIBUTE); + if (name.equals("*")) { + def.setName(NameConstants.ANY_NAME); + } else { + def.setName(resolver.getQName(name)); + } + + // simple attributes + def.setAutoCreated(Boolean.valueOf( + walker.getAttribute(Constants.AUTOCREATED_ATTRIBUTE))); + def.setMandatory(Boolean.valueOf( + walker.getAttribute(Constants.MANDATORY_ATTRIBUTE))); + def.setProtected(Boolean.valueOf( + walker.getAttribute(Constants.PROTECTED_ATTRIBUTE))); + def.setOnParentVersion(OnParentVersionAction.valueFromName( + walker.getAttribute(Constants.ONPARENTVERSION_ATTRIBUTE))); + def.setMultiple(Boolean.valueOf( + walker.getAttribute(Constants.MULTIPLE_ATTRIBUTE))); + def.setFullTextSearchable(Boolean.valueOf( + walker.getAttribute(Constants.ISFULLTEXTSEARCHABLE_ATTRIBUTE))); + def.setQueryOrderable(Boolean.valueOf( + walker.getAttribute(Constants.ISQUERYORDERABLE_ATTRIBUTE))); + String s = walker.getAttribute(Constants.AVAILABLEQUERYOPERATORS_ATTRIBUTE); + if (s != null && s.length() > 0) { + String[] ops = s.split(" "); + List queryOps = new ArrayList(); + for (String op1 : ops) { + String op = op1.trim(); + if (op.equals(Constants.EQ_ENTITY)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO); + } else if (op.equals(Constants.NE_ENTITY)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO); + } else if (op.equals(Constants.LT_ENTITY)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN); + } else if (op.equals(Constants.LE_ENTITY)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO); + } else if (op.equals(Constants.GT_ENTITY)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN); + } else if (op.equals(Constants.GE_ENTITY)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO); + } else if (op.equals(Constants.LIKE_ENTITY)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_LIKE); + } else { + throw new InvalidNodeTypeDefException("'" + op + "' is not a valid query operator"); + } + } + def.setAvailableQueryOperators(queryOps.toArray(new String[queryOps.size()])); + + } + def.setRequiredType(PropertyType.valueFromName( + walker.getAttribute(Constants.REQUIREDTYPE_ATTRIBUTE))); + + // value constraints + if (walker.enterElement(Constants.VALUECONSTRAINTS_ELEMENT)) { + List constraints = new ArrayList(); + int type = def.getRequiredType(); + while (walker.iterateElements(Constants.VALUECONSTRAINT_ELEMENT)) { + String constraint = walker.getContent(); + try { + constraints.add(ValueConstraint.create( + type, constraint.trim(), resolver)); + } catch (InvalidConstraintException e) { + throw new InvalidNodeTypeDefException( + "Invalid value constraint " + constraint, e); + } + } + def.setValueConstraints(constraints.toArray( + new QValueConstraint[constraints.size()])); + walker.leaveElement(); + } + + // default values + if (walker.enterElement(Constants.DEFAULTVALUES_ELEMENT)) { + List values = new ArrayList(); + int type = def.getRequiredType(); + if (type == PropertyType.UNDEFINED) { + type = PropertyType.STRING; + } + while (walker.iterateElements(Constants.DEFAULTVALUE_ELEMENT)) { + String value = walker.getContent(); + try { + Value v = ValueHelper.convert(value, type, valueFactory); + values.add((InternalValue) ValueFormat.getQValue(v, resolver, qValueFactory)); + } catch (RepositoryException e) { + throw new InvalidNodeTypeDefException( + "Unable to create default value: " + value, e); + } + } + def.setDefaultValues(values.toArray(new InternalValue[values.size()])); + walker.leaveElement(); + } + + return def; + } + + /** + * Returns the child node definition specified by the current element. + * + * @return child node definition + * @throws NameException if the definition contains an illegal name + * @throws NamespaceException if a namespace is not defined + */ + private QNodeDefinitionBuilder getChildNodeDef() throws NameException, NamespaceException { + QNodeDefinitionBuilder def = new QNodeDefinitionBuilder(); + String name = walker.getAttribute(Constants.NAME_ATTRIBUTE); + if (name.equals("*")) { + def.setName(NameConstants.ANY_NAME); + } else { + def.setName(resolver.getQName(name)); + } + + // simple attributes + def.setAutoCreated(Boolean.valueOf( + walker.getAttribute(Constants.AUTOCREATED_ATTRIBUTE))); + def.setMandatory(Boolean.valueOf( + walker.getAttribute(Constants.MANDATORY_ATTRIBUTE))); + def.setProtected(Boolean.valueOf( + walker.getAttribute(Constants.PROTECTED_ATTRIBUTE))); + def.setOnParentVersion(OnParentVersionAction.valueFromName( + walker.getAttribute(Constants.ONPARENTVERSION_ATTRIBUTE))); + def.setAllowsSameNameSiblings(Boolean.valueOf( + walker.getAttribute(Constants.SAMENAMESIBLINGS_ATTRIBUTE))); + + // default primary type + String type = + walker.getAttribute(Constants.DEFAULTPRIMARYTYPE_ATTRIBUTE); + if (type != null && type.length() > 0) { + def.setDefaultPrimaryType(resolver.getQName(type)); + } + + // required primary types + if (walker.enterElement(Constants.REQUIREDPRIMARYTYPES_ELEMENT)) { + List types = new ArrayList(); + while (walker.iterateElements(Constants.REQUIREDPRIMARYTYPE_ELEMENT)) { + types.add(resolver.getQName(walker.getContent())); + } + def.setRequiredPrimaryTypes(types.toArray(new Name[types.size()])); + walker.leaveElement(); + } else { + /* Default to nt:base? + throw new InvalidNodeTypeDefException( + "Required primary type(s) not defined for child node " + + def.getName() + " of node type " + + def.getDeclaringNodeType()); + */ + } + + return def; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/NodeTypeWriter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/NodeTypeWriter.java new file mode 100644 index 00000000000..631731fbe4d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/nodetype/xml/NodeTypeWriter.java @@ -0,0 +1,338 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype.xml; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.version.OnParentVersionAction; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.core.util.DOMBuilder; +import org.apache.jackrabbit.core.value.InternalValueFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; + +/** + * Node type definition writer. This class is used to write the + * persistent node type definition files used by Jackrabbit. + */ +public final class NodeTypeWriter { + + /** + * Writes a node type definition file. The file contents are written + * to the given output stream and will contain the given node type + * definitions. The given namespace registry is used for namespace + * mappings. + * + * @param xml XML output stream + * @param registry namespace registry + * @param types node types + * @throws IOException if the node type definitions cannot + * be written + * @throws RepositoryException on repository errors + */ + public static void write( + OutputStream xml, QNodeTypeDefinition[] types, NamespaceRegistry registry) + throws IOException, RepositoryException { + try { + NodeTypeWriter writer = new NodeTypeWriter(registry); + for (QNodeTypeDefinition type : types) { + writer.addNodeTypeDef(type); + } + writer.write(xml); + } catch (ParserConfigurationException e) { + IOException e2 = new IOException(e.getMessage()); + e2.initCause(e); + throw e2; + } catch (NamespaceException e) { + throw new RepositoryException( + "Invalid namespace reference in a node type definition", e); + } + } + + /** The node type document builder. */ + private final DOMBuilder builder; + + /** The namespace resolver. */ + private final NamePathResolver resolver; + + private final ValueFactoryQImpl factory; + + /** + * Creates a node type definition file writer. The given namespace + * registry is used for the XML namespace bindings. + * + * @param registry namespace registry + * @throws ParserConfigurationException if the node type definition + * document cannot be created + * @throws RepositoryException if the namespace mappings cannot + * be retrieved from the registry + */ + private NodeTypeWriter(NamespaceRegistry registry) + throws ParserConfigurationException, RepositoryException { + builder = new DOMBuilder(Constants.NODETYPES_ELEMENT); + + String[] prefixes = registry.getPrefixes(); + for (String prefix : prefixes) { + if (!"".equals(prefix)) { + String uri = registry.getURI(prefix); + builder.setAttribute("xmlns:" + prefix, uri); + } + } + + NamespaceResolver nsResolver = new AdditionalNamespaceResolver(registry); + resolver = new DefaultNamePathResolver(nsResolver); + factory = new ValueFactoryQImpl(InternalValueFactory.getInstance(), resolver); + } + + /** + * Builds a node type definition element under the current element. + * + * @param def node type definition + * @throws RepositoryException if the default property values + * cannot be serialized + * @throws NamespaceException if the node type definition contains + * invalid namespace references + */ + private void addNodeTypeDef(QNodeTypeDefinition def) + throws NamespaceException, RepositoryException { + builder.startElement(Constants.NODETYPE_ELEMENT); + + // simple attributes + builder.setAttribute( + Constants.NAME_ATTRIBUTE, resolver.getJCRName(def.getName())); + builder.setAttribute( + Constants.ISMIXIN_ATTRIBUTE, def.isMixin()); + builder.setAttribute( + Constants.ISQUERYABLE_ATTRIBUTE, def.isQueryable()); + builder.setAttribute( + Constants.ISABSTRACT_ATTRIBUTE, def.isAbstract()); + builder.setAttribute( + Constants.HASORDERABLECHILDNODES_ATTRIBUTE, + def.hasOrderableChildNodes()); + + // primary item name + Name item = def.getPrimaryItemName(); + if (item != null) { + builder.setAttribute( + Constants.PRIMARYITEMNAME_ATTRIBUTE, + resolver.getJCRName(item)); + } else { + builder.setAttribute(Constants.PRIMARYITEMNAME_ATTRIBUTE, ""); + } + + // supertype declarations + Name[] supertypes = def.getSupertypes(); + if (supertypes.length > 0) { + builder.startElement(Constants.SUPERTYPES_ELEMENT); + for (Name supertype : supertypes) { + builder.addContentElement( + Constants.SUPERTYPE_ELEMENT, + resolver.getJCRName(supertype)); + } + builder.endElement(); + } + + // property definitions + QPropertyDefinition[] properties = def.getPropertyDefs(); + for (QPropertyDefinition property : properties) { + addPropDef(property); + } + + // child node definitions + QNodeDefinition[] nodes = def.getChildNodeDefs(); + for (QNodeDefinition node : nodes) { + addChildNodeDef(node); + } + + builder.endElement(); + } + + /** + * Builds a property definition element under the current element. + * + * @param def property definition + * @throws RepositoryException if the default values cannot + * be serialized + * @throws NamespaceException if the property definition contains + * invalid namespace references + */ + private void addPropDef(QPropertyDefinition def) + throws NamespaceException, RepositoryException { + builder.startElement(Constants.PROPERTYDEFINITION_ELEMENT); + + // simple attributes + builder.setAttribute( + Constants.NAME_ATTRIBUTE, resolver.getJCRName(def.getName())); + builder.setAttribute( + Constants.AUTOCREATED_ATTRIBUTE, def.isAutoCreated()); + builder.setAttribute( + Constants.MANDATORY_ATTRIBUTE, def.isMandatory()); + builder.setAttribute( + Constants.PROTECTED_ATTRIBUTE, def.isProtected()); + builder.setAttribute( + Constants.ONPARENTVERSION_ATTRIBUTE, + OnParentVersionAction.nameFromValue(def.getOnParentVersion())); + builder.setAttribute( + Constants.MULTIPLE_ATTRIBUTE, def.isMultiple()); + builder.setAttribute( + Constants.ISFULLTEXTSEARCHABLE_ATTRIBUTE, def.isFullTextSearchable()); + builder.setAttribute( + Constants.ISQUERYORDERABLE_ATTRIBUTE, def.isQueryOrderable()); + // TODO do properly... + String[] qops = def.getAvailableQueryOperators(); + if (qops != null && qops.length > 0) { + List ops = Arrays.asList(qops); + List defaultOps = Arrays.asList(Operator.getAllQueryOperators()); + if (!ops.containsAll(defaultOps)) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < qops.length; i++) { + if (i > 0) { + sb.append(' '); + } + if (qops[i].equals(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO)) { + sb.append(Constants.EQ_ENTITY); + } else if (qops[i].equals(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO)) { + sb.append(Constants.NE_ENTITY); + } else if (qops[i].equals(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN)) { + sb.append(Constants.GT_ENTITY); + } else if (qops[i].equals(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO)) { + sb.append(Constants.GE_ENTITY); + } else if (qops[i].equals(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN)) { + sb.append(Constants.LT_ENTITY); + } else if (qops[i].equals(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO)) { + sb.append(Constants.LE_ENTITY); + } else if (qops[i].equals(QueryObjectModelConstants.JCR_OPERATOR_LIKE)) { + sb.append(Constants.LIKE_ENTITY); + } + } + builder.setAttribute( + Constants.AVAILABLEQUERYOPERATORS_ATTRIBUTE, sb.toString()); + } + } + + builder.setAttribute( + Constants.REQUIREDTYPE_ATTRIBUTE, + PropertyType.nameFromValue(def.getRequiredType())); + + // value constraints + QValueConstraint[] constraints = def.getValueConstraints(); + if (constraints != null && constraints.length > 0) { + builder.startElement(Constants.VALUECONSTRAINTS_ELEMENT); + for (QValueConstraint constraint : constraints) { + ValueConstraint vc = ValueConstraint.create( + def.getRequiredType(), constraint.getString()); + builder.addContentElement( + Constants.VALUECONSTRAINT_ELEMENT, + vc.getDefinition(resolver)); + } + builder.endElement(); + } + + // default values + QValue[] defaults = def.getDefaultValues(); + if (defaults != null && defaults.length > 0) { + builder.startElement(Constants.DEFAULTVALUES_ELEMENT); + for (QValue v : defaults) { + builder.addContentElement( + Constants.DEFAULTVALUE_ELEMENT, + factory.createValue(v).getString()); + } + builder.endElement(); + } + + builder.endElement(); + } + + /** + * Builds a child node definition element under the current element. + * + * @param def child node definition + * @throws NamespaceException if the child node definition contains + * invalid namespace references + */ + private void addChildNodeDef(QNodeDefinition def) + throws NamespaceException { + builder.startElement(Constants.CHILDNODEDEFINITION_ELEMENT); + + // simple attributes + builder.setAttribute( + Constants.NAME_ATTRIBUTE, resolver.getJCRName(def.getName())); + builder.setAttribute( + Constants.AUTOCREATED_ATTRIBUTE, def.isAutoCreated()); + builder.setAttribute( + Constants.MANDATORY_ATTRIBUTE, def.isMandatory()); + builder.setAttribute( + Constants.PROTECTED_ATTRIBUTE, def.isProtected()); + builder.setAttribute( + Constants.ONPARENTVERSION_ATTRIBUTE, + OnParentVersionAction.nameFromValue(def.getOnParentVersion())); + builder.setAttribute( + Constants.SAMENAMESIBLINGS_ATTRIBUTE, def.allowsSameNameSiblings()); + + // default primary type + Name type = def.getDefaultPrimaryType(); + if (type != null) { + builder.setAttribute( + Constants.DEFAULTPRIMARYTYPE_ATTRIBUTE, + resolver.getJCRName(type)); + } else { + builder.setAttribute(Constants.DEFAULTPRIMARYTYPE_ATTRIBUTE, ""); + } + + // required primary types + Name[] requiredTypes = def.getRequiredPrimaryTypes(); + builder.startElement(Constants.REQUIREDPRIMARYTYPES_ELEMENT); + for (Name requiredType : requiredTypes) { + builder.addContentElement( + Constants.REQUIREDPRIMARYTYPE_ELEMENT, + resolver.getJCRName(requiredType)); + } + builder.endElement(); + + builder.endElement(); + } + + /** + * Writes the node type definition document to the given output stream. + * + * @param xml XML output stream + * @throws IOException if the node type document could not be written + */ + private void write(OutputStream xml) throws IOException { + builder.write(xml); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ChangeLogBasedHierarchyMgr.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ChangeLogBasedHierarchyMgr.java new file mode 100644 index 00000000000..317c003755f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ChangeLogBasedHierarchyMgr.java @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.ZombieHierarchyManager; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.CachingHierarchyManager; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import java.util.HashMap; +import java.util.Map; + +/** + * Implements a {@link HierarchyManager} that uses a {@link ChangeLog} for + * the 'transient' changes on an underlying {@link ItemStateManager}. + * {@link ItemState}s in attic are provided from the removed {@link ItemState}s + * in the {@link ChangeLog}. The modified and added {@link ItemState}s in + * the {@link ChangeLog} overlay the {@link ItemState}s in the + * {@link ItemStateManager}. + */ +class ChangeLogBasedHierarchyMgr extends CachingHierarchyManager { + + ZombieHierarchyManager zombieHierMgr; + + /** + * Creates a new ChangeLogBasedHierarchyMgr that overlays + * manager with changes and uses the deleted + * map of the changes as an attic ItemStateManager. + * @param rootNodeId the id of the root node. + * @param manager the item state manager. + * @param changes the changes that will be applied on the item state manager. + */ + ChangeLogBasedHierarchyMgr(NodeId rootNodeId, + ItemStateManager manager, + ChangeLog changes) { + super(rootNodeId, new ChangeLogItemStateManager(manager, changes)); + zombieHierMgr = new ZombieHierarchyManager( + this, provider, new AtticItemStateManager(changes)); + } + + /** + * Same as {@link #getPath(ItemId)}} except that the old path is + * returned in case of a moved/removed item. + * + * @param id the id of the node for which to retrieve the path. + * @return the path of the item. + * @throws ItemNotFoundException if an item state cannot be found. + * @throws RepositoryException if another error occurs. + */ + public Path getZombiePath(ItemId id) + throws ItemNotFoundException, RepositoryException { + return zombieHierMgr.getPath(id); + } + + /** + * Same as {@link #getName(NodeId, NodeId)} except that the old path + * is returned in case of moved/removed item. + * + * @param id the id of the node for which to retrieve the name. + * @param parentId the id of the parent node. + * @return the name of the node. + * @throws ItemNotFoundException if an item state cannot be found. + * @throws RepositoryException if another error occurs. + */ + public Name getZombieName(NodeId id, NodeId parentId) + throws ItemNotFoundException, RepositoryException { + return zombieHierMgr.getName(id, parentId); + } + + /** + * Implements an ItemStateManager that is overlayed by a ChangeLog. + */ + private static class ChangeLogItemStateManager implements ItemStateManager { + + /** + * The changes that will be applied to the {@link #base}. + */ + private final ChangeLog changes; + + /** + * The underlying {@link ItemStateManager}. + */ + private final ItemStateManager base; + + /** + * Creates a new ChangeLogItemStateManager that overlays + * the {@link ItemState}s in base with the one found in + * changes. + * @param base the underlying {@link ItemStateManager}. + * @param changes + */ + private ChangeLogItemStateManager(ItemStateManager base, ChangeLog changes) { + this.base = base; + this.changes = changes; + } + + /** + * Returns the {@link ItemState} with the id. This + * ItemState manager first looks up the ChangeLog and then + * tries to find the ItemState in the base {@link ItemStateManager}. + * @param id the id of the {@link ItemState}. + * @return the {@link ItemState} with id. + * @throws NoSuchItemStateException if there is no ItemState with + * id. + * @throws ItemStateException if any other error occurs. + */ + public ItemState getItemState(ItemId id) throws NoSuchItemStateException, ItemStateException { + // check ChangeLog first + try { + ItemState state = changes.get(id); + if (state != null) { + return state; + } + } catch (NoSuchItemStateException e) { + // item has been deleted, but we still return it by asking base + } + return base.getItemState(id); + } + + /** + * Returns true if there exists a {@link ItemState} either + * in the {@link ChangeLog} or the base {@link ItemStateManager}; + * otherwise false is returned. + * @param id the id of the {@link ItemState}. + * @return true if there exists a {@link ItemState} either + * in the {@link ChangeLog} or the base {@link ItemStateManager}; + * otherwise false. + */ + public boolean hasItemState(ItemId id) { + // check ChangeLog first + try { + ItemState state = changes.get(id); + if (state != null) { + return true; + } + } catch (NoSuchItemStateException e) { + // item has been deleted, but we still might return true by asking base + } + return base.hasItemState(id); + } + + /** + * Always throws a {@link UnsupportedOperationException}. + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + return false; + } + } + + /** + * Returns the removed {@link ItemState}s from the ChangeLog. + */ + private static class AtticItemStateManager implements ItemStateManager { + + /** + * Map of deleted {@link ItemState}s indexed by {@link ItemId}. + */ + private final Map deleted = + new HashMap(); + + /** + * Creates a new AtticItemStateManager based on + * changes. + * @param changes deleted {@link ItemState} are retrieved from this + * ChangeLog. + */ + private AtticItemStateManager(ChangeLog changes) { + for (ItemState state : changes.deletedStates()) { + deleted.put(state.getId(), state); + } + } + + /** + * Returns an {@link ItemState} if it is found in the deleted map of the + * {@link ChangeLog}. + * @param id the id of the {@link ItemState}. + * @return the deleted {@link ItemState}. + * @throws NoSuchItemStateException if the {@link ItemState} cannot + * be found in the deleted map. + * @throws ItemStateException if any other error occurs. + */ + public ItemState getItemState(ItemId id) throws NoSuchItemStateException, ItemStateException { + ItemState state = (ItemState) deleted.get(id); + if (state != null) { + return state; + } else { + throw new NoSuchItemStateException("Item not in the attic: " + id); + } + } + + /** + * Returns true if an {@link ItemState} with id + * is found in the deleted map of the {@link ChangeLog}; false + * otherwise. + * @param id the id of the {@link ItemState}. + * @return true if an {@link ItemState} with id + * is found in the deleted map of the {@link ChangeLog}; false + * otherwise. + */ + public boolean hasItemState(ItemId id) { + return deleted.containsKey(id); + } + + /** + * Always throws a {@link UnsupportedOperationException}. + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + return false; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/DelegatingObservationDispatcher.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/DelegatingObservationDispatcher.java new file mode 100644 index 00000000000..e2566ed3a36 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/DelegatingObservationDispatcher.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.core.observation; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.spi.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.List; + +/** + * This Class implements an observation dispatcher, that delegates events to + * a set of underlying dispatchers. + */ +public class DelegatingObservationDispatcher extends EventDispatcher { + + /** + * Logger instance. + */ + private static Logger log = LoggerFactory.getLogger(DelegatingObservationDispatcher.class); + + /** + * the set of dispatchers + */ + private final HashSet dispatchers = new HashSet(); + + /** + * Adds a new observation dispatcher to the set of dispatchers + * + * @param dispatcher observation dispatcher + */ + public void addDispatcher(ObservationDispatcher dispatcher) { + synchronized (dispatchers) { + dispatchers.add(dispatcher); + } + } + + /** + * Removes a observation dispatcher from the set of dispatchers + * + * @param dispatcher observation dispatcher + */ + public void removeDispatcher(ObservationDispatcher dispatcher) { + synchronized (dispatchers) { + dispatchers.remove(dispatcher); + } + } + + /** + * Creates an EventStateCollection tied to the session + * given as argument. + * + * @param session event source + * @param pathPrefix event path prefix + * @return new EventStateCollection instance + */ + public EventStateCollection createEventStateCollection( + SessionImpl session, Path pathPrefix) { + return new EventStateCollection(this, session, pathPrefix); + } + + //------------------------------------------------------< EventDispatcher > + + /** + * {@inheritDoc} + */ + void prepareEvents(EventStateCollection events) { + // events will get prepared on dispatch + } + + /** + * {@inheritDoc} + */ + void prepareDeleted(EventStateCollection events, ChangeLog changes) { + // events will get prepared on dispatch + } + + /** + * {@inheritDoc} + */ + void dispatchEvents(EventStateCollection events) { + dispatch(events.getEvents(), events.getSession(), + events.getPathPrefix(), events.getUserData()); + } + + /** + * Dispatchers a list of events to all registered dispatchers. A new + * {@link EventStateCollection} is created for every dispatcher, fille with + * the given event list and then dispatched. + * + * @param eventList list of events + * @param session current session + * @param pathPrefix event path prefix + * @param userData the user data + */ + public void dispatch(List eventList, SessionImpl session, + Path pathPrefix, String userData) { + ObservationDispatcher[] disp; + synchronized (dispatchers) { + disp = (ObservationDispatcher[]) dispatchers.toArray( + new ObservationDispatcher[dispatchers.size()]); + } + for (int i = 0; i < disp.length; i++) { + EventStateCollection events = + new EventStateCollection(disp[i], session, pathPrefix); + events.setUserData(userData); + try { + events.addAll(eventList); + events.prepare(); + events.dispatch(); + } catch (Exception e) { + log.error("Error while dispatching events.", e); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/DispatchAction.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/DispatchAction.java new file mode 100644 index 00000000000..d44faba78dc --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/DispatchAction.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.core.observation; + +import java.util.Collection; + +/** + * The DispatchAction class is a simple struct that defines what + * EventStates should be dispatched to which + * EventConsumers. + */ +class DispatchAction { + + /** + * The collection of EventStates + */ + private final EventStateCollection eventStates; + + /** + * EventStates are dispatched to these + * EventConsumers. + */ + private final Collection eventConsumers; + + /** + * Creates a new DispatchAction struct with + * eventStates and eventConsumers. + */ + DispatchAction(EventStateCollection eventStates, Collection eventConsumers) { + this.eventStates = eventStates; + this.eventConsumers = eventConsumers; + } + + /** + * Returns a collection of {@link EventState}s to dispatch. + * + * @return a collection of {@link EventState}s to dispatch. + */ + EventStateCollection getEventStates() { + return eventStates; + } + + /** + * Returns a Collection of {@link EventConsumer}s where + * the events should be dispatched to. + * + * @return a Collection of {@link EventConsumer}s. + */ + Collection getEventConsumers() { + return eventConsumers; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventConsumer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventConsumer.java new file mode 100644 index 00000000000..e80c08a328e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventConsumer.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.core.observation; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The EventConsumer class combines the {@link + * javax.jcr.observation.EventListener} with the implementation of specified + * filter for the listener: {@link EventFilter}. + *

    + * Collections of {@link EventState} objects will be dispatched to {@link + * #consumeEvents}. + */ +class EventConsumer { + + /** + * The default Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(EventConsumer.class); + + private final PathFactory pathFactory = PathFactoryImpl.getInstance(); + + /** + * The Session associated with this EventConsumer. + */ + private final SessionImpl session; + + /** + * The listener part of this EventConsumer. + */ + private final EventListener listener; + + /** + * The EventFilter for this EventConsumer. + */ + private final EventFilter filter; + + /** + * A map of Set objects that hold references to + * ItemIds of denied ItemStates. The map uses the + * EventStateCollection as the key to reference a deny Set. + */ + private final Map> accessDenied = Collections.synchronizedMap(new WeakHashMap>()); + + /** + * cached hash code value + */ + private int hashCode; + + /** + * An EventConsumer consists of a Session, the + * attached EventListener and an EventFilter. + * + * @param session the Session that created this + * EventConsumer. + * @param listener the actual EventListener to call back. + * @param filter only pass an Event to the listener if the + * EventFilter allows the Event. + * @throws NullPointerException if session, listener + * or filter isnull. + */ + EventConsumer(SessionImpl session, EventListener listener, EventFilter filter) + throws NullPointerException { + if (session == null) { + throw new NullPointerException("session"); + } + if (listener == null) { + throw new NullPointerException("listener"); + } + if (filter == null) { + throw new NullPointerException("filter"); + } + + this.session = session; + this.listener = listener; + this.filter = filter; + } + + /** + * Returns the Session that is associated + * with this EventConsumer. + * + * @return the Session of this EventConsumer. + */ + Session getSession() { + return session; + } + + /** + * Returns the EventListener that is associated with this + * EventConsumer. + * + * @return the EventListener of this EventConsumer. + */ + EventListener getEventListener() { + return listener; + } + + /** + * Checks for what {@link EventState}s this EventConsumer has + * enough access rights to see the event. + * + * @param events the collection of {@link EventState}s. + */ + void prepareEvents(EventStateCollection events) { + Iterator it = events.iterator(); + Set denied = null; + while (it.hasNext()) { + EventState state = it.next(); + if (state.getType() == Event.NODE_REMOVED + || state.getType() == Event.PROPERTY_REMOVED) { + + if (session.equals(state.getSession())) { + // if we created the event, we can be sure that + // we have enough access rights to see the event + continue; + } + + // check read permission + ItemId targetId = state.getTargetId(); + boolean granted = false; + try { + granted = canRead(state); + } catch (RepositoryException e) { + log.warn("Unable to check access rights for item: " + targetId); + } + if (!granted) { + if (denied == null) { + denied = new HashSet(); + } + denied.add(targetId); + } + } + } + if (denied != null) { + accessDenied.put(events, denied); + } + } + + /** + * Checks for which deleted ItemStates this + * EventConsumer has enough access rights to see the event. + * + * @param events the collection of {@link EventState}s. + * @param deletedItems Iterator of deleted ItemStates. + */ + void prepareDeleted(EventStateCollection events, Iterable deletedItems) { + Set denied = null; + Set deletedIds = new HashSet(); + for (ItemState state : deletedItems) { + deletedIds.add(state.getId()); + } + + for (Iterator it = events.iterator(); it.hasNext();) { + EventState evState = it.next(); + ItemId targetId = evState.getTargetId(); + if (deletedIds.contains(targetId)) { + // check read permission + boolean granted = false; + try { + granted = canRead(evState); + } catch (RepositoryException e) { + log.warn("Unable to check access rights for item: " + targetId); + } + if (!granted) { + if (denied == null) { + denied = new HashSet(); + } + denied.add(targetId); + } + } + } + if (denied != null) { + accessDenied.put(events, denied); + } + } + + /** + * Dispatches the events to the EventListener. + * + * @param events a collection of {@link EventState}s + * to dispatch. + */ + void consumeEvents(EventStateCollection events) throws RepositoryException { + // Set of ItemIds of denied ItemStates + Set denied = accessDenied.remove(events); + if (denied == null) { + denied = new HashSet(); + } + + // check permissions + for (Iterator it = events.iterator(); it.hasNext() && session.isLive();) { + EventState state = it.next(); + if (state.getType() == Event.NODE_ADDED + || state.getType() == Event.PROPERTY_ADDED + || state.getType() == Event.PROPERTY_CHANGED) { + ItemId targetId = state.getTargetId(); + if (!canRead(state)) { + denied.add(targetId); + } + } + } + // only deliver if session is still live + if (!session.isLive()) { + return; + } + // check if filtered iterator has at least one event + EventIterator it = new FilteredEventIterator( + session, events.iterator(), events.getTimestamp(), + events.getUserData(), filter, denied, false); + if (it.hasNext()) { + long time = System.currentTimeMillis(); + listener.onEvent(it); + time = System.currentTimeMillis() - time; + if (log.isDebugEnabled()) { + log.debug("listener {} processed events in {} ms.", + listener.getClass().getName(), time); + } + } else { + // otherwise skip this listener + } + } + + /** + * Returns true if this EventConsumer is equal to + * some other object, false otherwise. + *

    + * Two EventConsumers are considered equal if they refer to the + * same Session and the EventListeners they + * reference are equal. Note that the EventFilter is ignored in + * this check. + * + * @param obj the reference object with which to compare. + * @return true if this EventConsumer is equal the + * other EventConsumer. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof EventConsumer) { + EventConsumer other = (EventConsumer) obj; + return session.equals(other.session) + && listener.equals(other.listener); + } + return false; + } + + /** + * Returns the hash code for this EventConsumer. + * + * @return the hash code for this EventConsumer. + */ + public int hashCode() { + if (hashCode == 0) { + hashCode = session.hashCode() ^ listener.hashCode(); + } + return hashCode; + } + + /** + * Returns true if the item corresponding to the specified + * eventState can be read the the current session. + * + * @param eventState + * @return + * @throws RepositoryException + */ + private boolean canRead(EventState eventState) throws RepositoryException { + Path targetPath = pathFactory.create(eventState.getParentPath(), eventState.getChildRelPath().getName(), eventState.getChildRelPath().getNormalizedIndex(), true); + return session.getAccessManager().isGranted(targetPath, Permission.READ); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventDispatcher.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventDispatcher.java new file mode 100755 index 00000000000..2e989f166f4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventDispatcher.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.core.observation; + +import org.apache.jackrabbit.core.state.ChangeLog; + +/** + * Defines an object that prepares and dispatches events. Made into an abstract + * class rather than an interface in order not to exhibit internal methods + * that should not be visible to everybody. + */ +abstract class EventDispatcher { + + /** + * Gives this dispatcher the opportunity to prepare the events for + * dispatching. + * + * @param events the {@link EventState}s to prepare. + */ + abstract void prepareEvents(EventStateCollection events); + + /** + * Prepares changes that involve deleted item states. + * + * @param events the event state collection. + * @param changes the changes. + */ + abstract void prepareDeleted(EventStateCollection events, ChangeLog changes); + + /** + * Dispatches the {@link EventStateCollection events}. + * + * @param events the {@link EventState}s to dispatch. + */ + abstract void dispatchEvents(EventStateCollection events); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventFilter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventFilter.java new file mode 100644 index 00000000000..b468fd2b028 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventFilter.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.spi.Path; + +/** + * The EventFilter class implements the filter logic based + * on the session's access rights and the specified filter rules. + */ +public class EventFilter { + + static final EventFilter BLOCK_ALL = new BlockAllFilter(); + + /** + * The session this EventFilter belongs to. + */ + private final SessionImpl session; + + /** + * This EventFilter should only allow events with the + * specified types. + */ + private final long eventTypes; + + /** + * Only allow Items with the specified paths + */ + private final List paths; + + /** + * If isDeep is true also Items under absPath + * are allowed. + */ + private final boolean isDeep; + + /** + * Only allow Nodes with the specified uuids. + */ + private final NodeId[] ids; + + /** + * Only allow Nodes with the specified {@link javax.jcr.nodetype.NodeType}s. + */ + private final NodeTypeImpl[] nodeTypes; + + /** + * If noLocal is true this filter will block events from + * the session that registered this filter. + */ + private final boolean noLocal; + + /** + * If noExternal is true this filter will block events from + * other cluster nodes. + */ + private final boolean noExternal; + + /** + * If noInternal is true this filter will block events from + * this cluster nodes. + */ + private final boolean noInternal; + + /** + * Creates a new EventFilter instance. + * + * @param session the Session that registered the {@link + * javax.jcr.observation.EventListener}. + * @param eventTypes only allow specified {@link javax.jcr.observation.Event} types. + * @param paths only allow {@link javax.jcr.Item} with a path in + * paths. + * @param isDeep if true also allow events for {@link + * javax.jcr.Item}s below absPath. + * @param ids only allow events for {@link javax.jcr.Node}s with + * specified NodeIDs. If null is passed no + * restriction regarding NodeIds is applied. + * @param nodeTypes only allow events for specified {@link + * javax.jcr.nodetype.NodeType}s. If null no + * node type restriction is applied. + * @param noLocal if true no events are allowed that were + * created from changes related to the Session + * that registered the {@link javax.jcr.observation.EventListener}. + * @param noExternal if true no events are allowed that were + * created from changes on an external cluster node. + * @param noInternal if true no events are allowed that were + * created from changes on the local cluster node. + */ + EventFilter(SessionImpl session, + long eventTypes, + List paths, + boolean isDeep, + NodeId[] ids, + NodeTypeImpl[] nodeTypes, + boolean noLocal, + boolean noExternal, + boolean noInternal) { + this.session = session; + this.eventTypes = eventTypes; + this.paths = paths; + this.isDeep = isDeep; + this.ids = ids; + this.noLocal = noLocal; + this.noExternal = noExternal; + this.noInternal = noInternal; + this.nodeTypes = nodeTypes; + } + + /** + * Returns true if this EventFilter does not allow + * the specified EventState; false otherwise. + * + * @param eventState the EventState in question. + * @return true if this EventFilter blocks the + * EventState. + * @throws RepositoryException if an error occurs while checking. + */ + boolean blocks(EventState eventState) throws RepositoryException { + // first do cheap checks + + // check event type + long type = eventState.getType(); + if ((eventTypes & type) == 0) { + return true; + } + + // check for session local changes + if (noLocal && session.equals(eventState.getSession())) { + // listener does not wish to get local events + return true; + } + + if (noExternal && eventState.isExternal()) { + return true; + } + + if (noInternal && !eventState.isExternal()) { + return true; + } + + // UUIDs, types, and paths do not need to match for persist + if (eventState.getType() == Event.PERSIST) { + return false; + } + + // check UUIDs + NodeId parentId = eventState.getParentId(); + if (ids != null) { + boolean match = false; + for (int i = 0; i < ids.length && !match; i++) { + match |= parentId.equals(ids[i]); + } + if (!match) { + return true; + } + } + + // check node types + if (nodeTypes != null) { + Set eventTypes = eventState.getNodeTypes(session.getNodeTypeManager()); + boolean match = false; + for (int i = 0; i < nodeTypes.length && !match; i++) { + for (NodeType eventType : eventTypes) { + NodeTypeImpl nodeType = (NodeTypeImpl) eventType; + match |= nodeType.getQName().equals(nodeTypes[i].getQName()) + || nodeType.isDerivedFrom(nodeTypes[i].getQName()); + } + } + if (!match) { + return true; + } + } + + // finally check paths + Path eventPath = eventState.getParentPath(); + boolean match = false; + for (Path path : paths) { + if (eventPath.equals(path) || isDeep && eventPath.isDescendantOf(path)) { + match = true; + break; + } + } + + return !match; + } + + /** + * This class implements an EventFilter that blocks + * all {@link EventState}s. + */ + private static final class BlockAllFilter extends EventFilter { + + /** + * Creates a new BlockAllFilter. + */ + BlockAllFilter() { + super(null, 0, Collections.emptyList(), true, null, null, true, true, true); + } + + /** + * Always return true. + * + * @return always true. + */ + @Override + boolean blocks(EventState eventState) { + return true; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventImpl.java new file mode 100644 index 00000000000..9b71875c4ed --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventImpl.java @@ -0,0 +1,345 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import java.util.Map; +import java.util.HashMap; +import java.util.Set; + +import org.apache.jackrabbit.api.observation.JackrabbitEvent; +import javax.jcr.observation.Event; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.commons.AdditionalEventInfo; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; + +/** + * Implementation of the {@link javax.jcr.observation.Event} and + * the {@link JackrabbitEvent} interface. + */ +public final class EventImpl implements JackrabbitEvent, AdditionalEventInfo, Event { + + /** + * Logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(EventImpl.class); + + /** + * The session of the {@link javax.jcr.observation.EventListener} this + * event will be delivered to. + */ + private final SessionImpl session; + + /** + * The shared {@link EventState} object. + */ + private final EventState eventState; + + /** + * The timestamp of this event. + */ + private final long timestamp; + + /** + * The user data associated with this event. + */ + private final String userData; + + /** + * Cached String value of this Event instance. + */ + private String stringValue; + + /** + * Creates a new {@link javax.jcr.observation.Event} instance based on an + * {@link EventState eventState}. + * + * @param session the session of the registered EventListener + * where this Event will be delivered to. + * @param eventState the underlying EventState. + * @param timestamp the time when the change occurred that caused this event. + * @param userData the user data associated with this event. + */ + EventImpl(SessionImpl session, EventState eventState, + long timestamp, String userData) { + this.session = session; + this.eventState = eventState; + this.timestamp = timestamp; + this.userData = userData; + } + + //---------------------------------------------------------------< Event > + + /** + * {@inheritDoc} + */ + public int getType() { + return eventState.getType(); + } + + /** + * {@inheritDoc} + */ + public String getPath() throws RepositoryException { + Path p = getQPath(); + return p != null ? session.getJCRPath(p) : null; + } + + /** + * {@inheritDoc} + */ + public String getUserID() { + return eventState.getUserId(); + } + + /** + * {@inheritDoc} + */ + public long getDate() { + return timestamp; + } + + /** + * {@inheritDoc} + */ + public String getUserData() { + return userData; + } + + /** + * {@inheritDoc} + */ + public String getIdentifier() throws RepositoryException { + if (eventState.getType() == Event.PERSIST) { + return null; + } + else { + NodeId id = eventState.getChildId(); + + if (id != null) { + return id.toString(); + } + else { + // property event + return eventState.getParentId().toString(); + } + } + } + + /** + * {@inheritDoc} + */ + public Map getInfo() throws RepositoryException { + Map info = new HashMap(); + for (Map.Entry entry : eventState.getInfo().entrySet()) { + InternalValue value = entry.getValue(); + String strValue = null; + if (value != null) { + strValue = ValueFormat.getJCRString(value, session); + } + info.put(entry.getKey(), strValue); + } + return info; + } + + //-----------------------------------------------------------< EventImpl > + + /** + * Returns the Path of this event. + * + * @return path or null when no path is associated with the event + * @throws RepositoryException if the path can't be constructed + */ + public Path getQPath() throws RepositoryException { + try { + Path parent = eventState.getParentPath(); + Path child = eventState.getChildRelPath(); + + if (parent == null || child == null) { + // an event without associated path information + return null; + } + else { + int index = child.getIndex(); + if (index > 0) { + return PathFactoryImpl.getInstance().create(parent, child.getName(), index, false); + } else { + return PathFactoryImpl.getInstance().create(parent, child.getName(), false); + } + } + } catch (MalformedPathException e) { + String msg = "internal error: malformed path for event"; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + /** + * Returns the uuid of the parent node. + * + * @return the uuid of the parent node. + */ + public NodeId getParentId() { + return eventState.getParentId(); + } + + /** + * Returns the id of a child node operation. + * If this Event was generated for a property + * operation this method returns null. + * + * @return the id of a child node operation. + */ + public NodeId getChildId() { + return eventState.getChildId(); + } + + /** + * Returns a flag indicating whether the child node of this event is a + * shareable node. Only applies to node added/removed events. + * + * @return true for a shareable child node, false + * otherwise. + */ + public boolean isShareableChildNode() { + return eventState.isShareableNode(); + } + + /** + * Return a flag indicating whether this is an externally generated event. + * + * @return true if this is an external event; + * false otherwise + * @see JackrabbitEvent#isExternal() + */ + public boolean isExternal() { + return eventState.isExternal(); + } + + //---------------------------------------------------------------< AdditionalEventInfo > + + /** + * @return the primary node type of the node associated with the event + * @see AdditionalEventInfo#getPrimaryNodeTypeName() + */ + public Name getPrimaryNodeTypeName() { + return eventState.getNodeType(); + } + + /** + * @return the mixin node types of the node associated with the event + * @see AdditionalEventInfo#getMixinTypeNames() + */ + public Set getMixinTypeNames() { + return eventState.getMixinNames(); + } + + /** + * @return the specified session attribute + */ + public Object getSessionAttribute(String name) { + return eventState.getSession().getAttribute(name); + } + + /** + * Returns a String representation of this Event. + * + * @return a String representation of this Event. + */ + public String toString() { + if (stringValue == null) { + StringBuilder sb = new StringBuilder(); + sb.append("Event: Path: "); + try { + sb.append(getPath()); + } catch (RepositoryException e) { + log.error("Exception retrieving path: " + e); + sb.append("[Error retrieving path]"); + } + sb.append(", ").append(EventState.valueOf(getType())).append(": "); + sb.append(", UserId: ").append(getUserID()); + sb.append(", Timestamp: ").append(timestamp); + sb.append(", UserData: ").append(userData); + sb.append(", Info: ").append(eventState.getInfo()); + stringValue = sb.toString(); + } + return stringValue; + } + + /** + * @see Object#hashCode() + */ + public int hashCode() { + int h = eventState.hashCode() ^ new Long(timestamp).hashCode() ^ session.hashCode(); + if (userData != null) { + h = h ^ userData.hashCode(); + } + return h; + } + + /** + * Returns true if this Event is equal to another + * object. + *

    + * Two Event instances are equal if their respective + * EventState instances are equal and both Event + * instances are intended for the same Session that registerd + * the EventListener. + * + * @param obj the reference object with which to compare. + * @return true if this Event is equal to another + * object. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof EventImpl) { + EventImpl other = (EventImpl) obj; + return this.eventState.equals(other.eventState) + && this.session.equals(other.session) + && this.timestamp == other.timestamp + && equals(this.userData, other.userData); + } + return false; + } + + /** + * Returns true if the objects are equal or both are + * null; otherwise returns false. + * + * @param o1 an object. + * @param o2 another object. + * @return true if equal; false otherwise. + */ + private static boolean equals(Object o1, Object o2) { + if (o1 == null) { + return o2 == null; + } else { + return o1.equals(o2); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventJournalImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventJournalImpl.java new file mode 100644 index 00000000000..32d8185b467 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventJournalImpl.java @@ -0,0 +1,421 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import java.util.List; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.Date; +import java.util.Collections; +import java.text.DateFormat; + +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; + +import javax.jcr.observation.EventJournal; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.cluster.PrivilegeRecord; +import org.apache.jackrabbit.core.journal.Journal; +import org.apache.jackrabbit.core.journal.RecordIterator; +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; +import org.apache.jackrabbit.core.cluster.ClusterRecordDeserializer; +import org.apache.jackrabbit.core.cluster.ClusterRecord; +import org.apache.jackrabbit.core.cluster.ClusterRecordProcessor; +import org.apache.jackrabbit.core.cluster.ChangeLogRecord; +import org.apache.jackrabbit.core.cluster.LockRecord; +import org.apache.jackrabbit.core.cluster.NamespaceRecord; +import org.apache.jackrabbit.core.cluster.NodeTypeRecord; +import org.apache.jackrabbit.core.cluster.WorkspaceRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * EventJournalImpl implements the JCR 2.0 {@link EventJournal}. + */ +public class EventJournalImpl implements EventJournal { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(EventJournalImpl.class); + + /** + * The minimum buffer size for events in {@link #eventBundleBuffer}. + */ + private static final int MIN_BUFFER_SIZE = 1024; + + /** + * Map of skip maps. Key=Journal, Value=SortedMap + *

    + * Each sorted map has the following structure: + * Key=Long (timestamp), Value=Long (revision) + */ + private static final Map> REVISION_SKIP_MAPS = new WeakHashMap>(); + + /** + * Last revision seen by this event journal. + */ + private Long lastRevision; + + /** + * The event filter. + */ + private final EventFilter filter; + + /** + * The journal of this repository. + */ + private final Journal journal; + + /** + * The producer id to filter journal records. + */ + private final String producerId; + + /** + * Target session. + */ + private final SessionImpl session; + + /** + * Buffer of {@link EventBundle}s. + */ + private final List eventBundleBuffer = new LinkedList(); + + /** + * The current position of this iterator. + */ + private long position; + + /** + * Creates a new event journal. + * + * @param filter for filtering the events read from the journal. + * @param journal the cluster journal. + * @param producerId the producer id of the cluster node. + * @param session target session + */ + public EventJournalImpl( + EventFilter filter, Journal journal, + String producerId, SessionImpl session) { + this.filter = filter; + this.journal = journal; + this.producerId = producerId; + this.session = session; + } + + //------------------------< EventJournal >--------------------------------- + + /** + * {@inheritDoc} + */ + public void skipTo(long date) { + long time = System.currentTimeMillis(); + + // get skip map for this journal + SortedMap skipMap = getSkipMap(); + synchronized (skipMap) { + SortedMap head = skipMap.headMap(new Long(date)); + if (!head.isEmpty()) { + eventBundleBuffer.clear(); + lastRevision = head.get(head.lastKey()); + } + } + + try { + while (hasNext()) { + EventBundle bundle = getCurrentBundle(); + if (bundle.timestamp <= date) { + eventBundleBuffer.remove(0); + } else { + break; + } + } + } finally { + time = System.currentTimeMillis() - time; + log.debug("Skipped event bundles in {} ms.", new Long(time)); + } + } + + //------------------------< EventIterator >--------------------------------- + + /** + * {@inheritDoc} + */ + public Event nextEvent() { + // calling hasNext() will also trigger refill if necessary! + if (!hasNext()) { + throw new NoSuchElementException(); + } + EventBundle bundle = getCurrentBundle(); + // above hasNext() call ensures that there is bundle with an event state + assert bundle != null && bundle.events.hasNext(); + + Event next = (Event) bundle.events.next(); + if (!bundle.events.hasNext()) { + // done with this bundle -> remove from buffer + eventBundleBuffer.remove(0); + } + position++; + return next; + } + + //------------------------< RangeIterator >--------------------------------- + + /** + * {@inheritDoc} + */ + public void skip(long skipNum) { + while (skipNum-- > 0) { + nextEvent(); + } + } + + /** + * @return always -1. + */ + public long getSize() { + return -1; + } + + /** + * {@inheritDoc} + */ + public long getPosition() { + // TODO: what happens to position when skipped + return position; + } + + //--------------------------< Iterator >------------------------------------ + + /** + * @throws UnsupportedOperationException always. + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + if (!eventBundleBuffer.isEmpty()) { + return true; + } + // try refill + refill(); + // check again + return !eventBundleBuffer.isEmpty(); + } + + /** + * {@inheritDoc} + */ + public Object next() { + return nextEvent(); + } + + //----------------------< ClusterRecordProcessor >-------------------------- + + /** + * Implements {@link ClusterRecordProcessor} and keeps track of the number + * of events read and the timestamp of the last record processed. + */ + private class RecordProcessor implements ClusterRecordProcessor { + + /** + * Number of events read so far. + */ + private int numEvents; + + /** + * The timestamp of the last record processed. + */ + private long lastTimestamp; + + /** + * @return the number of events read so far. + */ + private int getNumEvents() { + return numEvents; + } + + /** + * @return the timestamp of the last record processed. + */ + private long getLastTimestamp() { + return lastTimestamp; + } + + /** + * {@inheritDoc} + */ + public void process(ChangeLogRecord record) { + List events = record.getEvents(); + if (!events.isEmpty()) { + EventBundle bundle = new EventBundle( + events, record.getTimestamp(), record.getUserData()); + if (bundle.events.hasNext()) { + // only queue bundle if there is an event + eventBundleBuffer.add(bundle); + numEvents += events.size(); + lastTimestamp = record.getTimestamp(); + } + } + } + + public void process(LockRecord record) { + // ignore + } + + public void process(NamespaceRecord record) { + // ignore + } + + public void process(NodeTypeRecord record) { + // ignore + } + + public void process(PrivilegeRecord record) { + // ignore + } + + public void process(WorkspaceRecord record) { + // ignore + } + } + + //-------------------------------< internal >------------------------------- + + /** + * @return the current event bundle or null if there is none. + */ + private EventBundle getCurrentBundle() { + while (!eventBundleBuffer.isEmpty()) { + EventBundle bundle = eventBundleBuffer.get(0); + if (bundle.events.hasNext()) { + return bundle; + } else { + eventBundleBuffer.remove(0); + } + } + return null; + } + + /** + * Refills the {@link #eventBundleBuffer}. + */ + private void refill() { + assert eventBundleBuffer.isEmpty(); + try { + RecordProcessor processor = new RecordProcessor(); + ClusterRecordDeserializer deserializer = new ClusterRecordDeserializer(); + RecordIterator records; + if (lastRevision != null) { + log.debug("refilling event bundle buffer starting at revision {}", + lastRevision); + records = journal.getRecords(lastRevision.longValue()); + } else { + log.debug("refilling event bundle buffer starting at journal beginning"); + records = journal.getRecords(); + } + try { + while (processor.getNumEvents() < MIN_BUFFER_SIZE && records.hasNext()) { + Record record = records.nextRecord(); + if (record.getProducerId().equals(producerId)) { + ClusterRecord cr = deserializer.deserialize(record); + if (!session.getWorkspace().getName().equals(cr.getWorkspace())) { + continue; + } + cr.process(processor); + lastRevision = new Long(cr.getRevision()); + } + } + + if (processor.getNumEvents() >= MIN_BUFFER_SIZE) { + // remember in skip map + SortedMap skipMap = getSkipMap(); + Long timestamp = new Long(processor.getLastTimestamp()); + synchronized (skipMap) { + if (log.isDebugEnabled()) { + DateFormat df = DateFormat.getDateTimeInstance(); + log.debug("remember record in skip map: {} -> {}", + df.format(new Date(timestamp.longValue())), + lastRevision); + } + skipMap.put(timestamp, lastRevision); + } + } + } finally { + records.close(); + } + } catch (JournalException e) { + log.warn("Unable to read journal records", e); + } + } + + /** + * @return the revision skip map for this journal. + */ + private SortedMap getSkipMap() { + synchronized (REVISION_SKIP_MAPS) { + SortedMap map = REVISION_SKIP_MAPS.get(journal); + if (map == null) { + map = new TreeMap(); + REVISION_SKIP_MAPS.put(journal, map); + } + return map; + } + } + + /** + * Simple class to associate an {@link EventState} iterator with a timestamp. + */ + private final class EventBundle { + + /** + * An iterator of {@link Event}s. + */ + final EventIterator events; + + /** + * Timestamp when the events were created. + */ + final long timestamp; + + /** + * Creates a new event bundle. + * + * @param eventStates the {@link EventState}s that belong to this bundle. + * @param timestamp the timestamp when the events were created. + * @param userData the user data associated with this event. + */ + private EventBundle( + List eventStates, long timestamp, String userData) { + this.events = new FilteredEventIterator( + session, eventStates.iterator(), + timestamp, userData, filter, Collections.emptySet(), true); + this.timestamp = timestamp; + } + } +} diff --git a/src/java/org/apache/jackrabbit/core/observation/EventListenerIteratorImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventListenerIteratorImpl.java similarity index 83% rename from src/java/org/apache/jackrabbit/core/observation/EventListenerIteratorImpl.java rename to jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventListenerIteratorImpl.java index 7ad9f39c8fd..a02a36992bc 100644 --- a/src/java/org/apache/jackrabbit/core/observation/EventListenerIteratorImpl.java +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventListenerIteratorImpl.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -37,7 +37,7 @@ class EventListenerIteratorImpl implements EventListenerIterator { /** * Iterator over {@link EventConsumer} instances */ - private final Iterator consumers; + private final Iterator consumers; /** * The next EventListener that belongs to the session @@ -48,7 +48,7 @@ class EventListenerIteratorImpl implements EventListenerIterator { /** * Current position */ - private long pos = 0; + private long pos; /** * Creates a new EventListenerIteratorImpl. @@ -59,7 +59,8 @@ class EventListenerIteratorImpl implements EventListenerIterator { * @throws NullPointerException if ticket or consumer * is null. */ - EventListenerIteratorImpl(Session session, Collection sConsumers, Collection aConsumers) { + EventListenerIteratorImpl(Session session, Collection sConsumers, Collection aConsumers) + throws NullPointerException { if (session == null) { throw new NullPointerException("session"); } @@ -70,7 +71,7 @@ class EventListenerIteratorImpl implements EventListenerIterator { throw new NullPointerException("consumers"); } this.session = session; - Collection allConsumers = new ArrayList(sConsumers); + Collection allConsumers = new ArrayList(sConsumers); allConsumers.addAll(aConsumers); this.consumers = allConsumers.iterator(); fetchNext(); @@ -110,7 +111,7 @@ public long getSize() { /** * {@inheritDoc} */ - public long getPos() { + public long getPosition() { return pos; } diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventState.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventState.java new file mode 100644 index 00000000000..87d3ec6a06a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventState.java @@ -0,0 +1,962 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.CachingPathResolver; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingPathResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; + +import javax.jcr.observation.Event; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import javax.jcr.NamespaceException; +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; + +import java.util.List; +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Collections; +import java.util.Map; +import java.util.HashMap; + +/** + * The EventState class encapsulates the session + * independent state of an {@link javax.jcr.observation.Event}. + */ +public class EventState { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(EventState.class); + + /** + * The caching path resolver. + */ + private static CachingPathResolver cachingPathResolver; + + /** + * The key srcAbsPath in the info map. + */ + static final String SRC_ABS_PATH = "srcAbsPath"; + + /** + * The key destAbsPath in the info map. + */ + static final String DEST_ABS_PATH = "destAbsPath"; + + /** + * The key srcChildRelPath in the info map. + */ + static final String SRC_CHILD_REL_PATH = "srcChildRelPath"; + + /** + * The key destChildRelPath in the info map. + */ + static final String DEST_CHILD_REL_PATH = "destChildRelPath"; + + /** + * The {@link javax.jcr.observation.Event} of this event. + */ + private final int type; + + /** + * The Id of the parent node associated with this event. + */ + private final NodeId parentId; + + /** + * The path of the parent node associated with this event. + */ + private final Path parentPath; + + /** + * The UUID of a child node, in case this EventState is of type + * {@link javax.jcr.observation.Event#NODE_ADDED} or + * {@link javax.jcr.observation.Event#NODE_REMOVED}. + */ + private final NodeId childId; + + /** + * The relative path of the child item associated with this event. + * This is basically the name of the item with an optional index. + */ + private final Path childRelPath; + + /** + * The node type name of the parent node. + */ + private final Name nodeType; + + /** + * Set of mixin QNames assigned to the parent node. + */ + private final Set mixins; + + /** + * Set of node types. This Set consists of the primary node type and all + * mixin types assigned to the associated parent node of this event state. + *

    + * This Set is initialized when + * {@link #getNodeTypes(NodeTypeManagerImpl)} is called for the first time. + */ + private Set allTypes; + + /** + * The session that caused this event. + */ + private final Session session; + + /** + * Cached String representation of this EventState. + */ + private String stringValue; + + /** + * Cached hashCode value for this Event. + */ + private int hashCode; + + /** + * Flag indicating whether this is an external event, e.g. originating from + * another node in a clustered environment. + */ + private final boolean external; + + /** + * The info Map associated with this event. + */ + private Map info = Collections.emptyMap(); + + /** + * If set to true, indicates that the child node of a node + * added or removed event is a shareable node. + */ + private boolean shareableNode; + + /** + * Creates a new EventState instance. + * + * @param type the type of this event. + * @param parentId the id of the parent node associated with this event. + * @param parentPath the path of the parent node associated with this + * event. + * @param childId the id of the child node associated with this event. + * If the event type is one of: PROPERTY_ADDED, + * PROPERTY_CHANGED or PROPERTY_REMOVED + * this parameter must be null. + * @param childPath the relative path of the child item associated with + * this event. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the {@link javax.jcr.Session} that caused this event. + */ + private EventState(int type, NodeId parentId, Path parentPath, + NodeId childId, Path childPath, Name nodeType, + Set mixins, Session session, boolean external) { + + int mask = (Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED); + if ((type & mask) > 0) { + if (childId != null) { + throw new IllegalArgumentException("childId only allowed for Node events."); + } + } else { + if (childId == null && type != Event.PERSIST) { + throw new IllegalArgumentException("childId must not be null for Node events."); + } + } + this.type = type; + this.parentId = parentId; + this.parentPath = parentPath; + this.childId = childId; + this.childRelPath = childPath; + this.nodeType = nodeType; + this.mixins = mixins; + this.session = session; + this.external = external; + } + + //-----------------< factory methods >-------------------------------------- + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#NODE_ADDED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childId the id of the child node associated with this event. + * @param childPath the relative path of the child node that was added. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that added the node. + * @return an EventState instance. + */ + public static EventState childNodeAdded(NodeId parentId, + Path parentPath, + NodeId childId, + Path childPath, + Name nodeType, + Set mixins, + Session session) { + + return childNodeAdded(parentId, parentPath, childId, + childPath, nodeType, mixins, session, false); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#NODE_ADDED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childId the id of the child node associated with this event. + * @param childPath the relative path of the child node that was added. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that added the node. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + */ + public static EventState childNodeAdded(NodeId parentId, + Path parentPath, + NodeId childId, + Path childPath, + Name nodeType, + Set mixins, + Session session, + boolean external) { + + return new EventState(Event.NODE_ADDED, parentId, parentPath, + childId, childPath, nodeType, mixins, session, external); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#NODE_REMOVED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childId the id of the child node associated with this event. + * @param childPath the relative path of the child node that was removed. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that removed the node. + * @return an EventState instance. + */ + public static EventState childNodeRemoved(NodeId parentId, + Path parentPath, + NodeId childId, + Path childPath, + Name nodeType, + Set mixins, + Session session) { + + return childNodeRemoved(parentId, parentPath, childId, + childPath, nodeType, mixins, session, false); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#NODE_REMOVED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childId the id of the child node associated with this event. + * @param childPath the relative path of the child node that was removed. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that removed the node. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + */ + public static EventState childNodeRemoved(NodeId parentId, + Path parentPath, + NodeId childId, + Path childPath, + Name nodeType, + Set mixins, + Session session, + boolean external) { + + return new EventState(Event.NODE_REMOVED, parentId, parentPath, + childId, childPath, nodeType, mixins, session, external); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * NODE_MOVED. The parent node associated with this event type + * is the parent node of the destination of the move! + * This method creates an event state without an info map. A caller of this + * method must ensure that it is properly set afterwards. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childId the id of the child node associated with this event. + * @param childPath the relative path of the child node that was moved. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that moved the node. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + */ + public static EventState nodeMoved(NodeId parentId, + Path parentPath, + NodeId childId, + Path childPath, + Name nodeType, + Set mixins, + Session session, + boolean external) { + return new EventState(Event.NODE_MOVED, parentId, parentPath, + childId, childPath, nodeType, mixins, session, external); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * NODE_MOVED. The parent node associated with this event type + * is the parent node of the destination of the move! + * + * @param parentId the id of the parent node associated with this + * EventState. + * @param destPath the path of the destination of the move. + * @param childId the id of the child node associated with this event. + * @param srcPath the path of the source of the move. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that removed the node. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + * @throws ItemStateException if destPath does not have a + * parent. + */ + public static EventState nodeMovedWithInfo( + NodeId parentId, Path destPath, NodeId childId, Path srcPath, + Name nodeType, Set mixins, Session session, boolean external) + throws ItemStateException { + try { + EventState es = nodeMoved(parentId, destPath.getAncestor(1), + childId, destPath, nodeType, mixins, + session, external); + Map info = new HashMap(); + info.put(SRC_ABS_PATH, InternalValue.create(srcPath)); + info.put(DEST_ABS_PATH, InternalValue.create(destPath)); + es.setInfo(info); + return es; + } catch (RepositoryException e) { + // should never happen actually + String msg = "Unable to resolve parent for path: " + destPath; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * NODE_MOVED. The parent node associated with this event type + * is the parent node of the destination of the reorder! + * + * @param parentId the id of the parent node associated with this + * EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childId the id of the child node associated with this + * event. + * @param destChildPath the name element of the node before it was reordered. + * @param srcChildPath the name element of the reordered node before the + * reorder operation. + * @param beforeChildPath the name element of the node before which the + * reordered node is placed. (may be null + * if reordered to the end. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that removed the node. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + */ + public static EventState nodeReordered(NodeId parentId, + Path parentPath, + NodeId childId, + Path destChildPath, + Path srcChildPath, + Path beforeChildPath, + Name nodeType, + Set mixins, + Session session, + boolean external) { + EventState es = nodeMoved( + parentId, parentPath, childId, destChildPath, + nodeType, mixins, session, external); + Map info = new HashMap(); + info.put(SRC_CHILD_REL_PATH, createValue(srcChildPath)); + InternalValue value = null; + if (beforeChildPath != null) { + value = createValue(beforeChildPath); + } + info.put(DEST_CHILD_REL_PATH, value); + es.setInfo(info); + return es; + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#PROPERTY_ADDED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childPath the relative path of the property that was added. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that added the property. + * @return an EventState instance. + */ + public static EventState propertyAdded(NodeId parentId, + Path parentPath, + Path childPath, + Name nodeType, + Set mixins, + Session session) { + + return propertyAdded(parentId, parentPath, childPath, + nodeType, mixins, session, false); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#PROPERTY_ADDED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childPath the relative path of the property that was added. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that added the property. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + */ + public static EventState propertyAdded(NodeId parentId, + Path parentPath, + Path childPath, + Name nodeType, + Set mixins, + Session session, + boolean external) { + + return new EventState(Event.PROPERTY_ADDED, parentId, parentPath, + null, childPath, nodeType, mixins, session, external); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#PROPERTY_REMOVED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childPath the relative path of the property that was removed. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that removed the property. + * @return an EventState instance. + */ + public static EventState propertyRemoved(NodeId parentId, + Path parentPath, + Path childPath, + Name nodeType, + Set mixins, + Session session) { + + return propertyRemoved(parentId, parentPath, childPath, + nodeType, mixins, session, false); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#PROPERTY_REMOVED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childPath the relative path of the property that was removed. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that removed the property. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + */ + public static EventState propertyRemoved(NodeId parentId, + Path parentPath, + Path childPath, + Name nodeType, + Set mixins, + Session session, + boolean external) { + + return new EventState(Event.PROPERTY_REMOVED, parentId, parentPath, + null, childPath, nodeType, mixins, session, external); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#PROPERTY_CHANGED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childPath the relative path of the property that changed. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that changed the property. + * @return an EventState instance. + */ + public static EventState propertyChanged(NodeId parentId, + Path parentPath, + Path childPath, + Name nodeType, + Set mixins, + Session session) { + + return propertyChanged(parentId, parentPath, childPath, + nodeType, mixins, session, false); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#PROPERTY_CHANGED}. + * + * @param parentId the id of the parent node associated with + * this EventState. + * @param parentPath the path of the parent node associated with + * this EventState. + * @param childPath the relative path of the property that changed. + * @param nodeType the node type of the parent node. + * @param mixins mixins assigned to the parent node. + * @param session the session that changed the property. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + */ + public static EventState propertyChanged(NodeId parentId, + Path parentPath, + Path childPath, + Name nodeType, + Set mixins, + Session session, + boolean external) { + + return new EventState(Event.PROPERTY_CHANGED, parentId, parentPath, + null, childPath, nodeType, mixins, session, external); + } + + /** + * Creates a new {@link javax.jcr.observation.Event} of type + * {@link javax.jcr.observation.Event#PERSIST}. + * + * @param session the session that changed the property. + * @param external flag indicating whether this is an external event + * @return an EventState instance. + */ + public static EventState persist(Session session, boolean external) { + + return new EventState(Event.PERSIST, null, null, null, null, + null, null, session, external); + } + + /** + * {@inheritDoc} + */ + public int getType() { + return type; + } + + /** + * Returns the uuid of the parent node. + * + * @return the uuid of the parent node. + */ + public NodeId getParentId() { + return parentId; + } + + /** + * Returns the path of the parent node. + * + * @return the path of the parent node. + */ + public Path getParentPath() { + return parentPath; + } + + /** + * Returns the Id of a child node operation. + * If this EventState was generated for a property + * operation this method returns null. + * + * @return the id of a child node operation. + */ + public NodeId getChildId() { + return childId; + } + + /** + * Returns the relative {@link Path} of the child + * {@link javax.jcr.Item} associated with this event. + * + * @return the Path associated with this event. + */ + public Path getChildRelPath() { + return childRelPath; + } + + /** + * Returns the node type of the parent node associated with this event. + * + * @return the node type of the parent associated with this event. + */ + public Name getNodeType() { + return nodeType; + } + + /** + * Returns a set of Names which are the names of the mixins + * assigned to the parent node associated with this event. + * + * @return the mixin names as Names. + */ + public Set getMixinNames() { + return mixins; + } + + /** + * Returns the Set of {@link javax.jcr.nodetype.NodeType}s + * assigned to the parent node associated with this event. This + * Set includes the primary type as well as all the mixin types + * assigned to the parent node. + * + * @return Set of {@link javax.jcr.nodetype.NodeType}s. + */ + public Set getNodeTypes(NodeTypeManagerImpl ntMgr) { + if (allTypes == null) { + Set tmp = new HashSet(); + try { + tmp.add(ntMgr.getNodeType(nodeType)); + } catch (NoSuchNodeTypeException e) { + log.warn("Unknown node type: " + nodeType); + } + Iterator it = mixins.iterator(); + while (it.hasNext()) { + Name mixinName = it.next(); + try { + tmp.add(ntMgr.getNodeType(mixinName)); + } catch (NoSuchNodeTypeException e) { + log.warn("Unknown node type: " + mixinName); + } + } + allTypes = Collections.unmodifiableSet(tmp); + } + return allTypes; + } + + /** + * {@inheritDoc} + */ + public String getUserId() { + return session.getUserID(); + } + + /** + * Returns the Session that caused / created this + * EventState. + * + * @return the Session that caused / created this + * EventState. + */ + Session getSession() { + return session; + } + + /** + * Returns the id of the associated item of this EventState. + * + * @return the ItemId or null for {@link Event#PERSIST} events + */ + ItemId getTargetId() { + if (type == Event.PERSIST) { + return null; + } else if (childId == null) { + // property event + return new PropertyId(parentId, childRelPath.getName()); + } else { + // node event + return childId; + } + } + + /** + * Return a flag indicating whether this is an externally generated event. + * + * @return true if this is an external event; + * false otherwise + */ + boolean isExternal() { + return external; + } + + /** + * @return an unmodifiable info Map. + */ + public Map getInfo() { + return info; + } + + /** + * Sets a new info map for this event. + * + * @param info the new info map. + */ + public void setInfo(Map info) { + this.info = Collections.unmodifiableMap(new HashMap(info)); + } + + /** + * Returns a flag indicating whether the child node of this event is a + * shareable node. Only applies to node added/removed events. + * + * @return true for a shareable child node, false + * otherwise. + */ + boolean isShareableNode() { + return shareableNode; + } + + /** + * Sets a new value for the {@link #shareableNode} flag. + * + * @param shareableNode whether the child node is shareable. + * @see #isShareableNode() + */ + void setShareableNode(boolean shareableNode) { + this.shareableNode = shareableNode; + } + + /** + * Returns a String representation of this EventState. + * + * @return a String representation of this EventState. + */ + public String toString() { + if (stringValue == null) { + StringBuilder sb = new StringBuilder(); + sb.append("EventState: ").append(valueOf(type)); + sb.append(", Parent: ").append(parentId); + sb.append(", Child: ").append(childRelPath); + sb.append(", UserId: ").append(session.getUserID()); + sb.append(", Info: ").append(info); + stringValue = sb.toString(); + } + return stringValue; + } + + /** + * Returns a hashCode for this EventState. + * + * @return a hashCode for this EventState. + */ + public int hashCode() { + int h = hashCode; + if (h == 0) { + h = 37; + h = 37 * h + type; + h = 37 * h + (parentId != null ? parentId.hashCode() : 0); + h = 37 * h + (childRelPath != null ? childRelPath.hashCode() : 0); + h = 37 * h + session.hashCode(); + h = 37 * h + info.hashCode(); + hashCode = h; + } + return hashCode; + } + + /** + * Returns true if this EventState is equal to + * another object. + * + * @param obj the reference object with which to compare. + * @return true if object obj is equal to this + * EventState; false otherwise. + */ + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof EventState) { + EventState other = (EventState) obj; + return this.type == other.type + && this.parentId.equals(other.parentId) + && this.childRelPath.equals(other.childRelPath) + && this.session.equals(other.session) + && this.info.equals(other.info); + } + return false; + } + + /** + * Returns a String representation of eventType. + * + * @param eventType an event type defined by {@link Event}. + * @return a String representation of eventType. + */ + public static String valueOf(int eventType) { + if (eventType == Event.NODE_ADDED) { + return "NodeAdded"; + } else if (eventType == Event.NODE_MOVED) { + return "NodeMoved"; + } else if (eventType == Event.NODE_REMOVED) { + return "NodeRemoved"; + } else if (eventType == Event.PROPERTY_ADDED) { + return "PropertyAdded"; + } else if (eventType == Event.PROPERTY_CHANGED) { + return "PropertyChanged"; + } else if (eventType == Event.PROPERTY_REMOVED) { + return "PropertyRemoved"; + } else if (eventType == Event.PERSIST) { + return "Persist"; + } else { + return "UnknownEventType"; + } + } + + /** + * Creates an internal path value from the given path. + * + * @param path the path + * @return an internal value wrapping the path + */ + private static InternalValue createValue(Path path) { + return InternalValue.create(path); + } + + /** + * Get the longest common path of all event state paths. + * + * @param events The list of EventState + * @param session The associated session; it can be null + * @return the longest common path + */ + public static String getCommonPath(List events, SessionImpl session) { + String common = null; + try { + for (int i = 0; i < events.size(); i++) { + EventState state = events.get(i); + Path parentPath = state.getParentPath(); + String s; + if (session == null) { + s = getJCRPath(parentPath); + } else { + s = session.getJCRPath(parentPath); + } + + if (common == null) { + common = s; + } else if (!common.equals(s)) { + + // Assign the shorter path to common. + if (s.length() < common.length()) { + String temp = common; + common = s; + s = temp; + } + + // Find the real common. + while (!s.startsWith(common)) { + int idx = s.lastIndexOf('/'); + if (idx < 0) { + break; + } + common = s.substring(0, idx + 1); + } + } + } + } catch (NamespaceException e) { + log.debug("Problem in retrieving JCR path", e); + } + return common; + } + + private static String getJCRPath(Path path) { + + setupCachingPathResolver(); + + String jcrPath; + try { + jcrPath = cachingPathResolver.getJCRPath(path); + } catch (NamespaceException e) { + jcrPath = ""; + log.debug("Problem in retrieving JCR path", e); + } + return jcrPath; + } + + private static void setupCachingPathResolver() { + if (cachingPathResolver != null) { + return; + } + + PathResolver pathResolver = new ParsingPathResolver(PathFactoryImpl.getInstance(), new NameResolver() { + public Name getQName(String name) throws IllegalNameException, NamespaceException { + return null; + } + + public String getJCRName(Name name) throws NamespaceException { + return name.getLocalName(); + } + }); + + cachingPathResolver = new CachingPathResolver(pathResolver); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollection.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollection.java new file mode 100644 index 00000000000..5f795e03ab3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollection.java @@ -0,0 +1,799 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.observation.ObservationManager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.Collections; + +/** + * The EventStateCollection class implements how {@link EventState} + * objects are created based on the {@link org.apache.jackrabbit.core.state.ItemState}s + * passed to the {@link #createEventStates} method. + *

    + * The basic sequence of method calls is: + *

      + *
    • {@link #createEventStates} or {@link #addAll} to create or add event + * states to the collection
    • + *
    • {@link #prepare} or {@link #prepareDeleted} to prepare the events. If + * this step is omitted, EventListeners might see events of deleted item + * they are not allowed to see.
    • + *
    • {@link #dispatch()} to dispatch the events to the EventListeners.
    • + *
    + */ +public final class EventStateCollection { + + /** + * Logger instance for this class + */ + private static Logger log = LoggerFactory.getLogger(EventStateCollection.class); + + /** + * List of events + */ + private final List events = new ArrayList(); + + /** + * Event dispatcher. + */ + private final EventDispatcher dispatcher; + + /** + * The session that created these events + */ + private final SessionImpl session; + + /** + * The prefix to use for the event paths or null if no prefix + * should be used. + */ + private final Path pathPrefix; + + /** + * Timestamp when this collection was created. + */ + private long timestamp = System.currentTimeMillis(); + + /** + * The user data attached to this event state collection. + */ + private String userData; + + /** + * Creates a new empty EventStateCollection. + *

    + * Because the item state manager in {@link #createEventStates} may represent + * only a subset of the over all item state hierarchy, this constructor + * also takes a path prefix argument. If non null all events + * created by this collection are prefixed with this path. + * + * @param dispatcher event dispatcher + * @param session the session that created these events. + * @param pathPrefix the path to prefix the event paths or null + * if no prefix should be used. + */ + public EventStateCollection(EventDispatcher dispatcher, + SessionImpl session, + Path pathPrefix) { + this.dispatcher = dispatcher; + this.session = session; + this.pathPrefix = pathPrefix; + if (session != null) { + try { + ObservationManager manager = + session.getWorkspace().getObservationManager(); + this.userData = ((ObservationManagerImpl) manager).getUserData(); + } catch (RepositoryException e) { + // should never happen because this + // implementation supports observation + this.userData = null; + } + } else { + this.userData = null; + } + } + + /** + * Creates {@link EventState} instances from ItemState + * changes. + * + * @param rootNodeId the id of the root node. + * @param changes the changes on ItemStates. + * @param stateMgr an ItemStateManager to provide ItemState + * of items that are not contained in the changes collection. + * @throws ItemStateException if an error occurs while creating events + * states for the item state changes. + */ + public void createEventStates(NodeId rootNodeId, ChangeLog changes, ItemStateManager stateMgr) throws ItemStateException { + // create a hierarchy manager, that is based on the ChangeLog and + // the ItemStateProvider + ChangeLogBasedHierarchyMgr hmgr = + new ChangeLogBasedHierarchyMgr(rootNodeId, stateMgr, changes); + + /** + * Important: + * Do NOT change the sequence of events generated unless there's + * a very good reason for it! Some internal SynchronousEventListener + * implementations depend on the order of the events fired. + * LockManagerImpl for example expects that for any given path a + * childNodeRemoved event is fired before a childNodeAdded event. + */ + + // 1. modified items + for (ItemState state : changes.modifiedStates()) { + if (state.isNode()) { + // node changed + // covers the following cases: + // 1) property added + // 2) property removed + // 3) child node added + // 4) child node removed + // 5) node moved/reordered + // 6) node reordered + // 7) shareable node added + // 8) shareable node removed + // cases 1) and 2) are detected with added and deleted states + // on the PropertyState itself. + // cases 3) and 4) are detected with added and deleted states + // on the NodeState itself. + // in case 5) two or three nodes change. two nodes are changed + // when a child node is renamed. three nodes are changed when + // a node is really moved. In any case we are only interested in + // the node that actually got moved. + // in case 6) only one node state changes. the state of the + // parent node. + // in case 7) parent of added shareable node has new child node + // entry. + // in case 8) parent of removed shareable node has removed child + // node entry. + NodeState n = (NodeState) state; + + if (n.hasOverlayedState()) { + NodeId oldParentId = n.getOverlayedState().getParentId(); + NodeId newParentId = n.getParentId(); + if (newParentId != null && !oldParentId.equals(newParentId) && + !n.isShareable()) { + Path oldPath = getZombiePath(n.getNodeId(), hmgr); + + // node moved + // generate node removed & node added event + NodeState oldParent; + try { + oldParent = (NodeState) changes.get(oldParentId); + } catch (NoSuchItemStateException e) { + // old parent has been deleted, retrieve from + // shared item state manager + oldParent = (NodeState) stateMgr.getItemState(oldParentId); + } + if (oldParent != null) { + NodeTypeImpl oldParentNodeType = getNodeType(oldParent, session); + events.add(EventState.childNodeRemoved(oldParentId, + getParent(oldPath), n.getNodeId(), + oldPath.getLastElement(), + oldParentNodeType.getQName(), + oldParent.getMixinTypeNames(), session)); + } else { + // JCR-2298: In some cases the old parent node + // state is no longer available anywhere. Log an + // error since in this case we can't generate the + // correct REMOVE event. + log.error( + "The old parent (node id " + oldParentId + + ") of a moved node (old path " + + oldPath + ") is no longer available." + + " No REMOVE event generated!"); + } + + NodeState newParent = (NodeState) changes.get(newParentId); + NodeTypeImpl newParentNodeType = getNodeType(newParent, session); + Set mixins = newParent.getMixinTypeNames(); + Path newPath = getPath(n.getNodeId(), hmgr); + events.add(EventState.childNodeAdded(newParentId, + getParent(newPath), n.getNodeId(), + newPath.getLastElement(), + newParentNodeType.getQName(), + mixins, session)); + + events.add(EventState.nodeMovedWithInfo( + newParentId, newPath, n.getNodeId(), oldPath, + newParentNodeType.getQName(), mixins, + session, false)); + } else { + // a moved node always has a modified parent node + NodeState parent = null; + try { + // root node does not have a parent UUID + if (state.getParentId() != null) { + parent = (NodeState) changes.get(state.getParentId()); + } + } catch (NoSuchItemStateException e) { + // should never happen actually. this would mean + // the parent of this modified node is deleted + String msg = "Parent of node " + state.getId() + " is deleted."; + log.error(msg); + throw new ItemStateException(msg, e); + } + if (parent != null) { + // check if node has been renamed + ChildNodeEntry moved = null; + for (ChildNodeEntry child : parent.getRemovedChildNodeEntries()) { + if (child.getId().equals(n.getNodeId())) { + // found node re-added with different name + moved = child; + } + } + if (moved != null) { + NodeTypeImpl nodeType = getNodeType(parent, session); + Set mixins = parent.getMixinTypeNames(); + Path newPath = getPath(state.getId(), hmgr); + Path parentPath = getParent(newPath); + Path oldPath; + try { + if (moved.getIndex() == 0) { + oldPath = PathFactoryImpl.getInstance().create(parentPath, moved.getName(), false); + } else { + oldPath = PathFactoryImpl.getInstance().create( + parentPath, moved.getName(), moved.getIndex(), false); + } + } catch (RepositoryException e) { + // should never happen actually + String msg = "Malformed path for item: " + state.getId(); + log.error(msg); + throw new ItemStateException(msg, e); + } + events.add(EventState.childNodeRemoved( + parent.getNodeId(), parentPath, + n.getNodeId(), oldPath.getLastElement(), + nodeType.getQName(), mixins, session)); + + events.add(EventState.childNodeAdded( + parent.getNodeId(), parentPath, + n.getNodeId(), newPath.getLastElement(), + nodeType.getQName(), mixins, session)); + + events.add(EventState.nodeMovedWithInfo( + parent.getNodeId(), newPath, n.getNodeId(), + oldPath, nodeType.getQName(), mixins, + session, false)); + } + } + } + } + + // check if child nodes of modified node state have been reordered + List reordered = n.getReorderedChildNodeEntries(); + NodeTypeImpl nodeType = getNodeType(n, session); + Set mixins = n.getMixinTypeNames(); + if (reordered.size() > 0) { + // create a node removed and a node added event for every + // reorder + for (ChildNodeEntry child : reordered) { + Path addedElem = getPathElement(child); + Path parentPath = getPath(n.getNodeId(), hmgr); + // get removed index + NodeState overlayed = (NodeState) n.getOverlayedState(); + ChildNodeEntry entry = overlayed.getChildNodeEntry(child.getId()); + if (entry == null) { + throw new ItemStateException("Unable to retrieve old child index for item: " + child.getId()); + } + Path removedElem = getPathElement(entry); + + events.add(EventState.childNodeRemoved(n.getNodeId(), + parentPath, child.getId(), removedElem, + nodeType.getQName(), mixins, session)); + + events.add(EventState.childNodeAdded(n.getNodeId(), + parentPath, child.getId(), addedElem, + nodeType.getQName(), mixins, session)); + + List cne = n.getChildNodeEntries(); + // index of the child node entry before which this + // child node entry was reordered + int idx = cne.indexOf(child) + 1; + Path beforeElem = null; + if (idx < cne.size()) { + beforeElem = getPathElement(cne.get(idx)); + } + + events.add(EventState.nodeReordered(n.getNodeId(), + parentPath, child.getId(), addedElem, + removedElem, beforeElem, nodeType.getQName(), mixins, + session, false)); + } + } + + // create events if n is shareable + createShareableNodeEvents(n, changes, hmgr, stateMgr); + } else { + // property changed + Path path = getPath(state.getId(), hmgr); + NodeState parent = (NodeState) stateMgr.getItemState(state.getParentId()); + NodeTypeImpl nodeType = getNodeType(parent, session); + Set mixins = parent.getMixinTypeNames(); + events.add(EventState.propertyChanged(state.getParentId(), + getParent(path), path.getLastElement(), + nodeType.getQName(), mixins, session)); + } + } + + // 2. removed items + for (ItemState state : changes.deletedStates()) { + if (state.isNode()) { + // node deleted + NodeState n = (NodeState) state; + NodeState parent = (NodeState) stateMgr.getItemState(n.getParentId()); + NodeTypeImpl nodeType = getNodeType(parent, session); + Set mixins = parent.getMixinTypeNames(); + Path path = getZombiePath(state.getId(), hmgr); + events.add(EventState.childNodeRemoved(n.getParentId(), + getParent(path), + n.getNodeId(), + path.getLastElement(), + nodeType.getQName(), + mixins, + session)); + + // create events if n is shareable + createShareableNodeEvents(n, changes, hmgr, stateMgr); + } else { + // property removed + // only create an event if node still exists + try { + NodeState n = (NodeState) changes.get(state.getParentId()); + // node state exists -> only property removed + NodeTypeImpl nodeType = getNodeType(n, session); + Set mixins = n.getMixinTypeNames(); + Path path = getZombiePath(state.getId(), hmgr); + events.add(EventState.propertyRemoved(state.getParentId(), + getParent(path), + path.getLastElement(), + nodeType.getQName(), + mixins, + session)); + } catch (NoSuchItemStateException e) { + // node removed as well -> do not create an event + } + } + } + + // 3. added items + for (ItemState state : changes.addedStates()) { + if (state.isNode()) { + // node created + NodeState n = (NodeState) state; + NodeId parentId = n.getParentId(); + // the parent of an added item is always modified or new + NodeState parent = (NodeState) changes.get(parentId); + if (parent == null) { + String msg = "Parent " + parentId + " must be changed as well."; + log.error(msg); + throw new ItemStateException(msg); + } + NodeTypeImpl nodeType = getNodeType(parent, session); + Set mixins = parent.getMixinTypeNames(); + Path path = getPath(n.getNodeId(), hmgr); + events.add(EventState.childNodeAdded(parentId, + getParent(path), + n.getNodeId(), + path.getLastElement(), + nodeType.getQName(), + mixins, + session)); + + // create events if n is shareable + createShareableNodeEvents(n, changes, hmgr, stateMgr); + } else { + // property created / set + NodeState n = (NodeState) changes.get(state.getParentId()); + if (n == null) { + String msg = "Node " + state.getParentId() + " must be changed as well."; + log.error(msg); + throw new ItemStateException(msg); + } + NodeTypeImpl nodeType = getNodeType(n, session); + Set mixins = n.getMixinTypeNames(); + Path path = getPath(state.getId(), hmgr); + events.add(EventState.propertyAdded(state.getParentId(), + getParent(path), + path.getLastElement(), + nodeType.getQName(), + mixins, + session)); + } + } + } + + /** + * Adds all event states in the given collection to this collection + * + * @param c + */ + public void addAll(Collection c) { + events.addAll(c); + } + + /** + * Prepares already added events for dispatching. + */ + public void prepare() { + dispatcher.prepareEvents(this); + } + + /** + * Prepares deleted items from changes. + * + * @param changes the changes to prepare. + */ + public void prepareDeleted(ChangeLog changes) { + dispatcher.prepareDeleted(this, changes); + } + + /** + * Dispatches the events to the {@link javax.jcr.observation.EventListener}s. + */ + public void dispatch() { + dispatcher.dispatchEvents(this); + } + + /** + * Returns the path prefix for this event state collection or null + * if no path prefix was set in the constructor of this collection. See + * also {@link EventStateCollection#EventStateCollection}. + * + * @return the path prefix for this event state collection. + */ + public Path getPathPrefix() { + return pathPrefix; + } + + /** + * @return the timestamp when this collection was created. + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Sets a new timestamp for this collection. + * + * @param timestamp the new timestamp value. + */ + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + /** + * Returns an iterator over {@link EventState} instance. + * + * @return an iterator over {@link EventState} instance. + */ + Iterator iterator() { + return events.iterator(); + } + + /** + * Return the list of events. + * @return list of events + */ + public List getEvents() { + return Collections.unmodifiableList(events); + } + + /** + * Get the number of events. + * + * @return the size + */ + public int size() { + return events.size(); + } + + /** + * Return the session who is the origin of this events. + * @return event source + */ + public SessionImpl getSession() { + return session; + } + + /** + * @return the user data attached to this event state collection. + */ + public String getUserData() { + return userData; + } + + /** + * Sets the user data for this event state collection. + * + * @param userData the user data. + */ + public void setUserData(String userData) { + this.userData = userData; + } + + //----------------------------< internal >---------------------------------- + + private void createShareableNodeEvents(NodeState n, + ChangeLog changes, + ChangeLogBasedHierarchyMgr hmgr, + ItemStateManager stateMgr) + throws ItemStateException { + if (n.isShareable()) { + // check if a share was added or removed + for (NodeId parentId : n.getAddedShares()) { + // ignore primary parent id + if (n.getParentId().equals(parentId)) { + continue; + } + NodeState parent = (NodeState) changes.get(parentId); + if (parent == null) { + // happens when mix:shareable is added to an existing node + // usually the parent node state is in the change log + // when a node is added to a shared set -> new child node + // entry on parent node state. + parent = (NodeState) stateMgr.getItemState(parentId); + } + Name ntName = getNodeType(parent, session).getQName(); + EventState es = EventState.childNodeAdded(parentId, + getPath(parentId, hmgr), + n.getNodeId(), + getNameElement(n.getNodeId(), parentId, hmgr), + ntName, + parent.getMixinTypeNames(), + session); + es.setShareableNode(true); + events.add(es); + } + for (NodeId parentId : n.getRemovedShares()) { + // if this shareable node is removed, only create events for + // parent ids that are not primary + if (n.getParentId().equals(parentId)) { + continue; + } + NodeState parent = null; + try { + parent = (NodeState) changes.get(parentId); + } catch (NoSuchItemStateException e) { + // parent has been removed as well + // ignore and retrieve from stateMgr + } + if (parent == null) { + // happens when mix:shareable is removed from an existing + // node. Usually the parent node state is in the change log + // when a node is removed to a shared set -> removed child + // node entry on parent node state. + parent = (NodeState) stateMgr.getItemState(parentId); + } + Name ntName = getNodeType(parent, session).getQName(); + EventState es = EventState.childNodeRemoved(parentId, + getZombiePath(parentId, hmgr), + n.getNodeId(), + getZombieNameElement(n.getNodeId(), parentId, hmgr), + ntName, + parent.getMixinTypeNames(), + session); + es.setShareableNode(true); + events.add(es); + } + } + } + + /** + * Resolves the node type name in node into a {@link javax.jcr.nodetype.NodeType} + * object using the {@link javax.jcr.nodetype.NodeTypeManager} of session. + * + * @param node the node. + * @param session the session. + * @return the {@link javax.jcr.nodetype.NodeType} of node. + * @throws ItemStateException if the nodetype cannot be resolved. + */ + private NodeTypeImpl getNodeType(NodeState node, SessionImpl session) + throws ItemStateException { + try { + return session.getNodeTypeManager().getNodeType(node.getNodeTypeName()); + } catch (Exception e) { + // also catch eventual runtime exceptions here + // should never happen actually + String msg; + if (node == null) { + msg = "Node state is null"; + } else { + msg = "Item " + node.getNodeId() + " has unknown node type: " + node.getNodeTypeName(); + } + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * Returns the path of the parent node of node at path.. + * + * @param p the path. + * @return the parent path. + * @throws ItemStateException if p does not have a parent + * path. E.g. p designates root. + */ + private Path getParent(Path p) throws ItemStateException { + try { + return p.getAncestor(1); + } catch (RepositoryException e) { + // should never happen actually + String msg = "Unable to resolve parent for path: " + p; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * Resolves the path of the Item with id itemId. + * + * @param itemId the id of the item. + * @return the path of the item. + * @throws ItemStateException if the path cannot be resolved. + */ + private Path getPath(ItemId itemId, HierarchyManager hmgr) + throws ItemStateException { + try { + return prefixPath(hmgr.getPath(itemId)); + } catch (RepositoryException e) { + // should never happen actually + String msg = "Unable to resolve path for item: " + itemId; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * Returns the name element for the node with the given nodeId + * and its parent with parentId. This method is only useful + * if nodeId denotes a shareable node. + * + * @param nodeId the node id of a shareable node. + * @param parentId the id of the parent node. + * @param hmgr the hierarchy manager. + * @return the name element for the node. + * @throws ItemStateException if an error occurs while resolving the name. + */ + private Path getNameElement( + NodeId nodeId, NodeId parentId, HierarchyManager hmgr) + throws ItemStateException { + try { + Name name = hmgr.getName(nodeId, parentId); + return PathFactoryImpl.getInstance().create(name); + } catch (RepositoryException e) { + String msg = "Unable to get name for node with id: " + nodeId; + throw new ItemStateException(msg, e); + } + } + + /** + * Returns the zombie (i.e. the old) name element for the node with + * the given nodeId and its parent with parentId. + * This method is only useful if nodeId denotes a shareable + * node. + * + * @param nodeId the node id of a shareable node. + * @param parentId the id of the parent node. + * @param hmgr the hierarchy manager. + * @return the name element for the node. + * @throws ItemStateException if an error occurs while resolving the name. + */ + private Path getZombieNameElement( + NodeId nodeId, NodeId parentId, ChangeLogBasedHierarchyMgr hmgr) + throws ItemStateException { + try { + Name name = hmgr.getZombieName(nodeId, parentId); + return PathFactoryImpl.getInstance().create(name); + } catch (RepositoryException e) { + // should never happen actually + String msg = "Unable to resolve zombie name for item: " + nodeId; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * Resolves the zombie (i.e. the old) path of the Item with id + * itemId. + * + * @param itemId the id of the item. + * @return the path of the item. + * @throws ItemStateException if the path cannot be resolved. + */ + private Path getZombiePath(ItemId itemId, ChangeLogBasedHierarchyMgr hmgr) + throws ItemStateException { + try { + return prefixPath(hmgr.getZombiePath(itemId)); + } catch (RepositoryException e) { + // should never happen actually + String msg = "Unable to resolve zombie path for item: " + itemId; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * Prefixes the Path p with {@link #pathPrefix}. + * + * @param p the Path to prefix. + * @return the prefixed path or p itself if {@link #pathPrefix} + * is null. + * @throws RepositoryException if the path cannot be prefixed. + */ + private Path prefixPath(Path p) throws RepositoryException { + if (pathPrefix == null) { + return p; + } + PathBuilder builder = new PathBuilder(pathPrefix.getElements()); + Path.Element[] elements = p.getElements(); + for (int i = 0; i < elements.length; i++) { + if (elements[i].denotesRoot()) { + continue; + } + builder.addLast(elements[i]); + } + return builder.getPath(); + } + + /** + * Returns the path element for the given child node entry. + * + * @param entry a child node entry. + * @return the path element for the given entry. + */ + private Path getPathElement(ChildNodeEntry entry) { + Name name = entry.getName(); + int index = (entry.getIndex() != 1) ? entry.getIndex() : 0; + return PathFactoryImpl.getInstance().create(name, index); + } + + /** + * Get the longest common path of all event state paths. + * + * @return the longest common path + */ + public String getCommonPath() { + return EventState.getCommonPath(events, session); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollectionFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollectionFactory.java new file mode 100755 index 00000000000..66b2b827dad --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/EventStateCollectionFactory.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.core.observation; + +import javax.jcr.RepositoryException; + +/** + * Defines methods to create an {@link EventStateCollection} + */ +public interface EventStateCollectionFactory { + + /** + * Creates an EventStateCollection. + * + * @return a new EventStateCollection + * @throws RepositoryException if creation fails for some reason + */ + EventStateCollection createEventStateCollection() throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/FilteredEventIterator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/FilteredEventIterator.java new file mode 100644 index 00000000000..013978b0a2e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/FilteredEventIterator.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.core.observation; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.commons.iterator.EventIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.FilteredRangeIterator; +import org.apache.jackrabbit.commons.predicate.Predicate; +import org.apache.jackrabbit.core.SessionImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + */ +class FilteredEventIterator extends EventIteratorAdapter { + + /** + * Logger instance for this class + */ + private static final Logger log + = LoggerFactory.getLogger(FilteredEventIterator.class); + + /** + * Target session + */ + private final SessionImpl session; + + /** + * The timestamp when the events occurred. + */ + private final long timestamp; + + /** + * The user data associated with these events. + */ + private final String userData; + + /** + * Creates a new FilteredEventIterator. + * + * @param session target session + * @param eventStates an iterator over unfiltered {@link EventState}s. + * @param timestamp the time when the event were created. + * @param userData the user data associated with these events. + * @param filter only event that pass the filter will be dispatched to the + * event listener. + * @param denied Set of ItemIds of denied ItemStates + * rejected by the AccessManager + * @param includePersistEvent whether or not to include the {@link Event#PERSIST} event + */ + public FilteredEventIterator( + SessionImpl session, Iterator eventStates, + long timestamp, String userData, + final EventFilter filter, final Set denied, boolean includePersistEvent) { + super(new FilteredRangeIterator(wrapAndAddPersist(eventStates, includePersistEvent), new Predicate() { + public boolean evaluate(Object object) { + try { + EventState state = (EventState) object; + return !denied.contains(state.getTargetId()) + && !filter.blocks(state); + } catch (RepositoryException e) { + log.error("Exception while applying event filter", e); + return false; + } + } + })); + this.session = session; + this.timestamp = timestamp; + this.userData = userData; + } + + @Override + public Object next() { + return new EventImpl( + session, (EventState) super.next(), timestamp, userData); + } + + /** + * Optionally wrap the iterator into one that adds PERSIST events + */ + private static Iterator wrapAndAddPersist(final Iterator states, + boolean includePersistEvents) { + if (includePersistEvents) { + return new PersistEventAddingWrapper(states); + } + else { + return states; + } + } + + /** + * A wrapper around {@link Iterator} that adds a "PERSIST" event at the end. + */ + private static class PersistEventAddingWrapper implements Iterator { + + private Iterator states; + private boolean persistSent = false; + private EventState previous = null; + + public PersistEventAddingWrapper(Iterator states) { + this.states = states; + } + + public boolean hasNext() { + if (states.hasNext()) { + return true; + } else { + return !persistSent; + } + } + + public EventState next() { + if (states.hasNext()) { + previous = states.next(); + return previous; + } + else if (persistSent || previous == null) { + // we are at the end; either because we already sent + // PERSIST, or because the iterator was empty anyway + throw new NoSuchElementException(); + } + else { + persistSent = true; + return EventState.persist(previous.getSession(), previous.isExternal()); + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ObservationDispatcher.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ObservationDispatcher.java new file mode 100644 index 00000000000..f5277179064 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ObservationDispatcher.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import org.apache.commons.collections.Buffer; +import org.apache.commons.collections.BufferUtils; +import org.apache.commons.collections.buffer.UnboundedFifoBuffer; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Dispatcher for dispatching events to listeners within a single workspace. + */ +public final class ObservationDispatcher extends EventDispatcher + implements Runnable { + + /** + * Logger instance for this class + */ + private static final Logger log + = LoggerFactory.getLogger(ObservationDispatcher.class); + + /** + * Dummy DispatchAction indicating the notification thread to end + */ + private static final DispatchAction DISPOSE_MARKER = new DispatchAction(null, null); + + /** + * The maximum number of queued asynchronous events. To avoid of of memory + * problems, the default value is 200'000. To change the default, set the + * system property jackrabbit.maxQueuedEvents to the required value. If more + * events are in the queue, the current thread waits, unless the current thread is + * the observation dispatcher itself (in which case only a warning is logged + * - usually observation listeners shouldn't cause new events). + */ + private static final int MAX_QUEUED_EVENTS = Integer.parseInt(System.getProperty("jackrabbit.maxQueuedEvents", "200000")); + + /** + * Currently active EventConsumers for notification. + */ + private Set activeConsumers = new HashSet(); + + /** + * Currently active synchronous EventConsumers for notification. + */ + private Set synchronousConsumers = new HashSet(); + + /** + * Set of EventConsumers for read only Set access + */ + private Set readOnlyConsumers; + + /** + * Set of synchronous EventConsumers for read only Set access. + */ + private Set synchronousReadOnlyConsumers; + + /** + * synchronization monitor for listener changes + */ + private Object consumerChange = new Object(); + + /** + * Contains the pending events that will be delivered to event listeners + */ + private Buffer eventQueue + = BufferUtils.blockingBuffer(new UnboundedFifoBuffer()); + + private AtomicInteger eventQueueSize = new AtomicInteger(); + + /** + * The background notification thread + */ + private Thread notificationThread; + + private long lastError; + + /** + * Creates a new ObservationDispatcher instance + * and starts the notification thread daemon. + */ + public ObservationDispatcher() { + notificationThread = new Thread(this, "ObservationManager"); + notificationThread.setDaemon(true); + notificationThread.start(); + } + + /** + * Disposes this ObservationManager. This will + * effectively stop the background notification thread. + */ + public void dispose() { + // dispatch dummy event to mark end of notification + eventQueue.add(DISPOSE_MARKER); + try { + notificationThread.join(); + } catch (InterruptedException e) { + // FIXME log exception ? + } + log.info("Notification of EventListeners stopped."); + } + + /** + * Returns an unmodifiable Set of EventConsumers. + * + * @return Set of EventConsumers. + */ + Set getAsynchronousConsumers() { + synchronized (consumerChange) { + if (readOnlyConsumers == null) { + readOnlyConsumers = Collections.unmodifiableSet(new HashSet(activeConsumers)); + } + return readOnlyConsumers; + } + } + + Set getSynchronousConsumers() { + synchronized (consumerChange) { + if (synchronousReadOnlyConsumers == null) { + synchronousReadOnlyConsumers = Collections.unmodifiableSet(new HashSet(synchronousConsumers)); + } + return synchronousReadOnlyConsumers; + } + } + + /** + * Implements the run method of the background notification + * thread. + */ + public void run() { + DispatchAction action; + while ((action = (DispatchAction) eventQueue.remove()) != DISPOSE_MARKER) { + + eventQueueSize.getAndAdd(-action.getEventStates().size()); + log.debug("got EventStateCollection"); + log.debug("event delivery to " + action.getEventConsumers().size() + " consumers started..."); + for (Iterator it = action.getEventConsumers().iterator(); it.hasNext();) { + EventConsumer c = it.next(); + try { + c.consumeEvents(action.getEventStates()); + } catch (Throwable t) { + log.warn("EventConsumer " + + c.getEventListener().getClass().getName() + + " threw exception", t); + // move on to the next consumer + } + } + log.debug("event delivery finished."); + + } + } + + /** + * {@inheritDoc} + *

    + * Gives this observation manager the opportunity to + * prepare the events for dispatching. + */ + void prepareEvents(EventStateCollection events) { + Set consumers = new HashSet(); + consumers.addAll(getSynchronousConsumers()); + consumers.addAll(getAsynchronousConsumers()); + for (EventConsumer c : consumers) { + c.prepareEvents(events); + } + } + + /** + * {@inheritDoc} + */ + void prepareDeleted(EventStateCollection events, ChangeLog changes) { + Set consumers = new HashSet(); + consumers.addAll(getSynchronousConsumers()); + consumers.addAll(getAsynchronousConsumers()); + for (EventConsumer c : consumers) { + c.prepareDeleted(events, changes.deletedStates()); + } + } + + /** + * {@inheritDoc} + *

    + * Dispatches the {@link EventStateCollection events} to all + * registered {@link javax.jcr.observation.EventListener}s. + */ + void dispatchEvents(EventStateCollection events) { + // JCR-3426: log warning when changes are done + // with the notification thread + if (Thread.currentThread() == notificationThread) { + log.warn("Save call with event notification thread detected. This " + + "may lead to a growing event queue. Enable debug log to " + + "see the stack trace with the class calling save()."); + if (log.isDebugEnabled()) { + log.debug("Stack trace:", new Exception()); + } + } + // notify synchronous listeners + Set synchronous = getSynchronousConsumers(); + if (log.isDebugEnabled()) { + log.debug("notifying " + synchronous.size() + " synchronous listeners."); + } + for (EventConsumer c : synchronous) { + try { + c.consumeEvents(events); + } catch (Throwable t) { + log.error("Synchronous EventConsumer threw exception.", t); + // move on to next consumer + } + } + eventQueue.add(new DispatchAction(events, getAsynchronousConsumers())); + eventQueueSize.addAndGet(events.size()); + } + + /** + * Checks if the observation event queue contains more than the + * configured {@link #MAX_QUEUED_EVENTS maximum number of events}, + * and delays the current thread in such cases. No delay is added + * if the current thread is the observation thread, for example if + * an observation listener writes to the repository. + *

    + * This method should only be called outside the scope of internal + * repository access locks. + */ + public void delayIfEventQueueOverloaded() { + if (eventQueueSize.get() > MAX_QUEUED_EVENTS) { + boolean logWarning = false; + long now = System.currentTimeMillis(); + // log a warning at most every 5 seconds (to avoid filling the log file) + if (lastError == 0 || now > lastError + 5000) { + logWarning = true; + log.warn("More than " + MAX_QUEUED_EVENTS + " events in the queue", new Exception("Stack Trace")); + lastError = now; + } + if (Thread.currentThread() == notificationThread) { + if (logWarning) { + log.warn("Recursive notification?"); + } + } else { + if (logWarning) { + log.warn("Waiting"); + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + log.warn("Interrupted while rate-limiting writes", e); + } + } + } + } + + /** + * Adds or replaces an event consumer. + * @param consumer the EventConsumer to add or replace. + */ + void addConsumer(EventConsumer consumer) { + synchronized (consumerChange) { + if (consumer.getEventListener() instanceof SynchronousEventListener) { + // remove existing if any + synchronousConsumers.remove(consumer); + // re-add it + synchronousConsumers.add(consumer); + // reset read only consumer set + synchronousReadOnlyConsumers = null; + } else { + // remove existing if any + activeConsumers.remove(consumer); + // re-add it + activeConsumers.add(consumer); + // reset read only consumer set + readOnlyConsumers = null; + } + } + } + + /** + * Unregisters an event consumer from event notification. + * @param consumer the consumer to deregister. + */ + void removeConsumer(EventConsumer consumer) { + synchronized (consumerChange) { + if (consumer.getEventListener() instanceof SynchronousEventListener) { + synchronousConsumers.remove(consumer); + // reset read only listener set + synchronousReadOnlyConsumers = null; + } else { + activeConsumers.remove(consumer); + // reset read only listener set + readOnlyConsumers = null; + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ObservationManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ObservationManagerImpl.java new file mode 100644 index 00000000000..612db75a009 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/ObservationManagerImpl.java @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.jcr.AccessDeniedException; +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 org.apache.jackrabbit.api.observation.JackrabbitEventFilter; +import org.apache.jackrabbit.api.observation.JackrabbitObservationManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Each Session instance has its own ObservationManager + * instance. The class SessionLocalObservationManager implements + * this behaviour. + */ +public class ObservationManagerImpl implements EventStateCollectionFactory, + JackrabbitObservationManager { + + /** + * The logger instance of this class + */ + private static final Logger log = LoggerFactory.getLogger(ObservationManagerImpl.class); + + /** + * The Session this ObservationManager + * belongs to. + */ + private final SessionImpl session; + + /** + * The cluster node where this session is running. + */ + private final ClusterNode clusterNode; + + /** + * The ObservationDispatcher + */ + private final ObservationDispatcher dispatcher; + + /** + * The currently set user data. + */ + private String userData; + + static { + // preload EventListenerIteratorImpl to prevent classloader issues during shutdown + EventListenerIteratorImpl.class.hashCode(); + } + + /** + * Creates an ObservationManager instance. + * + * @param dispatcher observation dispatcher + * @param session the Session this ObservationManager + * belongs to. + * @param clusterNode + * @throws NullPointerException if dispatcher, session + * or clusterNode is null. + */ + public ObservationManagerImpl( + ObservationDispatcher dispatcher, SessionImpl session, + ClusterNode clusterNode) { + if (dispatcher == null) { + throw new NullPointerException("dispatcher"); + } + if (session == null) { + throw new NullPointerException("session"); + } + + this.dispatcher = dispatcher; + this.session = session; + this.clusterNode = clusterNode; + } + + /** + * {@inheritDoc} + */ + public void addEventListener(EventListener listener, + int eventTypes, + String absPath, + boolean isDeep, + String[] uuid, + String[] nodeTypeName, + boolean noLocal) + throws RepositoryException { + + // create filter + EventFilter filter = createEventFilter(eventTypes, Collections.singletonList(absPath), + isDeep, uuid, nodeTypeName, noLocal, false, false); + + dispatcher.addConsumer(new EventConsumer(session, listener, filter)); + } + + @Override + public void addEventListener(EventListener listener, JackrabbitEventFilter filter) + throws RepositoryException { + + String[] excludedPaths = filter.getExcludedPaths(); + if (excludedPaths.length > 0) { + log.warn("JackrabbitEventFilter excludedPaths is not implemented and will be ignored: {}", + Arrays.toString(excludedPaths)); + } + + List absPaths = new ArrayList(Arrays.asList(filter.getAdditionalPaths())); + if (filter.getAbsPath() != null) { + absPaths.add(filter.getAbsPath()); + } + + EventFilter f = createEventFilter(filter.getEventTypes(), absPaths, + filter.getIsDeep(), filter.getIdentifiers(), filter.getNodeTypes(), + filter.getNoLocal(), filter.getNoExternal(), filter.getNoInternal()); + + dispatcher.addConsumer(new EventConsumer(session, listener, f)); + } + + /** + * {@inheritDoc} + */ + public void removeEventListener(EventListener listener) + throws RepositoryException { + dispatcher.removeConsumer( + new EventConsumer(session, listener, EventFilter.BLOCK_ALL)); + + } + + /** + * {@inheritDoc} + */ + public EventListenerIterator getRegisteredEventListeners() + throws RepositoryException { + return new EventListenerIteratorImpl( + session, + dispatcher.getSynchronousConsumers(), + dispatcher.getAsynchronousConsumers()); + } + + /** + * {@inheritDoc} + */ + public void setUserData(String userData) throws RepositoryException { + this.userData = userData; + } + + /** + * @return the currently set user data. + */ + String getUserData() { + return userData; + } + + /** + * Unregisters all EventListeners. + */ + public void dispose() { + try { + EventListenerIterator it = getRegisteredEventListeners(); + while (it.hasNext()) { + EventListener l = it.nextEventListener(); + log.debug("removing EventListener: " + l); + removeEventListener(l); + } + } catch (RepositoryException e) { + log.error("Internal error: Unable to dispose ObservationManager.", e); + } + + } + + /** + * Creates a new event filter with the given restrictions. + * + * @param eventTypes A combination of one or more event type constants encoded as a bitmask. + * @param absPaths absolute paths. + * @param isDeep a boolean. + * @param uuid array of UUIDs. + * @param nodeTypeName array of node type names. + * @param noLocal a boolean. + * @param noExternal a boolean. + * @param noInternal a boolean. + * @return the event filter with the given restrictions. + * @throws RepositoryException if an error occurs. + */ + public EventFilter createEventFilter(int eventTypes, + List absPaths, + boolean isDeep, + String[] uuid, + String[] nodeTypeName, + boolean noLocal, + boolean noExternal, + boolean noInternal) + throws RepositoryException { + // create NodeType instances from names + NodeTypeImpl[] nodeTypes; + if (nodeTypeName == null) { + nodeTypes = null; + } else { + NodeTypeManagerImpl ntMgr = session.getNodeTypeManager(); + nodeTypes = new NodeTypeImpl[nodeTypeName.length]; + for (int i = 0; i < nodeTypes.length; i++) { + nodeTypes[i] = (NodeTypeImpl) ntMgr.getNodeType(nodeTypeName[i]); + } + } + + List paths = new ArrayList(); + for (String absPath : absPaths) { + try { + Path normalizedPath = session.getQPath(absPath).getNormalizedPath(); + if (!normalizedPath.isAbsolute()) { + throw new RepositoryException("absPath must be absolute"); + } + paths.add(normalizedPath); + } catch (NameException e) { + String msg = "invalid path syntax: " + absPath; + log.debug(msg); + throw new RepositoryException(msg, e); + + } } + NodeId[] ids = null; + if (uuid != null) { + ids = new NodeId[uuid.length]; + for (int i = 0; i < uuid.length; i++) { + ids[i] = NodeId.valueOf(uuid[i]); + } + } + // create filter + return new EventFilter( + session, eventTypes, paths, isDeep, ids, nodeTypes, noLocal, noExternal, noInternal); + } + + /** + * Returns the event journal for this workspace. The events are filtered + * according to the passed criteria. + * + * @param eventTypes A combination of one or more event type constants encoded as a bitmask. + * @param absPath an absolute path. + * @param isDeep a boolean. + * @param uuid array of UUIDs. + * @param nodeTypeName array of node type names. + * @return the event journal for this repository. + * @throws UnsupportedRepositoryOperationException if this repository does + * not support an event journal (cluster journal disabled). + * @throws RepositoryException if another error occurs. + * @see ObservationManager#getEventJournal(int, String, boolean, String[], String[]) + */ + public EventJournal getEventJournal( + int eventTypes, String absPath, boolean isDeep, + String[] uuid, String[] nodeTypeName) + throws RepositoryException { + if (clusterNode == null) { + throw new UnsupportedRepositoryOperationException( + "Event journal is only available in cluster deployments"); + } + + if (!session.isAdmin()) { + throw new AccessDeniedException("Only administrator session may access EventJournal"); + } + + EventFilter filter = createEventFilter( + eventTypes, Collections.singletonList(absPath), isDeep, uuid, nodeTypeName, false, false, false); + return new EventJournalImpl( + filter, clusterNode.getJournal(), clusterNode.getId(), session); + } + + /** + * Returns an unfiltered event journal for this workspace. + * + * @return the event journal for this repository. + * @throws UnsupportedRepositoryOperationException if this repository does + * not support an event journal (cluster journal disabled). + * @throws RepositoryException if another error occurs. + */ + public EventJournal getEventJournal() throws RepositoryException { + return getEventJournal(-1, "/", true, null, null); + } + + //------------------------------------------< EventStateCollectionFactory > + + /** + * {@inheritDoc} + *

    + * Creates an EventStateCollection tied to the session + * which is attached to this ObservationManager instance. + */ + public EventStateCollection createEventStateCollection() { + EventStateCollection esc = new EventStateCollection(dispatcher, session, null); + esc.setUserData(userData); + return esc; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/SynchronousEventListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/SynchronousEventListener.java new file mode 100644 index 00000000000..fdf2ceafc27 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/observation/SynchronousEventListener.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.core.observation; + +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; + +/** + * Defines a marker interface for {@link javax.jcr.observation.EventListener} + * implementations that wish a synchronous notification of changes to the + * workspace. That is, a SynchronousEventListener is called before + * the call to {@link javax.jcr.Item#save()} returns. In contrast, a regular + * {@link javax.jcr.observation.EventListener} might be called after + * save() returns. + *

    + * Important note: an implementation of {@link SynchronousEventListener} + * must not modify content with the thread that calls {@link + * #onEvent(EventIterator)} otherwise inconsistencies may occur. + */ +public interface SynchronousEventListener extends EventListener { +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/AbstractPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/AbstractPersistenceManager.java new file mode 100644 index 00000000000..40894893153 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/AbstractPersistenceManager.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.core.persistence; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; + +/** + * Implementation PersistenceManager that handles some + * concepts. + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public abstract class AbstractPersistenceManager implements PersistenceManager { + + /** + * {@inheritDoc} + */ + public NodeState createNew(NodeId id) { + return new NodeState(id, null, null, NodeState.STATUS_NEW, false); + } + + /** + * {@inheritDoc} + */ + public PropertyState createNew(PropertyId id) { + return new PropertyState(id, PropertyState.STATUS_NEW, false); + } + + /** + * Right now, this iterates over all items in the changelog and + * calls the individual methods that handle single item states + * or node references objects. Properly implemented, this method + * should ensure that changes are either written completely to + * the underlying persistence layer, or not at all. + * + * {@inheritDoc} + */ + public synchronized void store(ChangeLog changeLog) throws ItemStateException { + for (ItemState state : changeLog.deletedStates()) { + if (state.isNode()) { + destroy((NodeState) state); + } else { + destroy((PropertyState) state); + } + } + for (ItemState state : changeLog.addedStates()) { + if (state.isNode()) { + store((NodeState) state); + } else { + store((PropertyState) state); + } + } + for (ItemState state : changeLog.modifiedStates()) { + if (state.isNode()) { + store((NodeState) state); + } else { + store((PropertyState) state); + } + } + for (NodeReferences refs : changeLog.modifiedRefs()) { + if (refs.hasReferences()) { + store(refs); + } else { + if (existsReferencesTo(refs.getTargetId())) { + destroy(refs); + } + } + } + } + + /** + * This implementation does nothing. + * + * {@inheritDoc} + */ + public void checkConsistency(String[] uuids, boolean recursive, boolean fix) { + } + + /** + * Store a node state. Subclass responsibility. + * + * @param state node state to store + * @throws ItemStateException if an error occurs + */ + protected abstract void store(NodeState state) throws ItemStateException; + + /** + * Store a property state. Subclass responsibility. + * + * @param state property state to store + * @throws ItemStateException if an error occurs + */ + protected abstract void store(PropertyState state) throws ItemStateException; + + /** + * Store a references object. Subclass responsibility. + * + * @param refs references object to store + * @throws ItemStateException if an error occurs + */ + protected abstract void store(NodeReferences refs) throws ItemStateException; + + /** + * Destroy a node state. Subclass responsibility. + * + * @param state node state to destroy + * @throws ItemStateException if an error occurs + */ + protected abstract void destroy(NodeState state) throws ItemStateException; + + /** + * Destroy a property state. Subclass responsibility. + * + * @param state property state to destroy + * @throws ItemStateException if an error occurs + */ + protected abstract void destroy(PropertyState state) throws ItemStateException; + + /** + * Destroy a node references object. Subclass responsibility. + * + * @param refs node references object to destroy + * @throws ItemStateException if an error occurs + */ + protected abstract void destroy(NodeReferences refs) + throws ItemStateException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/CachingPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/CachingPersistenceManager.java new file mode 100644 index 00000000000..6936f394231 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/CachingPersistenceManager.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.core.persistence; + +import org.apache.jackrabbit.core.state.ChangeLog; + +/** + * Marker interface that is used by the shared item state manager for invalidate + * persistence manager caches as result of an external (cluster) modification. + */ +public interface CachingPersistenceManager { + + /** + * Notifies the persistence manager that an external (cluster) modification + * occurred. + * + * @param changes the set of changes of the external modification. + */ + void onExternalUpdate(ChangeLog changes); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/IterablePersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/IterablePersistenceManager.java new file mode 100644 index 00000000000..73d63dd7aff --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/IterablePersistenceManager.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.core.persistence; + +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.persistence.util.NodeInfo; +import org.apache.jackrabbit.core.state.ItemStateException; + +import javax.jcr.RepositoryException; + +/** + * The iterable persistence manager can return the list of {@link NodeId}s and + * {@link NodeInfo}s that are stored. + * Possible applications are backup, migration (copying a workspace or repository), + * data store garbage collection, and consistency checking. + */ +public interface IterablePersistenceManager extends PersistenceManager { + + /** + * Get all node ids. + * A typical application will call this method multiple times, where 'after' + * is the last row read previously. The maxCount parameter defines the maximum number of + * node ids returned, 0 meaning no limit. The order of the node ids is specific for the + * given persistent manager. Items that are added concurrently may not be included. + * + * @param after the lower limit, or null for no limit. + * @param maxCount the maximum number of node ids to return, or 0 for no limit. + * @return a list of all node ids. + * @throws ItemStateException if an error while loading occurs. + * @throws RepositoryException if a repository exception occurs. + */ + List getAllNodeIds(NodeId after, int maxCount) + throws ItemStateException, RepositoryException; + + + /** + * Get all {@link NodeInfo}s. + * A typical application will call this method multiple time, where 'after' + * is the last row read previously. The maxCount parameter defines the maximum number of + * node ids returned, 0 meaning no limit. The order of the node ids is specific for the + * given persistence manager. Items that are added concurrently may not be included. + * + * @param after the lower limit, or null for no limit. + * @param maxCount the maximum number of node infos to return, or 0 for no limit. + * @return a list of all node infos. + * @throws ItemStateException if an error while loading occurs. + * @throws RepositoryException if a repository exception occurs. + */ + Map getAllNodeInfos(NodeId after, int maxCount) + throws ItemStateException, RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PMContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PMContext.java new file mode 100644 index 00000000000..272bae85fc5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PMContext.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence; + +import java.io.File; + +import javax.jcr.NamespaceRegistry; + +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; + +/** + * A PMContext is used to provide context information for a + * PersistenceManager. + * + * @see PersistenceManager#init(PMContext) + */ +public class PMContext { + + /** + * the physical home dir + */ + private final File physicalHomeDir; + + /** + * the virtual jackrabbit filesystem + */ + private final FileSystem fs; + + /** + * namespace registry + */ + private final NamespaceRegistry nsReg; + + /** + * node type registry + */ + private final NodeTypeRegistry ntReg; + + /** + * uuid of the root node + */ + private final NodeId rootNodeId; + + /** + * Data store for binary properties. + */ + private final DataStore dataStore; + + /** Repository statistics collector. */ + private final RepositoryStatisticsImpl stats; + + /** + * Creates a new PMContext. + * + * @param homeDir the physical home directory + * @param fs the virtual jackrabbit filesystem + * @param rootNodeId id of the root node + * @param nsReg namespace registry + * @param ntReg node type registry + */ + public PMContext(File homeDir, + FileSystem fs, + NodeId rootNodeId, + NamespaceRegistry nsReg, + NodeTypeRegistry ntReg, + DataStore dataStore, + RepositoryStatisticsImpl stats) { + this.physicalHomeDir = homeDir; + this.fs = fs; + this.rootNodeId = rootNodeId; + this.nsReg = nsReg; + this.ntReg = ntReg; + this.dataStore = dataStore; + this.stats = stats; + } + + + /** + * Returns the physical home directory for this persistence manager + * @return the physical home directory for this persistence manager + */ + public File getHomeDir() { + return physicalHomeDir; + } + + /** + * Returns the virtual filesystem for this persistence manager + * @return the virtual filesystem for this persistence manager + */ + public FileSystem getFileSystem() { + return fs; + } + + /** + * Returns the id of the root node + * @return the id of the root node + */ + public NodeId getRootNodeId() { + return rootNodeId; + } + + /** + * Returns the namespace registry + * + * @return the namespace registry + */ + public NamespaceRegistry getNamespaceRegistry() { + return nsReg; + } + + /** + * Returns the node type registry + * + * @return the node type registry + */ + public NodeTypeRegistry getNodeTypeRegistry() { + return ntReg; + } + + /** + * Returns the data store + * + * @return the data store + */ + public DataStore getDataStore() { + return dataStore; + } + + + /** + * Returns the repository statistics collector. + * + * @return repository statistics + */ + public RepositoryStatisticsImpl getRepositoryStatistics() { + return stats; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PersistenceCopier.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PersistenceCopier.java new file mode 100644 index 00000000000..152bf70f69a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PersistenceCopier.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.core.persistence; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; + +/** + * Tool for copying item states from one persistence manager to another. + * Used for backing up or migrating repository content. + * + * @since Apache Jackrabbit 1.6 + */ +public class PersistenceCopier { + + /** + * Source persistence manager. + */ + private final PersistenceManager source; + + /** + * Target persistence manager. + */ + private final PersistenceManager target; + + /** + * Target data store, possibly null. + */ + private final DataStore store; + + /** + * Identifiers of the nodes that have already been copied or that + * should explicitly not be copied. Used to avoid duplicate copies + * of shareable nodes and to avoid trying to copy "missing" nodes + * like the virtual "/jcr:system" node. + */ + private final Set exclude = new HashSet(); + + /** + * Creates a tool for copying content from one persistence manager + * to another. + * + * @param source source persistence manager + * @param target target persistence manager + * @param store target data store + */ + public PersistenceCopier( + PersistenceManager source, PersistenceManager target, + DataStore store) { + this.source = source; + this.target = target; + this.store = store; + } + + /** + * Explicitly exclude the identified node from being copied. Used for + * excluding virtual nodes like "/jcr:system" from the copy process. + * + * @param id identifier of the node to be excluded + */ + public void excludeNode(NodeId id) { + exclude.add(id); + } + + /** + * Recursively copies the identified node and all its descendants. + * Explicitly excluded nodes and nodes that have already been copied + * are automatically skipped. + * + * @param id identifier of the node to be copied + * @throws RepositoryException if the copy operation fails + */ + public void copy(NodeId id) throws RepositoryException { + if (!exclude.contains(id)) { + try { + NodeState node = source.load(id); + + for (ChildNodeEntry entry : node.getChildNodeEntries()) { + copy(entry.getId()); + } + + copy(node); + exclude.add(id); + } catch (ItemStateException e) { + throw new RepositoryException("Unable to copy " + id, e); + } + } + } + + /** + * Copies the given node state and all associated property states + * to the target persistence manager. + * + * @param sourceNode source node state + * @throws RepositoryException if the copy operation fails + */ + private void copy(NodeState sourceNode) throws RepositoryException { + try { + ChangeLog changes = new ChangeLog(); + + // Copy the node state + NodeState targetNode = target.createNew(sourceNode.getNodeId()); + targetNode.setParentId(sourceNode.getParentId()); + targetNode.setNodeTypeName(sourceNode.getNodeTypeName()); + targetNode.setMixinTypeNames(sourceNode.getMixinTypeNames()); + targetNode.setPropertyNames(sourceNode.getPropertyNames()); + targetNode.setChildNodeEntries(sourceNode.getChildNodeEntries()); + if (target.exists(targetNode.getNodeId())) { + changes.modified(targetNode); + } else { + changes.added(targetNode); + } + + // Copy all associated property states + for (Name name : sourceNode.getPropertyNames()) { + PropertyId id = new PropertyId(sourceNode.getNodeId(), name); + PropertyState sourceState = source.load(id); + PropertyState targetState = target.createNew(id); + targetState.setType(sourceState.getType()); + targetState.setMultiValued(sourceState.isMultiValued()); + InternalValue[] values = sourceState.getValues(); + if (sourceState.getType() == PropertyType.BINARY) { + for (int i = 0; i < values.length; i++) { + InputStream stream = values[i].getStream(); + try { + values[i] = InternalValue.create(stream, store); + } finally { + stream.close(); + } + } + } + targetState.setValues(values); + if (target.exists(targetState.getPropertyId())) { + changes.modified(targetState); + } else { + changes.added(targetState); + } + } + + // Copy all node references + if (source.existsReferencesTo(sourceNode.getNodeId())) { + changes.modified(source.loadReferencesTo(sourceNode.getNodeId())); + } else if (target.existsReferencesTo(sourceNode.getNodeId())) { + NodeReferences references = + target.loadReferencesTo(sourceNode.getNodeId()); + references.clearAllReferences(); + changes.modified(references); + } + + // Persist the copied states + target.store(changes); + } catch (IOException e) { + throw new RepositoryException( + "Unable to copy binary values of " + sourceNode, e); + } catch (ItemStateException e) { + throw new RepositoryException("Unable to copy " + sourceNode, e); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PersistenceManager.java new file mode 100644 index 00000000000..736bd070bf7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/PersistenceManager.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.core.persistence; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.ChangeLog; + +/** + * Persistence manager interface. Persistence managers are + * internal Jackrabbit components that handle the persistent + * storage of content nodes and properties. A persistence + * manager knows how to retrieve the persistent states of + * content items and how to atomically save a set of changes + * to the persistent state. + *

    + * Each workspace of a Jackrabbit content repository uses separate + * persistence manager to store the content in that workspace. Also + * the Jackrabbit version handler uses a separate persistence manager. + * The persistence managers in use are configured in the Jackrabbit + * XML configuration files. The configured persistence managers are + * instantiated and initialized using the JavaBeans conventions. + * + *

    Persistence manager life cycle

    + *

    + * The life cycle of a persistence manager instance contains four phases: + *

      + *
    1. Instantiation, where the instance is created and possible + * configuration properties are set using the JavaBean conventions. + * During this phase the persistence manager should not attempt to + * reference any external resources. + *
    2. Initialization, where the {@link #init(PMContext) init} method + * is invoked to bind the persistence manager with a given + * {@link PMContext context}. + *
    3. Normal usage, where the various create, load, exists, and store + * methods of the persistence manager are used to manage the + * persistent content items. + *
    4. Closing, where the {@link #close() close} method is invoked + * to close the persistence manager and release all acquired + * resources. + *
    + * + *

    Concurrency

    + *

    + * A persistence manager instance should be thread-safe and guarantee that + * the {@link #store(ChangeLog)} method calls are atomic. Any load() or + * exists() calls started after a store() call has returned must access + * the updated content. The client accessing a persistence manager must + * guarantee that no two concurrent {@link #store(ChangeLog)} calls will + * modify the same items. + */ +public interface PersistenceManager { + + /** + * Initializes the persistence manager. The persistence manager is + * permanently bound to the given context, and any required external + * resources are acquired. + *

    + * An appropriate exception is thrown if the persistence manager + * initialization fails for whatever reason. In this case the + * state of the persistence manager is undefined and the instance + * should be discarded. + * + * @param context persistence manager context + * @throws Exception if the persistence manager initialization failed + */ + void init(PMContext context) throws Exception; + + /** + * Closes the persistence manager. The consistency of the persistent + * storage is guaranteed and all acquired resources are released. + * It is an error to invoke any methods on a closed persistence manager, + * and implementations are free to enforce this constraint by throwing + * IllegalStateExceptions in such cases. + *

    + * An appropriate exception is thrown if the persistence manager + * could not be closed properly. In this case the state of the + * persistence manager is undefined and the instance should be + * discarded. + * + * @throws Exception if the persistence manager failed to close properly + */ + void close() throws Exception; + + /** + * Creates a new node state instance with the given id. + * + * @param id node id + * @return node state instance + */ + NodeState createNew(NodeId id); + + /** + * Creates a new property state instance with the given id. + * + * @param id property id + * @return property state instance + */ + PropertyState createNew(PropertyId id); + + /** + * Load the persistent members of a node state. + * + * @param id node id + * @return loaded node state + * @throws NoSuchItemStateException if the node state does not exist + * @throws ItemStateException if another error occurs + */ + NodeState load(NodeId id) + throws NoSuchItemStateException, ItemStateException; + + /** + * Load the persistent members of a property state. + * + * @param id property id + * @return loaded property state + * @throws NoSuchItemStateException if the property state does not exist + * @throws ItemStateException if another error occurs + */ + PropertyState load(PropertyId id) + throws NoSuchItemStateException, ItemStateException; + + /** + * Load the persisted references to the node with the given identifier. + * + * @param id reference target node id + * @throws NoSuchItemStateException if the target node does not exist + * @throws ItemStateException if another error occurs + */ + NodeReferences loadReferencesTo(NodeId id) + throws NoSuchItemStateException, ItemStateException; + + /** + * Checks whether the identified node exists. + * + * @param id node id + * @return true if the node exists, + * false otherwise + * @throws ItemStateException on persistence manager errors + */ + boolean exists(NodeId id) throws ItemStateException; + + /** + * Checks whether the identified property exists. + * + * @param id property id + * @return true if the property exists, + * false otherwise + * @throws ItemStateException on persistence manager errors + */ + boolean exists(PropertyId id) throws ItemStateException; + + /** + * Checks whether references of the identified target node exist. + * + * @param targetId target node id + * @return true if the references exist, + * false otherwise + * @throws ItemStateException on persistence manager errors + */ + boolean existsReferencesTo(NodeId targetId) throws ItemStateException; + + /** + * Atomically saves the given set of changes. + * + * @param changeLog change log containing states that were changed + * @throws ItemStateException if the changes could not be saved + */ + void store(ChangeLog changeLog) throws ItemStateException; + + /** + * Perform a consistency check of the data. An example are non-existent + * nodes referenced in a child node entry. The existence of this feature and + * the scope of the implementation can vary in different PersistenceManager + * implementations. + * + * @param uuids + * list of UUIDs of nodes to be checked. if null, all nodes will + * be checked + * @param recursive + * if true, the tree(s) below the given node(s) will be traversed + * and checked as well + * @param fix + * if true, any problems found that can be repaired will be + * repaired. if false, no data will be modified, instead all + * inconsistencies will only get logged + */ + void checkConsistency(String[] uuids, boolean recursive, boolean fix); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/AbstractBundlePersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/AbstractBundlePersistenceManager.java new file mode 100644 index 00000000000..38cc0236215 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/AbstractBundlePersistenceManager.java @@ -0,0 +1,884 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.bundle; + +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_UUID; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.stats.RepositoryStatistics; +import org.apache.jackrabbit.core.cache.Cache; +import org.apache.jackrabbit.core.cache.CacheAccessListener; +import org.apache.jackrabbit.core.cache.ConcurrentCache; +import org.apache.jackrabbit.core.cluster.UpdateEventChannel; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.persistence.CachingPersistenceManager; +import org.apache.jackrabbit.core.persistence.IterablePersistenceManager; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.persistence.check.ConsistencyCheckListener; +import org.apache.jackrabbit.core.persistence.check.ConsistencyChecker; +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.persistence.util.FileBasedIndex; +import org.apache.jackrabbit.core.persistence.util.NodeInfo; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle.PropertyEntry; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; +import org.apache.jackrabbit.core.util.StringIndex; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The AbstractBundlePersistenceManager acts as base for all + * persistence managers that store the state in a {@link NodePropBundle}. + *

    + * The state and all property states of one node are stored together in one + * record. Property values of a certain size can be stored outside of the bundle. + * This currently only works for binary properties. NodeReferences are not + * included in the bundle since they are addressed by the target id. + *

    + * Some strings like namespaces and local names are additionally managed by + * separate indexes. only the index number is serialized to the records which + * reduces the amount of memory used. + *

    + * Special treatment is performed for the properties "jcr:uuid", "jcr:primaryType" + * and "jcr:mixinTypes". As they are also stored in the node state they are not + * included in the bundle but generated when required. + *

    + * In order to increase performance, there are two caches being maintained. One is the + * bundle cache that caches already loaded bundles. The other is the + * {@code LRUNodeIdCache} that caches non-existent bundles. This is useful + * because a lot of {@link #exists(NodeId)} calls are issued that would result + * in a useless persistence lookup if the desired bundle does not exist. + *

    + * Configuration:
    + *

      + *
    • <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/> + *
    + */ +public abstract class AbstractBundlePersistenceManager implements + PersistenceManager, CachingPersistenceManager, IterablePersistenceManager, CacheAccessListener, ConsistencyChecker { + + /** the audit logger */ + private static Logger auditLogger = LoggerFactory.getLogger("org.apache.jackrabbit.core.audit"); + + /** the default logger */ + private static Logger log = LoggerFactory.getLogger(AbstractBundlePersistenceManager.class); + + /** the prefix of a node file */ + protected static final String NODEFILENAME = "n"; + + /** the prefix of a node references file */ + protected static final String NODEREFSFILENAME = "r"; + + /** the name of the names-index resource */ + protected static final String RES_NAME_INDEX = "/names.properties"; + + /** the name of the namespace-index resource */ + protected static final String RES_NS_INDEX = "/namespaces.properties"; + + /** Sentinel instance used to mark a non-existent bundle in the cache */ + private static final NodePropBundle MISSING = + new NodePropBundle(NodeId.randomId()); + + /** + * The size estimate for the MISSING NodePropBundle. The sum of: + * - ConcurrentCache.E: 32 bytes + * - LinkedHashMap.Entry: 64 bytes + * - NodeId: 32 bytes + */ + private static final long MISSING_SIZE_ESTIMATE = 128; + + /** the index for namespaces */ + private StringIndex nsIndex; + + /** the index for local names */ + private StringIndex nameIndex; + + /** the cache of loaded bundles */ + private ConcurrentCache bundles; + + /** The default minimum stats logging interval (in ms). */ + private static final int DEFAULT_LOG_STATS_INTERVAL = 60 * 1000; + + /** The minimum interval time between stats are logged */ + private long minLogStatsInterval = Long.getLong( + "org.apache.jackrabbit.cacheLogStatsInterval", + DEFAULT_LOG_STATS_INTERVAL); + + /** The last time the cache stats were logged. */ + private volatile long nextLogStats = + System.currentTimeMillis() + DEFAULT_LOG_STATS_INTERVAL; + + /** the persistence manager context */ + protected PMContext context; + + /** default size of the bundle cache */ + private long bundleCacheSize = 8 * 1024 * 1024; + + /** Counter of read operations. */ + private AtomicLong readCounter; + + /** Counter of write operations. */ + private AtomicLong writeCounter; + + /** Duration of write operations. */ + private AtomicLong writeDuration; + + /** Counter of bundle cache accesses. */ + private AtomicLong cacheAccessCounter; + + /** Counter of bundle read operations. */ + private AtomicLong cacheMissCounter; + + /** Duration of bundle read operations. */ + private AtomicLong cacheMissDuration; + + /** Counter of bundle cache size. */ + private AtomicLong cacheSizeCounter; + + /** The update event channel to use by the consistency checker when fixing inconsistencies */ + private UpdateEventChannel eventChannel; + + /** + * Returns the size of the bundle cache in megabytes. + * @return the size of the bundle cache in megabytes. + */ + public String getBundleCacheSize() { + return String.valueOf(bundleCacheSize / (1024 * 1024)); + } + + /** + * Sets the size of the bundle cache in megabytes. + * the default is 8. + * + * @param bundleCacheSize the bundle cache size in megabytes. + */ + public void setBundleCacheSize(String bundleCacheSize) { + this.bundleCacheSize = Long.parseLong(bundleCacheSize) * 1024 * 1024; + } + + /** + * Creates the folder path for the given node id that is suitable for + * storing states in a filesystem. + * + * @param buf buffer to append to or null + * @param id the id of the node + * @return the buffer with the appended data. + */ + protected StringBuffer buildNodeFolderPath(StringBuffer buf, NodeId id) { + if (buf == null) { + buf = new StringBuffer(); + } + char[] chars = id.toString().toCharArray(); + int cnt = 0; + for (int i = 0; i < chars.length; i++) { + if (chars[i] == '-') { + continue; + } + //if (cnt > 0 && cnt % 4 == 0) { + if (cnt == 2 || cnt == 4) { + buf.append(FileSystem.SEPARATOR_CHAR); + } + buf.append(chars[i]); + cnt++; + } + return buf; + } + + /** + * Creates the folder path for the given property id that is suitable for + * storing states in a filesystem. + * + * @param buf buffer to append to or null + * @param id the id of the property + * @return the buffer with the appended data. + */ + protected StringBuffer buildPropFilePath(StringBuffer buf, PropertyId id) { + if (buf == null) { + buf = new StringBuffer(); + } + buildNodeFolderPath(buf, id.getParentId()); + buf.append(FileSystem.SEPARATOR); + buf.append(getNsIndex().stringToIndex(id.getName().getNamespaceURI())); + buf.append('.'); + buf.append(getNameIndex().stringToIndex(id.getName().getLocalName())); + return buf; + } + + /** + * Creates the file path for the given property id and value index that is + * suitable for storing property values in a filesystem. + * + * @param buf buffer to append to or null + * @param id the id of the property + * @param i the index of the property value + * @return the buffer with the appended data. + */ + protected StringBuffer buildBlobFilePath(StringBuffer buf, PropertyId id, + int i) { + if (buf == null) { + buf = new StringBuffer(); + } + buildPropFilePath(buf, id); + buf.append('.'); + buf.append(i); + return buf; + } + + /** + * Creates the file path for the given node id that is + * suitable for storing node states in a filesystem. + * + * @param buf buffer to append to or null + * @param id the id of the node + * @return the buffer with the appended data. + */ + protected StringBuffer buildNodeFilePath(StringBuffer buf, NodeId id) { + if (buf == null) { + buf = new StringBuffer(); + } + buildNodeFolderPath(buf, id); + buf.append(FileSystem.SEPARATOR); + buf.append(NODEFILENAME); + return buf; + } + + /** + * Creates the file path for the given references id that is + * suitable for storing reference states in a filesystem. + * + * @param buf buffer to append to or null + * @param id the id of the node + * @return the buffer with the appended data. + */ + protected StringBuffer buildNodeReferencesFilePath( + StringBuffer buf, NodeId id) { + if (buf == null) { + buf = new StringBuffer(); + } + buildNodeFolderPath(buf, id); + buf.append(FileSystem.SEPARATOR); + buf.append(NODEREFSFILENAME); + return buf; + } + + /** + * Returns the namespace index + * @return the namespace index + * @throws IllegalStateException if an error occurs. + */ + public StringIndex getNsIndex() { + try { + if (nsIndex == null) { + // load name and ns index + FileSystemResource nsFile = new FileSystemResource(context.getFileSystem(), RES_NS_INDEX); + if (nsFile.exists()) { + nsIndex = new FileBasedIndex(nsFile); + } else { + nsIndex = (StringIndex) context.getNamespaceRegistry(); + } + } + return nsIndex; + } catch (Exception e) { + IllegalStateException e2 = new IllegalStateException("Unable to create nsIndex."); + e2.initCause(e); + throw e2; + } + } + + /** + * Returns the local name index + * @return the local name index + * @throws IllegalStateException if an error occurs. + */ + public StringIndex getNameIndex() { + try { + if (nameIndex == null) { + nameIndex = new FileBasedIndex(new FileSystemResource( + context.getFileSystem(), RES_NAME_INDEX)); + } + return nameIndex; + } catch (Exception e) { + IllegalStateException e2 = new IllegalStateException("Unable to create nameIndex."); + e2.initCause(e); + throw e2; + } + } + + //-----------------------------------------< CacheablePersistenceManager >-- + + /** + * {@inheritDoc} + */ + public synchronized void onExternalUpdate(ChangeLog changes) { + for (ItemState state : changes.modifiedStates()) { + bundles.remove(getBundleId(state)); + } + for (ItemState state : changes.deletedStates()) { + bundles.remove(getBundleId(state)); + } + for (ItemState state : changes.addedStates()) { + // There may have been a cache miss entry + bundles.remove(getBundleId(state)); + } + } + + private NodeId getBundleId(ItemState state) { + if (state.isNode()) { + return (NodeId) state.getId(); + } else { + return state.getParentId(); + } + } + + //------------------------------------------< IterablePersistenceManager >-- + + @Override + public Map getAllNodeInfos(NodeId after, int maxCount) + throws ItemStateException, RepositoryException { + Map infos = new LinkedHashMap(); + for (NodeId nodeId : getAllNodeIds(after, maxCount)) { + infos.put(nodeId, new NodeInfo(loadBundle(nodeId))); + } + return infos; + } + + //----------------------------------------------------------------< spi >--- + + /** + * Loads a bundle from the underlying system. + * + * @param id the node id of the bundle + * @return the loaded bundle or null if the bundle does not + * exist. + * @throws ItemStateException if an error while loading occurs. + */ + protected abstract NodePropBundle loadBundle(NodeId id) + throws ItemStateException; + + /** + * Stores a bundle to the underlying system. + * + * @param bundle the bundle to store + * @throws ItemStateException if an error while storing occurs. + */ + protected abstract void storeBundle(NodePropBundle bundle) + throws ItemStateException; + + /** + * Deletes the bundle from the underlying system. + * + * @param bundle the bundle to destroy + * + * @throws ItemStateException if an error while destroying occurs. + */ + protected abstract void destroyBundle(NodePropBundle bundle) + throws ItemStateException; + + /** + * Deletes the node references from the underlying system. + * + * @param refs the node references to destroy. + * @throws ItemStateException if an error while destroying occurs. + */ + protected abstract void destroy(NodeReferences refs) + throws ItemStateException; + + /** + * Stores a node references to the underlying system. + * + * @param refs the node references to store. + * @throws ItemStateException if an error while storing occurs. + */ + protected abstract void store(NodeReferences refs) + throws ItemStateException; + + /** + * Returns the BLOB store used by this persistence manager. + * + * @return BLOB store + */ + protected abstract BLOBStore getBlobStore(); + + //-------------------------------------------------< PersistenceManager >--- + + /** + * {@inheritDoc} + * + * Initializes the internal structures of this abstract persistence manager. + */ + public void init(PMContext context) throws Exception { + this.context = context; + // init bundle cache + bundles = new ConcurrentCache(context.getHomeDir().getName() + "BundleCache"); + bundles.setMaxMemorySize(bundleCacheSize); + bundles.setAccessListener(this); + + // statistics + RepositoryStatisticsImpl stats = context.getRepositoryStatistics(); + readCounter = stats.getCounter( + RepositoryStatistics.Type.BUNDLE_READ_COUNTER); + writeCounter = stats.getCounter( + RepositoryStatistics.Type.BUNDLE_WRITE_COUNTER); + writeDuration = stats.getCounter( + RepositoryStatistics.Type.BUNDLE_WRITE_DURATION); + cacheAccessCounter = stats.getCounter( + RepositoryStatistics.Type.BUNDLE_CACHE_ACCESS_COUNTER); + cacheSizeCounter = stats.getCounter( + RepositoryStatistics.Type.BUNDLE_CACHE_SIZE_COUNTER); + cacheMissCounter = stats.getCounter( + RepositoryStatistics.Type.BUNDLE_CACHE_MISS_COUNTER); + cacheMissDuration = stats.getCounter( + RepositoryStatistics.Type.BUNDLE_CACHE_MISS_DURATION); + } + + /** + * {@inheritDoc} + * + * Closes the persistence manager, release acquired resources. + */ + public void close() throws Exception { + // clear caches + bundles.clear(); + } + + /** + * {@inheritDoc} + * + * Loads the state via the appropriate NodePropBundle. + */ + public NodeState load(NodeId id) throws NoSuchItemStateException, ItemStateException { + NodePropBundle bundle = getBundle(id); + if (bundle == null) { + throw new NoSuchItemStateException(id.toString()); + } + return bundle.createNodeState(this); + } + + /** + * {@inheritDoc} + * + * Loads the state via the appropriate NodePropBundle. + */ + public PropertyState load(PropertyId id) throws NoSuchItemStateException, ItemStateException { + NodePropBundle bundle = getBundle(id.getParentId()); + if (bundle != null) { + PropertyState state = createNew(id); + PropertyEntry p = bundle.getPropertyEntry(id.getName()); + if (p != null) { + state.setMultiValued(p.isMultiValued()); + state.setType(p.getType()); + state.setValues(p.getValues()); + state.setModCount(p.getModCount()); + } else if (id.getName().equals(JCR_UUID)) { + state.setType(PropertyType.STRING); + state.setMultiValued(false); + state.setValues(new InternalValue[] { + InternalValue.create(id.getParentId().toString()) }); + } else if (id.getName().equals(JCR_PRIMARYTYPE)) { + state.setType(PropertyType.NAME); + state.setMultiValued(false); + state.setValues(new InternalValue[] { + InternalValue.create(bundle.getNodeTypeName()) }); + } else if (id.getName().equals(JCR_MIXINTYPES)) { + state.setType(PropertyType.NAME); + state.setMultiValued(true); + Set mixins = bundle.getMixinTypeNames(); + state.setValues(InternalValue.create( + mixins.toArray(new Name[mixins.size()]))); + } else { + throw new NoSuchItemStateException(id.toString()); + } + return state; + } else { + throw new NoSuchItemStateException(id.toString()); + } + } + + /** + * {@inheritDoc} + * + * Loads the state via the appropriate NodePropBundle. + */ + public boolean exists(PropertyId id) throws ItemStateException { + NodePropBundle bundle = getBundle(id.getParentId()); + if (bundle != null) { + Name name = id.getName(); + return bundle.hasProperty(name) + || JCR_PRIMARYTYPE.equals(name) + || (JCR_UUID.equals(name) && bundle.isReferenceable()) + || (JCR_MIXINTYPES.equals(name) + && !bundle.getMixinTypeNames().isEmpty()); + } else { + return false; + } + } + + /** + * {@inheritDoc} + * + * Checks the existence via the appropriate NodePropBundle. + */ + public boolean exists(NodeId id) throws ItemStateException { + // anticipating a load followed by a exists + return getBundle(id) != null; + } + + /** + * {@inheritDoc} + */ + public NodeState createNew(NodeId id) { + return new NodeState(id, null, null, NodeState.STATUS_NEW, false); + } + + /** + * {@inheritDoc} + */ + public PropertyState createNew(PropertyId id) { + return new PropertyState(id, PropertyState.STATUS_NEW, false); + } + + /** + * Right now, this iterates over all items in the changelog and + * calls the individual methods that handle single item states + * or node references objects. Properly implemented, this method + * should ensure that changes are either written completely to + * the underlying persistence layer, or not at all. + * + * {@inheritDoc} + */ + public synchronized void store(ChangeLog changeLog) + throws ItemStateException { + boolean success = false; + try { + storeInternal(changeLog); + success = true; + } finally { + if (!success) { + bundles.clear(); + } + } + } + + /** + * Stores the given changelog and updates the bundle cache. + * + * @param changeLog the changelog to store + * @throws ItemStateException on failure + */ + private void storeInternal(ChangeLog changeLog) + throws ItemStateException { + // delete bundles + HashSet deleted = new HashSet(); + for (ItemState state : changeLog.deletedStates()) { + if (state.isNode()) { + NodePropBundle bundle = getBundle((NodeId) state.getId()); + if (bundle == null) { + throw new NoSuchItemStateException(state.getId().toString()); + } + deleteBundle(bundle); + deleted.add(state.getId()); + } + } + // gather added node states + HashMap modified = new HashMap(); + for (ItemState state : changeLog.addedStates()) { + if (state.isNode()) { + NodePropBundle bundle = new NodePropBundle((NodeState) state); + modified.put(state.getId(), bundle); + } + } + // gather modified node states + for (ItemState state : changeLog.modifiedStates()) { + if (state.isNode()) { + NodeId nodeId = (NodeId) state.getId(); + NodePropBundle bundle = modified.get(nodeId); + if (bundle == null) { + bundle = getBundle(nodeId); + if (bundle == null) { + throw new NoSuchItemStateException(nodeId.toString()); + } + modified.put(nodeId, bundle); + } + bundle.update((NodeState) state); + } else { + PropertyId id = (PropertyId) state.getId(); + // skip redundant primaryType and uuid properties + if (id.getName().equals(JCR_PRIMARYTYPE) + || id.getName().equals(JCR_UUID)) { + continue; + } + NodeId nodeId = id.getParentId(); + NodePropBundle bundle = modified.get(nodeId); + if (bundle == null) { + bundle = getBundle(nodeId); + if (bundle == null) { + throw new NoSuchItemStateException(nodeId.toString()); + } + modified.put(nodeId, bundle); + } + bundle.addProperty((PropertyState) state, getBlobStore()); + } + } + // add removed properties + for (ItemState state : changeLog.deletedStates()) { + if (state.isNode()) { + // check consistency + NodeId parentId = state.getParentId(); + if (!modified.containsKey(parentId) && !deleted.contains(parentId)) { + log.warn("Deleted node state's parent is not modified or deleted: " + parentId + "/" + state.getId()); + } + } else { + PropertyId id = (PropertyId) state.getId(); + NodeId nodeId = id.getParentId(); + if (!deleted.contains(nodeId)) { + NodePropBundle bundle = modified.get(nodeId); + if (bundle == null) { + // should actually not happen + log.warn("deleted property state's parent not modified!"); + bundle = getBundle(nodeId); + if (bundle == null) { + throw new NoSuchItemStateException(nodeId.toString()); + } + modified.put(nodeId, bundle); + } + bundle.removeProperty(id.getName(), getBlobStore()); + } + } + } + // add added properties + for (ItemState state : changeLog.addedStates()) { + if (!state.isNode()) { + PropertyId id = (PropertyId) state.getId(); + // skip primaryType and uuid properties + if (id.getName().equals(JCR_PRIMARYTYPE) + || id.getName().equals(JCR_UUID)) { + continue; + } + NodeId nodeId = id.getParentId(); + NodePropBundle bundle = modified.get(nodeId); + if (bundle == null) { + // should actually not happen + log.warn("added property state's parent not modified!"); + bundle = getBundle(nodeId); + if (bundle == null) { + throw new NoSuchItemStateException(nodeId.toString()); + } + modified.put(nodeId, bundle); + } + bundle.addProperty((PropertyState) state, getBlobStore()); + } + } + + // now store all modified bundles + long updateSize = 0; + for (NodePropBundle bundle : modified.values()) { + putBundle(bundle); + updateSize += bundle.getSize(); + } + changeLog.setUpdateSize(updateSize); + + // store the refs + for (NodeReferences refs : changeLog.modifiedRefs()) { + if (refs.hasReferences()) { + store(refs); + } else { + destroy(refs); + } + } + } + + /** + * Gets the bundle for the given node id. Read/write synchronization + * happens higher up at the SISM level, so we don't need to worry about + * conflicts here. + * + * @param id the id of the bundle to retrieve. + * @return the bundle or null if the bundle does not exist + * + * @throws ItemStateException if an error occurs. + */ + private NodePropBundle getBundle(NodeId id) throws ItemStateException { + NodePropBundle bundle = bundles.get(id); + readCounter.incrementAndGet(); + if (bundle == MISSING) { + return null; + } + if (bundle != null) { + return bundle; + } + // cache miss + return getBundleCacheMiss(id); + } + + /** + * Called when the bundle is not present in the cache, so we'll need to load + * it from the PM impl. + * + * This also updates the cache. + * + * @param id + * @return + * @throws ItemStateException + */ + private NodePropBundle getBundleCacheMiss(NodeId id) + throws ItemStateException { + long time = System.nanoTime(); + NodePropBundle bundle = loadBundle(id); + time = System.nanoTime() - time; + cacheMissDuration.addAndGet(time); + final long timeMs = time / 1000000; + log.debug("Loaded bundle {} in {}ms", id, timeMs); + cacheMissCounter.incrementAndGet(); + if (bundle != null) { + bundle.markOld(); + bundles.put(id, bundle, bundle.getSize()); + } else { + bundles.put(id, MISSING, MISSING_SIZE_ESTIMATE); + } + return bundle; + } + + /** + * Deletes the bundle + * + * @param bundle the bundle to delete + * @throws ItemStateException if an error occurs + */ + private void deleteBundle(NodePropBundle bundle) throws ItemStateException { + destroyBundle(bundle); + bundle.removeAllProperties(getBlobStore()); + bundles.put(bundle.getId(), MISSING, MISSING_SIZE_ESTIMATE); + } + + /** + * Stores the bundle and puts it to the cache. + * + * @param bundle the bundle to store + * @throws ItemStateException if an error occurs + */ + private void putBundle(NodePropBundle bundle) throws ItemStateException { + long time = System.nanoTime(); + storeBundle(bundle); + time = System.nanoTime() - time; + if (auditLogger.isDebugEnabled()) { + auditLogger.debug("{} ({})", bundle.getId(), bundle.getSize()); + } + writeDuration.addAndGet(time); + final long timeMs = time / 1000000; + log.debug("Stored bundle {} in {}ms", bundle.getId(), timeMs); + writeCounter.incrementAndGet(); + + bundle.markOld(); + + // only put to cache if already exists. this is to ensure proper + // overwrite and not creating big contention during bulk loads + if (bundles.containsKey(bundle.getId())) { + bundles.put(bundle.getId(), bundle, bundle.getSize()); + } + } + + /** + * {@inheritDoc} + */ + public void checkConsistency(String[] uuids, boolean recursive, boolean fix) { + try { + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(this, null, null, eventChannel); + checker.check(uuids, recursive); + checker.doubleCheckErrors(); + if (fix) { + checker.repair(); + } + } catch (RepositoryException ex) { + log.error("While running consistency check.", ex); + } + } + + public void setEventChannel(UpdateEventChannel eventChannel) { + this.eventChannel = eventChannel; + } + + /** + * {@inheritDoc} + */ + public ConsistencyReport check(String[] uuids, boolean recursive, + boolean fix, String lostNFoundId, ConsistencyCheckListener listener) + throws RepositoryException { + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(this, listener, lostNFoundId, eventChannel); + checker.check(uuids, recursive); + checker.doubleCheckErrors(); + if (fix) { + checker.repair(); + } + return checker.getReport(); + } + + /** + * Evicts the bundle with id from the bundle cache. + * + * @param id the id of the bundle. + */ + protected void evictBundle(NodeId id) { + bundles.remove(id); + } + + public void cacheAccessed(long accessCount) { + logCacheStats(); + cacheAccessCounter.addAndGet(accessCount); + cacheSizeCounter.set(bundles.getMemoryUsed()); + } + + private void logCacheStats() { + if (log.isInfoEnabled()) { + long now = System.currentTimeMillis(); + if (now < nextLogStats) { + return; + } + log.info(bundles.getCacheInfoAsString()); + nextLogStats = now + minLogStatsInterval; + } + } + + public void disposeCache(Cache cache) { + // NOOP + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/BundleFsPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/BundleFsPersistenceManager.java new file mode 100644 index 00000000000..ec1910b3a5f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/BundleFsPersistenceManager.java @@ -0,0 +1,553 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.bundle; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.BasedFileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.persistence.util.BundleBinding; +import org.apache.jackrabbit.core.persistence.util.ErrorHandling; +import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle; +import org.apache.jackrabbit.core.persistence.util.Serializer; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This is a generic persistence manager that stores the {@link NodePropBundle}s + * in a filesystem. + *

    + * Configuration:
    + *

      + *
    • <param name="{@link #setBlobFSBlockSize(String) blobFSBlockSize}" value="0"/> + *
    • <param name="{@link #setMinBlobSize(String) minBlobSize}" value="4096"/> + *
    • <param name="{@link #setErrorHandling(String) errorHandling}" value=""/> + *
    + */ +public class BundleFsPersistenceManager extends AbstractBundlePersistenceManager { + + /** the default logger */ + private static Logger log = LoggerFactory.getLogger(BundleFsPersistenceManager.class); + + /** flag indicating if this manager was initialized */ + protected boolean initialized; + + /** file system where BLOB data is stored */ + protected BundleFsPersistenceManager.CloseableBLOBStore blobStore; + + /** + * Default blocksize for BLOB filesystem: + * @see #setBlobFSBlockSize(String) + */ + private int blobFSBlockSize; + + /** + * the minimum size of a property until it gets written to the blob store + * @see #setMinBlobSize(String) + */ + private int minBlobSize = 0x1000; + + /** + * the filesystem where the items are stored + */ + private FileSystem itemFs; + + /** + * flag for error handling + */ + protected ErrorHandling errorHandling = new ErrorHandling(); + + /** + * the bundle binding + */ + protected BundleBinding binding; + + /** + * the name of this persistence manager + */ + private String name = super.toString(); + + + /** + * Returns the configured block size of the blob cqfs + * @return the block size. + */ + public String getBlobFSBlockSize() { + return String.valueOf(blobFSBlockSize); + } + + /** + * Sets the block size of the blob fs and controls how blobs are handled. + *
    + * If the size is 0, the blobs are stored within the workspace's physical filesystem. + *
    + * Otherwise, the blobs are stored within the item filesystem. + *
    + * Please note that not all binary properties are considered as blobs. They + * are only stored in the respective blob store, if their size exceeds + * {@link #getMinBlobSize()}. + * + * @param size the block size + */ + public void setBlobFSBlockSize(String size) { + this.blobFSBlockSize = Integer.decode(size).intValue(); + } + + /** + * Returns true if the blobs are stored in the local fs. + * @return true if the blobs are stored in the local fs. + */ + public boolean useLocalFsBlobStore() { + return blobFSBlockSize == 0; + } + + /** + * Returns the mininum blob size. + * @return the mininum blob size. + */ + public String getMinBlobSize() { + return String.valueOf(minBlobSize); + } + + /** + * Sets the minimum blob size. This size defines the threshold of which + * size a property is included in the bundle or is stored in the blob store. + * Very high values decrease the performance. + * + * @param minBlobSize + */ + public void setMinBlobSize(String minBlobSize) { + this.minBlobSize = Integer.decode(minBlobSize).intValue(); + } + + /** + * Sets the error handling behaviour of this manager. See {@link ErrorHandling} + * for details about the flags. + * + * @param errorHandling + */ + public void setErrorHandling(String errorHandling) { + this.errorHandling = new ErrorHandling(errorHandling); + } + + /** + * Returns the error handling configuration of this manager + * @return the error handling configuration of this manager + */ + public String getErrorHandling() { + return errorHandling.toString(); + } + + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + super.init(context); + + this.name = context.getHomeDir().getName(); + + // create item fs + itemFs = new BasedFileSystem(context.getFileSystem(), "items"); + + // create correct blob store + if (useLocalFsBlobStore()) { + LocalFileSystem blobFS = new LocalFileSystem(); + blobFS.setRoot(new File(context.getHomeDir(), "blobs")); + blobFS.init(); + blobStore = new BundleFsPersistenceManager.FSBlobStore(blobFS); + } else { + blobStore = new BundleFsPersistenceManager.FSBlobStore(itemFs); + } + + // load namespaces + binding = new BundleBinding(errorHandling, blobStore, getNsIndex(), getNameIndex(), context.getDataStore()); + binding.setMinBlobSize(minBlobSize); + + initialized = true; + } + + /** + * {@inheritDoc} + */ + @Override + protected BLOBStore getBlobStore() { + return blobStore; + } + + /** + * {@inheritDoc} + */ + public synchronized void close() throws Exception { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + // close blob store + blobStore.close(); + blobStore = null; + itemFs.close(); + itemFs = null; + super.close(); + } finally { + initialized = false; + } + } + + /** + * {@inheritDoc} + */ + protected NodePropBundle loadBundle(NodeId id) throws ItemStateException { + try { + String path = buildNodeFilePath(null, id).toString(); + if (!itemFs.exists(path)) { + return null; + } + InputStream in = itemFs.getInputStream(path); + try { + return binding.readBundle(in, id); + } finally { + IOUtils.closeQuietly(in); + } + } catch (Exception e) { + String msg = "failed to read bundle: " + id + ": " + e; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * Creates the file path for the given node id that is + * suitable for storing node states in a filesystem. + * + * @param buf buffer to append to or null + * @param id the id of the node + * @return the buffer with the appended data. + */ + protected StringBuffer buildNodeFilePath(StringBuffer buf, NodeId id) { + if (buf == null) { + buf = new StringBuffer(); + } + buildNodeFolderPath(buf, id); + buf.append('.'); + buf.append(NODEFILENAME); + return buf; + } + + /** + * Creates the file path for the given references id that is + * suitable for storing reference states in a filesystem. + * + * @param buf buffer to append to or null + * @param id the id of the node + * @return the buffer with the appended data. + */ + protected StringBuffer buildNodeReferencesFilePath( + StringBuffer buf, NodeId id) { + if (buf == null) { + buf = new StringBuffer(); + } + buildNodeFolderPath(buf, id); + buf.append('.'); + buf.append(NODEREFSFILENAME); + return buf; + } + + /** + * {@inheritDoc} + */ + protected synchronized void storeBundle(NodePropBundle bundle) throws ItemStateException { + try { + StringBuffer buf = buildNodeFolderPath(null, bundle.getId()); + buf.append('.'); + buf.append(NODEFILENAME); + String fileName = buf.toString(); + String dir = fileName.substring(0, fileName.lastIndexOf(FileSystem.SEPARATOR_CHAR)); + if (!itemFs.exists(dir)) { + itemFs.createFolder(dir); + } + OutputStream out = itemFs.getOutputStream(fileName); + try { + binding.writeBundle(out, bundle); + } finally { + out.close(); + } + } catch (Exception e) { + String msg = "failed to write bundle: " + bundle.getId(); + BundleFsPersistenceManager.log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected synchronized void destroyBundle(NodePropBundle bundle) throws ItemStateException { + try { + StringBuffer buf = buildNodeFilePath(null, bundle.getId()); + itemFs.deleteFile(buf.toString()); + } catch (Exception e) { + if (e instanceof NoSuchItemStateException) { + throw (NoSuchItemStateException) e; + } + String msg = "failed to delete bundle: " + bundle.getId(); + BundleFsPersistenceManager.log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized NodeReferences loadReferencesTo(NodeId targetId) + throws NoSuchItemStateException, ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + InputStream in = null; + try { + String path = buildNodeReferencesFilePath(null, targetId).toString(); + if (!itemFs.exists(path)) { + // special case + throw new NoSuchItemStateException(targetId.toString()); + } + in = itemFs.getInputStream(path); + NodeReferences refs = new NodeReferences(targetId); + Serializer.deserialize(refs, in); + return refs; + } catch (NoSuchItemStateException e) { + throw e; + } catch (Exception e) { + String msg = "failed to read references: " + targetId; + BundleFsPersistenceManager.log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * {@inheritDoc} + */ + public synchronized void store(NodeReferences refs) + throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + try { + StringBuffer buf = buildNodeFolderPath(null, refs.getTargetId()); + buf.append('.'); + buf.append(NODEREFSFILENAME); + String fileName = buf.toString(); + String dir = fileName.substring(0, fileName.lastIndexOf(FileSystem.SEPARATOR_CHAR)); + if (!itemFs.exists(dir)) { + itemFs.createFolder(dir); + } + OutputStream out = itemFs.getOutputStream(fileName); + Serializer.serialize(refs, out); + out.close(); + } catch (Exception e) { + String msg = "failed to write " + refs; + BundleFsPersistenceManager.log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized void destroy(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + try { + StringBuffer buf = buildNodeReferencesFilePath(null, refs.getTargetId()); + itemFs.deleteFile(buf.toString()); + } catch (Exception e) { + if (e instanceof NoSuchItemStateException) { + throw (NoSuchItemStateException) e; + } + String msg = "failed to delete " + refs; + BundleFsPersistenceManager.log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized boolean existsReferencesTo(NodeId targetId) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + try { + StringBuffer buf = buildNodeReferencesFilePath(null, targetId); + return itemFs.exists(buf.toString()); + } catch (Exception e) { + String msg = "failed to check existence of node references: " + targetId; + BundleFsPersistenceManager.log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * logs an sql exception + * @param message + * @param se + */ + protected void logException(String message, SQLException se) { + if (message != null) { + BundleFsPersistenceManager.log.error(message); + } + BundleFsPersistenceManager.log.error(" Reason: " + se.getMessage()); + BundleFsPersistenceManager.log.error( + " State/Code: " + se.getSQLState() + "/" + se.getErrorCode()); + BundleFsPersistenceManager.log.debug(" dump:", se); + } + + /** + * {@inheritDoc} + */ + public String toString() { + return name; + } + + /** + * Helper interface for closeable stores + */ + protected static interface CloseableBLOBStore extends BLOBStore { + void close(); + } + + /** + * own implementation of the filesystem blob store that uses a different + * blob-id scheme. + */ + private class FSBlobStore extends FileSystemBLOBStore implements BundleFsPersistenceManager.CloseableBLOBStore { + + private FileSystem fs; + + public FSBlobStore(FileSystem fs) { + super(fs); + this.fs = fs; + } + + public String createId(PropertyId id, int index) { + return buildBlobFilePath(null, id, index).toString(); + } + + public void close() { + try { + fs.close(); + fs = null; + } catch (Exception e) { + // ignore + } + } + } + + /** + * {@inheritDoc} + */ + public List getAllNodeIds(NodeId bigger, int maxCount) + throws ItemStateException { + ArrayList list = new ArrayList(); + try { + getListRecursive(list, "", bigger == null ? null : bigger, maxCount); + return list; + } catch (FileSystemException e) { + String msg = "failed to read node list: " + bigger + ": " + e; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected NodeId getIdFromFileName(String fileName) { + StringBuffer buff = new StringBuffer(35); + if (!fileName.endsWith("." + NODEFILENAME)) { + return null; + } + for (int i = 0; i < fileName.length(); i++) { + char c = fileName.charAt(i); + if (c == '.') { + break; + } + if (c != '/') { + buff.append(c); + int len = buff.length(); + if (len == 8 || len == 13 || len == 18 || len == 23) { + buff.append('-'); + } + } + } + return new NodeId(buff.toString()); + } + + private void getListRecursive( + ArrayList list, String path, NodeId bigger, int maxCount) + throws FileSystemException { + if (maxCount > 0 && list.size() >= maxCount) { + return; + } + String[] files = itemFs.listFiles(path); + Arrays.sort(files); + for (int i = 0; i < files.length; i++) { + String f = files[i]; + NodeId n = getIdFromFileName(path + FileSystem.SEPARATOR + f); + if (n == null) { + continue; + } + if (bigger != null && bigger.toString().compareTo(n.toString()) >= 0) { + continue; + } + list.add(n); + if (maxCount > 0 && list.size() >= maxCount) { + return; + } + } + String[] dirs = itemFs.listFolders(path); + Arrays.sort(dirs); + for (int i = 0; i < dirs.length; i++) { + getListRecursive(list, path + FileSystem.SEPARATOR + dirs[i], + bigger, maxCount); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/ConsistencyCheckerError.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/ConsistencyCheckerError.java new file mode 100644 index 00000000000..ccc2ac7495a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/ConsistencyCheckerError.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.core.persistence.bundle; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.persistence.check.ReportItem; +import org.apache.jackrabbit.core.persistence.check.ReportItemImpl; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemStateException; + +/** + * Base class for errors reported by the {@link ConsistencyCheckerImpl} + */ +abstract class ConsistencyCheckerError { + + protected final String message; + protected final NodeId nodeId; + protected boolean repaired; + + ConsistencyCheckerError(NodeId nodeId, String message) { + this.nodeId = nodeId; + this.message = message; + } + + final NodeId getNodeId() { + return nodeId; + } + + final void repair(final ChangeLog changes) throws ItemStateException { + doRepair(changes); + repaired = true; + } + + final ReportItem getReportItem() { + return new ReportItemImpl(nodeId.toString(), message, getType(), repaired); + } + + /** + * @return whether this error is repairable + */ + abstract boolean isRepairable(); + + /** + * Repair this error and update the changelog. + * + * @param changes the changelog to update with the changes made. + * @throws ItemStateException + */ + abstract void doRepair(final ChangeLog changes) throws ItemStateException; + + abstract ReportItem.Type getType(); + + /** + * Double check the error to eliminate false positives in live environments. + * @return whether the error was confirmed. + * @throws ItemStateException + */ + abstract boolean doubleCheck() throws ItemStateException; + + @Override + public String toString() { + return getType() + " - " + getNodeId(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/ConsistencyCheckerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/ConsistencyCheckerImpl.java new file mode 100644 index 00000000000..88c7f987f56 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/ConsistencyCheckerImpl.java @@ -0,0 +1,709 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.bundle; + +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 java.util.Random; +import java.util.Set; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.cluster.ClusterException; +import org.apache.jackrabbit.core.cluster.Update; +import org.apache.jackrabbit.core.cluster.UpdateEventChannel; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.persistence.check.ConsistencyCheckListener; +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; +import org.apache.jackrabbit.core.persistence.check.ConsistencyReportImpl; +import org.apache.jackrabbit.core.persistence.check.ReportItem; +import org.apache.jackrabbit.core.persistence.util.NodeInfo; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.DummyUpdateEventChannel; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConsistencyCheckerImpl { + + private static Logger log = LoggerFactory.getLogger(ConsistencyCheckerImpl.class); + + /** + * The number of nodes to fetch at once from the persistence manager. Defaults to 8kb + */ + private static final int NODESATONCE = Integer.getInteger("org.apache.jackrabbit.checker.nodesatonce", 1024 * 8); + + /** + * Attribute name used to store the size of the update. + */ + private static final String ATTRIBUTE_UPDATE_SIZE = "updateSize"; + + private final AbstractBundlePersistenceManager pm; + private final ConsistencyCheckListener listener; + private NodeId lostNFoundId; + private UpdateEventChannel eventChannel = new DummyUpdateEventChannel(); + private Map bundles; + private List errors; + private int nodeCount; + private long elapsedTime; + + public ConsistencyCheckerImpl(AbstractBundlePersistenceManager pm, ConsistencyCheckListener listener, + String lostNFoundId, final UpdateEventChannel eventChannel) { + this.pm = pm; + this.listener = listener; + if (lostNFoundId != null) { + this.lostNFoundId = new NodeId(lostNFoundId); + } + if (eventChannel != null) { + this.eventChannel = eventChannel; + } + } + + /** + * Check the database for inconsistencies. + * + * @param uuids a list of node identifiers to check or {@code null} in order to check all nodes + * @param recursive whether to recursively check the subtrees below the nodes identified by the provided uuids + * @throws RepositoryException + */ + public void check(String[] uuids, boolean recursive) throws RepositoryException { + errors = new ArrayList(); + long tstart = System.currentTimeMillis(); + nodeCount = internalCheckConsistency(uuids, recursive); + elapsedTime = System.currentTimeMillis() - tstart; + } + + /** + * Do a double check on the errors found during {@link #check}. + * Removes all false positives from the report. + */ + public void doubleCheckErrors() { + if (hasErrors()) { + final Iterator errorIterator = errors.iterator(); + while (errorIterator.hasNext()) { + final ConsistencyCheckerError error = errorIterator.next(); + try { + if (!error.doubleCheck()) { + info(null, "False positive: " + error); + errorIterator.remove(); + } + } catch (ItemStateException e) { + error(null, "Failed to double check error: " + error, e); + } + } + } + } + + /** + * Return the report of a consistency {@link #check} / {@link #doubleCheckErrors()} / {@link #repair} + */ + public ConsistencyReport getReport() { + final Set reportItems = new HashSet(); + if (hasErrors()) { + for (ConsistencyCheckerError error : errors) { + reportItems.add(error.getReportItem()); + } + } + return new ConsistencyReportImpl(nodeCount, elapsedTime, reportItems); + } + + /** + * Repair any errors found during a {@link #check}. Should be run after a {#check} and + * (if needed) {@link #doubleCheckErrors}. + * + * @throws RepositoryException + */ + public void repair() throws RepositoryException { + checkLostNFound(); + bundles = new HashMap(); + if (hasRepairableErrors()) { + boolean successful = false; + final CheckerUpdate update = new CheckerUpdate(); + try { + eventChannel.updateCreated(update); + for (ConsistencyCheckerError error : errors) { + if (error.isRepairable()) { + try { + error.repair(update.getChanges()); + info(null, "Repairing " + error); + } catch (ItemStateException e) { + error(null, "Failed to repair error: " + error, e); + } + } + } + + final ChangeLog changes = update.getChanges(); + if (changes.hasUpdates()) { + eventChannel.updatePrepared(update); + for (NodePropBundle bundle : bundles.values()) { + storeBundle(bundle); + } + update.setAttribute(ATTRIBUTE_UPDATE_SIZE, changes.getUpdateSize()); + successful = true; + } + } catch (ClusterException e) { + throw new RepositoryException("Cannot create update", e); + } finally { + if (successful) { + eventChannel.updateCommitted(update, "checker@"); + } else { + eventChannel.updateCancelled(update); + } + } + } + } + + private boolean hasErrors() { + return errors != null && !errors.isEmpty(); + } + + private boolean hasRepairableErrors() { + if (hasErrors()) { + for (ConsistencyCheckerError error : errors) { + if (error.isRepairable()) { + return true; + } + } + } + return false; + } + + private void checkLostNFound() { + if (lostNFoundId != null) { + // do we have a "lost+found" node? + try { + NodePropBundle lfBundle = pm.loadBundle(lostNFoundId); + if (lfBundle == null) { + error(lostNFoundId.toString(), "Specified 'lost+found' node does not exist"); + lostNFoundId = null; + } else if (!NameConstants.NT_UNSTRUCTURED.equals(lfBundle .getNodeTypeName())) { + error(lostNFoundId.toString(), "Specified 'lost+found' node is not of type nt:unstructured"); + lostNFoundId = null; + } + } catch (Exception ex) { + error(lostNFoundId.toString(), "finding 'lost+found' folder", ex); + lostNFoundId = null; + } + } else { + info(null, "No 'lost+found' node specified: orphans cannot be fixed"); + } + } + + private int internalCheckConsistency(String[] uuids, boolean recursive) throws RepositoryException { + int count = 0; + + if (uuids == null) { + // check all nodes + try { + Map batch = pm.getAllNodeInfos(null, NODESATONCE); + Map allInfos = batch; + + NodeId lastId = null; + while (!batch.isEmpty()) { + + for (Map.Entry entry : batch.entrySet()) { + lastId = entry.getKey(); + + count++; + if (count % 1000 == 0) { + log.info(pm + ": loaded " + count + " infos..."); + } + + } + + batch = pm.getAllNodeInfos(lastId, NODESATONCE); + + allInfos.putAll(batch); + } + + if (lastId == null) { + log.info("No nodes exists, skipping"); + } else if (pm.exists(lastId)) { + for (Map.Entry entry : allInfos.entrySet()) { + checkBundleConsistency(entry.getKey(), entry.getValue(), allInfos); + } + } else { + log.info("Failed to read all nodes, starting over"); + internalCheckConsistency(uuids, recursive); + } + + } catch (ItemStateException e) { + throw new RepositoryException("Error loading nodes", e); + } finally { + NodeInfo.clearPool(); + } + } else { + // check only given uuids, handle recursive flag + + List idList = new ArrayList(uuids.length); + for (final String uuid : uuids) { + try { + idList.add(new NodeId(uuid)); + } catch (IllegalArgumentException e) { + error(uuid, "Invalid id for consistency check, skipping: '" + uuid + "': " + e); + } + } + + for (int i = 0; i < idList.size(); i++) { + NodeId id = idList.get(i); + try { + final NodePropBundle bundle = pm.loadBundle(id); + if (bundle == null) { + if (!isVirtualNode(id)) { + error(id.toString(), "No bundle found for id '" + id + "'"); + } + } else { + checkBundleConsistency(id, new NodeInfo(bundle), Collections.emptyMap()); + + if (recursive) { + for (NodePropBundle.ChildNodeEntry entry : bundle.getChildNodeEntries()) { + idList.add(entry.getId()); + } + } + + count++; + if (count % 1000 == 0 && listener == null) { + log.info(pm + ": checked " + count + "/" + idList.size() + " bundles..."); + } + } + } catch (ItemStateException ignored) { + // problem already logged + } + } + } + + log.info(pm + ": checked " + count + " bundles."); + + return count; + } + + /** + * Checks a single bundle for inconsistencies, ie. inexistent child nodes, inexistent parents, and other + * structural inconsistencies. + * + * @param nodeId node id for the bundle to check + * @param nodeInfo the node info for the node to check + * @param infos all the {@link NodeInfo}s loaded in the current batch + */ + private void checkBundleConsistency(NodeId nodeId, NodeInfo nodeInfo, Map infos) { + + // skip all virtual nodes + if (!isRoot(nodeId) && isVirtualNode(nodeId)) { + return; + } + + if (listener != null) { + listener.startCheck(nodeId.toString()); + } + + // check the children + for (final NodeId childNodeId : nodeInfo.getChildren()) { + + if (isVirtualNode(childNodeId)) { + continue; + } + + NodeInfo childNodeInfo = infos.get(childNodeId); + + if (childNodeInfo == null) { + addError(new MissingChild(nodeId, childNodeId)); + } else { + if (!nodeId.equals(childNodeInfo.getParentId())) { + addError(new DisconnectedChild(nodeId, childNodeId, childNodeInfo.getParentId())); + } + } + } + + // check the parent + NodeId parentId = nodeInfo.getParentId(); + // skip root nodes + if (parentId != null && !isRoot(nodeId)) { + NodeInfo parentInfo = infos.get(parentId); + + if (parentInfo == null) { + addError(new OrphanedNode(nodeId, parentId)); + } else { + // if the parent exists, does it have a child node entry for us? + boolean found = false; + + for (NodeId childNodeId : parentInfo.getChildren()) { + if (childNodeId.equals(nodeId)){ + found = true; + break; + } + } + + if (!found) { + addError(new AbandonedNode(nodeId, parentId)); + } + + } + } + } + + protected boolean isVirtualNode(NodeId nodeId) { + return nodeId.toString().endsWith("babecafebabe"); + } + + private boolean isRoot(NodeId nodeId) { + return "cafebabe-cafe-babe-cafe-babecafebabe".equals(nodeId.toString()); + } + + private void addError(ConsistencyCheckerError error) { + if (listener != null) { + listener.report(error.getReportItem()); + } + errors.add(error); + } + + private void info(String id, String message) { + if (this.listener == null) { + String idstring = id == null ? "" : ("Node " + id + ": "); + log.info(idstring + message); + } else { + listener.info(id, message); + } + } + + private void error(String id, String message) { + if (this.listener == null) { + String idstring = id == null ? "" : ("Node " + id + ": "); + log.error(idstring + message); + } else { + listener.error(id, message); + } + } + + private void error(String id, String message, Throwable ex) { + String idstring = id == null ? "" : ("Node " + id + ": "); + log.error(idstring + message, ex); + if (listener != null) { + listener.error(id, message); + } + } + + private void storeBundle(NodePropBundle bundle) { + try { + bundle.markOld(); + bundle.setModCount((short) (bundle.getModCount()+1)); + pm.storeBundle(bundle); + pm.evictBundle(bundle.getId()); + } catch (ItemStateException e) { + log.error(pm + ": Error storing fixed bundle: " + e); + } + } + + private NodePropBundle getBundle(NodeId nodeId) throws ItemStateException { + if (bundles.containsKey(nodeId)) { + return bundles.get(nodeId); + } + return pm.loadBundle(nodeId); + } + + private void saveBundle(NodePropBundle bundle) { + bundles.put(bundle.getId(), bundle); + } + + /** + * A missing child is when the node referred to by a child node entry + * does not exist. + * + * This type of error is repaired by removing the corrupted child node entry. + */ + private class MissingChild extends ConsistencyCheckerError { + + private final NodeId childNodeId; + + private MissingChild(final NodeId nodeId, final NodeId childNodeId) { + super(nodeId, "NodeState '" + nodeId + "' references inexistent child '" + childNodeId + "'"); + this.childNodeId = childNodeId; + } + + @Override + ReportItem.Type getType() { + return ReportItem.Type.MISSING; + } + + @Override + boolean isRepairable() { + return true; + } + + @Override + void doRepair(final ChangeLog changes) throws ItemStateException { + final NodePropBundle bundle = getBundle(nodeId); + final Iterator entryIterator = bundle.getChildNodeEntries().iterator(); + while (entryIterator.hasNext()) { + final NodePropBundle.ChildNodeEntry childNodeEntry = entryIterator.next(); + if (childNodeEntry.getId().equals(childNodeId)) { + entryIterator.remove(); + saveBundle(bundle); + changes.modified(new NodeState(nodeId, null, null, ItemState.STATUS_EXISTING, false)); + } + } + } + + @Override + boolean doubleCheck() throws ItemStateException { + final NodePropBundle childBundle = pm.loadBundle(childNodeId); + if (childBundle == null) { + final NodePropBundle bundle = pm.loadBundle(nodeId); + if (bundle != null) { + for (NodePropBundle.ChildNodeEntry entry : bundle.getChildNodeEntries()) { + if (entry.getId().equals(childNodeId)) { + return true; + } + } + } + } + return false; + } + } + + /** + * A disconnected child is when a child node entry refers to a node + * that exists, but that node actually has a different parent. + * + * This type of error is repaired by removing the corrupted child node entry. + */ + private class DisconnectedChild extends ConsistencyCheckerError { + + private final NodeId childNodeId; + + DisconnectedChild(final NodeId nodeId, final NodeId childNodeId, final NodeId invalidParentId) { + super(nodeId, "Node has invalid parent id: '" + invalidParentId + "' (instead of '" + nodeId + "')"); + this.childNodeId = childNodeId; + } + + @Override + ReportItem.Type getType() { + return ReportItem.Type.DISCONNECTED; + } + + @Override + boolean isRepairable() { + return true; + } + + @Override + void doRepair(final ChangeLog changes) throws ItemStateException { + NodePropBundle bundle = getBundle(nodeId); + final Iterator entryIterator = bundle.getChildNodeEntries().iterator(); + while (entryIterator.hasNext()) { + final NodePropBundle.ChildNodeEntry childNodeEntry = entryIterator.next(); + if (childNodeEntry.getId().equals(childNodeId)) { + entryIterator.remove(); + saveBundle(bundle); + changes.modified(new NodeState(nodeId, null, null, ItemState.STATUS_EXISTING, false)); + break; + } + } + } + + @Override + boolean doubleCheck() throws ItemStateException { + final NodePropBundle childBundle = pm.loadBundle(childNodeId); + if (childBundle != null && !childBundle.getParentId().equals(nodeId)) { + final NodePropBundle bundle = pm.loadBundle(nodeId); + if (bundle != null) { + // double check if the child node entry is still there + for (NodePropBundle.ChildNodeEntry entry : bundle.getChildNodeEntries()) { + if (entry.getId().equals(childNodeId)) { + return true; + } + } + } + } + return false; + } + } + + /** + * An orphaned node is a node whose parent does not exist. + * + * This type of error is repaired by reattaching the orphan to + * a special purpose 'lost and found' node. + */ + private class OrphanedNode extends ConsistencyCheckerError { + + private final NodeId parentNodeId; + + OrphanedNode(final NodeId nodeId, final NodeId parentNodeId) { + super(nodeId, "NodeState '" + nodeId + "' references inexistent parent id '" + parentNodeId + "'"); + this.parentNodeId = parentNodeId; + } + + @Override + ReportItem.Type getType() { + return ReportItem.Type.ORPHANED; + } + + @Override + boolean isRepairable() { + return lostNFoundId != null; + } + + @Override + void doRepair(final ChangeLog changes) throws ItemStateException { + if (lostNFoundId != null) { + final NodePropBundle bundle = getBundle(nodeId); + final NodePropBundle lfBundle = getBundle(lostNFoundId); + + final String nodeName = nodeId + "-" + System.currentTimeMillis(); + final NameFactory nameFactory = NameFactoryImpl.getInstance(); + lfBundle.addChildNodeEntry(nameFactory.create("", nodeName), nodeId); + bundle.setParentId(lostNFoundId); + + saveBundle(bundle); + saveBundle(lfBundle); + + changes.modified(new NodeState(lostNFoundId, null, null, ItemState.STATUS_EXISTING, false)); + changes.modified(new NodeState(nodeId, null, null, ItemState.STATUS_EXISTING, false)); + } + } + + @Override + boolean doubleCheck() throws ItemStateException { + final NodePropBundle parentBundle = pm.loadBundle(parentNodeId); + if (parentBundle == null) { + final NodePropBundle bundle = pm.loadBundle(nodeId); + if (bundle != null) { + if (parentNodeId.equals(bundle.getParentId())) { + return true; + } + } + } + return false; + } + } + + /** + * An abandoned node is a node that points to an existing node + * as its parent, but that parent node does not have a corresponding + * child node entry for the child. + * + * This type of error is repaired by adding the missing child node entry + * to the parent. + */ + private class AbandonedNode extends ConsistencyCheckerError { + + private final NodeId nodeId; + private final NodeId parentNodeId; + + AbandonedNode(final NodeId nodeId, final NodeId parentNodeId) { + super(nodeId, "NodeState '" + nodeId + "' is not referenced by its parent node '" + parentNodeId + "'"); + this.nodeId = nodeId; + this.parentNodeId = parentNodeId; + } + + @Override + ReportItem.Type getType() { + return ReportItem.Type.ABANDONED; + } + + @Override + boolean isRepairable() { + return true; + } + + @Override + void doRepair(final ChangeLog changes) throws ItemStateException { + final NodePropBundle parentBundle = getBundle(parentNodeId); + + parentBundle.addChildNodeEntry(createNodeName(), nodeId); + + saveBundle(parentBundle); + changes.modified(new NodeState(parentNodeId, null, null, ItemState.STATUS_EXISTING, false)); + } + + private Name createNodeName() { + int n = (int) System.currentTimeMillis() + new Random().nextInt(); + final String localName = Integer.toHexString(n); + final NameFactory nameFactory = NameFactoryImpl.getInstance(); + return nameFactory.create("{}" + localName); + } + + @Override + boolean doubleCheck() throws ItemStateException { + final NodePropBundle parentBundle = pm.loadBundle(parentNodeId); + if (parentBundle != null) { + for (NodePropBundle.ChildNodeEntry entry : parentBundle.getChildNodeEntries()) { + if (entry.getId().equals(nodeId)) { + return false; + } + } + } + final NodePropBundle bundle = pm.loadBundle(nodeId); + if (bundle != null) { + if (parentNodeId.equals(bundle.getParentId())) { + return true; + } + } + return false; + } + } + + private class CheckerUpdate implements Update { + + private final Map attributes = new HashMap(); + private final ChangeLog changeLog = new ChangeLog(); + private final long timestamp = System.currentTimeMillis(); + + @Override + public void setAttribute(final String name, final Object value) { + attributes.put(name, value); + } + + @Override + public Object getAttribute(final String name) { + return attributes.get(name); + } + + @Override + public ChangeLog getChanges() { + return changeLog; + } + + @Override + public List getEvents() { + return Collections.emptyList(); + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public String getUserData() { + return null; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyCheckListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyCheckListener.java new file mode 100644 index 00000000000..c0fc074f343 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyCheckListener.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.core.persistence.check; + +public interface ConsistencyCheckListener { + + /** + * Called when checking of a node starts + * @param id node ID + */ + public void startCheck(String id); + + /** + * Called when there's a consistency problem to be reported + * @param item problem report + */ + public void report(ReportItem item); + + /** + * Called on errors with the check procedure + * @param id node id (can be null) + * @param message + */ + public void error(String id, String message); + + /** + * Called on progress with the check procedure + * @param id node id (can be null) + * @param message + */ + public void info(String id, String message); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyChecker.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyChecker.java new file mode 100644 index 00000000000..e771a1464eb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyChecker.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.core.persistence.check; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.cluster.UpdateEventChannel; + +/** + * Optional interface for Persistence Managers. Allows running consistency + * checks similar to the base one (see + * {@link org.apache.jackrabbit.core.persistence.bundle.AbstractBundlePersistenceManager#checkConsistency(String[], boolean, boolean)}) + * but providing a result that can be acted upon. + *

    + * Beware: this interface is designed for unit tests only. + */ +public interface ConsistencyChecker { + + /** + * Set the update event channel. Needed to inform the cluster of any changes made during repairs. + * + * @param eventChannel the update event channel + */ + public void setEventChannel(UpdateEventChannel eventChannel); + + /** + * Perform a consistency check of the data. An example are non-existent + * nodes referenced in a child node entry. The existence of this feature and + * the scope of the implementation can vary in different PersistenceManager + * implementations. + * + * @param uuids + * list of UUIDs of nodes to be checked. if null, all nodes will + * be checked + * @param recursive + * if true, the tree(s) below the given node(s) will be traversed + * and checked as well + * @param fix + * if true, any problems found that can be repaired will be + * repaired. if false, no data will be modified, instead all + * inconsistencies will only get logged + * @param lostNFoundId + * node to which to attach orphaned nodes (or null, + * in which case orphaned nodes will not get moved); this node + * should be of a node type that allows adding arbitrary child + * nodes + * @param listener + * to be called on each node that was checked (may be null) + */ + ConsistencyReport check(String[] uuids, boolean recursive, boolean fix, + String lostNFoundId, ConsistencyCheckListener listener) + throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyReport.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyReport.java new file mode 100644 index 00000000000..14422b29cbe --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyReport.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.core.persistence.check; + +import java.util.Set; + +/** + * Returned as result of a {@link ConsistencyChecker} run. + */ +public interface ConsistencyReport { + + /** + * @return number of nodes that were checked + */ + public int getNodeCount(); + + /** + * @return elapsed time in ms + */ + public long getElapsedTimeMs(); + + /** + * @return generated messages + */ + public Set getItems(); +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyReportImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyReportImpl.java new file mode 100644 index 00000000000..7b9bf659bc5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ConsistencyReportImpl.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.core.persistence.check; + +import java.util.Set; + +public class ConsistencyReportImpl implements ConsistencyReport { + + private final int nodeCount; + private final long elapsedTimeMs; + private final Set reports; + + public ConsistencyReportImpl(int nodeCount, long elapsedTimeMs, + Set reports) { + this.nodeCount = nodeCount; + this.elapsedTimeMs = elapsedTimeMs; + this.reports = reports; + } + + public int getNodeCount() { + return nodeCount; + } + + public long getElapsedTimeMs() { + return elapsedTimeMs; + } + + public Set getItems() { + return reports; + } + + @Override + public String toString() { + return "elapsedTimeMs " + elapsedTimeMs + ", nodeCount " + nodeCount + + ", reports: " + reports; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ReportItem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ReportItem.java new file mode 100644 index 00000000000..ed51f6e10c4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ReportItem.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.core.persistence.check; + +/** + * An item reported inside a {@link ConsistencyReport}. + */ +public interface ReportItem { + + public enum Type { + ORPHANED, DISCONNECTED, ABANDONED, MISSING, ERROR + } + + /** + * @return node id to which the message applies + */ + public String getNodeId(); + + /** + * @return message + */ + public String getMessage(); + + /** + * @return the type of inconsistency + */ + public Type getType(); + + /** + * @return whether this error was repaired + */ + public boolean isRepaired(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ReportItemImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ReportItemImpl.java new file mode 100644 index 00000000000..2b40e68e76d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/check/ReportItemImpl.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.core.persistence.check; + +public class ReportItemImpl implements ReportItem { + + private final String nodeId; + private final String message; + private final Type type; + private final boolean repaired; + + public ReportItemImpl(String nodeId, String message, Type type, boolean repaired) { + this.nodeId = nodeId; + this.message = message; + this.type = type; + this.repaired = repaired; + } + + public String getNodeId() { + return nodeId; + } + + public String getMessage() { + return message; + } + + @Override + public Type getType() { + return type; + } + + @Override + public boolean isRepaired() { + return repaired; + } + + @Override + public String toString() { + return type + ": " + nodeId + " -- " + message; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/DatabasePersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/DatabasePersistenceManager.java new file mode 100644 index 00000000000..fd5b39e625b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/DatabasePersistenceManager.java @@ -0,0 +1,1298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.db; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.apache.jackrabbit.core.persistence.AbstractPersistenceManager; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore; +import org.apache.jackrabbit.core.persistence.util.Serializer; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Abstract base class for database persistence managers. This class + * contains common functionality for database persistence manager subclasses + * that normally differ only in the way the database connection is acquired. + * Subclasses should override the {@link #getConnection()} method to return + * the configured database connection. + *

    + * See the {@link SimpleDbPersistenceManager} for a detailed description + * of the available configuration options and database behaviour. + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public abstract class DatabasePersistenceManager extends AbstractPersistenceManager { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(DatabasePersistenceManager.class); + + protected static final String SCHEMA_OBJECT_PREFIX_VARIABLE = + "${schemaObjectPrefix}"; + + protected boolean initialized; + + protected String schema; + protected String schemaObjectPrefix; + + protected boolean externalBLOBs; + + /** + * Whether the schema check must be done during initialization. + */ + private boolean schemaCheckEnabled = true; + + // initial size of buffer used to serialize objects + protected static final int INITIAL_BUFFER_SIZE = 1024; + + // jdbc connection + protected Connection con; + + // internal flag governing whether an automatic reconnect should be + // attempted after a SQLException had been encountered + protected boolean autoReconnect = true; + // time to sleep in ms before a reconnect is attempted + protected static final int SLEEP_BEFORE_RECONNECT = 10000; + + // the map of prepared statements (key: sql stmt, value: prepared stmt) + private Map preparedStatements = new HashMap(); + + // SQL statements for NodeState management + protected String nodeStateInsertSQL; + protected String nodeStateUpdateSQL; + protected String nodeStateSelectSQL; + protected String nodeStateSelectExistSQL; + protected String nodeStateDeleteSQL; + + // SQL statements for PropertyState management + protected String propertyStateInsertSQL; + protected String propertyStateUpdateSQL; + protected String propertyStateSelectSQL; + protected String propertyStateSelectExistSQL; + protected String propertyStateDeleteSQL; + + // SQL statements for NodeReference management + protected String nodeReferenceInsertSQL; + protected String nodeReferenceUpdateSQL; + protected String nodeReferenceSelectSQL; + protected String nodeReferenceSelectExistSQL; + protected String nodeReferenceDeleteSQL; + + // SQL statements for BLOB management + // (if externalBLOBs==false) + protected String blobInsertSQL; + protected String blobUpdateSQL; + protected String blobSelectSQL; + protected String blobSelectExistSQL; + protected String blobDeleteSQL; + + + + /** + * file system where BLOB data is stored + * (if externalBLOBs==true) + */ + protected FileSystem blobFS; + /** + * BLOBStore that manages BLOB data in the file system + * (if externalBLOBs==true) + */ + protected BLOBStore blobStore; + + /** + * Creates a new DatabasePersistenceManager instance. + */ + public DatabasePersistenceManager() { + schema = "default"; + schemaObjectPrefix = ""; + externalBLOBs = true; + initialized = false; + } + + //----------------------------------------------------< setters & getters > + + public String getSchemaObjectPrefix() { + return schemaObjectPrefix; + } + + public void setSchemaObjectPrefix(String schemaObjectPrefix) { + // make sure prefix is all uppercase + this.schemaObjectPrefix = schemaObjectPrefix.toUpperCase(); + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public boolean isExternalBLOBs() { + return externalBLOBs; + } + + public void setExternalBLOBs(boolean externalBLOBs) { + this.externalBLOBs = externalBLOBs; + } + + public void setExternalBLOBs(String externalBLOBs) { + this.externalBLOBs = Boolean.valueOf(externalBLOBs).booleanValue(); + } + + /** + * @return whether the schema check is enabled + */ + public final boolean isSchemaCheckEnabled() { + return schemaCheckEnabled; + } + + /** + * @param enabled set whether the schema check is enabled + */ + public final void setSchemaCheckEnabled(boolean enabled) { + schemaCheckEnabled = enabled; + } + + //---------------------------------------------------< PersistenceManager > + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + + // setup jdbc connection + initConnection(); + + DatabaseMetaData meta = con.getMetaData(); + try { + log.info("Database: " + meta.getDatabaseProductName() + " / " + meta.getDatabaseProductVersion()); + log.info("Driver: " + meta.getDriverName() + " / " + meta.getDriverVersion()); + } catch (SQLException e) { + log.warn("Can not retrieve database and driver name / version", e); + } + + // make sure schemaObjectPrefix consists of legal name characters only + prepareSchemaObjectPrefix(); + + // check if schema objects exist and create them if necessary + if (isSchemaCheckEnabled()) { + checkSchema(); + } + + // build sql statements + buildSQLStatements(); + + // prepare statements + initPreparedStatements(); + + if (externalBLOBs) { + /** + * store BLOBs in local file system in a sub directory + * of the workspace home directory + */ + LocalFileSystem blobFS = new LocalFileSystem(); + blobFS.setRoot(new File(context.getHomeDir(), "blobs")); + blobFS.init(); + this.blobFS = blobFS; + blobStore = new FileSystemBLOBStore(blobFS); + } else { + /** + * store BLOBs in db + */ + blobStore = new DbBLOBStore(); + } + + initialized = true; + } + + /** + * {@inheritDoc} + */ + public synchronized void close() throws Exception { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + // close shared prepared statements + for (PreparedStatement ps : preparedStatements.values()) { + closeStatement(ps); + } + preparedStatements.clear(); + + if (externalBLOBs) { + // close BLOB file system + blobFS.close(); + blobFS = null; + } + blobStore = null; + + // close jdbc connection + closeConnection(con); + + } finally { + initialized = false; + } + } + + /** + * {@inheritDoc} + */ + public synchronized void store(ChangeLog changeLog) + throws ItemStateException { + // temporarily disable automatic reconnect feature + // since the changes need to be persisted atomically + autoReconnect = false; + try { + ItemStateException ise = null; + // number of attempts to store the changes + int trials = 2; + while (trials > 0) { + try { + super.store(changeLog); + break; + } catch (ItemStateException e) { + // catch exception and fall through... + ise = e; + } + + if (ise != null && ise.getCause() instanceof SQLException) { + // a SQLException has been thrown + if (--trials > 0) { + // try to reconnect + log.warn("storing changes failed, about to reconnect...", ise.getCause()); + + // try to reconnect + if (reestablishConnection()) { + // now let's give it another try + ise = null; + continue; + } else { + // reconnect failed, proceed with error processing + break; + } + } + } else { + // a non-SQLException has been thrown, + // proceed with error processing + break; + } + } + + if (ise == null) { + // storing the changes succeeded, now commit the changes + try { + con.commit(); + } catch (SQLException e) { + String msg = "committing change log failed"; + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } else { + // storing the changes failed, rollback changes + try { + con.rollback(); + } catch (SQLException e) { + String msg = "rollback of change log failed"; + log.error(msg, e); + } + // re-throw original exception + throw ise; + } + } finally { + // re-enable automatic reconnect feature + autoReconnect = true; + } + } + + /** + * {@inheritDoc} + */ + public NodeState load(NodeId id) + throws NoSuchItemStateException, ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + synchronized (nodeStateSelectSQL) { + ResultSet rs = null; + InputStream in = null; + try { + Statement stmt = executeStmt(nodeStateSelectSQL, new Object[]{id.toString()}); + rs = stmt.getResultSet(); + if (!rs.next()) { + throw new NoSuchItemStateException(id.toString()); + } + + in = rs.getBinaryStream(1); + NodeState state = createNew(id); + Serializer.deserialize(state, in); + + return state; + } catch (Exception e) { + if (e instanceof NoSuchItemStateException) { + throw (NoSuchItemStateException) e; + } + String msg = "failed to read node state: " + id; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + IOUtils.closeQuietly(in); + closeResultSet(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public PropertyState load(PropertyId id) + throws NoSuchItemStateException, ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + synchronized (propertyStateSelectSQL) { + ResultSet rs = null; + InputStream in = null; + try { + Statement stmt = executeStmt(propertyStateSelectSQL, new Object[]{id.toString()}); + rs = stmt.getResultSet(); + if (!rs.next()) { + throw new NoSuchItemStateException(id.toString()); + } + + in = rs.getBinaryStream(1); + + if (!externalBLOBs) { + // JCR-1532: pre-fetch/buffer stream data + ByteArrayInputStream bain = new ByteArrayInputStream( + IOUtils.toByteArray(in)); + IOUtils.closeQuietly(in); + in = bain; + } + + PropertyState state = createNew(id); + Serializer.deserialize(state, in, blobStore); + + return state; + } catch (Exception e) { + if (e instanceof NoSuchItemStateException) { + throw (NoSuchItemStateException) e; + } + String msg = "failed to read property state: " + id; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + IOUtils.closeQuietly(in); + closeResultSet(rs); + } + } + } + + /** + * {@inheritDoc} + *

    + * This method uses shared PreparedStatements which must + * be executed strictly sequentially. Because this method synchronizes on + * the persistence manager instance there is no need to synchronize on the + * shared statement. If the method would not be synchronized the shared + * statements would have to be synchronized. + */ + @Override + public synchronized void store(NodeState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // check if insert or update + boolean update = state.getStatus() != ItemState.STATUS_NEW; + //boolean update = exists(state.getId()); + String sql = (update) ? nodeStateUpdateSQL : nodeStateInsertSQL; + + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize node state + Serializer.serialize(state, out); + + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + executeStmt(sql, new Object[]{out.toByteArray(), state.getNodeId().toString()}); + + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to write node state: " + state.getNodeId(); + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + *

    + * This method uses shared PreparedStatements which must + * be executed strictly sequentially. Because this method synchronizes on + * the persistence manager instance there is no need to synchronize on the + * shared statement. If the method would not be synchronized the shared + * statements would have to be synchronized. + */ + @Override + public synchronized void store(PropertyState state) + throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // check if insert or update + boolean update = state.getStatus() != ItemState.STATUS_NEW; + //boolean update = exists(state.getId()); + String sql = (update) ? propertyStateUpdateSQL : propertyStateInsertSQL; + + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize property state + Serializer.serialize(state, out, blobStore); + + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + executeStmt(sql, new Object[]{out.toByteArray(), state.getPropertyId().toString()}); + + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to write property state: " + state.getPropertyId(); + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized void destroy(NodeState state) + throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + executeStmt(nodeStateDeleteSQL, new Object[]{state.getNodeId().toString()}); + } catch (Exception e) { + String msg = "failed to delete node state: " + state.getNodeId(); + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized void destroy(PropertyState state) + throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // make sure binary values (BLOBs) are properly removed + InternalValue[] values = state.getValues(); + if (values != null) { + for (int i = 0; i < values.length; i++) { + InternalValue val = values[i]; + if (val != null) { + if (val.getType() == PropertyType.BINARY) { + val.deleteBinaryResource(); + // also remove from BLOBStore + String blobId = blobStore.createId(state.getPropertyId(), i); + try { + blobStore.remove(blobId); + } catch (Exception e) { + log.warn("failed to remove from BLOBStore: " + blobId, e); + } + } + } + } + } + + try { + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + executeStmt(propertyStateDeleteSQL, new Object[]{state.getPropertyId().toString()}); + } catch (Exception e) { + String msg = "failed to delete property state: " + state.getPropertyId(); + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public NodeReferences loadReferencesTo(NodeId targetId) + throws NoSuchItemStateException, ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + synchronized (nodeReferenceSelectSQL) { + ResultSet rs = null; + InputStream in = null; + try { + Statement stmt = executeStmt( + nodeReferenceSelectSQL, new Object[]{targetId.toString()}); + rs = stmt.getResultSet(); + if (!rs.next()) { + throw new NoSuchItemStateException(targetId.toString()); + } + + in = rs.getBinaryStream(1); + NodeReferences refs = new NodeReferences(targetId); + Serializer.deserialize(refs, in); + + return refs; + } catch (Exception e) { + if (e instanceof NoSuchItemStateException) { + throw (NoSuchItemStateException) e; + } + String msg = "failed to read node references: " + targetId; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + IOUtils.closeQuietly(in); + closeResultSet(rs); + } + } + } + + /** + * {@inheritDoc} + *

    + * This method uses shared PreparedStatements which must + * be executed strictly sequentially. Because this method synchronizes on + * the persistence manager instance there is no need to synchronize on the + * shared statement. If the method would not be synchronized the shared + * statements would have to be synchronized. + */ + @Override + public synchronized void store(NodeReferences refs) + throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // check if insert or update + boolean update = existsReferencesTo(refs.getTargetId()); + String sql = (update) ? nodeReferenceUpdateSQL : nodeReferenceInsertSQL; + + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize references + Serializer.serialize(refs, out); + + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + executeStmt(sql, new Object[]{out.toByteArray(), refs.getTargetId().toString()}); + + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to write " + refs; + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized void destroy(NodeReferences refs) + throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + executeStmt(nodeReferenceDeleteSQL, new Object[]{refs.getTargetId().toString()}); + } catch (Exception e) { + String msg = "failed to delete " + refs; + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public boolean exists(NodeId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + synchronized (nodeStateSelectExistSQL) { + ResultSet rs = null; + try { + Statement stmt = executeStmt(nodeStateSelectExistSQL, new Object[]{id.toString()}); + rs = stmt.getResultSet(); + + // a node state exists if the result has at least one entry + return rs.next(); + } catch (Exception e) { + String msg = "failed to check existence of node state: " + id; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + closeResultSet(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public boolean exists(PropertyId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + synchronized (propertyStateSelectExistSQL) { + ResultSet rs = null; + try { + Statement stmt = executeStmt( + propertyStateSelectExistSQL, new Object[]{id.toString()}); + rs = stmt.getResultSet(); + + // a property state exists if the result has at least one entry + return rs.next(); + } catch (Exception e) { + String msg = "failed to check existence of property state: " + id; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + closeResultSet(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public boolean existsReferencesTo(NodeId targetId) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + synchronized (nodeReferenceSelectExistSQL) { + ResultSet rs = null; + try { + Statement stmt = executeStmt( + nodeReferenceSelectExistSQL, new Object[]{targetId.toString()}); + rs = stmt.getResultSet(); + + // a reference exists if the result has at least one entry + return rs.next(); + } catch (Exception e) { + String msg = "failed to check existence of node references: " + + targetId; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + closeResultSet(rs); + } + } + } + + //----------------------------------< misc. helper methods & overridables > + + /** + * Initializes the database connection used by this persistence manager. + *

    + * Subclasses should normally override the {@link #getConnection()} + * method instead of this one. The default implementation calls + * {@link #getConnection()} to get the database connection and disables + * the autocommit feature. + * + * @throws Exception if an error occurs + */ + protected void initConnection() throws Exception { + con = getConnection(); + // JCR-1013: Setter may fail unnecessarily on a managed connection + if (con.getAutoCommit()) { + con.setAutoCommit(false); + } + } + + /** + * Abstract factory method for creating a new database connection. This + * method is called by {@link #init(PMContext)} when the persistence + * manager is started. The returned connection should come with the default + * JDBC settings, as the {@link #init(PMContext)} method will explicitly + * set the autoCommit and other properties as needed. + *

    + * Note that the returned database connection is kept during the entire + * lifetime of the persistence manager, after which it is closed by + * {@link #close()} using the {@link #closeConnection(Connection)} method. + * + * @return new connection + * @throws Exception if an error occurs + */ + protected Connection getConnection() throws Exception { + throw new UnsupportedOperationException("Override in a subclass!"); + } + + /** + * Closes the given database connection. This method is called by + * {@link #close()} to close the connection acquired using + * {@link #getConnection()} when the persistence manager was started. + *

    + * The default implementation just calls the {@link Connection#close()} + * method of the given connection, but subclasses can override this + * method to provide more extensive database and connection cleanup. + * + * @param connection database connection + * @throws Exception if an error occurs + */ + protected void closeConnection(Connection connection) throws Exception { + connection.close(); + } + + /** + * Re-establishes the database connection. This method is called by + * {@link #store(ChangeLog)} and {@link #executeStmt(String, Object[])} + * after a SQLException had been encountered. + * @return true if the connection could be successfully re-established, + * false otherwise. + */ + protected synchronized boolean reestablishConnection() { + // in any case try to shut down current connection + // gracefully in order to avoid potential memory leaks + + // close shared prepared statements + for (Iterator it = preparedStatements.values().iterator(); it.hasNext();) { + PreparedStatement stmt = it.next(); + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException se) { + // ignored, see JCR-765 + } + } + } + try { + closeConnection(con); + } catch (Exception ignore) { + } + + // sleep for a while to give database a chance + // to restart before a reconnect is attempted + + try { + Thread.sleep(SLEEP_BEFORE_RECONNECT); + } catch (InterruptedException ignore) { + } + + // now try to re-establish connection + + try { + initConnection(); + initPreparedStatements(); + return true; + } catch (Exception e) { + log.error("failed to re-establish connection", e); + // reconnect failed + return false; + } + } + + /** + * Executes the given SQL statement with the specified parameters. + * If a SQLException is encountered and + * autoReconnect==true one attempt is made to re-establish + * the database connection and re-execute the statement. + * + * @param sql statement to execute + * @param params parameters to set + * @return the Statement object that had been executed + * @throws SQLException if an error occurs + */ + protected Statement executeStmt(String sql, Object[] params) + throws SQLException { + int trials = autoReconnect ? 2 : 1; + while (true) { + PreparedStatement stmt = (PreparedStatement) preparedStatements.get(sql); + try { + for (int i = 0; i < params.length; i++) { + if (params[i] instanceof SizedInputStream) { + SizedInputStream in = (SizedInputStream) params[i]; + stmt.setBinaryStream(i + 1, in, (int) in.getSize()); + } else { + stmt.setObject(i + 1, params[i]); + } + } + stmt.execute(); + resetStatement(stmt); + return stmt; + } catch (SQLException se) { + if (--trials == 0) { + // no more trials, re-throw + throw se; + } + log.warn("execute failed, about to reconnect... {}", se.getMessage()); + + // try to reconnect + if (reestablishConnection()) { + // reconnect succeeded; check whether it's possible to + // re-execute the prepared stmt with the given parameters + for (int i = 0; i < params.length; i++) { + if (params[i] instanceof SizedInputStream) { + SizedInputStream in = (SizedInputStream) params[i]; + if (in.isConsumed()) { + // we're unable to re-execute the prepared stmt + // since an InputStream paramater has already + // been 'consumed'; + // re-throw previous SQLException + throw se; + } + } + } + + // try again to execute the statement + continue; + } else { + // reconnect failed, re-throw previous SQLException + throw se; + } + } + } + } + + /** + * Resets the given PreparedStatement by clearing the parameters + * and warnings contained. + *

    + * NOTE: This method MUST be called in a synchronized context as neither + * this method nor the PreparedStatement instance on which it + * operates are thread safe. + * + * @param stmt The PreparedStatement to reset. If + * null this method does nothing. + */ + protected void resetStatement(PreparedStatement stmt) { + if (stmt != null) { + try { + stmt.clearParameters(); + stmt.clearWarnings(); + } catch (SQLException se) { + logException("failed resetting PreparedStatement", se); + } + } + } + + protected void closeResultSet(ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException se) { + logException("failed closing ResultSet", se); + } + } + } + + protected void closeStatement(Statement stmt) { + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException se) { + logException("failed closing Statement", se); + } + } + } + + protected void logException(String message, SQLException se) { + if (message != null) { + log.error(message); + } + log.error(" reason: " + se.getMessage()); + log.error("state/code: " + se.getSQLState() + "/" + se.getErrorCode()); + log.debug(" dump:", se); + } + + /** + * Makes sure that schemaObjectPrefix does only consist of + * characters that are allowed in names on the target database. Illegal + * characters will be escaped as necessary. + * + * @throws Exception if an error occurs + */ + protected void prepareSchemaObjectPrefix() throws Exception { + DatabaseMetaData metaData = con.getMetaData(); + String legalChars = metaData.getExtraNameCharacters(); + legalChars += "ABCDEFGHIJKLMNOPQRSTUVWXZY0123456789_"; + + String prefix = schemaObjectPrefix.toUpperCase(); + StringBuilder escaped = new StringBuilder(); + for (int i = 0; i < prefix.length(); i++) { + char c = prefix.charAt(i); + if (legalChars.indexOf(c) == -1) { + escaped.append("_x"); + String hex = Integer.toHexString(c); + escaped.append("0000".toCharArray(), 0, 4 - hex.length()); + escaped.append(hex); + escaped.append("_"); + } else { + escaped.append(c); + } + } + schemaObjectPrefix = escaped.toString(); + } + + /** + * Checks if the required schema objects exist and creates them if they + * don't exist yet. + * + * @throws Exception if an error occurs + */ + protected void checkSchema() throws Exception { + DatabaseMetaData metaData = con.getMetaData(); + String tableName = schemaObjectPrefix + "NODE"; + if (metaData.storesLowerCaseIdentifiers()) { + tableName = tableName.toLowerCase(); + } else if (metaData.storesUpperCaseIdentifiers()) { + tableName = tableName.toUpperCase(); + } + + ResultSet rs = metaData.getTables(null, null, tableName, null); + boolean schemaExists; + try { + schemaExists = rs.next(); + } finally { + rs.close(); + } + + if (!schemaExists) { + // read ddl from resources + InputStream in = getSchemaDDL(); + if (in == null) { + String msg = "Configuration error: unknown schema '" + schema + "'"; + log.debug(msg); + throw new RepositoryException(msg); + } + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + Statement stmt = con.createStatement(); + try { + String sql = reader.readLine(); + while (sql != null) { + // Skip comments and empty lines + if (!sql.startsWith("#") && sql.length() > 0) { + // replace prefix variable + sql = createSchemaSql(sql); + // execute sql stmt + stmt.executeUpdate(sql); + } + // read next sql stmt + sql = reader.readLine(); + } + // commit the changes + con.commit(); + } finally { + IOUtils.closeQuietly(in); + closeStatement(stmt); + } + } + } + + /** + * Replace wildcards and return the expanded SQL statement. + * + * @param sql The SQL with embedded wildcards. + * @return The SQL with no wildcards present. + */ + protected String createSchemaSql(String sql) { + // replace prefix variable + return Text.replace(sql, SCHEMA_OBJECT_PREFIX_VARIABLE, schemaObjectPrefix); + } + + /** + * Returns an input stream to the schema DDL resource. + * @return an input stream to the schema DDL resource. + */ + protected InputStream getSchemaDDL() { + // JCR-595: Use the class explicitly instead of using getClass() + // to avoid problems when subclassed in a different package + return DatabasePersistenceManager.class.getResourceAsStream(schema + ".ddl"); + } + + /** + * Builds the SQL statements + */ + protected void buildSQLStatements() { + nodeStateInsertSQL = "insert into " + + schemaObjectPrefix + "NODE (NODE_DATA, NODE_ID) values (?, ?)"; + + nodeStateUpdateSQL = "update " + + schemaObjectPrefix + "NODE set NODE_DATA = ? where NODE_ID = ?"; + nodeStateSelectSQL = "select NODE_DATA from " + + schemaObjectPrefix + "NODE where NODE_ID = ?"; + nodeStateSelectExistSQL = "select 1 from " + + schemaObjectPrefix + "NODE where NODE_ID = ?"; + nodeStateDeleteSQL = "delete from " + + schemaObjectPrefix + "NODE where NODE_ID = ?"; + + propertyStateInsertSQL = "insert into " + + schemaObjectPrefix + "PROP (PROP_DATA, PROP_ID) values (?, ?)"; + propertyStateUpdateSQL = "update " + + schemaObjectPrefix + "PROP set PROP_DATA = ? where PROP_ID = ?"; + propertyStateSelectSQL = "select PROP_DATA from " + + schemaObjectPrefix + "PROP where PROP_ID = ?"; + propertyStateSelectExistSQL = "select 1 from " + + schemaObjectPrefix + "PROP where PROP_ID = ?"; + propertyStateDeleteSQL = "delete from " + + schemaObjectPrefix + "PROP where PROP_ID = ?"; + + nodeReferenceInsertSQL = "insert into " + + schemaObjectPrefix + "REFS (REFS_DATA, NODE_ID) values (?, ?)"; + nodeReferenceUpdateSQL = "update " + + schemaObjectPrefix + "REFS set REFS_DATA = ? where NODE_ID = ?"; + nodeReferenceSelectSQL = "select REFS_DATA from " + + schemaObjectPrefix + "REFS where NODE_ID = ?"; + nodeReferenceSelectExistSQL = "select 1 from " + + schemaObjectPrefix + "REFS where NODE_ID = ?"; + nodeReferenceDeleteSQL = "delete from " + + schemaObjectPrefix + "REFS where NODE_ID = ?"; + + if (!externalBLOBs) { + blobInsertSQL = "insert into " + + schemaObjectPrefix + "BINVAL (BINVAL_DATA, BINVAL_ID) values (?, ?)"; + blobUpdateSQL = "update " + + schemaObjectPrefix + "BINVAL set BINVAL_DATA = ? where BINVAL_ID = ?"; + blobSelectSQL = + "select BINVAL_DATA from " + + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; + blobSelectExistSQL = + "select 1 from " + + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; + blobDeleteSQL = "delete from " + + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; + } + } + + /** + * Initializes the map of prepared statements. + * + * @throws SQLException if an error occurs + */ + protected void initPreparedStatements() throws SQLException { + preparedStatements.put( + nodeStateInsertSQL, con.prepareStatement(nodeStateInsertSQL)); + preparedStatements.put( + nodeStateUpdateSQL, con.prepareStatement(nodeStateUpdateSQL)); + preparedStatements.put( + nodeStateSelectSQL, con.prepareStatement(nodeStateSelectSQL)); + preparedStatements.put( + nodeStateSelectExistSQL, con.prepareStatement(nodeStateSelectExistSQL)); + preparedStatements.put( + nodeStateDeleteSQL, con.prepareStatement(nodeStateDeleteSQL)); + + preparedStatements.put( + propertyStateInsertSQL, con.prepareStatement(propertyStateInsertSQL)); + preparedStatements.put( + propertyStateUpdateSQL, con.prepareStatement(propertyStateUpdateSQL)); + preparedStatements.put( + propertyStateSelectSQL, con.prepareStatement(propertyStateSelectSQL)); + preparedStatements.put( + propertyStateSelectExistSQL, con.prepareStatement(propertyStateSelectExistSQL)); + preparedStatements.put( + propertyStateDeleteSQL, con.prepareStatement(propertyStateDeleteSQL)); + + preparedStatements.put( + nodeReferenceInsertSQL, con.prepareStatement(nodeReferenceInsertSQL)); + preparedStatements.put( + nodeReferenceUpdateSQL, con.prepareStatement(nodeReferenceUpdateSQL)); + preparedStatements.put( + nodeReferenceSelectSQL, con.prepareStatement(nodeReferenceSelectSQL)); + preparedStatements.put( + nodeReferenceSelectExistSQL, con.prepareStatement(nodeReferenceSelectExistSQL)); + preparedStatements.put( + nodeReferenceDeleteSQL, con.prepareStatement(nodeReferenceDeleteSQL)); + + if (!externalBLOBs) { + preparedStatements.put(blobInsertSQL, con.prepareStatement(blobInsertSQL)); + preparedStatements.put(blobUpdateSQL, con.prepareStatement(blobUpdateSQL)); + preparedStatements.put(blobSelectSQL, con.prepareStatement(blobSelectSQL)); + preparedStatements.put(blobSelectExistSQL, con.prepareStatement(blobSelectExistSQL)); + preparedStatements.put(blobDeleteSQL, con.prepareStatement(blobDeleteSQL)); + } + } + + //--------------------------------------------------------< inner classes > + + static class SizedInputStream extends FilterInputStream { + private final long size; + private boolean consumed = false; + + SizedInputStream(InputStream in, long size) { + super(in); + this.size = size; + } + + long getSize() { + return size; + } + + boolean isConsumed() { + return consumed; + } + + public int read() throws IOException { + consumed = true; + return super.read(); + } + + public long skip(long n) throws IOException { + consumed = true; + return super.skip(n); + } + + public int read(byte[] b) throws IOException { + consumed = true; + return super.read(b); + } + + public int read(byte[] b, int off, int len) throws IOException { + consumed = true; + return super.read(b, off, len); + } + } + + class DbBLOBStore implements BLOBStore { + /** + * {@inheritDoc} + */ + public String createId(PropertyId id, int index) { + // the blobId is a simple string concatenation of id plus index + StringBuilder sb = new StringBuilder(); + sb.append(id.toString()); + sb.append('['); + sb.append(index); + sb.append(']'); + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + public InputStream get(String blobId) throws Exception { + synchronized (blobSelectSQL) { + Statement stmt = executeStmt(blobSelectSQL, new Object[]{blobId}); + final ResultSet rs = stmt.getResultSet(); + if (!rs.next()) { + closeResultSet(rs); + throw new Exception("no such BLOB: " + blobId); + } + InputStream in = rs.getBinaryStream(1); + if (in == null) { + // some databases treat zero-length values as NULL; + // return empty InputStream in such a case + closeResultSet(rs); + return new ByteArrayInputStream(new byte[0]); + } + + /** + * return an InputStream wrapper in order to + * close the ResultSet when the stream is closed + */ + return new FilterInputStream(in) { + public void close() throws IOException { + in.close(); + // now it's safe to close ResultSet + closeResultSet(rs); + } + }; + } + } + + /** + * {@inheritDoc} + */ + public synchronized void put(String blobId, InputStream in, long size) + throws Exception { + Statement stmt = executeStmt(blobSelectExistSQL, new Object[]{blobId}); + ResultSet rs = stmt.getResultSet(); + // a BLOB exists if the result has at least one entry + boolean exists = rs.next(); + closeResultSet(rs); + + String sql = (exists) ? blobUpdateSQL : blobInsertSQL; + executeStmt(sql, new Object[]{new SizedInputStream(in, size), blobId}); + } + + /** + * {@inheritDoc} + */ + public synchronized boolean remove(String blobId) throws Exception { + Statement stmt = executeStmt(blobDeleteSQL, new Object[]{blobId}); + return stmt.getUpdateCount() == 1; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/DerbyPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/DerbyPersistenceManager.java new file mode 100644 index 00000000000..ad5849be489 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/DerbyPersistenceManager.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.core.persistence.db; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +/** + * DerbyPersistenceManager is a JDBC-based + * PersistenceManager for Jackrabbit that persists + * ItemState and NodeReferences objects in an + * embedded or standalone Derby database using a simple custom serialization format and a + * very basic non-normalized database schema (in essence tables with one 'key' + * and one 'data' column). + *

    + * It is configured through the following properties: + *

      + *
    • url: the database url of the form + * "jdbc:derby:[//host:port/][db];[attributes]"
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    • driver: the FQN name of the JDBC driver class + * (default: "org.apache.derby.jdbc.EmbeddedDriver"); Use + * "org.apache.derby.jdbc.ClientDriver" when using a standalone database
    • + *
    • schema: type of schema to be used + * (default: "derby")
    • + *
    • user: the database user (default: null)
    • + *
    • password: the user's password (default: null)
    • + *
    • externalBLOBs: if true (the default) BINARY + * values (BLOBs) are stored in the local file system; + * if false BLOBs are stored in the database
    • + *
    • shutdownOnClose: if true (the default) the + * database is shutdown when the last connection is closed; + * set this to false when using a standalone database
    • + *
    + * See also {@link SimpleDbPersistenceManager}. + *

    + * The following is a fragment from a sample configuration: + *

    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.DerbyPersistenceManager">
    + *       <param name="url" value="jdbc:derby:${wsp.home}/db;create=true"/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="externalBLOBs" value="false"/>
    + *   </PersistenceManager>
    + * 
    + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public class DerbyPersistenceManager extends SimpleDbPersistenceManager { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(DerbyPersistenceManager.class); + + /** + * Flag indicating whether this derby database should be shutdown on close. + */ + protected boolean shutdownOnClose; + + /** + * Creates a new SimpleDbPersistenceManager instance. + */ + public DerbyPersistenceManager() { + // preset some attributes to reasonable defaults + schema = "derby"; + driver = "org.apache.derby.jdbc.EmbeddedDriver"; + schemaObjectPrefix = ""; + shutdownOnClose = true; + } + + //----------------------------------------------------< setters & getters > + + public boolean getShutdownOnClose() { + return shutdownOnClose; + } + + public void setShutdownOnClose(boolean shutdownOnClose) { + this.shutdownOnClose = shutdownOnClose; + } + + //------------------------------------------< DatabasePersistenceManager > + + /** + * Closes the given connection and shuts down the embedded Derby + * database if shutdownOnClose is set to true. + * + * @param connection database connection + * @throws SQLException if an error occurs + * @see DatabasePersistenceManager#closeConnection(Connection) + */ + protected void closeConnection(Connection connection) throws SQLException { + // prepare connection url for issuing shutdown command + String url; + try { + url = connection.getMetaData().getURL(); + } catch (SQLException e) { + // JCR-1557: embedded derby db probably already shut down; + // this happens when configuring multiple FS/PM instances + // to use the same embedded derby db instance. + log.debug("failed to retrieve connection url: embedded db probably already shut down", e); + return; + } + int pos = url.lastIndexOf(';'); + if (pos != -1) { + // strip any attributes from connection url + url = url.substring(0, pos); + } + url += ";shutdown=true"; + + // we have to reset the connection to 'autoCommit=true' before closing it; + // otherwise Derby would mysteriously complain about some pending uncommitted + // changes which can't possibly be true. + // @todo further investigate + connection.setAutoCommit(true); + connection.close(); + + if (shutdownOnClose) { + // now it's safe to shutdown the embedded Derby database + try { + DriverManager.getConnection(url); + } catch (SQLException e) { + // a shutdown command always raises a SQLException + log.info(e.getMessage()); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/JNDIDatabasePersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/JNDIDatabasePersistenceManager.java new file mode 100644 index 00000000000..2f7e51d2ced --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/JNDIDatabasePersistenceManager.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.core.persistence.db; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Database persistence manager that uses JNDI to acquire the database + * connection. The JNDI location of the {@link DataSource} to be used in + * given as the dataSourceLocation configuration property. + * See the {@link SimpleDbPersistenceManager} for more configuration + * details. + *

    + * WARNING: The acquired database connection is kept + * for the entire lifetime of the persistence manager instance. The + * configured data source should be prepared for this. + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public class JNDIDatabasePersistenceManager extends DatabasePersistenceManager { + + /** + * JNDI location of the data source used to acquire database connections. + */ + private String dataSourceLocation; + + //----------------------------------------------------< setters & getters > + + /** + * Returns the JNDI location of the data source. + * + * @return data source location + */ + public String getDataSourceLocation() { + return dataSourceLocation; + } + + /** + * Sets the JNDI location of the data source. + * + * @param dataSourceLocation data source location + */ + public void setDataSourceLocation(String dataSourceLocation) { + this.dataSourceLocation = dataSourceLocation; + } + + //-------------------------------------------< DatabasePersistenceManager > + + /** + * Returns a JDBC connection from a {@link DataSource} acquired from JNDI + * with the configured data source location. + * + * @return new database connection + * @throws NamingException if the given data source location does not exist + * @throws SQLException if a database access error occurs + * @see DatabasePersistenceManager#getConnection() + */ + protected Connection getConnection() throws NamingException, SQLException { + InitialContext ic = new InitialContext(); + DataSource dataSource = (DataSource) ic.lookup(dataSourceLocation); + return dataSource.getConnection(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/MSSqlPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/MSSqlPersistenceManager.java new file mode 100644 index 00000000000..9cad8e91dfd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/MSSqlPersistenceManager.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.core.persistence.db; + +import org.apache.jackrabbit.util.Text; + +/** + * MSSqlPersistenceManager is a JDBC-based + * PersistenceManager for Jackrabbit that persists + * ItemState and NodeReferences objects in MS SQL + * database using a simple custom serialization format and a + * very basic non-normalized database schema (in essence tables with one 'key' + * and one 'data' column). + *

    + * It is configured through the following properties: + *

      + *
    • driver: the FQN name of the JDBC driver class + * (default: "com.microsoft.sqlserver.jdbc.SQLServerDriver")
    • + *
    • schema: type of schema to be used + * (default: "mssql")
    • + *
    • url: the database url (e.g. + * "jdbc:microsoft:sqlserver://[host]:[port];databaseName=[dbname]")
    • + *
    • user: the database user
    • + *
    • password: the user's password
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    • tableSpace: the tablespace to use
    • + *
    • externalBLOBs: if true (the default) BINARY + * values (BLOBs) are stored in the local file system; + * if false BLOBs are stored in the database
    • + *
    + * See also {@link SimpleDbPersistenceManager}. + *

    + * The following is a fragment from a sample configuration: + *

    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.MSSqlPersistenceManager">
    + *       <param name="url" value="jdbc:microsoft:sqlserver://localhost:1433;mydb"/>
    + *       <param name="user" value="mydba"/>
    + *       <param name="password" value="mydba"/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="tableSpace" value=""/>
    + *       <param name="externalBLOBs" value="false"/>
    + *  </PersistenceManager>
    + * 
    + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public class MSSqlPersistenceManager extends SimpleDbPersistenceManager { + + /** the variable for the MSSql table space */ + public static final String TABLE_SPACE_VARIABLE = "${tableSpace}"; + + /** the MSSql table space to use */ + protected String tableSpace; + + /** + * Creates a new MSSqlPersistenceManager instance. + */ + public MSSqlPersistenceManager() { + // preset some attributes to reasonable defaults + schema = "mssql"; + driver = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + schemaObjectPrefix = ""; + user = ""; + password = ""; + initialized = false; + tableSpace = ""; + } + + /** + * Returns the configured MSSql table space. + * @return the configured MSSql table space. + */ + public String getTableSpace() { + return tableSpace; + } + + /** + * Sets the MSSql table space. + * @param tableSpace the MSSql table space. + */ + public void setTableSpace(String tableSpace) { + if (tableSpace != null && tableSpace.length() > 0) { + this.tableSpace = "on " + tableSpace.trim(); + } else { + this.tableSpace = ""; + } + } + + protected String createSchemaSql(String sql) { + return Text.replace( + super.createSchemaSql(sql), TABLE_SPACE_VARIABLE, tableSpace); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/OraclePersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/OraclePersistenceManager.java new file mode 100644 index 00000000000..c44337a2688 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/OraclePersistenceManager.java @@ -0,0 +1,434 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.db; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.util.Serializer; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.sql.Blob; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.DatabaseMetaData; +import java.sql.Statement; + +/** + * OraclePersistenceManager is a JDBC-based + * PersistenceManager for Jackrabbit that persists + * ItemState and NodeReferences objects in Oracle + * database using a simple custom serialization format and a + * very basic non-normalized database schema (in essence tables with one 'key' + * and one 'data' column). + *

    + * It is configured through the following properties: + *

      + *
    • driver: the FQN name of the JDBC driver class + * (default: "oracle.jdbc.OracleDriver")
    • + *
    • schema: type of schema to be used + * (default: "oracle")
    • + *
    • url: the database url (e.g. + * "jdbc:oracle:thin:@[host]:[port]:[sid]")
    • + *
    • user: the database user
    • + *
    • password: the user's password
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    • tableSpace: the tablespace to use
    • + *
    • externalBLOBs: if true (the default) BINARY + * values (BLOBs) are stored in the local file system; + * if false BLOBs are stored in the database
    • + *
    + * See also {@link SimpleDbPersistenceManager}. + *

    + * The following is a fragment from a sample configuration: + *

    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.OraclePersistenceManager">
    + *       <param name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:orcl"/>
    + *       <param name="user" value="scott"/>
    + *       <param name="password" value="tiger"/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="tableSpace" value=""/>
    + *       <param name="externalBLOBs" value="false"/>
    + *  </PersistenceManager>
    + * 
    + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public class OraclePersistenceManager extends SimpleDbPersistenceManager { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(OraclePersistenceManager.class); + + private Class blobClass; + private Integer durationSessionConstant; + private Integer modeReadWriteConstant; + + /** the variable for the Oracle table space */ + public static final String TABLE_SPACE_VARIABLE = + "${tableSpace}"; + + /** the Oracle table space to use */ + protected String tableSpace; + + /** + * Creates a new OraclePersistenceManager instance. + */ + public OraclePersistenceManager() { + // preset some attributes to reasonable defaults + schema = "oracle"; + driver = "oracle.jdbc.OracleDriver"; + schemaObjectPrefix = ""; + user = ""; + password = ""; + initialized = false; + } + + /** + * Returns the configured Oracle table space. + * @return the configured Oracle table space. + */ + public String getTableSpace() { + return tableSpace; + } + + /** + * Sets the Oracle table space. + * @param tableSpace the Oracle table space. + */ + public void setTableSpace(String tableSpace) { + if (tableSpace != null) { + this.tableSpace = tableSpace.trim(); + } else { + this.tableSpace = null; + } + } + + //---------------------------------< SimpleDbPersistenceManager overrides > + /** + * {@inheritDoc} + *

    + * Retrieve the oracle.sql.BLOB class via reflection, and + * initialize the values for the DURATION_SESSION and + * MODE_READWRITE constants defined there. + */ + public void init(PMContext context) throws Exception { + super.init(context); + + if (!externalBLOBs) { + blobStore = new OracleBLOBStore(); + } + + // initialize oracle.sql.BLOB class & constants + + // use the Connection object for using the exact same + // class loader that the Oracle driver was loaded with + blobClass = con.getClass().getClassLoader().loadClass("oracle.sql.BLOB"); + durationSessionConstant = + new Integer(blobClass.getField("DURATION_SESSION").getInt(null)); + modeReadWriteConstant = + new Integer(blobClass.getField("MODE_READWRITE").getInt(null)); + } + + /** + * {@inheritDoc} + */ + public synchronized void store(NodeState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // check if insert or update + boolean update = state.getStatus() != ItemState.STATUS_NEW; + //boolean update = exists((NodeId) state.getId()); + String sql = (update) ? nodeStateUpdateSQL : nodeStateInsertSQL; + + Blob blob = null; + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize node state + Serializer.serialize(state, out); + + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + blob = createTemporaryBlob(new ByteArrayInputStream(out.toByteArray())); + executeStmt(sql, new Object[]{blob, state.getNodeId().toString()}); + + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to write node state: " + state.getId(); + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + if (blob != null) { + try { + freeTemporaryBlob(blob); + } catch (Exception ignore) { + } + } + } + } + + /** + * {@inheritDoc} + */ + public synchronized void store(PropertyState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // check if insert or update + boolean update = state.getStatus() != ItemState.STATUS_NEW; + //boolean update = exists((PropertyId) state.getId()); + String sql = (update) ? propertyStateUpdateSQL : propertyStateInsertSQL; + + Blob blob = null; + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize property state + Serializer.serialize(state, out, blobStore); + + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + blob = createTemporaryBlob(new ByteArrayInputStream(out.toByteArray())); + executeStmt(sql, new Object[]{blob, state.getPropertyId().toString()}); + + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to write property state: " + state.getId(); + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + if (blob != null) { + try { + freeTemporaryBlob(blob); + } catch (Exception ignore) { + } + } + } + } + + /** + * {@inheritDoc} + */ + public synchronized void store(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // check if insert or update + boolean update = existsReferencesTo(refs.getTargetId()); + String sql = (update) ? nodeReferenceUpdateSQL : nodeReferenceInsertSQL; + + Blob blob = null; + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize references + Serializer.serialize(refs, out); + + // we are synchronized on this instance, therefore we do not + // not have to additionally synchronize on the sql statement + blob = createTemporaryBlob(new ByteArrayInputStream(out.toByteArray())); + executeStmt(sql, new Object[]{blob, refs.getTargetId().toString()}); + + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to write " + refs; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + if (blob != null) { + try { + freeTemporaryBlob(blob); + } catch (Exception ignore) { + } + } + } + } + + /** + * {@inheritDoc} + *

    + * Overridden in order to support multiple oracle schemas. Note that + * schema names in Oracle correspond to the username of the connection. + * See http://issues.apache.org/jira/browse/JCR-582 + * + * @throws Exception if an error occurs + */ + protected void checkSchema() throws Exception { + DatabaseMetaData metaData = con.getMetaData(); + String tableName = schemaObjectPrefix + "NODE"; + if (metaData.storesLowerCaseIdentifiers()) { + tableName = tableName.toLowerCase(); + } else if (metaData.storesUpperCaseIdentifiers()) { + tableName = tableName.toUpperCase(); + } + String userName = metaData.getUserName(); + + ResultSet rs = metaData.getTables(null, userName, tableName, null); + boolean schemaExists; + try { + schemaExists = rs.next(); + } finally { + rs.close(); + } + + if (!schemaExists) { + // read ddl from resources + InputStream in = getSchemaDDL(); + if (in == null) { + String msg = "Configuration error: unknown schema '" + schema + "'"; + log.debug(msg); + throw new RepositoryException(msg); + } + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + Statement stmt = con.createStatement(); + try { + String sql = reader.readLine(); + while (sql != null) { + // Skip comments and empty lines + if (!sql.startsWith("#") && sql.length() > 0) { + // replace prefix variable + sql = Text.replace(sql, SCHEMA_OBJECT_PREFIX_VARIABLE, schemaObjectPrefix); + + // set the tablespace if it is defined + String tspace; + if (tableSpace == null || "".equals(tableSpace)) { + tspace = ""; + } else { + tspace = "tablespace " + tableSpace; + } + sql = Text.replace(sql, TABLE_SPACE_VARIABLE, tspace).trim(); + + // execute sql stmt + stmt.executeUpdate(sql); + } + // read next sql stmt + sql = reader.readLine(); + } + // commit the changes + con.commit(); + } finally { + IOUtils.closeQuietly(in); + closeStatement(stmt); + } + } + } + + //----------------------------------------< oracle-specific blob handling > + /** + * Creates a temporary oracle.sql.BLOB instance via reflection and spools + * the contents of the specified stream. + */ + protected Blob createTemporaryBlob(InputStream in) throws Exception { + /* + BLOB blob = BLOB.createTemporary(con, false, BLOB.DURATION_SESSION); + blob.open(BLOB.MODE_READWRITE); + OutputStream out = blob.getBinaryOutputStream(); + ... + out.flush(); + out.close(); + blob.close(); + return blob; + */ + Method createTemporary = blobClass.getMethod("createTemporary", + new Class[]{Connection.class, Boolean.TYPE, Integer.TYPE}); + Object blob = createTemporary.invoke(null, new Object[]{ + ConnectionFactory.unwrap(con), + Boolean.FALSE, durationSessionConstant }); + Method open = blobClass.getMethod("open", new Class[]{Integer.TYPE}); + open.invoke(blob, new Object[]{modeReadWriteConstant}); + Method getBinaryOutputStream = + blobClass.getMethod("getBinaryOutputStream", new Class[0]); + OutputStream out = (OutputStream) getBinaryOutputStream.invoke(blob); + try { + IOUtils.copy(in, out); + } finally { + try { + out.flush(); + } catch (IOException ioe) { + } + out.close(); + } + Method close = blobClass.getMethod("close", new Class[0]); + close.invoke(blob); + return (Blob) blob; + } + + /** + * Frees a temporary oracle.sql.BLOB instance via reflection. + */ + protected void freeTemporaryBlob(Object blob) throws Exception { + // blob.freeTemporary(); + Method freeTemporary = blobClass.getMethod("freeTemporary", new Class[0]); + freeTemporary.invoke(blob); + } + + //--------------------------------------------------------< inner classes > + class OracleBLOBStore extends DbBLOBStore { + /** + * {@inheritDoc} + */ + public synchronized void put(String blobId, InputStream in, long size) + throws Exception { + Statement stmt = executeStmt(blobSelectExistSQL, new Object[]{blobId}); + ResultSet rs = stmt.getResultSet(); + // a BLOB exists if the result has at least one entry + boolean exists = rs.next(); + closeResultSet(rs); + + Blob blob = null; + try { + String sql = (exists) ? blobUpdateSQL : blobInsertSQL; + blob = createTemporaryBlob(in); + executeStmt(sql, new Object[]{blob, blobId}); + } finally { + if (blob != null) { + try { + freeTemporaryBlob(blob); + } catch (Exception ignore) { + } + } + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/SimpleDbPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/SimpleDbPersistenceManager.java new file mode 100644 index 00000000000..d194bedbe3d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/db/SimpleDbPersistenceManager.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.db; + +import org.apache.jackrabbit.core.persistence.util.Serializer; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.core.util.db.DatabaseAware; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import javax.jcr.RepositoryException; + +/** + * SimpleDbPersistenceManager is a generic JDBC-based + * PersistenceManager for Jackrabbit that persists + * ItemState and NodeReferences objects using a + * simple custom binary serialization format (see {@link Serializer}) and a + * very basic non-normalized database schema (in essence tables with one 'key' + * and one 'data' column). + *

    + * It is configured through the following properties: + *

      + *
    • driver: the FQN name of the JDBC driver class
    • + *
    • url: the database url of the form jdbc:subprotocol:subname
    • + *
    • user: the database user
    • + *
    • password: the user's password
    • + *
    • schema: type of schema to be used + * (e.g. mysql, mssql, etc.);
    • + *
    • schemaObjectPrefix: prefix to be prepended to schema objects
    • + *
    • externalBLOBs: if true (the default) BINARY + * values (BLOBs) are stored in the local file system; + * if false BLOBs are stored in the database
    • + *
    + * The required schema objects are automatically created by executing the DDL + * statements read from the [schema].ddl file. The .ddl file is read from the + * resources by calling getClass().getResourceAsStream(schema + ".ddl"). + * Every line in the specified .ddl file is executed separately by calling + * java.sql.Statement.execute(String) where every occurrence of the + * the string "${schemaObjectPrefix}" has been replaced with the + * value of the property schemaObjectPrefix. + *

    + * The following is a fragment from a sample configuration using MySQL: + *

    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.SimpleDbPersistenceManager">
    + *       <param name="driver" value="com.mysql.jdbc.Driver"/>
    + *       <param name="url" value="jdbc:mysql:///test?autoReconnect=true"/>
    + *       <param name="schema" value="mysql"/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="externalBLOBs" value="false"/>
    + *   </PersistenceManager>
    + * 
    + * The following is a fragment from a sample configuration using Daffodil One$DB Embedded: + *
    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.SimpleDbPersistenceManager">
    + *       <param name="driver" value="in.co.daffodil.db.jdbc.DaffodilDBDriver"/>
    + *       <param name="url" value="jdbc:daffodilDB_embedded:${wsp.name};path=${wsp.home}/../../databases;create=true"/>
    + *       <param name="user" value="daffodil"/>
    + *       <param name="password" value="daffodil"/>
    + *       <param name="schema" value="daffodil"/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="externalBLOBs" value="false"/>
    + *   </PersistenceManager>
    + * 
    + * The following is a fragment from a sample configuration using DB2: + *
    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.SimpleDbPersistenceManager">
    + *       <param name="driver" value="com.ibm.db2.jcc.DB2Driver"/>
    + *       <param name="url" value="jdbc:db2:test"/>
    + *       <param name="schema" value="db2"/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="externalBLOBs" value="false"/>
    + *   </PersistenceManager>
    + * 
    + * The following is a fragment from a sample configuration using MSSQL: + *
    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.SimpleDbPersistenceManager">
    + *       <param name="driver" value="com.microsoft.jdbc.sqlserver.SQLServerDriver"/>
    + *       <param name="url" value="jdbc:microsoft:sqlserver://localhost:1433;;DatabaseName=test;SelectMethod=Cursor;"/>
    + *       <param name="schema" value="mssql"/>
    + *       <param name="user" value="sa"/>
    + *       <param name="password" value=""/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="externalBLOBs" value="false"/>
    + *   </PersistenceManager>
    + * 
    + * The following is a fragment from a sample configuration using Ingres: + *
    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.SimpleDbPersistenceManager">
    + *       <param name="driver" value="com.ingres.jdbc.IngresDriver"/>
    + *       <param name="url" value="jdbc:ingres://localhost:II7/test"/>
    + *       <param name="schema" value="ingres"/>
    + *       <param name="user" value="ingres"/>
    + *       <param name="password" value="ingres"/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="externalBLOBs" value="false"/>
    + *   </PersistenceManager>
    + * 
    + * The following is a fragment from a sample configuration using PostgreSQL: + *
    + *   <PersistenceManager class="org.apache.jackrabbit.core.persistence.db.SimpleDbPersistenceManager">
    + *       <param name="driver" value="org.postgresql.Driver"/>
    + *       <param name="url" value="jdbc:postgresql://localhost/test"/>
    + *       <param name="schema" value="postgresql"/>
    + *       <param name="user" value="postgres"/>
    + *       <param name="password" value="postgres"/>
    + *       <param name="schemaObjectPrefix" value="${wsp.name}_"/>
    + *       <param name="externalBLOBs" value="false"/>
    + *   </PersistenceManager>
    + * 
    + * JNDI can be used to get the connection. In this case, use the javax.naming.InitialContext as the driver, + * and the JNDI name as the URL. If the user and password are configured in the JNDI resource, + * they should not be configured here. Example JNDI settings: + *
    + * <param name="driver" value="javax.naming.InitialContext" />
    + * <param name="url" value="java:comp/env/jdbc/Test" />
    + * 
    + * See also {@link DerbyPersistenceManager}, {@link OraclePersistenceManager}. + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public class SimpleDbPersistenceManager extends DatabasePersistenceManager implements DatabaseAware { + + protected String driver; + protected String url; + protected String user; + protected String password; + + /** + * The repositories {@link ConnectionFactory}. + */ + private ConnectionFactory connectionFactory; + + /** + * {@inheritDoc} + */ + public void setConnectionFactory(ConnectionFactory connnectionFactory) { + this.connectionFactory = connnectionFactory; + } + + //----------------------------------------------------< setters & getters > + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getDriver() { + return driver; + } + + public void setDriver(String driver) { + this.driver = driver; + } + + /** + * Returns a JDBC connection acquired using the JDBC {@link DriverManager}. + * @throws SQLException + * + * @throws RepositoryException if the driver could not be loaded + * @throws SQLException if the connection could not be established + * @see DatabasePersistenceManager#getConnection() + */ + protected Connection getConnection() throws RepositoryException, SQLException { + return connectionFactory.getDataSource(driver, url, user, password).getConnection(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/mem/InMemBundlePersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/mem/InMemBundlePersistenceManager.java new file mode 100644 index 00000000000..f8f2fbb93c6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/mem/InMemBundlePersistenceManager.java @@ -0,0 +1,625 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.mem; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.bundle.AbstractBundlePersistenceManager; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.persistence.util.BundleBinding; +import org.apache.jackrabbit.core.persistence.util.ErrorHandling; +import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore; +import org.apache.jackrabbit.core.persistence.util.NodeInfo; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle; +import org.apache.jackrabbit.core.persistence.util.Serializer; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * BundleInMemPersistenceManager is a HashMap-based + * PersistenceManager for Jackrabbit that keeps all data in memory + * and that is capable of storing and loading its contents using a simple custom + * binary serialization format (see {@link Serializer}). + *

    + * It is configured through the following properties: + *

      + *
    • initialCapacity: initial capacity of the hash map used to store the data
    • + *
    • loadFactor: load factor of the hash map used to store the data
    • + *
    • persistent: if true the contents of the hash map + * is loaded on startup and stored on shutdown; + * if false nothing is persisted
    • + *
    • useFileBlobStore: if true the contents of the blobs + * will be directly stored on the file system instead of in memory.
    • + *
    • minBlobSize use blob store for binaries properties larger + * than minBlobSite (bytes). Default is 4096.
    • + *
    + *

    + * Please note that this class should only be used for testing purposes. + * + */ +public class InMemBundlePersistenceManager extends AbstractBundlePersistenceManager { + + /** the default logger */ + private static Logger log = LoggerFactory.getLogger(InMemBundlePersistenceManager.class); + + /** flag indicating if this manager was initialized */ + protected boolean initialized; + + /** + * Path where bundles are stored on shutdown. + * (if persistent==true) + */ + protected static final String BUNDLE_FILE_PATH = "/data/.bundle.bin"; + + /** + * Path where blobs are stored on shutdown. + * (if persistent==true and useFileBlobStore==false) + */ + protected static final String BLOBS_FILE_PATH = "/data/.blobs.bin"; + + /** + * Path where references are stored on shutdown. + * (if persistent==true) + */ + protected static final String REFS_FILE_PATH = "/data/.refs.bin"; + + /** + * File system where the content of the hash maps are read from/written to + * (if persistent==true) + */ + protected FileSystem wspFS; + + /** + * File system where BLOB data is stored. + * (if useFileBlobStore==true) + */ + protected FileSystem blobFS; + + /** + * Initial size of buffer used to serialize objects. + */ + protected static final int INITIAL_BUFFER_SIZE = 1024; + + /** + * the minimum size of a property until it gets written to the blob store + * @see #setMinBlobSize(String) + */ + private int minBlobSize = 0x1000; + + /** + * Flag for error handling. + */ + protected ErrorHandling errorHandling = new ErrorHandling(); + + /** + * the bundle binding + */ + protected BundleBinding binding; + + /** + * Bundle memory store. + */ + protected Map bundleStore; + + /** + * References memory store. + */ + protected Map refsStore; + + /** + * Blob store. + */ + protected BLOBStore blobStore; + + /** + * Blobs memory store used by the InMemBlobStore. + * (if useFileBlobStore==false) + */ + private Map blobs; + + /** + * initial capacity + */ + protected int initialCapacity = 32768; + + /** + * load factor for the hash map + */ + protected float loadFactor = 0.75f; + + /** + * Should hash map be persisted? + */ + protected boolean persistent = true; + + /** + * Store blobs on file system instead of memory. + */ + protected boolean useFileBlobStore = false; + + /** + * Creates a new InMemBundlePersistenceManager instance. + */ + public InMemBundlePersistenceManager() { + initialized = false; + } + + public void setInitialCapacity(int initialCapacity) { + this.initialCapacity = initialCapacity; + } + + public void setInitialCapacity(String initialCapacity) { + this.initialCapacity = Integer.parseInt(initialCapacity); + } + + public String getInitialCapacity() { + return Integer.toString(initialCapacity); + } + + public void setLoadFactor(float loadFactor) { + this.loadFactor = loadFactor; + } + + public void setLoadFactor(String loadFactor) { + this.loadFactor = Float.parseFloat(loadFactor); + } + + public String getLoadFactor() { + return Float.toString(loadFactor); + } + + public boolean isPersistent() { + return persistent; + } + + public void setPersistent(boolean persistent) { + this.persistent = persistent; + } + + public void setPersistent(String persistent) { + this.persistent = Boolean.valueOf(persistent).booleanValue(); + } + + public void setUseFileBlobStore(boolean useFileBlobStore) { + this.useFileBlobStore = useFileBlobStore; + } + + public void setUseFileBlobStore(String useFileBlobStore) { + this.useFileBlobStore = Boolean.valueOf(useFileBlobStore).booleanValue(); + } + + public String getMinBlobSize() { + return String.valueOf(minBlobSize); + } + + public void setMinBlobSize(String minBlobSize) { + this.minBlobSize = Integer.decode(minBlobSize).intValue(); + } + + /** + * Reads the content of the hash maps from the file system + * + * @throws Exception if an error occurs + */ + public synchronized void loadContents() throws Exception { + // read item states + FileSystemResource fsRes = new FileSystemResource(wspFS, BUNDLE_FILE_PATH); + if (!fsRes.exists()) { + return; + } + BufferedInputStream bis = new BufferedInputStream(fsRes.getInputStream()); + DataInputStream in = new DataInputStream(bis); + + try { + int n = in.readInt(); // number of entries + while (n-- > 0) { + String s = in.readUTF(); // id + NodeId id = NodeId.valueOf(s); + int length = in.readInt(); // data length + byte[] data = new byte[length]; + in.readFully(data); // data + // store in map + bundleStore.put(id, data); + } + } finally { + in.close(); + } + + // read references + fsRes = new FileSystemResource(wspFS, REFS_FILE_PATH); + bis = new BufferedInputStream(fsRes.getInputStream()); + in = new DataInputStream(bis); + + try { + int n = in.readInt(); // number of entries + while (n-- > 0) { + String s = in.readUTF(); // target id + NodeId id = NodeId.valueOf(s); + int length = in.readInt(); // data length + byte[] data = new byte[length]; + in.readFully(data); // data + // store in map + refsStore.put(id, data); + } + } finally { + in.close(); + } + + if (!useFileBlobStore) { + // read blobs + fsRes = new FileSystemResource(wspFS, BLOBS_FILE_PATH); + bis = new BufferedInputStream(fsRes.getInputStream()); + in = new DataInputStream(bis); + + try { + int n = in.readInt(); // number of entries + while (n-- > 0) { + String id = in.readUTF(); // id + int length = in.readInt(); // data length + byte[] data = new byte[length]; + in.readFully(data); // data + // store in map + blobs.put(id, data); + } + } finally { + in.close(); + } + } + } + + /** + * Writes the content of the hash maps stores to the file system. + * + * @throws Exception if an error occurs + */ + public synchronized void storeContents() throws Exception { + // write bundles + FileSystemResource fsRes = new FileSystemResource(wspFS, BUNDLE_FILE_PATH); + fsRes.makeParentDirs(); + BufferedOutputStream bos = new BufferedOutputStream(fsRes.getOutputStream()); + DataOutputStream out = new DataOutputStream(bos); + + try { + out.writeInt(bundleStore.size()); // number of entries + // entries + for (NodeId id : bundleStore.keySet()) { + out.writeUTF(id.toString()); // id + + byte[] data = bundleStore.get(id); + out.writeInt(data.length); // data length + out.write(data); // data + } + } finally { + out.close(); + } + + // write references + fsRes = new FileSystemResource(wspFS, REFS_FILE_PATH); + fsRes.makeParentDirs(); + bos = new BufferedOutputStream(fsRes.getOutputStream()); + out = new DataOutputStream(bos); + + try { + out.writeInt(refsStore.size()); // number of entries + // entries + for (NodeId id : refsStore.keySet()) { + out.writeUTF(id.toString()); // target id + + byte[] data = refsStore.get(id); + out.writeInt(data.length); // data length + out.write(data); // data + } + } finally { + out.close(); + } + + if (!useFileBlobStore) { + // write blobs + fsRes = new FileSystemResource(wspFS, BLOBS_FILE_PATH); + fsRes.makeParentDirs(); + bos = new BufferedOutputStream(fsRes.getOutputStream()); + out = new DataOutputStream(bos); + + try { + out.writeInt(blobs.size()); // number of entries + // entries + for (String id : blobs.keySet()) { + out.writeUTF(id); // id + byte[] data = blobs.get(id); + out.writeInt(data.length); // data length + out.write(data); // data + } + } finally { + out.close(); + } + } + } + + //---------------------------------------------------< PersistenceManager > + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + super.init(context); + // initialize mem stores + bundleStore = new LinkedHashMap(initialCapacity, loadFactor); + refsStore = new HashMap(initialCapacity, loadFactor); + + // Choose a FileSystem for the BlobStore based on whether data is persistent or not + if (useFileBlobStore) { + blobFS = new LocalFileSystem(); + ((LocalFileSystem) blobFS).setRoot(new File(context.getHomeDir(), "blobs")); + blobFS.init(); + blobStore = new FileSystemBLOBStore(blobFS); + } else { + blobStore = new InMemBLOBStore(); + } + + wspFS = context.getFileSystem(); + + // load namespaces + binding = new BundleBinding(errorHandling, blobStore, getNsIndex(), getNameIndex(), context.getDataStore()); + binding.setMinBlobSize(minBlobSize); + + if (persistent) { + // deserialize contents of the stores + loadContents(); + } + initialized = true; + } + + /** + * {@inheritDoc} + */ + public synchronized void close() throws Exception { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + if (persistent) { + // serialize contents of state and refs stores + storeContents(); + } else if (useFileBlobStore) { + blobFS.close(); + // not persistent, clear out blobs + wspFS.deleteFolder("blobs"); + } + } finally { + initialized = false; + } + } + + /** + * {@inheritDoc} + */ + public NodeReferences loadReferencesTo(NodeId id) throws NoSuchItemStateException, ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + if (!refsStore.containsKey(id)) { + throw new NoSuchItemStateException(id.toString()); + } + + try { + NodeReferences refs = new NodeReferences(id); + Serializer.deserialize(refs, new ByteArrayInputStream(refsStore.get(id))); + return refs; + } catch (Exception e) { + String msg = "failed to read references: " + id; + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void store(NodeReferences refs) throws ItemStateException { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize references + Serializer.serialize(refs, out); + // store in serialized format in map for better memory efficiency + refsStore.put(refs.getTargetId(), out.toByteArray()); + } catch (Exception e) { + String msg = "failed to store " + refs; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public boolean existsReferencesTo(NodeId targetId) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + return refsStore.containsKey(targetId); + } + + /** + * {@inheritDoc} + */ + @Override + protected void destroy(NodeReferences refs) throws ItemStateException { + refsStore.remove(refs.getTargetId()); + } + + /** + * {@inheritDoc} + */ + public List getAllNodeIds(NodeId after, int maxCount) throws ItemStateException, RepositoryException { + final List result = new ArrayList(); + boolean add = after == null; + int count = 0; + for (NodeId nodeId : bundleStore.keySet()) { + if (add) { + result.add(nodeId); + if (++count == maxCount) { + break; + } + } else { + add = nodeId.equals(after); + } + } + return result; + } + + /** + * {@inheritDoc} + */ + @Override + protected NodePropBundle loadBundle(NodeId id) throws ItemStateException { + if (!bundleStore.containsKey(id)) { + return null; + } + try { + return binding.readBundle(new ByteArrayInputStream(bundleStore.get(id)), id); + } catch (Exception e) { + String msg = "failed to read bundle: " + id + ": " + e; + log.error(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void storeBundle(NodePropBundle bundle) throws ItemStateException { + ByteArrayOutputStream out = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + try { + binding.writeBundle(out, bundle); + bundleStore.put(bundle.getId(), out.toByteArray()); + } catch (IOException e) { + String msg = "failed to write bundle: " + bundle.getId(); + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void destroyBundle(NodePropBundle bundle) throws ItemStateException { + bundleStore.remove(bundle.getId()); + } + + /** + * {@inheritDoc} + */ + @Override + protected BLOBStore getBlobStore() { + return blobStore; + } + + /** + * Helper interface for closeable stores + */ + protected static interface CloseableBLOBStore extends BLOBStore { + void close(); + } + + /** + * Trivial {@link BLOBStore} implementation that is backed by a {@link HashMap}. + */ + protected class InMemBLOBStore implements CloseableBLOBStore { + + public InMemBLOBStore() { + blobs = new HashMap(); + } + + /** + * {@inheritDoc} + */ + public String createId(PropertyId id, int index) { + StringBuilder buf = new StringBuilder(); + buf.append(id.getParentId().toString()); + buf.append('.'); + buf.append(getNsIndex().stringToIndex(id.getName().getNamespaceURI())); + buf.append('.'); + buf.append(getNameIndex().stringToIndex(id.getName().getLocalName())); + buf.append('.'); + buf.append(index); + return buf.toString(); + } + + /** + * {@inheritDoc} + */ + public void put(String blobId, InputStream in, long size) throws Exception { + blobs.put(blobId, IOUtils.toByteArray(in)); + } + + /** + * {@inheritDoc} + */ + public InputStream get(String blobId) throws Exception { + if (blobs.containsKey(blobId)) { + return new ByteArrayInputStream(blobs.get(blobId)); + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + public boolean remove(String blobId) throws Exception { + return blobs.remove(blobId) != null; + } + + /** + * {@inheritDoc} + */ + public void close() { + blobs.clear(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/mem/InMemPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/mem/InMemPersistenceManager.java new file mode 100644 index 00000000000..f758c2dc739 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/mem/InMemPersistenceManager.java @@ -0,0 +1,589 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.mem; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemPathUtil; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.apache.jackrabbit.core.fs.mem.MemoryFileSystem; +import org.apache.jackrabbit.core.persistence.AbstractPersistenceManager; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore; +import org.apache.jackrabbit.core.persistence.util.Serializer; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * InMemPersistenceManager is a very simple HashMap-based + * PersistenceManager for Jackrabbit that keeps all data in memory + * and that is capable of storing and loading its contents using a simple custom + * binary serialization format (see {@link Serializer}). + *

    + * It is configured through the following properties: + *

      + *
    • initialCapacity: initial capacity of the hash map used to store the data
    • + *
    • loadFactor: load factor of the hash map used to store the data
    • + *
    • persistent: if true the contents of the hash map + * is loaded on startup and stored on shutdown; + * if false nothing is persisted
    • + *
    + * Please note that this class should only be used for testing purposes. + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public class InMemPersistenceManager extends AbstractPersistenceManager { + + private static Logger log = LoggerFactory.getLogger(InMemPersistenceManager.class); + + protected boolean initialized; + + protected Map stateStore; + protected Map refsStore; + + // initial size of buffer used to serialize objects + protected static final int INITIAL_BUFFER_SIZE = 1024; + + // some constants used in serialization + protected static final String STATE_FILE_PATH = "/data/.state.bin"; + protected static final String REFS_FILE_PATH = "/data/.refs.bin"; + protected static final byte NODE_ENTRY = 0; + protected static final byte PROP_ENTRY = 1; + + // file system where BLOB data is stored + protected FileSystem blobFS; + // BLOBStore that manages BLOB data in the file system + protected BLOBStore blobStore; + + /** + * file system where the content of the hash maps are read from/written to + * (if persistent==true) + */ + protected FileSystem wspFS; + + // initial capacity + protected int initialCapacity = 32768; + // load factor for the hash map + protected float loadFactor = 0.75f; + // should hash map be persisted? + protected boolean persistent = true; + + /** + * Creates a new InMemPersistenceManager instance. + */ + public InMemPersistenceManager() { + initialized = false; + } + + public void setInitialCapacity(int initialCapacity) { + this.initialCapacity = initialCapacity; + } + + public void setInitialCapacity(String initialCapacity) { + this.initialCapacity = Integer.parseInt(initialCapacity); + } + + public String getInitialCapacity() { + return Integer.toString(initialCapacity); + } + + public void setLoadFactor(float loadFactor) { + this.loadFactor = loadFactor; + } + + public void setLoadFactor(String loadFactor) { + this.loadFactor = Float.parseFloat(loadFactor); + } + + public String getLoadFactor() { + return Float.toString(loadFactor); + } + + public boolean isPersistent() { + return persistent; + } + + public void setPersistent(boolean persistent) { + this.persistent = persistent; + } + + public void setPersistent(String persistent) { + this.persistent = Boolean.valueOf(persistent).booleanValue(); + } + + protected static String buildBlobFilePath(String parentUUID, Name propName, int index) { + StringBuilder sb = new StringBuilder(); + char[] chars = parentUUID.toCharArray(); + int cnt = 0; + for (char ch : chars) { + if (ch == '-') { + continue; + } + //if (cnt > 0 && cnt % 4 == 0) { + if (cnt == 2 || cnt == 4) { + sb.append(FileSystem.SEPARATOR_CHAR); + } + sb.append(ch); + cnt++; + } + sb.append(FileSystem.SEPARATOR_CHAR); + sb.append(FileSystemPathUtil.escapeName(propName.toString())); + sb.append('.'); + sb.append(index); + sb.append(".bin"); + return sb.toString(); + } + + /** + * Reads the content of the hash maps from the file system + * + * @throws Exception if an error occurs + */ + public synchronized void loadContents() throws Exception { + // read item states + FileSystemResource fsRes = new FileSystemResource(wspFS, STATE_FILE_PATH); + if (!fsRes.exists()) { + return; + } + BufferedInputStream bis = new BufferedInputStream(fsRes.getInputStream()); + DataInputStream in = new DataInputStream(bis); + + try { + int n = in.readInt(); // number of entries + while (n-- > 0) { + byte type = in.readByte(); // entry type + ItemId id; + if (type == NODE_ENTRY) { + // entry type: node + String s = in.readUTF(); // id + id = NodeId.valueOf(s); + } else { + // entry type: property + String s = in.readUTF(); // id + id = PropertyId.valueOf(s); + } + int length = in.readInt(); // data length + byte[] data = new byte[length]; + in.readFully(data); // data + // store in map + stateStore.put(id, data); + } + } finally { + in.close(); + } + + // read references + fsRes = new FileSystemResource(wspFS, REFS_FILE_PATH); + bis = new BufferedInputStream(fsRes.getInputStream()); + in = new DataInputStream(bis); + + try { + int n = in.readInt(); // number of entries + while (n-- > 0) { + String s = in.readUTF(); // target id + NodeId id = NodeId.valueOf(s); + int length = in.readInt(); // data length + byte[] data = new byte[length]; + in.readFully(data); // data + // store in map + refsStore.put(id, data); + } + } finally { + in.close(); + } + } + + /** + * Writes the content of the hash maps to the file system + * + * @throws Exception if an error occurs + */ + public synchronized void storeContents() throws Exception { + // write item states + FileSystemResource fsRes = new FileSystemResource(wspFS, STATE_FILE_PATH); + fsRes.makeParentDirs(); + BufferedOutputStream bos = new BufferedOutputStream(fsRes.getOutputStream()); + DataOutputStream out = new DataOutputStream(bos); + + try { + + out.writeInt(stateStore.size()); // number of entries + // entries + for (ItemId id : stateStore.keySet()) { + if (id.denotesNode()) { + out.writeByte(NODE_ENTRY); // entry type + } else { + out.writeByte(PROP_ENTRY); // entry type + } + out.writeUTF(id.toString()); // id + byte[] data = stateStore.get(id); + out.writeInt(data.length); // data length + out.write(data); // data + } + } finally { + out.close(); + } + + // write references + fsRes = new FileSystemResource(wspFS, REFS_FILE_PATH); + fsRes.makeParentDirs(); + bos = new BufferedOutputStream(fsRes.getOutputStream()); + out = new DataOutputStream(bos); + + try { + out.writeInt(refsStore.size()); // number of entries + // entries + for (NodeId id : refsStore.keySet()) { + out.writeUTF(id.toString()); // target id + byte[] data = refsStore.get(id); + out.writeInt(data.length); // data length + out.write(data); // data + } + } finally { + out.close(); + } + } + + //---------------------------------------------------< PersistenceManager > + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + + stateStore = new HashMap(initialCapacity, loadFactor); + refsStore = new HashMap(initialCapacity, loadFactor); + + wspFS = context.getFileSystem(); + + // Choose a FileSystem for the BlobStore based on whether data is persistent or not + if (persistent) { + blobFS = new LocalFileSystem(); + ((LocalFileSystem) blobFS).setRoot(new File(context.getHomeDir(), "blobs")); + } else { + blobFS = new MemoryFileSystem(); + } + blobFS.init(); + blobStore = new FileSystemBLOBStore(blobFS); + + if (persistent) { + // deserialize contents of state and refs stores + loadContents(); + } + + initialized = true; + } + + /** + * {@inheritDoc} + */ + public synchronized void close() throws Exception { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + if (persistent) { + // serialize contents of state and refs stores + storeContents(); + } else { + // clear out blob store + try { + String[] folders = blobFS.listFolders("/"); + for (String folder: folders) { + blobFS.deleteFolder(folder); + } + String[] files = blobFS.listFiles("/"); + for (String file : files) { + blobFS.deleteFile(file); + } + } catch (Exception e) { + // ignore + } + } + + // close BLOB file system + blobFS.close(); + blobFS = null; + blobStore = null; + + stateStore.clear(); + stateStore = null; + refsStore.clear(); + refsStore = null; + } finally { + initialized = false; + } + } + + /** + * {@inheritDoc} + */ + public synchronized NodeState load(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + byte[] data = stateStore.get(id); + if (data == null) { + throw new NoSuchItemStateException(id.toString()); + } + + ByteArrayInputStream in = new ByteArrayInputStream(data); + try { + NodeState state = createNew(id); + Serializer.deserialize(state, in); + return state; + } catch (Exception e) { + String msg = "failed to read node state: " + id; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized PropertyState load(PropertyId id) + throws NoSuchItemStateException, ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + byte[] data = stateStore.get(id); + if (data == null) { + throw new NoSuchItemStateException(id.toString()); + } + + ByteArrayInputStream in = new ByteArrayInputStream(data); + try { + PropertyState state = createNew(id); + Serializer.deserialize(state, in, blobStore); + return state; + } catch (Exception e) { + String msg = "failed to read property state: " + id; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void store(NodeState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize node state + Serializer.serialize(state, out); + + // store in serialized format in map for better memory efficiency + stateStore.put(state.getNodeId(), out.toByteArray()); + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to write node state: " + state.getNodeId(); + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void store(PropertyState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize property state + Serializer.serialize(state, out, blobStore); + + // store in serialized format in map for better memory efficiency + stateStore.put(state.getPropertyId(), out.toByteArray()); + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to store property state: " + state.getPropertyId(); + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void destroy(NodeState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // remove node state + stateStore.remove(state.getNodeId()); + } + + /** + * {@inheritDoc} + */ + protected void destroy(PropertyState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // delete binary values (stored as files) + InternalValue[] values = state.getValues(); + if (values != null) { + for (InternalValue val : values) { + if (val != null) { + val.deleteBinaryResource(); + } + } + } + + // remove property state + stateStore.remove(state.getPropertyId()); + } + + /** + * {@inheritDoc} + */ + public synchronized NodeReferences loadReferencesTo(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + byte[] data = refsStore.get(id); + if (data == null) { + throw new NoSuchItemStateException(id.toString()); + } + + ByteArrayInputStream in = new ByteArrayInputStream(data); + try { + NodeReferences refs = new NodeReferences(id); + Serializer.deserialize(refs, in); + return refs; + } catch (Exception e) { + String msg = "failed to load references: " + id; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void store(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize references + Serializer.serialize(refs, out); + + // store in serialized format in map for better memory efficiency + refsStore.put(refs.getTargetId(), out.toByteArray()); + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to store " + refs; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void destroy(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // remove node references + refsStore.remove(refs.getTargetId()); + } + + /** + * {@inheritDoc} + */ + public boolean exists(PropertyId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + return stateStore.containsKey(id); + } + + /** + * {@inheritDoc} + */ + public boolean exists(NodeId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + return stateStore.containsKey(id); + } + + /** + * {@inheritDoc} + */ + public boolean existsReferencesTo(NodeId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + return refsStore.containsKey(id); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/obj/ObjectPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/obj/ObjectPersistenceManager.java new file mode 100644 index 00000000000..681291ee16f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/obj/ObjectPersistenceManager.java @@ -0,0 +1,513 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.obj; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.fs.BasedFileSystem; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.apache.jackrabbit.core.persistence.AbstractPersistenceManager; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore; +import org.apache.jackrabbit.core.persistence.util.Serializer; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * ObjectPersistenceManager is a FileSystem-based + * PersistenceManager that persists ItemState + * and NodeReferences objects using a simple custom binary + * serialization format (see {@link Serializer}). + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public class ObjectPersistenceManager extends AbstractPersistenceManager { + + private static Logger log = LoggerFactory.getLogger(ObjectPersistenceManager.class); + + /** + * hexdigits for toString + */ + private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray(); + + private static final String NODEFILENAME = ".node"; + + private static final String NODEREFSFILENAME = ".references"; + + private boolean initialized; + + // file system where the item state is stored + private FileSystem itemStateFS; + // file system where BLOB data is stored + private FileSystem blobFS; + // BLOBStore that manages BLOB data in the file system + private BLOBStore blobStore; + + /** + * Creates a new ObjectPersistenceManager instance. + */ + public ObjectPersistenceManager() { + initialized = false; + } + + private static String buildNodeFolderPath(NodeId id) { + StringBuilder sb = new StringBuilder(); + char[] chars = id.toString().toCharArray(); + int cnt = 0; + for (int i = 0; i < chars.length; i++) { + if (chars[i] == '-') { + continue; + } + //if (cnt > 0 && cnt % 4 == 0) { + if (cnt == 2 || cnt == 4) { + sb.append(FileSystem.SEPARATOR_CHAR); + } + sb.append(chars[i]); + cnt++; + } + return sb.toString(); + } + + private static String buildPropFilePath(PropertyId id) { + String fileName; + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(id.getName().getNamespaceURI().getBytes()); + md5.update(id.getName().getLocalName().getBytes()); + byte[] bytes = md5.digest(); + char[] chars = new char[32]; + for (int i = 0, j = 0; i < 16; i++) { + chars[j++] = HEXDIGITS[(bytes[i] >> 4) & 0x0f]; + chars[j++] = HEXDIGITS[bytes[i] & 0x0f]; + } + fileName = new String(chars); + } catch (NoSuchAlgorithmException nsae) { + // should never get here as MD5 should always be available in the JRE + String msg = "MD5 not available: "; + log.error(msg, nsae); + throw new InternalError(msg + nsae); + } + return buildNodeFolderPath(id.getParentId()) + FileSystem.SEPARATOR + fileName; + } + + private static String buildNodeFilePath(NodeId id) { + return buildNodeFolderPath(id) + FileSystem.SEPARATOR + NODEFILENAME; + } + + private static String buildNodeReferencesFilePath(NodeId id) { + return buildNodeFolderPath(id) + FileSystem.SEPARATOR + NODEREFSFILENAME; + } + + //---------------------------------------------------< PersistenceManager > + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + + FileSystem wspFS = context.getFileSystem(); + itemStateFS = new BasedFileSystem(wspFS, "/data"); + + /** + * store BLOB data in local file system in a sub directory + * of the workspace home directory + */ + LocalFileSystem blobFS = new LocalFileSystem(); + blobFS.setRoot(new File(context.getHomeDir(), "blobs")); + blobFS.init(); + this.blobFS = blobFS; + blobStore = new FileSystemBLOBStore(blobFS); + + initialized = true; + } + + /** + * {@inheritDoc} + */ + public synchronized void close() throws Exception { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + // close BLOB file system + blobFS.close(); + blobFS = null; + blobStore = null; + /** + * there's no need close the item state store because it + * is based in the workspace's file system which is + * closed by the repository + */ + } finally { + initialized = false; + } + } + + /** + * {@inheritDoc} + */ + public synchronized NodeState load(NodeId id) + throws NoSuchItemStateException, ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String nodeFilePath = buildNodeFilePath(id); + + try { + if (!itemStateFS.isFile(nodeFilePath)) { + throw new NoSuchItemStateException(nodeFilePath); + } + } catch (FileSystemException fse) { + String msg = "failed to read node state: " + nodeFilePath; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + + try { + BufferedInputStream in = + new BufferedInputStream(itemStateFS.getInputStream(nodeFilePath)); + try { + NodeState state = createNew(id); + Serializer.deserialize(state, in); + return state; + } catch (Exception e) { + String msg = "failed to read node state: " + id; + log.debug(msg); + throw new ItemStateException(msg, e); + } finally { + in.close(); + } + } catch (Exception e) { + String msg = "failed to read node state: " + nodeFilePath; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized PropertyState load(PropertyId id) + throws NoSuchItemStateException, ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String propFilePath = buildPropFilePath(id); + + try { + if (!itemStateFS.isFile(propFilePath)) { + throw new NoSuchItemStateException(propFilePath); + } + } catch (FileSystemException fse) { + String msg = "failed to read property state: " + propFilePath; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + + try { + BufferedInputStream in = + new BufferedInputStream(itemStateFS.getInputStream(propFilePath)); + try { + PropertyState state = createNew(id); + Serializer.deserialize(state, in, blobStore); + return state; + } finally { + in.close(); + } + } catch (Exception e) { + String msg = "failed to read property state: " + propFilePath; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized NodeReferences loadReferencesTo(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String refsFilePath = buildNodeReferencesFilePath(id); + + try { + if (!itemStateFS.isFile(refsFilePath)) { + throw new NoSuchItemStateException(id.toString()); + } + } catch (FileSystemException fse) { + String msg = "failed to load references: " + id; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + + try { + BufferedInputStream in = + new BufferedInputStream(itemStateFS.getInputStream(refsFilePath)); + try { + NodeReferences refs = new NodeReferences(id); + Serializer.deserialize(refs, in); + return refs; + } finally { + in.close(); + } + } catch (Exception e) { + String msg = "failed to load references: " + id; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void store(NodeState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String nodeFilePath = buildNodeFilePath(state.getNodeId()); + FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath); + try { + nodeFile.makeParentDirs(); + BufferedOutputStream out = new BufferedOutputStream(nodeFile.getOutputStream()); + try { + // serialize node state + Serializer.serialize(state, out); + } finally { + out.close(); + } + } catch (Exception e) { + String msg = "failed to write node state: " + state.getNodeId(); + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void store(PropertyState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String propFilePath = buildPropFilePath(state.getPropertyId()); + FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath); + try { + propFile.makeParentDirs(); + BufferedOutputStream out = new BufferedOutputStream(propFile.getOutputStream()); + try { + // serialize property state + Serializer.serialize(state, out, blobStore); + } finally { + out.close(); + } + } catch (Exception e) { + String msg = "failed to store property state: " + state.getParentId() + "/" + state.getName(); + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void store(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String refsFilePath = buildNodeReferencesFilePath(refs.getTargetId()); + FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath); + try { + refsFile.makeParentDirs(); + OutputStream out = new BufferedOutputStream(refsFile.getOutputStream()); + try { + Serializer.serialize(refs, out); + } finally { + out.close(); + } + } catch (Exception e) { + String msg = "failed to store " + refs; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void destroy(NodeState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String nodeFilePath = buildNodeFilePath(state.getNodeId()); + FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath); + try { + if (nodeFile.exists()) { + // delete resource and prune empty parent folders + nodeFile.delete(true); + } + } catch (FileSystemException fse) { + String msg = "failed to delete node state: " + state.getNodeId(); + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + protected void destroy(PropertyState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // delete binary values (stored as files) + InternalValue[] values = state.getValues(); + if (values != null) { + for (int i = 0; i < values.length; i++) { + InternalValue val = values[i]; + if (val != null) { + val.deleteBinaryResource(); + } + } + } + // delete property file + String propFilePath = buildPropFilePath(state.getPropertyId()); + FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath); + try { + if (propFile.exists()) { + // delete resource and prune empty parent folders + propFile.delete(true); + } + } catch (FileSystemException fse) { + String msg = "failed to delete property state: " + state.getParentId() + "/" + state.getName(); + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + protected void destroy(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String refsFilePath = buildNodeReferencesFilePath(refs.getTargetId()); + FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath); + try { + if (refsFile.exists()) { + // delete resource and prune empty parent folders + refsFile.delete(true); + } + } catch (FileSystemException fse) { + String msg = "failed to delete " + refs; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + public synchronized boolean exists(PropertyId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + String propFilePath = buildPropFilePath(id); + FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath); + return propFile.exists(); + } catch (FileSystemException fse) { + String msg = "failed to check existence of item state: " + id; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + public synchronized boolean exists(NodeId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + String nodeFilePath = buildNodeFilePath(id); + FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath); + return nodeFile.exists(); + } catch (FileSystemException fse) { + String msg = "failed to check existence of item state: " + id; + log.error(msg, fse); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + public synchronized boolean existsReferencesTo(NodeId id) + throws ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + String refsFilePath = buildNodeReferencesFilePath(id); + FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath); + return refsFile.exists(); + } catch (FileSystemException fse) { + String msg = "failed to check existence of references: " + id; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/BundleDbPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/BundleDbPersistenceManager.java new file mode 100644 index 00000000000..8d5a6cd0de8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/BundleDbPersistenceManager.java @@ -0,0 +1,1303 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.pool; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.sql.DataSource; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.bundle.AbstractBundlePersistenceManager; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.persistence.util.BundleBinding; +import org.apache.jackrabbit.core.persistence.util.ErrorHandling; +import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore; +import org.apache.jackrabbit.core.persistence.util.NodeInfo; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle; +import org.apache.jackrabbit.core.persistence.util.Serializer; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.util.StringIndex; +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DatabaseAware; +import org.apache.jackrabbit.core.util.db.DbUtility; +import org.apache.jackrabbit.core.util.db.StreamWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a generic persistence manager that stores the {@link NodePropBundle}s + * in a database. + *

    + * Configuration:
    + *

      + *
    • <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/> + *
    • <param name="{@link #setConsistencyCheck(String) consistencyCheck}" value="false"/> + *
    • <param name="{@link #setConsistencyFix(String) consistencyFix}" value="false"/> + *
    • <param name="{@link #setMinBlobSize(String) minBlobSize}" value="4096"/> + *
    • <param name="{@link #setDriver(String) driver}" value=""/> + *
    • <param name="{@link #setUrl(String) url}" value=""/> + *
    • <param name="{@link #setUser(String) user}" value=""/> + *
    • <param name="{@link #setPassword(String) password}" value=""/> + *
    • <param name="{@link #setDatabaseType(String) databaseType}" value=""/> + *
    • <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value=""/> + *
    • <param name="{@link #setErrorHandling(String) errorHandling}" value=""/> + *
    • <param name="{@link #setBlockOnConnectionLoss(String) blockOnConnectionLoss}" value="false"/> + *
    • <param name="{@link #setSchemaCheckEnabled(boolean) schemaCheckEnabled}" value="true"/> + *
    + */ +public class BundleDbPersistenceManager + extends AbstractBundlePersistenceManager implements DatabaseAware { + + /** the default logger */ + private static Logger log = LoggerFactory.getLogger(BundleDbPersistenceManager.class); + + /** storage model modifier: binary keys */ + public static final int SM_BINARY_KEYS = 1; + + /** storage model modifier: longlong keys */ + public static final int SM_LONGLONG_KEYS = 2; + + /** flag indicating if this manager was initialized */ + protected boolean initialized; + + /** the jdbc driver name */ + protected String driver; + + /** the jdbc url string */ + protected String url; + + /** the jdbc user */ + protected String user; + + /** the jdbc password */ + protected String password; + + /** the database type */ + protected String databaseType; + + /** the logical name of the data source to use */ + protected String dataSourceName; + + /** the {@link ConnectionHelper} set in the {@link #init(PMContext)} method */ + protected ConnectionHelper conHelper; + + /** the prefix for the database objects */ + protected String schemaObjectPrefix; + + /** flag indicating if a consistency check should be issued during startup */ + protected boolean consistencyCheck; + + /** flag indicating if the consistency check should attempt to fix issues */ + protected boolean consistencyFix; + + /** initial size of buffer used to serialize objects */ + protected static final int INITIAL_BUFFER_SIZE = 1024; + + /** indicates if uses (filesystem) blob store */ + protected boolean externalBLOBs; + + /** indicates whether to block if the database connection is lost */ + protected boolean blockOnConnectionLoss; + + // SQL statements for bundle management + protected String bundleInsertSQL; + protected String bundleUpdateSQL; + protected String bundleSelectSQL; + protected String bundleDeleteSQL; + protected String bundleSelectAllIdsFromSQL; + protected String bundleSelectAllIdsSQL; + protected String bundleSelectAllBundlesFromSQL; + protected String bundleSelectAllBundlesSQL; + + // SQL statements for NodeReference management + protected String nodeReferenceInsertSQL; + protected String nodeReferenceUpdateSQL; + protected String nodeReferenceSelectSQL; + protected String nodeReferenceDeleteSQL; + + /** file system where BLOB data is stored */ + protected CloseableBLOBStore blobStore; + + /** the index for local names */ + private StringIndex nameIndex; + + /** + * the minimum size of a property until it gets written to the blob store + * @see #setMinBlobSize(String) + */ + private int minBlobSize = 0x1000; + + /** + * flag for error handling + */ + protected ErrorHandling errorHandling = new ErrorHandling(); + + /** + * the bundle binding + */ + protected BundleBinding binding; + + /** + * the name of this persistence manager + */ + private String name = super.toString(); + + /** + * Whether the schema check must be done during initialization. + */ + private boolean schemaCheckEnabled = true; + + /** + * The repositories {@link ConnectionFactory}. + */ + private ConnectionFactory connectionFactory; + + /** + * {@inheritDoc} + */ + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * Returns the configured JDBC connection url. + * @return the configured JDBC connection url. + */ + public String getUrl() { + return url; + } + + /** + * Sets the JDBC connection URL. + * The connection can be created using a JNDI Data Source as well. + * To do that, the driver class name must reference a javax.naming.Context class + * (for example javax.naming.InitialContext), and the URL must be the JNDI URL + * (for example java:comp/env/jdbc/Test). + * + * @param url the url to set. + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Returns the configured user that is used to establish JDBC connections. + * @return the JDBC user. + */ + public String getUser() { + return user; + } + + /** + * Sets the user name that will be used to establish JDBC connections. + * @param user the user name. + */ + public void setUser(String user) { + this.user = user; + } + + /** + * Returns the configured password that is used to establish JDBC connections. + * @return the password. + */ + public String getPassword() { + return password; + } + + /** + * Sets the password that will be used to establish JDBC connections. + * @param password the password for the connection + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Returns the class name of the JDBC driver. + * @return the class name of the JDBC driver. + */ + public String getDriver() { + return driver; + } + + /** + * Sets the class name of the JDBC driver. The driver class will be loaded + * during {@link #init(PMContext) init} in order to assure the existence. + * If no driver is specified, the default driver for the database is used. + * + * @param driver the class name of the driver + */ + public void setDriver(String driver) { + this.driver = driver; + } + + /** + * Returns the configured schema object prefix. + * @return the configured schema object prefix. + */ + public String getSchemaObjectPrefix() { + return schemaObjectPrefix; + } + + /** + * Sets the schema object prefix. This string is used to prefix all schema + * objects, like tables and indexes. this is useful, if several persistence + * managers use the same database. + * + * @param schemaObjectPrefix the prefix for schema objects. + */ + public void setSchemaObjectPrefix(String schemaObjectPrefix) { + // make sure prefix is all uppercase + this.schemaObjectPrefix = schemaObjectPrefix.toUpperCase(); + } + + /** + * Returns the configured database type name. + * @deprecated + * This method is deprecated; {@link getDatabaseType} should be used instead. + * + * @return the database type name. + */ + public String getSchema() { + return databaseType; + } + + /** + * Returns the configured database type name. + * @return the database type name. + */ + public String getDatabaseType() { + return databaseType; + } + + /** + * Sets the database type. This identifier is used to load and execute + * the respective .ddl resource in order to create the required schema + * objects. + * @deprecated + * This method is deprecated; {@link setDatabaseType} should be used instead. + * + * @param databaseType database type name + */ + public void setSchema(String databaseType) { + this.databaseType = databaseType; + } + + /** + * Sets the database type. This identifier is used to load and execute + * the respective .ddl resource in order to create the required schema + * objects. + * + * @param databaseType database type name + */ + public void setDatabaseType(String databaseType) { + this.databaseType = databaseType; + } + + public String getDataSourceName() { + return dataSourceName; + } + + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + /** + * Returns if uses external (filesystem) blob store. + * @return if uses external (filesystem) blob store. + */ + public boolean isExternalBLOBs() { + return externalBLOBs; + } + + /** + * Sets the flag for external (filesystem) blob store usage. + * @param externalBLOBs a value of "true" indicates that an external blob + * store is to be used. + */ + public void setExternalBLOBs(boolean externalBLOBs) { + this.externalBLOBs = externalBLOBs; + } + + /** + * Checks if consistency check is enabled. + * @return true if consistency check is enabled. + */ + public String getConsistencyCheck() { + return Boolean.toString(consistencyCheck); + } + + /** + * Defines if a consistency check is to be performed on initialization. + * @param consistencyCheck the consistency check flag. + */ + public void setConsistencyCheck(String consistencyCheck) { + this.consistencyCheck = Boolean.valueOf(consistencyCheck).booleanValue(); + } + + /** + * Checks if consistency fix is enabled. + * @return true if consistency fix is enabled. + */ + public String getConsistencyFix() { + return Boolean.toString(consistencyFix); + } + + /** + * Defines if the consistency check should attempt to fix issues that + * it finds. + * + * @param consistencyFix the consistency fix flag. + */ + public void setConsistencyFix(String consistencyFix) { + this.consistencyFix = Boolean.valueOf(consistencyFix).booleanValue(); + } + + /** + * Returns the minimum blob size in bytes. + * @return the minimum blob size in bytes. + */ + public String getMinBlobSize() { + return String.valueOf(minBlobSize); + } + + /** + * Sets the minimum blob size. This size defines the threshold of which + * size a property is included in the bundle or is stored in the blob store. + * + * @param minBlobSize the minimum blob size in bytes. + */ + public void setMinBlobSize(String minBlobSize) { + this.minBlobSize = Integer.decode(minBlobSize).intValue(); + } + + /** + * Sets the error handling behaviour of this manager. See {@link ErrorHandling} + * for details about the flags. + * + * @param errorHandling the error handling flags + */ + public void setErrorHandling(String errorHandling) { + this.errorHandling = new ErrorHandling(errorHandling); + } + + /** + * Returns the error handling configuration of this manager + * @return the error handling configuration of this manager + */ + public String getErrorHandling() { + return errorHandling.toString(); + } + + public void setBlockOnConnectionLoss(String block) { + this.blockOnConnectionLoss = Boolean.valueOf(block).booleanValue(); + } + + public String getBlockOnConnectionLoss() { + return Boolean.toString(blockOnConnectionLoss); + } + + /** + * Returns true if the blobs are stored in the DB. + * @return true if the blobs are stored in the DB. + */ + public boolean useDbBlobStore() { + return !externalBLOBs; + } + + /** + * Returns true if the blobs are stored in the local fs. + * @return true if the blobs are stored in the local fs. + */ + public boolean useLocalFsBlobStore() { + return externalBLOBs; + } + + /** + * @return whether the schema check is enabled + */ + public final boolean isSchemaCheckEnabled() { + return schemaCheckEnabled; + } + + /** + * @param enabled set whether the schema check is enabled + */ + public final void setSchemaCheckEnabled(boolean enabled) { + schemaCheckEnabled = enabled; + } + + /** + * {@inheritDoc} + * + * Basically wraps a JDBC transaction around super.store(). + * + * FIXME: the retry logic is almost a duplicate of {@code ConnectionHelper.RetryManager}. + */ + public synchronized void store(final ChangeLog changeLog) throws ItemStateException { + int failures = 0; + ItemStateException lastException = null; + boolean sleepInterrupted = false; + while (!sleepInterrupted && (blockOnConnectionLoss || failures <= 1)) { + try { + conHelper.startBatch(); + super.store(changeLog); + conHelper.endBatch(true); + return; + } catch (SQLException e) { + // Either startBatch or stopBatch threw it: either way the + // transaction was not persisted and no action needs to be taken. + lastException = new ItemStateException(e.getMessage(), e); + } catch (ItemStateException e) { + // store call threw it: we need to cancel the transaction + lastException = e; + try { + conHelper.endBatch(false); + } catch (SQLException e2) { + DbUtility.logException("rollback failed", e2); + } + + // if we got here due to a constraint violation and we + // are running in test mode, we really want to stop + assert !isIntegrityConstraintViolation(e.getCause()); + } + failures++; + log.error("Failed to persist ChangeLog (stacktrace on DEBUG log level), blockOnConnectionLoss = " + + blockOnConnectionLoss + ": " + lastException); + log.debug("Failed to persist ChangeLog", lastException); + if (blockOnConnectionLoss || failures <= 1) { // if we're going to try again + try { + Thread.sleep(100); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + sleepInterrupted = true; + log.error("Interrupted: canceling retry of ChangeLog storage"); + } + } + } + throw lastException; + } + + private boolean isIntegrityConstraintViolation(Throwable t) { + if (t instanceof SQLException) { + String state = ((SQLException) t).getSQLState(); + return state != null && state.startsWith("23"); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + super.init(context); + + conHelper = createConnectionHelper(getDataSource()); + + this.name = context.getHomeDir().getName(); + + // make sure schemaObjectPrefix consists of legal name characters only + schemaObjectPrefix = conHelper.prepareDbIdentifier(schemaObjectPrefix); + + // check if schema objects exist and create them if necessary + if (isSchemaCheckEnabled()) { + createCheckSchemaOperation().run(); + } + + // create correct blob store + blobStore = createBlobStore(); + + buildSQLStatements(); + + // load namespaces + binding = new BundleBinding(errorHandling, blobStore, getNsIndex(), getNameIndex(), context.getDataStore()); + binding.setMinBlobSize(minBlobSize); + + initialized = true; + + if (consistencyCheck) { + // check all bundles + checkConsistency(null, true, consistencyFix); + } + + } + + private DataSource getDataSource() throws Exception { + if (getDataSourceName() == null || "".equals(getDataSourceName())) { + return connectionFactory.getDataSource(getDriver(), getUrl(), getUser(), getPassword()); + } else { + String dbType = connectionFactory.getDataBaseType(dataSourceName); + if (BundleDbPersistenceManager.class.getResourceAsStream(dbType + ".ddl") != null) { + setDatabaseType(dbType); + } + return connectionFactory.getDataSource(dataSourceName); + } + } + + /** + * This method is called from the {@link #init(PMContext)} method of this class and returns a + * {@link ConnectionHelper} instance which is assigned to the {@code conHelper} field. Subclasses may + * override it to return a specialized connection helper. + * + * @param dataSrc the {@link DataSource} of this persistence manager + * @return a {@link ConnectionHelper} + * @throws Exception on error + */ + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + return new ConnectionHelper(dataSrc, blockOnConnectionLoss); + } + + /** + * This method is called from {@link #init(PMContext)} after the + * {@link #createConnectionHelper(DataSource)} method, and returns a default {@link CheckSchemaOperation}. + * Subclasses can overrride this implementation to get a customized implementation. + * + * @return a new {@link CheckSchemaOperation} instance + */ + protected CheckSchemaOperation createCheckSchemaOperation() { + InputStream in = + AbstractBundlePersistenceManager.class.getResourceAsStream( + databaseType + ".ddl"); + return new CheckSchemaOperation(conHelper, in, schemaObjectPrefix + "BUNDLE").addVariableReplacement( + CheckSchemaOperation.SCHEMA_OBJECT_PREFIX_VARIABLE, schemaObjectPrefix); + } + + /** + * {@inheritDoc} + */ + @Override + protected BLOBStore getBlobStore() { + return blobStore; + } + + /** + * Creates a suitable blobstore + * @return a blobstore + * @throws Exception if an unspecified error occurs + */ + protected CloseableBLOBStore createBlobStore() throws Exception { + if (useLocalFsBlobStore()) { + return createLocalFSBlobStore(context); + } else { + return createDBBlobStore(context); + } + } + + /** + * Returns the local name index + * @return the local name index + * @throws IllegalStateException if an error occurs. + */ + public StringIndex getNameIndex() { + try { + if (nameIndex == null) { + FileSystemResource res = new FileSystemResource(context.getFileSystem(), RES_NAME_INDEX); + if (res.exists()) { + nameIndex = super.getNameIndex(); + } else { + // create db nameindex + nameIndex = createDbNameIndex(); + } + } + return nameIndex; + } catch (Exception e) { + IllegalStateException exception = + new IllegalStateException("Unable to create nsIndex"); + exception.initCause(e); + throw exception; + } + } + + /** + * Returns a new instance of a DbNameIndex. + * @return a new instance of a DbNameIndex. + * @throws SQLException if an SQL error occurs. + */ + protected DbNameIndex createDbNameIndex() throws SQLException { + return new DbNameIndex(conHelper, schemaObjectPrefix); + } + + /** + * returns the storage model + * @return the storage model + */ + public int getStorageModel() { + return SM_BINARY_KEYS; + } + + /** + * Creates a blob store that is based on a local fs. This is called by + * init if {@link #useLocalFsBlobStore()} returns true. + * + * @param context the persistence manager context + * @return a blob store + * @throws Exception if an error occurs. + */ + protected CloseableBLOBStore createLocalFSBlobStore(PMContext context) + throws Exception { + /** + * store blob's in local file system in a sub directory + * of the workspace home directory + */ + LocalFileSystem blobFS = new LocalFileSystem(); + blobFS.setRoot(new File(context.getHomeDir(), "blobs")); + blobFS.init(); + return new FSBlobStore(blobFS); + } + + /** + * Creates a blob store that uses the database. This is called by + * init if {@link #useDbBlobStore()} returns true. + * + * @param context the persistence manager context + * + * @return a blob store + * @throws Exception if an error occurs. + */ + protected CloseableBLOBStore createDBBlobStore(PMContext context) + throws Exception { + return new DbBlobStore(); + } + + /** + * {@inheritDoc} + */ + public synchronized void close() throws Exception { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + if (nameIndex instanceof DbNameIndex) { + ((DbNameIndex) nameIndex).close(); + } + // close blob store + blobStore.close(); + blobStore = null; + super.close(); + } finally { + initialized = false; + } + } + + /** + * Constructs a parameter list for a PreparedStatement + * for the given node identifier. + * + * @param id the node id + * @return a list of Objects + */ + protected Object[] getKey(NodeId id) { + if (getStorageModel() == SM_BINARY_KEYS) { + return new Object[] { id.getRawBytes() }; + } else { + return new Object[] { + id.getMostSignificantBits(), id.getLeastSignificantBits() }; + } + } + + /** + * Creates a parameter array for an SQL statement that needs + * (i) a node identifier, and (2) another parameter. + * + * @param id the node id + * @param p the other parameter + * @param before whether the other parameter should be before the uuid parameter + * @return an Object array that represents the parameters + */ + protected Object[] createParams(NodeId id, Object p, boolean before) { + + // Create the key + List key = new ArrayList(); + if (getStorageModel() == SM_BINARY_KEYS) { + key.add(id.getRawBytes()); + } else { + key.add(id.getMostSignificantBits()); + key.add(id.getLeastSignificantBits()); + } + + // Create the parameters + List params = new ArrayList(); + if (before) { + params.add(p); + params.addAll(key); + } else { + params.addAll(key); + params.add(p); + } + + return params.toArray(); + } + + /** + * {@inheritDoc} + */ + public synchronized List getAllNodeIds(NodeId bigger, int maxCount) + throws ItemStateException, RepositoryException { + ResultSet rs = null; + try { + String sql = bundleSelectAllIdsSQL; + NodeId lowId = null; + Object[] keys = new Object[0]; + if (bigger != null) { + sql = bundleSelectAllIdsFromSQL; + lowId = bigger; + keys = getKey(bigger); + } + if (getStorageModel() == SM_LONGLONG_KEYS && maxCount > 0) { + // get some more rows, in case the first row is smaller + // only required for SM_LONGLONG_KEYS + // probability is very low to get get the wrong first key, < 1 : 2^64 + // see also bundleSelectAllIdsFrom SQL statement + maxCount += 10; + } + rs = conHelper.exec(sql, keys, false, maxCount); + ArrayList result = new ArrayList(); + while ((maxCount == 0 || result.size() < maxCount) && rs.next()) { + NodeId current; + if (getStorageModel() == SM_BINARY_KEYS) { + current = new NodeId(rs.getBytes(1)); + } else { + long high = rs.getLong(1); + long low = rs.getLong(2); + current = new NodeId(high, low); + if (lowId != null) { + // skip the keys that are smaller or equal (see above, maxCount += 10) + // only required for SM_LONGLONG_KEYS + if (current.compareTo(lowId) <= 0) { + continue; + } + } + } + result.add(current); + } + return result; + } catch (SQLException e) { + String msg = "getAllNodeIds failed."; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + DbUtility.close(rs); + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized Map getAllNodeInfos(NodeId bigger, int maxCount) throws ItemStateException { + ResultSet rs = null; + try { + String sql = bundleSelectAllBundlesSQL; + NodeId lowId = null; + Object[] keys = new Object[0]; + if (bigger != null) { + sql = bundleSelectAllBundlesFromSQL; + lowId = bigger; + keys = getKey(bigger); + } + if (getStorageModel() == SM_LONGLONG_KEYS && maxCount > 0) { + // get some more rows, in case the first row is smaller + // only required for SM_LONGLONG_KEYS + // probability is very low to get get the wrong first key, < 1 : 2^64 + // see also bundleSelectAllIdsFrom SQL statement + maxCount += 10; + } + rs = conHelper.exec(sql, keys, false, maxCount); + Map result = new LinkedHashMap(maxCount); + while ((maxCount == 0 || result.size() < maxCount) && rs.next()) { + NodeId current; + if (getStorageModel() == SM_BINARY_KEYS) { + current = new NodeId(rs.getBytes(1)); + } else { + long high = rs.getLong(1); + long low = rs.getLong(2); + current = new NodeId(high, low); + } + if (getStorageModel() == SM_LONGLONG_KEYS && lowId != null) { + // skip the keys that are smaller or equal (see above, maxCount += 10) + if (current.compareTo(lowId) <= 0) { + continue; + } + } + NodePropBundle bundle = readBundle(current, rs, getStorageModel() == SM_LONGLONG_KEYS ? 3 : 2); + NodeInfo nodeInfo = new NodeInfo(bundle); + result.put(nodeInfo.getId(), nodeInfo); + } + return result; + } catch (SQLException e) { + String msg = "getAllNodeIds failed."; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + DbUtility.close(rs); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected NodePropBundle loadBundle(NodeId id) throws ItemStateException { + try { + ResultSet rs = + conHelper.exec(bundleSelectSQL, getKey(id), false, 0); + try { + if (rs != null && rs.next()) { + return readBundle(id, rs, 1); + } else { + return null; + } + } finally { + if (rs != null) { + rs.close(); + } + } + } catch (SQLException e) { + String msg = "failed to read bundle (stacktrace on DEBUG log level): " + id + ": " + e; + log.error(msg); + log.debug("failed to read bundle: " + id, e); + throw new ItemStateException(msg, e); + } + } + + /** + * Reads and parses a bundle from the BLOB in the given column of the + * current row of the given result set. This is a helper method to + * circumvent issues JCR-1039 and JCR-1474. + * + * @param id bundle identifier + * @param rs result set + * @param column BLOB column + * @return parsed bundle + * @throws SQLException if the bundle can not be read or parsed + */ + private NodePropBundle readBundle(NodeId id, ResultSet rs, int column) + throws SQLException { + try { + InputStream in; + if (rs.getMetaData().getColumnType(column) == Types.BLOB) { + in = rs.getBlob(column).getBinaryStream(); + } else { + in = rs.getBinaryStream(column); + } + try { + return binding.readBundle(in, id); + } finally { + in.close(); + } + } catch (IOException e) { + SQLException exception = + new SQLException("Failed to parse bundle " + id); + exception.initCause(e); + throw exception; + } + } + + /** + * {@inheritDoc} + */ + protected synchronized void storeBundle(NodePropBundle bundle) throws ItemStateException { + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + binding.writeBundle(out, bundle); + + String sql = bundle.isNew() ? bundleInsertSQL : bundleUpdateSQL; + Object[] params = createParams(bundle.getId(), out.toByteArray(), true); + conHelper.update(sql, params); + } catch (Exception e) { + String msg; + + if (isIntegrityConstraintViolation(e)) { + // we should never get an integrity constraint violation here + // other PMs may not be able to detect this and end up with + // corrupted data + msg = "FATAL error while writing the bundle: " + bundle.getId(); + } else { + msg = "failed to write bundle: " + bundle.getId(); + } + + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected synchronized void destroyBundle(NodePropBundle bundle) throws ItemStateException { + try { + conHelper.update(bundleDeleteSQL, getKey(bundle.getId())); + } catch (Exception e) { + if (e instanceof NoSuchItemStateException) { + throw (NoSuchItemStateException) e; + } + String msg = "failed to delete bundle: " + bundle.getId(); + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized NodeReferences loadReferencesTo(NodeId targetId) + throws NoSuchItemStateException, ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + ResultSet rs = null; + InputStream in = null; + try { + rs = conHelper.exec(nodeReferenceSelectSQL, getKey(targetId), false, 0); + if (!rs.next()) { + throw new NoSuchItemStateException(targetId.toString()); + } + + in = rs.getBinaryStream(1); + NodeReferences refs = new NodeReferences(targetId); + Serializer.deserialize(refs, in); + + return refs; + } catch (Exception e) { + if (e instanceof NoSuchItemStateException) { + throw (NoSuchItemStateException) e; + } + String msg = "failed to read references: " + targetId; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + IOUtils.closeQuietly(in); + DbUtility.close(rs); + } + } + + /** + * {@inheritDoc} + * + * This method uses shared PreparedStatements, which must + * be used strictly sequentially. Because this method synchronizes on the + * persistence manager instance, there is no need to synchronize on the + * shared statement. If the method would not be synchronized, the shared + * statement must be synchronized. + */ + public synchronized void store(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // check if insert or update + boolean update = existsReferencesTo(refs.getTargetId()); + String sql = (update) ? nodeReferenceUpdateSQL : nodeReferenceInsertSQL; + + try { + ByteArrayOutputStream out = + new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); + // serialize references + Serializer.serialize(refs, out); + + Object[] params = createParams(refs.getTargetId(), out.toByteArray(), true); + conHelper.exec(sql, params); + + // there's no need to close a ByteArrayOutputStream + //out.close(); + } catch (Exception e) { + String msg = "failed to write " + refs; + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized void destroy(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + conHelper.exec(nodeReferenceDeleteSQL, getKey(refs.getTargetId())); + } catch (Exception e) { + if (e instanceof NoSuchItemStateException) { + throw (NoSuchItemStateException) e; + } + String msg = "failed to delete " + refs; + log.error(msg, e); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + public synchronized boolean existsReferencesTo(NodeId targetId) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + ResultSet rs = null; + try { + rs = conHelper.exec(nodeReferenceSelectSQL, getKey(targetId), false, 0); + + // a reference exists if the result has at least one entry + return rs.next(); + } catch (Exception e) { + String msg = "failed to check existence of node references: " + + targetId; + log.error(msg, e); + throw new ItemStateException(msg, e); + } finally { + DbUtility.close(rs); + } + } + + /** + * {@inheritDoc} + */ + public String toString() { + return name; + } + + /** + * Initializes the SQL strings. + */ + protected void buildSQLStatements() { + // prepare statements + if (getStorageModel() == SM_BINARY_KEYS) { + bundleInsertSQL = "insert into " + schemaObjectPrefix + "BUNDLE (BUNDLE_DATA, NODE_ID) values (?, ?)"; + bundleUpdateSQL = "update " + schemaObjectPrefix + "BUNDLE set BUNDLE_DATA = ? where NODE_ID = ?"; + bundleSelectSQL = "select BUNDLE_DATA from " + schemaObjectPrefix + "BUNDLE where NODE_ID = ?"; + bundleDeleteSQL = "delete from " + schemaObjectPrefix + "BUNDLE where NODE_ID = ?"; + + nodeReferenceInsertSQL = "insert into " + schemaObjectPrefix + "REFS (REFS_DATA, NODE_ID) values (?, ?)"; + nodeReferenceUpdateSQL = "update " + schemaObjectPrefix + "REFS set REFS_DATA = ? where NODE_ID = ?"; + nodeReferenceSelectSQL = "select REFS_DATA from " + schemaObjectPrefix + "REFS where NODE_ID = ?"; + nodeReferenceDeleteSQL = "delete from " + schemaObjectPrefix + "REFS where NODE_ID = ?"; + + bundleSelectAllIdsSQL = "select NODE_ID from " + schemaObjectPrefix + "BUNDLE ORDER BY NODE_ID"; + bundleSelectAllIdsFromSQL = "select NODE_ID from " + schemaObjectPrefix + "BUNDLE WHERE NODE_ID > ? ORDER BY NODE_ID"; + bundleSelectAllBundlesSQL = "select NODE_ID, BUNDLE_DATA from " + schemaObjectPrefix + "BUNDLE ORDER BY NODE_ID"; + bundleSelectAllBundlesFromSQL = "select NODE_ID, BUNDLE_DATA from " + schemaObjectPrefix + "BUNDLE WHERE NODE_ID > ? ORDER BY NODE_ID"; + } else { + bundleInsertSQL = "insert into " + schemaObjectPrefix + "BUNDLE (BUNDLE_DATA, NODE_ID_HI, NODE_ID_LO) values (?, ?, ?)"; + bundleUpdateSQL = "update " + schemaObjectPrefix + "BUNDLE set BUNDLE_DATA = ? where NODE_ID_HI = ? and NODE_ID_LO = ?"; + bundleSelectSQL = "select BUNDLE_DATA from " + schemaObjectPrefix + "BUNDLE where NODE_ID_HI = ? and NODE_ID_LO = ?"; + bundleDeleteSQL = "delete from " + schemaObjectPrefix + "BUNDLE where NODE_ID_HI = ? and NODE_ID_LO = ?"; + + nodeReferenceInsertSQL = + "insert into " + schemaObjectPrefix + "REFS" + + " (REFS_DATA, NODE_ID_HI, NODE_ID_LO) values (?, ?, ?)"; + nodeReferenceUpdateSQL = + "update " + schemaObjectPrefix + "REFS" + + " set REFS_DATA = ? where NODE_ID_HI = ? and NODE_ID_LO = ?"; + nodeReferenceSelectSQL = "select REFS_DATA from " + schemaObjectPrefix + "REFS where NODE_ID_HI = ? and NODE_ID_LO = ?"; + nodeReferenceDeleteSQL = "delete from " + schemaObjectPrefix + "REFS where NODE_ID_HI = ? and NODE_ID_LO = ?"; + + bundleSelectAllIdsSQL = "select NODE_ID_HI, NODE_ID_LO from " + schemaObjectPrefix + + "BUNDLE ORDER BY NODE_ID_HI, NODE_ID_LO"; + // need to use HI and LO parameters + // this is not the exact statement, but not all databases support WHERE (NODE_ID_HI, NODE_ID_LOW) >= (?, ?) + bundleSelectAllIdsFromSQL = + "select NODE_ID_HI, NODE_ID_LO from " + schemaObjectPrefix + "BUNDLE" + + " WHERE (NODE_ID_HI >= ?) AND (? IS NOT NULL)" + + " ORDER BY NODE_ID_HI, NODE_ID_LO"; + + bundleSelectAllBundlesSQL = "select NODE_ID_HI, NODE_ID_LO, BUNDLE_DATA from " + schemaObjectPrefix + + "BUNDLE ORDER BY NODE_ID_HI, NODE_ID_LO"; + // need to use HI and LO parameters + // this is not the exact statement, but not all databases support WHERE (NODE_ID_HI, NODE_ID_LOW) >= (?, ?) + bundleSelectAllBundlesFromSQL = + "select NODE_ID_HI, NODE_ID_LO, BUNDLE_DATA from " + schemaObjectPrefix + "BUNDLE" + + " WHERE (NODE_ID_HI >= ?) AND (? IS NOT NULL)" + + " ORDER BY NODE_ID_HI, NODE_ID_LO"; + + } + + } + + /** + * Helper interface for closeable stores + */ + protected static interface CloseableBLOBStore extends BLOBStore { + void close(); + } + + /** + * own implementation of the filesystem blob store that uses a different + * blob-id scheme. + */ + protected class FSBlobStore extends FileSystemBLOBStore implements CloseableBLOBStore { + + private FileSystem fs; + + public FSBlobStore(FileSystem fs) { + super(fs); + this.fs = fs; + } + + public String createId(PropertyId id, int index) { + return buildBlobFilePath(null, id, index).toString(); + } + + public void close() { + try { + fs.close(); + fs = null; + } catch (Exception e) { + // ignore + } + } + } + + /** + * Implementation of a blob store that stores the data inside the database + */ + protected class DbBlobStore implements CloseableBLOBStore { + + protected String blobInsertSQL; + protected String blobUpdateSQL; + protected String blobSelectSQL; + protected String blobSelectExistSQL; + protected String blobDeleteSQL; + + public DbBlobStore() throws SQLException { + blobInsertSQL = "insert into " + schemaObjectPrefix + "BINVAL (BINVAL_DATA, BINVAL_ID) values (?, ?)"; + blobUpdateSQL = "update " + schemaObjectPrefix + "BINVAL set BINVAL_DATA = ? where BINVAL_ID = ?"; + blobSelectSQL = "select BINVAL_DATA from " + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; + blobSelectExistSQL = "select 1 from " + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; + blobDeleteSQL = "delete from " + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; + } + + /** + * {@inheritDoc} + */ + public String createId(PropertyId id, int index) { + StringBuilder buf = new StringBuilder(); + buf.append(id.getParentId().toString()); + buf.append('.'); + buf.append(getNsIndex().stringToIndex(id.getName().getNamespaceURI())); + buf.append('.'); + buf.append(getNameIndex().stringToIndex(id.getName().getLocalName())); + buf.append('.'); + buf.append(index); + return buf.toString(); + } + + /** + * {@inheritDoc} + */ + public InputStream get(String blobId) throws Exception { + ResultSet rs = null; + boolean close = true; + try { + rs = conHelper.exec(blobSelectSQL, new Object[]{blobId}, false, 0); + if (!rs.next()) { + throw new Exception("no such BLOB: " + blobId); + } + + InputStream in = rs.getBinaryStream(1); + if (in == null) { + // some databases treat zero-length values as NULL; + // return empty InputStream in such a case + return new ByteArrayInputStream(new byte[0]); + } + + // return an InputStream wrapper in order to close the ResultSet when the stream is closed + close = false; + final ResultSet rs2 = rs; + return new FilterInputStream(in) { + + public void close() throws IOException { + try { + in.close(); + } finally { + // now it's safe to close ResultSet + DbUtility.close(rs2); + } + } + }; + } finally { + if (close) { + DbUtility.close(rs); + } + } + } + + /** + * {@inheritDoc} + */ + public synchronized void put(String blobId, InputStream in, long size) + throws Exception { + ResultSet rs = null; + boolean exists; + try { + rs = conHelper.exec(blobSelectExistSQL, new Object[]{blobId}, false, 0); + // a BLOB exists if the result has at least one entry + exists = rs.next(); + } finally { + DbUtility.close(rs); + } + String sql = (exists) ? blobUpdateSQL : blobInsertSQL; + Object[] params = new Object[]{new StreamWrapper(in, size), blobId}; + conHelper.exec(sql, params); + } + + /** + * {@inheritDoc} + */ + public synchronized boolean remove(String blobId) throws Exception { + return conHelper.update(blobDeleteSQL, new Object[]{blobId}) == 1; + } + + public void close() { + // closing the database resources of this blobstore is left to the + // owning BundleDbPersistenceManager + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/DbNameIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/DbNameIndex.java new file mode 100644 index 00000000000..a1a0462e938 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/DbNameIndex.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.pool; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; + +import org.apache.jackrabbit.core.util.StringIndex; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DbUtility; + +/** + * Implements a {@link StringIndex} that stores and retrieves the names from a + * table in a database. + *

    + * Note that this class is not threadsafe by itself. it needs to be synchronized + * by the using application. + *

    + * Due to a bug with oracle that treats empty strings a null values + * (see JCR-815), all empty strings are replaced by a ' '. since names never + * start with a space, this it not problematic yet. + */ +public class DbNameIndex implements StringIndex { + + protected final ConnectionHelper conHelper; + + // name index statements + protected String nameSelectSQL; + protected String indexSelectSQL; + protected String nameInsertSQL; + + // caches + private final HashMap string2Index = new HashMap(); + private final HashMap index2String = new HashMap(); + + /** + * Creates a new index that is stored in a db. + * @param conHlpr the {@link ConnectionHelper} + * @param schemaObjectPrefix the prefix for table names + * @throws SQLException if the statements cannot be prepared. + */ + public DbNameIndex(ConnectionHelper conHlpr, String schemaObjectPrefix) + throws SQLException { + conHelper = conHlpr; + init(schemaObjectPrefix); + } + + /** + * Inits this index and prepares the statements. + * + * @param schemaObjectPrefix the prefix for table names + * @throws SQLException if the statements cannot be prepared. + */ + protected void init(String schemaObjectPrefix) + throws SQLException { + nameSelectSQL = "select NAME from " + schemaObjectPrefix + "NAMES where ID = ?"; + indexSelectSQL = "select ID from " + schemaObjectPrefix + "NAMES where NAME = ?"; + nameInsertSQL = "insert into " + schemaObjectPrefix + "NAMES (NAME) values (?)"; + } + + /** + * Closes this index and releases it's resources. + */ + public void close() { + // closing the database resources is done by the owning + // BundleDbPersistenceManager that created this index + } + + /** + * {@inheritDoc} + */ + public int stringToIndex(String string) { + // check cache + Integer index = string2Index.get(string); + if (index == null) { + String dbString = string.length() == 0 ? " " : string; + int idx = getIndex(dbString); + if (idx == -1) { + idx = insertString(dbString); + } + index = Integer.valueOf(idx); + string2Index.put(string, index); + index2String.put(index, string); + return idx; + } else { + return index.intValue(); + } + } + + /** + * {@inheritDoc} + */ + public String indexToString(int idx) throws IllegalArgumentException { + // check cache + Integer index = Integer.valueOf(idx); + String s = index2String.get(index); + if (s == null) { + s = getString(idx); + if (s.equals(" ")) { + s = ""; + } + index2String.put(index, s); + string2Index.put(s, index); + } + return s; + } + + /** + * Inserts a string into the database and returns the new index. + * + * @param string the string to insert + * @return the new index. + */ + protected int insertString(String string) { + // assert index does not exist + int result = -1; + ResultSet rs = null; + try { + rs = conHelper.exec(nameInsertSQL, new Object[] { string }, true, 0); + if (rs.next()) { + result = rs.getInt(1); + } + } catch (Exception e) { + IllegalStateException ise = new IllegalStateException( + "Unable to insert index for string: " + string); + ise.initCause(e); + throw ise; + } finally { + DbUtility.close(rs); + } + if (result != -1) { + return result; + } else { + // Could not get the index with getGeneratedKeys, try with SELECT + return getIndex(string); + } + } + + /** + * Retrieves the index from the database for the given string. + * @param string the string to retrieve the index for + * @return the index or -1 if not found. + */ + protected int getIndex(String string) { + ResultSet rs = null; + try { + rs = conHelper.exec(indexSelectSQL, new Object[] { string }, false, 0); + if (rs.next()) { + return rs.getInt(1); + } else { + return -1; + } + } catch (Exception e) { + IllegalStateException ise = new IllegalStateException( + "Unable to read index for string: " + string); + ise.initCause(e); + throw ise; + } finally { + DbUtility.close(rs); + } + } + + /** + * Retrieves the string from the database for the given index. + * @param index the index to retrieve the string for. + * @return the string + * @throws IllegalArgumentException if the string is not found + */ + protected String getString(int index) + throws IllegalArgumentException, IllegalStateException { + String result = null; + ResultSet rs = null; + try { + rs = conHelper.exec(nameSelectSQL, new Object[] { Integer.valueOf(index) }, false, 0); + if (rs.next()) { + result = rs.getString(1); + } + } catch (Exception e) { + IllegalStateException ise = new IllegalStateException( + "Unable to read name for index: " + index); + ise.initCause(e); + throw ise; + } finally { + DbUtility.close(rs); + } + if (result == null) { + throw new IllegalArgumentException("Index not found: " + index); + } + return result; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/DerbyPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/DerbyPersistenceManager.java new file mode 100644 index 00000000000..8f7f8ad5335 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/DerbyPersistenceManager.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.core.persistence.pool; + +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.persistence.db.DatabasePersistenceManager; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DerbyConnectionHelper; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +/** + * Extends the {@link BundleDbPersistenceManager} by derby specific code. + *

    + * Configuration:
    + *

      + *
    • <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/> + *
    • <param name="{@link #setConsistencyCheck(String) consistencyCheck}" value="false"/> + *
    • <param name="{@link #setMinBlobSize(String) minBlobSize}" value="16384"/> + *
    • <param name="{@link #setDriver(String) driver}" value="org.apache.derby.jdbc.EmbeddedDriver"/> + *
    • <param name="{@link #setUrl(String) url}" value="jdbc:derby:${wsp.home}/db/itemState;create=true"/> + *
    • <param name="{@link #setUser(String) user}" value=""/> + *
    • <param name="{@link #setPassword(String) password}" value=""/> + *
    • <param name="{@link #setSchema(String) schema}" value="derby"/> + *
    • <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value=""/> + *
    • <param name="{@link #setErrorHandling(String) errorHandling}" value=""/> + *
    • <param name="{@link #setDerbyStorageInitialPages(String) derbyStorageInitialPages}" value="16"/> + *
    • <param name="{@link #setDerbyStorageMinimumRecordSize(String) derbyStorageMinimumRecordSize}" value="256"/> + *
    • <param name="{@link #setDerbyStoragePageCacheSize(String) derbyStoragePageCacheSize}" value="1024"/> + *
    • <param name="{@link #setDerbyStoragePageReservedSpace(String) derbyStoragePageReservedSpace}" value="20"/> + *
    • <param name="{@link #setDerbyStoragePageSize(String) derbyStoragePageSize}" value="16384"/> + *
    + */ +public class DerbyPersistenceManager extends BundleDbPersistenceManager { + + /** name of the embedded driver */ + public static final String DERBY_EMBEDDED_DRIVER = "org.apache.derby.jdbc.EmbeddedDriver"; + + /** @see #setDerbyStorageInitialPages(String) */ + private int derbyStorageInitialPages = 16; + + /** @see #setDerbyStorageMinimumRecordSize(String) */ + private int derbyStorageMinimumRecordSize = 512; + + /** @see #setDerbyStoragePageCacheSize(String) */ + private int derbyStoragePageCacheSize = 1024; + + /** @see #setDerbyStoragePageReservedSpace(String) */ + private int derbyStoragePageReservedSpace = 20; + + /** @see #setDerbyStoragePageSize(String) */ + private int derbyStoragePageSize = 16384; + + /** + * @see #setDerbyStorageInitialPages + * @return the initial pages property + */ + public String getDerbyStorageInitialPages() { + return String.valueOf(derbyStorageInitialPages); + } + + /** + * The on-disk size of a Derby table grows by one page at a time until eight + * pages of user data (or nine pages of total disk use, one is used for + * overhead) have been allocated. Then it will grow by eight pages at a time + * if possible. + *

    + * A Derby table or index can be created with a number of pages already + * pre-allocated. To do so, specify the property prior to the CREATE TABLE + * or CREATE INDEX statement. + *

    + * Define the number of user pages the table or index is to be created with. + * The purpose of this property is to preallocate a table or index of + * reasonable size if the user expects that a large amount of data will be + * inserted into the table or index. A table or index that has the + * pre-allocated pages will enjoy a small performance improvement over a + * table or index that has no pre-allocated pages when the data are loaded. + *

    + * The total desired size of the table or index should be + *

    + * (1+derby.storage.initialPages) * derby.storage.pageSize bytes. + *

    + * When you create a table or an index after setting this property, Derby + * attempts to preallocate the requested number of user pages. However, the + * operations do not fail even if they are unable to preallocate the + * requested number of pages, as long as they allocate at least one page. + *

    + * Default is 16 + * + * @param derbyStorageInitialPages the number of initial pages + */ + public void setDerbyStorageInitialPages(String derbyStorageInitialPages) { + this.derbyStorageInitialPages = + Integer.decode(derbyStorageInitialPages).intValue(); + } + + /** + * @see #setDerbyStorageMinimumRecordSize + * @return the minimum record size + */ + public String getDerbyStorageMinimumRecordSize() { + return String.valueOf(derbyStorageMinimumRecordSize); + } + + /** + * Indicates the minimum user row size in bytes for on-disk database pages + * for tables when you are creating a table. This property ensures that + * there is enough room for a row to grow on a page when updated without + * having to overflow. This is generally most useful for VARCHAR and + * VARCHAR FOR BIT DATA data types and for tables that are updated a lot, + * in which the rows start small and grow due to updates. Reserving the + * space at the time of insertion minimizes row overflow due to updates, + * but it can result in wasted space. Set the property prior to issuing the + * CREATE TABLE statement. + *

    + * Default is 256 + * + * @param derbyStorageMinimumRecordSize the minimum record size + */ + public void setDerbyStorageMinimumRecordSize(String derbyStorageMinimumRecordSize) { + this.derbyStorageMinimumRecordSize = + Integer.decode(derbyStorageMinimumRecordSize).intValue(); + } + + /** + * @see #setDerbyStoragePageCacheSize + * @return the page cache size + */ + public String getDerbyStoragePageCacheSize() { + return String.valueOf(derbyStoragePageCacheSize); + } + + /** + * Defines the size, in number of pages, of the database's data page cache + * (data pages kept in memory). The actual amount of memory the page cache + * will use depends on the following: + *

      + *
    • the size of the cache (configured with {@link #setDerbyStoragePageCacheSize}) + *
    • the size of the pages (configured with {@link #setDerbyStoragePageSize}) + *
    • overhead (varies with JVMs) + *
    + * When increasing the size of the page cache, you typically have to allow + * more memory for the Java heap when starting the embedding application + * (taking into consideration, of course, the memory needs of the embedding + * application as well). For example, using the default page size of 4K, a + * page cache size of 2000 pages will require at least 8 MB of memory (and + * probably more, given the overhead). + *

    + * The minimum value is 40 pages. If you specify a lower value, Derby uses + * the default value. + *

    + * Default is 1024 (which gives about 16mb memory usage given + * the default of 16384 as page size). + * + * @param derbyStoragePageCacheSize the page cache size + */ + public void setDerbyStoragePageCacheSize(String derbyStoragePageCacheSize) { + this.derbyStoragePageCacheSize = + Integer.decode(derbyStoragePageCacheSize).intValue(); + } + + + /** + * @see #setDerbyStoragePageReservedSpace + * @return the page reserved space + */ + public String getDerbyStoragePageReservedSpace() { + return String.valueOf(derbyStoragePageReservedSpace); + } + + /** + * Defines the percentage of space reserved for updates on an on-disk + * database page for tables only (not indexes); indicates the percentage of + * space to keep free on a page when inserting. Leaving reserved space on a + * page can minimize row overflow (and the associated performance hit) + * during updates. Once a page has been filled up to the reserved-space + * threshold, no new rows are allowed on the page. This reserved space is + * used only for rows that increase in size when updated, not for new + * inserts. Set this property prior to issuing the CREATE TABLE statement. + *

    + * Regardless of the value of derby.storage.pageReservedSpace, an empty page + * always accepts at least one row. + *

    + * Default is 20% + * + * @param derbyStoragePageReservedSpace the page reserved space + */ + public void setDerbyStoragePageReservedSpace(String derbyStoragePageReservedSpace) { + this.derbyStoragePageReservedSpace = + Integer.decode(derbyStoragePageReservedSpace).intValue(); + } + + /** + * @see #setDerbyStoragePageSize + * @return the page size + */ + public String getDerbyStoragePageSize() { + return String.valueOf(derbyStoragePageSize); + } + + /** + * Defines the page size, in bytes, for on-disk database pages for tables or + * indexes used during table or index creation. Page size can only be one + * the following values: 4096, 8192, 16384, or 32768. Set this property + * prior to issuing the CREATE TABLE or CREATE INDEX statement. This value + * will be used for the lifetime of the newly created conglomerates. + *

    + * Default is 16384 + * + * @param derbyStoragePageSize the storage page size + */ + public void setDerbyStoragePageSize(String derbyStoragePageSize) { + this.derbyStoragePageSize = Integer.decode(derbyStoragePageSize).intValue(); + } + + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + // init default values + if (getDriver() == null) { + setDriver(DERBY_EMBEDDED_DRIVER); + } + if (getDatabaseType() == null) { + setDatabaseType("derby"); + } + if (getUrl() == null) { + setUrl("jdbc:derby:" + context.getHomeDir().getPath() + "/db/itemState;create=true"); + } + if (getSchemaObjectPrefix() == null) { + setSchemaObjectPrefix(""); + } + super.init(context); + // set properties + if (DERBY_EMBEDDED_DRIVER.equals(getDriver())) { + conHelper.exec("CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY " + + "('derby.storage.initialPages', '" + derbyStorageInitialPages + "')"); + conHelper.exec("CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY " + + "('derby.storage.minimumRecordSize', '" + derbyStorageMinimumRecordSize + "')"); + conHelper.exec("CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY " + + "('derby.storage.pageCacheSize', '" + derbyStoragePageCacheSize + "')"); + conHelper.exec("CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY " + + "('derby.storage.pageReservedSpace', '" + derbyStoragePageReservedSpace + "')"); + conHelper.exec("CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY " + "('derby.storage.pageSize', '" + + derbyStoragePageSize + "')"); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) { + return new DerbyConnectionHelper(dataSrc, blockOnConnectionLoss); + } + + /** + * {@inheritDoc} + * + * Since Derby cannot handle binary indexes, we use long-long keys. + * + * @return {@link BundleDbPersistenceManager#SM_LONGLONG_KEYS} + */ + public int getStorageModel() { + return BundleDbPersistenceManager.SM_LONGLONG_KEYS; + } + + /** + * Closes the given connection by shutting down the embedded Derby + * database. + * + * @throws SQLException if an error occurs + */ + public void close() throws Exception { + super.close(); + ((DerbyConnectionHelper) conHelper).shutDown(getDriver()); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/H2PersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/H2PersistenceManager.java new file mode 100644 index 00000000000..56046f225ca --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/H2PersistenceManager.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.core.persistence.pool; + +import org.apache.jackrabbit.core.persistence.PMContext; + +/** + * Extends the {@link BundleDbPersistenceManager} by H2 specific code. + *

    + * Configuration: + *

    + * <PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
    + *     <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/>
    + *     <param name="{@link #setConsistencyCheck(String) consistencyCheck}" value="false"/>
    + *     <param name="{@link #setMinBlobSize(String) minBlobSize}" value="16384"/>
    + *     <param name="{@link #setDriver(String) driver}" value="org.h2.Driver"/>
    + *     <param name="{@link #setUrl(String) url}" value="jdbc:h2:file:${wsp.home}/db/itemState"/>
    + *     <param name="{@link #setUser(String) user}" value=""/>
    + *     <param name="{@link #setPassword(String) password}" value=""/>
    + *     <param name="{@link #setSchema(String) schema}" value="h2"/>
    + *     <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value=""/>
    + *     <param name="{@link #setErrorHandling(String) errorHandling}" value=""/>
    + *     <param name="{@link #setLockTimeout(String) lockTimeout}" value="10000"/>
    + * </PersistenceManager>
    + * 
    + */ +public class H2PersistenceManager extends BundleDbPersistenceManager { + + /** the lock time out. see*/ + private long lockTimeout = 10000; + + /** + * Returns the lock timeout. + * @return the lock timeout + */ + public String getLockTimeout() { + return String.valueOf(lockTimeout); + } + + /** + * Sets the lock timeout in milliseconds. + * @param lockTimeout the lock timeout. + */ + public void setLockTimeout(String lockTimeout) { + this.lockTimeout = Long.parseLong(lockTimeout); + } + + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + // init default values + if (getDriver() == null) { + setDriver("org.h2.Driver"); + } + if (getUrl() == null) { + setUrl("jdbc:h2:file:" + context.getHomeDir().getPath() + "/db/itemState"); + } + if (getDatabaseType() == null) { + setDatabaseType("h2"); + } + if (getSchemaObjectPrefix() == null) { + setSchemaObjectPrefix(""); + } + + super.init(context); + + conHelper.exec("SET LOCK_TIMEOUT " + lockTimeout); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/MSSqlPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/MSSqlPersistenceManager.java new file mode 100644 index 00000000000..b066e447d97 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/MSSqlPersistenceManager.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.core.persistence.pool; + +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; + +/** + * Extends the {@link BundleDbPersistenceManager} by MS-SQL specific code. + *

    + * Configuration:
    + *

      + *
    • <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/> + *
    • <param name="{@link #setConsistencyCheck(String) consistencyCheck}" value="false"/> + *
    • <param name="{@link #setMinBlobSize(String) minBlobSize}" value="16384"/> + *
    • <param name="{@link #setDriver(String) driver}" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/> + *
    • <param name="{@link #setUrl(String) url}" value=""/> + *
    • <param name="{@link #setUser(String) user}" value=""/> + *
    • <param name="{@link #setPassword(String) password}" value=""/> + *
    • <param name="{@link #setSchema(String) schema}" value="mssql"/> + *
    • <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value=""/> + *
    • <param name="{@link #setErrorHandling(String) errorHandling}" value=""/> + *
    • <param name="{@link #setTableSpace(String) tableSpace}" value=""/> + *
    + */ +public class MSSqlPersistenceManager extends BundleDbPersistenceManager { + + /** the MS SQL table space to use */ + protected String tableSpace = ""; + + public MSSqlPersistenceManager() { + setDriver("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + setDatabaseType("mssql"); + } + + /** + * {@inheritDoc} + */ + @Override + protected CheckSchemaOperation createCheckSchemaOperation() { + return super.createCheckSchemaOperation().addVariableReplacement( + CheckSchemaOperation.TABLE_SPACE_VARIABLE, tableSpace); + } + + /** + * Returns the configured MS SQL table space. + * + * @return the configured MS SQL table space. + */ + public String getTableSpace() { + return tableSpace; + } + + /** + * Sets the MS SQL table space. + * + * @param tableSpace the MS SQL table space. + */ + public void setTableSpace(String tableSpace) { + if (tableSpace != null && tableSpace.trim().length() > 0) { + this.tableSpace = "on " + tableSpace.trim(); + } else { + this.tableSpace = ""; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/MySqlPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/MySqlPersistenceManager.java new file mode 100644 index 00000000000..e4ab4158da3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/MySqlPersistenceManager.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.core.persistence.pool; + +import org.apache.jackrabbit.core.persistence.PMContext; + +/** + * Extends the {@link BundleDbPersistenceManager} by mysql specific code. + *

    + * Configuration:
    + *

      + *
    • <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/> + *
    • <param name="{@link #setConsistencyCheck(String) consistencyCheck}" value="false"/> + *
    • <param name="{@link #setMinBlobSize(String) minBlobSize}" value="16384"/> + *
    • <param name="{@link #setDriver(String) driver}" value="org.gjt.mm.mysql.Driver"/> + *
    • <param name="{@link #setUrl(String) url}" value=""/> + *
    • <param name="{@link #setUser(String) user}" value=""/> + *
    • <param name="{@link #setPassword(String) password}" value=""/> + *
    • <param name="{@link #setSchema(String) schema}" value="mysql"/> + *
    • <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value=""/> + *
    • <param name="{@link #setErrorHandling(String) errorHandling}" value=""/> + *
    + */ +public class MySqlPersistenceManager extends BundleDbPersistenceManager { + + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + // init default values + if (getDriver() == null) { + setDriver("org.gjt.mm.mysql.Driver"); + } + if (getDatabaseType() == null) { + setDatabaseType("mysql"); + } + super.init(context); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/NGKDbNameIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/NGKDbNameIndex.java new file mode 100644 index 00000000000..c022419d3a1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/NGKDbNameIndex.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.core.persistence.pool; + +import java.sql.SQLException; +import java.sql.Statement; + +import org.apache.jackrabbit.core.util.db.ConnectionHelper; + +/** + * Same as {@link DbNameIndex} but does not make use of the + * {@link Statement#RETURN_GENERATED_KEYS} feature as it might not be provided + * by the underlying database (e.g. oracle). + */ +public class NGKDbNameIndex extends DbNameIndex { + + /** + * Creates a new index that is stored in a db. + * @param conHelper the {@link ConnectionHelper} + * @param schemaObjectPrefix the prefix for table names + * @throws SQLException if the statements cannot be prepared. + */ + public NGKDbNameIndex(ConnectionHelper conHelper, String schemaObjectPrefix) + throws SQLException { + super(conHelper, schemaObjectPrefix); + } + + /** + * {@inheritDoc} + */ + protected void init(String schemaObjectPrefix) + throws SQLException { + nameSelectSQL = "select NAME from " + schemaObjectPrefix + "NAMES where ID = ?"; + indexSelectSQL = "select ID from " + schemaObjectPrefix + "NAMES where NAME = ?"; + nameInsertSQL = "insert into " + schemaObjectPrefix + "NAMES (NAME) values (?)"; + } + + /** + * Inserts a string into the database and returns the new index. + *

    + * Instead of using the {@link Statement#RETURN_GENERATED_KEYS} feature, the + * newly inserted index is retrieved by a 2nd select statement. + * + * @param string the string to insert + * @return the new index. + */ + protected int insertString(String string) { + // assert index does not exist + try { + conHelper.exec(nameInsertSQL, new Object[] { string }); + } catch (Exception e) { + IllegalStateException ise = new IllegalStateException( + "Unable to insert index for string: " + string); + ise.initCause(e); + throw ise; + } + return getIndex(string); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/Oracle9PersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/Oracle9PersistenceManager.java new file mode 100644 index 00000000000..39b1cc87ec5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/Oracle9PersistenceManager.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.core.persistence.pool; + +import javax.sql.DataSource; + +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.Oracle10R1ConnectionHelper; + +/** + * OracleLegacyPersistenceManager provides support for Oracle jdbc drivers prior to version 10 + * which require special handling of BLOB data. + *

    Configuration:
    + *

      + *
    • <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/> + *
    • <param name="{@link #setConsistencyCheck(String) consistencyCheck}" value="false"/> + *
    • <param name="{@link #setMinBlobSize(String) minBlobSize}" value="16384"/> + *
    • <param name="{@link #setDriver(String) driver}" value="oracle.jdbc.OracleDriverr"/> + *
    • <param name="{@link #setUrl(String) url}" value="jdbc:oracle:thin:@127.0.0.1:1521:xe"/> + *
    • <param name="{@link #setUser(String) user}" value=""/> + *
    • <param name="{@link #setPassword(String) password}" value=""/> + *
    • <param name="{@link #setSchema(String) schema}" value="oracle"/> + *
    • <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value="${wsp.name}_"/> + *
    • <param name="{@link #setErrorHandling(String) errorHandling}" value=""/> + *
    + */ +public class Oracle9PersistenceManager extends OraclePersistenceManager { + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + Oracle10R1ConnectionHelper helper = new Oracle10R1ConnectionHelper(dataSrc, blockOnConnectionLoss); + helper.init(); + return helper; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/OraclePersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/OraclePersistenceManager.java new file mode 100644 index 00000000000..e242511ae85 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/OraclePersistenceManager.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.core.persistence.pool; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.OracleConnectionHelper; + +/** + * Extends the {@link BundleDbPersistenceManager} by Oracle specific code. + *

    + * Configuration:
    + *

      + *
    • <param name="{@link #setExternalBLOBs(boolean)} externalBLOBs}" value="false"/> + *
    • <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/> + *
    • <param name="{@link #setConsistencyCheck(String) consistencyCheck}" value="false"/> + *
    • <param name="{@link #setMinBlobSize(String) minBlobSize}" value="16384"/> + *
    • <param name="{@link #setDriver(String) driver}" value="oracle.jdbc.OracleDriverr"/> + *
    • <param name="{@link #setUrl(String) url}" value="jdbc:oracle:thin:@127.0.0.1:1521:xe"/> + *
    • <param name="{@link #setUser(String) user}" value=""/> + *
    • <param name="{@link #setPassword(String) password}" value=""/> + *
    • <param name="{@link #setSchema(String) schema}" value="oracle"/> + *
    • <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value="${wsp.name}_"/> + *
    • <param name="{@link #setTablespace(String) tableSpace}" value="user"/> + *
    • <param name="{@link #setIndexTablespace(String) tableSpace}" value="user"/> + *
    • <param name="{@link #setErrorHandling(String) errorHandling}" value=""/> + *
    + */ +public class OraclePersistenceManager extends BundleDbPersistenceManager { + /** + * The default tablespace clause used when {@link #tablespace} or {@link #indexTablespace} + * are not specified. + */ + protected static final String DEFAULT_TABLESPACE_CLAUSE = ""; + + /** + * Name of the replacement variable in the DDL for {@link #tablespace}. + */ + protected static final String TABLESPACE_VARIABLE = "${tablespace}"; + + /** + * Name of the replacement variable in the DDL for {@link #indexTablespace}. + */ + protected static final String INDEX_TABLESPACE_VARIABLE = "${indexTablespace}"; + + /** The Oracle tablespace to use for tables */ + protected String tablespace; + + /** The Oracle tablespace to use for indexes */ + protected String indexTablespace; + + /** + * Creates a new oracle persistence manager + */ + public OraclePersistenceManager() { + tablespace = DEFAULT_TABLESPACE_CLAUSE; + indexTablespace = DEFAULT_TABLESPACE_CLAUSE; + // enable db blob support + setExternalBLOBs(false); + } + + /** + * Returns the configured Oracle tablespace for tables. + * @return the configured Oracle tablespace for tables. + */ + public String getTablespace() { + return tablespace; + } + + /** + * Sets the Oracle tablespace for tables. + * @param tablespaceName the Oracle tablespace for tables. + */ + public void setTablespace(String tablespaceName) { + this.tablespace = this.buildTablespaceClause(tablespaceName); + } + + /** + * Returns the configured Oracle tablespace for indexes. + * @return the configured Oracle tablespace for indexes. + */ + public String getIndexTablespace() { + return indexTablespace; + } + + /** + * Sets the Oracle tablespace for indexes. + * @param tablespaceName the Oracle tablespace for indexes. + */ + public void setIndexTablespace(String tablespaceName) { + this.indexTablespace = this.buildTablespaceClause(tablespaceName); + } + + /** + * Constructs the tablespace <tbs name> clause from + * the supplied tablespace name. If the name is empty, {@link #DEFAULT_TABLESPACE_CLAUSE} + * is returned instead. + * + * @param tablespaceName A tablespace name + * @return A tablespace clause using the supplied name or + * {@value #DEFAULT_TABLESPACE_CLAUSE} if the name is empty + */ + private String buildTablespaceClause(String tablespaceName) { + if (tablespaceName == null || tablespaceName.trim().length() == 0) { + return DEFAULT_TABLESPACE_CLAUSE; + } else { + return "tablespace " + tablespaceName.trim(); + } + } + + public void init(PMContext context) throws Exception { + // init default values + if (getDriver() == null) { + setDriver("oracle.jdbc.OracleDriver"); + } + if (getUrl() == null) { + setUrl("jdbc:oracle:thin:@127.0.0.1:1521:xe"); + } + if (getDatabaseType() == null) { + setDatabaseType("oracle"); + } + if (getSchemaObjectPrefix() == null) { + setSchemaObjectPrefix(context.getHomeDir().getName() + "_"); + } + super.init(context); + } + + /** + * Returns a new instance of a NGKDbNameIndex. + * + * @return a new instance of a NGKDbNameIndex. + * @throws SQLException if an SQL error occurs. + */ + protected DbNameIndex createDbNameIndex() throws SQLException { + return new NGKDbNameIndex(conHelper, schemaObjectPrefix); + } + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + OracleConnectionHelper helper = new OracleConnectionHelper(dataSrc, blockOnConnectionLoss); + helper.init(); + return helper; + } + + /** + * {@inheritDoc} + */ + @Override + protected CheckSchemaOperation createCheckSchemaOperation() { + if (DEFAULT_TABLESPACE_CLAUSE.equals(indexTablespace) && !DEFAULT_TABLESPACE_CLAUSE.equals(tablespace)) { + // tablespace was set but not indexTablespace : use the same for both + indexTablespace = tablespace; + } + return super.createCheckSchemaOperation() + .addVariableReplacement(TABLESPACE_VARIABLE, tablespace) + .addVariableReplacement(INDEX_TABLESPACE_VARIABLE, indexTablespace); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/PostgreSQLNameIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/PostgreSQLNameIndex.java new file mode 100644 index 00000000000..0d132837ad1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/PostgreSQLNameIndex.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.core.persistence.pool; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DbUtility; + +/** + * Same as {@link DbNameIndex} but does not make use of the + * {@link java.sql.Statement#RETURN_GENERATED_KEYS} feature as it is not + * provided by the underlying database driver for PostgreSQL. + */ +public class PostgreSQLNameIndex extends DbNameIndex { + + protected String generatedKeySelectSQL; + + public PostgreSQLNameIndex(ConnectionHelper connectionHelper, String schemaObjectPrefix) + throws SQLException { + super(connectionHelper, schemaObjectPrefix); + } + + /** + * Inits this index and prepares the statements. + * + * @param schemaObjectPrefix the prefix for table names + * @throws SQLException if the statements cannot be prepared. + */ + protected void init(String schemaObjectPrefix) + throws SQLException { + nameSelectSQL = "select NAME from " + schemaObjectPrefix + "NAMES where ID = ?"; + indexSelectSQL = "select ID from " + schemaObjectPrefix + "NAMES where NAME = ?"; + nameInsertSQL = "insert into " + schemaObjectPrefix + "NAMES (NAME) values (?)"; + generatedKeySelectSQL = "select currval('" + schemaObjectPrefix + "NAMES_ID_SEQ')"; + } + + /** + * Inserts a string into the database and returns the new index. + *

    + * Instead of using the {@link java.sql.Statement#RETURN_GENERATED_KEYS} + * feature, the newly inserted index is retrieved by a 2nd select statement. + * + * @param string the string to insert + * @return the new index. + */ + protected int insertString(String string) { + // assert index does not exist + try { + conHelper.exec(nameInsertSQL, new Object[]{string}); + return getGeneratedKey(); + } catch (Exception e) { + IllegalStateException ise = new IllegalStateException("Unable to insert index for string: " + string); + ise.initCause(e); + throw ise; + } + } + + /** + * Retrieves the last assigned key from the database. + * @return the index. + */ + protected int getGeneratedKey() { + ResultSet rs = null; + try { + rs = conHelper.exec(generatedKeySelectSQL, null, false, 0); + if (!rs.next()) { + return -1; + } else { + return rs.getInt(1); + } + } catch (Exception e) { + IllegalStateException ise = new IllegalStateException("Unable to read generated index"); + ise.initCause(e); + throw ise; + } finally { + DbUtility.close(rs); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/PostgreSQLPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/PostgreSQLPersistenceManager.java new file mode 100644 index 00000000000..45255c4c69c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/pool/PostgreSQLPersistenceManager.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.core.persistence.pool; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.PostgreSQLConnectionHelper; + +/** + * Extends the {@link BundleDbPersistenceManager} by PostgreSQL specific code. + *

    + * Configuration:
    + *

      + *
    • <param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/> + *
    • <param name="{@link #setConsistencyCheck(String) consistencyCheck}" value="false"/> + *
    • <param name="{@link #setMinBlobSize(String) minBlobSize}" value="16384"/> + *
    • <param name="{@link #setDriver(String) driver}" value="org.postgresql.Driver"/> + *
    • <param name="{@link #setUrl(String) url}" value=""/> + *
    • <param name="{@link #setUser(String) user}" value=""/> + *
    • <param name="{@link #setPassword(String) password}" value=""/> + *
    • <param name="{@link #setSchema(String) schema}" value="postgresql"/> + *
    • <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value=""/> + *
    • <param name="{@link #setErrorHandling(String) errorHandling}" value=""/> + *
    + */ +public class PostgreSQLPersistenceManager extends BundleDbPersistenceManager { + + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + // init default values + if (getDriver() == null) { + setDriver("org.postgresql.Driver"); + } + if (getDatabaseType() == null) { + setDatabaseType("postgresql"); + } + super.init(context); + } + + /** + * Returns a new instance of a DbNameIndex. + * @return a new instance of a DbNameIndex. + * @throws java.sql.SQLException if an SQL error occurs. + */ + protected DbNameIndex createDbNameIndex() throws SQLException { + return new PostgreSQLNameIndex(conHelper, schemaObjectPrefix); + } + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + return new PostgreSQLConnectionHelper(dataSrc, blockOnConnectionLoss); + } + + /** + * returns the storage model + * @return the storage model + */ + public int getStorageModel() { + return SM_LONGLONG_KEYS; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BLOBStore.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BLOBStore.java new file mode 100644 index 00000000000..2d64639d64a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BLOBStore.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.core.persistence.util; + +import org.apache.jackrabbit.core.id.PropertyId; + +import java.io.InputStream; + +/** + * BLOBStore represents an abstract store for binary property + * values (BLOBs). + *

    + * Note that The DataStore should nowadays be used instead of the BLOBStore. + * This interface and the implementing classes are kept mostly for backwards + * compatibility. + * + * @see ResourceBasedBLOBStore + */ +public interface BLOBStore { + /** + * Creates a unique identifier for the BLOB data associated with the given + * property id and value subscript. + * + * @param id id of the property associated with the BLOB data + * @param index subscript of the value holding the BLOB data + * @return a string identifying the BLOB data + */ + String createId(PropertyId id, int index); + + /** + * Stores the BLOB data and returns a unique identifier. + * + * @param blobId identifier of the BLOB data as returned by + * {@link #createId(PropertyId, int)} + * @param in stream containing the BLOB data + * @param size size of the BLOB data + * @throws Exception if an error occured + */ + void put(String blobId, InputStream in, long size) throws Exception; + + /** + * Retrieves the BLOB data with the specified id as a binary stream. + * + * @param blobId identifier of the BLOB data as returned by + * {@link #createId(PropertyId, int)} + * @return an input stream that delivers the BLOB data + * @throws Exception if an error occured + */ + InputStream get(String blobId) throws Exception; + + /** + * Removes the BLOB data with the specified id. + * + * @param blobId identifier of the BLOB data as returned by + * {@link #createId(PropertyId, int)} + * @return true if BLOB data with the given id exists and has + * been successfully removed, false if there's no BLOB + * data with the given id. + * @throws Exception if an error occured + */ + boolean remove(String blobId) throws Exception; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleBinding.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleBinding.java new file mode 100644 index 00000000000..ad86eb0099e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleBinding.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.core.persistence.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.util.StringIndex; + +/** + * This Class implements efficient serialization methods for item states. + */ +public class BundleBinding { + + static final int BINARY_IN_BLOB_STORE = -1; + + static final int BINARY_IN_DATA_STORE = -2; + + /** + * A special UUID used to mark the null parent identifier + * of the root node. This is a proper type 1 UUID to prevent collisions + * with other identifiers, even special non-random ones like + * 00000000-0000-0000-0000-000000000000. + */ + static final NodeId NULL_PARENT_ID = + new NodeId("bb4e9d10-d857-11df-937b-0800200c9a66"); + + /** + * serialization version 1 + */ + static final int VERSION_1 = 1; + + /** + * serialization version 2 + */ + static final int VERSION_2 = 2; + + /** + * serialization version 3 + */ + static final int VERSION_3 = 3; + + /** + * current version + */ + static final int VERSION_CURRENT = VERSION_3; + + /** + * the namespace index + */ + protected final StringIndex nsIndex; + + /** + * the name index + */ + protected final StringIndex nameIndex; + + /** + * the blob store + */ + protected final BLOBStore blobStore; + + /** + * minimum size of binaries to store in blob store + */ + protected long minBlobSize = 0x4000; // 16k + + /** + * the error handling + */ + protected final ErrorHandling errorHandling; + + /** + * Data store for binary properties. + */ + protected final DataStore dataStore; + + /** + * Creates a new bundle binding + * + * @param errorHandling the error handling + * @param blobStore the blobstore for retrieving blobs + * @param nsIndex the namespace index + * @param nameIndex the name index + * @param dataStore the data store + */ + public BundleBinding( + ErrorHandling errorHandling, BLOBStore blobStore, + StringIndex nsIndex, StringIndex nameIndex, DataStore dataStore) { + this.errorHandling = errorHandling; + this.nsIndex = nsIndex; + this.nameIndex = nameIndex; + this.blobStore = blobStore; + this.dataStore = dataStore; + } + + /** + * Returns the minimum blob size + * @see #setMinBlobSize(long) for details. + * @return the minimum blob size + */ + public long getMinBlobSize() { + return minBlobSize; + } + + /** + * Sets the minimum blob size. Binary values that are shorted than this given + * size will be inlined in the serialization stream, binary value that are + * longer, will be stored in the blob store. default is 4k. + * + * @param minBlobSize the minimum blob size. + */ + public void setMinBlobSize(long minBlobSize) { + this.minBlobSize = minBlobSize; + } + + /** + * Returns the blob store that is associated with this binding. + * @return the blob store + */ + public BLOBStore getBlobStore() { + return blobStore; + } + + /** + * Deserializes a NodePropBundle from a data input stream. + * + * @param in the input stream + * @param id the node id for the new bundle + * @return the bundle + * @throws IOException if an I/O error occurs. + */ + public NodePropBundle readBundle(InputStream in, NodeId id) + throws IOException { + return new BundleReader(this, in).readBundle(id); + } + + /** + * Serializes a NodePropBundle to a data output stream + * + * @param out the output stream + * @param bundle the bundle to serialize + * @throws IOException if an I/O error occurs. + */ + public void writeBundle(OutputStream out, NodePropBundle bundle) + throws IOException { + new BundleWriter(this, out).writeBundle(bundle); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleDumper.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleDumper.java new file mode 100644 index 00000000000..975f90e583c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleDumper.java @@ -0,0 +1,631 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.util; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.UUID; + +/** + * This utility class can dump the contents of a node bundle. This class is + * based on BundleReader, but is able to dump even if the data is corrupt + * (unlike the BundleReader). The class does not have any dependencies so it can + * be run from the command line without problems (without having to add any jar + * files to the classpath). + */ +public class BundleDumper { + + private static final int VERSION_1 = 1; + private static final int VERSION_2 = 2; + private static final int VERSION_3 = 3; + + private static final int BINARY_IN_BLOB_STORE = -1; + private static final int BINARY_IN_DATA_STORE = -2; + private static final char[] HEX = "0123456789abcdef".toCharArray(); + + public static final int UNDEFINED = 0, STRING = 1, BINARY = 2, LONG = 3, DOUBLE = 4, DATE = 5, BOOLEAN = 6, + NAME = 7, PATH = 8, REFERENCE = 9, WEAKREFERENCE = 10, URI = 11, DECIMAL = 12; + + private static final String[] NAMES = { "undefined", "String", "Binary", "Long", "Double", "Date", "Boolean", + "Name", "Path", "Reference", "WeakReference", "URI", "Decimal" }; + + private StringBuilder buffer = new StringBuilder(); + + /** + * Pre-calculated {@link TimeZone} objects for common timezone offsets. + */ + private static final TimeZone[] COMMON_TIMEZONES = { + TimeZone.getTimeZone("GMT+00:00"), // 0b00000 + TimeZone.getTimeZone("GMT+01:00"), // 0b00001 + TimeZone.getTimeZone("GMT+02:00"), // 0b00010 + TimeZone.getTimeZone("GMT+03:00"), // 0b00011 + TimeZone.getTimeZone("GMT+04:00"), // 0b00100 + TimeZone.getTimeZone("GMT+05:00"), // 0b00101 + TimeZone.getTimeZone("GMT+06:00"), // 0b00110 + TimeZone.getTimeZone("GMT+07:00"), // 0b00111 + TimeZone.getTimeZone("GMT+08:00"), // 0b01000 + TimeZone.getTimeZone("GMT+09:00"), // 0b01001 + TimeZone.getTimeZone("GMT+10:00"), // 0b01010 + TimeZone.getTimeZone("GMT+11:00"), // 0b01011 + TimeZone.getTimeZone("GMT+12:00"), // 0b01100 + TimeZone.getTimeZone("GMT+13:00"), // 0b01101 + TimeZone.getTimeZone("GMT+14:00"), // 0b01110 + TimeZone.getTimeZone("GMT+15:00"), // 0b01111 + TimeZone.getTimeZone("GMT-16:00"), // 0b10000 + TimeZone.getTimeZone("GMT-15:00"), // 0b10001 + TimeZone.getTimeZone("GMT-14:00"), // 0b10010 + TimeZone.getTimeZone("GMT-13:00"), // 0b10011 + TimeZone.getTimeZone("GMT-12:00"), // 0b10100 + TimeZone.getTimeZone("GMT-11:00"), // 0b10101 + TimeZone.getTimeZone("GMT-10:00"), // 0b10110 + TimeZone.getTimeZone("GMT-09:00"), // 0b10111 + TimeZone.getTimeZone("GMT-08:00"), // 0b11000 + TimeZone.getTimeZone("GMT-07:00"), // 0b11001 + TimeZone.getTimeZone("GMT-06:00"), // 0b11010 + TimeZone.getTimeZone("GMT-05:00"), // 0b11011 + TimeZone.getTimeZone("GMT-04:00"), // 0b11100 + TimeZone.getTimeZone("GMT-03:00"), // 0b11101 + TimeZone.getTimeZone("GMT-02:00"), // 0b11110 + TimeZone.getTimeZone("GMT-01:00"), // 0b11111 + }; + + /** + * Wrapper for reading structured data from the input stream. + */ + private DataInputStream in; + + private int version; + + private final String[] namespaces = + // NOTE: The length of this array must be seven + { "", null, null, null, null, null, null }; + + + static final UUID NULL_PARENT_ID = + UUID.fromString("bb4e9d10-d857-11df-937b-0800200c9a66"); + + + public static void main(String... args) throws IOException { + new BundleDumper().run(args); + } + + void run(String... args) throws IOException { + if (args.length < 1) { + System.out.println("Usage: java " + getClass().getName() + " "); + System.out.println("where the file name points to a node bundle."); + return; + } + RandomAccessFile f = new RandomAccessFile(args[0], "r"); + byte[] bundle = new byte[(int) f.length()]; + f.readFully(bundle); + f.close(); + System.out.println(dump(bundle)); + } + + public String dump(byte[] bundle) throws IOException { + try { + ByteArrayInputStream bin = new ByteArrayInputStream(bundle); + this.in = new DataInputStream(bin); + version = in.readUnsignedByte(); + buffer.append("version: ").append(version).append("\n"); + if (version >= VERSION_3) { + readBundleNew(); + } else { + readBundleOld(); + } + } catch (Exception e) { + buffer.append("\n"); + buffer.append("error: ").append(e.toString()); + } + return buffer.toString(); + } + + private void readBundleNew() throws IOException { + // node type + buffer.append("nodeTypeName: ").append(readName()).append("\n"); + + // parentUUID + UUID parentId = readNodeId(); + buffer.append("parentId: ").append(parentId).append("\n"); + if (NULL_PARENT_ID.equals(parentId)) { + parentId = null; + buffer.append("parentId is null\n"); + } + + // read modcount + buffer.append("modCount: ").append((short) readVarInt()).append("\n"); + + int b = in.readUnsignedByte(); + buffer.append("referenceable: ").append((b & 1) != 0).append("\n"); + + // mixin types + int mn = readVarInt((b >> 7) & 1, 1); + if (mn == 1) { + buffer.append("mixing type:").append(readName()).append("\n"); + } else if (mn > 1) { + buffer.append("mixing type count:").append(mn).append("\n"); + for (int i = 0; i < mn; i++) { + buffer.append("mixing type:").append(readName()).append("\n"); + } + } + + // properties + int pn = readVarInt((b >> 4) & 7, 7); + for (int i = 0; i < pn; i++) { + buffer.append("property: ").append(readName()).append("\n"); + readPropertyEntry(); + } + + // child nodes (list of name/uuid pairs) + int nn = readVarInt((b >> 2) & 3, 3); + for (int i = 0; i < nn; i++) { + buffer.append("child node: ").append(readQName()). + append(" id: ").append(readNodeId()).append("\n"); + } + + // read shared set + int sn = readVarInt((b >> 1) & 1, 1); + if (sn == 1) { + buffer.append("shared set:").append(readNodeId()).append("\n"); + } else if (sn > 1) { + buffer.append("shared set count:").append(sn).append("\n"); + for (int i = 0; i < sn; i++) { + buffer.append("shared set:").append(readNodeId()).append("\n"); + } + } + } + + private void readBundleOld() throws IOException { + // read primary type...special handling + int a = in.readUnsignedByte(); + int b = in.readUnsignedByte(); + int c = in.readUnsignedByte(); + String uri = "#" + (a << 16 | b << 8 | c); + String local = "#" + in.readInt(); + buffer.append("nodeTypeName: ").append(uri).append(":").append(local).append("\n"); + + // parentUUID + buffer.append("parentUUID: ").append(readNodeId()).append("\n"); + + // definitionId + buffer.append("definitionId: ").append(in.readUTF()).append("\n"); + + // mixin types + String name = readIndexedQName(); + if (name != null) { + do { + buffer.append("mixin: ").append(name).append("\n"); + name = readIndexedQName(); + } while (name != null); + } else { + buffer.append("mixins: -\n"); + } + + // properties + name = readIndexedQName(); + while (name != null) { + buffer.append("property: ").append(name).append("\n"); + readPropertyEntry(); + name = readIndexedQName(); + } + + // set referenceable flag + buffer.append("referenceable: ").append(in.readBoolean()).append("\n"); + + // child nodes (list of uuid/name pairs) + UUID childId = readNodeId(); + while (childId != null) { + buffer.append("childId: ").append(childId).append(" ").append(readQName()).append("\n"); + childId = readNodeId(); + } + + // read modcount, since version 1.0 + if (version >= VERSION_1) { + buffer.append("modCount: ").append(in.readShort()).append("\n"); + } + + // read shared set, since version 2.0 + if (version >= VERSION_2) { + // shared set (list of parent uuids) + UUID parentId = readNodeId(); + if (parentId != null) { + do { + buffer.append("shared set parentId: ").append(parentId).append("\n"); + parentId = readNodeId(); + } while (parentId != null); + } + } + } + + private static String getType(int type) { + try { + return NAMES[type]; + } catch (Exception e) { + return "unknown type " + type; + } + } + + /** + * Deserializes a PropertyState from the data input stream. + * + * @param id the property id for the new property entry + * @return the property entry + * @throws IOException if an I/O error occurs. + */ + private void readPropertyEntry() + throws IOException { + int count = 1; + int type; + if (version >= VERSION_3) { + int b = in.readUnsignedByte(); + type = b & 0x0f; + buffer.append(" type: ").append(getType(type)).append("\n"); + int len = b >>> 4; + if (len != 0) { + buffer.append(" multivalued\n"); + if (len == 0x0f) { + count = readVarInt() + 0x0f - 1; + } else { + count = len - 1; + } + } + buffer.append(" modcount: ").append((short) readVarInt()).append("\n"); + } else { + // type and modcount + type = in.readInt(); + buffer.append(" modcount: ").append((short) ((type >> 16) & 0x0ffff)).append("\n"); + type &= 0x0ffff; + buffer.append(" type: ").append(getType(type)).append("\n"); + + // multiValued + boolean mv = in.readBoolean(); + if (mv) { + buffer.append(" multivalued\n"); + } + + // definitionId + buffer.append(" definitionId: ").append(in.readUTF()).append("\n"); + + // count + count = in.readInt(); + if (count != 1) { + buffer.append(" count: ").append(count).append("\n"); + } + } + + // values + for (int i = 0; i < count; i++) { + switch (type) { + case BINARY: + int size = in.readInt(); + if (size == BINARY_IN_DATA_STORE) { + buffer.append(" value: binary in datastore: ").append(readString()).append("\n"); + } else if (size == BINARY_IN_BLOB_STORE) { + buffer.append(" value: binary in blobstore: ").append(readString()).append("\n"); + } else { + // short values into memory + byte[] data = new byte[size]; + in.readFully(data); + buffer.append(" value: binary: ").append(convertBytesToHex(data)).append("\n"); + } + break; + case DOUBLE: + buffer.append(" value: double: ").append(in.readDouble()).append("\n"); + break; + case DECIMAL: + buffer.append(" value: double: ").append(readDecimal()).append("\n"); + break; + case LONG: + if (version >= VERSION_3) { + buffer.append(" value: varLong: ").append(readVarLong()).append("\n"); + } else { + buffer.append(" value: long: ").append(in.readLong()).append("\n"); + } + break; + case BOOLEAN: + buffer.append(" value: boolean: ").append(in.readBoolean()).append("\n"); + break; + case NAME: + buffer.append(" value: name: ").append(readQName()).append("\n"); + break; + case WEAKREFERENCE: + buffer.append(" value: weakreference: ").append(readNodeId()).append("\n"); + break; + case REFERENCE: + buffer.append(" value: reference: ").append(readNodeId()).append("\n"); + break; + case DATE: + if (version >= VERSION_3) { + buffer.append(" value: date: ").append(readDate()).append("\n"); + break; + } // else fall through + default: + if (version >= VERSION_3) { + buffer.append(" value: string: ").append(readString()).append("\n"); + } else { + // because writeUTF(String) has a size limit of 64k, + // Strings are serialized as + int len = in.readInt(); + byte[] bytes = new byte[len]; + in.readFully(bytes); + buffer.append(" value: string: ").append(new String(bytes, StandardCharsets.UTF_8)).append("\n"); + } + } + } + } + + /** + * Deserializes a node identifier + * + * @return the node id + * @throws IOException in an I/O error occurs. + */ + private UUID readNodeId() throws IOException { + if (version >= VERSION_3 || in.readBoolean()) { + long msb = in.readLong(); + long lsb = in.readLong(); + return new UUID(msb, lsb); + } else { + return null; + } + } + + /** + * Deserializes a BigDecimal + * + * @return the decimal + * @throws IOException in an I/O error occurs. + */ + private BigDecimal readDecimal() throws IOException { + if (in.readBoolean()) { + // TODO more efficient serialization format + return new BigDecimal(readString()); + } else { + return null; + } + } + + /** + * Deserializes a Name + * + * @return the qname + * @throws IOException in an I/O error occurs. + */ + private String readQName() throws IOException { + if (version >= VERSION_3) { + return readName(); + } + + String uri = "#" + in.readInt(); + String local = in.readUTF(); + return uri + ":" + local; + } + + /** + * Deserializes an indexed Name + * + * @return the qname + * @throws IOException in an I/O error occurs. + */ + private String readIndexedQName() throws IOException { + if (version >= VERSION_3) { + return readName(); + } + + int index = in.readInt(); + if (index < 0) { + return null; + } else { + String uri = "#" + index; + String local = "#" + in.readInt(); + return uri + ":" + local; + } + } + + /** + * Deserializes a name written using bundle serialization version 3. + * + * @return deserialized name + * @throws IOException if an I/O error occurs + */ + private String readName() throws IOException { + int b = in.readUnsignedByte(); + if ((b & 0x80) == 0) { + return "indexToName #" + b; + } else { + String uri; + int ns = (b >> 4) & 0x07; + if (ns < namespaces.length && namespaces[ns] != null) { + uri = namespaces[ns]; + } else { + uri = readString(); + if (ns < namespaces.length) { + namespaces[ns] = uri; + } + } + String local = new String(readBytes((b & 0x0f) + 1, 0x10), StandardCharsets.UTF_8); + return uri + ":" + local; + } + } + + /** + * Deserializes a variable-length integer written using bundle + * serialization version 3. + * + * @return deserialized integer + * @throws IOException if an I/O error occurs + */ + private int readVarInt() throws IOException { + int b = in.readUnsignedByte(); + if ((b & 0x80) == 0) { + return b; + } else { + return readVarInt() << 7 | b & 0x7f; + } + } + + private int readVarInt(int value, int base) throws IOException { + if (value < base) { + return value; + } else { + return readVarInt() + base; + } + } + + /** + * Deserializes a variable-length long written using bundle + * serialization version 3. + * + * @return deserialized long + * @throws IOException if an I/O error occurs + */ + private long readVarLong() throws IOException { + long value = 0; + int bits = 0; + long b; + do { + b = in.readUnsignedByte(); + if (bits < 57) { + value = (b & 0x7f) << 57 | value >>> 7; + bits += 7; + } else { + value = (b & 0x01) << 63 | value >>> 1; + bits = 64; + } + } while ((b & 0x80) != 0); + value = value >>> (64 - bits); + if ((value & 1) != 0) { + return ~(value >>> 1); + } else { + return value >>> 1; + } + } + + /** + * Deserializes a specially encoded date written using bundle + * serialization version 3. + * + * @return deserialized date + * @throws IOException if an I/O error occurs + */ + private Calendar readDate() throws IOException { + long ts = readVarLong(); + + TimeZone tz; + if ((ts & 1) == 0) { + tz = COMMON_TIMEZONES[0]; + ts >>= 1; + } else if ((ts & 2) == 0) { + tz = COMMON_TIMEZONES[((int) ts >> 2) & 0x1f]; // 5 bits; + ts >>= 7; + } else { + int m = ((int) ts << 19) >> 21; // 11 bits, sign-extended + int h = m / 60; + String s; + if (m < 0) { + s = String.format("GMT-%02d:%02d", -h, h * 60 - m); + } else { + s = String.format("GMT+%02d:%02d", h, m - h * 60); + } + tz = TimeZone.getTimeZone(s); + ts >>= 13; + } + + int u = 0; + int s = 0; + int m = 0; + int h = 0; + int type = (int) ts & 3; + ts >>= 2; + switch (type) { + case 3: + u = (int) ts & 0x3fffffff; // 30 bits + s = u / 1000; + m = s / 60; + h = m / 60; + m -= h * 60; + s -= (h * 60 + m) * 60; + u -= ((h * 60 + m) * 60 + s) * 1000; + ts >>= 30; + break; + case 2: + m = (int) ts & 0x07ff; // 11 bits + h = m / 60; + m -= h * 60; + ts >>= 11; + break; + case 1: + h = (int) ts & 0x1f; // 5 bits + ts >>= 5; + break; + } + + int d = (int) ts & 0x01ff; // 9 bits; + ts >>= 9; + int y = (int) (ts + 2010); + + Calendar value = Calendar.getInstance(tz); + if (y <= 0) { + value.set(Calendar.YEAR, 1 - y); + value.set(Calendar.ERA, GregorianCalendar.BC); + } else { + value.set(Calendar.YEAR, y); + value.set(Calendar.ERA, GregorianCalendar.AD); + } + value.set(Calendar.DAY_OF_YEAR, d); + value.set(Calendar.HOUR_OF_DAY, h); + value.set(Calendar.MINUTE, m); + value.set(Calendar.SECOND, s); + value.set(Calendar.MILLISECOND, u); + + return value; + } + + private String readString() throws IOException { + if (version >= VERSION_3) { + return new String(readBytes(0, 0), StandardCharsets.UTF_8); + } else { + return in.readUTF(); + } + } + + private byte[] readBytes(int len, int base) throws IOException { + byte[] bytes = new byte[readVarInt(len, base)]; + in.readFully(bytes); + return bytes; + } + + public static String convertBytesToHex(byte[] value) { + int len = value.length; + char[] buff = new char[len + len]; + char[] hex = HEX; + for (int i = 0; i < len; i++) { + int c = value[i] & 0xff; + buff[i + i] = hex[c >> 4]; + buff[i + i + 1] = hex[c & 0xf]; + } + return new String(buff); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleNames.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleNames.java new file mode 100644 index 00000000000..b9f745938b1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleNames.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.core.persistence.util; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.IOExceptionWithCause; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING // +// // +// The contents and behaviour of this class are tightly coupled with the // +// bundle serialization format, so make sure that you know what you're // +// doing before modifying this class! // +// // +// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING // + +/** + * Static collection of common JCR names. This class is used by the + * {@link BundleWriter} and {@link BundleReader} classes to optimize the + * serialization of names used in bundles. + */ +class BundleNames { + + /** + * Static list of standard names. + */ + private static final Name[] NAME_ARRAY = { + // WARNING: Only edit if you really know what you're doing! + + // Most frequently used names + NameConstants.NT_UNSTRUCTURED, + NameConstants.NT_RESOURCE, + NameConstants.NT_FILE, + NameConstants.NT_FOLDER, + NameConstants.NT_HIERARCHYNODE, + NameConstants.MIX_REFERENCEABLE, + NameConstants.JCR_CREATED, + NameConstants.JCR_CREATEDBY, + NameConstants.JCR_LASTMODIFIED, + NameConstants.JCR_LASTMODIFIEDBY, + NameConstants.JCR_CONTENT, + NameConstants.JCR_MIMETYPE, + NameConstants.JCR_DATA, + NameConstants.JCR_TITLE, + NameConstants.JCR_LANGUAGE, + NameConstants.JCR_ENCODING, + NameConstants.JCR_SYSTEM, + NameConstants.REP_ROOT, + NameConstants.REP_SYSTEM, + + // Access control + NameConstants.JCR_ADD_CHILD_NODES, + NameConstants.JCR_LIFECYCLE_MANAGEMENT, + NameConstants.JCR_LOCK_MANAGEMENT, + NameConstants.JCR_MODIFY_ACCESS_CONTROL, + NameConstants.JCR_MODIFY_PROPERTIES, + NameConstants.JCR_NODE_TYPE_MANAGEMENT, + NameConstants.JCR_READ, + NameConstants.JCR_READ_ACCESS_CONTROL, + NameConstants.JCR_REMOVE_CHILD_NODES, + NameConstants.JCR_REMOVE_NODE, + NameConstants.JCR_VERSION_MANAGEMENT, + NameConstants.REP_ACCESSCONTROL, + NameConstants.REP_ACCESS_CONTROL, + NameConstants.REP_ACCESS_CONTROLLABLE, + NameConstants.REP_ACE, + NameConstants.REP_ACL, + NameConstants.REP_DENY_ACE, + NameConstants.REP_GLOB, + NameConstants.REP_GRANT_ACE, + NameConstants.REP_POLICY, + NameConstants.REP_PRINCIPAL_ACCESS_CONTROL, + NameConstants.REP_PRINCIPAL_NAME, + NameConstants.REP_PRIVILEGES, + + // Locking + NameConstants.MIX_LOCKABLE, + NameConstants.JCR_LOCKISDEEP, + NameConstants.JCR_LOCKOWNER, + + // Versioning + NameConstants.MIX_VERSIONABLE, + NameConstants.NT_FROZENNODE, + NameConstants.NT_VERSION, + NameConstants.NT_VERSIONEDCHILD, + NameConstants.NT_VERSIONHISTORY, + NameConstants.NT_VERSIONLABELS, + NameConstants.JCR_VERSIONSTORAGE, + NameConstants.JCR_FROZENPRIMARYTYPE, + NameConstants.JCR_FROZENUUID, + NameConstants.JCR_FROZENNODE, + NameConstants.JCR_PREDECESSORS, + NameConstants.JCR_SUCCESSORS, + NameConstants.JCR_VERSIONLABELS, + NameConstants.JCR_VERSIONHISTORY, + NameConstants.JCR_VERSIONABLEUUID, + NameConstants.JCR_ROOTVERSION, + NameConstants.JCR_ISCHECKEDOUT, + NameConstants.JCR_BASEVERSION, + NameConstants.JCR_MERGEFAILED, + NameConstants.REP_NODETYPES, + + // Node types + NameConstants.NT_NODETYPE, + NameConstants.NT_PROPERTYDEFINITION, + NameConstants.NT_CHILDNODEDEFINITION, + NameConstants.NT_BASE, + NameConstants.JCR_NODETYPES, + NameConstants.JCR_PROTECTED, + NameConstants.JCR_ONPARENTVERSION, + NameConstants.JCR_MANDATORY, + NameConstants.JCR_AUTOCREATED, + NameConstants.JCR_FROZENMIXINTYPES, + NameConstants.JCR_NAME, + NameConstants.JCR_VALUECONSTRAINTS, + NameConstants.JCR_REQUIREDTYPE, + NameConstants.JCR_PROPERTYDEFINITION, + NameConstants.JCR_MULTIPLE, + NameConstants.JCR_DEFAULTVALUES, + NameConstants.JCR_SUPERTYPES, + NameConstants.JCR_NODETYPENAME, + NameConstants.JCR_ISMIXIN, + NameConstants.JCR_HASORDERABLECHILDNODES, + NameConstants.JCR_SAMENAMESIBLINGS, + NameConstants.JCR_REQUIREDPRIMARYTYPES, + NameConstants.JCR_CHILDNODEDEFINITION, + NameConstants.JCR_DEFAULTPRIMARYTYPE, + NameConstants.JCR_PRIMARYITEMNAME, + NameConstants.JCR_CHILDVERSIONHISTORY, + NameConstants.REP_VERSIONS, + NameConstants.REP_VERSIONSTORAGE, + NameConstants.REP_VERSION_REFERENCE, + NameConstants.REP_BASEVERSIONS, + + // Miscellaneous node types + NameConstants.MIX_CREATED, + NameConstants.MIX_ETAG, + NameConstants.MIX_LANGUAGE, + NameConstants.MIX_LASTMODIFIED, + NameConstants.MIX_LIFECYCLE, + NameConstants.MIX_MIMETYPE, + NameConstants.MIX_SHAREABLE, + NameConstants.MIX_SIMPLE_VERSIONABLE, + NameConstants.MIX_TITLE, + NameConstants.NT_ACTIVITY, + NameConstants.NT_ADDRESS, + NameConstants.NT_CONFIGURATION, + NameConstants.NT_QUERY, + NameConstants.NT_SHARE, + + // Miscellaneous names + NameConstants.REP_ACTIVITIES, + NameConstants.JCR_ACTIVITIES, + NameConstants.JCR_ACTIVITY, + NameConstants.JCR_ACTIVITY_TITLE, + NameConstants.JCR_XMLCHARACTERS, + NameConstants.JCR_XMLTEXT, + NameConstants.REP_CONFIGURATIONS, + NameConstants.JCR_CONFIGURATION, + NameConstants.JCR_CONFIGURATIONS, + NameConstants.JCR_COPIEDFROM, + NameConstants.JCR_CURRENT_LIFECYCLE_STATE, + NameConstants.JCR_ETAG, + NameConstants.JCR_HOST, + NameConstants.JCR_ID, + NameConstants.JCR_LIFECYCLE_POLICY, + NameConstants.JCR_PATH, + NameConstants.JCR_STATEMENT, + + }; // WARNING: Only edit if you really know what you're doing! + + private static final Map NAME_MAP = + new HashMap(); + + static { + assert NAME_ARRAY.length <= 0x80; + for (int i = 0; i < NAME_ARRAY.length; i++) { + NAME_MAP.put(NAME_ARRAY[i], i); + } + } + + /** + * Returns the seven-bit index of a common JCR name, or -1 if the given + * name is not known. + * + * @param name JCR name + * @return seven-bit index of the name, or -1 + */ + public static int nameToIndex(Name name) { + Integer index = NAME_MAP.get(name); + if (index != null) { + return index; + } else { + return -1; + } + } + + public static Name indexToName(int index) throws IOException { + try { + return NAME_ARRAY[index]; + } catch (ArrayIndexOutOfBoundsException e) { + throw new IOExceptionWithCause( + "Invalid common JCR name index: " + index, e); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleReader.java new file mode 100644 index 00000000000..a2926a8bc73 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleReader.java @@ -0,0 +1,675 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.commons.io.IOExceptionWithCause; +import org.apache.commons.io.input.CountingInputStream; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.Set; +import java.util.TimeZone; +import java.math.BigDecimal; + +import javax.jcr.PropertyType; + +/** + * Bundle deserializer. See the {@link BundleWriter} class for details of + * the serialization format. + * + * @see BundleWriter + */ +class BundleReader { + + /* + * Implementation note: if you change this class, also change BundleDumper + * accordingly. + */ + + /** Logger instance */ + private static Logger log = LoggerFactory.getLogger(BundleReader.class); + + /** + * Pre-calculated {@link TimeZone} objects for common timezone offsets. + */ + private static final TimeZone[] COMMON_TIMEZONES = { + TimeZone.getTimeZone("GMT+00:00"), // 0b00000 + TimeZone.getTimeZone("GMT+01:00"), // 0b00001 + TimeZone.getTimeZone("GMT+02:00"), // 0b00010 + TimeZone.getTimeZone("GMT+03:00"), // 0b00011 + TimeZone.getTimeZone("GMT+04:00"), // 0b00100 + TimeZone.getTimeZone("GMT+05:00"), // 0b00101 + TimeZone.getTimeZone("GMT+06:00"), // 0b00110 + TimeZone.getTimeZone("GMT+07:00"), // 0b00111 + TimeZone.getTimeZone("GMT+08:00"), // 0b01000 + TimeZone.getTimeZone("GMT+09:00"), // 0b01001 + TimeZone.getTimeZone("GMT+10:00"), // 0b01010 + TimeZone.getTimeZone("GMT+11:00"), // 0b01011 + TimeZone.getTimeZone("GMT+12:00"), // 0b01100 + TimeZone.getTimeZone("GMT+13:00"), // 0b01101 + TimeZone.getTimeZone("GMT+14:00"), // 0b01110 + TimeZone.getTimeZone("GMT+15:00"), // 0b01111 + TimeZone.getTimeZone("GMT-16:00"), // 0b10000 + TimeZone.getTimeZone("GMT-15:00"), // 0b10001 + TimeZone.getTimeZone("GMT-14:00"), // 0b10010 + TimeZone.getTimeZone("GMT-13:00"), // 0b10011 + TimeZone.getTimeZone("GMT-12:00"), // 0b10100 + TimeZone.getTimeZone("GMT-11:00"), // 0b10101 + TimeZone.getTimeZone("GMT-10:00"), // 0b10110 + TimeZone.getTimeZone("GMT-09:00"), // 0b10111 + TimeZone.getTimeZone("GMT-08:00"), // 0b11000 + TimeZone.getTimeZone("GMT-07:00"), // 0b11001 + TimeZone.getTimeZone("GMT-06:00"), // 0b11010 + TimeZone.getTimeZone("GMT-05:00"), // 0b11011 + TimeZone.getTimeZone("GMT-04:00"), // 0b11100 + TimeZone.getTimeZone("GMT-03:00"), // 0b11101 + TimeZone.getTimeZone("GMT-02:00"), // 0b11110 + TimeZone.getTimeZone("GMT-01:00"), // 0b11111 + }; + + private final BundleBinding binding; + + /** + * Counter for the number of bytes read from the input stream. + */ + private final CountingInputStream cin; + + /** + * Wrapper for reading structured data from the input stream. + */ + private final DataInputStream in; + + private final int version; + + /** + * The default namespace and the first six other namespaces used in this + * bundle. Used by the {@link #readName()} method to keep track of + * already seen namespaces. + */ + private final String[] namespaces = + // NOTE: The length of this array must be seven + { Name.NS_DEFAULT_URI, null, null, null, null, null, null }; + + /** + * Creates a new bundle deserializer. + * + * @param binding bundle binding + * @param stream stream from which the bundle is read + * @throws IOException if an I/O error occurs. + */ + public BundleReader(BundleBinding binding, InputStream stream) + throws IOException { + this.binding = binding; + this.cin = new CountingInputStream(stream); + this.in = new DataInputStream(cin); + this.version = in.readUnsignedByte(); + } + + /** + * Deserializes a NodePropBundle from a data input stream. + * + * @param id the node id for the new bundle + * @return the bundle + * @throws IOException if an I/O error occurs. + */ + public NodePropBundle readBundle(NodeId id) throws IOException { + long start = cin.getByteCount(); + NodePropBundle bundle = new NodePropBundle(id); + if (version >= BundleBinding.VERSION_3) { + readBundleNew(bundle); + } else { + readBundleOld(bundle); + } + bundle.setSize(cin.getByteCount() - start); + return bundle; + } + + private void readBundleNew(NodePropBundle bundle) throws IOException { + // node type + bundle.setNodeTypeName(readName()); + + // parentUUID + NodeId parentId = readNodeId(); + if (BundleBinding.NULL_PARENT_ID.equals(parentId)) { + parentId = null; + } + bundle.setParentId(parentId); + + // read modcount + bundle.setModCount((short) readVarInt()); + + int b = in.readUnsignedByte(); + bundle.setReferenceable((b & 1) != 0); + + // mixin types + int mn = readVarInt((b >> 7) & 1, 1); + if (mn == 0) { + bundle.setMixinTypeNames(Collections.emptySet()); + } else if (mn == 1) { + bundle.setMixinTypeNames(Collections.singleton(readName())); + } else { + Set mixins = new HashSet(mn * 2); + for (int i = 0; i < mn; i++) { + mixins.add(readName()); + } + bundle.setMixinTypeNames(mixins); + } + + // properties + int pn = readVarInt((b >> 4) & 7, 7); + for (int i = 0; i < pn; i++) { + PropertyId id = new PropertyId(bundle.getId(), readName()); + bundle.addProperty(readPropertyEntry(id)); + } + + // child nodes (list of name/uuid pairs) + int nn = readVarInt((b >> 2) & 3, 3); + for (int i = 0; i < nn; i++) { + Name name = readQName(); + NodeId id = readNodeId(); + bundle.addChildNodeEntry(name, id); + } + + // read shared set + int sn = readVarInt((b >> 1) & 1, 1); + if (sn == 0) { + bundle.setSharedSet(Collections.emptySet()); + } else if (sn == 1) { + bundle.setSharedSet(Collections.singleton(readNodeId())); + } else { + Set shared = new HashSet(); + for (int i = 0; i < sn; i++) { + shared.add(readNodeId()); + } + bundle.setSharedSet(shared); + } + } + + private void readBundleOld(NodePropBundle bundle) throws IOException { + // read primary type...special handling + int a = in.readUnsignedByte(); + int b = in.readUnsignedByte(); + int c = in.readUnsignedByte(); + String uri = binding.nsIndex.indexToString(a << 16 | b << 8 | c); + String local = binding.nameIndex.indexToString(in.readInt()); + bundle.setNodeTypeName( + NameFactoryImpl.getInstance().create(uri, local)); + + // parentUUID + bundle.setParentId(readNodeId()); + + // definitionId + in.readUTF(); + + // mixin types + Name name = readIndexedQName(); + if (name != null) { + Set mixinTypeNames = new HashSet(); + do { + mixinTypeNames.add(name); + name = readIndexedQName(); + } while (name != null); + bundle.setMixinTypeNames(mixinTypeNames); + } else { + bundle.setMixinTypeNames(Collections.emptySet()); + } + + // properties + name = readIndexedQName(); + while (name != null) { + PropertyId pId = new PropertyId(bundle.getId(), name); + NodePropBundle.PropertyEntry pState = readPropertyEntry(pId); + // skip redundant primaryType, mixinTypes and uuid properties + if (!name.equals(NameConstants.JCR_PRIMARYTYPE) + && !name.equals(NameConstants.JCR_UUID)) { + bundle.addProperty(pState); + } + name = readIndexedQName(); + } + + // set referenceable flag + bundle.setReferenceable(in.readBoolean()); + + // child nodes (list of uuid/name pairs) + NodeId childId = readNodeId(); + while (childId != null) { + bundle.addChildNodeEntry(readQName(), childId); + childId = readNodeId(); + } + + // read modcount, since version 1.0 + if (version >= BundleBinding.VERSION_1) { + bundle.setModCount(in.readShort()); + } + + // read shared set, since version 2.0 + if (version >= BundleBinding.VERSION_2) { + // shared set (list of parent uuids) + NodeId parentId = readNodeId(); + if (parentId != null) { + Set shared = new HashSet(); + do { + shared.add(parentId); + parentId = readNodeId(); + } while (parentId != null); + bundle.setSharedSet(shared); + } else { + bundle.setSharedSet(Collections.emptySet()); + } + } else { + bundle.setSharedSet(Collections.emptySet()); + } + } + + /** + * Deserializes a PropertyState from the data input stream. + * + * @param id the property id for the new property entry + * @return the property entry + * @throws IOException if an I/O error occurs. + */ + private NodePropBundle.PropertyEntry readPropertyEntry(PropertyId id) + throws IOException { + NodePropBundle.PropertyEntry entry = new NodePropBundle.PropertyEntry(id); + + int count = 1; + if (version >= BundleBinding.VERSION_3) { + int b = in.readUnsignedByte(); + + entry.setType(b & 0x0f); + + int len = b >>> 4; + if (len != 0) { + entry.setMultiValued(true); + if (len == 0x0f) { + count = readVarInt() + 0x0f - 1; + } else { + count = len - 1; + } + } + + entry.setModCount((short) readVarInt()); + } else { + // type and modcount + int type = in.readInt(); + entry.setModCount((short) ((type >> 16) & 0x0ffff)); + type &= 0x0ffff; + entry.setType(type); + + // multiValued + entry.setMultiValued(in.readBoolean()); + + // definitionId + in.readUTF(); + + // count + count = in.readInt(); + } + + // values + InternalValue[] values = new InternalValue[count]; + String[] blobIds = new String[count]; + for (int i = 0; i < count; i++) { + InternalValue val; + int type = entry.getType(); + switch (type) { + case PropertyType.BINARY: + int size = in.readInt(); + if (size == BundleBinding.BINARY_IN_DATA_STORE) { + val = InternalValue.create(binding.dataStore, readString()); + } else if (size == BundleBinding.BINARY_IN_BLOB_STORE) { + blobIds[i] = readString(); + try { + BLOBStore blobStore = binding.getBlobStore(); + if (blobStore instanceof ResourceBasedBLOBStore) { + val = InternalValue.create(((ResourceBasedBLOBStore) blobStore).getResource(blobIds[i])); + } else { + val = InternalValue.create(blobStore.get(blobIds[i])); + } + } catch (IOException e) { + if (binding.errorHandling.ignoreMissingBlobs()) { + log.warn("Ignoring error while reading blob-resource: " + e); + val = InternalValue.create(new byte[0]); + } else { + throw e; + } + } catch (Exception e) { + throw new IOExceptionWithCause("Unable to create property value: " + e.toString(), e); + } + } else { + // short values into memory + byte[] data = new byte[size]; + in.readFully(data); + val = InternalValue.create(data); + } + break; + case PropertyType.DOUBLE: + val = InternalValue.create(in.readDouble()); + break; + case PropertyType.DECIMAL: + val = InternalValue.create(readDecimal()); + break; + case PropertyType.LONG: + if (version >= BundleBinding.VERSION_3) { + val = InternalValue.create(readVarLong()); + } else { + val = InternalValue.create(in.readLong()); + } + break; + case PropertyType.BOOLEAN: + val = InternalValue.create(in.readBoolean()); + break; + case PropertyType.NAME: + val = InternalValue.create(readQName()); + break; + case PropertyType.WEAKREFERENCE: + val = InternalValue.create(readNodeId(), true); + break; + case PropertyType.REFERENCE: + val = InternalValue.create(readNodeId(), false); + break; + case PropertyType.DATE: + if (version >= BundleBinding.VERSION_3) { + val = InternalValue.create(readDate()); + break; + } // else fall through + default: + if (version >= BundleBinding.VERSION_3) { + val = InternalValue.valueOf( + readString(), entry.getType()); + } else { + // because writeUTF(String) has a size limit of 64k, + // Strings are serialized as + int len = in.readInt(); + byte[] bytes = new byte[len]; + in.readFully(bytes); + String stringVal = new String(bytes, StandardCharsets.UTF_8); + + // https://issues.apache.org/jira/browse/JCR-3083 + if (PropertyType.DATE == entry.getType()) { + val = InternalValue.createDate(stringVal); + } else { + val = InternalValue.valueOf(stringVal, entry.getType()); + } + } + } + values[i] = val; + } + entry.setValues(values); + entry.setBlobIds(blobIds); + + return entry; + } + + /** + * Deserializes a node identifier + * + * @return the node id + * @throws IOException in an I/O error occurs. + */ + private NodeId readNodeId() throws IOException { + if (version >= BundleBinding.VERSION_3 || in.readBoolean()) { + long msb = in.readLong(); + long lsb = in.readLong(); + return new NodeId(msb, lsb); + } else { + return null; + } + } + + /** + * Deserializes a BigDecimal + * + * @return the decimal + * @throws IOException in an I/O error occurs. + */ + private BigDecimal readDecimal() throws IOException { + if (in.readBoolean()) { + // TODO more efficient serialization format + return new BigDecimal(readString()); + } else { + return null; + } + } + + /** + * Deserializes a Name + * + * @return the qname + * @throws IOException in an I/O error occurs. + */ + private Name readQName() throws IOException { + if (version >= BundleBinding.VERSION_3) { + return readName(); + } + + String uri = binding.nsIndex.indexToString(in.readInt()); + String local = in.readUTF(); + return NameFactoryImpl.getInstance().create(uri, local); + } + + /** + * Deserializes an indexed Name + * + * @return the qname + * @throws IOException in an I/O error occurs. + */ + private Name readIndexedQName() throws IOException { + if (version >= BundleBinding.VERSION_3) { + return readName(); + } + + int index = in.readInt(); + if (index < 0) { + return null; + } else { + String uri = binding.nsIndex.indexToString(index); + String local = binding.nameIndex.indexToString(in.readInt()); + return NameFactoryImpl.getInstance().create(uri, local); + } + } + + /** + * Deserializes a name written using bundle serialization version 3. + * + * @return deserialized name + * @throws IOException if an I/O error occurs + */ + private Name readName() throws IOException { + int b = in.readUnsignedByte(); + if ((b & 0x80) == 0) { + return BundleNames.indexToName(b); + } else { + String uri; + int ns = (b >> 4) & 0x07; + if (ns < namespaces.length && namespaces[ns] != null) { + uri = namespaces[ns]; + } else { + uri = readString(); + if (ns < namespaces.length) { + namespaces[ns] = uri; + } + } + + String local = new String(readBytes((b & 0x0f) + 1, 0x10), StandardCharsets.UTF_8); + + return NameFactoryImpl.getInstance().create(uri, local); + } + } + + /** + * Deserializes a variable-length integer written using bundle + * serialization version 3. + * + * @return deserialized integer + * @throws IOException if an I/O error occurs + */ + private int readVarInt() throws IOException { + int b = in.readUnsignedByte(); + if ((b & 0x80) == 0) { + return b; + } else { + return readVarInt() << 7 | b & 0x7f; + } + } + + private int readVarInt(int value, int base) throws IOException { + if (value < base) { + return value; + } else { + return readVarInt() + base; + } + } + + /** + * Deserializes a variable-length long written using bundle + * serialization version 3. + * + * @return deserialized long + * @throws IOException if an I/O error occurs + */ + private long readVarLong() throws IOException { + long value = 0; + int bits = 0; + long b; + do { + b = in.readUnsignedByte(); + if (bits < 57) { + value = (b & 0x7f) << 57 | value >>> 7; + bits += 7; + } else { + value = (b & 0x01) << 63 | value >>> 1; + bits = 64; + } + } while ((b & 0x80) != 0); + value = value >>> (64 - bits); + if ((value & 1) != 0) { + return ~(value >>> 1); + } else { + return value >>> 1; + } + } + + /** + * Deserializes a specially encoded date written using bundle + * serialization version 3. + * + * @return deserialized date + * @throws IOException if an I/O error occurs + */ + private Calendar readDate() throws IOException { + long ts = readVarLong(); + + TimeZone tz; + if ((ts & 1) == 0) { + tz = COMMON_TIMEZONES[0]; + ts >>= 1; + } else if ((ts & 2) == 0) { + tz = COMMON_TIMEZONES[((int) ts >> 2) & 0x1f]; // 5 bits; + ts >>= 7; + } else { + int m = ((int) ts << 19) >> 21; // 11 bits, sign-extended + int h = m / 60; + String s; + if (m < 0) { + s = String.format("GMT-%02d:%02d", -h, h * 60 - m); + } else { + s = String.format("GMT+%02d:%02d", h, m - h * 60); + } + tz = TimeZone.getTimeZone(s); + ts >>= 13; + } + + int u = 0; + int s = 0; + int m = 0; + int h = 0; + int type = (int) ts & 3; + ts >>= 2; + switch (type) { + case 3: + u = (int) ts & 0x3fffffff; // 30 bits + s = u / 1000; + m = s / 60; + h = m / 60; + m -= h * 60; + s -= (h * 60 + m) * 60; + u -= ((h * 60 + m) * 60 + s) * 1000; + ts >>= 30; + break; + case 2: + m = (int) ts & 0x07ff; // 11 bits + h = m / 60; + m -= h * 60; + ts >>= 11; + break; + case 1: + h = (int) ts & 0x1f; // 5 bits + ts >>= 5; + break; + } + + int d = (int) ts & 0x01ff; // 9 bits; + ts >>= 9; + int y = (int) (ts + 2010); + + Calendar value = Calendar.getInstance(tz); + if (y <= 0) { + value.set(Calendar.YEAR, 1 - y); + value.set(Calendar.ERA, GregorianCalendar.BC); + } else { + value.set(Calendar.YEAR, y); + value.set(Calendar.ERA, GregorianCalendar.AD); + } + value.set(Calendar.DAY_OF_YEAR, d); + value.set(Calendar.HOUR_OF_DAY, h); + value.set(Calendar.MINUTE, m); + value.set(Calendar.SECOND, s); + value.set(Calendar.MILLISECOND, u); + + return value; + } + + private String readString() throws IOException { + if (version >= BundleBinding.VERSION_3) { + return new String(readBytes(0, 0), StandardCharsets.UTF_8); + } else { + return in.readUTF(); + } + } + + private byte[] readBytes(int len, int base) throws IOException { + byte[] bytes = new byte[readVarInt(len, base)]; + in.readFully(bytes); + return bytes; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleWriter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleWriter.java new file mode 100644 index 00000000000..5b346736cb8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/BundleWriter.java @@ -0,0 +1,729 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.util; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; +import java.util.Collection; +import java.util.GregorianCalendar; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.commons.io.IOExceptionWithCause; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle.ChildNodeEntry; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle.PropertyEntry; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Bundle serializer. + * + * @see BundleReader + */ +class BundleWriter { + + /** Logger instance */ + private static Logger log = LoggerFactory.getLogger(BundleWriter.class); + + private final BundleBinding binding; + + private final DataOutputStream out; + + /** + * The default namespace and the first six other namespaces used in this + * bundle. Used by the {@link #writeName(Name)} method to keep track of + * already seen namespaces. + */ + private final String[] namespaces = + // NOTE: The length of this array must be seven + { Name.NS_DEFAULT_URI, null, null, null, null, null, null }; + + /** + * Creates a new bundle serializer. + * + * @param binding bundle binding + * @param stream stream to which the bundle will be written + * @throws IOException if an I/O error occurs. + */ + public BundleWriter(BundleBinding binding, OutputStream stream) + throws IOException { + assert namespaces.length == 7; + this.binding = binding; + this.out = new DataOutputStream(stream); + this.out.writeByte(BundleBinding.VERSION_CURRENT); + } + + /** + * Serializes a NodePropBundle to a data output stream + * + * @param bundle the bundle to serialize + * @throws IOException if an I/O error occurs. + */ + public void writeBundle(NodePropBundle bundle) + throws IOException { + long size = out.size(); + + // primaryType + writeName(bundle.getNodeTypeName()); + + // parentUUID + NodeId parentId = bundle.getParentId(); + if (parentId == null) { + parentId = BundleBinding.NULL_PARENT_ID; + } + writeNodeId(parentId); + + // write mod count + writeVarInt(bundle.getModCount()); + + Collection mixins = bundle.getMixinTypeNames(); + Collection properties = bundle.getPropertyEntries(); + Collection nodes = bundle.getChildNodeEntries(); + Collection shared = bundle.getSharedSet(); + + int mn = mixins.size(); + int pn = properties.size(); + int nn = nodes.size(); + int sn = shared.size(); + int referenceable = 0; + if (bundle.isReferenceable()) { + referenceable = 1; + } + out.writeByte( + Math.min(mn, 1) << 7 + | Math.min(pn, 7) << 4 + | Math.min(nn, 3) << 2 + | Math.min(sn, 1) << 1 + | referenceable); + + // mixin types + writeVarInt(mn, 1); + for (Name name : mixins) { + writeName(name); + } + + // properties + writeVarInt(pn, 7); + for (PropertyEntry property : properties) { + writeState(property); + } + + // child nodes (list of name/uuid pairs) + writeVarInt(nn, 3); + for (ChildNodeEntry child : nodes) { + writeName(child.getName()); // name + writeNodeId(child.getId()); // uuid + } + + // write shared set + writeVarInt(sn, 1); + for (NodeId nodeId: shared) { + writeNodeId(nodeId); + } + + // set size of bundle + bundle.setSize(out.size() - size); + } + + /** + * Serializes a property entry. The serialization begins with the + * property name followed by a single byte that encodes the type and + * multi-valuedness of the property: + *

    +     * +-------------------------------+
    +     * |   mv count    |     type      |
    +     * +-------------------------------+
    +     * 
    + *

    + * The lower four bits encode the property type (0-12 in JCR 2.0) and + * higher bits indicate whether this is a multi-valued property and how + * many property values there are. A value of 0 is reserved for + * single-valued properties (that are guaranteed to always have just a + * single value), and all non-zero values indicate a multi-valued property. + *

    + * In multi-valued properties the exact value of the "mv count" field is + * the number of property values plus one and truncated at 15 (the highest + * four-bit value). If there are 14 or more (14 + 1 == 15) property values, + * then the number of additional values is serialized as a variable-length + * integer (see {@link #writeVarInt(int)}) right after this byte. + *

    + * The modification count of the property state is written next as a + * variable-length integer, followed by the serializations of all the + * values of this property. + * + * @param state the property entry to store + * @throws IOException if an I/O error occurs. + */ + private void writeState(NodePropBundle.PropertyEntry state) + throws IOException { + writeName(state.getName()); + + InternalValue[] values = state.getValues(); + + int type = state.getType(); + if (type < 0 || type > 0xf) { + throw new IOException("Illegal property type " + type); + } + if (state.isMultiValued()) { + int len = values.length + 1; + if (len < 0x0f) { + out.writeByte(len << 4 | type); + } else { + out.writeByte(0xf0 | type); + writeVarInt(len - 0x0f); + } + } else { + if (values.length != 1) { + throw new IOException( + "Single values property with " + values.length + " values: " + + state.getName()); + } + out.writeByte(type); + } + + writeVarInt(state.getModCount()); + + // values + for (int i = 0; i < values.length; i++) { + InternalValue val = values[i]; + switch (type) { + case PropertyType.BINARY: + try { + long size = val.getLength(); + if (val.isInDataStore()) { + out.writeInt(BundleBinding.BINARY_IN_DATA_STORE); + writeString(val.toString()); + } else if (binding.dataStore != null) { + writeSmallBinary(val, state, i); + } else if (size < 0) { + log.warn("Blob has negative size. Potential loss of data. " + + "id={} idx={}", state.getId(), String.valueOf(i)); + out.writeInt(0); + values[i] = InternalValue.create(new byte[0]); + val.discard(); + } else if (size > binding.getMinBlobSize()) { + // special handling required for binary value: + // spool binary value to file in blob store + out.writeInt(BundleBinding.BINARY_IN_BLOB_STORE); + String blobId = state.getBlobId(i); + if (blobId == null) { + BLOBStore blobStore = binding.getBlobStore(); + try { + InputStream in = val.getStream(); + try { + blobId = blobStore.createId(state.getId(), i); + blobStore.put(blobId, in, size); + state.setBlobId(blobId, i); + } finally { + IOUtils.closeQuietly(in); + } + } catch (Exception e) { + String msg = "Error while storing blob. id=" + + state.getId() + " idx=" + i + " size=" + size; + log.error(msg, e); + throw new IOExceptionWithCause(msg, e); + } + try { + // replace value instance with value + // backed by resource in blob store and delete temp file + if (blobStore instanceof ResourceBasedBLOBStore) { + values[i] = InternalValue.create(((ResourceBasedBLOBStore) blobStore).getResource(blobId)); + } else { + values[i] = InternalValue.create(blobStore.get(blobId)); + } + } catch (Exception e) { + log.error("Error while reloading blob. truncating. id=" + + state.getId() + " idx=" + i + " size=" + size, e); + values[i] = InternalValue.create(new byte[0]); + } + val.discard(); + } + // store id of blob as property value + writeString(blobId); // value + } else { + // delete evt. blob + byte[] data = writeSmallBinary(val, state, i); + // replace value instance with value + // backed by resource in blob store and delete temp file + values[i] = InternalValue.create(data); + val.discard(); + } + } catch (RepositoryException e) { + String msg = "Error while storing blob. id=" + + state.getId() + " idx=" + i + " value=" + val; + log.error(msg, e); + throw new IOExceptionWithCause(msg, e); + } + break; + case PropertyType.DOUBLE: + try { + out.writeDouble(val.getDouble()); + } catch (RepositoryException e) { + throw convertToIOException(type, e); + } + break; + case PropertyType.DECIMAL: + try { + writeDecimal(val.getDecimal()); + } catch (RepositoryException e) { + throw convertToIOException(type, e); + } + break; + case PropertyType.LONG: + try { + writeVarLong(val.getLong()); + } catch (RepositoryException e) { + throw convertToIOException(type, e); + } + break; + case PropertyType.BOOLEAN: + try { + out.writeBoolean(val.getBoolean()); + } catch (RepositoryException e) { + throw convertToIOException(type, e); + } + break; + case PropertyType.NAME: + try { + writeName(val.getName()); + } catch (RepositoryException e) { + throw convertToIOException(type, e); + } + break; + case PropertyType.WEAKREFERENCE: + case PropertyType.REFERENCE: + writeNodeId(val.getNodeId()); + break; + case PropertyType.DATE: + try { + writeDate(val.getCalendar()); + } catch (RepositoryException e) { + throw convertToIOException(type, e); + } + break; + case PropertyType.STRING: + case PropertyType.PATH: + case PropertyType.URI: + writeString(val.toString()); + break; + default: + throw new IOException("Inknown property type: " + type); + } + } + } + + private static IOException convertToIOException(int propertyType, Exception e) { + String typeName = PropertyType.nameFromValue(propertyType); + String message = "Unexpected error for property type "+ typeName +" value."; + log.error(message, e); + return new IOExceptionWithCause(message, e); + } + + /** + * Write a small binary value and return the data. + * + * @param value the binary value + * @param state the property state (for error messages) + * @param i the index (for error messages) + * @return the data + * @throws IOException if the data could not be read + */ + private byte[] writeSmallBinary( + InternalValue value, NodePropBundle.PropertyEntry state, int i) + throws IOException { + try { + int size = (int) value.getLength(); + out.writeInt(size); + byte[] data = new byte[size]; + DataInputStream in = + new DataInputStream(value.getStream()); + try { + in.readFully(data); + } finally { + IOUtils.closeQuietly(in); + } + out.write(data, 0, data.length); + return data; + } catch (Exception e) { + String msg = "Error while storing blob. id=" + + state.getId() + " idx=" + i + " value=" + value; + log.error(msg, e); + throw new IOExceptionWithCause(msg, e); + } + } + + /** + * Serializes a node identifier + * + * @param id the node id + * @throws IOException in an I/O error occurs. + */ + private void writeNodeId(NodeId id) throws IOException { + out.writeLong(id.getMostSignificantBits()); + out.writeLong(id.getLeastSignificantBits()); + } + + /** + * Serializes a BigDecimal + * + * @param decimal the decimal number + * @throws IOException in an I/O error occurs. + */ + private void writeDecimal(BigDecimal decimal) throws IOException { + if (decimal == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + // TODO more efficient serialization format + writeString(decimal.toString()); + } + } + + /** + * Serializes a name. The name encoding works as follows: + *

    + * First; if the name is known by the {@link BundleNames} class (this + * includes the null name), then the name is serialized + * as a single byte using the following format. + *

    +     * +-------------------------------+
    +     * | 0 |    common name index      |
    +     * +-------------------------------+
    +     * 
    + *

    + * Second; if the name is not known, it gets serialized as a + * variable-length field whose first byte looks like this: + *

    +     * +-------------------------------+
    +     * | 1 | ns index  |  name length  |
    +     * +-------------------------------+
    +     * 
    + *

    + * The three-bit namespace index identifies the namespace of the name. + * The serializer keeps track of the default namespace (value 0) and at + * most six other other namespaces (values 1-6), in the order they appear + * in the bundle. When one of these six custom namespaces first appears + * in the bundle, then the namespace URI is written using + * {@link #writeString(String)} right after this byte. + * Later uses of such a namespace simply refers back to the already read + * namespace URI string. Any other namespaces are identified with value 7 + * and always written to the bundle after this byte. + *

    + * The four-bit name length field indicates the length (in UTF-8 bytes) + * of the local part of the name. Since zero-length local names are not + * allowed, the length is first decremented by one before storing in this + * field. The UTF-8 byte sequence is written out after this byte and the + * possible namespace URI string. If the length of the local name is + * larger than 15 (i.e. would be stored as 0x0f or more), then the value + * 0x0f is stored as the name length and the name string is written as + * UTF-8 using {@link #writeBytes(byte[], int)} with a base length of + * 0x10 (0x0f + 1). + * + * @param name the name + * @throws IOException in an I/O error occurs. + */ + private void writeName(Name name) throws IOException { + int index = BundleNames.nameToIndex(name); + if (index != -1) { + assert 0 <= index && index < 0x80; + out.writeByte(index); + } else { + String uri = name.getNamespaceURI(); + int ns = 0; + while (ns < namespaces.length + && namespaces[ns] != null + && !namespaces[ns].equals(uri)) { + ns++; + } + + String local = name.getLocalName(); + if (local.length() == 0) { + throw new IOException("Attempt to write an empty local name: " + name); + } + byte[] bytes = local.getBytes(StandardCharsets.UTF_8); + int len = Math.min(bytes.length - 1, 0x0f); + + out.writeByte(0x80 | ns << 4 | len); + if (ns == namespaces.length || namespaces[ns] == null) { + writeString(uri); + if (ns < namespaces.length) { + namespaces[ns] = uri; + } + } + if (len != 0x0f) { + out.write(bytes); + } else { + writeBytes(bytes, 0x0f + 1); + } + } + } + + /** + * Serializes an integer using a variable-length encoding that favors + * small positive numbers. The serialization consists of one to five + * bytes of the following format: + *

    +     * +-------------------------------+
    +     * | c | 7 least significant bits  |
    +     * +-------------------------------+
    +     * 
    + *

    + * If the given integer fits in seven bits (i.e. the value between + * 0 and 127, inclusive), then it is written as-is in a single byte. + * Otherwise the continuation flag c is set and the least + * significant seven bits are written together with the flag as a single + * byte. The integer is then shifed right seven bits and the process + * continues from the beginning. + *

    + * This format uses a single byte for values 0-127, two bytes for + * 128-16343, three for 16343-2097151, four for 2097152-268435455 + * and five bytes for all other 32-bit numbers (including negative ones). + * + * @param integer integer value + * @throws IOException if an I/O error occurs + */ + private void writeVarInt(int value) throws IOException { + while (true) { + int b = value & 0x7f; + if (b != value) { + out.writeByte(b | 0x80); + value >>>= 7; // unsigned shift + } else { + out.writeByte(b); + return; + } + } + } + + private void writeVarInt(int value, int base) throws IOException { + if (value >= base) { + writeVarInt(value - base); + } + } + + /** + * Serializes a long value using a variable length encoding like the + * one used by {@link #writeVarInt(int)} for integer values. Before + * writing out, the value is first normalized to an unsigned value + * by moving the sign bit to be the end negating the other bits of + * a negative value. This normalization step maximizes the number of + * zero high order bits for typical small values (positive or negative), + * and thus keeps the serialization short. + * + * @param value long value + * @throws IOException if an I/O error occurs + */ + private void writeVarLong(long value) throws IOException { + // Normalize to an unsigned value with the sign as the lowest bit + if (value < 0) { + value = ~value << 1 | 1; + } else { + value <<= 1; + } + while (true) { + long b = value & 0x7f; + if (b != value) { + out.writeByte((int) b | 0x80); + value >>>= 7; // unsigned shift + } else { + out.writeByte((int) b); + return; + } + } + } + + /** + * Serializes a JCR date value using the {@link #writeVarLong(long)} + * serialization on a special 64-bit date encoding. This encoding maps + * the sYYYY-MM-DDThh:mm:ss.sssTZD date format used by + * JCR to an as small 64 bit integer (positive or negative) as possible, + * while preserving full accuracy (including time zone offsets) and + * favouring common levels of accuracy (per minute, hour and day) over + * full millisecond level detail. + *

    + * Each date value is mapped to separate timestamp and timezone fields, + * both of whose lenghts are variable: + *

    +     * +----- ... ------- ... --+
    +     * |  timestamp  | timezone |
    +     * +----- ... ------- ... --+
    +     * 
    + *

    + * The type and length of the timezone field can be determined by looking + * at the two least significant bits of the value: + *

    + *
    ?0
    + *
    + * UTC time. The length of the timezone field is just one bit, + * i.e. the second bit is already a part of the timestamp field. + *
    + *
    01
    + *
    + * The offset is counted as hours from UTC, and stored as the number + * of hours (positive or negative) in the next 5 bits (range from + * -16 to +15 hours), making the timezone field 7 bits long in total. + *
    + *
    11
    + *
    + * The offset is counted as hours and minutes from UTC, and stored + * as the total minute offset (positive or negative) in the next + * 11 bits (range from -17 to +17 hours), making the timezone field + * 13 bits long in total. + *
    + *
    + *

    + * The remaining 51-63 bits of the encoded value make up the timestamp + * field that also uses the two least significant bits to indicate the + * type and length of the field: + *

    + *
    00
    + *
    + * sYYYY-MM-DDT00:00:00.000, i.e. midnight of the + * specified date. The next 9 bits encode the day within the year + * (starting from 1, maximum value 366) and the remaining bits are + * used for the year, stored as an offset from year 2010. + *
    + *
    01
    + *
    + * sYYYY-MM-DDThh:00:00.000, i.e. at the hour. The + * next 5 bits encode the hour within the day (starting from 0, + * maximum value 23) and the remaining bits are used as described + * above for the date. + *
    + *
    10
    + *
    + * sYYYY-MM-DDThh:mm:00.000, i.e. at the minute. The + * next 11 bits encode the minute within the day (starting from 0, + * maximum value 1439) and the remaining bits are used as described + * above for the date. + *
    + *
    11
    + *
    + * sYYYY-MM-DDThh:mm:ss.sss, i.e. full millisecond + * accuracy. The next 30 bits encode the millisecond within the + * day (starting from 0, maximum value 87839999) and the remaining + * bits are used as described above for the date. + *
    + *
    + *

    + * With full timezone and millisecond accuracies, this encoding leaves + * 10 bits (64 - 9 - 30 - 2 - 11 - 2) for the date offset, which allows + * for representation of all timestamps between years 1498 and 2521. + * Timestamps outside this range and with a minute-level timezone offset + * are automatically truncated to minute-level accuracy to support the + * full range of years -9999 to 9999 specified in JCR. + *

    + * Note that the year, day of year, and time of day values are stored + * as separate bit sequences to avoid problems with changing leap second + * or leap year definitions. Bit fields are used for better encoding and + * decoding performance than what would be possible with the slightly more + * space efficient mechanism of using multiplication and modulo divisions + * to separate the different timestamp fields. + * + * @param value date value + * @throws IOException if an I/O error occurs + */ + private void writeDate(Calendar value) throws IOException { + int y = value.get(Calendar.YEAR); + if (value.isSet(Calendar.ERA) + && value.get(Calendar.ERA) == GregorianCalendar.BC) { + y = 1 - y; // convert to an astronomical year + } + y -= 2010; // use a recent offset NOTE: do not change this! + + int d = value.get(Calendar.DAY_OF_YEAR); + int h = value.get(Calendar.HOUR_OF_DAY); + int m = value.get(Calendar.MINUTE); + int s = value.get(Calendar.SECOND); + int u = value.get(Calendar.MILLISECOND); + int z = value.getTimeZone().getOffset(value.getTimeInMillis()) / (60 * 1000); + int zh = z / 60; + int zm = z - zh * 60; + + long ts = y << 9 | d & 0x01ff; + + if ((u != 0 || s != 0) && ((-512 <= y && y < 512) || zm == 0)) { + ts <<= 30; + ts |= (((h * 60 + m) * 60 + s) * 1000 + u) & 0x3fffffff; // 30 bits + ts <<= 2; + ts |= 3; + } else if (m != 0) { + ts <<= 11; + ts |= (h * 60 + m) & 0x07ff; // 11 bits + ts <<= 2; + ts |= 2; + } else if (h != 0) { + ts <<= 5; + ts |= h & 0x1f; // 5 bits + ts <<= 2; + ts |= 1; + } else { + ts <<= 2; + } + + if (zm != 0) { + ts <<= 11; + ts |= z & 0x07ff; // 11 bits + writeVarLong(ts << 2 | 3); + } else if (zh != 0) { + ts <<= 5; + ts |= zh & 0x1f; // 5 bits + writeVarLong(ts << 2 | 1); + } else { + writeVarLong(ts << 1); + } + } + + /** + * Serializes a string in UTF-8. The length of the UTF-8 byte sequence + * is first written as a variable-length string (see + * {@link #writeVarInt(int)}), and then the sequence itself is written. + * + * @param value string value + * @throws IOException if an I/O error occurs + */ + private void writeString(String value) throws IOException { + writeBytes(value.getBytes(StandardCharsets.UTF_8), 0); + } + + /** + * Serializes the given array of bytes. The length of the byte array is + * first written as a {@link #writeVarInt(int) variable length integer}, + * followed by the given bytes. + * + * @param bytes the bytes to be serialized + * @param base optional base length + * @throws IOException if an I/O error occurs + */ + private void writeBytes(byte[] bytes, int base) throws IOException { + assert bytes.length >= base; + writeVarInt(bytes.length - base); + out.write(bytes); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/ErrorHandling.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/ErrorHandling.java new file mode 100644 index 00000000000..9c674209912 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/ErrorHandling.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.core.persistence.util; + +import java.util.HashSet; + +/** + * ErrorHandling configuration abstraction class + */ +public final class ErrorHandling { + + /** + * Controls if references to missing blob resources are treated as errors + * or not. + */ + public static final String IGNORE_MISSING_BLOBS = "IGN_MISSING_BLOBS"; + + /** all available configuration codes */ + private static final String[] CODES = { + IGNORE_MISSING_BLOBS + }; + + /** the flags */ + private final HashSet flags = new HashSet(); + + /** + * Creates a default error handling config. + */ + public ErrorHandling() { + } + + /** + * Creates a new error handling configuration based on the given string. + * The individual flags should be separated with "|". + * + * @param str flags + */ + public ErrorHandling(String str) { + for (int i = 0; i < CODES.length; i++) { + if (str.indexOf(CODES[i]) >= 0) { + flags.add(CODES[i]); + } + } + } + + /** + * Checks if error handling is set to ignore missing blobs + * @return true if error handling is set to ignore missing blobs. + */ + public boolean ignoreMissingBlobs() { + return flags.contains(IGNORE_MISSING_BLOBS); + } + + /** + * Returns the string representation where the flags are separated + * with "|". + * @return the string representation. + */ + public String toString() { + StringBuilder ret = new StringBuilder("|"); + for (String flag : flags) { + ret.append(flag); + } + ret.append("|"); + return ret.toString(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/FileBasedIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/FileBasedIndex.java new file mode 100644 index 00000000000..791ec988030 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/FileBasedIndex.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.core.persistence.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Map; +import java.util.Properties; + +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.util.StringIndex; + +/** + * Implements a {@link StringIndex} that is based on a hashmap and persists + * the names as property file. + */ +public class FileBasedIndex extends HashMapIndex { + + /** + * the filesystem resource that stores the lookup tables. + */ + private FileSystemResource file; + + /** + * the time when the resource was last modified. + */ + private long lastModified = -1; + + /** + * Creates a new hashmap index and loads the lookup tables from the + * filesystem resource. If it does not exist yet, it will create a new one. + * + * @param file the filesystem resource that stores the lookup tables. + * + * @throws IOException if an I/O error occurs. + * @throws FileSystemException if an I/O error occurs. + */ + public FileBasedIndex(FileSystemResource file) + throws FileSystemException, IOException { + this.file = file; + if (!file.exists()) { + file.makeParentDirs(); + file.getOutputStream().close(); + } + load(); + } + + /** + * Loads the lookup table from the filesystem resource. + */ + protected void load() { + try { + long modTime = file.lastModified(); + if (modTime != lastModified) { + lastModified = modTime; + + InputStream in = file.getInputStream(); + try { + Properties properties = new Properties(); + properties.load(in); + for (Object name + : Collections.list(properties.propertyNames())) { + String string = name.toString(); + Integer index = + Integer.valueOf(properties.getProperty(string)); + stringToIndex.put(string, index); + indexToString.put(index, string); + } + } finally { + in.close(); + } + } + } catch (Exception e) { + throw new IllegalStateException("Unable to load lookup table", e); + } + } + + /** + * Saves the lookup table to the filesystem resource. + */ + protected void save() { + try { + OutputStream out = file.getOutputStream(); + try { + Properties properties = new Properties(); + for (Map.Entry entry + : stringToIndex.entrySet()) { + properties.setProperty( + entry.getKey(), entry.getValue().toString()); + } + properties.store(out, "string index"); + } finally { + out.close(); + } + lastModified = file.lastModified(); + } catch (Exception e) { + throw new IllegalStateException("Unable to store lookup table", e); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/FileSystemBLOBStore.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/FileSystemBLOBStore.java new file mode 100644 index 00000000000..8fdb9e4d01d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/FileSystemBLOBStore.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.core.persistence.util; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemPathUtil; +import org.apache.jackrabbit.core.fs.FileSystemResource; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * FileSystemBLOBStore is a ResourceBasedBLOBStore + * implementation that stores BLOB data in a FileSystem. + *

    + * Note that The DataStore should nowadays be used instead of the BLOBStore. + */ +public class FileSystemBLOBStore implements ResourceBasedBLOBStore { + + /** + * the file system where the BLOBs are stored + */ + private final FileSystem fs; + + /** + * Creates a new FileSystemBLOBStore instance. + * + * @param fs file system for storing the BLOB data + */ + public FileSystemBLOBStore(FileSystem fs) { + this.fs = fs; + } + + //------------------------------------------------------------< BLOBStore > + /** + * {@inheritDoc} + */ + public String createId(PropertyId id, int index) { + // the blobId is an absolute file system path + StringBuilder sb = new StringBuilder(); + sb.append(FileSystem.SEPARATOR_CHAR); + char[] chars = id.getParentId().toString().toCharArray(); + int cnt = 0; + for (int i = 0; i < chars.length; i++) { + if (chars[i] == '-') { + continue; + } + //if (cnt > 0 && cnt % 4 == 0) { + if (cnt == 2 || cnt == 4) { + sb.append(FileSystem.SEPARATOR_CHAR); + } + sb.append(chars[i]); + cnt++; + } + sb.append(FileSystem.SEPARATOR_CHAR); + sb.append(FileSystemPathUtil.escapeName(id.getName().toString())); + sb.append('.'); + sb.append(index); + sb.append(".bin"); + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + public InputStream get(String blobId) throws Exception { + return getResource(blobId).getInputStream(); + } + + /** + * {@inheritDoc} + */ + public void put(String blobId, InputStream in, long size) throws Exception { + // the blobId is an absolute file system path + FileSystemResource internalBlobFile = new FileSystemResource(fs, blobId); + internalBlobFile.makeParentDirs(); + OutputStream out = internalBlobFile.getOutputStream(); + try { + IOUtils.copy(in, out); + } finally { + out.close(); + } + } + + /** + * {@inheritDoc} + */ + public boolean remove(String blobId) throws Exception { + // the blobId is an absolute file system path + FileSystemResource res = new FileSystemResource(fs, blobId); + if (!res.exists()) { + return false; + } + // delete resource and prune empty parent folders + res.delete(true); + return true; + } + + //-----------------------------------------------< ResourceBasedBLOBStore > + /** + * {@inheritDoc} + */ + public FileSystemResource getResource(String blobId) + throws Exception { + // the blobId is an absolute file system path + return new FileSystemResource(fs, blobId); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/HashMapIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/HashMapIndex.java new file mode 100644 index 00000000000..9a4409be6a9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/HashMapIndex.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.core.persistence.util; + +import java.util.HashMap; + +import org.apache.jackrabbit.core.util.StringIndex; + +/** + * Implements a {@link StringIndex} that is based on a hashmap. Subclasses + * can override the protected {@link #load()} and {@link #save()} methods + * to implement persistent storage of the string index. + *

    + * This class is thread-safe. + */ +public class HashMapIndex implements StringIndex { + + /** + * holds the string-to-index lookups. + */ + protected final HashMap stringToIndex = + new HashMap(); + + /** + * holds the index-to-string lookups. + */ + protected final HashMap indexToString = + new HashMap(); + + /** + * Loads the lookup table. + */ + protected void load() { + } + + /** + * Saves the lookup table. + */ + protected void save() { + } + + /** + * {@inheritDoc} + * + * This implementation reloads the table from the resource if a lookup fails + * and if the resource was modified since. + */ + public synchronized int stringToIndex(String nsUri) { + Integer idx = stringToIndex.get(nsUri); + if (idx == null) { + load(); + idx = stringToIndex.get(nsUri); + } + if (idx == null) { + // Need to use only 24 bits, since that's what + // the BundleBinding class stores in bundles + idx = nsUri.hashCode() & 0x00ffffff; + while (indexToString.containsKey(idx)) { + idx = (idx + 1) & 0x00ffffff; + } + stringToIndex.put(nsUri, idx); + indexToString.put(idx, nsUri); + save(); + } + return idx; + } + + /** + * {@inheritDoc} + * + * This implementation reloads the table from the resource if a lookup fails + * and if the resource was modified since. + */ + public synchronized String indexToString(int i) { + Integer idx = Integer.valueOf(i); + String s = indexToString.get(idx); + if (s == null) { + load(); + s = indexToString.get(idx); + } + return s; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/NodeInfo.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/NodeInfo.java new file mode 100644 index 00000000000..ecc78fed4d5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/NodeInfo.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; + +/** + * Holds structural information about a node. Used by the consistency checker and garbage collector. + */ +public final class NodeInfo { + + /** + * The same node id in a NodeInfo graph typically occurs three times: as a the id of the current + * NodeInfo, as the parent to another NodeInfo, and as a child of another NodeInfo. In order to + * minimize the memory footprint use an NodeId object pool. + */ + private static final ConcurrentMap nodeIdPool = new ConcurrentHashMap(1000); + + /** + * The node id + */ + private final NodeId nodeId; + + /** + * The parent node id + */ + private final NodeId parentId; + + /** + * The child ids + */ + private List children; + + /** + * Map of reference property names of this node with their node id values + */ + private Map> references; + + /** + * Whether this node is referenceable or not + */ + private boolean isReferenceable; + + /** + * Whether this node has blob properties in data storage + */ + private boolean hasBlobsInDataStore; + + /** + * Create a new NodeInfo object from a bundle + * + * @param bundle the node bundle + */ + public NodeInfo(final NodePropBundle bundle) { + nodeId = getNodeId(bundle.getId()); + parentId = getNodeId(bundle.getParentId()); + + List childNodeEntries = bundle.getChildNodeEntries(); + if (!childNodeEntries.isEmpty()) { + children = new ArrayList(childNodeEntries.size()); + for (NodePropBundle.ChildNodeEntry entry : bundle.getChildNodeEntries()) { + children.add(getNodeId(entry.getId())); + } + } else { + children = Collections.emptyList(); + } + + for (NodePropBundle.PropertyEntry entry : bundle.getPropertyEntries()) { + if (entry.getType() == PropertyType.REFERENCE) { + if (references == null) { + references = new HashMap>(4); + } + List values = new ArrayList(entry.getValues().length); + for (InternalValue value : entry.getValues()) { + values.add(getNodeId(value.getNodeId())); + } + references.put(entry.getName(), values); + } + else if (entry.getType() == PropertyType.BINARY) { + for (InternalValue internalValue : entry.getValues()) { + if (internalValue.isInDataStore()) { + hasBlobsInDataStore = true; + break; + } + } + + } + } + + if (references == null) { + references = Collections.emptyMap(); + } + isReferenceable = bundle.isReferenceable(); + } + + /** + * @return the node id of this node + */ + public NodeId getId() { + return nodeId; + } + + /** + * @return the parent id of this node + */ + public NodeId getParentId() { + return parentId; + } + + /** + * @return the child ids of this node + */ + public List getChildren() { + return children; + } + + /** + * @return the reference properties along with their node id values of this node + */ + public Map> getReferences() { + return references; + } + + /** + * @return whether the node represented by this node info is referenceable + */ + public boolean isReferenceable() { + return isReferenceable; + } + + /** + * @return whether the node has blob properties that are inside the data storage + */ + public boolean hasBlobsInDataStore() { + return hasBlobsInDataStore; + } + + /** + * Simple pool implementation to minimize memory overhead from node id objects + * @param nodeId node id to cache + * @return the cached node id + */ + private static NodeId getNodeId(NodeId nodeId) { + if (nodeId == null) { + return null; + } + NodeId cached = nodeIdPool.get(nodeId); + if (cached == null) { + cached = nodeIdPool.putIfAbsent(nodeId, nodeId); + if (cached == null) { + cached = nodeId; + } + } + return cached; + } + + /** + * Clear the NodeId pool. + */ + public static void clearPool() { + nodeIdPool.clear(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/NodePropBundle.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/NodePropBundle.java new file mode 100644 index 00000000000..57d4a93312e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/NodePropBundle.java @@ -0,0 +1,762 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This Class provides a simple structure to hold the nodestate and related + * propertystate data. + */ +public class NodePropBundle { + + /** + * default logger + */ + private static Logger log = LoggerFactory.getLogger(NodePropBundle.class); + + /** + * the node id + */ + private final NodeId id; + + /** + * the parent node id + */ + private NodeId parentId; + + /** + * the nodetype name + */ + private Name nodeTypeName; + + /** + * the mixintype names + */ + private Set mixinTypeNames; + + /** + * the child node entries + */ + private LinkedList childNodeEntries = new LinkedList(); + + /** + * the properties + */ + private HashMap properties = new HashMap(); + + /** + * flag that indicates if this bundle is new + */ + private boolean isNew = true; + + /** + * flag that indicates if this bundle is referenceable + */ + private boolean isReferenceable; + + /** + * the mod count + */ + private short modCount; + + /** + * the size + */ + private long size; + + /** + * Shared set, consisting of the parent ids of this shareable node. This + * entry is null if this node is not shareable. + */ + private Set sharedSet; + + /** + * Creates a "new" bundle with the given id + * + * @param id the node id + */ + public NodePropBundle(NodeId id) { + this.id = id; + } + + /** + * Creates a bundle from the given state + * + * @param state the node state + */ + public NodePropBundle(NodeState state) { + this(state.getNodeId()); + update(state); + } + + /** + * Updates this bundle with values from the given state. + * @param state the node state + */ + public void update(NodeState state) { + if (!id.equals(state.getNodeId())) { + // sanity check + throw new IllegalArgumentException("Not allowed to update foreign state."); + } + parentId = state.getParentId(); + nodeTypeName = state.getNodeTypeName(); + mixinTypeNames = state.getMixinTypeNames(); + isReferenceable = state.hasPropertyName(NameConstants.JCR_UUID); + modCount = state.getModCount(); + List list = state.getChildNodeEntries(); + childNodeEntries.clear(); + for (org.apache.jackrabbit.core.state.ChildNodeEntry cne : list) { + addChildNodeEntry(cne.getName(), cne.getId()); + } + sharedSet = state.getSharedSet(); + } + + /** + * Creates a node state from the values of this bundle + * @param pMgr the persistence manager + * @return the new nodestate + */ + public NodeState createNodeState(PersistenceManager pMgr) { + NodeState state = pMgr.createNew(id); + state.setParentId(parentId); + state.setNodeTypeName(nodeTypeName); + state.setMixinTypeNames(mixinTypeNames); + state.setModCount(modCount); + for (ChildNodeEntry e : childNodeEntries) { + state.addChildNodeEntry(e.getName(), e.getId()); + } + state.setPropertyNames(properties.keySet()); + + // add fake property entries + state.addPropertyName(NameConstants.JCR_PRIMARYTYPE); + if (mixinTypeNames.size() > 0) { + state.addPropertyName(NameConstants.JCR_MIXINTYPES); + } + // uuid is special...only if 'referenceable' + if (isReferenceable) { + state.addPropertyName(NameConstants.JCR_UUID); + } + for (NodeId nodeId : sharedSet) { + state.addShare(nodeId); + } + return state; + } + + /** + * Checks if this bundle is new. + * @return true if this bundle is new; + * false otherwise. + */ + public boolean isNew() { + return isNew; + } + + /** + * Marks this bundle as 'not new'. + */ + public void markOld() { + isNew = false; + } + + /** + * Returns the node id of this bundle + * @return the node id of this bundle + */ + public NodeId getId() { + return id; + } + + /** + * Returns the parent id of this bundle + * @return the parent id of this bundle + */ + public NodeId getParentId() { + return parentId; + } + + /** + * Sets the parent id + * @param parentId the parent id + */ + public void setParentId(NodeId parentId) { + this.parentId = parentId; + } + + /** + * Returns the nodetype name of this bundle + * @return the nodetype name of this bundle + */ + public Name getNodeTypeName() { + return nodeTypeName; + } + + /** + * Sets the node type name + * @param nodeTypeName the nodetype name + */ + public void setNodeTypeName(Name nodeTypeName) { + this.nodeTypeName = nodeTypeName; + } + + /** + * Returns the mixin type names of this bundle. + * @return the mixin type names of this bundle. + */ + public Set getMixinTypeNames() { + return mixinTypeNames; + } + + /** + * Sets the mixin type names + * @param mixinTypeNames the mixin type names + */ + public void setMixinTypeNames(Set mixinTypeNames) { + this.mixinTypeNames = mixinTypeNames; + } + + /** + * Checks if this bundle is referenceable. + * @return true if this bundle is referenceable; + * false otherwise. + */ + public boolean isReferenceable() { + return isReferenceable; + } + + /** + * Sets the is referenceable flag on this bundle + * @param referenceable the ref. flag + */ + public void setReferenceable(boolean referenceable) { + isReferenceable = referenceable; + } + + /** + * Returns the mod count. + * + * @return the mod count. + */ + public short getModCount() { + return modCount; + } + + /** + * Sets the mod count + * + * @param modCount the mod count + */ + public void setModCount(short modCount) { + this.modCount = modCount; + } + + /** + * Returns the list of the child node entries. + * @return the list of the child node entries. + */ + public List getChildNodeEntries() { + return childNodeEntries; + } + + /** + * Adds a child node entry. + * @param name the name of the entry. + * @param id the id of the entry + */ + public void addChildNodeEntry(Name name, NodeId id) { + childNodeEntries.add(new ChildNodeEntry(name, id)); + } + + /** + * Adds a new property entry + * @param entry the enrty to add + */ + public void addProperty(PropertyEntry entry) { + assert !NameConstants.JCR_PRIMARYTYPE.equals(entry.getName()); + assert !NameConstants.JCR_UUID.equals(entry.getName()); + properties.put(entry.getName(), entry); + } + + /** + * Creates a property entry from the given state and adds it. + * + * @param state the property state + * @param blobStore BLOB store from where to delete previous property value + */ + public void addProperty(PropertyState state, BLOBStore blobStore) { + PropertyEntry old = + properties.put(state.getName(), new PropertyEntry(state)); + if (old != null) { + old.destroy(blobStore); + } + } + + /** + * Checks if this bundle has a property + * @param name the name of the property + * @return true if the property exists; + * false otherwise. + */ + public boolean hasProperty(Name name) { + return properties.containsKey(name); + } + + /** + * Returns a set of the property names. + * @return a set of the property names. + */ + public Set getPropertyNames() { + return properties.keySet(); + } + + /** + * Returns a collection of property entries. + * @return a collection of property entries. + */ + public Collection getPropertyEntries() { + return properties.values(); + } + + /** + * Returns the property entry with the given name. + * @param name the name of the property entry + * @return the desired property entry or null + */ + public PropertyEntry getPropertyEntry(Name name) { + return properties.get(name); + } + + /** + * Removes all property entries + * + * @param blobStore BLOB store from where to delete property values + */ + public void removeAllProperties(BLOBStore blobStore) { + for (Name name : new HashSet(properties.keySet())) { + removeProperty(name, blobStore); + } + } + + /** + * Removes the proprty with the given name from this bundle. + * + * @param name the name of the property + * @param blobStore BLOB store from where to delete the property value + */ + public void removeProperty(Name name, BLOBStore blobStore) { + PropertyEntry pe = properties.remove(name); + if (pe != null) { + pe.destroy(blobStore); + } + } + + /** + * Sets the shared set of this bundle. + * @return the shared set of this bundle. + */ + public Set getSharedSet() { + return sharedSet; + } + + /** + * Sets the shared set. + * @param sharedSet shared set + */ + public void setSharedSet(Set sharedSet) { + this.sharedSet = sharedSet; + } + + /** + * Returns the approx. size of this bundle. + * @return the approx. size of this bundle. + */ + public long getSize() { + // add some internal memory + // + shallow size: 64 + // + properties + // + shallow size: 40 + // + N * property entry: 218 + values + blobids + // + childnodes + // + shallow size: 24 + // + N * 24 + 160 + 44 + name.length + // + mixintypes names + // + shallow size: 16 + // + N * QNames + // + nodetype name: + // + shallow size: 24 + // + string: 20 + length + // + parentId: 160 + // + id: 160 + return 500 + size + 300 * (childNodeEntries.size() + properties.size() + 3); + } + + /** + * Sets the data size of this bundle + * @param size the data size + */ + public void setSize(long size) { + this.size = size; + } + + //--------------------------------------------------------------< Object > + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(id); + builder.append("("); + builder.append(parentId); + builder.append(","); + builder.append(nodeTypeName); + for (Name mixin : mixinTypeNames) { + builder.append(","); + builder.append(mixin); + } + if (isReferenceable) { + builder.append(",referenceable"); + } + builder.append(") = "); + if (!sharedSet.isEmpty()) { + builder.append(sharedSet); + builder.append(" "); + } + builder.append(properties.values()); + builder.append(" "); + builder.append(childNodeEntries); + return builder.toString(); + } + + @Override + public boolean equals(Object object) { + if (object instanceof NodePropBundle) { + NodePropBundle that = (NodePropBundle) object; + return equalNullSafe(id, that.id) + && equalNullSafe(parentId, that.parentId) + && equalNullSafe(nodeTypeName, that.nodeTypeName) + && equalNullSafe(mixinTypeNames, that.mixinTypeNames) + && isReferenceable == that.isReferenceable + && equalNullSafe(sharedSet, that.sharedSet) + && equalNullSafe(properties, that.properties) + && equalNullSafe(childNodeEntries, that.childNodeEntries); + } + return false; + } + + private static boolean equalNullSafe(Object a, Object b) { + if (a == null || b == null) { + return a == b; + } + return a.equals(b); + } + + //-----------------------------------------------------< ChildNodeEntry >--- + + /** + * Helper class for a child node entry + */ + public static class ChildNodeEntry { + + /** + * the name of the entry + */ + private final Name name; + + /** + * the id of the entry + */ + private final NodeId id; + + /** + * Creates a new entry with the given name and id + * @param name the name + * @param id the id + */ + public ChildNodeEntry(Name name, NodeId id) { + this.name = name; + this.id = id; + } + + /** + * Returns the name. + * @return the name. + */ + public Name getName() { + return name; + } + + /** + * Returns the id. + * @return the id. + */ + public NodeId getId() { + return id; + } + + //----------------------------------------------------------< Object > + + public String toString() { + return name + " => " + id; + } + + public boolean equals(Object object) { + if (object instanceof ChildNodeEntry) { + ChildNodeEntry that = (ChildNodeEntry) object; + return name.equals(that.name) && id.equals(that.id); + } else { + return false; + } + } + + } + + //------------------------------------------------------< PropertyEntry >--- + + /** + * Helper class for a property enrty + */ + public static class PropertyEntry { + + /** + * The property id + */ + private final PropertyId id; + + /** + * the internal value + */ + private InternalValue[] values; + + /** + * the property type + */ + private int type; + + /** + * the multivalued flag + */ + private boolean multiValued; + + /** + * the blob ids + */ + private String[] blobIds; + + /** + * the mod count + */ + private short modCount; + + /** + * Creates a new property entry with the given id. + * @param id the id + */ + public PropertyEntry(PropertyId id) { + this.id = id; + } + + /** + * Creates a new property entry and initialized it with values from + * the given property state. + * @param state the source property state. + */ + public PropertyEntry(PropertyState state) { + this((PropertyId) state.getId()); + values = state.getValues(); + type = state.getType(); + multiValued = state.isMultiValued(); + if (!multiValued && values.length != 1) { + throw new IllegalArgumentException("Non-multi-valued property with values.length " + values.length); + } + modCount = state.getModCount(); + if (type == PropertyType.BINARY) { + blobIds = new String[values.length]; + } + } + + /** + * Returns the property id. + * @return the property id. + */ + public PropertyId getId() { + return id; + } + + /** + * Returns the property name + * @return the property name + */ + public Name getName() { + return id.getName(); + } + + /** + * Retruns the internal values + * @return the internal values + */ + public InternalValue[] getValues() { + return values; + } + + /** + * Sets the internal values. + * @param values the internal values. + */ + public void setValues(InternalValue[] values) { + this.values = values; + } + + /** + * Returns the type. + * @return the type. + */ + public int getType() { + return type; + } + + /** + * Sets the type + * @param type the type + */ + public void setType(int type) { + this.type = type; + } + + /** + * Returns the multivalued flag. + * @return the multivalued flag. + */ + public boolean isMultiValued() { + return multiValued; + } + + /** + * Sets the multivalued flag. + * @param multiValued the multivalued flag + */ + public void setMultiValued(boolean multiValued) { + this.multiValued = multiValued; + } + + /** + * Returns the nth blob id. + * @param n the index of the blob id + * @return the blob id + */ + public String getBlobId(int n) { + return blobIds[n]; + } + + /** + * Sets the blob ids + * @param blobIds the blobids + */ + public void setBlobIds(String[] blobIds) { + this.blobIds = blobIds; + } + + /** + * Sets the nth blobid + * @param blobId the blob id + * @param n the index of the blob id + */ + public void setBlobId(String blobId, int n) { + blobIds[n] = blobId; + } + + /** + * Returns the mod count. + * @return the mod count. + */ + public short getModCount() { + return modCount; + } + + /** + * Sets the mod count + * @param modCount the mod count + */ + public void setModCount(short modCount) { + this.modCount = modCount; + } + + /** + * Destroys this property state and deletes temporary blob file values. + * @param blobStore the blobstore that will destroy the blobs + */ + private void destroy(BLOBStore blobStore) { + // delete blobs if needed + for (int i = 0; blobIds != null && i < blobIds.length; i++) { + if (blobIds[i] != null) { + try { + blobStore.remove(blobIds[i]); + log.debug("removed blob {}", blobIds[i]); + } catch (Exception e) { + log.error("Ignoring error while removing blob " + blobIds[i], e); + } + } + } + } + + //----------------------------------------------------------< Object > + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(id); + builder.append("("); + builder.append(PropertyType.nameFromValue(type)); + if (multiValued) { + builder.append(",multiple"); + } + builder.append(") = "); + builder.append(Arrays.toString(values)); + return builder.toString(); + } + + public boolean equals(Object object) { + if (object instanceof PropertyEntry) { + PropertyEntry that = (PropertyEntry) object; + return id.equals(that.id) + && type == that.type + && multiValued == that.multiValued + && Arrays.equals(values, that.values); + } else { + return false; + } + } + + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/ResourceBasedBLOBStore.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/ResourceBasedBLOBStore.java new file mode 100644 index 00000000000..af322181850 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/ResourceBasedBLOBStore.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.core.persistence.util; + +import org.apache.jackrabbit.core.fs.FileSystemResource; + +/** + * ResourceBasedBLOBStore extends the BLOBStore + * interface with the method {@link #getResource(String)} + *

    + * Note that The DataStore should nowadays be used instead of the BLOBStore. + */ +public interface ResourceBasedBLOBStore extends BLOBStore { + /** + * Retrieves the BLOB data with the specified id as a permanent resource. + * + * @param blobId identifier of the BLOB data as returned by + * {@link #createId(org.apache.jackrabbit.core.id.PropertyId , int)} + * @return a resource representing the BLOB data + * @throws Exception if an error occurred + */ + FileSystemResource getResource(String blobId) throws Exception; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/Serializer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/Serializer.java new file mode 100644 index 00000000000..7bb62aaa8b4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/util/Serializer.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.core.persistence.util; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; + +import javax.jcr.PropertyType; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Serializer is a utility class that provides static methods + * for serializing & deserializing ItemState and + * NodeReferences objects using a simple binary serialization + * format. + */ +public final class Serializer { + + private static final byte[] NULL_UUID_PLACEHOLDER_BYTES = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + + /** + * encoding used for serializing String values + */ + private static final Charset ENCODING = StandardCharsets.UTF_8; + + /** + * Serializes the specified NodeState object to the given + * binary stream. + * + * @param state state to serialize + * @param stream the stream where the state should be + * serialized to + * @throws Exception if an error occurs during the serialization + * @see #deserialize(NodeState, InputStream) + */ + public static void serialize(NodeState state, OutputStream stream) + throws Exception { + DataOutputStream out = new DataOutputStream(stream); + + // primaryType + out.writeUTF(state.getNodeTypeName().toString()); + // parentUUID + if (state.getParentId() == null) { + out.write(NULL_UUID_PLACEHOLDER_BYTES); + } else { + out.write(state.getParentId().getRawBytes()); + } + // definitionId + out.writeUTF(""); + // mixin types + Collection c = state.getMixinTypeNames(); + out.writeInt(c.size()); // count + for (Iterator iter = c.iterator(); iter.hasNext();) { + out.writeUTF(iter.next().toString()); // name + } + // modCount + out.writeShort(state.getModCount()); + // properties (names) + c = state.getPropertyNames(); + out.writeInt(c.size()); // count + for (Iterator iter = c.iterator(); iter.hasNext();) { + Name propName = iter.next(); + out.writeUTF(propName.toString()); // name + } + // child nodes (list of name/uuid pairs) + Collection collChildren = state.getChildNodeEntries(); + out.writeInt(collChildren.size()); // count + for (Iterator iter = collChildren.iterator(); iter.hasNext();) { + ChildNodeEntry entry = iter.next(); + out.writeUTF(entry.getName().toString()); // name + out.write(entry.getId().getRawBytes()); // uuid + } + } + + /** + * Deserializes a NodeState object from the given binary + * stream. + * + * @param state state to deserialize + * @param stream the stream where the state should be deserialized from + * @throws Exception if an error occurs during the deserialization + * @see #serialize(NodeState, OutputStream) + */ + public static void deserialize(NodeState state, InputStream stream) + throws Exception { + DataInputStream in = new DataInputStream(stream); + + // primaryType + String s = in.readUTF(); + state.setNodeTypeName(NameFactoryImpl.getInstance().create(s)); + // parentUUID (may be null) + byte[] uuidBytes = new byte[NodeId.UUID_BYTE_LENGTH]; + in.readFully(uuidBytes); + if (!Arrays.equals(uuidBytes, NULL_UUID_PLACEHOLDER_BYTES)) { + state.setParentId(new NodeId(uuidBytes)); + } + // definitionId + in.readUTF(); + // mixin types + int count = in.readInt(); // count + Set set = new HashSet(count); + for (int i = 0; i < count; i++) { + set.add(NameFactoryImpl.getInstance().create(in.readUTF())); + } + if (set.size() > 0) { + state.setMixinTypeNames(set); + } + // modCount + short modCount = in.readShort(); + state.setModCount(modCount); + // properties (names) + count = in.readInt(); // count + for (int i = 0; i < count; i++) { + state.addPropertyName(NameFactoryImpl.getInstance().create(in.readUTF())); // name + } + // child nodes (list of name/uuid pairs) + count = in.readInt(); // count + for (int i = 0; i < count; i++) { + Name name = NameFactoryImpl.getInstance().create(in.readUTF()); // name + // uuid + in.readFully(uuidBytes); + state.addChildNodeEntry(name, new NodeId(uuidBytes)); + } + } + + /** + * Serializes the specified PropertyState object to the given + * binary stream. Binary values are stored in the specified + * BLOBStore. + * + * @param state state to serialize + * @param stream the stream where the state should be + * serialized to + * @param blobStore handler for BLOB data + * @throws Exception if an error occurs during the serialization + * @see #deserialize(PropertyState, InputStream,BLOBStore) + */ + public static void serialize(PropertyState state, + OutputStream stream, + BLOBStore blobStore) + throws Exception { + DataOutputStream out = new DataOutputStream(stream); + + // type + out.writeInt(state.getType()); + // multiValued + out.writeBoolean(state.isMultiValued()); + // definitionId + out.writeUTF(""); + // modCount + out.writeShort(state.getModCount()); + // values + InternalValue[] values = state.getValues(); + out.writeInt(values.length); // count + for (int i = 0; i < values.length; i++) { + InternalValue val = values[i]; + if (state.getType() == PropertyType.BINARY) { + // special handling required for binary value: + // put binary value in BLOB store + InputStream in = val.getStream(); + String blobId = blobStore.createId(state.getPropertyId(), i); + try { + blobStore.put(blobId, in, val.getLength()); + } finally { + IOUtils.closeQuietly(in); + } + // store id of BLOB as property value + out.writeUTF(blobId); // value + // replace value instance with value backed by resource + // in BLOB store and discard old value instance (e.g. temp file) + if (blobStore instanceof ResourceBasedBLOBStore) { + // optimization: if the BLOB store is resource-based + // retrieve the resource directly rather than having + // to read the BLOB from an input stream + FileSystemResource fsRes = + ((ResourceBasedBLOBStore) blobStore).getResource(blobId); + values[i] = InternalValue.create(fsRes); + } else { + in = blobStore.get(blobId); + try { + values[i] = InternalValue.create(in); + } finally { + IOUtils.closeQuietly(in); + } + } + val.discard(); + } else { + /** + * because writeUTF(String) has a size limit of 65k, + * Strings are serialized as + */ + //out.writeUTF(val.toString()); // value + byte[] bytes = val.toString().getBytes(ENCODING); + out.writeInt(bytes.length); // lenght of byte[] + out.write(bytes); // byte[] + } + } + } + + /** + * Deserializes a PropertyState object from the given binary + * stream. Binary values are retrieved from the specified + * BLOBStore. + * + * @param state state to deserialize + * @param stream the stream where the state should be + * deserialized from + * @param blobStore handler for BLOB data + * @throws Exception if an error occurs during the deserialization + * @see #serialize(PropertyState, OutputStream, BLOBStore) + */ + public static void deserialize(PropertyState state, + InputStream stream, + BLOBStore blobStore) + throws Exception { + DataInputStream in = new DataInputStream(stream); + + // type + int type = in.readInt(); + state.setType(type); + // multiValued + boolean multiValued = in.readBoolean(); + state.setMultiValued(multiValued); + // definitionId + in.readUTF(); + // modCount + short modCount = in.readShort(); + state.setModCount(modCount); + // values + int count = in.readInt(); // count + InternalValue[] values = new InternalValue[count]; + for (int i = 0; i < count; i++) { + InternalValue val; + if (type == PropertyType.BINARY) { + String s = in.readUTF(); // value (i.e. blobId) + // special handling required for binary value: + // the value stores the id of the BLOB data + // in the BLOB store + if (blobStore instanceof ResourceBasedBLOBStore) { + // optimization: if the BLOB store is resource-based + // retrieve the resource directly rather than having + // to read the BLOB from an input stream + FileSystemResource fsRes = + ((ResourceBasedBLOBStore) blobStore).getResource(s); + val = InternalValue.create(fsRes); + } else { + InputStream is = blobStore.get(s); + try { + val = InternalValue.create(is); + } finally { + try { + is.close(); + } catch (IOException e) { + // ignore + } + } + } + } else { + /** + * because writeUTF(String) has a size limit of 65k, + * Strings are serialized as + */ + //s = in.readUTF(); // value + int len = in.readInt(); // lenght of byte[] + byte[] bytes = new byte[len]; + in.readFully(bytes); // byte[] + String s = new String(bytes, ENCODING); + val = InternalValue.valueOf(s, type); + } + values[i] = val; + } + state.setValues(values); + } + + /** + * Serializes the specified NodeReferences object to the given + * binary stream. + * + * @param refs object to serialize + * @param stream the stream where the object should be serialized to + * @throws Exception if an error occurs during the serialization + * @see #deserialize(NodeReferences, InputStream) + */ + public static void serialize(NodeReferences refs, OutputStream stream) + throws Exception { + DataOutputStream out = new DataOutputStream(stream); + + // references + Collection c = refs.getReferences(); + out.writeInt(c.size()); // count + for (Iterator iter = c.iterator(); iter.hasNext();) { + PropertyId propId = iter.next(); + out.writeUTF(propId.toString()); // propertyId + } + } + + /** + * Deserializes a NodeReferences object from the given + * binary stream. + * + * @param refs object to deserialize + * @param stream the stream where the object should be deserialized from + * @throws Exception if an error occurs during the deserialization + * @see #serialize(NodeReferences, OutputStream) + */ + public static void deserialize(NodeReferences refs, InputStream stream) + throws Exception { + DataInputStream in = new DataInputStream(stream); + + refs.clearAllReferences(); + + // references + int count = in.readInt(); // count + for (int i = 0; i < count; i++) { + refs.addReference(PropertyId.valueOf(in.readUTF())); // propertyId + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/xml/XMLPersistenceManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/xml/XMLPersistenceManager.java new file mode 100644 index 00000000000..9a777a8f27a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/xml/XMLPersistenceManager.java @@ -0,0 +1,905 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.xml; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.fs.BasedFileSystem; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.apache.jackrabbit.core.persistence.AbstractPersistenceManager; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.persistence.PMContext; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore; +import org.apache.jackrabbit.core.persistence.util.ResourceBasedBLOBStore; +import org.apache.jackrabbit.core.util.DOMWalker; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * XMLPersistenceManager is a FileSystem-based + * PersistenceManager that persists ItemState + * and NodeReferences objects in XML format. + * + * @deprecated Please migrate to a bundle persistence manager + * (JCR-2802) + */ +@Deprecated +public class XMLPersistenceManager extends AbstractPersistenceManager { + + private static Logger log = LoggerFactory.getLogger(XMLPersistenceManager.class); + + /** + * hexdigits for toString + */ + private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray(); + + /** + * The default encoding used in serialization + */ + public static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8; + + /** + * The XML elements and attributes used in serialization + */ + private static final String NODE_ELEMENT = "node"; + private static final String UUID_ATTRIBUTE = "uuid"; + private static final String NODETYPE_ATTRIBUTE = "nodeType"; + private static final String PARENTUUID_ATTRIBUTE = "parentUUID"; + private static final String MODCOUNT_ATTRIBUTE = "modCount"; + + private static final String MIXINTYPES_ELEMENT = "mixinTypes"; + private static final String MIXINTYPE_ELEMENT = "mixinType"; + + private static final String PROPERTIES_ELEMENT = "properties"; + private static final String PROPERTY_ELEMENT = "property"; + private static final String NAME_ATTRIBUTE = "name"; + private static final String TYPE_ATTRIBUTE = "type"; + private static final String MULTIVALUED_ATTRIBUTE = "multiValued"; + + private static final String VALUES_ELEMENT = "values"; + private static final String VALUE_ELEMENT = "value"; + + private static final String NODES_ELEMENT = "nodes"; + + private static final String NODEREFERENCES_ELEMENT = "references"; + private static final String TARGETID_ATTRIBUTE = "targetId"; + private static final String NODEREFERENCE_ELEMENT = "reference"; + private static final String PROPERTYID_ATTRIBUTE = "propertyId"; + + private static final String NODEFILENAME = ".node.xml"; + + private static final String NODEREFSFILENAME = ".references.xml"; + + private boolean initialized; + + // file system where the item state is stored + private FileSystem itemStateFS; + // file system where BLOB data is stored + private FileSystem blobFS; + // BLOBStore that manages BLOB data in the file system + private BLOBStore blobStore; + + /** + * Template for the subdirectory path for the files associated with + * a single node. The template is processed by replacing each + * "x" with the next hex digit in the UUID string. + * All other characters in the template are used as-is. + */ + private String nodePathTemplate = "xxxx/xxxx/xxxxxxxxxxxxxxxxxxxxxxxx"; + + private final NameFactory factory; + + /** + * Creates a new XMLPersistenceManager instance. + */ + public XMLPersistenceManager() { + initialized = false; + factory = NameFactoryImpl.getInstance(); + } + + /** + * Returns the node path template. + * + * @return node path template + */ + public String getNodePathTemplate() { + return nodePathTemplate; + } + + /** + * Sets the node path template. + * + * @param template node path template + */ + public void setNodePathTemplate(String template) { + nodePathTemplate = template; + } + + /** + * Builds the path of the node folder for the given node identifier + * based on the configured node path template. + * + * @param id node identifier + * @return node folder path + */ + private String buildNodeFolderPath(NodeId id) { + StringBuilder sb = new StringBuilder(); + char[] chars = id.toString().toCharArray(); + int cnt = 0; + for (int i = 0; i < nodePathTemplate.length(); i++) { + char ch = nodePathTemplate.charAt(i); + if (ch == 'x' && cnt < chars.length) { + ch = chars[cnt++]; + if (ch == '-') { + ch = chars[cnt++]; + } + } + sb.append(ch); + } + return sb.toString(); + } + + private String buildPropFilePath(PropertyId id) { + String fileName; + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(id.getName().getNamespaceURI().getBytes()); + md5.update(id.getName().getLocalName().getBytes()); + byte[] bytes = md5.digest(); + char[] chars = new char[32]; + for (int i = 0, j = 0; i < 16; i++) { + chars[j++] = HEXDIGITS[(bytes[i] >> 4) & 0x0f]; + chars[j++] = HEXDIGITS[bytes[i] & 0x0f]; + } + fileName = new String(chars) + ".xml"; + } catch (NoSuchAlgorithmException nsae) { + // should never get here as MD5 should always be available in the JRE + String msg = "MD5 not available"; + log.error(msg, nsae); + throw new InternalError(msg + nsae); + } + return buildNodeFolderPath(id.getParentId()) + "/" + fileName; + } + + private String buildNodeFilePath(NodeId id) { + return buildNodeFolderPath(id) + "/" + NODEFILENAME; + } + + private String buildNodeReferencesFilePath(NodeId id) { + return buildNodeFolderPath(id) + "/" + NODEREFSFILENAME; + } + + private void readState(DOMWalker walker, NodeState state) + throws ItemStateException { + // first do some paranoid sanity checks + if (!walker.getName().equals(NODE_ELEMENT)) { + String msg = "invalid serialization format (unexpected element: " + + walker.getName() + ")"; + log.debug(msg); + throw new ItemStateException(msg); + } + // check uuid + if (!state.getNodeId().toString().equals(walker.getAttribute(UUID_ATTRIBUTE))) { + String msg = "invalid serialized state: uuid mismatch"; + log.debug(msg); + throw new ItemStateException(msg); + } + // check nodetype + String ntName = walker.getAttribute(NODETYPE_ATTRIBUTE); + if (!factory.create(ntName).equals(state.getNodeTypeName())) { + String msg = "invalid serialized state: nodetype mismatch"; + log.debug(msg); + throw new ItemStateException(msg); + } + + // now we're ready to read state + + // primary parent + String parentUUID = walker.getAttribute(PARENTUUID_ATTRIBUTE); + if (parentUUID.length() > 0) { + state.setParentId(NodeId.valueOf(parentUUID)); + } + + // modification count + String modCount = walker.getAttribute(MODCOUNT_ATTRIBUTE); + state.setModCount(Short.parseShort(modCount)); + + // mixin types + if (walker.enterElement(MIXINTYPES_ELEMENT)) { + Set mixins = new HashSet(); + while (walker.iterateElements(MIXINTYPE_ELEMENT)) { + mixins.add(factory.create(walker.getAttribute(NAME_ATTRIBUTE))); + } + if (mixins.size() > 0) { + state.setMixinTypeNames(mixins); + } + walker.leaveElement(); + } + + // property entries + if (walker.enterElement(PROPERTIES_ELEMENT)) { + while (walker.iterateElements(PROPERTY_ELEMENT)) { + String propName = walker.getAttribute(NAME_ATTRIBUTE); + // @todo deserialize type and values + state.addPropertyName(factory.create(propName)); + } + walker.leaveElement(); + } + + // child node entries + if (walker.enterElement(NODES_ELEMENT)) { + while (walker.iterateElements(NODE_ELEMENT)) { + String childName = walker.getAttribute(NAME_ATTRIBUTE); + String childUUID = walker.getAttribute(UUID_ATTRIBUTE); + state.addChildNodeEntry(factory.create(childName), NodeId.valueOf(childUUID)); + } + walker.leaveElement(); + } + } + + private void readState(DOMWalker walker, PropertyState state) + throws ItemStateException { + // first do some paranoid sanity checks + if (!walker.getName().equals(PROPERTY_ELEMENT)) { + String msg = "invalid serialization format (unexpected element: " + + walker.getName() + ")"; + log.debug(msg); + throw new ItemStateException(msg); + } + // check name + if (!state.getName().equals(factory.create(walker.getAttribute(NAME_ATTRIBUTE)))) { + String msg = "invalid serialized state: name mismatch"; + log.debug(msg); + throw new ItemStateException(msg); + } + // check parentUUID + NodeId parentId = NodeId.valueOf(walker.getAttribute(PARENTUUID_ATTRIBUTE)); + if (!parentId.equals(state.getParentId())) { + String msg = "invalid serialized state: parentUUID mismatch"; + log.debug(msg); + throw new ItemStateException(msg); + } + + // now we're ready to read state + + // type + String typeName = walker.getAttribute(TYPE_ATTRIBUTE); + int type; + try { + type = PropertyType.valueFromName(typeName); + } catch (IllegalArgumentException iae) { + // should never be getting here + throw new ItemStateException("unexpected property-type: " + typeName, iae); + } + state.setType(type); + + // multiValued + String multiValued = walker.getAttribute(MULTIVALUED_ATTRIBUTE); + state.setMultiValued(Boolean.parseBoolean(multiValued)); + + // modification count + String modCount = walker.getAttribute(MODCOUNT_ATTRIBUTE); + state.setModCount(Short.parseShort(modCount)); + + // values + ArrayList values = new ArrayList(); + if (walker.enterElement(VALUES_ELEMENT)) { + while (walker.iterateElements(VALUE_ELEMENT)) { + // read serialized value + String content = walker.getContent(); + if (PropertyType.STRING == type) { + // STRING value can be empty; ignore length + values.add(InternalValue.valueOf(content, type)); + } else if (content.length() > 0) { + // non-empty non-STRING value + if (type == PropertyType.BINARY) { + try { + // special handling required for binary value: + // the value stores the id of the BLOB data + // in the BLOB store + if (blobStore instanceof ResourceBasedBLOBStore) { + // optimization: if the BLOB store is resource-based + // retrieve the resource directly rather than having + // to read the BLOB from an input stream + FileSystemResource fsRes = + ((ResourceBasedBLOBStore) blobStore).getResource(content); + values.add(InternalValue.create(fsRes)); + } else { + InputStream in = blobStore.get(content); + try { + values.add(InternalValue.create(in)); + } finally { + IOUtils.closeQuietly(in); + } + } + } catch (Exception e) { + String msg = "error while reading serialized binary value"; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } else { + // non-empty non-STRING non-BINARY value + values.add(InternalValue.valueOf(content, type)); + } + } else { + // empty non-STRING value + log.warn(state.getPropertyId() + ": ignoring empty value of type " + + PropertyType.nameFromValue(type)); + } + } + walker.leaveElement(); + } + state.setValues((InternalValue[]) + values.toArray(new InternalValue[values.size()])); + } + + private void readState(DOMWalker walker, NodeReferences refs) + throws ItemStateException { + // first do some paranoid sanity checks + if (!walker.getName().equals(NODEREFERENCES_ELEMENT)) { + String msg = "invalid serialization format (unexpected element: " + walker.getName() + ")"; + log.debug(msg); + throw new ItemStateException(msg); + } + // check targetId + if (!refs.getTargetId().equals(NodeId.valueOf(walker.getAttribute(TARGETID_ATTRIBUTE)))) { + String msg = "invalid serialized state: targetId mismatch"; + log.debug(msg); + throw new ItemStateException(msg); + } + + // now we're ready to read the references data + + // property id's + refs.clearAllReferences(); + while (walker.iterateElements(NODEREFERENCE_ELEMENT)) { + refs.addReference(PropertyId.valueOf(walker.getAttribute(PROPERTYID_ATTRIBUTE))); + } + } + + //---------------------------------------------------< PersistenceManager > + /** + * {@inheritDoc} + */ + public void init(PMContext context) throws Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + + itemStateFS = new BasedFileSystem(context.getFileSystem(), "/data"); + + /** + * store BLOB data in local file system in a sub directory + * of the workspace home directory + */ + LocalFileSystem blobFS = new LocalFileSystem(); + blobFS.setRoot(new File(context.getHomeDir(), "blobs")); + blobFS.init(); + this.blobFS = blobFS; + blobStore = new FileSystemBLOBStore(blobFS); + + initialized = true; + } + + /** + * {@inheritDoc} + */ + public synchronized void close() throws Exception { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + // close BLOB file system + blobFS.close(); + blobFS = null; + blobStore = null; + /** + * there's no need close the item state store because it + * is based in the workspace's file system which is + * closed by the repository + */ + } finally { + initialized = false; + } + } + + /** + * {@inheritDoc} + */ + public synchronized NodeState load(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + Exception e = null; + String nodeFilePath = buildNodeFilePath(id); + + try { + if (!itemStateFS.isFile(nodeFilePath)) { + throw new NoSuchItemStateException(id.toString()); + } + InputStream in = itemStateFS.getInputStream(nodeFilePath); + + try { + DOMWalker walker = new DOMWalker(in); + String ntName = walker.getAttribute(NODETYPE_ATTRIBUTE); + + NodeState state = createNew(id); + state.setNodeTypeName(factory.create(ntName)); + readState(walker, state); + return state; + } finally { + in.close(); + } + } catch (IOException ioe) { + e = ioe; + // fall through + } catch (FileSystemException fse) { + e = fse; + // fall through + } + String msg = "failed to read node state: " + id; + log.debug(msg); + throw new ItemStateException(msg, e); + } + + /** + * {@inheritDoc} + */ + public synchronized PropertyState load(PropertyId id) + throws NoSuchItemStateException, ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + Exception e = null; + String propFilePath = buildPropFilePath(id); + + try { + if (!itemStateFS.isFile(propFilePath)) { + throw new NoSuchItemStateException(id.toString()); + } + InputStream in = itemStateFS.getInputStream(propFilePath); + try { + DOMWalker walker = new DOMWalker(in); + PropertyState state = createNew(id); + readState(walker, state); + return state; + } finally { + in.close(); + } + } catch (IOException ioe) { + e = ioe; + // fall through + } catch (FileSystemException fse) { + e = fse; + // fall through + } + String msg = "failed to read property state: " + id.toString(); + log.debug(msg); + throw new ItemStateException(msg, e); + } + + /** + * {@inheritDoc} + */ + protected void store(NodeState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + NodeId id = state.getNodeId(); + String nodeFilePath = buildNodeFilePath(id); + FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath); + try { + nodeFile.makeParentDirs(); + OutputStream os = nodeFile.getOutputStream(); + Writer writer = null; + try { + writer = new BufferedWriter(new OutputStreamWriter(os, DEFAULT_ENCODING)); + + String parentId = (state.getParentId() == null) ? "" : state.getParentId().toString(); + String encodedNodeType = Text.encodeIllegalXMLCharacters(state.getNodeTypeName().toString()); + writer.write("\n"); + writer.write("<" + NODE_ELEMENT + " " + + UUID_ATTRIBUTE + "=\"" + id + "\" " + + PARENTUUID_ATTRIBUTE + "=\"" + parentId + "\" " + + MODCOUNT_ATTRIBUTE + "=\"" + state.getModCount() + "\" " + + NODETYPE_ATTRIBUTE + "=\"" + encodedNodeType + "\">\n"); + + // mixin types + writer.write("\t<" + MIXINTYPES_ELEMENT + ">\n"); + for (Name mixin : state.getMixinTypeNames()) { + writer.write("\t\t<" + MIXINTYPE_ELEMENT + " " + + NAME_ATTRIBUTE + "=\"" + Text.encodeIllegalXMLCharacters(mixin.toString()) + "\"/>\n"); + } + writer.write("\t\n"); + + // properties + writer.write("\t<" + PROPERTIES_ELEMENT + ">\n"); + for (Name propName : state.getPropertyNames()) { + writer.write("\t\t<" + PROPERTY_ELEMENT + " " + + NAME_ATTRIBUTE + "=\"" + Text.encodeIllegalXMLCharacters(propName.toString()) + "\">\n"); + // @todo serialize type, definition id and values + writer.write("\t\t\n"); + } + writer.write("\t\n"); + + // child nodes + writer.write("\t<" + NODES_ELEMENT + ">\n"); + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + writer.write("\t\t<" + NODE_ELEMENT + " " + + NAME_ATTRIBUTE + "=\"" + Text.encodeIllegalXMLCharacters(entry.getName().toString()) + "\" " + + UUID_ATTRIBUTE + "=\"" + entry.getId() + "\">\n"); + writer.write("\t\t\n"); + } + writer.write("\t\n"); + + writer.write("\n"); + } finally { + writer.close(); + } + } catch (Exception e) { + String msg = "failed to write node state: " + id; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void store(PropertyState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String propFilePath = buildPropFilePath(state.getPropertyId()); + FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath); + try { + propFile.makeParentDirs(); + OutputStream os = propFile.getOutputStream(); + // write property state to xml file + Writer writer = null; + try { + writer = new BufferedWriter(new OutputStreamWriter(os, DEFAULT_ENCODING)); + + String typeName; + int type = state.getType(); + try { + typeName = PropertyType.nameFromValue(type); + } catch (IllegalArgumentException iae) { + // should never be getting here + throw new ItemStateException("unexpected property-type ordinal: " + type, iae); + } + + writer.write("\n"); + writer.write("<" + PROPERTY_ELEMENT + " " + + NAME_ATTRIBUTE + "=\"" + Text.encodeIllegalXMLCharacters(state.getName().toString()) + "\" " + + PARENTUUID_ATTRIBUTE + "=\"" + state.getParentId() + "\" " + + MULTIVALUED_ATTRIBUTE + "=\"" + Boolean.toString(state.isMultiValued()) + "\" " + + MODCOUNT_ATTRIBUTE + "=\"" + state.getModCount() + "\" " + + TYPE_ATTRIBUTE + "=\"" + typeName + "\">\n"); + // values + writer.write("\t<" + VALUES_ELEMENT + ">\n"); + InternalValue[] values = state.getValues(); + if (values != null) { + for (int i = 0; i < values.length; i++) { + writer.write("\t\t<" + VALUE_ELEMENT + ">"); + InternalValue val = values[i]; + if (val != null) { + if (type == PropertyType.BINARY) { + // special handling required for binary value: + // put binary value in BLOB store + InputStream in = val.getStream(); + String blobId = blobStore.createId(state.getPropertyId(), i); + try { + blobStore.put(blobId, in, val.getLength()); + } finally { + IOUtils.closeQuietly(in); + } + // store id of BLOB as property value + writer.write(blobId); + // replace value instance with value backed by resource + // in BLOB store and discard old value instance (e.g. temp file) + if (blobStore instanceof ResourceBasedBLOBStore) { + // optimization: if the BLOB store is resource-based + // retrieve the resource directly rather than having + // to read the BLOB from an input stream + FileSystemResource fsRes = + ((ResourceBasedBLOBStore) blobStore).getResource(blobId); + values[i] = InternalValue.create(fsRes); + } else { + in = blobStore.get(blobId); + try { + values[i] = InternalValue.create(in); + } finally { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } + val.discard(); + } else { + writer.write(Text.encodeIllegalXMLCharacters(val.toString())); + } + } + writer.write("\n"); + } + } + writer.write("\t\n"); + writer.write("\n"); + } finally { + writer.close(); + } + } catch (Exception e) { + String msg = "failed to store property state: " + state.getParentId() + "/" + state.getName(); + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void destroy(NodeState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + NodeId id = state.getNodeId(); + String nodeFilePath = buildNodeFilePath(id); + FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath); + try { + if (nodeFile.exists()) { + // delete resource and prune empty parent folders + nodeFile.delete(true); + } + } catch (FileSystemException fse) { + String msg = "failed to delete node state: " + id; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + protected void destroy(PropertyState state) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + // delete binary values (stored as files) + InternalValue[] values = state.getValues(); + if (values != null) { + for (int i = 0; i < values.length; i++) { + InternalValue val = values[i]; + if (val != null) { + val.deleteBinaryResource(); + } + } + } + // delete property file + String propFilePath = buildPropFilePath(state.getPropertyId()); + FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath); + try { + if (propFile.exists()) { + // delete resource and prune empty parent folders + propFile.delete(true); + } + } catch (FileSystemException fse) { + String msg = "failed to delete property state: " + state.getParentId() + "/" + state.getName(); + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + public synchronized NodeReferences loadReferencesTo(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + Exception e = null; + String refsFilePath = buildNodeReferencesFilePath(id); + try { + if (!itemStateFS.isFile(refsFilePath)) { + throw new NoSuchItemStateException(id.toString()); + } + + InputStream in = itemStateFS.getInputStream(refsFilePath); + + try { + DOMWalker walker = new DOMWalker(in); + NodeReferences refs = new NodeReferences(id); + readState(walker, refs); + return refs; + } finally { + in.close(); + } + } catch (IOException ioe) { + e = ioe; + // fall through + } catch (FileSystemException fse) { + e = fse; + // fall through + } + String msg = "failed to load references: " + id; + log.debug(msg); + throw new ItemStateException(msg, e); + } + + /** + * {@inheritDoc} + */ + protected void store(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String refsFilePath = buildNodeReferencesFilePath(refs.getTargetId()); + FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath); + try { + refsFile.makeParentDirs(); + OutputStream os = refsFile.getOutputStream(); + BufferedWriter writer = null; + try { + writer = new BufferedWriter(new OutputStreamWriter(os, DEFAULT_ENCODING)); + + writer.write("\n"); + writer.write("<" + NODEREFERENCES_ELEMENT + " " + + TARGETID_ATTRIBUTE + "=\"" + refs.getTargetId() + "\">\n"); + // write references (i.e. the id's of the REFERENCE properties) + for (PropertyId propId : refs.getReferences()) { + writer.write("\t<" + NODEREFERENCE_ELEMENT + " " + + PROPERTYID_ATTRIBUTE + "=\"" + propId + "\"/>\n"); + } + writer.write("\n"); + } finally { + writer.close(); + } + } catch (Exception e) { + String msg = "failed to store " + refs; + log.debug(msg); + throw new ItemStateException(msg, e); + } + } + + /** + * {@inheritDoc} + */ + protected void destroy(NodeReferences refs) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + String refsFilePath = buildNodeReferencesFilePath(refs.getTargetId()); + FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath); + try { + if (refsFile.exists()) { + // delete resource and prune empty parent folders + refsFile.delete(true); + } + } catch (FileSystemException fse) { + String msg = "failed to delete " + refs; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + public synchronized boolean exists(NodeId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + String nodeFilePath = buildNodeFilePath(id); + FileSystemResource nodeFile = new FileSystemResource(itemStateFS, nodeFilePath); + return nodeFile.exists(); + } catch (FileSystemException fse) { + String msg = "failed to check existence of item state: " + id; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + public synchronized boolean exists(PropertyId id) throws ItemStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + String propFilePath = buildPropFilePath(id); + FileSystemResource propFile = new FileSystemResource(itemStateFS, propFilePath); + return propFile.exists(); + } catch (FileSystemException fse) { + String msg = "failed to check existence of item state: " + id; + log.error(msg, fse); + throw new ItemStateException(msg, fse); + } + } + + /** + * {@inheritDoc} + */ + public synchronized boolean existsReferencesTo(NodeId id) + throws ItemStateException { + + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + + try { + String refsFilePath = buildNodeReferencesFilePath(id); + FileSystemResource refsFile = new FileSystemResource(itemStateFS, refsFilePath); + return refsFile.exists(); + } catch (FileSystemException fse) { + String msg = "failed to check existence of references: " + id; + log.debug(msg); + throw new ItemStateException(msg, fse); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AQTQueryFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AQTQueryFactory.java new file mode 100644 index 00000000000..90f32119def --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AQTQueryFactory.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.core.query; + +import java.util.List; +import java.util.Arrays; + +import org.apache.jackrabbit.spi.commons.query.QueryTreeBuilderRegistry; + +/** + * AQTQueryFactory implements a query factory that creates AQT + * (Abstract Query Tree) based queries. + */ +public abstract class AQTQueryFactory implements QueryFactory { + + /** + * {@inheritDoc} + */ + public List getSupportedLanguages() { + return Arrays.asList(QueryTreeBuilderRegistry.getSupportedLanguages()); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AbstractQueryHandler.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AbstractQueryHandler.java new file mode 100644 index 00000000000..40502b033e9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AbstractQueryHandler.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.core.query; + +import org.apache.commons.io.IOExceptionWithCause; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.util.Iterator; + +/** + * Implements default behaviour for some methods of {@link QueryHandler}. + */ +public abstract class AbstractQueryHandler implements QueryHandler { + + /** + * Logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(AbstractQueryHandler.class); + + /** + * Search index file system, or null + */ + protected FileSystem fs; + + /** + * The context for this query handler. + */ + private QueryHandlerContext context; + + /** + * The {@link OnWorkspaceInconsistency} handler. Defaults to 'fail'. + */ + private OnWorkspaceInconsistency owi = OnWorkspaceInconsistency.FAIL; + + /** + * The name of a class that extends {@link AbstractQueryImpl}. + */ + private String queryClass = QueryImpl.class.getName(); + + /** + * The max idle time for this query handler until it is stopped. This + * property is actually not used anymore. + */ + private String idleTime; + + /** + * Initializes this query handler by setting all properties in this class + * with appropriate parameter values. + * + * @param fs search index file system, or null + * @param context the context for this query handler. + */ + public final void init(FileSystem fs, QueryHandlerContext context) + throws IOException { + this.fs = fs; + this.context = context; + doInit(); + } + + public void close() throws IOException { + if (fs != null) { + try { + fs.close(); + } catch (FileSystemException e) { + throw new IOExceptionWithCause( + "Unable to close search index file system: " + fs, e); + } + } + } + + /** + * This method must be implemented by concrete sub classes and will be + * called from {@link #init}. + * + * @throws IOException If an error occurs. + */ + protected abstract void doInit() throws IOException; + + /** + * Returns the context for this query handler. + * + * @return the QueryHandlerContext instance for this + * QueryHandler. + */ + public QueryHandlerContext getContext() { + return context; + } + + /** + * This default implementation calls the individual {@link #deleteNode(org.apache.jackrabbit.core.NodeId)} + * and {@link #addNode(org.apache.jackrabbit.core.state.NodeState)} methods + * for each entry in the iterators. First the nodes to remove are processed + * then the nodes to add. + * + * @param remove uuids of nodes to remove. + * @param add NodeStates to add. + * @throws RepositoryException if an error occurs while indexing a node. + * @throws IOException if an error occurs while updating the index. + */ + public synchronized void updateNodes( + Iterator remove, Iterator add) + throws RepositoryException, IOException { + while (remove.hasNext()) { + deleteNode(remove.next()); + } + while (add.hasNext()) { + addNode(add.next()); + } + } + + /** + * @return the {@link OnWorkspaceInconsistency} handler. + */ + public OnWorkspaceInconsistency getOnWorkspaceInconsistencyHandler() { + return owi; + } + + //--------------------------< properties >---------------------------------- + + /** + * Sets the {@link OnWorkspaceInconsistency} handler with the given name. + * Currently the valid names are: + *

      + *
    • fail
    • + *
    • log
    • + *
    + * + * @param name the name of a {@link OnWorkspaceInconsistency} handler. + */ + public void setOnWorkspaceInconsistency(String name) { + owi = OnWorkspaceInconsistency.fromString(name); + } + + /** + * @return the name of the currently set {@link OnWorkspaceInconsistency}. + */ + public String getOnWorkspaceInconsistency() { + return owi.getName(); + } + + /** + * Sets the name of the query class to use. + * + * @param queryClass the name of the query class to use. + */ + public void setQueryClass(String queryClass) { + this.queryClass = queryClass; + } + + /** + * @return the name of the query class to use. + */ + public String getQueryClass() { + return queryClass; + } + + /** + * Sets the query handler idle time. + * @deprecated + * This parameter is not supported any more. + * Please use 'maxIdleTime' in the repository configuration. + * + * @param idleTime the query handler idle time. + */ + public void setIdleTime(String idleTime) { + log.warn("Parameter 'idleTime' is not supported anymore. " + + "Please use 'maxIdleTime' in the repository configuration."); + this.idleTime = idleTime; + } + + /** + * @return the query handler idle time. + */ + public String getIdleTime() { + return idleTime; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AbstractQueryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AbstractQueryImpl.java new file mode 100644 index 00000000000..5d441edc9d7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/AbstractQueryImpl.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.core.query; + +import javax.jcr.Node; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; + +import org.apache.jackrabbit.core.session.SessionContext; + +/** + * Defines common initialisation methods for all query implementations. + */ +public abstract class AbstractQueryImpl implements Query { + + /** + * Initialises a query instance from a query string. + * + * @param sessionContext component context of the current session + * @param handler the query handler of the search index. + * @param statement the query statement. + * @param language the syntax of the query statement. + * @param node a nt:query node where the query was read from or + * null if it is not a stored query. + * @throws InvalidQueryException if the query statement is invalid according + * to the specified language. + */ + public abstract void init( + SessionContext sessionContext, QueryHandler handler, + String statement, String language, Node node) + throws InvalidQueryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/CompoundQueryFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/CompoundQueryFactory.java new file mode 100644 index 00000000000..ce3391310a4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/CompoundQueryFactory.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.core.query; + +import java.util.List; +import java.util.ArrayList; + +import javax.jcr.query.Query; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.RepositoryException; + +/** + * CompoundQueryFactory implements a query factory that consists of + * multiple other query factories. + */ +public class CompoundQueryFactory implements QueryFactory { + + /** + * The query factories. + */ + private List factories = new ArrayList(); + + /** + * Creates a compound query factory that consists of multiple other query + * factories. + * + * @param factories the query factories. + */ + public CompoundQueryFactory(List factories) { + this.factories.addAll(factories); + } + + /** + * {@inheritDoc} + */ + public List getSupportedLanguages() { + List languages = new ArrayList(); + for (QueryFactory factory : factories) { + for (String lang : factory.getSupportedLanguages()) { + languages.add(lang); + } + } + return languages; + } + + /** + * {@inheritDoc} + */ + public Query createQuery(String statement, String language) + throws InvalidQueryException, RepositoryException { + for (QueryFactory factory : factories) { + if (factory.getSupportedLanguages().contains(language)) { + return factory.createQuery(statement, language); + } + } + throw new InvalidQueryException("Unsupported language: " + language); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/ExecutableQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/ExecutableQuery.java new file mode 100644 index 00000000000..fafd5e6281e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/ExecutableQuery.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.core.query; + +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryResult; + +/** + * Specifies an interface for a query object implementation that can just be + * executed. + * @see QueryImpl + */ +public interface ExecutableQuery { + + /** + * Executes this query and returns a {@link QueryResult}. + * @param offset the offset in the total result set + * @param limit the maximum result size + * + * @return a QueryResult + * @throws RepositoryException if an error occurs + */ + QueryResult execute(long offset, long limit) throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/OnWorkspaceInconsistency.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/OnWorkspaceInconsistency.java new file mode 100644 index 00000000000..853c6efa085 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/OnWorkspaceInconsistency.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import javax.jcr.RepositoryException; +import java.util.Map; +import java.util.HashMap; + +/** + * OnWorkspaceInconsistency defines an interface to handle + * workspace inconsistencies. + */ +public abstract class OnWorkspaceInconsistency { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(OnWorkspaceInconsistency.class); + + /** + * An handler that simply logs the path of the parent node and the name + * of the missing child node and then re-throws the exception. + */ + public static final OnWorkspaceInconsistency FAIL = new OnWorkspaceInconsistency("fail") { + + public void handleMissingChildNode(NoSuchItemStateException exception, + QueryHandler handler, + Path path, + NodeState node, + ChildNodeEntry child) + throws RepositoryException, ItemStateException { + NamePathResolver resolver = new DefaultNamePathResolver( + handler.getContext().getNamespaceRegistry()); + log.error("Node {} ({}) has missing child '{}' ({})", + new Object[]{ + resolver.getJCRPath(path), + node.getNodeId(), + resolver.getJCRName(child.getName()), + child.getId() + }); + throw exception; + } + }; + + /** + * An handler that simply logs the path of the parent node and the name + * of the missing child node + */ + public static final OnWorkspaceInconsistency LOG = new OnWorkspaceInconsistency("log") { + + public void handleMissingChildNode(NoSuchItemStateException exception, + QueryHandler handler, + Path path, + NodeState node, + ChildNodeEntry child) + throws RepositoryException, ItemStateException { + NamePathResolver resolver = new DefaultNamePathResolver( + handler.getContext().getNamespaceRegistry()); + log.error("Node {} ({}) has missing child '{}' ({}). Please run a consistency check on this workspace!", + new Object[]{ + resolver.getJCRPath(path), + node.getNodeId(), + resolver.getJCRName(child.getName()), + child.getId() + }); + } + }; + + protected static final Map INSTANCES + = new HashMap(); + + static { + INSTANCES.put(FAIL.name, FAIL); + INSTANCES.put(LOG.name, LOG); + } + + /** + * The name of the {@link OnWorkspaceInconsistency} handler. + */ + private final String name; + + /** + * Protected constructor. + * + * @param name a unique name for this handler. + */ + protected OnWorkspaceInconsistency(String name) { + this.name = name; + } + + /** + * @return the name of this {@link OnWorkspaceInconsistency}. + */ + public String getName() { + return name; + } + + /** + * Returns the {@link OnWorkspaceInconsistency} with the given + * name. + * + * @param name the name of a {@link OnWorkspaceInconsistency}. + * @return the {@link OnWorkspaceInconsistency} with the given + * name. + * @throws IllegalArgumentException if name is not a well-known + * {@link OnWorkspaceInconsistency} name. + */ + public static OnWorkspaceInconsistency fromString(String name) + throws IllegalArgumentException { + OnWorkspaceInconsistency handler = INSTANCES.get(name.toLowerCase()); + if (handler == null) { + throw new IllegalArgumentException("Unknown name: " + name); + } else { + return handler; + } + } + + /** + * Handle a missing child node state. + * + * @param exception the exception that was thrown when the query handler + * tried to load the child node state. + * @param handler the query handler. + * @param path the path of the parent node. + * @param node the parent node state. + * @param child the child node entry, for which no node state could be + * found. + * @throws ItemStateException if an error occurs while handling the missing + * child node state. This may also be the passed + * exception instance. + * @throws RepositoryException if another error occurs not related to item + * state reading. + */ + public abstract void handleMissingChildNode(NoSuchItemStateException exception, + QueryHandler handler, + Path path, + NodeState node, + ChildNodeEntry child) + throws ItemStateException, RepositoryException; + + /** + * Logs a generic workspace inconsistency error. + * + * @param exception the exception that was thrown when working on the workspace + * @param handler the query handler. + * @param path the path of the parent node. + * @param node the parent node state. + * @param child the child node entry, for which no node state could be + * found. + * @throws RepositoryException if another error occurs not related to item + * state reading. + */ + public void logError(ItemStateException exception, QueryHandler handler, + Path path, NodeState node, ChildNodeEntry child) + throws RepositoryException { + if (log.isErrorEnabled()) { + NamePathResolver resolver = new DefaultNamePathResolver(handler + .getContext().getNamespaceRegistry()); + StringBuilder err = new StringBuilder(); + err.append("Workspace inconsistency error on node "); + err.append(resolver.getJCRPath(path)); + err.append(" ("); + err.append(node.getNodeId()); + err.append(") with child "); + err.append(resolver.getJCRName(child.getName())); + err.append(" ("); + err.append(child.getId()); + err.append(")."); + log.error(err.toString(), exception); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/PropertyTypeRegistry.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/PropertyTypeRegistry.java new file mode 100644 index 00000000000..6fbe9c47604 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/PropertyTypeRegistry.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistryListener; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * The PropertyTypeRegistry keeps track of registered node type + * definitions and its property types. It provides a fast type lookup for a + * given property name. + */ +public class PropertyTypeRegistry implements NodeTypeRegistryListener { + + /** The logger instance for this class */ + private static final Logger log = LoggerFactory.getLogger(PropertyTypeRegistry.class); + + /** + * Empty TypeMapping array as return value if no type is + * found + */ + private static final TypeMapping[] EMPTY = new TypeMapping[0]; + + /** The NodeTypeRegistry */ + private final NodeTypeRegistry registry; + + /** Property Name to TypeMapping[] mapping */ + private final Map typeMapping = new HashMap(); + + /** + * Creates a new PropertyTypeRegistry instance. This instance + * is *not* registered as listener to the NodeTypeRegistry in the constructor! + * @param reg the NodeTypeRegistry where to read the property + * type information. + */ + public PropertyTypeRegistry(NodeTypeRegistry reg) { + this.registry = reg; + fillCache(); + } + + /** + * Returns an array of type mappings for a given property name + * propName. If propName is not defined as a property + * in any registered node type an empty array is returned. + * @param propName the name of the property. + * @return an array of TypeMapping instances. + */ + public TypeMapping[] getPropertyTypes(Name propName) { + synchronized (typeMapping) { + TypeMapping[] types = typeMapping.get(propName); + if (types != null) { + return types; + } else { + return EMPTY; + } + } + } + + public void nodeTypeRegistered(Name ntName) { + try { + QNodeTypeDefinition def = registry.getNodeTypeDef(ntName); + QPropertyDefinition[] propDefs = def.getPropertyDefs(); + synchronized (typeMapping) { + for (QPropertyDefinition propDef : propDefs) { + int type = propDef.getRequiredType(); + if (!propDef.definesResidual() && type != PropertyType.UNDEFINED) { + Name name = propDef.getName(); + // only remember defined property types + TypeMapping[] types = typeMapping.get(name); + if (types == null) { + types = new TypeMapping[1]; + } else { + TypeMapping[] tmp = new TypeMapping[types.length + 1]; + System.arraycopy(types, 0, tmp, 0, types.length); + types = tmp; + } + types[types.length - 1] = new TypeMapping(ntName, type, propDef.isMultiple()); + typeMapping.put(name, types); + } + } + } + } catch (NoSuchNodeTypeException e) { + log.error("Unable to get newly registered node type definition for name: " + ntName); + } + } + + public void nodeTypeReRegistered(Name ntName) { + nodeTypesUnregistered(Collections.singleton(ntName)); + nodeTypeRegistered(ntName); + } + + public void nodeTypesUnregistered(Collection names) { + // remove all TypeMapping instances referring to this ntName + synchronized (typeMapping) { + Map modified = new HashMap(); + for (Iterator it = typeMapping.keySet().iterator(); it.hasNext();) { + Name propName = (Name) it.next(); + TypeMapping[] mapping = typeMapping.get(propName); + List remove = null; + for (TypeMapping tm : mapping) { + if (names.contains(tm.ntName)) { + if (remove == null) { + // not yet created + remove = new ArrayList(mapping.length); + } + remove.add(tm); + } + } + if (remove != null) { + it.remove(); + if (mapping.length == remove.size()) { + // all removed -> done + } else { + // only some removed + List remaining = new ArrayList(Arrays.asList(mapping)); + remaining.removeAll(remove); + modified.put(propName, remaining.toArray(new TypeMapping[remaining.size()])); + } + } + } + // finally re-add the modified mappings + typeMapping.putAll(modified); + } + } + + /** + * Initially fills the cache of this registry with property type definitions + * from the {@link org.apache.jackrabbit.core.nodetype.NodeTypeRegistry}. + */ + private void fillCache() { + for (Name ntName : registry.getRegisteredNodeTypes()) { + nodeTypeRegistered(ntName); + } + } + + public static class TypeMapping { + + /** The property type as an integer */ + public final int type; + + /** The Name of the node type where this type mapping originated */ + final Name ntName; + + /** True if the property type is multi-valued */ + public final boolean isMultiValued; + + private TypeMapping(Name ntName, int type, boolean isMultiValued) { + this.type = type; + this.ntName = ntName; + this.isMultiValued = isMultiValued; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QOMQueryFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QOMQueryFactory.java new file mode 100644 index 00000000000..20987d9f99a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QOMQueryFactory.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.core.query; + +import java.util.List; +import java.util.Arrays; + +import javax.jcr.query.Query; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.commons.query.QueryObjectModelBuilderRegistry; + +/** + * QOMQueryFactory implements a query factory that creates QOM + * based queries. + */ +public class QOMQueryFactory implements QueryFactory { + + /** + * The query object model factory. + */ + private final QueryObjectModelFactory qf; + + /** + * The value factory. + */ + private final ValueFactory vf; + + /** + * Creates a new QOM base query factory. + * + * @param qf the QOM factory. + * @param vf the value factory. + */ + public QOMQueryFactory(QueryObjectModelFactory qf, ValueFactory vf) { + this.qf = qf; + this.vf = vf; + } + + /** + * {@inheritDoc} + */ + public List getSupportedLanguages() { + return Arrays.asList(QueryObjectModelBuilderRegistry.getSupportedLanguages()); + } + + /** + * {@inheritDoc} + */ + public Query createQuery(String statement, String language) + throws InvalidQueryException, RepositoryException { + return QueryObjectModelBuilderRegistry.getQueryObjectModelBuilder( + language).createQueryObjectModel(statement, qf, vf); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryFactory.java new file mode 100644 index 00000000000..3b3a15aa6d9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryFactory.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.core.query; + +import java.util.List; + +import javax.jcr.query.Query; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.RepositoryException; + +/** + * QueryFactory defines a simple interface for turning a statement + * in a given language into a JCR Query instance. + */ +public interface QueryFactory { + + /** + * @return supported query languages by this factory. + */ + public List getSupportedLanguages(); + + /** + * Creates a JCR query instance from the given statement in the + * given language. + * + * @param statement the query statement. + * @param language the language of the query statement. + * @return the JCR query instance representing the query. + * @throws InvalidQueryException if the statement is malformed or the + * language is not supported. + * @throws RepositoryException if another error occurs. + */ + public Query createQuery(String statement, String language) + throws InvalidQueryException, RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandler.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandler.java new file mode 100644 index 00000000000..cec31ad45d9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandler.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.core.query; + +import java.io.IOException; +import java.util.Iterator; + +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; + +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.NodeState; + +/** + * Defines an interface for the actual node indexing and query execution. + * The goal is to allow different implementations based on the persistent + * manager in use. Some persistent model might allow to execute a query + * in an optimized manner, e.g. database persistence. + */ +public interface QueryHandler { + + /** + * Initializes this query handler. This method is called after the + * QueryHandler is instantiated. + *

    + * If a file system has been configured (i.e. the fs argument is not + * null), then the query handler is expected to close + * the given file system when the {@link #close()} method is called. + * + * @param fs the configured search index file system, or null + * @param context the context for this query handler. + * @throws IOException if an error occurs during initialization. + */ + void init(FileSystem fs, QueryHandlerContext context) throws IOException; + + /** + * Returns the query handler context that passed in {@link + * #init(FileSystem, QueryHandlerContext)}. + * + * @return the query handler context. + */ + QueryHandlerContext getContext(); + + /** + * Adds a Node to the search index. + * @param node the NodeState to add. + * @throws RepositoryException if an error occurs while indexing the node. + * @throws IOException if an error occurs while adding the node to the index. + */ + void addNode(NodeState node) throws RepositoryException, IOException; + + /** + * Deletes the Node with id from the search index. + * @param id the id of the node to delete. + * @throws IOException if an error occurs while deleting the node. + */ + void deleteNode(NodeId id) throws IOException; + + /** + * Updates the index in an atomic operation. Some nodes may be removed and + * added again in the same updateNodes() call, which is equivalent to an + * node update. + * + * @param remove Iterator of NodeIds of nodes to delete + * @param add Iterator of NodeState instance to add to the + * index. + * @throws RepositoryException if an error occurs while indexing a node. + * @throws IOException if an error occurs while updating the index. + */ + void updateNodes(Iterator remove, Iterator add) + throws RepositoryException, IOException; + + /** + * Closes this QueryHandler and frees resources attached + * to this handler. + */ + void close() throws IOException; + + /** + * Creates a new query by specifying the query statement itself and the + * language in which the query is stated. If the query statement is + * syntactically invalid, given the language specified, an + * InvalidQueryException is thrown. language must specify a query language + * string from among those returned by QueryManager.getSupportedQueryLanguages(); if it is not + * then an InvalidQueryException is thrown. + * + * @param sessionContext component context of the current session + * @param statement the query statement. + * @param language the syntax of the query statement. + * @throws InvalidQueryException if statement is invalid or language is unsupported. + * @return A Query object. + */ + ExecutableQuery createExecutableQuery( + SessionContext sessionContext, String statement, String language) + throws InvalidQueryException; + + /** + * @return the name of the query class to use. + */ + String getQueryClass(); + + /** + * Returns the ids of the nodes that refer to the node with id + * by weak references. + * + * @param id the id of the target node. + * @return the ids of the referring nodes. + * @throws RepositoryException if an error occurs. + * @throws IOException if an error occurs while reading from the + * index. + */ + public Iterable getWeaklyReferringNodes(NodeId id) + throws RepositoryException, IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandlerContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandlerContext.java new file mode 100644 index 00000000000..27473e2cc8c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandlerContext.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import java.util.concurrent.ScheduledExecutorService; + +import org.apache.jackrabbit.core.CachingHierarchyManager; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.NamespaceRegistryImpl; +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.SharedItemStateManager; + +/** + * Acts as an argument for the {@link QueryHandler} to keep the interface + * stable. This class provides access to the environment where the query + * handler is running in. + */ +public class QueryHandlerContext { + + /** + * The workspace + */ + private final String workspace; + + /** + * Repository context. + */ + private final RepositoryContext repositoryContext; + + /** + * The persistent ItemStateManager + */ + private final SharedItemStateManager stateMgr; + + /** + * The hierarchy manager on top of {@link #stateMgr}. + */ + private final CachingHierarchyManager hmgr; + + /** + * The underlying persistence manager. + */ + private final PersistenceManager pm; + + /** + * The id of the root node. + */ + private NodeId rootId; + + /** + * PropertyType registry to look up the type of a property with a given name. + */ + private final PropertyTypeRegistry propRegistry; + + /** + * The query handler for the jcr:system tree + */ + private final QueryHandler parentHandler; + + /** + * id of the node that should be excluded from indexing. + */ + private final NodeId excludedNodeId; + + /** + * Creates a new context instance. + * + * @param workspace the workspace name. + * @param repositoryContext the repository context. + * @param stateMgr provides persistent item states. + * @param pm the underlying persistence manager. + * @param rootId the id of the root node. + * @param parentHandler the parent query handler or null it + * there is no parent handler. + * @param excludedNodeId id of the node that should be excluded from + * indexing. Any descendant of that node is also + * excluded from indexing. + */ + public QueryHandlerContext( + String workspace, + RepositoryContext repositoryContext, + SharedItemStateManager stateMgr, + PersistenceManager pm, + NodeId rootId, + QueryHandler parentHandler, + NodeId excludedNodeId) { + this.workspace = workspace; + this.repositoryContext = repositoryContext; + this.stateMgr = stateMgr; + this.hmgr = new CachingHierarchyManager(rootId, stateMgr); + this.stateMgr.addListener(hmgr); + this.pm = pm; + this.rootId = rootId; + NodeTypeRegistry ntRegistry = repositoryContext.getNodeTypeRegistry(); + propRegistry = new PropertyTypeRegistry(ntRegistry); + this.parentHandler = parentHandler; + this.excludedNodeId = excludedNodeId; + ntRegistry.addListener(propRegistry); + } + + /** + * Returns the persistent {@link ItemStateManager} + * of the workspace this QueryHandler is based on. + * + * @return the persistent ItemStateManager of the current + * workspace. + */ + public ItemStateManager getItemStateManager() { + return stateMgr; + } + + /** + * Returns the hierarchy manager on top of the item state manager of this + * query handler context. + * + * @return the hierarchy manager. + */ + public HierarchyManager getHierarchyManager() { + return hmgr; + } + + /** + * @return the underlying persistence manager. + */ + public PersistenceManager getPersistenceManager() { + return pm; + } + + /** + * Returns the id of the root node. + * @return the idof the root node. + */ + public NodeId getRootId() { + return rootId; + } + + /** + * Returns the PropertyTypeRegistry for this repository. + * @return the PropertyTypeRegistry for this repository. + */ + public PropertyTypeRegistry getPropertyTypeRegistry() { + return propRegistry; + } + + /** + * Returns the NodeTypeRegistry for this repository. + * @return the NodeTypeRegistry for this repository. + */ + public NodeTypeRegistry getNodeTypeRegistry() { + return repositoryContext.getNodeTypeRegistry(); + } + + /** + * Returns the NamespaceRegistryImpl for this repository. + * @return the NamespaceRegistryImpl for this repository. + */ + public NamespaceRegistryImpl getNamespaceRegistry() { + return repositoryContext.getNamespaceRegistry(); + } + + /** + * Returns the parent query handler. + * @return the parent query handler. + */ + public QueryHandler getParentHandler() { + return parentHandler; + } + + /** + * Returns the id of the node that should be excluded from indexing. Any + * descendant of this node is also excluded from indexing. + * + * @return the uuid of the excluded node. + */ + public NodeId getExcludedNodeId() { + return excludedNodeId; + } + + /** + * Destroys this context and releases resources. + */ + public void destroy() { + repositoryContext.getNodeTypeRegistry().removeListener(propRegistry); + } + + /** + * Returns the background task executor. + * + * @return background task executor + */ + public ScheduledExecutorService getExecutor() { + return repositoryContext.getExecutor(); + } + + /** + * Returns the cluster node instance of this repository, or + * null if clustering is not enabled. + * + * @return cluster node + */ + public ClusterNode getClusterNode() { + return repositoryContext.getClusterNode(); + } + + public String getWorkspace() { + return workspace; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandlerFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandlerFactory.java new file mode 100644 index 00000000000..67829821bce --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryHandlerFactory.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.core.query; + +import javax.jcr.RepositoryException; + +public interface QueryHandlerFactory { + + QueryHandler getQueryHandler(QueryHandlerContext context) + throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryImpl.java new file mode 100644 index 00000000000..08187d54d7a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryImpl.java @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_LANGUAGE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_STATEMENT; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.NT_QUERY; + +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.QueryResult; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.api.stats.RepositoryStatistics.Type; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides the default implementation for a JCR query. + */ +public class QueryImpl extends AbstractQueryImpl { + + /** + * The logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(QueryImpl.class); + + /** + * Component context of the current session + */ + protected SessionContext sessionContext; + + /** + * The query statement + */ + protected String statement; + + /** + * The syntax of the query statement + */ + protected String language; + + /** + * The actual query implementation that can be executed + */ + protected ExecutableQuery query; + + /** + * The node where this query is persisted. Only set when this is a persisted + * query. + */ + protected Node node; + + /** + * The query handler for this query. + */ + protected QueryHandler handler; + + /** + * Flag indicating whether this query is initialized. + */ + private boolean initialized = false; + + /** + * The maximum result size + */ + protected long limit = -1; + + /** + * The offset in the total result set + */ + protected long offset = 0; + + /** + * {@inheritDoc} + */ + @Override + public void init( + SessionContext sessionContext, QueryHandler handler, + String statement, String language, Node node) + throws InvalidQueryException { + checkNotInitialized(); + this.sessionContext = sessionContext; + this.statement = statement; + this.language = language; + this.handler = handler; + this.node = node; + this.query = handler.createExecutableQuery(sessionContext, statement, language); + setInitialized(); + } + + /** + * This method simply forwards the execute call to the + * {@link ExecutableQuery} object returned by + * {@link QueryHandler#createExecutableQuery}. + * {@inheritDoc} + */ + public QueryResult execute() throws RepositoryException { + checkInitialized(); + long time = System.nanoTime(); + QueryResult result = sessionContext.getSessionState().perform( + new SessionOperation() { + public QueryResult perform(SessionContext context) + throws RepositoryException { + return query.execute(offset, limit); + } + + public String toString() { + return "query.execute(" + statement + ")"; + } + }); + time = System.nanoTime() - time; + final long timeMs = time / 1000000; + log.debug("executed in {} ms. ({})", timeMs, statement); + RepositoryStatisticsImpl statistics = sessionContext + .getRepositoryContext().getRepositoryStatistics(); + statistics.getCounter(Type.QUERY_COUNT).incrementAndGet(); + statistics.getCounter(Type.QUERY_DURATION).addAndGet(timeMs); + sessionContext.getRepositoryContext().getStatManager().getQueryStat() + .logQuery(language, statement, timeMs); + return result; + } + + /** + * {@inheritDoc} + */ + public String getStatement() { + checkInitialized(); + return statement; + } + + /** + * {@inheritDoc} + */ + public String getLanguage() { + checkInitialized(); + return language; + } + + /** + * {@inheritDoc} + */ + public String getStoredQueryPath() + throws ItemNotFoundException, RepositoryException { + checkInitialized(); + if (node == null) { + throw new ItemNotFoundException("not a persistent query"); + } + return node.getPath(); + } + + /** + * {@inheritDoc} + */ + public Node storeAsNode(String absPath) + throws ItemExistsException, + PathNotFoundException, + VersionException, + ConstraintViolationException, + LockException, + UnsupportedRepositoryOperationException, + RepositoryException { + + checkInitialized(); + try { + Path p = sessionContext.getQPath(absPath).getNormalizedPath(); + if (!p.isAbsolute()) { + throw new RepositoryException(absPath + " is not an absolute path"); + } + + String relPath = sessionContext.getJCRPath(p).substring(1); + Node queryNode = + sessionContext.getSessionImpl().getRootNode().addNode( + relPath, sessionContext.getJCRName(NT_QUERY)); + // set properties + queryNode.setProperty(sessionContext.getJCRName(JCR_LANGUAGE), language); + queryNode.setProperty(sessionContext.getJCRName(JCR_STATEMENT), statement); + node = queryNode; + return node; + } catch (NameException e) { + throw new RepositoryException(e.getMessage(), e); + } + } + + /** + * {@inheritDoc} + */ + public String[] getBindVariableNames() { + return new String[0]; + } + + /** + * Throws an {@link IllegalArgumentException} as XPath and SQL1 queries + * have no bind variables. + * + * @throws IllegalArgumentException always thrown + */ + public void bindValue(String varName, Value value) + throws IllegalArgumentException { + throw new IllegalArgumentException("No such bind variable: " + varName); + } + + /** + * Sets the maximum size of the result set. + * + * @param limit new maximum size of the result set + */ + public void setLimit(long limit) { + if (limit < 0) { + throw new IllegalArgumentException("limit must not be negative"); + } + this.limit = limit; + } + + /** + * Sets the start offset of the result set. + * + * @param offset new start offset of the result set + */ + public void setOffset(long offset) { + if (offset < 0) { + throw new IllegalArgumentException("offset must not be negative"); + } + this.offset = offset; + } + + //-----------------------------< internal >--------------------------------- + + /** + * Sets the initialized flag. + */ + protected void setInitialized() { + initialized = true; + } + + /** + * Checks if this query is not yet initialized and throws an + * IllegalStateException if it is already initialized. + */ + protected void checkNotInitialized() { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + } + + /** + * Checks if this query is initialized and throws an + * IllegalStateException if it is not yet initialized. + */ + protected void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryManagerImpl.java new file mode 100644 index 00000000000..4b5a8ed1576 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryManagerImpl.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.core.query; + +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_LANGUAGE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_STATEMENT; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.NT_QUERY; + +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; +import java.io.IOException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.core.SearchManager; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.spi.commons.query.qom.QueryObjectModelFactoryImpl; +import org.apache.jackrabbit.spi.commons.query.qom.QueryObjectModelTree; + +/** + * This class implements the {@link QueryManager} interface. + */ +public class QueryManagerImpl implements QueryManager { + + /** + * Component context of the current session. + */ + private final SessionContext sessionContext; + + /** + * The SearchManager holding the search index. + */ + private final SearchManager searchMgr; + + /** + * The QueryObjectModelFactory for this query manager. + */ + private final QueryObjectModelFactoryImpl qomFactory; + + /** + * Creates a new QueryManagerImpl for the passed + * session + * + * @param sessionContext component context of the current session + * @param searchMgr the search manager of this workspace. + * @throws RepositoryException if an error occurs while initializing the + * query manager. + */ + public QueryManagerImpl( + final SessionContext sessionContext, final SearchManager searchMgr) + throws RepositoryException { + this.sessionContext = sessionContext; + this.searchMgr = searchMgr; + this.qomFactory = new QueryObjectModelFactoryImpl(sessionContext) { + protected QueryObjectModel createQuery(QueryObjectModelTree qomTree) + throws InvalidQueryException, RepositoryException { + return searchMgr.createQueryObjectModel( + sessionContext, qomTree, Query.JCR_JQOM, null); + } + }; + } + + /** + * {@inheritDoc} + */ + public Query createQuery(final String statement, final String language) + throws RepositoryException { + return perform(new SessionOperation() { + public Query perform(SessionContext context) + throws RepositoryException { + QueryFactory qf = new QueryFactoryImpl(language); + return qf.createQuery(statement, language); + } + public String toString() { + return "node.createQuery(" + statement + ", " + language + ")"; + } + }); + } + + /** + * {@inheritDoc} + */ + public Query getQuery(final Node node) throws RepositoryException { + return perform(new SessionOperation() { + public Query perform(SessionContext context) + throws RepositoryException { + if (!node.isNodeType(context.getJCRName(NT_QUERY))) { + throw new InvalidQueryException( + "Node is not of type nt:query: " + node); + } + String statement = + node.getProperty(context.getJCRName(JCR_STATEMENT)).getString(); + String language = + node.getProperty(context.getJCRName(JCR_LANGUAGE)).getString(); + + QueryFactory qf = new QueryFactoryImpl(node, language); + return qf.createQuery(statement, language); + } + public String toString() { + return "queryManager.getQuery(node)"; + } + }); + } + + /** + * {@inheritDoc} + */ + public String[] getSupportedQueryLanguages() throws RepositoryException { + List languages = new QueryFactoryImpl(Query.JCR_JQOM).getSupportedLanguages(); + return languages.toArray(new String[languages.size()]); + } + + //---------------------------< JSR 283 >------------------------------------ + + /** + * Returns a QueryObjectModelFactory with which a JCR-JQOM + * query can be built programmatically. + * + * @return a QueryObjectModelFactory object + * @since JCR 2.0 + */ + public QueryObjectModelFactory getQOMFactory() { + return qomFactory; + } + + //-------------------------< Jackrabbit internal >-------------------------- + + /** + * Returns the ids of the nodes that refer to the node by weak + * references. + * + * @param node the target node. + * @return the referring nodes. + * @throws RepositoryException if an error occurs. + */ + public Iterable getWeaklyReferringNodes(final Node node) + throws RepositoryException { + return perform(new SessionOperation>() { + public Iterable perform(SessionContext context) + throws RepositoryException { + List nodes = new ArrayList(); + try { + NodeId nodeId = new NodeId(node.getIdentifier()); + for (NodeId id : searchMgr.getWeaklyReferringNodes(nodeId)) { + nodes.add(sessionContext.getSessionImpl().getNodeById(id)); + } + } catch (IOException e) { + throw new RepositoryException(e); + } + return nodes; + } + public String toString() { + return "queryManager.getWeaklyReferringNodes(node)"; + } + }); + } + + //------------------------< testing only >---------------------------------- + + /** + * @return the query handler implementation. + */ + QueryHandler getQueryHandler() { + return searchMgr.getQueryHandler(); + } + + //---------------------------< internal >----------------------------------- + + /** + * Performs the given session operation. + */ + private T perform(SessionOperation operation) + throws RepositoryException { + return sessionContext.getSessionState().perform(operation); + } + + private class QueryFactoryImpl extends CompoundQueryFactory { + + public QueryFactoryImpl(String language) { + this(null, language); + } + + public QueryFactoryImpl(final Node node, final String language) { + super(Arrays.asList( + new QOMQueryFactory(new QueryObjectModelFactoryImpl( + sessionContext.getSessionImpl()) { + @Override + protected QueryObjectModel createQuery( + QueryObjectModelTree qomTree) + throws RepositoryException { + return searchMgr.createQueryObjectModel( + sessionContext, qomTree, language, node); + } + }, + sessionContext.getSessionImpl().getValueFactory()), + new AQTQueryFactory() { + public Query createQuery(String statement, String language) + throws RepositoryException { + return searchMgr.createQuery( + sessionContext, statement, language, node); + } + })); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryObjectModelImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryObjectModelImpl.java new file mode 100644 index 00000000000..fa27dc73960 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/QueryObjectModelImpl.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.core.query; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.InvalidQueryException; +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.api.stats.RepositoryStatistics.Type; +import org.apache.jackrabbit.commons.query.QueryObjectModelBuilderRegistry; +import org.apache.jackrabbit.core.query.lucene.LuceneQueryFactory; +import org.apache.jackrabbit.core.query.lucene.SearchIndex; +import org.apache.jackrabbit.core.query.lucene.join.QueryEngine; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; +import org.apache.jackrabbit.spi.commons.query.qom.BindVariableValueImpl; +import org.apache.jackrabbit.spi.commons.query.qom.DefaultTraversingQOMTreeVisitor; +import org.apache.jackrabbit.spi.commons.query.qom.QueryObjectModelTree; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * QueryObjectModelImpl implements the query object model. + */ +public class QueryObjectModelImpl extends QueryImpl implements QueryObjectModel { + + /** + * The logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(QueryObjectModelImpl.class); + + /** + * The query object model tree. + */ + protected QueryObjectModelTree qomTree; + + /** Bind variables */ + private final Map variables = new HashMap(); + + private LuceneQueryFactory lqf; + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException always. + */ + @Override + public void init( + SessionContext sessionContext, QueryHandler handler, + String statement, String language, Node node) + throws InvalidQueryException { + throw new UnsupportedOperationException(); + } + + /** + * Initializes a query instance from a query object model. + * + * @param sessionContext component context of the current session + * @param handler the query handler of the search index. + * @param qomTree the query object model tree. + * @param language the original query syntax from where the JQOM was + * created. + * @param node a nt:query node where the query was read from or + * null if it is not a stored query. + * @throws InvalidQueryException if the qom tree cannot be serialized + * according to the given language. + * @throws RepositoryException if another error occurs + */ + public void init( + SessionContext sessionContext, QueryHandler handler, + QueryObjectModelTree qomTree, String language, Node node) + throws InvalidQueryException, RepositoryException { + checkNotInitialized(); + this.sessionContext = sessionContext; + this.language = language; + this.handler = handler; + this.qomTree = qomTree; + this.node = node; + this.statement = QueryObjectModelBuilderRegistry.getQueryObjectModelBuilder(language).toString(this); + + try { + qomTree.accept(new DefaultTraversingQOMTreeVisitor() { + @Override + public Object visit(BindVariableValueImpl node, Object data) { + variables.put(node.getBindVariableName(), null); + return data; + } + }, null); + } catch (Exception ignore) { + } + this.lqf = new LuceneQueryFactory( + sessionContext.getSessionImpl(), (SearchIndex) handler, + variables); + setInitialized(); + } + + public QueryResult execute() throws RepositoryException { + long time = System.nanoTime(); + final QueryResult result = sessionContext.getSessionState().perform( + new SessionOperation() { + public QueryResult perform(SessionContext context) + throws RepositoryException { + final QueryEngine engine = new QueryEngine( + sessionContext.getSessionImpl(), lqf, variables); + return engine.execute(getColumns(), getSource(), + getConstraint(), getOrderings(), offset, limit); + } + + public String toString() { + return "query.execute(" + statement + ")"; + } + }); + time = System.nanoTime() - time; + final long timeMs = time / 1000000; + log.debug("executed in {} ms. ({})", timeMs, statement); + RepositoryStatisticsImpl statistics = sessionContext + .getRepositoryContext().getRepositoryStatistics(); + statistics.getCounter(Type.QUERY_COUNT).incrementAndGet(); + statistics.getCounter(Type.QUERY_DURATION).addAndGet(timeMs); + sessionContext.getRepositoryContext().getStatManager().getQueryStat() + .logQuery(language, statement, timeMs); + return result; + } + + @Override + public String[] getBindVariableNames() { + return variables.keySet().toArray(new String[variables.size()]); + } + + @Override + public void bindValue(String varName, Value value) + throws IllegalArgumentException { + if (variables.containsKey(varName)) { + variables.put(varName, value); + } else { + throw new IllegalArgumentException( + "No such bind variable: " + varName); + } + } + + //-------------------------< QueryObjectModel >----------------------------- + + /** + * Gets the node-tuple source for this query. + * + * @return the node-tuple source; non-null + */ + public Source getSource() { + return qomTree.getSource(); + } + + /** + * Gets the constraint for this query. + * + * @return the constraint, or null if none + */ + public Constraint getConstraint() { + return qomTree.getConstraint(); + } + + /** + * Gets the orderings for this query. + * + * @return an array of zero or more orderings; non-null + */ + public Ordering[] getOrderings() { + return qomTree.getOrderings(); + } + + /** + * Gets the columns for this query. + * + * @return an array of zero or more columns; non-null + */ + public Column[] getColumns() { + return qomTree.getColumns(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractExcerpt.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractExcerpt.java new file mode 100644 index 00000000000..7113ea08846 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractExcerpt.java @@ -0,0 +1,325 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.OffsetAttribute; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Fieldable; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermFreqVector; +import org.apache.lucene.index.TermPositionVector; +import org.apache.lucene.index.TermVectorOffsetInfo; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractExcerpt implements base functionality for an excerpt + * provider. + */ +public abstract class AbstractExcerpt implements HighlightingExcerptProvider { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(AbstractExcerpt.class); + + /** + * The search index. + */ + protected SearchIndex index; + + /** + * The current query. + */ + protected Query query; + + /** + * Indicates whether the query is already rewritten. + */ + private boolean rewritten = false; + + /** + * {@inheritDoc} + */ + public void init(Query query, SearchIndex index) throws IOException { + this.index = index; + this.query = query; + } + + /** + * {@inheritDoc} + */ + public String getExcerpt(NodeId id, int maxFragments, int maxFragmentSize) + throws IOException { + IndexReader reader = index.getIndexReader(); + try { + checkRewritten(reader); + Term idTerm = TermFactory.createUUIDTerm(id.toString()); + TermDocs tDocs = reader.termDocs(idTerm); + int docNumber; + Document doc; + try { + if (tDocs.next()) { + docNumber = tDocs.doc(); + doc = reader.document(docNumber); + } else { + // node not found in index + return null; + } + } finally { + tDocs.close(); + } + Fieldable[] fields = doc.getFieldables(FieldNames.FULLTEXT); + if (fields.length == 0) { + log.debug("Fulltext field not stored, using {}", + SimpleExcerptProvider.class.getName()); + SimpleExcerptProvider exProvider = new SimpleExcerptProvider(); + exProvider.init(query, index); + return exProvider.getExcerpt(id, maxFragments, maxFragmentSize); + } + StringBuffer text = new StringBuffer(); + String separator = ""; + for (int i = 0; i < fields.length; i++) { + if (fields[i].stringValue().length() == 0) { + continue; + } + text.append(separator); + text.append(fields[i].stringValue()); + separator = " "; + } + TermFreqVector tfv = reader.getTermFreqVector( + docNumber, FieldNames.FULLTEXT); + if (tfv instanceof TermPositionVector) { + return createExcerpt((TermPositionVector) tfv, text.toString(), + maxFragments, maxFragmentSize); + } else { + log.debug("No TermPositionVector on Fulltext field."); + return null; + } + } finally { + Util.closeOrRelease(reader); + } + } + + /** + * {@inheritDoc} + */ + public String highlight(String text) throws IOException { + checkRewritten(null); + return createExcerpt(createTermPositionVector(text), + text, 1, (text.length() + 1) * 2); + } + + /** + * Creates an excerpt for the given text using token offset + * information provided by tpv. + * + * @param tpv the term position vector for the fulltext field. + * @param text the original text. + * @param maxFragments the maximum number of fragments to create. + * @param maxFragmentSize the maximum number of characters in a fragment. + * @return the xml excerpt. + * @throws IOException if an error occurs while creating the excerpt. + */ + protected abstract String createExcerpt(TermPositionVector tpv, + String text, + int maxFragments, + int maxFragmentSize) + throws IOException; + + /** + * @return the extracted terms from the query. + */ + protected final Set getQueryTerms() { + Set relevantTerms = new HashSet(); + getQueryTerms(query, relevantTerms); + return relevantTerms; + } + + private static void getQueryTerms(Query q, Set relevantTerms) { + if (q instanceof BooleanQuery) { + final BooleanQuery bq = (BooleanQuery) q; + for (BooleanClause clause : bq.getClauses()) { + getQueryTerms(clause.getQuery(), relevantTerms); + } + return; + } + //need to preserve insertion order + Set extractedTerms = new LinkedHashSet(); + q.extractTerms(extractedTerms); + Set filteredTerms = filterRelevantTerms(extractedTerms); + if (!filteredTerms.isEmpty()) { + if (q instanceof PhraseQuery) { + // inline the terms, basically a 'must all' condition + relevantTerms.add(filteredTerms.toArray(new Term[] {})); + } else { + // each possible term gets a new slot + for (Term t : filteredTerms) { + relevantTerms.add(new Term[] { t }); + } + } + } + } + + private static Set filterRelevantTerms(Set extractedTerms) { + //need to preserve insertion order + Set relevantTerms = new LinkedHashSet(); + // only keep terms for fulltext fields + for (Term t : extractedTerms) { + if (t.field().equals(FieldNames.FULLTEXT)) { + relevantTerms.add(t); + } else { + int idx = t.field().indexOf(FieldNames.FULLTEXT_PREFIX); + if (idx != -1) { + relevantTerms.add(new Term(FieldNames.FULLTEXT, t.text())); + } + } + } + return relevantTerms; + } + + /** + * Makes sure the {@link #query} is rewritten. If the query is already + * rewritten, this method returns immediately. + * + * @param reader an optional index reader, if none is passed this method + * will retrieve one from the {@link #index} and close it + * again after the rewrite operation. + * @throws IOException if an error occurs while the query is rewritten. + */ + private void checkRewritten(IndexReader reader) throws IOException { + if (!rewritten) { + IndexReader r = reader; + if (r == null) { + r = index.getIndexReader(); + } + try { + query = query.rewrite(r); + } finally { + // only close reader if this method opened one + if (reader == null) { + Util.closeOrRelease(r); + } + } + rewritten = true; + } + } + + /** + * @param text the text. + * @return a TermPositionVector for the given text. + */ + private TermPositionVector createTermPositionVector(String text) { + // term -> TermVectorOffsetInfo[] + final SortedMap termMap = + new TreeMap(); + Reader r = new StringReader(text); + TokenStream ts = index.getTextAnalyzer().tokenStream("", r); + try { + while (ts.incrementToken()) { + OffsetAttribute offset = ts.getAttribute(OffsetAttribute.class); + TermAttribute term = ts.getAttribute(TermAttribute.class); + String termText = term.term(); + TermVectorOffsetInfo[] info = termMap.get(termText); + if (info == null) { + info = new TermVectorOffsetInfo[1]; + } else { + TermVectorOffsetInfo[] tmp = info; + info = new TermVectorOffsetInfo[tmp.length + 1]; + System.arraycopy(tmp, 0, info, 0, tmp.length); + } + info[info.length - 1] = new TermVectorOffsetInfo( + offset.startOffset(), offset.endOffset()); + termMap.put(termText, info); + } + ts.end(); + ts.close(); + } catch (IOException e) { + // should never happen, we are reading from a string + } + + return new TermPositionVector() { + + private String[] terms = + (String[]) termMap.keySet().toArray(new String[termMap.size()]); + + public int[] getTermPositions(int index) { + return null; + } + + public TermVectorOffsetInfo[] getOffsets(int index) { + TermVectorOffsetInfo[] info = TermVectorOffsetInfo.EMPTY_OFFSET_INFO; + if (index >= 0 && index < terms.length) { + info = termMap.get(terms[index]); + } + return info; + } + + public String getField() { + return ""; + } + + public int size() { + return terms.length; + } + + public String[] getTerms() { + return terms; + } + + public int[] getTermFrequencies() { + int[] freqs = new int[terms.length]; + for (int i = 0; i < terms.length; i++) { + freqs[i] = termMap.get(terms[i]).length; + } + return freqs; + } + + public int indexOf(String term) { + int res = Arrays.binarySearch(terms, term); + return res >= 0 ? res : -1; + } + + public int[] indexesOf(String[] terms, int start, int len) { + int[] res = new int[len]; + for (int i = 0; i < len; i++) { + res[i] = indexOf(terms[i]); + } + return res; + } + }; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractIndex.java new file mode 100644 index 00000000000..0aa4017a996 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractIndex.java @@ -0,0 +1,622 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.PayloadAttribute; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Fieldable; +import org.apache.lucene.index.IndexDeletionPolicy; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.LogByteSizeMergePolicy; +import org.apache.lucene.index.LogMergePolicy; +import org.apache.lucene.index.Payload; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.Version; +import org.apache.tika.io.IOExceptionWithCause; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements common functionality for a lucene index. + *

    + * Note on synchronization: This class is not entirely thread-safe. Certain + * concurrent access is however allowed. Read-only access on this index using + * {@link #getReadOnlyIndexReader()} is thread-safe. That is, multiple threads + * my call that method concurrently and use the returned IndexReader at the same + * time.
    + * Modifying threads must be synchronized externally in a way that only one + * thread is using the returned IndexReader and IndexWriter instances returned + * by {@link #getIndexReader()} and {@link #getIndexWriter()} at a time.
    + * Concurrent access by one modifying thread and multiple read-only + * threads is safe! + */ +abstract class AbstractIndex { + + /** The logger instance for this class */ + private static final Logger log = LoggerFactory.getLogger(AbstractIndex.class); + + /** PrintStream that pipes all calls to println(String) into log.info() */ + private static final LoggingPrintStream STREAM_LOGGER = new LoggingPrintStream(); + + /** Executor with a pool size equal to the number of available processors */ + private final DynamicPooledExecutor executor = new DynamicPooledExecutor(); + + /** The currently set IndexWriter or null if none is set */ + private IndexWriter indexWriter; + + /** The currently set IndexReader or null if none is set */ + private CommittableIndexReader indexReader; + + /** The underlying Directory where the index is stored */ + private Directory directory; + + /** Analyzer we use to tokenize text */ + private Analyzer analyzer; + + /** The similarity in use for indexing and searching. */ + private final Similarity similarity; + + /** Compound file flag */ + private boolean useCompoundFile = true; + + /** termInfosIndexDivisor config parameter */ + private int termInfosIndexDivisor = SearchIndex.DEFAULT_TERM_INFOS_INDEX_DIVISOR; + + /** + * The document number cache if this index may use one. + */ + private DocNumberCache cache; + + /** The shared IndexReader for all read-only IndexReaders */ + private SharedIndexReader sharedReader; + + /** + * The most recent read-only reader if there is any. + */ + private ReadOnlyIndexReader readOnlyReader; + + /** + * The indexing queue. + */ + private IndexingQueue indexingQueue; + + /** + * Flag that indicates whether there was an index present in the directory + * when this AbstractIndex was created. + */ + private boolean isExisting; + + /** + * Constructs an index with an analyzer and a + * directory. + * + * @param analyzer the analyzer for text tokenizing. + * @param similarity the similarity implementation. + * @param directory the underlying directory. + * @param cache the document number cache if this index should use + * one; otherwise cache is + * null. + * @param indexingQueue the indexing queue. + * @throws IOException if the index cannot be initialized. + */ + AbstractIndex(Analyzer analyzer, + Similarity similarity, + Directory directory, + DocNumberCache cache, + IndexingQueue indexingQueue) throws IOException { + this.analyzer = analyzer; + this.similarity = similarity; + this.directory = directory; + this.cache = cache; + this.indexingQueue = indexingQueue; + this.isExisting = IndexReader.indexExists(directory); + + if (!isExisting) { + indexWriter = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36, analyzer)); + // immediately close, now that index has been created + indexWriter.close(); + indexWriter = null; + } + } + + /** + * Default implementation returns the same instance as passed + * in the constructor. + * + * @return the directory instance passed in the constructor + */ + Directory getDirectory() { + return directory; + } + + /** + * Returns true if this index was openend on a directory with + * an existing index in it; false otherwise. + * + * @return true if there was an index present when this index + * was created; false otherwise. + */ + boolean isExisting() { + return isExisting; + } + + /** + * Adds documents to this index and invalidates the shared reader. + * + * @param docs the documents to add. + * @throws IOException if an error occurs while writing to the index. + */ + void addDocuments(Document[] docs) throws IOException { + final List exceptions = + Collections.synchronizedList(new ArrayList()); + final CountDownLatch latch = new CountDownLatch(docs.length); + + final IndexWriter writer = getIndexWriter(); + for (final Document doc : docs) { + executor.execute(new Runnable() { + public void run() { + try { + // check if text extractor completed its work + Document document = getFinishedDocument(doc); + if (log.isDebugEnabled()) { + long start = System.nanoTime(); + writer.addDocument(document); + log.debug("Inverted a document in {}us", + (System.nanoTime() - start) / 1000); + } else { + writer.addDocument(document); + } + } catch (IOException e) { + log.warn("Exception while inverting a document", e); + exceptions.add(e); + } finally { + latch.countDown(); + } + } + }); + } + + for (;;) { + try { + latch.await(); + break; + } catch (InterruptedException e) { + // retry + } + } + invalidateSharedReader(); + + if (!exceptions.isEmpty()) { + throw new IOExceptionWithCause( + exceptions.size() + " of " + docs.length + + " background indexer tasks failed", exceptions.get(0)); + } + } + + /** + * Removes the document from this index. This call will not invalidate + * the shared reader. If a subclass whishes to do so, it should overwrite + * this method and call {@link #invalidateSharedReader()}. + * + * @param idTerm the id term of the document to remove. + * @throws IOException if an error occurs while removing the document. + * @return number of documents deleted + */ + int removeDocument(Term idTerm) throws IOException { + return getIndexReader().deleteDocuments(idTerm); + } + + /** + * Returns an IndexReader on this index. This index reader + * may be used to delete documents. + * + * @return an IndexReader on this index. + * @throws IOException if the reader cannot be obtained. + */ + protected synchronized CommittableIndexReader getIndexReader() throws IOException { + if (indexWriter != null) { + indexWriter.close(); + log.debug("closing IndexWriter."); + indexWriter = null; + } + if (indexReader == null) { + IndexDeletionPolicy idp = getIndexDeletionPolicy(); + IndexReader reader = IndexReader.open(getDirectory(), idp, false, termInfosIndexDivisor); + indexReader = new CommittableIndexReader(reader); + } + return indexReader; + } + + /** + * Returns the index deletion policy for this index. This implementation + * always returns null. + * + * @return the index deletion policy for this index or null if + * none is present. + */ + protected IndexDeletionPolicy getIndexDeletionPolicy() { + return null; + } + + /** + * Returns a read-only index reader, that can be used concurrently with + * other threads writing to this index. The returned index reader is + * read-only, that is, any attempt to delete a document from the index + * will throw an UnsupportedOperationException. + * + * @param initCache if the caches in the index reader should be initialized + * before the index reader is returned. + * @return a read-only index reader. + * @throws IOException if an error occurs while obtaining the index reader. + */ + synchronized ReadOnlyIndexReader getReadOnlyIndexReader(boolean initCache) + throws IOException { + // get current modifiable index reader + CommittableIndexReader modifiableReader = getIndexReader(); + long modCount = modifiableReader.getModificationCount(); + if (readOnlyReader != null) { + if (readOnlyReader.getDeletedDocsVersion() == modCount) { + // reader up-to-date + readOnlyReader.acquire(); + return readOnlyReader; + } else { + // reader outdated + if (readOnlyReader.getRefCountJr() == 1) { + // not in use, except by this index + // update the reader + readOnlyReader.updateDeletedDocs(modifiableReader); + readOnlyReader.acquire(); + return readOnlyReader; + } else { + // cannot update reader, it is still in use + // need to create a new instance + readOnlyReader.release(); + readOnlyReader = null; + } + } + } + // if we get here there is no up-to-date read-only reader + if (sharedReader == null) { + // create new shared reader + IndexReader reader = IndexReader.open(getDirectory(), termInfosIndexDivisor); + CachingIndexReader cr = new CachingIndexReader( + reader, cache, initCache); + sharedReader = new SharedIndexReader(cr); + } + readOnlyReader = new ReadOnlyIndexReader(sharedReader, + modifiableReader.getDeletedDocs(), modCount); + readOnlyReader.acquire(); + return readOnlyReader; + } + + /** + * Returns a read-only index reader, that can be used concurrently with + * other threads writing to this index. The returned index reader is + * read-only, that is, any attempt to delete a document from the index + * will throw an UnsupportedOperationException. + * + * @return a read-only index reader. + * @throws IOException if an error occurs while obtaining the index reader. + */ + protected ReadOnlyIndexReader getReadOnlyIndexReader() + throws IOException { + return getReadOnlyIndexReader(false); + } + + /** + * Returns an IndexWriter on this index. + * @return an IndexWriter on this index. + * @throws IOException if the writer cannot be obtained. + */ + protected synchronized IndexWriter getIndexWriter() throws IOException { + if (indexReader != null) { + indexReader.close(); + log.debug("closing IndexReader."); + indexReader = null; + } + if (indexWriter == null) { + IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_36, analyzer); + config.setSimilarity(similarity); + LogMergePolicy mergePolicy = new LogByteSizeMergePolicy(); + mergePolicy.setUseCompoundFile(useCompoundFile); + mergePolicy.setNoCFSRatio(1.0); + config.setMergePolicy(mergePolicy); + + indexWriter = new IndexWriter(getDirectory(), config); + indexWriter.setInfoStream(STREAM_LOGGER); + } + return indexWriter; + } + + /** + * Commits all pending changes to the underlying Directory. + * @throws IOException if an error occurs while commiting changes. + */ + protected void commit() throws IOException { + commit(false); + } + + /** + * Commits all pending changes to the underlying Directory. + * + * @param optimize if true the index is optimized after the + * commit. + * @throws IOException if an error occurs while commiting changes. + */ + protected synchronized void commit(boolean optimize) throws IOException { + if (indexReader != null) { + log.debug("committing IndexReader."); + indexReader.flush(); + } + if (indexWriter != null) { + log.debug("committing IndexWriter."); + indexWriter.commit(); + } + // optimize if requested + if (optimize) { + IndexWriter writer = getIndexWriter(); + writer.forceMerge(1, true); + writer.close(); + indexWriter = null; + } + } + + /** + * Closes this index, releasing all held resources. + */ + synchronized void close() { + releaseWriterAndReaders(); + if (directory != null) { + try { + directory.close(); + } catch (IOException e) { + directory = null; + } + } + executor.close(); + } + + /** + * Releases all potentially held index writer and readers. + */ + protected void releaseWriterAndReaders() { + if (indexWriter != null) { + try { + indexWriter.close(); + } catch (IOException e) { + log.warn("Exception closing index writer: " + e.toString()); + } + indexWriter = null; + } + if (indexReader != null) { + try { + indexReader.close(); + } catch (IOException e) { + log.warn("Exception closing index reader: " + e.toString()); + } + indexReader = null; + } + if (readOnlyReader != null) { + try { + readOnlyReader.release(); + } catch (IOException e) { + log.warn("Exception closing index reader: " + e.toString()); + } + readOnlyReader = null; + } + if (sharedReader != null) { + try { + sharedReader.release(); + } catch (IOException e) { + log.warn("Exception closing index reader: " + e.toString()); + } + sharedReader = null; + } + } + + /** + * @return the number of bytes this index occupies in memory. + */ + synchronized long getRamSizeInBytes() { + if (indexWriter != null) { + return indexWriter.ramSizeInBytes(); + } else { + return 0; + } + } + + /** + * Closes the shared reader. + * + * @throws IOException if an error occurs while closing the reader. + */ + protected synchronized void invalidateSharedReader() throws IOException { + // also close the read-only reader + if (readOnlyReader != null) { + readOnlyReader.release(); + readOnlyReader = null; + } + // invalidate shared reader + if (sharedReader != null) { + sharedReader.release(); + sharedReader = null; + } + } + + /** + * Returns a document that is finished with text extraction and is ready to + * be added to the index. + * + * @param doc the document to check. + * @return doc if it is finished already or a stripped down + * copy of doc without text extractors. + * @throws IOException if the document cannot be added to the indexing + * queue. + */ + private Document getFinishedDocument(Document doc) throws IOException { + if (!Util.isDocumentReady(doc)) { + Document copy = new Document(); + // mark the document that reindexing is required + copy.add(new Field(FieldNames.REINDEXING_REQUIRED, false, "", + Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + for (Fieldable f : doc.getFields()) { + Fieldable field = null; + Field.TermVector tv = getTermVectorParameter(f); + Field.Store stored = f.isStored() ? Field.Store.YES : Field.Store.NO; + Field.Index indexed = getIndexParameter(f); + if (f instanceof LazyTextExtractorField || f.readerValue() != null) { + // replace all readers with empty string reader + field = new Field(f.name(), new StringReader(""), tv); + } else if (f.stringValue() != null) { + field = new Field(f.name(), false, f.stringValue(), stored, + indexed, tv); + } else if (f.isBinary()) { + field = new Field(f.name(), f.getBinaryValue(), stored); + } else if (f.tokenStreamValue() != null && f.tokenStreamValue() instanceof SingletonTokenStream) { + TokenStream tokenStream = f.tokenStreamValue(); + TermAttribute termAttribute = tokenStream.addAttribute(TermAttribute.class); + PayloadAttribute payloadAttribute = tokenStream.addAttribute(PayloadAttribute.class); + tokenStream.incrementToken(); + String value = new String(termAttribute.termBuffer(), 0, termAttribute.termLength()); + tokenStream.reset(); + field = new Field(f.name(), new SingletonTokenStream(value, (Payload) payloadAttribute.getPayload().clone())); + } + if (field != null) { + field.setOmitNorms(f.getOmitNorms()); + copy.add(field); + } + } + // schedule the original document for later indexing + Document existing = indexingQueue.addDocument(doc); + if (existing != null) { + // the queue already contained a pending document for this + // node. -> dispose the document + Util.disposeDocument(existing); + } + // use the stripped down copy for now + doc = copy; + } + return doc; + } + + //-------------------------< properties >----------------------------------- + + /** + * Whether the index writer should use the compound file format + */ + void setUseCompoundFile(boolean b) { + useCompoundFile = b; + } + + /** + * @return the current value for termInfosIndexDivisor. + */ + public int getTermInfosIndexDivisor() { + return termInfosIndexDivisor; + } + + /** + * Sets a new value for termInfosIndexDivisor. + * + * @param termInfosIndexDivisor the new value. + */ + public void setTermInfosIndexDivisor(int termInfosIndexDivisor) { + this.termInfosIndexDivisor = termInfosIndexDivisor; + } + + //------------------------------< internal >-------------------------------- + + /** + * Returns the index parameter set on f. + * + * @param f a lucene field. + * @return the index parameter on f. + */ + private static Field.Index getIndexParameter(Fieldable f) { + if (!f.isIndexed()) { + return Field.Index.NO; + } else if (f.isTokenized()) { + return Field.Index.ANALYZED; + } else { + return Field.Index.NOT_ANALYZED; + } + } + + /** + * Returns the term vector parameter set on f. + * + * @param f a lucene field. + * @return the term vector parameter on f. + */ + private static Field.TermVector getTermVectorParameter(Fieldable f) { + if (f.isStorePositionWithTermVector() && f.isStoreOffsetWithTermVector()) { + return Field.TermVector.WITH_POSITIONS_OFFSETS; + } else if (f.isStorePositionWithTermVector()) { + return Field.TermVector.WITH_POSITIONS; + } else if (f.isStoreOffsetWithTermVector()) { + return Field.TermVector.WITH_OFFSETS; + } else if (f.isTermVectorStored()) { + return Field.TermVector.YES; + } else { + return Field.TermVector.NO; + } + } + + /** + * Adapter to pipe info messages from lucene into log messages. + */ + private static final class LoggingPrintStream extends PrintStream { + + /** Buffer print calls until a newline is written */ + private StringBuffer buffer = new StringBuffer(); + + public LoggingPrintStream() { + super(new OutputStream() { + public void write(int b) { + // do nothing + } + }); + } + + public void print(String s) { + buffer.append(s); + } + + public void println(String s) { + buffer.append(s); + log.debug(buffer.toString()); + buffer.setLength(0); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractNamespaceMappings.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractNamespaceMappings.java new file mode 100644 index 00000000000..248d7914012 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractNamespaceMappings.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.core.query.lucene; + +import javax.jcr.NamespaceException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +/** + * AbstractNamespaceMappings is the base class for index internal + * namespace mappings. + */ +public abstract class AbstractNamespaceMappings + implements NamespaceMappings, NamespaceResolver { + + /** + * The name resolver used to translate the Names to JCR name strings. + */ + private final NamePathResolver resolver; + + public AbstractNamespaceMappings() { + this.resolver = NamePathResolverImpl.create(this); + } + + //----------------------------< NamespaceMappings >------------------------- + + /** + * {@inheritDoc} + */ + public String translateName(Name qName) + throws IllegalNameException { + try { + return resolver.getJCRName(qName); + } catch (NamespaceException e) { + // should never happen actually, because we create yet unknown + // uri mappings on the fly. + throw new IllegalNameException("Internal error.", e); + } + } + + /** + * {@inheritDoc} + */ + public String translatePath(Path path) throws IllegalNameException { + try { + return resolver.getJCRPath(path); + } catch (NamespaceException e) { + // should never happen actually, because we create yet unknown + // uri mappings on the fly. + throw new IllegalNameException("Internal error.", e); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractQueryHits.java new file mode 100644 index 00000000000..73fccc7e662 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractQueryHits.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.core.query.lucene; + +import java.io.IOException; + +/** + * AbstractQueryHits serves as a base class for {@link QueryHits} + * implementations. + */ +public abstract class AbstractQueryHits implements QueryHits { + + /** + * This default implementation does nothing. + */ + public void close() throws IOException { + } + + /** + * Provides a default implementation: + *

    +     * while (n-- > 0) {
    +     *     if (nextScoreNode() == null) {
    +     *         return;
    +     *     }
    +     * }
    +     * 
    + * Sub classes may overwrite this method and implement are more efficient + * way to skip hits. + * + * @param n the number of hits to skip. + * @throws IOException if an error occurs while skipping. + */ + public void skip(int n) throws IOException { + while (n-- > 0) { + if (nextScoreNode() == null) { + return; + } + } + } + + /** + * This default implementation returns -1. + * + * @return -1. + */ + public int getSize() { + return -1; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractQueryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractQueryImpl.java new file mode 100644 index 00000000000..9b10f6c5da1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractQueryImpl.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.core.query.lucene; + +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.core.query.ExecutableQuery; +import org.apache.jackrabbit.core.query.PropertyTypeRegistry; +import org.apache.jackrabbit.core.session.SessionContext; + +/** + * AbstractQueryImpl provides a base class for executable queries + * based on {@link SearchIndex}. + */ +public abstract class AbstractQueryImpl implements ExecutableQuery { + + /** + * Component context of the current session + */ + protected final SessionContext sessionContext; + + /** + * The actual search index + */ + protected final SearchIndex index; + + /** + * The property type registry for type lookup. + */ + protected final PropertyTypeRegistry propReg; + + /** + * If true the default ordering of the result nodes is in + * document order. + */ + private boolean documentOrder = true; + + protected final PerQueryCache cache = new PerQueryCache(); + + /** + * Creates a new query instance from a query string. + * + * @param sessionContext component context of the current session + * @param index the search index. + * @param propReg the property type registry. + */ + public AbstractQueryImpl( + SessionContext sessionContext, SearchIndex index, + PropertyTypeRegistry propReg) { + this.sessionContext = sessionContext; + this.index = index; + this.propReg = propReg; + } + + /** + * If set true the result nodes will be in document order + * per default (if no order by clause is specified). If set to + * false the result nodes are returned in whatever sequence + * the index has stored the nodes. That sequence is stable over multiple + * invocations of the same query, but will change when nodes get added or + * removed from the index. + *

    + * The default value for this property is true. + * @return the current value of this property. + */ + public boolean getRespectDocumentOrder() { + return documentOrder; + } + + /** + * Sets a new value for this property. + * + * @param documentOrder if true the result nodes are in + * document order per default. + * + * @see #getRespectDocumentOrder() + */ + public void setRespectDocumentOrder(boolean documentOrder) { + this.documentOrder = documentOrder; + } + + /** + * @return the query object model factory. + * @throws RepositoryException if an error occurs. + */ + protected QueryObjectModelFactory getQOMFactory() + throws RepositoryException { + Workspace workspace = sessionContext.getSessionImpl().getWorkspace(); + return workspace.getQueryManager().getQOMFactory(); + } + + /** + * Returns true if this query node needs items under + * /jcr:system to be queried. + * + * @return true if this query node needs content under + * /jcr:system to be queried; false otherwise. + */ + public abstract boolean needsSystemTree(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractWeight.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractWeight.java new file mode 100644 index 00000000000..9ddc2619e3f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AbstractWeight.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.core.query.lucene; + +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.index.IndexReader; + +import java.io.IOException; + +/** + * AbstractWeight implements base functionality for custom lucene + * weights in jackrabbit. + */ +@SuppressWarnings("serial") +abstract class AbstractWeight extends Weight { + + /** + * The searcher for this weight. + */ + protected final Searcher searcher; + + /** + * Creates a new AbstractWeight for the given + * searcher. + * + * @param searcher the searcher instance for this weight. + */ + public AbstractWeight(Searcher searcher) { + this.searcher = searcher; + } + + /** + * Abstract factory method for crating a scorer instance for the + * specified reader. + * + * @param reader the index reader the created scorer instance should use + * @return the scorer instance + * @throws IOException if an error occurs while reading from the index + */ + protected abstract Scorer createScorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException; + + /** + * {@inheritDoc} + *

    + * Returns a {@link MultiScorer} if the passed reader is of + * type {@link MultiIndexReader}. + */ + public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + if (reader instanceof MultiIndexReader) { + MultiIndexReader mir = (MultiIndexReader) reader; + IndexReader[] readers = mir.getIndexReaders(); + int[] starts = new int[readers.length + 1]; + int maxDoc = 0; + for (int i = 0; i < readers.length; i++) { + starts[i] = maxDoc; + maxDoc += readers[i].maxDoc(); + } + + starts[readers.length] = maxDoc; + Scorer[] scorers = new Scorer[readers.length]; + for (int i = 0; i < readers.length; i++) { + scorers[i] = scorer(readers[i], scoreDocsInOrder, false); + } + + return new MultiScorer(searcher.getSimilarity(), scorers, starts); + } else { + return createScorer(reader, scoreDocsInOrder, topScorer); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AggregateRule.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AggregateRule.java new file mode 100644 index 00000000000..d73e920e02c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AggregateRule.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.core.query.lucene; + +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.PropertyState; + +import javax.jcr.RepositoryException; + +/** + * AggregateRule defines a configuration for a node index + * aggregate. It defines rules for items that should be included in the node + * scope index of an ancestor. Per default the values of properties are only + * added to the node scope index of the parent node. + */ +public interface AggregateRule { + + /** + * Returns root node state for the indexing aggregate where + * nodeState belongs to. + * + * @param nodeState + * @return the root node state of the indexing aggregate or + * null if nodeState does not belong to an + * indexing aggregate. + * @throws ItemStateException if an error occurs. + * @throws RepositoryException if an error occurs. + */ + NodeState getAggregateRoot(NodeState nodeState) + throws ItemStateException, RepositoryException; + + /** + * recursive aggregation (for same type nodes) limit. embedded aggregation + * of nodes that have the same type can go only this levels up. + * + * A value eq to 0 gives unlimited aggregation. + */ + long getRecursiveAggregationLimit(); + + /** + * Returns the node states that are part of the indexing aggregate of the + * nodeState. + * + * @param nodeState a node state + * @return the node states that are part of the indexing aggregate of + * nodeState. Returns null if this + * aggregate does not apply to nodeState. + * @throws ItemStateException if an error occurs. + */ + NodeState[] getAggregatedNodeStates(NodeState nodeState) + throws ItemStateException; + + /** + * Returns the property states that are part of the indexing aggregate of + * the nodeState. + * + * @param nodeState a node state + * @return the property states that are part of the indexing aggregate of + * nodeState. Returns null if this + * aggregate does not apply to nodeState. + * @throws ItemStateException if an error occurs. + */ + public PropertyState[] getAggregatedPropertyStates(NodeState nodeState) + throws ItemStateException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AggregateRuleImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AggregateRuleImpl.java new file mode 100644 index 00000000000..b6303b7e7c2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/AggregateRuleImpl.java @@ -0,0 +1,532 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.util.Text; +import org.w3c.dom.CharacterData; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * AggregateRule defines a configuration for a node index + * aggregate. It defines rules for items that should be included in the node + * scope index of an ancestor. Per default the values of properties are only + * added to the node scope index of the parent node. + */ +class AggregateRuleImpl implements AggregateRule { + + /** + * A name resolver for parsing QNames in the configuration. + */ + private final NameResolver resolver; + + /** + * The node type of the root node of the indexing aggregate. + */ + private final Name nodeTypeName; + + /** + * The node includes of this indexing aggregate. + */ + private final NodeInclude[] nodeIncludes; + + /** + * The property includes of this indexing aggregate. + */ + private final PropertyInclude[] propertyIncludes; + + /** + * The item state manager to retrieve additional item states. + */ + private final ItemStateManager ism; + + /** + * A hierarchy resolver for the item state manager. + */ + private final HierarchyManager hmgr; + + /** + * recursive aggregation (for same type nodes) default value. + */ + private static final boolean RECURSIVE_AGGREGATION_DEFAULT = false; + + /** + * flag to enable recursive aggregation (for same type nodes). + */ + private final boolean recursiveAggregation; + + /** + * recursive aggregation (for same type nodes) limit default value. + */ + + protected static final long RECURSIVE_AGGREGATION_LIMIT_DEFAULT = 100; + + /** + * recursive aggregation (for same type nodes) limit. embedded aggregation + * of nodes that have the same type can go only this levels up. + * + * A value eq to 0 gives unlimited aggregation. + */ + private final long recursiveAggregationLimit; + + /** + * Creates a new indexing aggregate using the given config. + * + * @param config the configuration for this indexing aggregate. + * @param resolver the name resolver for parsing Names within the config. + * @param ism the item state manager of the workspace. + * @param hmgr a hierarchy manager for the item state manager. + * @throws MalformedPathException if a path in the configuration is + * malformed. + * @throws IllegalNameException if a node type name contains illegal + * characters. + * @throws NamespaceException if a node type contains an unknown + * prefix. + * @throws RepositoryException If another error occurs. + */ + AggregateRuleImpl(Node config, + NameResolver resolver, + ItemStateManager ism, + HierarchyManager hmgr) throws MalformedPathException, + IllegalNameException, NamespaceException, RepositoryException { + this.resolver = resolver; + this.nodeTypeName = getNodeTypeName(config); + this.nodeIncludes = getNodeIncludes(config); + this.propertyIncludes = getPropertyIncludes(config); + this.ism = ism; + this.hmgr = hmgr; + this.recursiveAggregation = getRecursiveAggregation(config); + this.recursiveAggregationLimit = getRecursiveAggregationLimit(config); + } + + /** + * Returns root node state for the indexing aggregate where + * nodeState belongs to. + * + * @param nodeState the node state. + * @return the root node state of the indexing aggregate or + * null if nodeState does not belong to an + * indexing aggregate. + * @throws ItemStateException if an error occurs. + * @throws RepositoryException if an error occurs. + */ + public NodeState getAggregateRoot(NodeState nodeState) + throws ItemStateException, RepositoryException { + for (NodeInclude nodeInclude : nodeIncludes) { + NodeState aggregateRoot = nodeInclude.matches(nodeState); + if (aggregateRoot != null && aggregateRoot.getNodeTypeName().equals(nodeTypeName)) { + boolean sameNodeTypeAsRoot = nodeState.getNodeTypeName().equals(aggregateRoot.getNodeTypeName()); + if(!sameNodeTypeAsRoot || (sameNodeTypeAsRoot && recursiveAggregation)){ + return aggregateRoot; + } + } + } + + // check property includes + for (PropertyInclude propertyInclude : propertyIncludes) { + NodeState aggregateRoot = propertyInclude.matches(nodeState); + if (aggregateRoot != null && aggregateRoot.getNodeTypeName().equals(nodeTypeName)) { + boolean sameNodeTypeAsRoot = nodeState.getNodeTypeName().equals(aggregateRoot.getNodeTypeName()); + if(!sameNodeTypeAsRoot || (sameNodeTypeAsRoot && recursiveAggregation)){ + return aggregateRoot; + } + } + } + return null; + } + + /** + * Returns the node states that are part of the indexing aggregate of the + * nodeState. + * + * @param nodeState a node state + * @return the node states that are part of the indexing aggregate of + * nodeState. Returns null if this + * aggregate does not apply to nodeState. + * @throws ItemStateException if an error occurs. + */ + public NodeState[] getAggregatedNodeStates(NodeState nodeState) + throws ItemStateException { + if (nodeState.getNodeTypeName().equals(nodeTypeName)) { + List nodeStates = new ArrayList(); + for (NodeInclude nodeInclude : nodeIncludes) { + for (NodeState childNs : nodeInclude.resolve(nodeState)) { + boolean sameNodeTypeAsRoot = nodeState.getNodeTypeName().equals(childNs.getNodeTypeName()); + if (!sameNodeTypeAsRoot || (sameNodeTypeAsRoot && recursiveAggregation)) { + nodeStates.add(childNs); + } + } + } + if (nodeStates.size() > 0) { + return nodeStates.toArray(new NodeState[nodeStates.size()]); + } + } + return null; + } + + /** + * {@inheritDoc} + */ + public PropertyState[] getAggregatedPropertyStates(NodeState nodeState) + throws ItemStateException { + if (nodeState.getNodeTypeName().equals(nodeTypeName)) { + List propStates = new ArrayList(); + for (PropertyInclude propertyInclude : propertyIncludes) { + propStates.addAll(Arrays.asList(propertyInclude.resolvePropertyStates(nodeState))); + } + if (propStates.size() > 0) { + return propStates.toArray(new PropertyState[propStates.size()]); + } + } + return null; + } + + /** + * {@inheritDoc} + */ + public long getRecursiveAggregationLimit() { + return recursiveAggregationLimit; + } + + //---------------------------< internal >----------------------------------- + + /** + * Reads the node type of the root node of the indexing aggregate. + * + * @param config the configuration. + * @return the name of the node type. + * @throws IllegalNameException if the node type name contains illegal + * characters. + * @throws NamespaceException if the node type contains an unknown + * prefix. + */ + private Name getNodeTypeName(Node config) + throws IllegalNameException, NamespaceException { + String ntString = config.getAttributes().getNamedItem("primaryType").getNodeValue(); + return resolver.getQName(ntString); + } + + /** + * Creates node includes defined in the config. + * + * @param config the indexing aggregate configuration. + * @return the node includes defined in the config. + * @throws MalformedPathException if a path in the configuration is + * malformed. + * @throws IllegalNameException if the node type name contains illegal + * characters. + * @throws NamespaceException if the node type contains an unknown + * prefix. + */ + private NodeInclude[] getNodeIncludes(Node config) + throws MalformedPathException, IllegalNameException, NamespaceException { + List includes = new ArrayList(); + NodeList childNodes = config.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node n = childNodes.item(i); + if (n.getNodeName().equals("include")) { + Name ntName = null; + Node ntAttr = n.getAttributes().getNamedItem("primaryType"); + if (ntAttr != null) { + ntName = resolver.getQName(ntAttr.getNodeValue()); + } + PathBuilder builder = new PathBuilder(); + for (String element : Text.explode(getTextContent(n), '/')) { + if (element.equals("*")) { + builder.addLast(NameConstants.ANY_NAME); + } else { + builder.addLast(resolver.getQName(element)); + } + } + includes.add(new NodeInclude(builder.getPath(), ntName)); + } + } + return includes.toArray(new NodeInclude[includes.size()]); + } + + /** + * Creates property includes defined in the config. + * + * @param config the indexing aggregate configuration. + * @return the property includes defined in the config. + * @throws MalformedPathException if a path in the configuration is + * malformed. + * @throws IllegalNameException if the node type name contains illegal + * characters. + * @throws NamespaceException if the node type contains an unknown + * prefix. + * @throws RepositoryException If the PropertyInclude cannot be builded + * due to unknown ancestor relationship. + */ + private PropertyInclude[] getPropertyIncludes(Node config) throws + MalformedPathException, IllegalNameException, NamespaceException, + RepositoryException { + List includes = new ArrayList(); + NodeList childNodes = config.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node n = childNodes.item(i); + if (n.getNodeName().equals("include-property")) { + PathBuilder builder = new PathBuilder(); + for (String element : Text.explode(getTextContent(n), '/')) { + if (element.equals("*")) { + throw new IllegalNameException("* not supported in include-property"); + } + builder.addLast(resolver.getQName(element)); + } + includes.add(new PropertyInclude(builder.getPath())); + } + } + return includes.toArray(new PropertyInclude[includes.size()]); + } + + private boolean getRecursiveAggregation(Node config) { + Node rAttr = config.getAttributes().getNamedItem("recursive"); + if (rAttr == null) { + return RECURSIVE_AGGREGATION_DEFAULT; + } + return Boolean.valueOf(rAttr.getNodeValue()); + } + + private long getRecursiveAggregationLimit(Node config) + throws RepositoryException { + Node rAttr = config.getAttributes().getNamedItem("recursiveLimit"); + if (rAttr == null) { + return RECURSIVE_AGGREGATION_LIMIT_DEFAULT; + } + try { + return Long.valueOf(rAttr.getNodeValue()); + } catch (NumberFormatException e) { + throw new RepositoryException( + "Unable to read indexing configuration (recursiveLimit).", + e); + } + } + + //---------------------------< internal >----------------------------------- + + /** + * @param node a node. + * @return the text content of the node. + */ + private static String getTextContent(Node node) { + StringBuffer content = new StringBuffer(); + NodeList nodes = node.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + Node n = nodes.item(i); + if (n.getNodeType() == Node.TEXT_NODE) { + content.append(((CharacterData) n).getData()); + } + } + return content.toString(); + } + + private abstract class AbstractInclude { + + /** + * Optional node type name. + */ + protected final Name nodeTypeName; + + /** + * A relative path pattern. + */ + protected final Path pattern; + + /** + * Creates a new rule with a relative path pattern and an optional node + * type name. + * + * @param nodeTypeName node type name or null if all node + * types are allowed. + * @param pattern a relative path pattern. + */ + AbstractInclude(Path pattern, Name nodeTypeName) { + this.nodeTypeName = nodeTypeName; + this.pattern = pattern; + } + + /** + * If the given nodeState matches this rule the root node + * state of the indexing aggregate is returned. + * + * @param nodeState a node state. + * @return the root node state of the indexing aggregate or + * null if nodeState does not belong + * to an indexing aggregate defined by this rule. + * @throws ItemStateException if an error occurs while accessing node + * states. + * @throws RepositoryException if another error occurs. + */ + NodeState matches(NodeState nodeState) + throws ItemStateException, RepositoryException { + // first check node type + if (nodeTypeName == null + || nodeState.getNodeTypeName().equals(nodeTypeName)) { + // check pattern + Path.Element[] elements = pattern.getElements(); + for (int e = elements.length - 1; e >= 0; e--) { + NodeId parentId = nodeState.getParentId(); + if (parentId == null) { + // nodeState is root node + return null; + } + NodeState parent = (NodeState) ism.getItemState(parentId); + if (elements[e].getName().getLocalName().equals("*")) { + // match any parent + nodeState = parent; + } else { + // check name + Name name = hmgr.getName(nodeState.getId()); + if (elements[e].getName().equals(name)) { + nodeState = parent; + } else { + return null; + } + } + } + // if we get here nodeState became the root + // of the indexing aggregate and is valid + return nodeState; + } + return null; + } + + //-----------------------------< internal >----------------------------- + + /** + * Recursively resolves node states along the path {@link #pattern}. + * + * @param nodeState the current node state. + * @param collector resolved node states are collected using the list. + * @param offset the current path element offset into the path + * pattern. + * @throws ItemStateException if an error occurs while accessing node + * states. + */ + protected void resolve(NodeState nodeState, List collector, int offset) + throws ItemStateException { + Name currentName = pattern.getElements()[offset].getName(); + List cne; + if (currentName.getLocalName().equals("*")) { + // matches all + cne = nodeState.getChildNodeEntries(); + } else { + cne = nodeState.getChildNodeEntries(currentName); + } + if (pattern.getLength() - 1 == offset) { + // last segment -> add to collector if node type matches + for (ChildNodeEntry entry : cne) { + NodeState ns = (NodeState) ism.getItemState(entry.getId()); + if (nodeTypeName == null || ns.getNodeTypeName().equals(nodeTypeName)) { + collector.add(ns); + } + } + } else { + // traverse + offset++; + for (ChildNodeEntry entry : cne) { + NodeId id = entry.getId(); + resolve((NodeState) ism.getItemState(id), collector, offset); + } + } + } + } + + private final class NodeInclude extends AbstractInclude { + + /** + * Creates a new node include with a relative path pattern and an + * optional node type name. + * + * @param nodeTypeName node type name or null if all node + * types are allowed. + * @param pattern a relative path pattern. + */ + NodeInclude(Path pattern, Name nodeTypeName) { + super(pattern, nodeTypeName); + } + + /** + * Resolves the nodeState using this rule. + * + * @param nodeState the root node of the enclosing indexing aggregate. + * @return the descendant node states as defined by this rule. + * @throws ItemStateException if an error occurs while resolving the + * node states. + */ + NodeState[] resolve(NodeState nodeState) throws ItemStateException { + List nodeStates = new ArrayList(); + resolve(nodeState, nodeStates, 0); + return nodeStates.toArray(new NodeState[nodeStates.size()]); + } + } + + private final class PropertyInclude extends AbstractInclude { + + private final Name propertyName; + + PropertyInclude(Path pattern) + throws RepositoryException { + super(pattern.getAncestor(1), null); + this.propertyName = pattern.getName(); + } + + /** + * Resolves the nodeState using this rule. + * + * @param nodeState the root node of the enclosing indexing aggregate. + * @return the descendant property states as defined by this rule. + * @throws ItemStateException if an error occurs while resolving the + * property states. + */ + PropertyState[] resolvePropertyStates(NodeState nodeState) + throws ItemStateException { + List nodeStates = new ArrayList(); + resolve(nodeState, nodeStates, 0); + List propStates = new ArrayList(); + for (NodeState state : nodeStates) { + if (state.hasPropertyName(propertyName)) { + PropertyId propId = new PropertyId(state.getNodeId(), propertyName); + propStates.add((PropertyState) ism.getItemState(propId)); + } + } + return propStates.toArray(new PropertyState[propStates.size()]); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingIndexReader.java new file mode 100644 index 00000000000..ceade7110f8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingIndexReader.java @@ -0,0 +1,721 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.collections.map.LRUMap; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.FieldSelector; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.FilterIndexReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.util.ReaderUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements an IndexReader that maintains caches to resolve + * {@link #getParent(int, BitSet)} calls efficiently. + *

    + */ +class CachingIndexReader extends FilterIndexReader { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(CachingIndexReader.class); + + /** + * The number of nodes that are processed in a batch when the hierarchy + * cache is initialized. The value is 400'000, which will limit the + * temporary memory usage to initialize the hierarchy cache of a segment + * to 64MB (-> 170B * 400k) + */ + private static final int MAX_CACHE_INIT_BATCH_SIZE = 400 * 1000; + + /** + * The current value of the global creation tick counter. + */ + private static long currentTick; + + /** + * BitSet where bits that correspond to document numbers are set for + * shareable nodes. + */ + private final BitSet shareableNodes; + + /** + * Cache of nodes parent relation. If an entry in the array is >= 0, + * then that means the node with the document number = array-index has the + * node with the value at that position as parent. + */ + private final int[] inSegmentParents; + + /** + * Cache of nodes parent relation that point to a foreign index segment. + */ + private final Map foreignParentDocIds = new ConcurrentHashMap(); + + /** + * Initializes the {@link #inSegmentParents} and {@link #foreignParentDocIds} + * caches. + */ + private final CacheInitializer cacheInitializer; + + /** + * Tick when this index reader was created. + */ + private final long creationTick = getNextCreationTick(); + + /** + * Document number cache if available. May be null. + */ + private final DocNumberCache cache; + + /** + * Maps document number to node id. + */ + private final Map docNumber2id; + + /** + * A cache of TermDocs that are regularly read from the index. + */ + private final TermDocsCache termDocsCache; + + /** + * Creates a new CachingIndexReader based on + * delegatee + * + * @param delegatee the base IndexReader. + * @param cache a document number cache, or null if not + * available to this reader. + * @param initCache if the parent caches should be initialized + * when this index reader is constructed. + * @throws IOException if an error occurs while reading from the index. + */ + @SuppressWarnings("unchecked") + CachingIndexReader(IndexReader delegatee, + DocNumberCache cache, + boolean initCache) + throws IOException { + super(delegatee); + this.cache = cache; + this.inSegmentParents = new int[delegatee.maxDoc()]; + Arrays.fill(this.inSegmentParents, -1); + this.shareableNodes = initShareableNodes(delegatee); + this.cacheInitializer = new CacheInitializer(delegatee); + if (initCache) { + cacheInitializer.run(); + } + // limit cache to 1% of maxDoc(), but at least 10. + this.docNumber2id = Collections.synchronizedMap( + new LRUMap(Math.max(10, delegatee.maxDoc() / 100))); + this.termDocsCache = new TermDocsCache(delegatee, FieldNames.PROPERTIES); + } + + private BitSet initShareableNodes(IndexReader delegatee) throws IOException { + BitSet shareableNodes = new BitSet(); + TermDocs tDocs = delegatee.termDocs(new Term(FieldNames.SHAREABLE_NODE, + "")); + try { + while (tDocs.next()) { + shareableNodes.set(tDocs.doc()); + } + } finally { + tDocs.close(); + } + return shareableNodes; + } + + /** + * Returns the DocId of the parent of n or + * {@link DocId#NULL} if n does not have a parent + * (n is the root node). + * + * @param n the document number. + * @param deleted the documents that should be regarded as deleted. + * @return the DocId of n's parent. + * @throws IOException if an error occurs while reading from the index. + */ + DocId getParent(int n, BitSet deleted) throws IOException { + DocId parent; + boolean existing = false; + int parentDocNum = inSegmentParents[n]; + if (parentDocNum != -1) { + parent = DocId.create(parentDocNum); + } else { + parent = foreignParentDocIds.get(n); + } + + if (parent != null) { + existing = true; + + // check if valid and reset if necessary + if (!parent.isValid(deleted)) { + if (log.isDebugEnabled()) { + log.debug(parent + " not valid anymore."); + } + parent = null; + } + } + + if (parent == null) { + int plainDocId = -1; + Document doc = document(n, FieldSelectors.UUID_AND_PARENT); + String[] parentUUIDs = doc.getValues(FieldNames.PARENT); + if (parentUUIDs.length == 0 || parentUUIDs[0].length() == 0) { + // root node + parent = DocId.NULL; + } else { + if (shareableNodes.get(n)) { + parent = DocId.create(parentUUIDs); + } else { + if (!existing) { + Term id = TermFactory.createUUIDTerm(parentUUIDs[0]); + TermDocs docs = termDocs(id); + try { + while (docs.next()) { + if (!deleted.get(docs.doc())) { + plainDocId = docs.doc(); + parent = DocId.create(plainDocId); + break; + } + } + } finally { + docs.close(); + } + } + // if still null, then parent is not in this index, or existing + // DocId was invalid. thus, only allowed to create DocId from uuid + if (parent == null) { + parent = DocId.create(parentUUIDs[0]); + } + } + } + + // finally put to cache + if (plainDocId != -1) { + // PlainDocId + inSegmentParents[n] = plainDocId; + } else { + // UUIDDocId + foreignParentDocIds.put(n, parent); + if (existing) { + // there was an existing parent reference in + // inSegmentParents, which was invalid and is replaced + // with a UUIDDocId (points to a foreign segment). + // mark as unknown + inSegmentParents[n] = -1; + } + } + } + return parent; + } + + /** + * Returns the tick value when this reader was created. + * + * @return the creation tick for this reader. + */ + public long getCreationTick() { + return creationTick; + } + + //--------------------< FilterIndexReader overwrites >---------------------- + + @Override + public IndexReader[] getSequentialSubReaders() { + return null; + } + + @Override + public FieldInfos getFieldInfos() { + return ReaderUtil.getMergedFieldInfos(in); + } + + /** + * Uses the {@link #docNumber2id} cache for document lookups that are only + * interested in the {@link FieldSelectors#UUID}. + * + * @param n the document number. + * @param fieldSelector the field selector. + * @return the document. + * @throws CorruptIndexException if the index is corrupt. + * @throws IOException if an error occurs while reading from the index. + */ + public Document document(int n, FieldSelector fieldSelector) + throws CorruptIndexException, IOException { + if (fieldSelector == FieldSelectors.UUID) { + Document doc; + NodeId id = docNumber2id.get(n); + if (id == null) { + doc = super.document(n, fieldSelector); + id = new NodeId(doc.get(FieldNames.UUID)); + docNumber2id.put(n, id); + } else { + doc = new Document(); + doc.add(new IDField(id)); + } + return doc; + } else { + return super.document(n, fieldSelector); + } + } + + /** + * If the field of term is {@link FieldNames#UUID} this + * CachingIndexReader returns a TermDocs instance + * with a cached document id. If term has any other field + * the call is delegated to the base IndexReader.
    + * If term is for a {@link FieldNames#UUID} field and this + * CachingIndexReader does not have such a document, + * {@link EmptyTermDocs#INSTANCE} is returned. + * + * @param term the term to start the TermDocs enumeration. + * @return a TermDocs instance. + * @throws IOException if an error occurs while reading from the index. + */ + public TermDocs termDocs(Term term) throws IOException { + if (term != null && term.field() == FieldNames.UUID) { + // check cache if we have one + if (cache != null) { + DocNumberCache.Entry e = cache.get(term.text()); + if (e != null) { + // check if valid + // the cache may contain entries from a different reader + // with the same uuid. that happens when a node is updated + // and is reindexed. the node 'travels' from an older index + // to a newer one. the cache will still contain a cache + // entry from the old until it is overwritten by the + // newer index. + if (e.creationTick == creationTick && !isDeleted(e.doc)) { + return new SingleTermDocs(e.doc); + } + } + + // not in cache or invalid + TermDocs docs = in.termDocs(term); + try { + if (docs.next()) { + // put to cache + cache.put(term.text(), this, docs.doc()); + // and return + return new SingleTermDocs(docs.doc()); + } else { + return EmptyTermDocs.INSTANCE; + } + } finally { + docs.close(); + } + } + } + return termDocsCache.termDocs(term); + } + + /** + * {@inheritDoc} + */ + protected void doClose() throws IOException { + try { + cacheInitializer.waitUntilStopped(); + } catch (InterruptedException e) { + // ignore + } + super.doClose(); + } + + //----------------------< internal >---------------------------------------- + + /** + * Returns the next creation tick value. + * + * @return the next creation tick value. + */ + private static long getNextCreationTick() { + synchronized (CachingIndexReader.class) { + return currentTick++; + } + } + + /** + * Initializes the {@link CachingIndexReader#inSegmentParents} and + * {@link CachingIndexReader#foreignParentDocIds} caches. + */ + private class CacheInitializer implements Runnable { + + /** + * The {@link #inSegmentParents} is persisted using this filename. + */ + private static final String FILE_CACHE_NAME_ARRAY = "cache.inSegmentParents"; + + /** + * From where to read. + */ + private final IndexReader reader; + + /** + * Set to true while this initializer does its work. + */ + private boolean running = false; + + /** + * Set to true when this index reader is about to be closed. + */ + private volatile boolean stopRequested = false; + + /** + * Creates a new initializer with the given reader. + * @param reader + * an index reader. + */ + public CacheInitializer(IndexReader reader) { + this.reader = reader; + } + + /** + * Initializes the cache. + */ + public void run() { + synchronized (this) { + running = true; + } + try { + if (stopRequested) { + // immediately return when stop is requested + return; + } + boolean initCacheFromFile = loadCacheFromFile(); + if (!initCacheFromFile) { + // file-based cache is not available, load from the + // repository + log.debug("persisted cache is not available, will load directly from the repository."); + initializeParents(reader); + } + } catch (Exception e) { + // only log warn message during regular operation + if (!stopRequested) { + log.warn("Error initializing parents cache.", e); + } + } finally { + synchronized (this) { + running = false; + notifyAll(); + } + } + } + + /** + * Waits until this cache initializer is stopped. + * + * @throws InterruptedException if the current thread is interrupted. + */ + public void waitUntilStopped() throws InterruptedException { + stopRequested = true; + synchronized (this) { + while (running) { + wait(); + } + } + } + + /** + * Initializes the {@link CachingIndexReader#inSegmentParents} and + * {@link CachingIndexReader#foreignParentDocIds} caches. + * + * @param reader the underlying index reader. + * @throws IOException if an error occurs while reading from the index. + */ + private void initializeParents(IndexReader reader) throws IOException { + double foreignParents = 0; + long time = System.currentTimeMillis(); + + // initialize in multiple passes with + // a fixed number of nodes at a time + final Term[] startUUID = new Term[]{TermFactory.createUUIDTerm("")}; + + for (;;) { + final Map docs = new HashMap(); + final Map parents = new HashMap(); + + if (startUUID[0].text().length() != 0) { + // force reading the next uuid after startUUID + startUUID[0] = TermFactory.createUUIDTerm(startUUID[0].text() + "_"); + } + // read UUIDs + collectTermDocs(reader, startUUID[0], new TermDocsCollector() { + public boolean collect(Term term, TermDocs tDocs) throws IOException { + // remember start term for next batch + startUUID[0] = term; + if (docs.size() >= MAX_CACHE_INIT_BATCH_SIZE) { + return false; + } + NodeId id = new NodeId(term.text()); + while (tDocs.next()) { + int doc = tDocs.doc(); + // skip shareable nodes + if (!shareableNodes.get(doc)) { + NodeInfo info = new NodeInfo(doc, id); + docs.put(doc, info); + } + } + return true; + } + }); + + if (docs.isEmpty()) { + // no more nodes to initialize, persist cache to file + saveCacheToFile(); + break; + } + + // read PARENTs (full scan) + collectTermDocs(reader, new Term(FieldNames.PARENT, "0"), new TermDocsCollector() { + public boolean collect(Term term, TermDocs tDocs) throws IOException { + NodeId id = new NodeId(term.text()); + while (tDocs.next()) { + Integer docId = tDocs.doc(); + NodeInfo info = docs.get(docId); + if (info == null) { + // shareable node, see above + // or cache init is batched + } else { + info.parent = id; + docs.remove(docId); + docs.put(info.id, info); + parents.put(id, null); + } + } + return true; + } + }); + + // scan UUIDs again to get document numbers for parents + collectTermDocs(reader, TermFactory.createUUIDTerm(""), new TermDocsCollector() { + public boolean collect(Term term, TermDocs tDocs) throws IOException { + NodeId id = new NodeId(term.text()); + while (tDocs.next()) { + int doc = tDocs.doc(); + if (parents.containsKey(id)) { + parents.put(id, doc); + } + } + return true; + } + }); + + if (stopRequested) { + return; + } + + for (NodeInfo info : docs.values()) { + int parentDocId = -1; + NodeInfo parent = docs.get(info.parent); + if (parent != null) { + parentDocId = parent.docId; + } else { + Integer docId = parents.get(info.parent); + if (docId != null) { + parentDocId = docId; + } + } + if (parentDocId != -1) { + inSegmentParents[info.docId] = parentDocId; + } else if (info.parent != null) { + foreignParents++; + foreignParentDocIds.put(info.docId, DocId.create(info.parent)); + } else if (shareableNodes.get(info.docId)) { + Document doc = reader.document(info.docId, FieldSelectors.UUID_AND_PARENT); + foreignParentDocIds.put(info.docId, DocId.create(doc.getValues(FieldNames.PARENT))); + } else { + // no parent -> root node + foreignParentDocIds.put(info.docId, DocId.NULL); + } + } + } + + if (log.isDebugEnabled()) { + NumberFormat nf = NumberFormat.getPercentInstance(); + nf.setMaximumFractionDigits(1); + time = System.currentTimeMillis() - time; + if (inSegmentParents.length > 0) { + foreignParents /= inSegmentParents.length; + } + log.debug("initialized {} DocIds in {} ms, {} foreign parents", + new Object[]{ + inSegmentParents.length, + time, + nf.format(foreignParents) + }); + } + } + + /** + * Collects term docs for a given start term. All terms with the same + * field as start are enumerated. + * + * @param reader the index reader. + * @param start the term where to start the term enumeration. + * @param collector collects the term docs for each term. + * @throws IOException if an error occurs while reading from the index. + */ + private void collectTermDocs(IndexReader reader, + Term start, + TermDocsCollector collector) + throws IOException { + TermDocs tDocs = reader.termDocs(); + try { + TermEnum terms = reader.terms(start); + try { + int count = 0; + do { + Term t = terms.term(); + if (t != null && t.field() == start.field()) { + tDocs.seek(terms); + if (!collector.collect(t, tDocs)) { + // collector indicated break + break; + } + } else { + break; + } + // once in a while check if we should quit + if (++count % 10000 == 0) { + if (stopRequested) { + break; + } + } + } while (terms.next()); + } finally { + terms.close(); + } + } finally { + tDocs.close(); + } + } + + /** + * Persists the cache info {@link #inSegmentParents} to a file: + * {@link #FILE_CACHE_NAME_ARRAY}, for faster init times on startup. + * + * see https://issues.apache.org/jira/browse/JCR-3107 + */ + public void saveCacheToFile() throws IOException { + IndexOutput io = null; + try { + io = reader.directory().createOutput(FILE_CACHE_NAME_ARRAY); + for (int parent : inSegmentParents) { + io.writeInt(parent); + } + } catch (Exception e) { + log.error( + "Error saving " + FILE_CACHE_NAME_ARRAY + ": " + + e.getMessage(), e); + } finally { + if (io != null) { + io.close(); + } + } + } + + /** + * Loads the cache info {@link #inSegmentParents} from the file + * {@link #FILE_CACHE_NAME_ARRAY}. + * + * see https://issues.apache.org/jira/browse/JCR-3107 + * + * @return true if the cache has been initialized of false if the cache + * file does not exist yet, or an error happened + */ + private boolean loadCacheFromFile() throws IOException { + IndexInput ii = null; + try { + long time = System.currentTimeMillis(); + ii = reader.directory().openInput(FILE_CACHE_NAME_ARRAY); + for (int i = 0; i < inSegmentParents.length; i++) { + inSegmentParents[i] = ii.readInt(); + } + log.debug( + "persisted cache initialized {} DocIds in {} ms", + new Object[] { inSegmentParents.length, + System.currentTimeMillis() - time }); + return true; + } catch (FileNotFoundException ignore) { + // expected in the case where the file-based cache has not been + // initialized yet + } catch (IOException ignore) { + log.warn( + "Saved state of CachingIndexReader is corrupt, will try to remove offending file " + + FILE_CACHE_NAME_ARRAY, ignore); + // In the case where is a read error, the cache file is removed + // so it can be recreated after + // the cache loads the data from the repository directly + reader.directory().deleteFile(FILE_CACHE_NAME_ARRAY); + } finally { + if (ii != null) { + ii.close(); + } + } + return false; + } + } + + /** + * Simple interface to collect a term and its term docs. + */ + private interface TermDocsCollector { + + /** + * Called for each term encountered. + * + * @param term the term. + * @param tDocs the term docs of term. + * @return false if the collector does not wish to collect more TermDocs. + * @throws IOException if an error occurs while reading from the index. + */ + boolean collect(Term term, TermDocs tDocs) throws IOException; + } + + private final static class NodeInfo { + + final int docId; + + final NodeId id; + + NodeId parent; + + public NodeInfo(int docId, NodeId id) { + this.docId = docId; + this.id = id; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingMultiIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingMultiIndexReader.java new file mode 100644 index 00000000000..ea3cf22092e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CachingMultiIndexReader.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.core.query.lucene; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.index.MultiReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.IndexReader; + +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; + +/** + * Extends a MultiReader with support for cached TermDocs + * on {@link FieldNames#UUID} field. + */ +public final class CachingMultiIndexReader + extends MultiReader + implements HierarchyResolver, MultiIndexReader { + + /** + * The sub readers. + */ + private ReadOnlyIndexReader[] subReaders; + + /** + * Map of {@link OffsetReader}s, identified by creation tick. + */ + private final Map readersByCreationTick = + new HashMap(); + + /** + * Document number cache if available. May be null. + */ + private final DocNumberCache cache; + + /** + * Reference count. Every time close is called refCount is decremented. If + * refCount drops to zero the underlying readers are closed as well. + */ + private int refCount = 1; + + /** + * Creates a new CachingMultiIndexReader based on sub readers. + * + * @param subReaders the sub readers. + * @param cache the document number cache. + */ + public CachingMultiIndexReader(ReadOnlyIndexReader[] subReaders, + DocNumberCache cache) { + super(subReaders); + this.cache = cache; + this.subReaders = subReaders; + for (int i = 0; i < subReaders.length; i++) { + OffsetReader offsetReader = new OffsetReader(subReaders[i], starts[i]); + readersByCreationTick.put(subReaders[i].getCreationTick(), offsetReader); + } + } + + /** + * {@inheritDoc} + */ + public int[] getParents(int n, int[] docNumbers) throws IOException { + DocId id = getParentDocId(n); + return id.getDocumentNumbers(this, docNumbers); + } + + /** + * Returns the DocId of the parent of n or {@link DocId#NULL} + * if n does not have a parent (n is the root + * node). + * + * @param n the document number. + * @return the DocId of n's parent. + * @throws IOException if an error occurs while reading from the index. + */ + public DocId getParentDocId(int n) throws IOException { + int i = readerIndex(n); + DocId id = subReaders[i].getParent(n - starts[i]); + return id.applyOffset(starts[i]); + } + + /** + * {@inheritDoc} + */ + public TermDocs termDocs(Term term) throws IOException { + if (term != null && term.field() == FieldNames.UUID) { + // check cache + DocNumberCache.Entry e = cache.get(term.text()); + if (e != null) { + // check if valid: + // 1) reader must be in the set of readers + // 2) doc must not be deleted + OffsetReader offsetReader = + readersByCreationTick.get(e.creationTick); + if (offsetReader != null && !offsetReader.reader.isDeleted(e.doc)) { + return new SingleTermDocs(e.doc + offsetReader.offset); + } + } + + // if we get here, entry is either invalid or did not exist + // search through readers + for (int i = 0; i < subReaders.length; i++) { + TermDocs docs = subReaders[i].termDocs(term); + try { + if (docs.next()) { + return new SingleTermDocs(docs.doc() + starts[i]); + } + } finally { + docs.close(); + } + } + } + + return super.termDocs(term); + } + + /** + * Increments the reference count of this reader. Each call to this method + * must later be acknowledged by a call to {@link #release()}. + */ + synchronized void acquire() { + refCount++; + } + + /** + * {@inheritDoc} + */ + public synchronized final void release() throws IOException { + if (--refCount == 0) { + close(); + } + } + + /** + * {@inheritDoc} + */ + protected synchronized void doClose() throws IOException { + for (ReadOnlyIndexReader subReader : subReaders) { + subReader.release(); + } + subReaders = null; + readersByCreationTick.clear(); + } + + //-------------------------< MultiIndexReader >----------------------------- + + /** + * {@inheritDoc} + */ + public IndexReader[] getIndexReaders() { + IndexReader[] readers = new IndexReader[subReaders.length]; + System.arraycopy(subReaders, 0, readers, 0, subReaders.length); + return readers; + } + + /** + * {@inheritDoc} + */ + public ForeignSegmentDocId createDocId(NodeId id) throws IOException { + Term term = TermFactory.createUUIDTerm(id.toString()); + int doc; + long tick; + for (ReadOnlyIndexReader subReader : subReaders) { + TermDocs docs = subReader.termDocs(term); + try { + if (docs.next()) { + doc = docs.doc(); + tick = subReader.getCreationTick(); + return new ForeignSegmentDocId(doc, tick); + } + } finally { + docs.close(); + } + } + return null; + } + + /** + * {@inheritDoc} + */ + public int getDocumentNumber(ForeignSegmentDocId docId) { + OffsetReader r = readersByCreationTick.get(docId.getCreationTick()); + if (r != null && !r.reader.isDeleted(docId.getDocNumber())) { + return r.offset + docId.getDocNumber(); + } + return -1; + } + + //-----------------------< OffsetTermDocs >--------------------------------- + + /** + * Simple helper struct that associates an offset with an IndexReader. + */ + private static final class OffsetReader { + + /** + * The index reader. + */ + private final ReadOnlyIndexReader reader; + + /** + * The reader offset in this multi reader instance. + */ + private final int offset; + + /** + * Creates a new OffsetReader. + * + * @param reader the index reader. + * @param offset the reader offset in a multi reader. + */ + OffsetReader(ReadOnlyIndexReader reader, int offset) { + this.reader = reader; + this.offset = offset; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.java new file mode 100644 index 00000000000..6b21326bdf8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CaseTermQuery.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.core.query.lucene; + +import org.apache.lucene.index.Term; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.FilteredTermEnum; +import org.apache.lucene.util.ToStringUtils; + +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * CaseTermQuery implements a term query which convert the term + * from the index either to upper or lower case before it is matched. + */ +@SuppressWarnings("serial") +abstract class CaseTermQuery extends MultiTermQuery implements TransformConstants { + + /** + * Indicates whether terms from the index should be lower-cased or + * upper-cased. + */ + protected final int transform; + private final Term term; + + CaseTermQuery(Term term, int transform) { + this.term = term; + this.transform = transform; + setRewriteMethod(CONSTANT_SCORE_BOOLEAN_QUERY_REWRITE); + } + + /** + * {@inheritDoc} + */ + @Override + protected FilteredTermEnum getEnum(IndexReader reader) throws IOException { + return new CaseTermEnum(reader); + } + + /** Prints a user-readable version of this query. */ + @Override + public String toString(String field) { + StringBuffer buffer = new StringBuffer(); + if (!term.field().equals(field)) { + buffer.append(term.field()); + buffer.append(':'); + } + buffer.append(term.text()); + buffer.append(ToStringUtils.boost(getBoost())); + return buffer.toString(); + } + + static final class Upper extends CaseTermQuery { + + Upper(Term term) { + super(term, TRANSFORM_UPPER_CASE); + } + } + + static final class Lower extends CaseTermQuery { + + Lower(Term term) { + super(term, TRANSFORM_LOWER_CASE); + } + } + + private final class CaseTermEnum extends FilteredTermEnum { + + CaseTermEnum(IndexReader reader) throws IOException { + // gather all terms that match + // keep them in order and remember the doc frequency as value + final Map orderedTerms = + new LinkedHashMap(); + + // there are always two range scans: one with an initial + // lower case character and another one with an initial upper case + // character + List rangeScans = new ArrayList(2); + int nameLength = FieldNames.getNameLength(term.text()); + String propName = term.text().substring(0, nameLength); + OffsetCharSequence termText = new OffsetCharSequence(nameLength, term.text()); + OffsetCharSequence currentTerm = new OffsetCharSequence(nameLength, term.text(), transform); + + try { + // start with a term using the lower case character for the first + // character of the value. + if (term.text().length() > nameLength) { + // start with initial lower case + StringBuffer lowerLimit = new StringBuffer(propName); + String termStr = termText.toString(); + String upperTermStr = termStr.toUpperCase(); + String lowerTermStr = termStr.toLowerCase(); + + lowerLimit.append(upperTermStr); + lowerLimit.setCharAt(nameLength, Character.toLowerCase(lowerLimit.charAt(nameLength))); + StringBuffer upperLimit = new StringBuffer(propName); + upperLimit.append(lowerTermStr); + rangeScans.add(new RangeScan(reader, + new Term(term.field(), lowerLimit.toString()), + new Term(term.field(), upperLimit.toString()))); + + // second scan with upper case start + lowerLimit = new StringBuffer(propName); + lowerLimit.append(upperTermStr); + upperLimit = new StringBuffer(propName); + upperLimit.append(lowerTermStr); + upperLimit.setCharAt(nameLength, Character.toUpperCase(upperLimit.charAt(nameLength))); + rangeScans.add(new RangeScan(reader, + new Term(term.field(), lowerLimit.toString()), + new Term(term.field(), upperLimit.toString()))); + + } else { + // use term as is + rangeScans.add(new RangeScan(reader, term, term)); + } + + for (TermEnum terms : rangeScans) { + do { + Term t = terms.term(); + if (t != null) { + currentTerm.setBase(t.text()); + int compare = currentTerm.compareTo(termText); + if (compare == 0) { + orderedTerms.put(t, terms.docFreq()); + } + } else { + break; + } + } while (terms.next()); + } + } finally { + for (TermEnum terms : rangeScans) { + try { + terms.close(); + } catch (IOException e) { + // ignore + } + } + } + + final Iterator it = orderedTerms.keySet().iterator(); + + setEnum(new TermEnum() { + + private Term current; + + { + getNext(); + } + + @Override + public boolean next() { + getNext(); + return current != null; + } + + @Override + public Term term() { + return current; + } + + @Override + public int docFreq() { + Integer docFreq = orderedTerms.get(current); + return docFreq != null ? docFreq : 0; + } + + @Override + public void close() { + // nothing to close + } + + private void getNext() { + current = it.hasNext() ? it.next() : null; + } + }); + } + + @Override + protected boolean termCompare(Term term) { + // they all match + return true; + } + + @Override + public float difference() { + return 1.0f; + } + + @Override + protected boolean endEnum() { + // todo correct? + return false; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildAxisQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildAxisQuery.java new file mode 100644 index 00000000000..52aff368ab5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildAxisQuery.java @@ -0,0 +1,729 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector; +import org.apache.jackrabbit.core.query.lucene.hits.AdaptingHits; +import org.apache.jackrabbit.core.query.lucene.hits.Hits; +import org.apache.jackrabbit.core.query.lucene.hits.ScorerHits; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.query.LocationStepQueryNode; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; +import org.apache.lucene.search.Sort; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.HashMap; +import java.util.Map; +import java.util.HashSet; +import java.util.ArrayList; + +/** + * Implements a lucene Query which returns the child nodes of the + * nodes selected by another Query. + */ +@SuppressWarnings("serial") +class ChildAxisQuery extends Query implements JackrabbitQuery { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(ChildAxisQuery.class); + + /** + * Threshold when children calculation is switched to + * {@link HierarchyResolvingChildrenCalculator}. + */ + private static int CONTEXT_SIZE_THRESHOLD = 10; + + /** + * The item state manager containing persistent item states. + */ + private final ItemStateManager itemMgr; + + /** + * The context query + */ + private Query contextQuery; + + /** + * The nameTest to apply on the child axis, or null if all + * child nodes should be selected. + */ + private final Name nameTest; + + /** + * The context position for the selected child node, or + * {@link LocationStepQueryNode#NONE} if no position is specified. + */ + private final int position; + + /** + * The index format version. + */ + private final IndexFormatVersion version; + + /** + * The internal namespace mappings. + */ + private final NamespaceMappings nsMappings; + + /** + * The scorer of the context query + */ + private Scorer contextScorer; + + /** + * The scorer of the name test query + */ + private Scorer nameTestScorer; + + /** + * Creates a new ChildAxisQuery based on a context + * query. + * + * @param itemMgr the item state manager. + * @param context the context for this query. + * @param nameTest a name test or null if any child node is + * selected. + * @param version the index format version. + * @param nsMappings the internal namespace mappings. + */ + ChildAxisQuery(ItemStateManager itemMgr, + Query context, + Name nameTest, + IndexFormatVersion version, + NamespaceMappings nsMappings) { + this(itemMgr, context, nameTest, LocationStepQueryNode.NONE, version, nsMappings); + } + + /** + * Creates a new ChildAxisQuery based on a context + * query. + * + * @param itemMgr the item state manager. + * @param context the context for this query. + * @param nameTest a name test or null if any child node is + * selected. + * @param position the context position of the child node to select. If + * position is {@link LocationStepQueryNode#NONE}, the context + * position of the child node is not checked. + * @param version the index format version. + * @param nsMapping the internal namespace mappings. + */ + ChildAxisQuery(ItemStateManager itemMgr, + Query context, + Name nameTest, + int position, + IndexFormatVersion version, + NamespaceMappings nsMapping) { + this.itemMgr = itemMgr; + this.contextQuery = context; + this.nameTest = nameTest; + this.position = position; + this.version = version; + this.nsMappings = nsMapping; + } + + /** + * @return the context query of this child axis query. + */ + Query getContextQuery() { + return contextQuery; + } + + /** + * @return true if this child axis query matches any child + * node; false otherwise. + */ + boolean matchesAnyChildNode() { + return nameTest == null && position == LocationStepQueryNode.NONE; + } + + /** + * @return the name test or null if none was specified. + */ + Name getNameTest() { + return nameTest; + } + + /** + * @return the position check or {@link LocationStepQueryNode#NONE} is none + * was specified. + */ + int getPosition() { + return position; + } + + /** + * Creates a Weight instance for this query. + * + * @param searcher the Searcher instance to use. + * @return a ChildAxisWeight. + */ + public Weight createWeight(Searcher searcher) { + return new ChildAxisWeight(searcher); + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + contextQuery.extractTerms(terms); + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + Query cQuery = contextQuery.rewrite(reader); + // only try to compact if no position is specified + if (position == LocationStepQueryNode.NONE) { + if (cQuery instanceof DescendantSelfAxisQuery) { + DescendantSelfAxisQuery dsaq = (DescendantSelfAxisQuery) cQuery; + if (dsaq.subQueryMatchesAll()) { + Query sub; + if (nameTest == null) { + sub = new MatchAllDocsQuery(); + } else { + sub = new NameQuery(nameTest, version, nsMappings); + } + return new DescendantSelfAxisQuery(dsaq.getContextQuery(), + sub, dsaq.getMinLevels() + 1).rewrite(reader); + } + } + } + + // if we get here we could not compact the query + if (cQuery == contextQuery) { + return this; + } else { + return new ChildAxisQuery(itemMgr, cQuery, nameTest, + position, version, nsMappings); + } + } + + /** + * {@inheritDoc} + */ + public String toString(String field) { + StringBuffer sb = new StringBuffer(); + sb.append("ChildAxisQuery("); + sb.append(contextQuery); + sb.append(", "); + sb.append(nameTest); + if (position != LocationStepQueryNode.NONE) { + sb.append(", "); + sb.append(position); + } + sb.append(")"); + return sb.toString(); + } + + //-------------------< JackrabbitQuery >------------------------------------ + + /** + * {@inheritDoc} + */ + public QueryHits execute(JackrabbitIndexSearcher searcher, + SessionImpl session, + Sort sort) + throws IOException { + if (sort.getSort().length == 0 && matchesAnyChildNode()) { + Query context = getContextQuery(); + return new ChildNodesQueryHits(searcher.evaluate(context), session); + } else { + return null; + } + } + + //-------------------< ChildAxisWeight >------------------------------------ + + /** + * The Weight implementation for this ChildAxisQuery. + */ + private class ChildAxisWeight extends Weight { + + /** + * The searcher in use + */ + private final Searcher searcher; + + /** + * Creates a new ChildAxisWeight instance using + * searcher. + * + * @param searcher a Searcher instance. + */ + private ChildAxisWeight(Searcher searcher) { + this.searcher = searcher; + } + + /** + * Returns this ChildAxisQuery. + * + * @return this ChildAxisQuery. + */ + public Query getQuery() { + return ChildAxisQuery.this; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public void normalize(float norm) { + } + + /** + * Creates a scorer for this ChildAxisQuery. + * + * @param reader a reader for accessing the index. + * @return a ChildAxisScorer. + * @throws IOException if an error occurs while reading from the index. + */ + @Override + public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, boolean topScorer) throws IOException { + contextScorer = contextQuery.weight(searcher).scorer(reader, scoreDocsInOrder, false); + if (nameTest != null) { + nameTestScorer = new NameQuery(nameTest, version, nsMappings).weight(searcher).scorer(reader, scoreDocsInOrder, false); + } + return new ChildAxisScorer(searcher.getSimilarity(), + reader, (HierarchyResolver) reader); + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(); + } + } + + //----------------------< ChildAxisScorer >--------------------------------- + + /** + * Implements a Scorer for this ChildAxisQuery. + */ + private class ChildAxisScorer extends Scorer { + + /** + * An IndexReader to access the index. + */ + private final IndexReader reader; + + /** + * The HierarchyResolver of the index. + */ + private final HierarchyResolver hResolver; + + /** + * The next document id to return + */ + private int nextDoc = -1; + + /** + * A Hits instance containing all hits + */ + private Hits hits; + + /** + * Creates a new ChildAxisScorer. + * + * @param similarity the Similarity instance to use. + * @param reader for index access. + * @param hResolver the hierarchy resolver of reader. + */ + protected ChildAxisScorer(Similarity similarity, + IndexReader reader, + HierarchyResolver hResolver) { + super(similarity); + this.reader = reader; + this.hResolver = hResolver; + } + + @Override + public int nextDoc() throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + calculateChildren(); + do { + nextDoc = hits.next(); + } while (nextDoc > -1 && !indexIsValid(nextDoc)); + + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + @Override + public int docID() { + return nextDoc; + } + + @Override + public float score() throws IOException { + return 1.0f; + } + + @Override + public int advance(int target) throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + hits.skipTo(target); + nextDoc = NO_MORE_DOCS; + return nextDoc; + } + + calculateChildren(); + nextDoc = hits.skipTo(target); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + + while (nextDoc != NO_MORE_DOCS && !indexIsValid(nextDoc)) { + nextDoc(); + } + return nextDoc; + } + + private void calculateChildren() throws IOException { + if (hits == null) { + + final ChildrenCalculator[] calc = new ChildrenCalculator[1]; + if (nameTestScorer == null) { + // always use simple in that case + calc[0] = new SimpleChildrenCalculator(reader, hResolver); + contextScorer.score(new AbstractHitCollector() { + @Override + protected void collect(int doc, float score) { + calc[0].collectContextHit(doc); + } + }); + } else { + // start simple but switch once threshold is reached + calc[0] = new SimpleChildrenCalculator(reader, hResolver); + contextScorer.score(new AbstractHitCollector() { + + private List docIds = new ArrayList(); + + @Override + protected void collect(int doc, float score) { + calc[0].collectContextHit(doc); + if (docIds != null) { + docIds.add(doc); + if (docIds.size() > CONTEXT_SIZE_THRESHOLD) { + // switch + calc[0] = new HierarchyResolvingChildrenCalculator( + reader, hResolver); + for (int docId : docIds) { + calc[0].collectContextHit(docId); + } + // indicate that we switched + docIds = null; + } + } + } + }); + } + + hits = calc[0].getHits(); + } + } + + private boolean indexIsValid(int i) throws IOException { + if (position != LocationStepQueryNode.NONE) { + Document node = reader.document(i, FieldSelectors.UUID_AND_PARENT); + NodeId parentId = NodeId.valueOf(node.get(FieldNames.PARENT)); + NodeId id = NodeId.valueOf(node.get(FieldNames.UUID)); + try { + NodeState state = (NodeState) itemMgr.getItemState(parentId); + if (nameTest == null) { + // only select this node if it is the child at + // specified position + List childNodes = state.getChildNodeEntries(); + if (position == LocationStepQueryNode.LAST) { + // only select last + if (childNodes.size() == 0 + || !(childNodes.get(childNodes.size() - 1)).getId().equals(id)) { + return false; + } + } else { + if (position < 1 + || childNodes.size() < position + || !(childNodes.get(position - 1)).getId().equals(id)) { + return false; + } + } + } else { + // select the node when its index is equal to + // specified position + if (position == LocationStepQueryNode.LAST) { + // only select last + ChildNodeEntry entry = + state.getChildNodeEntry(id); + if (entry == null) { + // no such child node, probably deleted meanwhile + return false; + } else { + // only use the last one + Name name = entry.getName(); + List childNodes = state.getChildNodeEntries(name); + if (childNodes.size() == 0 + || !(childNodes.get(childNodes.size() - 1)).getId().equals(id)) { + return false; + } + } + } else { + ChildNodeEntry entry = + state.getChildNodeEntry(id); + if (entry == null) { + // no such child node, probably has been deleted meanwhile + return false; + } else { + if (entry.getIndex() != position) { + return false; + } + } + } + } + } catch (ItemStateException e) { + // ignore this node, probably has been deleted meanwhile + return false; + } + } + return true; + } + } + + /** + * Base class to calculate the children for a context query. + */ + private abstract class ChildrenCalculator { + + /** + * The current index reader. + */ + protected final IndexReader reader; + + /** + * The current hierarchy resolver. + */ + protected final HierarchyResolver hResolver; + + /** + * Creates a new children calculator with the given index reader and + * hierarchy resolver. + * + * @param reader the current index reader. + * @param hResolver the current hierarchy resolver. + */ + public ChildrenCalculator(IndexReader reader, + HierarchyResolver hResolver) { + this.reader = reader; + this.hResolver = hResolver; + } + + /** + * Collects a context hit. + * + * @param doc the lucene document number of the context hit. + */ + protected abstract void collectContextHit(int doc); + + /** + * @return the hits that contains the children. + * @throws IOException if an error occurs while reading from the index. + */ + public abstract Hits getHits() throws IOException; + } + + /** + * An implementation of a children calculator using the item state manager. + */ + private final class SimpleChildrenCalculator extends ChildrenCalculator { + + /** + * The context hits. + */ + private final Hits contextHits = new AdaptingHits(); + + /** + * Creates a new simple children calculator. + * + * @param reader the current index reader. + * @param hResolver the current hierarchy resolver. + */ + public SimpleChildrenCalculator(IndexReader reader, + HierarchyResolver hResolver) { + super(reader, hResolver); + } + + /** + * {@inheritDoc} + */ + protected void collectContextHit(int doc) { + contextHits.set(doc); + } + + /** + * {@inheritDoc} + */ + public Hits getHits() throws IOException { + // read the uuids of the context nodes + Map uuids = new HashMap(); + for (int i = contextHits.next(); i > -1; i = contextHits.next()) { + String uuid = reader.document(i, FieldSelectors.UUID).get(FieldNames.UUID); + uuids.put(i, uuid); + } + + // get child node entries for each hit + Hits childrenHits = new AdaptingHits(); + for (String uuid : uuids.values()) { + NodeId id = new NodeId(uuid); + try { + long time = System.currentTimeMillis(); + NodeState state = (NodeState) itemMgr.getItemState(id); + time = System.currentTimeMillis() - time; + log.debug("got NodeState with id {} in {} ms.", id, time); + List entries; + if (nameTest != null) { + entries = state.getChildNodeEntries(nameTest); + } else { + // get all children + entries = state.getChildNodeEntries(); + } + for (ChildNodeEntry entry : entries) { + NodeId childId = entry.getId(); + Term uuidTerm = TermFactory.createUUIDTerm(childId.toString()); + TermDocs docs = reader.termDocs(uuidTerm); + try { + if (docs.next()) { + childrenHits.set(docs.doc()); + } + } finally { + docs.close(); + } + } + } catch (ItemStateException e) { + // does not exist anymore -> ignore + } + } + return childrenHits; + } + } + + /** + * An implementation of a children calculator that uses the hierarchy + * resolver. This implementation requires that + * {@link ChildAxisQuery#nameTestScorer} is non null. + */ + private final class HierarchyResolvingChildrenCalculator + extends ChildrenCalculator { + + /** + * The document numbers of the context hits. + */ + private final Set docIds = new HashSet(); + + /** + * Creates a new hierarchy resolving children calculator. + * + * @param reader the current index reader. + * @param hResolver the current hierarchy resolver. + */ + public HierarchyResolvingChildrenCalculator(IndexReader reader, + HierarchyResolver hResolver) { + super(reader, hResolver); + } + + /** + * {@inheritDoc} + */ + protected void collectContextHit(int doc) { + docIds.add(doc); + } + + /** + * {@inheritDoc} + */ + public Hits getHits() throws IOException { + long time = System.currentTimeMillis(); + Hits childrenHits = new AdaptingHits(); + Hits nameHits = new ScorerHits(nameTestScorer); + int[] docs = new int[1]; + for (int h = nameHits.next(); h > -1; h = nameHits.next()) { + docs = hResolver.getParents(h, docs); + if (docs.length == 1) { + // optimize single value + if (docIds.contains(docs[0])) { + childrenHits.set(h); + } + } else { + for (int i = 0; i < docs.length; i++) { + if (docIds.contains(docs[i])) { + childrenHits.set(h); + } + } + } + } + time = System.currentTimeMillis() - time; + + log.debug("Filtered hits in {} ms.", time); + return childrenHits; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildNodesQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildNodesQueryHits.java new file mode 100644 index 00000000000..aeecafdca6e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ChildNodesQueryHits.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.core.query.lucene; + +import org.apache.jackrabbit.core.SessionImpl; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import java.io.IOException; + +/** + * ChildNodesQueryHits implements query hits that returns the child + * nodes of another given query hits. + */ +public class ChildNodesQueryHits extends AbstractQueryHits { + + /** + * The parent query hits. + */ + private final QueryHits parents; + + /** + * This session that executes the query. + */ + private final SessionImpl session; + + /** + * The current child hits. + */ + private QueryHits childHits; + + /** + * Creates a new ChildNodesQueryHits that returns the child + * nodes of all query hits from the given parents. + * + * @param parents the parent query hits. + * @param session the session that executes the query. + * @throws IOException if an error occurs while reading from + * parents + */ + public ChildNodesQueryHits(QueryHits parents, SessionImpl session) + throws IOException { + this.parents = parents; + this.session = session; + fetchNextChildHits(); + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + if (childHits != null) { + childHits.close(); + } + parents.close(); + } + + /** + * {@inheritDoc} + */ + public ScoreNode nextScoreNode() throws IOException { + while (childHits != null) { + ScoreNode sn = childHits.nextScoreNode(); + if (sn != null) { + return sn; + } else { + fetchNextChildHits(); + } + } + // if we get here there are no more score nodes + return null; + } + + /** + * Fetches the next {@link #childHits} + * + * @throws IOException if an error occurs while reading from the index. + */ + private void fetchNextChildHits() throws IOException { + if (childHits != null) { + childHits.close(); + } + ScoreNode nextParent = parents.nextScoreNode(); + if (nextParent != null) { + try { + Node parent = session.getNodeById(nextParent.getNodeId()); + childHits = new NodeTraversingQueryHits(parent, false, 1); + } catch (ItemNotFoundException e) { + // access denied to node, will just skip it + } catch (RepositoryException e) { + throw Util.createIOException(e); + } + } else { + childHits = null; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CloseableHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CloseableHits.java new file mode 100644 index 00000000000..3cbc92965e5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CloseableHits.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.core.query.lucene; + +import java.io.IOException; + +/** + * Defines an interface for query hits that need to be closed when done reading + * from it. A client will call {@link #close()} to release resources after a + * query has been executed and the results have been read. + */ +public interface CloseableHits { + + /** + * Releases resources held by this hits instance. + * + * @throws IOException if an error occurs while releasing resources. + */ + void close() throws IOException; + + /** + * @return the number of results or -1 if the size is unknown. + */ + int getSize(); + + /** + * Skips a n score nodes. + * + * @param n the number of score nodes to skip. + * @throws IOException if an error occurs while skipping. + */ + void skip(int n) throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CommittableIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CommittableIndexReader.java new file mode 100644 index 00000000000..9275ac11e53 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/CommittableIndexReader.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.core.query.lucene; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.FilterIndexReader; +import org.apache.lucene.index.IndexReader; + +/** + * Wraps an IndexReader and allows to commit changes without + * closing the reader. + */ +class CommittableIndexReader extends FilterIndexReader { + + /** + * The maximum size of the delete history. + */ + private static final int DELETE_HISTORY_SIZE = 1000; + + /** + * A modification count on this index reader. Initialized with + * {@link IndexReader#getVersion()} and incremented with every call to + * {@link #doDelete(int)}. + */ + private volatile long modCount; + + /** + * The history of the most recent deletes. + */ + private final List deleteHistory = new LinkedList(); + + /** + * The deleted docs for this index reader. + */ + private final BitSet deletedDocs = new BitSet(); + + /** + * Creates a new CommittableIndexReader based on in. + * + * @param in the IndexReader to wrap. + */ + CommittableIndexReader(IndexReader in) { + super(in); + modCount = in.getVersion(); + int maxDocs = in.maxDoc(); + for (int i = 0; i < maxDocs; i++) { + if (in.isDeleted(i)) { + deletedDocs.set(i); + } + } + } + + //------------------------< FilterIndexReader >----------------------------- + + /** + * {@inheritDoc} + *

    + * Increments the modification count. + */ + protected void doDelete(int n) throws CorruptIndexException, IOException { + super.doDelete(n); + modCount++; + if (deleteHistory.size() >= DELETE_HISTORY_SIZE) { + deleteHistory.remove(0); + } + deleteHistory.add(n); + deletedDocs.set(n); + } + + //------------------------< additional methods >---------------------------- + + /** + * @return the modification count of this index reader. + */ + long getModificationCount() { + return modCount; + } + + /** + * Returns the document numbers of deleted nodes since the given + * modCount. + * + * @param modCount a modification count. + * @return document numbers of deleted nodes or null if this + * index reader cannot provide those document number. e.g. modCount + * is too far back in the past. + * @throws IllegalArgumentException if modCount is larger than + * {@link #getModificationCount()}. + */ + Collection getDeletedSince(long modCount) + throws IllegalArgumentException { + if (modCount > this.modCount) { + throw new IllegalArgumentException("modCount: " + + modCount + " > " + this.modCount); + } + if (modCount == this.modCount) { + return Collections.emptyList(); + } + long num = this.modCount - modCount; + if (num > deleteHistory.size()) { + return null; + } + List deletes = new ArrayList((int) num); + for (Integer d : deleteHistory.subList((int) (deleteHistory.size() - num), + deleteHistory.size())) { + deletes.add(d); + } + return deletes; + } + + /** + * Returns a copy of the deleted documents BitSet. + * @return the deleted documents of this index reader. + */ + BitSet getDeletedDocs() { + return (BitSet) deletedDocs.clone(); + } + + @Override + public String toString() { + final StringBuilder buffer = new StringBuilder("CommittableIndexReader("); + buffer.append(in); + buffer.append(','); + buffer.append(modCount); + buffer.append(')'); + return buffer.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ConsistencyCheck.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ConsistencyCheck.java new file mode 100644 index 00000000000..8192984366c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ConsistencyCheck.java @@ -0,0 +1,776 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.commons.io.IOExceptionWithCause; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.cluster.ClusterException; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.persistence.IterablePersistenceManager; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.lucene.document.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.HashSet; + +/** + * Implements a consistency check on the search index. Currently the following + * checks are implemented: + *

      + *
    • Does the node exist in the ItemStateManager? If it does not exist + * anymore the node is deleted from the index.
    • + *
    • Is the parent of a node also present in the index? If it is not present it + * will be indexed.
    • + *
    • Is a node indexed multiple times? If that is the case, all occurrences + * in the index for such a node are removed, and the node is re-indexed.
    • + *
    • Is a node missing from the index? If so, it is added.
    • + *
    + */ +public class ConsistencyCheck { + + /** + * Logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(ConsistencyCheck.class); + + /** + * The number of nodes to fetch at once from the persistence manager. Defaults to 8kb + */ + private static final int NODESATONCE = Integer.getInteger("org.apache.jackrabbit.checker.nodesatonce", 1024 * 8); + + private final SearchIndex handler; + + /** + * The ItemStateManager of the workspace. + */ + private final ItemStateManager stateMgr; + + /** + * The PersistenceManager of the workspace. + */ + private IterablePersistenceManager pm; + + /** + * The index to check. + */ + private final MultiIndex index; + + /** + * All the node ids and whether they were found in the index. + */ + private Map nodeIds; + + /** + * Paths of nodes that are not be indexed + */ + private Set excludedPaths; + + /** + * Paths of nodes that will be excluded from consistency check + */ + private final Set ignoredPaths = new HashSet(); + + /** + * List of all errors. + */ + private final List errors = + new ArrayList(); + + /** + * Private constructor. + */ + private ConsistencyCheck(MultiIndex index, SearchIndex handler, Set excludedIds) { + this.index = index; + this.handler = handler; + final HierarchyManager hierarchyManager = handler.getContext().getHierarchyManager(); + excludedPaths = new HashSet(excludedIds.size()); + for (NodeId excludedId : excludedIds) { + try { + final Path path = hierarchyManager.getPath(excludedId); + excludedPaths.add(path); + } catch (ItemNotFoundException e) { + log.warn("Excluded node does not exist"); + } catch (RepositoryException e) { + log.error("Failed to get excluded path", e); + } + } + + //JCR-3773: ignore the tree jcr:nodeTypes + PathBuilder pathBuilder = new PathBuilder(); + pathBuilder.addRoot(); + pathBuilder.addLast(NameConstants.JCR_NODETYPES); + try { + Path path = pathBuilder.getPath(); + log.info("consistency check will skip " + path); + ignoredPaths.add(path); + } catch (MalformedPathException e) { + //will never happen + log.error("Malformed path", e); + } + + this.stateMgr = handler.getContext().getItemStateManager(); + final PersistenceManager pm = handler.getContext().getPersistenceManager(); + if (pm instanceof IterablePersistenceManager) { + this.pm = (IterablePersistenceManager) pm; + } + } + + /** + * Runs the consistency check on index. + * + * + * + * @param index the index to check. + * @param handler the QueryHandler to use. + * @param excludedIds the set of node ids that are not indexed + * @return the consistency check with the results. + * @throws IOException if an error occurs while checking. + */ + static ConsistencyCheck run(MultiIndex index, SearchIndex handler, final Set excludedIds) + throws IOException { + ConsistencyCheck check = new ConsistencyCheck(index, handler, excludedIds); + check.run(); + return check; + } + + /** + * Repairs detected errors during the consistency check. + * @param ignoreFailure if true repair failures are ignored, + * the repair continues without throwing an exception. If + * false the repair procedure is aborted on the first + * repair failure. + * @throws IOException if a repair failure occurs. + */ + public void repair(boolean ignoreFailure) throws IOException { + if (errors.size() == 0) { + log.info("No errors found."); + return; + } + int notRepairable = 0; + for (ConsistencyCheckError error : errors) { + try { + if (error.repairable()) { + error.repair(); + } else { + log.warn("Not repairable: " + error); + notRepairable++; + } + } catch (Exception e) { + if (ignoreFailure) { + log.warn("Exception while repairing: " + error, e); + } else if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new IOExceptionWithCause(e); + } + } + } + log.info("Repaired " + (errors.size() - notRepairable) + " errors."); + if (notRepairable > 0) { + log.warn("" + notRepairable + " error(s) not repairable."); + } + } + + /** + * Returns the errors detected by the consistency check. + * @return the errors detected by the consistency check. + */ + public List getErrors() { + return new ArrayList(errors); + } + + /** + * Runs the consistency check. + * @throws IOException if an error occurs while running the check. + */ + private void run() throws IOException { + log.info("Checking index of workspace " + handler.getContext().getWorkspace()); + loadNodes(); + if (nodeIds != null) { + checkIndexConsistency(); + checkIndexCompleteness(); + } + } + + public void doubleCheckErrors() { + if (!errors.isEmpty()) { + log.info("Double checking errors"); + final ClusterNode clusterNode = handler.getContext().getClusterNode(); + if (clusterNode != null) { + try { + clusterNode.sync(); + } catch (ClusterException e) { + log.error("Could not sync cluster node for double checking errors"); + } + } + final Iterator iterator = errors.iterator(); + while (iterator.hasNext()) { + try { + final ConsistencyCheckError error = iterator.next(); + if (!error.doubleCheck(handler, stateMgr)) { + log.info("False positive: " + error.toString()); + iterator.remove(); + } + } catch (RepositoryException e) { + log.error("Failed to double check consistency error", e); + } catch (IOException e) { + log.error("Failed to double check consistency error", e); + } + } + } + } + + private void loadNodes() { + log.info("Loading nodes"); + try { + int count = 0; + Map nodeIds = new HashMap(); + List batch = pm.getAllNodeIds(null, NODESATONCE); + NodeId lastId = null; + while (!batch.isEmpty()) { + for (NodeId nodeId : batch) { + lastId = nodeId; + + count++; + if (count % 1000 == 0) { + log.info(pm + ": loaded " + count + " node ids..."); + } + + nodeIds.put(nodeId, Boolean.FALSE); + + } + batch = pm.getAllNodeIds(lastId, NODESATONCE); + } + if (pm.exists(lastId)) { + this.nodeIds = nodeIds; + } else { + log.info("Failed to read all nodes, starting over"); + loadNodes(); + } + } catch (ItemStateException e) { + log.error("Exception while loading items to check", e); + } catch (RepositoryException e) { + log.error("Exception while loading items to check", e); + } + } + + private void checkIndexConsistency() throws IOException { + log.info("Checking index consistency"); + // Ids of multiple nodes in the index + Set multipleEntries = new HashSet(); + CachingMultiIndexReader reader = index.getIndexReader(); + try { + for (int i = 0; i < reader.maxDoc(); i++) { + if (i > 10 && i % (reader.maxDoc() / 5) == 0) { + long progress = Math.round((100.0 * (float) i) / ((float) reader.maxDoc() * 2f)); + log.info("progress: " + progress + "%"); + } + if (reader.isDeleted(i)) { + continue; + } + Document d = reader.document(i, FieldSelectors.UUID); + NodeId id = new NodeId(d.get(FieldNames.UUID)); + if (!isIgnored(id)) { + boolean nodeExists = nodeIds.containsKey(id); + if (nodeExists) { + Boolean alreadyIndexed = nodeIds.put(id, Boolean.TRUE); + if (alreadyIndexed) { + multipleEntries.add(id); + } + } else { + errors.add(new NodeDeleted(id)); + } + } + } + } finally { + reader.release(); + } + + // create multiple entries errors + for (NodeId id : multipleEntries) { + errors.add(new MultipleEntries(id)); + } + + reader = index.getIndexReader(); + try { + // run through documents again and check parent + for (int i = 0; i < reader.maxDoc(); i++) { + if (i > 10 && i % (reader.maxDoc() / 5) == 0) { + long progress = Math.round((100.0 * (float) i) / ((float) reader.maxDoc() * 2f)); + log.info("progress: " + (progress + 50) + "%"); + } + if (reader.isDeleted(i)) { + continue; + } + Document d = reader.document(i, FieldSelectors.UUID_AND_PARENT); + NodeId id = new NodeId(d.get(FieldNames.UUID)); + if (!nodeIds.containsKey(id) || isIgnored(id)) { + // this node is ignored or was already marked for deletion + continue; + } + String parent = d.get(FieldNames.PARENT); + if (parent == null || parent.isEmpty()) { + continue; + } + final NodeId parentId = new NodeId(parent); + + boolean parentExists = nodeIds.containsKey(parentId); + boolean parentIndexed = parentExists && nodeIds.get(parentId); + if (parentIndexed) { + continue; + } else if (id.equals(RepositoryImpl.SYSTEM_ROOT_NODE_ID) + && parentId.equals(RepositoryImpl.ROOT_NODE_ID)) { + continue; // special case for the /jcr:system node + } + + // parent is missing from index + if (parentExists) { + errors.add(new MissingAncestor(id, parentId)); + } else { + try { + final ItemState itemState = stateMgr.getItemState(id); + if (parentId.equals(itemState.getParentId())) { + // orphaned node + errors.add(new UnknownParent(id, parentId)); + } else { + errors.add(new WrongParent(id, parentId, itemState.getParentId())); + } + } catch (ItemStateException ignored) { + } + } + } + } finally { + reader.release(); + } + + } + + private void checkIndexCompleteness() { + log.info("Checking index completeness"); + int i = 0; + int size = nodeIds.size(); + for (Map.Entry entry : nodeIds.entrySet()) { + // check whether all nodes in the repository are indexed + NodeId nodeId = entry.getKey(); + boolean indexed = entry.getValue(); + try { + if (++i > 10 && i % (size / 10) == 0) { + long progress = Math.round((100.0 * (float) i) / (float) size); + log.info("progress: " + progress + "%"); + } + if (!indexed && !isIgnored(nodeId) && !isExcluded(nodeId)) { + NodeState nodeState = getNodeState(nodeId); + if (nodeState != null && !isBrokenNode(nodeId, nodeState)) { + errors.add(new NodeAdded(nodeId)); + } + } + } catch (ItemStateException e) { + log.error("Failed to check node: " + nodeId, e); + } + } + } + + private boolean isExcluded(NodeId id) { + try { + final HierarchyManager hierarchyManager = handler.getContext().getHierarchyManager(); + final Path path = hierarchyManager.getPath(id); + for (Path excludedPath : excludedPaths) { + if (excludedPath.isEquivalentTo(path) || excludedPath.isAncestorOf(path)) { + return true; + } + } + } catch (RepositoryException ignored) { + } + return false; + } + + private boolean isIgnored(NodeId id) { + try { + final HierarchyManager hierarchyManager = handler.getContext().getHierarchyManager(); + final Path path = hierarchyManager.getPath(id); + for (Path excludedPath : ignoredPaths) { + if (excludedPath.isEquivalentTo(path) || excludedPath.isAncestorOf(path)) { + return true; + } + } + } catch (RepositoryException ignored) { + } + return false; + } + + private NodeState getNodeState(NodeId nodeId) throws ItemStateException { + try { + return (NodeState) stateMgr.getItemState(nodeId); + } catch (NoSuchItemStateException e) { + return null; + } + } + + private boolean isBrokenNode(final NodeId nodeId, final NodeState nodeState) throws ItemStateException { + final NodeId parentId = nodeState.getParentId(); + if (parentId != null) { + final NodeState parentState = getNodeState(parentId); + if (parentState == null) { + log.warn("Node missing from index is orphaned node: " + nodeId); + return true; + } + if (!parentState.hasChildNodeEntry(nodeId)) { + log.warn("Node missing from index is abandoned node: " + nodeId); + return true; + } + } + return false; + } + + /** + * Returns the path for node. If an error occurs this method + * returns the uuid of the node. + * + * @param node the node to retrieve the path from + * @return the path of the node or its uuid. + */ + private String getPath(NodeState node) { + // remember as fallback + String uuid = node.getNodeId().toString(); + StringBuilder path = new StringBuilder(); + List elements = new ArrayList(); + try { + while (node.getParentId() != null) { + NodeId parentId = node.getParentId(); + NodeState parent = (NodeState) stateMgr.getItemState(parentId); + ChildNodeEntry entry = parent.getChildNodeEntry(node.getNodeId()); + if (entry == null) { + log.warn("Failed to build path: abandoned child {} of node {}. " + + "Please run a repository consistency check", node.getNodeId(), parentId); + return uuid; + } + elements.add(entry); + node = parent; + } + for (int i = elements.size() - 1; i > -1; i--) { + ChildNodeEntry entry = elements.get(i); + path.append('/').append(entry.getName().getLocalName()); + if (entry.getIndex() > 1) { + path.append('[').append(entry.getIndex()).append(']'); + } + } + if (path.length() == 0) { + path.append('/'); + } + return path.toString(); + } catch (ItemStateException e) { + return uuid; + } + } + + //-------------------< ConsistencyCheckError classes >---------------------- + + /** + * One or more ancestors of an indexed node are not available in the index. + */ + private class MissingAncestor extends ConsistencyCheckError { + + private final NodeId parentId; + + private MissingAncestor(NodeId id, NodeId parentId) { + super("Parent of " + id + " missing in index. Parent: " + parentId, id); + this.parentId = parentId; + } + + /** + * Returns true. + * @return true. + */ + public boolean repairable() { + return true; + } + + /** + * Repairs the missing node by indexing the missing ancestors. + * @throws Exception if an error occurs while repairing. + */ + public void repair() throws Exception { + NodeId ancestorId = parentId; + while (ancestorId != null && nodeIds.containsKey(ancestorId) && nodeIds.get(ancestorId)) { + NodeState n = (NodeState) stateMgr.getItemState(ancestorId); + log.info("Repairing missing node " + getPath(n) + " (" + ancestorId + ")"); + Document d = index.createDocument(n); + index.addDocument(d); + nodeIds.put(n.getNodeId(), Boolean.TRUE); + ancestorId = n.getParentId(); + } + } + + @Override + boolean doubleCheck(SearchIndex handler, ItemStateManager stateManager) + throws RepositoryException, IOException { + final List documents = handler.getNodeDocuments(id); + for (Document document : documents) { + final String parent = document.get(FieldNames.PARENT); + if (parent != null && !parent.isEmpty()) { + final NodeId parentId = new NodeId(parent); + if (handler.getNodeDocuments(parentId).isEmpty()) { + return true; + } + } + } + return false; + + } + } + + /** + * The parent of a node is not in the repository + */ + private static class UnknownParent extends ConsistencyCheckError { + + private NodeId parentId; + + private UnknownParent(NodeId id, NodeId parentId) { + super("Node " + id + " has unknown parent: " + parentId, id); + this.parentId = parentId; + } + + /** + * Not reparable (yet). + * @return false. + */ + public boolean repairable() { + return false; + } + + /** + * No operation. + */ + public void repair() { + log.warn("Unknown parent for " + id + " cannot be repaired"); + } + + @Override + boolean doubleCheck(SearchIndex handler, ItemStateManager stateManager) + throws IOException, RepositoryException { + final List documents = handler.getNodeDocuments(id); + for (Document document : documents) { + final String parent = document.get(FieldNames.PARENT); + if (parent != null && !parent.isEmpty()) { + final NodeId parentId = new NodeId(parent); + if (parentId.equals(this.parentId) && !stateManager.hasItemState(parentId)) { + return true; + } + } + } + return false; + } + } + + /** + * The parent as indexed does not correspond with the actual parent in the repository + */ + private class WrongParent extends ConsistencyCheckError { + + private NodeId indexedParentId; + + private WrongParent(NodeId id, NodeId indexedParentId, NodeId actualParentId) { + super("Node " + id + " has wrong parent: " + indexedParentId + ", should be : " + actualParentId, id); + this.indexedParentId = indexedParentId; + } + + @Override + public boolean repairable() { + return true; + } + + /** + * Reindex node. + */ + @Override + void repair() throws Exception { + index.removeAllDocuments(id); + try { + NodeState node = (NodeState) stateMgr.getItemState(id); + log.info("Re-indexing node with wrong parent in index: " + getPath(node)); + Document d = index.createDocument(node); + index.addDocument(d); + nodeIds.put(node.getNodeId(), Boolean.TRUE); + } catch (NoSuchItemStateException e) { + log.info("Not re-indexing node with wrong parent because node no longer exists"); + } + } + + @Override + boolean doubleCheck(final SearchIndex handler, final ItemStateManager stateManager) + throws RepositoryException, IOException { + final List documents = handler.getNodeDocuments(id); + for (Document document : documents) { + final String parent = document.get(FieldNames.PARENT); + if (parent != null && !parent.isEmpty()) { + final NodeId parentId = new NodeId(parent); + if (parentId.equals(indexedParentId) && !stateManager.hasItemState(parentId)) { + return true; + } + } + } + return false; + } + + } + + /** + * A node is present multiple times in the index. + */ + private class MultipleEntries extends ConsistencyCheckError { + + MultipleEntries(NodeId id) { + super("Multiple entries found for node " + id, id); + } + + /** + * Returns true. + * @return true. + */ + public boolean repairable() { + return true; + } + + /** + * Removes the nodes with the identical uuids from the index and + * re-index the node. + * @throws IOException if an error occurs while repairing. + */ + public void repair() throws Exception { + // first remove all occurrences + index.removeAllDocuments(id); + // then re-index the node + try { + NodeState node = (NodeState) stateMgr.getItemState(id); + log.info("Re-indexing duplicate node occurrences in index: " + getPath(node)); + Document d = index.createDocument(node); + index.addDocument(d); + nodeIds.put(node.getNodeId(), Boolean.TRUE); + } catch (NoSuchItemStateException e) { + log.info("Not re-indexing node with multiple occurrences because node no longer exists"); + } + } + + @Override + boolean doubleCheck(SearchIndex handler, ItemStateManager stateManager) + throws RepositoryException, IOException { + return handler.getNodeDocuments(id).size() > 1; + } + } + + /** + * Indicates that a node has been deleted but is still in the index. + */ + private class NodeDeleted extends ConsistencyCheckError { + + NodeDeleted(NodeId id) { + super("Node " + id + " no longer exists.", id); + } + + /** + * Returns true. + * @return true. + */ + public boolean repairable() { + return true; + } + + /** + * Deletes the nodes from the index. + * @throws IOException if an error occurs while repairing. + */ + public void repair() throws IOException { + log.info("Removing deleted node from index: " + id); + index.removeDocument(id); + } + + @Override + boolean doubleCheck(SearchIndex handler, ItemStateManager stateManager) + throws RepositoryException, IOException { + final List documents = handler.getNodeDocuments(id); + if (!documents.isEmpty()) { + if (!stateManager.hasItemState(id)) { + return true; + } + } + return false; + } + } + + private class NodeAdded extends ConsistencyCheckError { + + NodeAdded(final NodeId id) { + super("Node " + id + " is missing.", id); + } + + @Override + public boolean repairable() { + return true; + } + + @Override + void repair() throws Exception { + try { + NodeState nodeState = (NodeState) stateMgr.getItemState(id); + log.info("Adding missing node to index: " + getPath(nodeState)); + final Iterator remove = Collections.emptyList().iterator(); + final Iterator add = Collections.singletonList(nodeState).iterator(); + handler.updateNodes(remove, add); + } catch (NoSuchItemStateException e) { + log.info("Not adding missing node because node no longer exists"); + } + } + + @Override + boolean doubleCheck(SearchIndex handler, ItemStateManager stateManager) + throws RepositoryException, IOException { + final List documents = handler.getNodeDocuments(id); + if (documents.isEmpty()) { + if (stateManager.hasItemState(id)) { + return true; + } + } + return false; + } + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ConsistencyCheckError.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ConsistencyCheckError.java new file mode 100644 index 00000000000..5281548f0a9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ConsistencyCheckError.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.core.query.lucene; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ItemStateManager; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +/** + * Common base class for errors detected during the consistency check. + */ +public abstract class ConsistencyCheckError { + + /** + * Diagnostic message for this error. + */ + protected final String message; + + /** + * The id of the affected node. + */ + protected final NodeId id; + + ConsistencyCheckError(String message, NodeId id) { + this.message = message; + this.id = id; + } + + /** + * Returns the diagnostic message. + * @return the diagnostic message. + */ + public String toString() { + return message; + } + + /** + * Returns true if this error can be repaired. + * @return true if this error can be repaired. + */ + public abstract boolean repairable(); + + /** + * Executes the repair operation. + * @throws Exception if an error occurs while repairing. + */ + abstract void repair() throws Exception; + + /** + * Double check the error. Used to rule out false positives in live environments. + * + * @return true if the error was verified to still exist, else false. + * @throws RepositoryException + * @throws IOException + */ + abstract boolean doubleCheck(SearchIndex handler, ItemStateManager stateManager) + throws RepositoryException, IOException; +} diff --git a/src/java/org/apache/jackrabbit/core/search/lucene/DateField.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DateField.java similarity index 76% rename from src/java/org/apache/jackrabbit/core/search/lucene/DateField.java rename to jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DateField.java index 2c0aa197a71..7342f9710cc 100644 --- a/src/java/org/apache/jackrabbit/core/search/lucene/DateField.java +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DateField.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.core.search.lucene; +package org.apache.jackrabbit.core.query.lucene; import java.util.Date; @@ -23,7 +23,7 @@ * a way that the resulting String is suitable for indexing and * sorting. */ -class DateField { +public class DateField { private DateField() { } @@ -41,12 +41,12 @@ private DateField() { /** - * Returns '000000000' -> something around 30 BC + * Returns '000000000' -> something around 30 BC */ public static final String MIN_DATE_STRING = timeToString(-DATE_SHIFT); /** - * Returns 'zzzzzzzzz' -> something around 3189 + * Returns 'zzzzzzzzz' -> something around 3189 */ public static final String MAX_DATE_STRING; @@ -74,6 +74,8 @@ public static String dateToString(Date date) { /** * Converts a millisecond time to a string suitable for indexing. * Supported date range is: 30 BC - 3189 + * @throws IllegalArgumentException if the given time is not + * within the supported date range. */ public static String timeToString(long time) { @@ -81,13 +83,13 @@ public static String timeToString(long time) { if (time < 0) { - throw new RuntimeException("time too early"); + throw new IllegalArgumentException("time too early"); } String s = Long.toString(time, Character.MAX_RADIX); if (s.length() > DATE_LEN) { - throw new RuntimeException("time too late"); + throw new IllegalArgumentException("time too late"); } // Pad with leading zeros diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DecimalField.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DecimalField.java new file mode 100644 index 00000000000..fd2bed21e54 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DecimalField.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.core.query.lucene; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * The DecimalField class is a utility to convert + * java.math.BigDecimal values to String + * values that are lexicographically sortable according to the decimal value. + *

    + * The string format uses the characters '0' to '9' and consists of: + *

    + * { value signum +2 }
    + * { exponent signum +2 }
    + * { exponent length -1 }
    + * { exponent value }
    + * { value (-1 if inverted) }
    + * 
    + * Only the signum is encoded if the value is zero. The exponent is not + * encoded if zero. Negative values are "inverted" character by character + * ('0' -> 9, '1' -> '8', and so on). The same applies to the exponent. + *

    + * Examples: + * 0 => "2" + * 2 => "322" (signum 1; exponent 0; value 2) + * 120 => "330212" (signum 1; exponent signum 1, length 1, value 2; value 12). + * -1 => "179" (signum -1, rest inverted; exponent 0; value 1 (-1, inverted). + *

    + * Values between BigDecimal(BigInteger.ONE, Integer.MIN_VALUE) and + * BigDecimal(BigInteger.ONE, Integer.MAX_VALUE) are supported. + */ +public class DecimalField { + + /** + * Convert a BigDecimal to a String. + * + * @param value the BigDecimal + * @return the String + */ + public static String decimalToString(BigDecimal value) { + switch (value.signum()) { + case -1: + return "1" + invert(positiveDecimalToString(value.negate()), 1); + case 0: + return "2"; + default: + return "3" + positiveDecimalToString(value); + } + } + + /** + * Convert a String to a BigDecimal. + * + * @param value the String + * @return the BigDecimal + */ + public static BigDecimal stringToDecimal(String value) { + int sig = value.charAt(0) - '2'; + if (sig == 0) { + return BigDecimal.ZERO; + } else if (sig < 0) { + value = invert(value, 1); + } + long expSig = value.charAt(1) - '2', exp; + if (expSig == 0) { + exp = 0; + value = value.substring(2); + } else { + int expSize = value.charAt(2) - '0' + 1; + if (expSig < 0) { + expSize = 11 - expSize; + } + String e = value.substring(3, 3 + expSize); + exp = expSig * Long.parseLong(expSig < 0 ? invert(e, 0) : e); + value = value.substring(3 + expSize); + } + BigInteger x = new BigInteger(value); + int scale = (int) (value.length() - exp - 1); + return new BigDecimal(sig < 0 ? x.negate() : x, scale); + } + + private static String positiveDecimalToString(BigDecimal value) { + StringBuilder buff = new StringBuilder(); + long exp = value.precision() - value.scale() - 1; + // exponent signum and size + if (exp == 0) { + buff.append('2'); + } else { + String e = String.valueOf(Math.abs(exp)); + // exponent size is prepended + e = String.valueOf(e.length() - 1) + e; + // exponent signum + if (exp > 0) { + buff.append('3').append(e); + } else { + buff.append('1').append(invert(e, 0)); + } + } + String s = value.unscaledValue().toString(); + // remove trailing 0s + int max = s.length() - 1; + while (s.charAt(max) == '0') { + max--; + } + return buff.append(s.substring(0, max + 1)).toString(); + } + + /** + * "Invert" a number digit by digit (0 becomes 9, 9 becomes 0, and so on). + * + * @param s the original string + * @param incLast how much to increment the last character + * @return the negated string + */ + private static String invert(String s, int incLast) { + char[] chars = s.toCharArray(); + for (int i = 0; i < chars.length; i++) { + chars[i] = (char) ('9' - chars[i] + '0'); + } + chars[chars.length - 1] += incLast; + return String.valueOf(chars); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultHTMLExcerpt.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultHTMLExcerpt.java new file mode 100644 index 00000000000..36ade0f9f6c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultHTMLExcerpt.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.core.query.lucene; + +import org.apache.lucene.index.TermPositionVector; + +import java.io.IOException; + +/** + * DefaultHTMLExcerpt creates a HTML excerpt with the following + * format: + *

    + * <div>
    + *     <span><strong>Jackrabbit</strong> implements both the mandatory XPath and optional SQL <strong>query</strong> syntax.</span>
    + *     <span>Before parsing the XPath <strong>query</strong> in <strong>Jackrabbit</strong>, the statement is surrounded</span>
    + * </div>
    + * 
    + */ +public class DefaultHTMLExcerpt extends AbstractExcerpt { + + /** + * {@inheritDoc} + */ + protected String createExcerpt(TermPositionVector tpv, + String text, + int maxFragments, + int maxFragmentSize) throws IOException { + return DefaultHighlighter.highlight(tpv, getQueryTerms(), text, + "
    ", "
    ", "", "", "", "", + maxFragments, maxFragmentSize / 2); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultHighlighter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultHighlighter.java new file mode 100644 index 00000000000..4408c26f3ad --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultHighlighter.java @@ -0,0 +1,587 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.io.StringReader; +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 java.util.Map; +import java.util.Set; + +import org.apache.jackrabbit.util.Text; +import org.apache.lucene.document.Field; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermPositionVector; +import org.apache.lucene.index.TermVectorOffsetInfo; + +/** + * This is an adapted version of the FulltextHighlighter posted in + * issue: LUCENE-644. + *

    + * Important: for this highlighter to function properly, field must be stored + * with token offsets.
    Use Field constructor {@link + * Field#Field(String,String,Field.Store,Field.Index,Field.TermVector) + * Field(String, String, Field.Store, Field.Index, Field.TermVector)} where the + * last argument is either {@link Field.TermVector#WITH_POSITIONS_OFFSETS} or + * {@link org.apache.lucene.document.Field.TermVector#WITH_OFFSETS} + * + * @see org.apache.lucene.index.TermPositionVector + * @see org.apache.lucene.index.TermFreqVector + */ +public class DefaultHighlighter { + + /** + * A default value of 3 + */ + public static final int DEFAULT_MAXFRAGMENTS = 3; + + /** + * A default value of 75 + */ + public static final int DEFAULT_SURROUND = 75; + + public static final String START_EXCERPT = ""; + + public static final String END_EXCERPT = ""; + + public static final String START_FRAGMENT_SEPARATOR = ""; + + public static final String END_FRAGMENT_SEPARATOR = ""; + + public static final String START_HIGHLIGHT = ""; + + public static final String END_HIGHLIGHT = ""; + + protected DefaultHighlighter() { + } + + /** + * @param tvec the term position vector for this hit + * @param queryTerms the query terms. + * @param text the original text that was used to create the + * tokens. + * @param excerptStart this string is prepended to the excerpt + * @param excerptEnd this string is appended to the excerpt + * @param fragmentStart this string is prepended to every fragment + * @param fragmentEnd this string is appended to the end of every + * fragement. + * @param hlStart the string used to prepend a highlighted token, for + * example "<b>" + * @param hlEnd the string used to append a highlighted token, for + * example "</b>" + * @param maxFragments the maximum number of fragments + * @param surround the maximum number of chars surrounding a + * highlighted token + * @return a String with text fragments where tokens from the query are + * highlighted + */ + public static String highlight(TermPositionVector tvec, + Set queryTerms, + String text, + String excerptStart, + String excerptEnd, + String fragmentStart, + String fragmentEnd, + String hlStart, + String hlEnd, + int maxFragments, + int surround) + throws IOException { + return new DefaultHighlighter().doHighlight(tvec, queryTerms, text, + excerptStart, excerptEnd, fragmentStart, fragmentEnd, hlStart, + hlEnd, maxFragments, surround); + } + + /** + * @param tvec the term position vector for this hit + * @param queryTerms the query terms. + * @param text the original text that was used to create the tokens. + * @param maxFragments the maximum number of fragments + * @param surround the maximum number of chars surrounding a highlighted + * token + * @return a String with text fragments where tokens from the query are + * highlighted + */ + public static String highlight(TermPositionVector tvec, + Set queryTerms, + String text, + int maxFragments, + int surround) + throws IOException { + return highlight(tvec, queryTerms, text, START_EXCERPT, END_EXCERPT, + START_FRAGMENT_SEPARATOR, END_FRAGMENT_SEPARATOR, + START_HIGHLIGHT, END_HIGHLIGHT, maxFragments, surround); + } + + /** + * @see #highlight(TermPositionVector, Set, String, String, String, String, String, String, String, int, int) + */ + protected String doHighlight(TermPositionVector tvec, + Set queryTerms, + String text, + String excerptStart, + String excerptEnd, + String fragmentStart, + String fragmentEnd, + String hlStart, + String hlEnd, + int maxFragments, + int surround) throws IOException { + + List termOffsetInfo = new ArrayList(); + + Iterator it = queryTerms.iterator(); + while (it.hasNext()) { + Term[] qt = it.next(); + if (qt == null) { + continue; + } + final int qtLen = qt.length; + if (qtLen == 0) { + continue; + } + String[] qtText = new String[qtLen]; + for (int i = 0; i < qtLen; i++) { + qtText[i] = qt[i].text(); + } + int[] tvecindexes = tvec.indexesOf(qtText, 0, qtText.length); + Map localTermOffsetInfo = new HashMap(); + for (int tvecindex : tvecindexes) { + TermVectorOffsetInfo[] termoffsets = tvec.getOffsets(tvecindex); + if (termoffsets == null || termoffsets.length == 0) { + continue; + } + localTermOffsetInfo.put(tvecindex, termoffsets); + } + + // to keep the order of the keys, use tvecindexes, + // if a term is not found tvecindexes[] = -1 + // when dealing with multiple terms that have to exist, just check + // if the first one is there + if (tvecindexes.length > 0 && tvecindexes[0] >= 0) { + // we have to build one interval TermVectorOffsetInfo for each + // hit; + List intervalTermOffsetInfo = new ArrayList(); + + // pick all the first key's hist as interval start + TermVectorOffsetInfo[] firstKeyTermOffsets = localTermOffsetInfo + .get(tvecindexes[0]); + Arrays.sort(firstKeyTermOffsets, + new TermVectorOffsetInfoSorter()); + intervalTermOffsetInfo.addAll(Arrays + .asList(firstKeyTermOffsets)); + + // check if each key is part of an interval, if not, it is + // dropped from the list + for (int i = 1; i < tvecindexes.length; i++) { + final Integer key = tvecindexes[i]; + TermVectorOffsetInfo[] termoffsets = localTermOffsetInfo + .get(key); + if (termoffsets == null) { + continue; + } + Arrays.sort(termoffsets, new TermVectorOffsetInfoSorter()); + + Iterator intervalIterator = intervalTermOffsetInfo + .iterator(); + + int index = 0; + while (intervalIterator.hasNext()) { + TermVectorOffsetInfo intervalOI = intervalIterator + .next(); + if (index >= termoffsets.length) { + intervalIterator.remove(); + continue; + } + boolean matchSearch = true; + boolean matchFound = false; + while (matchSearch) { + TermVectorOffsetInfo localOI = termoffsets[index]; + // check interval match + // CJK languages will have the tokens from the PhraseQuery glued together (see LUCENE-2458) + int diff = localOI.getStartOffset() + - intervalOI.getEndOffset(); + // TODO we'll probably have to remove 'diff == 0' + // after upgrading to lucene 3.1 + if (diff == 1 || diff == 0) { + intervalOI.setEndOffset(localOI.getEndOffset()); + matchSearch = false; + matchFound = true; + } + index++; + if (index >= termoffsets.length) { + matchSearch = false; + } + } + if (!matchFound) { + index--; + intervalIterator.remove(); + } + } + } + termOffsetInfo.addAll(intervalTermOffsetInfo); + } + } + + TermVectorOffsetInfo[] offsets = termOffsetInfo.toArray(new TermVectorOffsetInfo[termOffsetInfo.size()]); + // sort offsets + if (offsets != null && offsets.length > 1) { + Arrays.sort(offsets, new TermVectorOffsetInfoSorter()); + } + + return mergeFragments(offsets, text, excerptStart, + excerptEnd, fragmentStart, fragmentEnd, hlStart, hlEnd, + maxFragments, surround); + } + + protected String mergeFragments(TermVectorOffsetInfo[] offsets, + String text, + String excerptStart, + String excerptEnd, + String fragmentStart, + String fragmentEnd, + String hlStart, + String hlEnd, + int maxFragments, + int surround) throws IOException { + if (offsets == null || offsets.length == 0) { + // nothing to highlight + return createDefaultExcerpt(text, excerptStart, excerptEnd, + fragmentStart, fragmentEnd, surround * 2); + } + int lastOffset = offsets.length; // Math.min(10, offsets.length); // 10 terms is plenty? + List fragmentInfoList = new ArrayList(); + if (offsets[0].getEndOffset() <= text.length()) { + FragmentInfo fi = new FragmentInfo(offsets[0], surround * 2); + for (int i = 1; i < lastOffset; i++) { + if (offsets[i].getEndOffset() > text.length()) { + break; + } + if (fi.add(offsets[i])) { + continue; + } + fragmentInfoList.add(fi); + fi = new FragmentInfo(offsets[i], surround * 2); + } + fragmentInfoList.add(fi); + } + + if (fragmentInfoList.isEmpty()) { + // nothing to highlight + return createDefaultExcerpt(text, excerptStart, excerptEnd, + fragmentStart, fragmentEnd, surround * 2); + } + + // sort with score + Collections.sort(fragmentInfoList, new FragmentInfoScoreSorter()); + + // extract best fragments + List bestFragmentsList = new ArrayList(); + for (int i = 0; i < Math.min(fragmentInfoList.size(), maxFragments); i++) { + bestFragmentsList.add(fragmentInfoList.get(i)); + } + + // re-sort with positions + Collections.sort(bestFragmentsList, new FragmentInfoPositionSorter()); + + // merge #maxFragments fragments + StringReader reader = new StringReader(text); + StringBuffer sb = new StringBuffer(excerptStart); + int pos = 0; + char[] cbuf; + int skip; + int nextStart; + int skippedChars; + int firstWhitespace; + for (int i = 0; i < bestFragmentsList.size(); i++) { + FragmentInfo fi = bestFragmentsList.get(i); + fi.trim(); + nextStart = fi.getStartOffset(); + skip = nextStart - pos; + if (skip > surround * 2) { + skip -= surround; + if (i > 0) { + // end last fragment + cbuf = new char[surround]; + reader.read(cbuf, 0, surround); + // find last whitespace + skippedChars = 1; + for (; skippedChars < surround + 1; skippedChars++) { + if (Character.isWhitespace(cbuf[surround - skippedChars])) { + break; + } + } + pos += surround; + if (skippedChars > surround) { + skippedChars = surround; + } + sb.append(escape(new String(cbuf, 0, surround + - skippedChars))); + sb.append(fragmentEnd); + } + } + + if (skip >= surround) { + if (i > 0) { + skip -= surround; + } + // skip + reader.skip((long) skip); + pos += skip; + } + // start fragment + cbuf = new char[nextStart - pos]; + skippedChars = Math.max(cbuf.length - 1, 0); + firstWhitespace = skippedChars; + reader.read(cbuf, 0, nextStart - pos); + pos += (nextStart - pos); + sb.append(fragmentStart); + // find last period followed by whitespace + if (cbuf.length > 0) { + for (; skippedChars >= 0; skippedChars--) { + if (Character.isWhitespace(cbuf[skippedChars])) { + firstWhitespace = skippedChars; + if (skippedChars - 1 >= 0 + && cbuf[skippedChars - 1] == '.') { + skippedChars++; + break; + } + } + } + } + boolean sentenceStart = true; + if (skippedChars == -1) { + if (pos == cbuf.length) { + // this fragment is the start of the text -> skip none + skippedChars = 0; + } else { + sentenceStart = false; + skippedChars = firstWhitespace + 1; + } + } + + if (!sentenceStart) { + sb.append("... "); + } + sb.append(escape(new String(cbuf, skippedChars, cbuf.length + - skippedChars))); + + // iterate terms + for (Iterator iter = fi.iterator(); iter.hasNext();) { + TermVectorOffsetInfo ti = iter.next(); + nextStart = ti.getStartOffset(); + if (nextStart - pos > 0) { + cbuf = new char[nextStart - pos]; + int charsRead = reader.read(cbuf, 0, nextStart - pos); + pos += (nextStart - pos); + sb.append(escape(new String(cbuf, 0, charsRead))); + } + sb.append(hlStart); + nextStart = ti.getEndOffset(); + // print term + cbuf = new char[nextStart - pos]; + reader.read(cbuf, 0, nextStart - pos); + pos += (nextStart - pos); + sb.append(escape(new String(cbuf))); + sb.append(hlEnd); + } + } + if (pos != 0) { + // end fragment + if (offsets.length > lastOffset) { + surround = Math.min(offsets[lastOffset].getStartOffset() - pos, surround); + } + cbuf = new char[surround]; + skip = reader.read(cbuf, 0, surround); + boolean EOF = reader.read() == -1; + if (skip >= 0) { + if (!EOF) { + skippedChars = 1; + for (; skippedChars < surround + 1; skippedChars++) { + if (Character.isWhitespace(cbuf[surround - skippedChars])) { + break; + } + } + if (skippedChars > surround) { + skippedChars = surround; + } + } else { + skippedChars = 0; + } + sb.append(escape(new String(cbuf, 0, EOF ? skip + : (surround - skippedChars)))); + if (!EOF) { + char lastChar = sb.charAt(sb.length() - 1); + if (lastChar != '.' && lastChar != '!' && lastChar != '?') { + sb.append(" ..."); + } + } + } + sb.append(fragmentEnd); + } + sb.append(excerptEnd); + return sb.toString(); + } + + /** + * Creates a default excerpt with the given text. + * + * @param text the text. + * @param excerptStart the excerpt start. + * @param excerptEnd the excerpt end. + * @param fragmentStart the fragment start. + * @param fragmentEnd the fragment end. + * @param maxLength the maximum length of the fragment. + * @return a default excerpt. + * @throws IOException if an error occurs while reading from the text. + */ + protected String createDefaultExcerpt(String text, + String excerptStart, + String excerptEnd, + String fragmentStart, + String fragmentEnd, + int maxLength) throws IOException { + StringReader reader = new StringReader(text); + StringBuffer excerpt = new StringBuffer(excerptStart); + excerpt.append(fragmentStart); + int min = excerpt.length(); + char[] buf = new char[maxLength]; + int len = reader.read(buf); + StringBuffer tmp = new StringBuffer(); + tmp.append(buf, 0, len); + if (len == buf.length) { + for (int i = tmp.length() - 1; i > min; i--) { + if (Character.isWhitespace(tmp.charAt(i))) { + tmp.delete(i, tmp.length()); + tmp.append(" ..."); + break; + } + } + } + excerpt.append(escape(tmp.toString())); + excerpt.append(fragmentEnd).append(excerptEnd); + return excerpt.toString(); + } + + + /** + * Escapes input text suitable for the output format. + *

    + * By default does XML-escaping. Can be overridden for + * other formats. + * + * @param input raw text. + * @return text suitably escaped. + */ + protected String escape(String input) { + return Text.encodeIllegalXMLCharacters(input); + } + + private static class FragmentInfo { + List offsetInfosList; + int startOffset; + int endOffset; + int mergeGap; + int numTerms; + + public FragmentInfo(TermVectorOffsetInfo offsetinfo, int mergeGap) { + offsetInfosList = new ArrayList(); + offsetInfosList.add(offsetinfo); + startOffset = offsetinfo.getStartOffset(); + endOffset = offsetinfo.getEndOffset(); + this.mergeGap = mergeGap; + numTerms = 1; + } + + public boolean add(TermVectorOffsetInfo offsetinfo) { + if (offsetinfo.getStartOffset() > (endOffset + mergeGap)) { + return false; + } + offsetInfosList.add(offsetinfo); + numTerms++; + return true; + } + + public Iterator iterator() { + return offsetInfosList.iterator(); + } + + public int getStartOffset() { + return startOffset; + } + + public int numTerms() { + return numTerms; + } + + public void trim() { + int end = startOffset + (mergeGap / 2); + Iterator it = offsetInfosList.iterator(); + while (it.hasNext()) { + TermVectorOffsetInfo tvoi = it.next(); + if (tvoi.getStartOffset() > end) { + it.remove(); + } + } + } + } + + private static class FragmentInfoScoreSorter + implements java.util.Comparator { + + public int compare(FragmentInfo o1, FragmentInfo o2) { + int s1 = o1.numTerms(); + int s2 = o2.numTerms(); + if (s1 == s2) { + return o1.getStartOffset() < o2.getStartOffset() ? -1 : 1; + } + return s1 > s2 ? -1 : 1; + } + } + + private static class FragmentInfoPositionSorter + implements java.util.Comparator { + + public int compare(FragmentInfo o1, FragmentInfo o2) { + int s1 = o1.getStartOffset(); + int s2 = o2.getStartOffset(); + if (s1 == s2) { + return 0; + } + return s1 < s2 ? -1 : 1; + } + } + + private static class TermVectorOffsetInfoSorter + implements java.util.Comparator { + + public int compare(TermVectorOffsetInfo o1, TermVectorOffsetInfo o2) { + int s1 = o1.getStartOffset(); + int s2 = o2.getStartOffset(); + if (s1 == s2) { + return 0; + } + return s1 < s2 ? -1 : 1; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultQueryHits.java new file mode 100644 index 00000000000..f602087d66c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultQueryHits.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.core.query.lucene; + +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; + +/** + * DefaultQueryHits implements {@link QueryHits} based on a + * collection of {@link ScoreNode}s. + */ +public class DefaultQueryHits extends AbstractQueryHits { + + /** + * The total number of score nodes. + */ + private final int size; + + /** + * An iterator over the query nodes. + */ + private final Iterator scoreNodes; + + /** + * Creates a new DefaultQueryHits instance based on the passed + * scoreNodes. + * + * @param scoreNodes a collection of {@link ScoreNode}s. + */ + public DefaultQueryHits(Collection scoreNodes) { + this.size = scoreNodes.size(); + this.scoreNodes = scoreNodes.iterator(); + } + + /** + * {@inheritDoc} + */ + public ScoreNode nextScoreNode() throws IOException { + if (scoreNodes.hasNext()) { + return (ScoreNode) scoreNodes.next(); + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + public int getSize() { + return size; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultRedoLog.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultRedoLog.java new file mode 100644 index 00000000000..efbcb70a389 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultRedoLog.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.lucene.store.Directory; +import org.apache.jackrabbit.core.query.lucene.directory.IndexOutputStream; +import org.apache.jackrabbit.core.query.lucene.directory.IndexInputStream; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements a redo log for changes that have not been committed to disk. While + * nodes are added to and removed from the volatile index (held in memory) a + * redo log is written to keep track of the changes. In case the Jackrabbit + * process terminates unexpected the redo log is applied when Jackrabbit is + * restarted the next time. + *

    + * This class is not thread-safe. + */ +class DefaultRedoLog implements RedoLog { + + /** + * Logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(DefaultRedoLog.class); + + /** + * Default name of the redo log file + */ + static final String REDO_LOG = "redo.log"; + + /** + * Prefix of the redo log files. + */ + static final String REDO_LOG_PREFIX = "redo_"; + + /** + * The .log extension. + */ + static final String DOT_LOG = ".log"; + + /** + * Implements a {@link ActionCollector} that counts all entries and sets + * {@link #entryCount}. + */ + private final ActionCollector ENTRY_COUNTER = new ActionCollector() { + public void collect(MultiIndex.Action a) { + entryCount++; + } + }; + + /** + * The directory where the log file is stored. + */ + private final Directory dir; + + /** + * The name of the log file. + */ + private final String fileName; + + /** + * The number of log entries in the log file + */ + private int entryCount = 0; + + /** + * Writer to the log file + */ + private Writer out; + + /** + * Creates a new RedoLog instance, which stores its log in the + * given directory. + * + * @param dir the directory where the redo log file is located. + * @param fileName the name of the redo log file. + * @throws IOException if an error occurs while reading the redo log. + */ + private DefaultRedoLog(Directory dir, String fileName) throws IOException { + this.dir = dir; + this.fileName = fileName; + read(ENTRY_COUNTER); + } + + /** + * Creates a new RedoLog instance, which stores its log in the + * given directory. + * + * @param dir the directory where the redo log file is located. + * @param generation the redo log generation number. + * @return the redo log. + * @throws IOException if the redo log cannot be created. + */ + static RedoLog create(Directory dir, long generation) throws IOException { + String fileName; + if (generation == 0) { + fileName = REDO_LOG; + } else { + fileName = REDO_LOG_PREFIX + Long.toString( + generation, Character.MAX_RADIX) + DOT_LOG; + } + return new DefaultRedoLog(dir, fileName); + } + + /** + * Returns true if this redo log contains any entries, + * false otherwise. + * @return true if this redo log contains any entries, + * false otherwise. + */ + public boolean hasEntries() { + return entryCount > 0; + } + + /** + * Returns the number of entries in this redo log. + * @return the number of entries in this redo log. + */ + public int getSize() { + return entryCount; + } + + /** + * Returns a List with all {@link MultiIndex.Action} instances in the + * redo log. + * + * @return an List with all {@link MultiIndex.Action} instances in the + * redo log. + * @throws IOException if an error occurs while reading from the redo log. + */ + public List getActions() throws IOException { + final List actions = new ArrayList(); + read(new ActionCollector() { + public void collect(MultiIndex.Action a) { + actions.add(a); + } + }); + return actions; + } + + /** + * Appends an action to the log. + * + * @param action the action to append. + * @throws IOException if the node cannot be written to the redo + * log. + */ + public void append(MultiIndex.Action action) throws IOException { + initOut(); + out.write(action.toString() + "\n"); + entryCount++; + } + + /** + * Flushes all pending writes to the underlying file. + * @throws IOException if an error occurs while writing. + */ + public void flush() throws IOException { + if (out != null) { + out.flush(); + } + } + + /** + * Closes this redo log. + * + * @throws IOException if an error occurs while flushing pending writes. + */ + public void close() throws IOException { + if (out != null) { + out.close(); + out = null; + } + } + + /** + * Initializes the {@link #out} stream if it is not yet set. + * @throws IOException if an error occurs while creating the + * output stream. + */ + private void initOut() throws IOException { + if (out == null) { + OutputStream os = new IndexOutputStream(dir.createOutput(fileName)); + out = new BufferedWriter(new OutputStreamWriter(os)); + } + } + + /** + * Reads the log file and calls back {@link DefaultRedoLog.ActionCollector}. + * + * @param collector called back for each {@link MultiIndex.Action} read. + * @throws IOException if an error occurs while reading from the + * log file. + */ + private void read(ActionCollector collector) throws IOException { + if (!dir.fileExists(fileName)) { + return; + } + InputStream in = new IndexInputStream(dir.openInput(fileName)); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String line; + while ((line = reader.readLine()) != null) { + try { + collector.collect(MultiIndex.Action.fromString(line)); + } catch (IllegalArgumentException e) { + log.warn("Malformed redo entry: " + e.getMessage()); + } + } + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + log.warn("Exception while closing redo log: " + e.toString()); + } + } + } + } + + //-----------------------< internal >--------------------------------------- + + /** + * Helper interface to collect Actions read from the redo log. + */ + interface ActionCollector { + + /** Called when an action is created */ + void collect(MultiIndex.Action action); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultRedoLogFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultRedoLogFactory.java new file mode 100644 index 00000000000..f89cb12a407 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultRedoLogFactory.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.core.query.lucene; + +import java.io.IOException; + +/** + * DefaultRedoLogFactory... + */ +public class DefaultRedoLogFactory implements RedoLogFactory { + + public RedoLog createRedoLog(MultiIndex index) throws IOException { + return DefaultRedoLog.create(index.getDirectory(), index.getIndexGeneration()); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultXMLExcerpt.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultXMLExcerpt.java new file mode 100644 index 00000000000..04faccfdcd6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DefaultXMLExcerpt.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.core.query.lucene; + +import org.apache.lucene.index.TermPositionVector; + +import java.io.IOException; + +/** + * DefaultXMLExcerpt creates an XML excerpt of a matching node. + *
    + * E.g. if you search for 'jackrabbit' and 'query' you may get the following + * result for a node: + *

    + * <excerpt>
    + *     <fragment><highlight>Jackrabbit</highlight> implements both the mandatory XPath and optional SQL <highlight>query</highlight> syntax.</fragment>
    + *     <fragment>Before parsing the XPath <highlight>query</highlight> in <highlight>Jackrabbit</highlight>, the statement is surrounded</fragment>
    + * </excerpt>
    + * 
    + */ +public class DefaultXMLExcerpt extends AbstractExcerpt { + + /** + * {@inheritDoc} + */ + protected String createExcerpt(TermPositionVector tpv, + String text, + int maxFragments, + int maxFragmentSize) + throws IOException { + return DefaultHighlighter.highlight(tpv, getQueryTerms(), text, + maxFragments, maxFragmentSize / 2); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DerefQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DerefQuery.java new file mode 100644 index 00000000000..86d3c8624c3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DerefQuery.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.index.Term; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; +import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector; +import org.apache.jackrabbit.spi.Name; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Set; + +/** + * Implements a lucene Query which returns the nodes selected by + * a reference property of the context node. + */ +@SuppressWarnings("serial") +class DerefQuery extends Query { + + /** + * The context query + */ + private final Query contextQuery; + + /** + * The name of the reference property. + */ + private final String refProperty; + + /** + * The nameTest to apply on target node, or null if all + * target nodes should be selected. + */ + private final Name nameTest; + + /** + * The index format version. + */ + private final IndexFormatVersion version; + + /** + * The internal namespace mappings. + */ + private final NamespaceMappings nsMappings; + + /** + * The scorer of the context query + */ + private Scorer contextScorer; + + /** + * The scorer of the name test query + */ + private Scorer nameTestScorer; + + /** + * Creates a new DerefQuery based on a context + * query. + * + * @param context the context for this query. + * @param refProperty the name of the reference property. + * @param nameTest a name test or null if any node is + * selected. + * @param version the index format version. + * @param nsMappings the namespace mappings. + */ + DerefQuery(Query context, String refProperty, Name nameTest, + IndexFormatVersion version, NamespaceMappings nsMappings) { + this.contextQuery = context; + this.refProperty = refProperty; + this.nameTest = nameTest; + this.version = version; + this.nsMappings = nsMappings; + } + + /** + * Creates a Weight instance for this query. + * + * @param searcher the Searcher instance to use. + * @return a DerefWeight. + */ + public Weight createWeight(Searcher searcher) { + return new DerefWeight(searcher); + } + + /** + * Always returns 'DerefQuery'. + * + * @param field the name of a field. + * @return 'DerefQuery'. + */ + public String toString(String field) { + StringBuffer sb = new StringBuffer(); + sb.append("DerefQuery("); + sb.append(refProperty); + sb.append(", "); + sb.append(contextQuery); + sb.append(", "); + sb.append(nameTest); + sb.append(")"); + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + // no terms to extract + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + Query cQuery = contextQuery.rewrite(reader); + if (cQuery == contextQuery) { + return this; + } else { + return new DerefQuery(cQuery, refProperty, nameTest, version, nsMappings); + } + } + + //-------------------< DerefWeight >------------------------------------ + + /** + * The Weight implementation for this DerefQuery. + */ + private class DerefWeight extends Weight { + + /** + * The searcher in use + */ + private final Searcher searcher; + + /** + * Creates a new DerefWeight instance using + * searcher. + * + * @param searcher a Searcher instance. + */ + private DerefWeight(Searcher searcher) { + this.searcher = searcher; + } + + /** + * Returns this DerefQuery. + * + * @return this DerefQuery. + */ + public Query getQuery() { + return DerefQuery.this; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public void normalize(float norm) { + } + + /** + * Creates a scorer for this DerefQuery. + * + * @param reader a reader for accessing the index. + * @return a DerefScorer. + * @throws IOException if an error occurs while reading from the index. + */ + @Override + public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + contextScorer = contextQuery.weight(searcher).scorer(reader, scoreDocsInOrder, false); + if (nameTest != null) { + nameTestScorer = new NameQuery(nameTest, version, nsMappings).weight(searcher).scorer(reader, scoreDocsInOrder, false); + } + return new DerefScorer(searcher.getSimilarity(), reader); + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(); + } + } + + //----------------------< DerefScorer >--------------------------------- + + /** + * Implements a Scorer for this DerefQuery. + */ + private class DerefScorer extends Scorer { + + /** + * An IndexReader to access the index. + */ + private final IndexReader reader; + + /** + * BitSet storing the id's of selected documents + */ + private final BitSet hits; + + /** + * List of UUIDs of selected nodes + */ + private List uuids = null; + + /** + * The next document id to return + */ + private int nextDoc = -1; + + /** + * Creates a new DerefScorer. + * + * @param similarity the Similarity instance to use. + * @param reader for index access. + */ + protected DerefScorer(Similarity similarity, IndexReader reader) { + super(similarity); + this.reader = reader; + this.hits = new BitSet(reader.maxDoc()); + } + + @Override + public int nextDoc() throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + calculateChildren(); + nextDoc = hits.nextSetBit(nextDoc + 1); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + @Override + public int docID() { + return nextDoc; + } + + @Override + public float score() throws IOException { + return 1.0f; + } + + @Override + public int advance(int target) throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + nextDoc = NO_MORE_DOCS; + return nextDoc; + } + + calculateChildren(); + nextDoc = hits.nextSetBit(target); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + /** + * 1. do context query + * 2. go through each document from the query + * 3. find reference property UUIDs + * 4. Use UUIDs to find document number + * 5. Use the name test to filter the documents + * + * @throws IOException if an exception occurs while reading from the + * index. + */ + private void calculateChildren() throws IOException { + if (uuids == null) { + uuids = new ArrayList(); + contextScorer.score(new AbstractHitCollector() { + @Override + protected void collect(int doc, float score) { + hits.set(doc); + } + }); + + // collect nameTest hits + final BitSet nameTestHits = new BitSet(); + if (nameTestScorer != null) { + nameTestScorer.score(new AbstractHitCollector() { + @Override + protected void collect(int doc, float score) { + nameTestHits.set(doc); + } + }); + } + + // retrieve uuids of target nodes + String prefix = FieldNames.createNamedValue(refProperty, ""); + for (int i = hits.nextSetBit(0); i >= 0; i = hits.nextSetBit(i + 1)) { + String[] values = reader.document(i).getValues(FieldNames.PROPERTIES); + if (values == null) { + // no reference properties at all on this node + continue; + } + for (String value : values) { + if (value.startsWith(prefix)) { + uuids.add(value.substring(prefix.length())); + } + } + } + + // collect the doc ids of all target nodes. we reuse the existing + // bitset. + hits.clear(); + for (String uuid : uuids) { + TermDocs node = reader.termDocs(TermFactory.createUUIDTerm(uuid)); + try { + while (node.next()) { + hits.set(node.doc()); + } + } finally { + node.close(); + } + } + // filter out the target nodes that do not match the name test + // if there is any name test at all. + if (nameTestScorer != null) { + hits.and(nameTestHits); + } + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DescendantSelfAxisQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DescendantSelfAxisQuery.java new file mode 100644 index 00000000000..1318b93a1c9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DescendantSelfAxisQuery.java @@ -0,0 +1,644 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.Weight; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.util.BitSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Implements a lucene Query which filters a sub query by checking + * whether the nodes selected by that sub query are descendants or self of + * nodes selected by a context query. + */ +@SuppressWarnings("serial") +class DescendantSelfAxisQuery extends Query implements JackrabbitQuery { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(DescendantSelfAxisQuery.class); + + /** + * The context query + */ + private final Query contextQuery; + + /** + * The scorer of the context query + */ + private Scorer contextScorer; + + /** + * The sub query to filter + */ + private final Query subQuery; + + /** + * The minimal levels required between context and sub nodes for a sub node + * to match. + */ + private final int minLevels; + + /** + * The scorer of the sub query to filter + */ + private Scorer subScorer; + + /** + * Creates a new DescendantSelfAxisQuery based on a + * context and matches all descendants of the context nodes. + * Whether the context nodes match as well is controlled by + * includeSelf. + * + * @param context the context for this query. + * @param includeSelf if true this query acts like a + * descendant-or-self axis. If false this + * query acts like a descendant axis. + */ + public DescendantSelfAxisQuery(Query context, boolean includeSelf) { + this(context, new MatchAllDocsQuery(), includeSelf); + } + + /** + * Creates a new DescendantSelfAxisQuery based on a + * context query and filtering the sub query. + * + * @param context the context for this query. + * @param sub the sub query. + */ + public DescendantSelfAxisQuery(Query context, Query sub) { + this(context, sub, true); + } + + /** + * Creates a new DescendantSelfAxisQuery based on a + * context query and filtering the sub query. + * + * @param context the context for this query. + * @param sub the sub query. + * @param includeSelf if true this query acts like a + * descendant-or-self axis. If false this query acts like + * a descendant axis. + */ + public DescendantSelfAxisQuery(Query context, Query sub, boolean includeSelf) { + this(context, sub, includeSelf ? 0 : 1); + } + + /** + * Creates a new DescendantSelfAxisQuery based on a + * context query and filtering the sub query. + * + * @param context the context for this query. + * @param sub the sub query. + * @param minLevels the minimal levels required between context and sub + * nodes for a sub node to match. + */ + public DescendantSelfAxisQuery(Query context, Query sub, int minLevels) { + this.contextQuery = context; + this.subQuery = sub; + this.minLevels = minLevels; + } + + /** + * @return the context query of this DescendantSelfAxisQuery. + */ + Query getContextQuery() { + return contextQuery; + } + + /** + * @return true if the sub query of this DescendantSelfAxisQuery + * matches all nodes. + */ + boolean subQueryMatchesAll() { + return subQuery instanceof MatchAllDocsQuery; + } + + /** + * Returns the minimal levels required between context and sub nodes for a + * sub node to match. + *
      + *
    • 0: a sub node S matches if it is a context + * node or one of the ancestors of S is a context node.
    • + *
    • 1: a sub node S matches if one of the + * ancestors of S is a context node.
    • + *
    • n: a sub node S matches if + * S.getAncestor(S.getDepth() - n) is a context node.
    • + *
    + * + * @return the minimal levels required between context and sub nodes for a + * sub node to match. + */ + int getMinLevels() { + return minLevels; + } + + /** + * Creates a Weight instance for this query. + * + * @param searcher the Searcher instance to use. + * @return a DescendantSelfAxisWeight. + */ + public Weight createWeight(Searcher searcher) { + return new DescendantSelfAxisWeight(searcher); + } + + /** + * {@inheritDoc} + */ + public String toString(String field) { + StringBuffer sb = new StringBuffer(); + sb.append("DescendantSelfAxisQuery("); + sb.append(contextQuery); + sb.append(", "); + sb.append(subQuery); + sb.append(", "); + sb.append(minLevels); + sb.append(")"); + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + contextQuery.extractTerms(terms); + subQuery.extractTerms(terms); + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + Query cQuery = contextQuery.rewrite(reader); + Query sQuery = subQuery.rewrite(reader); + if (contextQuery instanceof DescendantSelfAxisQuery) { + DescendantSelfAxisQuery dsaq = (DescendantSelfAxisQuery) contextQuery; + if (dsaq.subQueryMatchesAll()) { + return new DescendantSelfAxisQuery(dsaq.getContextQuery(), + sQuery, dsaq.getMinLevels() + getMinLevels()).rewrite(reader); + } + } + if (cQuery == contextQuery && sQuery == subQuery) { + return this; + } else { + return new DescendantSelfAxisQuery(cQuery, sQuery, minLevels); + } + } + + //------------------------< JackrabbitQuery >------------------------------- + + /** + * {@inheritDoc} + */ + public QueryHits execute(final JackrabbitIndexSearcher searcher, + final SessionImpl session, + final Sort sort) throws IOException { + if (sort.getSort().length == 0 && subQueryMatchesAll()) { + // maps path String to ScoreNode + Map startingPoints = new TreeMap(); + QueryHits result = searcher.evaluate(getContextQuery()); + try { + // minLevels 0 and 1 are handled with a series of + // NodeTraversingQueryHits directly on result. For minLevels >= 2 + // intermediate ChildNodesQueryHits are required. + for (int i = 2; i <= getMinLevels(); i++) { + result = new ChildNodesQueryHits(result, session); + } + + ScoreNode sn; + while ((sn = result.nextScoreNode()) != null) { + NodeId id = sn.getNodeId(); + try { + Node node = session.getNodeById(id); + startingPoints.put(node.getPath(), sn); + } catch (ItemNotFoundException e) { + // JCR-3001 access denied to score node, will just skip it + log.warn("Access denied to node id {}.", id); + } catch (RepositoryException e) { + throw Util.createIOException(e); + } + } + } finally { + result.close(); + } + + // prune overlapping starting points + String previousPath = null; + for (Iterator it = startingPoints.keySet().iterator(); it.hasNext(); ) { + String path = it.next(); + // if the previous path is a prefix of this path then the + // current path is obsolete + if (previousPath != null && path.startsWith(previousPath)) { + it.remove(); + } else { + previousPath = path; + } + } + + final Iterator scoreNodes = startingPoints.values().iterator(); + return new AbstractQueryHits() { + + private NodeTraversingQueryHits currentTraversal; + + { + fetchNextTraversal(); + } + + public void close() throws IOException { + if (currentTraversal != null) { + currentTraversal.close(); + } + } + + public ScoreNode nextScoreNode() throws IOException { + while (currentTraversal != null) { + ScoreNode sn = currentTraversal.nextScoreNode(); + if (sn != null) { + return sn; + } else { + fetchNextTraversal(); + } + } + // if we get here there are no more score nodes + return null; + } + + private void fetchNextTraversal() throws IOException { + if (currentTraversal != null) { + currentTraversal.close(); + } + currentTraversal = null; + // We only need one node, but because of the acls, we'll + // iterate until we find a good one + while (scoreNodes.hasNext()) { + ScoreNode sn = scoreNodes.next(); + NodeId id = sn.getNodeId(); + try { + Node node = session.getNodeById(id); + currentTraversal = new NodeTraversingQueryHits( + node, getMinLevels() == 0); + break; + } catch (ItemNotFoundException e) { + // JCR-3001 node access denied, will just skip it + log.warn("Access denied to node id {}.", id); + } catch (RepositoryException e) { + throw Util.createIOException(e); + } + } + } + }; + } else { + return null; + } + } + + //--------------------< DescendantSelfAxisWeight >-------------------------- + + /** + * The Weight implementation for this + * DescendantSelfAxisWeight. + */ + private class DescendantSelfAxisWeight extends Weight { + + /** + * The searcher in use + */ + private final Searcher searcher; + + /** + * Creates a new DescendantSelfAxisWeight instance using + * searcher. + * + * @param searcher a Searcher instance. + */ + private DescendantSelfAxisWeight(Searcher searcher) { + this.searcher = searcher; + } + + //-----------------------------< Weight >------------------------------- + + /** + * Returns this DescendantSelfAxisQuery. + * + * @return this DescendantSelfAxisQuery. + */ + public Query getQuery() { + return DescendantSelfAxisQuery.this; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public void normalize(float norm) { + } + + /** + * Creates a scorer for this DescendantSelfAxisScorer. + * + * @param reader a reader for accessing the index. + * @return a DescendantSelfAxisScorer. + * @throws IOException if an error occurs while reading from the index. + */ + public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + contextScorer = searcher.createNormalizedWeight(contextQuery).scorer(reader, scoreDocsInOrder, false); + subScorer = searcher.createNormalizedWeight(subQuery).scorer(reader, scoreDocsInOrder, false); + HierarchyResolver resolver = (HierarchyResolver) reader; + return new DescendantSelfAxisScorer(searcher.getSimilarity(), reader, resolver); + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(); + } + } + + //----------------------< DescendantSelfAxisScorer >--------------------------------- + /** + * Implements a Scorer for this + * DescendantSelfAxisQuery. + */ + private class DescendantSelfAxisScorer extends Scorer { + + /** + * The HierarchyResolver of the index. + */ + private final HierarchyResolver hResolver; + + /** + * BitSet storing the id's of selected documents + */ + private final BitSet contextHits; + + /** + * Set true once the context hits have been calculated. + */ + private boolean contextHitsCalculated = false; + + /** + * Remember document numbers of ancestors during validation + */ + private int[] ancestorDocs = new int[2]; + + /** + * Reusable array that holds document numbers of parents. + */ + private int[] pDocs = new int[1]; + + /** + * Reusable array that holds a single document number. + */ + private final int[] singleDoc = new int[1]; + + /** + * The next document id to be returned + */ + private int currentDoc = -1; + + /** + * Creates a new DescendantSelfAxisScorer. + * + * @param similarity the Similarity instance to use. + * @param reader for index access. + * @param hResolver the hierarchy resolver of reader. + */ + protected DescendantSelfAxisScorer(Similarity similarity, + IndexReader reader, + HierarchyResolver hResolver) { + super(similarity); + this.hResolver = hResolver; + // todo reuse BitSets? + this.contextHits = new BitSet(reader.maxDoc()); + } + + @Override + public int nextDoc() throws IOException { + if (currentDoc == NO_MORE_DOCS) { + return currentDoc; + } + + collectContextHits(); + if (contextHits.isEmpty()) { + currentDoc = NO_MORE_DOCS; + } else { + if (subScorer != null) { + currentDoc = subScorer.nextDoc(); + } else { + currentDoc = NO_MORE_DOCS; + } + } + while (currentDoc != NO_MORE_DOCS) { + if (isValid(currentDoc)) { + return currentDoc; + } + + // try next + currentDoc = subScorer.nextDoc(); + } + return currentDoc; + } + + @Override + public int docID() { + return currentDoc; + } + + @Override + public float score() throws IOException { + return subScorer.score(); + } + + @Override + public int advance(int target) throws IOException { + if (currentDoc == NO_MORE_DOCS) { + return currentDoc; + } + + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3082 + if (target == NO_MORE_DOCS) { + if (subScorer != null) { + subScorer.advance(target); + } + currentDoc = NO_MORE_DOCS; + return currentDoc; + } + + currentDoc = subScorer.advance(target); + if (currentDoc == NO_MORE_DOCS) { + return NO_MORE_DOCS; + } else { + collectContextHits(); + return isValid(currentDoc) ? currentDoc : nextDoc(); + } + } + + private void collectContextHits() throws IOException { + if (!contextHitsCalculated) { + long time = System.currentTimeMillis(); + if (contextScorer != null) { + contextScorer.score(new AbstractHitCollector() { + @Override + protected void collect(int doc, float score) { + contextHits.set(doc); + } + }); // find all + } + contextHitsCalculated = true; + time = System.currentTimeMillis() - time; + if (log.isDebugEnabled()) { + log.debug("Collected {} context hits in {} ms for {}", + new Object[]{ + contextHits.cardinality(), + time, + DescendantSelfAxisQuery.this + }); + } + } + } + + /** + * Returns true if doc is a valid match from + * the sub scorer against the context hits. The caller must ensure + * that the context hits are calculated before this method is called! + * + * @param doc the document number. + * @return true if doc is valid. + * @throws IOException if an error occurs while reading from the index. + */ + private boolean isValid(int doc) throws IOException { + // check self if necessary + if (minLevels == 0 && contextHits.get(doc)) { + return true; + } + + // check if doc is a descendant of one of the context nodes + pDocs = hResolver.getParents(doc, pDocs); + + if (pDocs.length == 0) { + return false; + } + + int ancestorCount = 0; + // can only remember one parent doc per level + ancestorDocs[ancestorCount++] = pDocs[0]; + + // traverse + while (pDocs.length != 0) { + boolean valid = false; + for (int pDoc : pDocs) { + if (ancestorCount >= minLevels && contextHits.get(pDoc)) { + valid = true; + break; + } + } + if (valid) { + break; + } else { + // load next level + pDocs = getParents(pDocs, singleDoc); + // resize array if needed + if (ancestorCount == ancestorDocs.length) { + // double the size of the new array + int[] copy = new int[ancestorDocs.length * 2]; + System.arraycopy(ancestorDocs, 0, copy, 0, ancestorDocs.length); + ancestorDocs = copy; + } + if (pDocs.length != 0) { + // can only remember one parent doc per level + ancestorDocs[ancestorCount++] = pDocs[0]; + } + } + } + + if (pDocs.length > 0) { + // since current parentDocs are descendants of one of the context + // docs we can promote all ancestorDocs to the context hits + for (int i = 0; i < ancestorCount; i++) { + contextHits.set(ancestorDocs[i]); + } + return true; + } + return false; + } + + /** + * Returns the parent document numbers for the given docs. + * + * @param docs the current document numbers, for which to get the + * parents. + * @param pDocs an array of document numbers for reuse as return value. + * @return the parent document number for the given docs. + * @throws IOException if an error occurs while reading from the index. + */ + private int[] getParents(int[] docs, int[] pDocs) throws IOException { + // optimize single doc + if (docs.length == 1) { + return hResolver.getParents(docs[0], pDocs); + } else { + pDocs = new int[0]; + for (int doc : docs) { + int[] p = hResolver.getParents(doc, new int[0]); + int[] tmp = new int[p.length + pDocs.length]; + System.arraycopy(pDocs, 0, tmp, 0, pDocs.length); + System.arraycopy(p, 0, tmp, pDocs.length, p.length); + pDocs = tmp; + } + return pDocs; + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocId.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocId.java new file mode 100644 index 00000000000..26055a02900 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocId.java @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.util.BitSet; + +import org.apache.jackrabbit.core.id.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements a document id which can be based on a Node uuid or a lucene + * document number. + */ +abstract class DocId { + + static final int[] EMPTY = new int[0]; + + /** + * All DocIds with a value smaller than {@link Short#MAX_VALUE}. + */ + private static final PlainDocId[] LOW_DOC_IDS = new PlainDocId[Short.MAX_VALUE]; + + static { + for (int i = 0; i < LOW_DOC_IDS.length; i++) { + LOW_DOC_IDS[i] = new PlainDocId(i); + } + } + + /** + * Indicates a null DocId. Will be returned if the root node is asked for + * its parent. + */ + static final DocId NULL = new DocId() { + + /** + * Always returns an empty array. + * @param reader the index reader. + * @param docNumbers a int array for reuse as return value. + * @return always an empty array. + */ + final int[] getDocumentNumbers(MultiIndexReader reader, + int[] docNumbers) { + return EMPTY; + } + + /** + * Always returns this. + * @param offset the offset to apply. + * @return always this. + */ + final DocId applyOffset(int offset) { + return this; + } + + /** + * Always returns true. + * @param deleted the deleted documents. + * @return always true. + */ + final boolean isValid(BitSet deleted) { + return true; + } + }; + + /** + * Returns the document numbers of this DocId. An empty array + * is returned if this id is invalid. + * + * @param reader the IndexReader to resolve this DocId. + * @param docNumbers an array for reuse. An implementation should use the + * passed array as a container for the return value, + * unless the length of the returned array is different + * from docNumbers. In which case an + * implementation will create a new array with an + * appropriate size. + * @return the document numbers of this DocId or + * empty if it is invalid (e.g. does not exist). + * @throws IOException if an error occurs while reading from the index. + */ + abstract int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) + throws IOException; + + /** + * Applies an offset to this DocId. The returned DocId + * may be the same as this if this DocId does + * not need to know about an offset. + * + * @param offset the offset to apply to. + * @return DocId with offset applied. + */ + abstract DocId applyOffset(int offset); + + /** + * Returns true if this DocId is valid against the + * set of deleted documents; otherwise false. + * + * @param deleted the deleted documents. + * @return true if this DocId is not delted; + * otherwise false. + */ + abstract boolean isValid(BitSet deleted); + + /** + * Creates a DocId based on a document number. + * + * @param docNumber the document number. + * @return a DocId based on a document number. + */ + static DocId create(int docNumber) { + if (docNumber < Short.MAX_VALUE) { + // use cached values for docNumbers up to 32k + return LOW_DOC_IDS[docNumber]; + } else { + return new PlainDocId(docNumber); + } + } + + /** + * Creates a DocId based on a UUID. + * + * @param uuid the UUID + * @return a DocId based on the UUID. + * @throws IllegalArgumentException if the uuid is malformed. + */ + static DocId create(String uuid) { + return create(new NodeId(uuid)); + } + + /** + * Creates a DocId based on a node id. + * + * @param id the node id + * @return a DocId based on the node id + */ + static DocId create(NodeId id) { + return new UUIDDocId(id); + } + + /** + * Creates a DocId that references multiple UUIDs. + * + * @param uuids the UUIDs of the referenced nodes. + * @return a DocId based on multiple node UUIDs. + */ + static DocId create(String[] uuids) { + return new MultiUUIDDocId(uuids); + } + + //--------------------------< internal >------------------------------------ + + /** + * DocId based on a document number. + */ + private static final class PlainDocId extends DocId { + + /** + * The document number or -1 if not set. + */ + private final int docNumber; + + /** + * Creates a DocId based on a document number. + * + * @param docNumber the lucene document number. + */ + PlainDocId(int docNumber) { + this.docNumber = docNumber; + } + + /** + * @inheritDoc + */ + int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) { + if (docNumbers.length == 1) { + docNumbers[0] = docNumber; + return docNumbers; + } else { + return new int[]{docNumber}; + } + } + + /** + * @inheritDoc + */ + DocId applyOffset(int offset) { + return new PlainDocId(docNumber + offset); + } + + /** + * @inheritDoc + */ + boolean isValid(BitSet deleted) { + return !deleted.get(docNumber); + } + + /** + * Returns a String representation for this DocId. + * + * @return a String representation for this DocId. + */ + public String toString() { + return "PlainDocId(" + docNumber + ")"; + } + } + + /** + * DocId based on a node id. + */ + private static final class UUIDDocId extends DocId { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(UUIDDocId.class); + + /** + * The node identifier. + */ + private final NodeId id; + + /** + * The previously calculated foreign segment document id. + */ + private ForeignSegmentDocId doc; + + /** + * Creates a DocId based on a node id. + * + * @param id the node id. + */ + UUIDDocId(NodeId id) { + this.id = id; + } + + /** + * @inheritDoc + */ + int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) + throws IOException { + int realDoc = -1; + ForeignSegmentDocId segDocId = doc; + if (segDocId != null) { + realDoc = reader.getDocumentNumber(segDocId); + } + if (realDoc == -1) { + // Cached doc was invalid => create new one + segDocId = reader.createDocId(id); + if (segDocId != null) { + realDoc = reader.getDocumentNumber(segDocId); + doc = segDocId; + } else { + log.warn("Unknown parent node with id {}", id); + return EMPTY; + } + } + + if (docNumbers.length == 1) { + docNumbers[0] = realDoc; + return docNumbers; + } else { + return new int[]{realDoc}; + } + } + + /** + * This implementation will return this. Document number is + * not known until resolved in {@link #getDocumentNumbers(MultiIndexReader,int[])}. + * + * @inheritDoc + */ + DocId applyOffset(int offset) { + return this; + } + + /** + * Always returns true. + * + * @param deleted the deleted documents. + * @return always true. + */ + boolean isValid(BitSet deleted) { + return true; + } + + /** + * Returns a String representation for this DocId. + * + * @return a String representation for this DocId. + */ + public String toString() { + return "UUIDDocId(" + id + ")"; + } + } + + /** + * A DocId based on multiple UUIDDocIds. + */ + private static final class MultiUUIDDocId extends DocId { + + /** + * The internal uuid based doc ids. + */ + private final UUIDDocId[] docIds; + + /** + * @param uuids the uuids of the referenced nodes. + * @throws IllegalArgumentException if one of the uuids is malformed. + */ + MultiUUIDDocId(String[] uuids) { + this.docIds = new UUIDDocId[uuids.length]; + for (int i = 0; i < uuids.length; i++) { + docIds[i] = new UUIDDocId(new NodeId(uuids[i])); + } + } + + /** + * @inheritDoc + */ + int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) + throws IOException { + int[] tmp = new int[1]; + docNumbers = new int[docIds.length]; + for (int i = 0; i < docNumbers.length; i++) { + docNumbers[i] = docIds[i].getDocumentNumbers(reader, tmp)[0]; + } + return docNumbers; + } + + /** + * This implementation will return this. Document number is + * not known until resolved in {@link #getDocumentNumbers(MultiIndexReader,int[])}. + * + * @inheritDoc + */ + DocId applyOffset(int offset) { + return this; + } + + /** + * Always returns true. + * + * @param deleted the deleted documents. + * @return always true. + */ + boolean isValid(BitSet deleted) { + return true; + } + + /** + * Returns a String representation for this DocId. + * + * @return a String representation for this DocId. + */ + public String toString() { + StringBuffer sb = new StringBuffer("MultiUUIDDocId("); + String separator = ""; + for (UUIDDocId docId : docIds) { + sb.append(separator); + separator = ", "; + sb.append(docId.id); + } + sb.append(")"); + return sb.toString(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocNumberCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocNumberCache.java new file mode 100644 index 00000000000..bc1c74c1434 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocNumberCache.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.core.query.lucene; + +import org.apache.commons.collections.map.LRUMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements a Document number cache with a fixed size and a LRU strategy. + */ +final class DocNumberCache { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(DocNumberCache.class); + + /** + * Log cache statistics at most every 10 seconds. + */ + private static final long LOG_INTERVAL = 1000 * 10; + + /** + * The number of cache segments. + */ + private static final int CACHE_SEGMENTS = 0x10; + + /** + * Mask to calculate segment number. + */ + private static final int CACHE_SEGMENTS_MASK = CACHE_SEGMENTS - 1; + + /** + * LRU Maps where key=uuid value=reader;docNumber + */ + private final LRUMap[] docNumbers = new LRUMap[CACHE_SEGMENTS]; + + /** + * Timestamp of the last cache statistics log. + */ + private long lastLog; + + /** + * Cache misses. + */ + private long misses; + + /** + * Cache accesses; + */ + private long accesses; + + /** + * Creates a new DocNumberCache with a limiting + * size. + * + * @param size the cache limit. + */ + DocNumberCache(int size) { + size = size / CACHE_SEGMENTS; + if (size < 0x40) { + // minimum size is 0x40 * 0x10 = 1024 + size = 0x40; + } + for (int i = 0; i < docNumbers.length; i++) { + docNumbers[i] = new LRUMap(size); + } + } + + /** + * Puts a document number into the cache using a uuid as key. An entry is + * only overwritten if the according reader is younger than the reader + * associated with the existing entry. + * + * @param uuid the key. + * @param reader the index reader from where the document number was read. + * @param n the document number. + */ + void put(String uuid, CachingIndexReader reader, int n) { + LRUMap cacheSegment = docNumbers[getSegmentIndex(uuid.charAt(0))]; + synchronized (cacheSegment) { + Entry e = (Entry) cacheSegment.get(uuid); + if (e != null) { + // existing entry + // ignore if reader is older than the one in entry + if (reader.getCreationTick() <= e.creationTick) { + if (log.isDebugEnabled()) { + log.debug("Ignoring put(). New entry is not from a newer reader. " + + "existing: " + e.creationTick + + ", new: " + reader.getCreationTick()); + } + e = null; + } + } else { + // entry did not exist + e = new Entry(reader.getCreationTick(), n); + } + + if (e != null) { + cacheSegment.put(uuid, e); + } + } + } + + /** + * Returns the cache entry for uuid, or null if + * no entry exists for uuid. + * + * @param uuid the key. + * @return cache entry or null. + */ + Entry get(String uuid) { + LRUMap cacheSegment = docNumbers[getSegmentIndex(uuid.charAt(0))]; + Entry entry; + synchronized (cacheSegment) { + entry = (Entry) cacheSegment.get(uuid); + } + if (log.isInfoEnabled()) { + accesses++; + if (entry == null) { + misses++; + } + // log at most after 1000 accesses and every 10 seconds + if (accesses > 1000 && System.currentTimeMillis() - lastLog > LOG_INTERVAL) { + long ratio = 100; + if (misses != 0) { + ratio -= misses * 100L / accesses; + } + StringBuffer statistics = new StringBuffer(); + int inUse = 0; + for (LRUMap docNumber : docNumbers) { + inUse += docNumber.size(); + } + statistics.append("size=").append(inUse); + statistics.append("/").append(docNumbers[0].maxSize() * CACHE_SEGMENTS); + statistics.append(", #accesses=").append(accesses); + statistics.append(", #hits=").append((accesses - misses)); + statistics.append(", #misses=").append(misses); + statistics.append(", cacheRatio=").append(ratio).append("%"); + log.info(statistics.toString()); + accesses = 0; + misses = 0; + lastLog = System.currentTimeMillis(); + } + } + return entry; + } + + /** + * Returns the segment index for character c. + */ + private static int getSegmentIndex(char c) { + if (c > '9') { + c += 9; + } + return c & CACHE_SEGMENTS_MASK; + } + + public static final class Entry { + + /** + * The creation tick of the IndexReader. + */ + final long creationTick; + + /** + * The document number. + */ + final int doc; + + Entry(long creationTick, int doc) { + this.creationTick = creationTick; + this.doc = doc; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocOrderScoreNodeIterator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocOrderScoreNodeIterator.java new file mode 100644 index 00000000000..2ab79f78b2e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DocOrderScoreNodeIterator.java @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.ArrayList; + +/** + * Implements a ScoreNodeIterator that returns the score nodes in document order. + */ +class DocOrderScoreNodeIterator implements ScoreNodeIterator { + + /** Logger instance for this class */ + private static final Logger log = LoggerFactory.getLogger(DocOrderScoreNodeIterator.class); + + /** A node iterator with ordered nodes */ + private ScoreNodeIterator orderedNodes; + + /** Unordered list of {@link ScoreNode}[]s. */ + private final List scoreNodes; + + /** ItemManager to turn UUIDs into Node instances */ + protected final ItemManager itemMgr; + + /** + * Apply document order on the score nodes with this selectorIndex. + */ + private final int selectorIndex; + + /** + * Creates a DocOrderScoreNodeIterator that orders the nodes in + * scoreNodes in document order. + * + * @param itemMgr the item manager of the session executing the + * query. + * @param scoreNodes the ids of the matching nodes with their score + * value. List<ScoreNode[]> + * @param selectorIndex apply document order on the score nodes with this + * selectorIndex. + */ + DocOrderScoreNodeIterator(ItemManager itemMgr, + List scoreNodes, + int selectorIndex) { + this.itemMgr = itemMgr; + this.scoreNodes = scoreNodes; + this.selectorIndex = selectorIndex; + } + + /** + * {@inheritDoc} + */ + public Object next() { + return nextScoreNodes(); + } + + /** + * {@inheritDoc} + */ + public ScoreNode[] nextScoreNodes() { + initOrderedIterator(); + return orderedNodes.nextScoreNodes(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + /** + * {@inheritDoc} + */ + public void skip(long skipNum) { + initOrderedIterator(); + orderedNodes.skip(skipNum); + } + + /** + * Returns the number of nodes in this iterator. + *

    + * Note: The number returned by this method may differ from the number + * of nodes actually returned by calls to hasNext() / getNextNode()! This + * is because this iterator works on a lazy instantiation basis and while + * iterating over the nodes some of them might have been deleted in the + * meantime. Those will not be returned by getNextNode(). As soon as an + * invalid node is detected, the size of this iterator is adjusted. + * + * @return the number of node in this iterator. + */ + public long getSize() { + if (orderedNodes != null) { + return orderedNodes.getSize(); + } else { + return scoreNodes.size(); + } + } + + /** + * {@inheritDoc} + */ + public long getPosition() { + initOrderedIterator(); + return orderedNodes.getPosition(); + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + initOrderedIterator(); + return orderedNodes.hasNext(); + } + + //------------------------< internal >-------------------------------------- + + /** + * Initializes the NodeIterator in document order + */ + private void initOrderedIterator() { + if (orderedNodes != null) { + return; + } + long time = System.currentTimeMillis(); + ScoreNode[][] nodes = scoreNodes.toArray(new ScoreNode[scoreNodes.size()][]); + + final List invalidIDs = new ArrayList(2); + + do { + if (invalidIDs.size() > 0) { + // previous sort run was not successful -> remove failed uuids + List tmp = new ArrayList(); + for (ScoreNode[] node : nodes) { + if (!invalidIDs.contains(node[selectorIndex].getNodeId())) { + tmp.add(node); + } + } + nodes = tmp.toArray(new ScoreNode[tmp.size()][]); + invalidIDs.clear(); + } + + try { + // sort the uuids + Arrays.sort(nodes, new Comparator() { + public int compare(ScoreNode[] o1, ScoreNode[] o2) { + ScoreNode n1 = o1[selectorIndex]; + ScoreNode n2 = o2[selectorIndex]; + // handle null values + // null is considered less than any value + if (n1 == n2) { + return 0; + } else if (n1 == null) { + return -1; + } else if (n2 == null) { + return 1; + } + try { + NodeImpl node1; + try { + node1 = (NodeImpl) itemMgr.getItem(n1.getNodeId()); + } catch (RepositoryException e) { + log.warn("Node " + n1.getNodeId() + " does not exist anymore: " + e); + // node does not exist anymore + invalidIDs.add(n1.getNodeId()); + SortFailedException sfe = new SortFailedException(); + sfe.initCause(e); + throw sfe; + } + NodeImpl node2; + try { + node2 = (NodeImpl) itemMgr.getItem(n2.getNodeId()); + } catch (RepositoryException e) { + log.warn("Node " + n2.getNodeId() + " does not exist anymore: " + e); + // node does not exist anymore + invalidIDs.add(n2.getNodeId()); + SortFailedException sfe = new SortFailedException(); + sfe.initCause(e); + throw sfe; + } + Path.Element[] path1 = node1.getPrimaryPath().getElements(); + Path.Element[] path2 = node2.getPrimaryPath().getElements(); + + // find nearest common ancestor + int commonDepth = 0; // root + while (path1.length > commonDepth && path2.length > commonDepth) { + if (path1[commonDepth].equals(path2[commonDepth])) { + commonDepth++; + } else { + break; + } + } + // path elements at last depth were equal + commonDepth--; + + // check if either path is an ancestor of the other + if (path1.length - 1 == commonDepth) { + // path1 itself is ancestor of path2 + return -1; + } + if (path2.length - 1 == commonDepth) { + // path2 itself is ancestor of path1 + return 1; + } + // get common ancestor node + NodeImpl commonNode = (NodeImpl) node1.getAncestor(commonDepth); + // move node1/node2 to the commonDepth + 1 + // node1 and node2 then will be child nodes of commonNode + node1 = (NodeImpl) node1.getAncestor(commonDepth + 1); + node2 = (NodeImpl) node2.getAncestor(commonDepth + 1); + for (NodeIterator it = commonNode.getNodes(); it.hasNext();) { + Node child = it.nextNode(); + if (child.isSame(node1)) { + return -1; + } else if (child.isSame(node2)) { + return 1; + } + } + log.error("Internal error: unable to determine document order of nodes:"); + log.error("\tNode1: " + node1.getPath()); + log.error("\tNode2: " + node2.getPath()); + } catch (RepositoryException e) { + log.error("Exception while sorting nodes in document order: " + e.toString(), e); + } + // if we get here something went wrong + // remove both uuids from array + invalidIDs.add(n1.getNodeId()); + invalidIDs.add(n2.getNodeId()); + // terminate sorting + throw new SortFailedException(); + } + }); + } catch (SortFailedException e) { + // retry + } + + } while (invalidIDs.size() > 0); + + if (log.isDebugEnabled()) { + log.debug("" + nodes.length + " node(s) ordered in " + (System.currentTimeMillis() - time) + " ms"); + } + orderedNodes = new ScoreNodeIteratorImpl(nodes); + } + + /** + * Indicates that sorting failed. + */ + @SuppressWarnings("serial") + private static final class SortFailedException extends RuntimeException { + } +} diff --git a/src/java/org/apache/jackrabbit/core/search/lucene/DoubleField.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DoubleField.java similarity index 83% rename from src/java/org/apache/jackrabbit/core/search/lucene/DoubleField.java rename to jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DoubleField.java index c6634d36d15..1e132ede91f 100644 --- a/src/java/org/apache/jackrabbit/core/search/lucene/DoubleField.java +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DoubleField.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.core.search.lucene; +package org.apache.jackrabbit.core.query.lucene; /** * The DoubleField class is a utility to convert double diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DynamicPooledExecutor.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DynamicPooledExecutor.java new file mode 100644 index 00000000000..6848404b783 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/DynamicPooledExecutor.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.core.query.lucene; + +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * DynamicPooledExecutor implements an executor, which dynamically + * adjusts its maximum number of threads according to the number of available + * processors returned by {@link Runtime#availableProcessors()}. + */ +public class DynamicPooledExecutor implements Executor { + + /** + * Number of instances that access the underlying executor. + * Used to automatically shutdown the thread pool when unused. + */ + private static int instances = 0; + + /** + * The underlying pooled executor. + */ + private static ThreadPoolExecutor executor = null; + + /** + * The time (in milliseconds) when the pool size was last checked. + */ + private static long lastCheck; + + /** + * Creates a new DynamicPooledExecutor. + */ + public DynamicPooledExecutor() { + startInstance(); + } + + /** + * Adjusts the pool size at most once every second. + */ + private static synchronized ThreadPoolExecutor adjustPoolSize() { + long now = System.currentTimeMillis(); + if (lastCheck + 1000 < now) { + int n = Runtime.getRuntime().availableProcessors(); + if (n != executor.getMaximumPoolSize()) { + executor.setMaximumPoolSize(n); + } + lastCheck = now; + } + return executor; + } + + /** + * Executes the given command. This method will block if all threads in the + * pool are busy and return only when the command has been accepted. Care + * must be taken, that no deadlock occurs when multiple commands are + * scheduled for execution. In general commands should not depend on the + * execution of other commands! + * + * @param command the command to execute. + */ + public void execute(Runnable command) { + ThreadPoolExecutor executor = adjustPoolSize(); + if (executor.getMaximumPoolSize() == 1) { + // if there is only one processor execute with current thread + command.run(); + } else { + executor.execute(command); + } + } + + public void close() { + stopInstance(); + } + + private static synchronized void startInstance() { + instances++; + if (executor == null) { + ThreadFactory f = new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "DynamicPooledExecutor"); + t.setDaemon(true); + return t; + } + }; + executor = new ThreadPoolExecutor( + 1, Runtime.getRuntime().availableProcessors(), + 500, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), f); + lastCheck = System.currentTimeMillis(); + } + } + + private static synchronized void stopInstance() { + instances--; + if (instances == 0) { + executor.shutdown(); + try { + executor.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // ignore and continue + } + executor = null; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/EmptyTermDocs.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/EmptyTermDocs.java new file mode 100644 index 00000000000..27f30f3b55d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/EmptyTermDocs.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.core.query.lucene; + +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; + +/** + * EmptyTermDocs implements a TermDocs, which is empty. + */ +class EmptyTermDocs implements TermDocs { + + /** + * Single instance of this class. + */ + public static final TermDocs INSTANCE = new EmptyTermDocs(); + + private EmptyTermDocs() { + } + + public void seek(Term term) { + } + + public void seek(TermEnum termEnum) { + } + + public int doc() { + return -1; + } + + public int freq() { + return -1; + } + + public boolean next() { + return false; + } + + public int read(int[] docs, int[] freqs) { + return 0; + } + + public boolean skipTo(int target) { + return false; + } + + public void close() { + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ExcerptProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ExcerptProvider.java new file mode 100644 index 00000000000..327142136b1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ExcerptProvider.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.core.query.lucene; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.lucene.search.Query; + +import java.io.IOException; + +/** + * ExcerptProvider defines an interface to create an excerpt for + * a matching node. The format of the excerpt is implementation specific. + */ +public interface ExcerptProvider { + + /** + * Name of the rep:excerpt function. + */ + Name REP_EXCERPT = NameFactoryImpl.getInstance().create( + Name.NS_REP_URI, "excerpt(.)"); + + /** + * Initializes this excerpt provider. + * + * @param query excerpts will be based on this query. + * @param index provides access to the search index. + * @throws IOException if an error occurs while initializing this excerpt + * provider. + */ + void init(Query query, SearchIndex index) throws IOException; + + /** + * Returns the XML excerpt for the node with id. + * + * @param id a node id. + * @param maxFragments the maximum number of fragments to create. + * @param maxFragmentSize the maximum number of characters in a fragment. + * @return the XML excerpt or null if there is no node with + * id. + * @throws IOException if an error occurs while creating the excerpt. + */ + String getExcerpt(NodeId id, int maxFragments, int maxFragmentSize) + throws IOException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldComparatorBase.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldComparatorBase.java new file mode 100644 index 00000000000..f7fbbd715d8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldComparatorBase.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.core.query.lucene; + +import org.apache.lucene.search.FieldComparator; + +import java.io.IOException; + +/** + * Abstract base class for FieldComparator implementations + * which are based on values in the form of Comparables. + */ +abstract public class FieldComparatorBase extends FieldComparator { + + /** + * The bottom value. + */ + private Comparable bottom; + + /** + * Value for a document + * + * @param doc id of the document + * @return the value for the given id + */ + protected abstract Comparable sortValue(int doc); + + /** + * Retrieves the value of a given slot + * + * @param slot index of the value to retrieve + * @return the value in the given slot + */ + protected abstract Comparable getValue(int slot); + + /** + * Puts a value into a given slot + * + * @param slot index where to put the value + * @param value the value to put into the given slot + */ + protected abstract void setValue(int slot, Comparable value); + + @Override + public int compare(int slot1, int slot2) { + return compare(getValue(slot1), getValue(slot2)); + } + + @Override + public int compareBottom(int doc) throws IOException { + return compare(bottom, sortValue(doc)); + } + + @Override + public void setBottom(int slot) { + bottom = getValue(slot); + } + + /** + * Compare two values + * + * @param val1 first value + * @param val2 second value + * @return A negative integer if val1 comes before val2, + * a positive integer if val1 comes after val2 and + * 0 if val1 and val2 are equal. + */ + protected int compare(Comparable val1, Comparable val2) { + if (val1 == null) { + if (val2 == null) { + return 0; + } + return -1; + } + else if (val2 == null) { + return 1; + } + return Util.compare(val1, val2); + } + + @Override + public void copy(int slot, int doc) throws IOException { + setValue(slot, sortValue(doc)); + } + + @Override + public Comparable value(int slot) { + return getValue(slot); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldComparatorDecorator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldComparatorDecorator.java new file mode 100644 index 00000000000..d3ea3e1ac8d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldComparatorDecorator.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.core.query.lucene; + +import org.apache.lucene.index.IndexReader; + +import java.io.IOException; + +/** + * Implements a FieldComparator which decorates a + * base comparator. + */ +abstract class FieldComparatorDecorator extends FieldComparatorBase { + + /** + * The base comparator + */ + private final FieldComparatorBase base; + + /** + * Create a new instance which delegates to a base comparator. + * @param base delegatee + */ + public FieldComparatorDecorator(FieldComparatorBase base) { + this.base = base; + } + + @Override + public void setNextReader(IndexReader reader, int docBase) throws IOException { + base.setNextReader(reader, docBase); + } + + @Override + protected Comparable sortValue(int doc) { + return base.sortValue(doc); + } + + @Override + protected Comparable getValue(int slot) { + return base.getValue(slot); + } + + @Override + protected void setValue(int slot, Comparable value) { + base.setValue(slot, value); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.java new file mode 100644 index 00000000000..b07a8e2f415 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldNames.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.core.query.lucene; + +/** + * Defines field names that are used internally to store UUID, etc in the + * search index. + */ +public class FieldNames { + + /** + * Private constructor. + */ + private FieldNames() { + } + + /** + * Name of the field that contains the UUID of the node. Terms are stored + * but not tokenized. + */ + public static final String UUID = "_:UUID".intern(); + + /** + * Name of the field that contains the fulltext index including terms + * from all properties of a node. Terms are tokenized. + */ + public static final String FULLTEXT = "_:FULLTEXT".intern(); + + /** + * Prefix for all field names that are fulltext indexed by property name. + */ + public static final String FULLTEXT_PREFIX = "FULL:"; + + /** + * Name of the field that contains the UUID of the parent node. Terms are + * stored and but not tokenized. + */ + public static final String PARENT = "_:PARENT".intern(); + + /** + * Name of the field that contains the label of the node. Terms are not + * tokenized. + */ + public static final String LABEL = "_:LABEL".intern(); + + /** + * Name of the field that contains the local name of the node. Terms are not + * tokenized. + */ + public static final String LOCAL_NAME = "_:LOCAL_NAME".intern(); + + /** + * Name of the field that contains the namespace URI of the node name. Terms + * are not tokenized. + */ + public static final String NAMESPACE_URI = "_:NAMESPACE_URI".intern(); + + /** + * Name of the field that contains the names of multi-valued properties that + * hold more than one value. Terms are not tokenized and not stored, only + * indexed. + */ + public static final String MVP = "_:MVP".intern(); + + /** + * Name of the field that contains all values of properties that are indexed + * as is without tokenizing. Terms are prefixed with the property name. + */ + public static final String PROPERTIES = "_:PROPERTIES".intern(); + + /** + * Name of the field that contains the names of all properties that are set + * on an indexed node. + */ + public static final String PROPERTIES_SET = "_:PROPERTIES_SET".intern(); + + /** + * Name of the field that contains the UUIDs of the aggregated nodes. The + * terms are not tokenized and not stored, only indexed. + */ + public static final String AGGREGATED_NODE_UUID = "_:AGGR_NODE_UUID".intern(); + + /** + * Name of the field that contains the lengths of properties. The lengths + * are encoded using {@link #createNamedLength(String, long)}. + */ + public static final String PROPERTY_LENGTHS = "_:PROPERTY_LENGTHS".intern(); + + /** + * Name of the field that marks nodes that require reindexing because the + * text extraction process timed out. See also {@link IndexingQueue}. + */ + public static final String REINDEXING_REQUIRED = "_:REINDEXING_REQUIRED".intern(); + + /** + * Name of the field that marks shareable nodes. + */ + public static final String SHAREABLE_NODE = "_:SHAREABLE_NODE".intern(); + + /** + * Name of the field that contains all weak reference property values. + */ + public static final String WEAK_REFS = "_:WEAK_REFS".intern(); + + /** + * Returns a named length for use as a term in the index. The named length + * is of the form: propertyName + '[' + + * {@link LongField#longToString(long)}. + * + * @param propertyName a property name. + * @param length the length of the property value. + * @return the named length string for use as a term in the index. + */ + public static String createNamedLength(String propertyName, long length) { + return propertyName + '[' + LongField.longToString(length); + } + + /** + * Returns a named value for use as a term in the index. The named + * value is of the form: fieldName + '[' + value + * + * @param fieldName the field name. + * @param value the value. + * @return value prefixed with field name. + */ + public static String createNamedValue(String fieldName, String value) { + return fieldName + '[' + value; + } + + /** + * Returns the length of the field prefix in namedValue. See + * also {@link #createNamedValue(String, String)}. If namedValue + * does not contain a name prefix, this method return 0. + * + * @param namedValue the named value as created by {@link #createNamedValue(String, String)}. + * @return the length of the field prefix including the separator char '['. + */ + public static int getNameLength(String namedValue) { + return namedValue.indexOf('[') + 1; + } + + /** + * Returns true if the given fieldName denotes a + * fulltext field like {@link #FULLTEXT} or a field with a + * {@link #FULLTEXT_PREFIX}. + * + * @param fieldName a field name. + * @return true if fieldName is a fulltext field; + * false otherwise. + */ + public static boolean isFulltextField(String fieldName) { + return fieldName.equals(FULLTEXT) + || fieldName.indexOf(FULLTEXT_PREFIX) != -1; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldSelectors.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldSelectors.java new file mode 100644 index 00000000000..01403c84f06 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FieldSelectors.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.core.query.lucene; + +import org.apache.lucene.document.FieldSelector; +import org.apache.lucene.document.FieldSelectorResult; + +/** + * FieldSelectors contains commonly used field selectors. + */ +public class FieldSelectors { + + /** + * Do not instantiate. + */ + private FieldSelectors() { + } + + @SuppressWarnings("serial") + public static final FieldSelector UUID = new FieldSelector() { + /** + * Only accepts {@link FieldNames#UUID}. + * + * @param fieldName the field name to check. + * @return result. + */ + public FieldSelectorResult accept(String fieldName) { + if (FieldNames.UUID == fieldName) { + return FieldSelectorResult.LOAD_AND_BREAK; + } else { + return FieldSelectorResult.NO_LOAD; + } + } + }; + + @SuppressWarnings("serial") + public static final FieldSelector UUID_AND_PARENT = new FieldSelector() { + /** + * Accepts {@link FieldNames#UUID} and {@link FieldNames#PARENT}. + * + * @param fieldName the field name to check. + * @return result. + */ + public FieldSelectorResult accept(String fieldName) { + if (FieldNames.UUID == fieldName) { + return FieldSelectorResult.LOAD; + } else if (FieldNames.PARENT == fieldName) { + return FieldSelectorResult.LOAD; + } else { + return FieldSelectorResult.NO_LOAD; + } + } + }; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FileBasedNamespaceMappings.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FileBasedNamespaceMappings.java new file mode 100644 index 00000000000..67c18a03ed1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FileBasedNamespaceMappings.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.core.query.lucene; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.NamespaceException; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * The class NamespaceMappings implements a + * {@link NamespaceResolver} that holds a namespace + * mapping that is used internally in the search index. Storing paths with the + * full uri of a namespace would require too much space in the search index. + *

    + * Whenever a yet unknown namespace uri to prefix mapping is requested, a new + * prefix is created on the fly and associated with the namespace. Known + * namespace mappings are stored in a properties file. + */ +public class FileBasedNamespaceMappings extends AbstractNamespaceMappings { + + /** + * Default logger instance for this class + */ + private static Logger log = LoggerFactory.getLogger(FileBasedNamespaceMappings.class); + + /** + * Location of the file that persists the uri / prefix mappings + */ + private final File storage; + + /** + * Map of uris indexed by prefixes + */ + private Map prefixToURI = new HashMap(); + + /** + * Map of prefixes indexed by uris + */ + private Map uriToPrefix = new HashMap(); + + /** + * Current prefix count. + */ + private int prefixCount; + + /** + * Creates NamespaceMappings instance. Initial mappings are + * loaded from file. + * + * @param file the File to load initial mappings. + * @throws IOException if an error occurs while reading initial namespace + * mappings from file. + */ + public FileBasedNamespaceMappings(File file) throws IOException { + storage = file; + load(); + } + + /** + * Returns a namespace uri for a prefix. + * + * @param prefix the namespace prefix. + * @return the namespace uri. + * @throws NamespaceException if no namespace uri is registered for + * prefix. + */ + public synchronized String getURI(String prefix) throws NamespaceException { + if (!prefixToURI.containsKey(prefix)) { + throw new NamespaceException(prefix + ": is not a registered namespace prefix."); + } + return prefixToURI.get(prefix); + } + + /** + * Returns a prefix for the namespace uri. If a namespace + * mapping exists, the already known prefix is returned; otherwise a new + * prefix is created and assigned to the namespace uri. + * + * @param uri the namespace uri. + * @return the prefix for the namespace uri. + * @throws NamespaceException if an yet unknown namespace uri / prefix + * mapping could not be stored. + */ + public synchronized String getPrefix(String uri) throws NamespaceException { + String prefix = uriToPrefix.get(uri); + if (prefix == null) { + // make sure prefix is not taken + while (prefixToURI.get(String.valueOf(prefixCount)) != null) { + prefixCount++; + } + prefix = String.valueOf(prefixCount); + prefixToURI.put(prefix, uri); + uriToPrefix.put(uri, prefix); + log.debug("adding new namespace mapping: " + prefix + " -> " + uri); + try { + store(); + } catch (IOException e) { + throw new NamespaceException("Could not obtain a prefix for uri: " + uri, e); + } + } + return prefix; + } + + //-----------------------< internal >--------------------------------------- + + /** + * Loads currently known mappings from a .properties file. + * + * @throws IOException if an error occurs while reading from the file. + */ + private void load() throws IOException { + if (storage.exists()) { + InputStream in = new FileInputStream(storage); + try { + Properties props = new Properties(); + log.debug("loading namespace mappings..."); + props.load(in); + + // read mappings from properties + for (Object o : props.keySet()) { + String prefix = (String) o; + String uri = props.getProperty(prefix); + log.debug(prefix + " -> " + uri); + prefixToURI.put(prefix, uri); + uriToPrefix.put(uri, prefix); + } + prefixCount = props.size(); + log.debug("namespace mappings loaded."); + } finally { + in.close(); + } + } + } + + /** + * Writes the currently known mappings into a .properties file. + * + * @throws IOException if an error occurs while writing the file. + */ + private void store() throws IOException { + Properties props = new Properties(); + + // store mappings in properties + props.putAll(prefixToURI); + + OutputStream out = new FileOutputStream(storage); + try { + out = new BufferedOutputStream(out); + props.store(out, null); + } finally { + // make sure stream is closed + out.close(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterMultiColumnQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterMultiColumnQuery.java new file mode 100644 index 00000000000..c0a70980b9a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterMultiColumnQuery.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.core.query.lucene; + +import java.io.IOException; +import java.util.Arrays; + +import org.apache.jackrabbit.core.query.lucene.constraint.Constraint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FilterMultiColumnQuery wraps a multi column query and filters + * out rows that do not satisfy a given constraint. + */ +public class FilterMultiColumnQuery implements MultiColumnQuery { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(FilterMultiColumnQuery.class); + + /** + * The query to filter. + */ + private final MultiColumnQuery query; + + /** + * The constraint for filtering. + */ + private final Constraint constraint; + + /** + * Creates a new filter multi column query for the given query + * and constraint. + * + * @param query the query to filter. + * @param constraint the constraint for filtering. + */ + public FilterMultiColumnQuery(MultiColumnQuery query, + Constraint constraint) { + this.query = query; + this.constraint = constraint; + } + + /** + * {@inheritDoc} + */ + public MultiColumnQueryHits execute(final JackrabbitIndexSearcher searcher, + Ordering[] orderings, + long resultFetchHint) + throws IOException { + MultiColumnQueryHits hits = new FilterMultiColumnQueryHits(query.execute( + searcher, orderings, resultFetchHint)) { + + { + log.debug(Arrays.asList(getSelectorNames()).toString()); + } + + public ScoreNode[] nextScoreNodes() throws IOException { + ScoreNode[] next; + do { + next = super.nextScoreNodes(); + if (log.isDebugEnabled()) { + if (next != null) { + log.debug(Arrays.asList(next).toString()); + } + } + } while (next != null && !constraint.evaluate(next, getSelectorNames(), searcher)); + return next; + } + + public int getSize() { + return -1; + } + + public void skip(int n) throws IOException { + while (n-- > 0) { + nextScoreNodes(); + } + } + }; + if (orderings.length > 0) { + hits = new SortedMultiColumnQueryHits(hits, orderings, searcher.getIndexReader()); + } + return hits; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterMultiColumnQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterMultiColumnQueryHits.java new file mode 100644 index 00000000000..1bd5e7ebdc3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterMultiColumnQueryHits.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.core.query.lucene; + +import org.apache.jackrabbit.spi.Name; + +import java.io.IOException; + +/** + * FilterMultiColumnQueryHits implements a + * {@link MultiColumnQueryHits} filter that forwards each call to the underlying + * query hits. + */ +public class FilterMultiColumnQueryHits implements MultiColumnQueryHits { + + /** + * The underlying query hits. + */ + private final MultiColumnQueryHits hits; + + /** + * Creates a new FilterMultiColumnQueryHits, which forwards + * each call to hits. + * + * @param hits the underlying query hits. + */ + public FilterMultiColumnQueryHits(MultiColumnQueryHits hits) { + this.hits = hits; + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + hits.close(); + } + + /** + * {@inheritDoc} + */ + public int getSize() { + return hits.getSize(); + } + + /** + * {@inheritDoc} + */ + public ScoreNode[] nextScoreNodes() throws IOException { + return hits.nextScoreNodes(); + } + + /** + * {@inheritDoc} + */ + public Name[] getSelectorNames() { + return hits.getSelectorNames(); + } + + /** + * {@inheritDoc} + */ + public void skip(int n) throws IOException { + hits.skip(n); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterSearcher.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterSearcher.java new file mode 100644 index 00000000000..322e5c74120 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/FilterSearcher.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.FieldSelector; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Filter; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TopFieldDocs; +import org.apache.lucene.search.Weight; + +/** + * FilterSearcher wraps another Searcher and forwards all + * calls to the wrapped Searcher. + */ +class FilterSearcher extends Searcher { + + private Searcher s; + + FilterSearcher(Searcher searcher) { + this.s = searcher; + } + + @Override + public void search(Weight weight, Filter filter, Collector results) + throws IOException { + s.search(weight, filter, results); + } + + @Override + public void close() throws IOException { + s.close(); + } + + @Override + public int docFreq(Term term) throws IOException { + return s.docFreq(term); + } + + @Override + public int maxDoc() throws IOException { + return s.maxDoc(); + } + + @Override + public TopDocs search(Weight weight, Filter filter, int n) + throws IOException { + return s.search(weight, filter, n); + } + + @Override + public Document doc(int i) throws CorruptIndexException, IOException { + return s.doc(i); + } + + @Override + public Document doc(int docid, FieldSelector fieldSelector) + throws CorruptIndexException, IOException { + return s.doc(docid, fieldSelector); + } + + @Override + public Query rewrite(Query query) throws IOException { + return s.rewrite(query); + } + + @Override + public Explanation explain(Weight weight, int doc) throws IOException { + return s.explain(weight, doc); + } + + @Override + public TopFieldDocs search(Weight weight, Filter filter, int n, Sort sort) + throws IOException { + return null; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ForeignSegmentDocId.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ForeignSegmentDocId.java new file mode 100644 index 00000000000..babeadb24e4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ForeignSegmentDocId.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.core.query.lucene; + +import java.io.IOException; +import java.util.BitSet; + +/** + * A DocId that contains a document number and the creation tick + * of the index segment. + */ +final class ForeignSegmentDocId extends DocId { + + /** + * Empty array of {@link ForeignSegmentDocId}s. + */ + static final ForeignSegmentDocId[] EMPTY_ARRAY = new ForeignSegmentDocId[0]; + + /** + * The document number. + */ + private final int docNumber; + + /** + * The creation tick of the index segment. + */ + private final long creationTick; + + /** + * Creates a DocId based on a document number in the index + * segment with the given creationTick. + * + * @param docNumber the lucene document number. + * @param creationTick the creation tick of the index segment. + */ + ForeignSegmentDocId(int docNumber, long creationTick) { + this.docNumber = docNumber; + this.creationTick = creationTick; + } + + /** + * @return the document number in the foreign index segment. + */ + int getDocNumber() { + return docNumber; + } + + /** + * @return the creation tick of the foreign index segment. + */ + long getCreationTick() { + return creationTick; + } + + /** + * @inheritDoc + */ + int[] getDocumentNumbers(MultiIndexReader reader, int[] docNumbers) throws IOException { + int doc = reader.getDocumentNumber(this); + if (doc == -1) { + return EMPTY; + } else { + if (docNumbers.length == 1) { + docNumbers[0] = doc; + return docNumbers; + } else { + return new int[]{doc}; + } + } + } + + /** + * This implementation will return this. Document number is + * not known until resolved in {@link DocId#getDocumentNumbers(MultiIndexReader,int[])}. + * + * {@inheritDoc} + */ + DocId applyOffset(int offset) { + return this; + } + + /** + * Always returns true because this calls is in context of the + * index segment where this DocId lives. Within this segment this DocId is + * always valid. Whether the target of this DocId is valid can only be + * checked in the method {@link DocId#getDocumentNumbers(MultiIndexReader,int[])}. + * + * @param deleted the deleted documents in the segment where this DocId + * lives. + * @return always true. + */ + boolean isValid(BitSet deleted) { + return true; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HierarchyResolver.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HierarchyResolver.java new file mode 100644 index 00000000000..e41dd986507 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HierarchyResolver.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.core.query.lucene; + +import java.io.IOException; + +/** + * HierarchyResolver extends an {@link org.apache.lucene.index.IndexReader} + * with the ability to resolve a JCR hierarchy. + */ +public interface HierarchyResolver { + + /** + * Returns the document number of the parent of n or an empty + * array if n does not have a parent (n is the + * root node). + * + * @param n the document number. + * @param docNumbers an array for reuse. An implementation should use the + * passed array as a container for the return value, + * unless the length of the returned array is different + * from docNumbers. In which case an + * implementation will create a new array with an + * appropriate size. + * @return the document number of n's parent. + * @throws java.io.IOException if an error occurs while reading from the + * index. + */ + int[] getParents(int n, int[] docNumbers) throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HighlightingExcerptProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HighlightingExcerptProvider.java new file mode 100644 index 00000000000..c09222a8ba7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/HighlightingExcerptProvider.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.core.query.lucene; + +import java.io.IOException; + +/** + * HighlightingExcerptProvider extends the + * ExcerptProvider interface with a method that highlights matching + * terms in arbitrary text. + */ +public interface HighlightingExcerptProvider extends ExcerptProvider { + + /** + * Highlights the matching terms in the passed text. + * + * @param text the input text. + * @return the highlighted text. + * @throws IOException if an error occurs while highlighting the text. + */ + String highlight(String text) throws IOException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IDField.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IDField.java new file mode 100644 index 00000000000..77d23ca812f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IDField.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.core.query.lucene; + +import java.io.Reader; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.document.AbstractField; +import org.apache.lucene.document.Field.TermVector; +import org.apache.lucene.index.FieldInfo.IndexOptions; + +/** + * IDField implements a lucene field for the id of a node. + */ +public class IDField extends AbstractField { + + private static final long serialVersionUID = 3322062255855425638L; + + private final NodeId id; + + public IDField(NodeId id) { + this.id = id; + this.name = FieldNames.UUID; + this.isStored = true; + this.isTokenized = false; + this.omitNorms = true; + setIndexOptions(IndexOptions.DOCS_ONLY); + setStoreTermVector(TermVector.NO); + } + + public String stringValue() { + return id.toString(); + } + + public Reader readerValue() { + return null; + } + + public TokenStream tokenStreamValue() { + return null; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IOCounters.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IOCounters.java new file mode 100644 index 00000000000..423aaa1e349 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IOCounters.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.core.query.lucene; + +import java.util.Map; +import java.util.WeakHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * IOCounters provides a basic mechanism to track I/O during query + * execution. + */ +public class IOCounters { + + private static final Logger log = LoggerFactory.getLogger(IOCounters.class); + + private static final Map counts = + new WeakHashMap(); + + /** + * @return the current read count for caused by the current thread. + */ + public static synchronized long getReads() { + Long count = counts.get(Thread.currentThread()); + return count != null ? count : 0; + } + + /** + * Increments the read count caused by the current thread. + */ + public static void incrRead() { + if (log.isDebugEnabled()) { + synchronized (IOCounters.class) { + counts.put(Thread.currentThread(), getReads() + 1); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexDeletionPolicyImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexDeletionPolicyImpl.java new file mode 100644 index 00000000000..5eec7cd66d4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexDeletionPolicyImpl.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.core.query.lucene; + +import org.apache.lucene.index.IndexDeletionPolicy; +import org.apache.lucene.index.IndexCommit; +import org.apache.lucene.store.Directory; + +import java.util.List; +import java.io.IOException; + +/** + * IndexDeletionPolicyImpl... + */ +public class IndexDeletionPolicyImpl implements IndexDeletionPolicy { + + private static final String SEGMENTS = "segments"; + + private final PersistentIndex index; + + private final long maxAge; + + public IndexDeletionPolicyImpl(PersistentIndex index, long maxAge) + throws IOException { + this.index = index; + this.maxAge = maxAge; + // read current generation + readCurrentGeneration(); + } + + public void onInit(List commits) throws IOException { + checkCommits(commits); + } + + public void onCommit(List commits) throws IOException { + checkCommits(commits); + + // report back current generation + IndexCommit current = commits.get(commits.size() - 1); + String name = current.getSegmentsFileName(); + if (name.equals(SEGMENTS)) { + index.setCurrentGeneration(0); + } else { + index.setCurrentGeneration( + Long.parseLong(name.substring(SEGMENTS.length() + 1), + Character.MAX_RADIX)); + } + } + + //-------------------------------< internal >------------------------------- + + private void checkCommits(List commits) throws IOException { + long currentTime = System.currentTimeMillis(); + for (int i = 0; i < commits.size() - 1; i++) { + IndexCommit ic = commits.get(i); + long lastModified = index.getDirectory().fileModified(ic.getSegmentsFileName()); + if (currentTime - lastModified > maxAge) { + ic.delete(); + } else { + // following commits are younger, no need to check + break; + } + } + } + + void readCurrentGeneration() throws IOException { + Directory dir = index.getDirectory(); + String[] names = dir.listAll(); + long max = 0; + if (names != null) { + for (String name : names) { + long gen = -1; + if (name.startsWith(SEGMENTS)) { + if (name.length() == SEGMENTS.length()) { + gen = 0; + } else if (name.charAt(SEGMENTS.length()) == '_') { + gen = Long.parseLong(name.substring(SEGMENTS.length() + 1), Character.MAX_RADIX); + } + } + if (gen > max) { + max = gen; + } + } + } + index.setCurrentGeneration(max); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexFormatVersion.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexFormatVersion.java new file mode 100644 index 00000000000..f672e36b2a7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexFormatVersion.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.core.query.lucene; + +import java.util.Collection; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.util.ReaderUtil; + +/** + * This class indicates the lucene index format that is used. + *

      + *
    • Version 1 is the initial index format, which is used for Jackrabbit + * releases 1.0 to 1.3.x. Unless a re-index happens upgraded Jackrabbit + * instances will still use this version.
    • + *
    • Version 2 is the index format introduced with Jackrabbit 1.4.x. It + * adds a PROPERTIES_SET field which contains all property names of + * a node. This speeds up queries that check the existence of a property.
    • + *
    • Version 3 is the index format introduced with Jackrabbit 1.5.x. It + * adds support for length and local name queries using the newly added + * fields PROPERTY_LENGTHS, LOCAL_NAME and + * NAMESPACE_URI. Furthermore a Payload is added to + * PROPERTIES fields to indicate the property type.
    • + *
    + * Please note that existing indexes are not automatically upgraded to a newer + * version! If you want to take advantage of a certain 'feature' in an index + * format version you need to re-index the repository. + */ +public class IndexFormatVersion { + + /** + * V1 is the index format for Jackrabbit releases 1.0 to 1.3.x. + */ + public static final IndexFormatVersion V1 = new IndexFormatVersion(1); + + /** + * V2 is the index format for Jackrabbit releases 1.4.x + */ + public static final IndexFormatVersion V2 = new IndexFormatVersion(2); + + /** + * V3 is the index format for Jackrabbit releases >= 1.5 + */ + public static final IndexFormatVersion V3 = new IndexFormatVersion(3); + + /** + * The used version of the index format + */ + private final int version; + + /** + * Creates a index format version. + * + * @param version The version of the index. + */ + private IndexFormatVersion(int version) { + this.version = version; + } + + /** + * Returns the index format version + * @return the index format version. + */ + public int getVersion() { + return version; + } + + /** + * Returns true if this version is at least as high as the + * given version. + * + * @param version the other version to compare. + * @return true if this version is at least as high as the + * provided; false otherwise. + */ + public boolean isAtLeast(IndexFormatVersion version) { + return this.version >= version.getVersion(); + } + + /** + * @return a string representation of this index format version. + */ + public String toString() { + return String.valueOf(getVersion()); + } + + /** + * @return the index format version of the index used by the given + * index reader. + */ + public static IndexFormatVersion getVersion(IndexReader indexReader) { + Collection fields = ReaderUtil.getIndexedFields(indexReader); + if (fields.contains(FieldNames.LOCAL_NAME) || indexReader.numDocs() == 0) { + return IndexFormatVersion.V3; + } else if (fields.contains(FieldNames.PROPERTIES_SET)) { + return IndexFormatVersion.V2; + } else { + return IndexFormatVersion.V1; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexHistory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexHistory.java new file mode 100644 index 00000000000..6f327e18de0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexHistory.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.lucene.store.Directory; + +import java.util.TreeMap; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.io.IOException; + +/** + * IndexHistory implements a history of index segments. Whenever + * the index is flushed a new {@link IndexInfos} instance is created which + * represents the current state of the index. This includes the names of the + * index segments as well as their current generation number. + */ +class IndexHistory { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(IndexHistory.class); + + /** + * Name of the file that contains the index infos. + */ + private static final String INDEXES = "indexes"; + + /** + * the directory from where to read the index history. + */ + private final Directory indexDir; + + /** + * The maximum age (in milliseconds) of an index infos generation until it + * is removed. + */ + private final long maxAge; + + /** + * Maps generation (Long) to {@link IndexInfos}. Youngest generation first + * (-> higher value). + */ + private final Map indexInfosMap = new TreeMap(Collections.reverseOrder()); + + /** + * Creates a new IndexHistory from the given dir. + * + * @param dir the directory from where to read the index history. + * @param maxAge the maximum age in milliseconds for unused index infos. + * @throws IOException if an error occurs while reading the index history. + */ + IndexHistory(Directory dir, long maxAge) throws IOException { + this.indexDir = dir; + this.maxAge = maxAge; + // read all index infos + String[] names = dir.listAll(); + if (names != null) { + for (String name : names) { + if (name.startsWith(INDEXES)) { + long gen; + if (name.length() == INDEXES.length()) { + gen = 0; + } else if (name.charAt(INDEXES.length()) == '_') { + gen = Long.parseLong(name.substring(INDEXES.length() + 1), Character.MAX_RADIX); + } else { + continue; + } + try { + IndexInfos infos = new IndexInfos(dir, INDEXES, gen); + indexInfosMap.put(gen, infos); + } catch (IOException e) { + log.warn("ignoring invalid index infos file: " + name); + } + } + } + } + } + + /** + * Returns the time when the index segment with the given indexName + * was in use for the last time. The returned time does not accurately + * say until when an index segment was in use, but it does guarantee that + * the index segment in question was not in use anymore at the returned + * time. + *

    + * There are two special cases of return values: + *

      + *
    • {@link Long#MAX_VALUE}: indicates that the index segment is still in active use.
    • + *
    • {@link Long#MIN_VALUE}: indicates that there is no index segment with the given name.
    • + *
    + * + * @param indexName name of an index segment. + * @return the time when the index segment with the given name was in use + * the last time. + */ + long getLastUseOf(String indexName) { + Long previous = null; + for (Map.Entry entry : indexInfosMap.entrySet()) { + IndexInfos infos = entry.getValue(); + if (infos.contains(indexName)) { + if (previous == null) { + // still in use + return Long.MAX_VALUE; + } else { + return previous; + } + } + previous = infos.getLastModified(); + } + return Long.MIN_VALUE; + } + + /** + * Removes index infos older than {@link #maxAge} from this history. + */ + void pruneOutdated() { + long threshold = System.currentTimeMillis() - maxAge; + log.debug("Pruning index infos older than: " + threshold + "(" + indexDir + ")"); + Iterator it = indexInfosMap.values().iterator(); + // never prune the current generation + if (it.hasNext()) { + IndexInfos infos = it.next(); + log.debug("Skipping first index infos. generation=" + infos.getGeneration()); + } + while (it.hasNext()) { + IndexInfos infos = (IndexInfos) it.next(); + if (infos.getLastModified() < threshold) { + // check associated redo log + try { + String logName = getRedoLogName(infos.getGeneration()); + if (indexDir.fileExists(logName)) { + long lastModified = indexDir.fileModified(logName); + if (lastModified > threshold) { + log.debug("Keeping redo log with generation={}, timestamp={}", + infos.getGeneration(), lastModified); + continue; + } + // try do delete it + try { + indexDir.deleteFile(logName); + log.debug("Deleted redo log with generation={}, timestamp={}", + infos.getGeneration(), lastModified); + } catch (IOException e) { + log.warn("Unable to delete: " + indexDir + "/" + logName); + continue; + } + } + // delete index infos + try { + indexDir.deleteFile(infos.getFileName()); + log.debug("Deleted index infos with generation={}", + infos.getGeneration()); + it.remove(); + } catch (IOException e) { + log.warn("Unable to delete: " + indexDir + "/" + infos.getFileName()); + } + } catch (IOException e) { + log.warn("Failed to check if {} is outdated: {}", + infos.getFileName(), e); + } + } + } + } + + /** + * Adds an index infos to the history. This method will not modify nor keep + * a reference to the passed infos. + * + * @param infos the index infos to add. + */ + void addIndexInfos(IndexInfos infos) { + // must clone infos because it is modifiable + indexInfosMap.put(infos.getGeneration(), infos.clone()); + } + + //-------------------------------< internal >------------------------------- + + /** + * Returns the name of the redo log file with the given generation. + * + * @param generation the index infos generation. + * @return the name of the redo log file with the given generation. + */ + String getRedoLogName(long generation) { + if (generation == 0) { + return DefaultRedoLog.REDO_LOG; + } else { + return DefaultRedoLog.REDO_LOG_PREFIX + + Long.toString(generation, Character.MAX_RADIX) + + DefaultRedoLog.DOT_LOG; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexInfo.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexInfo.java new file mode 100644 index 00000000000..3096c7f9553 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexInfo.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.core.query.lucene; + +/** + * IndexInfo implements a single index info, which consists of a + * index segment name and a generation number. + */ +final class IndexInfo implements Cloneable { + + /** + * The name of the index segment. + */ + private final String name; + + /** + * The generation number. + */ + private long generation; + + /** + * Creates a new index info. + * + * @param name the name of the index segment. + * @param generation the generation. + */ + IndexInfo(String name, long generation) { + this.name = name; + this.generation = generation; + } + + /** + * @return the name of the index segment. + */ + public String getName() { + return name; + } + + /** + * @return the generation of this index info. + */ + public long getGeneration() { + return generation; + } + + /** + * Sets a new generation + * @param generation + */ + public void setGeneration(long generation) { + this.generation = generation; + } + + public IndexInfo clone() { + try { + return (IndexInfo) super.clone(); + } catch (CloneNotSupportedException e) { + // will never happen, this class is cloneable + throw new RuntimeException(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexInfos.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexInfos.java new file mode 100644 index 00000000000..9b752a11d66 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexInfos.java @@ -0,0 +1,411 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.BufferedOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import org.apache.jackrabbit.core.query.lucene.directory.IndexInputStream; +import org.apache.jackrabbit.core.query.lucene.directory.IndexOutputStream; +import org.apache.lucene.store.Directory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Stores a sequence of index names and their current generation. + */ +class IndexInfos implements Cloneable { + + /** + * Logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(IndexInfos.class); + + /** + * IndexInfos version for Jackrabbit 1.0 to 1.5.x + */ + private static final int NAMES_ONLY = 0; + + /** + * IndexInfos version for Jackrabbit 2.0 + */ + private static final int WITH_GENERATION = 1; + + /** + * For new segment names. + */ + private int counter = 0; + + /** + * Map of {@link IndexInfo}s. Key=name + */ + private LinkedHashMap indexes = new LinkedHashMap(); + + /** + * The directory where the index infos are stored. + */ + private final Directory directory; + + /** + * Base name of the file where the infos are stored. + */ + private final String name; + + /** + * The generation for this index infos. + */ + private long generation = 0; + + /** + * When this index infos were last modified. + */ + private long lastModified; + + /** + * Creates a new IndexInfos using baseName and reads the + * current generation. + * + * @param dir the directory where the index infos are stored. + * @param baseName the name of the file where infos are stored. + * @throws IOException if an error occurs while reading the index infos + * file. + */ + IndexInfos(Directory dir, String baseName) throws IOException { + this.directory = dir; + this.name = baseName; + long gens[] = getGenerations(getFileNames(dir, baseName), baseName); + if (gens.length == 0) { + // write initial infos + write(); + } else { + // read most recent generation + for (int i = gens.length - 1; i >= 0; i--) { + try { + this.generation = gens[i]; + read(); + break; + } catch (EOFException e) { + String fileName = getFileName(gens[i]); + log.warn("deleting invalid index infos file: " + fileName); + dir.deleteFile(fileName); + // reset generation + this.generation = 0; + } + } + } + } + + /** + * Creates a new IndexInfos using fileName and reads the given + * generation of the index infos. + * + * @param dir the directory where the index infos are stored. + * @param baseName the name of the file where infos are stored. + * @param generation the generation to read. + * @throws IOException if an error occurs while reading the index infos + * file. + */ + IndexInfos(Directory dir, String baseName, long generation) throws IOException { + if (generation < 0) { + throw new IllegalArgumentException(); + } + this.directory = dir; + this.name = baseName; + this.generation = generation; + read(); + } + + /** + * Returns the name of the file with the most current version where infos + * are stored. + * + * @return the name of the file where infos are stored. + */ + String getFileName() { + return getFileName(generation); + } + + /** + * Writes the index infos to disk. + * + * @throws IOException if an error occurs. + */ + void write() throws IOException { + // increment generation + generation++; + String newName = getFileName(); + boolean success = false; + try { + OutputStream out = new BufferedOutputStream(new IndexOutputStream( + directory.createOutput(newName))); + try { + log.debug("Writing IndexInfos {}", newName); + DataOutputStream dataOut = new DataOutputStream(out); + dataOut.writeInt(WITH_GENERATION); + dataOut.writeInt(counter); + dataOut.writeInt(indexes.size()); + for (Iterator it = iterator(); it.hasNext(); ) { + IndexInfo info = it.next(); + dataOut.writeUTF(info.getName()); + dataOut.writeLong(info.getGeneration()); + log.debug(" + {}:{}", info.getName(), info.getGeneration()); + } + } finally { + out.close(); + } + directory.sync(Collections.singleton(newName)); + lastModified = System.currentTimeMillis(); + success = true; + } finally { + if (!success) { + // try to delete the file and decrement generation + try { + directory.deleteFile(newName); + } catch (IOException e) { + log.warn("Unable to delete file: " + directory + "/" + newName); + } + generation--; + } + } + } + + /** + * @return an iterator over the {@link IndexInfo}s contained in this index + * infos. + */ + Iterator iterator() { + return indexes.values().iterator(); + } + + + /** + * Returns the number of index names. + * @return the number of index names. + */ + int size() { + return indexes.size(); + } + + /** + * @return the time when this index infos where last modified. + */ + long getLastModified() { + return lastModified; + } + + /** + * Adds a name to the index infos. + * + * @param name the name to add. + * @param generation the current generation of the index. + */ + void addName(String name, long generation) { + if (indexes.containsKey(name)) { + throw new IllegalArgumentException("already contains: " + name); + } + indexes.put(name, new IndexInfo(name, generation)); + } + + void updateGeneration(String name, long generation) { + IndexInfo info = indexes.get(name); + if (info == null) { + throw new NoSuchElementException(name); + } + if (info.getGeneration() != generation) { + info.setGeneration(generation); + } + } + + /** + * Removes the name from the index infos. + * @param name the name to remove. + */ + void removeName(String name) { + indexes.remove(name); + } + + /** + * Returns true if name exists in this + * IndexInfos; false otherwise. + * + * @param name the name to test existence. + * @return true it is exists in this IndexInfos. + */ + boolean contains(String name) { + return indexes.containsKey(name); + } + + /** + * @return the generation of this index infos. + */ + long getGeneration() { + return generation; + } + + /** + * Returns a new unique name for an index folder. + * @return a new unique name for an index folder. + */ + String newName() { + return "_" + Integer.toString(counter++, Character.MAX_RADIX); + } + + /** + * Clones this index infos. + * + * @return a clone of this index infos. + */ + @SuppressWarnings("unchecked") + public IndexInfos clone() { + try { + IndexInfos clone = (IndexInfos) super.clone(); + clone.indexes = (LinkedHashMap) indexes.clone(); + for (Map.Entry entry : clone.indexes.entrySet()) { + entry.setValue(entry.getValue().clone()); + } + return clone; + } catch (CloneNotSupportedException e) { + // never happens, this class is cloneable + throw new RuntimeException(); + } + } + + //----------------------------------< internal >---------------------------- + + /** + * Reads the index infos with the currently set {@link #generation}. + * + * @throws IOException if an error occurs. + */ + private void read() throws IOException { + String fileName = getFileName(generation); + InputStream in = new BufferedInputStream(new IndexInputStream( + directory.openInput(fileName))); + try { + LinkedHashMap indexes = new LinkedHashMap(); + DataInputStream di = new DataInputStream(in); + int version; + if (generation == 0) { + version = NAMES_ONLY; + } else { + version = di.readInt(); + } + int counter = di.readInt(); + for (int i = di.readInt(); i > 0; i--) { + String indexName = di.readUTF(); + long gen = 0; + if (version >= WITH_GENERATION) { + gen = di.readLong(); + } + indexes.put(indexName, new IndexInfo(indexName, gen)); + } + // when successfully read set values + this.lastModified = directory.fileModified(fileName); + this.indexes = indexes; + this.counter = counter; + } finally { + in.close(); + } + } + + /** + * Returns the name of the file with the given generation where infos + * are stored. + * + * @param gen the generation of the file. + * @return the name of the file where infos are stored. + */ + private String getFileName(long gen) { + if (gen == 0) { + return name; + } else { + return name + "_" + Long.toString(gen, Character.MAX_RADIX); + } + } + + /** + * Returns all generations of this index infos. + * + * @param directory the directory where the index infos are stored. + * @param base the base name for the index infos. + * @return names of all generation files of this index infos. + */ + private static String[] getFileNames(Directory directory, final String base) { + String[] names = null; + try { + names = directory.listAll(); + } catch (IOException e) { + // TODO: log warning? or throw? + } + if (names == null) { + return new String[0]; + } + List nameList = new ArrayList(names.length); + for (String n : names) { + if (n.startsWith(base)) { + nameList.add(n); + } + } + return nameList.toArray(new String[nameList.size()]); + } + + /** + * Parse the generation off the file name and return it. + * + * @param fileName the generation file that contains index infos. + * @param base the base name. + * @return the generation of the given file. + */ + private static long generationFromFileName(String fileName, String base) { + if (fileName.equals(base)) { + return 0; + } else { + return Long.parseLong(fileName.substring(base.length() + 1), + Character.MAX_RADIX); + } + } + + /** + * Returns the generations fo the given files in ascending order. + * + * @param fileNames the file names from where to obtain the generations. + * @param base the base name. + * @return the generations in ascending order. + */ + private static long[] getGenerations(String[] fileNames, String base) { + long[] gens = new long[fileNames.length]; + for (int i = 0; i < fileNames.length; i++) { + gens[i] = generationFromFileName(fileNames[i], base); + } + Arrays.sort(gens); + return gens; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexListener.java new file mode 100644 index 00000000000..b3c857fdb24 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexListener.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.core.query.lucene; + +import org.apache.lucene.index.Term; + +/** + * Defines an interface that allows implementing classes to listen for index + * changes (namely document deletes) while using a {@link ReadOnlyIndexReader}. + */ +public interface IndexListener { + + /** + * Informs this listener, that the document with id has been + * deleted. + * + * @param id the Term that identifies the deleted document. + */ + void documentDeleted(Term id); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMerger.java new file mode 100644 index 00000000000..846a3216764 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMerger.java @@ -0,0 +1,624 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Merges indexes in a separate daemon thread. + */ +class IndexMerger implements IndexListener { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(IndexMerger.class); + + /** + * minMergeDocs config parameter. + */ + private int minMergeDocs = SearchIndex.DEFAULT_MIN_MERGE_DOCS; + + /** + * maxMergeDocs config parameter + */ + private int maxMergeDocs = SearchIndex.DEFAULT_MAX_MERGE_DOCS; + + /** + * mergeFactor config parameter + */ + private int mergeFactor = SearchIndex.DEFAULT_MERGE_FACTOR; + + /** + * List of IndexBuckets in ascending document limit. + */ + private final List indexBuckets = new ArrayList(); + + /** + * The MultiIndex this index merger is working on. + */ + private final MultiIndex multiIndex; + + /** + * The executor of the repository. + */ + private final Executor executor; + + /** + * Flag that indicates that this index merger is shuting down and should + * quit. + */ + private final AtomicBoolean quit = new AtomicBoolean(false); + + /** + * Flag that indicates if this index merger has already been started. + * @see #start() + */ + private final AtomicBoolean isStarted = new AtomicBoolean(false); + + /** + * Monitor object to synchronize merge calculation. + */ + private final Object lock = new Object(); + + /** + * Read/write lock for index segment replacement. A shared read lock is + * acquired for an index replacement. An exclusive write lock is acquired + * when this index merger is shutting down, to prevent further index + * replacements. + */ + private final ReadWriteLock indexReplacement = new ReentrantReadWriteLock(); + + /** + * List of merger threads that are currently busy. + */ + private final List busyMergers = new ArrayList(); + + /** + * Creates an IndexMerger. + * + * @param multiIndex the MultiIndex. + * @param executor the executor of the repository. + */ + IndexMerger(MultiIndex multiIndex, Executor executor) { + this.multiIndex = multiIndex; + this.executor = executor; + } + + /** + * Starts this index merger. + */ + void start() { + isStarted.set(true); + synchronized (busyMergers) { + for (Worker worker : busyMergers) { + worker.unblock(); + } + } + } + + /** + * Informs the index merger that an index was added / created. + * + * @param name the name of the index. + * @param numDocs the number of documents it contains. + */ + void indexAdded(String name, int numDocs) { + if (numDocs < 0) { + throw new IllegalArgumentException("numDocs must be positive"); + } + // multiple threads may enter this method: + // - the background thread of this IndexMerger, when it replaces indexes + // after a successful merge + // - a regular thread that updates the workspace + // + // therefore we have to synchronize this block + synchronized (lock) { + // initially create buckets + if (indexBuckets.size() == 0) { + long lower = 0; + long upper = minMergeDocs; + while (upper < maxMergeDocs) { + indexBuckets.add(new IndexBucket(lower, upper, true)); + lower = upper + 1; + upper *= mergeFactor; + } + // one with upper = maxMergeDocs + indexBuckets.add(new IndexBucket(lower, maxMergeDocs, false)); + // and another one as overflow, just in case... + indexBuckets.add(new IndexBucket(maxMergeDocs + 1, Long.MAX_VALUE, false)); + } + + // put index in bucket + IndexBucket bucket = indexBuckets.get(indexBuckets.size() - 1); + for (IndexBucket indexBucket : indexBuckets) { + bucket = indexBucket; + if (bucket.fits(numDocs)) { + break; + } + } + bucket.add(new Index(name, numDocs)); + + if (log.isDebugEnabled()) { + log.debug("index added: name=" + name + ", numDocs=" + numDocs); + } + + // if bucket does not allow merge, we don't have to continue + if (!bucket.allowsMerge()) { + return; + } + + // check if we need a merge + if (bucket.size() >= mergeFactor) { + long targetMergeDocs = bucket.upper; + targetMergeDocs = Math.min(targetMergeDocs * mergeFactor, maxMergeDocs); + // sum up docs in bucket + List indexesToMerge = new ArrayList(); + int mergeDocs = 0; + for (Iterator it = bucket.iterator(); it.hasNext() && mergeDocs <= targetMergeDocs;) { + indexesToMerge.add(it.next()); + } + if (indexesToMerge.size() > 2) { + // found merge + Index[] idxs = indexesToMerge.toArray(new Index[indexesToMerge.size()]); + bucket.removeAll(indexesToMerge); + if (log.isDebugEnabled()) { + log.debug("requesting merge for " + indexesToMerge); + } + addMergeTask(new Merge(idxs)); + if (log.isDebugEnabled()) { + int numBusy; + synchronized (busyMergers) { + numBusy = busyMergers.size(); + } + log.debug("# of busy merge workers: " + numBusy); + } + } + } + } + } + + /** + * @inheritDoc + */ + public void documentDeleted(Term id) { + log.debug("document deleted: " + id.text()); + synchronized (busyMergers) { + for (Worker w : busyMergers) { + w.documentDeleted(id); + } + } + } + + /** + * When the calling thread returns this index merger will be idle, that is + * there will be no merge tasks pending anymore. The method returns + * immediately if there are currently no tasks pending at all. + * + * @throws InterruptedException if this thread is interrupted while waiting + * for the worker threads to become idle. + */ + void waitUntilIdle() throws InterruptedException { + synchronized (busyMergers) { + while (!busyMergers.isEmpty()) { + busyMergers.wait(); + } + } + } + + /** + * Signals this IndexMerger to stop and waits until it + * has terminated. + */ + void dispose() { + log.debug("dispose IndexMerger"); + // get exclusive lock on index replacements + try { + indexReplacement.writeLock().lockInterruptibly(); + } catch (InterruptedException e) { + log.warn("Interrupted while acquiring index replacement exclusive lock: " + e); + // try to stop IndexMerger without the sync + } + + // set quit + quit.set(true); + log.debug("quit flag set"); + + try { + // give the merger threads some time to quit, + // it is possible that the mergers are busy working on a large index. + // if that is the case we will just ignore it and the daemon will + // die without being able to finish the merge. + // at this point it is not possible anymore to replace indexes + // on the MultiIndex because we hold all indexReplacement permits. + Worker[] workers; + synchronized (busyMergers) { + workers = busyMergers.toArray(new Worker[busyMergers.size()]); + } + for (Worker w : workers) { + w.join(500); + if (w.isAlive()) { + log.info("Unable to stop IndexMerger.Worker. Daemon is busy."); + } else { + log.debug("IndexMerger.Worker thread stopped"); + } + } + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for IndexMerger threads to terminate."); + } + } + + //-----------------------< merge properties >------------------------------- + + /** + * The merge factor. + * + * @param mergeFactor the merge factor. + */ + public void setMergeFactor(int mergeFactor) { + this.mergeFactor = mergeFactor; + } + + + /** + * The initial threshold for number of documents to merge to a new index. + * + * @param minMergeDocs the min merge docs number. + */ + public void setMinMergeDocs(int minMergeDocs) { + this.minMergeDocs = minMergeDocs; + } + + /** + * The maximum number of document to merge. + * + * @param maxMergeDocs the max merge docs number. + */ + public void setMaxMergeDocs(int maxMergeDocs) { + this.maxMergeDocs = maxMergeDocs; + } + + //------------------------------< internal >-------------------------------- + + private void addMergeTask(Merge task) { + // only enqueue if still running + if (!quit.get()) { + Worker worker = new Worker(task); + if (isStarted.get()) { + // immediately unblock if this index merger is already started + worker.unblock(); + } + synchronized (busyMergers) { + busyMergers.add(worker); + } + executor.execute(worker); + } + } + + /** + * Implements a simple struct that holds the name of an index and how + * many document it contains. Index is comparable using the + * number of documents it contains. + */ + private static final class Index implements Comparable { + + /** + * The name of the index. + */ + private final String name; + + /** + * The number of documents the index contains. + */ + private final int numDocs; + + /** + * Creates a new index struct. + * + * @param name name of an index. + * @param numDocs number of documents it contains. + */ + Index(String name, int numDocs) { + this.name = name; + this.numDocs = numDocs; + } + + /** + * Indexes are first ordered by {@link #numDocs} and then by {@link + * #name}. + * + * @param o the other Index. + * @return a negative integer, zero, or a positive integer as this + * Index is less than, equal to, or greater than the specified + * Index. + */ + public int compareTo(Index other) { + int val = numDocs < other.numDocs ? -1 : (numDocs == other.numDocs ? 0 : 1); + if (val != 0) { + return val; + } else { + return name.compareTo(other.name); + } + } + + /** + * @inheritDoc + */ + public String toString() { + return name + ":" + numDocs; + } + } + + /** + * Defines a merge task, to merge a couple of indexes into a new index. + */ + private static final class Merge { + + private final Index[] indexes; + + /** + * Merge task, to merge indexes into a new index with + * name. + * + * @param indexes the indexes to merge. + */ + Merge(Index[] indexes) { + this.indexes = new Index[indexes.length]; + System.arraycopy(indexes, 0, this.indexes, 0, indexes.length); + } + } + + /** + * Implements a List with a document limit value. An + * IndexBucket contains {@link Index}es with documents less + * or equal the document limit of the bucket. + */ + private static final class IndexBucket extends ArrayList { + + private static final long serialVersionUID = 2985514550083374904L; + + /** + * The lower document limit. + */ + private final long lower; + + /** + * The upper document limit. + */ + private final long upper; + + /** + * Flag indicating if indexes in this bucket can be merged. + */ + private final boolean allowMerge; + + /** + * Creates a new IndexBucket. Limits are both inclusive. + * + * @param lower document limit. + * @param upper document limit. + * @param allowMerge if indexes in this bucket can be merged. + */ + IndexBucket(long lower, long upper, boolean allowMerge) { + this.lower = lower; + this.upper = upper; + this.allowMerge = allowMerge; + } + + /** + * Returns true if the number of documents fit in this + * IndexBucket; otherwise false + * + * @param numDocs the number of documents. + * @return true if numDocs fit. + */ + boolean fits(long numDocs) { + return numDocs >= lower && numDocs <= upper; + } + + /** + * Returns true if indexes in this bucket can be merged. + * + * @return true if indexes in this bucket can be merged. + */ + boolean allowsMerge() { + return allowMerge; + } + } + + private class Worker implements Runnable, IndexListener { + + /** + * List of id Term that identify documents that were deleted + * while a merge was running. + */ + private final List deletedDocuments = Collections.synchronizedList(new ArrayList()); + + /** + * A latch that is set to zero when this worker is unblocked. + */ + private final CountDownLatch start = new CountDownLatch(1); + + /** + * Flag that indicates whether this worker has finished its work. + */ + private final AtomicBoolean terminated = new AtomicBoolean(false); + + /** + * The merge task. + */ + private final Merge task; + + /** + * Creates a new worker which is initially blocked. Call + * {@link #unblock()} to unblock it. + * + * @param task the merge task. + */ + private Worker(Merge task) { + this.task = task; + } + + /** + * Implements the index merging. + */ + public void run() { + // worker is initially suspended + try { + try { + start.await(); + } catch (InterruptedException e) { + // check if we should quit + if (!quit.get()) { + // enqueue task again and retry with another thread + addMergeTask(task); + } + return; + } + + log.debug("accepted merge request"); + + // get readers + String[] names = new String[task.indexes.length]; + for (int i = 0; i < task.indexes.length; i++) { + names[i] = task.indexes[i].name; + } + try { + log.debug("create new index"); + PersistentIndex index = multiIndex.getOrCreateIndex(null); + boolean success = false; + try { + + log.debug("get index readers from MultiIndex"); + IndexReader[] readers = multiIndex.getIndexReaders(names, IndexMerger.this); + try { + // do the merge + long time = System.currentTimeMillis(); + index.addIndexes(readers); + time = System.currentTimeMillis() - time; + int docCount = 0; + for (IndexReader reader : readers) { + docCount += reader.numDocs(); + } + log.info("merged " + docCount + " documents in " + time + " ms into " + index.getName() + "."); + } finally { + for (IndexReader reader : readers) { + try { + Util.closeOrRelease(reader); + } catch (IOException e) { + log.warn("Unable to close IndexReader: " + e); + } + } + } + + // inform multi index + // if we cannot get the sync immediately we have to quit + Lock shared = indexReplacement.readLock(); + if (!shared.tryLock()) { + log.debug("index merging canceled"); + return; + } + try { + log.debug("replace indexes"); + multiIndex.replaceIndexes(names, index, deletedDocuments); + } finally { + shared.unlock(); + } + + success = true; + + } finally { + if (!success) { + // delete index + log.debug("deleting index " + index.getName()); + multiIndex.deleteIndex(index); + // add task again and retry + addMergeTask(task); + } + } + } catch (Throwable e) { + log.error("Error while merging indexes: ", e); + } + } finally { + synchronized (terminated) { + terminated.set(true); + terminated.notifyAll(); + } + synchronized (busyMergers) { + busyMergers.remove(this); + busyMergers.notifyAll(); + } + log.debug("Worker finished"); + } + } + + /** + * @inheritDoc + */ + public void documentDeleted(Term id) { + log.debug("document deleted: " + id.text()); + deletedDocuments.add(id); + } + + /** + * Unblocks this worker and allows it to start with the index merging. + */ + void unblock() { + start.countDown(); + } + + /** + * Waits until this worker is finished or the specified amount of time + * has elapsed. + * + * @param timeout the timeout in milliseconds. + * @throws InterruptedException if the current thread is interrupted + * while waiting for this worker to + * terminate. + */ + void join(long timeout) throws InterruptedException { + synchronized (terminated) { + while (!terminated.get()) { + terminated.wait(timeout); + } + } + } + + /** + * @return true if this worker is still alive and not yet + * terminated. + */ + boolean isAlive() { + return !terminated.get(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMigration.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMigration.java new file mode 100644 index 00000000000..3422af88523 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexMigration.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.apache.jackrabbit.core.query.lucene.directory.DirectoryManager; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.FieldSelector; +import org.apache.lucene.document.Fieldable; +import org.apache.lucene.index.CorruptIndexException; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.FilterIndexReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.KeepOnlyLastCommitDeletionPolicy; +import org.apache.lucene.index.LogByteSizeMergePolicy; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.index.TermPositions; +import org.apache.lucene.index.UpgradeIndexMergePolicy; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.ReaderUtil; +import org.apache.lucene.util.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * IndexMigration implements a utility that migrates a Jackrabbit + * 1.4.x index to version 1.5. Until version 1.4.x, indexes used the character + * '\uFFFF' to separate the name of a property from the value. As of Lucene + * 2.3 this does not work anymore. See LUCENE-1221. Jackrabbit >= 1.5 uses + * the character '[' as a separator. Whenever an index is opened from disk, a + * quick check is run to find out whether a migration is required. See also + * JCR-1363 for more details. + */ +public class IndexMigration { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(IndexMigration.class); + + /** + * Checks if the given index needs to be migrated. + * + * @param index the index to check and migration if needed. + * @param directoryManager the directory manager. + * @param oldSeparatorChar the old separator char that needs to be replaced. + * @throws IOException if an error occurs while migrating the index. + */ + public static void migrate(PersistentIndex index, + DirectoryManager directoryManager, + char oldSeparatorChar) + throws IOException { + Directory indexDir = index.getDirectory(); + log.debug("Checking {} ...", indexDir); + ReadOnlyIndexReader reader = index.getReadOnlyIndexReader(); + try { + if (IndexFormatVersion.getVersion(reader).getVersion() >= + IndexFormatVersion.V3.getVersion()) { + // index was created with Jackrabbit 1.5 or higher + // no need for migration + log.debug("IndexFormatVersion >= V3, no migration needed"); + return; + } + // assert: there is at least one node in the index, otherwise the + // index format version would be at least V3 + TermEnum terms = reader.terms(new Term(FieldNames.PROPERTIES, "")); + try { + Term t = terms.term(); + if (t.text().indexOf(oldSeparatorChar) == -1) { + log.debug("Index already migrated"); + return; + } + } finally { + terms.close(); + } + } finally { + reader.release(); + index.releaseWriterAndReaders(); + } + + // if we get here then the index must be migrated + log.debug("Index requires migration {}", indexDir); + + String migrationName = index.getName() + "_v36"; + if (directoryManager.hasDirectory(migrationName)) { + directoryManager.delete(migrationName); + } + + Directory migrationDir = directoryManager.getDirectory(migrationName); + final IndexWriterConfig c = new IndexWriterConfig(Version.LUCENE_36, new JackrabbitAnalyzer()); + c.setMergePolicy(new UpgradeIndexMergePolicy(new LogByteSizeMergePolicy())); + c.setIndexDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); + try { + IndexWriter writer = new IndexWriter(migrationDir, c); + try { + IndexReader r = new MigrationIndexReader(IndexReader.open(index.getDirectory()), + oldSeparatorChar); + try { + writer.addIndexes(r); + writer.forceMerge(1); + writer.close(); + } finally { + r.close(); + } + } finally { + writer.close(); + } + } finally { + migrationDir.close(); + } + directoryManager.delete(index.getName()); + if (!directoryManager.rename(migrationName, index.getName())) { + throw new IOException("failed to move migrated directory " + migrationDir); + } + log.info("Migrated " + index.getName()); + } + + //---------------------------< internal helper >---------------------------- + + /** + * An index reader that migrates stored field values and term text on the + * fly. + */ + private static class MigrationIndexReader extends FilterIndexReader { + + private final char oldSepChar; + + public MigrationIndexReader(IndexReader in, char oldSepChar) { + super(in); + this.oldSepChar = oldSepChar; + } + + @Override + public IndexReader[] getSequentialSubReaders() { + return null; + } + + @Override + public FieldInfos getFieldInfos() { + return ReaderUtil.getMergedFieldInfos(in); + } + + @Override + public Document document(int n, FieldSelector fieldSelector) + throws CorruptIndexException, IOException { + Document doc = super.document(n, fieldSelector); + Fieldable[] fields = doc.getFieldables(FieldNames.PROPERTIES); + if (fields != null) { + doc.removeFields(FieldNames.PROPERTIES); + for (Fieldable field : fields) { + String value = field.stringValue(); + value = value.replace(oldSepChar, '['); + doc.add(new Field(FieldNames.PROPERTIES, false, value, + Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO)); + } + } + return doc; + } + + @Override + public TermEnum terms() throws IOException { + List enums = new ArrayList(); + List fieldNames = new ArrayList(ReaderUtil.getIndexedFields(in)); + Collections.sort(fieldNames); + for (String fieldName : fieldNames) { + if (fieldName.equals(FieldNames.PROPERTIES)) { + addPropertyTerms(enums); + } else { + enums.add(new RangeScan(in, new Term(fieldName, ""), new Term(fieldName, "\uFFFF"))); + } + } + return new MigrationTermEnum(new ChainedTermEnum(enums), oldSepChar); + } + + @Override + public TermPositions termPositions() throws IOException { + return new MigrationTermPositions(in.termPositions(), oldSepChar); + } + + private void addPropertyTerms(List enums) throws IOException { + SortedMap termEnums = new TreeMap( + new Comparator() { + public int compare(String s1, String s2) { + s1 = s1.replace(oldSepChar, '['); + s2 = s2.replace(oldSepChar, '['); + return s1.compareTo(s2); + } + }); + // scan through terms and find embedded field names + TermEnum terms = new RangeScan(in, + new Term(FieldNames.PROPERTIES, ""), + new Term(FieldNames.PROPERTIES, "\uFFFF")); + String previous = null; + while (terms.next()) { + Term t = terms.term(); + String name = t.text().substring(0, t.text().indexOf(oldSepChar) + 1); + if (!name.equals(previous)) { + termEnums.put(name, new RangeScan(in, + new Term(FieldNames.PROPERTIES, name), + new Term(FieldNames.PROPERTIES, name + "\uFFFF"))); + } + previous = name; + } + enums.addAll(termEnums.values()); + } + + private static class MigrationTermEnum extends FilterTermEnum { + + private final char oldSepChar; + + public MigrationTermEnum(TermEnum in, char oldSepChar) { + super(in); + this.oldSepChar = oldSepChar; + } + + public Term term() { + Term t = super.term(); + if (t == null) { + return t; + } + if (t.field().equals(FieldNames.PROPERTIES)) { + String text = t.text(); + return t.createTerm(text.replace(oldSepChar, '[')); + } else { + return t; + } + } + + TermEnum unwrap() { + return in; + } + } + + private static class MigrationTermPositions extends FilterTermPositions { + + private final char oldSepChar; + + public MigrationTermPositions(TermPositions in, char oldSepChar) { + super(in); + this.oldSepChar = oldSepChar; + } + + public void seek(Term term) throws IOException { + if (term.field().equals(FieldNames.PROPERTIES)) { + char[] text = term.text().toCharArray(); + text[term.text().indexOf('[')] = oldSepChar; + super.seek(term.createTerm(new String(text))); + } else { + super.seek(term); + } + } + + public void seek(TermEnum termEnum) throws IOException { + if (termEnum instanceof MigrationTermEnum) { + super.seek(((MigrationTermEnum) termEnum).unwrap()); + } else { + super.seek(termEnum); + } + } + } + } + + static final class ChainedTermEnum extends TermEnum { + + private Queue queue = new LinkedList(); + + public ChainedTermEnum(Collection enums) { + super(); + queue.addAll(enums); + } + + public boolean next() throws IOException { + boolean newEnum = false; + for (;;) { + TermEnum terms = queue.peek(); + if (terms == null) { + // no more enums + break; + } + if (newEnum && terms.term() != null) { + // need to check if enum is already positioned + // at first term + return true; + } + if (terms.next()) { + return true; + } else { + queue.remove(); + terms.close(); + newEnum = true; + } + } + return false; + } + + public Term term() { + TermEnum terms = queue.peek(); + if (terms != null) { + return terms.term(); + } + return null; + } + + public int docFreq() { + TermEnum terms = queue.peek(); + if (terms != null) { + return terms.docFreq(); + } + return 0; + } + + public void close() throws IOException { + // close remaining + while (!queue.isEmpty()) { + queue.remove().close(); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfiguration.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfiguration.java new file mode 100644 index 00000000000..b36aee2a051 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfiguration.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.core.query.lucene; + +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.query.QueryHandlerContext; +import org.apache.jackrabbit.spi.Name; +import org.apache.lucene.analysis.Analyzer; +import org.w3c.dom.Element; + +/** + * IndexingConfiguration defines the interface to check whether + * a certain property is indexed or not. This interface also provides access + * to aggregate rules. Those define how node indexes are combined into an + * aggregate to form a single node index that can be queried. + */ +public interface IndexingConfiguration { + + /** + * The default boost: 1.0f. + */ + float DEFAULT_BOOST = 1.0f; + + /** + * Initializes the configuration. + * + * @param config the document element of the configuration DOM. + * @param context the context of the query handler. + * @param namespaceMappings the namespaceMappings. + * @throws Exception if initialization fails. + */ + void init(Element config, QueryHandlerContext context, NamespaceMappings namespaceMappings) throws Exception; + + /** + * Returns the configured indexing aggregate rules or null if + * none exist. The caller must not modify the returned array! + * + * @return the configured rules or null if none exist. + */ + AggregateRule[] getAggregateRules(); + + /** + * Returns true if the property with the given name is indexed + * according to this configuration. + * + * @param state the node state. + * @param propertyName the name of a property. + * @return true if the property is indexed; false + * otherwise. + */ + boolean isIndexed(NodeState state, Name propertyName); + + /** + * Returns true if the property with the given name should be + * included in the node scope fulltext index. If there is no configuration + * entry for that property false is returned. + * + * @param state the node state. + * @param propertyName the name of a property. + * @return true if the property should be included in the node + * scope fulltext index. + */ + boolean isIncludedInNodeScopeIndex(NodeState state, Name propertyName); + + /** + * Returns true if the content of the property with the given + * name should show up in an excerpt. If there is no configuration entry for + * that property true is returned. + * + * @param state the node state. + * @param propertyName the name of a property. + * @return true if the content of the property should be + * included in an excerpt; false otherwise. + */ + boolean useInExcerpt(NodeState state, Name propertyName); + + /** + * Returns the boost value for the given property name. If there is no + * configuration entry for the property name the {@link #DEFAULT_BOOST} is + * returned. + * + * @param state the node state. + * @param propertyName the name of a property. + * @return the boost value for the property. + */ + float getPropertyBoost(NodeState state, Name propertyName); + + /** + * Returns the boost for the node scope fulltext index field. + * + * @param state the node state. + * @return the boost for the node scope fulltext index field. + */ + float getNodeBoost(NodeState state); + + /** + * Returns the analyzer configured for the property with this fieldName + * (the string representation ,JCR-style name, of the given Name + * prefixed with FieldNames.FULLTEXT_PREFIX), + * and null if none is configured, or the configured analyzer + * cannot be found. If null is returned, the default Analyzer + * is used. + * + * @param fieldName the string representation ,JCR-style name, of the given Name, + * prefixed with FieldNames.FULLTEXT_PREFIX) + * @return the analyzer to use for indexing this property + */ + Analyzer getPropertyAnalyzer(String fieldName); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationEntityResolver.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationEntityResolver.java new file mode 100644 index 00000000000..d2d5d78f495 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationEntityResolver.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.core.query.lucene; + +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; + +/** + * IndexingConfigurationEntityResolver implements an entity + * resolver for the indexing configuration DTD. + */ +public class IndexingConfigurationEntityResolver implements EntityResolver { + + /** + * Maps system ids to DTD resource names. + */ + private static final Map SYSTEM_IDS; + + static { + Map systemIds = new HashMap(); + systemIds.put( + "http://jackrabbit.apache.org/dtd/indexing-configuration-1.0.dtd", + "indexing-configuration-1.0.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/indexing-configuration-1.1.dtd", + "indexing-configuration-1.1.dtd"); + systemIds.put( + "http://jackrabbit.apache.org/dtd/indexing-configuration-1.2.dtd", + "indexing-configuration-1.2.dtd"); + SYSTEM_IDS = Collections.unmodifiableMap(systemIds); + } + + /** + * {@inheritDoc} + */ + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + String resourceName = SYSTEM_IDS.get(systemId); + if (resourceName != null) { + InputStream in = getClass().getResourceAsStream(resourceName); + if (in != null) { + return new InputSource(in); + } + } + return null; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationImpl.java new file mode 100644 index 00000000000..479e5503695 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationImpl.java @@ -0,0 +1,1047 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Properties; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import org.apache.commons.collections.iterators.AbstractIteratorDecorator; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.HierarchyManagerImpl; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistryListener; +import org.apache.jackrabbit.core.nodetype.xml.AdditionalNamespaceResolver; +import org.apache.jackrabbit.core.query.QueryHandlerContext; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingNameResolver; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.Pattern; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.lucene.analysis.Analyzer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Attr; +import org.w3c.dom.CharacterData; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * IndexingConfigurationImpl implements a concrete indexing + * configuration. + */ +public class IndexingConfigurationImpl + implements IndexingConfiguration, NodeTypeRegistryListener { + + /** + * The logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(IndexingConfigurationImpl.class); + + /** + * The path factory instance. + */ + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + /** + * An Iterator over an empty list of node states. + */ + private static final Iterator EMPTY_NODESTATE_ITERATOR; + static { + Collection empty = Collections.emptyList(); + EMPTY_NODESTATE_ITERATOR = empty.iterator(); + } + + + /** + * The indexing configuration. + */ + private Element configuration; + + /** + * A namespace resolver for parsing QNames in the configuration. + */ + private NameResolver resolver; + + /** + * The item state manager to retrieve additional item states. + */ + private ItemStateManager ism; + + /** + * A hierarchy resolver for the item state manager. + */ + private HierarchyManager hmgr; + + /** + * The node type registry. + */ + private NodeTypeRegistry ntReg; + + /** + * The {@link IndexingRule}s inside this configuration. + */ + private Map> configElements = new HashMap>(); + + /** + * The indexing aggregates inside this configuration. + */ + private AggregateRule[] aggregateRules; + + /** + * The configured analyzers for indexing properties. + */ + private Map analyzers = new HashMap(); + + /** + * {@inheritDoc} + */ + public void init(Element config, + QueryHandlerContext context, + NamespaceMappings nsMappings) throws Exception { + configuration = config; + ism = context.getItemStateManager(); + hmgr = new HierarchyManagerImpl(context.getRootId(), ism); + NamespaceResolver nsResolver = new AdditionalNamespaceResolver(getNamespaces(config)); + resolver = new ParsingNameResolver(NameFactoryImpl.getInstance(), nsResolver); + ntReg = context.getNodeTypeRegistry(); + ntReg.addListener(this); + + refreshIndexRules(); + List idxAggregates = new ArrayList(); + NodeList indexingConfigs = config.getChildNodes(); + for (int i = 0; i < indexingConfigs.getLength(); i++) { + Node configNode = indexingConfigs.item(i); + if (configNode.getNodeName().equals("aggregate")) { + idxAggregates.add(new AggregateRuleImpl( + configNode, resolver, ism, hmgr)); + } else if (configNode.getNodeName().equals("analyzers")) { + NodeList childNodes = configNode.getChildNodes(); + for (int j = 0; j < childNodes.getLength(); j++) { + Node analyzerNode = childNodes.item(j); + if (analyzerNode.getNodeName().equals("analyzer")) { + Analyzer analyzer = JackrabbitAnalyzer.getAnalyzerInstance( + analyzerNode.getAttributes().getNamedItem("class").getNodeValue()); + NodeList propertyChildNodes = analyzerNode.getChildNodes(); + for (int k = 0; k < propertyChildNodes.getLength(); k++) { + Node propertyNode = propertyChildNodes.item(k); + if (propertyNode.getNodeName().equals("property")) { + // get property name + Name propName = resolver.getQName(getTextContent(propertyNode)); + String fieldName = nsMappings.translateName(propName); + // set analyzer for the fulltext property fieldname + int idx = fieldName.indexOf(':'); + fieldName = fieldName.substring(0, idx + 1) + + FieldNames.FULLTEXT_PREFIX + fieldName.substring(idx + 1); + Object prevAnalyzer = analyzers.put(fieldName, analyzer); + if (prevAnalyzer != null) { + log.warn("Property " + propName.getLocalName() + + " has been configured for multiple analyzers. " + + " Last configured analyzer is used"); + } + } + } + } + } + } + + } + if (idxAggregates.isEmpty()) { + aggregateRules = null; + } else { + aggregateRules = idxAggregates.toArray(new AggregateRule[idxAggregates.size()]); + } + } + + /** + * Returns the configured indexing aggregate rules or null if + * none exist. + * + * @return the configured rules or null if none exist. + */ + public AggregateRule[] getAggregateRules() { + return aggregateRules; + } + + /** + * Returns true if the property with the given name is fulltext + * indexed according to this configuration. + * + * @param state the node state. + * @param propertyName the name of a property. + * @return true if the property is fulltext indexed; + * false otherwise. + */ + public boolean isIndexed(NodeState state, Name propertyName) { + IndexingRule rule = getApplicableIndexingRule(state, propertyName); + if (rule != null) { + return rule.isIndexed(propertyName); + } + // none of the configs matches -> index property + return true; + } + + /** + * Returns the boost value for the given property name. If there is no + * configuration entry for the property name the {@link #DEFAULT_BOOST} is + * returned. + * + * @param state the node state. + * @param propertyName the name of a property. + * @return the boost value for the property. + */ + public float getPropertyBoost(NodeState state, Name propertyName) { + IndexingRule rule = getApplicableIndexingRule(state, propertyName); + if (rule != null) { + return rule.getBoost(propertyName); + } + return DEFAULT_BOOST; + } + + /** + * Returns the boost for the node scope fulltext index field. + * + * @param state the node state. + * @return the boost for the node scope fulltext index field. + */ + public float getNodeBoost(NodeState state) { + IndexingRule rule = getApplicableIndexingRule(state, null); + if (rule != null) { + return rule.getNodeBoost(); + } + return DEFAULT_BOOST; + } + + /** + * Returns true if the property with the given name should be + * included in the node scope fulltext index. If there is not configuration + * entry for that propery false is returned. + * + * @param state the node state. + * @param propertyName the name of a property. + * @return true if the property should be included in the node + * scope fulltext index. + */ + public boolean isIncludedInNodeScopeIndex(NodeState state, + Name propertyName) { + IndexingRule rule = getApplicableIndexingRule(state, propertyName); + if (rule != null) { + return rule.isIncludedInNodeScopeIndex(propertyName); + } + // none of the config elements matched -> default is to include + return true; + } + + /** + * Returns true if the content of the property with the given + * name should show up in an excerpt. If there is no configuration entry for + * that property true is returned. + * + * @param state the node state. + * @param propertyName the name of a property. + * @return true if the content of the property should be + * included in an excerpt; false otherwise. + */ + public boolean useInExcerpt(NodeState state, Name propertyName) { + IndexingRule rule = getApplicableIndexingRule(state, propertyName); + if (rule != null) { + return rule.useInExcerpt(propertyName); + } + // none of the config elements matched -> default is to include + return true; + } + + /** + * Returns the analyzer configured for the property with this fieldName + * (the string representation ,JCR-style name, of the given Name + * prefixed with FieldNames.FULLTEXT_PREFIX)), + * and null if none is configured, or the configured analyzer + * cannot be found. If null is returned, the default Analyzer + * is used. + * + * @param fieldName the string representation ,JCR-style name, of the given Name + * prefixed with FieldNames.FULLTEXT_PREFIX)) + * @return the analyzer to use for indexing this property + */ + public Analyzer getPropertyAnalyzer(String fieldName) { + if (analyzers.containsKey(fieldName)) { + return analyzers.get(fieldName); + } + return null; + } + + //--------------------------< NodeTypeRegistryListener >-------------------- + + public void nodeTypeRegistered(Name ntName) { + try { + refreshIndexRules(); + } catch (Exception e) { + log.warn("Unable to refresh index rules", e); + } + } + + public void nodeTypeReRegistered(Name ntName) { + // not interested + } + + public void nodeTypesUnregistered(Collection names) { + // not interested + } + + //---------------------------------< internal >----------------------------- + + /** + * Refreshes the index rules in {@link #configElements} based on the current + * node types available in the node type registry. + * + * @throws Exception if an error occurs while refreshing the rules. + */ + private void refreshIndexRules() throws Exception { + Map> nt2rules = new HashMap>(); + Name[] ntNames = ntReg.getRegisteredNodeTypes(); + NodeList indexingConfigs = configuration.getChildNodes(); + for (int i = 0; i < indexingConfigs.getLength(); i++) { + Node configNode = indexingConfigs.item(i); + if (configNode.getNodeName().equals("index-rule")) { + IndexingRule element = new IndexingRule(configNode); + // register under node type and all its sub types + log.debug("Found rule '{}' for NodeType '{}'", element, element.getNodeTypeName()); + for (Name ntName : ntNames) { + if (ntReg.getEffectiveNodeType(ntName).includesNodeType(element.getNodeTypeName())) { + List perNtConfig = nt2rules.get(ntName); + if (perNtConfig == null) { + perNtConfig = new ArrayList(); + nt2rules.put(ntName, perNtConfig); + } + log.debug("Registering it for name '{}'", ntName); + perNtConfig.add(new IndexingRule(element, ntReg.getNodeTypeDef(ntName))); + } + } + } + } + configElements = nt2rules; + } + + + /** + * Returns the first indexing rule that applies to the given node + * state. + * + * @param state a node state. + * @param propertyName the property name to check. + * @return the indexing rule or null if none applies. + */ + private IndexingRule getApplicableIndexingRule(NodeState state, Name propertyName) { + List rules = null; + List r = configElements.get(state.getNodeTypeName()); + if (r != null) { + rules = new ArrayList(); + rules.addAll(r); + } + + for (Name name : state.getMixinTypeNames()) { + r = configElements.get(name); + if (r != null) { + if (rules == null) { + rules = new ArrayList(); + } + rules.addAll(r); + } + } + + if (rules != null) { + for (IndexingRule rule : rules) { + if (rule.appliesTo(state, propertyName)) { + return rule; + } + } + } + + // no applicable rule + return null; + } + + /** + * Returns the namespaces declared on the node. + * + * @param node a DOM node. + * @return the namespaces + */ + private Properties getNamespaces(Node node) { + Properties namespaces = new Properties(); + NamedNodeMap attributes = node.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Attr attribute = (Attr) attributes.item(i); + if (attribute.getName().startsWith("xmlns:")) { + namespaces.setProperty( + attribute.getName().substring(6), attribute.getValue()); + } + } + return namespaces; + } + + /** + * Creates property configurations defined in the config. + * + * @param config the fulltext indexing configuration. + * @param propConfigs will be filled with exact Name to + * PropertyConfig mappings. + * @param namePatterns will be filled with NamePatterns. + * @throws IllegalNameException if the node type name contains illegal + * characters. + * @throws NamespaceException if the node type contains an unknown + * prefix. + */ + private void createPropertyConfigs(Node config, + Map propConfigs, + List namePatterns) + throws IllegalNameException, NamespaceException { + NodeList childNodes = config.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node n = childNodes.item(i); + if (n.getNodeName().equals("property")) { + NamedNodeMap attributes = n.getAttributes(); + // get boost value + float boost = 1.0f; + Node boostAttr = attributes.getNamedItem("boost"); + if (boostAttr != null) { + try { + boost = Float.parseFloat(boostAttr.getNodeValue()); + } catch (NumberFormatException e) { + // use default + } + } + + // get nodeScopeIndex flag + boolean nodeScopeIndex = true; + Node nsIndex = attributes.getNamedItem("nodeScopeIndex"); + if (nsIndex != null) { + nodeScopeIndex = Boolean.valueOf(nsIndex.getNodeValue()); + } + + // get isRegexp flag + boolean isRegexp = false; + Node regexp = attributes.getNamedItem("isRegexp"); + if (regexp != null) { + isRegexp = Boolean.valueOf(regexp.getNodeValue()); + } + + // get useInExcerpt flag + boolean useInExcerpt = true; + Node excerpt = attributes.getNamedItem("useInExcerpt"); + if (excerpt != null) { + useInExcerpt = Boolean.valueOf(excerpt.getNodeValue()); + } + + PropertyConfig pc = new PropertyConfig( + boost, nodeScopeIndex, useInExcerpt); + + if (isRegexp) { + namePatterns.add(new NamePattern( + getTextContent(n), pc, resolver)); + } else { + Name propName = resolver.getQName(getTextContent(n)); + propConfigs.put(propName, pc); + } + } + } + } + + /** + * Gets the condition expression from the configuration. + * + * @param config the config node. + * @return the condition expression or null if there is no + * condition set on the config. + * @throws MalformedPathException if the condition string is malformed. + * @throws IllegalNameException if a name contains illegal characters. + * @throws NamespaceException if a name contains an unknown prefix. + */ + private PathExpression getCondition(Node config) + throws MalformedPathException, IllegalNameException, NamespaceException { + Node conditionAttr = config.getAttributes().getNamedItem("condition"); + if (conditionAttr == null) { + return null; + } + String conditionString = conditionAttr.getNodeValue(); + int idx; + int axis; + Name elementTest = null; + Name nameTest = null; + Name propertyName; + String propertyValue; + + // parse axis + if (conditionString.startsWith("ancestor::")) { + axis = PathExpression.ANCESTOR; + idx = "ancestor::".length(); + } else if (conditionString.startsWith("parent::")) { + axis = PathExpression.PARENT; + idx = "parent::".length(); + } else if (conditionString.startsWith("@")) { + axis = PathExpression.SELF; + idx = "@".length(); + } else { + axis = PathExpression.CHILD; + idx = 0; + } + + try { + if (conditionString.startsWith("element(", idx)) { + int colon = conditionString.indexOf(',', + idx + "element(".length()); + String name = conditionString.substring( + idx + "element(".length(), colon).trim(); + if (!name.equals("*")) { + nameTest = resolver.getQName(ISO9075.decode(name)); + } + idx = conditionString.indexOf(")/@", colon); + String type = conditionString.substring(colon + 1, idx).trim(); + elementTest = resolver.getQName(ISO9075.decode(type)); + idx += ")/@".length(); + } else { + if (axis == PathExpression.ANCESTOR + || axis == PathExpression.CHILD + || axis == PathExpression.PARENT) { + // simple name test + String name = conditionString.substring(idx, + conditionString.indexOf('/', idx)); + if (!name.equals("*")) { + nameTest = resolver.getQName(ISO9075.decode(name)); + } + idx += name.length() + "/@".length(); + } + } + + // parse property name + int eq = conditionString.indexOf('=', idx); + String name = conditionString.substring(idx, eq).trim(); + propertyName = resolver.getQName(ISO9075.decode(name)); + + // parse string value + int quote = conditionString.indexOf('\'', eq) + 1; + propertyValue = conditionString.substring(quote, + conditionString.indexOf('\'', quote)); + } catch (IndexOutOfBoundsException e) { + throw new MalformedPathException(conditionString); + } + + return new PathExpression(axis, elementTest, + nameTest, propertyName, propertyValue); + } + + /** + * @param node a node. + * @return the text content of the node. + */ + private static String getTextContent(Node node) { + StringBuffer content = new StringBuffer(); + NodeList nodes = node.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + Node n = nodes.item(i); + if (n.getNodeType() == Node.TEXT_NODE) { + content.append(((CharacterData) n).getData()); + } + } + return content.toString(); + } + + /** + * A property name pattern. + */ + private static final class NamePattern { + + /** + * The pattern to match. + */ + private final Pattern pattern; + + /** + * The associated configuration. + */ + private final PropertyConfig config; + + /** + * Creates a new name pattern. + * + * @param pattern the pattern as read from the configuration file. + * @param config the associated configuration. + * @param resolver a namespace resolver for parsing name from the + * configuration. + * @throws IllegalNameException if the prefix of the name pattern is + * illegal. + * @throws NamespaceException if the prefix of the name pattern cannot + * be resolved. + */ + private NamePattern(String pattern, + PropertyConfig config, + NameResolver resolver) + throws IllegalNameException, NamespaceException { + String uri = Name.NS_DEFAULT_URI; + String localPattern = pattern; + int idx = pattern.indexOf(':'); + if (idx != -1) { + String prefix = pattern.substring(0, idx); + if (prefix.equals(".*")) { + // match all namespaces + uri = prefix; + } else { + // match only single namespace + // use a dummy local name to get namespace uri + uri = resolver.getQName(prefix + ":a").getNamespaceURI(); + } + localPattern = pattern.substring(idx + 1); + } + this.pattern = Pattern.name(uri, localPattern); + this.config = config; + } + + /** + * @param path the path to match. + * @return true if path matches this name + * pattern; false otherwise. + */ + boolean matches(Path path) { + return pattern.match(path).isFullMatch(); + } + + /** + * @return the property configuration for this name pattern. + */ + PropertyConfig getConfig() { + return config; + } + } + + private class IndexingRule { + + /** + * The NodeTypeDefinition of this fulltext indexing rule. + */ + private final QNodeTypeDefinition nodeTypeDefinition; + + /** + * Map of {@link PropertyConfig}. Key=Name of property. + */ + private final Map propConfigs; + + /** + * List of {@link NamePattern}s. + */ + private final List namePatterns; + + /** + * An expression based on a relative path. + */ + private final PathExpression condition; + + /** + * The boost value for this config element. + */ + private final float boost; + + /** + * Creates a new indexing rule base on an existing one, but for a + * different node type name. + * + * @param original the existing rule. + * @param qNodeTypeDefinition the node type for the rule. + */ + IndexingRule(IndexingRule original, QNodeTypeDefinition qNodeTypeDefinition) { + this.nodeTypeDefinition = qNodeTypeDefinition; + this.propConfigs = original.propConfigs; + this.namePatterns = original.namePatterns; + this.condition = original.condition; + this.boost = original.boost; + } + + /** + * + * @param config the configuration for this rule. + * @throws MalformedPathException if the condition expression is malformed. + * @throws IllegalNameException if a name contains illegal characters. + * @throws NamespaceException if a name contains an unknown prefix. + * @throws NoSuchNodeTypeException if the nodeType could not be evaluated + */ + IndexingRule(Node config) + throws MalformedPathException, IllegalNameException, NamespaceException, NoSuchNodeTypeException { + this.nodeTypeDefinition = getNodeTypeDefinition(config); + this.condition = getCondition(config); + this.boost = getNodeBoost(config); + this.propConfigs = new HashMap(); + this.namePatterns = new ArrayList(); + createPropertyConfigs(config, propConfigs, namePatterns); + } + + /** + * Returns the name of the node type where this rule applies to. + * + * @return name of the node type. + */ + public Name getNodeTypeName() { + return nodeTypeDefinition.getName(); + } + + /** + * @return the value for the node boost. + */ + public float getNodeBoost() { + return boost; + } + + /** + * Returns true if the property with the given name is + * indexed according to this rule. + * + * @param propertyName the name of a property. + * @return true if the property is indexed; + * false otherwise. + */ + public boolean isIndexed(Name propertyName) { + return getConfig(propertyName) != null; + } + + /** + * Returns the boost value for the given property name. If there is no + * configuration entry for the property name the default boost value is + * returned. + * + * @param propertyName the name of a property. + * @return the boost value for the property. + */ + public float getBoost(Name propertyName) { + PropertyConfig config = getConfig(propertyName); + if (config != null) { + return config.boost; + } else { + return DEFAULT_BOOST; + } + } + + /** + * Returns true if the property with the given name should + * be included in the node scope fulltext index. If there is no + * configuration entry for that propery false is returned. + * + * @param propertyName the name of a property. + * @return true if the property should be included in the + * node scope fulltext index. + */ + public boolean isIncludedInNodeScopeIndex(Name propertyName) { + PropertyConfig config = getConfig(propertyName); + return config != null && config.nodeScopeIndex; + } + + /** + * Returns true if the content of the property with the + * given name should show up in an excerpt. If there is no configuration + * entry for that property true is returned. + * + * @param propertyName the name of a property. + * @return true if the content of the property should be + * included in an excerpt; false otherwise. + */ + public boolean useInExcerpt(Name propertyName) { + PropertyConfig config = getConfig(propertyName); + return config == null || config.useInExcerpt; + } + + /** + * Returns true if this rule applies to the given node + * state. + * + * @param state the state to check. + * @param propertyName the property name to check. + * @return true the rule applies to the given node; + * false otherwise. + */ + public boolean appliesTo(NodeState state, Name propertyName) { + Name nodeTypeName = getNodeTypeName(); + if (propertyName != null) { + for (QPropertyDefinition propertyDefinition : nodeTypeDefinition.getPropertyDefs()) { + if (propertyDefinition.getName().equals(propertyName)) { + return true; + } + } + } + if (!nodeTypeName.equals(state.getNodeTypeName())) { + return false; + } + return condition == null || condition.evaluate(state); + } + + //-------------------------< internal >--------------------------------- + + /** + * @param propertyName name of a property. + * @return the property configuration or null if this + * indexing rule does not contain a configuration for the given + * property. + */ + private PropertyConfig getConfig(Name propertyName) { + PropertyConfig config = propConfigs.get(propertyName); + if (config != null) { + return config; + } else if (namePatterns.size() > 0) { + Path path = PATH_FACTORY.create(propertyName); + // check patterns + for (NamePattern np : namePatterns) { + if (np.matches(path)) { + return np.getConfig(); + } + } + } + return null; + } + + /** + * Reads the node type of the root node of the indexing rule. + * + * @param config the configuration. + * @return the name of the node type. + * @throws IllegalNameException if the node type name contains illegal + * characters. + * @throws NamespaceException if the node type contains an unknown + * prefix. + * @throws NoSuchNodeTypeException if the node type could not be evaluated + */ + private QNodeTypeDefinition getNodeTypeDefinition(Node config) + throws IllegalNameException, NamespaceException, NoSuchNodeTypeException { + String ntString = config.getAttributes().getNamedItem("nodeType").getNodeValue(); + return ntReg.getNodeTypeDef(resolver.getQName(ntString)); + } + + /** + * Returns the node boost from the config. + * + * @param config the configuration. + * @return the configured node boost or the default boost if none is + * configured. + */ + private float getNodeBoost(Node config) { + Node boost = config.getAttributes().getNamedItem("boost"); + if (boost != null) { + try { + return Float.parseFloat(boost.getNodeValue()); + } catch (NumberFormatException e) { + // return default boost + } + } + return DEFAULT_BOOST; + } + } + + /** + * Simple class that holds boost and nodeScopeIndex flag. + */ + private static class PropertyConfig { + + /** + * The boost value for a property. + */ + final float boost; + + /** + * Flag that indicates whether a property is included in the node + * scope fulltext index of its parent. + */ + final boolean nodeScopeIndex; + + /** + * Flag that indicates whether the content of a property should be used + * to create an excerpt. + */ + final boolean useInExcerpt; + + PropertyConfig(float boost, + boolean nodeScopeIndex, + boolean useInExcerpt) { + this.boost = boost; + this.nodeScopeIndex = nodeScopeIndex; + this.useInExcerpt = useInExcerpt; + } + } + + private class PathExpression { + + static final int SELF = 0; + + static final int CHILD = 1; + + static final int ANCESTOR = 2; + + static final int PARENT = 3; + + private final int axis; + + private final Name elementTest; + + private final Name nameTest; + + private final Name propertyName; + + private final String propertyValue; + + PathExpression(int axis, Name elementTest, Name nameTest, + Name propertyName, String propertyValue) { + this.axis = axis; + this.elementTest = elementTest; + this.nameTest = nameTest; + this.propertyName = propertyName; + this.propertyValue = propertyValue; + } + + /** + * Evaluates this expression and returns true if the + * condition matches using state as the context node + * state. + * + * @param context the context from where the expression should be + * evaluated. + * @return expression result. + */ + boolean evaluate(final NodeState context) { + // get iterator along specified axis + Iterator nodeStates; + if (axis == SELF) { + nodeStates = Collections.singletonList(context).iterator(); + } else if (axis == CHILD) { + nodeStates = (Iterator) new AbstractIteratorDecorator( + context.getChildNodeEntries().iterator()) { + public Object next() { + ChildNodeEntry cne = + (ChildNodeEntry) super.next(); + try { + return ism.getItemState(cne.getId()); + } catch (ItemStateException e) { + NoSuchElementException nsee = new NoSuchElementException("No node with id " + cne.getId() + " found in child axis"); + nsee.initCause(e); + throw nsee; + } + } + }; + } else if (axis == ANCESTOR) { + try { + nodeStates = new Iterator() { + + private NodeState next = context.getParentId() == null ? null : + (NodeState) ism.getItemState(context.getParentId()); + + public void remove() { + throw new UnsupportedOperationException(); + } + + public boolean hasNext() { + return next != null; + } + + public NodeState next() { + NodeState tmp = next; + try { + if (next.getParentId() != null) { + next = (NodeState) ism.getItemState(next.getParentId()); + } else { + next = null; + } + } catch (ItemStateException e) { + next = null; + } + return tmp; + } + }; + } catch (ItemStateException e) { + nodeStates = EMPTY_NODESTATE_ITERATOR; + } + } else if (axis == PARENT) { + try { + if (context.getParentId() != null) { + NodeState state = (NodeState) ism.getItemState(context.getParentId()); + nodeStates = Collections.singletonList(state).iterator(); + } else { + nodeStates = EMPTY_NODESTATE_ITERATOR; + } + } catch (ItemStateException e) { + nodeStates = EMPTY_NODESTATE_ITERATOR; + } + } else { + // unsupported axis + nodeStates = EMPTY_NODESTATE_ITERATOR; + } + + // check node type, name and property value for each + while (nodeStates.hasNext()) { + try { + NodeState current = nodeStates.next(); + if (elementTest != null + && !current.getNodeTypeName().equals(elementTest)) { + continue; + } + if (nameTest != null + && !hmgr.getName(current.getNodeId()).equals(nameTest)) { + continue; + } + if (!current.hasPropertyName(propertyName)) { + continue; + } + PropertyId propId = new PropertyId( + current.getNodeId(), propertyName); + PropertyState propState = + (PropertyState) ism.getItemState(propId); + InternalValue[] values = propState.getValues(); + for (InternalValue value : values) { + if (value.toString().equals(propertyValue)) { + return true; + } + } + } catch (RepositoryException e) { + // move on to next one + } catch (ItemStateException e) { + // move on to next one + } + } + return false; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingQueue.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingQueue.java new file mode 100644 index 00000000000..4be7f649d48 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingQueue.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.core.query.lucene; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * IndexingQueue implements a queue which contains all the + * documents with pending text extractor jobs. + */ +class IndexingQueue { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(IndexingQueue.class); + + /** + * The store to persist uuids of pending documents. + */ + private final IndexingQueueStore queueStore; + + /** + * Maps UUID {@link String}s to {@link Document}s. + */ + private final Map pendingDocuments = new HashMap(); + + /** + * Flag that indicates whether this indexing queue had been + * {@link #initialize(MultiIndex) initialized}. + */ + private volatile boolean initialized = false; + + /** + * Creates an indexing queue. + * + * @param queueStore the store where to read the pending extraction jobs. + */ + IndexingQueue(IndexingQueueStore queueStore) { + this.queueStore = queueStore; + } + + /** + * Initializes the indexing queue. + * + * @param index the multi index this indexing queue belongs to. + * @throws IOException if an error occurs while reading from the index. + */ + void initialize(MultiIndex index) throws IOException { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + // check index for nodes that need to be reindexed + CachingMultiIndexReader reader = index.getIndexReader(); + try { + TermDocs tDocs = reader.termDocs( + new Term(FieldNames.REINDEXING_REQUIRED, "")); + try { + while (tDocs.next()) { + queueStore.addUUID(reader.document(tDocs.doc(), + FieldSelectors.UUID).get(FieldNames.UUID)); + } + } finally { + tDocs.close(); + } + } finally { + reader.release(); + } + String[] uuids = queueStore.getPending(); + for (String uuid : uuids) { + try { + Document doc = index.createDocument(new NodeId(uuid)); + pendingDocuments.put(uuid, doc); + log.debug("added node {}. New size of indexing queue: {}", + uuid, pendingDocuments.size()); + } catch (IllegalArgumentException e) { + log.warn("Invalid UUID in indexing queue store: " + uuid); + } catch (RepositoryException e) { + // node does not exist anymore + log.debug("Node with uuid {} does not exist anymore", uuid); + queueStore.removeUUID(uuid); + } + } + initialized = true; + } + + /** + * Returns the {@link Document}s that are finished. + * + * @return the {@link Document}s that are finished. + */ + public Document[] getFinishedDocuments() { + checkInitialized(); + List finished = new ArrayList(); + synchronized (this) { + finished.addAll(pendingDocuments.values()); + } + + Iterator it = finished.iterator(); + while (it.hasNext()) { + Document doc = it.next(); + if (!Util.isDocumentReady(doc)) { + it.remove(); + } + } + return finished.toArray(new Document[finished.size()]); + } + + /** + * Removes the document with the given uuid from the indexing + * queue. + * + * @param uuid the uuid of the document to return. + * @return the document for the given uuid or null + * if this queue does not contain a document with the given + * uuid. + */ + public synchronized Document removeDocument(String uuid) { + checkInitialized(); + Document doc = pendingDocuments.remove(uuid); + if (doc != null) { + queueStore.removeUUID(uuid); + log.debug("removed node {}. New size of indexing queue: {}", + uuid, pendingDocuments.size()); + } + return doc; + } + + /** + * Adds a document to this indexing queue. + * + * @param doc the document to add. + * @return an existing document in the queue with the same uuid as the one + * in doc or null if there was no such + * document. + */ + public synchronized Document addDocument(Document doc) { + checkInitialized(); + String uuid = doc.get(FieldNames.UUID); + Document existing = pendingDocuments.put(uuid, doc); + log.debug("added node {}. New size of indexing queue: {}", + uuid, pendingDocuments.size()); + if (existing == null) { + // document wasn't present, add it to the queue store + queueStore.addUUID(uuid); + } + // return existing if any + return existing; + } + + /** + * Closes this indexing queue and disposes all pending documents. + */ + public synchronized void close() { + checkInitialized(); + // go through pending documents and close readers + Iterator it = pendingDocuments.values().iterator(); + while (it.hasNext()) { + Document doc = it.next(); + Util.disposeDocument(doc); + it.remove(); + } + queueStore.close(); + } + + /** + * Checks if this indexing queue is initialized and otherwise throws a + * {@link IllegalStateException}. + */ + private void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + } + + /** + * Returns the number of pending documents. + * + * @return the number of the currently pending documents. + */ + synchronized int getNumPendingDocuments() { + return pendingDocuments.size(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingQueueStore.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingQueueStore.java new file mode 100644 index 00000000000..3b2ce8c12ff --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/IndexingQueueStore.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.query.lucene.directory.IndexInputStream; +import org.apache.lucene.store.Directory; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.HashSet; + +/** + * IndexingQueueStore implements a store that keeps the uuids of + * nodes that are pending in the indexing queue. Until Jackrabbit 1.4 this store + * was also persisted to disk. Starting with 1.5 the pending + * nodes are marked directly in the index with a special field. + * See {@link FieldNames#REINDEXING_REQUIRED}. + */ +class IndexingQueueStore { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(IndexingQueueStore.class); + + /** + * Encoding of the indexing queue store. + */ + private static final Charset ENCODING = StandardCharsets.UTF_8; + + /** + * Operation identifier for an added node. + */ + private static final String ADD = "ADD"; + + /** + * Operation identifier for an removed node. + */ + private static final String REMOVE = "REMOVE"; + + /** + * Name of the file that contains the indexing queue log. + */ + private static final String INDEXING_QUEUE_FILE = "indexing_queue.log"; + + /** + * The UUID Strings of the pending documents. + */ + private final Set pending = new HashSet(); + + /** + * The directory from where to read pending document UUIDs. + */ + private final Directory dir; + + /** + * Creates a new IndexingQueueStore using the given directory. + * + * @param directory the directory to use. + * @throws IOException if an error ocurrs while reading pending UUIDs. + */ + IndexingQueueStore(Directory directory) throws IOException { + this.dir = directory; + readStore(); + } + + /** + * @return the UUIDs of the pending text extraction jobs. + */ + public String[] getPending() { + return pending.toArray(new String[pending.size()]); + } + + /** + * Adds a uuid to the store. + * + * @param uuid the uuid to add. + */ + public void addUUID(String uuid) { + pending.add(uuid); + } + + /** + * Removes a uuid from the store. + * + * @param uuid the uuid to add. + */ + public void removeUUID(String uuid) { + pending.remove(uuid); + } + + /** + * Closes this queue store. + */ + public void close() { + if (pending.isEmpty()) { + try { + if (dir.fileExists(INDEXING_QUEUE_FILE)) { + dir.deleteFile(INDEXING_QUEUE_FILE); + } + } catch (IOException e) { + log.warn("unable to delete " + INDEXING_QUEUE_FILE); + } + } + } + + //----------------------------< internal >---------------------------------- + + /** + * Reads all pending UUIDs from the file and puts them into {@link + * #pending}. + * + * @throws IOException if an error occurs while reading. + */ + private void readStore() throws IOException { + if (dir.fileExists(INDEXING_QUEUE_FILE)) { + InputStream in = new IndexInputStream(dir.openInput(INDEXING_QUEUE_FILE)); + BufferedReader reader = new BufferedReader( + new InputStreamReader(in, ENCODING)); + try { + String line; + while ((line = reader.readLine()) != null) { + int idx = line.indexOf(' '); + if (idx == -1) { + // invalid line + log.warn("invalid line in {}: {}", INDEXING_QUEUE_FILE, line); + } else { + String cmd = line.substring(0, idx); + String uuid = line.substring(idx + 1, line.length()); + if (ADD.equals(cmd)) { + pending.add(uuid); + } else if (REMOVE.equals(cmd)) { + pending.remove(uuid); + } else { + // invalid line + log.warn("invalid line in {}: {}", INDEXING_QUEUE_FILE, line); + } + } + } + } finally { + in.close(); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitAnalyzer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitAnalyzer.java new file mode 100644 index 00000000000..9a07ca02f91 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitAnalyzer.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.core.query.lucene; + +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Constructor; +import java.util.Collections; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.standard.ClassicAnalyzer; +import org.apache.lucene.util.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the global jackrabbit lucene analyzer. By default, all + * properties are indexed with the StandardAnalyzer(new String[]{}), + * unless in the <SearchIndex> configuration a global analyzer is defined. + * + * In the indexing configuration, properties can be configured to be + * indexed with a specific analyzer. If configured, this analyzer is used to + * index the text of the property and to parse searchtext for this property. + */ +public class JackrabbitAnalyzer extends Analyzer { + + private static Logger log = + LoggerFactory.getLogger(JackrabbitAnalyzer.class); + + private static final Analyzer DEFAULT_ANALYZER = new ClassicAnalyzer( + Version.LUCENE_36, Collections.emptySet()); + + /** + * Returns a new instance of the named Lucene {@link Analyzer} class, + * or the default analyzer if the given class can not be instantiated. + * + * @param className name of the analyzer class + * @return new analyzer instance, or the default analyzer + */ + static Analyzer getAnalyzerInstance(String className) { + Class analyzerClass; + try { + analyzerClass = Class.forName(className); + } catch (ClassNotFoundException e) { + log.warn(className + " could not be found", e); + return DEFAULT_ANALYZER; + } + if (!Analyzer.class.isAssignableFrom(analyzerClass)) { + log.warn(className + " is not a Lucene Analyzer"); + return DEFAULT_ANALYZER; + } else if (JackrabbitAnalyzer.class.isAssignableFrom(analyzerClass)) { + log.warn(className + " can not be used as a JackrabbitAnalyzer component"); + return DEFAULT_ANALYZER; + } + + Exception cause = null; + Constructor[] constructors = analyzerClass.getConstructors(); + for (Constructor constructor : constructors) { + Class[] types = constructor.getParameterTypes(); + if (types.length == 1 && types[0] == Version.class) { + try { + return (Analyzer) constructor.newInstance(Version.LUCENE_36); + } catch (Exception e) { + cause = e; + } + } + } + for (Constructor constructor : constructors) { + if (constructor.getParameterTypes().length == 0) { + try { + return (Analyzer) constructor.newInstance(); + } catch (Exception e) { + cause = e; + } + } + } + + log.warn(className + " could not be instantiated", cause); + return DEFAULT_ANALYZER; + } + + /** + * The default Jackrabbit analyzer if none is configured in + * <SearchIndex> configuration. + */ + private Analyzer defaultAnalyzer = DEFAULT_ANALYZER; + + /** + * The indexing configuration. + */ + private IndexingConfiguration indexingConfig; + + /** + * A param indexingConfig the indexing configuration. + */ + protected void setIndexingConfig(IndexingConfiguration indexingConfig) { + this.indexingConfig = indexingConfig; + } + + /** + * @param analyzer the default jackrabbit analyzer + */ + protected void setDefaultAnalyzer(Analyzer analyzer) { + defaultAnalyzer = analyzer; + } + + String getDefaultAnalyzerClass() { + return defaultAnalyzer.getClass().getName(); + } + + void setDefaultAnalyzerClass(String className) { + setDefaultAnalyzer(getAnalyzerInstance(className)); + } + + /** + * Creates a TokenStream which tokenizes all the text in the provided + * Reader. If the fieldName (property) is configured to have a different + * analyzer than the default, this analyzer is used for tokenization + */ + public final TokenStream tokenStream(String fieldName, Reader reader) { + if (indexingConfig != null) { + Analyzer propertyAnalyzer = indexingConfig.getPropertyAnalyzer(fieldName); + if (propertyAnalyzer != null) { + return propertyAnalyzer.tokenStream(fieldName, reader); + } + } + return defaultAnalyzer.tokenStream(fieldName, reader); + } + + @Override + public final TokenStream reusableTokenStream(String fieldName, Reader reader) + throws IOException { + if (indexingConfig != null) { + Analyzer propertyAnalyzer = indexingConfig.getPropertyAnalyzer(fieldName); + if (propertyAnalyzer != null) { + return propertyAnalyzer.reusableTokenStream(fieldName, reader); + } + } + return defaultAnalyzer.reusableTokenStream(fieldName, reader); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexReader.java new file mode 100644 index 00000000000..0f75f267d6d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexReader.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.core.query.lucene; + +import org.apache.lucene.index.FilterIndexReader; +import org.apache.lucene.index.IndexReader; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; + +import java.io.IOException; + +/** + * JackrabbitIndexReader wraps an index reader and + * {@link ReleaseableIndexReader#release() releases} the underlying reader + * when a client calls {@link #close()} on this reader. This allows reusing + * of the underlying index reader instance. + */ +public final class JackrabbitIndexReader + extends FilterIndexReader + implements HierarchyResolver, MultiIndexReader { + + /** + * The hierarchy resolver. + */ + private final HierarchyResolver resolver; + + /** + * The underlying index reader exposed as a {@link MultiIndexReader}. + */ + private final MultiIndexReader reader; + + /** + * Creates a new JackrabbitIndexReader. The passed index reader + * must also implement the interfaces {@link HierarchyResolver} and + * {@link MultiIndexReader}. + * + * @param in the underlying index reader. + * @throws IllegalArgumentException if in does not implement + * {@link HierarchyResolver} and + * {@link MultiIndexReader}. + */ + public JackrabbitIndexReader(IndexReader in) { + super(in); + if (!(in instanceof MultiIndexReader)) { + throw new IllegalArgumentException("IndexReader must also implement MultiIndexReader"); + } + if (!(in instanceof HierarchyResolver)) { + throw new IllegalArgumentException("IndexReader must also implement HierarchyResolver"); + } + this.resolver = (HierarchyResolver) in; + this.reader = (MultiIndexReader) in; + } + + /** + * Overwrite termDocs(Term) and forward the call to the + * wrapped reader. + */ + @Override + public TermDocs termDocs(Term term) throws IOException { + return in.termDocs(term); + } + + //--------------------------< FilterIndexReader >--------------------------- + + /** + * Calls release on the underlying {@link MultiIndexReader} instead of + * closing it. + * + * @throws IOException if an error occurs while releaseing the underlying + * index reader. + */ + protected void doClose() throws IOException { + reader.release(); + } + + //------------------------< HierarchyResolver >----------------------------- + + /** + * {@inheritDoc} + */ + public int[] getParents(int n, int[] docNumbers) throws IOException { + return resolver.getParents(n, docNumbers); + } + + //-------------------------< MultiIndexReader >----------------------------- + + /** + * {@inheritDoc} + */ + public IndexReader[] getIndexReaders() { + return reader.getIndexReaders(); + } + + public IndexReader[] getSequentialSubReaders() { + // No sequential sub-readers + return null; + } + + /** + * {@inheritDoc} + */ + public ForeignSegmentDocId createDocId(NodeId id) throws IOException { + return reader.createDocId(id); + } + + /** + * {@inheritDoc} + */ + public int getDocumentNumber(ForeignSegmentDocId docId) throws IOException { + return reader.getDocumentNumber(docId); + } + + /** + * {@inheritDoc} + */ + public void release() throws IOException { + reader.release(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexSearcher.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexSearcher.java new file mode 100644 index 00000000000..0386854b7c5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitIndexSearcher.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.query.lucene.constraint.EvaluationContext; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Sort; + +/** + * JackrabbitIndexSearcher implements an index searcher with + * jackrabbit specific optimizations. + */ +public class JackrabbitIndexSearcher + extends IndexSearcher + implements EvaluationContext { + + /** + * The session that executes the query. + */ + private final SessionImpl session; + + /** + * The underlying index reader. + */ + private final IndexReader reader; + + /** + * The item state manager of the workspace. + */ + private final ItemStateManager ism; + + /** + * Creates a new jackrabbit index searcher. + * + * @param s the session that executes the query. + * @param r the index reader. + * @param ism the shared item state manager. + */ + public JackrabbitIndexSearcher(SessionImpl s, + IndexReader r, + ItemStateManager ism) { + super(r); + this.session = s; + this.reader = r; + this.ism = ism; + } + + /** + * Executes the query and returns the hits that match the query. + * + * @param query the query to execute. + * @param sort the sort criteria. + * @param resultFetchHint a hint on how many results should be fetched. + * @param selectorName the single selector name for the query hits. + * @return the query hits. + * @throws IOException if an error occurs while executing the query. + */ + public MultiColumnQueryHits execute(Query query, + Sort sort, + long resultFetchHint, + Name selectorName) + throws IOException { + return new QueryHitsAdapter( + evaluate(query, sort, resultFetchHint), selectorName); + } + + /** + * Evaluates the query and returns the hits that match the query. + * + * @param query the query to execute. + * @param sort the sort criteria. + * @param resultFetchHint a hint on how many results should be fetched. + * @return the query hits. + * @throws IOException if an error occurs while executing the query. + */ + public QueryHits evaluate(Query query, Sort sort, long resultFetchHint) + throws IOException { + query = query.rewrite(reader); + QueryHits hits = null; + if (query instanceof JackrabbitQuery) { + hits = ((JackrabbitQuery) query).execute(this, session, sort); + } + if (hits == null) { + if (sort.getSort().length == 0) { + hits = new LuceneQueryHits(reader, this, query); + } else { + hits = new SortedLuceneQueryHits(this, query, sort, + resultFetchHint); + } + } + return hits; + } + + //---------------------------< IndexSearcher >------------------------------ + + @Override + public int docFreq(Term term) throws IOException { + // provide a fixed document frequency for fields that are not fulltext + // indexed. correct frequency is only useful for fulltext queries. + if (FieldNames.isFulltextField(term.field())) { + return super.docFreq(term); + } else { + return 1; + } + } + + //------------------------< EvaluationContext >----------------------------- + + /** + * Evaluates the query and returns the hits that match the query. + * + * @param query the query to execute. + * @return the query hits. + * @throws IOException if an error occurs while executing the query. + */ + public QueryHits evaluate(Query query) throws IOException { + return evaluate(query, new Sort(), Integer.MAX_VALUE); + } + + /** + * @return session that executes the query. + */ + public SessionImpl getSession() { + return session; + } + + /** + * @return the item state manager of the workspace. + */ + public ItemStateManager getItemStateManager() { + return ism; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitQuery.java new file mode 100644 index 00000000000..d273f4a2817 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitQuery.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.core.query.lucene; + +import org.apache.lucene.search.Sort; +import org.apache.jackrabbit.core.SessionImpl; + +import java.io.IOException; + +/** + * JackrabbitQuery defines an interface for Jackrabbit query + * implementations that are at the root of the lucene query tree. It gives the + * implementation the opportunity to execute in an optimized way returning + * {@link QueryHits} instead of a result that is tied to Lucene. + */ +public interface JackrabbitQuery { + + /** + * Executes this query and returns {@link QueryHits} or null if + * this query should be executed using the regular Lucene API. + *

    + * Important note: an implementation must not call + * {@link JackrabbitIndexSearcher#execute(Query, Sort, long, org.apache.jackrabbit.spi.Name)} + * with this query instance as a parameter, otherwise a stack overflow will + * occur. + * + * @param searcher the jackrabbit index searcher. + * @param session the session that executes the query. + * @param sort the sort criteria that must be reflected in the returned + * {@link QueryHits}. + * @return the query hits or null if the regular Lucene API + * should be used by the caller. + * @throws IOException if an error occurs while executing the query. + */ + public QueryHits execute(JackrabbitIndexSearcher searcher, + SessionImpl session, + Sort sort) + throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitQueryParser.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitQueryParser.java new file mode 100644 index 00000000000..dff9dcdd798 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitQueryParser.java @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.ArrayList; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.standard.StandardTokenizer; +import org.apache.lucene.analysis.tokenattributes.TypeAttribute; +import org.apache.lucene.queryParser.QueryParser; +import org.apache.lucene.queryParser.ParseException; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.util.Version; + +/** + * JackrabbitQueryParser extends the standard lucene query parser + * and adds JCR specific customizations. + */ +public class JackrabbitQueryParser extends QueryParser { + + /** + * The Jackrabbit synonym provider or null if there is none. + */ + private final SynonymProvider synonymProvider; + + private final PerQueryCache cache; + + /** + * Creates a new query parser instance. + * + * @param fieldName the field name. + * @param analyzer the analyzer. + * @param synonymProvider the synonym provider or null if none + * is available. + */ + public JackrabbitQueryParser(String fieldName, + Analyzer analyzer, + SynonymProvider synonymProvider, + PerQueryCache cache) { + super(Version.LUCENE_36, fieldName, analyzer); + this.synonymProvider = synonymProvider; + this.cache = cache; + setAllowLeadingWildcard(true); + // see JCR-3075 + setAutoGeneratePhraseQueries(true); + setDefaultOperator(Operator.AND); + } + + /** + * {@inheritDoc} + */ + public Query parse(String textsearch) throws ParseException { + // replace escaped ' with just ' + StringBuffer rewritten = new StringBuffer(); + // the default lucene query parser recognizes 'AND' and 'NOT' as + // keywords. + textsearch = textsearch.replaceAll("AND", "and"); + textsearch = textsearch.replaceAll("NOT", "not"); + boolean escaped = false; + for (int i = 0; i < textsearch.length(); i++) { + if (textsearch.charAt(i) == '\\') { + if (escaped) { + rewritten.append("\\\\"); + escaped = false; + } else { + escaped = true; + } + } else if (textsearch.charAt(i) == '\'') { + if (escaped) { + escaped = false; + } + rewritten.append(textsearch.charAt(i)); + } else if (textsearch.charAt(i) == '~') { + if (i == 0 || Character.isWhitespace(textsearch.charAt(i - 1))) { + // escape tilde so we can use it for similarity query + rewritten.append("\\"); + } + rewritten.append('~'); + } else if (textsearch.charAt(i) == ':') { + // fields as known in lucene are not supported + rewritten.append("\\:"); + } else { + if (escaped) { + rewritten.append('\\'); + escaped = false; + } + rewritten.append(textsearch.charAt(i)); + } + } + return super.parse(rewritten.toString()); + } + + /** + * Factory method for generating a synonym query. + * Called when parser parses an input term token that has the synonym + * prefix (~term) prepended. + * + * @param field Name of the field query will use. + * @param termStr Term token to use for building term for the query + * + * @return Resulting {@link Query} built for the term + * @exception ParseException throw in overridden method to disallow + */ + protected Query getSynonymQuery(String field, String termStr) + throws ParseException { + List synonyms = new ArrayList(); + synonyms.add(new BooleanClause(getFieldQuery(field, termStr), + BooleanClause.Occur.SHOULD)); + if (synonymProvider != null) { + for (String term : synonymProvider.getSynonyms(termStr)) { + synonyms.add(new BooleanClause(getFieldQuery(field, term), BooleanClause.Occur.SHOULD)); + } + } + if (synonyms.size() == 1) { + return synonyms.get(0).getQuery(); + } else { + return getBooleanQuery(synonyms); + } + } + + + /** + * {@inheritDoc} + */ + protected Query getFieldQuery(String field, String queryText) + throws ParseException { + return getFieldQuery(field, queryText, true); + } + + /** + * {@inheritDoc} + */ + protected Query getFieldQuery(String field, String queryText, boolean quoted) + throws ParseException { + if (queryText.startsWith("~")) { + // synonym query + return getSynonymQuery(field, queryText.substring(1)); + } else { + return super.getFieldQuery(field, queryText, quoted); + } + } + + /** + * {@inheritDoc} + */ + protected Query getPrefixQuery(String field, String termStr) + throws ParseException { + // only create a prefix query when the term is a single word / token + Analyzer a = getAnalyzer(); + TokenStream ts = a.tokenStream(field, new StringReader(termStr)); + int count = 0; + boolean isCJ = false; + try { + TypeAttribute t = ts.addAttribute(TypeAttribute.class); + ts.reset(); + while (ts.incrementToken()) { + count++; + isCJ = StandardTokenizer.TOKEN_TYPES[StandardTokenizer.CJ].equals(t.type()); + } + ts.end(); + } catch (IOException e) { + throw new ParseException(e.getMessage()); + } finally { + try { + ts.close(); + } catch (IOException e) { + // ignore + } + } + if (count > 1 && isCJ) { + return getFieldQuery(field, termStr); + } else { + return getWildcardQuery(field, termStr + "*"); + } + } + + /** + * {@inheritDoc} + */ + protected Query getWildcardQuery(String field, String termStr) + throws ParseException { + if (getLowercaseExpandedTerms()) { + termStr = termStr.toLowerCase(); + } + return new WildcardQuery(field, null, translateWildcards(termStr), cache); + } + + /** + * Translates unescaped wildcards '*' and '?' into '%' and '_'. + * + * @param input the input String. + * @return the translated String. + */ + private String translateWildcards(String input) { + StringBuffer translated = new StringBuffer(input.length()); + boolean escaped = false; + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) == '\\') { + if (escaped) { + translated.append("\\\\"); + escaped = false; + } else { + escaped = true; + } + } else if (input.charAt(i) == '*') { + if (escaped) { + translated.append('*'); + escaped = false; + } else { + translated.append('%'); + } + } else if (input.charAt(i) == '?') { + if (escaped) { + translated.append('?'); + escaped = false; + } else { + translated.append('_'); + } + } else if (input.charAt(i) == '%' || input.charAt(i) == '_') { + // escape every occurrence of '%' and '_' + escaped = false; + translated.append('\\').append(input.charAt(i)); + } else { + if (escaped) { + translated.append('\\'); + escaped = false; + } + translated.append(input.charAt(i)); + } + } + return translated.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitTermQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitTermQuery.java new file mode 100644 index 00000000000..bdb679471f1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JackrabbitTermQuery.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.Weight; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.IndexReader; + +/** + * JackrabbitTermQuery implements a {@link TermQuery} where score + * values are retrieved on a per index segment basis using {@link MultiScorer}. + */ +public class JackrabbitTermQuery extends TermQuery { + + private static final long serialVersionUID = 4244799812287335957L; + + public JackrabbitTermQuery(Term t) { + super(t); + } + + public Weight createWeight(Searcher searcher) throws IOException { + // use a FilterSearcher to prevent per segment searches + // done by lucene. we handle that on our own + // see instanceof check in lucene TermWeight constructor + return new JackrabbitTermWeight(searcher, + super.createWeight(new FilterSearcher(searcher))); + } + + /** + * The weight implementation. + */ + protected class JackrabbitTermWeight extends AbstractWeight { + + private static final long serialVersionUID = -2070964510010945854L; + + /** + * The default lucene TermQuery weight. + */ + private final Weight weight; + + public JackrabbitTermWeight(Searcher searcher, Weight weight) { + super(searcher); + this.weight = weight; + } + + /** + * {@inheritDoc} + */ + protected Scorer createScorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + return weight.scorer(reader, scoreDocsInOrder, topScorer); + } + + /** + * {@inheritDoc} + */ + public Query getQuery() { + return JackrabbitTermQuery.this; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return weight.getValue(); + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + return weight.sumOfSquaredWeights(); + } + + /** + * {@inheritDoc} + */ + public void normalize(float norm) { + weight.normalize(norm); + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws + IOException { + return weight.explain(reader, doc); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JoinQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JoinQuery.java new file mode 100644 index 00000000000..94dac138e71 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/JoinQuery.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.commons.query.qom.JoinType; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.query.lucene.join.Join; +import org.apache.jackrabbit.spi.commons.query.qom.JoinConditionImpl; +import org.apache.lucene.index.IndexReader; + +/** + * JoinQuery implements a query that performs a join. + */ +public class JoinQuery implements MultiColumnQuery { + + /** + * The left side of the join. + */ + private final MultiColumnQuery left; + + /** + * The right side of the join. + */ + private final MultiColumnQuery right; + + /** + * The join type. + */ + private final JoinType joinType; + + /** + * The QOM join condition. + */ + private final JoinConditionImpl joinCondition; + + /** + * Namespace mappings of this index. + */ + private final NamespaceMappings nsMappings; + + /** + * The hierarchy manager of the workspace. + */ + private final HierarchyManager hmgr; + + /** + * Creates a new join query. + * + * @param left the left side of the query. + * @param right the right side of the query. + * @param joinType the join type. + * @param joinCondition the join condition. + * @param nsMappings namespace mappings of this index + * @param hmgr the hierarchy manager of the workspace. + */ + public JoinQuery(MultiColumnQuery left, + MultiColumnQuery right, + JoinType joinType, + JoinConditionImpl joinCondition, + NamespaceMappings nsMappings, + HierarchyManager hmgr) { + this.left = left; + this.right = right; + this.joinType = joinType; + this.joinCondition = joinCondition; + this.nsMappings = nsMappings; + this.hmgr = hmgr; + } + + /** + * {@inheritDoc} + */ + public MultiColumnQueryHits execute(JackrabbitIndexSearcher searcher, + Ordering[] orderings, + long resultFetchHint) + throws IOException { + IndexReader reader = searcher.getIndexReader(); + HierarchyResolver resolver = (HierarchyResolver) reader; + return Join.create(left.execute(searcher, orderings, resultFetchHint), + right.execute(searcher, orderings, resultFetchHint), + joinType, joinCondition, reader, resolver, nsMappings, hmgr); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LazyTextExtractorField.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LazyTextExtractorField.java new file mode 100644 index 00000000000..6ea0de30bda --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LazyTextExtractorField.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.core.query.lucene; + +import java.io.InputStream; +import java.io.Reader; +import java.util.concurrent.Executor; + +import org.apache.jackrabbit.core.LowPriorityTask; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.document.AbstractField; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.Field.TermVector; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; +import org.apache.tika.sax.BodyContentHandler; +import org.apache.tika.sax.WriteOutContentHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * LazyTextExtractorField implements a Lucene field with a String + * value that is lazily initialized from a given {@link Reader}. In addition + * this class provides a method to find out whether the purpose of the reader + * is to extract text and whether the extraction process is already finished. + * + * @see #isExtractorFinished() + */ +@SuppressWarnings("serial") +public class LazyTextExtractorField extends AbstractField { + + /** + * The logger instance for this class. + */ + private static final Logger log = + LoggerFactory.getLogger(LazyTextExtractorField.class); + + /** + * The extracted text content of the given binary value. + * Set to non-null when the text extraction task finishes. + */ + private volatile String extract = null; + + /** + * Creates a new LazyTextExtractorField. + * + * @param parser + * @param value + * @param metadata + * @param executor + * @param highlighting + * set to true to enable result highlighting support + * @param maxFieldLength + * @param withNorms + */ + public LazyTextExtractorField( + Parser parser, InternalValue value, Metadata metadata, + Executor executor, boolean highlighting, int maxFieldLength, + boolean withNorms) { + super(FieldNames.FULLTEXT, + highlighting ? Store.YES : Store.NO, + withNorms ? Field.Index.ANALYZED : Field.Index.ANALYZED_NO_NORMS, + highlighting ? TermVector.WITH_OFFSETS : TermVector.NO); + executor.execute(new ParsingTask(parser, value, metadata, + maxFieldLength) { + public void setExtractedText(String value) { + LazyTextExtractorField.this.setExtractedText(value); + } + }); + } + + /** + * Returns the extracted text. This method blocks until the text + * extraction task has been completed. + * + * @return the string value of this field + */ + public synchronized String stringValue() { + try { + while (!isExtractorFinished()) { + wait(); + } + return extract; + } catch (InterruptedException e) { + log.error("Text extraction thread was interrupted", e); + return ""; + } + } + + /** + * @return always null + */ + public Reader readerValue() { + return null; + } + + /** + * @return always null + */ + public byte[] binaryValue() { + return null; + } + + /** + * @return always null + */ + public TokenStream tokenStreamValue() { + return null; + } + + /** + * Checks whether the text extraction task has finished. + * + * @return true if the extracted text is available + */ + public boolean isExtractorFinished() { + return extract != null; + } + + private synchronized void setExtractedText(String value) { + extract = value; + notify(); + } + + /** + * Releases all resources associated with this field. + */ + public void dispose() { + // TODO: Cause the ContentHandler below to throw an exception + } + + /** + * The background task for extracting text from a binary value. + */ + abstract static class ParsingTask extends BodyContentHandler implements LowPriorityTask { + + private final Parser parser; + + private final InternalValue value; + + private final Metadata metadata; + + private final WriteOutContentHandler writeOutContentHandler; + + public ParsingTask(Parser parser, InternalValue value, + Metadata metadata, int maxFieldLength) { + this(new WriteOutContentHandler(maxFieldLength), parser, value, + metadata); + } + + private ParsingTask(WriteOutContentHandler writeOutContentHandler, + Parser parser, InternalValue value, Metadata metadata) { + super(writeOutContentHandler); + this.writeOutContentHandler = writeOutContentHandler; + this.parser = parser; + this.value = value; + this.metadata = metadata; + } + + public void run() { + try { + InputStream stream = value.getStream(); + try { + parser.parse(stream, this, metadata, new ParseContext()); + } finally { + stream.close(); + } + } catch (LinkageError e) { + // Capture and ignore errors caused by extraction libraries + // not being present. This is equivalent to disabling + // selected media types in configuration, so we can simply + // ignore these errors. + if (!writeOutContentHandler.isWriteLimitReached(e)) { + log.debug("Failed to extract text from a binary property." + + " This is a fairly common case, and nothing to" + + " worry about. The stack trace is included to" + + " help improve the text extraction feature.", e); + setExtractedText("TextExtractionError"); + return; + } + } catch (Throwable t) { + // Capture and report any other full text extraction problems. + // The special STOP exception is used for normal termination. + if (!writeOutContentHandler.isWriteLimitReached(t)) { + log.debug("Failed to extract text from a binary property." + + " This is a fairly common case, and nothing to" + + " worry about. The stack trace is included to" + + " help improve the text extraction feature.", t); + setExtractedText("TextExtractionError"); + return; + } + } finally { + value.discard(); + } + setExtractedText(writeOutContentHandler.toString()); + } + + protected abstract void setExtractedText(String value); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LengthSortComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LengthSortComparator.java new file mode 100644 index 00000000000..408f5977ebd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LengthSortComparator.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.core.query.lucene; + +import org.apache.jackrabbit.core.query.lucene.SharedFieldComparatorSource.SimpleFieldComparator; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.FieldComparatorSource; + +import java.io.IOException; + +/** + * LengthSortComparator implements a FieldComparator which + * sorts on the length of property values. + */ +public class LengthSortComparator extends FieldComparatorSource { + + /** + * The index internal namespace mappings. + */ + private final NamespaceMappings nsMappings; + + public LengthSortComparator(NamespaceMappings nsMappings) { + this.nsMappings = nsMappings; + } + + @Override + public FieldComparator newComparator(String fieldname, int numHits, int sortPos, boolean reversed) throws IOException { + NameFactory factory = NameFactoryImpl.getInstance(); + try { + return new SimpleFieldComparator(nsMappings.translateName(factory.create(fieldname)), FieldNames.PROPERTY_LENGTHS, numHits); + } + catch (IllegalNameException e) { + throw Util.createIOException(e); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LocalNameQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LocalNameQuery.java new file mode 100644 index 00000000000..ac558b774cb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LocalNameQuery.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.core.query.lucene; + +import org.apache.lucene.search.Query; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; + +import java.io.IOException; +import java.util.Set; + +/** + * LocalNameQuery implements a query for the local name of a node. + */ +@SuppressWarnings("serial") +public class LocalNameQuery extends Query { + + /** + * The local name of a node. + */ + private final String localName; + + /** + * The index format version. + */ + private final IndexFormatVersion version; + + /** + * Creates a new LocalNameQuery for the given + * localName. + * + * @param localName the local name of a node. + * @param version the version of the index. + */ + public LocalNameQuery(String localName, IndexFormatVersion version) { + this.localName = localName; + this.version = version; + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + if (version.getVersion() >= IndexFormatVersion.V3.getVersion()) { + return new JackrabbitTermQuery(new Term(FieldNames.LOCAL_NAME, localName)); + } else { + throw new IOException("LocalNameQuery requires IndexFormatVersion V3"); + } + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + } + + /** + * {@inheritDoc} + */ + public String toString(String field) { + return "local-name() = " + localName; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LocalNameRangeQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LocalNameRangeQuery.java new file mode 100644 index 00000000000..db881fb7296 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LocalNameRangeQuery.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.core.query.lucene; + +import org.apache.lucene.index.Term; + +/** + * LocalNameRangeQuery implements a range query on the local name + * of nodes. + */ +@SuppressWarnings("serial") +public class LocalNameRangeQuery extends RangeQuery { + + /** + * Creates a new LocalNameRangeQuery. The lower or the upper + * bound may be null, but not both! + * + * @param lowerName the lower bound or null. + * @param upperName the upper bound or null. + * @param inclusive if bounds are inclusive. + */ + public LocalNameRangeQuery(String lowerName, + String upperName, + boolean inclusive, + PerQueryCache cache) { + super(getLowerTerm(lowerName), getUpperTerm(upperName), inclusive, cache); + } + + /** + * Creates a {@link Term} for the lower bound local name. + * + * @param lowerName the lower bound local name. + * @return a {@link Term} for the lower bound local name. + */ + private static Term getLowerTerm(String lowerName) { + String text; + if (lowerName == null) { + text = ""; + } else { + text = lowerName; + } + return new Term(FieldNames.LOCAL_NAME, text); + } + + /** + * Creates a {@link Term} for the upper bound local name. + * + * @param upperName the upper bound local name. + * @return a {@link Term} for the upper bound local name. + */ + private static Term getUpperTerm(String upperName) { + String text; + if (upperName == null) { + text = "\uFFFF"; + } else { + text = upperName; + } + return new Term(FieldNames.LOCAL_NAME, text); + } +} diff --git a/src/java/org/apache/jackrabbit/core/search/lucene/LongField.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LongField.java similarity index 81% rename from src/java/org/apache/jackrabbit/core/search/lucene/LongField.java rename to jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LongField.java index 0971447c5bc..b003f82c43b 100644 --- a/src/java/org/apache/jackrabbit/core/search/lucene/LongField.java +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LongField.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.core.search.lucene; +package org.apache.jackrabbit.core.query.lucene; /** */ diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LowerCaseSortComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LowerCaseSortComparator.java new file mode 100644 index 00000000000..6b65c48ddb5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LowerCaseSortComparator.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.core.query.lucene; + +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.FieldComparatorSource; + +import java.io.IOException; + +/** + * LowerCaseSortComparator implements a FieldComparator which + * compares the lower-cased string values of a base comparator. + */ +public class LowerCaseSortComparator extends FieldComparatorSource { + + /** + * The base comparator. + */ + private final FieldComparatorSource base; + + /** + * Creates a new upper case sort comparator. + * + * @param base the base sort comparator source. + */ + public LowerCaseSortComparator(FieldComparatorSource base) { + this.base = base; + } + + @Override + public FieldComparator newComparator(String fieldname, int numHits, int sortPos, boolean reversed) throws IOException { + FieldComparator comparator = base.newComparator(fieldname, numHits, sortPos, reversed); + assert comparator instanceof FieldComparatorBase; + + return new FieldComparatorDecorator((FieldComparatorBase) comparator) { + @Override + protected Comparable sortValue(int doc) { + Comparable c = super.sortValue(doc); + return c == null ? null : c.toString().toLowerCase(); + } + }; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java new file mode 100644 index 00000000000..be3505701f2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryBuilder.java @@ -0,0 +1,1211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; +import java.math.BigDecimal; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.query.InvalidQueryException; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.HierarchyManagerImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.SearchManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.query.PropertyTypeRegistry; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.query.AndQueryNode; +import org.apache.jackrabbit.spi.commons.query.DefaultQueryNodeVisitor; +import org.apache.jackrabbit.spi.commons.query.DerefQueryNode; +import org.apache.jackrabbit.spi.commons.query.ExactQueryNode; +import org.apache.jackrabbit.spi.commons.query.LocationStepQueryNode; +import org.apache.jackrabbit.spi.commons.query.NodeTypeQueryNode; +import org.apache.jackrabbit.spi.commons.query.NotQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrderQueryNode; +import org.apache.jackrabbit.spi.commons.query.PathQueryNode; +import org.apache.jackrabbit.spi.commons.query.PropertyFunctionQueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryConstants; +import org.apache.jackrabbit.spi.commons.query.QueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryNodeVisitor; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.RelationQueryNode; +import org.apache.jackrabbit.spi.commons.query.TextsearchQueryNode; +import org.apache.jackrabbit.util.ISO8601; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.util.XMLChar; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryParser.ParseException; +import org.apache.lucene.queryParser.QueryParser; +import org.apache.lucene.search.*; +import org.apache.lucene.search.BooleanClause.Occur; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements a query builder that takes an abstract query tree and creates + * a lucene {@link org.apache.lucene.search.Query} tree that can be executed + * on an index. + * todo introduce a node type hierarchy for efficient translation of NodeTypeQueryNode + */ +public class LuceneQueryBuilder implements QueryNodeVisitor { + + /** + * Logger for this class + */ + private static final Logger log = LoggerFactory.getLogger(LuceneQueryBuilder.class); + + /** + * The path factory instance. + */ + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + /** + * The name of a parent path element. + */ + private static final Name PARENT_ELEMENT_NAME = PATH_FACTORY.getParentElement().getName(); + + /** + * Name constant for fn:name() + */ + private static final Name FN_NAME = NameFactoryImpl.getInstance().create(SearchManager.NS_FN_URI, "name()"); + + /** + * Root node of the abstract query tree + */ + private final QueryRootNode root; + + /** + * Session of the user executing this query + */ + private final SessionImpl session; + + /** + * The shared item state manager of the workspace. + */ + private final ItemStateManager sharedItemMgr; + + /** + * A hierarchy manager based on {@link #sharedItemMgr} to resolve paths. + */ + private final HierarchyManager hmgr; + + /** + * Namespace mappings to internal prefixes + */ + private final NamespaceMappings nsMappings; + + /** + * Name and Path resolver + */ + private final NamePathResolver resolver; + + /** + * The analyzer instance to use for contains function query parsing + */ + private final Analyzer analyzer; + + /** + * The property type registry. + */ + private final PropertyTypeRegistry propRegistry; + + /** + * The synonym provider or null if none is configured. + */ + private final SynonymProvider synonymProvider; + + /** + * Wether the index format is new or old. + */ + private final IndexFormatVersion indexFormatVersion; + + /** + * Exceptions thrown during tree translation + */ + private final List exceptions = new ArrayList(); + + private final PerQueryCache cache; + + /** + * Creates a new LuceneQueryBuilder instance. + * + * @param root the root node of the abstract query tree. + * @param session of the user executing this query. + * @param sharedItemMgr the shared item state manager of the + * workspace. + * @param hmgr a hierarchy manager based on sharedItemMgr. + * @param nsMappings namespace resolver for internal prefixes. + * @param analyzer for parsing the query statement of the contains + * function. + * @param propReg the property type registry. + * @param synonymProvider the synonym provider or null if + * node is configured. + * @param indexFormatVersion the index format version for the lucene query. + */ + private LuceneQueryBuilder(QueryRootNode root, + SessionImpl session, + ItemStateManager sharedItemMgr, + HierarchyManager hmgr, + NamespaceMappings nsMappings, + Analyzer analyzer, + PropertyTypeRegistry propReg, + SynonymProvider synonymProvider, + IndexFormatVersion indexFormatVersion, + PerQueryCache cache) { + this.root = root; + this.session = session; + this.sharedItemMgr = sharedItemMgr; + this.hmgr = hmgr; + this.nsMappings = nsMappings; + this.analyzer = analyzer; + this.propRegistry = propReg; + this.synonymProvider = synonymProvider; + this.indexFormatVersion = indexFormatVersion; + this.cache = cache; + + this.resolver = NamePathResolverImpl.create(nsMappings); + } + + /** + * Creates a lucene {@link org.apache.lucene.search.Query} tree from an + * abstract query tree. + * + * @param root the root node of the abstract query tree. + * @param session of the user executing the query. + * @param sharedItemMgr the shared item state manager of the workspace. + * @param nsMappings namespace resolver for internal prefixes. + * @param analyzer for parsing the query statement of the contains + * function. + * @param propReg the property type registry to lookup type + * information. + * @param synonymProvider the synonym provider or null if node + * is configured. + * @param indexFormatVersion the index format version to be used + * @return the lucene query tree. + * @throws RepositoryException if an error occurs during the translation. + */ + public static Query createQuery(QueryRootNode root, + SessionImpl session, + ItemStateManager sharedItemMgr, + NamespaceMappings nsMappings, + Analyzer analyzer, + PropertyTypeRegistry propReg, + SynonymProvider synonymProvider, + IndexFormatVersion indexFormatVersion, + PerQueryCache cache) + throws RepositoryException { + HierarchyManager hmgr = new HierarchyManagerImpl( + RepositoryImpl.ROOT_NODE_ID, sharedItemMgr); + LuceneQueryBuilder builder = new LuceneQueryBuilder( + root, session, sharedItemMgr, hmgr, nsMappings, + analyzer, propReg, synonymProvider, indexFormatVersion, + cache); + + Query q = builder.createLuceneQuery(); + if (builder.exceptions.size() > 0) { + StringBuffer msg = new StringBuffer(); + for (Exception exception : builder.exceptions) { + msg.append(exception.toString()).append('\n'); + } + throw new RepositoryException("Exception building query: " + msg.toString()); + } + return q; + } + + /** + * Starts the tree traversal and returns the lucene + * {@link org.apache.lucene.search.Query}. + * + * @return the lucene Query. + * @throws RepositoryException if an error occurs while building the lucene + * query. + */ + private Query createLuceneQuery() throws RepositoryException { + return (Query) root.accept(this, null); + } + + //---------------------< QueryNodeVisitor interface >----------------------- + + public Object visit(QueryRootNode node, Object data) throws RepositoryException { + BooleanQuery root = new BooleanQuery(); + + Query wrapped = root; + if (node.getLocationNode() != null) { + wrapped = (Query) node.getLocationNode().accept(this, root); + } + + return wrapped; + } + + public Object visit(OrQueryNode node, Object data) throws RepositoryException { + BooleanQuery orQuery = new BooleanQuery(); + Object[] result = node.acceptOperands(this, null); + for (Object aResult : result) { + Query operand = (Query) aResult; + if (operand instanceof BooleanQuery) { + // check if the clauses are all optional, then + // we can collapse into the the enclosing orQuery + boolean hasNonOptional = false; + for (BooleanClause clause : ((BooleanQuery) operand).getClauses()) { + if (clause.isProhibited() || clause.isRequired()) { + hasNonOptional = true; + break; + } + } + if (hasNonOptional) { + // cannot collapse + orQuery.add(operand, Occur.SHOULD); + } else { + // collapse + for (BooleanClause clause : ((BooleanQuery) operand).getClauses()) { + orQuery.add(clause); + } + } + } else { + orQuery.add(operand, Occur.SHOULD); + } + } + return orQuery; + } + + public Object visit(AndQueryNode node, Object data) throws RepositoryException { + Object[] result = node.acceptOperands(this, null); + if (result.length == 0) { + return null; + } + BooleanQuery andQuery = new BooleanQuery(); + for (Object aResult : result) { + Query operand = (Query) aResult; + andQuery.add(operand, Occur.MUST); + } + return andQuery; + } + + public Object visit(NotQueryNode node, Object data) throws RepositoryException { + Object[] result = node.acceptOperands(this, null); + if (result.length == 0) { + return data; + } + // join the results + BooleanQuery b = new BooleanQuery(); + for (Object aResult : result) { + b.add((Query) aResult, Occur.SHOULD); + } + // negate + return new NotQuery(b); + } + + public Object visit(ExactQueryNode node, Object data) { + String field = ""; + String value = ""; + try { + field = resolver.getJCRName(node.getPropertyName()); + value = resolver.getJCRName(node.getValue()); + } catch (NamespaceException e) { + // will never happen, prefixes are created when unknown + } + return new JackrabbitTermQuery(new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value))); + } + + public Object visit(NodeTypeQueryNode node, Object data) { + + List terms = new ArrayList(); + try { + String mixinTypesField = resolver.getJCRName(NameConstants.JCR_MIXINTYPES); + String primaryTypeField = resolver.getJCRName(NameConstants.JCR_PRIMARYTYPE); + + NodeTypeManager ntMgr = session.getWorkspace().getNodeTypeManager(); + NodeType base = ntMgr.getNodeType(session.getJCRName(node.getValue())); + + if (base.isMixin()) { + // search for nodes where jcr:mixinTypes is set to this mixin + Term t = new Term(FieldNames.PROPERTIES, + FieldNames.createNamedValue(mixinTypesField, + resolver.getJCRName(node.getValue()))); + terms.add(t); + } else { + // search for nodes where jcr:primaryType is set to this type + Term t = new Term(FieldNames.PROPERTIES, + FieldNames.createNamedValue(primaryTypeField, + resolver.getJCRName(node.getValue()))); + terms.add(t); + } + + // now search for all node types that are derived from base + NodeTypeIterator allTypes = ntMgr.getAllNodeTypes(); + while (allTypes.hasNext()) { + NodeType nt = allTypes.nextNodeType(); + NodeType[] superTypes = nt.getSupertypes(); + if (Arrays.asList(superTypes).contains(base)) { + Name n = session.getQName(nt.getName()); + String ntName = nsMappings.translateName(n); + Term t; + if (nt.isMixin()) { + // search on jcr:mixinTypes + t = new Term(FieldNames.PROPERTIES, + FieldNames.createNamedValue(mixinTypesField, ntName)); + } else { + // search on jcr:primaryType + t = new Term(FieldNames.PROPERTIES, + FieldNames.createNamedValue(primaryTypeField, ntName)); + } + terms.add(t); + } + } + } catch (NameException e) { + exceptions.add(e); + } catch (RepositoryException e) { + exceptions.add(e); + } + if (terms.size() == 0) { + // exception occured + return new BooleanQuery(); + } else if (terms.size() == 1) { + return new JackrabbitTermQuery(terms.get(0)); + } else { + BooleanQuery b = new BooleanQuery(); + for (Term term : terms) { + b.add(new JackrabbitTermQuery(term), Occur.SHOULD); + } + return b; + } + } + + public Object visit(TextsearchQueryNode node, Object data) { + try { + Path relPath = node.getRelativePath(); + String fieldname; + if (relPath == null || !node.getReferencesProperty()) { + // fulltext on node + fieldname = FieldNames.FULLTEXT; + } else { + // final path element is a property name + Name propName = relPath.getName(); + StringBuffer tmp = new StringBuffer(); + tmp.append(nsMappings.getPrefix(propName.getNamespaceURI())); + tmp.append(":").append(FieldNames.FULLTEXT_PREFIX); + tmp.append(propName.getLocalName()); + fieldname = tmp.toString(); + } + QueryParser parser = new JackrabbitQueryParser( + fieldname, analyzer, synonymProvider, cache); + Query context = parser.parse(node.getQuery()); + if (relPath != null && (!node.getReferencesProperty() || relPath.getLength() > 1)) { + // text search on some child axis + Path.Element[] elements = relPath.getElements(); + for (int i = elements.length - 1; i >= 0; i--) { + Name name = null; + if (!elements[i].getName().equals(RelationQueryNode.STAR_NAME_TEST)) { + name = elements[i].getName(); + } + // join text search with name test + // if path references property that's elements.length - 2 + // if path references node that's elements.length - 1 + if (name != null + && ((node.getReferencesProperty() && i == elements.length - 2) + || (!node.getReferencesProperty() && i == elements.length - 1))) { + Query q = new NameQuery(name, indexFormatVersion, nsMappings); + BooleanQuery and = new BooleanQuery(); + and.add(q, Occur.MUST); + and.add(context, Occur.MUST); + context = and; + } else if ((node.getReferencesProperty() && i < elements.length - 2) + || (!node.getReferencesProperty() && i < elements.length - 1)) { + // otherwise do a parent axis step + context = new ParentAxisQuery(context, name, + indexFormatVersion, nsMappings); + } + } + // finally select parent + context = new ParentAxisQuery(context, null, + indexFormatVersion, nsMappings); + } + return context; + } catch (NamespaceException e) { + exceptions.add(e); + } catch (ParseException e) { + exceptions.add(e); + } + return null; + } + + public Object visit(PathQueryNode node, Object data) throws RepositoryException { + Query context = null; + LocationStepQueryNode[] steps = node.getPathSteps(); + if (steps.length > 0) { + if (node.isAbsolute() && !steps[0].getIncludeDescendants()) { + // eat up first step + Name nameTest = steps[0].getNameTest(); + if (nameTest == null) { + // this is equivalent to the root node + context = new JackrabbitTermQuery(new Term(FieldNames.PARENT, "")); + } else if (nameTest.getLocalName().length() == 0) { + // root node + context = new JackrabbitTermQuery(new Term(FieldNames.PARENT, "")); + } else { + // then this is a node != the root node + // will never match anything! + BooleanQuery and = new BooleanQuery(); + and.add(new JackrabbitTermQuery(new Term(FieldNames.PARENT, "")), Occur.MUST); + and.add(new NameQuery(nameTest, indexFormatVersion, nsMappings), Occur.MUST); + context = and; + } + // apply predicates + Object[] predicates = steps[0].acceptOperands(this, context); + BooleanQuery andQuery = new BooleanQuery(); + for (Object predicate : predicates) { + andQuery.add((Query) predicate, Occur.MUST); + } + if (andQuery.clauses().size() > 0) { + andQuery.add(context, Occur.MUST); + context = andQuery; + } + + LocationStepQueryNode[] tmp = new LocationStepQueryNode[steps.length - 1]; + System.arraycopy(steps, 1, tmp, 0, steps.length - 1); + steps = tmp; + } else { + // path is 1) relative or 2) descendant-or-self + // use root node as context + context = new JackrabbitTermQuery(new Term(FieldNames.PARENT, "")); + } + } else { + exceptions.add(new InvalidQueryException("Number of location steps must be > 0")); + } + // loop over steps + for (LocationStepQueryNode step : steps) { + context = (Query) step.accept(this, context); + } + if (data instanceof BooleanQuery) { + BooleanQuery constraint = (BooleanQuery) data; + if (constraint.getClauses().length > 0) { + constraint.add(context, Occur.MUST); + context = constraint; + } + } + return context; + } + + public Object visit(LocationStepQueryNode node, Object data) throws RepositoryException { + Query context = (Query) data; + BooleanQuery andQuery = new BooleanQuery(); + + if (context == null) { + exceptions.add(new IllegalArgumentException("Unsupported query")); + } + + // predicate on step? + Object[] predicates = node.acceptOperands(this, data); + for (Object predicate : predicates) { + andQuery.add((Query) predicate, Occur.MUST); + } + + // check for position predicate + QueryNode[] pred = node.getPredicates(); + for (QueryNode aPred : pred) { + if (aPred.getType() == QueryNode.TYPE_RELATION) { + RelationQueryNode pos = (RelationQueryNode) aPred; + if (pos.getValueType() == QueryConstants.TYPE_POSITION) { + node.setIndex(pos.getPositionValue()); + } + } + } + + NameQuery nameTest = null; + if (node.getNameTest() != null) { + if (node.getNameTest().equals(PARENT_ELEMENT_NAME)) { + andQuery.add(new ParentAxisQuery(context, null, indexFormatVersion, nsMappings), Occur.MUST); + return andQuery; + } + nameTest = new NameQuery(node.getNameTest(), indexFormatVersion, nsMappings); + } + + if (node.getIncludeDescendants()) { + if (nameTest != null) { + andQuery.add(new DescendantSelfAxisQuery(context, nameTest, false), Occur.MUST); + } else { + // descendant-or-self with nametest=* + if (predicates.length > 0) { + // if we have a predicate attached, the condition acts as + // the sub query. + + // only use descendant axis if path is not //* + // otherwise the query for the predicate can be used itself + PathQueryNode pathNode = (PathQueryNode) node.getParent(); + if (pathNode.getPathSteps()[0] != node) { + Query subQuery = new DescendantSelfAxisQuery(context, andQuery, false); + andQuery = new BooleanQuery(); + andQuery.add(subQuery, Occur.MUST); + } + } else { + // todo this will traverse the whole index, optimize! + // only use descendant axis if path is not //* + PathQueryNode pathNode = (PathQueryNode) node.getParent(); + if (pathNode.getPathSteps()[0] != node) { + if (node.getIndex() == LocationStepQueryNode.NONE) { + context = new DescendantSelfAxisQuery(context, false); + andQuery.add(context, Occur.MUST); + } else { + context = new DescendantSelfAxisQuery(context, true); + andQuery.add(new ChildAxisQuery(sharedItemMgr, + context, null, node.getIndex(), + indexFormatVersion, nsMappings), Occur.MUST); + } + } else { + andQuery.add(new MatchAllDocsQuery(), Occur.MUST); + } + } + } + } else { + // name test + if (nameTest != null) { + andQuery.add(new ChildAxisQuery(sharedItemMgr, context, + nameTest.getName(), node.getIndex(), indexFormatVersion, + nsMappings), Occur.MUST); + } else { + // select child nodes + andQuery.add(new ChildAxisQuery(sharedItemMgr, context, null, + node.getIndex(), indexFormatVersion, nsMappings), + Occur.MUST); + } + } + + return andQuery; + } + + public Object visit(DerefQueryNode node, Object data) throws RepositoryException { + Query context = (Query) data; + if (context == null) { + exceptions.add(new IllegalArgumentException("Unsupported query")); + } + + try { + String refProperty = resolver.getJCRName(node.getRefProperty()); + + if (node.getIncludeDescendants()) { + Query refPropQuery = Util.createMatchAllQuery(refProperty, indexFormatVersion, cache); + context = new DescendantSelfAxisQuery(context, refPropQuery, false); + } + + context = new DerefQuery(context, refProperty, node.getNameTest(), + indexFormatVersion, nsMappings); + + // attach predicates + Object[] predicates = node.acceptOperands(this, data); + if (predicates.length > 0) { + BooleanQuery andQuery = new BooleanQuery(); + for (Object predicate : predicates) { + andQuery.add((Query) predicate, Occur.MUST); + } + andQuery.add(context, Occur.MUST); + context = andQuery; + } + + } catch (NamespaceException e) { + // should never happen + exceptions.add(e); + } + + return context; + } + + public Object visit(RelationQueryNode node, Object data) throws RepositoryException { + PathQueryNode relPath = node.getRelativePath(); + if (relPath == null + && node.getOperation() != QueryConstants.OPERATION_SIMILAR + && node.getOperation() != QueryConstants.OPERATION_SPELLCHECK) { + exceptions.add(new InvalidQueryException("@* not supported in predicate")); + return data; + } + LocationStepQueryNode[] steps = relPath.getPathSteps(); + Name propertyName; + if (node.getOperation() == QueryConstants.OPERATION_SIMILAR) { + // this is a bit ugly: + // use the name of a dummy property because relPath actually + // references a property. whereas the relPath of the similar + // operation references a node + propertyName = NameConstants.JCR_PRIMARYTYPE; + } else { + propertyName = steps[steps.length - 1].getNameTest(); + } + + Query query; + String[] stringValues = new String[1]; + switch (node.getValueType()) { + case 0: + // not set: either IS NULL or IS NOT NULL + break; + case QueryConstants.TYPE_DATE: + stringValues[0] = DateField.dateToString(node.getDateValue()); + break; + case QueryConstants.TYPE_DOUBLE: + stringValues[0] = DoubleField.doubleToString(node.getDoubleValue()); + break; + case QueryConstants.TYPE_LONG: + stringValues[0] = LongField.longToString(node.getLongValue()); + break; + case QueryConstants.TYPE_STRING: + if (node.getOperation() == QueryConstants.OPERATION_EQ_GENERAL + || node.getOperation() == QueryConstants.OPERATION_EQ_VALUE + || node.getOperation() == QueryConstants.OPERATION_NE_GENERAL + || node.getOperation() == QueryConstants.OPERATION_NE_VALUE) { + // only use coercing on non-range operations + stringValues = getStringValues(propertyName, node.getStringValue()); + } else { + stringValues[0] = node.getStringValue(); + } + break; + case QueryConstants.TYPE_POSITION: + // ignore position. is handled in the location step + return null; + default: + throw new IllegalArgumentException("Unknown relation type: " + + node.getValueType()); + } + + // get property transformation + final int[] transform = new int[]{TransformConstants.TRANSFORM_NONE}; + node.acceptOperands(new DefaultQueryNodeVisitor() { + public Object visit(PropertyFunctionQueryNode node, Object data) { + if (node.getFunctionName().equals(PropertyFunctionQueryNode.LOWER_CASE)) { + transform[0] = TransformConstants.TRANSFORM_LOWER_CASE; + } else if (node.getFunctionName().equals(PropertyFunctionQueryNode.UPPER_CASE)) { + transform[0] = TransformConstants.TRANSFORM_UPPER_CASE; + } + return data; + } + }, null); + + String field = ""; + try { + field = resolver.getJCRName(propertyName); + } catch (NamespaceException e) { + // should never happen + exceptions.add(e); + } + + // support for fn:name() + if (propertyName.equals(FN_NAME)) { + if (node.getValueType() != QueryConstants.TYPE_STRING) { + exceptions.add(new InvalidQueryException("Name function can " + + "only be used in conjunction with a string literal")); + return data; + } + if (node.getOperation() == QueryConstants.OPERATION_EQ_VALUE + || node.getOperation() == QueryConstants.OPERATION_EQ_GENERAL) { + // check if string literal is a valid XML Name + if (XMLChar.isValidName(node.getStringValue())) { + // parse string literal as JCR Name + try { + Name n = session.getQName(ISO9075.decode(node.getStringValue())); + query = new NameQuery(n, indexFormatVersion, nsMappings); + } catch (NameException e) { + exceptions.add(e); + return data; + } catch (NamespaceException e) { + exceptions.add(e); + return data; + } + } else { + // will never match -> create dummy query + query = new BooleanQuery(); + } + } else if (node.getOperation() == QueryConstants.OPERATION_LIKE) { + // the like operation always has one string value. + // no coercing, see above + if (stringValues[0].equals("%")) { + query = new org.apache.lucene.search.MatchAllDocsQuery(); + } else { + query = new WildcardNameQuery(stringValues[0], + transform[0], session, nsMappings, cache); + } + } else { + exceptions.add(new InvalidQueryException("Name function can " + + "only be used in conjunction with the following operators: equals, like")); + return data; + } + } else { + switch (node.getOperation()) { + case QueryConstants.OPERATION_EQ_VALUE: // = + case QueryConstants.OPERATION_EQ_GENERAL: + BooleanQuery or = new BooleanQuery(); + for (String value : stringValues) { + Term t = new Term(FieldNames.PROPERTIES, + FieldNames.createNamedValue(field, value)); + Query q; + if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE) { + q = new CaseTermQuery.Upper(t); + } else + if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE) { + q = new CaseTermQuery.Lower(t); + } else { + q = new JackrabbitTermQuery(t); + } + or.add(q, Occur.SHOULD); + } + query = or; + if (node.getOperation() == QueryConstants.OPERATION_EQ_VALUE) { + query = createSingleValueConstraint(or, field); + } + break; + case QueryConstants.OPERATION_GE_VALUE: // >= + case QueryConstants.OPERATION_GE_GENERAL: + or = new BooleanQuery(); + for (String value : stringValues) { + Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value)); + Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, "\uFFFF")); + or.add(new RangeQuery(lower, upper, true, transform[0], cache), Occur.SHOULD); + } + query = or; + if (node.getOperation() == QueryConstants.OPERATION_GE_VALUE) { + query = createSingleValueConstraint(or, field); + } + break; + case QueryConstants.OPERATION_GT_VALUE: // > + case QueryConstants.OPERATION_GT_GENERAL: + or = new BooleanQuery(); + for (String value : stringValues) { + Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value)); + Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, "\uFFFF")); + or.add(new RangeQuery(lower, upper, false, transform[0], cache), Occur.SHOULD); + } + query = or; + if (node.getOperation() == QueryConstants.OPERATION_GT_VALUE) { + query = createSingleValueConstraint(or, field); + } + break; + case QueryConstants.OPERATION_LE_VALUE: // <= + case QueryConstants.OPERATION_LE_GENERAL: // <= + or = new BooleanQuery(); + for (String value : stringValues) { + Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, "")); + Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value)); + or.add(new RangeQuery(lower, upper, true, transform[0], cache), Occur.SHOULD); + } + query = or; + if (node.getOperation() == QueryConstants.OPERATION_LE_VALUE) { + query = createSingleValueConstraint(query, field); + } + break; + case QueryConstants.OPERATION_LIKE: // LIKE + // the like operation always has one string value. + // no coercing, see above + if (stringValues[0].equals("%")) { + query = Util.createMatchAllQuery(field, indexFormatVersion, cache); + } else { + query = new WildcardQuery(FieldNames.PROPERTIES, field, stringValues[0], transform[0], cache); + } + break; + case QueryConstants.OPERATION_LT_VALUE: // < + case QueryConstants.OPERATION_LT_GENERAL: + or = new BooleanQuery(); + for (String value : stringValues) { + Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, "")); + Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value)); + or.add(new RangeQuery(lower, upper, false, transform[0], cache), Occur.SHOULD); + } + query = or; + if (node.getOperation() == QueryConstants.OPERATION_LT_VALUE) { + query = createSingleValueConstraint(or, field); + } + break; + case QueryConstants.OPERATION_NE_VALUE: // != + // match nodes with property 'field' that includes svp and mvp + BooleanQuery notQuery = new BooleanQuery(); + notQuery.add(Util.createMatchAllQuery(field, indexFormatVersion, cache), Occur.SHOULD); + // exclude all nodes where 'field' has the term in question + for (String value : stringValues) { + Term t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value)); + Query q; + if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE) { + q = new CaseTermQuery.Upper(t); + } else + if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE) { + q = new CaseTermQuery.Lower(t); + } else { + q = new JackrabbitTermQuery(t); + } + notQuery.add(q, Occur.MUST_NOT); + } + // and exclude all nodes where 'field' is multi valued + notQuery.add(new JackrabbitTermQuery(new Term(FieldNames.MVP, field)), Occur.MUST_NOT); + query = notQuery; + break; + case QueryConstants.OPERATION_NE_GENERAL: // != + // that's: + // all nodes with property 'field' + // minus the nodes that have a single property 'field' that is + // not equal to term in question + // minus the nodes that have a multi-valued property 'field' and + // all values are equal to term in question + notQuery = new BooleanQuery(); + notQuery.add(Util.createMatchAllQuery(field, indexFormatVersion, cache), Occur.SHOULD); + for (String value : stringValues) { + // exclude the nodes that have the term and are single valued + Term t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value)); + Query svp = new NotQuery(new JackrabbitTermQuery(new Term(FieldNames.MVP, field))); + BooleanQuery and = new BooleanQuery(); + Query q; + if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE) { + q = new CaseTermQuery.Upper(t); + } else + if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE) { + q = new CaseTermQuery.Lower(t); + } else { + q = new JackrabbitTermQuery(t); + } + and.add(q, Occur.MUST); + and.add(svp, Occur.MUST); + notQuery.add(and, Occur.MUST_NOT); + } + // todo above also excludes multi-valued properties that contain + // multiple instances of only stringValues. e.g. text={foo, foo} + query = notQuery; + break; + case QueryConstants.OPERATION_NULL: + query = new NotQuery(Util.createMatchAllQuery(field, indexFormatVersion, cache)); + break; + case QueryConstants.OPERATION_SIMILAR: + try { + NodeId id = hmgr.resolveNodePath(session.getQPath(node.getStringValue())); + if (id != null) { + query = new SimilarityQuery(id.toString(), analyzer); + } else { + query = new BooleanQuery(); + } + } catch (Exception e) { + exceptions.add(e); + query = new BooleanQuery(); + } + break; + case QueryConstants.OPERATION_NOT_NULL: + query = Util.createMatchAllQuery(field, indexFormatVersion, cache); + break; + case QueryConstants.OPERATION_SPELLCHECK: + query = Util.createMatchAllQuery(field, indexFormatVersion, cache); + break; + default: + throw new IllegalArgumentException("Unknown relation operation: " + + node.getOperation()); + } + } + + if (steps.length > 1) { + // child axis in relation + // elements.length - 1 = property name + // elements.length - 2 = last child axis name test + boolean selectParent = true; + for (int i = steps.length - 2; i >= 0; i--) { + LocationStepQueryNode step = steps[i]; + Name name = steps[i].getNameTest(); + if (i == steps.length - 2) { + if (step instanceof DerefQueryNode) { + query = createPredicateDeref(query, (DerefQueryNode) step, data); + if (steps.length == 2) { + selectParent = false; + } + } else if (step != null) { + // join name test with property query if there is one + if (name != null) { + if (!name.equals(PARENT_ELEMENT_NAME)) { + Query nameTest = new NameQuery(name, + indexFormatVersion, nsMappings); + BooleanQuery and = new BooleanQuery(); + and.add(query, Occur.MUST); + and.add(nameTest, Occur.MUST); + + query = and; + } else { + // If we're searching the parent, we want to return the child axis, + // not the parent because this is part of the predicate. For instance, + // if the query is //child[../base], this part of the code is operating + // on the "../base" portion. So we want to return all the child nodes + // of "base", which will then be matched against the non predicate part. + query = new ChildAxisQuery(sharedItemMgr, + query, + null, + indexFormatVersion, + nsMappings); + selectParent = false; + } + } else { + // otherwise the query can be used as is + } + } + } else if (name != null && name.equals(PARENT_ELEMENT_NAME)) { + // We need to select one of the properties if we haven't already. + if (selectParent) { + query = new ParentAxisQuery(query, null, + indexFormatVersion, nsMappings); + + selectParent = false; + } + + // See the note above on searching parents + query = new ChildAxisQuery(sharedItemMgr, + query, + null, + indexFormatVersion, + nsMappings); + } else { + if (step != null) { + query = new ParentAxisQuery(query, name, indexFormatVersion, nsMappings); + } else { + throw new UnsupportedOperationException(); + } + } + } + // finally select the parent of the selected nodes + if (selectParent) { + query = new ParentAxisQuery(query, null, indexFormatVersion, nsMappings); + } + } + + return query; + } + + public Query createPredicateDeref(Query subQuery, DerefQueryNode node, Object data) throws RepositoryException { + Query context = (Query) data; + + if (context == null) { + exceptions.add(new IllegalArgumentException("Unsupported query")); + } + + try { + String refProperty = resolver.getJCRName(node.getRefProperty()); + + context = new PredicateDerefQuery(subQuery, refProperty, node.getNameTest(), + indexFormatVersion, nsMappings); + + // attach predicates + Object[] predicates = node.acceptOperands(this, data); + if (predicates.length > 0) { + BooleanQuery andQuery = new BooleanQuery(); + for (Object predicate : predicates) { + andQuery.add((Query) predicate, Occur.MUST); + } + andQuery.add(context, Occur.MUST); + context = andQuery; + } + + } catch (NamespaceException e) { + // should never happen + exceptions.add(e); + } + + return context; + } + + public Object visit(OrderQueryNode node, Object data) { + return data; + } + + public Object visit(PropertyFunctionQueryNode node, Object data) { + return data; + } + + //---------------------------< internal >----------------------------------- + + /** + * Wraps a constraint query around q that limits the nodes to + * those where propName is the name of a single value property + * on the node instance. + * + * @param q the query to wrap. + * @param propName the name of a property that only has one value. + * @return the wrapped query q. + */ + private Query createSingleValueConstraint(Query q, String propName) { + // get nodes with multi-values in propName + Query mvp = new JackrabbitTermQuery(new Term(FieldNames.MVP, propName)); + // now negate, that gives the nodes that have propName as single + // values but also all others + Query svp = new NotQuery(mvp); + // now join the two, which will result in those nodes where propName + // only contains a single value. This works because q already restricts + // the result to those nodes that have a property propName + BooleanQuery and = new BooleanQuery(); + and.add(q, Occur.MUST); + and.add(svp, Occur.MUST); + return and; + } + + /** + * Returns an array of String values to be used as a term to lookup the search index + * for a String literal of a certain property name. This method + * will lookup the propertyName in the node type registry + * trying to find out the {@link javax.jcr.PropertyType}s. + * If no property type is found looking up node type information, this + * method will guess the property type. + * + * @param propertyName the name of the property in the relation. + * @param literal the String literal in the relation. + * @return the String values to use as term for the query. + */ + private String[] getStringValues(Name propertyName, String literal) { + PropertyTypeRegistry.TypeMapping[] types = propRegistry.getPropertyTypes(propertyName); + List values = new ArrayList(); + for (PropertyTypeRegistry.TypeMapping type : types) { + switch (type.type) { + case PropertyType.NAME: + // try to translate name + try { + Name n = session.getQName(literal); + values.add(nsMappings.translateName(n)); + log.debug("Coerced " + literal + " into NAME."); + } catch (NameException e) { + log.debug("Unable to coerce '" + literal + "' into a NAME: " + e.toString()); + } catch (NamespaceException e) { + log.debug("Unable to coerce '" + literal + "' into a NAME: " + e.toString()); + } + break; + case PropertyType.PATH: + // try to translate path + try { + Path p = session.getQPath(literal); + values.add(resolver.getJCRPath(p)); + log.debug("Coerced " + literal + " into PATH."); + } catch (NameException e) { + log.debug("Unable to coerce '" + literal + "' into a PATH: " + e.toString()); + } catch (NamespaceException e) { + log.debug("Unable to coerce '" + literal + "' into a PATH: " + e.toString()); + } + break; + case PropertyType.DATE: + // try to parse date + Calendar c = ISO8601.parse(literal); + if (c != null) { + values.add(DateField.timeToString(c.getTimeInMillis())); + log.debug("Coerced " + literal + " into DATE."); + } else { + log.debug("Unable to coerce '" + literal + "' into a DATE."); + } + break; + case PropertyType.DOUBLE: + // try to parse double + try { + double d = Double.parseDouble(literal); + values.add(DoubleField.doubleToString(d)); + log.debug("Coerced " + literal + " into DOUBLE."); + } catch (NumberFormatException e) { + log.debug("Unable to coerce '" + literal + "' into a DOUBLE: " + e.toString()); + } + break; + case PropertyType.LONG: + // try to parse long + try { + long l = Long.parseLong(literal); + values.add(LongField.longToString(l)); + log.debug("Coerced " + literal + " into LONG."); + } catch (NumberFormatException e) { + log.debug("Unable to coerce '" + literal + "' into a LONG: " + e.toString()); + } + break; + case PropertyType.DECIMAL: + // try to parse decimal + try { + BigDecimal d = new BigDecimal(literal); + values.add(DecimalField.decimalToString(d)); + log.debug("Coerced " + literal + " into DECIMAL."); + } catch (NumberFormatException e) { + log.debug("Unable to coerce '" + literal + "' into a DECIMAL: " + e.toString()); + } + break; + case PropertyType.URI: + // fall through... TODO: correct? + case PropertyType.STRING: + values.add(literal); + log.debug("Using literal " + literal + " as is."); + break; + } + } + if (values.size() == 0) { + // use literal as is then try to guess other types + values.add(literal); + + // try to guess property type + if (literal.indexOf('/') > -1) { + // might be a path + try { + values.add(resolver.getJCRPath(session.getQPath(literal))); + log.debug("Coerced " + literal + " into PATH."); + } catch (Exception e) { + // not a path + } + } + if (XMLChar.isValidName(literal)) { + // might be a name + try { + Name n = session.getQName(literal); + values.add(nsMappings.translateName(n)); + log.debug("Coerced " + literal + " into NAME."); + } catch (Exception e) { + // not a name + } + } + if (literal.indexOf(':') > -1) { + // is it a date? + Calendar c = ISO8601.parse(literal); + if (c != null) { + values.add(DateField.timeToString(c.getTimeInMillis())); + log.debug("Coerced " + literal + " into DATE."); + } + } else { + // long or double are possible at this point + try { + values.add(LongField.longToString(Long.parseLong(literal))); + log.debug("Coerced " + literal + " into LONG."); + } catch (NumberFormatException e) { + // not a long + // try double + try { + values.add(DoubleField.doubleToString(Double.parseDouble(literal))); + log.debug("Coerced " + literal + " into DOUBLE."); + } catch (NumberFormatException e1) { + // not a double + } + } + } + } + // if still no values use literal as is + if (values.size() == 0) { + values.add(literal); + log.debug("Using literal " + literal + " as is."); + } + return values.toArray(new String[values.size()]); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryFactory.java new file mode 100644 index 00000000000..fb8fb21c9fd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryFactory.java @@ -0,0 +1,781 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +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.STRING; +import static javax.jcr.PropertyType.UNDEFINED; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LIKE; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO; +import static org.apache.jackrabbit.core.query.lucene.FieldNames.LOCAL_NAME; +import static org.apache.jackrabbit.core.query.lucene.FieldNames.MVP; +import static org.apache.jackrabbit.core.query.lucene.FieldNames.NAMESPACE_URI; +import static org.apache.jackrabbit.core.query.lucene.FieldNames.PARENT; +import static org.apache.jackrabbit.core.query.lucene.FieldNames.PROPERTIES; +import static org.apache.jackrabbit.core.query.lucene.FieldNames.UUID; +import static org.apache.jackrabbit.core.query.lucene.TransformConstants.TRANSFORM_LOWER_CASE; +import static org.apache.jackrabbit.core.query.lucene.TransformConstants.TRANSFORM_NONE; +import static org.apache.jackrabbit.core.query.lucene.TransformConstants.TRANSFORM_UPPER_CASE; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_PRIMARYTYPE; +import static org.apache.lucene.search.BooleanClause.Occur.MUST; +import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT; +import static org.apache.lucene.search.BooleanClause.Occur.SHOULD; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Row; +import javax.jcr.query.qom.And; +import javax.jcr.query.qom.ChildNode; +import javax.jcr.query.qom.Comparison; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.DescendantNode; +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.FullTextSearch; +import javax.jcr.query.qom.FullTextSearchScore; +import javax.jcr.query.qom.Length; +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.PropertyExistence; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.SameNode; +import javax.jcr.query.qom.Selector; +import javax.jcr.query.qom.StaticOperand; +import javax.jcr.query.qom.UpperCase; + +import org.apache.jackrabbit.commons.predicate.Predicate; +import org.apache.jackrabbit.commons.predicate.Predicates; +import org.apache.jackrabbit.commons.predicate.RowPredicate; +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.query.lucene.join.SelectorRow; +import org.apache.jackrabbit.core.query.lucene.join.ValueComparator; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.query.qom.FullTextSearchImpl; +import org.apache.jackrabbit.spi.commons.query.qom.PropertyExistenceImpl; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryParser.ParseException; +import org.apache.lucene.queryParser.QueryParser; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Sort; + +/** + * Factory that creates Lucene queries from QOM elements. + */ +public class LuceneQueryFactory { + + /** + * Session of the user executing this query + */ + protected final SessionImpl session; + + /** + * Node type manager + */ + protected final NodeTypeManager ntManager; + + /** Lucene search index */ + protected final SearchIndex index; + + /** + * Namespace mappings to internal prefixes + */ + protected final NamespaceMappings nsMappings; + + /** + * NamePathResolver to map namespace mappings to internal prefixes + */ + protected final NamePathResolver npResolver; + + /** Operand evaluator */ + protected final OperandEvaluator evaluator; + + protected final String mixinTypesField; + + protected final String primaryTypeField; + + private final PerQueryCache cache = new PerQueryCache(); + + /** + * Creates a new lucene query factory. + * + * @param session the session that executes the query. + * @param index the search index + * @param bindVariables the bind variable values of the query + */ + public LuceneQueryFactory( + SessionImpl session, SearchIndex index, + Map bindVariables) throws RepositoryException { + this.session = session; + this.ntManager = session.getWorkspace().getNodeTypeManager(); + this.index = index; + this.nsMappings = index.getNamespaceMappings(); + this.npResolver = NamePathResolverImpl.create(nsMappings); + this.evaluator = + new OperandEvaluator(session.getValueFactory(), bindVariables); + this.mixinTypesField = nsMappings.translateName(JCR_MIXINTYPES); + this.primaryTypeField = nsMappings.translateName(JCR_PRIMARYTYPE); + } + + /** + * @param columns + * @param selector + * @param constraint + * @param externalSort + * if true it means that the lqf should just let the + * QueryEngine take care of sorting and applying applying offset + * and limit constraints + * @param offsetIn + * used in pagination + * @param limitIn + * used in pagination + * @return a list of rows + * @throws RepositoryException + * @throws IOException + */ + public List execute(Map columns, + Selector selector, Constraint constraint, Sort sort, + boolean externalSort, long offsetIn, long limitIn) + throws RepositoryException, IOException { + final IndexReader reader = index.getIndexReader(true); + final int offset = offsetIn < 0 ? 0 : (int) offsetIn; + final int limit = limitIn < 0 ? Integer.MAX_VALUE : (int) limitIn; + + QueryHits hits = null; + try { + JackrabbitIndexSearcher searcher = new JackrabbitIndexSearcher( + session, reader, index.getContext().getItemStateManager()); + searcher.setSimilarity(index.getSimilarity()); + + Predicate filter = Predicate.TRUE; + BooleanQuery query = new BooleanQuery(); + + QueryPair qp = new QueryPair(query); + + query.add(create(selector), MUST); + if (constraint != null) { + String name = selector.getSelectorName(); + NodeType type = + ntManager.getNodeType(selector.getNodeTypeName()); + filter = mapConstraintToQueryAndFilter(qp, + constraint, Collections.singletonMap(name, type), + searcher, reader); + } + + List rows = new ArrayList(); + + // TODO depending on the filters, we could push the offset info + // into the searcher + hits = searcher.evaluate(qp.mainQuery, sort, offset + limit); + int currentNode = 0; + int addedNodes = 0; + + ScoreNode node = hits.nextScoreNode(); + while (node != null) { + Row row = null; + try { + row = new SelectorRow(columns, evaluator, + selector.getSelectorName(), + session.getNodeById(node.getNodeId()), + node.getScore()); + } catch (ItemNotFoundException e) { + // skip the node + } + if (row != null && filter.evaluate(row)) { + if (externalSort) { + // return everything and not worry about sort + rows.add(row); + } else { + // apply limit and offset rules locally + if (currentNode >= offset + && currentNode - offset < limit) { + rows.add(row); + addedNodes++; + } + currentNode++; + // end the loop when going over the limit + if (addedNodes == limit) { + break; + } + } + } + node = hits.nextScoreNode(); + } + return rows; + } finally { + if (hits != null) { + hits.close(); + } + Util.closeOrRelease(reader); + } + } + + /** + * Creates a lucene query for the given QOM selector. + * + * @param selector the selector. + * @return a lucene query for the given selector. + * @throws RepositoryException if an error occurs while creating the query. + */ + public Query create(Selector selector) throws RepositoryException { + List terms = new ArrayList(); + + String name = selector.getNodeTypeName(); + NodeTypeIterator allTypes = ntManager.getAllNodeTypes(); + while (allTypes.hasNext()) { + NodeType nt = allTypes.nextNodeType(); + if (nt.isNodeType(name)) { + terms.add(createNodeTypeTerm(nt)); + } + } + + if (terms.size() == 1) { + return new JackrabbitTermQuery(terms.get(0)); + } else { + BooleanQuery b = new BooleanQuery(); + for (Term term : terms) { + b.add(new JackrabbitTermQuery(term), SHOULD); + } + return b; + } + } + + protected Term createNodeTypeTerm(NodeType type) throws RepositoryException { + String field; + if (type.isMixin()) { + // search for nodes where jcr:mixinTypes is set to this mixin + field = mixinTypesField; + } else { + // search for nodes where jcr:primaryType is set to this type + field = primaryTypeField; + } + String name = nsMappings.translateName(session.getQName(type.getName())); + return new Term(PROPERTIES, FieldNames.createNamedValue(field, name)); + } + + /** + * Creates a lucene query for the given QOM full text search. + * + * @param fts the full text search constraint. + * @return the lucene query for the given constraint. + * @throws RepositoryException if an error occurs while creating the query. + */ + public Query create(FullTextSearchImpl fts) throws RepositoryException { + String fieldname; + if (fts.getPropertyName() == null) { + // fulltext on node + fieldname = FieldNames.FULLTEXT; + } else { + // final path element is a property name + Name propName = fts.getPropertyQName(); + StringBuffer tmp = new StringBuffer(); + tmp.append(nsMappings.getPrefix(propName.getNamespaceURI())); + tmp.append(":").append(FieldNames.FULLTEXT_PREFIX); + tmp.append(propName.getLocalName()); + fieldname = tmp.toString(); + } + QueryParser parser = new JackrabbitQueryParser( + fieldname, index.getTextAnalyzer(), + index.getSynonymProvider(), cache); + try { + StaticOperand expr = fts.getFullTextSearchExpression(); + return parser.parse(evaluator.getValue(expr).getString()); + } catch (ParseException e) { + throw new RepositoryException(e); + } + } + + /** + * Creates a lucene query for the given QOM property existence constraint. + * + * @param prop the QOM constraint. + * @return the lucene query for the given constraint. + * @throws RepositoryException if an error occurs while creating the query. + */ + public Query create(PropertyExistenceImpl prop) throws RepositoryException { + String propName = npResolver.getJCRName(prop.getPropertyQName()); + return Util.createMatchAllQuery( + propName, index.getIndexFormatVersion(), cache); + } + + protected Predicate mapConstraintToQueryAndFilter( + QueryPair query, Constraint constraint, + Map selectorMap, + JackrabbitIndexSearcher searcher, IndexReader reader) + throws RepositoryException, IOException { + Predicate filter = Predicate.TRUE; + if (constraint instanceof And) { + And and = (And) constraint; + filter = mapConstraintToQueryAndFilter( + query, and.getConstraint1(), selectorMap, searcher, reader); + Predicate other = mapConstraintToQueryAndFilter( + query, and.getConstraint2(), selectorMap, searcher, reader); + if (filter == Predicate.TRUE) { + filter = other; + } else if (other != Predicate.TRUE) { + filter = Predicates.and(filter, other); + } + } else if (constraint instanceof Comparison) { + Comparison c = (Comparison) constraint; + Transform transform = new Transform(c.getOperand1()); + DynamicOperand left = transform.operand; + final String operator = c.getOperator(); + StaticOperand right = c.getOperand2(); + if (left instanceof Length + || left instanceof FullTextSearchScore + || (((!JCR_OPERATOR_EQUAL_TO.equals(operator) && !JCR_OPERATOR_LIKE + .equals(operator)) || transform.transform != TRANSFORM_NONE) && (left instanceof NodeName || left instanceof NodeLocalName))) { + try { + int type = PropertyType.UNDEFINED; + if (left instanceof Length) { + type = PropertyType.LONG; + } else if (left instanceof FullTextSearchScore) { + type = PropertyType.DOUBLE; + } + final DynamicOperand operand = c.getOperand1(); + final Value value = evaluator.getValue(right, type); + filter = new RowPredicate() { + @Override + protected boolean evaluate(Row row) + throws RepositoryException { + return new ValueComparator().evaluate( + operator, + evaluator.getValue(operand, row), value); + } + }; + } catch (ValueFormatException e) { + throw new InvalidQueryException(e); + } + } else { + Query cq = getComparisonQuery( + left, transform.transform, operator, right, selectorMap); + query.subQuery.add(cq, MUST); + } + } else if (constraint instanceof DescendantNode) { + final DescendantNode descendantNode = (DescendantNode) constraint; + Query context = getNodeIdQuery(UUID, descendantNode.getAncestorPath()); + query.mainQuery = new DescendantSelfAxisQuery(context, query.subQuery, false); + } else { + query.subQuery.add(create(constraint, selectorMap, searcher), MUST); + } + return filter; + } + + + protected Query create( + Constraint constraint, Map selectorMap, + JackrabbitIndexSearcher searcher) + throws RepositoryException, IOException { + if (constraint instanceof And) { + return getAndQuery((And) constraint, selectorMap, searcher); + } else if (constraint instanceof Or) { + return getOrQuery((Or) constraint, selectorMap, searcher); + } else if (constraint instanceof Not) { + return getNotQuery((Not) constraint, selectorMap, searcher); + } else if (constraint instanceof PropertyExistence) { + return getPropertyExistenceQuery((PropertyExistence) constraint); + } else if (constraint instanceof Comparison) { + Comparison c = (Comparison) constraint; + Transform left = new Transform(c.getOperand1()); + return getComparisonQuery( + left.operand, left.transform, c.getOperator(), + c.getOperand2(), selectorMap); + } else if (constraint instanceof FullTextSearch) { + return getFullTextSearchQuery((FullTextSearch) constraint); + } else if (constraint instanceof SameNode) { + SameNode sn = (SameNode) constraint; + return getNodeIdQuery(UUID, sn.getPath()); + } else if (constraint instanceof ChildNode) { + ChildNode cn = (ChildNode) constraint; + return getNodeIdQuery(PARENT, cn.getParentPath()); + } else if (constraint instanceof DescendantNode) { + DescendantNode dn = (DescendantNode) constraint; + return getDescendantNodeQuery(dn, searcher); + } else { + throw new UnsupportedRepositoryOperationException( + "Unknown constraint type: " + constraint); + } + } + + protected Query getDescendantNodeQuery( + DescendantNode dn, JackrabbitIndexSearcher searcher) + throws RepositoryException, IOException { + BooleanQuery query = new BooleanQuery(); + int clauses = 0; + + try { + LinkedList ids = new LinkedList(); + Node ancestor = session.getNode(dn.getAncestorPath()); + ids.add(ancestor.getIdentifier()); + while (!ids.isEmpty()) { + String id = ids.removeFirst(); + Query q = new JackrabbitTermQuery(new Term(FieldNames.PARENT, id)); + QueryHits hits = searcher.evaluate(q); + ScoreNode sn = hits.nextScoreNode(); + if (sn != null) { + // reset query so it does not overflow because of the max + // clause count condition, + // see JCR-3108 + clauses++; + if (clauses == BooleanQuery.getMaxClauseCount()) { + BooleanQuery wrapQ = new BooleanQuery(); + wrapQ.add(query, SHOULD); + query = wrapQ; + clauses = 1; + } + query.add(q, SHOULD); + do { + ids.add(sn.getNodeId().toString()); + sn = hits.nextScoreNode(); + } while (sn != null); + } + } + } catch (PathNotFoundException e) { + query.add(new JackrabbitTermQuery(new Term( + FieldNames.UUID, "invalid-node-id")), // never matches + SHOULD); + } + + return query; + } + + protected Query getFullTextSearchQuery(FullTextSearch fts) + throws RepositoryException { + String field = FieldNames.FULLTEXT; + String property = fts.getPropertyName(); + if (property != null) { + Name name = session.getQName(property); + field = nsMappings.getPrefix(name.getNamespaceURI()) + ":" + + FieldNames.FULLTEXT_PREFIX + name.getLocalName(); + } + + StaticOperand expression = fts.getFullTextSearchExpression(); + String query = evaluator.getValue(expression).getString(); + try { + QueryParser parser = new JackrabbitQueryParser( + field, index.getTextAnalyzer(), + index.getSynonymProvider(), cache); + return parser.parse(query); + } catch (ParseException e) { + throw new RepositoryException( + "Invalid full text search expression: " + query, e); + } + } + + protected BooleanQuery getAndQuery( + And and, Map selectorMap, + JackrabbitIndexSearcher searcher) + throws RepositoryException, IOException { + BooleanQuery query = new BooleanQuery(); + addBooleanConstraint( + query, and.getConstraint1(), MUST, selectorMap, searcher); + addBooleanConstraint( + query, and.getConstraint2(), MUST, selectorMap, searcher); + return query; + } + + protected BooleanQuery getOrQuery( + Or or, Map selectorMap, + JackrabbitIndexSearcher searcher) + throws RepositoryException, IOException { + BooleanQuery query = new BooleanQuery(); + addBooleanConstraint( + query, or.getConstraint1(), SHOULD, selectorMap, searcher); + addBooleanConstraint( + query, or.getConstraint2(), SHOULD, selectorMap, searcher); + return query; + } + + protected void addBooleanConstraint( + BooleanQuery query, Constraint constraint, Occur occur, + Map selectorMap, JackrabbitIndexSearcher searcher) + throws RepositoryException, IOException { + if (occur == MUST && constraint instanceof And) { + And and = (And) constraint; + addBooleanConstraint( + query, and.getConstraint1(), occur, selectorMap, searcher); + addBooleanConstraint( + query, and.getConstraint2(), occur, selectorMap, searcher); + } else if (occur == SHOULD && constraint instanceof Or) { + Or or = (Or) constraint; + addBooleanConstraint( + query, or.getConstraint1(), occur, selectorMap, searcher); + addBooleanConstraint( + query, or.getConstraint2(), occur, selectorMap, searcher); + } else { + query.add(create(constraint, selectorMap, searcher), occur); + } + } + + protected NotQuery getNotQuery( + Not not, Map selectorMap, + JackrabbitIndexSearcher searcher) + throws RepositoryException, IOException { + return new NotQuery(create(not.getConstraint(), selectorMap, searcher)); + } + + protected Query getPropertyExistenceQuery(PropertyExistence property) + throws RepositoryException { + String name = npResolver.getJCRName(session.getQName( + property.getPropertyName())); + return Util.createMatchAllQuery( + name, index.getIndexFormatVersion(), cache); + } + + protected static class Transform { + + private final DynamicOperand operand; + + private final int transform; + + public Transform(DynamicOperand operand) { + // Check the transformation type + if (operand instanceof UpperCase) { + this.transform = TRANSFORM_UPPER_CASE; + } else if (operand instanceof LowerCase) { + this.transform = TRANSFORM_LOWER_CASE; + } else { + this.transform = TRANSFORM_NONE; + } + + // Unwrap any nested transformations + while (true) { + if (operand instanceof UpperCase) { + operand = ((UpperCase) operand).getOperand(); + } else if (operand instanceof LowerCase) { + operand = ((LowerCase) operand).getOperand(); + } else { + break; + } + } + this.operand = operand; + } + } + + protected Query getComparisonQuery( + DynamicOperand left, int transform, String operator, + StaticOperand rigth, Map selectorMap) + throws RepositoryException { + if (left instanceof PropertyValue) { + PropertyValue pv = (PropertyValue) left; + String field = npResolver.getJCRName(session.getQName( + pv.getPropertyName())); + int type = PropertyType.UNDEFINED; + NodeType nt = selectorMap.get(pv.getSelectorName()); + if (nt != null) { + for (PropertyDefinition pd : nt.getPropertyDefinitions()) { + if (pd.getName().equals(pv.getPropertyName())) { + type = pd.getRequiredType(); + break; + } + } + } + return getPropertyValueQuery( + field, operator, evaluator.getValue(rigth), type, transform); + } else if (left instanceof NodeName) { + return getNodeNameQuery(transform, operator, rigth); + } else if (left instanceof NodeLocalName) { + return getNodeLocalNameQuery(transform, operator, rigth); + } else { + throw new UnsupportedRepositoryOperationException( + "Unknown operand type: " + left); // FIXME + } + } + + protected Query getNodeNameQuery( + int transform, String operator, StaticOperand right) + throws RepositoryException { + if (transform != TRANSFORM_NONE + || !JCR_OPERATOR_EQUAL_TO.equals(operator)) { + throw new UnsupportedRepositoryOperationException(); + } + + Value value = evaluator.getValue(right); + int type = value.getType(); + String string = value.getString(); + if (type == PropertyType.URI && string.startsWith("./")) { + string = string.substring("./".length()); + } else if (type == PropertyType.DOUBLE + || type == PropertyType.DECIMAL + || type == PropertyType.LONG + || type == PropertyType.BOOLEAN + || type == PropertyType.REFERENCE + || type == PropertyType.WEAKREFERENCE) { + throw new InvalidQueryException("Invalid name value: " + string); + } + + try { + Name name = session.getQName(string); + Term uri = new Term(NAMESPACE_URI, name.getNamespaceURI()); + Term local = new Term(LOCAL_NAME, name.getLocalName()); + + BooleanQuery query = new BooleanQuery(); + query.add(new JackrabbitTermQuery(uri), MUST); + query.add(new JackrabbitTermQuery(local), MUST); + return query; + } catch (IllegalNameException e) { + throw new InvalidQueryException("Illegal name: " + string, e); + } + } + + protected Query getNodeLocalNameQuery(int transform, String operator, + StaticOperand right) throws RepositoryException { + if (!JCR_OPERATOR_EQUAL_TO.equals(operator) && !JCR_OPERATOR_LIKE.equals(operator)) { + throw new UnsupportedRepositoryOperationException(); + } + String name = evaluator.getValue(right).getString(); + + if (JCR_OPERATOR_LIKE.equals(operator)) { + return new WildcardQuery(LOCAL_NAME, null, name, transform, cache); + } + return new JackrabbitTermQuery(new Term(LOCAL_NAME, name)); + } + + protected Query getNodeIdQuery(String field, String path) + throws RepositoryException { + String value; + try { + value = session.getNode(path).getIdentifier(); + } catch (PathNotFoundException e) { + value = "invalid-node-id"; // can never match a node + } + return new JackrabbitTermQuery(new Term(field, value)); + } + + protected Query getPropertyValueQuery( + String field, String operator, Value value, + int type, int transform) throws RepositoryException { + String string = getValueString(value, type); + if (JCR_OPERATOR_LIKE.equals(operator)) { + return new WildcardQuery(PROPERTIES, field, string, transform, cache); + } + + Term term = getTerm(field, string); + if (JCR_OPERATOR_EQUAL_TO.equals(operator)) { + switch (transform) { + case TRANSFORM_UPPER_CASE: + return new CaseTermQuery.Upper(term); + case TRANSFORM_LOWER_CASE: + return new CaseTermQuery.Lower(term); + default: + return new JackrabbitTermQuery(term); + } + } else if (JCR_OPERATOR_GREATER_THAN.equals(operator)) { + return new RangeQuery(term, getTerm(field, "\uFFFF"), false, transform, cache); + } else if (JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO.equals(operator)) { + return new RangeQuery(term, getTerm(field, "\uFFFF"), true, transform, cache); + } else if (JCR_OPERATOR_LESS_THAN.equals(operator)) { + return new RangeQuery(getTerm(field, ""), term, false, transform, cache); + } else if (JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO.equals(operator)) { + return new RangeQuery(getTerm(field, ""), term, true, transform, cache); + } else if (JCR_OPERATOR_NOT_EQUAL_TO.equals(operator)) { + BooleanQuery query = new BooleanQuery(); + query.add(Util.createMatchAllQuery( + field, index.getIndexFormatVersion(), cache), SHOULD); + if (transform == TRANSFORM_UPPER_CASE) { + query.add(new CaseTermQuery.Upper(term), MUST_NOT); + } else if (transform == TRANSFORM_LOWER_CASE) { + query.add(new CaseTermQuery.Lower(term), MUST_NOT); + } else { + query.add(new JackrabbitTermQuery(term), MUST_NOT); + } + // and exclude all nodes where 'field' is multi valued + query.add(new JackrabbitTermQuery(new Term(MVP, field)), MUST_NOT); + return query; + } else { + throw new UnsupportedRepositoryOperationException(); // FIXME + } + } + + protected Term getTerm(String field, String value) { + return new Term(PROPERTIES, FieldNames.createNamedValue(field, value)); + } + + protected String getValueString(Value value, int type) + throws RepositoryException { + switch (value.getType()) { + case DATE: + return DateField.dateToString(value.getDate().getTime()); + case DOUBLE: + return DoubleField.doubleToString(value.getDouble()); + case LONG: + return LongField.longToString(value.getLong()); + case DECIMAL: + return DecimalField.decimalToString(value.getDecimal()); + case NAME: + return npResolver.getJCRName(session.getQName(value.getString())); + case PATH: + return npResolver.getJCRPath(session.getQPath(value.getString())); + default: + String string = value.getString(); + if (type != UNDEFINED && type != STRING) { + return getValueString( + session.getValueFactory().createValue(string, type), + UNDEFINED); + } else { + return string; + } + } + } + + protected static class QueryPair { + Query mainQuery; + BooleanQuery subQuery; + + QueryPair(BooleanQuery mainQuery) { + this.mainQuery = mainQuery; + this.subQuery = mainQuery; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryHits.java new file mode 100644 index 00000000000..9b102f0e603 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/LuceneQueryHits.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.index.IndexReader; +import org.apache.jackrabbit.core.id.NodeId; + +/** + * Wraps a lucene query result and adds a close method that allows to release + * resources after a query has been executed and the results have been read + * completely. + */ +public class LuceneQueryHits implements QueryHits { + + /** + * The IndexReader in use by the lucene hits. + */ + private final IndexReader reader; + + /** + * The scorer for the query. + */ + private final Scorer scorer; + + public LuceneQueryHits(IndexReader reader, + IndexSearcher searcher, + Query query) + throws IOException { + this.reader = reader; + // We rely on Scorer#nextDoc() and Scorer#advance(int) so enable + // scoreDocsInOrder + this.scorer = query.createWeight(searcher).scorer(reader, true, false); + } + + /** + * {@inheritDoc} + */ + public ScoreNode nextScoreNode() throws IOException { + if (scorer == null) { + return null; + } + int doc = scorer.nextDoc(); + if (doc == DocIdSetIterator.NO_MORE_DOCS) { + return null; + } + NodeId id = new NodeId(reader.document( + doc, FieldSelectors.UUID).get(FieldNames.UUID)); + return new ScoreNode(id, scorer.score(), doc); + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + if (scorer != null) { + // make sure scorer frees resources + scorer.advance(Integer.MAX_VALUE); + } + } + + /** + * @return always -1. + */ + public int getSize() { + return -1; + } + + /** + * {@inheritDoc} + */ + public void skip(int n) throws IOException { + while (n-- > 0) { + if (nextScoreNode() == null) { + return; + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllDocsQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllDocsQuery.java new file mode 100644 index 00000000000..5dcfa9803d5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllDocsQuery.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.core.query.lucene; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.lucene.search.Sort; + +import javax.jcr.RepositoryException; +import java.io.IOException; + +/** + * MatchAllDocsQuery extends the lucene MatchAllDocsQuery + * and in addition implements {@link JackrabbitQuery}. + */ +@SuppressWarnings("serial") +public class MatchAllDocsQuery + extends org.apache.lucene.search.MatchAllDocsQuery + implements JackrabbitQuery { + + /** + * {@inheritDoc} + */ + public QueryHits execute(JackrabbitIndexSearcher searcher, + SessionImpl session, + Sort sort) throws IOException { + if (sort.getSort().length == 0) { + try { + return new NodeTraversingQueryHits( + session.getRootNode(), true); + } catch (RepositoryException e) { + throw Util.createIOException(e); + } + } else { + return null; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllQuery.java new file mode 100644 index 00000000000..4a288b4d56a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllQuery.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.core.query.lucene; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Weight; + +import java.util.Set; + +/** + * Specialized query that returns / scores all pages in the search index. + *

    Use this Query to perform a match '*'. + */ +@SuppressWarnings("serial") +class MatchAllQuery extends Query { + + private final String field; + + private final PerQueryCache cache; + + /** + * Creates a new MatchAllQuery . + *

    + * + * @param field the field name. + * @throws NullPointerException if field is null. + */ + MatchAllQuery(String field, PerQueryCache cache) + throws NullPointerException { + if (field == null) { + throw new NullPointerException("field"); + } + this.field = field.intern(); + this.cache = cache; + } + + /** + * Returns the Weight for this Query. + * + * @param searcher the current searcher. + * @return the Weight for this Query. + */ + public Weight createWeight(Searcher searcher) { + return new MatchAllWeight(this, searcher, field, cache); + } + + /** + * Returns the String "%". + * + * @param field default field for the query. + * @return the String "%". + */ + public String toString(String field) { + return "%"; + } + + /** + * Does nothing but simply returns. There are no terms to extract. + */ + public void extractTerms(Set terms) { + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllScorer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllScorer.java new file mode 100644 index 00000000000..ee0cbd59b86 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllScorer.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Similarity; + +import java.io.IOException; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; + +/** + * The MatchAllScorer implements a Scorer that scores / collects all + * documents in the index that match a field. + */ +class MatchAllScorer extends Scorer { + + /** + * next doc number + */ + private int nextDoc = -1; + + /** + * IndexReader giving access to index + */ + private IndexReader reader; + + /** + * The field to match + */ + private String field; + + /** + * BitSet filtering documents without content is specified field + */ + private BitSet docFilter; + + /** + * Creates a new MatchAllScorer. + * + * @param reader the IndexReader + * @param field the field name to match. + * @throws IOException if an error occurs while collecting hits. + * e.g. while reading from the search index. + */ + MatchAllScorer(IndexReader reader, String field, PerQueryCache cache) + throws IOException { + super(Similarity.getDefault()); + this.reader = reader; + this.field = field; + calculateDocFilter(cache); + } + + @Override + public void score(Collector collector) throws IOException { + collector.setScorer(this); + + while (nextDoc() != NO_MORE_DOCS) { + collector.collect(docID()); + } + } + + @Override + public int nextDoc() throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + nextDoc = docFilter.nextSetBit(nextDoc + 1); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + @Override + public int docID() { + return nextDoc; + } + + @Override + public float score() throws IOException { + return 1.0f; + } + + @Override + public int advance(int target) throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + nextDoc = NO_MORE_DOCS; + return nextDoc; + } + + nextDoc = target - 1; + return nextDoc(); + } + + /** + * Calculates a BitSet filter that includes all the nodes + * that have content in properties according to the field name + * passed in the constructor of this MatchAllScorer. + * + * @throws IOException if an error occurs while reading from + * the search index. + */ + @SuppressWarnings({"unchecked"}) + private void calculateDocFilter(PerQueryCache cache) throws IOException { + Map readerCache = (Map) cache.get(MatchAllScorer.class, reader); + if (readerCache == null) { + readerCache = new HashMap(); + cache.put(MatchAllScorer.class, reader, readerCache); + } + // get BitSet for field + docFilter = readerCache.get(field); + + if (docFilter != null) { + // use cached BitSet; + return; + } + + // otherwise calculate new + docFilter = new BitSet(reader.maxDoc()); + // we match all terms + String namedValue = FieldNames.createNamedValue(field, ""); + TermEnum terms = reader.terms(new Term(FieldNames.PROPERTIES, namedValue)); + try { + TermDocs docs = reader.termDocs(); + try { + while (terms.term() != null + && terms.term().field() == FieldNames.PROPERTIES + && terms.term().text().startsWith(namedValue)) { + docs.seek(terms); + while (docs.next()) { + docFilter.set(docs.doc()); + } + terms.next(); + } + } finally { + docs.close(); + } + } finally { + terms.close(); + } + + // put BitSet into cache + readerCache.put(field, docFilter); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllWeight.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllWeight.java new file mode 100644 index 00000000000..40c3268607b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MatchAllWeight.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.core.query.lucene; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; + +import java.io.IOException; + +/** + * This class implements the Weight calculation for the MatchAllQuery. + */ +@SuppressWarnings("serial") +class MatchAllWeight extends AbstractWeight { + + /** + * Name of the field to match. + */ + private final String field; + + /** + * the MatchAllQuery + */ + private final Query query; + + private final PerQueryCache cache; + + /** + * the weight value + */ + private float value; + + /** + * doc frequency for this weight + */ + private float idf; + + /** + * the query weight + */ + private float queryWeight; + + /** + * @param query + * @param searcher + * @param field name of the field to match + */ + MatchAllWeight( + Query query, Searcher searcher, String field, PerQueryCache cache) { + super(searcher); + this.query = query; + this.field = field; + this.cache = cache; + } + + /** + * Creates a {@link MatchAllScorer} instance. + * + * @param reader index reader + * @return a {@link MatchAllScorer} instance + */ + protected Scorer createScorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + return new MatchAllScorer(reader, field, cache); + } + + /** + * {@inheritDoc} + */ + public Query getQuery() { + return query; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return value; + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + idf = searcher.getSimilarity().idf(searcher.maxDoc(), searcher.maxDoc()); // compute idf + queryWeight = idf * 1.0f; // boost // compute query weight + return queryWeight * queryWeight; // square it + } + + /** + * {@inheritDoc} + */ + public void normalize(float queryNorm) { + queryWeight *= queryNorm; // normalize query weight + value = queryWeight * idf; // idf for document + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(Similarity.getDefault().idf(reader.maxDoc(), reader.maxDoc()), + "matchAll"); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MoreLikeThis.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MoreLikeThis.java new file mode 100644 index 00000000000..eb0ed9ce966 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MoreLikeThis.java @@ -0,0 +1,880 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.util.PriorityQueue; +import org.apache.lucene.util.ReaderUtil; +import org.apache.lucene.util.Version; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermFreqVector; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.DefaultSimilarity; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.document.Document; + +import java.util.List; +import java.util.Set; +import java.util.HashMap; +import java.util.Map; +import java.util.Collection; +import java.util.Iterator; +import java.io.IOException; +import java.io.Reader; +import java.io.File; +import java.io.StringReader; +import java.io.FileReader; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; + + +/** + * Generate "more like this" similarity queries. + * Based on this mail: + *

    + * Lucene does let you access the document frequency of terms, with IndexReader.docFreq().
    + * Term frequencies can be computed by re-tokenizing the text, which, for a single document,
    + * is usually fast enough.  But looking up the docFreq() of every term in the document is
    + * probably too slow.
    + *
    + * You can use some heuristics to prune the set of terms, to avoid calling docFreq() too much,
    + * or at all.  Since you're trying to maximize a tf*idf score, you're probably most interested
    + * in terms with a high tf. Choosing a tf threshold even as low as two or three will radically
    + * reduce the number of terms under consideration.  Another heuristic is that terms with a
    + * high idf (i.e., a low df) tend to be longer.  So you could threshold the terms by the
    + * number of characters, not selecting anything less than, e.g., six or seven characters.
    + * With these sorts of heuristics you can usually find small set of, e.g., ten or fewer terms
    + * that do a pretty good job of characterizing a document.
    + *
    + * It all depends on what you're trying to do.  If you're trying to eek out that last percent
    + * of precision and recall regardless of computational difficulty so that you can win a TREC
    + * competition, then the techniques I mention above are useless.  But if you're trying to
    + * provide a "more like this" button on a search results page that does a decent job and has
    + * good performance, such techniques might be useful.
    + *
    + * An efficient, effective "more-like-this" query generator would be a great contribution, if
    + * anyone's interested.  I'd imagine that it would take a Reader or a String (the document's
    + * text), analyzer Analyzer, and return a set of representative terms using heuristics like those
    + * above.  The frequency and length thresholds could be parameters, etc.
    + *
    + * Doug
    + * 
    + * + * + *

    + *

    Initial Usage

    + * + * This class has lots of options to try to make it efficient and flexible. + * + *
    + *
    + * IndexReader ir = ...
    + * IndexSearcher is = ...
    + * 
    + * MoreLikeThis mlt = new MoreLikeThis(ir);
    + * Reader target = ... // orig source of doc you want to find similarities to
    + * Query query = mlt.like( target);
    + * 
    + * Hits hits = is.search(query);
    + * // now the usual iteration thru 'hits' - the only thing to watch for is to make sure
    + * you ignore the doc if it matches your 'target' document, as it should be similar to itself 
    + *
    + * 
    + * + * Thus you: + *
      + *
    1. do your normal, Lucene setup for searching, + *
    2. create a MoreLikeThis, + *
    3. get the text of the doc you want to find similaries to + *
    4. then call one of the like() calls to generate a similarity query + *
    5. call the searcher to find the similar docs + *
    + * + *

    More Advanced Usage

    + * + * You may want to use {@link #setFieldNames setFieldNames(...)} so you can examine + * multiple fields (e.g. body and title) for similarity. + *

    + * + * Depending on the size of your index and the size and makeup of your documents you + * may want to call the other set methods to control how the similarity queries are + * generated: + *

      + *
    • {@link #setMinTermFreq setMinTermFreq(...)} + *
    • {@link #setMinDocFreq setMinDocFreq(...)} + *
    • {@link #setMinWordLen setMinWordLen(...)} + *
    • {@link #setMaxWordLen setMaxWordLen(...)} + *
    • {@link #setMaxQueryTerms setMaxQueryTerms(...)} + *
    • {@link #setMaxNumTokensParsed setMaxNumTokensParsed(...)} + *
    • {@link #setStopWords setStopWord(...)} + *
    + * + *
    + *
    + * Changes: Mark Harwood 29/02/04
    + * Some bugfixing, some refactoring, some optimisation.
    + *  - bugfix: retrieveTerms(int docNum) was not working for indexes without a termvector -added missing code
    + *  - bugfix: No significant terms being created for fields with a termvector - because
    + *            was only counting one occurence per term/field pair in calculations(ie not including frequency info from TermVector)
    + *  - refactor: moved common code into isNoiseWord()
    + *  - optimise: when no termvector support available - used maxNumTermsParsed to limit amount of tokenization
    + * 
    + * + */ +public final class MoreLikeThis { + + /** + * Default maximum number of tokens to parse in each example doc field that is not stored with TermVector support. + * @see #getMaxNumTokensParsed + */ + public static final int DEFAULT_MAX_NUM_TOKENS_PARSED = 5000; + + /** + * Default analyzer to parse source doc with. + * @see #getAnalyzer + */ + public static final Analyzer DEFAULT_ANALYZER = new StandardAnalyzer(Version.LUCENE_36); + + /** + * Ignore terms with less than this frequency in the source doc. + * @see #getMinTermFreq + * @see #setMinTermFreq + */ + public static final int DEFAULT_MIN_TERM_FREQ = 2; + + /** + * Ignore words which do not occur in at least this many docs. + * @see #getMinDocFreq + * @see #setMinDocFreq + */ + public static final int DEFAULT_MIN_DOC_FREQ = 5; + + /** + * Boost terms in query based on score. + * @see #isBoost + * @see #setBoost + */ + public static final boolean DEFAULT_BOOST = false; + + /** + * Default field names. Null is used to specify that the field names should be looked + * up at runtime from the provided reader. + */ + public static final String[] DEFAULT_FIELD_NAMES = new String[] { "contents"}; + + /** + * Ignore words less than this length or if 0 then this has no effect. + * @see #getMinWordLen + * @see #setMinWordLen + */ + public static final int DEFAULT_MIN_WORD_LENGTH = 0; + + /** + * Ignore words greater than this length or if 0 then this has no effect. + * @see #getMaxWordLen + * @see #setMaxWordLen + */ + public static final int DEFAULT_MAX_WORD_LENGTH = 0; + + /** + * Default set of stopwords. + * If null means to allow stop words. + * + * @see #setStopWords + * @see #getStopWords + */ + public static final Set DEFAULT_STOP_WORDS = null; + + /** + * Current set of stop words. + */ + private Set stopWords = DEFAULT_STOP_WORDS; + + /** + * Return a Query with no more than this many terms. + * + * @see BooleanQuery#getMaxClauseCount + * @see #getMaxQueryTerms + * @see #setMaxQueryTerms + */ + public static final int DEFAULT_MAX_QUERY_TERMS = 25; + + /** + * Analyzer that will be used to parse the doc. + */ + private Analyzer analyzer = DEFAULT_ANALYZER; + + /** + * Ignore words less freqent that this. + */ + private int minTermFreq = DEFAULT_MIN_TERM_FREQ; + + /** + * Ignore words which do not occur in at least this many docs. + */ + private int minDocFreq = DEFAULT_MIN_DOC_FREQ; + + /** + * Should we apply a boost to the Query based on the scores? + */ + private boolean boost = DEFAULT_BOOST; + + /** + * Field name we'll analyze. + */ + private String[] fieldNames = DEFAULT_FIELD_NAMES; + + /** + * The maximum number of tokens to parse in each example doc field that is not stored with TermVector support + */ + private int maxNumTokensParsed = DEFAULT_MAX_NUM_TOKENS_PARSED; + + /** + * Ignore words if less than this len. + */ + private int minWordLen = DEFAULT_MIN_WORD_LENGTH; + + /** + * Ignore words if greater than this len. + */ + private int maxWordLen = DEFAULT_MAX_WORD_LENGTH; + + /** + * Don't return a query longer than this. + */ + private int maxQueryTerms = DEFAULT_MAX_QUERY_TERMS; + + /** + * For idf() calculations. + */ + private Similarity similarity;// = new DefaultSimilarity(); + + /** + * IndexReader to use + */ + private final IndexReader ir; + + /** + * Constructor requiring an IndexReader. + */ + public MoreLikeThis(IndexReader ir) { + this(ir, new DefaultSimilarity()); + } + + public MoreLikeThis(IndexReader ir, Similarity sim){ + this.ir = ir; + this.similarity = sim; + } + + + public Similarity getSimilarity() { + return similarity; + } + + public void setSimilarity(Similarity similarity) { + this.similarity = similarity; + } + + /** + * Returns an analyzer that will be used to parse source doc with. The default analyzer + * is the {@link #DEFAULT_ANALYZER}. + * + * @return the analyzer that will be used to parse source doc with. + * @see #DEFAULT_ANALYZER + */ + public Analyzer getAnalyzer() { + return analyzer; + } + + /** + * Sets the analyzer to use. An analyzer is not required for generating a query with the + * {@link #like(int)} method, all other 'like' methods require an analyzer. + * + * @param analyzer the analyzer to use to tokenize text. + */ + public void setAnalyzer(Analyzer analyzer) { + this.analyzer = analyzer; + } + + /** + * Returns the frequency below which terms will be ignored in the source doc. The default + * frequency is the {@link #DEFAULT_MIN_TERM_FREQ}. + * + * @return the frequency below which terms will be ignored in the source doc. + */ + public int getMinTermFreq() { + return minTermFreq; + } + + /** + * Sets the frequency below which terms will be ignored in the source doc. + * + * @param minTermFreq the frequency below which terms will be ignored in the source doc. + */ + public void setMinTermFreq(int minTermFreq) { + this.minTermFreq = minTermFreq; + } + + /** + * Returns the frequency at which words will be ignored which do not occur in at least this + * many docs. The default frequency is {@link #DEFAULT_MIN_DOC_FREQ}. + * + * @return the frequency at which words will be ignored which do not occur in at least this + * many docs. + */ + public int getMinDocFreq() { + return minDocFreq; + } + + /** + * Sets the frequency at which words will be ignored which do not occur in at least this + * many docs. + * + * @param minDocFreq the frequency at which words will be ignored which do not occur in at + * least this many docs. + */ + public void setMinDocFreq(int minDocFreq) { + this.minDocFreq = minDocFreq; + } + + /** + * Returns whether to boost terms in query based on "score" or not. The default is + * {@link #DEFAULT_BOOST}. + * + * @return whether to boost terms in query based on "score" or not. + * @see #setBoost + */ + public boolean isBoost() { + return boost; + } + + /** + * Sets whether to boost terms in query based on "score" or not. + * + * @param boost true to boost terms in query based on "score", false otherwise. + * @see #isBoost + */ + public void setBoost(boolean boost) { + this.boost = boost; + } + + /** + * Returns the field names that will be used when generating the 'More Like This' query. + * The default field names that will be used is {@link #DEFAULT_FIELD_NAMES}. + * + * @return the field names that will be used when generating the 'More Like This' query. + */ + public String[] getFieldNames() { + return fieldNames; + } + + /** + * Sets the field names that will be used when generating the 'More Like This' query. + * Set this to null for the field names to be determined at runtime from the IndexReader + * provided in the constructor. + * + * @param fieldNames the field names that will be used when generating the 'More Like This' + * query. + */ + public void setFieldNames(String[] fieldNames) { + this.fieldNames = fieldNames; + } + + /** + * Returns the minimum word length below which words will be ignored. Set this to 0 for no + * minimum word length. The default is {@link #DEFAULT_MIN_WORD_LENGTH}. + * + * @return the minimum word length below which words will be ignored. + */ + public int getMinWordLen() { + return minWordLen; + } + + /** + * Sets the minimum word length below which words will be ignored. + * + * @param minWordLen the minimum word length below which words will be ignored. + */ + public void setMinWordLen(int minWordLen) { + this.minWordLen = minWordLen; + } + + /** + * Returns the maximum word length above which words will be ignored. Set this to 0 for no + * maximum word length. The default is {@link #DEFAULT_MAX_WORD_LENGTH}. + * + * @return the maximum word length above which words will be ignored. + */ + public int getMaxWordLen() { + return maxWordLen; + } + + /** + * Sets the maximum word length above which words will be ignored. + * + * @param maxWordLen the maximum word length above which words will be ignored. + */ + public void setMaxWordLen(int maxWordLen) { + this.maxWordLen = maxWordLen; + } + + /** + * Set the set of stopwords. + * Any word in this set is considered "uninteresting" and ignored. + * Even if your Analyzer allows stopwords, you might want to tell the MoreLikeThis code to ignore them, as + * for the purposes of document similarity it seems reasonable to assume that "a stop word is never interesting". + * + * @param stopWords set of stopwords, if null it means to allow stop words + * + * @see org.apache.lucene.analysis.StopFilter#makeStopSet StopFilter.makeStopSet() + * @see #getStopWords + */ + public void setStopWords(Set stopWords) { + this.stopWords = stopWords; + } + + /** + * Get the current stop words being used. + * @see #setStopWords + */ + public Set getStopWords() { + return stopWords; + } + + /** + * Returns the maximum number of query terms that will be included in any generated query. + * The default is {@link #DEFAULT_MAX_QUERY_TERMS}. + * + * @return the maximum number of query terms that will be included in any generated query. + */ + public int getMaxQueryTerms() { + return maxQueryTerms; + } + + /** + * Sets the maximum number of query terms that will be included in any generated query. + * + * @param maxQueryTerms the maximum number of query terms that will be included in any + * generated query. + */ + public void setMaxQueryTerms(int maxQueryTerms) { + this.maxQueryTerms = maxQueryTerms; + } + + /** + * @return The maximum number of tokens to parse in each example doc field that is not stored with TermVector support + * @see #DEFAULT_MAX_NUM_TOKENS_PARSED + */ + public int getMaxNumTokensParsed() { + return maxNumTokensParsed; + } + + /** + * @param i The maximum number of tokens to parse in each example doc field that is not stored with TermVector support + */ + public void setMaxNumTokensParsed(int i) { + maxNumTokensParsed = i; + } + + /** + * Return a query that will return docs like the passed lucene document ID. + * + * @param docNum the documentID of the lucene doc to generate the 'More Like This" query for. + * @return a query that will return docs like the passed lucene document ID. + */ + public Query like(int docNum) throws IOException { + if (fieldNames == null) { + // gather list of valid fields from lucene + Collection fields = ReaderUtil.getIndexedFields(ir); + fieldNames = fields.toArray(new String[fields.size()]); + } + + return createQuery(retrieveTerms(docNum)); + } + + /** + * Return a query that will return docs like the passed file. + * + * @return a query that will return docs like the passed file. + */ + public Query like(File f) throws IOException { + if (fieldNames == null) { + // gather list of valid fields from lucene + Collection fields = ReaderUtil.getIndexedFields(ir); + fieldNames = fields.toArray(new String[fields.size()]); + } + + return like(new FileReader(f)); + } + + /** + * Return a query that will return docs like the passed URL. + * + * @return a query that will return docs like the passed URL. + */ + public Query like(URL u) throws IOException { + return like(new InputStreamReader(u.openConnection().getInputStream())); + } + + /** + * Return a query that will return docs like the passed stream. + * + * @return a query that will return docs like the passed stream. + */ + public Query like(java.io.InputStream is) throws IOException { + return like(new InputStreamReader(is)); + } + + /** + * Return a query that will return docs like the passed Reader. + * + * @return a query that will return docs like the passed Reader. + */ + public Query like(Reader r) throws IOException { + return createQuery(retrieveTerms(r)); + } + + /** + * Create the More like query from a PriorityQueue + */ + private Query createQuery(PriorityQueue q) { + BooleanQuery query = new BooleanQuery(); + Object cur; + int qterms = 0; + float bestScore = 0; + + while (((cur = q.pop()) != null)) { + Object[] ar = (Object[]) cur; + TermQuery tq = new JackrabbitTermQuery(new Term((String) ar[1], (String) ar[0])); + + if (boost) { + if (qterms == 0) { + bestScore = ((Float) ar[2]).floatValue(); + } + float myScore = ((Float) ar[2]).floatValue(); + + tq.setBoost(myScore / bestScore); + } + + try { + query.add(tq, BooleanClause.Occur.SHOULD); + } + catch (BooleanQuery.TooManyClauses ignore) { + break; + } + + qterms++; + if (maxQueryTerms > 0 && qterms >= maxQueryTerms) { + break; + } + } + + return query; + } + + /** + * Create a PriorityQueue from a word->tf map. + * + * @param words a map of words keyed on the word(String) with Int objects as the values. + */ + private PriorityQueue createQueue(Map words) throws IOException { + // have collected all words in doc and their freqs + int numDocs = ir.numDocs(); + FreqQ res = new FreqQ(words.size()); // will order words by score + + Iterator> it = words.entrySet().iterator(); + while (it.hasNext()) { // for every word + Map.Entry entry = it.next(); + String word = entry.getKey(); + + int tf = entry.getValue().x; // term freq in the source doc + if (minTermFreq > 0 && tf < minTermFreq) { + continue; // filter out words that don't occur enough times in the source + } + + // go through all the fields and find the largest document frequency + String topField = fieldNames[0]; + int docFreq = 0; + for (int i = 0; i < fieldNames.length; i++) { + int freq = ir.docFreq(new Term(fieldNames[i], word)); + topField = (freq > docFreq) ? fieldNames[i] : topField; + docFreq = (freq > docFreq) ? freq : docFreq; + } + + if (minDocFreq > 0 && docFreq < minDocFreq) { + continue; // filter out words that don't occur in enough docs + } + + if (docFreq == 0) { + continue; // index update problem? + } + + float idf = similarity.idf(docFreq, numDocs); + float score = tf * idf; + + // only really need 1st 3 entries, other ones are for troubleshooting + res.insertWithOverflow(new Object[]{word, // the word + topField, // the top field + new Float(score), // overall score + new Float(idf), // idf + new Integer(docFreq), // freq in all docs + new Integer(tf) + }); + } + return res; + } + + /** + * Describe the parameters that control how the "more like this" query is formed. + */ + public String describeParams() { + StringBuffer sb = new StringBuffer(); + sb.append("\tmaxQueryTerms : ").append(maxQueryTerms).append("\n"); + sb.append("\tminWordLen : ").append(minWordLen).append("\n"); + sb.append("\tmaxWordLen : ").append(maxWordLen).append("\n"); + sb.append("\tfieldNames : "); + String delim = ""; + for (int i = 0; i < fieldNames.length; i++) { + String fieldName = fieldNames[i]; + sb.append(delim).append(fieldName); + delim = ", "; + } + sb.append("\n"); + sb.append("\tboost : ").append(boost).append("\n"); + sb.append("\tminTermFreq : ").append(minTermFreq).append("\n"); + sb.append("\tminDocFreq : ").append(minDocFreq).append("\n"); + return sb.toString(); + } + + /** + * Find words for a more-like-this query former. + * + * @param docNum the id of the lucene document from which to find terms + */ + public PriorityQueue retrieveTerms(int docNum) throws IOException { + Map termFreqMap = new HashMap(); + for (int i = 0; i < fieldNames.length; i++) { + String fieldName = fieldNames[i]; + TermFreqVector vector = ir.getTermFreqVector(docNum, fieldName); + + // field does not store term vector info + if (vector == null) { + Document d = ir.document(docNum); + String[] text = d.getValues(fieldName); + if (text != null) { + for (int j = 0; j < text.length; j++) { + addTermFrequencies(new StringReader(text[j]), termFreqMap, fieldName); + } + } + } + else { + addTermFrequencies(termFreqMap, vector); + } + + } + + return createQueue(termFreqMap); + } + + /** + * Adds terms and frequencies found in vector into the Map termFreqMap + * @param termFreqMap a Map of terms and their frequencies + * @param vector List of terms and their frequencies for a doc/field + */ + private void addTermFrequencies(Map termFreqMap, TermFreqVector vector) { + String[] terms = vector.getTerms(); + int[] freqs = vector.getTermFrequencies(); + for (int j = 0; j < terms.length; j++) { + String term = terms[j]; + + if (isNoiseWord(term)) { + continue; + } + // increment frequency + Int cnt = (Int) termFreqMap.get(term); + if (cnt == null) { + cnt = new Int(); + termFreqMap.put(term, cnt); + cnt.x = freqs[j]; + } + else { + cnt.x += freqs[j]; + } + } + } + + /** + * Adds term frequencies found by tokenizing text from reader into the Map words + * @param r a source of text to be tokenized + * @param termFreqMap a Map of terms and their frequencies + * @param fieldName Used by analyzer for any special per-field analysis + */ + private void addTermFrequencies(Reader r, Map termFreqMap, String fieldName) + throws IOException { + TokenStream ts = analyzer.tokenStream(fieldName, r); + int tokenCount = 0; + // for every token + while (ts.incrementToken()) { + TermAttribute term = ts.getAttribute(TermAttribute.class); + String word = term.term(); + tokenCount++; + if (tokenCount > maxNumTokensParsed) { + break; + } + if (isNoiseWord(word)) { + continue; + } + + // increment frequency + Int cnt = termFreqMap.get(word); + if (cnt == null) { + termFreqMap.put(word, new Int()); + } else { + cnt.x++; + } + } + ts.end(); + ts.close(); + } + + /** determines if the passed term is likely to be of interest in "more like" comparisons + * + * @param term The word being considered + * @return true if should be ignored, false if should be used in further analysis + */ + private boolean isNoiseWord(String term) { + int len = term.length(); + if (minWordLen > 0 && len < minWordLen) { + return true; + } + if (maxWordLen > 0 && len > maxWordLen) { + return true; + } + if (stopWords != null && stopWords.contains( term)) { + return true; + } + return false; + } + + + /** + * Find words for a more-like-this query former. + * The result is a priority queue of arrays with one entry for every word in the document. + * Each array has 6 elements. + * The elements are: + *
      + *
    1. The word (String) + *
    2. The top field that this word comes from (String) + *
    3. The score for this word (Float) + *
    4. The IDF value (Float) + *
    5. The frequency of this word in the index (Integer) + *
    6. The frequency of this word in the source document (Integer) + *
    + * This is a somewhat "advanced" routine, and in general only the 1st entry in the array is of interest. + * This method is exposed so that you can identify the "interesting words" in a document. + * For an easier method to call see {@link #retrieveInterestingTerms retrieveInterestingTerms()}. + * + * @param r the reader that has the content of the document + * @return the most interesting words in the document ordered by score, with the highest scoring, or best entry, first + * + * @see #retrieveInterestingTerms + */ + public PriorityQueue retrieveTerms(Reader r) throws IOException { + Map words = new HashMap(); + for (int i = 0; i < fieldNames.length; i++) { + String fieldName = fieldNames[i]; + addTermFrequencies(r, words, fieldName); + } + return createQueue(words); + } + + /** + * @see #retrieveInterestingTerms(java.io.Reader) + */ + public String[] retrieveInterestingTerms(int docNum) throws IOException { + List al = new ArrayList(maxQueryTerms); + PriorityQueue pq = retrieveTerms(docNum); + Object cur; + int lim = maxQueryTerms; // have to be careful, retrieveTerms returns all words but that's probably not useful to our caller... + // we just want to return the top words + while (((cur = pq.pop()) != null) && lim-- > 0) { + Object[] ar = (Object[]) cur; + al.add((String) ar[0]); // the 1st entry is the interesting word + } + return al.toArray(new String[al.size()]); + } + + /** + * Convenience routine to make it easy to return the most interesting words in a document. + * More advanced users will call {@link #retrieveTerms(java.io.Reader) retrieveTerms()} directly. + * @param r the source document + * @return the most interesting words in the document + * + * @see #retrieveTerms(java.io.Reader) + * @see #setMaxQueryTerms + */ + public String[] retrieveInterestingTerms(Reader r) throws IOException { + List al = new ArrayList(maxQueryTerms); + PriorityQueue pq = retrieveTerms(r); + Object cur; + int lim = maxQueryTerms; // have to be careful, retrieveTerms returns all words but that's probably not useful to our caller... + // we just want to return the top words + while (((cur = pq.pop()) != null) && lim-- > 0) { + Object[] ar = (Object[]) cur; + al.add((String) ar[0]); // the 1st entry is the interesting word + } + return al.toArray(new String[al.size()]); + } + + /** + * PriorityQueue that orders words by score. + */ + private static class FreqQ extends PriorityQueue { + FreqQ (int s) { + initialize(s); + } + + protected boolean lessThan(Object a, Object b) { + Object[] aa = (Object[]) a; + Object[] bb = (Object[]) b; + Float fa = (Float) aa[2]; + Float fb = (Float) bb[2]; + return fa.floatValue() > fb.floatValue(); + } + } + + /** + * Use for frequencies and to avoid renewing Integers. + */ + private static class Int { + int x; + + Int() { + x = 1; + } + } + + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQuery.java new file mode 100644 index 00000000000..ba6bc4f72f4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQuery.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.core.query.lucene; + +import java.io.IOException; + +/** + * MultiColumnQuery defines an interface for a query that returns + * {@link MultiColumnQueryHits}. + */ +public interface MultiColumnQuery { + + /** + * Executes this query and returns multi column query hits. + * + * @param searcher the index searcher. + * @param orderings the orderings. + * @param resultFetchHint the result fetch hint. + * @return the query hits. + * @throws IOException if an error occurs while executing the query. + */ + public MultiColumnQueryHits execute(JackrabbitIndexSearcher searcher, + Ordering[] orderings, + long resultFetchHint) + throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryAdapter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryAdapter.java new file mode 100644 index 00000000000..95fedf62a2a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryAdapter.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.lucene.search.Query; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.Sort; +import org.apache.jackrabbit.spi.Name; + +/** + * MultiColumnQueryAdapter adapts a lucene query to act like a + * {@link MultiColumnQuery}. + */ +public class MultiColumnQueryAdapter implements MultiColumnQuery { + + /** + * The underlying lucene query. + */ + private final Query query; + + /** + * The selector name for the query hits. + */ + private final Name selectorName; + + /** + * Creates a new adapter for the given query. + * + * @param query a lucene query. + * @param selectorName the selector name for the query hits. + */ + private MultiColumnQueryAdapter(Query query, Name selectorName) { + this.query = query; + this.selectorName = selectorName; + } + + /** + * Adapts the given query. + * + * @param query the lucene query to adapt. + * @param selectorName the selector name for the query hits. + * @return a {@link MultiColumnQuery} that wraps the given lucene query. + */ + public static MultiColumnQuery adapt(Query query, Name selectorName) { + return new MultiColumnQueryAdapter(query, selectorName); + } + + /** + * {@inheritDoc} + */ + public MultiColumnQueryHits execute(JackrabbitIndexSearcher searcher, + Ordering[] orderings, + long resultFetchHint) + throws IOException { + SortField[] fields = new SortField[orderings.length]; + for (int i = 0; i < orderings.length; i++) { + fields[i] = orderings[i].getSortField(); + } + return searcher.execute(query, new Sort(fields), resultFetchHint, selectorName); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryHits.java new file mode 100644 index 00000000000..222fac276a7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryHits.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.core.query.lucene; + +import org.apache.jackrabbit.spi.Name; + +import java.io.IOException; + +/** + * MultiColumnQueryHits defines an interface for reading tuples of + * {@link ScoreNode}s. The {@link ScoreNode}s within a tuple are identified by + * selector {@link Name}s. + */ +public interface MultiColumnQueryHits extends CloseableHits { + + /** + * Returns the next score nodes in this QueryHits or null if + * there are no more score nodes. + * + * @return the next score nodes in this QueryHits. + * @throws IOException if an error occurs while reading from the index. + */ + ScoreNode[] nextScoreNodes() throws IOException; + + /** + * @return the selector names that correspond to the {@link ScoreNode}s + * returned by {@link #nextScoreNodes()}. + */ + Name[] getSelectorNames(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryResult.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryResult.java new file mode 100644 index 00000000000..cdf248ea377 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiColumnQueryResult.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.core.query.lucene; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.spi.commons.query.qom.ColumnImpl; +import org.apache.jackrabbit.spi.commons.query.qom.OrderingImpl; + +/** + * MultiColumnQueryResult implements a query result that executes + * a {@link MultiColumnQuery}. + */ +public class MultiColumnQueryResult extends QueryResultImpl { + + /** + * The query to execute. + */ + private final MultiColumnQuery query; + + /** + * The order specifier for each of the order properties. + */ + protected final Ordering[] orderings; + + public MultiColumnQueryResult( + SearchIndex index, SessionContext sessionContext, + AbstractQueryImpl queryImpl, MultiColumnQuery query, + SpellSuggestion spellSuggestion, ColumnImpl[] columns, + OrderingImpl[] orderings, boolean documentOrder, + long offset, long limit) throws RepositoryException { + super(index, sessionContext, queryImpl, spellSuggestion, + columns, documentOrder, offset, limit); + this.query = query; + this.orderings = index.createOrderings(orderings); + // if document order is requested get all results right away + getResults(docOrder ? Integer.MAX_VALUE : index.getResultFetchSize()); + } + + /** + * {@inheritDoc} + */ + protected MultiColumnQueryHits executeQuery(long resultFetchHint) + throws IOException { + return index.executeQuery( + sessionContext.getSessionImpl(), + query, orderings, resultFetchHint); + } + + /** + * {@inheritDoc} + */ + protected ExcerptProvider createExcerptProvider() throws IOException { + // TODO + return null; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiIndex.java new file mode 100644 index 00000000000..a7239466c25 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiIndex.java @@ -0,0 +1,2145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +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 java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.query.lucene.directory.DirectoryManager; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.store.Directory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A MultiIndex consists of a {@link VolatileIndex} and multiple + * {@link PersistentIndex}es. The goal is to keep most parts of the index open + * with index readers and write new index data to the volatile index. When + * the volatile index reaches a certain size (see {@link SearchIndex#setMinMergeDocs(int)}) + * a new persistent index is created with the index data from the volatile index, + * the same happens when the volatile index has been idle for some time (see + * {@link SearchIndex#setVolatileIdleTime(int)}). + * The new persistent index is then added to the list of already existing + * persistent indexes. Further operations on the new persistent index will + * however only require an IndexReader which serves for queries + * but also for delete operations on the index. + *

    + * The persistent indexes are merged from time to time. The merge behaviour + * is configurable using the methods: {@link SearchIndex#setMaxMergeDocs(int)}, + * {@link SearchIndex#setMergeFactor(int)} and {@link SearchIndex#setMinMergeDocs(int)}. + * For detailed description of the configuration parameters see also the lucene + * IndexWriter class. + *

    + * This class is thread-safe. + *

    + * Note on implementation: Multiple modifying threads are synchronized on a + * MultiIndex instance itself. Synchronization between a modifying + * thread and reader threads is done using {@link #updateMonitor} and + * {@link #updateInProgress}. + */ +public class MultiIndex { + + /** + * The logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(MultiIndex.class); + + /** + * A path factory. + */ + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + /** + * Names of active persistent index directories. + */ + private final IndexInfos indexNames; + + /** + * The history of the multi index. + */ + private final IndexHistory indexHistory; + + /** + * Names of index directories that can be deleted. + * Key = index name (String), Value = time when last in use (Long) + */ + private final Map deletable = new HashMap(); + + /** + * List of open persistent indexes. This list may also contain an open + * PersistentIndex owned by the IndexMerger daemon. Such an index is not + * registered with indexNames and must not be used in regular index + * operations (delete node, etc.)! + */ + private final List indexes = + new ArrayList(); + + /** + * The internal namespace mappings of the query manager. + */ + private final NamespaceMappings nsMappings; + + /** + * The directory manager. + */ + private final DirectoryManager directoryManager; + + /** + * The redo log factory + */ + private final RedoLogFactory redoLogFactory; + + /** + * The base directory to store the index. + */ + private final Directory indexDir; + + /** + * The query handler + */ + private final SearchIndex handler; + + /** + * The volatile index. + */ + private VolatileIndex volatileIndex; + + /** + * Flag indicating whether an update operation is in progress. + */ + private boolean updateInProgress = false; + + /** + * If not null points to a valid IndexReader that + * reads from all indexes, including volatile and persistent indexes. + */ + private CachingMultiIndexReader multiReader; + + /** + * Shared document number cache across all persistent indexes. + */ + private final DocNumberCache cache; + + /** + * Monitor to use to synchronize access to {@link #multiReader} and + * {@link #updateInProgress}. + */ + private final Object updateMonitor = new Object(); + + /** + * true if the redo log contained entries on startup. + */ + private boolean redoLogApplied = false; + + /** + * The time this index was last flushed or a transaction was committed. + */ + private long lastFlushTime = 0; + + /** + * The IndexMerger for this MultiIndex. + */ + private final IndexMerger merger; + + /** + * Task that is periodically called by the repository timer for checking + * if index should be flushed. + */ + private ScheduledFuture flushTask = null; + + /** + * The RedoLog of this MultiIndex. + */ + private RedoLog redoLog; + + /** + * The indexing queue with pending text extraction jobs. + */ + private IndexingQueue indexingQueue; + + /** + * Only used for testing purpose. Set to true after finished + * extraction jobs have been removed from the queue and set to + * false again after the affected nodes have been updated in + * the index. + */ + private boolean indexingQueueCommitPending; + + /** + * Identifiers of nodes that should not be indexed. + */ + private final Set excludedIDs; + + /** + * The next transaction id. + */ + private long nextTransactionId = 0; + + /** + * The current transaction id. + */ + private long currentTransactionId = -1; + + /** + * Flag indicating whether re-indexing is running. + */ + private boolean reindexing = false; + + /** + * The index format version of this multi index. + */ + private final IndexFormatVersion version; + + /** + * Creates a new MultiIndex. + * + * @param handler the search handler + * @param excludedIDs identifiers of nodes that should + * neither be indexed nor further traversed + * @throws IOException if an error occurs + */ + MultiIndex(SearchIndex handler, Set excludedIDs) throws IOException { + this.directoryManager = handler.getDirectoryManager(); + this.redoLogFactory = handler.getRedoLogFactory(); + this.indexDir = directoryManager.getDirectory("."); + this.handler = handler; + this.cache = new DocNumberCache(handler.getCacheSize()); + this.excludedIDs = new HashSet(excludedIDs); + this.nsMappings = handler.getNamespaceMappings(); + + indexNames = new IndexInfos(indexDir, "indexes"); + + this.indexHistory = new IndexHistory(indexDir, + handler.getMaxHistoryAge() * 1000); + + // as of 1.5 deletable file is not used anymore + removeDeletable(); + + this.redoLog = redoLogFactory.createRedoLog(this); + + // initialize IndexMerger + merger = new IndexMerger(this, handler.getContext().getExecutor()); + merger.setMaxMergeDocs(handler.getMaxMergeDocs()); + merger.setMergeFactor(handler.getMergeFactor()); + merger.setMinMergeDocs(handler.getMinMergeDocs()); + + // initialize indexing queue + this.indexingQueue = new IndexingQueue(new IndexingQueueStore(indexDir)); + + // open persistent indexes + Iterator iterator = indexNames.iterator(); + while (iterator.hasNext()) { + IndexInfo info = iterator.next(); + String name = info.getName(); + // only open if it still exists + // it is possible that indexNames still contains a name for + // an index that has been deleted, but indexNames has not been + // written to disk. + if (!directoryManager.hasDirectory(name)) { + log.debug("index does not exist anymore: " + name); + // move on to next index + continue; + } + PersistentIndex index = new PersistentIndex(name, + handler.getTextAnalyzer(), handler.getSimilarity(), + cache, indexingQueue, directoryManager, + handler.getMaxHistoryAge()); + index.setUseCompoundFile(handler.getUseCompoundFile()); + index.setTermInfosIndexDivisor(handler.getTermInfosIndexDivisor()); + indexes.add(index); + merger.indexAdded(index.getName(), index.getNumDocuments()); + } + + // init volatile index + resetVolatileIndex(); + + // set index format version and at the same time + // initialize hierarchy cache if requested. + CachingMultiIndexReader reader = getIndexReader(handler.isInitializeHierarchyCache()); + try { + version = IndexFormatVersion.getVersion(reader); + } finally { + reader.release(); + } + + indexingQueue.initialize(this); + + redoLogApplied = redoLog.hasEntries(); + + // run recovery + Recovery.run(this, redoLog); + + // enqueue unused segments for deletion + enqueueUnusedSegments(); + attemptDelete(); + + // now that we are ready, start index merger + merger.start(); + + if (redoLogApplied) { + // wait for the index merge to finish pending jobs + try { + merger.waitUntilIdle(); + } catch (InterruptedException e) { + // move on + } + flush(); + } + + if (indexNames.size() > 0) { + scheduleFlushTask(); + } + } + + /** + * Returns the number of documents in this index. + * + * @return the number of documents in this index. + * @throws IOException if an error occurs while reading from the index. + */ + int numDocs() throws IOException { + if (indexNames.size() == 0) { + return volatileIndex.getNumDocuments(); + } else { + CachingMultiIndexReader reader = getIndexReader(); + try { + return reader.numDocs(); + } finally { + reader.release(); + } + } + } + + /** + * @return the index format version for this multi index. + */ + IndexFormatVersion getIndexFormatVersion() { + return version; + } + + /** + * Creates an initial index by traversing the node hierarchy starting at the + * node with rootId. + * + * @param stateMgr the item state manager. + * @param rootId the id of the node from where to start. + * @param rootPath the path of the node from where to start. + * @throws IOException if an error occurs while indexing the + * workspace. + * @throws IllegalStateException if this index is not empty. + */ + void createInitialIndex(ItemStateManager stateMgr, + NodeId rootId, + Path rootPath) + throws IOException { + // only do an initial index if there are no indexes at all + if (indexNames.size() == 0) { + reindexing = true; + try { + long count = 0; + // traverse and index workspace + executeAndLog(new Start(Action.INTERNAL_TRANSACTION)); + NodeState rootState = (NodeState) stateMgr.getItemState(rootId); + count = createIndex(rootState, rootPath, stateMgr, count); + checkIndexingQueue(true); + executeAndLog(new Commit(getTransactionId())); + log.debug("Created initial index for {} nodes", count); + releaseMultiReader(); + safeFlush(); + } catch (Exception e) { + String msg = "Error indexing workspace"; + IOException ex = new IOException(msg); + ex.initCause(e); + throw ex; + } finally { + reindexing = false; + scheduleFlushTask(); + } + } else { + throw new IllegalStateException("Index already present"); + } + } + + /** + * Atomically updates the index by removing some documents and adding + * others. + * + * @param remove collection of ids that identify documents to + * remove + * @param add collection of Documents to add. Some of the + * elements in this collection may be null, to + * indicate that a node could not be indexed successfully. + * @throws IOException if an error occurs while updating the index. + */ + synchronized void update( + Collection remove, Collection add) + throws IOException { + // make sure a reader is available during long updates + if (add.size() > handler.getBufferSize()) { + try { + getIndexReader().release(); + } catch (IOException e) { + // do not fail if an exception is thrown here + log.warn("unable to prepare index reader for queries during update", e); + } + } + + synchronized (updateMonitor) { + updateInProgress = true; + } + try { + long transactionId = nextTransactionId++; + executeAndLog(new Start(transactionId)); + + + long time = System.currentTimeMillis(); + for (NodeId id : remove) { + executeAndLog(new DeleteNode(transactionId, id)); + } + time = System.currentTimeMillis() - time; + log.debug("{} documents deleted in {}ms", remove.size(), time); + + time = System.currentTimeMillis(); + for (Document document : add) { + if (document != null) { + executeAndLog(new AddNode(transactionId, document)); + // commit volatile index if needed + checkVolatileCommit(); + } + } + time = System.currentTimeMillis() - time; + log.debug("{} documents added in {}ms", add.size(), time); + executeAndLog(new Commit(transactionId)); + } finally { + synchronized (updateMonitor) { + updateInProgress = false; + updateMonitor.notifyAll(); + releaseMultiReader(); + } + } + } + + /** + * Adds a document to the index. + * + * @param doc the document to add. + * @throws IOException if an error occurs while adding the document to the + * index. + */ + void addDocument(Document doc) throws IOException { + Collection empty = Collections.emptyList(); + update(empty, Collections.singleton(doc)); + } + + /** + * Deletes the first document that matches the id. + * + * @param id document that match this id will be deleted. + * @throws IOException if an error occurs while deleting the document. + */ + void removeDocument(NodeId id) throws IOException { + Collection empty = Collections.emptyList(); + update(Collections.singleton(id), empty); + } + + /** + * Deletes all documents that match the id. + * + * @param id documents that match this id will be deleted. + * @return the number of deleted documents. + * @throws IOException if an error occurs while deleting documents. + */ + synchronized int removeAllDocuments(NodeId id) throws IOException { + synchronized (updateMonitor) { + updateInProgress = true; + } + int num; + try { + Term idTerm = TermFactory.createUUIDTerm(id.toString()); + executeAndLog(new Start(Action.INTERNAL_TRANSACTION)); + num = volatileIndex.removeDocument(idTerm); + if (num > 0) { + redoLog.append(new DeleteNode(getTransactionId(), id)); + } + for (PersistentIndex index : indexes) { + // only remove documents from registered indexes + if (indexNames.contains(index.getName())) { + int removed = index.removeDocument(idTerm); + if (removed > 0) { + redoLog.append(new DeleteNode(getTransactionId(), id)); + } + num += removed; + } + } + executeAndLog(new Commit(getTransactionId())); + } finally { + synchronized (updateMonitor) { + updateInProgress = false; + updateMonitor.notifyAll(); + releaseMultiReader(); + } + } + return num; + } + + /** + * Returns IndexReaders for the indexes named + * indexNames. An IndexListener is registered and + * notified when documents are deleted from one of the indexes in + * indexNames. + *

    + * Note: the number of IndexReaders returned by this method is + * not necessarily the same as the number of index names passed. An index + * might have been deleted and is not reachable anymore. + * + * @param indexNames the names of the indexes for which to obtain readers. + * @param listener the listener to notify when documents are deleted. + * @return the IndexReaders. + * @throws IOException if an error occurs acquiring the index readers. + */ + synchronized IndexReader[] getIndexReaders( + String[] indexNames, IndexListener listener) throws IOException { + Set names = new HashSet(Arrays.asList(indexNames)); + Map indexReaders = + new HashMap(); + + try { + for (PersistentIndex index : indexes) { + if (names.contains(index.getName())) { + indexReaders.put(index.getReadOnlyIndexReader(listener), index); + } + } + } catch (IOException e) { + // release readers obtained so far + for (Map.Entry entry + : indexReaders.entrySet()) { + try { + entry.getKey().release(); + } catch (IOException ex) { + log.warn("Exception releasing index reader", ex); + } + entry.getValue().resetListener(); + } + throw e; + } + + return indexReaders.keySet().toArray(new IndexReader[indexReaders.size()]); + } + + /** + * Creates a new Persistent index. The new index is not registered with this + * MultiIndex. + * + * @param indexName the name of the index to open, or null if + * an index with a new name should be created. + * @return a new PersistentIndex. + * @throws IOException if a new index cannot be created. + */ + synchronized PersistentIndex getOrCreateIndex(String indexName) + throws IOException { + // check existing + for (PersistentIndex idx : indexes) { + if (idx.getName().equals(indexName)) { + return idx; + } + } + + // otherwise open / create it + if (indexName == null) { + do { + indexName = indexNames.newName(); + } while (directoryManager.hasDirectory(indexName)); + } + PersistentIndex index; + try { + index = new PersistentIndex(indexName, + handler.getTextAnalyzer(), handler.getSimilarity(), + cache, indexingQueue, directoryManager, + handler.getMaxHistoryAge()); + } catch (IOException e) { + // do some clean up + if (!directoryManager.delete(indexName)) { + deletable.put(indexName, Long.MIN_VALUE); + } + throw e; + } + index.setUseCompoundFile(handler.getUseCompoundFile()); + index.setTermInfosIndexDivisor(handler.getTermInfosIndexDivisor()); + + // add to list of open indexes and return it + indexes.add(index); + return index; + } + + /** + * Returns true if this multi index has an index segment with + * the given name. This method even returns true if an index + * segments has not yet been loaded / initialized but exists on disk. + * + * @param indexName the name of the index segment. + * @return true if it exists; otherwise false. + * @throws IOException if an error occurs while checking existence of + * directory. + */ + synchronized boolean hasIndex(String indexName) throws IOException { + // check existing + for (PersistentIndex idx : indexes) { + if (idx.getName().equals(indexName)) { + return true; + } + } + // check if it exists on disk + return directoryManager.hasDirectory(indexName); + } + + /** + * Replaces the indexes with names obsoleteIndexes with + * index. Documents that must be deleted in index + * can be identified with Terms in deleted. + * + * @param obsoleteIndexes the names of the indexes to replace. + * @param index the new index that is the result of a merge of the + * indexes to replace. + * @param deleted Terms that identify documents that must be + * deleted in index. + * @throws IOException if an exception occurs while replacing the indexes. + */ + void replaceIndexes(String[] obsoleteIndexes, + PersistentIndex index, + Collection deleted) + throws IOException { + + if (handler.isInitializeHierarchyCache()) { + // force initializing of caches + long time = System.currentTimeMillis(); + index.getReadOnlyIndexReader(true).release(); + time = System.currentTimeMillis() - time; + log.debug("hierarchy cache initialized in {} ms", time); + } + + synchronized (this) { + synchronized (updateMonitor) { + updateInProgress = true; + } + try { + // if we are reindexing there is already an active transaction + if (!reindexing) { + executeAndLog(new Start(Action.INTERNAL_TRANS_REPL_INDEXES)); + } + // delete obsolete indexes + Set names = new HashSet(Arrays.asList(obsoleteIndexes)); + for (String indexName : names) { + // do not try to delete indexes that are already gone + if (indexNames.contains(indexName)) { + executeAndLog(new DeleteIndex(getTransactionId(), indexName)); + } + } + + // Index merger does not log an action when it creates the target + // index of the merge. We have to do this here. + executeAndLog(new CreateIndex(getTransactionId(), index.getName())); + + executeAndLog(new AddIndex(getTransactionId(), index.getName())); + + // delete documents in index + for (Term id : deleted) { + index.removeDocument(id); + } + index.commit(); + + if (!reindexing) { + // only commit if we are not reindexing + // when reindexing the final commit is done at the very end + executeAndLog(new Commit(getTransactionId())); + } + } finally { + synchronized (updateMonitor) { + updateInProgress = false; + updateMonitor.notifyAll(); + releaseMultiReader(); + } + } + } + if (reindexing) { + // do some cleanup right away when reindexing + attemptDelete(); + } + } + + /** + * Returns an read-only IndexReader that spans alls indexes of this + * MultiIndex. + * + * @return an IndexReader. + * @throws IOException if an error occurs constructing the IndexReader. + */ + public CachingMultiIndexReader getIndexReader() throws IOException { + return getIndexReader(false); + } + + /** + * Returns an read-only IndexReader that spans alls indexes of this + * MultiIndex. + * + * @param initCache when set true the hierarchy cache is + * completely initialized before this call returns. + * @return an IndexReader. + * @throws IOException if an error occurs constructing the IndexReader. + */ + public synchronized CachingMultiIndexReader getIndexReader(boolean initCache) throws IOException { + synchronized (updateMonitor) { + if (multiReader != null) { + multiReader.acquire(); + return multiReader; + } + // no reader available + // wait until no update is in progress + while (updateInProgress) { + try { + updateMonitor.wait(); + } catch (InterruptedException e) { + throw new IOException("Interrupted while waiting to aquire reader"); + } + } + // some other read thread might have created the reader in the + // meantime -> check again + if (multiReader == null) { + List readerList = + new ArrayList(); + for (PersistentIndex pIdx : indexes) { + if (indexNames.contains(pIdx.getName())) { + readerList.add(pIdx.getReadOnlyIndexReader(initCache)); + } + } + readerList.add(volatileIndex.getReadOnlyIndexReader()); + ReadOnlyIndexReader[] readers = + readerList.toArray(new ReadOnlyIndexReader[readerList.size()]); + multiReader = new CachingMultiIndexReader(readers, cache); + } + multiReader.acquire(); + return multiReader; + } + } + + /** + * Returns the volatile index. + * + * @return the volatile index. + */ + VolatileIndex getVolatileIndex() { + return volatileIndex; + } + + /** + * Runs a consistency check on this multi index. + * + * @return the consistency check. + * @throws IOException if an error occurs while running the check. + */ + ConsistencyCheck runConsistencyCheck() throws IOException { + return ConsistencyCheck.run(this, handler, excludedIDs); + } + + /** + * Closes this MultiIndex. + */ + void close() { + + // stop index merger + // when calling this method we must not lock this MultiIndex, otherwise + // a deadlock might occur + merger.dispose(); + + synchronized (this) { + // stop timer + unscheduleFlushTask(); + + // commit / close indexes + try { + releaseMultiReader(); + } catch (IOException e) { + log.error("Exception while closing search index.", e); + } + try { + flush(); + } catch (IOException e) { + log.error("Exception while closing search index.", e); + } + volatileIndex.close(); + for (PersistentIndex index : indexes) { + index.close(); + } + + // close indexing queue + indexingQueue.close(); + + // finally close directory + try { + indexDir.close(); + } catch (IOException e) { + log.error("Exception while closing directory.", e); + } + } + } + + /** + * Returns the namespace mappings of this search index. + * @return the namespace mappings of this search index. + */ + NamespaceMappings getNamespaceMappings() { + return nsMappings; + } + + /** + * Returns the indexing queue for this multi index. + * @return the indexing queue for this multi index. + */ + IndexingQueue getIndexingQueue() { + return indexingQueue; + } + + /** + * @return the base directory of the index. + */ + Directory getDirectory() { + return indexDir; + } + + /** + * @return the current generation of the index names. + */ + long getIndexGeneration() { + return indexNames.getGeneration(); + } + + /** + * Returns a lucene Document for the node. + * + * @param node the node to index. + * @return the index document. + * @throws RepositoryException if an error occurs while reading from the + * workspace. + */ + Document createDocument(NodeState node) throws RepositoryException { + return handler.createDocument(node, nsMappings, version); + } + + /** + * Returns a lucene Document for the Node with id. + * + * @param id the id of the node to index. + * @return the index document. + * @throws RepositoryException if an error occurs while reading from the + * workspace or if there is no node with + * id. + */ + Document createDocument(NodeId id) throws RepositoryException { + try { + NodeState state = (NodeState) handler.getContext().getItemStateManager().getItemState(id); + return createDocument(state); + } catch (NoSuchItemStateException e) { + throw new RepositoryException("Node " + id + " does not exist", e); + } catch (ItemStateException e) { + throw new RepositoryException("Error retrieving node: " + id, e); + } + } + + /** + * Returns true if the redo log contained entries while + * this index was instantiated; false otherwise. + * @return true if the redo log contained entries. + */ + boolean getRedoLogApplied() { + return redoLogApplied; + } + + /** + * Removes the index from the list of active sub indexes. + * Depending on the {@link SearchIndex#getMaxHistoryAge()}, the + * Index is not deleted right away. + *

    + * This method does not close the index, but rather expects that the index + * has already been closed. + * + * @param index the index to delete. + */ + synchronized void deleteIndex(PersistentIndex index) { + // remove it from the lists if index is registered + indexes.remove(index); + indexNames.removeName(index.getName()); + synchronized (deletable) { + log.debug("Moved " + index.getName() + " to deletable"); + deletable.put(index.getName(), System.currentTimeMillis()); + } + } + + /** + * Flushes this MultiIndex. Persists all pending changes and + * resets the redo log. + * + * @throws IOException if the flush fails. + */ + private void flush() throws IOException { + synchronized (this) { + + // only start transaction when there is something to commit + boolean transactionStarted = false; + + if (volatileIndex.getNumDocuments() > 0) { + // commit volatile index + executeAndLog(new Start(Action.INTERNAL_TRANSACTION)); + transactionStarted = true; + commitVolatileIndex(); + } + + boolean indexesModified = false; + // commit persistent indexes + for (int i = indexes.size() - 1; i >= 0; i--) { + PersistentIndex index = indexes.get(i); + // only commit indexes we own + // index merger also places PersistentIndex instances in indexes, + // but does not make them public by registering the name in indexNames + if (indexNames.contains(index.getName())) { + long gen = index.getCurrentGeneration(); + index.commit(); + if (gen != index.getCurrentGeneration()) { + indexesModified = true; + log.debug("Committed revision {} of index {}", + Long.toString(index.getCurrentGeneration(), Character.MAX_RADIX), + index.getName()); + } + // check if index still contains documents + if (index.getNumDocuments() == 0) { + if (!transactionStarted) { + executeAndLog(new Start(Action.INTERNAL_TRANSACTION)); + transactionStarted = true; + } + executeAndLog(new DeleteIndex(getTransactionId(), index.getName())); + } + } + } + + if (transactionStarted) { + executeAndLog(new Commit(getTransactionId())); + } + + if (transactionStarted || indexesModified || redoLog.hasEntries()) { + indexNames.write(); + + indexHistory.addIndexInfos(indexNames); + + // close redo.log and create a new one based + // on the new indexNames generation + redoLog.close(); + redoLog = redoLogFactory.createRedoLog(this); + } + + lastFlushTime = System.currentTimeMillis(); + } + + indexHistory.pruneOutdated(); + + // delete obsolete indexes + attemptDelete(); + } + + /** + * Releases the {@link #multiReader} and sets it null. If the + * reader is already null this method does nothing. When this + * method returns {@link #multiReader} is guaranteed to be null + * even if an exception is thrown. + *

    + * Please note that this method does not take care of any synchronization. + * A caller must ensure that it is the only thread operating on this multi + * index, or that it holds the {@link #updateMonitor}. + * + * @throws IOException if an error occurs while releasing the reader. + */ + void releaseMultiReader() throws IOException { + if (multiReader != null) { + try { + multiReader.release(); + } finally { + multiReader = null; + } + } + } + + //-------------------------< testing only >--------------------------------- + + void waitUntilIndexingQueueIsEmpty() { + IndexingQueue iq = getIndexingQueue(); + synchronized (iq) { + while (iq.getNumPendingDocuments() > 0 || indexingQueueCommitPending) { + try { + log.debug( + "waiting for indexing queue to become empty. {} pending docs.", + iq.getNumPendingDocuments()); + iq.wait(); + log.debug("notified"); + } catch (InterruptedException e) { + // interrupted, check again if queue is empty + } + } + } + } + + void notifyIfIndexingQueueIsEmpty() { + IndexingQueue iq = getIndexingQueue(); + synchronized (iq) { + indexingQueueCommitPending = false; + if (iq.getNumPendingDocuments() == 0) { + iq.notifyAll(); + } + } + } + + //-------------------------< internal >------------------------------------- + + /** + * Enqueues unused segments for deletion in {@link #deletable}. This method + * does not synchronize on {@link #deletable}! A caller must ensure that it + * is the only one acting on the {@link #deletable} map. + * + * @throws IOException if an error occurs while reading directories. + */ + private void enqueueUnusedSegments() throws IOException { + // walk through index segments + for (String name : directoryManager.getDirectoryNames()) { + if (!name.startsWith("_")) { + continue; + } + long lastUse = indexHistory.getLastUseOf(name); + if (lastUse != Long.MAX_VALUE) { + if (log.isDebugEnabled()) { + String msg = "Segment " + name + " not is use anymore. "; + if (lastUse != Long.MIN_VALUE) { + Calendar cal = Calendar.getInstance(); + DateFormat df = DateFormat.getInstance(); + cal.setTimeInMillis(lastUse); + msg += "Unused since: " + df.format(cal.getTime()); + } else { + msg += "(orphaned)"; + } + log.debug(msg); + } + deletable.put(name, lastUse); + } + } + // now prune outdated index infos + indexHistory.pruneOutdated(); + } + + /** + * Schedules a background task for flushing the index once per second. + */ + private void scheduleFlushTask() { + ScheduledExecutorService executor = handler.getContext().getExecutor(); + flushTask = executor.scheduleWithFixedDelay(new Runnable() { + public void run() { + // check if there are any indexing jobs finished + checkIndexingQueue(false); + // check if volatile index should be flushed + checkFlush(); + } + }, 1, 1, TimeUnit.SECONDS); + } + + /** + * Cancels the scheduled background index flush task. + */ + private void unscheduleFlushTask() { + if (flushTask != null) { + flushTask.cancel(false); + flushTask = null; + } + } + + /** + * Resets the volatile index to a new instance. + * + * @throws IOException if the volatile index cannot be reset. + */ + private void resetVolatileIndex() throws IOException { + // JCR-3227 close VolatileIndex properly + if (volatileIndex != null) { + volatileIndex.close(); + } + volatileIndex = new VolatileIndex(handler.getTextAnalyzer(), + handler.getSimilarity(), indexingQueue); + volatileIndex.setUseCompoundFile(handler.getUseCompoundFile()); + volatileIndex.setBufferSize(handler.getBufferSize()); + } + + /** + * Returns the current transaction id. + * + * @return the current transaction id. + */ + private long getTransactionId() { + return currentTransactionId; + } + + /** + * Executes action a and appends the action to the redo log if + * successful. + * + * @param a the Action to execute. + * @return the executed action. + * @throws IOException if an error occurs while executing the action + * or appending the action to the redo log. + */ + private Action executeAndLog(Action a) + throws IOException { + a.execute(this); + redoLog.append(a); + // please note that flushing the redo log is only required on + // commit, but we also want to keep track of new indexes for sure. + // otherwise it might happen that unused index folders are orphaned + // after a crash. + if (a.getType() == Action.TYPE_COMMIT || a.getType() == Action.TYPE_ADD_INDEX) { + redoLog.flush(); + } + return a; + } + + /** + * Checks if it is needed to commit the volatile index according to {@link + * SearchIndex#getMaxVolatileIndexSize()}. + * + * @return true if the volatile index has been committed, + * false otherwise. + * @throws IOException if an error occurs while committing the volatile + * index. + */ + private boolean checkVolatileCommit() throws IOException { + if (volatileIndex.getRamSizeInBytes() >= handler.getMaxVolatileIndexSize()) { + commitVolatileIndex(); + return true; + } + return false; + } + + /** + * Commits the volatile index to a persistent index. The new persistent + * index is added to the list of indexes but not written to disk. When this + * method returns a new volatile index has been created. + * + * @throws IOException if an error occurs while writing the volatile index + * to disk. + */ + private void commitVolatileIndex() throws IOException { + + // check if volatile index contains documents at all + int volatileIndexDocuments = volatileIndex.getNumDocuments(); + if (volatileIndexDocuments > 0) { + + long time = System.currentTimeMillis(); + // create index + CreateIndex create = new CreateIndex(getTransactionId(), null); + executeAndLog(create); + + // commit volatile index + executeAndLog(new VolatileCommit(getTransactionId(), create.getIndexName())); + + // add new index + AddIndex add = new AddIndex(getTransactionId(), create.getIndexName()); + executeAndLog(add); + + // create new volatile index + resetVolatileIndex(); + + time = System.currentTimeMillis() - time; + log.debug("Committed in-memory index containing {} documents in {}ms.", volatileIndexDocuments, time); + } + } + + /** + * Recursively creates an index starting with the NodeState + * node. + * + * @param node the current NodeState. + * @param path the path of the current node state. + * @param stateMgr the shared item state manager. + * @param count the number of nodes already indexed. + * @return the number of nodes indexed so far. + * @throws IOException if an error occurs while writing to the + * index. + * @throws ItemStateException if an node state cannot be found. + * @throws RepositoryException if any other error occurs + */ + private long createIndex(NodeState node, + Path path, + ItemStateManager stateMgr, + long count) + throws IOException, ItemStateException, RepositoryException { + NodeId id = node.getNodeId(); + if (excludedIDs.contains(id)) { + return count; + } + executeAndLog(new AddNode(getTransactionId(), id)); + if (++count % 100 == 0) { + PathResolver resolver = new DefaultNamePathResolver( + handler.getContext().getNamespaceRegistry()); + log.info("indexing... {} ({})", resolver.getJCRPath(path), count); + } + if (count % 10 == 0) { + checkIndexingQueue(true); + } + checkVolatileCommit(); + for (ChildNodeEntry child : node.getChildNodeEntries()) { + Path childPath = PATH_FACTORY.create(path, child.getName(), + child.getIndex(), false); + NodeState childState = null; + try { + childState = (NodeState) stateMgr.getItemState(child.getId()); + } catch (NoSuchItemStateException e) { + handler.getOnWorkspaceInconsistencyHandler().handleMissingChildNode( + e, handler, path, node, child); + } catch (ItemStateException e) { + // JCR-3268 log bundle corruption and continue + handler.getOnWorkspaceInconsistencyHandler().logError(e, + handler, childPath, node, child); + } + if (childState != null) { + count = createIndex(childState, childPath, stateMgr, count); + } + } + return count; + } + + /** + * Attempts to delete all files that are older than + *{@link SearchIndex#getMaxHistoryAge()}. + */ + private void attemptDelete() { + synchronized (deletable) { + for (Iterator> it = deletable.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + String indexName = entry.getKey(); + long lastUse = entry.getValue(); + if (System.currentTimeMillis() - handler.getMaxHistoryAge() * 1000 > lastUse) { + if (directoryManager.delete(indexName)) { + it.remove(); + } else { + // JCR-2705: We will retry later, so only a debug log + log.debug("Unable to delete obsolete index: {}", indexName); + } + } + } + } + } + + /** + * Removes the deletable file if it exists. The file is not used anymore + * in Jackrabbit versions >= 1.5. + */ + private void removeDeletable() { + String fileName = "deletable"; + try { + if (indexDir.fileExists(fileName)) { + indexDir.deleteFile(fileName); + } + } catch (IOException e) { + log.warn("Unable to remove file 'deletable'.", e); + } + } + + /** + * Checks the duration between the last commit to this index and the + * current time and flushes the index (if there are changes at all) + * if the duration (idle time) is more than {@link SearchIndex#getVolatileIdleTime()} + * seconds. + */ + private synchronized void checkFlush() { + long idleTime = System.currentTimeMillis() - lastFlushTime; + // do not flush if volatileIdleTime is zero or negative + if (handler.getVolatileIdleTime() > 0 + && idleTime > handler.getVolatileIdleTime() * 1000) { + try { + if (redoLog.hasEntries()) { + long time = System.currentTimeMillis(); + log.debug("Flushing index after being idle for " + + idleTime + " ms."); + safeFlush(); + time = System.currentTimeMillis() - time; + log.debug("Index flushed in " + time + " ms."); + } + } catch (IOException e) { + log.error("Unable to commit volatile index", e); + } + } + } + + void safeFlush() throws IOException{ + synchronized (updateMonitor) { + updateInProgress = true; + } + try { + flush(); + } finally { + synchronized (updateMonitor) { + updateInProgress = false; + updateMonitor.notifyAll(); + releaseMultiReader(); + } + } + } + + /** + * Checks the indexing queue for finished text extrator jobs and updates the + * index accordingly if there are any new ones. + * + * @param transactionPresent whether a transaction is in progress and the + * current {@link #getTransactionId()} should be + * used. If false a new transaction + * is created when documents are transfered from + * the indexing queue to the index. + */ + private void checkIndexingQueue(boolean transactionPresent) { + Map finished = new HashMap(); + for (Document document : indexingQueue.getFinishedDocuments()) { + NodeId id = new NodeId(document.get(FieldNames.UUID)); + finished.put(id, document); + } + + // now update index with the remaining ones if there are any + if (!finished.isEmpty()) { + log.debug("updating index with {} nodes from indexing queue.", + finished.size()); + + // Only useful for testing + synchronized (getIndexingQueue()) { + indexingQueueCommitPending = true; + } + + try { + // remove documents from the queue + for (NodeId id : finished.keySet()) { + indexingQueue.removeDocument(id.toString()); + } + + try { + if (transactionPresent) { + synchronized (this) { + for (NodeId id : finished.keySet()) { + executeAndLog(new DeleteNode(getTransactionId(), id)); + } + for (Document document : finished.values()) { + executeAndLog(new AddNode(getTransactionId(), document)); + } + } + } else { + update(finished.keySet(), finished.values()); + } + } catch (IOException e) { + // update failed + log.warn("Failed to update index with deferred text extraction", e); + } + } finally { + // the following method also resets + // indexingQueueCommitPending back to false + notifyIfIndexingQueueIsEmpty(); + } + } + } + + //------------------------< Actions >--------------------------------------- + + /** + * Defines an action on an MultiIndex. + */ + public abstract static class Action { + + /** + * Action identifier in redo log for transaction start action. + */ + static final String START = "STR"; + + /** + * Action type for start action. + */ + public static final int TYPE_START = 0; + + /** + * Action identifier in redo log for add node action. + */ + static final String ADD_NODE = "ADD"; + + /** + * Action type for add node action. + */ + public static final int TYPE_ADD_NODE = 1; + + /** + * Action identifier in redo log for node delete action. + */ + static final String DELETE_NODE = "DEL"; + + /** + * Action type for delete node action. + */ + public static final int TYPE_DELETE_NODE = 2; + + /** + * Action identifier in redo log for transaction commit action. + */ + static final String COMMIT = "COM"; + + /** + * Action type for commit action. + */ + public static final int TYPE_COMMIT = 3; + + /** + * Action identifier in redo log for volatile index commit action. + */ + static final String VOLATILE_COMMIT = "VOL_COM"; + + /** + * Action type for volatile index commit action. + */ + public static final int TYPE_VOLATILE_COMMIT = 4; + + /** + * Action identifier in redo log for index create action. + */ + static final String CREATE_INDEX = "CRE_IDX"; + + /** + * Action type for create index action. + */ + public static final int TYPE_CREATE_INDEX = 5; + + /** + * Action identifier in redo log for index add action. + */ + static final String ADD_INDEX = "ADD_IDX"; + + /** + * Action type for add index action. + */ + public static final int TYPE_ADD_INDEX = 6; + + /** + * Action identifier in redo log for delete index action. + */ + static final String DELETE_INDEX = "DEL_IDX"; + + /** + * Action type for delete index action. + */ + public static final int TYPE_DELETE_INDEX = 7; + + /** + * Transaction identifier for internal actions like volatile index + * commit triggered by timer thread. + */ + static final long INTERNAL_TRANSACTION = -1; + + /** + * Transaction identifier for internal action that replaces indexs. + */ + static final long INTERNAL_TRANS_REPL_INDEXES = -2; + + /** + * The id of the transaction that executed this action. + */ + private final long transactionId; + + /** + * The action type. + */ + private final int type; + + /** + * Creates a new Action. + * + * @param transactionId the id of the transaction that executed this + * action. + * @param type the action type. + */ + Action(long transactionId, int type) { + this.transactionId = transactionId; + this.type = type; + } + + /** + * Returns the transaction id for this Action. + * + * @return the transaction id for this Action. + */ + long getTransactionId() { + return transactionId; + } + + /** + * Returns the action type. + * + * @return the action type. + */ + int getType() { + return type; + } + + /** + * Executes this action on the index. + * + * @param index the index where to execute the action. + * @throws IOException if the action fails due to some I/O error in + * the index or some other error. + */ + public abstract void execute(MultiIndex index) throws IOException; + + /** + * Executes the inverse operation of this action. That is, does an undo + * of this action. This default implementation does nothing, but returns + * silently. + * + * @param index the index where to undo the action. + * @throws IOException if the action cannot be undone. + */ + public void undo(MultiIndex index) throws IOException { + } + + /** + * Returns a String representation of this action that can be + * written to the {@link RedoLog}. + * + * @return a String representation of this action. + */ + public abstract String toString(); + + /** + * Parses an line in the redo log and created an {@link Action}. + * + * @param line the line from the redo log. + * @return an Action. + * @throws IllegalArgumentException if the line is malformed. + */ + static Action fromString(String line) throws IllegalArgumentException { + int endTransIdx = line.indexOf(' '); + if (endTransIdx == -1) { + throw new IllegalArgumentException(line); + } + long transactionId; + try { + transactionId = Long.parseLong(line.substring(0, endTransIdx)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(line); + } + int endActionIdx = line.indexOf(' ', endTransIdx + 1); + if (endActionIdx == -1) { + // action does not have arguments + endActionIdx = line.length(); + } + String actionLabel = line.substring(endTransIdx + 1, endActionIdx); + String arguments = ""; + if (endActionIdx + 1 <= line.length()) { + arguments = line.substring(endActionIdx + 1); + } + Action a; + if (actionLabel.equals(Action.ADD_NODE)) { + a = AddNode.fromString(transactionId, arguments); + } else if (actionLabel.equals(Action.ADD_INDEX)) { + a = AddIndex.fromString(transactionId, arguments); + } else if (actionLabel.equals(Action.COMMIT)) { + a = Commit.fromString(transactionId, arguments); + } else if (actionLabel.equals(Action.CREATE_INDEX)) { + a = CreateIndex.fromString(transactionId, arguments); + } else if (actionLabel.equals(Action.DELETE_INDEX)) { + a = DeleteIndex.fromString(transactionId, arguments); + } else if (actionLabel.equals(Action.DELETE_NODE)) { + a = DeleteNode.fromString(transactionId, arguments); + } else if (actionLabel.equals(Action.START)) { + a = Start.fromString(transactionId, arguments); + } else if (actionLabel.equals(Action.VOLATILE_COMMIT)) { + a = VolatileCommit.fromString(transactionId, arguments); + } else { + throw new IllegalArgumentException(line); + } + return a; + } + } + + /** + * Adds an index to the MultiIndex's active persistent index list. + */ + private static class AddIndex extends Action { + + /** + * The name of the index to add. + */ + private String indexName; + + /** + * Creates a new AddIndex action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param indexName the name of the index to add, or null + * if an index with a new name should be created. + */ + AddIndex(long transactionId, String indexName) { + super(transactionId, Action.TYPE_ADD_INDEX); + this.indexName = indexName; + } + + /** + * Creates a new AddIndex action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param arguments the name of the index to add. + * @return the AddIndex action. + * @throws IllegalArgumentException if the arguments are malformed. + */ + static AddIndex fromString(long transactionId, String arguments) { + return new AddIndex(transactionId, arguments); + } + + /** + * Adds a sub index to index. + * + * @inheritDoc + */ + public void execute(MultiIndex index) throws IOException { + PersistentIndex idx = index.getOrCreateIndex(indexName); + if (!index.indexNames.contains(indexName)) { + index.indexNames.addName(indexName, idx.getCurrentGeneration()); + // now that the index is in the active list let the merger know about it + index.merger.indexAdded(indexName, idx.getNumDocuments()); + } + } + + /** + * @inheritDoc + */ + public String toString() { + StringBuffer logLine = new StringBuffer(); + logLine.append(Long.toString(getTransactionId())); + logLine.append(' '); + logLine.append(Action.ADD_INDEX); + logLine.append(' '); + logLine.append(indexName); + return logLine.toString(); + } + } + + /** + * Adds a node to the index. + */ + private static class AddNode extends Action { + + /** + * The maximum length of a AddNode String. + */ + private static final int ENTRY_LENGTH = + Long.toString(Long.MAX_VALUE).length() + Action.ADD_NODE.length() + + new NodeId(0, 0).toString().length() + 2; + + /** + * The id of the node to add. + */ + private final NodeId id; + + /** + * The document to add to the index, or null if not available. + */ + private Document doc; + + /** + * Creates a new AddNode action. + * + * @param transactionId the id of the transaction that executes this action. + * @param id the id of the node to add. + */ + AddNode(long transactionId, NodeId id) { + super(transactionId, Action.TYPE_ADD_NODE); + this.id = id; + } + + /** + * Creates a new AddNode action. + * + * @param transactionId the id of the transaction that executes this action. + * @param doc the document to add. + */ + AddNode(long transactionId, Document doc) { + this(transactionId, new NodeId(doc.get(FieldNames.UUID))); + this.doc = doc; + } + + /** + * Creates a new AddNode action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param arguments The UUID of the node to add + * @return the AddNode action. + * @throws IllegalArgumentException if the arguments are malformed. Not a + * UUID. + */ + static AddNode fromString(long transactionId, String arguments) + throws IllegalArgumentException { + return new AddNode(transactionId, new NodeId(arguments)); + } + + /** + * Adds a node to the index. + * + * @inheritDoc + */ + public void execute(MultiIndex index) throws IOException { + if (doc == null) { + try { + doc = index.createDocument(id); + } catch (RepositoryException e) { + // node does not exist anymore + log.debug(e.getMessage()); + } + } + if (doc != null) { + index.volatileIndex.addDocuments(new Document[]{doc}); + } + } + + /** + * @inheritDoc + */ + public String toString() { + StringBuffer logLine = new StringBuffer(ENTRY_LENGTH); + logLine.append(Long.toString(getTransactionId())); + logLine.append(' '); + logLine.append(Action.ADD_NODE); + logLine.append(' '); + logLine.append(id); + return logLine.toString(); + } + } + + /** + * Commits a transaction. + */ + private static class Commit extends Action { + + /** + * Creates a new Commit action. + * + * @param transactionId the id of the transaction that is committed. + */ + Commit(long transactionId) { + super(transactionId, Action.TYPE_COMMIT); + } + + /** + * Creates a new Commit action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param arguments ignored by this method. + * @return the Commit action. + */ + static Commit fromString(long transactionId, String arguments) { + return new Commit(transactionId); + } + + /** + * Touches the last flush time (sets it to the current time). + * + * @inheritDoc + */ + public void execute(MultiIndex index) throws IOException { + index.lastFlushTime = System.currentTimeMillis(); + } + + /** + * @inheritDoc + */ + public String toString() { + return Long.toString(getTransactionId()) + ' ' + Action.COMMIT; + } + } + + /** + * Creates an new sub index but does not add it to the active persistent index + * list. + */ + private static class CreateIndex extends Action { + + /** + * The name of the index to add. + */ + private String indexName; + + /** + * Creates a new CreateIndex action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param indexName the name of the index to add, or null + * if an index with a new name should be created. + */ + CreateIndex(long transactionId, String indexName) { + super(transactionId, Action.TYPE_CREATE_INDEX); + this.indexName = indexName; + } + + /** + * Creates a new CreateIndex action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param arguments the name of the index to create. + * @return the AddIndex action. + * @throws IllegalArgumentException if the arguments are malformed. + */ + static CreateIndex fromString(long transactionId, String arguments) { + // when created from String, this action is executed as redo action + return new CreateIndex(transactionId, arguments); + } + + /** + * Creates a new index. + * + * @inheritDoc + */ + public void execute(MultiIndex index) throws IOException { + PersistentIndex idx = index.getOrCreateIndex(indexName); + indexName = idx.getName(); + } + + /** + * @inheritDoc + */ + public void undo(MultiIndex index) throws IOException { + if (index.hasIndex(indexName)) { + PersistentIndex idx = index.getOrCreateIndex(indexName); + idx.close(); + index.deleteIndex(idx); + } + } + + /** + * @inheritDoc + */ + public String toString() { + StringBuffer logLine = new StringBuffer(); + logLine.append(Long.toString(getTransactionId())); + logLine.append(' '); + logLine.append(Action.CREATE_INDEX); + logLine.append(' '); + logLine.append(indexName); + return logLine.toString(); + } + + /** + * Returns the index name that has been created. If this method is called + * before {@link #execute(MultiIndex)} it will return null. + * + * @return the name of the index that has been created. + */ + String getIndexName() { + return indexName; + } + } + + /** + * Closes and deletes an index that is no longer in use. + */ + private static class DeleteIndex extends Action { + + /** + * The name of the index to add. + */ + private String indexName; + + /** + * Creates a new DeleteIndex action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param indexName the name of the index to delete. + */ + DeleteIndex(long transactionId, String indexName) { + super(transactionId, Action.TYPE_DELETE_INDEX); + this.indexName = indexName; + } + + /** + * Creates a new DeleteIndex action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param arguments the name of the index to delete. + * @return the DeleteIndex action. + * @throws IllegalArgumentException if the arguments are malformed. + */ + static DeleteIndex fromString(long transactionId, String arguments) { + return new DeleteIndex(transactionId, arguments); + } + + /** + * Removes a sub index from index. + * + * @inheritDoc + */ + public void execute(MultiIndex index) throws IOException { + // get index if it exists + for (PersistentIndex idx : index.indexes) { + if (idx.getName().equals(indexName)) { + idx.close(); + index.deleteIndex(idx); + break; + } + } + } + + /** + * @inheritDoc + */ + public String toString() { + StringBuffer logLine = new StringBuffer(); + logLine.append(Long.toString(getTransactionId())); + logLine.append(' '); + logLine.append(Action.DELETE_INDEX); + logLine.append(' '); + logLine.append(indexName); + return logLine.toString(); + } + } + + /** + * Deletes a node from the index. + */ + private static class DeleteNode extends Action { + + /** + * The maximum length of a DeleteNode String. + */ + private static final int ENTRY_LENGTH = + Long.toString(Long.MAX_VALUE).length() + Action.DELETE_NODE.length() + + new NodeId(0, 0).toString().length() + 2; + + /** + * The id of the node to remove. + */ + private final NodeId id; + + /** + * Creates a new DeleteNode action. + * + * @param transactionId the id of the transaction that executes this action. + * @param id the id of the node to delete. + */ + DeleteNode(long transactionId, NodeId id) { + super(transactionId, Action.TYPE_DELETE_NODE); + this.id = id; + } + + /** + * Creates a new DeleteNode action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param arguments the UUID of the node to delete. + * @return the DeleteNode action. + * @throws IllegalArgumentException if the arguments are malformed. Not a + * UUID. + */ + static DeleteNode fromString(long transactionId, String arguments) { + return new DeleteNode(transactionId, new NodeId(arguments)); + } + + /** + * Deletes a node from the index. + * + * @inheritDoc + */ + public void execute(MultiIndex index) throws IOException { + String uuidString = id.toString(); + // check if indexing queue is still working on + // this node from a previous update + Document doc = index.indexingQueue.removeDocument(uuidString); + if (doc != null) { + Util.disposeDocument(doc); + index.notifyIfIndexingQueueIsEmpty(); + } + Term idTerm = TermFactory.createUUIDTerm(uuidString); + // if the document cannot be deleted from the volatile index + // delete it from one of the persistent indexes. + int num = index.volatileIndex.removeDocument(idTerm); + if (num == 0) { + for (int i = index.indexes.size() - 1; i >= 0; i--) { + // only look in registered indexes + PersistentIndex idx = index.indexes.get(i); + if (index.indexNames.contains(idx.getName())) { + num = idx.removeDocument(idTerm); + if (num > 0) { + return; + } + } + } + } + } + + /** + * @inheritDoc + */ + public String toString() { + StringBuffer logLine = new StringBuffer(ENTRY_LENGTH); + logLine.append(Long.toString(getTransactionId())); + logLine.append(' '); + logLine.append(Action.DELETE_NODE); + logLine.append(' '); + logLine.append(id); + return logLine.toString(); + } + } + + /** + * Starts a transaction. + */ + private static class Start extends Action { + + /** + * Creates a new Start transaction action. + * + * @param transactionId the id of the transaction that started. + */ + Start(long transactionId) { + super(transactionId, Action.TYPE_START); + } + + /** + * Creates a new Start action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param arguments ignored by this method. + * @return the Start action. + */ + static Start fromString(long transactionId, String arguments) { + return new Start(transactionId); + } + + /** + * Sets the current transaction id on index. + * + * @inheritDoc + */ + public void execute(MultiIndex index) throws IOException { + index.currentTransactionId = getTransactionId(); + } + + /** + * @inheritDoc + */ + public String toString() { + return Long.toString(getTransactionId()) + ' ' + Action.START; + } + } + + /** + * Commits the volatile index to disk. + */ + private static class VolatileCommit extends Action { + + /** + * The name of the target index to commit to. + */ + private final String targetIndex; + + /** + * Creates a new VolatileCommit action. + * + * @param transactionId the id of the transaction that executes this action. + * @param targetIndex the name of the index where the volatile index + * will be committed. + */ + VolatileCommit(long transactionId, String targetIndex) { + super(transactionId, Action.TYPE_VOLATILE_COMMIT); + this.targetIndex = targetIndex; + } + + /** + * Creates a new VolatileCommit action. + * + * @param transactionId the id of the transaction that executes this + * action. + * @param arguments ignored by this implementation. + * @return the VolatileCommit action. + */ + static VolatileCommit fromString(long transactionId, String arguments) { + return new VolatileCommit(transactionId, arguments); + } + + /** + * Commits the volatile index to disk. + * + * @inheritDoc + */ + public void execute(MultiIndex index) throws IOException { + VolatileIndex volatileIndex = index.getVolatileIndex(); + PersistentIndex persistentIndex = index.getOrCreateIndex(targetIndex); + persistentIndex.copyIndex(volatileIndex); + index.resetVolatileIndex(); + } + + /** + * @inheritDoc + */ + public String toString() { + StringBuffer logLine = new StringBuffer(); + logLine.append(Long.toString(getTransactionId())); + logLine.append(' '); + logLine.append(Action.VOLATILE_COMMIT); + logLine.append(' '); + logLine.append(targetIndex); + return logLine.toString(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiIndexReader.java new file mode 100644 index 00000000000..f382c00bc0f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiIndexReader.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.core.query.lucene; + +import org.apache.lucene.index.IndexReader; +import org.apache.jackrabbit.core.id.NodeId; + +import java.io.IOException; + +/** + * MultiIndexReader exposes methods to get access to the contained + * {@link IndexReader}s of this MultiIndexReader. + */ +public interface MultiIndexReader extends ReleaseableIndexReader { + + /** + * @return the IndexReaders that are contained in this + * MultiIndexReader. + */ + IndexReader[] getIndexReaders(); + + /** + * Creates a document id for the given node identifier. + * + * @param id the id of the node. + * @return a foreign segment doc id or null if there is no node + * with the given id. + * @throws IOException if an error occurs while reading from the index. + */ + ForeignSegmentDocId createDocId(NodeId id) throws IOException; + + /** + * Returns the document number for the passed docId. If the id + * is invalid -1 is returned. + * + * @param docId the document id to resolve. + * @return the document number or -1 if it is invalid (e.g. + * does not exist). + * @throws IOException if an error occurs while reading from the index. + */ + int getDocumentNumber(ForeignSegmentDocId docId) throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiScorer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiScorer.java new file mode 100644 index 00000000000..f882eb23c4b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/MultiScorer.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.core.query.lucene; + +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Similarity; + +import java.io.IOException; + +/** + * MultiScorer spans multiple Scorers and returns document numbers + * and score values in the order as supplied to the constructor of this + * MultiScorer. + */ +class MultiScorer extends Scorer { + + /** + * The sub scorers. + */ + private final Scorer[] scorers; + + /** + * The document start numbers of the sub scorers. + */ + private final int[] starts; + + /** + * Index of the current scorer. + */ + private int currentScorer; + + /** + * The next document id to be returned + */ + private int currentDoc = -1; + + /** + * Creates a new MultiScorer that spans multiple + * scorers. + * + * @param similarity the similarity implementation that should be use. + * @param scorers the sub scorers. + * @param starts the document number start for each sub scorer. + */ + MultiScorer(Similarity similarity, Scorer[] scorers, int[] starts) { + super(similarity); + this.scorers = scorers; + this.starts = starts; + } + + @Override + public int nextDoc() throws IOException { + while (currentDoc != NO_MORE_DOCS) { + if (scorers[currentScorer] != null && scorers[currentScorer].nextDoc() != NO_MORE_DOCS) { + currentDoc = scorers[currentScorer].docID() + starts[currentScorer]; + return currentDoc; + } else if (++currentScorer < scorers.length) { + // advance to next scorer + } else { + // no more scorers + currentDoc = NO_MORE_DOCS; + } + } + return currentDoc; + } + + @Override + public int docID() { + return currentDoc; + } + + @Override + public float score() throws IOException { + return scorers[currentScorer].score(); + } + + @Override + public int advance(int target) throws IOException { + if (currentDoc == NO_MORE_DOCS) { + return currentDoc; + } + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + // exhaust all the internal scorers + for (Scorer s : scorers) { + if (s.docID() != target) { + s.advance(target); + } + } + currentDoc = NO_MORE_DOCS; + return currentDoc; + } + + currentScorer = scorerIndex(target); + if (scorers[currentScorer].advance(target - starts[currentScorer]) != NO_MORE_DOCS) { + currentDoc = scorers[currentScorer].docID() + starts[currentScorer]; + return currentDoc; + } else { + if (++currentScorer < scorers.length) { + // simply move to the next if there is any + currentDoc = nextDoc(); + return currentDoc; + } else { + // no more document + currentDoc = NO_MORE_DOCS; + return currentDoc; + } + } + } + + //--------------------------< internal >------------------------------------ + + /** + * Returns the scorer index for document n. + * Implementation copied from lucene MultiReader class. + * + * @param n document number. + * @return the scorer index. + */ + private int scorerIndex(int n) { + int lo = 0; // search starts array + int hi = scorers.length - 1; // for first element less + + while (hi >= lo) { + int mid = (lo + hi) >> 1; + int midValue = starts[mid]; + if (n < midValue) { + hi = mid - 1; + } else if (n > midValue) { + lo = mid + 1; + } else { // found a match + while (mid + 1 < scorers.length && starts[mid + 1] == midValue) { + mid++; // scan to last match + } + return mid; + } + } + return hi; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NSRegistryBasedNamespaceMappings.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NSRegistryBasedNamespaceMappings.java new file mode 100644 index 00000000000..3f50bdcffec --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NSRegistryBasedNamespaceMappings.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.core.query.lucene; + +import org.apache.jackrabbit.core.NamespaceRegistryImpl; + +import javax.jcr.NamespaceException; + +/** + * NSRegistryBasedNamespaceMappings implements a namespace mapping + * based on the stable index prefix provided by the namespace registry. + */ +public class NSRegistryBasedNamespaceMappings extends AbstractNamespaceMappings { + + /** + * The namespace registry. + */ + private final NamespaceRegistryImpl nsReg; + + /** + * Creates a new NSRegistryBasedNamespaceMappings. + * + * @param nsReg the namespace registry of the repository. + */ + NSRegistryBasedNamespaceMappings(NamespaceRegistryImpl nsReg) { + this.nsReg = nsReg; + } + + //-------------------------------< NamespaceResolver >---------------------- + + /** + * {@inheritDoc} + */ + public String getURI(String prefix) throws NamespaceException { + try { + int index = Integer.parseInt(prefix); + return nsReg.indexToString(index); + } catch (IllegalArgumentException e) { + throw new NamespaceException( + "Unknown namespace prefix: " + prefix, e); + } + } + + /** + * {@inheritDoc} + */ + public String getPrefix(String uri) throws NamespaceException { + try { + return String.valueOf(nsReg.stringToIndex(uri)); + } catch (IllegalArgumentException e) { + throw new NamespaceException( + "Unknown namespace URI: " + uri, e); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NamePathResolverImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NamePathResolverImpl.java new file mode 100644 index 00000000000..4467fa7794c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NamePathResolverImpl.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.core.query.lucene; + +import javax.jcr.NamespaceException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NameParser; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingPathResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +/** + * NamePathResolverImpl... + */ +public class NamePathResolverImpl extends DefaultNamePathResolver { + + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + private NamePathResolverImpl(NameResolver nResolver, PathResolver pResolver) { + super(nResolver, pResolver); + } + + public static NamePathResolver create(NamespaceMappings nsMappings) { + NameResolver nResolver = new NameResolverImpl(nsMappings); + PathResolver pResolver = new ParsingPathResolver(PATH_FACTORY, nResolver); + return new NamePathResolverImpl(nResolver, pResolver); + } + + + //--------------------------------------------------------< inner class >--- + /** + * Query specific NameResolver that does not assume an empty prefix for the + * default namespace URI. Instead the prefix is always retrieved from the + * NamespaceResolver. + */ + private static class NameResolverImpl implements NameResolver { + + /** + * Namespace resolver. + */ + private final NamespaceResolver resolver; + + /** + * Creates a parsing name resolver. + * + * @param resolver namespace resolver + */ + public NameResolverImpl(NamespaceResolver resolver) { + this.resolver = resolver; + } + + //-------------------------------------------------------< NameResolver >--- + /** + * Parses the prefixed JCR name and returns the resolved Name object. + * + * @param name The JCR name string. + * @return The corresponding Name. + * @throws IllegalNameException if the JCR name format is invalid + * @throws NamespaceException if the namespace prefix can not be resolved + */ + public Name getQName(String name) throws IllegalNameException, NamespaceException { + return NameParser.parse(name, resolver, NAME_FACTORY); + } + + /** + * Returns the qualified JCR name for the given Name. + * Note, that the JCR prefix is always retrieved from the NamespaceResolver + * even if the name is in the default namespace. This is a special treatment + * for query specific implementation, which defines a prefix for all namespace + * URIs including the default namespace. + * + * @param name A Name object. + * @return The corresponding qualified JCR name string. + * @throws NamespaceException if the namespace URI can not be resolved + */ + public String getJCRName(Name name) throws NamespaceException { + String uri = name.getNamespaceURI(); + if (resolver.getPrefix(uri).length() == 0) { + return name.getLocalName(); + } else { + return resolver.getPrefix(uri) + ":" + name.getLocalName(); + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NameQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NameQuery.java new file mode 100644 index 00000000000..ad834acfa20 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NameQuery.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.core.query.lucene; + +import org.apache.lucene.search.Query; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; + +import java.io.IOException; +import java.util.Set; + +/** + * NameQuery implements a query for the name of a node. + */ +@SuppressWarnings("serial") +public class NameQuery extends Query { + + /** + * The node name. + */ + private final Name nodeName; + + /** + * The index format version. + */ + private final IndexFormatVersion version; + + /** + * The internal namespace mappings of the index. + */ + private final NamespaceMappings nsMappings; + + /** + * Creates a new NameQuery. + * + * @param nodeName the name of the nodes to return. + * @param version the version of the index. + * @param nsMappings the namespace mappings of the index. + */ + public NameQuery(Name nodeName, + IndexFormatVersion version, + NamespaceMappings nsMappings) { + this.nodeName = nodeName; + this.version = version; + this.nsMappings = nsMappings; + } + + /** + * @return the name of the nodes to return. + */ + public Name getName() { + return nodeName; + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + if (version.getVersion() >= IndexFormatVersion.V3.getVersion()) { + // use LOCAL_NAME and NAMESPACE_URI field + BooleanQuery name = new BooleanQuery(); + name.add(new JackrabbitTermQuery(new Term(FieldNames.NAMESPACE_URI, nodeName.getNamespaceURI())), + BooleanClause.Occur.MUST); + name.add(new JackrabbitTermQuery(new Term(FieldNames.LOCAL_NAME, + nodeName.getLocalName())), + BooleanClause.Occur.MUST); + return name; + } else { + // use LABEL field + try { + return new JackrabbitTermQuery(new Term(FieldNames.LABEL, + nsMappings.translateName(nodeName))); + } catch (IllegalNameException e) { + throw Util.createIOException(e); + } + } + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + } + + /** + * {@inheritDoc} + */ + public String toString(String field) { + return "name() = " + nodeName.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NameRangeQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NameRangeQuery.java new file mode 100644 index 00000000000..5232ae926d0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NameRangeQuery.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.core.query.lucene; + +import org.apache.lucene.search.Query; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.util.ToStringUtils; +import org.apache.jackrabbit.spi.Name; + +import javax.jcr.RepositoryException; +import java.io.IOException; + +/** + * NameRangeQuery... + */ +@SuppressWarnings("serial") +public class NameRangeQuery extends Query { + + /** + * The lower name. May be null if upperName is not + * null. + */ + private final Name lowerName; + + /** + * The upper name. May be null if lowerName is not + * null. + */ + private final Name upperName; + + /** + * If true the range interval is inclusive. + */ + private final boolean inclusive; + + /** + * The index format version. + */ + private final IndexFormatVersion version; + + /** + * The internal namespace mappings. + */ + private final NamespaceMappings nsMappings; + + private final PerQueryCache cache; + + /** + * Creates a new NameRangeQuery. The lower or the upper name may be + * null, but not both! + * + * @param lowerName the lower name of the interval, or null + * @param upperName the upper name of the interval, or null. + * @param inclusive if true the interval is inclusive. + * @param version the index format version. + * @param nsMappings the internal namespace mappings. + */ + public NameRangeQuery(Name lowerName, + Name upperName, + boolean inclusive, + IndexFormatVersion version, + NamespaceMappings nsMappings, + PerQueryCache cache) { + if (lowerName == null && upperName == null) { + throw new IllegalArgumentException("At least one term must be non-null"); + } + if (lowerName != null && upperName != null && + !lowerName.getNamespaceURI().equals(upperName.getNamespaceURI())) { + throw new IllegalArgumentException("Both names must have the same namespace URI"); + } + this.lowerName = lowerName; + this.upperName = upperName; + this.inclusive = inclusive; + this.version = version; + this.nsMappings = nsMappings; + this.cache = cache; + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + Query q; + if (version.getVersion() >= IndexFormatVersion.V3.getVersion()) { + RangeQuery localNames = new RangeQuery( + getLowerLocalNameTerm(), getUpperLocalNameTerm(), + inclusive, cache); + BooleanQuery query = new BooleanQuery(); + query.add(new JackrabbitTermQuery(new Term(FieldNames.NAMESPACE_URI, + getNamespaceURI())), BooleanClause.Occur.MUST); + query.add(localNames, BooleanClause.Occur.MUST); + q = query; + } else { + q = new RangeQuery( + getLowerTerm(), getUpperTerm(), inclusive, cache); + } + return q.rewrite(reader); + } + + /** + * {@inheritDoc} + */ + public String toString(String field) { + StringBuffer buffer = new StringBuffer(); + buffer.append("name():"); + buffer.append(inclusive ? "[" : "{"); + buffer.append(lowerName != null ? lowerName.toString() : "null"); + buffer.append(" TO "); + buffer.append(upperName != null ? upperName.toString() : "null"); + buffer.append(inclusive ? "]" : "}"); + buffer.append(ToStringUtils.boost(getBoost())); + return buffer.toString(); + } + + //----------------------------< internal >---------------------------------- + + /** + * @return the namespace URI of this name query. + */ + private String getNamespaceURI() { + return lowerName != null ? lowerName.getNamespaceURI() : upperName.getNamespaceURI(); + } + + /** + * @return the local name term of the lower name or null if no + * lower name is set. + */ + private Term getLowerLocalNameTerm() { + if (lowerName == null) { + return null; + } else { + return new Term(FieldNames.LOCAL_NAME, lowerName.getLocalName()); + } + } + + /** + * @return the local name term of the upper name or null if no + * upper name is set. + */ + private Term getUpperLocalNameTerm() { + if (upperName == null) { + return null; + } else { + return new Term(FieldNames.LOCAL_NAME, upperName.getLocalName()); + } + } + + /** + * @return the lower term. Must only be used for IndexFormatVersion < 3. + * @throws IOException if a name cannot be translated. + */ + private Term getLowerTerm() throws IOException { + try { + String text; + if (lowerName == null) { + text = nsMappings.getPrefix(upperName.getNamespaceURI()) + ":"; + } else { + text = nsMappings.translateName(lowerName); + } + return new Term(FieldNames.LABEL, text); + } catch (RepositoryException e) { + throw Util.createIOException(e); + } + } + + /** + * @return the upper term. Must only be used for IndexFormatVersion < 3. + * @throws IOException if a name cannot be translated. + */ + private Term getUpperTerm() throws IOException { + try { + String text; + if (upperName == null) { + text = nsMappings.getPrefix(lowerName.getNamespaceURI()) + ":\uFFFF"; + } else { + text = nsMappings.translateName(upperName); + } + return new Term(FieldNames.LABEL, text); + } catch (RepositoryException e) { + throw Util.createIOException(e); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NamespaceMappings.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NamespaceMappings.java new file mode 100644 index 00000000000..93cc6a42aa4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NamespaceMappings.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.core.query.lucene; + +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * The class NamespaceMappings holds a namespace mapping that is + * used internally in the search index. Storing paths with the full uri of a + * namespace would require too much space in the search index. + */ +public interface NamespaceMappings extends NamespaceResolver { + + /** + * Translates a name from a session local namespace mapping into a search + * index private namespace mapping. + * + * @param name the name to translate + * @return the translated JCR name + * @throws IllegalNameException if the name cannot be translated. + */ + String translateName(Name name) throws IllegalNameException; + + /** + * Translates a path into a search index private namespace mapping. + * + * @param path the path to translate + * @return the translated path. + * @throws IllegalNameException if the name cannot be translated. + */ + String translatePath(Path path) throws IllegalNameException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIndexer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIndexer.java new file mode 100644 index 00000000000..a224ec828e6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIndexer.java @@ -0,0 +1,1038 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.math.BigDecimal; +import java.net.URI; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Fieldable; +import org.apache.lucene.index.FieldInfo; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Creates a lucene Document object from a {@link javax.jcr.Node}. + */ +public class NodeIndexer { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(NodeIndexer.class); + + /** + * The default boost for a lucene field: 1.0f. + */ + protected static final float DEFAULT_BOOST = IndexingConfiguration.DEFAULT_BOOST; + + /** + * The NodeState of the node to index + */ + protected final NodeState node; + + /** + * The persistent item state provider + */ + protected final ItemStateManager stateProvider; + + /** + * Namespace mappings to use for indexing. This is the internal + * namespace mapping. + */ + protected final NamespaceMappings mappings; + + /** + * Name and Path resolver. + */ + protected final NamePathResolver resolver; + + /** + * Background task executor used for full text extraction. + */ + private final Executor executor; + + /** + * Parser used for extracting text content from binary properties + * for full text indexing. + */ + private final Parser parser; + + /** + * The media types supported by the parser used. + */ + private Set supportedMediaTypes; + + /** + * The indexing configuration or null if none is available. + */ + protected IndexingConfiguration indexingConfig; + + /** + * If set to true the fulltext field is stored and and a term + * vector is created with offset information. + */ + protected boolean supportHighlighting = false; + + /** + * Indicates index format for this node indexer. + */ + protected IndexFormatVersion indexFormatVersion = IndexFormatVersion.V1; + + /** + * List of {@link FieldNames#FULLTEXT} fields which should not be used in + * an excerpt. + */ + protected List doNotUseInExcerpt = new ArrayList(); + + /** + * The maximum number of characters to extract from binaries. + */ + private int maxExtractLength = Integer.MAX_VALUE; + + /** + * Creates a new node indexer. + * + * @param node the node state to index. + * @param stateProvider the persistent item state manager to retrieve properties. + * @param mappings internal namespace mappings. + * @param executor background task executor for text extraction + * @param parser parser for binary properties + */ + public NodeIndexer( + NodeState node, ItemStateManager stateProvider, + NamespaceMappings mappings, Executor executor, Parser parser) { + this.node = node; + this.stateProvider = stateProvider; + this.mappings = mappings; + this.resolver = NamePathResolverImpl.create(mappings); + this.executor = executor; + this.parser = parser; + } + + /** + * Returns the NodeId of the indexed node. + * @return the NodeId of the indexed node. + */ + public NodeId getNodeId() { + return node.getNodeId(); + } + + /** + * If set to true additional information is stored in the index + * to support highlighting using the rep:excerpt pseudo property. + * + * @param b true to enable highlighting support. + */ + public void setSupportHighlighting(boolean b) { + supportHighlighting = b; + } + + /** + * Sets the index format version + * + * @param indexFormatVersion the index format version + */ + public void setIndexFormatVersion(IndexFormatVersion indexFormatVersion) { + this.indexFormatVersion = indexFormatVersion; + } + + /** + * Sets the indexing configuration for this node indexer. + * + * @param config the indexing configuration. + */ + public void setIndexingConfiguration(IndexingConfiguration config) { + this.indexingConfig = config; + } + + /** + * Returns the maximum number of characters to extract from binaries. + * + * @return maximum extraction length + */ + public int getMaxExtractLength() { + return maxExtractLength; + } + + /** + * Sets the maximum number of characters to extract from binaries. + * + * @param length maximum extraction length + */ + public void setMaxExtractLength(int length) { + this.maxExtractLength = length; + } + + /** + * Creates a lucene Document. + * + * @return the lucene Document with the index layout. + * @throws RepositoryException if an error occurs while reading property + * values from the ItemStateProvider. + */ + public Document createDoc() throws RepositoryException { + doNotUseInExcerpt.clear(); + Document doc = new Document(); + + doc.setBoost(getNodeBoost()); + + // special fields + // UUID + doc.add(new IDField(node.getNodeId())); + try { + // parent UUID + if (node.getParentId() == null) { + // root node + Field parent = new Field(FieldNames.PARENT, false, "", + Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO); + parent.setIndexOptions(FieldInfo.IndexOptions.DOCS_ONLY); + doc.add(parent); + addNodeName(doc, "", ""); + } else if (node.getSharedSet().isEmpty()) { + addParentChildRelation(doc, node.getParentId()); + } else { + // shareable node + for (NodeId id : node.getSharedSet()) { + addParentChildRelation(doc, id); + } + // mark shareable nodes + doc.add(new Field(FieldNames.SHAREABLE_NODE, false, "", + Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO)); + } + } catch (NoSuchItemStateException e) { + throwRepositoryException(e); + } catch (ItemStateException e) { + throwRepositoryException(e); + } catch (NamespaceException e) { + // will never happen, because this.mappings will dynamically add + // unknown uri<->prefix mappings + } + + Set props = node.getPropertyNames(); + for (Name propName : props) { + if (isIndexed(propName)) { + PropertyId id = new PropertyId(node.getNodeId(), propName); + try { + PropertyState propState = + (PropertyState) stateProvider.getItemState(id); + + // add each property to the _PROPERTIES_SET for searching + // beginning with V2 + if (indexFormatVersion.getVersion() >= IndexFormatVersion.V2.getVersion()) { + addPropertyName(doc, propState.getName()); + } + + InternalValue[] values = propState.getValues(); + for (InternalValue value : values) { + addValue(doc, value, propState.getName()); + } + + if (values.length > 1) { + // real multi-valued + addMVPName(doc, propState.getName()); + } + } catch (NoSuchItemStateException e) { + throwRepositoryException(e); + } catch (ItemStateException e) { + throwRepositoryException(e); + } + } + } + + // now add fields that are not used in excerpt (must go at the end) + for (Fieldable field : doNotUseInExcerpt) { + doc.add(field); + } + return doc; + } + + /** + * Wraps the exception e into a RepositoryException + * and throws the created exception. + * + * @param e the base exception. + */ + protected void throwRepositoryException(Exception e) + throws RepositoryException { + String msg = "Error while indexing node: " + node.getNodeId() + " of " + + "type: " + node.getNodeTypeName(); + throw new RepositoryException(msg, e); + } + + /** + * Adds a {@link FieldNames#MVP} field to doc with the resolved + * name using the internal search index namespace mapping. + * + * @param doc the lucene document. + * @param name the name of the multi-value property. + */ + protected void addMVPName(Document doc, Name name) { + try { + String propName = resolver.getJCRName(name); + doc.add(new Field(FieldNames.MVP, false, propName, Field.Store.NO, + Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + } catch (NamespaceException e) { + // will never happen, prefixes are created dynamically + } + } + + /** + * Adds a value to the lucene Document. + * + * @param doc the document. + * @param value the internal jackrabbit value. + * @param name the name of the property. + */ + protected void addValue(Document doc, InternalValue value, Name name) throws RepositoryException { + String fieldName = name.getLocalName(); + try { + fieldName = resolver.getJCRName(name); + } catch (NamespaceException e) { + // will never happen + } + switch (value.getType()) { + case PropertyType.BINARY: + addBinaryValue(doc, fieldName, value); + break; + case PropertyType.BOOLEAN: + addBooleanValue(doc, fieldName, value.getBoolean()); + break; + case PropertyType.DATE: + addCalendarValue(doc, fieldName, value.getDate()); + break; + case PropertyType.DOUBLE: + addDoubleValue(doc, fieldName, value.getDouble()); + break; + case PropertyType.LONG: + addLongValue(doc, fieldName, value.getLong()); + break; + case PropertyType.REFERENCE: + addReferenceValue(doc, fieldName, value.getNodeId(), false); + break; + case PropertyType.WEAKREFERENCE: + addReferenceValue(doc, fieldName, value.getNodeId(), true); + break; + case PropertyType.PATH: + addPathValue(doc, fieldName, value.getPath()); + break; + case PropertyType.URI: + addURIValue(doc, fieldName, value.getURI()); + break; + case PropertyType.STRING: + // never fulltext index jcr:uuid String + if (name.equals(NameConstants.JCR_UUID)) { + addStringValue(doc, fieldName, value.getString(), + false, false, DEFAULT_BOOST, true); + } else { + addStringValue(doc, fieldName, value.getString(), + true, isIncludedInNodeIndex(name), + getPropertyBoost(name), useInExcerpt(name)); + } + break; + case PropertyType.NAME: + addNameValue(doc, fieldName, value.getName()); + break; + case PropertyType.DECIMAL: + addDecimalValue(doc, fieldName, value.getDecimal()); + break; + default: + throw new IllegalArgumentException("illegal internal value type: " + value.getType()); + } + addValueProperty(doc, value, name, fieldName); + } + + /** + * Adds a property related value to the lucene Document.
    + * + * Like length for indexed fields. + * + * @param doc + * the document. + * @param value + * the internal jackrabbit value. + * @param name + * the name of the property. + */ + protected void addValueProperty(Document doc, InternalValue value, + Name name, String fieldName) throws RepositoryException { + // add length + if (indexFormatVersion.getVersion() >= IndexFormatVersion.V3.getVersion()) { + addLength(doc, fieldName, value); + } + } + + /** + * Adds the property name to the lucene _:PROPERTIES_SET field. + * + * @param doc the document. + * @param name the name of the property. + */ + protected void addPropertyName(Document doc, Name name) { + String fieldName = name.getLocalName(); + try { + fieldName = resolver.getJCRName(name); + } catch (NamespaceException e) { + // will never happen + } + doc.add(new Field(FieldNames.PROPERTIES_SET, false, fieldName, + Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO)); + } + + /** + * Adds the binary value to the document as the named field. + *

    + * This implementation checks if this {@link #node} is of type nt:resource + * and if that is the case, tries to extract text from the binary property + * using the {@link #parser}. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + */ + protected void addBinaryValue(Document doc, + String fieldName, + InternalValue internalValue) { + // 'check' if node is of type nt:resource + try { + String jcrData = mappings.getPrefix(Name.NS_JCR_URI) + ":data"; + if (!jcrData.equals(fieldName)) { + // don't know how to index + return; + } + + InternalValue type = getValue(NameConstants.JCR_MIMETYPE); + if (type != null && isSupportedMediaType(type.getString())) { + Metadata metadata = new Metadata(); + metadata.set(Metadata.CONTENT_TYPE, type.getString()); + + // jcr:encoding is not mandatory + InternalValue encoding = getValue(NameConstants.JCR_ENCODING); + if (encoding != null) { + metadata.set( + Metadata.CONTENT_ENCODING, encoding.getString()); + } + + doc.add(createFulltextField(internalValue, metadata, false)); + } + } catch (Throwable t) { + // TODO: How to recover from a transient indexing failure? + log.warn("Exception while indexing binary property", t); + } + } + + /** + * Utility method that extracts the first value of the named property + * of the current node. Returns null if the property does + * not exist or contains no values. + * + * @param name property name + * @return value of the named property, or null + * @throws ItemStateException if the property can not be accessed + */ + protected InternalValue getValue(Name name) throws ItemStateException { + try { + PropertyId id = new PropertyId(node.getNodeId(), name); + PropertyState property = + (PropertyState) stateProvider.getItemState(id); + InternalValue[] values = property.getValues(); + if (values.length > 0) { + return values[0]; + } else { + return null; + } + } catch (NoSuchItemStateException e) { + return null; + } + } + + /** + * Adds the string representation of the boolean value to the document as + * the named field. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + */ + protected void addBooleanValue(Document doc, String fieldName, Object internalValue) { + doc.add(createFieldWithoutNorms(fieldName, internalValue.toString(), + PropertyType.BOOLEAN)); + } + + /** + * Creates a field of name fieldName with the value of + * internalValue. The created field is indexed without norms. + * + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + * @param propertyType the property type. + */ + protected Field createFieldWithoutNorms(String fieldName, + String internalValue, + int propertyType) { + if (indexFormatVersion.getVersion() + >= IndexFormatVersion.V3.getVersion()) { + Field field = new Field(FieldNames.PROPERTIES, + new SingletonTokenStream( + FieldNames.createNamedValue(fieldName, internalValue), + propertyType) + ); + field.setOmitNorms(true); + return field; + } else { + return new Field(FieldNames.PROPERTIES, false, + FieldNames.createNamedValue(fieldName, internalValue), + Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO); + } + } + + /** + * Adds the calendar value to the document as the named field. The calendar + * value is converted to an indexable string value using the + * {@link DateField} class. + * + * @param doc + * The document to which to add the field + * @param fieldName + * The name of the field to add + * @param internalValue + * The value for the field to add to the document. + */ + protected void addCalendarValue(Document doc, String fieldName, Calendar internalValue) { + try { + doc.add(createFieldWithoutNorms(fieldName, + DateField.timeToString(internalValue.getTimeInMillis()), + PropertyType.DATE)); + } catch (IllegalArgumentException e) { + log.warn("'{}' is outside of supported date value range.", + internalValue); + } + } + + /** + * Adds the double value to the document as the named field. The double + * value is converted to an indexable string value using the + * {@link DoubleField} class. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + */ + protected void addDoubleValue(Document doc, String fieldName, double internalValue) { + doc.add(createFieldWithoutNorms(fieldName, DoubleField.doubleToString(internalValue), + PropertyType.DOUBLE)); + } + + /** + * Adds the long value to the document as the named field. The long + * value is converted to an indexable string value using the {@link LongField} + * class. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + */ + protected void addLongValue(Document doc, String fieldName, long internalValue) { + doc.add(createFieldWithoutNorms(fieldName, LongField.longToString(internalValue), + PropertyType.LONG)); + } + + /** + * Adds the long value to the document as the named field. The long + * value is converted to an indexable string value using the {@link LongField} + * class. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + */ + protected void addDecimalValue(Document doc, String fieldName, BigDecimal internalValue) { + doc.add(createFieldWithoutNorms(fieldName, DecimalField.decimalToString(internalValue), + PropertyType.DECIMAL)); + } + + /** + * Adds the reference value to the document as the named field. The value's + * string representation is added as the reference data. Additionally the + * reference data is stored in the index. As of Jackrabbit 2.0 this method + * also adds the reference UUID as a {@link FieldNames#WEAK_REFS} field + * to the index if it is a weak reference. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + * @param weak Flag indicating whether it's a WEAKREFERENCE (true) or a REFERENCE (flase) + */ + protected void addReferenceValue(Document doc, String fieldName, NodeId internalValue, boolean weak) { + String uuid = internalValue.toString(); + doc.add(createFieldWithoutNorms(fieldName, uuid, + weak ? PropertyType.WEAKREFERENCE : PropertyType.REFERENCE)); + doc.add(new Field(FieldNames.PROPERTIES, false, FieldNames + .createNamedValue(fieldName, uuid), Field.Store.YES, + Field.Index.NO, Field.TermVector.NO)); + if (weak) { + doc.add(new Field(FieldNames.WEAK_REFS, false, uuid, + Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO)); + } + } + + /** + * Adds the path value to the document as the named field. The path + * value is converted to an indexable string value using the name space + * mappings with which this class has been created. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + */ + protected void addPathValue(Document doc, String fieldName, Path internalValue) { + String pathString = internalValue.toString(); + try { + pathString = resolver.getJCRPath(internalValue); + } catch (NamespaceException e) { + // will never happen + } + doc.add(createFieldWithoutNorms(fieldName, pathString, + PropertyType.PATH)); + } + + /** + * Adds the uri value to the document as the named field. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + */ + protected void addURIValue(Document doc, String fieldName, URI internalValue) { + doc.add(createFieldWithoutNorms(fieldName, internalValue.toString(), + PropertyType.URI)); + } + + /** + * Adds the string value to the document both as the named field and for + * full text indexing. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + * @deprecated Use {@link #addStringValue(Document, String, String, boolean) + * addStringValue(Document, String, Object, boolean)} instead. + */ + protected void addStringValue(Document doc, String fieldName, String internalValue) { + addStringValue(doc, fieldName, internalValue, true, true, DEFAULT_BOOST, true); + } + + /** + * Adds the string value to the document both as the named field and + * optionally for full text indexing if tokenized is + * true. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + * @param tokenized If true the string is also tokenized + * and fulltext indexed. + */ + protected void addStringValue(Document doc, String fieldName, + String internalValue, boolean tokenized) { + addStringValue(doc, fieldName, internalValue, tokenized, true, DEFAULT_BOOST, true); + } + + /** + * Adds the string value to the document both as the named field and + * optionally for full text indexing if tokenized is + * true. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the + * document. + * @param tokenized If true the string is also + * tokenized and fulltext indexed. + * @param includeInNodeIndex If true the string is also + * tokenized and added to the node scope fulltext + * index. + * @param boost the boost value for this string field. + * @deprecated use {@link #addStringValue(Document, String, String, boolean, boolean, float, boolean)} instead. + */ + protected void addStringValue(Document doc, String fieldName, + String internalValue, boolean tokenized, + boolean includeInNodeIndex, float boost) { + addStringValue(doc, fieldName, internalValue, tokenized, includeInNodeIndex, boost, true); + } + + /** + * Adds the string value to the document both as the named field and + * optionally for full text indexing if tokenized is + * true. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the + * document. + * @param tokenized If true the string is also + * tokenized and fulltext indexed. + * @param includeInNodeIndex If true the string is also + * tokenized and added to the node scope fulltext + * index. + * @param boost the boost value for this string field. + * @param useInExcerpt If true the string may show up in + * an excerpt. + */ + protected void addStringValue(Document doc, String fieldName, + String internalValue, boolean tokenized, + boolean includeInNodeIndex, float boost, + boolean useInExcerpt) { + + // simple String + doc.add(createFieldWithoutNorms(fieldName, internalValue, + PropertyType.STRING)); + if (tokenized) { + if (internalValue.length() == 0) { + return; + } + // create fulltext index on property + int idx = fieldName.indexOf(':'); + fieldName = fieldName.substring(0, idx + 1) + + FieldNames.FULLTEXT_PREFIX + fieldName.substring(idx + 1); + boolean hasNorms = boost != DEFAULT_BOOST; + Field.Index indexType = hasNorms ? Field.Index.ANALYZED + : Field.Index.ANALYZED_NO_NORMS; + Field f = new Field(fieldName, true, internalValue, Field.Store.NO, + indexType, Field.TermVector.NO); + f.setBoost(boost); + doc.add(f); + + if (includeInNodeIndex) { + // also create fulltext index of this value + boolean store = supportHighlighting && useInExcerpt; + f = createFulltextField(internalValue, store, supportHighlighting, hasNorms); + if (useInExcerpt) { + doc.add(f); + } else { + doNotUseInExcerpt.add(f); + } + } + } + } + + /** + * Adds the name value to the document as the named field. The name + * value is converted to an indexable string treating the internal value + * as a Name and mapping the name space using the name space + * mappings with which this class has been created. + * + * @param doc The document to which to add the field + * @param fieldName The name of the field to add + * @param internalValue The value for the field to add to the document. + */ + protected void addNameValue(Document doc, String fieldName, Name internalValue) { + try { + String normValue = mappings.getPrefix(internalValue.getNamespaceURI()) + + ":" + internalValue.getLocalName(); + doc.add(createFieldWithoutNorms(fieldName, normValue, + PropertyType.NAME)); + } catch (NamespaceException e) { + // will never happen + } + } + + /** + * Creates a fulltext field for the string value. + * + * @param value the string value. + * @return a lucene field. + * @deprecated use {@link #createFulltextField(String, boolean, boolean, boolean)} instead. + */ + protected Field createFulltextField(String value) { + return createFulltextField(value, supportHighlighting, supportHighlighting); + } + + /** + * Creates a fulltext field for the string value. + * + * @param value the string value. + * @param store if the value of the field should be stored. + * @param withOffsets if a term vector with offsets should be stored. + * @return a lucene field. + * @deprecated use {@link #createFulltextField(String, boolean, boolean, boolean)} instead. + */ + protected Field createFulltextField(String value, + boolean store, + boolean withOffsets) { + return createFulltextField(value, store, withOffsets, true); + } + + /** + * Creates a fulltext field for the string value. + * + * @param value the string value. + * @param store if the value of the field should be stored. + * @param withOffsets if a term vector with offsets should be stored. + * @param withNorms if norm information should be added for this value + * @return a lucene field. + */ + protected Field createFulltextField(String value, + boolean store, + boolean withOffsets, + boolean withNorms) { + Field.TermVector tv; + if (withOffsets) { + tv = Field.TermVector.WITH_OFFSETS; + } else { + tv = Field.TermVector.NO; + } + Field.Index index; + if (withNorms) { + index = Field.Index.ANALYZED; + } else { + index = Field.Index.ANALYZED_NO_NORMS; + } + if (store) { + // We would be able to store the field compressed or not depending + // on a criterion but then we could not determine later is this field + // has been compressed or not, so we choose to store it uncompressed + return new Field(FieldNames.FULLTEXT, false, value, Field.Store.YES, + index, tv); + } else { + return new Field(FieldNames.FULLTEXT, false, value, + Field.Store.NO, index, tv); + } + } + + /** + * Creates a fulltext field for the reader value. + * + * @param value the binary value + * @param metadata document metatadata + * @return a lucene field. + * @deprecated use {@link #createFulltextField(InternalValue, Metadata, boolean)} instead. + */ + protected Fieldable createFulltextField( + InternalValue value, Metadata metadata) { + return createFulltextField(value, metadata, true); + } + + /** + * Creates a fulltext field for the reader value. + * + * @param value the binary value + * @param metadata document metatadata + * @param withNorms if norm information should be added for this value + * @return a lucene field. + */ + protected Fieldable createFulltextField( + InternalValue value, Metadata metadata, boolean withNorms) { + return new LazyTextExtractorField(parser, value, metadata, executor, + supportHighlighting, getMaxExtractLength(), withNorms); + } + + /** + * Returns true if the property with the given name should + * be indexed. The default is to index all properties unless explicit + * indexing configuration is specified. The jcr:primaryType + * and jcr:mixinTypes properties are always indexed for + * correct node type resolution in queries. + * + * @param propertyName name of a property. + * @return true if the property should be indexed; + * false otherwise. + */ + protected boolean isIndexed(Name propertyName) { + return indexingConfig == null + || propertyName.equals(NameConstants.JCR_PRIMARYTYPE) + || propertyName.equals(NameConstants.JCR_MIXINTYPES) + || indexingConfig.isIndexed(node, propertyName); + } + + /** + * Returns true if the property with the given name should also + * be added to the node scope index. + * + * @param propertyName the name of a property. + * @return true if it should be added to the node scope index; + * false otherwise. + */ + protected boolean isIncludedInNodeIndex(Name propertyName) { + if (indexingConfig == null) { + return true; + } else { + return indexingConfig.isIncludedInNodeScopeIndex(node, propertyName); + } + } + + /** + * Returns true if the content of the property with the given + * name should the used to create an excerpt. + * + * @param propertyName the name of a property. + * @return true if it should be used to create an excerpt; + * false otherwise. + */ + protected boolean useInExcerpt(Name propertyName) { + if (indexingConfig == null) { + return true; + } else { + return indexingConfig.useInExcerpt(node, propertyName); + } + } + + /** + * Returns true if the provided type is among the types + * supported by the Tika parser we are using. + * + * @param type the type to check. + * @return whether the type is supported by the Tika parser we are using. + */ + protected boolean isSupportedMediaType(final String type) { + if (supportedMediaTypes == null) { + supportedMediaTypes = parser.getSupportedTypes(new ParseContext()); + } + return supportedMediaTypes.contains(MediaType.parse(type)); + } + + /** + * Returns the boost value for the given property name. + * + * @param propertyName the name of a property. + * @return the boost value for the given property name. + */ + protected float getPropertyBoost(Name propertyName) { + if (indexingConfig == null) { + return DEFAULT_BOOST; + } else { + return indexingConfig.getPropertyBoost(node, propertyName); + } + } + + /** + * @return the boost value for this {@link #node} state. + */ + protected float getNodeBoost() { + if (indexingConfig == null) { + return DEFAULT_BOOST; + } else { + return indexingConfig.getNodeBoost(node); + } + } + + /** + * Adds a {@link FieldNames#PROPERTY_LENGTHS} field to document + * with a named length value. + * + * @param doc the lucene document. + * @param propertyName the property name. + * @param value the internal value. + */ + protected void addLength(Document doc, + String propertyName, + InternalValue value) { + long length = Util.getLength(value); + if (length != -1) { + doc.add(new Field(FieldNames.PROPERTY_LENGTHS, false, FieldNames + .createNamedLength(propertyName, length), Field.Store.NO, + Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + } + } + + /** + * Depending on the index format version adds one or two fields to the + * document for the node name. + * + * @param doc the lucene document. + * @param namespaceURI the namespace URI of the node name. + * @param localName the local name of the node. + */ + protected void addNodeName(Document doc, + String namespaceURI, + String localName) throws NamespaceException { + String name = mappings.getPrefix(namespaceURI) + ":" + localName; + doc.add(new Field(FieldNames.LABEL, false, name, Field.Store.NO, + Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + // as of version 3, also index combination of namespace URI and local name + if (indexFormatVersion.getVersion() >= IndexFormatVersion.V3.getVersion()) { + doc.add(new Field(FieldNames.NAMESPACE_URI, false, namespaceURI, + Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO)); + doc.add(new Field(FieldNames.LOCAL_NAME, false, localName, + Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO)); + } + } + + /** + * Adds a parent child relation to the given doc. + * + * @param doc the document. + * @param parentId the id of the parent node. + * @throws ItemStateException if the parent node cannot be read. + * @throws RepositoryException if the parent node does not have a child node + * entry for the current node. + */ + protected void addParentChildRelation(Document doc, + NodeId parentId) + throws ItemStateException, RepositoryException { + Field parentField = new Field(FieldNames.PARENT, false, + parentId.toString(), Field.Store.YES, + Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO); + parentField.setIndexOptions(FieldInfo.IndexOptions.DOCS_ONLY); + doc.add(parentField); + NodeState parent = (NodeState) stateProvider.getItemState(parentId); + ChildNodeEntry child = parent.getChildNodeEntry(node.getNodeId()); + if (child == null) { + // this can only happen when jackrabbit + // is running in a cluster. + throw new RepositoryException( + "Missing child node entry for node with id: " + + node.getNodeId()); + } + Name name = child.getName(); + addNodeName(doc, name.getNamespaceURI(), name.getLocalName()); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIteratorImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIteratorImpl.java new file mode 100644 index 00000000000..61a6a6f2ce1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeIteratorImpl.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; + +import java.util.NoSuchElementException; + +/** + * Implements a {@link javax.jcr.NodeIterator} returned by + * {@link javax.jcr.query.QueryResult#getNodes()}. + */ +class NodeIteratorImpl implements NodeIterator { + + /** Logger instance for this class */ + private static final Logger log = LoggerFactory.getLogger(NodeIteratorImpl.class); + + /** + * Component context of the current session + */ + protected final SessionContext sessionContext; + + /** The node ids of the nodes in the result set with their score value */ + protected final ScoreNodeIterator scoreNodes; + + /** The index for the default selector within {@link #scoreNodes} */ + private final int selectorIndex; + + /** Number of invalid nodes */ + protected int invalid = 0; + + /** Reference to the next node instance */ + private NodeImpl next; + + /** + * Whether this iterator had been initialized. + */ + private boolean initialized; + + /** + * Creates a new NodeIteratorImpl instance. + * + * @param sessionContext component context of the current session + * @param scoreNodes iterator over score nodes. + * @param selectorIndex the index for the default selector within + * scoreNodes. + */ + NodeIteratorImpl( + SessionContext sessionContext, ScoreNodeIterator scoreNodes, + int selectorIndex) { + this.sessionContext = sessionContext; + this.scoreNodes = scoreNodes; + this.selectorIndex = selectorIndex; + } + + /** + * Returns the next Node in the result set. + * @return the next Node in the result set. + * @throws NoSuchElementException if iteration has no more + * Nodes. + */ + public Node nextNode() throws NoSuchElementException { + initialize(); + if (next == null) { + throw new NoSuchElementException(); + } + NodeImpl n = next; + fetchNext(); + return n; + } + + /** + * Returns the next Node in the result set. + * @return the next Node in the result set. + * @throws NoSuchElementException if iteration has no more + * Nodes. + */ + public Object next() throws NoSuchElementException { + initialize(); + return nextNode(); + } + + /** + * Skip a number of Nodes in this iterator. + * @param skipNum the non-negative number of Nodes to skip + * @throws NoSuchElementException + * if skipped past the last Node in this iterator. + */ + public void skip(long skipNum) throws NoSuchElementException { + initialize(); + if (skipNum > 0) { + scoreNodes.skip(skipNum - 1); + fetchNext(); + } + } + + /** + * Returns the number of nodes in this iterator. + *

    + * Note: The number returned by this method may differ from the number + * of nodes actually returned by calls to hasNext() / getNextNode()! This + * is because this iterator works on a lazy instantiation basis and while + * iterating over the nodes some of them might have been deleted in the + * meantime. Those will not be returned by getNextNode(). As soon as an + * invalid node is detected, the size of this iterator is adjusted. + * + * @return the number of node in this iterator. + */ + public long getSize() { + long size = scoreNodes.getSize(); + if (size == -1) { + return size; + } else { + return size - invalid; + } + } + + /** + * Returns the current position in this NodeIterator. + * @return the current position in this NodeIterator. + */ + public long getPosition() { + initialize(); + long position = scoreNodes.getPosition() - invalid; + // scoreNode.getPosition() is one ahead + // if there is a prefetched node + if (next != null) { + position--; + } + return position; + } + + /** + * Returns true if there is another Node + * available; false otherwise. + * @return true if there is another Node + * available; false otherwise. + */ + public boolean hasNext() { + initialize(); + return next != null; + } + + /** + * @throws UnsupportedOperationException always. + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + /** + * Clears {@link #next} and tries to fetch the next Node instance. + * When this method returns {@link #next} refers to the next available + * node instance in this iterator. If {@link #next} is null when this + * method returns, then there are no more valid element in this iterator. + */ + protected void fetchNext() { + try { + next = null; // reset + sessionContext.getSessionState().perform(new FetchNext()); + } catch (RepositoryException e) { + log.warn("Failed to fetch next node", e); + } + } + + private class FetchNext implements SessionOperation { + + public Object perform(SessionContext context) { + + ItemManager itemMgr = context.getItemManager(); + while (next == null && scoreNodes.hasNext()) { + ScoreNode[] sn = scoreNodes.nextScoreNodes(); + try { + next = (NodeImpl) itemMgr.getItem( + sn[selectorIndex].getNodeId()); + } catch (RepositoryException e) { + log.warn("Failed to retrieve query result node " + + sn[selectorIndex].getNodeId(), e); + // try next + invalid++; + } + } + + return this; + } + + } + + protected void initialize() { + if (!initialized) { + fetchNext(); + initialized = true; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeTraversingQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeTraversingQueryHits.java new file mode 100644 index 00000000000..b80bcf432c4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NodeTraversingQueryHits.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.commons.collections.iterators.IteratorChain; +import org.apache.jackrabbit.core.NodeImpl; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +/** + * NodeTraversingQueryHits implements query hits that traverse + * a node hierarchy. + */ +public class NodeTraversingQueryHits extends AbstractQueryHits { + + /** + * The nodes to traverse. + */ + private final Iterator nodes; + + /** + * Creates query hits that consist of the nodes that are traversed from a + * given start node. + * + * @param start the start node of the traversal. + * @param includeStart whether to include the start node in the result. + */ + public NodeTraversingQueryHits(Node start, boolean includeStart) { + this(start, includeStart, Integer.MAX_VALUE); + } + + /** + * Creates query hits that consist of the nodes that are traversed from a + * given start node. + * + * @param start the start node of the traversal. + * @param includeStart whether to include the start node in the result. + * @param maxDepth the maximum depth of nodes to traverse. + */ + public NodeTraversingQueryHits(Node start, + boolean includeStart, + int maxDepth) { + this.nodes = new TraversingNodeIterator(start, maxDepth); + if (!includeStart) { + nodes.next(); + } + } + + /** + * {@inheritDoc} + */ + public ScoreNode nextScoreNode() throws IOException { + if (nodes.hasNext()) { + NodeImpl n = (NodeImpl) nodes.next(); + return new ScoreNode(n.getNodeId(), 1.0f); + } else { + return null; + } + } + + /** + * Implements a node iterator that traverses a node tree in document + * order. + */ + private class TraversingNodeIterator implements Iterator { + + /** + * The current Node, which acts as the starting point for + * the traversal. + */ + private final Node currentNode; + + /** + * The maximum depth of the traversal. + */ + private final int maxDepth; + + /** + * The chain of iterators which includes the iterators of the children + * of the current node. + */ + private Iterator selfAndChildren; + + /** + * Creates a TraversingNodeIterator. + * + * @param start the node from where to start the traversal. + * @param maxDepth the maximum depth of nodes to traverse. + */ + TraversingNodeIterator(Node start, int maxDepth) { + if (maxDepth < 0) { + throw new IllegalArgumentException("maxDepth must be >= 0"); + } + currentNode = start; + this.maxDepth = maxDepth; + } + + /** + * @exception UnsupportedOperationException always. + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + /** + * @inheritDoc + */ + public boolean hasNext() { + init(); + return selfAndChildren.hasNext(); + } + + /** + * @inheritDoc + */ + public Node next() { + init(); + return selfAndChildren.next(); + } + + /** + * Initializes the iterator chain once. + */ + @SuppressWarnings({"unchecked"}) + private void init() { + if (selfAndChildren == null) { + List> allIterators = new ArrayList>(); + Iterator current = Collections.singletonList(currentNode).iterator(); + allIterators.add(current); + if (maxDepth == 0) { + // only current node + } else if (maxDepth == 1) { + try { + allIterators.add(currentNode.getNodes()); + } catch (RepositoryException e) { + // currentNode is probably stale + } + } else { + // create new TraversingNodeIterator for each child + try { + NodeIterator children = currentNode.getNodes(); + while (children.hasNext()) { + allIterators.add(new TraversingNodeIterator(children.nextNode(), maxDepth - 1)); + } + } catch (RepositoryException e) { + // currentNode is probably stale + } + } + selfAndChildren = new IteratorChain(allIterators); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NormalizeSortComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NormalizeSortComparator.java new file mode 100644 index 00000000000..e8775700358 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NormalizeSortComparator.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.lucene.analysis.ASCIIFoldingFilter; +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.FieldComparatorSource; + +/** + * NormalizeSortComparator implements a FieldComparator which + * compares the lower-cased and normalized string values of a base sort comparator. + */ +public class NormalizeSortComparator extends FieldComparatorSource { + + /** + * The base sort comparator. + */ + private final FieldComparatorSource base; + + /** + * Creates a new upper case sort comparator. + * + * @param base the base sort comparator source. + */ + public NormalizeSortComparator(FieldComparatorSource base) { + this.base = base; + } + + @Override + public FieldComparator newComparator( + String fieldname, int numHits, int sortPos, boolean reversed) + throws IOException { + FieldComparator comparator = + base.newComparator(fieldname, numHits, sortPos, reversed); + assert comparator instanceof FieldComparatorBase; + + return new FieldComparatorDecorator((FieldComparatorBase) comparator) { + @Override + protected Comparable sortValue(int doc) { + Comparable comparable = super.sortValue(doc); + if (comparable != null) { + char[] input = comparable.toString().toCharArray(); + + // Normalize to ASCII using Lucene's ASCIIFoldingFilter + char[] output = new char[input.length * 4]; // worst-case + int length = ASCIIFoldingFilter.foldToASCII( + input, 0, output, 0, input.length); + + // Convert to lower case, and check if output is different + boolean different = length != input.length; + for (int i = 0; i < length; i++) { + char c = output[i]; + if ('A'<= c && c <= 'Z') { + output[i] = (char) (c + 'a' - 'A'); + } + if (!different && input[i] != output[i]) { + different = true; + } + } + + if (different) { + comparable = new String(output, 0, length); + } + } + return comparable; + } + }; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NotQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NotQuery.java new file mode 100644 index 00000000000..7446bdc8a9c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/NotQuery.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; + +import java.io.IOException; +import java.util.Set; + +/** + * Implements a query that negates documents of a context query. Documents + * that matched the context query will not match the NotQuery and + * Documents that did not match the context query will be selected by this + * NotQuery. + */ +@SuppressWarnings("serial") +class NotQuery extends Query { + + /** + * The context query to invert. + */ + private final Query context; + + /** + * The context scorer to invert. + */ + private Scorer contextScorer; + + /** + * Creates a new NotQuery. + * @param context the context query. + */ + NotQuery(Query context) { + this.context = context; + } + + /** + * {@inheritDoc} + */ + public Weight createWeight(Searcher searcher) { + return new NotQueryWeight(searcher); + } + + /** + * {@inheritDoc} + */ + public String toString(String field) { + return "NotQuery"; + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + context.extractTerms(terms); + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + Query cQuery = context.rewrite(reader); + if (cQuery == context) { + return this; + } else { + return new NotQuery(cQuery); + } + } + + /** + * Implements a weight for this NotQuery. + */ + private class NotQueryWeight extends Weight { + + /** + * The searcher to access the index. + */ + private final Searcher searcher; + + /** + * Creates a new NotQueryWeight with a searcher. + * @param searcher the searcher. + */ + NotQueryWeight(Searcher searcher) { + this.searcher = searcher; + } + + /** + * @inheritDoc + */ + public Query getQuery() { + return NotQuery.this; + } + + /** + * @inheritDoc + */ + public float getValue() { + return 1.0f; + } + + /** + * @inheritDoc + */ + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + /** + * @inheritDoc + */ + public void normalize(float norm) { + } + + /** + * @inheritDoc + */ + public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + contextScorer = context.weight(searcher).scorer(reader, scoreDocsInOrder, topScorer); + if (contextScorer == null) { + // context query does not match any node + // the inverse is to match all nodes + return new MatchAllDocsQuery().createWeight(searcher).scorer( + reader, scoreDocsInOrder, false); + } + return new NotQueryScorer(reader); + } + + /** + * @throws UnsupportedOperationException always + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + throw new UnsupportedOperationException(); + } + } + + /** + * Implements a scorer that inverts the document matches of the context + * scorer. + */ + private class NotQueryScorer extends Scorer { + + /** + * The index reader. + */ + private final IndexReader reader; + + /** + * Current document number. + */ + private int docNo = -1; + + /** + * Current document number of the context scorer; + */ + private int contextNo = -1; + + private boolean firstTime = true; + + /** + * Creates a new scorer + * @param reader + */ + NotQueryScorer(IndexReader reader) { + super(Similarity.getDefault()); + this.reader = reader; + } + + @Override + public int nextDoc() throws IOException { + if (docNo == NO_MORE_DOCS) { + return docNo; + } + + if (firstTime) { + firstTime = false; + // get first doc of context scorer + int docId = contextScorer.nextDoc(); + if (docId != NO_MORE_DOCS) { + contextNo = docId; + } + } + // move to next candidate + do { + docNo++; + } while (reader.isDeleted(docNo) && docNo < reader.maxDoc()); + + // check with contextScorer + while (contextNo != -1 && contextNo == docNo) { + docNo++; + int docId = contextScorer.nextDoc(); + contextNo = docId == NO_MORE_DOCS ? -1 : docId; + } + if (docNo >= reader.maxDoc()) { + docNo = NO_MORE_DOCS; + } + return docNo; + } + + @Override + public int docID() { + return docNo; + } + + @Override + public float score() throws IOException { + return 1.0f; + } + + @Override + public int advance(int target) throws IOException { + if (docNo == NO_MORE_DOCS) { + return docNo; + } + + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + contextScorer.advance(target); + docNo = NO_MORE_DOCS; + return docNo; + } + + if (contextNo != -1 && contextNo < target) { + int docId = contextScorer.advance(target); + contextNo = docId == NO_MORE_DOCS ? -1 : docId; + } + docNo = target - 1; + return nextDoc(); + } + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.java new file mode 100644 index 00000000000..ddebe7f4791 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/OffsetCharSequence.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.core.query.lucene; + +/** + * CharSequence that applies an offset to a base CharSequence. The base + * CharSequence can be replaced without creating a new CharSequence. + */ +final class OffsetCharSequence implements CharSequence, Comparable, TransformConstants { + + /** + * Indicates how the underlying char sequence is exposed / tranformed. + */ + private final int transform; + + /** + * The offset to apply to the base CharSequence + */ + private final int offset; + + /** + * The base character sequence + */ + private CharSequence base; + + /** + * Creates a new OffsetCharSequence with an offset. + * + * @param offset the offset + * @param base the base CharSequence + * @param transform how the base char sequence is + * tranformed. + */ + OffsetCharSequence(int offset, CharSequence base, int transform) { + this.offset = offset; + this.base = base; + this.transform = transform; + } + + /** + * Creates a new OffsetCharSequence with an offset. + * + * @param offset the offset + * @param base the base CharSequence + */ + OffsetCharSequence(int offset, CharSequence base) { + this(offset, base, TRANSFORM_NONE); + } + + /** + * Sets a new base sequence. + * + * @param base the base character sequence + */ + public void setBase(CharSequence base) { + this.base = base; + } + + /** + * @inheritDoc + */ + public int length() { + return base.length() - offset; + } + + /** + * @inheritDoc + */ + public char charAt(int index) { + if (transform == TRANSFORM_NONE) { + return base.charAt(index + offset); + } else if (transform == TRANSFORM_LOWER_CASE) { + return Character.toLowerCase(base.charAt(index + offset)); + } else if (transform == TRANSFORM_UPPER_CASE) { + return Character.toUpperCase(base.charAt(index + offset)); + } + // shouldn't get here. return plain character + return base.charAt(index + offset); + } + + /** + * @inheritDoc + */ + public CharSequence subSequence(int start, int end) { + CharSequence seq = base.subSequence(start + offset, end + offset); + if (transform != TRANSFORM_NONE) { + seq = new OffsetCharSequence(0, seq, transform); + } + return seq; + } + + /** + * @inheritDoc + */ + public String toString() { + if (transform == TRANSFORM_NONE) { + return base.subSequence(offset, base.length()).toString(); + } else { + int len = length(); + StringBuffer buf = new StringBuffer(len); + for (int i = 0; i < len; i++) { + buf.append(charAt(i)); + } + return buf.toString(); + } + } + + //-----------------------------< Comparable >------------------------------- + + /** + * Compares this char sequence to another char sequence o. + * + * @param o the other char sequence. + * @return as defined in {@link String#compareTo(Object)} but also takes + * {@link #transform} into account. + */ + public int compareTo(OffsetCharSequence other) { + int len1 = length(); + int len2 = other.length(); + int lim = Math.min(len1, len2); + + for (int i = 0; i < lim; i++) { + char c1 = charAt(i); + char c2 = other.charAt(i); + if (c1 != c2) { + return c1 - c2; + } + } + return len1 - len2; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Ordering.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Ordering.java new file mode 100644 index 00000000000..a660adafa3d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Ordering.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.core.query.lucene; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.query.qom.DefaultTraversingQOMTreeVisitor; +import org.apache.jackrabbit.spi.commons.query.qom.DynamicOperandImpl; +import org.apache.jackrabbit.spi.commons.query.qom.FullTextSearchScoreImpl; +import org.apache.jackrabbit.spi.commons.query.qom.LengthImpl; +import org.apache.jackrabbit.spi.commons.query.qom.LowerCaseImpl; +import org.apache.jackrabbit.spi.commons.query.qom.NodeLocalNameImpl; +import org.apache.jackrabbit.spi.commons.query.qom.NodeNameImpl; +import org.apache.jackrabbit.spi.commons.query.qom.OrderingImpl; +import org.apache.jackrabbit.spi.commons.query.qom.PropertyValueImpl; +import org.apache.jackrabbit.spi.commons.query.qom.QOMTreeVisitor; +import org.apache.jackrabbit.spi.commons.query.qom.UpperCaseImpl; +import org.apache.lucene.search.SortField; + +import javax.jcr.RepositoryException; + +/** + * Ordering implements a single ordering specification. + */ +public class Ordering { + + /** + * The selector name where this ordering applies to. + */ + private final Name selectorName; + + /** + * The lucene sort field for this ordering. + */ + private final SortField sort; + + /** + * Private constructor. + * + * @param selectorName the selector name for this ordering. + * @param sort the lucene sort field for this ordering. + */ + private Ordering(Name selectorName, SortField sort) { + this.selectorName = selectorName; + this.sort = sort; + } + + /** + * @return the selector name where this ordering applies to. + */ + public Name getSelectorName() { + return selectorName; + } + + /** + * @return the lucene sort field for this ordering. + */ + public SortField getSortField() { + return sort; + } + + /** + * Creates an ordering from a JCR QOM ordering. + * + * @param ordering the JCR QOM ordering specification. + * @param scs the sort comparator source from the search index. + * @param nsMappings the index internal namespace mappings. + * @return an ordering. + * @throws RepositoryException if an error occurs while translating the JCR + * QOM ordering. + */ + public static Ordering fromQOM(final OrderingImpl ordering, + final SharedFieldComparatorSource scs, + final NamespaceMappings nsMappings) + throws RepositoryException { + final Name[] selectorName = new Name[1]; + QOMTreeVisitor visitor = new DefaultTraversingQOMTreeVisitor() { + + public Object visit(LengthImpl node, Object data) throws Exception { + PropertyValueImpl propValue = (PropertyValueImpl) node.getPropertyValue(); + selectorName[0] = propValue.getSelectorQName(); + return new SortField(propValue.getPropertyQName().toString(), + new LengthSortComparator(nsMappings), + !ordering.isAscending()); + } + + public Object visit(LowerCaseImpl node, Object data) + throws Exception { + SortField sf = (SortField) ((DynamicOperandImpl) node.getOperand()).accept(this, data); + selectorName[0] = node.getSelectorQName(); + return new SortField(sf.getField(), + new LowerCaseSortComparator(sf.getComparatorSource()), + !ordering.isAscending()); + } + + public Object visit(UpperCaseImpl node, Object data) + throws Exception { + SortField sf = (SortField) ((DynamicOperandImpl) node.getOperand()).accept(this, data); + selectorName[0] = node.getSelectorQName(); + return new SortField(sf.getField(), + new UpperCaseSortComparator(sf.getComparatorSource()), + !ordering.isAscending()); + } + + public Object visit(FullTextSearchScoreImpl node, Object data) + throws Exception { + selectorName[0] = node.getSelectorQName(); + return new SortField(null, SortField.SCORE, + !ordering.isAscending()); + } + + public Object visit(NodeLocalNameImpl node, Object data) throws Exception { + selectorName[0] = node.getSelectorQName(); + return new SortField(FieldNames.LOCAL_NAME, + SortField.STRING, !ordering.isAscending()); + } + + public Object visit(NodeNameImpl node, Object data) throws Exception { + selectorName[0] = node.getSelectorQName(); + return new SortField(FieldNames.LABEL, + SortField.STRING, !ordering.isAscending()); + } + + public Object visit(PropertyValueImpl node, Object data) + throws Exception { + selectorName[0] = node.getSelectorQName(); + return new SortField(node.getPropertyQName().toString(), + scs, !ordering.isAscending()); + } + + public Object visit(OrderingImpl node, Object data) + throws Exception { + return ((DynamicOperandImpl) node.getOperand()).accept(this, data); + } + }; + try { + SortField field = (SortField) ordering.accept(visitor, null); + return new Ordering(selectorName[0], field); + } catch (Exception e) { + throw new RepositoryException(e); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ParentAxisQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ParentAxisQuery.java new file mode 100644 index 00000000000..03765d23c38 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ParentAxisQuery.java @@ -0,0 +1,383 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector; +import org.apache.jackrabbit.core.query.lucene.hits.Hits; +import org.apache.jackrabbit.core.query.lucene.hits.ScorerHits; +import org.apache.jackrabbit.spi.Name; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; + +import java.io.IOException; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * ParentAxisQuery selects the parent nodes of a context query. + */ +@SuppressWarnings("serial") +class ParentAxisQuery extends Query { + + /** + * The context query + */ + private final Query contextQuery; + + /** + * The nameTest to apply on the parent axis, or null if any + * parent node should be selected. + */ + private final Name nameTest; + + /** + * The index format version. + */ + private final IndexFormatVersion version; + + /** + * The internal namespace mappings. + */ + private final NamespaceMappings nsMappings; + + /** + * The scorer of the context query + */ + private Scorer contextScorer; + + /** + * Creates a new ParentAxisQuery based on a + * context query. + * + * @param context the context for this query. + * @param nameTest a name test or null if any parent node is + * selected. + * @param version the index format version. + * @param nsMappings the internal namespace mappings. + */ + ParentAxisQuery(Query context, Name nameTest, + IndexFormatVersion version, NamespaceMappings nsMappings) { + this.contextQuery = context; + this.nameTest = nameTest; + this.version = version; + this.nsMappings = nsMappings; + } + + /** + * Creates a Weight instance for this query. + * + * @param searcher the Searcher instance to use. + * @return a ParentAxisWeight. + */ + public Weight createWeight(Searcher searcher) { + return new ParentAxisWeight(searcher); + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + contextQuery.extractTerms(terms); + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + Query cQuery = contextQuery.rewrite(reader); + if (cQuery == contextQuery) { + return this; + } else { + return new ParentAxisQuery(cQuery, nameTest, version, nsMappings); + } + } + + /** + * Always returns 'ParentAxisQuery'. + * + * @param field the name of a field. + * @return 'ParentAxisQuery'. + */ + public String toString(String field) { + StringBuffer sb = new StringBuffer(); + sb.append("ParentAxisQuery("); + sb.append(contextQuery); + sb.append(", "); + sb.append(nameTest); + sb.append(")"); + return sb.toString(); + } + + //-----------------------< ParentAxisWeight >------------------------------- + + /** + * The Weight implementation for this ParentAxisQuery. + */ + private class ParentAxisWeight extends Weight { + + /** + * The searcher in use + */ + private final Searcher searcher; + + /** + * Creates a new ParentAxisWeight instance using + * searcher. + * + * @param searcher a Searcher instance. + */ + private ParentAxisWeight(Searcher searcher) { + this.searcher = searcher; + } + + /** + * Returns this ParentAxisQuery. + * + * @return this ParentAxisQuery. + */ + public Query getQuery() { + return ParentAxisQuery.this; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public void normalize(float norm) { + } + + /** + * Creates a scorer for this ParentAxisQuery. + * + * @param reader a reader for accessing the index. + * @return a ParentAxisScorer. + * @throws IOException if an error occurs while reading from the index. + */ + public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + contextScorer = contextQuery.weight(searcher).scorer(reader, scoreDocsInOrder, false); + HierarchyResolver resolver = (HierarchyResolver) reader; + return new ParentAxisScorer(searcher.getSimilarity(), + reader, searcher, resolver); + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(); + } + } + + //--------------------------< ParentAxisScorer >---------------------------- + + /** + * Implements a Scorer for this ParentAxisQuery. + */ + private class ParentAxisScorer extends Scorer { + + /** + * An IndexReader to access the index. + */ + private final IndexReader reader; + + /** + * The HierarchyResolver of the index. + */ + private final HierarchyResolver hResolver; + + /** + * The searcher instance. + */ + private final Searcher searcher; + + /** + * BitSet storing the id's of selected documents + */ + private BitSet hits; + + /** + * Map that contains the scores from matching documents from the context + * query. To save memory only scores that are not equal to the score + * value of the first match are put to this map. + *

    + * key=[Integer] id of selected document from context query
    + * value=[Float] score for that document + */ + private final Map scores = new HashMap(); + + /** + * The next document id to return + */ + private int nextDoc = -1; + + /** + * The score of the first match. + */ + private Float firstScore; + + /** + * Creates a new ParentAxisScorer. + * + * @param similarity the Similarity instance to use. + * @param reader for index access. + * @param searcher the index searcher. + * @param resolver the hierarchy resolver. + */ + protected ParentAxisScorer(Similarity similarity, + IndexReader reader, + Searcher searcher, + HierarchyResolver resolver) { + super(similarity); + this.reader = reader; + this.searcher = searcher; + this.hResolver = resolver; + } + + @Override + public int nextDoc() throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + calculateParent(); + nextDoc = hits.nextSetBit(nextDoc + 1); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + @Override + public int docID() { + return nextDoc; + } + + @Override + public float score() throws IOException { + Float score = scores.get(nextDoc); + if (score == null) { + score = firstScore; + } + return score; + } + + @Override + public int advance(int target) throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + nextDoc = NO_MORE_DOCS; + return nextDoc; + } + calculateParent(); + nextDoc = hits.nextSetBit(target); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + private void calculateParent() throws IOException { + if (hits == null) { + hits = new BitSet(reader.maxDoc()); + + final IOException[] ex = new IOException[1]; + if (contextScorer != null) { + contextScorer.score(new AbstractHitCollector() { + private int[] docs = new int[1]; + + @Override + protected void collect(int doc, float score) { + try { + docs = hResolver.getParents(doc, docs); + if (docs.length == 1) { + // optimize single value + hits.set(docs[0]); + if (firstScore == null) { + firstScore = score; + } else if (firstScore != score) { + scores.put(doc, score); + } + } else { + for (int docNum : docs) { + hits.set(docNum); + if (firstScore == null) { + firstScore = score; + } else if (firstScore != score) { + scores.put(doc, score); + } + } + } + } catch (IOException e) { + ex[0] = e; + } + } + }); + } + + if (ex[0] != null) { + throw ex[0]; + } + + // filter out documents that do not match the name test + if (nameTest != null) { + Query nameQuery = new NameQuery(nameTest, version, nsMappings); + Hits nameHits = new ScorerHits(nameQuery.weight(searcher).scorer(reader, true, false)); + for (int i = hits.nextSetBit(0); i >= 0; i = hits.nextSetBit(i + 1)) { + int doc = nameHits.skipTo(i); + if (doc == -1) { + // no more name tests, clear remaining + hits.clear(i, hits.length()); + } else { + // assert doc >= i + if (doc > i) { + // clear hits + hits.clear(i, doc); + i = doc; + } + } + } + } + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PerQueryCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PerQueryCache.java new file mode 100644 index 00000000000..f05bee85796 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PerQueryCache.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.util.HashMap; +import java.util.Map; + +/** + * A cache of arbitrarily typed values used during the execution of a + * single query. + */ +class PerQueryCache { + + /** + * The internal map of this PerQueryCache. + */ + private final Map map = new HashMap(); + + /** + * Returns the value from the cache with the given type and + * key. + * + * @param type the query type. + * @param key the key object. + * @return the value assigned to type and key or + * null if it does not exist in the cache. + */ + Object get(Class type, Object key) { + return map.get(new Key(type, key)); + } + + /** + * Puts the value into the cache and assigns it to + * type and key. + * + * @param type the query type. + * @param key the key object. + * @param value the value to cache. + * @return the existing value in the cache assigned to type and + * key or null if there was none. + */ + Object put(Class type, Object key, Object value) { + return map.put(new Key(type, key), value); + } + + /** + * Simple key class. + */ + private static final class Key { + + /** + * The query type. + */ + private final Class type; + + /** + * The key object. + */ + private final Object key; + + /** + * Creates a new internal Key object. + * + * @param type the query type. + * @param key the key object. + */ + private Key(Class type, Object key) { + this.type = type; + this.key = key; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return type.hashCode() ^ key.hashCode(); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof Key) { + Key other = (Key) obj; + return type == other.type && key.equals(other.key); + } + return false; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PersistentIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PersistentIndex.java new file mode 100644 index 00000000000..c8bbf607831 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PersistentIndex.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.directory.DirectoryManager; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.index.IndexDeletionPolicy; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; + +/** + * Implements a lucene index which is based on a + * {@link org.apache.jackrabbit.core.fs.FileSystem}. + */ +class PersistentIndex extends AbstractIndex { + + /** The name of this persistent index */ + private final String name; + + /** + * If non null, listener needs to be informed + * when a document is deleted. + */ + private IndexListener listener; + + /** + * The index deletion policy. Old index generations are deleted when they + * reach a certain age. + */ + private final IndexDeletionPolicyImpl indexDelPolicy; + + /** + * The current generation of this persistent index. + */ + private long generation; + + /** + * Creates a new PersistentIndex. + * + * @param name the name of this index. + * @param analyzer the analyzer for text tokenizing. + * @param similarity the similarity implementation. + * @param cache the document number cache + * @param indexingQueue the indexing queue. + * @param directoryManager the directory manager. + * @param generationMaxAge age in seconds after which an index generation is + * deleted. + * @throws IOException if an error occurs while opening / creating the + * index. + */ + PersistentIndex(String name, Analyzer analyzer, + Similarity similarity, DocNumberCache cache, + IndexingQueue indexingQueue, + DirectoryManager directoryManager, long generationMaxAge) + throws IOException { + super(analyzer, similarity, directoryManager.getDirectory(name), + cache, indexingQueue); + this.name = name; + this.indexDelPolicy = new IndexDeletionPolicyImpl(this, + generationMaxAge * 1000); + if (isExisting()) { + IndexMigration.migrate(this, directoryManager, '\uFFFF'); + } + } + + /** + * @inheritDoc + */ + int removeDocument(Term idTerm) throws IOException { + int num = super.removeDocument(idTerm); + if (num > 0 && listener != null) { + listener.documentDeleted(idTerm); + } + return num; + } + + /** + * @return the index deletion policy of this index. + */ + protected IndexDeletionPolicy getIndexDeletionPolicy() { + return indexDelPolicy; + } + + /** + * Merges the provided indexes into this index. After this completes, the + * index is optimized. + *

    + * The provided IndexReaders are not closed. + * + * @param readers the readers of indexes to add. + * @throws IOException if an error occurs while adding indexes. + */ + void addIndexes(IndexReader[] readers) throws IOException { + getIndexWriter().addIndexes(readers); + getIndexWriter().optimize(); + } + + /** + * Copies index into this persistent index. This method should + * only be called when this index is empty otherwise the + * behaviour is undefined. + * + * @param index the index to copy from. + * @throws IOException if an error occurs while copying. + */ + void copyIndex(AbstractIndex index) throws IOException { + // commit changes to directory on other index. + index.commit(true); + // simply copy over the files + byte[] buffer = new byte[1024]; + Directory dir = index.getDirectory(); + Directory dest = getDirectory(); + String[] files = dir.listAll(); + for (String file : files) { + IndexInput in = dir.openInput(file); + try { + IndexOutput out = dest.createOutput(file); + try { + long remaining = in.length(); + while (remaining > 0) { + int num = (int) Math.min(remaining, buffer.length); + in.readBytes(buffer, 0, num); + out.writeBytes(buffer, num); + remaining -= num; + } + } finally { + out.close(); + } + } finally { + in.close(); + } + } + // refresh current generation + indexDelPolicy.readCurrentGeneration(); + } + + /** + * Returns a ReadOnlyIndexReader and registeres + * listener to send notifications when documents are deleted on + * this index. + * + * @param listener the listener to notify when documents are deleted. + * @return a ReadOnlyIndexReader. + * @throws IOException if the reader cannot be obtained. + */ + synchronized ReadOnlyIndexReader getReadOnlyIndexReader(IndexListener listener) + throws IOException { + ReadOnlyIndexReader reader = getReadOnlyIndexReader(); + this.listener = listener; + return reader; + } + + /** + * Removes a potentially registered {@link IndexListener}. + */ + synchronized void resetListener() { + this.listener = null; + } + + /** + * Returns the number of documents in this persistent index. + * + * @return the number of documents in this persistent index. + * @throws IOException if an error occurs while reading from the index. + */ + int getNumDocuments() throws IOException { + return getIndexReader().numDocs(); + } + + /** + * Returns the name of this index. + * @return the name of this index. + */ + String getName() { + return name; + } + + /** + * @return the current generation of this index. + */ + long getCurrentGeneration() { + return generation; + } + + /** + * Sets the current generation of this index. This method should only be + * called by {@link IndexDeletionPolicyImpl}. + * + * @param generation the current generation. + */ + void setCurrentGeneration(long generation) { + this.generation = generation; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PredicateDerefQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PredicateDerefQuery.java new file mode 100644 index 00000000000..b09d829812c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PredicateDerefQuery.java @@ -0,0 +1,369 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector; +import org.apache.jackrabbit.spi.Name; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Weight; + +import java.io.IOException; +import java.util.BitSet; +import java.util.Set; + +/** + * Implements a Lucene Query which returns the nodes which have a + * reference property which matches the nodes of the subquery. + */ +@SuppressWarnings("serial") +public class PredicateDerefQuery extends Query { + + /** + * The context query + */ + private final Query subQuery; + + /** + * The name of the reference property. + */ + private final String refProperty; + + /** + * The nameTest to apply on target node, or null if all + * target nodes should be selected. + */ + private final Name nameTest; + + /** + * The index format version. + */ + private final IndexFormatVersion version; + + /** + * The internal namespace mappings. + */ + private final NamespaceMappings nsMappings; + + /** + * The scorer of the context query + */ + private Scorer subQueryScorer; + + /** + * The scorer of the name test query + */ + private Scorer nameTestScorer; + /** + * Creates a new DerefQuery based on a context + * query. + * + * @param subQuery TODO + * @param refProperty the name of the reference property. + * @param nameTest a name test or null if any node is + * selected. + * @param version the index format version. + * @param nsMappings the namespace mappings. + */ + PredicateDerefQuery(Query subQuery, String refProperty, + Name nameTest, IndexFormatVersion version, NamespaceMappings nsMappings) { + this.subQuery = subQuery; + this.refProperty = refProperty; + this.nameTest = nameTest; + this.version = version; + this.nsMappings = nsMappings; + } + + /** + * Creates a Weight instance for this query. + * + * @param searcher the Searcher instance to use. + * @return a DerefWeight. + */ + public Weight createWeight(Searcher searcher) { + return new DerefWeight(searcher); + } + + /** + * Returns PredicateDerefQuery(subQuery, referenceNodeProperty, nameTest) + * + * @param field the name of a field. + * @return 'DerefQuery'. + */ + public String toString(String field) { + StringBuffer sb = new StringBuffer(); + sb.append("PredicateDerefQuery("); + sb.append(subQuery); + sb.append(", "); + sb.append(nameTest); + sb.append(", "); + sb.append(refProperty); + sb.append(")"); + return sb.toString(); + } + + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + // no terms to extract + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + Query cQuery = subQuery.rewrite(reader); + if (cQuery == subQuery) { + return this; + } else { + return new PredicateDerefQuery(cQuery, refProperty, nameTest, version, nsMappings); + } + } + + //-------------------< DerefWeight >------------------------------------ + + /** + * The Weight implementation for this DerefQuery. + */ + private class DerefWeight extends Weight { + + /** + * The searcher in use + */ + private final Searcher searcher; + + /** + * Creates a new DerefWeight instance using + * searcher. + * + * @param searcher a Searcher instance. + */ + private DerefWeight(Searcher searcher) { + this.searcher = searcher; + } + + /** + * Returns this DerefQuery. + * + * @return this DerefQuery. + */ + public Query getQuery() { + return PredicateDerefQuery.this; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public void normalize(float norm) { + } + + /** + * Creates a scorer for this DerefQuery. + * + * @param reader a reader for accessing the index. + * @return a DerefScorer. + * @throws IOException if an error occurs while reading from the index. + */ + public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + subQueryScorer = subQuery.weight(searcher).scorer(reader, scoreDocsInOrder, false); + if (nameTest != null) { + nameTestScorer = new NameQuery(nameTest, version, nsMappings).weight(searcher).scorer(reader, scoreDocsInOrder, false); + } + return new DerefScorer(searcher.getSimilarity(), reader); + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(); + } + } + + //----------------------< DerefScorer >--------------------------------- + + /** + * Implements a Scorer for this DerefQuery. + */ + private class DerefScorer extends Scorer { + + /** + * An IndexReader to access the index. + */ + private final IndexReader reader; + + /** + * BitSet storing the id's of selected documents + */ + private final BitSet subQueryHits; + + /** + * BitSet storing the id's of selected documents + */ + private final BitSet hits; + + + /** + * The next document id to return + */ + private int nextDoc = -1; + + /** + * Creates a new DerefScorer. + * + * @param similarity the Similarity instance to use. + * @param reader for index access. + */ + protected DerefScorer(Similarity similarity, IndexReader reader) { + super(similarity); + this.reader = reader; + this.hits = new BitSet(reader.maxDoc()); + this.subQueryHits = new BitSet(reader.maxDoc()); + } + + @Override + public int nextDoc() throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + calculateChildren(); + nextDoc = hits.nextSetBit(nextDoc + 1); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + @Override + public int docID() { + return nextDoc; + } + + @Override + public float score() throws IOException { + return 1.0f; + } + + @Override + public int advance(int target) throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + nextDoc = NO_MORE_DOCS; + return nextDoc; + } + + calculateChildren(); + nextDoc = hits.nextSetBit(target); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + /** + * Perform the sub query + * For each reference property UUID + * - find document number + * - if document # is in subquery bitset add to bit set + * Use the name test to filter the documents + * @throws IOException + */ + private void calculateChildren() throws IOException { +// subQueryHits.clear(); +// hits.clear(); + subQueryScorer.score(new AbstractHitCollector() { + @Override + protected void collect(int doc, float score) { + subQueryHits.set(doc); + } + }); + + TermDocs termDocs = reader.termDocs(new Term(FieldNames.PROPERTIES_SET, refProperty)); + String prefix = FieldNames.createNamedValue(refProperty, ""); + while (termDocs.next()) { + int doc = termDocs.doc(); + + String[] values = reader.document(doc).getValues(FieldNames.PROPERTIES); + if (values == null) { + // no reference properties at all on this node + continue; + } + for (int v = 0; v < values.length; v++) { + if (values[v].startsWith(prefix)) { + String uuid = values[v].substring(prefix.length()); + + TermDocs node = reader.termDocs(TermFactory.createUUIDTerm(uuid)); + try { + while (node.next()) { + if (subQueryHits.get(node.doc())) { + hits.set(doc); + } + } + } finally { + node.close(); + } + } + } + } + + // collect nameTest hits + final BitSet nameTestHits = new BitSet(); + if (nameTestScorer != null) { + nameTestScorer.score(new AbstractHitCollector() { + @Override + protected void collect(int doc, float score) { + nameTestHits.set(doc); + } + }); + } + + // filter out the target nodes that do not match the name test + // if there is any name test at all. + if (nameTestScorer != null) { + hits.and(nameTestHits); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PropertiesSynonymProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PropertiesSynonymProvider.java new file mode 100644 index 00000000000..e5aa66c8078 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PropertiesSynonymProvider.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements a synonym provider based on a properties file. Each line in the + * properties file is treated as a synonym definition. Example: + *

    + * A=B
    + * B=C
    + * 
    + * This synonym provider will return B as a synonym for A and vice versa. The + * same applies to B and C. However A is not considered a synonym for C, nor + * C a synonym for A. + */ +public class PropertiesSynonymProvider implements SynonymProvider { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(PropertiesSynonymProvider.class); + + /** + * An empty string array. Returned when no synonym is found. + */ + private static final String[] EMPTY_ARRAY = new String[0]; + + /** + * Check at most every 10 seconds for configuration updates. + */ + private static final long CHECK_INTERVAL = 10 * 1000; + + /** + * The file system resource that contains the configuration. + */ + private FileSystemResource config; + + /** + * Timestamp when the configuration was checked last. + */ + private long lastCheck; + + /** + * Timestamp when the configuration was last modified. + */ + private long configLastModified; + + /** + * Contains the synonym mapping. Map<String, String[]> + */ + private Map synonyms = new HashMap(); + + /** + * {@inheritDoc} + */ + public synchronized void initialize(FileSystemResource fsr) + throws IOException { + if (fsr == null) { + throw new IOException("PropertiesSynonymProvider requires a path configuration"); + } + try { + config = fsr; + synonyms = getSynonyms(config); + configLastModified = config.lastModified(); + lastCheck = System.currentTimeMillis(); + } catch (FileSystemException e) { + throw Util.createIOException(e); + } + } + + /** + * {@inheritDoc} + */ + public String[] getSynonyms(String term) { + checkConfigUpdated(); + term = term.toLowerCase(); + String[] syns; + synchronized (this) { + syns = synonyms.get(term); + } + if (syns == null) { + syns = EMPTY_ARRAY; + } + return syns; + } + + //---------------------------------< internal >----------------------------- + + /** + * Checks if the synonym properties file has been updated and this provider + * should reload the synonyms. This method performs the actual check at most + * every {@link #CHECK_INTERVAL}. If reloading fails an error is logged and + * this provider will retry after {@link #CHECK_INTERVAL}. + */ + private synchronized void checkConfigUpdated() { + if (lastCheck + CHECK_INTERVAL > System.currentTimeMillis()) { + return; + } + // check last modified + try { + if (configLastModified != config.lastModified()) { + synonyms = getSynonyms(config); + configLastModified = config.lastModified(); + log.info("Reloaded synonyms from {}", config.getPath()); + } + } catch (Exception e) { + log.error("Exception while reading synonyms", e); + } + // update lastCheck timestamp, even if error occurred (retry later) + lastCheck = System.currentTimeMillis(); + } + + /** + * Reads the synonym properties file and returns the contents as a synonym + * Map. + * + * @param config the synonym properties file. + * @return a Map containing the synonyms. + * @throws IOException if an error occurs while reading from the file system + * resource. + */ + private static Map getSynonyms(FileSystemResource config) throws IOException { + try { + Map synonyms = new HashMap(); + Properties props = new Properties(); + props.load(config.getInputStream()); + for (Map.Entry entry : props.entrySet()) { + String key = (String) entry.getKey(); + String value = (String) entry.getValue(); + addSynonym(key, value, synonyms); + addSynonym(value, key, synonyms); + } + return synonyms; + } catch (FileSystemException e) { + throw Util.createIOException(e); + } + } + + /** + * Adds a synonym definition to the map. + * + * @param term the term + * @param synonym synonym for term. + * @param synonyms the Map containing the synonyms. + */ + private static void addSynonym(String term, String synonym, Map synonyms) { + term = term.toLowerCase(); + String[] syns = synonyms.get(term); + if (syns == null) { + syns = new String[]{synonym}; + } else { + String[] tmp = new String[syns.length + 1]; + System.arraycopy(syns, 0, tmp, 0, syns.length); + tmp[syns.length] = synonym; + syns = tmp; + } + synonyms.put(term, syns); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PropertyMetaData.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PropertyMetaData.java new file mode 100644 index 00000000000..2c94694420c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/PropertyMetaData.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.core.query.lucene; + +/** + * PropertyMetaData encapsulates the payload byte array and + * provides methods to access the property meta data. + */ +public final class PropertyMetaData { + + /** + * The property type. + */ + private final int propertyType; + + /** + * Creates a new PropertyMetaData with the given propertyType. + * + * @param propertyType the property type. + */ + public PropertyMetaData(int propertyType) { + this.propertyType = propertyType; + } + + /** + * @return the property type. + * @see javax.jcr.PropertyType + */ + public int getPropertyType() { + return propertyType; + } + + /** + * Creates a PropertyMetaData from a byte array. + * + * @param data the payload data array. + * @return a PropertyMetaData from a byte array. + */ + public static PropertyMetaData fromByteArray(byte[] data) { + return new PropertyMetaData(data[0]); + } + + /** + * @return returns a byte array representation of this PropertyMetaData for + * use as a lucene token payload. + */ + public byte[] toByteArray() { + return new byte[]{(byte) propertyType}; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHits.java new file mode 100644 index 00000000000..3368d1fd6f9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHits.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.core.query.lucene; + +import java.io.IOException; + +/** + * Defines an interface for reading {@link ScoreNode}s + */ +public interface QueryHits extends CloseableHits { + + /** + * Returns the next score node in this QueryHits or null if + * there are no more score nodes. + * + * @return the next score node in this QueryHits. + * @throws IOException if an error occurs while reading from the index. + */ + ScoreNode nextScoreNode() throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHitsAdapter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHitsAdapter.java new file mode 100644 index 00000000000..04fdb56cc68 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHitsAdapter.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.core.query.lucene; + +import org.apache.jackrabbit.spi.Name; + +import java.io.IOException; + +/** + * QueryHitsAdapter implements an adapter for {@link QueryHits} and + * exposes them as {@link MultiColumnQueryHits}. + */ +public class QueryHitsAdapter implements MultiColumnQueryHits { + + /** + * The query hits to adapt. + */ + private final QueryHits hits; + + /** + * The single selector name to expose. + */ + private final Name selectorName; + + /** + * Creates a new adapter for hits. + * + * @param hits the query hits to adapt. + * @param selectorName the single selector name for the query hits. + */ + public QueryHitsAdapter(QueryHits hits, Name selectorName) { + this.hits = hits; + this.selectorName = selectorName; + } + + /** + * {@inheritDoc} + */ + public ScoreNode[] nextScoreNodes() throws IOException { + ScoreNode sn = hits.nextScoreNode(); + if (sn != null) { + return new ScoreNode[]{sn}; + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + public Name[] getSelectorNames() { + return new Name[]{selectorName}; + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + hits.close(); + } + + /** + * {@inheritDoc} + */ + public int getSize() { + return hits.getSize(); + } + + /** + * {@inheritDoc} + */ + public void skip(int n) throws IOException { + hits.skip(n); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHitsQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHitsQuery.java new file mode 100644 index 00000000000..787bacfa4ad --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryHitsQuery.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Weight; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Sort; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.jackrabbit.core.SessionImpl; + +import java.io.IOException; +import java.util.Set; +import java.util.Map; +import java.util.Iterator; +import java.util.HashMap; +import java.util.TreeSet; + +/** + * QueryHitsQuery exposes a {@link QueryHits} implementation again + * as a Lucene Query. + */ +@SuppressWarnings("serial") +public class QueryHitsQuery extends Query implements JackrabbitQuery{ + + /** + * The underlying query hits. + */ + private final QueryHits hits; + + /** + * Creates a new query based on {@link QueryHits}. + * + * @param hits the query hits. + */ + public QueryHitsQuery(QueryHits hits) { + this.hits = hits; + } + + /** + * {@inheritDoc} + */ + public Weight createWeight(Searcher searcher) throws IOException { + return new QueryHitsQueryWeight(searcher.getSimilarity()); + } + + /** + * {@inheritDoc} + */ + public String toString(String field) { + return "QueryHitsQuery"; + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + // no terms + } + + //-----------------------< JackrabbitQuery >-------------------------------- + + /** + * {@inheritDoc} + */ + public QueryHits execute(JackrabbitIndexSearcher searcher, + SessionImpl session, + Sort sort) throws IOException { + if (sort.getSort().length == 0) { + return hits; + } else { + return null; + } + } + + //------------------------< QueryHitsQueryWeight >-------------------------- + + /** + * The Weight implementation for this query. + */ + public class QueryHitsQueryWeight extends Weight { + + /** + * The similarity. + */ + private final Similarity similarity; + + /** + * Creates a new weight with the given similarity. + * + * @param similarity the similarity. + */ + public QueryHitsQueryWeight(Similarity similarity) { + this.similarity = similarity; + } + + /** + * {@inheritDoc} + */ + public Query getQuery() { + return QueryHitsQuery.this; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public void normalize(float norm) { + } + + /** + * {@inheritDoc} + */ + public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) throws IOException { + return new QueryHitsQueryScorer(reader, similarity); + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(); + } + } + + //-------------------< QueryHitsQueryScorer >------------------------------- + + /** + * the scorer implementation for this query. + */ + public class QueryHitsQueryScorer extends Scorer { + + /** + * Iterator over Integer instances identifying the + * lucene documents. Document numbers are iterated in ascending order. + */ + private final Iterator docs; + + /** + * Maps Integer document numbers to Float + * scores. + */ + private final Map scores = new HashMap(); + + /** + * The current document number. + */ + private Integer currentDoc = null; + + /** + * Creates a new scorer. + * + * @param reader the index reader. + * @param similarity the similarity implementation. + * @throws IOException if an error occurs while reading from the index. + */ + protected QueryHitsQueryScorer(IndexReader reader, + Similarity similarity) + throws IOException { + super(similarity); + ScoreNode node; + Set sortedDocs = new TreeSet(); + try { + while ((node = hits.nextScoreNode()) != null) { + String uuid = node.getNodeId().toString(); + Term id = TermFactory.createUUIDTerm(uuid); + TermDocs tDocs = reader.termDocs(id); + try { + if (tDocs.next()) { + Integer doc = tDocs.doc(); + sortedDocs.add(doc); + scores.put(doc, node.getScore()); + } + } finally { + tDocs.close(); + } + } + } finally { + hits.close(); + } + docs = sortedDocs.iterator(); + } + + @Override + public int nextDoc() throws IOException { + if (currentDoc == NO_MORE_DOCS) { + return currentDoc; + } + + currentDoc = docs.hasNext() ? docs.next() : NO_MORE_DOCS; + return currentDoc; + } + + @Override + public int docID() { + return currentDoc == null ? -1 : currentDoc; + } + + @Override + public float score() throws IOException { + return scores.get(currentDoc); + } + + @Override + public int advance(int target) throws IOException { + if (currentDoc == NO_MORE_DOCS) { + return currentDoc; + } + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + currentDoc = NO_MORE_DOCS; + return currentDoc; + } + + do { + if (nextDoc() == NO_MORE_DOCS) { + return NO_MORE_DOCS; + } + } while (target > docID()); + return docID(); + } + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryImpl.java new file mode 100644 index 00000000000..407e0095ee8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryImpl.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.query.PropertyTypeRegistry; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; +import org.apache.jackrabbit.spi.commons.query.AndQueryNode; +import org.apache.jackrabbit.spi.commons.query.DefaultQueryNodeVisitor; +import org.apache.jackrabbit.spi.commons.query.LocationStepQueryNode; +import org.apache.jackrabbit.spi.commons.query.NodeTypeQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrderQueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryNodeFactory; +import org.apache.jackrabbit.spi.commons.query.QueryParser; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.qom.ColumnImpl; +import org.apache.lucene.search.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.QueryResult; +import javax.jcr.query.qom.QueryObjectModelFactory; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Implements the {@link org.apache.jackrabbit.core.query.ExecutableQuery} + * interface. + */ +public class QueryImpl extends AbstractQueryImpl { + + /** + * The logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(QueryImpl.class); + + /** + * The default selector name 's'. + */ + public static final Name DEFAULT_SELECTOR_NAME = NameFactoryImpl.getInstance().create("", "s"); + + /** + * The root node of the query tree + */ + protected final QueryRootNode root; + + /** + * Creates a new query instance from a query string. + * + * @param sessionContext component context of the current session + * @param index the search index. + * @param propReg the property type registry. + * @param statement the query statement. + * @param language the syntax of the query statement. + * @param factory the query node factory. + * @throws InvalidQueryException if the query statement is invalid according + * to the specified language. + */ + public QueryImpl( + SessionContext sessionContext, SearchIndex index, + PropertyTypeRegistry propReg, String statement, String language, + QueryNodeFactory factory) throws InvalidQueryException { + super(sessionContext, index, propReg); + // parse query according to language + // build query tree using the passed factory + this.root = QueryParser.parse( + statement, language, sessionContext, factory); + } + + /** + * Executes this query and returns a {@link QueryResult}. + * + * @param offset the offset in the total result set + * @param limit the maximum result size + * @return a QueryResult + * @throws RepositoryException if an error occurs + */ + public QueryResult execute(long offset, long limit) throws RepositoryException { + if (log.isDebugEnabled()) { + log.debug("Executing query: \n" + root.dump()); + } + + // build lucene query + Query query = LuceneQueryBuilder.createQuery( + root, sessionContext.getSessionImpl(), + index.getContext().getItemStateManager(), + index.getNamespaceMappings(), index.getTextAnalyzer(), + propReg, index.getSynonymProvider(), + index.getIndexFormatVersion(), + cache); + + OrderQueryNode orderNode = root.getOrderNode(); + + OrderQueryNode.OrderSpec[] orderSpecs; + if (orderNode != null) { + orderSpecs = orderNode.getOrderSpecs(); + } else { + orderSpecs = new OrderQueryNode.OrderSpec[0]; + } + Path[] orderProperties = new Path[orderSpecs.length]; + boolean[] ascSpecs = new boolean[orderSpecs.length]; + String[] orderFuncs = new String[orderSpecs.length]; + for (int i = 0; i < orderSpecs.length; i++) { + orderProperties[i] = orderSpecs[i].getPropertyPath(); + ascSpecs[i] = orderSpecs[i].isAscending(); + orderFuncs[i] = orderSpecs[i].getFunction(); + } + + return new SingleColumnQueryResult( + index, sessionContext, this, query, + new SpellSuggestion(index.getSpellChecker(), root), + getColumns(), orderProperties, ascSpecs, orderFuncs, + orderProperties.length == 0 && getRespectDocumentOrder(), + offset, limit); + } + + /** + * Returns the columns for this query. + * + * @return array of columns. + * @throws RepositoryException if an error occurs. + */ + protected ColumnImpl[] getColumns() throws RepositoryException { + SessionImpl session = sessionContext.getSessionImpl(); + QueryObjectModelFactory qomFactory = + session.getWorkspace().getQueryManager().getQOMFactory(); + // get columns + Map columns = new LinkedHashMap(); + for (Name name : root.getSelectProperties()) { + String pn = sessionContext.getJCRName(name); + ColumnImpl col = (ColumnImpl) qomFactory.column( + sessionContext.getJCRName(DEFAULT_SELECTOR_NAME), pn, pn); + columns.put(name, col); + } + if (columns.size() == 0) { + // use node type constraint + LocationStepQueryNode[] steps = root.getLocationNode().getPathSteps(); + final Name[] ntName = new Name[1]; + steps[steps.length - 1].acceptOperands(new DefaultQueryNodeVisitor() { + + public Object visit(AndQueryNode node, Object data) throws RepositoryException { + return node.acceptOperands(this, data); + } + + public Object visit(NodeTypeQueryNode node, Object data) { + ntName[0] = node.getValue(); + return data; + } + }, null); + if (ntName[0] == null) { + ntName[0] = NameConstants.NT_BASE; + } + NodeTypeImpl nt = session.getNodeTypeManager().getNodeType(ntName[0]); + PropertyDefinition[] propDefs = nt.getPropertyDefinitions(); + for (PropertyDefinition pd : propDefs) { + QPropertyDefinition propDef = ((PropertyDefinitionImpl) pd).unwrap(); + if (!propDef.definesResidual() && !propDef.isMultiple()) { + columns.put(propDef.getName(), columnForName(propDef.getName())); + } + } + } + + // add jcr:path and jcr:score if not selected already + if (!columns.containsKey(NameConstants.JCR_PATH)) { + columns.put(NameConstants.JCR_PATH, columnForName(NameConstants.JCR_PATH)); + } + if (!columns.containsKey(NameConstants.JCR_SCORE)) { + columns.put(NameConstants.JCR_SCORE, columnForName(NameConstants.JCR_SCORE)); + } + + return columns.values().toArray(new ColumnImpl[columns.size()]); + } + + /** + * Returns true if this query node needs items under + * /jcr:system to be queried. + * + * @return true if this query node needs content under + * /jcr:system to be queried; false otherwise. + */ + public boolean needsSystemTree() { + return this.root.needsSystemTree(); + } + + /** + * Returns a column for the given property name and the default selector + * name. + * + * @param propertyName the name of the property as well as the column. + * @return a column. + * @throws RepositoryException if an error occurs while creating the column. + */ + protected ColumnImpl columnForName(Name propertyName) throws RepositoryException { + Workspace workspace = sessionContext.getSessionImpl().getWorkspace(); + QueryObjectModelFactory qomFactory = + workspace.getQueryManager().getQOMFactory(); + String name = sessionContext.getJCRName(propertyName); + return (ColumnImpl) qomFactory.column( + sessionContext.getJCRName(DEFAULT_SELECTOR_NAME), name, name); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java new file mode 100644 index 00000000000..6fe9220a467 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/QueryResultImpl.java @@ -0,0 +1,585 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.api.query.JackrabbitQueryResult; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.query.qom.ColumnImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements the QueryResult interface. + */ +public abstract class QueryResultImpl implements JackrabbitQueryResult { + + /** + * The logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(QueryResultImpl.class); + + /** + * The search index to execute the query. + */ + protected final SearchIndex index; + + /** + * Component context of the current session + */ + protected final SessionContext sessionContext; + + /** + * The query instance which created this query result. + */ + protected final AbstractQueryImpl queryImpl; + + /** + * The spell suggestion or null if not available. + */ + protected final SpellSuggestion spellSuggestion; + + /** + * The columns to select. + */ + protected final Map columns = new LinkedHashMap(); + + /** + * The result nodes including their score. This list is populated on a lazy + * basis while a client iterates through the results. + *

    + * The exact type is: List<ScoreNode[]> + */ + private final List resultNodes = new ArrayList(); + + /** + * This is the raw number of results that matched the query, ignoring limit and offset. Only set when accurate. + */ + private int totalResults = -1; + + /** + * This is the number of results that matched the query, with limit and offset. Only set when accurate. + */ + private int numResults = -1; + + /** + * The selector names associated with the score nodes. The selector names + * are set when the query is executed via {@link #getResults(long)}. + */ + private Name[] selectorNames; + + /** + * The number of results that are invalid, either because a node does not + * exist anymore or because the session does not have access to the node. + */ + private int invalid = 0; + + /** + * If true nodes are returned in document order. + */ + protected final boolean docOrder; + + /** + * The excerpt provider or null if none was created yet. + */ + private ExcerptProvider excerptProvider; + + /** + * The offset in the total result set + */ + private final long offset; + + /** + * The maximum size of this result if limit >= 0 + */ + private final long limit; + + private final boolean sizeEstimate; + + /** + * Creates a new query result. The concrete sub class is responsible for + * calling {@link #getResults(long)} after this constructor had been called. + * + * @param index the search index where the query is executed. + * @param sessionContext component context of the current session + * @param queryImpl the query instance which created this query + * result. + * @param spellSuggestion the spell suggestion or null if none + * is available. + * @param columns the select properties of the query. + * @param documentOrder if true the result is returned in + * document order. + * @param limit the maximum result size + * @param offset the offset in the total result set + * @throws RepositoryException if an error occurs while reading from the + * repository. + * @throws IllegalArgumentException if any of the columns does not have a + * column name. + */ + public QueryResultImpl( + SearchIndex index, SessionContext sessionContext, + AbstractQueryImpl queryImpl, SpellSuggestion spellSuggestion, + ColumnImpl[] columns, boolean documentOrder, + long offset, long limit) throws RepositoryException { + this.index = index; + this.sizeEstimate = index.getSizeEstimate(); + this.sessionContext = sessionContext; + this.queryImpl = queryImpl; + this.spellSuggestion = spellSuggestion; + this.docOrder = documentOrder; + this.offset = offset; + this.limit = limit; + for (ColumnImpl column : columns) { + String cn = column.getColumnName(); + if (cn == null) { + String msg = column + " does not have a column name"; + throw new IllegalArgumentException(msg); + } + this.columns.put(cn, column); + } + } + + /** + * {@inheritDoc} + */ + public String[] getSelectorNames() throws RepositoryException { + String[] names = new String[selectorNames.length]; + for (int i = 0; i < selectorNames.length; i++) { + names[i] = sessionContext.getJCRName(selectorNames[i]); + } + return names; + } + + /** + * {@inheritDoc} + */ + public String[] getColumnNames() throws RepositoryException { + return columns.keySet().toArray(new String[columns.size()]); + } + + /** + * {@inheritDoc} + */ + public NodeIterator getNodes() throws RepositoryException { + return new NodeIteratorImpl(sessionContext, getScoreNodes(), 0); + } + + /** + * {@inheritDoc} + */ + public RowIterator getRows() throws RepositoryException { + if (excerptProvider == null) { + try { + excerptProvider = createExcerptProvider(); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + return new RowIteratorImpl( + getScoreNodes(), columns, + selectorNames, sessionContext.getItemManager(), + index.getContext().getHierarchyManager(), + sessionContext, + sessionContext.getSessionImpl().getValueFactory(), + excerptProvider, spellSuggestion); + } + + /** + * Executes the query for this result and returns hits. The caller must + * close the query hits when he is done using it. + * + * @param resultFetchHint a hint on how many results should be fetched. + * @return hits for this query result. + * @throws IOException if an error occurs while executing the query. + */ + protected abstract MultiColumnQueryHits executeQuery(long resultFetchHint) + throws IOException; + + /** + * Creates an excerpt provider for this result set. + * + * @return an excerpt provider. + * @throws IOException if an error occurs. + */ + protected abstract ExcerptProvider createExcerptProvider() + throws IOException; + + //--------------------------------< internal >------------------------------ + + /** + * Creates a {@link ScoreNodeIterator} over the query result. + * + * @return a {@link ScoreNodeIterator} over the query result. + */ + private ScoreNodeIterator getScoreNodes() { + if (docOrder) { + return new DocOrderScoreNodeIterator( + sessionContext.getItemManager(), resultNodes, 0); + } else { + return new LazyScoreNodeIteratorImpl(); + } + } + + /** + * Attempts to get size results and puts them into {@link + * #resultNodes}. If the size of {@link #resultNodes} is less than + * size then there are no more than resultNodes.size() + * results for this query. + * + * @param size the number of results to fetch for the query. + * @throws RepositoryException if an error occurs while executing the + * query. + */ + protected void getResults(long size) throws RepositoryException { + if (log.isDebugEnabled()) { + log.debug("getResults({}) limit={}", size, limit); + } + + if (!sizeEstimate) { + // quick check + // if numResults is set, all relevant results have been fetched + if (numResults != -1) { + return; + } + } + + long maxResultSize = size; + + // is there any limit? + if (limit >= 0) { + maxResultSize = limit; + } + + if (resultNodes.size() >= maxResultSize && selectorNames != null) { + // we already have them all + return; + } + + // execute it + MultiColumnQueryHits result = null; + try { + long time = System.currentTimeMillis(); + long r1 = IOCounters.getReads(); + result = executeQuery(maxResultSize); + long r2 = IOCounters.getReads(); + log.debug("query executed in {} ms ({})", + System.currentTimeMillis() - time, r2 - r1); + // set selector names + selectorNames = result.getSelectorNames(); + + List offsetNodes = new ArrayList(); + if (resultNodes.isEmpty() && offset > 0) { + // collect result offset into dummy list + if (sizeEstimate) { + collectScoreNodes(result, new ArrayList(), offset); + } else { + collectScoreNodes(result, offsetNodes, offset); + } + } else { + int start = resultNodes.size() + invalid + (int) offset; + result.skip(start); + } + + time = System.currentTimeMillis(); + collectScoreNodes(result, resultNodes, maxResultSize); + long r3 = IOCounters.getReads(); + log.debug("retrieved ScoreNodes in {} ms ({})", + System.currentTimeMillis() - time, r3 - r2); + + if (sizeEstimate) { + // update numResults + numResults = result.getSize(); + } else { + // update numResults if all results have been fetched + // if resultNodes.getSize() is strictly smaller than maxResultSize, it means that all results have been fetched + int resultSize = resultNodes.size(); + if (resultSize < maxResultSize) { + if (resultNodes.isEmpty()) { + // if there's no result nodes, the actual totalResults if smaller or equals than the offset + totalResults = offsetNodes.size(); + numResults = 0; + } + else { + totalResults = resultSize + (int) offset; + numResults = resultSize; + } + } + else if (resultSize == limit) { + // if there's "limit" results, we can't know the total size (which may be greater), but the result size is the limit + numResults = (int) limit; + } + } + } catch (IOException e) { + throw new RepositoryException(e); + } finally { + if (result != null) { + try { + result.close(); + } catch (IOException e) { + log.warn("Unable to close query result: " + e); + } + } + } + } + + /** + * Collect score nodes from hits into the collector + * list until the size of collector reaches maxResults + * or there are not more results. + * + * @param hits the raw hits. + * @param collector where the access checked score nodes are collected. + * @param maxResults the maximum number of results in the collector. + * @throws IOException if an error occurs while reading from hits. + * @throws RepositoryException if an error occurs while checking access rights. + */ + private void collectScoreNodes(MultiColumnQueryHits hits, + List collector, + long maxResults) + throws IOException, RepositoryException { + while (collector.size() < maxResults) { + ScoreNode[] sn = hits.nextScoreNodes(); + if (sn == null) { + // no more results + break; + } + // check access + if (isAccessGranted(sn)) { + collector.add(sn); + } else { + invalid++; + } + } + } + + /** + * Checks if access is granted to all nodes. + * + * @param nodes the nodes to check. + * @return true if read access is granted to all + * nodes. + * @throws RepositoryException if an error occurs while checking access + * rights. + */ + protected boolean isAccessGranted(ScoreNode[] nodes) + throws RepositoryException { + for (ScoreNode node : nodes) { + try { + if (node != null && !sessionContext.getAccessManager().canRead( + null, node.getNodeId())) { + return false; + } + } catch (ItemNotFoundException e) { + // node deleted while query was executed + } + } + return true; + } + + /** + * Returns the total number of hits. This is the number of results you + * will get get if you don't set any limit or offset. This method may return + * -1 if the total size is unknown. + *

    + * If the "sizeEstimate" options is enabled: + * Keep in mind that this number may get smaller if nodes are found in + * the result set which the current session has no permission to access. + * This might be a security problem. + * + * @return the total number of hits. + */ + public int getTotalSize() { + if (sizeEstimate) { + if (numResults == -1) { + return -1; + } else { + return numResults - invalid; + } + } else { + return totalResults; + } + } + + private final class LazyScoreNodeIteratorImpl implements ScoreNodeIterator { + + private int position = -1; + + private boolean initialized = false; + + private ScoreNode[] next; + + public ScoreNode[] nextScoreNodes() { + initialize(); + if (next == null) { + throw new NoSuchElementException(); + } + ScoreNode[] sn = next; + fetchNext(); + return sn; + } + + /** + * {@inheritDoc} + */ + public void skip(long skipNum) { + initialize(); + if (skipNum < 0) { + throw new IllegalArgumentException("skipNum must not be negative"); + } + if (skipNum == 0) { + // do nothing + } else { + // attempt to get enough results + try { + getResults(position + invalid + (int) skipNum); + if (resultNodes.size() >= position + skipNum) { + // skip within already fetched results + position += skipNum - 1; + fetchNext(); + } else { + // not enough results after getResults() + throw new NoSuchElementException(); + } + } catch (RepositoryException e) { + throw new NoSuchElementException(e.getMessage()); + } + } + } + + /** + * {@inheritDoc} + *

    + * If the "sizeEstimate" options is enabled: + * This value may shrink when the query result encounters non-existing + * nodes or the session does not have access to a node. + */ + public long getSize() { + if (sizeEstimate) { + int total = getTotalSize(); + if (total == -1) { + return -1; + } + long size = offset > total ? 0 : total - offset; + if (limit >= 0 && size > limit) { + return limit; + } else { + return size; + } + } else { + return numResults; + } + } + + /** + * {@inheritDoc} + */ + public long getPosition() { + initialize(); + return position; + } + + /** + * @throws UnsupportedOperationException always. + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + initialize(); + return next != null; + } + + /** + * {@inheritDoc} + */ + public Object next() { + return nextScoreNodes(); + } + + /** + * Initializes this iterator but only if it is not yet initialized. + */ + private void initialize() { + if (!initialized) { + fetchNext(); + initialized = true; + } + } + + /** + * Fetches the next node to return by this iterator. If this method + * returns and {@link #next} is null then there is no next + * node. + */ + private void fetchNext() { + next = null; + int nextPos = position + 1; + while (next == null) { + if (nextPos >= resultNodes.size()) { + // quick check if there are more results at all + if (sizeEstimate) { + // this check is only possible if we have numResults + if (numResults != -1 && (nextPos + invalid) >= numResults) { + break; + } + } else { + // if numResults is set, all relevant results have been fetched + if (numResults != -1) { + break; + } + } + + // fetch more results + try { + int num; + if (resultNodes.size() == 0) { + num = index.getResultFetchSize(); + } else { + num = resultNodes.size() * 2; + } + getResults(num); + } catch (RepositoryException e) { + log.warn("Exception getting more results: " + e); + } + // check again + if (nextPos >= resultNodes.size()) { + // no more valid results + break; + } + } + next = resultNodes.get(nextPos); + } + position++; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeQuery.java new file mode 100644 index 00000000000..c227dc58fca --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeQuery.java @@ -0,0 +1,531 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.Weight; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Implements a variant of the lucene class {@code org.apache.lucene.search.RangeQuery}. + * This class does not rewrite to basic {@link org.apache.lucene.search.TermQuery} + * but will calculate the matching documents itself. That way a + * TooManyClauses can be avoided. + */ +@SuppressWarnings("serial") +public class RangeQuery extends Query implements Transformable { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(RangeQuery.class); + + /** + * The lower term. May be null if upperTerm is not + * null. + */ + private Term lowerTerm; + + /** + * The upper term. May be null if lowerTerm is not + * null. + */ + private Term upperTerm; + + /** + * If true the range interval is inclusive. + */ + private boolean inclusive; + + /** + * How the term enum is transformed before it is compared to lower and upper + * term. + */ + private int transform = TRANSFORM_NONE; + + private final PerQueryCache cache; + + /** + * The rewritten range query or null if the range spans more + * than {@link org.apache.lucene.search.BooleanQuery#maxClauseCount} terms. + */ + private Query stdRangeQuery; + + /** + * Creates a new RangeQuery. The lower or the upper term may be + * null, but not both! + * + * @param lowerTerm the lower term of the interval, or null + * @param upperTerm the upper term of the interval, or null. + * @param inclusive if true the interval is inclusive. + */ + public RangeQuery( + Term lowerTerm, Term upperTerm, boolean inclusive, + PerQueryCache cache) { + this(lowerTerm, upperTerm, inclusive, TRANSFORM_NONE, cache); + } + + /** + * Creates a new RangeQuery. The lower or the upper term may be + * null, but not both! + * + * @param lowerTerm the lower term of the interval, or null + * @param upperTerm the upper term of the interval, or null. + * @param inclusive if true the interval is inclusive. + * @param transform how term enums are transformed when read from the index. + */ + public RangeQuery( + Term lowerTerm, Term upperTerm, boolean inclusive, int transform, + PerQueryCache cache) { + if (lowerTerm == null && upperTerm == null) { + throw new IllegalArgumentException("At least one term must be non-null"); + } + if (lowerTerm != null && upperTerm != null && lowerTerm.field() != upperTerm.field()) { + throw new IllegalArgumentException("Both terms must be for the same field"); + } + + // if we have a lowerTerm, start there. otherwise, start at beginning + if (lowerTerm != null) { + this.lowerTerm = lowerTerm; + } else { + this.lowerTerm = new Term(upperTerm.field(), ""); + } + + this.upperTerm = upperTerm; + this.inclusive = inclusive; + this.transform = transform; + this.cache = cache; + } + + /** + * {@inheritDoc} + */ + public void setTransformation(int transformation) { + this.transform = transformation; + } + + /** + * Tries to rewrite this query into a standard lucene RangeQuery. + * This rewrite might fail with a TooManyClauses exception. If that + * happens, we use our own implementation. + * + * @param reader the index reader. + * @return the rewritten query or this query if rewriting is not possible. + * @throws IOException if an error occurs. + */ + public Query rewrite(IndexReader reader) throws IOException { + if (transform == TRANSFORM_NONE) { + TermRangeQuery stdRangeQueryImpl = new TermRangeQuery( + lowerTerm.field(), lowerTerm.text(), upperTerm.text(), + inclusive, inclusive); + stdRangeQueryImpl + .setRewriteMethod(MultiTermQuery.CONSTANT_SCORE_BOOLEAN_QUERY_REWRITE); + try { + stdRangeQuery = stdRangeQueryImpl.rewrite(reader); + return stdRangeQuery; + } catch (BooleanQuery.TooManyClauses e) { + log.debug("Too many terms to enumerate, using custom RangeQuery"); + // failed, use own implementation + return this; + } + } else { + // always use our implementation when we need to transform the + // term enum + return this; + } + } + + /** + * Creates the Weight for this query. + * + * @param searcher the searcher to use for the Weight. + * @return the Weigth for this query. + */ + public Weight createWeight(Searcher searcher) { + return new RangeQueryWeight(searcher, cache); + } + + /** + * Returns a string representation of this query. + * @param field the field name for which to create a string representation. + * @return a string representation of this query. + */ + public String toString(String field) { + StringBuffer buffer = new StringBuffer(); + if (!getField().equals(field)) { + buffer.append(getField()); + buffer.append(":"); + } + buffer.append(inclusive ? "[" : "{"); + buffer.append(lowerTerm != null ? lowerTerm.text() : "null"); + buffer.append(" TO "); + buffer.append(upperTerm != null ? upperTerm.text() : "null"); + buffer.append(inclusive ? "]" : "}"); + if (getBoost() != 1.0f) { + buffer.append("^"); + buffer.append(Float.toString(getBoost())); + } + return buffer.toString(); + } + + /** + * {@inheritDoc} + */ + public void extractTerms(Set terms) { + if (stdRangeQuery != null) { + stdRangeQuery.extractTerms(terms); + } + } + + /** + * Returns the field name for this query. + */ + private String getField() { + return (lowerTerm != null ? lowerTerm.field() : upperTerm.field()); + } + + //--------------------------< RangeQueryWeight >---------------------------- + + /** + * The Weight implementation for this RangeQuery. + */ + private class RangeQueryWeight extends AbstractWeight { + + private final PerQueryCache cache; + + /** + * Creates a new RangeQueryWeight instance using + * searcher. + * + * @param searcher a Searcher instance. + */ + RangeQueryWeight(Searcher searcher, PerQueryCache cache) { + super(searcher); + this.cache = cache; + } + + /** + * Creates a {@link RangeQueryScorer} instance. + * + * @param reader index reader + * @return a {@link RangeQueryScorer} instance + */ + protected Scorer createScorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) { + return new RangeQueryScorer(searcher.getSimilarity(), reader, cache); + } + + /** + * Returns this RangeQuery. + * + * @return this RangeQuery. + */ + public Query getQuery() { + return RangeQuery.this; + } + + /** + * {@inheritDoc} + */ + public float getValue() { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + /** + * {@inheritDoc} + */ + public void normalize(float norm) { + } + + /** + * {@inheritDoc} + */ + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(); + } + } + + //------------------------< RangeQueryScorer >------------------------------ + + /** + * Implements a Scorer for this RangeQuery. + */ + private final class RangeQueryScorer extends Scorer { + + /** + * The index reader to use for calculating the matching documents. + */ + private final IndexReader reader; + + /** + * The documents ids that match this range query. + */ + private final BitSet hits; + + /** + * Set to true when the hits have been calculated. + */ + private boolean hitsCalculated = false; + + /** + * The next document id to return + */ + private int nextDoc = -1; + + /** + * The cache key to use to store the results. + */ + private final String cacheKey; + + /** + * The map to store the results. + */ + private final Map resultMap; + + /** + * Creates a new RangeQueryScorer. + * @param similarity the similarity implementation. + * @param reader the index reader to use. + */ + @SuppressWarnings({"unchecked"}) + RangeQueryScorer( + Similarity similarity, IndexReader reader, + PerQueryCache cache) { + super(similarity); + this.reader = reader; + StringBuffer key = new StringBuffer(); + key.append(lowerTerm != null ? lowerTerm.field() : upperTerm.field()); + key.append('\uFFFF'); + key.append(lowerTerm != null ? lowerTerm.text() : ""); + key.append('\uFFFF'); + key.append(upperTerm != null ? upperTerm.text() : ""); + key.append('\uFFFF'); + key.append(inclusive); + key.append('\uFFFF'); + key.append(transform); + this.cacheKey = key.toString(); + // check cache + Map m = (Map) cache.get(RangeQueryScorer.class, reader); + if (m == null) { + m = new HashMap(); + cache.put(RangeQueryScorer.class, reader, m); + } + resultMap = m; + + BitSet result = resultMap.get(cacheKey); + if (result == null) { + result = new BitSet(reader.maxDoc()); + } else { + hitsCalculated = true; + } + hits = result; + } + + @Override + public int nextDoc() throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + calculateHits(); + nextDoc = hits.nextSetBit(nextDoc + 1); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + @Override + public int docID() { + return nextDoc; + } + + @Override + public float score() { + return 1.0f; + } + + @Override + public int advance(int target) throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + nextDoc = NO_MORE_DOCS; + return nextDoc; + } + + calculateHits(); + nextDoc = hits.nextSetBit(target); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + /** + * Calculates the ids of the documents matching this range query. + * @throws IOException if an error occurs while reading from the index. + */ + private void calculateHits() throws IOException { + if (hitsCalculated) { + return; + } + + String testField = getField(); + + boolean checkLower = false; + if (!inclusive || transform != TRANSFORM_NONE) { + // make adjustments to set to exclusive + checkLower = true; + } + + int propNameLength = FieldNames.getNameLength(lowerTerm.text()); + String namePrefix = ""; + if (propNameLength > 0) { + namePrefix = lowerTerm.text().substring(0, propNameLength); + } + List startTerms = new ArrayList(2); + + if (transform == TRANSFORM_NONE || lowerTerm.text().length() <= propNameLength) { + // use lowerTerm as is + startTerms.add(lowerTerm); + } else { + // first enumerate terms using lower case start character + StringBuffer termText = new StringBuffer(propNameLength + 1); + termText.append(lowerTerm.text().subSequence(0, propNameLength)); + char startCharacter = lowerTerm.text().charAt(propNameLength); + termText.append(Character.toLowerCase(startCharacter)); + startTerms.add(new Term(lowerTerm.field(), termText.toString())); + // second enumerate terms using upper case start character + termText.setCharAt(termText.length() - 1, Character.toUpperCase(startCharacter)); + startTerms.add(new Term(lowerTerm.field(), termText.toString())); + } + + for (Term startTerm : startTerms) { + TermEnum terms = reader.terms(startTerm); + try { + TermDocs docs = reader.termDocs(); + try { + do { + Term term = terms.term(); + if (term != null && term.field() == testField && term.text().startsWith(namePrefix)) { + if (checkLower) { + int compare = termCompare(term.text(), lowerTerm.text(), propNameLength); + if (compare > 0 || compare == 0 && inclusive) { + // do not check lower term anymore if no + // transformation is done on the term enum + checkLower = transform != TRANSFORM_NONE; + } else { + // continue with next term + continue; + } + } + if (upperTerm != null) { + int compare = termCompare(term.text(), upperTerm.text(), propNameLength); + // if beyond the upper term, or is exclusive and + // this is equal to the upper term + if ((compare > 0) || (!inclusive && compare == 0)) { + // only break out if no transformation + // was done on the term from the enum + if (transform == TRANSFORM_NONE) { + break; + } else { + // because of the transformation + // it is possible that the next + // term will be included again if + // we still enumerate on the same + // property name + if (term.text().startsWith(namePrefix)) { + continue; + } else { + break; + } + } + } + } + + docs.seek(terms); + while (docs.next()) { + hits.set(docs.doc()); + } + } else { + break; + } + } while (terms.next()); + } finally { + docs.close(); + } + } finally { + terms.close(); + } + } + + hitsCalculated = true; + // put to cache + resultMap.put(cacheKey, hits); + } + + /** + * Compares the text with the other String. This + * implementation behaves like {@link String#compareTo(Object)} but also + * respects the {@link RangeQuery#transform} property. + * + * @param text the text to compare to other. The + * transformation function is applied to this parameter before + * it is compared to other. + * @param other the other String. + * @param offset start comparing the two strings at offset. + * @return see {@link String#compareTo(Object)}. But also respects {@link + * RangeQuery#transform}. + */ + private int termCompare(String text, String other, int offset) { + OffsetCharSequence seq1 = new OffsetCharSequence(offset, text, transform); + OffsetCharSequence seq2 = new OffsetCharSequence(offset, other); + return seq1.compareTo(seq2); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.java new file mode 100644 index 00000000000..f7728ca8b8f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RangeScan.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.core.query.lucene; + +import org.apache.lucene.index.Term; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.FilteredTermEnum; + +import java.io.IOException; + +/** + * RangeScan implements a range scan on terms. + */ +class RangeScan extends FilteredTermEnum { + + private final Term upper; + + private boolean endEnum = false; + + /** + * Scans the index beginning at lower Term to upper. + * @param reader the index reader; + * @param lower the lower limit. + * @param upper the upper limit. + */ + RangeScan(IndexReader reader, Term lower, Term upper) throws IOException { + this.upper = upper; + setEnum(reader.terms(lower)); + } + + protected boolean termCompare(Term term) { + int compare = term.compareTo(upper); + if (compare > 0) { + endEnum = true; + } + return compare <= 0; + } + + public float difference() { + return 1.0f; + } + + protected boolean endEnum() { + return endEnum; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ReadOnlyIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ReadOnlyIndexReader.java new file mode 100644 index 00000000000..116ca896904 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ReadOnlyIndexReader.java @@ -0,0 +1,357 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermPositions; +import org.apache.lucene.util.ReaderUtil; + +import java.io.IOException; +import java.util.BitSet; +import java.util.Collection; +import java.util.Map; + +/** + * Overwrites the methods that would modify the index and throws an + * {@link UnsupportedOperationException} in each of those methods. A + * ReadOnlyIndexReader will always show all documents that have + * not been deleted at the time when the index reader is created. + */ +class ReadOnlyIndexReader extends RefCountingIndexReader { + + /** + * The deleted documents as initially read from the IndexReader passed + * in the constructor of this class. + */ + private BitSet deleted; + + /** + * The version of the index reader from where the deleted BitSet was + * obtained from. + */ + private long deletedDocsVersion; + + /** + * Creates a new index reader based on reader at + * modificationTick. + * + * @param reader the underlying IndexReader. + * @param deleted the documents that are deleted in + * reader. + * @param deletedDocsVersion the version of the index reader from where the + * deleted BitSet was obtained from. + */ + public ReadOnlyIndexReader(SharedIndexReader reader, + BitSet deleted, + long deletedDocsVersion) { + super(reader); + this.deleted = deleted; + this.deletedDocsVersion = deletedDocsVersion; + // acquire underlying reader + reader.acquire(); + } + + /** + * @return version of the deleted docs. + */ + long getDeletedDocsVersion() { + return deletedDocsVersion; + } + + /** + * Returns the tick value when the underlying {@link CachingIndexReader} was + * created. + * + * @return the creation tick for the underlying reader. + */ + long getCreationTick() { + return getBase().getCreationTick(); + } + + /** + * Updates the deleted documents in this index reader. When this method + * returns this index reader will have the same documents marked as deleted + * as the passed reader. + *

    + * This method is not thread-safe! Make sure no other thread is concurrently + * using this reader at the same time. + * + * @param reader the reader from where to obtain the deleted documents + * info. + */ + void updateDeletedDocs(CommittableIndexReader reader) { + Collection deletes = reader.getDeletedSince(deletedDocsVersion); + if (deletes == null) { + // full update needed + this.deleted = reader.getDeletedDocs(); + } else { + // incremental update + for (Integer d : deletes) { + deleted.set(d); + } + } + deletedDocsVersion = reader.getModificationCount(); + } + + /** + * Returns the DocId of the parent of n or + * {@link DocId#NULL} if n does not have a parent + * (n is the root node). + * + * @param n the document number. + * @return the DocId of n's parent. + * @throws IOException if an error occurs while reading from the index. + */ + public DocId getParent(int n) throws IOException { + return getBase().getParent(n, deleted); + } + + /** + * Returns the {@link SharedIndexReader} this reader is based on. + * + * @return the {@link SharedIndexReader} this reader is based on. + */ + public SharedIndexReader getBase() { + return (SharedIndexReader) in; + } + + //---------------------< IndexReader overwrites >--------------------------- + + /** + * Returns true if document n has been deleted + * @param n the document number + * @return true if document n has been deleted + */ + public boolean isDeleted(int n) { + return deleted.get(n); + } + + /** + * Returns true if any documents have been deleted. + * + * @return true if any documents have been deleted. + */ + public boolean hasDeletions() { + return !deleted.isEmpty(); + } + + /** + * Returns the number of documents in this index reader. + * + * @return the number of documents in this index reader. + */ + public int numDocs() { + return maxDoc() - deleted.cardinality(); + } + + /** + * @exception UnsupportedOperationException always + */ + protected final void doDelete(int docNum) { + throw new UnsupportedOperationException("IndexReader is read-only"); + } + + /** + * @exception UnsupportedOperationException always + */ + protected final void doUndeleteAll() { + throw new UnsupportedOperationException("IndexReader is read-only"); + } + + /** + * @exception UnsupportedOperationException always + */ + @Override + protected void doCommit(Map commitUserData) throws IOException { + if (!hasChanges) { + // change in behavior: IndexReader does not check for hasChanges + // before calling doCommit(); + return; + } + throw new UnsupportedOperationException("IndexReader is read-only"); + } + + /** + * Wraps the underlying TermDocs and filters out documents + * marked as deleted.
    + * If term is for a {@link FieldNames#UUID} field and this + * ReadOnlyIndexReader does not have such a document, + * {@link EmptyTermDocs#INSTANCE} is returned. + * + * @param term the term to enumerate the docs for. + * @return TermDocs for term. + * @throws IOException if an error occurs while reading from the index. + */ + public TermDocs termDocs(Term term) throws IOException { + // do not wrap for empty TermDocs + TermDocs td = in.termDocs(term); + if (td != EmptyTermDocs.INSTANCE) { + td = new FilteredTermDocs(td); + } + return td; + } + + /** + * Wraps the underlying TermDocs and filters out documents + * marked as deleted. + * + * @return TermDocs over the whole index. + * @throws IOException if an error occurs while reading from the index. + */ + public TermDocs termDocs() throws IOException { + return new FilteredTermDocs(super.termDocs()); + } + + /** + * Wraps the underlying TermPositions and filters out documents + * marked as deleted. + * + * @return TermPositions over the whole index. + * @throws IOException if an error occurs while reading from the index. + */ + public TermPositions termPositions() throws IOException { + return new FilteredTermPositions(super.termPositions()); + } + + @Override + public String toString() { + final StringBuilder buffer = new StringBuilder("ReadOnlyIndexReader("); + buffer.append(in); + buffer.append(','); + buffer.append(deletedDocsVersion); + buffer.append(')'); + return buffer.toString(); + } + + //----------------------< FilteredTermDocs >-------------------------------- + + /** + * Filters a wrapped TermDocs by omitting documents marked as deleted. + */ + private class FilteredTermDocs extends FilterTermDocs { + + /** + * Creates a new filtered TermDocs based on in. + * + * @param in the TermDocs to filter. + */ + public FilteredTermDocs(TermDocs in) { + super(in); + } + + /** + * @inheritDoc + */ + public final boolean next() throws IOException { + boolean hasNext = in.next(); + while (hasNext && deleted.get(in.doc())) { + hasNext = in.next(); + } + return hasNext; + } + + /** + * @inheritDoc + */ + public final int read(int[] docs, int[] freqs) throws IOException { + for (;;) { + int num = in.read(docs, freqs); + if (num == 0) { + // no more docs + return 0; + } + // need to check for deleted docs + int numDeleted = 0; + for (int i = 0; i < num; i++) { + if (deleted.get(docs[i])) { + numDeleted++; + continue; + } + // check if we need to shift + if (numDeleted > 0) { + docs[i - numDeleted] = docs[i]; + freqs[i - numDeleted] = freqs[i]; + } + } + if (num != numDeleted) { + return num - numDeleted; + } + } + } + + /** + * @inheritDoc + */ + public final boolean skipTo(int i) throws IOException { + boolean exists = in.skipTo(i); + while (exists && deleted.get(doc())) { + exists = next(); + } + return exists; + } + } + + //---------------------< FilteredTermPositions >---------------------------- + + /** + * Filters a wrapped TermPositions by omitting documents marked as deleted. + */ + private final class FilteredTermPositions extends FilteredTermDocs + implements TermPositions { + + /** + * Creates a new filtered TermPositions based on in. + * + * @param in the TermPositions to filter. + */ + public FilteredTermPositions(TermPositions in) { + super(in); + } + + /** + * @inheritDoc + */ + public int nextPosition() throws IOException { + return ((TermPositions) this.in).nextPosition(); + } + + /** + * @inheritDoc + */ + public int getPayloadLength() { + return ((TermPositions) in).getPayloadLength(); + } + + /** + * @inheritDoc + */ + public byte[] getPayload(byte[] data, int offset) throws IOException { + return ((TermPositions) in).getPayload(data, offset); + } + + /** + * @inheritDoc + */ + public boolean isPayloadAvailable() { + return ((TermPositions) in).isPayloadAvailable(); + } + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Recovery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Recovery.java new file mode 100644 index 00000000000..804729813bd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Recovery.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.core.query.lucene; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.io.IOException; + +/** + * Implements the recovery process. + */ +class Recovery { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(Recovery.class); + + /** + * The MultiIndex where to run the recovery on. + */ + private final MultiIndex index; + + /** + * The redo redoLog. + */ + private final RedoLog redoLog; + + /** + * The ids of the uncommitted transactions. Set of Long objects. + */ + private final Set losers = new HashSet(); + + /** + * Creates a new Recovery instance. + * + * @param index the MultiIndex to recover. + * @param redoLog the redo redoLog. + */ + private Recovery(MultiIndex index, RedoLog redoLog) { + this.index = index; + this.redoLog = redoLog; + } + + /** + * Runs a recovery on index if redoLog contains + * log entries. + *

    + * If recovery succeeds the index is flushed and the redo log + * is cleared. That is, the index is stable.
    + * If recovery fails an IOException is thrown, and the redo log will not + * be modified. The recovery process can then be executed again, after + * fixing the cause of the IOException (e.g. disk full). + * + * @param index the index to recover. + * @param redoLog the redo log. + * @throws IOException if the recovery fails. + */ + static void run(MultiIndex index, RedoLog redoLog) throws IOException { + if (!redoLog.hasEntries()) { + log.debug("RedoLog is empty, no recovery needed."); + return; + } + log.info("Found uncommitted redo log. Applying changes now..."); + Recovery r = new Recovery(index, redoLog); + r.run(); + log.info("Redo changes applied."); + } + + /** + * Runs the recovery process. + * + * @throws IOException if the recovery fails. + */ + private void run() throws IOException { + List actions = redoLog.getActions(); + + // find loser transactions + for (MultiIndex.Action a : actions) { + if (a.getType() == MultiIndex.Action.TYPE_START) { + losers.add(a.getTransactionId()); + } else if (a.getType() == MultiIndex.Action.TYPE_COMMIT) { + losers.remove(a.getTransactionId()); + } + } + + // find last volatile commit without changes from a loser + int lastSafeVolatileCommit = -1; + Set transactionIds = new HashSet(); + for (int i = 0; i < actions.size(); i++) { + MultiIndex.Action a = actions.get(i); + if (a.getType() == MultiIndex.Action.TYPE_COMMIT) { + transactionIds.clear(); + } else if (a.getType() == MultiIndex.Action.TYPE_VOLATILE_COMMIT) { + transactionIds.retainAll(losers); + // check if transactionIds contains losers + if (transactionIds.size() > 0) { + // found dirty volatile commit + break; + } else { + lastSafeVolatileCommit = i; + } + } else { + transactionIds.add(a.getTransactionId()); + } + } + + // delete dirty indexes + for (int i = lastSafeVolatileCommit + 1; i < actions.size(); i++) { + MultiIndex.Action a = actions.get(i); + if (a.getType() == MultiIndex.Action.TYPE_CREATE_INDEX) { + a.undo(index); + } + } + + // replay actions up to last safe volatile commit + // ignore add node actions, they are included in volatile commits + for (int i = 0; i < actions.size() && i <= lastSafeVolatileCommit; i++) { + MultiIndex.Action a = actions.get(i); + switch (a.getType()) { + case MultiIndex.Action.TYPE_ADD_INDEX: + case MultiIndex.Action.TYPE_CREATE_INDEX: + case MultiIndex.Action.TYPE_DELETE_INDEX: + case MultiIndex.Action.TYPE_DELETE_NODE: + // ignore actions by the index merger. + // the previously created index of a merge has been + // deleted because it was considered dirty. + // we are conservative here and let the index merger do + // its work again. + if (a.getTransactionId() == MultiIndex.Action.INTERNAL_TRANS_REPL_INDEXES) { + continue; + } + a.execute(index); + } + } + + // now replay the rest until we encounter a loser transaction + for (int i = lastSafeVolatileCommit + 1; i < actions.size(); i++) { + MultiIndex.Action a = actions.get(i); + if (losers.contains(new Long(a.getTransactionId()))) { + break; + } else { + // ignore actions by the index merger. + if (a.getTransactionId() == MultiIndex.Action.INTERNAL_TRANS_REPL_INDEXES) { + continue; + } + a.execute(index); + } + } + + // now we are consistent again -> flush + index.safeFlush(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RedoLog.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RedoLog.java new file mode 100644 index 00000000000..aa0050e029c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RedoLog.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.core.query.lucene; + +import java.io.IOException; +import java.util.List; + +/** + * Defines a redo log for changes that have not been committed to disk. While + * nodes are added to and removed from the volatile index (held in memory) a + * redo log is maintained to keep track of the changes. In case the Jackrabbit + * process terminates unexpected the redo log is applied when Jackrabbit is + * restarted the next time. + */ +public interface RedoLog { + + /** + * Returns true if this redo log contains any entries, + * false otherwise. + * @return true if this redo log contains any entries, + * false otherwise. + */ + boolean hasEntries(); + + /** + * Returns the number of entries in this redo log. + * @return the number of entries in this redo log. + */ + int getSize(); + + /** + * Returns a List with all {@link MultiIndex.Action} instances in the + * redo log. + * + * @return an List with all {@link MultiIndex.Action} instances in the + * redo log. + * @throws IOException if an error occurs while reading from the redo log. + */ + List getActions() throws IOException; + + /** + * Appends an action to the log. + * + * @param action the action to append. + * @throws IOException if the node cannot be written to the redo + * log. + */ + void append(MultiIndex.Action action) throws IOException; + + /** + * Flushes all pending writes to the redo log. + * + * @throws IOException if an error occurs while writing. + */ + void flush() throws IOException; + + /** + * Flushes all pending writes to the redo log and closes it. + * + * @throws IOException if an error occurs while writing. + */ + void close() throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RedoLogFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RedoLogFactory.java new file mode 100644 index 00000000000..87cbf13df11 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RedoLogFactory.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.core.query.lucene; + +import java.io.IOException; + +/** + * RedoLogFactory a factory for {@link RedoLog}s. + */ +public interface RedoLogFactory { + + /** + * Creates a redo log starting at the current state of the + * index. + * + * @param index the index. + * @return the redo log. + * @throws IOException if an error occurs while reading from the index or + * the log cannot be created for some other reason. + */ + public RedoLog createRedoLog(MultiIndex index) throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RefCountingIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RefCountingIndexReader.java new file mode 100644 index 00000000000..91614f2d517 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RefCountingIndexReader.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.core.query.lucene; + +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.FilterIndexReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.util.ReaderUtil; + +import java.io.IOException; + +/** + * RefCountingIndexReader... + */ +public class RefCountingIndexReader + extends FilterIndexReader + implements ReleaseableIndexReader { + + /** + * A reference counter. When constructed the refCount is one. + */ + private int refCount = 1; + + public RefCountingIndexReader(IndexReader in) { + super(in); + } + + /** + * Increments the reference count on this index reader. The reference count + * is decremented on {@link #release()}. + */ + synchronized final void acquire() { + refCount++; + } + + /** + * @return the current reference count value. + */ + public synchronized int getRefCountJr() { + return refCount; + } + + //-----------------------< ReleaseableIndexReader >-------------------------- + + /** + * {@inheritDoc} + */ + public synchronized final void release() throws IOException { + if (--refCount == 0) { + close(); + } + } + + //-----------------------< FilterIndexReader >-------------------------- + + @Override + public IndexReader[] getSequentialSubReaders() { + return null; + } + + @Override + public FieldInfos getFieldInfos() { + return ReaderUtil.getMergedFieldInfos(in); + } + + protected void doClose() throws IOException { + Util.closeOrRelease(in); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ReleaseableIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ReleaseableIndexReader.java new file mode 100644 index 00000000000..6fd904e8f54 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ReleaseableIndexReader.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.core.query.lucene; + +import java.io.IOException; + +/** + * ReleaseableIndexReader... + */ +public interface ReleaseableIndexReader { + + /** + * Releases this index reader and potentially frees resources. In contrast + * to {@link org.apache.lucene.index.IndexReader#close()} this method + * does not necessarily close the index reader, but gives the implementation + * the opportunity to do reference counting. + * + * @throws IOException if an error occurs while releasing the index reader. + */ + public void release() throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RowIteratorImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RowIteratorImpl.java new file mode 100644 index 00000000000..79066489497 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/RowIteratorImpl.java @@ -0,0 +1,703 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.spi.commons.query.qom.ColumnImpl; +import org.apache.jackrabbit.util.ISO9075; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.PathNotFoundException; +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.ValueFactory; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.io.IOException; + +/** + * Implements the {@link javax.jcr.query.RowIterator} interface returned by + * a {@link javax.jcr.query.QueryResult}. + */ +class RowIteratorImpl implements RowIterator { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(RowIteratorImpl.class); + + /** + * The name of the excerpt function without prefix but with left parenthesis. + */ + private static final String EXCERPT_FUNC_LPAR = "excerpt("; + + /** + * The name of the spell check function without prefix but with left + * parenthesis. + */ + private static final String SPELLCHECK_FUNC_LPAR = "spellcheck("; + + /** + * The start Name for the rep:excerpt function: rep:excerpt( + */ + private static final Name REP_EXCERPT_LPAR = NameFactoryImpl.getInstance().create( + Name.NS_REP_URI, EXCERPT_FUNC_LPAR); + + /** + * Iterator over nodes, that constitute the result set. + */ + private final ScoreNodeIterator scoreNodes; + + /** + * Linked map of {@link ColumnImpl columns}, indexed by their column name + * (String). + */ + private final Map columns; + + /** + * List of valid selector {@link Name}s. + */ + private final List selectorNames = new ArrayList(); + + /** + * The item manager of the session that executes the query. + */ + private final ItemManager itemMgr; + + /** + * The hierarchy manager of the workspace. + */ + private final HierarchyManager hmgr; + + /** + * The NamePathResolver of the user Session. + */ + private final NamePathResolver resolver; + + /** + * The excerpt provider or null if none is available. + */ + private final ExcerptProvider excerptProvider; + + /** + * The spell suggestion or null if none is available. + */ + private final SpellSuggestion spellSuggestion; + + /** + * A value factory for the session that executes the query. + */ + private final ValueFactoryQImpl valueFactory; + + /** + * Creates a new RowIteratorImpl that iterates over the result + * nodes. + * + * @param scoreNodes a ScoreNodeIterator that contains the + * nodes of the query result. + * @param columns the columns to select. + * @param selectorNames the selector names. + * @param itemMgr the item manager of the session that executes the + * query. + * @param hmgr the hierarchy manager of the workspace. + * @param resolver NamespaceResolver of the user + * Session. + * @param valueFactory the value factory of the current session. + * @param exProvider the excerpt provider associated with the query + * result that created this row iterator. + * @param spellSuggestion the spell suggestion associated with the query + * result or null if none is available. + * @throws NamespaceException if an error occurs while translating a JCR + * name. + */ + RowIteratorImpl(ScoreNodeIterator scoreNodes, + Map columns, + Name[] selectorNames, + ItemManager itemMgr, + HierarchyManager hmgr, + NamePathResolver resolver, + ValueFactory valueFactory, + ExcerptProvider exProvider, + SpellSuggestion spellSuggestion) { + this.scoreNodes = scoreNodes; + this.columns = columns; + this.selectorNames.addAll(Arrays.asList(selectorNames)); + this.itemMgr = itemMgr; + this.hmgr = hmgr; + this.resolver = resolver; + this.excerptProvider = exProvider; + this.spellSuggestion = spellSuggestion; + if (valueFactory instanceof ValueFactoryQImpl) { + this.valueFactory = (ValueFactoryQImpl) valueFactory; + } else { + QValueFactory qvf = QValueFactoryImpl.getInstance(); + this.valueFactory = new ValueFactoryQImpl(qvf, resolver); + } + } + + /** + * Returns the next Row in the iteration. + * + * @return the next Row in the iteration. + * @throws NoSuchElementException if iteration has no more + * Rows. + */ + public Row nextRow() throws NoSuchElementException { + return new RowImpl(scoreNodes.nextScoreNodes()); + } + + /** + * Skip a number of Rows in this iterator. + * + * @param skipNum the non-negative number of Rows to skip + * @throws NoSuchElementException if skipped past the last + * Row in this iterator. + */ + public void skip(long skipNum) throws NoSuchElementException { + scoreNodes.skip(skipNum); + } + + /** + * Returns the number of Rows in this iterator. + * + * @return the number of Rows in this iterator. + */ + public long getSize() { + return scoreNodes.getSize(); + } + + /** + * Returns the current position within this iterator. The number + * returned is the 0-based index of the next Row in the iterator, + * i.e. the one that will be returned on the subsequent next call. + *

    + * Note that this method does not check if there is a next element, + * i.e. an empty iterator will always return 0. + * + * @return the current position withing this iterator. + */ + public long getPosition() { + return scoreNodes.getPosition(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + /** + * Returns true if the iteration has more Rows. + * (In other words, returns true if next would + * return an Row rather than throwing an exception.) + * + * @return true if the iterator has more elements. + */ + public boolean hasNext() { + return scoreNodes.hasNext(); + } + + /** + * Returns the next Row in the iteration. + * + * @return the next Row in the iteration. + * @throws NoSuchElementException if iteration has no more Rows. + */ + public Object next() throws NoSuchElementException { + return nextRow(); + } + + //---------------------< class RowImpl >------------------------------------ + + /** + * Implements the {@link javax.jcr.query.Row} interface, which represents + * a row in the query result. + */ + class RowImpl implements javax.jcr.query.Row { + + /** + * The score for this result row + */ + private final float score; + + /** + * The underlying Node of this result row. + */ + private NodeImpl node; + + /** + * The score nodes associated with this row. + */ + private final ScoreNode[] sn; + + /** + * Cached value array for returned by {@link #getValues()}. + */ + private Value[] values; + + /** + * Creates a new RowImpl instance based on node. + * + * @param sn the score nodes associated with this row. + */ + RowImpl(ScoreNode[] sn) { + this.sn = sn; + this.score = sn[0].getScore(); + } + + /** + * Returns an array of all the values in the same order as the property + * names (column names) returned by + * {@link javax.jcr.query.QueryResult#getColumnNames()}. + * + * @return a Value array. + * @throws RepositoryException if an error occurs while retrieving the + * values from the Node. + */ + public Value[] getValues() throws RepositoryException { + if (values == null) { + Value[] tmp = new Value[columns.size()]; + int i = 0; + for (String columnName : columns.keySet()) { + tmp[i++] = getValue(columnName); + } + values = tmp; + } + // return a copy of the array + Value[] ret = new Value[values.length]; + System.arraycopy(values, 0, ret, 0, values.length); + return ret; + } + + /** + * Returns the value of the indicated column in this Row. + *

    + * If columnbName is not among the column names of the + * query result table, an ItemNotFoundException is thrown. + * + * @return a Value + * @throws ItemNotFoundException if columnName is not + * among the column names of the query result table. + * @throws RepositoryException if another error occurs. + */ + public Value getValue(String columnName) throws ItemNotFoundException, RepositoryException { + try { + ColumnImpl col = columns.get(columnName); + if (col == null) { + if (isExcerptFunction(columnName)) { + // excerpt function with parameter + return getExcerpt(columnName); + } else { + throw new ItemNotFoundException(columnName); + } + } + Node n = getNode(col.getSelectorName()); + if (n == null) { + return null; + } + + if (NameConstants.JCR_PATH.equals(col.getPropertyQName())) { + int idx = getSelectorIndex(col.getSelectorName()); + QValue p = valueFactory.getQValueFactory().create(hmgr.getPath(sn[idx].getNodeId())); + return valueFactory.createValue(p); + } else if (n.hasProperty(col.getPropertyName())) { + Property p = n.getProperty(col.getPropertyName()); + if (p.isMultiple()) { + // mvp values cannot be returned + return null; + } else { + if (p.getDefinition().getRequiredType() == PropertyType.UNDEFINED) { + return valueFactory.createValue(p.getString()); + } else { + return p.getValue(); + } + } + } else { + Name prop = resolver.getQName(columnName); + // either jcr:score, rep:excerpt, + // rep:spellcheck or not set + if (NameConstants.JCR_SCORE.equals(prop)) { + return valueFactory.createValue(Math.round(score * 1000f)); + } else if (isExcerptFunction(prop)) { + return getExcerpt(); + } else if (isSpellCheckFunction(prop)) { + return getSpellCheckedStatement(); + } else { + return null; + } + } + } catch (NameException e) { + if (isExcerptFunction(columnName)) { + // excerpt function with parameter + return getExcerpt(columnName); + } else { + throw new RepositoryException(e.getMessage(), e); + } + } + } + + /** + * Returns the Node corresponding to this Row. + *

    + * A RepositoryException is thrown if this Row + * contains values from more than one node. This will be the case when more + * than one selector is included among the columns specified for the query. + * + * @return a Node + * @throws RepositoryException if this query has more than one selector + * (and therefore, this Row corresponds to more than one + * Node) or if another error occurs. + * @since JCR 2.0 + */ + public Node getNode() throws RepositoryException { + checkSingleSelector("Use getNode(String) instead."); + return getNodeImpl(); + } + + /** + * Returns the Node corresponding to this Row and + * the specified selector. + * + * @param selectorName a String + * @return a Node + * @throws RepositoryException if selectorName is not the alias + * of a selector in this query or if another error occurs. + * @since JCR 2.0 + */ + public Node getNode(String selectorName) throws RepositoryException { + ScoreNode s = sn[getSelectorIndex(selectorName)]; + if (s == null) { + return null; + } + return (Node) itemMgr.getItem(s.getNodeId()); + } + + /** + * Equivalent to Row.getNode().getPath(). However, some + * implementations may be able gain efficiency by not resolving the actual + * Node. + * + * @return a String + * @throws RepositoryException if this query has more than one selector + * (and therefore, this Row corresponds to more than one + * Node) or if another error occurs. + * @since JCR 2.0 + */ + public String getPath() throws RepositoryException { + checkSingleSelector("Use getPath(String) instead."); + return resolver.getJCRPath(hmgr.getPath(sn[0].getNodeId())); + } + + /** + * Equivalent to Row.getNode(selectorName).getPath(). However, some + * implementations may be able gain efficiency by not resolving the actual + * Node. + * + * @param selectorName a String + * @return a String + * @throws RepositoryException if selectorName is not the alias + * of a selector in this query or if another error occurs. + * @since JCR 2.0 + */ + public String getPath(String selectorName) throws RepositoryException { + Node n = getNode(selectorName); + if (n != null) { + return n.getPath(); + } else { + return null; + } + } + + /** + * Returns the full text search score for this row associated with the + * default selector. This corresponds to the score of a particular node. + *

    + * If no FullTextSearchScore AQM object is associated with the + * default selector this method will still return a value. However, in that + * case the returned value may not be meaningful or may simply reflect the + * minimum possible relevance level (for example, in some systems this might + * be a score of 0). + *

    + * Note, in JCR-SQL2 a FullTextSearchScore AQM object is represented + * by a SCORE() function. In JCR-JQOM it is represented by a + * Java object of type javax.jcr.query.qom.FullTextSearchScore. + * + * @return a double + * @throws RepositoryException if this query has more than one selector + * (and therefore, this Row corresponds to more than one + * Node) or if another error occurs. + * @since JCR 2.0 + */ + public double getScore() throws RepositoryException { + checkSingleSelector("Use getScore(String) instead."); + return score; + } + + /** + * Returns the full text search score for this row associated with the + * specified selector. This corresponds to the score of a particular node. + *

    + * If no FullTextSearchScore AQM object is associated with the + * selector selectorName this method will still return a value. + * However, in that case the returned value may not be meaningful or may + * simply reflect the minimum possible relevance level (for example, in some + * systems this might be a score of 0). + *

    + * Note, in JCR-SQL2 a FullTextSearchScore AQM object is represented + * by a SCORE() function. In JCR-JQOM it is represented by a + * Java object of type javax.jcr.query.qom.FullTextSearchScore. + * + * @param selectorName a String + * @return a String + * @throws RepositoryException if selectorName is not the alias + * of a selector in this query or if another error occurs. + * @since JCR 2.0 + */ + public double getScore(String selectorName) throws RepositoryException { + ScoreNode s = sn[getSelectorIndex(selectorName)]; + if (s == null) { + return 0; + } + return s.getScore(); + } + + //-----------------------------< internal >----------------------------- + + /** + * Returns the node corresponding to this row. + * + * @return the node. + * @throws RepositoryException if an error occurs while retrieving the + * node. e.g. node does not exist anymore. + */ + private NodeImpl getNodeImpl() throws RepositoryException { + if (node == null) { + node = (NodeImpl) itemMgr.getItem(sn[0].getNodeId()); + } + return node; + } + + /** + * Checks if there is a single selector and otherwise throws a + * RepositoryException. + * + * @param useInstead message telling, which method to use instead. + * @throws RepositoryException if there is more than one selector. + */ + private void checkSingleSelector(String useInstead) throws RepositoryException { + if (sn.length > 1) { + String msg = "More than one selector. " + useInstead; + throw new RepositoryException(msg); + } + } + + /** + * Gets the selector index for the given selectorName. + * + * @param selectorName the selector name. + * @return the selector index. + * @throws RepositoryException if the selector name is not a valid JCR + * name or the selector name is not the + * alias of a selector in this query. + */ + private int getSelectorIndex(String selectorName) + throws RepositoryException { + int idx = selectorNames.indexOf(resolver.getQName(selectorName)); + if (idx == -1) { + throw new RepositoryException("Unknown selector name: " + selectorName); + } + return idx; + } + + /** + * @param name a Name. + * @return true if name is the rep:excerpt + * function, false otherwise. + */ + private boolean isExcerptFunction(Name name) { + return name.getNamespaceURI().equals(Name.NS_REP_URI) + && name.getLocalName().startsWith(EXCERPT_FUNC_LPAR); + } + + /** + * @param name a String. + * @return true if name is the rep:excerpt + * function, false otherwise. + */ + private boolean isExcerptFunction(String name) { + try { + return name.startsWith( + resolver.getJCRName(REP_EXCERPT_LPAR)); + } catch (NamespaceException e) { + // will never happen + return false; + } + } + + /** + * Returns an excerpt for the node associated with this row. + * + * @return a StringValue or null if the excerpt cannot be + * created or an error occurs. + */ + private Value getExcerpt() { + return createExcerpt(sn[0].getNodeId()); + } + + /** + * Returns an excerpt for the node indicated by the relative path + * parameter of the rep:excerpt function. The relative path is resolved + * against the node associated with this row. + * + * @param excerptCall the rep:excerpt function with the parameter as + * string. + * @return a StringValue or null if the excerpt cannot be + * created or an error occurs. + * @throws RepositoryException if the function call is not well-formed. + */ + private Value getExcerpt(String excerptCall) throws RepositoryException { + int idx = excerptCall.indexOf(EXCERPT_FUNC_LPAR); + int end = excerptCall.lastIndexOf(')'); + if (end == -1) { + throw new RepositoryException("Missing right parenthesis"); + } + String pathStr = excerptCall.substring( + idx + EXCERPT_FUNC_LPAR.length(), end).trim(); + String decodedPath = ISO9075.decode(pathStr); + try { + NodeImpl n = (NodeImpl) getNodeImpl().getNode(decodedPath); + return createExcerpt(n.getNodeId()); + } catch (PathNotFoundException e) { + // does not exist or references a property + try { + Property p = getNode().getProperty(decodedPath); + return highlight(p.getValue().getString()); + } catch (PathNotFoundException e1) { + // does not exist + return null; + } + } + } + + /** + * Creates an excerpt for node with the given id. + * + * @param id a node id. + * @return a StringValue or null if the excerpt cannot be + * created or an error occurs. + */ + private Value createExcerpt(NodeId id) { + if (excerptProvider == null) { + return null; + } + try { + long time = System.currentTimeMillis(); + String excerpt = excerptProvider.getExcerpt(id, 3, 150); + time = System.currentTimeMillis() - time; + log.debug("Created excerpt in {} ms.", time); + if (excerpt != null) { + return valueFactory.createValue(excerpt); + } else { + return null; + } + } catch (IOException e) { + return null; + } + } + + /** + * Highlights the matching terms in the passed text. + * + * @param text the text where to apply highlighting. + * @return a StringValue or null if highlighting fails. + */ + private Value highlight(String text) { + if (!(excerptProvider instanceof HighlightingExcerptProvider)) { + return null; + } + HighlightingExcerptProvider hep = + (HighlightingExcerptProvider) excerptProvider; + try { + long time = System.currentTimeMillis(); + text = hep.highlight(text); + time = System.currentTimeMillis() - time; + log.debug("Highlighted text in {} ms.", time); + return valueFactory.createValue(text); + } catch (IOException e) { + return null; + } + } + + /** + * @param name a Name. + * @return true if name is the rep:spellcheck + * function, false otherwise. + */ + private boolean isSpellCheckFunction(Name name) { + return name.getNamespaceURI().equals(Name.NS_REP_URI) + && name.getLocalName().startsWith(SPELLCHECK_FUNC_LPAR); + } + + /** + * Returns the spell checked string of the first relation query node + * with a spellcheck operation. + * + * @return a StringValue or null if the spell checker + * thinks the words are spelled correctly. This method also + * returns null if no spell checker is configured. + */ + private Value getSpellCheckedStatement() { + String v = null; + if (spellSuggestion != null) { + try { + v = spellSuggestion.getSuggestion(); + } catch (IOException e) { + log.warn("Spell checking failed", e); + } + } + if (v != null) { + return valueFactory.createValue(v); + } else { + return null; + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNode.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNode.java new file mode 100644 index 00000000000..c0919b9f6fe --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNode.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.TermDocs; + +/** + * ScoreNode implements a simple container which holds a mapping + * of {@link NodeId} to a score value. + */ +public final class ScoreNode { + + /** + * The id of a node. + */ + private final NodeId id; + + /** + * The score of the node. + */ + private float score; + + /** + * The lucene document number for this score node. Set to -1 if + * unknown. + */ + private final int doc; + + /** + * Creates a new ScoreNode. + * + * @param id the node id. + * @param score the score value. + */ + public ScoreNode(NodeId id, float score) { + this(id, score, -1); + } + + /** + * Creates a new ScoreNode. + * + * @param id the node id. + * @param score the score value. + * @param doc the document number. + */ + public ScoreNode(NodeId id, float score, int doc) { + this.id = id; + this.score = score; + this.doc = doc; + } + + /** + * @return the node id for this ScoreNode. + */ + public NodeId getNodeId() { + return id; + } + + /** + * @return the score for this ScoreNode. + */ + public float getScore() { + return score; + } + + /** + * Sets a new score value. + * + * @param score the score value. + */ + public void setScore(float score) { + this.score = score; + } + + /** + * Returns the document number for this score node. + * + * @param reader the current index reader to look up the document if + * needed. + * @return the document number. + * @throws IOException if an error occurs while reading from the index or + * the node is not present in the index. + */ + public int getDoc(IndexReader reader) throws IOException { + if (doc == -1) { + TermDocs docs = reader.termDocs(TermFactory.createUUIDTerm(id.toString())); + try { + if (docs.next()) { + return docs.doc(); + } else { + throw new IOException("Node with id " + id + " not found in index"); + } + } finally { + docs.close(); + } + } else { + return doc; + } + } + + public String toString() { + StringBuffer sb = new StringBuffer(id.toString()); + sb.append("("); + if (doc != -1) { + sb.append(doc); + } else { + sb.append("?"); + } + sb.append(")"); + return sb.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNodeIterator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNodeIterator.java new file mode 100644 index 00000000000..63bbc50ee3f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNodeIterator.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.core.query.lucene; + +import javax.jcr.RangeIterator; + +/** + * A range iterator over {@link ScoreNode}[]. + */ +public interface ScoreNodeIterator extends RangeIterator { + + /** + * Returns the next score nodes. + * + * @return the next score nodes. + * @throws java.util.NoSuchElementException + * if there are no next score nodes. + */ + ScoreNode[] nextScoreNodes(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNodeIteratorImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNodeIteratorImpl.java new file mode 100644 index 00000000000..aa3a39a9f1a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/ScoreNodeIteratorImpl.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.core.query.lucene; + +import java.util.Arrays; + +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; + +/** + * ScoreNodeIteratorImpl implements a {@link ScoreNodeIterator} + * over an array of {@link ScoreNode ScoreNode[]}. + */ +public class ScoreNodeIteratorImpl + extends RangeIteratorAdapter + implements ScoreNodeIterator { + + public ScoreNodeIteratorImpl(ScoreNode[][] scoreNodes) { + super(Arrays.asList(scoreNodes)); + } + + /** + * {@inheritDoc} + */ + public ScoreNode[] nextScoreNodes() { + return (ScoreNode[]) next(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SearchIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SearchIndex.java new file mode 100644 index 00000000000..6baef1fdbe7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SearchIndex.java @@ -0,0 +1,2686 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +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.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.cluster.ChangeLogRecord; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.cluster.ClusterRecord; +import org.apache.jackrabbit.core.cluster.ClusterRecordDeserializer; +import org.apache.jackrabbit.core.cluster.ClusterRecordProcessor; +import org.apache.jackrabbit.core.cluster.LockRecord; +import org.apache.jackrabbit.core.cluster.NamespaceRecord; +import org.apache.jackrabbit.core.cluster.NodeTypeRecord; +import org.apache.jackrabbit.core.cluster.PrivilegeRecord; +import org.apache.jackrabbit.core.cluster.WorkspaceRecord; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.journal.Journal; +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.Record; +import org.apache.jackrabbit.core.journal.RecordIterator; +import org.apache.jackrabbit.core.query.AbstractQueryHandler; +import org.apache.jackrabbit.core.query.ExecutableQuery; +import org.apache.jackrabbit.core.query.QueryHandler; +import org.apache.jackrabbit.core.query.QueryHandlerContext; +import org.apache.jackrabbit.core.query.lucene.directory.DirectoryManager; +import org.apache.jackrabbit.core.query.lucene.directory.FSDirectoryManager; +import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.query.DefaultQueryNodeFactory; +import org.apache.jackrabbit.spi.commons.query.qom.OrderingImpl; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LimitTokenCountAnalyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.PayloadAttribute; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Fieldable; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.MultiReader; +import org.apache.lucene.index.Payload; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.util.Version; +import org.apache.tika.config.TikaConfig; +import org.apache.tika.fork.ForkParser; +import org.apache.tika.parser.AutoDetectParser; +import org.apache.tika.parser.Parser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * Implements a {@link org.apache.jackrabbit.core.query.QueryHandler} using + * Lucene. + */ +public class SearchIndex extends AbstractQueryHandler { + + /** + * Valid node type names under /jcr:system. Used to determine if a + * query needs to be executed also against the /jcr:system tree. + */ + public static final Collection VALID_SYSTEM_INDEX_NODE_TYPE_NAMES = + Collections.unmodifiableCollection(Arrays.asList( + NameConstants.NT_CHILDNODEDEFINITION, + NameConstants.NT_FROZENNODE, + NameConstants.NT_NODETYPE, + NameConstants.NT_PROPERTYDEFINITION, + NameConstants.NT_VERSION, + NameConstants.NT_VERSIONEDCHILD, + NameConstants.NT_VERSIONHISTORY, + NameConstants.NT_VERSIONLABELS, + NameConstants.REP_NODETYPES, + NameConstants.REP_SYSTEM, + NameConstants.REP_VERSIONSTORAGE, + // Supertypes + NameConstants.NT_BASE, + NameConstants.MIX_REFERENCEABLE)); + + /** + * Default query node factory. + */ + private static final DefaultQueryNodeFactory DEFAULT_QUERY_NODE_FACTORY = + new DefaultQueryNodeFactory(VALID_SYSTEM_INDEX_NODE_TYPE_NAMES); + + /** The logger instance for this class */ + private static final Logger log = LoggerFactory.getLogger(SearchIndex.class); + + /** + * Name of the file to persist search internal namespace mappings. + */ + private static final String NS_MAPPING_FILE = "ns_mappings.properties"; + + /** + * The default value for property {@link #minMergeDocs}. + */ + public static final int DEFAULT_MIN_MERGE_DOCS = 100; + + /** + * The default value for property {@link #maxMergeDocs}. + */ + public static final int DEFAULT_MAX_MERGE_DOCS = Integer.MAX_VALUE; + + /** + * the default value for property {@link #mergeFactor}. + */ + public static final int DEFAULT_MERGE_FACTOR = 10; + + /** + * the default value for property {@link #maxFieldLength}. + */ + public static final int DEFAULT_MAX_FIELD_LENGTH = 10000; + + /** + * The default value for property {@link #extractorPoolSize}. + * @deprecated this value is not used anymore. Instead the default value + * is calculated as follows: 2 * Runtime.getRuntime().availableProcessors(). + */ + public static final int DEFAULT_EXTRACTOR_POOL_SIZE = 0; + + /** + * The default value for property {@link #extractorBackLog}. + */ + public static final int DEFAULT_EXTRACTOR_BACK_LOG = Integer.MAX_VALUE; + + /** + * The default timeout in milliseconds which is granted to the text + * extraction process until fulltext indexing is deferred to a background + * thread. + */ + public static final long DEFAULT_EXTRACTOR_TIMEOUT = 100; + + /** + * The default value for {@link #termInfosIndexDivisor}. + */ + public static final int DEFAULT_TERM_INFOS_INDEX_DIVISOR = 1; + + /** + * The path factory. + */ + protected static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + /** + * The path of the root node. + */ + protected static final Path ROOT_PATH; + + /** + * The path /jcr:system. + */ + protected static final Path JCR_SYSTEM_PATH; + + static { + ROOT_PATH = PATH_FACTORY.create(NameConstants.ROOT); + try { + JCR_SYSTEM_PATH = PATH_FACTORY.create(ROOT_PATH, NameConstants.JCR_SYSTEM, false); + } catch (RepositoryException e) { + // should never happen, path is always valid + throw new InternalError(e.getMessage()); + } + } + + /** + * The actual index + */ + protected MultiIndex index; + + /** + * The analyzer we use for indexing. + */ + private final JackrabbitAnalyzer analyzer = new JackrabbitAnalyzer(); + + /** + * Path of the Tika configuration file used for text extraction. + */ + private String tikaConfigPath = null; + + /** + * Java command used to fork external parser processes, + * or null (the default) for in-process text extraction. + */ + private String forkJavaCommand = null; + + /** + * The Tika parser for extracting text content from binary properties. + * Initialized by the {@link #getParser()} method during first access. + */ + private Parser parser = null; + + /** + * The namespace mappings used internally. + */ + private NamespaceMappings nsMappings; + + /** + * The location of the search index. + *

    + * Note: This is a mandatory parameter! + */ + private String path; + + /** + * minMergeDocs config parameter. + */ + private int minMergeDocs = DEFAULT_MIN_MERGE_DOCS; + + /** + * The maximum volatile index size in bytes until it is written to disk. + * The default value is 1048576 (1MB). + */ + private long maxVolatileIndexSize = 1024 * 1024; + + /** + * volatileIdleTime config parameter. + */ + private int volatileIdleTime = 3; + + /** + * The maximum age (in seconds) of the index history. The default value is + * zero. Which means, index commits are deleted as soon as they are not used + * anymore. + */ + private long maxHistoryAge = 0; + + /** + * maxMergeDocs config parameter + */ + private int maxMergeDocs = DEFAULT_MAX_MERGE_DOCS; + + /** + * mergeFactor config parameter + */ + private int mergeFactor = DEFAULT_MERGE_FACTOR; + + /** + * maxFieldLength config parameter + */ + private int maxFieldLength = DEFAULT_MAX_FIELD_LENGTH; + + /** + * maxExtractLength config parameter. Positive values are used as-is, + * negative values are interpreted as factors of the maxFieldLength + * parameter. + */ + private int maxExtractLength = -10; + + /** + * extractorPoolSize config parameter + */ + private int extractorPoolSize = 2 * Runtime.getRuntime().availableProcessors(); + + /** + * extractorBackLog config parameter + */ + private int extractorBackLog = DEFAULT_EXTRACTOR_BACK_LOG; + + /** + * extractorTimeout config parameter + */ + private long extractorTimeout = DEFAULT_EXTRACTOR_TIMEOUT; + + /** + * Number of documents that are buffered before they are added to the index. + */ + private int bufferSize = 10; + + /** + * Compound file flag + */ + private boolean useCompoundFile = true; + + /** + * Flag indicating whether document order is enabled as the default + * ordering. + *

    + * Default value is: false. + */ + private boolean documentOrder = false; + + /** + * If set true the index is checked for consistency on startup. + * If false a consistency check is only performed when there + * are entries in the redo log on startup. + *

    + * Default value is: false. + */ + private boolean forceConsistencyCheck = false; + + /** + * If set true the index is checked for consistency depending + * on the {@link #forceConsistencyCheck} parameter. If set to + * false, no consistency check is performed, even if the redo + * log had been applied on startup. + *

    + * Default value is: false. + */ + private boolean consistencyCheckEnabled = false; + + /** + * If set true errors detected by the consistency check are + * repaired. If false the errors are only reported in the log. + *

    + * Default value is: true. + */ + private boolean autoRepair = true; + + /** + * The id resolver cache size. + *

    + * Default value is: 1000. + */ + private int cacheSize = 1000; + + /** + * The number of documents that are pre fetched when a query is executed. + *

    + * Default value is: {@link Integer#MAX_VALUE}. + */ + private int resultFetchSize = Integer.MAX_VALUE; + + /** + * If set to true the fulltext field is stored and and a term + * vector is created with offset information. + *

    + * Default value is: false. + */ + private boolean supportHighlighting = false; + + /** + * If enabled, NodeIterator.getSize() may report a larger value than the + * actual result. This value may shrink when the query result encounters + * non-existing nodes or the session does not have access to a node. This + * might be a security problem. + */ + private boolean sizeEstimate = false; + + /** + * The excerpt provider class. Implements {@link ExcerptProvider}. + */ + private Class excerptProviderClass = DefaultHTMLExcerpt.class; + + /** + * The path to the indexing configuration file (can be an absolute path to a + * file or a classpath resource). + */ + private String indexingConfigPath; + + /** + * The DOM with the indexing configuration or null if there + * is no such configuration. + */ + private Element indexingConfiguration; + + /** + * The indexing configuration. + */ + private IndexingConfiguration indexingConfig; + + /** + * The indexing configuration class. + * Implements {@link IndexingConfiguration}. + */ + private Class indexingConfigurationClass = IndexingConfigurationImpl.class; + + /** + * The class that implements {@link SynonymProvider}. + */ + private Class synonymProviderClass; + + /** + * The currently set synonym provider. + */ + private SynonymProvider synProvider; + + /** + * The configuration path for the synonym provider. + */ + private String synonymProviderConfigPath; + + /** + * The FileSystem for the synonym if the query handler context does not + * provide one. + */ + private FileSystem synonymProviderConfigFs; + + /** + * Indicates the index format version which is relevant to a query. This + * value may be different from what {@link MultiIndex#getIndexFormatVersion()} + * returns because queries may be executed on two physical indexes with + * different formats. Index format versions are considered backward + * compatible. That is, the lower version of the two physical indexes is + * used for querying. + */ + private IndexFormatVersion indexFormatVersion; + + /** + * The class that implements {@link SpellChecker}. + */ + private Class spellCheckerClass; + + /** + * The spell checker for this query handler or null if none is + * configured. + */ + private SpellChecker spellChecker; + + /** + * The similarity in use for indexing and searching. + */ + private Similarity similarity = Similarity.getDefault(); + + /** + * The name of the directory manager class implementation. + */ + private String directoryManagerClass = FSDirectoryManager.class.getName(); + + /** + * The directory manager. + */ + private DirectoryManager directoryManager; + + /** + * Flag that indicates whether the {@link DirectoryManager} should + * use the SimpleFSDirectory instead of letting Lucene + * automatically pick an implementation based on the platform we are + * running on. Note: see JCR-3818 for a discussion on the trade-off. + */ + private boolean useSimpleFSDirectory = true; + + /** + * The termInfosIndexDivisor. + */ + private int termInfosIndexDivisor = DEFAULT_TERM_INFOS_INDEX_DIVISOR; + + /** + * The field comparator source for indexed properties. + */ + private SharedFieldComparatorSource scs; + + /** + * Flag that indicates whether the hierarchy cache should be initialized + * immediately on startup. + */ + private boolean initializeHierarchyCache = true; + + /** + * The name of the redo log factory class implementation. + */ + private String redoLogFactoryClass = DefaultRedoLogFactory.class.getName(); + + /** + * The redo log factory. + */ + private RedoLogFactory redoLogFactory; + + /** + * Indicates if this SearchIndex is closed and cannot be used + * anymore. + */ + private boolean closed = false; + + /** + * Initializes this QueryHandler. This implementation requires + * that a path parameter is set in the configuration. If this condition + * is not met, a IOException is thrown. + * + * @throws IOException if an error occurs while initializing this handler. + */ + protected void doInit() throws IOException { + QueryHandlerContext context = getContext(); + if (path == null) { + throw new IOException("SearchIndex requires 'path' parameter in configuration!"); + } + + Set excludedIDs = new HashSet(); + if (context.getExcludedNodeId() != null) { + excludedIDs.add(context.getExcludedNodeId()); + } + + synProvider = createSynonymProvider(); + directoryManager = createDirectoryManager(); + redoLogFactory = createRedoLogFactory(); + + if (context.getParentHandler() instanceof SearchIndex) { + // use system namespace mappings + SearchIndex sysIndex = (SearchIndex) context.getParentHandler(); + nsMappings = sysIndex.getNamespaceMappings(); + } else { + // read local namespace mappings + File mapFile = new File(new File(path), NS_MAPPING_FILE); + if (mapFile.exists()) { + // be backward compatible and use ns_mappings.properties from + // index folder + nsMappings = new FileBasedNamespaceMappings(mapFile); + } else { + // otherwise use repository wide stable index prefix from + // namespace registry + nsMappings = new NSRegistryBasedNamespaceMappings( + context.getNamespaceRegistry()); + } + } + + scs = new SharedFieldComparatorSource( + FieldNames.PROPERTIES, context.getItemStateManager(), + context.getHierarchyManager(), nsMappings); + indexingConfig = createIndexingConfiguration(nsMappings); + analyzer.setIndexingConfig(indexingConfig); + + // initialize the Tika parser + parser = createParser(); + + index = new MultiIndex(this, excludedIDs); + if (index.numDocs() == 0) { + Path rootPath; + if (excludedIDs.isEmpty()) { + // this is the index for jcr:system + rootPath = JCR_SYSTEM_PATH; + } else { + rootPath = ROOT_PATH; + } + index.createInitialIndex(context.getItemStateManager(), + context.getRootId(), rootPath); + checkPendingJournalChanges(context); + } + if (consistencyCheckEnabled + && (index.getRedoLogApplied() || forceConsistencyCheck)) { + log.info("Running consistency check..."); + try { + ConsistencyCheck check = runConsistencyCheck(); + if (autoRepair) { + check.repair(true); + } else { + List errors = check.getErrors(); + if (errors.size() == 0) { + log.info("No errors detected."); + } + for (ConsistencyCheckError err : errors) { + log.info(err.toString()); + } + } + } catch (Exception e) { + log.warn("Failed to run consistency check on index: " + e); + } + } + + // initialize spell checker + spellChecker = createSpellChecker(); + + log.info("Index initialized: {} Version: {}", + new Object[]{path, index.getIndexFormatVersion()}); + if (!index.getIndexFormatVersion().equals(getIndexFormatVersion())) { + log.warn("Using Version {} for reading. Please re-index version " + + "storage for optimal performance.", + getIndexFormatVersion().getVersion()); + } + } + + /** + * Adds the node to the search index. + * @param node the node to add. + * @throws RepositoryException if an error occurs while indexing the node. + * @throws IOException if an error occurs while adding the node to the index. + */ + public void addNode(NodeState node) throws RepositoryException, IOException { + throw new UnsupportedOperationException("addNode"); + } + + /** + * Removes the node with id from the search index. + * @param id the id of the node to remove from the index. + * @throws IOException if an error occurs while removing the node from + * the index. + */ + public void deleteNode(NodeId id) throws IOException { + throw new UnsupportedOperationException("deleteNode"); + } + + /** + * This implementation forwards the call to + * {@link MultiIndex#update(Collection, Collection)} and + * transforms the two iterators to the required types. + * + * @param remove ids of nodes to remove. + * @param add NodeStates to add. Calls to next() on this + * iterator may return null, to indicate that a + * node could not be indexed successfully. + * @throws RepositoryException if an error occurs while indexing a node. + * @throws IOException if an error occurs while updating the index. + */ + public void updateNodes(Iterator remove, Iterator add) + throws RepositoryException, IOException { + checkOpen(); + + Map aggregateRoots = new HashMap(); + Set removedIds = new HashSet(); + Set addedIds = new HashSet(); + + long time = System.currentTimeMillis(); + Collection removeCollection = new ArrayList(); + while (remove.hasNext()) { + NodeId id = remove.next(); + removeCollection.add(id); + removedIds.add(id); + } + + Collection addCollection = new ArrayList(); + while (add.hasNext()) { + NodeState state = add.next(); + if (state != null) { + NodeId id = state.getNodeId(); + addedIds.add(id); + retrieveAggregateRoot(state, aggregateRoots); + + try { + addCollection.add(createDocument( + state, getNamespaceMappings(), + index.getIndexFormatVersion())); + } catch (RepositoryException e) { + log.warn("Exception while creating document for node: " + + state.getNodeId() + ": " + e.toString()); + } + } + } + time = System.currentTimeMillis() - time; + log.debug("created the removeCollection {} and addCollection {} in {}ms", new Object[] {removeCollection.size(), addCollection.size(), time}); + + index.update(removeCollection, addCollection); + + // remove any aggregateRoot nodes that are new + // and therefore already up-to-date + aggregateRoots.keySet().removeAll(addedIds); + + // based on removed ids get affected aggregate root nodes + retrieveAggregateRoot(removedIds, aggregateRoots); + + // update aggregates if there are any affected + if (!aggregateRoots.isEmpty()) { + Collection modified = + new ArrayList(aggregateRoots.size()); + + for (NodeState state : aggregateRoots.values()) { + try { + modified.add(createDocument( + state, getNamespaceMappings(), + index.getIndexFormatVersion())); + } catch (RepositoryException e) { + log.warn("Exception while creating document for node: " + + state.getNodeId(), e); + } + } + + index.update(aggregateRoots.keySet(), modified); + } + } + + /** + * Creates a new query by specifying the query statement itself and the + * language in which the query is stated. If the query statement is + * syntactically invalid, given the language specified, an + * InvalidQueryException is thrown. language must specify a query language + * string from among those returned by QueryManager.getSupportedQueryLanguages(); if it is not + * then an InvalidQueryException is thrown. + * + * @param sessionContext component context of the current session + * @param statement the query statement. + * @param language the syntax of the query statement. + * @throws InvalidQueryException if statement is invalid or language is unsupported. + * @return A Query object. + */ + public ExecutableQuery createExecutableQuery( + SessionContext sessionContext, String statement, String language) + throws InvalidQueryException { + QueryImpl query = new QueryImpl( + sessionContext, this, getContext().getPropertyTypeRegistry(), + statement, language, getQueryNodeFactory()); + query.setRespectDocumentOrder(documentOrder); + return query; + } + + /** + * {@inheritDoc} + */ + public Iterable getWeaklyReferringNodes(NodeId id) + throws RepositoryException, IOException { + final List docs = new ArrayList(); + final List ids = new ArrayList(); + final IndexReader reader = getIndexReader(); + try { + IndexSearcher searcher = new IndexSearcher(reader); + try { + Query q = new TermQuery(new Term( + FieldNames.WEAK_REFS, id.toString())); + searcher.search(q, new AbstractHitCollector() { + @Override + public void collect(int doc, float score) { + docs.add(doc); + } + }); + } finally { + searcher.close(); + } + for (Integer doc : docs) { + Document d = reader.document(doc, FieldSelectors.UUID); + ids.add(new NodeId(d.get(FieldNames.UUID))); + } + } finally { + Util.closeOrRelease(reader); + } + return ids; + } + + List getNodeDocuments(NodeId id) throws RepositoryException, IOException { + final List docIds = new ArrayList(1); + final List docs = new ArrayList(); + final IndexReader reader = getIndexReader(); + try { + IndexSearcher searcher = new IndexSearcher(reader); + try { + Query q = new TermQuery(new Term(FieldNames.UUID, id.toString())); + searcher.search(q, new AbstractHitCollector() { + @Override + protected void collect(final int doc, final float score) { + docIds.add(doc); + } + }); + for (Integer docId : docIds) { + docs.add(reader.document(docId, FieldSelectors.UUID_AND_PARENT)); + } + } finally { + searcher.close(); + } + } finally { + Util.closeOrRelease(reader); + } + return docs; + } + + /** + * This method returns the QueryNodeFactory used to parse Queries. This method + * may be overridden to provide a customized QueryNodeFactory + * + * @return the query node factory. + */ + protected DefaultQueryNodeFactory getQueryNodeFactory() { + return DEFAULT_QUERY_NODE_FACTORY; + } + + /** + * Waits until all pending text extraction tasks have been processed + * and the updated index has been flushed to disk. + * + * @throws RepositoryException if the index update can not be written + */ + public void flush() throws RepositoryException { + try { + index.waitUntilIndexingQueueIsEmpty(); + index.safeFlush(); + // flush may have pushed nodes into the indexing queue + // -> wait again + index.waitUntilIndexingQueueIsEmpty(); + } catch (IOException e) { + throw new RepositoryException("Failed to flush the index", e); + } + } + + /** + * Closes this QueryHandler and frees resources attached + * to this handler. + */ + public void close() throws IOException { + if (synonymProviderConfigFs != null) { + try { + synonymProviderConfigFs.close(); + } catch (FileSystemException e) { + log.warn("Exception while closing FileSystem", e); + } + } + if (spellChecker != null) { + spellChecker.close(); + } + index.close(); + getContext().destroy(); + super.close(); + closed = true; + log.info("Index closed: " + path); + } + + /** + * Executes the query on the search index. + * + * @param session the session that executes the query. + * @param queryImpl the query impl. + * @param query the lucene query. + * @param orderProps name of the properties for sort order. + * @param orderSpecs the order specs for the sort order properties. + * true indicates ascending order, + * false indicates descending. + * @param orderFuncs functions for the properties for sort order. + * @param resultFetchHint a hint on how many results should be fetched. @return the query hits. + * @throws IOException if an error occurs while searching the index. + */ + public MultiColumnQueryHits executeQuery(SessionImpl session, + AbstractQueryImpl queryImpl, + Query query, + Path[] orderProps, + boolean[] orderSpecs, + String[] orderFuncs, long resultFetchHint) + throws IOException { + checkOpen(); + + Sort sort = new Sort(createSortFields(orderProps, orderSpecs, orderFuncs)); + + final IndexReader reader = getIndexReader(queryImpl.needsSystemTree()); + JackrabbitIndexSearcher searcher = new JackrabbitIndexSearcher( + session, reader, getContext().getItemStateManager()); + searcher.setSimilarity(getSimilarity()); + return new FilterMultiColumnQueryHits( + searcher.execute(query, sort, resultFetchHint, + QueryImpl.DEFAULT_SELECTOR_NAME)) { + public void close() throws IOException { + try { + super.close(); + } finally { + Util.closeOrRelease(reader); + } + } + }; + } + + /** + * Executes the query on the search index. + * + * @param session the session that executes the query. + * @param query the query. + * @param orderings the order specs for the sort order. + * @param resultFetchHint a hint on how many results should be fetched. + * @return the query hits. + * @throws IOException if an error occurs while searching the index. + */ + public MultiColumnQueryHits executeQuery(SessionImpl session, + MultiColumnQuery query, + Ordering[] orderings, + long resultFetchHint) + throws IOException { + checkOpen(); + + final IndexReader reader = getIndexReader(); + JackrabbitIndexSearcher searcher = new JackrabbitIndexSearcher( + session, reader, getContext().getItemStateManager()); + searcher.setSimilarity(getSimilarity()); + return new FilterMultiColumnQueryHits( + query.execute(searcher, orderings, resultFetchHint)) { + public void close() throws IOException { + try { + super.close(); + } finally { + Util.closeOrRelease(reader); + } + } + }; + } + + /** + * Creates an excerpt provider for the given query. + * + * @param query the query. + * @return an excerpt provider for the given query. + * @throws IOException if the provider cannot be created. + */ + public ExcerptProvider createExcerptProvider(Query query) + throws IOException { + ExcerptProvider ep; + try { + ep = (ExcerptProvider) excerptProviderClass.newInstance(); + } catch (Exception e) { + throw Util.createIOException(e); + } + ep.init(query, this); + return ep; + } + + /** + * Returns the analyzer in use for indexing. + * @return the analyzer in use for indexing. + */ + public Analyzer getTextAnalyzer() { + return new LimitTokenCountAnalyzer(analyzer, getMaxFieldLength()); + } + + /** + * Returns the path of the Tika configuration used for text extraction. + * + * @return path of the Tika configuration file + */ + public String getTikaConfigPath() { + return tikaConfigPath; + } + + /** + * Sets the path of the Tika configuration used for text extraction. + * The path can be either a file system or a class resource path. + * The default setting is the tika-config.xml class resource relative + * to org.apache.core.query.lucene. + * + * @param tikaConfigPath path of the Tika configuration file + */ + public void setTikaConfigPath(String tikaConfigPath) { + this.tikaConfigPath = tikaConfigPath; + } + + /** + * Returns the java command used to fork external parser processes, + * or null (the default) for in-process text extraction. + * + * @return fork java command + */ + public String getForkJavaCommand() { + return forkJavaCommand; + } + + /** + * Sets the java command used to fork external parser processes. + * + * @param command fork java command, + * or null for in-process extraction + */ + public void setForkJavaCommand(String command) { + this.forkJavaCommand = command; + } + + /** + * Returns the parser used for extracting text content + * from binary properties for full text indexing. + * + * @return the configured parser + */ + public Parser getParser() { + return parser; + } + + private Parser createParser() { + URL url = null; + if (tikaConfigPath != null) { + File file = new File(tikaConfigPath); + if (file.exists()) { + try { + url = file.toURI().toURL(); + } catch (MalformedURLException e) { + log.warn("Invalid Tika configuration path: " + file, e); + } + } else { + ClassLoader loader = SearchIndex.class.getClassLoader(); + url = loader.getResource(tikaConfigPath); + } + } + if (url == null) { + url = SearchIndex.class.getResource("tika-config.xml"); + } + + TikaConfig config = null; + if (url != null) { + try { + config = new TikaConfig(url); + } catch (Exception e) { + log.warn("Tika configuration not available: " + url, e); + } + } + if (config == null) { + config = TikaConfig.getDefaultConfig(); + } + + if (forkJavaCommand != null) { + ForkParser forkParser = new ForkParser( + SearchIndex.class.getClassLoader(), + new AutoDetectParser(config)); + forkParser.setJavaCommand(forkJavaCommand); + forkParser.setPoolSize(extractorPoolSize); + return forkParser; + } else { + return new AutoDetectParser(config); + } + } + + /** + * Returns the namespace mappings for the internal representation. + * @return the namespace mappings for the internal representation. + */ + public NamespaceMappings getNamespaceMappings() { + return nsMappings; + } + + /** + * @return the indexing configuration or null if there is + * none. + */ + public IndexingConfiguration getIndexingConfig() { + return indexingConfig; + } + + /** + * @return the synonym provider of this search index. If none is set for + * this search index the synonym provider of the parent handler is + * returned if there is any. + */ + public SynonymProvider getSynonymProvider() { + if (synProvider != null) { + return synProvider; + } else { + QueryHandler handler = getContext().getParentHandler(); + if (handler instanceof SearchIndex) { + return ((SearchIndex) handler).getSynonymProvider(); + } else { + return null; + } + } + } + + /** + * @return the spell checker of this search index. If none is configured + * this method returns null. + */ + public SpellChecker getSpellChecker() { + return spellChecker; + } + + /** + * @return the similarity, which should be used for indexing and searching. + */ + public Similarity getSimilarity() { + return similarity; + } + + /** + * Returns an index reader for this search index. The caller of this method + * is responsible for closing the index reader when he is finished using + * it. + * + * @return an index reader for this search index. + * @throws IOException the index reader cannot be obtained. + */ + public IndexReader getIndexReader() throws IOException { + return getIndexReader(true); + } + + /** + * Returns the index format version that this search index is able to + * support when a query is executed on this index. + * + * @return the index format version for this search index. + */ + public IndexFormatVersion getIndexFormatVersion() { + if (indexFormatVersion == null) { + if (getContext().getParentHandler() instanceof SearchIndex) { + SearchIndex parent = (SearchIndex) getContext().getParentHandler(); + if (parent.getIndexFormatVersion().getVersion() + < index.getIndexFormatVersion().getVersion()) { + indexFormatVersion = parent.getIndexFormatVersion(); + } else { + indexFormatVersion = index.getIndexFormatVersion(); + } + } else { + indexFormatVersion = index.getIndexFormatVersion(); + } + } + return indexFormatVersion; + } + + /** + * @return the directory manager for this search index. + */ + public DirectoryManager getDirectoryManager() { + return directoryManager; + } + + /** + * @return the redo log factory for this search index. + */ + public RedoLogFactory getRedoLogFactory() { + return redoLogFactory; + } + + /** + * Runs a consistency check on this search index. + * + * @return the result of the consistency check. + * @throws IOException if an error occurs while running the check. + */ + public ConsistencyCheck runConsistencyCheck() throws IOException { + return index.runConsistencyCheck(); + } + + /** + * Returns an index reader for this search index. The caller of this method + * is responsible for closing the index reader when he is finished using + * it. + * + * @param includeSystemIndex if true the index reader will + * cover the complete workspace. If + * false the returned index reader + * will not contains any nodes under /jcr:system. + * @return an index reader for this search index. + * @throws IOException the index reader cannot be obtained. + */ + protected IndexReader getIndexReader(boolean includeSystemIndex) + throws IOException { + QueryHandler parentHandler = getContext().getParentHandler(); + CachingMultiIndexReader parentReader = null; + if (parentHandler instanceof SearchIndex && includeSystemIndex) { + parentReader = ((SearchIndex) parentHandler).index.getIndexReader(); + } + + IndexReader reader; + if (parentReader != null) { + CachingMultiIndexReader[] readers = {index.getIndexReader(), parentReader}; + reader = new CombinedIndexReader(readers); + } else { + reader = index.getIndexReader(); + } + return new JackrabbitIndexReader(reader); + } + + /** + * Creates the SortFields for the order properties. + * + * @param orderProps the order properties. + * @param orderSpecs the order specs for the properties. + * @param orderFuncs the functions for the properties. + * @return an array of sort fields + */ + protected SortField[] createSortFields(Path[] orderProps, + boolean[] orderSpecs, String[] orderFuncs) { + List sortFields = new ArrayList(); + for (int i = 0; i < orderProps.length; i++) { + if (orderProps[i].getLength() == 1 + && NameConstants.JCR_SCORE.equals(orderProps[i].getName())) { + // order on jcr:score does not use the natural order as + // implemented in lucene. score ascending in lucene means that + // higher scores are first. JCR specs that lower score values + // are first. + sortFields.add(new SortField(null, SortField.SCORE, orderSpecs[i])); + } else { + if ("upper-case".equals(orderFuncs[i])) { + sortFields.add(new SortField(orderProps[i].getString(), new UpperCaseSortComparator(scs), !orderSpecs[i])); + } else if ("lower-case".equals(orderFuncs[i])) { + sortFields.add(new SortField(orderProps[i].getString(), new LowerCaseSortComparator(scs), !orderSpecs[i])); + } else if ("normalize".equals(orderFuncs[i])) { + sortFields.add(new SortField(orderProps[i].getString(), new NormalizeSortComparator(scs), !orderSpecs[i])); + } else { + sortFields.add(new SortField(orderProps[i].getString(), scs, !orderSpecs[i])); + } + } + } + return sortFields.toArray(new SortField[sortFields.size()]); + } + + /** + * Creates internal orderings for the QOM ordering specifications. + * + * @param orderings the QOM ordering specifications. + * @return the internal orderings. + * @throws RepositoryException if an error occurs. + */ + protected Ordering[] createOrderings(OrderingImpl[] orderings) + throws RepositoryException { + Ordering[] ords = new Ordering[orderings.length]; + for (int i = 0; i < orderings.length; i++) { + ords[i] = Ordering.fromQOM(orderings[i], scs, nsMappings); + } + return ords; + } + + /** + * Creates a lucene Document for a node state using the + * namespace mappings nsMappings. + * + * @param node the node state to index. + * @param nsMappings the namespace mappings of the search index. + * @param indexFormatVersion the index format version that should be used to + * index the passed node state. + * @return a lucene Document that contains all properties of + * node. + * @throws RepositoryException if an error occurs while indexing the + * node. + */ + protected Document createDocument(NodeState node, + NamespaceMappings nsMappings, + IndexFormatVersion indexFormatVersion) + throws RepositoryException { + NodeIndexer indexer = new NodeIndexer( + node, getContext().getItemStateManager(), nsMappings, + getContext().getExecutor(), parser); + indexer.setSupportHighlighting(supportHighlighting); + indexer.setIndexingConfiguration(indexingConfig); + indexer.setIndexFormatVersion(indexFormatVersion); + indexer.setMaxExtractLength(getMaxExtractLength()); + Document doc = indexer.createDoc(); + mergeAggregatedNodeIndexes(node, doc, indexFormatVersion); + return doc; + } + + /** + * Returns the actual index. + * + * @return the actual index. + */ + protected MultiIndex getIndex() { + return index; + } + + /** + * @return the field comparator source for this index. + */ + protected SharedFieldComparatorSource getSortComparatorSource() { + return scs; + } + + /** + * @param namespaceMappings The namespace mappings + * @return the fulltext indexing configuration or null if there + * is no configuration. + */ + protected IndexingConfiguration createIndexingConfiguration(NamespaceMappings namespaceMappings) { + Element docElement = getIndexingConfigurationDOM(); + if (docElement == null) { + return null; + } + try { + IndexingConfiguration idxCfg = (IndexingConfiguration) + indexingConfigurationClass.newInstance(); + idxCfg.init(docElement, getContext(), namespaceMappings); + return idxCfg; + } catch (Exception e) { + log.warn("Exception initializing indexing configuration from: " + + indexingConfigPath, e); + } + log.warn(indexingConfigPath + " ignored."); + return null; + } + + /** + * @return the configured synonym provider or null if none is + * configured or an error occurs. + */ + protected SynonymProvider createSynonymProvider() { + SynonymProvider sp = null; + if (synonymProviderClass != null) { + try { + sp = (SynonymProvider) synonymProviderClass.newInstance(); + sp.initialize(createSynonymProviderConfigResource()); + } catch (Exception e) { + log.warn("Exception initializing synonym provider: " + + synonymProviderClass, e); + sp = null; + } + } + return sp; + } + + /** + * @return an initialized {@link DirectoryManager}. + * @throws IOException if the directory manager cannot be instantiated or + * an exception occurs while initializing the manager. + */ + protected DirectoryManager createDirectoryManager() + throws IOException { + try { + Class clazz = Class.forName(directoryManagerClass); + if (!DirectoryManager.class.isAssignableFrom(clazz)) { + throw new IOException(directoryManagerClass + + " is not a DirectoryManager implementation"); + } + DirectoryManager df = (DirectoryManager) clazz.newInstance(); + df.init(this); + return df; + } catch (IOException e) { + throw e; + } catch (Exception e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } + + /** + * Creates a redo log factory based on {@link #getRedoLogFactoryClass()}. + * + * @return the redo log factory. + * @throws IOException if an error occurs while creating the factory. + */ + protected RedoLogFactory createRedoLogFactory() throws IOException { + try { + Class clazz = Class.forName(redoLogFactoryClass); + if (!RedoLogFactory.class.isAssignableFrom(clazz)) { + throw new IOException(redoLogFactoryClass + + " is not a RedoLogFactory implementation"); + } + return (RedoLogFactory) clazz.newInstance(); + } catch (Exception e) { + IOException ex = new IOException(); + ex.initCause(e); + throw ex; + } + } + + /** + * Creates a file system resource to the synonym provider configuration. + * + * @return a file system resource or null if no path was + * configured. + * @throws FileSystemException if an exception occurs accessing the file + * system. + * @throws IOException if another exception occurs. + */ + protected FileSystemResource createSynonymProviderConfigResource() + throws FileSystemException, IOException { + if (synonymProviderConfigPath != null) { + FileSystemResource fsr; + // simple sanity check + if (synonymProviderConfigPath.endsWith(FileSystem.SEPARATOR)) { + throw new FileSystemException( + "Invalid synonymProviderConfigPath: " + + synonymProviderConfigPath); + } + if (fs == null) { + fs = new LocalFileSystem(); + int lastSeparator = synonymProviderConfigPath.lastIndexOf( + FileSystem.SEPARATOR_CHAR); + if (lastSeparator != -1) { + File root = new File(path, + synonymProviderConfigPath.substring(0, lastSeparator)); + ((LocalFileSystem) fs).setRoot(root.getCanonicalFile()); + fs.init(); + fsr = new FileSystemResource(fs, + synonymProviderConfigPath.substring(lastSeparator + 1)); + } else { + ((LocalFileSystem) fs).setPath(path); + fs.init(); + fsr = new FileSystemResource(fs, synonymProviderConfigPath); + } + synonymProviderConfigFs = fs; + } else { + fsr = new FileSystemResource(fs, synonymProviderConfigPath); + } + return fsr; + } else { + // path not configured + return null; + } + } + + /** + * Creates a spell checker for this query handler. + * + * @return the spell checker or null if none is configured or + * an error occurs. + */ + protected SpellChecker createSpellChecker() { + SpellChecker spCheck = null; + if (spellCheckerClass != null) { + try { + spCheck = (SpellChecker) spellCheckerClass.newInstance(); + spCheck.init(this); + } catch (Exception e) { + log.warn("Exception initializing spell checker: " + + spellCheckerClass, e); + } + } + return spCheck; + } + + /** + * Returns the document element of the indexing configuration or + * null if there is no indexing configuration. + * + * @return the indexing configuration or null if there is + * none. + */ + protected Element getIndexingConfigurationDOM() { + if (indexingConfiguration != null) { + return indexingConfiguration; + } + if (indexingConfigPath == null) { + return null; + } + File config = new File(indexingConfigPath); + InputStream configStream = null; + + if (!config.exists()) { + // check if it's a classpath resource + configStream = getClass().getResourceAsStream(indexingConfigPath); + + if (configStream == null) { + // only warn if not available also in the classpath + log.warn("File does not exist: " + indexingConfigPath); + return null; + } + } else if (!config.canRead()) { + log.warn("Cannot read file: " + indexingConfigPath); + return null; + } + try { + DocumentBuilderFactory factory = + DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new IndexingConfigurationEntityResolver()); + + if (configStream != null) { + indexingConfiguration = builder + .parse(configStream).getDocumentElement(); + } else { + indexingConfiguration = builder + .parse(config).getDocumentElement(); + } + } catch (ParserConfigurationException e) { + log.warn("Unable to create XML parser", e); + } catch (IOException e) { + log.warn("Exception parsing " + indexingConfigPath, e); + } catch (SAXException e) { + log.warn("Exception parsing " + indexingConfigPath, e); + } finally { + if (configStream != null) { + try { + configStream.close(); + } catch (IOException e) { + // ignore + } + } + } + return indexingConfiguration; + } + + /** + * Merges the fulltext indexed fields of the aggregated node states into + * doc. + * + * @param state the node state on which doc was created. + * @param doc the lucene document with index fields from state. + * @param ifv the current index format version. + */ + protected void mergeAggregatedNodeIndexes(NodeState state, + Document doc, + IndexFormatVersion ifv) { + if (indexingConfig != null) { + AggregateRule[] aggregateRules = indexingConfig.getAggregateRules(); + if (aggregateRules == null) { + return; + } + try { + ItemStateManager ism = getContext().getItemStateManager(); + for (AggregateRule aggregateRule : aggregateRules) { + boolean ruleMatched = false; + // node includes + NodeState[] aggregates = aggregateRule.getAggregatedNodeStates(state); + if (aggregates != null) { + ruleMatched = true; + for (NodeState aggregate : aggregates) { + Document aDoc = createDocument(aggregate, getNamespaceMappings(), ifv); + // transfer fields to doc if there are any + Fieldable[] fulltextFields = aDoc.getFieldables(FieldNames.FULLTEXT); + if (fulltextFields != null) { + for (Fieldable fulltextField : fulltextFields) { + doc.add(fulltextField); + } + doc.add(new Field( + FieldNames.AGGREGATED_NODE_UUID, false, + aggregate.getNodeId().toString(), + Field.Store.NO, + Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO)); + } + } + // make sure that fulltext fields are aligned properly + // first all stored fields, then remaining + Fieldable[] fulltextFields = doc + .getFieldables(FieldNames.FULLTEXT); + doc.removeFields(FieldNames.FULLTEXT); + Arrays.sort(fulltextFields, FIELDS_COMPARATOR_STORED); + for (Fieldable f : fulltextFields) { + doc.add(f); + } + } + // property includes + PropertyState[] propStates = aggregateRule.getAggregatedPropertyStates(state); + if (propStates != null) { + ruleMatched = true; + for (PropertyState propState : propStates) { + String namePrefix = FieldNames.createNamedValue(getNamespaceMappings().translateName(propState.getName()), ""); + NodeState parent = (NodeState) ism.getItemState(propState.getParentId()); + Document aDoc = createDocument(parent, getNamespaceMappings(), ifv); + try { + // find the right fields to transfer + Fieldable[] fields = aDoc.getFieldables(FieldNames.PROPERTIES); + for (Fieldable field : fields) { + + // assume properties fields use SingleTokenStream + TokenStream tokenStream = field.tokenStreamValue(); + TermAttribute termAttribute = tokenStream.addAttribute(TermAttribute.class); + PayloadAttribute payloadAttribute = tokenStream.addAttribute(PayloadAttribute.class); + tokenStream.incrementToken(); + tokenStream.end(); + tokenStream.close(); + + String value = new String(termAttribute.termBuffer(), 0, termAttribute.termLength()); + if (value.startsWith(namePrefix)) { + // extract value + String rawValue = value.substring(namePrefix.length()); + // create new named value + Path p = getRelativePath(state, propState); + String path = getNamespaceMappings().translatePath(p); + value = FieldNames.createNamedValue(path, rawValue); + termAttribute.setTermBuffer(value); + PropertyMetaData pdm = PropertyMetaData + .fromByteArray(payloadAttribute + .getPayload().getData()); + doc.add(new Field(field.name(), + new SingletonTokenStream(value, + pdm.getPropertyType()))); + doc.add(new Field( + FieldNames.AGGREGATED_NODE_UUID, + false, + parent.getNodeId().toString(), + Field.Store.NO, + Field.Index.NOT_ANALYZED_NO_NORMS, + Field.TermVector.NO)); + if (pdm.getPropertyType() == PropertyType.STRING) { + // add to fulltext index + Field ft = new Field( + FieldNames.FULLTEXT, + false, + rawValue, + Field.Store.YES, + Field.Index.ANALYZED_NO_NORMS, + Field.TermVector.NO); + doc.add(ft); + } + } + } + } finally { + Util.disposeDocument(aDoc); + } + } + } + + // only use first aggregate definition that matches + if (ruleMatched) { + break; + } + } + } catch (NoSuchItemStateException e) { + // do not fail if aggregate cannot be created + log.info( + "Exception while building indexing aggregate for {}. Node is not available {}.", + state.getNodeId(), e.getMessage()); + } catch (Exception e) { + // do not fail if aggregate cannot be created + log.warn("Exception while building indexing aggregate for " + + state.getNodeId(), e); + } + } + } + + private static final Comparator FIELDS_COMPARATOR_STORED = new Comparator() { + public int compare(Fieldable o1, Fieldable o2) { + return Boolean.valueOf(o2.isStored()).compareTo(o1.isStored()); + } + }; + + /** + * Returns the relative path from nodeState to + * propState. + * + * @param nodeState a node state. + * @param propState a property state. + * @return the relative path. + * @throws RepositoryException if an error occurs while resolving paths. + * @throws ItemStateException if an error occurs while reading item + * states. + */ + protected Path getRelativePath(NodeState nodeState, PropertyState propState) + throws RepositoryException, ItemStateException { + HierarchyManager hmgr = getContext().getHierarchyManager(); + Path nodePath = hmgr.getPath(nodeState.getId()); + Path propPath = hmgr.getPath(propState.getId()); + Path p = nodePath.computeRelativePath(propPath); + // make sure it does not contain indexes + boolean clean = true; + Path.Element[] elements = p.getElements(); + for (int i = 0; i < elements.length; i++) { + if (elements[i].getIndex() != 0) { + elements[i] = PATH_FACTORY.createElement(elements[i].getName()); + clean = false; + } + } + if (!clean) { + p = PATH_FACTORY.create(elements); + } + return p.getNormalizedPath(); + } + + /** + * Retrieves the root of the indexing aggregate for state and + * puts it into aggregates map. + * + * @param state the node state for which we want to retrieve the aggregate + * root. + * @param aggregates aggregate roots are collected in this map. + */ + protected void retrieveAggregateRoot(NodeState state, + Map aggregates) { + retrieveAggregateRoot(state, aggregates, state.getNodeId().toString(), 0); + } + + /** + * Retrieves the root of the indexing aggregate for state and + * puts it into aggregates map. + * + * @param state + * the node state for which we want to retrieve the aggregate + * root. + * @param aggregates + * aggregate roots are collected in this map. + * @param originNodeId + * the originating node, used for reporting only + * @param level + * current aggregation level, used to limit recursive aggregation + * of nodes that have the same type + */ + private void retrieveAggregateRoot(NodeState state, + Map aggregates, String originNodeId, long level) { + if (indexingConfig == null) { + return; + } + AggregateRule[] aggregateRules = indexingConfig.getAggregateRules(); + if (aggregateRules == null) { + return; + } + for (AggregateRule aggregateRule : aggregateRules) { + NodeState root = null; + try { + root = aggregateRule.getAggregateRoot(state); + } catch (Exception e) { + log.warn("Unable to get aggregate root for " + state.getNodeId(), e); + } + if (root == null) { + continue; + } + if (root.getNodeTypeName().equals(state.getNodeTypeName())) { + level++; + } else { + level = 0; + } + + // JCR-2989 Support for embedded index aggregates + if ((aggregateRule.getRecursiveAggregationLimit() == 0) + || (aggregateRule.getRecursiveAggregationLimit() != 0 && level <= aggregateRule + .getRecursiveAggregationLimit())) { + + // check if the update parent is already in the + // map, then all its parents are already there so I can + // skip this update subtree + if (aggregates.put(root.getNodeId(), root) == null) { + retrieveAggregateRoot(root, aggregates, originNodeId, level); + } + } else { + log.warn( + "Reached {} levels of recursive aggregation for nodeId {}, type {}, will stop at nodeId {}. Are you sure this did not occur by mistake? Please check the indexing-configuration.xml.", + new Object[] { level, originNodeId, + root.getNodeTypeName(), root.getNodeId() }); + } + } + } + + /** + * Retrieves the root of the indexing aggregate for removedIds + * and puts it into map. + * + * @param removedIds the ids of removed nodes. + * @param aggregates aggregate roots are collected in this map + */ + protected void retrieveAggregateRoot( + Set removedIds, Map aggregates) { + if(removedIds.isEmpty() || indexingConfig == null){ + return; + } + AggregateRule[] aggregateRules = indexingConfig.getAggregateRules(); + if (aggregateRules == null) { + return; + } + int found = 0; + long time = System.currentTimeMillis(); + try { + CachingMultiIndexReader reader = index.getIndexReader(); + try { + Term aggregateIds = + new Term(FieldNames.AGGREGATED_NODE_UUID, ""); + TermDocs tDocs = reader.termDocs(); + try { + ItemStateManager ism = getContext().getItemStateManager(); + for (NodeId id : removedIds) { + aggregateIds = + aggregateIds.createTerm(id.toString()); + tDocs.seek(aggregateIds); + while (tDocs.next()) { + Document doc = reader.document( + tDocs.doc(), FieldSelectors.UUID); + NodeId nId = new NodeId(doc.get(FieldNames.UUID)); + NodeState nodeState = (NodeState) ism.getItemState(nId); + aggregates.put(nId, nodeState); + found++; + + // JCR-2989 Support for embedded index aggregates + int sizeBefore = aggregates.size(); + retrieveAggregateRoot(nodeState, aggregates); + found += aggregates.size() - sizeBefore; + } + } + } finally { + tDocs.close(); + } + } finally { + reader.release(); + } + } catch (NoSuchItemStateException e) { + log.info( + "Exception while retrieving aggregate roots. Node is not available {}.", + e.getMessage()); + } catch (Exception e) { + log.warn("Exception while retrieving aggregate roots", e); + } + time = System.currentTimeMillis() - time; + log.debug("Retrieved {} aggregate roots in {} ms.", found, time); + } + + //----------------------------< internal >---------------------------------- + + /** + * Combines multiple {@link CachingMultiIndexReader} into a MultiReader + * with {@link HierarchyResolver} support. + */ + protected static final class CombinedIndexReader + extends MultiReader + implements HierarchyResolver, MultiIndexReader { + + /** + * The sub readers. + */ + private final CachingMultiIndexReader[] subReaders; + + public CombinedIndexReader(CachingMultiIndexReader[] indexReaders) { + super(indexReaders); + this.subReaders = indexReaders; + } + + /** + * {@inheritDoc} + */ + public int[] getParents(int n, int[] docNumbers) throws IOException { + int i = readerIndex(n); + DocId id = subReaders[i].getParentDocId(n - starts[i]); + id = id.applyOffset(starts[i]); + return id.getDocumentNumbers(this, docNumbers); + } + + //-------------------------< MultiIndexReader >------------------------- + + /** + * {@inheritDoc} + */ + public IndexReader[] getIndexReaders() { + IndexReader[] readers = new IndexReader[subReaders.length]; + System.arraycopy(subReaders, 0, readers, 0, subReaders.length); + return readers; + } + + /** + * {@inheritDoc} + */ + public void release() throws IOException { + for (CachingMultiIndexReader subReader : subReaders) { + subReader.release(); + } + } + + public boolean equals(Object obj) { + if (obj instanceof CombinedIndexReader) { + CombinedIndexReader other = (CombinedIndexReader) obj; + return Arrays.equals(subReaders, other.subReaders); + } + return false; + } + + public int hashCode() { + int hash = 0; + for (CachingMultiIndexReader subReader : subReaders) { + hash = 31 * hash + subReader.hashCode(); + } + return hash; + } + + /** + * {@inheritDoc} + */ + public ForeignSegmentDocId createDocId(NodeId id) throws IOException { + for (CachingMultiIndexReader subReader : subReaders) { + ForeignSegmentDocId doc = subReader.createDocId(id); + if (doc != null) { + return doc; + } + } + return null; + } + + /** + * {@inheritDoc} + */ + public int getDocumentNumber(ForeignSegmentDocId docId) { + for (int i = 0; i < subReaders.length; i++) { + CachingMultiIndexReader subReader = subReaders[i]; + int realDoc = subReader.getDocumentNumber(docId); + if (realDoc >= 0) { + return realDoc + starts[i]; + } + } + return -1; + } + } + + //--------------------------< properties >---------------------------------- + + /** + * Sets the default analyzer in use for indexing. The given analyzer + * class name must satisfy the following conditions: + *

      + *
    • the class must exist in the class path
    • + *
    • the class must have a public default constructor, or + * a constructor that takes a Lucene {@link Version} argument
    • + *
    • the class must be a Lucene Analyzer
    • + *
    + *

    + * If the above conditions are met, then a new instance of the class is + * set as the analyzer. Otherwise a warning is logged and the current + * analyzer is not changed. + *

    + * This property setter method is normally invoked by the Jackrabbit + * configuration mechanism if the "analyzer" parameter is set in the + * search configuration. + * + * @param analyzerClassName the analyzer class name + */ + public void setAnalyzer(String analyzerClassName) { + analyzer.setDefaultAnalyzerClass(analyzerClassName); + } + + /** + * Returns the class name of the default analyzer that is currently in use. + * + * @return class name of analyzer in use. + */ + public String getAnalyzer() { + return analyzer.getDefaultAnalyzerClass(); + } + + /** + * Sets the location of the search index. + * + * @param path the location of the search index. + */ + public void setPath(String path) { + this.path = path; + } + + /** + * Returns the location of the search index. Returns null if + * not set. + * + * @return the location of the search index. + */ + public String getPath() { + return path; + } + + /** + * The lucene index writer property: useCompoundFile + */ + public void setUseCompoundFile(boolean b) { + useCompoundFile = b; + } + + /** + * Returns the current value for useCompoundFile. + * + * @return the current value for useCompoundFile. + */ + public boolean getUseCompoundFile() { + return useCompoundFile; + } + + /** + * The lucene index writer property: minMergeDocs + */ + public void setMinMergeDocs(int minMergeDocs) { + this.minMergeDocs = minMergeDocs; + } + + /** + * Returns the current value for minMergeDocs. + * + * @return the current value for minMergeDocs. + */ + public int getMinMergeDocs() { + return minMergeDocs; + } + + /** + * Sets the property: volatileIdleTime + * + * @param volatileIdleTime idle time in seconds + */ + public void setVolatileIdleTime(int volatileIdleTime) { + this.volatileIdleTime = volatileIdleTime; + } + + /** + * Returns the current value for volatileIdleTime. + * + * @return the current value for volatileIdleTime. + */ + public int getVolatileIdleTime() { + return volatileIdleTime; + } + + /** + * The lucene index writer property: maxMergeDocs + */ + public void setMaxMergeDocs(int maxMergeDocs) { + this.maxMergeDocs = maxMergeDocs; + } + + /** + * Returns the current value for maxMergeDocs. + * + * @return the current value for maxMergeDocs. + */ + public int getMaxMergeDocs() { + return maxMergeDocs; + } + + /** + * The lucene index writer property: mergeFactor + */ + public void setMergeFactor(int mergeFactor) { + this.mergeFactor = mergeFactor; + } + + /** + * Returns the current value for the merge factor. + * + * @return the current value for the merge factor. + */ + public int getMergeFactor() { + return mergeFactor; + } + + /** + * @see VolatileIndex#setBufferSize(int) + */ + public void setBufferSize(int size) { + bufferSize = size; + } + + /** + * Returns the current value for the buffer size. + * + * @return the current value for the buffer size. + */ + public int getBufferSize() { + return bufferSize; + } + + public void setRespectDocumentOrder(boolean docOrder) { + documentOrder = docOrder; + } + + public boolean getRespectDocumentOrder() { + return documentOrder; + } + + public void setForceConsistencyCheck(boolean b) { + forceConsistencyCheck = b; + } + + public boolean getForceConsistencyCheck() { + return forceConsistencyCheck; + } + + public void setAutoRepair(boolean b) { + autoRepair = b; + } + + public boolean getAutoRepair() { + return autoRepair; + } + + public void setCacheSize(int size) { + cacheSize = size; + } + + public int getCacheSize() { + return cacheSize; + } + + public void setMaxFieldLength(int length) { + maxFieldLength = length; + } + + public int getMaxFieldLength() { + return maxFieldLength; + } + + public void setMaxExtractLength(int length) { + maxExtractLength = length; + } + + public int getMaxExtractLength() { + if (maxExtractLength < 0) { + return -maxExtractLength * maxFieldLength; + } else { + return maxExtractLength; + } + } + + /** + * Sets the list of text extractors (and text filters) to use for + * extracting text content from binary properties. The list must be + * comma (or whitespace) separated, and contain fully qualified class + * names of the {@code TextExtractor} (and {@code org.apache.jackrabbit.core.query.TextFilter}) classes + * to be used. The configured classes must all have a public default + * constructor. + * + * @param filterClasses comma separated list of class names + * @deprecated + */ + public void setTextFilterClasses(String filterClasses) { + log.warn("The textFilterClasses configuration parameter has" + + " been deprecated, and the configured value will" + + " be ignored: {}", filterClasses); + } + + /** + * Returns the fully qualified class names of the text filter instances + * currently in use. The names are comma separated. + * + * @return class names of the text filters in use. + * @deprecated + */ + public String getTextFilterClasses() { + return "deprectated"; + } + + /** + * Tells the query handler how many result should be fetched initially when + * a query is executed. + * + * @param size the number of results to fetch initially. + */ + public void setResultFetchSize(int size) { + resultFetchSize = size; + } + + /** + * @return the number of results the query handler will fetch initially when + * a query is executed. + */ + public int getResultFetchSize() { + return resultFetchSize; + } + + /** + * The number of background threads for the extractor pool. + * + * @param numThreads the number of threads. + */ + public void setExtractorPoolSize(int numThreads) { + if (numThreads < 0) { + numThreads = 0; + } + extractorPoolSize = numThreads; + } + + /** + * @return the size of the thread pool which is used to run the text + * extractors when binary content is indexed. + */ + public int getExtractorPoolSize() { + return extractorPoolSize; + } + + /** + * The number of extractor jobs that are queued until a new job is executed + * with the current thread instead of using the thread pool. + * + * @param backLog size of the extractor job queue. + */ + public void setExtractorBackLogSize(int backLog) { + extractorBackLog = backLog; + } + + /** + * @return the size of the extractor queue back log. + */ + public int getExtractorBackLogSize() { + return extractorBackLog; + } + + /** + * The timeout in milliseconds which is granted to the text extraction + * process until fulltext indexing is deferred to a background thread. + * + * @param timeout the timeout in milliseconds. + */ + public void setExtractorTimeout(long timeout) { + extractorTimeout = timeout; + } + + /** + * @return the extractor timeout in milliseconds. + */ + public long getExtractorTimeout() { + return extractorTimeout; + } + + /** + * If enabled, NodeIterator.getSize() may report a larger value than the + * actual result. This value may shrink when the query result encounters + * non-existing nodes or the session does not have access to a node. This + * might be a security problem. + * + * @param b true to enable + */ + public void setSizeEstimate(boolean b) { + if (b) { + log.info("Size estimation is enabled"); + } + this.sizeEstimate = b; + } + + /** + * Get the size estimate setting. + * + * @return the setting + */ + public boolean getSizeEstimate() { + return sizeEstimate; + } + + /** + * If set to true additional information is stored in the index + * to support highlighting using the rep:excerpt pseudo property. + * + * @param b true to enable highlighting support. + */ + public void setSupportHighlighting(boolean b) { + supportHighlighting = b; + } + + /** + * @return true if highlighting support is enabled. + */ + public boolean getSupportHighlighting() { + return supportHighlighting; + } + + /** + * Sets the class name for the {@link ExcerptProvider} that should be used + * for the rep:excerpt pseudo property in a query. + * + * @param className the name of a class that implements {@link + * ExcerptProvider}. + */ + public void setExcerptProviderClass(String className) { + try { + Class clazz = Class.forName(className); + if (ExcerptProvider.class.isAssignableFrom(clazz)) { + excerptProviderClass = clazz; + } else { + log.warn("Invalid value for excerptProviderClass, {} does " + + "not implement ExcerptProvider interface.", className); + } + } catch (ClassNotFoundException e) { + log.warn("Invalid value for excerptProviderClass, class {} not found.", + className); + } + } + + /** + * @return the class name of the excerpt provider implementation. + */ + public String getExcerptProviderClass() { + return excerptProviderClass.getName(); + } + + /** + * Sets the path to the indexing configuration file. + * + * @param path the path to the configuration file. + */ + public void setIndexingConfiguration(String path) { + indexingConfigPath = path; + } + + /** + * @return the path to the indexing configuration file. + */ + public String getIndexingConfiguration() { + return indexingConfigPath; + } + + /** + * Sets the name of the class that implements {@link IndexingConfiguration}. + * The default value is org.apache.jackrabbit.core.query.lucene.IndexingConfigurationImpl. + * + * @param className the name of the class that implements {@link + * IndexingConfiguration}. + */ + public void setIndexingConfigurationClass(String className) { + try { + Class clazz = Class.forName(className); + if (IndexingConfiguration.class.isAssignableFrom(clazz)) { + indexingConfigurationClass = clazz; + } else { + log.warn("Invalid value for indexingConfigurationClass, {} " + + "does not implement IndexingConfiguration interface.", + className); + } + } catch (ClassNotFoundException e) { + log.warn("Invalid value for indexingConfigurationClass, class {} not found.", + className); + } + } + + /** + * @return the class name of the indexing configuration implementation. + */ + public String getIndexingConfigurationClass() { + return indexingConfigurationClass.getName(); + } + + /** + * Sets the name of the class that implements {@link SynonymProvider}. The + * default value is null (none set). + * + * @param className name of the class that implements {@link + * SynonymProvider}. + */ + public void setSynonymProviderClass(String className) { + try { + Class clazz = Class.forName(className); + if (SynonymProvider.class.isAssignableFrom(clazz)) { + synonymProviderClass = clazz; + } else { + log.warn("Invalid value for synonymProviderClass, {} " + + "does not implement SynonymProvider interface.", + className); + } + } catch (ClassNotFoundException e) { + log.warn("Invalid value for synonymProviderClass, class {} not found.", + className); + } + } + + /** + * @return the class name of the synonym provider implementation or + * null if none is set. + */ + public String getSynonymProviderClass() { + if (synonymProviderClass != null) { + return synonymProviderClass.getName(); + } else { + return null; + } + } + + /** + * Sets the name of the class that implements {@link SpellChecker}. The + * default value is null (none set). + * + * @param className name of the class that implements {@link SpellChecker}. + */ + public void setSpellCheckerClass(String className) { + try { + Class clazz = Class.forName(className); + if (SpellChecker.class.isAssignableFrom(clazz)) { + spellCheckerClass = clazz; + } else { + log.warn("Invalid value for spellCheckerClass, {} " + + "does not implement SpellChecker interface.", + className); + } + } catch (ClassNotFoundException e) { + log.warn("Invalid value for spellCheckerClass," + + " class {} not found.", className); + } + } + + /** + * @return the class name of the spell checker implementation or + * null if none is set. + */ + public String getSpellCheckerClass() { + if (spellCheckerClass != null) { + return spellCheckerClass.getName(); + } else { + return null; + } + } + + /** + * Enables or disables the consistency check on startup. Consistency checks + * are disabled per default. + * + * @param b true enables consistency checks. + * @see #setForceConsistencyCheck(boolean) + */ + public void setEnableConsistencyCheck(boolean b) { + this.consistencyCheckEnabled = b; + } + + /** + * @return true if consistency checks are enabled. + */ + public boolean getEnableConsistencyCheck() { + return consistencyCheckEnabled; + } + + /** + * Sets the configuration path for the synonym provider. + * + * @param path the configuration path for the synonym provider. + */ + public void setSynonymProviderConfigPath(String path) { + synonymProviderConfigPath = path; + } + + /** + * @return the configuration path for the synonym provider. If none is set + * this method returns null. + */ + public String getSynonymProviderConfigPath() { + return synonymProviderConfigPath; + } + + /** + * Sets the similarity implementation, which will be used for indexing and + * searching. The implementation must extend {@link Similarity}. + * + * @param className a {@link Similarity} implementation. + */ + public void setSimilarityClass(String className) { + try { + Class similarityClass = Class.forName(className); + similarity = (Similarity) similarityClass.newInstance(); + } catch (Exception e) { + log.warn("Invalid Similarity class: " + className, e); + } + } + + /** + * @return the name of the similarity class. + */ + public String getSimilarityClass() { + return similarity.getClass().getName(); + } + + /** + * Sets a new maxVolatileIndexSize value. + * + * @param maxVolatileIndexSize the new value. + */ + public void setMaxVolatileIndexSize(long maxVolatileIndexSize) { + this.maxVolatileIndexSize = maxVolatileIndexSize; + } + + /** + * @return the maxVolatileIndexSize in bytes. + */ + public long getMaxVolatileIndexSize() { + return maxVolatileIndexSize; + } + + /** + * @return the name of the directory manager class. + */ + public String getDirectoryManagerClass() { + return directoryManagerClass; + } + + /** + * Sets name of the directory manager class. The class must implement + * {@link DirectoryManager}. + * + * @param className the name of the class that implements directory manager. + */ + public void setDirectoryManagerClass(String className) { + this.directoryManagerClass = className; + } + + /** + * If set true will indicate to the {@link DirectoryManager} + * to use the SimpleFSDirectory. + * + * @param useSimpleFSDirectory whether to use SimpleFSDirectory + * or automatically pick an implementation based + * on the current platform. + */ + public void setUseSimpleFSDirectory(boolean useSimpleFSDirectory) { + this.useSimpleFSDirectory = useSimpleFSDirectory; + } + + /** + * @return true if the {@link DirectoryManager} should use + * the SimpleFSDirectory. + */ + public boolean isUseSimpleFSDirectory() { + return useSimpleFSDirectory; + } + + /** + * @return the current value for termInfosIndexDivisor. + */ + public int getTermInfosIndexDivisor() { + return termInfosIndexDivisor; + } + + /** + * Sets a new value for termInfosIndexDivisor. + * + * @param termInfosIndexDivisor the new value. + */ + public void setTermInfosIndexDivisor(int termInfosIndexDivisor) { + this.termInfosIndexDivisor = termInfosIndexDivisor; + } + + /** + * @return true if the hierarchy cache should be initialized + * immediately on startup. + */ + public boolean isInitializeHierarchyCache() { + return initializeHierarchyCache; + } + + /** + * Whether the hierarchy cache should be initialized immediately on + * startup. + * + * @param initializeHierarchyCache true if the cache should be + * initialized immediately. + */ + public void setInitializeHierarchyCache(boolean initializeHierarchyCache) { + this.initializeHierarchyCache = initializeHierarchyCache; + } + + /** + * @return the maximum age in seconds for outdated generations of + * {@link IndexInfos}. + */ + public long getMaxHistoryAge() { + return maxHistoryAge; + } + + /** + * Sets a new value for the maximum age in seconds for outdated generations + * of {@link IndexInfos}. + * + * @param maxHistoryAge age in seconds. + */ + public void setMaxHistoryAge(long maxHistoryAge) { + this.maxHistoryAge = maxHistoryAge; + } + + /** + * @return the name of the redo log factory class. + */ + public String getRedoLogFactoryClass() { + return redoLogFactoryClass; + } + + /** + * Sets the name of the redo log factory class. Must implement + * {@link RedoLogFactory}. + * + * @param className the name of the redo log factory class. + */ + public void setRedoLogFactoryClass(String className) { + this.redoLogFactoryClass = className; + } + + /** + * In the case of an initial index build operation, this checks if there are + * some new nodes pending in the journal and tries to preemptively delete + * them, to keep the index consistent. + * + * See JCR-3162 + * + * @param context + * @throws IOException + */ + private void checkPendingJournalChanges(QueryHandlerContext context) { + ClusterNode cn = context.getClusterNode(); + if (cn == null) { + return; + } + + List addedIds = new ArrayList(); + long rev = cn.getRevision(); + + List changes = getChangeLogRecords(rev, context.getWorkspace()); + Iterator iterator = changes.iterator(); + while (iterator.hasNext()) { + ChangeLogRecord record = iterator.next(); + for (ItemState state : record.getChanges().addedStates()) { + if (!state.isNode()) { + continue; + } + addedIds.add((NodeId) state.getId()); + } + } + if (!addedIds.isEmpty()) { + Collection empty = Collections.emptyList(); + try { + updateNodes(addedIds.iterator(), empty.iterator()); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + } + + //----------------------------< internal >---------------------------------- + + /** + * Checks if this SearchIndex is open, otherwise throws + * an IOException. + * + * @throws IOException if this SearchIndex had been closed. + */ + protected void checkOpen() throws IOException { + if (closed) { + throw new IOException("query handler closed and cannot be used anymore."); + } + } + + /** + * Polls the underlying journal for events of the type ChangeLogRecord that + * happened after a given revision, on a given workspace. + * + * @param revision + * starting revision + * @param workspace + * the workspace name + * @return + */ + private List getChangeLogRecords(long revision, + final String workspace) { + log.debug( + "Get changes from the Journal for revision {} and workspace {}.", + revision, workspace); + ClusterNode cn = getContext().getClusterNode(); + if (cn == null) { + return Collections.emptyList(); + } + Journal journal = cn.getJournal(); + final List events = new ArrayList(); + ClusterRecordDeserializer deserializer = new ClusterRecordDeserializer(); + RecordIterator records = null; + try { + records = journal.getRecords(revision); + while (records.hasNext()) { + Record record = records.nextRecord(); + if (!record.getProducerId().equals(cn.getId())) { + continue; + } + ClusterRecord r = null; + try { + r = deserializer.deserialize(record); + } catch (JournalException e) { + log.error( + "Unable to read revision '" + record.getRevision() + + "'.", e); + } + if (r == null) { + continue; + } + r.process(new ClusterRecordProcessor() { + public void process(ChangeLogRecord record) { + String eventW = record.getWorkspace(); + if (eventW != null ? eventW.equals(workspace) : workspace == null) { + events.add(record); + } + } + + public void process(LockRecord record) { + } + + public void process(NamespaceRecord record) { + } + + public void process(NodeTypeRecord record) { + } + + public void process(PrivilegeRecord record) { + } + + public void process(WorkspaceRecord record) { + } + }); + } + } catch (JournalException e1) { + log.error(e1.getMessage(), e1); + } finally { + if (records != null) { + records.close(); + } + } + return events; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedFieldCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedFieldCache.java new file mode 100644 index 00000000000..e22cbac3b88 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedFieldCache.java @@ -0,0 +1,422 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; + +import javax.jcr.PropertyType; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.index.TermPositions; + +/** + * Implements a variant of the lucene class org.apache.lucene.search.FieldCacheImpl. + * The lucene FieldCache class has some sort of support for custom comparators + * but it only works on the basis of a field name. There is no further control + * over the terms to iterate, that's why we use our own implementation. + */ +public class SharedFieldCache { + + /** + * Expert: Stores term text values and document ordering data. + */ + public static class ValueIndex { + + /** + * Some heuristic factor that determines whether the array is sparse. Note that if less then + * 1% is set, we already count the array as sparse. This is because it will become memory consuming + * quickly by keeping the (sparse) arrays + */ + private static final int SPARSE_FACTOR = 100; + + /** + * Values indexed by document id. + */ + private final Comparable[] values; + + /** + * Values (Comparable) map indexed by document id. + */ + public final Map> valuesMap; + + /** + * Boolean indicating whether the {@link #valuesMap} impl has to be used + */ + public final boolean sparse; + + /** + * Creates one of these objects + */ + public ValueIndex(Comparable[] values, int setValues) { + if (isSparse(values, setValues)) { + this.sparse = true; + this.values = null; + if (setValues == 0) { + this.valuesMap = null; + } else { + this.valuesMap = getValuesMap(values, setValues); + } + } else { + this.sparse = false; + this.values = values; + this.valuesMap = null; + } + } + + public Comparable getValue(int i) { + if (sparse) { + return valuesMap == null ? null : valuesMap.get(i); + } else { + return values[i]; + } + } + + private static Map> getValuesMap(Comparable[] values, int setValues) { + Map> map = new HashMap>(setValues); + for (int i = 0; i < values.length && setValues > 0; i++) { + if (values[i] != null) { + map.put(i, values[i]); + setValues--; + } + } + return map; + } + + private static boolean isSparse(Comparable[] values, int setValues) { + // some really simple test to test whether the array is sparse. Currently, when less then 1% is set, the array is already sparse + // for this typical cache to avoid memory issues + if (setValues * SPARSE_FACTOR < values.length) { + return true; + } + return false; + } + } + + static class ComparableArray implements Comparable { + + private int offset = 0; + + private Comparable[] c = new Comparable[0]; + + public ComparableArray(Comparable item, int index) { + insert(item, index); + } + + public int compareTo(ComparableArray o) { + return Util.compare(c, o.c); + } + + /** + * testing purpose only. + * + * @return the offset + */ + int getOffset() { + return offset; + } + + public ComparableArray insert(Comparable item, int index) { + // optimize for most common scenario + if (c.length == 0) { + offset = index; + c = new Comparable[] { item }; + return this; + } + + // inside + if (index >= offset && index < offset + c.length) { + c[index - offset] = item; + return this; + } + + // before + if (index < offset) { + int relativeOffset = offset - index; + Comparable[] newC = new Comparable[relativeOffset + c.length]; + newC[0] = item; + System.arraycopy(c, 0, newC, relativeOffset, c.length); + c = newC; + offset = index; + return this; + } + + // after + if (index >= offset + c.length) { + Comparable[] newC = new Comparable[index - offset + 1]; + System.arraycopy(c, 0, newC, 0, c.length); + newC[index - offset] = item; + c = newC; + return this; + } + return this; + } + + /* + * This is needed by {@link UpperCaseSortComparator} and {@link LowerCaseSortComparator} + */ + @Override + public String toString() { + if (c == null) { + return null; + } + if (c.length == 1) { + return c[0].toString(); + } + return Arrays.toString(c); + } + } + + /** + * Reference to the single instance of SharedFieldCache. + */ + public static final SharedFieldCache INSTANCE = new SharedFieldCache(); + + /** + * The internal cache. Maps Entry to array of interpreted term values. + */ + private final Map> cache = new WeakHashMap>(); + + /** + * Private constructor. + */ + private SharedFieldCache() { + } + + /** + * Creates a ValueIndex for a field and a term + * prefix. The term prefix acts as the property name for the + * shared field. + *

    + * This method is an adapted version of: FieldCacheImpl.getStringIndex() + * + * @param reader the IndexReader. + * @param field name of the shared field. + * @param prefix the property name, will be used as term prefix. + * @return a ValueIndex that contains the field values and order + * information. + * @throws IOException if an error occurs while reading from the index. + */ + public ValueIndex getValueIndex(IndexReader reader, String field, + String prefix) throws IOException { + + if (reader instanceof ReadOnlyIndexReader) { + reader = ((ReadOnlyIndexReader) reader).getBase(); + } + + field = field.intern(); + ValueIndex ret = lookup(reader, field, prefix); + if (ret == null) { + final int maxDocs = reader.maxDoc(); + Comparable[] retArray = new Comparable[maxDocs]; + Map positions = new HashMap(); + boolean usingSimpleComparable = true; + int setValues = 0; + if (maxDocs > 0) { + IndexFormatVersion version = IndexFormatVersion.getVersion(reader); + boolean hasPayloads = version.isAtLeast(IndexFormatVersion.V3); + TermDocs termDocs; + byte[] payload = null; + int type; + if (hasPayloads) { + termDocs = reader.termPositions(); + payload = new byte[1]; + } else { + termDocs = reader.termDocs(); + } + TermEnum termEnum = reader.terms(new Term(field, prefix)); + try { + if (termEnum.term() == null) { + throw new RuntimeException("no terms in field " + field); + } + do { + Term term = termEnum.term(); + if (term.field() != field || !term.text().startsWith(prefix)) { + break; + } + final String value = termValueAsString(term, prefix); + termDocs.seek(term); + while (termDocs.next()) { + int termPosition = 0; + type = PropertyType.UNDEFINED; + if (hasPayloads) { + TermPositions termPos = (TermPositions) termDocs; + termPosition = termPos.nextPosition(); + if (termPos.isPayloadAvailable()) { + payload = termPos.getPayload(payload, 0); + type = PropertyMetaData.fromByteArray(payload).getPropertyType(); + } + } + setValues++; + Comparable v = getValue(value, type); + int doc = termDocs.doc(); + Comparable ca = retArray[doc]; + if (ca == null) { + if (usingSimpleComparable) { + // put simple value on the queue + positions.put(doc, termPosition); + retArray[doc] = v; + } else { + retArray[doc] = new ComparableArray(v, + termPosition); + } + } else { + if (ca instanceof ComparableArray) { + ((ComparableArray) ca).insert(v, + termPosition); + } else { + // transform all of the existing values from + // Comparable to ComparableArray + for (int pos : positions.keySet()) { + retArray[pos] = new ComparableArray( + retArray[pos], + positions.get(pos)); + } + positions = null; + usingSimpleComparable = false; + ComparableArray caNew = (ComparableArray) retArray[doc]; + retArray[doc] = caNew.insert(v, + termPosition); + } + } + } + } while (termEnum.next()); + } finally { + termDocs.close(); + termEnum.close(); + } + } + ValueIndex value = new ValueIndex(retArray, setValues); + store(reader, field, prefix, value); + return value; + } + return ret; + } + + /** + * Extracts the value from a given Term as a String + * + * @param term + * @param prefix + * @return string value contained in the term + */ + private static String termValueAsString(Term term, String prefix) { + // make sure term is compacted + String text = term.text(); + int length = text.length() - prefix.length(); + char[] tmp = new char[length]; + text.getChars(prefix.length(), text.length(), tmp, 0); + return new String(tmp, 0, length); + } + + /** + * See if a ValueIndex object is in the cache. + */ + ValueIndex lookup(IndexReader reader, String field, String prefix) { + synchronized (cache) { + Map readerCache = cache.get(reader); + if (readerCache == null) { + return null; + } + return readerCache.get(new Key(field, prefix)); + } + } + + /** + * Put a ValueIndex value to cache. + */ + void store(IndexReader reader, String field, String prefix, ValueIndex value) { + synchronized (cache) { + Map readerCache = cache.get(reader); + if (readerCache == null) { + readerCache = new HashMap(); + cache.put(reader, readerCache); + } + readerCache.put(new Key(field, prefix), value); + } + } + + /** + * Returns a comparable for the given value that is read from + * the index. + * + * @param value the value as read from the index. + * @param type the property type. + * @return a comparable for the value. + */ + private Comparable getValue(String value, int type) { + switch (type) { + case PropertyType.BOOLEAN: + return Boolean.valueOf(value); + case PropertyType.DATE: + return DateField.stringToTime(value); + case PropertyType.LONG: + return LongField.stringToLong(value); + case PropertyType.DOUBLE: + return DoubleField.stringToDouble(value); + case PropertyType.DECIMAL: + return DecimalField.stringToDecimal(value); + default: + return value; + } + } + + /** + * A compound Key that consist of field + * and prefix. + */ + static class Key { + + private final String field; + private final String prefix; + + /** + * Creates Key for ValueIndex lookup. + */ + Key(String field, String prefix) { + this.field = field.intern(); + this.prefix = prefix.intern(); + } + + /** + * Returns true if o is a Key + * instance and refers to the same field and prefix. + */ + public boolean equals(Object o) { + if (o instanceof Key) { + Key other = (Key) o; + return other.field == field + && other.prefix == prefix; + } + return false; + } + + /** + * Composes a hashcode based on the field and prefix. + */ + public int hashCode() { + return field.hashCode() ^ prefix.hashCode(); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedFieldComparatorSource.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedFieldComparatorSource.java new file mode 100644 index 00000000000..be69823a6c9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedFieldComparatorSource.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.query.lucene.sort.AbstractFieldComparator; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.FieldComparatorSource; + +/** + * Implements a FieldComparatorSource for FieldComparators which + * know how to sort on a lucene field that contains values for multiple properties. + */ +public class SharedFieldComparatorSource extends FieldComparatorSource { + + private static final long serialVersionUID = -5803240954874585429L; + + /** + * The name of the shared field in the lucene index. + */ + private final String field; + + /** + * The item state manager. + */ + private final ItemStateManager ism; + + /** + * The hierarchy manager on top of {@link #ism}. + */ + private final HierarchyManager hmgr; + + /** + * The index internal namespace mappings. + */ + private final NamespaceMappings nsMappings; + + /** + * Create a new SharedFieldComparatorSource for a given shared field. + * + * @param fieldname the shared field. + * @param ism the item state manager of this workspace. + * @param hmgr the hierarchy manager of this workspace. + * @param nsMappings the index internal namespace mappings. + */ + public SharedFieldComparatorSource(String fieldname, ItemStateManager ism, + HierarchyManager hmgr, NamespaceMappings nsMappings) { + this.field = fieldname; + this.ism = ism; + this.hmgr = hmgr; + this.nsMappings = nsMappings; + } + + /** + * Create a new FieldComparator for an embedded propertyName + * and a reader. + * + * @param propertyName the relative path to the property to sort on as returned + * by {@link org.apache.jackrabbit.spi.Path#getString()}. + * @return a FieldComparator + * @throws java.io.IOException if an error occurs + */ + @Override + public FieldComparator newComparator(String propertyName, int numHits, int sortPos, + boolean reversed) throws IOException { + + PathFactory factory = PathFactoryImpl.getInstance(); + Path path = factory.create(propertyName); + + try { + SimpleFieldComparator simple = new SimpleFieldComparator(nsMappings.translatePath(path), field, numHits); + + return path.getLength() == 1 + ? simple + : new CompoundScoreFieldComparator( + new FieldComparator[] { simple, new RelPathFieldComparator(path, numHits) }, numHits); + + } + catch (IllegalNameException e) { + throw Util.createIOException(e); + } + } + + /** + * A FieldComparator which works for order by clauses with properties + * directly on the result nodes. + */ + static final class SimpleFieldComparator extends AbstractFieldComparator { + + /** + * The term look ups of the index segments. + */ + protected SharedFieldCache.ValueIndex[] indexes; + + /** + * The name of the property + */ + private final String propertyName; + + /** + * The name of the field in the index + */ + private final String fieldName; + + /** + * Create a new instance of the FieldComparator. + * + * @param propertyName the name of the property + * @param fieldName the name of the field in the index + * @param numHits the number of values + */ + public SimpleFieldComparator(String propertyName, String fieldName, int numHits) { + super(numHits); + this.propertyName = propertyName; + this.fieldName = fieldName; + } + + @Override + public void setNextReader(IndexReader reader, int docBase) throws IOException { + super.setNextReader(reader, docBase); + + indexes = new SharedFieldCache.ValueIndex[readers.size()]; + + String namedValue = FieldNames.createNamedValue(propertyName, ""); + for (int i = 0; i < readers.size(); i++) { + IndexReader r = readers.get(i); + indexes[i] = SharedFieldCache.INSTANCE.getValueIndex(r, + fieldName, namedValue); + } + } + + @Override + protected Comparable sortValue(int doc) { + int idx = readerIndex(doc); + return indexes[idx].getValue(doc - starts[idx]); + } + + } + + /** + * A FieldComparator which works with order by clauses that use a + * relative path to a property to sort on. + */ + private final class RelPathFieldComparator extends AbstractFieldComparator { + + /** + * Relative path to the property + */ + private final Path propertyName; + + /** + * Create a new instance of the FieldComparator. + * + * @param propertyName relative path of the property + * @param numHits the number of values + */ + public RelPathFieldComparator(Path propertyName, int numHits) { + super(numHits); + this.propertyName = propertyName; + } + + @Override + protected Comparable sortValue(int doc) { + try { + final String uuid = getUUIDForIndex(doc); + + Path path = hmgr.getPath(new NodeId(uuid)); + PathBuilder builder = new PathBuilder(path); + builder.addAll(propertyName.getElements()); + PropertyId id = hmgr.resolvePropertyPath(builder.getPath()); + + if (id == null) { + return null; + } + + PropertyState state = (PropertyState) ism.getItemState(id); + if (state == null) { + return null; + } + + InternalValue[] values = state.getValues(); + if (values.length > 0) { + return Util.getComparable(values[0]); + } + } + catch (Exception ignore) { } + + return null; + } + + } + + /** + * Implements a compound FieldComparator which delegates to several + * other comparators. The comparators are asked for a sort value in the + * sequence they are passed to the constructor. + */ + private static final class CompoundScoreFieldComparator extends AbstractFieldComparator { + private final FieldComparator[] fieldComparators; + + /** + * Create a new instance of the FieldComparator. + * + * @param fieldComparators delegatees + * @param numHits the number of values + */ + public CompoundScoreFieldComparator(FieldComparator[] fieldComparators, int numHits) { + super(numHits); + this.fieldComparators = fieldComparators; + } + + @Override + public Comparable sortValue(int doc) { + for (FieldComparator fieldComparator : fieldComparators) { + if (fieldComparator instanceof FieldComparatorBase) { + Comparable c = ((FieldComparatorBase) fieldComparator).sortValue(doc); + + if (c != null) { + return c; + } + } + } + return null; + } + + @Override + public void setNextReader(IndexReader reader, int docBase) throws IOException { + for (FieldComparator fieldComparator : fieldComparators) { + fieldComparator.setNextReader(reader, docBase); + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedIndexReader.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedIndexReader.java new file mode 100644 index 00000000000..73b04f1ae82 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SharedIndexReader.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.Term; + +import java.util.BitSet; +import java.io.IOException; + +/** + * Implements an IndexReader, that will close when all connected + * clients are disconnected AND the SharedIndexReaders + * close() method itself has been called. + */ +class SharedIndexReader extends RefCountingIndexReader { + + /** + * Creates a new SharedIndexReader which is based on + * in. + * @param in the underlying IndexReader. + */ + public SharedIndexReader(CachingIndexReader in) { + super(in); + } + + /** + * Returns the tick value when the underlying {@link CachingIndexReader} was + * created. + * + * @return the creation tick for the underlying reader. + */ + long getCreationTick() { + return getBase().getCreationTick(); + } + + /** + * Returns the DocId of the parent of n or + * {@link DocId#NULL} if n does not have a parent + * (n is the root node). + * + * @param n the document number. + * @param deleted the documents that should be regarded as deleted. + * @return the DocId of n's parent. + * @throws IOException if an error occurs while reading from the index. + */ + public DocId getParent(int n, BitSet deleted) throws IOException { + return getBase().getParent(n, deleted); + } + + /** + * Simply passes the call to the wrapped reader as is.
    + * If term is for a {@link FieldNames#UUID} field and this + * SharedIndexReader does not have such a document, + * {@link EmptyTermDocs#INSTANCE} is returned. + * + * @param term the term to enumerate the docs for. + * @return TermDocs for term. + * @throws IOException if an error occurs while reading from the index. + */ + public TermDocs termDocs(Term term) throws IOException { + return in.termDocs(term); + } + + /** + * Returns the {@link CachingIndexReader} this reader is based on. + * + * @return the {@link CachingIndexReader} this reader is based on. + */ + public CachingIndexReader getBase() { + return (CachingIndexReader) in; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SimilarityQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SimilarityQuery.java new file mode 100644 index 00000000000..d6b6374e1ba --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SimilarityQuery.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Query; + +/** + * SimilarityQuery implements a query that returns similar nodes + * for a given node UUID. + */ +@SuppressWarnings("serial") +public class SimilarityQuery extends Query { + + /** + * The UUID of the node for which to find similar nodes. + */ + private final String uuid; + + /** + * The analyzer in use. + */ + private final Analyzer analyzer; + + public SimilarityQuery(String uuid, Analyzer analyzer) { + this.uuid = uuid; + this.analyzer = analyzer; + } + + /** + * {@inheritDoc} + */ + public Query rewrite(IndexReader reader) throws IOException { + MoreLikeThis more = new MoreLikeThis(reader); + more.setAnalyzer(analyzer); + more.setFieldNames(new String[]{FieldNames.FULLTEXT}); + more.setMinWordLen(4); + Query similarityQuery = null; + TermDocs td = reader.termDocs(TermFactory.createUUIDTerm(uuid)); + try { + if (td.next()) { + similarityQuery = more.like(td.doc()); + } + } finally { + td.close(); + } + if (similarityQuery != null) { + return similarityQuery.rewrite(reader); + } else { + // return dummy query that never matches + return new BooleanQuery(); + } + } + + /** + * {@inheritDoc} + */ + public String toString(String field) { + return "rep:similar(" + uuid + ")"; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SimpleExcerptProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SimpleExcerptProvider.java new file mode 100644 index 00000000000..06ff2e29211 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SimpleExcerptProvider.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.core.query.lucene; + +import org.apache.lucene.search.Query; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.spi.Name; + +import javax.jcr.PropertyType; +import java.io.IOException; +import java.util.Iterator; + +/** + * SimpleExcerptProvider is a very simple excerpt provider. + * It does not do any highlighting and simply returns up to + * maxFragmentSize characters of string properties for a given + * node. + * @see #getExcerpt(org.apache.jackrabbit.core.NodeId, int, int) + */ +public class SimpleExcerptProvider implements ExcerptProvider { + + /** + * The item state manager. + */ + private ItemStateManager ism; + + /** + * {@inheritDoc} + */ + public void init(Query query, SearchIndex index) throws IOException { + ism = index.getContext().getItemStateManager(); + } + + /** + * {@inheritDoc} + */ + public String getExcerpt(NodeId id, int maxFragments, int maxFragmentSize) + throws IOException { + StringBuffer text = new StringBuffer(); + try { + NodeState nodeState = (NodeState) ism.getItemState(id); + String separator = ""; + Iterator it = nodeState.getPropertyNames().iterator(); + while (it.hasNext() && text.length() < maxFragmentSize) { + PropertyId propId = new PropertyId(id, it.next()); + PropertyState propState = (PropertyState) ism.getItemState(propId); + if (propState.getType() == PropertyType.STRING) { + text.append(separator); + separator = " ... "; + InternalValue[] values = propState.getValues(); + for (InternalValue value : values) { + text.append(value.toString()); + } + } + } + } catch (ItemStateException e) { + // ignore + } + if (text.length() > maxFragmentSize) { + int lastSpace = text.lastIndexOf(" ", maxFragmentSize); + if (lastSpace != -1) { + text.setLength(lastSpace); + } else { + text.setLength(maxFragmentSize); + } + text.append(" ..."); + } + return "" + text.toString() + ""; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingleColumnQueryResult.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingleColumnQueryResult.java new file mode 100644 index 00000000000..e265421857e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingleColumnQueryResult.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.core.query.lucene; + +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.query.qom.ColumnImpl; +import org.apache.lucene.search.Query; + +import javax.jcr.RepositoryException; +import java.io.IOException; + +/** + * SingleColumnQueryResult implements a query result that returns + * a single column. That is, executes a lucene query. + */ +public class SingleColumnQueryResult extends QueryResultImpl { + + /** + * The query to execute. + */ + private final Query query; + + /** + * The relative paths of properties to use for ordering the result set. + */ + protected final Path[] orderProps; + + /** + * The order specifier for each of the order properties. + */ + protected final boolean[] orderSpecs; + + /** + * Function for each of the order properties (or null if none). + */ + private String[] orderFuncs; + + public SingleColumnQueryResult( + SearchIndex index, SessionContext sessionContext, + AbstractQueryImpl queryImpl, Query query, + SpellSuggestion spellSuggestion, ColumnImpl[] columns, + Path[] orderProps, boolean[] orderSpecs, String[] orderFuncs, boolean documentOrder, + long offset, long limit) throws RepositoryException { + super(index, sessionContext, queryImpl, spellSuggestion, + columns, documentOrder, offset, limit); + this.query = query; + this.orderProps = orderProps; + this.orderSpecs = orderSpecs; + this.orderFuncs = orderFuncs; + // if document order is requested get all results right away + getResults(docOrder ? Integer.MAX_VALUE : index.getResultFetchSize()); + } + + /** + * {@inheritDoc} + */ + protected MultiColumnQueryHits executeQuery(long resultFetchHint) + throws IOException { + return index.executeQuery( + sessionContext.getSessionImpl(), queryImpl, query, + orderProps, orderSpecs, orderFuncs, resultFetchHint); + } + + /** + * {@inheritDoc} + */ + protected ExcerptProvider createExcerptProvider() throws IOException { + return index.createExcerptProvider(query); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingleTermDocs.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingleTermDocs.java new file mode 100644 index 00000000000..e392b9e0e69 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingleTermDocs.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.core.query.lucene; + +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; + +import java.io.IOException; + +/** + * Implements a TermDocs with a single document. + */ +class SingleTermDocs implements TermDocs { + + /** + * Single document number; + */ + private final int doc; + + /** + * Flag to return the document number once. + */ + private boolean next = true; + + /** + * Creates a SingleTermDocs that returns doc as + * its single document. + * + * @param doc the document number. + */ + SingleTermDocs(int doc) { + this.doc = doc; + } + + /** + * @throws UnsupportedOperationException always + */ + public void seek(Term term) { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always + */ + public void seek(TermEnum termEnum) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int doc() { + return doc; + } + + /** + * {@inheritDoc} + */ + public int freq() { + return 1; + } + + /** + * {@inheritDoc} + */ + public boolean next() throws IOException { + boolean hasNext = next; + next = false; + return hasNext; + } + + /** + * {@inheritDoc} + */ + public int read(int[] docs, int[] freqs) throws IOException { + if (next && docs.length > 0) { + docs[0] = doc; + freqs[0] = 1; + next = false; + return 1; + } + return 0; + } + + /** + * {@inheritDoc} + */ + public boolean skipTo(int target) throws IOException { + return false; + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingletonTokenStream.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingletonTokenStream.java new file mode 100644 index 00000000000..bdffa37f197 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SingletonTokenStream.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.PayloadAttribute; +import org.apache.lucene.analysis.tokenattributes.TermAttribute; +import org.apache.lucene.index.Payload; + +/** + * SingletonTokenStream implements a token stream that wraps a + * single value with a given property type. The property type is stored as a + * payload on the single returned token. + */ +public final class SingletonTokenStream extends TokenStream { + + /** + * The string value of the token. + */ + private String value; + + /** + * The payload of the token. + */ + private Payload payload; + + /** + * The term attribute of the current token + */ + private TermAttribute termAttribute; + + /** + * The payload attribute of the current token + */ + private PayloadAttribute payloadAttribute; + + private boolean consumed = false; + + /** + * Creates a new SingleTokenStream with the given value and payload. + * + * @param value + * the string value that will be returned with the token. + * @param payload + * the payload that will be attached to this token + */ + public SingletonTokenStream(String value, Payload payload) { + this.value = value; + this.payload = payload; + termAttribute = (TermAttribute) addAttribute(TermAttribute.class); + payloadAttribute = (PayloadAttribute) addAttribute(PayloadAttribute.class); + } + + /** + * Creates a new SingleTokenStream with the given value and a property + * type. + * + * @param value + * the string value that will be returned with the token. + * @param type + * the JCR property type. + */ + public SingletonTokenStream(String value, int type) { + this(value, new Payload(new PropertyMetaData(type).toByteArray())); + } + + @Override + public boolean incrementToken() throws IOException { + if (consumed) { + return false; + } + clearAttributes(); + termAttribute.setTermBuffer(value); + payloadAttribute.setPayload(payload); + consumed = true; + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public void reset() throws IOException { + consumed = false; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + consumed = true; + value = null; + payload = null; + payloadAttribute = null; + termAttribute = null; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SortedLuceneQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SortedLuceneQueryHits.java new file mode 100644 index 00000000000..f0f8aa517a7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SortedLuceneQueryHits.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.TopFieldCollector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps a lucene query result and adds a close method that allows to release + * resources after a query has been executed and the results have been read + * completely. + */ +public final class SortedLuceneQueryHits extends AbstractQueryHits { + + /** + * The Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(SortedLuceneQueryHits.class); + + /** + * The upper limit for the initial fetch size. + */ + private static final int MAX_FETCH_SIZE = 32 * 1024; + + /** + * The lower limit for the initial fetch size. + */ + private static final int MIN_FETCH_SIZE = 32; + + /** + * The index searcher. + */ + private final IndexSearcher searcher; + + /** + * The query to execute. + */ + private final Query query; + + /** + * The sort criteria. + */ + private final Sort sort; + + /** + * The index of the current hit. Initially invalid. + */ + private int hitIndex = -1; + + /** + * The score docs. + */ + private ScoreDoc[] scoreDocs = new ScoreDoc[0]; + + /** + * The total number of hits. + */ + private int size; + + /** + * Number of hits to be pre-fetched from the lucene index (will be around 2x + * resultFetchHint). + */ + private int numHits; + + private int offset = 0; + + /** + * Creates a new QueryHits instance wrapping hits. + * + * @param searcher + * the index searcher. + * @param query + * the query to execute. + * @param sort + * the sort criteria. + * @param resultFetchHint + * a hint on how many results should be pre-fetched from the + * lucene index. + * @throws IOException + * if an error occurs while reading from the index. + */ + public SortedLuceneQueryHits(IndexSearcher searcher, Query query, + Sort sort, long resultFetchHint) throws IOException { + this.searcher = searcher; + this.query = query; + this.sort = sort; + this.numHits = (int) Math.min( + Math.max(resultFetchHint, MIN_FETCH_SIZE), + MAX_FETCH_SIZE); + getHits(); + } + + /** + * {@inheritDoc} + */ + public int getSize() { + return size; + } + + /** + * {@inheritDoc} + */ + public ScoreNode nextScoreNode() throws IOException { + if (++hitIndex >= size) { + // no more score nodes + return null; + } else if (hitIndex - offset >= scoreDocs.length) { + // refill at least numHits or twice hitIndex + this.numHits = Math.max(this.numHits, hitIndex * 2); + getHits(); + } + ScoreDoc doc = scoreDocs[hitIndex - offset]; + String uuid = searcher.doc(doc.doc, + FieldSelectors.UUID).get(FieldNames.UUID); + NodeId id = new NodeId(uuid); + return new ScoreNode(id, doc.score, doc.doc); + } + + /** + * Skips n hits. + * + * @param n the number of hits to skip. + * @throws IOException if an error occurs while skipping. + */ + public void skip(int n) throws IOException { + hitIndex += n; + } + + //-------------------------------< internal >------------------------------- + + private void getHits() throws IOException { + long time = System.nanoTime(); + TopFieldCollector collector = TopFieldCollector.create(sort, numHits, false, true, false, false); + searcher.search(query, collector); + size = collector.getTotalHits(); + offset += scoreDocs.length; + scoreDocs = collector.topDocs(offset, numHits).scoreDocs; + time = System.nanoTime() - time; + final long timeMs = time / 1000000; + log.debug("getHits() in {} ms. {}/{}/{}", new Object[] {timeMs, scoreDocs.length, numHits, size}); + // double hits for next round + numHits *= 2; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SortedMultiColumnQueryHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SortedMultiColumnQueryHits.java new file mode 100644 index 00000000000..52f27da7de2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SortedMultiColumnQueryHits.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.core.query.lucene; + +import org.apache.jackrabbit.spi.Name; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.SortField; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +/** + * SortedMultiColumnQueryHits implements sorting of query hits + * based on {@link Ordering}s. + */ +public class SortedMultiColumnQueryHits extends FilterMultiColumnQueryHits { + + /** + * Iterator over sorted ScoreNode[]s. + */ + private final Iterator it; + + /** + * Creates sorted query hits. + * + * @param hits the hits to sort. + * @param orderings the ordering specifications. + * @param reader the current index reader. + * @throws IOException if an error occurs while reading from the index. + */ + public SortedMultiColumnQueryHits(MultiColumnQueryHits hits, + Ordering[] orderings, + IndexReader reader) + throws IOException { + super(hits); + List sortedHits = new ArrayList(); + ScoreNode[] next; + while ((next = hits.nextScoreNodes()) != null) { + sortedHits.add(next); + } + try { + Collections.sort(sortedHits, new ScoreNodeComparator( + reader, orderings, hits.getSelectorNames(), sortedHits.size())); + } catch (RuntimeException e) { + // might be thrown by ScoreNodeComparator#compare + throw Util.createIOException(e); + } + this.it = sortedHits.iterator(); + } + + /** + * {@inheritDoc} + */ + public ScoreNode[] nextScoreNodes() throws IOException { + if (it.hasNext()) { + return it.next(); + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + public void skip(int n) throws IOException { + while (n-- > 0) { + nextScoreNodes(); + } + } + + /** + * A comparator that compares ScoreNode[]. + */ + private static final class ScoreNodeComparator + implements Comparator { + + /** + * The current index reader. + */ + private final IndexReader reader; + + /** + * The ordering specifications. + */ + private final Ordering[] orderings; + + /** + * The selector name index for each of the {@link #orderings}. + */ + private final int[] idx; + + /** + * The score doc comparator for each of the {@link #orderings}. + */ + private final ScoreDocComparator[] comparators; + + /** + * The reverse flag for each of the {@link #orderings}. + */ + private final boolean[] isReverse; + + /** + * Reusable ScoreDoc for use in {@link #compare(ScoreNode[], ScoreNode[])}. + */ + private final ScoreDoc doc1 = new ScoreDoc(0, 1.0f); + + /** + * Reusable ScoreDoc for use in {@link #compare(ScoreNode[], ScoreNode[])}. + */ + private final ScoreDoc doc2 = new ScoreDoc(0, 1.0f); + + /** + * Creates a new comparator. + * + * @param reader the current index reader. + * @param orderings the ordering specifications. + * @param selectorNames the selector names associated with the + * ScoreNode[] used in + * {@link #compare(ScoreNode[], ScoreNode[])}. + * @throws IOException if an error occurs while reading from the index. + */ + private ScoreNodeComparator(IndexReader reader, + Ordering[] orderings, + Name[] selectorNames, + int numHits) + throws IOException { + this.reader = reader; + this.orderings = orderings; + List names = Arrays.asList(selectorNames); + this.idx = new int[orderings.length]; + this.comparators = new ScoreDocComparator[orderings.length]; + this.isReverse = new boolean[orderings.length]; + for (int i = 0; i < orderings.length; i++) { + idx[i] = names.indexOf(orderings[i].getSelectorName()); + SortField sf = orderings[i].getSortField(); + if (sf.getComparatorSource() != null) { + FieldComparator c = sf.getComparatorSource().newComparator(sf.getField(), numHits, 0, false); + assert c instanceof FieldComparatorBase; + comparators[i] = new ScoreDocComparator((FieldComparatorBase) c); + comparators[i].setNextReader(reader, 0); + } + isReverse[i] = sf.getReverse(); + } + } + + /** + * {@inheritDoc} + */ + public int compare(ScoreNode[] sn1, ScoreNode[] sn2) { + for (int i = 0; i < orderings.length; i++) { + int c; + int scoreNodeIndex = idx[i]; + ScoreNode n1 = sn1[scoreNodeIndex]; + ScoreNode n2 = sn2[scoreNodeIndex]; + if (n1 == n2) { + continue; + } else if (n1 == null) { + c = -1; + } else if (n2 == null) { + c = 1; + } else if (comparators[i] != null) { + try { + doc1.doc = n1.getDoc(reader); + doc1.score = n1.getScore(); + doc2.doc = n2.getDoc(reader); + doc2.score = n2.getScore(); + } catch (IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + c = comparators[i].compareDocs(doc1.doc, doc2.doc); + } else { + // compare score + c = new Float(n1.getScore()).compareTo(n2.getScore()); + } + if (c != 0) { + if (isReverse[i]) { + c = -c; + } + return c; + } + } + return 0; + } + + } + + private static final class ScoreDocComparator extends FieldComparatorDecorator { + + public ScoreDocComparator(FieldComparatorBase base) { + super(base); + } + + public int compareDocs(int doc1, int doc2) { + return compare(sortValue(doc1), sortValue(doc2)); + } + + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SpellChecker.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SpellChecker.java new file mode 100644 index 00000000000..d6ff178c049 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SpellChecker.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.core.query.lucene; + +import org.apache.jackrabbit.core.query.QueryHandler; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; + +import java.io.IOException; + +/** + * SpellChecker defines an interface to run a spellchecker over + * a fulltext query statement. + */ +public interface SpellChecker { + + /** + * Initializes this spell checker with an abstract query tree. + * + * @param handler the query handler that created this spell checker. + * @throws IOException if an error occurs while initializing the spell + * checker. + */ + void init(QueryHandler handler) throws IOException; + + /** + * Runs the spell checker over the first spellcheck relation query node in + * the abstract query tree and returns a suggestion in case this + * spellchecker thinks the words are misspelled. If the spellchecker + * determines that the words are spelled correctly null is + * returned. + * + * @param aqt the abstract query tree, which may contain a relation query + * node with a spellcheck operation. + * @return a suggestion or null if this spell checker + * determines that the fulltext query statement is spelled + * correctly. + */ + String check(QueryRootNode aqt) throws IOException; + + /** + * Closes this spell checker and allows it to free resources. + */ + void close(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SpellSuggestion.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SpellSuggestion.java new file mode 100644 index 00000000000..5c13db116ad --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SpellSuggestion.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; + +/** + * SpellSuggestion implements a spell suggestion, which uses the + * spell checker. + */ +class SpellSuggestion { + + /** + * The spell checker. + */ + private final SpellChecker spellChecker; + + /** + * The abstract query tree. + */ + private final QueryRootNode root; + + /** + * Creates a new spell suggestion. + * + * @param spellChecker the spell checker or null if none is + * available. + * @param root the abstract query tree. + */ + SpellSuggestion(SpellChecker spellChecker, QueryRootNode root) { + this.spellChecker = spellChecker; + this.root = root; + } + + /** + * @return a suggestion for the spellcheck query node in the abstract query + * tree passed in the constructor of this SpellSuggestion. + * This method returns null if the spell checker thinks + * the spelling is correct or no spell checker was provided. + * @throws IOException if an error occurs while checking the spelling. + */ + public String getSuggestion() throws IOException { + if (spellChecker != null) { + return spellChecker.check(root); + } else { + return null; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SynonymProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SynonymProvider.java new file mode 100644 index 00000000000..12cd2464d70 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/SynonymProvider.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.core.query.lucene; + +import org.apache.jackrabbit.core.fs.FileSystemResource; + +import java.io.IOException; + +/** + * SynonymProvider defines an interface for a component that + * returns synonyms for a given term. + */ +public interface SynonymProvider { + + /** + * Initializes the synonym provider and passes the file system resource to + * the synonym provider configuration defined by the configuration value of + * the synonymProviderConfigPath parameter. The resource may be + * null if the configuration parameter is not set. + * + * @param fsr the file system resource to the synonym provider + * configuration. + * @throws IOException if an error occurs while initializing the synonym + * provider. + */ + void initialize(FileSystemResource fsr) throws IOException; + + /** + * Returns an array of terms that are considered synonyms for the given + * term. + * + * @param term a search term. + * @return an array of synonyms for the given term or an empty + * array if no synonyms are known. + */ + String[] getSynonyms(String term); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TermDocsCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TermDocsCache.java new file mode 100644 index 00000000000..153e8e95ccb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TermDocsCache.java @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.util.Map; +import java.util.Collections; +import java.util.BitSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TermDocsCache implements a cache for frequently read + * {@link TermDocs}. + */ +public class TermDocsCache { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(TermDocsCache.class); + + /** + * The default cache size. + */ + private static final int CACHE_SIZE = 10; + + /** + * The underlying index reader. + */ + private final IndexReader reader; + + /** + * Only TermDocs for the given field are cached. + */ + private final String field; + + /** + * Map of {@link Term#text()} that are unknown to the underlying index. + */ + private final Map unknownValues = Collections.synchronizedMap(new LinkedHashMap() { + private static final long serialVersionUID = 1443679637070403838L; + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 100; + } + }); + + /** + * The cache of the {@link #CACHE_SIZE} most frequently requested TermDocs. + * Maps term text String to {@link CacheEntry}. + */ + private final Map cache = new LinkedHashMap(); + + /** + * Creates a new cache for the given reader and + * field. + * + * @param reader the index reader. + * @param field the field name of the terms to potentially cache. + */ + public TermDocsCache(IndexReader reader, String field) { + this.reader = reader; + this.field = field; + } + + /** + * Returns the {@link TermDocs} for the given term. + * + * @param t the term. + * @return the term docs for the given term. + * @throws IOException if an error occurs while reading from the index. + */ + public TermDocs termDocs(final Term t) throws IOException { + if (t == null || t.field() != field) { + return reader.termDocs(t); + } + + String text = t.text(); + if (unknownValues.get(text) != null) { + log.debug("EmptyTermDocs({},{})", field, text); + return EmptyTermDocs.INSTANCE; + } + + // maintain cache + CacheEntry entry; + synchronized (cache) { + entry = cache.get(text); + if (entry == null) { + // check space + if (cache.size() >= CACHE_SIZE) { + // prune half of them and adjust the rest + CacheEntry[] entries = cache.values().toArray( + new CacheEntry[cache.size()]); + Arrays.sort(entries); + int threshold = entries[CACHE_SIZE / 2].numAccessed; + for (Iterator> it = cache.entrySet().iterator(); it.hasNext(); ) { + Map.Entry e = it.next(); + if (e.getValue().numAccessed <= threshold) { + // prune + it.remove(); + } else { + // adjust + CacheEntry ce = e.getValue(); + ce.numAccessed = (int) Math.sqrt(ce.numAccessed); + } + } + } + entry = new CacheEntry(); + cache.put(text, entry); + } else { + entry.numAccessed++; + } + } + + // this is a threshold to prevent caching of TermDocs + // that are read only irregularly. + if (entry.numAccessed < 10) { + if (log.isDebugEnabled()) { + log.debug("#{} TermDocs({},{})", + new Object[]{entry.numAccessed, field, text}); + } + return reader.termDocs(t); + } + + if (entry.bits == null) { + // collect bits + BitSet bits = null; + TermDocs tDocs = reader.termDocs(t); + try { + while (tDocs.next()) { + if (bits == null) { + bits = new BitSet(reader.maxDoc()); + } + bits.set(tDocs.doc()); + } + } finally { + tDocs.close(); + } + if (bits != null) { + entry.bits = bits; + } + } + + if (entry.bits == null) { + // none collected + unknownValues.put(text, text); + return EmptyTermDocs.INSTANCE; + } else { + if (log.isDebugEnabled()) { + log.debug("CachedTermDocs({},{},{}/{})", new Object[]{ + field, text, entry.bits.cardinality(), reader.maxDoc()}); + } + return new CachedTermDocs(entry.bits); + } + } + + /** + * Implements a {@link TermDocs} base on a {@link BitSet}. + */ + private static final class CachedTermDocs implements TermDocs { + + /** + * The cached docs for this term. + */ + private final BitSet docs; + + /** + * The current position into the {@link #docs}. + */ + private int position = -1; + + /** + * true if there are potentially more docs. + */ + private boolean moreDocs = true; + + public CachedTermDocs(BitSet docs) { + this.docs = docs; + } + + /** + * @throws UnsupportedOperationException always. + */ + public void seek(Term term) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public void seek(TermEnum termEnum) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int doc() { + return position; + } + + /** + * {@inheritDoc} + */ + public int freq() { + return 1; + } + + /** + * {@inheritDoc} + */ + public boolean next() throws IOException { + if (moreDocs) { + position = docs.nextSetBit(position + 1); + moreDocs = position != -1; + } + return moreDocs; + } + + /** + * {@inheritDoc} + */ + public int read(int[] docs, int[] freqs) throws IOException { + int count; + for (count = 0; count < docs.length && next(); count++) { + docs[count] = doc(); + freqs[count] = 1; + } + return count; + } + + /** + * {@inheritDoc} + */ + public boolean skipTo(int target) throws IOException { + if (moreDocs) { + position = docs.nextSetBit(target); + moreDocs = position != -1; + } + return moreDocs; + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + } + } + + private static final class CacheEntry implements Comparable { + + private volatile int numAccessed = 1; + + private volatile BitSet bits; + + public int compareTo(CacheEntry other) { + return (numAccessed < other.numAccessed ? -1 : (numAccessed == other.numAccessed ? 0 : 1)); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TermFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TermFactory.java new file mode 100644 index 00000000000..645a3c58e55 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TermFactory.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.core.query.lucene; + +import org.apache.lucene.index.Term; + +/** + * TermFactory is a factory for Term instances with + * frequently used field names. + */ +public final class TermFactory { + + /** + * Template for UUID terms. + */ + private static final Term UUID_TERM_TEMPLATE = new Term(FieldNames.UUID); + + /** + * Creates a Term with the given id value and with a field + * name {@link FieldNames#UUID}. + * + * @param id the id. + * @return the UUIDTerm. + */ + public static Term createUUIDTerm(String id) { + return UUID_TERM_TEMPLATE.createTerm(id); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.java new file mode 100644 index 00000000000..32fe15d7fef --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/TransformConstants.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.core.query.lucene; + +/** + * TransformConstants defines constants for query processing. + */ +public interface TransformConstants { + + /** + * No transformation is done on the term enum. + */ + int TRANSFORM_NONE = 0; + + /** + * The underlying term enum is transformed to lower case characters. + */ + int TRANSFORM_LOWER_CASE = 1; + + /** + * The underlying term enum is transformed to upper case characters. + */ + int TRANSFORM_UPPER_CASE = 2; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Transformable.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Transformable.java new file mode 100644 index 00000000000..5e73cead938 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Transformable.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.core.query.lucene; + +/** + * Transformable marks queries that can transform the value to + * upper- or lower-case. + */ +public interface Transformable extends TransformConstants { + + /** + * Sets the transformation. Must be one of the following values: + *

      + *
    • {@link #TRANSFORM_LOWER_CASE}
    • + *
    • {@link #TRANSFORM_NONE}
    • + *
    • {@link #TRANSFORM_UPPER_CASE}
    • + *
    + * @param transformation a transform constant. + */ + void setTransformation(int transformation); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/UpperCaseSortComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/UpperCaseSortComparator.java new file mode 100644 index 00000000000..1317a4f5b16 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/UpperCaseSortComparator.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.core.query.lucene; + +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.FieldComparatorSource; + +import java.io.IOException; + +/** + * UpperCaseSortComparator implements a FieldComparator which + * compares the upper-cased string values of a base sort comparator. + */ +public class UpperCaseSortComparator extends FieldComparatorSource { + + /** + * The base sort comparator. + */ + private final FieldComparatorSource base; + + /** + * Creates a new upper case sort comparator. + * + * @param base the base sort comparator source. + */ + public UpperCaseSortComparator(FieldComparatorSource base) { + this.base = base; + } + + @Override + public FieldComparator newComparator(String fieldname, int numHits, int sortPos, boolean reversed) throws IOException { + FieldComparator comparator = base.newComparator(fieldname, numHits, sortPos, reversed); + assert comparator instanceof FieldComparatorBase; + + return new FieldComparatorDecorator((FieldComparatorBase) comparator) { + @Override + protected Comparable sortValue(int doc) { + Comparable c = super.sortValue(doc); + return c == null ? null : c.toString().toUpperCase(); + } + }; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Util.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Util.java new file mode 100644 index 00000000000..e8fe0bf4907 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/Util.java @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.regex.Pattern; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Fieldable; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Util provides various static utility methods. + */ +public class Util { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(Util.class); + + /** + * Disposes the document old. Closes any potentially open + * readers held by the document. + * + * @param old + * the document to dispose. + */ + public static void disposeDocument(Document old) { + for (Fieldable f : old.getFields()) { + try { + if (f.readerValue() != null) { + f.readerValue().close(); + } else if (f instanceof LazyTextExtractorField) { + LazyTextExtractorField field = (LazyTextExtractorField) f; + field.dispose(); + } + } catch (IOException ex) { + log.warn("Exception while disposing index document: " + ex); + } + } + } + + /** + * Returns true if the document is ready to be added to the + * index. That is all text extractors have finished their work. + * + * @param doc + * the document to check. + * @return true if the document is ready; false + * otherwise. + */ + public static boolean isDocumentReady(Document doc) { + for (Fieldable f : doc.getFields()) { + if (f instanceof LazyTextExtractorField) { + LazyTextExtractorField field = (LazyTextExtractorField) f; + if (!field.isExtractorFinished()) { + return false; + } + } + } + return true; + } + + /** + * Depending on the index format this method returns a query that matches + * all nodes that have a property with a given name. + * + * @param name + * the property name. + * @param version + * the index format version. + * @return Query that matches all nodes that have a property with the given + * name. + */ + public static Query createMatchAllQuery(String name, + IndexFormatVersion version, PerQueryCache cache) { + if (version.getVersion() >= IndexFormatVersion.V2.getVersion()) { + // new index format style + return new JackrabbitTermQuery(new Term(FieldNames.PROPERTIES_SET, + name)); + } else { + return new MatchAllQuery(name, cache); + } + } + + /** + * Creates an {@link IOException} with t as its cause. + * + * @param t + * the cause. + */ + public static IOException createIOException(Throwable t) { + IOException ex = new IOException(t.getMessage()); + ex.initCause(t); + return ex; + } + + /** + * Depending on the type of the reader this method either + * closes or releases the reader. The reader is released if it implements + * {@link ReleaseableIndexReader}. + * + * @param reader + * the index reader to close or release. + * @throws IOException + * if an error occurs while closing or releasing the index + * reader. + */ + public static void closeOrRelease(IndexReader reader) throws IOException { + if (reader instanceof ReleaseableIndexReader) { + ((ReleaseableIndexReader) reader).release(); + } else { + reader.close(); + } + } + + /** + * Returns a comparable for the internal value. + * + * @param value + * an internal value. + * @return a comparable for the given value. + * @throws RepositoryException + * if retrieving the Comparable fails. + */ + public static Comparable getComparable(InternalValue value) + throws RepositoryException { + switch (value.getType()) { + case PropertyType.BINARY: + return null; + case PropertyType.BOOLEAN: + return value.getBoolean(); + case PropertyType.DATE: + return value.getDate().getTimeInMillis(); + case PropertyType.DOUBLE: + return value.getDouble(); + case PropertyType.LONG: + return value.getLong(); + case PropertyType.DECIMAL: + return value.getDecimal(); + case PropertyType.NAME: + return value.getName().toString(); + case PropertyType.PATH: + return value.getPath().toString(); + case PropertyType.URI: + case PropertyType.WEAKREFERENCE: + case PropertyType.REFERENCE: + case PropertyType.STRING: + return value.getString(); + default: + return null; + } + } + + /** + * Returns a comparable for the internal value. + * + * @param value + * an internal value. + * @return a comparable for the given value. + * @throws ValueFormatException + * if the given value cannot be converted into a + * comparable (i.e. unsupported type). + * @throws RepositoryException + * if an error occurs while converting the value. + */ + public static Comparable getComparable(Value value) + throws ValueFormatException, RepositoryException { + switch (value.getType()) { + case PropertyType.BOOLEAN: + return value.getBoolean(); + case PropertyType.DATE: + return value.getDate().getTimeInMillis(); + case PropertyType.DOUBLE: + return value.getDouble(); + case PropertyType.LONG: + return value.getLong(); + case PropertyType.DECIMAL: + return value.getDecimal(); + case PropertyType.NAME: + case PropertyType.PATH: + case PropertyType.URI: + case PropertyType.WEAKREFERENCE: + case PropertyType.REFERENCE: + case PropertyType.STRING: + return value.getString(); + default: + throw new RepositoryException("Unsupported type: " + + PropertyType.nameFromValue(value.getType())); + } + } + + /** + * Compares values c1 and c2. If the values have + * differing types, then the order is defined on the type itself by calling + * compareTo() on the respective type class names. + * + * @param c1 + * the first value. + * @param c2 + * the second value. + * @return a negative integer if c1 should come before + * c2
    + * a positive integer if c1 should come after + * c2
    + * 0 if they are equal. + */ + public static int compare(Comparable c1, Comparable c2) { + if (c1 == c2) { + return 0; + } else if (c1 == null) { + return -1; + } else if (c2 == null) { + return 1; + } else if (c1.getClass() == c2.getClass()) { + return c1.compareTo(c2); + } else { + // differing types -> compare class names + String name1 = c1.getClass().getName(); + String name2 = c2.getClass().getName(); + return name1.compareTo(name2); + } + } + + /** + * Compares two arrays of Comparable(s) in the same style as + * {@link #compare(Value[], Value[])}. + * + * The 2 methods *have* to work in the same way for the sort to be + * consistent + */ + public static int compare(Comparable[] c1, Comparable[] c2) { + if(c1 == null && c2 == null){ + return 0; + } + if (c1 == null) { + return -1; + } + if (c2 == null) { + return 1; + } + for (int i = 0; i < c1.length && i < c2.length; i++) { + int d = compare(c1[i], c2[i]); + if (d != 0) { + return d; + } + } + return c1.length - c2.length; + } + + /** + * Compares two arrays of Value(s) in the same style as + * {@link #compare(Comparable[], Comparable[])}. + * + * The 2 methods *have* to work in the same way for the sort to be + * consistent + */ + public static int compare(Value[] a, Value[] b) throws RepositoryException { + if(a == null && b == null){ + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + for (int i = 0; i < a.length && i < b.length; i++) { + int d = compare(a[i], b[i]); + if (d != 0) { + return d; + } + } + return a.length - b.length; + } + + /** + * Compares the two values. If the values have differing types, then an + * attempt is made to convert the second value into the type of the first + * value. + *

    + * Comparison of binary values is not supported. + * + * @param v1 + * the first value. + * @param v2 + * the second value. + * @return result of the comparison as specified in + * {@link Comparable#compareTo(Object)}. + * @throws ValueFormatException + * if the given value cannot be converted into a + * comparable (i.e. unsupported type). + * @throws RepositoryException + * if an error occurs while converting the value. + */ + public static int compare(Value v1, Value v2) throws ValueFormatException, + RepositoryException { + Comparable c1 = getComparable(v1); + Comparable c2; + switch (v1.getType()) { + case PropertyType.BOOLEAN: + c2 = v2.getBoolean(); + break; + case PropertyType.DATE: + c2 = v2.getDate().getTimeInMillis(); + break; + case PropertyType.DOUBLE: + c2 = v2.getDouble(); + break; + case PropertyType.LONG: + c2 = v2.getLong(); + break; + case PropertyType.DECIMAL: + c2 = v2.getDecimal(); + break; + case PropertyType.NAME: + if (v2.getType() == PropertyType.URI) { + String s = v2.getString(); + if (s.startsWith("./")) { + s = s.substring(2); + } + // need to decode + try { + c2 = URLDecoder.decode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(e); + } + } else { + c2 = v2.getString(); + } + break; + case PropertyType.PATH: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + case PropertyType.URI: + case PropertyType.STRING: + c2 = v2.getString(); + break; + default: + throw new RepositoryException("Unsupported type: " + + PropertyType.nameFromValue(v2.getType())); + } + return compare(c1, c2); + } + + /** + * Creates a regexp from likePattern. + * + * @param likePattern + * the pattern. + * @return the regular expression Pattern. + */ + public static Pattern createRegexp(String likePattern) { + // - escape all non alphabetic characters + // - escape constructs like \ into \\ + // - replace non escaped _ % into . and .* + StringBuffer regexp = new StringBuffer(); + boolean escaped = false; + for (int i = 0; i < likePattern.length(); i++) { + if (likePattern.charAt(i) == '\\') { + if (escaped) { + regexp.append("\\\\"); + escaped = false; + } else { + escaped = true; + } + } else { + if (Character.isLetterOrDigit(likePattern.charAt(i))) { + if (escaped) { + regexp.append("\\\\").append(likePattern.charAt(i)); + escaped = false; + } else { + regexp.append(likePattern.charAt(i)); + } + } else { + if (escaped) { + regexp.append('\\').append(likePattern.charAt(i)); + escaped = false; + } else { + switch (likePattern.charAt(i)) { + case '_': + regexp.append('.'); + break; + case '%': + regexp.append(".*"); + break; + default: + regexp.append('\\').append(likePattern.charAt(i)); + } + } + } + } + } + return Pattern.compile(regexp.toString(), Pattern.DOTALL); + } + + /** + * Returns length of the internal value. + * + * @param value + * a value. + * @return the length of the internal value or -1 if the length + * cannot be determined. + */ + public static long getLength(InternalValue value) { + if (value.getType() == PropertyType.NAME + || value.getType() == PropertyType.PATH) { + return -1; + } else { + try { + return value.getLength(); + } catch (RepositoryException e) { + log.warn("Unable to determine length of value. {}", e.getMessage()); + return -1; + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/VolatileIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/VolatileIndex.java new file mode 100644 index 00000000000..9974c513056 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/VolatileIndex.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.commons.collections.map.LinkedMap; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.Term; +import org.apache.lucene.store.RAMDirectory; +import org.apache.lucene.search.Similarity; + +import java.io.IOException; +import java.util.Map; + +/** + * Implements an in-memory index with a pending buffer. + */ +class VolatileIndex extends AbstractIndex { + + /** + * Default value for {@link #bufferSize}. + */ + private static final int DEFAULT_BUFFER_SIZE = 10; + + /** + * Map of pending documents to add to the index + */ + @SuppressWarnings("unchecked") + private final Map pending = new LinkedMap(); + + /** + * Number of documents that are buffered before they are added to the index. + */ + private int bufferSize = DEFAULT_BUFFER_SIZE; + + /** + * The number of documents in this index. + */ + private int numDocs = 0; + + /** + * Creates a new VolatileIndex using an analyzer. + * + * @param analyzer the analyzer to use. + * @param similarity the similarity implementation. + * @param indexingQueue the indexing queue. + * @throws IOException if an error occurs while opening the index. + */ + VolatileIndex(Analyzer analyzer, + Similarity similarity, + IndexingQueue indexingQueue) throws IOException { + super(analyzer, similarity, new RAMDirectory(), null, indexingQueue); + } + + /** + * Overwrites the default implementation by adding the documents to a + * pending list and commits the pending list if needed. + * + * @param docs the documents to add to the index. + * @throws IOException if an error occurs while writing to the index. + */ + void addDocuments(Document[] docs) throws IOException { + for (int i = 0; i < docs.length; i++) { + Document old = (Document) pending.put(docs[i].get(FieldNames.UUID), docs[i]); + if (old != null) { + Util.disposeDocument(old); + } + if (pending.size() >= bufferSize) { + commitPending(); + } + numDocs++; + } + invalidateSharedReader(); + } + + /** + * Overwrites the default implementation to remove the document from the + * pending list if it is present or simply calls super.removeDocument(). + * + * @param idTerm the uuid term of the document to remove. + * @return the number of deleted documents + * @throws IOException if an error occurs while removing the document from + * the index. + */ + int removeDocument(Term idTerm) throws IOException { + Document doc = (Document) pending.remove(idTerm.text()); + int num; + if (doc != null) { + Util.disposeDocument(doc); + // pending document has been removed + num = 1; + } else { + // remove document from index + num = super.getIndexReader().deleteDocuments(idTerm); + } + numDocs -= num; + return num; + } + + /** + * Returns the number of valid documents in this index. + * + * @return the number of valid documents in this index. + */ + int getNumDocuments() { + return numDocs; + } + + /** + * Overwrites the implementation in {@link AbstractIndex} to trigger + * commit of pending documents to index. + * @return the index reader for this index. + * @throws IOException if an error occurs building a reader. + */ + protected synchronized CommittableIndexReader getIndexReader() throws IOException { + commitPending(); + return super.getIndexReader(); + } + + /** + * Overwrites the implementation in {@link AbstractIndex} to commit + * pending documents. + * @param optimize if true the index is optimized after the + * commit. + */ + protected synchronized void commit(boolean optimize) throws IOException { + commitPending(); + super.commit(optimize); + } + + /** + * {@inheritDoc} + */ + long getRamSizeInBytes() { + return super.getRamSizeInBytes() + ((RAMDirectory) getDirectory()).sizeInBytes(); + } + + /** + * Sets a new buffer size for pending documents to add to the index. + * Higher values consume more memory, but help to avoid multiple index + * cycles when a node is changed / saved multiple times. + * + * @param size the new buffer size. + */ + void setBufferSize(int size) { + bufferSize = size; + } + + /** + * Commits pending documents to the index. + * + * @throws IOException if committing pending documents fails. + */ + private void commitPending() throws IOException { + if (pending.isEmpty()) { + return; + } + super.addDocuments((Document[]) pending.values().toArray( + new Document[pending.size()])); + pending.clear(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedHTMLExcerpt.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedHTMLExcerpt.java new file mode 100644 index 00000000000..adf5a824e8a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedHTMLExcerpt.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.core.query.lucene; + +import org.apache.lucene.index.TermPositionVector; + +import java.io.IOException; + +/** + * WeightedHTMLExcerpt creates a HTML excerpt with the following + * format: + *

    + * <div>
    + *     <span><strong>Jackrabbit</strong> implements both the mandatory XPath and optional SQL <strong>query</strong> syntax.</span>
    + *     <span>Before parsing the XPath <strong>query</strong> in <strong>Jackrabbit</strong>, the statement is surrounded</span>
    + * </div>
    + * 
    + * In contrast to {@link DefaultHTMLExcerpt} this implementation weights + * fragments based on the proximity of highlighted terms. Highlighted terms that + * are adjacent have a higher weight. In addition, the more highlighted terms, + * the higher the weight. + * + * @see WeightedHighlighter + */ +public class WeightedHTMLExcerpt extends AbstractExcerpt { + + /** + * {@inheritDoc} + */ + protected String createExcerpt(TermPositionVector tpv, + String text, + int maxFragments, + int maxFragmentSize) throws IOException { + return WeightedHighlighter.highlight(tpv, getQueryTerms(), text, + "
    ", "
    ", "", "", "", "", + maxFragments, maxFragmentSize / 2); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedHighlighter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedHighlighter.java new file mode 100644 index 00000000000..da8d555cb8a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedHighlighter.java @@ -0,0 +1,360 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import org.apache.lucene.index.TermPositionVector; +import org.apache.lucene.index.TermVectorOffsetInfo; +import org.apache.lucene.index.Term; +import org.apache.lucene.util.PriorityQueue; + +import java.util.Set; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.LinkedList; +import java.io.IOException; + +/** + * WeightedHighlighter implements a highlighter that weights the + * fragments based on the proximity of the highlighted terms to each other. The + * returned fragments are not necessarily in sequence as the text occurs in the + * content. + */ +public class WeightedHighlighter extends DefaultHighlighter { + + /** + * Punctuation characters that mark the end of a sentence. + */ + private static final BitSet PUNCTUATION = new BitSet(); + + static { + PUNCTUATION.set('.'); + PUNCTUATION.set('!'); + PUNCTUATION.set(0xa1); // inverted exclamation mark + PUNCTUATION.set('?'); + PUNCTUATION.set(0xbf); // inverted question mark + // todo add more + } + + protected WeightedHighlighter() { + } + + /** + * @param tvec the term position vector for this hit + * @param queryTerms the query terms. + * @param text the original text that was used to create the + * tokens. + * @param excerptStart this string is prepended to the excerpt + * @param excerptEnd this string is appended to the excerpt + * @param fragmentStart this string is prepended to every fragment + * @param fragmentEnd this string is appended to the end of every + * fragment. + * @param hlStart the string used to prepend a highlighted token, for + * example "<b>" + * @param hlEnd the string used to append a highlighted token, for + * example "</b>" + * @param maxFragments the maximum number of fragments + * @param surround the maximum number of chars surrounding a + * highlighted token + * @return a String with text fragments where tokens from the query are + * highlighted + */ + public static String highlight(TermPositionVector tvec, + Set queryTerms, + String text, + String excerptStart, + String excerptEnd, + String fragmentStart, + String fragmentEnd, + String hlStart, + String hlEnd, + int maxFragments, + int surround) throws IOException { + return new WeightedHighlighter().doHighlight(tvec, queryTerms, text, + excerptStart, excerptEnd, fragmentStart, fragmentEnd, hlStart, + hlEnd, maxFragments, surround); + } + + /** + * @param tvec the term position vector for this hit + * @param queryTerms the query terms. + * @param text the original text that was used to create the tokens. + * @param maxFragments the maximum number of fragments + * @param surround the maximum number of chars surrounding a highlighted + * token + * @return a String with text fragments where tokens from the query are + * highlighted + */ + public static String highlight(TermPositionVector tvec, + Set queryTerms, + String text, + int maxFragments, + int surround) throws IOException { + return highlight(tvec, queryTerms, text, START_EXCERPT, END_EXCERPT, + START_FRAGMENT_SEPARATOR, END_FRAGMENT_SEPARATOR, + START_HIGHLIGHT, END_HIGHLIGHT, maxFragments, surround); + } + + @Override + protected String mergeFragments(TermVectorOffsetInfo[] offsets, + String text, + String excerptStart, + String excerptEnd, + String fragmentStart, + String fragmentEnd, + String hlStart, + String hlEnd, + int maxFragments, + int surround) throws IOException { + if (offsets == null || offsets.length == 0) { + // nothing to highlight + return createDefaultExcerpt(text, excerptStart, excerptEnd, + fragmentStart, fragmentEnd, surround * 2); + } + + PriorityQueue bestFragments = new FragmentInfoPriorityQueue(maxFragments); + for (int i = 0; i < offsets.length; i++) { + if (offsets[i].getEndOffset() <= text.length()) { + FragmentInfo fi = new FragmentInfo(offsets[i], surround * 2); + for (int j = i + 1; j < offsets.length; j++) { + if (offsets[j].getEndOffset() > text.length()) { + break; + } + if (!fi.add(offsets[j], text)) { + break; + } + } + bestFragments.insertWithOverflow(fi); + } + } + + if (bestFragments.size() == 0) { + return createDefaultExcerpt(text, excerptStart, excerptEnd, + fragmentStart, fragmentEnd, surround * 2); + } + + // retrieve fragment infos from queue and fill into list, least + // fragment comes out first + List infos = new LinkedList(); + while (bestFragments.size() > 0) { + FragmentInfo fi = (FragmentInfo) bestFragments.pop(); + infos.add(0, fi); + } + + Map offsetInfos = new IdentityHashMap(); + // remove overlapping fragment infos + Iterator it = infos.iterator(); + while (it.hasNext()) { + FragmentInfo fi = it.next(); + boolean overlap = false; + Iterator fit = fi.iterator(); + while (fit.hasNext() && !overlap) { + TermVectorOffsetInfo oi = fit.next(); + if (offsetInfos.containsKey(oi)) { + overlap = true; + } + } + if (overlap) { + it.remove(); + } else { + Iterator oit = fi.iterator(); + while (oit.hasNext()) { + offsetInfos.put(oit.next(), null); + } + } + } + + // create excerpts + StringBuffer sb = new StringBuffer(excerptStart); + it = infos.iterator(); + while (it.hasNext()) { + FragmentInfo fi = it.next(); + sb.append(fragmentStart); + int limit = Math.max(0, fi.getStartOffset() / 2 + fi.getEndOffset() / 2 - surround); + int len = startFragment(sb, text, fi.getStartOffset(), limit); + TermVectorOffsetInfo lastOffsetInfo = null; + Iterator fIt = fi.iterator(); + while (fIt.hasNext()) { + TermVectorOffsetInfo oi = fIt.next(); + if (lastOffsetInfo != null) { + // fill in text between terms + sb.append(escape(text.substring( + lastOffsetInfo.getEndOffset(), oi.getStartOffset()))); + } + sb.append(hlStart); + sb.append(escape(text.substring(oi.getStartOffset(), + oi.getEndOffset()))); + sb.append(hlEnd); + lastOffsetInfo = oi; + } + limit = Math.min(text.length(), fi.getStartOffset() - len + + (surround * 2)); + endFragment(sb, text, fi.getEndOffset(), limit); + sb.append(fragmentEnd); + } + sb.append(excerptEnd); + return sb.toString(); + } + + /** + * Writes the start of a fragment to the string buffer sb. The + * first occurrence of a matching term is indicated by the + * offset into the text. + * + * @param sb where to append the start of the fragment. + * @param text the original text. + * @param offset the start offset of the first matching term in the + * fragment. + * @param limit do not go back further than limit. + * @return the length of the start fragment that was appended to + * sb. + */ + private int startFragment(StringBuffer sb, String text, int offset, int limit) { + if (limit == 0) { + // append all + sb.append(escape(text.substring(0, offset))); + return offset; + } + String intro = "... "; + int start = offset; + for (int i = offset - 1; i >= limit; i--) { + if (Character.isWhitespace(text.charAt(i))) { + // potential start + start = i + 1; + if (i - 1 >= limit && PUNCTUATION.get(text.charAt(i - 1))) { + // start of sentence found + intro = ""; + break; + } + } + } + sb.append(intro).append(escape(text.substring(start, offset))); + return offset - start; + } + + /** + * Writes the end of a fragment to the string buffer sb. The + * last occurrence of a matching term is indicated by the + * offset into the text. + * + * @param sb where to append the start of the fragment. + * @param text the original text. + * @param offset the end offset of the last matching term in the fragment. + * @param limit do not go further than limit. + */ + private void endFragment(StringBuffer sb, String text, int offset, int limit) { + if (limit == text.length()) { + // append all + sb.append(escape(text.substring(offset))); + return; + } + int end = offset; + for (int i = end; i < limit; i++) { + if (Character.isWhitespace(text.charAt(i))) { + // potential end + end = i; + } + } + sb.append(escape(text.substring(offset, end))).append(" ..."); + } + + private static class FragmentInfo { + List offsetInfosList; + int startOffset; + int endOffset; + int maxFragmentSize; + int quality; + + public FragmentInfo(TermVectorOffsetInfo offsetinfo, int maxFragmentSize) { + offsetInfosList = new ArrayList(); + offsetInfosList.add(offsetinfo); + startOffset = offsetinfo.getStartOffset(); + endOffset = offsetinfo.getEndOffset(); + this.maxFragmentSize = maxFragmentSize; + quality = 0; + } + + public boolean add(TermVectorOffsetInfo offsetinfo, String text) { + if (offsetinfo.getEndOffset() > (startOffset + maxFragmentSize)) { + return false; + } + offsetInfosList.add(offsetinfo); + if (offsetinfo.getStartOffset() - endOffset <= 3) { + // boost quality when terms are adjacent + // and only separated by whitespace character + boolean boost = true; + for (int i = endOffset; i < offsetinfo.getStartOffset(); i++) { + if (!Character.isWhitespace(text.charAt(i))) { + boost = false; + break; + } + } + if (boost) { + quality += 10; + } else { + quality++; + } + } else { + quality++; + } + endOffset = offsetinfo.getEndOffset(); + return true; + } + + public Iterator iterator() { + return offsetInfosList.iterator(); + } + + public int getStartOffset() { + return startOffset; + } + + public int getEndOffset() { + return endOffset; + } + + public int getQuality() { + return quality; + } + + } + + private static class FragmentInfoPriorityQueue extends PriorityQueue { + + public FragmentInfoPriorityQueue(int size) { + initialize(size); + } + + /** + * Checks the quality of two {@link FragmentInfo} objects. The one with + * the lower quality is considered less than the other. If both + * fragments have the same quality, the one with the higher start offset + * is considered the lesser. This will result in a queue that keeps the + * {@link FragmentInfo} with the best quality. + */ + @Override + protected boolean lessThan(FragmentInfo infoA, FragmentInfo infoB) { + if (infoA.getQuality() == infoB.getQuality()) { + return infoA.getStartOffset() > infoB.getStartOffset(); + } + return infoA.getQuality() < infoB.getQuality(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedXMLExcerpt.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedXMLExcerpt.java new file mode 100644 index 00000000000..480e4699724 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WeightedXMLExcerpt.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.core.query.lucene; + +import org.apache.lucene.index.TermPositionVector; + +import java.io.IOException; + +/** + * WeightedXMLExcerpt creates an XML excerpt of a matching node. In + * contrast to {@link DefaultXMLExcerpt} this implementation weights fragments + * based on the proximity of highlighted terms. Highlighted terms that are + * adjacent have a higher weight. In addition, the more highlighted terms, the + * higher the weight. + *
    + * E.g. if you search for 'jackrabbit' and 'query' you may get the following + * result for a node: + *
    + * <excerpt>
    + *     <fragment><highlight>Jackrabbit</highlight> implements both the mandatory XPath and optional SQL <highlight>query</highlight> syntax.</fragment>
    + *     <fragment>Before parsing the XPath <highlight>query</highlight> in <highlight>Jackrabbit</highlight>, the statement is surrounded</fragment>
    + * </excerpt>
    + * 
    + * + * @see WeightedHighlighter + */ +public class WeightedXMLExcerpt extends AbstractExcerpt { + + /** + * {@inheritDoc} + */ + protected String createExcerpt(TermPositionVector tpv, + String text, + int maxFragments, + int maxFragmentSize) + throws IOException { + return WeightedHighlighter.highlight(tpv, getQueryTerms(), text, + maxFragments, maxFragmentSize / 2); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.java new file mode 100644 index 00000000000..cb6ba646174 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardNameQuery.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.core.query.lucene; + +import javax.jcr.NamespaceException; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements a wildcard query on the name field. + *

    + * Wildcards are: + *

      + *
    • % : matches zero or more characters
    • + *
    • _ : matches exactly one character
    • + *
    + * Wildcards in the namespace prefix are not supported and will not match. + */ +public class WildcardNameQuery extends WildcardQuery { + + private static final long serialVersionUID = -4705104992551930918L; + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(WildcardNameQuery.class); + + public WildcardNameQuery(String pattern, + int transform, + NamespaceResolver resolver, + NamespaceMappings nsMappings, + PerQueryCache cache) { + super(FieldNames.LABEL, null, + convertPattern(pattern, resolver, nsMappings), transform, cache); + } + + private static String convertPattern(String pattern, + NamespaceResolver resolver, + NamespaceMappings nsMappings) { + String prefix = ""; + int idx = pattern.indexOf(':'); + if (idx != -1) { + prefix = pattern.substring(0, idx); + } + StringBuffer sb = new StringBuffer(); + // translate prefix + try { + sb.append(nsMappings.getPrefix(resolver.getURI(prefix))); + } catch (NamespaceException e) { + // prefix in pattern is probably unknown + log.debug("unknown namespace prefix in pattern: " + pattern); + // -> ignore and use empty string for index internal prefix + // this will not match anything + } + sb.append(":"); + // remaining pattern, may also be whole pattern + sb.append(pattern.substring(idx + 1)); + return sb.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.java new file mode 100644 index 00000000000..1c1dfdbddad --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardQuery.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.core.query.lucene; + +import org.apache.jackrabbit.core.query.lucene.WildcardTermEnum.TermValueFactory; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.search.FilteredTermEnum; +import org.apache.lucene.search.MultiTermQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.Weight; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.util.ToStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.BitSet; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; + +/** + * Implements a wildcard query on a lucene field with an embedded property name + * and a pattern. + *

    + * Wildcards are: + *

      + *
    • % : matches zero or more characters
    • + *
    • _ : matches exactly one character
    • + *
    + */ +@SuppressWarnings("serial") +public class WildcardQuery extends Query implements Transformable { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(WildcardQuery.class); + + /** + * Name of the field to search. + */ + private final String field; + + /** + * Creates a term value for a given string. + */ + private final TermValueFactory tvf; + + /** + * The wildcard pattern. + */ + private final String pattern; + + /** + * How property values are transformed before they are matched using the + * provided pattern. + */ + private int transform = TRANSFORM_NONE; + + private final PerQueryCache cache; + + /** + * The standard multi term query to execute wildcard queries. This is only + * set if the pattern matches less than {@link org.apache.lucene.search.BooleanQuery#maxClauseCount}. + */ + private Query multiTermQuery; + + /** + * Creates a new WildcardQuery. + * + * @param field the name of the field to search. + * @param propName name of the property to search. + * @param pattern the wildcard pattern. + * @param transform how property values are transformed before they are + * matched using the pattern. + */ + public WildcardQuery( + String field, final String propName, String pattern, int transform, + PerQueryCache cache) { + this.field = field.intern(); + this.pattern = pattern; + this.transform = transform; + this.cache = cache; + if (propName != null) { + tvf = new WildcardTermEnum.TermValueFactory() { + @Override + public String createValue(String s) { + return FieldNames.createNamedValue(propName, s); + } + }; + } else { + tvf = new WildcardTermEnum.TermValueFactory(); + } + } + + /** + * Creates a new WildcardQuery. + * + * @param field the name of the field to search. + * @param propName name of the property to search. + * @param pattern the wildcard pattern. + */ + public WildcardQuery( + String field, String propName, String pattern, PerQueryCache cache) { + this(field, propName, pattern, TRANSFORM_NONE, cache); + } + + /** + * {@inheritDoc} + */ + public void setTransformation(int transformation) { + this.transform = transformation; + } + + /** + * Either rewrites this query to a lucene MultiTermQuery or in case of + * a TooManyClauses exception to a custom jackrabbit query implementation + * that uses a BitSet to collect all hits. + * + * @param reader the index reader to use for the search. + * @return the rewritten query. + * @throws IOException if an error occurs while reading from the index. + */ + @Override + public Query rewrite(IndexReader reader) throws IOException { + try { + multiTermQuery = new StdWildcardQuery(field, tvf, pattern, + transform).rewrite(reader); + return multiTermQuery; + } catch (BooleanQuery.TooManyClauses e) { + // MultiTermQuery not possible + log.debug("Too many terms to enumerate, using custom WildcardQuery."); + return this; + } + } + + /** + * Creates the Weight for this query. + * + * @param searcher the searcher to use for the Weight. + * @return the Weigth for this query. + */ + @Override + public Weight createWeight(Searcher searcher) { + return new WildcardQueryWeight(searcher, cache); + } + + /** + * Returns a string representation of this query. + * + * @param field the field name for which to create a string representation. + * @return a string representation of this query. + */ + @Override + public String toString(String field) { + return field + ":" + tvf.createValue(pattern); + } + + @Override + public void extractTerms(Set terms) { + if (multiTermQuery != null) { + multiTermQuery.extractTerms(terms); + } + } + + private static class StdWildcardQuery extends MultiTermQuery { + + private final String field; + private final TermValueFactory tvf; + private final String pattern; + private final int transform; + + public StdWildcardQuery(String field, TermValueFactory tvf, + String pattern, int transform) { + this.field = field; + this.tvf = tvf; + this.pattern = pattern; + this.transform = transform; + setRewriteMethod(CONSTANT_SCORE_BOOLEAN_QUERY_REWRITE); + } + + @Override + protected FilteredTermEnum getEnum(IndexReader reader) + throws IOException { + return new WildcardTermEnum(reader, field, tvf, pattern, transform); + } + + /** Prints a user-readable version of this query. */ + @Override + public String toString(String field) { + StringBuffer buffer = new StringBuffer(); + buffer.append(field); + buffer.append(':'); + buffer.append(ToStringUtils.boost(getBoost())); + return buffer.toString(); + } + } + + /** + * The Weight implementation for this WildcardQuery. + */ + private class WildcardQueryWeight extends AbstractWeight { + + private final PerQueryCache cache; + + /** + * Creates a new WildcardQueryWeight instance using + * searcher. + * + * @param searcher a Searcher instance. + */ + public WildcardQueryWeight(Searcher searcher, PerQueryCache cache) { + super(searcher); + this.cache = cache; + } + + /** + * Creates a {@link WildcardQueryScorer} instance. + * + * @param reader index reader + * @return a {@link WildcardQueryScorer} instance + */ + protected Scorer createScorer(IndexReader reader, boolean scoreDocsInOrder, + boolean topScorer) { + return new WildcardQueryScorer(searcher.getSimilarity(), reader, cache); + } + + /** + * Returns this WildcardQuery. + * + * @return this WildcardQuery. + */ + @Override + public Query getQuery() { + return WildcardQuery.this; + } + + @Override + public float getValue() { + return 1.0f; + } + + @Override + public float sumOfSquaredWeights() throws IOException { + return 1.0f; + } + + @Override + public void normalize(float norm) { + } + + @Override + public Explanation explain(IndexReader reader, int doc) throws IOException { + return new Explanation(); + } + } + + /** + * Implements a Scorer for this WildcardQuery. + */ + private final class WildcardQueryScorer extends Scorer { + + /** + * The index reader to use for calculating the matching documents. + */ + private final IndexReader reader; + + /** + * The documents ids that match this wildcard query. + */ + private final BitSet hits; + + /** + * Set to true when the hits have been calculated. + */ + private boolean hitsCalculated = false; + + /** + * The next document id to return + */ + private int nextDoc = -1; + + /** + * The cache key to use to store the results. + */ + private final String cacheKey; + + /** + * The map to store the results. + */ + private final Map resultMap; + + /** + * Creates a new WildcardQueryScorer. + * + * @param similarity the similarity implementation. + * @param reader the index reader to use. + */ + @SuppressWarnings({"unchecked"}) + WildcardQueryScorer( + Similarity similarity, IndexReader reader, + PerQueryCache cache) { + super(similarity); + this.reader = reader; + this.cacheKey = field + '\uFFFF' + tvf.createValue('\uFFFF' + pattern) + '\uFFFF' + transform; + // check cache + Map m = (Map) cache.get(WildcardQueryScorer.class, reader); + if (m == null) { + m = new HashMap(); + cache.put(WildcardQueryScorer.class, reader, m); + } + resultMap = m; + + BitSet result = resultMap.get(cacheKey); + if (result == null) { + result = new BitSet(reader.maxDoc()); + } else { + hitsCalculated = true; + } + hits = result; + } + + @Override + public int nextDoc() throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + + calculateHits(); + nextDoc = hits.nextSetBit(nextDoc + 1); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + @Override + public int docID() { + return nextDoc; + } + + @Override + public float score() { + return 1.0f; + } + + @Override + public int advance(int target) throws IOException { + if (nextDoc == NO_MORE_DOCS) { + return nextDoc; + } + // optimize in the case of an advance to finish. + // see https://issues.apache.org/jira/browse/JCR-3091 + if (target == NO_MORE_DOCS) { + nextDoc = NO_MORE_DOCS; + return nextDoc; + } + + calculateHits(); + nextDoc = hits.nextSetBit(target); + if (nextDoc < 0) { + nextDoc = NO_MORE_DOCS; + } + return nextDoc; + } + + /** + * Calculates the ids of the documents matching this wildcard query. + * @throws IOException if an error occurs while reading from the index. + */ + private void calculateHits() throws IOException { + if (hitsCalculated) { + return; + } + TermEnum terms = new WildcardTermEnum(reader, field, tvf, pattern, transform); + try { + // use unpositioned TermDocs + TermDocs docs = reader.termDocs(); + try { + while (terms.term() != null) { + docs.seek(terms); + while (docs.next()) { + hits.set(docs.doc()); + } + if (!terms.next()) { + break; + } + } + } finally { + docs.close(); + } + } finally { + terms.close(); + } + hitsCalculated = true; + // put to cache + resultMap.put(cacheKey, hits); + } + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.java new file mode 100644 index 00000000000..7eadf50d7d0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/WildcardTermEnum.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.core.query.lucene; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.search.FilteredTermEnum; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements a wildcard term enum that optionally supports embedded property + * names in lucene term texts. + */ +class WildcardTermEnum extends FilteredTermEnum implements TransformConstants { + + /** + * The pattern matcher. + */ + private final Matcher pattern; + + /** + * The lucene field to search. + */ + private final String field; + + /** + * Factory for term values. + */ + private final TermValueFactory tvf; + + /** + * The term prefix without wildcards + */ + private final String prefix; + + /** + * Flag that indicates the end of the term enum. + */ + private boolean endEnum = false; + + /** + * The input for the pattern matcher. + */ + private final OffsetCharSequence input; + + /** + * How terms from the index are transformed. + */ + private final int transform; + + /** + * Creates a new WildcardTermEnum. + * + * @param reader the index reader. + * @param field the lucene field to search. + * @param tvf the term value factory. + * @param pattern the pattern to match the values. + * @param transform the transformation that should be applied to the term + * enum from the index reader. + * @throws IOException if an error occurs while reading from + * the index. + * @throws IllegalArgumentException if transform is not a valid + * value. + */ + public WildcardTermEnum(IndexReader reader, + String field, + TermValueFactory tvf, + String pattern, + int transform) throws IOException { + if (transform < TRANSFORM_NONE || transform > TRANSFORM_UPPER_CASE) { + throw new IllegalArgumentException("invalid transform parameter"); + } + this.field = field; + this.transform = transform; + this.tvf = tvf; + + int idx = 0; + + if (transform == TRANSFORM_NONE) { + // optimize the term comparison by removing the prefix from the pattern + // and therefore use a more precise range scan + while (idx < pattern.length() + && (Character.isLetterOrDigit(pattern.charAt(idx)) || pattern.charAt(idx) == ':')) { + idx++; + } + + prefix = tvf.createValue(pattern.substring(0, idx)); + } else { + prefix = tvf.createValue(""); + } + + // initialize with prefix as dummy value + input = new OffsetCharSequence(prefix.length(), prefix, transform); + this.pattern = Util.createRegexp(pattern.substring(idx)).matcher(input); + + if (transform == TRANSFORM_NONE) { + setEnum(reader.terms(new Term(field, prefix))); + } else { + setEnum(new LowerUpperCaseTermEnum(reader, field, pattern, transform)); + } + } + + /** + * @inheritDoc + */ + protected boolean termCompare(Term term) { + if (transform == TRANSFORM_NONE) { + if (term.field() == field && term.text().startsWith(prefix)) { + input.setBase(term.text()); + return pattern.reset().matches(); + } + endEnum = true; + return false; + } else { + // pre filtered, no need to check + return true; + } + } + + /** + * @inheritDoc + */ + public float difference() { + return 1.0f; + } + + /** + * @inheritDoc + */ + protected boolean endEnum() { + return endEnum; + } + + //--------------------------< internal >------------------------------------ + + /** + * Implements a term enum which respects the transformation flag and + * matches a pattern on the enumerated terms. + */ + private class LowerUpperCaseTermEnum extends TermEnum { + + /** + * The matching terms + */ + private final Map orderedTerms = new LinkedHashMap(); + + /** + * Iterator over all matching terms + */ + private final Iterator it; + + public LowerUpperCaseTermEnum(IndexReader reader, + String field, + String pattern, + int transform) throws IOException { + if (transform != TRANSFORM_LOWER_CASE && transform != TRANSFORM_UPPER_CASE) { + throw new IllegalArgumentException("transform"); + } + + // check if pattern never matches + boolean neverMatches = false; + for (int i = 0; i < pattern.length() && !neverMatches; i++) { + if (transform == TRANSFORM_LOWER_CASE) { + neverMatches = Character.isUpperCase(pattern.charAt(i)); + } else if (transform == TRANSFORM_UPPER_CASE) { + neverMatches = Character.isLowerCase(pattern.charAt(i)); + } + } + + if (!neverMatches) { + // create range scans + List rangeScans = new ArrayList(2); + try { + int idx = 0; + while (idx < pattern.length() + && Character.isLetterOrDigit(pattern.charAt(idx))) { + idx++; + } + String patternPrefix = pattern.substring(0, idx); + if (patternPrefix.length() == 0) { + // scan full property range + String prefix = tvf.createValue(""); + String limit = tvf.createValue("\uFFFF"); + rangeScans.add(new RangeScan(reader, + new Term(field, prefix), new Term(field, limit))); + } else { + // start with initial lower case + StringBuffer lowerLimit = new StringBuffer(patternPrefix.toUpperCase()); + lowerLimit.setCharAt(0, Character.toLowerCase(lowerLimit.charAt(0))); + String prefix = tvf.createValue(lowerLimit.toString()); + + StringBuffer upperLimit = new StringBuffer(patternPrefix.toLowerCase()); + upperLimit.append('\uFFFF'); + String limit = tvf.createValue(upperLimit.toString()); + rangeScans.add(new RangeScan(reader, + new Term(field, prefix), new Term(field, limit))); + + // second scan with upper case start + prefix = tvf.createValue(patternPrefix.toUpperCase()); + upperLimit = new StringBuffer(patternPrefix.toLowerCase()); + upperLimit.setCharAt(0, Character.toUpperCase(upperLimit.charAt(0))); + upperLimit.append('\uFFFF'); + limit = tvf.createValue(upperLimit.toString()); + rangeScans.add(new RangeScan(reader, + new Term(field, prefix), new Term(field, limit))); + } + + // do range scans with pattern matcher + for (RangeScan scan : rangeScans) { + do { + Term t = scan.term(); + if (t != null) { + input.setBase(t.text()); + if (WildcardTermEnum.this.pattern.reset().matches()) { + orderedTerms.put(t, scan.docFreq()); + } + } + } while (scan.next()); + } + + } finally { + // close range scans + for (RangeScan scan : rangeScans) { + try { + scan.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + it = orderedTerms.keySet().iterator(); + getNext(); + } + + /** + * The current term in this enum. + */ + private Term current; + + /** + * {@inheritDoc} + */ + public boolean next() { + getNext(); + return current != null; + } + + /** + * {@inheritDoc} + */ + public Term term() { + return current; + } + + /** + * {@inheritDoc} + */ + public int docFreq() { + Integer docFreq = orderedTerms.get(current); + return docFreq != null ? docFreq : 0; + } + + /** + * {@inheritDoc} + */ + public void close() { + // nothing to do here + } + + /** + * Sets the current field to the next term in this enum or to + * null if there is no next. + */ + private void getNext() { + current = it.hasNext() ? it.next() : null; + } + } + + public static class TermValueFactory { + + /** + * Creates a term value from the given string. This implementation + * simply returns the given string. Sub classes my apply some + * transformation to the string. + * + * @param s the string. + * @return the term value to use in the query. + */ + public String createValue(String s) { + return s; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/AndConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/AndConstraint.java new file mode 100644 index 00000000000..c1e763e7ba4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/AndConstraint.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.core.query.lucene.constraint; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.spi.Name; + +/** + * AndConstraint implements an AND constraint. + */ +public class AndConstraint implements Constraint { + + /** + * The left operand. + */ + private final Constraint left; + + /** + * The right operand. + */ + private final Constraint right; + + /** + * Creates a new AND constraint. + * + * @param left the left operand. + * @param right the right operand. + */ + public AndConstraint(Constraint left, Constraint right) { + this.left = left; + this.right = right; + } + + /** + * {@inheritDoc} + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) + throws IOException { + return left.evaluate(row, selectorNames, context) + && right.evaluate(row, selectorNames, context); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ChildNodeConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ChildNodeConstraint.java new file mode 100644 index 00000000000..3e61c5d7257 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ChildNodeConstraint.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.core.query.lucene.constraint; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.commons.query.qom.ChildNodeImpl; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.NodeImpl; + +/** + * ChildNodeConstraint implements a child node constraint. + */ +public class ChildNodeConstraint extends HierarchyConstraint { + + /** + * Creates a child node constraint from the given QOM + * constraint on the given selector. + * + * @param constraint the QOM child node constraint. + * @param selector the selector. + */ + public ChildNodeConstraint(ChildNodeImpl constraint, + SelectorImpl selector) { + super(constraint.getParentPath(), selector); + } + + /** + * {@inheritDoc} + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) + throws IOException { + ScoreNode sn = row[getSelectorIndex(selectorNames)]; + if (sn == null) { + return false; + } + SessionImpl session = context.getSession(); + NodeImpl parent; + try { + parent = (NodeImpl) session.getNodeById(sn.getNodeId()).getParent(); + } catch (RepositoryException e) { + return false; + } + return parent.getId().equals(getBaseNodeId(context)); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ComparisonConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ComparisonConstraint.java new file mode 100644 index 00000000000..8755bc6282a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ComparisonConstraint.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.core.query.lucene.constraint; + +import java.io.IOException; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.query.lucene.Util; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; + +/** + * ComparisonConstraint implements a comparison constraint. + */ +public class ComparisonConstraint extends SelectorBasedConstraint { + + /** + * The dynamic operand. + */ + private final DynamicOperand operand1; + + /** + * The operator. + */ + private final Operator operator; + + /** + * The static operand. + */ + private final Value operand2; + + /** + * Creates a new comparison constraint. + * + * @param operand1 the dynamic operand. + * @param operator the operator. + * @param operand2 the static operand. + * @param selector the selector for this constraint. + */ + public ComparisonConstraint(DynamicOperand operand1, + Operator operator, + Value operand2, + SelectorImpl selector) { + super(selector); + this.operand1 = operand1; + this.operator = operator; + this.operand2 = operand2; + } + + /** + * {@inheritDoc} + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) + throws IOException { + ScoreNode sn = row[getSelectorIndex(selectorNames)]; + if (sn == null) { + return false; + } + try { + Value[] values = operand1.getValues(sn, context); + for (Value value : values) { + if (evaluate(value)) { + return true; + } + } + } catch (RepositoryException e) { + throw Util.createIOException(e); + } + return false; + } + + /** + * Evaluates this constraint for the given dynamic operand value + * op1. + * + * @param op1 the current value of the dynamic operand. + * @return true if the given value satisfies the constraint. + * @throws RepositoryException if an error occurs while converting the + * values. + */ + protected boolean evaluate(Value op1) throws RepositoryException { + int c = Util.compare(op1, operand2); + if (operator == Operator.EQ) { + return c == 0; + } else if (operator == Operator.GT) { + return c > 0; + } else if (operator == Operator.GE) { + return c >= 0; + } else if (operator == Operator.LT) { + return c < 0; + } else if (operator == Operator.LE) { + return c <= 0; + } else if (operator == Operator.NE) { + return c != 0; + } else { + throw new UnsupportedOperationException( + "Unsupported comparison operator: " + operator); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/Constraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/Constraint.java new file mode 100644 index 00000000000..37a00b0a96b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/Constraint.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.core.query.lucene.constraint; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.spi.Name; + +/** + * Constraint defines an interface for a QOM constraint. + */ +public interface Constraint { + + /** + * Evaluates this constraint for the given row. + * + * @param row the current row of score nodes. + * @param selectorNames the selector names associated with row. + * @param context the evaluation context. + * @return true if the row satisfies the constraint, + * false otherwise. + * @throws IOException if an error occurs while evaluating the constraint. + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ConstraintBuilder.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ConstraintBuilder.java new file mode 100644 index 00000000000..8b34fe7fe83 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/ConstraintBuilder.java @@ -0,0 +1,297 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene.constraint; + +import java.net.URLDecoder; +import java.util.Map; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; +import javax.jcr.query.InvalidQueryException; + +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.core.query.lucene.LuceneQueryFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.query.qom.AndImpl; +import org.apache.jackrabbit.spi.commons.query.qom.BindVariableValueImpl; +import org.apache.jackrabbit.spi.commons.query.qom.ChildNodeImpl; +import org.apache.jackrabbit.spi.commons.query.qom.ComparisonImpl; +import org.apache.jackrabbit.spi.commons.query.qom.ConstraintImpl; +import org.apache.jackrabbit.spi.commons.query.qom.DefaultQOMTreeVisitor; +import org.apache.jackrabbit.spi.commons.query.qom.DescendantNodeImpl; +import org.apache.jackrabbit.spi.commons.query.qom.DynamicOperandImpl; +import org.apache.jackrabbit.spi.commons.query.qom.FullTextSearchImpl; +import org.apache.jackrabbit.spi.commons.query.qom.FullTextSearchScoreImpl; +import org.apache.jackrabbit.spi.commons.query.qom.LengthImpl; +import org.apache.jackrabbit.spi.commons.query.qom.LiteralImpl; +import org.apache.jackrabbit.spi.commons.query.qom.LowerCaseImpl; +import org.apache.jackrabbit.spi.commons.query.qom.NodeLocalNameImpl; +import org.apache.jackrabbit.spi.commons.query.qom.NodeNameImpl; +import org.apache.jackrabbit.spi.commons.query.qom.NotImpl; +import org.apache.jackrabbit.spi.commons.query.qom.OrImpl; +import org.apache.jackrabbit.spi.commons.query.qom.PropertyExistenceImpl; +import org.apache.jackrabbit.spi.commons.query.qom.PropertyValueImpl; +import org.apache.jackrabbit.spi.commons.query.qom.SameNodeImpl; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; +import org.apache.jackrabbit.spi.commons.query.qom.StaticOperandImpl; +import org.apache.jackrabbit.spi.commons.query.qom.UpperCaseImpl; + +/** + * ConstraintBuilder builds a {@link Constraint} from a tree of + * QOM constraints. + */ +public class ConstraintBuilder { + + /** + * Creates a {@link Constraint} from a QOM constraint. + * + * @param constraint the QOM constraint. + * @param bindVariableValues the map of bind variables and their respective + * value. + * @param selectors the selectors of the current query. + * @param factory the lucene query factory. + * @param vf the value factory of the current session. + * @return a {@link Constraint}. + * @throws RepositoryException if an error occurs while building the + * constraint. + */ + public static Constraint create(ConstraintImpl constraint, + Map bindVariableValues, + SelectorImpl[] selectors, + LuceneQueryFactory factory, + ValueFactory vf) + throws RepositoryException { + try { + return (Constraint) constraint.accept(new Visitor( + bindVariableValues, selectors, factory, vf), null); + } catch (RepositoryException e) { + throw e; + } catch (Exception e) { + throw new RepositoryException(e); + } + } + + /** + * A QOM tree visitor that translates the contraints. + */ + private static final class Visitor extends DefaultQOMTreeVisitor { + + /** + * The bind variables and their values. + */ + private final Map bindVariableValues; + + /** + * The selectors of the query. + */ + private final SelectorImpl[] selectors; + + /** + * The lucene query builder. + */ + private final LuceneQueryFactory factory; + + /** + * The value factory of the current session. + */ + private final ValueFactory vf; + + /** + * Creates a new visitor. + * + * @param bindVariableValues the bound values. + * @param selectors the selectors of the current query. + * @param factory the lucene query factory. + * @param vf the value factory of the current session. + */ + Visitor(Map bindVariableValues, + SelectorImpl[] selectors, + LuceneQueryFactory factory, + ValueFactory vf) { + this.bindVariableValues = bindVariableValues; + this.selectors = selectors; + this.factory = factory; + this.vf = vf; + } + + public Object visit(AndImpl node, Object data) throws Exception { + ConstraintImpl left = (ConstraintImpl) node.getConstraint1(); + ConstraintImpl right = (ConstraintImpl) node.getConstraint2(); + return new AndConstraint((Constraint) left.accept(this, null), + (Constraint) right.accept(this, null)); + } + + public Object visit(BindVariableValueImpl node, Object data) + throws Exception { + String name = node.getBindVariableName(); + Value value = bindVariableValues.get(name); + if (value != null) { + return value; + } else { + throw new RepositoryException( + "No value specified for bind variable " + name); + } + } + + public Object visit(ChildNodeImpl node, Object data) throws Exception { + return new ChildNodeConstraint(node, + getSelector(node.getSelectorQName())); + } + + public Object visit(ComparisonImpl node, Object data) throws Exception { + DynamicOperandImpl op1 = (DynamicOperandImpl) node.getOperand1(); + Operator operator = node.getOperatorInstance(); + StaticOperandImpl op2 = ((StaticOperandImpl) node.getOperand2()); + Value staticValue = (Value) op2.accept(this, null); + + DynamicOperand dynOp = (DynamicOperand) op1.accept(this, staticValue); + SelectorImpl selector = getSelector(op1.getSelectorQName()); + if (operator == Operator.LIKE) { + return new LikeConstraint(dynOp, staticValue, selector); + } else { + return new ComparisonConstraint( + dynOp, operator, staticValue, selector); + } + } + + public Object visit(DescendantNodeImpl node, Object data) + throws Exception { + return new DescendantNodeConstraint(node, + getSelector(node.getSelectorQName())); + } + + public Object visit(FullTextSearchImpl node, Object data) + throws Exception { + return new FullTextConstraint(node, + getSelector(node.getSelectorQName()), factory); + } + + public Object visit(FullTextSearchScoreImpl node, Object data) + throws Exception { + return new FullTextSearchScoreOperand(); + } + + public Object visit(LengthImpl node, Object data) throws Exception { + Value staticValue = (Value) data; + // make sure it can be converted to Long + try { + staticValue.getLong(); + } catch (ValueFormatException e) { + throw new InvalidQueryException("Static value " + + staticValue.getString() + " cannot be converted to a Long"); + } + PropertyValueImpl propValue = (PropertyValueImpl) node.getPropertyValue(); + return new LengthOperand((PropertyValueOperand) propValue.accept(this, null)); + } + + public Object visit(LiteralImpl node, Object data) throws Exception { + return node.getLiteralValue(); + } + + public Object visit(LowerCaseImpl node, Object data) throws Exception { + DynamicOperandImpl operand = (DynamicOperandImpl) node.getOperand(); + return new LowerCaseOperand((DynamicOperand) operand.accept(this, data)); + } + + public Object visit(NodeLocalNameImpl node, Object data) throws Exception { + return new NodeLocalNameOperand(); + } + + public Object visit(NodeNameImpl node, Object data) throws Exception { + Value staticValue = (Value) data; + switch (staticValue.getType()) { + // STRING, PATH and URI may be convertable to a NAME -> check + case PropertyType.STRING: + case PropertyType.PATH: + case PropertyType.URI: + // make sure static value is valid NAME + try { + String s = staticValue.getString(); + if (staticValue.getType() == PropertyType.URI) { + if (s.startsWith("./")) { + s = s.substring(2); + } + // need to decode + s = URLDecoder.decode(s, "UTF-8"); + } + vf.createValue(s, PropertyType.NAME); + } catch (ValueFormatException e) { + throw new InvalidQueryException("Value " + + staticValue.getString() + + " cannot be converted into NAME"); + } + break; + // the following types cannot be converted to NAME + case PropertyType.DATE: + case PropertyType.DOUBLE: + case PropertyType.DECIMAL: + case PropertyType.LONG: + case PropertyType.BOOLEAN: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + throw new InvalidQueryException(staticValue.getString() + + " cannot be converted into a NAME value"); + } + + return new NodeNameOperand(); + } + + public Object visit(NotImpl node, Object data) throws Exception { + ConstraintImpl c = (ConstraintImpl) node.getConstraint(); + return new NotConstraint((Constraint) c.accept(this, null)); + } + + public Object visit(OrImpl node, Object data) throws Exception { + ConstraintImpl left = (ConstraintImpl) node.getConstraint1(); + ConstraintImpl right = (ConstraintImpl) node.getConstraint2(); + return new OrConstraint((Constraint) left.accept(this, null), + (Constraint) right.accept(this, null)); + } + + public Object visit(PropertyExistenceImpl node, Object data) + throws Exception { + return new PropertyExistenceConstraint(node, + getSelector(node.getSelectorQName()), factory); + } + + public Object visit(PropertyValueImpl node, Object data) throws Exception { + return new PropertyValueOperand(node); + } + + public Object visit(SameNodeImpl node, Object data) throws Exception { + return new SameNodeConstraint(node, + getSelector(node.getSelectorQName())); + } + + public Object visit(UpperCaseImpl node, Object data) throws Exception { + DynamicOperandImpl operand = (DynamicOperandImpl) node.getOperand(); + return new UpperCaseOperand((DynamicOperand) operand.accept(this, data)); + } + + private SelectorImpl getSelector(Name name) { + for (SelectorImpl selector : selectors) { + if (selector.getSelectorQName().equals(name)) { + return selector; + } + } + return null; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/DescendantNodeConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/DescendantNodeConstraint.java new file mode 100644 index 00000000000..7a1332fc36f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/DescendantNodeConstraint.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.core.query.lucene.constraint; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.commons.query.qom.DescendantNodeImpl; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; + +/** + * DescendantNodeConstraint implements a descendant node + * constraint. + */ +public class DescendantNodeConstraint extends HierarchyConstraint { + + /** + * Creates a new descendant node constraint from the given QOM constraint. + * + * @param constraint the QOM descendant node constraint. + * @param selector the selector. + */ + public DescendantNodeConstraint(DescendantNodeImpl constraint, + SelectorImpl selector) { + super(constraint.getAncestorPath(), selector); + } + + /** + * {@inheritDoc} + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) + throws IOException { + NodeId baseId = getBaseNodeId(context); + if (baseId == null) { + return false; + } + ScoreNode sn = row[getSelectorIndex(selectorNames)]; + if (sn == null) { + return false; + } + NodeId id = sn.getNodeId(); + SessionImpl session = context.getSession(); + try { + NodeImpl parent = session.getNodeById(id); + for (;;) { + // throws exception if there is no parent + parent = (NodeImpl) parent.getParent(); + if (parent.getId().equals(baseId)) { + return true; + } + } + } catch (RepositoryException e) { + return false; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/DynamicOperand.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/DynamicOperand.java new file mode 100644 index 00000000000..b81f50d8a70 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/DynamicOperand.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.core.query.lucene.constraint; + +import javax.jcr.Value; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; + +/** + * DynamicOperand is a base class for dynamic operands. + */ +public abstract class DynamicOperand { + + /** + * An empty {@link Value} array. + */ + protected static final Value[] EMPTY = new Value[0]; + + /** + * Returns the values for the given score node sn of this + * dynamic operand. If there are no values for the given score node, then + * an empty array is returned. + * + * @param sn the current score node. + * @param context the evaluation context. + * @return the values for the given score node. + * @throws RepositoryException if an error occurs while retrieving the value. + */ + public abstract Value[] getValues(ScoreNode sn, EvaluationContext context) + throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/EvaluationContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/EvaluationContext.java new file mode 100644 index 00000000000..05398dfa194 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/EvaluationContext.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.core.query.lucene.constraint; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.QueryHits; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.lucene.search.Query; +import org.apache.lucene.index.IndexReader; + +/** + * EvaluationContext defines a context with various resources that + * are needed for constraint evaluation. + */ +public interface EvaluationContext { + + /** + * Evaluates the given lucene query and returns the query + * hits. + * + * @param query the lucene query to evaluate. + * @return the query hits for the given query. + * @throws IOException if an error occurs while reading from the index. + */ + public QueryHits evaluate(Query query) throws IOException; + + /** + * @return the index reader. + */ + public IndexReader getIndexReader(); + + /** + * @return the session that executes the query. + */ + public SessionImpl getSession(); + + /** + * @return the shared item state manager of the current workspace. + */ + public ItemStateManager getItemStateManager(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/FullTextConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/FullTextConstraint.java new file mode 100644 index 00000000000..c76c7189773 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/FullTextConstraint.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.core.query.lucene.constraint; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.commons.query.qom.FullTextSearchImpl; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; +import org.apache.jackrabbit.core.query.lucene.LuceneQueryFactory; + +/** + * FullTextConstraint implements a full text search constraint. + */ +public class FullTextConstraint extends QueryConstraint { + + /** + * Creates a new full text search constraint. + * + * @param fts the QOM constraint. + * @param selector the selector for this constraint. + * @param factory the lucene query factory. + * @throws RepositoryException if an error occurs while building the query. + */ + public FullTextConstraint(FullTextSearchImpl fts, + SelectorImpl selector, + LuceneQueryFactory factory) + throws RepositoryException { + super(factory.create(fts), selector, factory); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/FullTextSearchScoreOperand.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/FullTextSearchScoreOperand.java new file mode 100644 index 00000000000..b376c61fbf0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/FullTextSearchScoreOperand.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.core.query.lucene.constraint; + +import javax.jcr.Value; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; + +/** + * FullTextSearchScoreOperand implements a full text search score + * operand. + */ +public class FullTextSearchScoreOperand extends DynamicOperand { + + /** + * {@inheritDoc} + */ + public Value[] getValues(ScoreNode sn, EvaluationContext context) + throws RepositoryException { + return new Value[]{context.getSession().getValueFactory().createValue(sn.getScore())}; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/HierarchyConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/HierarchyConstraint.java new file mode 100644 index 00000000000..6c3e6ee8ed9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/HierarchyConstraint.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.core.query.lucene.constraint; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.NodeImpl; + +/** + * HierarchyConstraint is a base class for hierarchy related + * constraints. + */ +public abstract class HierarchyConstraint extends SelectorBasedConstraint { + + /** + * A base path. + */ + private final String path; + + /** + * The id of the node at {@link #path}. + */ + private NodeId id; + + /** + * Creates a new hierarchy constraint with the given base + * path. + * + * @param path the base path. + * @param selector the selector this constraint is placed on. + */ + public HierarchyConstraint(String path, SelectorImpl selector) { + super(selector); + this.path = path; + } + + /** + * Returns the id of the base node or null if there is no node + * at the base path. + * + * @param context the evaluation context. + * @return the id or null if it doesn't exist. + */ + protected final NodeId getBaseNodeId(EvaluationContext context) { + if (id == null) { + try { + NodeImpl node = (NodeImpl) context.getSession().getNode(path); + id = (NodeId) node.getId(); + } catch (RepositoryException e) { + return null; + } + } + return id; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LengthOperand.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LengthOperand.java new file mode 100644 index 00000000000..a165828009a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LengthOperand.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.core.query.lucene.constraint; + +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.query.lucene.Util; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.value.ValueFactoryImpl; +import org.apache.jackrabbit.spi.QValueFactory; + +/** + * LengthOperand implements a length operand. + */ +public class LengthOperand extends DynamicOperand { + + /** + * The property value operand for which to return the length. + */ + private final PropertyValueOperand property; + + /** + * Creates a new length operand. + * + * @param property the operand for which to return the length. + */ + public LengthOperand(PropertyValueOperand property) { + super(); + this.property = property; + } + + /** + * {@inheritDoc} + */ + public Value[] getValues(ScoreNode sn, EvaluationContext context) + throws RepositoryException { + PropertyState ps = property.getPropertyState(sn, context); + if (ps == null) { + return EMPTY; + } else { + ValueFactoryImpl vf = (ValueFactoryImpl) context.getSession().getValueFactory(); + QValueFactory qvf = vf.getQValueFactory(); + InternalValue[] values = ps.getValues(); + Value[] lengths = new Value[values.length]; + for (int i = 0; i < lengths.length; i++) { + long len; + int type = values[i].getType(); + if (type == PropertyType.NAME) { + len = vf.createValue(qvf.create(values[i].getName())).getString().length(); + } else if (type == PropertyType.PATH) { + len = vf.createValue(qvf.create(values[i].getPath())).getString().length(); + } else { + len = Util.getLength(values[i]); + } + lengths[i] = vf.createValue(len); + } + return lengths; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LikeConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LikeConstraint.java new file mode 100644 index 00000000000..d60db7b4d46 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LikeConstraint.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.core.query.lucene.constraint; + +import java.util.regex.Matcher; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.core.query.lucene.Util; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; + +/** + * LikeConstraint implements a like constraint. + */ +public class LikeConstraint extends ComparisonConstraint { + + /** + * A regular expression matcher for the like constraint. + */ + private final Matcher matcher; + + /** + * Creates a new like constraint. + * + * @param operand1 the dynamic operand. + * @param operand2 the static operand. + * @param selector the selector for the dynamic operand. + * @throws RepositoryException if an error occurs reading the string value + * from the static operand. + */ + public LikeConstraint(DynamicOperand operand1, + Value operand2, + SelectorImpl selector) throws RepositoryException { + super(operand1, Operator.LIKE, operand2, selector); + this.matcher = Util.createRegexp(operand2.getString()).matcher(""); + } + + /** + * {@inheritDoc} + */ + protected boolean evaluate(Value op1) throws RepositoryException { + return matcher.reset(op1.getString()).matches(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LowerCaseOperand.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LowerCaseOperand.java new file mode 100644 index 00000000000..ec8e185d9ad --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/LowerCaseOperand.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.core.query.lucene.constraint; + +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; + +/** + * LowerCaseOperand implements a lower case operand. + */ +public class LowerCaseOperand extends DynamicOperand { + + /** + * The dynamic operand for which to lower case the value. + */ + private final DynamicOperand operand; + + /** + * Creates a new lower case operand. + * + * @param operand the operand to lower case the value. + */ + public LowerCaseOperand(DynamicOperand operand) { + super(); + this.operand = operand; + } + + /** + * {@inheritDoc} + */ + public Value[] getValues(ScoreNode sn, EvaluationContext context) + throws RepositoryException { + ValueFactory vf = context.getSession().getValueFactory(); + Value[] values = operand.getValues(sn, context); + for (int i = 0; i < values.length; i++) { + values[i] = vf.createValue(values[i].getString().toLowerCase()); + } + return values; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NodeLocalNameOperand.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NodeLocalNameOperand.java new file mode 100644 index 00000000000..781d2493470 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NodeLocalNameOperand.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.core.query.lucene.constraint; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.util.Text; + +/** + * NodeLocalNameOperand implements a node local name operand. + */ +public class NodeLocalNameOperand extends DynamicOperand { + + /** + * Returns the local name of the node denoted by the given score node + * sn. + * + * @param sn the score node. + * @param context the evaluation context. + * @return the local node name. + * @throws RepositoryException if an error occurs while reading the local + * name. + */ + public Value[] getValues(ScoreNode sn, EvaluationContext context) + throws RepositoryException { + SessionImpl session = context.getSession(); + try { + String name = session.getNodeById(sn.getNodeId()).getName(); + return new Value[]{session.getValueFactory().createValue( + Text.getLocalName(name))}; + } catch (ItemNotFoundException e) { + // access denied to score node + return new Value[0]; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NodeNameOperand.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NodeNameOperand.java new file mode 100644 index 00000000000..7dd80efa613 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NodeNameOperand.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.core.query.lucene.constraint; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; + +/** + * NodeNameOperand implements a node name operand. + */ +public class NodeNameOperand extends DynamicOperand { + + /** + * Returns the name of the node denoted by the given score node + * sn. + * + * @param sn the score node. + * @param context the evaluation context. + * @return the node name. + * @throws RepositoryException if an error occurs while reading the name. + */ + public Value[] getValues(ScoreNode sn, EvaluationContext context) + throws RepositoryException { + SessionImpl session = context.getSession(); + try { + String name = session.getNodeById(sn.getNodeId()).getName(); + return new Value[]{session.getValueFactory().createValue(name, PropertyType.NAME)}; + } catch (ItemNotFoundException e) { + // access denied to score node + return new Value[0]; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NotConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NotConstraint.java new file mode 100644 index 00000000000..4b08adf08c8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/NotConstraint.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.core.query.lucene.constraint; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.spi.Name; + +/** + * NotConstraint implements a NOT constraint. + */ +public class NotConstraint implements Constraint { + + /** + * The single operand. + */ + private final Constraint constraint; + + /** + * Creates a new NOT constraint with the given constraint as + * its operand. + * + * @param constraint the operand. + */ + public NotConstraint(Constraint constraint) { + this.constraint = constraint; + } + + /** + * {@inheritDoc} + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) + throws IOException { + return !constraint.evaluate(row, selectorNames, context); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/OrConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/OrConstraint.java new file mode 100644 index 00000000000..aadcae8db9d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/OrConstraint.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.core.query.lucene.constraint; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.spi.Name; + +/** + * OrConstraint implements an OR constraint. + */ +public class OrConstraint implements Constraint { + + /** + * The left operand. + */ + private final Constraint left; + + /** + * The right operand. + */ + private final Constraint right; + + /** + * Creates a new OR constraint. + * + * @param left the left operand. + * @param right the right operand. + */ + public OrConstraint(Constraint left, Constraint right) { + this.left = left; + this.right = right; + } + + /** + * {@inheritDoc} + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) + throws IOException { + return left.evaluate(row, selectorNames, context) + || right.evaluate(row, selectorNames, context); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/PropertyExistenceConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/PropertyExistenceConstraint.java new file mode 100644 index 00000000000..7edab3a7f20 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/PropertyExistenceConstraint.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.core.query.lucene.constraint; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.commons.query.qom.PropertyExistenceImpl; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; +import org.apache.jackrabbit.core.query.lucene.LuceneQueryFactory; + +/** + * PropertyExistenceConstraint implements a property existence + * constraint. + */ +public class PropertyExistenceConstraint extends QueryConstraint { + + /** + * Creates a new property existence constraint. + * + * @param prop the QOM property existence constraint. + * @param selector the selector for this constraint. + * @param factory the lucene query factory. + * @throws RepositoryException if an error occurs while creating a lucene + * query for this constraint. + */ + public PropertyExistenceConstraint(PropertyExistenceImpl prop, + SelectorImpl selector, + LuceneQueryFactory factory) + throws RepositoryException { + super(factory.create(prop), selector, factory); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/PropertyValueOperand.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/PropertyValueOperand.java new file mode 100644 index 00000000000..9cf8ab07d7f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/PropertyValueOperand.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.core.query.lucene.constraint; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.spi.commons.query.qom.PropertyValueImpl; + +/** + * PropertyValueOperand implements a property value operand. + */ +public class PropertyValueOperand extends DynamicOperand { + + /** + * The QOM operand. + */ + private final PropertyValueImpl operand; + + /** + * Creates a new property value operand. + * + * @param operand the QOM operand. + */ + public PropertyValueOperand(PropertyValueImpl operand) { + super(); + this.operand = operand; + } + + /** + * Returns the property state for the given score node or null + * if none exists. + * + * @param sn the current score node. + * @param context the evaluation context. + * @return the property state or null. + * @throws RepositoryException if an error occurs while reading. + */ + public final PropertyState getPropertyState(ScoreNode sn, + EvaluationContext context) + throws RepositoryException { + ItemStateManager ism = context.getItemStateManager(); + PropertyId propId = new PropertyId(sn.getNodeId(), operand.getPropertyQName()); + try { + return (PropertyState) ism.getItemState(propId); + } catch (NoSuchItemStateException e) { + return null; + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * Returns the property for the given score node or null if + * none exists. + * + * @param sn the current score node. + * @param context the evaluation context. + * @return the property or null. + * @throws RepositoryException if an error occurs while reading. + */ + public final Property getProperty(ScoreNode sn, + EvaluationContext context) + throws RepositoryException { + SessionImpl session = context.getSession(); + try { + Node n = session.getNodeById(sn.getNodeId()); + return n.getProperty(operand.getPropertyName()); + } catch (ItemNotFoundException e) { + // access denied to score node + return null; + } catch (PathNotFoundException e) { + // property not found + return null; + } + } + + /** + * {@inheritDoc} + */ + public Value[] getValues(ScoreNode sn, EvaluationContext context) + throws RepositoryException { + Property prop = getProperty(sn, context); + if (prop == null) { + return EMPTY; + } else { + if (prop.isMultiple()) { + return prop.getValues(); + } else { + return new Value[]{prop.getValue()}; + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/QueryConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/QueryConstraint.java new file mode 100644 index 00000000000..04fceac72da --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/QueryConstraint.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.core.query.lucene.constraint; + +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.query.lucene.QueryHits; +import org.apache.jackrabbit.core.query.lucene.Util; +import org.apache.jackrabbit.core.query.lucene.LuceneQueryFactory; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.index.IndexReader; + +/** + * QueryConstraint implements a constraint that is based on a + * lucene query. + */ +public abstract class QueryConstraint extends SelectorBasedConstraint { + + /** + * The constraint query. + */ + private final Query constraint; + + /** + * The lucene query factory. + */ + private final LuceneQueryFactory factory; + + /** + * Map of document numbers with their respective score value that match the + * query constraint. + */ + private Map matches; + + /** + * Creates a new query constraint using the given lucene query. + * + * @param constraint the lucene query constraint. + * @param selector the selector for this constraint. + * @param factory the lucene query factory. + */ + public QueryConstraint(Query constraint, + SelectorImpl selector, + LuceneQueryFactory factory) { + super(selector); + this.constraint = constraint; + this.factory = factory; + } + + //----------------------------< Constraint >-------------------------------- + + /** + * {@inheritDoc} + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) + throws IOException { + ScoreNode sn = row[getSelectorIndex(selectorNames)]; + return sn != null && evaluate(sn, context); + } + + //--------------------------------< internal >------------------------------ + + /** + * Evaluates this constraint for the given score node sn. + * + * @param sn the current score node. + * @param context the evaluation context. + * @return true if this constraint is satisfied for the given + * score node sn; false otherwise. + * @throws IOException if an error occurs while reading from the index. + */ + private boolean evaluate(ScoreNode sn, EvaluationContext context) + throws IOException { + initMatches(context); + Float score = matches.get(sn.getDoc(context.getIndexReader())); + if (score != null) { + sn.setScore(score); + } + return score != null; + } + + /** + * Initializes the matches for the constraint query. If the matches are + * already initialized then this method returns immediately. + * + * @param context the evaluation context. + * @throws IOException if an error occurs while reading from the index. + */ + private void initMatches(EvaluationContext context) throws IOException { + if (matches == null) { + Query selectorQuery; + BooleanQuery and = new BooleanQuery(); + try { + selectorQuery = factory.create(getSelector()); + and.add(selectorQuery, BooleanClause.Occur.MUST); + and.add(constraint, BooleanClause.Occur.MUST); + } catch (RepositoryException e) { + throw Util.createIOException(e); + } + + IndexReader reader = context.getIndexReader(); + QueryHits hits = context.evaluate(and); + try { + matches = new HashMap(); + ScoreNode sn; + while ((sn = hits.nextScoreNode()) != null) { + matches.put(sn.getDoc(reader), sn.getScore()); + } + } finally { + hits.close(); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/SameNodeConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/SameNodeConstraint.java new file mode 100644 index 00000000000..25a223e316e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/SameNodeConstraint.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.core.query.lucene.constraint; + +import java.io.IOException; + +import org.apache.jackrabbit.spi.commons.query.qom.SameNodeImpl; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; + +/** + * SameNodeConstraint implements a same node constraint. + */ +public class SameNodeConstraint extends HierarchyConstraint { + + /** + * Creates a same node constraint. + * + * @param constraint the QOM constraint. + * @param selector the selector for this constraint. + */ + public SameNodeConstraint(SameNodeImpl constraint, SelectorImpl selector) { + super(constraint.getPath(), selector); + } + + /** + * {@inheritDoc} + */ + public boolean evaluate(ScoreNode[] row, + Name[] selectorNames, + EvaluationContext context) + throws IOException { + ScoreNode sn = row[getSelectorIndex(selectorNames)]; + if (sn == null) { + return false; + } else { + return sn.getNodeId().equals(getBaseNodeId(context)); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/SelectorBasedConstraint.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/SelectorBasedConstraint.java new file mode 100644 index 00000000000..bc51e6088b6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/SelectorBasedConstraint.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.core.query.lucene.constraint; + +import java.util.Arrays; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.query.qom.SelectorImpl; + +/** + * SelectorBasedConstraint implements a constraint that is based + * on a named selector. + */ +public abstract class SelectorBasedConstraint implements Constraint { + + /** + * The selector this constrained is based on. + */ + private final SelectorImpl selector; + + /** + * Cached selector index. Initially set to -1. + */ + private int selectorIndex = -1; + + /** + * Creates a new constraint based on the given selector. + * + * @param selector the selector this constraint is based on. + */ + public SelectorBasedConstraint(SelectorImpl selector) { + this.selector = selector; + } + + /** + * Returns the selector index of this constraint. + * + * @param names the selector names. + * @return the selector index. + */ + protected int getSelectorIndex(Name[] names) { + if (selectorIndex == -1) { + selectorIndex = Arrays.asList(names).indexOf( + selector.getSelectorQName()); + } + return selectorIndex; + } + + /** + * @return the selector of this constraint. + */ + protected SelectorImpl getSelector() { + return selector; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/UpperCaseOperand.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/UpperCaseOperand.java new file mode 100644 index 00000000000..e41ab4718e0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/constraint/UpperCaseOperand.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.core.query.lucene.constraint; + +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; + +/** + * UpperCaseOperand implements an upper case operand. + */ +public class UpperCaseOperand extends DynamicOperand { + + /** + * The dynamic operand for which to lower case the value. + */ + private final DynamicOperand operand; + + /** + * Creates a new upper case operand. + * + * @param operand the operand to upper case the value. + */ + public UpperCaseOperand(DynamicOperand operand) { + super(); + this.operand = operand; + } + + /** + * {@inheritDoc} + */ + public Value[] getValues(ScoreNode sn, EvaluationContext context) + throws RepositoryException { + ValueFactory vf = context.getSession().getValueFactory(); + Value[] values = operand.getValues(sn, context); + for (int i = 0; i < values.length; i++) { + values[i] = vf.createValue(values[i].getString().toUpperCase()); + } + return values; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/DirectoryManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/DirectoryManager.java new file mode 100644 index 00000000000..4b267c05f1e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/DirectoryManager.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.core.query.lucene.directory; + +import org.apache.lucene.store.Directory; +import org.apache.jackrabbit.core.query.lucene.SearchIndex; + +import java.io.IOException; + +/** + * DirectoryManager defines an interface for managing directory + * instances used by the search index. + */ +public interface DirectoryManager { + + /** + * Initializes the directory manager with a reference to the search index. + * + * @param handler the query handler implementation. + * @throws IOException if an error occurs while initializing the directory + * manager. + */ + void init(SearchIndex handler) throws IOException; + + /** + * Checks if there exists a directory with the given name. + * + * @param name the name of a directory. + * @return true if the directory exists; false + * otherwise. + * @throws IOException if an error occurs while looking up directories. + */ + boolean hasDirectory(String name) throws IOException; + + /** + * Gets the directory with the given name. If the directory + * does not yet exist then it will be created. + * + * @param name the name of a directory. + * @return the directory. + * @throws IOException if an error occurs while getting or creating the + * directory. + */ + Directory getDirectory(String name) throws IOException; + + /** + * Returns the names of the currently available directories. + * + * @return names of the currently available directories. + * @throws IOException if an error occurs while retrieving the directory + * names. + */ + String[] getDirectoryNames() throws IOException; + + /** + * Deletes the directory with the given name. + * + * @param name the name of the directory to delete. + * @return true if the directory could be deleted successfully, + * false otherwise. This method also returns + * false when the directory with the given + * name does not exist. + */ + boolean delete(String name); + + /** + * Renames a directory. + * + * @param from the name of the directory to rename. + * @param to the new name for the directory. + * @return true if the directory was successfully renamed. + * Returns false if there is no directory with name + * from or there already exists a directory with name + * to or an error occurs while renaming the directory. + */ + boolean rename(String from, String to); + + /** + * Frees resources associated with this directory manager. + */ + void dispose(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/FSDirectoryManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/FSDirectoryManager.java new file mode 100644 index 00000000000..d2cbe0d74a4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/FSDirectoryManager.java @@ -0,0 +1,303 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene.directory; + +import org.apache.jackrabbit.core.query.lucene.IOCounters; +import org.apache.jackrabbit.core.query.lucene.SearchIndex; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.store.IndexOutput; +import org.apache.lucene.store.Lock; +import org.apache.lucene.store.LockFactory; +import org.apache.lucene.store.NativeFSLockFactory; +import org.apache.lucene.store.SimpleFSDirectory; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; + +/** + * FSDirectoryManager implements a directory manager for + * {@link FSDirectory} instances. + */ +public class FSDirectoryManager implements DirectoryManager { + + /** + * The base directory. + */ + private File baseDir; + + private boolean useSimpleFSDirectory; + + /** + * {@inheritDoc} + */ + public void init(SearchIndex handler) throws IOException { + baseDir = new File(handler.getPath()); + useSimpleFSDirectory = handler.isUseSimpleFSDirectory(); + } + + /** + * {@inheritDoc} + */ + public boolean hasDirectory(String name) throws IOException { + return new File(baseDir, name).exists(); + } + + /** + * {@inheritDoc} + */ + public Directory getDirectory(String name) + throws IOException { + File dir; + if (name.equals(".")) { + dir = baseDir; + } else { + dir = new File(baseDir, name); + } + return new FSDir(dir, useSimpleFSDirectory); + } + + /** + * {@inheritDoc} + */ + public String[] getDirectoryNames() throws IOException { + File[] dirs = baseDir.listFiles(new FileFilter() { + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + }); + if (dirs != null) { + String[] names = new String[dirs.length]; + for (int i = 0; i < dirs.length; i++) { + names[i] = dirs[i].getName(); + } + return names; + } else { + throw new IOException("listFiles for " + baseDir.getPath() + " returned null"); + } + } + + /** + * {@inheritDoc} + */ + public boolean delete(String name) { + File directory = new File(baseDir, name); + // trivial if it does not exist anymore + if (!directory.exists()) { + return true; + } + // delete files first + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (!file.delete()) { + return false; + } + } + } else { + return false; + } + // now delete directory itself + return directory.delete(); + } + + /** + * {@inheritDoc} + */ + public boolean rename(String from, String to) { + File src = new File(baseDir, from); + File dest = new File(baseDir, to); + return src.renameTo(dest); + } + + /** + * {@inheritDoc} + */ + public void dispose() { + } + + //-----------------------< internal >--------------------------------------- + + private static final class FSDir extends Directory { + + private static final FileFilter FILTER = new FileFilter() { + public boolean accept(File pathname) { + return pathname.isFile(); + } + }; + + private final FSDirectory directory; + + public FSDir(File dir, boolean simpleFS) throws IOException { + if (!dir.mkdirs()) { + if (!dir.isDirectory()) { + throw new IOException("Unable to create directory: '" + dir + "'"); + } + } + LockFactory lockFactory = new NativeFSLockFactory(dir); + if (simpleFS) { + directory = new SimpleFSDirectory(dir, lockFactory); + } else { + directory = FSDirectory.open(dir, lockFactory); + } + } + + @Override + public String[] listAll() throws IOException { + File[] files = directory.getDirectory().listFiles(FILTER); + if (files == null) { + return null; + } + String[] names = new String[files.length]; + for (int i = 0; i < names.length; i++) { + names[i] = files[i].getName(); + } + return names; + } + + @Override + public boolean fileExists(String name) throws IOException { + return directory.fileExists(name); + } + + @Override + public long fileModified(String name) throws IOException { + return directory.fileModified(name); + } + + @Override + public void touchFile(String name) throws IOException { + directory.touchFile(name); + } + + @Override + public void deleteFile(String name) throws IOException { + directory.deleteFile(name); + } + + @Override + public long fileLength(String name) throws IOException { + return directory.fileLength(name); + } + + @Override + public IndexOutput createOutput(String name) throws IOException { + return directory.createOutput(name); + } + + @Override + public IndexInput openInput(String name) throws IOException { + IndexInput in = directory.openInput(name); + return new IndexInputLogWrapper(name, in); + } + + @Override + public void close() throws IOException { + directory.close(); + } + + @Override + public IndexInput openInput(String name, int bufferSize) + throws IOException { + IndexInput in = directory.openInput(name, bufferSize); + return new IndexInputLogWrapper(name, in); + } + + @Override + public Lock makeLock(String name) { + return directory.makeLock(name); + } + + @Override + public void clearLock(String name) throws IOException { + directory.clearLock(name); + } + + @Override + public void setLockFactory(LockFactory lockFactory) throws IOException { + directory.setLockFactory(lockFactory); + } + + @Override + public LockFactory getLockFactory() { + return directory.getLockFactory(); + } + + @Override + public String getLockID() { + return directory.getLockID(); + } + + public String toString() { + return getClass().getName() + '@' + directory; + } + } + + /** + * Implements an index input wrapper that logs the number of time bytes + * are read from storage. + */ + private static final class IndexInputLogWrapper extends IndexInput { + + private IndexInput in; + + IndexInputLogWrapper(String name, IndexInput in) { + super(name); + this.in = in; + } + + @Override + public byte readByte() throws IOException { + return in.readByte(); + } + + @Override + public void readBytes(byte[] b, int offset, int len) throws IOException { + IOCounters.incrRead(); + in.readBytes(b, offset, len); + } + + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public long getFilePointer() { + return in.getFilePointer(); + } + + @Override + public void seek(long pos) throws IOException { + in.seek(pos); + } + + @Override + public long length() { + return in.length(); + } + + @Override + public Object clone() { + IndexInputLogWrapper clone = (IndexInputLogWrapper) super.clone(); + clone.in = (IndexInput) in.clone(); + return clone; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/IndexInputStream.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/IndexInputStream.java new file mode 100644 index 00000000000..128e022669b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/IndexInputStream.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.core.query.lucene.directory; + +import org.apache.lucene.store.IndexInput; + +import java.io.InputStream; +import java.io.IOException; + +/** + * IndexInputStream implements an {@link InputStream} that wraps + * a lucene {@link IndexInput}. + */ +public class IndexInputStream extends InputStream { + + /** + * The underlying index input. + */ + private final IndexInput in; + + /** + * The length of the index input. + */ + private final long len; + + /** + * The position where the next read will occur. + */ + private long pos; + + /** + * Creates a new index input stream wrapping the given lucene index + * input. + * + * @param input the index input to wrap. + */ + public IndexInputStream(IndexInput input) { + this.in = input; + this.len = input.length(); + } + + /** + * {@inheritDoc} + */ + public int read() throws IOException { + byte[] buf = new byte[1]; + if (read(buf, 0, 1) == -1) { + return -1; + } else { + return buf[0] & 0xff; + } + } + + /** + * {@inheritDoc} + */ + public int read(byte b[], int off, int len) throws IOException { + if (pos >= this.len) { + // EOF + return -1; + } + int num = (int) Math.min(len - off, this.len - pos); + in.readBytes(b, off, num); + pos += num; + return num; + } + + /** + * {@inheritDoc} + *

    + * Closes the underlying index input. + */ + public void close() throws IOException { + in.close(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/IndexOutputStream.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/IndexOutputStream.java new file mode 100644 index 00000000000..917bc42a894 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/IndexOutputStream.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.core.query.lucene.directory; + +import java.io.OutputStream; +import java.io.IOException; + +import org.apache.lucene.store.IndexOutput; + +/** + * IndexOutputStream wraps an {@link IndexOutput} and exposes it + * as a regular {@link OutputStream}. + */ +public class IndexOutputStream extends OutputStream { + + /** + * The underlying index output. + */ + private final IndexOutput out; + + /** + * Creates a new index output stream and wraps the given + * output. Bytes will always be written at the end of the + * output. + * + * @param output the lucene index output. + * @throws IOException if an error occurs while seeking to the end of the + * index output. + */ + public IndexOutputStream(IndexOutput output) + throws IOException { + this.out = output; + this.out.seek(output.length()); + } + + /** + * {@inheritDoc} + */ + public void write(int b) throws IOException { + byte[] buf = new byte[]{(byte) (b & 0xff)}; + write(buf, 0, 1); + } + + /** + * {@inheritDoc} + */ + public void write(byte b[], int off, int len) throws IOException { + out.writeBytes(b, off, len); + } + + /** + * {@inheritDoc} + *

    + * Flushes the underlying index output. + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * {@inheritDoc} + *

    + * Closes the underlying index output. + */ + public void close() throws IOException { + out.close(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/RAMDirectoryManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/RAMDirectoryManager.java new file mode 100644 index 00000000000..92ae7ef0d9b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/directory/RAMDirectoryManager.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.core.query.lucene.directory; + +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.RAMDirectory; +import org.apache.jackrabbit.core.query.lucene.SearchIndex; + +import java.util.Map; +import java.util.HashMap; +import java.io.IOException; + +/** + * RAMDirectoryManager implements a directory manager for + * {@link RAMDirectory} instances. + */ +public class RAMDirectoryManager implements DirectoryManager { + + /** + * Map of directories. Key=String(directory name), Value=Directory. + */ + private final Map directories = new HashMap(); + + /** + * {@inheritDoc} + */ + public void init(SearchIndex handler) throws IOException { + } + + /** + * {@inheritDoc} + */ + public boolean hasDirectory(String name) throws IOException { + synchronized (directories) { + return directories.containsKey(name); + } + } + + /** + * {@inheritDoc} + */ + public Directory getDirectory(String name) { + synchronized (directories) { + Directory dir = directories.get(name); + if (dir == null) { + dir = new RAMDirectory(); + directories.put(name, dir); + } + return dir; + } + } + + /** + * {@inheritDoc} + */ + public String[] getDirectoryNames() throws IOException { + synchronized (directories) { + return directories.keySet().toArray( + new String[directories.size()]); + } + } + + /** + * {@inheritDoc} + */ + public boolean delete(String name) { + synchronized (directories) { + directories.remove(name); + } + return true; + } + + /** + * {@inheritDoc} + */ + public boolean rename(String from, String to) { + synchronized (directories) { + if (directories.containsKey(to)) { + return false; + } + Directory dir = directories.remove(from); + if (dir == null) { + return false; + } + directories.put(to, dir); + return true; + } + } + + /** + * {@inheritDoc} + */ + public void dispose() { + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/AbstractHitCollector.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/AbstractHitCollector.java new file mode 100644 index 00000000000..d8e1d3b0bed --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/AbstractHitCollector.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.core.query.lucene.hits; + +import java.io.IOException; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.Scorer; + +/** + * Collector implementation which simply provides the collection + * of re-based doc base with scorer. + */ +public abstract class AbstractHitCollector extends Collector { + protected int base = 0; + protected Scorer scorer = null; + + @Override + public void setNextReader(IndexReader reader, int docBase) throws IOException { + base = docBase; + } + + @Override + public void setScorer(Scorer scorer) throws IOException { + this.scorer = scorer; + } + + @Override + public void collect(int doc) throws IOException { + collect(base + doc, scorer.score()); + } + + /** + * Called once for every document matching a query, with the re-based document + * number and its computed score. + * @param doc the re-based document number. + * @param score the document's score. + */ + protected abstract void collect(int doc, float score); + + @Override + public boolean acceptsDocsOutOfOrder() { + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/AdaptingHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/AdaptingHits.java new file mode 100644 index 00000000000..048559658a2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/AdaptingHits.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.core.query.lucene.hits; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is an implementation of Hits which starts with marking hits in an + * ArrayHits instance and switches to a BitSetHits instance if at least the + * threshold of 8kb for the ArrayHits is reached and a BitSetHits instance + * would consume less memory. + */ +public class AdaptingHits implements Hits { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(AdaptingHits.class); + + /** + * The lower threshold before a conversion is tried + */ + private static final int DEFAULT_THRESHOLD = 2048; + + /** + * Internal hits instance + */ + private Hits hits; + + /** + * The maximum doc number in hits. Used to calculate the expected + * BitSetHits memory footprint. + */ + private int maxDoc; + + /** + * The total number of hits. Used to calculate the memory footprint of the + * initial ArrayHits instance. + */ + private int docCount; + + private int threshold; + + public AdaptingHits() { + this(DEFAULT_THRESHOLD); + } + + public AdaptingHits(int threshold) { + this.threshold = threshold; + hits = new ArrayHits(); + maxDoc = 0; + } + + /** + * {@inheritDoc} + */ + public int next() throws IOException { + // delegate to the internal Hits instance + return hits.next(); + } + + /** + * {@inheritDoc} + */ + public void set(int doc) { + hits.set(doc); + docCount++; + if (doc > maxDoc) { + maxDoc = doc; + } + + if (docCount > threshold && (hits instanceof ArrayHits)) { + int intArraySize = docCount * 4; + int bitSetSize = maxDoc / 8; + if (bitSetSize < intArraySize) { + log.debug("BitSet is smaller than int[]: " + + bitSetSize + " vs " + intArraySize); + BitSetHits bitSetHits = new BitSetHits(); + int i = 0; + while (i > -1) { + try { + i = hits.next(); + if (i > -1) { + bitSetHits.set(i); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + hits = bitSetHits; + } + } + } + + /** + * {@inheritDoc} + */ + public int skipTo(int target) throws IOException { + // delegate to the internal Hits instance + return hits.skipTo(target); + } + + Hits getInternalHits() { + return hits; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/ArrayHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/ArrayHits.java new file mode 100644 index 00000000000..62c736cfb09 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/ArrayHits.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.core.query.lucene.hits; + +import java.util.Arrays; + +/** + * Uses an integer array to store the hit set. This implementation uses less + * memory than {@link BitSetHits} if the total number of documents is high and + * and the number of hits is low or if your hits doc numbers are mostly in the + * upper part of your doc number range. + * If you don't know about your hit distribution in advance use + * {@link AdaptingHits} instead. + */ +public class ArrayHits implements Hits { + + private static final int INITIAL_SIZE = 100; + private int[] hits; + private int index; + private boolean initialized; + + public ArrayHits() { + this(INITIAL_SIZE); + } + + public ArrayHits(int initialSize) { + hits = new int[initialSize]; + index = 0; + initialized = false; + } + + private void initialize() { + if (!initialized) { + Arrays.sort(hits); + index = hits.length - index; + initialized = true; + } + } + + /** + * {@inheritDoc} + */ + public void set(int doc) { + if (initialized) { + throw new IllegalStateException( + "You must not call set() after next() or skipTo()"); + } + if (index >= hits.length) { + int[] resizedHits = new int[hits.length * 2]; + System.arraycopy(hits, 0, resizedHits, 0, hits.length); + hits = resizedHits; + } + hits[index++] = doc; + } + + /** + * {@inheritDoc} + */ + public int next() { + initialize(); + if (index >= hits.length) { + return -1; + } else { + return hits[index++]; + } + } + + /** + * {@inheritDoc} + */ + public int skipTo(int target) { + initialize(); + for (int i = index; i < hits.length; i++) { + int nextDocValue = hits[i]; + if (nextDocValue >= target) { + index = i; + return next(); + } + } + return -1; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/BitSetHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/BitSetHits.java new file mode 100644 index 00000000000..c2eea351345 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/BitSetHits.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.core.query.lucene.hits; + +import java.util.BitSet; + +/** + * Uses a BitSet instance to store the hit set. Keep in mind that this BitSet + * is at least as large as the highest doc number in the hit set. This means it + * might need of lot of memory for large indexes. + */ +public class BitSetHits implements Hits { + private BitSet hits; + private int index; + + public BitSetHits() { + hits = new BitSet(); + index = 0; + } + + /** + * {@inheritDoc} + */ + public void set(int doc) { + hits.set(doc); + } + + /** + * {@inheritDoc} + */ + public int next() { + int result = hits.nextSetBit(index); + index = result + 1; + return result; + } + + /** + * {@inheritDoc} + */ + public int skipTo(int target) { + index = target; + return next(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/Hits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/Hits.java new file mode 100644 index 00000000000..0f0df9d95ca --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/Hits.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.core.query.lucene.hits; + +import java.io.IOException; + +/** + * Representation of a set of hits + */ +public interface Hits { + + /** + * Marks the document with doc number doc as a hit. + * Implementations may throw an exception if you call set() after next() or + * skipTo() has been called. + */ + void set(int doc); + + /** + * Return the doc number of the next hit in the set. Subsequent calls never + * return the same doc number. + */ + int next() throws IOException; + + /** + * Skips to the first match beyond the current whose document number is + * greater than or equal to the given target. Returns -1 if there is no + * matching document number greater than target. + */ + int skipTo(int target) throws IOException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/HitsIntersection.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/HitsIntersection.java new file mode 100644 index 00000000000..ec80ba4d6dd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/HitsIntersection.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.core.query.lucene.hits; + +import java.io.IOException; + +/** + * Creates the intersection of two hit sets. + */ +public class HitsIntersection implements Hits { + + private final Hits hits1; + private final Hits hits2; + + private int nextChildrenHit = -1; + private int nextNameTestHit = -1; + + public HitsIntersection(Hits hits1, Hits hits2) { + this.hits1 = hits1; + this.hits2 = hits2; + } + + /** + * {@inheritDoc} + */ + public void set(int doc) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int next() throws IOException { + do { + if (nextChildrenHit == nextNameTestHit) { + nextNameTestHit = hits2.next(); + nextChildrenHit = hits1.next(); + } else if (nextNameTestHit < nextChildrenHit) { + nextNameTestHit = hits2.skipTo(nextChildrenHit); + } else { + nextChildrenHit = hits1.skipTo(nextNameTestHit); + } + } while (nextChildrenHit > -1 && nextNameTestHit > -1 + && nextNameTestHit != nextChildrenHit); + + int nextDoc = -1; + if (nextChildrenHit == nextNameTestHit) { + nextDoc = nextChildrenHit; + } + return nextDoc; + } + + /** + * {@inheritDoc} + */ + public int skipTo(int target) throws IOException { + nextChildrenHit = hits1.skipTo(target); + nextNameTestHit = hits2.skipTo(target); + if (nextChildrenHit == nextNameTestHit) { + return nextChildrenHit; + } else { + return next(); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/ScorerHits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/ScorerHits.java new file mode 100644 index 00000000000..11e7c071965 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/hits/ScorerHits.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.core.query.lucene.hits; + +import java.io.IOException; + +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.Scorer; + +/** + * Wraps a {@link org.apache.lucene.search.Scorer} in a {@link Hits} instance. + */ +public class ScorerHits implements Hits { + + private final Scorer scorer; + + public ScorerHits(Scorer scorer) { + this.scorer = scorer; + } + + /** + * {@inheritDoc} + */ + public void set(int doc) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int next() throws IOException { + int docId = scorer.nextDoc(); + if (docId != DocIdSetIterator.NO_MORE_DOCS) { + return docId; + } else { + return -1; + } + } + + /** + * {@inheritDoc} + */ + public int skipTo(int target) throws IOException { + int docId = scorer.advance(target); + if (docId != DocIdSetIterator.NO_MORE_DOCS) { + return docId; + } else { + return -1; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractCondition.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractCondition.java new file mode 100644 index 00000000000..8763e7fb54f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractCondition.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.core.query.lucene.join; + +import java.util.Arrays; +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.spi.Name; + +/** + * AbstractCondition is a base class for join conditions. + */ +public abstract class AbstractCondition implements Condition { + + /** + * The inner query hits. + */ + protected final MultiColumnQueryHits inner; + + /** + * Creates a new join condition with the given inner query + * hits. + * + * @param inner the inner query hits. + */ + public AbstractCondition(MultiColumnQueryHits inner) { + this.inner = inner; + } + + /** + * @return selector names of the inner query hits. + */ + public Name[] getInnerSelectorNames() { + return inner.getSelectorNames(); + } + + /** + * Closes this join condition and frees resources. Namely closes the inner + * query hits. + * + * @throws IOException if an error occurs while closing the inner query + * hits. + */ + public void close() throws IOException { + inner.close(); + } + + /** + * Returns the index of the selector with the given selectorName + * within the given source. + * + * @param source a source. + * @param selectorName a selector name. + * @return the index within the source or -1 if the name does + * not exist in source. + */ + protected static int getIndex(MultiColumnQueryHits source, Name selectorName) { + return Arrays.asList(source.getSelectorNames()).indexOf(selectorName); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractRow.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractRow.java new file mode 100644 index 00000000000..5c9bd5fb051 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AbstractRow.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.core.query.lucene.join; + +import java.util.Map; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Row; +import javax.jcr.query.qom.Operand; +import javax.jcr.query.qom.PropertyValue; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +abstract class AbstractRow implements Row { + + private final Map columns; + + private final OperandEvaluator evaluator; + + protected AbstractRow( + Map columns, OperandEvaluator evaluator) { + this.columns = columns; + this.evaluator = evaluator; + } + + public Value[] getValues() throws RepositoryException { + Value[] values = new Value[columns.size()]; + int i = 0; + for (String columnName : columns.keySet()) { + values[i++] = getValue(columnName); + } + return values; + } + + public Value getValue(String columnName) + throws ItemNotFoundException, RepositoryException { + Operand operand = columns.get(columnName); + if (operand != null) { + return evaluator.getValue(operand, this); + } else { + throw new ItemNotFoundException( + "Column " + columnName + " is not included in this row"); + } + } + + public String getPath() throws RepositoryException { + Node node = getNode(); + if (node != null) { + return node.getPath(); + } else { + return null; + } + } + + public String getPath(String selectorName) throws RepositoryException { + Node node = getNode(selectorName); + if (node != null) { + return node.getPath(); + } else { + return null; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AncestorNodeJoin.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AncestorNodeJoin.java new file mode 100644 index 00000000000..d02338330f8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AncestorNodeJoin.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.core.query.lucene.join; + +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.query.lucene.HierarchyResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.lucene.index.IndexReader; + +/** + * AncestorNodeJoin implements an ancestor node join condition. + */ +public class AncestorNodeJoin extends AbstractCondition { + + /** + * A score node map with the score nodes from the inner query hits. The + * inner score nodes are indexed by the document numbers of their ancestor + * nodes. + */ + private final ScoreNodeMap contextIndex = new ScoreNodeMap(); + + /** + * The index reader. + */ + private final IndexReader reader; + + /** + * The hierarchy resolver. + */ + private final HierarchyResolver resolver; + + /** + * Reusable array of document numbers. + */ + private int[] docNums = new int[1]; + + /** + * Reusable list of ancestor document numbers. + */ + private final List ancestors = new ArrayList(); + + /** + * Creates a new ancestor node join condition. + * + * @param context the inner query hits. + * @param contextSelectorName the selector name for the inner query hits. + * @param reader the index reader. + * @param resolver the hierarchy resolver. + * @throws IOException if an error occurs while reading from the index. + */ + public AncestorNodeJoin(MultiColumnQueryHits context, + Name contextSelectorName, + IndexReader reader, + HierarchyResolver resolver) throws IOException { + super(context); + this.reader = reader; + this.resolver = resolver; + int idx = getIndex(context, contextSelectorName); + ScoreNode[] nodes; + while ((nodes = context.nextScoreNodes()) != null) { + Integer docNum = nodes[idx].getDoc(reader); + ancestors.clear(); + collectAncestors(docNum); + for (Integer doc : ancestors) { + contextIndex.addScoreNodes(doc, nodes); + } + } + } + + /** + * {@inheritDoc} + *

    + * The outer query hits loop contains the ancestor score nodes. + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode ancestor) + throws IOException { + Integer doc = ancestor.getDoc(reader); + return contextIndex.getScoreNodes(doc); + } + + /** + * Collects the ancestors of the given doc number into + * {@link #ancestors}. + * + * @param doc the current document number. + * @throws IOException if an error occurs while reading from the index. + */ + private void collectAncestors(int doc) throws IOException { + docNums = resolver.getParents(doc, docNums); + if (docNums.length == 1) { + ancestors.add(docNums[0]); + collectAncestors(docNums[0]); + } else if (docNums.length > 1) { + // clone because recursion uses docNums again + for (int docNum : docNums.clone()) { + ancestors.add(docNum); + collectAncestors(docNum); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AncestorPathNodeJoin.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AncestorPathNodeJoin.java new file mode 100644 index 00000000000..e9d379bd344 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/AncestorPathNodeJoin.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.core.query.lucene.join; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; + +/** + * AncestorPathNodeJoin implements an ancestor path node join + * condition. + */ +public class AncestorPathNodeJoin extends AbstractCondition { + + /** + * A score node map with the score nodes from the inner query hits, indexed + * by the path of the inner query hits. + */ + private final ScoreNodeMap contextIndex = new ScoreNodeMap(); + + /** + * The hierarchy manager. + */ + private final HierarchyManager hmgr; + + /** + * The relative path from the outer to the inner query hits. + */ + private final Path relPath; + + /** + * Creates an ancestor path node join. + * + * @param context the inner query hits. + * @param contextSelectorName the selector name for the inner query hits. + * @param relPath the relative path of the join condition. + * @param hmgr the hierarchy manager of the workspace. + * @throws IOException if an error occurs while reading from the index. + */ + public AncestorPathNodeJoin(MultiColumnQueryHits context, + Name contextSelectorName, + Path relPath, + HierarchyManager hmgr) throws IOException { + super(context); + this.hmgr = hmgr; + this.relPath = relPath; + int idx = getIndex(context, contextSelectorName); + ScoreNode[] nodes; + while ((nodes = context.nextScoreNodes()) != null) { + try { + Path p = hmgr.getPath(nodes[idx].getNodeId()); + contextIndex.addScoreNodes(p, nodes); + } catch (RepositoryException e) { + // ignore + } + } + } + + /** + * {@inheritDoc} + *

    + * The outer query hits loop contains the ancestor nodes. + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode ancestor) + throws IOException { + try { + Path ancestorPath = hmgr.getPath(ancestor.getNodeId()); + PathBuilder builder = new PathBuilder(ancestorPath); + builder.addAll(relPath.getElements()); + return contextIndex.getScoreNodes(builder.getPath().getNormalizedPath()); + } catch (RepositoryException e) { + // ignore, probably does not exist anymore + return null; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoin.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoin.java new file mode 100644 index 00000000000..679b08ad8d1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoin.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene.join; + +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.query.lucene.HierarchyResolver; +import org.apache.jackrabbit.spi.commons.query.qom.ChildNodeJoinConditionImpl; +import org.apache.lucene.index.IndexReader; + +/** + * ChildNodeJoin implements a child node join condition. + */ +public class ChildNodeJoin extends AbstractCondition { + + /** + * A score node map with the score nodes from the inner query hits, indexed + * by the document number of the parent node. + */ + private final ScoreNodeMap parentIndex = new ScoreNodeMap(); + + /** + * The index reader. + */ + private final IndexReader reader; + + /** + * The hierarchy resolver. + */ + private final HierarchyResolver resolver; + + /** + * Reusable array of document numbers. + */ + private int[] docNums = new int[1]; + + /** + * Reusable list of score nodes. + */ + private List tmpScoreNodes = new ArrayList(); + + /** + * Creates a new child node join condition. + * + * @param parent the inner query hits. + * @param reader the index reader. + * @param resolver the hierarchy resolver. + * @param condition the QOM child node join condition. + * @throws IOException if an error occurs while reading from the index. + */ + public ChildNodeJoin(MultiColumnQueryHits parent, + IndexReader reader, + HierarchyResolver resolver, + ChildNodeJoinConditionImpl condition) + throws IOException { + super(parent); + this.reader = reader; + this.resolver = resolver; + int idx = getIndex(parent, condition.getParentSelectorQName()); + ScoreNode[] nodes; + while ((nodes = parent.nextScoreNodes()) != null) { + Integer docNum = nodes[idx].getDoc(reader); + parentIndex.addScoreNodes(docNum, nodes); + } + } + + /** + * {@inheritDoc} + *

    + * The outer query hits loop contains the child nodes. + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode child) throws IOException { + docNums = resolver.getParents(child.getDoc(reader), docNums); + tmpScoreNodes.clear(); + for (int docNum : docNums) { + ScoreNode[][] sn = parentIndex.getScoreNodes(docNum); + if (sn != null) { + for (ScoreNode[] aSn : sn) { + tmpScoreNodes.add(aSn); + } + } + } + if (tmpScoreNodes.isEmpty()) { + return null; + } else { + return tmpScoreNodes.toArray(new ScoreNode[tmpScoreNodes.size()][]); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.java new file mode 100644 index 00000000000..62db16d2f66 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ChildNodeJoinMerger.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.core.query.lucene.join; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.Row; +import javax.jcr.query.qom.ChildNodeJoinCondition; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +class ChildNodeJoinMerger extends JoinMerger { + + private final String childSelector; + + private final String parentSelector; + + public ChildNodeJoinMerger( + Join join, Map columns, + OperandEvaluator evaluator, QueryObjectModelFactory factory, + ChildNodeJoinCondition condition) + throws RepositoryException { + super(join, columns, evaluator, factory); + this.childSelector = condition.getChildSelectorName(); + this.parentSelector = condition.getParentSelectorName(); + } + + @Override + public Set getLeftValues(Row row) throws RepositoryException { + return getValues(leftSelectors, row); + } + + @Override + public Set getRightValues(Row row) throws RepositoryException { + return getValues(rightSelectors, row); + } + + @Override + public List getRightJoinConstraints(Collection leftRows) + throws RepositoryException { + Set paths = new HashSet(); + for (Row row : leftRows) { + paths.addAll(getLeftValues(row)); + } + + List constraints = new ArrayList(); + for (String path: paths) { + if (rightSelectors.contains(childSelector)) { + constraints.add(factory.childNode(childSelector, path)); + } else { + constraints.add(factory.sameNode(parentSelector, path)); + } + } + return constraints; + } + + private Set getValues(Set selectors, Row row) + throws RepositoryException { + if (selectors.contains(childSelector)) { + Node node = row.getNode(childSelector); + if (node != null && node.getDepth() > 0) { + return Collections.singleton(node.getParent().getPath()); + } + } else if (selectors.contains(parentSelector)) { + Node node = row.getNode(parentSelector); + if (node != null) { + return Collections.singleton(node.getPath()); + } + } else { + throw new RepositoryException("Invalid child node join"); + } + return Collections.emptySet(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Condition.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Condition.java new file mode 100644 index 00000000000..3c8de078857 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Condition.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.core.query.lucene.join; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.spi.Name; + +/** + * Condition defines an interface for a join condition. + */ +public interface Condition { + + /** + * Returns the matching inner score nodes for the given outer score node + * sn. + * + * @param outer the current score nodes of the outer source. + * @return the matching score nodes in the inner source. + * @throws IOException if an error occurs while evaluating the condition. + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode outer) + throws IOException; + + /** + * @return the selector name of the inner hits. + */ + public Name[] getInnerSelectorNames(); + + /** + * Closes this condition and frees resources. + * + * @throws IOException if an error occurs while closing this condition. + */ + public void close() throws IOException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitInfo.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitInfo.java new file mode 100644 index 00000000000..dae743bc9e5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitInfo.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.core.query.lucene.join; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.QueryObjectModelFactory; + +class ConstraintSplitInfo { + + private final QueryObjectModelFactory factory; + + private final Join source; + + private final List leftConstraints; + + private final List rightConstraints; + + private boolean isMultiple; + + private boolean hasLeftConstraints; + + private boolean hasRightConstraints; + + private ConstraintSplitInfo leftInnerConstraints = null; + + private ConstraintSplitInfo rightInnerConstraints = null; + + public ConstraintSplitInfo(QueryObjectModelFactory factory, Join source) { + this(factory, source, new ArrayList(), + new ArrayList()); + } + + private ConstraintSplitInfo(QueryObjectModelFactory factory, Join source, + List leftConstraints, List rightConstraints) { + this.factory = factory; + this.source = source; + this.leftConstraints = leftConstraints; + this.rightConstraints = rightConstraints; + this.isMultiple = false; + this.hasLeftConstraints = false; + this.hasRightConstraints = false; + } + + public void addLeftConstraint(Constraint c) { + if (isMultiple) { + leftInnerConstraints.addLeftConstraint(c); + rightInnerConstraints.addLeftConstraint(c); + return; + } + leftConstraints.add(c); + this.hasLeftConstraints = true; + } + + public void addRightConstraint(Constraint c) { + if (isMultiple) { + leftInnerConstraints.addRightConstraint(c); + rightInnerConstraints.addRightConstraint(c); + return; + } + rightConstraints.add(c); + this.hasRightConstraints = true; + } + + public void splitOr() { + + if (isMultiple) { + // this should never happen + return; + } + + this.isMultiple = true; + ConstraintSplitInfo csi1 = new ConstraintSplitInfo(factory, source, + new ArrayList(leftConstraints), + new ArrayList(rightConstraints)); + csi1.hasLeftConstraints = this.hasLeftConstraints; + csi1.hasRightConstraints = this.hasRightConstraints; + this.leftInnerConstraints = csi1; + + ConstraintSplitInfo csi2 = new ConstraintSplitInfo(factory, source, + new ArrayList(leftConstraints), + new ArrayList(rightConstraints)); + csi2.hasLeftConstraints = this.hasLeftConstraints; + csi2.hasRightConstraints = this.hasRightConstraints; + this.rightInnerConstraints = csi2; + + this.leftConstraints.clear(); + this.rightConstraints.clear(); + this.hasLeftConstraints = false; + this.hasRightConstraints = false; + } + + public boolean isMultiple() { + return isMultiple; + } + + public ConstraintSplitInfo getLeftInnerConstraints() { + return leftInnerConstraints; + } + + public ConstraintSplitInfo getRightInnerConstraints() { + return rightInnerConstraints; + } + + public Join getSource() { + return source; + } + + /** + * @return the left constraint + */ + public Constraint getLeftConstraint() throws RepositoryException { + return Constraints.and(factory, leftConstraints); + } + + /** + * @return the right constraint + */ + public Constraint getRightConstraint() throws RepositoryException { + return Constraints.and(factory, rightConstraints); + } + + public boolean isHasLeftConstraints() { + return hasLeftConstraints; + } + + public boolean isHasRightConstraints() { + return hasRightConstraints; + } + + @Override + public String toString() { + if (isMultiple) { + return "ConstraintSplitInfo [multiple=" + ", leftInnerConstraints=" + + leftInnerConstraints + ", rightInnerConstraints=" + + rightInnerConstraints + "]"; + } + return "ConstraintSplitInfo [single" + ", leftConstraints=" + + leftConstraints + ", rightConstraints=" + rightConstraints + + ", hasLeftConstraints=" + hasLeftConstraints + + ", hasRightConstraints=" + hasRightConstraints + "]"; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java new file mode 100644 index 00000000000..c17d08081d9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ConstraintSplitter.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene.join; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.query.qom.And; +import javax.jcr.query.qom.ChildNode; +import javax.jcr.query.qom.Comparison; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.DescendantNode; +import javax.jcr.query.qom.DynamicOperand; +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.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.PropertyExistence; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.SameNode; +import javax.jcr.query.qom.UpperCase; + +/** + * Returns a mapped constraint that only refers to the given set of selectors. + * The returned constraint is guaranteed to match an as small as possible + * superset of the node tuples matched by the given original constraints. + * + * @param constraint + * original constraint + * @param selectors + * target selectors + * @return mapped constraint + * @throws RepositoryException + * if the constraint mapping fails + */ +class ConstraintSplitter { + + private final QueryObjectModelFactory factory; + + private final Set leftSelectors; + + private final Set rightSelectors; + + private final ConstraintSplitInfo constraintSplitInfo; + + public ConstraintSplitter(Constraint constraint, + QueryObjectModelFactory factory, Set leftSelectors, + Set rightSelectors, Join join) throws RepositoryException { + this.factory = factory; + this.leftSelectors = leftSelectors; + this.rightSelectors = rightSelectors; + constraintSplitInfo = new ConstraintSplitInfo(this.factory, join); + if (constraint != null) { + split(constraintSplitInfo, constraint); + } + } + + private void split(ConstraintSplitInfo constraintSplitInfo, Constraint constraint) throws RepositoryException { + if (constraint instanceof Not) { + splitNot(constraintSplitInfo, (Not) constraint); + } else if (constraint instanceof And) { + And and = (And) constraint; + split(constraintSplitInfo, and.getConstraint1()); + split(constraintSplitInfo, and.getConstraint2()); + } else if (constraint instanceof Or) { + if (isReferencingBothSides(getSelectorNames(constraint))) { + Or or = (Or) constraint; + //the problem here is when you split an OR that has both condition sides referencing both join sides. + // it should split into 2 joins + constraintSplitInfo.splitOr(); + split(constraintSplitInfo.getLeftInnerConstraints(), or.getConstraint1()); + split(constraintSplitInfo.getRightInnerConstraints(),or.getConstraint2()); + } else { + splitBySelectors(constraintSplitInfo, constraint, getSelectorNames(constraint)); + } + } else { + splitBySelectors(constraintSplitInfo, constraint, getSelectorNames(constraint)); + } + } + + private boolean isReferencingBothSides(Set selectors) { + return !leftSelectors.containsAll(selectors) + && !rightSelectors.containsAll(selectors); + } + + private void splitNot(ConstraintSplitInfo constraintSplitInfo, Not not) throws RepositoryException { + Constraint constraint = not.getConstraint(); + if (constraint instanceof Not) { + split(constraintSplitInfo, ((Not) constraint).getConstraint()); + } else if (constraint instanceof And) { + And and = (And) constraint; + split(constraintSplitInfo, factory.or(factory.not(and.getConstraint1()), + factory.not(and.getConstraint2()))); + } else if (constraint instanceof Or) { + Or or = (Or) constraint; + split(constraintSplitInfo, factory.and(factory.not(or.getConstraint1()), + factory.not(or.getConstraint2()))); + } else { + splitBySelectors(constraintSplitInfo, not, getSelectorNames(constraint)); + } + } + + private void splitBySelectors(ConstraintSplitInfo constraintSplitInfo, Constraint constraint, Set selectors) + throws UnsupportedRepositoryOperationException { + if (leftSelectors.containsAll(selectors)) { + constraintSplitInfo.addLeftConstraint(constraint); + } else if (rightSelectors.containsAll(selectors)) { + constraintSplitInfo.addRightConstraint(constraint); + } else { + throw new UnsupportedRepositoryOperationException( + "Unable to split a constraint that references" + + " both sides of a join: " + constraint); + } + } + + /** + * Returns the names of the selectors referenced by the given constraint. + * + * @param constraint + * constraint + * @return referenced selector names + * @throws UnsupportedRepositoryOperationException + * if the constraint type is unknown + */ + private Set getSelectorNames(Constraint constraint) + throws UnsupportedRepositoryOperationException { + if (constraint instanceof And) { + And and = (And) constraint; + return getSelectorNames(and.getConstraint1(), and.getConstraint2()); + } else if (constraint instanceof Or) { + Or or = (Or) constraint; + return getSelectorNames(or.getConstraint1(), or.getConstraint2()); + } else if (constraint instanceof Not) { + Not not = (Not) constraint; + return getSelectorNames(not.getConstraint()); + } else if (constraint instanceof PropertyExistence) { + PropertyExistence pe = (PropertyExistence) constraint; + return Collections.singleton(pe.getSelectorName()); + } else if (constraint instanceof Comparison) { + Comparison c = (Comparison) constraint; + return Collections.singleton(getSelectorName(c.getOperand1())); + } else if (constraint instanceof SameNode) { + SameNode sn = (SameNode) constraint; + return Collections.singleton(sn.getSelectorName()); + } else if (constraint instanceof ChildNode) { + ChildNode cn = (ChildNode) constraint; + return Collections.singleton(cn.getSelectorName()); + } else if (constraint instanceof DescendantNode) { + DescendantNode dn = (DescendantNode) constraint; + return Collections.singleton(dn.getSelectorName()); + } else if (constraint instanceof FullTextSearch) { + FullTextSearch fts = (FullTextSearch) constraint; + return Collections.singleton(fts.getSelectorName()); + } else { + throw new UnsupportedRepositoryOperationException( + "Unknown constraint type: " + constraint); + } + } + + /** + * Returns the combined set of selector names referenced by the given two + * constraint. + * + * @param a + * first constraint + * @param b + * second constraint + * @return selector names + * @throws UnsupportedRepositoryOperationException + * if the constraint types are unknown + */ + private Set getSelectorNames(Constraint a, Constraint b) + throws UnsupportedRepositoryOperationException { + Set set = new HashSet(); + set.addAll(getSelectorNames(a)); + set.addAll(getSelectorNames(b)); + return set; + } + + /** + * Returns the selector name referenced by the given dynamic operand. + * + * @param operand + * dynamic operand + * @return selector name + * @throws UnsupportedRepositoryOperationException + * if the operand type is unknown + */ + private String getSelectorName(DynamicOperand operand) + throws UnsupportedRepositoryOperationException { + if (operand instanceof FullTextSearchScore) { + FullTextSearchScore ftss = (FullTextSearchScore) operand; + return ftss.getSelectorName(); + } else if (operand instanceof Length) { + Length length = (Length) operand; + return getSelectorName(length.getPropertyValue()); + } else if (operand instanceof LowerCase) { + LowerCase lower = (LowerCase) operand; + return getSelectorName(lower.getOperand()); + } else if (operand instanceof NodeLocalName) { + NodeLocalName local = (NodeLocalName) operand; + return local.getSelectorName(); + } else if (operand instanceof NodeName) { + NodeName name = (NodeName) operand; + return name.getSelectorName(); + } else if (operand instanceof PropertyValue) { + PropertyValue value = (PropertyValue) operand; + return value.getSelectorName(); + } else if (operand instanceof UpperCase) { + UpperCase upper = (UpperCase) operand; + return getSelectorName(upper.getOperand()); + } else { + throw new UnsupportedRepositoryOperationException( + "Unknown dynamic operand type: " + operand); + } + } + + public ConstraintSplitInfo getConstraintSplitInfo() { + return constraintSplitInfo; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Constraints.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Constraints.java new file mode 100644 index 00000000000..e22aec62b4f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Constraints.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.core.query.lucene.join; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.QueryObjectModelFactory; + +public class Constraints { + + public static Constraint and( + QueryObjectModelFactory factory, List constraints) + throws RepositoryException { + int n = constraints.size(); + if (n == 0) { + return null; + } else if (n == 1) { + return constraints.get(0); + } else { + int m = n / 2; + return factory.and( + and(factory, constraints.subList(0, m)), + and(factory, constraints.subList(m, n))); + } + } + + public static Constraint and( + QueryObjectModelFactory factory, Constraint... constraints) + throws RepositoryException { + List list = new ArrayList(constraints.length); + for (Constraint constraint : constraints) { + if (constraint != null) { + list.add(constraint); + } + } + return and(factory, list); + } + + public static Constraint or( + QueryObjectModelFactory factory, List constraints) + throws RepositoryException { + int n = constraints.size(); + if (n == 0) { + return null; + } else if (n == 1) { + return constraints.get(0); + } else { + int m = n / 2; + return factory.or( + or(factory, constraints.subList(0, m)), + or(factory, constraints.subList(m, n))); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoin.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoin.java new file mode 100644 index 00000000000..a682e0edfdb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoin.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.core.query.lucene.join; + +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.query.lucene.HierarchyResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.lucene.index.IndexReader; + +/** + * DescendantNodeJoin implements a descendant node join condition. + */ +public class DescendantNodeJoin extends AbstractCondition { + + /** + * A score node map with the score nodes from the inner query hits, indexed + * by the document number. + */ + private final ScoreNodeMap contextIndex = new ScoreNodeMap(); + + /** + * The index reader. + */ + private final IndexReader reader; + + /** + * The hierarchy resolver. + */ + private final HierarchyResolver resolver; + + /** + * Reusable array of document numbers. + */ + private int[] docNums = new int[1]; + + /** + * Reusable list of document number. + */ + private final List ancestors = new ArrayList(); + + /** + * Reusable list of score nodes. + */ + private final List scoreNodes = new ArrayList(); + + /** + * Creates a new descendant node join condition. + * + * @param context the inner query hits. + * @param contextSelectorName the selector name for the inner query hits. + * @param reader the index reader. + * @param resolver the hierarchy resolver. + * @throws IOException if an error occurs while reading fromt the index. + */ + public DescendantNodeJoin(MultiColumnQueryHits context, + Name contextSelectorName, + IndexReader reader, + HierarchyResolver resolver) throws IOException { + super(context); + this.reader = reader; + this.resolver = resolver; + int idx = getIndex(context, contextSelectorName); + ScoreNode[] nodes; + while ((nodes = context.nextScoreNodes()) != null) { + Integer docNum = nodes[idx].getDoc(reader); + contextIndex.addScoreNodes(docNum, nodes); + } + } + + /** + * {@inheritDoc} + *

    + * The outer query hits loop contains the descendant nodes. + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode descendant) + throws IOException { + ancestors.clear(); + collectAncestors(descendant.getDoc(reader)); + scoreNodes.clear(); + for (Integer ancestor : ancestors) { + ScoreNode[][] sn = contextIndex.getScoreNodes(ancestor); + if (sn != null) { + for (ScoreNode[] aSn : sn) { + scoreNodes.add(aSn); + } + } + } + if (scoreNodes.isEmpty()) { + return null; + } else { + return scoreNodes.toArray(new ScoreNode[scoreNodes.size()][]); + } + } + + /** + * Collects the ancestors of the given doc number into + * {@link #ancestors}. + * + * @param doc the current document number. + * @throws IOException if an error occurs while reading from the index. + */ + private void collectAncestors(int doc) throws IOException { + docNums = resolver.getParents(doc, docNums); + if (docNums.length == 1) { + ancestors.add(docNums[0]); + collectAncestors(docNums[0]); + } else if (docNums.length > 1) { + // clone because recursion uses docNums again + for (int docNum : docNums.clone()) { + ancestors.add(docNum); + collectAncestors(docNum); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java new file mode 100644 index 00000000000..cb1babe5dcc --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantNodeJoinMerger.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene.join; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.Row; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.DescendantNodeJoinCondition; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +class DescendantNodeJoinMerger extends JoinMerger { + + private final String descendantSelector; + + private final String ancestorSelector; + + public DescendantNodeJoinMerger( + Join join, Map columns, + OperandEvaluator evaluator, QueryObjectModelFactory factory, + DescendantNodeJoinCondition condition) + throws RepositoryException { + super(join, columns, evaluator, factory); + this.descendantSelector = condition.getDescendantSelectorName(); + this.ancestorSelector = condition.getAncestorSelectorName(); + } + + @Override + public Set getLeftValues(Row row) throws RepositoryException { + return getValues(leftSelectors, row); + } + + @Override + public Set getRightValues(Row row) throws RepositoryException { + return getValues(rightSelectors, row); + } + + @Override + public List getRightJoinConstraints(Collection leftRows) + throws RepositoryException { + Set paths = new HashSet(); + for (Row row : leftRows) { + paths.addAll(getLeftValues(row)); + } + + List constraints = new ArrayList(); + for (String path : paths) { + if (rightSelectors.contains(descendantSelector)) { + constraints.add( + factory.descendantNode(descendantSelector, path)); + } else { + constraints.add(factory.sameNode(ancestorSelector, path)); + } + } + return constraints; + } + + private Set getValues(Set selectors, Row row) + throws RepositoryException { + if (selectors.contains(descendantSelector)) { + Node node = row.getNode(descendantSelector); + if (node != null) { + Set values = new HashSet(); + while (node.getDepth() > 0) { + node = node.getParent(); + values.add(node.getPath()); + } + return values; + } + } else if (selectors.contains(ancestorSelector)) { + Node node = row.getNode(ancestorSelector); + if (node != null) { + return Collections.singleton(node.getPath()); + } + } else { + throw new RepositoryException("Invalid descendant node join"); + } + return Collections.emptySet(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantPathNodeJoin.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantPathNodeJoin.java new file mode 100644 index 00000000000..d27651060ed --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/DescendantPathNodeJoin.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.core.query.lucene.join; + +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; + +/** + * DescendantPathNodeJoin implements a descendant path node join + * condition. + */ +public class DescendantPathNodeJoin extends AbstractCondition { + + /** + * A score node map with the score nodes from the inner query hits, indexed + * by the path of the of the inner query hits plus the relative path of this + * condition. + */ + private final ScoreNodeMap contextIndex = new ScoreNodeMap(); + + /** + * The hierarchy manager. + */ + private final HierarchyManager hmgr; + + /** + * Creates a new descendant path node join condition. + * + * @param context the inner query hits. + * @param contextSelectorName the selector name for the inner query hits. + * @param relPath the relative path of the join condition. + * @param hmgr the hierarchy manager. + * @throws IOException if an error occurs while reading from the index. + */ + public DescendantPathNodeJoin(MultiColumnQueryHits context, + Name contextSelectorName, + Path relPath, + HierarchyManager hmgr) throws IOException { + super(context); + this.hmgr = hmgr; + int idx = getIndex(context, contextSelectorName); + ScoreNode[] nodes; + while ((nodes = context.nextScoreNodes()) != null) { + try { + Path p = hmgr.getPath(nodes[idx].getNodeId()); + PathBuilder builder = new PathBuilder(p); + builder.addAll(relPath.getElements()); + p = builder.getPath().getNormalizedPath(); + contextIndex.addScoreNodes(p, nodes); + } catch (RepositoryException e) { + // ignore + } + } + } + + /** + * {@inheritDoc} + *

    + * The outer query hits loop contains the descendant nodes. + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode descendant) + throws IOException { + try { + Path p = hmgr.getPath(descendant.getNodeId()); + return contextIndex.getScoreNodes(p); + } catch (RepositoryException e) { + // ignore, probably does not exist anymore + } + return null; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoin.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoin.java new file mode 100644 index 00000000000..b989f0bf802 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoin.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.core.query.lucene.join; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.jackrabbit.core.query.lucene.FieldNames; +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.query.lucene.NamespaceMappings; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermDocs; +import org.apache.lucene.index.TermEnum; + +/** + * EquiJoin implements an equi join condition. + */ +public class EquiJoin extends AbstractCondition { + + /** + * The index reader. + */ + private final IndexReader reader; + + private final Map> rowsByInnerNodeValue = + new HashMap>(); + + private final Map> valuesByOuterNodeDocument = + new HashMap>(); + + /** + * Creates a new equi join condition. + * + * @param inner the inner query hits. + * @param innerScoreNodeIndex the selector name for the inner query hits. + * @param nsMappings the namespace mappings + * @param reader the index reader. + * @param innerProperty the name of the property of the inner query + * hits. + * @param outerProperty the name of the property of the outer query + * hits. + * @throws IOException if an error occurs while reading from the index. + * @throws IllegalNameException + */ + public EquiJoin( + MultiColumnQueryHits inner, int innerScoreNodeIndex, + NamespaceMappings nsMappings, IndexReader reader, + Name innerProperty, Name outerProperty) + throws IOException, IllegalNameException { + super(inner); + this.reader = reader; + + // create lookup map + Map> rowsByInnerDocument = + new HashMap>(); + ScoreNode[] row = inner.nextScoreNodes(); + while (row != null) { + int document = row[innerScoreNodeIndex].getDoc(reader); + List rows = rowsByInnerDocument.get(document); + if (rows == null) { + rows = new ArrayList(); + rowsByInnerDocument.put(document, rows); + } + rows.add(row); + row = inner.nextScoreNodes(); + } + + // Build the rowsByInnerNodeValue map for efficient lookup in + // the getMatchingScoreNodes() method + String innerName = nsMappings.translateName(innerProperty); + for (Map.Entry entry : getPropertyTerms(innerName)) { + String value = entry.getValue(); + TermDocs docs = reader.termDocs(entry.getKey()); + while (docs.next()) { + List match = rowsByInnerDocument.get(docs.doc()); + if (match != null) { + List rows = rowsByInnerNodeValue.get(value); + if (rows == null) { + rows = new ArrayList(); + rowsByInnerNodeValue.put(value, rows); + } + rows.addAll(match); + } + } + } + + // Build the valuesByOuterNodeDocument map for efficient lookup in + // the getMatchingScoreNodes() method + String outerName = nsMappings.translateName(outerProperty); + for (Map.Entry entry : getPropertyTerms(outerName)) { + String value = entry.getValue(); + TermDocs docs = reader.termDocs(entry.getKey()); + while (docs.next()) { + Set values = valuesByOuterNodeDocument.get(docs.doc()); + if (values == null) { + values = new HashSet(); + valuesByOuterNodeDocument.put(docs.doc(), values); + } + values.add(value); + } + } + } + + /** + * {@inheritDoc} + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode outer) + throws IOException { + List list = new ArrayList(); + + Set values = valuesByOuterNodeDocument.get(outer.getDoc(reader)); + if (values != null) { + for (String value : values) { + List rows = rowsByInnerNodeValue.get(value); + if (rows != null) { + list.addAll(rows); + } + } + } + + return list.toArray(new ScoreNode[list.size()][]); + } + + private Set> getPropertyTerms(String property) + throws IOException { + Map map = new HashMap(); + + Term prefix = new Term( + FieldNames.PROPERTIES, + FieldNames.createNamedValue(property, "")); + TermEnum terms = reader.terms(prefix); + do { + Term term = terms.term(); + if (term == null + || !term.field().equals(prefix.field()) + || !term.text().startsWith(prefix.text())) { + break; + } + map.put(term, term.text().substring(prefix.text().length())); + } while (terms.next()); + + return map.entrySet(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.java new file mode 100644 index 00000000000..c4a0a6fdf4c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/EquiJoinMerger.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.core.query.lucene.join; + +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Row; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.EquiJoinCondition; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.Literal; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +class EquiJoinMerger extends JoinMerger { + + private final PropertyValue leftProperty; + + private final PropertyValue rightProperty; + + public EquiJoinMerger( + Join join, Map columns, + OperandEvaluator evaluator, QueryObjectModelFactory factory, + EquiJoinCondition condition) throws RepositoryException { + super(join, columns, evaluator, factory); + + PropertyValue property1 = factory.propertyValue( + condition.getSelector1Name(), condition.getProperty1Name()); + PropertyValue property2 = factory.propertyValue( + condition.getSelector2Name(), condition.getProperty2Name()); + + if (leftSelectors.contains(property1.getSelectorName()) + && rightSelectors.contains(property2.getSelectorName())) { + leftProperty = property1; + rightProperty = property2; + } else if (leftSelectors.contains(property2.getSelectorName()) + && rightSelectors.contains(property1.getSelectorName())) { + leftProperty = property2; + rightProperty = property1; + } else { + throw new RepositoryException("Invalid equi-join"); + } + } + + @Override + public Set getLeftValues(Row row) throws RepositoryException { + return getValues(leftProperty, row); + } + + @Override + public Set getRightValues(Row row) throws RepositoryException { + return getValues(rightProperty, row); + } + + @Override + public List getRightJoinConstraints(Collection leftRows) + throws RepositoryException { + Map literals = new HashMap(); + for (Row leftRow : leftRows) { + for (Value value : evaluator.getValues(leftProperty, leftRow)) { + literals.put(value.getString(), factory.literal(value)); + } + } + + List constraints = + new ArrayList(literals.size()); + for (Literal literal : literals.values()) { + constraints.add(factory.comparison( + rightProperty, JCR_OPERATOR_EQUAL_TO, literal)); + } + return constraints; + } + + private Set getValues(PropertyValue property, Row row) + throws RepositoryException { + Set strings = new HashSet(); + for (Value value : evaluator.getValues(property, row)) { + strings.add(value.getString()); + } + return strings; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Join.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Join.java new file mode 100644 index 00000000000..67822265ba3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/Join.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.core.query.lucene.join; + +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.apache.jackrabbit.commons.query.qom.JoinType; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.query.lucene.HierarchyResolver; +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.query.lucene.NamespaceMappings; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.query.qom.ChildNodeJoinConditionImpl; +import org.apache.jackrabbit.spi.commons.query.qom.DefaultQOMTreeVisitor; +import org.apache.jackrabbit.spi.commons.query.qom.DescendantNodeJoinConditionImpl; +import org.apache.jackrabbit.spi.commons.query.qom.EquiJoinConditionImpl; +import org.apache.jackrabbit.spi.commons.query.qom.JoinConditionImpl; +import org.apache.jackrabbit.spi.commons.query.qom.SameNodeJoinConditionImpl; +import org.apache.lucene.index.IndexReader; + +/** + * Join implements the result of a join. + */ +public class Join implements MultiColumnQueryHits { + + /** + * The outer query hits. + */ + protected final MultiColumnQueryHits outer; + + /** + * The score node index of the outer query hits. + */ + protected final int outerScoreNodeIndex; + + /** + * Whether this is an inner join. + */ + protected final boolean innerJoin; + + /** + * The join condition. + */ + protected final Condition condition; + + /** + * The selector names. + */ + protected final Name[] selectorNames; + + /** + * An array of empty inner query hits. + */ + protected final ScoreNode[] emptyInnerHits; + + /** + * A buffer for joined score node rows. + */ + protected final List buffer = new LinkedList(); + + /** + * Creates a new join. + * + * @param outer the outer query hits. + * @param outerScoreNodeIndex the score node index of the outer query hits + * that is used for the join. + * @param innerJoin whether this is an inner join. + * @param condition the join condition. + */ + private Join(MultiColumnQueryHits outer, + int outerScoreNodeIndex, + boolean innerJoin, + Condition condition) { + this.outer = outer; + this.outerScoreNodeIndex = outerScoreNodeIndex; + this.innerJoin = innerJoin; + this.condition = condition; + this.emptyInnerHits = new ScoreNode[condition.getInnerSelectorNames().length]; + // outer selector names go to the left, inner selector + // names go to the right. + // this needs to be in sync with ScoreNode[] aggregration/joining + // in nextScoreNodes() ! + this.selectorNames = new Name[outer.getSelectorNames().length + emptyInnerHits.length]; + System.arraycopy(outer.getSelectorNames(), 0, selectorNames, 0, outer.getSelectorNames().length); + System.arraycopy(condition.getInnerSelectorNames(), 0, selectorNames, outer.getSelectorNames().length, emptyInnerHits.length); + } + + /** + * Creates a new join result. + * + * @param left the left query hits. + * @param right the right query hits. + * @param joinType the join type. + * @param condition the QOM join condition. + * @param reader the index reader. + * @param resolver the hierarchy resolver. + * @param nsMappings namespace mappings of this index + * @param hmgr the hierarchy manager of the workspace. + * @return the join result. + * @throws IOException if an error occurs while executing the join. + */ + public static Join create(final MultiColumnQueryHits left, + final MultiColumnQueryHits right, + final JoinType joinType, + final JoinConditionImpl condition, + final IndexReader reader, + final HierarchyResolver resolver, + final NamespaceMappings nsMappings, + final HierarchyManager hmgr) + throws IOException { + try { + return (Join) condition.accept(new DefaultQOMTreeVisitor() { + + private boolean isInner = JoinType.INNER == joinType; + private MultiColumnQueryHits outer; + private int outerIdx; + + public Object visit(DescendantNodeJoinConditionImpl node, Object data) + throws Exception { + MultiColumnQueryHits ancestor = getSourceWithName(node.getAncestorSelectorQName(), left, right); + MultiColumnQueryHits descendant = getSourceWithName(node.getDescendantSelectorQName(), left, right); + Condition c; + if (isInner + || descendant == left && JoinType.LEFT == joinType + || descendant == right && JoinType.RIGHT == joinType) { + // also applies to inner join + // assumption: DescendantNodeJoin is more + // efficient than AncestorNodeJoin, TODO: verify + outer = descendant; + outerIdx = getIndex(outer, node.getDescendantSelectorQName()); + c = new DescendantNodeJoin(ancestor, node.getAncestorSelectorQName(), reader, resolver); + } else { + // left == ancestor + outer = ancestor; + outerIdx = getIndex(outer, node.getAncestorSelectorQName()); + c = new AncestorNodeJoin(descendant, node.getDescendantSelectorQName(), reader, resolver); + } + return new Join(outer, outerIdx, isInner, c); + } + + public Object visit(EquiJoinConditionImpl node, Object data) + throws Exception { + MultiColumnQueryHits src1 = getSourceWithName(node.getSelector1QName(), left, right); + MultiColumnQueryHits src2 = getSourceWithName(node.getSelector2QName(), left, right); + MultiColumnQueryHits inner; + Name innerName; + Name innerPropName; + Name outerPropName; + if (isInner + || src1 == left && JoinType.LEFT == joinType + || src1 == right && JoinType.RIGHT == joinType) { + outer = src1; + outerIdx = getIndex(outer, node.getSelector1QName()); + inner = src2; + innerName = node.getSelector2QName(); + innerPropName = node.getProperty2QName(); + outerPropName = node.getProperty1QName(); + } else { + outer = src2; + outerIdx = getIndex(outer, node.getSelector2QName()); + inner = src1; + innerName = node.getSelector1QName(); + innerPropName = node.getProperty1QName(); + outerPropName = node.getProperty2QName(); + } + + Condition c = new EquiJoin( + inner, getIndex(inner, innerName), nsMappings, + reader, innerPropName, outerPropName); + return new Join(outer, outerIdx, isInner, c); + } + + public Object visit(ChildNodeJoinConditionImpl node, Object data) + throws Exception { + MultiColumnQueryHits child = getSourceWithName(node.getChildSelectorQName(), left, right); + MultiColumnQueryHits parent = getSourceWithName(node.getParentSelectorQName(), left, right); + Condition c; + if (child == left && JoinType.LEFT == joinType + || child == right && JoinType.RIGHT == joinType) { + outer = child; + outerIdx = getIndex(outer, node.getChildSelectorQName()); + c = new ChildNodeJoin(parent, reader, resolver, node); + } else { + // also applies to inner joins + // assumption: ParentNodeJoin is more efficient than + // ChildNodeJoin, TODO: verify + outer = parent; + outerIdx = getIndex(outer, node.getParentSelectorQName()); + c = new ParentNodeJoin(child, reader, resolver, node); + } + return new Join(outer, outerIdx, isInner, c); + } + + public Object visit(SameNodeJoinConditionImpl node, Object data) + throws Exception { + MultiColumnQueryHits src1 = getSourceWithName(node.getSelector1QName(), left, right); + MultiColumnQueryHits src2 = getSourceWithName(node.getSelector2QName(), left, right); + Condition c; + if (isInner + || src1 == left && JoinType.LEFT == joinType + || src1 == right && JoinType.RIGHT == joinType) { + outer = src1; + outerIdx = getIndex(outer, node.getSelector1QName()); + Path selector2Path = node.getSelector2QPath(); + if (selector2Path == null || (selector2Path.getLength() == 1 && selector2Path.denotesCurrent())) { + c = new SameNodeJoin(src2, node.getSelector2QName(), reader); + } else { + c = new DescendantPathNodeJoin(src2, node.getSelector2QName(), + node.getSelector2QPath(), hmgr); + } + } else { + outer = src2; + outerIdx = getIndex(outer, node.getSelector2QName()); + Path selector2Path = node.getSelector2QPath(); + if (selector2Path == null || (selector2Path.getLength() == 1 && selector2Path.denotesCurrent())) { + c = new SameNodeJoin(src1, node.getSelector1QName(), reader); + } else { + c = new AncestorPathNodeJoin(src1, node.getSelector1QName(), + node.getSelector2QPath(), hmgr); + } + } + return new Join(outer, outerIdx, isInner, c); + } + }, null); + } catch (IOException e) { + throw e; + } catch (Exception e) { + IOException ex = new IOException(e.getMessage()); + ex.initCause(e); + throw ex; + } + } + + /** + * {@inheritDoc} + */ + public ScoreNode[] nextScoreNodes() throws IOException { + if (!buffer.isEmpty()) { + return buffer.remove(0); + } + do { + // refill buffer + ScoreNode[] sn = outer.nextScoreNodes(); + if (sn == null) { + return null; + } + ScoreNode[][] nodes = condition.getMatchingScoreNodes(sn[outerScoreNodeIndex]); + if (nodes != null) { + for (ScoreNode[] node : nodes) { + // create array with both outer and inner + ScoreNode[] tmp = new ScoreNode[sn.length + node.length]; + System.arraycopy(sn, 0, tmp, 0, sn.length); + System.arraycopy(node, 0, tmp, sn.length, node.length); + buffer.add(tmp); + } + } else if (!innerJoin) { + // create array with both inner and outer + ScoreNode[] tmp = new ScoreNode[sn.length + emptyInnerHits.length]; + System.arraycopy(sn, 0, tmp, 0, sn.length); + System.arraycopy(emptyInnerHits, 0, tmp, sn.length, emptyInnerHits.length); + buffer.add(tmp); + } + } while (buffer.isEmpty()); + + return buffer.remove(0); + } + + /** + * {@inheritDoc} + */ + public Name[] getSelectorNames() { + return selectorNames; + } + + /** + * {@inheritDoc} + * Closes {@link #outer} source and the {@link #condition}. + */ + public void close() throws IOException { + IOException ex = null; + try { + outer.close(); + } catch (IOException e) { + ex = e; + } + try { + condition.close(); + } catch (IOException e) { + if (ex == null) { + ex = e; + } + } + if (ex != null) { + throw ex; + } + } + + /** + * This default implementation always returns -1. + * + * @return always -1. + */ + public int getSize() { + return -1; + } + + /** + * {@inheritDoc} + * Skips by calling {@link #nextScoreNodes()} n times. Sub + * classes may provide a more performance implementation. + */ + public void skip(int n) throws IOException { + while (n-- > 0) { + if (nextScoreNodes() == null) { + return; + } + } + } + + protected static MultiColumnQueryHits getSourceWithName( + Name selectorName, + MultiColumnQueryHits left, + MultiColumnQueryHits right) { + if (Arrays.asList(left.getSelectorNames()).contains(selectorName)) { + return left; + } else if (Arrays.asList(right.getSelectorNames()).contains(selectorName)) { + return right; + } else { + throw new IllegalArgumentException("unknown selector name: " + selectorName); + } + } + + /** + * Returns the index of the selector with the given selectorName + * within the given source. + * + * @param source a source. + * @param selectorName a selector name. + * @return the index within the source or -1 if the name does + * not exist in source. + */ + protected static int getIndex(MultiColumnQueryHits source, + Name selectorName) { + return Arrays.asList(source.getSelectorNames()).indexOf(selectorName); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java new file mode 100644 index 00000000000..99b3fb07837 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinMerger.java @@ -0,0 +1,321 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene.join; + +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_JOIN_TYPE_INNER; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; +import javax.jcr.query.qom.ChildNodeJoinCondition; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.DescendantNodeJoinCondition; +import javax.jcr.query.qom.EquiJoinCondition; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.SameNodeJoinCondition; +import javax.jcr.query.qom.Selector; +import javax.jcr.query.qom.Source; + +import org.apache.jackrabbit.commons.iterator.RowIterable; +import org.apache.jackrabbit.commons.iterator.RowIteratorAdapter; +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +/** + * A join merger is used by the {@link QueryEngine} class to efficiently + * merge together two parts of a join query. + *

    + * Each join condition type ({@link EquiJoinCondition equi-} and + * {@link SameNodeJoinCondition same}, {@link ChildNodeJoinCondition child} + * or {@link DescendantJoinCondition descendant} node joins) has it's own + * merger class that extends the functionality of this abstract base class + * with functionality specific to that join condition. + */ +abstract class JoinMerger { + + /** + * Static factory method for creating a merger for the given join. + * + * @param join join + * @param columns columns of the query + * @param evaluator operand evaluator + * @param factory QOM factory + * @return join merger + * @throws RepositoryException if the merger can not be created + */ + public static JoinMerger getJoinMerger( + Join join, Map columns, + OperandEvaluator evaluator, QueryObjectModelFactory factory) + throws RepositoryException { + JoinCondition condition = join.getJoinCondition(); + if (condition instanceof EquiJoinCondition) { + return new EquiJoinMerger( + join, columns, evaluator, factory, + (EquiJoinCondition) condition); + } else if (condition instanceof SameNodeJoinCondition) { + return new SameNodeJoinMerger( + join, columns, evaluator, factory, + (SameNodeJoinCondition) condition); + } else if (condition instanceof ChildNodeJoinCondition) { + return new ChildNodeJoinMerger( + join, columns, evaluator, factory, + (ChildNodeJoinCondition) condition); + } else if (condition instanceof DescendantNodeJoinCondition) { + return new DescendantNodeJoinMerger( + join, columns, evaluator, factory, + (DescendantNodeJoinCondition) condition); + } else { + throw new UnsupportedRepositoryOperationException( + "Unsupported join condition type: " + condition); + } + } + + private final String type; + + protected final Set leftSelectors; + + protected final Set rightSelectors; + + private final String[] selectorNames; + + private final Map columns; + + private final String[] columnNames; + + protected final OperandEvaluator evaluator; + + protected final QueryObjectModelFactory factory; + + protected JoinMerger( + Join join, Map columns, + OperandEvaluator evaluator, QueryObjectModelFactory factory) + throws RepositoryException { + this.type = join.getJoinType(); + + this.leftSelectors = getSelectorNames(join.getLeft()); + this.rightSelectors = getSelectorNames(join.getRight()); + + Set selectors = new LinkedHashSet(); + selectors.addAll(leftSelectors); + selectors.addAll(rightSelectors); + this.selectorNames = + selectors.toArray(new String[selectors.size()]); + + this.columns = columns; + this.columnNames = + columns.keySet().toArray(new String[columns.size()]); + + this.evaluator = evaluator; + this.factory = factory; + } + + public String[] getColumnNames(){ + return columnNames; + } + + public String[] getSelectorNames(){ + return selectorNames; + } + + public Set getLeftSelectors() { + return leftSelectors; + } + + public Set getRightSelectors() { + return rightSelectors; + } + + private Set getSelectorNames(Source source) + throws RepositoryException { + if (source instanceof Selector) { + Selector selector = (Selector) source; + return Collections.singleton(selector.getSelectorName()); + } else if (source instanceof Join) { + Join join = (Join) source; + Set set = new LinkedHashSet(); + set.addAll(getSelectorNames(join.getLeft())); + set.addAll(getSelectorNames(join.getRight())); + return set; + } else { + throw new UnsupportedRepositoryOperationException( + "Unknown source type: " + source); + } + } + + /** + * Merges the left and right dataset of a join query. Take special + * precaution for outer joins, as extra checks are needed to distinguish + * 'null' nodes vs 'not to be included' nodes + * + * + * @param leftRows + * the left dataset of the join + * @param rightRows + * the right dataset of the join + * @param excludingOuterJoinRowsSet + * if not null must be taken into consideration when + * merging OUTER JOINs + * @param rowComparator + * a comparator implementation that has to handle the 'is row + * equal to' problem, in the case of outer joins with + * excludingOuterJoinRowsSet + * @return a QueryResult that has the final JOIN resultset + * @throws RepositoryException + */ + public QueryResult merge(RowIterator leftRows, RowIterator rightRows, + Set excludingOuterJoinRowsSet, Comparator rowComparator) + throws RepositoryException { + Map> map = buildRightRowValues(rightRows); + + if (JCR_JOIN_TYPE_INNER.equals(type) && !map.isEmpty()) { + List rows = new ArrayList(); + for (Row leftRow : new RowIterable(leftRows)) { + for (String value : getLeftValues(leftRow)) { + List matchingRows = map.get(value); + if (matchingRows != null) { + for (Row rightRow : matchingRows) { + rows.add(mergeRow(leftRow, rightRow)); + } + } + } + } + return asQueryResult(new RowIteratorAdapter(rows)); + } + + if (JCR_JOIN_TYPE_LEFT_OUTER.equals(type)) { + // there are no RIGHT dataset values + if (map.isEmpty()) { + // if there are no WHERE conditions, just return everything + // else return an empty set + if (excludingOuterJoinRowsSet == null) { + return asQueryResult(new RowIteratorAdapter(leftRows) { + @Override + public Object next() { + return mergeRow((Row) super.next(), null); + } + }); + } + return asQueryResult(new RowIteratorAdapter( + Collections.emptySet())); + } + + List rows = new ArrayList(); + for (Row leftRow : new RowIterable(leftRows)) { + Set leftValues = getLeftValues(leftRow); + if(leftValues.isEmpty()){ + leftValues.add(null); + } + for (String value : leftValues) { + List matchingRows = map.get(value); + if (matchingRows != null) { + for (Row rightRow : matchingRows) { + // I have possible WHERE clauses on the join that I + // need to look at for each rightRow + if (excludingOuterJoinRowsSet == null) { + rows.add(mergeRow(leftRow, rightRow)); + } else { + boolean isIncluded = false; + // apparently + // 'excludingOuterJoinRowsSet.contains' fails to + // match rows + + // TODO can 'rightRow.getNode()' break because + // of joins that are bigger than 2 way? + // how does this perform for 3 way joins ? + for (Row r : excludingOuterJoinRowsSet) { + if(rowComparator.compare(rightRow, r) == 0){ + isIncluded = true; + break; + } + } + if (isIncluded) { + rows.add(mergeRow(leftRow, rightRow)); + } + } + } + } else { + // No matches in an outer join -> add a null row, if + // there are no 'WHERE' conditions + if (excludingOuterJoinRowsSet == null) { + rows.add(mergeRow(leftRow, null)); + } + } + } + } + return asQueryResult(new RowIteratorAdapter(rows)); + } + return asQueryResult(new RowIteratorAdapter(Collections.emptySet())); + } + + private QueryResult asQueryResult(RowIterator rowIterator) { + return new SimpleQueryResult(columnNames, selectorNames, rowIterator); + } + + private Map> buildRightRowValues(RowIterator rightRows) + throws RepositoryException { + Map> map = new HashMap>(); + for (Row row : new RowIterable(rightRows)) { + for (String value : getRightValues(row)) { + List rows = map.get(value); + if (rows == null) { + rows = new ArrayList(); + map.put(value, rows); + } + rows.add(row); + } + } + return map; + } + + /** + * Merges the given left and right rows to a single joined row. + * + * @param left left row, possibly null in a right outer join + * @param right right row, possibly null in a left outer join + * @return joined row + */ + private Row mergeRow(Row left, Row right) { + return new JoinRow( + columns, evaluator, + left, leftSelectors, right, rightSelectors); + } + + public abstract Set getLeftValues(Row row) + throws RepositoryException; + + public abstract Set getRightValues(Row row) + throws RepositoryException; + + public abstract List getRightJoinConstraints(Collection leftRows) + throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinRow.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinRow.java new file mode 100644 index 00000000000..71a2945c41c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/JoinRow.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.core.query.lucene.join; + +import java.util.Map; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.Row; +import javax.jcr.query.qom.PropertyValue; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +public class JoinRow extends AbstractRow { + + private final Row leftRow; + + private final Set leftSelectors; + + private final Row rightRow; + + private final Set rightSelectors; + + public JoinRow( + Map columns, OperandEvaluator evaluator, + Row leftRow, Set leftSelectors, + Row rightRow, Set rightSelectors) { + super(columns, evaluator); + this.leftRow = leftRow; + this.leftSelectors = leftSelectors; + this.rightRow = rightRow; + this.rightSelectors = rightSelectors; + } + + public Node getNode() throws RepositoryException { + throw new RepositoryException(); + } + + public Node getNode(String selectorName) throws RepositoryException { + Row row = getRow(selectorName); + if (row != null) { + return row.getNode(selectorName); + } else { + return null; + } + } + + public double getScore() throws RepositoryException { + throw new RepositoryException(); + } + + public double getScore(String selectorName) throws RepositoryException { + Row row = getRow(selectorName); + if (row != null) { + return row.getScore(selectorName); + } else { + return 0.0; + } + } + + private Row getRow(String selector) throws RepositoryException { + if (leftSelectors.contains(selector)) { + return leftRow; + } else if (rightSelectors.contains(selector)) { + return rightRow; + } else { + throw new RepositoryException( + "Selector " + selector + " is not included in this row"); + } + } + + //--------------------------------------------------------------< Object > + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("{ "); + for (String selector : leftSelectors) { + builder.append(selector); + builder.append("="); + try { + builder.append(leftRow.getNode(selector)); + } catch (RepositoryException e) { + builder.append(e.getMessage()); + } + builder.append(" "); + } + for (String selector : rightSelectors) { + builder.append(selector); + builder.append("="); + if(rightRow != null){ + try { + builder.append(rightRow.getNode(selector)); + } catch (RepositoryException e) { + builder.append(e.getMessage()); + } + }else{ + builder.append("null"); + } + builder.append(" "); + } + builder.append("}"); + return builder.toString(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ParentNodeJoin.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ParentNodeJoin.java new file mode 100644 index 00000000000..0c186566b18 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ParentNodeJoin.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.core.query.lucene.join; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.query.lucene.HierarchyResolver; +import org.apache.jackrabbit.spi.commons.query.qom.ChildNodeJoinConditionImpl; +import org.apache.lucene.index.IndexReader; + +/** + * ParentNodeJoin implements a parent node join condition. + */ +public class ParentNodeJoin extends AbstractCondition { + + /** + * The child score nodes indexed by their parent document number. + */ + private final ScoreNodeMap childIndex = new ScoreNodeMap(); + + /** + * The index reader. + */ + private final IndexReader reader; + + /** + * Creates a new parent node join condition. + * + * @param child the inner query hits. + * @param reader the index reader. + * @param resolver the hierarchy resolver. + * @param condition the QOM child node join condition. + * @throws IOException if an error occurs while reading from the index. + */ + public ParentNodeJoin(MultiColumnQueryHits child, + IndexReader reader, + HierarchyResolver resolver, + ChildNodeJoinConditionImpl condition) + throws IOException { + super(child); + this.reader = reader; + int idx = getIndex(child, condition.getChildSelectorQName()); + ScoreNode[] nodes; + int[] docNums = new int[1]; + while ((nodes = child.nextScoreNodes()) != null) { + docNums = resolver.getParents(nodes[idx].getDoc(reader), docNums); + for (int parentId : docNums) { + childIndex.addScoreNodes(parentId, nodes); + } + } + } + + /** + * {@inheritDoc} + * The outer query hits loop contains the parent score nodes. + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode parent) + throws IOException { + return childIndex.getScoreNodes(parent.getDoc(reader)); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/QueryEngine.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/QueryEngine.java new file mode 100644 index 00000000000..11716774086 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/QueryEngine.java @@ -0,0 +1,644 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene.join; + +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; +import javax.jcr.query.qom.Column; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.Ordering; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.Selector; +import javax.jcr.query.qom.Source; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.commons.iterator.RowIteratorAdapter; +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; +import org.apache.jackrabbit.core.query.lucene.LuceneQueryFactory; +import org.apache.jackrabbit.core.query.lucene.sort.DynamicOperandFieldComparatorSource; +import org.apache.jackrabbit.core.query.lucene.sort.RowComparator; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class QueryEngine { + + /** + * The logger instance for this class + */ + private static final Logger log = LoggerFactory + .getLogger(QueryEngine.class); + + //TODO remove this when the implementation is stable + public static final String NATIVE_SORT_SYSTEM_PROPERTY = "useNativeSort"; + + private static final boolean NATIVE_SORT = Boolean.valueOf(System + .getProperty(NATIVE_SORT_SYSTEM_PROPERTY, "false")); + + private static final int printIndentStep = 4; + + private final Session session; + + private final LuceneQueryFactory lqf; + + private final NodeTypeManager ntManager; + + private final QueryObjectModelFactory qomFactory; + + private final ValueFactory valueFactory; + + private final OperandEvaluator evaluator; + + public QueryEngine(Session session, LuceneQueryFactory lqf, + Map variables) throws RepositoryException { + this.session = session; + this.lqf = lqf; + Workspace workspace = session.getWorkspace(); + this.ntManager = workspace.getNodeTypeManager(); + this.qomFactory = workspace.getQueryManager().getQOMFactory(); + this.valueFactory = session.getValueFactory(); + + this.evaluator = new OperandEvaluator(valueFactory, variables); + } + + public QueryResult execute(Column[] columns, Source source, + Constraint constraint, Ordering[] orderings, long offset, long limit) + throws RepositoryException { + long time = System.currentTimeMillis(); + QueryResult qr = execute(columns, source, constraint, orderings, + offset, limit, 2); + log.debug("SQL2 QUERY execute took {} ms. native sort is {}.", + System.currentTimeMillis() - time, NATIVE_SORT); + return qr; + } + + protected QueryResult execute(Column[] columns, Source source, + Constraint constraint, Ordering[] orderings, long offset, + long limit, int printIndentation) throws RepositoryException { + if (source instanceof Selector) { + return execute(columns, (Selector) source, constraint, orderings, + offset, limit, printIndentation); + } + if (source instanceof Join) { + return execute(columns, (Join) source, constraint, orderings, + offset, limit, printIndentation); + } + throw new UnsupportedRepositoryOperationException( + "Unknown source type: " + source); + } + + protected QueryResult execute(Column[] columns, Join join, + Constraint constraint, Ordering[] orderings, long offset, + long limit, int printIndentation) throws RepositoryException { + // Swap the join sources to normalize all outer joins to left + if (JCR_JOIN_TYPE_RIGHT_OUTER.equalsIgnoreCase(join.getJoinType())) { + log.debug( + "{} SQL2 RIGHT OUTER JOIN transformed to LEFT OUTER JOIN.", + genString(printIndentation)); + Join betterJoin = qomFactory.join(join.getRight(), join.getLeft(), + JCR_JOIN_TYPE_LEFT_OUTER, join.getJoinCondition()); + return execute(columns, betterJoin, constraint, orderings, offset, + limit, printIndentation); + } + JoinMerger merger = JoinMerger.getJoinMerger(join, + getColumnMap(columns, getSelectorNames(join)), evaluator, + qomFactory); + ConstraintSplitter splitter = new ConstraintSplitter(constraint, + qomFactory, merger.getLeftSelectors(), + merger.getRightSelectors(), join); + ConstraintSplitInfo csInfo = splitter.getConstraintSplitInfo(); + + logQueryAnalysis(csInfo, printIndentation); + + boolean isOuterJoin = JCR_JOIN_TYPE_LEFT_OUTER.equalsIgnoreCase(join + .getJoinType()); + QueryResult result = execute(merger, csInfo, isOuterJoin, + printIndentation); + + long sort = System.currentTimeMillis(); + QueryResult sortedResult = sort(result, orderings, evaluator, offset, + limit); + log.debug(" {} SQL2 SORT took {} ms.", genString(printIndentation), + System.currentTimeMillis() - sort); + return sortedResult; + } + + protected QueryResult execute(JoinMerger merger, + ConstraintSplitInfo csInfo, boolean isOuterJoin, + int printIndentation) throws RepositoryException { + + Comparator leftCo = new RowPathComparator( + merger.getLeftSelectors()); + long timeJoinLeftSide = System.currentTimeMillis(); + + if (csInfo.isMultiple()) { + log.debug("{} SQL2 JOIN execute: there are multiple inner splits.", + genString(printIndentation)); + + // first branch + long bTime = System.currentTimeMillis(); + QueryResult branch1 = execute(merger, + csInfo.getLeftInnerConstraints(), isOuterJoin, + printIndentation + printIndentStep); + Set allRows = new TreeSet(new RowPathComparator( + Arrays.asList(merger.getSelectorNames()))); + RowIterator ri1 = branch1.getRows(); + while (ri1.hasNext()) { + Row r = ri1.nextRow(); + allRows.add(r); + } + log.debug("{} SQL2 JOIN executed first branch, took {} ms.", + genString(printIndentation), System.currentTimeMillis() + - bTime); + + // second branch + bTime = System.currentTimeMillis(); + QueryResult branch2 = execute(merger, + csInfo.getRightInnerConstraints(), isOuterJoin, + printIndentation + printIndentStep); + RowIterator ri2 = branch2.getRows(); + while (ri2.hasNext()) { + Row r = ri2.nextRow(); + allRows.add(r); + } + log.debug("{} SQL2 JOIN executed second branch, took {} ms.", + genString(printIndentation), System.currentTimeMillis() + - bTime); + return new SimpleQueryResult(merger.getColumnNames(), + merger.getSelectorNames(), new RowIteratorAdapter(allRows)); + } + + Set leftRows = buildLeftRowsJoin(csInfo, leftCo, printIndentation + + printIndentStep); + if (log.isDebugEnabled()) { + timeJoinLeftSide = System.currentTimeMillis() - timeJoinLeftSide; + log.debug(genString(printIndentation) + "SQL2 JOIN LEFT SIDE took " + + timeJoinLeftSide + " ms. fetched " + leftRows.size() + + " rows."); + } + + // The join constraint information is split into: + // - rightConstraints selects just the 'ON' constraints + // - csInfo has the 'WHERE' constraints + // + // So, in the case of an OUTER JOIN we'll run 2 queries, one with + // 'ON' + // and one with 'ON' + 'WHERE' conditions + // this way, at merge time in case of an outer join we can tell if + // it's a 'null' row, or a bad row -> one that must not be returned. + // This way at the end we'll have: + // - rightRowsSet containing the 'ON' dataset + // - excludingOuterJoinRowsSet: the 'ON' + 'WHERE' condition + // dataset, or + // NULL if there is no 'WHERE' condition + + long timeJoinRightSide = System.currentTimeMillis(); + List rightConstraints = merger + .getRightJoinConstraints(leftRows); + Comparator rightCo = new RowPathComparator( + merger.getRightSelectors()); + + if (leftRows == null || leftRows.isEmpty()) { + return merger.merge(new RowIteratorAdapter((leftRows == null) ? Collections.emptySet() : leftRows), + new RowIteratorAdapter(new TreeSet()), null, rightCo); + } + + Set rightRows = buildRightRowsJoin(csInfo, rightConstraints, + isOuterJoin, rightCo, printIndentation + printIndentStep); + + // this has to be initialized as null + Set excludingOuterJoinRowsSet = null; + if (isOuterJoin && csInfo.getRightConstraint() != null) { + excludingOuterJoinRowsSet = buildRightRowsJoin(csInfo, + rightConstraints, false, rightCo, printIndentation + + printIndentStep); + } + + if (log.isDebugEnabled()) { + timeJoinRightSide = System.currentTimeMillis() - timeJoinRightSide; + log.debug(genString(printIndentation) + + "SQL2 JOIN RIGHT SIDE took " + timeJoinRightSide + + " ms. fetched " + rightRows.size() + " rows."); + } + // merge left with right datasets + return merger.merge(new RowIteratorAdapter(leftRows), + new RowIteratorAdapter(rightRows), excludingOuterJoinRowsSet, + rightCo); + + } + + private Set buildLeftRowsJoin(ConstraintSplitInfo csi, + Comparator comparator, int printIndentation) + throws RepositoryException { + + if (csi.isMultiple()) { + if (log.isDebugEnabled()) { + log.debug(genString(printIndentation) + + "SQL2 JOIN LEFT SIDE there are multiple inner splits."); + } + Set leftRows = new TreeSet(comparator); + leftRows.addAll(buildLeftRowsJoin(csi.getLeftInnerConstraints(), + comparator, printIndentation + printIndentStep)); + leftRows.addAll(buildLeftRowsJoin(csi.getRightInnerConstraints(), + comparator, printIndentation + printIndentStep)); + return leftRows; + } + + Set leftRows = new TreeSet(comparator); + QueryResult leftResult = execute(null, csi.getSource().getLeft(), + csi.getLeftConstraint(), null, 0, -1, printIndentation); + for (Row row : JcrUtils.getRows(leftResult)) { + leftRows.add(row); + } + return leftRows; + } + + /** + * @param csi + * contains 'WHERE' constraints and the source information + * @param rightConstraints + * contains 'ON' constraints + * @param ignoreWhereConstraints + * @param comparator + * used to merge similar rows together + * @param printIndentation + * used in logging + * @return the right-side dataset of the join operation + * @throws RepositoryException + */ + private Set buildRightRowsJoin(ConstraintSplitInfo csi, + List rightConstraints, boolean ignoreWhereConstraints, + Comparator comparator, int printIndentation) + throws RepositoryException { + + if (csi.isMultiple()) { + if (log.isDebugEnabled()) { + log.debug(genString(printIndentation) + + "SQL2 JOIN RIGHT SIDE there are multiple inner splits."); + } + Set rightRows = new TreeSet(comparator); + rightRows.addAll(buildRightRowsJoin(csi.getLeftInnerConstraints(), + rightConstraints, ignoreWhereConstraints, comparator, + printIndentation + printIndentStep)); + rightRows.addAll(buildRightRowsJoin(csi.getRightInnerConstraints(), + rightConstraints, ignoreWhereConstraints, comparator, + printIndentation + printIndentStep)); + return rightRows; + } + + if (rightConstraints.size() < 500) { + Set rightRows = new TreeSet(comparator); + List localRightContraints = rightConstraints; + Constraint rightConstraint = Constraints.and(qomFactory, + Constraints.or(qomFactory, localRightContraints), + csi.getRightConstraint()); + if (ignoreWhereConstraints) { + rightConstraint = Constraints.or(qomFactory, + localRightContraints); + } + QueryResult rightResult = execute(null, csi.getSource().getRight(), + rightConstraint, null, 0, -1, printIndentation); + for (Row row : JcrUtils.getRows(rightResult)) { + rightRows.add(row); + } + return rightRows; + } + + // the 'batch by 500' approach + Set rightRows = new TreeSet(comparator); + for (int i = 0; i < rightConstraints.size(); i += 500) { + if (log.isDebugEnabled()) { + log.debug(genString(printIndentation) + + "SQL2 JOIN RIGHT SIDE executing batch # " + i + "."); + } + List localRightContraints = rightConstraints.subList(i, + Math.min(i + 500, rightConstraints.size())); + Constraint rightConstraint = Constraints.and(qomFactory, + Constraints.or(qomFactory, localRightContraints), + csi.getRightConstraint()); + if (ignoreWhereConstraints) { + rightConstraint = Constraints.or(qomFactory, + localRightContraints); + } + + QueryResult rightResult = execute(null, csi.getSource().getRight(), + rightConstraint, null, 0, -1, printIndentation); + for (Row row : JcrUtils.getRows(rightResult)) { + rightRows.add(row); + } + } + return rightRows; + } + + private static String genString(int len) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + sb.append(" "); + } + return sb.toString(); + } + + private static void logQueryAnalysis(ConstraintSplitInfo csi, + int printIndentation) throws RepositoryException { + if (!log.isDebugEnabled()) { + return; + } + StringBuilder sb = new StringBuilder(); + sb.append(genString(printIndentation)); + sb.append("SQL2 JOIN analysis:"); + sb.append(IOUtils.LINE_SEPARATOR); + sb.append(constraintSplitInfoToString(csi, 2)); + log.debug(sb.toString()); + } + + private static String constraintSplitInfoToString(ConstraintSplitInfo csi, + int printIndentation) throws RepositoryException { + + if (csi.isMultiple()) { + StringBuilder sb = new StringBuilder(); + sb.append(genString(printIndentation)); + sb.append("SQL2 JOIN inner split -> "); + sb.append(IOUtils.LINE_SEPARATOR); + sb.append(genString(printIndentation)); + sb.append("+"); + sb.append(IOUtils.LINE_SEPARATOR); + sb.append(constraintSplitInfoToString( + csi.getLeftInnerConstraints(), printIndentation + + printIndentStep)); + sb.append(IOUtils.LINE_SEPARATOR); + sb.append(genString(printIndentation)); + sb.append("+"); + sb.append(IOUtils.LINE_SEPARATOR); + sb.append(constraintSplitInfoToString( + csi.getRightInnerConstraints(), printIndentation + + printIndentStep)); + return sb.toString(); + } + + StringBuilder sb = new StringBuilder(); + sb.append(genString(printIndentation)); + sb.append("SQL2 JOIN source: "); + sb.append(csi.getSource()); + sb.append(IOUtils.LINE_SEPARATOR); + sb.append(genString(printIndentation)); + sb.append("SQL2 JOIN left constraint: "); + sb.append(csi.getLeftConstraint()); + sb.append(IOUtils.LINE_SEPARATOR); + sb.append(genString(printIndentation)); + sb.append("SQL2 JOIN right constraint: "); + sb.append(csi.getRightConstraint()); + return sb.toString(); + } + + protected QueryResult execute(Column[] columns, Selector selector, + Constraint constraint, Ordering[] orderings, long offset, + long limit, int printIndentation) throws RepositoryException { + long time = System.currentTimeMillis(); + + Map selectorMap = getSelectorNames(selector); + String[] selectorNames = selectorMap.keySet().toArray( + new String[selectorMap.size()]); + + Map columnMap = getColumnMap(columns, + selectorMap); + String[] columnNames = columnMap.keySet().toArray( + new String[columnMap.size()]); + + Sort sort = new Sort(); + if (NATIVE_SORT) { + sort = new Sort(createSortFields(orderings, session)); + } + + // if true it means that the LuceneQueryFactory should just let the + // QueryEngine take care of sorting and applying offset and limit + // constraints + boolean externalSort = !NATIVE_SORT; + RowIterator rows = null; + try { + rows = new RowIteratorAdapter(lqf.execute(columnMap, selector, + constraint, sort, externalSort, offset, limit)); + } catch (IOException e) { + throw new RepositoryException("Failed to access the query index", e); + } finally { + log.debug( + "{}SQL2 SELECT took {} ms. selector: {}, columns: {}, constraint: {}, offset {}, limit {}", + new Object[] { genString(printIndentation), + System.currentTimeMillis() - time, selector, + Arrays.toString(columnNames), constraint, offset, + limit }); + } + QueryResult result = new SimpleQueryResult(columnNames, selectorNames, + rows); + if (NATIVE_SORT) { + return result; + } + + long timeSort = System.currentTimeMillis(); + QueryResult sorted = sort(result, orderings, evaluator, offset, limit); + log.debug("{}SQL2 SORT took {} ms.", genString(printIndentation), + System.currentTimeMillis() - timeSort); + return sorted; + } + + public SortField[] createSortFields(Ordering[] orderings, Session session) + throws RepositoryException { + + if (orderings == null || orderings.length == 0) { + return new SortField[] { SortField.FIELD_SCORE }; + } + // orderings[] -> (property, ordering) + Map orderByProperties = new HashMap(); + for (Ordering o : orderings) { + final String p = o.toString(); + if (!orderByProperties.containsKey(p)) { + orderByProperties.put(p, o); + } + } + final DynamicOperandFieldComparatorSource dofcs = new DynamicOperandFieldComparatorSource( + session, evaluator, orderByProperties); + + List sortFields = new ArrayList(); + + // as it turn out, orderByProperties.keySet() doesn't keep the original + // insertion order + for (Ordering o : orderings) { + final String p = o.toString(); + // order on jcr:score does not use the natural order as + // implemented in lucene. score ascending in lucene means that + // higher scores are first. JCR specs that lower score values + // are first. + boolean isAsc = QueryObjectModelConstants.JCR_ORDER_ASCENDING + .equals(o.getOrder()); + if (JcrConstants.JCR_SCORE.equals(p)) { + sortFields.add(new SortField(null, SortField.SCORE, !isAsc)); + } else { + // TODO use native sort if available + sortFields.add(new SortField(p, dofcs, !isAsc)); + } + } + return sortFields.toArray(new SortField[sortFields.size()]); + } + + private Map getColumnMap(Column[] columns, + Map selectors) throws RepositoryException { + Map map = new LinkedHashMap(); + if (columns != null && columns.length > 0) { + for (int i = 0; i < columns.length; i++) { + String name = columns[i].getColumnName(); + if (name != null) { + map.put(name, + qomFactory.propertyValue( + columns[i].getSelectorName(), + columns[i].getPropertyName())); + } else { + String selector = columns[i].getSelectorName(); + map.putAll(getColumnMap(selector, selectors.get(selector))); + } + } + } else { + for (Map.Entry selector : selectors.entrySet()) { + map.putAll(getColumnMap(selector.getKey(), selector.getValue())); + } + } + return map; + } + + private Map getColumnMap(String selector, + NodeType type) throws RepositoryException { + Map map = new LinkedHashMap(); + for (PropertyDefinition definition : type.getPropertyDefinitions()) { + String name = definition.getName(); + if (!definition.isMultiple() && !"*".equals(name)) { + // TODO: Add proper quoting + map.put(selector + "." + name, + qomFactory.propertyValue(selector, name)); + } + } + return map; + } + + private Map getSelectorNames(Source source) + throws RepositoryException { + if (source instanceof Selector) { + Selector selector = (Selector) source; + return Collections.singletonMap(selector.getSelectorName(), + getNodeType(selector)); + } else if (source instanceof Join) { + Join join = (Join) source; + Map map = new LinkedHashMap(); + map.putAll(getSelectorNames(join.getLeft())); + map.putAll(getSelectorNames(join.getRight())); + return map; + } else { + throw new UnsupportedRepositoryOperationException( + "Unknown source type: " + source); + } + } + + private NodeType getNodeType(Selector selector) throws RepositoryException { + try { + return ntManager.getNodeType(selector.getNodeTypeName()); + } catch (NoSuchNodeTypeException e) { + throw new InvalidQueryException( + "Selected node type does not exist: " + selector, e); + } + } + + /** + * Sorts the given query results according to the given QOM orderings. If + * one or more orderings have been specified, this method will iterate + * through the entire original result set, order the collected rows, and + * return a new result set based on the sorted collection of rows. + * + * @param result + * original query results + * @param orderings + * QOM orderings + * @param offset + * result offset + * @param limit + * result limit + * @return sorted query results + * @throws RepositoryException + * if the results can not be sorted + */ + protected static QueryResult sort(QueryResult result, + final Ordering[] orderings, OperandEvaluator evaluator, + long offset, long limit) throws RepositoryException { + if ((orderings != null && orderings.length > 0) || offset != 0 + || limit >= 0) { + List rows = new ArrayList(); + + RowIterator iterator = result.getRows(); + while (iterator.hasNext()) { + rows.add(iterator.nextRow()); + } + + if (orderings != null && orderings.length > 0) { + Collections.sort(rows, new RowComparator(orderings, evaluator)); + } + + if (offset > 0) { + int size = rows.size(); + rows = rows.subList((int) Math.min(offset, size), size); + } + if (limit >= 0) { + int size = rows.size(); + rows = rows.subList(0, (int) Math.min(limit, size)); + } + + return new SimpleQueryResult(result.getColumnNames(), + result.getSelectorNames(), new RowIteratorAdapter(rows)); + } else { + return result; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/RowPathComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/RowPathComparator.java new file mode 100644 index 00000000000..f18aa7613c1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/RowPathComparator.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.core.query.lucene.join; + +import java.util.Collection; +import java.util.Comparator; + +import javax.jcr.RepositoryException; +import javax.jcr.query.Row; + +/** + * Comparator for {@link Row} instances that looks only at the node paths. + */ +public class RowPathComparator implements Comparator { + + private boolean isNullEqual = true; + + /** + * a superset of selectors, in cases where there are joins and such, the + * possibility that a selector node does not exist in a row can happen + */ + private Collection selectors = null; + + public RowPathComparator(Collection selectors) { + this.selectors = selectors; + } + + public RowPathComparator() { + this(null); + } + + /** + * Compares two rows. + */ + public int compare(Row a, Row b) { + + if (selectors != null) { + // will look at equality for each path in the row + for (String selector : selectors) { + + String pA = null; + boolean aExists = true; + String pB = null; + boolean bExists = true; + + try { + pA = a.getPath(selector); + } catch (RepositoryException e) { + // non existing A selector + aExists = false; + } + + try { + pB = b.getPath(selector); + } catch (RepositoryException e) { + // non existing B selector + bExists = false; + } + + // in the case that there is a missing selector node that exists + // on the other row, they are definitely not equal + if ((!aExists && bExists) || (aExists && !bExists)) { + return aExists ? -1 : 1; + } + + if (pA == null || pB == null) { + if (!isNullEqual) { + return -1; + } + } else { + int local = pA.compareTo(pB); + if (local != 0) { + return local; + } + } + + } + return 0; + } + + try { + return a.getPath().compareTo(b.getPath()); + } catch (RepositoryException e) { + throw new RuntimeException("Unable to compare rows " + a + " and " + + b, e); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoin.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoin.java new file mode 100644 index 00000000000..7e3aefb0ae3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoin.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.core.query.lucene.join; + +import java.io.IOException; + +import org.apache.jackrabbit.core.query.lucene.MultiColumnQueryHits; +import org.apache.jackrabbit.core.query.lucene.ScoreNode; +import org.apache.jackrabbit.spi.Name; +import org.apache.lucene.index.IndexReader; + +/** + * SameNodeJoin implements a same node join condition. + */ +public class SameNodeJoin extends AbstractCondition { + + /** + * A score node map with the score nodes from the inner query hits, indexed + * by their document number. + */ + private final ScoreNodeMap innerIndex = new ScoreNodeMap(); + + /** + * The index reader. + */ + private final IndexReader reader; + + /** + * Creates a new same node join. + * + * @param inner the inner query hits. + * @param innerSelectorName the selector name for the inner query hits. + * @param reader the index reader. + * @throws IOException if an error occurs while reading from the index. + */ + public SameNodeJoin(MultiColumnQueryHits inner, + Name innerSelectorName, + IndexReader reader) throws IOException { + super(inner); + this.reader = reader; + int idx = getIndex(inner, innerSelectorName); + ScoreNode[] nodes; + while ((nodes = inner.nextScoreNodes()) != null) { + innerIndex.addScoreNodes(nodes[idx].getDoc(reader), nodes); + } + } + + /** + * {@inheritDoc} + */ + public ScoreNode[][] getMatchingScoreNodes(ScoreNode outer) + throws IOException { + return innerIndex.getScoreNodes(outer.getDoc(reader)); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoinMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoinMerger.java new file mode 100644 index 00000000000..52cb553da7c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SameNodeJoinMerger.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.core.query.lucene.join; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.query.Row; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.SameNodeJoinCondition; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +class SameNodeJoinMerger extends JoinMerger { + + private final String selector1; + + private final String selector2; + + private final String path; + + public SameNodeJoinMerger( + Join join, Map columns, + OperandEvaluator evaluator, QueryObjectModelFactory factory, + SameNodeJoinCondition condition) throws RepositoryException { + super(join, columns, evaluator, factory); + this.selector1 = condition.getSelector1Name(); + this.selector2 = condition.getSelector2Name(); + this.path = condition.getSelector2Path(); + } + + @Override + public Set getLeftValues(Row row) throws RepositoryException { + return getValues(leftSelectors, row); + } + + @Override + public Set getRightValues(Row row) throws RepositoryException { + return getValues(rightSelectors, row); + } + + @Override + public List getRightJoinConstraints(Collection leftRows) + throws RepositoryException { + Set paths = new HashSet(); + for (Row row : leftRows) { + paths.addAll(getLeftValues(row)); + } + + List constraints = new ArrayList(); + for (String path : paths) { + if (rightSelectors.contains(selector1)) { + constraints.add(factory.sameNode(selector1, path)); + } else { + constraints.add(factory.sameNode(selector2, path)); + } + } + return constraints; + } + + private Set getValues(Set selectors, Row row) + throws RepositoryException { + if (selectors.contains(selector1)) { + Node node = row.getNode(selector1); + if (node != null) { + return Collections.singleton(node.getPath()); + } + } else if (selectors.contains(selector2)) { + Node node = row.getNode(selector2); + if (node != null) { + try { + String p = node.getPath(); + if (path != null && !".".equals(path)) { + if (!"/".equals(p)) { + p += "/"; + } + p += path; + } + return Collections.singleton(p); + } catch (PathNotFoundException e) { + // fall through + } + } + } else { + throw new RepositoryException("Invalid same node join"); + } + return Collections.emptySet(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ScoreNodeMap.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ScoreNodeMap.java new file mode 100644 index 00000000000..a4e0c6d457f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ScoreNodeMap.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.core.query.lucene.join; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; + +import org.apache.jackrabbit.core.query.lucene.ScoreNode; + +/** + * ScoreNodeMap implements a simple mapping of an arbitrary key + * to an array of ScoreNode[]. + */ +public final class ScoreNodeMap { + + /** + * The internal map. + */ + private final Map map = new HashMap(); + + /** + * Adds scoreNodes to this map under the given key. + * If there already exists a mapping with the given key the + * scoreNodes are added to the existing mapping. The add + * operation works as follows: + *

      + *
    • If the existing value for key is a ScoreNode[], + * then the value is turned into a List and the existing value + * as well as the new value are added to the List. Finally + * the List is uses as the new value for the mapping. + *
    • + *
    • If the existing value for key is a List the + * scoreNodes are simply added to the List. + *
    • + *
    + * + * @param key the lookup key. + * @param nodes the score nodes. + */ + public void addScoreNodes(Object key, ScoreNode[] nodes) { + Object existing = map.get(key); + if (existing == null) { + existing = nodes; + map.put(key, existing); + } else if (existing instanceof List) { + @SuppressWarnings("unchecked") + List existingNodes = (List) existing; + existingNodes.add(nodes); + } else { + // ScoreNode[] + List tmp = new ArrayList(); + tmp.add((ScoreNode[]) existing); + tmp.add(nodes); + existing = tmp; + map.put(key, existing); + } + } + + /** + * Returns an array of ScoreNode[] for the given + * key. + * + * @param key the key. + * @return an array of ScoreNode[] that match the given + * key or null if there is none. + */ + public ScoreNode[][] getScoreNodes(Object key) { + Object sn = map.get(key); + if (sn == null) { + return null; + } else if (sn instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) sn; + return list.toArray(new ScoreNode[list.size()][]); + } else { + // ScoreNode[] + return new ScoreNode[][]{(ScoreNode[]) sn}; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SelectorRow.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SelectorRow.java new file mode 100644 index 00000000000..901cec139fb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SelectorRow.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.core.query.lucene.join; + +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.PropertyValue; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +/** + * A row implementation for a query with just a single selector. + */ +public class SelectorRow extends AbstractRow { + + private final String selector; + + private final Node node; + + private final double score; + + public SelectorRow( + Map columns, OperandEvaluator evaluator, + String selector, Node node, double score) { + super(columns, evaluator); + this.selector = selector; + this.node = node; + this.score = score; + } + + public Node getNode() { + return node; + } + + public Node getNode(String selectorName) throws RepositoryException { + checkSelectorName(selectorName); + return node; + } + + public double getScore() { + return score; + } + + public double getScore(String selectorName) throws RepositoryException { + checkSelectorName(selectorName); + return score; + } + + private void checkSelectorName(String name) throws RepositoryException { + if (!selector.equals(name)) { + throw new RepositoryException( + "Selector " + name + " is not included in this row"); + } + } + + //--------------------------------------------------------------< Object > + + public String toString() { + return "{ " + selector + ": " + node + " }"; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SimpleQueryResult.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SimpleQueryResult.java new file mode 100644 index 00000000000..1d9db8724f2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/SimpleQueryResult.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.core.query.lucene.join; + +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; + +/** + * Simple query result implementation. + */ +public class SimpleQueryResult implements QueryResult { + + /** + * The column names of this query. + */ + private final String[] columnNames; + + /** + * The selector names of this query. + */ + private final String[] selectorNames; + + /** + * Query result rows. Set to null when the iterator has + * already been accessed. + */ + private RowIterator rowIterator; + + /** + * Creates a query result with the given column and selector names and + * row iterator. + * + * @param columnNames column names + * @param selectorNames selector names + * @param rowIterator iterator over matching rows + */ + protected SimpleQueryResult( + String[] columnNames, String[] selectorNames, + RowIterator rowIterator) { + assert columnNames != null; + assert selectorNames != null && selectorNames.length >= 1; + assert rowIterator != null; + this.columnNames = columnNames; + this.selectorNames = selectorNames; + this.rowIterator = rowIterator; + } + + /** + * Returns the column names of this query. Note that the returned array + * is not protected against modification by the client application. + * + * @return column names + */ + public String[] getColumnNames() { + return columnNames; + } + + /** + * Returns the selector names of this query. Note that the returned array + * is not protected against modification by the client application. + * + * @return selector names + */ + public String[] getSelectorNames() { + return selectorNames; + } + + /** + * Returns the query result rows. + * + * @return query result rows + * @throws RepositoryException if the query results have already + * been iterated through + */ + public synchronized RowIterator getRows() throws RepositoryException { + if (rowIterator != null) { + RowIterator iterator = rowIterator; + rowIterator = null; + return iterator; + } else { + throw new RepositoryException( + "This query result has already been iterated through"); + } + } + + /** + * Returns the nodes that match this query. + * + * @return matching nodes + * @throws RepositoryException if this query has more than one selector, + * or if the query results have already been + * iterated through + */ + public NodeIterator getNodes() throws RepositoryException { + if (selectorNames.length == 1) { + return new NodeIteratorAdapter(getRows()) { + @Override + public Object next() { + Row row = (Row) super.next(); + try { + return row.getNode(); + } catch (RepositoryException e) { + throw new RuntimeException( + "Unable to access the node in " + row, e); + } + } + }; + } else { + throw new RepositoryException( + "This query result contains more than one selector"); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ValueComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ValueComparator.java new file mode 100644 index 00000000000..6394c4d069e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/join/ValueComparator.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.core.query.lucene.join; + +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LIKE; +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.regex.Pattern; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.core.query.lucene.Util; + +/** + * Comparator for {@link Value} instances. + */ +public class ValueComparator implements Comparator { + + /** + * Compares two values. + */ + public int compare(Value a, Value b) { + try { + return Util.compare(a, b); + } catch (RepositoryException e) { + throw new RuntimeException("Unable to compare values " + a + + " and " + b, e); + } + } + + /** + * Evaluates the given QOM comparison operation with the given values. + * + * @param operator QOM comparison operator + * @param a left value + * @param b right value + * @return result of the comparison + */ + public boolean evaluate(String operator, Value a, Value b) { + if (JCR_OPERATOR_EQUAL_TO.equals(operator)) { + return compare(a, b) == 0; + } else if (JCR_OPERATOR_GREATER_THAN.equals(operator)) { + return compare(a, b) > 0; + } else if (JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO.equals(operator)) { + return compare(a, b) >= 0; + } else if (JCR_OPERATOR_LESS_THAN.equals(operator)) { + return compare(a, b) < 0; + } else if (JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO.equals(operator)) { + return compare(a, b) <= 0; + } else if (JCR_OPERATOR_NOT_EQUAL_TO.equals(operator)) { + return compare(a, b) != 0; + } else if (JCR_OPERATOR_LIKE.equals(operator)) { + try { + Pattern pattern = Util.createRegexp(b.getString()); + return pattern.matcher(a.getString()).matches(); + } catch (RepositoryException e) { + throw new RuntimeException( + "Unable to compare values " + a + " and " + b, e); + } + } else { + throw new IllegalArgumentException( + "Unknown comparison operator: " + operator); + } + } + + public int compare(Value[] a, Value[] b) { + try { + return Util.compare(a, b); + } catch (RepositoryException e) { + throw new RuntimeException("Unable to compare values " + + Arrays.toString(a) + " and " + Arrays.toString(b), e); + } + } + + /** + * Evaluates the given QOM comparison operation with the given value arrays. + * + * @param operator QOM comparison operator + * @param a left values + * @param b right values + * @return result of the comparison + */ + public boolean evaluate(String operator, Value[] a, Value[] b) { + if (JCR_OPERATOR_EQUAL_TO.equals(operator)) { + return compare(a, b) == 0; + } else if (JCR_OPERATOR_GREATER_THAN.equals(operator)) { + return compare(a, b) > 0; + } else if (JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO.equals(operator)) { + return compare(a, b) >= 0; + } else if (JCR_OPERATOR_LESS_THAN.equals(operator)) { + return compare(a, b) < 0; + } else if (JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO.equals(operator)) { + return compare(a, b) <= 0; + } else if (JCR_OPERATOR_NOT_EQUAL_TO.equals(operator)) { + return compare(a, b) != 0; + } + // TODO JCR_OPERATOR_LIKE + throw new IllegalArgumentException("Unknown comparison operator: " + + operator); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/AbstractFieldComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/AbstractFieldComparator.java new file mode 100644 index 00000000000..91c3d34892b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/AbstractFieldComparator.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.core.query.lucene.sort; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.jackrabbit.core.query.lucene.FieldComparatorBase; +import org.apache.jackrabbit.core.query.lucene.FieldNames; +import org.apache.jackrabbit.core.query.lucene.MultiIndexReader; +import org.apache.lucene.document.Document; +import org.apache.lucene.index.IndexReader; + +/** + * Abstract base class for FieldComparators which keep their values + * (Comparables) in an array. + */ +public abstract class AbstractFieldComparator extends FieldComparatorBase { + + /** + * The values for comparing. + */ + private final Comparable[] values; + + /** + * The index readers. + */ + + protected final List readers = new ArrayList(); + /** + * The document number starts for the {@link #readers}. + */ + protected int[] starts; + + /** + * Create a new instance with the given number of values. + * + * @param numHits the number of values + */ + protected AbstractFieldComparator(int numHits) { + values = new Comparable[numHits]; + } + + /** + * Returns the reader index for document n. + * + * @param n document number. + * @return the reader index. + */ + protected final int readerIndex(int n) { + int lo = 0; + int hi = readers.size() - 1; + + while (hi >= lo) { + int mid = (lo + hi) >> 1; + int midValue = starts[mid]; + if (n < midValue) { + hi = mid - 1; + } + else if (n > midValue) { + lo = mid + 1; + } + else { + while (mid + 1 < readers.size() && starts[mid + 1] == midValue) { + mid++; + } + return mid; + } + } + return hi; + } + + /** + * Add the given value to the values array + * + * @param slot index into values + * @param value value for adding + */ + @Override + public void setValue(int slot, Comparable value) { + values[slot] = value; + } + + /** + * Return a value from the values array + * + * @param slot index to retrieve + * @return the retrieved value + */ + @Override + public Comparable getValue(int slot) { + return values[slot]; + } + + @Override + public void setNextReader(IndexReader reader, int docBase) throws IOException { + getIndexReaders(readers, reader); + + int maxDoc = 0; + starts = new int[readers.size() + 1]; + + for (int i = 0; i < readers.size(); i++) { + IndexReader r = readers.get(i); + starts[i] = maxDoc; + maxDoc += r.maxDoc(); + } + starts[readers.size()] = maxDoc; + } + + /** + * Checks if reader is of type {@link MultiIndexReader} and if + * so calls itself recursively for each reader within the + * MultiIndexReader or otherwise adds the reader to the list. + * + * @param readers list of index readers. + * @param reader reader to decompose + */ + private static void getIndexReaders(List readers, IndexReader reader) { + if (reader instanceof MultiIndexReader) { + for (IndexReader r : ((MultiIndexReader) reader).getIndexReaders()) { + getIndexReaders(readers, r); + } + } + else { + readers.add(reader); + } + } + + protected String getUUIDForIndex(int doc) throws IOException { + int idx = readerIndex(doc); + IndexReader reader = readers.get(idx); + Document document = reader.document(doc - starts[idx]); + return document.get(FieldNames.UUID); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/DynamicOperandFieldComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/DynamicOperandFieldComparator.java new file mode 100644 index 00000000000..9b343488c5d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/DynamicOperandFieldComparator.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.core.query.lucene.sort; + +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.query.qom.Ordering; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; + +public final class DynamicOperandFieldComparator extends + AbstractFieldComparator { + + private final Session session; + private final OperandEvaluator evaluator; + private final Ordering ordering; + + public DynamicOperandFieldComparator(final Session session, + final OperandEvaluator evaluator, final Ordering ordering, + int numHits) { + super(numHits); + this.session = session; + this.evaluator = evaluator; + this.ordering = ordering; + } + + @Override + protected Comparable sortValue(int doc) { + try { + final String uuid = getUUIDForIndex(doc); + final Node n = session.getNodeByIdentifier(uuid); + final Value[] v = evaluator.getValues(ordering.getOperand(), n); + return new ValueComparableWrapper(v); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/DynamicOperandFieldComparatorSource.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/DynamicOperandFieldComparatorSource.java new file mode 100644 index 00000000000..fd1a09fd9a6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/DynamicOperandFieldComparatorSource.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.core.query.lucene.sort; + +import java.io.IOException; +import java.util.Map; + +import javax.jcr.Session; +import javax.jcr.query.qom.Ordering; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; +import org.apache.lucene.search.FieldComparator; +import org.apache.lucene.search.FieldComparatorSource; + +public final class DynamicOperandFieldComparatorSource extends + FieldComparatorSource { + + private static final long serialVersionUID = 1L; + + private final Session session; + private final OperandEvaluator evaluator; + private Map orderByProperties; + + public DynamicOperandFieldComparatorSource(final Session session, + final OperandEvaluator evaluator, + final Map orderByProperties) { + this.session = session; + this.evaluator = evaluator; + this.orderByProperties = orderByProperties; + } + + @Override + public FieldComparator newComparator(String fieldname, int numHits, + int sortPos, boolean reversed) throws IOException { + final Ordering o = orderByProperties.get(fieldname); + return new DynamicOperandFieldComparator(session, evaluator, o, numHits); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/RowComparator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/RowComparator.java new file mode 100644 index 00000000000..94ecdcc2ea0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/RowComparator.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.core.query.lucene.sort; + +import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_ORDER_DESCENDING; + +import java.util.Comparator; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Row; +import javax.jcr.query.qom.Operand; +import javax.jcr.query.qom.Ordering; + +import org.apache.jackrabbit.commons.query.qom.OperandEvaluator; +import org.apache.jackrabbit.core.query.lucene.join.ValueComparator; + +/** + * Row comparator. + */ +public class RowComparator implements Comparator { + + private static final ValueComparator comparator = new ValueComparator(); + + private final Ordering[] orderings; + + private final OperandEvaluator evaluator; + + public RowComparator(Ordering[] orderings, OperandEvaluator evaluator) { + this.orderings = orderings; + this.evaluator = evaluator; + } + + public int compare(Row a, Row b) { + try { + for (Ordering ordering : orderings) { + Operand operand = ordering.getOperand(); + Value[] va = evaluator.getValues(operand, a); + Value[] vb = evaluator.getValues(operand, b); + int d = comparator.compare(va, vb); + if (d != 0) { + if (JCR_ORDER_DESCENDING.equals(ordering.getOrder())) { + return -d; + } else { + return d; + } + } + } + return 0; + } catch (RepositoryException e) { + throw new RuntimeException("Unable to compare rows " + a + " and " + + b, e); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/ValueComparableWrapper.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/ValueComparableWrapper.java new file mode 100644 index 00000000000..aeb4cfaf13e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/query/lucene/sort/ValueComparableWrapper.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.core.query.lucene.sort; + +import javax.jcr.Value; + +import org.apache.jackrabbit.core.query.lucene.join.ValueComparator; + +class ValueComparableWrapper implements Comparable { + + private static final ValueComparator comparator = new ValueComparator(); + + private final Value[] v; + + public ValueComparableWrapper(final Value[] v) { + this.v = v; + } + + public int compareTo(ValueComparableWrapper o) { + return comparator.compare(v, o.v); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/HoldImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/HoldImpl.java new file mode 100644 index 00000000000..6ee2db98dc6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/HoldImpl.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.core.retention; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.SessionImpl; +import javax.jcr.retention.Hold; + +import javax.jcr.Value; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; + +/** + * HoldImpl... + */ +class HoldImpl implements Hold { + + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + + private static final String DEEP = "D_"; + private static final String SHALLOW = "S_"; + + private final Name name; + private final boolean isDeep; + private final NodeId nodeId; + + private final NameResolver resolver; + + private int hashCode = 0; + + HoldImpl(Name name, boolean isDeep, NodeId nodeId, NameResolver resolver) { + this.name = name; + this.isDeep = isDeep; + this.nodeId = nodeId; + this.resolver = resolver; + } + + NodeId getNodeId() { + return nodeId; + } + + Value toValue(ValueFactory valueFactory) throws RepositoryException { + String str = ((isDeep) ? DEEP : SHALLOW) + name.toString(); + return valueFactory.createValue(str); + } + + static HoldImpl createFromValue(Value val, NodeId nodeId, NameResolver resolver) throws RepositoryException { + String str = val.getString(); + Name name = NAME_FACTORY.create(str.substring(2)); + boolean isDeep = str.startsWith(DEEP); + return new HoldImpl(name, isDeep, nodeId, resolver); + } + + static HoldImpl[] createFromProperty(PropertyImpl property, NodeId nodeId) throws RepositoryException { + Value[] vs = property.getValues(); + HoldImpl[] holds = new HoldImpl[vs.length]; + for (int i = 0; i < vs.length; i++) { + holds[i] = createFromValue(vs[i], nodeId, (SessionImpl) property.getSession()); + } + return holds; + } + + //---------------------------------------------------------------< Hold >--- + /** + * @see javax.jcr.retention.Hold#isDeep() + */ + public boolean isDeep() throws RepositoryException { + return isDeep; + } + + /** + * @see javax.jcr.retention.Hold#getName() + */ + public String getName() throws RepositoryException { + return resolver.getJCRName(name); + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + if (hashCode == 0) { + int h = 17; + h = 37 * h + name.hashCode(); + h = 37 * h + nodeId.hashCode(); + h = 37 * h + Boolean.valueOf(isDeep).hashCode(); + hashCode = h; + } + return hashCode; + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof HoldImpl) { + HoldImpl other = (HoldImpl) obj; + return isDeep == other.isDeep && name.equals(other.name) && nodeId.equals(other.nodeId); + } + return false; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionManagerImpl.java new file mode 100644 index 00000000000..a3f4d2222be --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionManagerImpl.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.retention; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.ProtectedItemModifier; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.retention.Hold; +import javax.jcr.retention.RetentionManager; +import javax.jcr.retention.RetentionPolicy; +import javax.jcr.version.VersionException; +import java.util.ArrayList; +import java.util.List; + +/** + * RetentionManagerImpl... + */ +public class RetentionManagerImpl extends ProtectedItemModifier implements RetentionManager { + + private static Logger log = LoggerFactory.getLogger(RetentionManagerImpl.class); + + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + + static final Name REP_RETENTION_MANAGEABLE = NAME_FACTORY.create(Name.NS_REP_URI, "RetentionManageable"); + static final Name REP_HOLD = NAME_FACTORY.create(Name.NS_REP_URI, "hold"); + static final Name REP_RETENTION_POLICY = NAME_FACTORY.create(Name.NS_REP_URI, "retentionPolicy"); + + private final SessionImpl session; + + /** + * + * @param session The editing session. + */ + public RetentionManagerImpl(SessionImpl session) { + super(Permission.RETENTION_MNGMT); + this.session = session; + } + + //---------------------------------------------------< RetentionManager >--- + /** + * @see RetentionManager#getHolds(String) + */ + public Hold[] getHolds(String absPath) throws PathNotFoundException, + AccessDeniedException, RepositoryException { + + NodeImpl n = (NodeImpl) session.getNode(absPath); + session.getAccessManager().checkPermission(session.getQPath(absPath), Permission.RETENTION_MNGMT); + + Hold[] holds; + if (n.isNodeType(REP_RETENTION_MANAGEABLE) && n.hasProperty(REP_HOLD)) { + holds = HoldImpl.createFromProperty(n.getProperty(REP_HOLD), n.getNodeId()); + } else { + holds = new Hold[0]; + } + return holds; + } + + /** + * @see RetentionManager#addHold(String, String, boolean) + */ + public Hold addHold(String absPath, String name, boolean isDeep) throws + PathNotFoundException, AccessDeniedException, LockException, + VersionException, RepositoryException { + + NodeImpl n = (NodeImpl) session.getNode(absPath); + if (!n.isNodeType(REP_RETENTION_MANAGEABLE)) { + n.addMixin(REP_RETENTION_MANAGEABLE); + } + + HoldImpl hold = new HoldImpl(session.getQName(name), isDeep, n.getNodeId(), session); + Value[] vls; + if (n.hasProperty(REP_HOLD)) { + Value[] vs = n.getProperty(REP_HOLD).getValues(); + // check if the same hold already exists + for (Value v : vs) { + if (hold.equals(HoldImpl.createFromValue(v, n.getNodeId(), session))) { + throw new RepositoryException("Hold already exists."); + } + } + vls = new Value[vs.length + 1]; + System.arraycopy(vs, 0, vls, 0, vs.length); + } else { + vls = new Value[1]; + } + + // add the value of the new hold + vls[vls.length - 1] = hold.toValue(session.getValueFactory()); + setProperty(n, REP_HOLD, vls); + return hold; + } + + /** + * @see RetentionManager#removeHold(String, Hold) + */ + public void removeHold(String absPath, Hold hold) throws + PathNotFoundException, AccessDeniedException, LockException, + VersionException, RepositoryException { + + NodeImpl n = (NodeImpl) session.getNode(absPath); + if (hold instanceof HoldImpl + && n.getNodeId().equals(((HoldImpl) hold).getNodeId()) + && n.isNodeType(REP_RETENTION_MANAGEABLE) + && n.hasProperty(REP_HOLD)) { + + PropertyImpl p = n.getProperty(REP_HOLD); + Value[] vls = p.getValues(); + + List newValues = new ArrayList(vls.length - 1); + for (Value v : vls) { + if (!hold.equals(HoldImpl.createFromValue(v, n.getNodeId(), session))) { + newValues.add(v); + } + } + if (newValues.size() < vls.length) { + if (newValues.size() == 0) { + removeItem(p); + } else { + setProperty(n, REP_HOLD, newValues.toArray(new Value[newValues.size()])); + } + } else { + // no matching hold. + throw new RepositoryException("Cannot remove '" + hold.getName() + "' at " + absPath + "."); + } + } else { + // invalid hold or no hold at absPath + throw new RepositoryException("Cannot remove '" + hold.getName() + "' at " + absPath + "."); + } + } + + /** + * @see RetentionManager#getRetentionPolicy(String) + */ + public RetentionPolicy getRetentionPolicy(String absPath) throws + PathNotFoundException, AccessDeniedException, RepositoryException { + + NodeImpl n = (NodeImpl) session.getNode(absPath); + session.getAccessManager().checkPermission(session.getQPath(absPath), Permission.RETENTION_MNGMT); + + RetentionPolicy rPolicy = null; + if (n.isNodeType(REP_RETENTION_MANAGEABLE) && n.hasProperty(REP_RETENTION_POLICY)) { + String jcrName = n.getProperty(REP_RETENTION_POLICY).getString(); + rPolicy = new RetentionPolicyImpl(jcrName, n.getNodeId(), session); + } + + return rPolicy; + } + + /** + * @see RetentionManager#setRetentionPolicy(String, RetentionPolicy) + */ + public void setRetentionPolicy(String absPath, RetentionPolicy retentionPolicy) + throws PathNotFoundException, AccessDeniedException, LockException, + VersionException, RepositoryException { + + NodeImpl n = (NodeImpl) session.getNode(absPath); + if (!(retentionPolicy instanceof RetentionPolicyImpl)) { + throw new RepositoryException("Invalid retention policy."); + } + Value retentionReference = session.getValueFactory().createValue(retentionPolicy.getName(), PropertyType.NAME); + if (!n.isNodeType(REP_RETENTION_MANAGEABLE)) { + n.addMixin(REP_RETENTION_MANAGEABLE); + } + setProperty(n, REP_RETENTION_POLICY, retentionReference); + } + + /** + * @see RetentionManager#removeRetentionPolicy(String) + */ + public void removeRetentionPolicy(String absPath) throws + PathNotFoundException, AccessDeniedException, LockException, + VersionException, RepositoryException { + + NodeImpl n = (NodeImpl) session.getNode(absPath); + if (n.isNodeType(REP_RETENTION_MANAGEABLE) && n.hasProperty(REP_RETENTION_POLICY)) { + removeItem(n.getProperty(REP_RETENTION_POLICY)); + } else { + throw new RepositoryException("Cannot remove retention policy at absPath."); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionPolicyImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionPolicyImpl.java new file mode 100644 index 00000000000..4ef737cc1f9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionPolicyImpl.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.core.retention; + +import javax.jcr.retention.RetentionPolicy; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; + +import javax.jcr.RepositoryException; +import javax.jcr.NamespaceException; +import javax.jcr.Session; + +/** + * Basic implementation of the RetentionPolicy interface. + */ +public class RetentionPolicyImpl implements RetentionPolicy { + + private final Name name; + private final NodeId nodeId; + private final NameResolver resolver; + + private int hashCode = 0; + + /** + * Creates a new RetentionPolicy that can be applied to a + * Node using {@link javax.jcr.retention.RetentionManager#setRetentionPolicy(String, javax.jcr.retention.RetentionPolicy)}. + * + * @param jcrName The name of the policy. It must be a valid JCR name. + * @param session The editing Session from which the retention + * manager will be obtained. + * @return a new RetentionPolicy + * @throws RepositoryException If the jcr name isn't valid or if same other + * error occurs. + */ + public static RetentionPolicy createRetentionPolicy(String jcrName, Session session) throws RepositoryException { + NameResolver resolver; + if (session instanceof NameResolver) { + resolver = (NameResolver) session; + } else { + resolver = new DefaultNamePathResolver(session); + } + return new RetentionPolicyImpl(jcrName, null, resolver); + } + + RetentionPolicyImpl(String jcrName, NodeId nodeId, NameResolver resolver) throws IllegalNameException, NamespaceException { + this(resolver.getQName(jcrName), nodeId, resolver); + } + + private RetentionPolicyImpl(Name name, NodeId nodeId, NameResolver resolver) { + this.name = name; + this.nodeId = nodeId; + this.resolver = resolver; + } + + NodeId getNodeId() { + return nodeId; + } + + //----------------------------------------------------< RetentionPolicy >--- + /** + * @see javax.jcr.retention.RetentionPolicy#getName() + */ + public String getName() throws RepositoryException { + return resolver.getJCRName(name); + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + if (hashCode == 0) { + int h = 17; + h = 37 * h + name.hashCode(); + h = 37 * h + nodeId.hashCode(); + hashCode = h; + } + return hashCode; + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof RetentionPolicyImpl) { + RetentionPolicyImpl other = (RetentionPolicyImpl) obj; + return name.equals(other.name) && ((nodeId == null) ? other.nodeId == null : nodeId.equals(other.nodeId)); + } + return false; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionRegistry.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionRegistry.java new file mode 100644 index 00000000000..c0f727f0f1c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionRegistry.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.core.retention; + +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.RepositoryException; + +/** + * RetentionEvaluator... + */ +public interface RetentionRegistry { + + public boolean hasEffectiveHold(Path nodePath, boolean checkParent) throws RepositoryException; + + public boolean hasEffectiveRetention(Path nodePath, boolean checkParent) throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionRegistryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionRegistryImpl.java new file mode 100644 index 00000000000..bc2329c0d17 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/retention/RetentionRegistryImpl.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.core.retention; + +import org.apache.commons.io.IOUtils; +import javax.jcr.retention.Hold; +import javax.jcr.retention.RetentionPolicy; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.PathMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * RetentionEvaluatorImpl... + */ +public class RetentionRegistryImpl implements RetentionRegistry, SynchronousEventListener { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(RetentionRegistryImpl.class); + /** + * Name of the file storing the existing retention/holds + */ + private static final String FILE_NAME = "retention"; + + private final PathMap retentionMap = + new PathMap(); + + private final PathMap> holdMap = new PathMap>(); + + private final SessionImpl session; + private final FileSystemResource retentionFile; + + private long holdCnt; + private long retentionCnt; + + private boolean initialized; + + public RetentionRegistryImpl(SessionImpl session, FileSystem fs) throws RepositoryException { + this.session = session; + this.retentionFile = new FileSystemResource(fs, FileSystem.SEPARATOR + FILE_NAME); + + // start listening to added/changed or removed holds and retention policies. + Workspace wsp = session.getWorkspace(); + // register event listener to be informed about new/removed holds and + // retention policies. + int types = Event.PROPERTY_ADDED | Event.PROPERTY_REMOVED | Event.PROPERTY_CHANGED; + String[] ntFilter = new String[] {session.getJCRName(RetentionManagerImpl.REP_RETENTION_MANAGEABLE)}; + wsp.getObservationManager().addEventListener(this, types, "/", true, null, ntFilter, false); + + // populate the retentionMap and the holdMap with the effective + // holds and retention policies present within the content. + try { + readRetentionFile(); + } catch (FileSystemException e) { + throw new RepositoryException("Error while reading retention/holds from '" + retentionFile.getPath() + "'", e); + } catch (IOException e) { + throw new RepositoryException("Error while reading retention/holds from '" + retentionFile.getPath() + "'", e); + } + initialized = true; + } + + /** + * Read the file system resource containing the node ids of those nodes + * contain holds/retention policies and populate the 2 path maps. + * + * If an entry in the retention file doesn't have a corresponding entry + * (either rep:hold property or rep:retentionPolicy property at the + * node identified by the node id entry) or doesn't correspond to an existing + * node, that entry will be ignored. Upon {@link #close()} of this + * manager, the file will be updated to reflect the actual set of holds/ + * retentions present and effective in the content. + * + * @throws IOException + * @throws FileSystemException + */ + private void readRetentionFile() throws IOException, FileSystemException { + if (retentionFile.exists()) { + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(retentionFile.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + NodeId nodeId = NodeId.valueOf(line); + try { + NodeImpl node = (NodeImpl) session.getItemManager().getItem(nodeId); + Path nodePath = node.getPrimaryPath(); + + if (node.hasProperty(RetentionManagerImpl.REP_HOLD)) { + PropertyImpl prop = node.getProperty(RetentionManagerImpl.REP_HOLD); + addHolds(nodePath, prop); + } + if (node.hasProperty(RetentionManagerImpl.REP_RETENTION_POLICY)) { + PropertyImpl prop = node.getProperty(RetentionManagerImpl.REP_RETENTION_POLICY); + addRetentionPolicy(nodePath, prop); + } + } catch (RepositoryException e) { + // node doesn't exist any more or hold/retention has been removed. + // ignore. upon close() the file will not contain the given nodeId + // any more. + log.warn("Unable to read retention policy / holds from node '" + nodeId + "': " + e.getMessage()); + } + } + } finally { + IOUtils.closeQuietly(reader); + } + } + } + + /** + * Write back the file system resource containing the node ids of those + * nodes containing holds and/or retention policies. Each node id is + * present only once. + */ + private void writeRetentionFile() { + final Set nodeIds = new HashSet(); + + // first look for nodes containing holds + holdMap.traverse(new PathMap.ElementVisitor>() { + public void elementVisited(PathMap.Element> element) { + List holds = element.get(); + if (!holds.isEmpty()) { + nodeIds.add(holds.get(0).getNodeId()); + } + } + }, false); + + // then collect ids of nodes having an retention policy + retentionMap.traverse(new PathMap.ElementVisitor() { + public void elementVisited(PathMap.Element element) { + nodeIds.add(element.get().getNodeId()); + } + }, false); + + if (!nodeIds.isEmpty()) { + BufferedWriter writer = null; + try { + writer = new BufferedWriter(new OutputStreamWriter(retentionFile.getOutputStream())); + for (Iterator it = nodeIds.iterator(); it.hasNext();) { + writer.write(it.next().toString()); + if (it.hasNext()) { + writer.newLine(); + } + } + } catch (FileSystemException fse) { + log.error("Error while saving locks to '" + retentionFile.getPath() + "': " + fse.getMessage()); + } catch (IOException ioe) { + log.error("Error while saving locks to '" + retentionFile.getPath() + "': " + ioe.getMessage()); + } finally { + IOUtils.closeQuietly(writer); + } + } + } + + public void close() { + writeRetentionFile(); + initialized = false; + } + + private void addHolds(Path nodePath, PropertyImpl p) throws RepositoryException { + synchronized (holdMap) { + HoldImpl[] holds = HoldImpl.createFromProperty(p, ((PropertyId) p.getId()).getParentId()); + holdMap.put(nodePath, Arrays.asList(holds)); + holdCnt++; + } + } + + private void removeHolds(Path nodePath) { + synchronized (holdMap) { + PathMap.Element> el = holdMap.map(nodePath, true); + if (el != null) { + el.remove(); + holdCnt--; + } // else: no entry for holds on nodePath (should not occur) + } + } + + private void addRetentionPolicy(Path nodePath, PropertyImpl p) throws RepositoryException { + synchronized (retentionMap) { + RetentionPolicyImpl rp = new RetentionPolicyImpl( + p.getString(), ((PropertyId) p.getId()).getParentId(), session); + retentionMap.put(nodePath, rp); + retentionCnt++; + } + } + + private void removeRetentionPolicy(Path nodePath) { + synchronized (retentionMap) { + PathMap.Element el = + retentionMap.map(nodePath, true); + if (el != null) { + el.remove(); + retentionCnt--; + } // else: no entry for holds on nodePath (should not occur) + } + } + + //--------------------------------------------------< RetentionRegistry >--- + /** + * @see RetentionRegistry#hasEffectiveHold(org.apache.jackrabbit.spi.Path,boolean) + */ + public boolean hasEffectiveHold(Path nodePath, boolean checkParent) throws RepositoryException { + if (!initialized) { + throw new IllegalStateException("Not initialized."); + } + if (holdCnt <= 0) { + return false; + } + PathMap.Element> element = holdMap.map(nodePath, false); + List holds = element.get(); + if (holds != null) { + if (element.hasPath(nodePath)) { + // one or more holds on the specified path + return true; + } else if (checkParent && !nodePath.denotesRoot() && + element.hasPath(nodePath.getAncestor(1))) { + // hold present on the parent node without checking for being + // a deep hold. + // this required for removal of a node that can be inhibited + // by a hold on the node itself, by a hold on the parent or + // by a deep hold on any ancestor. + return true; + } else { + for (Hold hold : holds) { + if (hold.isDeep()) { + return true; + } + } + } + } + // no hold at path or no deep hold on parent. + return false; + } + + /** + * @see RetentionRegistry#hasEffectiveRetention(org.apache.jackrabbit.spi.Path,boolean) + */ + public boolean hasEffectiveRetention(Path nodePath, boolean checkParent) throws RepositoryException { + if (!initialized) { + throw new IllegalStateException("Not initialized."); + } + if (retentionCnt <= 0) { + return false; + } + RetentionPolicy rp = null; + PathMap.Element element = retentionMap.map(nodePath, true); + if (element != null) { + rp = element.get(); + } + if (rp == null && checkParent && (!nodePath.denotesRoot())) { + element = retentionMap.map(nodePath.getAncestor(1), true); + if (element != null) { + rp = element.get(); + } + } + return rp != null; + } + + //-------------------------------------------< SynchronousEventListener >--- + /** + * @param events Events reporting hold/retention policy changes. + */ + public void onEvent(EventIterator events) { + while (events.hasNext()) { + Event ev = events.nextEvent(); + try { + Path evPath = session.getQPath(ev.getPath()); + Path nodePath = evPath.getAncestor(1); + Name propName = evPath.getName(); + + if (RetentionManagerImpl.REP_HOLD.equals(propName)) { + // hold changes + switch (ev.getType()) { + case Event.PROPERTY_ADDED: + case Event.PROPERTY_CHANGED: + // build the Hold objects from the rep:hold property + // and put them into the hold map. + PropertyImpl p = (PropertyImpl) session.getProperty(ev.getPath()); + addHolds(nodePath, p); + break; + case Event.PROPERTY_REMOVED: + // all holds present on this node were remove + // -> remove the corresponding entry in the holdMap. + removeHolds(nodePath); + break; + } + } else if (RetentionManagerImpl.REP_RETENTION_POLICY.equals(propName)) { + // retention policy changes + switch (ev.getType()) { + case Event.PROPERTY_ADDED: + case Event.PROPERTY_CHANGED: + // build the RetentionPolicy objects from the rep:retentionPolicy property + // and put it into the retentionMap. + PropertyImpl p = (PropertyImpl) session.getProperty(ev.getPath()); + addRetentionPolicy(nodePath, p); + break; + case Event.PROPERTY_REMOVED: + // retention policy present on this node was remove + // -> remove the corresponding entry in the retentionMap. + removeRetentionPolicy(nodePath); + break; + } + } + // else: not interested in any other property -> ignore. + + } catch (RepositoryException e) { + log.warn("Internal error while processing event. {}", e.getMessage()); + // ignore. + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AMContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AMContext.java new file mode 100644 index 00000000000..9f0172958cc --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AMContext.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.core.security; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.Session; +import javax.security.auth.Subject; +import java.io.File; + +/** + * An AMContext is used to provide Session specific + * context information for an AccessManager. + * + * @see AccessManager#init(AMContext) + * @see AccessManager#init(AMContext, org.apache.jackrabbit.core.security.authorization.AccessControlProvider, org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager) + */ +public class AMContext { + + /** + * the physical home dir + */ + private final File physicalHomeDir; + + /** + * the virtual jackrabbit filesystem + */ + private final FileSystem fs; + + /** + * Subject whose access rights the access manager should reflect + */ + private final Subject subject; + + private final Session session; + + /** + * hierarchy manager for resolving ItemId-to-Path mapping + */ + private final HierarchyManager hierMgr; + + /** + * The privilege manager + */ + private final PrivilegeManager privilegeMgr; + + /** + * name and path resolver for resolving JCR name/path strings to internal + * Name/Path objects (and vice versa). + */ + private final NamePathResolver resolver; + + /** + * name of the workspace + */ + private final String workspaceName; + + /** + * Creates a new AMContext. + * + * @param physicalHomeDir the physical home directory + * @param fs the virtual jackrabbit filesystem + * @param session the session. + * @param subject subject whose access rights should be reflected + * @param hierMgr hierarchy manager + * @param privilegeMgr privilege manager + * @param resolver name and path resolver + * @param workspaceName workspace name + */ + public AMContext(File physicalHomeDir, + FileSystem fs, + Session session, + Subject subject, + HierarchyManager hierMgr, + PrivilegeManager privilegeMgr, + NamePathResolver resolver, + String workspaceName) { + this.physicalHomeDir = physicalHomeDir; + this.fs = fs; + this.session = session; + this.subject = subject; + this.hierMgr = hierMgr; + this.privilegeMgr = privilegeMgr; + this.resolver = resolver; + this.workspaceName = workspaceName; + } + + + /** + * Returns the physical home directory + * + * @return the physical home directory + */ + public File getHomeDir() { + return physicalHomeDir; + } + + /** + * Returns the virtual filesystem + * + * @return the virtual filesystem + */ + public FileSystem getFileSystem() { + return fs; + } + + /** + * Returns the session + * + * @return the session + */ + public Session getSession() { + return session; + } + + /** + * Returns the subject + * + * @return the subject + */ + public Subject getSubject() { + return subject; + } + + /** + * Returns the hierarchy manager + * + * @return the hierarchy manager + */ + public HierarchyManager getHierarchyManager() { + return hierMgr; + } + + /** + * Returns the privilege manager + * + * @return the privilege manager + */ + public PrivilegeManager getPrivilegeManager() { + return privilegeMgr; + } + + /** + * Returns the namespace resolver + * + * @return the namespace resolver + */ + public NamePathResolver getNamePathResolver() { + return resolver; + } + + /** + * Returns the name of the workspace. + * + * @return the name of the workspace + */ + public String getWorkspaceName() { + return workspaceName; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AbstractAccessControlManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AbstractAccessControlManager.java new file mode 100644 index 00000000000..aa553164067 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AbstractAccessControlManager.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.commons.iterator.AccessControlPolicyIteratorAdapter; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import java.security.Principal; + +/** + * AbstractAccessControlManager... + */ +public abstract class AbstractAccessControlManager implements JackrabbitAccessControlManager { + + private static Logger log = LoggerFactory.getLogger(AbstractAccessControlManager.class); + + /** + * Always returns all registered Privileges. + * + * @param absPath Path to an existing node. + * @return Always returns all registered Privileges. + * @see javax.jcr.security.AccessControlManager#getSupportedPrivileges(String) + */ + public Privilege[] getSupportedPrivileges(String absPath) throws PathNotFoundException, RepositoryException { + checkInitialized(); + checkValidNodePath(absPath); + + // return all known privileges everywhere. + return getPrivilegeManager().getRegisteredPrivileges(); + } + + /** + * @see javax.jcr.security.AccessControlManager#privilegeFromName(String) + */ + public Privilege privilegeFromName(String privilegeName) + throws AccessControlException, RepositoryException { + checkInitialized(); + + return getPrivilegeManager().getPrivilege(privilegeName); + } + + /** + * Returns null. + * + * @param absPath Path to an existing node. + * @return always returns null. + * @see javax.jcr.security.AccessControlManager#getApplicablePolicies(String) + */ + public AccessControlPolicy[] getPolicies(String absPath) throws PathNotFoundException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.READ_AC); + + log.debug("Implementation does not provide applicable policies -> getPolicy() always returns an empty array."); + return new AccessControlPolicy[0]; + } + + /** + * Returns an empty iterator. + * + * @param absPath Path to an existing node. + * @return always returns an empty iterator. + * @see javax.jcr.security.AccessControlManager#getApplicablePolicies(String) + */ + public AccessControlPolicyIterator getApplicablePolicies(String absPath) throws PathNotFoundException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.READ_AC); + + log.debug("Implementation does not provide applicable policies -> returning empty iterator."); + return AccessControlPolicyIteratorAdapter.EMPTY; + } + + /** + * Always throws AccessControlException + * + * @see javax.jcr.security.AccessControlManager#setPolicy(String, AccessControlPolicy) + */ + public void setPolicy(String absPath, AccessControlPolicy policy) throws PathNotFoundException, AccessControlException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.MODIFY_AC); + + throw new AccessControlException("AccessControlPolicy " + policy + " cannot be applied."); + } + + /** + * Always throws AccessControlException + * + * @see javax.jcr.security.AccessControlManager#removePolicy(String, AccessControlPolicy) + */ + public void removePolicy(String absPath, AccessControlPolicy policy) throws PathNotFoundException, AccessControlException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.MODIFY_AC); + + throw new AccessControlException("No AccessControlPolicy has been set through this API -> Cannot be removed."); + } + + + //-------------------------------------< JackrabbitAccessControlManager >--- + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getApplicablePolicies(java.security.Principal) + */ + public JackrabbitAccessControlPolicy[] getApplicablePolicies(Principal principal) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException { + checkInitialized(); + + log.debug("Implementation does not provide applicable policies -> returning empty array."); + return new JackrabbitAccessControlPolicy[0]; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getPolicies(java.security.Principal) + */ + public JackrabbitAccessControlPolicy[] getPolicies(Principal principal) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException { + checkInitialized(); + + log.debug("Implementation does not provide applicable policies -> returning empty array."); + return new JackrabbitAccessControlPolicy[0]; + } + + //-------------------------------------------------------------------------- + /** + * Check if this manager has been properly initialized. + * + * @throws IllegalStateException If this manager has not been properly initialized. + */ + protected abstract void checkInitialized() throws IllegalStateException; + + /** + * Check if the specified privileges are granted at absPath. + * + * @param absPath Path to an existing node. + * @param permission Permissions to be checked. + * @throws AccessDeniedException if the session does not have the + * specified privileges. + * @throws PathNotFoundException if no node exists at absPath + * of if the session does not have the permission to READ it. + * @throws RepositoryException If another error occurs. + */ + protected abstract void checkPermission(String absPath, int permission) throws AccessDeniedException, PathNotFoundException, RepositoryException; + + /** + * @return the privilege manager + * @throws RepositoryException If another error occurs. + */ + protected abstract PrivilegeManager getPrivilegeManager() throws RepositoryException; + + /** + * Tests if the given absPath is absolute and points to an existing node. + * + * @param absPath Path to an existing node. + * @throws PathNotFoundException if no node at absPath exists + * or the session does not have privilege to retrieve the node. + * @throws RepositoryException If the given absPath is not + * absolute or if some other error occurs. + */ + protected abstract void checkValidNodePath(String absPath) throws PathNotFoundException, RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AccessManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AccessManager.java new file mode 100644 index 00000000000..3343bafe4d0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AccessManager.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.core.security; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +/** + * The AccessManager can be queried to determines whether privileges + * are granted on a specific item. + */ +public interface AccessManager { + + /** + * READ permission constant + * @deprecated + */ + int READ = 1; + + /** + * WRITE permission constant + * @deprecated + */ + int WRITE = 2; + + /** + * REMOVE permission constant + * @deprecated + */ + int REMOVE = 4; + + /** + * Initialize this access manager. An AccessDeniedException will + * be thrown if the subject of the given context is not + * granted access to the specified workspace. + * + * @param context access manager context + * @throws AccessDeniedException if the subject is not granted access + * to the specified workspace. + * @throws Exception if another error occurs + */ + void init(AMContext context) throws AccessDeniedException, Exception; + + /** + * Initialize this access manager. An AccessDeniedException will + * be thrown if the subject of the given context is not + * granted access to the specified workspace. + * + * @param context access manager context. + * @param acProvider The access control provider. + * @param wspAccessMgr The workspace access manager. + * @throws AccessDeniedException if the subject is not granted access + * to the specified workspace. + * @throws Exception if another error occurs + */ + void init(AMContext context, AccessControlProvider acProvider, + WorkspaceAccessManager wspAccessMgr) throws AccessDeniedException, Exception; + + /** + * Close this access manager. After having closed an access manager, + * further operations on this object are treated as illegal and throw + * + * @throws Exception if an error occurs + */ + void close() throws Exception; + + /** + * Determines whether the specified permissions are granted + * on the item with the specified id (i.e. the target item). + * + * @param id the id of the target item + * @param permissions A combination of one or more of the following constants + * encoded as a bitmask value: + *
      + *
    • READ
    • + *
    • WRITE
    • + *
    • REMOVE
    • + *
    + * @throws AccessDeniedException if permission is denied + * @throws ItemNotFoundException if the target item does not exist + * @throws RepositoryException it an error occurs + * @deprecated + */ + void checkPermission(ItemId id, int permissions) + throws AccessDeniedException, ItemNotFoundException, RepositoryException; + + /** + * Determines whether the specified permissions are granted + * on the item with the specified id (i.e. the target item). + * + * @param absPath Path to an item. + * @param permissions A combination of one or more of the + * {@link org.apache.jackrabbit.core.security.authorization.Permission} + * constants encoded as a bitmask value. + * @throws AccessDeniedException if permission is denied + * @throws RepositoryException it another error occurs + */ + void checkPermission(Path absPath, int permissions) throws AccessDeniedException, RepositoryException; + + /** + * Determines whether the specified permissions are granted + * on the repository level. + * + * @param permissions The permissions to check. + * @throws AccessDeniedException if permissions are denied. + * @throws RepositoryException if another error occurs. + */ + void checkRepositoryPermission(int permissions) throws AccessDeniedException, RepositoryException; + + /** + * Determines whether the specified permissions are granted + * on the item with the specified id (i.e. the target item). + * + * @param id the id of the target item + * @param permissions A combination of one or more of the following constants + * encoded as a bitmask value: + *
      + *
    • READ
    • + *
    • WRITE
    • + *
    • REMOVE
    • + *
    + * @return true if permission is granted; otherwise false + * @throws ItemNotFoundException if the target item does not exist + * @throws RepositoryException if another error occurs + * @deprecated + */ + boolean isGranted(ItemId id, int permissions) + throws ItemNotFoundException, RepositoryException; + + /** + * Determines whether the specified permissions are granted + * on the item with the specified absPath (i.e. the target + * item, that may or may not yet exist). + * + * @param absPath the absolute path to test + * @param permissions A combination of one or more of the + * {@link org.apache.jackrabbit.core.security.authorization.Permission} + * constants encoded as a bitmask value. + * @return true if the specified permissions are granted; + * otherwise false. + * @throws RepositoryException if an error occurs. + */ + boolean isGranted(Path absPath, int permissions) throws RepositoryException; + + /** + * Determines whether the specified permissions are granted + * on an item represented by the combination of the given + * parentPath and childName (i.e. the target + * item, that may or may not yet exist). + * + * @param parentPath Path to an existing parent node. + * @param childName Name of the child item that may or may not exist yet. + * @param permissions A combination of one or more of the + * {@link org.apache.jackrabbit.core.security.authorization.Permission} + * constants encoded as a bitmask value. + * @return true if the specified permissions are granted; + * otherwise false. + * @throws RepositoryException if an error occurs. + */ + boolean isGranted(Path parentPath, Name childName, int permissions) throws RepositoryException; + + /** + * Determines whether the item with the specified itemPath + * or itemId can be read. Either of the two parameters + * may be null.
    + * Note, that this method should only be called for persisted items as NEW + * items may not be visible to the permission evaluation. + * For new items {@link #isGranted(Path, int)} should be used instead. + *

    + * If this method is called with both Path and ItemId it is left to the + * evaluation, which parameter is used. + * + * @param itemPath The path to the item or null if itemId + * should be used to determine the READ permission. + * @param itemId Id of the item to be tested or null if the + * itemPath should be used to determine the permission. + * @return true if the item can be read; otherwise false. + * @throws RepositoryException if the item is NEW and only an itemId is + * specified or if another error occurs. + */ + boolean canRead(Path itemPath, ItemId itemId) throws RepositoryException; + + /** + * Determines whether the subject of the current context is granted access + * to the given workspace. Note that an implementation is free to test for + * the existence of a workspace with the specified name. In this case + * the expected return value is false, if no such workspace + * exists. + * + * @param workspaceName name of workspace + * @return true if the subject of the current context is + * granted access to the given workspace; otherwise false. + * @throws RepositoryException if an error occurs. + */ + boolean canAccess(String workspaceName) throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AnonymousPrincipal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AnonymousPrincipal.java new file mode 100644 index 00000000000..8bf33200e65 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/AnonymousPrincipal.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.core.security; + +import java.io.Serializable; +import java.security.Principal; + +/** + * A AnonymousPrincipal ... + */ +public final class AnonymousPrincipal implements Principal, Serializable { + + private static final String ANONYMOUS_NAME = "anonymous"; + + /** + * Creates an AnonymousPrincipal. + */ + public AnonymousPrincipal() { + } + + //-------------------------------------------------------------< Object >--- + @Override + public String toString() { + return ("AnonymousPrincipal"); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof AnonymousPrincipal) { + return true; + } + return false; + } + + @Override + public int hashCode() { + return ANONYMOUS_NAME.hashCode(); + } + + //----------------------------------------------------------< Principal >--- + /** + * @return Always returns "anonymous" + * @see Principal#getName() + */ + public String getName() { + return ANONYMOUS_NAME; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/DefaultAccessManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/DefaultAccessManager.java new file mode 100644 index 00000000000..0327f4c1b5d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/DefaultAccessManager.java @@ -0,0 +1,560 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.commons.iterator.AccessControlPolicyIteratorAdapter; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.security.authorization.AccessControlEditor; +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import javax.security.auth.Subject; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * The DefaultAccessManager controls access by evaluating access + * control policies for the Subject attached to the + * Session this manager has been built for.

    + * Please note the following exceptional situations:
    + * This manager allows all privileges for a particular item if + *

      + *
    • the Session's represents a system session or a session associated with + * the repository's administrator
    • + *
    + *

    + * It allows to access all available workspaces if + *

      + *
    • no WorkspaceAccessManager is defined.
    • + *
    + *

    + * How access control policies are matched to a particular item is defined by + * the AccessControlProvider set to this AccessManager. + * + * @see AccessManager + * @see javax.jcr.security.AccessControlManager + */ +public class DefaultAccessManager extends AbstractAccessControlManager implements AccessManager { + + private static final Logger log = LoggerFactory.getLogger(DefaultAccessManager.class); + + private boolean initialized; + + private NamePathResolver resolver; + + private Set principals; + + private AccessControlProvider acProvider; + + private AccessControlEditor editor; + + /** + * the workspace access + */ + private WorkspaceAccess wspAccess; + + /** + * the hierarchy manager used to resolve path from itemId + */ + private HierarchyManager hierMgr; + + /** + * The privilege manager + */ + private PrivilegeManager privilegeManager; + + /** + * The permissions that apply for the principals, that are present with + * the session subject this manager has been created for. + * TODO: if the users group-membership gets modified the compiledPermissions + * TODO should ev. be recalculated. currently those modifications are only + * TODO reflected upon re-login to the repository. + */ + private CompiledPermissions compiledPermissions; + + //------------------------------------------------------< AccessManager >--- + /** + * @see AccessManager#init(AMContext) + */ + public void init(AMContext amContext) throws AccessDeniedException, Exception { + init(amContext, null, null); + } + + /** + * @see AccessManager#init(AMContext, AccessControlProvider, WorkspaceAccessManager) + */ + public void init(AMContext amContext, AccessControlProvider acProvider, + WorkspaceAccessManager wspAccessManager) throws AccessDeniedException, Exception { + if (initialized) { + throw new IllegalStateException("Already initialized."); + } + + this.acProvider = acProvider; + + resolver = amContext.getNamePathResolver(); + hierMgr = amContext.getHierarchyManager(); + + Subject subject = amContext.getSubject(); + if (subject == null) { + principals = Collections.emptySet(); + } else { + principals = subject.getPrincipals(); + } + + wspAccess = new WorkspaceAccess(wspAccessManager, isSystemOrAdmin(amContext.getSession())); + privilegeManager = amContext.getPrivilegeManager(); + + if (acProvider != null) { + editor = acProvider.getEditor(amContext.getSession()); + compiledPermissions = acProvider.compilePermissions(principals); + } else { + log.warn("No AccessControlProvider defined -> no access is granted."); + editor = null; + compiledPermissions = CompiledPermissions.NO_PERMISSION; + } + + initialized = true; + + if (!canAccess(amContext.getWorkspaceName())) { + throw new AccessDeniedException("Not allowed to access Workspace " + amContext.getWorkspaceName()); + } + } + + /** + * @see AccessManager#close() + */ + public void close() throws Exception { + if (!initialized) { + throw new IllegalStateException("Manager is not initialized."); + } + initialized = false; + compiledPermissions.close(); + + hierMgr = null; + acProvider = null; + editor = null; + wspAccess = null; + } + + /** + * @see AccessManager#checkPermission(ItemId, int) + */ + public void checkPermission(ItemId id, int permissions) throws AccessDeniedException, ItemNotFoundException, RepositoryException { + if (!isGranted(id, permissions)) { + throw new AccessDeniedException("Access denied."); + } + } + + /** + * @see AccessManager#checkPermission(Path, int) + */ + public void checkPermission(Path absPath, int permissions) throws AccessDeniedException, RepositoryException { + if (!isGranted(absPath, permissions)) { + throw new AccessDeniedException("Access denied."); + } + } + + /** + * @see AccessManager#checkRepositoryPermission(int) + */ + public void checkRepositoryPermission(int permissions) throws AccessDeniedException, RepositoryException { + checkInitialized(); + if (!compiledPermissions.grants(null, permissions)) { + throw new AccessDeniedException("Access denied."); + } + } + + /** + * @see AccessManager#isGranted(ItemId, int) + */ + public boolean isGranted(ItemId id, int actions) + throws ItemNotFoundException, RepositoryException { + checkInitialized(); + if (actions == READ && compiledPermissions.canReadAll()) { + return true; + } else { + int perm = 0; + if ((actions & READ) == READ) { + perm |= Permission.READ; + } + if ((actions & WRITE) == WRITE) { + if (id.denotesNode()) { + // TODO: check again if correct + perm |= Permission.SET_PROPERTY; + perm |= Permission.ADD_NODE; + } else { + perm |= Permission.SET_PROPERTY; + } + } + if ((actions & REMOVE) == REMOVE) { + perm |= (id.denotesNode()) ? Permission.REMOVE_NODE : Permission.REMOVE_PROPERTY; + } + + Path path = hierMgr.getPath(id); + return isGranted(path, perm); + } + } + + /** + * @see AccessManager#isGranted(Path, int) + */ + public boolean isGranted(Path absPath, int permissions) throws RepositoryException { + checkInitialized(); + if (!absPath.isAbsolute()) { + throw new RepositoryException("Absolute path expected"); + } + return compiledPermissions.grants(absPath, permissions); + } + + /** + * @see AccessManager#isGranted(Path, Name, int) + */ + public boolean isGranted(Path parentPath, Name childName, int permissions) throws RepositoryException { + Path p = PathFactoryImpl.getInstance().create(parentPath, childName, true); + return isGranted(p, permissions); + } + + /** + * @see AccessManager#canRead(org.apache.jackrabbit.spi.Path,org.apache.jackrabbit.core.id.ItemId) + */ + public boolean canRead(Path itemPath, ItemId itemId) throws RepositoryException { + checkInitialized(); + if (compiledPermissions.canReadAll()) { + return true; + } else { + return compiledPermissions.canRead(itemPath, itemId); + } + } + + /** + * @see AccessManager#canAccess(String) + */ + public boolean canAccess(String workspaceName) throws RepositoryException { + checkInitialized(); + return wspAccess.canAccess(workspaceName); + } + + //-----------------------------------------------< AccessControlManager >--- + /** + * @see javax.jcr.security.AccessControlManager#hasPrivileges(String, Privilege[]) + */ + public boolean hasPrivileges(String absPath, Privilege[] privileges) throws PathNotFoundException, RepositoryException { + checkInitialized(); + checkValidNodePath(absPath); + if (privileges == null || privileges.length == 0) { + // null or empty privilege array -> return true + log.debug("No privileges passed -> allowed."); + return true; + } else { + Path p = getPath(absPath); + return compiledPermissions.hasPrivileges(p, privileges); + } + } + + /** + * @see javax.jcr.security.AccessControlManager#getPrivileges(String) + */ + public Privilege[] getPrivileges(String absPath) throws PathNotFoundException, RepositoryException { + checkInitialized(); + checkValidNodePath(absPath); + Set privs = compiledPermissions.getPrivilegeSet(getPath(absPath)); + return privs.toArray(new Privilege[privs.size()]); + } + + /** + * @see javax.jcr.security.AccessControlManager#getPolicies(String) + */ + @Override + public AccessControlPolicy[] getPolicies(String absPath) throws PathNotFoundException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.READ_AC); + + AccessControlPolicy[] policies; + if (editor != null) { + policies = editor.getPolicies(absPath); + } else { + policies = new AccessControlPolicy[0]; + } + return policies; + } + + /** + * @see javax.jcr.security.AccessControlManager#getEffectivePolicies(String) + */ + public AccessControlPolicy[] getEffectivePolicies(String absPath) throws PathNotFoundException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.READ_AC); + + return acProvider.getEffectivePolicies(getPath(absPath), compiledPermissions); + } + + /** + * @see javax.jcr.security.AccessControlManager#getApplicablePolicies(String) + */ + @Override + public AccessControlPolicyIterator getApplicablePolicies(String absPath) throws PathNotFoundException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.READ_AC); + + if (editor != null) { + try { + AccessControlPolicy[] applicable = editor.editAccessControlPolicies(absPath); + return new AccessControlPolicyIteratorAdapter(Arrays.asList(applicable)); + } catch (AccessControlException e) { + log.debug("No applicable policy at " + absPath); + } + } + // no applicable policies -> return empty iterator. + return AccessControlPolicyIteratorAdapter.EMPTY; + } + + /** + * @see javax.jcr.security.AccessControlManager#setPolicy(String, AccessControlPolicy) + */ + @Override + public void setPolicy(String absPath, AccessControlPolicy policy) throws PathNotFoundException, AccessControlException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.MODIFY_AC); + if (editor == null) { + throw new UnsupportedRepositoryOperationException("Modification of AccessControlPolicies is not supported. "); + } + editor.setPolicy(absPath, policy); + } + + /** + * @see javax.jcr.security.AccessControlManager#removePolicy(String, AccessControlPolicy) + */ + @Override + public void removePolicy(String absPath, AccessControlPolicy policy) throws PathNotFoundException, AccessControlException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.MODIFY_AC); + if (editor == null) { + throw new UnsupportedRepositoryOperationException("Removal of AccessControlPolicies is not supported."); + } + editor.removePolicy(absPath, policy); + } + + //-------------------------------------< JackrabbitAccessControlManager >--- + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getApplicablePolicies(Principal) + */ + @Override + public JackrabbitAccessControlPolicy[] getApplicablePolicies(Principal principal) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException { + checkInitialized(); + if (editor == null) { + throw new UnsupportedRepositoryOperationException("Editing of access control policies is not supported."); + } + return editor.editAccessControlPolicies(principal); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getPolicies(Principal) + */ + @Override + public JackrabbitAccessControlPolicy[] getPolicies(Principal principal) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException { + checkInitialized(); + if (editor == null) { + throw new UnsupportedRepositoryOperationException("Editing of access control policies is not supported."); + } + return editor.getPolicies(principal); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getEffectivePolicies(Set) + */ + public AccessControlPolicy[] getEffectivePolicies(Set principals) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException { + checkInitialized(); + return acProvider.getEffectivePolicies(principals, compiledPermissions); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#hasPrivileges(String, Set, Privilege[]) + */ + public boolean hasPrivileges(String absPath, Set principals, Privilege[] privileges) throws PathNotFoundException, RepositoryException { + checkInitialized(); + checkValidNodePath(absPath); + checkPermission(absPath, Permission.READ_AC); + + if (privileges == null || privileges.length == 0) { + // null or empty privilege array -> return true + log.debug("No privileges passed -> allowed."); + return true; + } else { + Path p = getPath(absPath); + CompiledPermissions perms = acProvider.compilePermissions(principals); + try { + return perms.hasPrivileges(p, privileges); + } finally { + perms.close(); + } + } + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getPrivileges(String, Set) + */ + public Privilege[] getPrivileges(String absPath, Set principals) throws PathNotFoundException, RepositoryException { + checkInitialized(); + checkValidNodePath(absPath); + checkPermission(absPath, Permission.READ_AC); + + CompiledPermissions perms = acProvider.compilePermissions(principals); + try { + Set privs = perms.getPrivilegeSet(getPath(absPath)); + return privs.toArray(new Privilege[privs.size()]); + } finally { + perms.close(); + } + } + + //---------------------------------------< AbstractAccessControlManager >--- + /** + * @see AbstractAccessControlManager#checkInitialized() + */ + @Override + protected void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + } + + /** + * @see AbstractAccessControlManager#checkValidNodePath(String) + */ + @Override + protected void checkValidNodePath(String absPath) throws PathNotFoundException, RepositoryException { + Path p = getPath(absPath); + if (p != null) { + if (!p.isAbsolute()) { + throw new RepositoryException("Absolute path expected."); + } + if (hierMgr.resolveNodePath(p) == null) { + throw new PathNotFoundException("No such node " + absPath); + } + } + } + + /** + * @see AbstractAccessControlManager#checkPermission(String,int) + */ + @Override + protected void checkPermission(String absPath, int permission) throws AccessDeniedException, RepositoryException { + checkValidNodePath(absPath); + Path p = getPath(absPath); + if (!compiledPermissions.grants(p, permission)) { + throw new AccessDeniedException("Access denied at " + absPath); + } + } + + /** + * @see AbstractAccessControlManager#getPrivilegeManager() + */ + @Override + protected PrivilegeManager getPrivilegeManager() throws RepositoryException { + checkInitialized(); + return privilegeManager; + } + + //------------------------------------------------------------< private >--- + private Path getPath(String absPath) throws RepositoryException { + return (absPath == null) ? null : resolver.getQPath(absPath); + } + + /** + * @param s the session + * @return if created with system-privileges + */ + private static boolean isSystemOrAdmin(Session s) { + if (s == null || !(s instanceof SessionImpl)) { + return false; + } else { + SessionImpl sImpl = (SessionImpl) s; + return sImpl.isSystem() || sImpl.isAdmin(); + } + } + + //-------------------------------------------------------------------------- + /** + * Simple wrapper around the repository's WorkspaceAccessManager + * that remembers for which workspaces the access has already been + * evaluated. + */ + private class WorkspaceAccess { + + private final WorkspaceAccessManager wspAccessManager; + + private final boolean alwaysAllowed; + // TODO: entries must be cleared if access permission to wsp changes. + private final List allowed; + private final List denied; + + private WorkspaceAccess(WorkspaceAccessManager wspAccessManager, + boolean alwaysAllowed) { + this.wspAccessManager = wspAccessManager; + this.alwaysAllowed = alwaysAllowed; + if (!alwaysAllowed) { + allowed = new ArrayList(5); + denied = new ArrayList(5); + } else { + allowed = denied = null; + } + } + + private boolean canAccess(String workspaceName) throws RepositoryException { + if (alwaysAllowed || wspAccessManager == null || allowed.contains(workspaceName)) { + return true; + } else if (denied.contains(workspaceName)) { + return false; + } + + // not yet tested -> ask the workspace-accessmanager. + boolean canAccess = wspAccessManager.grants(principals, workspaceName); + if (canAccess) { + allowed.add(workspaceName); + } else { + denied.add(workspaceName); + } + return canAccess; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/JackrabbitSecurityManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/JackrabbitSecurityManager.java new file mode 100644 index 00000000000..fa4c8d93adf --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/JackrabbitSecurityManager.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security; + +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.security.authentication.AuthContext; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; + +/** + * JackrabbitSecurityManager... + */ +public interface JackrabbitSecurityManager { + + void init(Repository repository, Session systemSession) throws RepositoryException; + + /** + * Disposes those parts of this security manager that are related to the + * workspace indicated by the given workspaceName. + * + * @param workspaceName Name of the workspace that is being disposed. + */ + void dispose(String workspaceName); + + /** + * Disposes this security manager instance and cleans all internal caches. + */ + void close(); + + /** + * Returns a new AuthContext for the specified credentials and + * subject. + * + * @param creds + * @param subject + * @param workspaceName The name of the workspace to login. + * @return A new AuthContext for the given creds + * and subject. + * @throws RepositoryException + */ + AuthContext getAuthContext(Credentials creds, Subject subject, String workspaceName) throws RepositoryException; + + /** + * Retrieve the AccessManager for the given session. + * + * @param session + * @param amContext + * @return AccessManager for the specified session. + * @throws RepositoryException + */ + AccessManager getAccessManager(Session session, AMContext amContext) throws RepositoryException; + + /** + * Retrieve the principal manager for the given session. + * + * @param session + * @return PrincipalManager for the given session. + * @throws javax.jcr.UnsupportedRepositoryOperationException If principal management + * is not supported. + * @throws RepositoryException if an error occurs + */ + PrincipalManager getPrincipalManager(Session session) throws RepositoryException; + + /** + * Returns the user manager for the specified session. + * + * @param session + * @return UserManager for the given session. + * @throws javax.jcr.UnsupportedRepositoryOperationException If user management is + * not supported. + * @throws RepositoryException + */ + UserManager getUserManager(Session session) throws RepositoryException; + + /** + * Retrieve the id to be displayed upon {@link Session#getUserID()} for + * the specified subject. + * + * @param subject + * @param workspaceName + * @return userID to be displayed upon {@link Session#getUserID()}. + * @throws RepositoryException + */ + String getUserID(Subject subject, String workspaceName) throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/SecurityConstants.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/SecurityConstants.java new file mode 100644 index 00000000000..7b011be135c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/SecurityConstants.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.core.security; + +/** + * This interface defines miscellaneous security related constants. + */ +public interface SecurityConstants { + + /** + * Name of the internal SimpleCredentials attribute where + * the Subject of the impersonating Session + * is stored. + * + * @see javax.jcr.Session#impersonate(javax.jcr.Credentials) + */ + String IMPERSONATOR_ATTRIBUTE = "org.apache.jackrabbit.core.security.impersonator"; + + /** + * The default principal name of the administrators group + */ + String ADMINISTRATORS_NAME = "administrators"; + + /** + * The default userID of the administrator. + */ + String ADMIN_ID = "admin"; + + /** + * The default userID for anonymous login + */ + String ANONYMOUS_ID = "anonymous"; + + /** + * To be used for the encryption. E.g. for passwords in + * {@link javax.jcr.SimpleCredentials#getPassword()} SimpleCredentials} + */ + String DEFAULT_DIGEST = "sha1"; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/SystemPrincipal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/SystemPrincipal.java new file mode 100644 index 00000000000..cb93dadb5d7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/SystemPrincipal.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.core.security; + +import java.io.Serializable; +import java.security.Principal; + +/** + * A SystemPrincipal ... + */ +public final class SystemPrincipal implements Principal, Serializable { + + private static final String SYSTEM_USER = "system"; + + /** + * Creates a SystemPrincipal. + */ + public SystemPrincipal() { + } + + //-------------------------------------------------------------< Object >--- + @Override + public String toString() { + return ("SystemPrincipal"); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof SystemPrincipal) { + return true; + } + return false; + } + + @Override + public int hashCode() { + return SYSTEM_USER.hashCode(); + } + + //----------------------------------------------------------< Principal >--- + /** + * {@inheritDoc} + */ + public String getName() { + return SYSTEM_USER; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/UserPrincipal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/UserPrincipal.java new file mode 100644 index 00000000000..983ff1d0a7b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/UserPrincipal.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.core.security; + +import java.io.Serializable; +import java.security.Principal; + +/** + * A UserPrincipal ... + */ +public class UserPrincipal implements Principal, Serializable { + + private final String name; + + /** + * Creates a UserPrincipal with the given name. + * + * @param name the name of this principal + * @throws IllegalArgumentException if name is null. + */ + public UserPrincipal(String name) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("name can not be null"); + } + this.name = name; + } + + //-------------------------------------------------------------< Object >--- + @Override + public String toString() { + return ("UserPrincipal: " + name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof UserPrincipal) { + UserPrincipal other = (UserPrincipal) obj; + return name.equals(other.name); + } + return false; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + //----------------------------------------------------------< Principal >--- + /** + * {@inheritDoc} + */ + public String getName() { + return name; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AbstractLoginModule.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AbstractLoginModule.java new file mode 100644 index 00000000000..d2b5237d491 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AbstractLoginModule.java @@ -0,0 +1,798 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authentication; + +import java.io.IOException; +import java.security.Principal; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +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.NameCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.core.config.LoginModuleConfig; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.principal.PrincipalProvider; +import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractLoginModule provides the means for the common + * authentication tasks within the Repository. + *

    + * On successful authentication it associates the credentials to principals + * using the {@link PrincipalProvider} configured for this LoginModule + *

    + * Jackrabbit distinguishes between Login and Impersonation dispatching the + * the corresponding Repository/Session methods to + * {@link #authenticate(java.security.Principal, javax.jcr.Credentials)} and + * {@link #impersonate(java.security.Principal, javax.jcr.Credentials)}, respectively. + *
    + * This LoginModule implements default behavior for either method. + * + * @see LoginModule + */ +public abstract class AbstractLoginModule implements LoginModule { + + private static final Logger log = LoggerFactory.getLogger(AbstractLoginModule.class); + + private static final String KEY_CREDENTIALS = "org.apache.jackrabbit.credentials"; + private static final String KEY_LOGIN_NAME = "javax.security.auth.login.name"; + + /** + * The name of the login module configuration option providing the name + * of the SimpleCredentials attribute used to identify a pre-authenticated + * login. + * + * @see #isPreAuthenticated(Credentials) + * @deprecated For security reasons this configuration option has been + * deprecated and will no longer be supported in a subsequent release. + * See also JCR-3293 + */ + private static final String PRE_AUTHENTICATED_ATTRIBUTE_OPTION = "trust_credentials_attribute"; + + private String principalProviderClassName; + private boolean initialized; + + protected String adminId; + protected String anonymousId; + + /** + * The name of the credentials attribute providing a hint that the + * credentials should be taken as is and the user requesting access + * has already been authenticated outside of this LoginModule. + * + * @see #getPreAuthAttributeName() + * @deprecated For security reasons the support for the preAuth attribute + * has been deprecated and will no longer be available in a subsequent release. + * See also JCR-3293 + */ + private String preAuthAttributeName; + + + protected CallbackHandler callbackHandler; + + protected Principal principal; + protected SimpleCredentials credentials; + protected Subject subject; + protected PrincipalProvider principalProvider; + + protected Map sharedState; + + /** + * Initialize this LoginModule and sets the following fields for later usage: + *

      + *
    • {@link PrincipalProvider} for user-{@link Principal} resolution.
    • + *
    • {@link LoginModuleConfig#PARAM_ADMIN_ID} option is evaluated
    • + *
    • {@link LoginModuleConfig#PARAM_ANONYMOUS_ID} option is evaluated
    • + *
    + * Implementations are called via + * {@link #doInit(CallbackHandler, Session, Map)} to implement + * additional initialization + * + * @param subject the Subject to be authenticated.

    + * @param callbackHandler a CallbackHandler for communicating + * with the end user (prompting for usernames and + * passwords, for example).

    + * @param sharedState state shared with other configured + * LoginModules.

    + * @param options options specified in the login Configuration + * for this particular LoginModule. + * @see LoginModule#initialize(Subject, CallbackHandler, Map, Map) + * @see #doInit(CallbackHandler, Session, Map) + * @see #isInitialized() + */ + public void initialize(Subject subject, CallbackHandler callbackHandler, + Map sharedState, Map options) { + // common jaas state variables + this.callbackHandler = callbackHandler; + this.subject = subject; + this.sharedState = sharedState; + + // initialize the login module + try { + log.debug("Initialize LoginModule: "); + RepositoryCallback repositoryCb = new RepositoryCallback(); + callbackHandler.handle(new Callback[]{repositoryCb}); + + PrincipalProviderRegistry registry = repositoryCb.getPrincipalProviderRegistry(); + // check if the class name of a PrincipalProvider implementation + // is present with the module configuration. + if (options.containsKey(LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS)) { + Object pcOption = options.get(LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS); + if (pcOption != null) { + principalProviderClassName = pcOption.toString(); + } + } + if (principalProviderClassName == null) { + // try compatibility parameters + if (options.containsKey(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_NAME)) { + principalProviderClassName = options.get(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_NAME).toString(); + } else if (options.containsKey(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_CLASS)) { + principalProviderClassName = options.get(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_CLASS).toString(); + } + } + if (principalProviderClassName != null) { + principalProvider = registry.getProvider(principalProviderClassName); + } + if (principalProvider == null) { + principalProvider = registry.getDefault(); + if (principalProvider == null) { + return; // abort. not even a default principal provider + } + } + log.debug("- PrincipalProvider -> '" + principalProvider.getClass().getName() + "'"); + + // call implementation for additional setup + doInit(callbackHandler, repositoryCb.getSession(), options); + + // adminId: if not present in options -> retrieve from callback + if (options.containsKey(LoginModuleConfig.PARAM_ADMIN_ID)) { + adminId = (String) options.get(LoginModuleConfig.PARAM_ADMIN_ID); + } + if (adminId == null) { + adminId = repositoryCb.getAdminId(); + } + // anonymousId: if not present in options -> retrieve from callback + if (options.containsKey(LoginModuleConfig.PARAM_ANONYMOUS_ID)) { + anonymousId = (String) options.get(LoginModuleConfig.PARAM_ANONYMOUS_ID); + } + if (anonymousId == null) { + anonymousId = repositoryCb.getAnonymousId(); + } + // trusted credentials attribute name (may be missing to not + // support) (normalized to null aka missing aka unset if an empty + // string) + preAuthAttributeName = (String) options.get(PRE_AUTHENTICATED_ATTRIBUTE_OPTION); + if (preAuthAttributeName != null + && preAuthAttributeName.length() == 0) { + preAuthAttributeName = null; + } + + //log config values for debug + if (log.isDebugEnabled()) { + for (String option : options.keySet()) { + log.debug("- Option: " + option + " -> '" + options.get(option) + "'"); + } + } + initialized = (this.subject != null); + + } catch (Exception e) { + log.error("LoginModule failed to initialize.", e); + } + } + + /** + * Implementations may set-up their own state. + * + * @param callbackHandler as passed by {@link javax.security.auth.login.LoginContext} + * @param session to security-workspace of Jackrabbit + * @param options options from LoginModule config + * @throws LoginException in case initialization fails. + */ + protected abstract void doInit(CallbackHandler callbackHandler, + Session session, + Map options) throws LoginException; + + + /** + * Returns true if this module has been successfully initialized. + * + * @return true if this module has been successfully initialized. + * @see LoginModule#initialize(Subject, CallbackHandler, Map, Map) + */ + protected boolean isInitialized() { + return initialized; + } + + /** + * Method to authenticate a Subject (phase 1). + *

    + * The login is divided into 3 Phases: + *

    + * 1) User-ID resolution
    + * In a first step it is tried to resolve a User-ID for further validation. + * As for JCR the identification is marked with the {@link Credentials} + * interface, credentials are accessed in this phase.
    + * If no User-ID can be found, anonymous access is granted with the ID of + * the anonymous user (as defined in the security configuration). + * Anonymous access can be switched off removing the configuration entry. + *
    This implementation uses two helper-methods, which allow for + * customization: + *

      + *
    • {@link #getCredentials()} and
    • + *
    • {@link #getUserID(Credentials)}
    • + *
    + *

    + * + * 2) User-Principal resolution
    + * In a second step it is tested, if the resolved User-ID belongs to a User + * known to the system, i.e. if the {@link PrincipalProvider} has a principal + * for the given ID and the principal can be found via + * {@link PrincipalProvider#findPrincipals(String)}.
    + * The provider implementation can be set by the LoginModule configuration. + * If the option is missing, the system default principal provider will + * be used. + *

    + * 3) Verification
    + * There are four cases, how the User-ID can be verified: + * The login is anonymous, pre-authenticated or the login is the result of + * an impersonation request (see {@link javax.jcr.Session#impersonate(Credentials)} + * or of a login to the Repository ({@link javax.jcr.Repository#login(Credentials)}). + * The concrete implementation of the LoginModule is responsible for all + * four cases: + *

      + *
    • {@link #isAnonymous(Credentials)}
    • + *
    • {@link #isPreAuthenticated(Credentials)}
    • + *
    • {@link #authenticate(Principal, Credentials)}
    • + *
    • {@link #impersonate(Principal, Credentials)}
    • + *
    + * + * Under the following conditions, the login process is aborted and the + * module is marked to be ignored: + *
      + *
    • No User-ID could be resolve, and anonymous access is switched off
    • + *
    • No Principal is found for the User-ID resolved
    • + *
    + * + * Under the following conditions, the login process is marked to be invalid + * by throwing an LoginException: + *
      + *
    • It is an impersonation request, but the impersonator is not allowed + * to impersonate to the requested User-ID
    • + *
    • The user tries to login, but the Credentials can not be verified.
    • + *
    + *

    + * The LoginModule keeps the Credentials and the Principal as instance fields, + * to mark that login has been successful. + * + * @return true if the authentication succeeded, or false if this + * LoginModule should be ignored. + * @throws LoginException if the authentication fails + * @see javax.security.auth.spi.LoginModule#login() + * @see #getCredentials() + * @see #getUserID(Credentials) + * @see #getImpersonatorSubject(Credentials) + */ + public boolean login() throws LoginException { + if (!isInitialized()) { + log.warn("Unable to perform login: initialization not completed."); + return false; + } + + // check the availability and validity of Credentials + Credentials creds = getCredentials(); + if (creds == null) { + log.debug("No credentials available -> try default (anonymous) authentication."); + } else if (!supportsCredentials(creds)) { + log.debug("Unsupported credentials implementation : " + creds.getClass().getName()); + return false; + } + + try { + Principal userPrincipal = getPrincipal(creds); + if (userPrincipal == null) { + // unknown or disabled user or a group + log.debug("No valid user -> ignore."); + return false; + } + boolean authenticated; + // test for anonymous, pre-authentication, impersonation or common authentication. + if (isAnonymous(creds) || isPreAuthenticated(creds)) { + authenticated = true; + } else if (isImpersonation(creds)) { + authenticated = impersonate(userPrincipal, creds); + } else { + authenticated = authenticate(userPrincipal, creds); + } + + // process authenticated user + if (authenticated) { + if (creds instanceof SimpleCredentials) { + credentials = (SimpleCredentials) creds; + } else { + credentials = new SimpleCredentials(getUserID(creds), new char[0]); + } + principal = userPrincipal; + return true; + } + } catch (RepositoryException e) { + log.error("Login failed:", e); + } + return false; + } + + /** + * Method to commit the authentication process (phase 2). + *

    + * This method is called if the LoginContext's overall authentication + * succeeded (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL + * LoginModules succeeded). + *

    + * If this LoginModule's own authentication attempt succeeded (checked + * by retrieving the private state saved by the login method), + * then this method associates relevant Principals and Credentials with the + * Subject located in the LoginModule. If this + * LoginModule's own authentication attempted failed, then this method + * removes/destroys any state that was originally saved. + *

    + * The login is considered as succeeded if there is a principal set. + *

    + * The implementation stores the principal associated to the UserID and all + * the Groups it is member of with the Subject and in addition adds an + * instance of (#link SimpleCredentials} to the Subject's public credentials. + * + * @return true if this method succeeded, or false if this + * LoginModule should be ignored. + * @throws LoginException if the commit fails + * @see javax.security.auth.spi.LoginModule#commit() + */ + public boolean commit() throws LoginException { + if (!isInitialized() || principal == null) { + return false; + } + + Set principals = getPrincipals(); + subject.getPrincipals().addAll(principals); + subject.getPublicCredentials().add(credentials); + return true; + } + + /** + * Method to abort the authentication process (phase 2). + *

    + *

    This method is called if the LoginContext's overall authentication + * failed. (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL + * LoginModules did not succeed). + *

    + *

    If this LoginModule's own authentication attempt succeeded (checked + * by retrieving the private state saved by the login method), + * then this method cleans up any state that was originally saved. + *

    + *

    + * + * @return true if this method succeeded, or false if this + * LoginModule should be ignored. + * @throws LoginException if the abort fails + * @see javax.security.auth.spi.LoginModule#abort() + */ + public boolean abort() throws LoginException { + if (!isInitialized()) { + return false; + } else { + sharedState.remove(KEY_CREDENTIALS); + callbackHandler = null; + principal = null; + credentials = null; + return logout(); + } + } + + /** + * @return true if this method succeeded, + * or false if this LoginModule should be ignored. + * @throws LoginException if the logout fails + * @see javax.security.auth.spi.LoginModule#logout() + */ + public boolean logout() throws LoginException { + if (subject.getPrincipals().isEmpty() || subject.getPublicCredentials(Credentials.class).isEmpty()) { + return false; + } else { + // clear subject if not readonly + if (!subject.isReadOnly()) { + subject.getPrincipals().clear(); + subject.getPublicCredentials().clear(); + } + return true; + } + } + + /** + * @param principal Principal used to retrieve the Authentication + * object. + * @param credentials Credentials used for the authentication. + * @return true if Credentials authenticate, + * false if no Authentication can handle + * the given Credentials + * @throws javax.security.auth.login.FailedLoginException + * if the authentication failed. + * @throws RepositoryException If another error occurs. + * @see AbstractLoginModule#getAuthentication(java.security.Principal, javax.jcr.Credentials) + * @see AbstractLoginModule#authenticate(java.security.Principal, javax.jcr.Credentials) + */ + protected boolean authenticate(Principal principal, Credentials credentials) + throws FailedLoginException, RepositoryException { + + Authentication auth = getAuthentication(principal, credentials); + if (auth == null) { + return false; + } else if (auth.authenticate(credentials)) { + return true; + } + throw new FailedLoginException(); + } + + /** + * Test if the current request is an Impersonation attempt. The default + * implementation returns true if an + * {@link #getImpersonatorSubject(Credentials) subject} for the + * impersonation can be retrieved. + * + * @param credentials potentially containing impersonation data + * @return true if this is an impersonation attempt + * @see #getImpersonatorSubject(Credentials) + */ + protected boolean isImpersonation(Credentials credentials) { + return getImpersonatorSubject(credentials) != null; + } + + /** + * Handles the impersonation of given Credentials. + * + * @param principal Principal to impersonate. + * @param credentials Credentials used to create the impersonation subject. + * @return false, if there is no User to impersonate, + * true if impersonation is allowed + * @throws LoginException If credentials don't allow to impersonate to principal. + * @throws RepositoryException If another error occurs. + */ + protected abstract boolean impersonate(Principal principal, Credentials credentials) + throws RepositoryException, LoginException; + + /** + * Retrieve the Authentication. + * + * @param principal A principal. + * @param creds The Credentials used for the login. + * @return Authentication object for the given principal / credentials. + * @throws RepositoryException If an error occurs. + */ + protected abstract Authentication getAuthentication(Principal principal, Credentials creds) + throws RepositoryException; + + /** + * Method tries to acquire an Impersonator in the following order: + *

      + *
    • Try to access it from the {@link Credentials} via {@link SimpleCredentials#getAttribute(String)}
    • + *
    • Ask CallbackHandler for Impersonator with use of {@link ImpersonationCallback}.
    • + *
    + * + * @param credentials which, may contain an impersonation Subject + * @return impersonation subject or null if non contained + * @see #login() + * @see #impersonate(java.security.Principal, javax.jcr.Credentials) + */ + protected Subject getImpersonatorSubject(Credentials credentials) { + Subject impersonator = null; + if (credentials == null) { + try { + ImpersonationCallback impers = new ImpersonationCallback(); + callbackHandler.handle(new Callback[]{impers}); + impersonator = impers.getImpersonator(); + } catch (UnsupportedCallbackException e) { + log.warn(e.getCallback().getClass().getName() + " not supported: Unable to perform Impersonation."); + } catch (IOException e) { + log.error("Impersonation-Callback failed: " + e.getMessage() + ": Unable to perform Impersonation."); + } + } else if (credentials instanceof SimpleCredentials) { + SimpleCredentials sc = (SimpleCredentials) credentials; + impersonator = (Subject) sc.getAttribute(SecurityConstants.IMPERSONATOR_ATTRIBUTE); + } + return impersonator; + } + + /** + * Method tries to resolve the {@link Credentials} used for login. It takes + * authentication-extension of an already authenticated {@link Subject} into + * account. + *

    + * Therefore the credentials are retrieved as follows: + *

      + *
    1. Test if the shared state contains credentials.
    2. + *
    3. Ask CallbackHandler for Credentials with using a {@link + * CredentialsCallback}. Expects {@link CredentialsCallback#getCredentials} + * to return an instance of {@link Credentials}.
    4. + *
    5. Ask the Subject for its public SimpleCredentials see + * {@link Subject#getPublicCredentials(Class)}, thus enabling to + * pre-authenticate the Subject.
    6. + *
    + * + * @return Credentials or null if not found + * @see #login() + */ + protected Credentials getCredentials() { + Credentials credentials = null; + if (sharedState.containsKey(KEY_CREDENTIALS)) { + credentials = (Credentials) sharedState.get(KEY_CREDENTIALS); + } else { + try { + CredentialsCallback callback = new CredentialsCallback(); + callbackHandler.handle(new Callback[]{callback}); + credentials = callback.getCredentials(); + if (credentials != null && supportsCredentials(credentials)) { + sharedState.put(KEY_CREDENTIALS, credentials); + } + } catch (UnsupportedCallbackException e) { + log.warn("Credentials-Callback not supported try Name-Callback"); + } catch (IOException e) { + log.error("Credentials-Callback failed: " + e.getMessage() + ": try Name-Callback"); + } + } + // if still no credentials -> try to retrieve them from the subject. + if (null == credentials) { + // try if subject contains SimpleCredentials + Set preAuthCreds = subject.getPublicCredentials(SimpleCredentials.class); + if (!preAuthCreds.isEmpty()) { + credentials = preAuthCreds.iterator().next(); + } + } + if (null == credentials) { + // try if subject contains GuestCredentials + Set preAuthCreds = subject.getPublicCredentials(GuestCredentials.class); + if (!preAuthCreds.isEmpty()) { + credentials = preAuthCreds.iterator().next(); + } + } + return credentials; + } + + /** + * Return a flag indicating whether the credentials are supported by + * this login module. Default implementation supports + * {@link SimpleCredentials} and {@link GuestCredentials}. + * + * @param creds credentials + * @return true if the credentials are supported; + * false otherwise + */ + protected boolean supportsCredentials(Credentials creds) { + return creds instanceof SimpleCredentials || + creds instanceof GuestCredentials; + } + + /** + * Method supports tries to acquire a UserID in the following order: + *
      + *
    1. If passed credentials are {@link GuestCredentials} the anonymous user id + * is returned.
    2. + *
    3. Try to access it from the {@link Credentials} via {@link + * SimpleCredentials#getUserID()}
    4. + *
    5. Ask CallbackHandler for User-ID with use of {@link NameCallback}.
    6. + *
    7. Test if the 'sharedState' contains a login name.
    8. + *
    9. Fallback: return the anonymous UserID.
    10. + *
    + * + * @param credentials which, may contain a User-ID + * @return The userId retrieved from the credentials or by any other means + * described above. + * @see #login() + */ + protected String getUserID(Credentials credentials) { + String userId = null; + if (credentials != null) { + if (credentials instanceof GuestCredentials) { + userId = anonymousId; + } else if (credentials instanceof SimpleCredentials) { + userId = ((SimpleCredentials) credentials).getUserID(); + } else { + try { + NameCallback callback = new NameCallback("User-ID: "); + callbackHandler.handle(new Callback[]{callback}); + userId = callback.getName(); + } catch (UnsupportedCallbackException e) { + log.warn("Credentials- or NameCallback must be supported"); + } catch (IOException e) { + log.error("Name-Callback failed: " + e.getMessage()); + } + } + } + if (userId == null && sharedState.containsKey(KEY_LOGIN_NAME)) { + userId = (String) sharedState.get(KEY_LOGIN_NAME); + } + + // still no userId -> anonymousID if its has been defined. + // TODO: check again if correct when used with 'extendedAuth' + if (userId == null) { + userId = anonymousId; + } + return userId; + } + + /** + * Indicate if the given Credentials are considered to be anonymous. + * + * @param credentials The Credentials to be tested. + * @return true if is anonymous; false otherwise. + */ + protected boolean isAnonymous(Credentials credentials) { + if (credentials instanceof GuestCredentials) { + return true; + } else { + // TODO: review again. former simple-login-module treated 'null' as anonymous (probably wrong). + String userId = getUserID(credentials); + return (anonymousId == null) ? userId == null : anonymousId.equals(userId); + } + } + + + /** + * Authentication process associates a Principal to Credentials
    + * This method resolves the Principal for the given Credentials. If no valid + * Principal can be determined, the LoginModule should be ignored. + * + * @param credentials Credentials used for to login. + * @return the principal associated with the given credentials or null. + */ + protected abstract Principal getPrincipal(Credentials credentials); + + /** + * @return a Collection of principals that contains the current user + * principal and all groups it is member of. + */ + protected Set getPrincipals() { + // use linked HashSet instead of HashSet in order to maintain the order + // of principals (as in the Subject). + Set principals = new LinkedHashSet(); + principals.add(principal); + PrincipalIterator groups = principalProvider.getGroupMembership(principal); + while (groups.hasNext()) { + principals.add(groups.nextPrincipal()); + } + return principals; + } + + //-------------------------------------------------------------------------- + /** + * Returns the admin user id. + * + * @return admin user id + */ + public String getAdminId() { + return adminId; + } + + /** + * Sets the administrator's user id. + * + * @param adminId the administrator's user id. + */ + public void setAdminId(String adminId) { + this.adminId = adminId; + } + + /** + * Returns the anonymous user id. + * + * @return anonymous user id + */ + public String getAnonymousId() { + return anonymousId; + } + + /** + * Sets the anonymous user id. + * + * @param anonymousId anonymous user id + */ + public void setAnonymousId(String anonymousId) { + this.anonymousId = anonymousId; + } + + /** + * Returns the configured name of the principal provider class. + * + * @return name of the principal provider class. + */ + public String getPrincipalProvider() { + return principalProviderClassName; + } + + /** + * Sets the configured name of the principal provider class + * + * @param principalProvider Name of the principal provider class. + */ + public void setPrincipalProvider(String principalProvider) { + this.principalProviderClassName = principalProvider; + } + + /** + * The name of the credentials attribute providing a hint that the + * credentials should be taken as is and the user requesting access + * has already been authenticated outside of this LoginModule. + *

    + * This name is configured as the value of the LoginModule configuration + * parameter trust_credentials_attribute. If the configuration + * parameter is missing (or empty) the name is not set and this method + * returns null. + * + * @see #isPreAuthenticated(Credentials) + * @deprecated For security reasons the support for the preAuth attribute + * has been deprecated and will no longer be available in a subsequent release. + * See also JCR-3293 + */ + protected final String getPreAuthAttributeName() { + return preAuthAttributeName; + } + + /** + * Returns true if the credentials should be considered as + * pre-authenticated and a password check is not required. + *

    + * This base class implementation returns true if the + * creds object is a SimpleCredentials instance and the + * configured {@link #getPreAuthAttributeName() trusted + * credentials property} is set to a non-null value in the + * credentials attributes. + *

    + * Extensions of this class may overwrite this method to apply more or + * different checks to the credentials. + * + * @param creds The Credentials to check + * + * @see #getPreAuthAttributeName() + * @deprecated For security reasons the support for the preAuth attribute + * has been deprecated and will no longer be available in a subsequent release. + * See also JCR-3293 + */ + protected boolean isPreAuthenticated(final Credentials creds) { + final String preAuthAttrName = getPreAuthAttributeName(); + boolean isPreAuth = preAuthAttrName != null + && (creds instanceof SimpleCredentials) + && ((SimpleCredentials) creds).getAttribute(preAuthAttrName) != null; + if (isPreAuth) { + log.warn("Usage of deprecated 'trust_credentials_attribute' option. " + + "Please note that for security reasons this feature will not" + + "be supported in future releases."); + } + return isPreAuth; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AuthContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AuthContext.java new file mode 100644 index 00000000000..6e32d85ed5a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AuthContext.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.core.security.authentication; + +import javax.security.auth.Subject; +import javax.security.auth.login.LoginException; + +/** + * An authentication context used to authenticate users. It is similar to JAAS' LoginContext + * but can work in a non-JAAS environment. + *

    + * This class is abstract and has two implementations: + *

      + *
    • {@link JAASAuthContext} which delegates to a regular JAAS LoginContext
    • + *
    • {@link LocalAuthContext} which implements authentication using a locally-defined + * JAAS LoginModule
    • + *
    + */ +public interface AuthContext { + + /** + * Perform the authentication and, if successful, associate Principals and Credentials + * with the authenticatedSubject. + * + * @see javax.security.auth.login.LoginContext#login() + * @throws LoginException if the authentication fails. + */ + void login() throws LoginException; + + /** + * Return the authenticated Subject. + * + * @see javax.security.auth.login.LoginContext#getSubject() + * @return the authenticated Subject or null if authentication failed. + */ + Subject getSubject(); + + /** + * Logout the Subject. + * + * @see javax.security.auth.login.LoginContext#logout() + * @throws LoginException if the logout fails. + */ + void logout() throws LoginException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AuthContextProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AuthContextProvider.java new file mode 100644 index 00000000000..e99ef1b76c9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/AuthContextProvider.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.core.security.authentication; + +import org.apache.jackrabbit.core.config.LoginModuleConfig; +import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * AuthContextProvider defines how the current request for login is + * handled. By default the {@link org.apache.jackrabbit.core.config.RepositoryConfig + * local repository configuration} takes precedence over JAAS configuration. + * If no local configuration is present a JAAS configuration must be provided + * otherwise {@link #getAuthContext} fails with RepositoryException. + */ +public class AuthContextProvider { + + private boolean initialized; + + /** + * configuration state -> if a JAAS Configuration exists for this application + */ + private boolean isJAAS; + + /** + * Configuration of the optional local LoginModule + */ + private final LoginModuleConfig config; + + /** + * Application Name for the LoginConfig entry + */ + private final String appName; + + /** + * @param appName LoginConfig application name used for this instance + * @param config optional LoginModule-configuration to use without JAAS + */ + public AuthContextProvider(String appName, LoginModuleConfig config) { + this.appName = appName; + this.config = config; + } + + /** + * + * @param credentials + * @param subject + * @param session + * @param principalProviderRegistry + * @param adminId + * @param anonymousId + * @return context of for authentication and log-out + * @throws RepositoryException in case neither an JAASContext + * nor a LocalContext can be successfully created. + */ + public AuthContext getAuthContext(Credentials credentials, + Subject subject, + Session session, + PrincipalProviderRegistry principalProviderRegistry, + String adminId, + String anonymousId) + throws RepositoryException { + + CallbackHandler cbHandler = new CallbackHandlerImpl(credentials, session, principalProviderRegistry, adminId, anonymousId); + + if (isLocal()) { + return new LocalAuthContext(config, cbHandler, subject); + } else if (isJAAS()) { + return new JAASAuthContext(appName, cbHandler, subject); + } else { + throw new RepositoryException("No Login-Configuration"); + } + } + + /** + * @return true if a application entry is available in a JAAS- {@link Configuration} + */ + public boolean isJAAS() { + if (!isLocal() && !initialized) { + AppConfigurationEntry[] entries = getJAASConfig(); + isJAAS = entries != null && entries.length > 0; + initialized = true; + } + return isJAAS; + } + + /** + * @return true if a login-module is configured. + */ + public boolean isLocal() { + return config != null; + } + + /** + * @return options configured for the LoginModules to use. + */ + public Properties[] getModuleConfig() { + Properties[] props = new Properties[0]; + if (isLocal()) { + props = new Properties[] {config.getParameters()}; + } else { + AppConfigurationEntry[] entries = getJAASConfig(); + if (entries != null) { + List tmp = new ArrayList(entries.length); + for (AppConfigurationEntry entry : entries) { + Map opt = entry.getOptions(); + if (opt != null) { + Properties prop = new Properties(); + prop.putAll(opt); + tmp.add(prop); + } + } + props = tmp.toArray(new Properties[tmp.size()]); + } + } + return props; + } + + /** + * @return all JAAS-Login Modules for this application or null if none + */ + private AppConfigurationEntry[] getJAASConfig() { + + // check if jaas-loginModule or fallback is configured + Configuration logins = null; + try { + logins = Configuration.getConfiguration(); + } catch (Exception e) { + // means no JAAS configuration file OR no permission to read it + } + if (logins != null) { + try { + return logins.getAppConfigurationEntry(appName); + } catch (Exception e) { + // WLP 9.2.0 throws IllegalArgumentException for unknown appName + } + } + return null; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/Authentication.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/Authentication.java new file mode 100644 index 00000000000..ef26a2a2331 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/Authentication.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.core.security.authentication; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; + +/** + * The Authentication interface defines methods to validate + * {@link javax.jcr.Credentials Credentials} upon authentication. The validation + * dependants on the authentication mechanism used, i.e. + *
      + *
    • comparison of UserID/password pair retrieved from the given Credentials + * with Credentials stored for a particular user,
    • + *
    • bind to a LDAP with a given ID,
    • + *
    • validation of a SSO ticket.
    • + *
    + * + */ +public interface Authentication { + + /** + * An Authentication may only be able to handle certain types of + * Credentials as the authentication process is tightly coupled + * to the semantics of the Credentials. + * E.g.: A ticket based Authentication is dependant on a + * Credentials implementation which allows access to this ticket. + * + * @param credentials in questions + * @return true if the current Authentication handles the given Credentials + */ + boolean canHandle(Credentials credentials); + + /** + * True if the Credentials identify the User related to this + * Authentication. + * + * @param credentials to verify + * @return true if Credentials identify the + * User. + * @throws RepositoryException If an error occurs. + */ + boolean authenticate(Credentials credentials) throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CallbackHandlerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CallbackHandlerImpl.java new file mode 100644 index 00000000000..6c3e08bdf06 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CallbackHandlerImpl.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.core.security.authentication; + +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.Session; +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 java.io.IOException; + +/** + * CallbackHandler that deals with the following callbacks: + *
      + *
    • {@link NameCallback} + *
    • {@link PasswordCallback} + *
    • {@link CredentialsCallback} + *
    • {@link ImpersonationCallback} + *
    • {@link RepositoryCallback} + *
    + */ +public class CallbackHandlerImpl implements CallbackHandler { + + private static final Logger log = LoggerFactory.getLogger(CallbackHandlerImpl.class); + + private final Session session; + private final Credentials credentials; + private final PrincipalProviderRegistry principalProviderRegistry; + private final String adminId; + private final String anonymousId; + + /** + * Instantiate with the data needed to handle callbacks + * + * @param credentials + * @param session + */ + public CallbackHandlerImpl(Credentials credentials, Session session, + PrincipalProviderRegistry principalProviderRegistry, + String adminId, String anonymousId) { + this.credentials = credentials; + this.session = session; + this.principalProviderRegistry = principalProviderRegistry; + this.adminId = adminId; + this.anonymousId = anonymousId; + + if (session == null) { + log.debug("Session is null -> CallbackHandler won't be able to handle RepositoryCallback."); + } + if (principalProviderRegistry == null) { + log.debug("PrincipalProviderRegistry is null -> CallbackHandler won't be able to handle RepositoryCallback."); + } + } + + /** + * @param callbacks + * @throws IOException + * @throws UnsupportedCallbackException + * @see CallbackHandler#handle(Callback[]) + */ + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + + for (Callback callback : callbacks) { + if (callback instanceof CredentialsCallback) { + ((CredentialsCallback) callback).setCredentials(credentials); + } else if (callback instanceof RepositoryCallback) { + /* + if callback handler has been created with null session or + null principalProviderRegistry this handler cannot properly + deal with RepositoryCallback + */ + if (session == null || principalProviderRegistry == null) { + throw new UnsupportedCallbackException(callback); + } + RepositoryCallback rcb = (RepositoryCallback) callback; + rcb.setSession(session); + rcb.setPrincipalProviderRegistry(principalProviderRegistry); + rcb.setAdminId(adminId); + rcb.setAnonymousId(anonymousId); + } else if (credentials != null && credentials instanceof SimpleCredentials) { + SimpleCredentials simpleCreds = (SimpleCredentials) credentials; + if (callback instanceof NameCallback) { + String userId = simpleCreds.getUserID(); + ((NameCallback) callback).setName(userId); + } else if (callback instanceof PasswordCallback) { + char[] pw = simpleCreds.getPassword(); + ((PasswordCallback) callback).setPassword(pw); + } else if (callback instanceof ImpersonationCallback) { + Object impersAttr = simpleCreds.getAttribute(SecurityConstants.IMPERSONATOR_ATTRIBUTE); + ((ImpersonationCallback) callback).setImpersonator(impersAttr); + } else { + throw new UnsupportedCallbackException(callback); + } + } else { + throw new UnsupportedCallbackException(callback); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CredentialsCallback.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CredentialsCallback.java new file mode 100644 index 00000000000..71d9073c004 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CredentialsCallback.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.core.security.authentication; + +import javax.jcr.Credentials; +import javax.security.auth.callback.Callback; +import java.io.Serializable; + +/** + * A CredentialsCallback + */ +public class CredentialsCallback implements Callback, Serializable { + + private Credentials credentials; + + /** + * Get the retrieved credentials. + * + * @return the retrieved credentials (which may be null) + */ + public Credentials getCredentials() { + return credentials; + } + + /** + * Set the retrieved credentials. + * + * @param credentials the retrieved credentials (which may be null) + */ + public void setCredentials(Credentials credentials) { + this.credentials = credentials; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.java new file mode 100644 index 00000000000..c4370edb0db --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentials.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.core.security.authentication; + +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.user.PasswordUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Crypted variant of the {@link javax.jcr.SimpleCredentials}. + */ +public class CryptedSimpleCredentials implements Credentials { + + private static final Logger log = LoggerFactory.getLogger(CryptedSimpleCredentials.class); + + private final String hashedPassword; + private final String userId; + private final Map attributes; + + /** + * Build a new instance of CryptedSimpleCredentials from the + * given {@link javax.jcr.SimpleCredentials SimpleCredentials} and create + * the crypted password field using the {@link SecurityConstants#DEFAULT_DIGEST + * default digest}. + * + * @param credentials + * @throws NoSuchAlgorithmException + * @throws UnsupportedEncodingException + * @deprecated + */ + public CryptedSimpleCredentials(SimpleCredentials credentials) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + userId = credentials.getUserID(); + if (userId == null || userId.length() == 0) { + throw new IllegalArgumentException(); + } + char[] pwd = credentials.getPassword(); + if (pwd == null) { + throw new IllegalArgumentException(); + } + String password = new String(pwd); + hashedPassword = PasswordUtility.buildPasswordHash(password); + + String[] attNames = credentials.getAttributeNames(); + attributes = new HashMap(attNames.length); + for (String attName : attNames) { + attributes.put(attName, credentials.getAttribute(attName)); + } + } + + /** + * Create a new instanceof CryptedSimpleCredentials from the + * given userId and hashedPassword strings. + * In contrast to {@link CryptedSimpleCredentials(SimpleCredentials)} that + * expects the password to be plain text this constructor expects the + * password to be already crypted. However, it performs a simple validation + * and calls {@link PasswordUtility#buildPasswordHash(String)} in case the + * given password is found to be plain text. + * + * @param userId + * @param hashedPassword + * @throws NoSuchAlgorithmException + * @throws UnsupportedEncodingException + */ + public CryptedSimpleCredentials(String userId, String hashedPassword) throws NoSuchAlgorithmException, UnsupportedEncodingException { + if (userId == null || userId.length() == 0) { + throw new IllegalArgumentException("Invalid userID: The userID must have a length > 0."); + } + if (hashedPassword == null) { + throw new IllegalArgumentException("Password may not be null."); + } + this.userId = userId; + if (PasswordUtility.isPlainTextPassword(hashedPassword)) { + // password is plain text (including those starting with {invalidAlgorithm}) + log.warn("Plain text password -> Using default algorithm to create digest."); + this.hashedPassword = PasswordUtility.buildPasswordHash(hashedPassword); + } else { + this.hashedPassword = hashedPassword; + } + attributes = Collections.emptyMap(); + } + + public String getUserID() { + return userId; + } + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public String[] getAttributeNames() { + return attributes.keySet().toArray(new String[attributes.size()]); + } + + public String getAlgorithm() { + return PasswordUtility.extractAlgorithm(hashedPassword); + } + + public String getPassword() { + return hashedPassword; + } + + /** + * Compares this instance with the given SimpleCredentials and + * returns true if both match. Successful match is defined to + * be the result of + *
      + *
    • Case-insensitive comparison of the UserIDs
    • + *
    • Equality of the passwords if the password contained in the simple + * credentials is hashed with the algorithm defined in this credentials object.
    • + *
    + * + * NOTE, that the simple credentials are exptected to contain the plain text + * password. + * + * @param credentials An instance of simple credentials. + * @return true if {@link SimpleCredentials#getUserID() UserID} and + * {@link SimpleCredentials#getPassword() Password} match. + * @throws NoSuchAlgorithmException + * @throws UnsupportedEncodingException + */ + public boolean matches(SimpleCredentials credentials) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + + if (getUserID().equalsIgnoreCase(credentials.getUserID())) { + // crypt the password retrieved from the given simple credentials + // and test if it is equal to the password hash defined with this + // CryptedSimpleCredentials instance. + return PasswordUtility.isSame(hashedPassword, String.valueOf(credentials.getPassword())); + } + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/DefaultLoginModule.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/DefaultLoginModule.java new file mode 100644 index 00000000000..9a7dd744840 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/DefaultLoginModule.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authentication; + +import java.security.Principal; +import java.util.Map; +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.FailedLoginException; +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.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authentication.token.TokenBasedAuthentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The DefaultLoginModule authenticates Credentials related to + * a {@link User} of the Repository
    + * In any other case it is marked to be ignored.

    + * This Module can deal with the following credentials + *

      + *
    • SimpleCredentials -> handled by {@link SimpleCredentialsAuthentication}.
    • + *
    • TokenCredentials -> handled by {@link TokenBasedAuthentication}.
    • + *
    + * In both cases the login is successful if the system contains a non-disabled, + * valid user that matches the given credentials. + *

    + * Correspondingly impersonation is delegated to the User's + * {@link User#getImpersonation() Impersonation} object. + * + * @see AbstractLoginModule + */ +public class DefaultLoginModule extends AbstractLoginModule { + + private static final Logger log = LoggerFactory.getLogger(DefaultLoginModule.class); + + /** + * Optional configuration parameter to disable token based authentication. + */ + private static final String PARAM_DISABLE_TOKEN_AUTH = "disableTokenAuth"; + + /** + * Optional configuration parameter to disable token based authentication. + */ + private static final String PARAM_TOKEN_EXPIRATION = "tokenExpiration"; + + /** + * Flag indicating if Token-based authentication is disabled by the + * LoginModule configuration. + */ + private boolean disableTokenAuth; + + /** + * The expiration time for login tokens as set by the LoginModule configuration. + */ + private long tokenExpiration = TokenBasedAuthentication.TOKEN_EXPIRATION; + + /** + * The user object retrieved during the authentication process. + */ + protected User user; + private SessionImpl session; + private UserManager userManager; + + /** + * The TokenCredentials or null in case of another credentials. + */ + private TokenCredentials tokenCredentials; + + //--------------------------------------------------------< LoginModule >--- + /** + * @see javax.security.auth.spi.LoginModule#commit() + */ + @Override + public boolean commit() throws LoginException { + boolean success = super.commit(); + if (success && !disableTokenAuth) { + if (TokenBasedAuthentication.doCreateToken(credentials)) { + Session s = null; + try { + /* + use a different session instance to create the token + node in order to prevent concurrent modifications with + the shared system session. + */ + s = session.createSession(session.getWorkspace().getName()); + Credentials tc = TokenBasedAuthentication.createToken(user, credentials, tokenExpiration, s); + if (tc != null) { + subject.getPublicCredentials().add(tc); + } + } catch (RepositoryException e) { + LoginException le = new LoginException("Failed to commit: " + e.getMessage()); + le.initCause(e); + throw le; + } finally { + if (s != null) { + s.logout(); + } + } + } else if (tokenCredentials != null) { + subject.getPublicCredentials().add(tokenCredentials); + } + } + return success; + } + + //------------------------------------------------< AbstractLoginModule >--- + /** + * Retrieves the user manager from the specified session. If this fails + * this login modules initialization must fail. + * + * @see AbstractLoginModule#doInit(CallbackHandler, Session, Map) + */ + @Override + protected void doInit(CallbackHandler callbackHandler, Session session, Map options) throws LoginException { + if (!(session instanceof SessionImpl)) { + throw new LoginException("Unable to initialize LoginModule: SessionImpl expected."); + } + try { + this.session = (SessionImpl) session; + userManager = this.session.getUserManager(); + log.debug("- UserManager -> '" + userManager.getClass().getName() + "'"); + } catch (RepositoryException e) { + throw new LoginException("Unable to initialize LoginModule: " + e.getMessage()); + } + + // configuration options related to token based authentication + if (options.containsKey(PARAM_DISABLE_TOKEN_AUTH)) { + disableTokenAuth = Boolean.parseBoolean(options.get(PARAM_DISABLE_TOKEN_AUTH).toString()); + log.debug("- Token authentication disabled -> '" + disableTokenAuth + "'"); + } + if (options.containsKey(PARAM_TOKEN_EXPIRATION)) { + try { + tokenExpiration = Long.parseLong(options.get(PARAM_TOKEN_EXPIRATION).toString()); + log.debug("- Token expiration -> '" + tokenExpiration + "'"); + } catch (NumberFormatException e) { + log.warn("Unabled to parse token expiration: {}", e.getMessage()); + } + } + } + + /** + * Resolves the userID from the given credentials and obtains the + * principal from the User object associated with the given userID. + * If the the userID cannot be resolved to a User or if obtaining the + * principal fail, null is returned. + * + * @param credentials Credentials to retrieve the principal for. + * @return a user principal or null. + * @see AbstractLoginModule#getPrincipal(Credentials) + */ + @Override + protected Principal getPrincipal(Credentials credentials) { + Principal principal = null; + String userId = getUserID(credentials); + try { + Authorizable authrz = userManager.getAuthorizable(userId); + if (authrz != null && !authrz.isGroup()) { + user = (User) authrz; + if (user.isDisabled()) { + // log message and return null -> login module returns false. + log.debug("User " + userId + " has been disabled."); + } else { + principal = user.getPrincipal(); + } + } + } catch (RepositoryException e) { + // should not get here + log.warn("Error while retrieving principal. {}", e.getMessage()); + } + return principal; + } + + /** + * @see AbstractLoginModule#supportsCredentials(javax.jcr.Credentials) + */ + @Override + protected boolean supportsCredentials(Credentials creds) { + if (creds instanceof TokenCredentials) { + return !disableTokenAuth; + } else { + return super.supportsCredentials(creds); + } + } + + /** + * @see AbstractLoginModule#getUserID(javax.jcr.Credentials) + */ + @Override + protected String getUserID(Credentials credentials) { + // shortcut to avoid duplicate evaluation. + if (user != null) { + try { + return user.getID(); + } catch (RepositoryException e) { + log.warn("Failed to retrieve userID from user", e); + // ignore and re-evaluate credentials. + } + } + + // handle TokenCredentials + if (!disableTokenAuth && TokenBasedAuthentication.isTokenBasedLogin(credentials)) { + // special token based login + tokenCredentials = ((TokenCredentials) credentials); + try { + return TokenBasedAuthentication.getUserId(tokenCredentials, session); + } catch (RepositoryException e) { + if (log.isDebugEnabled()) { + log.warn("Failed to retrieve UserID from token-based credentials", e); + } else { + log.warn("Failed to retrieve UserID from token-based credentials: {}", e.toString()); + } + } + // failed to retrieve the user from loginToken. + return null; + } else { + // regular login -> extraction of userID is handled by the super class. + return super.getUserID(credentials); + } + } + + /** + * @see AbstractLoginModule#getAuthentication(Principal, Credentials) + */ + @Override + protected Authentication getAuthentication(Principal principal, Credentials creds) throws RepositoryException { + if (!disableTokenAuth && tokenCredentials != null) { + Authentication authentication = new TokenBasedAuthentication(tokenCredentials.getToken(), tokenExpiration, session); + if (authentication.canHandle(creds)) { + return authentication; + } + } + + if (user != null) { + Authentication authentication = new SimpleCredentialsAuthentication(user); + if (authentication.canHandle(creds)) { + return authentication; + } + } + // no valid user or authentication could not handle the given credentials + return null; + } + + /** + * Handles the impersonation of given Credentials. + *

    + * Current implementation takes {@link User} for the given Principal and + * delegates the check to + * {@link org.apache.jackrabbit.api.security.user.Impersonation#allows(javax.security.auth.Subject)} + * + * @param principal Principal to impersonate. + * @param credentials Credentials used to create the impersonation subject. + * @return false, if there is no User to impersonate, + * true if impersonation is allowed + * @throws javax.jcr.RepositoryException + * @throws javax.security.auth.login.FailedLoginException + * if credentials don't allow to impersonate to principal + * @see AbstractLoginModule#impersonate(Principal, Credentials) + */ + @Override + protected boolean impersonate(Principal principal, Credentials credentials) + throws RepositoryException, FailedLoginException { + if (user != null) { + Subject impersSubject = getImpersonatorSubject(credentials); + if (user.getImpersonation().allows(impersSubject)) { + return true; + } else { + throw new FailedLoginException("attempt to impersonate denied for " + principal.getName()); + } + } else { + log.debug("Failed to retrieve user to impersonate for principal name " + principal.getName()); + return false; + } + } + + //-------------------------------------------------------------------------- + // methods used for token based login + //-------------------------------------------------------------------------- + /** + * Return a flag indicating if token based authentication is disabled. + * + * @return true if token based authentication is disabled; + * false otherwise. + */ + public boolean isDisableTokenAuth() { + return disableTokenAuth; + } + + /** + * Set a flag indicating if token based authentication is disabled. + * + * @param disableTokenAuth true to disable token based + * authentication; false otherwise + */ + public void setDisableTokenAuth(boolean disableTokenAuth) { + this.disableTokenAuth = disableTokenAuth; + } + + /** + * @return The configured expiration time for login tokens in milliseconds. + */ + public long getTokenExpiration() { + return tokenExpiration; + } + + /** + * @param tokenExpiration Sets the configured expiration time (in milliseconds) + * of login tokens. + */ + public void setTokenExpiration(long tokenExpiration) { + this.tokenExpiration = tokenExpiration; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/ImpersonationCallback.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/ImpersonationCallback.java new file mode 100644 index 00000000000..b1736569f28 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/ImpersonationCallback.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.core.security.authentication; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; + +/** + * Callback for a {@link javax.security.auth.callback.CallbackHandler} to ask + * for a the impersonating {@link javax.security.auth.Subject} to create a + * {@link javax.jcr.Session} to access the {@link javax.jcr.Repository}. + */ +public class ImpersonationCallback implements Callback { + + /** + * The impersonating {@link Subject}. + * @see #setImpersonator(Object) + * @see #getImpersonator() + */ + private Subject impersonator; + + /** + * Sets the impersonator in this callback. + * + * @param impersonator The impersonator to set on this callback. If this is + * not a {@link Subject} this method does nothing. + */ + public void setImpersonator(Object impersonator) { + if (impersonator instanceof Subject) { + this.impersonator = (Subject) impersonator; + } + } + + /** + * Returns the impersonator {@link Subject} set on this callback or + * null if not set. + * + * @return the impersonator {@link Subject} set on this callback or + * null if not set. + */ + public Subject getImpersonator() { + return impersonator; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/JAASAuthContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/JAASAuthContext.java new file mode 100644 index 00000000000..d5ff93b11b7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/JAASAuthContext.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.core.security.authentication; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +/** + * Implements the common {@link AuthContext} interface for the JAAS environment. + * + * @see AuthContext + */ +public class JAASAuthContext implements AuthContext { + + private LoginContext context; + + /** + * @param appName application name in JAAS Login-Configuration to use + * @param cbHandler CallbackHandler for login-modules + * @param subject to extend authentication + */ + protected JAASAuthContext(String appName, CallbackHandler cbHandler, + Subject subject) { + + // make sure we are using our own context class loader when we + // instantiate a LoginContext. See bug# 14329. + Thread current = Thread.currentThread(); + ClassLoader orig = current.getContextClassLoader(); + try { + current.setContextClassLoader(JAASAuthContext.class.getClassLoader()); + if (null == subject) { + context = new LoginContext(appName, cbHandler); + } else { + context = new LoginContext(appName, subject, cbHandler); + } + } catch (LoginException e) { + //all cases it is thrown are checked -> ignore + } finally { + current.setContextClassLoader(orig); + } + } + + public void login() throws LoginException { + context.login(); + } + + public Subject getSubject() { + return context.getSubject(); + } + + public void logout() throws LoginException { + context.logout(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/LocalAuthContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/LocalAuthContext.java new file mode 100644 index 00000000000..b790b80c5ae --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/LocalAuthContext.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.core.security.authentication; + +import org.apache.jackrabbit.core.config.ConfigurationException; +import org.apache.jackrabbit.core.config.LoginModuleConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Provide AuthContext interface, for a JAAS-LoginModule not running in + * a {@link javax.security.auth.login.LoginContext} + * + * @see AuthContext + */ +public class LocalAuthContext implements AuthContext { + + private static final Logger log = LoggerFactory.getLogger(LocalAuthContext.class); + + private Subject subject; + + private LoginModuleConfig config; + + private LoginModule module; + + private final CallbackHandler cbHandler; + + /** + * Create Context and set Subject to extend its authentication + * + * @param config Configuration to be used for the LoginModule + * @param cbHandler CallbackHandler for the LoginModule + * @param subject Subject if a pre-authenticated exists + */ + protected LocalAuthContext(LoginModuleConfig config, + CallbackHandler cbHandler, + Subject subject) { + this.config = config; + this.cbHandler = cbHandler; + this.subject = (null == subject) ? new Subject() : subject; + } + + public void login() throws LoginException { + try { + module = config.getLoginModule(); + } catch (ConfigurationException e) { + throw new LoginException(e.getMessage()); + } + + Map state = new HashMap(); + Map options = new HashMap(); + Properties parameters = config.getParameters(); + Enumeration< ? > names = parameters.propertyNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + options.put(name, parameters.getProperty(name)); + } + module.initialize(subject, cbHandler, state, options); + + try { + if (!(module.login() && module.commit())) { + throw new FailedLoginException("LoginModule ignored Credentials"); + } + } catch (LoginException le) { + module.abort(); + throw le; + } catch (Exception e) { + module.abort(); + LoginException le = new LoginException("LoginModule could not perform authentication: " + + e.getMessage()); + le.initCause(e); + log.debug("Login failed to runtime-exception: ", e); + throw le; + } + } + + public Subject getSubject() { + return subject; + } + + public void logout() throws LoginException { + if (subject != null) { + module.logout(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/RepositoryCallback.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/RepositoryCallback.java new file mode 100644 index 00000000000..f644b3949e2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/RepositoryCallback.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.core.security.authentication; + +import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry; + +import javax.jcr.Session; +import javax.security.auth.callback.Callback; + +/** + * Callback for a {@link javax.security.auth.callback.CallbackHandler} to ask for + * a {@link Session} to access the {@link javax.jcr.Repository} + */ +public class RepositoryCallback implements Callback { + + private Session session; + private PrincipalProviderRegistry principalProviderRegistry; + private String adminId; + private String anonymousId; + + public void setSession(Session session) { + this.session = session; + } + + public Session getSession() { + return session; + } + + public void setPrincipalProviderRegistry(PrincipalProviderRegistry principalProviderRegistry) { + this.principalProviderRegistry = principalProviderRegistry; + } + + public PrincipalProviderRegistry getPrincipalProviderRegistry() { + return principalProviderRegistry; + } + + public String getAdminId() { + return adminId; + } + + public void setAdminId(String adminId) { + this.adminId = adminId; + } + + public String getAnonymousId() { + return anonymousId; + } + + public void setAnonymousId(String anonymousId) { + this.anonymousId = anonymousId; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/SimpleCredentialsAuthentication.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/SimpleCredentialsAuthentication.java new file mode 100644 index 00000000000..c233c397a72 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/SimpleCredentialsAuthentication.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.core.security.authentication; + +import org.apache.jackrabbit.api.security.user.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; + +/** + * This Authentication implementation compare + * {@link javax.jcr.SimpleCredentials SimpleCredentials} stored + * for a given {@link org.apache.jackrabbit.api.security.user.User#getCredentials() User} + * to the credentials passed to {@link #authenticate(Credentials)}. + * + * @see org.apache.jackrabbit.core.security.authentication.Authentication + * @see javax.jcr.SimpleCredentials + */ +class SimpleCredentialsAuthentication implements Authentication { + + private static final Logger log = LoggerFactory.getLogger(SimpleCredentialsAuthentication.class); + + private final CryptedSimpleCredentials creds; + + /** + * Create a new Authentication instance for the given User. + * + * @param user to create the Authentication. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + SimpleCredentialsAuthentication(User user) throws RepositoryException { + Credentials creds = user.getCredentials(); + if (creds instanceof CryptedSimpleCredentials) { + this.creds = (CryptedSimpleCredentials) creds; + } else if (creds instanceof SimpleCredentials) { + try { + this.creds = new CryptedSimpleCredentials((SimpleCredentials) creds); + } catch (NoSuchAlgorithmException e) { + throw new RepositoryException(e); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(e); + } + } else { + log.warn("No Credentials found with user " + user.getID()); + this.creds = null; + } + } + + //------------------------------------------------< Authentication >-------- + /** + * This Authentication is able to handle the validation of SimpleCredentials. + * + * @param credentials to test + * @return true if the specified Credentials are + * SimpleCredentials and if the User used to + * construct this instance provides credentials that can be compared to + * SimpleCredentials. + * @see Authentication#canHandle(Credentials) + */ + public boolean canHandle(Credentials credentials) { + return creds != null && credentials instanceof SimpleCredentials; + } + + /** + * Compare the + * {@link org.apache.jackrabbit.api.security.user.User#getCredentials() Credentials} obtained from the User + * with the specified credentials.
    + * If the specified credentials are an instance of + * SimpleCredentials and match the user's credentials this + * method returns true; otherwise false. + * + * @param credentials Credentials to be used for the authentication. + * @return true if the given Credentials' UserID/Password pair match + * the credentials attached to the user this SimpleCredentialsAuthentication + * has been built for. + * @throws RepositoryException If an error occurs. + */ + public boolean authenticate(Credentials credentials) throws RepositoryException { + if (!(credentials instanceof SimpleCredentials)) { + throw new RepositoryException("SimpleCredentials expected. Cannot handle " + credentials.getClass().getName()); + } + try { + if (creds != null && creds.matches((SimpleCredentials) credentials)) { + return true; + } + } catch (NoSuchAlgorithmException e) { + log.debug("Failed to verify Credentials with {}: {}.", credentials.toString(), e); + } catch (UnsupportedEncodingException e) { + log.debug("Failed to verify Credentials with {}: {}.", credentials.toString(), e); + } + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/CompatTokenProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/CompatTokenProvider.java new file mode 100644 index 00000000000..900c2ed884b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/CompatTokenProvider.java @@ -0,0 +1,427 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authentication.token; + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +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.Map; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +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.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.user.UserImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.util.ISO8601; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Backport of the TokenProvider implementation present with OAK adjusted to + * match some subtle differences in jackrabbit token login. + */ +class CompatTokenProvider { + + private static final Logger log = LoggerFactory.getLogger(CompatTokenProvider.class); + + private static final String TOKEN_ATTRIBUTE = ".token"; + private static final String TOKEN_ATTRIBUTE_EXPIRY = TOKEN_ATTRIBUTE + ".exp"; + private static final String TOKEN_ATTRIBUTE_KEY = TOKEN_ATTRIBUTE + ".key"; + private static final String TOKENS_NODE_NAME = ".tokens"; + private static final String TOKENS_NT_NAME = "nt:unstructured"; // TODO: configurable + + private static final char DELIM = '_'; + + private final SessionImpl session; + private final UserManager userManager; + private final long tokenExpiration; + + CompatTokenProvider(SessionImpl session, long tokenExpiration) throws RepositoryException { + this.session = session; + this.userManager = session.getUserManager(); + this.tokenExpiration = tokenExpiration; + } + + /** + * Create a separate token node underneath a dedicated token store within + * the user home node. That token node contains the hashed token, the + * expiration time and additional mandatory attributes that will be verified + * during login. + * + * @param user + * @param sc The current simple credentials. + * @return A new {@code TokenInfo} or {@code null} if the token could not + * be created. + */ + public TokenInfo createToken(User user, SimpleCredentials sc) throws RepositoryException { + String userPath = null; + Principal pr = user.getPrincipal(); + if (pr instanceof ItemBasedPrincipal) { + userPath = ((ItemBasedPrincipal) pr).getPath(); + } + + TokenCredentials tokenCredentials; + if (userPath != null && session.nodeExists(userPath)) { + Node userNode = session.getNode(userPath); + Node tokenParent; + if (!userNode.hasNode(TOKENS_NODE_NAME)) { + userNode.addNode(TOKENS_NODE_NAME, TOKENS_NT_NAME); + try { + session.save(); + } catch (RepositoryException e) { + // may happen when .tokens node is created concurrently + session.refresh(false); + } + } + tokenParent = userNode.getNode(TOKENS_NODE_NAME); + + long creationTime = new Date().getTime(); + long expirationTime = creationTime + tokenExpiration; + + Calendar cal = GregorianCalendar.getInstance(); + cal.setTimeInMillis(creationTime); + + // generate key part of the login token + String key = generateKey(8); + + // create the token node + String tokenName = Text.replace(ISO8601.format(cal), ":", "."); + Node tokenNode; + // avoid usage of sequential nodeIDs + if (System.getProperty(NodeIdFactory.SEQUENTIAL_NODE_ID) == null) { + tokenNode = tokenParent.addNode(tokenName); + } else { + tokenNode = ((NodeImpl) tokenParent).addNodeWithUuid(tokenName, NodeId.randomId().toString()); + } + + StringBuilder sb = new StringBuilder(tokenNode.getIdentifier()); + sb.append(DELIM).append(key); + + String token = sb.toString(); + tokenCredentials = new TokenCredentials(token); + sc.setAttribute(TOKEN_ATTRIBUTE, token); + + // add key property + tokenNode.setProperty(TOKEN_ATTRIBUTE_KEY, getDigestedKey(key)); + + // add expiration time property + cal.setTimeInMillis(expirationTime); + tokenNode.setProperty(TOKEN_ATTRIBUTE_EXPIRY, session.getValueFactory().createValue(cal)); + + // add additional attributes passed in by the credentials. + for (String name : sc.getAttributeNames()) { + if (!TOKEN_ATTRIBUTE.equals(name)) { + String value = sc.getAttribute(name).toString(); + tokenNode.setProperty(name, value); + tokenCredentials.setAttribute(name, value); + } + } + session.save(); + return new CompatModeInfo(token, tokenNode); + } else { + throw new RepositoryException("Cannot create login token: No corresponding node for User " + user.getID() +" in workspace '" + session.getWorkspace().getName() + "'."); + } + } + + /** + * Retrieves the token information associated with the specified login + * token. If no accessible {@code Tree} exists for the given token or if + * the token is not associated with a valid user this method returns {@code null}. + * + * @param token A valid login token. + * @return The {@code TokenInfo} associated with the specified token or + * {@code null} of the corresponding information does not exist or is not + * associated with a valid user. + */ + public TokenInfo getTokenInfo(String token) throws RepositoryException { + if (token == null) { + return null; + } + NodeImpl tokenNode = (NodeImpl) getTokenNode(token, session); + String userId = getUserId(tokenNode, userManager); + if (userId == null || !isValidTokenTree(tokenNode)) { + return null; + } else { + return new CompatModeInfo(token); + } + } + + static Node getTokenNode(String token, Session session) throws RepositoryException { + int pos = token.indexOf(DELIM); + String id = (pos == -1) ? token : token.substring(0, pos); + return session.getNodeByIdentifier(id); + } + + public static String getUserId(TokenCredentials tokenCredentials, Session session) throws RepositoryException { + if (!(session instanceof JackrabbitSession)) { + throw new RepositoryException("JackrabbitSession expected"); + } + NodeImpl n = (NodeImpl) getTokenNode(tokenCredentials.getToken(), session); + return getUserId(n, ((JackrabbitSession) session).getUserManager()); + } + + private static String getUserId(NodeImpl tokenNode, UserManager userManager) throws RepositoryException { + if (tokenNode != null) { + final NodeImpl userNode = (NodeImpl) tokenNode.getParent().getParent(); + final String principalName = userNode.getProperty(UserImpl.P_PRINCIPAL_NAME).getString(); + if (userNode.isNodeType(UserImpl.NT_REP_USER)) { + Authorizable a = userManager.getAuthorizable(new ItemBasedPrincipal() { + public String getPath() throws RepositoryException { + return userNode.getPath(); + } + + public String getName() { + return principalName; + } + }); + if (a != null && !a.isGroup() && !((User)a).isDisabled()) { + return a.getID(); + } + } else { + throw new RepositoryException("Failed to calculate userId from token credentials"); + } + } + return null; + } + + /** + * Returns {@code true} if the specified {@code attributeName} + * starts with or equals {@link #TOKEN_ATTRIBUTE}. + * + * @param attributeName The attribute name. + * @return {@code true} if the specified {@code attributeName} + * starts with or equals {@link #TOKEN_ATTRIBUTE}. + */ + static boolean isMandatoryAttribute(String attributeName) { + return attributeName != null && attributeName.startsWith(TOKEN_ATTRIBUTE); + } + + /** + * Returns false if the specified attribute name doesn't have + * a 'jcr' or 'rep' namespace prefix; true otherwise. This is + * a lazy evaluation in order to avoid testing the defining node type of + * the associated jcr property. + * + * @param propertyName + * @return 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 !Name.NS_JCR_PREFIX.equals(prefix) && !Name.NS_REP_PREFIX.equals(prefix); + } + + private static boolean isValidTokenTree(NodeImpl tokenNode) throws RepositoryException { + if (tokenNode == null) { + return false; + } else { + return TOKENS_NODE_NAME.equals(tokenNode.getParent().getName()); + } + } + + private static String generateKey(int size) { + SecureRandom random = new SecureRandom(); + byte key[] = new byte[size]; + random.nextBytes(key); + + StringBuffer res = new StringBuffer(key.length * 2); + for (byte b : key) { + res.append(Text.hexTable[(b >> 4) & 15]); + res.append(Text.hexTable[b & 15]); + } + return res.toString(); + } + + private static String getDigestedKey(TokenCredentials tc) throws RepositoryException { + String tk = tc.getToken(); + int pos = tk.indexOf(DELIM); + if (pos > -1) { + return getDigestedKey(tk.substring(pos+1)); + } + return null; + } + + private static String getDigestedKey(String key) throws RepositoryException { + try { + StringBuilder sb = new StringBuilder(); + sb.append("{").append(SecurityConstants.DEFAULT_DIGEST).append("}"); + sb.append(Text.digest(SecurityConstants.DEFAULT_DIGEST, key, "UTF-8")); + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RepositoryException("Failed to generate login token."); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException("Failed to generate login token."); + } + } + + private final class CompatModeInfo implements TokenInfo { + + private final String token; + + private final Map attributes; + private final Map info; + private final long expiry; + private final String key; + + private CompatModeInfo(String token) throws RepositoryException { + this(token, getTokenNode(token, session)); + } + + private CompatModeInfo(String token, Node n) throws RepositoryException { + this.token = token; + long expTime = Long.MAX_VALUE; + String keyV = null; + if (token != null) { + attributes = new HashMap(); + info = new HashMap(); + + PropertyIterator it = n.getProperties(); + while (it.hasNext()) { + Property p = it.nextProperty(); + String name = p.getName(); + if (TOKEN_ATTRIBUTE_EXPIRY.equals(name)) { + expTime = p.getLong(); + } else if (TOKEN_ATTRIBUTE_KEY.equals(name)) { + keyV = p.getString(); + } else if (isMandatoryAttribute(name)) { + attributes.put(name, p.getString()); + } else if (isInfoAttribute(name)) { + info.put(name, p.getString()); + } // else: jcr property -> ignore + } + } else { + attributes = Collections.emptyMap(); + info = Collections.emptyMap(); + } + expiry = expTime; + key = keyV; + } + + public String getToken() { + return token; + } + + public boolean isExpired(long loginTime) { + return expiry < loginTime; + } + + public boolean remove() { + Session s = null; + try { + s = ((SessionImpl) session).createSession(session.getWorkspace().getName()); + Node tokenNode = getTokenNode(token, s); + + tokenNode.remove(); + s.save(); + return true; + } catch (RepositoryException e) { + log.warn("Internal error while removing token node.", e); + } finally { + if (s != null) { + s.logout(); + } + } + return false; + } + + public boolean matches(TokenCredentials tokenCredentials) throws RepositoryException { + // test for matching key + if (key != null && !key.equals(getDigestedKey(tokenCredentials))) { + return false; + } + + // check if all other required attributes match + for (String name : attributes.keySet()) { + if (!attributes.get(name).equals(tokenCredentials.getAttribute(name))) { + // no match -> login fails. + 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 key : info.keySet()) { + if (!attrNames.contains(key)) { + tokenCredentials.setAttribute(key, info.get(key)); + } + } + + return true; + } + + public boolean resetExpiration(long loginTime) throws RepositoryException { + Node tokenNode; + Session s = null; + try { + // expiry... + if (expiry - loginTime <= tokenExpiration/2) { + long expirationTime = loginTime + tokenExpiration; + Calendar cal = GregorianCalendar.getInstance(); + cal.setTimeInMillis(expirationTime); + + s = ((SessionImpl) session).createSession(session.getWorkspace().getName()); + tokenNode = getTokenNode(token, s); + tokenNode.setProperty(TOKEN_ATTRIBUTE_EXPIRY, s.getValueFactory().createValue(cal)); + s.save(); + return true; + } + } catch (RepositoryException e) { + log.warn("Failed to update expiry or informative attributes of token node.", e); + } finally { + if (s != null) { + s.logout(); + } + } + return false; + } + + public TokenCredentials getCredentials() { + TokenCredentials tc = new TokenCredentials(token); + for (String name : attributes.keySet()) { + tc.setAttribute(name, attributes.get(name)); + } + for (String name : info.keySet()) { + tc.setAttribute(name, info.get(name)); + } + return tc; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthentication.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthentication.java new file mode 100644 index 00000000000..d578d217727 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthentication.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.core.security.authentication.token; + +import java.util.Date; +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authentication.Authentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Authentication implementation that compares the tokens stored with a + * given user node to the token present in the SimpleCredentials attributes. + * Authentication succeeds if the login token refers to a non-expired + * token node and if all other credential attributes are equal to the + * corresponding properties. + */ +public class TokenBasedAuthentication implements Authentication { + + private static final Logger log = LoggerFactory.getLogger(TokenBasedAuthentication.class); + + /** + * Default expiration time for login tokens is 2 hours. + */ + public static final long TOKEN_EXPIRATION = 2 * 3600 * 1000; + + /** + * The name of the login token attribute. + */ + public static final String TOKEN_ATTRIBUTE = ".token"; + + /** + * @deprecated This system parameter allows to enable backwards compatible + * behavior of the {@code TokenBasedAuthentication}. Note that as of OAK 1.0 + * this flag will no be supported. + */ + public static final String PARAM_COMPAT = "TokenCompatMode"; + + private final TokenInfo tokenInfo; + + public TokenBasedAuthentication(String token, long tokenExpiration, Session session) throws RepositoryException { + if (compatMode()) { + this.tokenInfo = new CompatTokenProvider((SessionImpl) session, tokenExpiration).getTokenInfo(token); + } else { + this.tokenInfo = new TokenProvider((SessionImpl) session, tokenExpiration).getTokenInfo(token); + } + + } + + /** + * @see Authentication#canHandle(javax.jcr.Credentials) + */ + public boolean canHandle(Credentials credentials) { + return tokenInfo != null && isTokenBasedLogin(credentials); + } + + /** + * @see Authentication#authenticate(javax.jcr.Credentials) + */ + public boolean authenticate(Credentials credentials) throws RepositoryException { + if (!(credentials instanceof TokenCredentials)) { + throw new RepositoryException("TokenCredentials expected. Cannot handle " + credentials.getClass().getName()); + } + TokenCredentials tokenCredentials = (TokenCredentials) credentials; + return validateCredentials(tokenCredentials); + } + + private boolean validateCredentials(TokenCredentials tokenCredentials) throws RepositoryException { + 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"); + tokenInfo.remove(); + return false; + } + + if (tokenInfo.matches(tokenCredentials)) { + tokenInfo.resetExpiration(loginTime); + return true; + } + + return false; + } + + //-------------------------------------------------------------------------- + /** + * Returns true if the given credentials object + * is an instance of TokenCredentials. + * + * @param credentials + * @return true if the given credentials object + * is an instance of TokenCredentials; false otherwise. + */ + public static boolean isTokenBasedLogin(Credentials credentials) { + return credentials instanceof TokenCredentials; + } + + /** + * Returns true if the specified attributeName + * starts with or equals {@link #TOKEN_ATTRIBUTE}. + * + * @param attributeName + * @return true if the specified attributeName + * starts with or equals {@link #TOKEN_ATTRIBUTE}. + */ + public static boolean isMandatoryAttribute(String attributeName) { + if (compatMode()) { + return CompatTokenProvider.isMandatoryAttribute(attributeName); + } else { + return TokenProvider.isMandatoryAttribute(attributeName); + } + } + + /** + * Returns true if the specified credentials + * should be used to create a new login token. + * + * @param credentials + * @return true if upon successful authentication a new + * login token should be created; false otherwise. + */ + public static boolean doCreateToken(Credentials credentials) { + if (credentials instanceof SimpleCredentials) { + Object attr = ((SimpleCredentials) credentials).getAttribute(TOKEN_ATTRIBUTE); + return (attr != null && "".equals(attr.toString())); + } + return false; + } + + /** + * Create a new token node for the specified user. + * + * @param user + * @param credentials + * @param tokenExpiration + * @param session + * @return A new instance of TokenCredentials to be used for + * further login actions against this Authentication implementation. + * @throws RepositoryException If there is no node corresponding to the + * specified user in the current workspace or if an error occurs while + * creating the token node. + */ + public static Credentials createToken(User user, SimpleCredentials credentials, + long tokenExpiration, Session session) throws RepositoryException { + String workspaceName = session.getWorkspace().getName(); + if (user == null) { + throw new RepositoryException("Cannot create login token: No corresponding node for 'null' user in workspace '" + workspaceName + "'."); + } + + TokenInfo ti; + if (compatMode()) { + ti = new CompatTokenProvider((SessionImpl) session, tokenExpiration).createToken(user, credentials); + } else { + ti = new TokenProvider((SessionImpl) session, tokenExpiration).createToken(user, credentials); + } + + if (ti != null) { + return ti.getCredentials(); + } else { + throw new RepositoryException("Cannot create login token."); + } + } + + public static Node getTokenNode(TokenCredentials credentials, Session session) throws RepositoryException { + if (compatMode()) { + return CompatTokenProvider.getTokenNode(credentials.getToken(), session); + } else { + return TokenProvider.getTokenNode(credentials.getToken(), session); + } + } + + + public static String getUserId(TokenCredentials tokenCredentials, Session session) throws RepositoryException { + if (compatMode()) { + return CompatTokenProvider.getUserId(tokenCredentials, session); + } else { + if (!(session instanceof JackrabbitSession)) { + throw new RepositoryException("JackrabbitSession expected"); + } + NodeImpl n = (NodeImpl) getTokenNode(tokenCredentials, session); + return TokenProvider.getUserId(n, ((JackrabbitSession) session).getUserManager()); + } + } + + private static boolean compatMode() { + return Boolean.parseBoolean(System.getProperty(PARAM_COMPAT)); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenInfo.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenInfo.java new file mode 100644 index 00000000000..ebddb16fa12 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenInfo.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.core.security.authentication.token; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; + +/** + * TokenInfo... TODO + */ +interface TokenInfo { + + String getToken(); + + + boolean isExpired(long loginTime); + + boolean remove(); + + boolean matches(TokenCredentials tokenCredentials) throws RepositoryException; + + boolean resetExpiration(long loginTime) throws RepositoryException; + + TokenCredentials getCredentials(); +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenProvider.java new file mode 100644 index 00000000000..1b32a3312f4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenProvider.java @@ -0,0 +1,475 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authentication.token; + +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +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.jcr.AccessDeniedException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +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.core.NodeImpl; +import org.apache.jackrabbit.core.ProtectedItemModifier; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.user.PasswordUtility; +import org.apache.jackrabbit.core.security.user.UserImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.util.ISO8601; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Backport of the TokenProvider implementation present with OAK adjusted to + * match some subtle differences in jackrabbit token login. + */ +public class TokenProvider extends ProtectedItemModifier { + + private static final Logger log = LoggerFactory.getLogger(TokenProvider.class); + + 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 TOKEN_NT_NAME = "rep:Token"; + private static final Name TOKENS_NT_NAME = NameConstants.NT_UNSTRUCTURED; + + private static final char DELIM = '_'; + + private static final Set RESERVED_ATTRIBUTES = new HashSet(3); + static { + RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE); + RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE_EXPIRY); + RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE_KEY); + } + + private static final Collection RESERVED_PREFIXES = Collections.unmodifiableList(Arrays.asList( + NamespaceRegistry.PREFIX_XML, + NamespaceRegistry.PREFIX_JCR, + NamespaceRegistry.PREFIX_NT, + NamespaceRegistry.PREFIX_MIX, + Name.NS_XMLNS_PREFIX, + Name.NS_REP_PREFIX, + Name.NS_SV_PREFIX + )); + + private final SessionImpl session; + private final UserManager userManager; + private final long tokenExpiration; + + TokenProvider(SessionImpl session, long tokenExpiration) throws RepositoryException { + this.session = session; + this.userManager = session.getUserManager(); + this.tokenExpiration = tokenExpiration; + } + + /** + * Create a separate token node underneath a dedicated token store within + * the user home node. That token node contains the hashed token, the + * expiration time and additional mandatory attributes that will be verified + * during login. + * + * @param user + * @param sc The current simple credentials. + * @return A new {@code TokenInfo} or {@code null} if the token could not + * be created. + */ + public TokenInfo createToken(User user, SimpleCredentials sc) throws RepositoryException { + TokenInfo tokenInfo = null; + if (sc != null && user != null && user.getID().equalsIgnoreCase(sc.getUserID())) { + String[] attrNames = sc.getAttributeNames(); + Map attributes = new HashMap(attrNames.length); + for (String attrName : sc.getAttributeNames()) { + attributes.put(attrName, sc.getAttribute(attrName).toString()); + } + tokenInfo = createToken(user, attributes); + if (tokenInfo != null) { + // also set the new token to the simple credentials. + sc.setAttribute(TOKEN_ATTRIBUTE, tokenInfo.getToken()); + } + } + + return tokenInfo; + } + + /** + * Create a separate token node underneath a dedicated token store within + * the user home node. That token node contains the hashed token, the + * expiration time and additional mandatory attributes that will be verified + * during login. + * + * @param userId The identifier of the user for which a new token should + * be created. + * @param attributes The attributes associated with the new token. + * @return A new {@code TokenInfo} or {@code null} if the token could not + * be created. + */ + private TokenInfo createToken(User user, Map attributes) throws RepositoryException { + String error = "Failed to create login token. "; + NodeImpl tokenParent = getTokenParent(user); + if (tokenParent != null) { + try { + ValueFactory vf = session.getValueFactory(); + long creationTime = new Date().getTime(); + Calendar creation = GregorianCalendar.getInstance(); + creation.setTimeInMillis(creationTime); + + Name tokenName = session.getQName(Text.replace(ISO8601.format(creation), ":", ".")); + NodeImpl tokenNode = super.addNode(tokenParent, tokenName, session.getQName(TOKEN_NT_NAME), NodeId.randomId()); + + String key = generateKey(8); + String token = new StringBuilder(tokenNode.getId().toString()).append(DELIM).append(key).toString(); + + String keyHash = PasswordUtility.buildPasswordHash(getKeyValue(key, user.getID())); + setProperty(tokenNode, session.getQName(TOKEN_ATTRIBUTE_KEY), vf.createValue(keyHash)); + setProperty(tokenNode, session.getQName(TOKEN_ATTRIBUTE_EXPIRY), createExpirationValue(creationTime, session)); + + for (String name : attributes.keySet()) { + if (!RESERVED_ATTRIBUTES.contains(name)) { + String attr = attributes.get(name).toString(); + setProperty(tokenNode, session.getQName(name), vf.createValue(attr)); + } + } + session.save(); + return new TokenInfoImpl(tokenNode, token, user.getID()); + } catch (NoSuchAlgorithmException e) { + // error while generating login token + log.error(error, e); + } catch (AccessDeniedException e) { + log.warn(error, e); + } + } else { + log.warn("Unable to get/create token store for user {}", user.getID()); + } + return null; + } + + private Value createExpirationValue(long creationTime, Session session) throws RepositoryException { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(createExpirationTime(creationTime, tokenExpiration)); + return session.getValueFactory().createValue(cal); + } + + /** + * Retrieves the token information associated with the specified login + * token. If no accessible {@code Tree} exists for the given token or if + * the token is not associated with a valid user this method returns {@code null}. + * + * @param token A valid login token. + * @return The {@code TokenInfo} associated with the specified token or + * {@code null} of the corresponding information does not exist or is not + * associated with a valid user. + */ + public TokenInfo getTokenInfo(String token) throws RepositoryException { + if (token == null) { + return null; + } + NodeImpl tokenNode = (NodeImpl) getTokenNode(token, session); + String userId = getUserId(tokenNode, userManager); + if (userId == null || !isValidTokenTree(tokenNode)) { + return null; + } else { + return new TokenInfoImpl(tokenNode, token, userId); + } + } + + static Node getTokenNode(String token, Session session) throws RepositoryException { + int pos = token.indexOf(DELIM); + String id = (pos == -1) ? token : token.substring(0, pos); + return session.getNodeByIdentifier(id); + } + + static String getUserId(NodeImpl tokenNode, UserManager userManager) throws RepositoryException { + if (tokenNode != null) { + final NodeImpl userNode = (NodeImpl) tokenNode.getParent().getParent(); + final String principalName = userNode.getProperty(UserImpl.P_PRINCIPAL_NAME).getString(); + if (userNode.isNodeType(UserImpl.NT_REP_USER)) { + Authorizable a = userManager.getAuthorizable(new ItemBasedPrincipal() { + public String getPath() throws RepositoryException { + return userNode.getPath(); + } + + public String getName() { + return principalName; + } + }); + if (a != null && !a.isGroup() && !((User)a).isDisabled()) { + return a.getID(); + } + } else { + throw new RepositoryException("Failed to calculate userId from token credentials"); + } + } + return null; + } + + /** + * Returns {@code true} if the specified {@code attributeName} + * starts with or equals {@link #TOKEN_ATTRIBUTE}. + * + * @param attributeName The attribute name. + * @return {@code true} if the specified {@code attributeName} + * starts with or equals {@link #TOKEN_ATTRIBUTE}. + */ + 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 attributeName The attribute name. + * @return {@code true} if the specified property name doesn't seem + * to represent repository internal information. + */ + static boolean isInfoAttribute(String attributeName) { + String prefix = Text.getNamespacePrefix(attributeName); + return !RESERVED_PREFIXES.contains(prefix); + } + + private static long createExpirationTime(long creationTime, long tokenExpiration) { + return creationTime + tokenExpiration; + } + + private static long getExpirationTime(NodeImpl tokenNode, long defaultValue) throws RepositoryException { + if (tokenNode.hasProperty(TOKEN_ATTRIBUTE_EXPIRY)) { + return tokenNode.getProperty(TOKEN_ATTRIBUTE_EXPIRY).getLong(); + } else { + return defaultValue; + } + } + + 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(); + } + + private static String getKeyValue(String key, String userId) { + return key + userId; + } + + private static boolean isValidTokenTree(NodeImpl tokenNode) throws RepositoryException { + if (tokenNode == null) { + return false; + } else { + return TOKENS_NODE_NAME.equals(tokenNode.getParent().getName()) && + TOKEN_NT_NAME.equals(tokenNode.getPrimaryNodeType().getName()); + } + } + + private NodeImpl getTokenParent(User user) throws RepositoryException { + NodeImpl tokenParent = null; + String parentPath = null; + try { + if (user != null) { + Principal pr = user.getPrincipal(); + if (pr instanceof ItemBasedPrincipal) { + String userPath = ((ItemBasedPrincipal) pr).getPath(); + NodeImpl userNode = (NodeImpl) session.getNode(userPath); + if (userNode.hasNode(TOKENS_NODE_NAME)) { + tokenParent = (NodeImpl) userNode.getNode(TOKENS_NODE_NAME); + } else { + tokenParent = userNode.addNode(session.getQName(TOKENS_NODE_NAME), TOKENS_NT_NAME, NodeId.randomId()); + parentPath = userPath + '/' + TOKENS_NODE_NAME; + session.save(); + } + } + } else { + log.debug("Cannot create login token: No user specified. (null)"); + } + } catch (RepositoryException e) { + // conflict while creating token store for this user -> refresh and + // try to get the tree from the updated root. + log.debug("Conflict while creating token store -> retrying", e); + session.refresh(false); + if (parentPath != null && session.nodeExists(parentPath)) { + tokenParent = (NodeImpl) session.getNode(parentPath); + } + } + return tokenParent; + } + + private 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(NodeImpl tokenNode, String token, String userId) throws RepositoryException { + this.token = token; + this.tokenPath = tokenNode.getPath(); + this.userId = userId; + + expirationTime = getExpirationTime(tokenNode, Long.MIN_VALUE); + key = tokenNode.getProperty(TOKEN_ATTRIBUTE_KEY).getString(); + + mandatoryAttributes = new HashMap(); + publicAttributes = new HashMap(); + PropertyIterator pit = tokenNode.getProperties(); + while (pit.hasNext()) { + Property property = pit.nextProperty(); + String name = property.getName(); + String value = property.getString(); + 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 + } + } + + public String getToken() { + return token; + } + + public boolean isExpired(long loginTime) { + return expirationTime < loginTime; + } + + public boolean resetExpiration(long loginTime) throws RepositoryException { + if (isExpired(loginTime)) { + log.debug("Attempt to reset an expired token."); + return false; + } + + Session s = null; + try { + if (expirationTime - loginTime <= tokenExpiration / 2) { + s = session.createSession(session.getWorkspace().getName()); + setProperty((NodeImpl) s.getNode(tokenPath), session.getQName(TOKEN_ATTRIBUTE_EXPIRY), createExpirationValue(loginTime, session)); + s.save(); + log.debug("Successfully reset token expiration time."); + return true; + } + } catch (RepositoryException e) { + log.warn("Error while resetting token expiration", e); + } finally { + if (s != null) { + s.logout(); + } + } + return false; + } + + public boolean matches(TokenCredentials tokenCredentials) { + String tk = tokenCredentials.getToken(); + int pos = tk.lastIndexOf(DELIM); + if (pos > -1) { + tk = tk.substring(pos + 1); + } + if (key == null || !PasswordUtility.isSame(key, getKeyValue(tk, userId))) { + 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; + } + + public boolean remove() { + Session s = null; + try { + s = session.createSession(session.getWorkspace().getName()); + Node node = s.getNode(tokenPath); + node.remove(); + s.save(); + return true; + } catch (RepositoryException e) { + log.warn("Internal error while removing token node.", e); + } finally { + if (s != null) { + s.logout(); + } + } + return false; + } + + public TokenCredentials getCredentials() { + TokenCredentials tc = new TokenCredentials(token); + for (String name : mandatoryAttributes.keySet()) { + tc.setAttribute(name, mandatoryAttributes.get(name)); + } + for (String name : publicAttributes.keySet()) { + tc.setAttribute(name, publicAttributes.get(name)); + } + return tc; + } + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractACLTemplate.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractACLTemplate.java new file mode 100644 index 00000000000..49e14c6a5b0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractACLTemplate.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.core.security.authorization; + +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import javax.jcr.security.AccessControlEntry; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractACLTemplate... + */ +public abstract class AbstractACLTemplate implements JackrabbitAccessControlList, + AccessControlConstants { + + private static Logger log = LoggerFactory.getLogger(AbstractACLTemplate.class); + + /** + * Path of the node this ACL template has been created for. + */ + protected final String path; + + /** + * The value factory + */ + protected final ValueFactory valueFactory; + + protected AbstractACLTemplate(String path, ValueFactory valueFactory) { + this.path = path; + this.valueFactory = valueFactory; + } + + /** + * Validates the given parameters to create a new ACE and throws an + * AccessControlException if any of them is invalid. Otherwise + * this method returns silently. + * + * @param principal The principal to create the ACE for. + * @param privileges The privileges to be granted/denied by the ACE. + * @param isAllow Defines if the privileges are allowed or denied. + * @param restrictions The additional restrictions. + * @throws AccessControlException If any of the given parameters is invalid. + */ + protected abstract void checkValidEntry(Principal principal, + Privilege[] privileges, + boolean isAllow, + Map restrictions) throws AccessControlException; + + /** + * Return the list of entries, if they are held in an orderable list. + * + * @return the list of entries. + * @see #orderBefore(AccessControlEntry, AccessControlEntry) + */ + protected abstract List getEntries(); + + //--------------------------------------< JackrabbitAccessControlPolicy >--- + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy#getPath() + */ + public String getPath() { + return path; + } + + //----------------------------------------< JackrabbitAccessControlList >--- + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean) + */ + public boolean addEntry(Principal principal, Privilege[] privileges, boolean isAllow) + throws AccessControlException, RepositoryException { + return addEntry(principal, privileges, isAllow, Collections.emptyMap()); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean, Map, Map) + */ + public boolean addEntry(Principal principal, Privilege[] privileges, boolean isAllow, Map restrictions, Map mvRestrictions) throws AccessControlException, RepositoryException { + if (mvRestrictions == null || mvRestrictions.isEmpty()) { + return addEntry(principal, privileges, isAllow, restrictions); + } else { + throw new UnsupportedRepositoryOperationException("Not implemented. Please use Jackrabbit OAK to get support for multi-valued restrictions."); + } + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#size() + */ + public int size() { + return getEntries().size(); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#isEmpty() + */ + public boolean isEmpty() { + return getEntries().isEmpty(); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#orderBefore(javax.jcr.security.AccessControlEntry, javax.jcr.security.AccessControlEntry) + */ + public void orderBefore(AccessControlEntry srcEntry, AccessControlEntry destEntry) throws AccessControlException, UnsupportedRepositoryOperationException, RepositoryException { + if (srcEntry.equals(destEntry)) { + log.debug("srcEntry equals destEntry -> no reordering required."); + return; + } + + List entries = getEntries(); + int index = (destEntry == null) ? entries.size()-1 : entries.indexOf(destEntry); + if (index < 0) { + throw new AccessControlException("destEntry not contained in this AccessControlList"); + } else { + if (entries.remove(srcEntry)) { + // re-insert the srcEntry at the new position. + entries.add(index, srcEntry); + } else { + // src entry not contained in this list. + throw new AccessControlException("srcEntry not contained in this AccessControlList"); + } + } + } + + //--------------------------------------------------< AccessControlList >--- + /** + * @see javax.jcr.security.AccessControlList#getAccessControlEntries() + */ + public AccessControlEntry[] getAccessControlEntries() throws RepositoryException { + List l = getEntries(); + return l.toArray(new AccessControlEntry[l.size()]); + } + + /** + * @see javax.jcr.security.AccessControlList#addAccessControlEntry(java.security.Principal , javax.jcr.security.Privilege[]) + */ + public boolean addAccessControlEntry(Principal principal, Privilege[] privileges) + throws AccessControlException, RepositoryException { + return addEntry(principal, privileges, true, Collections.emptyMap()); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractAccessControlProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractAccessControlProvider.java new file mode 100644 index 00000000000..04682a4ca58 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractAccessControlProvider.java @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.ObservationManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.core.ItemImpl; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.security.SystemPrincipal; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * AbstractAccessControlProvider... + */ +public abstract class AbstractAccessControlProvider implements AccessControlProvider, + AccessControlUtils, AccessControlConstants { + + /** + * Constant for the name of the configuration option "omit-default-permission". + * The option is a flag indicating whether default permissions should be + * created upon initialization of this provider. + *

    + * If this option is present in the configuration no initial ACL content + * is created.
    + * If this configuration option is omitted the default permissions are + * installed. Note however, that the initialization should not overwrite + * previously installed AC content. + */ + public static final String PARAM_OMIT_DEFAULT_PERMISSIONS = "omit-default-permission"; + + /** + * the system session this provider has been created for. + */ + protected SessionImpl session; + protected ObservationManager observationMgr; + protected PrivilegeManagerImpl privilegeManager; + + private boolean initialized; + + protected AbstractAccessControlProvider() { + } + + /** + * Throws IllegalStateException if the provider has not + * been initialized or has been closed. + */ + protected void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("Not initialized or already closed."); + } + } + + /** + * @return the PrivilegeManager + * @throws RepositoryException + */ + protected PrivilegeManagerImpl getPrivilegeManagerImpl() throws RepositoryException { + return privilegeManager; + } + + /** + * Returns compiled permissions for the administrator i.e. permissions + * that grants everything and returns the int representation of {@link Privilege#JCR_ALL} + * upon {@link CompiledPermissions#getPrivileges(Path)} for all + * paths. + * + * @return an implementation of CompiledPermissions that + * grants everything and always returns the int representation of + * {@link Privilege#JCR_ALL} upon {@link CompiledPermissions#getPrivileges(Path)}. + */ + protected CompiledPermissions getAdminPermissions() { + return new CompiledPermissions() { + public void close() { + //nop + } + public boolean grants(Path absPath, int permissions) { + return true; + } + public int getPrivileges(Path absPath) throws RepositoryException { + return PrivilegeRegistry.getBits(new Privilege[] {getAllPrivilege()}); + } + public boolean hasPrivileges(Path absPath, Privilege... privileges) { + return true; + } + public Set getPrivilegeSet(Path absPath) throws RepositoryException { + return Collections.singleton(getAllPrivilege()); + } + public boolean canReadAll() { + return true; + } + public boolean canRead(Path itemPath, ItemId itemId) { + return true; + } + + private Privilege getAllPrivilege() throws RepositoryException { + return getPrivilegeManagerImpl().getPrivilege(Privilege.JCR_ALL); + } + }; + } + + /** + * Returns compiled permissions for a read-only user i.e. permissions + * that grants READ permission for all non-AC items. + * + * @return an implementation of CompiledPermissions that + * grants READ permission for all non-AC items. + */ + protected CompiledPermissions getReadOnlyPermissions() { + return new CompiledPermissions() { + public void close() { + //nop + } + public boolean grants(Path absPath, int permissions) throws RepositoryException { + if (isAcItem(absPath)) { + // read-only never has read-AC permission + return false; + } else { + return permissions == Permission.READ; + } + } + public int getPrivileges(Path absPath) throws RepositoryException { + if (isAcItem(absPath)) { + return PrivilegeRegistry.NO_PRIVILEGE; + } else { + return PrivilegeRegistry.getBits(new Privilege[] {getReadPrivilege()}); + } + } + public boolean hasPrivileges(Path absPath, Privilege... privileges) throws RepositoryException { + if (isAcItem(absPath)) { + return false; + } else { + return privileges != null && privileges.length == 1 && getReadPrivilege().equals(privileges[0]); + } + } + public Set getPrivilegeSet(Path absPath) throws RepositoryException { + if (isAcItem(absPath)) { + return Collections.emptySet(); + } else { + return Collections.singleton(getReadPrivilege()); + } + } + public boolean canReadAll() { + return false; + } + public boolean canRead(Path itemPath, ItemId itemId) throws RepositoryException { + if (itemPath != null) { + return !isAcItem(itemPath); + } else { + return !isAcItem(session.getItemManager().getItem(itemId)); + } + } + + private Privilege getReadPrivilege() throws RepositoryException { + return getPrivilegeManagerImpl().getPrivilege(Privilege.JCR_READ); + } + }; + } + + //-------------------------------------------------< AccessControlUtils >--- + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlUtils#isAcItem(Path) + */ + public boolean isAcItem(Path absPath) throws RepositoryException { + Path.Element[] elems = absPath.getElements(); + // start looking for a rep:policy name starting from the last element. + // NOTE: with the current content structure max. 3 levels must be looked + // at as the rep:policy node may only have ACE nodes with properties. + if (elems.length > 1) { + for (int index = elems.length-1, j = 1; index >= 0 && j <= 3; index--, j++) { + if (N_POLICY.equals(elems[index].getName())) { + return true; + } + } + } + return false; + } + + /** + * Test if the given node is itself a rep:ACL or a rep:ACE node. + * @see org.apache.jackrabbit.core.security.authorization.AccessControlUtils#isAcItem(org.apache.jackrabbit.core.ItemImpl) + */ + public boolean isAcItem(ItemImpl item) throws RepositoryException { + NodeImpl n = ((item.isNode()) ? (NodeImpl) item : (NodeImpl) item.getParent()); + Name ntName = ((NodeTypeImpl) n.getPrimaryNodeType()).getQName(); + return ntName.equals(NT_REP_ACL) || + ntName.equals(NT_REP_GRANT_ACE) || + ntName.equals(NT_REP_DENY_ACE); + } + + /** + * @see AccessControlUtils#isAdminOrSystem(Set) + */ + public boolean isAdminOrSystem(Set principals) { + for (Principal p : principals) { + if (p instanceof AdminPrincipal || p instanceof SystemPrincipal) { + return true; + } + } + return false; + } + + /** + * @see AccessControlUtils#isReadOnly(Set) + */ + public boolean isReadOnly(Set principals) { + // TODO: find ways to determine read-only status + return false; + } + + //----------------------------------------------< AccessControlProvider >--- + /** + * Tests if the given systemSession is a SessionImpl and + * retrieves the observation manager. The it sets the internal 'initialized' + * field to true. + * + * @throws RepositoryException If the specified session is not a + * SessionImpl or if retrieving the observation manager fails. + * @see AccessControlProvider#init(Session, Map) + */ + public void init(Session systemSession, Map configuration) throws RepositoryException { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + if (!(systemSession instanceof SessionImpl)) { + throw new RepositoryException("SessionImpl (system session) expected."); + } + session = (SessionImpl) systemSession; + observationMgr = systemSession.getWorkspace().getObservationManager(); + privilegeManager = (PrivilegeManagerImpl) ((JackrabbitWorkspace) session.getWorkspace()).getPrivilegeManager(); + + initialized = true; + } + + /** + * @see AccessControlProvider#close() + */ + public void close() { + checkInitialized(); + initialized = false; + } + + /** + * @see AccessControlProvider#isLive() + */ + public boolean isLive() { + return initialized && session.isLive(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractCompiledPermissions.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractCompiledPermissions.java new file mode 100644 index 00000000000..557faf791e8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AbstractCompiledPermissions.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.commons.collections.map.LRUMap; +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.RepositoryException; +import javax.jcr.security.Privilege; +import java.util.Map; +import java.util.Set; + +/** + * AbstractCompiledPermissions... + */ +public abstract class AbstractCompiledPermissions implements CompiledPermissions { + + // cache mapping a Path to a 'Result' containing permissions and privileges. + private final Map cache; + private final Object monitor = new Object(); + + @SuppressWarnings("unchecked") + protected AbstractCompiledPermissions() { + cache = new LRUMap(1000); + } + + /** + * + * @param absPath Absolute path to return the result for. + * @return the Result for the give absPath. + * @throws RepositoryException if an error occurs. + */ + public Result getResult(Path absPath) throws RepositoryException { + Result result; + synchronized (monitor) { + result = cache.get(absPath); + if (result == null) { + if (absPath == null) { + result = buildRepositoryResult(); + } else { + result = buildResult(absPath); + } + cache.put(absPath, result); + } + } + return result; + } + + /** + * Retrieve the result for the specified path. + * + * @param absPath Absolute path to build the result for. + * @return Result for the specified absPath. + * @throws RepositoryException If an error occurs. + */ + protected abstract Result buildResult(Path absPath) throws RepositoryException; + + /** + * Retrieve the result for repository level operations. + * + * @return The result instance for those permissions and privileges granted + * for repository level operations. + * @throws RepositoryException + */ + protected abstract Result buildRepositoryResult() throws RepositoryException; + + /** + * Retrieve the privilege manager. + * + * @return An instance of privilege manager. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + protected abstract PrivilegeManagerImpl getPrivilegeManagerImpl() throws RepositoryException; + + /** + * Removes all entries from the cache. + */ + protected void clearCache() { + synchronized (monitor) { + cache.clear(); + } + } + + //------------------------------------------------< CompiledPermissions >--- + /** + * @see CompiledPermissions#close() + */ + public void close() { + clearCache(); + } + + /** + * @see CompiledPermissions#grants(Path, int) + */ + public boolean grants(Path absPath, int permissions) throws RepositoryException { + return getResult(absPath).grants(permissions); + } + + /** + * @see CompiledPermissions#getPrivileges(Path) + */ + public int getPrivileges(Path absPath) throws RepositoryException { + Set pvs = getPrivilegeSet(absPath); + return PrivilegeRegistry.getBits(pvs.toArray(new Privilege[pvs.size()])); + } + + /** + * @see CompiledPermissions#hasPrivileges(org.apache.jackrabbit.spi.Path, javax.jcr.security.Privilege[]) + */ + public boolean hasPrivileges(Path absPath, Privilege... privileges) throws RepositoryException { + Result result = getResult(absPath); + + PrivilegeBits bits = getPrivilegeManagerImpl().getBits(privileges); + return result.allowPrivileges.includes(bits); + } + + /** + * @see CompiledPermissions#getPrivilegeSet(Path) + */ + public Set getPrivilegeSet(Path absPath) throws RepositoryException { + Result result = getResult(absPath); + return getPrivilegeManagerImpl().getPrivileges(result.allowPrivileges); + } + + /** + * @see CompiledPermissions#canReadAll() + */ + public boolean canReadAll() throws RepositoryException { + return false; + } + + //--------------------------------------------------------< inner class >--- + /** + * Result of permission (and optionally privilege) evaluation for a given path. + */ + public static class Result { + + public static final Result EMPTY = new Result(Permission.NONE, Permission.NONE, PrivilegeBits.EMPTY, PrivilegeBits.EMPTY); + private final int allows; + private final int denies; + private final PrivilegeBits allowPrivileges; + private final PrivilegeBits denyPrivileges; + + private int hashCode = -1; + + /** + * @deprecated + */ + public Result(int allows, int denies, int allowPrivileges, int denyPrivileges) { + this(allows, denies, PrivilegeBits.getInstance(allowPrivileges), PrivilegeBits.getInstance(denyPrivileges)); + } + + public Result(int allows, int denies, PrivilegeBits allowPrivileges, PrivilegeBits denyPrivileges) { + this.allows = allows; + this.denies = denies; + // make sure privilegebits are unmodifiable -> proper hashcode generation + this.allowPrivileges = allowPrivileges.unmodifiable(); + this.denyPrivileges = denyPrivileges.unmodifiable(); + } + + public boolean grants(int permissions) { + return (this.allows | ~permissions) == -1; + } + + /** + * @deprecated jackrabbit 2.3 (throws UnsupportedOperationException, use getPrivilegeBits instead) + */ + public int getPrivileges() { + throw new UnsupportedOperationException("use #getPrivilegeBits instead."); + } + + public PrivilegeBits getPrivilegeBits() { + return allowPrivileges; + } + + public Result combine(Result other) { + int cAllows = allows | Permission.diff(other.allows, denies); + int cDenies = denies | Permission.diff(other.denies, allows); + + PrivilegeBits cAPrivs = PrivilegeBits.getInstance(allowPrivileges); + cAPrivs.addDifference(other.allowPrivileges, denyPrivileges); + PrivilegeBits cdPrivs = PrivilegeBits.getInstance(denyPrivileges); + cdPrivs.addDifference(other.denyPrivileges, allowPrivileges); + + return new Result(cAllows, cDenies, allowPrivileges, denyPrivileges); + } + + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + if (hashCode == -1) { + int h = 17; + h = 37 * h + allows; + h = 37 * h + denies; + h = 37 * h + allowPrivileges.hashCode(); + h = 37 * h + denyPrivileges.hashCode(); + hashCode = h; + } + return hashCode; + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (object instanceof Result) { + Result other = (Result) object; + return allows == other.allows && + denies == other.denies && + allowPrivileges.equals(other.allowPrivileges) && + denyPrivileges.equals(other.denyPrivileges); + } + return false; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlConstants.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlConstants.java new file mode 100644 index 00000000000..b23a1aa8fa3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlConstants.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.core.security.authorization; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * AccessControlConstants... + */ +public interface AccessControlConstants { + + //---------------------------------------------------------< node names >--- + /** + * Default name for a node of type rep:Policy. + */ + Name N_POLICY = NameConstants.REP_POLICY; + + /** + * Name name for a node of type rep:Policy storing repository level privileges. + */ + Name N_REPO_POLICY = NameConstants.REP_REPO_POLICY; + + /** + * PrincipalBased-ACL: + * Name of the root-node of all access-control-nodes that store the + * privileges for individual principals. This node is created upon + * initializing this provider. + */ + Name N_ACCESSCONTROL = NameConstants.REP_ACCESSCONTROL; + + //-----------------------------------------------------< property names >--- + /** + * rep:privileges property name + */ + Name P_PRIVILEGES = NameConstants.REP_PRIVILEGES; + /** + * rep:principalName property name + */ + Name P_PRINCIPAL_NAME = NameConstants.REP_PRINCIPAL_NAME; + + /** + * rep:glob property name used to restrict the number of child nodes + * or properties that are affected by an ACL inherited from a parent node. + */ + Name P_GLOB = NameConstants.REP_GLOB; + + //----------------------------------------------------< node type names >--- + /** + * rep:AccessControllable nodetype + */ + Name NT_REP_ACCESS_CONTROLLABLE = NameConstants.REP_ACCESS_CONTROLLABLE; + /** + * rep:RepoAccessControllable nodetype + */ + Name NT_REP_REPO_ACCESS_CONTROLLABLE = NameConstants.REP_REPO_ACCESS_CONTROLLABLE; + /** + * rep:ACL nodetype + */ + Name NT_REP_ACL = NameConstants.REP_ACL; + /** + * rep:ACE nodetype + */ + Name NT_REP_ACE = NameConstants.REP_ACE; + /** + * rep:GrantACE nodetype + */ + Name NT_REP_GRANT_ACE = NameConstants.REP_GRANT_ACE; + /** + * rep:DenyACE nodetype + */ + Name NT_REP_DENY_ACE = NameConstants.REP_DENY_ACE; + + //----------------------------------< node types for principal based ac >--- + /** + * rep:AccessControl nodetype + */ + Name NT_REP_ACCESS_CONTROL = NameConstants.REP_ACCESS_CONTROL; + + /** + * rep:PrincipalAccessControl nodetype + */ + Name NT_REP_PRINCIPAL_ACCESS_CONTROL = NameConstants.REP_PRINCIPAL_ACCESS_CONTROL; + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlEditor.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlEditor.java new file mode 100644 index 00000000000..5867da61416 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlEditor.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.core.security.authorization; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlPolicy; +import java.security.Principal; + +/** + * AccessControlEditor is used to edit the access control policy + * and entry objects provided by the respective service. + */ +public interface AccessControlEditor { + + /** + * Retrieves the policies for the Node identified by the given + * nodePath. In contrast to {@link #editAccessControlPolicies} this method + * returns an empty array if no policy has been applied before by calling + * {@link #setPolicy}). Still the returned policies are detached from + * the AccessControlProvider and are only an external representation. + * Modification will therefore not take effect, until they are written back to + * the editor and persisted. + *

    + * Compared to the policy returned by {@link AccessControlProvider#getEffectivePolicies(org.apache.jackrabbit.spi.Path, CompiledPermissions)}, + * the scope of the policies it limited to the Node itself and does + * not take inherited elements into account. + * + * @param nodePath Absolute path to an existing node object. + * @return the policies applied so far or an empty array if no + * policy has been applied to the node before. + * @throws AccessControlException If the Node identified by the given + * nodePath does not allow access control modifications (e.g. + * the node itself stores the access control information for its parent). + * @throws PathNotFoundException if no node exists for the given + * nodePath. + * @throws RepositoryException if an error occurs + */ + AccessControlPolicy[] getPolicies(String nodePath) throws AccessControlException, + PathNotFoundException, RepositoryException; + + /** + * Retrieves the policies that have been applied before for the given + * principal. In contrast to {@link #editAccessControlPolicies} + * this method returns an empty array if no policy has been applied before + * by calling {@link #setPolicy}). Still the returned policies are detached from + * the AccessControlProvider and are only an external representation. + * Modification will therefore not take effect, until they are written back to + * the editor and persisted. + * + * @param principal Principal for which the editable policies should be + * returned. + * @return the policies applied so far or an empty array if no + * policy has been applied before. + * @throws AccessControlException if the specified principal does not exist, + * if this implementation cannot provide policies for individual principals or + * if same other access control related exception occurs. + * @throws RepositoryException if an error occurs + */ + JackrabbitAccessControlPolicy[] getPolicies(Principal principal) + throws AccessControlException, RepositoryException; + + /** + * Retrieves the editable policies for the Node identified by the given + * nodePath that are applicable but have not yet have been set.
    + * The AccessControlPolicy objects returned are detached from the underlying + * AccessControlProvider and is only an external + * representation. Modification will therefore not take effect, until a + * modified policy is written back to the editor and persisted. + *

    + * See {@link #getPolicies(String)} for the corresponding method that returns + * the editable policies that have been set to the node at + * nodePath before. + *

    + * Compared to the policies returned by {@link AccessControlProvider#getEffectivePolicies(org.apache.jackrabbit.spi.Path, CompiledPermissions)}, + * the scope of the policies returned by this methods it limited to the Node + * itself and does never not take inherited elements into account. + * + * @param nodePath Absolute path to an existing node object. + * @return an array of editable access control policies. + * @throws AccessControlException If the Node identified by the given + * nodePath does not allow access control modifications. + * @throws PathNotFoundException if no node exists for the given + * nodePath. + * @throws RepositoryException if an error occurs + */ + AccessControlPolicy[] editAccessControlPolicies(String nodePath) + throws AccessControlException, PathNotFoundException, RepositoryException; + + /** + * Returns an array of editable policies for the given principal. + * + * @param principal Principal for which the editable policies should be + * returned. + * @return an array of editable policies for the given principal. + * @throws AccessDeniedException If the editing session is not allowed to + * edit policies. + * @throws AccessControlException if the specified principal does not exist, + * if this implementation cannot provide policies for individual principals or + * if same other access control related exception occurs. + * @throws RepositoryException if another error occurs. + */ + JackrabbitAccessControlPolicy[] editAccessControlPolicies(Principal principal) + throws AccessDeniedException, AccessControlException, RepositoryException; + + /** + * Stores the policy template to the respective node. + * + * @param nodePath Absolute path to an existing node object. + * @param policy the AccessControlPolicy to store. + * @throws AccessControlException If the policy is null or + * if it is not applicable to the Node identified by the given + * nodePath. + * @throws PathNotFoundException if no node exists for the given + * nodePath. + * @throws RepositoryException if an other error occurs. + */ + void setPolicy(String nodePath, AccessControlPolicy policy) + throws AccessControlException, PathNotFoundException, RepositoryException; + + /** + * Removes the specified policy from the node at nodePath. + * + * @param nodePath Absolute path to an existing node object. + * @param policy The policy to be removed at nodePath. + * @throws AccessControlException If the Node identified by the given + * nodePath does not allow policy modifications or does not have + * the specified policy attached. + * @throws PathNotFoundException if no node exists for the given + * nodePath. + * @throws RepositoryException if an other error occurs + */ + void removePolicy(String nodePath, AccessControlPolicy policy) + throws AccessControlException, PathNotFoundException, RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlEntryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlEntryImpl.java new file mode 100644 index 00000000000..2bac64bc3af --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlEntryImpl.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.value.ValueHelper; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Simple, immutable implementation of the + * {@link javax.jcr.security.AccessControlEntry} + * and the {@link JackrabbitAccessControlEntry} interfaces. + */ +public abstract class AccessControlEntryImpl implements JackrabbitAccessControlEntry { + + /** + * All privileges contained in this entry. + */ + private Privilege[] privileges; + + /** + * PrivilegeBits calculated from built-in privileges + */ + private final PrivilegeBits privilegeBits; + + /** + * the Principal of this entry + */ + private final Principal principal; + + /** + * Jackrabbit specific extension: if the actions contained are allowed or + * denied. + */ + private final boolean allow; + + /** + * Jackrabbit specific extension: the list of additional restrictions to be + * included in the evaluation. + */ + private final Map restrictions; + + /** + * Hash code being calculated on demand. + */ + private int hashCode = -1; + + /** + * Construct an access control entry for the given principal and privileges. + * + * @param principal Principal for this access control entry. + * @param privileges Privileges for this access control entry. + * @param isAllow true if this ACE grants the specified + * privileges to the specified principal; false otherwise. + * @param restrictions A map of restriction name (String) to restriction + * (Value). See {@link org.apache.jackrabbit.api.security.JackrabbitAccessControlList#getRestrictionNames()} + * and {@link org.apache.jackrabbit.api.security.JackrabbitAccessControlList#getRestrictionType(String)}. + * @throws AccessControlException if either principal or privileges are invalid. + * @throws RepositoryException if another error occurs. + */ + protected AccessControlEntryImpl(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions) + throws AccessControlException, RepositoryException { + if (principal == null || privileges == null) { + throw new AccessControlException(); + } + // make sure no abstract privileges are passed. + for (Privilege privilege : privileges) { + if (privilege.isAbstract()) { + throw new AccessControlException("Privilege " + privilege + " is abstract."); + } + } + this.principal = principal; + this.privileges = privileges; + this.privilegeBits = getPrivilegeManager().getBits(privileges).unmodifiable(); + this.allow = isAllow; + + if (restrictions == null) { + this.restrictions = Collections.emptyMap(); + } else { + this.restrictions = new HashMap(restrictions.size()); + // validate the passed restrictions and fill the map + for (String name : restrictions.keySet()) { + Value value = ValueHelper.copy(restrictions.get(name), getValueFactory()); + this.restrictions.put(getResolver().getQName(name), value); + } + } + } + + /** + * Construct an access control entry for the given principal and privileges. + * + * @param principal Principal for this access control entry. + * @param privilegesBits Privileges for this access control entry. + * @param isAllow true if this ACE grants the specified + * privileges to the specified principal; false otherwise. + * @param restrictions A map of restriction name (String) to restriction + * (Value). See {@link org.apache.jackrabbit.api.security.JackrabbitAccessControlList#getRestrictionNames()} + * and {@link org.apache.jackrabbit.api.security.JackrabbitAccessControlList#getRestrictionType(String)}. + * @throws RepositoryException if another error occurs. + */ + protected AccessControlEntryImpl(Principal principal, PrivilegeBits privilegesBits, + boolean isAllow, Map restrictions) + throws RepositoryException { + if (principal == null || privilegesBits == null) { + throw new IllegalArgumentException(); + } + this.principal = principal; + this.privilegeBits = privilegesBits.unmodifiable(); + this.allow = isAllow; + + if (restrictions == null) { + this.restrictions = Collections.emptyMap(); + } else { + this.restrictions = new HashMap(restrictions.size()); + // validate the passed restrictions and fill the map + for (String name : restrictions.keySet()) { + Value value = ValueHelper.copy(restrictions.get(name), getValueFactory()); + this.restrictions.put(getResolver().getQName(name), value); + } + } + } + + /** + * + * @param base + * @param privilegeBits + * @param isAllow + * @throws AccessControlException + */ + protected AccessControlEntryImpl(AccessControlEntryImpl base, PrivilegeBits privilegeBits, boolean isAllow) + throws AccessControlException, RepositoryException { + this(base.principal, privilegeBits, isAllow, (base.restrictions.isEmpty()) ? null : Collections.emptyMap()); + + if (!base.restrictions.isEmpty()) { + // validate the passed restrictions and fill the map + for (Name name : base.restrictions.keySet()) { + Value value = ValueHelper.copy(base.restrictions.get(name), getValueFactory()); + this.restrictions.put(name, value); + } + } + } + + /** + * + * @param base + * @param privileges + * @param isAllow + * @throws AccessControlException + */ + protected AccessControlEntryImpl(AccessControlEntryImpl base, Privilege[] privileges, boolean isAllow) + throws AccessControlException, RepositoryException { + this(base.principal, privileges, isAllow, (base.restrictions.isEmpty()) ? null : Collections.emptyMap()); + + if (!base.restrictions.isEmpty()) { + // validate the passed restrictions and fill the map + for (Name name : base.restrictions.keySet()) { + Value value = ValueHelper.copy(base.restrictions.get(name), getValueFactory()); + this.restrictions.put(name, value); + } + } + } + + /** + * @return the permission bits that correspond to the privileges defined by this entry. + */ + public PrivilegeBits getPrivilegeBits() { + return privilegeBits; + } + + /** + * Returns true if this ACE defines any restriction. + * + * @return true if this ACE defines any restriction; + * false otherwise. + */ + public boolean hasRestrictions() { + return !restrictions.isEmpty(); + } + + /** + * Returns the restrictions defined for this entry. + * + * @return the restrictions defined for this entry. + */ + public Map getRestrictions() { + return Collections.unmodifiableMap(restrictions); + } + + /** + * @param restrictionName + * @return The restriction with the specified name or null. + */ + public Value getRestriction(Name restrictionName) { + return ValueHelper.copy(restrictions.get(restrictionName), getValueFactory()); + } + + /** + * @return Returns the name resolver used to convert JCR names to Name and vice versa. + */ + protected abstract NameResolver getResolver(); + + /** + * @return The value factory to be used. + */ + protected abstract ValueFactory getValueFactory(); + + /** + * @return The privilege manager in use. + */ + protected abstract PrivilegeManagerImpl getPrivilegeManager(); + + /** + * Build the hash code. + * + * @return the hash code. + */ + protected int buildHashCode() { + int h = 17; + h = 37 * h + principal.getName().hashCode(); + h = 37 * h + privilegeBits.hashCode(); + h = 37 * h + Boolean.valueOf(allow).hashCode(); + h = 37 * h + restrictions.hashCode(); + return h; + } + + //-------------------------------------------------< AccessControlEntry >--- + /** + * @see javax.jcr.security.AccessControlEntry#getPrincipal() + */ + public Principal getPrincipal() { + return principal; + } + + /** + * @see javax.jcr.security.AccessControlEntry#getPrivileges() + */ + public Privilege[] getPrivileges() { + if (privileges == null) { + Set ps = getPrivilegeManager().getPrivileges(privilegeBits); + privileges = ps.toArray(new Privilege[ps.size()]); + } + return privileges; + } + + + //---------------------------------------< JackrabbitAccessControlEntry >--- + /** + * @see JackrabbitAccessControlEntry#isAllow() + */ + public boolean isAllow() { + return allow; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry#getRestrictionNames() + */ + public String[] getRestrictionNames() throws NamespaceException { + String[] restrNames = new String[restrictions.size()]; + int i = 0; + for (Name n : restrictions.keySet()) { + restrNames[i] = getResolver().getJCRName(n); + i++; + } + return restrNames; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry#getRestriction(String) + */ + public Value getRestriction(String restrictionName) throws RepositoryException { + return getRestriction(getResolver().getQName(restrictionName)); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry#getRestrictions(String) + */ + public Value[] getRestrictions(String restrictionName) throws RepositoryException { + return new Value[] {getRestriction(restrictionName)}; + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + if (hashCode == -1) { + hashCode = buildHashCode(); + } + return hashCode; + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof AccessControlEntryImpl) { + AccessControlEntryImpl other = (AccessControlEntryImpl) obj; + return principal.getName().equals(other.principal.getName()) && + privilegeBits.equals(other.privilegeBits) && + allow == other.allow && + restrictions.equals(other.restrictions); + } + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlListener.java new file mode 100644 index 00000000000..2c1ee26a95e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlListener.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.core.security.authorization; + +/** + * + */ +public interface AccessControlListener { + + /** + * Informs this listener about changes made to access control content. + * + * @param modifications Information about access control modifications. + */ + void acModified(AccessControlModifications modifications); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlModifications.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlModifications.java new file mode 100644 index 00000000000..e02d091a25a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlModifications.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.core.security.authorization; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * AccessControlModifications is an unmodifiable collection of + * modifications made to access control content allowing the + * {@link AccessControlListener modification listeners} to keep caches up to date. + */ +public class AccessControlModifications { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(AccessControlModifications.class); + + private final Map modificationMap; + + /** + * @param modificationMap Map specifying the access control modifications. + * The keys allows to identify the Node that was modified by + * the policy modifications. The values specifies the modification type, + * which may be any of + *

      + *
    • {@link AccessControlObserver#POLICY_ADDED}
    • + *
    • {@link AccessControlObserver#POLICY_MODIFIED}
    • + *
    • {@link AccessControlObserver#POLICY_REMOVED}
    • + *
    + */ + public AccessControlModifications(Map modificationMap) { + this.modificationMap = Collections.unmodifiableMap(modificationMap); + } + + /** + * @return Set of Node identifiers or paths. + */ + public Set getNodeIdentifiers() { + return modificationMap.keySet(); + } + + /** + * @param identifier + * @return The modification type for the specified "identifier". Note that + * the object type of the identifier is independent specific. + */ + public Integer getType(K identifier) { + return modificationMap.get(identifier); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlObserver.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlObserver.java new file mode 100644 index 00000000000..be2094ce7be --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlObserver.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.core.security.authorization; + +import org.apache.jackrabbit.core.observation.SynchronousEventListener; + +import java.util.HashSet; +import java.util.Set; + +/** + * AccessControlObserver... + */ +public abstract class AccessControlObserver implements SynchronousEventListener { + + public static final int POLICY_ADDED = 1; + public static final int POLICY_REMOVED = 2; + public static final int POLICY_MODIFIED = 4; + public static final int MOVE = 8; + + private final Set listeners = new HashSet(); + private final Object listenerMonitor = new Object(); + + protected void close() { + synchronized (listenerMonitor) { + listeners.clear(); + } + } + + /** + * Add a listener that needs to be informed about changes made to access + * control. + * + * @param listener EntryListener to be added. + */ + public void addListener(AccessControlListener listener) { + synchronized (listenerMonitor) { + listeners.add(listener); + } + } + + /** + * Remove a listener added before. + * + * @param listener EntryListener to be removed. + */ + public void removeListener(AccessControlListener listener) { + synchronized (listenerMonitor) { + listeners.remove(listener); + } + } + + /** + * Notifies the listeners about AC modifications. + * + * @param modifications + */ + protected void notifyListeners(AccessControlModifications modifications) { + AccessControlListener[] lstnrs; + synchronized (listenerMonitor) { + lstnrs = listeners.toArray(new AccessControlListener[listeners.size()]); + } + for (AccessControlListener lstnr : lstnrs) { + lstnr.acModified(modifications); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProvider.java new file mode 100644 index 00000000000..8d6b653824d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProvider.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import javax.jcr.security.AccessControlPolicy; + +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import java.security.Principal; +import java.util.Map; +import java.util.Set; + +/** + * The AccessControlProvider is used to provide access control policy and entry + * objects that apply to an item in a single workspace. The provider is bound + * to a system session in contrast to the AccessControlManager that + * is bound to a specific session/subject. + *

    + * Please note following additional special conditions: + *

      + *
    • The detection of access control policy/entries is an implementation + * detail. They may be resource based or retrieved by other means.
    • + *
    • An access control policy/entry may be inherited across the item hierarchy. + * The details are left to the implementation
    • + *
    • If no policy can be determined for a particular Item the implementation + * must return some implementation specific default policy.
    • + *
    • Transient (NEW) items created within a regular Session object are unknown + * to and cannot be handled by the AccessControlProvider.
    • + *
    • If the item id passed to the corresponding calls doesn't point to an + * existing item, ItemNotFoundException will be thrown. It is + * therefore recommended to evaluate the id of the closest not-new ancestor + * node before calling any methods on the provider.
    • + *
    • Changes to access control policy and entries made through the + * AccessControlEditor are not effective unless + * they are persisted by calling Session.save() on the session + * that has been used to obtain the editor.
    • + *
    + * + * @see AccessControlProviderFactory + */ +public interface AccessControlProvider { + + /** + * Allows the {@link AccessControlProviderFactory} to pass a session + * and configuration parameters to the AccessControlProvider. + * + * @param systemSession System session. + * @param configuration Configuration used to initialize this provider. + * @throws RepositoryException If an error occurs. + */ + void init(Session systemSession, Map configuration) throws RepositoryException; + + /** + * Closes this provider when it is no longer used by the respective + * workspace and release resources bound by this provider. + */ + void close(); + + /** + * Returns true, if this provider is still alive and able to + * evaluate permissions; false otherwise. + * + * @return true, if this provider is still alive and able to + * evaluate permissions; false otherwise. + */ + boolean isLive(); + + /** + * Returns the effective policies for the node at the given absPath. + * + * @param absPath an absolute path. + * @param permissions The effective permissions of the editing + * sessions that attempts to view the effective policies. + * @return The effective policies that apply at absPath or + * an empty array if the implementation cannot determine the effective + * policy at the given path. + * @throws ItemNotFoundException If no Node with the specified + * absPath exists. + * @throws RepositoryException If another error occurs. + * @see javax.jcr.security.AccessControlManager#getEffectivePolicies(String) + */ + AccessControlPolicy[] getEffectivePolicies(Path absPath, CompiledPermissions permissions) throws ItemNotFoundException, RepositoryException; + + /** + * Returns the effective policies for the given principals. + * + * @param principals A set of principal. + * @param permissions The effective permissions of the editing + * sessions that attempts to view the effective policies. @return The effective policies that are in effect for the given + * principal or an empty array. + * @throws RepositoryException If error occurs. + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getEffectivePolicies(Set) + */ + AccessControlPolicy[] getEffectivePolicies(Set principals, CompiledPermissions permissions) throws RepositoryException; + + /** + * Returns an AccessControlEditor for the given Session object + * or null if the implementation does not support editing + * of access control policies. + * + * @param session The editing session. + * @return the ACL editor or null. + * @throws RepositoryException If an error occurs. + */ + AccessControlEditor getEditor(Session session) throws RepositoryException; + + /** + * Compiles the effective policy for the specified set of + * Principals. + * + * @param principals Set of principals to compile the permissions for. If + * the order of evaluating permissions for principals is meaningful, the + * caller should pass a Set that respects the order of insertion. + * @return The effective, compiled CompiledPolicy that applies for the + * specified set of principals. + * @throws RepositoryException If an error occurs. + */ + CompiledPermissions compilePermissions(Set principals) throws RepositoryException; + + /** + * Returns true if the given set of principals can access the + * root node of the workspace this provider has been built for; + * false otherwise. + * + * @param principals Set of principals to be tested for being allowed to + * access the root node. + * @return true if the given set of principals can access the + * root node of the workspace this provider has been built for; + * false otherwise. + * @throws RepositoryException If an error occurs. + */ + boolean canAccessRoot(Set principals) throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProviderFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProviderFactory.java new file mode 100644 index 00000000000..4c190a63385 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProviderFactory.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.core.security.authorization; + +import org.apache.jackrabbit.core.config.WorkspaceSecurityConfig; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * The AccessControlProviderFactory is used to create + * {@link AccessControlProvider}s for the various workspaces present in the + * repository. If a provider is no longer used by the workspace, it is + * {@link AccessControlProvider#close() closed}. + *

    + * The factory does not need to cache the created {@link AccessControlProvider}s. + * They are used during the entire lifetime of their workspace, and are cached + * together with the respective workspace related objects by the repository + * implementation. + *

    + * The {@link AccessControlProvider}s are requested using a + * {@link Session system Session}. The system sessions have a distinct access + * control rules in order to prevent chicken-egg problems when setting up + * security for a workspace. + */ +public interface AccessControlProviderFactory { + + /** + * Initialize this factory. + * + * @param securitySession Security Session. + * @throws RepositoryException If an error occurs. + */ + void init(Session securitySession) throws RepositoryException; + + /** + * Dispose this AccessControlProviderFactory and its resources. + * + * @throws RepositoryException if an error occurs. + */ + void close() throws RepositoryException; + + /** + * Creates an AccessControlProvider for the workspace of the given + * system session. If the passed configuration is null or + * does not have a provider entry, this factory must create a default + * provider. In any case the provider must be initialized before it + * is returned to the caller. + * + * @param systemSession the system session for the workspace the + * AccessControlProvider should be created for. + * @param config The security configuration for that workspace or + * null if no config entry is present. In this case the + * factory must use its default. The configuration is used to determine + * the implementation of AccessControlProvider to be used + * and to retrieve eventual configuration parameters. + * @return a new, initialized AccessControlProvider. + * @throws RepositoryException if an error occurs + */ + AccessControlProvider createProvider(Session systemSession, WorkspaceSecurityConfig config) throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProviderFactoryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProviderFactoryImpl.java new file mode 100644 index 00000000000..82fb6de8ea4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlProviderFactoryImpl.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.core.security.authorization; + +import org.apache.jackrabbit.core.config.BeanConfig; +import org.apache.jackrabbit.core.config.WorkspaceSecurityConfig; +import org.apache.jackrabbit.core.security.authorization.acl.ACLProvider; +import org.apache.jackrabbit.core.security.user.UserAccessControlProvider; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.Collections; +import java.util.Map; + +/** + * Default implementation of the AccessControlProviderFactory + */ +public class AccessControlProviderFactoryImpl implements AccessControlProviderFactory { + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(AccessControlProviderFactoryImpl.class); + + /** + * The name of the security workspace (containing users...) + */ + private String secWorkspaceName; + private String defaultWorkspaceName; + + //---------------------------------------< AccessControlProviderFactory >--- + /** + * @see AccessControlProviderFactory#init(Session) + */ + public void init(Session securitySession) throws RepositoryException { + secWorkspaceName = securitySession.getWorkspace().getName(); + if (securitySession instanceof SessionImpl) { + defaultWorkspaceName = ((RepositoryImpl) securitySession.getRepository()).getConfig().getDefaultWorkspaceName(); + } // else: unable to determine default workspace name + } + + /** + * @see AccessControlProviderFactory#close() + */ + public void close() throws RepositoryException { + // nothing to do + } + + /** + * @see AccessControlProviderFactory#createProvider(Session, WorkspaceSecurityConfig) + */ + public AccessControlProvider createProvider(Session systemSession, WorkspaceSecurityConfig config) + throws RepositoryException { + String workspaceName = systemSession.getWorkspace().getName(); + AccessControlProvider prov; + Map props; + if (config != null && config.getAccessControlProviderConfig() != null) { + BeanConfig bc = config.getAccessControlProviderConfig(); + prov = bc.newInstance(AccessControlProvider.class); + props = bc.getParameters(); + } else { + log.debug("No ac-provider configuration for workspace " + workspaceName + " -> using defaults."); + if (workspaceName.equals(secWorkspaceName) && !workspaceName.equals(defaultWorkspaceName)) { + // UserAccessControlProvider is designed to work with an extra + // workspace storing user and groups. therefore avoid returning + // this ac provider for the default workspace. + prov = new UserAccessControlProvider(); + } else { + prov = new ACLProvider(); + } + log.debug("Default provider for workspace " + workspaceName + " = " + prov.getClass().getName()); + props = Collections.emptyMap(); + } + + prov.init(systemSession, props); + return prov; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlUtils.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlUtils.java new file mode 100644 index 00000000000..ac5fe65788b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/AccessControlUtils.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.core.security.authorization; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.core.ItemImpl; + +import javax.jcr.RepositoryException; + +import java.security.Principal; +import java.util.Set; + +/** + * AccessControlUtils... + */ +public interface AccessControlUtils { + + /** + * Test if the specified path points to an item that defines AC + * information. + * + * @param absPath Path to an item. + * @return true if the item at the specified absPath contains + * access control information. + * @throws RepositoryException If an error occurs. + */ + boolean isAcItem(Path absPath) throws RepositoryException; + + /** + * Test if the specified path points to an item that defines AC + * information and consequently should be considered protected. + * + * @param item An item. + * @return true if the item at the specified item defines + * access control related information is should therefore be considered + * protected. + * @throws RepositoryException If an error occurs. + */ + boolean isAcItem(ItemImpl item) throws RepositoryException; + + /** + * Test if the specified set of principals contains an admin or system + * principal. + * + * @param principals A set of principals. + * @return true if the specified set of principals contains an + * AdminPrincipal or a SystemPrincipal. + */ + boolean isAdminOrSystem(Set principals); + + /** + * Test if if the specified set of principals will have read-only permissions + * only. False otherwise (or if it cannot be determined from the principal + * set only). + * + * @param principals A set of principals. + * @return true if the specified set of principals will only be granted + * read permission on all items. + */ + boolean isReadOnly(Set principals); + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/CompiledPermissions.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/CompiledPermissions.java new file mode 100644 index 00000000000..57b9e92dd2d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/CompiledPermissions.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.core.security.authorization; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.core.id.ItemId; + +import javax.jcr.RepositoryException; +import javax.jcr.security.Privilege; +import java.util.Collections; +import java.util.Set; + +/** + * CompiledPermissions represents the evaluation of an + * AccessControlPolicy that applies for a given set of + * Principals (normally obtained from the Subject + * of a Session). + */ +public interface CompiledPermissions { + + /** + * Indicate to this CompiledPermissions object that it is + * not used any more. + */ + void close(); + + /** + * Returns true if the specified permissions are granted + * on the item identified by the given path. + * + * @param absPath Absolute path pointing to an item. If the item does + * not exist yet (asking for 'add-node' and 'set-property' permission), + * it's direct ancestor must exist. + * @param permissions A combination of one or more of permission constants + * defined by {@link Permission} encoded as a bitmask value + * @return true if the specified permissions are granted, + * false otherwise. + * @throws RepositoryException if an error occurs. + */ + boolean grants(Path absPath, int permissions) throws RepositoryException; + + /** + * Returns the Privilege bits granted by the underlying policy + * if the given absPath. + * + * @param absPath Absolute path to a Node. + * @return the granted privileges at absPath. + * @throws RepositoryException if an error occurs + * @deprecated Use {@link #getPrivilegeSet(Path)} instead. + */ + int getPrivileges(Path absPath) throws RepositoryException; + + /** + * Returns true if the given privileges are granted at the + * specified absPath. + * + * @param absPath + * @param privileges + * @return true if the given privileges are granted at the + * specified absPath. + * @throws RepositoryException + */ + boolean hasPrivileges(Path absPath, Privilege... privileges) throws RepositoryException; + + /** + * Returns the Privileges granted by the underlying policy + * at the given absPath. + * + * @param absPath Absolute path to a Node. + * @return the granted privileges at absPath. + * @throws RepositoryException if an error occurs + */ + Set getPrivilegeSet(Path absPath) throws RepositoryException; + + /** + * Returns true if READ permission is granted everywhere. + * This method acts as shortcut for {@link #grants(Path, int)} where + * permissions is {@link Permission#READ} and allows to shorten the + * evaluation time given the fact that a check for READ permission is + * considered to be the most frequent test. + * + * @return true if the READ permission is granted everywhere. + * @throws RepositoryException if an error occurs + */ + boolean canReadAll() throws RepositoryException; + + /** + * Returns true if READ permission is granted for the + * existing item with the given Path and/or + * ItemId. + * This method acts as shortcut for {@link #grants(Path, int)} where + * permissions is {@link Permission#READ} and allows to shorten the + * evaluation time given the fact that a check for READ permissions is + * considered to be the most frequent test.
    + * If both Path and ItemId are not null it is left to the + * implementation which parameter to use.n + * + * @param itemPath The path to the item or null if the ID + * should be used to determine the READ permission. + * @param itemId The itemId or null if the path should be + * used to determine the READ permission. + * @return true if the READ permission is granted. + * @throws RepositoryException If no item exists with the specified path or + * itemId or if some other error occurs. + */ + boolean canRead(Path itemPath, ItemId itemId) throws RepositoryException; + + /** + * Static implementation of a CompiledPermissions that doesn't + * grant any permissions at all. + */ + public static final CompiledPermissions NO_PERMISSION = new CompiledPermissions() { + public void close() { + //nop + } + public boolean grants(Path absPath, int permissions) { + // deny everything + return false; + } + public int getPrivileges(Path absPath) { + return PrivilegeRegistry.NO_PRIVILEGE; + } + + public boolean hasPrivileges(Path absPath, Privilege... privileges) throws RepositoryException { + return false; + } + public Set getPrivilegeSet(Path absPath) throws RepositoryException { + return Collections.emptySet(); + } + + public boolean canReadAll() { + return false; + } + public boolean canRead(Path itemPath, ItemId itemId) throws RepositoryException { + return false; + } + }; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/GlobPattern.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/GlobPattern.java new file mode 100644 index 00000000000..5a0acc74756 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/GlobPattern.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GlobPattern defines a simplistic pattern matching. It consists + * of a mandatory (leading) path and an optional "glob" that may contain one or + * more wildcard characters ("*") according to the glob matching + * defined by {@link javax.jcr.Node#getNodes(String[])}. In contrast to that + * method the GlobPattern operates on path (not only names). + *

    + * Please note the following special cases: + *

    + * NodePath     |   Restriction   |   Matches
    + * -----------------------------------------------------------------------------
    + * /foo         |   null          |   matches /foo and all children of /foo
    + * /foo         |   ""            |   matches /foo only
    + * 
    + *

    + * Examples including wildcard char: + *

    + * NodePath = "/foo"
    + * Restriction   |   Matches
    + * -----------------------------------------------------------------------------
    + * *         |   all siblings of foo and foo's and the siblings' descendants
    + * /*cat     |   all children of /foo whose path ends with "cat"
    + * /*/cat    |   all non-direct descendants of /foo named "cat"
    + * /cat*     |   all descendant path of /foo that have the direct foo-descendant segment starting with "cat"
    + * *cat      |   all siblings and descendants of foo that have a name ending with cat
    + * */cat     |   all descendants of /foo and foo's siblings that have a name segment "cat"
    + * cat/*     |   all descendants of '/foocat'
    + * /cat/*    |   all descendants of '/foo/cat'
    + * *cat/*    |   all descendants of /foo that have an intermediate segment ending with 'cat'
    + * 
    + */ +public final class GlobPattern { + + private static Logger log = LoggerFactory.getLogger(GlobPattern.class); + + private static final char WILDCARD_CHAR = '*'; + private static final int MAX_WILDCARD = 20; + + private final String nodePath; + private final String restriction; + + private final Pattern pattern; + + private GlobPattern(String nodePath, String restriction) { + if (nodePath == null) { + throw new IllegalArgumentException(); + } + + this.nodePath = nodePath; + this.restriction = restriction; + + if (restriction != null && restriction.length() > 0) { + StringBuilder b = new StringBuilder(nodePath); + b.append(restriction); + + int lastPos = restriction.lastIndexOf(WILDCARD_CHAR); + if (lastPos >= 0) { + String end; + if (lastPos != restriction.length()-1) { + end = restriction.substring(lastPos + 1); + } else { + end = null; + } + pattern = new WildcardPattern(b.toString(), end); + } else { + pattern = new PathPattern(b.toString()); + } + } else { + pattern = new PathPattern(); + } + } + + public static GlobPattern create(String nodePath, String restrictions) { + return new GlobPattern(nodePath, restrictions); + } + + public static GlobPattern create(String nodePath) { + return create(nodePath, null); + } + + public boolean matches(String toMatch) { + if (toMatch == null) { + return false; + } else { + return pattern.matches(toMatch); + } + } + + public boolean matches(Item itemToMatch) { + try { + // TODO: missing proper impl + return matches(itemToMatch.getPath()); + } catch (RepositoryException e) { + log.error("Unable to determine match. {}", e.getMessage()); + return false; + } + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + int h = 37 * 17 + nodePath.hashCode(); + if (restriction != null) { + h = 37 * h + restriction.hashCode(); + } + return h; + } + + /** + * @see Object#toString() + */ + @Override + public String toString() { + return nodePath + " : " + restriction; + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof GlobPattern) { + GlobPattern other = (GlobPattern) obj; + return nodePath.equals(other.nodePath) && + ((restriction == null) ? other.restriction == null : restriction.equals(other.restriction)); + } + return false; + } + + //------------------------------------------------------< inner classes >--- + /** + * Base for PathPattern and WildcardPattern + */ + private abstract class Pattern { + abstract boolean matches(String toMatch); + } + + /** + * Path pattern: The restriction is missing or doesn't contain any wildcard character. + */ + private final class PathPattern extends Pattern { + + private final String patternStr; + + private PathPattern() { + this(null); + } + private PathPattern(String patternStr) { + this.patternStr = patternStr; + } + + @Override + boolean matches(String toMatch) { + if (restriction == null) { + return Text.isDescendantOrEqual(nodePath, toMatch); + } else if (restriction.length() == 0) { + return nodePath.equals(toMatch); + } else { + // no wildcard contained in restriction: use path defined + // by nodePath + restriction to calculate the match + return Text.isDescendantOrEqual(patternStr, toMatch); + } + } + } + + /** + * Wildcard pattern: The specified restriction contains one or more wildcard character(s). + */ + private final class WildcardPattern extends Pattern { + + private final String patternEnd; + private final char[] patternChars; + + private WildcardPattern(String patternStr, String patternEnd) { + patternChars = patternStr.toCharArray(); + this.patternEnd = patternEnd; + } + + @Override + boolean matches(String toMatch) { + if (patternEnd != null && !toMatch.endsWith(patternEnd)) { + // shortcut: verify if end of pattern matches end of toMatch + return false; + } + char[] tm = (toMatch.endsWith("/")) ? toMatch.substring(0, toMatch.length()-1).toCharArray() : toMatch.toCharArray(); + // shortcut didn't reveal mismatch -> need to process the internal match method. + return matches(patternChars, 0, tm, 0, MAX_WILDCARD); + } + + /** + * + * @param pattern The pattern + * @param pOff + * @param s + * @param sOff + * @return true if matches, false otherwise + */ + private boolean matches(char[] pattern, int pOff, + char[] s, int sOff, int cnt) { + + if (cnt <= 0) { + throw new IllegalArgumentException("Illegal glob pattern " + GlobPattern.this); + } + /* + NOTE: code has been copied (and slightly modified) from + ChildrenCollectorFilter#internalMatches. + TODO: move them to a common utility. + */ + + int pLength = pattern.length; + int sLength = s.length; + + while (true) { + // end of pattern reached: matches only if sOff points at the end + // of the string to match. + if (pOff >= pLength) { + return sOff >= sLength; + } + + // the end of the string to match has been reached but pattern + // doesn't have '*' at patternIndex -> no match + if (sOff >= sLength && pattern[pOff] != WILDCARD_CHAR) { + return false; + } + + // the next character of the pattern is '*' + // -> recursively test if the rest of the specified string matches + if (pattern[pOff] == WILDCARD_CHAR) { + if (++pOff >= pLength) { + return true; + } + + cnt--; + while (true) { + if (matches(pattern, pOff, s, sOff, cnt)) { + return true; + } + if (sOff >= sLength) { + return false; + } + sOff++; + } + } + + // not yet reached end of patter nor string and not wildcard character. + // the 2 strings don't match in case the characters at the current + // position are not the same. + if (pOff < pLength && sOff < sLength) { + if (pattern[pOff] != s[sOff]) { + return false; + } + } + pOff++; + sOff++; + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/NamedAccessControlPolicyImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/NamedAccessControlPolicyImpl.java new file mode 100644 index 00000000000..2915f1d2493 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/NamedAccessControlPolicyImpl.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.core.security.authorization; + +import javax.jcr.security.NamedAccessControlPolicy; + +import javax.jcr.RepositoryException; + +/** NamedAccessControlPolicyImpl... */ +public final class NamedAccessControlPolicyImpl implements NamedAccessControlPolicy { + + private final String jcrName; + + public NamedAccessControlPolicyImpl(String jcrName) { + if (jcrName == null) { + throw new IllegalArgumentException("The name of a named access control policy might not be null."); + } + this.jcrName = jcrName; + } + + //-------------------------------------------< NamedAccessControlPolicy >--- + /** + * @see NamedAccessControlPolicy#getName() + */ + public String getName() throws RepositoryException { + return jcrName; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/Permission.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/Permission.java new file mode 100644 index 00000000000..c9578ce7588 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/Permission.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.core.security.authorization; + +/** + * Permission... + */ +public final class Permission { + + public static final int NONE = 0; + + public static final int READ = 1; + + public static final int SET_PROPERTY = READ << 1; + + public static final int ADD_NODE = SET_PROPERTY << 1; + + public static final int REMOVE_NODE = ADD_NODE << 1; + + public static final int REMOVE_PROPERTY = REMOVE_NODE << 1; + + public static final int READ_AC = REMOVE_PROPERTY << 1; + + public static final int MODIFY_AC = READ_AC << 1; + + public static final int NODE_TYPE_MNGMT = MODIFY_AC << 1; + + public static final int VERSION_MNGMT = NODE_TYPE_MNGMT << 1; + + public static final int LOCK_MNGMT = VERSION_MNGMT << 1; + + public static final int LIFECYCLE_MNGMT = LOCK_MNGMT << 1; + + public static final int RETENTION_MNGMT = LIFECYCLE_MNGMT << 1; + + public static final int MODIFY_CHILD_NODE_COLLECTION = RETENTION_MNGMT << 1; + + public static final int NODE_TYPE_DEF_MNGMT = MODIFY_CHILD_NODE_COLLECTION << 1; + + public static final int NAMESPACE_MNGMT = NODE_TYPE_DEF_MNGMT << 1; + + public static final int WORKSPACE_MNGMT = NAMESPACE_MNGMT << 1; + + public static final int PRIVILEGE_MNGMT = WORKSPACE_MNGMT << 1; + + public static final int ALL = (READ | SET_PROPERTY | ADD_NODE | REMOVE_NODE + | REMOVE_PROPERTY | READ_AC | MODIFY_AC | NODE_TYPE_MNGMT + | VERSION_MNGMT | LOCK_MNGMT | LIFECYCLE_MNGMT | RETENTION_MNGMT + | MODIFY_CHILD_NODE_COLLECTION | NODE_TYPE_DEF_MNGMT | NAMESPACE_MNGMT + | WORKSPACE_MNGMT | PRIVILEGE_MNGMT); + + /** + * Returns those bits from permissions that are not present in + * the otherPermissions, i.e. subtracts the other permissions + * from permissions.
    + * If the specified otherPermissions do not intersect with + * permissions, permissions are returned.
    + * If permissions is included in otherPermissions, + * {@link #NONE} is returned. + * + * @param permissions + * @param otherPermissions + * @return the differences of the 2 permissions or {@link #NONE}. + */ + public static int diff(int permissions, int otherPermissions) { + return permissions & ~otherPermissions; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeBits.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeBits.java new file mode 100644 index 00000000000..8b2afe589fc --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeBits.java @@ -0,0 +1,611 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * PrivilegeBits + */ +public class PrivilegeBits { + + public static final PrivilegeBits EMPTY = new PrivilegeBits(UnmodifiableData.EMPTY); + + private static final long READ = 1; // PrivilegeRegistry.READ + + private static final Map BUILT_IN = new HashMap(); + static { + BUILT_IN.put(EMPTY.longValue(), EMPTY); + } + + private final Data d; + + /** + * Private constructor. + * + * @param d + */ + private PrivilegeBits(Data d) { + this.d = d; + } + + /** + * Package private method used by PrivilegeRegistry to handle + * built-in privileges and calculate internal permissions. + * + * @return long representation of this instance. + * @see PrivilegeRegistry#calculatePermissions(PrivilegeBits, PrivilegeBits, boolean, boolean) + */ + long longValue() { + return d.longValue(); + } + + /** + * Package private method used by PrivilegeRegistry to calculate + * the privilege bits associated with a given built-in or custom privilege + * definition. + * + * @return an instance of PrivilegeBits + */ + PrivilegeBits nextBits() { + if (this == EMPTY) { + return EMPTY; + } else { + PrivilegeBits pb = new PrivilegeBits(d.next()); + if (pb.d.isSimple()) { + BUILT_IN.put(pb.longValue(), pb); + } + return pb; + } + } + + /** + * Package private method used by PrivilegeRegistry to get or + * create an instance of privilege bits for the specified long value. + * + * @param bits + * @return an instance of PrivilegeBits + */ + static PrivilegeBits getInstance(long bits) { + if (bits == PrivilegeRegistry.NO_PRIVILEGE) { + return EMPTY; + } else if (bits < PrivilegeRegistry.NO_PRIVILEGE) { + throw new IllegalArgumentException(); + } else { + PrivilegeBits pb = BUILT_IN.get(bits); + if (pb == null) { + pb = new PrivilegeBits(new UnmodifiableData(bits)); + BUILT_IN.put(bits, pb); + } + return pb; + } + } + + /** + * Internal method to create a new instance of PrivilegeBits. + * + * @param bits + * @return an instance of PrivilegeBits + */ + private static PrivilegeBits getInstance(long[] bits) { + long[] bts = new long[bits.length]; + System.arraycopy(bits, 0, bts, 0, bits.length); + return new PrivilegeBits(new UnmodifiableData(bts)); + } + + /** + * Creates a mutable instance of privilege bits. + * + * @return a new instance of privilege bits. + */ + public static PrivilegeBits getInstance() { + return new PrivilegeBits(new ModifiableData()); + } + + /** + * Creates a mutable instance of privilege bits. + * + * @param base + * @return a new instance of privilege bits. + */ + public static PrivilegeBits getInstance(PrivilegeBits base) { + return new PrivilegeBits(new ModifiableData(base.d)); + } + + /** + * Returns true if this privilege bits includes no privileges + * at all. + * + * @return true if this privilege bits includes no privileges + * at all; false otherwise. + * @see PrivilegeRegistry#NO_PRIVILEGE + */ + public boolean isEmpty() { + return d.isEmpty(); + } + + /** + * Returns an unmodifiable instance. + * + * @return an unmodifiable PrivilegeBits instance. + */ + public PrivilegeBits unmodifiable() { + if (d instanceof ModifiableData) { + return (d.isSimple()) ? getInstance(d.longValue()) : getInstance(d.longValues()); + } else { + return this; + } + } + + /** + * Returns true if this privilege bits instance can be altered. + * + * @return true if this privilege bits instance can be altered. + */ + public boolean isModifiable() { + return (d instanceof ModifiableData); + } + + /** + * Returns true if this instance includes the jcr:read + * privilege. Shortcut for calling {@link PrivilegeBits#includes(PrivilegeBits)} + * where the other bits represented the jcr:read privilege. + * + * @return true if this instance includes the jcr:read + * privilege; false otherwise. + */ + public boolean includesRead() { + if (this == EMPTY) { + return false; + } else { + return d.includesRead(); + } + } + + /** + * Returns true if all privileges defined by the specified + * otherBits are present in this instance. + * + * @param otherBits + * @return true if all privileges defined by the specified + * otherBits are included in this instance; false + * otherwise. + */ + public boolean includes(PrivilegeBits otherBits) { + return d.includes(otherBits.d); + } + + /** + * Adds the other privilege bits to this instance. + * + * @param other The other privilege bits to be added. + * @throws UnsupportedOperationException if this instance is immutable. + */ + public void add(PrivilegeBits other) { + if (d instanceof ModifiableData) { + ((ModifiableData) d).add(other.d); + } else { + throw new UnsupportedOperationException("immutable privilege bits"); + } + } + + /** + * Subtracts the other PrivilegeBits from the this.
    + * If the specified bits do not intersect with this, it isn't modified.
    + * If this is included in other {@link #EMPTY empty} + * privilege bits is returned. + * + * @param other The other privilege bits to be substracted from this instance. + * @throws UnsupportedOperationException if this instance is immutable. + */ + public void diff(PrivilegeBits other) { + if (d instanceof ModifiableData) { + ((ModifiableData) d).diff(other.d); + } else { + throw new UnsupportedOperationException("immutable privilege bits"); + } + } + + /** + * Subtracts the b from a and adds the result (diff) + * to this instance. + * + * @param a An instance of privilege bits. + * @param b An instance of privilege bits. + * @throws UnsupportedOperationException if this instance is immutable. + */ + public void addDifference(PrivilegeBits a, PrivilegeBits b) { + if (d instanceof ModifiableData) { + ((ModifiableData) d).addDifference(a.d, b.d); + } else { + throw new UnsupportedOperationException("immutable privilege bits"); + } + } + + //-------------------------------------------------------------< Object >--- + @Override + public int hashCode() { + return d.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } else if (o instanceof PrivilegeBits) { + return d.equals(((PrivilegeBits) o).d); + } else { + return false; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("PrivilegeBits: "); + if (d.isSimple()) { + sb.append(d.longValue()); + } else { + sb.append(Arrays.toString(d.longValues())); + } + return sb.toString(); + } + + //------------------------------------------------------< inner classes >--- + /** + * Base class for the internal privilege bits representation and handling. + */ + private static abstract class Data { + + abstract boolean isEmpty(); + + abstract long longValue(); + + abstract long[] longValues(); + + abstract boolean isSimple(); + + abstract Data next(); + + abstract boolean includes(Data other); + + abstract boolean includesRead(); + + boolean equalData(Data d) { + if (isSimple() != d.isSimple()) { + return false; + } + if (isSimple()) { + return longValue() == d.longValue(); + } else { + return Arrays.equals(longValues(), d.longValues()); + } + } + + static boolean includes(long bits, long otherBits) { + return (bits | ~otherBits) == -1; + } + + static boolean includes(long[] bits, long[] otherBits) { + if (otherBits.length <= bits.length) { + // test for each long if is included + for (int i = 0; i < otherBits.length; i++) { + if ((bits[i] | ~otherBits[i]) != -1) { + return false; + } + } + return true; + } else { + // otherbits array is longer > cannot be included in bits + return false; + } + } + } + + /** + * Immutable Data object + */ + private static class UnmodifiableData extends Data { + + private static final long MAX = Long.MAX_VALUE / 2; + + private static final UnmodifiableData EMPTY = new UnmodifiableData(PrivilegeRegistry.NO_PRIVILEGE); + + private final long bits; + private final long[] bitsArr; + private final boolean isSimple; + private final boolean includesRead; + + private UnmodifiableData(long bits) { + this.bits = bits; + bitsArr = new long[] {bits}; + isSimple = true; + includesRead = (bits & READ) == READ; + } + + private UnmodifiableData(long[] bitsArr) { + bits = PrivilegeRegistry.NO_PRIVILEGE; + this.bitsArr = bitsArr; + isSimple = false; + includesRead = (bitsArr[0] & READ) == READ; + } + + @Override + boolean isEmpty() { + return this == EMPTY; + } + + @Override + long longValue() { + return bits; + } + + @Override + long[] longValues() { + return bitsArr; + } + + @Override + boolean isSimple() { + return isSimple; + } + + @Override + Data next() { + if (this == EMPTY) { + return EMPTY; + } else if (isSimple) { + if (bits < MAX) { + long b = bits << 1; + return new UnmodifiableData(b); + } else { + return new UnmodifiableData(new long[] {bits}).next(); + } + } else { + long[] bts; + long last = bitsArr[bitsArr.length-1]; + if (last < MAX) { + bts = new long[bitsArr.length]; + System.arraycopy(bitsArr, 0, bts, 0, bitsArr.length); + bts[bts.length-1] = last << 1; + } else { + bts = new long[bitsArr.length + 1]; + bts[bts.length-1] = 1; + } + return new UnmodifiableData(bts); + } + } + + @Override + boolean includes(Data other) { + if (isSimple) { + return (other.isSimple()) ? includes(bits, other.longValue()) : false; + } else { + return includes(bitsArr, other.longValues()); + } + } + + @Override + boolean includesRead() { + return includesRead; + } + + //---------------------------------------------------------< Object >--- + @Override + public int hashCode() { + return (isSimple) ? new Long(bits).hashCode() : Arrays.hashCode(bitsArr); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } else if (o instanceof UnmodifiableData) { + UnmodifiableData d = (UnmodifiableData) o; + if (isSimple != d.isSimple) { + return false; + } + if (isSimple) { + return bits == d.bits; + } else { + return Arrays.equals(bitsArr, d.bitsArr); + } + } else if (o instanceof ModifiableData) { + return equalData((Data) o); + } else { + return false; + } + } + } + + /** + * Mutable implementation of the Data base class. + */ + private static class ModifiableData extends Data { + + private long[] bits; + + private ModifiableData() { + bits = new long[] {PrivilegeRegistry.NO_PRIVILEGE}; + } + + private ModifiableData(Data base) { + long[] b = base.longValues(); + switch (b.length) { + case 0: + // empty + bits = new long[] {PrivilegeRegistry.NO_PRIVILEGE}; + break; + case 1: + // single long + bits = new long[] {b[0]}; + break; + default: + // copy + bits = new long[b.length]; + System.arraycopy(b, 0, bits, 0, b.length); + } + } + + @Override + boolean isEmpty() { + return bits.length == 1 && bits[0] == PrivilegeRegistry.NO_PRIVILEGE; + } + + @Override + long longValue() { + return (bits.length == 1) ? bits[0] : PrivilegeRegistry.NO_PRIVILEGE; + } + + @Override + long[] longValues() { + return bits; + } + + @Override + boolean isSimple() { + return bits.length == 1; + } + + @Override + Data next() { + throw new UnsupportedOperationException("Not implemented."); + } + + @Override + boolean includes(Data other) { + if (bits.length == 1) { + return other.isSimple() && includes(bits[0], other.longValue()); + } else { + return includes(bits, other.longValues()); + } + } + + @Override + boolean includesRead() { + return (bits[0] & READ) == READ; + } + + /** + * Add the other Data to this instance. + * + * @param other + */ + private void add(Data other) { + if (other != this) { + if (bits.length == 1 && other.isSimple()) { + bits[0] |= other.longValue(); + } else { + or(other.longValues()); + } + } + } + + /** + * Subtract the other Data from this instance. + * + * @param other + */ + private void diff(Data other) { + if (bits.length == 1 && other.isSimple()) { + bits[0] = bits[0] & ~other.longValue(); + } else { + bits = diff(bits, other.longValues()); + } + } + + /** + * Add the diff between the specified Data a and b. + * + * @param a + * @param b + */ + private void addDifference(Data a, Data b) { + if (a.isSimple() && b.isSimple()) { + bits[0] |= a.longValue() & ~b.longValue(); + } else { + long[] diff = diff(a.longValues(), b.longValues()); + or(diff); + } + } + + private void or(long[] b) { + if (b.length > bits.length) { + // enlarge the array + long[] res = new long[b.length]; + System.arraycopy(bits, 0, res, 0, bits.length); + bits = res; + } + for (int i = 0; i < b.length; i++) { + bits[i] |= b[i]; + } + } + + private static long[] diff(long[] a, long[] b) { + int index = -1; + long[] res = new long[((a.length > b.length) ? a.length : b.length)]; + for (int i = 0; i < res.length; i++) { + if (i < a.length && i < b.length) { + res[i] = a[i] & ~b[i]; + } else { + res[i] = (i < a.length) ? a[i] : 0; + } + // remember start of trailing 0 array entries + if (res[i] != 0) { + index = -1; + } else if (index == -1) { + index = i; + } + } + switch (index) { + case -1: + // no need to remove trailing 0-long from the array + return res; + case 0 : + // array consisting of one or multiple 0 + return new long[] {PrivilegeRegistry.NO_PRIVILEGE}; + default: + // remove trailing 0-long entries from the array + long[] r2 = new long[index]; + System.arraycopy(res, 0, r2, 0, index); + return r2; + } + } + + //---------------------------------------------------------< Object >--- + @Override + public int hashCode() { + // NOTE: mutable object. hashCode not implemented. + return 0; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } else if (o instanceof ModifiableData) { + ModifiableData d = (ModifiableData) o; + return Arrays.equals(bits, d.bits); + } else if (o instanceof UnmodifiableData) { + return equalData((Data) o); + } else { + return false; + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeManagerImpl.java new file mode 100644 index 00000000000..9004ec6f6f0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeManagerImpl.java @@ -0,0 +1,383 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * PrivilegeManager... + */ +public final class PrivilegeManagerImpl implements PrivilegeManager, PrivilegeRegistry.Listener { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(PrivilegeManagerImpl.class); + + private static final Privilege[] EMPTY_ARRAY = new Privilege[0]; + + /** + * The privilege registry + */ + private final PrivilegeRegistry registry; + + /** + * The name resolver used to determine the correct privilege + * {@link javax.jcr.security.Privilege#getName() name} depending on the sessions namespace + * mappings. + */ + private final NameResolver resolver; + + /** + * Per instance map containing the namespace aware representation of + * the registered privileges. + */ + private final Map cache; + + public PrivilegeManagerImpl(PrivilegeRegistry registry, NameResolver nameResolver) { + this.registry = registry; + this.resolver = nameResolver; + this.cache = new HashMap(); + + // listen to privilege registration (due to weak references no explicit + // stop-listening call required + registry.addListener(this); + } + + /** + * Disposes this privilege manager + */ + public void dispose() { + registry.removeListener(this); + } + + //---------------------------------------------------< PrivilegeManager >--- + /** + * @see PrivilegeManager#getRegisteredPrivileges() + */ + public Privilege[] getRegisteredPrivileges() throws RepositoryException { + PrivilegeDefinition[] allDefs = registry.getAll(); + if (allDefs.length != cache.size()) { + synchronized (cache) { + for (PrivilegeDefinition def : allDefs) { + if (!cache.containsKey(def.getName())) { + cache.put(def.getName(), new PrivilegeImpl(def)); + } + } + } + } + return cache.values().toArray(new Privilege[allDefs.length]); + } + + /** + * @see PrivilegeManager#getPrivilege(String) + */ + public Privilege getPrivilege(String privilegeName) throws AccessControlException, RepositoryException { + Name name = resolver.getQName(privilegeName); + return getPrivilege(name); + } + + /** + * Register a new custom privilege with the specified characteristics. + *

    + * The current implementation has the following limitations and constraints: + * + *

      + *
    • the name may not be in use by another privilege
    • + *
    • the namespace URI must be a valid, registered namespace excluding + * those namespaces marked as being reserved
    • + *
    • an aggregate custom privilege is valid if all declared aggregate + * names can be resolved to registered privileges and if there exists + * no registered privilege with the same aggregated privileges.
    • + *
    + *

    + * Please note
    + * Custom privilege(s) will not be enforced for any kind of repository + * operations. Those are exclusively covered by the built-in privileges. + * This also implies that the {@link Permission}s are not affected by + * custom privileges. + *

    + * Applications making use of the custom privilege(s) are in charge of + * asserting whether the privileges are granted/denied according to their + * application specific needs. + * + * @param privilegeName The name of the new custom privilege. + * @param isAbstract Boolean flag indicating if the privilege is abstract. + * @param declaredAggregateNames An array of privilege names referring to + * registered privileges being aggregated by this new custom privilege. + * In case of a non aggregate privilege an empty array should be passed. + * @return the new privilege. + * @throws AccessDeniedException If the session this manager has been created + * lacks rep:privilegeManagement privilege. + * @throws RepositoryException If the privilege could not be registered due + * to constraint violations or if persisting the custom privilege fails. + * @see PrivilegeManager#registerPrivilege(String, boolean, String[]) + */ + public Privilege registerPrivilege(String privilegeName, boolean isAbstract, + String[] declaredAggregateNames) + throws AccessDeniedException, RepositoryException { + if (resolver instanceof SessionImpl) { + SessionImpl sImpl = (SessionImpl) resolver; + sImpl.getAccessManager().checkRepositoryPermission(Permission.PRIVILEGE_MNGMT); + } else { + // cannot evaluate + throw new AccessDeniedException("Registering privileges is not allowed for the editing session."); + } + + Name name = resolver.getQName(privilegeName); + Set daNames; + if (declaredAggregateNames == null || declaredAggregateNames.length == 0) { + daNames = Collections.emptySet(); + } else { + daNames = new HashSet(declaredAggregateNames.length); + for (String declaredAggregateName : declaredAggregateNames) { + daNames.add(resolver.getQName(declaredAggregateName)); + } + } + registry.registerDefinition(name, isAbstract, daNames); + + return getPrivilege(privilegeName); + } + + //-----------------------------< implementation specific public methods >--- + /** + * @param privileges An array of privileges. + * @return The bits of the privileges contained in the specified + * array. + * @throws AccessControlException If the specified array is null, empty + * or if it contains an unregistered privilege. + */ + public PrivilegeBits getBits(Privilege... privileges) throws AccessControlException { + if (privileges == null || privileges.length == 0) { + throw new AccessControlException("Privilege array is empty or null."); + } + + PrivilegeDefinition[] defs = new PrivilegeDefinition[privileges.length]; + for (int i = 0; i < privileges.length; i++) { + Privilege p = privileges[i]; + if (p instanceof PrivilegeImpl) { + defs[i] = ((PrivilegeImpl) p).definition; + } else { + String name = (p == null) ? "null" : p.getName(); + throw new AccessControlException("Unknown privilege '" + name + "'."); + } + } + return registry.getBits(defs); + } + + /** + * @param privilegeNames An array of privilege names. + * @return The bits of the privileges contained in the specified + * array. + * @throws AccessControlException If the specified array is null or if it + * contains the name of an unregistered privilege. + */ + public PrivilegeBits getBits(Name... privilegeNames) throws RepositoryException { + if (privilegeNames == null) { + throw new AccessControlException("Privilege name array is null."); + } + return registry.getBits(privilegeNames); + } + + /** + * Returns an array of registered Privileges. If the specified + * bits represent a single registered privilege the returned array + * contains a single element. Otherwise the returned array contains the + * individual registered privileges that are combined in the given + * bits. If bits does not match to any registered + * privilege an empty array will be returned. + * + * @param bits Privilege bits as obtained from {@link #getBits(Privilege...)}. + * @return Array of Privileges that are presented by the given + * bits or an empty array if bits cannot be + * resolved to registered Privileges. + * @see #getBits(Privilege...) + */ + public Set getPrivileges(PrivilegeBits bits) { + Name[] names = registry.getNames(bits); + if (names.length == 0) { + return Collections.emptySet(); + } else { + Set privs = new HashSet(names.length); + for (Name n : names) { + try { + privs.add(getPrivilege(n)); + } catch (RepositoryException e) { + log.error("Internal error: invalid privilege name " + n.toString()); + } + } + return privs; + } + } + + //------------------------------------------------------------< private >--- + /** + * @param name + * @return The privilege with the specified name. + * @throws AccessControlException + * @throws RepositoryException + */ + private Privilege getPrivilege(Name name) throws AccessControlException, RepositoryException { + Privilege privilege; + synchronized (cache) { + if (cache.containsKey(name)) { + privilege = cache.get(name); + } else { + PrivilegeDefinition def = registry.get(name); + if (def != null) { + privilege = new PrivilegeImpl(def); + cache.put(name, privilege); + } else { + throw new AccessControlException("Unknown privilege " + resolver.getJCRName(name)); + } + } + } + return privilege; + } + + //-----------------------------------------< PrivilegeRegistry.Listener >--- + /** + * @see PrivilegeRegistry.Listener#privilegesRegistered(java.util.Set) + * @param privilegeNames + */ + public void privilegesRegistered(Set privilegeNames) { + // force recalculation of jcr:all privilege + synchronized (cache) { + cache.remove(NameConstants.JCR_ALL); + } + } + + //----------------------------------------------------------< Privilege >--- + /** + * Simple wrapper used to provide an public representation of the + * registered internal privileges properly exposing the JCR name. + */ + private class PrivilegeImpl implements Privilege { + + private final PrivilegeDefinition definition; + + private final Privilege[] declaredAggregates; + private final Privilege[] aggregates; + + private PrivilegeImpl(PrivilegeDefinition definition) throws RepositoryException { + this.definition = definition; + + Set set = definition.getDeclaredAggregateNames(); + Name[] declAggrNames = set.toArray(new Name[set.size()]); + if (declAggrNames.length == 0) { + declaredAggregates = EMPTY_ARRAY; + aggregates = EMPTY_ARRAY; + } else { + declaredAggregates = new Privilege[declAggrNames.length]; + for (int i = 0; i < declAggrNames.length; i++) { + declaredAggregates[i] = getPrivilege(declAggrNames[i]); + } + + Set aggr = new HashSet(); + for (Privilege decl : declaredAggregates) { + aggr.add(decl); + if (decl.isAggregate()) { + aggr.addAll(Arrays.asList(decl.getAggregatePrivileges())); + } + } + aggregates = aggr.toArray(new Privilege[aggr.size()]); + } + } + + /** + * @see Privilege#getName() + */ + public String getName() { + try { + return resolver.getJCRName(definition.getName()); + } catch (NamespaceException e) { + // should not occur -> return internal name representation. + return definition.getName().toString(); + } + } + + /** + * @see Privilege#isAbstract() + */ + public boolean isAbstract() { + return definition.isAbstract(); + } + + /** + * @see Privilege#isAggregate() + */ + public boolean isAggregate() { + return declaredAggregates.length > 0; + } + + /** + * @see Privilege#getDeclaredAggregatePrivileges() + */ + public Privilege[] getDeclaredAggregatePrivileges() { + return declaredAggregates; + } + + /** + * @see Privilege#getAggregatePrivileges() + */ + public Privilege[] getAggregatePrivileges() { + return aggregates; + } + + //---------------------------------------------------------< Object >--- + @Override + public String toString() { + return getName(); + } + + @Override + public int hashCode() { + return definition.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof PrivilegeImpl) { + PrivilegeImpl other = (PrivilegeImpl) obj; + return definition.equals(other.definition); + } + return false; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeRegistry.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeRegistry.java new file mode 100644 index 00000000000..2ab514f0ed7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/PrivilegeRegistry.java @@ -0,0 +1,1136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.core.cluster.PrivilegeEventChannel; +import org.apache.jackrabbit.core.cluster.PrivilegeEventListener; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.commons.privilege.ParseException; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionReader; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionWriter; +import org.apache.jackrabbit.core.NamespaceRegistryImpl; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The PrivilegeRegistry defines the set of Privileges + * known to the repository. + */ +public final class PrivilegeRegistry implements PrivilegeEventListener { + + private static final Logger log = LoggerFactory.getLogger(PrivilegeRegistry.class); + + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + + /** + * Jackrabbit specific write privilege that combines {@link Privilege#JCR_WRITE} + * and {@link Privilege#JCR_NODE_TYPE_MANAGEMENT}. + */ + public static final String REP_WRITE = "{" + Name.NS_REP_URI + "}write"; + public static final Name REP_WRITE_NAME = NAME_FACTORY.create(REP_WRITE); + + /** + * Jackrabbit specific privilege for privilege management. + */ + public static final String REP_PRIVILEGE_MANAGEMENT = "{" + Name.NS_REP_URI + "}privilegeManagement"; + public static final Name REP_PRIVILEGE_MANAGEMENT_NAME = NAME_FACTORY.create(REP_PRIVILEGE_MANAGEMENT); + + /** + * No privileges + */ + public static final int NO_PRIVILEGE = 0; + + private static final int READ = 1; + private static final int MODIFY_PROPERTIES = READ << 1; + private static final int ADD_CHILD_NODES = MODIFY_PROPERTIES << 1; + private static final int REMOVE_CHILD_NODES = ADD_CHILD_NODES << 1; + private static final int REMOVE_NODE = REMOVE_CHILD_NODES << 1; + private static final int READ_AC = REMOVE_NODE << 1; + private static final int MODIFY_AC = READ_AC << 1; + private static final int NODE_TYPE_MNGMT = MODIFY_AC << 1; + private static final int VERSION_MNGMT = NODE_TYPE_MNGMT << 1; + private static final int LOCK_MNGMT = VERSION_MNGMT << 1; + private static final int LIFECYCLE_MNGMT = LOCK_MNGMT << 1; + private static final int RETENTION_MNGMT = LIFECYCLE_MNGMT << 1; + private static final int WORKSPACE_MNGMT = RETENTION_MNGMT << 1; + private static final int NODE_TYPE_DEF_MNGMT = WORKSPACE_MNGMT << 1; + private static final int NAMESPACE_MNGMT = NODE_TYPE_DEF_MNGMT << 1; + private static final int PRIVILEGE_MNGMT = NAMESPACE_MNGMT << 1; + + private static final Map PRIVILEGE_NAMES = new HashMap(); + static { + PRIVILEGE_NAMES.put(NameConstants.JCR_READ, READ); + PRIVILEGE_NAMES.put(NameConstants.JCR_MODIFY_PROPERTIES, MODIFY_PROPERTIES); + PRIVILEGE_NAMES.put(NameConstants.JCR_ADD_CHILD_NODES, ADD_CHILD_NODES); + PRIVILEGE_NAMES.put(NameConstants.JCR_REMOVE_CHILD_NODES, REMOVE_CHILD_NODES); + PRIVILEGE_NAMES.put(NameConstants.JCR_REMOVE_NODE, REMOVE_NODE); + PRIVILEGE_NAMES.put(NameConstants.JCR_READ_ACCESS_CONTROL, READ_AC); + PRIVILEGE_NAMES.put(NameConstants.JCR_MODIFY_ACCESS_CONTROL, MODIFY_AC); + PRIVILEGE_NAMES.put(NameConstants.JCR_NODE_TYPE_MANAGEMENT, NODE_TYPE_MNGMT); + PRIVILEGE_NAMES.put(NameConstants.JCR_VERSION_MANAGEMENT, VERSION_MNGMT); + PRIVILEGE_NAMES.put(NameConstants.JCR_LOCK_MANAGEMENT, LOCK_MNGMT); + PRIVILEGE_NAMES.put(NameConstants.JCR_LIFECYCLE_MANAGEMENT, LIFECYCLE_MNGMT); + PRIVILEGE_NAMES.put(NameConstants.JCR_RETENTION_MANAGEMENT, RETENTION_MNGMT); + PRIVILEGE_NAMES.put(NameConstants.JCR_WORKSPACE_MANAGEMENT, WORKSPACE_MNGMT); + PRIVILEGE_NAMES.put(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT, NODE_TYPE_DEF_MNGMT); + PRIVILEGE_NAMES.put(NameConstants.JCR_NAMESPACE_MANAGEMENT, NAMESPACE_MNGMT); + PRIVILEGE_NAMES.put(REP_PRIVILEGE_MANAGEMENT_NAME, PRIVILEGE_MNGMT); + } + + /** + * Path to the file system resource used to persist custom privileges + * registered with this repository. + */ + private static final String CUSTOM_PRIVILEGES_RESOURCE_NAME = "/privileges/custom_privileges.xml"; + + + private final Map registeredPrivileges = new HashMap(); + private final Map> bitsToNames = new HashMap>(); + + @SuppressWarnings("unchecked") + private final Map listeners = Collections.synchronizedMap(new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK)); + + private final NamespaceRegistry namespaceRegistry; + private final CustomPrivilegeStore customPrivilegesStore; + + private final NameResolver resolver; + + private PrivilegeBits nextBits = PrivilegeBits.getInstance(PRIVILEGE_MNGMT).nextBits(); + + /** + * Privilege event channel for clustering support. + */ + private PrivilegeEventChannel eventChannel; + + /** + * Create a new PrivilegeRegistry instance. + * + * @param namespaceRegistry + * @param fs + * @throws RepositoryException + */ + public PrivilegeRegistry(NamespaceRegistry namespaceRegistry, FileSystem fs) + throws RepositoryException { + + this.namespaceRegistry = namespaceRegistry; + this.customPrivilegesStore = new CustomPrivilegeStore(new FileSystemResource(fs, CUSTOM_PRIVILEGES_RESOURCE_NAME)); + cacheDefinitions(createBuiltInPrivilegeDefinitions()); + + try { + Map customDefs = customPrivilegesStore.load(); + Map definitions = createCustomDefinitions(customDefs); + cacheDefinitions(definitions); + } catch (IOException e) { + throw new RepositoryException("Failed to load custom privileges", e); + } catch (FileSystemException e) { + throw new RepositoryException("Failed to load custom privileges", e); + } catch (ParseException e) { + throw new RepositoryException("Failed to load custom privileges", e); + } + + this.resolver = new DefaultNamePathResolver(namespaceRegistry); + } + + /** + * Create a new PrivilegeRegistry instance defining only + * built-in privileges. + * + * @param resolver + * @deprecated Use {@link org.apache.jackrabbit.api.security.authorization.PrivilegeManager} instead. + * @see org.apache.jackrabbit.api.JackrabbitWorkspace#getPrivilegeManager() + */ + public PrivilegeRegistry(NameResolver resolver) { + cacheDefinitions(createBuiltInPrivilegeDefinitions()); + + namespaceRegistry = null; + customPrivilegesStore = null; + + this.resolver = resolver; + } + + + //---------------------------------------------< PrivilegeEventListener >--- + /** + * {@inheritDoc} + * @see PrivilegeEventListener#externalRegisteredPrivileges(java.util.Collection) + */ + public void externalRegisteredPrivileges(Collection definitions) throws RepositoryException { + Map defs = new HashMap(definitions.size()); + for (PrivilegeDefinition d : definitions) { + defs.put(d.getName(), d); + } + registerCustomDefinitions(defs); + } + + //----------------------------------------< public methods : clustering >--- + + /** + * Set a clustering event channel to inform about changes. + * + * @param eventChannel event channel + */ + public void setEventChannel(PrivilegeEventChannel eventChannel) { + this.eventChannel = eventChannel; + eventChannel.setListener(this); + } + + //--------------------------------< public methods : privilege registry >--- + /** + * Throws UnsupportedOperationException. + * + * @return all registered privileges. + * @deprecated Use {@link org.apache.jackrabbit.api.security.authorization.PrivilegeManager#getRegisteredPrivileges()} instead. + */ + public Privilege[] getRegisteredPrivileges() { + try { + return new PrivilegeManagerImpl(this, resolver).getRegisteredPrivileges(); + } catch (RepositoryException e) { + throw new UnsupportedOperationException("No supported any more. Use PrivilegeManager#getRegisteredPrivileges() instead."); + } + } + + /** + * Creates a new PrivilegeManager from the specified resolver + * and calls {@link PrivilegeManagerImpl#getRegisteredPrivileges()}. + * + * @param privilegeName Name of the privilege. + * @return the privilege with the specified privilegeName. + * @throws AccessControlException If no privilege with the given name exists. + * @throws RepositoryException If another error occurs. + * @deprecated Use {@link org.apache.jackrabbit.api.security.authorization.PrivilegeManager#getPrivilege(String)} instead. + */ + public Privilege getPrivilege(String privilegeName) throws AccessControlException, RepositoryException { + return new PrivilegeManagerImpl(this, resolver).getPrivilege(privilegeName); + } + + /** + * Creates a new PrivilegeManager from the specified resolver + * and calls {@link PrivilegeManagerImpl#getPrivileges(PrivilegeBits)}. + * + * @param bits Privilege bits as obtained from {@link #getBits(Privilege[])}. + * @return Array of Privileges that are presented by the given it + * or an empty array if bits is lower than {@link #READ} or + * cannot be resolved to registered Privileges. + * @see #getBits(Privilege[]) + * @deprecated Use {@link PrivilegeManagerImpl#getPrivileges(PrivilegeBits)} instead. + */ + public Privilege[] getPrivileges(int bits) { + Set prvs = new PrivilegeManagerImpl(this, resolver).getPrivileges(PrivilegeBits.getInstance(bits)); + return prvs.toArray(new Privilege[prvs.size()]); + } + + /** + * Best effort approach to calculate bits for built-in privileges. Throws + * UnsupportedOperationException if the workaround fails. + * + * @param privileges An array of privileges. + * @return The privilege bits. + * @throws AccessControlException If the specified array is null + * or if it contains an unregistered privilege. + * @see #getPrivileges(int) + * @deprecated Use {@link PrivilegeManagerImpl#getBits(javax.jcr.security.Privilege...)} instead. + */ + public static int getBits(Privilege[] privileges) throws AccessControlException { + if (privileges == null || privileges.length == 0) { + throw new AccessControlException("Privilege array is empty or null."); + } + + Map lookup = new HashMap(2); + lookup.put(Name.NS_REP_PREFIX, Name.NS_REP_URI); + lookup.put(Name.NS_JCR_PREFIX, Name.NS_JCR_URI); + + int bits = NO_PRIVILEGE; + for (Privilege priv : privileges) { + String prefix = Text.getNamespacePrefix(priv.getName()); + if (lookup.containsKey(prefix)) { + Name n = NAME_FACTORY.create(lookup.get(prefix), Text.getLocalName(priv.getName())); + if (PRIVILEGE_NAMES.containsKey(n)) { + bits |= PRIVILEGE_NAMES.get(n); + } else if (NameConstants.JCR_WRITE.equals(n)) { + bits |= createJcrWriteDefinition().bits.longValue(); + } else if (REP_WRITE_NAME.equals(n)) { + Definition jcrWrite = createJcrWriteDefinition(); + bits |= createRepWriteDefinition(jcrWrite).bits.longValue(); + } else if (NameConstants.JCR_ALL.equals(n)) { + for (Name pn : PRIVILEGE_NAMES.keySet()) { + bits |= PRIVILEGE_NAMES.get(pn); + } + } else { + throw new AccessControlException("Unknown privilege '" + priv.getName() + "'."); + } + } else { + throw new AccessControlException("Unknown privilege '" + priv.getName() + "'."); + } + } + return bits; + } + + /** + * Build the permissions granted by evaluating the given privileges. Note, + * that only built-in privileges can be mapped to permissions. Any other + * privileges will be ignored. + * + * @param privs The privileges granted on the Node itself (for properties + * the ACL of the direct ancestor). + * @param parentPrivs The privileges granted on the parent of the Node. Not + * relevant for properties since it only is used to determine permissions + * on a Node (add_child_nodes, remove_child_nodes). + * @param isAllow true if the privileges are granted; false + * otherwise. + * @param protectsPolicy If true the affected item itself + * defines access control related information. + * @return the permissions granted evaluating the given privileges. + */ + public static int calculatePermissions(PrivilegeBits privs, PrivilegeBits parentPrivs, boolean isAllow, boolean protectsPolicy) { + return calculatePermissions(privs.longValue(), parentPrivs.longValue(), isAllow, protectsPolicy); + } + + /** + * Build the permissions granted by evaluating the given privileges. Note, + * that only built-in privileges can be mapped to permissions. Any other + * privileges will be ignored. + * + * @param privs The privileges granted on the Node itself (for properties + * the ACL of the direct ancestor). + * @param parentPrivs The privileges granted on the parent of the Node. Not + * relevant for properties since it only is used to determine permissions + * on a Node (add_child_nodes, remove_child_nodes). + * @param isAllow true if the privileges are granted; false + * otherwise. + * @param protectsPolicy If true the affected item itself + * defines access control related information. + * @return the permissions granted evaluating the given privileges. + * @deprecated Use {@link #calculatePermissions(PrivilegeBits, PrivilegeBits, boolean, boolean)} instead. + */ + public static int calculatePermissions(int privs, int parentPrivs, boolean isAllow, boolean protectsPolicy) { + return calculatePermissions((long) privs, (long) parentPrivs, isAllow, protectsPolicy); + } + + private static int calculatePermissions(long privs, long parentPrivs, boolean isAllow, boolean protectsPolicy) { + int perm = Permission.NONE; + if (protectsPolicy) { + if ((parentPrivs & READ_AC) == READ_AC) { + perm |= Permission.READ; + } + if ((parentPrivs & MODIFY_AC) == MODIFY_AC) { + perm |= Permission.ADD_NODE; + perm |= Permission.SET_PROPERTY; + perm |= Permission.REMOVE_NODE; + perm |= Permission.REMOVE_PROPERTY; + perm |= Permission.NODE_TYPE_MNGMT; + } + } else { + if ((privs & READ) == READ) { + perm |= Permission.READ; + } + if ((privs & MODIFY_PROPERTIES) == MODIFY_PROPERTIES) { + perm |= Permission.SET_PROPERTY; + perm |= Permission.REMOVE_PROPERTY; + } + // add_node permission is granted through privilege on the parent. + if ((parentPrivs & ADD_CHILD_NODES) == ADD_CHILD_NODES) { + perm |= Permission.ADD_NODE; + } + + /* + remove_node is + allowed: only if remove_child_nodes privilege is present on + the parent AND remove_node is present on the node itself + denied : if either remove_child_nodes is denied on the parent + OR remove_node is denied on the node itself. + */ + if (isAllow) { + if ((parentPrivs & REMOVE_CHILD_NODES) == REMOVE_CHILD_NODES && + (privs & REMOVE_NODE) == REMOVE_NODE) { + perm |= Permission.REMOVE_NODE; + } + } else { + if ((parentPrivs & REMOVE_CHILD_NODES) == REMOVE_CHILD_NODES || + (privs & REMOVE_NODE) == REMOVE_NODE) { + perm |= Permission.REMOVE_NODE; + } + } + } + + // modify_child_node_collection permission is granted through + // privileges on the parent + if ((parentPrivs & ADD_CHILD_NODES) == ADD_CHILD_NODES && + (parentPrivs & REMOVE_CHILD_NODES) == REMOVE_CHILD_NODES) { + perm |= Permission.MODIFY_CHILD_NODE_COLLECTION; + } + + // the remaining (special) permissions are simply defined on the node + if ((privs & READ_AC) == READ_AC) { + perm |= Permission.READ_AC; + } + if ((privs & MODIFY_AC) == MODIFY_AC) { + perm |= Permission.MODIFY_AC; + } + if ((privs & LIFECYCLE_MNGMT) == LIFECYCLE_MNGMT) { + perm |= Permission.LIFECYCLE_MNGMT; + } + if ((privs & LOCK_MNGMT) == LOCK_MNGMT) { + perm |= Permission.LOCK_MNGMT; + } + if ((privs & NODE_TYPE_MNGMT) == NODE_TYPE_MNGMT) { + perm |= Permission.NODE_TYPE_MNGMT; + } + if ((privs & RETENTION_MNGMT) == RETENTION_MNGMT) { + perm |= Permission.RETENTION_MNGMT; + } + if ((privs & VERSION_MNGMT) == VERSION_MNGMT) { + perm |= Permission.VERSION_MNGMT; + } + if ((privs & WORKSPACE_MNGMT) == WORKSPACE_MNGMT) { + perm |= Permission.WORKSPACE_MNGMT; + } + if ((privs & NODE_TYPE_DEF_MNGMT) == NODE_TYPE_DEF_MNGMT) { + perm |= Permission.NODE_TYPE_DEF_MNGMT; + } + if ((privs & NAMESPACE_MNGMT) == NAMESPACE_MNGMT) { + perm |= Permission.NAMESPACE_MNGMT; + } + if ((privs & PRIVILEGE_MNGMT) == PRIVILEGE_MNGMT) { + perm |= Permission.PRIVILEGE_MNGMT; + } + return perm; + } + + //-----------------------------------< methods used by PrivilegeManager >--- + /** + * Validates and registers a new custom privilege definition with the + * specified characteristics. Upon successful registration the new custom + * definition is persisted in the corresponding file system resource.

    + * + *

    The validation includes the following steps:

    + * + *
      + *
    • assert uniqueness of the specified privilegeName
    • + *
    • make sure the name doesn't use a reserved namespace
    • + *
    • assert that all names referenced in the specified name set refer + * to existing privilege definitions.
    • + *
    + * + * @param privilegeName + * @param isAbstract + * @param declaredAggregateNames + * @throws RepositoryException If the privilege could not be registered due + * to constraint violations or if persisting the custom privilege fails. + */ + void registerDefinition(Name privilegeName, boolean isAbstract, Set declaredAggregateNames) throws RepositoryException { + PrivilegeDefinition def = new PrivilegeDefinitionImpl(privilegeName, isAbstract, declaredAggregateNames); + Map stubs = Collections.singletonMap(privilegeName, def); + registerCustomDefinitions(stubs); + + // inform clustering about the new privilege. + if (eventChannel != null) { + eventChannel.registeredPrivileges(stubs.values()); + } + } + + /** + * Returns all registered internal privileges. + * + * @return all registered internal privileges + */ + PrivilegeDefinition[] getAll() { + return registeredPrivileges.values().toArray(new Definition[registeredPrivileges.size()]); + } + + /** + * Returns the internal privilege with the specified name or null. + * + * @param name Name of the internal privilege. + * @return the internal privilege with the specified name or null + */ + PrivilegeDefinition get(Name name) { + return registeredPrivileges.get(name); + } + + /** + * Returns the names of the privileges identified by the specified bits. + * Note, that custom privileges don't have a integer representation as they + * are not used for permission calculation. + * + * @param privilegeBits The privilege bits. + * @return Privilege names that corresponds to the given bits. + */ + Name[] getNames(PrivilegeBits privilegeBits) { + if (privilegeBits == null || privilegeBits.isEmpty()) { + return Name.EMPTY_ARRAY; + } else if (bitsToNames.containsKey(privilegeBits)) { + // matches all built-in aggregates and single built-in privileges + Set ips = bitsToNames.get(privilegeBits); + return ips.toArray(new Name[ips.size()]); + } else { + // bits are a combination of built-in privileges. + Set names = new HashSet(); + long bits = privilegeBits.longValue(); + if ((bits & READ) == READ) { + names.add(NameConstants.JCR_READ); + } + long repWrite = registeredPrivileges.get(REP_WRITE_NAME).bits.longValue(); + long jcrWrite = registeredPrivileges.get(NameConstants.JCR_WRITE).bits.longValue(); + if ((bits & repWrite) == repWrite) { + names.add(REP_WRITE_NAME); + } else if ((bits & jcrWrite) == jcrWrite) { + names.add(NameConstants.JCR_WRITE); + } else { + if ((bits & MODIFY_PROPERTIES) == MODIFY_PROPERTIES) { + names.add(NameConstants.JCR_MODIFY_PROPERTIES); + } + if ((bits & ADD_CHILD_NODES) == ADD_CHILD_NODES) { + names.add(NameConstants.JCR_ADD_CHILD_NODES); + } + if ((bits & REMOVE_CHILD_NODES) == REMOVE_CHILD_NODES) { + names.add(NameConstants.JCR_REMOVE_CHILD_NODES); + } + if ((bits & REMOVE_NODE) == REMOVE_NODE) { + names.add(NameConstants.JCR_REMOVE_NODE); + } + if ((bits & NODE_TYPE_MNGMT) == NODE_TYPE_MNGMT) { + names.add(NameConstants.JCR_NODE_TYPE_MANAGEMENT); + } + } + if ((bits & READ_AC) == READ_AC) { + names.add(NameConstants.JCR_READ_ACCESS_CONTROL); + } + if ((bits & MODIFY_AC) == MODIFY_AC) { + names.add(NameConstants.JCR_MODIFY_ACCESS_CONTROL); + } + if ((bits & VERSION_MNGMT) == VERSION_MNGMT) { + names.add(NameConstants.JCR_VERSION_MANAGEMENT); + } + if ((bits & LOCK_MNGMT) == LOCK_MNGMT) { + names.add(NameConstants.JCR_LOCK_MANAGEMENT); + } + if ((bits & LIFECYCLE_MNGMT) == LIFECYCLE_MNGMT) { + names.add(NameConstants.JCR_LIFECYCLE_MANAGEMENT); + } + if ((bits & RETENTION_MNGMT) == RETENTION_MNGMT) { + names.add(NameConstants.JCR_RETENTION_MANAGEMENT); + } + if ((bits & WORKSPACE_MNGMT) == WORKSPACE_MNGMT) { + names.add(NameConstants.JCR_WORKSPACE_MANAGEMENT); + } + if ((bits & NODE_TYPE_DEF_MNGMT) == NODE_TYPE_DEF_MNGMT) { + names.add(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT); + } + if ((bits & NAMESPACE_MNGMT) == NAMESPACE_MNGMT) { + names.add(NameConstants.JCR_NAMESPACE_MANAGEMENT); + } + if ((bits & PRIVILEGE_MNGMT) == PRIVILEGE_MNGMT) { + names.add(REP_PRIVILEGE_MANAGEMENT_NAME); + } + + // include matching custom privilege names + Set customNames = new HashSet(); + Set aggr = new HashSet(); + for (Definition def : registeredPrivileges.values()) { + if (def.isCustom && privilegeBits.includes(def.bits)) { + customNames.add(def.getName()); + if (!def.getDeclaredAggregateNames().isEmpty()) { + aggr.add(def); + } + } + } + // avoid redundant entries in case of aggregate privileges. + for (Definition aggregate : aggr) { + customNames.removeAll(aggregate.getDeclaredAggregateNames()); + } + names.addAll(customNames); + + // remember this resolution. + if (!names.isEmpty()) { + bitsToNames.put(privilegeBits, names); + } + return names.toArray(new Name[names.size()]); + } + } + + /** + * Return the privilege bits for the specified privilege definitions. + * + * @param definitions + * @return privilege bits. + */ + PrivilegeBits getBits(PrivilegeDefinition... definitions) { + switch (definitions.length) { + case 0: + return PrivilegeBits.EMPTY; + + case 1: + if (definitions[0] instanceof Definition) { + return ((Definition) definitions[0]).bits; + } else { + return PrivilegeBits.EMPTY; + } + + default: + PrivilegeBits bts = PrivilegeBits.getInstance(); + for (PrivilegeDefinition d : definitions) { + if (d instanceof Definition) { + bts.add(((Definition) d).bits); + } + } + return bts; + } + } + + /** + * Return the privilege bits for the specified privilege names. + * + * @param privilegeNames + * @return privilege bits. + */ + PrivilegeBits getBits(Name... privilegeNames) { + switch (privilegeNames.length) { + case 0: + return PrivilegeBits.EMPTY; + + case 1: + return getBits(privilegeNames[0]); + + default: + PrivilegeBits bts = PrivilegeBits.getInstance(); + for (Name privName : privilegeNames) { + bts.add(getBits(privName)); + } + return bts; + } + } + + /** + * Return the privilege bits for the specified privilege name. + * + * @param privilegeName + * @return privilege bits. + */ + PrivilegeBits getBits(Name privilegeName) { + Definition def = registeredPrivileges.get(privilegeName); + return (def == null) ? PrivilegeBits.EMPTY : def.bits; + } + + /** + * Add a privilege registration listener. + * + * @param listener + */ + void addListener(Listener listener) { + listeners.put(listener,listener); + } + + /** + * Removes a privilege registration listener. + * + * @param listener + */ + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + //---------------------------------------------< privilege registration >--- + /** + * Register the specified custom privilege definitions. + * + * @param stubs + * @throws RepositoryException If an error occurs. + */ + private void registerCustomDefinitions(Map stubs) throws RepositoryException { + if (customPrivilegesStore == null) { + throw new UnsupportedOperationException("No privilege store defined."); + } + synchronized (registeredPrivileges) { + Map definitions = createCustomDefinitions(stubs); + try { + // write the new custom privilege to the store and upon successful + // update of the file system resource add finally it to the map of + // registered privileges. + customPrivilegesStore.append(definitions); + cacheDefinitions(definitions); + + } catch (IOException e) { + throw new RepositoryException("Failed to register custom privilegess.", e); + } catch (FileSystemException e) { + throw new RepositoryException("Failed to register custom privileges.", e); + } catch (ParseException e) { + throw new RepositoryException("Failed to register custom privileges.", e); + } + } + + for (Listener l : listeners.keySet()) { + l.privilegesRegistered(stubs.keySet()); + } + } + + /** + * Adds the specified privilege definitions to the internal map(s) and + * recalculates the jcr:all privilege definition. + * + * @param definitions + */ + private void cacheDefinitions(Map definitions) { + registeredPrivileges.putAll(definitions); + for (Definition def : definitions.values()) { + bitsToNames.put(def.bits, Collections.singleton(def.getName())); + } + + if (!definitions.containsKey(NameConstants.JCR_ALL)) { + // redefine the jcr:all privilege definition + Definition all = registeredPrivileges.get(NameConstants.JCR_ALL); + bitsToNames.remove(all.bits); + + Set allAggrNames = new HashSet(all.getDeclaredAggregateNames()); + allAggrNames.addAll(definitions.keySet()); + + PrivilegeBits allbits = PrivilegeBits.getInstance(all.bits); + for (Definition d : definitions.values()) { + allbits.add(d.bits); + } + + Definition newAll = new Definition(NameConstants.JCR_ALL, false, allAggrNames, allbits, false); + registeredPrivileges.put(NameConstants.JCR_ALL, newAll); + bitsToNames.put(newAll.bits, Collections.singleton(NameConstants.JCR_ALL)); + } + } + + /** + * Creates PrivilegeDefinitions for all built-in privileges. + * + * @return definitions for all built-in privileges. + */ + private Map createBuiltInPrivilegeDefinitions() { + Map defs = new HashMap(); + + // all non-aggregate privileges + int jcrAllBits = NO_PRIVILEGE; + for (Name privilegeName : PRIVILEGE_NAMES.keySet()) { + int bits = PRIVILEGE_NAMES.get(privilegeName); + Definition def = new Definition(privilegeName, false, bits); + defs.put(privilegeName, def); + jcrAllBits |= bits; + } + + // jcr:write + Definition jcrWrite = createJcrWriteDefinition(); + defs.put(jcrWrite.getName(), jcrWrite); + + // rep:write + Definition repWrite = createRepWriteDefinition(jcrWrite); + defs.put(repWrite.getName(), repWrite); + + // jcr:all + Set jcrAllAggregates = new HashSet(10); + jcrAllAggregates.add(NameConstants.JCR_READ); + jcrAllAggregates.add(NameConstants.JCR_READ_ACCESS_CONTROL); + jcrAllAggregates.add(NameConstants.JCR_MODIFY_ACCESS_CONTROL); + jcrAllAggregates.add(NameConstants.JCR_LOCK_MANAGEMENT); + jcrAllAggregates.add(NameConstants.JCR_VERSION_MANAGEMENT); + jcrAllAggregates.add(NameConstants.JCR_NODE_TYPE_MANAGEMENT); + jcrAllAggregates.add(NameConstants.JCR_RETENTION_MANAGEMENT); + jcrAllAggregates.add(NameConstants.JCR_LIFECYCLE_MANAGEMENT); + jcrAllAggregates.add(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT); + jcrAllAggregates.add(NameConstants.JCR_NAMESPACE_MANAGEMENT); + jcrAllAggregates.add(NameConstants.JCR_WORKSPACE_MANAGEMENT); + jcrAllAggregates.add(NameConstants.JCR_WRITE); + jcrAllAggregates.add(REP_WRITE_NAME); + jcrAllAggregates.add(REP_PRIVILEGE_MANAGEMENT_NAME); + + Definition jcrAll = new Definition(NameConstants.JCR_ALL, false, jcrAllAggregates, jcrAllBits); + defs.put(jcrAll.getName(), jcrAll); + + return defs; + } + + /** + * Validates the specified DefinitionStubs and creates + * new custom PrivilegeDefinitions. The validation includes name + * validation and resolution of declared aggregate names. The latter + * also includes checks to prevent cyclic aggregation. + * + * @param toRegister + * @return new privilege definitions. + * @throws RepositoryException If any of the specified stubs is invalid. + */ + private Map createCustomDefinitions(Map toRegister) throws RepositoryException { + Map definitions = new HashMap(toRegister.size()); + Set aggregates = new HashSet(); + + for (PrivilegeDefinition stub : toRegister.values()) { + Name name = stub.getName(); + if (name == null) { + throw new RepositoryException("Name of custom privilege may not be null."); + } + if (registeredPrivileges.containsKey(name)) { + throw new RepositoryException("Registered privilege with name " + name + " already exists."); + } + + // namespace validation: + // - make sure the specified name defines a registered namespace + namespaceRegistry.getPrefix(name.getNamespaceURI()); + // - and isn't one of the reserved namespaces + if (isReservedNamespaceURI(name.getNamespaceURI())) { + throw new RepositoryException("Failed to register custom privilege: Reserved namespace URI: " + name.getNamespaceURI()); + } + + // validate aggregates + Set dagn = stub.getDeclaredAggregateNames(); + if (dagn.isEmpty()) { + // not an aggregate priv definition. + definitions.put(name, new Definition(stub, nextBits())); + } else { + for (Name declaredAggregateName : dagn) { + if (name.equals(declaredAggregateName)) { + throw new RepositoryException("Declared aggregate name '"+ declaredAggregateName.toString() +"'refers to the same custom privilege."); + } + if (registeredPrivileges.containsKey(declaredAggregateName)) { + log.debug("Declared aggregate name '"+ declaredAggregateName.toString() +"' referring to registered privilege."); + } else if (toRegister.containsKey(declaredAggregateName)) { + log.debug("Declared aggregate name '"+ declaredAggregateName.toString() +"' referring to un-registered privilege."); + // need to check for circular aggregates + if (isCircularAggregation(stub, declaredAggregateName, toRegister)) { + throw new RepositoryException("Detected circular aggregation within custom privilege caused by " + declaredAggregateName.toString()); + } + } else { + throw new RepositoryException("Found unresolvable name of declared aggregate privilege " + declaredAggregateName.toString()); + } + } + // remember for further processing + aggregates.add(stub); + } + } + + // process the aggregate stubs in order to calculate the 'bits' + while (aggregates.size() > 0) { + // monitor progress of resolution into proper definitions. + int cnt = aggregates.size(); + + // look for those definitions whose declared aggregates have all been processed. + for (Iterator itr = aggregates.iterator(); itr.hasNext();) { + PrivilegeDefinition stub = itr.next(); + PrivilegeBits bts = getAggregateBits(stub.getDeclaredAggregateNames(), definitions); + if (!bts.isEmpty()) { + // make sure the same aggregation is not yet covered by an + // already registered privilege + if (bitsToNames.containsKey(bts) && bitsToNames.get(bts).size() == 1) { + Name existingName = bitsToNames.get(bts).iterator().next(); + throw new RepositoryException("Custom aggregate privilege '" + stub.getName() + "' is already covered by '" + existingName.toString() + "'"); + } + // ... nor is present within the set of definitions that have + // been created before for registration. + for (Definition d : definitions.values()) { + if (bts.equals(d.bits)) { + throw new RepositoryException("Custom aggregate privilege '" + stub.getName() + "' is already defined by '"+ d.getName()+"'"); + } + } + + // now its save to create the new definition + Definition def = new Definition(stub, bts); + definitions.put(def.getName(), def); + itr.remove(); + } // unresolvable bts -> postpone to next iterator. + } + + if (cnt == aggregates.size()) { + // none of the remaining aggregate-definitions could be processed + throw new RepositoryException("Invalid aggregate privilege definition. Failed to resolve aggregate names."); + } + } + + return definitions; + } + + private boolean isReservedNamespaceURI(String uri) { + if (namespaceRegistry instanceof NamespaceRegistryImpl) { + return ((NamespaceRegistryImpl) namespaceRegistry).isReservedURI(uri); + } else { + // hardcoded fallback + return Name.NS_REP_URI.equals(uri) + || (uri.startsWith("http://www.w3.org")) + || uri.startsWith("http://www.jcp.org"); + } + } + + /** + * + * @return + */ + private PrivilegeBits nextBits() { + PrivilegeBits b = nextBits; + nextBits = nextBits.nextBits(); + return b; + } + + /** + * + * @param declaredAggregateNames + * @param toRegister + * @return + */ + private PrivilegeBits getAggregateBits(Set declaredAggregateNames, Map toRegister) { + PrivilegeBits bts = PrivilegeBits.getInstance(); + for (Name n : declaredAggregateNames) { + if (registeredPrivileges.containsKey(n)) { + bts.add(registeredPrivileges.get(n).bits); + } else if (toRegister.containsKey(n)) { + Definition def = toRegister.get(n); + bts.add(def.bits); + } else { + // unknown dependency (should not get here) -> return the empty set. + return PrivilegeBits.EMPTY; + } + } + return bts.unmodifiable(); + } + + /** + * + * @param def + * @param declaredAggregateName + * @param toRegister + * @return + */ + private boolean isCircularAggregation(PrivilegeDefinition def, Name declaredAggregateName, Map toRegister) { + PrivilegeDefinition d = toRegister.get(declaredAggregateName); + if (d.getDeclaredAggregateNames().isEmpty()) { + return false; + } else { + boolean isCircular = false; + for (Name n : d.getDeclaredAggregateNames()) { + if (def.getName().equals(n)) { + return true; + } + if (toRegister.containsKey(n)) { + isCircular = isCircularAggregation(def, n, toRegister); + } + } + return isCircular; + } + } + + /** + * @return PrivilegeDefinition for the jcr:write privilege + */ + private static Definition createJcrWriteDefinition() { + Set jcrWriteAggregates = new HashSet(4); + jcrWriteAggregates.add(NameConstants.JCR_MODIFY_PROPERTIES); + jcrWriteAggregates.add(NameConstants.JCR_ADD_CHILD_NODES); + jcrWriteAggregates.add(NameConstants.JCR_REMOVE_CHILD_NODES); + jcrWriteAggregates.add(NameConstants.JCR_REMOVE_NODE); + + int jcrWriteBits = NO_PRIVILEGE; + for (Name privilegeName : jcrWriteAggregates) { + jcrWriteBits |= PRIVILEGE_NAMES.get(privilegeName); + } + return new Definition(NameConstants.JCR_WRITE, false, jcrWriteAggregates, jcrWriteBits); + } + + private static Definition createRepWriteDefinition(Definition jcrWrite) { + Set repWriteAggregates = new HashSet(2); + repWriteAggregates.add(NameConstants.JCR_WRITE); + repWriteAggregates.add(NameConstants.JCR_NODE_TYPE_MANAGEMENT); + + long repWriteBits = jcrWrite.bits.longValue() | PRIVILEGE_NAMES.get(NameConstants.JCR_NODE_TYPE_MANAGEMENT); + return new Definition(REP_WRITE_NAME, false, repWriteAggregates, repWriteBits); + } + + //-------------------------------------------------------------------------- + /** + * Notification about new registered privileges + */ + interface Listener { + /** + * @param privilegeNames + */ + void privilegesRegistered(Set privilegeNames); + } + + /** + * Internal definition of a JCR privilege extending from the general + * privilege definition. It defines addition information that ease + * the evaluation of privileges. + */ + private final static class Definition extends PrivilegeDefinitionImpl { + + private final PrivilegeBits bits; + private final boolean isCustom; + + private int hashCode; + + private Definition(PrivilegeDefinition stub, PrivilegeBits bits) { + this(stub.getName(), stub.isAbstract(), stub.getDeclaredAggregateNames(), bits, true); + } + + private Definition(Name name, boolean isAbstract, long bits) { + this(name, isAbstract, Collections.emptySet(), PrivilegeBits.getInstance(bits), false); + } + + private Definition(Name name, boolean isAbstract, Set declaredAggregateNames, long bits) { + this(name, isAbstract, declaredAggregateNames, PrivilegeBits.getInstance(bits), false); + } + + private Definition(Name name, boolean isAbstract, Set declaredAggregateNames, PrivilegeBits bits, boolean isCustom) { + super(name, isAbstract, declaredAggregateNames); + if (bits == null || bits.isEmpty()) { + throw new IllegalArgumentException("Failed to build bit representation of PrivilegeDefinition."); + } else { + this.bits = bits.unmodifiable(); + } + this.isCustom = isCustom; + } + + //---------------------------------------------------------< Object >--- + @Override + public int hashCode() { + if (hashCode == 0) { + int h = super.hashCode(); + h = 37 * h + bits.hashCode(); + hashCode = h; + } + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Definition) { + Definition other = (Definition) obj; + return bits.equals(other.bits) && super.equals(other); + } + return false; + } + } + + /** + * CustomPrivilegeStore used to read and write custom privilege definitions + * from/to a file system resource. + */ + private final class CustomPrivilegeStore { + + /** + * File system resource used to persist custom privileges registered with + * the repository. + */ + private final FileSystemResource customPrivilegesResource; + + private CustomPrivilegeStore(FileSystemResource customPrivilegesResource) throws RepositoryException { + this.customPrivilegesResource = customPrivilegesResource; + try { + // make sure path to resource exists + if (!customPrivilegesResource.exists()) { + customPrivilegesResource.makeParentDirs(); + } + } catch (FileSystemException e) { + String error = "Internal error: Failed to access/create file system resource for custom privileges at " + customPrivilegesResource.getPath(); + log.debug(error); + throw new RepositoryException(error, e); + } + } + + private Map load() throws FileSystemException, RepositoryException, ParseException, IOException { + Map stubs = new LinkedHashMap(); + + if (customPrivilegesResource.exists()) { + InputStream in = customPrivilegesResource.getInputStream(); + try { + PrivilegeDefinitionReader pr = new PrivilegeDefinitionReader(in, "text/xml"); + for (PrivilegeDefinition def : pr.getPrivilegeDefinitions()) { + Name privName = def.getName(); + if (stubs.containsKey(privName)) { + throw new RepositoryException("Duplicate entry for custom privilege with name " + privName.toString()); + } + stubs.put(privName, def); + } + } finally { + in.close(); + } + } + return stubs; + } + + private void append(Map newPrivilegeDefinitions) throws IOException, FileSystemException, RepositoryException, ParseException { + List jcrDefs; + Map nsMapping; + + if (customPrivilegesResource.exists()) { + InputStream in = customPrivilegesResource.getInputStream(); + try { + PrivilegeDefinitionReader pr = new PrivilegeDefinitionReader(in, "text/xml"); + jcrDefs = new ArrayList(Arrays.asList(pr.getPrivilegeDefinitions())); + nsMapping = pr.getNamespaces(); + } finally { + in.close(); + } + } else { + jcrDefs = new ArrayList(); + nsMapping = new HashMap(); + } + + for (Definition d : newPrivilegeDefinitions.values()) { + String uri = d.getName().getNamespaceURI(); + nsMapping.put(namespaceRegistry.getPrefix(uri), uri); + + for (Name dan : d.getDeclaredAggregateNames()) { + uri = dan.getNamespaceURI(); + nsMapping.put(namespaceRegistry.getPrefix(uri), uri); + } + jcrDefs.add(d); + } + + OutputStream out = customPrivilegesResource.getOutputStream(); + try { + PrivilegeDefinitionWriter pdw = new PrivilegeDefinitionWriter("text/xml"); + pdw.writeDefinitions(out, jcrDefs.toArray(new PrivilegeDefinition[jcrDefs.size()]), nsMapping); + } finally { + out.close(); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/UnmodifiableAccessControlList.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/UnmodifiableAccessControlList.java new file mode 100644 index 00000000000..ef33e46a7f2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/UnmodifiableAccessControlList.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.core.security.authorization; + +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.Privilege; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.PropertyType; + +import java.security.Principal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Collections; +import java.util.HashMap; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; + +/** + * An implementation of the AccessControlList interface that only + * allows for reading. The write methods + * ({@link #addAccessControlEntry(Principal principal, Privilege[] privileges) addAccessControlEntry} + * and {@link #removeAccessControlEntry(AccessControlEntry) removeAccessControlEntry}) + * throw an AccessControlException. + */ +public class UnmodifiableAccessControlList implements JackrabbitAccessControlList { + + private final AccessControlEntry[] accessControlEntries; + + private final Map restrictions; + + private final String path; + + private int hashCode = 0; + + /** + * Construct a new UnmodifiableAccessControlList + * + * @param acl The AccessControlList to be wrapped in order to prevent + * it's modification. + * @throws RepositoryException The the entries cannot be retrieved from the + * specified AccessControlList. + */ + public UnmodifiableAccessControlList(AccessControlList acl) throws RepositoryException { + if (acl instanceof JackrabbitAccessControlList) { + JackrabbitAccessControlList jAcl = (JackrabbitAccessControlList) acl; + accessControlEntries = acl.getAccessControlEntries(); + path = jAcl.getPath(); + Map r = new HashMap(); + for (String name: jAcl.getRestrictionNames()) { + r.put(name, jAcl.getRestrictionType(name)); + } + restrictions = Collections.unmodifiableMap(r); + } else { + accessControlEntries = acl.getAccessControlEntries(); + path = null; + restrictions = Collections.emptyMap(); + } + } + + /** + * Construct a new UnmodifiableAccessControlList + * + * @param accessControlEntries A list of {@link AccessControlEntry access control entries}. + */ + public UnmodifiableAccessControlList(List accessControlEntries) { + this(accessControlEntries, null, Collections.emptyMap()); + } + + /** + * Construct a new UnmodifiableAccessControlList + * + * @param accessControlEntries + * @param path + * @param restrictions + */ + public UnmodifiableAccessControlList(List accessControlEntries, String path, Maprestrictions) { + this.accessControlEntries = accessControlEntries.toArray(new AccessControlEntry[accessControlEntries.size()]); + this.path = path; + this.restrictions = restrictions; + } + + //--------------------------------------------------< AccessControlList >--- + /** + * @see AccessControlList#getAccessControlEntries() + */ + public AccessControlEntry[] getAccessControlEntries() + throws RepositoryException { + return accessControlEntries; + } + + /** + * @see AccessControlList#addAccessControlEntry(Principal, Privilege[]) + */ + public boolean addAccessControlEntry(Principal principal, + Privilege[] privileges) + throws AccessControlException, RepositoryException { + throw new AccessControlException("Unmodifiable ACL. Use AccessControlManager#getApplicablePolicies in order to obtain an modifiable ACL."); + } + + /** + * @see AccessControlList#removeAccessControlEntry(AccessControlEntry) + */ + public void removeAccessControlEntry(AccessControlEntry ace) + throws AccessControlException, RepositoryException { + throw new AccessControlException("Unmodifiable ACL. Use AccessControlManager#getApplicablePolicies in order to obtain an modifiable ACL."); + } + + //----------------------------------------< JackrabbitAccessControlList >--- + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#getRestrictionNames() + */ + public String[] getRestrictionNames() { + return restrictions.keySet().toArray(new String[restrictions.size()]); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#getRestrictionType(String) + */ + public int getRestrictionType(String restrictionName) { + if (restrictions.containsKey(restrictionName)) { + return restrictions.get(restrictionName); + } else { + return PropertyType.UNDEFINED; + } + } + + @Override + public boolean isMultiValueRestriction(String restrictionName) throws RepositoryException { + return false; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#isEmpty() + */ + public boolean isEmpty() { + return accessControlEntries.length == 0; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#size() + */ + public int size() { + return accessControlEntries.length; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean) + */ + public boolean addEntry(Principal principal, Privilege[] privileges, boolean isAllow) throws AccessControlException { + throw new AccessControlException("Unmodifiable ACL. Use AccessControlManager#getPolicy or #getApplicablePolicies in order to obtain an modifiable ACL."); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean, Map) + */ + public boolean addEntry(Principal principal, Privilege[] privileges, boolean isAllow, Map restrictions) throws AccessControlException { + throw new AccessControlException("Unmodifiable ACL. Use AccessControlManager#getPolicy or #getApplicablePolicies in order to obtain an modifiable ACL."); + } + + public boolean addEntry(Principal principal, Privilege[] privileges, boolean isAllow, Map restrictions, Map mvRestrictions) throws AccessControlException, RepositoryException { + throw new AccessControlException("Unmodifiable ACL. Use AccessControlManager#getPolicy or #getApplicablePolicies in order to obtain an modifiable ACL."); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#orderBefore(AccessControlEntry, AccessControlEntry) + */ + public void orderBefore(AccessControlEntry srcEntry, AccessControlEntry destEntry) throws AccessControlException { + throw new AccessControlException("Unmodifiable ACL. Use AccessControlManager#getPolicy or #getApplicablePolicy in order to obtain a modifiable ACL."); + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#getPath() + */ + public String getPath() { + return path; + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + if (hashCode == 0) { + int result = 17; + result = 37 * result + (path != null ? path.hashCode() : 0); + for (AccessControlEntry entry : accessControlEntries) { + result = 37 * result + entry.hashCode(); + } + for (String restrictionName : restrictions.keySet()) { + result = 37 * (restrictionName + "." + restrictions.get(restrictionName)).hashCode(); + } + hashCode = result; + } + return hashCode; + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof UnmodifiableAccessControlList) { + UnmodifiableAccessControlList acl = (UnmodifiableAccessControlList) obj; + return ((path == null) ? acl.path == null : path.equals(acl.path)) && + Arrays.equals(accessControlEntries, acl.accessControlEntries) && + restrictions.equals(acl.restrictions); + } + return false; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/WorkspaceAccessManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/WorkspaceAccessManager.java new file mode 100644 index 00000000000..41df638c676 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/WorkspaceAccessManager.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.core.security.authorization; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.Set; +import java.security.Principal; + +/** + * The WorkspaceAccessManager is responsible for workspace access. + * In contrast to Items that are identified, workspaces are named Objects + * on different class hierarchy. + */ +public interface WorkspaceAccessManager { + + /** + * Initialize this WorkspaceAccessManager. + * + * @param systemSession Session used to initialize this instance. + * @throws RepositoryException if an error occurs. + */ + void init(Session systemSession) throws RepositoryException; + + /** + * Dispose this WorkspaceAccessManager and its resources. + * + * @throws RepositoryException if an error occurs. + */ + void close() throws RepositoryException; + + /** + * Returns true if access to the workspace with the given name + * is granted to the to any of the specified principals. + * + * @param principals A set of principals to be tested for being allowed to + * access workspace identified by workspaceName. + * @param workspaceName Name of the workspace to be tested. + * @return true if the given set of principals is allowed to access the + * workspace with the specified name. + * @throws RepositoryException If an error occurs. + */ + boolean grants(Set principals, String workspaceName) + throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLEditor.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLEditor.java new file mode 100644 index 00000000000..2919ade7de3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLEditor.java @@ -0,0 +1,406 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.ProtectedItemModifier; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.AccessControlEditor; +import org.apache.jackrabbit.core.security.authorization.AccessControlEntryImpl; +import org.apache.jackrabbit.core.security.authorization.AccessControlUtils; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +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.ValueFormatException; +import javax.jcr.NodeIterator; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Set; + +/** + * ACLEditor... + */ +public class ACLEditor extends ProtectedItemModifier implements AccessControlEditor, AccessControlConstants { + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(ACLEditor.class); + /** + * Default name for ace nodes + */ + private static final String DEFAULT_ACE_NAME = "ace"; + /** + * the editing session + */ + private final SessionImpl session; + private final AccessControlUtils utils; + private final boolean allowUnknownPrincipals; + + ACLEditor(Session editingSession, AccessControlUtils utils, boolean allowUnknownPrincipals) { + super(Permission.MODIFY_AC); + if (editingSession instanceof SessionImpl) { + session = ((SessionImpl) editingSession); + } else { + throw new IllegalArgumentException("org.apache.jackrabbit.core.SessionImpl expected. Found " + editingSession.getClass()); + } + this.utils = utils; + this.allowUnknownPrincipals = allowUnknownPrincipals; + } + + /** + * + * @param aclNode the node + * @param path + * @return the control list + * @throws RepositoryException if an error occurs + */ + ACLTemplate getACL(NodeImpl aclNode, String path) throws RepositoryException { + return new ACLTemplate(aclNode, path, allowUnknownPrincipals); + } + + //------------------------------------------------< AccessControlEditor >--- + /** + * @see AccessControlEditor#getPolicies(String) + */ + public AccessControlPolicy[] getPolicies(String nodePath) throws AccessControlException, PathNotFoundException, RepositoryException { + checkProtectsNode(nodePath); + + NodeImpl aclNode = getAclNode(nodePath); + if (aclNode == null) { + return new AccessControlPolicy[0]; + } else { + return new AccessControlPolicy[] {getACL(aclNode, nodePath)}; + } + } + + /** + * Always returns an empty array as no applicable policies are exposed. + * + * @see AccessControlEditor#getPolicies(Principal) + */ + public JackrabbitAccessControlPolicy[] getPolicies(Principal principal) throws AccessControlException, RepositoryException { + if (!session.getPrincipalManager().hasPrincipal(principal.getName())) { + throw new AccessControlException("Unknown principal."); + } + // TODO: impl. missing + return new JackrabbitAccessControlPolicy[0]; + } + + /** + * @see AccessControlEditor#editAccessControlPolicies(String) + */ + public AccessControlPolicy[] editAccessControlPolicies(String nodePath) throws AccessControlException, PathNotFoundException, RepositoryException { + checkProtectsNode(nodePath); + + String mixin; + Name aclName; + NodeImpl controlledNode; + + if (nodePath == null) { + controlledNode = (NodeImpl) session.getRootNode(); + mixin = session.getJCRName(NT_REP_REPO_ACCESS_CONTROLLABLE); + aclName = N_REPO_POLICY; + } else { + controlledNode = getNode(nodePath); + mixin = session.getJCRName(NT_REP_ACCESS_CONTROLLABLE); + aclName = N_POLICY; + } + + AccessControlPolicy acl = null; + NodeImpl aclNode = getAclNode(controlledNode, nodePath); + if (aclNode == null) { + // create an empty acl unless the node is protected or cannot have + // mixin set (e.g. due to a lock) or + // has colliding rep:policy or rep:repoPolicy child node set. + if (controlledNode.hasNode(aclName)) { + // policy child node without node being access controlled + log.warn("Colliding policy child without node being access controllable ({}).", nodePath); + } else { + PrivilegeManager privMgr = ((JackrabbitWorkspace) session.getWorkspace()).getPrivilegeManager(); + if (controlledNode.isNodeType(mixin) || controlledNode.canAddMixin(mixin)) { + acl = new ACLTemplate(nodePath, session.getPrincipalManager(), + privMgr, session.getValueFactory(), session, allowUnknownPrincipals); + } else { + log.warn("Node {} cannot be made access controllable.", nodePath); + } + } + } // else: acl already present -> getPolicies must be used. + + return (acl != null) ? new AccessControlPolicy[] {acl} : new AccessControlPolicy[0]; + } + + /** + * @see AccessControlEditor#editAccessControlPolicies(Principal) + */ + public JackrabbitAccessControlPolicy[] editAccessControlPolicies(Principal principal) throws AccessDeniedException, AccessControlException, RepositoryException { + if (!session.getPrincipalManager().hasPrincipal(principal.getName())) { + throw new AccessControlException("Unknown principal."); + } + // TODO: impl. missing + return new JackrabbitAccessControlPolicy[0]; + } + + /** + * @see AccessControlEditor#setPolicy(String,AccessControlPolicy) + */ + public void setPolicy(String nodePath, AccessControlPolicy policy) throws RepositoryException { + checkProtectsNode(nodePath); + checkValidPolicy(nodePath, policy); + + NodeImpl aclNode = getAclNode(nodePath); + if (aclNode != null) { + // remove all existing aces + for (NodeIterator aceNodes = aclNode.getNodes(); aceNodes.hasNext();) { + NodeImpl aceNode = (NodeImpl) aceNodes.nextNode(); + removeItem(aceNode); + } + } else { + // create the acl node + aclNode = (nodePath == null) ? createRepoAclNode() : createAclNode(nodePath); + } + + AccessControlEntry[] entries = ((ACLTemplate) policy).getAccessControlEntries(); + for (AccessControlEntry entry : entries) { + AccessControlEntryImpl ace = (AccessControlEntryImpl) entry; + + Name nodeName = getUniqueNodeName(aclNode, ace.isAllow() ? "allow" : "deny"); + Name ntName = (ace.isAllow()) ? NT_REP_GRANT_ACE : NT_REP_DENY_ACE; + ValueFactory vf = session.getValueFactory(); + + // create the ACE node + NodeImpl aceNode = addNode(aclNode, nodeName, ntName); + + // write the rep:principalName property + String principalName = ace.getPrincipal().getName(); + setProperty(aceNode, P_PRINCIPAL_NAME, vf.createValue(principalName)); + + // ... and the rep:privileges property + Privilege[] pvlgs = ace.getPrivileges(); + Value[] names = getPrivilegeNames(pvlgs, vf); + setProperty(aceNode, P_PRIVILEGES, names); + + // store the restrictions: + Set restrNames = ace.getRestrictions().keySet(); + for (Name restrName : restrNames) { + Value value = ace.getRestriction(restrName); + setProperty(aceNode, restrName, value); + } + } + + // mark the parent modified. + markModified(((NodeImpl)aclNode.getParent())); + } + + /** + * @see AccessControlEditor#removePolicy(String,AccessControlPolicy) + */ + public synchronized void removePolicy(String nodePath, AccessControlPolicy policy) throws AccessControlException, RepositoryException { + checkProtectsNode(nodePath); + checkValidPolicy(nodePath, policy); + + NodeImpl aclNode = getAclNode(nodePath); + if (aclNode != null) { + removeItem(aclNode); + } else { + throw new AccessControlException("No policy to remove at " + nodePath); + } + } + + //-------------------------------------------------------------------------- + /** + * Check if the Node identified by nodePath is itself part of ACL + * defining content. It this case setting or modifying an AC-policy is + * obviously not possible. + * + * @param nodePath the node path + * @throws AccessControlException If the given nodePath identifies a Node that + * represents a ACL or ACE item. + * @throws RepositoryException + */ + private void checkProtectsNode(String nodePath) throws RepositoryException { + if (nodePath != null) { + NodeImpl node = getNode(nodePath); + if (utils.isAcItem(node)) { + throw new AccessControlException("Node " + nodePath + " defines ACL or ACE itself."); + } + } + } + + /** + * Check if the specified policy can be set/removed from this editor. + * + * @param nodePath the node path + * @param policy the policy + * @throws AccessControlException if not allowed + */ + private static void checkValidPolicy(String nodePath, AccessControlPolicy policy) throws AccessControlException { + if (policy == null || !(policy instanceof ACLTemplate)) { + throw new AccessControlException("Attempt to set/remove invalid policy " + policy); + } + ACLTemplate acl = (ACLTemplate) policy; + boolean matchingPath = (nodePath == null) ? acl.getPath() == null : nodePath.equals(acl.getPath()); + if (!matchingPath) { + throw new AccessControlException("Policy " + policy + " cannot be applied/removed from the node at " + nodePath); + } + } + + /** + * + * @param path the path + * @return the node + * @throws RepositoryException if an error occurs + */ + private NodeImpl getNode(String path) throws RepositoryException { + return (NodeImpl) session.getNode(path); + } + + /** + * Returns the rep:Policy node below the Node identified at the given + * path or null if the node is not mix:AccessControllable + * or if no policy node exists. + * + * @param nodePath the node path + * @return node or null + * @throws PathNotFoundException if not found + * @throws RepositoryException if an error occurs + */ + private NodeImpl getAclNode(String nodePath) throws PathNotFoundException, RepositoryException { + NodeImpl controlledNode; + if (nodePath == null) { + controlledNode = (NodeImpl) session.getRootNode(); + } else { + controlledNode = getNode(nodePath); + } + return getAclNode(controlledNode, nodePath); + } + + /** + * Returns the rep:Policy node below the given Node or null + * if the node is not mix:AccessControllable or if no policy node exists. + * + * @param controlledNode the controlled node + * @param nodePath + * @return node or null + * @throws RepositoryException if an error occurs + */ + private NodeImpl getAclNode(NodeImpl controlledNode, String nodePath) throws RepositoryException { + NodeImpl aclNode = null; + if (nodePath == null) { + if (ACLProvider.isRepoAccessControlled(controlledNode)) { + aclNode = controlledNode.getNode(N_REPO_POLICY); + } + } else { + if (ACLProvider.isAccessControlled(controlledNode)) { + aclNode = controlledNode.getNode(N_POLICY); + } + } + return aclNode; + } + + /** + * + * @param nodePath the node path + * @return the new node + * @throws RepositoryException if an error occurs + */ + private NodeImpl createAclNode(String nodePath) throws RepositoryException { + NodeImpl protectedNode = getNode(nodePath); + if (!protectedNode.isNodeType(NT_REP_ACCESS_CONTROLLABLE)) { + protectedNode.addMixin(NT_REP_ACCESS_CONTROLLABLE); + } + return addNode(protectedNode, N_POLICY, NT_REP_ACL); + } + + /** + * + * @return the new acl node used to store repository level privileges. + * @throws RepositoryException if an error occurs + */ + private NodeImpl createRepoAclNode() throws RepositoryException { + NodeImpl root = (NodeImpl) session.getRootNode(); + if (!root.isNodeType(NT_REP_REPO_ACCESS_CONTROLLABLE)) { + root.addMixin(NT_REP_REPO_ACCESS_CONTROLLABLE); + } + return addNode(root, N_REPO_POLICY, NT_REP_ACL); + } + + /** + * Create a unique valid name for the Permission nodes to be save. + * + * @param node a name for the child is resolved + * @param name if missing the {@link #DEFAULT_ACE_NAME} is taken + * @return the name + * @throws RepositoryException if an error occurs + */ + protected static Name getUniqueNodeName(Node node, String name) throws RepositoryException { + if (name == null) { + name = DEFAULT_ACE_NAME; + } else { + try { + NameParser.checkFormat(name); + } catch (NameException e) { + name = DEFAULT_ACE_NAME; + log.debug("Invalid path name for Permission: " + name + "."); + } + } + int i = 0; + String check = name; + while (node.hasNode(check)) { + check = name + i; + i++; + } + return ((SessionImpl) node.getSession()).getQName(check); + } + + /** + * Build an array of Value from the specified privileges using + * the given valueFactory. + * + * @param privileges the privileges + * @param valueFactory the value factory + * @return an array of Value. + * @throws ValueFormatException if an error occurs + */ + private static Value[] getPrivilegeNames(Privilege[] privileges, ValueFactory valueFactory) + throws ValueFormatException { + Value[] names = new Value[privileges.length]; + for (int i = 0; i < privileges.length; i++) { + names[i] = valueFactory.createValue(privileges[i].getName(), PropertyType.NAME); + } + return names; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLProvider.java new file mode 100644 index 00000000000..c6c938270cd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLProvider.java @@ -0,0 +1,431 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.authorization.AbstractAccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.AccessControlEditor; +import org.apache.jackrabbit.core.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.UnmodifiableAccessControlList; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.QueryResult; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The ACLProvider generates access control policies out of the items stored + * in the workspace applying the following rules: + *
      + *
    • A Node is considered access controlled if an ACL has + * been explicitly assigned to it by adding the mixin type + * rep:AccessControllable and adding child node of type + * rep:acl that forms the acl.
    • + *
    • a Property is considered 'access controlled' if its parent Node is.
    • + *
    • An ACL is never assigned to a Property item.
    • + *
    • A Node that is not access controlled may inherit the ACL. + * The ACL is inherited from the closest access controlled ancestor.
    • + *
    • It may be possible that a given Node has no effective ACL, in + * which case some a default policy is returned that grants READ privilege to + * any principal and denies all other privileges.
    • + *
    • an item is considered an ACL item if it is used to define an ACL. + * ACL items inherit the ACL from node they defined the ACL for.
    • + *
    + * + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider for additional information. + */ +public class ACLProvider extends AbstractAccessControlProvider implements AccessControlConstants { + + /** + * Constant for the name of the configuration option {@code allow-unknown-principals}. + * The option is a flag indicating whether access control entries with principals not known to the system + * can be added to an ACL. the default is {@code false}. + *

    + * Please note that the current implementation does only check principal existence when adding a new access + * control entry, but does not validate all ACEs when removing a principal. So even if this flag is {@code false}, + * it's possible to create an ACL with a unknown principal. + */ + public static final String PARAM_ALLOW_UNKNOWN_PRINCIPALS = "allow-unknown-principals"; + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(ACLProvider.class); + + /** + * The node id of the root node + */ + private NodeId rootNodeId; + + /** + * Cache to ease the retrieval of ACEs defined for a given node. This cache + * is used by the ACLPermissions created individually for each Session + * instance. + */ + private EntryCollector entryCollector; + + /** + * controls if unknown principals are allowed in ACLs + */ + private boolean allowUnknownPrincipals; + + //----------------------------------------------< AccessControlProvider >--- + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#init(Session, Map) + */ + @Override + public void init(Session systemSession, Map configuration) throws RepositoryException { + super.init(systemSession, configuration); + allowUnknownPrincipals = "true".equals(configuration.get(PARAM_ALLOW_UNKNOWN_PRINCIPALS)); + + // make sure the workspace of the given systemSession has a + // minimal protection on the root node. + NodeImpl root = (NodeImpl) session.getRootNode(); + rootNodeId = root.getNodeId(); + ACLEditor systemEditor = new ACLEditor(session, this, allowUnknownPrincipals); + + // TODO: replace by configurable default policy (see JCR-2331) + boolean initializedWithDefaults = !configuration.containsKey(PARAM_OMIT_DEFAULT_PERMISSIONS); + if (initializedWithDefaults && !isAccessControlled(root)) { + initRootACL(session, systemEditor); + } + + entryCollector = createEntryCollector(session); + } + + @Override + public void close() { + super.close(); + entryCollector.close(); + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(org.apache.jackrabbit.spi.Path,org.apache.jackrabbit.core.security.authorization.CompiledPermissions) + */ + public AccessControlPolicy[] getEffectivePolicies(Path absPath, CompiledPermissions permissions) throws ItemNotFoundException, RepositoryException { + checkInitialized(); + + NodeImpl targetNode; + List acls = new ArrayList(); + if (absPath == null) { + targetNode = (NodeImpl) session.getRootNode(); + if (isRepoAccessControlled(targetNode)) { + if (permissions.grants(targetNode.getPrimaryPath(), Permission.READ_AC)) { + acls.add(getACL(targetNode, N_REPO_POLICY, null)); + } else { + throw new AccessDeniedException("Access denied at " + targetNode.getPath()); + } + } + } else { + targetNode = (NodeImpl) session.getNode(session.getJCRPath(absPath)); + NodeImpl node = getNode(targetNode, isAcItem(targetNode)); + + // collect all ACLs effective at node + collectAcls(node, permissions, acls); + } + + // if no effective ACLs are present -> add a default, empty acl. + if (acls.isEmpty()) { + // no access control information can be retrieved for the specified + // node, since neither the node nor any of its parents is access + // controlled. TODO: there should be a default policy in this case (see JCR-2331) + log.warn("No access controlled node present in item hierarchy starting from " + targetNode.getPath()); + } + return acls.toArray(new AccessControlList[acls.size()]); + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(java.util.Set, CompiledPermissions) + */ + public AccessControlPolicy[] getEffectivePolicies(Set principals, CompiledPermissions permissions) throws RepositoryException { + String propName = ISO9075.encode(session.getJCRName(P_PRINCIPAL_NAME)); + + StringBuilder stmt = new StringBuilder("/jcr:root"); + stmt.append("//element(*,"); + stmt.append(session.getJCRName(NT_REP_ACE)); + stmt.append(")["); + int i = 0; + for (Principal principal : principals) { + if (i > 0) { + stmt.append(" or "); + } + stmt.append("@"); + stmt.append(propName); + stmt.append("='"); + stmt.append(principal.getName().replaceAll("'", "''")); + stmt.append("'"); + i++; + } + stmt.append("]"); + + QueryResult result; + try { + QueryManager qm = session.getWorkspace().getQueryManager(); + Query q = qm.createQuery(stmt.toString(), Query.XPATH); + result = q.execute(); + } catch (RepositoryException e) { + log.error("Unexpected error while searching effective policies. {}", e.getMessage()); + throw new UnsupportedOperationException("Retrieve effective policies for set of principals not supported.", e); + } + + Set acls = new LinkedHashSet(); + for (NodeIterator it = result.getNodes(); it.hasNext();) { + NodeImpl aclNode = (NodeImpl) it.nextNode().getParent(); + Name aclName = aclNode.getQName(); + NodeImpl accessControlledNode = (NodeImpl) aclNode.getParent(); + + if (N_POLICY.equals(aclName) && isAccessControlled(accessControlledNode)) { + if (permissions.canRead(aclNode.getPrimaryPath(), aclNode.getNodeId())) { + acls.add(getACL(accessControlledNode, N_POLICY, accessControlledNode.getPath())); + } else { + throw new AccessDeniedException("Access denied at " + Text.getRelativeParent(aclNode.getPath(), 1)); + } + } else if (N_REPO_POLICY.equals(aclName) && isRepoAccessControlled(accessControlledNode)) { + if (permissions.canRead(aclNode.getPrimaryPath(), aclNode.getNodeId())) { + acls.add(getACL(accessControlledNode, N_REPO_POLICY, null)); + } else { + throw new AccessDeniedException("Access denied at " + Text.getRelativeParent(aclNode.getPath(), 1)); + } + } // else: not a regular policy node -> ignore. + } + + return acls.toArray(new AccessControlPolicy[acls.size()]); + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEditor(Session) + */ + public AccessControlEditor getEditor(Session session) { + checkInitialized(); + return new ACLEditor(session, this, allowUnknownPrincipals); + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#compilePermissions(Set) + */ + public CompiledPermissions compilePermissions(Set principals) throws RepositoryException { + checkInitialized(); + if (isAdminOrSystem(principals)) { + return getAdminPermissions(); + } else if (isReadOnly(principals)) { + return getReadOnlyPermissions(); + } else { + return new CompiledPermissionsImpl(principals, session, entryCollector, this, true); + } + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#canAccessRoot(Set) + */ + public boolean canAccessRoot(Set principals) throws RepositoryException { + checkInitialized(); + if (isAdminOrSystem(principals)) { + return true; + } else { + CompiledPermissions cp = new CompiledPermissionsImpl(principals, session, entryCollector, this, false); + try { + return cp.canRead(null, rootNodeId); + } finally { + cp.close(); + } + } + } + + //----------------------------------------------------------< protected >--- + /** + * Create the EntryCollector instance that is used by this + * provider to gather the effective ACEs for a given list of principals at a + * given node during AC evaluation. + * + * @param systemSession The system session to create the entry collector for. + * @return A new instance of CachingEntryCollector. + * @throws RepositoryException If an error occurs. + */ + protected EntryCollector createEntryCollector(SessionImpl systemSession) throws RepositoryException { + return new CachingEntryCollector(systemSession, rootNodeId); + } + + //------------------------------------------------------------< private >--- + /** + * Recursively collects all ACLs that are effective on the specified node. + * + * @param node the Node to collect the ACLs for, which must NOT be part of the + * structure defined by mix:AccessControllable. + * @param permissions + * @param acls List used to collect the effective acls. + * @throws RepositoryException if an error occurs + */ + private void collectAcls(NodeImpl node, CompiledPermissions permissions, List acls) throws RepositoryException { + // if the given node is access-controlled, construct a new ACL and add + // it to the list + if (isAccessControlled(node)) { + if (permissions.grants(node.getPrimaryPath(), Permission.READ_AC)) { + acls.add(getACL(node, N_POLICY, node.getPath())); + } else { + throw new AccessDeniedException("Access denied at " + node.getPath()); + } + } + // then, recursively look for access controlled parents up the hierarchy. + if (!rootNodeId.equals(node.getId())) { + NodeImpl parentNode = (NodeImpl) node.getParent(); + collectAcls(parentNode, permissions, acls); + } + } + + private AccessControlList getACL(NodeImpl accessControlledNode, Name policyName, String path) throws RepositoryException { + // collect the aces of that node. + NodeImpl aclNode = accessControlledNode.getNode(policyName); + AccessControlList acl = new ACLTemplate(aclNode, path, allowUnknownPrincipals); + + return new UnmodifiableAccessControlList(acl); + } + + /** + * Set-up minimal permissions for the workspace: + * + *

      + *
    • 'adminstrators' principal -> all privileges
    • + *
    • 'everyone' -> read privilege
    • + *
    + * + * @param session to the workspace to set-up initial ACL to + * @param editor for the specified session. + * @throws RepositoryException If an error occurs. + */ + private static void initRootACL(SessionImpl session, AccessControlEditor editor) throws RepositoryException { + try { + log.debug("Install initial ACL:..."); + String rootPath = session.getRootNode().getPath(); + AccessControlPolicy[] acls = editor.editAccessControlPolicies(rootPath); + if (acls.length > 0) { + ACLTemplate acl = (ACLTemplate) acls[0]; + + PrincipalManager pMgr = session.getPrincipalManager(); + AccessControlManager acMgr = session.getAccessControlManager(); + + String pName = SecurityConstants.ADMINISTRATORS_NAME; + if (pMgr.hasPrincipal(pName)) { + Principal administrators = pMgr.getPrincipal(pName); + log.debug("... Privilege.ALL for administrators."); + Privilege[] privs = new Privilege[]{acMgr.privilegeFromName(Privilege.JCR_ALL)}; + acl.addAccessControlEntry(administrators, privs); + } else { + log.info("Administrators principal group is missing -> omitting initialization of default permissions."); + } + + Principal everyone = pMgr.getEveryone(); + log.debug("... Privilege.READ for everyone."); + Privilege[] privs = new Privilege[]{acMgr.privilegeFromName(Privilege.JCR_READ)}; + acl.addAccessControlEntry(everyone, privs); + + editor.setPolicy(rootPath, acl); + session.save(); + } else { + log.info("No applicable ACL available for the root node -> skip initialization of the root node's ACL."); + } + } catch (RepositoryException e) { + log.error("Failed to set-up minimal access control for root node of workspace " + session.getWorkspace().getName()); + session.getRootNode().refresh(false); + } + } + + /** + * Test if the given node is access controlled. + * + * @param node the node to be tested + * @return true if the node is access controlled. + * @throws RepositoryException if an error occurs + * @see org.apache.jackrabbit.core.NodeImpl#isAccessControllable() + */ + static boolean isAccessControlled(NodeImpl node) throws RepositoryException { + return node.isAccessControllable(); + } + + + /** + * Test if the given node is access controlled. The node is access + * controlled if it is of node type + * {@link AccessControlConstants#NT_REP_REPO_ACCESS_CONTROLLABLE "rep:RepoAccessControllable"} + * and if it has a child node named + * {@link AccessControlConstants#N_REPO_POLICY}. + * + * @param node the node to be tested + * @return true if the node is access controlled and has a + * rep:policy child; false otherwise. + * @throws RepositoryException if an error occurs + */ + static boolean isRepoAccessControlled(NodeImpl node) throws RepositoryException { + return node.hasNode(N_REPO_POLICY) && node.isNodeType(NT_REP_REPO_ACCESS_CONTROLLABLE); + } + + /** + * Returns the given targetNode unless the node itself stores + * access control information in which case it's nearest non-ac-parent is + * searched and returned. + * + * @param targetNode The node for which AC information needs to be retrieved. + * @param isAcItem true if the specified target node defines access control + * content; false otherwise. + * @return the given targetNode or the nearest non-ac-parent + * in case the targetNode itself defines access control content. + * @throws RepositoryException if an error occurs + */ + static NodeImpl getNode(NodeImpl targetNode, boolean isAcItem) throws RepositoryException { + NodeImpl node; + if (isAcItem) { + Name ntName = ((NodeTypeImpl) targetNode.getPrimaryNodeType()).getQName(); + if (ntName.equals(NT_REP_ACL)) { + node = (NodeImpl) targetNode.getParent(); + } else if (ntName.equals(NT_REP_GRANT_ACE) || ntName.equals(NT_REP_DENY_ACE)) { + node = (NodeImpl) targetNode.getParent().getParent(); + } else { + // target node already points to the nearest existing ancestor of the ac-item + node = targetNode; + } + } else { + node = targetNode; + } + return node; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplate.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplate.java new file mode 100644 index 00000000000..4a804e64e6d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplate.java @@ -0,0 +1,450 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.jcr.NamespaceException; +import javax.jcr.NodeIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractACLTemplate; +import org.apache.jackrabbit.core.security.authorization.AccessControlEntryImpl; +import org.apache.jackrabbit.core.security.authorization.PrivilegeBits; +import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.core.security.principal.UnknownPrincipal; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the + * {@link org.apache.jackrabbit.api.security.JackrabbitAccessControlList} + * interface that is detached from the effective access control content. + * Consequently, any modifications applied to this ACL only take effect, if + * the policy gets + * {@link javax.jcr.security.AccessControlManager#setPolicy(String, javax.jcr.security.AccessControlPolicy) reapplied} + * to the AccessControlManager and the changes are saved. + */ +class ACLTemplate extends AbstractACLTemplate { + + private static final Logger log = LoggerFactory.getLogger(ACLTemplate.class); + + /** + * List containing the entries of this ACL Template. + */ + private final List entries = new ArrayList(); + + /** + * The principal manager used for validation checks + */ + private final PrincipalManager principalMgr; + + /** + * The privilege mgr + */ + private final PrivilegeManagerImpl privilegeMgr; + + /** + * The name resolver + */ + private final NameResolver resolver; + + /** + * Namespace sensitive name of the REP_GLOB property in standard JCR form. + */ + private final String jcrRepGlob; + + /** + * controls if unknown principals can be used in access control entries. + */ + private final boolean allowUnknownPrincipals; + + /** + * Construct a new empty {@link ACLTemplate}. + * + * @param path path + * @param privilegeMgr + * @param valueFactory value factory + * @param resolver + * @param principalMgr manager + * @throws javax.jcr.NamespaceException + */ + ACLTemplate(String path, PrincipalManager principalMgr, + PrivilegeManager privilegeMgr, ValueFactory valueFactory, + NamePathResolver resolver, boolean allowUnknownPrincipals) throws NamespaceException { + super(path, valueFactory); + this.principalMgr = principalMgr; + this.privilegeMgr = (PrivilegeManagerImpl) privilegeMgr; + this.resolver = resolver; + this.allowUnknownPrincipals = allowUnknownPrincipals; + + jcrRepGlob = resolver.getJCRName(P_GLOB); + } + + /** + * Create a {@link ACLTemplate} that is used to edit an existing ACL + * node. + * + * @param aclNode node + * @param path The path as exposed by {@link JackrabbitAccessControlList#getPath()} + * @throws RepositoryException if an error occurs + */ + ACLTemplate(NodeImpl aclNode, String path, boolean allowUnknownPrincipals) throws RepositoryException { + super(path, (aclNode != null) ? aclNode.getSession().getValueFactory() : null); + if (aclNode == null || !NT_REP_ACL.equals(aclNode.getPrimaryNodeTypeName())) { + throw new IllegalArgumentException("Node must be of type 'rep:ACL'"); + } + SessionImpl sImpl = (SessionImpl) aclNode.getSession(); + principalMgr = sImpl.getPrincipalManager(); + privilegeMgr = (PrivilegeManagerImpl) ((JackrabbitWorkspace) sImpl.getWorkspace()).getPrivilegeManager(); + this.allowUnknownPrincipals = allowUnknownPrincipals; + + this.resolver = sImpl; + jcrRepGlob = sImpl.getJCRName(P_GLOB); + + // load the entries: + NodeIterator itr = aclNode.getNodes(); + while (itr.hasNext()) { + NodeImpl aceNode = (NodeImpl) itr.nextNode(); + try { + String principalName = aceNode.getProperty(P_PRINCIPAL_NAME).getString(); + Principal princ = principalMgr.getPrincipal(principalName); + if (princ == null) { + log.debug("Principal with name " + principalName + " unknown to PrincipalManager."); + princ = new PrincipalImpl(principalName); + } + + InternalValue[] privValues = aceNode.getProperty(P_PRIVILEGES).internalGetValues(); + Name[] privNames = new Name[privValues.length]; + for (int i = 0; i < privValues.length; i++) { + privNames[i] = privValues[i].getName(); + } + + Map restrictions = null; + if (aceNode.hasProperty(P_GLOB)) { + restrictions = Collections.singletonMap(jcrRepGlob, aceNode.getProperty(P_GLOB).getValue()); + } + // create a new ACEImpl (omitting validation check) + boolean isAllow = NT_REP_GRANT_ACE.equals(aceNode.getPrimaryNodeTypeName()); + Entry ace = new Entry(princ, privilegeMgr.getBits(privNames), isAllow, restrictions); + // add the entry omitting any validation. + entries.add(ace); + } catch (RepositoryException e) { + log.debug("Failed to build ACE from content. {}", e.getMessage()); + } + } + } + + /** + * Create a new entry omitting any validation checks. + * + * @param principal + * @param privileges + * @param isAllow + * @param restrictions + * @return A new entry + */ + Entry createEntry(Principal principal, Privilege[] privileges, boolean isAllow, Map restrictions) throws RepositoryException { + return new Entry(principal, privileges, isAllow, restrictions); + } + + Entry createEntry(Entry base, Privilege[] newPrivileges, boolean isAllow) throws RepositoryException { + return new Entry(base, newPrivileges, isAllow); + } + + private List internalGetEntries(Principal principal) { + String principalName = principal.getName(); + List entriesPerPrincipal = new ArrayList(2); + for (AccessControlEntry entry : entries) { + if (principalName.equals(entry.getPrincipal().getName())) { + entriesPerPrincipal.add((Entry) entry); + } + } + return entriesPerPrincipal; + } + + private synchronized boolean internalAdd(Entry entry) throws RepositoryException { + Principal principal = entry.getPrincipal(); + List entriesPerPrincipal = internalGetEntries(principal); + if (entriesPerPrincipal.isEmpty()) { + // simple case: just add the new entry at the end of the list. + entries.add(entry); + return true; + } else { + if (entriesPerPrincipal.contains(entry)) { + // the same entry is already contained -> no modification + return false; + } + + // check if need to adjust existing entries + int updateIndex = -1; + Entry complementEntry = null; + + for (Entry e : entriesPerPrincipal) { + if (equalRestriction(entry, e)) { + if (entry.isAllow() == e.isAllow()) { + // need to update an existing entry + if (e.getPrivilegeBits().includes(entry.getPrivilegeBits())) { + // all privileges to be granted/denied are already present + // in the existing entry -> not modified + return false; + } + + // remember the index of the existing entry to be updated later on. + updateIndex = entries.indexOf(e); + + // remove the existing entry and create a new one that + // includes both the new privileges and the existing ones. + entries.remove(e); + + PrivilegeBits mergedBits = PrivilegeBits.getInstance(e.getPrivilegeBits()); + mergedBits.add(entry.getPrivilegeBits()); + + // omit validation check. + entry = new Entry(entry, mergedBits, entry.isAllow()); + } else { + complementEntry = e; + } + } + } + + // make sure, that the complement entry (if existing) does not + // grant/deny the same privileges -> remove privileges that are now + // denied/granted. + if (complementEntry != null) { + + PrivilegeBits complPrivs = complementEntry.getPrivilegeBits(); + PrivilegeBits diff = PrivilegeBits.getInstance(complPrivs); + diff.diff(entry.getPrivilegeBits()); + + if (diff.isEmpty()) { + // remove the complement entry as the new entry covers + // all privileges granted by the existing entry. + entries.remove(complementEntry); + updateIndex--; + + } else if (!diff.equals(complPrivs)) { + // replace the existing entry having the privileges adjusted + int index = entries.indexOf(complementEntry); + entries.remove(complementEntry); + + // combine set of new builtin and custom privileges + // and create a new entry. + Entry tmpl = new Entry(entry, diff, !entry.isAllow()); + entries.add(index, tmpl); + } /* else: does not need to be modified.*/ + } + + // finally update the existing entry or add the new entry passed + // to this method at the end. + if (updateIndex < 0) { + entries.add(entry); + } else { + entries.add(updateIndex, entry); + } + return true; + } + } + + private boolean equalRestriction(Entry entry1, Entry entry2) throws RepositoryException { + Value v1 = entry1.getRestriction(jcrRepGlob); + Value v2 = entry2.getRestriction(jcrRepGlob); + + return (v1 == null) ? v2 == null : v1.equals(v2); + } + + //------------------------------------------------< AbstractACLTemplate >--- + /** + * @see AbstractACLTemplate#checkValidEntry(java.security.Principal, javax.jcr.security.Privilege[], boolean, java.util.Map) + */ + @Override + protected void checkValidEntry(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions) + throws AccessControlException { + // validate principal + if (principal instanceof UnknownPrincipal) { + log.debug("Consider fallback principal as valid: {}", principal.getName()); + } else if (!principalMgr.hasPrincipal(principal.getName())) { + if (!allowUnknownPrincipals) { + throw new AccessControlException("Principal " + principal.getName() + " does not exist."); + } + log.debug("Consider fallback principal as valid: {}", principal.getName()); + } + + if (path == null && restrictions != null && !restrictions.isEmpty()) { + throw new AccessControlException("Repository level policy does not support restrictions."); + } + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AbstractACLTemplate#getEntries() + */ + @Override + protected List getEntries() { + return entries; + } + + //--------------------------------------------------< AccessControlList >--- + /** + * @see javax.jcr.security.AccessControlList#removeAccessControlEntry(AccessControlEntry) + */ + public synchronized void removeAccessControlEntry(AccessControlEntry ace) + throws AccessControlException, RepositoryException { + if (!(ace instanceof Entry)) { + throw new AccessControlException("Invalid AccessControlEntry implementation " + ace.getClass().getName() + "."); + } + if (entries.contains(ace)) { + entries.remove(ace); + } else { + throw new AccessControlException("AccessControlEntry " + ace + " cannot be removed from ACL defined at " + getPath()); + } + } + + //----------------------------------------< JackrabbitAccessControlList >--- + /** + * @see JackrabbitAccessControlList#getRestrictionNames() + */ + public String[] getRestrictionNames() { + return (path == null) ? new String[0] : new String[] {jcrRepGlob}; + } + + /** + * @see JackrabbitAccessControlList#getRestrictionType(String) + */ + public int getRestrictionType(String restrictionName) { + if (jcrRepGlob.equals(restrictionName) || P_GLOB.toString().equals(restrictionName)) { + return PropertyType.STRING; + } else { + return PropertyType.UNDEFINED; + } + } + + @Override + public boolean isMultiValueRestriction(String restrictionName) throws RepositoryException { + return false; + } + + /** + * The only known restriction is: + *
    +     *   rep:glob (optional)  value-type: STRING
    +     * 
    + * + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean, Map) + */ + public boolean addEntry(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions) + throws AccessControlException, RepositoryException { + checkValidEntry(principal, privileges, isAllow, restrictions); + Entry ace = createEntry(principal, privileges, isAllow, restrictions); + return internalAdd(ace); + } + + //-------------------------------------------------------------< Object >--- + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return 0; + } + + /** + * Returns true if the path and the entries are equal; false otherwise. + * + * @param obj Object to be tested. + * @return true if the path and the entries are equal; false otherwise. + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ACLTemplate) { + ACLTemplate acl = (ACLTemplate) obj; + return path.equals(acl.path) && entries.equals(acl.entries); + } + return false; + } + + //-------------------------------------------------------------------------- + /** + * + */ + class Entry extends AccessControlEntryImpl { + + private Entry(Principal principal, PrivilegeBits privilegeBits, boolean allow, Map restrictions) + throws RepositoryException { + super(principal, privilegeBits, allow, restrictions); + } + + private Entry(Principal principal, Privilege[] privileges, boolean allow, Map restrictions) + throws RepositoryException { + super(principal, privileges, allow, restrictions); + } + + private Entry(Entry base, PrivilegeBits newPrivilegeBits, boolean isAllow) throws RepositoryException { + super(base, newPrivilegeBits, isAllow); + } + + private Entry(Entry base, Privilege[] newPrivileges, boolean isAllow) throws RepositoryException { + super(base, newPrivileges, isAllow); + } + + @Override + protected NameResolver getResolver() { + return resolver; + } + + @Override + protected ValueFactory getValueFactory() { + return valueFactory; + } + + @Override + protected PrivilegeManagerImpl getPrivilegeManager() { + return privilegeMgr; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/CachingEntryCollector.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/CachingEntryCollector.java new file mode 100644 index 00000000000..351550ffda1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/CachingEntryCollector.java @@ -0,0 +1,456 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.cache.GrowingLRUMap; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.AccessControlModifications; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * CachingEntryCollector extends EntryCollector by + * keeping a cache of ACEs per access controlled nodeId. + */ +class CachingEntryCollector extends EntryCollector { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(CachingEntryCollector.class); + + /** + * Cache to look up the list of access control entries defined at a given + * nodeID (key). The map only contains an entry if the corresponding Node + * is access controlled. + */ + private final EntryCache cache; + + private ConcurrentMap futures = new ConcurrentHashMap(); + private final String strategy; + private final boolean cacheNoAcl; + + /** + * Create a new instance. + * + * @param systemSession A system session. + * @param rootID The id of the root node. + * @throws RepositoryException If an error occurs. + */ + CachingEntryCollector(SessionImpl systemSession, NodeId rootID) throws RepositoryException { + super(systemSession, rootID); + cache = new EntryCache(); + + // for testing purposes, see JCR-2950 + String propname = "org.apache.jackrabbit.core.security.authorization.acl.CachingEntryCollector.strategy"; + strategy = System.getProperty(propname, "T"); + if (!("S".equals(strategy) || "T".equals(strategy) || "P".equals(strategy))) { + throw new RepositoryException("Invalid value " + strategy + " specified for system property " + propname); + } + + log.info("Cache Update Strategy: " + strategy); + + propname = "org.apache.jackrabbit.core.security.authorization.acl.CachingEntryCollector.cacheNoACL"; + cacheNoAcl = Boolean.parseBoolean(System.getProperty(propname, "false")); + + log.info("Caching entries with no ACLs: " + cacheNoAcl); + } + + @Override + protected void close() { + super.close(); + cache.clear(); + } + + //-----------------------------------------------------< EntryCollector >--- + /** + * @see EntryCollector#getEntries(org.apache.jackrabbit.core.NodeImpl) + */ + @Override + protected Entries getEntries(NodeImpl node) throws RepositoryException { + NodeId nodeId = node.getNodeId(); + Entries entries = cache.get(nodeId); + if (entries == null) { + // fetch entries and update the cache + entries = updateCache(node); + } + return entries; + } + + /** + * @see EntryCollector#getEntries(org.apache.jackrabbit.core.id.NodeId) + */ + @Override + protected Entries getEntries(NodeId nodeId) throws RepositoryException { + Entries entries = cache.get(nodeId); + if (entries == null) { + // fetch entries and update the cache + NodeImpl n = getNodeById(nodeId); + entries = updateCache(n); + } + return entries; + } + + /** + * Read the entries defined for the specified node and update the cache + * accordingly. + * + * @param node The target node + * @return The list of entries present on the specified node or an empty list. + * @throws RepositoryException If an error occurs. + */ + private Entries internalUpdateCache(NodeImpl node) throws RepositoryException { + Entries entries = super.getEntries(node); + if (cacheNoAcl || (isRootId(node.getNodeId()) && cache.specialCasesRoot()) || !entries.isEmpty()) { + // adjust the 'nextId' to point to the next access controlled + // ancestor node instead of the parent and remember the entries. + entries.setNextId(getNextID(node)); + cache.put(node.getNodeId(), entries); + } // else: not access controlled -> ignore. + return entries; + } + + /** + * Update cache for the given node id + * @param node The target node + * @return The list of entries present on the specified node or an empty list. + * @throws RepositoryException + */ + private Entries updateCache(NodeImpl node) throws RepositoryException { + if ("T".equals(strategy)) { + return throttledUpdateCache(node); + } else if ("S".equals(strategy)) { + return synchronizedUpdateCache(node); + } else if ("P".equals(strategy)) { + return parallelUpdateCache(node); + } else { + // panic + throw new RuntimeException("invalid value for updateCacheStrategy: " + strategy); + } + } + + /** + * See {@link CachingEntryCollector#updateCache(NodeImpl)} ; this variant runs fully synchronized + */ + synchronized private Entries synchronizedUpdateCache(NodeImpl node) throws RepositoryException { + return internalUpdateCache(node); + } + + /** + * See {@link CachingEntryCollector#updateCache(NodeImpl)} ; this variant runs fully parallel + */ + private Entries parallelUpdateCache(NodeImpl node) throws RepositoryException { + return internalUpdateCache(node); + } + + /** + * See {@link CachingEntryCollector#updateCache(NodeImpl)} ; this variant blocks the current + * thread if a concurrent update for the same node id takes place + */ + private Entries throttledUpdateCache(NodeImpl node) throws RepositoryException { + NodeId id = node.getNodeId(); + FutureEntries fe = null; + FutureEntries nfe = new FutureEntries(); + boolean found = true; + + fe = futures.putIfAbsent(id, nfe); + if (fe == null) { + found = false; + fe = nfe; + } + + if (found) { + // we have found a previous FutureEntries object, so use it + return fe.get(); + } else { + // otherwise obtain result and when done notify waiting FutureEntries + try { + Entries e = internalUpdateCache(node); + futures.remove(id); + fe.setResult(e); + return e; + } catch (Throwable problem) { + futures.remove(id); + fe.setProblem(problem); + if (problem instanceof RepositoryException) { + throw (RepositoryException)problem; + } else { + throw new RuntimeException(problem); + } + } + } + } + + /** + * Find the next access control ancestor in the hierarchy 'null' indicates + * that there is no ac-controlled ancestor. + * + * @param node The target node for which the cache needs to be updated. + * @return The NodeId of the next access controlled ancestor in the hierarchy + * or null + */ + private NodeId getNextID(NodeImpl node) throws RepositoryException { + NodeImpl n = node; + NodeId nextId = null; + while (nextId == null && !isRootId(n.getNodeId())) { + NodeId parentId = n.getParentId(); + if (cache.containsKey(parentId)) { + nextId = parentId; + } else { + NodeImpl parent = (NodeImpl) n.getParent(); + if (hasEntries(parent)) { + nextId = parentId; + } else { + // try next ancestor + n = parent; + } + } + } + return nextId; + } + + /** + * Returns {@code true} if the specified {@code nodeId} is the ID of the + * root node; false otherwise. + * + * @param nodeId The identifier of the node to be tested. + * @return {@code true} if the given id is the identifier of the root node. + */ + private boolean isRootId(NodeId nodeId) { + return rootID.equals(nodeId); + } + + /** + * Evaluates if the given node is access controlled and holds a non-empty + * rep:policy child node. + * + * @param n The node to test. + * @return true if the specified node is access controlled and holds a + * non-empty policy child node. + * @throws RepositoryException If an error occurs. + */ + private static boolean hasEntries(NodeImpl n) throws RepositoryException { + if (ACLProvider.isAccessControlled(n)) { + NodeImpl aclNode = n.getNode(N_POLICY); + return aclNode.hasNodes(); + } + + // no ACL defined here + return false; + } + + /** + * @see EntryCollector#notifyListeners(org.apache.jackrabbit.core.security.authorization.AccessControlModifications) + */ + @Override + @SuppressWarnings("unchecked") + public void notifyListeners(AccessControlModifications modifications) { + /* Update cache for all affected access controlled nodes */ + for (Object key : modifications.getNodeIdentifiers()) { + if (!(key instanceof NodeId)) { + log.warn("Cannot process AC modificationMap entry. Keys must be NodeId."); + continue; + } + NodeId nodeId = (NodeId) key; + int type = modifications.getType(nodeId); + if ((type & POLICY_ADDED) == POLICY_ADDED) { + // clear the complete cache since the nextAcNodeId may + // have changed due to the added ACL. + log.debug("Policy added, clearing the cache"); + cache.clear(); + break; // no need for further processing. + } else if ((type & POLICY_REMOVED) == POLICY_REMOVED) { + // clear the entry and change the entries having a nextID + // pointing to this node. + cache.remove(nodeId, true); + } else if ((type & POLICY_MODIFIED) == POLICY_MODIFIED) { + // simply clear the cache entry -> reload upon next access. + cache.remove(nodeId, false); + } else if ((type & MOVE) == MOVE) { + // some sort of move operation that may affect the cache + log.debug("Move operation, clearing the cache"); + cache.clear(); + break; // no need for further processing. + } + } + super.notifyListeners(modifications); + } + + /** + * A place holder for a yet to be computed {@link Entries} result + */ + private class FutureEntries { + + private boolean ready = false; + private Entries result = null; + private Throwable problem = null; + + synchronized public Entries get() throws RepositoryException { + while (!ready) { + try { + wait(); + } catch (InterruptedException e) { + } + } + if (problem != null) { + if (problem instanceof RepositoryException) { + throw new RepositoryException(problem); + } else { + throw new RuntimeException(problem); + } + } + return result; + } + + synchronized public void setResult(Entries e) { + result = e; + ready = true; + notifyAll(); + } + + synchronized public void setProblem(Throwable t) { + problem = t; + ready = true; + notifyAll(); + } + } + + /** + * A cache to lookup the ACEs defined on a given (access controlled) + * node. The internal map uses the ID of the node as key while the value + * consists of {@Entries} objects that not only provide the ACEs defined + * for that node but also the ID of the next access controlled parent node. + */ + private class EntryCache { + + private final Map cache; + private Entries rootEntries; + private boolean specialCaseRoot = true; + + @SuppressWarnings("unchecked") + public EntryCache() { + int maxsize = 5000; + String propname = "org.apache.jackrabbit.core.security.authorization.acl.CachingEntryCollector.maxsize"; + try { + maxsize = Integer.parseInt(System.getProperty(propname, Integer.toString(maxsize))); + } catch (NumberFormatException ex) { + log.debug("Parsing system property " + propname + " with value: " + System.getProperty(propname), ex); + } + + log.info("Creating cache with max size of: " + maxsize); + + cache = new GrowingLRUMap(1024, maxsize); + + String propsrname = "org.apache.jackrabbit.core.security.authorization.acl.CachingEntryCollector.scroot"; + specialCaseRoot = Boolean.parseBoolean(System.getProperty(propsrname, "true")); + + log.info("Root is special-cased: " + specialCaseRoot); + } + + public boolean specialCasesRoot() { + return specialCaseRoot; + } + + public boolean containsKey(NodeId id) { + if (specialCaseRoot && isRootId(id)) { + return rootEntries != null; + } else { + synchronized (cache) { + return cache.containsKey(id); + } + } + } + + public void clear() { + rootEntries = null; + synchronized (cache) { + cache.clear(); + } + } + + public Entries get(NodeId id) { + Entries result; + + if (specialCaseRoot && isRootId(id)) { + result = rootEntries; + } else { + synchronized (cache) { + result = cache.get(id); + } + } + + if (result != null) { + log.debug("Cache hit for nodeId {}", id); + } else { + log.debug("Cache miss for nodeId {}", id); + } + + return result; + } + + public void put(NodeId id, Entries entries) { + log.debug("Updating cache for nodeId {}", id); + + // fail early on potential cache corruption + if (id.equals(entries.getNextId())) { + throw new IllegalArgumentException("Trying to update cache entry for " + id + " with a circular reference"); + } + + if (specialCaseRoot && isRootId(id)) { + rootEntries = entries; + } else { + synchronized (cache) { + cache.put(id, entries); + } + } + } + + public void remove(NodeId id, boolean adjustNextIds) { + log.debug("Removing nodeId {} from cache", id); + Entries result; + synchronized (cache) { + if (specialCaseRoot && isRootId(id)) { + result = rootEntries; + rootEntries = null; + } else { + result = cache.remove(id); + } + + if (adjustNextIds && result != null) { + NodeId nextId = result.getNextId(); + for (Entries entry : cache.values()) { + if (id.equals(entry.getNextId())) { + // fail early on potential cache corruption + if (id.equals(nextId)) { + throw new IllegalArgumentException("Trying to update cache entry for " + id + " with a circular reference"); + } + entry.setNextId(nextId); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/CompiledPermissionsImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/CompiledPermissionsImpl.java new file mode 100644 index 00000000000..45e66ac2485 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/CompiledPermissionsImpl.java @@ -0,0 +1,297 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.core.cache.GrowingLRUMap; +import org.apache.jackrabbit.core.ItemImpl; +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.security.authorization.AbstractCompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.AccessControlListener; +import org.apache.jackrabbit.core.security.authorization.AccessControlModifications; +import org.apache.jackrabbit.core.security.authorization.AccessControlUtils; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.PrivilegeBits; +import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * CompiledPermissionsImpl... + */ +class CompiledPermissionsImpl extends AbstractCompiledPermissions implements AccessControlListener { + + private static final Logger LOG = LoggerFactory.getLogger(CompiledPermissionsImpl.class); + + public static final int DEFAULT_MAX_CACHE_SIZE = 5000; + + public static final int MAX_CACHE_SIZE = Integer.getInteger( + "org.apache.jackrabbit.core.security.authorization.acl.CompiledPermissionsImpl.cacheSize", + DEFAULT_MAX_CACHE_SIZE + ); + + private final List principalNames; + private final SessionImpl session; + private final EntryCollector entryCollector; + private final AccessControlUtils util; + + /* + * Start with initial map size of 1024 and grow up to 5000 before + * removing LRU items. + */ + @SuppressWarnings("unchecked") + private final Map readCache = new GrowingLRUMap(1024, MAX_CACHE_SIZE); + + private final Object monitor = new Object(); + + CompiledPermissionsImpl(Set principals, SessionImpl session, + EntryCollector entryCollector, AccessControlUtils util, + boolean listenToEvents) throws RepositoryException { + this.session = session; + this.entryCollector = entryCollector; + this.util = util; + + principalNames = new ArrayList(principals.size()); + for (Principal princ : principals) { + principalNames.add(princ.getName()); + } + + if (listenToEvents) { + /* + Make sure this AclPermission recalculates the permissions if + any ACL concerning it is modified. + */ + entryCollector.addListener(this); + } + + LOG.debug("Read permission cache size = {}", MAX_CACHE_SIZE); + } + + private Result buildResult(NodeImpl node, boolean isExistingNode, + boolean isAcItem, EntryFilterImpl filter) throws RepositoryException { + // retrieve all ACEs at path or at the direct ancestor of path that + // apply for the principal names. + NodeImpl n = ACLProvider.getNode(node, isAcItem); + Iterator entries = entryCollector.collectEntries(n, filter).iterator(); + + /* + Calculate privileges and permissions: + Since the ACEs only define privileges on a node and do not allow + to add additional restrictions, the permissions can be determined + without taking the given target name or target item into account. + */ + int allows = Permission.NONE; + int denies = Permission.NONE; + + PrivilegeBits allowBits = PrivilegeBits.getInstance(); + PrivilegeBits denyBits = PrivilegeBits.getInstance(); + PrivilegeBits parentAllowBits = PrivilegeBits.getInstance(); + PrivilegeBits parentDenyBits = PrivilegeBits.getInstance(); + + String parentPath = Text.getRelativeParent(filter.getPath(), 1); + NodeId nodeId = (node == null) ? null : node.getNodeId(); + + while (entries.hasNext()) { + Entry ace = entries.next(); + /* + Determine if the ACE also takes effect on the parent: + Some permissions (e.g. add-node or removal) must be determined + from privileges defined for the parent. + A 'local' entry defined on the target node never effects the + parent. For inherited ACEs determine if the ACE matches the + parent path. + */ + PrivilegeBits entryBits = ace.getPrivilegeBits(); + boolean isLocal = isExistingNode && ace.isLocal(nodeId); + boolean matchesParent = (!isLocal && ace.matches(parentPath)); + if (matchesParent) { + if (ace.isAllow()) { + parentAllowBits.addDifference(entryBits, parentDenyBits); + } else { + parentDenyBits.addDifference(entryBits, parentAllowBits); + } + } + if (ace.isAllow()) { + allowBits.addDifference(entryBits, denyBits); + int permissions = PrivilegeRegistry.calculatePermissions(allowBits, parentAllowBits, true, isAcItem); + allows |= Permission.diff(permissions, denies); + } else { + denyBits.addDifference(entryBits, allowBits); + int permissions = PrivilegeRegistry.calculatePermissions(denyBits, parentDenyBits, false, isAcItem); + denies |= Permission.diff(permissions, allows); + } + } + + return new Result(allows, denies, allowBits, denyBits); + } + + //------------------------------------< AbstractCompiledPermissions >--- + /** + * @see AbstractCompiledPermissions#buildResult(org.apache.jackrabbit.spi.Path) + */ + @Override + protected Result buildResult(Path absPath) throws RepositoryException { + boolean existingNode = false; + NodeImpl node; + + ItemManager itemMgr = session.getItemManager(); + try { + ItemImpl item = itemMgr.getItem(absPath); + if (item.isNode()) { + node = (NodeImpl) item; + existingNode = true; + } else { + node = (NodeImpl) item.getParent(); + } + } catch (RepositoryException e) { + // path points to a non-persisted item. + // -> find the nearest persisted node starting from the root. + Path.Element[] elems = absPath.getElements(); + NodeImpl parent = (NodeImpl) session.getRootNode(); + for (int i = 1; i < elems.length - 1; i++) { + Name name = elems[i].getName(); + int index = elems[i].getIndex(); + if (!parent.hasNode(name, index)) { + // last persisted node reached + break; + } + parent = parent.getNode(name, index); + + } + node = parent; + } + + if (node == null) { + // should never get here + throw new ItemNotFoundException("Item out of hierarchy."); + } + + boolean isAcItem = util.isAcItem(absPath); + return buildResult(node, existingNode, isAcItem, new EntryFilterImpl(principalNames, absPath, session)); + } + + @Override + protected Result buildRepositoryResult() throws RepositoryException { + return buildResult(null, true, false, new EntryFilterImpl(principalNames, session.getQPath("/"), session)); + } + + /** + * @see AbstractCompiledPermissions#getPrivilegeManagerImpl() + */ + @Override + protected PrivilegeManagerImpl getPrivilegeManagerImpl() throws RepositoryException { + return (PrivilegeManagerImpl) ((JackrabbitWorkspace) session.getWorkspace()).getPrivilegeManager(); + } + + /** + * @see AbstractCompiledPermissions#clearCache() + */ + @Override + protected void clearCache() { + synchronized (monitor) { + readCache.clear(); + } + super.clearCache(); + } + + //--------------------------------------------< CompiledPermissions >--- + /** + * @see org.apache.jackrabbit.core.security.authorization.CompiledPermissions#close() + */ + @Override + public void close() { + entryCollector.removeListener(this); + // NOTE: do not logout shared session. + super.close(); + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.CompiledPermissions#canRead(Path, ItemId) + */ + public boolean canRead(Path path, ItemId itemId) throws RepositoryException { + ItemId id = (itemId == null) ? session.getHierarchyManager().resolvePath(path) : itemId; + // no extra check for existence as method may only be called for existing items. + boolean isExistingNode = id.denotesNode(); + boolean canRead = false; + synchronized (monitor) { + if (readCache.containsKey(id)) { + canRead = readCache.get(id); + } else { + ItemManager itemMgr = session.getItemManager(); + NodeId nodeId = (isExistingNode) ? (NodeId) id : ((PropertyId) id).getParentId(); + NodeImpl node = (NodeImpl) itemMgr.getItem(nodeId); + + boolean isAcItem = util.isAcItem(node); + EntryFilterImpl filter; + if (path == null) { + filter = new EntryFilterImpl(principalNames, id, session); + } else { + filter = new EntryFilterImpl(principalNames, path, session); + } + + if (isAcItem) { + /* item defines ac content -> regular evaluation */ + Result result = buildResult(node, isExistingNode, isAcItem, filter); + canRead = result.grants(Permission.READ); + } else { + /* + simplified evaluation focusing on READ permission. this allows + to omit evaluation of parent node permissions that are + required when calculating the complete set of permissions + (see special treatment of remove, create or ac-specific + permissions). + */ + for (Entry ace : entryCollector.collectEntries(node, filter)) { + if (ace.getPrivilegeBits().includesRead()) { + canRead = ace.isAllow(); + break; + } + } + } + readCache.put(id, canRead); + } + } + return canRead; + } + + //----------------------------------------< ACLModificationListener >--- + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlListener#acModified(org.apache.jackrabbit.core.security.authorization.AccessControlModifications) + */ + public void acModified(AccessControlModifications modifications) { + // ignore the details of the modifications and clear all caches. + clearCache(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/Entry.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/Entry.java new file mode 100644 index 00000000000..538f7534c3f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/Entry.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.core.security.authorization.acl; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.GlobPattern; +import org.apache.jackrabbit.core.security.authorization.PrivilegeBits; +import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Entry... TODO + */ +class Entry implements AccessControlConstants { + + private static final Logger log = LoggerFactory.getLogger(Entry.class); + + private final String principalName; + private final boolean isGroupEntry; + private final PrivilegeBits privilegeBits; + private final boolean isAllow; + private final NodeId id; + private final GlobPattern pattern; + private final boolean hasRestrictions; + + private int hashCode; + + private Entry(NodeId id, String principalName, boolean isGroupEntry, + PrivilegeBits privilegeBits, boolean allow, String path, Value globValue) throws RepositoryException { + + this.principalName = principalName; + this.isGroupEntry = isGroupEntry; + this.privilegeBits = privilegeBits; + this.isAllow = allow; + this.id = id; + this.pattern = calculatePattern(path, globValue); + this.hasRestrictions = (globValue != null); + } + + static List readEntries(NodeImpl aclNode, String path) throws RepositoryException { + if (aclNode == null || !NT_REP_ACL.equals(aclNode.getPrimaryNodeTypeName())) { + throw new IllegalArgumentException("Node must be of type 'rep:ACL'"); + } + SessionImpl sImpl = (SessionImpl) aclNode.getSession(); + PrincipalManager principalMgr = sImpl.getPrincipalManager(); + PrivilegeManagerImpl privilegeMgr = (PrivilegeManagerImpl) ((JackrabbitWorkspace) sImpl.getWorkspace()).getPrivilegeManager(); + + NodeId nodeId = aclNode.getParentId(); + + List entries = new ArrayList(); + // load the entries: + NodeIterator itr = aclNode.getNodes(); + while (itr.hasNext()) { + NodeImpl aceNode = (NodeImpl) itr.nextNode(); + try { + String principalName = aceNode.getProperty(P_PRINCIPAL_NAME).getString(); + boolean isGroupEntry = false; + Principal princ = principalMgr.getPrincipal(principalName); + if (princ != null) { + isGroupEntry = GroupPrincipals.isGroup(princ); + } + + InternalValue[] privValues = aceNode.getProperty(P_PRIVILEGES).internalGetValues(); + Name[] privNames = new Name[privValues.length]; + for (int i = 0; i < privValues.length; i++) { + privNames[i] = privValues[i].getName(); + } + + Value globValue = null; + if (aceNode.hasProperty(P_GLOB)) { + globValue = aceNode.getProperty(P_GLOB).getValue(); + } + + boolean isAllow = NT_REP_GRANT_ACE.equals(aceNode.getPrimaryNodeTypeName()); + Entry ace = new Entry(nodeId, principalName, isGroupEntry, privilegeMgr.getBits(privNames), isAllow, path, globValue); + entries.add(ace); + } catch (RepositoryException e) { + log.debug("Failed to build ACE from content. {}", e.getMessage()); + } + } + + return entries; + } + + private static GlobPattern calculatePattern(String path, Value globValue) throws RepositoryException { + if (path == null) { + return null; + } else { + if (globValue == null) { + return GlobPattern.create(path); + } else { + return GlobPattern.create(path, globValue.getString()); + } + } + } + + /** + * @param nodeId + * @return true if this entry is defined on the node + * at nodeId + */ + boolean isLocal(NodeId nodeId) { + return id != null && id.equals(nodeId); + } + + /** + * + * @param jcrPath + * @return + */ + boolean matches(String jcrPath) { + return pattern != null && pattern.matches(jcrPath); + } + + PrivilegeBits getPrivilegeBits() { + return privilegeBits; + } + + boolean isAllow() { + return isAllow; + } + + String getPrincipalName() { + return principalName; + } + + boolean isGroupEntry() { + return isGroupEntry; + } + + boolean hasRestrictions() { + return hasRestrictions; + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + if (hashCode == -1) { + int h = 17; + h = 37 * h + principalName.hashCode(); + h = 37 * h + privilegeBits.hashCode(); + h = 37 * h + Boolean.valueOf(isAllow).hashCode(); + h = 37 * h + pattern.hashCode(); + hashCode = h; + } + return hashCode; + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Entry) { + Entry other = (Entry) obj; + return principalName.equals(other.principalName) && + privilegeBits.equals(other.privilegeBits) && + isAllow == other.isAllow && + pattern.equals(other.pattern); + } + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryCollector.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryCollector.java new file mode 100644 index 00000000000..887ff037919 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryCollector.java @@ -0,0 +1,472 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.AccessControlModifications; +import org.apache.jackrabbit.core.security.authorization.AccessControlObserver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * EntryCollector collects ACEs defined and effective for a + * given Node and listens to access control modifications in order + * to inform listeners. + */ +public class EntryCollector extends AccessControlObserver implements AccessControlConstants { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(EntryCollector.class); + + /** + * The system session used to register an event listener and process the + * events as well as collect AC entries. + */ + protected final SessionImpl systemSession; + + /** + * The root id. + */ + protected final NodeId rootID; + + private final EventListener moveListener; + + /** + * + * @param systemSession + * @param rootID + * @throws RepositoryException + */ + protected EntryCollector(SessionImpl systemSession, NodeId rootID) throws RepositoryException { + this.systemSession = systemSession; + this.rootID = rootID; + + ObservationManager observationMgr = systemSession.getWorkspace().getObservationManager(); + /* + Make sure the collector and all subscribed listeners are informed upon + ACL modifications. Interesting events are: + - new ACL (NODE_ADDED) + - new ACE (NODE_ADDED) + - changing ACE (PROPERTY_CHANGED) + - removed ACL (NODE_REMOVED) + - removed ACE (NODE_REMOVED) + */ + int events = Event.PROPERTY_CHANGED | Event.NODE_ADDED | Event.NODE_REMOVED; + String[] ntNames = new String[] { + systemSession.getJCRName(NT_REP_ACCESS_CONTROLLABLE), + systemSession.getJCRName(NT_REP_ACL), + systemSession.getJCRName(NT_REP_ACE) + }; + String rootPath = systemSession.getRootNode().getPath(); + observationMgr.addEventListener(this, events, rootPath, true, null, ntNames, true); + /* + In addition both the collector and all subscribed listeners should be + informed about any kind of move events. + */ + moveListener = new MoveListener(); + observationMgr.addEventListener(moveListener, Event.NODE_MOVED, rootPath, true, null, null, true); + } + + /** + * Release all resources contained by this instance. It will no longer be + * used. This implementation only stops listening to ac modification events. + */ + @Override + protected void close() { + super.close(); + try { + ObservationManager observationMgr = systemSession.getWorkspace().getObservationManager(); + observationMgr.removeEventListener(this); + observationMgr.removeEventListener(moveListener); + } catch (RepositoryException e) { + log.error("Unexpected error while closing CachingEntryCollector", e); + } + } + + /** + * Collect the ACEs effective at the given node applying the specified + * filter. + * + * @param node + * @param filter + * @return + * @throws RepositoryException + */ + protected List collectEntries(NodeImpl node, EntryFilter filter) throws RepositoryException { + LinkedList userAces = new LinkedList(); + LinkedList groupAces = new LinkedList(); + + if (node == null) { + // repository level permissions + NodeImpl root = (NodeImpl) systemSession.getRootNode(); + if (ACLProvider.isRepoAccessControlled(root)) { + NodeImpl aclNode = root.getNode(N_REPO_POLICY); + filterEntries(filter, Entry.readEntries(aclNode, null), userAces, groupAces); + } + } else { + filterEntries(filter, getEntries(node).getACEs(), userAces, groupAces); + NodeId next = node.getParentId(); + while (next != null) { + Entries entries = getEntries(next); + filterEntries(filter, entries.getACEs(), userAces, groupAces); + next = entries.getNextId(); + } + } + + List entries = new ArrayList(userAces.size() + groupAces.size()); + entries.addAll(userAces); + entries.addAll(groupAces); + + return entries; + } + + /** + * Filter the specified access control entries + * + * @param filter + * @param aces + * @param userAces + * @param groupAces + */ + @SuppressWarnings("unchecked") + private static void filterEntries(EntryFilter filter, List aces, + LinkedList userAces, + LinkedList groupAces) { + if (!aces.isEmpty() && filter != null) { + filter.filterEntries(aces, userAces, groupAces); + } + } + + /** + * Retrieve the access control entries defined for the given node. If the + * node is not access controlled or if the ACL is empty this method returns + * an empty list. + * + * @param node + * @return + * @throws RepositoryException + */ + protected Entries getEntries(NodeImpl node) throws RepositoryException { + List aces; + if (ACLProvider.isAccessControlled(node)) { + // collect the aces of that node. + NodeImpl aclNode = node.getNode(N_POLICY); + aces = Entry.readEntries(aclNode, node.getPath()); + } else { + // not access controlled + aces = Collections.emptyList(); + } + return new Entries(aces, node.getParentId()); + } + + /** + * + * @param nodeId + * @return + * @throws RepositoryException + */ + protected Entries getEntries(NodeId nodeId) throws RepositoryException { + NodeImpl node = getNodeById(nodeId); + return getEntries(node); + } + + /** + * + * @param nodeId + * @return + * @throws javax.jcr.RepositoryException + */ + NodeImpl getNodeById(NodeId nodeId) throws RepositoryException { + return ((NodeImpl) systemSession.getItemManager().getItem(nodeId)); + } + + //------------------------------------------------------< EventListener >--- + /** + * Collects access controlled nodes that are effected by access control + * changes together with the corresponding modification types, and + * notifies access control listeners about the modifications. + * + * @param events + */ + public void onEvent(EventIterator events) { + try { + // JCR-2890: We need to use a fresh new session here to avoid + // deadlocks caused by concurrent threads possibly using the + // systemSession instance for other purposes. + String workspaceName = systemSession.getWorkspace().getName(); + Session session = systemSession.createSession(workspaceName); + try { + // Sift through the events to find access control modifications + ACLEventSieve sieve = new ACLEventSieve(session, (NameResolver) session); + sieve.siftEvents(events); + + // Notify listeners and eventually clean up internal caches + AccessControlModifications mods = sieve.getModifications(); + if (!mods.getNodeIdentifiers().isEmpty()) { + notifyListeners(mods); + } + } finally { + session.logout(); + } + } catch (RepositoryException e) { + log.error("Failed to process access control modifications", e); + } + } + + //--------------------------------------------------------< inner class >--- + /** + * Private utility class for sifting through observation events on + * ACL, ACE and Policy nodes to find out the nodes whose access controls + * have changed. Used by the {@link EntryCollector#onEvent(EventIterator)} + * method. + */ + private static class ACLEventSieve { + + /** Session with system privileges. */ + private final Session session; + + /** + * Standard JCR name form of the + * {@link AccessControlConstants#N_POLICY} constant. + */ + private final String repPolicyName; + + /** + * Map of access-controlled nodeId to type of access control modification. + */ + private final Map modMap = new HashMap(); + + private ACLEventSieve(Session session, NameResolver resolver) throws RepositoryException { + this.session = session; + this.repPolicyName = resolver.getJCRName(AccessControlConstants.N_POLICY); + } + + /** + * Collects the identifiers of all access controlled nodes that have + * been affected by the events, and thus need their cache entries + * updated or cleared. + * + * @param events access control modification events + */ + private void siftEvents(EventIterator events) { + while (events.hasNext()) { + Event event = events.nextEvent(); + try { + switch (event.getType()) { + case Event.NODE_ADDED: + siftNodeAdded(event.getIdentifier()); + break; + case Event.NODE_REMOVED: + siftNodeRemoved(event.getPath()); + break; + case Event.PROPERTY_CHANGED: + siftPropertyChanged(event.getIdentifier()); + break; + default: + // illegal event-type: should never occur. ignore + } + } catch (RepositoryException e) { + // should not get here + log.warn("Failed to process ACL event: " + event, e); + } + } + } + + /** + * Returns the access control modifications collected from + * related observation events. + * + * @return access control modifications + */ + private AccessControlModifications getModifications() { + return new AccessControlModifications(modMap); + } + + private void siftNodeAdded(String identifier) throws RepositoryException { + try { + NodeImpl n = (NodeImpl) session.getNodeByIdentifier(identifier); + if (n.isNodeType(EntryCollector.NT_REP_ACL)) { + // a new ACL was added -> use the added node to update + // the cache. + addModification( + accessControlledIdFromAclNode(n), + AccessControlObserver.POLICY_ADDED); + } else if (n.isNodeType(EntryCollector.NT_REP_ACE)) { + // a new ACE was added -> use the parent node (acl) + // to update the cache. + addModification( + accessControlledIdFromAceNode(n), + AccessControlObserver.POLICY_MODIFIED); + } /* else: some other node added below an access controlled + parent node -> not interested. */ + } catch (ItemNotFoundException e) { + log.debug("Cannot process NODE_ADDED event. Node {} doesn't exist (anymore).", identifier); + } + } + + private void siftNodeRemoved(String path) throws RepositoryException { + String parentPath = Text.getRelativeParent(path, 1); + if (session.nodeExists(parentPath)) { + NodeImpl parent = (NodeImpl) session.getNode(parentPath); + if (repPolicyName.equals(Text.getName(path))){ + // the complete ACL was removed -> clear cache entry + addModification( + parent.getNodeId(), + AccessControlObserver.POLICY_REMOVED); + } else if (parent.isNodeType(EntryCollector.NT_REP_ACL)) { + // an ace was removed -> refresh cache for the + // containing access control list upon next access + addModification( + accessControlledIdFromAclNode(parent), + AccessControlObserver.POLICY_MODIFIED); + } /* else: + a) some other child node of an access controlled + node -> not interested. + b) a child node of an ACE. not relevant for this + implementation -> ignore + */ + } else { + log.debug("Cannot process NODE_REMOVED event. Parent {} doesn't exist (anymore).", parentPath); + } + } + + private void siftPropertyChanged(String identifier) throws RepositoryException { + try { + // test if the changed prop belongs to an ACE + NodeImpl parent = (NodeImpl) session.getNodeByIdentifier(identifier); + if (parent.isNodeType(EntryCollector.NT_REP_ACE)) { + addModification( + accessControlledIdFromAceNode(parent), + AccessControlObserver.POLICY_MODIFIED); + } /* some other property below an access controlled node + changed -> not interested. (NOTE: rep:ACL doesn't + define any properties. */ + } catch (ItemNotFoundException e) { + log.debug("Cannot process PROPERTY_CHANGED event. Node {} doesn't exist (anymore).", identifier); + } + } + + private NodeId accessControlledIdFromAclNode(Node aclNode) throws RepositoryException { + return ((NodeImpl) aclNode.getParent()).getNodeId(); + } + + private NodeId accessControlledIdFromAceNode(Node aceNode) throws RepositoryException { + return accessControlledIdFromAclNode(aceNode.getParent()); + } + + private void addModification(NodeId accessControllNodeId, int modType) { + if (modMap.containsKey(accessControllNodeId)) { + // update modMap + modType |= modMap.get(accessControllNodeId); + } + modMap.put(accessControllNodeId, modType); + } + } + + /** + * Listening to any kind of move events in the hierarchy. Since ac content + * is associated with individual nodes the caches need to be informed about + * any kind of move as well even if the target node is not access control + * content s.str. + */ + private class MoveListener implements SynchronousEventListener { + + public void onEvent(EventIterator events) { + // NOTE: simplified event handling as all listeners just clear + // the cache in case of any move event. therefore there is currently + // no need to process all events and using the rootID as marker. + while (events.hasNext()) { + Event event = events.nextEvent(); + if (event.getType() == Event.NODE_MOVED) { + Map m = Collections.singletonMap(rootID, AccessControlObserver.MOVE); + AccessControlModifications mods = new AccessControlModifications(m); + notifyListeners(mods); + break; + } //else: illegal event-type: should never occur. ignore + } + } + } + + //-------------------------------------------------------------------------- + /** + * Inner class combining a list of access control entries with the information + * where to start looking for inherited entries. + * + * Thus nextId either points to the parent of the access + * controlled node associated with aces or to the next + * access controlled ancestor. It is null if the root node has + * been reached and there is no additional ancestor to retrieve access control + * entries from. + */ + static class Entries { + + private final List aces; + private NodeId nextId; + + Entries(List aces, NodeId nextId) { + this.aces = aces; + this.nextId = nextId; + } + + List getACEs() { + return aces; + } + + NodeId getNextId() { + return nextId; + } + + void setNextId(NodeId nextId) { + this.nextId = nextId; + } + + boolean isEmpty() { + return aces.isEmpty(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("size = ").append(aces.size()).append(", "); + sb.append("nextNodeId = ").append(nextId); + return sb.toString(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryFilter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryFilter.java new file mode 100644 index 00000000000..2db93f9abd2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryFilter.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.core.security.authorization.acl; + +import java.util.List; + +/** + * EntryFilter... + */ +public interface EntryFilter { + + void filterEntries(List entries, List... resultLists); + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryFilterImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryFilterImpl.java new file mode 100644 index 00000000000..b298f7a3f63 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/acl/EntryFilterImpl.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.core.security.authorization.acl; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import java.util.Collection; +import java.util.List; + +/** + * EntryFilterImpl... + */ +class EntryFilterImpl implements EntryFilter { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(EntryFilterImpl.class); + + private final Collection principalNames; + private final PathProvider pathProvider; + + private String itemPath; + + EntryFilterImpl(Collection principalNames, final ItemId id, final SessionImpl sessionImpl) { + this.principalNames = principalNames; + this.pathProvider = new PathProvider() { + public String getPath() throws RepositoryException { + Path p = sessionImpl.getHierarchyManager().getPath(id); + return sessionImpl.getJCRPath(p); + } + }; + } + + EntryFilterImpl(Collection principalNames, final Path absPath, final PathResolver pathResolver) { + this.principalNames = principalNames; + this.pathProvider = new PathProvider() { + public String getPath() throws RepositoryException { + return pathResolver.getJCRPath(absPath); + } + }; + } + + /** + * Separately collect the entries defined for the user and group + * principals. + * + * @param entries + * @param resultLists + * @see EntryFilter#filterEntries(java.util.List, java.util.List[]) + */ + public void filterEntries(List entries, List... resultLists) { + if (resultLists.length == 2) { + List userAces = resultLists[0]; + List groupAces = resultLists[1]; + + int uInsertIndex = userAces.size(); + int gInsertIndex = groupAces.size(); + + // first collect aces present on the given aclNode. + for (Entry ace : entries) { + // only process ace if 'principalName' is contained in the given set + if (matches(ace)) { + // add it to the proper list (e.g. separated by principals) + /** + * NOTE: access control entries must be collected in reverse + * order in order to assert proper evaluation. + */ + if (ace.isGroupEntry()) { + groupAces.add(gInsertIndex, ace); + } else { + userAces.add(uInsertIndex, ace); + } + } + } + } else { + log.warn("Filtering aborted. Expected 2 result lists."); + } + } + + private boolean matches(Entry entry) { + if (principalNames == null || principalNames.contains(entry.getPrincipalName())) { + if (!entry.hasRestrictions()) { + // short cut: there is no glob-restriction -> the entry matches + // because it is either defined on the node or inherited. + return true; + } else { + // there is a glob-restriction: check if the target path matches + // this entry. + try { + return entry.matches(getPath()); + } catch (RepositoryException e) { + log.error("Cannot determine ACE match.", e); + } + } + } + + // doesn't match this filter -> ignore + return false; + } + + String getPath() throws RepositoryException { + if (itemPath == null) { + itemPath = pathProvider.getPath(); + } + return itemPath; + } + + //-------------------------------------------------------------------------- + /** + * Interface for lazy calculation of the JCR path used for evaluation of ACE + * matching in case of entries defining restriction(s). + */ + private interface PathProvider { + + String getPath() throws RepositoryException; + + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/combined/CombinedEditor.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/combined/CombinedEditor.java new file mode 100644 index 00000000000..b6b9cfeb74a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/combined/CombinedEditor.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.core.security.authorization.combined; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; +import org.apache.jackrabbit.core.security.authorization.AccessControlEditor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlPolicy; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * CombinedEditor... + */ +class CombinedEditor implements AccessControlEditor { + + private static Logger log = LoggerFactory.getLogger(CombinedEditor.class); + + private final AccessControlEditor[] editors; + + CombinedEditor(AccessControlEditor[] editors) { + this.editors = editors; + } + + //------------------------------------------------< AccessControlEditor >--- + /** + * @see AccessControlEditor#getPolicies(String) + */ + public AccessControlPolicy[] getPolicies(String nodePath) throws AccessControlException, PathNotFoundException, RepositoryException { + List templates = new ArrayList(); + for (AccessControlEditor editor : editors) { + AccessControlPolicy[] ts = editor.getPolicies(nodePath); + if (ts != null && ts.length > 0) { + templates.addAll(Arrays.asList(ts)); + } + } + return templates.toArray(new AccessControlPolicy[templates.size()]); + } + + /** + * @see AccessControlEditor#getPolicies(Principal) + */ + public JackrabbitAccessControlPolicy[] getPolicies(Principal principal) throws AccessControlException, RepositoryException { + List templates = new ArrayList(); + for (AccessControlEditor editor : editors) { + JackrabbitAccessControlPolicy[] ts = editor.getPolicies(principal); + if (ts != null && ts.length > 0) { + templates.addAll(Arrays.asList(ts)); + } + } + return templates.toArray(new JackrabbitAccessControlPolicy[templates.size()]); + } + + /** + * @see AccessControlEditor#editAccessControlPolicies(String) + */ + public AccessControlPolicy[] editAccessControlPolicies(String nodePath) throws AccessControlException, PathNotFoundException, RepositoryException { + List templates = new ArrayList(); + for (AccessControlEditor editor : editors) { + try { + templates.addAll(Arrays.asList(editor.editAccessControlPolicies(nodePath))); + } catch (AccessControlException e) { + log.debug(e.getMessage()); + // ignore. + } + } + return templates.toArray(new AccessControlPolicy[templates.size()]); + } + + /** + * @see AccessControlEditor#editAccessControlPolicies(Principal) + */ + public JackrabbitAccessControlPolicy[] editAccessControlPolicies(Principal principal) throws RepositoryException { + List templates = new ArrayList(); + for (AccessControlEditor editor : editors) { + try { + templates.addAll(Arrays.asList(editor.editAccessControlPolicies(principal))); + } catch (AccessControlException e) { + log.debug(e.getMessage()); + // ignore. + } + } + return templates.toArray(new JackrabbitAccessControlPolicy[templates.size()]); + } + + /** + * @see AccessControlEditor#setPolicy(String,AccessControlPolicy) + */ + public void setPolicy(String nodePath, AccessControlPolicy template) throws AccessControlException, PathNotFoundException, RepositoryException { + for (AccessControlEditor editor : editors) { + try { + // return as soon as the first editor successfully handled the + // specified template + editor.setPolicy(nodePath, template); + log.debug("Set template " + template + " using " + editor); + return; + } catch (AccessControlException e) { + log.debug(e.getMessage()); + // ignore and try next + } + } + + // none accepted -> throw + throw new AccessControlException("None of the editors accepted policy " + template + " at " + nodePath); + } + + /** + * @see AccessControlEditor#removePolicy(String,AccessControlPolicy) + */ + public void removePolicy(String nodePath, + AccessControlPolicy policy) throws AccessControlException, PathNotFoundException, RepositoryException { + for (AccessControlEditor editor : editors) { + try { + // return as soon as the first editor successfully handled the + // specified template + editor.removePolicy(nodePath, policy); + log.debug("Removed template " + policy + " using " + editor); + return; + } catch (AccessControlException e) { + log.debug(e.getMessage()); + // ignore and try next + } + } + // neither of the editors was able to remove a policy at nodePath + throw new AccessControlException("Unable to remove template " + policy); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/combined/CombinedProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/combined/CombinedProvider.java new file mode 100644 index 00000000000..e7615b0abcd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/combined/CombinedProvider.java @@ -0,0 +1,287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.combined; + +import org.apache.jackrabbit.core.security.authorization.AbstractAccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.AccessControlEditor; +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.AccessControlUtils; +import org.apache.jackrabbit.core.security.authorization.AbstractCompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl; +import org.apache.jackrabbit.core.security.authorization.principalbased.ACLProvider; +import org.apache.jackrabbit.core.ItemImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import javax.jcr.security.AccessControlPolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ItemNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.HashMap; +import java.util.Arrays; +import java.util.Iterator; +import java.security.Principal; + +/** + * CombinedProvider... + */ +public class CombinedProvider extends AbstractAccessControlProvider { + + private static Logger log = LoggerFactory.getLogger(CombinedProvider.class); + + private AccessControlProvider[] providers; + + //-------------------------------------------------< AccessControlUtils >--- + /** + * @see AccessControlUtils#isAcItem(Path) + */ + @Override + public boolean isAcItem(Path absPath) throws RepositoryException { + for (AccessControlProvider provider : providers) { + if (provider instanceof AccessControlUtils && ((AccessControlUtils) provider).isAcItem(absPath)) { + return true; + } + } + return false; + } + + /** + * @see AccessControlUtils#isAcItem(ItemImpl) + */ + @Override + public boolean isAcItem(ItemImpl item) throws RepositoryException { + for (AccessControlProvider provider : providers) { + if (provider instanceof AccessControlUtils && ((AccessControlUtils) provider).isAcItem(item)) { + return true; + } + } + return false; + } + + //----------------------------------------------< AccessControlProvider >--- + /** + * @see AccessControlProvider#close() + */ + @Override + public void close() { + for (AccessControlProvider provider : providers) { + provider.close(); + } + super.close(); + } + + /** + * @see AccessControlProvider#init(javax.jcr.Session, java.util.Map) + */ + @Override + public void init(Session systemSession, Map configuration) throws RepositoryException { + super.init(systemSession, configuration); + + // this provider combines the result of 2 (currently hard coded) AC-providers + // TODO: make this configurable + providers = new AccessControlProvider[2]; + + // 1) a resource-based ACL provider, that is not initialized with default + // permissions and should only be used to overrule the permissions + // granted or denied by the default provider (see 2). + providers[0] = new org.apache.jackrabbit.core.security.authorization.acl.ACLProvider(); + Map config = new HashMap(configuration); + config.put(PARAM_OMIT_DEFAULT_PERMISSIONS, Boolean.TRUE); + providers[0].init(session, config); + + // 2) the principal-base ACL provider which is intended to provide + // the default/standard permissions present at an item for a given + // set of principals. + providers[1] = new ACLProvider(); + providers[1].init(session, configuration); + } + + /** + * @see AccessControlProvider#getEffectivePolicies(org.apache.jackrabbit.spi.Path,org.apache.jackrabbit.core.security.authorization.CompiledPermissions) + */ + public AccessControlPolicy[] getEffectivePolicies(Path absPath, CompiledPermissions permissions) + throws ItemNotFoundException, RepositoryException { + List l = new ArrayList(); + for (AccessControlProvider provider : providers) { + l.addAll(Arrays.asList(provider.getEffectivePolicies(absPath, permissions))); + } + return l.toArray(new AccessControlPolicy[l.size()]); + } + + /** + * @see AccessControlProvider#getEffectivePolicies(java.util.Set, CompiledPermissions) + */ + public AccessControlPolicy[] getEffectivePolicies(Set principals, CompiledPermissions permissions) throws RepositoryException { + List l = new ArrayList(); + for (AccessControlProvider provider : providers) { + l.addAll(Arrays.asList(provider.getEffectivePolicies(principals, permissions))); + } + return l.toArray(new AccessControlPolicy[l.size()]); + } + + /** + * @see AccessControlProvider#getEditor(javax.jcr.Session) + */ + public AccessControlEditor getEditor(Session editingSession) { + checkInitialized(); + List editors = new ArrayList(); + for (AccessControlProvider provider : providers) { + try { + editors.add(provider.getEditor(editingSession)); + } catch (RepositoryException e) { + log.debug(e.getMessage()); + // ignore. + } + } + if (!editors.isEmpty()) { + return new CombinedEditor(editors.toArray(new AccessControlEditor[editors.size()])); + } else { + log.debug("None of the derived access control providers supports editing."); + return null; + } + } + + /** + * @see AccessControlProvider#compilePermissions(Set) + */ + public CompiledPermissions compilePermissions(Set principals) throws RepositoryException { + checkInitialized(); + if (isAdminOrSystem(principals)) { + return getAdminPermissions(); + } else { + return new CompiledPermissionImpl(principals); + } + } + + /** + * @see AccessControlProvider#canAccessRoot(Set) + */ + public boolean canAccessRoot(Set principals) throws RepositoryException { + checkInitialized(); + if (isAdminOrSystem(principals)) { + return true; + } else { + CompiledPermissions cp = new CompiledPermissionImpl(principals); + try { + Path rootPath = PathFactoryImpl.getInstance().getRootPath(); + return cp.grants(rootPath, Permission.READ); + } finally { + cp.close(); + } + } + } + + //-----------------------------------------------------< CompiledPolicy >--- + /** + * + */ + private class CompiledPermissionImpl extends AbstractCompiledPermissions { + + private final List cPermissions; + + /** + * @param principals the principals + * @throws RepositoryException if an error occurs + */ + private CompiledPermissionImpl(Set principals) throws + RepositoryException { + this.cPermissions = new ArrayList(); + for (AccessControlProvider provider : providers) { + CompiledPermissions cp = provider.compilePermissions(principals); + if (cp instanceof AbstractCompiledPermissions) { + cPermissions.add((AbstractCompiledPermissions) cp); + } else { + // TODO: deal with other implementations + log.warn("AbstractCompiledPermissions expected. Found " + cp.getClass().getName() + " -> ignore."); + } + } + } + + //------------------------------------< AbstractCompiledPermissions >--- + /** + * @see AbstractCompiledPermissions#getResult(Path) + */ + @Override + public Result getResult(Path absPath) throws RepositoryException { + // TODO: missing caching + return buildResult(absPath); + } + + /** + * @see AbstractCompiledPermissions#buildResult(Path) + */ + @Override + protected Result buildResult(Path absPath) throws RepositoryException { + Result res = null; + for (AbstractCompiledPermissions acp : cPermissions) { + Result other = acp.getResult(absPath); + res = (res == null) ? other : res.combine(other); + } + return res; + } + + @Override + protected Result buildRepositoryResult() throws RepositoryException { + Result res = null; + for (AbstractCompiledPermissions acp : cPermissions) { + Result other = acp.getResult(null); + res = (res == null) ? other : res.combine(other); + } + return res; + } + + /** + * @see AbstractCompiledPermissions#getPrivilegeManagerImpl() + */ + @Override + protected PrivilegeManagerImpl getPrivilegeManagerImpl() throws RepositoryException { + return CombinedProvider.this.getPrivilegeManagerImpl(); + } + + //--------------------------------------------< CompiledPermissions >--- + /** + * @see CompiledPermissions#close() + */ + @Override + public synchronized void close() { + // close all c-permissions retained in the list and clear the list. + for (Iterator it = cPermissions.iterator(); it.hasNext();) { + CompiledPermissions cp = it.next(); + cp.close(); + it.remove(); + } + super.close(); + } + + /** + * @see CompiledPermissions#canRead(Path, ItemId) + */ + public boolean canRead(Path path, ItemId itemId) throws RepositoryException { + Path p = (path == null) ? session.getItemManager().getItem(itemId).getPrimaryPath() : path; + return grants(p, Permission.READ); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLEditor.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLEditor.java new file mode 100644 index 00000000000..8038513075f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLEditor.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.core.security.authorization.principalbased; + +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import javax.jcr.security.AccessControlPolicy; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.ProtectedItemModifier; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.AccessControlEditor; +import org.apache.jackrabbit.core.security.authorization.AccessControlEntryImpl; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameParser; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.PropertyType; +import javax.jcr.NodeIterator; +import java.security.Principal; +import java.util.Set; + +/** + * ACLEditor... + */ +public class ACLEditor extends ProtectedItemModifier implements AccessControlEditor, AccessControlConstants { + + private static Logger log = LoggerFactory.getLogger(ACLEditor.class); + + /** + * Default name for ace nodes + */ + private static final String DEFAULT_ACE_NAME = "ace"; + + /** + * the editing session + */ + private final SessionImpl session; + + private final String acRootPath; + + ACLEditor(SessionImpl session, Path acRootPath) throws RepositoryException { + super(Permission.MODIFY_AC); + this.session = session; + this.acRootPath = session.getJCRPath(acRootPath); + } + + ACLTemplate getACL(Principal principal) throws RepositoryException { + if (!session.getPrincipalManager().hasPrincipal(principal.getName())) { + throw new AccessControlException("Unknown principal."); + } + String nPath = getPathToAcNode(principal); + ACLTemplate acl = null; + if (session.nodeExists(nPath)) { + AccessControlPolicy[] plcs = getPolicies(nPath); + if (plcs.length > 0) { + acl = (ACLTemplate) plcs[0]; + } + } + if (acl == null) { + // no policy for the given principal + log.debug("No policy template for Principal " + principal.getName()); + } + return acl; + } + + //------------------------------------------------< AccessControlEditor >--- + /** + * @see AccessControlEditor#getPolicies(String) + */ + public AccessControlPolicy[] getPolicies(String nodePath) throws AccessControlException, PathNotFoundException, RepositoryException { + checkProtectsNode(nodePath); + + NodeImpl acNode = getAcNode(nodePath); + if (isAccessControlled(acNode)) { + return new AccessControlPolicy[] {createTemplate(acNode)}; + } else { + return new AccessControlPolicy[0]; + } + } + + /** + * @see AccessControlEditor#getPolicies(Principal) + */ + public JackrabbitAccessControlPolicy[] getPolicies(Principal principal) throws AccessControlException, RepositoryException { + if (!session.getPrincipalManager().hasPrincipal(principal.getName())) { + throw new AccessControlException("Cannot edit access control: " + principal.getName() +" isn't a known principal."); + } + JackrabbitAccessControlPolicy acl = getACL(principal); + if (acl == null) { + return new JackrabbitAccessControlPolicy[0]; + } else { + return new JackrabbitAccessControlPolicy[] {acl}; + } + } + + /** + * @see AccessControlEditor#editAccessControlPolicies(String) + */ + public AccessControlPolicy[] editAccessControlPolicies(String nodePath) throws AccessControlException, PathNotFoundException, RepositoryException { + checkProtectsNode(nodePath); + + if (Text.isDescendant(acRootPath, nodePath)) { + NodeImpl acNode = getAcNode(nodePath); + if (acNode == null) { + // check validity and create the ac node + Principal p = getPrincipal(nodePath); + if (p == null) { + throw new AccessControlException("Access control modification not allowed at " + nodePath); + } + acNode = createAcNode(nodePath); + } + + if (!isAccessControlled(acNode)) { + return new AccessControlPolicy[] {createTemplate(acNode)}; + } // else: acl has already been set before -> use getPolicies instead + } + + // nodePath not below rep:policy -> not editable + // or policy has been set before in which case getPolicies should be used instead. + return new AccessControlPolicy[0]; + } + + /** + * @see AccessControlEditor#editAccessControlPolicies(Principal) + */ + public JackrabbitAccessControlPolicy[] editAccessControlPolicies(Principal principal) throws RepositoryException { + if (!session.getPrincipalManager().hasPrincipal(principal.getName())) { + throw new AccessControlException("Cannot edit access control: " + principal.getName() +" isn't a known principal."); + } + String nPath = getPathToAcNode(principal); + NodeImpl acNode; + if (!session.nodeExists(nPath)) { + acNode = createAcNode(nPath); + } else { + acNode = (NodeImpl) session.getNode(nPath); + } + if (!isAccessControlled(acNode)) { + return new JackrabbitAccessControlPolicy[] {createTemplate(acNode)}; + } else { + // policy child node has already been created -> set policy has + // been called before for this principal and getPolicy is used + // to retrieve the ACL template. + // no additional applicable policies present. + return new JackrabbitAccessControlPolicy[0]; + } + } + + /** + * @see AccessControlEditor#setPolicy(String,AccessControlPolicy) + */ + public void setPolicy(String nodePath, AccessControlPolicy policy) + throws AccessControlException, PathNotFoundException, RepositoryException { + checkProtectsNode(nodePath); + checkValidPolicy(nodePath, policy); + + ACLTemplate acl = (ACLTemplate) policy; + NodeImpl acNode = getAcNode(nodePath); + if (acNode == null) { + throw new PathNotFoundException("No such node " + nodePath); + } + + // write the entries to the node + NodeImpl aclNode; + if (acNode.hasNode(N_POLICY)) { + aclNode = acNode.getNode(N_POLICY); + // remove all existing aces + for (NodeIterator aceNodes = aclNode.getNodes(); aceNodes.hasNext();) { + NodeImpl aceNode = (NodeImpl) aceNodes.nextNode(); + removeItem(aceNode); + } + } else { + /* doesn't exist yet -> create */ + aclNode = addNode(acNode, N_POLICY, NT_REP_ACL); + } + + /* add all new entries defined on the template */ + AccessControlEntry[] aces = acl.getAccessControlEntries(); + for (AccessControlEntry ace1 : aces) { + AccessControlEntryImpl ace = (AccessControlEntryImpl) ace1; + + // create the ACE node + Name nodeName = getUniqueNodeName(aclNode, "entry"); + Name ntName = (ace.isAllow()) ? NT_REP_GRANT_ACE : NT_REP_DENY_ACE; + NodeImpl aceNode = addNode(aclNode, nodeName, ntName); + + ValueFactory vf = session.getValueFactory(); + // write the rep:principalName property + setProperty(aceNode, P_PRINCIPAL_NAME, vf.createValue(ace.getPrincipal().getName())); + // ... and the rep:privileges property + Privilege[] privs = ace.getPrivileges(); + Value[] vs = new Value[privs.length]; + for (int j = 0; j < privs.length; j++) { + vs[j] = vf.createValue(privs[j].getName(), PropertyType.NAME); + } + setProperty(aceNode, P_PRIVILEGES, vs); + + // store the restrictions: + Set restrNames = ace.getRestrictions().keySet(); + for (Name restrName : restrNames) { + Value value = ace.getRestriction(restrName); + setProperty(aceNode, restrName, value); + } + } + + // mark the parent modified. + markModified((NodeImpl) aclNode.getParent()); + } + + /** + * @see AccessControlEditor#removePolicy(String,AccessControlPolicy) + */ + public void removePolicy(String nodePath, AccessControlPolicy policy) throws AccessControlException, PathNotFoundException, RepositoryException { + checkProtectsNode(nodePath); + checkValidPolicy(nodePath, policy); + + NodeImpl acNode = getAcNode(nodePath); + if (isAccessControlled(acNode)) { + // build the template in order to have a return value + AccessControlPolicy tmpl = createTemplate(acNode); + if (tmpl.equals(policy)) { + removeItem(acNode.getNode(N_POLICY)); + return; + } + } + // node either not access-controlled or the passed policy didn't apply + // to the node at 'nodePath' -> throw exception. no policy was removed + throw new AccessControlException("Policy " + policy + " does not apply to " + nodePath); + } + + //------------------------------------------------------------< private >--- + /** + * + * @param nodePath the node path + * @return the node + * @throws PathNotFoundException if the node does not exist + * @throws RepositoryException if an error occurs + */ + private NodeImpl getAcNode(String nodePath) throws PathNotFoundException, + RepositoryException { + if (Text.isDescendant(acRootPath, nodePath)) { + return (NodeImpl) session.getNode(nodePath); + } else { + // node outside of rep:policy tree -> not handled by this editor. + return null; + } + } + + private NodeImpl createAcNode(String acPath) throws RepositoryException { + String[] segms = Text.explode(acPath, '/', false); + StringBuilder currentPath = new StringBuilder(); + NodeImpl node = (NodeImpl) session.getRootNode(); + for (int i = 0; i < segms.length; i++) { + if (i > 0) { + currentPath.append('/').append(segms[i]); + } + Name nName = session.getQName(segms[i]); + Name ntName; + if (denotesPrincipalPath(currentPath.toString())) { + ntName = NT_REP_PRINCIPAL_ACCESS_CONTROL; + } else { + ntName = (i < segms.length - 1) ? NT_REP_ACCESS_CONTROL : NT_REP_PRINCIPAL_ACCESS_CONTROL; + } + if (node.hasNode(nName)) { + NodeImpl n = node.getNode(nName); + if (!n.isNodeType(ntName)) { + // should never get here. + throw new RepositoryException("Error while creating access control node: Expected nodetype " + session.getJCRName(ntName) + " below /rep:accessControl, was " + node.getPrimaryNodeType().getName() + " instead"); + } + node = n; + } else { + node = addNode(node, nName, ntName); + } + } + return node; + } + + private boolean denotesPrincipalPath(final String path) { + if (path == null || path.length() == 0) { + return false; + } + ItemBasedPrincipal princ = new ItemBasedPrincipal() { + public String getPath() throws RepositoryException { + return path; + } + public String getName() { + return Text.getName(path); + } + }; + try { + return session.getUserManager().getAuthorizable(princ) != null; + } catch (RepositoryException e) { + return false; + } + } + + /** + * Check if the Node identified by id is itself part of ACL + * defining content. It this case setting or modifying an AC-policy is + * obviously not possible. + * + * @param nodePath the node path + * @throws AccessControlException If the given id identifies a Node that + * represents a ACL or ACE item. + * @throws RepositoryException + */ + private void checkProtectsNode(String nodePath) throws RepositoryException { + if (nodePath == null) { + // TODO: JCR-2774 + throw new UnsupportedRepositoryOperationException("JCR-2774"); + } + if (session.nodeExists(nodePath)) { + NodeImpl n = (NodeImpl) session.getNode(nodePath); + if (n.isNodeType(NT_REP_ACL) || n.isNodeType(NT_REP_ACE)) { + throw new AccessControlException("Node " + nodePath + " defines ACL or ACE."); + } + } + } + + /** + * Check if the specified policy can be set or removed at nodePath. + * + * @param nodePath the node path + * @param policy the policy + * @throws AccessControlException if not allowed + */ + private void checkValidPolicy(String nodePath, AccessControlPolicy policy) + throws AccessControlException { + if (policy == null || !(policy instanceof ACLTemplate)) { + throw new AccessControlException("Attempt to set/remove invalid policy " + policy); + } + ACLTemplate acl = (ACLTemplate) policy; + if (!nodePath.equals(acl.getPath())) { + throw new AccessControlException("Policy " + policy + " is not applicable or does not apply to the node at " + nodePath); + } + } + + /** + * + * @param principal the principal + * @return the path + * @throws RepositoryException if an error occurs + */ + String getPathToAcNode(Principal principal) throws RepositoryException { + StringBuffer princPath = new StringBuffer(acRootPath); + if (principal instanceof ItemBasedPrincipal) { + princPath.append(((ItemBasedPrincipal) principal).getPath()); + } else { + princPath.append("/"); + princPath.append(Text.escapeIllegalJcrChars(principal.getName())); + } + return princPath.toString(); + } + + /** + * Returns the principal for the given path or null. + * + * @param pathToACNode + * @return + * @throws RepositoryException + */ + private Principal getPrincipal(final String pathToACNode) throws RepositoryException { + final String id = getPathName(pathToACNode); + UserManager uMgr = session.getUserManager(); + Authorizable authorizable = uMgr.getAuthorizable(id); + if (authorizable == null) { + // human readable id in node name is different from the hashed id + // use workaround to retrieve the principal + if (pathToACNode.startsWith(acRootPath)) { + final String principalPath = pathToACNode.substring(acRootPath.length()); + if (principalPath.indexOf('/', 1) > 0) { + // safe to build an item based principal + authorizable = uMgr.getAuthorizable(new ItemBasedPrincipal() { + public String getPath() throws RepositoryException { + return principalPath; + } + public String getName() { + return Text.getName(principalPath); + } + }); + } else { + // principal name was just appended to the acRootPath prefix + // see getPathToAcNode above -> try to retrieve principal by name. + return session.getPrincipalManager().getPrincipal(Text.getName(principalPath)); + } + } // else: path doesn't start with acRootPath -> return null. + } + + return (authorizable == null) ? null : authorizable.getPrincipal(); + } + + private static String getPathName(String pathToACNode) { + return Text.unescapeIllegalJcrChars(Text.getName(pathToACNode)); + } + + /** + * + * @param node the node + * @return true if access controlled + * @throws RepositoryException if an error occurs + */ + private static boolean isAccessControlled(NodeImpl node) throws RepositoryException { + return node != null && node.isNodeType(NT_REP_PRINCIPAL_ACCESS_CONTROL) && node.hasNode(N_POLICY); + } + + /** + * + * @param acNode the acl node + * @return the polict + * @throws RepositoryException if an error occurs + */ + private JackrabbitAccessControlPolicy createTemplate(NodeImpl acNode) throws RepositoryException { + if (!acNode.isNodeType(NT_REP_PRINCIPAL_ACCESS_CONTROL)) { + String msg = "Unable to edit Access Control at "+ acNode.getPath()+ ". Expected node of type rep:PrinicipalAccessControl, was " + acNode.getPrimaryNodeType().getName(); + log.debug(msg); + throw new AccessControlException(msg); + } + + Principal principal = getPrincipal(acNode.getPath()); + if (principal == null) { + // use fall back in order to be able to get/remove the policy + String principalName = getPathName(acNode.getPath()); + log.warn("Principal with name " + principalName + " unknown to PrincipalManager."); + principal = new PrincipalImpl(principalName); + } + return new ACLTemplate(principal, acNode); + } + + /** + * Create a unique valid name for the Permission nodes to be save. + * + * @param node a name for the child is resolved + * @param name if missing the {@link #DEFAULT_ACE_NAME} is taken + * @return the name + * @throws RepositoryException if an error occurs + */ + protected static Name getUniqueNodeName(Node node, String name) throws RepositoryException { + if (name == null) { + name = DEFAULT_ACE_NAME; + } else { + try { + NameParser.checkFormat(name); + } catch (NameException e) { + name = DEFAULT_ACE_NAME; + log.debug("Invalid path name for Permission: " + name + "."); + } + } + int i = 0; + String check = name; + while (node.hasNode(check)) { + check = name + i; + i++; + } + return ((SessionImpl) node.getSession()).getQName(check); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLProvider.java new file mode 100644 index 00000000000..cf3a9d3b182 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLProvider.java @@ -0,0 +1,529 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.core.cache.GrowingLRUMap; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.authorization.AbstractAccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.AbstractCompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.AccessControlEditor; +import org.apache.jackrabbit.core.security.authorization.AccessControlEntryImpl; +import org.apache.jackrabbit.core.security.authorization.AccessControlListener; +import org.apache.jackrabbit.core.security.authorization.AccessControlModifications; +import org.apache.jackrabbit.core.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.PrivilegeBits; +import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.core.security.authorization.UnmodifiableAccessControlList; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.QueryResult; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * ACLProvider... + */ +public class ACLProvider extends AbstractAccessControlProvider implements AccessControlConstants { + + private static Logger log = LoggerFactory.getLogger(ACLProvider.class); + + private NodeImpl acRoot; + private ACLEditor editor; + + private EntriesCache entriesCache; + + //----------------------------------------------< AccessControlProvider >--- + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#init(javax.jcr.Session, java.util.Map) + */ + @Override + public void init(Session systemSession, Map configuration) throws RepositoryException { + super.init(systemSession, configuration); + + NodeImpl root = (NodeImpl) session.getRootNode(); + if (root.hasNode(N_ACCESSCONTROL)) { + acRoot = root.getNode(N_ACCESSCONTROL); + if (!acRoot.isNodeType(NT_REP_ACCESS_CONTROL)) { + throw new RepositoryException("Error while initializing Access Control Provider: Found ac-root to be wrong node type " + acRoot.getPrimaryNodeType().getName()); + } + } else { + acRoot = root.addNode(N_ACCESSCONTROL, NT_REP_ACCESS_CONTROL, null); + } + + editor = new ACLEditor(session, session.getQPath(acRoot.getPath())); + entriesCache = new EntriesCache(session, editor, acRoot.getPath()); + + // TODO: replace by configurable default policy (see JCR-2331) + if (!configuration.containsKey(PARAM_OMIT_DEFAULT_PERMISSIONS)) { + try { + log.debug("Install initial permissions: ..."); + + ValueFactory vf = session.getValueFactory(); + Map restrictions = new HashMap(); + restrictions.put(session.getJCRName(ACLTemplate.P_NODE_PATH), vf.createValue(root.getPath(), PropertyType.PATH)); + + PrincipalManager pMgr = session.getPrincipalManager(); + AccessControlManager acMgr = session.getAccessControlManager(); + + // initial default permissions for the administrators group + String pName = SecurityConstants.ADMINISTRATORS_NAME; + if (pMgr.hasPrincipal(pName)) { + Principal administrators = pMgr.getPrincipal(pName); + installDefaultPermissions(administrators, + new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_ALL)}, + restrictions, editor); + } else { + log.info("Administrators principal group is missing -> Not adding default permissions."); + } + + // initialize default permissions for the everyone group + installDefaultPermissions(pMgr.getEveryone(), + new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_READ)}, + restrictions, editor); + + session.save(); + } catch (RepositoryException e) { + log.error("Failed to set-up minimal access control for root node of workspace " + session.getWorkspace().getName()); + session.getRootNode().refresh(false); + } + } + } + + private static void installDefaultPermissions(Principal principal, + Privilege[] privs, + Map restrictions, + AccessControlEditor editor) + throws RepositoryException, AccessControlException { + AccessControlPolicy[] acls = editor.editAccessControlPolicies(principal); + if (acls.length > 0) { + ACLTemplate acl = (ACLTemplate) acls[0]; + if (acl.isEmpty()) { + acl.addEntry(principal, privs, true, restrictions); + editor.setPolicy(acl.getPath(), acl); + } else { + log.debug("... policy for principal '"+principal.getName()+"' already present."); + } + } else { + log.debug("... policy for principal '"+principal.getName()+"' already present."); + } + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#close() + */ + @Override + public void close() { + super.close(); + entriesCache.close(); + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(org.apache.jackrabbit.spi.Path,org.apache.jackrabbit.core.security.authorization.CompiledPermissions) + */ + public AccessControlPolicy[] getEffectivePolicies(Path absPath, CompiledPermissions permissions) throws ItemNotFoundException, RepositoryException { + if (absPath == null) { + // TODO: JCR-2774 + log.warn("TODO: JCR-2774 - Repository level permissions."); + return new AccessControlPolicy[0]; + } + + String jcrPath = session.getJCRPath(absPath); + String pName = ISO9075.encode(session.getJCRName(ACLTemplate.P_NODE_PATH)); + int ancestorCnt = absPath.getAncestorCount(); + + // search all ACEs whose rep:nodePath property equals the specified + // absPath or any of it's ancestors + StringBuilder stmt = new StringBuilder("/jcr:root"); + stmt.append(acRoot.getPath()); + stmt.append("//element(*,"); + stmt.append(session.getJCRName(NT_REP_ACE)); + stmt.append(")["); + for (int i = 0; i <= ancestorCnt; i++) { + String path = Text.getRelativeParent(jcrPath, i); + if (i > 0) { + stmt.append(" or "); + } + stmt.append("@"); + stmt.append(pName); + stmt.append("='"); + stmt.append(path.replaceAll("'", "''")); + stmt.append("'"); + } + stmt.append("]"); + + QueryResult result; + try { + QueryManager qm = session.getWorkspace().getQueryManager(); + Query q = qm.createQuery(stmt.toString(), Query.XPATH); + result = q.execute(); + } catch (RepositoryException e) { + log.error("Unexpected error while searching effective policies. {}", e.getMessage()); + throw new UnsupportedOperationException("Retrieve effective policies at absPath '" +jcrPath+ "' not supported.", e); + } + + /** + * Loop over query results and verify that + * - the corresponding ACE really takes effect on the specified absPath. + * - the corresponding ACL can be read by the editing session. + */ + Set acls = new LinkedHashSet(); + for (NodeIterator it = result.getNodes(); it.hasNext();) { + Node aceNode = it.nextNode(); + String accessControlledNodePath = Text.getRelativeParent(aceNode.getPath(), 2); + Path acPath = session.getQPath(accessControlledNodePath); + + AccessControlPolicy[] policies = editor.getPolicies(accessControlledNodePath); + if (policies.length > 0) { + ACLTemplate acl = (ACLTemplate) policies[0]; + for (AccessControlEntry ace : acl.getAccessControlEntries()) { + ACLTemplate.Entry entry = (ACLTemplate.Entry) ace; + if (entry.matches(jcrPath)) { + if (permissions.grants(acPath, Permission.READ_AC)) { + acls.add(new UnmodifiableAccessControlList(acl)); + break; + } else { + throw new AccessDeniedException("Access denied at " + accessControlledNodePath); + } + } + } + } + } + return acls.toArray(new AccessControlPolicy[acls.size()]); + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(java.util.Set, CompiledPermissions) + */ + public AccessControlPolicy[] getEffectivePolicies(Set principals, CompiledPermissions permissions) throws RepositoryException { + List acls = new ArrayList(principals.size()); + for (Principal principal : principals) { + ACLTemplate acl = editor.getACL(principal); + if (acl != null) { + if (permissions.grants(session.getQPath(acl.getPath()), Permission.READ_AC)) { + acls.add(new UnmodifiableAccessControlList(acl)); + } else { + throw new AccessDeniedException("Access denied at " + acl.getPath()); + } + } + } + return acls.toArray(new AccessControlPolicy[acls.size()]); + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEditor(Session) + */ + public AccessControlEditor getEditor(Session editingSession) { + checkInitialized(); + if (editingSession instanceof SessionImpl) { + try { + return new ACLEditor((SessionImpl) editingSession, session.getQPath(acRoot.getPath())); + } catch (RepositoryException e) { + // should never get here + log.error("Internal error: {}", e.getMessage()); + } + } + + log.debug("Unable to build access control editor " + ACLEditor.class.getName() + "."); + return null; + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#compilePermissions(Set) + */ + public CompiledPermissions compilePermissions(Set principals) throws RepositoryException { + checkInitialized(); + if (isAdminOrSystem(principals)) { + return getAdminPermissions(); + } else if (isReadOnly(principals)) { + return getReadOnlyPermissions(); + } else { + return new CompiledPermissionImpl(principals); + } + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#canAccessRoot(Set) + */ + public boolean canAccessRoot(Set principals) throws RepositoryException { + checkInitialized(); + if (isAdminOrSystem(principals)) { + return true; + } else { + CompiledPermissionImpl cp = new CompiledPermissionImpl(principals, false); + return cp.canRead(((NodeImpl) session.getRootNode()).getPrimaryPath()); + } + } + + //-----------------------------------------------------< CompiledPolicy >--- + /** + * + */ + private class CompiledPermissionImpl extends AbstractCompiledPermissions + implements AccessControlListener { + + private final Set principals; + private final Set acPaths; + private List entries; + + private boolean canReadAll; + + @SuppressWarnings("unchecked") + private final Map readCache = new GrowingLRUMap(1024, 5000); + + private final Object monitor = new Object(); + + /** + * @param principals the underlying principals + * @throws RepositoryException if an error occurs + */ + private CompiledPermissionImpl(Set principals) throws RepositoryException { + this(principals, true); + } + + /** + * @param principals the underlying principals + * @param listenToEvents if true listens to events + * @throws RepositoryException if an error occurs + */ + private CompiledPermissionImpl(Set principals, boolean listenToEvents) throws RepositoryException { + + this.principals = principals; + acPaths = new HashSet(principals.size()); + reload(); + + if (listenToEvents) { + /* + Make sure this AclPermission recalculates the permissions if + any ACL concerning it is modified. + */ + entriesCache.addListener(this); + } + } + + /** + * @throws RepositoryException if an error occurs + */ + private void reload() throws RepositoryException { + // reload the paths + acPaths.clear(); + for (Principal p : principals) { + acPaths.add(editor.getPathToAcNode(p)); + } + + // and retrieve the entries from the entry-collector. + entries = entriesCache.getEntries(principals); + + // in addition: trivial check if read access is denied somewhere + canReadAll = canRead(session.getQPath("/")); + if (canReadAll) { + for (AccessControlEntry entry : entries) { + AccessControlEntryImpl ace = (AccessControlEntryImpl) entry; + if (!ace.isAllow() && ace.getPrivilegeBits().includesRead()) { + // found an ace that defines read deny for a sub tree + // -> canReadAll is false. + canReadAll = false; + break; + } + } + } + } + + //------------------------------------< AbstractCompiledPermissions >--- + /** + * @see AbstractCompiledPermissions#buildResult(Path) + */ + @Override + protected synchronized Result buildResult(Path absPath) throws RepositoryException { + if (!absPath.isAbsolute()) { + throw new RepositoryException("Absolute path expected."); + } + + boolean isAcItem = isAcItem(absPath); + String jcrPath = session.getJCRPath(absPath); + + // retrieve principal-based permissions and privileges + return buildResult(jcrPath, isAcItem); + } + + @Override + protected Result buildRepositoryResult() throws RepositoryException { + log.warn("TODO: JCR-2774 - Repository level permissions."); + PrivilegeManagerImpl pm = getPrivilegeManagerImpl(); + return new Result(Permission.NONE, Permission.NONE, PrivilegeBits.EMPTY, PrivilegeBits.EMPTY); + } + + /** + * @see AbstractCompiledPermissions#getPrivilegeManagerImpl() + */ + @Override + protected PrivilegeManagerImpl getPrivilegeManagerImpl() throws RepositoryException { + return ACLProvider.this.getPrivilegeManagerImpl(); + } + + /** + * Loop over all entries and evaluate allows/denies for those matching + * the given jcrPath. + * + * @param targetPath Path used for the evaluation; pointing to an + * existing or non-existing item. + * @param isAcItem the item. + * @return the result + * @throws RepositoryException if an error occurs + */ + private Result buildResult(String targetPath, + boolean isAcItem) throws RepositoryException { + int allows = Permission.NONE; + int denies = Permission.NONE; + + PrivilegeBits allowBits = PrivilegeBits.getInstance(); + PrivilegeBits denyBits = PrivilegeBits.getInstance(); + PrivilegeBits parentAllowBits = PrivilegeBits.getInstance(); + PrivilegeBits parentDenyBits = PrivilegeBits.getInstance(); + + String parentPath = Text.getRelativeParent(targetPath, 1); + for (AccessControlEntry entry : entries) { + if (!(entry instanceof ACLTemplate.Entry)) { + log.warn("Unexpected AccessControlEntry instance -> ignore"); + continue; + } + ACLTemplate.Entry entr = (ACLTemplate.Entry) entry; + PrivilegeBits privs = entr.getPrivilegeBits(); + + if (!"".equals(parentPath) && entr.matches(parentPath)) { + if (entr.isAllow()) { + parentAllowBits.addDifference(privs, parentDenyBits); + } else { + parentDenyBits.addDifference(privs, parentAllowBits); + } + } + + boolean matches = entr.matches(targetPath); + if (matches) { + if (entr.isAllow()) { + allowBits.addDifference(privs, denyBits); + int permissions = PrivilegeRegistry.calculatePermissions(allowBits, parentAllowBits, true, isAcItem); + allows |= Permission.diff(permissions, denies); + } else { + denyBits.addDifference(privs, allowBits); + int permissions = PrivilegeRegistry.calculatePermissions(denyBits, parentDenyBits, false, isAcItem); + denies |= Permission.diff(permissions, allows); + } + } + } + + return new Result(allows, denies, allowBits, denyBits); + } + + //--------------------------------------------< CompiledPermissions >--- + /** + * @see CompiledPermissions#close() + */ + @Override + public void close() { + entriesCache.removeListener(this); + super.close(); + } + + /** + * @see CompiledPermissions#canRead(Path, ItemId) + */ + public boolean canRead(Path path, ItemId itemId) throws RepositoryException { + boolean canRead; + if (path == null) { + // only itemId: try to avoid expensive resolution from itemID to path + synchronized (monitor) { + if (readCache.containsKey(itemId)) { + // id has been evaluated before -> shortcut + canRead = readCache.get(itemId); + } else { + canRead = canRead(session.getHierarchyManager().getPath(itemId)); + readCache.put(itemId, canRead); + return canRead; + } + } + } else { + // path param present: + canRead = canRead(path); + } + return canRead; + } + + private boolean canRead(Path path) throws RepositoryException { + // first try if reading non-ac-items was always granted -> no eval + // otherwise evaluate the permissions. + return (canReadAll && !isAcItem(path)) || grants(path, Permission.READ); + } + + //------------------------------------------< AccessControlListener >--- + /** + * @see AccessControlListener#acModified(org.apache.jackrabbit.core.security.authorization.AccessControlModifications) + */ + public void acModified(AccessControlModifications modifications) { + try { + boolean reload = false; + Iterator keys = modifications.getNodeIdentifiers().iterator(); + while (keys.hasNext() && !reload) { + String path = keys.next().toString(); + reload = acPaths.contains(path); + } + // eventually reload the ACL and clear the cache + if (reload) { + clearCache(); + // reload the ac-path list and the list of aces + reload(); + } + } catch (RepositoryException e) { + // should never get here + log.warn("Internal error: {}", e.getMessage()); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLTemplate.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLTemplate.java new file mode 100644 index 00000000000..b86b1252bfc --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLTemplate.java @@ -0,0 +1,371 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AccessControlEntryImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractACLTemplate; +import org.apache.jackrabbit.core.security.authorization.GlobPattern; +import org.apache.jackrabbit.core.security.authorization.PrivilegeBits; +import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Implementation of the {@link org.apache.jackrabbit.api.security.JackrabbitAccessControlList} interface that + * is detached from the effective access control content. Consequently, any + * modifications applied to this ACL only take effect, if the policy gets + * {@link javax.jcr.security.AccessControlManager#setPolicy(String, javax.jcr.security.AccessControlPolicy) reapplied} + * to the AccessControlManager and the changes are saved. + */ +class ACLTemplate extends AbstractACLTemplate { + + private static Logger log = LoggerFactory.getLogger(ACLTemplate.class); + + /** + * rep:nodePath property name (optional if the ACL is stored with the + * node itself). + */ + static final Name P_NODE_PATH = NameConstants.REP_NODE_PATH; + + private final Principal principal; + + private final List entries = new ArrayList(); + + private final String jcrNodePathName; + private final String jcrGlobName; + + private final NameResolver resolver; + private final PrivilegeManagerImpl privilegeMgr; + + ACLTemplate(Principal principal, String path, NamePathResolver resolver, ValueFactory vf) + throws RepositoryException { + this(principal, path, null, resolver, vf); + } + + ACLTemplate(Principal principal, NodeImpl acNode) throws RepositoryException { + this(principal, acNode.getPath(), acNode, (SessionImpl) acNode.getSession(), + acNode.getSession().getValueFactory()); + } + + private ACLTemplate(Principal principal, String path, NodeImpl acNode, + NamePathResolver resolver, ValueFactory vf) + throws RepositoryException { + super(path, vf); + + this.principal = principal; + + jcrNodePathName = resolver.getJCRName(P_NODE_PATH); + jcrGlobName = resolver.getJCRName(P_GLOB); + + this.resolver = resolver; + Session session = (acNode != null) ? acNode.getSession() : (Session) resolver; + this.privilegeMgr = (PrivilegeManagerImpl) ((JackrabbitWorkspace) session.getWorkspace()).getPrivilegeManager(); + + if (acNode != null && acNode.hasNode(N_POLICY)) { + // build the list of policy entries; + NodeImpl aclNode = acNode.getNode(N_POLICY); + + // loop over all entries in the aclNode for the princ-Principal + for (NodeIterator aceNodes = aclNode.getNodes(); aceNodes.hasNext();) { + NodeImpl aceNode = (NodeImpl) aceNodes.nextNode(); + if (aceNode.isNodeType(NT_REP_ACE)) { + // the isAllow flag: + boolean isAllow = aceNode.isNodeType(NT_REP_GRANT_ACE); + // the privileges + InternalValue[] pValues = aceNode.getProperty(P_PRIVILEGES).internalGetValues(); + Name[] privilegeNames = new Name[pValues.length]; + for (int i = 0; i < pValues.length; i++) { + privilegeNames[i] = pValues[i].getName(); + } + // the restrictions: + Map restrictions = new HashMap(2); + Property prop = aceNode.getProperty(P_NODE_PATH); + restrictions.put(prop.getName(), prop.getValue()); + + if (aceNode.hasProperty(P_GLOB)) { + prop = aceNode.getProperty(P_GLOB); + restrictions.put(prop.getName(), prop.getValue()); + } + // finally add the entry + AccessControlEntry entry = new Entry(principal, privilegeMgr.getBits(privilegeNames), isAllow, restrictions); + entries.add(entry); + } else { + log.warn("ACE must be of nodetype rep:ACE -> ignored child-node " + aceNode.getPath()); + } + } + } // else: no-node at all or no acl-node present. + } + + AccessControlEntry createEntry(Principal princ, Privilege[] privileges, + boolean allow, Map restrictions) throws RepositoryException { + // adjust restrictions if necessary + Map rest = adjustRestrictions(restrictions); + checkValidEntry(princ, privileges, allow, rest); + return new Entry(princ, privileges, allow, rest); + } + + private Map adjustRestrictions(Map restrictions) throws RepositoryException { + // make sure the nodePath restriction is of type PATH + Value v = restrictions.get(jcrNodePathName); + if (v == null) { + v = restrictions.get(P_NODE_PATH.toString()); + } + if (v != null && v.getType() != PropertyType.PATH) { + v = valueFactory.createValue(v.getString(), PropertyType.PATH); + restrictions.put(jcrNodePathName, v); + } + // ... and glob is of type STRING. + v = restrictions.get(jcrGlobName); + if (v == null) { + v = restrictions.get(P_GLOB.toString()); + } + if (v != null && v.getType() != PropertyType.STRING) { + v = valueFactory.createValue(v.getString(), PropertyType.STRING); + restrictions.put(jcrGlobName, v); + } + + return restrictions; + } + + //------------------------------------------------< AbstractACLTemplate >--- + /** + * @see AbstractACLTemplate#checkValidEntry(java.security.Principal, javax.jcr.security.Privilege[], boolean, java.util.Map) + */ + @Override + protected void checkValidEntry(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions) + throws AccessControlException { + if (!this.principal.equals(principal)) { + throw new AccessControlException("Invalid principal. Expected: " + principal); + } + + Set rNames = restrictions.keySet(); + if (!rNames.contains(jcrNodePathName) && !rNames.contains(P_NODE_PATH.toString())) { + throw new AccessControlException("Missing mandatory restriction: " + jcrNodePathName); + } + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AbstractACLTemplate#getEntries() + */ + @Override + protected List getEntries() { + return entries; + } + + //----------------------------------------< JackrabbitAccessControlList >--- + /** + * @see JackrabbitAccessControlList#getRestrictionNames() + */ + public String[] getRestrictionNames() { + return new String[] {jcrNodePathName, jcrGlobName}; + } + + /** + * @see JackrabbitAccessControlList#getRestrictionType(String) + */ + public int getRestrictionType(String restrictionName) { + if (jcrNodePathName.equals(restrictionName) || P_NODE_PATH.toString().equals(restrictionName)) { + return PropertyType.PATH; + } else if (jcrGlobName.equals(restrictionName) || P_GLOB.toString().equals(restrictionName)) { + return PropertyType.STRING; + } else { + return PropertyType.UNDEFINED; + } + } + + @Override + public boolean isMultiValueRestriction(String restrictionName) throws RepositoryException { + return false; + } + + /** + * Known restrictions are: + *
    +     *   rep:nodePath  (mandatory) value-type: PATH
    +     *   rep:glob      (optional)  value-type: STRING
    +     * 
    + * + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean, Map) + */ + public boolean addEntry(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions) + throws AccessControlException, RepositoryException { + if (restrictions == null || restrictions.isEmpty()) { + log.debug("Restrictions missing. Using default: rep:nodePath = " + getPath() + "; rep:glob = null."); + // default restrictions: + restrictions = Collections.singletonMap(jcrNodePathName, + valueFactory.createValue(getPath(), PropertyType.PATH)); + } + AccessControlEntry entry = createEntry(principal, privileges, isAllow, restrictions); + if (entries.contains(entry)) { + log.debug("Entry is already contained in policy -> no modification."); + return false; + } else { + // TODO: to be improved. clean redundant entries + entries.add(0, entry); + return true; + } + } + + //--------------------------------------------------< AccessControlList >--- + /** + * @see javax.jcr.security.AccessControlList#removeAccessControlEntry(AccessControlEntry) + */ + public void removeAccessControlEntry(AccessControlEntry ace) + throws AccessControlException, RepositoryException { + if (!(ace instanceof Entry)) { + throw new AccessControlException("Invalid AccessControlEntry implementation " + ace.getClass().getName() + "."); + } + if (!entries.remove(ace)) { + throw new AccessControlException("Cannot remove AccessControlEntry " + ace); + } + } + + //-------------------------------------------------------------< Object >--- + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return 0; + } + + /** + * Returns true if the path and the entries are equal; false otherwise. + * + * @param obj Object to test. + * @return true if the path and the entries are equal; false otherwise. + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ACLTemplate) { + ACLTemplate acl = (ACLTemplate) obj; + return principal.equals(acl.principal) && + path.equals(acl.path) && entries.equals(acl.entries); + } + return false; + } + + //-------------------------------------------------------------------------- + /** + * The access control entry of a principalbased ACL. + */ + class Entry extends AccessControlEntryImpl { + + /** + * The path of the Node this entry applies to. + */ + private final String nodePath; + + /** + * Globing pattern + */ + private final GlobPattern pattern; + + private Entry(Principal principal, Privilege[] privileges, boolean allow, + Map restrictions) + throws AccessControlException, RepositoryException { + super(principal, privileges, allow, restrictions); + + Map rstr = getRestrictions(); + nodePath = rstr.get(P_NODE_PATH).getString(); + Value glob = rstr.get(P_GLOB); + if (glob != null) { + pattern = GlobPattern.create(nodePath, glob.getString()); + } else { + pattern = GlobPattern.create(nodePath); + } + } + + private Entry(Principal principal, PrivilegeBits privilegeBits, boolean allow, + Map restrictions) + throws AccessControlException, RepositoryException { + super(principal, privilegeBits, allow, restrictions); + + Map rstr = getRestrictions(); + nodePath = rstr.get(P_NODE_PATH).getString(); + Value glob = rstr.get(P_GLOB); + if (glob != null) { + pattern = GlobPattern.create(nodePath, glob.getString()); + } else { + pattern = GlobPattern.create(nodePath); + } + } + + boolean matches(String jcrPath) throws RepositoryException { + return pattern.matches(jcrPath); + } + + boolean matches(Item item) throws RepositoryException { + return pattern.matches(item); + } + + @Override + protected NameResolver getResolver() { + return resolver; + } + + @Override + protected ValueFactory getValueFactory() { + return valueFactory; + } + + @Override + protected PrivilegeManagerImpl getPrivilegeManager() { + return privilegeMgr; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/EntriesCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/EntriesCache.java new file mode 100644 index 00000000000..bac78a1c252 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authorization/principalbased/EntriesCache.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.principalbased; + +import org.apache.commons.collections.map.LRUMap; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.AccessControlModifications; +import org.apache.jackrabbit.core.security.authorization.AccessControlObserver; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.ObservationManager; +import javax.jcr.security.AccessControlEntry; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * EntriesCache... + */ +class EntriesCache extends AccessControlObserver implements AccessControlConstants { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(EntriesCache.class); + + private final SessionImpl systemSession; + private final ACLEditor systemEditor; + + private final String repPolicyName; + + /** + * Cache to look up the list of access control entries defined for a given + * set of principals. + */ + private final Map> cache; + private final Object monitor = new Object(); + + /** + * + * @param systemSession + * @param systemEditor + * @param accessControlRootPath + * @throws javax.jcr.RepositoryException + */ + EntriesCache(SessionImpl systemSession, ACLEditor systemEditor, + String accessControlRootPath) throws RepositoryException { + this.systemSession = systemSession; + this.systemEditor = systemEditor; + + repPolicyName = systemSession.getJCRName(N_POLICY); + + cache = new LRUMap(1000); + + ObservationManager observationMgr = systemSession.getWorkspace().getObservationManager(); + /* + Make sure the collector and all subscribed listeners are informed upon + ACL modifications. Interesting events are: + - new ACL (NODE_ADDED) + - new ACE (NODE_ADDED) + - changing ACE (PROPERTY_CHANGED) + - removed ACL (NODE_REMOVED) + - removed ACE (NODE_REMOVED) + */ + int events = Event.PROPERTY_CHANGED | Event.NODE_ADDED | Event.NODE_REMOVED; + String[] ntNames = new String[] { + systemSession.getJCRName(NT_REP_ACCESS_CONTROLLABLE), + systemSession.getJCRName(NT_REP_ACL), + systemSession.getJCRName(NT_REP_ACE) + }; + observationMgr.addEventListener(this, events, accessControlRootPath, true, null, ntNames, false); + } + + /** + * Release all resources contained by this instance. It will no longer be + * used. This implementation only stops listening to ac modification events. + */ + @Override + protected void close() { + super.close(); + try { + systemSession.getWorkspace().getObservationManager().removeEventListener(this); + } catch (RepositoryException e) { + log.error("Unexpected error while closing CachingEntryCollector", e); + } + } + + List getEntries(Collection principals) throws RepositoryException { + String key = getCacheKey(principals); + List entries; + synchronized (monitor) { + entries = cache.get(key); + if (entries == null) { + // acNodes must be ordered in the same order as the principals + // in order to obtain proper acl-evaluation in case the given + // principal-set is ordered. + entries = new ArrayList(); + // build acl-hierarchy assuming that principal-order determines + // the acl-inheritance. + for (Principal p : principals) { + ACLTemplate acl = systemEditor.getACL(p); + if (acl != null && !acl.isEmpty()) { + AccessControlEntry[] aces = acl.getAccessControlEntries(); + entries.addAll(Arrays.asList(aces)); + } + } + cache.put(key, entries); + } + } + return entries; + } + + private static String getCacheKey(Collection principals) { + StringBuilder sb = new StringBuilder(); + for (Principal p : principals) { + sb.append(p.getName()).append('/'); + } + return sb.toString(); + } + + //------------------------------------------------------< EventListener >--- + /** + * @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator) + */ + public synchronized void onEvent(EventIterator events) { + /* map of path-string to modification type. + the path identifies the access-controlled node for a given principal.*/ + Map modMap = new HashMap(); + + // collect the paths of all access controlled nodes that have been affected + // by the events and thus need their cache entries updated or cleared. + while (events.hasNext()) { + try { + Event ev = events.nextEvent(); + String identifier = ev.getIdentifier(); + String path = ev.getPath(); + + switch (ev.getType()) { + case Event.NODE_ADDED: + NodeImpl n = (NodeImpl) systemSession.getNodeByIdentifier(identifier); + if (n.isNodeType(NT_REP_ACL)) { + // a new ACL was added -> use the added node to update + // the cache. + modMap.put(Text.getRelativeParent(path, 1), POLICY_ADDED); + } else if (n.isNodeType(NT_REP_ACE)) { + // a new ACE was added -> use the parent node (acl) + // to update the cache. + modMap.put(Text.getRelativeParent(path, 2), POLICY_MODIFIED); + } /* else: some other node added below an access controlled + parent node -> not interested. */ + break; + case Event.NODE_REMOVED: + String parentPath = Text.getRelativeParent(path, 1); + if (systemSession.nodeExists(parentPath)) { + NodeImpl parent = (NodeImpl) systemSession.getNode(parentPath); + if (repPolicyName.equals(Text.getName(path))){ + // the complete acl was removed -> clear cache entry + modMap.put(parentPath, POLICY_REMOVED); + } else if (parent.isNodeType(NT_REP_ACL)) { + // an ace was removed -> refresh cache for the + // containing access control list upon next access + modMap.put(Text.getRelativeParent(path, 2), POLICY_MODIFIED); + } /* else: + a) some other child node of an access controlled + node -> not interested. + b) a child node of an ACE. not relevant for this + implementation -> ignore + */ + } else { + log.debug("Cannot process NODE_REMOVED event. Parent " + parentPath + " doesn't exist (anymore)."); + } + break; + case Event.PROPERTY_CHANGED: + // test if the changed prop belongs to an ACE + NodeImpl parent = (NodeImpl) systemSession.getNodeByIdentifier(identifier); + if (parent.isNodeType(NT_REP_ACE)) { + modMap.put(Text.getRelativeParent(path, 3), POLICY_MODIFIED); + } /* some other property below an access controlled node + changed -> not interested. (NOTE: rep:ACL doesn't + define any properties. */ + break; + default: + // illegal event-type: should never occur. ignore + } + + } catch (RepositoryException e) { + // should never get here + log.warn("Internal error: {}", e.getMessage()); + } + + } + + if (!modMap.isEmpty()) { + // notify listeners and eventually clean up internal caches. + synchronized (monitor) { + cache.clear(); + } + notifyListeners(new AccessControlModifications(modMap)); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalIterator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalIterator.java new file mode 100644 index 00000000000..f597aa6ad41 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalIterator.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.core.security.principal; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.Principal; +import java.util.NoSuchElementException; + +/** + * Lazy implementation of the PrincipalIterator that allows to + * avoid retrieving all elements beforehand. + * NOTE: Subclasses must call {#link seekNext()} + * upon object construction and assign the value to the 'next' field. + */ +abstract class AbstractPrincipalIterator implements PrincipalIterator { + + private static Logger log = LoggerFactory.getLogger(AbstractPrincipalIterator.class); + + long size; + long position; + Principal next; + + AbstractPrincipalIterator() { + size = -1; + position = 0; + } + + /** + * Subclasses must call {#link seekNext()} upon object construction and + * assign the value to the 'next' field. + * + * @return The principal to be return upon the subsequent {@link #next()} + * or {@link #nextPrincipal()} call or null if no next principal + * exists. + */ + abstract Principal seekNext(); + + //--------------------------------------------------< PrincipalIterator >--- + public Principal nextPrincipal() { + Principal p = next; + if (p == null) { + throw new NoSuchElementException(); + } + next = seekNext(); + position++; + return p; + } + + //------------------------------------------------------< RangeIterator >--- + public void skip(long skipNum) { + while (skipNum-- > 0) { + next(); + } + } + + public long getSize() { + return size; + } + + public long getPosition() { + return position; + } + + //-----------------------------------------------------------< Iterator >--- + public boolean hasNext() { + return next != null; + } + + public Object next() { + return nextPrincipal(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalProvider.java new file mode 100644 index 00000000000..43250a098c9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalProvider.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.core.security.principal; + +import org.apache.commons.collections.map.LRUMap; + +import java.security.Principal; +import java.util.Properties; + +/** + * A base class of a principal provider implementing common tasks and a + * caching facility. Extending classes should only deal with the retrieval of + * {@link Principal}s from their source, the caching of the principals is done + * by this implementation. + *

    + * The {@link PrincipalProvider} methods that that involve searching like + * {@link PrincipalProvider#getGroupMembership(Principal)} are not cached. + */ +public abstract class AbstractPrincipalProvider implements PrincipalProvider { + + /** Option name for the max size of the cache to use */ + public static final String MAXSIZE_KEY = "cacheMaxSize"; + /** Option name to enable negative cache entries (see JCR-2672) */ + public static final String NEGATIVE_ENTRY_KEY = "cacheIncludesNegative"; + + /** flag indicating if the instance has not been {@link #close() closed} */ + private boolean initialized; + + /** + * flag indicating if the cache should include 'negative' entries. + * @see #NEGATIVE_ENTRY_KEY + */ + private boolean includeNegative; + + /** the principal cache */ + private LRUMap cache; + + /** + * Create a new instance of AbstractPrincipalProvider. + * Initialization and cache are set up upon {@link #init(Properties)} + */ + protected AbstractPrincipalProvider() { + } + + /** + * Check if the instance has been closed {@link #close()}. + * + * @throws IllegalStateException if this instance was closed. + */ + protected void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("Not initialized."); + } + } + + /** + * Clear the principal cache. + */ + protected synchronized void clearCache() { + cache.clear(); + } + + /** + * Add an entry to the principal cache. + * + * @param principal to be cached. + */ + protected synchronized void addToCache(Principal principal) { + cache.put(principal.getName(), principal); + } + + /** + * Called if the cache does not contain the principal requested.
    + * Implementations should return a {@link Principal} from their source, + * if it contains one for the given name or null. + * + * @param principalName Name of the principal to be returned. + * @return Principal or null, if non provided for the given name + * @see #getPrincipal(String) + */ + protected abstract Principal providePrincipal(String principalName); + + //--------------------------------------------------< PrincipalProvider >--- + /** + * {@inheritDoc} + * + * {@link #providePrincipal(String)} is called, if no matching entry + * is present in the cache.
    + * NOTE: If the cache is enabled to contain negative entries (see + * {@link #NEGATIVE_ENTRY_KEY} configuration option), the cache will also + * store negative matches (as null values) in the principal cache. + */ + public synchronized Principal getPrincipal(String principalName) { + checkInitialized(); + if (cache.containsKey(principalName)) { + return (Principal) cache.get(principalName); + } else { + Principal principal = providePrincipal(principalName); + if (principal != null || includeNegative) { + cache.put(principalName, principal); + } + return principal; + } + } + + /** + * @see PrincipalProvider#init(java.util.Properties) + */ + public synchronized void init(Properties options) { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + + int maxSize = Integer.parseInt(options.getProperty(MAXSIZE_KEY, "1000")); + cache = new LRUMap(maxSize); + includeNegative = Boolean.parseBoolean(options.getProperty(NEGATIVE_ENTRY_KEY, "false")); + + initialized = true; + } + + /** + * Clears the cache and calls the implementation to close their resources + * @see PrincipalProvider#close() + */ + public synchronized void close() { + checkInitialized(); + cache.clear(); + initialized = false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AdminPrincipal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AdminPrincipal.java new file mode 100644 index 00000000000..60f82cf06ac --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/AdminPrincipal.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.core.security.principal; + +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; + +/** + * This principal represents the admin user as a distinct principal having all + * the access rights and is being authenticated (in contrast to the + * {@link org.apache.jackrabbit.core.security.SystemPrincipal SystemPrincipal}). + */ +public class AdminPrincipal implements JackrabbitPrincipal { + + private final String adminId; + + public AdminPrincipal(String adminId) { + if (adminId == null) { + throw new IllegalArgumentException("name can not be null"); + } + this.adminId = adminId; + } + + public String getName() { + return adminId; + } + + @Override + public int hashCode() { + return adminId.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj == this || obj instanceof AdminPrincipal; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/DefaultPrincipalProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/DefaultPrincipalProvider.java new file mode 100644 index 00000000000..1e9fc06168f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/DefaultPrincipalProvider.java @@ -0,0 +1,369 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.principal; + +import org.apache.commons.collections.iterators.IteratorChain; +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.api.security.user.UserManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.core.security.user.UserManagerImpl; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import java.security.Principal; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Properties; +import java.util.Set; + +/** + * Provides principals for the users contained within the Repository. + *

    + * Each {@link Authorizable} accessible via {@link UserManager} + * is respected and the provider serves {@link Authorizable#getPrincipal() + * Principal}s retrieved from those Authorizable objects. + *

    + * In addition this provider exposes the everyone principal, which has no + * content (user/group) representation. + *

    + * Unless explicitly configured (see {@link #NEGATIVE_ENTRY_KEY negative entry + * option} this implementation of the PrincipalProvider interface + * caches both positive and negative (null) results of the {@link #providePrincipal} + * method. The cache is kept up to date by observation listening to creation + * and removal of users and groups. + *

    + * Membership cache:
    + * In addition to the caching provided by AbstractPrincipalProvider + * this implementation keeps an extra membership cache, which is notified in + * case of changes made to the members of any group. + */ +public class DefaultPrincipalProvider extends AbstractPrincipalProvider implements SynchronousEventListener { + + /** + * the default logger + */ + private static Logger log = LoggerFactory.getLogger(DefaultPrincipalProvider.class); + + /** + * Principal-Base of this Provider + */ + private final UserManagerImpl userManager; + + private final EveryonePrincipal everyonePrincipal; + private final String pPrincipalName; + + /** + * Creates a new DefaultPrincipalProvider reading the principals from the + * storage below the given security root node. + * + * @param systemSession for repository access. + * @param systemUserManager Used to retrieve the principals. + * @throws RepositoryException if an error accessing the repository occurs. + */ + public DefaultPrincipalProvider(Session systemSession, + UserManagerImpl systemUserManager) throws RepositoryException { + + this.userManager = systemUserManager; + everyonePrincipal = EveryonePrincipal.getInstance(); + + String[] ntNames = new String[1]; + if (systemSession instanceof SessionImpl) { + NameResolver resolver = (NameResolver) systemSession; + ntNames[0] = resolver.getJCRName(UserManagerImpl.NT_REP_AUTHORIZABLE_FOLDER); + pPrincipalName = resolver.getJCRName(UserManagerImpl.P_PRINCIPAL_NAME); + } else { + ntNames[0] = "rep:AuthorizableFolder"; + pPrincipalName = "rep:principalName"; + } + + String groupPath = userManager.getGroupsPath(); + String userPath = userManager.getUsersPath(); + String targetPath = groupPath; + while (!Text.isDescendantOrEqual(targetPath, userPath)) { + targetPath = Text.getRelativeParent(targetPath, 1); + } + systemSession.getWorkspace().getObservationManager().addEventListener(this, + Event.NODE_ADDED | Event.NODE_REMOVED, targetPath, true, null, ntNames, false); + } + + //------------------------------------------< AbstractPrincipalProvider >--- + /** + * {@inheritDoc} + *

    + * This implementation uses the user and node resolver to find the + * appropriate nodes. + */ + @Override + protected Principal providePrincipal(String principalName) { + try { + Principal principal = new PrincipalImpl(principalName); + Authorizable ath = userManager.getAuthorizable(principal); + if (ath != null) { + return ath.getPrincipal(); + } else if (EveryonePrincipal.NAME.equals(principalName)) { + return everyonePrincipal; + } + } catch (RepositoryException e) { + log.error("Failed to access Authorizable for Principal " + principalName, e); + } + return null; + } + + /** + * Sets the {@link #NEGATIVE_ENTRY_KEY} option value to true if + * it isn't included yet in the passed options, before calling the init + * method of the base class. + * + * @param options + */ + @Override + public void init(Properties options) { + if (!options.containsKey(NEGATIVE_ENTRY_KEY)) { + options.put(NEGATIVE_ENTRY_KEY, "true"); + } + super.init(options); + } + + //--------------------------------------------------< PrincipalProvider >--- + /** + * @see PrincipalProvider#findPrincipals(String) + */ + public PrincipalIterator findPrincipals(String simpleFilter) { + return findPrincipals(simpleFilter, PrincipalManager.SEARCH_TYPE_ALL); + } + + /** + * @see PrincipalProvider#findPrincipals(String, int) + */ + public PrincipalIterator findPrincipals(String simpleFilter, int searchType) { + checkInitialized(); + switch (searchType) { + case PrincipalManager.SEARCH_TYPE_GROUP: + return findGroupPrincipals(simpleFilter); + case PrincipalManager.SEARCH_TYPE_NOT_GROUP: + return findUserPrincipals(simpleFilter); + case PrincipalManager.SEARCH_TYPE_ALL: + PrincipalIterator[] its = new PrincipalIterator[] { + findUserPrincipals(simpleFilter), + findGroupPrincipals(simpleFilter) + }; + return new PrincipalIteratorAdapter(new IteratorChain(its)); + default: + throw new IllegalArgumentException("Invalid searchType"); + } + } + + /** + * @see PrincipalProvider#getPrincipals(int) + * @param searchType Any of the following search types: + *

      + *
    • {@link PrincipalManager#SEARCH_TYPE_GROUP}
    • + *
    • {@link PrincipalManager#SEARCH_TYPE_NOT_GROUP}
    • + *
    • {@link PrincipalManager#SEARCH_TYPE_ALL}
    • + *
    + * @see PrincipalProvider#getPrincipals(int) + */ + public PrincipalIterator getPrincipals(int searchType) { + return findPrincipals(null, searchType); + } + + /** + * @see PrincipalProvider#getGroupMembership(Principal) + */ + public PrincipalIterator getGroupMembership(Principal userPrincipal) { + checkInitialized(); + Set mship = collectGroupMembership(userPrincipal); + // make sure everyone-group is not missing + if (!mship.contains(everyonePrincipal) && everyonePrincipal.isMember(userPrincipal)) { + mship.add(everyonePrincipal); + } + return new PrincipalIteratorAdapter(mship); + + } + + /** + * @see PrincipalProvider#close() + */ + @Override + public synchronized void close() { + super.close(); + } + + /** + * @see PrincipalProvider#canReadPrincipal(javax.jcr.Session,java.security.Principal) + */ + public boolean canReadPrincipal(Session session, Principal principal) { + checkInitialized(); + // check if the session can read the user/group associated with the + // given principal + if (session instanceof SessionImpl) { + SessionImpl sImpl = (SessionImpl) session; + if (sImpl.isAdmin() || sImpl.isSystem()) { + return true; + } + try { + UserManager umgr = sImpl.getUserManager(); + return umgr.getAuthorizable(principal) != null; + } catch (RepositoryException e) { + log.error("Failed to determine accessibility of Principal {}", principal, e); + } + } + return false; + } + + //------------------------------------------------------< EventListener >--- + /** + * @see EventListener#onEvent(EventIterator) + */ + public void onEvent(EventIterator eventIterator) { + // superclass: flush all cached + clearCache(); + } + + //-------------------------------------------------------------------------- + /** + * Recursively collect all Group-principals the specified principal is + * member of. + * + * @param princ Principal for which the group membership will be collected. + * @return all Group principals the specified princ is member of + * including inherited membership. + */ + private Set collectGroupMembership(Principal princ) { + final Set membership = new LinkedHashSet(); + try { + final Authorizable auth = userManager.getAuthorizable(princ); + if (auth != null) { + /* + make sure the principal is contained in the cache. + however, avoid putting the given 'princ' but assert that + the cached principal is obtained with the system session + used to deliver principals with this provider implementation. + */ + addToCache(auth.getPrincipal()); + Iterator itr = auth.memberOf(); + while (itr.hasNext()) { + Group group = itr.next(); + Principal gp = group.getPrincipal(); + addToCache(gp); + membership.add(gp); + } + } else { + log.debug("Cannot find authorizable for principal " + princ.getName()); + } + } catch (RepositoryException e) { + log.warn("Failed to determine membership for " + princ.getName(), e.getMessage()); + } + return membership; + } + + /** + * @param simpleFilter Principal name or fragment. + * @return An iterator over the main principals of the authorizables found + * by the user manager. + */ + private PrincipalIterator findUserPrincipals(String simpleFilter) { + synchronized (userManager) { + try { + Iterator itr = userManager.findAuthorizables(pPrincipalName, simpleFilter, UserManager.SEARCH_TYPE_USER); + return new PrincipalIteratorImpl(itr, false); + } catch (RepositoryException e) { + log.error("Error while searching user principals.", e); + return PrincipalIteratorAdapter.EMPTY; + } + } + } + + /** + * @param simpleFilter Principal name or fragment. + * @return An iterator over the main principals of the authorizables found + * by the user manager. + */ + private PrincipalIterator findGroupPrincipals(final String simpleFilter) { + synchronized (userManager) { + try { + Iterator itr = userManager.findAuthorizables(pPrincipalName, simpleFilter, UserManager.SEARCH_TYPE_GROUP); + + // everyone will not be found by the user manager -> extra test + boolean addEveryone = everyonePrincipal.getName().matches(".*"+simpleFilter+".*"); + return new PrincipalIteratorImpl(itr, addEveryone); + + } catch (RepositoryException e) { + log.error("Error while searching group principals.", e); + return PrincipalIteratorAdapter.EMPTY; + } + } + } + + //--------------------------------------------------------< inner class >--- + /** + * Extension of AbstractPrincipalIterator that retrieves the next + * principal from the iterator over authorizables by calling + * {@link Authorizable#getPrincipal()}. + */ + private class PrincipalIteratorImpl extends AbstractPrincipalIterator { + + private final Iterator authorizableItr; + private boolean addEveryone; + + private PrincipalIteratorImpl(Iterator authorizableItr, boolean addEveryone) { + this.authorizableItr = authorizableItr; + this.addEveryone = addEveryone; + + next = seekNext(); + } + + /** + * @see org.apache.jackrabbit.core.security.principal.AbstractPrincipalIterator#seekNext() + */ + @Override + protected Principal seekNext() { + while (authorizableItr.hasNext()) { + try { + Principal p = authorizableItr.next().getPrincipal(); + if (everyonePrincipal.equals(p)) { + addEveryone = false; + } + addToCache(p); + return p; + } catch (RepositoryException e) { + // should never get here + log.warn("Error while retrieving principal from group -> skip."); + } + } + + if (addEveryone) { + addEveryone = false; // make sure iteration stops + return everyonePrincipal; + } else { + // end of iteration reached + return null; + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/EveryonePrincipal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/EveryonePrincipal.java new file mode 100644 index 00000000000..dfa3a0281b5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/EveryonePrincipal.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.core.security.principal; + +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; + +import java.security.Principal; +import java.util.Enumeration; + +/** + * The EveryonePrincipal contains all principals (excluding itself). + */ +public final class EveryonePrincipal implements java.security.acl.Group, GroupPrincipal, JackrabbitPrincipal { + + public static final String NAME = "everyone"; + private static final EveryonePrincipal INSTANCE = new EveryonePrincipal(); + + private EveryonePrincipal() { } + + public static EveryonePrincipal getInstance() { + return INSTANCE; + } + + //----------------------------------------------------------< Principal >--- + public String getName() { + return NAME; + } + + //--------------------------------------------------------------< Group >--- + public boolean addMember(Principal user) { + return false; + } + + public boolean removeMember(Principal user) { + throw new UnsupportedOperationException("Cannot remove a member from the everyone group."); + } + + public boolean isMember(Principal member) { + return !member.equals(this); + } + + 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 || obj instanceof EveryonePrincipal) { + return true; + } else if (obj instanceof JackrabbitPrincipal) { + return NAME.equals(((JackrabbitPrincipal) obj).getName()); + } + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/FallbackPrincipalProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/FallbackPrincipalProvider.java new file mode 100644 index 00000000000..fa5e602c23b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/FallbackPrincipalProvider.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.core.security.principal; + +import java.security.Principal; +import java.util.Properties; + +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; + +/** + * The FallbackPrincipalProvider is used to provide any desired + * principal. It is used to defined ACE for principals that are not known to + * the repository yet or that were deleted. + */ +public class FallbackPrincipalProvider implements PrincipalProvider { + + /** + * name of the "disabled" option. + */ + public static final String OPTION_DISABLED = "disabled"; + + /** + * If true this principal provider is disabled. + */ + private boolean disabled; + + /** + * {@inheritDoc} + * + * @return a {@link UnknownPrincipal} with the given name. + */ + public Principal getPrincipal(String principalName) { + return disabled ? null : new UnknownPrincipal(principalName); + } + + /** + * {@inheritDoc} + * + * @return an empty principal iterator + */ + public PrincipalIterator findPrincipals(String simpleFilter) { + return PrincipalIteratorAdapter.EMPTY; + } + + /** + * {@inheritDoc} + * + * @return an empty principal iterator + */ + public PrincipalIterator findPrincipals(String simpleFilter, int searchType) { + return PrincipalIteratorAdapter.EMPTY; + } + + /** + * {@inheritDoc} + * + * @return an empty principal iterator + */ + public PrincipalIterator getPrincipals(int searchType) { + return PrincipalIteratorAdapter.EMPTY; + } + + /** + * {@inheritDoc} + * + * @return an empty principal iterator + */ + public PrincipalIterator getGroupMembership(Principal principal) { + return PrincipalIteratorAdapter.EMPTY; + } + + /** + * {@inheritDoc} + */ + public void init(Properties options) { + disabled = "true".equals(options.get(OPTION_DISABLED)); + } + + /** + * {@inheritDoc} + */ + public void close() { + } + + /** + * {@inheritDoc} + * + * @return true + */ + public boolean canReadPrincipal(Session session, Principal principalToRead) { + return true; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/GroupPrincipals.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/GroupPrincipals.java new file mode 100644 index 00000000000..62953a7d196 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/GroupPrincipals.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.core.security.principal; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.Collections; +import java.util.Enumeration; + +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; + +/** + * Helper class to deal with the migration between the 2 types of groups + * + */ +public final class GroupPrincipals { + + private GroupPrincipals() { + } + + /** + * Checks if the provided principal is a group. + * + * @param principal + * to be checked. + * + * @return true if the principal is of type group. + */ + public static boolean isGroup(Principal principal) { + return principal instanceof Group || principal instanceof GroupPrincipal; + } + + /** + * Returns an enumeration of the members in the group. + * @param principal the principal whose membership is listed. + * @return an enumeration of the group members. + */ + public static Enumeration members(Principal principal) { + if (principal instanceof Group) { + return ((Group) principal).members(); + } + if (principal instanceof GroupPrincipal) { + return ((GroupPrincipal) principal).members(); + } + return Collections.emptyEnumeration(); + } + + /** + * Returns true if the passed principal is a member of the group. + * @param principal the principal whose members are being checked. + * @param member the principal whose membership is to be checked. + * @return true if the principal is a member of this group, false otherwise. + */ + public static boolean isMember(Principal principal, Principal member) { + if (principal instanceof Group) { + return ((Group) principal).isMember(member); + } + if (principal instanceof GroupPrincipal) { + return ((GroupPrincipal) principal).isMember(member); + } + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalImpl.java new file mode 100644 index 00000000000..b1ba2b1d8d5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalImpl.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.core.security.principal; + +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; + +import java.io.Serializable; +import java.security.Principal; + +/** + * Base class for implementations of JackrabbitPrincipal. + */ +public class PrincipalImpl implements JackrabbitPrincipal, Serializable { + + /** the serial number */ + private static final long serialVersionUID = 384040549033267804L; + + /** + * the name of this principal + */ + private final String name; + + + /** + * Creates a new principal with the given name. + * + * @param name the name of this principal + */ + public PrincipalImpl(String name) { + if (name == null || name.length() == 0) { + throw new IllegalArgumentException("Principal name can neither be null nor empty String."); + } + this.name = name; + } + + //----------------------------------------------------------< Principal >--- + /** + * {@inheritDoc} + */ + public String getName() { + return name; + } + + //-------------------------------------------------------------< 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 name.equals(((Principal) obj).getName()); + } + return false; + } + + /** + * @return the hash code of the principals name. + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return name.hashCode(); + } + + /** + * @see Object#toString() + */ + @Override + public String toString() { + return getClass().getName() + ":" + name; + } +} + diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalIteratorAdapter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalIteratorAdapter.java new file mode 100644 index 00000000000..9a38bfb60c5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalIteratorAdapter.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.core.security.principal; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.RangeIteratorDecorator; + +import java.security.Principal; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * PrincipalIteratorAdapter... + */ +public class PrincipalIteratorAdapter extends RangeIteratorDecorator + implements PrincipalIterator { + + /** + * Static instance of an empty {@link PrincipalIterator}. + */ + public static final PrincipalIteratorAdapter EMPTY = new PrincipalIteratorAdapter(RangeIteratorAdapter.EMPTY); + + + /** + * Creates an adapter for the given {@link Iterator} of principals. + * + * @param iterator iterator of {@link Principal}s + */ + public PrincipalIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + /** + * Creates an iterator for the given collection of Principals. + * + * @param collection collection of {@link Principal} objects. + */ + public PrincipalIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //----------------------------------------< AccessControlPolicyIterator >--- + /** + * Returns the next policy. + * + * @return next policy. + * @throws NoSuchElementException if there is no next policy. + */ + public Principal nextPrincipal() throws NoSuchElementException { + return (Principal) next(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalManagerImpl.java new file mode 100644 index 00000000000..ac0281152d0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalManagerImpl.java @@ -0,0 +1,343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.principal; + +import java.security.Principal; +import java.security.acl.Group; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; + +/** + * This principal manager implementation uses the {@link DefaultPrincipalProvider} + * in order to dispatch the respective requests and assemble the required + * data. It is bound to a session and therefore obliges the access restrictions + * of the respective subject. + */ +public class PrincipalManagerImpl implements PrincipalManager { + + /** the Session this manager has been created for*/ + private final Session session; + + /** the principalProviders */ + private final PrincipalProvider[] providers; + + private boolean closed; + + /** + * Creates a new default principal manager implementation. + * + * @param session the underlying session + * @param providers the providers + */ + public PrincipalManagerImpl(Session session, PrincipalProvider[] providers) { + this.session = session; + this.providers = providers; + closed = false; + } + + /** + * {@inheritDoc} + */ + public boolean hasPrincipal(String principalName) { + return internalGetPrincipal(principalName) != null; + } + + /** + * {@inheritDoc} + */ + public Principal getPrincipal(String principalName) { + return internalGetPrincipal(principalName); + } + + /** + * {@inheritDoc} + */ + public PrincipalIterator findPrincipals(String simpleFilter) { + checkIsValid(); + List entries = new ArrayList(providers.length); + for (PrincipalProvider pp : providers) { + PrincipalIterator it = pp.findPrincipals(simpleFilter); + if (it.hasNext()) { + entries.add(new CheckedIteratorEntry(it, pp)); + } + } + return new CheckedPrincipalIterator(entries); + } + + /** + * {@inheritDoc} + */ + public PrincipalIterator findPrincipals(String simpleFilter, int searchType) { + checkIsValid(); + List entries = new ArrayList(providers.length); + for (PrincipalProvider pp : providers) { + PrincipalIterator it = pp.findPrincipals(simpleFilter, searchType); + if (it.hasNext()) { + entries.add(new CheckedIteratorEntry(it, pp)); + } + } + return new CheckedPrincipalIterator(entries); + } + + /** + * {@inheritDoc} + * @param searchType + */ + public PrincipalIterator getPrincipals(int searchType) { + checkIsValid(); + List entries = new ArrayList(providers.length); + for (PrincipalProvider pp : providers) { + PrincipalIterator it = pp.getPrincipals(searchType); + if (it.hasNext()) { + entries.add(new CheckedIteratorEntry(it, pp)); + } + } + return new CheckedPrincipalIterator(entries); + } + + /** + * {@inheritDoc} + */ + public PrincipalIterator getGroupMembership(Principal principal) { + checkIsValid(); + List entries = new ArrayList(providers.length + 1); + for (PrincipalProvider pp : providers) { + PrincipalIterator groups = pp.getGroupMembership(principal); + if (groups.hasNext()) { + entries.add(new CheckedIteratorEntry(groups, pp)); + } + } + // additional entry for the 'everyone' group + if (!(principal instanceof EveryonePrincipal)) { + Iterator it = Collections.singletonList(getEveryone()).iterator(); + entries.add(new CheckedIteratorEntry(it, null)); + } + return new CheckedPrincipalIterator(entries); + } + + /** + * {@inheritDoc} + */ + public Principal getEveryone() { + checkIsValid(); + Principal everyone = getPrincipal(EveryonePrincipal.NAME); + if (everyone == null) { + everyone = EveryonePrincipal.getInstance(); + } + return everyone; + } + + //-------------------------------------------------------------------------- + /** + * Check if the instance has been closed. + * + * @throws IllegalStateException if this instance was closed. + */ + private void checkIsValid() { + if (closed) { + throw new IllegalStateException("PrincipalManagerImpl instance has been closed."); + } + } + + /** + * @param principalName the name of the principal + * @return The principal with the given name or null if none + * of the providers knows that principal of if the Session is not allowed + * to see it. + */ + private Principal internalGetPrincipal(String principalName) { + checkIsValid(); + for (PrincipalProvider provider : providers) { + Principal principal = provider.getPrincipal(principalName); + if (principal != null && provider.canReadPrincipal(session, principal)) { + return disguise(principal, provider); + } + } + // nothing found or not allowed to see it. + return null; + } + + /** + * @param principal the principal + * @param provider the provider + * @return A group that only reveals those members that are visible to the + * current session or the specified principal if its not a group or the + * everyone principal. + */ + private Principal disguise(Principal principal, PrincipalProvider provider) { + if (!GroupPrincipals.isGroup(principal) || principal instanceof EveryonePrincipal) { + // nothing to do. + return principal; + } + // make sure all groups except for the 'everyone' group expose only + // principals visible to the session. + if (principal instanceof ItemBasedPrincipal) { + return new ItemBasedCheckedGroup(principal, provider); + } else { + return new CheckedGroup(principal, provider); + } + } + + //-------------------------------------------------------------------------- + /** + * An implementation of the Group interface that wraps another + * Group and makes sure, that all exposed members are visible to the Session + * the PrincipalManager has been built for. This is required + * due to the fact, that the principal provider is not bound to a particular + * Session object. + */ + private class CheckedGroup implements Group, GroupPrincipal, JackrabbitPrincipal { + + final Principal delegatee; + private final PrincipalProvider provider; + + private CheckedGroup(Principal delegatee, PrincipalProvider provider) { + this.delegatee = delegatee; + this.provider = provider; + } + + public boolean addMember(Principal user) { + throw new UnsupportedOperationException("Not implemented"); + } + + public boolean removeMember(Principal user) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public boolean isMember(Principal member) { + return GroupPrincipals.isMember(delegatee, member); + } + + public Enumeration members() { + Iterator it = Collections.list(GroupPrincipals.members(delegatee)).iterator(); + final PrincipalIterator members = new CheckedPrincipalIterator(it, provider); + return new Enumeration() { + public boolean hasMoreElements() { + return members.hasNext(); + } + public Principal nextElement() { + return members.nextPrincipal(); + } + }; + } + + public String getName() { + return delegatee.getName(); + } + + //---------------------------------------------------------< Object >--- + @Override + public int hashCode() { + return delegatee.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return delegatee.equals(obj instanceof CheckedGroup ? ((CheckedGroup) obj).delegatee : obj); + } + } + + /** + * Same as {@link CheckedGroup} but wrapping an ItemBasePrincipal. + */ + private class ItemBasedCheckedGroup extends CheckedGroup implements ItemBasedPrincipal { + + private ItemBasedCheckedGroup(Principal delegatee, PrincipalProvider provider) { + super(delegatee, provider); + if (!(delegatee instanceof ItemBasedPrincipal)) { + throw new IllegalArgumentException(); + } + } + + public String getPath() throws RepositoryException { + return ((ItemBasedPrincipal) delegatee).getPath(); + } + } + + //-------------------------------------------------------------------------- + /** + * A PrincipalIterator implementation that tests for each principal + * in the passed base iterators whether it is visible to the Session + * the PrincipalManager has been built for. A principal that is not + * accessible is skipped during the iteration. + */ + private class CheckedPrincipalIterator extends AbstractPrincipalIterator { + + private final List entries; + + private CheckedPrincipalIterator(Iterator it, PrincipalProvider provider) { + entries = new ArrayList(1); + entries.add(new CheckedIteratorEntry(it, provider)); + next = seekNext(); + } + + private CheckedPrincipalIterator(List entries) { + this.entries = new ArrayList(entries); + next = seekNext(); + } + + /** + * @see org.apache.jackrabbit.core.security.principal.AbstractPrincipalIterator#seekNext() + */ + @Override + protected final Principal seekNext() { + while (!entries.isEmpty()) { + // first test if current iterator has more elements + CheckedIteratorEntry current = entries.get(0); + Iterator iterator = current.iterator; + while (iterator.hasNext()) { + Principal chk = iterator.next(); + if (current.provider == null || + current.provider.canReadPrincipal(session, chk)) { + return disguise(chk, current.provider); + } + } + // no more elements in current iterator -> move to next iterator. + entries.remove(0); + } + return null; + } + } + + //-------------------------------------------------------------------------- + /** + * + */ + private static class CheckedIteratorEntry { + + private final PrincipalProvider provider; + private final Iterator iterator; + + private CheckedIteratorEntry(Iterator iterator, PrincipalProvider provider) { + this.iterator = iterator; + this.provider = provider; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalProvider.java new file mode 100644 index 00000000000..8e11c5f39d8 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalProvider.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.core.security.principal; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; + +import javax.jcr.Session; +import java.security.Principal; +import java.util.Properties; + +/** + * This interface defines methods to provide access to sources of + * {@link Principal}s. This allows the security framework share any external + * sources for authorization and authentication, as may be used by a custom + * {@link javax.security.auth.spi.LoginModule} for example. + * + * @see org.apache.jackrabbit.api.security.principal.PrincipalManager for more details about principals, users and groups. + */ +public interface PrincipalProvider { + + /** + * Returns the principal with the given name if is known to this provider + * + * @param principalName the name of the principal to retrieve + * @return return the requested principal or null + */ + Principal getPrincipal(String principalName); + + /** + * Searches for Principals that match the given String. + * NOTE: Groups are included in the search result. + * + * @param simpleFilter + * @return + * @see #findPrincipals(String,int) + */ + PrincipalIterator findPrincipals(String simpleFilter); + + /** + * Searches for Principals that match the given String. + * + * @param simpleFilter + * @param searchType searchType Any of the following constants: + *
      + *
    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_ALL}
    • + *
    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_GROUP}
    • + *
    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_NOT_GROUP}
    • + *
    + * @return + * @see #findPrincipals(String) + */ + PrincipalIterator findPrincipals(String simpleFilter, int searchType); + + /** + * Returns an iterator over all principals that match the given search type. + * + * @return an iterator over all principals that match the given search type. + * @param searchType searchType Any of the following constants: + *
      + *
    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_ALL}
    • + *
    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_GROUP}
    • + *
    • {@link org.apache.jackrabbit.api.security.principal.PrincipalManager#SEARCH_TYPE_NOT_GROUP}
    • + *
    + */ + PrincipalIterator getPrincipals(int searchType); + + /** + * Returns an iterator over all group principals for which the given + * principal is either direct or indirect member of. If a principal is + * a direct member of a group, then + * {@link org.apache.jackrabbit.api.security.principal.GroupPrincipal#isMember(Principal)} + * evaluates to true. A principal is an indirect member of a + * group if any of its groups (to any degree of separation) is direct member + * of the group. + *

    + * 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. + */ + PrincipalIterator getGroupMembership(Principal principal); + + /** + * Initialize this provider. + * + * @param options the options that are set + */ + void init(Properties options); + + /** + * This is called when a provider is not longer used by the repository. + * An implementation can then release any resources bound to this + * provider, eg. disconnect from a back end system. + */ + void close(); + + /** + * Tests if the provided session is allowed to read the given principal. + * Since the principal providers do not restrict the access + * on the principals they provide, this method is used by the PrincipalManger + * to ensure proper access rights for the client requesting the principals. + * + * @param session + * @param principalToRead The principal to be accessed by the specified subject. + * @return true if the session is allowed to read the principal; + * false otherwise. + */ + boolean canReadPrincipal(Session session, Principal principalToRead); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalProviderRegistry.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalProviderRegistry.java new file mode 100644 index 00000000000..1ebaf8ae327 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/PrincipalProviderRegistry.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.core.security.principal; + +import javax.jcr.RepositoryException; +import java.util.Properties; + +/** + * Registry used to store and retrieve PrincipalProviders. + * + * @see PrincipalProvider + */ +public interface PrincipalProviderRegistry { + + /** + * Registers a new provider by means of a configuration. The + * registry expects the properties to contain a + * {@link org.apache.jackrabbit.core.config.LoginModuleConfig#PARAM_PRINCIPAL_PROVIDER_CLASS} + * to be able to create a instance of PrincipalProvider. + *

    + * The Properties will be passed to the instantiated Provider via + * {@link PrincipalProvider#init(Properties)} + * + * @param configuration Properties for the Provider + * @return the newly added Provider or null if the configuration + * was incomplete. + * @throws RepositoryException if an error occurs while creating the + * provider instance. + */ + PrincipalProvider registerProvider(Properties configuration) throws RepositoryException; + + /** + * @return the default principal provider + */ + PrincipalProvider getDefault(); + + /** + * @param className Name of the principal provider class. + * @return PrincipalProvider or null if no provider with + * the given class name was registered. + */ + PrincipalProvider getProvider(String className); + + /** + * Returns all registered providers. + * + * @return all registered providers. + */ + PrincipalProvider[] getProviders(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/ProviderRegistryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/ProviderRegistryImpl.java new file mode 100644 index 00000000000..6a233db45f1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/ProviderRegistryImpl.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.core.security.principal; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.config.BeanConfig; +import org.apache.jackrabbit.core.config.LoginModuleConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the default implementation of the {@link PrincipalProviderRegistry} + * interface. + */ +public class ProviderRegistryImpl implements PrincipalProviderRegistry { + + /** the default logger */ + private static final Logger log = LoggerFactory.getLogger(ProviderRegistryImpl.class); + + private final PrincipalProvider defaultPrincipalProvider; + private final Map providers = new LinkedHashMap(); + + /** + * Create an instance of ProviderRegistryImpl with the given + * default principal provider. + * NOTE that the provider must be initialized by the caller. + * + * @param defaultPrincipalProvider The default principal provider. + */ + public ProviderRegistryImpl(PrincipalProvider defaultPrincipalProvider) { + this.defaultPrincipalProvider = defaultPrincipalProvider; + if (defaultPrincipalProvider != null) { + providers.put(defaultPrincipalProvider.getClass().getName(), defaultPrincipalProvider); + } + } + + //------------------------------------------< PrincipalProviderRegistry >--- + /** + * @see PrincipalProviderRegistry#registerProvider(Properties) + */ + public PrincipalProvider registerProvider(Properties config) throws RepositoryException { + PrincipalProvider provider = createProvider(config); + if (provider != null) { + synchronized (providers) { + String providerName = (String) config.get(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_NAME); + if (null == providerName || "".equals(providerName)) { + // no name param -> use class name instead. + providerName = provider.getClass().getName(); + } + providers.put(providerName, provider); + } + } else { + log.debug("Could not register principal provider: " + + LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS + + " configuration entry missing."); + } + return provider; + } + + /** + * @see PrincipalProviderRegistry#getDefault() + */ + public PrincipalProvider getDefault() { + return defaultPrincipalProvider; + } + + /** + * @see PrincipalProviderRegistry#getProviders() + */ + public PrincipalProvider getProvider(String className) { + synchronized (providers) { + return providers.get(className); + } + } + + /** + * @see PrincipalProviderRegistry#getProviders() + */ + public PrincipalProvider[] getProviders() { + synchronized (providers) { + Collection pps = providers.values(); + return pps.toArray(new PrincipalProvider[pps.size()]); + } + } + + //-------------------------------------------------------------------------- + /** + * Read the map and instantiate the class indicated by the + * {@link LoginModuleConfig#PARAM_PRINCIPAL_PROVIDER_CLASS} key.
    + * The class gets set the properties of the given map, via a Bean mechanism + * + * @param config Configuration properties. + * @return the new provider instance or null if the + * configuration does not contain the required entry. + * @throws RepositoryException If an error occurs while creating the + * principal provider. + */ + private PrincipalProvider createProvider(Properties config) + throws RepositoryException { + + String className = config.getProperty(LoginModuleConfig.PARAM_PRINCIPAL_PROVIDER_CLASS); + if (className == null) { + // try alternative key (backwards compatibility) + className = config.getProperty(LoginModuleConfig.COMPAT_PRINCIPAL_PROVIDER_CLASS); + } + if (className == null) { + return null; + } + + try { + Class pc = Class.forName(className, true, BeanConfig.getDefaultClassLoader()); + PrincipalProvider pp = (PrincipalProvider) pc.newInstance(); + pp.init(config); + return pp; + } catch (ClassNotFoundException e) { + throw new RepositoryException("Unable to create new principal provider.", e); + } catch (IllegalAccessException e) { + throw new RepositoryException("Unable to create new principal provider.", e); + } catch (InstantiationException e) { + throw new RepositoryException("Unable to create new principal provider.", e); + } catch (ClassCastException e) { + throw new RepositoryException("Unable to create new principal provider.", e); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/UnknownPrincipal.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/UnknownPrincipal.java new file mode 100644 index 00000000000..524d3e1c05e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/principal/UnknownPrincipal.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.core.security.principal; + +/** + * Implements a principal that is used by the ACL importer for unknown + * principals. + */ +public class UnknownPrincipal extends PrincipalImpl { + + public UnknownPrincipal(String name) { + super(name); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleAccessManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleAccessManager.java new file mode 100644 index 00000000000..53b34cd2cce --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleAccessManager.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.core.security.simple; + +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.security.AMContext; +import org.apache.jackrabbit.core.security.AbstractAccessControlManager; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.AnonymousPrincipal; +import org.apache.jackrabbit.core.security.SystemPrincipal; +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.NamedAccessControlPolicyImpl; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.security.auth.Subject; +import java.security.Principal; +import java.util.Set; + +/** + * SimpleAccessManager ... + */ +public class SimpleAccessManager extends AbstractAccessControlManager implements AccessManager { + + /** + * The policy returned upon {@link #getEffectivePolicies(String)} + */ + private static final AccessControlPolicy POLICY = new NamedAccessControlPolicyImpl("Simple AccessControlPolicy"); + + /** + * Subject whose access rights this AccessManager should reflect + */ + private Subject subject; + + /** + * hierarchy manager used for ACL-based access control model + */ + private HierarchyManager hierMgr; + + private NamePathResolver resolver; + private WorkspaceAccessManager wspAccessMgr; + private PrivilegeManager privilegeManager; + + private boolean initialized; + + private boolean system; + private boolean anonymous; + + /** + * Empty constructor + */ + public SimpleAccessManager() { + initialized = false; + anonymous = false; + system = false; + } + + //------------------------------------------------------< AccessManager >--- + /** + * {@inheritDoc} + */ + public void init(AMContext context) + throws AccessDeniedException, Exception { + init(context, null, null); + } + + /** + * {@inheritDoc} + */ + public void init(AMContext context, AccessControlProvider acProvider, WorkspaceAccessManager wspAccessManager) throws AccessDeniedException, Exception { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + + subject = context.getSubject(); + hierMgr = context.getHierarchyManager(); + resolver = context.getNamePathResolver(); + privilegeManager = ((JackrabbitWorkspace) context.getSession().getWorkspace()).getPrivilegeManager(); + wspAccessMgr = wspAccessManager; + anonymous = !subject.getPrincipals(AnonymousPrincipal.class).isEmpty(); + system = !subject.getPrincipals(SystemPrincipal.class).isEmpty(); + + // @todo check permission to access given workspace based on principals + initialized = true; + + if (!canAccess(context.getWorkspaceName())) { + throw new AccessDeniedException("Not allowed to access Workspace " + context.getWorkspaceName()); + } + } + + /** + * {@inheritDoc} + */ + public synchronized void close() throws Exception { + checkInitialized(); + initialized = false; + } + + /** + * {@inheritDoc} + */ + public void checkPermission(ItemId id, int permissions) + throws AccessDeniedException, RepositoryException { + if (!isGranted(id, permissions)) { + throw new AccessDeniedException("Access denied"); + } + } + + /** + * {@inheritDoc} + */ + public void checkPermission(Path absPath, int permissions) throws AccessDeniedException, RepositoryException { + if (!isGranted(absPath, permissions)) { + throw new AccessDeniedException("Access denied"); + } + } + + /** + * {@inheritDoc} + */ + public void checkRepositoryPermission(int permissions) throws AccessDeniedException, RepositoryException { + if (!isGranted((ItemId) null, permissions)) { + throw new AccessDeniedException("Access denied"); + } + } + + /** + * {@inheritDoc} + */ + public boolean isGranted(ItemId id, int permissions) throws RepositoryException { + checkInitialized(); + if (system) { + // system has always all permissions + return true; + } else if (anonymous) { + // anonymous is always denied WRITE & REMOVE permissions + if ((permissions & WRITE) == WRITE + || (permissions & REMOVE) == REMOVE) { + return false; + } + } + + // @todo check permission based on principals + return true; + } + + public boolean isGranted(Path absPath, int permissions) throws RepositoryException { + return internalIsGranted(absPath, permissions); + } + + public boolean isGranted(Path parentPath, Name childName, int permissions) throws RepositoryException { + return internalIsGranted(parentPath, permissions); + } + + public boolean canRead(Path itemPath, ItemId itemId) throws RepositoryException { + return true; + } + + private boolean internalIsGranted(Path absPath, int permissions) throws RepositoryException { + if (!absPath.isAbsolute()) { + throw new RepositoryException("Absolute path expected"); + } + checkInitialized(); + if (system) { + // system has always all permissions + return true; + } else if (anonymous) { + // anonymous is only granted READ permissions + return permissions == Permission.READ; + } + + // @todo check permission based on principals + return true; + } + + /** + * {@inheritDoc} + */ + public boolean canAccess(String workspaceName) throws RepositoryException { + if (system || wspAccessMgr == null) { + return true; + } + return wspAccessMgr.grants(subject.getPrincipals(), workspaceName); + } + + //-----------------------------------------------< AccessControlManager >--- + /** + * {@inheritDoc} + */ + public boolean hasPrivileges(String absPath, Privilege[] privileges) throws PathNotFoundException, RepositoryException { + checkInitialized(); + // make sure absPath points to an existing node + checkValidNodePath(absPath); + + if (privileges == null || privileges.length == 0) { + // null or empty privilege array -> return true + return true; + } else { + if (system) { + // system has always all permissions + return true; + } else if (anonymous) { + if (privileges.length != 1 || !privileges[0].equals(privilegeManager.getPrivilege(Privilege.JCR_READ))) { + // anonymous is only granted READ permissions + return false; + } + } + + // @todo check permission based on principals + return true; + } + } + + /** + * {@inheritDoc} + */ + public Privilege[] getPrivileges(String absPath) throws PathNotFoundException, RepositoryException { + checkInitialized(); + checkValidNodePath(absPath); + + Privilege priv; + if (anonymous) { + priv = privilegeManager.getPrivilege(Privilege.JCR_READ); + } else if (system) { + priv = privilegeManager.getPrivilege(Privilege.JCR_ALL); + } else { + // @todo check permission based on principals + priv = privilegeManager.getPrivilege(Privilege.JCR_ALL); + } + return new Privilege[] {priv}; + } + + /** + * {@inheritDoc} + */ + public AccessControlPolicy[] getEffectivePolicies(String absPath) throws PathNotFoundException, AccessDeniedException, RepositoryException { + checkInitialized(); + checkPermission(absPath, Permission.READ_AC); + + return new AccessControlPolicy[] {POLICY}; + } + + //---------------------------------------< AbstractAccessControlManager >--- + /** + * @see AbstractAccessControlManager#checkInitialized() + */ + @Override + protected void checkInitialized() throws IllegalStateException { + if (!initialized) { + throw new IllegalStateException("not initialized"); + } + } + + /** + * @see AbstractAccessControlManager#checkPermission(String,int) + */ + @Override + protected void checkPermission(String absPath, int permission) throws AccessDeniedException, PathNotFoundException, RepositoryException { + checkValidNodePath(absPath); + if (anonymous && permission != Permission.READ) { + throw new AccessDeniedException("Anonymous may only READ."); + } + } + + /** + * @see AbstractAccessControlManager#getPrivilegeManager() + */ + @Override + protected PrivilegeManager getPrivilegeManager() throws RepositoryException { + return privilegeManager; + } + + /** + * @see AbstractAccessControlManager#checkValidNodePath(String) + */ + @Override + protected void checkValidNodePath(String absPath) throws PathNotFoundException, RepositoryException { + if (absPath != null) { + Path path = resolver.getQPath(absPath); + if (!path.isAbsolute()) { + throw new RepositoryException("Absolute path expected. Found: " + absPath); + } + + if (hierMgr.resolveNodePath(path) == null) { + throw new PathNotFoundException(absPath); + } + } + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getEffectivePolicies(Set) + */ + public AccessControlPolicy[] getEffectivePolicies(Set principals) throws AccessDeniedException, AccessControlException, UnsupportedRepositoryOperationException, RepositoryException { + checkInitialized(); + /* + TOBEFIXED: + check permissions on the root node as a workaround to only expose + effective policies for principals that are allowed to see ac content. + */ + checkPermission(resolver.getQPath("/"), Permission.READ_AC); + + return new AccessControlPolicy[] {POLICY}; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#hasPrivileges(String, Set, Privilege[]) + */ + public boolean hasPrivileges(String absPath, Set principals, Privilege[] privileges) throws PathNotFoundException, RepositoryException { + if (anonymous) { + // anonymous doesn't have READ_AC privilege + throw new AccessDeniedException(); + } + + if (principals.size() == 1) { + Principal princ = principals.iterator().next(); + if (princ instanceof AnonymousPrincipal) { + return privileges.length == 1 && privileges[0].equals(privilegeManager.getPrivilege(Privilege.JCR_READ)); + } + } + + // @todo check permission based on principals + return true; + } + + /** + * @see org.apache.jackrabbit.api.security.JackrabbitAccessControlManager#getPrivileges(String, Set) + */ + public Privilege[] getPrivileges(String absPath, Set principals) throws PathNotFoundException, RepositoryException { + if (anonymous) { + // anonymous doesn't have READ_AC privilege + throw new AccessDeniedException(); + } + + if (principals.size() == 1) { + Principal princ = principals.iterator().next(); + if (princ instanceof AnonymousPrincipal) { + return new Privilege[] {privilegeManager.getPrivilege(Privilege.JCR_READ)}; + } + } + + // @todo check permission based on principals + return new Privilege[] {privilegeManager.getPrivilege(Privilege.JCR_ALL)}; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleLoginModule.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleLoginModule.java new file mode 100644 index 00000000000..26af3bfac80 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleLoginModule.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.core.security.simple; + +import org.apache.jackrabbit.core.security.authentication.AbstractLoginModule; +import org.apache.jackrabbit.core.security.authentication.Authentication; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import java.security.Principal; +import java.util.Map; + +/** + * SimpleLoginModule... + */ +public class SimpleLoginModule extends AbstractLoginModule { + + private static Logger log = LoggerFactory.getLogger(SimpleLoginModule.class); + + /** + * @see AbstractLoginModule#doInit(javax.security.auth.callback.CallbackHandler, javax.jcr.Session, java.util.Map) + */ + @Override + protected void doInit(CallbackHandler callbackHandler, Session session, Map options) throws LoginException { + // nothing to do + log.debug("init: SimpleLoginModule. Done."); + } + + /** + * @see AbstractLoginModule#impersonate(java.security.Principal, javax.jcr.Credentials) + */ + @Override + protected boolean impersonate(Principal principal, Credentials credentials) throws RepositoryException, LoginException { + if (GroupPrincipals.isGroup(principal)) { + return false; + } + Subject impersSubject = getImpersonatorSubject(credentials); + return impersSubject != null; + } + + /** + * @see AbstractLoginModule#getAuthentication(java.security.Principal, javax.jcr.Credentials) + */ + @Override + protected Authentication getAuthentication(Principal principal, Credentials creds) throws RepositoryException { + if (GroupPrincipals.isGroup(principal)) { + return null; + } + return new Authentication() { + public boolean canHandle(Credentials credentials) { + return true; + } + public boolean authenticate(Credentials credentials) throws RepositoryException { + return true; + } + }; + } + + /** + * Uses the configured {@link org.apache.jackrabbit.core.security.principal.PrincipalProvider} to retrieve the principal. + * It takes the {@link org.apache.jackrabbit.core.security.principal.PrincipalProvider#getPrincipal(String)} for the User-ID + * resolved by {@link #getUserID(Credentials)}, assuming that + * User-ID and the corresponding principal name are always identical. + * + * @param credentials Credentials for which the principal should be resolved. + * @return principal or null if the principal provider does + * not contain a user-principal with the given userID/principal name. + * + * @see AbstractLoginModule#getPrincipal(Credentials) + */ + @Override + protected Principal getPrincipal(Credentials credentials) { + String userId = getUserID(credentials); + Principal principal = principalProvider.getPrincipal(userId); + if (principal == null || GroupPrincipals.isGroup(principal)) { + // no matching user principal + return null; + } else { + return principal; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleSecurityManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleSecurityManager.java new file mode 100644 index 00000000000..26e65b7add6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleSecurityManager.java @@ -0,0 +1,390 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.simple; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.config.AccessManagerConfig; +import org.apache.jackrabbit.core.config.LoginModuleConfig; +import org.apache.jackrabbit.core.config.SecurityConfig; +import org.apache.jackrabbit.core.config.SecurityManagerConfig; +import org.apache.jackrabbit.core.security.AMContext; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.AnonymousPrincipal; +import org.apache.jackrabbit.core.security.JackrabbitSecurityManager; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.UserPrincipal; +import org.apache.jackrabbit.core.security.authentication.AuthContext; +import org.apache.jackrabbit.core.security.authentication.AuthContextProvider; +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.apache.jackrabbit.core.security.principal.PrincipalIteratorAdapter; +import org.apache.jackrabbit.core.security.principal.PrincipalManagerImpl; +import org.apache.jackrabbit.core.security.principal.PrincipalProvider; +import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry; +import org.apache.jackrabbit.core.security.principal.ProviderRegistryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SimpleSecurityManager: simple implementation ignoring both + * configuration entries for 'principalProvider' and for 'workspaceAccessManager'. + * The AccessManager is initialized using + * {@link AccessManager#init(org.apache.jackrabbit.core.security.AMContext)}. + */ +public class SimpleSecurityManager implements JackrabbitSecurityManager { + + private static Logger log = LoggerFactory.getLogger(SimpleSecurityManager.class); + + private boolean initialized; + + private SecurityConfig config; + + /** + * session on the system workspace. + */ + private Session systemSession; + + /** + * the principal provider registry + */ + private PrincipalProviderRegistry principalProviderRegistry; + + /** + * The workspace access manager + */ + private WorkspaceAccessManager workspaceAccessManager; + + /** + * factory for login-context {@see Repository#login()) + */ + private AuthContextProvider authCtxProvider; + + private String adminID; + private String anonymID; + + /** + * Always returns null. AccessControlProvider configuration + * is ignored with this security manager. Subclasses may overwrite this + * lazy behavior that originates from the SimpleAccessManager. + * + * @param systemSession The system session used to init the security manager. + * @param workspaceName The name of the workspace for which the provider + * should be retrieved. + * @return Always returns null. + */ + protected AccessControlProvider getAccessControlProvider(Session systemSession, String workspaceName) { + return null; + } + + //------------------------------------------< JackrabbitSecurityManager >--- + /** + * @see JackrabbitSecurityManager#init(Repository, Session) + */ + public void init(Repository repository, Session systemSession) throws RepositoryException { + if (initialized) { + throw new IllegalStateException("already initialized"); + } + if (!(repository instanceof RepositoryImpl)) { + throw new RepositoryException("RepositoryImpl expected"); + } + + this.systemSession = systemSession; + config = ((RepositoryImpl) repository).getConfig().getSecurityConfig(); + + // read the LoginModule configuration + LoginModuleConfig loginModConf = config.getLoginModuleConfig(); + authCtxProvider = new AuthContextProvider(config.getAppName(), loginModConf); + if (authCtxProvider.isLocal()) { + log.info("init: using Repository LoginModule configuration for " + config.getAppName()); + } else if (authCtxProvider.isJAAS()) { + log.info("init: using JAAS LoginModule configuration for " + config.getAppName()); + } else { + String msg = "No valid LoginModule configuriation for " + config.getAppName(); + log.error(msg); + throw new RepositoryException(msg); + } + + Properties[] moduleConfig = authCtxProvider.getModuleConfig(); + + // retrieve default-ids (admin and anonymous) from login-module-configuration. + for (Properties aModuleConfig1 : moduleConfig) { + if (aModuleConfig1.containsKey(LoginModuleConfig.PARAM_ADMIN_ID)) { + adminID = aModuleConfig1.getProperty(LoginModuleConfig.PARAM_ADMIN_ID); + } + if (aModuleConfig1.containsKey(LoginModuleConfig.PARAM_ANONYMOUS_ID)) { + anonymID = aModuleConfig1.getProperty(LoginModuleConfig.PARAM_ANONYMOUS_ID); + } + } + // fallback: + if (adminID == null) { + log.debug("No adminID defined in LoginModule/JAAS config -> using default."); + adminID = SecurityConstants.ADMIN_ID; + } + if (anonymID == null) { + log.debug("No anonymousID defined in LoginModule/JAAS config -> using default."); + anonymID = SecurityConstants.ANONYMOUS_ID; + } + + // most simple principal provider registry, that does not read anything + // from configuration + PrincipalProvider principalProvider = new SimplePrincipalProvider(); + // skip init of provider (nop) + principalProviderRegistry = new ProviderRegistryImpl(principalProvider); + // register all configured principal providers. + for (Properties aModuleConfig : moduleConfig) { + principalProviderRegistry.registerProvider(aModuleConfig); + } + + SecurityManagerConfig smc = config.getSecurityManagerConfig(); + if (smc != null && smc.getWorkspaceAccessConfig() != null) { + workspaceAccessManager = + smc.getWorkspaceAccessConfig().newInstance(WorkspaceAccessManager.class); + } else { + // fallback -> the default simple implementation + log.debug("No WorkspaceAccessManager configured; using default."); + workspaceAccessManager = new SimpleWorkspaceAccessManager(); + } + workspaceAccessManager.init(systemSession); + + initialized = true; + } + + /** + * @see JackrabbitSecurityManager#dispose(String) + */ + public void dispose(String workspaceName) { + checkInitialized(); + // nop + } + + /** + * @see JackrabbitSecurityManager#close() + */ + public void close() { + checkInitialized(); + } + + /** + * @see JackrabbitSecurityManager#getAccessManager(Session,AMContext) + */ + public AccessManager getAccessManager(Session session, AMContext amContext) throws RepositoryException { + checkInitialized(); + try { + String wspName = session.getWorkspace().getName(); + AccessControlProvider acP = getAccessControlProvider(systemSession, wspName); + + AccessManagerConfig amc = config.getAccessManagerConfig(); + AccessManager accessMgr; + if (amc == null) { + accessMgr = new SimpleAccessManager(); + } else { + accessMgr = amc.newInstance(AccessManager.class); + } + accessMgr.init(amContext, acP, workspaceAccessManager); + return accessMgr; + } catch (AccessDeniedException ade) { + // re-throw + throw ade; + } catch (Exception e) { + // wrap in RepositoryException + String msg = "failed to instantiate AccessManager implementation: " + SimpleAccessManager.class.getName(); + log.error(msg, e); + throw new RepositoryException(msg, e); + } + } + + /** + * @see JackrabbitSecurityManager#getPrincipalManager(Session) + */ + public synchronized PrincipalManager getPrincipalManager(Session session) + throws RepositoryException { + checkInitialized(); + if (session instanceof SessionImpl) { + SessionImpl sImpl = ((SessionImpl)session); + return new PrincipalManagerImpl(sImpl, principalProviderRegistry.getProviders()); + } else { + throw new RepositoryException("Internal error: SessionImpl expected."); + } + } + + /** + * @see JackrabbitSecurityManager#getUserManager(Session) + */ + public UserManager getUserManager(Session session) throws RepositoryException { + checkInitialized(); + throw new UnsupportedRepositoryOperationException("UserManager not supported."); + } + + /** + * @see JackrabbitSecurityManager#getUserID(javax.security.auth.Subject, String) + */ + public String getUserID(Subject subject, String workspaceName) throws RepositoryException { + String uid = null; + + // if SimpleCredentials are present, the UserID can easily be retrieved. + Iterator creds = subject.getPublicCredentials(SimpleCredentials.class).iterator(); + if (creds.hasNext()) { + SimpleCredentials sc = creds.next(); + uid = sc.getUserID(); + } else if (anonymID != null && !subject.getPrincipals(AnonymousPrincipal.class).isEmpty()) { + uid = anonymID; + } else { + // assume that UserID and principal name + // are the same (not totally correct) and thus return the name + // of the first non-group principal. + for (Principal p : subject.getPrincipals()) { + if (!GroupPrincipals.isGroup(p)) { + uid = p.getName(); + break; + } + } + } + return uid; + } + + /** + * Creates an AuthContext for the given {@link Credentials} and + * {@link Subject}.
    + * This includes selection of applicatoin specific LoginModules and + * initalization with credentials and Session to System-Workspace + * + * @return an {@link AuthContext} for the given Credentials, Subject + * @throws RepositoryException in other exceptional repository states + */ + public AuthContext getAuthContext(Credentials creds, Subject subject, String workspaceName) + throws RepositoryException { + checkInitialized(); + return authCtxProvider.getAuthContext(creds, subject, systemSession, principalProviderRegistry, adminID, anonymID); + } + + //-------------------------------------------------------------------------- + private void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("Not initialized"); + } + } + + /** + * Simple Principal provider + */ + private class SimplePrincipalProvider implements PrincipalProvider { + + private final Map principals = new HashMap(); + + private SimplePrincipalProvider() { + if (adminID != null) { + principals.put(adminID, new AdminPrincipal(adminID)); + } + if (anonymID != null) { + principals.put(anonymID, new AnonymousPrincipal()); + } + + EveryonePrincipal everyone = EveryonePrincipal.getInstance(); + principals.put(everyone.getName(), everyone); + } + + public Principal getPrincipal(String principalName) { + if (principals.containsKey(principalName)) { + return principals.get(principalName); + } else { + return new UserPrincipal(principalName); + } + } + + public PrincipalIterator findPrincipals(String simpleFilter) { + return findPrincipals(simpleFilter, PrincipalManager.SEARCH_TYPE_ALL); + } + + public PrincipalIterator findPrincipals(String simpleFilter, int searchType) { + Principal p = getPrincipal(simpleFilter); + if (p == null) { + return PrincipalIteratorAdapter.EMPTY; + } else if (GroupPrincipals.isGroup(p) && searchType == PrincipalManager.SEARCH_TYPE_NOT_GROUP || + !GroupPrincipals.isGroup(p) && searchType == PrincipalManager.SEARCH_TYPE_GROUP) { + return PrincipalIteratorAdapter.EMPTY; + } else { + return new PrincipalIteratorAdapter(Collections.singletonList(p)); + } + } + + public PrincipalIterator getPrincipals(int searchType) { + PrincipalIterator it; + switch (searchType) { + case PrincipalManager.SEARCH_TYPE_GROUP: + it = new PrincipalIteratorAdapter(Collections.singletonList(EveryonePrincipal.getInstance())); + break; + case PrincipalManager.SEARCH_TYPE_NOT_GROUP: + Set set = new HashSet(principals.values()); + set.remove(EveryonePrincipal.getInstance()); + it = new PrincipalIteratorAdapter(set); + break; + case PrincipalManager.SEARCH_TYPE_ALL: + it = new PrincipalIteratorAdapter(principals.values()); + break; + // no default + default: + throw new IllegalArgumentException("Unknown search type " + searchType); + } + return it; + } + + public PrincipalIterator getGroupMembership(Principal principal) { + if (principal instanceof EveryonePrincipal) { + return PrincipalIteratorAdapter.EMPTY; + } else { + return new PrincipalIteratorAdapter(Collections.singletonList(EveryonePrincipal.getInstance())); + } + } + + public void init(Properties options) { + // nothing to do + } + + public void close() { + // nothing to do + } + + public boolean canReadPrincipal(Session session, Principal principal) { + return true; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleWorkspaceAccessManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleWorkspaceAccessManager.java new file mode 100644 index 00000000000..e8f03869eec --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/simple/SimpleWorkspaceAccessManager.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.core.security.simple; + +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; + +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import java.util.Set; +import java.security.Principal; + +/** + * SimpleWorkspaceAccessManager always allows any set of principals + * to access any workspace. + */ +public class SimpleWorkspaceAccessManager implements WorkspaceAccessManager { + + /** + * @see WorkspaceAccessManager#init(Session) + */ + public void init(Session systemSession) { + // nothing to do + } + + /** + * @see WorkspaceAccessManager#close() + */ + public void close() throws RepositoryException { + // nothing to do. + } + + /** + * Always returns true allowing any set of principals to + * access all workspaces. + * + * @see WorkspaceAccessManager#grants(java.util.Set, String) + */ + public boolean grants(Set principals, String workspaceName) + throws RepositoryException { + return true; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/AuthorizableImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/AuthorizableImpl.java new file mode 100644 index 00000000000..9bbc0879779 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/AuthorizableImpl.java @@ -0,0 +1,529 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +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.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.PropertyDefinition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * AuthorizableImpl + */ +abstract class AuthorizableImpl implements Authorizable, UserConstants { + + static final Logger log = LoggerFactory.getLogger(AuthorizableImpl.class); + + final UserManagerImpl userManager; + private final NodeImpl node; + private int hashCode; + + /** + * @param node The node this Authorizable is persisted to. + * @param userManager UserManager that created this Authorizable. + * @throws IllegalArgumentException if the given node isn't of node type + * {@link #NT_REP_AUTHORIZABLE}. + */ + protected AuthorizableImpl(NodeImpl node, UserManagerImpl userManager) { + this.node = node; + this.userManager = userManager; + } + + //-------------------------------------------------------< Authorizable >--- + /** + * Returns the unescaped name of the node that defines this Authorizable. + * + * @return the unescaped name of the node that defines this Authorizable. + * @see Authorizable#getID() + */ + public String getID() throws RepositoryException { + return Text.unescapeIllegalJcrChars(getNode().getName()); + } + + /** + * @see Authorizable#declaredMemberOf() + */ + public Iterator declaredMemberOf() throws RepositoryException { + return collectMembership(false); + } + + /** + * @see Authorizable#memberOf() + */ + public Iterator memberOf() throws RepositoryException { + return collectMembership(true); + } + + /** + * @see Authorizable#getPropertyNames() + */ + public Iterator getPropertyNames() throws RepositoryException { + List l = new ArrayList(); + for (PropertyIterator it = node.getProperties(); it.hasNext();) { + Property prop = it.nextProperty(); + if (isAuthorizableProperty(prop, false)) { + l.add(prop.getName()); + } + } + return l.iterator(); + } + + /** + * @see Authorizable#getPropertyNames(String) + */ + public Iterator getPropertyNames(String relPath) throws RepositoryException { + Node n = node.getNode(relPath); + if (n.isSame(node)) { + // same as #getPropertyNames() + return getPropertyNames(); + } else if (Text.isDescendant(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 " + getID()); + } + } + + /** + * @see #getProperty(String) + */ + public boolean hasProperty(String relPath) throws RepositoryException { + return node.hasProperty(relPath) && isAuthorizableProperty(node.getProperty(relPath), true); + } + + /** + * @see #hasProperty(String) + * @see Authorizable#getProperty(String) + */ + public Value[] getProperty(String relPath) throws RepositoryException { + if (node.hasProperty(relPath)) { + Property prop = node.getProperty(relPath); + if (isAuthorizableProperty(prop, true)) { + if (prop.isMultiple()) { + return prop.getValues(); + } else { + return new Value[]{prop.getValue()}; + } + } + } + return null; + } + + /** + * Sets the Value for the given name. If a value existed, it is replaced, + * if not it is created. + * + * @param relPath The relative path to the property or the property name. + * @param value The property value. + * @throws RepositoryException If the specified name defines a property + * that needs to be modified by this user API or setting the corresponding + * JCR property fails. + * @see Authorizable#setProperty(String, Value) + */ + public synchronized void setProperty(String relPath, Value value) throws RepositoryException { + String name = Text.getName(relPath); + String intermediate = (relPath.equals(name)) ? null : Text.getRelativeParent(relPath, 1); + checkProtectedProperty(name); + try { + 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); + if (userManager.isAutoSave()) { + node.save(); + } + } catch (RepositoryException e) { + log.debug("Failed to set Property " + name + " for " + this, e); + node.refresh(false); + throw e; + } + } + + /** + * Sets the Value[] for the given name. If a value existed, it is replaced, + * if not it is created. + * + * @param relPath The relative path to the property or the property name. + * @param values The property values. + * @throws RepositoryException If the specified name defines a property + * that needs to be modified by this user API or setting the corresponding + * JCR property fails. + * @see Authorizable#setProperty(String, Value[]) + */ + public synchronized void setProperty(String relPath, Value[] values) throws RepositoryException { + String name = Text.getName(relPath); + String intermediate = (relPath.equals(name)) ? null : Text.getRelativeParent(relPath, 1); + checkProtectedProperty(name); + try { + 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); + if (userManager.isAutoSave()) { + node.save(); + } + } catch (RepositoryException e) { + log.debug("Failed to set Property " + name + " for " + this, e); + node.refresh(false); + throw e; + } + } + + /** + * @see Authorizable#removeProperty(String) + */ + public synchronized boolean removeProperty(String relPath) throws RepositoryException { + String name = Text.getName(relPath); + checkProtectedProperty(name); + try { + if (node.hasProperty(relPath)) { + Property p = node.getProperty(relPath); + if (isAuthorizableProperty(p, true)) { + p.remove(); + if (userManager.isAutoSave()) { + node.save(); + } + return true; + } + } + // no such property or wasn't a property of this authorizable. + return false; + } catch (RepositoryException e) { + log.debug("Failed to remove Property " + relPath + " from " + this, e); + node.refresh(false); + throw e; + } + } + + /** + * @see Authorizable#remove() + */ + public synchronized 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."); + } + Session s = getSession(); + userManager.onRemove(this); + node.remove(); + if (userManager.isAutoSave()) { + s.save(); + } + } + + /** + * @see Authorizable#getPath() + */ + public String getPath() throws UnsupportedRepositoryOperationException, RepositoryException { + return userManager.getPath(node); + } + + //-------------------------------------------------------------< Object >--- + @Override + public int hashCode() { + if (hashCode == 0) { + try { + StringBuilder sb = new StringBuilder(); + sb.append(isGroup() ? "group:" : "user:"); + sb.append(getSession().getWorkspace().getName()); + sb.append(":"); + sb.append(node.getIdentifier()); + hashCode = sb.toString().hashCode(); + } catch (RepositoryException e) { + } + } + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof AuthorizableImpl) { + AuthorizableImpl otherAuth = (AuthorizableImpl) obj; + try { + return isGroup() == otherAuth.isGroup() && node.isSame(otherAuth.node); + } catch (RepositoryException e) { + // should not occur -> return false in this case. + } + } + return false; + } + + @Override + public String toString() { + try { + String typeStr = (isGroup()) ? "Group '" : "User '"; + return new StringBuilder().append(typeStr).append(getID()).append("'").toString(); + } catch (RepositoryException e) { + return super.toString(); + } + } + + //-------------------------------------------------------------------------- + /** + * @return node The underlying Node object. + */ + NodeImpl getNode() { + return node; + } + + SessionImpl getSession() throws RepositoryException { + return (SessionImpl) node.getSession(); + } + + String getPrincipalName() throws RepositoryException { + // principal name is mandatory property -> no check required. + return node.getProperty(P_PRINCIPAL_NAME).getString(); + } + + boolean isEveryone() throws RepositoryException { + return isGroup() && EveryonePrincipal.NAME.equals(getPrincipalName()); + } + + private Iterator collectMembership(boolean includeIndirect) throws RepositoryException { + Collection groupNodeIds; + MembershipCache cache = userManager.getMembershipCache(); + String nid = node.getIdentifier(); + + final long t0 = System.nanoTime(); + boolean collect = false; + if (node.getSession().hasPendingChanges()) { + collect = true; + // avoid retrieving outdated cache entries or filling the cache with + // invalid data due to group-membership changes pending on the + // current session. + // this is mainly for backwards compatibility reasons (no cache present) + // where transient changes (in non-autosave mode) were reflected to the + // editing session (see JCR-2713) + Session session = node.getSession(); + groupNodeIds = (includeIndirect) ? cache.collectMembership(nid, session) : cache.collectDeclaredMembership(nid, session); + } else { + // retrieve cached membership. there are no pending changes. + groupNodeIds = (includeIndirect) ? cache.getMemberOf(nid) : cache.getDeclaredMemberOf(nid); + } + final long t1 = System.nanoTime(); + Set groups = new HashSet(groupNodeIds.size()); + for (String identifier : groupNodeIds) { + try { + NodeImpl n = (NodeImpl) getSession().getNodeByIdentifier(identifier); + Group group = userManager.createGroup(n); + groups.add(group); + } catch (RepositoryException e) { + // group node doesn't exist or cannot be read -> ignore. + } + } + final long t2 = System.nanoTime(); + if (log.isDebugEnabled()) { + log.debug("Collected {} {} group ids for [{}] in {}us, loaded {} groups in {}us (collect={}, cachesize={})", new Object[]{ + groupNodeIds.size(), + includeIndirect ? "all" : "declared", + getID(), + (t1-t0) / 1000, + groups.size(), + (t2-t1) / 1000, + collect, + cache.getSize() + }); + } + return new RangeIteratorAdapter(groups.iterator(), groups.size()); + } + + /** + * 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 true if the given property is defined + * by the rep:authorizable node type or one of it's sub-node types; + * false otherwise. + * @throws RepositoryException If the property definition cannot be retrieved. + */ + private boolean isAuthorizableProperty(Property prop, boolean verifyAncestor) throws RepositoryException { + 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())) { + NodeTypeImpl declaringNt = (NodeTypeImpl) prop.getDefinition().getDeclaringNodeType(); + return declaringNt.isNodeType(UserConstants.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; + } + } + + /** + * Test if the JCR property to be modified/removed is one of the + * following that has a special meaning and must be altered using this + * user API: + *

      + *
    • rep:principalName
    • + *
    • rep:members
    • + *
    • rep:impersonators
    • + *
    • rep:password
    • + *
    + * Those properties are 'protected' in their property definition. This + * method is a simple utility in order to save the extra effort to modify + * the props just to find out later that they are in fact protected. + * + * @param propertyName Name of the property. + * @return true if the property with the given name represents a protected + * user/group property that needs to be changed through the API. + * @throws RepositoryException If the specified name is not valid. + */ + private boolean isProtectedProperty(String propertyName) throws RepositoryException { + Name pName = getSession().getQName(propertyName); + return P_PRINCIPAL_NAME.equals(pName) + || P_MEMBERS.equals(pName) + || P_IMPERSONATORS.equals(pName) + || P_DISABLED.equals(pName) + || P_PASSWORD.equals(pName); + } + + /** + * Throws ConstraintViolationException if {@link #isProtectedProperty(String)} + * returns true. + * + * @param propertyName Name of the property. + * @throws ConstraintViolationException If the property is protected according + * to {@link #isProtectedProperty(String)}. + * @throws RepositoryException If another error occurs. + */ + private void checkProtectedProperty(String propertyName) throws ConstraintViolationException, RepositoryException { + if (isProtectedProperty(propertyName)) { + throw new ConstraintViolationException("Attempt to modify protected property " + propertyName + " of " + this); + } + } + + /** + * + * @param relPath A relative path. + * @return The corresponding node. + * @throws RepositoryException If an error occurs. + */ + private Node getOrCreateTargetNode(String relPath) throws RepositoryException { + Node n; + if (relPath != null) { + if (node.hasNode(relPath)) { + n = node.getNode(relPath); + } else { + n = node; + for (String segment : Text.explode(relPath, '/')) { + if (n.hasNode(segment)) { + n = n.getNode(segment); + } else { + n = n.addNode(segment); + } + } + } + if (!Text.isDescendantOrEqual(node.getPath(), n.getPath())) { + node.refresh(false); + throw new RepositoryException("Relative path " + relPath + " outside of scope of " + this); + } + } else { + n = node; + } + return n; + } + + //-------------------------------------------------------------------------- + /** + * + */ + class NodeBasedPrincipal extends PrincipalImpl implements ItemBasedPrincipal { + + /** + * @param name for the principal + */ + NodeBasedPrincipal(String name) { + super(name); + } + + NodeId getNodeId() { + return node.getNodeId(); + } + + //---------------------------------------------< ItemBasedPrincipal >--- + /** + * Method revealing the path to the Node that represents the + * Authorizable this principal is created for. + * + * @return The path of the underlying node. + * @see ItemBasedPrincipal#getPath() + */ + public String getPath() throws RepositoryException { + return node.getPath(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java new file mode 100644 index 00000000000..e3a9e71edfa --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/GroupImpl.java @@ -0,0 +1,802 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; +import javax.jcr.ItemNotFoundException; +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 org.apache.jackrabbit.api.security.principal.GroupPrincipal; +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.flat.BTreeManager; +import org.apache.jackrabbit.commons.flat.ItemSequence; +import org.apache.jackrabbit.commons.flat.PropertySequence; +import org.apache.jackrabbit.commons.flat.Rank; +import org.apache.jackrabbit.commons.flat.TreeManager; +import org.apache.jackrabbit.commons.iterator.LazyIteratorChain; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionWriteOperation; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * GroupImpl... + */ +class GroupImpl extends AuthorizableImpl implements Group { + + private static final Logger log = LoggerFactory.getLogger(GroupImpl.class); + + private Principal principal; + + protected GroupImpl(NodeImpl node, UserManagerImpl userManager) { + super(node, userManager); + } + + //-------------------------------------------------------< Authorizable >--- + + /** + * @see Authorizable#isGroup() + */ + public boolean isGroup() { + return true; + } + + /** + * @see Authorizable#getPrincipal() + */ + public Principal getPrincipal() throws RepositoryException { + if (principal == null) { + principal = new NodeBasedGroup(getPrincipalName()); + } + return principal; + } + + //--------------------------------------------------------------< Group >--- + + /** + * @see Group#getDeclaredMembers() + */ + public Iterator getDeclaredMembers() throws RepositoryException { + if (isEveryone()) { + return userManager.findAuthorizables(getSession().getJCRName(P_PRINCIPAL_NAME), null, UserManager.SEARCH_TYPE_AUTHORIZABLE); + } else { + return getMembers(false, UserManager.SEARCH_TYPE_AUTHORIZABLE); + } + } + + /** + * @see Group#getMembers() + */ + public Iterator getMembers() throws RepositoryException { + if (isEveryone()) { + return getDeclaredMembers(); + } else { + return getMembers(true, UserManager.SEARCH_TYPE_AUTHORIZABLE); + } + } + + public boolean isDeclaredMember(Authorizable authorizable) throws RepositoryException { + if (authorizable == null || !(authorizable instanceof AuthorizableImpl) + || getNode().isSame(((AuthorizableImpl) authorizable).getNode())) { + return false; + } else if (isEveryone()) { + return true; + } else { + return getMembershipProvider(getNode()).hasMember((AuthorizableImpl) authorizable); + } + } + + /** + * @see Group#isMember(Authorizable) + */ + public boolean isMember(Authorizable authorizable) throws RepositoryException { + if (authorizable == null || !(authorizable instanceof AuthorizableImpl) + || getNode().isSame(((AuthorizableImpl) authorizable).getNode())) { + return false; + } else if (isEveryone()) { + return true; + } else { + String thisID = getID(); + AuthorizableImpl impl = (AuthorizableImpl) authorizable; + for (Iterator it = impl.memberOf(); it.hasNext(); ) { + if (thisID.equals(it.next().getID())) { + return true; + } + } + return false; + } + } + + /** + * @see Group#addMember(Authorizable) + */ + public boolean addMember(Authorizable authorizable) throws RepositoryException { + if (!(authorizable instanceof AuthorizableImpl)) { + log.warn("Invalid Authorizable: {}", authorizable); + return false; + } + if (isEveryone() || ((AuthorizableImpl) authorizable).isEveryone()) { + return false; + } + + AuthorizableImpl authImpl = ((AuthorizableImpl) authorizable); + Node memberNode = authImpl.getNode(); + if (memberNode.isSame(getNode())) { + String msg = "Attempt to add a group as member of itself (" + getID() + ")."; + log.warn(msg); + return false; + } + + if (isCyclicMembership(authImpl)) { + log.warn("Attempt to create circular group membership."); + return false; + } + + return getMembershipProvider(getNode()).addMember(authImpl); + } + + @Override + public Set addMembers(String... memberIds) throws RepositoryException { + throw new UnsupportedRepositoryOperationException("not implemented"); + } + + + /** + * @see Group#removeMember(Authorizable) + */ + public boolean removeMember(Authorizable authorizable) throws RepositoryException { + if (!(authorizable instanceof AuthorizableImpl)) { + log.warn("Invalid Authorizable: {}", authorizable); + return false; + } + if (isEveryone()) { + return false; + } + + return getMembershipProvider(getNode()).removeMember((AuthorizableImpl) authorizable); + } + + @Override + public Set removeMembers(String... memberIds) throws RepositoryException { + throw new UnsupportedRepositoryOperationException("not implemented"); + } + + //-------------------------------------------------------------------------- + /** + * Retrieve the membership provider for this group. This method deals with + * members stored in the P_MEMBERS property and with those + * repositories the store group members in a separate tree underneath the + * N_MEMBERS node. + * + * @param node The node associated with this group. + * @return an instance of MembershipProvider. + * @throws RepositoryException If an error occurs. + */ + private MembershipProvider getMembershipProvider(NodeImpl node) throws RepositoryException { + MembershipProvider msp; + if (userManager.hasMemberSplitSize()) { + if (node.hasNode(N_MEMBERS) || !node.hasProperty(P_MEMBERS)) { + msp = new NodeBasedMembershipProvider(node); + } else { + msp = new PropertyBasedMembershipProvider(node); + } + } else { + msp = new PropertyBasedMembershipProvider(node); + } + + if (node.hasProperty(P_MEMBERS) && node.hasNode(N_MEMBERS)) { + log.warn("Found members node and members property on node {}. Ignoring {} members", node, + userManager.hasMemberSplitSize() ? "property" : "node"); + } + + return msp; + } + + /** + * @param includeIndirect If true all members of this group + * will be return; otherwise only the declared members. + * @param type Any of {@link UserManager#SEARCH_TYPE_AUTHORIZABLE}, + * {@link UserManager#SEARCH_TYPE_GROUP}, {@link UserManager#SEARCH_TYPE_USER}. + * @return A collection of members of this group. + * @throws RepositoryException If an error occurs while collecting the members. + */ + private Iterator getMembers(boolean includeIndirect, int type) throws RepositoryException { + return getMembershipProvider(getNode()).getMembers(includeIndirect, type); + } + + /** + * Returns true if the given newMember is a Group + * and contains this Group as declared or inherited member. + * + * @param newMember The new member to be tested for cyclic membership. + * @return true if the 'newMember' is a group and 'this' is an declared or + * inherited member of it. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + private boolean isCyclicMembership(AuthorizableImpl newMember) throws RepositoryException { + if (newMember.isGroup()) { + GroupImpl gr = (GroupImpl) newMember; + for (Iterator it = gr.getMembers(true, UserManager.SEARCH_TYPE_GROUP); it.hasNext(); ) { + Authorizable member = it.next(); + GroupImpl grMemberImpl = (GroupImpl) member; + if (getNode().getUUID().equals(grMemberImpl.getNode().getUUID())) { + // found cyclic group membership + return true; + } + + } + } + return false; + } + + private String safeGetID() { + try { + return getID(); + } catch (RepositoryException e) { + return getNode().toString(); + } + } + + static PropertySequence getPropertySequence(Node nMembers, UserManagerImpl userManager) throws RepositoryException { + Comparator order = Rank.comparableComparator(); + int maxChildren = userManager.getMemberSplitSize(); + int minChildren = maxChildren / 2; + + TreeManager treeManager = new BTreeManager(nMembers, minChildren, maxChildren, order, + userManager.isAutoSave()); + + return ItemSequence.createPropertySequence(treeManager); + } + + //------------------------------------------------------< inner classes >--- + /** + * Principal Implementation + */ + private class NodeBasedGroup extends NodeBasedPrincipal implements java.security.acl.Group, GroupPrincipal { + + private NodeBasedGroup(String name) { + super(name); + } + + //----------------------------------------------------------< Group >--- + + /** + * @return Always false. Group membership must be edited + * using the enclosing GroupImpl object. + * @see java.security.acl.Group#addMember(Principal) + */ + public boolean addMember(Principal user) { + return false; + } + + /** + * @return Always false. Group membership must be edited + * using the enclosing GroupImpl object. + * @see java.security.acl.Group#isMember(Principal) + */ + public boolean removeMember(Principal user) { + return false; + } + + //----------------------------------------------------------< GroupPrincipal >--- + + /** + * Returns true, if the given Principal is represented by + * a Authorizable, that is a member of the underlying UserGroup. + * + * @see org.apache.jackrabbit.api.security.principal.GroupPrincipal#isMember(Principal) + */ + public boolean isMember(Principal member) { + // shortcut for everyone group -> avoid collecting all members + // as all users and groups are member of everyone. + try { + if (isEveryone()) { + return !getPrincipal().equals(member); + } + } catch (RepositoryException e) { + // continue using regular membership evaluation + } + + Collection members = getMembers(); + return members.contains(member); + } + + /** + * Return all principals that refer to every member of the underlying + * user group. + * + * @see org.apache.jackrabbit.api.security.principal.GroupPrincipal#members() + */ + public Enumeration members() { + return Collections.enumeration(getMembers()); + } + + //---------------------------------------------------< Serializable >--- + /** + * implement the writeObject method to assert initialization of all members + * before serialization. + * + * @param stream The object output stream. + * @throws IOException If an error occurs. + */ + private void writeObject(ObjectOutputStream stream) throws IOException { + getMembers(); + stream.defaultWriteObject(); + } + + //---------------------------------------------------------------------- + /** + * Collect the member of this group principal. + * + * @return the members of this group principal. + */ + private Collection getMembers() { + Set members = new HashSet(); + try { + for (Iterator it = GroupImpl.this.getMembers(); it.hasNext(); ) { + members.add(it.next().getPrincipal()); + } + } catch (RepositoryException e) { + // should not occur. + log.error("Unable to retrieve Group members."); + } + return members; + } + } + + /** + * Inner MembershipProvider interface + */ + private interface MembershipProvider { + boolean addMember(AuthorizableImpl authorizable) throws RepositoryException; + + boolean removeMember(AuthorizableImpl authorizable) throws RepositoryException; + + Iterator getMembers(boolean includeIndirect, int type) throws RepositoryException; + + boolean hasMember(AuthorizableImpl authorizable) throws RepositoryException; + } + + /** + * PropertyBasedMembershipProvider + */ + private class PropertyBasedMembershipProvider implements MembershipProvider { + private final NodeImpl node; + + private PropertyBasedMembershipProvider(NodeImpl node) { + super(); + this.node = node; + } + + /** + * @see MembershipProvider#addMember(AuthorizableImpl) + */ + public boolean addMember(AuthorizableImpl authorizable) throws RepositoryException { + Node memberNode = authorizable.getNode(); + + Value[] values; + Value toAdd = getSession().getValueFactory().createValue(memberNode, true); + if (node.hasProperty(P_MEMBERS)) { + Value[] old = node.getProperty(P_MEMBERS).getValues(); + for (Value v : old) { + if (v.equals(toAdd)) { + log.debug("Authorizable {} is already member of {}", authorizable, this); + return false; + } + } + + values = new Value[old.length + 1]; + System.arraycopy(old, 0, values, 0, old.length); + } else { + values = new Value[1]; + } + values[values.length - 1] = toAdd; + + userManager.setProtectedProperty(node, P_MEMBERS, values, PropertyType.WEAKREFERENCE); + return true; + } + + /** + * @see MembershipProvider#removeMember(AuthorizableImpl) + */ + public boolean removeMember(AuthorizableImpl authorizable) throws RepositoryException { + if (!node.hasProperty(P_MEMBERS)) { + log.debug("Group has no members -> cannot remove member {}", authorizable.getID()); + return false; + } + + Value toRemove = getSession().getValueFactory().createValue((authorizable).getNode(), true); + + PropertyImpl property = node.getProperty(P_MEMBERS); + List valList = new ArrayList(Arrays.asList(property.getValues())); + + if (valList.remove(toRemove)) { + try { + if (valList.isEmpty()) { + userManager.removeProtectedItem(property, node); + } else { + Value[] values = valList.toArray(new Value[valList.size()]); + userManager.setProtectedProperty(node, P_MEMBERS, values); + } + return true; + } catch (RepositoryException e) { + // modification failed -> revert all pending changes. + node.refresh(false); + throw e; + } + } else { + // nothing changed + log.debug("Authorizable {} was not member of {}", authorizable.getID(), getID()); + return false; + } + } + + /** + * @see MembershipProvider#getMembers(boolean, int) + */ + public Iterator getMembers(boolean includeIndirect, int type) throws RepositoryException { + if (node.hasProperty(P_MEMBERS)) { + Value[] members = node.getProperty(P_MEMBERS).getValues(); + + if (includeIndirect) { + return includeIndirect(toAuthorizables(members, type), type); + } else { + return new RangeIteratorAdapter(toAuthorizables(members, type), members.length); + } + } else { + return Iterators.empty(); + } + } + + /** + * @see MembershipProvider#hasMember(AuthorizableImpl) + */ + public boolean hasMember(AuthorizableImpl authorizable) throws RepositoryException { + if (node.hasProperty(P_MEMBERS)) { + Value[] members = node.getProperty(P_MEMBERS).getValues(); + for (Value v : members) { + if (authorizable.getNode().getIdentifier().equals(v.getString())) { + return true; + } + } + return false; + } else { + return false; + } + } + + } + + /** + * NodeBasedMembershipProvider + */ + private class NodeBasedMembershipProvider implements MembershipProvider { + private final NodeImpl node; + + private NodeBasedMembershipProvider(NodeImpl node) { + super(); + this.node = node; + } + + /** + * @see MembershipProvider#addMember(AuthorizableImpl) + */ + public boolean addMember(final AuthorizableImpl authorizable) throws RepositoryException { + return userManager.performProtectedOperation(getSession(), new SessionWriteOperation() { + public Boolean perform(SessionContext context) throws RepositoryException { + NodeImpl nMembers = (node.hasNode(N_MEMBERS) + ? node.getNode(N_MEMBERS) + : node.addNode(N_MEMBERS, NT_REP_MEMBERS, null)); + + try { + PropertySequence properties = getPropertySequence(nMembers, userManager); + String propName = Text.escapeIllegalJcrChars(authorizable.getID()); + if (properties.hasItem(propName)) { + log.debug("Authorizable {} is already member of {}", authorizable, this); + return false; + } else { + Value newMember = getSession().getValueFactory().createValue(authorizable.getNode(), true); + properties.addProperty(propName, newMember); + } + + if (userManager.isAutoSave()) { + node.save(); + } + return true; + } catch (RepositoryException e) { + log.debug("addMember failed. Reverting changes", e); + if (nMembers.isNew()) { + node.refresh(false); + } else { + nMembers.refresh(false); + } + throw e; + } + } + }); + } + + /** + * @see MembershipProvider#removeMember(AuthorizableImpl) + */ + public boolean removeMember(final AuthorizableImpl authorizable) throws RepositoryException { + if (!node.hasNode(N_MEMBERS)) { + log.debug("Group has no members -> cannot remove member {}", authorizable.getID()); + return false; + } + + return userManager.performProtectedOperation(getSession(), new SessionWriteOperation() { + public Boolean perform(SessionContext context) throws RepositoryException { + NodeImpl nMembers = node.getNode(N_MEMBERS); + try { + PropertySequence properties = getPropertySequence(nMembers, userManager); + String propName = Text.escapeIllegalJcrChars(authorizable.getID()); + if (properties.hasItem(propName)) { + properties.removeProperty(propName); + if (!properties.iterator().hasNext()) { + nMembers.remove(); + } + } else { + log.debug("Authorizable {} was not member of {}", authorizable.getID(), getID()); + return false; + } + + if (userManager.isAutoSave()) { + node.save(); + } + return true; + } catch (RepositoryException e) { + log.debug("removeMember failed. Reverting changes", e); + nMembers.refresh(false); + throw e; + } + } + }); + } + + /** + * @see MembershipProvider#getMembers(boolean, int) + */ + public Iterator getMembers(boolean includeIndirect, int type) throws RepositoryException { + if (node.hasNode(N_MEMBERS)) { + PropertySequence members = getPropertySequence(node.getNode(N_MEMBERS), userManager); + if (includeIndirect) { + return includeIndirect(toAuthorizables(members.iterator(), type), type); + } else { + return toAuthorizables(members.iterator(), type); + } + } else { + return Iterators.empty(); + } + } + + /** + * @see MembershipProvider#hasMember(AuthorizableImpl) + */ + public boolean hasMember(AuthorizableImpl authorizable) throws RepositoryException { + if (node.hasNode(N_MEMBERS)) { + PropertySequence members = getPropertySequence(node.getNode(N_MEMBERS), userManager); + return members.hasItem(authorizable.getID()); + } else { + return false; + } + } + + } + + // -----------------------------------------------------------< utility >--- + /** + * Returns an iterator of authorizables which includes all indirect members of the given iterator + * of authorizables. + * + * @param authorizables + * @param type + * @return Iterator of Authorizable objects + */ + private Iterator includeIndirect(final Iterator authorizables, final int type) { + Iterator> indirectMembers = new Iterator>() { + public boolean hasNext() { + return authorizables.hasNext(); + } + + public Iterator next() { + Authorizable next = authorizables.next(); + return Iterators.iteratorChain(Iterators.singleton(next), indirect(next)); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Returns the transitive closure over the members of this authorizable. + * + * @param authorizable + * @return Iterator of Authorizable objects + */ + private Iterator indirect(Authorizable authorizable) { + if (authorizable.isGroup()) { + try { + return ((GroupImpl) authorizable).getMembers(true, type); + } catch (RepositoryException e) { + log.warn("Could not determine members of " + authorizable, e); + } + } + return Iterators.empty(); + } + }; + + return unique(new LazyIteratorChain(indirectMembers)); + } + + /** + * Filter the passed {@code authorizables} in order to ensure uniqueness. + * @param authorizables + * @return all members of {@code authorizable} with duplicates removed + * + * @see JCR-3156 + */ + private Iterator unique(Iterator authorizables) { + final HashSet seenAuthorizables = new HashSet(); + return Iterators.filterIterator(authorizables, + new org.apache.jackrabbit.spi.commons.iterator.Predicate() { + + public boolean evaluate(Authorizable authorizable) { + try { + return seenAuthorizables.add(authorizable.getID()); + } + catch (RepositoryException e) { + log.warn("Could not determine id of " + authorizable, e); + return true; + } + } + }); + } + + /** + * Map an array of values to an iterator of authorizables. + * + * @param members + * @param type + * @return Iterator of Authorizable objects + */ + private Iterator toAuthorizables(final Value[] members, int type) { + return new AuthorizableIterator(type) { + private int pos; + + @Override + protected String getNextMemberRef() throws RepositoryException { + return pos < members.length + ? members[pos++].getString() + : null; + } + }; + } + + /** + * Map an iterator of properties to an iterator of authorizables. + * + * @param members + * @param type + * @return Iterator of Authorizable objects + */ + private Iterator toAuthorizables(final Iterator members, int type) { + return new AuthorizableIterator(type) { + @Override + protected String getNextMemberRef() throws RepositoryException { + return members.hasNext() + ? members.next().getString() + : null; + } + }; + } + + /** + * Iterator of authorizables of a specific type. + */ + private abstract class AuthorizableIterator implements Iterator { + private Authorizable next; + private final int type; + + public AuthorizableIterator(int type) { + super(); + this.type = type; + } + + public boolean hasNext() { + prefetch(); + return next != null; + } + + public Authorizable next() { + prefetch(); + if (next == null) { + throw new NoSuchElementException(); + } + + Authorizable element = next; + next = null; + return element; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Returns the reference value of the next node representing the next authorizable or + * null if there there are no more. + * + * @return reference value of the next node representing the next authorizable or + * null if there there are no more. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + protected abstract String getNextMemberRef() throws RepositoryException; + + private void prefetch() { + while (next == null) { + try { + String memberRef = getNextMemberRef(); + if (memberRef == null) { + return; + } + + NodeImpl member = (NodeImpl) getSession().getNodeByIdentifier(memberRef); + if (type != UserManager.SEARCH_TYPE_USER && member.isNodeType(NT_REP_GROUP)) { + next = userManager.createGroup(member); + } else if (type != UserManager.SEARCH_TYPE_GROUP && member.isNodeType(NT_REP_USER)) { + next = userManager.createUser(member); + } else { + log.debug("Group member entry with invalid node type {} -> " + + "Not included in member set.", member.getPrimaryNodeType().getName()); + } + } catch (ItemNotFoundException e) { + log.debug("Authorizable node referenced by {} doesn't exist any more -> " + + "Ignored from member list.", safeGetID()); + } catch (RepositoryException e) { + log.debug("Error pre-fetching member for " + safeGetID(), e); + } + + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/ImpersonationImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/ImpersonationImpl.java new file mode 100644 index 00000000000..91c55b9431c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/ImpersonationImpl.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.security.Principal; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.security.auth.Subject; + +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.Impersonation; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.core.security.principal.PrincipalIteratorAdapter; +import org.apache.jackrabbit.value.StringValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ImpersonationImpl + */ +class ImpersonationImpl implements Impersonation, UserConstants { + + private static final Logger log = LoggerFactory.getLogger(ImpersonationImpl.class); + + private final UserImpl user; + private final UserManagerImpl userManager; + + ImpersonationImpl(UserImpl user, UserManagerImpl userManager) throws RepositoryException { + this.user = user; + this.userManager = userManager; + } + + //------------------------------------------------------< Impersonation >--- + /** + * @see Impersonation#getImpersonators() + */ + public PrincipalIterator getImpersonators() throws RepositoryException { + Set impersonators = getImpersonatorNames(); + if (impersonators.isEmpty()) { + return PrincipalIteratorAdapter.EMPTY; + } else { + final PrincipalManager pMgr = user.getSession().getPrincipalManager(); + + Set s = new HashSet(); + for (String pName : impersonators) { + Principal p = pMgr.getPrincipal(pName); + if (p == null) { + log.debug("Impersonator " + pName + " does not correspond to a known Principal."); + p = new PrincipalImpl(pName); + } + s.add(p); + + } + return new PrincipalIteratorAdapter(s); + } + } + + /** + * @see Impersonation#grantImpersonation(Principal) + */ + public synchronized boolean grantImpersonation(Principal principal) throws RepositoryException { + // make sure the given principals belong to an existing authorizable + Authorizable auth = user.userManager.getAuthorizable(principal); + if (auth == null || auth.isGroup()) { + log.warn("Cannot grant impersonation to a principal that is a Group or an unknown Authorizable."); + return false; + } + + // make sure the given principal doesn't refer to the admin user. + if (user.userManager.isAdminId(auth.getID())) { + log.warn("Admin principal is already granted impersonation."); + return false; + } + + String pName = principal.getName(); + // make sure user does not impersonate himself + if (user.getPrincipal().getName().equals(pName)) { + log.warn("Cannot grant impersonation to oneself."); + return false; + } + + boolean granted = false; + Set impersonators = getImpersonatorNames(); + if (impersonators.add(pName)) { + updateImpersonatorNames(impersonators); + granted = true; + } + return granted; + } + + /** + * @see Impersonation#revokeImpersonation(Principal) + */ + public synchronized boolean revokeImpersonation(Principal principal) throws RepositoryException { + boolean revoked = false; + String pName = principal.getName(); + + Set impersonators = getImpersonatorNames(); + if (impersonators.remove(pName)) { + updateImpersonatorNames(impersonators); + revoked = true; + } + return revoked; + } + + /** + * @see Impersonation#allows(Subject) + */ + public boolean allows(Subject subject) throws RepositoryException { + if (subject == null) { + return false; + } + + Set principalNames = new HashSet(); + for (Principal p : subject.getPrincipals()) { + principalNames.add(p.getName()); + } + + boolean allows; + Set impersonators = getImpersonatorNames(); + allows = impersonators.removeAll(principalNames); + + if (!allows) { + // check if subject belongs to administrator user + for (Principal p : subject.getPrincipals()) { + if (GroupPrincipals.isGroup(p)) { + continue; + } + Authorizable a = userManager.getAuthorizable(p); + if (a != null && userManager.isAdminId(a.getID())) { + allows = true; + break; + } + } + } + return allows; + } + + //------------------------------------------------------------< private >--- + + private Set getImpersonatorNames() throws RepositoryException { + Set princNames = new HashSet(); + if (user.getNode().hasProperty(P_IMPERSONATORS)) { + Value[] vs = user.getNode().getProperty(P_IMPERSONATORS).getValues(); + for (Value v : vs) { + princNames.add(v.getString()); + } + } + return princNames; + } + + private void updateImpersonatorNames(Set principalNames) throws RepositoryException { + NodeImpl userNode = user.getNode(); + try { + String[] pNames = principalNames.toArray(new String[principalNames.size()]); + if (pNames.length == 0) { + PropertyImpl prop = userNode.getProperty(P_IMPERSONATORS); + userManager.removeProtectedItem(prop, userNode); + } else { + Value[] values = new Value[pNames.length]; + for (int i = 0; i < pNames.length; i++) { + values[i] = new StringValue(pNames[i]); + } + userManager.setProtectedProperty(userNode, P_IMPERSONATORS, values); + } + } catch (RepositoryException e) { + // revert pending changes + userNode.refresh(false); + throw e; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/IndexNodeResolver.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/IndexNodeResolver.java new file mode 100644 index 00000000000..451ff0abee5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/IndexNodeResolver.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.util.ISO9075; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * + */ +class IndexNodeResolver extends NodeResolver { + + private static Logger log = LoggerFactory.getLogger(IndexNodeResolver.class); + + private final QueryManager queryManager; + + IndexNodeResolver(Session session, NamePathResolver resolver) throws RepositoryException { + super(session, resolver); + queryManager = session.getWorkspace().getQueryManager(); + } + + //-------------------------------------------------------< NodeResolver >--- + /** + * @inheritDoc + */ + @Override + public Node findNode(Name nodeName, Name ntName) throws RepositoryException { + Query query = buildQuery(nodeName, ntName); + query.setLimit(1); + NodeIterator res = query.execute().getNodes(); + if (res.hasNext()) { + return res.nextNode(); + } + return null; + } + + /** + * @inheritDoc + */ + @Override + public Node findNode(Name propertyName, String value, Name ntName) throws RepositoryException { + Query query = buildQuery(value, Collections.singleton(propertyName), ntName, true, 1); + NodeIterator res = query.execute().getNodes(); + if (res.hasNext()) { + return res.nextNode(); + } + return null; + } + + /** + * Search authorizable nodes of the specified node type having the specified + * properties with the specified value. + * + * @param propertyNames + * @param value + * @param ntName NodeType the hits have to have + * @param exact if true match must be exact + * @return + * @throws javax.jcr.RepositoryException + */ + @Override + public NodeIterator findNodes(Set propertyNames, String value, Name ntName, + boolean exact, long maxSize) throws RepositoryException { + Query query = buildQuery(value, propertyNames, ntName, exact, maxSize); + return query.execute().getNodes(); + } + + @Override + public NodeIterator findNodes(Path relPath, String value, int authorizableType, boolean exact, long maxSize) throws RepositoryException { + Query query; + if (relPath.getLength() == 1) { + Set names = Collections.singleton(relPath.getName()); + // search without nt-restriction in order not to limit the query to the + // authorizable nodes and filter non-matching results later. + query = buildQuery(value, names, null, exact, maxSize, getSearchRoot(authorizableType)); + } else { + query = buildQuery(value, relPath, exact, maxSize, getSearchRoot(authorizableType)); + } + return new ResultFilteringNodeIterator(query.execute().getNodes(), getAuthorizableTypePredicate(authorizableType, false)); + } + + //-------------------------------------------------------------------------- + /** + * + * @param nodeName + * @param ntName + * @return + * @throws RepositoryException + */ + private Query buildQuery(Name nodeName, Name ntName) throws RepositoryException { + StringBuilder stmt = new StringBuilder("/jcr:root"); + stmt.append(getSearchRoot(ntName)); + stmt.append("//element("); + stmt.append(ISO9075.encode(getNamePathResolver().getJCRName(nodeName))); + stmt.append(","); + stmt.append(getNamePathResolver().getJCRName(ntName)); + stmt.append(")"); + return queryManager.createQuery(stmt.toString(), Query.XPATH); + } + + /** + * + * @param value + * @param props + * @param ntName + * @param exact + * @param maxSize + * @return + * @throws RepositoryException + */ + private Query buildQuery(String value, Set props, Name ntName, + boolean exact, long maxSize) throws RepositoryException { + String searchRoot = getSearchRoot(ntName); + return buildQuery(value, props, ntName, exact, maxSize, searchRoot); + } + + /** + * + * @param value + * @param props + * @param ntName + * @param exact + * @param maxSize + * @return + * @throws RepositoryException + */ + private Query buildQuery(String value, Set props, Name ntName, + boolean exact, long maxSize, String searchRoot) throws RepositoryException { + StringBuilder stmt = new StringBuilder("/jcr:root"); + if (!"/".equals(searchRoot)) { + stmt.append(searchRoot); + } + + if (ntName != null) { + stmt.append("//element(*,"); + stmt.append(getNamePathResolver().getJCRName(ntName)); + } else { + stmt.append("//element(*"); + } + + if (value == null) { + stmt.append(")"); + } else { + stmt.append(")["); + int i = 0; + for (Name prop : props) { + stmt.append((exact) ? "@" : "jcr:like(@"); + String pName = getNamePathResolver().getJCRName(prop); + stmt.append(ISO9075.encode(pName)); + if (exact) { + stmt.append("='"); + stmt.append(value.replaceAll("'", "''")); + stmt.append("'"); + } else { + stmt.append(",'%"); + stmt.append(escapeForQuery(value)); + stmt.append("%')"); + } + if (++i < props.size()) { + stmt.append(" or "); + } + } + stmt.append("]"); + } + Query q = queryManager.createQuery(stmt.toString(), Query.XPATH); + q.setLimit(maxSize); + return q; + } + + /** + * + * @param value + * @param relPath + * @param exact + * @param maxSize + * @return + * @throws RepositoryException + */ + private Query buildQuery(String value, Path relPath, boolean exact, long maxSize, String searchRoot) + throws RepositoryException { + StringBuilder stmt = new StringBuilder("/jcr:root"); + if (!"/".equals(searchRoot)) { + stmt.append(searchRoot); + } + + String p = getNamePathResolver().getJCRPath(relPath.getAncestor(1)); + stmt.append("//").append(p); + + if (value != null) { + stmt.append("["); + Name prop = relPath.getName(); + stmt.append((exact) ? "@" : "jcr:like(@"); + String pName = getNamePathResolver().getJCRName(prop); + stmt.append(ISO9075.encode(pName)); + if (exact) { + stmt.append("='"); + stmt.append(value.replaceAll("'", "''")); + stmt.append("'"); + } else { + stmt.append(",'%"); + stmt.append(escapeForQuery(value)); + stmt.append("%')"); + } + stmt.append("]"); + } + Query q = queryManager.createQuery(stmt.toString(), Query.XPATH); + q.setLimit(maxSize); + return q; + } + + 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(); + } + + //-------------------------------------------------------------------------- + /** + * + */ + private static class ResultFilteringNodeIterator extends FilteringNodeIterator { + + private Set authorizableIDs; + + private ResultFilteringNodeIterator(NodeIterator base, AuthorizableTypePredicate filter) { + super(base, filter); + } + + @Override + protected Node seekNext() { + if (authorizableIDs == null) { + authorizableIDs = new HashSet(); + } + Node n = null; + while (n == null && base.hasNext()) { + NodeImpl nextRes = (NodeImpl) base.nextNode(); + Node authorizableNode = ((AuthorizableTypePredicate) filter).getAuthorizableNode(nextRes); + try { + if (authorizableNode != null && authorizableIDs.add(authorizableNode.getIdentifier())) { + n = authorizableNode; + } + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + } + return n; + } + + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/MembershipCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/MembershipCache.java new file mode 100644 index 00000000000..45eebc9cf2e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/MembershipCache.java @@ -0,0 +1,561 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.SessionListener; +import org.apache.jackrabbit.core.cache.ConcurrentCache; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MembershipCache... + */ +public class MembershipCache implements UserConstants, SynchronousEventListener, SessionListener { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(MembershipCache.class); + + /** + * The maximum size of this cache + */ + private static final int MAX_CACHE_SIZE = + Integer.getInteger("org.apache.jackrabbit.MembershipCache", 5000); + + private final SessionImpl systemSession; + private final String groupsPath; + private final boolean useMembersNode; + private final String pMembers; + private final ConcurrentCache> cache; + + MembershipCache(SessionImpl systemSession, String groupsPath, boolean useMembersNode) throws RepositoryException { + this.systemSession = systemSession; + this.groupsPath = (groupsPath == null) ? UserConstants.GROUPS_PATH : groupsPath; + this.useMembersNode = useMembersNode; + + pMembers = systemSession.getJCRName(UserManagerImpl.P_MEMBERS); + cache = new ConcurrentCache>("MembershipCache", 16); + cache.setMaxMemorySize(MAX_CACHE_SIZE); + + String[] ntNames = new String[] { + systemSession.getJCRName(UserConstants.NT_REP_GROUP), + systemSession.getJCRName(UserConstants.NT_REP_MEMBERS) + }; + // register event listener to be informed about membership changes. + systemSession.getWorkspace().getObservationManager().addEventListener(this, + Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED, + groupsPath, + true, + null, + ntNames, + false); + // make sure the membership cache is informed if the system session is + // logged out in order to stop listening to events. + systemSession.addListener(this); + log.debug("Membership cache initialized. Max Size = {}", MAX_CACHE_SIZE); + } + + + //------------------------------------------------------< EventListener >--- + /** + * @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator) + */ + public void onEvent(EventIterator eventIterator) { + // evaluate if the membership cache needs to be cleared; + boolean clear = false; + while (eventIterator.hasNext() && !clear) { + Event ev = eventIterator.nextEvent(); + try { + if (pMembers.equals(Text.getName(ev.getPath()))) { + // simple case: a rep:members property that is affected + clear = true; + } else if (useMembersNode) { + // test if it affects a property defined by rep:Members node type. + int type = ev.getType(); + if (type == Event.PROPERTY_ADDED || type == Event.PROPERTY_CHANGED) { + Property p = systemSession.getProperty(ev.getPath()); + Name declNtName = ((NodeTypeImpl) p.getDefinition().getDeclaringNodeType()).getQName(); + clear = NT_REP_MEMBERS.equals(declNtName); + } else { + // PROPERTY_REMOVED + // test if the primary node type of the parent node is rep:Members + // this could potentially by some other property as well as the + // rep:Members node are not protected and could changed by + // adding a mixin type. + // ignoring this and simply clear the cache + String parentId = ev.getIdentifier(); + Node n = systemSession.getNodeByIdentifier(parentId); + Name ntName = ((NodeTypeImpl) n.getPrimaryNodeType()).getQName(); + clear = (UserConstants.NT_REP_MEMBERS.equals(ntName)); + } + } + } catch (RepositoryException e) { + log.warn(e.getMessage()); + // exception while processing the event -> clear the cache to + // be sure it isn't outdated. + clear = true; + } + } + + if (clear) { + cache.clear(); + log.debug("Membership cache cleared because of observation event."); + } + } + + //----------------------------------------------------< SessionListener >--- + /** + * @see SessionListener#loggingOut(org.apache.jackrabbit.core.SessionImpl) + */ + public void loggingOut(SessionImpl session) { + try { + systemSession.getWorkspace().getObservationManager().removeEventListener(this); + } catch (RepositoryException e) { + log.error("Unexpected error: Failed to stop event listening of MembershipCache.", e); + } + + } + + /** + * @see SessionListener#loggedOut(org.apache.jackrabbit.core.SessionImpl) + */ + public void loggedOut(SessionImpl session) { + // nothing to do + } + + //-------------------------------------------------------------------------- + /** + * @param authorizableNodeIdentifier The identifier of the node representing + * the authorizable to retrieve the declared membership for. + * @return A collection of node identifiers of those group nodes the + * authorizable in question is declared member of. + * @throws RepositoryException If an error occurs. + */ + Collection getDeclaredMemberOf(String authorizableNodeIdentifier) throws RepositoryException { + return declaredMemberOf(authorizableNodeIdentifier); + } + + /** + * @param authorizableNodeIdentifier The identifier of the node representing + * the authorizable to retrieve the membership for. + * @return A collection of node identifiers of those group nodes the + * authorizable in question is a direct or indirect member of. + * @throws RepositoryException If an error occurs. + */ + Collection getMemberOf(String authorizableNodeIdentifier) throws RepositoryException { + Set groupNodeIds = new HashSet(); + memberOf(authorizableNodeIdentifier, groupNodeIds); + return Collections.unmodifiableCollection(groupNodeIds); + } + + /** + * Returns the size of the membership cache + * @return the size + */ + int getSize() { + return (int) cache.getElementCount(); + } + + /** + * For testing purposes only. + */ + void clear() { + cache.clear(); + } + + /** + * Collects the declared memberships for the specified identifier of an + * authorizable using the specified session. + * + * @param authorizableNodeIdentifier The identifier of the node representing + * the authorizable to retrieve the membership for. + * @param session The session to be used to read the membership information. + * @return @return A collection of node identifiers of those group nodes the + * authorizable in question is a direct member of. + * @throws RepositoryException If an error occurs. + */ + Collection collectDeclaredMembership(String authorizableNodeIdentifier, Session session) throws RepositoryException { + final long t0 = System.nanoTime(); + + Collection groupNodeIds = collectDeclaredMembershipFromReferences(authorizableNodeIdentifier, session); + + final long t1 = System.nanoTime(); + if (log.isDebugEnabled()) { + log.debug(" collected {} groups for {} via references in {}us", new Object[]{ + groupNodeIds == null ? -1 : groupNodeIds.size(), + authorizableNodeIdentifier, + (t1-t0) / 1000 + }); + } + + if (groupNodeIds == null) { + groupNodeIds = collectDeclaredMembershipFromTraversal(authorizableNodeIdentifier, session); + + final long t2 = System.nanoTime(); + if (log.isDebugEnabled()) { + log.debug(" collected {} groups for {} via traversal in {}us", new Object[]{ + groupNodeIds == null ? -1 : groupNodeIds.size(), + authorizableNodeIdentifier, + (t2-t1) / 1000 + }); + } + } + return groupNodeIds; + } + + /** + * Collects the complete memberships for the specified identifier of an + * authorizable using the specified session. + * + * @param authorizableNodeIdentifier The identifier of the node representing + * the authorizable to retrieve the membership for. + * @param session The session to be used to read the membership information. + * @return A collection of node identifiers of those group nodes the + * authorizable in question is a direct or indirect member of. + * @throws RepositoryException If an error occurs. + */ + Collection collectMembership(String authorizableNodeIdentifier, Session session) throws RepositoryException { + Set groupNodeIds = new HashSet(); + memberOf(authorizableNodeIdentifier, groupNodeIds, session); + return groupNodeIds; + } + + //------------------------------------------------------------< private >--- + /** + * Collects the groups where the given authorizable is a declared member of. If the information is not cached, it + * is collected from the repository. + * + * @param authorizableNodeIdentifier Identifier of the authorizable node + * @return the collection of groups where the authorizable is a declared member of + * @throws RepositoryException if an error occurs + */ + private Collection declaredMemberOf(String authorizableNodeIdentifier) throws RepositoryException { + final long t0 = System.nanoTime(); + + Collection groupNodeIds = cache.get(authorizableNodeIdentifier); + + boolean wasCached = true; + if (groupNodeIds == null) { + wasCached = false; + // retrieve a new session with system-subject in order to avoid + // concurrent read operations using the system session of this workspace. + Session session = getSession(); + try { + groupNodeIds = collectDeclaredMembership(authorizableNodeIdentifier, session); + cache.put(authorizableNodeIdentifier, Collections.unmodifiableCollection(groupNodeIds), 1); + } + finally { + // release session if it isn't the original system session + if (session != systemSession) { + session.logout(); + } + } + } + + if (log.isDebugEnabled()) { + final long t1 = System.nanoTime(); + log.debug("Membership cache {} {} declared memberships of {} in {}us. cache size = {}", new Object[]{ + wasCached ? "returns" : "collected", + groupNodeIds.size(), + authorizableNodeIdentifier, + (t1-t0) / 1000, + cache.getElementCount() + }); + } + return groupNodeIds; + } + + /** + * Collects the groups where the given authorizable is a member of by recursively fetching the declared memberships + * via {@link #declaredMemberOf(String)} (cached). + * + * @param authorizableNodeIdentifier Identifier of the authorizable node + * @param groupNodeIds Map to receive the node ids of the groups + * @throws RepositoryException if an error occurs + */ + private void memberOf(String authorizableNodeIdentifier, Collection groupNodeIds) throws RepositoryException { + Collection declared = declaredMemberOf(authorizableNodeIdentifier); + for (String identifier : declared) { + if (groupNodeIds.add(identifier)) { + memberOf(identifier, groupNodeIds); + } + } + } + + /** + * Collects the groups where the given authorizable is a member of by recursively fetching the declared memberships + * by reading the relations from the repository (uncached!). + * + * @param authorizableNodeIdentifier Identifier of the authorizable node + * @param groupNodeIds Map to receive the node ids of the groups + * @param session the session to read from + * @throws RepositoryException if an error occurs + */ + private void memberOf(String authorizableNodeIdentifier, Collection groupNodeIds, Session session) throws RepositoryException { + Collection declared = collectDeclaredMembership(authorizableNodeIdentifier, session); + for (String identifier : declared) { + if (groupNodeIds.add(identifier)) { + memberOf(identifier, groupNodeIds, session); + } + } + } + + /** + * Collects the declared memberships for the given authorizable by resolving the week references to the authorizable. + * If the lookup fails, null is returned. This most likely the case if the authorizable does not exit (yet) + * in the session that is used for the lookup. + * + * @param authorizableNodeIdentifier Identifier of the authorizable node + * @param session the session to read from + * @return a collection of group node ids or null if the lookup failed. + * @throws RepositoryException if an error occurs + */ + private Collection collectDeclaredMembershipFromReferences(String authorizableNodeIdentifier, + Session session) throws RepositoryException { + Set pIds = new HashSet(); + Set nIds = new HashSet(); + + // Try to get membership information from references + PropertyIterator refs = getMembershipReferences(authorizableNodeIdentifier, session); + if (refs == null) { + return null; + } + + while (refs.hasNext()) { + try { + PropertyImpl pMember = (PropertyImpl) refs.nextProperty(); + NodeImpl nGroup = (NodeImpl) pMember.getParent(); + + Set groupNodeIdentifiers; + if (P_MEMBERS.equals(pMember.getQName())) { + // Found membership information in members property + groupNodeIdentifiers = pIds; + } else { + // Found membership information in members node + groupNodeIdentifiers = nIds; + while (nGroup.isNodeType(NT_REP_MEMBERS)) { + nGroup = (NodeImpl) nGroup.getParent(); + } + } + + if (nGroup.isNodeType(NT_REP_GROUP)) { + groupNodeIdentifiers.add(nGroup.getIdentifier()); + } else { + // weak-ref property 'rep:members' that doesn't reside under an + // group node -> doesn't represent a valid group member. + log.debug("Invalid member reference to '{}' -> Not included in membership set.", this); + } + } catch (ItemNotFoundException e) { + // group node doesn't exist -> -> ignore exception + // and skip this reference from membership list. + } catch (AccessDeniedException e) { + // not allowed to see the group node -> ignore exception + // and skip this reference from membership list. + } + } + + // Based on the user's setting return either of the found membership information + return select(pIds, nIds); + } + + /** + * Collects the declared memberships for the given authorizable by traversing the groups structure. + * + * @param authorizableNodeIdentifier Identifier of the authorizable node + * @param session the session to read from + * @return a collection of group node ids. + * @throws RepositoryException if an error occurs + */ + private Collection collectDeclaredMembershipFromTraversal( + final String authorizableNodeIdentifier, Session session) throws RepositoryException { + + final Set pIds = new HashSet(); + final Set nIds = new HashSet(); + + // workaround for failure of Node#getWeakReferences + // traverse the tree below groups-path and collect membership manually. + log.info("Traversing groups tree to collect membership."); + if (session.nodeExists(groupsPath)) { + Node groupsNode = session.getNode(groupsPath); + traverseAndCollect(authorizableNodeIdentifier, pIds, nIds, (NodeImpl) groupsNode); + } // else: no groups exist -> nothing to do. + + // Based on the user's setting return either of the found membership information + return select(pIds, nIds); + } + + /** + * traverses the groups structure to find the groups of which the given authorizable is member of. + * + * @param authorizableNodeIdentifier Identifier of the authorizable node + * @param pIds output set to update of group node ids that were found via the property memberships + * @param nIds output set to update of group node ids that were found via the node memberships + * @param node the node to traverse + * @throws RepositoryException if an error occurs + */ + private void traverseAndCollect(String authorizableNodeIdentifier, Set pIds, Set nIds, NodeImpl node) + throws RepositoryException { + if (node.isNodeType(NT_REP_GROUP)) { + String groupId = node.getIdentifier(); + if (node.hasProperty(P_MEMBERS)) { + for (Value value : node.getProperty(P_MEMBERS).getValues()) { + String v = value.getString(); + if (v.equals(authorizableNodeIdentifier)) { + pIds.add(groupId); + } + } + } + NodeIterator iter = node.getNodes(); + while (iter.hasNext()) { + NodeImpl child = (NodeImpl) iter.nextNode(); + if (child.isNodeType(NT_REP_MEMBERS)) { + isMemberOfNodeBaseMembershipGroup(authorizableNodeIdentifier, groupId, nIds, child); + } + } + } else { + NodeIterator iter = node.getNodes(); + while (iter.hasNext()) { + NodeImpl child = (NodeImpl) iter.nextNode(); + traverseAndCollect(authorizableNodeIdentifier, pIds, nIds, child); + } + } + } + + /** + * traverses the group structure of a node-based group to check if the given authorizable is member of this group. + * + * @param authorizableNodeIdentifier Identifier of the authorizable node + * @param groupId if of the group + * @param nIds output set to update of group node ids that were found via the node memberships + * @param node the node to traverse + * @throws RepositoryException if an error occurs + */ + private void isMemberOfNodeBaseMembershipGroup(String authorizableNodeIdentifier, String groupId, Set nIds, + NodeImpl node) + throws RepositoryException { + PropertyIterator pIter = node.getProperties(); + while (pIter.hasNext()) { + PropertyImpl p = (PropertyImpl) pIter.nextProperty(); + if (p.getType() == PropertyType.WEAKREFERENCE) { + Value[] values = p.isMultiple() + ? p.getValues() + : new Value[]{p.getValue()}; + for (Value v: values) { + if (v.getString().equals(authorizableNodeIdentifier)) { + nIds.add(groupId); + return; + } + } + } + } + NodeIterator iter = node.getNodes(); + while (iter.hasNext()) { + NodeImpl child = (NodeImpl) iter.nextNode(); + if (child.isNodeType(NT_REP_MEMBERS)) { + isMemberOfNodeBaseMembershipGroup(authorizableNodeIdentifier, groupId, nIds, child); + } + } + } + + /** + * Return either of both sets depending on the users setting whether + * to use the members property or the members node to record membership + * information. If both sets are non empty, the one configured in the + * settings will take precedence and an warning is logged. + * + * @param pIds the set of group node ids retrieved through membership properties + * @param nIds the set of group node ids retrieved through membership nodes + * @return the selected set. + */ + private Set select(Set pIds, Set nIds) { + Set result; + if (useMembersNode) { + if (!nIds.isEmpty() || pIds.isEmpty()) { + result = nIds; + } else { + result = pIds; + } + } else { + if (!pIds.isEmpty() || nIds.isEmpty()) { + result = pIds; + } else { + result = nIds; + } + } + + if (!pIds.isEmpty() && !nIds.isEmpty()) { + log.warn("Found members node and members property. Ignoring {} members", useMembersNode ? "property" : "node"); + } + + return result; + } + + + /** + * @return a new Session that needs to be properly released after usage. + */ + private SessionImpl getSession() { + try { + return (SessionImpl) systemSession.createSession(systemSession.getWorkspace().getName()); + } catch (RepositoryException e) { + // fallback + return systemSession; + } + } + + /** + * Returns the membership references for the given authorizable. + * @param authorizableNodeIdentifier Identifier of the authorizable node + * @param session session to read from + * @return the property iterator or null + */ + private static PropertyIterator getMembershipReferences(String authorizableNodeIdentifier, Session session) { + PropertyIterator refs = null; + try { + refs = session.getNodeByIdentifier(authorizableNodeIdentifier).getWeakReferences(null); + } catch (RepositoryException e) { + log.error("Failed to retrieve membership references of " + authorizableNodeIdentifier + ".", e); + } + return refs; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/NodeResolver.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/NodeResolver.java new file mode 100644 index 00000000000..baadcfcc93e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/NodeResolver.java @@ -0,0 +1,273 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.util.Collections; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.commons.predicate.Predicate; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Resolver: searches for user and/or groups stored in Nodes of a {@link javax.jcr.Workspace} + * which match a certain criteria + */ +abstract class NodeResolver { + + private static Logger log = LoggerFactory.getLogger(NodeResolver.class); + + private final Session session; + private final NamePathResolver resolver; + + private String userSearchRoot = UserConstants.USERS_PATH; + private String groupSearchRoot = UserConstants.GROUPS_PATH; + private String authorizableSearchRoot = UserConstants.AUTHORIZABLES_PATH; + + /** + * Create a new NodeResolver. + * + * @param session to use for repository access + * @param resolver The NamePathResolver used to convert {@link org.apache.jackrabbit.spi.Name} + * and {@link org.apache.jackrabbit.spi.Path} to JCR names/path. + */ + NodeResolver(Session session, NamePathResolver resolver) { + this.session = session; + this.resolver = resolver; + } + + void setSearchRoots(String userSearchRoot, String groupSearchRoot) { + this.userSearchRoot = userSearchRoot; + this.groupSearchRoot = groupSearchRoot; + + authorizableSearchRoot = userSearchRoot; + while (!Text.isDescendant(authorizableSearchRoot, groupSearchRoot)) { + authorizableSearchRoot = Text.getRelativeParent(authorizableSearchRoot, 1); + } + } + + /** + * Get the first node that matches ntName and whose name + * exactly matches the given nodeName. + * + * @param nodeName Name of the node to find. + * @param ntName Node type name of the node to find. + * @return A matching node or null. + * @throws RepositoryException If an error occurs. + */ + public abstract Node findNode(Name nodeName, Name ntName) throws RepositoryException; + + /** + * Get the first node that matches ntName and has a + * property whose value exactly matches the given value. Same as + * {@link #findNodes(Set,String,Name,boolean,long)} but returning a single node or null. + * + * @param propertyName Name of the property to find. + * @param value Value of the property to find. + * @param ntName Name of the parent node's node type. + * @return The first node that matches the specified node type name and has + * a property with the given propertyName that exactly matches the given + * value or null. + * @throws RepositoryException If an error occurs. + */ + public abstract Node findNode(Name propertyName, String value, Name ntName) throws RepositoryException; + + /** + * Search for Nodes which contain an exact match for the given value in + * their property as indicated by the propertyName argument.
    + * Same as {@link #findNodes(Set,String,Name,boolean,long)}; where + * the maxSize parameters is set to {@link Long#MAX_VALUE)}. + * + * @param propertyName Name of the property to be searched. + * @param value Value to be matched. + * @param ntName Name of the parent node's node type. + * @param exact If true value has to match exactly. + * @return matching nodes (or an empty iterator if no match was found). + * @throws RepositoryException If an error occurs. + */ + public NodeIterator findNodes(Name propertyName, String value, Name ntName, boolean exact) + throws RepositoryException { + return findNodes(Collections.singleton(propertyName), value, ntName, exact, Long.MAX_VALUE); + } + + /** + * Search nodes. Take the arguments as search criteria. + * The queried value has to be a string fragment of one of the Properties + * contained in the given set. And the node have to be of a requested nodetype + * + * @param propertyNames Names of the property to be searched. + * @param value The value to find. + * @param ntName NodeType the hits have to have + * @param exact if true match must be exact + * @param maxSize maximal number of results to search for. + * @return matching nodes (or an empty iterator if no match was found). + * @throws RepositoryException If an error occurs. + */ + public abstract NodeIterator findNodes(Set propertyNames, String value, + Name ntName, boolean exact, long maxSize) + throws RepositoryException; + + /** + * Search all properties underneath an authorizable of the specified type + * that match the specified value and relative path. If the relative path + * consists of a single name element the path constraint is omitted. + * + * @param relPath + * @param value + * @param authorizableType + * @param exact + * @param maxSize + * @return + * @throws RepositoryException + */ + public abstract NodeIterator findNodes(Path relPath, String value, + int authorizableType, boolean exact, + long maxSize) throws RepositoryException; + + /** + * @return Session this instance has been constructed with. + */ + Session getSession() { + return session; + } + + /** + * @return The NamePathResolver. + */ + NamePathResolver getNamePathResolver() { + return resolver; + } + + /** + * @param ntName Any of the following node type names: + * {@link UserConstants#NT_REP_USER}, {@link UserConstants#NT_REP_GROUP} or + * {@link UserConstants#NT_REP_AUTHORIZABLE}. + * @return The path of search root for the specified node type name. + */ + String getSearchRoot(Name ntName) { + String searchRoot; + if (UserConstants.NT_REP_USER.equals(ntName)) { + searchRoot = userSearchRoot; + } else if (UserConstants.NT_REP_GROUP.equals(ntName)) { + searchRoot = groupSearchRoot; + } else { + searchRoot = authorizableSearchRoot; + } + return searchRoot; + } + + /** + * @param authorizableType + * @return The path of search root for the specified authorizable type. + */ + String getSearchRoot(int authorizableType) { + switch (authorizableType) { + case UserManager.SEARCH_TYPE_USER: + return userSearchRoot; + case UserManager.SEARCH_TYPE_GROUP: + return groupSearchRoot; + default: + return authorizableSearchRoot; + } + } + + /** + * + * @param authorizableType + * @param exact If exact is true, the predicate only evaluates to true if the + * passed node is of the required authorizable node type. Otherwise, all + * ancestors are taken into account as well. + * @return a new AuthorizableTypePredicate instance. + */ + AuthorizableTypePredicate getAuthorizableTypePredicate(int authorizableType, boolean exact) { + return new AuthorizableTypePredicate(authorizableType, exact); + } + + //-------------------------------------------------------------------------- + /** + * + */ + static class AuthorizableTypePredicate implements Predicate { + + private final int authorizableType; + private final boolean exact; + + private AuthorizableTypePredicate(int authorizableType, boolean exact) { + this.authorizableType = authorizableType; + this.exact = exact; + } + + /** + * @see Predicate#evaluate(Object) + */ + public boolean evaluate(Object object) { + if (object instanceof NodeImpl) { + Node n = getAuthorizableNode((NodeImpl) object); + return n != null; + } + return false; + } + + Node getAuthorizableNode(NodeImpl n) { + try { + if (matches(n)) { + return n; + } + + if (!exact) { + // walk up the node hierarchy to verify it is a child node + // of an authorizable node of the expected type. + while (n.getDepth() > 0) { + n = (NodeImpl) n.getParent(); + if (matches(n)) { + return n; + } + } + } + } catch (RepositoryException e) { + log.debug(e.getMessage()); + } + return null; + } + + private boolean matches(NodeImpl result) throws RepositoryException { + Name ntName = ((NodeTypeImpl) result.getPrimaryNodeType()).getQName(); + switch (authorizableType) { + case UserManager.SEARCH_TYPE_GROUP: + return UserConstants.NT_REP_GROUP.equals(ntName); + case UserManager.SEARCH_TYPE_USER: + return UserConstants.NT_REP_USER.equals(ntName); + default: + return UserConstants.NT_REP_USER.equals(ntName) || UserConstants.NT_REP_GROUP.equals(ntName); + } + } + } +} + + diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.java new file mode 100644 index 00000000000..48aa241b0d9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/PasswordUtility.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.core.security.user; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +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 Charset ENCODING = StandardCharsets.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. + */ + public static String buildPasswordHash(String password) throws NoSuchAlgorithmException { + 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. + */ + public static String buildPasswordHash(String password, String algorithm, + int saltSize, int iterations) throws NoSuchAlgorithmException { + 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); + } + + /** + * 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(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 of 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 compareSecure(hashedPassword, hash); + } // hashedPassword is plaintext -> return false + } catch (NoSuchAlgorithmException e) { + log.warn(e.getMessage()); + } + return false; + } + + /** + * 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. + */ + public static String extractAlgorithm(String hashedPwd) { + if (hashedPwd != null && hashedPwd.length() > 0) { + 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 >--- + + private static String generateHash(String pwd, String algorithm, String salt, int iterations) throws NoSuchAlgorithmException { + StringBuilder passwordHash = new StringBuilder(); + passwordHash.append('{').append(algorithm).append('}'); + if (salt != null && salt.length() > 0) { + 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 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(); + } + + 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; + } + + /** + * Compare two strings. The comparison is constant time: it will always loop + * over all characters and doesn't use conditional operations in the loop to + * make sure an attacker can not use a timing attack. + * + * @param a + * @param b + * @return true if both parameters contain the same data. + */ + private static boolean compareSecure(String a, String b) { + if ((a == null) || (b == null)) { + return (a == null) && (b == null); + } + int len = a.length(); + if (len != b.length()) { + return false; + } + if (len == 0) { + return true; + } + // don't use conditional operations inside the loop + int bits = 0; + for (int i = 0; i < len; i++) { + // this will never reset any bits + bits |= a.charAt(i) ^ b.charAt(i); + } + return bits == 0; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/TraversingNodeResolver.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/TraversingNodeResolver.java new file mode 100644 index 00000000000..75e9c5a3bc4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/TraversingNodeResolver.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.core.security.user; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.regex.PatternSyntaxException; + +/** + * + */ +class TraversingNodeResolver extends NodeResolver { + + private static final Logger log = LoggerFactory.getLogger(TraversingNodeResolver.class); + + /** + * Additionally to the NodeType-Argument the resolvers searched is narrowed + * by indicating a Path to an {@link javax.jcr.Item} as start for the search + * + * @param session to use for repository access + * @param resolver The NamePathResolver used to convert {@link org.apache.jackrabbit.spi.Name} + * and {@link org.apache.jackrabbit.spi.Path} to JCR names/path. + */ + TraversingNodeResolver(Session session, NamePathResolver resolver) { + super(session, resolver); + } + + //-------------------------------------------------------< NodeResolver >--- + /** + * @inheritDoc + */ + @Override + public Node findNode(Name nodeName, Name ntName) throws RepositoryException { + String sr = getSearchRoot(ntName); + if (getSession().nodeExists(sr)) { + try { + Node root = getSession().getNode(sr); + return collectNode(nodeName, ntName, root.getNodes()); + } catch (PathNotFoundException e) { + // should not get here + log.warn("Error while retrieving node " + sr); + } + } // else: searchRoot does not exist yet -> omit the search + return null; + } + + /** + * @inheritDoc + */ + @Override + public Node findNode(Name propertyName, String value, Name ntName) throws RepositoryException { + String sr = getSearchRoot(ntName); + if (getSession().nodeExists(sr)) { + try { + Node root = getSession().getNode(sr); + Set matchSet = new HashSet(); + collectNodes(value, Collections.singleton(propertyName), ntName, root.getNodes(), matchSet, true, 1); + + NodeIterator it = new NodeIteratorAdapter(matchSet); + if (it.hasNext()) { + return it.nextNode(); + } + } catch (PathNotFoundException e) { + // should not get here + log.warn("Error while retrieving node " + sr); + } + } // else: searchRoot does not exist yet -> omit the search + return null; + } + + /** + * @inheritDoc + */ + @Override + public NodeIterator findNodes(Set propertyNames, String value, Name ntName, + boolean exact, long maxSize) throws RepositoryException { + String sr = getSearchRoot(ntName); + if (getSession().nodeExists(sr)) { + try { + Node root = getSession().getNode(sr); + Set matchSet = new HashSet(); + collectNodes(value, propertyNames, ntName, root.getNodes(), matchSet, exact, maxSize); + return new NodeIteratorAdapter(matchSet); + } catch (PathNotFoundException e) { + // should not get here + log.warn("Error while retrieving node " + sr); + } + } // else: searchRoot does not exist yet -> omit the search + return NodeIteratorAdapter.EMPTY; + } + + /** + * @inheritDoc + */ + @Override + public NodeIterator findNodes(Path relPath, String value, int authorizableType, boolean exact, long maxSize) throws RepositoryException { + String sr = getSearchRoot(authorizableType); + if (getSession().nodeExists(sr)) { + try { + String path = getNamePathResolver().getJCRPath(relPath); + AuthorizableTypePredicate pred = getAuthorizableTypePredicate(authorizableType, relPath.getLength() > 1); + + Node root = getSession().getNode(sr); + Map matchingNodes = new HashMap(); + collectNodes(value, path, pred, root.getNodes(), matchingNodes, exact, maxSize); + + return new NodeIteratorAdapter(matchingNodes.values()); + } catch (PathNotFoundException e) { + // should not get here + log.warn("Error while retrieving node " + sr); + } + } // else: searchRoot does not exist yet -> omit the search + return NodeIteratorAdapter.EMPTY; + } + + //-------------------------------------------------------------------------- + /** + * + * @param nodeName + * @param ntName + * @param nodes + * @return The first matching node or null. + */ + private Node collectNode(Name nodeName, Name ntName, NodeIterator nodes) { + Node match = null; + while (match == null && nodes.hasNext()) { + NodeImpl node = (NodeImpl) nodes.nextNode(); + try { + if (node.isNodeType(ntName) && nodeName.equals(node.getQName())) { + match = node; + } else if (node.hasNodes()) { + match = collectNode(nodeName, ntName, node.getNodes()); + } + } catch (RepositoryException e) { + log.warn("Internal error while accessing node", e); + } + } + return match; + } + + /** + * Searches the given value in the range of the given NodeIterator. + * This method is called recursively to look within the complete tree + * of authorizable nodes. + * + * @param value the value to be found in the nodes + * @param propertyNames property to be searched, or null if {@link javax.jcr.Item#getName()} + * @param nodeTypeName name of node types to search + * @param itr range of nodes and descendants to be searched + * @param matchSet Set of found matches to append results + * @param exact if set to true the value has to match exact + * @param maxSize + */ + private void collectNodes(String value, Set propertyNames, + Name nodeTypeName, NodeIterator itr, + Set matchSet, boolean exact, long maxSize) { + while (itr.hasNext()) { + NodeImpl node = (NodeImpl) itr.nextNode(); + try { + if (matches(node, nodeTypeName, propertyNames, value, exact)) { + matchSet.add(node); + maxSize--; + } + if (node.hasNodes() && maxSize > 0) { + collectNodes(value, propertyNames, nodeTypeName, + node.getNodes(), matchSet, exact, maxSize); + } + } catch (RepositoryException e) { + log.warn("Internal error while accessing node", e); + } + } + } + + private void collectNodes(String value, String relPath, + AuthorizableTypePredicate predicate, NodeIterator itr, + Map matchingNodes, boolean exact, long maxSize) { + while (itr.hasNext()) { + NodeImpl node = (NodeImpl) itr.nextNode(); + try { + Node authNode = getMatchingNode(node, predicate, relPath, value, exact); + if (authNode != null) { + matchingNodes.put(authNode.getIdentifier(), authNode); + maxSize--; + } else if (node.hasNodes() && maxSize > 0) { + collectNodes(value, relPath, predicate, node.getNodes(), matchingNodes, exact, maxSize); + } + } catch (RepositoryException e) { + log.warn("Internal error while accessing node", e); + } + } + } + + /** + * + * @param node + * @param nodeTypeName + * @param propertyNames + * @param value + * @param exact + * @return + * @throws RepositoryException + */ + private static boolean matches(NodeImpl node, Name nodeTypeName, + Collection propertyNames, String value, + boolean exact) throws RepositoryException { + + boolean match = false; + if (node.isNodeType(nodeTypeName)) { + if (value == null) { + match = true; + } else { + try { + if (propertyNames.isEmpty()) { + match = (exact) ? node.getName().equals(value) : + node.getName().matches(".*"+value+".*"); + } else { + Iterator pItr = propertyNames.iterator(); + while (!match && pItr.hasNext()) { + Name propertyName = pItr.next(); + if (node.hasProperty(propertyName)) { + Property prop = node.getProperty(propertyName); + if (prop.isMultiple()) { + Value[] values = prop.getValues(); + for (int i = 0; i < values.length && !match; i++) { + match = matches(value, values[i].getString(), exact); + } + } else { + match = matches(value, prop.getString(), exact); + } + } + } + } + } catch (PatternSyntaxException pe) { + log.debug("couldn't search for {}, pattern invalid: {}", + value, pe.getMessage()); + } + } + } + return match; + } + + /** + * + * @param node + * @param predicate + * @param relPath + * @param value + * @param exact + * @return + * @throws RepositoryException + */ + private static Node getMatchingNode(NodeImpl node, AuthorizableTypePredicate predicate, + String relPath, String value, + boolean exact) throws RepositoryException { + boolean match = false; + Node authNode = predicate.getAuthorizableNode(node); + if (authNode != null && node.hasProperty(relPath)) { + try { + Property prop = node.getProperty(relPath); + if (prop.isMultiple()) { + Value[] values = prop.getValues(); + for (int i = 0; i < values.length && !match; i++) { + match = matches(value, values[i].getString(), exact); + } + } else { + match = matches(value, prop.getString(), exact); + } + } catch (PatternSyntaxException pe) { + log.debug("couldn't search for {}, pattern invalid: {}", value, pe.getMessage()); + } + } + return (match) ? authNode : null; + } + + private static boolean matches(String value, String toMatch, boolean exact) { + return (exact) ? toMatch.equals(value) : toMatch.matches(".*"+value+".*"); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java new file mode 100644 index 00000000000..2fc7d76b1fb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserAccessControlProvider.java @@ -0,0 +1,601 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.ItemImpl; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.core.security.AnonymousPrincipal; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.authorization.AbstractAccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.AbstractCompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.AccessControlEditor; +import org.apache.jackrabbit.core.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.NamedAccessControlPolicyImpl; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.PrivilegeBits; +import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; + +import java.security.Principal; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Implementation of the AccessControlProvider interface that + * is used to protected the 'security workspace' containing the user and + * group data. It applies special care to make sure that modifying user data + * (e.g. password), group membership and impersonation is properly controlled. + *

    + * This provider creates upon initialization the following 2 groups: + *

      + *
    • User administrator
    • + *
    • Group administrator
    • + *
    + * + * The default access control policy defined by this provider has the following + * characteristics: + *
      + *
    • All authenticated users have READ permission to all items. If {link #PARAM_ANONYMOUS_ACCESS} + * is configured to be true this also applies to the anonymous user.
    • + * + *
    • every known user is allowed to modify it's own properties except for + * her/his group membership,
    • + * + *
    • members of the 'User administrator' group are allowed to create, modify + * and remove users,
    • + * + *
    • members of the 'Group administrator' group are allowed to create, modify + * and remove groups,
    • + * + *
    • group membership can only be edited by members of the 'Group administrator' + * and the 'User administrator' group.
    • + *
    + */ +public class UserAccessControlProvider extends AbstractAccessControlProvider + implements UserConstants { + + private static Logger log = LoggerFactory.getLogger(UserAccessControlProvider.class); + + /** + * Constant for the name of the configuration option "anonymousId". + * The option is a flag indicating the name of the anonymous user id. + */ + public static final String PARAM_ANONYMOUS_ID = "anonymousId"; + + /** + * Constant for the name of the configuration option "anonymousAccess". + */ + public static final String PARAM_ANONYMOUS_ACCESS = "anonymousAccess"; + + private final AccessControlPolicy policy; + + private String groupsPath; + private String usersPath; + + private Principal userAdminGroup; + private Principal groupAdminGroup; + + private String userAdminGroupPath; + private String groupAdminGroupPath; + private String administratorsGroupPath; + private boolean membersInProperty; + + private String anonymousId; + private boolean anonymousAccess; + + /** + * + */ + public UserAccessControlProvider() { + policy = new NamedAccessControlPolicyImpl("userPolicy"); + } + + //-------------------------------------------------< AccessControlUtils >--- + /** + * Always returns false, since this ac provider does not use content stored + * in items to evaluate AC information. + * + * @see org.apache.jackrabbit.core.security.authorization.AccessControlUtils#isAcItem(Path) + */ + @Override + public boolean isAcItem(Path absPath) throws RepositoryException { + return false; + } + + /** + * Always returns false, since this ac provider does not use content stored + * in items to evaluate AC information. + * + * @see org.apache.jackrabbit.core.security.authorization.AccessControlUtils#isAcItem(ItemImpl) + */ + @Override + public boolean isAcItem(ItemImpl item) throws RepositoryException { + return false; + } + + //----------------------------------------------< AccessControlProvider >--- + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#init(Session, Map) + */ + @Override + public void init(Session systemSession, Map configuration) throws RepositoryException { + super.init(systemSession, configuration); + if (systemSession instanceof SessionImpl) { + SessionImpl sImpl = (SessionImpl) systemSession; + String userAdminName = (configuration.containsKey(USER_ADMIN_GROUP_NAME)) ? configuration.get(USER_ADMIN_GROUP_NAME).toString() : USER_ADMIN_GROUP_NAME; + String groupAdminName = (configuration.containsKey(GROUP_ADMIN_GROUP_NAME)) ? configuration.get(GROUP_ADMIN_GROUP_NAME).toString() : GROUP_ADMIN_GROUP_NAME; + + // make sure the groups exist (and possibly create them). + UserManager uMgr = sImpl.getUserManager(); + userAdminGroup = initGroup(uMgr, userAdminName); + if (userAdminGroup != null && userAdminGroup instanceof ItemBasedPrincipal) { + userAdminGroupPath = ((ItemBasedPrincipal) userAdminGroup).getPath(); + } + groupAdminGroup = initGroup(uMgr, groupAdminName); + if (groupAdminGroup != null && groupAdminGroup instanceof ItemBasedPrincipal) { + groupAdminGroupPath = ((ItemBasedPrincipal) groupAdminGroup).getPath(); + } + + Principal administrators = initGroup(uMgr, SecurityConstants.ADMINISTRATORS_NAME); + if (administrators != null && administrators instanceof ItemBasedPrincipal) { + administratorsGroupPath = ((ItemBasedPrincipal) administrators).getPath(); + } + usersPath = (uMgr instanceof UserManagerImpl) ? ((UserManagerImpl) uMgr).getUsersPath() : UserConstants.USERS_PATH; + groupsPath = (uMgr instanceof UserManagerImpl) ? ((UserManagerImpl) uMgr).getGroupsPath() : UserConstants.GROUPS_PATH; + + membersInProperty = !(uMgr instanceof UserManagerImpl) || !((UserManagerImpl) uMgr).hasMemberSplitSize(); + + if (configuration.containsKey(PARAM_ANONYMOUS_ID)) { + anonymousId = (String) configuration.get(PARAM_ANONYMOUS_ID); + } else { + anonymousId = SecurityConstants.ANONYMOUS_ID; + } + + if (configuration.containsKey(PARAM_ANONYMOUS_ACCESS)) { + anonymousAccess = Boolean.parseBoolean((String) configuration.get(PARAM_ANONYMOUS_ACCESS)); + } else { + anonymousAccess = true; + } + + } else { + throw new RepositoryException("SessionImpl (system session) expected."); + } + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(org.apache.jackrabbit.spi.Path,org.apache.jackrabbit.core.security.authorization.CompiledPermissions) + */ + public AccessControlPolicy[] getEffectivePolicies(Path absPath, CompiledPermissions permissions) throws ItemNotFoundException, RepositoryException { + checkInitialized(); + return new AccessControlPolicy[] {policy}; + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEffectivePolicies(java.util.Set, CompiledPermissions) + */ + public AccessControlPolicy[] getEffectivePolicies(Set principals, CompiledPermissions permission) throws ItemNotFoundException, RepositoryException { + checkInitialized(); + return new AccessControlPolicy[] {policy}; + } + + /** + * Always returns null. + * + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#getEditor(Session) + */ + public AccessControlEditor getEditor(Session session) { + checkInitialized(); + // not editable at all: policy is always the default and cannot be + // changed using the JCR API. + return null; + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#compilePermissions(Set) + */ + public CompiledPermissions compilePermissions(Set principals) throws RepositoryException { + checkInitialized(); + if (isAdminOrSystem(principals)) { + return getAdminPermissions(); + } else { + if (!anonymousAccess && isAnonymous(principals)) { + return CompiledPermissions.NO_PERMISSION; + } + + // determined the 'user' present in the given set of principals. + ItemBasedPrincipal userPrincipal = getUserPrincipal(principals); + NodeImpl userNode = getUserNode(userPrincipal); + if (userNode == null) { + // no 'user' within set of principals -> no permissions in the + // security workspace. + return CompiledPermissions.NO_PERMISSION; + } else { + return new CompiledPermissionsImpl(principals, userNode.getPath()); + } + } + } + + /** + * @see org.apache.jackrabbit.core.security.authorization.AccessControlProvider#canAccessRoot(Set) + */ + public boolean canAccessRoot(Set principals) throws RepositoryException { + checkInitialized(); + if (!anonymousAccess && isAnonymous(principals)) { + return false; + } + return true; + } + + //------------------------------------------------------------< private >--- + + private ItemBasedPrincipal getUserPrincipal(Set principals) { + try { + UserManager uMgr = session.getUserManager(); + for (Principal p : principals) { + if (!(GroupPrincipals.isGroup(p)) && p instanceof ItemBasedPrincipal + && uMgr.getAuthorizable(p) != null) { + return (ItemBasedPrincipal) p; + } + } + } catch (RepositoryException e) { + // should never get here + log.error("Internal error while retrieving user principal: {}", e.getMessage()); + } + // none of the principals in the set is assigned to a User. + return null; + } + + private NodeImpl getUserNode(ItemBasedPrincipal principal) { + NodeImpl userNode = null; + if (principal != null) { + try { + String path = principal.getPath(); + userNode = (NodeImpl) session.getNode(path); + } catch (RepositoryException e) { + log.warn("Error while retrieving user node. {}", e.getMessage()); + } + } + return userNode; + } + + private Node getExistingNode(Path path) throws RepositoryException { + String absPath = session.getJCRPath(path.getNormalizedPath()); + if (session.nodeExists(absPath)) { + return session.getNode(absPath); + } else if (session.propertyExists(absPath)) { + return session.getProperty(absPath).getParent(); + } else { + String pPath = Text.getRelativeParent(absPath, 1); + while (!"/".equals(pPath)) { + if (session.nodeExists(pPath)) { + return session.getNode(pPath); + } else { + pPath = Text.getRelativeParent(pPath, 1); + } + } + throw new ItemNotFoundException("Unable to determine permissions: No item and no existing parent for target path " + absPath); + } + } + + private static boolean containsGroup(Set principals, Principal group) { + for (Iterator it = principals.iterator(); it.hasNext() && group != null;) { + Principal p = it.next(); + if (p.getName().equals(group.getName())) { + return true; + } + } + return false; + } + + private static Principal initGroup(UserManager uMgr, String principalName) { + Principal prnc = new PrincipalImpl(principalName); + try { + Authorizable auth = uMgr.getAuthorizable(prnc); + if (auth == null) { + auth = uMgr.createGroup(prnc); + } else { + if (!auth.isGroup()) { + log.warn("Cannot create group '" + principalName + "'; User with that principal already exists."); + auth = null; + } + } + if (auth != null) { + return auth.getPrincipal(); + } + } catch (RepositoryException e) { + // should never get here + log.error("Error while initializing user/group administrators: {}", e.getMessage()); + } + return null; + } + + private boolean isAnonymous(Set principals) { + for (Principal p : principals) { + if (p instanceof AnonymousPrincipal) { + return true; + } else if (p.getName().equals(anonymousId)) { + return true; + } + } + return false; + } + + //--------------------------------------------------------< inner class >--- + /** + * + */ + private class CompiledPermissionsImpl extends AbstractCompiledPermissions + implements SynchronousEventListener { + + private final String userNodePath; + private final Set principals; + + protected CompiledPermissionsImpl(Set principals, String userNodePath) throws RepositoryException { + this.userNodePath = userNodePath; + this.principals = principals; + + int events = Event.PROPERTY_CHANGED | Event.PROPERTY_ADDED | Event.PROPERTY_REMOVED; + observationMgr.addEventListener(this, events, groupsPath, true, null, null, false); + } + + private PrivilegeBits getPrivilegeBits(String... privNames) throws RepositoryException { + PrivilegeManagerImpl impl = getPrivilegeManagerImpl(); + Name[] names = new Name[privNames.length]; + for (int i = 0; i < privNames.length; i++) { + names[i] = session.getQName(privNames[i]); + } + return impl.getBits(names); + } + + private PrivilegeBits assertModifiable(PrivilegeBits bits) { + if (bits.isModifiable()) { + return bits; + } else { + return PrivilegeBits.getInstance(bits); + } + } + + //------------------------------------< AbstractCompiledPermissions >--- + /** + * @see AbstractCompiledPermissions#buildResult(Path) + */ + @Override + protected Result buildResult(Path path) throws RepositoryException { + NodeImpl userNode = null; + try { + if (session.nodeExists(userNodePath)) { + userNode = (NodeImpl) session.getNode(userNodePath); + } + } catch (RepositoryException e) { + // ignore + } + + if (userNode == null) { + // no Node corresponding to user for which the permissions are + // calculated -> no permissions/privileges. + log.debug("No node at " + userNodePath); + return Result.EMPTY; + } + + // no explicit denied permissions: + int denies = Permission.NONE; + // default allow permission and default privileges + int allows = Permission.READ; + PrivilegeBits privs; + // Determine if for path, the set of privileges must be calculated: + // Generally, privileges can only be determined for existing nodes. + String jcrPath = session.getJCRPath(path.getNormalizedPath()); + boolean calcPrivs = session.nodeExists(jcrPath); + if (calcPrivs) { + privs = getPrivilegeBits(Privilege.JCR_READ); + } else { + privs = PrivilegeBits.EMPTY; + } + + if (Text.isDescendant(usersPath, jcrPath)) { + boolean isUserAdmin = containsGroup(principals, userAdminGroup); + /* + below the user-tree + - determine position of target relative to the editing user + - target may not be below an existing user but only below an + authorizable folder. + - determine if the editing user is user-admin + */ + NodeImpl node = (NodeImpl) getExistingNode(path); + if (node.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) { + // an authorizable folder -> must be user admin in order + // to have permission to write. + if (isUserAdmin) { + allows |= (Permission.ADD_NODE | Permission.REMOVE_NODE | Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY | Permission.NODE_TYPE_MNGMT); + if (calcPrivs) { + // grant WRITE privilege + // note: ac-read/modification is not included + privs = assertModifiable(privs); + privs.add(getPrivilegeBits(PrivilegeRegistry.REP_WRITE)); + } + } + } else { + // rep:User node or some other custom node below an existing user. + // as the authorizable folder doesn't allow other residual + // child nodes. + boolean editingOwnUser = node.isSame(userNode); + if (editingOwnUser) { + // user can only read && write his own props + allows |= (Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY); + if (calcPrivs) { + privs = assertModifiable(privs); + privs.add(getPrivilegeBits(Privilege.JCR_MODIFY_PROPERTIES)); + } + } else if (isUserAdmin) { + allows |= (Permission.ADD_NODE | Permission.REMOVE_NODE | Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY | Permission.NODE_TYPE_MNGMT); + if (calcPrivs) { + // grant WRITE privilege + // note: ac-read/modification is not included + privs = assertModifiable(privs); + privs.add(getPrivilegeBits(PrivilegeRegistry.REP_WRITE)); + } + } // else: normal user that isn't allowed to modify another user. + } + } else if (Text.isDescendant(groupsPath, jcrPath)) { + boolean isGroupAdmin = containsGroup(principals, groupAdminGroup); + /* + below group-tree: + - test if the user is group-administrator. + - make sure group-admin cannot modify user-admin or administrators + - ... and cannot remove itself. + */ + if (isGroupAdmin) { + if (!jcrPath.startsWith(administratorsGroupPath) && + !jcrPath.startsWith(userAdminGroupPath)) { + if (jcrPath.equals(groupAdminGroupPath)) { + // no remove perm on group-admin node + allows |= (Permission.ADD_NODE | Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY | Permission.NODE_TYPE_MNGMT); + if (calcPrivs) { + privs = assertModifiable(privs); + privs.add(getPrivilegeBits(Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_MODIFY_PROPERTIES, Privilege.JCR_NODE_TYPE_MANAGEMENT)); + } + } else { + // complete write + allows |= (Permission.ADD_NODE | Permission.REMOVE_NODE | Permission.SET_PROPERTY | Permission.REMOVE_PROPERTY | Permission.NODE_TYPE_MNGMT); + if (calcPrivs) { + privs = assertModifiable(privs); + privs.add(getPrivilegeBits(PrivilegeRegistry.REP_WRITE)); + } + } + } + } + } // else outside of user/group tree -> read only. + return new Result(allows, denies, privs, PrivilegeBits.EMPTY); + } + + @Override + protected Result buildRepositoryResult() throws RepositoryException { + log.warn("TODO: JCR-2774 - Repository level permissions."); + return new Result(Permission.NONE, Permission.NONE, PrivilegeBits.EMPTY, PrivilegeBits.EMPTY); + } + + @Override + protected PrivilegeManagerImpl getPrivilegeManagerImpl() throws RepositoryException { + return (PrivilegeManagerImpl) ((JackrabbitWorkspace) session.getWorkspace()).getPrivilegeManager(); + } + + //--------------------------------------------< CompiledPermissions >--- + /** + * @see CompiledPermissions#close() + */ + @Override + public void close() { + try { + observationMgr.removeEventListener(this); + } catch (RepositoryException e) { + log.error("Internal error: {}", e.getMessage()); + } + super.close(); + } + + /** + * @see CompiledPermissions#grants(Path, int) + */ + @Override + public boolean grants(Path absPath, int permissions) throws RepositoryException { + if (permissions == Permission.READ) { + return canReadAll(); + } + // otherwise: retrieve from cache (or build) + return super.grants(absPath, permissions); + } + + /** + * @see CompiledPermissions#canReadAll() + */ + @Override + public boolean canReadAll() throws RepositoryException { + // for consistency with 'grants(Path, int) this method only returns + // true if there exists a node for 'userNodePath' + return session.nodeExists(userNodePath); + } + + /** + * @see CompiledPermissions#canRead(Path, ItemId) + */ + public boolean canRead(Path path, ItemId itemId) throws RepositoryException { + return canReadAll(); + } + + //--------------------------------------------------< EventListener >--- + /** + * Event listener is only interested in changes of group-membership + * that effect the permission-evaluation. + * + * @see javax.jcr.observation.EventListener#onEvent(EventIterator) + */ + public void onEvent(EventIterator events) { + while (events.hasNext()) { + Event ev = events.nextEvent(); + try { + String evPath = ev.getPath(); + String repMembers = session.getJCRName(UserConstants.P_MEMBERS); + if (repMembers.equals(Text.getName(evPath))) { + // invalidate the cached results + clearCache(); + // only need to clear the cache once. stop processing + break; + } else if (!membersInProperty) { + /* the affected property is not rep:Members and members are + stored in a tree structure (user manager configuration. + test if the parent node is of type rep:Members in order + to determine if any membership modification occurred.*/ + Node parent = session.getNodeByIdentifier(ev.getIdentifier()); + if (UserConstants.NT_REP_MEMBERS.equals(((NodeTypeImpl) parent.getPrimaryNodeType()).getQName())) { + clearCache(); + } + + } // else: not interested. + } catch (RepositoryException e) { + // should never get here + log.warn("Internal error: {}", e.getMessage()); + clearCache(); + } + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserConstants.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserConstants.java new file mode 100644 index 00000000000..a5d994d278d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserConstants.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.core.security.user; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; + +/** + * UserConstants... + */ +interface UserConstants { + + NameFactory NF = NameFactoryImpl.getInstance(); + + /** + * root-path to security related content e.g. principals + */ + String SECURITY_ROOT_PATH = "/rep:security"; + String AUTHORIZABLES_PATH = SECURITY_ROOT_PATH + "/rep:authorizables"; + String USERS_PATH = AUTHORIZABLES_PATH + "/rep:users"; + String GROUPS_PATH = AUTHORIZABLES_PATH + "/rep:groups"; + + /** + * Configuration key and default value for the the name of the + * 'UserAdmin' group-principal. + */ + String USER_ADMIN_GROUP_NAME = "UserAdmin"; + /** + * Configuration key and default value for the the name of the + * 'GroupAdmin' group-principal + */ + String GROUP_ADMIN_GROUP_NAME = "GroupAdmin"; + + Name P_PRINCIPAL_NAME = NF.create(Name.NS_REP_URI, "principalName"); + /** + * @deprecated As of 2.0 the id-hash is stored with the jcr:uuid making the + * rep:userId property redundant. It has been removed from the node type + * definition. + */ + Name P_USERID = NF.create(Name.NS_REP_URI, "userId"); + Name P_PASSWORD = NF.create(Name.NS_REP_URI, "password"); + Name P_DISABLED = NF.create(Name.NS_REP_URI, "disabled"); + + /** + * @deprecated As of 2.0 group membership is stored with the group node. + * @see #P_MEMBERS + */ + Name P_GROUPS = NF.create(Name.NS_REP_URI, "groups"); + + Name P_MEMBERS = NF.create(Name.NS_REP_URI, "members"); + Name N_MEMBERS = NF.create(Name.NS_REP_URI, "members"); + + /** + * Name of the user property containing the principal names of those allowed + * to impersonate. + */ + Name P_IMPERSONATORS = NF.create(Name.NS_REP_URI, "impersonators"); + + Name NT_REP_AUTHORIZABLE = NF.create(Name.NS_REP_URI, "Authorizable"); + Name NT_REP_AUTHORIZABLE_FOLDER = NF.create(Name.NS_REP_URI, "AuthorizableFolder"); + Name NT_REP_USER = NF.create(Name.NS_REP_URI, "User"); + Name NT_REP_GROUP = NF.create(Name.NS_REP_URI, "Group"); + Name NT_REP_MEMBERS = NF.create(Name.NS_REP_URI, "Members"); + Name MIX_REP_IMPERSONATABLE = NF.create(Name.NS_REP_URI, "Impersonatable"); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java new file mode 100644 index 00000000000..a76fa720fcb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImpl.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.user.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.security.authentication.CryptedSimpleCredentials; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; + +/** + * UserImpl + */ +public class UserImpl extends AuthorizableImpl implements User { + + private Principal principal; + private Impersonation impersonation; + + protected UserImpl(NodeImpl node, UserManagerImpl userManager) { + super(node, userManager); + } + + //-------------------------------------------------------< Authorizable >--- + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#isGroup() + */ + public boolean isGroup() { + return false; + } + + /** + * @see org.apache.jackrabbit.api.security.user.Authorizable#getPrincipal() + */ + public Principal getPrincipal() throws RepositoryException { + if (principal == null) { + if (isAdmin()) { + principal = new NodeBasedAdminPrincipal(getPrincipalName()); + } else { + principal = new NodeBasedPrincipal(getPrincipalName()); + } + } + return principal; + } + + //---------------------------------------------------------------< User >--- + /** + * @see User#isAdmin() + */ + public boolean isAdmin() { + try { + return userManager.isAdminId(getID()); + } catch (RepositoryException e) { + // should never get here + log.error("Internal error while retrieving UserID.", e); + return false; + } + } + + public boolean isSystemUser() { + return false; + } + + /** + * @see User#getCredentials() + */ + public Credentials getCredentials() throws RepositoryException { + try { + String password = getNode().getProperty(P_PASSWORD).getString(); + return new CryptedSimpleCredentials(getID(), password); + } catch (NoSuchAlgorithmException e) { + throw new RepositoryException(e); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(e); + } + } + + /** + * @see User#getImpersonation() + */ + public Impersonation getImpersonation() throws RepositoryException { + if (impersonation == null) { + impersonation = new ImpersonationImpl(this, userManager); + } + return impersonation; + } + + /** + * @see User#changePassword(String) + */ + public void changePassword(String password) throws RepositoryException { + userManager.onPasswordChange(this, password); + userManager.setPassword(getNode(), password, true); + if (userManager.isAutoSave()) { + getNode().save(); + } + } + + /** + * @see User#changePassword(String, String) + */ + public void changePassword(String password, String oldPassword) throws RepositoryException { + // make sure the old password matches. + String pwHash = getNode().getProperty(P_PASSWORD).getString(); + if (!PasswordUtility.isSame(pwHash, oldPassword)) { + throw new RepositoryException("Failed to change password: Old password does not match."); + } + changePassword(password); + } + + /** + * @see User#disable(String) + */ + public void disable(String reason) throws RepositoryException { + if (isAdmin()) { + throw new RepositoryException("The administrator user cannot be disabled."); + } + if (reason == null) { + if (isDisabled()) { + // enable the user again. + PropertyImpl disableProp = getNode().getProperty(P_DISABLED); + userManager.removeProtectedItem(disableProp, getNode()); + } // else: nothing to do. + } else { + Value v = getSession().getValueFactory().createValue(reason); + userManager.setProtectedProperty(getNode(), P_DISABLED, v); + } + } + + /** + * @see User#isDisabled() + */ + public boolean isDisabled() throws RepositoryException { + return getNode().hasProperty(P_DISABLED); + } + + /** + * @see User#getDisabledReason() + */ + public String getDisabledReason() throws RepositoryException { + if (isDisabled()) { + return getNode().getProperty(P_DISABLED).getString(); + } else { + return null; + } + } + + //-------------------------------------------------------------------------- + /** + * + */ + private class NodeBasedAdminPrincipal extends AdminPrincipal implements ItemBasedPrincipal { + + public NodeBasedAdminPrincipal(String adminId) { + super(adminId); + } + + public String getPath() throws RepositoryException { + return getNode().getPath(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java new file mode 100644 index 00000000000..e8b108780f5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserImporter.java @@ -0,0 +1,765 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.commons.flat.PropertySequence; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.session.SessionWriteOperation; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.core.xml.NodeInfo; +import org.apache.jackrabbit.core.xml.PropInfo; +import org.apache.jackrabbit.core.xml.ProtectedNodeImporter; +import org.apache.jackrabbit.core.xml.ProtectedPropertyImporter; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * UserImporter implements a DefaultProtectedPropertyImporter + * and DefaultProtectedNodeImporter that is able to deal with + * user/group content as defined by the default user related node types present + * with jackrabbit-core. + *

    + * The importer is intended to be used by applications that import user content + * extracted from another repository instance and immediately persist the + * imported content using {@link javax.jcr.Session#save()}. Omitting the + * save call will lead to transient, semi-validated user content and eventually + * to inconsistencies. + *

    + * Note the following restrictions: + *

      + *
    • The importer will only be initialized if the user manager is an instance + * of UserPerWorkspaceUserManager. + *
    • + *
    • The importer will only be initialized if the editing session starting + * this import is the same as the UserManager's Session instance. + *
    • + *
    • The jcr:uuid property of user and groups is defined to represent the + * hashed authorizable id as calculated by the UserManager. This importer + * is therefore not able to handle imports with + * {@link ImportUUIDBehavior#IMPORT_UUID_CREATE_NEW}.
    • + *
    • Importing user/group nodes outside of the hierarchy defined by + * {@link org.apache.jackrabbit.core.security.user.UserManagerImpl#getUsersPath()} + * and {@link org.apache.jackrabbit.core.security.user.UserManagerImpl#getGroupsPath()} + * will fail upon save as the mandatory properties will not be imported. The same may + * be true in case of {@link ImportUUIDBehavior#IMPORT_UUID_COLLISION_REPLACE_EXISTING} + * inserting the user/group node at some other place in the node hierarchy.
    • + *
    • While creating user/groups through the API the UserManagerImpl makes + * sure that authorizables are never nested and are created below a hierarchy + * of nt:AuthorizableFolder nodes. This isn't enforced by means of node type + * constraints but only by the API. This importer currently doesn't perform such + * a validation check.
    • + *
    • Any attempt to import conflicting data will cause the import to fail + * either immediately or upon calling {@link javax.jcr.Session#save()} with the + * following exceptions: + *
        + *
      • rep:members : Group membership
      • + *
      • rep:impersonators : Impersonators of a User.
      • + *
      + * The import behavior of these two properties is defined by the {@link #PARAM_IMPORT_BEHAVIOR} + * configuration parameter, which can be set to + *
        + *
      • {@link ImportBehavior#NAME_IGNORE ignore}: A warning is logged.
      • + *
      • {@link ImportBehavior#NAME_BESTEFFORT best effort}: A warning is logged + * and the importer tries to fix the problem.
      • + *
      • {@link ImportBehavior#NAME_ABORT abort}: The import is immediately + * aborted with a ConstraintViolationException. (default)
      • + *
      + *
    • + *
    + * Known Issue:
    + * Importing rep:impersonators property referring to principals + * that are created during this import AND have principalName different from the + * ID will no succeed, as the validation in ImpersonationImpl isn't able + * to find the authorizable with the given principal (reason: query will only + * find persisted content). + */ +public class UserImporter implements ProtectedPropertyImporter, ProtectedNodeImporter { + + private static final Logger log = LoggerFactory.getLogger(UserImporter.class); + + /** + * Parameter name for the import behavior configuration option. + */ + public static final String PARAM_IMPORT_BEHAVIOR = "importBehavior"; + + private JackrabbitSession session; + + private NamePathResolver resolver; + + private ReferenceChangeTracker referenceTracker; + + private UserPerWorkspaceUserManager userManager; + + private boolean initialized = false; + + private boolean resetAutoSave = false; + + private int importBehavior = ImportBehavior.IGNORE; + + /** + * Container used to collect group members stored in protected nodes. + */ + private Membership currentMembership; + + /** + * Temporary store for the pw an imported new user to be able to call + * the creation actions irrespective of the order of protected properties + */ + private Map currentPw = new HashMap(1); + + public boolean init(JackrabbitSession session, NamePathResolver resolver, + boolean isWorkspaceImport, + int uuidBehavior, ReferenceChangeTracker referenceTracker) { + + this.session = session; + this.resolver = resolver; + this.referenceTracker = referenceTracker; + + if (initialized) { + throw new IllegalStateException("Already initialized"); + } + if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) { + log.debug("ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW isn't supported when importing users or groups."); + return false; + } + if (isWorkspaceImport) { + log.debug("Only Session-Import is supported when importing users or groups."); + return false; + } + try { + UserManager uMgr = session.getUserManager(); + if (uMgr instanceof UserPerWorkspaceUserManager) { + // make sure the user managers autosave flag can be changed to false. + if (uMgr.isAutoSave()) { + uMgr.autoSave(false); + resetAutoSave = true; + log.debug("Changed autosave behavior of UserManager to 'false'."); + } + userManager = (UserPerWorkspaceUserManager) uMgr; + initialized = true; + } else { + // either wrong implementation or one that implicitly calls save. + log.debug("Failed to initialize UserImporter: UserManager isn't instance of UserPerWorkspaceUserManager or does implicit save call."); + } + } catch (RepositoryException e) { + // failed to access user manager or to set the autosave behavior + // -> return false (not initialized) as importer can't operate. + log.error("Failed to initialize UserImporter: ", e); + } + return initialized; + } + + // -----------------------------------------< ProtectedPropertyImporter >--- + /** + * @see ProtectedPropertyImporter#handlePropInfo(org.apache.jackrabbit.core.NodeImpl, org.apache.jackrabbit.core.xml.PropInfo, org.apache.jackrabbit.spi.QPropertyDefinition) + */ + public boolean handlePropInfo(NodeImpl parent, PropInfo protectedPropInfo, QPropertyDefinition def) throws RepositoryException { + if (!initialized) { + throw new IllegalStateException("Not initialized"); + } + + /* importer can only handle protected properties below user/group + nodes that are properly stored underneath the configured users/groups + hierarchies (see {@link UserManagerImpl#getAuthorizable(NodeImpl)}. + this prevents from importing user/group nodes somewhere in the + content hierarchy which isn't possible when creating user/groups + using the corresponding API calls {@link UserManager#createUser} or + {@link UserManager#createGroup} respectively. */ + Authorizable a = userManager.getAuthorizable(parent); + if (a == null) { + log.warn("Cannot handle protected PropInfo " + protectedPropInfo + ". Node " + parent + " doesn't represent a valid Authorizable."); + return false; + } + + // TODO: check if import should be aborted in case of nested authorizable. + + // assert that user manager is isn't in auto-save mode + if (userManager.isAutoSave()) { + userManager.autoSave(false); + } + try { + Name propName = protectedPropInfo.getName(); + if (UserConstants.P_PRINCIPAL_NAME.equals(propName)) { + // minimal validation that passed definition really matches the + // protected rep:principalName property defined by rep:Authorizable. + if (def.isMultiple() || !UserConstants.NT_REP_AUTHORIZABLE.equals(def.getDeclaringNodeType())) { + // some other unexpected property definition -> cannot handle + log.warn("Unexpected definition for property rep:principalName"); + return false; + } + + Value v = protectedPropInfo.getValues(PropertyType.STRING, resolver)[0]; + String princName = v.getString(); + userManager.setPrincipal(parent, new PrincipalImpl(princName)); + + /* + Execute authorizable actions for a NEW group as this is the + same place in the userManager#createGroup that the actions + are called. + In case of a NEW user the actions are executed if the password + has been imported before. + */ + if (parent.isNew()) { + if (a.isGroup()) { + userManager.onCreate((Group) a); + } else if (currentPw.containsKey(a.getID())) { + userManager.onCreate((User) a, currentPw.remove(a.getID())); + } + } + + return true; + } else if (UserConstants.P_PASSWORD.equals(propName)) { + if (a.isGroup()) { + log.warn("Expected parent node of type rep:User."); + return false; + } + // minimal validation of the passed definition + if (def.isMultiple() || !UserConstants.NT_REP_USER.equals(def.getDeclaringNodeType())) { + // some other unexpected property definition -> cannot handle + log.warn("Unexpected definition for property rep:password"); + return false; + } + + Value v = protectedPropInfo.getValues(PropertyType.STRING, resolver)[0]; + String pw = v.getString(); + userManager.setPassword(parent, pw, false); + + /* + Execute authorizable actions for a NEW user at this point after + having set the password if the principal name has already been + processed, otherwise postpone it. + */ + if (parent.isNew()) { + if (parent.hasProperty(UserConstants.P_PRINCIPAL_NAME)) { + userManager.onCreate((User) a, pw); + } else { + // principal name not yet available -> remember the pw + currentPw.clear(); + currentPw.put(a.getID(), pw); + } + } + return true; + + } else if (UserConstants.P_IMPERSONATORS.equals(propName)) { + if (a.isGroup()) { + // unexpected parent type -> cannot handle + log.warn("Expected parent node of type rep:User."); + return false; + } + + // minimal validation of the passed definition + if (!def.isMultiple() || !UserConstants.MIX_REP_IMPERSONATABLE.equals(def.getDeclaringNodeType())) { + // some other unexpected property definition -> cannot handle + log.warn("Unexpected definition for property rep:impersonators"); + return false; + } + + // since impersonators may be imported later on, postpone processing + // to the end. + // see -> process References + Value[] vs = protectedPropInfo.getValues(PropertyType.STRING, resolver); + referenceTracker.processedReference(new Impersonators(a.getID(), vs)); + return true; + + } else if (UserConstants.P_DISABLED.equals(propName)) { + if (a.isGroup()) { + log.warn("Expected parent node of type rep:User."); + return false; + } + // minimal validation of the passed definition + if (def.isMultiple() || !UserConstants.NT_REP_USER.equals(def.getDeclaringNodeType())) { + // some other unexpected property definition -> cannot handle + log.warn("Unexpected definition for property rep:disabled"); + return false; + } + + Value v = protectedPropInfo.getValues(PropertyType.STRING, resolver)[0]; + ((User) a).disable(v.getString()); + + return true; + + } else if (UserConstants.P_MEMBERS.equals(propName)) { + if (!a.isGroup()) { + // unexpected parent type -> cannot handle + log.warn("Expected parent node of type rep:Group."); + return false; + } + + // minimal validation of the passed definition + if (!def.isMultiple() || !UserConstants.NT_REP_GROUP.equals(def.getDeclaringNodeType())) { + // some other unexpected property definition -> cannot handle + log.warn("Unexpected definition for property rep:members"); + return false; + } + + // since group-members are references to user/groups that potentially + // are to be imported later on -> postpone processing to the end. + // see -> process References + Membership membership = new Membership(a.getID()); + for (Value v : protectedPropInfo.getValues(PropertyType.WEAKREFERENCE, resolver)) { + membership.addMember(new NodeId(v.getString())); + } + referenceTracker.processedReference(membership); + return true; + + } // else: cannot handle -> return false + + return false; + } finally { + // reset the autosave mode of the user manager in order to restore + // the original state. + if (resetAutoSave) { + userManager.autoSave(true); + } + } + } + + /** + * @see ProtectedPropertyImporter#handlePropInfo(org.apache.jackrabbit.core.NodeImpl, org.apache.jackrabbit.core.xml.PropInfo, org.apache.jackrabbit.spi.QPropertyDefinition) + */ + public boolean handlePropInfo(NodeState parent, PropInfo protectedPropInfo, QPropertyDefinition def) throws RepositoryException { + return false; + } + + /** + * @see org.apache.jackrabbit.core.xml.ProtectedPropertyImporter#processReferences() + */ + public void processReferences() throws RepositoryException { + if (!initialized) { + throw new IllegalStateException("Not initialized"); + } + + // assert that user manager is isn't in auto-save mode + if (userManager.isAutoSave()) { + userManager.autoSave(false); + } + try { + List processed = new ArrayList(); + for (Iterator it = referenceTracker.getProcessedReferences(); it.hasNext();) { + Object reference = it.next(); + if (reference instanceof Membership) { + Authorizable a = userManager.getAuthorizable(((Membership) reference).groupId); + if (a == null || !a.isGroup()) { + throw new RepositoryException(((Membership) reference).groupId + " does not represent a valid group."); + } + + final Group gr = (Group) a; + // 1. collect members to add and to remove. + Map toRemove = new HashMap(); + for (Iterator declMembers = gr.getDeclaredMembers(); declMembers.hasNext();) { + Authorizable dm = declMembers.next(); + toRemove.put(dm.getID(), dm); + } + + List toAdd = new ArrayList(); + final List nonExisting = new ArrayList(); + + for (Membership.Member member : ((Membership) reference).members) { + NodeId remapped = referenceTracker.getMappedId(member.id); + NodeId id = (remapped == null) ? member.id : remapped; + + Authorizable authorz = null; + try { + NodeImpl n = ((SessionImpl) session).getNodeById(id); + authorz = userManager.getAuthorizable(n); + } catch (RepositoryException e) { + // no such node or failed to retrieve authorizable + // warning is logged below. + } + if (authorz != null) { + if (toRemove.remove(authorz.getID()) == null) { + toAdd.add(authorz); + } // else: no need to remove from rep:members + } else { + handleFailure("New member of " + gr + ": No such authorizable (NodeID = " + id + ")"); + if (importBehavior == ImportBehavior.BESTEFFORT) { + log.info("ImportBehavior.BESTEFFORT: Remember non-existing member for processing."); + nonExisting.add(member); + } + } + } + + // 2. adjust members of the group + for (Authorizable m : toRemove.values()) { + if (!gr.removeMember(m)) { + handleFailure("Failed remove existing member (" + m + ") from " + gr); + } + } + for (Authorizable m : toAdd) { + if (!gr.addMember(m)) { + handleFailure("Failed add member (" + m + ") to " + gr); + } + } + + // handling non-existing members in case of best-effort + if (!nonExisting.isEmpty()) { + log.info("ImportBehavior.BESTEFFORT: Found " + nonExisting.size() + " entries of rep:members pointing to non-existing authorizables. Adding to rep:members."); + final NodeImpl groupNode = ((AuthorizableImpl) gr).getNode(); + + if (userManager.hasMemberSplitSize()) { + userManager.performProtectedOperation((SessionImpl) session, new SessionWriteOperation() { + public Boolean perform(SessionContext context) throws RepositoryException { + NodeImpl nMembers = (groupNode.hasNode(UserConstants.N_MEMBERS) + ? groupNode.getNode(UserConstants.N_MEMBERS) + : groupNode.addNode(UserConstants.N_MEMBERS, UserConstants.NT_REP_MEMBERS, null)); + + // Create N_MEMBERS node structure for holding member references + for (Membership.Member member : nonExisting) { + PropertySequence properties = GroupImpl.getPropertySequence(nMembers, userManager); + String propName = member.name; + if (propName == null) { + log.debug("Ignoring unnamed user with id {}", member.id); + continue; + } + if (properties.hasItem(propName)) { + log.debug("Overwriting authorizable {} which is already member of {}.", propName, gr); + properties.removeProperty(propName); + } + Value newMember = session.getValueFactory().createValue(member.id.toString(), PropertyType.WEAKREFERENCE); + properties.addProperty(propName, newMember); + } + return null; + } + }); + } else { + // Create P_MEMBERS for holding member references + // build list of valid members set before .... + List memberValues = new ArrayList(); + if (groupNode.hasProperty(UserConstants.P_MEMBERS)) { + Value[] vls = groupNode.getProperty(UserConstants.P_MEMBERS).getValues(); + memberValues.addAll(Arrays.asList(vls)); + } + + // ... and the non-Existing ones. + for (Membership.Member member : nonExisting) { + memberValues.add(session.getValueFactory().createValue(member.id.toString(), PropertyType.WEAKREFERENCE)); + } + + // and use implementation specific method to set the + // value of rep:members properties which was not possible + // through the API + userManager.setProtectedProperty(groupNode, + UserConstants.P_MEMBERS, + memberValues.toArray(new Value[memberValues.size()]), + PropertyType.WEAKREFERENCE); + } + } + + processed.add(reference); + + } else if (reference instanceof Impersonators) { + Authorizable a = userManager.getAuthorizable(((Impersonators) reference).userId); + if (a == null || a.isGroup()) { + throw new RepositoryException(((Impersonators) reference).userId + " does not represent a valid user."); + } + + Impersonation imp = ((User) a).getImpersonation(); + + // 1. collect principals to add and to remove. + Map toRemove = new HashMap(); + for (PrincipalIterator pit = imp.getImpersonators(); pit.hasNext();) { + Principal princ = pit.nextPrincipal(); + toRemove.put(princ.getName(), princ); + } + + List toAdd = new ArrayList(); + Value[] vs = ((Impersonators) reference).values; + for (Value v : vs) { + String princName = v.getString(); + if (toRemove.remove(princName) == null) { + // add it to the list of new impersonators to be added. + toAdd.add(new PrincipalImpl(princName)); + } // else: no need to revoke impersonation for the given principal. + } + + // 2. adjust set of impersonators + for (Principal princ : toRemove.values()) { + if (!imp.revokeImpersonation(princ)) { + handleFailure("Failed to revoke impersonation for " + princ.getName() + " on " + a); + } + } + for (Principal princ : toAdd) { + if (!imp.grantImpersonation(princ)) { + handleFailure("Failed to grant impersonation for " + princ.getName() + " on " + a); + } + } + // NOTE: no best effort handling so far. (TODO) + + processed.add(reference); + } + } + // successfully processed this entry of the reference tracker + // -> remove from the reference tracker. + referenceTracker.removeReferences(processed); + } finally { + // reset the autosave mode of the user manager in order to restore + // the original state. + if (resetAutoSave) { + userManager.autoSave(true); + } + } + } + + // ---------------------------------------------< ProtectedNodeImporter >--- + /** + * @see ProtectedNodeImporter#start(org.apache.jackrabbit.core.NodeImpl) + */ + public boolean start(NodeImpl protectedParent) throws RepositoryException { + String repMembers = resolver.getJCRName(UserConstants.NT_REP_MEMBERS); + if (repMembers.equals(protectedParent.getPrimaryNodeType().getName())) { + NodeImpl groupNode = protectedParent; + while (groupNode.getDepth() != 0 && + repMembers.equals(groupNode.getPrimaryNodeType().getName())) { + + groupNode = (NodeImpl) groupNode.getParent(); + } + Authorizable auth = userManager.getAuthorizable(groupNode); + if (auth == null) { + log.debug("Cannot handle protected node " + protectedParent + ". It nor one of its parents represent a valid Authorizable."); + return false; + } else { + currentMembership = new Membership(auth.getID()); + return true; + } + } else { + return false; + } + } + + /** + * @see ProtectedNodeImporter#start(org.apache.jackrabbit.core.state.NodeState) + */ + public boolean start(NodeState protectedParent) { + return false; + } + + /** + * @see ProtectedNodeImporter#start(org.apache.jackrabbit.core.NodeImpl) + */ + public void startChildInfo(NodeInfo childInfo, List propInfos) throws RepositoryException { + assert (currentMembership != null); + + if (UserConstants.NT_REP_MEMBERS.equals(childInfo.getNodeTypeName())) { + for (PropInfo prop : propInfos) { + for (Value v : prop.getValues(PropertyType.WEAKREFERENCE, resolver)) { + String name = resolver.getJCRName(prop.getName()); + NodeId id = new NodeId(v.getString()); + currentMembership.addMember(name, id); + } + } + } else { + log.warn("{} is not of type {}", childInfo.getName(), UserConstants.NT_REP_MEMBERS); + } + } + + /** + * @see org.apache.jackrabbit.core.xml.ProtectedNodeImporter#endChildInfo() + */ + public void endChildInfo() throws RepositoryException { + } + + /** + * @see ProtectedNodeImporter#end(org.apache.jackrabbit.core.NodeImpl) + */ + public void end(NodeImpl protectedParent) throws RepositoryException { + referenceTracker.processedReference(currentMembership); + currentMembership = null; + } + + /** + * @see ProtectedNodeImporter#end(org.apache.jackrabbit.core.state.NodeState) + */ + public void end(NodeState protectedParent) { + } + + //---------------------------------------------------------< BeanConfig >--- + /** + * @return human readable representation of the importBehavior value. + */ + public String getImportBehavior() { + return ImportBehavior.nameFromValue(importBehavior); + } + + /** + * + * @param importBehaviorStr + */ + public void setImportBehavior(String importBehaviorStr) { + this.importBehavior = ImportBehavior.valueFromName(importBehaviorStr); + } + + //------------------------------------------------------------< private >--- + /** + * Handling the import behavior + * + * @param msg + * @throws RepositoryException + */ + private void handleFailure(String msg) throws RepositoryException { + switch (importBehavior) { + case ImportBehavior.IGNORE: + case ImportBehavior.BESTEFFORT: + log.warn(msg); + break; + case ImportBehavior.ABORT: + throw new ConstraintViolationException(msg); + default: + // no other behavior. nothing to do. + + } + } + + //------------------------------------------------------< inner classes >--- + /** + * Inner class used to postpone import of group membership to the very end + * of the import. This allows to import membership of user/groups that + * are only being created during this import. + * + * @see ImportBehavior For additional configuration options. + */ + private static final class Membership { + private final String groupId; + private final List members = new LinkedList(); + + public Membership(String groupId) { + this.groupId = groupId; + } + + public void addMember(String name, NodeId id) { + members.add(new Member(name, id)); + } + + public void addMember(NodeId id) { + addMember(null, id); + } + + // If only Java had tuples... + public class Member { + private final String name; + private final NodeId id; + + public Member(String name, NodeId id) { + super(); + this.name = name; + this.id = id; + } + } + } + + /** + * Inner class used to postpone import of impersonators to the very end + * of the import. This allows to import impersonation values pointing + * to user that are only being created during this import. + * + * @see ImportBehavior For additional configuration options. + */ + private static final class Impersonators { + + private final String userId; + private final Value[] values; + + private Impersonators(String userId, Value[] values) { + this.userId = userId; + this.values = values; + } + } + + /** + * Inner class defining the treatment of membership or impersonator + * values pointing to non-existing authorizables. + */ + public static final class ImportBehavior { + + /** + * If a member or impersonator value cannot be set due to constraints + * enforced by the API implementation, the failure is logged as + * warning but otherwise ignored. + */ + public static final int IGNORE = 1; + /** + * Same as {@link #IGNORE} but in addition tries to circumvent the + * problem. This option should only be used with validated and trusted + * XML passed to the SessionImporter. + */ + public static final int BESTEFFORT = 2; + /** + * Aborts the import as soon as invalid values are detected throwing + * a ConstraintViolationException. + */ + public static final int ABORT = 3; + + public static final String NAME_IGNORE = "ignore"; + public static final String NAME_BESTEFFORT = "besteffort"; + public static final String NAME_ABORT = "abort"; + + public static int valueFromName(String behaviorString) { + if (NAME_IGNORE.equalsIgnoreCase(behaviorString)) { + return IGNORE; + } else if (NAME_BESTEFFORT.equalsIgnoreCase(behaviorString)) { + return BESTEFFORT; + } else if (NAME_ABORT.equalsIgnoreCase(behaviorString)) { + return ABORT; + } else { + log.error("Invalid behavior " + behaviorString + " -> Using default: ABORT."); + return ABORT; + } + } + + public static String nameFromValue(int importBehavior) { + switch (importBehavior) { + case ImportBehavior.IGNORE: + return NAME_IGNORE; + case ImportBehavior.ABORT: + return NAME_ABORT; + case ImportBehavior.BESTEFFORT: + return NAME_BESTEFFORT; + default: + throw new IllegalArgumentException("Invalid import behavior: " + importBehavior); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerConfig.java new file mode 100644 index 00000000000..f90cd041c36 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerConfig.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.core.security.user; + +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Properties; + +/** + * Utility to retrieve configuration parameters for UserManagerImpl + */ +class UserManagerConfig { + + private static final Logger log = LoggerFactory.getLogger(UserManagerConfig.class); + + private final Properties config; + private final String adminId; + /** + * Authorizable actions that will all be executed upon creation and removal + * of authorizables in the order they are contained in the array.

    + * Note, that if {@link #isAutoSave() autosave} is turned on, the configured + * actions are executed before persisting the creation or removal. + */ + private AuthorizableAction[] actions; + + UserManagerConfig(Properties config, String adminId, AuthorizableAction[] actions) { + this.config = config; + this.adminId = adminId; + this.actions = (actions == null) ? new AuthorizableAction[0] : actions; + } + + public T getConfigValue(String key, T defaultValue) { + if (config != null && config.containsKey(key)) { + return convert(config.get(key), defaultValue); + } else { + return defaultValue; + } + } + + public String getAdminId() { + return adminId; + } + + public AuthorizableAction[] getAuthorizableActions() { + return actions; + } + + public void setAuthorizableActions(AuthorizableAction[] actions) { + if (actions != null) { + this.actions = actions; + } + } + + //--------------------------------------------------------< private >--- + private T convert(Object v, T defaultValue) { + if (v == null) { + return null; + } + + T value; + String str = v.toString(); + Class targetClass = (defaultValue == null) ? String.class : defaultValue.getClass(); + try { + 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(), v); + throw new IllegalArgumentException("Cannot convert config entry " + v + " to " + targetClass.getName()); + } + } catch (NumberFormatException e) { + log.warn("Invalid value {}; cannot be parsed into {}", v, targetClass.getName()); + value = defaultValue; + } + + return value; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java new file mode 100644 index 00000000000..cc50a886f43 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserManagerImpl.java @@ -0,0 +1,1627 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.AuthorizableExistsException; +import org.apache.jackrabbit.api.security.user.AuthorizableTypeException; +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.core.ItemImpl; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.ProtectedItemModifier; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.SessionListener; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; +import org.apache.jackrabbit.core.session.SessionOperation; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.util.HashSet; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; + +/** + *

    Implementation Characteristics

    + * + * Default implementation of the UserManager interface with the + * following characteristics: + * + *
      + *
    • Users and Groups are stored in the repository as JCR nodes.
    • + *
    • Users are created below {@link UserConstants#USERS_PATH},
      Groups are + * created below {@link UserConstants#GROUPS_PATH} (unless otherwise configured).
    • + *
    • The Id of an authorizable is stored in the jcr:uuid property (md5 hash).
    • + *
    • In order to structure the users and groups tree and avoid creating a flat + * hierarchy, additional hierarchy nodes of type "rep:AuthorizableFolder" are + * introduced using + *
        + *
      • the specified intermediate path passed to the create methods
      • + *
      • or some built-in logic if the intermediate path is missing.
      • + *
      + *
    • + *
    + * + *

    Authorizable Creation

    + * + * The built-in logic applies the following rules: + *
      + *
    • The names of the hierarchy folders is determined from ID of the + * authorizable to be created, consisting of the leading N chars where N is + * the relative depth starting from the node at {@link #getUsersPath()} + * or {@link #getGroupsPath()}.
    • + *
    • By default 2 levels (depth == 2) are created.
    • + *
    • Parent nodes are expected to consist of folder structure only.
    • + *
    • If the ID contains invalid JCR chars that would prevent the creation of + * a Node with that name, the names of authorizable node and the intermediate + * hierarchy nodes are {@link Text#escapeIllegalJcrChars(String) escaped}.
    • + *
    + * Examples: + * Creating an non-existing user with ID 'aSmith' without specifying an + * intermediate path would result in the following structure: + * + *
    + * + rep:security            [nt:unstructured]
    + *   + rep:authorizables     [rep:AuthorizableFolder]
    + *     + rep:users           [rep:AuthorizableFolder]
    + *       + a                 [rep:AuthorizableFolder]
    + *         + aS              [rep:AuthorizableFolder]
    + *           + aSmith        [rep:User]
    + * 
    + * + * Creating a non-existing user with ID 'aSmith' specifying an intermediate + * path 'some/tree' would result in the following structure: + * + *
    + * + rep:security            [nt:unstructured]
    + *   + rep:authorizables     [rep:AuthorizableFolder]
    + *     + rep:users           [rep:AuthorizableFolder]
    + *       + some              [rep:AuthorizableFolder]
    + *         + tree            [rep:AuthorizableFolder]
    + *           + aSmith        [rep:User]
    + * 
    + * + *

    Configuration

    + * + * This UserManager is able to handle the following configuration + * options: + * + *

    Configuration Parameters

    + *
      + *
    • {@link #PARAM_USERS_PATH}: Defines where user nodes are created. + * If missing set to {@link #USERS_PATH}.
    • + *
    • {@link #PARAM_GROUPS_PATH}. Defines where group nodes are created. + * If missing set to {@link #GROUPS_PATH}.
    • + *
    • {@link #PARAM_COMPATIBLE_JR16}: If the param is present and its + * value is true looking up authorizables by ID will use the + * NodeResolver if not found otherwise.
      + * If the parameter is missing (or false) users and groups created + * with a Jackrabbit repository < v2.0 will not be found any more.
      + * By default this option is disabled.
    • + *
    • {@link #PARAM_DEFAULT_DEPTH}: Parameter used to change the number of + * levels that are used by default to store authorizable nodes.
      The value is + * expected to be a positive integer greater than zero. The default + * number of levels is 2. + *
    • + *
    • {@link #PARAM_AUTO_EXPAND_TREE}: If this parameter is present and its + * value is true, the trees containing user and group nodes will + * automatically created additional hierarchy levels if the number of nodes + * on a given level exceeds the maximal allowed {@link #PARAM_AUTO_EXPAND_SIZE size}. + *
      By default this option is disabled.
    • + *
    • {@link #PARAM_AUTO_EXPAND_SIZE}: This parameter only takes effect + * if {@link #PARAM_AUTO_EXPAND_TREE} is enabled.
      The value is expected to be + * a positive long greater than zero. The default value is 1000.
    • + *
    • {@link #PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE}: If this parameter is present + * group memberships are collected in a node structure below {@link UserConstants#N_MEMBERS} + * instead of the default multi valued property {@link UserConstants#P_MEMBERS}. + * Its value determines the maximum number of member properties until additional + * intermediate nodes are inserted. Valid parameter values are integers > 4.
    • + *
    • {@link #PARAM_PASSWORD_HASH_ALGORITHM}: Optional parameter to configure + * the algorithm used for password hash generation. The default value is + * {@link PasswordUtility#DEFAULT_ALGORITHM}.
    • + *
    • {@link #PARAM_PASSWORD_HASH_ITERATIONS}: Optional parameter to configure + * the number of iterations used for password hash generations. The default + * value is {@link PasswordUtility#DEFAULT_ITERATIONS}.
    • + *
    + * + *

    Authorizable Actions

    + * In addition to the specified configuration parameters this user manager + * implementation allows to define zero to many {@link AuthorizableAction}s. + * Authorizable actions provide the ability to execute additional validation or + * tasks upon authorizable creation, removal and upon changing a users password.
    + * See also {@link org.apache.jackrabbit.core.config.UserManagerConfig#getAuthorizableActions()} + */ +public class UserManagerImpl extends ProtectedItemModifier + implements UserManager, UserConstants, SessionListener { + + /** + * Configuration option to change the + * {@link UserConstants#USERS_PATH default path} for creating users. + */ + public static final String PARAM_USERS_PATH = "usersPath"; + + /** + * Configuration option to change the + * {@link UserConstants#GROUPS_PATH default path} for creating groups. + */ + public static final String PARAM_GROUPS_PATH = "groupsPath"; + + /** + * @deprecated Use {@link #PARAM_COMPATIBLE_JR16} instead. + */ + public static final String PARAM_COMPATIBILE_JR16 = "compatibleJR16"; + + /** + * Flag to enable a minimal backwards compatibility with Jackrabbit < + * v2.0
    + * If the param is present and its value is true looking up + * authorizables by ID will use the NodeResolver if not found + * otherwise.
    + * If the parameter is missing (or false) users and groups created + * with a Jackrabbit repository < v2.0 will not be found any more.
    + * By default this option is disabled. + */ + public static final String PARAM_COMPATIBLE_JR16 = "compatibleJR16"; + + /** + * Parameter used to change the number of levels that are used by default + * store authorizable nodes.
    The default number of levels is 2. + *

    + * NOTE: Changing the default depth once users and groups + * have been created in the repository will cause inconsistencies, due to + * the fact that the resolution of ID to an authorizable relies on the + * structure defined by the default depth.
    + * It is recommended to remove all authorizable nodes that will not be + * reachable any more, before this config option is changed. + *

      + *
    • If default depth is increased:
      + * All authorizables on levels < default depth are not reachable any more.
    • + *
    • If default depth is decreased:
      + * All authorizables on levels > default depth aren't reachable any more + * unless the {@link #PARAM_AUTO_EXPAND_TREE} flag is set to true.
    • + *
    + */ + public static final String PARAM_DEFAULT_DEPTH = "defaultDepth"; + + /** + * If this parameter is present and its value is true, the trees + * containing user and group nodes will automatically created additional + * hierarchy levels if the number of nodes on a given level exceeds the + * maximal allowed {@link #PARAM_AUTO_EXPAND_SIZE size}. + *
    By default this option is disabled. + */ + public static final String PARAM_AUTO_EXPAND_TREE = "autoExpandTree"; + + /** + * This parameter only takes effect if {@link #PARAM_AUTO_EXPAND_TREE} is + * enabled.
    The default value is 1000. + */ + public static final String PARAM_AUTO_EXPAND_SIZE = "autoExpandSize"; + + /** + * If this parameter is present group members are collected in a node + * structure below {@link UserConstants#N_MEMBERS} instead of the default + * multi valued property {@link UserConstants#P_MEMBERS}. Its value determines + * the maximum number of member properties until additional intermediate nodes + * are inserted. Valid values are integers > 4. The default value is 0 and + * indicates that the {@link UserConstants#P_MEMBERS} property is used to + * record group members. + */ + public static final String PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE = "groupMembershipSplitSize"; + + /** + * Configuration parameter to change the default algorithm used to generate + * password hashes. The default value is {@link PasswordUtility#DEFAULT_ALGORITHM}. + */ + public static final String PARAM_PASSWORD_HASH_ALGORITHM = "passwordHashAlgorithm"; + + /** + * Configuration parameter to change the number of iterations used for + * password hash generation. The default value is {@link PasswordUtility#DEFAULT_ITERATIONS}. + */ + public static final String PARAM_PASSWORD_HASH_ITERATIONS = "passwordHashIterations"; + + private static final Logger log = LoggerFactory.getLogger(UserManagerImpl.class); + + private final SessionImpl session; + private final String adminId; + private final NodeResolver authResolver; + private final NodeCreator nodeCreator; + private final UserManagerConfig config; + + private final String usersPath; + private final String groupsPath; + private final MembershipCache membershipCache; + + /** + * Create a new UserManager with the default configuration. + * + * @param session The editing/reading session. + * @param adminId The user ID of the administrator. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + public UserManagerImpl(SessionImpl session, String adminId) throws RepositoryException { + this(session, adminId, null, null); + } + + /** + * Create a new UserManager + * + * @param session The editing/reading session. + * @param adminId The user ID of the administrator. + * @param config The configuration parameters. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + public UserManagerImpl(SessionImpl session, String adminId, Properties config) throws RepositoryException { + this(session, adminId, config, null); + } + + /** + * Create a new UserManager for the given session. + * Currently the following configuration options are respected: + * + *
      + *
    • {@link #PARAM_USERS_PATH}. If missing set to {@link UserConstants#USERS_PATH}.
    • + *
    • {@link #PARAM_GROUPS_PATH}. If missing set to {@link UserConstants#GROUPS_PATH}.
    • + *
    • {@link #PARAM_DEFAULT_DEPTH}. The default number of levels is 2.
    • + *
    • {@link #PARAM_AUTO_EXPAND_TREE}. By default this option is disabled.
    • + *
    • {@link #PARAM_AUTO_EXPAND_SIZE}. The default value is 1000.
    • + *
    • {@link #PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE}. The default is 0 which means use + * {@link UserConstants#P_MEMBERS}.
    • + *
    + * + * See the overall {@link UserManagerImpl introduction} for details. + * + * @param session The editing/reading session. + * @param adminId The user ID of the administrator. + * @param config The configuration parameters. + * @param mCache Shared membership cache. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + public UserManagerImpl(SessionImpl session, String adminId, Properties config, + MembershipCache mCache) throws RepositoryException { + this(session, new UserManagerConfig(config, adminId, null), mCache); + } + + /** + * Create a new UserManager for the given session. + * + * @param session The editing/reading session. + * @param config The user manager configuration. + * @param mCache The shared membership cache. + * @throws RepositoryException If an error occurs. + */ + private UserManagerImpl(SessionImpl session, UserManagerConfig config, MembershipCache mCache) throws RepositoryException { + this.session = session; + this.adminId = config.getAdminId(); + this.config = config; + + nodeCreator = new NodeCreator(config); + + this.usersPath = config.getConfigValue(PARAM_USERS_PATH, USERS_PATH); + this.groupsPath = config.getConfigValue(PARAM_GROUPS_PATH, GROUPS_PATH); + + if (mCache != null) { + membershipCache = mCache; + } else { + membershipCache = new MembershipCache(session, groupsPath, hasMemberSplitSize()); + } + + NodeResolver nr; + try { + nr = new IndexNodeResolver(session, session); + } catch (RepositoryException e) { + log.debug("UserManager: no QueryManager available for workspace '" + session.getWorkspace().getName() + "' -> Use traversing node resolver."); + nr = new TraversingNodeResolver(session, session); + } + authResolver = nr; + authResolver.setSearchRoots(usersPath, groupsPath); + } + + /** + * Implementation specific methods revealing where users are created within + * the content. + * + * @return root path for user content. + * @see #PARAM_USERS_PATH For the corresponding configuration parameter. + */ + public String getUsersPath() { + return usersPath; + } + + /** + * Implementation specific methods revealing where groups are created within + * the content. + * + * @return root path for group content. + * @see #PARAM_GROUPS_PATH For the corresponding configuration parameter. + */ + public String getGroupsPath() { + return groupsPath; + } + + /** + * @return The membership cache present with this user manager instance. + */ + public MembershipCache getMembershipCache() { + return membershipCache; + } + + /** + * Maximum number of properties on the group membership node structure under + * {@link UserConstants#N_MEMBERS} until additional intermediate nodes are inserted. + * If 0 (default), {@link UserConstants#P_MEMBERS} is used to record group + * memberships. + * + * @return The maximum number of group members before splitting up the structure. + */ + public int getMemberSplitSize() { + int splitSize = config.getConfigValue(PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE, 0); + if (splitSize != 0 && splitSize < 4) { + log.warn("Invalid value {} for {}. Expected integer >= 4", splitSize, PARAM_GROUP_MEMBERSHIP_SPLIT_SIZE); + splitSize = 0; + } + return splitSize; + } + + /** + * Returns true if the split-member configuration parameter + * is greater or equal than 4 indicating that group members should be stored + * in a tree instead of a single multivalued property. + * + * @return true if group members are being stored in a tree instead of a + * single multivalued property. + */ + public boolean hasMemberSplitSize() { + return getMemberSplitSize() >= 4; + } + + /** + * Set the authorizable actions that will be invoked upon authorizable + * creation and removal. + * + * @param authorizableActions An array of authorizable actions. + */ + public void setAuthorizableActions(AuthorizableAction[] authorizableActions) { + config.setAuthorizableActions(authorizableActions); + } + + //--------------------------------------------------------< UserManager >--- + /** + * @see UserManager#getAuthorizable(String) + */ + public Authorizable getAuthorizable(String id) throws RepositoryException { + if (id == null || id.length() == 0) { + throw new IllegalArgumentException("Invalid authorizable name '" + id + "'"); + } + Authorizable a = internalGetAuthorizable(id); + /** + * Extra check for the existence of the administrator user that must + * always exist. + * In case it got removed if must be recreated using a system session. + * Since a regular session may lack read permission on the admin-user's + * node an explicit test for the current editing session being + * a system session is performed. + */ + if (a == null && adminId.equals(id) && session.isSystem()) { + log.info("Admin user does not exist."); + a = createAdmin(); + } + + return a; + } + + /** + * @see UserManager#getAuthorizable(String, Class) + */ + public T getAuthorizable(String id, Class authorizableClass) throws AuthorizableTypeException, RepositoryException { + Authorizable authorizable = getAuthorizable(id); + if (authorizable == null) { + return null; + } else { + if (authorizableClass != null && authorizableClass.isInstance(authorizable)) { + return authorizableClass.cast(authorizable); + } else { + throw new AuthorizableTypeException("Invalid authorizable type for authorizable '" + id + "'"); + } + } + } + + /** + * @see UserManager#getAuthorizable(Principal) + */ + public Authorizable getAuthorizable(Principal principal) throws RepositoryException { + NodeImpl n = null; + // shortcuts that avoids executing a query. + if (principal instanceof AuthorizableImpl.NodeBasedPrincipal) { + NodeId nodeId = ((AuthorizableImpl.NodeBasedPrincipal) principal).getNodeId(); + try { + n = session.getNodeById(nodeId); + } catch (ItemNotFoundException e) { + // no such authorizable -> null + } + } else if (principal instanceof ItemBasedPrincipal) { + String authPath = ((ItemBasedPrincipal) principal).getPath(); + if (session.nodeExists(authPath)) { + n = (NodeImpl) session.getNode(authPath); + } + } else { + // another Principal implementation. + // a) try short-cut that works in case of ID.equals(principalName) only. + // b) execute query in case of pName mismatch or exception. however, query + // requires persisted user nodes (see known issue of UserImporter). + String name = principal.getName(); + try { + Authorizable a = internalGetAuthorizable(name); + if (a != null && name.equals(a.getPrincipal().getName())) { + return a; + } + } catch (RepositoryException e) { + // ignore and execute the query. + } + // authorizable whose ID matched the principal name -> search. + n = (NodeImpl) authResolver.findNode(P_PRINCIPAL_NAME, name, NT_REP_AUTHORIZABLE); + } + // build the corresponding authorizable object + return getAuthorizable(n); + } + + /** + * Always throws UnsupportedRepositoryOperationException since + * this implementation of the user management API does not allow to retrieve + * the path of an authorizable. + * + * @see UserManager#getAuthorizableByPath(String) + */ + public Authorizable getAuthorizableByPath(String path) throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @see UserManager#findAuthorizables(String,String) + */ + public Iterator findAuthorizables(String relPath, String value) throws RepositoryException { + return findAuthorizables(relPath, value, SEARCH_TYPE_AUTHORIZABLE); + } + + /** + * @see UserManager#findAuthorizables(String,String, int) + */ + public Iterator findAuthorizables(String relPath, String value, int searchType) + throws RepositoryException { + if (searchType < SEARCH_TYPE_USER || searchType > SEARCH_TYPE_AUTHORIZABLE) { + throw new IllegalArgumentException("Invalid search type " + searchType); + } + + Path path = session.getQPath(relPath); + NodeIterator nodes; + if (relPath.indexOf('/') == -1) { + // search for properties somewhere below an authorizable node + nodes = authResolver.findNodes(path, value, searchType, true, Long.MAX_VALUE); + } else { + path = path.getNormalizedPath(); + if (path.getLength() == 1) { + // only search below the authorizable node + Name ntName; + switch (searchType) { + case SEARCH_TYPE_GROUP: + ntName = NT_REP_GROUP; + break; + case SEARCH_TYPE_USER: + ntName = NT_REP_USER; + break; + default: + ntName = NT_REP_AUTHORIZABLE; + } + nodes = authResolver.findNodes(path.getName(), value, ntName, true); + } else { + // search below authorizable nodes but take some path constraints + // into account. + nodes = authResolver.findNodes(path, value, searchType, true, Long.MAX_VALUE); + } + } + return new AuthorizableIterator(nodes); + } + + /** + * @see UserManager#findAuthorizables(Query) + */ + public Iterator findAuthorizables(Query query) throws RepositoryException { + XPathQueryBuilder builder = new XPathQueryBuilder(); + query.build(builder); + return new XPathQueryEvaluator(builder, this, session).eval(); + } + + /** + * @see UserManager#createUser(String,String) + */ + public User createUser(String userID, String password) throws RepositoryException { + return createUser(userID, password, new PrincipalImpl(userID), null); + } + + /** + * @see UserManager#createUser(String, String, java.security.Principal, String) + */ + public User createUser(String userID, String password, + Principal principal, String intermediatePath) + throws AuthorizableExistsException, RepositoryException { + checkValidID(userID); + + // NOTE: password validation during setPassword and onCreate. + // NOTE: principal validation during setPrincipal call. + + try { + NodeImpl userNode = (NodeImpl) nodeCreator.createUserNode(userID, intermediatePath); + setPrincipal(userNode, principal); + setPassword(userNode, password, true); + + User user = createUser(userNode); + onCreate(user, password); + if (isAutoSave()) { + session.save(); + } + + log.debug("User created: " + userID + "; " + userNode.getPath()); + return user; + } catch (RepositoryException e) { + // something went wrong -> revert changes and re-throw + session.refresh(false); + log.debug("Failed to create new User, reverting changes."); + throw e; + } + } + + public User createSystemUser(String userID, String intermediatePath) throws AuthorizableExistsException, RepositoryException { + throw new UnsupportedRepositoryOperationException("Not yet implemented."); + } + + /** + * @see UserManager#createGroup(String) + */ + public Group createGroup(String groupID) throws AuthorizableExistsException, RepositoryException { + return createGroup(groupID, new PrincipalImpl(groupID), null); + } + + /** + * Same as {@link #createGroup(java.security.Principal, String)} where the + * intermediate path is null. + * @see UserManager#createGroup(Principal) + */ + public Group createGroup(Principal principal) throws RepositoryException { + return createGroup(principal, null); + } + + /** + * Same as {@link #createGroup(String, Principal, String)} where a groupID + * is generated from the principal name. If the name conflicts with an + * existing authorizable ID (may happen in cases where + * principal name != ID) the principal name is expanded by a suffix; + * otherwise the resulting group ID equals the principal name. + * + * @param principal A principal that doesn't yet represent an existing user + * or group. + * @param intermediatePath Is always ignored. + * @return A new group. + * @throws AuthorizableExistsException + * @throws RepositoryException + * @see UserManager#createGroup(java.security.Principal, String) + */ + public Group createGroup(Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException { + checkValidPrincipal(principal, true); + + String groupID = getGroupId(principal.getName()); + return createGroup(groupID, principal, intermediatePath); + } + + /** + * Create a new Group from the given groupID and + * principal. It will be created below the defined + * {@link #getGroupsPath() group path}.
    + * Non-existent elements of the Path will be created as nodes + * of type {@link #NT_REP_AUTHORIZABLE_FOLDER rep:AuthorizableFolder}. + * + * @param groupID A groupID that hasn't been used before for another + * user or group. + * @param principal A principal that doesn't yet represent an existing user + * or group. + * @param intermediatePath Is always ignored. + * @return A new group. + * @throws AuthorizableExistsException + * @throws RepositoryException + * @see UserManager#createGroup(String, java.security.Principal, String) + */ + public Group createGroup(String groupID, Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException { + checkValidID(groupID); + // NOTE: principal validation during setPrincipal call. + try { + NodeImpl groupNode = (NodeImpl) nodeCreator.createGroupNode(groupID, intermediatePath); + + if (principal != null) { + setPrincipal(groupNode, principal); + } + + Group group = createGroup(groupNode); + onCreate(group); + if (isAutoSave()) { + session.save(); + } + + log.debug("Group created: " + groupID + "; " + groupNode.getPath()); + return group; + } catch (RepositoryException e) { + session.refresh(false); + log.debug("newInstance new Group failed, revert changes on parent"); + throw e; + } + } + + /** + * Always returns true as by default the autoSave behavior + * cannot be altered (see also {@link #autoSave(boolean)}. + * + * @return Always true. + * @see org.apache.jackrabbit.api.security.user.UserManager#isAutoSave() + */ + public boolean isAutoSave() { + return true; + } + + /** + * Always throws unsupportedRepositoryOperationException as + * modification of the autosave behavior is not supported. + * + * @see UserManager#autoSave(boolean) + */ + public void autoSave(boolean enable) throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException("Cannot change autosave behavior."); + } + + //-------------------------------------------------------------------------- + /** + * + * @param node The new user/group node. + * @param principal A valid non-null principal. + * @throws AuthorizableExistsException If there is already another user/group + * with the same principal name. + * @throws RepositoryException If another error occurs. + */ + void setPrincipal(NodeImpl node, Principal principal) throws AuthorizableExistsException, RepositoryException { + checkValidPrincipal(principal, node.isNodeType(NT_REP_GROUP)); + /* + Check if there is *another* authorizable with the same principal. + The additional validation (nodes not be same) is required in order to + circumvent problems with re-importing existing authorizable in which + case the original user/group node is being recreated but the search + used to look for an colliding authorizable still finds the persisted + node. + */ + Authorizable existing = getAuthorizable(principal); + if (existing != null && !((AuthorizableImpl) existing).getNode().isSame(node)) { + throw new AuthorizableExistsException("Authorizable for '" + principal.getName() + "' already exists: "); + } + if (!node.isNew() || node.hasProperty(P_PRINCIPAL_NAME)) { + throw new RepositoryException("rep:principalName can only be set once on a new node."); + } + setProperty(node, P_PRINCIPAL_NAME, getValue(principal.getName()), true); + } + + /** + * Generate a password value from the specified string and set the + * {@link UserConstants#P_PASSWORD} property to the given user node. + * + * @param userNode A user node. + * @param password The password value. + * @param forceHash If true the specified password string will + * always be hashed; otherwise the hash will only be generated if it appears + * to be a {@link PasswordUtility#isPlainTextPassword(String) plain text} password. + * @throws RepositoryException If an exception occurs. + */ + void setPassword(NodeImpl userNode, String password, boolean forceHash) throws RepositoryException { + if (password == null) { + if (userNode.isNew()) { + // allow creation of system-only users with 'null' passwords that cannot login + return; + } else { + throw new IllegalArgumentException("Password may not be null."); + } + } + String pwHash; + if (forceHash || PasswordUtility.isPlainTextPassword(password)) { + try { + String algorithm = config.getConfigValue(PARAM_PASSWORD_HASH_ALGORITHM, PasswordUtility.DEFAULT_ALGORITHM); + int iterations = config.getConfigValue(PARAM_PASSWORD_HASH_ITERATIONS, PasswordUtility.DEFAULT_ITERATIONS); + pwHash = PasswordUtility.buildPasswordHash(password, algorithm, PasswordUtility.DEFAULT_SALT_SIZE, iterations); + } catch (NoSuchAlgorithmException e) { + throw new RepositoryException(e); + } + } else { + pwHash = password; + } + setProperty(userNode, P_PASSWORD, getValue(pwHash), userNode.isNew()); + } + + void setProtectedProperty(NodeImpl node, Name propName, Value value) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException { + setProperty(node, propName, value); + if (isAutoSave()) { + node.save(); + } + } + + void setProtectedProperty(NodeImpl node, Name propName, Value[] values) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException { + setProperty(node, propName, values); + if (isAutoSave()) { + node.save(); + } + } + + void setProtectedProperty(NodeImpl node, Name propName, Value[] values, int type) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException { + setProperty(node, propName, values, type); + if (isAutoSave()) { + node.save(); + } + } + + void removeProtectedItem(ItemImpl item, Node parent) throws RepositoryException, AccessDeniedException, VersionException { + removeItem(item); + if (isAutoSave()) { + parent.save(); + } + } + + NodeImpl addProtectedNode(NodeImpl parent, Name name, Name ntName) throws RepositoryException { + NodeImpl n = addNode(parent, name, ntName); + if (isAutoSave()) { + parent.save(); + } + return n; + } + + T performProtectedOperation(SessionImpl session, SessionOperation operation) throws RepositoryException { + return performProtected(session, operation); + } + + /** + * Implementation specific method used to retrieve a user/group by Node. + * Null is returned if + *
    +     * - the passed node is null,
    +     * - doesn't have the correct node type or
    +     * - isn't placed underneath the configured user/group tree.
    +     * 
    + * + * @param n A user/group node. + * @return An authorizable or null. + * @throws RepositoryException If an error occurs. + */ + Authorizable getAuthorizable(NodeImpl n) throws RepositoryException { + Authorizable authorz = null; + if (n != null) { + String path = n.getPath(); + if (n.isNodeType(NT_REP_USER)) { + if (Text.isDescendant(usersPath, path)) { + authorz = createUser(n); + } else { + /* user node outside of configured tree -> return null */ + log.error("User node '" + path + "' outside of configured user tree ('" + usersPath + "') -> Not a valid user."); + } + } else if (n.isNodeType(NT_REP_GROUP)) { + if (Text.isDescendant(groupsPath, path)) { + authorz = createGroup(n); + } else { + /* group node outside of configured tree -> return null */ + log.error("Group node '" + path + "' outside of configured group tree ('" + groupsPath + "') -> Not a valid group."); + } + } else { + /* else some other node type -> return null. */ + log.warn("Unexpected user/group node type " + n.getPrimaryNodeType().getName()); + } + } /* else no matching node -> return null */ + return authorz; + } + + /** + * Always throws UnsupportedRepositoryOperationException since + * the node may reside in a different workspace than the editing Session. + */ + String getPath(Node authorizableNode) throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * Returns the session associated with this user manager. + * + * @return the session. + */ + SessionImpl getSession() { + return session; + } + + /** + * Test if a user or group exists that has the given principals name as ID, + * which might happen if userID != principal-name. + * In this case: generate another ID for the group to be created. + * + * @param principalName to be used as hint for the group id. + * @return a group id. + * @throws RepositoryException If an error occurs. + */ + private String getGroupId(String principalName) throws RepositoryException { + String groupID = principalName; + int i = 0; + while (internalGetAuthorizable(groupID) != null) { + groupID = principalName + "_" + i; + i++; + } + return groupID; + } + + /** + * @param id The user or group ID. + * @return The authorizable with the given id or null. + * @throws RepositoryException If an error occurs. + */ + private Authorizable internalGetAuthorizable(String id) throws RepositoryException { + NodeId nodeId = buildNodeId(id); + NodeImpl n = null; + try { + n = session.getNodeById(nodeId); + } catch (ItemNotFoundException e) { + boolean compatibleJR16 = config.getConfigValue(PARAM_COMPATIBLE_JR16, false); + if (compatibleJR16) { + // backwards-compatibility with JR < 2.0 user/group structure that doesn't + // allow to determine existence of an authorizable from the id directly. + // search for it the node belonging to that id + n = (NodeImpl) authResolver.findNode(P_USERID, id, NT_REP_USER); + if (n == null) { + // no user -> look for group. + // NOTE: JR < 2.0 always returned groupIDs that didn't contain any + // illegal JCR chars. Since Group.getID() 'unescapes' the node + // name additional escaping is required. + Name nodeName = session.getQName(Text.escapeIllegalJcrChars(id)); + n = (NodeImpl) authResolver.findNode(nodeName, NT_REP_GROUP); + } + } // else: no matching node found -> ignore exception. + } + + return getAuthorizable(n); + } + + private Value getValue(String strValue) { + return session.getValueFactory().createValue(strValue); + } + + /** + * @param userID A userID. + * @return true if the given userID belongs to the administrator user. + */ + boolean isAdminId(String userID) { + return (adminId != null) && adminId.equals(userID); + } + + /** + * Build the User object from the given user node. + * + * @param userNode The new user node. + * @return An instance of User. + * @throws RepositoryException If the node isn't a child of the configured + * usersPath-node or if another error occurs. + */ + User createUser(NodeImpl userNode) throws RepositoryException { + if (userNode == null || !userNode.isNodeType(NT_REP_USER)) { + throw new IllegalArgumentException(); + } + if (!Text.isDescendant(usersPath, userNode.getPath())) { + throw new RepositoryException("User has to be within the User Path"); + } + return doCreateUser(userNode); + } + + /** + * Build the user object from the given user node. May be overridden to + * return a custom implementation. + * + * @param node user node + * @return the user object + * @throws RepositoryException if an error occurs + */ + protected User doCreateUser(NodeImpl node) throws RepositoryException { + return new UserImpl(node, this); + } + + + /** + * Build the Group object from the given group node. + * + * @param groupNode The new group node. + * @return An instance of Group. + * @throws RepositoryException If the node isn't a child of the configured + * groupsPath-node or if another error occurs. + */ + Group createGroup(NodeImpl groupNode) throws RepositoryException { + if (groupNode == null || !groupNode.isNodeType(NT_REP_GROUP)) { + throw new IllegalArgumentException(); + } + if (!Text.isDescendant(groupsPath, groupNode.getPath())) { + throw new RepositoryException("Group has to be within the Group Path"); + } + return doCreateGroup(groupNode); + } + + /** + * Build the group object from the given group node. May be overridden to + * return a custom implementation. + * + * @param node group node + * @return A group + * @throws RepositoryException if an error occurs + */ + protected Group doCreateGroup(NodeImpl node) throws RepositoryException { + return new GroupImpl(node, this); + } + + /** + * Create the administrator user. If the node to be created collides + * with an existing node (ItemExistsException) the existing node gets removed + * and the admin user node is (re)created. + *

    + * Collision with an existing node may occur under the following circumstances: + * + *

      + *
    • The usersPath has been modified in the user manager + * configuration after a successful repository start that already created + * the administrator user.
    • + *
    • The NodeId created by {@link #buildNodeId(String)} by coincidence + * collides with another NodeId created during the regular node creation + * process.
    • + *
    + * + * @return The admin user. + * @throws RepositoryException If an error occurs. + */ + private User createAdmin() throws RepositoryException { + User admin; + try { + admin = createUser(adminId, adminId); + if (!isAutoSave()) { + session.save(); + } + log.info("... created admin user with id \'" + adminId + "\' and default pw."); + } catch (ItemExistsException e) { + NodeImpl conflictingNode = session.getNodeById(buildNodeId(adminId)); + String conflictPath = conflictingNode.getPath(); + log.error("Detected conflicting node " + conflictPath + " of node type " + conflictingNode.getPrimaryNodeType().getName() + "."); + + // TODO move conflicting node of type rep:User instead of removing and recreating. + conflictingNode.remove(); + log.info("Removed conflicting node at " + conflictPath); + + admin = createUser(adminId, adminId); + if (!isAutoSave()) { + session.save(); + } + log.info("Resolved conflict and (re)created admin user with id \'" + adminId + "\' and default pw."); + } + return admin; + } + + /** + * Creates a UUID from the given id String that is converted + * to lower case before. + * + * @param id The user/group id that needs to be converted to a valid NodeId. + * @return a new NodeId. + */ + private NodeId buildNodeId(String id) { + UUID uuid = UUID.nameUUIDFromBytes(id.toLowerCase().getBytes(StandardCharsets.UTF_8)); + return new NodeId(uuid); + } + + /** + * Checks if the specified id is a non-empty string and not yet + * in use for another user or group. + * + * @param id The id of the user or group to be created. + * @throws IllegalArgumentException If the specified id is null or empty string. + * @throws AuthorizableExistsException If the id is already in use. + * @throws RepositoryException If another error occurs. + */ + private void checkValidID(String id) throws IllegalArgumentException, AuthorizableExistsException, RepositoryException { + if (id == null || id.length() == 0) { + throw new IllegalArgumentException("Cannot create authorizable: ID can neither be null nor empty String."); + } + if (internalGetAuthorizable(id) != null) { + throw new AuthorizableExistsException("User or Group for '" + id + "' already exists"); + } + } + + /** + * Throws IllegalArgumentException if the specified principal + * is null or if it's name is null or empty string. + * @param principal The principal to be validated. + * @param isGroup Flag indicating if the principal represents a group. + */ + private static 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."); + } + } + + //-------------------------------------------------------------------------- + /** + * Let the configured AuthorizableActions perform additional + * tasks associated with the creation of the new user before the + * corresponding new node is persisted. + * + * @param user The new user. + * @param pw The password. + * @throws RepositoryException If an exception occurs. + */ + void onCreate(User user, String pw) throws RepositoryException { + for (AuthorizableAction action : config.getAuthorizableActions()) { + action.onCreate(user, pw, session); + } + } + + /** + * Let the configured AuthorizableActions 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 : config.getAuthorizableActions()) { + action.onCreate(group, session); + } + } + + /** + * Let the configured AuthorizableActions 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 : config.getAuthorizableActions()) { + action.onRemove(authorizable, session); + } + } + + /** + * Let the configured AuthorizableActions 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 : config.getAuthorizableActions()) { + action.onPasswordChange(user, password, session); + } + } + + //----------------------------------------------------< SessionListener >--- + /** + * @see SessionListener#loggingOut(org.apache.jackrabbit.core.SessionImpl) + */ + public void loggingOut(SessionImpl session) { + // nothing to do. + } + + /** + * @see SessionListener#loggedOut(org.apache.jackrabbit.core.SessionImpl) + */ + public void loggedOut(SessionImpl session) { + // and logout the session unless it is the logged-out session itself. + if (session != this.session) { + this.session.logout(); + } + } + + //------------------------------------------------------< inner classes >--- + /** + * Inner class + */ + private final class AuthorizableIterator implements Iterator { + + private final Set served = new HashSet(); + + private Authorizable next; + private final NodeIterator authNodeIter; + + private AuthorizableIterator(NodeIterator authNodeIter) { + this.authNodeIter = authNodeIter; + next = seekNext(); + } + + //-------------------------------------------------------< Iterator >--- + /** + * @see Iterator#hasNext() + */ + public boolean hasNext() { + return next != null; + } + + /** + * @see Iterator#next() + */ + public Authorizable next() { + Authorizable authr = next; + if (authr == null) { + throw new NoSuchElementException(); + } + next = seekNext(); + return authr; + } + + /** + * @see Iterator#remove() + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + //---------------------------------------------------------------------- + private Authorizable seekNext() { + while (authNodeIter.hasNext()) { + NodeImpl node = (NodeImpl) authNodeIter.nextNode(); + try { + if (!served.contains(node.getUUID())) { + Authorizable authr = getAuthorizable(node); + served.add(node.getUUID()); + if (authr != null) { + return authr; + } + } + } catch (RepositoryException e) { + log.debug(e.getMessage()); + // continue seeking next authorizable + } + } + + // no next authorizable -> iteration is completed. + return null; + } + } + + //-------------------------------------------------------------------------- + /** + * Inner class creating 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 corresponding path configured.
    • + *
    • Groups are created below /rep:security/rep:authorizables/rep:groups or + * the corresponding path configured.
    • + *
    • 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 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            [nt:unstructured]
      +     *   + rep:authorizables     [rep:AuthorizableFolder]
      +     *     + rep:users           [rep:AuthorizableFolder]
      +     *       + a                 [rep:AuthorizableFolder]
      +     *         + aS              [rep:AuthorizableFolder]
      +     * ->        + aSmith        [rep:User]
      +     * 
      + *
    • + *
    • In case of a user the node name is calculated from the specified UserID + * {@link Text#escapeIllegalJcrChars(String) escaping} any illegal JCR chars. + * In case of a Group the node name is calculated from the specified principal + * name circumventing any conflicts with existing ids and escaping illegal 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 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            [nt:unstructured]
      +     *   + rep:authorizables     [rep:AuthorizableFolder]
      +     *     + rep:users           [rep:AuthorizableFolder]
      +     *       + a                 [rep:AuthorizableFolder]
      +     *         + aa              [rep:AuthorizableFolder]
      +     * ->        + a             [rep:User]
      +     * 
      + *
    • + *
    • If the autoExpandTree option is true the + * user tree will be automatically expanded using additional levels if + * autoExpandSize is exceeded within a given level.
    • + *
    + * + * The auto-expansion of the authorizable tree is defined by the following + * steps and exceptional cases: + *
      + *
    • As long as autoExpandSize isn't reached authorizable + * nodes are created within the structure defined by the + * defaultDepth. (see above)
    • + *
    • If autoExpandSize is reached additional intermediate + * folders will be created.
      + * E.g. creating a user node for an ID 'aSmith1001' would result in the + * following structure: + *
      +     * + rep:security            [nt:unstructured]
      +     *   + rep:authorizables     [rep:AuthorizableFolder]
      +     *     + rep:users           [rep:AuthorizableFolder]
      +     *       + a                 [rep:AuthorizableFolder]
      +     *         + aS              [rep:AuthorizableFolder]
      +     *           + aSmith1       [rep:User]
      +     *           + aSmith2       [rep:User]
      +     *           [...]
      +     *           + aSmith1000    [rep:User]
      +     * ->        + aSm           [rep:AuthorizableFolder]
      +     * ->          + aSmith1001  [rep:User]
      +     * 
      + *
    • + *
    • Conflicts: In order to prevent any conflicts that would arise from + * creating a authorizable node that upon later expansion could conflict + * with an authorizable folder, intermediate levels are always created if + * the node name equals any of the names reserved for the next level of + * folders.
      + * In the example above any attempt to create a user with ID 'aSm' would + * result in an intermediate level irrespective if max-size has been + * reached or not: + *
      +     * + rep:security            [nt:unstructured]
      +     *   + rep:authorizables     [rep:AuthorizableFolder]
      +     *     + rep:users           [rep:AuthorizableFolder]
      +     *       + a                 [rep:AuthorizableFolder]
      +     *         + aS              [rep:AuthorizableFolder]
      +     * ->        + aSm           [rep:AuthorizableFolder]
      +     * ->          + aSm         [rep:User]
      +     * 
      + *
    • + *
    • Special case: If the name of the authorizable node to be created is + * shorter or equal to the length of the folder at level N, the authorizable + * node is created even if max-size has been reached before.
      + * An attempt to create the users 'aS' and 'aSm' in a structure containing + * tons of 'aSmith' users will therefore result in: + *
      +     * + rep:security            [nt:unstructured]
      +     *   + rep:authorizables     [rep:AuthorizableFolder]
      +     *     + rep:users           [rep:AuthorizableFolder]
      +     *       + a                 [rep:AuthorizableFolder]
      +     *         + aS              [rep:AuthorizableFolder]
      +     *           + aSmith1       [rep:User]
      +     *           + aSmith2       [rep:User]
      +     *           [...]
      +     *           + aSmith1000    [rep:User]
      +     * ->        + aS            [rep:User]
      +     *           + aSm           [rep:AuthorizableFolder]
      +     *             + aSmith1001  [rep:User]
      +     * ->          + aSm         [rep:User]
      +     * 
      + *
    • + *
    • Special case: If autoExpandTree is enabled later on + * AND any of the existing authorizable nodes collides with an intermediate + * folder to be created the auto-expansion is aborted and the new + * authorizable is inserted at the last valid level irrespective of + * max-size being reached. + *
    • + *
    + * + * The configuration options: + *
      + *
    • defaultDepth:
      + * A positive integer greater than zero defining the depth of + * the default structure that is always created.
      + * Default value: 2
    • + *
    • autoExpandTree:
      + * boolean defining if the tree gets automatically expanded + * if within a level the maximum number of child nodes is reached.
      + * Default value: false
    • + *
    • autoExpandSize:
      + * A positive long greater than zero defining the maximum + * number of child nodes that are allowed at a given level.
      + * Default value: 1000
      + * NOTE: that total number of child nodes may still be greater that + * autoExpandSize.
    • + *
    + */ + private class NodeCreator { + + private static final String DELIMITER = "/"; + private static final int DEFAULT_DEPTH = 2; + private static final long DEFAULT_SIZE = 1000; + + private final int defaultDepth; + private final boolean autoExpandTree; + // best effort max-size of authorizables per folder. there may be + // more nodes created if the editing session isn't allowed to see + // all child nodes. + private final long autoExpandSize; + + private NodeCreator(UserManagerConfig config) { + int d = DEFAULT_DEPTH; + boolean expand = false; + long size = DEFAULT_SIZE; + + if (config != null) { + d = config.getConfigValue(PARAM_DEFAULT_DEPTH, DEFAULT_DEPTH); + if (d <= 0) { + log.warn("Invalid defaultDepth '" + d + "' -> using default."); + d = DEFAULT_DEPTH; + } + expand = config.getConfigValue(PARAM_AUTO_EXPAND_TREE, false); + size = config.getConfigValue(PARAM_AUTO_EXPAND_SIZE, DEFAULT_SIZE); + if (expand && size <= 0) { + log.warn("Invalid autoExpandSize '" + size + "' -> using default."); + size = DEFAULT_SIZE; + } + } + + defaultDepth = d; + autoExpandTree = expand; + autoExpandSize = size; + } + + public Node createUserNode(String userID, String intermediatePath) throws RepositoryException { + return createAuthorizableNode(userID, false, intermediatePath); + } + + public Node createGroupNode(String groupID, String intermediatePath) throws RepositoryException { + return createAuthorizableNode(groupID, true, intermediatePath); + } + + private Node createAuthorizableNode(String id, boolean isGroup, String intermediatePath) throws RepositoryException { + String escapedId = Text.escapeIllegalJcrChars(id); + + Node folder; + // first create the default folder nodes, that are always present. + folder = createDefaultFolderNodes(id, escapedId, isGroup, intermediatePath); + // eventually create additional intermediate folders. + if (intermediatePath == null) { + // internal logic only + folder = createIntermediateFolderNodes(id, escapedId, folder); + } + + Name nodeName = session.getQName(escapedId); + Name ntName = (isGroup) ? NT_REP_GROUP : NT_REP_USER; + NodeId nid = buildNodeId(id); + + // check if there exists an colliding folder child node. + while (((NodeImpl) folder).hasNode(nodeName)) { + NodeImpl colliding = ((NodeImpl) folder).getNode(nodeName); + if (colliding.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) { + log.warn("Existing folder node collides with user/group to be created. Expanding path: " + colliding.getPath()); + folder = colliding; + } else { + // should never get here as folder creation above already + // asserts that only rep:authorizable folders exist. + // similarly collisions with existing authorizable have been + // checked. + String msg = "Failed to create authorizable with id '" + id + "' : Detected conflicting node of unexpected nodetype '" + colliding.getPrimaryNodeType().getName() + "'."; + log.error(msg); + throw new ConstraintViolationException(msg); + } + } + + // check for collision with existing node outside of the user/group tree + if (session.getItemManager().itemExists(nid)) { + String msg = "Failed to create authorizable with id '" + id + "' : Detected conflict with existing node (NodeID: " + nid + ")"; + log.error(msg); + throw new ItemExistsException(msg); + } + + // finally create the authorizable node + return addNode((NodeImpl) folder, nodeName, ntName, nid); + } + + private Node createDefaultFolderNodes(String id, String escapedId, + boolean isGroup, String intermediatePath) throws RepositoryException { + + String defaultPath = getDefaultFolderPath(id, isGroup, intermediatePath); + + // make sure users/groups are never nested and exclusively created + // under a tree of rep:AuthorizableFolder(s) starting at usersPath + // or groupsPath, respectively. ancestors of the usersPath/groupsPath + // may or may not be rep:AuthorizableFolder(s). + // therefore the shortcut Session.getNode(defaultPath) is omitted. + String[] segmts = defaultPath.split("/"); + NodeImpl folder = (NodeImpl) session.getRootNode(); + String authRoot = (isGroup) ? groupsPath : usersPath; + + for (String segment : segmts) { + if (segment.length() < 1) { + continue; + } + if (folder.hasNode(segment)) { + folder = (NodeImpl) folder.getNode(segment); + if (Text.isDescendantOrEqual(authRoot, folder.getPath()) && + !folder.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) { + throw new ConstraintViolationException("Invalid intermediate path. Must be of type rep:AuthorizableFolder."); + } + } else { + folder = addNode(folder, session.getQName(segment), NT_REP_AUTHORIZABLE_FOLDER); + } + } + + // validation check if authorizable to be created doesn't conflict. + checkAuthorizableNodeExists(escapedId, folder); + return folder; + } + + private String getDefaultFolderPath(String id, boolean isGroup, String intermediatePath) { + StringBuilder bld = new StringBuilder(); + if (isGroup) { + bld.append(groupsPath); + } else { + bld.append(usersPath); + } + + if (intermediatePath == null) { + // internal logic + StringBuilder lastSegment = new StringBuilder(defaultDepth); + int idLength = id.length(); + for (int i = 0; i < defaultDepth; i++) { + if (idLength > i) { + lastSegment.append(id.charAt(i)); + } else { + // escapedID is too short -> append the last char again + lastSegment.append(id.charAt(idLength-1)); + } + bld.append(DELIMITER).append(Text.escapeIllegalJcrChars(lastSegment.toString())); + } + } else { + // structure defined by intermediate path + if (intermediatePath.startsWith(bld.toString())) { + intermediatePath = intermediatePath.substring(bld.toString().length()); + } + if (intermediatePath.length() > 0 && !"/".equals(intermediatePath)) { + if (!intermediatePath.startsWith("/")) { + bld.append("/"); + } + bld.append(intermediatePath); + } + } + return bld.toString(); + } + + private Node createIntermediateFolderNodes(String id, String escapedId, Node folder) throws RepositoryException { + if (!autoExpandTree) { + // additional folders are never created + return folder; + } + + // additional folders needs be created if + // - the maximal size of child nodes is reached + // - if the authorizable node to be created potentially collides with + // any of the intermediate nodes. + int segmLength = defaultDepth +1; + + while (intermediateFolderNeeded(escapedId, folder)) { + String folderName = Text.escapeIllegalJcrChars(id.substring(0, segmLength)); + if (folder.hasNode(folderName)) { + NodeImpl n = (NodeImpl) folder.getNode(folderName); + // validation check: folder must be of type rep:AuthorizableFolder + // and not an authorizable node. + if (n.isNodeType(NT_REP_AUTHORIZABLE_FOLDER)) { + // expected nodetype -> no violation + folder = n; + } else if (n.isNodeType(NT_REP_AUTHORIZABLE)){ + /* + an authorizable node has been created before with the + name of the intermediate folder to be created. + this may only occur if the 'autoExpandTree' option has + been enabled later on. + Resolution: + - abort auto-expanding and create the authorizable + at the current level, ignoring that max-size is reached. + - note, that this behavior has been preferred over tmp. + removing and recreating the colliding authorizable node. + */ + log.warn("Auto-expanding aborted. An existing authorizable node '" + n.getName() +"'conflicts with intermediate folder to be created."); + break; + } else { + // should never get here: some other, unexpected node type + String msg = "Failed to create authorizable node: Detected conflict with node of unexpected nodetype '" + n.getPrimaryNodeType().getName() + "'."; + log.error(msg); + throw new ConstraintViolationException(msg); + } + } else { + // folder doesn't exist nor does another colliding child node. + folder = addNode((NodeImpl) folder, session.getQName(folderName), NT_REP_AUTHORIZABLE_FOLDER); + } + segmLength++; + } + + // final validation check if authorizable to be created doesn't conflict. + checkAuthorizableNodeExists(escapedId, folder); + return folder; + } + + private void checkAuthorizableNodeExists(String nodeName, Node folder) throws AuthorizableExistsException, RepositoryException { + if (folder.hasNode(nodeName) && + ((NodeImpl) folder.getNode(nodeName)).isNodeType(NT_REP_AUTHORIZABLE)) { + throw new AuthorizableExistsException("Unable to create Group/User: Collision with existing authorizable."); + } + } + + private boolean intermediateFolderNeeded(String nodeName, Node folder) throws RepositoryException { + // don't create additional intermediate folders for ids that are + // shorter or equally long as the folder name. In this case the + // MAX_SIZE flag is ignored. + if (nodeName.length() <= folder.getName().length()) { + return false; + } + + // test for potential (or existing) collision in which case the + // intermediate node is created irrespective of the MAX_SIZE and the + // existing number of children. + if (nodeName.length() == folder.getName().length()+1) { + // max-size may not yet be reached yet on folder but the node to + // be created potentially collides with an intermediate folder. + // e.g.: + // existing folder structure: a/ab + // authID to be created : abt + // OR + // existing collision that would result from + // existing folder structure: a/ab/abt + // authID to be create : abt + return true; + } + + // last possibility: max-size is reached. + if (folder.getNodes().getSize() >= autoExpandSize) { + return true; + } + + // no collision and no need to create an additional intermediate + // folder due to max-size reached + return false; + } + } + + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserPerWorkspaceUserManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserPerWorkspaceUserManager.java new file mode 100644 index 00000000000..000967bc051 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/UserPerWorkspaceUserManager.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.core.security.user; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import java.util.Properties; + +/** + * Derived UserManager implementation that allows to switch between autosaving + * and transient change mode. + *

    + * NOTE: This requires that the Session passed to the user manager upon creation + * is identical to the Session passed to + * {@link org.apache.jackrabbit.core.security.JackrabbitSecurityManager#getUserManager(Session)}. + * + * @see UserPerWorkspaceUserManager + */ +public class UserPerWorkspaceUserManager extends UserManagerImpl { + + private boolean autoSave = true; + + /** + * Same as UserPerWorkspaceUserManager(session, adminID, null, null). + * + * @param session + * @param adminId + * @throws RepositoryException + */ + public UserPerWorkspaceUserManager(SessionImpl session, String adminId) throws RepositoryException { + super(session, adminId); + } + + /** + * Creates a UserManager that doesn't implicitly save changes but requires + * an explicit call to {@link javax.jcr.Session#save()}. + * + * @param session + * @param adminId + * @param config + * @throws javax.jcr.RepositoryException + */ + public UserPerWorkspaceUserManager(SessionImpl session, String adminId, Properties config) throws RepositoryException { + super(session, adminId, config); + } + + /** + * Creates a UserManager that doesn't implicitly save changes but requires + * an explicit call to {@link javax.jcr.Session#save()}. + * + * @param session + * @param adminId + * @param config + * @throws javax.jcr.RepositoryException + */ + public UserPerWorkspaceUserManager(SessionImpl session, String adminId, + Properties config, MembershipCache mCache) throws RepositoryException { + super(session, adminId, config, mCache); + } + + //--------------------------------------------------------< UserManager >--- + /** + * @see org.apache.jackrabbit.api.security.user.UserManager#getAuthorizableByPath(String) + */ + @Override + public Authorizable getAuthorizableByPath(String path) throws UnsupportedRepositoryOperationException, RepositoryException { + SessionImpl session = getSession(); + if (session.nodeExists(path)) { + NodeImpl n = (NodeImpl) session.getNode(path); + return getAuthorizable(n); + } else { + return null; + } + } + + /** + * @see org.apache.jackrabbit.api.security.user.UserManager#isAutoSave() + */ + @Override + public boolean isAutoSave() { + return autoSave; + } + + /** + * @see org.apache.jackrabbit.api.security.user.UserManager#autoSave(boolean) + */ + @Override + public void autoSave(boolean enable) throws UnsupportedRepositoryOperationException, RepositoryException { + autoSave = enable; + } + + //-------------------------------------------------------------------------- + /** + * Returns the path of the specified authorizableNode. + * + * @param authorizableNode Node associated with an authorizable. + * @return The path of the node. + * @throws RepositoryException If an error occurs while retrieving the path. + */ + @Override + String getPath(Node authorizableNode) throws RepositoryException { + return authorizableNode.getPath(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/XPathQueryBuilder.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/XPathQueryBuilder.java new file mode 100644 index 00000000000..13f85f3fc36 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/XPathQueryBuilder.java @@ -0,0 +1,385 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.QueryBuilder; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class XPathQueryBuilder implements QueryBuilder { + + /** + * 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; + } + + public String getOp() { + return op; + } + } + + interface Condition { + void accept(ConditionVisitor visitor) throws RepositoryException; + } + + interface ConditionVisitor { + void visit(NodeCondition nodeCondition) throws RepositoryException; + + void visit(PropertyCondition condition) throws RepositoryException; + + void visit(ContainsCondition condition); + + void visit(ImpersonationCondition condition); + + void visit(NotCondition condition) throws RepositoryException; + + void visit(AndCondition condition) throws RepositoryException; + + void visit(OrCondition condition) throws RepositoryException; + } + + 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; + + Class getSelector() { + return selector; + } + + public String getGroupName() { + return groupName; + } + + public 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; + } + + //------------------------------------------< QueryBuilder >--- + + public void setSelector(Class selector) { + this.selector = selector; + } + + public void setScope(String groupName, boolean declaredOnly) { + this.groupName = groupName; + declaredMembersOnly = declaredOnly; + } + + public void setCondition(Condition condition) { + this.condition = condition; + } + + public void setSortOrder(String propertyName, Direction direction, boolean ignoreCase) { + sortProperty = propertyName; + sortDirection = direction; + sortIgnoreCase = ignoreCase; + } + + public void setSortOrder(String propertyName, Direction direction) { + setSortOrder(propertyName, direction, false); + } + + public void setLimit(Value bound, long maxCount) { + offset = 0; // Unset any previously set offset + this.bound = bound; + this.maxCount = maxCount; + } + + public void setLimit(long offset, long maxCount) { + bound = null; // Unset any previously set bound + this.offset = offset; + this.maxCount = maxCount; + } + + public Condition property(String relPath, RelationOp op, Value value) { + return new PropertyCondition(relPath, op, value); + } + + public Condition nameMatches(String pattern) { + return new NodeCondition(pattern); + } + + public Condition neq(String relPath, Value value) { + return new PropertyCondition(relPath, RelationOp.NE, value); + } + + public Condition eq(String relPath, Value value) { + return new PropertyCondition(relPath, RelationOp.EQ, value); + } + + public Condition lt(String relPath, Value value) { + return new PropertyCondition(relPath, RelationOp.LT, value); + } + + public Condition le(String relPath, Value value) { + return new PropertyCondition(relPath, RelationOp.LE, value); + } + + public Condition gt(String relPath, Value value) { + return new PropertyCondition(relPath, RelationOp.GT, value); + } + + public Condition ge(String relPath, Value value) { + return new PropertyCondition(relPath, RelationOp.GE, value); + } + + public Condition exists(String relPath) { + return new PropertyCondition(relPath, RelationOp.EX); + } + + public Condition like(String relPath, String pattern) { + return new PropertyCondition(relPath, RelationOp.LIKE, pattern); + } + + public Condition contains(String relPath, String searchExpr) { + return new ContainsCondition(relPath, searchExpr); + } + + public Condition impersonates(String name) { + return new ImpersonationCondition(name); + } + + public Condition not(Condition condition) { + return new NotCondition(condition); + } + + public Condition and(Condition condition1, Condition condition2) { + return new AndCondition(condition1, condition2); + } + + public Condition or(Condition condition1, Condition condition2) { + return new OrCondition(condition1, condition2); + } + + //------------------------------------------< private >--- + + static class NodeCondition implements Condition { + private final String pattern; + + public NodeCondition(String pattern) { + this.pattern = pattern; + } + + public String getPattern() { + return pattern; + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } + + static class PropertyCondition implements Condition { + private final String relPath; + private final RelationOp op; + private final Value value; + private final String pattern; + + public PropertyCondition(String relPath, RelationOp op, Value value) { + this.relPath = relPath; + this.op = op; + this.value = value; + pattern = null; + } + + public PropertyCondition(String relPath, RelationOp op, String pattern) { + this.relPath = relPath; + this.op = op; + value = null; + this.pattern = pattern; + } + + public PropertyCondition(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 ContainsCondition implements Condition { + private final String relPath; + private final String searchExpr; + + public ContainsCondition(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 ImpersonationCondition implements Condition { + private final String name; + + public ImpersonationCondition(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void accept(ConditionVisitor visitor) { + visitor.visit(this); + } + } + + static class NotCondition implements Condition { + private final Condition condition; + + public NotCondition(Condition condition) { + this.condition = condition; + } + + public Condition getCondition() { + return condition; + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } + + abstract static class CompoundCondition implements Condition, Iterable { + private final List conditions = new ArrayList(); + + public CompoundCondition() { + super(); + } + + public CompoundCondition(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 AndCondition extends CompoundCondition { + public AndCondition(Condition condition1, Condition condition2) { + super(condition1, condition2); + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } + + static class OrCondition extends CompoundCondition { + public OrCondition(Condition condition1, Condition condition2) { + super(condition1, condition2); + } + + public void accept(ConditionVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/XPathQueryEvaluator.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/XPathQueryEvaluator.java new file mode 100644 index 00000000000..6b724006939 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/XPathQueryEvaluator.java @@ -0,0 +1,357 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.core.security.user; + +import java.util.Iterator; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.QueryBuilder.Direction; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.user.XPathQueryBuilder.Condition; +import org.apache.jackrabbit.core.security.user.XPathQueryBuilder.RelationOp; +import org.apache.jackrabbit.spi.commons.iterator.BoundedIterator; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; +import org.apache.jackrabbit.spi.commons.iterator.Predicate; +import org.apache.jackrabbit.spi.commons.iterator.Predicates; +import org.apache.jackrabbit.spi.commons.iterator.Transformer; +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 XPathQueryBuilder.ConditionVisitor { + static final Logger log = LoggerFactory.getLogger(XPathQueryEvaluator.class); + + private final XPathQueryBuilder builder; + private final UserManagerImpl userManager; + private final SessionImpl session; + private final StringBuilder xPath = new StringBuilder(); + + public XPathQueryEvaluator(XPathQueryBuilder builder, UserManagerImpl userManager, SessionImpl session) { + this.builder = builder; + this.userManager = userManager; + this.session = session; + } + + public Iterator eval() throws RepositoryException { + xPath.append("//element(*,") + .append(getNtName(builder.getSelector())) + .append(')'); + + Value bound = builder.getBound(); + long offset = builder.getOffset(); + if (bound != null && offset > 0) { + log.warn("Found bound {} and offset {} in limit. Discarding offset.", bound, offset); + offset = 0; + } + + Condition condition = builder.getCondition(); + String sortCol = builder.getSortProperty(); + 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()); + } + + QueryManager queryManager = session.getWorkspace().getQueryManager(); + Query query = queryManager.createQuery(xPath.toString(), Query.XPATH); + long maxCount = builder.getMaxCount(); + if (maxCount == 0) { + return Iterators.empty(); + } + + // 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 + if (builder.getGroupName() == null) { + if (offset > 0) { + query.setOffset(offset); + } + if (maxCount > 0) { + query.setLimit(maxCount); + } + return toAuthorizables(execute(query)); + } else { + Iterator result = toAuthorizables(execute(query)); + Iterator filtered = filter(result, builder.getGroupName(), builder.isDeclaredMembersOnly()); + return BoundedIterator.create(offset, maxCount, filtered); + } + } + + //------------------------------------------< ConditionVisitor >--- + + public void visit(XPathQueryBuilder.NodeCondition condition) throws RepositoryException { + String repPrincipal = session.getJCRName(UserConstants.P_PRINCIPAL_NAME); + + xPath.append('(') + .append("jcr:like(@") + .append(escapeForQuery(repPrincipal)) + .append(",'") + .append(escapeForQuery(condition.getPattern())) + .append("')") + .append(" or ") + .append("jcr:like(fn:name(),'") + .append(escapeForQuery(escape(condition.getPattern()))) + .append("')") + .append(')'); + } + + public void visit(XPathQueryBuilder.PropertyCondition condition) throws RepositoryException { + RelationOp relOp = condition.getOp(); + if (relOp == RelationOp.EX) { + xPath.append(escapeForQuery(condition.getRelPath())); + } else if (relOp == RelationOp.LIKE) { + xPath.append("jcr:like(") + .append(escapeForQuery(condition.getRelPath())) + .append(",'") + .append(escapeForQuery(condition.getPattern())) + .append("')"); + } else { + xPath.append(escapeForQuery(condition.getRelPath())) + .append(condition.getOp().getOp()) + .append(format(condition.getValue())); + } + } + + public void visit(XPathQueryBuilder.ContainsCondition condition) { + xPath.append("jcr:contains(") + .append(escapeForQuery(condition.getRelPath())) + .append(",'") + .append(escapeForQuery(condition.getSearchExpr())) + .append("')"); + } + + public void visit(XPathQueryBuilder.ImpersonationCondition condition) { + xPath.append("@rep:impersonators='") + .append(escapeForQuery(condition.getName())) + .append('\''); + } + + public void visit(XPathQueryBuilder.NotCondition condition) throws RepositoryException { + xPath.append("not("); + condition.getCondition().accept(this); + xPath.append(')'); + } + + public void visit(XPathQueryBuilder.AndCondition condition) throws RepositoryException { + int count = 0; + for (Condition c : condition) { + xPath.append(count++ > 0 ? " and " : ""); + c.accept(this); + } + } + + public void visit(XPathQueryBuilder.OrCondition 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 string for matching in jcr escaped node names + * + * @param string string to escape + * @return escaped string + */ + 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(); + } + + public 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(); + } + + private String getNtName(Class selector) throws RepositoryException { + if (User.class.isAssignableFrom(selector)) { + return session.getJCRName(UserConstants.NT_REP_USER); + } else if (Group.class.isAssignableFrom(selector)) { + return session.getJCRName(UserConstants.NT_REP_GROUP); + } else { + return session.getJCRName(UserConstants.NT_REP_AUTHORIZABLE); + } + } + + 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"); + } + } + + private static RelationOp getCollation(Direction direction) throws RepositoryException { + switch (direction) { + case ASCENDING: + return RelationOp.GT; + + case DESCENDING: + return RelationOp.LT; + + default: + throw new RepositoryException("Unknown sort order " + direction); + } + } + + @SuppressWarnings("unchecked") + private static Iterator execute(Query query) throws RepositoryException { + return query.execute().getNodes(); + } + + private Iterator toAuthorizables(Iterator nodes) { + Transformer transformer = new Transformer() { + public Authorizable transform(Node node) { + try { + return userManager.getAuthorizable((NodeImpl) node); + } catch (RepositoryException e) { + log.warn("Cannot create authorizable from node {}", node); + log.debug(e.getMessage(), e); + return null; + } + } + }; + + return Iterators.transformIterator(nodes, transformer); + } + + private Iterator filter(Iterator authorizables, String groupName, + boolean declaredMembersOnly) throws RepositoryException { + + Predicate predicate; + Authorizable groupAuth = userManager.getAuthorizable(groupName); + if (groupAuth == null || !groupAuth.isGroup()) { + predicate = Predicates.FALSE(); + } else { + final Group group = (Group) groupAuth; + if (declaredMembersOnly) { + predicate = new Predicate() { + public boolean evaluate(Authorizable authorizable) { + try { + return authorizable != null && group.isDeclaredMember(authorizable); + } catch (RepositoryException e) { + log.warn("Cannot determine whether {} is member of group {}", authorizable, group); + log.debug(e.getMessage(), e); + return false; + } + } + }; + + } else { + predicate = new Predicate() { + public boolean evaluate(Authorizable authorizable) { + try { + return authorizable != null && group.isMember(authorizable); + } catch (RepositoryException e) { + log.warn("Cannot determine whether {} is member of group {}", authorizable, group); + log.debug(e.getMessage(), e); + return false; + } + } + }; + } + } + + return Iterators.filterIterator(authorizables, predicate); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AbstractAuthorizableAction.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AbstractAuthorizableAction.java new file mode 100644 index 00000000000..38f769e05c0 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AbstractAuthorizableAction.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.core.security.user.action; + +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.util.Text; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract implementation of the 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, javax.jcr.Session) + */ + public void onCreate(Group group, Session session) throws RepositoryException { + // nothing to do + + } + + /** + * Doesn't perform any action. + * + * @see AuthorizableAction#onCreate(org.apache.jackrabbit.api.security.user.User, String, javax.jcr.Session) + */ + public void onCreate(User user, String password, Session session) throws RepositoryException { + // nothing to do + } + + /** + * Doesn't perform any action. + * + * @see AuthorizableAction#onRemove(org.apache.jackrabbit.api.security.user.Authorizable, javax.jcr.Session) + */ + public void onRemove(Authorizable authorizable, Session session) throws RepositoryException { + // nothing to do + } + + /** + * Doesn't perform any action. + * + * @see AuthorizableAction#onPasswordChange(org.apache.jackrabbit.api.security.user.User, String, javax.jcr.Session) + */ + public void onPasswordChange(User user, String newPassword, Session session) throws RepositoryException { + // nothing to do + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AccessControlAction.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AccessControlAction.java new file mode 100644 index 00000000000..6b9a0b4173f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AccessControlAction.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user.action; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +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.core.security.principal.UnknownPrincipal; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +/** + * The 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: + *

    + *    <UserManager class="org.apache.jackrabbit.core.security.user.UserPerWorkspaceUserManager">
    + *       <AuthorizableAction class="org.apache.jackrabbit.core.security.user.action.AccessControlAction">
    + *          <param name="groupPrivilegeNames" value="jcr:read"/>
    + *          <param name="userPrivilegeNames" value="jcr:read, rep:write"/>
    + *       </AuthorizableAction>
    + *    </UserManager>
    + * 
    + *

    + * The example configuration will lead to the following content structure upon + * user or group creation:: + * + *

    + *     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]; + + /** + * Create a new instance. + */ + public AccessControlAction() {} + + //-------------------------------------------------< AuthorizableAction >--- + /** + * @see AuthorizableAction#onCreate(org.apache.jackrabbit.api.security.user.Group, javax.jcr.Session) + */ + @Override + public void onCreate(Group group, Session session) throws RepositoryException { + setAC(group, session); + } + + /** + * @see AuthorizableAction#onCreate(org.apache.jackrabbit.api.security.user.User, String, javax.jcr.Session) + */ + @Override + public void onCreate(User user, String password, Session session) throws RepositoryException { + setAC(user, session); + } + + //--------------------------------------------------------< Bean Config >--- + + /** + * 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, Session session) throws RepositoryException { + 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 = new UnknownPrincipal(authorizable.getPrincipal().getName()); + 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 + * @param acMgr + * @return Array of 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; + } + + /** + * + * @param configParam + * @return + */ + 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()]); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AuthorizableAction.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AuthorizableAction.java new file mode 100644 index 00000000000..417073d18a1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/AuthorizableAction.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.core.security.user.action; + +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 javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * The AuthorizableAction interface provide an implementation + * specific way to execute additional validation or write tasks upon + * + *
      + *
    • {@link #onCreate(org.apache.jackrabbit.api.security.user.User, String, javax.jcr.Session) User creation},
    • + *
    • {@link #onCreate(org.apache.jackrabbit.api.security.user.Group, javax.jcr.Session) Group creation},
    • + *
    • {@link #onRemove(org.apache.jackrabbit.api.security.user.Authorizable, javax.jcr.Session) Authorizable removal} and
    • + *
    • {@link #onPasswordChange(org.apache.jackrabbit.api.security.user.User, String, javax.jcr.Session) User password modification}.
    • + *
    + * + * The actions are attached to a given UserManager instance upon creation + * by calling {@link org.apache.jackrabbit.core.security.user.UserManagerImpl#setAuthorizableActions(AuthorizableAction[])}. + * + * @see org.apache.jackrabbit.core.config.UserManagerConfig + */ +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 Session.save call. + * + * @param group The new group that has not yet been persisted; + * e.g. the associated node is still 'NEW'. + * @param session The editing session associated with the user manager. + * @throws RepositoryException If an error occurs. + */ + void onCreate(Group group, Session session) 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 Session.save call. + * + * @param user The new user that has not yet been persisted; + * e.g. the associated node is still 'NEW'. + * @param password The password that was specified upon user creation. + * @param session The editing session associated with the user manager. + * @throws RepositoryException If an error occurs. + */ + void onCreate(User user, String password, Session session) throws RepositoryException; + + /** + * Allows to add application specific behavior associated with the removal + * of an authorizable. Note, that this method is called before + * {@link Authorizable#remove} is executed (and persisted); thus the + * target authorizable still exists. + * + * @param authorizable The authorizable to be removed. + * @param session The editing session associated with the user manager. + * @throws RepositoryException If an error occurs. + */ + void onRemove(Authorizable authorizable, Session session) 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 User#changePassword} + * @param session The editing session associated with the user manager. + * @throws RepositoryException If an exception or error occurs. + */ + void onPasswordChange(User user, String newPassword, Session session) throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/ClearMembershipAction.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/ClearMembershipAction.java new file mode 100644 index 00000000000..bef801578af --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/ClearMembershipAction.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.core.security.user.action; + +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.Iterator; + +/** + * ClearMembershipAction... + */ +public class ClearMembershipAction extends AbstractAuthorizableAction { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ClearMembershipAction.class); + + /** + * Create a new instance. + */ + public ClearMembershipAction() {} + + //-------------------------------------------------< AuthorizableAction >--- + /** + * @see AuthorizableAction#onRemove(org.apache.jackrabbit.api.security.user.Authorizable, javax.jcr.Session) + */ + @Override + public void onRemove(Authorizable authorizable, Session session) throws RepositoryException { + Iterator membership = authorizable.declaredMemberOf(); + while (membership.hasNext()) { + membership.next().removeMember(authorizable); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.java new file mode 100644 index 00000000000..54a936a9ab3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/user/action/PasswordValidationAction.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.core.security.user.action; + +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.core.security.user.PasswordUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.ConstraintViolationException; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * PasswordValidationAction provides a simple password validation + * mechanism with the following configurable option: + * + *
      + *
    • constraint: a regular expression that can be compiled + * to a {@link Pattern} defining validation rules for a password.
    • + *
    + *

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

    + * Example configuration: + *

    + *    <UserManager class="org.apache.jackrabbit.core.security.user.UserPerWorkspaceUserManager">
    + *       <AuthorizableAction class="org.apache.jackrabbit.core.security.user.action.PasswordValidationAction">
    + *          <!--
    + *          password length must be at least 8 chars and it must contain at least
    + *          one upper and one lowercase ASCII character.
    + *          -->
    + *          <param name="constraint" value="^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z]).*"/>
    + *       </AuthorizableAction>
    + *    </UserManager>
    + * 
    + * + * @see org.apache.jackrabbit.api.security.user.UserManager#createUser(String, String) + * @see User#changePassword(String) + * @see 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, Session session) throws RepositoryException { + validatePassword(password, false); // don't force validation of hashed passwords. + } + + @Override + public void onPasswordChange(User user, String newPassword, Session session) throws RepositoryException { + validatePassword(newPassword, true); // force validation of all passwords + } + + //---------------------------------------------------------< BeanConfig >--- + /** + * 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() + ")."); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/AddNodeOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/AddNodeOperation.java new file mode 100644 index 00000000000..4e5b5e9daf2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/AddNodeOperation.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.core.session; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; + +/** + * Session operation for adding a new node. + */ +public class AddNodeOperation implements SessionWriteOperation { + + private final NodeImpl node; + + private final String relPath; + + private final String nodeTypeName; + + private final String uuid; + + public AddNodeOperation( + NodeImpl node, String relPath, + String nodeTypeName, String uuid) { + this.node = node; + this.relPath = relPath; + this.nodeTypeName = nodeTypeName; + this.uuid = uuid; + } + + public Node perform(SessionContext context) throws RepositoryException { + ItemManager itemMgr = context.getItemManager(); + + // Get the canonical path of the new node + Path path; + try { + path = PathFactoryImpl.getInstance().create( + node.getPrimaryPath(), context.getQPath(relPath), true); + } catch (NameException e) { + throw new RepositoryException( + "Failed to resolve path " + relPath + + " relative to " + node, e); + } + + // Check that the last path element is a simple name + if (!path.denotesName() || path.getIndex() != Path.INDEX_UNDEFINED) { + throw new RepositoryException( + "Invalid last path element for adding node " + + relPath + " relative to " + node); + } + + // Get the parent node instance + NodeImpl parentNode; + Path parentPath = path.getAncestor(1); + try { + parentNode = itemMgr.getNode(parentPath); + } catch (PathNotFoundException e) { + if (itemMgr.propertyExists(parentPath)) { + throw new ConstraintViolationException( + "Unable to add a child node to property " + + context.getJCRPath(parentPath)); + } + throw e; + } catch (AccessDeniedException ade) { + throw new PathNotFoundException( + "Failed to resolve path " + relPath + + " relative to " + node); + } + + // Resolve node type name (if any) + Name typeName = null; + if (nodeTypeName != null) { + typeName = context.getQName(nodeTypeName); + } + + // Check that the given UUID (if any) does not already exist + NodeId id = null; + if (uuid != null) { + id = new NodeId(uuid); + if (itemMgr.itemExists(id)) { + throw new ItemExistsException( + "A node with this UUID already exists: " + uuid); + } + } + + return parentNode.addNode(path.getName(), typeName, id); + } + + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "node.addNode(" + relPath + ", " + nodeTypeName + ", " + uuid + ")"; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/NodeNameNormalizer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/NodeNameNormalizer.java new file mode 100644 index 00000000000..7ed36577ce5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/NodeNameNormalizer.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.core.session; + +import java.text.Normalizer; +import java.text.Normalizer.Form; + +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Convenience class for checking/logging "weird" node names. + *

    + * For now, it only checks that a node name being created uses NFC + * + * @see http://www.unicode.org/reports/tr15/tr15-23.html + */ +public class NodeNameNormalizer { + + private static Logger log = LoggerFactory.getLogger(NodeNameNormalizer.class); + + public static void check(Name name) { + if (log.isDebugEnabled()) { + String lname = name.getLocalName(); + String normalized = Normalizer.normalize(lname, Form.NFC); + if (!lname.equals(normalized)) { + String message = "The new node name '" + dump(lname) + "' is not in Unicode NFC form ('" + dump(normalized) + "')."; + log.debug(message, new Exception("Call chain")); + } + } + } + + private static String dump(String lname) { + StringBuilder sb = new StringBuilder(); + for (char c : lname.toCharArray()) { + if (c > ' ' && c < 127) { + sb.append(c); + } else { + sb.append(String.format("\\u%04x", (int) c)); + } + } + return sb.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionContext.java new file mode 100644 index 00000000000..11f27989014 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionContext.java @@ -0,0 +1,400 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.session; + +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.ItemValidator; +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.WorkspaceImpl; +import org.apache.jackrabbit.core.config.WorkspaceConfig; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.observation.ObservationManagerImpl; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl; +import org.apache.jackrabbit.core.state.SessionItemStateManager; +import org.apache.jackrabbit.core.value.ValueFactoryImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.AccessDeniedException; +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFactory; + +/** + * Component context of a session. This class keeps track of the internal + * components associated with a session. + */ +public class SessionContext implements NamePathResolver { + + /** + * The repository context of this session. + */ + private final RepositoryContext repositoryContext; + + /** + * This session. + */ + private final SessionImpl session; + + /** + * The state of this session. + */ + private final SessionState state; + + /** + * The value factory of this session + */ + private final ValueFactory valueFactory; + + /** + * The item validator of this session + */ + private final ItemValidator itemValidator; + + /** + * Node type manager of this session + */ + private final NodeTypeManagerImpl nodeTypeManager; + + /** + * Privilege manager of this session. + */ + private final PrivilegeManagerImpl privilegeManager; + + /** + * The namespace registry exposed for this session context that includes + * permission checks. + */ + private final NamespaceRegistry nsRegistry; + + /** + * The workspace of this session + */ + private final WorkspaceImpl workspace; + + /** + * The item state manager of this session + */ + private volatile SessionItemStateManager itemStateManager; + + /** + * The item manager of this session + */ + private volatile ItemManager itemManager; + + /** + * The access manager of this session + */ + private volatile AccessManager accessManager; + + /** + * The observation manager of this session. + */ + private volatile ObservationManagerImpl observationManager; + + /** + * Creates a component context for the given session. + * + * @param repositoryContext repository context of the session + * @param session the session + * @param workspaceConfig workspace configuration + * @throws RepositoryException if the workspace can not be accessed + */ + public SessionContext( + RepositoryContext repositoryContext, SessionImpl session, + WorkspaceConfig workspaceConfig) throws RepositoryException { + assert repositoryContext != null; + assert session != null; + this.repositoryContext = repositoryContext; + this.session = session; + this.state = new SessionState(this); + this.valueFactory = + new ValueFactoryImpl(session, repositoryContext.getDataStore()); + this.itemValidator = new ItemValidator(this); + this.nodeTypeManager = new NodeTypeManagerImpl(this); + this.privilegeManager = new PrivilegeManagerImpl(repositoryContext.getPrivilegeRegistry(), session); + this.nsRegistry = new PermissionAwareNamespaceRegistry(); + this.workspace = new WorkspaceImpl(this, workspaceConfig); + } + + //-------------------------------------------< per-repository components > + + /** + * Returns the repository context of the session. + * + * @return repository context + */ + public RepositoryContext getRepositoryContext() { + return repositoryContext; + } + + /** + * Returns this repository. + * + * @return repository + */ + public RepositoryImpl getRepository() { + return repositoryContext.getRepository(); + } + + /** + * Returns the root node identifier of the repository. + * + * @return root node identifier + */ + public NodeId getRootNodeId() { + return repositoryContext.getRootNodeId(); + } + + /** + * Returns the data store of this repository, or null + * if a data store is not configured. + * + * @return data store, or null + */ + public DataStore getDataStore() { + return repositoryContext.getDataStore(); + } + + /** + * Returns the node type registry of this repository. + * + * @return node type registry + */ + public NodeTypeRegistry getNodeTypeRegistry() { + return repositoryContext.getNodeTypeRegistry(); + } + + //----------------------------------------------< per-session components > + + /** + * Returns this session. + * + * @return session + */ + public SessionImpl getSessionImpl() { + return session; + } + + /** + * Returns the state of this session. + * + * @return session state + */ + public SessionState getSessionState() { + return state; + } + + /** + * Returns the value factory of this session. + * + * @return value factory + */ + public ValueFactory getValueFactory() { + return valueFactory; + } + + /** + * Returns the item validator of this session. + * + * @return item validator + */ + public ItemValidator getItemValidator() { + return itemValidator; + } + + /** + * Returns the node type manager of this session. + * + * @return node type manager + */ + public NodeTypeManagerImpl getNodeTypeManager() { + return nodeTypeManager; + } + + /** + * Returns the privilege manager of this session. + * + * @return the privilege manager. + */ + public PrivilegeManagerImpl getPrivilegeManager() { + return privilegeManager; + } + + /** + * Returns a namespace registry instance which asserts that the editing + * session is allowed to modify the namespace registry. + * + * @return + */ + public NamespaceRegistry getNamespaceRegistry() { + return nsRegistry; + } + + /** + * Returns the workspace of this session. + * + * @return workspace + */ + public WorkspaceImpl getWorkspace() { + return workspace; + } + + public SessionItemStateManager getItemStateManager() { + assert itemStateManager != null; + return itemStateManager; + } + + public void setItemStateManager(SessionItemStateManager itemStateManager) { + assert itemStateManager != null; + this.itemStateManager = itemStateManager; + } + + public HierarchyManager getHierarchyManager() { + assert itemStateManager != null; + return itemStateManager.getHierarchyMgr(); + } + + public ItemManager getItemManager() { + assert itemManager != null; + return itemManager; + } + + public void setItemManager(ItemManager itemManager) { + assert itemManager != null; + this.itemManager = itemManager; + } + + public AccessManager getAccessManager() { + assert accessManager != null; + return accessManager; + } + + public void setAccessManager(AccessManager accessManager) { + assert accessManager != null; + this.accessManager = accessManager; + } + + public ObservationManagerImpl getObservationManager() { + assert observationManager != null; + return observationManager; + } + + public void setObservationManager( + ObservationManagerImpl observationManager) { + assert observationManager != null; + this.observationManager = observationManager; + } + + public NodeIdFactory getNodeIdFactory() { + return repositoryContext.getNodeIdFactory(); + } + + //--------------------------------------------------------< NameResolver > + + public Name getQName(String name) + throws IllegalNameException, NamespaceException { + return session.getQName(name); + } + + public String getJCRName(Name name) throws NamespaceException { + return session.getJCRName(name); + } + + //--------------------------------------------------------< PathResolver > + + public Path getQPath(String path) + throws MalformedPathException, IllegalNameException, + NamespaceException { + return session.getQPath(path); + } + + public Path getQPath(String path, boolean normalizeIdentifier) + throws MalformedPathException, IllegalNameException, + NamespaceException { + return session.getQPath(path, normalizeIdentifier); + } + + public String getJCRPath(Path path) throws NamespaceException { + return session.getJCRPath(path); + } + + //--------------------------------------------------------------< Object > + + /** + * Dumps the session internals to a string. + * + * @return string representation of session internals + */ + @Override + public String toString() { + return session + ":\n" + itemManager + "\n" + itemStateManager; + } + + //-------------------------------------------------------------------------- + /** + * Permission aware namespace registry implementation that makes sure that + * modifications of the namespace registry are only allowed if the editing + * session has the corresponding permissions. + */ + private class PermissionAwareNamespaceRegistry implements NamespaceRegistry { + + private final NamespaceRegistry nsRegistry = repositoryContext.getNamespaceRegistry(); + + public void registerNamespace(String prefix, String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + session.getAccessManager().checkRepositoryPermission(Permission.NAMESPACE_MNGMT); + nsRegistry.registerNamespace(prefix, uri); + } + + public void unregisterNamespace(String prefix) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + session.getAccessManager().checkRepositoryPermission(Permission.NAMESPACE_MNGMT); + nsRegistry.unregisterNamespace(prefix); + } + + public String[] getPrefixes() throws RepositoryException { + return nsRegistry.getPrefixes(); + } + + public String[] getURIs() throws RepositoryException { + return nsRegistry.getURIs(); + } + + public String getURI(String prefix) throws NamespaceException, RepositoryException { + return nsRegistry.getURI(prefix); + } + + public String getPrefix(String uri) throws NamespaceException, RepositoryException { + return nsRegistry.getPrefix(uri); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionItemOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionItemOperation.java new file mode 100644 index 00000000000..ae70db44650 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionItemOperation.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.session; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.ItemImpl; +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; + +/** + * Session operation for accessing an item at a given path. See the static + * methods for factories of different kinds of item operations. + */ +public abstract class SessionItemOperation implements SessionOperation { + + //----------------------------------------------< static factory methods > + + /** + * Creates a session operation for checking the existence of an item + * at the given path. + * + * @param path absolute path of the item + * @return session operation + */ + public static SessionItemOperation itemExists(String path) { + return new SessionItemOperation("itemExists", path) { + @Override @SuppressWarnings("deprecation") + protected Boolean perform(ItemManager manager, Path path) { + return manager.itemExists(path); + } + }; + } + + /** + * Creates a session operation for checking the existence of a property + * at the given path. + * + * @param path absolute path of the property + * @return session operation + */ + public static SessionItemOperation propertyExists(String path) { + return new SessionItemOperation("propertyExists", path) { + @Override + protected Boolean perform(ItemManager manager, Path path) { + return manager.propertyExists(path); + } + }; + } + + /** + * Creates a session operation for checking the existence of a node + * at the given path. + * + * @param path absolute path of the node + * @return session operation + */ + public static SessionItemOperation nodeExists(String path) { + return new SessionItemOperation("nodeExists", path) { + @Override + protected Boolean perform(ItemManager manager, Path path) { + return manager.nodeExists(path); + } + }; + } + + /** + * Creates a session operation for getting the item at the given path. + * + * @param path absolute path of the item + * @return session operation + */ + public static SessionItemOperation getItem(String path) { + return new SessionItemOperation("getItem", path) { + @Override @SuppressWarnings("deprecation") + protected ItemImpl perform(ItemManager manager, Path path) + throws RepositoryException { + return manager.getItem(path); + } + }; + } + + /** + * Creates a session operation for getting the property at the given path. + * + * @param path absolute path of the property + * @return session operation + */ + public static SessionItemOperation getProperty(String path) { + return new SessionItemOperation("getProperty", path) { + @Override + protected PropertyImpl perform(ItemManager manager, Path path) + throws RepositoryException { + return manager.getProperty(path); + } + }; + } + + /** + * Creates a session operation for getting the node at the given path. + * + * @param path absolute path of the node + * @return session operation + */ + public static SessionItemOperation getNode(String path) { + return new SessionItemOperation("getNode", path) { + @Override + protected NodeImpl perform(ItemManager manager, Path path) + throws RepositoryException { + return manager.getNode(path); + } + }; + } + + /** + * Creates a session operation for removing the item at the given path. + * + * @param path absolute path of the item + * @return session operation + */ + public static SessionItemOperation remove(String path) { + return new SessionItemOperation("remove", path) { + @Override @SuppressWarnings("deprecation") + protected Object perform(ItemManager manager, Path path) + throws RepositoryException { + manager.getItem(path).remove(); + return this; + } + }; + } + + //------------------------------------------------< SessionItemOperation > + + /** + * The method being executed (itemExists/getItem/remove/etc.) + */ + private final String method; + + /** + * Absolute path of the item that this operation accesses. + */ + private final String path; + + /** + * Creates a new operation for a accessing the item at the given path. + * + * @param method method being executed + * @param path absolute path of the item + */ + private SessionItemOperation(String method, String path) { + this.method = method; + this.path = path; + } + + /** + * Performs this operation on the specified item. This method resolves + * the given absolute path and calls the abstract + * {@link #perform(ItemManager, Path)} method to actually perform the + * selected operation. + * + * @throws RepositoryException if the operation fails + */ + public T perform(SessionContext context) throws RepositoryException { + try { + Path normalized = + context.getQPath(path).getNormalizedPath(); + if (normalized.isAbsolute()) { + return perform(context.getItemManager(), normalized); + } else { + throw new RepositoryException("Not an absolute path: " + path); + } + } catch (AccessDeniedException e) { + throw new PathNotFoundException(path); + } catch (NameException e) { + throw new RepositoryException("Invalid path:" + path, e); + } + } + + /** + * Performs this operation using the given item manager. + * + * @param manager item manager of this session + * @param path resolved path of the item + * @throws RepositoryException if the operation fails + */ + protected abstract T perform(ItemManager manager, Path path) + throws RepositoryException; + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + * + * @return "getItem(/path/to/item)", etc. + */ + public String toString() { + return method + "(" + path + ")"; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionOperation.java new file mode 100644 index 00000000000..c82210c69fe --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionOperation.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.core.session; + +import javax.jcr.RepositoryException; + +/** + * Session operation. Used by the {@link SessionState} class to implement + * generic controls like synchronization and liveness checks on all session + * operation. + */ +public interface SessionOperation { + + /** + * Performs the session operation. + * + * @param context component context of this session + * @throws RepositoryException if the operation fails + */ + T perform(SessionContext context) throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionRefreshOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionRefreshOperation.java new file mode 100644 index 00000000000..ad22ca9a330 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionRefreshOperation.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.core.session; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.cluster.ClusterException; +import org.apache.jackrabbit.core.cluster.ClusterNode; + +/** + * Operation to refresh the state of a session. + */ +public class SessionRefreshOperation implements SessionOperation { + + /** + * Flag to keep transient changes. + */ + private final boolean keepChanges; + + /** + * Flag to synchronise with other cluster nodes. + */ + private final boolean clusterSync; + + /** + * Creates a session refresh operation. + * + * @param keepChanges whether to keep transient changes + * @param clusterSync whether to synchronise with other cluster nodes + */ + public SessionRefreshOperation(boolean keepChanges, boolean clusterSync) { + this.keepChanges = keepChanges; + this.clusterSync = clusterSync; + } + + /** + * Refreshes the session. + */ + public Object perform(SessionContext context) throws RepositoryException { + // JCR-1753: Ensure that we are up to date with cluster changes + ClusterNode cluster = context.getRepositoryContext().getClusterNode(); + if (cluster != null && clusterSync) { + try { + cluster.sync(); + } catch (ClusterException e) { + throw new RepositoryException( + "Unable to synchronize with the cluster", e); + } + } + + if (!keepChanges) { + context.getItemStateManager().disposeAllTransientItemStates(); + } else { + // FIXME should reset Item#status field to STATUS_NORMAL + // of all non-transient instances; maybe also + // have to reset stale ItemState instances + } + return this; + } + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "session.refresh(" + keepChanges + ")"; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionSaveOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionSaveOperation.java new file mode 100644 index 00000000000..c7ebf1e24ca --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionSaveOperation.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.core.session; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.ItemImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Operation to persist transient changes in a session. + */ +public class SessionSaveOperation implements SessionWriteOperation { + + private static final Logger LOG = LoggerFactory.getLogger(SessionSaveOperation.class); + private static final boolean LOG_WITH_STACKTRACE = Boolean.getBoolean("org.jackrabbit.logWithStackTrace"); + + /** + * Persists transient changes by delegating to the save() method of the + * root node (or the parent of transient changes if access to the root + * node is not available to this session). + */ + public Object perform(SessionContext context) throws RepositoryException { + NodeId id; + // JCR-2425: check whether session is allowed to read root node + if (context.getSessionImpl().hasPermission("/", Session.ACTION_READ)) { + id = context.getRootNodeId(); + } else { + id = context.getItemStateManager().getIdOfRootTransientNodeState(); + } + if (LOG.isDebugEnabled()) { + String path; + try { + NodeId transientRoot = context.getItemStateManager().getIdOfRootTransientNodeState(); + ItemImpl item = context.getItemManager().getItem(transientRoot); + path = item.getPath(); + } catch (Exception e) { + LOG.warn("Could not get the path", e); + path = "?"; + } + if (LOG_WITH_STACKTRACE) { + LOG.debug("Saving changes under " + path, new Exception()); + } else { + LOG.debug("Saving changes under " + path); + } + } + if (id != null) { + context.getItemManager().getItem(id).save(); + } + return this; + } + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this operation. + */ + public String toString() { + return "session.save()"; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionState.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionState.java new file mode 100644 index 00000000000..d296368c91e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionState.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.session; + +import static org.apache.jackrabbit.api.stats.RepositoryStatistics.Type; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.WorkspaceManager; +import org.apache.jackrabbit.core.observation.ObservationDispatcher; +import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Internal session state. This class keeps track of the lifecycle of + * a session and controls concurrent access to the session internals. + *

    + * The session lifecycle is pretty simple: there are only two lifecycle + * states, "alive" and "closed", and only one possible state transition, + * from "alive" to "closed". + *

    + * Concurrent access to session internals is controlled by the + * {@link #perform(SessionOperation)} method that guarantees that no two + * {@link SessionOperation operations} are performed concurrently on the + * same session. + * + * @see JCR-890 + */ +public class SessionState { + + /** + * Logger instance. + */ + private static final Logger log = + LoggerFactory.getLogger(SessionState.class); + + /** + * Component context of this session. + */ + private final SessionContext context; + + /** + * Counter of read operations. + */ + private final AtomicLong readCounter; + + /** + * Counter of write operations. + */ + private final AtomicLong writeCounter; + + /** + * Duration of read operations. + */ + private final AtomicLong readDuration; + + /** + * Duration of write operations. + */ + private final AtomicLong writeDuration; + + /** + * Number of open sessions. + */ + private final AtomicLong sessionCount; + + /** + * The lock used to guarantee synchronized execution of repository + * operations. An explicit lock is used instead of normal Java + * synchronization in order to be able to log attempts to concurrently + * use a session. + */ + private final Lock lock = new ReentrantLock(); + + /** + * Flag to indicate that the current operation is a write operation. + * Used to detect concurrent writes. + */ + private volatile boolean isWriteOperation = false; + + /** + * Flag to indicate a closed session. When null, the session + * is still alive. And when the session is closed, this reference is set + * to an exception that contains the stack trace of where the session was + * closed. The stack trace is used as extra information in possible + * warning logs caused by clients attempting to access a closed session. + */ + private volatile Exception closed = null; + + /** + * Creates a state instance for a session. + * + * @param context component context of this session + */ + public SessionState(SessionContext context) { + this.context = context; + + RepositoryStatisticsImpl statistics = + context.getRepositoryContext().getRepositoryStatistics(); + this.readCounter = statistics.getCounter(Type.SESSION_READ_COUNTER); + this.writeCounter = statistics.getCounter(Type.SESSION_WRITE_COUNTER); + this.readDuration = statistics.getCounter(Type.SESSION_READ_DURATION); + this.writeDuration = statistics.getCounter(Type.SESSION_WRITE_DURATION); + this.sessionCount = statistics.getCounter(Type.SESSION_COUNT); + statistics.getCounter(Type.SESSION_LOGIN_COUNTER).incrementAndGet(); + sessionCount.incrementAndGet(); + } + + /** + * Checks whether this session is alive. This method should generally + * only be called from within a performed {@link SessionOperation}, as + * otherwise there's no guarantee against another thread closing the + * session right after this method has returned. + * + * @see Session#isLive() + * @return true if the session is alive, + * false otherwise + */ + public boolean isAlive() { + return closed == null; + } + + /** + * Throws an exception if this session is not alive. + * + * @throws RepositoryException throw if this session is not alive + */ + public void checkAlive() throws RepositoryException { + if (!isAlive()) { + throw new RepositoryException( + "This session has been closed. See the chained exception" + + " for a trace of where the session was closed.", closed); + } + } + + /** + * Performs the given operation within a synchronized block. Special care + * is made to detect attempts to access the session concurrently and to + * log detailed warnings in such cases. + * + * @param operation session operation + * @return the return value of the executed operation + * @throws RepositoryException if the operation fails or + * if the session has already been closed + */ + public T perform(SessionOperation operation) + throws RepositoryException { + String session = context.getSessionImpl().toString(); + + // Acquire the exclusive lock for accessing session internals. + // No other session should be holding the lock, so we log a + // message to let the user know of such cases. + if (!lock.tryLock()) { + if (isWriteOperation + && operation instanceof SessionWriteOperation) { + Exception trace = new Exception( + "Stack trace of concurrent access to " + session); + log.warn("Attempt to perform " + operation + + " while another thread is concurrently writing" + + " to " + session + ". Blocking until the other" + + " thread is finished using this session. Please" + + " review your code to avoid concurrent use of" + + " a session.", trace); + } else if (log.isDebugEnabled()) { + Exception trace = new Exception( + "Stack trace of concurrent access to " + session); + log.debug("Attempt to perform " + operation + " while" + + " another thread is concurrently reading from " + + session + ". Blocking until the other thread" + + " is finished using this session. Please" + + " review your code to avoid concurrent use of" + + " a session.", trace); + } + lock.lock(); + } + + boolean isOutermostWriteOperation = false; + try { + // Check that the session is still alive + checkAlive(); + + // Raise the isWriteOperation flag for write operations. + // The flag is used to set the appropriate log level above. + boolean wasWriteOperation = isWriteOperation; + if (!wasWriteOperation + && operation instanceof SessionWriteOperation) { + isWriteOperation = true; + isOutermostWriteOperation = true; + } + + try { + // Perform the actual operation + log.debug("Performing {}", operation); + long start = System.nanoTime(); + try { + return operation.perform(context); + } finally { + long time = System.nanoTime() - start; + + // JCR-3040: Increment the operation counters + if (isWriteOperation) { + writeCounter.incrementAndGet(); + writeDuration.addAndGet(time); + } else { + readCounter.incrementAndGet(); + readDuration.addAndGet(time); + } + + log.debug("Performed {} in {}ns", operation, time); + } + } finally { + isWriteOperation = wasWriteOperation; + } + } finally { + lock.unlock(); + + // Delay return from a write operation if the observation queue + // is being overloaded. This needs to be done after releasing + // the (outermost) write locks to prevent potential deadlocks. + // See https://issues.apache.org/jira/browse/JCR-2746 + if (isOutermostWriteOperation) { + WorkspaceManager manager = + context.getRepositoryContext().getWorkspaceManager(); + ObservationDispatcher dispatcher = + manager.getObservationDispatcher(context.getWorkspace().getName()); + dispatcher.delayIfEventQueueOverloaded(); + } + } + } + + /** + * Closes this session. Special care is made to detect attempts to + * access the session concurrently or to close it more than once, and to + * log detailed warnings in such cases. + * + * @return true if the session was closed, or + * false if the session had already been closed + */ + public boolean close() { + String session = context.getSessionImpl().toString(); + + if (!lock.tryLock()) { + Exception trace = new Exception( + "Stack trace of concurrent access to " + session); + log.warn("Attempt to close " + session + " while another" + + " thread is concurrently accessing this session." + + " Blocking until the other thread is finished" + + " using this session. Please review your code" + + " to avoid concurrent use of a session.", trace); + lock.lock(); + } + try { + if (isAlive()) { + sessionCount.decrementAndGet(); + closed = new Exception( + "Stack trace of where " + session + + " was originally closed"); + return true; + } else { + Exception trace = new Exception( + "Stack trace of the duplicate attempt to close " + + session); + log.warn("Attempt to close " + session + " after it has" + + " already been closed. Please review your code" + + " for proper session management.", trace); + log.warn(session + " has already been closed. See the" + + " attached exception for a trace of where this" + + " session was closed.", closed); + return false; + } + } finally { + lock.unlock(); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionWriteOperation.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionWriteOperation.java new file mode 100644 index 00000000000..b7278403d64 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/session/SessionWriteOperation.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.session; + +/** + * Marker interface that marks a {@link SessionOperation} that modifies + * the state of the session or the repository. + */ +public interface SessionWriteOperation extends SessionOperation { +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChangeLog.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChangeLog.java new file mode 100644 index 00000000000..f4c81076a68 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChangeLog.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.core.state; + +import java.util.Map; + +import org.apache.commons.collections.map.LinkedMap; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.version.VersionItemStateManager; + +/** + * Registers changes made to states and references and consolidates + * empty changes. + */ +public class ChangeLog { + + /** + * Added states + */ + @SuppressWarnings("unchecked") + private final Map addedStates = (Map) new LinkedMap(); + + /** + * Modified states + */ + @SuppressWarnings("unchecked") + private final Map modifiedStates = (Map) new LinkedMap(); + + /** + * Deleted states + */ + @SuppressWarnings("unchecked") + private final Map deletedStates = (Map) new LinkedMap(); + + /** + * Modified references + */ + @SuppressWarnings("unchecked") + private final Map modifiedRefs = (Map) new LinkedMap(); + + private long updateSize; + + /** + * Checks whether this change log contains any changes. This method is + * used to avoid extra work on updates that contain no changes. + * + * @since Apache Jackrabbit 1.5 + * @see JCR-1813 + * @return true if this log contains at least one change, + * false otherwise + */ + public boolean hasUpdates() { + return !(addedStates.isEmpty() && modifiedStates.isEmpty() + && deletedStates.isEmpty() && modifiedRefs.isEmpty()); + } + + /** + * A state has been added + * + * @param state state that has been added + */ + public void added(ItemState state) { + addedStates.put(state.getId(), state); + } + + /** + * A state has been modified. If the state is not a new state + * (not in the collection of added ones), then disconnect + * the local state from its underlying shared state and add + * it to the modified states collection. + * + * @param state state that has been modified + */ + public void modified(ItemState state) { + if (!addedStates.containsKey(state.getId())) { + state.disconnect(); + modifiedStates.put(state.getId(), state); + } + } + + /** + * A state has been deleted. If the state is not a new state + * (not in the collection of added ones), then disconnect + * the local state from its underlying shared state, remove + * it from the modified states collection and add it to the + * deleted states collection. + * + * @param state state that has been deleted + */ + public void deleted(ItemState state) { + assert state != null; + if (addedStates.remove(state.getId()) == null) { + state.disconnect(); + modifiedStates.remove(state.getId()); + deletedStates.put(state.getId(), state); + } + } + + /** + * A references has been modified + * + * @param refs refs that has been modified + */ + public void modified(NodeReferences refs) { + modifiedRefs.put(refs.id, refs); + } + + /** + * Removes the references entry with the given target node id. + * This method is called by {@link VersionItemStateManager} to drop + * references to virtual nodes. + * + * @param targetId target node id + */ + public void removeReferencesEntry(NodeId targetId) { + modifiedRefs.remove(targetId); + } + + /** + * Return an item state given its id. Returns null + * if the item state is neither in the added nor in the modified + * section. Throws a NoSuchItemStateException if + * the item state is in the deleted section. + * + * @return item state or null + * @throws NoSuchItemStateException if the item has been deleted + */ + public ItemState get(ItemId id) throws NoSuchItemStateException { + ItemState state = addedStates.get(id); + if (state == null) { + state = modifiedStates.get(id); + if (state == null) { + if (deletedStates.containsKey(id)) { + throw new NoSuchItemStateException("State has been marked destroyed: " + id); + } + } + } + return state; + } + + /** + * Return a flag indicating whether a given item state exists. + * + * @return true if item state exists within this + * log; false otherwise + */ + public boolean has(ItemId id) { + return addedStates.containsKey(id) || modifiedStates.containsKey(id); + } + + /** + * Return a flag indicating whether a given item state is marked as + * deleted in this log. + * + * @return true if item state is marked as deleted in this + * log; false otherwise + */ + public boolean deleted(ItemId id) { + return deletedStates.containsKey(id); + } + + /** + * Return a flag indicating whether a given item state is marked as + * added in this log. + * + * @return true if item state is marked as added in this + * log; false otherwise + */ + public boolean isAdded(ItemId id) { + return addedStates.containsKey(id); + } + + /** + * Returns a flag indicating whether a given item state is marked as + * modified in this log. + * + * @param id the id of the item. + * @return true if the item state is marked as modified in this + * log; false otherwise. + */ + public boolean isModified(ItemId id) { + return modifiedStates.containsKey(id); + } + + /** + * Return a node references object given the target node id. Returns + * null if the node reference is not in the modified + * section. + * + * @return node references or null + */ + public NodeReferences getReferencesTo(NodeId id) { + return modifiedRefs.get(id); + } + + /** + * Return the added states in this change log. + * + * @return added states + */ + public Iterable addedStates() { + return addedStates.values(); + } + + /** + * Return the modified states in this change log. + *

    + * Note that this change log must not be modified while iterating + * through the returned states. + * + * @return modified states + */ + public Iterable modifiedStates() { + return modifiedStates.values(); + } + + /** + * Return the deleted states in this change log. + *

    + * Note that this change log must not be modified while iterating + * through the returned states. + * + * @return deleted states + */ + public Iterable deletedStates() { + return deletedStates.values(); + } + + /** + * Return the modified references in this change log. + *

    + * Note that this change log must not be modified while iterating + * through the returned states. + * + * @return modified references + */ + public Iterable modifiedRefs() { + return modifiedRefs.values(); + } + + /** + * Merge another change log with this change log + * + * @param other other change log + */ + public void merge(ChangeLog other) { + // Remove all states from our 'added' set that have now been deleted + for (ItemState state : other.deletedStates()) { + if (addedStates.remove(state.getId()) == null) { + deletedStates.put(state.getId(), state); + } + // also remove from possibly modified state + modifiedStates.remove(state.getId()); + } + + // only add modified states that are not already 'added' + for (ItemState state : other.modifiedStates()) { + if (!addedStates.containsKey(state.getId())) { + modifiedStates.put(state.getId(), state); + } else { + // adapt status and replace 'added' + state.setStatus(ItemState.STATUS_NEW); + addedStates.put(state.getId(), state); + } + } + + // add 'added' states + for (ItemState state : other.addedStates()) { + addedStates.put(state.getId(), state); + } + + // add refs + modifiedRefs.putAll(other.modifiedRefs); + } + + /** + * Push all states contained in the various maps of + * items we have. + */ + public void push() { + for (ItemState state : modifiedStates()) { + state.push(); + } + for (ItemState state : deletedStates()) { + state.push(); + } + for (ItemState state : addedStates()) { + state.push(); + } + } + + /** + * After the states have actually been persisted, update their + * internal states and notify listeners. + */ + public void persisted() { + for (ItemState state : modifiedStates()) { + state.setStatus(ItemState.STATUS_EXISTING); + state.notifyStateUpdated(); + } + for (ItemState state : deletedStates()) { + state.setStatus(ItemState.STATUS_EXISTING_REMOVED); + state.notifyStateDestroyed(); + state.discard(); + } + for (ItemState state : addedStates()) { + state.setStatus(ItemState.STATUS_EXISTING); + state.notifyStateCreated(); + } + } + + /** + * Reset this change log, removing all members inside the + * maps we built. + */ + public void reset() { + addedStates.clear(); + modifiedStates.clear(); + deletedStates.clear(); + modifiedRefs.clear(); + } + + /** + * Disconnect all states in the change log from their overlaid + * states. + */ + public void disconnect() { + for (ItemState state : modifiedStates()) { + state.disconnect(); + } + for (ItemState state : deletedStates()) { + state.disconnect(); + } + for (ItemState state : addedStates()) { + state.disconnect(); + } + } + + /** + * Undo changes made to items in the change log. Discards + * added items, refreshes modified and resurrects deleted + * items. + * + * @param parent parent manager that will hold current data + */ + public void undo(ItemStateManager parent) { + for (ItemState state : modifiedStates()) { + try { + state.connect(parent.getItemState(state.getId())); + state.pull(); + } catch (ItemStateException e) { + state.discard(); + } + } + for (ItemState state : deletedStates()) { + try { + state.connect(parent.getItemState(state.getId())); + state.pull(); + } catch (ItemStateException e) { + state.discard(); + } + } + for (ItemState state : addedStates()) { + state.discard(); + } + reset(); + } + + /** + * Returns the update size of the change log. + * + * @return The update size. + */ + public long getUpdateSize() { + return updateSize; + } + + /** + * Sets the update size of the change log. + * + * @param updateSize The update size. + */ + public void setUpdateSize(long updateSize) { + this.updateSize = updateSize; + } + + /** + * Returns a string representation of this change log for diagnostic + * purposes. + * + * @return a string representation of this change log + */ + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("{"); + buf.append("#addedStates=").append(addedStates.size()); + buf.append(", #modifiedStates=").append(modifiedStates.size()); + buf.append(", #deletedStates=").append(deletedStates.size()); + buf.append(", #modifiedRefs=").append(modifiedRefs.size()); + buf.append("}"); + return buf.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChildNodeEntries.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChildNodeEntries.java new file mode 100644 index 00000000000..0eeaddfbd95 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChildNodeEntries.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.core.state; + +import org.apache.commons.collections.map.LinkedMap; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.util.EmptyLinkedMap; +import org.apache.jackrabbit.spi.Name; + +import java.util.List; +import java.util.HashMap; +import java.util.Collections; +import java.util.ArrayList; +import java.util.Map; + +/** + * ChildNodeEntries represents an insertion-ordered + * collection of ChildNodeEntrys that also maintains + * the index values of same-name siblings on insertion and removal. + */ +class ChildNodeEntries implements Cloneable { + + /** + * Insertion-ordered map of entries + * (key=NodeId, value=entry) + */ + private LinkedMap entries; + + /** + * Map used for lookup by name + * (key=name, value=either a single entry or a list of sns entries) + */ + private Map nameMap; + + /** + * Indicates whether the entries and nameMap are shared with another + * ChildNodeEntries instance. + */ + private boolean shared; + + ChildNodeEntries() { + init(); + } + + ChildNodeEntry get(NodeId id) { + return (ChildNodeEntry) entries.get(id); + } + + @SuppressWarnings("unchecked") + List get(Name nodeName) { + Object obj = nameMap.get(nodeName); + if (obj == null) { + return Collections.emptyList(); + } + if (obj instanceof List) { + // map entry is a list of siblings + return Collections.unmodifiableList((List) obj); + } else { + // map entry is a single child node entry + return Collections.singletonList((ChildNodeEntry) obj); + } + } + + @SuppressWarnings("unchecked") + ChildNodeEntry get(Name nodeName, int index) { + if (index < 1) { + throw new IllegalArgumentException("index is 1-based"); + } + + Object obj = nameMap.get(nodeName); + if (obj == null) { + return null; + } + if (obj instanceof List) { + // map entry is a list of siblings + List siblings = (List) obj; + if (index <= siblings.size()) { + return siblings.get(index - 1); + } + } else { + // map entry is a single child node entry + if (index == 1) { + return (ChildNodeEntry) obj; + } + } + return null; + } + + @SuppressWarnings("unchecked") + ChildNodeEntry add(Name nodeName, NodeId id) { + ensureModifiable(); + List siblings = null; + int index = 0; + Object obj = nameMap.get(nodeName); + if (obj != null) { + if (obj instanceof List) { + // map entry is a list of siblings + siblings = (List) obj; + if (siblings.size() > 0) { + // reuse immutable Name instance from 1st same name sibling + // in order to help gc conserving memory + nodeName = siblings.get(0).getName(); + } + } else { + // map entry is a single child node entry, + // convert to siblings list + siblings = new ArrayList(); + siblings.add((ChildNodeEntry) obj); + nameMap.put(nodeName, siblings); + } + index = siblings.size(); + } + + index++; + + ChildNodeEntry entry = new ChildNodeEntry(nodeName, id, index); + if (siblings != null) { + siblings.add(entry); + } else { + nameMap.put(nodeName, entry); + } + entries.put(id, entry); + + return entry; + } + + void addAll(List entriesList) { + for (ChildNodeEntry entry : entriesList) { + // delegate to add(Name, String) to maintain consistency + add(entry.getName(), entry.getId()); + } + } + + // The index may have changed because of changes by another session. Use remove(NodeId id) + // instead + @Deprecated + @SuppressWarnings("unchecked") + public ChildNodeEntry remove(Name nodeName, int index) { + if (index < 1) { + throw new IllegalArgumentException("index is 1-based"); + } + + ensureModifiable(); + Object obj = nameMap.get(nodeName); + if (obj == null) { + return null; + } + + if (obj instanceof ChildNodeEntry) { + // map entry is a single child node entry + if (index != 1) { + return null; + } + ChildNodeEntry removedEntry = (ChildNodeEntry) obj; + nameMap.remove(nodeName); + entries.remove(removedEntry.getId()); + return removedEntry; + } + + // map entry is a list of siblings + List siblings = (List) obj; + if (index > siblings.size()) { + return null; + } + + // remove from siblings list + ChildNodeEntry removedEntry = siblings.remove(index - 1); + // remove from ordered entries map + entries.remove(removedEntry.getId()); + + // update indices of subsequent same-name siblings + for (int i = index - 1; i < siblings.size(); i++) { + ChildNodeEntry oldEntry = siblings.get(i); + ChildNodeEntry newEntry = new ChildNodeEntry(nodeName, oldEntry.getId(), oldEntry.getIndex() - 1); + // overwrite old entry with updated entry in siblings list + siblings.set(i, newEntry); + // overwrite old entry with updated entry in ordered entries map + entries.put(newEntry.getId(), newEntry); + } + + // clean up name lookup map if necessary + if (siblings.size() == 0) { + // no more entries with that name left: + // remove from name lookup map as well + nameMap.remove(nodeName); + } else if (siblings.size() == 1) { + // just one entry with that name left: + // discard siblings list and update name lookup map accordingly + nameMap.put(nodeName, siblings.get(0)); + } + + // we're done + return removedEntry; + } + + /** + * Removes the child node entry refering to the node with the given id. + * + * @param id id of node whose entry is to be removed. + * @return the removed entry or null if there is no such entry. + */ + ChildNodeEntry remove(NodeId id) { + ChildNodeEntry entry = (ChildNodeEntry) entries.get(id); + if (entry != null) { + return remove(entry.getName(), entry.getIndex()); + } + return entry; + } + + /** + * Removes the given child node entry. + * + * @param entry entry to be removed. + * @return the removed entry or null if there is no such entry. + */ + public ChildNodeEntry remove(ChildNodeEntry entry) { + return remove(entry.getId()); + } + + /** + * Removes all child node entries + */ + public void removeAll() { + init(); + } + + /** + * Returns a list of ChildNodeEntrys who do only exist in + * this but not in other. + *

    + * Note that two entries are considered identical in this context if + * they have the same name and uuid, i.e. the index is disregarded + * whereas ChildNodeEntry.equals(Object) also compares + * the index. + * + * @param other entries to be removed + * @return a new list of those entries that do only exist in + * this but not in other + */ + List removeAll(ChildNodeEntries other) { + if (entries.isEmpty()) { + return Collections.emptyList(); + } + if (other.isEmpty()) { + return list(); + } + + List result = new ArrayList(); + for (Object e : entries.values()) { + ChildNodeEntry entry = (ChildNodeEntry) e; + ChildNodeEntry otherEntry = other.get(entry.getId()); + if (entry == otherEntry) { + continue; + } + if (otherEntry == null + || !entry.getName().equals(otherEntry.getName())) { + result.add(entry); + } + } + return result; + } + + /** + * Returns a list of ChildNodeEntrys who do exist in + * this and in other. + *

    + * Note that two entries are considered identical in this context if + * they have the same name and uuid, i.e. the index is disregarded + * whereas ChildNodeEntry.equals(Object) also compares + * the index. + * + * @param other entries to be retained + * @return a new list of those entries that do exist in + * this and in other + */ + List retainAll(ChildNodeEntries other) { + if (entries.isEmpty() + || other.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList(); + for (Object e : entries.values()) { + ChildNodeEntry entry = (ChildNodeEntry) e; + ChildNodeEntry otherEntry = other.get(entry.getId()); + if (entry == otherEntry) { + result.add(entry); + } else if (otherEntry != null + && entry.getName().equals(otherEntry.getName())) { + result.add(entry); + } + } + return result; + } + + //-----------------------------------------------< unmodifiable List view > + + public boolean isEmpty() { + return entries.isEmpty(); + } + + @SuppressWarnings("unchecked") + public List list() { + return new ArrayList(entries.values()); + } + + public List getRenamedEntries(ChildNodeEntries that) { + List renamed = Collections.emptyList(); + for (Object e : entries.values()) { + ChildNodeEntry entry = (ChildNodeEntry) e; + ChildNodeEntry other = that.get(entry.getId()); + if (other != null && !entry.getName().equals(other.getName())) { + // child node entry with same id but different name exists in + // overlaid and this state => renamed entry detected + if (renamed.isEmpty()) { + renamed = new ArrayList(); + } + renamed.add(entry); + } + } + return renamed; + } + + public int size() { + return entries.size(); + } + + //-------------------------------------------< java.lang.Object overrides > + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ChildNodeEntries) { + ChildNodeEntries other = (ChildNodeEntries) obj; + return (nameMap.equals(other.nameMap) + && entries.equals(other.entries) + && shared == other.shared); + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //----------------------------------------------------< Cloneable support > + + /** + * Returns a shallow copy of this ChildNodeEntries instance; + * the entries themselves are not cloned. + * + * @return a shallow copy of this instance. + */ + protected Object clone() { + try { + ChildNodeEntries clone = (ChildNodeEntries) super.clone(); + if (nameMap != Collections.EMPTY_MAP) { + clone.shared = true; + shared = true; + } + return clone; + } catch (CloneNotSupportedException e) { + // never happens, this class is cloneable + throw new InternalError(); + } + } + + //-------------------------------------------------------------< internal > + + /** + * Initializes the name and entries map with unmodifiable empty instances. + */ + private void init() { + nameMap = Collections.emptyMap(); + entries = EmptyLinkedMap.INSTANCE; + shared = false; + } + + /** + * Ensures that the {@link #nameMap} and {@link #entries} map are + * modifiable. + */ + @SuppressWarnings("unchecked") + private void ensureModifiable() { + if (nameMap == Collections.EMPTY_MAP) { + nameMap = new HashMap(); + entries = new LinkedMap(); + } else if (shared) { + entries = (LinkedMap) entries.clone(); + nameMap = new HashMap(nameMap); + for (Map.Entry entry : nameMap.entrySet()) { + Object value = entry.getValue(); + if (value instanceof List) { + entry.setValue(new ArrayList( + (List) value)); + } + } + shared = false; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChildNodeEntry.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChildNodeEntry.java new file mode 100644 index 00000000000..bf30f420e9c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ChildNodeEntry.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.core.state; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.id.NodeId; + +/** + * ChildNodeEntry specifies the name, index (in the case of + * same-name siblings) and the UUID of a child node entry. + *

    + * ChildNodeEntry instances are immutable. + */ +public final class ChildNodeEntry { + + private int hash; + + private final Name name; + private final int index; // 1-based index for same-name siblings + private final NodeId id; + + ChildNodeEntry(Name name, NodeId id, int index) { + if (name == null) { + throw new IllegalArgumentException("name can not be null"); + } + this.name = name; + + if (id == null) { + throw new IllegalArgumentException("id can not be null"); + } + this.id = id; + + if (index < 1) { + throw new IllegalArgumentException("index is 1-based"); + } + this.index = index; + } + + public NodeId getId() { + return id; + } + + public Name getName() { + return name; + } + + public int getIndex() { + return index; + } + + //---------------------------------------< java.lang.Object overrides > + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ChildNodeEntry) { + ChildNodeEntry other = (ChildNodeEntry) obj; + return (name.equals(other.name) && id.equals(other.id) + && index == other.index); + } + return false; + } + + public String toString() { + return name.toString() + "[" + index + "] -> " + id; + } + + public int hashCode() { + // ChildNodeEntry is immutable, we can store the computed hash code value + int h = hash; + if (h == 0) { + h = 17; + h = 37 * h + name.hashCode(); + h = 37 * h + id.hashCode(); + h = 37 * h + index; + hash = h; + } + return h; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/DefaultISMLocking.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/DefaultISMLocking.java new file mode 100644 index 00000000000..20da2cddebf --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/DefaultISMLocking.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.core.state; + +import static org.apache.jackrabbit.data.core.TransactionContext.getCurrentThreadId; +import static org.apache.jackrabbit.data.core.TransactionContext.isSameThreadId; + +import org.apache.jackrabbit.core.id.ItemId; + +/** + * Default item state locking strategy. The default strategy is simply to use + * a single coarse-grained read-write lock over the entire workspace. + */ +public class DefaultISMLocking implements ISMLocking { + + /** + * The read lock instance used by readers to release the acquired lock. + */ + private final ReadLock readLock = new ReadLock() { + public void release() { + releaseReadLock(); + } + }; + + /** + * The write lock instance used by writers to release or downgrade the + * acquired lock. + */ + private final WriteLock writeLock = new WriteLock() { + public void release() { + releaseWriteLock(false); + } + public ReadLock downgrade() { + releaseWriteLock(true); + return readLock; + } + }; + + /** + * Flag for determining whether this locking strategy should give + * preference to writers or not. If writers are preferred (which + * is the default setting), then all readers will get blocked whenever + * there's a writer waiting for the lock. + */ + private boolean writerPreference = true; + + /** + * Number of writer threads waiting. While greater than zero, no new + * (unrelated) readers are allowed to proceed. + */ + private int writersWaiting = 0; + + /** + * The thread identifier of the current writer, or null if + * no write is in progress. A thread with the same identifier (i.e. the + * same thread or another thread in the same transaction) can re-acquire + * read or write locks without limitation, while all other readers and + * writers remain blocked. Note that a downgraded write lock still retains + * the writer thread identifier, which allows related threads to reacquire + * read or write locks even when there are concurrent writers waiting. + */ + private Object writerId = null; + + /** + * Number of acquired write locks. All the concurrent write locks are + * guaranteed to share the same thread identifier (see {@link #writerId}). + */ + private int writerCount = 0; + + /** + * Number of acquired read locks. + */ + private int readerCount = 0; + + /** + * Returns the writer preference status of this locking strategy. + * + * @return writer preference + */ + public boolean isWriterPreference() { + return writerPreference; + } + + /** + * Sets the writer preference status of this locking strategy. + * + * @param preference writer preference + */ + public void setWriterPreference(boolean preference) { + this.writerPreference = preference; + } + + /** + * Increments the reader count and returns the acquired read lock once + * there are no more writers or the current writer shares the thread id + * with this reader. + */ + public synchronized ReadLock acquireReadLock(ItemId id) + throws InterruptedException { + Object currentId = getCurrentThreadId(); + while (writerId != null + ? (writerCount > 0 && !isSameThreadId(writerId, currentId)) + : (writerPreference && writersWaiting > 0)) { + wait(); + } + + readerCount++; + return readLock; + } + + /** + * Decrements the reader count and notifies all pending threads if the + * lock is now available. Used by the {@link #readLock} instance. + */ + private synchronized void releaseReadLock() { + readerCount--; + if (readerCount == 0 && writerCount == 0) { + writerId = null; + notifyAll(); + } + } + + /** + * Increments the writer count, sets the writer identifier and returns + * the acquired write lock once there are no other active readers or + * writers or the current writer shares the thread id with this writer. + */ + public synchronized WriteLock acquireWriteLock(ChangeLog changeLog) + throws InterruptedException { + Object currentId = getCurrentThreadId(); + + writersWaiting++; + try { + while (writerId != null + ? !isSameThreadId(writerId, currentId) : readerCount > 0) { + wait(); + } + } finally { + writersWaiting--; + } + + if (writerCount++ == 0) { + writerId = currentId; + } + return writeLock; + } + + /** + * Decrements the writer count (and possibly clears the writer identifier) + * and notifies all pending threads if the lock is now available. If the + * downgrade argument is true, then the reader count is incremented before + * notifying any pending threads. Used by the {@link #writeLock} instance. + */ + private synchronized void releaseWriteLock(boolean downgrade) { + writerCount--; + if (downgrade) { + readerCount++; + } + if (writerCount == 0) { + if (readerCount == 0) { + writerId = null; + } + notifyAll(); + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/DummyUpdateEventChannel.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/DummyUpdateEventChannel.java new file mode 100644 index 00000000000..fc7634f4989 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/DummyUpdateEventChannel.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.core.state; + +import org.apache.jackrabbit.core.cluster.Update; +import org.apache.jackrabbit.core.cluster.UpdateEventChannel; +import org.apache.jackrabbit.core.cluster.UpdateEventListener; + +/** + * Package-private utility class used as a sentinel by the + * {@link SharedItemStateManager} class. + */ +public class DummyUpdateEventChannel implements UpdateEventChannel { + + public void updatePrepared(Update update) {} + + public void updateCreated(Update update) {} + + public void updateCommitted(Update update, String path) {} + + public void updateCancelled(Update update) {} + + public void setListener(UpdateEventListener listener) {} + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/FineGrainedISMLocking.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/FineGrainedISMLocking.java new file mode 100644 index 00000000000..746cdd43a4f --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/FineGrainedISMLocking.java @@ -0,0 +1,415 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import static org.apache.jackrabbit.data.core.TransactionContext.getCurrentThreadId; +import static org.apache.jackrabbit.data.core.TransactionContext.isSameThreadId; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; + +import EDU.oswego.cs.dl.util.concurrent.Latch; +import EDU.oswego.cs.dl.util.concurrent.ReadWriteLock; +import EDU.oswego.cs.dl.util.concurrent.Sync; +import EDU.oswego.cs.dl.util.concurrent.WriterPreferenceReadWriteLock; + +/** + * FineGrainedISMLocking... + */ +public class FineGrainedISMLocking implements ISMLocking { + + /** + * Avoid creating commonly used Integer instances. + */ + private static final Integer ONE = new Integer(1); + + /** + * An anonymous read lock without an id assigned. + */ + private final ReadLock anonymousReadLock = new ReadLockImpl(); + + /** + * The active writer or null if there is none. + */ + private WriteLockImpl activeWriter; + + private volatile Object activeWriterId; + + private ReadWriteLock writerStateRWLock = new WriterPreferenceReadWriteLock(); + + /** + * Map that contains the read locks. + */ + private final LockMap readLockMap = new LockMap(); + + /** + * Number of current readers. + */ + private final AtomicInteger readerCount = new AtomicInteger(0); + + /** + * List of waiting readers that are blocked because they conflict with + * the current writer. + */ + private List waitingReaders = + Collections.synchronizedList(new LinkedList()); + + /** + * List of waiting writers that are blocked because there is already a + * current writer or one of the current reads conflicts with the change log + * of the blocked writer. + */ + private List waitingWriters = new LinkedList(); + + /** + * {@inheritDoc} + */ + public ReadLock acquireReadLock(ItemId id) + throws InterruptedException { + if (isSameThreadId(activeWriterId, getCurrentThreadId())) { + // we hold the write lock + readerCount.incrementAndGet(); + readLockMap.addLock(id); + return new ReadLockImpl(id); + } + + // if we get here the following is true: + // - the current thread does not hold a write lock + for (;;) { + Sync signal; + // make sure writer state does not change + Sync shared = writerStateRWLock.readLock(); + shared.acquire(); + try { + if (activeWriter == null + || !hasDependency(activeWriter.changes, id)) { + readerCount.incrementAndGet(); + readLockMap.addLock(id); + return new ReadLockImpl(id); + } else { + signal = new Latch(); + waitingReaders.add(signal); + } + } finally { + shared.release(); + } + + // if we get here there was an active writer with + // a dependency to the current id. + // wait for the writer until it is done, then try again + signal.acquire(); + } + } + + /** + * {@inheritDoc} + */ + public WriteLock acquireWriteLock(ChangeLog changeLog) + throws InterruptedException { + for (;;) { + Sync signal; + // we want to become the current writer + Sync exclusive = writerStateRWLock.writeLock(); + exclusive.acquire(); + Object currentId = getCurrentThreadId(); + try { + if (activeWriter == null + && !readLockMap.hasDependency(changeLog)) { + activeWriter = new WriteLockImpl(changeLog); + activeWriterId = currentId; + return activeWriter; + } else { + if (isSameThreadId(activeWriterId, currentId) + && !readLockMap.hasDependency(changeLog)) { + return activeWriter; + } else { + signal = new Latch(); + waitingWriters.add(signal); + } + } + } finally { + exclusive.release(); + } + // if we get here there is an active writer or there is a read + // lock that conflicts with the change log + signal.acquire(); + } + } + + //----------------------------< internal >---------------------------------- + + private final class WriteLockImpl implements WriteLock { + + private final ChangeLog changes; + + WriteLockImpl(ChangeLog changes) { + this.changes = changes; + } + + public void release() { + Sync exclusive = writerStateRWLock.writeLock(); + for (;;) { + try { + exclusive.acquire(); + break; + } catch (InterruptedException e) { + // try again + Thread.interrupted(); + } + } + try { + activeWriter = null; + activeWriterId = null; + notifyWaitingReaders(); + notifyWaitingWriters(); + } finally { + exclusive.release(); + } + } + + public ReadLock downgrade() { + readerCount.incrementAndGet(); + readLockMap.addLock(null); + Sync exclusive = writerStateRWLock.writeLock(); + for (;;) { + try { + exclusive.acquire(); + break; + } catch (InterruptedException e) { + // try again + Thread.interrupted(); + } + } + try { + activeWriter = null; + // only notify waiting readers since we still hold a down + // graded lock, which is kind of exclusiv with respect to + // other writers + notifyWaitingReaders(); + } finally { + exclusive.release(); + } + return anonymousReadLock; + } + + } + + private final class ReadLockImpl implements ReadLock { + + private final ItemId id; + + public ReadLockImpl() { + this(null); + } + + ReadLockImpl(ItemId id) { + this.id = id; + } + + public void release() { + Sync shared = writerStateRWLock.readLock(); + for (;;) { + try { + shared.acquire(); + break; + } catch (InterruptedException e) { + // try again + Thread.interrupted(); + } + } + try { + readLockMap.removeLock(id); + if (readerCount.decrementAndGet() == 0 && activeWriter == null) { + activeWriterId = null; + } + if (!isSameThreadId(activeWriterId, getCurrentThreadId())) { + // only notify waiting writers if we do *not* hold a write + // lock at the same time. that would be a waste of cpu time. + notifyWaitingWriters(); + } + } finally { + shared.release(); + } + } + } + + private static boolean hasDependency(ChangeLog changeLog, ItemId id) { + try { + if (changeLog.get(id) == null) { + if (!id.denotesNode() || changeLog.getReferencesTo((NodeId) id) == null) { + // change log does not contain the item + return false; + } + } + } catch (NoSuchItemStateException e) { + // is deleted + } + return true; + } + + /** + * This method is not thread-safe and calling threads must ensure that + * only one thread calls this method at a time. + */ + private void notifyWaitingReaders() { + Iterator it = waitingReaders.iterator(); + while (it.hasNext()) { + it.next().release(); + it.remove(); + } + } + + /** + * This method may be called concurrently by multiple threads. + */ + private void notifyWaitingWriters() { + synchronized (waitingWriters) { + if (waitingWriters.isEmpty()) { + return; + } + Iterator it = waitingWriters.iterator(); + while (it.hasNext()) { + it.next().release(); + it.remove(); + } + } + } + + private static final class LockMap { + + /** + * 16 slots + */ + @SuppressWarnings("unchecked") + private final Map[] slots = new Map[0x10]; + + /** + * Flag that indicates if the entire map is locked. + */ + private volatile boolean global = false; + + public LockMap() { + for (int i = 0; i < slots.length; i++) { + slots[i] = new HashMap(); + } + } + + /** + * This method must be called while holding the reader sync of the + * {@link FineGrainedISMLocking#writerStateRWLock}! + * + * @param id the item id. + */ + public void addLock(ItemId id) { + if (id == null) { + if (global) { + throw new IllegalStateException( + "Map already globally locked"); + } + global = true; + return; + } + Map locks = slots[slotIndex(id)]; + synchronized (locks) { + Integer i = (Integer) locks.get(id); + if (i == null) { + i = ONE; + } else { + i = new Integer(i.intValue() + 1); + } + locks.put(id, i); + } + } + + /** + * This method must be called while holding the reader sync of the + * {@link FineGrainedISMLocking#writerStateRWLock}! + * + * @param id the item id. + */ + public void removeLock(ItemId id) { + if (id == null) { + if (!global) { + throw new IllegalStateException( + "Map not globally locked"); + } + global = false; + return; + } + Map locks = slots[slotIndex(id)]; + synchronized (locks) { + Integer i = (Integer) locks.get(id); + if (i != null) { + if (i.intValue() == 1) { + locks.remove(id); + } else { + locks.put(id, new Integer(i.intValue() - 1)); + } + } else { + throw new IllegalStateException( + "No lock present for id: " + id); + } + } + } + + /** + * This method must be called while holding the write sync of {@link + * FineGrainedISMLocking#writerStateRWLock} to make sure no additional + * read locks are added to or removed from the map! + * + * @param changes the change log. + * @return if the change log has a dependency to the locks currently + * present in this map. + */ + public boolean hasDependency(ChangeLog changes) { + if (global) { + // read lock present, which was downgraded from a write lock + return true; + } + for (int i = 0; i < slots.length; i++) { + Map locks = slots[i]; + synchronized (locks) { + for (ItemId id : locks.keySet()) { + if (FineGrainedISMLocking.hasDependency(changes, id)) { + return true; + } + } + } + } + return false; + } + + private static int slotIndex(ItemId id) { + NodeId nodeId; + if (id.denotesNode()) { + nodeId = (NodeId) id; + } else { + nodeId = ((PropertyId) id).getParentId(); + } + return ((int) nodeId.getLeastSignificantBits()) & 0xf; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ISMLocking.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ISMLocking.java new file mode 100644 index 00000000000..824d9296590 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ISMLocking.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.core.state; + +import org.apache.jackrabbit.core.id.ItemId; + +/** + * ISMLocking defines an interface for a locking strategy of an + * {@link ItemStateManager}. + *

    + * An implementation of ISMLocking must meet the following + * requirements: + *

      + *
    • While a read lock is held for a given item with id I an + * implementation must ensure that no write lock is issued for a change log + * that contains a reference to an item with id I.
    • + *
    • While a write lock is held for a given change log C an + * implementation must ensure that no read lock is issued for an item that is + * contained in C, unless the current thread is the owner of the + * write lock!
    • + *
    • While a write lock is held for a given change log C an + * implementation must ensure that no write lock is issued for a change log + * C' that intersects with C. That is both change + * logs contain a reference to the same item. Please note that an implementation + * is free to block requests entirely for additional write lock while a write + * lock is active. It is not a requirement to support concurrent write locks. + *
    • + *
    • While a write lock is held for a change log C, the holder + * of the write lock (and any related threads) needs to be able to acquire + * a read lock even if other writers are waiting for the lock. This behaviour + * must continue also when the write lock has been downgraded. Note that it + * is not necessary for a holder of a read lock to be able to upgrade to a + * write lock.
    • + *
    + */ +public interface ISMLocking { + + /** + * Acquire a read lock for the given item id. + * @param id an item id. + */ + ReadLock acquireReadLock(ItemId id) throws InterruptedException; + + /** + * Acquires a write lock for the given changeLog. + * + * @param changeLog the change log + * @return the write lock for the given changeLog. + * @throws InterruptedException if the thread is interrupted while creating + * the write lock. + */ + WriteLock acquireWriteLock(ChangeLog changeLog) throws InterruptedException; + + public interface ReadLock { + + /** + * Releases this lock. + */ + void release(); + + } + + public interface WriteLock { + + /** + * Releases this lock. + */ + void release(); + + /** + * Downgrades this lock into a read lock. When this method returns this + * write lock is effectively released and the returned read lock must be + * used to further release the read lock. + * + * @return the read lock downgraded from this write lock. + */ + ReadLock downgrade(); + + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ISMLockingFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ISMLockingFactory.java new file mode 100644 index 00000000000..7c089d6556d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ISMLockingFactory.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.core.state; + +import javax.jcr.RepositoryException; + +/** + * Factory interface for creating {@link ISMLocking} instances. Used + * to decouple the repository internals from the repository configuration + * mechanism. + */ +public interface ISMLockingFactory { + + /** + * Creates, initializes, and returns an {@link ISMLocking} instance + * for use by the repository. Note that no information is passed from + * the client, so all required configuration information must be + * encapsulated in the factory. + * + * @return initialized item state locking strategy + * @throws RepositoryException if the locking strategy can not be created + */ + ISMLocking getISMLocking() throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemState.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemState.java new file mode 100644 index 00000000000..e926daf95c4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemState.java @@ -0,0 +1,417 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ItemState represents the state of an Item. + */ +public abstract class ItemState { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(ItemState.class); + + //------------------< flags defining the current status of this instance > + /** + * the status is undefined + */ + public static final int STATUS_UNDEFINED = 0; + /** + * 'existing', i.e. persistent state + */ + public static final int STATUS_EXISTING = 1; + /** + * 'existing', i.e. persistent state that has been transiently modified (copy-on-write) + */ + public static final int STATUS_EXISTING_MODIFIED = 2; + /** + * 'existing', i.e. persistent state that has been transiently removed (copy-on-write) + */ + public static final int STATUS_EXISTING_REMOVED = 3; + /** + * 'new' state + */ + public static final int STATUS_NEW = 4; + /** + * 'existing', i.e. persistent state that has been destroyed by somebody else + */ + public static final int STATUS_STALE_DESTROYED = 6; + + /** + * the internal status of this item state + */ + protected int status = STATUS_UNDEFINED; + + /** + * a modification counter used to prevent concurrent modifications + */ + private short modCount; + + /** + * Flag indicating whether this state is transient + */ + private final boolean isTransient; + + /** + * Parent container. + */ + private ItemStateListener container; + + /** + * the backing persistent item state (may be null) + */ + protected ItemState overlayedState; + + /** + * Constructs a new unconnected item state + * + * @param initialStatus the initial status of the item state object + * @param isTransient flag indicating whether this state is transient or not + */ + protected ItemState(int initialStatus, boolean isTransient) { + switch (initialStatus) { + case STATUS_EXISTING: + case STATUS_NEW: + status = initialStatus; + break; + default: + String msg = "illegal status: " + initialStatus; + log.debug(msg); + throw new IllegalArgumentException(msg); + } + modCount = 0; + overlayedState = null; + this.isTransient = isTransient; + } + + /** + * Constructs a new item state that is initially connected to an overlayed + * state. + * + * @param overlayedState the backing item state being overlayed + * @param initialStatus the initial status of the new ItemState instance + * @param isTransient flag indicating whether this state is transient or not + */ + protected ItemState(ItemState overlayedState, int initialStatus, boolean isTransient) { + switch (initialStatus) { + case STATUS_EXISTING: + case STATUS_EXISTING_MODIFIED: + case STATUS_EXISTING_REMOVED: + status = initialStatus; + break; + + case STATUS_UNDEFINED: + // see http://issues.apache.org/jira/browse/JCR-897 + log.debug("creating ItemState instance with initialStatus=" + STATUS_UNDEFINED + ", id=" + overlayedState.getId()); + status = initialStatus; + break; + + default: + String msg = "illegal status: " + initialStatus; + log.debug(msg); + throw new IllegalArgumentException(msg); + } + this.isTransient = isTransient; + this.overlayedState = overlayedState; + } + + /** + * Copy state information from another state into this state + * @param state source state information + * @param syncModCount if the modCount should be synchronized. + */ + public abstract void copy(ItemState state, boolean syncModCount); + + /** + * Pull state information from overlayed state. + */ + synchronized void pull() { + ItemState state = overlayedState; + if (state != null) { + // sync modification count + copy(state, true); + } + } + + /** + * Push state information into overlayed state. + */ + void push() { + ItemState state = overlayedState; + if (state != null) { + state.copy(this, false); + } + } + + /** + * Called by TransientItemStateManager and + * LocalItemStateManager when this item state has been disposed. + */ + void onDisposed() { + disconnect(); + overlayedState = null; + status = STATUS_UNDEFINED; + } + + /** + * Connect this state to some underlying overlayed state. + */ + public void connect(ItemState overlayedState) + throws ItemStateException { + if (this.overlayedState != null + && this.overlayedState != overlayedState) { + throw new ItemStateException( + "Item state already connected to another" + + " underlying state: " + this); + } + this.overlayedState = overlayedState; + } + + /** + * Reconnect this state to the overlayed state that it has been + * disconnected from earlier. + */ + protected void reconnect() throws ItemStateException { + if (this.overlayedState == null) { + throw new ItemStateException( + "Item state cannot be reconnected because there's no" + + " underlying state to reconnect to: " + this); + } + } + + /** + * Disconnect this state from the underlying overlayed state. + */ + protected void disconnect() { + if (overlayedState != null) { + overlayedState = null; + } + } + + /** + * Return a flag indicating whether this state is connected to some other state. + * @return true if this state is connected, false otherwise. + */ + protected boolean isConnected() { + return overlayedState != null; + } + + /** + * Notify the parent container about changes to this state. + */ + protected void notifyStateDiscarded() { + if (container != null) { + container.stateDiscarded(this); + } + } + + /** + * Notify the parent container about changes to this state. + */ + protected void notifyStateCreated() { + if (container != null) { + container.stateCreated(this); + } + } + + /** + * Notify the parent container about changes to this state. + */ + public void notifyStateUpdated() { + if (container != null) { + container.stateModified(this); + } + } + + /** + * Notify the parent container about changes to this state. + */ + protected void notifyStateDestroyed() { + if (container != null) { + container.stateDestroyed(this); + } + } + + //-------------------------------------------------------< public methods > + /** + * Determines if this item state represents a node. + * + * @return true if this item state represents a node, otherwise false. + */ + public abstract boolean isNode(); + + /** + * Returns the identifier of this item. + * + * @return the id of this item. + */ + public abstract ItemId getId(); + + /** + * Returns true if this item state represents new or modified + * state (i.e. the result of copy-on-write) or false if it + * represents existing, unmodified state. + * + * @return true if this item state is modified or new, + * otherwise false + */ + public boolean isTransient() { + return isTransient; + } + + /** + * Determines whether this item state has become stale. + * @return true if this item state has become stale, false otherwise. + */ + public boolean isStale() { + return overlayedState != null + && modCount != overlayedState.getModCount(); + } + + /** + * Returns the NodeId of the parent NodeState or null + * if either this item state represents the root node or this item state is + * 'free floating', i.e. not attached to the repository's hierarchy. + * + * @return the parent NodeState's Id + */ + public abstract NodeId getParentId(); + + /** + * Returns the status of this item. + * + * @return the status of this item. + */ + public int getStatus() { + return status; + } + + /** + * Sets the new status of this item. + * + * @param newStatus the new status + */ + public void setStatus(int newStatus) { + switch (newStatus) { + case STATUS_NEW: + case STATUS_EXISTING: + case STATUS_EXISTING_REMOVED: + case STATUS_EXISTING_MODIFIED: + case STATUS_STALE_DESTROYED: + case STATUS_UNDEFINED: + status = newStatus; + return; + default: + String msg = "illegal status: " + newStatus; + log.debug(msg); + throw new IllegalArgumentException(msg); + } + } + + /** + * Returns the modification count. + * + * @return the modification count. + */ + public short getModCount() { + return modCount; + } + + /** + * Sets the modification count. + * + * @param modCount the modification count of this item + */ + public void setModCount(short modCount) { + this.modCount = modCount; + } + + /** + * Updates the modification count. + */ + synchronized void touch() { + modCount++; + } + + /** + * Discards this instance, i.e. renders it 'invalid'. + */ + public void discard() { + if (status != STATUS_UNDEFINED) { + // notify listeners + notifyStateDiscarded(); + // reset status + status = STATUS_UNDEFINED; + } + } + + /** + * Determines if this item state is overlying persistent state. + * + * @return true if this item state is overlying persistent + * state, otherwise false. + */ + public boolean hasOverlayedState() { + return overlayedState != null; + } + + /** + * Returns the persistent state backing this transient state or + * null if there is no persistent state (i.e.. this + * state is purely transient). + * + * @return the persistent item state or null if there is + * no persistent state. + */ + public ItemState getOverlayedState() { + return overlayedState; + } + + /** + * Set the parent container that will receive notifications about changes to this state. + * @param container container to be informed on modifications + */ + public void setContainer(ItemStateListener container) { + if (this.container != null) { + throw new IllegalStateException("State already connected to a container: " + this.container); + } + this.container = container; + } + + /** + * Return the parent container that will receive notifications about changes to this state. Returns + * null if none has been yet assigned. + * @return container or null + */ + public ItemStateListener getContainer() { + return container; + } + + /** + * Returns the approximate memory consumption of this state. + * + * @return the approximate memory consumption of this state. + */ + public abstract long calculateMemoryFootprint(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateCache.java new file mode 100644 index 00000000000..13270008390 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateCache.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.core.state; + +import org.apache.jackrabbit.core.id.ItemId; + +/** + * An ItemStateCache maintains a cache of ItemState + * instances. + */ +public interface ItemStateCache { + /** + * Returns true if this cache contains an ItemState + * object with the specified id. + * + * @param id id of ItemState object whose presence should be + * tested. + * @return true if there's a corresponding cache entry, + * otherwise false. + */ + boolean isCached(ItemId id); + + /** + * Returns the ItemState object with the specified + * id if it is present or null if no entry exists + * with that id. + * + * @param id the id of the ItemState object to be returned. + * @return the ItemState object with the specified + * id or or null if no entry exists + * with that id + */ + ItemState retrieve(ItemId id); + + /** + * Returns all the cached item states. + * + * @return newly allocated item state array + */ + ItemState[] retrieveAll(); + + /** + * Stores the specified ItemState object in the map + * using its ItemId as the key. + * + * @param state the ItemState object to cache + */ + void cache(ItemState state); + + /** + * Removes the ItemState object with the specified id from + * this cache if it is present. + * + * @param id the id of the ItemState object which should be + * removed from this cache. + */ + void evict(ItemId id); + + /** + * Clears all entries from this cache. + */ + void evictAll(); + + /** + * Returns true if this cache contains no entries. + * + * @return true if this cache contains no entries. + */ + boolean isEmpty(); + + /** + * Informs the cache that it is no longer in use. + */ + void dispose(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateCacheFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateCacheFactory.java new file mode 100644 index 00000000000..cd931c7ba99 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateCacheFactory.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.core.state; + +/** + * An item state cache factory to construct new item state caches. + */ +public interface ItemStateCacheFactory { + + /** + * Construct a new item state cache. + * + * @return the new cache + */ + ItemStateCache newItemStateCache(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateException.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateException.java new file mode 100644 index 00000000000..614110e5091 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateException.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.core.state; + +/** + * The ItemStateException ... + */ +public class ItemStateException extends Exception { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public ItemStateException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public ItemStateException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateListener.java new file mode 100644 index 00000000000..518ea153d04 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateListener.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.core.state; + +/** + * The ItemStateListener interface allows an implementing object + * to be informed about changes on an ItemState. + */ +public interface ItemStateListener { + + /** + * Called when an ItemState has successfully + * been created (i.e. its underlying persistent state was created). + * + * @param created the ItemState that has been 'created' + */ + void stateCreated(ItemState created); + + /** + * Called when an ItemState has successfully + * been modified (i.e. its underlying persistent state has changed). + * + * @param modified the ItemState that has been 'modified' + */ + void stateModified(ItemState modified); + + /** + * Called when an ItemState has successfully been + * removed (i.e. its underlying persistent state has been destroyed). + * + * @param destroyed the ItemState that has been 'destroyed' + */ + void stateDestroyed(ItemState destroyed); + + /** + * Called when an ItemState has been discarded (i.e. it has + * been rendered 'invalid'). + * + * @param discarded the ItemState that has been discarded + * @see ItemState#discard + */ + void stateDiscarded(ItemState discarded); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateManager.java new file mode 100644 index 00000000000..2c9099e31dd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateManager.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.core.state; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; + +/** + * The ItemStateManager interface provides methods for retrieving + * ItemState and NodeReferences instances by id. + */ +public interface ItemStateManager { + + /** + * Return an item state, given its item id. + * @param id item id + * @return item state + * @throws NoSuchItemStateException if the item does not exist + * @throws ItemStateException if an error occurs + */ + ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException; + + /** + * Return a flag indicating whether an item state for a given + * item id exists. + * @param id item id + * @return true if an item state exists, + * otherwise false + */ + boolean hasItemState(ItemId id); + + /** + * Return a node references object, given its target id + * @param id target id + * @return node references object + * @throws NoSuchItemStateException if the item does not exist + * @throws ItemStateException if an error occurs + */ + NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException; + + /** + * Return a flag indicating whether a node references object + * for a given target id exists. + * @param id target id + * @return true if a node reference object exists for the given + * id, otherwise false. + */ + boolean hasNodeReferences(NodeId id); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateReferenceCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateReferenceCache.java new file mode 100644 index 00000000000..af85c4aaa2c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ItemStateReferenceCache.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.core.state; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.core.id.ItemId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * ItemStateReferenceCache internally consists of 2 components: + *
      + *
    • an ItemStateReferenceMap serving as the primary (or main) + * cache; it holds weak references to ItemState instances. This + * ItemStateCache implementation directly represents the + * contents of the primary cache, i.e. {@link #isCached(ItemId)}, + * {@link #retrieve(ItemId)}}, {@link #isEmpty()} etc. only refer to the contents + * of the primary cache.
    • + *
    • an ItemStateCache implementing a custom eviction policy and + * serving as the secondary (or auxiliary) cache; entries that are automatically + * flushed from this secondary cache through its eviction policy (LRU, etc.) + * will be indirectly flushed from the primary (reference) cache by the garbage + * collector if they are thus rendered weakly reachable. + *
    • + *
    + *

    + * This implementation of ItemStateCache is thread-safe. + */ +public class ItemStateReferenceCache implements ItemStateCache { + + /** Logger instance */ + private static final Logger log = + LoggerFactory.getLogger(ItemStateReferenceCache.class); + + /** + * The number of cache segments to use. Use the number of available + * processors (even if that might change during runtime!) as a reasonable + * approximation of the amount of parallelism we should expect in the + * worst case. + *

    + * One reason for this value being a constant is that the + * {@link Runtime#availableProcessors()} call is somewhat expensive at + * least in some environments. + */ + private static int NUMBER_OF_SEGMENTS = + Runtime.getRuntime().availableProcessors(); + + /** + * Cache that automatically flushes entries based on some eviction policy; + * entries flushed from the secondary cache will be indirectly flushed + * from the reference map by the garbage collector if they thus are + * rendered weakly reachable. + */ + private final ItemStateCache cache; + + /** + * Segments of the weak reference map used to keep track of item states. + */ + private final Map[] segments; + + /** + * Creates a new ItemStateReferenceCache that uses a + * MLRUItemStateCache instance as internal cache. + */ + public ItemStateReferenceCache(ItemStateCacheFactory cacheFactory) { + this(cacheFactory.newItemStateCache()); + } + + /** + * Creates a new ItemStateReferenceCache that uses the + * specified ItemStateCache instance as internal secondary + * cache. + * + * @param cache secondary cache implementing a custom eviction policy + */ + @SuppressWarnings("unchecked") + public ItemStateReferenceCache(ItemStateCache cache) { + this.cache = cache; + this.segments = new Map[NUMBER_OF_SEGMENTS]; + for (int i = 0; i < segments.length; i++) { + // I tried using soft instead of weak references here, but that + // seems to have some unexpected performance consequences (notable + // increase in the JCR TCK run time). So even though soft references + // are generally recommended over weak references for caching + // purposes, it seems that using weak references is safer here. + segments[i] = + new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK); + } + } + + /** + * Returns the reference map segment for the given entry key. The segment + * is selected based on the hash code of the key, after a transformation + * to prevent interfering with the optimal performance of the segment + * hash map. + * + * @param id item identifier + * @return reference map segment + */ + private Map getSegment(ItemId id) { + // Unsigned shift right to prevent negative indexes and to + // prevent too similar keys to all get stored in the same segment + return segments[(id.hashCode() >>> 1) % segments.length]; + } + + //-------------------------------------------------------< ItemStateCache > + /** + * {@inheritDoc} + */ + public boolean isCached(ItemId id) { + Map segment = getSegment(id); + synchronized (segment) { + return segment.containsKey(id); + } + } + + /** + * {@inheritDoc} + */ + public ItemState retrieve(ItemId id) { + // Update the access statistics in the cache + ItemState state = cache.retrieve(id); + if (state != null) { + // Return fast to avoid the second lookup below + return state; + } + + Map segment = getSegment(id); + synchronized (segment) { + return segment.get(id); + } + } + + /** + * {@inheritDoc} + */ + public ItemState[] retrieveAll() { + List states = new ArrayList(); + for (int i = 0; i < segments.length; i++) { + synchronized (segments[i]) { + states.addAll(segments[i].values()); + } + } + return states.toArray(new ItemState[states.size()]); + } + + /** + * {@inheritDoc} + */ + public void cache(ItemState state) { + // Update the cache + cache.cache(state); + + // Store a weak reference in the reference map + ItemId id = state.getId(); + Map segment = getSegment(id); + synchronized (segment) { + ItemState s = segment.put(id, state); + // overwriting the same instance is OK + if (s != null && s != state) { + log.warn("overwriting cached entry " + id); + } + } + } + + /** + * {@inheritDoc} + */ + public void evict(ItemId id) { + // Update the cache + cache.evict(id); + // Remove from reference map + // TODO: Allow the weak reference to be cleared automatically? + Map segment = getSegment(id); + synchronized (segment) { + segment.remove(id); + } + } + + /** + * {@inheritDoc} + */ + public void dispose() { + cache.dispose(); + } + + /** + * {@inheritDoc} + */ + public void evictAll() { + // Update the cache + cache.evictAll(); + // remove all weak references from reference map + // TODO: Allow the weak reference to be cleared automatically? + for (int i = 0; i < segments.length; i++) { + synchronized (segments[i]) { + segments[i].clear(); + } + } + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + for (int i = 0; i < segments.length; i++) { + synchronized (segments[i]) { + if (!segments[i].isEmpty()) { + return false; + } + } + } + return true; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/LocalItemStateManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/LocalItemStateManager.java new file mode 100644 index 00000000000..613c5db86ab --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/LocalItemStateManager.java @@ -0,0 +1,615 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.observation.EventStateCollectionFactory; +import org.apache.jackrabbit.spi.Name; + +/** + * Local ItemStateManager that isolates changes to + * persistent states from other clients. + */ +public class LocalItemStateManager + implements UpdatableItemStateManager, NodeStateListener { + + /** + * cache of weak references to ItemState objects issued by this + * ItemStateManager + */ + private final ItemStateCache cache; + + /** + * Shared item state manager + */ + protected final SharedItemStateManager sharedStateMgr; + + /** + * Event state collection factory. + */ + protected final EventStateCollectionFactory factory; + + /** + * Flag indicating whether this item state manager is in edit mode + */ + private boolean editMode; + + /** + * Change log + */ + private final ChangeLog changeLog = new ChangeLog(); + + /** + * State change dispatcher. + */ + private final transient StateChangeDispatcher dispatcher = new StateChangeDispatcher(); + + /** + * Creates a new LocalItemStateManager instance. + * @param sharedStateMgr shared state manager + * @param factory event state collection factory + */ + protected LocalItemStateManager(SharedItemStateManager sharedStateMgr, + EventStateCollectionFactory factory, ItemStateCacheFactory cacheFactory) { + cache = new ItemStateReferenceCache(cacheFactory); + this.sharedStateMgr = sharedStateMgr; + this.factory = factory; + } + + /** + * Creates a new {@code LocalItemStateManager} instance and registers it as an {@link ItemStateListener} + * with the given {@link SharedItemStateManager}. + * + * @param sharedStateMgr the {@link SharedItemStateManager} + * @param factory the {@link EventStateCollectionFactory} + * @param cacheFactory the {@link ItemStateCacheFactory} + * @return a new {@code LocalItemStateManager} instance + */ + public static LocalItemStateManager createInstance(SharedItemStateManager sharedStateMgr, + EventStateCollectionFactory factory, ItemStateCacheFactory cacheFactory) { + LocalItemStateManager mgr = new LocalItemStateManager(sharedStateMgr, factory, cacheFactory); + sharedStateMgr.addListener(mgr); + return mgr; + } + + /** + * Retrieve a node state from the parent shared state manager and + * wraps it into a intermediate object that helps us handle local + * modifications. + * + * @param id node id + * @return node state + * @throws NoSuchItemStateException + * @throws ItemStateException + */ + protected NodeState getNodeState(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + // load from parent manager and wrap + NodeState state = (NodeState) sharedStateMgr.getItemState(id); + state = new NodeState(state, state.getStatus(), false); + + // put it in cache + cache.cache(state); + + // set parent container + state.setContainer(this); + return state; + } + + /** + * Retrieve a property state from the parent shared state manager and + * wraps it into a intermediate object that helps us handle local + * modifications. + * + * @param id property id + * @return property state + * @throws NoSuchItemStateException + * @throws ItemStateException + */ + protected PropertyState getPropertyState(PropertyId id) + throws NoSuchItemStateException, ItemStateException { + + // load from parent manager and wrap + PropertyState state = (PropertyState) sharedStateMgr.getItemState(id); + state = new PropertyState(state, state.getStatus(), false); + + // put it in cache + cache.cache(state); + + // set parent container + state.setContainer(this); + return state; + } + + /** + * Returns the change log that contains the current changes in this local + * item state manager. + * + * @return the change log with the current changes. + */ + protected ChangeLog getChanges() { + return changeLog; + } + + //-----------------------------------------------------< ItemStateManager > + /** + * {@inheritDoc} + */ + public ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + // check change log + ItemState state = changeLog.get(id); + if (state != null) { + return state; + } + + // check cache. synchronized to ensure an entry is not created twice. + synchronized (this) { + state = cache.retrieve(id); + if (state == null) { + // regular behaviour + if (id.denotesNode()) { + state = getNodeState((NodeId) id); + } else { + state = getPropertyState((PropertyId) id); + } + } + return state; + } + } + + /** + * {@inheritDoc} + */ + public boolean hasItemState(ItemId id) { + + // check items in change log + try { + ItemState state = changeLog.get(id); + if (state != null) { + return true; + } + } catch (NoSuchItemStateException e) { + return false; + } + + // check cache + if (cache.isCached(id)) { + return true; + } + + // regular behaviour + return sharedStateMgr.hasItemState(id); + } + + /** + * {@inheritDoc} + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + // check change log + NodeReferences refs = changeLog.getReferencesTo(id); + if (refs != null) { + return refs; + } + return sharedStateMgr.getNodeReferences(id); + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + // check change log + if (changeLog.getReferencesTo(id) != null) { + return true; + } + return sharedStateMgr.hasNodeReferences(id); + } + + + //--------------------------------------------< UpdatableItemStateManager > + /** + * {@inheritDoc} + */ + public synchronized void edit() throws IllegalStateException { + if (editMode) { + throw new IllegalStateException("Already in edit mode"); + } + changeLog.reset(); + + editMode = true; + } + + /** + * {@inheritDoc} + */ + public boolean inEditMode() { + return editMode; + } + + /** + * {@inheritDoc} + */ + public NodeState createNew( + NodeId id, Name nodeTypeName, NodeId parentId) + throws RepositoryException { + if (!editMode) { + throw new RepositoryException("Not in edit mode"); + } + + boolean nonRandomId = true; + if (id == null) { + id = getNodeIdFactory().newNodeId(); + nonRandomId = false; + } + + NodeState state = new NodeState( + id, nodeTypeName, parentId, ItemState.STATUS_NEW, false); + changeLog.added(state); + state.setContainer(this); + + if (nonRandomId && !changeLog.deleted(id) + && sharedStateMgr.hasItemState(id)) { + throw new InvalidItemStateException( + "Node " + id + " already exists"); + } + + return state; + } + + /** + * Returns the local node state below the given transient one. If given + * a fresh new node state, then a new local state is created and added + * to the change log. + * + * @param transientState transient state + * @return local node state + * @throws RepositoryException if the local state could not be created + */ + public NodeState getOrCreateLocalState(NodeState transientState) + throws RepositoryException { + NodeState localState = (NodeState) transientState.getOverlayedState(); + if (localState == null) { + // The transient node state is new, create a new local state + localState = new NodeState( + transientState.getNodeId(), + transientState.getNodeTypeName(), + transientState.getParentId(), + ItemState.STATUS_NEW, + false); + changeLog.added(localState); + localState.setContainer(this); + try { + transientState.connect(localState); + } catch (ItemStateException e) { + // should never happen + throw new RepositoryException(e); + } + } + return localState; + } + + /** + * {@inheritDoc} + */ + public PropertyState createNew(Name propName, NodeId parentId) + throws IllegalStateException { + if (!editMode) { + throw new IllegalStateException("Not in edit mode"); + } + PropertyState state = new PropertyState( + new PropertyId(parentId, propName), ItemState.STATUS_NEW, false); + changeLog.added(state); + state.setContainer(this); + return state; + } + + /** + * {@inheritDoc} + */ + public void store(ItemState state) throws IllegalStateException { + if (!editMode) { + throw new IllegalStateException("Not in edit mode"); + } + changeLog.modified(state); + } + + /** + * {@inheritDoc} + */ + public void destroy(ItemState state) throws IllegalStateException { + assert state != null; + if (!editMode) { + throw new IllegalStateException("Not in edit mode"); + } + changeLog.deleted(state); + } + + /** + * {@inheritDoc} + */ + public void cancel() throws IllegalStateException { + if (!editMode) { + throw new IllegalStateException("Not in edit mode"); + } + changeLog.undo(sharedStateMgr); + + editMode = false; + } + + /** + * {@inheritDoc} + */ + public void update() + throws ReferentialIntegrityException, StaleItemStateException, + ItemStateException, IllegalStateException { + if (!editMode) { + throw new IllegalStateException("Not in edit mode"); + } + // JCR-1813: Only execute the update when there are some changes + if (changeLog.hasUpdates()) { + update(changeLog); + changeLog.reset(); + } + + editMode = false; + } + + /** + * End an update operation. Fetch the states and references from + * the parent (shared) item manager, reconnect them to the items + * collected in our (local) change log and overwrite the shared + * items with our copies. + * + * @param changeLog change log containing local states and references + * @throws ReferentialIntegrityException if a new or modified REFERENCE + * property refers to a non-existent + * target or if a removed node is still + * being referenced + * @throws StaleItemStateException if at least one of the affected item + * states has become stale in the meantime + * @throws ItemStateException if an error occurs + */ + protected void update(ChangeLog changeLog) + throws ReferentialIntegrityException, StaleItemStateException, ItemStateException { + + sharedStateMgr.update(changeLog, factory); + changeLog.persisted(); + } + + /** + * {@inheritDoc} + */ + public void dispose() { + sharedStateMgr.removeListener(this); + + // this LocalItemStateManager instance is no longer needed; + // cached item states can now be safely discarded + ItemState[] states = cache.retrieveAll(); + for (int i = 0; i < states.length; i++) { + ItemState state = states[i]; + if (state != null) { + dispatcher.notifyStateDiscarded(state); + // let the item state know that it has been disposed + state.onDisposed(); + } + } + + // clear cache + cache.evictAll(); + cache.dispose(); + } + + /** + * Add an ItemStateListener + * @param listener the new listener to be informed on modifications + */ + public void addListener(ItemStateListener listener) { + dispatcher.addListener(listener); + } + + /** + * Remove an ItemStateListener + * @param listener an existing listener + */ + public void removeListener(ItemStateListener listener) { + dispatcher.removeListener(listener); + } + + //----------------------------------------------------< ItemStateListener > + + /** + * {@inheritDoc} + *

    + * Notification handler gets called for both local states that this state manager + * has created, as well as states that were created by the shared state manager + * we're listening to. + */ + public void stateCreated(ItemState created) { + ItemState local = null; + if (created.getContainer() != this) { + // shared state was created + try { + local = changeLog.get(created.getId()); + if (local != null) { + if (local.isNode() && local.getOverlayedState() != created) { + // mid-air collision of concurrent node state creation + // with same id (JCR-2272) + if (local.getStatus() == ItemState.STATUS_NEW) { + local.setStatus(ItemState.STATUS_UNDEFINED); // we need a state that is != NEW + } + } else { + if (local.getOverlayedState() == created) { + // underlying state has been permanently created + local.pull(); + local.setStatus(ItemState.STATUS_EXISTING); + cache.cache(local); + } + } + } + } catch (NoSuchItemStateException e) { + /* ignore */ + } + } else { + // local state was created + local = created; + // just ensure that the newly created state is still cached. it can + // happen during a restore operation that a state with the same id + // is deleted and created (JCR-1197) + if (!cache.isCached(created.getId())) { + cache.cache(local); + } + } + dispatcher.notifyStateCreated(created); + } + + /** + * {@inheritDoc} + *

    + * Notification handler gets called for both local states that this state manager + * has created, as well as states that were created by the shared state manager + * we're listening to. + */ + public void stateModified(ItemState modified) { + ItemState local; + if (modified.getContainer() != this) { + // shared state was modified + local = cache.retrieve(modified.getId()); + if (local != null && local.isConnected()) { + // this instance represents existing state, update it + local.pull(); + } + } else { + // local state was modified + local = modified; + } + if (local != null) { + dispatcher.notifyStateModified(local); + } else if (modified.isNode()) { + // if the state is not ours (and is not cached) it could have + // vanished from the weak-ref cache due to a gc. but there could + // still be some listeners (e.g. CachingHierarchyManager) that want + // to get notified. + dispatcher.notifyNodeModified((NodeState) modified); + } + } + + /** + * {@inheritDoc} + *

    + * Notification handler gets called for both local states that this state manager + * has created, as well as states that were created by the shared state manager + * we're listening to. + */ + public void stateDestroyed(ItemState destroyed) { + ItemState local = null; + if (destroyed.getContainer() != this) { + // shared state was destroyed + local = cache.retrieve(destroyed.getId()); + if (local != null && local.isConnected()) { + local.setStatus(ItemState.STATUS_EXISTING_REMOVED); + } + } else { + // local state was destroyed + local = destroyed; + } + cache.evict(destroyed.getId()); + if (local != null) { + dispatcher.notifyStateDestroyed(local); + } + } + + /** + * {@inheritDoc} + *

    + * Notification handler gets called for both local states that this state manager + * has created, as well as states that were created by the shared state manager + * we're listening to. + */ + public void stateDiscarded(ItemState discarded) { + ItemState local = null; + if (discarded.getContainer() != this) { + // shared state was discarded + local = cache.retrieve(discarded.getId()); + if (local != null && local.isConnected()) { + local.setStatus(ItemState.STATUS_UNDEFINED); + } + } else { + // local state was discarded + local = discarded; + } + cache.evict(discarded.getId()); + if (local != null) { + dispatcher.notifyStateDiscarded(local); + } + } + + /** + * {@inheritDoc} + *

    + * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state + * concerned must be a local state. + */ + public void nodeAdded(NodeState state, Name name, int index, NodeId id) { + dispatcher.notifyNodeAdded(state, name, index, id); + } + + /** + * {@inheritDoc} + *

    + * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state + * concerned must be a local state. + */ + public void nodesReplaced(NodeState state) { + dispatcher.notifyNodesReplaced(state); + } + + /** + * {@inheritDoc} + *

    + * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state + * concerned must be a local state. + */ + public void nodeModified(NodeState state) { + dispatcher.notifyNodeModified(state); + } + + /** + * {@inheritDoc} + *

    + * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state + * concerned must be a local state. + */ + public void nodeRemoved(NodeState state, Name name, int index, NodeId id) { + dispatcher.notifyNodeRemoved(state, name, index, id); + } + + public NodeIdFactory getNodeIdFactory() { + return sharedStateMgr.getNodeIdFactory(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/MLRUItemStateCache.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/MLRUItemStateCache.java new file mode 100644 index 00000000000..90bf9d2db7e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/MLRUItemStateCache.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.core.state; + +import java.util.List; + +import org.apache.commons.collections.map.LinkedMap; +import org.apache.jackrabbit.core.cache.CacheManager; +import org.apache.jackrabbit.core.cache.ConcurrentCache; +import org.apache.jackrabbit.core.id.ItemId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An ItemStateCache implementation that internally uses a + * {@link LinkedMap} to maintain a cache of ItemState objects. The + * cache uses a rough estimate of the memory consumption of the cached item + * states for calculating the maximum number of entries. The oldest entries + * are flushed once the cache size has exceeded a certain limit. + *

    + * TODO rename class to something more appropriate, e.g. FIFOItemSateCache since + * it doesn't use a LRU eviction policy anymore. + */ +public class MLRUItemStateCache implements ItemStateCache { + + /** Logger instance */ + private static Logger log = LoggerFactory.getLogger(MLRUItemStateCache.class); + + /** default maximum memory to use */ + public static final int DEFAULT_MAX_MEM = 4 * 1024 * 1024; + + /** the number of writes */ + private volatile long numWrites = 0; + + private final ConcurrentCache cache = + new ConcurrentCache(MLRUItemStateCache.class.getSimpleName()); + + public MLRUItemStateCache(CacheManager cacheMgr) { + cache.setMaxMemorySize(DEFAULT_MAX_MEM); + cache.setAccessListener(cacheMgr); + cacheMgr.add(cache); + } + + //-------------------------------------------------------< ItemStateCache > + + /** + * {@inheritDoc} + */ + public boolean isCached(ItemId id) { + return cache.containsKey(id); + } + + /** + * {@inheritDoc} + */ + public ItemState retrieve(ItemId id) { + return cache.get(id); + } + + /** + * {@inheritDoc} + */ + public ItemState[] retrieveAll() { + List values = cache.values(); + return values.toArray(new ItemState[values.size()]); + } + + /** + * {@inheritDoc} + */ + public void cache(ItemState state) { + cache.put(state.getId(), state, state.calculateMemoryFootprint()); + + if (numWrites++ % 10000 == 0 && log.isDebugEnabled()) { + log.debug("Item state cache size: {}% of {} bytes", + cache.getMemoryUsed() * 100 / cache.getMaxMemorySize(), + cache.getMaxMemorySize()); + } + } + + /** + * {@inheritDoc} + */ + public void evict(ItemId id) { + cache.remove(id); + } + + /** + * {@inheritDoc} + */ + public void evictAll() { + cache.clear(); + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + return cache.isEmpty(); + } + + /** + * {@inheritDoc} + */ + public void dispose() { + cache.dispose(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ManagedMLRUItemStateCacheFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ManagedMLRUItemStateCacheFactory.java new file mode 100644 index 00000000000..266bdc5db23 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/ManagedMLRUItemStateCacheFactory.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.core.state; + +import org.apache.jackrabbit.core.cache.CacheManager; + +/** + * This class constructs new MLRUItemStateCache. + * This class adds the new caches to the cache manager, + * and links the caches to the cache manager. + */ +public class ManagedMLRUItemStateCacheFactory implements ItemStateCacheFactory { + + /** The cache manager. */ + private CacheManager cacheMgr; + + /** + * Construct a new factory using a cache manager. + * + * @param cacheMgr the cache manager + */ + public ManagedMLRUItemStateCacheFactory(CacheManager cacheMgr) { + this.cacheMgr = cacheMgr; + } + + /** + * Create a new cache instance and link it to the cache manager. + */ + public ItemStateCache newItemStateCache() { + return new MLRUItemStateCache(cacheMgr); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NameSet.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NameSet.java new file mode 100644 index 00000000000..9bbfb89be0c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NameSet.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.core.state; + +import org.apache.jackrabbit.spi.Name; + +import java.util.Set; +import java.util.Iterator; +import java.util.Collection; +import java.util.HashSet; + +/** + * NameSet implements a collection of unique {@link Name}s. The + * methods exposed via the {@link Set} interface are for read only access, which + * means this implementation will throw a {@link UnsupportedOperationException} + * for all modifying methods specified by the {@link Set} interface. + */ +final class NameSet implements Set, Cloneable { + + /** + * The name set cache instance. + */ + private static final NameSetCache CACHE = new NameSetCache(); + + /** + * The maximum number of names in a set that are cached. + */ + private static final int NUM_NAMES_THRESHOLD = 5; + + /** + * The set of property {@link Name}s. + */ + private HashSet names = CACHE.getEmptySet(); + + /** + * Flag indicating whether the {@link #names} set is shared with another + * {@link NameSet} instance. The initial value is true because + * {@link #names} is initialized with an empty set from the cache. + */ + private boolean shared = true; + + /** + * Adds a name. + * + * @param name the name to add. + * @return true if the name is already present, + * false otherwise. + */ + public boolean add(Name name) { + if (names.size() > NUM_NAMES_THRESHOLD) { + ensureModifiable(); + return names.add(name); + } else { + int size = names.size(); + // get a cached set + names = CACHE.get(names, name, !shared); + // a set from the cache is always shared + shared = true; + return names.size() != size; + } + } + + /** + * Removes a name. + * + * @param name the name to remove. + * @return true if the name was removed, false + * if the name was unknown. + */ + boolean remove(Name name) { + ensureModifiable(); + return names.remove(name); + } + + /** + * Removes all names from this {@link NameSet}. + */ + void removeAll() { + ensureModifiable(); + names.clear(); + } + + /** + * Removes all names currently present and adds all names from + * c. + * + * @param c the {@link Name}s to add. + */ + void replaceAll(Collection c) { + if (c instanceof NameSet) { + NameSet propNames = (NameSet) c; + names = propNames.names; + shared = true; + propNames.shared = true; + } else if (c instanceof HashSet) { + names = CACHE.get((HashSet) c); + shared = true; + } else { + ensureModifiable(); + names.clear(); + names.addAll(c); + } + } + + //------------------------------------------------< unmodifiable Set view > + + /** + * {@inheritDoc} + */ + public int size() { + return names.size(); + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + return names.isEmpty(); + } + + /** + * {@inheritDoc} + */ + public boolean contains(Object o) { + return names.contains(o); + } + + /** + * {@inheritDoc} + *

    + * The returned iterator will throw a {@link UnsupportedOperationException} + * on {@link Iterator#remove()}. + */ + public Iterator iterator() { + return new Iterator() { + Iterator i = names.iterator(); + + public boolean hasNext() { + return i.hasNext(); + } + + public Object next() { + return i.next(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** + * {@inheritDoc} + */ + public Object[] toArray() { + return names.toArray(); + } + + /** + * {@inheritDoc} + */ + public Object[] toArray(Object[] a) { + return names.toArray(a); + } + + /** + * @throws UnsupportedOperationException always. + */ + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean containsAll(Collection c) { + return names.containsAll(c); + } + + /** + * @throws UnsupportedOperationException always. + */ + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public void clear() { + throw new UnsupportedOperationException(); + } + + //--------------------------------------------------< equals and hashCode > + + /** + * {@inheritDoc} + */ + public int hashCode() { + return names.hashCode(); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof NameSet) { + NameSet other = (NameSet) obj; + return this.names.equals(other.names); + } + return false; + } + + //----------------------------------------------------< Cloneable support > + + /** + * Returns a clone of this PropertyNames instance. + * + * @return a clone of this PropertyNames instance. + */ + public Object clone() { + try { + NameSet propNames = (NameSet) super.clone(); + shared = true; + propNames.shared = true; + return propNames; + } catch (CloneNotSupportedException e) { + // will never happen + throw new InternalError(); + } + } + + //-------------------------------------------------------------< internal > + + /** + * Ensures that {@link #names} can be modified (-> not shared). + */ + private void ensureModifiable() { + if (shared) { + names = (HashSet) names.clone(); + shared = false; + } + } + + /** + * Implements a simple HashSet<Name> cache. + *

    + * Please note that this cache does not ensures that the sets are immutable! + * It is the responsibility of the caller to make sure that sets passed to + * {@link #get} are not modified by multiple threads. Modifying a cached + * set is not a problem in general because it will only cause cache misses. + */ + private static final class NameSetCache { + + /** + * Size of the cache (must be a power of two). Note that this is the + * maximum number of objects kept in the cache, but due to hashing it + * can well be that only a part of the cache array is filled even if + * many more distinct objects are being accessed. + */ + private static final int SIZE_POWER_OF_2 = 1024; + + /** + * Array of cached hash sets, indexed by their hash codes + * (module size of the array). + */ + private final HashSet[] array = new HashSet[SIZE_POWER_OF_2]; + + /** + * Returns a set that contains all elements from set and + * obj. If a cached copy of the set already exists, then + * this method returns that copy. Otherwise obj is added + * to the given set, the set is cached and + * then returned. + * + * @param set the initial set. + * @param obj the object to add to set. + * @param modifiable true if set may be modified. + * @return a cached set that contains all elements from set + * and obj. + */ + public HashSet get(HashSet set, Object obj, boolean modifiable) { + if (set.contains(obj)) { + return set; + } + int position = (set.hashCode() + obj.hashCode()) & (SIZE_POWER_OF_2 - 1); + HashSet previous = array[position]; + if (previous != null && + previous.size() == set.size() + 1 && + previous.containsAll(set) && + previous.contains(obj)) { + return previous; + } else { + if (modifiable) { + set.add(obj); + } else { + set = (HashSet) set.clone(); + set.add(obj); + } + array[position] = set; + return set; + } + } + + /** + * If a cached copy of the given set already exists, then this method + * returns that copy. Otherwise the given set is cached and returned. + * + * @param set set to return from the cache + * @return the given set or a previously cached copy + */ + public HashSet get(HashSet set) { + int position = set.hashCode() & (SIZE_POWER_OF_2 - 1); + HashSet previous = array[position]; + if (set.equals(previous)) { + return previous; + } else { + array[position] = set; + return set; + } + } + + /** + * Returns a cached copy of an empty set. + * + * @return a cached copy of an empty set. + */ + public HashSet getEmptySet() { + HashSet set = array[0]; + if (set == null || !set.isEmpty()) { + set = new HashSet(); + array[0] = set; + } + return set; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NoSuchItemStateException.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NoSuchItemStateException.java new file mode 100644 index 00000000000..ad11a063bcb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NoSuchItemStateException.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.core.state; + +/** + * The NoSuchItemStateException ... + */ +public class NoSuchItemStateException extends ItemStateException { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public NoSuchItemStateException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public NoSuchItemStateException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeReferences.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeReferences.java new file mode 100644 index 00000000000..5ac56773349 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeReferences.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.core.state; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; + +/** + * NodeReferences represents the references (i.e. properties of + * type REFERENCE) to a particular node (denoted by its uuid). + */ +public class NodeReferences implements Serializable { + + /** + * Serial UID + */ + static final long serialVersionUID = 7007727035982680717L; + + /** + * Identifier of the target node. + */ + protected NodeId id; + + /** + * list of PropertyId's (i.e. the id's of the properties that refer to + * the target node denoted by id.getTargetId()). + *

    + * note that the list can contain duplicate entries because a specific + * REFERENCE property can contain multiple references (if it's multi-valued) + * to potentially the same target node. + */ + protected ArrayList references = new ArrayList(); + + public NodeReferences(NodeId id) { + this.id = id; + } + + /** + * Returns the identifier of the target node. + * + * @return the id of the target node + */ + public NodeId getTargetId() { + return id; + } + + /** + * Returns a flag indicating whether this object holds any references + * + * @return true if this object holds references, + * false otherwise + */ + public boolean hasReferences() { + return !references.isEmpty(); + } + + /** + * @return the list of references + */ + public List getReferences() { + return Collections.unmodifiableList(references); + } + + /** + * @param refId + */ + public void addReference(PropertyId refId) { + references.add(refId); + } + + /** + * @param references + */ + public void addAllReferences(List references) { + this.references.addAll(references); + } + + /** + * @param refId + * @return true if the reference was removed; + * false otherwise. + */ + public boolean removeReference(PropertyId refId) { + return references.remove(refId); + } + + /** + * + */ + public void clearAllReferences() { + references.clear(); + } + + //--------------------------------------------------------------< Object > + + public String toString() { + return "references to " + id; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeState.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeState.java new file mode 100644 index 00000000000..e57e0abeaf9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeState.java @@ -0,0 +1,918 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; + +/** + * NodeState represents the state of a Node. + */ +public class NodeState extends ItemState { + + /** + * the name of this node's primary type + */ + private Name nodeTypeName; + + /** + * the names of this node's mixin types + */ + private NameSet mixinTypeNames = new NameSet(); + + /** + * the id of this node. + */ + private NodeId id; + + /** + * the id of the parent node or null if this instance + * represents the root node + */ + private NodeId parentId; + + /** + * insertion-ordered collection of ChildNodeEntry objects + */ + private ChildNodeEntries childNodeEntries = new ChildNodeEntries(); + + /** + * set of property names (Name objects) + */ + private NameSet propertyNames = new NameSet(); + + /** + * Shared set, consisting of the parent ids of this shareable node. This + * entry is {@link Collections#EMPTY_SET} if this node is not shareable. + */ + private Set sharedSet = Collections.emptySet(); + + /** + * Flag indicating whether we are using a read-write shared set. + */ + private boolean sharedSetRW; + + /** + * Listener. + */ + private transient NodeStateListener listener; + + /** + * Constructs a new node state that is initially connected to an overlayed + * state. + * + * @param overlayedState the backing node state being overlayed + * @param initialStatus the initial status of the node state object + * @param isTransient flag indicating whether this state is transient or not + */ + public NodeState(NodeState overlayedState, int initialStatus, + boolean isTransient) { + super(overlayedState, initialStatus, isTransient); + pull(); + } + + /** + * Constructs a new node state that is not connected. + * + * @param id id of this node + * @param nodeTypeName node type of this node + * @param parentId id of the parent node + * @param initialStatus the initial status of the node state object + * @param isTransient flag indicating whether this state is transient or not + */ + public NodeState(NodeId id, Name nodeTypeName, NodeId parentId, + int initialStatus, boolean isTransient) { + super(initialStatus, isTransient); + this.id = id; + this.parentId = parentId; + this.nodeTypeName = nodeTypeName; + } + + //-------------------------------------------------------< public methods > + /** + * {@inheritDoc} + */ + @Override + public synchronized void copy(ItemState state, boolean syncModCount) { + synchronized (state) { + NodeState nodeState = (NodeState) state; + id = nodeState.id; + parentId = nodeState.parentId; + nodeTypeName = nodeState.nodeTypeName; + mixinTypeNames = (NameSet) nodeState.mixinTypeNames.clone(); + propertyNames = (NameSet) nodeState.propertyNames.clone(); + childNodeEntries = (ChildNodeEntries) nodeState.childNodeEntries.clone(); + if (syncModCount) { + setModCount(state.getModCount()); + } + sharedSet = nodeState.sharedSet; + sharedSetRW = false; + } + } + + /** + * {@inheritDoc} + * + * @return always true + */ + @Override + public final boolean isNode() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public NodeId getParentId() { + return parentId; + } + + /** + * {@inheritDoc} + */ + @Override + public ItemId getId() { + return id; + } + + /** + * Returns the identifier of this node. + * + * @return the id of this node. + */ + public NodeId getNodeId() { + return id; + } + + /** + * Sets the id of this node's parent. + * + * @param parentId the parent node's id or null + * if either this node state should represent the root node or this node + * state should be 'free floating', i.e. detached from the workspace's + * hierarchy. + */ + public void setParentId(NodeId parentId) { + this.parentId = parentId; + } + + /** + * Returns the name of this node's node type. + * + * @return the name of this node's node type. + */ + public Name getNodeTypeName() { + return nodeTypeName; + } + + /** + * Returns the names of this node's mixin types. + * + * @return a set of the names of this node's mixin types. + */ + public synchronized Set getMixinTypeNames() { + return mixinTypeNames; + } + + /** + * Sets the names of this node's mixin types. + * + * @param names set of names of mixin types + */ + public synchronized void setMixinTypeNames(Set names) { + mixinTypeNames.replaceAll(names); + } + + /** + * Determines if there are any child node entries. + * + * @return true if there are child node entries, + * false otherwise. + */ + public boolean hasChildNodeEntries() { + return !childNodeEntries.isEmpty(); + } + + /** + * Determines if there is a ChildNodeEntry with the + * specified name. + * + * @param name Name object specifying a node name + * @return true if there is a ChildNodeEntry with + * the specified name. + */ + public synchronized boolean hasChildNodeEntry(Name name) { + return !childNodeEntries.get(name).isEmpty(); + } + + /** + * Determines if there is a ChildNodeEntry with the + * specified NodeId. + * + * @param id the id of the child node + * @return true if there is a ChildNodeEntry with + * the specified name. + */ + public synchronized boolean hasChildNodeEntry(NodeId id) { + return childNodeEntries.get(id) != null; + } + + /** + * Determines if there is a ChildNodeEntry with the + * specified name and index. + * + * @param name Name object specifying a node name + * @param index 1-based index if there are same-name child node entries + * @return true if there is a ChildNodeEntry with + * the specified name and index. + */ + public synchronized boolean hasChildNodeEntry(Name name, int index) { + return childNodeEntries.get(name, index) != null; + } + + /** + * Determines if there is a property entry with the specified + * Name. + * + * @param propName Name object specifying a property name + * @return true if there is a property entry with the specified + * Name. + */ + public synchronized boolean hasPropertyName(Name propName) { + return propertyNames.contains(propName); + } + + /** + * Returns the ChildNodeEntry with the specified name and index + * or null if there's no matching entry. + * + * @param nodeName Name object specifying a node name + * @param index 1-based index if there are same-name child node entries + * @return the ChildNodeEntry with the specified name and index + * or null if there's no matching entry. + */ + public synchronized ChildNodeEntry getChildNodeEntry(Name nodeName, int index) { + return childNodeEntries.get(nodeName, index); + } + + /** + * Returns the ChildNodeEntry with the specified NodeId or + * null if there's no matching entry. + * + * @param id the id of the child node + * @return the ChildNodeEntry with the specified NodeId or + * null if there's no matching entry. + * @see #addChildNodeEntry + * @see #removeChildNodeEntry + */ + public synchronized ChildNodeEntry getChildNodeEntry(NodeId id) { + return childNodeEntries.get(id); + } + + /** + * Returns a list of ChildNodeEntry objects denoting the + * child nodes of this node. + * + * @return list of ChildNodeEntry objects + * @see #addChildNodeEntry + * @see #removeChildNodeEntry + */ + public synchronized List getChildNodeEntries() { + return childNodeEntries.list(); + } + + /** + * Returns a list of ChildNodeEntrys with the specified name. + * + * @param nodeName name of the child node entries that should be returned + * @return list of ChildNodeEntry objects + * @see #addChildNodeEntry + * @see #removeChildNodeEntry + */ + public synchronized List getChildNodeEntries(Name nodeName) { + return childNodeEntries.get(nodeName); + } + + /** + * Adds a new ChildNodeEntry. + * + * @param nodeName Name object specifying the name of the new entry. + * @param id the id the new entry is refering to. + * @return the newly added ChildNodeEntry + */ + public ChildNodeEntry addChildNodeEntry(Name nodeName, + NodeId id) { + ChildNodeEntry entry = null; + synchronized (this) { + entry = childNodeEntries.add(nodeName, id); + } + notifyNodeAdded(entry); + return entry; + } + + /** + * Renames a ChildNodeEntry by removing the old entry and + * appending the new entry to the end of the list. + * + * @param oldName Name object specifying the entry's old name + * @param index 1-based index if there are same-name child node entries + * @param newName Name object specifying the entry's new name + * @return true if the entry was successfully renamed; + * otherwise false + */ + public boolean renameChildNodeEntry(Name oldName, int index, + Name newName) { + ChildNodeEntry oldEntry = childNodeEntries.get(oldName, index); + if (oldEntry != null) { + return renameChildNodeEntry(oldEntry.getId(), newName); + } + return false; + } + + /** + * Renames a ChildNodeEntry by removing the old entry and + * appending the new entry to the end of the list. + * + * @param id id the entry to be renamed is refering to. + * @param newName Name object specifying the entry's new name + * @return true if the entry was successfully renamed; + * otherwise false + */ + public boolean renameChildNodeEntry(NodeId id, Name newName) { + ChildNodeEntry oldEntry = null; + ChildNodeEntry newEntry = null; + synchronized (this) { + oldEntry = childNodeEntries.remove(id); + if (oldEntry != null) { + newEntry = + childNodeEntries.add(newName, oldEntry.getId()); + } + } + if (oldEntry != null) { + if (oldEntry.getName().equals(newName)) { + notifyNodesReplaced(); + } else { + notifyNodeAdded(newEntry); + notifyNodeRemoved(oldEntry); + } + return true; + } + return false; + } + + /** + * Replaces the ChildNodeEntry identified by oldId + * with a new entry. Note that the entry will overwrite the old + * entry at the same relative position within the child node entries list. + * + * @param oldId id the entry to be replaced is referring to. + * @param newName Name object specifying the entry's new name + * @param newId the id the new entry is referring to. + * @return true if the entry was successfully replaced; + * otherwise false + */ + public boolean replaceChildNodeEntry(NodeId oldId, Name newName, NodeId newId) { + synchronized (this) { + ChildNodeEntry oldEntry = childNodeEntries.get(oldId); + if (oldEntry == null) { + return false; + } + + ChildNodeEntries entries = new ChildNodeEntries(); + for (ChildNodeEntry entry : childNodeEntries.list()) { + if (entry.getId() == oldId) { + entries.add(newName, newId); + } else { + entries.add(entry.getName(), entry.getId()); + } + } + childNodeEntries = entries; + } + + notifyNodesReplaced(); + return true; + } + + /** + * Removes a ChildNodeEntry. + * + * @param nodeName ChildNodeEntry object specifying a node name + * @param index 1-based index if there are same-name child node entries + * @return true if the specified child node entry was found + * in the list of child node entries and could be removed. + */ + public boolean removeChildNodeEntry(Name nodeName, int index) { + ChildNodeEntry entry = null; + synchronized (this) { + entry = childNodeEntries.remove(nodeName, index); + } + if (entry != null) { + notifyNodeRemoved(entry); + } + return entry != null; + } + + /** + * Removes a ChildNodeEntry. + * + * @param id the id of the entry to be removed + * @return true if the specified child node entry was found + * in the list of child node entries and could be removed. + */ + public boolean removeChildNodeEntry(NodeId id) { + ChildNodeEntry entry = null; + synchronized (this) { + entry = childNodeEntries.remove(id); + } + if (entry != null) { + notifyNodeRemoved(entry); + } + return entry != null; + } + + /** + * Removes all ChildNodeEntrys. + */ + public void removeAllChildNodeEntries() { + synchronized (this) { + childNodeEntries.removeAll(); + } + notifyNodesReplaced(); + } + + /** + * Sets the list of ChildNodeEntry objects denoting the + * child nodes of this node. + * @param nodeEntries list of {@link ChildNodeEntry}s + */ + public void setChildNodeEntries(List nodeEntries) { + synchronized (this) { + childNodeEntries.removeAll(); + childNodeEntries.addAll(nodeEntries); + } + notifyNodesReplaced(); + } + + /** + * Returns the names of this node's properties as a set of + * QNames objects. + * + * @return set of QNames objects + * @see #addPropertyName + * @see #removePropertyName + */ + public synchronized Set getPropertyNames() { + return propertyNames; + } + + /** + * Adds a property name entry. + * + * @param propName Name object specifying the property name + */ + public synchronized void addPropertyName(Name propName) { + propertyNames.add(propName); + } + + /** + * Removes a property name entry. + * + * @param propName Name object specifying the property name + * @return true if the specified property name was found + * in the list of property name entries and could be removed. + */ + public synchronized boolean removePropertyName(Name propName) { + return propertyNames.remove(propName); + } + + /** + * Removes all property name entries. + */ + public synchronized void removeAllPropertyNames() { + propertyNames.removeAll(); + } + + /** + * Sets the set of Name objects denoting the + * properties of this node. + * @param propNames set of {@link Name}s. + */ + public synchronized void setPropertyNames(Set propNames) { + propertyNames.replaceAll(propNames); + } + + /** + * Set the node type name. Needed for deserialization and should therefore + * not change the internal status. + * + * @param nodeTypeName node type name + */ + public synchronized void setNodeTypeName(Name nodeTypeName) { + this.nodeTypeName = nodeTypeName; + } + + /** + * Return a flag indicating whether this state is shareable, i.e. whether + * there is at least one member inside its shared set. + * @return true if this state is shareable. + */ + public synchronized boolean isShareable() { + return sharedSet != Collections.EMPTY_SET; + } + + /** + * Add a parent to the shared set. + * + * @param parentId parent id to add to the shared set + * @return true if the parent was successfully added; + * false otherwise + */ + public synchronized boolean addShare(NodeId parentId) { + // check first before making changes + if (sharedSet.contains(parentId)) { + return false; + } + if (!sharedSetRW) { + sharedSet = new LinkedHashSet(sharedSet); + sharedSetRW = true; + } + return sharedSet.add(parentId); + } + + /** + * Return a flag whether the given parent id appears in the shared set. + * + * @param parentId parent id + * @return true if the parent id appears in the shared set; + * false otherwise. + */ + public synchronized boolean containsShare(NodeId parentId) { + return sharedSet.contains(parentId); + } + + /** + * Return the shared set as an unmodifiable collection. + * + * @return unmodifiable collection + */ + public Set getSharedSet() { + if (sharedSet != Collections.EMPTY_SET) { + return Collections.unmodifiableSet(sharedSet); + } + return Collections.emptySet(); + } + + /** + * Set the shared set of this state to the shared set of another state. + * This state will get a deep copy of the shared set given. + * + * @param set shared set + */ + public synchronized void setSharedSet(Set set) { + if (set != Collections.EMPTY_SET) { + sharedSet = new LinkedHashSet(set); + sharedSetRW = true; + } else { + sharedSet = Collections.emptySet(); + sharedSetRW = false; + } + } + + /** + * Remove a parent from the shared set. Returns the number of + * elements in the shared set. If this number is 0, + * the shared set is empty, i.e. there are no more parent items + * referencing this item and the state is free floating. + * + * @param parentId parent id to remove from the shared set + * @return the number of elements left in the shared set + */ + public synchronized int removeShare(NodeId parentId) { + // check first before making changes + if (sharedSet.contains(parentId)) { + if (!sharedSetRW) { + sharedSet = new LinkedHashSet(sharedSet); + sharedSetRW = true; + } + sharedSet.remove(parentId); + if (parentId.equals(this.parentId)) { + if (!sharedSet.isEmpty()) { + this.parentId = sharedSet.iterator().next(); + } else { + this.parentId = null; + } + } + } + return sharedSet.size(); + } + + //---------------------------------------------------------< diff methods > + /** + * Returns a set of Names denoting those properties that + * do not exist in the overlayed node state but have been added to + * this node state. + * + * @return set of Names denoting the properties that have + * been added. + */ + public synchronized Set getAddedPropertyNames() { + if (!hasOverlayedState()) { + return propertyNames; + } + + NodeState other = (NodeState) getOverlayedState(); + HashSet set = new HashSet(propertyNames); + set.removeAll(other.propertyNames); + return set; + } + + /** + * Returns a list of child node entries that do not exist in the overlayed + * node state but have been added to this node state. + * + * @return list of added child node entries + */ + public synchronized List getAddedChildNodeEntries() { + if (!hasOverlayedState()) { + return childNodeEntries.list(); + } + + NodeState other = (NodeState) getOverlayedState(); + return childNodeEntries.removeAll(other.childNodeEntries); + } + + /** + * Returns a set of Names denoting those properties that + * exist in the overlayed node state but have been removed from + * this node state. + * + * @return set of Names denoting the properties that have + * been removed. + */ + public synchronized Set getRemovedPropertyNames() { + if (!hasOverlayedState()) { + return Collections.emptySet(); + } + + NodeState other = (NodeState) getOverlayedState(); + HashSet set = new HashSet(other.propertyNames); + set.removeAll(propertyNames); + return set; + } + + /** + * Returns a list of child node entries, that exist in the overlayed node state + * but have been removed from this node state. + * + * @return list of removed child node entries + */ + public synchronized List getRemovedChildNodeEntries() { + if (!hasOverlayedState()) { + return Collections.emptyList(); + } + + NodeState other = (NodeState) getOverlayedState(); + return other.childNodeEntries.removeAll(childNodeEntries); + } + + /** + * Returns a list of child node entries that exist both in this node + * state and in the overlayed node state but have been renamed. + * + * @return list of renamed child node entries + */ + public synchronized List getRenamedChildNodeEntries() { + if (!hasOverlayedState()) { + return childNodeEntries.getRenamedEntries( + ((NodeState) overlayedState).childNodeEntries); + } else { + return Collections.emptyList(); + } + } + + /** + * Returns a list of child node entries that exist both in this node + * state and in the overlayed node state but have been reordered. + *

    + * The list may include only the minimal set of nodes that have been + * reordered. That is, even though a certain number of nodes have changed + * their absolute position the list may include less that this number of + * nodes. + *

    + * Example:
    + * Initial state: + *

    +     *  + node1
    +     *  + node2
    +     *  + node3
    +     * 
    + * After reorder: + *
    +     *  + node2
    +     *  + node3
    +     *  + node1
    +     * 
    + * All nodes have changed their absolute position. The returned list however + * may only return that node1 has been reordered (from the + * first position to the end). + * + * @return list of reordered child node enties. + */ + public synchronized List getReorderedChildNodeEntries() { + if (!hasOverlayedState()) { + return Collections.emptyList(); + } + + ChildNodeEntries otherChildNodeEntries = + ((NodeState) overlayedState).childNodeEntries; + + if (childNodeEntries.isEmpty() + || otherChildNodeEntries.isEmpty()) { + return Collections.emptyList(); + } + + // build intersections of both collections, + // each preserving their relative order + List ours = childNodeEntries.retainAll(otherChildNodeEntries); + List others = otherChildNodeEntries.retainAll(childNodeEntries); + + // do a lazy init + List reordered = null; + // both entry lists now contain the set of nodes that have not + // been removed or added, but they may have changed their position. + for (int i = 0; i < ours.size();) { + ChildNodeEntry entry = ours.get(i); + ChildNodeEntry other = others.get(i); + if (entry == other || entry.getId().equals(other.getId())) { + // no reorder, move to next child entry + i++; + } else { + // reordered entry detected + if (reordered == null) { + reordered = new ArrayList(); + } + // Note that this check will not necessarily find the + // minimal reorder operations required to convert the overlayed + // child node entries into the current. + + // is there a next entry? + if (i + 1 < ours.size()) { + // if entry is the next in the other list then probably + // the other entry at position i was reordered + if (entry.getId().equals(others.get(i + 1).getId())) { + // scan for the uuid of the other entry in our list + for (int j = i; j < ours.size(); j++) { + if (ours.get(j).getId().equals(other.getId())) { + // found it + entry = ours.get(j); + break; + } + } + } + } + + reordered.add(entry); + // remove the entry from both lists + // entries > i are already cleaned + for (int j = i; j < ours.size(); j++) { + if (ours.get(j).getId().equals(entry.getId())) { + ours.remove(j); + } + } + for (int j = i; j < ours.size(); j++) { + if (others.get(j).getId().equals(entry.getId())) { + others.remove(j); + } + } + // if a reorder has been detected index i is not + // incremented because entries will be shifted when the + // reordered entry is removed. + } + } + if (reordered == null) { + return Collections.emptyList(); + } else { + return reordered; + } + } + + /** + * Returns a set of shares that were added. + * + * @return the set of shares that were added. Set of {@link NodeId}s. + */ + public synchronized Set getAddedShares() { + if (!hasOverlayedState() || !isShareable()) { + return Collections.emptySet(); + } + NodeState other = (NodeState) getOverlayedState(); + HashSet set = new HashSet(sharedSet); + set.removeAll(other.sharedSet); + return set; + } + + /** + * Returns a set of shares that were removed. + * + * @return the set of shares that were removed. Set of {@link NodeId}s. + */ + public synchronized Set getRemovedShares() { + if (!hasOverlayedState() || !isShareable()) { + return Collections.emptySet(); + } + NodeState other = (NodeState) getOverlayedState(); + HashSet set = new HashSet(other.sharedSet); + set.removeAll(sharedSet); + return set; + } + + //--------------------------------------------------< ItemState overrides > + + /** + * {@inheritDoc} + *

    + * If the listener passed is at the same time a NodeStateListener + * we remember it as well. + */ + @Override + public void setContainer(ItemStateListener listener) { + if (listener instanceof NodeStateListener) { + if (this.listener != null) { + throw new IllegalStateException("State already connected to a listener: " + this.listener); + } + this.listener = (NodeStateListener) listener; + } + super.setContainer(listener); + } + + //-------------------------------------------------< misc. helper methods > + + /** + * Returns an estimate of the memory size of this node state. The return + * value actually highly overestimates the amount of required memory, but + * changing the estimates would likely cause OOMs in many downstream + * deployments that have set their cache sizes based on experience with + * these erroneous size estimates. So we don't change the formula used + * by this method. + */ + @Override + public long calculateMemoryFootprint() { + // Don't change this formula! See javadoc above. + return 350 + + mixinTypeNames.size() * 250 + + childNodeEntries.size() * 300 + + propertyNames.size() * 250; + } + + /** + * Notify the listeners that a child node entry has been added + * @param added the entry that was added + */ + protected void notifyNodeAdded(ChildNodeEntry added) { + if (listener != null) { + listener.nodeAdded(this, added.getName(), added.getIndex(), added.getId()); + } + } + + /** + * Notify the listeners that the child node entries have been replaced + */ + protected void notifyNodesReplaced() { + if (listener != null) { + listener.nodesReplaced(this); + } + } + + /** + * Notify the listeners that a child node entry has been removed + * @param removed the entry that was removed + */ + protected void notifyNodeRemoved(ChildNodeEntry removed) { + if (listener != null) { + listener.nodeRemoved(this, removed.getName(), removed.getIndex(), removed.getId()); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateListener.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateListener.java new file mode 100644 index 00000000000..114d9c4d14a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateListener.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.core.state; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.id.NodeId; + +/** + * Extends the ItemStateListener allowing a client to be + * additionally informed about changes on a NodeState. + */ +public interface NodeStateListener extends ItemStateListener { + + /** + * Called when a child node has been added + * + * @param state node state that changed + * @param name name of node that was added + * @param index index of new node + * @param id id of new node + */ + void nodeAdded(NodeState state, + Name name, int index, NodeId id); + + /** + * Called when a node has been modified, typically as a result of removal + * or addition of a child node. + *

    + * Please note, that this method is not called if + * {@link #stateModified(ItemState)} was called. + * + * @param state node state that changed + */ + void nodeModified(NodeState state); + + /** + * Called when the children nodes were replaced by other nodes, typically + * as result of a reorder operation. + * + * @param state node state that changed + */ + void nodesReplaced(NodeState state); + + /** + * Called when a child node has been removed + * + * @param state node state that changed + * @param name name of node that was removed + * @param index index of removed node + * @param id id of removed node + */ + void nodeRemoved(NodeState state, Name name, int index, NodeId id); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateMerger.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateMerger.java new file mode 100644 index 00000000000..c989a72233d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/NodeStateMerger.java @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +import javax.jcr.nodetype.NoSuchNodeTypeException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * Internal utility class used for merging concurrent changes that occurred + * on different NodeState instances representing the same node. + *

    + * See http://issues.apache.org/jira/browse/JCR-584. + */ +class NodeStateMerger { + + /** + * Tries to silently merge the given state with its + * externally (e.g. through another session) modified overlayed state + * in order to avoid an InvalidItemStateException. + *

    + * See http://issues.apache.org/jira/browse/JCR-584. + * See also http://issues.apache.org/jira/browse/JCR-3290. + * + * @param state node state whose modified overlayed state should be + * merged + * @param context used for analyzing the context of the modifications + * @return true if the changes could be successfully merged into the + * given node state; false otherwise + */ + static boolean merge(NodeState state, MergeContext context) { + + NodeState overlayedState = (NodeState) state.getOverlayedState(); + if (overlayedState == null + || state.getModCount() == overlayedState.getModCount()) { + return false; + } + + synchronized (overlayedState) { + synchronized (state) { + /** + * some examples for trivial non-conflicting changes: + * - s1 added child node a, s2 removes child node b + * - s1 adds child node a, s2 adds child node b + * - s1 adds child node a, s2 adds mixin type + * + * conflicting changes causing staleness: + * - s1 added non-sns child node or property a, + * s2 added non-sns child node or property a => name clash + * - either session reordered child nodes + * (some combinations could possibly be merged) + * - either session moved node + */ + + // compare current state with externally modified overlayed + // state and determine what has been changed by whom + + // child node entries order + if (!state.getReorderedChildNodeEntries().isEmpty()) { + // for now we don't even try to merge the result of + // a reorder operation + return false; + } + + // the primary node type + if (!state.getNodeTypeName().equals(overlayedState.getNodeTypeName())) { + // the primary node type has changed either in 'state' or 'overlayedState'. + return false; + } + + // mixin types + if (!mergeMixinTypes(state, overlayedState, context)) { + return false; + } + + // parent id + if (state.getParentId() != null + && !state.getParentId().equals(overlayedState.getParentId())) { + return false; + } + + // child node entries + if (!state.getChildNodeEntries().equals( + overlayedState.getChildNodeEntries())) { + ArrayList added = new ArrayList(); + ArrayList removed = new ArrayList(); + + for (ChildNodeEntry cne : state.getAddedChildNodeEntries()) { + // locally added or moved? + if (context.isAdded(cne.getId()) || (context.isModified(cne.getId()) && isParent(state, cne, context))) { + // a new child node entry has been added to this state; + // check for name collisions with other state + if (overlayedState.hasChildNodeEntry(cne.getName())) { + // conflicting names + if (cne.getIndex() < 2) { + // check if same-name siblings are allowed + if (!context.allowsSameNameSiblings(cne.getId())) { + return false; + } + } + // assume same-name siblings are allowed since index is >= 2 + } + + added.add(cne); + } + } + + for (ChildNodeEntry cne : state.getRemovedChildNodeEntries()) { + // locally removed? + if (context.isDeleted(cne.getId()) || context.isModified(cne.getId())) { + // a child node entry has been removed from this node state + removed.add(cne); + } + } + + // copy child node entries from other state and + // re-apply changes made on this state + state.setChildNodeEntries(overlayedState.getChildNodeEntries()); + for (ChildNodeEntry cne : removed) { + state.removeChildNodeEntry(cne.getId()); + } + for (ChildNodeEntry cne : added) { + state.addChildNodeEntry(cne.getName(), cne.getId()); + } + } + + // property names + if (!state.getPropertyNames().equals( + overlayedState.getPropertyNames())) { + HashSet added = new HashSet(); + HashSet removed = new HashSet(); + + for (Name name : state.getAddedPropertyNames()) { + PropertyId propId = + new PropertyId(state.getNodeId(), name); + if (context.isAdded(propId)) { + added.add(name); + } + } + for (Name name : state.getRemovedPropertyNames()) { + PropertyId propId = + new PropertyId(state.getNodeId(), name); + if (context.isDeleted(propId)) { + // a property name has been removed from this state + removed.add(name); + } + } + + // copy property names from other and + // re-apply changes made on this state + state.setPropertyNames(overlayedState.getPropertyNames()); + for (Name name : added) { + state.addPropertyName(name); + } + for (Name name : removed) { + state.removePropertyName(name); + } + } + + // finally sync modification count + state.setModCount(overlayedState.getModCount()); + + return true; + } + } + } + + /** + * + * @param state + * @param overlayedState + * @return true if the mixin type names are the same in both node states or + * if the mixin modifications do not conflict (and could be merged); false + * otherwise. + */ + private static boolean mergeMixinTypes(NodeState state, NodeState overlayedState, MergeContext ctx) { + Set mixins = new HashSet(state.getMixinTypeNames()); + Set overlayedMixins = new HashSet(overlayedState.getMixinTypeNames()); + if (mixins.equals(overlayedMixins)) { + // no net effect modifications at all -> merge child items defined + // by the mixins according to the general rule. + return true; + } + + PropertyId mixinPropId = new PropertyId(state.getNodeId(), NameConstants.JCR_MIXINTYPES); + + boolean mergeDone; + if (ctx.isAdded(mixinPropId)) { + // jcr:mixinTypes property was created for 'state'. + // changes is safe (without need to merge), + // - overlayed state doesn't have any mixins OR + // - overlayed state got the same (or a subset) added + // and non of the items defined by the new mixin(s) collides with + // existing items on the overlayed state + if (overlayedMixins.isEmpty() || mixins.containsAll(overlayedMixins)) { + mixins.removeAll(overlayedMixins); + mergeDone = !conflicts(state, mixins, ctx, true); + } else { + // different mixins added in overlayedState and state + // -> don't merge + mergeDone = false; + } + } else if (ctx.isDeleted(mixinPropId)) { + // jcr:mixinTypes property was removed in 'state'. + // we can't determine if there was any change to mixin types in the + // overlayed state. + // -> don't merge. + mergeDone = false; + } else if (ctx.isModified(mixinPropId)) { + /* jcr:mixinTypes property was modified in 'state'. + NOTE: if the mixins of the overlayed state was modified as well + the property (jcr:mixinTypes) cannot not be persisted (stale). + + since there is not way to determine if the overlayed mixins have + been modified just check for conflicts related to a net mixin + addition. + */ + if (mixins.containsAll(overlayedMixins)) { + // net result of modifications is only addition. + // -> so far the changes are save if there are no conflicts + // caused by mixins modification in 'state'. + // NOTE: the save may still fail if the mixin property has + // been modified in the overlayed state as well. + mixins.removeAll(overlayedMixins); + mergeDone = !conflicts(state, mixins, ctx, true); + } else { + // net result is either a removal in 'state' or modifications + // in both node states. + // -> don't merge. + mergeDone = false; + } + } else { + // jcr:mixinTypes property was added or modified in the overlayed + // state but neither added nor modified in 'state'. + if (overlayedMixins.containsAll(mixins)) { + // the modification in the overlayed state only includes the + // addition of mixin node types, but no removal. + // -> need to check if any added items from state would + // collide with the items defined by the new mixin on the + // overlayed state. + overlayedMixins.removeAll(mixins); + if (!conflicts(state, overlayedMixins, ctx, false)) { + // update the mixin names in 'state'. the child items defined + // by the new mixins will be added later on during merge of + // child nodes and properties. + state.setMixinTypeNames(overlayedMixins); + mergeDone = true; + } else { + mergeDone = false; + } + } else { + // either remove-mixin(s) or both add and removal of mixin in + // the overlayed state. + // -> we cannot merge easily + mergeDone = false; + } + } + + return mergeDone; + } + + /** + * + * @param state The state of the node to be saved. + * @param addedMixins The added mixins to be used for testing + * @param ctx + * @param compareToOverlayed + * @return true if a conflict can be determined, false otherwise. + */ + private static boolean conflicts(NodeState state, + Set addedMixins, + MergeContext ctx, boolean compareToOverlayed) { + try { + // check for all added mixin types in one state if there are colliding + // child items in the other state. + // this is currently a simple check for named item definitions; + // if the mixin defines residual item definitions -> return false. + for (Name mixinName : addedMixins) { + EffectiveNodeType ent = ctx.getEffectiveNodeType(mixinName); + + if (ent.getUnnamedItemDefs().length > 0) { + // the mixin defines residual child definitions -> cannot + // easily determine conflicts + return false; + } + + NodeState overlayed = (NodeState) state.getOverlayedState(); + for (ChildNodeEntry cne : state.getChildNodeEntries()) { + if (ent.getNamedNodeDefs(cne.getName()).length > 0) { + if (ctx.isAdded(cne.getId()) || isAutoCreated(cne, ent)) { + if (!compareToOverlayed || overlayed.hasChildNodeEntry(cne.getName())) { + return true; + } + } // else: neither added nor autocreated in 'state' . + + } // else: child node not defined by the added mixin type + } + + for (Name propName : state.getPropertyNames()) { + if (ent.getNamedPropDefs(propName).length > 0) { + PropertyId pid = new PropertyId(state.getNodeId(), propName); + if (ctx.isAdded(pid) || isAutoCreated(propName, ent)) { + if (!compareToOverlayed || overlayed.hasPropertyName(propName)) { + return true; + } + } // else: neither added nor autocreated in 'state' + } // else: property not defined by added mixin + } + } + } catch (NoSuchNodeTypeException e) { + // unable to determine collision + return true; + } + + // no conflict detected + return false; + } + + private static boolean isParent(NodeState state, ChildNodeEntry entry, MergeContext context) { + try { + return state.getId().equals(context.getNodeState(entry.getId()).getParentId()); + } catch (ItemStateException e) { + return false; + } + } + + private static boolean isAutoCreated(ChildNodeEntry cne, EffectiveNodeType ent) { + for (QNodeDefinition def : ent.getAutoCreateNodeDefs()) { + if (def.getName().equals(cne.getName())) { + return true; + } + } + return false; + } + + private static boolean isAutoCreated(Name propertyName, EffectiveNodeType ent) { + for (QPropertyDefinition def : ent.getAutoCreatePropDefs()) { + if (def.getName().equals(propertyName)) { + return true; + } + } + return false; + } + + //-----------------------------------------------------< inner interfaces > + + /** + * The context of a modification. + */ + static interface MergeContext { + boolean isAdded(ItemId id); + boolean isDeleted(ItemId id); + boolean isModified(ItemId id); + boolean allowsSameNameSiblings(NodeId id); + EffectiveNodeType getEffectiveNodeType(Name ntName) throws NoSuchNodeTypeException; + NodeState getNodeState(NodeId id) throws ItemStateException; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/PropertyState.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/PropertyState.java new file mode 100644 index 00000000000..094833273cd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/PropertyState.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.core.state; + +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; + +import javax.jcr.PropertyType; + +/** + * PropertyState represents the state of a Property. + */ +public class PropertyState extends ItemState { + + /** + * the id of this property state + */ + private PropertyId id; + + /** + * the internal values + */ + private InternalValue[] values; + + /** + * the type of this property state + */ + private int type; + + /** + * flag indicating if this is a multivalue property + */ + private boolean multiValued; + + /** + * Constructs a new property state that is initially connected to an + * overlayed state. + * + * @param overlayedState the backing property state being overlayed + * @param initialStatus the initial status of the property state object + * @param isTransient flag indicating whether this state is transient or not + */ + public PropertyState(PropertyState overlayedState, int initialStatus, + boolean isTransient) { + super(overlayedState, initialStatus, isTransient); + pull(); + } + + /** + * Create a new PropertyState + * + * @param id id of the property + * @param initialStatus the initial status of the property state object + * @param isTransient flag indicating whether this state is transient or not + */ + public PropertyState(PropertyId id, int initialStatus, boolean isTransient) { + super(initialStatus, isTransient); + this.id = id; + type = PropertyType.UNDEFINED; + values = InternalValue.EMPTY_ARRAY; + multiValued = false; + } + + //-------------------------------------------------------< public methods > + /** + * {@inheritDoc} + */ + public synchronized void copy(ItemState state, boolean syncModCount) { + synchronized (state) { + PropertyState propState = (PropertyState) state; + id = propState.id; + type = propState.type; + values = propState.values; + multiValued = propState.multiValued; + if (syncModCount) { + setModCount(state.getModCount()); + } + } + } + + /** + * Determines if this item state represents a node. + * + * @return always false + * @see ItemState#isNode + */ + public boolean isNode() { + return false; + } + + /** + * {@inheritDoc} + */ + public ItemId getId() { + return id; + } + + /** + * Returns the identifier of this property. + * + * @return the id of this property. + */ + public PropertyId getPropertyId() { + return id; + } + + /** + * {@inheritDoc} + */ + public NodeId getParentId() { + return id.getParentId(); + } + + /** + * Returns the name of this property. + * + * @return the name of this property. + */ + public Name getName() { + return id.getName(); + } + + /** + * Sets the type of this property. + * + * @param type the type to be set + * @see PropertyType + */ + public void setType(int type) { + this.type = type; + } + + /** + * Sets the flag indicating whether this property is multi-valued. + * + * @param multiValued flag indicating whether this property is multi-valued + */ + public void setMultiValued(boolean multiValued) { + this.multiValued = multiValued; + } + + /** + * Returns the type of this property. + * + * @return the type of this property. + * @see PropertyType + */ + public int getType() { + return type; + } + + /** + * Returns true if this property is multi-valued, otherwise false. + * + * @return true if this property is multi-valued, otherwise false. + */ + public boolean isMultiValued() { + return multiValued; + } + + /** + * Sets the value(s) of this property. + * + * @param values the new values + */ + public void setValues(InternalValue[] values) { + this.values = values; + } + + /** + * Returns the value(s) of this property. + * + * @return the value(s) of this property. + */ + public InternalValue[] getValues() { + return values; + } + + /** + * Returns an estimate of the memory size of this property state. The + * return value actually highly overestimates the amount of required + * memory, but changing the estimates would likely cause OOMs in many + * downstream deployments that have set their cache sizes based on + * experience with these erroneous size estimates. So we don't change + * the formula used by this method. + */ + @Override + public long calculateMemoryFootprint() { + return 350 + values.length * 100; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SessionItemStateManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SessionItemStateManager.java new file mode 100644 index 00000000000..21a1ee3891b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SessionItemStateManager.java @@ -0,0 +1,1007 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.LinkedList; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.CachingHierarchyManager; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.ZombieHierarchyManager; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Item state manager that handles both transient and persistent items. + */ +public class SessionItemStateManager + implements UpdatableItemStateManager, NodeStateListener { + + private static Logger log = LoggerFactory.getLogger(SessionItemStateManager.class); + + /** + * State manager that allows updates + */ + private final LocalItemStateManager stateMgr; + + /** + * Hierarchy manager + */ + private CachingHierarchyManager hierMgr; + + /** + * map of those states that have been removed transiently + */ + private final Map atticStore = + Collections.synchronizedMap(new HashMap()); + + /** + * map of new or modified transient states + */ + private final Map transientStore = + Collections.synchronizedMap(new HashMap()); + + /** + * ItemStateManager view of the states in the attic; lazily instantiated + * in {@link #getAttic()} + */ + private AtticItemStateManager attic; + + /** + * State change dispatcher. + */ + private final transient StateChangeDispatcher dispatcher = new StateChangeDispatcher(); + + /** + * Creates a new SessionItemStateManager instance. + * + * @param rootNodeId the root node id + * @param stateMgr the local item state manager + */ + public SessionItemStateManager( + NodeId rootNodeId, LocalItemStateManager stateMgr) { + this.stateMgr = stateMgr; + + // create hierarchy manager that uses both transient and persistent state + hierMgr = new CachingHierarchyManager(rootNodeId, this); + addListener(hierMgr); + } + + /** + * Returns the hierarchy manager + * + * @return the hierarchy manager + */ + public HierarchyManager getHierarchyMgr() { + return hierMgr; + } + + /** + * Returns an attic-aware hierarchy manager, i.e. an hierarchy manager that + * is also able to build/resolve paths of those items that have been moved + * or removed (i.e. moved to the attic). + * + * @return an attic-aware hierarchy manager + */ + public HierarchyManager getAtticAwareHierarchyMgr() { + return new ZombieHierarchyManager(hierMgr, this, getAttic()); + } + + //--------------------------------------------------------------< Object > + + /** + * {@inheritDoc} + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("SessionItemStateManager (" + super.toString() + ")\n"); + builder.append("[transient]\n"); + builder.append(transientStore); + builder.append("[attic]\n"); + builder.append(atticStore); + return builder.toString(); + } + + //-----------------------------------------------------< ItemStateManager > + + /** + * {@inheritDoc} + */ + public ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + // first check if the specified item has been transiently removed + if (atticStore.containsKey(id)) { + /** + * check if there's new transient state for the specified item + * (e.g. if a property with name 'x' has been removed and a new + * property with same name has been created); + * this will throw a NoSuchItemStateException if there's no new + * transient state + */ + return getTransientItemState(id); + } + + // check if there's transient state for the specified item + if (transientStore.containsKey(id)) { + return getTransientItemState(id); + } + + return stateMgr.getItemState(id); + } + + /** + * {@inheritDoc} + */ + public boolean hasItemState(ItemId id) { + // first check if the specified item has been transiently removed + if (atticStore.containsKey(id)) { + /** + * check if there's new transient state for the specified item + * (e.g. if a property with name 'x' has been removed and a new + * property with same name has been created); + */ + return transientStore.containsKey(id); + } + // check if there's transient state for the specified item + if (transientStore.containsKey(id)) { + return true; + } + // check if there's persistent state for the specified item + return stateMgr.hasItemState(id); + } + + /** + * {@inheritDoc} + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + return stateMgr.getNodeReferences(id); + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + return stateMgr.hasNodeReferences(id); + } + + //--------------------------------------------< UpdatableItemStateManager > + + /** + * {@inheritDoc} + */ + public void edit() throws IllegalStateException { + stateMgr.edit(); + } + + /** + * {@inheritDoc} + */ + public boolean inEditMode() { + return stateMgr.inEditMode(); + } + + /** + * {@inheritDoc} + */ + public NodeState createNew( + NodeId id, Name nodeTypeName, NodeId parentId) + throws RepositoryException { + return stateMgr.createNew(id, nodeTypeName, parentId); + } + + /** + * {@inheritDoc} + */ + public PropertyState createNew(Name propName, NodeId parentId) + throws IllegalStateException { + return stateMgr.createNew(propName, parentId); + } + + /** + * Customized variant of {@link #createNew(Name, NodeId)} that + * connects the newly created persistent state with the transient state. + */ + public PropertyState createNew(PropertyState transientState) + throws ItemStateException { + PropertyState persistentState = createNew(transientState.getName(), + transientState.getParentId()); + transientState.connect(persistentState); + return persistentState; + } + + + /** + * {@inheritDoc} + */ + public void store(ItemState state) throws IllegalStateException { + stateMgr.store(state); + } + + /** + * {@inheritDoc} + */ + public void destroy(ItemState state) throws IllegalStateException { + assert state != null; + stateMgr.destroy(state); + } + + /** + * {@inheritDoc} + */ + public void cancel() throws IllegalStateException { + stateMgr.cancel(); + } + + /** + * {@inheritDoc} + */ + public void update() + throws ReferentialIntegrityException, StaleItemStateException, + ItemStateException, IllegalStateException { + stateMgr.update(); + } + + /** + * {@inheritDoc} + */ + public void dispose() { + // remove hierarchy manager as listener to avoid + // unnecessary work during stateMgr.dispose() + removeListener(hierMgr); + // discard all transient changes + disposeAllTransientItemStates(); + } + + //< more methods for listing and retrieving transient ItemState instances > + + /** + * @param id + * @return + * @throws NoSuchItemStateException + * @throws ItemStateException + */ + public ItemState getTransientItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + ItemState state = transientStore.get(id); + if (state != null) { + return state; + } else { + throw new NoSuchItemStateException(id.toString()); + } + } + + /** + * + * @param id + * @return + */ + public boolean hasTransientItemState(ItemId id) { + return transientStore.containsKey(id); + } + + /** + * + * @param id + * @return + */ + public boolean hasTransientItemStateInAttic(ItemId id) { + return atticStore.containsKey(id); + } + + /** + * @return true if this manager has any transient state; + * false otherwise. + */ + public boolean hasAnyTransientItemStates() { + return !transientStore.isEmpty(); + } + + /** + * Returns a collection of those transient item state instances that are + * direct or indirect descendants of the item state with the given parent. + * The transient item state instance with the given identifier itself + * (if there is such) will not be included. + *

    + * The instances are returned in depth-first tree traversal order. + * + * @param id identifier of the common parent of the transient item state + * instances to be returned + * @return collection of descendant transient item state instances + * @throws InvalidItemStateException if any descendant item state has been + * deleted externally + * @throws RepositoryException if another error occurs + */ + public Collection getDescendantTransientItemStates(ItemId id) + throws InvalidItemStateException, RepositoryException { + try { + return getDescendantItemStates( + id, transientStore, getAtticAwareHierarchyMgr()); + } catch (ItemNotFoundException e) { + // one of the parents of the specified item has been + // removed externally; as we don't know its path, + // we can't determine if it is a descendant; + // InvalidItemStateException should only be thrown if + // a descendant is affected; + // => throw InvalidItemStateException for now (FIXME) + // unable to determine relative depth, assume that the item + // (or any of its ancestors) has been removed externally + throw new InvalidItemStateException( + "Item seems to have been removed externally", e); + } + } + + /** + * Same as {@link #getDescendantTransientItemStates(ItemId)} + * except that item state instances in the attic are returned. + * + * @param id identifier of the common parent of the transient item state + * instances to be returned + * @return collection of descendant transient item state instances + * in the attic + */ + public Iterable getDescendantTransientItemStatesInAttic( + ItemId id) throws RepositoryException { + return getDescendantItemStates( + id, atticStore, + new ZombieHierarchyManager(hierMgr, this, getAttic())); + } + + /** + * Utility method used by the + * {@link #getDescendantTransientItemStates(ItemId)} and + * {@link #getDescendantTransientItemStatesInAttic(ItemId)} methods + * to collect descendant item states from the given item state store. + * + * @param id identifier of the parent item + * @param store item state store + * @param hierarchyManager hierarchy manager associated with the store + * @return descendants of the identified item + * @throws RepositoryException if the descendants could not be accessed + */ + private List getDescendantItemStates( + ItemId id, Map store, + HierarchyManager hierarchyManager) throws RepositoryException { + if (id.denotesNode() && !store.isEmpty()) { + // Group the descendants by reverse relative depth + SortedMap> statesByReverseDepth = + new TreeMap>(); + ItemState[] states = store.values().toArray(new ItemState[0]); + for (ItemState state : states) { + // determine relative depth: > 0 means it's a descendant + int depth = hierarchyManager.getShareRelativeDepth( + (NodeId) id, state.getId()); + if (depth > 0) { + Collection statesAtDepth = + statesByReverseDepth.get(-depth); + if (statesAtDepth == null) { + statesAtDepth = new ArrayList(); + statesByReverseDepth.put(-depth, statesAtDepth); + } + statesAtDepth.add(state); + } + } + + // Collect the descendants in decreasing depth order + List descendants = new ArrayList(); + for (Collection statesAtDepth + : statesByReverseDepth.values()) { + descendants.addAll(statesAtDepth); + } + return descendants; + } else { + return Collections.emptyList(); + } + } + + /** + * Returns the id of the root of the minimal subtree including all + * transient states. + * + * @return id of nearest common ancestor of all transient states or null + * if there's no transient state. + * @throws RepositoryException if an error occurs + */ + public NodeId getIdOfRootTransientNodeState() throws RepositoryException { + if (transientStore.isEmpty()) { + return null; + } + + // short cut + if (transientStore.containsKey(hierMgr.getRootNodeId())) { + return hierMgr.getRootNodeId(); + } + + // the nearest common ancestor of all transient states + // must be either + // a) a node state with STATUS_EXISTING_MODIFIED or STATUS_STALE_DESTROYED, or + // b) the parent node of a property state with STATUS_EXISTING_MODIFIED or STATUS_STALE_DESTROYED + + // collect all candidates based on above criteria + Collection candidateIds = new LinkedList(); + try { + HierarchyManager hierMgr = getHierarchyMgr(); + ItemState[] states = + transientStore.values().toArray(new ItemState[0]); + for (ItemState state : states) { + if (state.getStatus() == ItemState.STATUS_EXISTING_MODIFIED + || state.getStatus() == ItemState.STATUS_STALE_DESTROYED) { + NodeId nodeId; + if (state.isNode()) { + nodeId = (NodeId) state.getId(); + } else { + nodeId = state.getParentId(); + } + // remove any descendant candidates + boolean skip = false; + for (Iterator it = candidateIds.iterator(); it.hasNext();) { + NodeId id = it.next(); + if (nodeId.equals(id) || hierMgr.isAncestor(id, nodeId)) { + // already a candidate or a descendant thereof + // => skip + skip = true; + break; + } + if (hierMgr.isAncestor(nodeId, id)) { + // candidate is a descendant => remove + it.remove(); + } + } + if (!skip) { + // add to candidates + candidateIds.add(nodeId); + } + } + } + + if (candidateIds.size() == 1) { + return candidateIds.iterator().next(); + } + + // pick (any) candidate with shortest path to start with + NodeId candidateId = null; + for (NodeId id : candidateIds) { + if (candidateId == null) { + candidateId = id; + } else { + if (hierMgr.getDepth(id) < hierMgr.getDepth(candidateId)) { + candidateId = id; + } + } + } + + // starting with this candidate closest to root, find first parent + // which is an ancestor of all candidates + NodeState state = (NodeState) getItemState(candidateId); + NodeId parentId = state.getParentId(); + boolean continueWithParent = false; + while (parentId != null) { + for (NodeId id : candidateIds) { + if (hierMgr.getRelativeDepth(parentId, id) == -1) { + continueWithParent = true; + break; + } + } + if (continueWithParent) { + state = (NodeState) getItemState(parentId); + parentId = state.getParentId(); + continueWithParent = false; + } else { + break; + } + } + return parentId; + } catch (ItemStateException e) { + throw new RepositoryException("failed to determine common root of transient changes", e); + } + } + + /** + * Return a flag indicating whether the specified item is in the transient + * item state manager's attic space. + * + * @param id item id + * @return true if the item state is in the attic space; + * false otherwise + */ + public boolean isItemStateInAttic(ItemId id) { + return atticStore.containsKey(id); + } + + //------< methods for creating & discarding transient ItemState instances > + + /** + * @param id + * @param nodeTypeName + * @param parentId + * @param initialStatus + * @return + * @throws RepositoryException + */ + public NodeState createTransientNodeState(NodeId id, Name nodeTypeName, NodeId parentId, int initialStatus) + throws RepositoryException { + if (initialStatus == ItemState.STATUS_NEW && id != null + && hasItemState(id)) { + throw new InvalidItemStateException( + "Node " + id + " already exists"); + } + + // check map; synchronized to ensure an entry is not created twice. + synchronized (transientStore) { + if (id == null) { + id = stateMgr.getNodeIdFactory().newNodeId(); + } else if (transientStore.containsKey(id)) { + throw new RepositoryException( + "There is already a transient state for node " + id); + } + + NodeState state = new NodeState( + id, nodeTypeName, parentId, initialStatus, true); + // put transient state in the map + transientStore.put(state.getId(), state); + state.setContainer(this); + return state; + } + } + + /** + * @param overlayedState + * @param initialStatus + * @return + * @throws ItemStateException + */ + public NodeState createTransientNodeState(NodeState overlayedState, int initialStatus) + throws ItemStateException { + + ItemId id = overlayedState.getNodeId(); + + // check map; synchronized to ensure an entry is not created twice. + synchronized (transientStore) { + if (transientStore.containsKey(id)) { + String msg = "there's already a node state instance with id " + id; + log.debug(msg); + throw new ItemStateException(msg); + } + + NodeState state = new NodeState(overlayedState, initialStatus, true); + // put transient state in the map + transientStore.put(id, state); + state.setContainer(this); + return state; + } + } + + /** + * @param parentId + * @param propName + * @param initialStatus + * @return + * @throws ItemStateException + */ + public PropertyState createTransientPropertyState(NodeId parentId, Name propName, int initialStatus) + throws ItemStateException { + + PropertyId id = new PropertyId(parentId, propName); + + // check map; synchronized to ensure an entry is not created twice. + synchronized (transientStore) { + if (transientStore.containsKey(id)) { + String msg = "there's already a property state instance with id " + id; + log.debug(msg); + throw new ItemStateException(msg); + } + + PropertyState state = new PropertyState(id, initialStatus, true); + // put transient state in the map + transientStore.put(id, state); + state.setContainer(this); + return state; + } + } + + /** + * @param overlayedState + * @param initialStatus + * @return + * @throws ItemStateException + */ + public PropertyState createTransientPropertyState(PropertyState overlayedState, int initialStatus) + throws ItemStateException { + + PropertyId id = overlayedState.getPropertyId(); + + // check map; synchronized to ensure an entry is not created twice. + synchronized (transientStore) { + if (transientStore.containsKey(id)) { + String msg = "there's already a property state instance with id " + id; + log.debug(msg); + throw new ItemStateException(msg); + } + + PropertyState state = new PropertyState(overlayedState, initialStatus, true); + // put transient state in the map + transientStore.put(id, state); + state.setContainer(this); + return state; + } + } + + /** + * Disconnect a transient item state from its underlying persistent state. + * Notifies the HierarchyManager about the changed identity. + * + * @param state the transient ItemState instance that should + * be disconnected + */ + public void disconnectTransientItemState(ItemState state) { + state.disconnect(); + } + + /** + * Disposes the specified transient item state instance, i.e. discards it + * and clears it from cache. + * + * @param state the transient ItemState instance that should + * be disposed + * @see ItemState#discard() + */ + public void disposeTransientItemState(ItemState state) { + // discard item state, this will invalidate the wrapping Item + // instance of the transient state + state.discard(); + // remove from map + transientStore.remove(state.getId()); + // give the instance a chance to prepare to get gc'ed + state.onDisposed(); + } + + /** + * Transfers the specified transient item state instance from the 'active' + * cache to the attic. + * + * @param state the transient ItemState instance that should + * be moved to the attic + */ + public void moveTransientItemStateToAttic(ItemState state) { + // remove from map + transientStore.remove(state.getId()); + // add to attic + atticStore.put(state.getId(), state); + } + + /** + * Disposes the specified transient item state instance in the attic, i.e. + * discards it and removes it from the attic. + * + * @param state the transient ItemState instance that should + * be disposed @see ItemState#discard() + */ + public void disposeTransientItemStateInAttic(ItemState state) { + // discard item state, this will invalidate the wrapping Item + // instance of the transient state + state.discard(); + // remove from attic + atticStore.remove(state.getId()); + // give the instance a chance to prepare to get gc'ed + state.onDisposed(); + } + + /** + * Disposes all transient item states in the cache and in the attic. + */ + public void disposeAllTransientItemStates() { + // dispose item states in transient map & attic + // (use temp collection to avoid ConcurrentModificationException) + ItemState[] tmp; + tmp = transientStore.values().toArray(new ItemState[0]); + for (ItemState state : tmp) { + disposeTransientItemState(state); + } + tmp = atticStore.values().toArray(new ItemState[0]); + for (ItemState state : tmp) { + disposeTransientItemStateInAttic(state); + } + } + + /** + * Add an ItemStateListener + * + * @param listener the new listener to be informed on modifications + */ + public void addListener(ItemStateListener listener) { + dispatcher.addListener(listener); + } + + /** + * Remove an ItemStateListener + * + * @param listener an existing listener + */ + public void removeListener(ItemStateListener listener) { + dispatcher.removeListener(listener); + } + + /** + * Return the attic item state provider that holds all items + * moved into the attic. + * + * @return attic + */ + public ItemStateManager getAttic() { + if (attic == null) { + attic = new AtticItemStateManager(); + } + return attic; + } + + //----------------------------------------------------< ItemStateListener > + + /** + * {@inheritDoc} + *

    + * Notification handler gets called for both transient states that this state manager + * has created, as well as states that were created by the local state manager + * we're listening to. + */ + public void stateCreated(ItemState created) { + ItemState visibleState = created; + if (created.getContainer() != this) { + // local state was created + ItemState transientState = transientStore.get(created.getId()); + if (transientState != null) { + if (transientState.hasOverlayedState()) { + // underlying state has been permanently created + transientState.pull(); + transientState.setStatus(ItemState.STATUS_EXISTING); + } else { + // this is a notification from another session + try { + ItemState local = stateMgr.getItemState(created.getId()); + transientState.connect(local); + // update mod count + transientState.setModCount(local.getModCount()); + transientState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } catch (ItemStateException e) { + // something went wrong + transientState.setStatus(ItemState.STATUS_UNDEFINED); + } + } + visibleState = transientState; + } + } + boolean notifyTransientSpace; + if (visibleState instanceof NodeState) { + // No need to push "node created" to transient space: + // either the transient already knows about this state, or it doesn't. + // If we notify in this case, this can lead to a deadlock, + // see JCR-3226. + notifyTransientSpace = false; + } else { + notifyTransientSpace = true; + } + if (notifyTransientSpace) { + dispatcher.notifyStateCreated(visibleState); + } + } + + /** + * {@inheritDoc} + *

    + * Notification handler gets called for both transient states that this state manager + * has created, as well as states that were created by the local state manager + * we're listening to. + */ + public void stateModified(ItemState modified) { + ItemState visibleState = modified; + // JCR-2650: ignore external changes, they will be considered/merged on save(). + dispatcher.notifyStateModified(visibleState); + } + + /** + * {@inheritDoc} + *

    + * Notification handler gets called for both transient states that this state manager + * has created, as well as states that were created by the local state manager + * we're listening to. + */ + public void stateDestroyed(ItemState destroyed) { + ItemState visibleState = destroyed; + if (destroyed.getContainer() != this) { + // local state was destroyed + ItemState transientState = transientStore.get(destroyed.getId()); + if (transientState != null) { + transientState.setStatus(ItemState.STATUS_STALE_DESTROYED); + visibleState = transientState; + } else { + // check attic + transientState = atticStore.remove(destroyed.getId()); + if (transientState != null) { + transientState.onDisposed(); + } + } + } + dispatcher.notifyStateDestroyed(visibleState); + } + + /** + * {@inheritDoc} + *

    + * Notification handler gets called for both transient states that this state manager + * has created, as well as states that were created by the local state manager + * we're listening to. + */ + public void stateDiscarded(ItemState discarded) { + ItemState visibleState = discarded; + if (discarded.getContainer() != this) { + // local state was discarded + ItemState transientState = transientStore.get(discarded.getId()); + if (transientState != null) { + transientState.setStatus(ItemState.STATUS_UNDEFINED); + visibleState = transientState; + } + } + dispatcher.notifyStateDiscarded(visibleState); + } + + /** + * {@inheritDoc} + *

    + * Pass notification to listeners if a transient state was modified + * or if the local state is not overlayed. + */ + public void nodeAdded(NodeState state, Name name, int index, NodeId id) { + if (state.getContainer() == this + || !transientStore.containsKey(state.getId())) { + dispatcher.notifyNodeAdded(state, name, index, id); + } + } + + /** + * {@inheritDoc} + *

    + * Pass notification to listeners if a transient state was modified + * or if the local state is not overlayed. + */ + public void nodesReplaced(NodeState state) { + if (state.getContainer() == this + || !transientStore.containsKey(state.getId())) { + dispatcher.notifyNodesReplaced(state); + } + } + + /** + * {@inheritDoc} + *

    + * Pass notification to listeners if a transient state was modified + * or if the local state is not overlayed. + */ + public void nodeModified(NodeState state) { + if (state.getContainer() == this + || !transientStore.containsKey(state.getId())) { + dispatcher.notifyNodeModified(state); + } + } + + /** + * {@inheritDoc} + *

    + * Pass notification to listeners if a transient state was modified + * or if the local state is not overlayed. + */ + public void nodeRemoved(NodeState state, Name name, int index, NodeId id) { + if (state.getContainer() == this + || !transientStore.containsKey(state.getId())) { + dispatcher.notifyNodeRemoved(state, name, index, id); + } + } + + //--------------------------------------------------------< inner classes > + + /** + * ItemStateManager view of the states in the attic + * + * @see SessionItemStateManager#getAttic + */ + private class AtticItemStateManager implements ItemStateManager { + + /** + * {@inheritDoc} + */ + public ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + ItemState state = atticStore.get(id); + if (state != null) { + return state; + } else { + throw new NoSuchItemStateException(id.toString()); + } + } + + /** + * {@inheritDoc} + */ + public boolean hasItemState(ItemId id) { + return atticStore.containsKey(id); + } + + /** + * {@inheritDoc} + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + // n/a + throw new ItemStateException("getNodeReferences() not implemented"); + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + // n/a + return false; + } + } + + /** + * Pushes the given transient state to the change log so it'll be + * persisted when the change log is committed. The transient state + * is replaced with the local state that has been pushed to the + * change log. + * + * @param transientState transient state + * @return the local state to be persisted + * @throws RepositoryException if the transiet state can not be persisted + */ + public NodeState makePersistent(NodeState transientState) + throws RepositoryException { + NodeState localState = stateMgr.getOrCreateLocalState(transientState); + + synchronized (localState) { + // copy state from transient state: + localState.copy(transientState, true); + // make state persistent + store(localState); + } + + // disconnect the transient item state + disconnectTransientItemState(transientState); + + return localState; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SharedItemStateManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SharedItemStateManager.java new file mode 100644 index 00000000000..b109e76ad7a --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/SharedItemStateManager.java @@ -0,0 +1,1917 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.cluster.ClusterException; +import org.apache.jackrabbit.core.cluster.UpdateEventChannel; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.observation.EventStateCollection; +import org.apache.jackrabbit.core.observation.EventStateCollectionFactory; +import org.apache.jackrabbit.core.persistence.CachingPersistenceManager; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shared ItemStateManager (SISM). Caches objects returned from a + * PersistenceManager. Objects returned by this item state + * manager are shared among all sessions. + *

    + * A shared item state manager operates on a PersistenceManager + * (PM) that is used to load and store the item states. Additionally, a SISM can + * have VirtualItemStateProviders (VISP) that are used to provide + * additional, non-persistent, read-only states. Examples of VISP are the + * content representation of the NodeTypes (/jcr:system/jcr:nodeTypes) and the + * version store (/jcr:system/jcr:versionStore). those 2 VISP are added to the + * SISM during initialization of a workspace. i.e. they are 'mounted' to all + * workspaces. we assume, that VISP cannot be added dynamically, neither during + * runtime nor by configuration. + *

    + * The states from the VISP are read-only. by the exception for node references. + * remember that the referrers are stored in a {@link NodeReferences} state, + * having the ID of the target state. + *

    + * there are 5 types of referential relations to be distinguished: + *

      + *
    1. normal --> normal (references from 'normal' states to 'normal' states) + * this is the normal case and will be handled by the SISM. + * + *
    2. normal --> virtual (references from 'normal' states to 'virtual' states) + * those references should be handled by the VISP rather by the SISM. + * + *
    3. virtual --> normal (references from 'virtual' states to 'normal' states) + * such references are not supported. eg. references of versioned nodes do + * not impose any constraints on the referenced nodes. + * + *
    4. virtual --> virtual (references from 'virtual' states to 'virtual' + * states of the same VISP). + * intra-virtual references are handled by the item state manager of the VISP. + * + *
    5. virtual --> virtual' (references from 'virtual' states to 'virtual' + * states of different VISP). + * those do currently not occur and are therefore not supported. + *
    + *

    + * if VISP are not dynamic, there is not risk that NV-type references can dangle + * (since a VISP cannot be 'unmounted', leaving eventual references dangling). + * although multi-workspace-referrers are not explicitly supported, the + * architecture of NodeReferences support multiple referrers with + * the same PropertyId. So the number of references can be tracked (an example + * of multi-workspace-referrers is a version referenced by the jcr:baseVersion + * of several (corresponding) nodes in multiple workspaces). + *

    + * As mentioned, VN-type references should not impose any constraints on the + * referrers (e.g. a normal node referenced by a versioned reference property). + * In case of the version store, the VN-type references are not stored at + * all, but reinforced as NN-type references in the normal states in case of a + * checkout operation. + *

    + * VV-type references should be handled by the respective VISP. they look as + * NN-type references in the scope if the VISP anyway...so no special treatment + * should be necessary. + *

    + * VV'-type references are currently not possible, since the version store and + * virtual node type representation don't allow such references. + */ +public class SharedItemStateManager + implements ItemStateManager, ItemStateListener { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(SharedItemStateManager.class); + + /** + * Flag for enabling hierarchy validation. + * @see JCR-2598 + */ + private static final boolean VALIDATE_HIERARCHY = + Boolean.getBoolean("org.apache.jackrabbit.core.state.validatehierarchy"); + + /** + * cache of weak references to ItemState objects issued by this + * ItemStateManager + */ + private final ItemStateCache cache; + + /** + * Persistence Manager used for loading and storing items + */ + private final PersistenceManager persistMgr; + + /** + * node type registry used for identifying referenceable nodes + */ + private final NodeTypeRegistry ntReg; + + /** + * Flag indicating whether this item state manager uses node references to + * verify integrity of its reference properties. + */ + private final boolean usesReferences; + + /** + * Flag indicating whether this item state manager is checking referential + * integrity when storing modifications. The default is to to check + * for referential integrity. + * Should be changed very carefully by experienced developers only. + * + * @see "https://issues.apache.org/jira/browse/JCR-954" + */ + private boolean checkReferences = true; + + /** + * id of root node + */ + private final NodeId rootNodeId; + + /** + * Virtual item state providers + */ + private VirtualItemStateProvider[] virtualProviders = + new VirtualItemStateProvider[0]; + + /** + * State change dispatcher. + */ + private final transient StateChangeDispatcher dispatcher = new StateChangeDispatcher(); + + /** + * The locking strategy. + */ + private ISMLocking ismLocking; + + /** + * Update event channel. By default this is a dummy channel that simply + * ignores all events (so we don't need to check for null all the time), + * but in clustered environments the + * {@link #setEventChannel(UpdateEventChannel)} method should be called + * during initialization to connect this SISM instance with the cluster. + */ + private UpdateEventChannel eventChannel = new DummyUpdateEventChannel(); + + private final NodeIdFactory nodeIdFactory; + + /** + * Creates a new SharedItemStateManager instance. + * + * @param persistMgr + * @param rootNodeId + * @param ntReg + */ + public SharedItemStateManager(PersistenceManager persistMgr, + NodeId rootNodeId, + NodeTypeRegistry ntReg, + boolean usesReferences, + ItemStateCacheFactory cacheFactory, + ISMLocking locking, + NodeIdFactory nodeIdFactory) + throws ItemStateException { + cache = new ItemStateReferenceCache(cacheFactory); + this.persistMgr = persistMgr; + this.ntReg = ntReg; + this.usesReferences = usesReferences; + this.rootNodeId = rootNodeId; + this.ismLocking = locking; + this.nodeIdFactory = nodeIdFactory; + // create root node state if it doesn't yet exist + if (!hasNonVirtualItemState(rootNodeId)) { + createRootNodeState(rootNodeId, ntReg); + } + ensureActivitiesNode(); + } + + /** + * Enables or disables the referential integrity checking, this + * should be used very carefully by experienced developers only. + * + * @see "https://issues.apache.org/jira/browse/JCR-954" + * @param checkReferences whether to do referential integrity checks + */ + public void setCheckReferences(boolean checkReferences) { + this.checkReferences = checkReferences; + } + + /** + * Set an update event channel + * + * @param eventChannel update event channel + */ + public void setEventChannel(UpdateEventChannel eventChannel) { + this.eventChannel = eventChannel; + } + + /** + * Sets a new locking strategy. + * + * @param ismLocking the locking strategy for this item state manager. + */ + public void setISMLocking(ISMLocking ismLocking) { + if (ismLocking == null) { + throw new NullPointerException(); + } + this.ismLocking = ismLocking; + } + + //-----------------------------------------------------< ItemStateManager > + /** + * {@inheritDoc} + */ + public ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + // check the virtual root ids (needed for overlay) + for (VirtualItemStateProvider virtualProvider : virtualProviders) { + if (virtualProvider.isVirtualRoot(id)) { + return virtualProvider.getItemState(id); + } + } + + Exception ex = null; + ISMLocking.ReadLock readLock = acquireReadLock(id); + try { + // check internal first + return getNonVirtualItemState(id); + } catch (NoSuchItemStateException e) { + // Fall through to virtual state providers. We can afford the + // exception-for-control-flow performance hit here, as almost + // all performance-critical content is non-virtual. With this + // catch we can avoid an extra hasNonVirtualItemState() call. + ex = e; + } finally { + readLock.release(); + } + + // check if there is a virtual state for the specified item + for (VirtualItemStateProvider virtualProvider : virtualProviders) { + if (virtualProvider.hasItemState(id)) { + return virtualProvider.getItemState(id); + } + } + + String message = id.toString(); + throw ex == null ? new NoSuchItemStateException(message) : new NoSuchItemStateException(message, ex); + } + + /** + * {@inheritDoc} + */ + public boolean hasItemState(ItemId id) { + // check the virtual root ids (needed for overlay) + for (VirtualItemStateProvider virtualProvider : virtualProviders) { + if (virtualProvider.isVirtualRoot(id)) { + return true; + } + } + + ISMLocking.ReadLock readLock; + try { + readLock = acquireReadLock(id); + } catch (ItemStateException e) { + return false; + } + + try { + if (cache.isCached(id)) { + return true; + } + + // check if this manager has the item state + if (hasNonVirtualItemState(id)) { + return true; + } + } finally { + readLock.release(); + } + + // otherwise check virtual ones + for (VirtualItemStateProvider virtualProvider : virtualProviders) { + if (virtualProvider.hasItemState(id)) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + ISMLocking.ReadLock readLock = acquireReadLock(id); + try { + // check persistence manager + try { + return persistMgr.loadReferencesTo(id); + } catch (NoSuchItemStateException e) { + // ignore + } + } finally { + readLock.release(); + } + + // check virtual providers + for (VirtualItemStateProvider virtualProvider : virtualProviders) { + try { + return virtualProvider.getNodeReferences(id); + } catch (NoSuchItemStateException e) { + // ignore + } + } + + // throw + throw new NoSuchItemStateException(id.toString()); + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + ISMLocking.ReadLock readLock; + try { + readLock = acquireReadLock(id); + } catch (ItemStateException e) { + return false; + } + try { + // check persistence manager + try { + if (persistMgr.existsReferencesTo(id)) { + return true; + } + } catch (ItemStateException e) { + // ignore + } + } finally { + readLock.release(); + } + + // check virtual providers + for (VirtualItemStateProvider virtualProvider : virtualProviders) { + if (virtualProvider.hasNodeReferences(id)) { + return true; + } + } + + return false; + } + + //----------------------------------------------------< ItemStateListener > + + /** + * {@inheritDoc} + *

    + * Notifications are received for items that this manager created itself or items that are + * managed by one of the virtual providers. + */ + public void stateCreated(ItemState created) { + if (created.getContainer() == this) { + // shared state was created + cache.cache(created); + } + dispatcher.notifyStateCreated(created); + } + + /** + * {@inheritDoc} + *

    + * Notifications are received for items that this manager created itself or items that are + * managed by one of the virtual providers. + */ + public void stateModified(ItemState modified) { + dispatcher.notifyStateModified(modified); + } + + /** + * {@inheritDoc} + *

    + * Notifications are received for items that this manager created itself or items that are + * managed by one of the virtual providers. + */ + public void stateDestroyed(ItemState destroyed) { + if (destroyed.getContainer() == this) { + // shared state was destroyed + cache.evict(destroyed.getId()); + } + dispatcher.notifyStateDestroyed(destroyed); + } + + /** + * {@inheritDoc} + *

    + * Notifications are received for items that this manager created itself or items that are + * managed by one of the virtual providers. + */ + public void stateDiscarded(ItemState discarded) { + if (discarded.getContainer() == this) { + // shared state was discarded + cache.evict(discarded.getId()); + } + dispatcher.notifyStateDiscarded(discarded); + } + + //--------------------------------------------------------------< Object > + + /** + * {@inheritDoc} + */ + public String toString() { + return "SharedItemStateManager (" + super.toString() + ")\n" + + "[referenceCache]\n" + cache; + } + + //-------------------------------------------------< misc. public methods > + /** + * Disposes this SharedItemStateManager and frees resources. + */ + public void dispose() { + // remove virtual item state providers (see JCR-2023) + for (VirtualItemStateProvider virtualProvider : virtualProviders) { + virtualProvider.removeListener(this); + } + virtualProviders = new VirtualItemStateProvider[0]; + + // clear cache + cache.evictAll(); + } + + /** + * Adds a new virtual item state provider. + *

    + * NOTE: This method is not synchronized, because it is called right after + * creation only by the same thread and therefore concurrency issues + * do not occur. Should this ever change, the synchronization status + * has to be re-examined. + * + * @param prov + */ + public void addVirtualItemStateProvider(VirtualItemStateProvider prov) { + VirtualItemStateProvider[] provs = + new VirtualItemStateProvider[virtualProviders.length + 1]; + System.arraycopy(virtualProviders, 0, provs, 0, virtualProviders.length); + provs[virtualProviders.length] = prov; + virtualProviders = provs; + + prov.addListener(this); + } + + /** + * Object representing a single update operation. + */ + class Update implements org.apache.jackrabbit.core.cluster.Update { + + /** + * Attribute name used to store the size of the update. + */ + private static final String ATTRIBUTE_UPDATE_SIZE = "updateSize"; + + /** + * Local change log. + */ + private final ChangeLog local; + + /** + * Event state collection factory. + */ + private final EventStateCollectionFactory factory; + + /** + * Virtual provider containing references to be left out when updating + * references. + */ + private final VirtualItemStateProvider virtualProvider; + + /** + * Shared change log. + */ + private ChangeLog shared; + + /** + * Virtual node references. + */ + private ChangeLog[] virtualNodeReferences; + + /** + * Events to dispatch. + */ + private EventStateCollection events; + + /** + * The write lock we currently hold or null if none is + * hold. + */ + private ISMLocking.WriteLock writeLock; + + /** + * Map of attributes stored for this update operation. + */ + private HashMap attributes; + + /** + * Timestamp when this update was created. + */ + private long timestamp = System.currentTimeMillis(); + + /** + * Create a new instance of this class. + */ + public Update(ChangeLog local, EventStateCollectionFactory factory, + VirtualItemStateProvider virtualProvider) { + this.local = local; + this.factory = factory; + this.virtualProvider = virtualProvider; + } + + /** + * Begin update operation. Prepares everything upto the point where + * the persistence manager's store method may be invoked. + * If this method succeeds, a write lock will have been acquired on the + * item state manager and either {@link #end()} or {@link #cancel()} has + * to be called in order to release it. + */ + public void begin() throws ItemStateException, ReferentialIntegrityException { + shared = new ChangeLog(); + + virtualNodeReferences = new ChangeLog[virtualProviders.length]; + + // let listener know about change + try { + eventChannel.updateCreated(this); + } catch (ClusterException e) { + throw new ItemStateException(e.getMessage(), e); + } + + try { + writeLock = acquireWriteLock(local); + } finally { + if (writeLock == null) { + eventChannel.updateCancelled(this); + } + } + + boolean succeeded = false; + + try { + if (usesReferences) { + // Update node references based on modifications in change + // log (added/modified/removed REFERENCE properties) + updateReferences(); + } + + // If enabled, check whether reference targets + // exist/were not removed + if (checkReferences) { + checkReferentialIntegrity(); + } + + /** + * prepare the events. this needs to be after the referential + * integrity check, since another transaction could have modified + * the states. + */ + try { + events = factory.createEventStateCollection(); + } catch (RepositoryException e) { + String msg = "Unable to create event state collection."; + log.error(msg); + throw new ItemStateException(msg, e); + } + + /** + * Reconnect all items contained in the change log to their + * respective shared item and add the shared items to a + * new change log. + */ + for (ItemState state : local.modifiedStates()) { + state.connect(getItemState(state.getId())); + if (state.isStale()) { + boolean merged = false; + if (state.isNode()) { + NodeStateMerger.MergeContext context = + new NodeStateMerger.MergeContext() { + public boolean isAdded(ItemId id) { + try { + ItemState is = local.get(id); + return is != null + && is.getStatus() == ItemState.STATUS_NEW; + } catch (NoSuchItemStateException e) { + return false; + } + } + + public boolean isDeleted(ItemId id) { + return local.deleted(id); + } + + public boolean isModified(ItemId id) { + return local.isModified(id); + } + + public boolean allowsSameNameSiblings(NodeId id) { + try { + NodeState ns = getNodeState(id); + NodeState parent = getNodeState(ns.getParentId()); + Name name = parent.getChildNodeEntry(id).getName(); + EffectiveNodeType ent = ntReg.getEffectiveNodeType( + parent.getNodeTypeName(), + parent.getMixinTypeNames()); + QNodeDefinition def = ent.getApplicableChildNodeDef(name, ns.getNodeTypeName(), ntReg); + return def != null ? def.allowsSameNameSiblings() : false; + } catch (Exception e) { + log.warn("Unable to get node definition", e); + return false; + } + } + + public EffectiveNodeType getEffectiveNodeType(Name ntName) throws NoSuchNodeTypeException { + return ntReg.getEffectiveNodeType(ntName); + } + + public NodeState getNodeState(NodeId id) + throws ItemStateException { + if (local.has(id)) { + return (NodeState) local.get(id); + } else { + return (NodeState) getItemState(id); + } + } + }; + + merged = NodeStateMerger.merge((NodeState) state, context); + } + if (!merged) { + String msg = state.getId() + " has been modified externally"; + log.debug(msg); + throw new StaleItemStateException(msg); + } + // merge succeeded, fall through + } + + // update modification count (will be persisted as well) + state.getOverlayedState().touch(); + + shared.modified(state.getOverlayedState()); + } + Iterator deleted = local.deletedStates().iterator(); + while (deleted.hasNext()) { + ItemState state = deleted.next(); + try { + state.connect(getItemState(state.getId())); + if (state.isStale()) { + String msg = state.getId() + " has been modified externally"; + log.debug(msg); + throw new StaleItemStateException(msg); + } + shared.deleted(state.getOverlayedState()); + } catch (NoSuchItemStateException e) { + // item state was already deleted externally + deleted.remove(); + } + } + for (ItemState state : local.addedStates()) { + if (state.isNode() && state.getStatus() != ItemState.STATUS_NEW) { + // another node with same id had been created + // in the meantime, probably caused by mid-air collision + // of concurrent versioning operations (JCR-2272) + String msg = state.getId() + + " has been created externally (status " + + state.getStatus() + ")"; + log.debug(msg); + throw new StaleItemStateException(msg); + } + state.connect(createInstance(state)); + shared.added(state.getOverlayedState()); + } + + // filter out virtual node references for later processing + // (see comment above) + for (NodeReferences refs : local.modifiedRefs()) { + boolean virtual = false; + NodeId id = refs.getTargetId(); + for (int i = 0; i < virtualProviders.length; i++) { + if (virtualProviders[i].hasItemState(id)) { + ChangeLog virtualRefs = virtualNodeReferences[i]; + if (virtualRefs == null) { + virtualRefs = new ChangeLog(); + virtualNodeReferences[i] = virtualRefs; + } + virtualRefs.modified(refs); + virtual = true; + break; + } + } + if (!virtual) { + // if target of node reference does not lie in a virtual + // space, add to modified set of normal provider. + shared.modified(refs); + } + } + + checkAddedChildNodes(); + + /* create event states */ + events.createEventStates(rootNodeId, local, SharedItemStateManager.this); + + // let listener know about change + try { + eventChannel.updatePrepared(this); + } catch (ClusterException e) { + throw new ItemStateException(e.getMessage(), e); + } + + if (VALIDATE_HIERARCHY) { + log.debug("Validating change-set hierarchy"); + try { + validateHierarchy(local); + } catch (ItemStateException e) { + throw e; + } catch (RepositoryException e) { + throw new ItemStateException("Invalid hierarchy", e); + } + } + + /* Push all changes from the local items to the shared items */ + local.push(); + + succeeded = true; + + } finally { + if (!succeeded) { + cancel(); + } + } + } + + /** + * End update operation. This will store the changes to the associated + * PersistenceManager. At the end of this operation, an + * eventual read or write lock on the item state manager will have + * been released. + * @throws ItemStateException if some error occurs + */ + public void end() throws ItemStateException { + boolean succeeded = false; + + try { + /* Store items in the underlying persistence manager */ + long t0 = System.currentTimeMillis(); + persistMgr.store(shared); + setAttribute(ATTRIBUTE_UPDATE_SIZE, shared.getUpdateSize()); + succeeded = true; + if (log.isDebugEnabled()) { + long t1 = System.currentTimeMillis(); + log.debug("persisting change log " + shared + " took " + (t1 - t0) + "ms"); + } + } finally { + if (!succeeded) { + cancel(); + } + } + + ISMLocking.ReadLock readLock = null; + try { + // make sure new item states are present/referenced in cache + // we do this before the lock is downgraded to a read lock + // because then other threads will be able to read from + // this SISM again and potentially read an added item state + // before the ones here are put into the cache (via + // shared.persisted()). See JCR-3345 + for (ItemState state : shared.addedStates()) { + // there is one exception though. it is possible that the + // shared ChangeLog contains the an item both as removed and + // added. For those items we don't update the cache here, + // because that would lead to WARN messages in the + // ItemStateReferenceCache. See JCR-3419 + if (!shared.deleted(state.getId())) { + state.setStatus(ItemState.STATUS_EXISTING); + cache.cache(state); + } + } + + // downgrade to read lock + readLock = writeLock.downgrade(); + writeLock = null; + + // Let the shared item listeners know about the change + // JCR-2171: This must happen after downgrading the lock! + shared.persisted(); + + /* notify virtual providers about node references */ + for (int i = 0; i < virtualNodeReferences.length; i++) { + ChangeLog virtualRefs = virtualNodeReferences[i]; + if (virtualRefs != null) { + virtualProviders[i].setNodeReferences(virtualRefs); + } + } + + } finally { + // Let listener know about finished operation. This needs + // to happen in the finally block so that the cluster lock + // always gets released, even if a post-store() exception + // is thrown from the code above. See also JCR-2272. + String path = events.getSession().getUserID() + + "@" + events.getSession().getWorkspace().getName() + + ":" + events.getCommonPath(); + eventChannel.updateCommitted(this, path); + setAttribute(ATTRIBUTE_UPDATE_SIZE, null); + + if (writeLock != null) { + // exception occurred before downgrading lock + writeLock.release(); + writeLock = null; + } else if (readLock != null) { + try { + if (succeeded) { + /* dispatch the events */ + events.dispatch(); + } + } finally { + readLock.release(); + } + } + + } + } + + /** + * Cancel update operation. At the end of this operation, the write lock + * on the item state manager will have been released. + */ + public void cancel() { + try { + // let listener know about canceled operation + eventChannel.updateCancelled(this); + + local.disconnect(); + + for (ItemState state : shared.modifiedStates()) { + try { + state.copy(loadItemState(state.getId()), true); + } catch (ItemStateException e) { + state.discard(); + } + } + for (ItemState state : shared.deletedStates()) { + try { + state.copy(loadItemState(state.getId()), true); + } catch (ItemStateException e) { + state.discard(); + } + } + for (ItemState state : shared.addedStates()) { + state.discard(); + } + } finally { + if (writeLock != null) { + writeLock.release(); + writeLock = null; + } + } + } + + /** + * {@inheritDoc} + */ + public void setAttribute(String name, Object value) { + if (attributes == null) { + attributes = new HashMap(); + } + attributes.put(name, value); + } + + /** + * {@inheritDoc} + */ + public Object getAttribute(String name) { + if (attributes != null) { + return attributes.get(name); + } + return null; + } + + /** + * {@inheritDoc} + */ + public ChangeLog getChanges() { + return local; + } + + /** + * {@inheritDoc} + */ + public List getEvents() { + return events.getEvents(); + } + + /** + * {@inheritDoc} + */ + public long getTimestamp() { + return timestamp; + } + + public String getUserData() { + return events.getUserData(); + } + + /** + * Updates the target node references collections based on the + * modifications in the change log (i.e. added/removed/modified + * REFERENCE properties). + *

    + * Important node: For consistency reasons this method must + * only be called once per change log and the change log + * should not be modified anymore afterwards. + * + * @throws ItemStateException if an error occurs + */ + private void updateReferences() throws ItemStateException { + // process added REFERENCE properties + for (ItemState state : local.addedStates()) { + if (!state.isNode()) { + // add new references to the target + addReferences((PropertyState) state); + } + } + + // process modified REFERENCE properties + for (ItemState state : local.modifiedStates()) { + if (!state.isNode()) { + // remove old references from the target + removeReferences(getItemState(state.getId())); + // add new references to the target + addReferences((PropertyState) state); + } + } + + // process removed REFERENCE properties + for (ItemState state : local.deletedStates()) { + removeReferences(state); + } + } + + private void addReferences(PropertyState property) throws ItemStateException { + if (property.getType() == PropertyType.REFERENCE) { + InternalValue[] values = property.getValues(); + for (int i = 0; values != null && i < values.length; i++) { + addReference(property.getPropertyId(), values[i].getNodeId()); + } + } + } + + private void addReference(PropertyId id, NodeId target) + throws ItemStateException { + if (virtualProvider == null + || !virtualProvider.hasNodeReferences(target)) { + // get or create the references instance + NodeReferences refs = local.getReferencesTo(target); + if (refs == null) { + if (hasNodeReferences(target)) { + refs = getNodeReferences(target); + } else { + refs = new NodeReferences(target); + } + } + // add reference + refs.addReference(id); + // update change log + local.modified(refs); + } + } + + private void removeReferences(ItemState state) + throws ItemStateException { + if (!state.isNode()) { + PropertyState property = (PropertyState) state; + if (property.getType() == PropertyType.REFERENCE) { + InternalValue[] values = property.getValues(); + for (int i = 0; values != null && i < values.length; i++) { + removeReference( + property.getPropertyId(), values[i].getNodeId()); + } + } + } + } + + private void removeReference(PropertyId id, NodeId target) + throws ItemStateException { + if (virtualProvider == null + || !virtualProvider.hasNodeReferences(target)) { + // either get node references from change log or load from + // persistence manager + NodeReferences refs = local.getReferencesTo(target); + if (refs == null && hasNodeReferences(target)) { + refs = getNodeReferences(target); + } + if (refs != null) { + // remove reference + refs.removeReference(id); + // update change log + local.modified(refs); + } + } + } + + /** + * Verify the added child nodes of the added or modified states exist. + * If they don't exist, most likely the problem is that the same session + * is used concurrently. + */ + private void checkAddedChildNodes() throws ItemStateException { + for (ItemState state : local.addedStates()) { + checkAddedChildNode(state); + } + for (ItemState state : local.modifiedStates()) { + checkAddedChildNode(state); + } + } + + private void checkAddedChildNode(ItemState state) throws ItemStateException { + if (state.isNode()) { + NodeState node = (NodeState) state; + for (ChildNodeEntry child : node.getAddedChildNodeEntries()) { + NodeId id = child.getId(); + if (local.get(id) == null && + !id.equals(RepositoryImpl.VERSION_STORAGE_NODE_ID) && + !id.equals(RepositoryImpl.ACTIVITIES_NODE_ID) && + !id.equals(RepositoryImpl.NODETYPES_NODE_ID) && + !cache.isCached(id) && + !persistMgr.exists(id)) { + String msg = "Trying to add a non-existing child node: " + id; + log.debug(msg); + throw new ItemStateException(msg); + } + } + } + } + + /** + * Verifies that + *

      + *
    • no referenceable nodes are deleted if they are still being referenced
    • + *
    • targets of modified node references exist
    • + *
    + * + * @throws ReferentialIntegrityException if a new or modified REFERENCE + * property refers to a non-existent + * target or if a removed node is still + * being referenced + * @throws ItemStateException if another error occurs + */ + private void checkReferentialIntegrity() + throws ReferentialIntegrityException, ItemStateException { + + // check whether removed referenceable nodes are still being referenced + for (ItemState state : local.deletedStates()) { + if (state.isNode()) { + NodeState node = (NodeState) state; + if (isReferenceable(node)) { + NodeId targetId = node.getNodeId(); + // either get node references from change log or + // load from persistence manager + NodeReferences refs = local.getReferencesTo(targetId); + if (refs == null) { + if (!hasNodeReferences(targetId)) { + continue; + } + refs = getNodeReferences(targetId); + } + // in some versioning operations (such as restore) a node + // may actually be deleted and then again added with the + // same UUID, i.e. the node is still referenceable. + if (refs.hasReferences() && !local.has(targetId)) { + String msg = + node.getNodeId() + " cannot be removed" + + " because it is still being referenced"; + log.debug("{} from {}", msg, refs.getReferences()); + throw new ReferentialIntegrityException(msg); + } + } + } + } + + // check whether targets of modified node references exist + for (NodeReferences refs : local.modifiedRefs()) { + // no need to check existence of target if there are no references + if (refs.hasReferences()) { + // please note: + // virtual providers are indirectly checked via 'hasItemState()' + NodeId id = refs.getTargetId(); + if (!local.has(id) && !hasItemState(id)) { + String msg = "Target node " + id + + " of REFERENCE property does not exist"; + log.debug(msg); + throw new ReferentialIntegrityException(msg); + } + } + } + } + + /** + * Determines whether the specified node is referenceable, i.e. + * whether the mixin type mix:referenceable is either + * directly assigned or indirectly inherited. + * + * @param state node state to check + * @return true if the specified node is referenceable, false otherwise. + * @throws ItemStateException if an error occurs + */ + private boolean isReferenceable(NodeState state) throws ItemStateException { + // shortcut: check some well known built-in types first + Name primary = state.getNodeTypeName(); + Set mixins = state.getMixinTypeNames(); + if (mixins.contains(NameConstants.MIX_REFERENCEABLE) + || mixins.contains(NameConstants.MIX_VERSIONABLE) + || primary.equals(NameConstants.NT_RESOURCE)) { + return true; + } + + // build effective node type + try { + EffectiveNodeType type = ntReg.getEffectiveNodeType(primary, mixins); + return type.includesNodeType(NameConstants.MIX_REFERENCEABLE); + } catch (NodeTypeConflictException ntce) { + String msg = "internal error: failed to build effective node type for node " + + state.getNodeId(); + log.debug(msg); + throw new ItemStateException(msg, ntce); + } catch (NoSuchNodeTypeException nsnte) { + String msg = "internal error: failed to build effective node type for node " + + state.getNodeId(); + log.debug(msg); + throw new ItemStateException(msg, nsnte); + } + } + + } + + /** + * Validates the hierarchy consistency of the changes in the changelog. + * + * @param changeLog + * The local changelog the should be validated + * @throws ItemStateException + * If the hierarchy changes are inconsistent. + * @throws RepositoryException + * If the consistency could not be validated + * + */ + private void validateHierarchy(ChangeLog changeLog) throws ItemStateException, RepositoryException { + + // Check the deleted node states + validateDeleted(changeLog); + + // Check the added node states + validateAdded(changeLog); + + // Check the modified node states + validateModified(changeLog); + } + + /** + * Checks the parents and children of all deleted node states in the changelog. + * + * @param changeLog + * The local changelog the should be validated + * @throws ItemStateException + * If the hierarchy changes are inconsistent. + */ + private void validateDeleted(ChangeLog changeLog) throws ItemStateException { + + // Check each deleted nodestate + for (ItemState removedState : changeLog.deletedStates()) { + if (removedState instanceof NodeState) { + + // Get the next state + NodeState removedNodeState = (NodeState) removedState; + NodeId id = removedNodeState.getNodeId(); + + // Get and check the corresponding overlayed state + NodeState overlayedState = (NodeState) removedState.getOverlayedState(); + if (overlayedState == null) { + String message = "Unable to load persistent state for removed node " + id; + overlayedState = (NodeState) SharedItemStateManager.this.getItemState(id); + if (overlayedState == null) { + log.error(message); + throw new ItemStateException(message); + } + } + + // Check whether an version of this node has been restored + boolean addedAndRemoved = changeLog.has(removedNodeState.getId()); + if (!addedAndRemoved) { + + // Check the old parent; it should be either also deleted + // or at least modified to reflect the removal of a child + NodeId oldParentId = overlayedState.getParentId(); + if (!changeLog.deleted(oldParentId) + && !changeLog.isModified(oldParentId)) { + String message = "Node with id " + id + + " has been removed, but the parent node isn't part of the changelog " + oldParentId; + log.error(message); + throw new ItemStateException(message); + } + + // Get the original list of child ids + for (ChildNodeEntry entry : overlayedState.getChildNodeEntries()) { + + // Check the next child; it should be either also deleted + // or at least modified to reflect being moved elsewhere + NodeId childId = entry.getId(); + if (!changeLog.deleted(childId) + && !changeLog.isModified(childId)) { + String message = "Node with id " + id + + " has been removed, but the old child node isn't part of the changelog " + + childId; + log.error(message); + throw new ItemStateException(message); + } + } + } + } + } + } + + /** + * Checks the parents and children of all added node states in the changelog. + * + * @param changeLog + * The local changelog the should be validated + * @throws ItemStateException + * If the hierarchy changes are inconsistent. + */ + private void validateAdded(ChangeLog changeLog) throws ItemStateException { + + // Check each added node + for (ItemState state : changeLog.addedStates()) { + if (state instanceof NodeState) { + + // Get the next added node + NodeState addedNodeState = (NodeState) state; + NodeId id = addedNodeState.getNodeId(); + + // Check the parent + NodeId parentId = addedNodeState.getParentId(); + if (changeLog.has(parentId)) { // Added or modified + // the modified state will be check later on + checkParent(changeLog, addedNodeState, parentId); + } else { + String message = "Node with id " + id + + " has been added, but the parent node isn't part of the changelog " + parentId; + log.error(message); + throw new ItemStateException(message); + } + + // Check the children + for (ChildNodeEntry entry : addedNodeState.getChildNodeEntries()) { + + // Get the next child + NodeId childId = entry.getId(); + + if (changeLog.has(childId)) { + NodeState childState = (NodeState) changeLog.get(childId); + checkParent(changeLog, childState, id); + // the child state will be check later on + + } else { + String message = "Node with id " + id + + " has been added, but the child node isn't part of the changelog " + childId; + log.error(message); + throw new ItemStateException(message); + } + } + } + } + } + + /** + * Checks the parents and children of all modified node states in the changelog. + * + * @param changeLog + * The local changelog the should be validated + * @throws ItemStateException + * If the hierarchy changes are inconsistent. + */ + private void validateModified(ChangeLog changeLog) throws ItemStateException, RepositoryException { + + // Check all modified nodes + for (ItemState state : changeLog.modifiedStates()) { + if (state instanceof NodeState) { + + // Check the next node + NodeState modifiedNodeState = (NodeState) state; + NodeId id = modifiedNodeState.getNodeId(); + + // Check whether to overlayed state is present for determining diffs + NodeState overlayedState = (NodeState) modifiedNodeState.getOverlayedState(); + if (overlayedState == null) { + String message = "Unable to load persistent state for modified node " + id; + log.error(message); + throw new ItemStateException(message); + } + + // Check the parent + NodeId parentId = modifiedNodeState.getParentId(); + NodeId oldParentId = overlayedState.getParentId(); + + // The parent should not be deleted + if (parentId != null && changeLog.deleted(parentId) && !changeLog.isAdded(parentId)) { + String message = "Parent of node with id " + id + " has been deleted"; + log.error(message); + throw new ItemStateException(message); + } + + if (parentId != null && changeLog.has(parentId)) { + checkParent(changeLog, modifiedNodeState, parentId); + } + + if (!(parentId == null && oldParentId == null) + && (parentId != null && !parentId.equals(oldParentId))) { + // This node (not the root) has been moved; check + // whether the parent has been modified as well + if (changeLog.has(parentId)) { + checkParent(changeLog, modifiedNodeState, parentId); + } else if (!isShareable(modifiedNodeState)) { + String message = "New parent of node " + id + " is not present in the changelog " + id; + log.error(message); + throw new ItemStateException(message); + } + + // The old parent must be modified or deleted + if (!changeLog.isModified(oldParentId) && !changeLog.deleted(oldParentId)) { + String message = "Node with id " + id + + " has been moved, but the original parent is not part of the changelog: " + + oldParentId; + log.error(message); + throw new ItemStateException(message); + } + } + + // Check all assigned children + for (ChildNodeEntry entry : modifiedNodeState.getChildNodeEntries()) { + + NodeId childId = entry.getId(); + + // Check whether this node has a deleted childid + if (changeLog.deleted(childId) && !changeLog.has(childId)) { // Versionable + String message = "Node with id " + id + " has a deleted childid: " + childId; + log.error(message); + throw new ItemStateException(message); + } + + if (changeLog.has(childId)) { + NodeState childState = (NodeState) changeLog.get(childId); + checkParent(changeLog, childState, id); + } + } + + // Check all children the have been added + for (ChildNodeEntry entry : modifiedNodeState.getAddedChildNodeEntries()) { + NodeId childId = entry.getId(); + if (!changeLog.has(childId)) { + String message = "ChildId " + childId + " has been added to parent " + id + + ", but is not present in the changelog"; + log.error(message); + throw new ItemStateException(message); + } + } + + // Check all children the have been moved or removed + for (ChildNodeEntry entry : modifiedNodeState.getRemovedChildNodeEntries()) { + NodeId childId = entry.getId(); + if (!changeLog.isModified(childId) && !changeLog.deleted(childId)) { + String message = "Child node entry with id " + childId + + " has been removed, but is not present in the changelog"; + log.error(message); + throw new ItemStateException(message); + } + } + } + } + } + + /** + * Check the consistency of a parent/child relationship. + * + * @param changeLog + * The changelog to check + * @param childState + * The id of the node for which the parent/child relationship should be validated. + * @param expectedParent + * The expected parent id of the child node. + * @throws ItemStateException + * If a inconsistency has been detected. + */ + void checkParent(ChangeLog changeLog, NodeState childState, NodeId expectedParent) throws ItemStateException { + + // Check whether the the changelog contains an entry for the parent as well. + NodeId parentId = childState.getParentId(); + if (!parentId.equals(expectedParent)) { + Set sharedSet = childState.getSharedSet(); + if (sharedSet.contains(expectedParent)) { + return; + } + String message = "Child node " + childState.getId() + " has another parent id " + parentId + ", expected " + expectedParent; + log.error(message); + throw new ItemStateException(message); + } + + if (!changeLog.has(parentId)) { + String message = "Parent not part of changelog"; + log.error(message); + throw new ItemStateException(message); + } + + // Get the parent from the changelog + NodeState parent = (NodeState) changeLog.get(parentId); + + // Get and check the child node entry from the parent + NodeId childId = childState.getNodeId(); + ChildNodeEntry childNodeEntry = parent.getChildNodeEntry(childId); + if (childNodeEntry == null) { + String message = "Child not present in parent"; + log.error(message); + throw new ItemStateException(message); + } + } + + /** + * Determines whether the specified node is shareable, i.e. whether the mixin type mix:shareable + * is either directly assigned or indirectly inherited. + * + * @param state + * node state to check + * @return true if the specified node is shareable, false otherwise. + * @throws RepositoryException + * if an error occurs + */ + private boolean isShareable(NodeState state) throws RepositoryException { + // shortcut: check some well-known built-in types first + Name primary = state.getNodeTypeName(); + Set mixins = state.getMixinTypeNames(); + if (mixins.contains(NameConstants.MIX_SHAREABLE)) { + return true; + } + + try { + EffectiveNodeType type = ntReg.getEffectiveNodeType(primary, mixins); + return type.includesNodeType(NameConstants.MIX_SHAREABLE); + } catch (NodeTypeConflictException ntce) { + String msg = "internal error: failed to build effective node type for node " + state.getNodeId(); + log.debug(msg); + throw new RepositoryException(msg, ntce); + } + } + + /** + * Begin update operation. This will return an object that can itself be + * ended/canceled. + */ + public Update beginUpdate(ChangeLog local, EventStateCollectionFactory factory, + VirtualItemStateProvider virtualProvider) + throws ReferentialIntegrityException, StaleItemStateException, + ItemStateException { + + Update update = new Update(local, factory, virtualProvider); + update.begin(); + return update; + } + + /** + * Store modifications registered in a ChangeLog. The items + * contained in the ChangeLog are not states returned by this + * item state manager but rather must be reconnected to items provided + * by this state manager. + *

    + * After successfully storing the states the observation manager is informed + * about the changes, if an observation manager is passed to this method. + *

    + * NOTE: This method is not synchronized, because all methods it invokes + * on instance members (such as {@link PersistenceManager#store} are + * considered to be thread-safe. Should this ever change, the + * synchronization status has to be re-examined. + * + * @param local change log containing local items + * @param factory event state collection factory + * @throws ReferentialIntegrityException if a new or modified REFERENCE + * property refers to a non-existent + * target or if a removed node is still + * being referenced + * @throws StaleItemStateException if at least one of the affected item + * states has become stale + * @throws ItemStateException if another error occurs + */ + public void update(ChangeLog local, EventStateCollectionFactory factory) + throws ReferentialIntegrityException, StaleItemStateException, + ItemStateException { + + beginUpdate(local, factory, null).end(); + } + + /** + * Handle an external update. + * + * @param external external change containing only node and property ids. + * @param events events to deliver + */ + public void externalUpdate(ChangeLog external, EventStateCollection events) { + boolean holdingWriteLock = false; + + ISMLocking.WriteLock wLock = null; + ISMLocking.ReadLock rLock = null; + try { + try { + wLock = acquireWriteLock(external); + holdingWriteLock = true; + + doExternalUpdate(external); + } catch (ItemStateException e) { + String msg = "Unable to acquire write lock."; + log.error(msg); + } + + if (wLock != null) { + rLock = wLock.downgrade(); + holdingWriteLock = false; + events.dispatch(); + } + } finally { + if (holdingWriteLock) { + if (wLock != null) { + wLock.release(); + } + } else { + if (rLock != null) { + rLock.release(); + } + } + } + + } + + /** + * Perform the external update. While executing this method, the + * writeLock on this manager is held. + * + * @param external external change containing only node and property ids. + */ + protected void doExternalUpdate(ChangeLog external) { + // workaround to flush cache of persistence manager + if (persistMgr instanceof CachingPersistenceManager) { + ((CachingPersistenceManager) persistMgr).onExternalUpdate(external); + } + + ChangeLog shared = new ChangeLog(); + + // Build a copy of the external change log, consisting of shared + // states we have in our cache. Inform listeners about this + // change. + for (ItemState state : external.modifiedStates()) { + state = cache.retrieve(state.getId()); + if (state != null) { + try { + ItemState currentState = loadItemState(state.getId()); + state.copy(currentState, true); + shared.modified(state); + } catch (NoSuchItemStateException e) { + // This is likely to happen because a subsequent delete + // of this very state has not yet been transmitted. + String msg = "Unable to retrieve state: " + state.getId() + ", ignored."; + log.info(msg); + state.discard(); + } catch (ItemStateException e) { + String msg = "Unable to retrieve state: " + state.getId(); + log.warn(msg); + state.discard(); + } + } + } + for (ItemState state : external.deletedStates()) { + state = cache.retrieve(state.getId()); + if (state != null) { + shared.deleted(state); + } + } + shared.persisted(); + } + + /** + * Add an ItemStateListener + * @param listener the new listener to be informed on modifications + */ + public void addListener(ItemStateListener listener) { + dispatcher.addListener(listener); + } + + /** + * Remove an ItemStateListener + * @param listener an existing listener + */ + public void removeListener(ItemStateListener listener) { + dispatcher.removeListener(listener); + } + + //-------------------------------------------------------< implementation > + + /** + * Create a new node state instance + * + * @param id uuid + * @param nodeTypeName node type name + * @param parentId parent UUID + * @return new node state instance + */ + private NodeState createInstance(NodeId id, Name nodeTypeName, + NodeId parentId) { + + NodeState state = persistMgr.createNew(id); + state.setNodeTypeName(nodeTypeName); + state.setParentId(parentId); + state.setStatus(ItemState.STATUS_NEW); + state.setContainer(this); + + return state; + } + + /** + * Create root node state + * + * @param rootNodeId root node id + * @param ntReg node type registry + * @return root node state + * @throws ItemStateException if an error occurs + */ + private NodeState createRootNodeState(NodeId rootNodeId, + NodeTypeRegistry ntReg) + throws ItemStateException { + + NodeState rootState = createInstance(rootNodeId, NameConstants.REP_ROOT, null); + NodeState jcrSystemState = createInstance(RepositoryImpl.SYSTEM_ROOT_NODE_ID, NameConstants.REP_SYSTEM, rootNodeId); + + // FIXME need to manually setup root node by creating mandatory jcr:primaryType property + // @todo delegate setup of root node to NodeTypeInstanceHandler + + // create jcr:primaryType property on root node state + rootState.addPropertyName(NameConstants.JCR_PRIMARYTYPE); + + PropertyState prop = createInstance(NameConstants.JCR_PRIMARYTYPE, rootNodeId); + prop.setValues(new InternalValue[]{InternalValue.create(NameConstants.REP_ROOT)}); + prop.setType(PropertyType.NAME); + prop.setMultiValued(false); + + // create jcr:primaryType property on jcr:system node state + jcrSystemState.addPropertyName(NameConstants.JCR_PRIMARYTYPE); + + PropertyState primaryTypeProp = createInstance(NameConstants.JCR_PRIMARYTYPE, jcrSystemState.getNodeId()); + primaryTypeProp.setValues(new InternalValue[]{InternalValue.create(NameConstants.REP_SYSTEM)}); + primaryTypeProp.setType(PropertyType.NAME); + primaryTypeProp.setMultiValued(false); + + // add child node entry for jcr:system node + rootState.addChildNodeEntry(NameConstants.JCR_SYSTEM, RepositoryImpl.SYSTEM_ROOT_NODE_ID); + + // add child node entry for virtual jcr:versionStorage + jcrSystemState.addChildNodeEntry(NameConstants.JCR_VERSIONSTORAGE, RepositoryImpl.VERSION_STORAGE_NODE_ID); + + // add child node entry for virtual jcr:activities + jcrSystemState.addChildNodeEntry(NameConstants.JCR_ACTIVITIES, RepositoryImpl.ACTIVITIES_NODE_ID); + + // add child node entry for virtual jcr:nodeTypes + jcrSystemState.addChildNodeEntry(NameConstants.JCR_NODETYPES, RepositoryImpl.NODETYPES_NODE_ID); + + + ChangeLog changeLog = new ChangeLog(); + changeLog.added(rootState); + changeLog.added(prop); + changeLog.added(jcrSystemState); + changeLog.added(primaryTypeProp); + + persistMgr.store(changeLog); + changeLog.persisted(); + + return rootState; + } + + /** + * Makes sure child node entry for mandatory jcr:activities exist. + * Repositories upgraded from 1.x do not have it. + *

    + * This method assumes that the jcr:system node already exists. + * + * @throws ItemStateException if an error occurs while reading or writing to + * the persistence manager. + */ + private void ensureActivitiesNode() throws ItemStateException { + NodeState jcrSystemState = (NodeState) getNonVirtualItemState(RepositoryImpl.SYSTEM_ROOT_NODE_ID); + if (!jcrSystemState.hasChildNodeEntry(RepositoryImpl.ACTIVITIES_NODE_ID)) { + jcrSystemState.addChildNodeEntry(NameConstants.JCR_ACTIVITIES, RepositoryImpl.ACTIVITIES_NODE_ID); + + ChangeLog changeLog = new ChangeLog(); + changeLog.modified(jcrSystemState); + + persistMgr.store(changeLog); + changeLog.persisted(); + } + } + + /** + * Identifiers of the item states that are currently being loaded from + * the underlying persistence manager. Used exclusively by the + * {@link #getNonVirtualItemState(ItemId)} method to prevent two threads + * from concurrently loading the same items. + */ + private final Set currentlyLoading = new HashSet(); + + /** + * Returns the item state for the given id without considering virtual + * item state providers. + */ + private ItemState getNonVirtualItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + // First check if the item state is already in the cache + ItemState state = cache.retrieve(id); + if (state != null) { + return state; + } + + // Wait if another thread is already loading this item state + synchronized (this) { + while (currentlyLoading.contains(id)) { + try { + wait(); + } catch (InterruptedException e) { + throw new ItemStateException( + "Interrupted while waiting for " + id, e); + } + } + + state = cache.retrieve(id); + if (state != null) { + return state; + } + + // No other thread has loaded the item state, so we'll do it + currentlyLoading.add(id); + } + + try { + // Load the item state from persistent storage + // NOTE: This needs to happen outside a synchronized block! + state = loadItemState(id); + state.setStatus(ItemState.STATUS_EXISTING); + state.setContainer(this); + + // put it in cache + cache.cache(state); + + return state; + } finally { + // Notify other concurrent threads that we're done with this item + // NOTE: This needs to happen within the finally block! + synchronized (this) { + currentlyLoading.remove(id); + notifyAll(); + } + } + } + + /** + * Checks if this item state manager has the given item state without + * considering the virtual item state managers. + */ + protected boolean hasNonVirtualItemState(ItemId id) { + if (cache.isCached(id)) { + return true; + } + + try { + if (id.denotesNode()) { + return persistMgr.exists((NodeId) id); + } else { + return persistMgr.exists((PropertyId) id); + } + } catch (ItemStateException ise) { + return false; + } + } + + /** + * Create a new item state instance + * + * @param other other state associated with new instance + * @return new node state instance + */ + private ItemState createInstance(ItemState other) { + if (other.isNode()) { + NodeState ns = (NodeState) other; + return createInstance(ns.getNodeId(), ns.getNodeTypeName(), ns.getParentId()); + } else { + PropertyState ps = (PropertyState) other; + return createInstance(ps.getName(), ps.getParentId()); + } + } + + /** + * Create a new property state instance + * + * @param propName property name + * @param parentId parent Id + * @return new property state instance + */ + private PropertyState createInstance(Name propName, NodeId parentId) { + PropertyState state = persistMgr.createNew(new PropertyId(parentId, propName)); + state.setStatus(ItemState.STATUS_NEW); + state.setContainer(this); + + return state; + } + + /** + * Load item state from persistent storage. + * + * @param id item id + * @return item state + */ + private ItemState loadItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + ItemState state; + if (id.denotesNode()) { + state = persistMgr.load((NodeId) id); + } else { + state = persistMgr.load((PropertyId) id); + } + return state; + } + + /** + * Acquires the read lock on this item state manager. + * + * @param id the id of the item for which to acquire a read lock. + * @throws ItemStateException if the read lock cannot be acquired. + */ + private ISMLocking.ReadLock acquireReadLock(ItemId id) throws ItemStateException { + try { + return ismLocking.acquireReadLock(id); + } catch (InterruptedException e) { + throw new ItemStateException("Interrupted while acquiring read lock"); + } + } + + /** + * Acquires the write lock on this item state manager. + * + * @param changeLog the change log for which to acquire a write lock. + * @throws ItemStateException if the write lock cannot be acquired. + */ + private ISMLocking.WriteLock acquireWriteLock(ChangeLog changeLog) throws ItemStateException { + try { + return ismLocking.acquireWriteLock(changeLog); + } catch (InterruptedException e) { + throw new ItemStateException("Interrupted while acquiring write lock"); + } + } + + public NodeIdFactory getNodeIdFactory() { + return this.nodeIdFactory; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/StaleItemStateException.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/StaleItemStateException.java new file mode 100644 index 00000000000..d80aa7ff7f1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/StaleItemStateException.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.core.state; + +/** + * Signals that an item has been modified externally and that the item state + * representing it has thus become stale. + */ +public class StaleItemStateException extends ItemStateException { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public StaleItemStateException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public StaleItemStateException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/StateChangeDispatcher.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/StateChangeDispatcher.java new file mode 100644 index 00000000000..0f6fbf8fc44 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/StateChangeDispatcher.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Component that holds references to listeners interested in changes to item + * states and dispatches notifications. + */ +public class StateChangeDispatcher { + + /** + * Simple item state listeners. + * A copy on write array list is used so that no synchronization is required. + */ + private final Collection> listeners = + new CopyOnWriteArrayList>(); + + /** + * Node state listeners + * A copy on write array list is used so that no synchronization is required. + */ + private final Collection> nsListeners = + new CopyOnWriteArrayList>(); + + /** + * Add an ItemStateListener. + * @param listener the new listener to be informed on modifications + */ + public void addListener(ItemStateListener listener) { + assert getReference(listeners, listener) == null; + listeners.add(new WeakReference(listener)); + + if (listener instanceof NodeStateListener) { + NodeStateListener nsListener = (NodeStateListener) listener; + assert getReference(nsListeners, nsListener) == null; + nsListeners.add(new WeakReference(nsListener)); + } + } + + private Reference getReference(Collection< ? extends Reference> coll, ItemStateListener listener) { + for (Reference ref : coll) { + Object o = ref.get(); + if (o == listener) { + return ref; + } else if (o == null) { + // clean up unreferenced objects + coll.remove(ref); + } + } + return null; + } + + /** + * Remove an ItemStateListener + * @param listener an existing listener + */ + public void removeListener(ItemStateListener listener) { + if (listener instanceof NodeStateListener) { + nsListeners.remove(getReference(nsListeners, listener)); + } + listeners.remove(getReference(listeners, listener)); + } + + /** + * Notify listeners about changes to some state. + * @param created created state. + */ + public void notifyStateCreated(ItemState created) { + for (Reference ref : listeners) { + ItemStateListener l = ref.get(); + if (l != null) { + l.stateCreated(created); + } + } + } + + /** + * Notify listeners about changes to some state. + * @param modified modified state. + */ + public void notifyStateModified(ItemState modified) { + for (Reference ref : listeners) { + ItemStateListener l = ref.get(); + if (l != null) { + l.stateModified(modified); + } + } + } + + /** + * Notify listeners about changes to some state. + * @param destroyed destroyed state. + */ + public void notifyStateDestroyed(ItemState destroyed) { + for (Reference ref : listeners) { + ItemStateListener l = ref.get(); + if (l != null) { + l.stateDestroyed(destroyed); + } + } + } + + /** + * Notify listeners about changes to some state. + * @param discarded discarded state. + */ + public void notifyStateDiscarded(ItemState discarded) { + for (Reference ref : listeners) { + ItemStateListener l = ref.get(); + if (l != null) { + l.stateDiscarded(discarded); + } + } + } + + /** + * Notify listeners about changes to some state. + * @param state node state that changed + * @param name name of node that was added + * @param index index of new node + * @param id id of new node + */ + public void notifyNodeAdded(NodeState state, Name name, int index, NodeId id) { + for (Reference ref : nsListeners) { + NodeStateListener n = ref.get(); + if (n != null) { + n.nodeAdded(state, name, index, id); + } + } + } + + /** + * Notify listeners about changes to some state. + * @param state node state that changed + */ + public void notifyNodesReplaced(NodeState state) { + for (Reference ref : nsListeners) { + NodeStateListener n = ref.get(); + if (n != null) { + n.nodesReplaced(state); + } + } + } + + /** + * Notify listeners about changes to some state. + * @param state node state that changed + */ + public void notifyNodeModified(NodeState state) { + for (Reference ref : nsListeners) { + NodeStateListener n = ref.get(); + if (n != null) { + n.nodeModified(state); + } + } + } + + /** + * Notify listeners about changes to some state. + * @param state node state that changed + * @param name name of node that was added + * @param index index of new node + * @param id id of new node + */ + public void notifyNodeRemoved(NodeState state, Name name, int index, NodeId id) { + for (Reference ref : nsListeners) { + NodeStateListener n = ref.get(); + if (n != null) { + n.nodeRemoved(state, name, index, id); + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/UpdatableItemStateManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/UpdatableItemStateManager.java new file mode 100644 index 00000000000..5067cc1639b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/UpdatableItemStateManager.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.core.state; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.id.NodeId; + +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; + +/** + * Identifies an ItemStateManager that allows updating + * items. + */ +public interface UpdatableItemStateManager extends ItemStateManager { + + /** + * Start an edit operation on items inside this manager. This + * allows calling the operations defined below. At the end of + * this operation, either {@link #update} or {@link #cancel} + * must be invoked. + * + * @throws IllegalStateException if the manager is already in edit mode. + */ + void edit() throws IllegalStateException; + + /** + * Returns true if this manager is in edit mode i.e. + * if an edit operation has been started by invoking {@link #edit}, + * otherwise returns false. + * + * @return true if this manager is in edit mode, otherwise + * false + */ + boolean inEditMode(); + + /** + * Creates a {@link NodeState} instance representing new, + * i.e. not yet existing state. Call {@link #store} + * on the returned object to make it persistent. + * + * @param id the id of the node, or null for a new node id + * @param nodeTypeName The node type name + * @param parentId parent node's id + * @return a node state + * @throws RepositoryException if the node state can not be created + */ + NodeState createNew( + NodeId id, Name nodeTypeName, NodeId parentId) + throws RepositoryException; + + /** + * Creates a {@link PropertyState} instance representing new, + * i.e. not yet existing state. Call {@link #store} + * on the returned object to make it persistent. + * + * @param propName property name + * @param parentId parent node Id + * @return a property state + * @throws IllegalStateException if the manager is not in edit mode. + */ + PropertyState createNew(Name propName, NodeId parentId) + throws IllegalStateException; + + /** + * Store an item state. + * + * @param state item state that should be stored + * @throws IllegalStateException if the manager is not in edit mode. + */ + void store(ItemState state) throws IllegalStateException; + + /** + * Destroy an item state. + * + * @param state item state that should be destroyed + * @throws IllegalStateException if the manager is not in edit mode. + */ + void destroy(ItemState state) throws IllegalStateException; + + /** + * Cancel an update operation. This will undo all changes + * made to objects inside this item state manager. + * + * @throws IllegalStateException if the manager is not in edit mode. + */ + void cancel() throws IllegalStateException; + + /** + * End an update operation. This will save all items + * added to this update operation in a single step. + * If this operation fails, no item will have been saved. + * + * @throws ReferentialIntegrityException if a new or modified REFERENCE + * property refers to a non-existent + * target or if a removed node is still + * being referenced + * @throws StaleItemStateException if at least one of the affected items + * has become stale in the meantime + * @throws ItemStateException if the operation failed for another reason + * @throws IllegalStateException if the manager is not in edit mode. + */ + void update() + throws ReferentialIntegrityException, StaleItemStateException, + ItemStateException, IllegalStateException; + + /** + * Disposes this UpdatableItemStateManager and frees resources. + */ + void dispose(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/XAItemStateManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/XAItemStateManager.java new file mode 100644 index 00000000000..30ac2c69a36 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/state/XAItemStateManager.java @@ -0,0 +1,598 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; + +import org.apache.commons.collections.Predicate; +import org.apache.commons.collections.iterators.FilterIterator; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.observation.EventStateCollectionFactory; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider; +import org.apache.jackrabbit.data.core.InternalXAResource; +import org.apache.jackrabbit.data.core.TransactionContext; +import org.apache.jackrabbit.data.core.TransactionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extension to LocalItemStateManager that remembers changes on + * multiple save() requests and commits them only when an associated transaction + * is itself committed. + */ +public class XAItemStateManager extends LocalItemStateManager implements InternalXAResource { + + /** + * The logger instance. + */ + private static Logger log = LoggerFactory.getLogger(XAItemStateManager.class); + + /** + * Default change log attribute name. + */ + private static final String DEFAULT_ATTRIBUTE_NAME = "ChangeLog"; + + /** + * This map holds the ChangeLog on a per thread basis while this state + * manager is in one of the {@link #prepare}, {@link #commit}, {@link + * #rollback} methods. + */ + private final Map commitLogs = Collections.synchronizedMap(new IdentityHashMap()); + + /** + * Current instance-local change log. + */ + private transient ChangeLog txLog; + + /** + * Current update operation. + */ + private transient SharedItemStateManager.Update update; + + /** + * Change log attribute name. + */ + private final String attributeName; + + /** + * Optional virtual item state provider. + */ + private VirtualItemStateProvider virtualProvider; + + /** + * Creates a new instance of this class with a custom attribute name. + * + * @param sharedStateMgr shared state manager + * @param factory event state collection factory + * @param attributeName the attribute name, if {@code null} then a default name is used + */ + protected XAItemStateManager(SharedItemStateManager sharedStateMgr, + EventStateCollectionFactory factory, + String attributeName, + ItemStateCacheFactory cacheFactory) { + super(sharedStateMgr, factory, cacheFactory); + if (attributeName != null) { + this.attributeName = attributeName; + } else { + this.attributeName = DEFAULT_ATTRIBUTE_NAME; + } + } + + /** + * Creates a new {@code XAItemStateManager} instance and registers it as an {@link ItemStateListener} + * with the given {@link SharedItemStateManager}. + * + * @param sharedStateMgr the {@link SharedItemStateManager} + * @param factory the {@link EventStateCollectionFactory} + * @param attributeName the attribute name, if {@code null} then a default name is used + * @param cacheFactory the {@link ItemStateCacheFactory} + * @return a new {@code XAItemStateManager} instance + */ + public static XAItemStateManager createInstance(SharedItemStateManager sharedStateMgr, + EventStateCollectionFactory factory, String attributeName, ItemStateCacheFactory cacheFactory) { + XAItemStateManager mgr = new XAItemStateManager(sharedStateMgr, factory, attributeName, cacheFactory); + sharedStateMgr.addListener(mgr); + return mgr; + } + + /** + * Set optional virtual item state provider. + */ + public void setVirtualProvider(VirtualItemStateProvider virtualProvider) { + this.virtualProvider = virtualProvider; + } + + /** + * {@inheritDoc} + */ + public void associate(TransactionContext tx) { + ChangeLog txLog = null; + if (tx != null) { + txLog = (ChangeLog) tx.getAttribute(attributeName); + if (txLog == null) { + txLog = new ChangeLog(); + tx.setAttribute(attributeName, txLog); + } + } + this.txLog = txLog; + } + + /** + * {@inheritDoc} + */ + public void beforeOperation(TransactionContext tx) { + ChangeLog txLog = (ChangeLog) tx.getAttribute(attributeName); + if (txLog != null) { + commitLogs.put(Thread.currentThread(), txLog); + } + } + + /** + * {@inheritDoc} + */ + public void prepare(TransactionContext tx) throws TransactionException { + ChangeLog txLog = (ChangeLog) tx.getAttribute(attributeName); + if (txLog != null && txLog.hasUpdates()) { + try { + if (virtualProvider != null) { + updateVirtualReferences(txLog); + } + update = sharedStateMgr.beginUpdate(txLog, factory, virtualProvider); + } catch (ReferentialIntegrityException rie) { + txLog.undo(sharedStateMgr); + throw new TransactionException("Unable to prepare transaction.", rie); + } catch (ItemStateException ise) { + txLog.undo(sharedStateMgr); + throw new TransactionException("Unable to prepare transaction.", ise); + } + } + } + + /** + * {@inheritDoc} + */ + public void commit(TransactionContext tx) throws TransactionException { + ChangeLog txLog = (ChangeLog) tx.getAttribute(attributeName); + if (txLog != null && txLog.hasUpdates()) { + try { + update.end(); + } catch (ItemStateException ise) { + txLog.undo(sharedStateMgr); + throw new TransactionException("Unable to commit transaction.", ise); + } + txLog.reset(); + } + } + + /** + * {@inheritDoc} + */ + public void rollback(TransactionContext tx) { + ChangeLog txLog = (ChangeLog) tx.getAttribute(attributeName); + if (txLog != null && txLog.hasUpdates()) { + if (update != null) { + update.cancel(); + } + txLog.undo(sharedStateMgr); + } + } + + /** + * {@inheritDoc} + */ + public void afterOperation(TransactionContext tx) { + commitLogs.remove(Thread.currentThread()); + } + + /** + * Returns the current change log. First tries thread-local change log, + * then instance-local change log. Returns null if no + * change log was found. + */ + public ChangeLog getChangeLog() { + ChangeLog changeLog = (ChangeLog) commitLogs.get(Thread.currentThread()); + if (changeLog == null) { + changeLog = txLog; + } + return changeLog; + } + + /** + * @throws UnsupportedOperationException always. + */ + protected ChangeLog getChanges() { + throw new UnsupportedOperationException("getChanges"); + } + + //-----------------------------------------------------< ItemStateManager > + /** + * {@inheritDoc} + *

    + * If this state manager is committing changes, this method first checks + * the commitLog ThreadLocal. Else if associated to a transaction check + * the transactional change log. Fallback is always the call to the base + * class. + */ + public ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + if (virtualProvider != null && virtualProvider.hasItemState(id)) { + return virtualProvider.getItemState(id); + } + // 1) check local changes + ChangeLog changeLog = super.getChanges(); + ItemState state = changeLog.get(id); + if (state != null) { + return state; + } + // 2) check tx log + changeLog = getChangeLog(); + if (changeLog != null) { + state = changeLog.get(id); + if (state != null) { + return state; + } + } + // 3) fallback to base class + return super.getItemState(id); + } + + /** + * {@inheritDoc} + *

    + * If this state manager is committing changes, this method first checks + * the commitLog ThreadLocal. Else if associated to a transaction check + * the transactional change log. Fallback is always the call to the base + * class. + */ + public boolean hasItemState(ItemId id) { + if (virtualProvider != null && virtualProvider.hasItemState(id)) { + return true; + } + // 1) check local changes + ChangeLog changeLog = super.getChanges(); + try { + ItemState state = changeLog.get(id); + if (state != null) { + return true; + } + } catch (NoSuchItemStateException e) { + // marked removed in local ism + return false; + } + // if we get here, then there is no item state with + // the given id known to the local ism + // 2) check tx log + changeLog = getChangeLog(); + if (changeLog != null) { + try { + ItemState state = changeLog.get(id); + if (state != null) { + return true; + } + } catch (NoSuchItemStateException e) { + // marked removed in tx log + return false; + } + } + // 3) fallback to shared ism + return sharedStateMgr.hasItemState(id); + } + + /** + * {@inheritDoc} + *

    + * If this state manager is committing changes, this method first + * checks the commitLog ThreadLocal. Else if associated to a transaction + * check the transactional change log. Fallback is always the call to + * the base class. + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + if (virtualProvider != null && virtualProvider.hasNodeReferences(id)) { + return virtualProvider.getNodeReferences(id); + } + return getReferences(id); + } + + /** + * {@inheritDoc} + *

    + * If this state manager is committing changes, this method first + * checks the commitLog ThreadLocal. Else if associated to a transaction + * check the transactional change log. Fallback is always the call to + * the base class. + */ + public boolean hasNodeReferences(NodeId id) { + if (virtualProvider != null && virtualProvider.hasNodeReferences(id)) { + return true; + } + try { + return getReferences(id).hasReferences(); + } catch (ItemStateException e) { + return false; + } + } + + /** + * {@inheritDoc} + *

    + * If associated with a transaction, simply merge the changes given to + * the ones already known (removing items that were first added and + * then again deleted). + */ + protected void update(ChangeLog changeLog) + throws ReferentialIntegrityException, StaleItemStateException, + ItemStateException { + if (txLog != null) { + txLog.merge(changeLog); + } else { + super.update(changeLog); + } + } + + //-------------------------------------------------------< implementation > + + /** + * Returns the node references for the given id. + * + * @param id the node references id. + * @return the node references for the given id. + * @throws ItemStateException if an error occurs while reading from the + * underlying shared item state manager. + */ + private NodeReferences getReferences(NodeId id) + throws ItemStateException { + NodeReferences refs; + try { + refs = super.getNodeReferences(id); + } catch (NoSuchItemStateException e) { + refs = new NodeReferences(id); + } + // apply changes from change log + ChangeLog changes = getChangeLog(); + if (changes != null) { + // check removed reference properties + for (PropertyState prop : filterReferenceProperties(changes.deletedStates())) { + InternalValue[] values = prop.getValues(); + for (int i = 0; i < values.length; i++) { + if (values[i].getNodeId().equals(id)) { + refs.removeReference(prop.getPropertyId()); + break; + } + } + } + // check added reference properties + for (PropertyState prop : filterReferenceProperties(changes.addedStates())) { + InternalValue[] values = prop.getValues(); + for (int i = 0; i < values.length; i++) { + if (values[i].getNodeId().equals(id)) { + refs.addReference(prop.getPropertyId()); + break; + } + } + } + // check modified properties + for (ItemState state : changes.modifiedStates()) { + if (state.isNode()) { + continue; + } + try { + PropertyState old = (PropertyState) sharedStateMgr.getItemState(state.getId()); + if (old.getType() == PropertyType.REFERENCE) { + // remove if one of the old values references the node + InternalValue[] values = old.getValues(); + for (int i = 0; i < values.length; i++) { + if (values[i].getNodeId().equals(id)) { + refs.removeReference(old.getPropertyId()); + break; + } + } + } + } catch (NoSuchItemStateException e) { + // property is stale + } + + PropertyState prop = (PropertyState) state; + if (prop.getType() == PropertyType.REFERENCE) { + // add if modified value references node + InternalValue[] values = prop.getValues(); + for (int i = 0; i < values.length; i++) { + if (values[i].getNodeId().equals(id)) { + refs.addReference(prop.getPropertyId()); + break; + } + } + } + } + } + return refs; + } + + /** + * Takes an iterator over {@link ItemState}s and returns a new iterator that + * filters out all but REFERENCE {@link PropertyState}s. + * + * @param itemStates item state source iterator. + * @return iterator over reference property states. + */ + private Iterable filterReferenceProperties( + final Iterable itemStates) { + return new Iterable() { + @SuppressWarnings("unchecked") + public Iterator iterator() { + return (Iterator) new FilterIterator( + itemStates.iterator(), new Predicate() { + public boolean evaluate(Object object) { + ItemState state = (ItemState) object; + if (!state.isNode()) { + PropertyState prop = (PropertyState) state; + return prop.getType() == PropertyType.REFERENCE; + } + return false; + } + }); + } + }; + } + + /** + * Determine all node references whose targets only exist in the view of + * this transaction and store the modified view back to the virtual provider. + * @param changes change log + * @throws ItemStateException if an error occurs + */ + private void updateVirtualReferences(ChangeLog changes) throws ItemStateException { + ChangeLog references = new ChangeLog(); + + for (ItemState state : changes.addedStates()) { + if (!state.isNode()) { + PropertyState prop = (PropertyState) state; + if (prop.getType() == PropertyType.REFERENCE) { + InternalValue[] vals = prop.getValues(); + for (int i = 0; vals != null && i < vals.length; i++) { + addVirtualReference( + references, prop.getPropertyId(), + vals[i].getNodeId()); + } + } + } + } + for (ItemState state : changes.modifiedStates()) { + if (!state.isNode()) { + PropertyState newProp = (PropertyState) state; + PropertyState oldProp = + (PropertyState) getItemState(state.getId()); + if (oldProp.getType() == PropertyType.REFERENCE) { + InternalValue[] vals = oldProp.getValues(); + for (int i = 0; vals != null && i < vals.length; i++) { + removeVirtualReference( + references, oldProp.getPropertyId(), + vals[i].getNodeId()); + } + } + if (newProp.getType() == PropertyType.REFERENCE) { + InternalValue[] vals = newProp.getValues(); + for (int i = 0; vals != null && i < vals.length; i++) { + addVirtualReference( + references, newProp.getPropertyId(), + vals[i].getNodeId()); + } + } + } + } + for (ItemState state : changes.deletedStates()) { + if (!state.isNode()) { + PropertyState prop = (PropertyState) state; + if (prop.getType() == PropertyType.REFERENCE) { + InternalValue[] vals = prop.getValues(); + for (int i = 0; vals != null && i < vals.length; i++) { + removeVirtualReference( + references, prop.getPropertyId(), + vals[i].getNodeId()); + } + } + } + } + + virtualProvider.setNodeReferences(references); + } + + /** + * Add a virtual reference from some reference property to a virtual node. + * Ignored if refsId.getTargetId() does not denote a + * virtual node. + * @param sourceId property id + * @param targetId target node id + */ + private void addVirtualReference( + ChangeLog references, PropertyId sourceId, NodeId targetId) + throws NoSuchItemStateException, ItemStateException { + + NodeReferences refs = references.getReferencesTo(targetId); + if (refs == null) { + refs = virtualProvider.getNodeReferences(targetId); + } + if (refs == null && virtualProvider.hasItemState(targetId)) { + refs = new NodeReferences(targetId); + } + if (refs != null) { + refs.addReference(sourceId); + references.modified(refs); + } + } + + /** + * Remove a virtual reference from some reference property to a virtual node. + * Ignored if refsId.getTargetId() does not denote a + * virtual node. + * @param sourceId property id + * @param targetId target node id + */ + private void removeVirtualReference( + ChangeLog references, PropertyId sourceId, NodeId targetId) + throws NoSuchItemStateException, ItemStateException { + + NodeReferences refs = references.getReferencesTo(targetId); + if (refs == null) { + refs = virtualProvider.getNodeReferences(targetId); + } + if (refs == null && virtualProvider.hasItemState(targetId)) { + refs = new NodeReferences(targetId); + } + if (refs != null) { + refs.removeReference(sourceId); + references.modified(refs); + } + } + + /** + * {@inheritDoc} + * + * Check whether the shared state modified is contained in our transactional + * log: in that case, update its state as well, as it might get reused + * in a subsequent transaction (see JCR-1554). + */ + public void stateModified(ItemState modified) { + ChangeLog changeLog = (ChangeLog) commitLogs.get(Thread.currentThread()); + if (changeLog != null) { + ItemState local; + if (modified.getContainer() != this) { + // shared state was modified + try { + local = changeLog.get(modified.getId()); + if (local != null && local.isConnected()) { + local.pull(); + } + } catch (NoSuchItemStateException e) { + log.warn("Modified state marked for deletion: " + modified.getId()); + } + } + } + super.stateModified(modified); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/stats/StatManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/stats/StatManager.java new file mode 100644 index 00000000000..d71c0172739 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/stats/StatManager.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.core.stats; + +import static java.lang.Boolean.getBoolean; + +import org.apache.jackrabbit.stats.QueryStatCore; +import org.apache.jackrabbit.stats.QueryStatImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * StatManager represents a single entry point to the statistics objects + * available.
    + * + */ +public class StatManager { + + public static String ALL_STATS_ENABLED_PROPERTY = "org.apache.jackrabbit.api.stats.ALL"; + public static String QUERY_STATS_ENABLED_PROPERTY = "org.apache.jackrabbit.api.stats.QueryStat"; + + private static final Logger log = LoggerFactory + .getLogger(StatManager.class); + + /* STAT OBJECTS */ + private final QueryStatCore queryStat = new QueryStatImpl(); + + public StatManager() { + init(); + } + + protected void init() { + boolean allEnabled = getBoolean(ALL_STATS_ENABLED_PROPERTY); + queryStat.setEnabled(allEnabled + || getBoolean(QUERY_STATS_ENABLED_PROPERTY)); + log.debug( + "Started StatManager. QueryStat is enabled {}", + new Object[] { queryStat.isEnabled() }); + } + + public QueryStatCore getQueryStat() { + return queryStat; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/CooperativeFileLock.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/CooperativeFileLock.java new file mode 100644 index 00000000000..a8fa45d3713 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/CooperativeFileLock.java @@ -0,0 +1,321 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.util.Properties; +import java.util.UUID; + +import javax.jcr.RepositoryException; + +/** + * The file lock is used to ensure a resource is only open once at any time. + * It uses a cooperative locking protocol. + */ +public class CooperativeFileLock implements RepositoryLockMechanism { + + /** + * Logger instance. + */ + private static final Logger LOG = + LoggerFactory.getLogger(CooperativeFileLock.class); + + private static final String MAGIC = "CooperativeFileLock"; + private static final String FILE_NAME = "lock.properties"; + private static final int MAX_FILE_RETRY = 16; + private static final int SLEEP_GAP = 25; + private static final int TIME_GRANULARITY = 2000; + private static final int LOCK_SLEEP = 1000; + + private String fileName; + private long lastWrite; + private Properties properties; + private boolean locked; + private volatile boolean stop; + + private Thread watchdog; + + /** + * Create a new file locking object using the given file name. + * + * @param path basic path to append {@link #FILE_NAME} to. + */ + public void init(String path) { + this.fileName = path + File.separatorChar + FILE_NAME; + } + + /** + * Lock the directory if possible. + * This method will also start a background watchdog thread. + * A file may only be locked once. + * + * @throws RepositoryException if locking was not successful + */ + public synchronized void acquire() throws RepositoryException { + if (locked) { + throw new RepositoryException("Already locked " + fileName); + } + stop = false; + lockFile(); + locked = true; + } + + /** + * Unlock the directory. + * The watchdog thread is stopped. + * This method does nothing if the file is already unlocked. + */ + public synchronized void release() { + if (!locked) { + return; + } + try { + stop = true; + if (fileName != null) { + if (load().equals(properties)) { + delete(fileName); + } + } + } catch (Exception e) { + LOG.warn("Error unlocking " + fileName, e); + } finally { + locked = false; + } + try { + if (watchdog != null) { + watchdog.interrupt(); + } + } catch (Exception e) { + LOG.debug("Error stopping watchdog " + fileName, e); + } + } + + /** + * Save the properties file. + */ + private void save() throws RepositoryException { + try { + OutputStream out = new FileOutputStream(fileName); + try { + properties.store(out, MAGIC); + } finally { + out.close(); + } + lastWrite = new File(fileName).lastModified(); + if (LOG.isDebugEnabled()) { + LOG.debug("Save " + properties); + } + } catch (IOException e) { + throw getException(e); + } + } + + /** + * Load the properties file. + * + * @return the properties object + */ + private Properties load() throws RepositoryException { + try { + Properties p2 = new Properties(); + InputStream in = new FileInputStream(fileName); + try { + p2.load(in); + } finally { + in.close(); + } + if (LOG.isDebugEnabled()) { + LOG.debug("Load " + p2); + } + return p2; + } catch (IOException e) { + throw getException(e); + } + } + + /** + * Wait until the file is old (not modified for a certain time). + */ + private void waitUntilOld() throws RepositoryException { + for (int i = 0; i < TIME_GRANULARITY / SLEEP_GAP; i++) { + File f = new File(fileName); + long last = f.lastModified(); + long dist = System.currentTimeMillis() - last; + if (dist < -TIME_GRANULARITY) { + // lock file modified in the future - + // wait for a bit longer than usual + try { + Thread.sleep(2 * LOCK_SLEEP); + } catch (Exception e) { + LOG.debug("Sleep", e); + } + return; + } else if (dist > TIME_GRANULARITY) { + return; + } + try { + Thread.sleep(SLEEP_GAP); + } catch (Exception e) { + LOG.debug("Sleep", e); + } + } + throw error("Lock file recently modified"); + } + + /** + * Lock the file. + */ + private void lockFile() throws RepositoryException { + properties = new Properties(); + UUID uuid = UUID.randomUUID(); + properties.setProperty("id", uuid.toString()); + if (!createNewFile(fileName)) { + waitUntilOld(); + save(); + // wait twice the watchdog sleep time + for (int i = 0; i < 8; i++) { + sleep(LOCK_SLEEP / 4); + if (!load().equals(properties)) { + throw error("Locked by another process"); + } + } + delete(fileName); + if (!createNewFile(fileName)) { + throw error("Another process was faster"); + } + } + save(); + sleep(SLEEP_GAP); + if (!load().equals(properties)) { + stop = true; + throw error("Concurrent update"); + } + watchdog = new Thread(new Runnable() { + public void run() { + try { + while (!stop) { + // debug("Watchdog check"); + try { + File f = new File(fileName); + if (!f.exists() || f.lastModified() != lastWrite) { + save(); + } + Thread.sleep(LOCK_SLEEP); + } catch (OutOfMemoryError e) { + // ignore + } catch (InterruptedException e) { + // ignore + } catch (NullPointerException e) { + // ignore + } catch (Exception e) { + LOG.debug("Watchdog", e); + } + } + } catch (Exception e) { + LOG.debug("Watchdog", e); + } + LOG.debug("Watchdog end"); + } + }); + watchdog.setName(MAGIC + " Watchdog " + fileName); + watchdog.setDaemon(true); + watchdog.setPriority(Thread.MAX_PRIORITY - 1); + watchdog.start(); + } + + private RepositoryException getException(Throwable t) { + return new RepositoryException("Internal error in file lock " + fileName, t); + } + + private RepositoryException error(String reason) { + return new RepositoryException("Error locking " + fileName + ", reason: " + reason); + } + + private void sleep(int time) throws RepositoryException { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + throw getException(e); + } + } + + /** + * Create a new file, and retry if this doesn't work. + * If it still doesn't work after some time, this method returns false. + * + * @param fileName the name of the file to create + * @return if the file was created + */ + private static boolean createNewFile(String fileName) { + File file = new File(fileName); + 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; + } + + /** + * Delete a file, and retry if this doesn't work. + * If it still doesn't work after some time, an exception is thrown. + * + * @param fileName the name of the file to delete + * @throws RepositoryException if the file could not be deleted + */ + private static void delete(String fileName) throws RepositoryException { + File file = new File(fileName); + if (file.exists()) { + for (int i = 0; i < MAX_FILE_RETRY; i++) { + if (LOG.isDebugEnabled()) { + LOG.debug("Deleting " + fileName); + } + boolean ok = file.delete(); + if (ok) { + return; + } + wait(i); + } + throw new RepositoryException("Could not delete file " + fileName); + } + } + + 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 + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/DOMBuilder.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/DOMBuilder.java new file mode 100644 index 00000000000..b0328fb4e25 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/DOMBuilder.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.util; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Document builder class. This class provides an intuitive + * interface for incrementally building DOM documents. + */ +public final class DOMBuilder { + + /** Static factory for creating DOM DocumentBuilder instances. */ + private static final DocumentBuilderFactory BUILDER_FACTORY = + DocumentBuilderFactory.newInstance(); + + /** Static factory for creating document to output stream transformers. */ + private static final TransformerFactory TRANSFORMER_FACTORY = + TransformerFactory.newInstance(); + + /** The DOM document being built by this builder. */ + private final Document document; + + /** The current element. */ + private Element current; + + /** + * Creates a builder for a new DOM document. A new DOM document is + * instantiated and initialized to contain a root element with the given + * name. The root element is set as the current element of this builder. + * + * @param name name of the root element + * @throws ParserConfigurationException if a document cannot be created + */ + public DOMBuilder(String name) throws ParserConfigurationException { + DocumentBuilder builder = BUILDER_FACTORY.newDocumentBuilder(); + document = builder.newDocument(); + current = document.createElement(name); + document.appendChild(current); + } + + + /** + * Writes the document built by this builder into the given output stream. + * This method is normally invoked only when the document is fully built. + * + * @param xml XML output stream + * @throws IOException if the document could not be written + */ + public void write(OutputStream xml) throws IOException { + try { + Transformer transformer = TRANSFORMER_FACTORY.newTransformer(); + transformer.transform( + new DOMSource(document), new StreamResult(xml)); + } catch (TransformerConfigurationException e) { + IOException ioe = new IOException(e.getMessage()); + ioe.initCause(e); + throw ioe; + } catch (TransformerException e) { + IOException ioe = new IOException(e.getMessage()); + ioe.initCause(e); + throw ioe; + } + } + + /** + * Creates a new element with the given name as the child of the + * current element and makes the created element current. The + * {@link #endElement() endElement} method needs to be called + * to return back to the original element. + * + * @param name name of the new element + */ + public void startElement(String name) { + Element element = document.createElement(name); + current.appendChild(element); + current = element; + } + + /** + * Makes the parent element current. This method should be invoked + * after a child element created with the + * {@link #startElement(String) startElement} method has been fully + * built. + */ + public void endElement() { + current = (Element) current.getParentNode(); + } + + /** + * Sets the named attribute of the current element. + * + * @param name attribute name + * @param value attribute value + */ + public void setAttribute(String name, String value) { + current.setAttribute(name, value); + } + + /** + * Sets the named boolean attribute of the current element. + * + * @param name attribute name + * @param value boolean attribute value + */ + public void setAttribute(String name, boolean value) { + setAttribute(name, String.valueOf(value)); + } + + /** + * Adds the given string as text content to the current element. + * + * @param content text content + */ + public void addContent(String content) { + current.appendChild(document.createTextNode(content)); + } + + /** + * Adds a new child element with the given name and text content. + * The created element will contain no attributes and no child elements + * of its own. + * + * @param name child element name + * @param content child element content + */ + public void addContentElement(String name, String content) { + startElement(name); + addContent(content); + endElement(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/DOMWalker.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/DOMWalker.java new file mode 100644 index 00000000000..9689f7cba7d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/DOMWalker.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.util; + +import org.w3c.dom.Attr; +import org.w3c.dom.CharacterData; +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 javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Document walker class. This class provides an intuitive + * interface for traversing a parsed DOM document. + */ +public final class DOMWalker { + + /** Static factory for creating stream to DOM transformers. */ + private static final DocumentBuilderFactory factory = + DocumentBuilderFactory.newInstance(); + + /** The DOM document being traversed by this walker. */ + private final Document document; + + /** The current element. */ + private Element current; + + /** + * Creates a walker for traversing a DOM document read from the given + * input stream. The root element of the document is set as the current + * element. + * + * @param xml XML input stream + * @throws IOException if a document cannot be read from the stream + */ + public DOMWalker(InputStream xml) throws IOException { + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + document = builder.parse(xml); + current = document.getDocumentElement(); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw (IOException) new IOException(e.getMessage()).initCause(e); + } + } + + /** + * Returns the namespace mappings defined in the current element. + * The returned property set contains the prefix to namespace + * mappings specified by the xmlns attributes of the + * current element. + * + * @return prefix to namespace mappings of the current element + */ + public Properties getNamespaces() { + Properties namespaces = new Properties(); + NamedNodeMap attributes = current.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Attr attribute = (Attr) attributes.item(i); + if (attribute.getName().startsWith("xmlns:")) { + namespaces.setProperty( + attribute.getName().substring(6), attribute.getValue()); + } + } + return namespaces; + } + + /** + * Returns the name of the current element. + * + * @return element name + */ + public String getName() { + return current.getNodeName(); + } + + /** + * Returns the value of the named attribute of the current element. + * + * @param name attribute name + * @return attribute value, or null if not found + */ + public String getAttribute(String name) { + Attr attribute = current.getAttributeNode(name); + if (attribute != null) { + return attribute.getValue(); + } else { + return null; + } + } + + /** + * Returns the text content of the current element. + * + * @return text content + */ + public String getContent() { + StringBuilder content = new StringBuilder(); + + NodeList nodes = current.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + if (node.getNodeType() == Node.TEXT_NODE) { + content.append(((CharacterData) node).getData()); + } + } + + return content.toString(); + } + + /** + * Enters the named child element. If the named child element is + * found, then it is made the current element and true + * is returned. Otherwise the current element is not changed and + * false is returned. + *

    + * The standard call sequence for this method is show below. + *

    +     *     DOMWalker walker = ...;
    +     *     if (walker.enterElement("...")) {
    +     *         ...;
    +     *         walker.leaveElement();
    +     *     }
    +     * 
    + * + * @param name child element name + * @return true if the element was entered, + * false otherwise + */ + public boolean enterElement(String name) { + NodeList children = current.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE + && name.equals(child.getNodeName())) { + current = (Element) child; + return true; + } + } + return false; + } + + /** + * Leaves the current element. The parent element is set as the new + * current element. + * + * @see #enterElement(String) + */ + public void leaveElement() { + current = (Element) current.getParentNode(); + } + + /** + * Iterates through the named child elements over multiple calls. + * This method makes it possible to use the following code to + * walk through all the child elements with the given name. + *
    +     *     DOMWalker walker = ...;
    +     *     while (walker.iterateElements("...")) {
    +     *         ...;
    +     *     }
    +     * 
    + *

    + * WARNING: This method should only be used when + * walker.getName() does not equal name when + * the while loop is started. Otherwise the walker will not be positioned + * at the same node when the while loop ends. + * + * @param name name of the iterated elements + * @return true if another iterated element was entered, or + * false if no more iterated elements were found + * and the original element is restored as the current element + */ + public boolean iterateElements(String name) { + Node next; + if (name.equals(current.getNodeName())) { + next = current.getNextSibling(); + } else { + next = current.getFirstChild(); + } + + while (next != null) { + if (next.getNodeType() == Node.ELEMENT_NODE + && name.equals(next.getNodeName())) { + current = (Element) next; + return true; + } else { + next = next.getNextSibling(); + } + } + + if (name.equals(current.getNodeName())) { + Node parent = current.getParentNode(); + if (parent instanceof Element) { + current = (Element) parent; + } + } + return false; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/EmptyLinkedMap.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/EmptyLinkedMap.java new file mode 100644 index 00000000000..5c392dde1db --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/EmptyLinkedMap.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.core.util; + +import org.apache.commons.collections.map.LinkedMap; + +import java.util.Map; +import java.util.Set; +import java.util.Collection; +import java.util.Collections; + +/** + * EmptyLinkedMap implements an empty unmodifiable {@link LinkedMap}. + */ +public class EmptyLinkedMap extends LinkedMap { + + private static final long serialVersionUID = -9165910643562370800L; + + /** + * The only instance of this class. + */ + public static final LinkedMap INSTANCE = new EmptyLinkedMap(); + + private EmptyLinkedMap() { + super(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public Object remove(int i) { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public void clear() { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public Object put(Object o, Object o1) { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public void putAll(Map map) { + throw new UnsupportedOperationException(); + } + + /** + * @throws UnsupportedOperationException always. + */ + public Object remove(Object o) { + throw new UnsupportedOperationException(); + } + + /** + * Returns an unmodifiable empty set. + * + * @return an unmodifiable empty set. + */ + public Set entrySet() { + return Collections.EMPTY_SET; + } + + /** + * Returns an unmodifiable empty set. + * + * @return an unmodifiable empty set. + */ + public Set keySet() { + return Collections.EMPTY_SET; + } + + /** + * Returns an unmodifiable empty collection. + * + * @return an unmodifiable empty collection. + */ + public Collection values() { + return Collections.EMPTY_LIST; + } + + //----------------------------------------------------< Cloneable support > + + /** + * Returns the single instance of this class. + * + * @return {@link #INSTANCE}. + */ + public Object clone() { + return INSTANCE; + } + + //-------------------------------------------------< Serializable support > + + /** + * Returns the single instance of this class. + * + * @return {@link #INSTANCE}. + */ + private Object readResolve() { + return INSTANCE; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/ReferenceChangeTracker.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/ReferenceChangeTracker.java new file mode 100644 index 00000000000..208f913f2e5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/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.core.util; + +import org.apache.jackrabbit.core.id.NodeId; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.List; + +/** + * Simple helper class that can be used to keep track of node id mappings + * (e.g. if the id of an imported or copied node is mapped to a new id) + * and processed (e.g. imported or copied) reference properties that might + * need correcting depending on the id mappings. + */ +public class ReferenceChangeTracker { + + /** + * mapping from original id to new id of mix:referenceable nodes + */ + private final Map idMap = new HashMap(); + + /** + * list of processed reference properties that might need correcting + */ + private final ArrayList references = new ArrayList(); + + /** + * Resets all internal state. + */ + public void clear() { + idMap.clear(); + references.clear(); + } + + /** + * Store the given id mapping for later lookup using + * {@link #getMappedId(NodeId)}. + * + * @param oldId old node id + * @param newId new node id + */ + public void mappedId(NodeId oldId, NodeId newId) { + idMap.put(oldId, newId); + } + + /** + * Store the given reference property for later retrieval using + * {@link #getProcessedReferences()}. + * + * @param refProp reference property + */ + public void processedReference(Object refProp) { + references.add(refProp); + } + + /** + * Returns the new node id to which oldId has been mapped + * or null if no such mapping exists. + * + * @param oldId old node id + * @return mapped new id or null if no such mapping exists + * @see #mappedId(NodeId, NodeId) + */ + public NodeId getMappedId(NodeId oldId) { + return idMap.get(oldId); + } + + /** + * 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 true if the internal list of references changed. + */ + public boolean removeReferences(List processedReferences) { + return references.removeAll(processedReferences); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLock.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLock.java new file mode 100644 index 00000000000..d9268a7c5a7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLock.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.util; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; + +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Exclusive lock on a repository home directory. This class encapsulates + * collective experience on how to acquire an exclusive lock on a given + * directory. The lock is expected to be exclusive both across process + * boundaries and within a single JVM. The lock mechanism must also work + * consistently on a variety of operating systems and JVM implementations. + * + * @see JCR-213 + * @see JCR-233 + * @see JCR-254 + * @see JCR-912 + * @see JCR-933 + */ +public class RepositoryLock implements RepositoryLockMechanism { + + /** + * Name of the lock file within a directory. + */ + private static final String LOCK = ".lock"; + + /** + * Logger instance. + */ + private static final Logger LOG = + LoggerFactory.getLogger(RepositoryLock.class); + + /** + * The locked directory. + */ + private File directory; + + /** + * The lock file within the given directory. + */ + private File file; + + /** + * The random access file. + */ + private RandomAccessFile randomAccessFile; + + /** + * Unique identifier (canonical path name) of the locked directory. + * Used to ensure exclusive locking within a single JVM. + * + * @see https://issues.apache.org/jira/browse/JCR-933 + */ + private String identifier; + + /** + * The file lock. Used to ensure exclusive locking across process boundaries. + * + * @see https://issues.apache.org/jira/browse/JCR-233 + */ + private FileLock lock; + + public RepositoryLock() { + // used by the factory + } + + /** + * Create a new RepositoryLock object and initialize it. + * @deprecated + * This constructor is deprecated; use the default constructor + * and {@link #init(String)} instead. + * + * @param path directory path + * @throws RepositoryException if the canonical path of the directory + * can not be determined + */ + public RepositoryLock(String path) throws RepositoryException { + init(path); + } + + /** + * Initialize the instance for the given directory path. The lock still needs to be + * explicitly acquired using the {@link #acquire()} method. + * + * @param path directory path + * @throws RepositoryException if the canonical path of the directory + * can not be determined + */ + public void init(String path) throws RepositoryException { + try { + directory = new File(path).getCanonicalFile(); + file = new File(directory, LOCK); + identifier = + (RepositoryLock.class.getName() + ":" + directory.getPath()) + .intern(); + lock = null; + } catch (IOException e) { + throw new RepositoryException( + "Unable to determine canonical path of " + path, e); + } + } + + /** + * Lock the repository home. + * + * @throws RepositoryException if the repository lock can not be acquired + */ + public void acquire() throws RepositoryException { + if (file.exists()) { + LOG.warn("Existing lock file " + file + " detected." + + " Repository was not shut down properly."); + } + try { + tryLock(); + } catch (RepositoryException e) { + closeRandomAccessFile(); + throw e; + } + } + + /** + * Try to lock the random access file. + * + * @throws RepositoryException + */ + private void tryLock() throws RepositoryException { + try { + randomAccessFile = new RandomAccessFile(file, "rw"); + lock = randomAccessFile.getChannel().tryLock(); + } catch (IOException e) { + throw new RepositoryException( + "Unable to create or lock file " + file, e); + } catch (OverlappingFileLockException e) { + // JCR-912: OverlappingFileLockException with JRE 1.6 + throw new RepositoryException( + "The repository home " + directory + " appears to be in use" + + " since the file named " + file.getName() + + " is already locked by the current process."); + } + + if (lock == null) { + throw new RepositoryException( + "The repository home " + directory + " appears to be in use" + + " since the file named " + file.getName() + + " is locked by another process."); + } + + // JCR-933: due to a bug in java 1.4/1.5 on *nix platforms + // it's possible that java.nio.channels.FileChannel.tryLock() + // returns a non-null FileLock object although the lock is already + // held by *this* jvm process + synchronized (identifier) { + if (null != System.getProperty(identifier)) { + // note that the newly acquired (redundant) file lock + // is deliberately *not* released because this could + // potentially cause, depending on the implementation, + // the previously acquired lock(s) to be released + // as well + throw new RepositoryException( + "The repository home " + directory + " appears to be" + + " already locked by the current process."); + } else { + try { + System.setProperty(identifier, identifier); + } catch (SecurityException e) { + LOG.warn("Unable to set system property: " + identifier, e); + } + } + } + } + + /** + * Close the random access file if it is open, and set it to null. + */ + private void closeRandomAccessFile() { + if (randomAccessFile != null) { + try { + randomAccessFile.close(); + } catch (IOException e) { + LOG.warn("Unable to close the random access file " + file, e); + } + randomAccessFile = null; + } + } + + /** + * Releases repository lock. + */ + public void release() { + if (lock != null) { + try { + FileChannel channel = lock.channel(); + lock.release(); + channel.close(); + } catch (IOException e) { + // ignore + } + lock = null; + closeRandomAccessFile(); + } + + if (!file.delete()) { + LOG.warn("Unable to delete repository lock file"); + } + + // JCR-933: see #acquire() + synchronized (identifier) { + try { + System.getProperties().remove(identifier); + } catch (SecurityException e) { + LOG.error("Unable to clear system property: " + identifier, e); + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLockMechanism.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLockMechanism.java new file mode 100644 index 00000000000..08e7fbec348 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLockMechanism.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.core.util; + +import javax.jcr.RepositoryException; + +/** + * Exclusive lock on a repository home directory. This class encapsulates + * collective experience on how to acquire an exclusive lock on a given + * directory. The lock is expected to be exclusive both across process + * boundaries and within a single JVM. + */ +public interface RepositoryLockMechanism { + + /** + * Initialize the instance for the given directory path. The lock still needs to be + * explicitly acquired using the {@link #acquire()} method. + * + * @param homeDir directory path + * @throws RepositoryException if the canonical path of the directory + * can not be determined + */ + void init(String homeDir) throws RepositoryException; + + /** + * Lock the repository home. + * + * @throws RepositoryException if the repository lock can not be acquired + */ + void acquire() throws RepositoryException; + + /** + * Releases repository lock. + * + * @throws RepositoryException if the repository lock can not be released + */ + void release() throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLockMechanismFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLockMechanismFactory.java new file mode 100644 index 00000000000..c9982b2d287 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/RepositoryLockMechanismFactory.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.core.util; + +import javax.jcr.RepositoryException; + +/** + * Factory interface for creating {@link RepositoryLockMechanism} instances. Used + * to decouple the repository internals from the repository configuration + * mechanism. + * + * @since Jackrabbit 1.5 + * @see JCR-1438 + */ +public interface RepositoryLockMechanismFactory { + + /** + * Creates, configures, and returns a {@link RepositoryLockMechanism} instance + * for use by the repository. Note that no information is passed from + * the client, so all required configuration information must be + * encapsulated in the factory. + * + * @return the configures repository lock mechanism + * @throws RepositoryException if the repository lock mechanism can not be created + */ + RepositoryLockMechanism getRepositoryLockMechanism() throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/StringIndex.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/StringIndex.java new file mode 100644 index 00000000000..e2cbb3263c2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/StringIndex.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.core.util; + +/** + * A persistent two-way mapping between strings and index integers. The + * index may or may not be sequential. + */ +public interface StringIndex { + + /** + * Returns the index for a given string. If the given string is not + * already indexed, the implementation can either automatically index + * it or throw an exception. + * + * @param string the indexed (or to be indexed) string + * @return index of the string + * @throws IllegalArgumentException if the string is not + * (and can not be) indexed + */ + int stringToIndex(String string) throws IllegalArgumentException; + + /** + * Returns the string for a given index. + * + * @param idx index of a string + * @return the indexed string + * @throws IllegalArgumentException if the indexed string does not exist + */ + String indexToString(int idx) throws IllegalArgumentException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/XAReentrantLock.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/XAReentrantLock.java new file mode 100644 index 00000000000..cf0eeb45a96 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/XAReentrantLock.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.core.util; + +import static org.apache.jackrabbit.data.core.TransactionContext.isSameThreadId; + +import org.apache.jackrabbit.data.core.TransactionContext; + +import EDU.oswego.cs.dl.util.concurrent.ReentrantLock; + +/** + * A reentrant lock for synchronization. + * Unlike a normal reentrant lock, this one allows the lock + * to be re-entered not just by a thread that's already holding the lock but + * by any thread within the same transaction. + */ +public class XAReentrantLock extends ReentrantLock { + + /** + * The active lock holder of this {@link ReentrantLock} + */ + private Object activeId; + + /** + * {@inheritDoc} + */ + @Override + public void acquire() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + Object currentId = TransactionContext.getCurrentThreadId(); + synchronized(this) { + if (currentId == activeId || (activeId != null && isSameThreadId(activeId, currentId))) { + ++holds_; + } else { + try { + while (activeId != null) { + wait(); + } + activeId = currentId; + holds_ = 1; + } catch (InterruptedException ex) { + notify(); + throw ex; + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void release() { + Object currentId = TransactionContext.getCurrentThreadId(); + if (activeId != null && !isSameThreadId(activeId, currentId)) { + throw new Error("Illegal Lock usage"); + } + + if (--holds_ == 0) { + activeId = null; + notify(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/XAReentrantWriterPreferenceReadWriteLock.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/XAReentrantWriterPreferenceReadWriteLock.java new file mode 100644 index 00000000000..109a8d82711 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/util/XAReentrantWriterPreferenceReadWriteLock.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.core.util; + +import static org.apache.jackrabbit.data.core.TransactionContext.getCurrentThreadId; +import static org.apache.jackrabbit.data.core.TransactionContext.isSameThreadId; +import EDU.oswego.cs.dl.util.concurrent.ReentrantWriterPreferenceReadWriteLock; + +/** + * A reentrant read-write lock for synchronization. + * Unlike a normal reentrant lock, this one allows the lock + * to be re-entered not just by a thread that's already holding the lock but + * by any thread within the same transaction. + */ +public class XAReentrantWriterPreferenceReadWriteLock extends ReentrantWriterPreferenceReadWriteLock{ + + private Object activeWriter; + + /** + * {@inheritDoc} + */ + protected boolean allowReader() { + Object currentId = getCurrentThreadId(); + return (activeWriter == null && waitingWriters_ == 0) || isSameThreadId(activeWriter, currentId); + } + + /** + * {@inheritDoc} + */ + protected synchronized boolean startWrite() { + Object currentId = getCurrentThreadId(); + if (activeWriter != null && isSameThreadId(activeWriter, currentId)) { // already held; re-acquire + ++writeHolds_; + return true; + } else if (writeHolds_ == 0) { + if (activeReaders_ == 0 || (readers_.size() == 1 && readers_.get(currentId) != null)) { + activeWriter = currentId; + writeHolds_ = 1; + return true; + } else { + return false; + } + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + protected synchronized Signaller endWrite() { + --writeHolds_; + if (writeHolds_ > 0) { // still being held + return null; + } else { + activeWriter = null; + if (waitingReaders_ > 0 && allowReader()) { + return readerLock_; + } else if (waitingWriters_ > 0) { + return writerLock_; + } else { + return null; + } + } + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + protected synchronized boolean startRead() { + Object currentId = getCurrentThreadId(); + Object c = readers_.get(currentId); + if (c != null) { // already held -- just increment hold count + readers_.put(currentId, new Integer(((Integer)(c)).intValue()+1)); + ++activeReaders_; + return true; + } else if (allowReader()) { + readers_.put(currentId, IONE); + ++activeReaders_; + return true; + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + protected synchronized Signaller endRead() { + Object currentId = getCurrentThreadId(); + Object c = readers_.get(currentId); + if (c == null) { + throw new IllegalStateException(); + } + --activeReaders_; + if (c != IONE) { // more than one hold; decrement count + int h = ((Integer)(c)).intValue()-1; + Integer ih = (h == 1)? IONE : new Integer(h); + readers_.put(currentId, ih); + return null; + } else { + readers_.remove(currentId); + + if (writeHolds_ > 0) { // a write lock is still held + return null; + } else if (activeReaders_ == 0 && waitingWriters_ > 0) { + return writerLock_; + } else { + return null; + } + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBFileValue.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBFileValue.java new file mode 100644 index 00000000000..eae54bad85b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBFileValue.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.value; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.RepositoryException; +import javax.jcr.Binary; + +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataStore; + +/** + * Represents binary data which is backed by a resource or byte[]. + * Unlike BinaryValue it has no state, i.e. + * the getStream() method always returns a fresh + * InputStream instance. + *

    + * Important Note: + *

    + * This interface is for Jackrabbit-internal use only. Applications should + * use javax.jcr.ValueFactory to create binary values. + */ +abstract class BLOBFileValue implements Binary { + + /** + * Deletes the persistent resource backing this BLOBFileValue. + * + * @param pruneEmptyParentDirs if true, empty parent directories + * will automatically be deleted + */ + abstract void delete(boolean pruneEmptyParentDirs); + + /** + * Returns a copy of this BLOB file value. The returned copy may also be + * this object. However an implementation must guarantee that the returned + * value has state that is independent from this value. Immutable values + * can savely return the same value (this object). + *

    + * Specifically, {@link #dispose()} on the returned value must not have an + * effect on this value! + * + * @return a value that can be used independently from this value. + * @throws RepositoryException if an error occur while copying this value. + */ + abstract BLOBFileValue copy() throws RepositoryException; + + public abstract boolean equals(Object obj); + + public abstract String toString(); + + public abstract int hashCode(); + + /** + * Get the data identifier if one is available. + * + * @return the data identifier or null + */ + DataIdentifier getDataIdentifier() { + return null; + } + + //-----------------------------------------------------< javax.jcr.Binary > + + public int read(byte[] b, long position) throws IOException, RepositoryException { + InputStream in = getStream(); + try { + long skip = position; + while (skip > 0) { + long skipped = in.skip(skip); + if (skipped <= 0) { + return -1; + } + skip -= skipped; + } + return in.read(b); + } finally { + in.close(); + } + } + + /** + * Check if this blob uses the given data store. + * + * @param s the other data store + * @return true if it does + */ + boolean usesDataStore(DataStore s) { + return false; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInDataStore.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInDataStore.java new file mode 100644 index 00000000000..e6730ce24d1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInDataStore.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.core.value; + +import org.apache.jackrabbit.api.ReferenceBinary; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataRecord; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; + +import javax.jcr.RepositoryException; + +/** + * Represents binary data which is stored in the data store. + */ +class BLOBInDataStore extends BLOBFileValue implements ReferenceBinary { + + private final DataStore store; + private final DataIdentifier identifier; + + /** + * the prefix of the string representation of this value + */ + private static final String PREFIX = "dataStore:"; + + /** + * The default logger + */ + private static Logger log = LoggerFactory.getLogger(BLOBInDataStore.class); + + /** the audit logger */ + private static Logger auditLogger = LoggerFactory.getLogger("org.apache.jackrabbit.core.audit"); + + private BLOBInDataStore(DataStore store, DataIdentifier identifier) { + assert store != null; + assert identifier != null; + this.store = store; + this.identifier = identifier; + } + + void delete(boolean pruneEmptyParentDirs) { + // do nothing + } + + public void dispose() { + // do nothing + } + + DataIdentifier getDataIdentifier() { + return identifier; + } + + BLOBFileValue copy() throws RepositoryException { + return this; + } + + public boolean equals(Object obj) { + if (!(obj instanceof BLOBInDataStore) || obj == null) { + return false; + } + BLOBInDataStore other = (BLOBInDataStore) obj; + return store == other.store && identifier.equals(other.identifier); + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + public long getSize() { + try { + return getDataRecord().getLength(); + } catch (DataStoreException e) { + log.warn("getSize for " + identifier + " failed", e); + return -1; + } + } + + public InputStream getStream() throws RepositoryException { + return getDataRecord().getStream(); + } + + @Override + public String getReference() { + try { + return getDataRecord().getReference(); + } catch (DataStoreException e) { + log.warn("Unable to access the reference to " + identifier, e); + return null; + } + } + + public String toString() { + return PREFIX + identifier; + } + + static BLOBInDataStore getInstance(DataStore store, String s) { + String id = s.substring(PREFIX.length()); + DataIdentifier identifier = new DataIdentifier(id); + return new BLOBInDataStore(store, identifier); + } + + static BLOBInDataStore getInstance(DataStore store, DataIdentifier identifier) { + return new BLOBInDataStore(store, identifier); + } + + static BLOBInDataStore getInstance(DataStore store, InputStream in) throws DataStoreException { + DataRecord rec = store.addRecord(in); + DataIdentifier identifier = rec.getIdentifier(); + if (auditLogger.isDebugEnabled()) { + auditLogger.debug("{} ({})", identifier, rec.getLength()); + } + return new BLOBInDataStore(store, identifier); + } + + /** + * Checks if String can be converted to an instance of this class. + * @param s + * @return true if it can be converted + */ + static boolean isInstance(String s) { + return s.startsWith(PREFIX); + } + + private DataRecord getDataRecord() throws DataStoreException { + // may not keep the record, otherwise garbage collection doesn't work + return store.getRecord(identifier); + } + + boolean usesDataStore(DataStore s) { + return store == s; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInMemory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInMemory.java new file mode 100644 index 00000000000..77afa162bf6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInMemory.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.value; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Arrays; + +/** + * Represents binary data which is backed by a byte[] (in memory). + */ +class BLOBInMemory extends BLOBFileValue { + + /** + * Logger instance for this class + */ + private static Logger log = LoggerFactory.getLogger(BLOBInMemory.class); + + /** + * the prefix of the string representation of this value + */ + private static final String PREFIX = "0x"; + + /** + * the data + */ + private byte[] data; + + /** + * empty array + */ + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * empty instance + */ + private static final BLOBInMemory EMPTY = new BLOBInMemory(EMPTY_BYTE_ARRAY); + + /** + * Creates a new instance from a + * byte[] array. + * + * @param data the byte array + */ + private BLOBInMemory(byte[] data) { + this.data = data; + } + + /** + * Creates a new instance from a + * byte[] array. + * + * @param data the byte array + */ + static BLOBInMemory getInstance(byte[] data) { + if (data.length == 0) { + return EMPTY; + } else { + return new BLOBInMemory(data); + } + } + + /** + * Checks if String can be converted to an instance of this class. + * @param s + * @return true if it can be converted + */ + static boolean isInstance(String s) { + return s.startsWith(PREFIX); + } + + /** + * Convert a String to an instance of this class. + * @param s + * @return the instance + */ + static BLOBInMemory getInstance(String s) throws IllegalArgumentException { + assert s.startsWith(PREFIX); + s = s.substring(PREFIX.length()); + int len = s.length(); + if (len % 2 != 0) { + String msg = "unable to deserialize byte array " + s + " , length=" + s.length(); + log.debug(msg); + throw new IllegalArgumentException(msg); + } + len /= 2; + byte[] data = new byte[len]; + try { + for (int i = 0; i < len; i++) { + data[i] = (byte) ((Character.digit(s.charAt(2 * i), 16) << 4) | (Character.digit(s.charAt(2 * i + 1), 16))); + } + } catch (NumberFormatException e) { + String msg = "unable to deserialize byte array " + s; + log.debug(msg); + throw new IllegalArgumentException(msg); + } + return BLOBInMemory.getInstance(data); + } + + void delete(boolean pruneEmptyParentDirs) { + // do nothing + // this object could still be referenced + // the data will be garbage collected + } + + public void dispose() { + // do nothing + // this object could still be referenced + // the data will be garbage collected + } + + BLOBFileValue copy() throws RepositoryException { + return this; + } + + public long getSize() { + return data.length; + } + + public InputStream getStream() { + return new ByteArrayInputStream(data); + } + + public String toString() { + StringBuilder buff = new StringBuilder(PREFIX.length() + 2 * data.length); + buff.append(PREFIX); + for (int i = 0; i < data.length; i++) { + int c = data[i] & 0xff; + buff.append(Integer.toHexString(c >> 4)); + buff.append(Integer.toHexString(c & 0xf)); + } + return buff.toString(); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BLOBInMemory) { + BLOBInMemory other = (BLOBInMemory) obj; + return Arrays.equals(data, other.data); + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInResource.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInResource.java new file mode 100644 index 00000000000..bdd167cd0e6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInResource.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.core.value; + +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.RepositoryException; + +/** + * Represents binary data which is stored in a file system resource. + */ +class BLOBInResource extends BLOBFileValue { + + /** + * The default logger + */ + private static final Logger LOG = LoggerFactory.getLogger(BLOBInResource.class); + + /** + * the prefix of the string representation of this value + */ + private static final String PREFIX = "fsResource:"; + + /** + * underlying file system resource + */ + private final FileSystemResource fsResource; + + /** + * the file length + */ + private final long length; + + /** + * Creates a new instance from a stream. + * + * @param fsResource the file system resource + * @throws IOException + */ + private BLOBInResource(FileSystemResource fsResource) throws IOException { + try { + if (!fsResource.exists()) { + throw new IOException(fsResource.getPath() + + ": the specified resource does not exist"); + } + length = fsResource.length(); + } catch (FileSystemException fse) { + IOException e2 = new IOException(fsResource.getPath() + + ": Error while creating value: " + fse.toString()); + e2.initCause(fse); + throw e2; + } + this.fsResource = fsResource; + } + + /** + * Creates a new instance from a file system resource. + * + * @param fsResource the resource + */ + static BLOBInResource getInstance(FileSystemResource fsResource) throws IOException { + return new BLOBInResource(fsResource); + } + + void delete(boolean pruneEmptyParentDirs) { + try { + fsResource.delete(pruneEmptyParentDirs); + } catch (FileSystemException fse) { + // ignore + LOG.warn("Error while deleting BLOBFileValue: " + fse.getMessage()); + } + + } + + public void dispose() { + // this instance is not backed by temporarily allocated resource/buffer + } + + BLOBFileValue copy() throws RepositoryException { + return BLOBInTempFile.getInstance(getStream(), true); + } + + public long getSize() { + return length; + } + + public InputStream getStream() throws RepositoryException { + try { + return fsResource.getInputStream(); + } catch (FileSystemException fse) { + throw new RepositoryException(fsResource.getPath() + + ": the specified resource does not exist", fse); + } + } + + public String toString() { + return PREFIX + fsResource.toString(); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BLOBInResource) { + BLOBInResource other = (BLOBInResource) obj; + return length == other.length && fsResource.equals(other.fsResource); + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInTempFile.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInTempFile.java new file mode 100644 index 00000000000..6ebf2293546 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BLOBInTempFile.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.core.value; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.data.LazyFileInputStream; +import org.apache.jackrabbit.util.TransientFileFactory; + +import java.io.File; +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 javax.jcr.RepositoryException; + +/** + * Represents binary data which is stored in a temporary file. + */ +class BLOBInTempFile extends BLOBFileValue { + + /** + * the prefix of the string representation of this value + */ + private static final String PREFIX = "file:"; + + private File file; + private long length; + private final boolean temp; + + /** + * Creates a new instance from a stream. + * The input stream is always closed by this method. + * + * @param in the input stream + * @param temp + * @throws RepositoryException + */ + private BLOBInTempFile(InputStream in, boolean temp) throws RepositoryException { + this.temp = temp; + OutputStream out = null; + try { + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + file = fileFactory.createTransientFile("bin", null, null); + out = new FileOutputStream(file); + length = IOUtils.copyLarge(in, out); + } catch (IOException e) { + throw new RepositoryException("Error creating temporary file", e); + } finally { + IOUtils.closeQuietly(in); + if (out != null) { + try { + out.close(); + } catch (IOException e) { + throw new RepositoryException("Error creating temporary file", e); + } + } + } + } + + /** + * Creates a new instance from file. + * + * @param file + * @param temp + */ + private BLOBInTempFile(File file, boolean temp) { + this.file = file; + this.length = file.length(); + this.temp = temp; + } + + /** + * Creates a new instance from a stream. + * + * @param in the stream + * @param temp + */ + static BLOBFileValue getInstance(InputStream in, boolean temp) throws RepositoryException { + if (temp) { + return new RefCountingBLOBFileValue(new BLOBInTempFile(in, temp)); + } else { + return new BLOBInTempFile(in, temp); + } + } + + /** + * Creates a new instance from a file. + * + * @param file the file + */ + static BLOBInTempFile getInstance(File file, boolean temp) { + return new BLOBInTempFile(file, temp); + } + + void delete(boolean pruneEmptyParentDirs) { + file.delete(); + length = -1; + file = null; + } + + public void dispose() { + if (temp) { + delete(true); + } + } + + BLOBFileValue copy() throws RepositoryException { + if (temp) { + return BLOBInTempFile.getInstance(getStream(), temp); + } else { + return BLOBInTempFile.getInstance(file, temp); + } + } + + public long getSize() { + return length; + } + + public InputStream getStream() throws IllegalStateException, RepositoryException { + try { + return new LazyFileInputStream(file); + } catch (FileNotFoundException fnfe) { + throw new RepositoryException("file backing binary value not found", fnfe); + } + } + + public String toString() { + return PREFIX + file.toString(); + } + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BLOBInTempFile) { + BLOBInTempFile other = (BLOBInTempFile) obj; + return (file == other.file) || (length == other.length && file != null && file.equals(other.file)); + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + public int read(byte[] b, long position) throws IOException, RepositoryException { + RandomAccessFile raf = new RandomAccessFile(file, "r"); + try { + raf.seek(position); + return raf.read(b); + } finally { + raf.close(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BinaryValueImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BinaryValueImpl.java new file mode 100644 index 00000000000..f68632d1dc2 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/BinaryValueImpl.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.core.value; + +import javax.jcr.RepositoryException; +import org.apache.jackrabbit.api.JackrabbitValue; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.value.BinaryValue; + +/** + * Represents a binary value that is backed by a blob file value. + */ +class BinaryValueImpl extends BinaryValue implements JackrabbitValue { + + private final BLOBFileValue blob; + + /** + * Construct a new object from the given blob. + * + * @param blob the blob + */ + BinaryValueImpl(BLOBFileValue blob) throws RepositoryException { + super(blob); + this.blob = blob; + } + + public String getContentIdentity() { + DataIdentifier id = blob.getDataIdentifier(); + return id == null ? null : id.toString(); + } + + /** + * Get the data identifier if one is available. + * + * @return the data identifier or null + */ + DataIdentifier getDataIdentifier() { + return blob.getDataIdentifier(); + } + + boolean usesDataStore(DataStore s) { + return blob.usesDataStore(s); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/InternalValue.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/InternalValue.java new file mode 100644 index 00000000000..bb4754ba9ec --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/InternalValue.java @@ -0,0 +1,736 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.value; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.URI; +import java.net.URISyntaxException; +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.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.AbstractQValue; +import org.apache.jackrabbit.spi.commons.value.AbstractQValueFactory; +import org.apache.jackrabbit.spi.commons.value.QValueValue; +import org.apache.jackrabbit.util.ISO8601; + +/** + * InternalValue represents the internal format of a property value. + *

    + * The following table specifies the internal format for every property type: + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    PropertyTypeInternal Format
    STRINGString
    LONGLong
    DOUBLEDouble
    DATECalendar
    BOOLEANBoolean
    NAMEName
    PATHPath
    URIURI
    DECIMALBigDecimal
    BINARYBLOBFileValue
    REFERENCE{@link NodeId}
    + */ +public class InternalValue extends AbstractQValue { + + private static final long serialVersionUID = -7340744360527434409L; + + public static final InternalValue[] EMPTY_ARRAY = new InternalValue[0]; + + private static final InternalValue BOOLEAN_TRUE = new InternalValue(true); + + private static final InternalValue BOOLEAN_FALSE = new InternalValue(false); + + /** + * Temporary binary values smaller or equal this size are kept in memory + */ + private static final int MIN_BLOB_FILE_SIZE = 1024; + + //------------------------------------------------------< factory methods > + /** + * Create a new internal value from the given JCR value. + * Large binary values are stored in a temporary file. + * + * @param value the JCR value + * @param resolver + * @return the created internal value + * @throws RepositoryException + * @throws ValueFormatException + */ + public static InternalValue create(Value value, NamePathResolver resolver) + throws ValueFormatException, RepositoryException { + return create(value, resolver, null); + } + + /** + * Create a new internal value from the given JCR value. + * If the data store is enabled, large binary values are stored in the data store. + * + * @param value the JCR value + * @param resolver + * @param store the data store + * @return the created internal value + * @throws RepositoryException + * @throws ValueFormatException + */ + public static InternalValue create(Value value, NamePathResolver resolver, DataStore store) + throws ValueFormatException, RepositoryException { + switch (value.getType()) { + case PropertyType.BINARY: + BLOBFileValue blob = null; + if (value instanceof BinaryValueImpl) { + BinaryValueImpl bin = (BinaryValueImpl) value; + DataIdentifier identifier = bin.getDataIdentifier(); + if (identifier != null) { + if (bin.usesDataStore(store)) { + // access the record to ensure it is not garbage collected + store.getRecord(identifier); + blob = BLOBInDataStore.getInstance(store, identifier); + } else { + if (store.getRecordIfStored(identifier) != null) { + // it exists - so we don't need to stream it again + // but we need to create a new object because the original + // one might be in a different data store (repository) + blob = BLOBInDataStore.getInstance(store, identifier); + } + } + } + } + if (blob == null) { + Binary b = value.getBinary(); + boolean dispose = false; + try { + if (b instanceof BLOBFileValue) { + // use as is + blob = (BLOBFileValue) b; + } else { + // create a copy from the stream + dispose = true; + blob = getBLOBFileValue(store, b.getStream(), true); + } + } finally { + if (dispose) { + b.dispose(); + } + } + } + return new InternalValue(blob); + case PropertyType.BOOLEAN: + return create(value.getBoolean()); + case PropertyType.DATE: + return create(value.getDate()); + case PropertyType.DOUBLE: + return create(value.getDouble()); + case PropertyType.DECIMAL: + return create(value.getDecimal()); + case PropertyType.LONG: + return create(value.getLong()); + case PropertyType.REFERENCE: + return create(new NodeId(value.getString())); + case PropertyType.WEAKREFERENCE: + return create(new NodeId(value.getString()), true); + case PropertyType.URI: + try { + return create(new URI(value.getString())); + } catch (URISyntaxException e) { + throw new ValueFormatException(e.getMessage()); + } + case PropertyType.NAME: + try { + if (value instanceof QValueValue) { + QValue qv = ((QValueValue) value).getQValue(); + if (qv instanceof InternalValue) { + return (InternalValue) qv; + } else { + return create(qv.getName()); + } + } else { + return create(resolver.getQName(value.getString())); + } + } catch (NameException e) { + throw new ValueFormatException(e.getMessage()); + } + case PropertyType.PATH: + try { + if (value instanceof QValueValue) { + QValue qv = ((QValueValue) value).getQValue(); + if (qv instanceof InternalValue) { + return (InternalValue) qv; + } else { + return create(qv.getPath()); + } + } else { + return create(resolver.getQPath(value.getString(), false)); + } + } catch (MalformedPathException mpe) { + throw new ValueFormatException(mpe.getMessage()); + } + case PropertyType.STRING: + return create(value.getString()); + default: + throw new IllegalArgumentException("illegal value"); + } + } + + public static InternalValue create(QValue value) + throws RepositoryException { + switch (value.getType()) { + case PropertyType.BINARY: + try { + return create(value.getString().getBytes(AbstractQValueFactory.DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException e) { + throw new InternalError(AbstractQValueFactory.DEFAULT_ENCODING + " not supported"); + } + case PropertyType.BOOLEAN: + return new InternalValue(value.getBoolean()); + case PropertyType.DATE: + return new InternalValue(value.getCalendar()); + case PropertyType.DOUBLE: + return new InternalValue(value.getDouble()); + case PropertyType.DECIMAL: + return new InternalValue(value.getDecimal()); + case PropertyType.LONG: + return new InternalValue(value.getLong()); + case PropertyType.REFERENCE: + return create(new NodeId(value.getString())); + case PropertyType.WEAKREFERENCE: + return create(new NodeId(value.getString()), true); + case PropertyType.URI: + return new InternalValue(value.getURI()); + case PropertyType.NAME: + return new InternalValue(value.getName()); + case PropertyType.PATH: + return new InternalValue(value.getPath()); + case PropertyType.STRING: + return new InternalValue(value.getString(), PropertyType.STRING); + default: + throw new IllegalArgumentException("illegal value"); + } + } + + public static InternalValue[] create(QValue[] values) + throws RepositoryException { + if (values == null) { + return null; + } + InternalValue[] tmp = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + tmp[i] = InternalValue.create(values[i]); + } + return tmp; + } + + /** + * Get the internal value for this blob. + * + * @param identifier the identifier + * @param store the data store + * @param verify verify if the record exists, and return null if not + * @return the internal value or null + */ + static InternalValue getInternalValue(DataIdentifier identifier, DataStore store, boolean verify) throws DataStoreException { + if (verify) { + if (store.getRecordIfStored(identifier) == null) { + return null; + } + } else { + // access the record to ensure it is not garbage collected + store.getRecord(identifier); + } + // it exists - so we don't need to stream it again + // but we need to create a new object because the original + // one might be in a different data store (repository) + BLOBFileValue blob = BLOBInDataStore.getInstance(store, identifier); + return new InternalValue(blob); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(String value) { + return new InternalValue(value, PropertyType.STRING); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(long value) { + return new InternalValue(value); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(double value) { + return new InternalValue(value); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(Calendar value) { + return new InternalValue(value); + } + + /** + * https://issues.apache.org/jira/browse/JCR-3083 + * + * @param value + * @return the created value + */ + public static InternalValue createDate(String value) { + return new InternalValue(value, PropertyType.DATE); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(BigDecimal value) { + return new InternalValue(value); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(URI value) { + return new InternalValue(value); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(boolean value) { + return value ? BOOLEAN_TRUE : BOOLEAN_FALSE; + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(byte[] value) { + return new InternalValue(BLOBInMemory.getInstance(value)); + } + + /** + * Create an internal value that is backed by a temporary file. + * + * @param value the stream + * @return the internal value + * @throws RepositoryException + */ + public static InternalValue createTemporary(InputStream value) throws RepositoryException { + return new InternalValue(getBLOBFileValue(null, value, true)); + } + + /** + * Create an internal value that is stored in the data store (if enabled). + * + * @param value the input stream + * @param store + * @return the internal value + * @throws RepositoryException + */ + public static InternalValue create(InputStream value, DataStore store) throws RepositoryException { + return new InternalValue(getBLOBFileValue(store, value, false)); + } + + /** + * @param value + * @return + * @throws RepositoryException + */ + public static InternalValue create(InputStream value) throws RepositoryException { + return create(value, null); + } + + /** + * @param value + * @return + * @throws IOException + */ + public static InternalValue create(FileSystemResource value) throws IOException { + return new InternalValue(BLOBInResource.getInstance(value)); + } + + /** + * Create a binary object with the given identifier. + * + * @param store the data store + * @param id the identifier + * @return the value + */ + public static InternalValue create(DataStore store, String id) { + return new InternalValue(getBLOBFileValue(store, id)); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(Name value) { + return new InternalValue(value); + } + + /** + * @param values + * @return the created value + */ + public static InternalValue[] create(Name[] values) { + InternalValue[] ret = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + ret[i] = new InternalValue(values[i]); + } + return ret; + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(Path value) { + return new InternalValue(value); + } + + /** + * @param value + * @return the created value + */ + public static InternalValue create(NodeId value) { + return create(value, false); + } + + /** + * @param value + * @param weak + * @return the created value + */ + public static InternalValue create(NodeId value, boolean weak) { + return new InternalValue(value, weak); + } + + //----------------------------------------------------< conversions, etc. > + + BLOBFileValue getBLOBFileValue() { + assert val != null && type == PropertyType.BINARY; + return (BLOBFileValue) val; + } + + public NodeId getNodeId() { + assert val != null && (type == PropertyType.REFERENCE || type == PropertyType.WEAKREFERENCE); + return (NodeId) val; + } + + public Calendar getDate() throws RepositoryException { + assert val != null && type == PropertyType.DATE; + return getCalendar(); + } + + /** + * Create a copy of this object. Immutable values will return itself, + * while mutable values will return a copy. + * + * @return itself or a copy + * @throws RepositoryException + */ + public InternalValue createCopy() throws RepositoryException { + if (type != PropertyType.BINARY) { + // for all types except BINARY it's safe to return 'this' because the + // wrapped value is immutable (and therefore this instance as well) + return this; + } + // return a copy of the wrapped BLOBFileValue + return new InternalValue(((BLOBFileValue) val).copy()); + } + + /** + * Parses the given string as an InternalValue of the + * specified type. The string must be in the format returned by the + * InternalValue.toString() method. + * + * @param s a String containing the InternalValue + * representation to be parsed. + * @param type + * @return the InternalValue represented by the arguments + * @throws IllegalArgumentException if the specified string can not be parsed + * as an InternalValue of the + * specified type. + * @see #toString() + */ + public static InternalValue valueOf(String s, int type) { + switch (type) { + case PropertyType.BOOLEAN: + return create(Boolean.valueOf(s)); + case PropertyType.DATE: + return create(ISO8601.parse(s)); + case PropertyType.DOUBLE: + return create(Double.parseDouble(s)); + case PropertyType.LONG: + return create(Long.parseLong(s)); + case PropertyType.DECIMAL: + return create(new BigDecimal(s)); + case PropertyType.REFERENCE: + return create(new NodeId(s)); + case PropertyType.WEAKREFERENCE: + return create(new NodeId(s), true); + case PropertyType.PATH: + return create(PathFactoryImpl.getInstance().create(s)); + case PropertyType.NAME: + return create(NameFactoryImpl.getInstance().create(s)); + case PropertyType.URI: + return create(URI.create(s)); + case PropertyType.STRING: + return create(s); + + case PropertyType.BINARY: + throw new IllegalArgumentException( + "this method does not support the type PropertyType.BINARY"); + default: + throw new IllegalArgumentException("illegal type: " + type); + } + } + + //-------------------------------------------------------< implementation > + private InternalValue(String value, int type) { + super(value, type); + } + + private InternalValue(Name value) { + super(value); + } + + private InternalValue(long value) { + super(value); + } + + private InternalValue(double value) { + super(value); + } + + private InternalValue(Calendar value) { + super(value); + } + + private InternalValue(boolean value) { + super(value); + } + + private InternalValue(URI value) { + super(value); + } + + private InternalValue(BigDecimal value) { + super(value); + } + + private InternalValue(BLOBFileValue value) { + super(value, PropertyType.BINARY); + } + + private InternalValue(Path value) { + super(value); + } + + private InternalValue(NodeId value, boolean weak) { + super(value, weak ? PropertyType.WEAKREFERENCE : PropertyType.REFERENCE); + } + + /** + * Create a BLOB value from in input stream. Small objects will create an in-memory object, + * while large objects are stored in the data store or in a temp file (if the store parameter is not set). + * + * @param store the data store (optional) + * @param in the input stream + * @param temporary if the file should be deleted when discard is called (ignored if a data store is used) + * @return the value + * @throws RepositoryException + */ + private static BLOBFileValue getBLOBFileValue(DataStore store, InputStream in, boolean temporary) throws RepositoryException { + int maxMemorySize; + if (store != null) { + maxMemorySize = store.getMinRecordLength() - 1; + } else { + maxMemorySize = MIN_BLOB_FILE_SIZE; + } + maxMemorySize = Math.max(0, maxMemorySize); + byte[] buffer = new byte[maxMemorySize]; + int pos = 0, len = maxMemorySize; + try { + while (pos < maxMemorySize) { + int l = in.read(buffer, pos, len); + if (l < 0) { + break; + } + pos += l; + len -= l; + } + } catch (IOException e) { + throw new RepositoryException("Could not read from stream", e); + } + if (pos < maxMemorySize) { + // shrink the buffer + byte[] data = new byte[pos]; + System.arraycopy(buffer, 0, data, 0, pos); + return BLOBInMemory.getInstance(data); + } else { + // a few bytes are already read, need to re-build the input stream + in = new SequenceInputStream(new ByteArrayInputStream(buffer, 0, pos), in); + if (store != null) { + return BLOBInDataStore.getInstance(store, in); + } else { + return BLOBInTempFile.getInstance(in, temporary); + } + } + } + + private static BLOBFileValue getBLOBFileValue(DataStore store, String id) { + if (BLOBInMemory.isInstance(id)) { + return BLOBInMemory.getInstance(id); + } else if (BLOBInDataStore.isInstance(id)) { + return BLOBInDataStore.getInstance(store, id); + } else { + throw new IllegalArgumentException("illegal binary id: " + id); + } + } + + public boolean isInDataStore() { + return val instanceof BLOBInDataStore; + } + + //-------------------------------------------------------------< QValue >--- + /** + * @see org.apache.jackrabbit.spi.QValue#getLength() + */ + @Override + public long getLength() throws RepositoryException { + if (PropertyType.BINARY == type) { + return ((Binary) val).getSize(); + } else { + return super.getLength(); + } + } + + /** + * @see org.apache.jackrabbit.spi.QValue#getStream() + */ + public InputStream getStream() throws RepositoryException { + if (type == PropertyType.BINARY) { + return ((Binary) val).getStream(); + } else { + try { + // convert via string + return new ByteArrayInputStream(getString().getBytes(InternalValueFactory.DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(InternalValueFactory.DEFAULT_ENCODING + " is not supported encoding on this platform", e); + } + } + } + + /** + * @see org.apache.jackrabbit.spi.QValue#getBinary() + */ + @Override + public Binary getBinary() throws RepositoryException { + if (type == PropertyType.BINARY) { + // return an independent copy that can be disposed without + // affecting this value + return ((BLOBFileValue) val).copy(); + } else { + try { + // convert via string + byte[] data = getString().getBytes(InternalValueFactory.DEFAULT_ENCODING); + return BLOBInMemory.getInstance(data); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(InternalValueFactory.DEFAULT_ENCODING + " is not supported encoding on this platform", e); + } + } + } + + /** + * @see org.apache.jackrabbit.spi.QValue#discard() + */ + @Override + public void discard() { + if (type == PropertyType.BINARY) { + BLOBFileValue bfv = (BLOBFileValue) val; + bfv.dispose(); + } else { + super.discard(); + } + } + + /** + * Delete persistent binary objects. This method does not delete objects in + * the data store. + */ + public void deleteBinaryResource() { + if (type == PropertyType.BINARY) { + BLOBFileValue bfv = (BLOBFileValue) val; + bfv.delete(true); + } + } + + public boolean equals(Object object) { + if (object instanceof InternalValue) { + InternalValue that = (InternalValue) object; + if (type == PropertyType.DATE) { + try { + return that.type == PropertyType.DATE + && getDate().getTimeInMillis() == that.getDate() + .getTimeInMillis(); + } catch (RepositoryException e) { + return false; + } + } else { + return type == that.type && val.equals(that.val); + } + } else { + return false; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/InternalValueFactory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/InternalValueFactory.java new file mode 100644 index 00000000000..6c5ae6b36d6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/InternalValueFactory.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.core.value; + +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.value.AbstractQValueFactory; + +import javax.jcr.RepositoryException; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.FileInputStream; +import java.util.Calendar; +import java.net.URI; +import java.math.BigDecimal; + +/** + * InternalValueFactory implements a {@link QValueFactory} that + * creates {@link InternalValue} instances for binary values. + */ +public final class InternalValueFactory extends AbstractQValueFactory { + + private static final QValueFactory INSTANCE = new InternalValueFactory(null); + + private final DataStore store; + + InternalValueFactory(DataStore store) { + this.store = store; + } + + public static QValueFactory getInstance() { + return INSTANCE; + } + + @Override + public QValue create(Calendar value) throws RepositoryException { + return InternalValue.create(value); + } + + @Override + public QValue create(double value) throws RepositoryException { + return InternalValue.create(value); + } + + @Override + public QValue create(long value) throws RepositoryException { + return InternalValue.create(value); + } + + @Override + public QValue create(boolean value) throws RepositoryException { + return InternalValue.create(value); + } + + @Override + public QValue create(Name value) throws RepositoryException { + return InternalValue.create(value); + } + + @Override + public QValue create(Path value) throws RepositoryException { + return InternalValue.create(value); + } + + @Override + public QValue create(URI value) throws RepositoryException { + return InternalValue.create(value); + } + + @Override + public QValue create(BigDecimal value) throws RepositoryException { + return InternalValue.create(value); + } + + public QValue create(byte[] value) throws RepositoryException { + if (store == null) { + return InternalValue.create(value); + } else { + return InternalValue.create(new ByteArrayInputStream(value), store); + } + } + + public QValue create(InputStream value) throws RepositoryException, IOException { + if (store == null) { + return InternalValue.createTemporary(value); + } else { + return InternalValue.create(value, store); + } + } + + public QValue create(File value) throws RepositoryException, IOException { + InputStream in = new FileInputStream(value); + if (store == null) { + return InternalValue.createTemporary(in); + } else { + return InternalValue.create(in, store); + } + } + + @Override + protected QValue createReference(String ref, boolean weak) { + return InternalValue.create(new NodeId(ref), weak); + } + + @Override + protected QValue createString(String value) { + return InternalValue.create(value); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/RefCountingBLOBFileValue.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/RefCountingBLOBFileValue.java new file mode 100644 index 00000000000..dce5a6eee84 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/RefCountingBLOBFileValue.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.core.value; + +import java.io.InputStream; + +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RefCountingBLOBFileValue implements a reference counting BLOB + * file value on top of an existing {@link BLOBFileValue}. Whenever a + * {@link #copy()} is created from this BLOB file value, a new light weight + * {@link RefCountBinary} is created and the reference count {@link #refCount} + * is incremented. The underlying value is discarded once this + * {@link RefCountingBLOBFileValue} and all its light weight + * {@link RefCountBinary} instances have been discarded. + */ +public class RefCountingBLOBFileValue extends BLOBFileValue { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(RefCountingBLOBFileValue.class); + + /** + * The actual value. + */ + private final BLOBFileValue value; + + /** + * The current ref count. Initially set to one. + */ + private int refCount = 1; + + /** + * Whether this instance has been discarded and cannot be used anymore. + */ + private boolean disposed = false; + + /** + * Creates a new reference counting blob file value based on the given + * value. + * + * @param value the underlying value. + */ + public RefCountingBLOBFileValue(BLOBFileValue value) { + this.value = value; + } + + //----------------------------< BLOBFileValue >----------------------------- + + /** + * Discards the underlying value if the reference count drops to zero. + */ + public synchronized void dispose() { + if (refCount > 0) { + if (--refCount == 0) { + log.debug("{}@refCount={}, discarding value...", + System.identityHashCode(this), refCount); + value.dispose(); + disposed = true; + } else { + log.debug("{}@refCount={}", + System.identityHashCode(this), refCount); + } + } + } + + /** + * Forwards the call to the underlying value. + * + * @param pruneEmptyParentDirs if true, empty parent + * directories will automatically be deleted + */ + @Override + void delete(boolean pruneEmptyParentDirs) { + value.delete(pruneEmptyParentDirs); + } + + /** + * Returns a light weight copy of this BLOB file value. + * + * @return a copy of this value. + * @throws RepositoryException if an error occurs while creating the copy or + * if this value has been disposed already. + */ + @Override + synchronized BLOBFileValue copy() throws RepositoryException { + if (refCount <= 0) { + throw new RepositoryException("this BLOBFileValue has been disposed"); + } + BLOBFileValue bin = new RefCountBinary(); + refCount++; + log.debug("{}@refCount={}", System.identityHashCode(this), refCount); + return bin; + } + + public boolean equals(Object obj) { + if (obj instanceof RefCountingBLOBFileValue) { + RefCountingBLOBFileValue val = (RefCountingBLOBFileValue) obj; + return value.equals(val.value); + } + return false; + } + + public String toString() { + return value.toString(); + } + + public int hashCode() { + return 0; + } + + //-----------------------------------------------------< javax.jcr.Binary > + + public long getSize() throws RepositoryException { + return value.getSize(); + } + + public InputStream getStream() throws RepositoryException { + return value.getStream(); + } + + @Override + protected void finalize() throws Throwable { + if (!disposed) { + dispose(); + } + super.finalize(); + } + + //------------------------< RefCountBinary >-------------------------------- + + private final class RefCountBinary extends BLOBFileValue { + + private boolean disposed; + + public InputStream getStream() throws RepositoryException { + checkDisposed(); + return getInternalValue().getStream(); + } + + public long getSize() throws RepositoryException { + checkDisposed(); + return getInternalValue().getSize(); + } + + public void dispose() { + if (!disposed) { + disposed = true; + getInternalValue().dispose(); + } + } + + @Override + void delete(boolean pruneEmptyParentDirs) { + getInternalValue().delete(pruneEmptyParentDirs); + } + + @Override + BLOBFileValue copy() throws RepositoryException { + checkDisposed(); + return getInternalValue().copy(); + } + + public boolean equals(Object obj) { + if (obj instanceof RefCountBinary) { + RefCountBinary other = (RefCountBinary) obj; + return getInternalValue().equals(other.getInternalValue()); + } + return false; + } + + public String toString() { + return getInternalValue().toString(); + } + + public int hashCode() { + return 0; + } + + @Override + protected void finalize() throws Throwable { + dispose(); + super.finalize(); + } + + private BLOBFileValue getInternalValue() { + return RefCountingBLOBFileValue.this; + } + + private void checkDisposed() throws RepositoryException { + if (disposed) { + throw new RepositoryException("this BLOBFileValue is disposed"); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/ValueFactoryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/ValueFactoryImpl.java new file mode 100644 index 00000000000..460956b5050 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/value/ValueFactoryImpl.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.core.value; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.api.ReferenceBinary; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataRecord; +import org.apache.jackrabbit.core.data.DataStore; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.value.BinaryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Binary; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.ValueFormatException; +import javax.jcr.RepositoryException; +import java.io.InputStream; +import java.io.IOException; + +/** + * ValueFactoryImpl... + */ +public class ValueFactoryImpl extends ValueFactoryQImpl { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ValueFactoryImpl.class); + + private final DataStore store; + + /** + * Constructs a new ValueFactoryQImpl based + * on an existing SPI QValueFactory and a + * NamePathResolver. + * + * @param resolver wrapped NamePathResolver + */ + public ValueFactoryImpl(NamePathResolver resolver, DataStore store) { + super(new InternalValueFactory(store), resolver); + this.store = store; + } + + @Override + public Value createValue(QValue qvalue) { + if (qvalue instanceof InternalValue && PropertyType.BINARY == qvalue.getType()) { + try { + return new BinaryValueImpl(((InternalValue) qvalue).getBLOBFileValue().copy()); + } catch (RepositoryException e) { + // should not get here + log.error(e.getMessage(), e); + } + } + return super.createValue(qvalue); + } + + @Override + public Binary createBinary(InputStream stream) throws RepositoryException { + try { + QValue value = getQValueFactory().create(stream); + if (value instanceof InternalValue) { + return ((InternalValue) value).getBLOBFileValue(); + } else { + return new BinaryImpl(stream); + } + } catch (IOException e) { + throw new RepositoryException(e); + } finally { + // JCR-2903 + IOUtils.closeQuietly(stream); + } + } + + @Override + public Value createValue(Binary binary) { + try { + if (binary instanceof BLOBInDataStore) { + BLOBInDataStore blob = (BLOBInDataStore) binary; + DataIdentifier identifier = blob.getDataIdentifier(); + InternalValue value; + if (blob.usesDataStore(store)) { + value = InternalValue.getInternalValue(identifier, store, false); + } else { + value = InternalValue.getInternalValue(identifier, store, true); + } + if (value != null) { + // if the value is already in this data store + return new BinaryValueImpl(value.getBLOBFileValue()); + } + } else if (binary instanceof BLOBFileValue) { + return new BinaryValueImpl(((BLOBFileValue) binary).copy()); + } else if (binary instanceof ReferenceBinary) { + String reference = ((ReferenceBinary) binary).getReference(); + DataRecord record = store.getRecordFromReference(reference); + if (record != null) { + return new BinaryValueImpl(BLOBInDataStore.getInstance( + store, record.getIdentifier())); + } + } + return createValue(binary.getStream()); + } catch (RepositoryException e) { + log.error(e.getMessage(), e); + // ignore - the super method may be smarter + } + return super.createValue(binary); + } + + @Override + public Value createValue(InputStream value) { + try { + InternalValue qvalue = (InternalValue) getQValueFactory().create(value); + return new BinaryValueImpl(qvalue.getBLOBFileValue()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } finally { + // JCR-2903 + IOUtils.closeQuietly(value); + } + } + + @Override + public Value createValue(String value, int type) throws ValueFormatException { + if (PropertyType.BINARY == type) { + try { + InternalValue qvalue = (InternalValue) getQValueFactory().create(value, type); + return new BinaryValueImpl(qvalue.getBLOBFileValue()); + } catch (RepositoryException e) { + throw new ValueFormatException(e); + } + } else { + return super.createValue(value, type); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/DateVersionSelector.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/DateVersionSelector.java new file mode 100644 index 00000000000..ea8af192858 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/DateVersionSelector.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.core.version; + +import java.util.Calendar; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; + +/** + * This Class implements a version selector that selects a version by creation + * date. The selected version is the latest that is older or equal than the + * given date. If no version could be found null is returned + * unless the returnLatest flag is set to true, where + * the latest version is returned. + *

    + * V1.0 - 02-Sep-2006
    + * V1.1 - 03-Sep-2006
    + * V1.2 - 05-Sep-2006
    + *
    + * new DateVersionSelector("03-Sep-2006").select() -> V1.1
    + * new DateVersionSelector("04-Sep-2006").select() -> V1.1
    + * new DateVersionSelector("01-Sep-2006").select() -> null
    + * new DateVersionSelector("01-Sep-2006", true).select() -> V1.2
    + * new DateVersionSelector(null, true).select() -> V1.2
    + * 
    + */ +public class DateVersionSelector implements VersionSelector { + + /** + * a version date hint + */ + private Calendar date = null; + + /** + * flag indicating that it should return the latest version, if no other + * found + */ + private boolean returnLatest = false; + + /** + * Creates a DateVersionSelector that will select the latest + * version of all those that are older than the given date. + * + * @param date reference date + */ + public DateVersionSelector(Calendar date) { + this.date = date; + } + + /** + * Creates a DateVersionSelector that will select the latest + * version of all those that are older than the given date. + * + * @param date reference date + * @param returnLatest if true latest is selected + */ + public DateVersionSelector(Calendar date, boolean returnLatest) { + this.date = date; + this.returnLatest = returnLatest; + } + + /** + * Returns the date hint + * + * @return the date hint. + */ + public Calendar getDate() { + return date; + } + + /** + * Sets the date hint + * + * @param date reference date + */ + public void setDate(Calendar date) { + this.date = date; + } + + /** + * Returns the flag, if the latest version should be selected, if no + * version can be found using the given hint. + * + * @return true if it returns latest. + */ + public boolean isReturnLatest() { + return returnLatest; + } + + /** + * Sets the flag, if the latest version should be selected, if no + * version can be found using the given hint. + * + * @param returnLatest the returnLatest flag + */ + public void setReturnLatest(boolean returnLatest) { + this.returnLatest = returnLatest; + } + + /** + * {@inheritDoc} + * + * Selects a version from the given version history using the previously + * assigned hint in the following order: name, label, date, latest. + */ + public InternalVersion select(InternalVersionHistory versionHistory) throws RepositoryException { + InternalVersion selected = null; + if (date != null) { + selected = DateVersionSelector.selectByDate(versionHistory, date); + } + if (selected == null && returnLatest) { + selected = DateVersionSelector.selectByDate(versionHistory, null); + } + return selected; + } + + /** + * Selects a version by date. + * + * @param history history to select from + * @param date reference date + * @return the latest version that is older than the given date date or + * null + * @throws RepositoryException if an error occurs + */ + public static InternalVersion selectByDate(InternalVersionHistory history, Calendar date) + throws RepositoryException { + long time = (date != null) ? date.getTimeInMillis() : Long.MAX_VALUE; + long latestDate = Long.MIN_VALUE; + InternalVersion latestVersion = null; + for (Name name: history.getVersionNames()) { + InternalVersion v = history.getVersion(name); + if (v.isRootVersion()) { + // ignore root version + continue; + } + long c = v.getCreated().getTimeInMillis(); + if (c > latestDate && c <= time) { + latestDate = c; + latestVersion = v; + } + } + return latestVersion; + } + + /** + * returns debug information + * @return debug information + */ + public String toString() { + StringBuilder ret = new StringBuilder(); + ret.append("DateVersionSelector("); + ret.append("date="); + ret.append(date); + ret.append(", returnLatest="); + ret.append(returnLatest); + ret.append(")"); + return ret.toString(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InconsistentVersioningState.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InconsistentVersioningState.java new file mode 100644 index 00000000000..74822d5d98b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InconsistentVersioningState.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.core.version; + +import org.apache.jackrabbit.core.id.NodeId; + +/** + * The InconsistentVersionControlState is used to signal + * inconsistencies in the versioning related state of a node, such + * as missing mandatory properties, missing version nodes, etc. + */ +public class InconsistentVersioningState extends RuntimeException { + + private final NodeId versionHistoryNodeId; + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public InconsistentVersioningState(String message) { + super(message); + this.versionHistoryNodeId = null; + } + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root cause (or otherwise null) + * @param versionHistoryNodeId NodeId of the version history that has problems (or otherwise null + */ + public InconsistentVersioningState(String message, NodeId versionHistoryNodeId, Throwable rootCause) { + super(message, rootCause); + this.versionHistoryNodeId = versionHistoryNodeId; + } + + /** + * @return the NodeId of the version history having problems or null + * when unknown. + */ + public NodeId getVersionHistoryNodeId() { + return this.versionHistoryNodeId; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalActivity.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalActivity.java new file mode 100644 index 00000000000..b2cffafc1be --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalActivity.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.core.version; + +import javax.jcr.RepositoryException; + +/** + * This interface defines the internal activity. + */ +public interface InternalActivity extends InternalVersionItem { + + /** + * Returns the latest version of the given history that is referenced in this activity. + * @param history the history + * @return the version + * @throws RepositoryException if an error occurs + */ + InternalVersion getLatestVersion(InternalVersionHistory history) + throws RepositoryException; + + /** + * Returns the changeset of this activity. + * This is the set of versions that are the latest members of this activity + * in their respective version histories. + * + * @return the changeset + * @throws RepositoryException if an error occurs + */ + VersionSet getChangeSet() throws RepositoryException; + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalActivityImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalActivityImpl.java new file mode 100644 index 00000000000..0febc046936 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalActivityImpl.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.core.version; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * Implements a internal representation of an activity node. + * this is only for the {@link InternalXAVersionManager}. + */ +class InternalActivityImpl extends InternalVersionItemImpl implements InternalActivity { + + /** + * Creates a new VersionHistory object for the given node state. + * @param vMgr version manager + * @param node version history node state + * @throws RepositoryException if an error occurs + */ + public InternalActivityImpl(InternalVersionManagerBase vMgr, NodeStateEx node) + throws RepositoryException { + super(vMgr, node); + } + + /** + * {@inheritDoc} + */ + @Override + public NodeId getId() { + return node.getNodeId(); + } + + /** + * {@inheritDoc} + */ + @Override + public InternalVersionItem getParent() { + return null; + } + + /** + * Creates a new activity history below the given parent node and with + * the given name. + * + * @param parent parent node + * @param name activity name + * @param activityId node id for the new activity + * @param title title of the activity + * @return new node state + * @throws RepositoryException if an error occurs + */ + static NodeStateEx create(NodeStateEx parent, Name name, NodeId activityId, + String title) + throws RepositoryException { + + // create new activity node in the persistent state + NodeStateEx pNode = parent.addNode(name, NameConstants.NT_ACTIVITY, activityId, true); + Set mix = new HashSet(); + mix.add(NameConstants.REP_VERSION_REFERENCE); + pNode.setMixins(mix); + + // set the title + pNode.setPropertyValue(NameConstants.JCR_ACTIVITY_TITLE, InternalValue.create(title)); + + parent.store(); + + return pNode; + } + + /** + * Adds a version reference + * @param v the version + * @throws RepositoryException if an error occurs + */ + public void addVersion(InternalVersionImpl v) throws RepositoryException { + InternalValue[] versions; + if (node.hasProperty(NameConstants.REP_VERSIONS)) { + InternalValue[] vs = node.getPropertyValues(NameConstants.REP_VERSIONS); + versions = new InternalValue[vs.length+1]; + System.arraycopy(vs, 0, versions, 0, vs.length); + versions[vs.length] = InternalValue.create(v.getId()); + } else { + versions = new InternalValue[]{InternalValue.create(v.getId())}; + } + node.setPropertyValues(NameConstants.REP_VERSIONS, PropertyType.REFERENCE, versions); + node.store(); + } + + /** + * Removes the given version from the list of references + * @param v the version + * @throws RepositoryException if an error occurs + */ + public void removeVersion(InternalVersionImpl v) throws RepositoryException { + List versions = new LinkedList(); + if (node.hasProperty(NameConstants.REP_VERSIONS)) { + NodeId vId = v.getId(); + for (InternalValue ref: node.getPropertyValues(NameConstants.REP_VERSIONS)) { + if (!vId.equals(ref.getNodeId())) { + versions.add(ref); + } + } + } + node.setPropertyValues(NameConstants.REP_VERSIONS, PropertyType.REFERENCE, versions.toArray(new InternalValue[versions.size()])); + node.store(); + + } + + /** + * Returns the latest version of the given history that is referenced in this activity. + * @param history the history + * @return the version + * @throws RepositoryException if an error occurs + */ + public InternalVersion getLatestVersion(InternalVersionHistory history) + throws RepositoryException { + if (node.hasProperty(NameConstants.REP_VERSIONS)) { + InternalVersion best = null; + for (InternalValue ref: node.getPropertyValues(NameConstants.REP_VERSIONS)) { + InternalVersion v = history.getVersion(ref.getNodeId()); + if (v != null) { + // currently we assume that the last version is the best + best = v; + } + } + return best; + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + public VersionSet getChangeSet() throws RepositoryException { + Map changeset = new HashMap(); + if (node.hasProperty(NameConstants.REP_VERSIONS)) { + for (InternalValue ref: node.getPropertyValues(NameConstants.REP_VERSIONS)) { + InternalVersion v = vMgr.getVersion(ref.getNodeId()); + changeset.put(v.getVersionHistory().getId(), v); + } + } + return new VersionSet(changeset); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalBaseline.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalBaseline.java new file mode 100644 index 00000000000..ab1b054c4c6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalBaseline.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.core.version; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; + +/** + * This interface defines the internal baseline. + *

    + * A baseline is the state of a configuration at some point in time, recorded in + * version storage. A baseline is similar to a normal version except that + * instead of representing the state of a single node, it represents the state + * of an entire partial subgraph. + *

    + * The internal baseline is the version of the internal configuration. + */ +public interface InternalBaseline extends InternalVersion { + + /** + * Returns the recorded base versions of all versionable nodes in the + * configuration. + * + * @return a map of base versions. the map key is the nodeid of the + * version history. + * @throws RepositoryException if an error occurs + */ + VersionSet getBaseVersions() throws RepositoryException; + + /** + * Returns the id of the nt:configuration node. this is basically the + * versionable id of the history. + * + * @return the configuration node id + */ + NodeId getConfigurationId(); + + /** + * Returns the id of the root node of a workspace configuration. this is + * basically the jcr:root property of the frozen configuration. + * + * @return the configuration root node id + */ + NodeId getConfigurationRootId(); +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalBaselineImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalBaselineImpl.java new file mode 100644 index 00000000000..4b8adb730cd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalBaselineImpl.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.core.version; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * Implements a internal representation of a baseline node. + */ +class InternalBaselineImpl extends InternalVersionImpl + implements InternalBaseline { + + /** + * Creates a new internal baseline with the given version history and + * persistence node. please note, that versions must be created by the + * version history. + * + * @param vh containing version history + * @param node node state of this version + * @param name name of this version + */ + InternalBaselineImpl(InternalVersionHistoryImpl vh, NodeStateEx node, Name name) throws RepositoryException { + super(vh, node, name); + } + + /** + * {@inheritDoc} + */ + public VersionSet getBaseVersions() throws RepositoryException { + InternalValue[] vs = ((InternalFrozenNodeImpl) getFrozenNode()).node. + getPropertyValues(NameConstants.REP_VERSIONS); + Map versions = new HashMap(); + if (vs != null) { + for (InternalValue v: vs) { + InternalVersion iv = vMgr.getVersion(v.getNodeId()); + if (iv != null) { + versions.put(iv.getVersionHistory().getId(), iv); + } + } + } + return new VersionSet(versions); + } + + /** + * {@inheritDoc} + */ + public NodeId getConfigurationId() { + return getVersionHistory().getVersionableId(); + } + + /** + * {@inheritDoc} + */ + public NodeId getConfigurationRootId() { + return ((InternalFrozenNodeImpl) getFrozenNode()).node.getPropertyValue + (NameConstants.JCR_ROOT).getNodeId(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFreeze.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFreeze.java new file mode 100644 index 00000000000..7e59aabee29 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFreeze.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.core.version; + +import org.apache.jackrabbit.spi.Name; + +/** + * the base interface for nodes that were versioned and turned either into + * InternalFrozenNode or InternalFrozenVersionHistory. + */ +public interface InternalFreeze extends InternalVersionItem { + + /** + * returns the name of the node. + * + * @return the name of the node. + */ + Name getName(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFreezeImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFreezeImpl.java new file mode 100644 index 00000000000..e51f55cfd86 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFreezeImpl.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.core.version; + +/** + * Implements a InternalFreeze + */ +abstract class InternalFreezeImpl extends InternalVersionItemImpl + implements InternalFreeze { + + /** + * The parent item + */ + private final InternalVersionItem parent; + + /** + * Creates a new InternalFreezeImpl + * + * @param vMgr + * @param parent + */ + protected InternalFreezeImpl(InternalVersionManagerBase vMgr, NodeStateEx node, InternalVersionItem parent) { + super(vMgr, node); + this.parent = parent; + } + + /** + * {@inheritDoc} + */ + @Override + public InternalVersionItem getParent() { + return parent; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenNode.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenNode.java new file mode 100644 index 00000000000..0ea0dc598c5 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenNode.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.core.version; + +import java.util.Set; +import java.util.List; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.spi.Name; + +import javax.jcr.version.VersionException; +import javax.jcr.RepositoryException; + +/** + * The InternalFrozenNode interface represents the frozen node that was generated + * during a {@link javax.jcr.Node#checkin()}. It holds the set of frozen + * properties, the frozen child nodes and the frozen version history + * references of the original node. + */ +public interface InternalFrozenNode extends InternalFreeze { + + /** + * Returns the list of frozen child nodes + * + * @return an array of internal freezes + * @throws VersionException if the freezes cannot be retrieved + */ + List getFrozenChildNodes() throws VersionException; + + /** + * Returns the list of frozen properties. + * + * @return an array of property states + */ + PropertyState[] getFrozenProperties(); + + /** + * Returns the frozen node id. + * + * @return the frozen id + */ + NodeId getFrozenId(); + + /** + * Returns the name of frozen primary type. + * + * @return the name of the frozen primary type. + */ + Name getFrozenPrimaryType(); + + /** + * Returns the list of names of the frozen mixin types. + * + * @return the list of names of the frozen mixin types. + */ + Set getFrozenMixinTypes(); + + /** + * Checks if this frozen node had the indicated child node. + * @param name name of the childnode + * @param idx 1-based index + * @return true if the child node exists + */ + boolean hasFrozenChildNode(Name name, int idx); + + /** + * Returns the frozen child node or null + * @param name name of the childnode + * @param idx 1-based index + * @return the child node + * @throws RepositoryException if an error occurs + */ + InternalFreeze getFrozenChildNode(Name name, int idx) throws RepositoryException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenNodeImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenNodeImpl.java new file mode 100644 index 00000000000..d1be5c4a591 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenNodeImpl.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.core.version; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.HashSet; +import java.util.Collections; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * Implements a InternalFrozenNode + */ +class InternalFrozenNodeImpl extends InternalFreezeImpl + implements InternalFrozenNode { + + /** + * the list of frozen properties + */ + private PropertyState[] frozenProperties; + + /** + * the frozen id of the original node + */ + private NodeId frozenUUID = null; + + /** + * the frozen primary type of the original node + */ + private Name frozenPrimaryType = null; + + /** + * the frozen list of mixin types of the original node + */ + private Set frozenMixinTypes = null; + + /** + * Creates a new frozen node based on the given persistence node. + * + * @param vMgr version manager + * @param node underlying node + * @param parent parent item + * @throws RepositoryException if an error occurs + */ + public InternalFrozenNodeImpl(InternalVersionManagerBase vMgr, NodeStateEx node, + InternalVersionItem parent) + throws RepositoryException { + super(vMgr, node, parent); + + // init the frozen properties + PropertyState[] props; + try { + props = node.getProperties(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + List propList = new ArrayList(); + + Set mixins = new HashSet(); + for (PropertyState prop : props) { + if (prop.getName().equals(NameConstants.JCR_FROZENUUID)) { + // special property + InternalValue value = + node.getPropertyValue(NameConstants.JCR_FROZENUUID); + // JCR-1803: The value should be a STRING, but older Jackrabbit + // versions (< 1.1, see JCR-487) used REFERENCE values. Since + // we do not automatically upgrade old content, we need to be + // ready to handle both types of values here. + if (value.getType() == PropertyType.STRING) { + frozenUUID = new NodeId(value.getString()); + } else { + frozenUUID = value.getNodeId(); + } + } else if (prop.getName().equals(NameConstants.JCR_FROZENPRIMARYTYPE)) { + // special property + frozenPrimaryType = node.getPropertyValue(NameConstants.JCR_FROZENPRIMARYTYPE).getName(); + } else if (prop.getName().equals(NameConstants.JCR_FROZENMIXINTYPES)) { + // special property + InternalValue[] values = node.getPropertyValues(NameConstants.JCR_FROZENMIXINTYPES); + if (values != null) { + for (InternalValue value : values) { + mixins.add(value.getName()); + } + } + } else if (!prop.getName().equals(NameConstants.JCR_PRIMARYTYPE) + && !prop.getName().equals(NameConstants.JCR_UUID)) { + propList.add(prop); + } + } + frozenProperties = propList.toArray(new PropertyState[propList.size()]); + frozenMixinTypes = Collections.unmodifiableSet(mixins); + + // do some checks + if (frozenPrimaryType == null) { + throw new RepositoryException("Illegal frozen node. Must have 'frozenPrimaryType'"); + } + } + + /** + * {@inheritDoc} + */ + public Name getName() { + return node.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public NodeId getId() { + return node.getNodeId(); + } + + /** + * {@inheritDoc} + */ + public List getFrozenChildNodes() + throws VersionException { + return node.getState().getChildNodeEntries(); + } + + /** + * {@inheritDoc} + */ + public boolean hasFrozenChildNode(Name name, int idx) { + return node.getState().hasChildNodeEntry(name, idx); + } + + /** + * {@inheritDoc} + */ + public InternalFreeze getFrozenChildNode(Name name, int idx) + throws RepositoryException { + ChildNodeEntry e = node.getState().getChildNodeEntry(name, idx); + return e == null + ? null + : (InternalFreeze) vMgr.getItem(e.getId()); + } + + /** + * {@inheritDoc} + */ + public PropertyState[] getFrozenProperties() { + return frozenProperties; + } + + /** + * {@inheritDoc} + */ + public NodeId getFrozenId() { + return frozenUUID; + } + + /** + * {@inheritDoc} + */ + public Name getFrozenPrimaryType() { + return frozenPrimaryType; + } + + /** + * {@inheritDoc} + */ + public Set getFrozenMixinTypes() { + return frozenMixinTypes; + } + + /** + * Checks-in a src node. It creates a new child node of + * parent with the given name and adds the + * source nodes properties according to their OPV value to the + * list of frozen properties. It creates frozen child nodes for each child + * node of src according to its OPV value. + * + * @param parent destination parent + * @param name new node name + * @param src source node state + * @return the node node state + * @throws RepositoryException if an error occurs + */ + protected static NodeStateEx checkin(NodeStateEx parent, Name name, + NodeStateEx src) + throws RepositoryException { + try { + return checkin(parent, name, src, false); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * Checks-in a src node. It creates a new child node of + * parent with the given name and adds the + * source nodes properties according to their OPV value to the + * list of frozen properties. It creates frozen child nodes for each child + * node of src according to its OPV value. + * + * @param parent destination parent + * @param name new node name + * @param src source node state + * @param forceCopy if true the OPV is ignored and a COPY is performed + * @return the nde node state + * @throws RepositoryException if an error occurs + * @throws ItemStateException if an error during reading the items occurs + */ + private static NodeStateEx checkin(NodeStateEx parent, Name name, + NodeStateEx src, boolean forceCopy) + throws RepositoryException, ItemStateException { + + // create new node + NodeStateEx node = parent.addNode(name, NameConstants.NT_FROZENNODE, null, true); + + // initialize the internal properties + node.setPropertyValue(NameConstants.JCR_FROZENUUID, + InternalValue.create(src.getNodeId().toString())); + node.setPropertyValue(NameConstants.JCR_FROZENPRIMARYTYPE, + InternalValue.create(src.getState().getNodeTypeName())); + if (src.hasProperty(NameConstants.JCR_MIXINTYPES)) { + node.setPropertyValues(NameConstants.JCR_FROZENMIXINTYPES, + PropertyType.NAME, src.getPropertyValues(NameConstants.JCR_MIXINTYPES)); + } + + // add the properties + for (PropertyState prop: src.getProperties()) { + int opv; + if (forceCopy) { + opv = OnParentVersionAction.COPY; + } else { + opv = src.getDefinition(prop).getOnParentVersion(); + } + + Name propName = prop.getName(); + if (opv == OnParentVersionAction.ABORT) { + parent.reload(); + throw new VersionException("Checkin aborted due to OPV abort in " + propName); + } else if (opv == OnParentVersionAction.VERSION + || opv == OnParentVersionAction.COPY) { + // ignore frozen properties + if (!propName.equals(NameConstants.JCR_PRIMARYTYPE) + && !propName.equals(NameConstants.JCR_MIXINTYPES) + && !propName.equals(NameConstants.JCR_UUID) + // JCR-3635: should never occur in normal content... + && !propName.equals(NameConstants.JCR_FROZENPRIMARYTYPE) + && !propName.equals(NameConstants.JCR_FROZENMIXINTYPES) + && !propName.equals(NameConstants.JCR_FROZENUUID)) { + node.copyFrom(prop); + } + } + } + + // add the frozen children and histories + boolean isFull = src.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE); + for (NodeStateEx child: src.getChildNodes()) { + int opv; + if (forceCopy) { + opv = OnParentVersionAction.COPY; + } else { + opv = child.getDefinition().getOnParentVersion(); + } + + if (opv == OnParentVersionAction.ABORT) { + throw new VersionException("Checkin aborted due to OPV in " + child); + } else if (opv == OnParentVersionAction.VERSION) { + if (isFull && child.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) { + // create frozen versionable child + NodeId histId = child.getPropertyValue(NameConstants.JCR_VERSIONHISTORY).getNodeId(); + NodeStateEx newChild = node.addNode(child.getName(), NameConstants.NT_VERSIONEDCHILD, null, false); + newChild.setPropertyValue( + NameConstants.JCR_CHILDVERSIONHISTORY, + InternalValue.create(histId)); + } else { + // else copy + checkin(node, child.getName(), child, true); + } + } else if (opv == OnParentVersionAction.COPY) { + checkin(node, child.getName(), child, true); + } + } + return node; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenVHImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenVHImpl.java new file mode 100644 index 00000000000..53bb53d928d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenVHImpl.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionException; + +/** + * Implements a InternalFrozenVersionHistory + */ +class InternalFrozenVHImpl extends InternalFreezeImpl + implements InternalFrozenVersionHistory { + + /** + * Creates a new frozen version history. + * + * @param node + */ + public InternalFrozenVHImpl(InternalVersionManagerBase vMgr, NodeStateEx node, + InternalVersionItem parent) { + super(vMgr, node, parent); + } + + + /** + * {@inheritDoc} + */ + public Name getName() { + return node.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public NodeId getId() { + return node.getNodeId(); + } + + /** + * {@inheritDoc} + */ + public NodeId getVersionHistoryId() { + return node.getPropertyValue(NameConstants.JCR_CHILDVERSIONHISTORY).getNodeId(); + } + + /** + * {@inheritDoc} + */ + public InternalVersionHistory getVersionHistory() + throws VersionException { + try { + return vMgr.getVersionHistory(getVersionHistoryId()); + } catch (RepositoryException e) { + throw new VersionException(e); + } + } + + /** + * {@inheritDoc} + */ + public NodeId getBaseVersionId() { + return node.getPropertyValue(NameConstants.JCR_BASEVERSION).getNodeId(); + } + + /** + * @deprecate Use {@link #getBaseVersion()} instead. + */ + public InternalVersion getBaseVesion() + throws VersionException { + return getBaseVersion(); + } + + /** + * {@inheritDoc} + */ + public InternalVersion getBaseVersion() + throws VersionException { + try { + InternalVersionHistory history = vMgr.getVersionHistory(getVersionHistoryId()); + return history.getVersion(getBaseVersionId()); + } catch (RepositoryException e) { + throw new VersionException(e); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenVersionHistory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenVersionHistory.java new file mode 100644 index 00000000000..05d3c22ff94 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalFrozenVersionHistory.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.core.version; + +import org.apache.jackrabbit.core.id.NodeId; + +import javax.jcr.version.VersionException; + +/** + * This interface defines a frozen versionable child node, that was created + * during a {@link javax.jcr.Node#checkin()} with a OPV==Version node. + */ +public interface InternalFrozenVersionHistory extends InternalFreeze { + + /** + * Returns the id of the version history that was assigned to the node at + * the time it was versioned. + * + * @return the id of the version history + */ + NodeId getVersionHistoryId(); + + /** + * Returns the version history that was assigned to the node at + * the time it was versioned. + * + * @return the internal version history. + * @throws VersionException if the history cannot be retrieved. + */ + InternalVersionHistory getVersionHistory() + throws VersionException; + + /** + * Returns the id of the base version that was assigned to the node at + * the time it was versioned. + * + * @return the id of the base version + */ + NodeId getBaseVersionId(); + + /** + * @deprecated use {@link #getBaseVersion()} instead + */ + InternalVersion getBaseVesion() throws VersionException; + + /** + * Returns the base version that was assigned to the node at + * the time it was versioned. + * + * @return the internal base version + * @throws VersionException if the version could not be retrieved + */ + InternalVersion getBaseVersion() throws VersionException; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersion.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersion.java new file mode 100644 index 00000000000..b2ba4efe119 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersion.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.core.version; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.id.NodeId; +import javax.jcr.version.Version; + +import java.util.Calendar; +import java.util.List; + +/** + * This interface defines the internal version. + */ +public interface InternalVersion extends InternalVersionItem { + + /** + * Returns the name of this version. + * + * @return the name of this version. + */ + Name getName(); + + /** + * Returns the frozen node of this version. + * + * @return the frozen node. + */ + InternalFrozenNode getFrozenNode(); + + /** + * Returns the node id of the frozen node. + * + * @return the node id of the frozen node; + */ + NodeId getFrozenNodeId(); + + /** + * Equivalent to {@link javax.jcr.version.Version#getCreated()} + * + * @see javax.jcr.version.Version#getCreated() + * @return the created date + */ + Calendar getCreated(); + + /** + * Equivalent to {@link javax.jcr.version.Version#getSuccessors()}} + * + * @see javax.jcr.version.Version#getSuccessors() + * @return the successors as internal versions + */ + List getSuccessors(); + + /** + * Equivalent to {@link Version#getLinearSuccessor()}. + * + * @param baseVersion base version to determine single line of descent + * @return the successor as internal version + * + * @see Version#getLinearSuccessor() + */ + InternalVersion getLinearSuccessor(InternalVersion baseVersion); + + /** + * Equivalent to {@link javax.jcr.version.Version#getPredecessors()}} + * + * @see javax.jcr.version.Version#getPredecessors() + * @return the predecessors as internal versions + */ + InternalVersion[] getPredecessors(); + + /** + * Equivalent to {@link Version#getLinearPredecessor()} + * + * @see Version#getLinearPredecessor() + * @return the predecessor as internal version + */ + InternalVersion getLinearPredecessor(); + + /** + * Checks if this version is more recent than the given version v. + * A version is more recent if and only if it is a successor (or a successor + * of a successor, etc., to any degree of separation) of the compared one. + * + * @param v the version to check + * @return true if the version is more recent; + * false otherwise. + */ + boolean isMoreRecent(InternalVersion v); + + /** + * returns the internal version history in which this version lives in. + * + * @return the version history for this version. + */ + InternalVersionHistory getVersionHistory(); + + /** + * checks if this is the root version. + * + * @return true if this version is the root version; + * false otherwise. + */ + boolean isRootVersion(); + + /** + * Checks, if this version has the given label associated + * + * @param label the label to check. + * @return true if the label is assigned to this version; + * false otherwise. + */ + boolean hasLabel(Name label); + + /** + * returns the labels that are assigned to this version + * + * @return a string array of labels. + */ + Name[] getLabels(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionHistory.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionHistory.java new file mode 100644 index 00000000000..f66659991eb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionHistory.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.core.version; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.core.id.NodeId; + +import javax.jcr.version.VersionException; + +/** + * This interface defines the internal version history. + */ +public interface InternalVersionHistory extends InternalVersionItem { + + /** + * Equivalent to {@link javax.jcr.version.VersionHistory#getRootVersion()}. + * + * @return the root version + * @see javax.jcr.version.VersionHistory#getRootVersion() + */ + InternalVersion getRootVersion(); + + /** + * Equivalent to {@link javax.jcr.version.VersionHistory#getVersion(java.lang.String)}. + * + * @param versionName the name of the version + * @return the version + * @throws VersionException if the version does not exist + * @see javax.jcr.version.VersionHistory#getVersion(java.lang.String) + */ + InternalVersion getVersion(Name versionName) throws VersionException; + + /** + * Checks if the version with the given name exists in this version history. + * + * @param versionName the name of the version + * @return true if the version exists; + * false otherwise. + */ + boolean hasVersion(Name versionName); + + /** + * Returns the version with the given uuid or null if the + * respective version does not exist. + * + * @param id the id of the version + * @return the internal version ot null + */ + InternalVersion getVersion(NodeId id); + + /** + * Equivalent to {@link javax.jcr.version.VersionHistory#getVersionByLabel(java.lang.String)} + * but returns null if the version does not exists. + * + * @param label the lable + * @see javax.jcr.version.VersionHistory#getVersionByLabel(java.lang.String) + * @return the version or null if not exists + */ + InternalVersion getVersionByLabel(Name label); + + /** + * Returns the number of versions in this version history. + * + * @return the number of versions, including the root version. + */ + int getNumVersions(); + + /** + * Returns the id of the versionable node that this history belongs to. + * + * @return the id of the versionable node. + */ + NodeId getVersionableId(); + + /** + * Returns a name array of all version labels that exist in this + * version history + * + * @return the labels + */ + Name[] getVersionLabels(); + + /** + * Returns a name array of all version names that exist in this version history. + * @return the names + */ + Name[] getVersionNames(); + + /** + * Returns the Id of the version labels node. + * + * @return the id of the version labels node. + */ + NodeId getVersionLabelsId(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionHistoryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionHistoryImpl.java new file mode 100644 index 00000000000..5f356b286a1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionHistoryImpl.java @@ -0,0 +1,692 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements a InternalVersionHistory + */ +class InternalVersionHistoryImpl extends InternalVersionItemImpl + implements InternalVersionHistory { + + /** + * default logger + */ + private static Logger log = LoggerFactory.getLogger(InternalVersionHistoryImpl.class); + + /** + * The last current time that was returned by {@link #getCurrentTime()}. + */ + private static final Calendar CURRENT_TIME = Calendar.getInstance(); + + /** + * the cache of the version labels + * key = version label (String) + * value = version name + */ + private Map labelCache = new HashMap(); + + /** + * the root version of this history + */ + private InternalVersion rootVersion; + + /** + * the hashmap of all versions names + * key = version name + * value = version id (NodeId) + */ + private Map nameCache = new LinkedHashMap(); + + /** + * the hashmap of all versions + * key = version id (NodeId) + * value = version + */ + private Map versionCache = new HashMap(); + + /** + * Temporary version cache, used on a refresh. + */ + private Map tempVersionCache = new HashMap(); + + /** + * the node that holds the label nodes + */ + private NodeStateEx labelNode; + + /** + * the id of this history + */ + private NodeId historyId; + + /** + * the id of the versionable node + */ + private NodeId versionableId; + + /** + * Creates a new VersionHistory object for the given node state. + * @param vMgr version manager + * @param node version history node state + * @throws RepositoryException if an error occurs + */ + public InternalVersionHistoryImpl(InternalVersionManagerBase vMgr, NodeStateEx node) + throws RepositoryException { + super(vMgr, node); + init(); + fixLegacy(); + } + + /** + * Initializes the history and loads all internal caches + * + * @throws RepositoryException if an error occurs + */ + private synchronized void init() throws RepositoryException { + nameCache.clear(); + versionCache.clear(); + labelCache.clear(); + + // get id + historyId = node.getNodeId(); + + // get versionable id + versionableId = NodeId.valueOf(node.getPropertyValue(NameConstants.JCR_VERSIONABLEUUID).toString()); + + // get label node + labelNode = node.getNode(NameConstants.JCR_VERSIONLABELS, 1); + + // init label cache + try { + PropertyState[] labels = labelNode.getProperties(); + for (PropertyState pState : labels) { + if (pState.getType() == PropertyType.REFERENCE) { + Name labelName = pState.getName(); + NodeId id = pState.getValues()[0].getNodeId(); + if (node.getState().hasChildNodeEntry(id)) { + labelCache.put(labelName, node.getState().getChildNodeEntry(id).getName()); + } else { + log.warn("Error while resolving label reference. Version missing: " + id); + } + } + } + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + + // get root version + rootVersion = createVersionInstance(NameConstants.JCR_ROOTVERSION); + + // get version entries + for (ChildNodeEntry child : node.getState().getChildNodeEntries()) { + if (child.getName().equals(NameConstants.JCR_VERSIONLABELS)) { + continue; + } + nameCache.put(child.getName(), child.getId()); + } + } + + + // fix legacy + private void fixLegacy() throws RepositoryException { + if (rootVersion.getSuccessors().isEmpty()) { + for (Name versionName : getVersionNames()) { + InternalVersionImpl v = createVersionInstance(versionName); + v.legacyResolveSuccessors(); + } + } + } + + /** + * Reload this object and all its dependent version objects. + * @throws RepositoryException if an error occurs + */ + synchronized void reload() throws RepositoryException { + tempVersionCache.putAll(versionCache); + + init(); + + // invalidate all versions that are not referenced any more + for (Object o : tempVersionCache.values()) { + InternalVersionImpl v = (InternalVersionImpl) o; + v.invalidate(); + } + tempVersionCache.clear(); + } + + /** + * Create a version instance. + * @param name name of the version + * @return the new internal version + * @throws IllegalArgumentException if the version does not exist + */ + synchronized InternalVersionImpl createVersionInstance(Name name) { + try { + NodeStateEx nodeStateEx = node.getNode(name, 1); + InternalVersionImpl v = createVersionInstance(nodeStateEx); + versionCache.put(v.getId(), v); + vMgr.versionCreated(v); + + // add labels + for (Name labelName: labelCache.keySet()) { + Name versionName = labelCache.get(labelName); + if (v.getName().equals(versionName)) { + v.internalAddLabel(labelName); + } + } + return v; + } catch (RepositoryException e) { + throw new InconsistentVersioningState("Failed to create version " + name + " in VHR " + historyId + ".", historyId, null); + } + } + + /** + * Create a version instance. May resurrect versions temporarily swapped + * out when refreshing this history. + * @param child child node state + * @return new version instance + */ + synchronized InternalVersionImpl createVersionInstance(NodeStateEx child) { + InternalVersionImpl v = (InternalVersionImpl) tempVersionCache.remove(child.getNodeId()); + if (v != null) { + v.clear(); + } else { + // check if baseline + try { + NodeStateEx frozen = child.getNode(NameConstants.JCR_FROZENNODE, 1); + Name frozenType = frozen.getPropertyValue(NameConstants.JCR_FROZENPRIMARYTYPE).getName(); + if (NameConstants.NT_CONFIGURATION.equals(frozenType)) { + v = new InternalBaselineImpl(this, child, child.getName()); + } else { + v = new InternalVersionImpl(this, child, child.getName()); + } + } catch (RepositoryException e) { + throw new InconsistentVersioningState("Version does not have a jcr:frozenNode: " + child.getNodeId(), historyId, e); + } + } + return v; + } + + /** + * {@inheritDoc} + */ + @Override + public NodeId getId() { + return historyId; + } + + /** + * {@inheritDoc} + */ + @Override + public InternalVersionItem getParent() { + return null; + } + + /** + * {@inheritDoc} + */ + public InternalVersion getRootVersion() { + return rootVersion; + } + + /** + * {@inheritDoc} + */ + public synchronized InternalVersion getVersion(Name versionName) + throws VersionException { + NodeId versionId = nameCache.get(versionName); + if (versionId == null) { + throw new VersionException("Version " + versionName + " does not exist."); + } + + InternalVersion v = versionCache.get(versionId); + if (v == null) { + v = createVersionInstance(versionName); + } + return v; + } + + /** + * {@inheritDoc} + */ + public synchronized boolean hasVersion(Name versionName) { + return nameCache.containsKey(versionName); + } + + /** + * {@inheritDoc} + */ + public InternalVersion getVersion(NodeId id) { + InternalVersion v = getCachedVersion(id); + + // If the version was not found, our cache may not have been + // synchronized with updates from another cluster node. Reload the history + // to be sure we have the latest updates and try again. + if (v == null) { + try { + reload(); + } catch (RepositoryException e) { + + // We should add the checked exception to this method definition + // so we don't need to wrap it. + // Avoiding it for now to limit impact of this fix. + throw new RuntimeException(e); + } + v = getCachedVersion(id); + } + + return v; + } + + /** + * Returns the version from cache, or null if it is not + * present. + * @param id the id of the version + * @return the version or null if not cached. + */ + private synchronized InternalVersion getCachedVersion(NodeId id) { + InternalVersion v = versionCache.get(id); + if (v == null) { + for (Name versionName : nameCache.keySet()) { + if (nameCache.get(versionName).equals(id)) { + v = createVersionInstance(versionName); + break; + } + } + } + return v; + } + + /** + * {@inheritDoc} + */ + public synchronized InternalVersion getVersionByLabel(Name label) { + Name versionName = labelCache.get(label); + if (versionName == null) { + return null; + } + + NodeId id = nameCache.get(versionName); + InternalVersion v = versionCache.get(id); + if (v == null) { + v = createVersionInstance(versionName); + } + return v; + } + + /** + * {@inheritDoc} + */ + public synchronized Name[] getVersionNames() { + return nameCache.keySet().toArray(new Name[nameCache.size()]); + } + + /** + * {@inheritDoc} + */ + public synchronized int getNumVersions() { + return nameCache.size(); + } + + /** + * {@inheritDoc} + */ + public NodeId getVersionableId() { + return versionableId; + } + + /** + * {@inheritDoc} + */ + public synchronized Name[] getVersionLabels() { + return labelCache.keySet().toArray(new Name[labelCache.size()]); + } + + /** + * {@inheritDoc} + */ + public NodeId getVersionLabelsId() { + return labelNode.getNodeId(); + } + + /** + * Removes the indicated version from this VersionHistory. If the specified + * vesion does not exist, if it specifies the root version or if it is + * referenced by any node e.g. as base version, a VersionException is thrown. + *

    + * all successors of the removed version become successors of the + * predecessors of the removed version and vice versa. then, the entire + * version node and all its subnodes are removed. + * + * @param versionName name of the version to remove + * @throws VersionException if removal is not possible + */ + synchronized void removeVersion(Name versionName) throws RepositoryException { + + InternalVersionImpl v = (InternalVersionImpl) getVersion(versionName); + if (v.equals(rootVersion)) { + String msg = "Removal of " + versionName + " not allowed."; + log.debug(msg); + throw new VersionException(msg); + } + // check if any references (from outside the version storage) exist on this version + if (vMgr.hasItemReferences(v.getId())) { + throw new ReferentialIntegrityException("Unable to remove version. At least once referenced."); + } + + // unregister from labels + Name[] labels = v.internalGetLabels(); + for (Name label : labels) { + v.internalRemoveLabel(label); + labelNode.removeProperty(label); + } + // detach from the version graph + v.internalDetach(); + + // check if referenced by an activity + InternalActivityImpl activity = v.getActivity(); + if (activity != null) { + activity.removeVersion(v); + } + + // remove from persistence state + node.removeNode(v.getName()); + + // and remove from history + versionCache.remove(v.getId()); + nameCache.remove(versionName); + vMgr.versionDestroyed(v); + + // Check if this was the last version in addition to the root version + if (!vMgr.hasItemReferences(node.getNodeId())) { + log.debug("Current version history has no references"); + NodeStateEx[] childNodes = node.getChildNodes(); + + // Check if there is only root version and version labels nodes + if (childNodes.length == 2) { + log.debug("Removing orphan version history as it contains only two children"); + NodeStateEx parentNode = node.getParent(); + // Remove version history node + parentNode.removeNode(node.getName()); + // store changes for this node and his children + parentNode.store(); + } else { + node.store(); + } + } else { + log.debug("Current version history has at least one reference"); + // store changes + node.store(); + } + + // now also remove from labelCache + for (Name label : labels) { + labelCache.remove(label); + } + } + + /** + * Sets the version label to the given version. + * If the label is already assigned to another version, a VersionException is + * thrown unless move is true. If version + * is null, the label is removed from the respective version. + * In either case, the version the label was previously assigned to is returned, + * or null of the label was not moved. + * + * @param versionName the name of the version + * @param label the label to assign + * @param move flag what to do by collisions + * @return the version that was previously assigned by this label or null. + * @throws VersionException if the version does not exist or if the label is already defined. + */ + synchronized InternalVersion setVersionLabel(Name versionName, Name label, boolean move) + throws VersionException { + InternalVersion version = + (versionName != null) ? getVersion(versionName) : null; + if (versionName != null && version == null) { + throw new VersionException("Version " + versionName + " does not exist in this version history."); + } + Name prevName = labelCache.get(label); + InternalVersionImpl prev = null; + if (prevName == null) { + if (version == null) { + return null; + } + } else { + prev = (InternalVersionImpl) getVersion(prevName); + if (prev.equals(version)) { + return version; + } else if (!move) { + // already defined elsewhere, throw + throw new VersionException("Version label " + label + " already defined for version " + prev.getName()); + } + } + + // update persistence + try { + if (version == null) { + labelNode.removeProperty(label); + } else { + labelNode.setPropertyValue( + label, InternalValue.create(version.getId())); + } + labelNode.store(); + } catch (RepositoryException e) { + throw new VersionException(e); + } + + // update internal structures + if (prev != null) { + prev.internalRemoveLabel(label); + labelCache.remove(label); + } + if (version != null) { + labelCache.put(label, version.getName()); + ((InternalVersionImpl) version).internalAddLabel(label); + } + return prev; + } + + /** + * Checks in a node. It creates a new version with the given name and freezes + * the state of the given node. + * + * @param name new version name + * @param src source node to version + * @param created optional created date + * @return the newly created version + * @throws RepositoryException if an error occurs + */ + synchronized InternalVersionImpl checkin( + Name name, NodeStateEx src, Calendar created) + throws RepositoryException { + + // copy predecessors from src node + InternalValue[] predecessors; + if (src.hasProperty(NameConstants.JCR_PREDECESSORS)) { + predecessors = src.getPropertyValues(NameConstants.JCR_PREDECESSORS); + // check all predecessors + for (InternalValue pred: predecessors) { + NodeId predId = pred.getNodeId(); + // check if version exist + if (!nameCache.containsValue(predId)) { + throw new RepositoryException( + "Invalid predecessor in source node: " + predId); + } + } + } else { + // with simple versioning, the node does not contain a predecessors + // property and we just use the 'head' version as predecessor + Iterator iter = nameCache.values().iterator(); + NodeId last = null; + while (iter.hasNext()) { + last = iter.next(); + } + if (last == null) { + // should never happen + last = rootVersion.getId(); + } + predecessors = new InternalValue[]{InternalValue.create(last)}; + } + + NodeId versionId = vMgr.getNodeIdFactory().newNodeId(); + NodeStateEx vNode = node.addNode(name, NameConstants.NT_VERSION, versionId, true); + + // check for jcr:activity + if (src.hasProperty(NameConstants.JCR_ACTIVITY)) { + InternalValue act = src.getPropertyValue(NameConstants.JCR_ACTIVITY); + vNode.setPropertyValue(NameConstants.JCR_ACTIVITY, act); + } + + // initialize 'created', 'predecessors' and 'successors' + if (created == null) { + created = getCurrentTime(); + } + vNode.setPropertyValue(NameConstants.JCR_CREATED, InternalValue.create(created)); + vNode.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, predecessors); + vNode.setPropertyValues(NameConstants.JCR_SUCCESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY); + + // checkin source node + InternalFrozenNodeImpl.checkin(vNode, NameConstants.JCR_FROZENNODE, src); + + // update version graph + boolean isConfiguration = src.getEffectiveNodeType().includesNodeType(NameConstants.NT_CONFIGURATION); + InternalVersionImpl version = isConfiguration + ? new InternalBaselineImpl(this, vNode, name) + : new InternalVersionImpl(this, vNode, name); + version.internalAttach(); + + // and store + node.store(); + + vMgr.versionCreated(version); + + // update cache + versionCache.put(version.getId(), version); + nameCache.put(version.getName(), version.getId()); + + return version; + } + + /** + * Creates a new version history below the given parent node and with + * the given name. + * + * @param vMgr version manager + * @param parent parent node + * @param name history name + * @param nodeState node state + * @param copiedFrom the id of the base version + * @return new node state + * @throws RepositoryException if an error occurs + */ + static NodeStateEx create( + InternalVersionManagerBase vMgr, NodeStateEx parent, Name name, + NodeState nodeState, NodeId copiedFrom) throws RepositoryException { + + // create history node + NodeId historyId = vMgr.getNodeIdFactory().newNodeId(); + NodeStateEx pNode = parent.addNode(name, NameConstants.NT_VERSIONHISTORY, historyId, true); + + // set the versionable uuid + String versionableUUID = nodeState.getNodeId().toString(); + pNode.setPropertyValue(NameConstants.JCR_VERSIONABLEUUID, InternalValue.create(versionableUUID)); + + // create label node + pNode.addNode(NameConstants.JCR_VERSIONLABELS, NameConstants.NT_VERSIONLABELS, null, false); + + // initialize the 'jcr:copiedFrom' property + if (copiedFrom != null) { + pNode.setPropertyValue(NameConstants.JCR_COPIEDFROM, InternalValue.create(copiedFrom, true)); + } + + // create root version + NodeId versionId = vMgr.getNodeIdFactory().newNodeId(); + NodeStateEx vNode = pNode.addNode(NameConstants.JCR_ROOTVERSION, NameConstants.NT_VERSION, versionId, true); + + // initialize 'created' and 'predecessors' + vNode.setPropertyValue(NameConstants.JCR_CREATED, InternalValue.create(getCurrentTime())); + vNode.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY); + vNode.setPropertyValues(NameConstants.JCR_SUCCESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY); + + // add also an empty frozen node to the root version + NodeStateEx node = vNode.addNode(NameConstants.JCR_FROZENNODE, NameConstants.NT_FROZENNODE, null, true); + + // initialize the internal properties + node.setPropertyValue(NameConstants.JCR_FROZENUUID, InternalValue.create(versionableUUID)); + node.setPropertyValue(NameConstants.JCR_FROZENPRIMARYTYPE, + InternalValue.create(nodeState.getNodeTypeName())); + + Set mixins = nodeState.getMixinTypeNames(); + if (!mixins.isEmpty()) { + InternalValue[] ivalues = new InternalValue[mixins.size()]; + Iterator iter = mixins.iterator(); + for (int i = 0; i < mixins.size(); i++) { + ivalues[i] = InternalValue.create(iter.next()); + } + node.setPropertyValues(NameConstants.JCR_FROZENMIXINTYPES, PropertyType.NAME, ivalues); + } + + parent.store(false); + pNode.store(true); + return pNode; + } + + /** + * Returns the current time as a calendar instance and makes sure that no + * two Calendar instances represent the exact same time. If this method is + * called quickly in succession each Calendar instance returned is at least + * one millisecond later than the previous one. + * + * @return the current time. + */ + static Calendar getCurrentTime() { + long time = System.currentTimeMillis(); + synchronized (CURRENT_TIME) { + if (time > CURRENT_TIME.getTimeInMillis()) { + CURRENT_TIME.setTimeInMillis(time); + } else { + CURRENT_TIME.add(Calendar.MILLISECOND, 1); + } + return (Calendar) CURRENT_TIME.clone(); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionImpl.java new file mode 100644 index 00000000000..a1c0369fc6e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionImpl.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.core.version; + +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; + +/** + * Implements a InternalVersion + */ +class InternalVersionImpl extends InternalVersionItemImpl + implements InternalVersion { + + /** Logger instance */ + private static final Logger log = + LoggerFactory.getLogger(InternalVersionImpl.class); + + /** + * the date when this version was created + */ + private Calendar created; + + /** + * the set of version labes of this history (values == String) + */ + private HashSet labelCache = null; + + /** + * specifies if this is the root version + */ + private final boolean isRoot; + + /** + * the version name + */ + private final Name name; + + /** + * the version history + */ + private final InternalVersionHistory versionHistory; + + /** + * Creates a new internal version with the given version history and + * persistence node. please note, that versions must be created by the + * version history. + * + * @param vh containing version history + * @param node node state of this version + * @param name name of this version + */ + public InternalVersionImpl(InternalVersionHistoryImpl vh, NodeStateEx node, Name name) throws RepositoryException{ + super(vh.getVersionManager(), node); + this.versionHistory = vh; + this.name = name; + + // init internal values + InternalValue[] values = node.getPropertyValues(NameConstants.JCR_CREATED); + if (values != null) { + created = values[0].getDate(); + } + isRoot = name.equals(NameConstants.JCR_ROOTVERSION); + } + + /** + * {@inheritDoc} + */ + @Override + public NodeId getId() { + return node.getNodeId(); + } + + /** + * {@inheritDoc} + */ + @Override + public InternalVersionItem getParent() { + return versionHistory; + } + + /** + * {@inheritDoc} + */ + public Name getName() { + return name; + } + + /** + * {@inheritDoc} + */ + public InternalFrozenNode getFrozenNode() { + // get frozen node + try { + return (InternalFrozenNode) vMgr.getItem(getFrozenNodeId()); + } catch (RepositoryException e) { + throw new InconsistentVersioningState("unable to retrieve frozen node: " + e, null, e); + } + } + + /** + * {@inheritDoc} + */ + public NodeId getFrozenNodeId() { + ChildNodeEntry entry = node.getState().getChildNodeEntry(NameConstants.JCR_FROZENNODE, 1); + if (entry == null) { + throw new InconsistentVersioningState("version has no frozen node: " + getId()); + } + return entry.getId(); + } + + /** + * {@inheritDoc} + */ + public Calendar getCreated() { + return created; + } + + /** + * {@inheritDoc} + */ + public List getSuccessors() { + VersioningLock.ReadLock lock = vMgr.acquireReadLock(); + try { + InternalValue[] values = + node.getPropertyValues(NameConstants.JCR_SUCCESSORS); + if (values != null) { + List versions = + new ArrayList(values.length); + for (InternalValue value : values) { + InternalVersion version = + versionHistory.getVersion(value.getNodeId()); + if (version != null) { + versions.add(version); + } else { + // Can happen with a corrupted repository (JCR-2655) + log.warn("Missing successor {}", value.getNodeId()); + } + } + return versions; + } else { + return Collections.emptyList(); + } + } finally { + lock.release(); + } + } + + /** + * {@inheritDoc} + */ + public InternalVersion getLinearSuccessor(InternalVersion baseVersion) { + // walk up all predecessors of the base version until 'this' version + // is found. + InternalVersion pred = baseVersion.getLinearPredecessor(); + while (pred != null && !pred.getId().equals(getId())) { + baseVersion = pred; + pred = baseVersion.getLinearPredecessor(); + } + return pred == null ? null : baseVersion; + } + + /** + * {@inheritDoc} + */ + public InternalVersion[] getPredecessors() { + InternalValue[] values = node.getPropertyValues(NameConstants.JCR_PREDECESSORS); + if (values != null) { + InternalVersion[] versions = new InternalVersion[values.length]; + for (int i = 0; i < values.length; i++) { + versions[i] = versionHistory.getVersion(values[i].getNodeId()); + } + return versions; + } else { + return new InternalVersion[0]; + } + } + + /** + * {@inheritDoc} + * + * Always return the left most predecessor + */ + public InternalVersion getLinearPredecessor() { + InternalValue[] values = node.getPropertyValues(NameConstants.JCR_PREDECESSORS); + if (values != null && values.length > 0) { + return versionHistory.getVersion(values[0].getNodeId()); + } else { + return null; + } + } + + /** + * {@inheritDoc} + */ + public boolean isMoreRecent(InternalVersion v) { + InternalVersion[] preds = getPredecessors(); + for (InternalVersion pred : preds) { + if (pred.equals(v) || pred.isMoreRecent(v)) { + return true; + } + } + return false; + } + + /** + * {@inheritDoc} + */ + public InternalVersionHistory getVersionHistory() { + return versionHistory; + } + + /** + * {@inheritDoc} + */ + public boolean hasLabel(Name label) { + return internalHasLabel(label); + } + + /** + * {@inheritDoc} + */ + public Name[] getLabels() { + return internalGetLabels(); + } + + /** + * {@inheritDoc} + */ + public boolean isRootVersion() { + return isRoot; + } + + /** + * Clear the list of predecessors/successors and the label cache. + */ + synchronized void clear() { + labelCache = null; + } + + /** + * stores the given successors or predecessors to the persistence node + * + * @param cessors list of versions to store + * @param propname property name to store + * @param store if true the node is stored + * @throws RepositoryException if a repository error occurs + */ + private void storeXCessors(List cessors, Name propname, boolean store) + throws RepositoryException { + InternalValue[] values = new InternalValue[cessors.size()]; + for (int i = 0; i < values.length; i++) { + values[i] = InternalValue.create((cessors.get(i)).getId()); + } + node.setPropertyValues(propname, PropertyType.REFERENCE, values); + if (store) { + node.store(); + } + } + + /** + * Detaches itself from the version graph. + * + * @throws RepositoryException if a repository error occurs + */ + void internalDetach() throws RepositoryException { + // detach this from all successors + for (InternalVersion aSucc : getSuccessors()) { + ((InternalVersionImpl) aSucc).internalDetachPredecessor(this, true); + } + + // detach cached successors from predecessors + InternalVersion[] preds = getPredecessors(); + for (InternalVersion pred : preds) { + ((InternalVersionImpl) pred).internalDetachSuccessor(this, true); + } + + // clear properties + clear(); + } + + /** + * Attaches this version as successor to all predecessors. assuming that the + * predecessors are already set. + * + * @throws RepositoryException if a repository error occurs + */ + void internalAttach() throws RepositoryException { + InternalVersion[] preds = getPredecessors(); + for (InternalVersion pred : preds) { + ((InternalVersionImpl) pred).internalAddSuccessor(this, true); + } + } + + /** + * Adds a version to the set of successors. + * + * @param succ successor + * @param store true if node is stored + * @throws RepositoryException if a repository error occurs + */ + private void internalAddSuccessor(InternalVersionImpl succ, boolean store) + throws RepositoryException { + List l = new ArrayList(getSuccessors()); + if (!l.contains(succ)) { + l.add(succ); + storeXCessors(l, NameConstants.JCR_SUCCESSORS, store); + } + } + + /** + * Removes the predecessor V of this predecessors list and adds all of Vs + * predecessors to it. + *

    + * please note, that this operation might corrupt the version graph + * + * @param v the successor to detach + * @param store true if node is stored immediately + * @throws RepositoryException if a repository error occurs + */ + private void internalDetachPredecessor(InternalVersionImpl v, boolean store) + throws RepositoryException { + // remove 'v' from predecessor list + List l = new ArrayList(Arrays.asList(getPredecessors())); + l.remove(v); + + // attach V's predecessors + l.addAll(Arrays.asList(v.getPredecessors())); + storeXCessors(l, NameConstants.JCR_PREDECESSORS, store); + } + + /** + * Removes the successor V of this successors list and adds all of Vs + * successors to it. + *

    + * please note, that this operation might corrupt the version graph + * + * @param v the successor to detach + * @param store true if node is stored immediately + * @throws RepositoryException if a repository error occurs + */ + private void internalDetachSuccessor(InternalVersionImpl v, boolean store) + throws RepositoryException { + // remove 'v' from successors list + List l = new ArrayList(getSuccessors()); + l.remove(v); + + // attach V's successors + l.addAll(v.getSuccessors()); + storeXCessors(l, NameConstants.JCR_SUCCESSORS, store); + } + + /** + * adds a label to the label cache. does not affect storage + * + * @param label label to add + * @return true if the label was added + */ + synchronized boolean internalAddLabel(Name label) { + if (labelCache == null) { + labelCache = new HashSet(); + } + return labelCache.add(label); + } + + /** + * removes a label from the label cache. does not affect storage + * + * @param label label to remove + * @return true if the label was removed + */ + synchronized boolean internalRemoveLabel(Name label) { + return labelCache != null && labelCache.remove(label); + } + + /** + * checks, if a label is in the label cache + * + * @param label label to check + * @return true if the label exists + */ + synchronized boolean internalHasLabel(Name label) { + return labelCache != null && labelCache.contains(label); + } + + /** + * returns the array of the cached labels + * + * @return the internal labels + */ + synchronized Name[] internalGetLabels() { + if (labelCache == null) { + return Name.EMPTY_ARRAY; + } else { + return labelCache.toArray(new Name[labelCache.size()]); + } + } + + /** + * Invalidate this item. + */ + void invalidate() { + node.getState().discard(); + } + + /** + * Resolves jcr:successor properties that are missing. + * + * @throws RepositoryException if a repository error occurs + */ + void legacyResolveSuccessors() throws RepositoryException { + InternalValue[] values = node.getPropertyValues(NameConstants.JCR_PREDECESSORS); + if (values != null) { + for (InternalValue value : values) { + InternalVersionImpl v = (InternalVersionImpl) + versionHistory.getVersion(value.getNodeId()); + // version may be null if history is broken + if (v != null) { + v.internalAddSuccessor(this, false); + } + } + } + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof InternalVersionImpl) { + InternalVersionImpl v = (InternalVersionImpl) obj; + return v.getId().equals(getId()); + } + return false; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return getId().hashCode(); + } + + /** + * Returns the activity of this version or null if not defined + * @return the activity or null + * @throws RepositoryException if an error occurs + */ + public InternalActivityImpl getActivity() throws RepositoryException { + if (node.hasProperty(NameConstants.JCR_ACTIVITY)) { + InternalValue value = node.getPropertyValue(NameConstants.JCR_ACTIVITY); + return (InternalActivityImpl) vMgr.getItem(value.getNodeId()); + } else { + return null; + } + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionItem.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionItem.java new file mode 100644 index 00000000000..1444b8fb539 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionItem.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.core.version; + +import org.apache.jackrabbit.core.id.NodeId; + +/** + * This interface defines the base for all internal versioning items. Internal + * versioning items are decoupled from their external form as exposed to the + * repository or in form of the node extensions {@link javax.jcr.version.Version} + * or {@link javax.jcr.version.VersionHistory}. + */ +public interface InternalVersionItem { + + /** + * Returns the id of this item. + * + * @return the id of this item. + */ + NodeId getId(); + + /** + * returns the parent version item or null. + * + * @return the parent version item. + */ + InternalVersionItem getParent(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionItemImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionItemImpl.java new file mode 100644 index 00000000000..02349729daa --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionItemImpl.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.core.version; + +import org.apache.jackrabbit.core.id.NodeId; + +/** + * Implements a InternalVersionItem. + */ +abstract class InternalVersionItemImpl implements InternalVersionItem { + + /** + * the underlying persistence node + */ + protected final NodeStateEx node; + + /** + * the version manager + */ + protected final InternalVersionManagerBase vMgr; + + /** + * Creates a new Internal version item impl + * + * @param vMgr + */ + protected InternalVersionItemImpl(InternalVersionManagerBase vMgr, NodeStateEx node) { + this.vMgr = vMgr; + this.node = node; + } + + /** + * Returns the persistent version manager for this item + * + * @return the version manager. + */ + protected InternalVersionManagerBase getVersionManager() { + return vMgr; + } + + /** + * Returns the id of this item + * + * @return the id of this item. + */ + public abstract NodeId getId(); + + /** + * returns the parent version item or null. + * + * @return the parent version item or null. + */ + public abstract InternalVersionItem getParent(); +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManager.java new file mode 100644 index 00000000000..fc003b848d3 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManager.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.core.version; + +import java.util.Calendar; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider; +import org.apache.jackrabbit.spi.Name; + +/** + * This interface defines the version manager. It gives access to the underlying + * persistence layer of the versioning. + */ +public interface InternalVersionManager { + + /** + * returns the virtual item state provider that exposes the internal versions + * as items. + * + * @return the virtual item state provider. + */ + VirtualItemStateProvider getVirtualItemStateProvider(); + + /** + * Returns information about the version history of the specified node. + * If the given node does not already have an associated version history, + * then an empty history is automatically created. This method should + * only be called by code that already knows that the specified node + * is versionable. + * + * @param session workspace session + * @param vNode node whose version history should be returned + * @param copiedFrom the node id for the jcr:copiedFrom property use for copied nodes + * @return identifiers of the version history and root version nodes + * @throws RepositoryException if an error occurs + */ + VersionHistoryInfo getVersionHistory(Session session, NodeState vNode, + NodeId copiedFrom) + throws RepositoryException; + + /** + * invokes the checkin() on the persistent version manager and remaps the + * newly created version objects. + * + * @param session session that invokes the checkin + * @param node node to checkin + * @param created create time of the new version, + * or null for the current time + * @return the newly created version + * @throws RepositoryException if an error occurs + */ + InternalVersion checkin(Session session, NodeStateEx node, Calendar created) + throws RepositoryException; + + /** + * invokes the checkout() on the persistent version manager. + * + * @param state node to checkout + * @param activityId node id if the current activity + * @return the base version id + * @throws RepositoryException if an error occurs + */ + NodeId canCheckout(NodeStateEx state, NodeId activityId) throws RepositoryException; + + /** + * Removes the specified version from the given version history. + * @param session the session that performs the remove + * @param history version history to remove the version from + * @param versionName name of the version + * @throws RepositoryException if an error occurs + */ + void removeVersion(Session session, InternalVersionHistory history, Name versionName) + throws RepositoryException; + + /** + * Removes the specified version history from storage. + * + * @param session the session that performs the remove + * @param history the version history to remove + * @throws RepositoryException if an error occurs + */ + void removeVersionHistory(Session session, InternalVersionHistory history) throws RepositoryException; + + /** + * Sets the version label to the given version. + * If the label is already assigned to another version, a VersionException is + * thrown unless move is true. If version + * is null, the label is removed from the respective version. + * In either case, the version the label was previously assigned is returned, + * or null of the label was not moved. + * + * @param session the session that performs the operation + * @param history version history + * @param version name of the version + * @param label new label + * @param move if true label will be moved + * @return the version that had the label or null + * @throws RepositoryException if an error occurs + */ + InternalVersion setVersionLabel(Session session, + InternalVersionHistory history, + Name version, Name label, + boolean move) + throws RepositoryException; + + /** + * Returns the version history with the given id + * + * @param id id of the version history + * @return the version history. + * @throws RepositoryException if an error occurs + */ + InternalVersionHistory getVersionHistory(NodeId id) + throws RepositoryException; + + /** + * Returns the version history for the versionable node with the given id. + * + * @param id id of the node to retrieve the version history for + * @return the version history + * @throws RepositoryException if an error occurs or the history does not exit + */ + InternalVersionHistory getVersionHistoryOfNode(NodeId id) + throws RepositoryException; + + /** + * Returns the version with the given id + * + * @param id id of the version to retrieve + * @return the version or null + * @throws RepositoryException if an error occurs + */ + InternalVersion getVersion(NodeId id) throws RepositoryException; + + /** + * Returns the baseline with the given id + * + * @param id id of the baseline version to retrieve + * @return the baseline or null if not found + * @throws RepositoryException if an error occurs + */ + InternalBaseline getBaseline(NodeId id) throws RepositoryException; + + /** + * Returns the activity with the given id + * + * @param id id of the activity to retrieve + * @return the activity. + * @throws RepositoryException if an error occurs + */ + InternalActivity getActivity(NodeId id) throws RepositoryException; + + /** + * Returns the head version of the node with the given id. this is always + * the last of all versions. this only works correctly for liner version + * graphs (i.e. simple versioning) + * + * @param id id of the node to retrieve the version for + * @return the version. + * @throws RepositoryException if an error occurs + */ + InternalVersion getHeadVersionOfNode(NodeId id) throws RepositoryException; + + /** + * Creates a new activity + * @param session the current session + * @param title title of the new activity + * @return the nodeid of the new activity + * @throws RepositoryException if an error occurs + */ + NodeId createActivity(Session session, String title) throws RepositoryException; + + /** + * Removes an activity and all + * @param session the current session + * @param nodeId id of the activity to remove + * @throws RepositoryException if an error occurs + */ + void removeActivity(Session session, NodeId nodeId) throws RepositoryException; + + /** + * Close this version manager. After having closed a persistence + * manager, further operations on this object are treated as illegal + * and throw + * + * @throws Exception if an error occurs + */ + void close() throws Exception; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManagerBase.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManagerBase.java new file mode 100755 index 00000000000..2f27af977a1 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManagerBase.java @@ -0,0 +1,907 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_ACTIVITY; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_ROOTVERSION; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_VERSIONHISTORY; +import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_VERSIONABLE; + +import java.util.Calendar; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.LocalItemStateManager; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base implementation of the {@link InternalVersionManager} interface. + *

    + * All read operations must acquire the read lock before reading, all write + * operations must acquire the write lock. + */ +abstract class InternalVersionManagerBase implements InternalVersionManager { + + /** + * Logger instance. + */ + private static Logger log = LoggerFactory.getLogger(InternalVersionManagerBase.class); + + /** + * State manager for the version storage. + */ + protected LocalItemStateManager stateMgr; + + /** + * Node type registry. + */ + protected final NodeTypeRegistry ntReg; + + protected final NodeId historiesId; + + protected final NodeId activitiesId; + + /** + * the lock on this version manager + */ + private final VersioningLock rwLock = new VersioningLock(); + + private final NodeIdFactory nodeIdFactory; + + protected InternalVersionManagerBase(NodeTypeRegistry ntReg, + NodeId historiesId, + NodeId activitiesId, + NodeIdFactory nodeIdFactory) { + this.ntReg = ntReg; + this.historiesId = historiesId; + this.activitiesId = activitiesId; + this.nodeIdFactory = nodeIdFactory; + } + +//-------------------------------------------------------< InternalVersionManager > + + /** + * {@inheritDoc} + */ + public InternalVersion getVersion(NodeId id) throws RepositoryException { + // lock handling via getItem() + InternalVersion v = (InternalVersion) getItem(id); + if (v == null) { + log.warn("Versioning item not found: " + id); + } + return v; + } + + /** + * {@inheritDoc} + */ + public InternalBaseline getBaseline(NodeId id) throws RepositoryException { + // lock handling via getItem() + InternalVersionItem item = getItem(id); + if (item == null) { + log.warn("Versioning item not found: " + id); + } else if (!(item instanceof InternalBaseline)) { + log.warn("Versioning item is not a baseline: " + id); + item = null; + } + return (InternalBaseline) item; + } + + /** + * {@inheritDoc} + */ + public InternalActivity getActivity(NodeId id) throws RepositoryException { + // lock handling via getItem() + InternalActivity v = (InternalActivity) getItem(id); + if (v == null) { + log.warn("Versioning item not found: " + id); + } + return v; + } + + /** + * {@inheritDoc} + */ + public InternalVersionHistory getVersionHistory(NodeId id) + throws RepositoryException { + // lock handling via getItem() + return (InternalVersionHistory) getItem(id); + } + + /** + * {@inheritDoc} + */ + public InternalVersionHistory getVersionHistoryOfNode(NodeId id) + throws RepositoryException { + VersioningLock.ReadLock lock = acquireReadLock(); + try { + String uuid = id.toString(); + Name name = getName(uuid); + + NodeStateEx parent = getParentNode(getHistoryRoot(), uuid, null); + if (parent != null && parent.hasNode(name)) { + NodeStateEx history = parent.getNode(name, 1); + if (history == null) { + throw new InconsistentVersioningState("Unexpected failure to get child node " + name + " on parent node" + parent.getNodeId()); + } + return getVersionHistory(history.getNodeId()); + } else { + throw new ItemNotFoundException("Version history of node " + id + " not found."); + } + } finally { + lock.release(); + } + } + + /** + * {@inheritDoc} + * + * Assumes that all versions are stored chronologically below the version + * history and just returns the last one. i.e. currently only works for + * simple versioning. + */ + public InternalVersion getHeadVersionOfNode(NodeId id) throws RepositoryException { + InternalVersionHistory vh = getVersionHistoryOfNode(id); + Name[] names = vh.getVersionNames(); + InternalVersion last = vh.getVersion(names[names.length - 1]); + return getVersion(last.getId()); + } + + //-------------------------------------------------------< implementation > + + /** + * Acquires the write lock on this version manager. + * @return returns the write lock + */ + protected VersioningLock.WriteLock acquireWriteLock() { + while (true) { + try { + return rwLock.acquireWriteLock(); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * acquires the read lock on this version manager. + * @return returns the read lock + */ + public VersioningLock.ReadLock acquireReadLock() { + while (true) { + try { + return rwLock.acquireReadLock(); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * returns the id of the version history root node + * + * @return the id of the version history root node + */ + abstract protected NodeStateEx getHistoryRoot(); + + /** + * returns the id of the activities root node + * + * @return the id of the activities root node + */ + abstract protected NodeStateEx getActivitiesRoot(); + + + /** + * Helper for managing write operations. + */ + private class WriteOperation { + + /** + * Flag for successful completion of the write operation. + */ + private boolean success = false; + + private final VersioningLock.WriteLock lock; + + public WriteOperation(VersioningLock.WriteLock lock) { + this.lock = lock; + } + + /** + * Saves the pending operations in the {@link LocalItemStateManager}. + * + * @throws ItemStateException if the pending state is invalid + * @throws RepositoryException if the pending state could not be persisted + */ + public void save() throws ItemStateException, RepositoryException { + stateMgr.update(); + success = true; + } + + /** + * Closes the write operation. The pending operations are canceled + * if they could not be properly saved. Finally the write lock is + * released. + */ + public void close() { + try { + if (!success) { + // update operation failed, cancel all modifications + stateMgr.cancel(); + } + } finally { + lock.release(); + } + } + } + + /** + * Starts a write operation by acquiring the write lock and setting the + * item state manager to the "edit" state. If something goes wrong, the + * write lock is released and an exception is thrown. + *

    + * The pattern for using this method and the returned helper instance is: + *

    +     *     WriteOperation operation = startWriteOperation();
    +     *     try {
    +     *         ...
    +     *         operation.save(); // if everything is OK
    +     *         ...
    +     *     } catch (...) {
    +     *         ...
    +     *     } finally {
    +     *         operation.close();
    +     *     }
    +     * 
    + * + * @return write operation helper + * @throws RepositoryException if the write operation could not be started + */ + private WriteOperation startWriteOperation() throws RepositoryException { + boolean success = false; + VersioningLock.WriteLock lock = acquireWriteLock(); + try { + stateMgr.edit(); + success = true; + return new WriteOperation(lock); + } catch (IllegalStateException e) { + throw new RepositoryException("Unable to start edit operation.", e); + } finally { + if (!success) { + lock.release(); + } + } + } + + /** + * Returns information about the version history of the specified node + * or null when unavailable. + * + * @param node node whose version history should be returned + * @return identifiers of the version history and root version nodes + * @throws RepositoryException if an error occurs + */ + public VersionHistoryInfo getVersionHistoryInfoForNode(NodeState node) throws RepositoryException { + VersionHistoryInfo info = null; + + VersioningLock.ReadLock lock = acquireReadLock(); + try { + String uuid = node.getNodeId().toString(); + Name name = getName(uuid); + + NodeStateEx parent = getParentNode(getHistoryRoot(), uuid, null); + if (parent != null && parent.hasNode(name)) { + NodeStateEx history = parent.getNode(name, 1); + if (history == null) { + throw new InconsistentVersioningState("Unexpected failure to get child node " + name + " on parent node " + parent.getNodeId()); + } + ChildNodeEntry rootv = history.getState().getChildNodeEntry(JCR_ROOTVERSION, 1); + if (rootv == null) { + throw new InconsistentVersioningState("missing child node entry for " + JCR_ROOTVERSION + " on version history node " + history.getNodeId(), + history.getNodeId(), null); + } + info = new VersionHistoryInfo(history.getNodeId(), + rootv.getId()); + } + } finally { + lock.release(); + } + + return info; + } + + /** + * {@inheritDoc} + */ + public VersionHistoryInfo getVersionHistory(Session session, NodeState node, + NodeId copiedFrom) + throws RepositoryException { + VersionHistoryInfo info = getVersionHistoryInfoForNode(node); + + if (info == null) { + info = createVersionHistory(session, node, copiedFrom); + } + + return info; + } + + /** + * Creates a new version history. This action is needed either when creating + * a new 'mix:versionable' node or when adding the 'mix:versionable' mixin + * to a node. + * + * @param session repository session + * @param node versionable node state + * @param copiedFrom node id for the jcr:copiedFrom property + * @return identifier of the new version history node + * @throws RepositoryException if an error occurs + * @see #getVersionHistory(Session, NodeState, NodeId) + */ + protected abstract VersionHistoryInfo createVersionHistory(Session session, + NodeState node, + NodeId copiedFrom) + throws RepositoryException; + + /** + * Returns the item with the given persistent id. Subclass responsibility. + *

    + * Please note, that the overridden method must acquire the readlock before + * reading the state manager. + * + * @param id the id of the item + * @return version item + * @throws RepositoryException if an error occurs + */ + protected abstract InternalVersionItem getItem(NodeId id) + throws RepositoryException; + + /** + * Return a flag indicating if the item specified exists. + * Subclass responsibility. + * @param id the id of the item + * @return true if the item exists; + * false otherwise + */ + protected abstract boolean hasItem(NodeId id); + + /** + * Checks if there are item references (from outside the version storage) + * that reference the given node. Subclass responsibility. + *

    + * Please note, that the overridden method must acquire the readlock before + * reading the state manager. + * + * @param id the id of the node + * @return true if there are item references from outside the + * version storage; false otherwise. + * @throws RepositoryException if an error occurs while reading from the + * repository. + */ + protected abstract boolean hasItemReferences(NodeId id) + throws RepositoryException; + + /** + * Returns the node with the given persistent id. Subclass responsibility. + *

    + * Please note, that the overridden method must acquire the readlock before + * reading the state manager. + * + * @param id the id of the node + * @throws RepositoryException if an error occurs while reading from the + * repository. + * @return the nodestate for the given id. + */ + protected abstract NodeStateEx getNodeStateEx(NodeId id) + throws RepositoryException; + + /** + * Creates a new Version History. + * + * @param node the node for which the version history is to be initialized + * @param copiedFrom node id for the jcr:copiedFrom parameter + * @return the identifiers of the newly created version history and root version + * @throws RepositoryException if an error occurs + */ + NodeStateEx internalCreateVersionHistory(NodeState node, NodeId copiedFrom) + throws RepositoryException { + WriteOperation operation = startWriteOperation(); + try { + // create deep path + String uuid = node.getNodeId().toString(); + NodeStateEx parent = getParentNode(getHistoryRoot(), uuid, NameConstants.REP_VERSIONSTORAGE); + Name name = getName(uuid); + if (parent.hasNode(name)) { + // already exists + return null; + } + + // create new history node in the persistent state + NodeStateEx history = + InternalVersionHistoryImpl.create(this, parent, name, node, copiedFrom); + + // end update + operation.save(); + + log.debug( + "Created new version history " + history.getNodeId() + + " for " + node + "."); + return history; + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + operation.close(); + } + } + + /** + * Creates a new activity. + * + * @param title title of the new activity + * @return the id of the newly created activity + * @throws RepositoryException if an error occurs + */ + NodeStateEx internalCreateActivity(String title) + throws RepositoryException { + WriteOperation operation = startWriteOperation(); + try { + // create deep path + NodeId activityId = nodeIdFactory.newNodeId(); + NodeStateEx parent = getParentNode(getActivitiesRoot(), activityId.toString(), NameConstants.REP_ACTIVITIES); + Name name = getName(activityId.toString()); + + // create new activity node in the persistent state + NodeStateEx pNode = InternalActivityImpl.create(parent, name, activityId, title); + + // end update + operation.save(); + + log.debug("Created new activity " + activityId + + " with title " + title + "."); + return pNode; + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + operation.close(); + } + } + + /** + * Removes the specified activity + * + * @param activity the activity to remove + * @throws javax.jcr.RepositoryException if any other error occurs. + */ + protected void internalRemoveActivity(InternalActivityImpl activity) + throws RepositoryException { + WriteOperation operation = startWriteOperation(); + try { + // check if the activity has any references in the workspaces + NodeId nodeId = activity.getId(); + if (stateMgr.hasNodeReferences(nodeId)) { + NodeReferences refs = stateMgr.getNodeReferences(nodeId); + if (refs.hasReferences()) { + throw new ReferentialIntegrityException("Unable to delete activity. still referenced."); + } + } + // TODO: + // check if the activity is used in anywhere in the version storage + // and reject removal + + // remove activity and possible empty parent directories + NodeStateEx act = getNodeStateEx(nodeId); + NodeId parentId = act.getParentId(); + Name name = act.getName(); + while (parentId != null) { + NodeStateEx parent = getNodeStateEx(parentId); + parent.removeNode(name); + parent.store(); + if (parent.getChildNodes().length == 0 && !parentId.equals(activitiesId)) { + name = parent.getName(); + parentId = parent.getParentId(); + } else { + parentId = null; + } + } + operation.save(); + } catch (ItemStateException e) { + log.error("Error while storing: " + e.toString()); + } finally { + operation.close(); + } + } + + /** + * Utility method that returns the given string as a name in the default + * namespace. + * + * @param name string name + * @return A Name object. + */ + protected static Name getName(String name) { + return NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, name); + } + + /** + * Utility method that returns the parent node under which the version + * history of the identified versionable node is or will be stored. If + * the interNT is not null then the returned + * parent node and any ancestor nodes are automatically created if they do + * not already exist. Otherwise + * null is returned if the parent node does not exist. + * + * @param parent the parent node + * @param uuid UUID of a versionable node + * @param interNT intermediate nodetype. + * @return parent node of the version history, or null + * @throws RepositoryException if an error occurs + */ + protected static NodeStateEx getParentNode(NodeStateEx parent, String uuid, Name interNT) + throws RepositoryException { + NodeStateEx n = parent; + for (int i = 0; i < 3; i++) { + Name name = getName(uuid.substring(i * 2, i * 2 + 2)); + NodeStateEx childn = n.getNode(name, 1); + if (childn != null) { + n = childn; + } else if (interNT != null) { + childn = n.addNode(name, interNT, null, false); + n.store(false); + childn.store(true); + n = childn; + } else { + return null; + } + } + return n; + } + + /** + * Creates a new version of the given node using the given version + * creation time. + * + * @param node the node to be checked in + * @param created version creation time + * @return the new version + * @throws RepositoryException if an error occurs + */ + protected InternalVersion checkin(NodeStateEx node, Calendar created) + throws RepositoryException { + WriteOperation operation = startWriteOperation(); + try { + boolean simple = + !node.getEffectiveNodeType().includesNodeType(MIX_VERSIONABLE); + InternalVersionHistoryImpl vh; + if (simple) { + // in simple versioning the history id needs to be calculated + vh = (InternalVersionHistoryImpl) getVersionHistoryOfNode( + node.getNodeId()); + } else { + // in full versioning, the history id can be retrieved via + // the property + vh = (InternalVersionHistoryImpl) getVersionHistory( + node.getPropertyValue(JCR_VERSIONHISTORY).getNodeId()); + } + + InternalVersion version = + internalCheckin(vh, node, simple, created); + + operation.save(); + return version; + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + operation.close(); + } + } + + /** + * Checks in a node + * + * @param history the version history + * @param node node to checkin + * @param simple flag indicates simple versioning + * @param created optional created date. + * @return internal version + * @throws javax.jcr.RepositoryException if an error occurs + * @see javax.jcr.Node#checkin() + */ + protected InternalVersion internalCheckin( + InternalVersionHistoryImpl history, + NodeStateEx node, boolean simple, Calendar created) + throws RepositoryException { + String versionName = calculateCheckinVersionName(history, node, simple); + InternalVersionImpl v = history.checkin( + NameFactoryImpl.getInstance().create("", versionName), + node, created); + + // check for jcr:activity + if (node.hasProperty(JCR_ACTIVITY)) { + NodeId actId = node.getPropertyValue(JCR_ACTIVITY).getNodeId(); + InternalActivityImpl act = (InternalActivityImpl) getItem(actId); + act.addVersion(v); + } + + return v; + } + + /** + * Calculates the name of the new version that will be created by a + * checkin call. The name is determined as follows: + *

      + *
    • first the predecessor version with the shortest name is searched. + *
    • if that predecessor version is the root version, the new version gets + * the name "{number of successors}+1" + ".0" + *
    • if that predecessor version has no successor, the last digit of it's + * version number is incremented. + *
    • if that predecessor version has successors but the incremented name + * does not exist, that name is used. + *
    • otherwise a ".0" is added to the name until a non conflicting name + * is found. + *
    + * + * Example Graph: + *
    +     * jcr:rootVersion
    +     *  |     |
    +     * 1.0   2.0
    +     *  |
    +     * 1.1
    +     *  |
    +     * 1.2 ---\  ------\
    +     *  |      \        \
    +     * 1.3   1.2.0   1.2.0.0
    +     *  |      |
    +     * 1.4   1.2.1 ----\
    +     *  |      |        \
    +     * 1.5   1.2.2   1.2.1.0
    +     *  |      |        |
    +     * 1.6     |     1.2.1.1
    +     *  |-----/
    +     * 1.7
    +     * 
    + * + * @param history the version history + * @param node the node to checkin + * @param simple if true indicates simple versioning + * @return the new version name + * @throws RepositoryException if an error occurs. + */ + protected String calculateCheckinVersionName(InternalVersionHistoryImpl history, + NodeStateEx node, boolean simple) + throws RepositoryException { + + if (history == null) { + String message = "Node " + node.getNodeId() + " has no version history"; + log.error(message); + throw new VersionException(message); + } + + InternalVersion best = null; + if (simple) { + // 1. in simple versioning just take the 'head' version + Name[] names = history.getVersionNames(); + best = history.getVersion(names[names.length - 1]); + } else { + // 1. search a predecessor, suitable for generating the new name + InternalValue[] values = node.getPropertyValues(NameConstants.JCR_PREDECESSORS); + + if (values == null || values.length == 0) { + String message; + if (values == null) { + message = "Mandatory jcr:predecessors property missing on node " + node.getNodeId(); + } else { + message = "Mandatory jcr:predecessors property is empty on node " + node.getNodeId(); + } + log.error(message); + throw new VersionException(message); + } + + for (InternalValue value: values) { + InternalVersion pred = history.getVersion(value.getNodeId()); + if (pred == null) { + String message = "Could not instantiate InternalVersion for nodeId " + value.getNodeId() + " (VHR + " + history.getId() + ", node " + node.getNodeId() + ")"; + log.error(message); + throw new VersionException(message); + } + if (best == null + || pred.getName().getLocalName().length() < best.getName().getLocalName().length()) { + best = pred; + } + } + } + + if (best == null) { + String message = "Could not find 'best' predecessor node for " + node.getNodeId(); + log.error(message); + throw new VersionException(message); + } + + // 2. generate version name (assume no namespaces in version names) + String versionName = best.getName().getLocalName(); + int pos = versionName.lastIndexOf('.'); + if (pos > 0) { + String newVersionName = versionName.substring(0, pos + 1) + + (Integer.parseInt(versionName.substring(pos + 1)) + 1); + while (history.hasVersion(NameFactoryImpl.getInstance().create("", newVersionName))) { + versionName += ".0"; + newVersionName = versionName; + } + return newVersionName; + } else { + // best is root version + return String.valueOf(best.getSuccessors().size() + 1) + ".0"; + } + } + + /** + * Removes the specified version from the history + * + * @param history the version history from where to remove the version. + * @param name the name of the version to remove. + * @throws javax.jcr.version.VersionException if the version history does + * not have a version with name. + * @throws javax.jcr.RepositoryException if any other error occurs. + */ + protected void internalRemoveVersion(InternalVersionHistoryImpl history, Name name) + throws VersionException, RepositoryException { + WriteOperation operation = startWriteOperation(); + try { + history.removeVersion(name); + operation.save(); + } catch (ItemStateException e) { + log.error("Error while storing: " + e.toString()); + } finally { + operation.close(); + } + } + + /** + * Removes the specified history from the storage + * + * @param history the version history to remove + * @throws VersionException + * @throws RepositoryException + */ + public void internalRemoveVersionHistory(InternalVersionHistoryImpl history) + throws VersionException, RepositoryException { + String versionableUuid = history.getVersionableId().toString(); + WriteOperation operation = startWriteOperation(); + try { + NodeStateEx parent = getParentNode(getHistoryRoot(), versionableUuid, null); + parent.removeNode(history.node.getName()); + parent.store(); + operation.save(); + } catch (ItemStateException e) { + log.error("Error while storing: " + e.toString()); + } finally { + operation.close(); + } + } + + /** + * Set version label on the specified version. + * + * @param history version history + * @param version version name + * @param label version label + * @param move true to move from existing version; + * false otherwise. + * @return The internal version. + * @throws RepositoryException if an error occurs + */ + protected InternalVersion setVersionLabel(InternalVersionHistoryImpl history, + Name version, Name label, + boolean move) + throws RepositoryException { + WriteOperation operation = startWriteOperation(); + try { + InternalVersion v = history.setVersionLabel(version, label, move); + operation.save(); + return v; + } catch (ItemStateException e) { + log.error("Error while storing: " + e.toString()); + return null; + } finally { + operation.close(); + } + } + + /** + * Invoked when a new internal item has been created. + * @param version internal version item + */ + protected void versionCreated(InternalVersion version) { + } + + /** + * Invoked when a new internal item has been destroyed. + * @param version internal version item + */ + protected void versionDestroyed(InternalVersion version) { + } + + /** + * Invoked by the internal version item itself, when it's underlying + * persistence state was discarded. + * + * @param item item that was discarded + */ + protected void itemDiscarded(InternalVersionItem item) { + } + + /** + * Creates an {@link InternalVersionItem} based on the {@link NodeState} + * identified by id. + * + * @param id the node id of the version item. + * @return the version item or null if there is no node state + * with the given id. + * @throws RepositoryException if an error occurs while reading from the + * version storage. + */ + protected InternalVersionItem createInternalVersionItem(NodeId id) + throws RepositoryException { + try { + if (stateMgr.hasItemState(id)) { + NodeState state = (NodeState) stateMgr.getItemState(id); + NodeStateEx pNode = new NodeStateEx(stateMgr, ntReg, state, null); + NodeId parentId = pNode.getParentId(); + InternalVersionItem parent = getItem(parentId); + Name ntName = state.getNodeTypeName(); + if (ntName.equals(NameConstants.NT_FROZENNODE)) { + return new InternalFrozenNodeImpl(this, pNode, parent); + } else if (ntName.equals(NameConstants.NT_VERSIONEDCHILD)) { + return new InternalFrozenVHImpl(this, pNode, parent); + } else if (ntName.equals(NameConstants.NT_VERSION)) { + return ((InternalVersionHistory) parent).getVersion(id); + } else if (ntName.equals(NameConstants.NT_VERSIONHISTORY)) { + return new InternalVersionHistoryImpl(this, pNode); + } else if (ntName.equals(NameConstants.NT_ACTIVITY)) { + return new InternalActivityImpl(this, pNode); + } else { + return null; + } + } else { + return null; + } + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + public NodeIdFactory getNodeIdFactory() { + return nodeIdFactory; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManagerImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManagerImpl.java new file mode 100644 index 00000000000..b137448755c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalVersionManagerImpl.java @@ -0,0 +1,750 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.ActivityViolationException; +import javax.jcr.version.VersionException; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.cluster.UpdateEventChannel; +import org.apache.jackrabbit.core.cluster.UpdateEventListener; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.observation.DelegatingObservationDispatcher; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.observation.EventStateCollection; +import org.apache.jackrabbit.core.observation.EventStateCollectionFactory; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ISMLocking; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateCacheFactory; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateListener; +import org.apache.jackrabbit.core.state.LocalItemStateManager; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.SharedItemStateManager; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This Class implements a VersionManager. + */ +public class InternalVersionManagerImpl extends InternalVersionManagerBase + implements ItemStateListener, UpdateEventListener { + + /** + * the default logger + */ + private static Logger log = LoggerFactory.getLogger(InternalVersionManagerImpl.class); + + /** + * The path of the jcr:system node: /jcr:system + */ + private static final Path SYSTEM_PATH; + + static { + PathFactory factory = PathFactoryImpl.getInstance(); + SYSTEM_PATH = factory.getRootPath().resolve( + factory.createElement(NameConstants.JCR_SYSTEM)); + } + + /** + * The persistence manager for the versions + */ + private final PersistenceManager pMgr; + + /** + * The file system for this version manager + */ + private final FileSystem fs; + + /** + * the version state manager for the version storage + */ + private VersionItemStateManager sharedStateMgr; + + /** + * the virtual item state provider that exposes the version storage + */ + private final VersionItemStateProvider versProvider; + + /** + * the dynamic event state collection factory + */ + private final DynamicESCFactory escFactory; + + /** + * Persistent root node of the version histories. + */ + private final NodeStateEx historyRoot; + + /** + * Persistent root node of the activities. + */ + private final NodeStateEx activitiesRoot; + + /** + * Map of returned items. this is kept for invalidating + */ + @SuppressWarnings("unchecked") + private final Map versionItems = + new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK); + + /** + * Creates a new internal version manager + * + * @param pMgr underlying persistence manager + * @param fs workspace file system + * @param ntReg node type registry + * @param obsMgr observation manager + * @param systemId node id of the version storage parent (i.e. jcr:system) + * @param historiesId node id of the version storage (i.e. jcr:versionStorage) + * @param activitiesId node id of the activities storage (i.e. jcr:activities) + * @param cacheFactory item state cache factory + * @param ismLocking workspace item state locking + * @throws RepositoryException if an error occurs + */ + public InternalVersionManagerImpl(PersistenceManager pMgr, FileSystem fs, + NodeTypeRegistry ntReg, + DelegatingObservationDispatcher obsMgr, + NodeId systemId, + NodeId historiesId, + NodeId activitiesId, + ItemStateCacheFactory cacheFactory, + ISMLocking ismLocking, + NodeIdFactory nodeIdFactory) throws RepositoryException { + super(ntReg, historiesId, activitiesId, nodeIdFactory); + try { + this.pMgr = pMgr; + this.fs = fs; + this.escFactory = new DynamicESCFactory(obsMgr); + + // need to store the version storage root directly into the persistence manager + if (!pMgr.exists(historiesId)) { + NodeState root = pMgr.createNew(historiesId); + root.setParentId(systemId); + root.setNodeTypeName(NameConstants.REP_VERSIONSTORAGE); + PropertyState pt = pMgr.createNew(new PropertyId(historiesId, NameConstants.JCR_PRIMARYTYPE)); + pt.setMultiValued(false); + pt.setType(PropertyType.NAME); + pt.setValues(new InternalValue[]{InternalValue.create(NameConstants.REP_VERSIONSTORAGE)}); + root.addPropertyName(pt.getName()); + ChangeLog cl = new ChangeLog(); + cl.added(root); + cl.added(pt); + pMgr.store(cl); + } + + // check for jcr:activities + if (!pMgr.exists(activitiesId)) { + NodeState root = pMgr.createNew(activitiesId); + root.setParentId(systemId); + root.setNodeTypeName(NameConstants.REP_ACTIVITIES); + PropertyState pt = pMgr.createNew(new PropertyId(activitiesId, NameConstants.JCR_PRIMARYTYPE)); + pt.setMultiValued(false); + pt.setType(PropertyType.NAME); + pt.setValues(new InternalValue[]{InternalValue.create(NameConstants.REP_ACTIVITIES)}); + root.addPropertyName(pt.getName()); + ChangeLog cl = new ChangeLog(); + cl.added(root); + cl.added(pt); + pMgr.store(cl); + } + + sharedStateMgr = createItemStateManager(pMgr, systemId, ntReg, cacheFactory, ismLocking, nodeIdFactory); + + stateMgr = LocalItemStateManager.createInstance(sharedStateMgr, escFactory, cacheFactory); + stateMgr.addListener(this); + + NodeState nodeState = (NodeState) stateMgr.getItemState(historiesId); + historyRoot = new NodeStateEx(stateMgr, ntReg, nodeState, NameConstants.JCR_VERSIONSTORAGE); + + nodeState = (NodeState) stateMgr.getItemState(activitiesId); + activitiesRoot = new NodeStateEx(stateMgr, ntReg, nodeState, NameConstants.JCR_ACTIVITIES); + + // create the virtual item state provider + versProvider = new VersionItemStateProvider(historiesId, activitiesId, sharedStateMgr); + + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * {@inheritDoc} + */ + public VirtualItemStateProvider getVirtualItemStateProvider() { + return versProvider; + } + + /** + * Return the persistence manager. + * + * @return the persistence manager + */ + public PersistenceManager getPersistenceManager() { + return pMgr; + } + + /** + * {@inheritDoc} + */ + public void close() throws Exception { + pMgr.close(); + fs.close(); + } + + /** + * Returns the event state collection factory. + * @return the event state collection factory. + */ + public DynamicESCFactory getEscFactory() { + return escFactory; + } + + /** + * {@inheritDoc} + *

    + * This method must not be synchronized since it could cause deadlocks with + * item-reading listeners in the observation thread. + */ + @Override + protected VersionHistoryInfo createVersionHistory(Session session, + final NodeState node, final NodeId copiedFrom) + throws RepositoryException { + NodeStateEx state = (NodeStateEx) + escFactory.doSourced((SessionImpl) session, new SourcedTarget() { + public Object run() throws RepositoryException { + return internalCreateVersionHistory(node, copiedFrom); + } + }); + + if (state == null) { + throw new InvalidItemStateException( + "History already exists for node " + node.getNodeId()); + } + Name root = NameConstants.JCR_ROOTVERSION; + return new VersionHistoryInfo( + state.getNodeId(), + state.getState().getChildNodeEntry(root, 1).getId()); + } + + /** + * {@inheritDoc} + *

    + * This method must not be synchronized since it could cause deadlocks with + * item-reading listeners in the observation thread. + */ + public NodeId createActivity(Session session, final String title) throws RepositoryException { + NodeStateEx state = (NodeStateEx) + escFactory.doSourced((SessionImpl) session, new SourcedTarget() { + public Object run() throws RepositoryException { + return internalCreateActivity(title); + } + }); + return state.getNodeId(); + } + + /** + * {@inheritDoc} + *

    + * This method must not be synchronized since it could cause deadlocks with + * item-reading listeners in the observation thread. + */ + public void removeActivity(Session session, final NodeId nodeId) + throws RepositoryException { + escFactory.doSourced((SessionImpl) session, new SourcedTarget() { + public Object run() throws RepositoryException { + InternalActivityImpl act = (InternalActivityImpl) getItem(nodeId); + internalRemoveActivity(act); + return null; + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hasItem(NodeId id) { + VersioningLock.ReadLock lock = acquireReadLock(); + try { + return stateMgr.hasItemState(id); + } finally { + lock.release(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected InternalVersionItem getItem(NodeId id) + throws RepositoryException { + + if (id.equals(historiesId)) { + return null; + } + if (id.equals(activitiesId)) { + return null; + } + VersioningLock.ReadLock lock = acquireReadLock(); + try { + synchronized (versionItems) { + InternalVersionItem item = versionItems.get(id); + if (item == null) { + item = createInternalVersionItem(id); + if (item != null) { + versionItems.put(id, item); + } else { + return null; + } + } + return item; + } + } finally { + lock.release(); + } + } + /** + * {@inheritDoc} + * + * this method currently does no modifications to the persistence and just + * checks if the checkout is valid in respect to a possible activity set on + * the session + */ + public NodeId canCheckout(NodeStateEx state, NodeId activityId) throws RepositoryException { + NodeId baseId = state.getPropertyValue(NameConstants.JCR_BASEVERSION).getNodeId(); + if (activityId != null) { + // If there exists another workspace with node N' where N' also has version + // history H, N' is checked out and the jcr:activity property of N' + // references A, then the checkout fails with an + // ActivityViolationException indicating which workspace currently has + // the checkout. + + // we're currently leverage the fact, that only references to "real" + // workspaces are recorded. so check all references if their sources + // exist in 'this' workspace + if (stateMgr.hasNodeReferences(activityId)) { + try { + NodeReferences refs = stateMgr.getNodeReferences(activityId); + for (PropertyId id: refs.getReferences()) { + if (!state.hasNode(id.getParentId())) { + throw new ActivityViolationException("Unable to checkout. " + + "Activity is already used for the same node in " + + "another workspace."); + } + } + } catch (ItemStateException e) { + throw new RepositoryException("Error during checkout.", e); + } + } + + // If there is a version in H that is not an eventual predecessor of N but + // whose jcr:activity references A, then the checkout fails with an + // ActivityViolationException + InternalActivityImpl a = (InternalActivityImpl) getItem(activityId); + NodeId historyId = state.getPropertyValue(NameConstants.JCR_VERSIONHISTORY).getNodeId(); + InternalVersionHistory history = (InternalVersionHistory) getItem(historyId); + InternalVersion version = a.getLatestVersion(history); + if (version != null) { + InternalVersion baseVersion = (InternalVersion) getItem(baseId); + while (baseVersion != null && !baseVersion.getId().equals(version.getId())) { + baseVersion = baseVersion.getLinearPredecessor(); + } + if (baseVersion == null) { + throw new ActivityViolationException("Unable to checkout. " + + "Activity is used by another version on a different branch: " + version.getName()); + } + } + } + return baseId; + } + + /** + * {@inheritDoc} + *

    + * This method must not be synchronized since it could cause deadlocks with + * item-reading listeners in the observation thread. + */ + public InternalVersion checkin( + Session session, final NodeStateEx node, final Calendar created) + throws RepositoryException { + return (InternalVersion) escFactory.doSourced( + (SessionImpl) session, + new SourcedTarget() { + public Object run() throws RepositoryException { + return checkin(node, created); + } + }); + } + + /** + * {@inheritDoc} + *

    + * This method must not be synchronized since it could cause deadlocks with + * item-reading listeners in the observation thread. + */ + public void removeVersion(Session session, + final InternalVersionHistory history, + final Name name) + throws VersionException, RepositoryException { + + if (!history.hasVersion(name)) { + throw new VersionException("Version with name " + name.toString() + + " does not exist in this VersionHistory"); + } + + escFactory.doSourced((SessionImpl) session, new SourcedTarget() { + public Object run() throws RepositoryException { + internalRemoveVersion((InternalVersionHistoryImpl) history, name); + return null; + } + }); + } + + /** + * {@inheritDoc} + *

    + * This method must not be synchronized since it could cause deadlocks with + * item-reading listeners in the observation thread. + */ + public void removeVersionHistory(Session session, final InternalVersionHistory history) throws RepositoryException { + escFactory.doSourced((SessionImpl) session, new SourcedTarget() { + public Object run() throws RepositoryException { + internalRemoveVersionHistory((InternalVersionHistoryImpl) history); + return null; + } + }); + } + + /** + * {@inheritDoc} + *

    + * This method must not be synchronized since it could cause deadlocks with + * item-reading listeners in the observation thread. + */ + public InternalVersion setVersionLabel(Session session, + final InternalVersionHistory history, + final Name version, final Name label, + final boolean move) + throws RepositoryException { + + return (InternalVersion) + escFactory.doSourced((SessionImpl) session, new SourcedTarget() { + public Object run() throws RepositoryException { + return setVersionLabel((InternalVersionHistoryImpl) history, version, label, move); + } + }); + } + + /** + * Invoked by some external source to indicate that some items in the + * versions tree were updated. Version histories are reloaded if possible. + * Matching items are removed from the cache. + * + * @param items items updated + */ + public void itemsUpdated(Collection items) { + VersioningLock.ReadLock lock = acquireReadLock(); + try { + synchronized (versionItems) { + for (InternalVersionItem item : items) { + InternalVersionItem cached = versionItems.remove(item.getId()); + if (cached != null) { + if (cached instanceof InternalVersionHistoryImpl) { + InternalVersionHistoryImpl vh = (InternalVersionHistoryImpl) cached; + try { + vh.reload(); + versionItems.put(vh.getId(), vh); + } catch (RepositoryException e) { + log.warn("Unable to update version history: " + e.toString()); + } + } + } + } + } + } finally { + lock.release(); + } + } + + /** + * Set an event channel to inform about updates. + * + * @param eventChannel event channel + */ + public void setEventChannel(UpdateEventChannel eventChannel) { + sharedStateMgr.setEventChannel(eventChannel); + eventChannel.setListener(this); + } + + /** + * {@inheritDoc} + */ + @Override + protected void itemDiscarded(InternalVersionItem item) { + // evict removed item from cache + VersioningLock.ReadLock lock = acquireReadLock(); + try { + versionItems.remove(item.getId()); + } finally { + lock.release(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean hasItemReferences(NodeId id) + throws RepositoryException { + return stateMgr.hasNodeReferences(id); + } + + /** + * {@inheritDoc} + */ + @Override + protected NodeStateEx getNodeStateEx(NodeId parentNodeId) + throws RepositoryException { + try { + NodeState state = (NodeState) stateMgr.getItemState(parentNodeId); + return new NodeStateEx(stateMgr, ntReg, state, null); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * returns the version history root node + * + * @return the version history root node + */ + @Override + protected NodeStateEx getHistoryRoot() { + return historyRoot; + } + + /** + * returns the activities root node + * + * @return the activities root node + */ + @Override + protected NodeStateEx getActivitiesRoot() { + return activitiesRoot; + } + + /** + * Returns the shared item state manager. + * @return the shared item state manager. + */ + protected SharedItemStateManager getSharedStateMgr() { + return sharedStateMgr; + } + + /** + * Creates a VersionItemStateManager or derivative. + * + * @param pMgr persistence manager + * @param rootId root node id + * @param ntReg node type registry + * @param cacheFactory cache factory + * @param ismLocking the ISM locking implementation + * @return item state manager + * @throws ItemStateException if an error occurs + */ + protected VersionItemStateManager createItemStateManager(PersistenceManager pMgr, + NodeId rootId, + NodeTypeRegistry ntReg, + ItemStateCacheFactory cacheFactory, + ISMLocking ismLocking, + NodeIdFactory nodeIdFactory) + throws ItemStateException { + return new VersionItemStateManager(pMgr, rootId, ntReg, cacheFactory, ismLocking, nodeIdFactory); + } + + /** + * {@inheritDoc} + *

    + * Not used. + */ + public void stateCreated(ItemState created) { + } + + /** + * {@inheritDoc} + *

    + * Not used. + */ + public void stateModified(ItemState modified) { + } + + /** + * {@inheritDoc} + *

    + * Remove item from cache on removal. + */ + public void stateDestroyed(ItemState destroyed) { + // evict removed item from cache + VersioningLock.ReadLock lock = acquireReadLock(); + try { + versionItems.remove(destroyed.getId()); + } finally { + lock.release(); + } + } + + /** + * {@inheritDoc} + *

    + * Not used. + */ + public void stateDiscarded(ItemState discarded) { + } + + //--------------------------------------------------< UpdateEventListener > + + /** + * {@inheritDoc} + */ + public void externalUpdate(ChangeLog changes, List events, + long timestamp, String userData) + throws RepositoryException { + EventStateCollection esc = getEscFactory().createEventStateCollection(null); + esc.addAll(events); + esc.setTimestamp(timestamp); + esc.setUserData(userData); + + sharedStateMgr.externalUpdate(changes, esc); + + Collection items = + new ArrayList(); + synchronized (versionItems) { + for (Map.Entry entry : versionItems + .entrySet()) { + if (changes.has(entry.getKey())) { + items.add(entry.getValue()); + } + } + } + itemsUpdated(items); + } + + //--------------------------------------------------------< inner classes > + + public static final class DynamicESCFactory implements EventStateCollectionFactory { + + /** + * the observation manager + */ + private final DelegatingObservationDispatcher obsMgr; + + /** + * The event source of the current thread. + */ + private final ThreadLocal source = + new ThreadLocal(); + + /** + * Creates a new event state collection factory + * @param obsMgr dispatcher + */ + public DynamicESCFactory(DelegatingObservationDispatcher obsMgr) { + this.obsMgr = obsMgr; + } + + /** + * {@inheritDoc} + *

    + * This object uses one instance of a LocalItemStateManager + * to update data on behalf of many sessions. In order to maintain the + * association between update operation and session who actually invoked + * the update, an internal event source is used. + */ + public EventStateCollection createEventStateCollection() + throws RepositoryException { + SessionImpl session = source.get(); + if (session != null) { + return createEventStateCollection(session); + } else { + throw new RepositoryException("Unknown event source."); + } + } + + /** + * {@inheritDoc} + *

    + * This object uses one instance of a LocalItemStateManager + * to update data on behalf of many sessions. In order to maintain the + * association between update operation and session who actually invoked + * the update, an internal event source is used. + */ + public EventStateCollection createEventStateCollection(SessionImpl source) { + return obsMgr.createEventStateCollection(source, SYSTEM_PATH); + } + + /** + * Executes the given runnable using the given event source. + * + * @param eventSource event source + * @param runnable the runnable to execute + * @return the return value of the executed runnable + * @throws RepositoryException if an error occurs + */ + public Object doSourced(SessionImpl eventSource, SourcedTarget runnable) + throws RepositoryException { + source.set(eventSource); + try { + return runnable.run(); + } finally { + source.remove(); + } + } + + } + + private abstract class SourcedTarget { + public abstract Object run() throws RepositoryException; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalXAVersionManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalXAVersionManager.java new file mode 100755 index 00000000000..e13d3c1c5fa --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/InternalXAVersionManager.java @@ -0,0 +1,791 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.observation.EventStateCollection; +import org.apache.jackrabbit.core.observation.EventStateCollectionFactory; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateCacheFactory; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateListener; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.XAItemStateManager; +import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider; +import org.apache.jackrabbit.core.virtual.VirtualNodeState; +import org.apache.jackrabbit.core.virtual.VirtualPropertyState; +import org.apache.jackrabbit.data.core.InternalXAResource; +import org.apache.jackrabbit.data.core.TransactionContext; +import org.apache.jackrabbit.data.core.TransactionException; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * Implementation of a {@link InternalVersionManager} that works in an XA environment. + * Works as a filter between a version manager client and the global version + * manager. + */ +public class InternalXAVersionManager extends InternalVersionManagerBase + implements EventStateCollectionFactory, VirtualItemStateProvider, InternalXAResource { + + /** + * Attribute name for associated change log. + */ + private static final String CHANGE_LOG_ATTRIBUTE_NAME = "XAVersionManager.ChangeLog"; + + /** + * Attribute name for items. + */ + private static final String ITEMS_ATTRIBUTE_NAME = "VersionItems"; + + /** + * Repository version manager. + */ + private final InternalVersionManagerImpl vMgr; + + /** + * The session that uses this version manager. + */ + private SessionImpl session; + + /** + * Items that have been modified and are part of the XA environment. + */ + private Map xaItems; + + /** + * flag that indicates if the version manager was locked during prepare + */ + private boolean vmgrLocked = false; + + /** + * The global write lock on the version manager. + */ + private VersioningLock.WriteLock vmgrLock; + + /** + * Persistent root node of the version histories. + */ + private final NodeStateEx historyRoot; + + /** + * Persistent root node of the activities. + */ + private final NodeStateEx activitiesRoot; + + /** + * Creates a new instance of this class. + * + * @param vMgr the underlying version manager + * @param ntReg node type registry + * @param session the session + * @param cacheFactory cache factory + * @throws RepositoryException if a an error occurs + */ + public InternalXAVersionManager(InternalVersionManagerImpl vMgr, NodeTypeRegistry ntReg, + SessionImpl session, ItemStateCacheFactory cacheFactory) + throws RepositoryException { + super(ntReg, vMgr.historiesId, vMgr.activitiesId, vMgr.getNodeIdFactory()); + this.vMgr = vMgr; + this.session = session; + this.stateMgr = XAItemStateManager.createInstance(vMgr.getSharedStateMgr(), + this, CHANGE_LOG_ATTRIBUTE_NAME, cacheFactory); + + NodeState state; + try { + state = (NodeState) stateMgr.getItemState(historiesId); + } catch (ItemStateException e) { + throw new RepositoryException("Unable to retrieve history root", e); + } + this.historyRoot = new NodeStateEx(stateMgr, ntReg, state, NameConstants.JCR_VERSIONSTORAGE); + try { + state = (NodeState) stateMgr.getItemState(activitiesId); + } catch (ItemStateException e) { + throw new RepositoryException("Unable to retrieve activities root", e); + } + this.activitiesRoot = new NodeStateEx(stateMgr, ntReg, state, NameConstants.JCR_ACTIVITIES); + } + + //------------------------------------------< EventStateCollectionFactory > + + /** + * {@inheritDoc} + */ + public EventStateCollection createEventStateCollection() + throws RepositoryException { + return vMgr.getEscFactory().createEventStateCollection(session); + } + + //-------------------------------------------------------< InternalVersionManager > + + /** + * {@inheritDoc} + */ + public VirtualItemStateProvider getVirtualItemStateProvider() { + return this; + } + + /** + * {@inheritDoc} + */ + @Override + protected VersionHistoryInfo createVersionHistory(Session session, + NodeState node, + NodeId copiedFrom) + throws RepositoryException { + + if (isInXA()) { + NodeStateEx state = internalCreateVersionHistory(node, copiedFrom); + InternalVersionHistory history = + new InternalVersionHistoryImpl(vMgr, state); + xaItems.put(state.getNodeId(), history); + Name root = NameConstants.JCR_ROOTVERSION; + return new VersionHistoryInfo( + state.getNodeId(), + state.getState().getChildNodeEntry(root, 1).getId()); + } + return vMgr.createVersionHistory(session, node, copiedFrom); + } + + /** + * {@inheritDoc} + */ + public NodeId createActivity(Session session, String title) + throws RepositoryException { + if (isInXA()) { + NodeStateEx state = internalCreateActivity(title); + InternalActivityImpl activity = + new InternalActivityImpl(vMgr, state); + xaItems.put(state.getNodeId(), activity); + return state.getNodeId(); + } else { + return vMgr.createActivity(session, title); + } + } + + /** + * {@inheritDoc} + */ + public void removeActivity(Session session, NodeId nodeId) + throws RepositoryException { + + if (isInXA()) { + InternalActivityImpl act = (InternalActivityImpl) getItem(nodeId); + internalRemoveActivity(act); + } + vMgr.removeActivity(session, nodeId); + } + + /** + * {@inheritDoc} + *

    + * Before modifying activity, make a local copy of it. + */ + @Override + protected void internalRemoveActivity(InternalActivityImpl activity) + throws VersionException, RepositoryException { + if (activity.getVersionManager() != this) { + activity = makeLocalCopy(activity); + xaItems.put(activity.getId(), activity); + } + super.internalRemoveActivity(activity); + } + + /** + * {@inheritDoc} + */ + public NodeId canCheckout(NodeStateEx state, NodeId activityId) throws RepositoryException { + return vMgr.canCheckout(state, activityId); + } + + /** + * {@inheritDoc} + */ + public InternalVersion checkin( + Session session, NodeStateEx node, Calendar created) + throws RepositoryException { + if (isInXA()) { + return checkin(node, created); + } else { + return vMgr.checkin(session, node, created); + } + } + + /** + * {@inheritDoc} + */ + public void removeVersion(Session session, InternalVersionHistory history, + Name versionName) + throws RepositoryException { + if (isInXA()) { + internalRemoveVersion((InternalVersionHistoryImpl) history, versionName); + } else { + vMgr.removeVersion(session, history, versionName); + } + } + + /** + * {@inheritDoc} + */ + public void removeVersionHistory(Session session, InternalVersionHistory history) + throws RepositoryException { + if (isInXA()) { + internalRemoveVersionHistory((InternalVersionHistoryImpl) history); + } else { + vMgr.removeVersionHistory(session, history); + } + } + + /** + * {@inheritDoc} + */ + public InternalVersion setVersionLabel(Session session, + InternalVersionHistory history, + Name version, + Name label, boolean move) + throws RepositoryException { + + if (isInXA()) { + return setVersionLabel((InternalVersionHistoryImpl) history, + version, label, move); + } else { + return vMgr.setVersionLabel(session, history, version, label, move); + } + } + + /** + * {@inheritDoc} + */ + public void close() throws Exception { + stateMgr.dispose(); + } + + //---------------------------------------------< VirtualItemStateProvider > + + /** + * {@inheritDoc} + */ + public boolean isVirtualRoot(ItemId id) { + return false; + } + + /** + * {@inheritDoc} + */ + public NodeId getVirtualRootId() { + // never used + return null; + } + + public NodeId[] getVirtualRootIds() { + // never used + return null; + } + + /** + * {@inheritDoc} + */ + public VirtualPropertyState createPropertyState(VirtualNodeState parent, + Name name, int type, + boolean multiValued) + throws RepositoryException { + + throw new IllegalStateException("Read-only"); + } + + /** + * {@inheritDoc} + */ + public VirtualNodeState createNodeState(VirtualNodeState parent, Name name, + NodeId id, Name nodeTypeName) + throws RepositoryException { + + throw new IllegalStateException("Read-only"); + } + + /** + * {@inheritDoc} + */ + public boolean setNodeReferences(ChangeLog references) { + ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog(); + if (changeLog != null) { + for (NodeReferences refs : references.modifiedRefs()) { + changeLog.modified(refs); + } + return true; + } else { + return false; + } + } + + /** + * {@inheritDoc} + *

    + * Return item states for changes only. Global version manager will return + * other items. + */ + public ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog(); + if (changeLog != null) { + return changeLog.get(id); + } + throw new NoSuchItemStateException("State not in change log: " + id); + } + + /** + * {@inheritDoc} + */ + public boolean hasItemState(ItemId id) { + ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog(); + if (changeLog != null) { + return changeLog.has(id); + } + return false; + } + + /** + * {@inheritDoc} + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + + ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog(); + if (changeLog != null) { + return changeLog.getReferencesTo(id); + } + return null; + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + ChangeLog changeLog = ((XAItemStateManager) stateMgr).getChangeLog(); + if (changeLog != null) { + return changeLog.getReferencesTo(id) != null; + } + return false; + } + + /** + * {@inheritDoc} + *

    + * Not needed. + */ + public void addListener(ItemStateListener listener) { + } + + /** + * {@inheritDoc} + *

    + * Not needed. + */ + public void removeListener(ItemStateListener listener) { + } + + //-----------------------------------------------< InternalVersionManagerBase > + + /** + * {@inheritDoc} + */ + @Override + protected NodeStateEx getHistoryRoot() { + return historyRoot; + } + + /** + * {@inheritDoc} + */ + @Override + protected NodeStateEx getActivitiesRoot() { + return activitiesRoot; + } + + /** + * {@inheritDoc} + */ + @Override + protected InternalVersionItem getItem(NodeId id) throws RepositoryException { + InternalVersionItem item = null; + if (xaItems != null) { + item = xaItems.get(id); + } + if (item == null) { + item = vMgr.getItem(id); + } + return item; + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean hasItem(NodeId id) { + if (xaItems != null && xaItems.containsKey(id)) { + return true; + } + return vMgr.hasItem(id); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean hasItemReferences(NodeId id) + throws RepositoryException { + return session.getNodeById(id).getReferences().hasNext(); + } + + /** + * {@inheritDoc} + */ + @Override + protected NodeStateEx getNodeStateEx(NodeId parentNodeId) + throws RepositoryException { + try { + NodeState state = (NodeState) stateMgr.getItemState(parentNodeId); + return new NodeStateEx(stateMgr, ntReg, state, null); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * {@inheritDoc} + *

    + * Before modifying version history given, make a local copy of it. + */ + @Override + protected InternalVersion internalCheckin( + InternalVersionHistoryImpl history, + NodeStateEx node, boolean simple, Calendar created) + throws RepositoryException { + + if (history.getVersionManager() != this) { + history = makeLocalCopy(history); + xaItems.put(history.getId(), history); + } + InternalVersion version = + super.internalCheckin(history, node, simple, created); + NodeId frozenNodeId = version.getFrozenNodeId(); + InternalVersionItem frozenNode = createInternalVersionItem(frozenNodeId); + if (frozenNode != null) { + xaItems.put(frozenNodeId, frozenNode); + } + return version; + } + + /** + * {@inheritDoc} + *

    + * Before modifying version history given, make a local copy of it. + */ + @Override + protected void internalRemoveVersion(InternalVersionHistoryImpl history, Name name) + throws VersionException, RepositoryException { + + if (history.getVersionManager() != this) { + history = makeLocalCopy(history); + xaItems.put(history.getId(), history); + // also put 'successor' and 'predecessor' version items to xaItem sets + InternalVersion v = history.getVersion(name); + for (InternalVersion v1 : v.getSuccessors()) { + xaItems.put(v1.getId(), v1); + } + for (InternalVersion v1 : v.getPredecessors()) { + xaItems.put(v1.getId(), v1); + } + } + super.internalRemoveVersion(history, name); + } + + /** + * {@inheritDoc} + *

    + * Before modifying version history given, make a local copy of it. + */ + @Override + protected InternalVersion setVersionLabel(InternalVersionHistoryImpl history, + Name version, Name label, + boolean move) + throws RepositoryException { + + if (history.getVersionManager() != this) { + history = makeLocalCopy(history); + xaItems.put(history.getId(), history); + } + return super.setVersionLabel(history, version, label, move); + } + + /** + * {@inheritDoc} + *

    + * Put the version object into our cache. + */ + @Override + protected void versionCreated(InternalVersion version) { + xaItems.put(version.getId(), version); + } + + /** + * {@inheritDoc} + *

    + * Remove the version object from our cache. + */ + @Override + protected void versionDestroyed(InternalVersion version) { + xaItems.remove(version.getId()); + } + + //-------------------------------------------------------------------< XA > + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + public void associate(TransactionContext tx) { + ((XAItemStateManager) stateMgr).associate(tx); + + Map xaItems = null; + if (tx != null) { + xaItems = (Map) tx.getAttribute(ITEMS_ATTRIBUTE_NAME); + if (xaItems == null) { + xaItems = new HashMap(); + tx.setAttribute(ITEMS_ATTRIBUTE_NAME, xaItems); + } + } + this.xaItems = xaItems; + } + + /** + * {@inheritDoc} + *

    + * Delegate the call to our XA item state manager. + */ + public void beforeOperation(TransactionContext tx) { + ((XAItemStateManager) stateMgr).beforeOperation(tx); + } + + /** + * {@inheritDoc} + *

    + * Delegate the call to our XA item state manager. + */ + public void prepare(TransactionContext tx) throws TransactionException { + if (vmgrLocked) { + ((XAItemStateManager) stateMgr).prepare(tx); + } + } + + /** + * {@inheritDoc} + *

    + * Delegate the call to our XA item state manager. If successful, inform + * global repository manager to update its caches. + */ + @SuppressWarnings("unchecked") + public void commit(TransactionContext tx) throws TransactionException { + if (vmgrLocked) { + ((XAItemStateManager) stateMgr).commit(tx); + Map xaItems = + (Map) tx.getAttribute(ITEMS_ATTRIBUTE_NAME); + vMgr.itemsUpdated(xaItems.values()); + } + } + + /** + * {@inheritDoc} + *

    + * Delegate the call to our XA item state manager. + */ + public void rollback(TransactionContext tx) { + if (vmgrLocked) { + ((XAItemStateManager) stateMgr).rollback(tx); + } + } + + /** + * {@inheritDoc} + *

    + * Delegate the call to our XA item state manager. + */ + public void afterOperation(TransactionContext tx) { + ((XAItemStateManager) stateMgr).afterOperation(tx); + } + + /** + * Returns an {@link InternalXAResource} that acquires a write lock on the + * version manager in {@link InternalXAResource#prepare(TransactionContext)}. + * + * @return an internal XA resource. + */ + public InternalXAResource getXAResourceBegin() { + return new InternalXAResource() { + public void associate(TransactionContext tx) { + } + + public void beforeOperation(TransactionContext tx) { + } + + public void prepare(TransactionContext tx) { + vmgrLock = vMgr.acquireWriteLock(); + vmgrLocked = true; + } + + public void commit(TransactionContext tx) { + // JCR-2712: Ensure that the transaction is prepared + if (!vmgrLocked) { + prepare(tx); + } + } + + public void rollback(TransactionContext tx) { + // JCR-2712: Ensure that the transaction is prepared + if (!vmgrLocked) { + prepare(tx); + } + } + + public void afterOperation(TransactionContext tx) { + } + }; + } + + /** + * Returns an {@link InternalXAResource} that releases the write lock on the + * version manager in {@link InternalXAResource#commit(TransactionContext)} + * or {@link InternalXAResource#rollback(TransactionContext)}. + * + * @return an internal XA resource. + */ + public InternalXAResource getXAResourceEnd() { + return new InternalXAResource() { + public void associate(TransactionContext tx) { + } + + public void beforeOperation(TransactionContext tx) { + } + + public void prepare(TransactionContext tx) { + } + + public void commit(TransactionContext tx) { + internalReleaseWriteLock(); + } + + public void rollback(TransactionContext tx) { + internalReleaseWriteLock(); + } + + public void afterOperation(TransactionContext tx) { + } + + private void internalReleaseWriteLock() { + if (vmgrLocked) { + vmgrLock.release(); + vmgrLocked = false; + } + } + }; + } + + //-------------------------------------------------------< implementation > + + /** + * Return a flag indicating whether this version manager is currently + * associated with an XA transaction. + * @return true if the version manager is in a transaction + */ + private boolean isInXA() { + return xaItems != null; + } + + /** + * Make a local copy of an internal version item. This will recreate the + * (global) version item with state information from our own state + * manager. + * @param history source + * @return the new copy + * @throws RepositoryException if an error occurs + */ + private InternalVersionHistoryImpl makeLocalCopy(InternalVersionHistoryImpl history) + throws RepositoryException { + VersioningLock.ReadLock lock = acquireReadLock(); + try { + NodeState state = (NodeState) stateMgr.getItemState(history.getId()); + NodeStateEx stateEx = new NodeStateEx(stateMgr, ntReg, state, null); + return new InternalVersionHistoryImpl(this, stateEx); + } catch (ItemStateException e) { + throw new RepositoryException("Unable to make local copy", e); + } finally { + lock.release(); + } + } + + /** + * Make a local copy of an internal version item. This will recreate the + * (global) version item with state information from our own state + * manager. + * @param act source + * @return the new copy + * @throws RepositoryException if an error occurs + */ + private InternalActivityImpl makeLocalCopy(InternalActivityImpl act) + throws RepositoryException { + VersioningLock.ReadLock lock = acquireReadLock(); + try { + NodeState state = (NodeState) stateMgr.getItemState(act.getId()); + NodeStateEx stateEx = new NodeStateEx(stateMgr, ntReg, state, null); + return new InternalActivityImpl(this, stateEx); + } catch (ItemStateException e) { + throw new RepositoryException("Unable to make local copy", e); + } finally { + lock.release(); + } + } + + /** + * Return a flag indicating whether an internal version item belongs to + * a different XA environment. + * @param item the item to check + * @return true if in a different env + */ + boolean differentXAEnv(InternalVersionItemImpl item) { + if (item.getVersionManager() == this) { + if (xaItems == null || !xaItems.containsKey(item.getId())) { + return true; + } + } + return false; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/LabelVersionSelector.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/LabelVersionSelector.java new file mode 100644 index 00000000000..46b8e32219d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/LabelVersionSelector.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.core.version; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; + +/** + * This Class implements a version selector that selects a version by label. + * + *

    + * V1.0
    + * V1.1 - "foo"
    + *
    + * new LabelVersionSelector("foo").select() --> V1.1
    + * new LabelVersionSelector("bar").select() --> null
    + *
    + * 
    + */ +public class LabelVersionSelector implements VersionSelector { + + /** + * a versionlabel hint + */ + private Name label = null; + + /** + * Creates a LabelVersionSelector that will try to select a + * version with the given label. + * + * @param label label hint + */ + public LabelVersionSelector(Name label) { + this.label = label; + } + + /** + * Returns the label hint + * + * @return the label hint. + */ + public Name getLabel() { + return label; + } + + /** + * Sets the label hint + * + * @param label label hint + */ + public void setLabel(Name label) { + this.label = label; + } + + /** + * {@inheritDoc} + * + * Selects a version from the given version history using the previously + * assigned hint in the following order: name, label, date, latest. + */ + public InternalVersion select(InternalVersionHistory versionHistory) + throws RepositoryException { + return selectByLabel(versionHistory, label); + } + + /** + * Selects a version by label + * + * @param history history to select from + * @param label desired label + * @return the version with the given label or null + * @throws RepositoryException if an error occurs + */ + public static InternalVersion selectByLabel(InternalVersionHistory history, Name label) + throws RepositoryException { + return history.getVersionByLabel(label); + } + + /** + * returns debug information + * @return debug information + */ + public String toString() { + StringBuilder ret = new StringBuilder(); + ret.append("LabelVersionSelector("); + ret.append("label="); + ret.append(label); + ret.append(")"); + return ret.toString(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/NodeStateEx.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/NodeStateEx.java new file mode 100644 index 00000000000..63e7161f926 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/NodeStateEx.java @@ -0,0 +1,878 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import java.util.List; +import java.util.Set; + +import javax.jcr.ItemExistsException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.UpdatableItemStateManager; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * This Class provides some basic node operations directly on the node state. + */ +public class NodeStateEx { + + /** + * the underlying persistent state + */ + private NodeState nodeState; + + /** + * the state manager + */ + private final UpdatableItemStateManager stateMgr; + + /** + * the node type registry for resolving item defs + */ + private final NodeTypeRegistry ntReg; + + /** + * the cached name + */ + private Name name; + + /** + * the cached node definition + */ + private QNodeDefinition def; + + /** + * Creates a new persistent node + * + * @param stateMgr state manager + * @param ntReg node type registry + * @param nodeState underlying node state + * @param name name (can be null) + */ + public NodeStateEx(UpdatableItemStateManager stateMgr, + NodeTypeRegistry ntReg, + NodeState nodeState, Name name) { + this.nodeState = nodeState; + this.ntReg = ntReg; + this.stateMgr = stateMgr; + this.name = name; + } + + /** + * Creates a new persistent node + * + * @param stateMgr state manager + * @param ntReg node type registry + * @param nodeId node id + * @throws RepositoryException if the node state can't be loaded + */ + public NodeStateEx(UpdatableItemStateManager stateMgr, + NodeTypeRegistry ntReg, + NodeId nodeId) throws RepositoryException { + try { + this.ntReg = ntReg; + this.stateMgr = stateMgr; + this.nodeState = (NodeState) stateMgr.getItemState(nodeId); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + + /** + * returns the name of this node + * + * @return the name of this node + */ + public Name getName() { + if (name == null) { + try { + NodeId parentId = nodeState.getParentId(); + NodeState parent = (NodeState) stateMgr.getItemState(parentId); + name = parent.getChildNodeEntry(nodeState.getNodeId()).getName(); + } catch (ItemStateException e) { + // should never occur + throw new IllegalStateException(e.toString()); + } + } + return name; + } + + /** + * Returns the id of this node. + * + * @return the id of this node. + */ + public NodeId getNodeId() { + return nodeState.getNodeId(); + } + + /** + * Returns the parent id of this node + * + * @return the parent id of this node + */ + public NodeId getParentId() { + return nodeState.getParentId(); + } + + /** + * Returns the parent node of this node + * + * @return the parent node of this node or null if root node + * @throws RepositoryException if an error occurs + */ + public NodeStateEx getParent() throws RepositoryException { + if (nodeState.getParentId() == null) { + return null; + } + return getNode(nodeState.getParentId()); + } + + /** + * Returns the underlaying node state. + * @return the underlaying node state. + */ + public NodeState getState() { + return nodeState; + } + + /** + * Returns the properties of this node + * + * @return the properties of this node + * @throws ItemStateException if an error occurs + */ + public PropertyState[] getProperties() throws ItemStateException { + Set set = nodeState.getPropertyNames(); + PropertyState[] props = new PropertyState[set.size()]; + int i = 0; + for (Name propName : set) { + PropertyId propId = new PropertyId(nodeState.getNodeId(), propName); + props[i++] = (PropertyState) stateMgr.getItemState(propId); + } + return props; + } + + /** + * Checks if the given property exists + * + * @param name name of the property + * @return true if the given property exists. + */ + public boolean hasProperty(Name name) { + PropertyId propId = new PropertyId(nodeState.getNodeId(), name); + return stateMgr.hasItemState(propId); + } + + /** + * Returns the values of the given property or null + * + * @param name name of the property + * @return the values of the given property. + */ + public InternalValue[] getPropertyValues(Name name) { + PropertyId propId = new PropertyId(nodeState.getNodeId(), name); + try { + PropertyState ps = (PropertyState) stateMgr.getItemState(propId); + return ps.getValues(); + } catch (ItemStateException e) { + return null; + } + } + + /** + * Returns the value of the given property or null + * + * @param name name of the property + * @return the value of the given property. + */ + public InternalValue getPropertyValue(Name name) { + PropertyId propId = new PropertyId(nodeState.getNodeId(), name); + try { + PropertyState ps = (PropertyState) stateMgr.getItemState(propId); + return ps.getValues()[0]; + } catch (ItemStateException e) { + return null; + } + } + + /** + * Sets the property value + * + * @param name name of the property + * @param value value to set + * @throws RepositoryException if an error occurs + */ + public void setPropertyValue(Name name, InternalValue value) + throws RepositoryException { + setPropertyValues(name, value.getType(), new InternalValue[]{value}, false); + } + + /** + * Sets the property values + * + * @param name name of the property + * @param type property type + * @param values values to set + * @throws RepositoryException if an error occurs + */ + public void setPropertyValues(Name name, int type, InternalValue[] values) + throws RepositoryException { + setPropertyValues(name, type, values, true); + } + + /** + * Sets the property values + * + * @param name name of the property + * @param type type of the values + * @param values values to set + * @param multiple truefor MV properties + * @return the modified property state + * @throws RepositoryException if an error occurs + */ + public PropertyState setPropertyValues(Name name, int type, InternalValue[] values, boolean multiple) + throws RepositoryException { + PropertyId propId = new PropertyId(nodeState.getNodeId(), name); + if (stateMgr.hasItemState(propId)) { + try { + PropertyState propState = (PropertyState) stateMgr.getItemState(propId); + if (propState.getStatus() == ItemState.STATUS_EXISTING) { + propState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + // although this is not quite correct, we mark node as modified as well + if (nodeState.getStatus() == ItemState.STATUS_EXISTING) { + nodeState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + propState.setType(type); + propState.setMultiValued(multiple); + propState.setValues(values); + return propState; + } catch (ItemStateException e) { + throw new RepositoryException("Unable to create property: " + e.toString()); + } + } else { + PropertyState propState = stateMgr.createNew(name, nodeState.getNodeId()); + propState.setType(type); + propState.setMultiValued(multiple); + propState.setValues(values); + + // need to store node state + nodeState.addPropertyName(name); + if (nodeState.getStatus() == ItemState.STATUS_EXISTING) { + nodeState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + return propState; + } + } + + /** + * Returns the effective (i.e. merged and resolved) node type representation + * of this node's primary and mixin node types. + * + * @return the effective node type + * @throws RepositoryException if an error occurs + */ + public EffectiveNodeType getEffectiveNodeType() throws RepositoryException { + try { + return ntReg.getEffectiveNodeType( + nodeState.getNodeTypeName(), nodeState.getMixinTypeNames()); + } catch (NodeTypeConflictException ntce) { + String msg = "internal error: failed to build effective node type for node " + nodeState.getNodeId(); + throw new RepositoryException(msg, ntce); + } + } + + /** + * checks if the given child node exists. + * + * @param name name of the node + * @return true if the given child exists. + */ + public boolean hasNode(Name name) { + return nodeState.hasChildNodeEntry(name); + } + + /** + * removes the (first) child node with the given name. + * + * @param name name of the node + * @return true if the child was removed + * @throws RepositoryException if an error occurs + */ + public boolean removeNode(Name name) throws RepositoryException { + return removeNode(name, 1); + } + + /** + * removes the given child node + * + * @param node child node to remove + * @return true if the child was removed + * @throws RepositoryException if an error occurs + */ + public boolean removeNode(NodeStateEx node) throws RepositoryException { + // locate child node entry + return removeNode(nodeState.getChildNodeEntry(node.getNodeId())); + } + + + /** + * removes the child node with the given name and 1-based index + * + * @param name name of the child node + * @param index index of the child node + * @return true if the child was removed. + * @throws RepositoryException if an error occurs + */ + public boolean removeNode(Name name, int index) throws RepositoryException { + return removeNode(nodeState.getChildNodeEntry(name, index)); + } + + /** + * removes the child node with the given child node entry + * + * @param entry entry to remove + * @return true if the child was removed. + * @throws RepositoryException if an error occurs + */ + public boolean removeNode(ChildNodeEntry entry) throws RepositoryException { + try { + if (entry == null) { + return false; + } else { + removeNode(entry.getId()); + nodeState.removeChildNodeEntry(entry.getId()); + nodeState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + return true; + } + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * removes recursively the node with the given id + * + * @param id node id + * @throws ItemStateException if an error occurs + */ + private void removeNode(NodeId id) throws ItemStateException { + NodeState state = (NodeState) stateMgr.getItemState(id); + + // remove properties + for (Name name : state.getPropertyNames()) { + PropertyId propId = new PropertyId(id, name); + PropertyState propState = (PropertyState) stateMgr.getItemState(propId); + stateMgr.destroy(propState); + } + state.removeAllPropertyNames(); + + // remove child nodes + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + removeNode(entry.getId()); + } + state.removeAllChildNodeEntries(); + + // destroy the state itself + stateMgr.destroy(state); + } + + /** + * removes the property with the given name + * + * @param name name of the property + * @return true if the property was removed. + * @throws RepositoryException if an error occurs + */ + public boolean removeProperty(Name name) throws RepositoryException { + try { + if (!nodeState.hasPropertyName(name)) { + return false; + } else { + PropertyId propId = new PropertyId(nodeState.getNodeId(), name); + ItemState state = stateMgr.getItemState(propId); + stateMgr.destroy(state); + nodeState.removePropertyName(name); + if (nodeState.getStatus() != ItemState.STATUS_NEW) { + nodeState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + return true; + } + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * retrieves the child node with the given name and 1-base index or + * null if the node does not exist. + * + * @param name name of the child node + * @param index index of the child node + * @return the node state. + * @throws RepositoryException if an error occurs + */ + public NodeStateEx getNode(Name name, int index) throws RepositoryException { + ChildNodeEntry entry = nodeState.getChildNodeEntry(name, index); + if (entry == null) { + return null; + } + try { + NodeState state = (NodeState) stateMgr.getItemState(entry.getId()); + return new NodeStateEx(stateMgr, ntReg, state, name); + } catch (ItemStateException e) { + throw new RepositoryException("Unable to getNode: " + e.toString()); + } + } + + /** + * Returns the node with the given id. + * @param id node id + * @return the new node state + * @throws RepositoryException if an error occurs + */ + public NodeStateEx getNode(NodeId id) throws RepositoryException { + try { + NodeState state = (NodeState) stateMgr.getItemState(id); + return new NodeStateEx(stateMgr, ntReg, state, name); + } catch (ItemStateException e) { + throw new RepositoryException("Unable to getNode: " + e.toString()); + } + } + + /** + * Checks if the given node state exists + * @param id node id + * @return true if the node state exists + */ + public boolean hasNode(NodeId id) { + return stateMgr.hasItemState(id); + } + + /** + * Checks if the given property state exists + * @param id property id + * @return true if the property state exists + */ + public boolean hasProperty(PropertyId id) { + return stateMgr.hasItemState(id); + } + + /** + * Adds a new child node with the given name + * + * @param nodeName name of the new node + * @param nodeTypeName node type name + * @param id id of the new node + * @return the node state + * @throws NoSuchNodeTypeException if the node type does not exist + * @throws ConstraintViolationException if there is a constraint violation + * @throws RepositoryException if an error occurs + */ + public NodeStateEx addNode(Name nodeName, Name nodeTypeName, NodeId id) + throws NoSuchNodeTypeException, ConstraintViolationException, RepositoryException { + return addNode(nodeName, nodeTypeName, id, + ntReg.getEffectiveNodeType(nodeTypeName).includesNodeType(NameConstants.MIX_REFERENCEABLE)); + } + + /** + * Adds a new child node with the given name + * + * @param nodeName name of the new node + * @param nodeTypeName node type name + * @param id id of the new node + * @param referenceable if true, a UUID property is created + * @return the node state + * @throws NoSuchNodeTypeException if the node type does not exist + * @throws ConstraintViolationException if there is a constraint violation + * @throws RepositoryException if an error occurs + */ + public NodeStateEx addNode(Name nodeName, Name nodeTypeName, + NodeId id, boolean referenceable) + throws NoSuchNodeTypeException, ConstraintViolationException, RepositoryException { + + NodeStateEx node = createChildNode(nodeName, nodeTypeName, id); + if (referenceable) { + node.setPropertyValue(NameConstants.JCR_UUID, InternalValue.create(node.getNodeId().toString())); + } + return node; + } + + /** + * Sets the given mixin types + * @param mixinTypeNames the mixin type names + * @throws RepositoryException if an error occurs + */ + public void setMixins(Set mixinTypeNames) throws RepositoryException { + nodeState.setMixinTypeNames(mixinTypeNames); + // update jcr:mixinTypes property + setPropertyValues(NameConstants.JCR_MIXINTYPES, PropertyType.NAME, + InternalValue.create( + mixinTypeNames.toArray(new Name[mixinTypeNames.size()])) + ); + } + /** + * creates a new child node + * + * @param name name + * @param nodeTypeName node type name + * @param id id + * @return the newly created node. + * @throws RepositoryException if an error occurs + */ + private NodeStateEx createChildNode(Name name, Name nodeTypeName, NodeId id) + throws RepositoryException { + NodeId parentId = nodeState.getNodeId(); + // create a new node state + NodeState state = stateMgr.createNew(id, nodeTypeName, parentId); + + // create Node instance wrapping new node state + NodeStateEx node = new NodeStateEx(stateMgr, ntReg, state, name); + node.setPropertyValue(NameConstants.JCR_PRIMARYTYPE, InternalValue.create(nodeTypeName)); + + // add new child node entry + nodeState.addChildNodeEntry(name, state.getNodeId()); + if (nodeState.getStatus() == ItemState.STATUS_EXISTING) { + nodeState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + return node; + } + + /** + * Moves the source node to this node using the given name. + * @param src shareable source node + * @param name name of new node + * @param createShare if true a share is created instead. + * @return child node + * @throws RepositoryException if an error occurs + */ + public NodeStateEx moveFrom(NodeStateEx src, Name name, boolean createShare) + throws RepositoryException { + if (name == null) { + name = src.getName(); + } + EffectiveNodeType ent = getEffectiveNodeType(); + // (4) check for name collisions + QNodeDefinition def; + try { + def = ent.getApplicableChildNodeDef(name, nodeState.getNodeTypeName(), ntReg); + } catch (RepositoryException re) { + String msg = "no definition found in parent node's node type for new node"; + throw new ConstraintViolationException(msg, re); + } + ChildNodeEntry cne = nodeState.getChildNodeEntry(name, 1); + if (cne != null) { + // there's already a child node entry with that name; + // check same-name sibling setting of new node + if (!def.allowsSameNameSiblings()) { + throw new ItemExistsException(getNodeId() + "/" + name); + } + NodeState existingChild; + try { + // check same-name sibling setting of existing node + existingChild = (NodeState) stateMgr.getItemState(cne.getId()); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + QNodeDefinition existingChildDef = ent.getApplicableChildNodeDef( + cne.getName(), existingChild.getNodeTypeName(), ntReg); + if (!existingChildDef.allowsSameNameSiblings()) { + throw new ItemExistsException(existingChild.toString()); + } + } else { + // check if 'add' is allowed + if (getDefinition().isProtected()) { + String msg = "not allowed to modify a protected node"; + throw new ConstraintViolationException(msg); + } + } + + if (createShare) { + // (5) do clone operation + NodeId parentId = getNodeId(); + src.addShareParent(parentId); + // attach to this parent + nodeState.addChildNodeEntry(name, src.getNodeId()); + if (nodeState.getStatus() == ItemState.STATUS_EXISTING) { + nodeState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + return new NodeStateEx(stateMgr, ntReg, src.getState(), name); + } else { + // detach from parent + NodeStateEx parent = getNode(src.getParentId()); + parent.nodeState.removeChildNodeEntry(src.getNodeId()); + if (parent.nodeState.getStatus() == ItemState.STATUS_EXISTING) { + parent.nodeState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + // attach to this parent + nodeState.addChildNodeEntry(name, src.getNodeId()); + if (nodeState.getStatus() == ItemState.STATUS_EXISTING) { + nodeState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + NodeState srcState = src.getState(); + srcState.setParentId(getNodeId()); + + if (srcState.getStatus() == ItemState.STATUS_EXISTING) { + srcState.setStatus(ItemState.STATUS_EXISTING_MODIFIED); + } + return new NodeStateEx(stateMgr, ntReg, srcState, name); + } + } + + /** + * Adds a share parent id + * @param parentId the parent id + * @throws RepositoryException if an error occurs + */ + private void addShareParent(NodeId parentId) throws RepositoryException { + // verify that we're shareable + if (!nodeState.isShareable()) { + String msg = this + " is not shareable."; + throw new RepositoryException(msg); + } + + // detect share cycle (TODO) + // NodeId srcId = getNodeId(); + //HierarchyManager hierMgr = session.getHierarchyManager(); + //if (parentId.equals(srcId) || hierMgr.isAncestor(srcId, parentId)) { + // String msg = "This would create a share cycle."; + // log.debug(msg); + // throw new RepositoryException(msg); + //} + + if (!nodeState.containsShare(parentId)) { + if (nodeState.addShare(parentId)) { + return; + } + } + String msg = "Adding a shareable node twice to the same parent is not supported."; + throw new UnsupportedRepositoryOperationException(msg); + } + + /** + * returns all child nodes + * + * @return the child nodes. + * @throws RepositoryException if an error occurs + */ + public NodeStateEx[] getChildNodes() throws RepositoryException { + try { + List entries = nodeState.getChildNodeEntries(); + NodeStateEx[] children = new NodeStateEx[entries.size()]; + int i = 0; + for (ChildNodeEntry entry : entries) { + NodeState state = (NodeState) stateMgr.getItemState(entry.getId()); + children[i++] = new NodeStateEx(stateMgr, ntReg, state, entry.getName()); + } + return children; + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * stores the persistent state recursively + * + * @throws RepositoryException if an error occurs + */ + public void store() throws RepositoryException { + store(true); + } + + /** + * Stores the persistent state and depending on the recursively + * flag also stores the modified child nodes recursively. + * + * + * @param recursively whether to store the nodes recursively or just this + * single node. + * @throws RepositoryException if an error occurs + */ + public void store(boolean recursively) throws RepositoryException { + try { + store(nodeState, recursively); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * stores the given persistent state recursively + * + * @param state node state to store + * @throws ItemStateException if an error occurs + */ + private void store(NodeState state, boolean recursively) + throws ItemStateException { + + if (state.getStatus() != ItemState.STATUS_EXISTING) { + // first store all transient properties + for (Name propName : state.getPropertyNames()) { + PropertyState pstate = (PropertyState) stateMgr.getItemState( + new PropertyId(state.getNodeId(), propName)); + if (pstate.getStatus() != ItemState.STATUS_EXISTING) { + stateMgr.store(pstate); + } + } + if (recursively) { + // now store all child node entries + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + NodeState nstate = (NodeState) stateMgr.getItemState(entry.getId()); + store(nstate, true); + } + } + // and store itself + stateMgr.store(state); + } + } + + /** + * reloads the persistent state recursively + * + * @throws RepositoryException if an error occurs + */ + public void reload() throws RepositoryException { + try { + reload(nodeState); + // refetch node state if discarded + nodeState = (NodeState) stateMgr.getItemState(nodeState.getNodeId()); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * reloads the given persistent state recursively + * + * @param state node state + * @throws ItemStateException if an error occurs + */ + private void reload(NodeState state) throws ItemStateException { + if (state.getStatus() != ItemState.STATUS_EXISTING) { + // first discard all all transient properties + for (Name propName : state.getPropertyNames()) { + PropertyState pstate = (PropertyState) stateMgr.getItemState( + new PropertyId(state.getNodeId(), propName)); + if (pstate.getStatus() != ItemState.STATUS_EXISTING) { + pstate.discard(); + } + } + // now reload all child node entries + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + NodeState nstate = (NodeState) stateMgr.getItemState(entry.getId()); + reload(nstate); + } + // and reload itself + state.discard(); + } + } + + /** + * copies a property + * + * @param prop source property + * @throws RepositoryException if an error occurs + */ + public void copyFrom(PropertyImpl prop) throws RepositoryException { + if (prop.isMultiple()) { + InternalValue[] values = prop.internalGetValues(); + InternalValue[] copiedValues = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + copiedValues[i] = values[i].createCopy(); + } + setPropertyValues(prop.getQName(), prop.getType(), copiedValues); + } else { + setPropertyValue(prop.getQName(), prop.internalGetValue().createCopy()); + } + } + + /** + * copies a property + * + * @param prop source property + * @throws RepositoryException if an error occurs + */ + public void copyFrom(PropertyState prop) throws RepositoryException { + InternalValue[] values = prop.getValues(); + InternalValue[] copiedValues = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + copiedValues[i] = values[i].createCopy(); + } + setPropertyValues(prop.getName(), prop.getType(), copiedValues, prop.isMultiValued()); + } + + /** + * Returns the QNodeDefinition for this state + * @return the node def + * @throws RepositoryException if an error occurs + */ + public QNodeDefinition getDefinition() throws RepositoryException { + if (def == null) { + EffectiveNodeType ent = getParent().getEffectiveNodeType(); + def = ent.getApplicableChildNodeDef(getName(), + nodeState.getNodeTypeName(), ntReg); + } + return def; + } + + /** + * Returns the property definition for the property state + * @param prop the property state + * @return the prop def + * @throws RepositoryException if an error occurs + */ + public QPropertyDefinition getDefinition(PropertyState prop) + throws RepositoryException { + return getEffectiveNodeType().getApplicablePropertyDef( + prop.getName(), prop.getType(), prop.isMultiValued()); + } + + /** + * Checks if this state has the indicated ancestor + * @param nodeId the node id of the ancestor + * @return true if it has the indicated ancestor + * @throws RepositoryException if an error occurs + */ + public boolean hasAncestor(NodeId nodeId) throws RepositoryException { + if (nodeId.equals(nodeState.getParentId())) { + return true; + } + NodeStateEx parent = getParent(); + return parent != null && parent.hasAncestor(nodeId); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionHistoryImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionHistoryImpl.java new file mode 100644 index 00000000000..770cac26603 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionHistoryImpl.java @@ -0,0 +1,387 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import org.apache.jackrabbit.core.AbstractNodeData; +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.commons.iterator.FrozenNodeIteratorAdapter; + +import javax.jcr.version.VersionHistory; +import javax.jcr.version.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.AccessDeniedException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionIterator; + +/** + * Base implementation of the {@link javax.jcr.version.VersionHistory} interface. + */ +public class VersionHistoryImpl extends NodeImpl implements VersionHistory { + + /** + * Logger instance. + */ + private static Logger log = LoggerFactory.getLogger(VersionHistoryImpl.class); + + /** + * Create a new instance of this class. + * @param itemMgr item manager + * @param sessionContext component context of the associated session + * @param data node data + */ + public VersionHistoryImpl( + ItemManager itemMgr, SessionContext sessionContext, + AbstractNodeData data) { + super(itemMgr, sessionContext, data); + } + + /** + * Returns the internal version history. Subclass responsibility. + * + * @return internal version history + * @throws RepositoryException if the internal version history is not available + */ + protected InternalVersionHistory getInternalVersionHistory() + throws RepositoryException { + SessionImpl session = sessionContext.getSessionImpl(); + InternalVersionHistory history = + session.getInternalVersionManager().getVersionHistory((NodeId) id); + if (history == null) { + throw new InvalidItemStateException(id + ": the item does not exist anymore"); + } + return history; + } + + /** + * @see javax.jcr.version.VersionHistory#getRootVersion() + */ + public javax.jcr.version.Version getRootVersion() throws RepositoryException { + SessionImpl session = sessionContext.getSessionImpl(); + return (Version) session.getNodeById( + getInternalVersionHistory().getRootVersion().getId()); + } + + /** + * @see javax.jcr.version.VersionHistory#getAllVersions() + */ + public VersionIterator getAllVersions() throws RepositoryException { + return new VersionIteratorImpl( + getSession(), getInternalVersionHistory().getRootVersion()); + } + + /** + * @see VersionHistory#getAllFrozenNodes() + */ + public NodeIterator getAllFrozenNodes() throws RepositoryException { + return new FrozenNodeIteratorAdapter(getAllVersions()); + } + + /** + * @see VersionHistory#getAllLinearVersions() + */ + @SuppressWarnings("deprecation") + public VersionIterator getAllLinearVersions() throws RepositoryException { + // get base version. this can certainly be optimized + SessionImpl session = sessionContext.getSessionImpl(); + InternalVersionHistory vh = getInternalVersionHistory(); + Node vn = session.getNodeById(vh.getVersionableId()); + InternalVersion base = ((VersionImpl) vn.getBaseVersion()).getInternalVersion(); + + return new VersionIteratorImpl(getSession(), vh.getRootVersion(), base); + } + + /** + * @see VersionHistory#getAllLinearFrozenNodes() + */ + public NodeIterator getAllLinearFrozenNodes() throws RepositoryException { + return new FrozenNodeIteratorAdapter(getAllLinearVersions()); + } + + /** + * @see javax.jcr.version.VersionHistory#getVersion(String) + */ + public javax.jcr.version.Version getVersion(String versionName) + throws VersionException, RepositoryException { + try { + Name name = sessionContext.getQName(versionName); + InternalVersion v = getInternalVersionHistory().getVersion(name); + if (v == null) { + throw new VersionException("No version with name '" + versionName + "' exists in this version history."); + } + return (Version) sessionContext.getSessionImpl().getNodeById(v.getId()); + } catch (NameException e) { + throw new VersionException(e); + } + } + + /** + * @see javax.jcr.version.VersionHistory#getVersionByLabel(String) + */ + public javax.jcr.version.Version getVersionByLabel(String label) throws RepositoryException { + try { + Name qLabel = sessionContext.getQName(label); + InternalVersion v = + getInternalVersionHistory().getVersionByLabel(qLabel); + if (v == null) { + throw new VersionException("No version with label '" + label + "' exists in this version history."); + } + return (Version) sessionContext.getSessionImpl().getNodeById(v.getId()); + } catch (NameException e) { + throw new VersionException(e); + } + } + + /** + * @see javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean) + */ + public void addVersionLabel(String versionName, String label, boolean move) + throws VersionException, RepositoryException { + try { + // check permissions + checkVersionManagementPermission(); + sessionContext.getSessionImpl().getInternalVersionManager().setVersionLabel( + getSession(), getInternalVersionHistory(), + sessionContext.getQName(versionName), + sessionContext.getQName(label), move); + } catch (NameException e) { + throw new VersionException(e); + } + } + + /** + * @see javax.jcr.version.VersionHistory#removeVersionLabel(String) + */ + public void removeVersionLabel(String label) throws RepositoryException { + try { + // check permissions + checkVersionManagementPermission(); + InternalVersion existing = sessionContext.getSessionImpl().getInternalVersionManager().setVersionLabel( + getSession(), getInternalVersionHistory(), + null, sessionContext.getQName(label), true); + if (existing == null) { + throw new VersionException("No version with label '" + label + "' exists in this version history."); + } + } catch (NameException e) { + throw new VersionException(e); + } + } + + + /** + * @see javax.jcr.version.VersionHistory#getVersionLabels + */ + public String[] getVersionLabels() throws RepositoryException { + Name[] labels = getInternalVersionHistory().getVersionLabels(); + String[] ret = new String[labels.length]; + for (int i = 0; i < labels.length; i++) { + ret[i] = sessionContext.getJCRName(labels[i]); + } + return ret; + } + + /** + * @see javax.jcr.version.VersionHistory#getVersionLabels(javax.jcr.version.Version) + */ + public String[] getVersionLabels(javax.jcr.version.Version version) + throws VersionException, RepositoryException { + checkOwnVersion(version); + Name[] labels = ((VersionImpl) version).getInternalVersion().getLabels(); + String[] ret = new String[labels.length]; + for (int i = 0; i < labels.length; i++) { + ret[i] = sessionContext.getJCRName(labels[i]); + } + return ret; + } + + /** + * @see javax.jcr.version.VersionHistory#hasVersionLabel(String) + */ + public boolean hasVersionLabel(String label) throws RepositoryException { + try { + Name qLabel = sessionContext.getQName(label); + return getInternalVersionHistory().getVersionByLabel(qLabel) != null; + } catch (NameException e) { + throw new IllegalArgumentException("Unable to resolve label: " + e); + } + } + + /** + * @see javax.jcr.version.VersionHistory#hasVersionLabel(javax.jcr.version.Version, String) + */ + public boolean hasVersionLabel(javax.jcr.version.Version version, String label) + throws VersionException, RepositoryException { + checkOwnVersion(version); + try { + Name qLabel = sessionContext.getQName(label); + return ((VersionImpl) version).getInternalVersion().hasLabel(qLabel); + } catch (NameException e) { + throw new VersionException(e); + } + } + + /** + * @see javax.jcr.version.VersionHistory#removeVersion(String) + */ + public void removeVersion(String versionName) + throws UnsupportedRepositoryOperationException, VersionException, + RepositoryException { + try { + // check permissions + checkVersionManagementPermission(); + sessionContext.getSessionImpl().getInternalVersionManager().removeVersion( + getSession(), + getInternalVersionHistory(), + sessionContext.getQName(versionName)); + } catch (NameException e) { + throw new RepositoryException(e); + } + } + + /** + * Removes this VersionHistory from storage. + * + * @throws RepositoryException if an error occurs. + */ + public void removeVersionHistory() throws RepositoryException { + checkVersionManagementPermission(); + InternalVersionManager internalVersionManager = + sessionContext.getSessionImpl().getInternalVersionManager(); + internalVersionManager.removeVersionHistory( + getSession(), + getInternalVersionHistory()); + } + + /** + * @see javax.jcr.Item#isSame(javax.jcr.Item) + */ + @Override + public boolean isSame(Item otherItem) { + if (otherItem instanceof VersionHistoryImpl) { + // since all version histories live in the same workspace, we can compare the uuids + try { + InternalVersionHistory other = ((VersionHistoryImpl) otherItem).getInternalVersionHistory(); + return other.getId().equals(getInternalVersionHistory().getId()); + } catch (RepositoryException e) { + log.warn("Unable to retrieve internal version history objects: " + e.getMessage()); + log.debug("Stack dump:", e); + } + } + return false; + } + + /** + * {@inheritDoc} + */ + public String getVersionableUUID() throws RepositoryException { + return getVersionableIdentifier(); + } + + /** + * {@inheritDoc} + */ + public String getVersionableIdentifier() throws RepositoryException { + return getInternalVersionHistory().getVersionableId().toString(); + } + + /** + * Checks if the current session has version management permission + * + * @throws AccessDeniedException if version management is not allowed + * @throws RepositoryException if an error occurs + */ + private void checkVersionManagementPermission() throws RepositoryException { + try { + sessionContext.getAccessManager().checkPermission(getPrimaryPath(), Permission.VERSION_MNGMT); + } catch (ItemNotFoundException e) { + // ignore. + } + } + + /** + * Checks if the given version belongs to this history + * + * @param version the version + * @throws javax.jcr.version.VersionException if the specified version is + * not part of this version history + * @throws javax.jcr.RepositoryException if a repository error occurs + */ + private void checkOwnVersion(Version version) + throws VersionException, RepositoryException { + if (!version.getParent().isSame(this)) { + throw new VersionException("Specified version not contained in this history."); + } + } + + //--------------------------------------< Overwrite "protected" methods >--- + + /** + * Always throws a {@link javax.jcr.nodetype.ConstraintViolationException} since this node + * is protected. + * + * @throws javax.jcr.nodetype.ConstraintViolationException + */ + @Override + public void update(String srcWorkspaceName) throws ConstraintViolationException { + String msg = "update operation not allowed: " + this; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + + /** + * Always throws a {@link javax.jcr.nodetype.ConstraintViolationException} since this node + * is protected. + * + * @throws javax.jcr.nodetype.ConstraintViolationException + */ + @Override + public NodeIterator merge(String srcWorkspace, boolean bestEffort) + throws ConstraintViolationException { + String msg = "merge operation not allowed: " + this; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + + //--------------------------------------------------------------< Object > + + /** + * Return a string representation of this version history node + * for diagnostic purposes. + * + * @return "version history node /path/to/item" + */ + public String toString() { + return "version history " + super.toString(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionHistoryInfo.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionHistoryInfo.java new file mode 100644 index 00000000000..c04fee3de58 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionHistoryInfo.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.core.version; + +import org.apache.jackrabbit.core.id.NodeId; + +/** + * Simple data object that carries the identifiers of a version history node + * and the related root version node. + * + * @since Apache Jackrabbit 1.5 + * @see JCR-1775 + */ +public class VersionHistoryInfo { + + /** + * Identifier of the version history node. + */ + private final NodeId versionHistoryId; + + /** + * Identifier of the root version node. + */ + private final NodeId rootVersionId; + + /** + * Creates an object that carries the given version history information. + * + * @param versionHistoryId identifier of the version history node + * @param rootVersionId identifier of the root version node + */ + public VersionHistoryInfo(NodeId versionHistoryId, NodeId rootVersionId) { + this.versionHistoryId = versionHistoryId; + this.rootVersionId = rootVersionId; + } + + /** + * Returns the identifier of the version history node. + */ + public NodeId getVersionHistoryId() { + return versionHistoryId; + } + + /** + * Returns the identifier of the root version node. + */ + public NodeId getRootVersionId() { + return rootVersionId; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionImpl.java new file mode 100644 index 00000000000..2070a6a74c6 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionImpl.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import org.apache.jackrabbit.core.ItemManager; +import org.apache.jackrabbit.core.AbstractNodeData; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.NodeImpl; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.nodetype.ConstraintViolationException; +import java.util.Calendar; +import java.util.List; + +/** + * Base implementation of the {@link javax.jcr.version.Version} interface. + */ +public class VersionImpl extends NodeImpl implements Version { + + /** + * Logger instance. + */ + private static Logger log = LoggerFactory.getLogger(VersionImpl.class); + + /** + * Create a new instance of this class. + * @param itemMgr item manager + * @param sessionContext component context of the associated session + * @param data node data + */ + public VersionImpl( + ItemManager itemMgr, SessionContext sessionContext, + AbstractNodeData data) { + super(itemMgr, sessionContext, data); + } + + /** + * Returns the internal version. Subclass responsibility. + * @return internal version + * @throws RepositoryException if the internal version is not available + */ + protected InternalVersion getInternalVersion() throws RepositoryException { + SessionImpl session = sessionContext.getSessionImpl(); + InternalVersion version = + session.getInternalVersionManager().getVersion((NodeId) id); + if (version == null) { + throw new InvalidItemStateException(id + ": the item does not exist anymore"); + } + return version; + } + + /** + * {@inheritDoc} + */ + public Calendar getCreated() throws RepositoryException { + return getInternalVersion().getCreated(); + } + + /** + * {@inheritDoc} + */ + public javax.jcr.version.Version[] getSuccessors() throws RepositoryException { + // need to wrap it around proper node + List suc = getInternalVersion().getSuccessors(); + Version[] ret = new Version[suc.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = (Version) sessionContext.getSessionImpl().getNodeById( + suc.get(i).getId()); + } + return ret; + } + + /** + * {@inheritDoc} + */ + public javax.jcr.version.Version[] getPredecessors() throws RepositoryException { + // need to wrap it around proper node + InternalVersion[] pred = getInternalVersion().getPredecessors(); + Version[] ret = new Version[pred.length]; + for (int i = 0; i < pred.length; i++) { + ret[i] = (Version) sessionContext.getSessionImpl().getNodeById(pred[i].getId()); + } + return ret; + } + + /** + * {@inheritDoc} + */ + public Version getLinearSuccessor() throws RepositoryException { + // get base version. this can certainly be optimized + SessionImpl session = sessionContext.getSessionImpl(); + InternalVersionHistory vh = ((VersionHistoryImpl) getContainingHistory()) + .getInternalVersionHistory(); + Node vn = session.getNodeById(vh.getVersionableId()); + InternalVersion base = ((VersionImpl) vn.getBaseVersion()).getInternalVersion(); + + InternalVersion suc = getInternalVersion().getLinearSuccessor(base); + return suc == null ? null : (Version) session.getNodeById(suc.getId()); + } + + /** + * {@inheritDoc} + */ + public javax.jcr.version.Version getLinearPredecessor() throws RepositoryException { + InternalVersion pred = getInternalVersion().getLinearPredecessor(); + return pred == null ? null : (Version) sessionContext.getSessionImpl().getNodeById(pred.getId()); + } + + /** + * {@inheritDoc} + */ + public javax.jcr.version.VersionHistory getContainingHistory() throws RepositoryException { + return (VersionHistory) getParent(); + } + + /** + * Returns the frozen node of this version + * + * @return the internal frozen node + * @throws javax.jcr.RepositoryException if an error occurs + */ + public InternalFrozenNode getInternalFrozenNode() throws RepositoryException { + return getInternalVersion().getFrozenNode(); + } + + /** + * {@inheritDoc} + */ + public Node getFrozenNode() throws RepositoryException { + return sessionContext.getSessionImpl().getNodeById(getInternalVersion().getFrozenNodeId()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isSame(Item otherItem) { + if (otherItem instanceof VersionImpl) { + // since all versions live in the same workspace, we can compare the uuids + try { + InternalVersion other = ((VersionImpl) otherItem).getInternalVersion(); + return other.getId().equals(getInternalVersion().getId()); + } catch (RepositoryException e) { + log.warn("Unable to retrieve internal version objects: " + e.getMessage()); + log.debug("Stack dump:", e); + } + } + return false; + } + + /** + * Checks if this version is more recent than the given version v. + * A version is more recent if and only if it is a successor (or a successor + * of a successor, etc., to any degree of separation) of the compared one. + * + * @param v the version to check + * @return true if the version is more recent; + * false otherwise. + * @throws RepositoryException if a repository error occurs + */ + public boolean isMoreRecent(VersionImpl v) throws RepositoryException { + return getInternalVersion().isMoreRecent(v.getInternalVersion()); + } + + /** + * Checks if this is the root version. + * @return true if this version is the root version; + * false otherwise. + * @throws RepositoryException if a repository error occurs + */ + public boolean isRootVersion() throws RepositoryException { + return getInternalVersion().isRootVersion(); + } + + //--------------------------------------< Overwrite "protected" methods >--- + + + /** + * Always throws a {@link javax.jcr.nodetype.ConstraintViolationException} since this node + * is protected. + * + * @throws javax.jcr.nodetype.ConstraintViolationException + */ + @Override + public void update(String srcWorkspaceName) throws ConstraintViolationException { + String msg = "update operation not allowed: " + this; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + + /** + * Always throws a {@link javax.jcr.nodetype.ConstraintViolationException} since this node + * is protected. + * + * @throws javax.jcr.nodetype.ConstraintViolationException + */ + @Override + public NodeIterator merge(String srcWorkspace, boolean bestEffort) + throws ConstraintViolationException { + String msg = "merge operation not allowed: " + this; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + + //--------------------------------------------------------------< Object > + + /** + * Return a string representation of this version node for diagnostic + * purposes. + * + * @return "version node /path/to/item" + */ + public String toString() { + return "version " + super.toString(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionItemStateManager.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionItemStateManager.java new file mode 100644 index 00000000000..8c638630488 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionItemStateManager.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.core.version; + +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.ReferentialIntegrityException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.NodeIdFactory; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ISMLocking; +import org.apache.jackrabbit.core.state.ItemStateCacheFactory; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.SharedItemStateManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Specialized SharedItemStateManager that filters out NodeReferences to + * non-versioning states. + */ +public class VersionItemStateManager extends SharedItemStateManager { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(VersionItemStateManager.class); + + /** + * The persistence manager. + */ + private final PersistenceManager pMgr; + + public VersionItemStateManager(PersistenceManager persistMgr, + NodeId rootNodeId, + NodeTypeRegistry ntReg, + ItemStateCacheFactory cacheFactory, + ISMLocking locking, + NodeIdFactory nodeIdFactory) + throws ItemStateException { + super(persistMgr, rootNodeId, ntReg, false, cacheFactory, locking, nodeIdFactory); + this.pMgr = persistMgr; + } + + @Override + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + // check persistence manager + try { + return pMgr.loadReferencesTo(id); + } catch (NoSuchItemStateException e) { + // ignore + } + // throw + throw new NoSuchItemStateException(id.toString()); + } + + @Override + public boolean hasNodeReferences(NodeId id) { + // check persistence manager + try { + if (pMgr.existsReferencesTo(id)) { + return true; + } + } catch (ItemStateException e) { + // ignore + } + return false; + } + + /** + * Sets the + * @param references + * @return + */ + public boolean setNodeReferences(ChangeLog references) { + try { + ChangeLog log = new ChangeLog(); + + for (NodeReferences source : references.modifiedRefs()) { + // filter out version storage intern ones + NodeReferences target = new NodeReferences(source.getTargetId()); + for (PropertyId id : source.getReferences()) { + if (!hasNonVirtualItemState(id.getParentId())) { + target.addReference(id); + } + } + log.modified(target); + } + + if (log.hasUpdates()) { + pMgr.store(log); + } + return true; + } catch (ItemStateException e) { + log.error("Error while setting references: " + e.toString()); + return false; + } + } + + protected void checkReferentialIntegrity(ChangeLog changes) + throws ReferentialIntegrityException, ItemStateException { + // only store VV-type references and NV-type references + + // check whether targets of modified node references exist + Set remove = new HashSet(); + for (NodeReferences refs : changes.modifiedRefs()) { + // no need to check existence of target if there are no references + if (refs.hasReferences()) { + NodeId id = refs.getTargetId(); + if (!changes.has(id) && !hasNonVirtualItemState(id)) { + remove.add(refs.getTargetId()); + } + } + } + // remove references + for (NodeId id : remove) { + changes.removeReferencesEntry(id); + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionItemStateProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionItemStateProvider.java new file mode 100644 index 00000000000..ac73e89ce07 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionItemStateProvider.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import javax.jcr.RepositoryException; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateListener; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider; +import org.apache.jackrabbit.core.virtual.VirtualNodeState; +import org.apache.jackrabbit.core.virtual.VirtualPropertyState; +import org.apache.jackrabbit.spi.Name; + +/** + * This Class implements a virtual item state provider. + */ +class VersionItemStateProvider implements VirtualItemStateProvider, ItemStateListener { + + /** + * The root node UUID for the version storage + */ + private final NodeId historyRootId; + + /** + * The root node UUID for the activity storage + */ + private final NodeId activitiesRootId; + + /** + * The item state manager directly on the version persistence mgr + */ + private final VersionItemStateManager stateMgr; + + /** + * Map of returned items. this is kept for invalidating + */ + private ReferenceMap items = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK); + + /** + * Creates a new version manager + * + */ + public VersionItemStateProvider(NodeId historyRootId, + NodeId activitiesRootId, + VersionItemStateManager stateMgr) { + this.historyRootId = historyRootId; + this.activitiesRootId = activitiesRootId; + this.stateMgr = stateMgr; + + stateMgr.addListener(this); + } + + //------------------------------------------< VirtualItemStateProvider >--- + + /** + * @inheritDoc + */ + public boolean isVirtualRoot(ItemId id) { + return id.equals(historyRootId) || id.equals(activitiesRootId); + } + + /** + * @inheritDoc + */ + public NodeId getVirtualRootId() { + return historyRootId; + } + + /** + * @inheritDoc + */ + public NodeId[] getVirtualRootIds() { + return new NodeId[]{historyRootId, activitiesRootId}; + } + + /** + * @inheritDoc + */ + public VirtualPropertyState createPropertyState(VirtualNodeState parent, + Name name, int type, + boolean multiValued) + throws RepositoryException { + throw new IllegalStateException("InternalVersionManager should never create a VirtualPropertyState"); + } + + /** + * @inheritDoc + */ + public VirtualNodeState createNodeState(VirtualNodeState parent, Name name, + NodeId id, Name nodeTypeName) + throws RepositoryException { + throw new IllegalStateException("InternalVersionManager should never create a VirtualNodeState"); + } + + /** + * @inheritDoc + */ + public synchronized ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + ItemState item = (ItemState) items.get(id); + if (item == null) { + item = stateMgr.getItemState(id); + items.put(id, item); + } + return item; + } + + /** + * @inheritDoc + */ + public boolean setNodeReferences(ChangeLog references) { + return stateMgr.setNodeReferences(references); + } + + /** + * @inheritDoc + */ + public boolean hasItemState(ItemId id) { + return items.get(id) != null || stateMgr.hasItemState(id); + } + + /** + * @inheritDoc + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + return stateMgr.getNodeReferences(id); + } + + /** + * @inheritDoc + */ + public boolean hasNodeReferences(NodeId id) { + return stateMgr.hasNodeReferences(id); + } + + /** + * {@inheritDoc} + */ + public void addListener(ItemStateListener listener) { + stateMgr.addListener(listener); + } + + /** + * {@inheritDoc} + */ + public void removeListener(ItemStateListener listener) { + stateMgr.removeListener(listener); + } + + //-------------------------------------------------< ItemStateListener >--- + + /** + * @inheritDoc + */ + public void stateCreated(ItemState created) { + // ignore + } + + /** + * @inheritDoc + */ + public void stateModified(ItemState modified) { + // ignore + } + + /** + * @inheritDoc + */ + public void stateDestroyed(ItemState destroyed) { + items.remove(destroyed.getId()); + } + + /** + * @inheritDoc + */ + public void stateDiscarded(ItemState discarded) { + items.remove(discarded.getId()); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionIteratorImpl.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionIteratorImpl.java new file mode 100644 index 00000000000..5110f42fc3d --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionIteratorImpl.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.core.version; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.SessionImpl; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionIterator; +import java.util.ConcurrentModificationException; +import java.util.LinkedList; +import java.util.NoSuchElementException; + +/** + * This Class implements a VersionIterator that iterates over a version + * graph following the successor nodes. When this iterator is created, it gathers + * the id's of the versions and returns them when iterating. please note, that + * a version can be deleted while traversing this iterator and the 'nextVersion' + * would produce a ConcurrentModificationException. + *

    + * If this iterator is initialized with a base version, it will only iterate + * on the versions of a single line of decent from the given root version to the + * base version. + */ +class VersionIteratorImpl implements VersionIterator { + + /** + * the id's of the versions to return + */ + private final LinkedList versions = new LinkedList(); + + /** + * the current position + */ + private int pos = 0; + + /** + * the session for wrapping the versions + */ + private final SessionImpl session; + + /** + * The number of versions available. + */ + private final long size; + + /** + * Creates a new VersionIterator that iterates over the version tree, + * starting the root node. + * + * @param session repository session + * @param rootVersion the root version + */ + public VersionIteratorImpl(Session session, InternalVersion rootVersion) { + this(session, rootVersion, null); + } + + /** + * Creates a new VersionIterator that iterates over a single line of decent + * of all versions starting at the root version and ending at the given + * base version + * + * @param session repository session + * @param rootVersion the root version + * @param baseVersion the ending base version + */ + public VersionIteratorImpl(Session session, InternalVersion rootVersion, + InternalVersion baseVersion) { + this.session = (SessionImpl) session; + if (baseVersion == null) { + collectAllVersions(rootVersion); + } else { + collectLinearVersions(baseVersion); + } + // retrieve initial size, since size of the list is not stable + size = versions.size(); + } + + /** + * {@inheritDoc} + */ + public Version nextVersion() { + if (versions.isEmpty()) { + throw new NoSuchElementException(); + } + NodeId id = versions.removeFirst(); + pos++; + + try { + return (Version) session.getNodeById(id); + } catch (RepositoryException e) { + throw new ConcurrentModificationException("Unable to provide element: " + e.toString()); + } + } + + /** + * {@inheritDoc} + */ + public void skip(long skipNum) { + while (skipNum > 0) { + skipNum--; + nextVersion(); + } + } + + /** + * {@inheritDoc} + */ + public long getSize() { + return size; + } + + /** + * {@inheritDoc} + */ + public long getPosition() { + return pos; + } + + /** + * {@inheritDoc} + * @throws UnsupportedOperationException since this operation is not supported + */ + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + return !versions.isEmpty(); + } + + /** + * {@inheritDoc} + */ + public Object next() { + return nextVersion(); + } + + /** + * Adds the version subtree starting at root to the internal + * set of versions. + * + * @param root the root version + */ + private synchronized void collectAllVersions(InternalVersion root) { + LinkedList workQueue = new LinkedList(); + workQueue.add(root); + while (!workQueue.isEmpty()) { + InternalVersion currentVersion = workQueue.removeFirst(); + NodeId id = currentVersion.getId(); + if (!versions.contains(id)) { + versions.add(id); + workQueue.addAll(currentVersion.getSuccessors()); + } + } + + } + + /** + * Adds all versions of a single line of decent starting from the root + * version to the base version. + * + * @param base the base version + */ + private synchronized void collectLinearVersions(InternalVersion base) { + while (base != null) { + versions.addFirst(base.getId()); + InternalVersion[] preds = base.getPredecessors(); + if (preds.length == 0) { + base = null; + } else { + base = preds[0]; + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplBase.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplBase.java new file mode 100644 index 00000000000..ee352075109 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplBase.java @@ -0,0 +1,559 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import java.util.Calendar; +import java.util.Set; +import java.util.HashSet; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.ItemNotFoundException; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.LocalItemStateManager; +import org.apache.jackrabbit.core.state.UpdatableItemStateManager; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.Path; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +/** + * The JCR Version Manager implementation is split in several classes in order to + * group related methods together. + *

    + * This class provides basic routines for all operations and the methods related + * to checkin and checkout. + */ +abstract public class VersionManagerImplBase { + + /** + * default logger + */ + private static final Logger log = LoggerFactory.getLogger(VersionManagerImplBase.class); + + /** + * Component context of the current session + */ + protected final SessionContext context; + + /** + * workspace session + */ + protected final SessionImpl session; + + /** + * item state manager for all workspace operations + */ + protected final UpdatableItemStateManager stateMgr; + + /** + * hierarchy manager that operates on the locale state manager + */ + protected final HierarchyManager hierMgr; + + /** + * node type registry + */ + protected final NodeTypeRegistry ntReg; + + /** + * the session version manager. + */ + protected final InternalVersionManager vMgr; + + /** + * the lock on this version manager + */ + private final VersioningLock rwLock = new VersioningLock(); + + /** + * the node id of the current activity + */ + protected NodeId currentActivity; + + /** + * Creates a new version manager base for the given session + * + * @param context component context of the current session + * @param stateMgr the underlying state manager + * @param hierMgr local hierarchy manager + */ + protected VersionManagerImplBase( + SessionContext context, UpdatableItemStateManager stateMgr, + HierarchyManager hierMgr) { + this.context = context; + this.session = context.getSessionImpl(); + this.stateMgr = stateMgr; + this.hierMgr = hierMgr; + this.ntReg = session.getNodeTypeManager().getNodeTypeRegistry(); + this.vMgr = session.getInternalVersionManager(); + } + + /** + * Performs a checkin or checkout operation. if checkin is + * true the node is checked in. if checkout is + * true the node is checked out. if both flags are true + * the checkin is performed prior to the checkout and the operation is + * equivalent to a checkpoint operation. + * + * @param state node state + * @param checkin if true the node is checked in. + * @param checkout if true the node is checked out. + * @param created create time of the new version (if any), + * or null for the current time + * @return the node id of the base version or null for a pure + * checkout. + * @throws RepositoryException if an error occurs + */ + protected NodeId checkoutCheckin( + NodeStateEx state, + boolean checkin, boolean checkout, Calendar created) + throws RepositoryException { + assert(checkin || checkout); + + // check if versionable + boolean isFull = checkVersionable(state); + + // check flags + if (isCheckedOut(state)) { + if (checkout && !checkin) { + // pure checkout + String msg = safeGetJCRPath(state) + ": Node is already checked-out. ignoring."; + log.debug(msg); + return null; + } + } else { + if (!checkout) { + // pure checkin + String msg = safeGetJCRPath(state) + ": Node is already checked-in. ignoring."; + log.debug(msg); + if (isFull) { + return getBaseVersionId(state); + } else { + // get base version from version history + return vMgr.getHeadVersionOfNode(state.getNodeId()).getId(); + } + } + checkin = false; + } + + NodeId baseId = isFull && checkout + ? vMgr.canCheckout(state, currentActivity) + : null; + + // perform operation + WriteOperation ops = startWriteOperation(); + try { + // the 2 cases could be consolidated but is clearer this way + if (checkin) { + // check for configuration + if (state.getEffectiveNodeType().includesNodeType(NameConstants.NT_CONFIGURATION)) { + // collect the base versions and the the rep:versions property of the configuration + Set baseVersions = collectBaseVersions(state); + InternalValue[] vs = new InternalValue[baseVersions.size()]; + int i=0; + for (NodeId id: baseVersions) { + vs[i++] = InternalValue.create(id); + } + state.setPropertyValues(NameConstants.REP_VERSIONS, PropertyType.REFERENCE, vs); + state.store(); + } + InternalVersion v = vMgr.checkin(session, state, created); + baseId = v.getId(); + if (isFull) { + state.setPropertyValue( + NameConstants.JCR_BASEVERSION, + InternalValue.create(baseId)); + state.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY); + state.removeProperty(NameConstants.JCR_ACTIVITY); + } + } + if (checkout) { + if (isFull) { + state.setPropertyValues( + NameConstants.JCR_PREDECESSORS, + PropertyType.REFERENCE, + new InternalValue[]{InternalValue.create(baseId)} + ); + if (currentActivity != null) { + state.setPropertyValue( + NameConstants.JCR_ACTIVITY, + InternalValue.create(currentActivity) + ); + } + } + } + state.setPropertyValue(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(checkout)); + state.store(); + ops.save(); + return baseId; + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + + /** + * Collects the base versions for the workspace configuration referenced by + * the given config node. + * @param config the config + * @return the id of the new base version + * @throws RepositoryException if an error occurs + */ + private Set collectBaseVersions(NodeStateEx config) throws RepositoryException { + NodeId rootId = config.getPropertyValue(NameConstants.JCR_ROOT).getNodeId(); + NodeStateEx root = getNodeStateEx(rootId); + if (root == null) { + String msg = "Configuration root node for " + safeGetJCRPath(config) + " not found."; + log.error(msg); + throw new ItemNotFoundException(msg); + } + Set baseVersions = new HashSet(); + collectBaseVersions(root, baseVersions); + return baseVersions; + } + + /** + * Recursively collects all base versions of this configuration tree. + * + * @param root node to traverse + * @param baseVersions set of base versions to fill + * @throws RepositoryException if an error occurs + */ + private void collectBaseVersions(NodeStateEx root, Set baseVersions) + throws RepositoryException { + if (!baseVersions.isEmpty()) { + // base version of configuration root already recorded + if (root.hasProperty(NameConstants.JCR_CONFIGURATION) + && root.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) { + // don't traverse into child nodes that have a jcr:configuration + // property as they belong to a different configuration. + return; + } + } + InternalVersion baseVersion = getBaseVersion(root); + if (baseVersion.isRootVersion()) { + String msg = "Unable to checkin configuration as it has unversioned child node: " + safeGetJCRPath(root); + log.debug(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + baseVersions.add(baseVersion.getId()); + + for (NodeStateEx child: root.getChildNodes()) { + collectBaseVersions(child, baseVersions); + } + } + + /** + * Checks if the underlying node is versionable, i.e. has 'mix:versionable' or a + * 'mix:simpleVersionable'. + * @param state node state + * @return true if this node is full versionable, i.e. is + * of nodetype mix:versionable + * @throws UnsupportedRepositoryOperationException if this node is not versionable at all + */ + protected boolean checkVersionable(NodeStateEx state) + throws UnsupportedRepositoryOperationException, RepositoryException { + if (state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) { + return true; + } else if (state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_SIMPLE_VERSIONABLE)) { + return false; + } else { + String msg = "Unable to perform a versioning operation on a " + + "non versionable node: " + safeGetJCRPath(state); + log.debug(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + } + + /** + * Returns the JCR path for the given node state without throwing an exception. + * @param state node state + * @return a JCR path string + */ + protected String safeGetJCRPath(NodeStateEx state) { + Path path; + try { + path = hierMgr.getPath(state.getNodeId()); + } catch (RepositoryException e) { + log.warn("unable to calculate path for {}", state.getNodeId()); + return state.getNodeId().toString(); + } + try { + return session.getJCRPath(path); + } catch (NamespaceException e) { + log.warn("unable to calculate path for {}", path); + return path.toString(); + } + } + + /** + * Determines the checked-out status of the given node state. + *

    + * A node is considered checked-out if it is versionable and + * checked-out, or is non-versionable but its nearest versionable ancestor + * is checked-out, or is non-versionable and there are no versionable + * ancestors. + * + * @param state node state + * @return a boolean + * @see javax.jcr.version.VersionManager#isCheckedOut(String) + * @see Node#isCheckedOut() + * @throws RepositoryException if an error occurs + */ + protected boolean isCheckedOut(NodeStateEx state) throws RepositoryException { + return state.getPropertyValue(NameConstants.JCR_ISCHECKEDOUT).getBoolean(); + } + + /** + * Returns the node id of the base version, retrieved from the node state + * @param state node state + * @return the node id of the base version or null if not defined + */ + protected NodeId getBaseVersionId(NodeStateEx state) { + InternalValue value = state.getPropertyValue(NameConstants.JCR_BASEVERSION); + return value == null ? null : value.getNodeId(); + } + + /** + * Returns the internal version history for the underlying node. + * @param state node state + * @return internal version history + * @throws RepositoryException if an error occurs + */ + protected InternalVersionHistory getVersionHistory(NodeStateEx state) + throws RepositoryException { + boolean isFull = checkVersionable(state); + if (isFull) { + NodeId id = state.getPropertyValue(NameConstants.JCR_VERSIONHISTORY).getNodeId(); + return vMgr.getVersionHistory(id); + } else { + return vMgr.getVersionHistoryOfNode(state.getNodeId()); + } + } + + /** + * helper class that returns the internal version for a JCR one. + * @param v the jcr version + * @return the internal version + * @throws RepositoryException if an error occurs + */ + protected InternalVersion getVersion(Version v) throws RepositoryException { + if (v == null) { + return null; + } else { + return vMgr.getVersion(((VersionImpl) v).getNodeId()); + } + } + + /** + * Returns the internal base version for the underlying node. + * @param state node state + * @return internal base version + * @throws RepositoryException if an error occurs + */ + protected InternalVersion getBaseVersion(NodeStateEx state) throws RepositoryException { + boolean isFull = checkVersionable(state); + if (isFull) { + NodeId id = getBaseVersionId(state); + return vMgr.getVersion(id); + } else { + // note, that the method currently only works for linear version + // graphs (i.e. simple versioning) + return vMgr.getHeadVersionOfNode(state.getNodeId()); + } + } + + /** + * returns the node state for the given node id + * @param nodeId the node id + * @throws RepositoryException if an error occurs + * @return the node state or null if not found + */ + protected NodeStateEx getNodeStateEx(NodeId nodeId) throws RepositoryException { + if (!stateMgr.hasItemState(nodeId)) { + return null; + } + try { + return new NodeStateEx( + stateMgr, + ntReg, + (NodeState) stateMgr.getItemState(nodeId), + null); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } + } + + /** + * Checks modify and permissions + * @param state state to check + * @param options options to check + * @param permissions permissions to check + * @throws RepositoryException if an error occurs + */ + protected void checkModify(NodeStateEx state, int options, int permissions) + throws RepositoryException { + NodeImpl node; + try { + node = session.getNodeById(state.getNodeId()); + } catch (RepositoryException e) { + // ignore + return; + } + context.getItemValidator().checkModify(node, options, permissions); + } + + /** + * Checks modify and permissions + * @param node node to check + * @param options options to check + * @param permissions permissions to check + * @throws RepositoryException if an error occurs + */ + protected void checkModify(NodeImpl node, int options, int permissions) + throws RepositoryException { + context.getItemValidator().checkModify(node, options, permissions); + } + + /** + * Helper for managing write operations. + */ + public class WriteOperation { + + /** + * Flag for successful completion of the write operation. + */ + private boolean success = false; + + private final VersioningLock.WriteLock lock; + + public WriteOperation(VersioningLock.WriteLock lock) { + this.lock = lock; + } + + /** + * Saves the pending operations in the {@link LocalItemStateManager}. + * + * @throws ItemStateException if the pending state is invalid + * @throws RepositoryException if the pending state could not be persisted + */ + public void save() throws ItemStateException, RepositoryException { + stateMgr.update(); + success = true; + } + + /** + * Closes the write operation. The pending operations are cancelled + * if they could not be properly saved. Finally the write lock is + * released. + */ + public void close() { + try { + if (!success) { + // update operation failed, cancel all modifications + stateMgr.cancel(); + } + } finally { + lock.release(); + } + } + } + + /** + * Acquires the write lock on this version manager. + * @return returns the write lock + */ + protected VersioningLock.WriteLock acquireWriteLock() { + while (true) { + try { + return rwLock.acquireWriteLock(); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * acquires the read lock on this version manager. + * @return returns the read lock + */ + protected VersioningLock.ReadLock acquireReadLock() { + while (true) { + try { + return rwLock.acquireReadLock(); + } catch (InterruptedException e) { + // ignore + } + } + } + + + /** + * Starts a write operation by acquiring the write lock and setting the + * item state manager to the "edit" state. If something goes wrong, the + * write lock is released and an exception is thrown. + *

    + * The pattern for using this method and the returned helper instance is: + *

    +     *     WriteOperation operation = startWriteOperation();
    +     *     try {
    +     *         ...
    +     *         operation.save(); // if everything is OK
    +     *         ...
    +     *     } catch (...) {
    +     *         ...
    +     *     } finally {
    +     *         operation.close();
    +     *     }
    +     * 
    + * + * @return write operation helper + * @throws RepositoryException if the write operation could not be started + */ + public WriteOperation startWriteOperation() throws RepositoryException { + boolean success = false; + VersioningLock.WriteLock lock = acquireWriteLock(); + try { + stateMgr.edit(); + success = true; + return new WriteOperation(lock); + } catch (IllegalStateException e) { + String msg = "Unable to start edit operation."; + throw new RepositoryException(msg, e); + } finally { + if (!success) { + lock.release(); + } + } + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplConfig.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplConfig.java new file mode 100644 index 00000000000..56c86b46473 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplConfig.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.core.version; + +import java.util.Set; +import java.util.HashSet; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.UpdatableItemStateManager; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +/** + * The JCR Version Manager implementation is split in several classes in order to + * group related methods together. + *

    + * this class provides methods for the configuration and baselines related operations. + *

    + * Implementation note: methods starting with "internal" are considered to be + * executed within a "write operations" block. + */ +abstract public class VersionManagerImplConfig extends VersionManagerImplMerge { + + /** + * default logger + */ + private static final Logger log = LoggerFactory.getLogger(VersionManagerImplConfig.class); + + /** + * Creates a new version manager for the given session + * + * @param context component context of the current session + * @param stateMgr the underlying state manager + * @param hierMgr local hierarchy manager + */ + protected VersionManagerImplConfig( + SessionContext context, UpdatableItemStateManager stateMgr, + HierarchyManager hierMgr) { + super(context, stateMgr, hierMgr); + } + + /** + * Restores the versions recorded in the given baseline below the specified + * path. + * @param parent the parent state + * @param name the name of the new node (tree) + * @param baseline the baseline that recorded the versions + * @return the node id of the configuration + * @throws RepositoryException if an error occurs + */ + protected NodeId restore(NodeStateEx parent, Name name, + InternalBaseline baseline) + throws RepositoryException { + // check if nt:configuration exists + NodeId configId = baseline.getConfigurationId(); + NodeId rootId = baseline.getConfigurationRootId(); + if (stateMgr.hasItemState(rootId)) { + NodeStateEx existing = parent.getNode(rootId); + String msg = "Configuration for the given baseline already exists at: " + safeGetJCRPath(existing); + log.error(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + + // find version for configuration root + VersionSet versions = baseline.getBaseVersions(); + InternalVersion rootVersion = null; + for (InternalVersion v: versions.versions().values()) { + if (v.getVersionHistory().getVersionableId().equals(rootId)) { + rootVersion = v; + break; + } + } + if (rootVersion == null) { + String msg = "Internal error: supplied baseline has no version for its configuration root."; + log.error(msg); + throw new RepositoryException(msg); + } + + // create new node below parent + WriteOperation ops = startWriteOperation(); + try { + if (!stateMgr.hasItemState(configId)) { + // create if nt:configuration node is not exists + internalCreateConfiguration(rootId, configId, baseline.getId()); + } + NodeStateEx config = parent.getNode(configId); + + // create the root node so that the restore works + InternalFrozenNode fn = rootVersion.getFrozenNode(); + NodeStateEx state = parent.addNode(name, fn.getFrozenPrimaryType(), fn.getFrozenId()); + state.setMixins(fn.getFrozenMixinTypes()); + parent.store(); + + // and finally restore the config and root + internalRestore(config, baseline, null, false); + + ops.save(); + return configId; + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + + /** + * Creates a new configuration node. + *

    + * The nt:confguration is stored within the nt:configurations storage using + * the nodeid of the configuration root (rootId) as path. + * + * @param state the node of the workspace configuration + * @return the node id of the created configuration + * @throws RepositoryException if an error occurs + */ + protected NodeId createConfiguration(NodeStateEx state) + throws RepositoryException { + + WriteOperation ops = startWriteOperation(); + try { + NodeId configId = internalCreateConfiguration(state.getNodeId(), null, null); + + // set configuration reference in state + state.setPropertyValue(NameConstants.JCR_CONFIGURATION, InternalValue.create(configId)); + state.store(); + ops.save(); + return configId; + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + + /** + * Creates a new configuration node. + *

    + * The nt:confguration is stored within the nt:configurations storage using + * the nodeid of the configuration root (rootId) as path. + * + * @param rootId the id of the configuration root node + * @param configId the id of the configuration node + * @param baseLine id of the baseline version or null + * @return the node id of the created configuration + * @throws RepositoryException if an error occurs + */ + private NodeId internalCreateConfiguration(NodeId rootId, + NodeId configId, NodeId baseLine) + throws RepositoryException { + + NodeStateEx configRoot = internalGetConfigRoot(); + NodeStateEx configParent = InternalVersionManagerBase.getParentNode( + configRoot, + rootId.toString(), + NameConstants.REP_CONFIGURATIONS); + Name name = InternalVersionManagerBase.getName(rootId.toString()); + + if (configId == null) { + configId = context.getNodeIdFactory().newNodeId(); + } + NodeStateEx config = configParent.addNode(name, NameConstants.NT_CONFIGURATION, configId, true); + Set mix = new HashSet(); + mix.add(NameConstants.REP_VERSION_REFERENCE); + config.setMixins(mix); + config.setPropertyValue(NameConstants.JCR_ROOT, InternalValue.create(rootId)); + + // init mix:versionable flags + VersionHistoryInfo vh = vMgr.getVersionHistory(session, config.getState(), null); + + // and set the base version and history to the config + InternalValue historyId = InternalValue.create(vh.getVersionHistoryId()); + InternalValue versionId = InternalValue.create( + baseLine == null ? vh.getRootVersionId() : baseLine); + + config.setPropertyValue(NameConstants.JCR_BASEVERSION, versionId); + config.setPropertyValue(NameConstants.JCR_VERSIONHISTORY, historyId); + config.setPropertyValue(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(true)); + config.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, new InternalValue[]{versionId}); + configParent.store(); + + return configId; + } + + /** + * Returns the root node of the configurations storage located at + * "/jcr:system/jcr:configurations" + * + * @return the root node + * @throws RepositoryException if an error occurs + */ + private NodeStateEx internalGetConfigRoot() throws RepositoryException { + NodeStateEx system = getNodeStateEx(RepositoryImpl.SYSTEM_ROOT_NODE_ID); + NodeStateEx root = system.getNode(NameConstants.JCR_CONFIGURATIONS, 1); + if (root == null) { + root = system.addNode( + NameConstants.JCR_CONFIGURATIONS, + NameConstants.REP_CONFIGURATIONS, + RepositoryImpl.CONFIGURATIONS_NODE_ID, false); + system.store(); + } + return root; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplMerge.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplMerge.java new file mode 100644 index 00000000000..ec1e2dbb469 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplMerge.java @@ -0,0 +1,523 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.MergeException; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.ItemValidator; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.UpdatableItemStateManager; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The JCR Version Manager implementation is split in several classes in order to + * group related methods together. + *

    + * This class provides methods for the merge operations. + */ +abstract public class VersionManagerImplMerge extends VersionManagerImplRestore { + + /** + * default logger + */ + private static final Logger log = LoggerFactory.getLogger(VersionManagerImplMerge.class); + + /** + * Creates a new version manager for the given session + * + * @param context component context of the current session + * @param stateMgr the underlying state manager + * @param hierMgr local hierarchy manager + */ + protected VersionManagerImplMerge( + SessionContext context, UpdatableItemStateManager stateMgr, + HierarchyManager hierMgr) { + super(context, stateMgr, hierMgr); + } + + /** + * Merges/Updates this node with its corresponding ones + * + * @param state state to merge or update + * @param srcRoot src workspace root node + * @param failedIds list of failed ids + * @param bestEffort best effort flag + * @param shallow is shallow flag + * @throws RepositoryException if an error occurs + * @throws ItemStateException if an error occurs + */ + protected void merge(NodeStateEx state, NodeStateEx srcRoot, + List failedIds, + boolean bestEffort, boolean shallow) + throws RepositoryException, ItemStateException { + + if (shallow) { + // If isShallow is true and this node is not + // versionable, then this method returns and no changes are made. + if (!state.getEffectiveNodeType().includesNodeType( + NameConstants.MIX_SIMPLE_VERSIONABLE)) { + return; + } + } + NodeStateEx srcNode = getCorrespondingNode(state, srcRoot); + if (srcNode == null) { + // If this node (the one on which merge is called) does not have a corresponding + // node in the indicated workspace, then the merge method returns quietly and no + // changes are made. + return; + } + WriteOperation ops = startWriteOperation(); + try { + internalMerge(state, srcRoot, failedIds, bestEffort, shallow); + state.store(); + ops.save(); + } finally { + ops.close(); + } + } + + /** + * Merges/Updates this node with its corresponding ones + * + * @param state state to merge or update + * @param srcRoot src workspace root node + * @param failedIds list of failed ids + * @param bestEffort best effort flag + * @param shallow is shallow flag + * @throws RepositoryException if an error occurs + * @throws ItemStateException if an error occurs + */ + private void internalMerge(NodeStateEx state, NodeStateEx srcRoot, + List failedIds, + boolean bestEffort, boolean shallow) + throws RepositoryException, ItemStateException { + + NodeStateEx srcNode = doMergeTest(state, srcRoot, failedIds, bestEffort); + if (srcNode == null) { + if (!shallow) { + // leave, iterate over children, but ignore non-versionable child + // nodes (see JCR-1046) + for (NodeStateEx n: state.getChildNodes()) { + if (n.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) { + internalMerge(n, srcRoot, failedIds, bestEffort, shallow); + } + } + } + return; + } + + // check lock and hold status if node exists + checkModify(state, ItemValidator.CHECK_LOCK | ItemValidator.CHECK_HOLD, Permission.NONE); + + // remove all properties that are not present in srcNode + for (PropertyState prop: state.getProperties()) { + if (!srcNode.hasProperty(prop.getName())) { + state.removeProperty(prop.getName()); + } + } + + // update all properties from the src node + for (PropertyState prop: srcNode.getProperties()) { + Name propName = prop.getName(); + // ignore system types + if (propName.equals(NameConstants.JCR_PRIMARYTYPE) + || propName.equals(NameConstants.JCR_MIXINTYPES) + || propName.equals(NameConstants.JCR_UUID)) { + continue; + } + state.copyFrom(prop); + } + + // update the mixin types + state.setMixins(srcNode.getState().getMixinTypeNames()); + + // remove the child nodes in N but not in N' + LinkedList toDelete = new LinkedList(); + for (ChildNodeEntry entry: state.getState().getChildNodeEntries()) { + if (!srcNode.getState().hasChildNodeEntry(entry.getName(), entry.getIndex())) { + toDelete.add(entry); + } + } + for (ChildNodeEntry entry: toDelete) { + state.removeNode(entry.getName(), entry.getIndex()); + } + state.store(); + + // add source ones + for (ChildNodeEntry entry: srcNode.getState().getChildNodeEntries()) { + NodeStateEx child = state.getNode(entry.getName(), entry.getIndex()); + if (child == null) { + // if destination workspace already has such an node, remove it + if (state.hasNode(entry.getId())) { + child = state.getNode(entry.getId()); + NodeStateEx parent = child.getParent(); + parent.removeNode(child); + parent.store(); + } + // create new child + NodeStateEx srcChild = srcNode.getNode(entry.getId()); + child = state.addNode(entry.getName(), srcChild.getState().getNodeTypeName(), srcChild.getNodeId()); + child.setMixins(srcChild.getState().getMixinTypeNames()); + // copy src child + state.store(); + internalMerge(child, srcRoot, null, bestEffort, false); + } else if (!shallow) { + // recursively merge + internalMerge(child, srcRoot, failedIds, bestEffort, false); + + } + } + } + + /** + * Returns the corresponding node in the workspace of the given session. + *

    + * Given a node N1 in workspace W1, its corresponding node N2 in workspace + * W2 is defined as follows: + *

      + *
    • If N1 is the root node of W1 then N2 is the root node of W2. + *
    • If N1 is referenceable (has a UUID) then N2 is the node in W2 with + * the same UUID. + *
    • If N1 is not referenceable (does not have a UUID) then there is some + * node M1 which is either the nearest ancestor of N1 that is + * referenceable, or is the root node of W1. If the corresponding node + * of M1 is M2 in W2, then N2 is the node with the same relative path + * from M2 as N1 has from M1. + *
    + * + * @param state N1 + * @param srcRoot the root node state of W2 + * @return the corresponding node or null if no corresponding + * node exists. + * @throws RepositoryException If another error occurs. + */ + private NodeStateEx getCorrespondingNode(NodeStateEx state, NodeStateEx srcRoot) + throws RepositoryException { + + // search nearest ancestor that is referenceable + NodeStateEx m1 = state; + LinkedList elements = new LinkedList(); + while (m1.getParentId() != null && + !m1.getEffectiveNodeType().includesNodeType(NameConstants.MIX_REFERENCEABLE)) { + NodeStateEx parent = m1.getParent(); + elements.addFirst(parent.getState().getChildNodeEntry(m1.getNodeId())); + m1 = parent; + } + + // check if corresponding ancestor exists + if (srcRoot.hasNode(m1.getNodeId())) { + NodeStateEx m2 = srcRoot.getNode(m1.getNodeId()); + Iterator iter = elements.iterator(); + while (iter.hasNext() && m2 != null) { + ChildNodeEntry e = iter.next(); + m2 = m2.getNode(e.getName(), e.getIndex()); + } + return m2; + } else { + return null; + } + } + + /** + * Performs the merge test. If the result is 'update', then the corresponding + * source node is returned. if the result is 'leave' or 'besteffort-fail' + * then null is returned. If the result of the merge test is + * 'fail' with bestEffort set to false a MergeException is + * thrown. + *

    + * jsr170 - 8.2.10 Merge: + * [...] + *

      + *
    • If N is currently checked-in then:
    • + *
        + *
      • If V' is a successor (to any degree) of V, then the merge result + * for N is update. + *
      • + *
      • If V' is a predecessor (to any degree) of V or if V and + * V' are identical (i.e., are actually the same version), + * then the merge result for N is leave. + *
      • + *
      • If V is neither a successor of, predecessor of, nor + * identical with V', then the merge result for N is failed. + * This is the case where N and N' represent divergent + * branches of the version graph, thus determining the + * result of a merge is non-trivial. + *
      • + *
      + *
    • If N is currently checked-out then:
    • + *
        + *
      • If V' is a predecessor (to any degree) of V or if V and + * V' are identical (i.e., are actually the same version), + * then the merge result for N is leave. + *
      • + *
      • If any other relationship holds between V and V', + * then the merge result for N is fail. + *
      • + *
      + *
    + * + * @param state state to test + * @param srcRoot root node state of the source workspace + * @param failedIds the list to store the failed node ids. + * @param bestEffort the best effort flag + * @return the corresponding source node or null + * @throws RepositoryException if an error occurs. + * @throws AccessDeniedException if access is denied + */ + private NodeStateEx doMergeTest(NodeStateEx state, NodeStateEx srcRoot, List failedIds, boolean bestEffort) + throws RepositoryException, AccessDeniedException { + + // If N does not have a corresponding node then the merge result for N is leave. + NodeStateEx srcNode = getCorrespondingNode(state, srcRoot); + if (srcNode == null) { + return null; + } + + // if not versionable, update + if (!state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE) || failedIds == null) { + return srcNode; + } + // if source node is not versionable, leave + if (!srcNode.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) { + return null; + } + // test versions. the following code could be simplified but is + // intentionally expanded for follow the spec. + InternalVersion v = getBaseVersion(state); + InternalVersion vp = getBaseVersion(srcNode); + if (!isCheckedOut(state)) { + // If N is currently checked-in then: + if (vp.isMoreRecent(v)) { + // - If V' is an eventual successor of V, then the merge result for N is update. + return srcNode; + } else if (v.equals(vp) || v.isMoreRecent(vp)) { + // - If V' is an eventual predecessor of V or if V and V' are identical (i.e., are + // actually the same version), then the merge result for N is leave. + return null; + } + // - If V is neither an eventual successor of, eventual predecessor of, nor + // identical with V', then the merge result for N is failed. This is the case + // where N and N' represent divergent branches of the version graph. + + // failed is covered below + } else { + // If N is currently checked-out then: + if (v.equals(vp) || v.isMoreRecent(vp)) { + // - If V' is an eventual predecessor of V or if V and V' are identical (i.e., are + // actually the same version), then the merge result for + // N is leave. + return null; + } + // - If any other relationship holds between V and V', then the merge result + // for N is fail. + + // failed is covered below + } + + + if (vp.isMoreRecent(v) && !isCheckedOut(state)) { + // I f V' is a successor (to any degree) of V, then the merge result for + // N is update. This case can be thought of as the case where N' is + // "newer" than N and therefore N should be updated to reflect N'. + return srcNode; + } else if (v.equals(vp) || v.isMoreRecent(vp)) { + // If V' is a predecessor (to any degree) of V or if V and V' are + // identical (i.e., are actually the same version), then the merge + // result for N is leave. This case can be thought of as the case where + // N' is "older" or the "same age" as N and therefore N should be left alone. + return null; + } else { + // If V is neither a successor of, predecessor of, nor identical + // with V', then the merge result for N is failed. This is the case + // where N and N' represent divergent branches of the version graph, + // thus determining the result of a merge is non-trivial. + if (bestEffort) { + // add 'offending' version to jcr:mergeFailed property + Set set = getMergeFailed(state); + set.add(vp.getId()); + setMergeFailed(state, set); + failedIds.add(state.getNodeId()); + state.store(); + return null; + } else { + String msg = + "Unable to merge nodes. Violating versions. " + safeGetJCRPath(state); + log.debug(msg); + throw new MergeException(msg); + } + } + } + + /** + * Perform {@link Node#cancelMerge(Version)} or {@link Node#doneMerge(Version)} + * depending on the value of cancel. + * @param state state to finish + * @param version version + * @param cancel flag inidicates if this is a cancel operation + * @throws RepositoryException if an error occurs + */ + protected void finishMerge(NodeStateEx state, Version version, boolean cancel) + throws RepositoryException { + // check versionable + if (!checkVersionable(state)) { + throw new UnsupportedRepositoryOperationException("Node not full versionable: " + safeGetJCRPath(state)); + } + + // check if version is in mergeFailed list + Set failed = getMergeFailed(state); + NodeId versionId = ((VersionImpl) version).getNodeId(); + if (!failed.remove(versionId)) { + String msg = + "Unable to finish merge. Specified version is not in" + + " jcr:mergeFailed property: " + safeGetJCRPath(state); + log.error(msg); + throw new VersionException(msg); + } + + WriteOperation ops = startWriteOperation(); + try { + // remove version from mergeFailed list + setMergeFailed(state, failed); + + if (!cancel) { + // add version to jcr:predecessors list + InternalValue[] vals = state.getPropertyValues(NameConstants.JCR_PREDECESSORS); + InternalValue[] v = new InternalValue[vals.length + 1]; + for (int i = 0; i < vals.length; i++) { + v[i] = InternalValue.create(vals[i].getNodeId()); + } + v[vals.length] = InternalValue.create(versionId); + state.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, v, true); + } + state.store(); + ops.save(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + + /** + * Returns a set of nodeid from the jcr:mergeFailed property of the given state + * @param state the state + * @return set of node ids + */ + private Set getMergeFailed(NodeStateEx state) { + Set set = new HashSet(); + if (state.hasProperty(NameConstants.JCR_MERGEFAILED)) { + InternalValue[] vals = state.getPropertyValues(NameConstants.JCR_MERGEFAILED); + for (InternalValue val : vals) { + set.add(val.getNodeId()); + } + } + return set; + } + + /** + * Updates the merge failed property of the given state/ + * @param state the state to update + * @param set the set of ids + * @throws RepositoryException if an error occurs + */ + private void setMergeFailed(NodeStateEx state, Set set) + throws RepositoryException { + if (set.isEmpty()) { + state.removeProperty(NameConstants.JCR_MERGEFAILED); + } else { + InternalValue[] vals = new InternalValue[set.size()]; + Iterator iter = set.iterator(); + int i = 0; + while (iter.hasNext()) { + NodeId id = iter.next(); + vals[i++] = InternalValue.create(id); + } + state.setPropertyValues(NameConstants.JCR_MERGEFAILED, PropertyType.REFERENCE, vals, true); + } + } + + /** + * Merge the given activity to this workspace + * + * @param activity internal activity + * @param failedIds list of failed ids + * @throws RepositoryException if an error occurs + */ + protected void merge(InternalActivity activity, List failedIds) + throws RepositoryException { + + VersionSet changeSet = activity.getChangeSet(); + WriteOperation ops = startWriteOperation(); + try { + Iterator iter = changeSet.versions().keySet().iterator(); + while (iter.hasNext()) { + InternalVersion v = changeSet.versions().remove(iter.next()); + NodeStateEx state = getNodeStateEx(v.getFrozenNode().getFrozenId()); + if (state != null) { + InternalVersion base = getBaseVersion(state); + // if base version is newer than version, add to failed list + // but merge it anyways + if (base.isMoreRecent(v)) { + failedIds.add(state.getNodeId()); + // add it to the jcr:mergeFailed property + Set set = getMergeFailed(state); + set.add(base.getId()); + setMergeFailed(state, set); + state.store(); + } else { + for (InternalVersion restored: internalRestore(state, v, changeSet, true)) { + changeSet.versions().remove(restored.getVersionHistory().getId()); + } + } + } + + // reset iterator + iter = changeSet.versions().keySet().iterator(); + } + ops.save(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplRestore.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplRestore.java new file mode 100644 index 00000000000..1082f0d0c04 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionManagerImplRestore.java @@ -0,0 +1,682 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import java.util.*; + +import javax.jcr.ItemExistsException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.ItemValidator; +import org.apache.jackrabbit.core.NodeTypeInstanceHandler; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.state.UpdatableItemStateManager; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The JCR Version Manager implementation is split in several classes in order to + * group related methods together. + *

    + * this class provides methods for the restore operations. + *

    + * Implementation note: methods starting with "internal" are considered to be + * executed within a "write operations" block. + */ +abstract public class VersionManagerImplRestore extends VersionManagerImplBase { + + /** + * default logger + */ + private static final Logger log = LoggerFactory.getLogger(VersionManagerImplRestore.class); + + /** + * Creates a new version manager for the given session + * + * @param context component context of the current session + * @param stateMgr the underlying state manager + * @param hierMgr local hierarchy manager + */ + protected VersionManagerImplRestore( + SessionContext context, UpdatableItemStateManager stateMgr, + HierarchyManager hierMgr) { + super(context, stateMgr, hierMgr); + } + + /** + * @param state the state to restore + * @param v the version to restore + * @param removeExisting remove existing flag + * @throws RepositoryException if an error occurs + * + * @see javax.jcr.version.VersionManager#restore(String, Version, boolean) + */ + protected void restore(NodeStateEx state, InternalVersion v, boolean removeExisting) + throws RepositoryException { + checkVersionable(state); + + // check if 'own' version + if (!v.getVersionHistory().equals(getVersionHistory(state))) { + String msg = "Unable to restore version. Not same version history."; + log.error(msg); + throw new VersionException(msg); + } + WriteOperation ops = startWriteOperation(); + try { + internalRestore(state, v, new DateVersionSelector(v.getCreated()), removeExisting); + ops.save(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + + /** + * @param state the state to restore + * @param versionName the name of the version to restore + * @param removeExisting remove existing flag + * @throws RepositoryException if an error occurs + * + * @see VersionManager#restore(String, String, boolean) + */ + protected void restore(NodeStateEx state, Name versionName, boolean removeExisting) + throws RepositoryException { + checkVersionable(state); + InternalVersion v = getVersionHistory(state).getVersion(versionName); + DateVersionSelector gvs = new DateVersionSelector(v.getCreated()); + WriteOperation ops = startWriteOperation(); + try { + internalRestore(state, v, gvs, removeExisting); + ops.save(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + + /** + * @param state the state to restore + * @param versionLabel the name of the version to restore + * @param removeExisting remove existing flag + * @throws RepositoryException if an error occurs + * + * @see VersionManager#restoreByLabel(String, String, boolean) + */ + protected void restoreByLabel(NodeStateEx state, Name versionLabel, boolean removeExisting) + throws RepositoryException { + checkVersionable(state); + InternalVersion v = getVersionHistory(state).getVersionByLabel(versionLabel); + if (v == null) { + String msg = "No version for label " + versionLabel + " found."; + log.error(msg); + throw new VersionException(msg); + } + WriteOperation ops = startWriteOperation(); + try { + internalRestore(state, v, new LabelVersionSelector(versionLabel), removeExisting); + ops.save(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + + /** + * Restores the version below the parent node + * using the indicated name + * + * @param parent parent node + * @param name desired name + * @param v version to restore + * @param removeExisting remove exiting flag + * @throws RepositoryException if an error occurs + */ + protected void restore(NodeStateEx parent, Name name, InternalVersion v, + boolean removeExisting) + throws RepositoryException { + // check if versionable node exists + InternalFrozenNode fn = v.getFrozenNode(); + if (stateMgr.hasItemState(fn.getFrozenId())) { + if (removeExisting) { + NodeStateEx existing = parent.getNode(fn.getFrozenId()); + checkVersionable(existing); + + // move versionable node below this one using the given "name" + WriteOperation ops = startWriteOperation(); + try { + NodeStateEx exParent = existing.getParent(); + NodeStateEx state = parent.moveFrom(existing, name, false); + exParent.store(); + parent.store(); + // and restore it + internalRestore(state, v, new DateVersionSelector(v.getCreated()), removeExisting); + ops.save(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } else { + String msg = "Unable to restore version. Versionable node already exists."; + log.error(msg); + throw new ItemExistsException(msg); + } + } else { + WriteOperation ops = startWriteOperation(); + try { + // create new node below parent + NodeStateEx state = parent.addNode(name, fn.getFrozenPrimaryType(), fn.getFrozenId()); + state.setMixins(fn.getFrozenMixinTypes()); + internalRestore(state, v, new DateVersionSelector(v.getCreated()), removeExisting); + parent.store(); + ops.save(); + } catch (ItemStateException e) { + throw new RepositoryException(e); + } finally { + ops.close(); + } + } + } + + /** + * @param versions Versions to restore + * @param removeExisting remove existing flag + * @throws RepositoryException if an error occurs + * @throws ItemStateException if an error occurs + * + * @see VersionManager#restore(Version[], boolean) + * @see VersionManager#restore(Version, boolean) + */ + protected void internalRestore(VersionSet versions, boolean removeExisting) + throws RepositoryException, ItemStateException { + // now restore all versions that have a node in the workspace + int numRestored = 0; + while (versions.versions().size() > 0) { + Set restored = null; + for (InternalVersion v : versions.versions().values()) { + NodeStateEx state = getNodeStateEx(v.getFrozenNode().getFrozenId()); + if (state != null) { + // todo: check should operate on workspace states, too + int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_HOLD; + checkModify(state, options, Permission.NONE); + restored = internalRestore(state, v, versions, removeExisting); + // remove restored versions from set + for (InternalVersion r : restored) { + versions.versions().remove(r.getVersionHistory().getId()); + } + numRestored += restored.size(); + break; + } + } + if (restored == null) { + String msg = numRestored == 0 + ? "Unable to restore. At least one version needs existing versionable node in workspace." + : "Unable to restore. All versions with non existing versionable nodes need parent."; + log.error(msg); + throw new VersionException(msg); + } + } + } + + /** + * Internal method to restore a version. + * + * @param state the state to restore + * @param version version to restore + * @param vsel the version selector that will select the correct version for + * OPV=Version child nodes. + * @param removeExisting remove existing flag + * @return set of restored versions + * @throws RepositoryException if an error occurs + * @throws ItemStateException if an error occurs + */ + protected Set internalRestore(NodeStateEx state, + InternalVersion version, + VersionSelector vsel, + boolean removeExisting) + throws RepositoryException, ItemStateException { + + // fail if root version + if (version.isRootVersion()) { + String msg = "Restore of root version not allowed."; + log.error(msg); + throw new VersionException(msg); + } + + boolean isFull = checkVersionable(state); + + // check permission + Path path = hierMgr.getPath(state.getNodeId()); + session.getAccessManager().checkPermission(path, Permission.VERSION_MNGMT); + + // 1. The child node and properties of N will be changed, removed or + // added to, depending on their corresponding copies in V and their + // own OnParentVersion attributes (see 7.2.8, below, for details). + Set restored = new HashSet(); + internalRestoreFrozen(state, version.getFrozenNode(), vsel, restored, removeExisting, false); + restored.add(version); + + if (isFull) { + // 2. N's jcr:baseVersion property will be changed to point to V. + state.setPropertyValue( + NameConstants.JCR_BASEVERSION, InternalValue.create(version.getId())); + + // 4. N's jcr:predecessor property is set to null + state.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, InternalValue.EMPTY_ARRAY); + + // set version history + state.setPropertyValue(NameConstants.JCR_VERSIONHISTORY, InternalValue.create(version.getVersionHistory().getId())); + + // also clear mergeFailed + state.removeProperty(NameConstants.JCR_MERGEFAILED); + + } else { + // with simple versioning, the node is checked in automatically, + // thus not allowing any branches + vMgr.checkin(session, state, null); + } + // 3. N's jcr:isCheckedOut property is set to false. + state.setPropertyValue(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(false)); + state.store(); + + // check if a baseline is restored + if (version instanceof InternalBaseline) { + // just restore all base versions + InternalBaseline baseline = (InternalBaseline) version; + internalRestore(baseline.getBaseVersions(), true); + + // ensure that the restored root node has a jcr:configuration property + // since it might not have been recorded by the initial checkin of the + // configuration + NodeId configId = baseline.getConfigurationId(); + NodeId rootId = baseline.getConfigurationRootId(); + NodeStateEx rootNode = state.getNode(rootId); + rootNode.setPropertyValue(NameConstants.JCR_CONFIGURATION, InternalValue.create(configId)); + rootNode.store(); + } + + return restored; + } + + /** + * Restores the properties and child nodes from the frozen state. + * + * @param state state to restore + * @param freeze the frozen node + * @param vsel version selector + * @param restored set of restored versions + * @param removeExisting remove existing flag + * @param copy if true a pure copy is performed + * @throws RepositoryException if an error occurs + * @throws ItemStateException if an error occurs + */ + protected void internalRestoreFrozen(NodeStateEx state, + InternalFrozenNode freeze, + VersionSelector vsel, + Set restored, + boolean removeExisting, + boolean copy) + throws RepositoryException, ItemStateException { + + // check uuid + if (state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_REFERENCEABLE)) { + if (!state.getNodeId().equals(freeze.getFrozenId())) { + String msg = "Unable to restore version of " + safeGetJCRPath(state) + ". UUID changed."; + log.error(msg); + throw new ItemExistsException(msg); + } + } + + // check primary type + if (!freeze.getFrozenPrimaryType().equals(state.getState().getNodeTypeName())) { + // todo: implement + String msg = "Unable to restore version of " + safeGetJCRPath(state) + ". PrimaryType change not supported yet."; + log.error(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + + // adjust mixins + state.setMixins(freeze.getFrozenMixinTypes()); + + // For each property P present on F (other than jcr:frozenPrimaryType, + // jcr:frozenMixinTypes and jcr:frozenUuid): + // - If P has an OPV of COPY or VERSION then F/P is copied to N/P, + // replacing any existing N/P. + // - F will never have a property with an OPV of IGNORE, INITIALIZE, COMPUTE + // or ABORT (see 15.2 Check-In: Creating a Version). + Set propNames = new HashSet(); + PropertyState[] props = freeze.getFrozenProperties(); + for (PropertyState prop : props) { + // don't restore jcr:activity + Name name = prop.getName(); + if (!name.equals(NameConstants.JCR_ACTIVITY)) { + state.copyFrom(prop); + propNames.add(name); + } + } + + // For each property P present on N but not on F: + // - If P has an OPV of COPY, VERSION or ABORT then N/P is removed. Note that + // while a node with a child item of OPV ABORT cannot be versioned, it is + // legal for a previously versioned node to have such a child item added to it + // and then for it to be restored to the state that it had before that item was + // added, as this step indicates. + // - If P has an OPV of IGNORE then no change is made to N/P. + // - If P has an OPV of INITIALIZE then, if N/P has a default value (either + // defined in the node type of N or implementation-defined) its value is + // changed to that default value. If N/P has no default value then it is left + // unchanged. + // - If P has an OPV of COMPUTE then the value of N/P may be changed + // according to an implementation-specific mechanism. + + // remove properties that do not exist in the frozen representation + for (PropertyState prop: state.getProperties()) { + Name propName = prop.getName(); + if (!propNames.contains(propName)) { + int opv = state.getDefinition(prop).getOnParentVersion(); + if (opv == OnParentVersionAction.COPY + || opv == OnParentVersionAction.VERSION + || opv == OnParentVersionAction.ABORT) { + state.removeProperty(propName); + } else if (opv == OnParentVersionAction.INITIALIZE) { + InternalValue[] values = computeAutoValues(state, state.getDefinition(prop), true); + if (values != null) { + state.setPropertyValues(propName, prop.getType(), values, prop.isMultiValued()); + } + } else if (opv == OnParentVersionAction.COMPUTE) { + InternalValue[] values = computeAutoValues(state, state.getDefinition(prop), false); + if (values != null) { + state.setPropertyValues(propName, prop.getType(), values, prop.isMultiValued()); + } + } + } + } + + // add 'auto-create' properties that do not exist yet + for (QPropertyDefinition def: state.getEffectiveNodeType().getAutoCreatePropDefs()) { + if (!state.hasProperty(def.getName())) { + InternalValue[] values = computeAutoValues(state, def, true); + if (values != null) { + state.setPropertyValues(def.getName(), def.getRequiredType(), values, def.isMultiple()); + } + } + } + + // For each child node C present on N but not on F: + // - If C has an OPV of COPY, VERSION or ABORT then N/C is removed. + // Note that while a node with a child item of OPV ABORT cannot be + // versioned, it is legal for a previously versioned node to have such + // a child item added to it and then for it to be restored to the state + // that it had before that item was added, as this step indicates. + // - If C has an OPV of IGNORE then no change is made to N/C. + // - If C has an OPV of INITIALIZE then N/C is re-initialized as if it + // were newly created, as defined in its node type. + // - If C has an OPV of COMPUTE then N/C may be re-initialized according + // to an implementation-specific mechanism. + LinkedList toDelete = new LinkedList(); + for (ChildNodeEntry entry: state.getState().getChildNodeEntries()) { + if (!freeze.hasFrozenChildNode(entry.getName(), entry.getIndex())) { + NodeStateEx child = state.getNode(entry.getName(), entry.getIndex()); + int opv = child.getDefinition().getOnParentVersion(); + if (copy || opv == OnParentVersionAction.COPY + || opv == OnParentVersionAction.VERSION + || opv == OnParentVersionAction.ABORT) { + toDelete.addFirst(entry); + } else if (opv == OnParentVersionAction.INITIALIZE) { + log.warn("OPV.INITIALIZE not supported yet on restore of existing child nodes: " + safeGetJCRPath(child)); + } else if (opv == OnParentVersionAction.COMPUTE) { + log.warn("OPV.COMPUTE not supported yet on restore of existing child nodes: " + safeGetJCRPath(child)); + } + } + } + for (ChildNodeEntry entry: toDelete) { + state.removeNode(entry.getName(), entry.getIndex()); + } + // need to sync with state manager + state.store(); + + // create a map that contains a int->NodeStateEx mapping for each child name + Map> entryToNodeStateExMapping = new HashMap>(); + for (ChildNodeEntry entry : state.getState().getChildNodeEntries()) { + Map id2stateMap = entryToNodeStateExMapping + .get(entry.getName()); + if (id2stateMap == null) { + id2stateMap = new HashMap(); + } + id2stateMap.put(entry.getIndex(), + state.getNode(entry.getName(), entry.getIndex())); + entryToNodeStateExMapping.put(entry.getName(), id2stateMap); + } + + // For each child node C present on F: + // - F will never have a child node with an OPV of IGNORE, INITIALIZE, + // COMPUTE or ABORT (see 15.2 Check-In: Creating a Version). + for (ChildNodeEntry entry : freeze.getFrozenChildNodes()) { + InternalFreeze child = freeze.getFrozenChildNode(entry.getName(), entry.getIndex()); + NodeStateEx restoredChild = null; + if (child instanceof InternalFrozenNode) { + // - If C has an OPV of COPY or VERSION: + // - B is true, then F/C and its subgraph is copied to N/C, replacing + // any existing N/C and its subgraph and any node in the workspace + // with the same identifier as C or a node in the subgraph of C is + // removed. + // - B is false, then F/C and its subgraph is copied to N/C, replacing + // any existing N/C and its subgraph unless there exists a node in the + // workspace with the same identifier as C, or a node in the subgraph + // of C, in which case an ItemExistsException is thrown , all + // changes made by the restore are rolled back leaving N unchanged. + + InternalFrozenNode f = (InternalFrozenNode) child; + + // if node is present, remove it + Map id2stateMap = entryToNodeStateExMapping + .get(entry.getName()); + if (id2stateMap != null + && id2stateMap.containsKey(entry.getIndex())) { + state.removeNode(id2stateMap.get(entry.getIndex())); + } + + // check for existing + if (f.getFrozenId() != null) { + if (stateMgr.hasItemState(f.getFrozenId())) { + NodeStateEx existing = state.getNode(f.getFrozenId()); + if (removeExisting) { + NodeStateEx parent = existing.getParent(); + parent.removeNode(existing); + parent.store(); + } else if (existing.getState().isShareable()) { + // if existing node is shareable, then clone it + restoredChild = state.moveFrom(existing, existing.getName(), true); + } else if (!existing.hasAncestor(state.getNodeId())){ + String msg = "Unable to restore node, item already exists " + + "outside of restored tree: " + safeGetJCRPath(existing); + log.error(msg); + throw new ItemExistsException(msg); + } + + } + } + if (restoredChild == null) { + restoredChild = state.addNode(f.getName(), f.getFrozenPrimaryType(), f.getFrozenId()); + restoredChild.setMixins(f.getFrozenMixinTypes()); + } + internalRestoreFrozen(restoredChild, f, vsel, restored, removeExisting, true); + + } else if (child instanceof InternalFrozenVersionHistory) { + // Each child node C of N where C has an OPV of VERSION and C is + // mix:versionable, is represented in F not as a copy of N/C but as + // special node containing a reference to the version history of + // C. On restore, the following occurs: + // - If the workspace currently has an already existing node corresponding + // to C's version history and the removeExisting flag of the restore is + // set to true, then that instance of C becomes the child of the restored N. + // - If the workspace currently has an already existing node corresponding + // to C's version history and the removeExisting flag of the restore is + // set to false then an ItemExistsException is thrown. + // - If the workspace does not have an instance of C then one is restored from + // C's version history: + // - If the restore was initiated through a restoreByLabel where L is + // the specified label and there is a version of C with the label L then + // that version is restored. + // - If the version history of C does not contain a version with the label + // L or the restore was initiated by a method call that does not specify + // a label then the workspace in which the restore is being performed + // will determine which particular version of C will be restored. This + // determination depends on the configuration of the workspace and + // is outside the scope of this specification. + InternalFrozenVersionHistory fh = (InternalFrozenVersionHistory) child; + InternalVersionHistory vh = vMgr.getVersionHistory(fh.getVersionHistoryId()); + // get desired version from version selector + InternalVersion v = vsel.select(vh); + Name oldVersion = null; + + // check if representing versionable already exists somewhere + NodeId nodeId = vh.getVersionableId(); + if (stateMgr.hasItemState(nodeId)) { + restoredChild = state.getNode(nodeId); + if (restoredChild.getParentId().equals(state.getNodeId())) { + // if same parent, ignore + } else if (removeExisting) { + NodeStateEx parent = restoredChild.getNode(restoredChild.getParentId()); + state.moveFrom(restoredChild, fh.getName(), false); + parent.store(); + + // get old version name + oldVersion = getBaseVersion(restoredChild).getName(); + } else { + // since we delete the OPV=Copy children beforehand, all + // found nodes must be outside of this tree + String msg = "Unable to restore node, item already exists " + + "outside of restored tree: " + safeGetJCRPath(restoredChild); + log.error(msg); + throw new ItemExistsException(msg); + } + } + + // check existing version of item exists + if (restoredChild == null) { + if (v == null) { + // if version selector was unable to select version, + // choose the initial one + List vs = vh.getRootVersion().getSuccessors(); + if (vs.isEmpty()) { + String msg = "Unable to select appropriate version for " + + child.getName() + " using " + vsel; + log.error(msg); + throw new VersionException(msg); + } + v = vs.get(0); + } + InternalFrozenNode f = v.getFrozenNode(); + restoredChild = state.addNode(fh.getName(), f.getFrozenPrimaryType(), f.getFrozenId()); + restoredChild.setMixins(f.getFrozenMixinTypes()); + } else { + if (v == null || oldVersion == null || v.getName().equals(oldVersion)) { + v = null; + } + } + if (v != null) { + try { + internalRestore(restoredChild, v, vsel, removeExisting); + } catch (RepositoryException e) { + log.error("Error while restoring node: " + e); + log.error(" child path: " + restoredChild); + log.error(" selected version: " + v.getName()); + StringBuilder avail = new StringBuilder(); + for (Name name: vh.getVersionNames()) { + avail.append(name); + avail.append(", "); + } + log.error(" available versions: " + avail); + log.error(" versionselector: " + vsel); + throw e; + } + // add this version to set + restored.add(v); + } + } + if (restoredChild != null && state.getEffectiveNodeType().hasOrderableChildNodes()) { + // In a repository that supports orderable child nodes, the relative + // ordering of the set of child nodes C that are copied from F is + // preserved. + + // order at end + ArrayList list = new ArrayList(state.getState().getChildNodeEntries()); + ChildNodeEntry toReorder = null; + boolean isLast = true; + for (ChildNodeEntry e: list) { + if (e.getId().equals(restoredChild.getNodeId())) { + toReorder = e; + } else if (toReorder != null) { + isLast = false; + } + } + if (toReorder != null && !isLast) { + list.remove(toReorder); + list.add(toReorder); + state.getState().setChildNodeEntries(list); + } + } + } + } + + /** + * Computes the auto generated values and falls back to the default values + * specified in the property definition + * @param state parent state + * @param def property definition + * @param useDefaultValues if true the default values are respected + * @return the values or null + * @throws RepositoryException if the values cannot be computed. + */ + private InternalValue[] computeAutoValues(NodeStateEx state, QPropertyDefinition def, + boolean useDefaultValues) + throws RepositoryException { + // compute system generated values if necessary + InternalValue[] values = + new NodeTypeInstanceHandler(session.getUserID()). + computeSystemGeneratedPropertyValues(state.getState(), def); + if (values == null && useDefaultValues) { + values = InternalValue.create(def.getDefaultValues()); + } + // avoid empty value array for single value property + if (values != null && values.length == 0 && !def.isMultiple()) { + values = null; + } + return values; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionSelector.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionSelector.java new file mode 100644 index 00000000000..b615be9901c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionSelector.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.core.version; + +import javax.jcr.RepositoryException; + +/** + * This Interface defines the version selector that needs to provide a version, + * given some hints and a version history. the selector is used in the various + * restore methods in order to select the correct version of previously versioned + * OPV=Version children upon restore. JSR170 states: "This determination + * [of the version] depends on the configuration of the workspace and is outside + * the scope of this specification." + *

    + * The version selection in jackrabbit works as follows:
    + * The Node.restore() methods uses the + * {@link DateVersionSelector} which is initialized with the creation date of + * the parent version. This selector selects the latest version that is equal + * or older than the given date. if no such version exists, the initial one + * is restored.
    + * The Node.restoreByLabel() uses the + * {@link LabelVersionSelector} which is initialized with the label of the + * version to be restored. This selector selects the version with the same + * label. if no such version exists, the initial one is restored. + *

    + * + * @see DateVersionSelector + * @see LabelVersionSelector + * @see javax.jcr.version.VersionManager#restore + * + */ +public interface VersionSelector { + + /** + * Selects a version of the given version history. If this VersionSelector + * is unable to select one, it can return null. Please note, + * that a version selector is not allowed to return the root version. + * + * @param versionHistory version history to select a version from + * @return A version or null. + * @throws RepositoryException if an error occurs. + */ + InternalVersion select(InternalVersionHistory versionHistory) throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionSet.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionSet.java new file mode 100644 index 00000000000..d113c6d6eef --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersionSet.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.core.version; + +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; + +/** + * This Class implements a version selector that is based on a set of versions. + */ +public class VersionSet implements VersionSelector { + + /** + * the internal changeset + */ + private final Map versions; + + /** + * fallback flag + */ + private final boolean dateFallback; + + /** + * Creates a ChangeSetVersionSelector that will try to select a + * version within the given set of versions. + * + * @param versions the set of versions + */ + public VersionSet(Map versions) { + this(versions, false); + } + + /** + * Creates a ChangeSetVersionSelector that will try to select a + * version in the given set. + * + * @param versions the set of versions + * @param dateFallback if true date fallback is enabled. + */ + public VersionSet(Map versions, boolean dateFallback) { + this.versions = versions; + this.dateFallback = dateFallback; + } + + /** + * Returns the (modifiable) changeset of this selector. the keys of the + * map are the node ids of the version histories. + * @return the change set + */ + public Map versions() { + return versions; + } + + /** + * {@inheritDoc} + * + * Selects a version from set having the given version history. + */ + public InternalVersion select(InternalVersionHistory versionHistory) + throws RepositoryException { + InternalVersion v = versions.get(versionHistory.getId()); + if (v == null && dateFallback) { + // select latest one + v = DateVersionSelector.selectByDate(versionHistory, null); + } + return v; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersioningLock.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersioningLock.java new file mode 100644 index 00000000000..8f58e320b62 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/version/VersioningLock.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.core.version; + +import org.apache.jackrabbit.core.util.XAReentrantWriterPreferenceReadWriteLock; + +import EDU.oswego.cs.dl.util.concurrent.ReadWriteLock; +import EDU.oswego.cs.dl.util.concurrent.Sync; + +/** + * A reentrant read-write lock used by the internal version manager for + * synchronization. Unlike a normal reentrant lock, this one allows the lock + * to be re-entered not just by a thread that's already holding the lock but + * by any thread within the same transaction. + */ +public class VersioningLock { + + /** + * The internal read-write lock. + */ + private final XAReentrantWriterPreferenceReadWriteLock rwLock = new XAReentrantWriterPreferenceReadWriteLock(); + + public ReadLock acquireReadLock() throws InterruptedException { + return new ReadLock(rwLock.readLock()); + } + + public WriteLock acquireWriteLock() throws InterruptedException { + return new WriteLock(rwLock); + } + + public static class WriteLock { + + private ReadWriteLock readWriteLock; + + private WriteLock(ReadWriteLock readWriteLock) + throws InterruptedException { + this.readWriteLock = readWriteLock; + this.readWriteLock.writeLock().acquire(); + } + + public void release() { + readWriteLock.writeLock().release(); + } + + public ReadLock downgrade() throws InterruptedException { + ReadLock rLock = new ReadLock(readWriteLock.readLock()); + release(); + return rLock; + } + + } + + public static class ReadLock { + + private final Sync readLock; + + private ReadLock(Sync readLock) throws InterruptedException { + this.readLock = readLock; + this.readLock.acquire(); + } + + public void release() { + readLock.release(); + } + + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/AbstractVISProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/AbstractVISProvider.java new file mode 100644 index 00000000000..661ea2c9e87 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/AbstractVISProvider.java @@ -0,0 +1,530 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.virtual; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.ItemStateListener; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.util.WeakIdentityCollection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import java.util.Collection; +import java.util.Map; + +/** + * This Class implements a virtual item state provider, in order to expose the + * versions to the version storage. + */ +public abstract class AbstractVISProvider implements VirtualItemStateProvider, ItemStateListener { + /** + * the default logger + */ + private static Logger log = LoggerFactory.getLogger(AbstractVISProvider.class); + + /** + * the root node + */ + private VirtualNodeState root = null; + + /** + * the root node id + */ + protected final NodeId rootNodeId; + + /** + * the node type registry + */ + protected final NodeTypeRegistry ntReg; + + /** + * the cache node states. key=ItemId, value=ItemState + */ + @SuppressWarnings("unchecked") + private final Map nodes = + // Using soft references instead of weak ones seems to have + // some unexpected performance consequences, so for now it's + // better to stick with weak references. + new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK); + + /** + * Listeners (weak references) + */ + @SuppressWarnings("unchecked") + private final transient Collection listeners = + new WeakIdentityCollection(5); + + /** + * Creates an abstract virtual item state provider + * + * @param ntReg + * @param rootNodeId + */ + public AbstractVISProvider(NodeTypeRegistry ntReg, NodeId rootNodeId) { + this.ntReg = ntReg; + this.rootNodeId = rootNodeId; + } + + /** + * Creates the root node state. + * + * @return The virtual root node state. + * @throws RepositoryException + */ + protected abstract VirtualNodeState createRootNodeState() throws RepositoryException; + + //-----------------------------------------------------< ItemStateManager > + + /** + * {@inheritDoc} + */ + public boolean hasItemState(ItemId id) { + if (id instanceof NodeId) { + if (nodes.containsKey(id)) { + return true; + } else if (id.equals(rootNodeId)) { + return true; + } else { + return internalHasNodeState((NodeId) id); + } + } else { + return internalHasPropertyState((PropertyId) id); + } + } + + /** + * {@inheritDoc} + */ + public ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + if (id instanceof NodeId) { + ItemState s; + if (nodes.containsKey(id)) { + s = nodes.get(id); + } else if (id.equals(rootNodeId)) { + s = getRootState(); + } else { + s = cache(internalGetNodeState((NodeId) id)); + } + return s; + } else { + return internalGetPropertyState((PropertyId) id); + } + } + + /** + * {@inheritDoc} + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + throw new NoSuchItemStateException(id.toString()); + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + return false; + } + + //-------------------------------------------< VirtualItemStateProvider >--- + + /** + * {@inheritDoc} + */ + public boolean isVirtualRoot(ItemId id) { + return id.equals(rootNodeId); + } + + /** + * {@inheritDoc} + */ + public NodeId getVirtualRootId() { + return rootNodeId; + } + + /** + * {@inheritDoc} + */ + public NodeId[] getVirtualRootIds() { + return new NodeId[]{rootNodeId}; + } + + /** + * Returns the root state + * + * @return the root state + * @throws ItemStateException If the root node state does not exist and its + * creation fails. + */ + public synchronized NodeState getRootState() throws ItemStateException { + try { + if (root == null) { + root = createRootNodeState(); + } + return root; + } catch (RepositoryException e) { + throw new ItemStateException("Error creating root node state", e); + } + } + + /** + * Discards all virtual item states and prepares for the root state + * to be recreated when next accessed. + * + * @see JCR-2617 + */ + protected synchronized void discardAll() { + if (root != null) { + discardTree(root); + nodes.clear(); + root = null; + } + } + + /** + * Recursively discards all the properties and nodes in the subtree + * rooted at the given node state. + * + * @param state root of the subtree to be discarded + */ + private void discardTree(NodeState state) { + for (Name name : state.getPropertyNames()) { + try { + getItemState(new PropertyId(state.getNodeId(), name)).discard(); + } catch (ItemStateException e) { + log.warn("Unable to discard virtual property " + name, e); + } + } + for (ChildNodeEntry entry : state.getChildNodeEntries()) { + try { + discardTree((NodeState) getItemState(entry.getId())); + } catch (ItemStateException e) { + log.warn("Unable to discard virtual node " + entry.getId(), e); + } + } + state.discard(); + } + + /** + * Checks if this provide has the node state of the given node id + * + * @param id + * @return true if it has the node state + */ + protected abstract boolean internalHasNodeState(NodeId id); + + /** + * Retrieves the node state with the given node id + * + * @param id + * @return + * @throws NoSuchItemStateException + * @throws ItemStateException + */ + protected abstract VirtualNodeState internalGetNodeState(NodeId id) + throws NoSuchItemStateException, ItemStateException; + + /** + * Checks if this provider has the property state of the given id. + * + * @param id + * @return true if it has the property state + */ + protected boolean internalHasPropertyState(PropertyId id) { + + try { + // get parent state + NodeState parent = (NodeState) getItemState(id.getParentId()); + + // handle some default prop states + if (parent instanceof VirtualNodeState) { + return parent.hasPropertyName(id.getName()); + } + } catch (ItemStateException e) { + // ignore + } + return false; + } + + /** + * Retrieves the property state for the given id + * + * @param id + * @return + * @throws NoSuchItemStateException + * @throws ItemStateException + */ + protected VirtualPropertyState internalGetPropertyState(PropertyId id) + throws NoSuchItemStateException, ItemStateException { + + // get parent state + NodeState parent = (NodeState) getItemState(id.getParentId()); + + // handle some default prop states + if (parent instanceof VirtualNodeState) { + return ((VirtualNodeState) parent).getProperty(id.getName()); + } + throw new NoSuchItemStateException(id.toString()); + } + + /** + * {@inheritDoc} + */ + public VirtualPropertyState createPropertyState(VirtualNodeState parent, + Name name, int type, + boolean multiValued) + throws RepositoryException { + PropertyId id = new PropertyId(parent.getNodeId(), name); + VirtualPropertyState prop = new VirtualPropertyState(id); + prop.setType(type); + prop.setMultiValued(multiValued); + return prop; + } + + /** + * {@inheritDoc} + */ + public VirtualNodeState createNodeState(VirtualNodeState parent, Name name, + NodeId id, Name nodeTypeName) + throws RepositoryException { + + assert id != null; + + // create a new node state + VirtualNodeState state = new VirtualNodeState(this, parent.getNodeId(), id, nodeTypeName, Name.EMPTY_ARRAY); + + cache(state); + return state; + } + + /** + * {@inheritDoc} + */ + public void addListener(ItemStateListener listener) { + synchronized (listeners) { + assert (!listeners.contains(listener)); + listeners.add(listener); + } + } + + /** + * {@inheritDoc} + */ + public void removeListener(ItemStateListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + /** + * returns the node type manager + * + * @return the node type manager + */ + protected NodeTypeRegistry getNodeTypeRegistry() { + return ntReg; + } + + /** + * adds the node state to the cache + * + * @param state + * @return The same state. + */ + protected NodeState cache(NodeState state) { + if (state != null) { + nodes.put(state.getNodeId(), state); + log.debug("item added to cache. size=" + nodes.size()); + } + return state; + } + + /** + * removes the node state from the cache + * + * @param id + */ + protected void evict(NodeId id) { + nodes.remove(id); + } + + /** + * invalidates the item + * + * @param id + * @param recursive + */ + public void invalidateItem(ItemId id, boolean recursive) { + VirtualNodeState state = id.equals(rootNodeId) ? root : (VirtualNodeState) nodes.get(id); + if (state != null) { + if (recursive) { + VirtualPropertyState[] props = state.getProperties(); + for (int i = 0; i < props.length; i++) { + props[i].notifyStateUpdated(); + } + for (ChildNodeEntry pe : state.getChildNodeEntries()) { + invalidateItem(pe.getId(), true); + } + } + state.notifyStateUpdated(); + } + } + + /** + * retrieves the property definition for the given constraints + * + * @param parent The parent node state. + * @param propertyName The name of the property. + * @param type + * @param multiValued + * @return + * @throws RepositoryException + */ + protected QPropertyDefinition getApplicablePropertyDef(NodeState parent, Name propertyName, + int type, boolean multiValued) + throws RepositoryException { + return getEffectiveNodeType(parent).getApplicablePropertyDef(propertyName, type, multiValued); + } + + /** + * Retrieves the node definition for the given constraints. + * + * @param parent The parent state. + * @param nodeName + * @param nodeTypeName + * @return + * @throws RepositoryException + */ + protected QNodeDefinition getApplicableChildNodeDef(NodeState parent, Name nodeName, Name nodeTypeName) + throws RepositoryException { + return getEffectiveNodeType(parent).getApplicableChildNodeDef( + nodeName, nodeTypeName, getNodeTypeRegistry()); + } + + /** + * Returns the effective (i.e. merged and resolved) node type representation + * of this node's primary and mixin node types. + * + * @return the effective node type + * @throws RepositoryException + */ + protected EffectiveNodeType getEffectiveNodeType(NodeState parent) throws RepositoryException { + try { + NodeTypeRegistry ntReg = getNodeTypeRegistry(); + return ntReg.getEffectiveNodeType( + parent.getNodeTypeName(), parent.getMixinTypeNames()); + } catch (NodeTypeConflictException ntce) { + String msg = "internal error: failed to build effective node type for node " + parent.getNodeId(); + throw new RepositoryException(msg, ntce); + } + } + + /** + * {@inheritDoc} + */ + public void stateCreated(ItemState created) { + ItemStateListener[] la; + synchronized (listeners) { + la = listeners.toArray(new ItemStateListener[listeners.size()]); + } + for (int i = 0; i < la.length; i++) { + if (la[i] != null) { + la[i].stateCreated(created); + } + } + } + + /** + * {@inheritDoc} + */ + public void stateModified(ItemState modified) { + ItemStateListener[] la; + synchronized (listeners) { + la = listeners.toArray(new ItemStateListener[listeners.size()]); + } + for (int i = 0; i < la.length; i++) { + if (la[i] != null) { + la[i].stateModified(modified); + } + } + } + + /** + * {@inheritDoc} + */ + public void stateDestroyed(ItemState destroyed) { + if (destroyed.isNode() && destroyed.getId().equals(rootNodeId)) { + try { + root = createRootNodeState(); + } catch (RepositoryException e) { + // ignore + } + } + evict((NodeId) destroyed.getId()); + + ItemStateListener[] la; + synchronized (listeners) { + la = listeners.toArray(new ItemStateListener[listeners.size()]); + } + for (int i = 0; i < la.length; i++) { + if (la[i] != null) { + la[i].stateDestroyed(destroyed); + } + } + } + + /** + * {@inheritDoc} + */ + public void stateDiscarded(ItemState discarded) { + if (discarded.isNode() && discarded.getId().equals(rootNodeId)) { + try { + root = createRootNodeState(); + } catch (RepositoryException e) { + // ignore + } + } + evict((NodeId) discarded.getId()); + + ItemStateListener[] la; + synchronized (listeners) { + la = listeners.toArray(new ItemStateListener[listeners.size()]); + } + for (int i = 0; i < la.length; i++) { + if (la[i] != null) { + la[i].stateDiscarded(discarded); + } + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualItemStateProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualItemStateProvider.java new file mode 100644 index 00000000000..26bdc9c9081 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualItemStateProvider.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.core.virtual; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemStateListener; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.spi.Name; + +/** + * This Interface defines a virtual item state provider. + */ +public interface VirtualItemStateProvider extends ItemStateManager { + + /** + * Checks if the id refers to the root of a virtual tree. + * + * @param id + * @return true if it is the root + */ + boolean isVirtualRoot(ItemId id); + + /** + * Returns the id of the root node of the virtual tree. + * + * @return the id of the root node of the virtual tree. + * @deprecated use {@link #getVirtualRootIds()} instead. + */ + NodeId getVirtualRootId(); + + /** + * Returns the ids of the root nodes of the virtual tree. + * + * @return the ids of the roots node of the virtual tree. + */ + NodeId[] getVirtualRootIds(); + + /** + * Creats a new virtual property state + * + * @param parent + * @param name + * @param type + * @param multiValued + * @return + * @throws RepositoryException + */ + VirtualPropertyState createPropertyState(VirtualNodeState parent, + Name name, int type, + boolean multiValued) + throws RepositoryException; + + /** + * Creates a new virtual node state + * + * @param parent + * @param name + * @param id (must not be null) + * @param nodeTypeName + * @return + * @throws RepositoryException + */ + VirtualNodeState createNodeState(VirtualNodeState parent, Name name, + NodeId id, Name nodeTypeName) + throws RepositoryException; + + /** + * Informs this provider that the node references to some of its states + * have changed. + * + * @param references collection of {@link NodeReferences} instances + * @return true if the reference target is one of its items. + */ + boolean setNodeReferences(ChangeLog references); + + + /** + * Add an ItemStateListener + * @param listener the new listener to be informed on modifications + */ + void addListener(ItemStateListener listener); + + /** + * Remove an ItemStateListener + * + * @param listener an existing listener + */ + void removeListener(ItemStateListener listener); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualNodeState.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualNodeState.java new file mode 100644 index 00000000000..d42e4b7bb41 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualNodeState.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.core.virtual; + +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.util.HashMap; +import java.util.HashSet; + +/** + * This Class implements a virtual node state + */ +public class VirtualNodeState extends NodeState { + + /** + * The virtual item state provide that created this node state + */ + protected final VirtualItemStateProvider stateMgr; + + /** + * map of property states of this node state + */ + private final HashMap properties = + new HashMap(); + + /** a set of hard references to child states */ + private HashSet stateRefs = null; + + /** + * creates a new virtual node state + * + * @param stateMgr + * @param parentId + * @param id + * @param nodeTypeName + * @param mixins + * @throws RepositoryException + */ + public VirtualNodeState(AbstractVISProvider stateMgr, + NodeId parentId, + NodeId id, + Name nodeTypeName, + Name[] mixins) + throws RepositoryException { + super(id, nodeTypeName, parentId, ItemState.STATUS_EXISTING, false); + this.stateMgr = stateMgr; + setContainer(stateMgr); + // add default properties + setPropertyValue(NameConstants.JCR_PRIMARYTYPE, InternalValue.create(nodeTypeName)); + setMixinNodeTypes(mixins); + } + + /** + * Returns the properties of this node + * + * @return the properties. + */ + public VirtualPropertyState[] getProperties() { + return properties.values().toArray(new VirtualPropertyState[properties.size()]); + } + + + /** + * Returns the values of the given property of null + * + * @param name + * @return the values + */ + public InternalValue[] getPropertyValues(Name name) throws NoSuchItemStateException { + VirtualPropertyState ps = getProperty(name); + if (ps == null) { + return null; + } else { + return ps.getValues(); + } + } + + /** + * Returns the value of the given property or null + * + * @param name + * @return the value + */ + public InternalValue getPropertyValue(Name name) throws NoSuchItemStateException { + VirtualPropertyState ps = getProperty(name); + if (ps == null || ps.getValues().length == 0) { + return null; + } else { + return ps.getValues()[0]; + } + } + + /** + * returns the property state of the given name + * + * @param name + * @return + * @throws NoSuchItemStateException + */ + public VirtualPropertyState getProperty(Name name) throws NoSuchItemStateException { + return (VirtualPropertyState) properties.get(name); + } + + /** + * Sets the property value + * + * @param name + * @param value + * @throws javax.jcr.RepositoryException + */ + public void setPropertyValue(Name name, InternalValue value) + throws RepositoryException { + setPropertyValues(name, value.getType(), new InternalValue[]{value}, false); + } + + /** + * Sets the property values + * + * @param name + * @param type + * @param values + * @throws RepositoryException + */ + public void setPropertyValues(Name name, int type, InternalValue[] values) + throws RepositoryException { + setPropertyValues(name, type, values, true); + } + + /** + * Sets the property values + * + * @param name + * @param type + * @param values + * @throws RepositoryException + */ + public void setPropertyValues(Name name, int type, InternalValue[] values, boolean multiple) + throws RepositoryException { + VirtualPropertyState prop = getOrCreatePropertyState(name, type, multiple); + prop.setValues(values); + } + + /** + * Retrieves or creates a new property state as child property of this node + * + * @param name + * @param type + * @param multiValued + * @return + * @throws RepositoryException + */ + protected VirtualPropertyState getOrCreatePropertyState(Name name, int type, boolean multiValued) + throws RepositoryException { + + VirtualPropertyState prop = (VirtualPropertyState) properties.get(name); + if (prop == null) { + prop = stateMgr.createPropertyState(this, name, type, multiValued); + properties.put(name, prop); + addPropertyName(name); + } + return prop; + } + + /** + * sets the mixing node type and adds the respective property + * + * @param mixins + * @throws RepositoryException + */ + public void setMixinNodeTypes(Name[] mixins) throws RepositoryException { + if (mixins != null) { + HashSet set = new HashSet(); + InternalValue[] values = new InternalValue[mixins.length]; + for (int i = 0; i < mixins.length; i++) { + set.add(mixins[i]); + values[i] = InternalValue.create(mixins[i]); + } + setMixinTypeNames(set); + setPropertyValues(NameConstants.JCR_MIXINTYPES, PropertyType.NAME, values); + } + } + + /** + * Adds a hard reference to another state + * @param state + */ + public void addStateReference(NodeState state) { + if (stateRefs == null) { + stateRefs = new HashSet(); + } + stateRefs.add(state); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualPropertyState.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualPropertyState.java new file mode 100644 index 00000000000..ca997f0b65c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualPropertyState.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.core.virtual; + +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.value.InternalValue; + +/** + * This Class implements a virtual property state + */ +public class VirtualPropertyState extends PropertyState { + + /** + * a virtual value provider, if needed. + */ + private VirtualValueProvider valueProvider; + + /** + * Creates a new virtual property state + * @param id + */ + public VirtualPropertyState(PropertyId id) { + super(id, ItemState.STATUS_EXISTING, false); + } + + /** + * Returns the virtual value provider, if registered. + * @return the virtual value provider + */ + public VirtualValueProvider getValueProvider() { + return valueProvider; + } + + /** + * Sets a virtual value provider for this property + * @param valueProvider + */ + public void setValueProvider(VirtualValueProvider valueProvider) { + this.valueProvider = valueProvider; + } + + /** + * Returns the value of this state evt. by using the registered virtual + * value provider. + * @return the values + */ + public InternalValue[] getValues() { + InternalValue[] values = null; + if (valueProvider != null) { + values = valueProvider.getVirtualValues(getName()); + } + return valueProvider == null ? super.getValues() : values; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualValueProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualValueProvider.java new file mode 100644 index 00000000000..1c1e7e97f60 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/virtual/VirtualValueProvider.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.core.virtual; + +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; + +/** + * A VirtualValueProvider is used for virtual properties that + * want to provide values dynamically. + */ +public interface VirtualValueProvider { + + /** + * Returns the values for the given name + * @param propName the name of the property + * @return the values + */ + InternalValue[] getVirtualValues(Name propName); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/AccessControlImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/AccessControlImporter.java new file mode 100644 index 00000000000..de2e7c64f6c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/AccessControlImporter.java @@ -0,0 +1,491 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.xml; + +import static org.apache.jackrabbit.core.security.authorization.AccessControlConstants.NT_REP_ACCESS_CONTROL; +import static org.apache.jackrabbit.core.security.authorization.AccessControlConstants.NT_REP_PRINCIPAL_ACCESS_CONTROL; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.principal.UnknownPrincipal; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AccessControlImporter implements a + * ProtectedNodeImporter that is able to deal with access control + * content as defined by the default ac related node types present with + * jackrabbit-core. + */ +public class AccessControlImporter extends DefaultProtectedNodeImporter { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(AccessControlImporter.class); + + private static final int STATUS_UNDEFINED = 0; + private static final int STATUS_AC_FOLDER = 1; + private static final int STATUS_PRINCIPAL_AC = 2; + private static final int STATUS_ACL = 3; + private static final int STATUS_ACE = 4; + + private static final Set ACE_NODETYPES = new HashSet(2); + static { + ACE_NODETYPES.add(AccessControlConstants.NT_REP_DENY_ACE); + ACE_NODETYPES.add(AccessControlConstants.NT_REP_GRANT_ACE); + } + + private final Stack prevStatus = new Stack(); + + private AccessControlManager acMgr; + private int status = STATUS_UNDEFINED; + private NodeImpl parent = null; + + private boolean principalbased = false; + + private boolean initialized = false; + + // keep best-effort for backward compatibility reasons + private ImportBehavior importBehavior = ImportBehavior.BEST_EFFORT; + + /** + * the ACL for non-principal based + */ + private JackrabbitAccessControlList acl = null; + + @Override + public boolean init(JackrabbitSession session, NamePathResolver resolver, + boolean isWorkspaceImport, int uuidBehavior, + ReferenceChangeTracker referenceTracker) { + if (super.init(session, resolver, isWorkspaceImport, uuidBehavior, referenceTracker)) { + if (initialized) { + throw new IllegalStateException("Already initialized"); + } + try { + acMgr = session.getAccessControlManager(); + initialized = true; + } catch (RepositoryException e) { + // initialization failed. ac-import not possible + } + } + return initialized; + } + + @Override + public boolean start(NodeImpl protectedParent) throws RepositoryException { + if (!initialized) { + throw new IllegalStateException("Not initialized"); + } + if (isStarted()) { + // only ok if same parent + if (!protectedParent.isSame(parent)) { + throw new IllegalStateException(); + } + return true; + } + if (isWorkspaceImport) { + log.debug("AccessControlImporter may not be used with the WorkspaceImporter"); + return false; + } + if (!protectedParent.getDefinition().isProtected()) { + log.debug("AccessControlImporter may not be started with a non-protected parent."); + return false; + } + + if (isPolicyNode(protectedParent)) { + String parentPath = protectedParent.getParent().getPath(); + acl = getACL(parentPath); + if (acl == null) { + log.warn("AccessControlImporter cannot be started: no ACL for {}.", parentPath); + return false; + } + status = STATUS_ACL; + } else if (isRepoPolicyNode(protectedParent)) { + acl = getACL(null); + if (acl == null) { + log.warn("AccessControlImporter cannot be started: no Repo ACL."); + return false; + } + status = STATUS_ACL; + } else if (protectedParent.isNodeType(AccessControlConstants.NT_REP_ACCESS_CONTROL)) { + status = STATUS_AC_FOLDER; + principalbased = true; + acl = null; + } // else: nothing this importer can deal with. + + if (isStarted()) { + parent = protectedParent; + return true; + } else { + return false; + } + } + + private JackrabbitAccessControlList getACL(String path) throws RepositoryException, AccessDeniedException { + JackrabbitAccessControlList acl = null; + for (AccessControlPolicy p: acMgr.getPolicies(path)) { + if (p instanceof JackrabbitAccessControlList) { + acl = (JackrabbitAccessControlList) p; + break; + } + } + if (acl != null) { + // clear all existing entries + for (AccessControlEntry ace: acl.getAccessControlEntries()) { + acl.removeAccessControlEntry(ace); + } + } + return acl; + } + + @Override + public boolean start(NodeState protectedParent) throws IllegalStateException, RepositoryException { + if (isStarted()) { + throw new IllegalStateException(); + } + if (isWorkspaceImport) { + log.debug("AccessControlImporter may not be used with the WorkspaceImporter"); + return false; + } + return false; + } + + @Override + public void end(NodeImpl protectedParent) throws RepositoryException { + if (!isStarted()) { + return; + } + + if (!principalbased) { + checkStatus(STATUS_ACL, "STATUS_ACL expected."); + acMgr.setPolicy(acl.getPath(), acl); + } else { + checkStatus(STATUS_AC_FOLDER, "STATUS_AC_FOLDER expected."); + if (!prevStatus.isEmpty()) { + throw new ConstraintViolationException("Incomplete protected item tree: "+ prevStatus.size()+ " calls to 'endChildInfo' missing."); + } + } + reset(); + } + + @Override + public void end(NodeState protectedParent) throws IllegalStateException, ConstraintViolationException, RepositoryException { + // nothing to do. will never get here. + } + + @Override + public void startChildInfo(NodeInfo childInfo, List propInfos) throws RepositoryException { + if (!isStarted()) { + return; + } + + Name ntName = childInfo.getNodeTypeName(); + int previousStatus = status; + + if (!principalbased) { + checkStatus(STATUS_ACL, "Cannot handle childInfo " + childInfo + "; rep:ACL may only contain a single level of child nodes representing the ACEs"); + addACE(childInfo, propInfos); + status = STATUS_ACE; + } else { + switch (status) { + case STATUS_AC_FOLDER: + if (NT_REP_ACCESS_CONTROL.equals(ntName)) { + // yet another intermediate node -> keep status + status = STATUS_AC_FOLDER; + } else if (NT_REP_PRINCIPAL_ACCESS_CONTROL.equals(ntName)) { + // the start of a principal-based acl + status = STATUS_PRINCIPAL_AC; + } else { + // illegal node type -> throw exception + throw new ConstraintViolationException("Unexpected node type " + ntName + ". Should be rep:AccessControl or rep:PrincipalAccessControl."); + } + checkIdMixins(childInfo); + break; + case STATUS_PRINCIPAL_AC: + if (NT_REP_ACCESS_CONTROL.equals(ntName)) { + // some intermediate path between principal paths. + status = STATUS_AC_FOLDER; + } else if (NT_REP_PRINCIPAL_ACCESS_CONTROL.equals(ntName)) { + // principal-based ac node underneath another one -> keep status + status = STATUS_PRINCIPAL_AC; + } else { + // the start the acl definition itself + checkDefinition(childInfo, AccessControlConstants.N_POLICY, AccessControlConstants.NT_REP_ACL); + status = STATUS_ACL; + } + checkIdMixins(childInfo); + break; + case STATUS_ACL: + // nodeinfo must define an ACE + addACE(childInfo, propInfos); + status = STATUS_ACE; + break; + default: + throw new ConstraintViolationException("Cannot handle childInfo " + childInfo + "; unexpected status " + status + " ."); + + } + } + prevStatus.push(previousStatus); + } + + @Override + public void endChildInfo() throws RepositoryException { + if (!isStarted()) { + return; + } + + // if the protected imported is started at an existing protected node + // SessionImporter does not remember it on the stack of parents node. + if (!principalbased) { + // childInfo + props have already been handled + // -> assert valid status + // -> no further actions required. + checkStatus(STATUS_ACE, "Upon completion of a NodeInfo the status must be STATUS_ACE."); + } + + // reset the status + status = prevStatus.pop(); + } + + private boolean isStarted() { + return status > STATUS_UNDEFINED; + } + + private void reset() { + status = STATUS_UNDEFINED; + parent = null; + acl = null; + } + + private void checkStatus(int expectedStatus, String message) throws ConstraintViolationException { + if (status != expectedStatus) { + throw new ConstraintViolationException(message); + } + } + + private static boolean isPolicyNode(NodeImpl node) throws RepositoryException { + Name nodeName = node.getQName(); + return AccessControlConstants.N_POLICY.equals(nodeName) && node.isNodeType(AccessControlConstants.NT_REP_ACL); + } + + /** + * @param node The node to be tested. + * @return true if the specified node is the 'rep:repoPolicy' + * acl node underneath the root node; false otherwise. + * @throws RepositoryException If an error occurs. + */ + private static boolean isRepoPolicyNode(NodeImpl node) throws RepositoryException { + Name nodeName = node.getQName(); + return AccessControlConstants.N_REPO_POLICY.equals(nodeName) && + node.isNodeType(AccessControlConstants.NT_REP_ACL) && + node.getDepth() == 1; + } + + private static void checkDefinition(NodeInfo nInfo, Name expName, Name expNodeTypeName) throws ConstraintViolationException { + if (expName != null && !expName.equals(nInfo.getName())) { + // illegal name + throw new ConstraintViolationException("Unexpected Node name "+ nInfo.getName() +". Node name should be " + expName + "."); + } + if (expNodeTypeName != null && !expNodeTypeName.equals(nInfo.getNodeTypeName())) { + // illegal name + throw new ConstraintViolationException("Unexpected node type " + nInfo.getNodeTypeName() + ". Node type should be " + expNodeTypeName + "."); + } + } + + private static void checkIdMixins(NodeInfo nInfo) throws ConstraintViolationException { + // neither explicit id NOR mixin types may be present. + Name[] mixins = nInfo.getMixinNames(); + NodeId id = nInfo.getId(); + if (id != null || mixins != null) { + throw new ConstraintViolationException("The node represented by NodeInfo " + nInfo + " may neither be referenceable nor have mixin types."); + } + } + + private void addACE(NodeInfo childInfo, List propInfos) throws RepositoryException, UnsupportedRepositoryOperationException { + + // node type may only be rep:GrantACE or rep:DenyACE + Name ntName = childInfo.getNodeTypeName(); + if (!ACE_NODETYPES.contains(ntName)) { + throw new ConstraintViolationException("Cannot handle childInfo " + childInfo + "; expected a valid, applicable rep:ACE node definition."); + } + + checkIdMixins(childInfo); + + boolean isAllow = AccessControlConstants.NT_REP_GRANT_ACE.equals(ntName); + Principal principal = null; + Privilege[] privileges = null; + Map restrictions = new HashMap(); + + for (PropInfo pInfo : propInfos) { + Name name = pInfo.getName(); + if (AccessControlConstants.P_PRINCIPAL_NAME.equals(name)) { + Value[] values = pInfo.getValues(PropertyType.STRING, resolver); + if (values == null || values.length != 1) { + throw new ConstraintViolationException(""); + } + String pName = values[0].getString(); + principal = session.getPrincipalManager().getPrincipal(pName); + if (principal == null) { + if (importBehavior == ImportBehavior.BEST_EFFORT) { + // create "fake" principal that is always accepted in ACLTemplate.checkValidEntry() + principal = new UnknownPrincipal(pName); + } else { + // create "fake" principal. this is checked again in ACLTemplate.checkValidEntry() + principal = new PrincipalImpl(pName); + } + } + } else if (AccessControlConstants.P_PRIVILEGES.equals(name)) { + Value[] values = pInfo.getValues(PropertyType.NAME, resolver); + privileges = new Privilege[values.length]; + for (int i = 0; i < values.length; i++) { + privileges[i] = acMgr.privilegeFromName(values[i].getString()); + } + } else { + TextValue[] txtVls = pInfo.getTextValues(); + for (TextValue txtV : txtVls) { + restrictions.put(resolver.getJCRName(name), txtV); + } + } + } + + if (principalbased) { + // try to access policies + List policies = new ArrayList(); + if (acMgr instanceof JackrabbitAccessControlManager) { + JackrabbitAccessControlManager jacMgr = (JackrabbitAccessControlManager) acMgr; + policies.addAll(Arrays.asList(jacMgr.getPolicies(principal))); + policies.addAll(Arrays.asList(jacMgr.getApplicablePolicies(principal))); + } + for (AccessControlPolicy policy : policies) { + if (policy instanceof JackrabbitAccessControlList) { + JackrabbitAccessControlList acl = (JackrabbitAccessControlList) policy; + Map restr = new HashMap(); + for (String restName : acl.getRestrictionNames()) { + TextValue txtVal = restrictions.remove(restName); + if (txtVal != null) { + restr.put(restName, txtVal.getValue(acl.getRestrictionType(restName), resolver)); + } + } + if (!restrictions.isEmpty()) { + throw new ConstraintViolationException("ACE childInfo contained restrictions that could not be applied."); + } + acl.addEntry(principal, privileges, isAllow, restr); + acMgr.setPolicy(acl.getPath(), acl); + return; + } + } + } else { + Map restr = new HashMap(); + for (String restName : acl.getRestrictionNames()) { + TextValue txtVal = restrictions.remove(restName); + if (txtVal != null) { + restr.put(restName, txtVal.getValue(acl.getRestrictionType(restName), resolver)); + } + } + if (!restrictions.isEmpty()) { + throw new ConstraintViolationException("ACE childInfo contained restrictions that could not be applied."); + } + acl.addEntry(principal, privileges, isAllow, restr); + return; + } + + + // could not apply the ACE. No suitable ACL found. + throw new ConstraintViolationException("Cannot handle childInfo " + childInfo + "; No policy found to apply the ACE."); + } + + //---------------------------------------------------------< BeanConfig >--- + /** + * @return human readable representation of the importBehavior value. + */ + public String getImportBehavior() { + return importBehavior.getString(); + } + + /** + * + * @param importBehaviorStr + */ + public void setImportBehavior(String importBehaviorStr) { + this.importBehavior = ImportBehavior.fromString(importBehaviorStr); + } + + + public static enum ImportBehavior { + + /** + * Default behavior that does not try to prevent errors or incompatibilities between the content + * and the ACL manager (eg. does not try to fix missing principals) + */ + DEFAULT("default"), + + /** + * Tries to minimize errors by adapting the content and bypassing validation checks (e.g. allows adding + * ACEs with missing principals, even if ACL manager would not allow this). + */ + BEST_EFFORT("bestEffort"); + + private final String value; + + ImportBehavior(String value) { + this.value = value; + } + + public static ImportBehavior fromString(String str) { + if (str.equals("bestEffort")) { + return BEST_EFFORT; + } else { + return ImportBehavior.valueOf(str.toUpperCase()); + } + } + + public String getString() { + return value; + } + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/BufferedStringValue.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/BufferedStringValue.java new file mode 100644 index 00000000000..d318d6da541 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/BufferedStringValue.java @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.xml; + +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.util.Base64; +import org.apache.jackrabbit.util.TransientFileFactory; +import org.apache.jackrabbit.value.ValueHelper; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.ValueFactory; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +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.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +/** + * BufferedStringValue represents an appendable + * serialized value that is either buffered in-memory or backed + * by a temporary file if its size exceeds a certain limit. + *

    + * Important: Note that in order to free resources + * {@link #dispose()} should be called as soon as + * BufferedStringValue instance is not used anymore. + */ +class BufferedStringValue implements TextValue { + + private static Logger log = LoggerFactory.getLogger(BufferedStringValue.class); + + /** + * The maximum size for buffering data in memory. + */ + private static final int MAX_BUFFER_SIZE = 0x10000; + + /** + * The in-memory buffer. + */ + private StringWriter buffer; + + /** + * The number of characters written so far. + * If the in-memory buffer is used, this is position within buffer (size of actual data in buffer) + */ + private long length; + + /** + * Backing temporary file created when size of data exceeds + * MAX_BUFFER_SIZE. + */ + private File tmpFile; + + /** + * Writer used to write to tmpFile. + */ + private Writer writer; + + private final NamePathResolver nsContext; + private final ValueFactory valueFactory; + + /** + * Whether the value is base64 encoded. + */ + private boolean base64; + + /** + * Constructs a new empty BufferedStringValue. + * @param nsContext + */ + protected BufferedStringValue(NamePathResolver nsContext, ValueFactory valueFactory) { + buffer = new StringWriter(); + length = 0; + tmpFile = null; + writer = null; + this.nsContext = nsContext; + this.valueFactory = valueFactory; + } + + /** + * Returns the length of the serialized value. + * + * @return the length of the serialized value + * @throws IOException if an I/O error occurs + */ + public long length() throws IOException { + return length; + } + + private String retrieveString() throws IOException { + String value = retrieve(); + if (base64) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Base64.decode(value, out); + value = new String(out.toByteArray(), StandardCharsets.UTF_8); + } + return value; + } + + /** + * Retrieves the serialized value. + * + * @return the serialized value + * @throws IOException if an I/O error occurs + */ + public String retrieve() throws IOException { + if (buffer != null) { + return buffer.toString(); + } else if (tmpFile != null) { + // close writer first + writer.close(); + if (tmpFile.length() > Integer.MAX_VALUE) { + throw new IOException("size of value is too big, use reader()"); + } + StringBuilder sb = new StringBuilder((int) length); + char[] chunk = new char[0x2000]; + Reader reader = openReader(); + try { + int read; + while ((read = reader.read(chunk)) > -1) { + sb.append(chunk, 0, read); + } + } finally { + reader.close(); + } + return sb.toString(); + } else { + throw new IOException("this instance has already been disposed"); + } + } + + private Reader openReader() throws IOException { + return new InputStreamReader( + new BufferedInputStream(new FileInputStream(tmpFile)), StandardCharsets.UTF_8); + } + + /** + * Returns a Reader for reading the serialized value. + * + * @return a Reader for reading the serialized value. + * @throws IOException if an I/O error occurs + */ + public Reader reader() throws IOException { + if (buffer != null) { + return new StringReader(retrieve()); + } else if (tmpFile != null) { + // close writer first + writer.close(); + return openReader(); + } else { + throw new IOException("this instance has already been disposed"); + } + } + + /** + * Append a portion of an array of characters. + * + * @param chars the characters to be appended + * @param start the index of the first character to append + * @param len the number of characters to append + * @throws IOException if an I/O error occurs + */ + public void append(char[] chars, int start, int len) + throws IOException { + if (buffer != null) { + if (this.length + len > MAX_BUFFER_SIZE) { + // threshold for keeping data in memory exceeded; + // create temp file and spool buffer contents + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + tmpFile = fileFactory.createTransientFile("txt", null, null); + BufferedOutputStream fout = new BufferedOutputStream(new FileOutputStream(tmpFile)); + writer = new OutputStreamWriter(fout, StandardCharsets.UTF_8); + writer.write(buffer.toString()); + writer.write(chars, start, len); + // reset the in-memory buffer + buffer = null; + } else { + buffer.write(chars, start, len); + } + } else if (tmpFile != null) { + writer.write(chars, start, len); + } else { + throw new IOException("this instance has already been disposed"); + } + length += len; + } + + /** + * Close this value. Once a value has been closed, + * further append() invocations will cause an IOException to be thrown. + * + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + if (buffer != null) { + // nop + } else if (tmpFile != null) { + writer.close(); + } else { + throw new IOException("this instance has already been disposed"); + } + } + + //--------------------------------------------------------< TextValue > + + public Value getValue(int targetType, NamePathResolver resolver) + throws ValueFormatException, RepositoryException { + try { + if (targetType == PropertyType.NAME + || targetType == PropertyType.PATH) { + // NAME and PATH require special treatment because + // they depend on the current namespace context + // of the xml document + + // convert serialized value to InternalValue using + // current namespace context of xml document + InternalValue ival = + InternalValue.create(ValueHelper.convert( + retrieve(), targetType, valueFactory), nsContext); + // convert InternalValue to Value using this + // session's namespace mappings + return ValueFormat.getJCRValue(ival, resolver, valueFactory); + } else if (targetType == PropertyType.BINARY) { + if (length() < 0x10000) { + // < 65kb: deserialize BINARY type using String + return ValueHelper.deserialize(retrieve(), targetType, false, valueFactory); + } else { + // >= 65kb: deserialize BINARY type using Reader + Reader reader = reader(); + try { + return ValueHelper.deserialize(reader, targetType, false, valueFactory); + } finally { + reader.close(); + } + } + } else { + // all other types + return ValueHelper.deserialize(retrieveString(), targetType, false, valueFactory); + } + } catch (IOException e) { + String msg = "failed to retrieve serialized value"; + log.debug(msg, e); + throw new RepositoryException(msg, e); + } + } + + public InternalValue getInternalValue(int type) + throws ValueFormatException, RepositoryException { + try { + if (type == PropertyType.BINARY) { + // base64 encoded BINARY type; + // decode using Reader + if (length() < 0x10000) { + // < 65kb: deserialize BINARY type in memory + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Base64.decode(retrieve(), baos); + // no need to close ByteArrayOutputStream + //baos.close(); + return InternalValue.create(baos.toByteArray()); + } else { + // >= 65kb: deserialize BINARY type + // using Reader and temporary file + Base64ReaderInputStream in = new Base64ReaderInputStream(reader()); + return InternalValue.createTemporary(in); + } + } else { + // convert serialized value to InternalValue using + // current namespace context of xml document + return InternalValue.create(ValueHelper.convert( + retrieveString(), type, valueFactory), nsContext); + } + } catch (IOException e) { + throw new RepositoryException("Error accessing property value", e); + } + } + + /** + * This class converts the text read Converts a base64 reader to an input stream. + */ + private static class Base64ReaderInputStream extends InputStream { + + private static final int BUFFER_SIZE = 1024; + private final char[] chars; + private final ByteArrayOutputStream out; + private final Reader reader; + private int pos; + private int remaining; + private byte[] buffer; + + public Base64ReaderInputStream(Reader reader) { + chars = new char[BUFFER_SIZE]; + this.reader = reader; + out = new ByteArrayOutputStream(BUFFER_SIZE); + } + + private void fillBuffer() throws IOException { + int len = reader.read(chars, 0, BUFFER_SIZE); + if (len < 0) { + remaining = -1; + return; + } + Base64.decode(chars, 0, len, out); + buffer = out.toByteArray(); + pos = 0; + remaining = buffer.length; + out.reset(); + } + + public int read() throws IOException { + if (remaining == 0) { + fillBuffer(); + } + if (remaining < 0) { + return -1; + } + remaining--; + return buffer[pos++] & 0xff; + } + } + + /** + * {@inheritDoc} + */ + public void dispose() { + if (buffer != null) { + buffer = null; + } else if (tmpFile != null) { + try { + writer.close(); + tmpFile.delete(); + tmpFile = null; + writer = null; + } catch (IOException e) { + log.warn("Problem disposing property value", e); + } + } else { + log.warn("this instance has already been disposed"); + } + } + + /** + * Whether this value is base64 encoded + * + * @param base64 the flag + */ + public void setBase64(boolean base64) { + this.base64 = base64; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ClonedInputSource.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ClonedInputSource.java new file mode 100644 index 00000000000..f8639fd5ccb --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ClonedInputSource.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.core.xml; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +import javax.jcr.RepositoryException; + +import org.xml.sax.InputSource; + +/** + * Input source that clones existing input source. After cloning the existing + * input source should not be used anymore. To make more copies call + * {@link #cloneInputSource()} method. + */ +public class ClonedInputSource extends InputSource { + private final char[] characterArray; + private final byte[] byteArray; + + /** + * Clone existing input source. + * + * @param input + * @throws RepositoryException + */ + public ClonedInputSource(InputSource input) throws RepositoryException { + + if (input == null) { + throw new IllegalArgumentException( + "Argument 'input' may not be null."); + } + + characterArray = read(input.getCharacterStream()); + byteArray = read(input.getByteStream()); + + setEncoding(input.getEncoding()); + setPublicId(input.getPublicId()); + setSystemId(input.getSystemId()); + if (characterArray != null) { + setCharacterStream(new CharArrayReader(characterArray)); + } + if (byteArray != null) { + setByteStream(new ByteArrayInputStream(byteArray)); + } + } + + private ClonedInputSource(char[] characterArray, byte[] byteArray) { + super(); + this.characterArray = characterArray; + this.byteArray = byteArray; + } + + public char[] getCharacterArray() { + return characterArray; + } + + public byte[] getByteArray() { + return byteArray; + } + + /** + * Make a clone if this input source. The input source being cloned is still + * valid after cloning. + * + * @return input source clone. + */ + public ClonedInputSource cloneInputSource() { + + ClonedInputSource res = new ClonedInputSource(characterArray, byteArray); + + res.setEncoding(getEncoding()); + res.setPublicId(getPublicId()); + res.setSystemId(getSystemId()); + + if (byteArray != null) { + res.setByteStream(new ByteArrayInputStream(byteArray)); + } + if (characterArray != null) { + res.setCharacterStream(new CharArrayReader(characterArray)); + } + + return res; + } + + private static byte[] read(InputStream stream) throws RepositoryException { + if (stream != null) { + try { + final int bufferSize = Math.min(stream.available(), 4096); + ByteArrayOutputStream s = new ByteArrayOutputStream(bufferSize); + + byte[] buffer = new byte[bufferSize]; + while (true) { + int numRead = stream.read(buffer); + if (numRead > 0) { + s.write(buffer, 0, numRead); + } + if (numRead != bufferSize) { + break; + } + } + + return s.toByteArray(); + } catch (IOException e) { + throw new RepositoryException(e); + } finally { + try { + stream.close(); + } catch (IOException ignore) { + + } + } + } else { + return null; + } + } + + private static char[] read(Reader reader) throws RepositoryException { + if (reader != null) { + try { + final int bufferSize = 4096; + CharArrayWriter w = new CharArrayWriter(bufferSize); + + char[] buffer = new char[bufferSize]; + while (true) { + int numRead = reader.read(buffer); + if (numRead > 0) { + w.write(buffer, 0, numRead); + } + if (numRead != bufferSize) { + break; + } + } + return w.toCharArray(); + } catch (IOException e) { + throw new RepositoryException(e); + } finally { + try { + reader.close(); + } catch (IOException ignore) { + + } + } + } else { + return null; + } + + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedItemImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedItemImporter.java new file mode 100644 index 00000000000..aff0090038e --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedItemImporter.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.core.xml; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.RepositoryException; +import java.util.List; + +/** + * DefaultProtectedItemImporter... + */ +public class DefaultProtectedItemImporter implements ProtectedPropertyImporter, ProtectedNodeImporter { + + protected JackrabbitSession session; + protected NamePathResolver resolver; + protected boolean isWorkspaceImport; + protected int uuidBehavior; + protected ReferenceChangeTracker referenceTracker; + + public DefaultProtectedItemImporter() { + } + + //----------------------------------------------< ProtectedItemImporter >--- + /** + * @see ProtectedItemImporter#init(org.apache.jackrabbit.api.JackrabbitSession, org.apache.jackrabbit.spi.commons.conversion.NamePathResolver, boolean, int, org.apache.jackrabbit.core.util.ReferenceChangeTracker) + */ + public boolean init(JackrabbitSession session, NamePathResolver resolver, boolean isWorkspaceImport, int uuidBehavior, ReferenceChangeTracker referenceTracker) { + this.session = session; + this.resolver = resolver; + this.isWorkspaceImport = isWorkspaceImport; + this.uuidBehavior = uuidBehavior; + this.referenceTracker = referenceTracker; + return true; + } + + //----------------------------------------------< ProtectedNodeImporter >--- + /** + * Always returns false. + * + * @see ProtectedNodeImporter#start(org.apache.jackrabbit.core.NodeImpl) + */ + public boolean start(NodeImpl protectedParent) throws RepositoryException { + return false; + } + + /** + * Always returns false. + * + * @see ProtectedNodeImporter#start(org.apache.jackrabbit.core.state.NodeState) + */ + public boolean start(NodeState protectedParent) throws RepositoryException { + return false; + } + + /** + * Does nothing. + * + * @see ProtectedNodeImporter#end(NodeImpl) + */ + public void end(NodeImpl protectedParent) throws RepositoryException { + } + + /** + * Does nothing. + * + * @see ProtectedNodeImporter#end(NodeState) + */ + public void end(NodeState protectedParent) throws RepositoryException { + } + + /** + * Does nothing. + * + * @see ProtectedNodeImporter#startChildInfo(NodeInfo, java.util.List) + */ + public void startChildInfo(NodeInfo childInfo, List propInfos) throws RepositoryException { + } + + /** + * Does nothing. + * + * @see ProtectedNodeImporter#endChildInfo() + */ + public void endChildInfo() throws RepositoryException { + } + + + + //------------------------------------------< ProtectedPropertyImporter >--- + /** + * Always returns false. + * + * @see ProtectedPropertyImporter#handlePropInfo(org.apache.jackrabbit.core.NodeImpl, PropInfo, org.apache.jackrabbit.spi.QPropertyDefinition) + */ + public boolean handlePropInfo(NodeImpl parent, PropInfo protectedPropInfo, QPropertyDefinition def) throws RepositoryException { + return false; + } + + /** + * Always returns false. + * + * @see ProtectedPropertyImporter#handlePropInfo(org.apache.jackrabbit.core.state.NodeState, PropInfo, QPropertyDefinition) + */ + public boolean handlePropInfo(NodeState parent, PropInfo protectedPropInfo, QPropertyDefinition def) throws RepositoryException { + return false; + } + + /** + * Always returns false. + * + * @see ProtectedPropertyImporter#processReferences() + */ + public void processReferences() throws RepositoryException { + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedNodeImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedNodeImporter.java new file mode 100644 index 00000000000..a2f48aff653 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedNodeImporter.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.core.xml; + +/** + * Default implementation that isn't able to handle any protected nodes. + */ +public class DefaultProtectedNodeImporter extends DefaultProtectedItemImporter { + + public DefaultProtectedNodeImporter() { + super(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedPropertyImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedPropertyImporter.java new file mode 100644 index 00000000000..bbf4406b2dd --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DefaultProtectedPropertyImporter.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.core.xml; + +/** + * Default implementation that isn't able to handle any protected properties. + */ +public class DefaultProtectedPropertyImporter extends DefaultProtectedItemImporter { + + protected boolean isWorkspaceImport; + + protected int uuidBehavior; + + public DefaultProtectedPropertyImporter() { + super(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DocViewImportHandler.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DocViewImportHandler.java new file mode 100644 index 00000000000..ed29a75466c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/DocViewImportHandler.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.core.xml; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameParser; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.util.ISO9075; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +/** + * DocViewImportHandler processes Document View XML SAX events + * and 'translates' them into {@link Importer} method calls. + */ +class DocViewImportHandler extends TargetImportHandler { + + private static Logger log = LoggerFactory.getLogger(DocViewImportHandler.class); + + /** + * stack of NodeInfo instances; an instance is pushed onto the stack + * in the startElement method and is popped from the stack in the + * endElement method. + */ + private final Stack stack = new Stack(); + // buffer used to merge adjacent character data + private BufferedStringValue textHandler = null; + + /** + * Constructs a new DocViewImportHandler. + * + * @param importer the importer + * @param valueFactory a value factory + */ + DocViewImportHandler(Importer importer, ValueFactory valueFactory) { + super(importer, valueFactory); + } + + /** + * Parses the given string as a list of JCR names. Any whitespace sequence + * is supported as a names separator instead of just a single space to + * be more liberal in what we accept. The current namespace context is + * used to convert the prefixed name strings to QNames. + * + * @param value string value + * @return the parsed names + * @throws SAXException if an invalid name was encountered + */ + private Name[] parseNames(String value) throws SAXException { + String[] names = value.split("\\p{Space}+"); + Name[] qnames = new Name[names.length]; + for (int i = 0; i < names.length; i++) { + try { + qnames[i] = resolver.getQName(names[i]); + } catch (NameException ne) { + throw new SAXException("Invalid name: " + names[i], ne); + } catch (NamespaceException e) { + throw new SAXException("Invalid name: " + names[i], e); + } + } + return qnames; + } + + /** + * Appends the given character data to the internal buffer. + * + * @param ch the characters to be appended + * @param start the index of the first character to append + * @param length the number of characters to append + * @throws SAXException if an error occurs + * @see #characters(char[], int, int) + * @see #ignorableWhitespace(char[], int, int) + * @see #processCharacters() + */ + private void appendCharacters(char[] ch, int start, int length) + throws SAXException { + if (textHandler == null) { + textHandler = new BufferedStringValue(resolver, valueFactory); + } + try { + textHandler.append(ch, start, length); + } catch (IOException ioe) { + String msg = "internal error while processing internal buffer data"; + log.error(msg, ioe); + throw new SAXException(msg, ioe); + } + } + + /** + * Translates character data reported by the + * {@link #characters(char[], int, int)} & + * {@link #ignorableWhitespace(char[], int, int)} SAX events + * into a jcr:xmltext child node with one + * jcr:xmlcharacters property. + * + * @throws SAXException if an error occurs + * @see #appendCharacters(char[], int, int) + */ + private void processCharacters() + throws SAXException { + try { + if (textHandler != null && textHandler.length() > 0) { + // there is character data that needs to be added to + // the current node + + // check for pure whitespace character data + Reader reader = textHandler.reader(); + try { + int ch; + while ((ch = reader.read()) != -1) { + if (ch > 0x20) { + break; + } + } + if (ch == -1) { + // the character data consists of pure whitespace, ignore + log.debug("ignoring pure whitespace character data..."); + // reset handler + textHandler.dispose(); + textHandler = null; + return; + } + } finally { + reader.close(); + } + + NodeInfo node = + new NodeInfo(NameConstants.JCR_XMLTEXT, null, null, null); + TextValue[] values = + new TextValue[]{textHandler}; + ArrayList props = new ArrayList(); + props.add(new PropInfo( + NameConstants.JCR_XMLCHARACTERS, + PropertyType.STRING, values)); + // call Importer + importer.startNode(node, props); + importer.endNode(node); + + // reset handler + textHandler.dispose(); + textHandler = null; + } + } catch (IOException ioe) { + String msg = "internal error while processing internal buffer data"; + log.error(msg, ioe); + throw new SAXException(msg, ioe); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + /** + * Processes the given name, i.e. decodes it and checks + * the format of the decoded name. + * + * @param name name to process + * @return the decoded and valid jcr name or the original name if it is + * not encoded or if the resulting decoded name would be illegal. + */ + private Name processName(Name name) { + String decodedLocalName = ISO9075.decode(name.getLocalName()); + Name decoded = NameFactoryImpl.getInstance().create(name.getNamespaceURI(), decodedLocalName); + if (!decoded.equals(name)) { + // only need to check format of decoded name since + // an xml name is always a legal jcr name + // (http://issues.apache.org/jira/browse/JCR-821) + try { + NameParser.checkFormat(decoded.getLocalName()); + return decoded; + } catch (IllegalNameException ine) { + // decoded name would be illegal according to jsr 170, + // use encoded name as a fallback + log.warn("encountered illegal encoded name '" + + name.getLocalName() + "': " + + ine.getMessage()); + } + } + return name; + } + + //-------------------------------------------------------< ContentHandler > + + /** + * {@inheritDoc} + *

    + * See also {@link org.apache.jackrabbit.commons.xml.Exporter#exportProperties(Node)} + * regarding special handling of multi-valued properties on export. + */ + @Override + public void startElement(String namespaceURI, String localName, + String qName, Attributes atts) + throws SAXException { + // process buffered character data + processCharacters(); + + try { + Name nodeName = NameFactoryImpl.getInstance().create(namespaceURI, localName); + // process node name + nodeName = processName(nodeName); + + // properties + NodeId id = null; + Name nodeTypeName = null; + Name[] mixinTypes = null; + + List props = new ArrayList(atts.getLength()); + for (int i = 0; i < atts.getLength(); i++) { + if (atts.getURI(i).equals(Name.NS_XMLNS_URI)) { + // skip namespace declarations reported as attributes + // see http://issues.apache.org/jira/browse/JCR-620#action_12448164 + continue; + } + Name propName = NameFactoryImpl.getInstance().create(atts.getURI(i), atts.getLocalName(i)); + // process property name + propName = processName(propName); + + // value(s) + String attrValue = atts.getValue(i); + TextValue[] propValues; + + // always assume single-valued property for the time being + // until a way of properly serializing/detecting multi-valued + // properties on re-import is found (see JCR-325); + // see also DocViewSAXEventGenerator#leavingProperties(Node, int) + // todo proper multi-value serialization support + propValues = new TextValue[1]; + propValues[0] = new StringValue(attrValue, resolver, valueFactory); + + if (propName.equals(NameConstants.JCR_PRIMARYTYPE)) { + // jcr:primaryType + if (attrValue.length() > 0) { + try { + nodeTypeName = resolver.getQName(attrValue); + } catch (NameException ne) { + throw new SAXException("illegal jcr:primaryType value: " + + attrValue, ne); + } + } + } else if (propName.equals(NameConstants.JCR_MIXINTYPES)) { + // jcr:mixinTypes + mixinTypes = parseNames(attrValue); + } else if (propName.equals(NameConstants.JCR_UUID)) { + // jcr:uuid + if (attrValue.length() > 0) { + id = NodeId.valueOf(attrValue); + } + } else { + props.add(new PropInfo( + propName, PropertyType.UNDEFINED, propValues)); + } + } + + NodeInfo node = + new NodeInfo(nodeName, nodeTypeName, mixinTypes, id); + // all information has been collected, now delegate to importer + importer.startNode(node, props); + // push current node data onto stack + stack.push(node); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + /** + * buffer data reported by the characters event; + * will be processed on the next endElement or startElement event. + */ + appendCharacters(ch, start, length); + } + + /** + * {@inheritDoc} + */ + @Override + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + /** + * buffer data reported by the ignorableWhitespace event; + * will be processed on the next endElement or startElement event. + */ + appendCharacters(ch, start, length); + } + + /** + * {@inheritDoc} + */ + @Override + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + // process buffered character data + processCharacters(); + + NodeInfo node = stack.peek(); + try { + // call Importer + importer.endNode(node); + } catch (RepositoryException re) { + throw new SAXException(re); + } + // we're done with this node, pop it from stack + stack.pop(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ImportHandler.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ImportHandler.java new file mode 100644 index 00000000000..bde4ced2fd9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ImportHandler.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.xml; + +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.commons.NamespaceHelper; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * An ImportHandler instance can be used to import serialized + * data in System View XML or Document View XML. Processing of the XML is + * handled by specialized ContentHandlers + * (i.e. SysViewImportHandler and DocViewImportHandler). + *

    + * The actual task of importing though is delegated to the implementation of + * the {@link Importer} interface. + *

    + * Important Note: + *

    + * These SAX Event Handlers expect that Namespace URI's and local names are + * reported in the start/endElement events and that + * start/endPrefixMapping events are reported + * (i.e. default SAX2 Namespace processing). + */ +public class ImportHandler extends DefaultHandler { + + private static Logger log = LoggerFactory.getLogger(ImportHandler.class); + + protected final Importer importer; + + private final NamespaceHelper helper; + + private final ValueFactory valueFactory; + + protected Locator locator; + + private TargetImportHandler targetHandler = null; + + /** + * The local namespace mappings reported by + * {@link #startPrefixMapping(String, String)}. These mappings are used + * to instantiate the local namespace context in + * {@link #startElement(String, String, String, Attributes)}. + */ + private Map localNamespaceMappings; + + public ImportHandler(Importer importer, Session session) + throws RepositoryException { + this.importer = importer; + this.helper = new NamespaceHelper(session); + this.localNamespaceMappings = helper.getNamespaces(); + this.valueFactory = session.getValueFactory(); + } + + //---------------------------------------------------------< ErrorHandler > + /** + * {@inheritDoc} + */ + @Override + public void warning(SAXParseException e) throws SAXException { + // log exception and carry on... + log.warn("warning encountered at line: " + e.getLineNumber() + + ", column: " + e.getColumnNumber() + + " while parsing XML stream", e); + } + + /** + * {@inheritDoc} + */ + @Override + public void error(SAXParseException e) throws SAXException { + // log exception and carry on... + log.error("error encountered at line: " + e.getLineNumber() + + ", column: " + e.getColumnNumber() + + " while parsing XML stream: " + e.toString()); + } + + /** + * {@inheritDoc} + */ + @Override + public void fatalError(SAXParseException e) throws SAXException { + // log and re-throw exception + log.error("fatal error encountered at line: " + e.getLineNumber() + + ", column: " + e.getColumnNumber() + + " while parsing XML stream: " + e.toString()); + throw e; + } + + //-------------------------------------------------------< ContentHandler > + + /** + * {@inheritDoc} + */ + @Override + public void endDocument() throws SAXException { + // delegate to target handler + if (targetHandler != null) { + targetHandler.endDocument(); + } + } + + /** + * Records the given namespace mapping to be included in the local + * namespace context. The local namespace context is instantiated + * in {@link #startElement(String, String, String, Attributes)} using + * all the the namespace mappings recorded for the current XML element. + *

    + * The namespace is also recorded in the persistent namespace registry + * unless it is already known. + * + * @param prefix namespace prefix + * @param uri namespace URI + */ + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + localNamespaceMappings.put(prefix, uri); + try { + helper.registerNamespace(prefix, uri); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void startElement(String namespaceURI, String localName, String qName, + Attributes atts) throws SAXException { + if (targetHandler == null) { + // the namespace of the first element determines the type of XML + // (system view/document view) + if (Name.NS_SV_URI.equals(namespaceURI)) { + targetHandler = new SysViewImportHandler(importer, valueFactory); + } else { + targetHandler = new DocViewImportHandler(importer, valueFactory); + } + + targetHandler.startDocument(); + } + + // Start a namespace context for this element + targetHandler.startNamespaceContext(localNamespaceMappings); + localNamespaceMappings.clear(); + + // delegate to target handler + targetHandler.startElement(namespaceURI, localName, qName, atts); + } + + /** + * {@inheritDoc} + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + // delegate to target handler + targetHandler.characters(ch, start, length); + } + + /** + * Delegates the call to the underlying target handler and asks the + * handler to end the current namespace context. + * {@inheritDoc} + */ + @Override + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + targetHandler.endElement(namespaceURI, localName, qName); + targetHandler.endNamespaceContext(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/Importer.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/Importer.java new file mode 100644 index 00000000000..81b343c2047 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/Importer.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.core.xml; + +import java.util.List; + +import javax.jcr.RepositoryException; + +/** + * Content importer. The XML import handlers use this interface to submit + * the parsed content to the repository. The implementation of this class + * decides how the content is actually persisted; either through the + * transient space of a session, or directly into the workspace. + */ +public interface Importer { + + /** + * Called once at the beginning of the content import. + * + * @throws RepositoryException on a repository error + */ + void start() throws RepositoryException; + + /** + * Called to start the import of a node. Information about the + * imported node and all it's properties are passed as arguments. + * Possible child nodes are imported recursively using this same + * method until a {@link #endNode(NodeInfo)} call is made with the + * same node information. + * + * @param nodeInfo information about the node being imported + * @param propInfos information about the properties being imported + * (list of {@link PropInfo} instances) + * @throws RepositoryException on a repository error + */ + void startNode(NodeInfo nodeInfo, List propInfos) + throws RepositoryException; + + /** + * Called to end the import of a node. This method is called after + * a {@link #startNode(NodeInfo, List)} call with the stame node + * information and after all the possible child nodes have been + * imported with respective startNode/endNode calls. + *

    + * Just like XML elements, the startNode/endNode calls are guaranteed + * to be properly nested and complete. + * + * @param nodeInfo information about the node being imported + * @throws RepositoryException on a repository error + */ + void endNode(NodeInfo nodeInfo) throws RepositoryException; + + /** + * Called once at the end of the content import. + * + * @throws RepositoryException on a repository error + */ + void end() throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/NamespaceContext.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/NamespaceContext.java new file mode 100644 index 00000000000..31e324d64b4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/NamespaceContext.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.core.xml; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.NamespaceException; +import java.util.HashMap; +import java.util.Map; + +/** + * Hierarchically scoped namespace resolver. Each NamespaceContext instance + * contains an immutable set of namespace mappings and a reference (possibly + * null) to a parent NamespaceContext. Namespace resolution is + * performed by first looking at the local namespace mappings and then using + * the parent resolver if no local match is found. + *

    + * The local namespace mappings are stored internally as two hash maps, one + * that maps the namespace prefixes to namespace URIs and another that contains + * the reverse mapping. + */ +class NamespaceContext implements NamespaceResolver { + + /** + * The parent namespace context. + */ + private final NamespaceContext parent; + + /** + * The namespace prefix to namespace URI mapping. + */ + private final Map prefixToURI; + + /** + * The namespace URI to namespace prefix mapping. + */ + private final Map uriToPrefix; + + /** + * Creates a NamespaceContext instance with the given parent context + * and local namespace mappings. + * + * @param parent parent context + * @param mappings local namespace mappings (prefix -> URI) + */ + public NamespaceContext(NamespaceContext parent, Map mappings) { + this.parent = parent; + this.prefixToURI = new HashMap(); + this.uriToPrefix = new HashMap(); + + for (Map.Entry mapping : mappings.entrySet()) { + prefixToURI.put(mapping.getKey(), mapping.getValue()); + uriToPrefix.put(mapping.getValue(), mapping.getKey()); + } + } + + /** + * Returns the parent namespace context. + * + * @return parent namespace context + */ + public NamespaceContext getParent() { + return parent; + + } + //------------------------------------------------< NamespaceResolver > + + /** + * Returns the namespace URI mapped to the given prefix. + * + * @param prefix namespace prefix + * @return namespace URI + * @throws NamespaceException if the prefix is not mapped + */ + public String getURI(String prefix) throws NamespaceException { + NamespaceContext current = this; + while (current != null) { + String uri = (String) current.prefixToURI.get(prefix); + if (uri != null) { + return uri; + } + current = current.parent; + } + throw new NamespaceException("Unknown prefix: " + prefix); + } + + /** + * Returns the namespace prefix mapped to the given URI. + * + * @param uri namespace URI + * @return namespace prefix + * @throws NamespaceException if the URI is not mapped + */ + public String getPrefix(String uri) throws NamespaceException { + NamespaceContext current = this; + while (current != null) { + String prefix = (String) current.uriToPrefix.get(uri); + if (prefix != null) { + return prefix; + } + current = current.parent; + } + throw new NamespaceException("Unknown URI: " + uri); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/NodeInfo.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/NodeInfo.java new file mode 100644 index 00000000000..84160b908e4 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/NodeInfo.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.core.xml; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; + +/** + * Information about a node being imported. This class is used + * by the XML import handlers to pass the parsed node information through + * the {@link Importer} interface to the actual import process. + *

    + * An instance of this class is simply a container for the node name, + * node identifier, 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 Name name; + + /** + * Name of the primary type of the node being imported. + */ + private final Name nodeTypeName; + + /** + * Names of the mixin types of the node being imported. + */ + private final Name[] mixinNames; + + /** + * Identifier of the node being imported. + */ + private final NodeId id; + + /** + * Creates a node information instance. + * + * @param name name of the node being imported + * @param nodeTypeName name of the primary type of the node being imported + * @param mixinNames names of the mixin types of the node being imported + * @param id identifier of the node being imported + */ + public NodeInfo(Name name, Name nodeTypeName, Name[] mixinNames, + NodeId id) { + this.name = name; + this.nodeTypeName = nodeTypeName; + this.mixinNames = mixinNames; + this.id = id; + } + + /** + * Returns the name of the node being imported. + * + * @return node name + */ + public Name getName() { + return name; + } + + /** + * Returns the name of the primary type of the node being imported. + * + * @return primary type name + */ + public Name getNodeTypeName() { + return nodeTypeName; + } + + /** + * Returns the names of the mixin types of the node being imported. + * + * @return mixin type names + */ + public Name[] getMixinNames() { + return mixinNames; + } + + /** + * Returns the identifier of the node being imported. + * + * @return node identifier + */ + public NodeId getId() { + return id; + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/PropInfo.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/PropInfo.java new file mode 100644 index 00000000000..2c92c467d44 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/PropInfo.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.core.xml; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * Information about a property being imported. This class is used + * by the XML import handlers to pass the parsed property information + * through the {@link Importer} interface to the actual 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 NodeImpl} instance through a session or directly to a + * {@link NodeState} instance in a workspace. + */ +public class PropInfo { + + /** + * Name of the property being imported. + */ + private final Name name; + + /** + * Type of the property being imported. + */ + private final int 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(Name name, int 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(Name name, int 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 int getTargetType(QPropertyDefinition def) { + int target = def.getRequiredType(); + if (target != PropertyType.UNDEFINED) { + return target; + } else if (type != PropertyType.UNDEFINED) { + return type; + } else { + return PropertyType.STRING; + } + } + + public QPropertyDefinition getApplicablePropertyDef(EffectiveNodeType ent) + throws ConstraintViolationException { + if (multipleStatus == MultipleStatus.MULTIPLE) { + return ent.getApplicablePropertyDef(name, type, true); + } else if (multipleStatus == MultipleStatus.SINGLE) { + return ent.getApplicablePropertyDef(name, type, false); + } else { + // multipleStatus == MultipleStatus.UNKNOWN + if (values.length == 1) { + // one value => could be single- or multi-valued + return ent.getApplicablePropertyDef(name, type); + } else { + // zero or more than one values => must be multi-valued + return ent.getApplicablePropertyDef(name, type, true); + } + } + } + + public Name getName() { + return name; + } + + public int getType() { + return type; + } + + public TextValue[] getTextValues() { + return values; + } + + public Value[] getValues(int targetType, NamePathResolver resolver) throws RepositoryException { + Value[] va = new Value[values.length]; + for (int i = 0; i < values.length; i++) { + va[i] = values[i].getValue(targetType, resolver); + } + return va; + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedItemImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedItemImporter.java new file mode 100644 index 00000000000..c87a3325dd7 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedItemImporter.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.core.xml; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.RepositoryException; + +/** + * ProtectedItemImporter... + */ +public interface ProtectedItemImporter { + + /** + * + * @param session + * @param resolver + * @param isWorkspaceImport + * @param uuidBehavior + * @param referenceTracker + * @return + */ + boolean init(JackrabbitSession session, NamePathResolver resolver, + 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. This method is called + * from {@link org.apache.jackrabbit.core.xml.Importer#end()}. + * + * @throws javax.jcr.RepositoryException If an error occurs. + */ + void processReferences() throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedNodeImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedNodeImporter.java new file mode 100644 index 00000000000..1c6fdc9caea --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedNodeImporter.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.core.xml; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.state.NodeState; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import java.util.List; + +/** + * ProtectedNodeImporter provides means to import protected + * Nodes and the subtree defined below such nodes. + *

    + * The import of a protected tree is started by the Importer by + * calling {@link #start(NodeImpl)}. If the ProtectedNodeImporter + * is able to deal with that type of protected node, it is in charge of dealing + * with all subsequent child NodeInfos present below the protected + * parent until {@link #end(NodeImpl)} 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 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(NodeImpl)}. + * @throws RepositoryException If an error occurs. + */ + boolean start(NodeImpl protectedParent) throws IllegalStateException, + RepositoryException; + + /** + * 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 true If this importer is able to deal with the + * tree that may be present below the given protected NodeState. + * @throws IllegalStateException If this method is called on + * this importer without having reached {@link #end(NodeState)}. + * @throws RepositoryException If an error occurs. + */ + boolean start(NodeState protectedParent) throws IllegalStateException, + RepositoryException; + + + /** + * Informs this importer that the tree to be imported below + * protectedParent has bee completed. This allows the importer + * to be reset in order to be able to deal with another call to + * {@link #start(NodeImpl)}. + *

    + * If {@link #start(NodeImpl)} hasn't been called before, this method returns + * silently. + * + * @param protectedParent + * @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(NodeImpl protectedParent) throws IllegalStateException, + ConstraintViolationException, RepositoryException; + + /** + * Informs this importer that the tree to be imported below + * protectedParent has bee completed. This allows the importer + * to be reset in order to be able to deal with another call to + * {@link #start(NodeState)}. + *

    + * If {@link #start(NodeState)} hasn't been called before, this method returns + * silently. + * + * @param protectedParent + * @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(NodeState protectedParent) throws IllegalStateException, + ConstraintViolationException, RepositoryException; + + /** + * Informs this importer about a new childInfo and it's properties. + * If the importer is able to successfully import the given information + * this method returns silently. Otherwise + * 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(NodeImpl)} 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(NodeImpl)} 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; +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedPropertyImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedPropertyImporter.java new file mode 100644 index 00000000000..30f754ea3b9 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/ProtectedPropertyImporter.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.core.xml; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.QPropertyDefinition; + +import javax.jcr.RepositoryException; + +/** + * ProtectedPropertyImporter is in charge of importing single + * properties with a protected QPropertyDefinition. + * + * @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 PropInfo to be imported. + * @param def The property definition determined by the importer that + * calls this method. + * @return true If the property could be successfully imported; + * false otherwise. + * @throws RepositoryException If an error occurs. + */ + boolean handlePropInfo(NodeImpl parent, PropInfo protectedPropInfo, + QPropertyDefinition def) + throws RepositoryException; + + /** + * Handles a single protected property. + * + * @param parent The affected parent node. + * @param protectedPropInfo The PropInfo to be imported. + * @param def The property definition determined by the importer that + * calls this method. + * @return true If the property could be successfully imported; + * false otherwise. + * @throws RepositoryException If an error occurs. + */ + boolean handlePropInfo(NodeState parent, PropInfo protectedPropInfo, + QPropertyDefinition def) + throws RepositoryException; + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/SessionImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/SessionImporter.java new file mode 100644 index 00000000000..62739fc8e69 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/SessionImporter.java @@ -0,0 +1,526 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.xml; + +import java.util.Iterator; +import java.util.List; +import java.util.Stack; +import java.util.ArrayList; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeDefinition; + +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.config.ImportConfig; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SessionImporter ... + */ +public class SessionImporter implements Importer { + + private static Logger log = LoggerFactory.getLogger(SessionImporter.class); + + private final SessionImpl session; + private final NodeImpl importTargetNode; + private final int uuidBehavior; + + private Stack parents; + + /** + * helper object that keeps track of remapped uuid's and imported reference + * properties that might need correcting depending on the uuid mappings + */ + private final ReferenceChangeTracker refTracker; + + private final List pItemImporters = new ArrayList(); + + /** + * Currently active importer for protected nodes. + */ + private ProtectedNodeImporter pnImporter = null; + + /** + * Creates a new SessionImporter instance. + * + * @param importTargetNode the target node + * @param session session + * @param uuidBehavior any of the constants declared by + * {@link javax.jcr.ImportUUIDBehavior} + */ + public SessionImporter(NodeImpl importTargetNode, + SessionImpl session, + int uuidBehavior) { + this(importTargetNode, session, uuidBehavior, null); + } + + /** + * Creates a new SessionImporter instance. + * + * @param importTargetNode the target node + * @param session session + * @param uuidBehavior the desired uuid behavior as defined + * by {@link javax.jcr.ImportUUIDBehavior}. + * @param config + */ + public SessionImporter(NodeImpl importTargetNode, SessionImpl session, + int uuidBehavior, ImportConfig config) { + this.importTargetNode = importTargetNode; + this.session = session; + this.uuidBehavior = uuidBehavior; + + refTracker = new ReferenceChangeTracker(); + + parents = new Stack(); + parents.push(importTargetNode); + + if (config != null) { + List iList = config.getProtectedItemImporters(); + for (ProtectedItemImporter importer : iList) { + if (importer.init(session, session, false, uuidBehavior, refTracker)) { + pItemImporters.add(importer); + } + } + } + + // missing config -> initialize defaults. + if (pItemImporters.isEmpty()) { + ProtectedItemImporter def = new DefaultProtectedItemImporter(); + if (def.init(session, session, false, uuidBehavior, refTracker)) { + pItemImporters.add(def); + } + } + } + + /** + * make sure the editing session is allowed create nodes with a + * specified node type (and ev. mixins),
    + * NOTE: this check is not executed in a single place as the parent + * may change in case of + * {@link javax.jcr.ImportUUIDBehavior#IMPORT_UUID_COLLISION_REPLACE_EXISTING IMPORT_UUID_COLLISION_REPLACE_EXISTING}. + * + * @param parent parent node + * @param nodeName the name + * @throws RepositoryException if an error occurs + */ + protected void checkPermission(NodeImpl parent, Name nodeName) + throws RepositoryException { + if (!session.getAccessManager().isGranted(session.getQPath(parent.getPath()), nodeName, Permission.NODE_TYPE_MNGMT)) { + throw new AccessDeniedException("Insufficient permission."); + } + } + + protected NodeImpl createNode(NodeImpl parent, + Name nodeName, + Name nodeTypeName, + Name[] mixinNames, + NodeId id) + throws RepositoryException { + NodeImpl node; + + // add node + node = parent.addNode(nodeName, nodeTypeName, id); + // add mixins + if (mixinNames != null) { + for (Name mixinName : mixinNames) { + node.addMixin(mixinName); + } + } + return node; + } + + + protected void createProperty(NodeImpl node, PropInfo pInfo, QPropertyDefinition def) throws RepositoryException { + // convert serialized values to Value objects + Value[] va = pInfo.getValues(pInfo.getTargetType(def), session); + + // multi- or single-valued property? + Name name = pInfo.getName(); + int type = pInfo.getType(); + if (va.length == 1 && !def.isMultiple()) { + Exception e = null; + try { + // set single-value + node.setProperty(name, va[0]); + } catch (ValueFormatException vfe) { + e = vfe; + } catch (ConstraintViolationException cve) { + e = cve; + } + if (e != null) { + // setting single-value failed, try setting value array + // as a last resort (in case there are ambiguous property + // definitions) + node.setProperty(name, va, type); + } + } else { + // can only be multi-valued (n == 0 || n > 1) + node.setProperty(name, va, type); + } + if (type == PropertyType.REFERENCE || type == PropertyType.WEAKREFERENCE) { + // store reference for later resolution + refTracker.processedReference(node.getProperty(name)); + } + } + + protected NodeImpl resolveUUIDConflict(NodeImpl parent, + NodeId conflictingId, + NodeInfo nodeInfo) + throws RepositoryException { + NodeImpl node; + + NodeImpl conflicting; + try { + conflicting = session.getNodeById(conflictingId); + } catch (ItemNotFoundException infe) { + // conflicting node can't be read, + // most likely due to lack of read permission + conflicting = null; + } + + if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) { + // create new with new uuid + checkPermission(parent, nodeInfo.getName()); + node = createNode(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), null); + // remember uuid mapping + if (node.isNodeType(NameConstants.MIX_REFERENCEABLE)) { + refTracker.mappedId(nodeInfo.getId(), node.getNodeId()); + } + } else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW) { + // if conflicting node is shareable, then clone it + if (conflicting != null + && conflicting.isNodeType(NameConstants.MIX_SHAREABLE)) { + parent.clone(conflicting, nodeInfo.getName()); + return null; + } + String msg = "a node with uuid " + nodeInfo.getId() + " already exists!"; + log.debug(msg); + throw new ItemExistsException(msg); + } else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING) { + if (conflicting == null) { + // since the conflicting node can't be read, + // we can't remove it + String msg = "node with uuid " + conflictingId + " cannot be removed"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // make sure conflicting node is not importTargetNode or an ancestor thereof + if (importTargetNode.getPath().startsWith(conflicting.getPath())) { + String msg = "cannot remove ancestor node"; + log.debug(msg); + throw new ConstraintViolationException (msg); + } + // remove conflicting + conflicting.remove(); + // create new with given uuid + checkPermission(parent, nodeInfo.getName()); + node = createNode(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), + nodeInfo.getId()); + } else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING) { + if (conflicting == null) { + // since the conflicting node can't be read, + // we can't replace it + String msg = "node with uuid " + conflictingId + " cannot be replaced"; + log.debug(msg); + throw new RepositoryException(msg); + } + + if (conflicting.getDepth() == 0) { + String msg = "root node cannot be replaced"; + log.debug(msg); + throw new RepositoryException(msg); + } + // 'replace' current parent with parent of conflicting + parent = (NodeImpl) conflicting.getParent(); + + // replace child node + checkPermission(parent, nodeInfo.getName()); + node = parent.replaceChildNode(nodeInfo.getId(), nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames()); + } else { + String msg = "unknown uuidBehavior: " + uuidBehavior; + log.debug(msg); + throw new RepositoryException(msg); + } + return node; + } + + //-------------------------------------------------------------< Importer > + /** + * {@inheritDoc} + */ + public void start() throws RepositoryException { + // nop + } + + /** + * {@inheritDoc} + */ + public void startNode(NodeInfo nodeInfo, List propInfos) + throws RepositoryException { + NodeImpl parent = parents.peek(); + + // process node + + NodeImpl node = null; + NodeId id = nodeInfo.getId(); + Name nodeName = nodeInfo.getName(); + Name ntName = nodeInfo.getNodeTypeName(); + Name[] mixins = nodeInfo.getMixinNames(); + + if (parent == null) { + log.debug("Skipping node: " + nodeName); + // parent node was skipped, skip this child node too + parents.push(null); // push null onto stack for skipped node + // notify the p-i-importer + if (pnImporter != null) { + pnImporter.startChildInfo(nodeInfo, propInfos); + } + return; + } + + if (parent.getDefinition().isProtected()) { + // skip protected node + parents.push(null); + log.debug("Skipping protected node: " + nodeName); + + if (pnImporter != null) { + // pnImporter was already started (current nodeInfo is a sibling) + // notify it about this child node. + pnImporter.startChildInfo(nodeInfo, propInfos); + } else { + // no importer defined yet: + // test if there is a ProtectedNodeImporter among the configured + // importers that can handle this. + // if there is one, notify the ProtectedNodeImporter about the + // start of a item tree that is protected by this parent. If it + // potentially is able to deal with it, notify it about the child node. + for (ProtectedItemImporter pni : pItemImporters) { + if (pni instanceof ProtectedNodeImporter && ((ProtectedNodeImporter) pni).start(parent)) { + log.debug("Protected node -> delegated to ProtectedNodeImporter"); + pnImporter = (ProtectedNodeImporter) pni; + pnImporter.startChildInfo(nodeInfo, propInfos); + break; + } /* else: p-i-Importer isn't able to deal with the protected tree. + try next. and if none can handle the passed parent the + tree below will be skipped */ + } + } + return; + } + + if (parent.hasNode(nodeName)) { + // a node with that name already exists... + NodeImpl existing = parent.getNode(nodeName); + NodeDefinition def = existing.getDefinition(); + if (!def.allowsSameNameSiblings()) { + // existing doesn't allow same-name siblings, + // check for potential conflicts + if (def.isProtected() && existing.isNodeType(ntName)) { + /* + use the existing node as parent for the possible subsequent + import of a protected tree, that the protected node importer + may or may not be able to deal with. + -> upon the next 'startNode' the check for the parent being + protected will notify the protected node importer. + -> if the importer is able to deal with that node it needs + to care of the complete subtree until it is notified + during the 'endNode' call. + -> if the import can't deal with that node or if that node + is the a leaf in the tree to be imported 'end' will + not have an effect on the importer, that was never started. + */ + log.debug("Skipping protected node: " + existing); + parents.push(existing); + return; + } + if (def.isAutoCreated() && existing.isNodeType(ntName)) { + // this node has already been auto-created, no need to create it + node = existing; + } else { + // edge case: colliding node does have same uuid + // (see http://issues.apache.org/jira/browse/JCR-1128) + if (!(existing.getId().equals(id) + && (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING + || uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING))) { + throw new ItemExistsException( + "Node with the same UUID exists:" + existing); + } + // fall through + } + } + } + + if (node == null) { + // create node + if (id == null) { + // no potential uuid conflict, always add new node + checkPermission(parent, nodeName); + node = createNode(parent, nodeName, ntName, mixins, null); + } else { + // potential uuid conflict + boolean isConflicting; + try { + // the following is a fail-fast test whether + // an item exists (regardless of access control) + session.getHierarchyManager().getName(id); + isConflicting = true; + } catch (ItemNotFoundException infe) { + isConflicting = false; + } + + if (isConflicting) { + // resolve uuid conflict + node = resolveUUIDConflict(parent, id, nodeInfo); + if (node == null) { + // no new node has been created, so skip this node + parents.push(null); // push null onto stack for skipped node + log.debug("Skipping existing node " + nodeInfo.getName()); + return; + } + } else { + // create new with given uuid + checkPermission(parent, nodeName); + node = createNode(parent, nodeName, ntName, mixins, id); + } + } + } + + // process properties + + for (PropInfo pi : propInfos) { + // find applicable definition + QPropertyDefinition def = pi.getApplicablePropertyDef(node.getEffectiveNodeType()); + if (def.isProtected()) { + // skip protected property + log.debug("Skipping protected property " + pi.getName()); + + // notify the ProtectedPropertyImporter. + for (ProtectedItemImporter ppi : pItemImporters) { + if (ppi instanceof ProtectedPropertyImporter && ((ProtectedPropertyImporter) ppi).handlePropInfo(node, pi, def)) { + log.debug("Protected property -> delegated to ProtectedPropertyImporter"); + break; + } /* else: p-i-Importer isn't able to deal with this property. + try next pp-importer */ + + } + } else { + // regular property -> create the property + createProperty(node, pi, def); + } + } + + parents.push(node); + } + + + /** + * {@inheritDoc} + */ + public void endNode(NodeInfo nodeInfo) throws RepositoryException { + NodeImpl parent = parents.pop(); + if (parent == null) { + if (pnImporter != null) { + pnImporter.endChildInfo(); + } + } else if (parent.getDefinition().isProtected()) { + if (pnImporter != null) { + pnImporter.end(parent); + // and reset the pnImporter field waiting for the next protected + // parent -> selecting again from available importers + pnImporter = null; + } + } + } + + /** + * {@inheritDoc} + */ + public void end() throws RepositoryException { + /** + * adjust references that refer to uuid's which have been mapped to + * newly generated uuid's on import + */ + // 1. let protected property/node importers handle protected ref-properties + // and (protected) properties underneath a protected parent node. + for (ProtectedItemImporter ppi : pItemImporters) { + ppi.processReferences(); + } + + // 2. regular non-protected properties. + Iterator iter = refTracker.getProcessedReferences(); + while (iter.hasNext()) { + Object ref = iter.next(); + if (!(ref instanceof Property)) { + continue; + } + + Property prop = (Property) ref; + // being paranoid... + if (prop.getType() != PropertyType.REFERENCE + && prop.getType() != PropertyType.WEAKREFERENCE) { + continue; + } + if (prop.isMultiple()) { + Value[] values = prop.getValues(); + Value[] newVals = new Value[values.length]; + for (int i = 0; i < values.length; i++) { + Value val = values[i]; + NodeId original = new NodeId(val.getString()); + NodeId adjusted = refTracker.getMappedId(original); + if (adjusted != null) { + newVals[i] = session.getValueFactory().createValue( + session.getNodeById(adjusted), + prop.getType() != PropertyType.REFERENCE); + } else { + // reference doesn't need adjusting, just copy old value + newVals[i] = val; + } + } + prop.setValue(newVals); + } else { + Value val = prop.getValue(); + NodeId original = new NodeId(val.getString()); + NodeId adjusted = refTracker.getMappedId(original); + if (adjusted != null) { + prop.setValue(session.getNodeById(adjusted).getUUID()); + } + } + } + refTracker.clear(); + } +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/StringValue.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/StringValue.java new file mode 100644 index 00000000000..6e7d560a01b --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/StringValue.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.xml; + +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.util.Base64; +import org.apache.jackrabbit.value.ValueHelper; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.ValueFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * StringValue represents an immutable serialized value. + */ +class StringValue implements TextValue { + + private final String value; + + private final NamePathResolver nsContext; + private final ValueFactory valueFactory; + + /** + * Constructs a new StringValue representing the given + * value. + * + * @param value + * @param nsContext + * @param valueFactory + */ + protected StringValue(String value, NamePathResolver nsContext, ValueFactory valueFactory) { + this.value = value; + this.nsContext = nsContext; + this.valueFactory = valueFactory; + } + + //--------------------------------------------------------< TextValue > + + public Value getValue(int type, NamePathResolver resolver) + throws ValueFormatException, RepositoryException { + if (type == PropertyType.NAME || type == PropertyType.PATH) { + // NAME and PATH require special treatment because + // they depend on the current namespace context + // of the xml document + + // convert serialized value to InternalValue using + // current namespace context of xml document + InternalValue ival = + InternalValue.create(ValueHelper.convert( + value, type, valueFactory), nsContext); + // convert InternalValue to Value using this + // session's namespace mappings + return ValueFormat.getJCRValue(ival, resolver, valueFactory); + } else { + // all other types + return ValueHelper.deserialize(value, type, false, valueFactory); + } + } + + public InternalValue getInternalValue(int targetType) + throws ValueFormatException, RepositoryException { + try { + if (targetType == PropertyType.BINARY) { + // base64 encoded BINARY type; + // decode using Reader + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Base64.decode(value, baos); + return InternalValue.create(baos.toByteArray()); + } else { + // convert serialized value to InternalValue using + // current namespace context of xml document + return InternalValue.create(ValueHelper.convert( + value, targetType, valueFactory), nsContext); + } + } catch (IOException e) { + throw new RepositoryException("Error decoding Base64 content", e); + } + } + + public void dispose() { + // do nothing + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/SysViewImportHandler.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/SysViewImportHandler.java new file mode 100644 index 00000000000..3fef78bcc23 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/SysViewImportHandler.java @@ -0,0 +1,370 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.xml; + +import org.apache.jackrabbit.core.xml.PropInfo.MultipleStatus; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +/** + * SysViewImportHandler ... + */ +class SysViewImportHandler extends TargetImportHandler { + + /** + * stack of ImportState instances; an instance is pushed onto the stack + * in the startElement method every time a sv:node element is encountered; + * the same instance is popped from the stack in the endElement method + * when the corresponding sv:node element is encountered. + */ + private final Stack stack = new Stack(); + + /** + * fields used temporarily while processing sv:property and sv:value elements + */ + private Name currentPropName; + private int currentPropType = PropertyType.UNDEFINED; + private MultipleStatus currentPropMultipleStatus = MultipleStatus.UNKNOWN; + // list of appendable value objects + private ArrayList currentPropValues = + new ArrayList(); + private BufferedStringValue currentPropValue; + + /** + * Constructs a new SysViewImportHandler. + * + * @param importer the underlying importer + * @param valueFactory the value factory + */ + SysViewImportHandler(Importer importer, ValueFactory valueFactory) { + super(importer, valueFactory); + } + + private void processNode(ImportState state, boolean start, boolean end) + throws SAXException { + if (!start && !end) { + return; + } + Name[] mixinNames = null; + if (state.mixinNames != null) { + mixinNames = state.mixinNames.toArray( + new Name[state.mixinNames.size()]); + } + NodeId id = null; + if (state.uuid != null) { + id = NodeId.valueOf(state.uuid); + } + NodeInfo node = + new NodeInfo(state.nodeName, state.nodeTypeName, mixinNames, id); + // call Importer + try { + if (start) { + importer.startNode(node, state.props); + // dispose temporary property values + for (PropInfo pi : state.props) { + pi.dispose(); + } + + } + if (end) { + importer.endNode(node); + } + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + //-------------------------------------------------------< ContentHandler > + + /** + * {@inheritDoc} + */ + @Override + public void startElement(String namespaceURI, String localName, + String qName, Attributes atts) + throws SAXException { + Name name = NameFactoryImpl.getInstance().create(namespaceURI, localName); + // check element name + if (name.equals(NameConstants.SV_NODE)) { + // sv:node element + + // node name (value of sv:name attribute) + String svName = getAttribute(atts, NameConstants.SV_NAME); + if (svName == null) { + throw new SAXException(new InvalidSerializedDataException( + "missing mandatory sv:name attribute of element sv:node")); + } + + if (!stack.isEmpty()) { + // process current node first + ImportState current = stack.peek(); + // need to start current node + if (!current.started) { + processNode(current, true, false); + current.started = true; + } + } + + // push new ImportState instance onto the stack + ImportState state = new ImportState(); + try { + state.nodeName = resolver.getQName(svName); + } catch (NameException e) { + throw new SAXException(new InvalidSerializedDataException("illegal node name: " + name, e)); + } catch (NamespaceException e) { + throw new SAXException(new InvalidSerializedDataException("illegal node name: " + name, e)); + } + stack.push(state); + } else if (name.equals(NameConstants.SV_PROPERTY)) { + // sv:property element + + // reset temp fields + currentPropValues.clear(); + + // property name (value of sv:name attribute) + String svName = getAttribute(atts, NameConstants.SV_NAME); + if (svName == null) { + throw new SAXException(new InvalidSerializedDataException( + "missing mandatory sv:name attribute of element sv:property")); + } + try { + currentPropName = resolver.getQName(svName); + } catch (NameException e) { + throw new SAXException(new InvalidSerializedDataException("illegal property name: " + name, e)); + } catch (NamespaceException e) { + throw new SAXException(new InvalidSerializedDataException("illegal property name: " + name, e)); + } + // property type (sv:type attribute) + String type = getAttribute(atts, NameConstants.SV_TYPE); + if (type == null) { + throw new SAXException(new InvalidSerializedDataException( + "missing mandatory sv:type attribute of element sv:property")); + } + try { + currentPropType = PropertyType.valueFromName(type); + } catch (IllegalArgumentException e) { + throw new SAXException(new InvalidSerializedDataException( + "Unknown property type: " + type, e)); + } + // 'multi-value' hint (sv:multiple attribute) + String multiple = getAttribute(atts, NameConstants.SV_MULTIPLE); + if (multiple != null) { + currentPropMultipleStatus = MultipleStatus.MULTIPLE; + } else { + currentPropMultipleStatus = MultipleStatus.UNKNOWN; + } + } else if (name.equals(NameConstants.SV_VALUE)) { + // sv:value element + currentPropValue = new BufferedStringValue(resolver, valueFactory); + String xsiType = atts.getValue("xsi:type"); + currentPropValue.setBase64("xs:base64Binary".equals(xsiType)); + } else { + throw new SAXException(new InvalidSerializedDataException( + "Unexpected element in system view xml document: " + name)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + if (currentPropValue != null) { + // property value (character data of sv:value element) + try { + currentPropValue.append(ch, start, length); + } catch (IOException ioe) { + throw new SAXException("error while processing property value", + ioe); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + if (currentPropValue != null) { + // property value + + // data reported by the ignorableWhitespace event within + // sv:value tags is considered part of the value + try { + currentPropValue.append(ch, start, length); + } catch (IOException ioe) { + throw new SAXException("error while processing property value", + ioe); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + Name name = NameFactoryImpl.getInstance().create(namespaceURI, localName); + // check element name + ImportState state = stack.peek(); + if (name.equals(NameConstants.SV_NODE)) { + // sv:node element + if (!state.started) { + // need to start & end current node + processNode(state, true, true); + state.started = true; + } else { + // need to end current node + processNode(state, false, true); + } + // pop current state from stack + stack.pop(); + } else if (name.equals(NameConstants.SV_PROPERTY)) { + // sv:property element + + // check if all system properties (jcr:primaryType, jcr:uuid etc.) + // have been collected and create node as necessary + if (currentPropName.equals(NameConstants.JCR_PRIMARYTYPE)) { + BufferedStringValue val = currentPropValues.get(0); + String s = null; + try { + s = val.retrieve(); + state.nodeTypeName = resolver.getQName(s); + } catch (IOException ioe) { + throw new SAXException("error while retrieving value", ioe); + } catch (NameException e) { + throw new SAXException(new InvalidSerializedDataException("illegal node type name: " + s, e)); + } catch (NamespaceException e) { + throw new SAXException(new InvalidSerializedDataException("illegal node type name: " + s, e)); + } + } else if (currentPropName.equals(NameConstants.JCR_MIXINTYPES)) { + if (state.mixinNames == null) { + state.mixinNames = new ArrayList(currentPropValues.size()); + } + for (BufferedStringValue val : currentPropValues) { + String s = null; + try { + s = val.retrieve(); + Name mixin = resolver.getQName(s); + state.mixinNames.add(mixin); + } catch (IOException ioe) { + throw new SAXException("error while retrieving value", ioe); + } catch (NameException e) { + throw new SAXException(new InvalidSerializedDataException("illegal mixin type name: " + s, e)); + } catch (NamespaceException e) { + throw new SAXException(new InvalidSerializedDataException("illegal mixin type name: " + s, e)); + } + } + } else if (currentPropName.equals(NameConstants.JCR_UUID)) { + BufferedStringValue val = currentPropValues.get(0); + try { + state.uuid = val.retrieve(); + } catch (IOException ioe) { + throw new SAXException("error while retrieving value", ioe); + } + } else { + if (currentPropMultipleStatus == MultipleStatus.UNKNOWN + && currentPropValues.size() != 1) { + currentPropMultipleStatus = MultipleStatus.MULTIPLE; + } + PropInfo prop = new PropInfo( + currentPropName, + currentPropType, + currentPropValues.toArray(new TextValue[currentPropValues.size()]), + currentPropMultipleStatus); + state.props.add(prop); + } + // reset temp fields + currentPropValues.clear(); + } else if (name.equals(NameConstants.SV_VALUE)) { + // sv:value element + currentPropValues.add(currentPropValue); + // reset temp fields + currentPropValue = null; + } else { + throw new SAXException(new InvalidSerializedDataException("invalid element in system view xml document: " + localName)); + } + } + + //--------------------------------------------------------< inner classes > + /** + * The state of parsing the XML stream. + */ + static class ImportState { + /** + * name of current node + */ + Name nodeName; + /** + * primary type of current node + */ + Name nodeTypeName; + /** + * list of mixin types of current node + */ + List mixinNames; + /** + * uuid of current node + */ + String uuid; + + /** + * list of PropInfo instances representing properties of current node + */ + List props = new ArrayList(); + + /** + * flag indicating whether startNode() has been called for current node + */ + boolean started; + } + + //-------------------------------------------------------------< private > + + /** + * Returns the value of the named XML attribute. + * + * @param attributes set of XML attributes + * @param name attribute name + * @return attribute value, + * or null if the named attribute is not found + */ + private static String getAttribute(Attributes attributes, Name name) { + return attributes.getValue(name.getNamespaceURI(), name.getLocalName()); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/TargetImportHandler.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/TargetImportHandler.java new file mode 100644 index 00000000000..d1d2061e34c --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/TargetImportHandler.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.core.xml; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; + +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; + +/** + * TargetImportHandler serves as the base class for the concrete + * classes {@link DocViewImportHandler} and + * {@link SysViewImportHandler}. + */ +abstract class TargetImportHandler extends DefaultHandler { + + protected final Importer importer; + + protected final ValueFactory valueFactory; + + /** + * The current namespace context. A new namespace context is created + * for each XML element and the parent reference is used to link the + * namespace contexts together in a tree hierarchy. This variable contains + * a reference to the namespace context associated with the XML element + * that is currently being processed. + */ + protected NamespaceContext nsContext; + + protected NamePathResolver resolver; + + protected TargetImportHandler(Importer importer, ValueFactory valueFactory) { + this.importer = importer; + this.valueFactory = valueFactory; + } + + /** + * Initializes the underlying {@link Importer} instance. This method + * is called by the XML parser when the XML document starts. + * + * @throws SAXException if the importer can not be initialized + * @see DefaultHandler#startDocument() + */ + @Override + public void startDocument() throws SAXException { + try { + importer.start(); + nsContext = null; + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + /** + * Closes the underlying {@link Importer} instance. This method + * is called by the XML parser when the XML document ends. + * + * @throws SAXException if the importer can not be closed + * @see DefaultHandler#endDocument() + */ + @Override + public void endDocument() throws SAXException { + try { + importer.end(); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + /** + * Starts a local namespace context for the current XML element. + * This method is called by {@link ImportHandler} when the processing of + * an XML element starts. The given local namespace mappings have been + * recorded by {@link ImportHandler#startPrefixMapping(String, String)} + * for the current XML element. + * + * @param mappings local namespace mappings + */ + public final void startNamespaceContext(Map mappings) { + nsContext = new NamespaceContext(nsContext, mappings); + resolver = new DefaultNamePathResolver(nsContext); + } + + /** + * Restores the parent namespace context. This method is called by + * {@link ImportHandler} when the processing of an XML element ends. + */ + public final void endNamespaceContext() { + nsContext = nsContext.getParent(); + } + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/TextValue.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/TextValue.java new file mode 100644 index 00000000000..4ad54987763 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/TextValue.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.core.xml; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * TextValue represents a serialized property value read + * from a System or Document View XML document. + */ +public interface TextValue { + + Value getValue(int type, NamePathResolver resolver) + throws ValueFormatException, RepositoryException; + + InternalValue getInternalValue(int type) + 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(); + +} diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/WorkspaceImporter.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/WorkspaceImporter.java new file mode 100644 index 00000000000..e0245740e28 --- /dev/null +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/xml/WorkspaceImporter.java @@ -0,0 +1,755 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.xml; + +import org.apache.jackrabbit.core.BatchedItemOperations; +import org.apache.jackrabbit.core.HierarchyManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.WorkspaceImpl; +import org.apache.jackrabbit.core.config.ImportConfig; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.core.session.SessionContext; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.core.version.InternalVersionManager; +import org.apache.jackrabbit.core.version.VersionHistoryInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; + +/** + * WorkspaceImporter ... + */ +public class WorkspaceImporter implements Importer { + + private static Logger log = LoggerFactory.getLogger(WorkspaceImporter.class); + + private final NodeState importTarget; + private final WorkspaceImpl wsp; + private final SessionImpl session; + private final InternalVersionManager versionManager; + private final HierarchyManager hierMgr; + private final BatchedItemOperations itemOps; + + private final int uuidBehavior; + + private boolean aborted; + private final Stack parents; + + /** + * helper object that keeps track of remapped uuid's and imported reference + * properties that might need correcting depending on the uuid mappings + */ + private final ReferenceChangeTracker refTracker; + + /** + * Creates a new WorkspaceImporter instance. + * + * @param parentPath target path where to add the imported subtree + * @param wsp the workspace to operate on + * @param sessionContext the session context + * @param uuidBehavior flag that governs how incoming UUIDs are handled + * @throws PathNotFoundException if no node exists at + * parentPath or if the + * current session is not granted read + * access. + * @throws ConstraintViolationException if the node at + * parentPath is protected + * @throws VersionException if the node at + * parentPath is not + * checked-out + * @throws LockException if a lock prevents the addition of + * the subtree + * @throws RepositoryException if another error occurs + */ + public WorkspaceImporter(Path parentPath, + WorkspaceImpl wsp, + SessionContext sessionContext, + int uuidBehavior) + throws PathNotFoundException, ConstraintViolationException, + VersionException, LockException, RepositoryException { + this(parentPath, wsp, sessionContext, uuidBehavior, null); + } + + /** + * Creates a new WorkspaceImporter instance. + * + * @param parentPath target path where to add the imported subtree + * @param wsp the workspace to operate on + * @param sessionContext the session context + * @param uuidBehavior flag that governs how incoming UUIDs are handled + * @param config import configuration. + * @throws PathNotFoundException if no node exists at + * parentPath or if the + * current session is not granted read + * access. + * @throws ConstraintViolationException if the node at + * parentPath is protected + * @throws VersionException if the node at + * parentPath is not + * checked-out + * @throws LockException if a lock prevents the addition of + * the subtree + * @throws RepositoryException if another error occurs + */ + public WorkspaceImporter( + Path parentPath, WorkspaceImpl wsp, SessionContext sessionContext, + int uuidBehavior, ImportConfig config) + throws PathNotFoundException, ConstraintViolationException, + VersionException, LockException, RepositoryException { + this.wsp = wsp; + this.session = sessionContext.getSessionImpl(); + this.versionManager = session.getInternalVersionManager(); + this.uuidBehavior = uuidBehavior; + + itemOps = new BatchedItemOperations( + wsp.getItemStateManager(), sessionContext); + hierMgr = wsp.getHierarchyManager(); + + // perform preliminary checks + itemOps.verifyCanWrite(parentPath); + importTarget = itemOps.getNodeState(parentPath); + + aborted = false; + + refTracker = new ReferenceChangeTracker(); + + parents = new Stack(); + parents.push(importTarget); + + // TODO: TOBEFIXED importer doesn't yet pass protected items to the configured importers. + // for the time being log an exception if an importer is configured that + // is expected to work with workspace import. see JCR-2521 + if (config != null) { + List pi = config.getProtectedItemImporters(); + for (ProtectedItemImporter ppi : pi) { + if (ppi.init(session, session, true, uuidBehavior, refTracker)) { + log.warn("Protected item importer configured is not supported by workspace import."); + //throw new UnsupportedOperationException("Workspace-Import of protected nodes: Not yet implement. "); + } + } + } + } + + /** + * @param parent parent node state + * @param conflicting conflicting node state + * @param nodeInfo the node info + * @return the resolved node state + * @throws RepositoryException if an error occurs + */ + protected NodeState resolveUUIDConflict(NodeState parent, + NodeState conflicting, + NodeInfo nodeInfo) + throws RepositoryException { + + NodeState node; + if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) { + // create new with new uuid: + // check if new node can be added (check access rights & + // node type constraints only, assume locking & versioning status + // and retention/hold has already been checked on ancestor) + itemOps.checkAddNode(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_CONSTRAINTS); + node = itemOps.createNodeState(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), null); + // remember uuid mapping + EffectiveNodeType ent = itemOps.getEffectiveNodeType(node); + if (ent.includesNodeType(NameConstants.MIX_REFERENCEABLE)) { + refTracker.mappedId(nodeInfo.getId(), node.getNodeId()); + } + } else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW) { + // if existing node is shareable, then instead of failing, create + // new node and share with existing + if (conflicting.isShareable()) { + itemOps.clone(conflicting, parent, nodeInfo.getName()); + return null; + } + String msg = "a node with uuid " + nodeInfo.getId() + + " already exists!"; + log.debug(msg); + throw new ItemExistsException(msg); + } else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING) { + // make sure conflicting node is not importTarget or an ancestor thereof + Path p0 = hierMgr.getPath(importTarget.getNodeId()); + Path p1 = hierMgr.getPath(conflicting.getNodeId()); + try { + if (p1.equals(p0) || p1.isAncestorOf(p0)) { + String msg = "cannot remove ancestor node"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } catch (MalformedPathException mpe) { + // should never get here... + String msg = "internal error: failed to determine degree of relationship"; + log.error(msg, mpe); + throw new RepositoryException(msg, mpe); + } + // remove conflicting: + // check if conflicting can be removed + // (access rights, node type constraints, locking & versioning status) + itemOps.checkRemoveNode(conflicting, + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_LOCK + | BatchedItemOperations.CHECK_CHECKED_OUT + | BatchedItemOperations.CHECK_CONSTRAINTS + | BatchedItemOperations.CHECK_HOLD + | BatchedItemOperations.CHECK_RETENTION); + // do remove conflicting (recursive) + itemOps.removeNodeState(conflicting); + + // create new with given uuid: + // check if new node can be added (check access rights & + // node type constraints only, assume locking & versioning status + // and retention/hold has already been checked on ancestor) + itemOps.checkAddNode(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_CONSTRAINTS); + // do create new node + node = itemOps.createNodeState(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), + nodeInfo.getId()); + } else if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING) { + NodeId parentId = conflicting.getParentId(); + if (parentId == null) { + String msg = "root node cannot be replaced"; + log.debug(msg); + throw new RepositoryException(msg); + } + // 'replace' current parent with parent of conflicting + try { + parent = itemOps.getNodeState(parentId); + } catch (ItemNotFoundException infe) { + // should never get here... + String msg = "internal error: failed to retrieve parent state"; + log.error(msg, infe); + throw new RepositoryException(msg, infe); + } + // remove conflicting: + // check if conflicting can be removed + // (access rights, node type constraints, locking & versioning status) + itemOps.checkRemoveNode(conflicting, + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_LOCK + | BatchedItemOperations.CHECK_CHECKED_OUT + | BatchedItemOperations.CHECK_CONSTRAINTS + | BatchedItemOperations.CHECK_HOLD + | BatchedItemOperations.CHECK_RETENTION); + + // 'replace' is actually a 'remove existing/add new' operation; + // this unfortunately changes the order of the parent's + // child node entries (JCR-1055); + // => backup list of child node entries beforehand in order + // to restore it afterwards + ChildNodeEntry cneConflicting = parent.getChildNodeEntry(nodeInfo.getId()); + List cneList = new ArrayList(parent.getChildNodeEntries()); + // do remove conflicting (recursive) + itemOps.removeNodeState(conflicting); + // create new with given uuid at same location as conflicting: + // check if new node can be added at other location + // (access rights, node type constraints, locking & versioning + // status and retention/hold) + itemOps.checkAddNode(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_LOCK + | BatchedItemOperations.CHECK_CHECKED_OUT + | BatchedItemOperations.CHECK_CONSTRAINTS + | BatchedItemOperations.CHECK_HOLD + | BatchedItemOperations.CHECK_RETENTION); + // do create new node + node = itemOps.createNodeState(parent, nodeInfo.getName(), + nodeInfo.getNodeTypeName(), nodeInfo.getMixinNames(), + nodeInfo.getId()); + // restore list of child node entries (JCR-1055) + if (cneConflicting.getName().equals(nodeInfo.getName())) { + // restore original child node list + parent.setChildNodeEntries(cneList); + } else { + // replace child node entry with different name + // but preserving original position + parent.removeAllChildNodeEntries(); + for (ChildNodeEntry cne : cneList) { + if (cne.getId().equals(nodeInfo.getId())) { + // replace entry with different name + parent.addChildNodeEntry(nodeInfo.getName(), nodeInfo.getId()); + } else { + parent.addChildNodeEntry(cne.getName(), cne.getId()); + } + } + } + } else { + String msg = "unknown uuidBehavior: " + uuidBehavior; + log.debug(msg); + throw new RepositoryException(msg); + } + + return node; + } + + /** + * Post-process imported node (initialize properties with special + * semantics etc.) + * + * @param node the node state + * @throws RepositoryException if an error occurs + */ + protected void postProcessNode(NodeState node) throws RepositoryException { + /** + * special handling required for properties with special semantics + * (e.g. those defined by mix:referenceable, mix:versionable, + * mix:lockable, et.al.) + * + * todo FIXME delegate to 'node type instance handler' + */ + EffectiveNodeType ent = itemOps.getEffectiveNodeType(node); + if (ent.includesNodeType(NameConstants.MIX_SIMPLE_VERSIONABLE)) { + /** + * check if there's already a version history for that + * node; this would e.g. be the case if a versionable node + * had been exported, removed and re-imported with either + * IMPORT_UUID_COLLISION_REMOVE_EXISTING or + * IMPORT_UUID_COLLISION_REPLACE_EXISTING; + * otherwise create a new version history + */ + VersionHistoryInfo history = + versionManager.getVersionHistory(session, node, null); + InternalValue historyId = InternalValue.create( + history.getVersionHistoryId()); + InternalValue versionId = InternalValue.create( + history.getRootVersionId()); + + // jcr:isCheckedOut + conditionalAddProperty( + node, NameConstants.JCR_ISCHECKEDOUT, + PropertyType.BOOLEAN, false, InternalValue.create(true)); + + // set extra properties only for full versionable nodes + if (ent.includesNodeType(NameConstants.MIX_VERSIONABLE)) { + // jcr:versionHistory + conditionalAddProperty( + node, NameConstants.JCR_VERSIONHISTORY, + PropertyType.REFERENCE, false, historyId); + + // jcr:baseVersion + conditionalAddProperty( + node, NameConstants.JCR_BASEVERSION, + PropertyType.REFERENCE, false, versionId); + + // jcr:predecessors + conditionalAddProperty( + node, NameConstants.JCR_PREDECESSORS, + PropertyType.REFERENCE, true, versionId); + } + } + } + + protected void processProperty(NodeState node, PropInfo pInfo) throws RepositoryException { + PropertyState prop; + QPropertyDefinition def; + + Name name = pInfo.getName(); + int type = pInfo.getType(); + + if (node.hasPropertyName(name)) { + // a property with that name already exists... + PropertyId idExisting = new PropertyId(node.getNodeId(), name); + prop = (PropertyState) itemOps.getItemState(idExisting); + def = itemOps.findApplicablePropertyDefinition(prop.getName(), prop.getType(), prop.isMultiValued(), node); + if (def.isProtected()) { + // skip protected property + log.debug("skipping protected property " + + itemOps.safeGetJCRPath(idExisting)); + return; + } + if (!def.isAutoCreated() + || (prop.getType() != type && type != PropertyType.UNDEFINED) + || def.isMultiple() != prop.isMultiValued()) { + throw new ItemExistsException(itemOps.safeGetJCRPath(prop.getPropertyId())); + } + } else { + // there's no property with that name, + // find applicable definition + def = pInfo.getApplicablePropertyDef(itemOps.getEffectiveNodeType(node)); + if (def.isProtected()) { + // skip protected property + log.debug("skipping protected property " + name); + return; + } + + // create new property + prop = itemOps.createPropertyState(node, name, type, def); + } + + // check multi-valued characteristic + TextValue[] values = pInfo.getTextValues(); + if (values.length != 1 && !def.isMultiple()) { + throw new ConstraintViolationException(itemOps.safeGetJCRPath(prop.getPropertyId()) + + " is not multi-valued"); + } + + // convert serialized values to InternalValue objects + int targetType = pInfo.getTargetType(def); + InternalValue[] iva = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + iva[i] = values[i].getInternalValue(targetType); + } + + // set values + prop.setValues(iva); + + // make sure property is valid according to its definition + itemOps.validate(prop); + + if (prop.getType() == PropertyType.REFERENCE + || prop.getType() == PropertyType.WEAKREFERENCE) { + // store reference for later resolution + refTracker.processedReference(prop); + } + + // store property + itemOps.store(prop); + } + + /** + * Adds the the given property to a node unless the property already + * exists. + * + * @param node the node to which the property is added + * @param name name of the property + * @param type property type (see {@link PropertyType}) + * @param multiple whether the property is multivalued + * @param value initial value of the property, if it needs to be added + * @throws RepositoryException if the property could not be added + */ + private void conditionalAddProperty( + NodeState node, Name name, int type, boolean multiple, + InternalValue value) + throws RepositoryException { + if (!node.hasPropertyName(name)) { + QPropertyDefinition def = itemOps.findApplicablePropertyDefinition( + name, type, multiple, node); + PropertyState prop = itemOps.createPropertyState( + node, name, type, def); + prop.setValues(new InternalValue[] { value }); + } + } + + //-------------------------------------------------------------< Importer > + /** + * {@inheritDoc} + */ + public void start() throws RepositoryException { + try { + // start update operation + itemOps.edit(); + } catch (IllegalStateException ise) { + aborted = true; + String msg = "internal error: failed to start update operation"; + log.debug(msg); + throw new RepositoryException(msg, ise); + } + } + + /** + * {@inheritDoc} + */ + public void startNode(NodeInfo nodeInfo, List propInfos) + throws RepositoryException { + if (aborted) { + // the import has been aborted, get outta here... + return; + } + + boolean succeeded = false; + NodeState parent; + try { + // check sanity of workspace/session first + wsp.sanityCheck(); + + parent = parents.peek(); + + // process node + + NodeState node = null; + NodeId id = nodeInfo.getId(); + Name nodeName = nodeInfo.getName(); + Name ntName = nodeInfo.getNodeTypeName(); + Name[] mixins = nodeInfo.getMixinNames(); + + if (parent == null) { + // parent node was skipped, skip this child node too + parents.push(null); // push null onto stack for skipped node + succeeded = true; + log.debug("skipping node " + nodeName); + return; + } + if (parent.hasChildNodeEntry(nodeName)) { + // a node with that name already exists... + ChildNodeEntry entry = + parent.getChildNodeEntry(nodeName, 1); + NodeId idExisting = entry.getId(); + NodeState existing = (NodeState) itemOps.getItemState(idExisting); + QNodeDefinition def = itemOps.findApplicableNodeDefinition( + nodeName, existing.getNodeTypeName(), parent); + + if (!def.allowsSameNameSiblings()) { + // existing doesn't allow same-name siblings, + // check for potential conflicts + EffectiveNodeType entExisting = + itemOps.getEffectiveNodeType(existing); + if (def.isProtected() && entExisting.includesNodeType(ntName)) { + // skip protected node + parents.push(null); // push null onto stack for skipped node + succeeded = true; + log.debug("skipping protected node " + + itemOps.safeGetJCRPath(existing.getNodeId())); + return; + } + if (def.isAutoCreated() && entExisting.includesNodeType(ntName)) { + // this node has already been auto-created, + // no need to create it + node = existing; + } else { + // edge case: colliding node does have same uuid + // (see http://issues.apache.org/jira/browse/JCR-1128) + if (!(idExisting.equals(id) + && (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING + || uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING))) { + throw new ItemExistsException(itemOps.safeGetJCRPath(existing.getNodeId())); + } + // fall through + } + } + } + + if (node == null) { + // there's no node with that name... + if (id == null) { + // no potential uuid conflict, always create new node + QNodeDefinition def = itemOps.findApplicableNodeDefinition( + nodeName, ntName, parent); + if (def.isProtected()) { + // skip protected node + parents.push(null); // push null onto stack for skipped node + succeeded = true; + log.debug("skipping protected node " + nodeName); + return; + } + + // check if new node can be added (check access rights & + // node type constraints only, assume locking & versioning status + // and retention/hold has already been checked on ancestor) + itemOps.checkAddNode(parent, nodeName, ntName, + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_CONSTRAINTS); + // do create new node + node = itemOps.createNodeState(parent, nodeName, ntName, mixins, null, def); + } else { + // potential uuid conflict + try { + NodeState conflicting = itemOps.getNodeState(id); + // resolve uuid conflict + node = resolveUUIDConflict(parent, conflicting, nodeInfo); + if (node == null) { + // no new node has been created, so skip this node + parents.push(null); // push null onto stack for skipped node + succeeded = true; + log.debug("skipping existing node: " + nodeName); + return; + } + } catch (ItemNotFoundException e) { + // create new with given uuid + QNodeDefinition def = itemOps.findApplicableNodeDefinition( + nodeName, ntName, parent); + if (def.isProtected()) { + // skip protected node + parents.push(null); // push null onto stack for skipped node + succeeded = true; + log.debug("skipping protected node " + nodeName); + return; + } + + // check if new node can be added (check access rights & + // node type constraints only, assume locking & versioning status + // and retention/hold has already been checked on ancestor) + itemOps.checkAddNode(parent, nodeName, ntName, + BatchedItemOperations.CHECK_ACCESS + | BatchedItemOperations.CHECK_CONSTRAINTS); + // do create new node + node = itemOps.createNodeState(parent, nodeName, ntName, mixins, id, def); + } + } + } + + // process properties + for (PropInfo propInfo : propInfos) { + processProperty(node, propInfo); + } + + // store affected nodes + itemOps.store(node); + itemOps.store(parent); + + // push current node onto stack of parents + parents.push(node); + + succeeded = true; + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + aborted = true; + itemOps.cancel(); + } + } + } + + /** + * {@inheritDoc} + */ + public void endNode(NodeInfo nodeInfo) throws RepositoryException { + if (aborted) { + // the import has been aborted, get outta here... + return; + } + NodeState node = parents.pop(); + if (node == null) { + // node was skipped, nothing to do here + return; + } + boolean succeeded = false; + try { + // check sanity of workspace/session first + wsp.sanityCheck(); + + // post-process node (initialize properties with special semantics etc.) + postProcessNode(node); + + // make sure node is valid according to its definition + itemOps.validate(node); + + // we're done with that node, now store its state + itemOps.store(node); + succeeded = true; + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + aborted = true; + itemOps.cancel(); + } + } + } + + /** + * {@inheritDoc} + */ + public void end() throws RepositoryException { + if (aborted) { + // the import has been aborted, get outta here... + return; + } + + boolean succeeded = false; + try { + // check sanity of workspace/session first + wsp.sanityCheck(); + + /** + * adjust references that refer to uuids which have been mapped to + * newly generated uuids on import + */ + Iterator iter = refTracker.getProcessedReferences(); + while (iter.hasNext()) { + PropertyState prop = (PropertyState) iter.next(); + // being paranoid... + if (prop.getType() != PropertyType.REFERENCE + && prop.getType() != PropertyType.WEAKREFERENCE) { + continue; + } + boolean modified = false; + InternalValue[] values = prop.getValues(); + InternalValue[] newVals = new InternalValue[values.length]; + for (int i = 0; i < values.length; i++) { + NodeId adjusted = + refTracker.getMappedId(values[i].getNodeId()); + if (adjusted != null) { + newVals[i] = InternalValue.create( + adjusted, + prop.getType() != PropertyType.REFERENCE); + modified = true; + } else { + // reference doesn't need adjusting, just copy old value + newVals[i] = values[i]; + } + } + if (modified) { + prop.setValues(newVals); + itemOps.store(prop); + } + } + refTracker.clear(); + + // make sure import target is valid according to its definition + itemOps.validate(importTarget); + + // finally store the state of the import target + // (the parent of the imported subtree) + itemOps.store(importTarget); + succeeded = true; + } finally { + if (!succeeded) { + // update operation failed, cancel all modifications + aborted = true; + itemOps.cancel(); + } + } + + if (!aborted) { + // finish update + itemOps.update(); + } + } + +} diff --git a/src/java/org/apache/jackrabbit/core/config/doc-files/class.jpg b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/config/doc-files/class.jpg similarity index 100% rename from src/java/org/apache/jackrabbit/core/config/doc-files/class.jpg rename to jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/config/doc-files/class.jpg diff --git a/src/java/org/apache/jackrabbit/core/config/doc-files/class.uxf b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/config/doc-files/class.uxf similarity index 100% rename from src/java/org/apache/jackrabbit/core/config/doc-files/class.uxf rename to jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/config/doc-files/class.uxf diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/config/package.html b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/config/package.html new file mode 100644 index 00000000000..f9cef2e0ccc --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/config/package.html @@ -0,0 +1,73 @@ + + +Jackrabbit configuration handling. +

    +This package contains functionality for reading and manipulating the +Jackrabbit configuration. The configuration functionality can be +divided in two parts: parsing and representing configuration. + +

    Parsing configuration

    +

    +The configuration parsing functionality is located in the +{@link org.apache.jackrabbit.core.config.ConfigurationParser ConfigurationParser} +class. The parser class reads repository and workspace configuration +files and builds corresponding configuration object structures. +

    +

    +The static create() factory methods of the +{@link org.apache.jackrabbit.core.config.RepositoryConfig RepositoryConfig} +class are the standard ways of invoking the configuration parser. +

    + +

    Representating configuration

    +

    +The parsed configuration information is represented using Config object +structures. The +{@link org.apache.jackrabbit.core.config.RepositoryConfig RepositoryConfig} +and +{@link org.apache.jackrabbit.core.config.WorkspaceConfig WorkspaceConfig} +classes are used for the top-level configuration. Other configuration classes +are used according to the class diagram shown below. +

    + +

    +In addition to the static configuration information, some Config classes +contain references to configured implementation objects. These implementation +objects are not instantiated in the Config constructors to simplify +configuration parsing. Instead the Config classes provide an +init() method that instantiates and initializes all +contained implementation objects. +

    +The +{@link org.apache.jackrabbit.core.config.RepositoryConfig RepositoryConfig} +class contains also a set of static and instance methods for manipulating +the repository configuration, especially creating and loading workspace +configurations. + +

    Modifying configuration

    +

    +The configuration classes in this package are immutable to enforce the +contract that the configuration should not be modified once it has been +intstantiated. It is possible to subclass the configuration classes as long +as the immutability contract is not broken. The public constructors allow +a limited form of dynamic configurability for Jackrabbit, but note that the +only officially supported Jackrabbit configuration interface are the XML +configuration files. +

    + + diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/RepositoryImpl.jpg b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/RepositoryImpl.jpg new file mode 100644 index 00000000000..5c44ff870d7 Binary files /dev/null and b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/RepositoryImpl.jpg differ diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/RepositoryImpl.uxf b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/RepositoryImpl.uxf new file mode 100644 index 00000000000..96abb397331 --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/RepositoryImpl.uxf @@ -0,0 +1,28 @@ +com.umlet.element.base.Class10210190480RepositoryImpl +-- +-repProps: Properties +-rootNodeId: NodeId +-nodescount: long +-propscount: long +-disposed: boolean +-repLock: FileLockcom.umlet.element.base.Relation18022626054lt=-> +repStore20;34;240;34com.umlet.element.base.Class1010019040<<interface>> +JackrabbitRepository +com.umlet.element.base.Class102019040<<interface>> +Repository +com.umlet.element.base.Relation100120240110lt=<<-220;20;220;50;20;50;20;90com.umlet.element.base.Relation8012040110lt=<<-20;20;20;90com.umlet.element.base.Relation80404080lt=<<-20;20;20;60com.umlet.element.base.Class42061014030WorkspaceInfocom.umlet.element.base.Class42035024030NamespaceRegistryImplcom.umlet.element.base.Relation18058626054lt=<<<< +wspInfos20;34;240;34com.umlet.element.base.Relation18062626054lt=<<<< +activeSessions20;34;240;34com.umlet.element.base.Class42065014030SessionImplcom.umlet.element.base.Relation18026626054lt=-> +metaDataStore20;34;240;34com.umlet.element.base.Relation18018626054lt=-> +repConfig20;34;240;34com.umlet.element.base.Class42021014030RepositoryConfigcom.umlet.element.base.Class42025014030FileSystemcom.umlet.element.base.Class42029014030FileSystemcom.umlet.element.base.Relation18052626054lt=-> +delegatingDispatcher20;34;240;34com.umlet.element.base.Relation18048626054lt=-> +systemSearchMgr20;34;240;34com.umlet.element.base.Relation18040626054lt=-> +virtNTMgr20;34;240;34com.umlet.element.base.Relation18044626054lt=-> +vMgr20;34;240;34com.umlet.element.base.Relation18036626054lt=-> +ntReg20;34;240;34com.umlet.element.base.Relation18032626054lt=-> +nsReg20;34;240;34com.umlet.element.base.Class42055024030DelegatingObservationDispatcher +com.umlet.element.base.Class42051024030SearchManagercom.umlet.element.base.Class42047024030VersionManagercom.umlet.element.base.Class42043024030VirtualNodeTypeStateManagercom.umlet.element.base.Class42039024030NodeTypeRegistrycom.umlet.element.base.Relation120120430110lt=<<-410;20;410;70;20;70;20;90com.umlet.element.base.Class22010019040<<interface>> +SessionListener +com.umlet.element.base.Class43010019040<<interface>> +EventListener + \ No newline at end of file diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/SessionImpl.jpg b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/SessionImpl.jpg new file mode 100644 index 00000000000..65b69a384ed Binary files /dev/null and b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/SessionImpl.jpg differ diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/SessionImpl.uxf b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/SessionImpl.uxf new file mode 100644 index 00000000000..111f2e7edfe --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/SessionImpl.uxf @@ -0,0 +1,29 @@ +com.umlet.element.base.Relation1705704080lt=-Z>>20;60;20;20com.umlet.element.base.Relation35060617054lt=-> +tx20;34;150;34com.umlet.element.base.Relation35068617054lt=-> +lockMgr20;34;150;34com.umlet.element.base.Relation35064617054lt=<<<<-> +txResources20;34;150;34com.umlet.element.base.Class50071019026LockManagercom.umlet.element.base.Class50067019026InternalXAResourcecom.umlet.element.base.Class50063019026TransactionContextcom.umlet.element.base.Relation35047217054lt=-> +valueFactory20;34;150;34com.umlet.element.base.Class50050019026ValueFactorycom.umlet.element.base.Class150630220110XASessionImpl +-- +-txTimeout: intcom.umlet.element.base.Relation1004040530lt=-Z>>20;510;20;20com.umlet.element.base.Relation240404080lt=-Z>>20;60;20;20com.umlet.element.base.Class802022040<<interface>> +Sessioncom.umlet.element.base.Class1055022040<<interface>> +XASessioncom.umlet.element.base.Relation3105704080lt=-Z>>20;60;20;20com.umlet.element.base.Class29055022040<<interface>> +XAResourcecom.umlet.element.base.Relation24051040140lt=-Z>>20;120;20;20com.umlet.element.base.Relation35039217054lt=-> +versionMgr20;34;150;34com.umlet.element.base.Class150100220426SessionImpl +-- +#alive: boolean +#loginContext: AuthContext +#subject: Subject +#userId: String +#attributes: HashMap +#lockTokens: Set (of Strings) +com.umlet.element.base.Class50010019026RepositoryImpl +com.umlet.element.base.Class50034019030WorkspaceImplcom.umlet.element.base.Class50014019026NodeTypeManagerImplcom.umlet.element.base.Class50022019026SessionItemStateManagercom.umlet.element.base.Relation35043217054lt=<<<- +listeners20;34;150;34com.umlet.element.base.Relation35035217054lt=-> +nsMappings20;34;150;34com.umlet.element.base.Relation35031217054lt=-> +wsp20;34;150;34com.umlet.element.base.Relation35027217054lt=-> +itemMgr20;34;150;34com.umlet.element.base.Relation35023217054lt=-> +hierMgr20;34;150;34com.umlet.element.base.Relation35019217054lt=-> +itemStateMgr20;34;150;34com.umlet.element.base.Relation35015217054lt=-> +accessMgr20;34;150;34com.umlet.element.base.Relation35011217054lt=-> +ntMgr20;34;150;34com.umlet.element.base.Relation3507617054lt=<<<<- +rep150;34;20;34com.umlet.element.base.Class50042019026VersionManagercom.umlet.element.base.Class50046019026SessionListenercom.umlet.element.base.Class50038019026LocalNamespaceMappingscom.umlet.element.base.Class50030019026ItemManagercom.umlet.element.base.Class50026019026HierarchyManagercom.umlet.element.base.Class50018019026AccessManager \ No newline at end of file diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceImpl.jpg b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceImpl.jpg new file mode 100644 index 00000000000..829f257bdff Binary files /dev/null and b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceImpl.jpg differ diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceImpl.uxf b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceImpl.uxf new file mode 100644 index 00000000000..ccf2940ace3 --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceImpl.uxf @@ -0,0 +1,17 @@ +com.umlet.element.base.Relation60404080lt=->> +20;60;20;20com.umlet.element.base.Relation601204080lt=->> +20;60;20;20com.umlet.element.base.Relation10012018080lt=->> +20;60;20;40;160;40;160;20com.umlet.element.base.Relation14015017054lt=-> +wspConfig20;34;150;34com.umlet.element.base.Class20010020040<<interface>> +EventStateCollectionFactorycom.umlet.element.base.Class1010015040<<interface>> +JackrabbitWorkspacecom.umlet.element.base.Class102015040<<interface>> +Workspacecom.umlet.element.base.Class10180150310WorkspaceImpl +-- +com.umlet.element.base.Class29026018026LocalItemStateMangercom.umlet.element.base.Class29022018026RepositoryImplcom.umlet.element.base.Class29030018026HierarchyManagerImplcom.umlet.element.base.Class29034018026ObservationManagerImplcom.umlet.element.base.Class29038018026QueryManagerImplcom.umlet.element.base.Class29042018026SessionImplcom.umlet.element.base.Relation14043017054lt=-> +lockMgr20;34;150;34com.umlet.element.base.Relation14039017054lt=<-> +session20;34;150;34com.umlet.element.base.Relation14035017054lt=-> +queryManager20;34;150;34com.umlet.element.base.Relation14031017054lt=-> +obsMgr20;34;150;34com.umlet.element.base.Relation14027017054lt=-> +hierMgr20;34;150;34com.umlet.element.base.Relation14023017054lt=-> +stateMgr20;34;150;34com.umlet.element.base.Relation14019017054lt=-> +rep20;34;150;34com.umlet.element.base.Class29018018026WorkspaceConfigcom.umlet.element.base.Class29046018026LockManager \ No newline at end of file diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceInfo.jpg b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceInfo.jpg new file mode 100644 index 00000000000..c932a024191 Binary files /dev/null and b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceInfo.jpg differ diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceInfo.uxf b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceInfo.uxf new file mode 100644 index 00000000000..f861fcb76dc --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/WorkspaceInfo.uxf @@ -0,0 +1,15 @@ +com.umlet.element.base.Class34014320030SharedItemStateManagercom.umlet.element.base.Class1023190310WorkspaceInfo +-- +-initialized: boolean +-initLock: ReadWriteLock +-idleTimestamp: long +-xaLock: Mutex +com.umlet.element.base.Relation18012018054lt=-> +itemStateMgr20;34;160;34com.umlet.element.base.Relation18028018054lt=-> +lockMgr20;34;160;34com.umlet.element.base.Relation18024018054lt=-> +searchMgr20;34;160;34com.umlet.element.base.Relation18020018054lt=-> +systemSession20;34;160;34com.umlet.element.base.Relation18016018054lt=-> +obsMgrFactory20;34;160;34com.umlet.element.base.Relation1808018054lt=-> +persistMgr20;34;160;34com.umlet.element.base.Relation1804018054lt=-> +fs20;34;160;34com.umlet.element.base.Relation180018054lt=-> +config20;34;160;34com.umlet.element.base.Class34030320030LockManagerImplcom.umlet.element.base.Class34026320030SearchManagercom.umlet.element.base.Class34022320030SystemSessioncom.umlet.element.base.Class34018320030ObservationManagerFactorycom.umlet.element.base.Class34010320030PersistenceManagercom.umlet.element.base.Class3406320030FileSystemcom.umlet.element.base.Class3402320030WorkspaceConfig \ No newline at end of file diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/core.jpg b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/core.jpg new file mode 100644 index 00000000000..7bb7a5fc3ae Binary files /dev/null and b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/core.jpg differ diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/core.uxf b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/core.uxf new file mode 100644 index 00000000000..605dec91b5a --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/doc-files/core.uxf @@ -0,0 +1,24 @@ +com.umlet.element.base.Class1010017090RepositoryImpl +bg=yellowcom.umlet.element.base.Class2037015080WorkspaceInfo +bg=yellowcom.umlet.element.base.Class32039013030WorkspaceConfig +bg=yellowcom.umlet.element.base.Class32026020090WorkspaceImpl +bg=yellowcom.umlet.element.base.Class32010020090SessionImpl +bg=yellowcom.umlet.element.base.Class64043018030SharedItemStateManager +bg=yellowcom.umlet.element.base.Class64026018030LocalItemStateManager +bg=yellowcom.umlet.element.base.Class73018019030TransientItemStateManager +bg=yellowcom.umlet.element.base.Class63010018030SessionItemStateManager +bg=yellowcom.umlet.element.base.Relation121170219130lt=-> +rep199;110;29;110;29;20com.umlet.element.base.Relation15036619054lt=-> +config20;34;170;34com.umlet.element.base.Relation15040651054lt=-> +itemStateMgr20;34;490;34com.umlet.element.base.Relation32833010480lt=-> +wspConfig52;20;52;60com.umlet.element.base.Relation70511015090lt=-> +transientStateMgr75;20;75;70com.umlet.element.base.Relation582110156170lt=-> +persistentStateMgr78;20;78;150com.umlet.element.base.Relation651270138180lt=-> +sharedStateMgr69;20;69;160com.umlet.element.base.Relation50023616054lt=-> +stateMgr20;34;140;34com.umlet.element.base.Relation5007615054lt=-> +itemStateMgr20;34;130;34com.umlet.element.base.Relation16013618054lt=-> +rep160;34;20;34com.umlet.element.base.Relation1607618054lt=<<<<-> +activeSessions20;34;160;34com.umlet.element.base.Relation33717086110lt=-> +session43;90;43;20com.umlet.element.base.Relation43817064110lt=-> +wsp32;20;32;90com.umlet.element.base.Relation1317094220lt=<<<<- +wspInfos47;20;47;200 \ No newline at end of file diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/fs/db/package.html b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/fs/db/package.html new file mode 100644 index 00000000000..f2aea8ec472 --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/fs/db/package.html @@ -0,0 +1,33 @@ + + +This package contains the class {@link DbFileSystem}, +a simple generic JDBC-based FileSystem implementation +for Jackrabbit. +

    +It also contains [schemaName].ddl files which are read by +{@link DbFileSystem} in order to automatically +create the required schema objects on the target database. Every line in a +[schemaName].ddl file is executed separatly by calling +java.sql.Statement.execute(String) where every occurence of the +the string "${schemaObjectPrefix}" has been replaced with the +value of the property schemaObjectPrefix (see + {@link DbFileSystem#setSchemaObjectPrefix(String)}). +The schema name is either set programmtically by calling +{@link DbFileSystem#setSchema(String)} or configured +through the schema bean property. + diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/observation/doc-files/class.jpg b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/observation/doc-files/class.jpg new file mode 100644 index 00000000000..b4f99ceeb37 Binary files /dev/null and b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/observation/doc-files/class.jpg differ diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/observation/doc-files/class.uxf b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/observation/doc-files/class.uxf new file mode 100644 index 00000000000..d5567871b28 --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/observation/doc-files/class.uxf @@ -0,0 +1,18 @@ +com.umlet.element.base.Note26033014070Green classes +are session-local +bg=greencom.umlet.element.base.Note10033014070Yellow classes +are global, i.e. not +bound to a session +bg=yellowcom.umlet.element.base.Class50035018050<<interface>> +/EventListener/com.umlet.element.base.Class7403013050SessionImpl +bg=greencom.umlet.element.base.Class5003018050ItemManager +bg=greencom.umlet.element.base.Class2603018050WorkspaceImpl +bg=greencom.umlet.element.base.Package230100480210observation +bg=com.umlet.element.base.Class50024018050EventConsumer +bg=greencom.umlet.element.base.Class50014018050EventFilter +bg=greencom.umlet.element.base.Class26024018050<<thread>> +ObservationDispatcher +bg=yellowcom.umlet.element.base.Class26014018050ObservationManagerImpl +bg=greencom.umlet.element.base.Class2014018050RepositoryImpl +bg=yellowcom.umlet.element.base.Class2024018050WorkspaceInfo +bg=yellowcom.umlet.element.base.Relation66060170210lt=->20;190;150;190;150;20com.umlet.element.base.Relation66060170110lt=->20;90;150;90;150;20com.umlet.element.base.Relation901704090lt=<<<<20;20;20;70com.umlet.element.base.Relation3301704090lt=->20;20;20;70com.umlet.element.base.Relation42023010040lt=<<<<20;20;80;20com.umlet.element.base.Relation5706040100lt=->20;80;20;20com.umlet.element.base.Relation5701704090lt=->20;70;20;20com.umlet.element.base.Relation3306040100lt=->20;20;20;80com.umlet.element.base.Relation57027040100lt=->20;20;20;80com.umlet.element.base.Relation18023010040lt=->20;20;80;20 \ No newline at end of file diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/package.html b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/package.html new file mode 100644 index 00000000000..18abbcc6711 --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/package.html @@ -0,0 +1,115 @@ + + +Contains the core classes that provide the implementation of the JCR API. +

    +The following table lists the core JCR interfaces and the corresponding +Jackrabbit implementation classes found in this package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    JCR interfaceImplementation class
    {@link javax.jcr.Repository Repository}{@link org.apache.jackrabbit.core.RepositoryImpl RepositoryImpl}
    {@link javax.jcr.Session Session}{@link org.apache.jackrabbit.core.SessionImpl SessionImpl}
    {@link javax.jcr.Workspace Workspace}{@link org.apache.jackrabbit.core.WorkspaceImpl WorkspaceImpl}
    {@link javax.jcr.Item Item}{@link org.apache.jackrabbit.core.ItemImpl ItemImpl}
    {@link javax.jcr.Property Property}{@link org.apache.jackrabbit.core.PropertyImpl PropertyImpl}
    {@link javax.jcr.Node Node}{@link org.apache.jackrabbit.core.NodeImpl NodeImpl}
    +

    +A Jackrabbit repository instance can be created using the static +{@link org.apache.jackrabbit.core.RepositoryImpl#create(org.apache.jackrabbit.core.config.RepositoryConfig) RepositoryImpl.create(RepositoryConfig)} +method. The +{@link org.apache.jackrabbit.core.jndi.RepositoryHelper RepositoryHelper} +and other classes in the +{@link org.apache.jackrabbit.core.jndi org.apache.jackrabbit.core.jndi} +package provide a mechanism for binding a Jackrabbit repository in a +JNDI directory context. +

    +A SessionImpl instance is created upon successfully login to the +Repository (see Repository#login(Credentials, String)). +

    +A Session is always tied to the Workspace +specified in the Repository#login(Credentials, String) call. A +workspace represents a persistent tree of repository items (i.e. Nodes +and Propertys). The items in a workspace are 'visible' to all +sessions accessing it (subject to their access rights, of course). +A WorkspaceImpl instance represents a specifc workspace as +seen by the session that accesses it. +

    +Every repository item is uniquely identified by its ItemId. The id +of a node (NodeId) consists of the node's uuid. The id of a property +(PropertyId) consists of the parent node's uuid and the +name of the property. + +

    Item managers

    +

    +Every SessionImpl instance has its own ItemManager. +The per-session instance of ItemManager acts as item factory (i.e. +it creates NodeImpl and PropertyImpl instances) and +provides item access by item id and item caching. +

    +The data (or state) of an item is represented by the following classes in the +subpackage state: +

      +
    • {@link org.apache.jackrabbit.core.state.ItemState ItemState}
    • +
    • {@link org.apache.jackrabbit.core.state.PropertyState PropertyState}
    • +
    • {@link org.apache.jackrabbit.core.state.NodeState NodeState}
    • +
    +

    +There's one SharedItemStateManager for every workspace. +It provides item state caching and it guarantees that there's only one +(persistent) item state instance for any distinct item id in that workspace. +

    +Every session has its own SessionItemStateManager that consists +of the session's TransientItemStateManager and the workspace's +SharedItemStateManager. +

    +Each item (i.e. NodeImpl and PropertyImpl) instance +is holding an ItemState instance. When e.g. a session is modifying +a property by changing the property's value, a new transient item state +is created by the session's TransientItemStateManager. This +transient state is actually wrapping the (old) shared state (copy on write). +The PropertyImpl's state is then replaced by the new transient state. +

    +Transient (i.e. unsaved) modifications are 'session-local', i.e. they are not +visible to other sessions. When the modifications are saved they become instantly +visible to all sessions accessing the same workspace. + diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/persistence/db/package.html b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/persistence/db/package.html new file mode 100644 index 00000000000..1d4a9ff7633 --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/persistence/db/package.html @@ -0,0 +1,32 @@ + + +This package contains the class {@link SimpleDbPersistenceManager}, +a simple generic JDBC-based PersistenceManager for Jackrabbit. +

    +It also contains [schemaName].ddl files which are read by +{@link SimpleDbPersistenceManager} in order to automatically +create the required schema objects on the target database. Every line in a +[schemaName].ddl file is executed separatly by calling +java.sql.Statement.execute(String) where every occurence of the +the string "${schemaObjectPrefix}" has been replaced with the +value of the property schemaObjectPrefix (see + {@link SimpleDbPersistenceManager#setSchemaObjectPrefix(String)}). +The schema name is either set programmtically by calling +{@link SimpleDbPersistenceManager#setSchema(String)} or configured +through the schema bean property. + diff --git a/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/version/package.html b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/version/package.html new file mode 100644 index 00000000000..0a7212e8353 --- /dev/null +++ b/jackrabbit-core/src/main/javadoc/org/apache/jackrabbit/core/version/package.html @@ -0,0 +1,40 @@ + + +The versioning framework in jackrabbit consists of 3 layers. A persistence layer, +an application layer and an presentation layer.

    +The persistence layer uses a 'normal' workspace as storage. The storage can later +be exchanged by a more scaleable one. The application layer operates on the +persistent one and provides the internal view of the version storage. It is +manifested through the InternalVersion and +InternalVersionHistory objects. +The internal versions are also mapped to the content and exposed through the +API (i.e. the Version extends Node and can be inspected +with normal Node methods). Furthermore the version store is also exposed below +/jcr:system/jcr:versionStorage. The exact structure of it is not +defined yet, so applications should not relay on it and rather use +Node.getVersionHistory() to access a particular version history or +use the search mechanisms to find the respective versions.

    +The presentation layer is managed by the VersionManager. it is +responsible for mapping the version storage to the content. +

    +Open issues: +

      +
    • consider multi-threading behaviour +
    + + diff --git a/jackrabbit-core/src/main/resources-filtered/org/apache/jackrabbit/core/repository.properties b/jackrabbit-core/src/main/resources-filtered/org/apache/jackrabbit/core/repository.properties new file mode 100644 index 00000000000..dd48dca05c7 --- /dev/null +++ b/jackrabbit-core/src/main/resources-filtered/org/apache/jackrabbit/core/repository.properties @@ -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. + +################################################################################ +# Repository Version and Implementation Information +# + +# The descriptor for the version of the specification that this repository implements. +jcr.specification.version = 2.0 + +# The descriptor for the name of the specification that this repository implements. +jcr.specification.name = Content Repository API for Java(TM) Technology Specification + +# The descriptor for the name of the repository vendor. +jcr.repository.vendor = Apache Software Foundation + +# The descriptor for the URL of the repository vendor. +jcr.repository.vendor.url = http://jackrabbit.apache.org/ + +# The descriptor for the name of this repository implementation. +jcr.repository.name = Jackrabbit + +# The descriptor for the version of this repository implementation. +jcr.repository.version = ${pom.version} + +# The repository supports the AdditionalEventInfo extension +org.apache.jackrabbit.spi.commons.AdditionalEventInfo = true diff --git a/jackrabbit-core/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory b/jackrabbit-core/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory new file mode 100644 index 00000000000..cfc5ddfa6d6 --- /dev/null +++ b/jackrabbit-core/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory @@ -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. + +# +# This file lists the repository factory implementation for jackrabbit +# + +org.apache.jackrabbit.core.RepositoryFactoryImpl diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/JackrabbitRepositoryStub.properties b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/JackrabbitRepositoryStub.properties new file mode 100644 index 00000000000..8a2f82192ce --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/JackrabbitRepositoryStub.properties @@ -0,0 +1,526 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# 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.nodetypetestroot=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=test + +# 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.core.RepositoryFactoryImpl + +# 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: 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/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/deprecated-classes.properties b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/deprecated-classes.properties new file mode 100644 index 00000000000..ef0adad04d9 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/deprecated-classes.properties @@ -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. + +# This file contains a mapping of deprecated classes to their replacement. +# When a BeanConfig reads the class attribute it will automatically replace +# the deprecated class name with its replacement and log an info message. + +# Security +org.apache.jackrabbit.core.security.SimpleAccessManager=org.apache.jackrabbit.core.security.simple.SimpleAccessManager +org.apache.jackrabbit.core.security.SimpleLoginModule=org.apache.jackrabbit.core.security.simple.SimpleLoginModule + +# DB persistence managers +org.apache.jackrabbit.core.state.db.DerbyPersistenceManager=org.apache.jackrabbit.core.persistence.db.DerbyPersistenceManager +org.apache.jackrabbit.core.state.db.JNDIDatabasePersistenceManager=org.apache.jackrabbit.core.persistence.db.JNDIDatabasePersistenceManager +org.apache.jackrabbit.core.state.db.OraclePersistenceManager=org.apache.jackrabbit.core.persistence.db.OraclePersistenceManager +org.apache.jackrabbit.core.state.db.SimpleDbPersistenceManager=org.apache.jackrabbit.core.persistence.db.SimpleDbPersistenceManager + +# In memory persistence manager +org.apache.jackrabbit.core.state.mem.InMemPersistenceManager=org.apache.jackrabbit.core.persistence.mem.InMemPersistenceManager + +# Object persistence manager +org.apache.jackrabbit.core.state.obj.ObjectPersistenceManager=org.apache.jackrabbit.core.persistence.obj.ObjectPersistenceManager + +# XML persistence manager +org.apache.jackrabbit.core.state.xml.XMLPersistenceManager=org.apache.jackrabbit.core.persistence.xml.XMLPersistenceManager + +# "Pooled" bundle fs persistence manager +org.apache.jackrabbit.core.persistence.pool.BundleFsPersistenceManager=org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager + +# Non-pooled bundle db persistence managers +org.apache.jackrabbit.core.persistence.bundle.BundleDbPersistenceManager=org.apache.jackrabbit.core.persistence.pool.BundleDbPersistenceManager +org.apache.jackrabbit.core.persistence.bundle.DerbyPersistenceManager=org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager +org.apache.jackrabbit.core.persistence.bundle.H2PersistenceManager=org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager +org.apache.jackrabbit.core.persistence.bundle.MSSqlPersistenceManager=org.apache.jackrabbit.core.persistence.pool.MSSqlPersistenceManager +org.apache.jackrabbit.core.persistence.bundle.MySqlPersistenceManager=org.apache.jackrabbit.core.persistence.pool.MySqlPersistenceManager +org.apache.jackrabbit.core.persistence.bundle.OraclePersistenceManager=org.apache.jackrabbit.core.persistence.pool.OraclePersistenceManager +org.apache.jackrabbit.core.persistence.bundle.Oracle9PersistenceManager=org.apache.jackrabbit.core.persistence.pool.Oracle9PersistenceManager +org.apache.jackrabbit.core.persistence.bundle.PostgreSQLPersistenceManager=org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.0.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.0.dtd new file mode 100644 index 00000000000..fa636e0f3f7 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.0.dtd @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.2.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.2.dtd new file mode 100644 index 00000000000..5bee20fb51f --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.2.dtd @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.4.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.4.dtd new file mode 100644 index 00000000000..02a7f72a47c --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.4.dtd @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.5.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.5.dtd new file mode 100644 index 00000000000..7ef6a8fcaac --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.5.dtd @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.6.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.6.dtd new file mode 100644 index 00000000000..0150ac5738d --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-1.6.dtd @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.0-elements.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.0-elements.dtd new file mode 100644 index 00000000000..816fa5df522 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.0-elements.dtd @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.0.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.0.dtd new file mode 100644 index 00000000000..4f9123d7356 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.0.dtd @@ -0,0 +1,62 @@ + + + +%repository-elements; + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.4-elements.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.4-elements.dtd new file mode 100644 index 00000000000..4a2d69d3f1e --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.4-elements.dtd @@ -0,0 +1,262 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.4.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.4.dtd new file mode 100644 index 00000000000..36e69bd6cd5 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.4.dtd @@ -0,0 +1,64 @@ + + + +%repository-elements; + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd new file mode 100644 index 00000000000..94db1c110d2 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd @@ -0,0 +1,266 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd new file mode 100644 index 00000000000..8b948fd0ecf --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd @@ -0,0 +1,64 @@ + + + +%repository-elements; + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/azure.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/azure.ddl new file mode 100644 index 00000000000..00b89d24234 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/azure.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar(2048) not null, FSENTRY_NAME varchar(255) not null, FSENTRY_DATA image null, FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) +create unique clustered index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) \ No newline at end of file diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/daffodil.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/daffodil.ddl new file mode 100644 index 00000000000..c76975b5849 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/daffodil.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar(2048) not null, FSENTRY_NAME varchar(255) not null, FSENTRY_DATA blob, FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) +create index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/db2.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/db2.ddl new file mode 100644 index 00000000000..ada420f7bd2 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/db2.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar(745) not null, FSENTRY_NAME varchar(255) not null, FSENTRY_DATA blob(100M), FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/default.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/default.ddl new file mode 100644 index 00000000000..282e7a6dcae --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/default.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar not null, FSENTRY_NAME varchar not null, FSENTRY_DATA varbinary null, FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/derby.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/derby.ddl new file mode 100644 index 00000000000..4d5b9aa2f9f --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/derby.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar(2048) not null, FSENTRY_NAME varchar(255) not null, FSENTRY_DATA blob(100M), FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/ingres.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/ingres.ddl new file mode 100644 index 00000000000..e50d3719705 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/ingres.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar(744) not null, FSENTRY_NAME varchar(255) not null, FSENTRY_DATA long byte with null, FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/maxdb.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/maxdb.ddl new file mode 100644 index 00000000000..36549d2f385 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/maxdb.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH VARCHAR(255) not null, FSENTRY_NAME VARCHAR(255) not null, FSENTRY_DATA LONG BYTE null, FSENTRY_LASTMOD FIXED(38,0) not null, FSENTRY_LENGTH FIXED(38,0) not null) +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/mssql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/mssql.ddl new file mode 100644 index 00000000000..ab37b1c0358 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/mssql.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar(2048) not null, FSENTRY_NAME varchar(255) not null, FSENTRY_DATA image null, FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) ${tableSpace} +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) ${tableSpace} diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/mysql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/mysql.ddl new file mode 100644 index 00000000000..a1a1f6ab611 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/mysql.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH text not null, FSENTRY_NAME varchar(255) not null, FSENTRY_DATA longblob null, FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) character set latin1 +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH(245), FSENTRY_NAME) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/oracle.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/oracle.ddl new file mode 100644 index 00000000000..596d8977ada --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/oracle.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar2(2048) not null, FSENTRY_NAME varchar2(255) not null, FSENTRY_DATA blob null, FSENTRY_LASTMOD number(38,0) not null, FSENTRY_LENGTH number(38,0) null) ${tablespace} +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) ${indexTablespace} diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/postgresql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/postgresql.ddl new file mode 100644 index 00000000000..ecfb26b0197 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/fs/db/postgresql.ddl @@ -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. +create table ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH varchar not null, FSENTRY_NAME varchar not null, FSENTRY_DATA bytea null, FSENTRY_LASTMOD bigint not null, FSENTRY_LENGTH bigint not null) +create unique index ${schemaObjectPrefix}FSENTRY_IDX on ${schemaObjectPrefix}FSENTRY (FSENTRY_PATH, FSENTRY_NAME) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/azure.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/azure.ddl new file mode 100644 index 00000000000..5289f54417c --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/azure.ddl @@ -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. + +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID BIGINT NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA IMAGE) +create unique clustered index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID BIGINT NOT NULL) +create unique clustered index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID BIGINT NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) \ No newline at end of file diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/db2.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/db2.ddl new file mode 100644 index 00000000000..a9a73d35afc --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/db2.ddl @@ -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. +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID BIGINT NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA blob) +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID BIGINT NOT NULL) +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID BIGINT NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/default.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/default.ddl new file mode 100644 index 00000000000..e67cd0f46e2 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/default.ddl @@ -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. +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID BIGINT NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA varbinary) +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID BIGINT NOT NULL) +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID BIGINT NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/derby.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/derby.ddl new file mode 100644 index 00000000000..60c7f342297 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/derby.ddl @@ -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. +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID BIGINT NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA blob) +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID BIGINT NOT NULL) +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID BIGINT NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/h2.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/h2.ddl new file mode 100644 index 00000000000..000c8ea8582 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/h2.ddl @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# DDL script for the H2 database engine (http://www.h2database.com) +# +set max_length_inplace_lob 4096 +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID bigint primary key, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA blob) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID bigint primary key) +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID bigint NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/ingres.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/ingres.ddl new file mode 100644 index 00000000000..cdff83a8fbb --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/ingres.ddl @@ -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. +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID BIGINT NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA long byte) +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID BIGINT NOT NULL) +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID BIGINT NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) \ No newline at end of file diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/maxdb.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/maxdb.ddl new file mode 100644 index 00000000000..e61dbfae426 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/maxdb.ddl @@ -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. +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID FIXED(38,0) NOT NULL, JOURNAL_ID VARCHAR(255), PRODUCER_ID VARCHAR(255), REVISION_DATA LONG BYTE) +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID FIXED(38,0) NOT NULL) +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/mssql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/mssql.ddl new file mode 100644 index 00000000000..ccec6e3d15b --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/mssql.ddl @@ -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. + +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID BIGINT NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA IMAGE) ${tableSpace} +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) ${tableSpace} +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID BIGINT NOT NULL) ${tableSpace} +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) ${tableSpace} +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID BIGINT NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/mysql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/mysql.ddl new file mode 100644 index 00000000000..30520af7127 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/mysql.ddl @@ -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. +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID BIGINT NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA longblob) +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID BIGINT NOT NULL) +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID BIGINT NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/oracle.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/oracle.ddl new file mode 100644 index 00000000000..6977012580d --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/oracle.ddl @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID number(20,0) NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA blob) ${tablespace} +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) ${indexTablespace} + +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID number(20,0) NOT NULL) ${tablespace} +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) ${indexTablespace} + +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID number(20,0) NOT NULL) ${tablespace} + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/postgresql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/postgresql.ddl new file mode 100644 index 00000000000..7b9921e4eee --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/journal/postgresql.ddl @@ -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. +create table ${schemaObjectPrefix}JOURNAL (REVISION_ID BIGINT NOT NULL, JOURNAL_ID varchar(255), PRODUCER_ID varchar(255), REVISION_DATA bytea) +create unique index ${schemaObjectPrefix}JOURNAL_IDX on ${schemaObjectPrefix}JOURNAL (REVISION_ID) +create table ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID BIGINT NOT NULL) +create unique index ${schemaObjectPrefix}GLOBAL_REVISION_IDX on ${schemaObjectPrefix}GLOBAL_REVISION (REVISION_ID) +create table ${schemaObjectPrefix}LOCAL_REVISIONS (JOURNAL_ID varchar(255) NOT NULL, REVISION_ID BIGINT NOT NULL) + +# Inserting the one and only revision counter record now helps avoiding race conditions +insert into ${schemaObjectPrefix}GLOBAL_REVISION VALUES(0) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/nodetype/builtin_nodetypes.cnd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/nodetype/builtin_nodetypes.cnd new file mode 100644 index 00000000000..dfd785d6103 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/nodetype/builtin_nodetypes.cnd @@ -0,0 +1,638 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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) + +//------------------------------------------------------------------------------ +// 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 + + +/** + * 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 + - * (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' + +// ----------------------------------------------------------------------------- +// 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 + +// ----------------------------------------------------------------------------- +// Token Management (backported from oak) +// ----------------------------------------------------------------------------- +[rep:Token] > mix:referenceable + - rep:token.key (STRING) protected mandatory + - rep:token.exp (DATE) protected mandatory + - * (UNDEFINED) protected + - * (UNDEFINED) multiple protected diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/azure.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/azure.ddl new file mode 100644 index 00000000000..09610c132b8 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/azure.ddl @@ -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. +create table ${schemaObjectPrefix}BUNDLE (NODE_ID binary(16) not null, BUNDLE_DATA image not null) +create unique clustered index ${schemaObjectPrefix}BUNDLE_IDX on ${schemaObjectPrefix}BUNDLE (NODE_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID binary(16) not null, REFS_DATA image not null) +create unique clustered index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(64) not null, BINVAL_DATA image not null) +create unique clustered index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) +create table ${schemaObjectPrefix}NAMES (ID INTEGER IDENTITY(1,1) PRIMARY KEY, NAME varchar(255) COLLATE Latin1_General_CS_AS not null) \ No newline at end of file diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/db2.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/db2.ddl new file mode 100644 index 00000000000..7b271e04ee1 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/db2.ddl @@ -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. +create table ${schemaObjectPrefix}BUNDLE (NODE_ID CHAR(16) FOR BIT DATA not null, BUNDLE_DATA blob(100M) not null) +create unique index ${schemaObjectPrefix}BUNDLE_IDX on ${schemaObjectPrefix}BUNDLE (NODE_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID CHAR(16) FOR BIT DATA not null, REFS_DATA blob(100M) not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(64) not null, BINVAL_DATA blob(1000M) not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) +create table ${schemaObjectPrefix}NAMES (ID INTEGER GENERATED ALWAYS AS IDENTITY, NAME varchar(255) not null) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/derby.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/derby.ddl new file mode 100644 index 00000000000..857285a6e3e --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/derby.ddl @@ -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. +create table ${schemaObjectPrefix}BUNDLE (NODE_ID_HI bigint not null, NODE_ID_LO bigint not null, BUNDLE_DATA blob(2G) not null, PRIMARY KEY (NODE_ID_HI, NODE_ID_LO)) +create table ${schemaObjectPrefix}REFS (NODE_ID_HI bigint not null, NODE_ID_LO bigint not null, REFS_DATA blob(2G) not null, PRIMARY KEY (NODE_ID_HI, NODE_ID_LO)) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID char(64) PRIMARY KEY, BINVAL_DATA blob(2G) not null) +create table ${schemaObjectPrefix}NAMES (ID INTEGER GENERATED ALWAYS AS IDENTITY, NAME varchar(255) not null, PRIMARY KEY (ID, NAME)) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/h2.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/h2.ddl new file mode 100644 index 00000000000..ab5317eb342 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/h2.ddl @@ -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. +create cached table ${schemaObjectPrefix}BUNDLE (NODE_ID binary(16) PRIMARY KEY, BUNDLE_DATA varbinary not null) +create cached table ${schemaObjectPrefix}REFS (NODE_ID binary(16) PRIMARY KEY, REFS_DATA varbinary not null) +create cached table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(64) PRIMARY KEY, BINVAL_DATA blob not null) +create cached table ${schemaObjectPrefix}NAMES (ID INTEGER AUTO_INCREMENT PRIMARY KEY, NAME varchar(255) not null) \ No newline at end of file diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/ingres.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/ingres.ddl new file mode 100644 index 00000000000..d57a91b160c --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/ingres.ddl @@ -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. +create table ${schemaObjectPrefix}BUNDLE (NODE_ID byte(16) not null, BUNDLE_DATA long byte not null) +create unique index ${schemaObjectPrefix}BUNDLE_IDX on ${schemaObjectPrefix}BUNDLE (NODE_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID byte(16) not null, REFS_DATA long byte not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(64), BINVAL_DATA long byte not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) +create sequence ${schemaObjectPrefix}seq_names_id +create table ${schemaObjectPrefix}NAMES (ID INTEGER PRIMARY KEY WITH DEFAULT NEXT VALUE FOR ${schemaObjectPrefix}seq_names_id, NAME nvarchar(255) not null) \ No newline at end of file diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/mssql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/mssql.ddl new file mode 100644 index 00000000000..a8914f5e5ea --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/mssql.ddl @@ -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. +create table ${schemaObjectPrefix}BUNDLE (NODE_ID binary(16) not null, BUNDLE_DATA image not null) ${tableSpace} +create unique index ${schemaObjectPrefix}BUNDLE_IDX on ${schemaObjectPrefix}BUNDLE (NODE_ID) ${tableSpace} +create table ${schemaObjectPrefix}REFS (NODE_ID binary(16) not null, REFS_DATA image not null) ${tableSpace} +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) ${tableSpace} +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(64) not null, BINVAL_DATA image not null) ${tableSpace} +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) ${tableSpace} +create table ${schemaObjectPrefix}NAMES (ID INTEGER IDENTITY(1,1) PRIMARY KEY, NAME varchar(255) COLLATE Latin1_General_CS_AS not null) ${tableSpace} diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/mysql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/mysql.ddl new file mode 100644 index 00000000000..4d80a14b13d --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/mysql.ddl @@ -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. +create table ${schemaObjectPrefix}BUNDLE (NODE_ID varbinary(16) not null, BUNDLE_DATA longblob not null) +create unique index ${schemaObjectPrefix}BUNDLE_IDX on ${schemaObjectPrefix}BUNDLE (NODE_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID varbinary(16) not null, REFS_DATA longblob not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(64) not null, BINVAL_DATA longblob not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) +create table ${schemaObjectPrefix}NAMES (ID INTEGER AUTO_INCREMENT PRIMARY KEY, NAME varchar(255) character set utf8 collate utf8_bin not null) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/oracle.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/oracle.ddl new file mode 100644 index 00000000000..5b35d2a7b83 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/oracle.ddl @@ -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. +create table ${schemaObjectPrefix}BUNDLE (NODE_ID raw(16) not null, BUNDLE_DATA blob not null) ${tablespace} +create unique index ${schemaObjectPrefix}BUNDLE_IDX on ${schemaObjectPrefix}BUNDLE (NODE_ID) ${indexTablespace} + +create table ${schemaObjectPrefix}REFS (NODE_ID raw(16) not null, REFS_DATA blob not null) ${tablespace} +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) ${indexTablespace} + +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar2(64) not null, BINVAL_DATA blob null) ${tablespace} +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) ${indexTablespace} + +create table ${schemaObjectPrefix}NAMES (ID INTEGER primary key using index ${indexTablespace}, NAME varchar2(255) not null) ${tablespace} +create unique index ${schemaObjectPrefix}NAMES_IDX on ${schemaObjectPrefix}NAMES (NAME) ${indexTablespace} + +create sequence ${schemaObjectPrefix}seq_names_id +create trigger ${schemaObjectPrefix}t1 before insert on ${schemaObjectPrefix}NAMES for each row begin select ${schemaObjectPrefix}seq_names_id.nextval into :new.id from dual; end; \ No newline at end of file diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/postgresql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/postgresql.ddl new file mode 100644 index 00000000000..875a85f6e3d --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/bundle/postgresql.ddl @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +create table ${schemaObjectPrefix}BUNDLE (NODE_ID_HI bigint not null, NODE_ID_LO bigint not null, BUNDLE_DATA bytea not null, PRIMARY KEY (NODE_ID_HI, NODE_ID_LO)) +create table ${schemaObjectPrefix}REFS (NODE_ID_HI bigint not null, NODE_ID_LO bigint not null, REFS_DATA bytea not null, PRIMARY KEY (NODE_ID_HI, NODE_ID_LO)) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(64) not null, BINVAL_DATA bytea not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) +create table ${schemaObjectPrefix}NAMES (ID SERIAL PRIMARY KEY, NAME varchar(255) not null) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/azure.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/azure.ddl new file mode 100644 index 00000000000..2ff9f5946f3 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/azure.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA image not null) +create unique clustered index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar(1024) not null, PROP_DATA image not null) +create unique clustered index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA image not null) +create unique clustered index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(1024) not null, BINVAL_DATA image not null) +create unique clustered index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) \ No newline at end of file diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/daffodil.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/daffodil.ddl new file mode 100644 index 00000000000..6fb094f90e9 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/daffodil.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA blob not null) +create index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar(1024) not null, PROP_DATA blob not null) +create index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA blob not null) +create index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(1024) not null, BINVAL_DATA blob not null) +create index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/db2.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/db2.ddl new file mode 100644 index 00000000000..587793d5d62 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/db2.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA blob not null) +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar(1000) not null, PROP_DATA blob not null) +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA blob not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(1000) not null, BINVAL_DATA blob(100M) not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/default.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/default.ddl new file mode 100644 index 00000000000..b656cb4aa85 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/default.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA varbinary not null) +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar not null, PROP_DATA varbinary not null) +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA varbinary not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar not null, BINVAL_DATA varbinary not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/derby.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/derby.ddl new file mode 100644 index 00000000000..2846894724d --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/derby.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA blob not null) +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar(1024) not null, PROP_DATA blob not null) +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA blob not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(1024) not null, BINVAL_DATA blob(100M) not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/h2.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/h2.ddl new file mode 100644 index 00000000000..b150489a85b --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/h2.ddl @@ -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. +# +# DDL script for the H2 database engine (http://www.h2database.com) +# +set max_length_inplace_lob 4096 +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) primary key, NODE_DATA blob not null) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar(1024) primary key, PROP_DATA blob not null) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) primary key, REFS_DATA blob not null) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(1024) primary key, BINVAL_DATA blob not null) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/ingres.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/ingres.ddl new file mode 100644 index 00000000000..d92f10ebc7d --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/ingres.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA long byte not null) +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar(1000) not null, PROP_DATA long byte not null) +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA long byte not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(1000) not null, BINVAL_DATA long byte not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/maxdb.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/maxdb.ddl new file mode 100644 index 00000000000..1fcedc75060 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/maxdb.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID VARCHAR(36) not null, NODE_DATA LONG BYTE not null) +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID VARCHAR(1000) not null, PROP_DATA LONG BYTE not null) +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID VARCHAR(36) not null, REFS_DATA LONG BYTE not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID VARCHAR(1000) not null, BINVAL_DATA LONG BYTE not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/mssql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/mssql.ddl new file mode 100644 index 00000000000..4c26da3a32d --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/mssql.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA image not null) ${tableSpace} +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) ${tableSpace} +create table ${schemaObjectPrefix}PROP (PROP_ID varchar(1024) not null, PROP_DATA image not null) ${tableSpace} +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) ${tableSpace} +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA image not null) ${tableSpace} +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) ${tableSpace} +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(1024) not null, BINVAL_DATA image not null) ${tableSpace} +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) ${tableSpace} diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/mysql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/mysql.ddl new file mode 100644 index 00000000000..26d45637523 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/mysql.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA mediumblob not null) +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar(255) not null, PROP_DATA mediumblob not null) +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA mediumblob not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar(255) not null, BINVAL_DATA longblob not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/oracle.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/oracle.ddl new file mode 100644 index 00000000000..f7dcaed65a9 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/oracle.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA blob not null) ${tableSpace} +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) ${tableSpace} +create table ${schemaObjectPrefix}PROP (PROP_ID varchar2(1024) not null, PROP_DATA blob not null) ${tableSpace} +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) ${tableSpace} +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA blob not null) ${tableSpace} +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) ${tableSpace} +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar2(1024) not null, BINVAL_DATA blob null) ${tableSpace} +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) ${tableSpace} diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/postgresql.ddl b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/postgresql.ddl new file mode 100644 index 00000000000..c561f5b0689 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/persistence/db/postgresql.ddl @@ -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. +create table ${schemaObjectPrefix}NODE (NODE_ID char(36) not null, NODE_DATA bytea not null) +create unique index ${schemaObjectPrefix}NODE_IDX on ${schemaObjectPrefix}NODE (NODE_ID) +create table ${schemaObjectPrefix}PROP (PROP_ID varchar not null, PROP_DATA bytea not null) +create unique index ${schemaObjectPrefix}PROP_IDX on ${schemaObjectPrefix}PROP (PROP_ID) +create table ${schemaObjectPrefix}REFS (NODE_ID char(36) not null, REFS_DATA bytea not null) +create unique index ${schemaObjectPrefix}REFS_IDX on ${schemaObjectPrefix}REFS (NODE_ID) +create table ${schemaObjectPrefix}BINVAL (BINVAL_ID varchar not null, BINVAL_DATA bytea not null) +create unique index ${schemaObjectPrefix}BINVAL_IDX on ${schemaObjectPrefix}BINVAL (BINVAL_ID) diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.0.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.0.dtd new file mode 100644 index 00000000000..93d759d3b1a --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.0.dtd @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.1.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.1.dtd new file mode 100644 index 00000000000..98f05784586 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.1.dtd @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.2.dtd b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.2.dtd new file mode 100644 index 00000000000..ad5bdda16aa --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/indexing-configuration-1.2.dtd @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/tika-config.xml b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/tika-config.xml new file mode 100644 index 00000000000..8f82ba73a02 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/query/lucene/tika-config.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + application/x-archive + application/x-bzip + application/x-bzip2 + application/x-cpio + application/x-gtar + application/x-gzip + application/x-tar + application/zip + + image/bmp + image/gif + image/jpeg + image/png + image/vnd.wap.wbmp + image/x-icon + image/x-psd + image/x-xcf + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/repository.xml b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/repository.xml new file mode 100644 index 00000000000..78ad2bd9343 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/repository.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/test-nodetypes.xml b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/test-nodetypes.xml new file mode 100644 index 00000000000..0d1d3a38dd3 --- /dev/null +++ b/jackrabbit-core/src/main/resources/org/apache/jackrabbit/core/test-nodetypes.xml @@ -0,0 +1,245 @@ + + + + + + mix:versionable + nt:base + + + + + + + + + + + nt:base + + + + + nt:base + + + + + nt:base + + + + + nt:base + + + + + nt:base + + + + + nt:base + + + + + nt:base + + + + + + + + mix:versionable + nt:base + + + + + + + + nt:base + + + + + + abc + def + ghi + + + + + abc + def + ghi + + + + + + + (,100) + + + + + (,100) + + + + + + + (1974-02-15T00:00:00.000Z,) + + + + + (,1974-02-15T00:00:00.000Z) + + + + + + + (100,) + + + + + (,100) + + + + + + + (100,) + + + + + (,100) + + + + + + + true + + + + + true + + + + + + + abc + + + + + abc + + + + + + + /abc + + + + + /abc + + + + + test:canSetProperty + + + + + test:canSetProperty + + + + + + + + nt:base + + + + nt:base + + + + + nt:base + + + + + + + + nt:base + mix:referenceable + + + + + + + + + + nt:base + + + + + + + + + + nt:base + + + + + nt:base + + + + + diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/JackrabbitNodeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/JackrabbitNodeTest.java new file mode 100644 index 00000000000..3a8f9929d22 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/JackrabbitNodeTest.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.api; + +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.api.observation.EventResult; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.ObservationManager; +import java.io.InputStreamReader; +import java.io.Reader; + +/** + * JackrabbitNodeTest... + */ +public class JackrabbitNodeTest extends AbstractJCRTest { + + static final String SEQ_BEFORE = "jackrabbit"; + static final String SEQ_AFTER = "jackraBbit"; + static final int RELPOS = 6; + + static final String TEST_NODETYPES = "org/apache/jackrabbit/api/test_mixin_nodetypes.cnd"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + assertTrue(testRootNode.getPrimaryNodeType().hasOrderableChildNodes()); + for (char c : SEQ_BEFORE.toCharArray()) { + testRootNode.addNode(new String(new char[]{c})); + } + superuser.save(); + + Reader cnd = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(TEST_NODETYPES)); + CndImporter.registerNodeTypes(cnd, superuser); + cnd.close(); + } + + public void testRename() throws RepositoryException { + Node renamedNode = null; + NodeIterator it = testRootNode.getNodes(); + int pos = 0; + while (it.hasNext()) { + Node n = it.nextNode(); + String name = n.getName(); + assertEquals(name, new String(new char[]{SEQ_BEFORE.charAt(pos)})); + if (pos == RELPOS) { + JackrabbitNode node = (JackrabbitNode) n; + node.rename(name.toUpperCase()); + renamedNode = n; + } + pos++; + } + + it = testRootNode.getNodes(); + pos = 0; + while (it.hasNext()) { + Node n = it.nextNode(); + String name = n.getName(); + assertEquals(name, new String(new char[]{SEQ_AFTER.charAt(pos)})); + if (pos == RELPOS) { + assertTrue(n.isSame(renamedNode)); + } + pos++; + } + } + + public void testRenameEventHandling() throws RepositoryException { + Session s = getHelper().getSuperuserSession(); + ObservationManager mgr = s.getWorkspace().getObservationManager(); + EventResult result = new EventResult(log); + + try { + mgr.addEventListener(result, Event.PERSIST|Event.NODE_ADDED|Event.NODE_MOVED|Event.NODE_REMOVED, testRootNode.getPath(), true, null, null, false); + + NodeIterator it = testRootNode.getNodes(); + + Node n = it.nextNode(); + String name = n.getName(); + + JackrabbitNode node = (JackrabbitNode) n; + node.rename(name.toUpperCase()); + superuser.save(); + + boolean foundMove = false; + for (Event event : result.getEvents(5000)) { + if (Event.NODE_MOVED == event.getType()) { + foundMove = true; + break; + } + } + + if (!foundMove) { + fail("Expected NODE_MOVED event upon renaming a node."); + } + } finally { + mgr.removeEventListener(result); + s.logout(); + } + } + + public void testSetMixins() throws RepositoryException { + // create node with mixin test:AA + Node n = testRootNode.addNode("foo", "nt:folder"); + n.addMixin("test:AA"); + n.setProperty("test:propAA", "AA"); + n.setProperty("test:propA", "A"); + superuser.save(); + + // 'downgrade' from test:AA to test:A + ((JackrabbitNode) n).setMixins(new String[]{"test:A"}); + superuser.save(); + + assertTrue(n.hasProperty("test:propA")); + assertFalse(n.hasProperty("test:propAA")); + + // 'upgrade' from test:A to test:AA + ((JackrabbitNode) n).setMixins(new String[]{"test:AA"}); + n.setProperty("test:propAA", "AA"); + superuser.save(); + + assertTrue(n.hasProperty("test:propA")); + assertTrue(n.hasProperty("test:propAA")); + + // replace test:AA with mix:title + ((JackrabbitNode) n).setMixins(new String[]{"mix:title"}); + n.setProperty("jcr:title", "..."); + n.setProperty("jcr:description", "blah blah"); + superuser.save(); + + assertTrue(n.hasProperty("jcr:title")); + assertTrue(n.hasProperty("jcr:description")); + assertFalse(n.hasProperty("test:propA")); + assertFalse(n.hasProperty("test:propAA")); + + // clean up + n.remove(); + superuser.save(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/JackrabbitObservationManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/JackrabbitObservationManagerTest.java new file mode 100644 index 00000000000..bb5c01cb288 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/JackrabbitObservationManagerTest.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.api; + +import static javax.jcr.observation.Event.NODE_ADDED; + +import java.util.concurrent.ExecutionException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.api.observation.JackrabbitEventFilter; +import org.apache.jackrabbit.api.observation.JackrabbitObservationManager; +import org.apache.jackrabbit.test.api.observation.AbstractObservationTest; +import org.apache.jackrabbit.test.api.observation.EventResult; + +public class JackrabbitObservationManagerTest extends AbstractObservationTest { + + public void testImplementsJackrabbitObservationManager() throws RepositoryException { + assertTrue(obsMgr instanceof JackrabbitObservationManager); + } + + public void testDisjunctPaths() throws ExecutionException, InterruptedException, RepositoryException { + JackrabbitObservationManager oManager = (JackrabbitObservationManager) obsMgr; + EventResult listener = new EventResult(log); + JackrabbitEventFilter filter = new JackrabbitEventFilter() + .setAdditionalPaths('/' + testPath + "/a", '/' + testPath + "/x") + .setEventTypes(NODE_ADDED); + oManager.addEventListener(listener, filter); + try { + Node b = testRootNode.addNode("a").addNode("b"); + b.addNode("c"); + Node y = testRootNode.addNode("x").addNode("y"); + y.addNode("z"); + testRootNode.getSession().save(); + + Event[] added = listener.getEvents(DEFAULT_WAIT_TIMEOUT); + checkNodeAdded(added, new String[] {"a/b", "x/y"}, null); + } finally { + oManager.removeEventListener(listener); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/TestAll.java new file mode 100644 index 00000000000..3cf1e6e6d93 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/TestAll.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.api; + +import junit.framework.Test; +import junit.framework.TestSuite; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * TestAll... + */ +public class TestAll extends AbstractJCRTest { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("api tests"); + + suite.addTestSuite(JackrabbitNodeTest.class); + suite.addTestSuite(JackrabbitObservationManagerTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlManagerTest.java new file mode 100644 index 00000000000..ea8a4935b56 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/JackrabbitAccessControlManagerTest.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.api.security; + +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; + +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.security.Privilege; +import java.util.Collections; +import java.util.Set; +import java.security.Principal; + +/** + * JackrabbitAccessControlManagerTest... + */ +public class JackrabbitAccessControlManagerTest extends AbstractAccessControlTest { + + Set principals; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!(acMgr instanceof JackrabbitAccessControlManager)) { + throw new NotExecutableException(); + } + + Principal principal = EveryonePrincipal.getInstance(); + principals = Collections.singleton(principal); + } + + public void testHasPrivilegeThrowsAccessDenied() throws RepositoryException { + Session readOnly = getHelper().getReadOnlySession(); + JackrabbitAccessControlManager jacMgr = (JackrabbitAccessControlManager) readOnly.getAccessControlManager(); + try { + jacMgr.hasPrivileges(testRoot, principals, new Privilege[] {jacMgr.privilegeFromName(Privilege.JCR_READ)}); + fail("ReadOnly session isn't allowed to determine the privileges of other principals."); + } catch (AccessDeniedException e) { + // success + } finally { + readOnly.logout(); + } + } + + public void testGetPrivilegesThrowsAccessDenied() throws RepositoryException { + Session readOnly = getHelper().getReadOnlySession(); + JackrabbitAccessControlManager jacMgr = (JackrabbitAccessControlManager) readOnly.getAccessControlManager(); + try { + jacMgr.getPrivileges(testRoot, principals); + fail("ReadOnly session isn't allowed to determine the privileges of other principals."); + } catch (AccessDeniedException e) { + // success + } finally { + readOnly.logout(); + } + } + + public void testHasPrivilegesWithInvalidPath() throws RepositoryException { + JackrabbitAccessControlManager jacMgr = (JackrabbitAccessControlManager) acMgr; + String invalidPath = testRoot; + while (superuser.nodeExists(invalidPath)) { + invalidPath += "_"; + } + + try { + jacMgr.hasPrivileges(invalidPath, principals, new Privilege[] {jacMgr.privilegeFromName(Privilege.JCR_READ)}); + fail("Invalid path must be detected"); + } catch (PathNotFoundException e) { + // success + } + } + + public void testGetPrivilegesWithInvalidPath() throws RepositoryException { + JackrabbitAccessControlManager jacMgr = (JackrabbitAccessControlManager) acMgr; + String invalidPath = testRoot; + while (superuser.nodeExists(invalidPath)) { + invalidPath += "_"; + } + + try { + jacMgr.getPrivileges(invalidPath, principals); + fail("Invalid path must be detected"); + } catch (PathNotFoundException e) { + // success + } + } + + // TODO add more tests +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/TestAll.java new file mode 100644 index 00000000000..2318926ff31 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/TestAll.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.api.security; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * TestAll... + */ +public class TestAll extends AbstractJCRTest { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("api.security tests"); + + suite.addTestSuite(JackrabbitAccessControlManagerTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/authorization/PrivilegeManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/authorization/PrivilegeManagerTest.java new file mode 100644 index 00000000000..b1a5e19cd6e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/authorization/PrivilegeManagerTest.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.authorization; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * PrivilegeManagerTest... + */ +public class PrivilegeManagerTest extends AbstractJCRTest { + + private NameResolver resolver; + protected PrivilegeManager privilegeMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + resolver = (SessionImpl) superuser; + privilegeMgr = ((JackrabbitWorkspace) superuser.getWorkspace()).getPrivilegeManager(); + } + + protected void assertSamePrivilegeName(String expected, String present) throws NamespaceException, IllegalNameException { + assertEquals("Privilege names are not the same", resolver.getQName(expected), resolver.getQName(present)); + } + + public void testRegisteredPrivileges() throws RepositoryException { + Privilege[] ps = privilegeMgr.getRegisteredPrivileges(); + + List l = new ArrayList(Arrays.asList(ps)); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_READ))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_ADD_CHILD_NODES))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_REMOVE_CHILD_NODES))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_MODIFY_PROPERTIES))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_REMOVE_NODE))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_READ_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_MODIFY_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_WRITE))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_ALL))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_LIFECYCLE_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_LOCK_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_NODE_TYPE_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_RETENTION_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(PrivilegeRegistry.REP_WRITE))); + // including repo-level operation privileges + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT))); + + assertTrue(l.isEmpty()); + } + + public void testAllPrivilege() throws RepositoryException { + Privilege p = privilegeMgr.getPrivilege(Privilege.JCR_ALL); + assertSamePrivilegeName(p.getName(), Privilege.JCR_ALL); + assertTrue(p.isAggregate()); + assertFalse(p.isAbstract()); + + List l = new ArrayList(Arrays.asList(p.getAggregatePrivileges())); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_READ))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_ADD_CHILD_NODES))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_REMOVE_CHILD_NODES))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_MODIFY_PROPERTIES))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_REMOVE_NODE))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_READ_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_MODIFY_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_LIFECYCLE_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_LOCK_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_NODE_TYPE_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_RETENTION_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_WRITE))); + assertTrue(l.remove(privilegeMgr.getPrivilege(PrivilegeRegistry.REP_WRITE))); + // including repo-level operation privileges + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT))); + + assertTrue(l.isEmpty()); + + l = new ArrayList(Arrays.asList(p.getDeclaredAggregatePrivileges())); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_READ))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_WRITE))); + assertTrue(l.remove(privilegeMgr.getPrivilege(PrivilegeRegistry.REP_WRITE))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_READ_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_MODIFY_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_LIFECYCLE_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_LOCK_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_RETENTION_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT))); + assertTrue(l.remove(privilegeMgr.getPrivilege(Privilege.JCR_NODE_TYPE_MANAGEMENT))); + // including repo-level operation privileges + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeMgr.getPrivilege(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT))); + + assertTrue(l.isEmpty()); + } + + public void testGetPrivilegeFromName() throws AccessControlException, RepositoryException { + Privilege p = privilegeMgr.getPrivilege(Privilege.JCR_READ); + + assertTrue(p != null); + assertSamePrivilegeName(Privilege.JCR_READ, p.getName()); + assertFalse(p.isAggregate()); + + p = privilegeMgr.getPrivilege(Privilege.JCR_WRITE); + + assertTrue(p != null); + assertSamePrivilegeName(p.getName(), Privilege.JCR_WRITE); + assertTrue(p.isAggregate()); + } + + public void testGetPrivilegesFromInvalidName() throws RepositoryException { + try { + privilegeMgr.getPrivilege("unknown"); + fail("invalid privilege name"); + } catch (AccessControlException e) { + // OK + } + } + + public void testGetPrivilegesFromEmptyNames() { + try { + privilegeMgr.getPrivilege(""); + fail("invalid privilege name array"); + } catch (AccessControlException e) { + // OK + } catch (RepositoryException e) { + // OK + } + } + + public void testGetPrivilegesFromNullNames() { + try { + privilegeMgr.getPrivilege(null); + fail("invalid privilege name (null)"); + } catch (Exception e) { + // OK + } + } + + public void testRegisterPrivilegeWithIllegalName() throws RepositoryException { + Map illegal = new HashMap(); + illegal.put("invalid:privilegeName", new String[0]); + illegal.put("jcr:newPrivilege", new String[] {"invalid:privilegeName"}); + illegal.put(".e:privilegeName", new String[0]); + illegal.put("jcr:newPrivilege", new String[] {".e:privilegeName"}); + + for (String illegalName : illegal.keySet()) { + try { + privilegeMgr.registerPrivilege(illegalName, true, illegal.get(illegalName)); + fail("Illegal name -> Exception expected"); + } catch (NamespaceException e) { + // success + } catch (IllegalNameException e) { + // success + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/authorization/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/authorization/TestAll.java new file mode 100644 index 00000000000..d23bcf5299b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/authorization/TestAll.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.api.security.authorization; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import org.apache.jackrabbit.api.security.principal.PrincipalManagerTest; + +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("api.security.principal tests"); + + suite.addTestSuite(PrivilegeManagerTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/principal/PrincipalManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/principal/PrincipalManagerTest.java new file mode 100644 index 00000000000..ff18b338198 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/principal/PrincipalManagerTest.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.principal; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.security.Principal; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +/** + * PrincipalManagerTest... + */ +public class PrincipalManagerTest extends AbstractJCRTest { + + private PrincipalManager principalMgr; + private GroupPrincipal everyone; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!(superuser instanceof JackrabbitSession)) { + superuser.logout(); + throw new NotExecutableException(); + } + principalMgr = ((JackrabbitSession) superuser).getPrincipalManager(); + everyone = (GroupPrincipal) principalMgr.getEveryone(); + } + + private static Principal[] getPrincipals(Session session) { + // TODO: get rid of SessionImpl dependency + Set princ = ((SessionImpl) session).getSubject().getPrincipals(); + return princ.toArray(new Principal[princ.size()]); + } + + private static boolean isGroup(Principal p) { + return p instanceof GroupPrincipal; + } + + public void testGetEveryone() { + Principal principal = principalMgr.getEveryone(); + assertTrue(principal != null); + assertTrue(isGroup(principal)); + } + + public void testSuperUserIsEveryOne() { + Principal[] pcpls = getPrincipals(superuser); + for (Principal pcpl : pcpls) { + if (!(pcpl.equals(everyone))) { + assertTrue(everyone.isMember(pcpl)); + } + } + } + + public void testReadOnlyIsEveryOne() throws RepositoryException { + Session s = getHelper().getReadOnlySession(); + try { + Principal[] pcpls = getPrincipals(s); + for (Principal pcpl : pcpls) { + if (!(pcpl.equals(everyone))) { + assertTrue(everyone.isMember(pcpl)); + } + } + } finally { + s.logout(); + } + } + + public void testHasPrincipal() { + assertTrue(principalMgr.hasPrincipal(everyone.getName())); + + Principal[] pcpls = getPrincipals(superuser); + for (Principal pcpl : pcpls) { + assertTrue(principalMgr.hasPrincipal(pcpl.getName())); + } + } + + public void testGetPrincipal() { + Principal p = principalMgr.getPrincipal(everyone.getName()); + assertEquals(everyone, p); + + Principal[] pcpls = getPrincipals(superuser); + for (Principal pcpl : pcpls) { + Principal pp = principalMgr.getPrincipal(pcpl.getName()); + assertEquals("PrincipalManager.getPrincipal returned Principal with different Name", pcpl.getName(), pp.getName()); + } + } + + public void testGetPrincipalGetName() { + Principal[] pcpls = getPrincipals(superuser); + for (Principal pcpl : pcpls) { + Principal pp = principalMgr.getPrincipal(pcpl.getName()); + assertEquals("PrincipalManager.getPrincipal returned Principal with different Name", pcpl.getName(), pp.getName()); + } + } + + public void testGetPrincipals() { + PrincipalIterator it = principalMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_NOT_GROUP); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + assertFalse(isGroup(p)); + } + } + + public void testGetGroupPrincipals() { + PrincipalIterator it = principalMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_GROUP); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + assertTrue(isGroup(p)); + } + } + + 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); + } + } + + 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 = ((GroupPrincipal) p).members(); + while (en.hasMoreElements()) { + Principal memb = en.nextElement(); + assertTrue(principalMgr.hasPrincipal(memb.getName())); + } + } + } + } + + 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); + + } + } + } + + 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 = ((GroupPrincipal) 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()); + } + } + } + + public void testFindPrincipal() { + Principal[] pcpls = getPrincipals(superuser); + for (Principal pcpl : pcpls) { + 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()); + } + } + + public void testFindPrincipalByType() { + Principal[] pcpls = getPrincipals(superuser); + for (Principal pcpl : pcpls) { + 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()); + } + } + } + + public void testFindPrincipalByTypeAll() { + Principal[] pcpls = getPrincipals(superuser); + for (Principal pcpl : pcpls) { + 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()); + } + } + + 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/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/principal/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/principal/TestAll.java new file mode 100644 index 00000000000..4ae4eec90ba --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/principal/TestAll.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.api.security.principal; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("api.security.principal tests"); + + suite.addTestSuite(PrincipalManagerTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/AbstractUserTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/AbstractUserTest.java new file mode 100644 index 00000000000..e3196dc205b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/AbstractUserTest.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.user; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.TestPrincipal; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.security.auth.Subject; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.util.Collections; +import java.util.Set; +import java.util.UUID; + +/** + * AbstractUserTest... + */ +public abstract class AbstractUserTest extends AbstractJCRTest { + + protected UserManager userMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + try { + userMgr = getUserManager(superuser); + } catch (Exception e) { + superuser.logout(); + throw e; + } + } + + /** + * Conditional save if the userManager expects an extra save() call. + * NOTE: only works if changes are made with UserManager affect the passed + * session object (i.e. if the Session hold within the UserManager is + * the same as the passes Session s. + * + * @param s + * @throws RepositoryException + */ + protected static void save(Session s) throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(s); + if (!umgr.isAutoSave() && s.hasPendingChanges()) { + s.save(); + } + } + + 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 Principal getTestPrincipal() throws RepositoryException { + String pn = "any_principal" + UUID.randomUUID(); + return getTestPrincipal(pn); + } + + protected Principal getTestPrincipal(String name) throws RepositoryException { + return new TestPrincipal(name); + } + + protected String buildPassword(String uid, boolean createDigest) throws IllegalArgumentException { + if (createDigest) { + try { + StringBuilder password = new StringBuilder(); + password.append("{").append(SecurityConstants.DEFAULT_DIGEST).append("}"); + password.append(Text.digest(SecurityConstants.DEFAULT_DIGEST, uid.getBytes(StandardCharsets.UTF_8))); + return password.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e.toString()); + } + } else { + return uid; + } + } + + protected String buildPassword(String uid) { + return buildPassword(uid, false); + } + + protected String buildPassword(Principal p) { + return buildPassword(p.getName(), false); + } + + protected Credentials buildCredentials(String uID, String pw) { + return new SimpleCredentials(uID, pw.toCharArray()); + } + + protected static Set getPrincipalSetFromSession(Session session) throws NotExecutableException { + if (session instanceof SessionImpl) { + return ((SessionImpl) session).getSubject().getPrincipals(); + } else { + // TODO add fallback + throw new NotExecutableException(); + } + } + + 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."); + } + + protected Group getTestGroup(Session session) throws NotExecutableException, RepositoryException { + for (Principal principal : getPrincipalSetFromSession(session)) { + Authorizable auth = getUserManager(session).getAuthorizable(principal); + if (auth != null && auth.isGroup()) { + return (Group) auth; + } + } + // may happen -> don't throw RepositoryException + throw new NotExecutableException("No Group found."); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/AuthorizableTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/AuthorizableTest.java new file mode 100644 index 00000000000..1b8174c79e9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/AuthorizableTest.java @@ -0,0 +1,454 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.user; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * UserTest... + */ +public class AuthorizableTest extends AbstractUserTest { + + public void testGetId() throws NotExecutableException, RepositoryException { + User user = getTestUser(superuser); + assertNotNull(user.getID()); + } + + public void testGroupGetId() throws NotExecutableException, RepositoryException { + Group gr = getTestGroup(superuser); + assertNotNull(gr.getID()); + } + + public void testGetPrincipalNotNull() throws RepositoryException, NotExecutableException { + User user = getTestUser(superuser); + assertNotNull(user.getPrincipal()); + } + + public void testGroupGetPrincipalNotNull() throws RepositoryException, NotExecutableException { + Group gr = getTestGroup(superuser); + assertNotNull(gr.getPrincipal()); + } + + public void testSetProperty() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + // TODO: retrieve propname and value from config + String propName = "Fullname"; + Value v = superuser.getValueFactory().createValue("Super User"); + try { + auth.setProperty(propName, v); + save(superuser); + } 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)); + + save(superuser); + } finally { + // try to remove the property again even if previous calls failed. + auth.removeProperty(propName); + save(superuser); + } + } + + public void testSetMultiValueProperty() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + // TODO: retrieve propname and values from config + String propName = "Fullname"; + Value[] v = new Value[] {superuser.getValueFactory().createValue("Super User")}; + try { + auth.setProperty(propName, v); + save(superuser); + } 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)); + + save(superuser); + } finally { + // try to remove the property again even if previous calls failed. + auth.removeProperty(propName); + save(superuser); + } + } + + 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); + save(superuser); + + 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); + save(superuser); + } + } + } + + 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."); + } catch (Exception e) { + // success. + } + } + } + + 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 + } + } + } + + 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 + } + } + } + + public void testGetPropertyNames() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + // TODO: retrieve propname and value from config + String propName = "Fullname"; + Value v = superuser.getValueFactory().createValue("Super User"); + try { + auth.setProperty(propName, v); + save(superuser); + } 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); + save(superuser); + } + } + + public void testGetPropertyNamesByRelPath() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + // TODO: retrieve propname and value from config + String relPath = "testing/Fullname"; + Value v = superuser.getValueFactory().createValue("Super User"); + try { + auth.setProperty(relPath, v); + save(superuser); + } 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); + save(superuser); + } + } + + 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 + } + } + } + + 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)); + } + + 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)); + save(superuser); + } + + public void testMemberOf() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + Iterator it = auth.memberOf(); + while (it.hasNext()) { + Object group = it.next(); + assertTrue(group instanceof Group); + } + } + + public void testDeclaredMemberOf() throws NotExecutableException, RepositoryException { + Authorizable auth = getTestUser(superuser); + + Iterator it = auth.declaredMemberOf(); + while (it.hasNext()) { + Object group = it.next(); + assertTrue(group instanceof Group); + } + } + + /** + * 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()); + save(superuser); + newUserId = newUser.getID(); + + newGroup = userMgr.createGroup(getTestPrincipal()); + newGroup.addMember(newUser); + save(superuser); + + // remove the new user that is still listed as member. + newUser.remove(); + save(superuser); + } 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(); + } + save(superuser); + } + } + + public void testRecreateUser() throws RepositoryException, NotExecutableException { + String id = "bla"; + Authorizable auth = userMgr.getAuthorizable(id); + if (auth == null) { + auth = userMgr.createUser(id, id); + } + auth.remove(); + save(superuser); + + assertNull(userMgr.getAuthorizable(id)); + + // recreate the user using another session. + Session s2 = getHelper().getSuperuserSession(); + User u2 = null; + try { + UserManager umgr = ((JackrabbitSession) s2).getUserManager(); + assertNull(umgr.getAuthorizable(id)); + + // recreation must succeed + u2 = umgr.createUser(id, id); + + // must be present with both session. + assertNotNull(umgr.getAuthorizable(id)); + assertNotNull(userMgr.getAuthorizable(id)); + + } finally { + if (u2 != null) { + u2.remove(); + save(s2); + } + s2.logout(); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/ConcurrentCreateUserTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/ConcurrentCreateUserTest.java new file mode 100644 index 00000000000..ae0249dda8d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/ConcurrentCreateUserTest.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.api.security.user; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.core.AbstractConcurrencyTest; +import org.apache.jackrabbit.core.security.TestPrincipal; + +public class ConcurrentCreateUserTest extends AbstractConcurrencyTest { + + /** + * The number of threads. + */ + private static final int CONCURRENCY = 5; + + /** + * Number of tries per user. + */ + private static final int RETRIES = 3; + + List userIDs = Collections + .synchronizedList(new ArrayList()); + public static final String INTERMEDIATE_PATH = UUID.randomUUID().toString(); + + @Override + protected void tearDown() throws Exception { + + try { + for (String id : userIDs) { + Authorizable a = ((JackrabbitSession) superuser) + .getUserManager().getAuthorizable(id); + a.remove(); + superuser.save(); + } + } catch (Exception e) { + // this is best effort + } + + super.tearDown(); + } + + public void testCreateUsers() throws Exception { + + log.println("ConcurrentCreateUserTest.testCreateUsers: c=" + + CONCURRENCY); + log.flush(); + + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + JackrabbitSession s = null; + try { + s = (JackrabbitSession) getHelper().getSuperuserSession(); + + String name = "newname-" + UUID.randomUUID(); + Authorizable authorizable = null; + int maxtries = RETRIES; + RepositoryException lastex = null; + + while (authorizable == null && maxtries > 0) { + try { + maxtries -= 1; + authorizable = s.getUserManager().createUser(name, + "password1", new TestPrincipal(name), + INTERMEDIATE_PATH); + s.save(); + } catch (InvalidItemStateException ex) { + lastex = ex; + log.println("got " + ex + ", retrying"); + } + } + if (authorizable == null) { + throw new RepositoryException("user " + name + + " not created in " + RETRIES + " attempts.", + lastex); + } + userIDs.add(authorizable.getID()); + log.println(authorizable + " created"); + } finally { + s.logout(); + } + } + }, CONCURRENCY, "/" + testPath); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/GroupTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/GroupTest.java new file mode 100644 index 00000000000..7831aa4cde2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/GroupTest.java @@ -0,0 +1,644 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.user; + +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * GroupTest... + */ +public class GroupTest extends AbstractUserTest { + + 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); + } + + public void testIsGroup() throws NotExecutableException, RepositoryException { + Group gr = getTestGroup(superuser); + assertTrue(gr.isGroup()); + } + + public void testGetDeclaredMembers() throws NotExecutableException, RepositoryException { + Group gr = getTestGroup(superuser); + Iterator it = gr.getDeclaredMembers(); + assertNotNull(it); + while (it.hasNext()) { + assertTrue(it.next() != null); + } + } + + public void testGetMembers() throws NotExecutableException, RepositoryException { + Group gr = getTestGroup(superuser); + Iterator it = gr.getMembers(); + assertNotNull(it); + while (it.hasNext()) { + assertTrue(it.next() != null); + } + } + + public void testGetMembersAgainstIsMember() throws NotExecutableException, RepositoryException { + Group gr = getTestGroup(superuser); + + Iterator it = gr.getMembers(); + while (it.hasNext()) { + Authorizable auth = it.next(); + assertTrue(gr.isMember(auth)); + } + } + + public void testGetMembersAgainstMemberOf() throws NotExecutableException, RepositoryException { + Group gr = getTestGroup(superuser); + + Iterator it = gr.getMembers(); + while (it.hasNext()) { + Authorizable auth = it.next(); + assertTrueMemberOfContainsGroup(auth.memberOf(), gr); + } + } + + public void testGetDeclaredMembersAgainstDeclaredMemberOf() throws NotExecutableException, RepositoryException { + Group gr = getTestGroup(superuser); + + Iterator it = gr.getDeclaredMembers(); + while (it.hasNext()) { + Authorizable auth = it.next(); + assertTrueMemberOfContainsGroup(auth.declaredMemberOf(), gr); + } + } + + public void testGetMembersContainsDeclaredMembers() throws NotExecutableException, RepositoryException { + Group gr = getTestGroup(superuser); + List l = new ArrayList(); + for (Iterator it = gr.getMembers(); it.hasNext();) { + l.add(it.next().getID()); + } + for (Iterator it = gr.getDeclaredMembers(); it.hasNext();) { + assertTrue("All declared members must also be part of the Iterator " + + "returned upon getMembers()",l.contains(it.next().getID())); + } + } + + public void testAddMember() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + assertFalse(newGroup.isMember(auth)); + assertFalse(newGroup.removeMember(auth)); + save(superuser); + + assertTrue(newGroup.addMember(auth)); + save(superuser); + assertTrue(newGroup.isMember(auth)); + assertTrue(newGroup.isMember(userMgr.getAuthorizable(auth.getID()))); + + } finally { + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + save(superuser); + } + } + } + + 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(getTestPrincipal()); + save(superuser); + + for (int k = 0; k < size; k++) { + users.add(userMgr.createUser("user_" + k, "pass_" + k)); + } + save(superuser); + + for (User user : users) { + assertTrue(newGroup.addMember(user)); + } + save(superuser); + + for (User user : users) { + assertTrue(newGroup.isMember(user)); + } + + for (User user : users) { + assertTrue(newGroup.removeMember(user)); + } + save(superuser); + + for (User user : users) { + assertFalse(newGroup.isMember(user)); + } + } finally { + for (User user : users) { + user.remove(); + save(superuser); + } + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + save(superuser); + } + } + } + + public void testAddRemoveMember() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup1 = null; + Group newGroup2 = null; + try { + newGroup1 = userMgr.createGroup(getTestPrincipal()); + newGroup2 = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + assertFalse(newGroup1.isMember(auth)); + assertFalse(newGroup1.removeMember(auth)); + save(superuser); + assertFalse(newGroup2.isMember(auth)); + assertFalse(newGroup2.removeMember(auth)); + save(superuser); + + assertTrue(newGroup1.addMember(auth)); + save(superuser); + assertTrue(newGroup1.isMember(auth)); + assertTrue(newGroup1.isMember(userMgr.getAuthorizable(auth.getID()))); + + assertTrue(newGroup2.addMember(auth)); + save(superuser); + assertTrue(newGroup2.isMember(auth)); + assertTrue(newGroup2.isMember(userMgr.getAuthorizable(auth.getID()))); + + assertTrue(newGroup1.removeMember(auth)); + save(superuser); + assertTrue(newGroup2.removeMember(auth)); + save(superuser); + + assertTrue(newGroup1.addMember(auth)); + save(superuser); + assertTrue(newGroup1.isMember(auth)); + assertTrue(newGroup1.isMember(userMgr.getAuthorizable(auth.getID()))); + assertTrue(newGroup1.removeMember(auth)); + save(superuser); + + + } finally { + if (newGroup1 != null) { + newGroup1.removeMember(auth); + newGroup1.remove(); + save(superuser); + } + if (newGroup2 != null) { + newGroup2.removeMember(auth); + newGroup2.remove(); + save(superuser); + } + } + } + + public void testIsDeclaredMember() throws RepositoryException, NotExecutableException { + User auth = getTestUser(superuser); + Group newGroup1 = null; + Group newGroup2 = null; + try { + newGroup1 = userMgr.createGroup(getTestPrincipal()); + newGroup2 = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + assertFalse(newGroup1.isDeclaredMember(auth)); + assertFalse(newGroup2.isDeclaredMember(auth)); + + assertTrue(newGroup2.addMember(auth)); + save(superuser); + assertTrue(newGroup2.isDeclaredMember(auth)); + assertTrue(newGroup2.isDeclaredMember(userMgr.getAuthorizable(auth.getID()))); + + assertTrue(newGroup1.addMember(newGroup2)); + save(superuser); + 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(); + save(superuser); + } + if (newGroup2 != null) { + newGroup2.remove(); + save(superuser); + } + } + } + + public void testAddMemberTwice() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + assertTrue(newGroup.addMember(auth)); + save(superuser); + assertFalse(newGroup.addMember(auth)); + save(superuser); + assertTrue(newGroup.isMember(auth)); + + } finally { + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + save(superuser); + } + } + } + + public void testAddMemberModifiesMemberOf() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + assertFalseMemberOfContainsGroup(auth.memberOf(), newGroup); + assertTrue(newGroup.addMember(auth)); + save(superuser); + + assertTrueMemberOfContainsGroup(auth.declaredMemberOf(), newGroup); + assertTrueMemberOfContainsGroup(auth.memberOf(), newGroup); + } finally { + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + save(superuser); + } + } + } + + public void testAddMemberModifiesGetMembers() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + assertFalseIsMember(newGroup.getMembers(), auth); + assertFalseIsMember(newGroup.getDeclaredMembers(), auth); + assertTrue(newGroup.addMember(auth)); + save(superuser); + + assertTrueIsMember(newGroup.getMembers(), auth); + assertTrueIsMember(newGroup.getDeclaredMembers(), auth); + } finally { + if (newGroup != null) { + newGroup.removeMember(auth); + newGroup.remove(); + save(superuser); + } + } + } + + public void testIndirectMembers() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + Group newGroup2 = null; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + newGroup2 = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + newGroup.addMember(newGroup2); + save(superuser); + assertTrue(newGroup.isMember(newGroup2)); + + newGroup2.addMember(auth); + save(superuser); + + // testuser must not be declared member of 'newGroup' + assertFalseIsMember(newGroup.getDeclaredMembers(), auth); + assertFalseMemberOfContainsGroup(auth.declaredMemberOf(), newGroup); + + // testuser must however be member of 'newGroup' (indirect). + assertTrueIsMember(newGroup.getMembers(), auth); + assertTrueMemberOfContainsGroup(auth.memberOf(), newGroup); + + // testuser cannot be removed from 'newGroup' + assertFalse(newGroup.removeMember(auth)); + save(superuser); + } finally { + if (newGroup != null) { + newGroup.removeMember(newGroup2); + newGroup.remove(); + save(superuser); + } + if (newGroup2 != null) { + newGroup2.removeMember(auth); + newGroup2.remove(); + save(superuser); + } + } + } + + public void testMembersInPrincipal() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + Group newGroup2 = null; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + newGroup2 = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + newGroup.addMember(newGroup2); + save(superuser); + newGroup2.addMember(auth); + save(superuser); + + GroupPrincipal ngPrincipal = (GroupPrincipal) newGroup.getPrincipal(); + GroupPrincipal ng2Principal = (GroupPrincipal) 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(); + save(superuser); + } + if (newGroup2 != null) { + newGroup2.removeMember(auth); + newGroup2.remove(); + save(superuser); + } + } + } + + public void testDeeplyNestedGroups() throws NotExecutableException, RepositoryException { + Set groups = new HashSet(); + try { + User auth = getTestUser(superuser); + Group topGroup = userMgr.createGroup(getTestPrincipal()); + + // 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(getTestPrincipal()); + 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(); + } + } + } + + public void testInheritedMembers() throws Exception { + Set authorizables = new HashSet(); + try { + User testUser = userMgr.createUser(getTestPrincipal().getName(), "pw"); + authorizables.add(testUser); + Group group1 = userMgr.createGroup(getTestPrincipal()); + authorizables.add(group1); + Group group2 = userMgr.createGroup(getTestPrincipal()); + authorizables.add(group2); + Group group3 = userMgr.createGroup(getTestPrincipal()); + + 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(); + } + } + } + + public void testCyclicGroups() throws AuthorizableExistsException, RepositoryException, NotExecutableException { + Group group1 = null; + Group group2 = null; + Group group3 = null; + try { + group1 = userMgr.createGroup(getTestPrincipal()); + group2 = userMgr.createGroup(getTestPrincipal()); + group3 = userMgr.createGroup(getTestPrincipal()); + + 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(); + } + } + + public void testRemoveMemberTwice() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + assertTrue(newGroup.addMember(auth)); + save(superuser); + assertTrue(newGroup.removeMember(userMgr.getAuthorizable(auth.getID()))); + save(superuser); + assertFalse(newGroup.removeMember(auth)); + save(superuser); + } finally { + if (newGroup != null) { + newGroup.remove(); + save(superuser); + } + } + } + + public void testAddItselfAsMember() throws RepositoryException, NotExecutableException { + Group newGroup = null; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + assertFalse(newGroup.addMember(newGroup)); + save(superuser); + newGroup.removeMember(newGroup); + save(superuser); + } finally { + if (newGroup != null) { + newGroup.remove(); + save(superuser); + } + } + } + + /** + * Removing a GroupImpl must be possible even if there are still existing + * members present. + * + * @throws RepositoryException if an error occurs + * @throws NotExecutableException if not executable + */ + public void testRemoveGroupIfMemberExist() throws RepositoryException, NotExecutableException { + User auth = getTestUser(superuser); + String newGroupId = null; + + try { + Group newGroup = userMgr.createGroup(getTestPrincipal()); + save(superuser); + newGroupId = newGroup.getID(); + + assertTrue(newGroup.addMember(auth)); + newGroup.remove(); + save(superuser); + } finally { + Group gr = (Group) userMgr.getAuthorizable(newGroupId); + if (gr != null) { + gr.removeMember(auth); + gr.remove(); + save(superuser); + } + } + } + + public void testRemoveGroupClearsMembership() throws NotExecutableException, RepositoryException { + User auth = getTestUser(superuser); + Group newGroup = null; + String groupId; + try { + newGroup = userMgr.createGroup(getTestPrincipal()); + groupId = newGroup.getID(); + save(superuser); + + assertTrue(newGroup.addMember(auth)); + save(superuser); + + 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(); + save(superuser); + } + } + + 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())); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/ImpersonationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/ImpersonationTest.java new file mode 100644 index 00000000000..c31c26a6c23 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/ImpersonationTest.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.api.security.user; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; +import java.security.Principal; +import java.util.Collections; +import java.util.Set; + +/** + * ImpersonationTest... + */ +public class ImpersonationTest extends AbstractUserTest { + + private static Logger log = LoggerFactory.getLogger(ImpersonationTest.class); + + private User newUser; + private Impersonation impersonation; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Principal test = getTestPrincipal(); + String pw = buildPassword(test); + newUser = userMgr.createUser(test.getName(), pw); + save(superuser); + + impersonation = newUser.getImpersonation(); + } + + @Override + protected void tearDown() throws Exception { + newUser.remove(); + save(superuser); + super.tearDown(); + } + + public void testUnknownCannotImpersonate() throws RepositoryException { + Principal test = getTestPrincipal(); + Subject subject = createSubject(test); + assertFalse("An unknown principal should not be allowed to impersonate.", impersonation.allows(subject)); + } + + public void testGrantImpersonationUnknownUser() throws RepositoryException, NotExecutableException { + Principal test = getTestPrincipal(); + try { + assertFalse("Granting impersonation to an unknown principal should not be successful.", impersonation.grantImpersonation(test)); + } finally { + impersonation.revokeImpersonation(test); + save(superuser); + } + } + + public void testImpersonateGroup() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + Principal group = getTestGroup(s).getPrincipal(); + Subject subject = createSubject(group); + assertFalse("An group principal should not be allowed to impersonate.", impersonation.allows(subject)); + } finally { + s.logout(); + } + } + + public void testGrantImpersonationToGroupPrincipal() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + Principal group = getTestGroup(s).getPrincipal(); + try { + assertFalse("Granting impersonation to a Group should not be successful.", impersonation.grantImpersonation(group)); + } finally { + impersonation.revokeImpersonation(group); + save(superuser); + } + } finally { + s.logout(); + } + } + + public void testGrantImpersonation() throws RepositoryException, NotExecutableException { + User u = null; + Principal test = getTestPrincipal(); + try { + u = userMgr.createUser(test.getName(), buildPassword(test)); + save(superuser); + assertTrue("Admin should be allowed to edit impersonation and grant to another test-user.", impersonation.grantImpersonation(test)); + } finally { + impersonation.revokeImpersonation(test); + if (u != null) { + u.remove(); + } + save(superuser); + } + } + + public void testGrantImpersonationTwice() throws RepositoryException, NotExecutableException { + Principal test = getTestPrincipal(); + User u = null; + try { + u = userMgr.createUser(test.getName(), buildPassword(test)); + save(superuser); + impersonation.grantImpersonation(test); + save(superuser); + // try again + assertFalse("Granting impersonation twice should not succeed.", impersonation.grantImpersonation(test)); + } finally { + impersonation.revokeImpersonation(test); + if (u != null) { + u.remove(); + } + save(superuser); + } + } + + public void testRevokeImpersonation() throws RepositoryException, NotExecutableException { + User u = null; + Principal test = getTestPrincipal(); + try { + u = userMgr.createUser(test.getName(), buildPassword(test)); + save(superuser); + impersonation.grantImpersonation(test); + save(superuser); + + assertTrue(impersonation.revokeImpersonation(test)); + } finally { + if (u != null) { + u.remove(); + } + } + } + + public void testRevokeImpersonationTwice() throws RepositoryException, NotExecutableException { + User u = null; + Principal test = getTestPrincipal(); + try { + u = userMgr.createUser(test.getName(), buildPassword(test)); + save(superuser); + impersonation.grantImpersonation(test); + save(superuser); + impersonation.revokeImpersonation(test); + save(superuser); + // try again + assertFalse("Revoking impersonation twice should not succeed.", impersonation.revokeImpersonation(test)); + } finally { + if (u != null) { + u.remove(); + } + } + } + + public void testAdministratorCanImpersonate() throws RepositoryException, NotExecutableException { + User admin = getTestUser(superuser); + Subject subject = createSubject(admin); + assertTrue(impersonation.allows(subject)); + } + + public void testCannotGrantImpersonationForAdministrator() throws RepositoryException, NotExecutableException { + User admin = getTestUser(superuser); + try { + assertFalse(impersonation.grantImpersonation(admin.getPrincipal())); + } finally { + impersonation.revokeImpersonation(admin.getPrincipal()); + } + } + + public void testCannotRevokeImpersonationForAdministrator() throws RepositoryException, NotExecutableException { + User admin = getTestUser(superuser); + assertFalse(impersonation.revokeImpersonation(admin.getPrincipal())); + } + + public void testImpersonatingOneself() throws RepositoryException { + Subject subject = createSubject(newUser); + assertFalse(impersonation.allows(subject)); + } + + /** + * @see JCR-2931 + */ + public void testAdminImpersonatingOneself() throws RepositoryException, NotExecutableException { + User admin = getTestUser(superuser); + Subject subject = createSubject(admin); + assertTrue(admin.getImpersonation().allows(subject)); + } + + public void testGrantImpersonatingForOneself() throws RepositoryException { + Principal main = newUser.getPrincipal(); + try { + assertFalse(impersonation.grantImpersonation(main)); + } finally { + impersonation.revokeImpersonation(main); + } + } + + public void testRevokeImpersonatingForOneself() throws RepositoryException { + Principal main = newUser.getPrincipal(); + assertFalse(impersonation.revokeImpersonation(main)); + } + + private Subject createSubject(User u) throws RepositoryException { + Principal main = u.getPrincipal(); + return createSubject(main); + } + + private Subject createSubject(Principal p) throws RepositoryException { + Set creds = Collections.singleton(buildCredentials(p.getName(), buildPassword(p))); + return new Subject(true, Collections.singleton(p), creds, creds); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/NestedGroupTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/NestedGroupTest.java new file mode 100644 index 00000000000..dc84eed2d97 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/NestedGroupTest.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.user; + +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.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import java.security.Principal; + +/** + * NestedGroupTest... + */ +public class NestedGroupTest extends AbstractUserTest { + + private static Logger log = LoggerFactory.getLogger(NestedGroupTest.class); + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + private Group createGroup(Principal p) throws RepositoryException, NotExecutableException { + Group gr = userMgr.createGroup(p); + save(superuser); + return gr; + } + + private void removeGroup(Group gr) throws RepositoryException, NotExecutableException { + gr.remove(); + save(superuser); + } + + private boolean addMember(Group gr, Authorizable member) throws RepositoryException, NotExecutableException { + boolean added = gr.addMember(member); + save(superuser); + return added; + } + + private boolean removeMember(Group gr, Authorizable member) throws RepositoryException, NotExecutableException { + boolean removed = gr.removeMember(member); + save(superuser); + return removed; + } + + 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); + } + } + } + + 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); + } + } + + 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); + + } + } + + 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/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/TestAll.java new file mode 100644 index 00000000000..951389869c6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/TestAll.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.api.security.user; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("api.security.user tests"); + + suite.addTestSuite(UserManagerTest.class); + suite.addTestSuite(UserManagerCreateGroupTest.class); + suite.addTestSuite(UserManagerCreateUserTest.class); + + suite.addTestSuite(AuthorizableTest.class); + suite.addTestSuite(UserTest.class); + suite.addTestSuite(GroupTest.class); + suite.addTestSuite(NestedGroupTest.class); + suite.addTestSuite(ImpersonationTest.class); + suite.addTestSuite(UserManagerSearchTest.class); + suite.addTestSuite(ConcurrentCreateUserTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerCreateGroupTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerCreateGroupTest.java new file mode 100644 index 00000000000..15a093a7cb2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerCreateGroupTest.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.api.security.user; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +/** + * UserManagerCreateGroupTest... + */ +public class UserManagerCreateGroupTest extends AbstractUserTest { + + private static Logger log = LoggerFactory.getLogger(UserManagerCreateGroupTest.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, NotExecutableException { + Group gr = userMgr.createGroup(p); + save(superuser); + return gr; + } + + private Group createGroup(Principal p, String iPath) throws RepositoryException, NotExecutableException { + Group gr = userMgr.createGroup(p, iPath); + save(superuser); + return gr; + } + + 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()); + } + + 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()); + } + + 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 + } + } + + 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. + } + } + + public void testAutoSave() throws RepositoryException { + boolean autosave = userMgr.isAutoSave(); + if (autosave) { + try { + userMgr.autoSave(false); + autosave = false; + } catch (RepositoryException e) { + // cannot change autosave behavior + // ignore -> test will behave differently. + } + } + + Principal p = getTestPrincipal(); + Group gr = userMgr.createGroup(p); + String id = gr.getID(); + superuser.refresh(false); + + if (!autosave) { + // transient changes must be gone after the refresh-call. + assertNull(userMgr.getAuthorizable(id)); + assertNull(userMgr.getAuthorizable(p)); + } else { + // no transient changes as autosave could not be disabled. + createdGroups.add(gr); + assertNotNull(userMgr.getAuthorizable(id)); + assertNotNull(userMgr.getAuthorizable(p)); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerCreateUserTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerCreateUserTest.java new file mode 100644 index 00000000000..684d41a6fe6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerCreateUserTest.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.api.security.user; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * UserManagerCreateGroupTest... + */ +public class UserManagerCreateUserTest extends AbstractUserTest { + + private static Logger log = LoggerFactory.getLogger(UserManagerCreateUserTest.class); + + private List createdUsers = new ArrayList(); + + @Override + protected void tearDown() throws Exception { + // 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); + save(superuser); + return u; + } + + private User createUser(String uid, String pw, Principal p, String iPath) throws RepositoryException, NotExecutableException { + User u = userMgr.createUser(uid, pw, p, iPath); + save(superuser); + return u; + } + + public void testCreateUser() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, buildPassword(uid)); + createdUsers.add(user); + + assertNotNull(user.getID()); + assertEquals(p.getName(), user.getPrincipal().getName()); + } + + public void testCreateUserWithPath() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, buildPassword(uid), p, "/any/path/to/the/new/user"); + createdUsers.add(user); + + assertNotNull(user.getID()); + assertEquals(p.getName(), user.getPrincipal().getName()); + } + + public void testCreateUserWithPath2() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, buildPassword(uid), p, "any/path"); + createdUsers.add(user); + + assertNotNull(user.getID()); + assertEquals(p.getName(), user.getPrincipal().getName()); + } + + public void testCreateUserWithDifferentPrincipalName() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestPrincipal().getName(); + User user = createUser(uid, buildPassword(uid), p, "/any/path/to/the/new/user"); + createdUsers.add(user); + + assertNotNull(user.getID()); + assertEquals(p.getName(), user.getPrincipal().getName()); + } + + 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 + } + } + + 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 + } + } + + 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 for changed behavior that allows creating of users with 'null' password. + * + * @since Jackrabbit 2.7 + */ + public void testCreateUserWithNullPassword() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + User user = createUser(p.getName(), null); + createdUsers.add(user); + } + + public void testCreateUserWithEmptyPassword() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + User user = createUser(p.getName(), ""); + createdUsers.add(user); + } + + public void testCreateUserWithNullPrincipal() throws RepositoryException { + try { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, buildPassword(uid), 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, buildPassword(uid), 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, buildPassword(uid), 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, buildPassword(uid)); + createdUsers.add(user); + + try { + User user2 = createUser(uid, buildPassword("anyPW")); + createdUsers.add(user2); + + fail("Creating 2 users with the same UserID should throw AuthorizableExistsException."); + } catch (AuthorizableExistsException e) { + // success. + } + } + + public void testCreateTwiceWithSamePrincipal() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + User user = createUser(uid, buildPassword(uid), p, "a/b/c"); + createdUsers.add(user); + + try { + uid = getTestPrincipal().getName(); + User user2 = createUser(uid, buildPassword(uid), p, null); + createdUsers.add(user2); + + fail("Creating 2 users with the same Principal should throw AuthorizableExistsException."); + } catch (AuthorizableExistsException e) { + // success. + } + } + + public void testGetUserAfterCreation() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + User user = createUser(uid, buildPassword(uid)); + createdUsers.add(user); + + assertNotNull(userMgr.getAuthorizable(user.getID())); + assertNotNull(userMgr.getAuthorizable(p)); + } + + + public void testAutoSave() throws RepositoryException { + boolean autosave = userMgr.isAutoSave(); + if (autosave) { + try { + userMgr.autoSave(false); + autosave = false; + } catch (RepositoryException e) { + // cannot change autosave behavior + // ignore -> test will behave differently. + } + } + + Principal p = getTestPrincipal(); + String uid = p.getName(); + + User user = userMgr.createUser(uid, buildPassword(uid)); + superuser.refresh(false); + + if (!autosave) { + // transient changes must be gone after the refresh-call. + assertNull(userMgr.getAuthorizable(uid)); + assertNull(userMgr.getAuthorizable(p)); + } else { + // changes are persisted automatically -> must not be gone. + createdUsers.add(user); + assertNotNull(userMgr.getAuthorizable(uid)); + assertNotNull(userMgr.getAuthorizable(p)); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerSearchTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerSearchTest.java new file mode 100644 index 00000000000..96d6d9f9e5d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerSearchTest.java @@ -0,0 +1,824 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.api.security.user; + +import org.apache.jackrabbit.api.security.user.QueryBuilder.Direction; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; +import org.apache.jackrabbit.spi.commons.iterator.Predicate; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import java.security.Principal; +import java.util.*; + +public class UserManagerSearchTest extends AbstractUserTest { + + // users + private User blackWidow; + private User gardenSpider; + private User jumpingSpider; + private User ant; + private User bee; + private User fly; + private User jackrabbit; + private User deer; + private User opposum; + private User kangaroo; + private User elephant; + private User lemur; + private User gibbon; + private User crocodile; + private User turtle; + private User lizard; + private User kestrel; + private User goose; + private User pelican; + private User dove; + private User salamander; + private User goldenToad; + private User poisonDartFrog; + + private final Set users = new HashSet(); + + // groups + private Group animals; + private Group invertebrates; + private Group arachnids; + private Group insects; + private Group vertebrates; + private Group mammals; + private Group apes; + private Group reptiles; + private Group birds; + private Group amphibians; + + 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); + } + + // Create a zoo. Please excuse my ignorance in zoology ;-) + animals = createGroup("animals"); + invertebrates = createGroup("invertebrates"); + arachnids = createGroup("arachnids"); + insects = createGroup("insects"); + vertebrates = createGroup("vertebrates"); + mammals = createGroup("mammals"); + apes = createGroup("apes"); + reptiles = createGroup("reptiles"); + birds = createGroup("birds"); + 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); + + blackWidow = createUser("black widow", "flies", 2, false); + gardenSpider = createUser("garden spider", "flies", 2, false); + jumpingSpider = createUser("jumping spider", "insects", 1, false); + addMembers(arachnids, blackWidow, gardenSpider, jumpingSpider); + + ant = createUser("ant", "leaves", 0.5, false); + bee = createUser("bee", "honey", 2.5, true); + fly = createUser("fly", "dirt", 1.3, false); + addMembers(insects, ant, bee, fly); + + jackrabbit = createUser("jackrabbit", "carrots", 2500, true); + deer = createUser("deer", "leaves", 120000, true); + 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); + + lemur = createUser("lemur", "nectar", 1100, true); + gibbon = createUser("gibbon", "meat", 20000, true); + addMembers(apes, lemur, gibbon); + + crocodile = createUser("crocodile", "meat", 80000, false); + turtle = createUser("turtle", "leaves", 10000, true); + lizard = createUser("lizard", "leaves", 1900, false); + addMembers(reptiles, crocodile, turtle, lizard); + + kestrel = createUser("kestrel", "mice", 2000, false); + goose = createUser("goose", "snails", 13000, true); + pelican = createUser("pelican", "fish", 15000, true); + dove = createUser("dove", "insects", 1600, false); + addMembers(birds, kestrel, goose, pelican, dove); + + salamander = createUser("salamander", "insects", 800, true); + goldenToad = createUser("golden toad", "insects", 700, false); + 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(); + } + + public void testAny() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { /* any */ } + }); + + assertSameElements(result, authorizables.iterator()); + } + + 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()); + } + } + } + + 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); + } + } + + 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); + } + } + + 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.filterIterator(members, new Predicate() { + public boolean evaluate(Authorizable authorizable) { + return !authorizable.isGroup(); + } + }); + assertSameElements(result, users); + } + } + + 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.filterIterator(members, new Predicate() { + public boolean evaluate(Authorizable authorizable) { + return authorizable.isGroup(); + } + }); + assertSameElements(result, users); + } + } + + public void testNameMatch() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder.nameMatches("a%")); + } + }); + + Iterator expected = Iterators.filterIterator(authorizables.iterator(), new Predicate() { + public boolean evaluate(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); + } + + 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.filterIterator(users.iterator(), new Predicate() { + public boolean evaluate(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); + } + + 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.filterIterator(users.iterator(), new Predicate() { + public boolean evaluate(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); + } + + 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.filterIterator(users.iterator(), new Predicate() { + public boolean evaluate(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); + } + + public void testPropertyExistence() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + exists("@poisonous")); + } + }); + + Iterator expected = Iterators.filterIterator(users.iterator(), new Predicate() { + public boolean evaluate(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); + } + + 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.filterIterator(users.iterator(), new Predicate() { + public boolean evaluate(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); + } + + public void testContains1() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + contains(".", "gold")); + } + }); + + Iterator expected = Iterators.singleton(goldenToad); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + 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.singleton(goldenToad); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + 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.singleton(kangaroo); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + 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.singleton(kangaroo); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + 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.filterIterator(users.iterator(), new Predicate() { + public boolean evaluate(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); + } + + 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.filterIterator(users.iterator(), new Predicate() { + public boolean evaluate(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); + } + + public void testImpersonation() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + impersonates("jackrabbit")); + } + }); + + Iterator expected = Iterators.singleton(elephant); + assertTrue(result.hasNext()); + assertSameElements(result, expected); + } + + public void testSortOrder1() throws RepositoryException { + Iterator result = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setCondition(builder. + exists("@color")); + builder.setSortOrder("@color", 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(); + } + } + + 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", 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(); + } + } + + 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", Direction.ASCENDING); + builder.setLimit(offset, count); + } + }); + + Iterator expected = userMgr.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + builder.setSortOrder("profile/@weight", Direction.ASCENDING); + } + }); + + skip(expected, offset); + assertSame(expected, result, count); + assertFalse(result.hasNext()); + } + } + + 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", Direction.ASCENDING, true); + builder.setLimit(vf.createValue(1000.0), count); + } + }); + + Iterator expected = Iterators.filterIterator(sortedUsers.iterator(), new Predicate() { + public boolean evaluate(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()); + } + } + + 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", 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", Direction.ASCENDING); + } + }); + + skip(expected, offset); + assertSame(expected, result, count); + assertFalse(result.hasNext()); + } + + 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", 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", 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(); + } + } + + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerTest.java new file mode 100644 index 00000000000..a67b67554e4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserManagerTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.api.security.user; + +import org.apache.jackrabbit.commons.jackrabbit.user.AuthorizableQueryManager; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; +import org.junit.Test; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import java.security.Principal; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * UserManagerTest... + */ +public class UserManagerTest extends AbstractUserTest { + + // TODO: add generic tests for UserManager.findAuthorizables + // TODO: test creating users/groups if root is locked OR checked-in. + + public void testGetAuthorizableByPrincipal() throws RepositoryException, NotExecutableException { + Authorizable auth = null; + Set principals = getPrincipalSetFromSession(superuser); + for (Iterator it = principals.iterator(); it.hasNext() && auth == null;) { + Principal p = it.next(); + auth = userMgr.getAuthorizable(p); + } + assertNotNull("At least one of the Sessions principal must be a known authorizable to the UserManager", auth); + } + + public void testGetAuthorizableById() throws RepositoryException, NotExecutableException { + Authorizable auth = null; + for (Principal principal : getPrincipalSetFromSession(superuser)) { + Principal p = principal; + auth = userMgr.getAuthorizable(p); + + if (auth != null) { + Authorizable authByID = userMgr.getAuthorizable(auth.getID()); + assertEquals("Equal ID expected", auth.getID(), authByID.getID()); + } + } + } + + 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(); + } + } + + + public void testGetAuthorizableByIdAndType() throws NotExecutableException, RepositoryException { + for (Principal principal : getPrincipalSetFromSession(superuser)) { + Principal p = principal; + Authorizable a = userMgr.getAuthorizable(p); + if (a != null) { + Authorizable authorizable = userMgr.getAuthorizable(a.getID(), a.getClass()); + assertEquals("Equal ID expected", a.getID(), authorizable.getID()); + + authorizable = userMgr.getAuthorizable(a.getID(), Authorizable.class); + assertEquals("Equal ID expected", a.getID(), authorizable.getID()); + } + } + } + + public void testGetAuthorizableByIdAndWrongType() throws NotExecutableException, RepositoryException { + for (Principal principal : getPrincipalSetFromSession(superuser)) { + Principal p = principal; + Authorizable auth = userMgr.getAuthorizable(p); + if (auth != null) { + Class otherType = auth.isGroup() ? User.class : Group.class; + try { + userMgr.getAuthorizable(auth.getID(), otherType); + fail("Wrong Authorizable type is not detected."); + } catch (AuthorizableTypeException e) { + // success + } + } + } + } + + public void testGetNonExistingAuthorizableByIdAndType() throws NotExecutableException, RepositoryException { + Authorizable auth = userMgr.getAuthorizable("nonExistingAuthorizable", User.class); + assertNull(auth); + + auth = userMgr.getAuthorizable("nonExistingAuthorizable", Authorizable.class); + assertNull(auth); + } + + public void testGetAuthorizableByNullType() throws Exception { + String uid = superuser.getUserID(); + Authorizable auth = userMgr.getAuthorizable(uid); + if (auth != null) { + try { + userMgr.getAuthorizable(uid, null); + fail("Null Authorizable type is not detected."); + } catch (AuthorizableTypeException e) { + // success + } + } + } + + public void testGetNonExistingAuthorizableByNullType() throws Exception { + assertNull(userMgr.getAuthorizable("nonExistingAuthorizable", null)); + } + + @Test + public void testFindUserWithSpecialCharIdByPrincipalName() throws RepositoryException { + List ids = Arrays.asList("'", Text.escapeIllegalJcrChars("']"), Text.escape("']")); + for (String id : ids) { + User user = null; + try { + user = userMgr.createUser(id, "pw"); + superuser.save(); + + boolean found = false; + Iterator it = userMgr.findAuthorizables("rep:principalName", id, UserManager.SEARCH_TYPE_USER); + while (it.hasNext() && !found) { + Authorizable a = it.next(); + found = id.equals(a.getID()); + } + assertTrue(found); + } finally { + if (user != null) { + user.remove(); + superuser.save(); + } + } + } + } + + @Test + public void testFindUserWithSpecialCharIdByPrincipalName2() throws RepositoryException { + List ids = Arrays.asList("]"); + for (String id : ids) { + User user = null; + try { + user = userMgr.createUser(id, "pw"); + superuser.save(); + + boolean found = false; + Iterator it = userMgr.findAuthorizables("rep:principalName", id, UserManager.SEARCH_TYPE_USER); + while (it.hasNext() && !found) { + Authorizable a = it.next(); + found = id.equals(a.getID()); + } + assertTrue(found); + } finally { + if (user != null) { + user.remove(); + superuser.save(); + } + } + } + } + + @Test + public void testQueryUserWithSpecialCharId() throws Exception { + List ids = Arrays.asList("'", "]"); + for (String id : ids) { + User user = null; + try { + user = userMgr.createUser(id, "pw"); + superuser.save(); + + boolean found = false; + String query = "{\"condition\":[{\"named\":\"" + id + "\"}]}"; + AuthorizableQueryManager queryManager = new AuthorizableQueryManager(userMgr, superuser.getValueFactory()); + Iterator it = queryManager.execute(query); + while (it.hasNext() && !found) { + Authorizable a = it.next(); + found = id.equals(a.getID()); + } + assertTrue(found); + } finally { + if (user != null) { + user.remove(); + superuser.save(); + } + } + } + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserTest.java new file mode 100644 index 00000000000..e308282ae4c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/api/security/user/UserTest.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.api.security.user; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +/** + * UserTest... + */ +public class UserTest extends AbstractUserTest { + + private static Logger log = LoggerFactory.getLogger(UserTest.class); + + public void testNotIsGroup() throws NotExecutableException, RepositoryException { + User user = getTestUser(superuser); + assertFalse(user.isGroup()); + } + + public void testSuperuserIsAdmin() throws NotExecutableException, RepositoryException { + User user = getTestUser(superuser); + assertTrue(user.isAdmin()); + } + + public void testReadOnlyIsntAdmin() throws NotExecutableException, RepositoryException { + Session s = getHelper().getReadOnlySession(); + try { + User user = getTestUser(s); + assertFalse(user.isAdmin()); + } finally { + s.logout(); + } + } + + public void testUserHasCredentials() throws RepositoryException, NotExecutableException { + User user = getTestUser(superuser); + Credentials creds = user.getCredentials(); + assertTrue(creds != null); + } + + public void testChangePassword() throws RepositoryException, NotExecutableException { + String oldPw = getHelper().getProperty("javax.jcr.tck.superuser.pwd"); + if (oldPw == null) { + // missing property + throw new NotExecutableException(); + } + + User user = getTestUser(superuser); + try { + user.changePassword("pw"); + save(superuser); + + // make sure the user can login with the new pw + Session s = getHelper().getRepository().login(new SimpleCredentials(user.getID(), "pw".toCharArray())); + s.logout(); + } finally { + user.changePassword(oldPw); + save(superuser); + } + } + + public void testChangePassword2() throws RepositoryException, NotExecutableException { + String oldPw = getHelper().getProperty("javax.jcr.tck.superuser.pwd"); + if (oldPw == null) { + // missing property + throw new NotExecutableException(); + } + + User user = getTestUser(superuser); + try { + user.changePassword("pw"); + save(superuser); + + Session s = getHelper().getRepository().login(new SimpleCredentials(user.getID(), oldPw.toCharArray())); + s.logout(); + fail("superuser pw has changed. login must fail."); + } catch (LoginException e) { + // success + } finally { + user.changePassword(oldPw); + save(superuser); + } + } + + public void testChangePasswordWithOldPassword() throws RepositoryException, NotExecutableException { + String oldPw = getHelper().getProperty("javax.jcr.tck.superuser.pwd"); + if (oldPw == null) { + // missing property + throw new NotExecutableException(); + } + + User user = getTestUser(superuser); + try { + try { + user.changePassword("pw", "wrongOldPw"); + save(superuser); + fail("old password didn't match -> changePassword(String,String) should fail."); + } catch (RepositoryException e) { + // success. + } + + user.changePassword("pw", oldPw); + save(superuser); + + // make sure the user can login with the new pw + Session s = getHelper().getRepository().login(new SimpleCredentials(user.getID(), "pw".toCharArray())); + s.logout(); + } finally { + user.changePassword(oldPw); + save(superuser); + } + } + + public void testChangePasswordWithOldPassword2() throws RepositoryException, NotExecutableException { + String oldPw = getHelper().getProperty("javax.jcr.tck.superuser.pwd"); + if (oldPw == null) { + // missing property + throw new NotExecutableException(); + } + + User user = getTestUser(superuser); + try { + user.changePassword("pw", oldPw); + save(superuser); + + Session s = getHelper().getRepository().login(new SimpleCredentials(user.getID(), oldPw.toCharArray())); + s.logout(); + fail("superuser pw has changed. login must fail."); + } catch (LoginException e) { + // success + } finally { + user.changePassword(oldPw); + save(superuser); + } + } + + public void testDisable() throws Exception { + boolean remove = false; + Session s = getHelper().getReadOnlySession(); + + User user = null; + String userID = null; + String pw = ""; + + try { + User readonlyUser = getTestUser(s); + if (readonlyUser.isAdmin()) { + // configured readonly user is admin + // -> need to create another test user + pw = "test"; + userID = getUserManager(superuser).createUser(getTestPrincipal().getName(), pw).getID(); + remove = true; + } else { + userID = readonlyUser.getID(); + } + + user = (User) getUserManager(superuser).getAuthorizable(userID); + + // by default a user isn't disabled + assertFalse(user.isDisabled()); + assertNull(user.getDisabledReason()); + + // disable user + String reason = "readonly user is disabled!"; + user.disable(reason); + save(superuser); + assertTrue(user.isDisabled()); + assertEquals(reason, user.getDisabledReason()); + + // user must still be retrievable from user manager + assertNotNull(getUserManager(superuser).getAuthorizable(userID)); + // ... and from principal manager as well + assertTrue(((JackrabbitSession) superuser).getPrincipalManager().hasPrincipal(user.getPrincipal().getName())); + + // -> login must fail + try { + Session ss = getHelper().getRepository().login(new SimpleCredentials(userID, pw.toCharArray())); + ss.logout(); + fail("A disabled user must not be allowed to login any more"); + } catch (LoginException e) { + // success + } + + // -> impersonating this user must fail + try { + Session ss = superuser.impersonate(new SimpleCredentials(userID, new char[0])); + ss.logout(); + fail("A disabled user cannot be impersonated any more."); + } catch (LoginException e) { + // success + } + + // enable user again + user.disable(null); + save(superuser); + assertFalse(user.isDisabled()); + + // -> login must succeed again + getHelper().getRepository().login(new SimpleCredentials(userID, pw.toCharArray())).logout(); + + } finally { + s.logout(); + + if (user != null) { + if (user.isDisabled()) { + user.disable(null); + } + + if (remove) { + user.remove(); + save(superuser); + } + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/AbstractConcurrencyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/AbstractConcurrencyTest.java new file mode 100644 index 00000000000..d3706f58726 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/AbstractConcurrencyTest.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.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; + +/** + * AbstractConcurrencyTest provides utility methods to run tests + * using concurrent threads. + */ +public abstract class AbstractConcurrencyTest extends AbstractJCRTest { + + /** + * Logger instance for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(AbstractConcurrencyTest.class); + + /** + * Runs a task with the given concurrency and creates an individual test + * node for each thread. + * + * @param task the task to run. + * @param concurrency the concurrency. + * @throws RepositoryException if an error occurs. + */ + protected void runTask(Task task, int concurrency) throws RepositoryException { + runTasks(new Task[]{task}, concurrency, + // run for at most one year ;) + getOneYearAhead()); + } + + /** + * Runs each of the tasks with the given concurrency and creates an + * individual test node for each thread. + * + * @param tasks the tasks to run. + * @param concurrency the concurrency. + * @param timeout when System.currentTimeMillis() reaches timeout the + * threads executing the tasks should be interrupted. + * This indicates that a deadlock occured. + * @throws RepositoryException if an error occurs. + */ + protected void runTasks(Task[] tasks, int concurrency, long timeout) + throws RepositoryException { + Executor[] executors = new Executor[concurrency * tasks.length]; + for (int t = 0; t < tasks.length; t++) { + for (int i = 0; i < concurrency; i++) { + int id = t * concurrency + i; + Session s = getHelper().getSuperuserSession(); + Node test = s.getRootNode().addNode(testPath + "/node" + id); + s.save(); + executors[id] = new Executor(s, test, tasks[t]); + } + } + executeAll(executors, timeout); + } + + /** + * Runs a task with the given concurrency on the node identified by path. + * + * @param task the task to run. + * @param concurrency the concurrency. + * @param path the path to the test node. + * @throws RepositoryException if an error occurs. + */ + protected void runTask(Task task, int concurrency, String path) + throws RepositoryException { + Executor[] executors = new Executor[concurrency]; + for (int i = 0; i < concurrency; i++) { + Session s = getHelper().getSuperuserSession(); + Node test = (Node) s.getItem(path); + s.save(); + executors[i] = new Executor(s, test, task); + } + executeAll(executors, getOneYearAhead()); + } + + /** + * Executes all executors using individual threads. + * + * @param executors the executors. + * @param timeout time when running threads should be interrupted. + * @throws RepositoryException if one of the executors throws an exception. + */ + private void executeAll(Executor[] executors, long timeout) throws RepositoryException { + Thread[] threads = new Thread[executors.length]; + for (int i = 0; i < executors.length; i++) { + threads[i] = new Thread(executors[i], "Executor " + i); + } + for (int i = 0; i < threads.length; i++) { + threads[i].start(); + } + + boolean stacksDumped = false; + for (int i = 0; i < threads.length; i++) { + try { + long wait = Math.max(timeout - System.currentTimeMillis(), 1000); + threads[i].join(wait); + if (threads[i].isAlive()) { + if (!stacksDumped) { + dumpStacks(threads); + stacksDumped = true; + } + threads[i].interrupt(); + // give the thread a couple of seconds, then call stop + Thread.sleep(5 * 1000); + if (threads[i].isAlive()) { + threads[i].stop(); + } + } + } catch (InterruptedException e) { + // ignore + } + } + for (int i = 0; i < executors.length; i++) { + if (executors[i].getException() != null) { + throw executors[i].getException(); + } + } + } + + protected long getOneYearAhead() { + return System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 30L * 12L; + } + + /** + * If tests are run in a 1.5 JVM or higher the stack of the given threads + * are dumped to the logger with level ERROR. + * + * @param threads An array of threads. + */ + protected static void dumpStacks(Thread[] threads) { + try { + Method m = Thread.class.getMethod("getStackTrace", null); + StringBuffer dumps = new StringBuffer(); + for (int t = 0; t < threads.length; t++) { + StackTraceElement[] elements = (StackTraceElement[]) m.invoke( + threads[t], null); + dumps.append(threads[t].toString()).append('\n'); + for (int i = 0; i < elements.length; i++) { + dumps.append("\tat " + elements[i]).append('\n'); + } + dumps.append('\n'); + } + logger.error("Thread dumps:\n{}", dumps); + } catch (NoSuchMethodException e) { + // not a 1.5 JVM + } catch (IllegalAccessException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } + } + + /** + * Task implementations must be thread safe! Multiple threads will call + * {@link #execute(Session, Node)} concurrently. + */ + public interface Task { + + public abstract void execute(Session session, Node test) + throws RepositoryException; + } + + protected static class Executor implements Runnable { + + protected final Session session; + + protected final Node test; + + protected final Task task; + + protected RepositoryException exception; + + public Executor(Session session, Node test, Task task) { + this.session = session; + this.test = test; + this.task = task; + } + + public RepositoryException getException() { + return exception; + } + + public void run() { + try { + task.execute(session, test); + } catch (RepositoryException e) { + exception = e; + } finally { + session.logout(); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/AddMoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/AddMoveTest.java new file mode 100644 index 00000000000..db367258c86 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/AddMoveTest.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.core; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +public class AddMoveTest extends AbstractJCRTest { + + private String folder1Path; + private String folder2Path; + + @Override + public void setUp() throws Exception { + super.setUp(); + Node folder1 = testRootNode.addNode("folder1"); + folder1Path = folder1.getPath(); + Node folder2 = testRootNode.addNode("folder2"); + folder2Path = folder2.getPath(); + folder1.addNode("node1"); + testRootNode.getSession().save(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + } + + public void testAddMove() throws RepositoryException, NotExecutableException { + Session session1 = getHelper().getReadWriteSession(); + Session session2 = getHelper().getReadWriteSession(); + + session1.getNode(folder1Path).addNode("node2"); + session2.move(folder1Path + "/node1", folder2Path + "/node1"); + session2.save(); + Node node = session1.getNode(folder2Path + "/node1"); + node.setProperty("foo", "bar"); + session1.save(); + + ConsistencyReport consistencyReport = TestHelper.checkConsistency(testRootNode.getSession(), false, null); + //for (ReportItem item : consistencyReport.getItems()) { + // System.out.println(item.getMessage()); + //} + assertTrue(consistencyReport.getItems().size() == 0); + } + + /** + * Add a top level node and rename it. Exposes a bug in the {@code CachingHierarchyManager}, + * reported in JCR-3379. + */ + public void testTopLevelAddMove() throws Exception { + Session session = getHelper().getReadWriteSession(); + session.getRootNode().addNode("foo"); + session.save(); + Node fooNode = session.getNode("/foo"); + assertEquals("/foo", fooNode.getPath()); + session.move("/foo", "/bar"); + Node barNode = session.getNode("/bar"); + assertEquals("/bar", barNode.getPath()); + } + + /** + * Add a top level node and remove it. Exposes a bug in the {@code CachingHierarchyManager}, + * reported in JCR-3368. + */ + public void testTopLevelAddRemove() throws Exception { + Session session = getHelper().getReadWriteSession(); + session.getRootNode().addNode("foo").addNode("bar"); + session.save(); + session.getNode("/foo").remove(); + assertFalse(session.getRootNode().hasNode("foo/bar")); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/CachingHierarchyManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/CachingHierarchyManagerTest.java new file mode 100644 index 00000000000..7406c942d01 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/CachingHierarchyManagerTest.java @@ -0,0 +1,606 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.ArrayList; +import java.util.HashMap; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.state.ChildNodeEntry; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.ItemStateManager; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.NodeStateListener; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; + +public class CachingHierarchyManagerTest extends TestCase { + + volatile Exception exception; + volatile boolean stop; + CachingHierarchyManager cache; + + /** + * Test multi-threaded read and write access to the cache. + */ + public void testResolveNodePath() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + ism.addNode(ism.getRoot(), "a"); + ism.addNode(ism.getRoot(), "b"); + final Path aPath = toPath("/a"); + final Path bPath = toPath("/b"); + for (int i = 0; i < 3; i++) { + new Thread(new Runnable() { + public void run() { + while (!stop) { + try { + cache.resolveNodePath(aPath); + cache.resolveNodePath(bPath); + } catch (Exception e) { + exception = e; + } + } + } + }).start(); + } + Thread.sleep(1000); + stop = true; + if (exception != null) { + throw exception; + } + } + + //-------------------------------------------------------------- basic tests + + /** + * Verify that resolving node and property paths will only return valid hits. + */ + public void testResolveNodePropertyPath() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a = ism.addNode(ism.getRoot(), "a"); + NodeState b = ism.addNode(a, "b"); + + Path path = toPath("/a/b"); + + // /a/b points to node only + assertIsNodeId(cache.resolvePath(path)); + assertIsNodeId(cache.resolveNodePath(path)); + assertNull(cache.resolvePropertyPath(path)); + + ism.addProperty(a, "b"); + + // /a/b points to node and property + assertNotNull(cache.resolvePath(path)); + assertIsNodeId(cache.resolveNodePath(path)); + assertIsPropertyId(cache.resolvePropertyPath(path)); + + ism.removeNode(b); + + // /a/b points to property only + assertIsPropertyId(cache.resolvePath(path)); + assertNull(cache.resolveNodePath(path)); + assertIsPropertyId(cache.resolvePropertyPath(path)); + } + + /** + * Assert that an item id is a property id. + * @param id item id + */ + private static void assertIsPropertyId(ItemId id) { + assertTrue(id instanceof PropertyId); + } + + /** + * Assert that an item id is a node id. + * @param id item id + */ + private static void assertIsNodeId(ItemId id) { + assertTrue(id instanceof NodeId); + } + + //------------------------------------------------------------ caching tests + + /** + * Add a SNS (same name sibling) and verify that cached paths are + * adapted accordingly. + */ + public void testAddSNS() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a = ism.addNode(ism.getRoot(), "a"); + NodeState b1 = ism.addNode(a, "b"); + Path path = cache.getPath(b1.getNodeId()); + assertEquals(toPath("/a/b"), path); + NodeState b2 = ism.addNode(a, "b"); + ism.orderBefore(b2, b1); + assertTrue(cache.isCached(b1.getNodeId(), null)); + path = cache.getPath(b1.getNodeId()); + assertEquals(toPath("/a/b[2]"), path); + } + + /** + * Clone a node, cache its path and remove it afterwards. Should remove + * the cached path as well. + */ + public void testCloneAndRemove() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a1 = ism.addNode(ism.getRoot(), "a1"); + NodeState a2 = ism.addNode(ism.getRoot(), "a2"); + NodeState b1 = ism.addNode(a1, "b1"); + b1.addShare(b1.getParentId()); + ism.cloneNode(b1, a2, "b2"); + + Path path1 = toPath("/a1/b1"); + Path path2 = toPath("/a2/b2"); + + assertNotNull(cache.resolvePath(path1)); + assertTrue(cache.isCached(b1.getNodeId(), path1)); + + ism.removeNode(b1); + + assertNull(cache.resolvePath(path1)); + assertNotNull(cache.resolvePath(path2)); + } + + /** + * Clone a node, create a child and resolve its path in all valid + * combinations. Then, move the child away. Should remove the cached + * paths as well. + */ + public void testCloneAndAddChildAndMove() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a1 = ism.addNode(ism.getRoot(), "a1"); + NodeState a2 = ism.addNode(ism.getRoot(), "a2"); + NodeState b1 = ism.addNode(a1, "b1"); + b1.addShare(b1.getParentId()); + ism.cloneNode(b1, a2, "b2"); + NodeState c = ism.addNode(b1, "c"); + + Path path1 = toPath("/a1/b1/c"); + Path path2 = toPath("/a2/b2/c"); + + assertNotNull(cache.resolvePath(path1)); + assertTrue(cache.isCached(c.getNodeId(), path1)); + assertNotNull(cache.resolvePath(path2)); + assertTrue(cache.isCached(c.getNodeId(), path2)); + + ism.moveNode(c, a1, "c"); + + assertNull(cache.resolvePath(path1)); + assertNull(cache.resolvePath(path2)); + } + + /** + * Move a node and verify that cached path is adapted. + */ + public void testMove() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a1 = ism.addNode(ism.getRoot(), "a1"); + NodeState a2 = ism.addNode(ism.getRoot(), "a2"); + NodeState b1 = ism.addNode(a1, "b1"); + Path path = cache.getPath(b1.getNodeId()); + assertEquals(toPath("/a1/b1"), path); + ism.moveNode(b1, a2, "b2"); + path = cache.getPath(b1.getNodeId()); + assertEquals(toPath("/a2/b2"), path); + } + + /** + * Reorder child nodes and verify that cached paths are still adequate. + */ + public void testOrderBefore() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a = ism.addNode(ism.getRoot(), "a"); + NodeState b1 = ism.addNode(a, "b"); + NodeState b2 = ism.addNode(a, "b"); + NodeState b3 = ism.addNode(a, "b"); + Path path = cache.getPath(b1.getNodeId()); + assertEquals(toPath("/a/b"), path); + ism.orderBefore(b2, b1); + ism.orderBefore(b1, b3); + path = cache.getPath(b1.getNodeId()); + assertEquals(toPath("/a/b[2]"), path); + } + + /** + * Remove a node and verify that cached path is gone. + */ + public void testRemove() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a = ism.addNode(ism.getRoot(), "a"); + NodeState b = ism.addNode(a, "b"); + NodeState c = ism.addNode(b, "c"); + cache.getPath(c.getNodeId()); + assertTrue(cache.isCached((NodeId) c.getId(), null)); + ism.removeNode(b); + assertFalse(cache.isCached((NodeId) c.getId(), null)); + } + + /** + * Remove a SNS (same name sibling) and verify that cached paths are + * adapted accordingly. The removed SNS's path is not cached. + */ + public void testRemoveSNS() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a = ism.addNode(ism.getRoot(), "a"); + NodeState b1 = ism.addNode(a, "b"); + NodeState b2 = ism.addNode(a, "b"); + Path path = cache.getPath(b2.getNodeId()); + assertEquals(toPath("/a/b[2]"), path); + ism.removeNode(b1); + path = cache.getPath(b2.getNodeId()); + assertEquals(toPath("/a/b"), path); + } + + /** + * Remove a SNS (same name sibling) and verify that cached paths are + * adapted accordingly. The removed SNS's path is cached. + */ + public void testRemoveSNSWithCachedPath() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a = ism.addNode(ism.getRoot(), "a"); + NodeState b1 = ism.addNode(a, "b"); + NodeState b2 = ism.addNode(a, "b"); + cache.getPath(b1.getNodeId()); + Path path = cache.getPath(b2.getNodeId()); + assertEquals(toPath("/a/b[2]"), path); + ism.removeNode(b1); + path = cache.getPath(b2.getNodeId()); + assertEquals(toPath("/a/b"), path); + } + + /** + * Rename a node and verify that cached path is adapted. + */ + public void testRename() throws Exception { + StaticItemStateManager ism = new StaticItemStateManager(); + cache = new CachingHierarchyManager(ism.getRootNodeId(), ism); + ism.setContainer(cache); + NodeState a1 = ism.addNode(ism.getRoot(), "a1"); + NodeState b1 = ism.addNode(a1, "b"); + NodeState b2 = ism.addNode(a1, "b"); + Path path = cache.getPath(b1.getNodeId()); + assertEquals(toPath("/a1/b"), path); + path = cache.getPath(b2.getNodeId()); + assertEquals(toPath("/a1/b[2]"), path); + ism.renameNode(b1, "b1"); + assertTrue(cache.isCached(b1.getNodeId(), null)); + assertTrue(cache.isCached(b2.getNodeId(), null)); + path = cache.getPath(b1.getNodeId()); + assertEquals(toPath("/a1/b1"), path); + } + + /** + * Static item state manager, that can be filled programmatically and that + * keeps a hash map of item states. ItemIds generated by + * this state manager start with 0. + */ + static class StaticItemStateManager implements ItemStateManager { + + /** Root node id */ + private final NodeId rootNodeId; + + /** Map of item states */ + private final HashMap states = new HashMap(); + + /** UUID generator base */ + private long lsbGenerator; + + /** Root node state */ + private NodeState root; + + /** Node state listener to register in item states */ + private NodeStateListener listener; + + /** + * Create a new instance of this class. + */ + public StaticItemStateManager() { + rootNodeId = nextNodeId(); + } + + /** + * Return the root node id. + * + * @return root node id + */ + public NodeId getRootNodeId() { + return rootNodeId; + } + + /** + * Return the root node. + * + * @return root node + */ + public NodeState getRoot() { + if (root == null) { + root = new NodeState(rootNodeId, NameConstants.JCR_ROOT, + null, NodeState.STATUS_EXISTING, false); + if (listener != null) { + root.setContainer(listener); + } + } + return root; + } + + /** + * Set the listener that should be registered in new item states. + * + * @param listener listener + */ + public void setContainer(NodeStateListener listener) { + this.listener = listener; + } + + /** + * Add a node. + * + * @param parent parent node + * @param name node name + * @return new node + */ + public NodeState addNode(NodeState parent, String name) { + NodeId id = nextNodeId(); + NodeState child = new NodeState(id, NameConstants.NT_UNSTRUCTURED, + parent.getNodeId(), NodeState.STATUS_EXISTING, false); + if (listener != null) { + child.setContainer(listener); + } + states.put(id, child); + parent.addChildNodeEntry(toName(name), child.getNodeId()); + return child; + } + + /** + * Add a property. + * + * @param parent parent node + * @param name property name + * @return new property + */ + public PropertyState addProperty(NodeState parent, String name) { + PropertyId id = new PropertyId(parent.getNodeId(), toName(name)); + PropertyState child = new PropertyState(id, + PropertyState.STATUS_EXISTING, false); + if (listener != null) { + child.setContainer(listener); + } + states.put(id, child); + parent.addPropertyName(toName(name)); + return child; + } + + /** + * Clone a node. + * + * @param src node to clone + * @param parent destination parent node + * @param name node name + */ + public void cloneNode(NodeState src, NodeState parent, String name) { + src.addShare(parent.getNodeId()); + parent.addChildNodeEntry(toName(name), src.getNodeId()); + } + + /** + * Move a node. + * + * @param child node to move + * @param newParent destination parent node + * @param name node name + * @throws ItemStateException if getting the old parent node fails + */ + public void moveNode(NodeState child, NodeState newParent, String name) + throws ItemStateException { + + NodeState oldParent = (NodeState) getItemState(child.getParentId()); + ChildNodeEntry cne = oldParent.getChildNodeEntry(child.getNodeId()); + if (cne == null) { + throw new ItemStateException(child.getNodeId().toString()); + } + oldParent.removeChildNodeEntry(cne.getName(), cne.getIndex()); + child.setParentId(newParent.getNodeId()); + newParent.addChildNodeEntry(toName(name), child.getNodeId()); + } + + /** + * Order a child node before another node. + * + * @param src src node + * @param dest destination node, may be null + * @throws ItemStateException if getting the parent node fails + */ + public void orderBefore(NodeState src, NodeState dest) + throws ItemStateException { + + NodeState parent = (NodeState) getItemState(src.getParentId()); + + ArrayList list = new ArrayList(parent.getChildNodeEntries()); + + int srcIndex = -1, destIndex = -1; + for (int i = 0; i < list.size(); i++) { + ChildNodeEntry cne = (ChildNodeEntry) list.get(i); + if (cne.getId().equals(src.getId())) { + srcIndex = i; + } else if (dest != null && cne.getId().equals(dest.getId())) { + destIndex = i; + } + } + if (destIndex == -1) { + list.add(list.remove(srcIndex)); + } else { + if (srcIndex < destIndex) { + list.add(destIndex, list.get(srcIndex)); + list.remove(srcIndex); + } else { + list.add(destIndex, list.remove(srcIndex)); + } + } + parent.setChildNodeEntries(list); + } + + /** + * Remove a node. + * + * @param child node to remove + * @throws ItemStateException if getting the parent node fails + */ + public void removeNode(NodeState child) throws ItemStateException { + NodeState parent = (NodeState) getItemState(child.getParentId()); + if (child.isShareable()) { + if (child.removeShare(parent.getNodeId()) == 0) { + child.setParentId(null); + } + } + parent.removeChildNodeEntry(child.getNodeId()); + } + + /** + * Rename a node. + * + * @param child node to rename + * @param newName new name + * @throws ItemStateException if getting the parent node fails + */ + public void renameNode(NodeState child, String newName) throws ItemStateException { + NodeState parent = (NodeState) getItemState(child.getParentId()); + ChildNodeEntry cne = parent.getChildNodeEntry(child.getNodeId()); + if (cne == null) { + throw new ItemStateException(child.getNodeId().toString()); + } + parent.renameChildNodeEntry(cne.getName(), cne.getIndex(), toName(newName)); + } + + /** + * Return the next available node id. Simply increments the last UUID + * returned by 1. + * + * @return next UUID + */ + private NodeId nextNodeId() { + return new NodeId(0, lsbGenerator++); + } + + //----------------------------------------------------- ItemStateManager + + /** + * {@inheritDoc} + */ + public ItemState getItemState(ItemId id) + throws NoSuchItemStateException, ItemStateException { + + if (id.equals(root.getId())) { + return root; + } + ItemState item = (ItemState) states.get(id); + if (item == null) { + throw new NoSuchItemStateException(id.toString()); + } + return item; + } + + /** + * {@inheritDoc} + */ + public boolean hasItemState(ItemId id) { + if (id.equals(root.getId())) { + return true; + } + return states.containsKey(id); + } + + /** + * {@inheritDoc} + */ + public NodeReferences getNodeReferences(NodeId id) + throws NoSuchItemStateException, ItemStateException { + return null; + } + + /** + * {@inheritDoc} + */ + public boolean hasNodeReferences(NodeId id) { + return false; + } + } + + /** + * Utility method, converting a string into a path. + * + * @param s string + * @return path + */ + private static Path toPath(String s) { + StringBuffer buf = new StringBuffer("{}"); + int start = 1, length = s.length(); + while (start < length) { + int end = s.indexOf('/', start); + if (end == -1) { + end = length; + } + String name = s.substring(start, end); + if (name.length() > 0) { + buf.append("\t{}"); + buf.append(name); + } + start = end + 1; + } + return PathFactoryImpl.getInstance().create(buf.toString()); + } + + /** + * Utility method, converting a string into a name. + * + * @param s string + * @return name + */ + private static Name toName(String s) { + return NameFactoryImpl.getInstance().create("", s); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrencyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrencyTest.java new file mode 100644 index 00000000000..d0c47694fb0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrencyTest.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.Session; +import java.util.Random; +import java.util.ArrayList; +import java.util.Iterator; + +public class ConcurrencyTest extends AbstractJCRTest { + + private static final int NUM_ITERATIONS = 2; + private static final int NUM_SESSIONS = 100; + private static final int NUM_NODES = 100; + + final ArrayList exceptions = new ArrayList(); + + protected void setUp() throws Exception { + super.setUp(); + // @todo setup test environment + } + + protected void tearDown() throws Exception { + try { + // @todo cleanup test environment + } finally { + super.tearDown(); + } + } + + /** + * Runs the test. + */ + public void testConcurrentWritingSessions() throws Exception { + int n = NUM_ITERATIONS; + while (n-- > 0) { + Thread[] threads = new Thread[NUM_SESSIONS]; + for (int i = 0; i < threads.length; i++) { + // create new session + Session session = getHelper().getSuperuserSession(); + TestSession ts = new TestSession("s" + i, session); + Thread t = new Thread(ts); + t.setName((NUM_ITERATIONS - n) + "-s" + i); + t.start(); + threads[i] = t; + Thread.sleep(100); + } + for (int i = 0; i < threads.length; i++) { + threads[i].join(); + } + } + + if (!exceptions.isEmpty()) { + Exception e = null; + for (Iterator it = exceptions.iterator(); it.hasNext();) { + e = (Exception) it.next(); + e.printStackTrace(log); + } + throw e; + //fail(); + } + } + + //--------------------------------------------------------< inner classes > + class TestSession implements Runnable { + + Session session; + String identity; + Random r; + + TestSession(String identity, Session s) { + session = s; + this.identity = identity; + r = new Random(); + } + + private void randomSleep() { + long l = r.nextInt(90) + 20; + try { + Thread.sleep(l); + } catch (InterruptedException ie) { + } + } + + public void run() { + + log.println("started."); + String state = ""; + try { + Node rn = session.getRootNode().getNode(testPath); + + state = "searching testnode"; + Node n; + try { + if (rn.hasNode("testnode-" + identity)) { + state = "removing testnode"; + rn.getNode("testnode-" + identity).remove(); + session.save(); + randomSleep(); + } + state = "adding testnode"; + n = rn.addNode("testnode-" + identity, "nt:unstructured"); + session.save(); + } catch (InvalidItemStateException e) { + // expected + log.println("encountered InvalidItemStateException while " + state + ", quitting..."); + //e.printStackTrace(log); + return; + } + + state = "setting property"; + n.setProperty("testprop", "Hello World!"); + session.save(); + randomSleep(); + + for (int i = 0; i < NUM_NODES; i++) { + state = "adding subnode " + i; + n.addNode("x" + i, "nt:unstructured"); + state = "adding property to subnode " + i; + n.setProperty("testprop", "xxx"); + if (i % 10 == 0) { + state = "saving pending subnodes"; + session.save(); + } + randomSleep(); + } + session.save(); + } catch (Exception e) { + log.println("Exception while " + state + ": " + e.getMessage()); + //e.printStackTrace(); + exceptions.add(e); + } finally { + session.logout(); + } + + log.println("ended."); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrencyTest3.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrencyTest3.java new file mode 100644 index 00000000000..b1482d51c51 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrencyTest3.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.core; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +public class ConcurrencyTest3 extends TestCase { + + private static final int NUM_ITERATIONS = 1; + + private static final int NUM_THREADS = 5; + + private File repoDescriptor; + + private File repoHome; + + private RepositoryImpl repository; + + public void setUp() throws IOException, RepositoryException { + File baseDir = new File(System.getProperty("basedir", ".")); + File repoBaseDir = new File(baseDir, "target/corruption-test3"); + FileUtils.deleteQuietly(repoBaseDir); + + repoDescriptor = new File(repoBaseDir, "repository.xml"); + repoHome = new File(repoBaseDir, "repository"); + repoHome.mkdirs(); + + File repositoryDescriptor = new File(baseDir, "src/test/repository/repository.xml"); + FileUtils.copyFile(repositoryDescriptor, repoDescriptor); + } + + public void tearDown() throws IOException, InterruptedException { + FileUtils.deleteQuietly(repoHome.getParentFile()); + } + + /** + * Runs the test. + */ + public void testConcurrentWritingSessions() throws Exception { + + startRepository(); + + // Create test root node + Session session = login(); + Node testRoot = session.getRootNode().addNode("test"); + Node testRoot2 = session.getRootNode().addNode("test2"); + testRoot.addMixin("mix:referenceable"); + session.save(); + + for (int i = 0; i < NUM_ITERATIONS; i++) { + JcrTestThread[] threads = new JcrTestThread[NUM_THREADS]; + Session[] sessions = new Session[NUM_THREADS]; + for (int j = 0; j < threads.length; j++) { + // create new session and a new thread + Session threadSession = login(); + JcrTestThread thread = new JcrTestThread(testRoot.getUUID(), threadSession); + thread.setName("Iteration " + i + " - Thread " + j); + thread.start(); + threads[j] = thread; + sessions[j] = threadSession; + } + for (int j = 0; j < threads.length; j++) { + if (threads[j] != null) { + threads[j].join(); + } + } + for (int j = 0; j < sessions.length; j++) { + if (sessions[j] != null) { + sessions[j].logout(); + } + } + } + + session.logout(); + stopRepository(); + + // Restart with an empty index, scan and delete test root node + deleteIndex(); + startRepository(); + session = login(); + testRoot = session.getRootNode().getNode("test"); + testRoot.addNode("new node"); + session.save(); + scan(testRoot); + testRoot.remove(); + session.save(); + session.logout(); + stopRepository(); + } + + private void startRepository() throws RepositoryException { + repository = + RepositoryImpl.create(RepositoryConfig.create(repoDescriptor.getAbsolutePath(), repoHome + .getAbsolutePath())); + } + + private Session login() throws RepositoryException { + return repository.login(new SimpleCredentials("admin", "admin".toCharArray()), null); + } + + private void stopRepository() throws RepositoryException { + repository.shutdown(); + } + + private void deleteIndex() throws IOException { + FileUtils.deleteDirectory(new File(repoHome, "workspaces/default/index")); + } + + private void scan(Node node) throws RepositoryException { + // System.err.println(node.getName() + " - " + node.getPath()); + for (NodeIterator it = node.getNodes(); it.hasNext();) { + Node child = it.nextNode(); + scan(child); + } + } + + class JcrTestThread extends Thread { + + private String testRootUuid; + + private Session jcrSession; + + private ArrayList nodes = new ArrayList(); + + private int RUN_SIZE = 100; + + private int ACTION_SIZE = 1000; + + JcrTestThread(String uuid, Session session) throws RepositoryException { + testRootUuid = uuid; + jcrSession = session; + } + + public void run() { + outer: for (int i = 0; i < RUN_SIZE; i++) { + for (int j = 0; j < ACTION_SIZE; j++) { + int random = (int) Math.floor(2 * Math.random()); + if (random == 0) { + try { + // Add a node called "P" + Node addedNode = jcrSession.getNodeByUUID(testRootUuid).addNode("P"); + addedNode.addMixin("mix:referenceable"); + nodes.add(addedNode.getUUID()); + + } catch (RepositoryException ignore) { + } + } else { + if (nodes.size() > 0) { + int randomIndex = (int) Math.floor(nodes.size() * Math.random()); + try { + // Remove a random node we created within this + // thread and within this session + Node removeNode = jcrSession.getNodeByUUID((String) nodes.get(randomIndex)); + String path = removeNode.getPath(); + if (path.indexOf("test2") == -1) { + jcrSession.move(removeNode.getPath(), removeNode.getParent().getPath() + + "2/P"); + } else { + removeNode.remove(); + nodes.remove(randomIndex); + } + } catch (RepositoryException ignore) { + System.err.println(" 1 " + ignore.toString()); + } + } + } + } + try { + jcrSession.save(); + } catch (RepositoryException e) { + System.err.println(" 2 " + e.toString()); + break outer; + } + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddMoveRemoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddMoveRemoveTest.java new file mode 100644 index 00000000000..ff6b4b4390b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddMoveRemoveTest.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.core; + +import java.util.Random; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.integration.random.operation.Operation; +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; + +/** + * ConcurrentAddMoveRemoveTest performs a test with 5 threads which + * concurrently add, (workspace) move and remove nodes. This test is intended to + * make sure these concurrent actions don't lead to inconsistencies in the database. + * See also: JCR-3292. + */ +public class ConcurrentAddMoveRemoveTest extends AbstractConcurrencyTest { + + private static final int RUN_NUM_SECONDS = 20; + + private long end; + + private String folder1Path; + private String folder2Path; + + protected void setUp() throws Exception { + super.setUp(); + end = System.currentTimeMillis() + RUN_NUM_SECONDS * 1000; + folder1Path = testRootNode.addNode("folder1").getPath(); + folder2Path = testRootNode.addNode("folder2").getPath(); + testRootNode.getSession().save(); + } + + @Override + public void tearDown() throws Exception { + ConsistencyReport consistencyReport = TestHelper.checkConsistency(testRootNode.getSession(), false, null); + //for (ReportItem item : consistencyReport.getItems()) { + // System.out.println(item.getMessage()); + //} + assertTrue(consistencyReport.getItems().size() == 0); + super.tearDown(); + } + + public void testRandomOperations() throws RepositoryException { + runTask(new AddMoveRemoveTask(end, folder1Path, folder2Path), 5, testRootNode.getPath()); + } + + public static class AddMoveRemoveTask implements Task { + + private final long end; + private final String folder1Path; + private final String folder2Path; + + private final Random random = new Random(); + + public AddMoveRemoveTask(long end, String folder1Path, String folder2Path) { + this.end = end; + this.folder1Path = folder1Path; + this.folder2Path = folder2Path; + } + + public void execute(final Session session, final Node test) throws RepositoryException { + while (end > System.currentTimeMillis()) { + try { + getRandomOperation(session).execute(); + } catch (RepositoryException e) { + // RepositoryExceptions are expected during concurrent actions. + session.refresh(false); + } catch (Exception e) { + // only a RepositoryException is allowed from a Task + throw new RepositoryException("Failure during concurrent execution", e); + } + } + } + + private Operation getRandomOperation(Session session) throws RepositoryException { + switch (random.nextInt(3)) { + case 0: + return new Add(session, getRandomParent()); + case 1: { + String folderPath = getRandomParent(); + return new WorkspaceMove(session, getRandomChild(session, folderPath), + folderPath.equals(folder1Path) ? folder2Path : folder1Path); + } + default: { + String folderPath = getRandomParent(); + return new Remove(session, getRandomChild(session, folderPath)); + } + } + } + + private String getRandomChild(Session session, String parentPath) throws RepositoryException { + Node parent = session.getNode(parentPath); + NodeIterator nodes = parent.getNodes(); + int size = (int) nodes.getSize(); + if (size > 0) { + int i = 1; + int offset = random.nextInt(size); + while (nodes.hasNext()) { + if (i == offset) { + return nodes.nextNode().getPath(); + } + nodes.nextNode(); + i++; + } + } + return null; + } + + private String getRandomParent() { + switch (random.nextInt(2)) { + case 0: + return folder1Path; + default: + return folder2Path; + } + } + + private class Add extends Operation { + + private final String name; + + public Add(Session s, String folderPath) { + super(s, folderPath); + this.name = getRandomText(4); + } + + @Override + public NodeIterator execute() throws Exception { + getNode().addNode(name); + getSession().save(); + return null; + } + } + + private class Remove extends Operation { + + public Remove(Session s, String path) { + super(s, path); + } + + @Override + public NodeIterator execute() throws Exception { + if (getPath() != null) { + getNode().remove(); + getSession().save(); + } + return null; + } + + } + + private class WorkspaceMove extends Operation { + + private final String folderPath; + + public WorkspaceMove(Session s, String path, String folderPath) { + super(s, path); + this.folderPath = folderPath; + } + + @Override + public NodeIterator execute() throws Exception { + if (getPath() != null) { + String destination = folderPath + "/" + getRandomText(4); + getSession().getWorkspace().move(getPath(), destination); + } + return null; + } + + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemoveMoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemoveMoveTest.java new file mode 100644 index 00000000000..f95f377a542 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemoveMoveTest.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.core; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; + +public final class ConcurrentAddRemoveMoveTest extends ConcurrentModificationBase { + + /** + * {@inheritDoc} + */ + public void setUp() throws Exception { + super.setUp(); + testRootNode.addNode("A").addNode("B"); + testRootNode.addNode("C"); + testRootNode.getSession().save(); + } + + public void testAddWithMoveFrom() throws Exception { + testRootNode.getNode("A").addNode("D"); + session.move(testRoot + "/A/B", testRoot + "/C/B"); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + public void testAddWithMoveTo() throws Exception { + testRootNode.getNode("A").addNode("D"); + session.move(testRoot + "/C", testRoot + "/A/C"); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + public void testRemoveWithMoveFrom() throws Exception { + Node d = testRootNode.getNode("A").addNode("D"); + superuser.save(); + d.remove(); + session.move(testRoot + "/A/B", testRoot + "/C/B"); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + public void testRemoveWithMoveTo() throws Exception { + Node d = testRootNode.getNode("A").addNode("D"); + superuser.save(); + d.remove(); + session.move(testRoot + "/C", testRoot + "/A/C"); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + public void testMoveFromWithAdd() throws Exception { + superuser.move(testRoot + "/A/B", testRoot + "/C/B"); + session.getNode(testRoot).getNode("A").addNode("D"); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + public void testMoveToWithAdd() throws Exception { + superuser.move(testRoot + "/C", testRoot + "/A/C"); + session.getNode(testRoot).getNode("A").addNode("D"); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + public void testMoveFromWithRemove() throws Exception { + Node d = session.getNode(testRoot).getNode("A").addNode("D"); + session.save(); + + superuser.move(testRoot + "/A/B", testRoot + "/C/B"); + d.remove(); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + public void testMoveToWithRemove() throws Exception { + Node d = session.getNode(testRoot).getNode("A").addNode("D"); + session.save(); + + superuser.move(testRoot + "/C", testRoot + "/A/C"); + d.remove(); + + testRootNode.getSession().save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + //-------------------------< concurrent add >------------------------------- + + public void testAddAdd() throws Exception { + testRootNode.getNode("A").addNode("D"); + session.getNode(testRoot).getNode("A").addNode("E"); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } + + //-------------------------< concurrent remove >---------------------------- + + public void testRemoveRemove() throws Exception { + Node d = testRootNode.getNode("A").addNode("D"); + superuser.save(); + d.remove(); + session.getNode(testRoot).getNode("A").getNode("B").remove(); + + superuser.save(); + + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw exception"); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemoveNodeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemoveNodeTest.java new file mode 100644 index 00000000000..784dc3874eb --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemoveNodeTest.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.core; + +import java.util.concurrent.atomic.AtomicInteger; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * ConcurrentAddRemoveNodeTest checks if concurrent modifications + * on a node are properly handled. Adding and removing distinct child nodes + * by separate sessions must succeed. + */ +public class ConcurrentAddRemoveNodeTest extends AbstractConcurrencyTest { + + private final AtomicInteger count = new AtomicInteger(); + + public void testAddRemove() throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + String name = "node-" + count.getAndIncrement(); + for (int i = 0; i < 10; i++) { + if (test.hasNode(name)) { + test.getNode(name).remove(); + } else { + test.addNode(name); + } + session.save(); + } + } + }, 10, testRootNode.getPath()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemovePropertyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemovePropertyTest.java new file mode 100644 index 00000000000..7c2281ac6bd --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentAddRemovePropertyTest.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.core; + +import javax.jcr.Node; +import javax.jcr.InvalidItemStateException; + +/** + * ConcurrentAddRemovePropertyTest checks if concurrently adding + * and removing properties does not throw an InvalidItemStateException. + */ +public class ConcurrentAddRemovePropertyTest extends ConcurrentModificationBase { + + public void testAdd() throws Exception { + Node n = testRootNode.addNode(nodeName1); + superuser.save(); + n.setProperty(propertyName1, "foo"); + session.getNode(testRoot).getNode(nodeName1).setProperty(propertyName2, "bar"); + superuser.save(); + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw InvalidItemStateException"); + } + } + + public void testAddSameName() throws Exception { + Node n = testRootNode.addNode(nodeName1); + superuser.save(); + n.setProperty(propertyName1, "foo"); + session.getNode(testRoot).getNode(nodeName1).setProperty(propertyName1, "bar"); + superuser.save(); + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw InvalidItemStateException"); + } + assertEquals("bar", n.getProperty(propertyName1).getString()); + } + + public void testRemove() throws Exception { + Node n = testRootNode.addNode(nodeName1); + n.setProperty(propertyName1, "foo"); + n.setProperty(propertyName2, "bar"); + superuser.save(); + n.getProperty(propertyName1).remove(); + session.getNode(testRoot).getNode(nodeName1).getProperty(propertyName2).remove(); + superuser.save(); + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw InvalidItemStateException"); + } + } + + public void testRemoveSameName() throws Exception { + Node n = testRootNode.addNode(nodeName1); + n.setProperty(propertyName1, "foo"); + superuser.save(); + n.getProperty(propertyName1).remove(); + session.getNode(testRoot).getNode(nodeName1).getProperty(propertyName1).remove(); + superuser.save(); + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw InvalidItemStateException"); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentCheckinMixedTransactionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentCheckinMixedTransactionTest.java new file mode 100644 index 00000000000..582169ff6d8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentCheckinMixedTransactionTest.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.core; + +import org.apache.jackrabbit.core.state.StaleItemStateException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.InvalidItemStateException; +import javax.transaction.NotSupportedException; +import javax.transaction.SystemException; +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.RollbackException; + +/** + * ConcurrentCheckinMixedTransactionTest performs concurrent + * version operations with only some threads using XATransactions. + */ +public class ConcurrentCheckinMixedTransactionTest + extends AbstractConcurrencyTest { + + private static final int NUM_THREADS = 10; + + private static final int RUN_NUM_SECONDS = 20; + + public void testCheckInOut() throws RepositoryException { + final long end = System.currentTimeMillis() + RUN_NUM_SECONDS * 1000; + // tasks with even ids run within transactions + final int[] taskId = new int[1]; + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + int id; + synchronized (ConcurrentCheckinMixedTransactionTest.this) { + id = taskId[0]++; + } + int i = 0; + while (end > System.currentTimeMillis()) { + UserTransactionImpl uTx = null; + try { + if (id % 2 == 0) { + uTx = new UserTransactionImpl(session); + uTx.begin(); + } + Node n = test.addNode("node" + i++); + n.addMixin(mixVersionable); + session.save(); + n.checkout(); + n.checkin(); + if (uTx != null) { + uTx.commit(); + } + } catch (NotSupportedException e) { + throw new RepositoryException(e); + } catch (SystemException e) { + throw new RepositoryException(e); + } catch (HeuristicMixedException e) { + throw new RepositoryException(e); + } catch (HeuristicRollbackException e) { + throw new RepositoryException(e); + } catch (RollbackException e) { + Throwable t = e; + do { + t = t.getCause(); + if (t instanceof StaleItemStateException) { + break; + } + } while (t != null); + if (t == null) { + throw new RepositoryException(e); + } + } catch (InvalidItemStateException e) { + // try again + } + } + } + }, NUM_THREADS); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentCyclicMoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentCyclicMoveTest.java new file mode 100644 index 00000000000..1ad3bb48594 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentCyclicMoveTest.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.core; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +public class ConcurrentCyclicMoveTest extends AbstractJCRTest { + + private String testRootPath; + + public void testConcurrentSessionMove() throws RepositoryException { + + testRootPath = testRootNode.getPath(); + Node aa = testRootNode.addNode("a").addNode("aa"); + Node b = testRootNode.addNode("b"); + testRootNode.getSession().save(); + + String aaId = aa.getIdentifier(); + String bId= b.getIdentifier(); + + Session session1 = getHelper().getReadWriteSession(); + Session session2 = getHelper().getReadWriteSession(); + + // results in /b/a/aa + session1.move(testRootPath + "/a", testRootPath + "/b/a"); + assertEquals(testRootPath + "/b/a/aa", session1.getNodeByIdentifier(aaId).getPath()); + + // results in a/aa/b + session2.move(testRootPath + "/b", testRootPath + "/a/aa/b"); + assertEquals(testRootPath + "/a/aa/b", session2.getNodeByIdentifier(bId).getPath()); + + session1.save(); + + try { + session2.getNodeByIdentifier(bId).getPath(); + fail("It should not be possible to access a cyclic path"); + } catch (InvalidItemStateException expected) { + } + + try { + session2.save(); + fail("Save should have failed. Possible cyclic persistent path created."); + } catch (InvalidItemStateException expected) { + } + } + + + public void testConcurrentWorkspaceMove() throws RepositoryException { + + testRootPath = testRootNode.getPath(); + testRootNode.addNode("b"); + Node aa = testRootNode.addNode("a").addNode("aa"); + testRootNode.getSession().save(); + + String aaId = aa.getIdentifier(); + + Session session1 = getHelper().getReadWriteSession(); + Session session2 = getHelper().getReadWriteSession(); + + // results in /b/a/aa + session1.getWorkspace().move(testRootPath + "/a", testRootPath + "/b/a"); + assertEquals(testRootPath + "/b/a/aa", session1.getNodeByIdentifier(aaId).getPath()); + + // try to move b into a/aa (should fail as the above move is persisted + try { + session2.getWorkspace().move(testRootPath + "/b", testRootPath + "/a/aa/b"); + fail("Workspace.move() should not have succeeded. Possible cyclic path created."); + } catch (PathNotFoundException e) { + // expected. + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentImportTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentImportTest.java new file mode 100644 index 00000000000..57f818f194d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentImportTest.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.test.NotExecutableException; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * ConcurrentVersioningTest contains test cases that run version + * operations with concurrent threads. + */ +public class ConcurrentImportTest extends AbstractConcurrencyTest { + + private static final Attributes EMPTY_ATTRS = new AttributesImpl(); + + /** + * The number of threads. + */ + private static final int CONCURRENCY = 4; + + /** + * The total number of operations to execute. E.g. number of checkins + * performed by the threads. + */ + private static final int NUM_NODES = 10; + + public void testConcurrentImport() throws RepositoryException { + try { + concurrentImport(new String[]{JcrConstants.MIX_REFERENCEABLE}, false); + } + finally { + checkConsistency(); + } + } + + public void testConcurrentImportSynced() throws RepositoryException { + concurrentImport(new String[]{JcrConstants.MIX_REFERENCEABLE}, true); + } + + public void testConcurrentImportVersionable() throws RepositoryException { + concurrentImport(new String[]{JcrConstants.MIX_VERSIONABLE}, false); + } + + public void testConcurrentImportVersionableSynced() throws RepositoryException { + concurrentImport(new String[]{JcrConstants.MIX_VERSIONABLE}, true); + } + + private void concurrentImport(final String[] mixins, final boolean sync) throws RepositoryException { + final String[] uuids = new String[NUM_NODES]; + for (int i=0; inull + * @param mixins mixins + * @return the new node. + * @throws RepositoryException if an error occurs + */ + public static Node addNode(Node parent, String name, String type, String uuid, String[] mixins) + throws RepositoryException { + try { + final Session session = parent.getSession(); + final ContentHandler handler = session.getImportContentHandler( + parent.getPath(), + ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); + + // first define the current namespaces + String[] prefixes = session.getNamespacePrefixes(); + handler.startDocument(); + for (String prefix : prefixes) { + handler.startPrefixMapping(prefix, session.getNamespaceURI(prefix)); + } + AttributesImpl attrs = new AttributesImpl(); + + attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", "CDATA", name); + handler.startElement(Name.NS_SV_URI, "node", "sv:node", attrs); + + // add the jcr:primaryTye + attrs = new AttributesImpl(); + attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", "CDATA", JcrConstants.JCR_PRIMARYTYPE); + attrs.addAttribute(Name.NS_SV_URI, "type", "sv:type", "CDATA", "Name"); + handler.startElement(Name.NS_SV_URI, "property", "sv:property", attrs); + handler.startElement(Name.NS_SV_URI, "value", "sv:value", EMPTY_ATTRS); + handler.characters(type.toCharArray(), 0, type.length()); + handler.endElement(Name.NS_SV_URI, "value", "sv:value"); + handler.endElement(Name.NS_SV_URI, "property", "sv:property"); + + if (mixins.length > 0) { + // add the jcr:mixinTypes + attrs = new AttributesImpl(); + attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", "CDATA", JcrConstants.JCR_MIXINTYPES); + attrs.addAttribute(Name.NS_SV_URI, "type", "sv:type", "CDATA", "Name"); + handler.startElement(Name.NS_SV_URI, "property", "sv:property", attrs); + for (String mix: mixins) { + handler.startElement(Name.NS_SV_URI, "value", "sv:value", EMPTY_ATTRS); + handler.characters(mix.toCharArray(), 0, mix.length()); + handler.endElement(Name.NS_SV_URI, "value", "sv:value"); + } + handler.endElement(Name.NS_SV_URI, "property", "sv:property"); + } + + // add the jcr:uuid + if (uuid != null) { + attrs = new AttributesImpl(); + attrs.addAttribute(Name.NS_SV_URI, "name", "sv:name", "CDATA", JcrConstants.JCR_UUID); + attrs.addAttribute(Name.NS_SV_URI, "type", "sv:type", "CDATA", "String"); + handler.startElement(Name.NS_SV_URI, "property", "sv:property", attrs); + handler.startElement(Name.NS_SV_URI, "value", "sv:value", EMPTY_ATTRS); + handler.characters(uuid.toCharArray(), 0, uuid.length()); + handler.endElement(Name.NS_SV_URI, "value", "sv:value"); + handler.endElement(Name.NS_SV_URI, "property", "sv:property"); + } + + + handler.endElement(Name.NS_SV_URI, "node", "sv:node"); + handler.endDocument(); + + return parent.getNode(name); + + } catch (SAXException e) { + Exception root = e.getException(); + if (root instanceof RepositoryException) { + throw (RepositoryException) root; + } else if (root instanceof RuntimeException) { + throw (RuntimeException) root; + } else { + throw new RepositoryException("Error while creating node", root); + } + } + + } + + private void checkConsistency() throws RepositoryException { + try { + ConsistencyReport rep = TestHelper.checkConsistency(testRootNode.getSession(), false, null); + assertEquals("Found broken nodes in repository: " + rep, 0, rep.getItems().size()); + } catch (NotExecutableException ex) { + // ignore + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentLoginTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentLoginTest.java new file mode 100644 index 00000000000..fbb117e533b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentLoginTest.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.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Credentials; +import java.util.ArrayList; +import java.util.List; +import java.util.Iterator; + +/** + * ConcurrentLoginTest starts multiple threads and repeatedly does + * a {@link javax.jcr.Repository#login(javax.jcr.Credentials)}. + */ +public class ConcurrentLoginTest extends AbstractJCRTest { + + /** + * The number of threads. + */ + private static final int NUM_THREADS = 10; + + /** + * The number of login calls each thread will do in a test run. + */ + private static final int NUM_LOGINS_PER_THREAD = 100; + + /** + * Tests concurrent logins on the Repository. + */ + public void testLogin() throws RepositoryException { + final Exception[] exception = new Exception[1]; + List testRunner = new ArrayList(); + for (int i = 0; i < NUM_THREADS; i++) { + testRunner.add(new Thread(new Runnable() { + public void run() { + Credentials cred = getHelper().getSuperuserCredentials(); + for (int i = 0; i < NUM_LOGINS_PER_THREAD; i++) { + try { + Session s = getHelper().getRepository().login(cred); + // immediately logout + s.logout(); + } catch (Exception e) { + exception[0] = e; + break; + } + } + } + })); + } + + // start threads + for (Iterator it = testRunner.iterator(); it.hasNext(); ) { + ((Thread) it.next()).start(); + } + + // join threads + for (Iterator it = testRunner.iterator(); it.hasNext(); ) { + try { + ((Thread) it.next()).join(); + } catch (InterruptedException e) { + fail(e.toString()); + } + } + + if (exception[0] != null) { + fail(exception[0].toString()); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentMixinModificationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentMixinModificationTest.java new file mode 100644 index 00000000000..733ed936060 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentMixinModificationTest.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.core; + +import javax.jcr.Node; +import javax.jcr.InvalidItemStateException; + +/** + * ConcurrentMixinModificationTest checks if concurrent + * modifications of mixins trigger an InvalidItemStateException. + */ +public class ConcurrentMixinModificationTest extends ConcurrentModificationBase { + + public void testMixin() throws Exception { + Node n = testRootNode.addNode(nodeName1); + superuser.save(); + n.addMixin(mixReferenceable); + session.getNode(testRoot).getNode(nodeName1).addMixin(mixLockable); + superuser.save(); + try { + session.save(); + fail("InvalidItemStateException expected"); + } catch (InvalidItemStateException e) { + // expected + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentModificationBase.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentModificationBase.java new file mode 100644 index 00000000000..0c966831d6f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentModificationBase.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.core; + +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * ConcurrentModificationBase is a base class for concurrent + * modification tests. + */ +public abstract class ConcurrentModificationBase extends AbstractJCRTest { + + protected Session session; + + protected void setUp() throws Exception { + super.setUp(); + session = getHelper().getSuperuserSession(); + } + + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentModificationWithSNSTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentModificationWithSNSTest.java new file mode 100644 index 00000000000..de439c077a0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentModificationWithSNSTest.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.core; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; + +/** + * ConcurrentModificationWithSNSTest checks if interleaving node + * modifications with same name siblings do not throw InvalidItemStateException. + */ +public class ConcurrentModificationWithSNSTest extends ConcurrentModificationBase { + + protected void setUp() throws Exception { + super.setUp(); + testRootNode.addNode("A"); + testRootNode.addNode("A"); + testRootNode.addNode("A"); + superuser.save(); + } + + public void testAddAdd() throws Exception { + testRootNode.addNode("A"); + session.getNode(testRoot).addNode("A"); + superuser.save(); + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw InvalidItemStateException"); + } + } + + public void testAddRemove() throws Exception { + testRootNode.addNode("A"); + session.getNode(testRoot).getNode("A[2]").remove(); + superuser.save(); + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw InvalidItemStateException"); + } + } + + public void testRemoveAdd() throws Exception { + testRootNode.getNode("A[2]").remove(); + session.getNode(testRoot).addNode("A"); + superuser.save(); + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw InvalidItemStateException"); + } + } + + public void testRemoveRemove() throws Exception { + testRootNode.getNode("A[1]").remove(); + session.getNode(testRoot).getNode("A[3]").remove(); + superuser.save(); + try { + session.save(); + } catch (InvalidItemStateException e) { + fail("must not throw InvalidItemStateException"); + } + } + + public void testSNSNotAllowed() throws Exception { + cleanUpTestRoot(superuser); + Node f = testRootNode.addNode("folder", "nt:folder"); + superuser.save(); + f.addNode("A", "nt:folder"); + session.getNode(f.getPath()).addNode("A", "nt:folder"); + superuser.save(); + try { + session.save(); + fail("InvalidItemStateException expected"); + } catch (InvalidItemStateException e) { + // expected + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentMoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentMoveTest.java new file mode 100644 index 00000000000..3ebde75429a --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentMoveTest.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.core; + +import javax.jcr.InvalidItemStateException; + +/** + * ConcurrentMoveTest... + */ +public class ConcurrentMoveTest extends ConcurrentModificationBase { + + protected String srcAbsPath; + + protected String destAbsPath1; + + protected String destAbsPath2; + + protected void setUp() throws Exception { + super.setUp(); + srcAbsPath = testRootNode.addNode("A").getPath(); + destAbsPath1 = testRootNode.addNode("B").getPath() + "/D"; + destAbsPath2 = testRootNode.addNode("C").getPath() + "/D"; + superuser.save(); + } + + public void testMove() throws Exception { + superuser.move(srcAbsPath, destAbsPath1); + session.move(srcAbsPath, destAbsPath2); + superuser.save(); + try { + session.save(); + fail("InvalidItemStateException expected"); + } catch (InvalidItemStateException e) { + // expected + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentNodeModificationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentNodeModificationTest.java new file mode 100644 index 00000000000..887fa09fb68 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentNodeModificationTest.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.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +/** + * Performs a test with n sessions concurrently performing non-conflicting + * modifications on the same node. + *

    + * See http://issues.apache.org/jira/browse/JCR-584. + */ +public class ConcurrentNodeModificationTest extends AbstractJCRTest { + + /** + * Logger instance. + */ + private static final Logger log = + LoggerFactory.getLogger(ConcurrentNodeModificationTest.class); + + private static final int NUM_SESSIONS = 100; + private static final int NUM_ITERATIONS = 10; + private static final int NUM_NODES = 10; + + private volatile boolean success; + + /** + * Runs the test. + */ + public void testConcurrentNodeModificationSessions() throws Exception { + success = true; + + Thread[] threads = new Thread[NUM_SESSIONS]; + for (int i = 0; i < threads.length; i++) { + TestSession ts = new TestSession("s" + i); + threads[i] = new Thread(ts, "CNMT " + i); + } + for (Thread thread : threads) { + thread.start(); + } + for (Thread thread : threads) { + thread.join(); + } + + assertTrue("Unexpected exceptions during test, see the log file for details", success); + } + + //--------------------------------------------------------< inner classes > + class TestSession implements Runnable { + + private final Session session; + private final String identity; + + TestSession(String identity) throws RepositoryException { + this.session = getHelper().getSuperuserSession(); + this.identity = identity; + } + + public void run() { + log.debug("started."); + try { + for (int i = 0; success && i < NUM_ITERATIONS; i++) { + runIteration(); + } + } catch (Exception e) { + log.error("Operation failed", e); + success = false; + } finally { + session.logout(); + } + + log.info("ended."); + } + + private void runIteration() throws RepositoryException { + Node n = session.getRootNode().getNode(testPath); + + String propName = "prop_" + identity; + + log.info("setting property {}", propName); + n.setProperty(propName, "Hello World!"); + Thread.yield(); // maximize chances of interference + session.save(); + + log.info("removing property {}", propName); + n.setProperty(propName, (Value) null); + Thread.yield(); // maximize chances of interference + session.save(); + + for (int i = 0; i < NUM_NODES; i++) { + String name = "x_" + identity + "_" + i; + log.info("adding subnode {}", name); + //Node n1 = n.addNode("x" + i, "nt:unstructured"); + Node n1 = n.addNode(name, "nt:unstructured"); + n1.setProperty("testprop", "xxx"); + Thread.yield(); // maximize chances of interference + session.save(); + } + + for (int i = 0; i < NUM_NODES; i++) { + String name = "x_" + identity + "_" + i; + log.info("removing subnode {}", name); + n.getNode(name).remove(); + Thread.yield(); // maximize chances of interference + session.save(); + } + } + + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentReadWriteTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentReadWriteTest.java new file mode 100644 index 00000000000..17db6ef7fba --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentReadWriteTest.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.core; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.PropertyIterator; +import javax.jcr.Property; +import javax.jcr.InvalidItemStateException; +import java.util.List; +import java.util.ArrayList; +import java.util.Random; +import java.util.Collections; + +/** + * ConcurrentReadWriteTest performs a test with a number of + * concurrent readers and one writer. + */ +public class ConcurrentReadWriteTest extends AbstractConcurrencyTest { + + private static final int NUM_NODES = 5; + + private static final int NUM_THREADS = 5; + + private static final int RUN_NUM_SECONDS = 20; + + public void testReadWrite() throws RepositoryException { + final List uuids = new ArrayList(); + for (int i = 0; i < NUM_NODES; i++) { + Node n = testRootNode.addNode("node" + i); + n.addMixin(mixReferenceable); + uuids.add(n.getUUID()); + } + final List exceptions = Collections.synchronizedList(new ArrayList()); + final long[] numReads = new long[]{0}; + testRootNode.save(); + Thread t = new Thread(new Runnable() { + public void run() { + try { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + Random rand = new Random(); + long start = System.currentTimeMillis(); + long reads = 0; + while (System.currentTimeMillis() < start + RUN_NUM_SECONDS * 1000) { + String uuid = (String) uuids.get(rand.nextInt(uuids.size())); + Node n = session.getNodeByUUID(uuid); + try { + for (PropertyIterator it = n.getProperties(); it.hasNext(); ) { + Property p = it.nextProperty(); + if (p.isMultiple()) { + p.getValues(); + } else { + p.getValue(); + } + } + } catch (InvalidItemStateException e) { + // ignore + } + reads++; + } + synchronized (numReads) { + numReads[0] += reads; + } + } + }, NUM_THREADS, testRoot); + } catch (RepositoryException e) { + exceptions.add(e); + } + } + }); + t.start(); + long numWrites = 0; + while (t.isAlive()) { + Random rand = new Random(); + String uuid = (String) uuids.get(rand.nextInt(uuids.size())); + Node n = superuser.getNodeByUUID(uuid); + if (n.hasProperty("test")) { + n.getProperty("test").remove(); + } else { + n.setProperty("test", "hello world"); + } + n.save(); + numWrites++; + } + log.println("#writes performed: " + numWrites); + log.println("#reads performed: " + numReads[0]); + if (!exceptions.isEmpty()) { + fail(((RepositoryException) exceptions.get(0)).getMessage()); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentRenameTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentRenameTest.java new file mode 100644 index 00000000000..e531b63dab6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentRenameTest.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.core; + +import org.apache.jackrabbit.util.Text; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * org.apache.jackrabbit.core.ConcurrentRenameTest... + */ +public class ConcurrentRenameTest extends AbstractConcurrencyTest { + + private static final int NUM_MOVES = 100; + private static final int NUM_THREADS = 2; + + public void testConcurrentRename() throws Exception { + runTask(new Task() { + + public void execute(Session session, Node test) + throws RepositoryException { + String name = Thread.currentThread().getName(); + // create node + Node n = test.addNode(name); + session.save(); + // do moves + for (int i = 0; i < NUM_MOVES; i++) { + String path = n.getPath(); + String newName = name + "-" + i; + String newPath = Text.getRelativeParent(path, 1) + "/" + newName; + session.move(path, newPath); + session.save(); + n = session.getNode(newPath); + } + } + }, NUM_THREADS, testRootNode.getPath()); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentReorderTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentReorderTest.java new file mode 100644 index 00000000000..167edd7ec23 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentReorderTest.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.core; + +import javax.jcr.InvalidItemStateException; + +/** + * ConcurrentReorderTest checks if a reorder interleaved with + * a modification by another session throws an InvalidItemStateException. + */ +public class ConcurrentReorderTest extends ConcurrentModificationBase { + + protected void setUp() throws Exception { + super.setUp(); + testRootNode.addNode("A"); + testRootNode.addNode("B"); + testRootNode.addNode("C"); + superuser.save(); + } + + public void testReorderWithAdd() throws Exception { + testRootNode.orderBefore("C", "A"); + session.getNode(testRoot).addNode("D"); + session.save(); + try { + superuser.save(); + fail("must throw InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // expected + } + } + + public void testAddWithReorder() throws Exception { + testRootNode.addNode("D"); + session.getNode(testRoot).orderBefore("C", "A"); + session.save(); + try { + superuser.save(); + fail("must throw InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // expected + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentSaveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentSaveTest.java new file mode 100644 index 00000000000..4f57ef5969b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentSaveTest.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.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.Value; +import java.util.ArrayList; +import java.util.List; + +/** + * Performs a test with two threads. One thread adds nodes to the testRootNode + * and sets a property. The second thread removes the property as soon as it + * sees a newly created node. + */ +public class ConcurrentSaveTest extends AbstractJCRTest { + + /** logger instance */ + private static final Logger log = LoggerFactory.getLogger(ConcurrentSaveTest.class); + + private final int NUM_NODES = 1000; + private Session addNodeSession; + private Session removePropertySession; + + protected void setUp() throws Exception { + super.setUp(); + addNodeSession = getHelper().getSuperuserSession(); + removePropertySession = getHelper().getSuperuserSession(); + } + + protected void tearDown() throws Exception { + try { + if (addNodeSession != null) { + addNodeSession.logout(); + addNodeSession = null; + } + if (removePropertySession != null) { + removePropertySession.logout(); + removePropertySession = null; + } + } finally { + super.tearDown(); + } + } + + /** + * Runs the test. + */ + public void testConcurrentSave() throws Exception { + + final String path = testPath; + final List exceptions = new ArrayList(); + + Thread addNodeWorker = new Thread() { + public void run() { + try { + Node testNode = addNodeSession.getRootNode().getNode(path); + for (int i = 0; i < NUM_NODES; i++) { + Node n = testNode.addNode("node" + i); + n.setProperty("foo", "some text"); + log.info("creating node: node" + i); + testNode.save(); + log.info("created node: node" + i); + // give other thread a chance to catch up + yield(); + } + } catch (Exception e) { + exceptions.add(e); + } + } + }; + + Thread removePropertyWorker = new Thread() { + public void run() { + try { + Node rootNode = removePropertySession.getRootNode().getNode(path); + for (int i = 0; i < NUM_NODES; i++) { + // wait for node to be created + while (!rootNode.hasNode("node" + i)) { + Thread.sleep(0, 50); + } + Node n = rootNode.getNode("node" + i); + // remove property + n.setProperty("foo", (Value) null); + log.info("removing property from node: node" + i); + n.save(); + log.info("property removed from node: node" + i); + } + } catch (Exception e) { + exceptions.add(e); + } + } + }; + + addNodeWorker.start(); + removePropertyWorker.start(); + addNodeWorker.join(); + removePropertyWorker.join(); + + if (exceptions.size() > 0) { + throw (Exception) exceptions.get(0); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentVersioningTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentVersioningTest.java new file mode 100644 index 00000000000..d76dfbde802 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentVersioningTest.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.core; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.version.Version; +import java.util.List; +import java.util.ArrayList; +import java.util.Random; + +/** + * ConcurrentVersioningTest contains test cases that run version + * operations with concurrent threads. + */ +public class ConcurrentVersioningTest extends AbstractConcurrencyTest { + + /** + * The number of threads. + */ + private static final int CONCURRENCY = 10; + + /** + * The total number of operations to execute. E.g. number of checkins + * performed by the threads. + */ + private static final int NUM_OPERATIONS = 200; + + public void testConcurrentAddVersionable() throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) throws RepositoryException { + // add versionable nodes + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + Node n = test.addNode("test" + i); + n.addMixin(mixVersionable); + session.save(); + } + } + }, CONCURRENCY); + } + + public void testConcurrentCheckin() throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) throws RepositoryException { + Node n = test.addNode("test"); + n.addMixin(mixVersionable); + session.save(); + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + n.checkout(); + n.checkin(); + } + n.checkout(); + } + }, CONCURRENCY); + } + + public void testConcurrentCreateAndCheckinCheckout() throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) throws RepositoryException { + // add versionable nodes + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + Node n = test.addNode("test" + i); + n.addMixin(mixVersionable); + session.save(); + n.checkout(); + n.checkin(); + n.checkout(); + } + } + }, CONCURRENCY); + } + + public void testConcurrentRestore() throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) throws RepositoryException { + Node n = test.addNode("test"); + n.addMixin(mixVersionable); + session.save(); + // create 3 version + List versions = new ArrayList(); + for (int i = 0; i < 3; i++) { + n.checkout(); + versions.add(n.checkin()); + } + // do random restores + Random rand = new Random(); + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + Version v = (Version) versions.get(rand.nextInt(versions.size())); + n.restore(v, true); + } + n.checkout(); + } + }, CONCURRENCY); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentVersioningWithTransactionsTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentVersioningWithTransactionsTest.java new file mode 100644 index 00000000000..ff349ec4a27 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentVersioningWithTransactionsTest.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.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.InvalidItemStateException; +import javax.jcr.version.Version; +import javax.transaction.UserTransaction; + +import org.apache.jackrabbit.core.state.StaleItemStateException; + +/** + * ConcurrentVersioningTest contains test cases that run version + * operations with concurrent threads. + */ +public class ConcurrentVersioningWithTransactionsTest extends AbstractConcurrencyTest { + + /** + * The number of threads. + */ + private static final int CONCURRENCY = 100; + + /** + * The total number of operations to execute. E.g. number of checkins + * performed by the threads. + */ + private static final int NUM_OPERATIONS = 100; + + /** + * Creates the named test node. The node is created within a transaction + * to avoid mixing transactional and non-transactional writes within a + * concurrent test run. + */ + private static synchronized Node createParentNode(Node test, String name) + throws RepositoryException { + try { + UserTransaction utx = new UserTransactionImpl(test.getSession()); + utx.begin(); + Node parent = test.addNode(name); + test.save(); + utx.commit(); + return parent; + } catch (RepositoryException e) { + throw e; + } catch (Exception e) { + throw new RepositoryException("Failed to add node: " + name, e); + } + } + + public void testConcurrentAddVersionableInTransaction() + throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + // add versionable nodes + final String threadName = Thread.currentThread().getName(); + Node parent = createParentNode(test, threadName); + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + try { + final UserTransaction utx = new UserTransactionImpl(test.getSession()); + utx.begin(); + final String nodeName = "test" + i; + Node n = parent.addNode(nodeName); + n.addMixin(mixVersionable); + session.save(); + utx.commit(); + } catch (InvalidItemStateException e) { + // ignore + } catch (Exception e) { + final Throwable deepCause = getLevel2Cause(e); + if (deepCause != null && deepCause instanceof StaleItemStateException) { + // ignore + } else { + throw new RepositoryException(threadName + ", i=" + i + ":" + e.getClass().getName(), e); + } + } + } + } + }, CONCURRENCY); + } + + public void testConcurrentCheckinInTransaction() + throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + int i = 0; + try { + Node n = test.addNode("test"); + n.addMixin(mixVersionable); + session.save(); + for (i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + final UserTransaction utx = new UserTransactionImpl(test.getSession()); + utx.begin(); + n.checkout(); + n.checkin(); + utx.commit(); + } + n.checkout(); + } catch (Exception e) { + final String threadName = Thread.currentThread().getName(); + final Throwable deepCause = getLevel2Cause(e); + if (deepCause != null && deepCause instanceof StaleItemStateException) { + // ignore + } else { + throw new RepositoryException(threadName + ", i=" + i + ":" + e.getClass().getName(), e); + } + } + } + }, CONCURRENCY); + } + + public void testConcurrentCreateAndCheckinCheckoutInTransaction() + throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + // add versionable nodes + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + try { + final UserTransaction utx = new UserTransactionImpl(test.getSession()); + utx.begin(); + Node n = test.addNode("test" + i); + n.addMixin(mixVersionable); + session.save(); + n.checkout(); + n.checkin(); + n.checkout(); + utx.commit(); + } catch (Exception e) { + final String threadName = Thread.currentThread().getName(); + final Throwable deepCause = getLevel2Cause(e); + if (deepCause != null && deepCause instanceof StaleItemStateException) { + // ignore + } else { + throw new RepositoryException(threadName + ", i=" + i + ":" + e.getClass().getName(), e); + } + } + } + } + }, CONCURRENCY); + } + + public void testConcurrentRestoreInTransaction() + throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + int i = 0; + try { + Node n = test.addNode("test"); + n.addMixin(mixVersionable); + session.save(); + // create 3 version + List versions = new ArrayList(); + for (i = 0; i < 3; i++) { + n.checkout(); + versions.add(n.checkin()); + } + // do random restores + Random rand = new Random(); + for (i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + Version v = (Version) versions.get(rand.nextInt(versions.size())); + n.restore(v, true); + } + n.checkout(); + } catch (Exception e) { + final String threadName = Thread.currentThread().getName(); + final Throwable deepCause = getLevel2Cause(e); + if (deepCause != null && deepCause instanceof StaleItemStateException) { + // ignore + } else { + throw new RepositoryException(threadName + ", i=" + i + ":" + e.getClass().getName(), e); + } + } + } + }, CONCURRENCY); + } + + private static Throwable getLevel2Cause(Throwable t) { + Throwable result = null; + try { + result = t.getCause().getCause(); + } catch (NullPointerException npe) { + // ignore, we have no deep cause + } + return result; + + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentWorkspaceCopyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentWorkspaceCopyTest.java new file mode 100644 index 00000000000..a469399e7a0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConcurrentWorkspaceCopyTest.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.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import java.util.Random; + + +public class ConcurrentWorkspaceCopyTest extends AbstractJCRTest { + + private static final int NUM_ITERATIONS = 40; + private static final int NUM_SESSIONS = 2; + + static final String TARGET_NAME = "copy of src"; + String sourcePath; + String destParentPath; + String destPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create a parent node where allowSameNameSiblings is set to false + destParentPath = + testRootNode.addNode("destParent", + "nt:folder").getPath(); + destPath = destParentPath + "/" + TARGET_NAME; + // create a source node + sourcePath = testRootNode.addNode("src", "nt:folder").getPath(); + + testRootNode.getSession().save(); + } + + public void testConcurrentCopy() throws Exception { + for (int n = 0; n < NUM_ITERATIONS; n++) { + // cleanup + while (superuser.nodeExists(destPath)) { + superuser.getNode(destPath).remove(); + superuser.save(); + } + + Thread[] threads = new Thread[NUM_SESSIONS]; + for (int i = 0; i < threads.length; i++) { + // create new session + Session session = getHelper().getSuperuserSession(); + String id = "session#" + i; + TestSession ts = new TestSession(id, session); + Thread t = new Thread(ts); + t.setName(id); + t.start(); + threads[i] = t; + } + for (int i = 0; i < threads.length; i++) { + threads[i].join(); + } + + NodeIterator results = superuser.getNode(destParentPath).getNodes(TARGET_NAME); + + assertEquals(1, results.getSize()); + } + } + + + // -------------------------------------------------------< inner classes > + class TestSession implements Runnable { + + Session session; + String identity; + Random r; + + TestSession(String identity, Session s) { + session = s; + this.identity = identity; + r = new Random(); + } + + private void randomSleep() { + long l = r.nextInt(90) + 20; + try { + Thread.sleep(l); + } catch (InterruptedException ie) { + } + } + + public void run() { + + try { + session.getWorkspace().copy(sourcePath, destPath); + session.save(); + + randomSleep(); + } catch (RepositoryException e) { + // expected + } finally { + session.logout(); + } + + } + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConsistencyCheck.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConsistencyCheck.java new file mode 100644 index 00000000000..05d5ccfac19 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ConsistencyCheck.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.core; + +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.LogPrintWriter; + +public class ConsistencyCheck extends AbstractJCRTest { + + private LogPrintWriter log = new LogPrintWriter(logger); + + // Why are we running these twice? + public void testDo1() throws Exception { + runCheck(); + } + + // ...because AbstractJCRTests iterates through multiple test repositories. + // this way, we should check at least two of them. Yes, this is a hack. + public void testDo2() throws Exception { + runCheck(); + } + + private void runCheck() throws Exception { + log.print("running consistency check on repository " + + getHelper().getRepository()); + + ConsistencyReport rep = TestHelper.checkConsistency(testRootNode.getSession(), false, null); + assertEquals("Found broken nodes in repository: " + rep, 0, rep.getItems().size()); + + rep = TestHelper.checkVersionStoreConsistency(testRootNode.getSession(), false, null); + assertEquals("Found broken nodes in version storage: " + rep, 0, rep.getItems().size()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/InvalidDateTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/InvalidDateTest.java new file mode 100644 index 00000000000..859ae867ed7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/InvalidDateTest.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.core; + +import java.util.Calendar; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.ValueFormatException; +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * InvalidDateTest contains tests for JCR-1996. + */ +public class InvalidDateTest extends AbstractJCRTest { + + public void testDateRange() throws RepositoryException { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.YEAR, 20000); + Node n = testRootNode.addNode(nodeName1); + try { + superuser.getValueFactory().createValue(cal); + fail("must throw IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + try { + n.setProperty(propertyName1, cal); + fail("must throw ValueFormatException"); + } catch (ValueFormatException e) { + // expected + } + String calString = superuser.getValueFactory().createValue( + Calendar.getInstance()).getString(); + calString = "1" + calString; + try { + n.setProperty(propertyName1, calString, PropertyType.DATE); + fail("must throw ValueFormatException"); + } catch (ValueFormatException e) { + // expected + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockTest.java new file mode 100644 index 00000000000..6a9ded1c2e6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockTest.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.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.util.Locked; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.Property; +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Random; + +/** + * LockTest tests the utility + * {@link org.apache.jackrabbit.util.Locked}. + */ +public class LockTest extends AbstractJCRTest { + + private static final int NUM_THREADS = 100; + + private static final int NUM_CHANGES = 10; + + private static final int NUM_VALUE_GETS = 10; + + /** + * Tests the utility {@link org.apache.jackrabbit.util.Locked} by + * implementing running multiple threads concurrently that apply changes to + * a lockable node. + */ + public void testLockUtility() throws RepositoryException { + final Node lockable = testRootNode.addNode(nodeName1); + lockable.addMixin(mixLockable); + superuser.save(); + + final List worker = new ArrayList(); + for (int i = 0; i < NUM_THREADS; i++) { + worker.add(new Thread() { + + private final int threadNumber = worker.size(); + + public void run() { + final Session s; + try { + s = getHelper().getSuperuserSession(); + } catch (RepositoryException e) { + fail(e.getMessage()); + return; + } + try { + for (int i = 0; i < NUM_CHANGES; i++) { + Node n = (Node) s.getItem(lockable.getPath()); + new Locked() { + protected Object run(Node n) + throws RepositoryException { + String nodeName = "node" + threadNumber; + if (n.hasNode(nodeName)) { + n.getNode(nodeName).remove(); + } else { + n.addNode(nodeName); + } + s.save(); + log.println("Thread" + threadNumber + + ": saved modification"); + + return null; + } + }.with(n, false); + // do a random wait + Thread.sleep(new Random().nextInt(100)); + } + } catch (RepositoryException e) { + log.println("exception while running code with lock:" + + e.getMessage()); + } catch (InterruptedException e) { + log.println(Thread.currentThread() + + " interrupted while waiting for lock"); + } finally { + s.logout(); + } + } + }); + } + + for (Iterator it = worker.iterator(); it.hasNext();) { + it.next().start(); + } + + for (Iterator it = worker.iterator(); it.hasNext();) { + try { + it.next().join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * Tests the utility {@link org.apache.jackrabbit.util.Locked} by + * implementing a persistent counter. + */ + public void testSequence() throws RepositoryException { + final Node counter = testRootNode.addNode(nodeName1); + counter.setProperty("value", 0); + counter.addMixin(mixLockable); + superuser.save(); + + final List worker = new ArrayList(); + for (int i = 0; i < NUM_THREADS; i++) { + worker.add(new Thread() { + + private final int threadNumber = worker.size(); + + public void run() { + final Session s; + try { + s = getHelper().getSuperuserSession(); + } catch (RepositoryException e) { + fail(e.getMessage()); + return; + } + try { + for (int i = 0; i < NUM_VALUE_GETS; i++) { + Node n = (Node) s.getItem(counter.getPath()); + long currentValue = ((Long) new Locked() { + protected Object run(Node n) + throws RepositoryException { + Property seqProp = n.getProperty("value"); + long value = seqProp.getLong(); + seqProp.setValue(++value); + s.save(); + return new Long(value); + } + }.with(n, false)).longValue(); + log.println("Thread" + threadNumber + + ": got sequence number: " + currentValue); + // do a random wait + Thread.sleep(new Random().nextInt(100)); + } + } catch (RepositoryException e) { + log.println("exception while running code with lock:" + + e.getMessage()); + } catch (InterruptedException e) { + log.println(Thread.currentThread() + + " interrupted while waiting for lock"); + } finally { + s.logout(); + } + } + }); + } + + for (Iterator it = worker.iterator(); it.hasNext();) { + it.next().start(); + } + + for (Iterator it = worker.iterator(); it.hasNext();) { + try { + it.next().join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * Tests the utility {@link org.apache.jackrabbit.util.Locked} by + * implementing a persistent counter with a timeout when the next value of + * the counter is retrieved. The number of values that can be retrieved by + * this test depends on system performance and the configured persistence + * manager. + */ + public void testSequenceWithTimeout() throws RepositoryException { + final Node counter = testRootNode.addNode(nodeName1); + counter.setProperty("value", 0); + counter.addMixin(mixLockable); + superuser.save(); + + final List worker = new ArrayList(); + for (int i = 0; i < NUM_THREADS; i++) { + worker.add(new Thread() { + + private final int threadNumber = worker.size(); + + public void run() { + final Session s; + try { + s = getHelper().getSuperuserSession(); + } catch (RepositoryException e) { + fail(e.getMessage()); + return; + } + try { + for (int i = 0; i < NUM_VALUE_GETS; i++) { + Node n = (Node) s.getItem(counter.getPath()); + Object ret = new Locked() { + protected Object run(Node n) + throws RepositoryException { + Property seqProp = n.getProperty("value"); + long value = seqProp.getLong(); + seqProp.setValue(++value); + s.save(); + return new Long(value); + } + }.with(n, false, 10 * 1000); // expect a value after + // ten seconds + if (ret == Locked.TIMED_OUT) { + log.println("Thread" + + threadNumber + + ": could not get a sequence number within 10 seconds"); + } else { + long currentValue = ((Long) ret).longValue(); + log.println("Thread" + threadNumber + + ": got sequence number: " + + currentValue); + } + // do a random wait + Thread.sleep(new Random().nextInt(100)); + } + } catch (RepositoryException e) { + log.println("exception while running code with lock:" + + e.getMessage()); + } catch (InterruptedException e) { + log.println(Thread.currentThread() + + " interrupted while waiting for lock"); + } finally { + s.logout(); + } + } + }); + } + + for (Iterator it = worker.iterator(); it.hasNext();) { + it.next().start(); + } + + for (Iterator it = worker.iterator(); it.hasNext();) { + try { + it.next().join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockedWrapperTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockedWrapperTest.java new file mode 100644 index 00000000000..46103d56410 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LockedWrapperTest.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.util.LockedWrapper; + +/** + * tests for the utility class {@link org.apache.jackrabbit.util.LockedWrapper}. + */ +public class LockedWrapperTest extends AbstractJCRTest { + + private static final int NUM_THREADS = 100; + + private static final int NUM_CHANGES = 10; + + private static final int NUM_VALUE_GETS = 10; + + private final Random random = new Random(); + + private AtomicInteger counter = new AtomicInteger(0); + + /** + * Tests the utility {@link org.apache.jackrabbit.util.Locked} by + * implementing running multiple threads concurrently that apply changes to + * a lockable node. + */ + public void testConcurrentUpdates() throws RepositoryException, + InterruptedException { + + final Node lockable = testRootNode.addNode(nodeName1); + lockable.addMixin(mixLockable); + superuser.save(); + + List> futures = new ArrayList>(); + ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; i++) { + futures.add(e.submit(buildNewConcurrentUpdateCallable(i, + lockable.getPath(), false))); + } + e.shutdown(); + assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS)); + + for (Future future : futures) { + try { + future.get(); + } catch (ExecutionException ex) { + fail(ex.getMessage()); + } + } + } + + /** + * Tests the utility {@link org.apache.jackrabbit.util.Locked} by + * implementing running multiple threads concurrently that apply changes to + * a lockable node, also refreshing the session on each operation + */ + public void testConcurrentUpdatesWithSessionRefresh() + throws RepositoryException, InterruptedException { + + final Node lockable = testRootNode.addNode(nodeName1); + lockable.addMixin(mixLockable); + superuser.save(); + + List> futures = new ArrayList>(); + ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS); + for (int i = 0; i < NUM_THREADS; i++) { + futures.add(e.submit(buildNewConcurrentUpdateCallable(i, + lockable.getPath(), true))); + } + e.shutdown(); + assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS)); + + for (Future future : futures) { + try { + future.get(); + } catch (ExecutionException ex) { + fail(ex.getMessage()); + } + } + } + + public Callable buildNewConcurrentUpdateCallable( + final int threadNumber, final String lockablePath, + final boolean withSessionRefresh) { + + return new Callable() { + + public Void call() throws RepositoryException, InterruptedException { + + final Session s = getHelper().getSuperuserSession(); + + try { + + for (int i = 0; i < NUM_CHANGES; i++) { + + if (withSessionRefresh) { + s.refresh(false); + } + Node n = s.getNode(lockablePath); + + new LockedWrapper() { + @Override + protected Void run(Node node) + throws RepositoryException { + String nodeName = "node" + threadNumber; + if (node.hasNode(nodeName)) { + node.getNode(nodeName).remove(); + } else { + node.addNode(nodeName); + } + s.save(); + log.println("Thread" + threadNumber + + ": saved modification"); + return null; + } + }.with(n, false); + + // do a random wait + Thread.sleep(random.nextInt(100)); + } + } finally { + s.logout(); + } + return null; + } + + }; + } + + public void testTimeout() throws RepositoryException, InterruptedException { + + final Node lockable = testRootNode.addNode("testTimeout" + + System.currentTimeMillis()); + lockable.addMixin(mixLockable); + superuser.save(); + + ExecutorService e = Executors.newFixedThreadPool(2); + Future lockMaster = e.submit(buildNewTimeoutCallable( + lockable.getPath(), true)); + Future lockSlave = e.submit(buildNewTimeoutCallable( + lockable.getPath(), false)); + e.shutdown(); + + try { + lockMaster.get(); + lockSlave.get(); + } catch (ExecutionException ex) { + if (ex.getCause().getClass().isAssignableFrom(LockException.class)) { + return; + } + fail(ex.getMessage()); + } + fail("was expecting a LockException"); + } + + public Callable buildNewTimeoutCallable(final String lockablePath, + final boolean isLockMaster) { + + return new Callable() { + + public Void call() throws RepositoryException, InterruptedException { + + // allow master to be first to obtain the lock on the node + if (!isLockMaster) { + TimeUnit.SECONDS.sleep(2); + } + + final Session s = getHelper().getSuperuserSession(); + + try { + Node n = s.getNode(lockablePath); + + new LockedWrapper() { + + @Override + protected Void run(Node node) + throws RepositoryException { + + if (isLockMaster) { + try { + TimeUnit.SECONDS.sleep(15); + } catch (InterruptedException e) { + // + } + } + return null; + } + }.with(n, false, 2000); + + } finally { + s.logout(); + } + return null; + } + }; + } + + /** + * Tests concurrent updates on a persistent counter + */ + public void testSequence() throws RepositoryException, InterruptedException { + + counter = new AtomicInteger(0); + + final Node lockable = testRootNode.addNode(nodeName1); + lockable.setProperty("value", counter.getAndIncrement()); + lockable.addMixin(mixLockable); + superuser.save(); + + List> futures = new ArrayList>(); + ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS); + for (int i = 0; i < NUM_THREADS * NUM_VALUE_GETS; i++) { + futures.add(e.submit(buildNewSequenceUpdateCallable( + lockable.getPath(), false))); + } + e.shutdown(); + assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS)); + + for (Future future : futures) { + try { + Long v = future.get(); + log.println("Got sequence number: " + v); + } catch (ExecutionException ex) { + fail(ex.getMessage()); + } + } + } + + /** + * Tests concurrent updates on a persistent counter, with session refresh + */ + public void testSequenceWithSessionRefresh() throws RepositoryException, + InterruptedException { + + counter = new AtomicInteger(0); + + final Node lockable = testRootNode.addNode(nodeName1); + lockable.setProperty("value", counter.getAndIncrement()); + lockable.addMixin(mixLockable); + superuser.save(); + + List> futures = new ArrayList>(); + ExecutorService e = Executors.newFixedThreadPool(NUM_THREADS); + for (int i = 0; i < NUM_THREADS * NUM_VALUE_GETS; i++) { + futures.add(e.submit(buildNewSequenceUpdateCallable( + lockable.getPath(), true))); + } + e.shutdown(); + assertTrue(e.awaitTermination(10 * 60, TimeUnit.SECONDS)); + + for (Future future : futures) { + try { + Long v = future.get(); + log.println("Got sequence number: " + v); + } catch (ExecutionException ex) { + fail(ex.getMessage()); + } + } + } + + public Callable buildNewSequenceUpdateCallable( + final String counterPath, final boolean withSessionRefresh) { + + return new Callable() { + + public Long call() throws RepositoryException, InterruptedException { + + final Session s = getHelper().getSuperuserSession(); + + try { + if (withSessionRefresh) { + s.refresh(false); + } + Node n = s.getNode(counterPath); + + long value = new LockedWrapper() { + + @Override + protected Long run(Node node) + throws RepositoryException { + + Property seqProp = node.getProperty("value"); + long value = seqProp.getLong(); + seqProp.setValue(++value); + s.save(); + return value; + } + }.with(n, false); + + // check that the sequence is ok + assertEquals(counter.getAndIncrement(), value); + + // do a random wait + Thread.sleep(random.nextInt(100)); + + return value; + } finally { + s.logout(); + } + } + }; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LostFromCacheIssueTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LostFromCacheIssueTest.java new file mode 100644 index 00000000000..c5d387b73d1 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/LostFromCacheIssueTest.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.core; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException; +import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +public class LostFromCacheIssueTest extends AbstractJCRTest { + + private static final String NAMESPACE_PREFIX = "LostFromCacheIssueTestNamespacePrefix"; + private static final String NAMESPACE_URI = "http://www.onehippo.org/test/1.0"; + + private static final String TESTNODE_PATH = "/LostFromCacheIssueTest/node"; + + private static final String NODETYPE_1 = NAMESPACE_PREFIX + ":mixin"; + private static final String NODETYPE_2 = NAMESPACE_PREFIX + ":mxn"; + + public Property mixinTypes; + + public void setUp() throws Exception { + + super.setUp(); + + Workspace workspace = superuser.getWorkspace(); + NamespaceRegistry namespaceRegistry = workspace.getNamespaceRegistry(); + NodeTypeManager ntmgr = workspace.getNodeTypeManager(); + NodeTypeRegistry nodetypeRegistry = ((NodeTypeManagerImpl)ntmgr).getNodeTypeRegistry(); + try { + namespaceRegistry.registerNamespace(NAMESPACE_PREFIX, NAMESPACE_URI); + } catch (NamespaceException ignore) { + //already exists + } + QNodeTypeDefinition nodeTypeDefinition = new QNodeTypeDefinitionImpl( + ((SessionImpl)superuser).getQName(NODETYPE_1), + Name.EMPTY_ARRAY, + Name.EMPTY_ARRAY, + true, + false, + true, + false, + null, + QPropertyDefinition.EMPTY_ARRAY, + QNodeDefinition.EMPTY_ARRAY + ); + try { + nodetypeRegistry.registerNodeType(nodeTypeDefinition); + } catch (InvalidNodeTypeDefException ignore) { + //already exists + } + nodeTypeDefinition = new QNodeTypeDefinitionImpl( + ((SessionImpl)superuser).getQName(NODETYPE_2), + Name.EMPTY_ARRAY, + Name.EMPTY_ARRAY, + true, + false, + true, + false, + null, + QPropertyDefinition.EMPTY_ARRAY, + QNodeDefinition.EMPTY_ARRAY + ); + try { + nodetypeRegistry.registerNodeType(nodeTypeDefinition); + } catch (InvalidNodeTypeDefException ignore) { + //already exists + } + + getOrCreate(superuser.getRootNode(), TESTNODE_PATH); + superuser.save(); + } + + private static Node getOrCreate(Node parent, String path) throws RepositoryException { + if (parent == null || path == null || path.trim().isEmpty()) { + throw new IllegalArgumentException("Missing `parent` or `path`"); + } + + String p = path; + if (path.startsWith("/")) { + p = path.substring(1); + } + + Node node = null; + try { + node = parent.getNode(p); + } catch (PathNotFoundException e) { + // swallowing exception + } + + if (node == null) { + // if null is not there and therefore creating it + for (String n : p.split("/")) { + if (node == null) { + node = parent.addNode(n); + } else { + node = node.addNode(n); + } + } + } + + return node; + } + + public void testIssue() throws Exception { + Node node = superuser.getRootNode().getNode(TESTNODE_PATH.substring(1)); + node.addMixin(NODETYPE_2); + mixinTypes = node.getProperty("jcr:mixinTypes"); + superuser.save(); + node.addMixin(NODETYPE_1); + superuser.save(); + node.removeMixin(NODETYPE_2); + node.removeMixin(NODETYPE_1); + superuser.save(); + node.addMixin(NODETYPE_1); + superuser.save(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveAtRootTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveAtRootTest.java new file mode 100644 index 00000000000..98bc0fedd71 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveAtRootTest.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.core; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +public class MoveAtRootTest extends AbstractJCRTest { + + public void testMoveAtRoot() throws RepositoryException { + final String pathA = "/" + getClass().getSimpleName() + "_A"; + final String pathB = "/" + getClass().getSimpleName() + "_B"; + + final String testText = "Hello." + Math.random(); + + Session s = getHelper().getSuperuserSession(); + if (s.itemExists(pathA)) { + s.removeItem(pathA); + s.save(); + } + + assertFalse(s.itemExists(pathA)); + + Node n = s.getRootNode().addNode(pathA.substring(1)); + n.setProperty("text", testText); + + s.save(); + + s.refresh(false); + + assertTrue(s.itemExists(pathA)); + + // Move to pathB + if (s.itemExists(pathB)) { + s.removeItem(pathB); + s.save(); + } + + assertFalse(s.itemExists(pathB)); + + s.move(pathA, pathB); + s.save(); + + s.refresh(false); + + assertFalse(s.itemExists(pathA)); + assertTrue(s.itemExists(pathB)); + + n = (Node) s.getItem(pathB); + assertEquals(testText, n.getProperty("text").getString()); + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveRemoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveRemoveTest.java new file mode 100644 index 00000000000..e62dac68e3c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveRemoveTest.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.core; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; +import org.apache.jackrabbit.core.state.StaleItemStateException; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +public class MoveRemoveTest extends AbstractJCRTest { + + private String folder1Path; + private String folder2Path; + + @Override + public void setUp() throws Exception { + super.setUp(); + Node folder1 = testRootNode.addNode("folder1"); + folder1Path = folder1.getPath(); + Node folder2 = testRootNode.addNode("folder2"); + folder2Path = folder2.getPath(); + folder1.addNode("node"); + testRootNode.getSession().save(); + } + + public void testMoveRemove() throws RepositoryException, NotExecutableException { + Session session1 = getHelper().getSuperuserSession(); + Session session2 = getHelper().getSuperuserSession(); + session1.move(folder1Path + "/node", folder2Path + "/node"); + session2.getNode(folder1Path + "/node").remove(); + session1.save(); + try { + session2.save(); + } catch (InvalidItemStateException e) { + if (e.getCause() == null || e.getCause().getClass() != StaleItemStateException.class) { + throw e; + } + } + ConsistencyReport consistencyReport = TestHelper.checkConsistency(testRootNode.getSession(), false, null); + //for (ReportItem item : consistencyReport.getItems()) { + // System.out.println(item.getMessage()); + //} + assertTrue(consistencyReport.getItems().size() == 0); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveTest.java new file mode 100644 index 00000000000..6d39a275fc6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MoveTest.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.core; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Tests moving, refreshing, and saving nodes. + */ +public class MoveTest extends AbstractJCRTest { + + /** + * Test case for + * JCR-2720 + */ + public void testMoveVisibilityAcrossSessions() throws RepositoryException { + Session session1 = getHelper().getReadWriteSession(); + Session session2 = getHelper().getReadWriteSession(); + + if (session1.itemExists("/foo")) { + session1.removeItem("/foo"); + session1.save(); + } + + session1.getRootNode().addNode("libs").addNode("foo").addNode("install"); + session1.save(); + + assertTrue(session1.itemExists("/libs/foo/install")); + assertFalse(session1.itemExists("/foo")); + + assertTrue(session2.itemExists("/libs/foo/install")); + assertFalse(session2.itemExists("/foo")); + + session1.move("/libs", "/foo"); + session1.save(); + + assertFalse(session1.itemExists("/libs/foo/install")); + + session2.refresh(false); + + assertFalse("JCR-2720", session2.itemExists("/libs/foo/install")); + } + + /** + * Tests moving a node, and then refreshing or saving it. + */ + public void testMove() throws RepositoryException { + doTestMove(true); + doTestMove(false); + } + + private void doTestMove(boolean save) throws RepositoryException { + Session session = testRootNode.getSession(); + for (NodeIterator it = testRootNode.getNodes(); it.hasNext();) { + it.nextNode().remove(); + session.save(); + } + Node node1 = testRootNode.addNode(nodeName1); + Node node2 = node1.addNode(nodeName2); + session.save(); + String from = node2.getPath(); + String to = node1.getParent().getPath() + "/" + nodeName2; + session.move(from, to); + try { + if (save) { + node2.save(); + } else { + node2.refresh(false); + } + fail("Refresh and Save should not work for moved nodes"); + } catch (RepositoryException e) { + // expected + } + session.save(); + NodeIterator it = node2.getParent().getNodes(nodeName2); + assertTrue(it.hasNext()); + it.nextNode(); + assertFalse(it.hasNext()); + node2.getParent().getPath(); + + // for (it = testRootNode.getNodes(); it.hasNext();) { + // System.out.println(it.nextNode().getPath()); + // } + String now = node2.getPath(); + assertEquals(testRootNode.getPath() + "/" + nodeName2, now); + } + + /** + * Test reordering same-name-siblings using move + */ + public void testReorderSameNameSiblingsUsingMove() throws RepositoryException { + Session session = testRootNode.getSession(); + for (NodeIterator it = testRootNode.getNodes(); it.hasNext();) { + it.nextNode().remove(); + session.save(); + } + Node node1 = testRootNode.addNode(nodeName1); + Node node2 = testRootNode.addNode(nodeName1); + String path = node1.getPath(); + + // re-order the nodes using move + session.move(path, path); + + assertEquals(path + "[2]", node1.getPath()); + assertEquals(path, node2.getPath()); + } + + /** + * Verify paths of same name siblings are correct after a (reordering) move + * Issue JCR-1880 + */ + public void testGetPathDoesNotInfluencePathsAfterMove() throws RepositoryException { + doTestMoveWithGetPath(false); + doTestMoveWithGetPath(true); + } + + private void doTestMoveWithGetPath(boolean index) throws RepositoryException { + Session session = testRootNode.getSession(); + for (NodeIterator it = testRootNode.getNodes(); it.hasNext();) { + it.nextNode().remove(); + session.save(); + } + String testPath = testRootNode.getPath(); + Node a = testRootNode.addNode("a"); + Node b = a.addNode("b"); + session.save(); + session.move(testPath + "/a/b", testPath + "/a"); + if (index) { + b.getPath(); + } + session.move(testPath + "/a", testPath + "/a"); + assertEquals(testPath + "/a[2]", a.getPath()); + assertEquals(testPath + "/a", b.getPath()); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MultiWorkspaceShareableNodeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MultiWorkspaceShareableNodeTest.java new file mode 100644 index 00000000000..dc94e7bdd95 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/MultiWorkspaceShareableNodeTest.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * Tests features available with shareable nodes that require multiple + * workspaces. + */ +public class MultiWorkspaceShareableNodeTest extends AbstractJCRTest { + + /** + * The superuser session for the non default workspace + */ + protected Session superuserW2; + + /** + * A read-write session for the non default workspace + */ + protected Session rwSessionW2; + + /** + * The workspace in the non default session. + */ + Workspace workspaceW2; + + /** + * The testroot node in the non default session + */ + Node testRootNodeW2; + + /** + * A referenceable node in default workspace + */ + protected Node node1W2; + + /** + * A non-referenceable node in default workspace + */ + protected Node node2W2; + + /** + * The workspace in the default session. + */ + Workspace workspace; + + protected void setUp() throws Exception { + super.setUp(); + + workspace = superuser.getWorkspace(); + + // init second workspace + String otherWspName = getOtherWorkspaceName(); + superuserW2 = getHelper().getSuperuserSession(otherWspName); + rwSessionW2 = getHelper().getReadWriteSession(otherWspName); + workspaceW2 = superuserW2.getWorkspace(); + + initNodesW2(); + } + + protected void tearDown() throws Exception { + workspace = null; + + // remove all test nodes in second workspace + if (superuserW2 != null) { + try { + if (!isReadOnly) { + // do a 'rollback' + cleanUpTestRoot(superuserW2); + } + } finally { + superuserW2.logout(); + superuserW2 = null; + } + } + if (rwSessionW2 != null) { + rwSessionW2.logout(); + rwSessionW2 = null; + } + workspaceW2 = null; + testRootNodeW2 = null; + node1W2 = null; + node2W2 = null; + super.tearDown(); + } + + protected String getOtherWorkspaceName() throws NotExecutableException { + if (workspace.getName().equals(workspaceName)) { + throw new NotExecutableException("Cannot test copy between workspaces. 'workspaceName' points to default workspace as well."); + } + return workspaceName; + } + + protected void initNodesW2() throws RepositoryException { + + // testroot + if (superuserW2.getRootNode().hasNode(testPath)) { + testRootNodeW2 = superuserW2.getRootNode().getNode(testPath); + // clean test root + for (NodeIterator it = testRootNodeW2.getNodes(); it.hasNext(); ) { + it.nextNode().remove(); + } + testRootNodeW2.save(); + } else { + testRootNodeW2 = superuserW2.getRootNode().addNode(testPath, testNodeType); + superuserW2.save(); + } + } + + /** + * Test cloning a hierarchy of nodes containing a set of shareable nodes. + * See also JCR-2473 + * + * @throws Exception if the test fails + */ + public void testClone() throws Exception { + Session s1 = testRootNode.getSession(); + Node a = testRootNode.addNode("a"); + Node b = a.addNode("b"); + Node c = a.addNode("c"); + Node d = b.addNode("d"); + ensureMixinType(d, mixShareable); + s1.save(); + + String path = c.getPath() + "/d"; + workspace.clone(workspace.getName(), d.getPath(), path, false); + + // we now have this hierarchy of nodes: + // a + // / \ + // b c + // \ / + // d + + // now clone 'a' to another workspace + workspaceW2.clone(workspace.getName(), a.getPath(), a.getPath(), false); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/NPEandCMETest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/NPEandCMETest.java new file mode 100644 index 00000000000..db55de6ce93 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/NPEandCMETest.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.core; + +import java.util.ConcurrentModificationException; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NPEandCMETest extends AbstractJCRTest { + + /** Logger instance */ + private static final Logger log = + LoggerFactory.getLogger(NPEandCMETest.class); + + private final static int NUM_THREADS = 10; + private final static boolean SHOW_STACKTRACE = true; + + protected void setUp() throws Exception { + super.setUp(); + Session session = getHelper().getSuperuserSession(); + session.getRootNode().addNode("test"); + session.save(); + } + + protected void tearDown() throws Exception { + try { + Session session = getHelper().getSuperuserSession(); + if (session.getRootNode().hasNode("test")) { + session.getRootNode().getNode("test").remove(); + session.save(); + } + } finally { + super.tearDown(); + } + } + + public void testDo() throws Exception { + Thread[] threads = new Thread[NUM_THREADS]; + TestTask[] tasks = new TestTask[NUM_THREADS]; + for (int i = 0; i < NUM_THREADS; i++) { + Session session = getHelper().getSuperuserSession(); + tasks[i] = new TestTask(i, session); + } + for (int i = 0; i < NUM_THREADS; i++) { + threads[i] = new Thread(tasks[i]); + threads[i].start(); + } + for (int i = 0; i < NUM_THREADS; i++) { + threads[i].join(); + } + int npes = 0, cmes = 0; + for(int i = 0; i < NUM_THREADS; i++) { + npes += tasks[i].npes; + cmes += tasks[i].cmes; + } + assertEquals("Total NPEs > 0", 0, npes); + assertEquals("Total CMEs > 0", 0, cmes); + } + + private static class TestTask implements Runnable { + + private final Session session; + private final int id; + private final Node test; + + private int npes = 0; + private int cmes = 0; + + private TestTask(int id, Session session) throws RepositoryException { + this.id = id; + this.session = session; + test = this.session.getRootNode().getNode("test"); + } + + public void run() { + try { + for (int i = 0; i < 500; i++) { + NodeIterator nodes = test.getNodes(); + if (nodes.getSize() > 100) { + long count = nodes.getSize() - 100; + while (nodes.hasNext() && count-- > 0) { + Node node = nodes.nextNode(); + if (node != null) { + try { + node.remove(); + } + catch (ItemNotFoundException e) { + // item was already removed + } + catch (InvalidItemStateException e) { + // ignorable + } + } + } + session.save(); + } + test.addNode("test-" + id + "-" + i); + session.save(); + } + + } + catch (InvalidItemStateException e) { + // ignorable + } + catch (RepositoryException e) { + Throwable cause = e.getCause(); + if (!(cause instanceof NoSuchItemStateException)) { + log.warn("Unexpected RepositoryException caught", e); + } + // else ignorable + } + catch (NullPointerException e) { + log.error("NPE caught", e); + npes++; + } + catch (ConcurrentModificationException e) { + log.error("CME caught", e); + cmes++; + } + } + + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/NodeImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/NodeImplTest.java new file mode 100644 index 00000000000..60557673917 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/NodeImplTest.java @@ -0,0 +1,412 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.security.Principal; +import java.util.Calendar; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +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.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.security.principal.GroupPrincipals; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** NodeImplTest... */ +public class NodeImplTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(NodeImplTest.class); + + protected void setUp() throws Exception { + super.setUp(); + if (!(testRootNode instanceof NodeImpl) && !(testRootNode.getSession() instanceof SessionImpl)) { + throw new NotExecutableException(); + } + } + + public static void changeReadPermission(Principal principal, Node n, boolean allowRead) throws RepositoryException, NotExecutableException { + SessionImpl s = (SessionImpl) n.getSession(); + JackrabbitAccessControlList acl = null; + AccessControlManager acMgr = s.getAccessControlManager(); + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(n.getPath()); + while (it.hasNext()) { + AccessControlPolicy acp = it.nextAccessControlPolicy(); + if (acp instanceof JackrabbitAccessControlList) { + acl = (JackrabbitAccessControlList) acp; + break; + } + } + if (acl == null) { + AccessControlPolicy[] acps = acMgr.getPolicies(n.getPath()); + for (AccessControlPolicy acp : acps) { + if (acp instanceof JackrabbitAccessControlList) { + acl = (JackrabbitAccessControlList) acp; + break; + } + } + } + + if (acl != null) { + acl.addEntry(principal, new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_READ)}, allowRead); + acMgr.setPolicy(n.getPath(), acl); + s.save(); + } else { + // no JackrabbitAccessControlList found. + throw new NotExecutableException(); + } + } + + public static Principal getReadOnlyPrincipal(RepositoryHelper helper) throws RepositoryException, NotExecutableException { + SessionImpl s = (SessionImpl) helper.getReadOnlySession(); + try { + for (Principal p : s.getSubject().getPrincipals()) { + if (!GroupPrincipals.isGroup(p)) { + return p; + } + } + } finally { + s.logout(); + } + throw new NotExecutableException(); + } + + /** + * Test case for #JCR-1729. Note, that test will only be executable with + * a security configurations that allows to set Deny-ACEs. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testIsCheckedOut() throws RepositoryException, NotExecutableException { + Node n = testRootNode.addNode(nodeName1); + NodeImpl testNode = (NodeImpl) n.addNode(nodeName2); + testRootNode.save(); + + Principal principal = getReadOnlyPrincipal(getHelper()); + changeReadPermission(principal, n, false); + changeReadPermission(principal, testNode, true); + + Session readOnly = getHelper().getReadOnlySession(); + try { + NodeImpl tn = (NodeImpl) readOnly.getItem(testNode.getPath()); + assertTrue(tn.isCheckedOut()); + + n.addMixin(mixVersionable); + testRootNode.save(); + n.checkin(); + + assertFalse(tn.isCheckedOut()); + } finally { + readOnly.logout(); + // reset the denied read-access + n.checkout(); + changeReadPermission(principal, n, true); + } + } + + public void testAddNodeUuid() throws RepositoryException, NotExecutableException { + String uuid = "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"; + Node n = testRootNode.addNode(nodeName1); + Node testNode = ((NodeImpl) n).addNodeWithUuid(nodeName2, uuid); + testNode.addMixin(NodeType.MIX_REFERENCEABLE); + testRootNode.getSession().save(); + assertEquals( + "Node identifier should be: " + uuid, + uuid, testNode.getIdentifier()); + } + + public void testAddNodeUuidCollision() throws RepositoryException, NotExecutableException { + String uuid = "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"; + Node n = testRootNode.addNode(nodeName1); + Node testNode1 = ((NodeImpl) n).addNodeWithUuid(nodeName2, uuid); + testNode1.addMixin(NodeType.MIX_REFERENCEABLE); + testRootNode.getSession().save(); + + try { + ((NodeImpl) n).addNodeWithUuid(nodeName2, uuid); + fail("UUID collision not detected by addNodeWithUuid"); + } catch (ItemExistsException e) { + } + } + /** + * Test case for JCR-2336. Setting jcr:data (of type BINARY) must convert + * the String value to a binary. + * + * @throws RepositoryException - + */ + public void testSetPropertyConvertValue() throws RepositoryException { + Node content = testRootNode.addNode("jcr:content", "nt:resource"); + content.setProperty("jcr:lastModified", Calendar.getInstance()); + content.setProperty("jcr:mimeType", "text/plain"); + content.setProperty("jcr:data", "Hello"); + superuser.save(); + } + + public void testSetPropertyConvertToString() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, "nt:folder"); + n.addMixin("mix:title"); + // must convert to string there is no other definition for this property + Property p = n.setProperty("jcr:title", 123); + assertEquals(PropertyType.nameFromValue(PropertyType.STRING), + PropertyType.nameFromValue(p.getType())); + } + + public void testSetPropertyExplicitType() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, ntUnstructured); + n.addMixin("mix:title"); + Property p = n.setProperty("jcr:title", "foo"); + assertEquals(PropertyType.nameFromValue(PropertyType.STRING), + PropertyType.nameFromValue(p.getType())); + assertEquals(PropertyType.nameFromValue(PropertyType.STRING), + PropertyType.nameFromValue(p.getDefinition().getRequiredType())); + p.remove(); + // must use residual definition from nt:unstructured + p = n.setProperty("jcr:title", 123); + assertEquals(PropertyType.nameFromValue(PropertyType.LONG), + PropertyType.nameFromValue(p.getType())); + assertEquals(PropertyType.nameFromValue(PropertyType.UNDEFINED), + PropertyType.nameFromValue(p.getDefinition().getRequiredType())); + } + + public void testSetPropertyConvertMultiValued() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, "test:canSetProperty"); + // must convert to long there is no other definition for this property + Property p = n.setProperty("LongMultiple", new String[]{"123", "456"}); + assertEquals(PropertyType.nameFromValue(PropertyType.LONG), + PropertyType.nameFromValue(p.getType())); + } + + /** + * Test case for JCR-2130 and JCR-2408. + * + * @throws RepositoryException + */ + public void testAddRemoveMixin() throws RepositoryException { + // add mix:title to a nt:folder node and set jcr:title property + Node n = testRootNode.addNode(nodeName1, "nt:folder"); + n.addMixin("mix:title"); + n.setProperty("jcr:title", "blah blah"); + testRootNode.getSession().save(); + + // remove mix:title, jcr:title should be gone as there's no matching + // definition in nt:folder + n.removeMixin("mix:title"); + testRootNode.getSession().save(); + assertFalse(n.hasProperty("jcr:title")); + + // add mix:title to a nt:unstructured node and set jcr:title property + Node n1 = testRootNode.addNode(nodeName2, ntUnstructured); + n1.addMixin("mix:title"); + n1.setProperty("jcr:title", "blah blah"); + assertEquals( + n1.getProperty("jcr:title").getDefinition().getDeclaringNodeType().getName(), + "mix:title"); + + // remove mix:title, jcr:title should stay since it adopts the residual + // property definition declared in nt:unstructured + testRootNode.getSession().save(); + + n1.removeMixin("mix:title"); + testRootNode.getSession().save(); + assertTrue(n1.hasProperty("jcr:title")); + + assertEquals( + n1.getProperty("jcr:title").getDefinition().getDeclaringNodeType().getName(), + ntUnstructured); + + // add mix:referenceable to a nt:unstructured node, jcr:uuid is + // automatically added + Node n2 = testRootNode.addNode(nodeName3, ntUnstructured); + n2.addMixin(mixReferenceable); + testRootNode.getSession().save(); + + // remove mix:referenceable, jcr:uuid should always get removed + // since it is a protcted property + n2.removeMixin(mixReferenceable); + testRootNode.getSession().save(); + assertFalse(n2.hasProperty("jcr:uuid")); + } + + + /** + * Test corruption in session / persistence state after + * {@link ReferentialIntegrityException}. + * + * @see JCR-2503 + */ + public void testReferentialIntegrityCorruption() throws Exception { + Session session = testRootNode.getSession(); + Node root = testRootNode.addNode("testReferentialIntegrityCorruption"); + + // Create test nodes P1 and P2 + Node nodeP1 = root.addNode("P1"); + nodeP1.addMixin("mix:referenceable"); + Node nodeP2 = root.addNode("P2"); + nodeP2.addMixin("mix:referenceable"); + session.save(); + + // Create reference from P2 to P1 and save + nodeP2.setProperty("referencetoP1", nodeP1); + session.save(); + + // Add node P3 + Node nodeP3 = root.addNode("P3"); + nodeP3.addMixin("mix:referenceable"); + + // And try to remove P1 while P2 still references P1 + nodeP1.remove(); + try { + session.save(); + } catch (ReferentialIntegrityException expected) { + // Got ReferentialIntegrityException as expected + } + + // Remove P2 and save again, this will succeed. As P1, P2 + // should be removed and P3 should exist + try { + nodeP2.remove(); + session.save(); + } catch (Exception e) { + String msg = + "JCR-2503: Saving delete after" + + " ReferentialIntegrityException failed"; + log.error(msg, e); + fail(msg); + } + + try { + nodeP3 = session.getNodeByIdentifier(nodeP3.getIdentifier()); + } catch (Exception e) { + String msg = + "JCR-2503: Retrieving P3 by uuid failed. Corrupt session?"; + log.error(msg, e); + fail(msg); + } + + try { + nodeP3.remove(); + session.save(); + } catch (Exception e) { + String msg = "JCR-2503: Removing P3 failed. Corrupt session?"; + log.error(msg, e); + fail(msg); + } + + try { + root = testRootNode.getNode("testReferentialIntegrityCorruption"); + for (Node ignore : JcrUtils.getChildNodes(root)) { + } + } catch (Exception e) { + String msg = "JCR-2503: Failed to scan empty node. Corrupt session?"; + log.error(msg, e); + fail(msg); + } + + root.remove(); + session.save(); + } + + /** + * Test corruption in session / persistence state after + * {@link ReferentialIntegrityException}. + *

    + * This is a variant of {@link #testReferentialIntegrityCorruption()} + * that checks that {@link Node#getPath()} works after the save operation. + * + * @see JCR-3018 + */ + public void testReferentialIntegrityCorruptionGetPath() throws Exception { + Session session = testRootNode.getSession(); + Node root = testRootNode.addNode("testReferentialIntegrityCorruption"); + + // Create test nodes P1 and P2 + Node nodeP1 = root.addNode("P1"); + nodeP1.addMixin("mix:referenceable"); + Node nodeP2 = root.addNode("P2"); + nodeP2.addMixin("mix:referenceable"); + session.save(); + + // Create reference from P2 to P1 and save + nodeP2.setProperty("referencetoP1", nodeP1); + session.save(); + + // Add node P3 + Node nodeP3 = root.addNode("P3"); + nodeP3.addMixin("mix:referenceable"); + + String nodeP3path = nodeP3.getPath(); + + // And try to remove P1 while P2 still references P1 + nodeP1.remove(); + try { + session.save(); + } catch (ReferentialIntegrityException expected) { + // Got ReferentialIntegrityException as expected + } + + // Remove P2 and save again, this will succeed. As P1, P2 + // should be removed and P3 should exist + try { + nodeP2.remove(); + session.save(); + } catch (Exception e) { + String msg = "JCR-3018: Saving delete after" + + " ReferentialIntegrityException failed"; + log.error(msg, e); + fail(msg); + } + + try { + assertEquals(nodeP3path, nodeP3.getPath()); + nodeP3 = session.getNodeByIdentifier(nodeP3.getIdentifier()); + } catch (Exception e) { + String msg = "JCR-3018: getting path of P3 failed. Corrupt session?"; + log.error(msg, e); + fail(msg); + } + + root.remove(); + session.save(); + } + + public void testBracketsInNodeName() throws Exception { + final Node root = testRootNode.addNode("testBracketsInNodeName"); + + final String[] childNames = { "{A}", "B}", "{C", "(D)", "E)", "(F", }; + + for (String name : childNames) { + root.addNode(name); + root.getSession().save(); + assertTrue("Expecting child " + name + " to have been created", root.hasNode(name)); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OracleRepositoryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OracleRepositoryTest.java new file mode 100755 index 00000000000..8d641b0b550 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OracleRepositoryTest.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.core; + +import java.io.File; +import java.io.InputStream; +import java.util.Properties; + +import javax.jcr.Session; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +/** + * Tests the creation of a repository using Oracle persistence and different table and index tablespaces. + * + * @author Edouard Hue + */ +public class OracleRepositoryTest extends TestCase { + private File dir; + + private RepositoryConfig config; + + protected void setUp() throws Exception { + final Properties sysProps = System.getProperties(); + if (!sysProps.containsKey("tests.oracle.url") + || !sysProps.containsKey("tests.oracle.user") + || !sysProps.containsKey("tests.oracle.password") + || !sysProps.containsKey("tests.oracle.tablespace") + || !sysProps.containsKey("tests.oracle.indexTablespace")) { + throw new IllegalStateException("Missing system property for test"); + } + dir = File.createTempFile("jackrabbit_", null, new File("target")); + dir.delete(); + dir.mkdir(); + final InputStream in = getClass().getResourceAsStream( + "/org/apache/jackrabbit/core/repository-oracle.xml"); + config = RepositoryConfig.create(in, dir.getPath()); + } + + /** + * Attempt to start a {@link TransientRepository} using {@link #config}, open + * a new session with default credentials and workspace, then shutdown the repo. + */ + public void testConfiguration() throws Exception { + final TransientRepository repo = new TransientRepository(config); + try { + final Session session = repo.login(); + session.logout(); + } finally { + repo.shutdown(); + } + } + + protected void tearDown() throws Exception { + if (dir != null) { + FileUtils.deleteQuietly(dir); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OracleRetrocompatibleRepositoryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OracleRetrocompatibleRepositoryTest.java new file mode 100755 index 00000000000..327d1210855 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OracleRetrocompatibleRepositoryTest.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.core; + +import java.io.File; +import java.io.InputStream; +import java.util.Properties; + +import javax.jcr.Session; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +/** + * Tests the creation of a repository using Oracle persistence and no index tablespace specified. + * + * @author Edouard Hue + */ +public class OracleRetrocompatibleRepositoryTest extends TestCase { + private File dir; + + private RepositoryConfig config; + + protected void setUp() throws Exception { + final Properties sysProps = System.getProperties(); + if (!sysProps.containsKey("tests.oracle.url") + || !sysProps.containsKey("tests.oracle.user") + || !sysProps.containsKey("tests.oracle.password") + || !sysProps.containsKey("tests.oracle.tablespace")) { + throw new IllegalStateException("Missing system property for test"); + } + dir = File.createTempFile("jackrabbit_", null, new File("target")); + dir.delete(); + dir.mkdir(); + final InputStream in = getClass().getResourceAsStream( + "/org/apache/jackrabbit/core/repository-oracle-compat.xml"); + config = RepositoryConfig.create(in, dir.getPath()); + } + + /** + * Attempt to start a {@link TransientRepository} using {@link #config}, open + * a new session with default credentials and workspace, then shutdown the repo. + */ + public void testConfiguration() throws Exception { + final TransientRepository repo = new TransientRepository(config); + try { + final Session session = repo.login(); + session.logout(); + } finally { + repo.shutdown(); + } + } + + protected void tearDown() throws Exception { + if (dir != null) { + FileUtils.deleteQuietly(dir); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OverlappingNodeAddTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OverlappingNodeAddTest.java new file mode 100755 index 00000000000..d5e71dcd37b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/OverlappingNodeAddTest.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.core; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +public class OverlappingNodeAddTest extends AbstractJCRTest { + + private Node testfolder; + private Session s1; + private Session s2; + + public void setUp() throws Exception { + super.setUp(); + testfolder = testRootNode.addNode("container", "nt:folder"); + s1 = testfolder.getSession(); + s2 = getHelper().getReadWriteSession(); + s1.save(); + } + + /** + * Performs add operations on a single node (no SNS) through 2 sessions + */ + public void testWith2Folders() throws Exception { + + boolean bWasSaved = false; + + String testpath = testfolder.getPath(); + + Node f1 = s1.getNode(testpath).addNode("folder", "nt:folder"); + Node c1 = f1.addNode("a", "nt:file"); + Node r1 = c1.addNode("jcr:content", "nt:resource"); + r1.setProperty("jcr:data", "foo"); + + Node f2 = s2.getNode(testpath).addNode("folder", "nt:folder"); + Node c2 = f2.addNode("b", "nt:file"); + Node r2 = c2.addNode("jcr:content", "nt:resource"); + r2.setProperty("jcr:data", "bar"); + + s1.save(); + + String s1FolderId = f1.getIdentifier(); + + try { + s2.save(); + bWasSaved = true; + } catch (InvalidItemStateException ex) { + // expected + + // retry; adding refresh doesn't change anything here + try { + s2.save(); + + bWasSaved = true; + + } catch (InvalidItemStateException ex2) { + // we would be cool with this + } + } + + // we don't have changes in s1, so the keepChanges flag should be + // irrelevant + s1.refresh(false); + + // be nice and get a new Node instance + Node newf1 = s1.getNode(testpath + "/folder"); + + // if bWasSaved it should now be visible to Session 1 + assertEquals("'b' was saved, so session 1 should see it", bWasSaved, + newf1.hasNode("b")); + + // 'a' was saved by Session 1 earlier on + if (!newf1.hasNode("a")) { + String message = "child node 'a' not present"; + + if (bWasSaved && !s1FolderId.equals(newf1.getIdentifier())) { + message += ", and also the folder's identifier changed from " + + s1FolderId + " to " + newf1.getIdentifier(); + } + + Node oldf1 = null; + + try { + oldf1 = s1.getNodeByIdentifier(s1FolderId); + } catch (ItemNotFoundException ex) { + message += "; node with id " + + s1FolderId + + " can't be retrieved using getNodeByIdentifier either"; + } + + if (oldf1 != null) { + try { + oldf1.getPath(); + } catch (Exception ex) { + message += "; node with id " + + s1FolderId + + " can be retrieved using getNodeByIdentifier, but getPath() fails with: " + + ex.getMessage(); + } + } + + fail(message); + } + } + + protected void tearDown() throws Exception { + if (s2 != null) { + s2.logout(); + s2 = null; + } + super.tearDown(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReadVersionsWhileModified.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReadVersionsWhileModified.java new file mode 100644 index 00000000000..a092d46640d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReadVersionsWhileModified.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.core; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.Version; + +/** + * ReadVersionsWhileModified tests if version history can be read + * consistently while it is modified by another thread. + *

    + * This is a test case for: + * JCR-18 + */ +public class ReadVersionsWhileModified extends AbstractConcurrencyTest { + + private static final int RUN_NUM_SECONDS = 20; + + public void testVersionHistory() throws RepositoryException { + final Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + testRootNode.save(); + final Session s = getHelper().getSuperuserSession(); + Thread t = new Thread(new Runnable() { + public void run() { + long end = System.currentTimeMillis() + RUN_NUM_SECONDS * 1000; + try { + Node vn = (Node) s.getItem(n.getPath()); + while (end > System.currentTimeMillis()) { + vn.checkout(); + vn.checkin(); + } + } catch (RepositoryException e) { + e.printStackTrace(); + } finally { + s.logout(); + } + } + }); + t.start(); + VersionHistory vh = n.getVersionHistory(); + while (t.isAlive()) { + // walk version history + Version v = vh.getRootVersion(); + while (v.getSuccessors().length > 0) { + v = v.getSuccessors()[0]; + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReadWhileSaveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReadWhileSaveTest.java new file mode 100644 index 00000000000..7eb6f48034c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReadWhileSaveTest.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.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import java.io.File; +import java.io.IOException; +import java.io.BufferedOutputStream; +import java.io.OutputStream; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.BufferedInputStream; +import java.io.FileNotFoundException; +import java.util.Random; + +/** + * ReadWhileSaveTest reads nodes using a new session each time + * while a large binary is written to the workspace. + */ +public class ReadWhileSaveTest extends AbstractJCRTest { + + public void testReadWhileSave() throws RepositoryException, IOException { + Thread t = runExpensiveSave(); + long numReads = 0; + while (t.isAlive()) { + Session s = getHelper().getSuperuserSession(); + try { + for (NodeIterator it = s.getRootNode().getNodes(); it.hasNext(); ) { + it.nextNode(); + } + numReads++; + } finally { + s.logout(); + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // ignore + } + } + log.println("numReads: " + numReads); + } + + private Thread runExpensiveSave() throws RepositoryException, IOException { + // create a temp file with 10 mb random data + final File tmp = File.createTempFile("garbage", null); + OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp)); + Random rand = new Random(); + byte[] randomKb = new byte[1024]; + for (int i = 0; i < 1024 * 10; i++) { + rand.nextBytes(randomKb); + out.write(randomKb); + } + out.close(); + final Session s = getHelper().getSuperuserSession(); + final Node stuff = s.getRootNode().getNode(testPath).addNode("stuff"); + Thread t = new Thread(new Runnable() { + public void run() { + try { + for (int i = 0; i < 10; i++) { + stuff.setProperty("binary", new BufferedInputStream(new FileInputStream(tmp))); + s.save(); + stuff.getProperty("binary").remove(); + s.save(); + } + } catch (RepositoryException e) { + e.printStackTrace(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + s.logout(); + tmp.delete(); + } + } + }); + t.start(); + return t; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReferencesTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReferencesTest.java new file mode 100644 index 00000000000..9b5a594cd7d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReferencesTest.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * + */ +public final class ReferencesTest extends AbstractJCRTest { + + private String uniquePrefix; + + private static int cnt = 0; + + /** + * {@inheritDoc} + */ + public void setUp() throws Exception { + uniquePrefix = "referencesTest" + System.currentTimeMillis() + "-" + cnt; + cnt++; + Session session = createSession(); + Node a = getTestRootNode(session).addNode("A"); + Node b = a.addNode("B"); + a.addMixin("mix:referenceable"); + b.addMixin("mix:referenceable"); + getTestRootNode(session).addNode("C"); + saveAndlogout(session); + } + + /** + * Tries to create a double back-reference to "ref to B" property. + * + * @throws Exception on test error + */ + public void testDoubleBackReference() throws Exception { + Session session1 = createSession(); + Node bses1 = getTestRootNode(session1).getNode("A").getNode("B"); + getTestRootNode(session1).getNode("C").setProperty("ref to B", bses1); + + Session session2 = createSession(); + Node bses2 = getTestRootNode(session2).getNode("A").getNode("B"); + getTestRootNode(session2).getNode("C").setProperty("ref to B", bses2); + + saveAndlogout(session1, session2); + assertRemoveTestNodes(); + } + + /** + * Tries to create a single back-reference to "ref to B" property which does not exist. + * + * @throws Exception on test error + */ + public void testBackRefToNonExistingProp() throws Exception { + Session session2 = createSession(); + Node bses2 = getTestRootNode(session2).getNode("A").getNode("B"); + getTestRootNode(session2).getNode("C").setProperty("ref to B", bses2); + + Session session3 = createSession(); + getTestRootNode(session3).getNode("C").setProperty("ref to B", new Value[]{}); + + saveAndlogout(session2, session3); + assertRemoveTestNodes(); + } + + /** + * Tries to create a single back-reference to "ref" property for both A and B whereas "ref" is single + * valued and points to A. + * + * @throws Exception on test error + */ + public void testMisdirectedBackRef() throws Exception { + Session session2 = createSession(); + Node bses2 = getTestRootNode(session2).getNode("A").getNode("B"); + getTestRootNode(session2).getNode("C").setProperty("ref", bses2); + + Session session3 = createSession(); + Node ases3 = getTestRootNode(session3).getNode("A"); + getTestRootNode(session3).getNode("C").setProperty("ref", ases3); + + saveAndlogout(session2, session3); + assertRemoveTestNodes(); + } + + /** + * Variant of {@link #testDoubleBackReference()} for mult-valued props. + * + * @throws Exception on test error + */ + public void testDoubleBackRefReferenceMultiValued() throws Exception { + Session session2 = createSession(); + ValueFactory valFac2 = session2.getValueFactory(); + Node bses2 = getTestRootNode(session2).getNode("A").getNode("B"); + getTestRootNode(session2).getNode("C").setProperty("ref to B", + new Value[]{valFac2.createValue(bses2), valFac2.createValue(bses2)}); + + Session session3 = createSession(); + ValueFactory valFac3 = session3.getValueFactory(); + Node bses3 = getTestRootNode(session3).getNode("A").getNode("B"); + getTestRootNode(session3).getNode("C").setProperty("ref to B", + new Value[]{valFac3.createValue(bses3), valFac3.createValue(bses3)}); + + saveAndlogout(session2, session3); + assertRemoveTestNodes(); + } + + /** + * Variant of {@link #testMisdirectedBackRef()} for multi-valued props. + * + * @throws Exception on test error + */ + public void testMisdirectedBackRefMultiValued() throws Exception { + Session session2 = createSession(); + ValueFactory valFac2 = session2.getValueFactory(); + Node ases2 = getTestRootNode(session2).getNode("A"); + getTestRootNode(session2).getNode("C").setProperty("ref", + new Value[]{valFac2.createValue(ases2), valFac2.createValue(ases2)}); + + Session session3 = createSession(); + ValueFactory valFac3 = session3.getValueFactory(); + Node bses3 = getTestRootNode(session3).getNode("A").getNode("B"); + getTestRootNode(session3).getNode("C").setProperty("ref", new Value[]{valFac3.createValue(bses3)}); + + saveAndlogout(session2, session3); + assertRemoveTestNodes(); + } + + /** + * Regular references usage. + * + * @throws Exception on test error + */ + public void testRegularReference() throws Exception { + Session session1 = createSession(); + Node bses1 = getTestRootNode(session1).getNode("A").getNode("B"); + getTestRootNode(session1).getNode("A").setProperty("ref to B", bses1); + + Session session2 = createSession(); + ValueFactory valFac2 = session2.getValueFactory(); + Node bses2 = getTestRootNode(session2).getNode("A").getNode("B"); + getTestRootNode(session2).getNode("C").setProperty("ref to B", + new Value[]{valFac2.createValue(bses2), valFac2.createValue(bses2)}); + getTestRootNode(session2).getNode("C").setProperty("another ref to B", bses2); + + saveAndlogout(session1, session2); + assertRemoveTestNodes(); + } + + private void assertRemoveTestNodes() throws RepositoryException { + Session session = createSession(); + getTestRootNode(session).remove(); + assertSave(session); + session.logout(); + } + + /** + * @param session the session to save + */ + private void assertSave(Session session) { + try { + session.save(); + } catch (RepositoryException e) { + fail("saving session failed: " + e.getMessage()); + } + } + + /** + * @return a super user session + * @throws RepositoryException on error + */ + private Session createSession() throws RepositoryException { + return getHelper().getSuperuserSession(); + } + + private void saveAndlogout(Session... sessions) throws RepositoryException { + if (sessions != null) { + for (Session session : sessions) { + session.save(); + session.logout(); + } + } + } + + /** + * @param session the session to use + * @return a node which is more or less unique per testcase + * @throws RepositoryException on error + */ + private Node getTestRootNode(Session session) throws RepositoryException { + if (session.getRootNode().hasNode(uniquePrefix)) { + return session.getRootNode().getNode(uniquePrefix); + } else { + return session.getRootNode().addNode(uniquePrefix); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RemoveAddNodeWithUUIDTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RemoveAddNodeWithUUIDTest.java new file mode 100644 index 00000000000..95e142a5e9d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RemoveAddNodeWithUUIDTest.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.core; + +import java.io.File; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * RemoveAddNodeWithUUIDTest check if no 'overwriting cached entry' + * warnings are written to the log when a node is re-created with the same UUID. + * See: JCR-3419 + */ +public class RemoveAddNodeWithUUIDTest extends AbstractJCRTest { + + public void testRemoveAdd() throws Exception { + Tail tail = Tail.start(new File("target", "jcr.log"), "overwriting cached entry"); + try { + Node test = testRootNode.addNode("test"); + test.setProperty("prop", 1); + test.addMixin(mixReferenceable); + superuser.save(); + String testId = test.getIdentifier(); + + Session s = getHelper().getSuperuserSession(); + try { + Node testOther = s.getNode(test.getPath()); + + test.remove(); + test = ((NodeImpl) testRootNode).addNodeWithUuid("test", testId); + test.setProperty("prop", 2); + superuser.save(); + + // now test node instance is not accessible anymore for s + try { + testOther.getProperty("prop"); + fail("test node instance must not be accessibly anymore"); + } catch (InvalidItemStateException e) { + // expected + } + // getting it again must succeed and return updated property value + testOther = s.getNode(test.getPath()); + assertEquals("property outdated", 2, testOther.getProperty("prop").getLong()); + + assertFalse("detected 'overwriting cached entry' messages in log", tail.getLines().iterator().hasNext()); + } finally { + s.logout(); + } + } finally { + tail.close(); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReplacePropertyWhileOthersReadTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReplacePropertyWhileOthersReadTest.java new file mode 100644 index 00000000000..4741294e683 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReplacePropertyWhileOthersReadTest.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.core; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReplacePropertyWhileOthersReadTest... + */ +public class ReplacePropertyWhileOthersReadTest extends AbstractJCRTest { + + private static final Logger log = LoggerFactory.getLogger(ReplacePropertyWhileOthersReadTest.class); + + private final List values = new ArrayList(); + + private Node test; + + private final Random rand = new Random(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + test = testRootNode.addNode("test"); + test.addMixin(mixReferenceable); + superuser.save(); + values.add(vf.createValue("value")); + values.add(vf.createValue(new BigDecimal(1234))); + values.add(vf.createValue(Calendar.getInstance())); + values.add(vf.createValue(1.234)); + values.add(vf.createValue(true)); + values.add(vf.createValue(test)); + values.add(vf.createValue(vf.createBinary( + new ByteArrayInputStream(new byte[0])))); + } + + @Override + protected void tearDown() throws Exception { + test = null; + values.clear(); + super.tearDown(); + } + + public void testAddRemove() throws Exception { + final Property prop = test.setProperty("prop", getRandomValue()); + superuser.save(); + + Thread reader = new Thread(new Runnable() { + + String path = prop.getPath(); + + public void run() { + // run for three seconds + long stop = System.currentTimeMillis() + + TimeUnit.SECONDS.toMillis(3); + while (System.currentTimeMillis() < stop) { + try { + Session s = getHelper().getSuperuserSession(); + try { + s.getProperty(path); + } finally { + s.logout(); + } + } catch (RepositoryException e) { + log.warn("", e); + } + } + } + }); + Tail tail = Tail.start(new File("target", "jcr.log"), + "overwriting cached entry"); + try { + reader.start(); + while (reader.isAlive()) { + test.getProperty("prop").remove(); + int type; + boolean isMultivalued; + if (rand.nextBoolean()) { + Value v = getRandomValue(); + isMultivalued = false; + type = v.getType(); + test.setProperty("prop", v); + } else { + Value[] v = getRandomMultiValue(); + type = v[0].getType(); + isMultivalued = true; + test.setProperty("prop", v); + } + superuser.save(); + assertEquals(isMultivalued, test.getProperty("prop").isMultiple()); + assertEquals(type, test.getProperty("prop").getType()); + } + assertFalse("detected 'overwriting cached entry' messages in log", + tail.getLines().iterator().hasNext()); + } finally { + tail.close(); + } + } + + private Value getRandomValue() { + return values.get(rand.nextInt(values.size())); + } + + private Value[] getRandomMultiValue() { + return new Value[]{getRandomValue()}; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReplaceTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReplaceTest.java new file mode 100644 index 00000000000..581489f3f11 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ReplaceTest.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.core; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * ReplaceTest checks if the node definition for a removed node + * is correctly calculated when its parent node had been replaced with a new + * node and the uuid is still the same. + */ +public class ReplaceTest extends AbstractJCRTest { + + public void testReplace() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.addMixin(mixReferenceable); + String uuid = n1.getIdentifier(); + n1.addNode("node2"); + superuser.save(); + + n1.remove(); + ((NodeImpl) testRootNode).addNodeWithUuid("node1", uuid); + superuser.save(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RepositoryCopierTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RepositoryCopierTest.java new file mode 100644 index 00000000000..f4b7dfb9460 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RepositoryCopierTest.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.core; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.Random; + +import javax.jcr.Binary; +import javax.jcr.Credentials; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +public class RepositoryCopierTest extends TestCase { + + private static final Credentials CREDENTIALS = + new SimpleCredentials("admin", "admin".toCharArray()); + + private static final File BASE = new File("target", "RepositoryCopierTest"); + + private static final File SOURCE = new File(BASE, "source"); + + private static final File TARGET = new File(BASE, "target"); + + private static final Calendar DATE = Calendar.getInstance(); + + private static final byte[] BINARY = new byte[64 * 1024]; + + static { + new Random().nextBytes(BINARY); + } + + private String identifier; + + protected void setUp() { + BASE.mkdirs(); + } + + protected void tearDown() throws Exception { + FileUtils.deleteDirectory(BASE); + } + + public void testRepositoryCopy() throws Exception { + createSourceRepository(); + RepositoryCopier.copy(SOURCE, TARGET); + verifyTargetRepository(); + } + + private void createSourceRepository() throws Exception { + RepositoryImpl repository = RepositoryImpl.create( + RepositoryConfig.install(SOURCE)); + try { + Session session = repository.login(CREDENTIALS); + try { + NamespaceRegistry registry = + session.getWorkspace().getNamespaceRegistry(); + registry.registerNamespace("test", "http://www.example.org/"); + + NodeTypeManager manager = + session.getWorkspace().getNodeTypeManager(); + NodeTypeTemplate template = manager.createNodeTypeTemplate(); + template.setName("test:unstructured"); + template.setDeclaredSuperTypeNames( + new String[] { "nt:unstructured" }); + manager.registerNodeType(template, false); + + Node root = session.getRootNode(); + + Node referenceable = + root.addNode("referenceable", "test:unstructured"); + referenceable.addMixin(NodeType.MIX_REFERENCEABLE); + session.save(); + identifier = referenceable.getIdentifier(); + + Node properties = root.addNode("properties", "test:unstructured"); + properties.setProperty("boolean", true); + Binary binary = session.getValueFactory().createBinary( + new ByteArrayInputStream(BINARY)); + try { + properties.setProperty("binary", binary); + } finally { + binary.dispose(); + } + properties.setProperty("date", DATE); + properties.setProperty("decimal", new BigDecimal(123)); + properties.setProperty("double", Math.PI); + properties.setProperty("long", 9876543210L); + properties.setProperty("reference", referenceable); + properties.setProperty("string", "test"); + properties.setProperty("multiple", "a,b,c".split(",")); + session.save(); + + binary = properties.getProperty("binary").getBinary(); + try { + InputStream stream = binary.getStream(); + try { + for (int i = 0; i < BINARY.length; i++) { + assertEquals(BINARY[i], (byte) stream.read()); + } + assertEquals(-1, stream.read()); + } finally { + stream.close(); + } + } finally { + binary.dispose(); + } + } finally { + session.logout(); + } + } finally { + repository.shutdown(); + } + } + + private void verifyTargetRepository() throws Exception { + RepositoryImpl repository = RepositoryImpl.create( + RepositoryConfig.create(TARGET)); + try { + Session session = repository.login(CREDENTIALS); + try { + assertEquals( + "http://www.example.org/", + session.getNamespaceURI("test")); + + NodeTypeManager manager = + session.getWorkspace().getNodeTypeManager(); + assertTrue(manager.hasNodeType("test:unstructured")); + NodeType type = manager.getNodeType("test:unstructured"); + assertFalse(type.isMixin()); + assertTrue(type.isNodeType("nt:unstructured")); + + assertTrue(session.nodeExists("/properties")); + Node properties = session.getNode("/properties"); + assertEquals( + PropertyType.BOOLEAN, + properties.getProperty("boolean").getType()); + assertEquals( + true, properties.getProperty("boolean").getBoolean()); + assertEquals( + PropertyType.BINARY, + properties.getProperty("binary").getType()); + Binary binary = properties.getProperty("binary").getBinary(); + try { + InputStream stream = binary.getStream(); + try { + for (int i = 0; i < BINARY.length; i++) { + assertEquals(BINARY[i], (byte) stream.read()); + } + assertEquals(-1, stream.read()); + } finally { + stream.close(); + } + } finally { + binary.dispose(); + } + assertEquals( + PropertyType.DATE, + properties.getProperty("date").getType()); + assertEquals( + DATE.getTimeInMillis(), + properties.getProperty("date").getDate().getTimeInMillis()); + assertEquals( + PropertyType.DECIMAL, + properties.getProperty("decimal").getType()); + assertEquals( + new BigDecimal(123), + properties.getProperty("decimal").getDecimal()); + assertEquals( + PropertyType.DOUBLE, + properties.getProperty("double").getType()); + assertEquals( + Math.PI, properties.getProperty("double").getDouble()); + assertEquals( + PropertyType.LONG, + properties.getProperty("long").getType()); + assertEquals( + 9876543210L, properties.getProperty("long").getLong()); + assertEquals( + PropertyType.REFERENCE, + properties.getProperty("reference").getType()); + assertEquals( + identifier, + properties.getProperty("reference").getString()); + assertEquals( + "/referenceable", + properties.getProperty("reference").getNode().getPath()); + assertEquals( + PropertyType.STRING, + properties.getProperty("string").getType()); + assertEquals( + "test", properties.getProperty("string").getString()); + assertEquals( + PropertyType.STRING, + properties.getProperty("multiple").getType()); + Value[] values = properties.getProperty("multiple").getValues(); + assertEquals(3, values.length); + assertEquals("a", values[0].getString()); + assertEquals("b", values[1].getString()); + assertEquals("c", values[2].getString()); + } finally { + session.logout(); + } + } finally { + repository.shutdown(); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RestoreAndCheckoutTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RestoreAndCheckoutTest.java new file mode 100644 index 00000000000..921fd741fa8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RestoreAndCheckoutTest.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.core; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * RestoreAndCheckoutTest tests for JCR-1197 + * "Node.restore() may throw InvalidItemStateException". + * + */ +public class RestoreAndCheckoutTest extends AbstractJCRTest { + + private static final int NODES_COUNT = 10; + + public void testRestoreAndCheckout() throws RepositoryException { + Session session = getHelper().getSuperuserSession(); + + Node rootNode = session.getRootNode(); + Node myRoot = rootNode.addNode("myRoot"); + myRoot.addMixin("mix:versionable"); + rootNode.save(); + myRoot.checkin(); + + // create n child and grandchild versionable nodes + for (int i = 0; i < NODES_COUNT; i++) { + myRoot.checkout(); + Node childNode = myRoot.addNode("child" + i); + childNode.addMixin("mix:versionable"); + Node grandChildNode = childNode.addNode("grandChild"); + grandChildNode.addMixin("mix:versionable"); + myRoot.save(); + grandChildNode.checkin(); + childNode.checkin(); + myRoot.checkin(); + } + + // restore child, then restore/checkout grandchild nodes + for (int i = 0; i < NODES_COUNT; i++) { + Node childNode = myRoot.getNode("child" + i); + childNode.restore("1.0", false); + Node grandChildNode = childNode.getNode("grandChild"); + grandChildNode.restore("1.0", false); + // critical location regarding item state manager caching (see + // JCR-1197) + grandChildNode.checkout(); + grandChildNode.checkin(); + } + + session.logout(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RetentionRegistryImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RetentionRegistryImplTest.java new file mode 100644 index 00000000000..776573a5212 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/RetentionRegistryImplTest.java @@ -0,0 +1,349 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.retention.Hold; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.fs.mem.MemoryFileSystem; +import org.apache.jackrabbit.core.retention.AbstractRetentionTest; +import org.apache.jackrabbit.core.retention.RetentionRegistry; +import org.apache.jackrabbit.core.retention.RetentionRegistryImpl; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RetentionEvaluatorImplTest... + */ +public class RetentionRegistryImplTest extends AbstractRetentionTest { + + private static Logger log = LoggerFactory.getLogger(RetentionRegistryImplTest.class); + + private Node childN; + private String childNPath; + private Node childN2; + private Node childN3; + private Property childP; + + protected void setUp() throws Exception { + super.setUp(); + + if (!(superuser instanceof SessionImpl)) { + throw new NotExecutableException(); + } + + childN = testRootNode.addNode(nodeName1); + childNPath = childN.getPath(); + childN2 = testRootNode.addNode(nodeName2); + childN3 = childN.addNode(nodeName3); + + Value v = getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE1, RepositoryStub.PROP_PROP_TYPE1, "test"); + childP = testRootNode.setProperty(propertyName1, v); + testRootNode.save(); + + retentionMgr.addHold(childNPath, getHoldName(), true); + retentionMgr.setRetentionPolicy(childNPath, getApplicableRetentionPolicy("test")); + + superuser.save(); + } + + protected void tearDown() throws Exception { + try { + Hold[] hs = retentionMgr.getHolds(childNPath); + for (int i = 0; i < hs.length; i++) { + retentionMgr.removeHold(childNPath, hs[i]); + } + superuser.save(); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + try { + if (retentionMgr.getRetentionPolicy(childNPath) != null) { + retentionMgr.removeRetentionPolicy(childNPath); + } + superuser.save(); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + super.tearDown(); + } + + private FileSystem createFileSystem() { + FileSystem fs = new MemoryFileSystem(); + BufferedWriter writer = null; + try { + fs.createFolder("/"); + FileSystemResource file = new FileSystemResource(fs, "/retention"); + + writer = new BufferedWriter(new OutputStreamWriter(file.getOutputStream())); + writer.write(((NodeImpl) childN).getNodeId().toString()); + } catch (FileSystemException e) { + log.error(e.getMessage()); + } catch (IOException e) { + log.error(e.getMessage()); + } finally { + IOUtils.closeQuietly(writer); + } + return fs; + } + + public void testReadHoldFromFile() throws RepositoryException { + PathResolver resolver = (SessionImpl) superuser; + RetentionRegistryImpl re = new RetentionRegistryImpl((SessionImpl) superuser, createFileSystem()); + try { + assertTrue(re.hasEffectiveHold(resolver.getQPath(childNPath), false)); + assertTrue(re.hasEffectiveHold(resolver.getQPath(childN3.getPath()), false)); + assertTrue(re.hasEffectiveHold(resolver.getQPath(childNPath + "/somechild"), false)); + assertTrue(re.hasEffectiveHold(resolver.getQPath(childNPath + "/hold/is/deep"), false)); + + assertFalse(re.hasEffectiveHold(resolver.getQPath(testNodePath), false)); + assertFalse(re.hasEffectiveHold(resolver.getQPath(childN2.getPath()), false)); + + } finally { + re.close(); + } + } + + public void testReadRetentionFromFile() throws RepositoryException { + SessionImpl s = (SessionImpl) getHelper().getSuperuserSession(); + RetentionRegistryImpl re = new RetentionRegistryImpl(s, createFileSystem()); + try { + assertTrue(re.hasEffectiveRetention(s.getQPath(childNPath), false)); + assertTrue(re.hasEffectiveRetention(s.getQPath(childNPath + "/somechild"), true)); + + assertFalse(re.hasEffectiveRetention(s.getQPath(testNodePath), false)); + assertFalse(re.hasEffectiveRetention(s.getQPath(childNPath + "/somechild"), false)); + assertFalse(re.hasEffectiveRetention(s.getQPath(childNPath + "/somechild/deepchild"), true)); + assertFalse(re.hasEffectiveRetention(s.getQPath(childP.getPath()), false)); + + } finally { + re.close(); + s.logout(); + } + } + + public void testWriteFile() throws RepositoryException { + PathResolver resolver = (SessionImpl) superuser; + FileSystem fs = createFileSystem(); + RetentionRegistryImpl re = new RetentionRegistryImpl((SessionImpl) superuser, fs); + + try { + // write the changes to the fs + re.close(); + + // create a new dummy registry again. + re = new RetentionRegistryImpl((SessionImpl) superuser, fs); + + // test holds + assertTrue(re.hasEffectiveHold(resolver.getQPath(childNPath), false)); + assertTrue(re.hasEffectiveHold(resolver.getQPath(childN3.getPath()), false)); + assertTrue(re.hasEffectiveHold(resolver.getQPath(childNPath + "/somechild"), false)); + assertTrue(re.hasEffectiveHold(resolver.getQPath(childNPath + "/hold/is/deep"), false)); + + // test policies + assertTrue(re.hasEffectiveRetention(resolver.getQPath(childNPath), false)); + assertTrue(re.hasEffectiveRetention(resolver.getQPath(childNPath + "/somechild"), true)); + } finally { + re.close(); + } + } + + public void testWriteFileWithChanges() throws RepositoryException, NotExecutableException { + PathResolver resolver = (SessionImpl) superuser; + FileSystem fs = createFileSystem(); + RetentionRegistryImpl re = new RetentionRegistryImpl((SessionImpl) superuser, fs); + String childN3Path = childN3.getPath(); + try { + retentionMgr.removeRetentionPolicy(childNPath); + retentionMgr.removeHold(childNPath, retentionMgr.getHolds(childNPath)[0]); + superuser.save(); + + retentionMgr.setRetentionPolicy(childN3Path, getApplicableRetentionPolicy("retentionOnChild2")); + retentionMgr.addHold(childNPath, "holdOnChild", false); + superuser.save(); + + // write the changes to the fs + re.close(); + + // create a new dummy registry again. + re = new RetentionRegistryImpl((SessionImpl) superuser, fs); + + // test holds + assertTrue(re.hasEffectiveHold(resolver.getQPath(childNPath), false)); + assertTrue(re.hasEffectiveHold(resolver.getQPath(childNPath), true)); + assertTrue(re.hasEffectiveHold(resolver.getQPath(childN3Path), true)); + + assertFalse(re.hasEffectiveHold(resolver.getQPath(childN3Path), false)); + assertFalse(re.hasEffectiveHold(resolver.getQPath(childN3Path + "/child"), false)); + assertFalse(re.hasEffectiveHold(resolver.getQPath(childN3Path + "/child"), true)); + + // test policies + assertTrue(re.hasEffectiveRetention(resolver.getQPath(childN3Path), false)); + assertTrue(re.hasEffectiveRetention(resolver.getQPath(childN3Path + "/child"), true)); + + assertFalse(re.hasEffectiveRetention(resolver.getQPath(childN3Path + "/child/more"), true)); + assertFalse(re.hasEffectiveRetention(resolver.getQPath(childNPath), true)); + assertFalse(re.hasEffectiveRetention(resolver.getQPath(childNPath), false)); + } finally { + re.close(); + // remove the extra policy that is not cleared upon teardown + if (retentionMgr.getRetentionPolicy(childN3Path) != null) { + retentionMgr.removeRetentionPolicy(childN3.getPath()); + } + superuser.save(); + } + } + + public void testRemoveHold() throws RepositoryException { + SessionImpl s = (SessionImpl) getHelper().getSuperuserSession(); + RetentionRegistry re = s.getRetentionRegistry(); + try { + Hold[] holds = retentionMgr.getHolds(childNPath); + for (int i = 0; i < holds.length; i++) { + retentionMgr.removeHold(childNPath, holds[i]); + // hold must still be in effect. + assertTrue(re.hasEffectiveHold(s.getQPath(childNPath), false)); + } + superuser.save(); + + assertFalse(re.hasEffectiveHold(s.getQPath(childNPath), false)); + + } finally { + s.logout(); + } + } + + public void testRemoveRetentionPolicy() throws RepositoryException { + SessionImpl s = (SessionImpl) getHelper().getSuperuserSession(); + RetentionRegistry re = s.getRetentionRegistry(); + try { + retentionMgr.removeRetentionPolicy(childNPath); + // retention must still be in effect. + assertTrue(re.hasEffectiveRetention(s.getQPath(childNPath), false)); + + superuser.save(); + + assertFalse(re.hasEffectiveRetention(s.getQPath(childNPath), false)); + + } finally { + s.logout(); + } + } + + public void testAddHold() throws RepositoryException, NotExecutableException { + SessionImpl s = (SessionImpl) getHelper().getSuperuserSession(); + RetentionRegistry re = s.getRetentionRegistry(); + Hold h = null; + try { + h = retentionMgr.addHold(childN2.getPath(), getHoldName(), false); + // hold must not be effective yet + assertFalse(re.hasEffectiveHold(s.getQPath(childN2.getPath()), false)); + + superuser.save(); + assertTrue(re.hasEffectiveHold(s.getQPath(childN2.getPath()), false)); + + } finally { + s.logout(); + + if (h != null) { + retentionMgr.removeHold(childN2.getPath(), h); + superuser.save(); + } + } + } + + public void testAddMultipleHold() throws RepositoryException, NotExecutableException { + SessionImpl s = (SessionImpl) getHelper().getSuperuserSession(); + RetentionRegistry re = s.getRetentionRegistry(); + try { + retentionMgr.addHold(childN2.getPath(), getHoldName(), false); + superuser.save(); + + // hold is not deep -> only applies to childN2 + assertTrue(re.hasEffectiveHold(s.getQPath(childN2.getPath()), false)); + assertFalse(re.hasEffectiveHold(s.getQPath(childN2.getPath() + "/child"), false)); + + retentionMgr.addHold(childN2.getPath(), getHoldName(), true); + superuser.save(); + + // now deep hold must be taken into account + assertTrue(re.hasEffectiveHold(s.getQPath(childN2.getPath()), false)); + assertTrue(re.hasEffectiveHold(s.getQPath(childN2.getPath() + "/child"), false)); + + } finally { + s.logout(); + + Hold[] hdls = retentionMgr.getHolds(childN2.getPath()); + for (int i = 0; i < hdls.length; i++) { + retentionMgr.removeHold(childN2.getPath(), hdls[i]); + } + superuser.save(); + } + } + + public void testSetRetentionPolicy() throws RepositoryException, NotExecutableException { + SessionImpl s = (SessionImpl) getHelper().getSuperuserSession(); + RetentionRegistry re = s.getRetentionRegistry(); + try { + retentionMgr.setRetentionPolicy(childN2.getPath(), getApplicableRetentionPolicy("test2")); + // retention must not be effective yet + assertFalse(re.hasEffectiveRetention(s.getQPath(childN2.getPath()), false)); + + superuser.save(); + assertTrue(re.hasEffectiveRetention(s.getQPath(childN2.getPath()), false)); + + } finally { + s.logout(); + + retentionMgr.removeRetentionPolicy(childN2.getPath()); + superuser.save(); + } + } + + public void testChangeRetentionPolicy() throws RepositoryException, NotExecutableException { + SessionImpl s = (SessionImpl) getHelper().getSuperuserSession(); + RetentionRegistry re = s.getRetentionRegistry(); + try { + retentionMgr.setRetentionPolicy(childN2.getPath(), getApplicableRetentionPolicy("test2")); + superuser.save(); + retentionMgr.setRetentionPolicy(childN2.getPath(), getApplicableRetentionPolicy("test3")); + superuser.save(); + + assertTrue(re.hasEffectiveRetention(s.getQPath(childN2.getPath()), false)); + + } finally { + s.logout(); + + retentionMgr.removeRetentionPolicy(childN2.getPath()); + superuser.save(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/SessionGarbageCollectedTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/SessionGarbageCollectedTest.java new file mode 100644 index 00000000000..2a8ace97f4d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/SessionGarbageCollectedTest.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.core; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test if sessions get garbage collected. + */ +public class SessionGarbageCollectedTest extends AbstractJCRTest { + + public void testSessionsGetGarbageCollected() throws RepositoryException { + ArrayList> list = new ArrayList>(); + ReferenceQueue detect = new ReferenceQueue(); + Error error = null; + try { + for (int i = 0;; i++) { + Session s = getHelper().getReadWriteSession(); + // eat a lot of memory so it gets garbage collected quickly + // (or quickly runs out of memory) + Node n = s.getRootNode().addNode("n" + i); + n.setProperty("x", new String(new char[1000000])); + list.add(new WeakReference(s, detect)); + if (detect.poll() != null) { + break; + } + } + } catch (OutOfMemoryError e) { + error = e; + } + for (int i = 0; i < list.size(); i++) { + Reference ref = list.get(i); + Session s = ref.get(); + if (s != null) { + s.logout(); + } + } + if (error != null) { + throw error; + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ShareableNodeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ShareableNodeTest.java new file mode 100644 index 00000000000..a5d61d7d765 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/ShareableNodeTest.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.core; + +import javax.jcr.Node; +import javax.jcr.Workspace; +import javax.jcr.NodeIterator; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.ObservationManager; + +import org.apache.jackrabbit.core.observation.SynchronousEventListener; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Tests features available with shareable nodes. + */ +public class ShareableNodeTest extends AbstractJCRTest { + + //------------------------------------------------------ specification tests + + /** + * Verify that observation events are sent only once (6.13.15). + */ + public void testObservation() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.save(); + + // add mixin + b1.addMixin("mix:shareable"); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // event listener that counts events received + class EventCounter implements SynchronousEventListener { + + private int count; + + public void onEvent(EventIterator events) { + while (events.hasNext()) { + events.nextEvent(); + count++; + } + } + + public int getEventCount() { + return count; + } + + public void resetCount() { + count = 0; + } + } + + EventCounter el = new EventCounter(); + ObservationManager om = superuser.getWorkspace().getObservationManager(); + + // add node underneath shared set: verify it generates one event only + om.addEventListener(el, Event.NODE_ADDED, testRootNode.getPath(), + true, null, null, false); + b1.addNode("c"); + b1.save(); + superuser.getWorkspace().getObservationManager().removeEventListener(el); + assertEquals(1, el.getEventCount()); + + // remove node underneath shared set: verify it generates one event only + el.resetCount(); + om.addEventListener(el, Event.NODE_REMOVED, testRootNode.getPath(), + true, null, null, false); + b1.getNode("c").remove(); + b1.save(); + superuser.getWorkspace().getObservationManager().removeEventListener(el); + assertEquals(1, el.getEventCount()); + + // add property underneath shared set: verify it generates one event only + el.resetCount(); + om.addEventListener(el, Event.PROPERTY_ADDED, testRootNode.getPath(), + true, null, null, false); + b1.setProperty("c", "1"); + b1.save(); + superuser.getWorkspace().getObservationManager().removeEventListener(el); + assertEquals(1, el.getEventCount()); + + // modify property underneath shared set: verify it generates one event only + el.resetCount(); + om.addEventListener(el, Event.PROPERTY_CHANGED, testRootNode.getPath(), + true, null, null, false); + b1.setProperty("c", "2"); + b1.save(); + superuser.getWorkspace().getObservationManager().removeEventListener(el); + assertEquals(1, el.getEventCount()); + + // remove property underneath shared set: verify it generates one event only + el.resetCount(); + om.addEventListener(el, Event.PROPERTY_REMOVED, testRootNode.getPath(), + true, null, null, false); + b1.getProperty("c").remove(); + b1.save(); + superuser.getWorkspace().getObservationManager().removeEventListener(el); + assertEquals(1, el.getEventCount()); + } + + /** + * Verify that a shared node is removed when the ancestor is removed. + * See also JCR-2257. + */ + public void testRemoveAncestorOfSharedNode() throws Exception { + Node a1 = testRootNode.addNode("a1"); + Node a2 = a1.addNode("a2"); + Node b1 = a1.addNode("b1"); + ensureMixinType(b1, mixShareable); + testRootNode.save(); + //now we have a shareable node N with path a1/b1 + + Workspace workspace = testRootNode.getSession().getWorkspace(); + String path = a2.getPath() + "/b2"; + workspace.clone(workspace.getName(), b1.getPath(), path, false); + testRootNode.save(); + //now we have another shareable node N' in the same shared set as N with path a1/a2/b2 + + a2.remove(); + testRootNode.save(); + + // assert b2 is removed from index + QueryManager qm = superuser.getWorkspace().getQueryManager(); + String stmt = testPath + "//b2"; + NodeIterator it = qm.createQuery(stmt, Query.XPATH).execute().getNodes(); + assertFalse(it.hasNext()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/Tail.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/Tail.java new file mode 100644 index 00000000000..2af715dd18f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/Tail.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.core; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Iterator; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.commons.iterator.FilterIterator; +import org.apache.jackrabbit.commons.predicate.Predicate; + +/** + * Tail is a test utility class to tail and grep a text file. + */ +public class Tail implements Closeable { + + private final String grep; + + private final BufferedReader reader; + + private Tail(File file, String grep) throws IOException { + this.grep = grep; + this.reader = new BufferedReader(new InputStreamReader( + new FileInputStream(file))); + while (reader.skip(Integer.MAX_VALUE) > 0) { + // skip more, until end of file + } + } + + /** + * Create a tail on the given file with an optional string to + * match lines. + * + * @param file the file to tail. + * @param grep the string to match or null if all lines should + * be returned. + * @return a tail on the file. + * @throws IOException if the files does not exist or some other I/O error + * occurs. + */ + public static Tail start(File file, String grep) throws IOException { + return new Tail(file, grep); + } + + /** + * Returns the lines that were written to the file since + * Tail.start() or the last call to getLines(). + * + * @return the matching lines. + * @throws IOException if an error occurs while reading from the file. + */ + public Iterable getLines() throws IOException { + return new Iterable() { + public Iterator iterator() { + Iterator it = IOUtils.lineIterator(reader); + if (grep == null || grep.length() == 0) { + return it; + } else { + // filter + return new FilterIterator(it, new Predicate() { + public boolean evaluate(Object o) { + return o.toString().contains(grep); + } + }); + } + } + }; + } + + /** + * Releases the underlying stream from the file. + * + * @throws IOException If an I/O error occurs. + */ + public void close() throws IOException { + reader.close(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestAll.java new file mode 100644 index 00000000000..ef5ca389d4c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestAll.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.core; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +/** + * Test suite that includes all testcases for the Core module. + */ +public class TestAll extends TestCase { + + /** + * @return a Test suite that executes all tests inside this + * package, except the multi-threading related ones. + */ + public static Test suite() { + TestSuite suite = new ConcurrentTestSuite("Core tests"); + + suite.addTestSuite(ReplacePropertyWhileOthersReadTest.class); + suite.addTestSuite(CachingHierarchyManagerTest.class); + suite.addTestSuite(ShareableNodeTest.class); + suite.addTestSuite(MultiWorkspaceShareableNodeTest.class); + suite.addTestSuite(TransientRepositoryTest.class); + suite.addTestSuite(XATest.class); + suite.addTestSuite(RestoreAndCheckoutTest.class); + suite.addTestSuite(NodeImplTest.class); + suite.addTestSuite(RetentionRegistryImplTest.class); + suite.addTestSuite(InvalidDateTest.class); + suite.addTestSuite(SessionGarbageCollectedTest.class); + suite.addTestSuite(ReferencesTest.class); + suite.addTestSuite(ReplaceTest.class); + + // test related to NodeStateMerger + suite.addTestSuite(ConcurrentImportTest.class); + suite.addTestSuite(ConcurrentAddRemoveMoveTest.class); + suite.addTestSuite(ConcurrentAddRemovePropertyTest.class); + suite.addTestSuite(ConcurrentMixinModificationTest.class); + suite.addTestSuite(ConcurrentModificationWithSNSTest.class); + suite.addTestSuite(ConcurrentMoveTest.class); + suite.addTestSuite(ConcurrentReorderTest.class); + suite.addTestSuite(ConcurrentAddRemoveNodeTest.class); + + suite.addTestSuite(LostFromCacheIssueTest.class); + + // TODO: These tests pass, but they cause some instability in other + // parts of the test suite, most likely due to uncleaned test data + if (Boolean.getBoolean("org.apache.jackrabbit.test.integration")) { + suite.addTestSuite(ConcurrencyTest.class); +// // suite.addTestSuite(ConcurrencyTest3.class); + suite.addTestSuite(ConcurrentVersioningTest.class); +// // suite.addTestSuite(ConcurrentVersioningWithTransactionsTest.class); +// suite.addTestSuite(ConcurrentCheckinMixedTransactionTest.class); +// suite.addTestSuite(ConcurrentLoginTest.class); +// suite.addTestSuite(ConcurrentNodeModificationTest.class); +// suite.addTestSuite(ConcurrentReadWriteTest.class); + suite.addTestSuite(ConcurrentRenameTest.class); + suite.addTestSuite(ConcurrentSaveTest.class); +// suite.addTestSuite(ConcurrentWorkspaceCopyTest.class); + } + + suite.addTestSuite(UserPerWorkspaceSecurityManagerTest.class); + suite.addTestSuite(OverlappingNodeAddTest.class); + suite.addTestSuite(NPEandCMETest.class); + suite.addTestSuite(ConsistencyCheck.class); + suite.addTestSuite(RemoveAddNodeWithUUIDTest.class); + suite.addTestSuite(MoveAtRootTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestHelper.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestHelper.java new file mode 100644 index 00000000000..84aa1774751 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestHelper.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.core; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.persistence.check.ConsistencyChecker; +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; +import org.apache.jackrabbit.core.query.QueryHandler; +import org.apache.jackrabbit.core.query.lucene.ConsistencyCheck; +import org.apache.jackrabbit.core.query.lucene.SearchIndex; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * TestHelper provides test utility methods. + */ +public class TestHelper { + + /** + * Shuts down the workspace with the given name. + * + * @param name the name of the workspace to shut down. + * @param repo the repository. + * @throws RepositoryException if the shutdown fails or there is no + * workspace with the given name. + */ + public static void shutdownWorkspace(String name, RepositoryImpl repo) + throws RepositoryException { + repo.getWorkspaceInfo(name).dispose(); + } + + /** + * Runs a consistency check on the workspace used by the specified session. + * + * @param session the Session accessing the workspace to be checked + * @param runFix whether to attempt fixup + * @param lostNFoundId node to which to attach orphaned nodes (or null) + * @throws RepositoryException if an error occurs while getting the + * workspace with the given name. + * @throws NotExecutableException if the {@link PersistenceManager} does + * not implement {@link ConsistencyChecker}, or if the associated + * {@link Repository} is not a {@link RepositoryImpl}. + */ + public static ConsistencyReport checkConsistency(Session session, boolean runFix, String lostNFoundId) + throws NotExecutableException, RepositoryException { + Repository r = session.getRepository(); + if (!(r instanceof RepositoryImpl)) { + throw new NotExecutableException(); + } else { + RepositoryImpl ri = (RepositoryImpl) r; + PersistenceManager pm = ri.getWorkspaceInfo( + session.getWorkspace().getName()).getPersistenceManager(); + if (!(pm instanceof ConsistencyChecker)) { + throw new NotExecutableException(); + } else { + return ((ConsistencyChecker) pm).check(null, true, runFix, lostNFoundId, null); + } + } + } + + public static ConsistencyCheck checkIndexConsistency(Session session) throws RepositoryException, NotExecutableException, IOException { + Repository r = session.getRepository(); + if (!(r instanceof RepositoryImpl)) { + throw new NotExecutableException(); + } + RepositoryImpl ri = (RepositoryImpl) r; + final String workspaceName = session.getWorkspace().getName(); + QueryHandler qh = ri.getSearchManager(workspaceName).getQueryHandler(); + if (!(qh instanceof SearchIndex)) { + throw new NotExecutableException("No search index"); + } + SearchIndex si = (SearchIndex) qh; + return si.runConsistencyCheck(); + } + + /** + * Runs a consistency check on the versioning store used by the specified session. + * + * @param session the Session accessing the workspace to be checked + * @param runFix whether to attempt fixup + * @param lostNFoundId node to which to attach orphaned nodes (or null) + * @throws RepositoryException + * @throws NotExecutableException if the {@link PersistenceManager} does + * not implement {@link ConsistencyChecker}, or if the associated + * {@link Repository} is not a {@link RepositoryImpl}. + */ + public static ConsistencyReport checkVersionStoreConsistency(Session session, boolean runFix, String lostNFoundId) + throws NotExecutableException, RepositoryException { + Repository r = session.getRepository(); + if (!(r instanceof RepositoryImpl)) { + throw new NotExecutableException(); + } else { + RepositoryImpl ri = (RepositoryImpl) r; + PersistenceManager pm = ri.getRepositoryContext() + .getInternalVersionManager().getPersistenceManager(); + if (!(pm instanceof ConsistencyChecker)) { + throw new NotExecutableException(); + } else { + return ((ConsistencyChecker) pm).check(null, true, runFix, lostNFoundId, null); + } + } + } + + /** + * wait for async text-extraction tasks to finish + */ + public static void waitForTextExtractionTasksToFinish(Session session) throws Exception { + final RepositoryContext context = JackrabbitRepositoryStub + .getRepositoryContext(session.getRepository()); + JackrabbitThreadPool jtp = ((JackrabbitThreadPool) context + .getExecutor()); + while (jtp.getPendingLowPriorityTaskCount() != 0) { + TimeUnit.MILLISECONDS.sleep(100); + } + } + + public static SearchManager getSearchManager(Session session) throws NotExecutableException, RepositoryException { + Repository r = session.getRepository(); + if (!(r instanceof RepositoryImpl)) { + throw new NotExecutableException(); + } + RepositoryImpl ri = (RepositoryImpl) r; + return ri.getSearchManager(session.getWorkspace().getName()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestRepository.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestRepository.java new file mode 100644 index 00000000000..1eacfa5f792 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TestRepository.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.core; + +import java.io.InputStream; +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.collections.BeanMap; +import org.apache.jackrabbit.commons.repository.ProxyRepository; +import org.apache.jackrabbit.commons.repository.RepositoryFactory; +import org.apache.jackrabbit.core.config.ConfigurationException; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +/** + * Utility class for easy handling a test repository. This class contains + * a static test repository instance for use by test cases. The + * {@link javax.jcr.Repository#login()} method of the test repository + * instance should return a session with full read-write access. + */ +public class TestRepository { + + /** + * Name of the resource containing the test repository configuration. + * The test repository configuration is located inside the Jackrabbit + * jar file to enforce a standard test environment. + */ + private static final String CONF_RESOURCE = "repository.xml"; + + /** + * Name of the system property that can be used to override the + * default test repository location. + */ + private static final String HOME_PROPERTY = + "org.apache.jackrabbit.test.repository.home"; + + /** + * Default test repository location. + */ + private static final String HOME_DEFAULT = "jackrabbit-test-repository"; + + /** + * The test repository instance. + */ + private static Repository instance = null; + + /** + * Returns the test repository instance. If a repository instance has + * not yet been registered using {@link #setInstance(Repository)} as + * the test repostitory, then a simple {@link TransientRepository} + * instance is created with the standard test repository configuration + * and the test repository location (either "jackrabbit-test-repository" + * or the value of the "org.apache.jackrabbit.test.repository.home" + * system property). + * + * @return test repository instance + * @throws RepositoryException if a test repository can not be instantiated + */ + public static synchronized Repository getInstance() throws RepositoryException { + try { + if (instance == null) { + try { + // Try to get the main test suite repository + instance = getIntegratedInstance(); + } catch (RepositoryException e) { + throw e; + } catch (Exception e) { + // Not running within the main test suite + InputStream xml = + TestRepository.class.getResourceAsStream(CONF_RESOURCE); + String home = System.getProperty(HOME_PROPERTY, HOME_DEFAULT); + RepositoryConfig config = RepositoryConfig.create(xml, home); + instance = new TransientRepository(config); + } + } + return instance; + } catch (ConfigurationException e) { + throw new RepositoryException( + "Error in test repository configuration", e); + } + } + + /** + * Attempts to retrieve the test repository instance used by the + * Jackrabbit main test suite without having a direct dependency to any + * of the classes in src/test/java. This method assumes that we are + * running within the Jackrabbit main test suite if the AbstractJCRTest + * class is available. The initialized RepositoryHelper instance is + * retrieved from the static "helper" field of the AbstractJCRTest class, + * and the underlying repository and configured superuser credentials are + * extracted from the helper instance. This information is in turn used + * to create a custom Repository adapter that delegates calls to the + * underlying repository and uses the superuser credentials for the login + * methods where no credentials are passed by the client. + * + * @return test repository instance + * @throws Exception if the test repository could not be retrieved + */ + private static Repository getIntegratedInstance() throws Exception { + Class test = + Class.forName("org.apache.jackrabbit.test.AbstractJCRTest"); + Map helper = new BeanMap(test.getField("helper").get(null)); + final Repository repository = + (Repository) helper.get("repository"); + final Credentials superuser = + (Credentials) helper.get("superuserCredentials"); + return new ProxyRepository(new RepositoryFactory() { + + public Repository getRepository() throws RepositoryException { + return repository; + } + + }) { + + public Session login(String workspace) throws RepositoryException { + return repository.login(superuser, workspace); + } + + public Session login() throws RepositoryException { + return repository.login(superuser); + } + + }; + } + + /** + * Sets the given repository as the test repository instance. This method + * is designed for use by the main Jackrabbit test suite to facilitate + * smooth integration of standalone test cases. + * + * @param repository test repository + */ + public static synchronized void setInstance(Repository repository) { + instance = repository; + } + + /** + * Private constructor to prevent instantiation of this class. + */ + private TestRepository() { + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TransientRepositoryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TransientRepositoryTest.java new file mode 100644 index 00000000000..f5f0ce837f5 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/TransientRepositoryTest.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.core; + +import java.io.IOException; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import junit.framework.TestCase; + +/** + * Test case for the {@link TransientRepository} class. Currently only the + * static repository descriptor access is tested due to the difficulty of + * setting up mock {@link RepositoryImpl} instances. + */ +public class TransientRepositoryTest extends TestCase { + + /** + * The TransientRepository instance under test. + */ + private Repository repository; + + /** + * Creates the TransientRepository instance used in testing. + */ + protected void setUp() throws IOException { + repository = new TransientRepository(new TransientRepository.RepositoryFactory() { + public RepositoryImpl getRepository() throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + }, null); + } + + /** + * Tests that {@link TransientRepository} returns descriptor keys + * even before the underlying repository has been initialized. + */ + public void testGetDescriptorKeys() { + String[] keys = repository.getDescriptorKeys(); + assertNotNull(keys); + assertTrue(keys.length > 0); + } + + /** + * Tests that {@link TransientRepository} returns descriptor values + * even before the underlying repository has been initialized. + */ + public void testGetDescriptor() { + String expected = + "Content Repository API for Java(TM) Technology Specification"; + String actual = repository.getDescriptor(Repository.SPEC_NAME_DESC); + assertEquals(expected, actual); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/UserPerWorkspaceSecurityManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/UserPerWorkspaceSecurityManagerTest.java new file mode 100644 index 00000000000..c43426c76d4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/UserPerWorkspaceSecurityManagerTest.java @@ -0,0 +1,355 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +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.core.security.JackrabbitSecurityManager; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.Item; +import javax.jcr.LoginException; +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 java.security.Principal; +import java.util.Arrays; + +/** + * SecurityManagerTest... + */ +public class UserPerWorkspaceSecurityManagerTest extends AbstractJCRTest { + + private JackrabbitSecurityManager secMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + RepositoryImpl repo = (RepositoryImpl) superuser.getRepository(); + secMgr = repo.getRepositoryContext().getSecurityManager(); + if (!(secMgr instanceof UserPerWorkspaceSecurityManager)) { + throw new NotExecutableException(); + } + } + + private String getAlternativeWorkspaceName() throws RepositoryException { + String altWsp = null; + String[] wsps = superuser.getWorkspace().getAccessibleWorkspaceNames(); + if (wsps.length == 1) { + superuser.getWorkspace().createWorkspace("tmp"); + altWsp = "tmp"; + } else { + for (String name : wsps) { + if (!name.equals(superuser.getWorkspace().getName())) { + altWsp = name; + break; + } + } + } + return altWsp; + } + + public void testSystemUserCreation() throws Exception { + String altWsp = getAlternativeWorkspaceName(); + if (altWsp == null) { + throw new NotExecutableException(); + } + + // system users must be automatically be created -> login with admin + // must therefore succeed. + Session s = getHelper().getSuperuserSession(altWsp); + try { + // admin/anonymous must be present + String adminId = ((UserPerWorkspaceSecurityManager) secMgr).adminId; + assertEquals(adminId, s.getUserID()); + + UserManager umgr = ((JackrabbitSession) s).getUserManager(); + assertNotNull(umgr.getAuthorizable(adminId)); + assertNotNull(umgr.getAuthorizable(((UserPerWorkspaceSecurityManager) secMgr).anonymousId)); + + } finally { + s.logout(); + } + } + + public void testSystemUsersAreSaved() throws Exception { + String adminId = ((UserPerWorkspaceSecurityManager) secMgr).adminId; + UserManager umgr = ((JackrabbitSession) superuser).getUserManager(); + Principal p = umgr.getAuthorizable(adminId).getPrincipal(); + + if (p instanceof ItemBasedPrincipal) { + Item item = superuser.getItem(((ItemBasedPrincipal) p).getPath()); + assertFalse(item.isNew()); + assertFalse(item.isModified()); + } + } + + public void testUsersArePerWorkspace() throws Exception { + String altWsp = getAlternativeWorkspaceName(); + if (altWsp == null) { + throw new NotExecutableException(); + } + + Session s = getHelper().getSuperuserSession(altWsp); + User u = null; + try { + // other users created in the default workspace... + u = ((JackrabbitSession) superuser).getUserManager().createUser("testUser", "testUser"); + superuser.save(); + + // ... must not be present in the alternate-workspace + UserManager umgr = ((JackrabbitSession) s).getUserManager(); + assertNull(umgr.getAuthorizable("testUser")); + + try { + Session us = getHelper().getRepository().login(new SimpleCredentials("testUser", "testUser".toCharArray()), altWsp); + us.logout(); + fail("testUser must not be able to login to a workspace without this user."); + } catch (LoginException e) { + // success + } + + } finally { + s.logout(); + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + public void testAccessibleWorkspaceNames() throws Exception { + String altWsp = getAlternativeWorkspaceName(); + if (altWsp == null) { + throw new NotExecutableException(); + } + + Session s = getHelper().getSuperuserSession(altWsp); + User u = null; + Session us = null; + try { + // other users created in the default workspace... + u = ((JackrabbitSession) superuser).getUserManager().createUser("testUser", "testUser"); + superuser.save(); + + us = getHelper().getRepository().login(new SimpleCredentials("testUser", "testUser".toCharArray())); + String[] wspNames = us.getWorkspace().getAccessibleWorkspaceNames(); + assertFalse(Arrays.asList(wspNames).contains(altWsp)); + + } finally { + s.logout(); + if (us != null) { + us.logout(); + } + if (u != null) { + u.remove(); + superuser.save(); + } + } + + } + + public void testCloneUser() throws Exception { + String altWsp = getAlternativeWorkspaceName(); + if (altWsp == null) { + throw new NotExecutableException(); + } + + UserManager uMgr = ((JackrabbitSession) superuser).getUserManager(); + + Session s = getHelper().getSuperuserSession(altWsp); + User u = null; + try { + // other users created in the default workspace... + u = uMgr.createUser("testUser", "testUser"); + superuser.save(); + + String userPath = null; + if (u.getPrincipal() instanceof ItemBasedPrincipal) { + userPath = ((ItemBasedPrincipal) u.getPrincipal()).getPath(); + assertTrue(superuser.nodeExists(userPath)); + } else { + throw new NotExecutableException(); + } + + // ... must not be present in the alternate-workspace + UserManager umgr = ((JackrabbitSession) s).getUserManager(); + assertNull(umgr.getAuthorizable("testUser")); + assertFalse(s.nodeExists(userPath)); + + String clonePath = userPath; + String parentPath = Text.getRelativeParent(clonePath, 1); + while (!s.nodeExists(parentPath)) { + clonePath = parentPath; + parentPath = Text.getRelativeParent(parentPath, 1); + } + + // clone the user into the second workspace + s.getWorkspace().clone(superuser.getWorkspace().getName(), clonePath, clonePath, true); + + // ... now the user must be visible + assertNotNull(umgr.getAuthorizable("testUser")); + if (userPath != null) { + assertTrue(s.nodeExists(userPath)); + } + // ... and able to login to that workspace + Session us = getHelper().getRepository().login(new SimpleCredentials("testUser", "testUser".toCharArray()), altWsp); + us.logout(); + + } finally { + // remove the test user in the second workspace + Authorizable dest = ((JackrabbitSession) s).getUserManager().getAuthorizable("testUser"); + if (dest != null) { + dest.remove(); + s.save(); + } + // logout the session + s.logout(); + if (u != null) { + // remove as well in the first workspace + u.remove(); + superuser.save(); + } + } + } + + public void testUpdateUser() throws NotExecutableException, RepositoryException { + // create the same use in 2 different workspace must make the 'corresponding' + // and updating must succeed + String altWsp = getAlternativeWorkspaceName(); + if (altWsp == null) { + throw new NotExecutableException(); + } + + UserManager uMgr = ((JackrabbitSession) superuser).getUserManager(); + + Session s = getHelper().getSuperuserSession(altWsp); + User u = null; + try { + // other users created in the default workspace... + u = uMgr.createUser("testUser", "testUser"); + superuser.save(); + + String userPath = null; + if (u.getPrincipal() instanceof ItemBasedPrincipal) { + userPath = ((ItemBasedPrincipal) u.getPrincipal()).getPath(); + assertTrue(superuser.nodeExists(userPath)); + } else { + throw new NotExecutableException(); + } + + // ... must not be present in the alternate-workspace + UserManager umgr = ((JackrabbitSession) s).getUserManager(); + assertNull(umgr.getAuthorizable("testUser")); + assertFalse(s.nodeExists(userPath)); + + User u2 = umgr.createUser("testUser", "testUser"); + s.save(); + assertTrue(s.nodeExists(userPath)); + + Value value = superuser.getValueFactory().createValue("anyValue"); + u.setProperty(propertyName1, value); + superuser.save(); + + // no automatic sync. + assertFalse(u2.hasProperty(propertyName1)); + + // update nodes + Node n2 = s.getNode(userPath); + n2.update(superuser.getWorkspace().getName()); + + // now the value must be visible + assertTrue(u2.hasProperty(propertyName1)); + assertEquals(value.getString(), u2.getProperty(propertyName1)[0].getString()); + + } finally { + // remove the test user in the destination workspace + Authorizable dest = ((JackrabbitSession) s).getUserManager().getAuthorizable("testUser"); + if (dest != null) { + dest.remove(); + s.save(); + } + // logout the session to the destination workspace + s.logout(); + if (u != null) { + // and remove it in the default workspace as well + u.remove(); + superuser.save(); + } + } + } + + public void testNewUsersCanLogin() throws Exception { + Session s = null; + User u = null; + try { + // other users created in the default workspace... + u = ((JackrabbitSession) superuser).getUserManager().createUser("testUser", "testUser"); + superuser.save(); + + // the new user must be able to login to the repo + s = getHelper().getRepository().login(new SimpleCredentials("testUser", "testUser".toCharArray())); + + } finally { + if (s != null) { + s.logout(); + } + if (u != null) { + u.remove(); + superuser.save(); + } + } + } + + public void testTransientUserCannotLogin() throws RepositoryException, UnsupportedRepositoryOperationException { + Session s = null; + String uid = "testUser"; + UserManager umgr = ((JackrabbitSession) superuser).getUserManager(); + umgr.autoSave(false); + try { + // other users created in the default workspace... + umgr.createUser(uid, uid); + // the new user must be able to login to the repo + s = getHelper().getRepository().login(new SimpleCredentials(uid, uid.toCharArray())); + + fail("Non-saved user node -> must not be able to login."); + + } catch (LoginException e) { + // success + } finally { + if (s != null) { + s.logout(); + } + superuser.refresh(false); + Authorizable a = ((JackrabbitSession) superuser).getUserManager().getAuthorizable(uid); + if (a != null) { + a.remove(); + superuser.save(); + } + umgr.autoSave(true); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/UserTransactionImpl.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/UserTransactionImpl.java new file mode 100644 index 00000000000..c81b6854416 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/UserTransactionImpl.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; +import javax.transaction.xa.XAException; +import javax.transaction.UserTransaction; +import javax.transaction.Status; +import javax.transaction.NotSupportedException; +import javax.transaction.SystemException; +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.RollbackException; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.XASession; + +/** + * Internal {@link javax.transaction.UserTransaction} implementation. + */ +public class UserTransactionImpl implements UserTransaction { + + /** + * Global transaction id counter + */ + private static byte counter = 0; + + /** + * The XAResources map + */ + private Map xaResources = new HashMap(); + + /** + * Status + */ + private int status = Status.STATUS_NO_TRANSACTION; + + private boolean distributedThreadAccess = false; + + /** + * Create a new instance of this class. Takes a session as parameter. + * @param session session. If session is not of type + * {@link XAResource}, an IllegalArgumentException + * is thrown + */ + public UserTransactionImpl(Session session) { + this(session, false); + } + + /** + * Create a new instance of this class. Takes a session as parameter. + * @param session session. If session is not of type + * {@link XAResource}, an IllegalArgumentException + * is thrown + */ + public UserTransactionImpl(Session session, boolean distributedThreadAccess) { + if (session instanceof XAResource) { + counter++; + xaResources.put((XAResource) session, new XidImpl(counter)); + this.distributedThreadAccess = distributedThreadAccess; + } else { + throw new IllegalArgumentException("Session not of type XAResource"); + } + } + + /** + * Enlists the given Session to this UserTransaction + * @param session + */ + public void enlistXAResource(Session session) { + xaResources.put(session, new XidImpl(counter)); + } + + /** + * @see javax.transaction.UserTransaction#begin + */ + public void begin() throws NotSupportedException, SystemException { + if (status != Status.STATUS_NO_TRANSACTION) { + throw new IllegalStateException("Transaction already active"); + } + + try { + for (Iterator it = xaResources.keySet().iterator(); it.hasNext(); ) { + XAResource resource = (XAResource) it.next(); + XidImpl xid = (XidImpl) xaResources.get(resource); + resource.start(xid, XAResource.TMNOFLAGS); + } + status = Status.STATUS_ACTIVE; + + } catch (XAException e) { + + throw new SystemException("Unable to begin transaction: " + + "XA_ERR=" + e.errorCode); + } + } + + /** + * @see javax.transaction.UserTransaction#commit + */ + public void commit() throws HeuristicMixedException, + HeuristicRollbackException, IllegalStateException, + RollbackException, SecurityException, SystemException { + + if (status != Status.STATUS_ACTIVE) { + throw new IllegalStateException("Transaction not active"); + } + + try { + for (Iterator it = xaResources.keySet().iterator(); it.hasNext(); ) { + XAResource resource = (XAResource) it.next(); + XidImpl xid = (XidImpl) xaResources.get(resource); + resource.end(xid, XAResource.TMSUCCESS); + } + + status = Status.STATUS_PREPARING; + for (Iterator it = xaResources.keySet().iterator(); it.hasNext(); ) { + XAResource resource = (XAResource) it.next(); + XidImpl xid = (XidImpl) xaResources.get(resource); + resource.prepare(xid); + } + status = Status.STATUS_PREPARED; + + status = Status.STATUS_COMMITTING; + if (distributedThreadAccess) { + Thread distributedThread = new Thread() { + public void run() { + try { + for (Iterator it = xaResources.keySet().iterator(); it.hasNext(); ) { + XAResource resource = (XAResource) it.next(); + XidImpl xid = (XidImpl) xaResources.get(resource); + resource.commit(xid, false); + } + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + }; + distributedThread.start(); + distributedThread.join(1000); + if (distributedThread.isAlive()) { + throw new SystemException( + "Commit from different thread but same XID must not block"); + } + } else { + for (Iterator it = xaResources.keySet().iterator(); it.hasNext(); ) { + XAResource resource = (XAResource) it.next(); + XidImpl xid = (XidImpl) xaResources.get(resource); + resource.commit(xid, false); + } + } + + status = Status.STATUS_COMMITTED; + + } catch (XAException e) { + + if (e.errorCode >= XAException.XA_RBBASE && + e.errorCode <= XAException.XA_RBEND) { + RollbackException re = new RollbackException( + "Transaction rolled back: XA_ERR=" + e.errorCode); + re.initCause(e.getCause()); + throw re; + } else { + SystemException se = new SystemException( + "Unable to commit transaction: XA_ERR=" + e.errorCode); + se.initCause(e.getCause()); + throw se; + } + } catch (InterruptedException e) { + throw new SystemException("Thread.join() interrupted"); + } + } + + /** + * @see javax.transaction.UserTransaction#getStatus + */ + public int getStatus() throws SystemException { + return status; + } + + /** + * @see javax.transaction.UserTransaction#rollback + */ + public void rollback() throws IllegalStateException, SecurityException, + SystemException { + + if (status != Status.STATUS_ACTIVE && + status != Status.STATUS_MARKED_ROLLBACK) { + + throw new IllegalStateException("Transaction not active"); + } + + try { + for (Iterator it = xaResources.keySet().iterator(); it.hasNext(); ) { + XAResource resource = (XAResource) it.next(); + XidImpl xid = (XidImpl) xaResources.get(resource); + resource.end(xid, XAResource.TMFAIL); + } + + status = Status.STATUS_ROLLING_BACK; + for (Iterator it = xaResources.keySet().iterator(); it.hasNext(); ) { + XAResource resource = (XAResource) it.next(); + XidImpl xid = (XidImpl) xaResources.get(resource); + resource.rollback(xid); + } + status = Status.STATUS_ROLLEDBACK; + + } catch (XAException e) { + SystemException se = new SystemException( + "Unable to rollback transaction: XA_ERR=" + e.errorCode); + se.initCause(e.getCause()); + throw se; + } + } + + /** + * @see javax.transaction.UserTransaction#setRollbackOnly() + */ + public void setRollbackOnly() throws IllegalStateException, SystemException { + if (status != Status.STATUS_ACTIVE) { + throw new IllegalStateException("Transaction not active"); + } + status = Status.STATUS_MARKED_ROLLBACK; + } + + /** + * @see javax.transaction.UserTransaction#setTransactionTimeout + */ + public void setTransactionTimeout(int seconds) throws SystemException { + try { + for (Iterator it = xaResources.keySet().iterator(); it.hasNext(); ) { + ((XAResource) it.next()).setTransactionTimeout(seconds); + } + } catch (XAException e) { + SystemException se = new SystemException( + "Unable to set the TransactionTiomeout: XA_ERR=" + e.errorCode); + se.initCause(e.getCause()); + throw se; + } + } + + + /** + * Internal {@link Xid} implementation. + */ + class XidImpl implements Xid { + + /** Global transaction id */ + private final byte[] globalTxId; + + /** + * Create a new instance of this class. Takes a global + * transaction number as parameter + * @param globalTxNumber global transaction number + */ + public XidImpl(byte globalTxNumber) { + this.globalTxId = new byte[] { globalTxNumber }; + } + + /** + * @see javax.transaction.xa.Xid#getFormatId() + */ + public int getFormatId() { + return 0; + } + + /** + * @see javax.transaction.xa.Xid#getBranchQualifier() + */ + public byte[] getBranchQualifier() { + return new byte[0]; + } + + /** + * @see javax.transaction.xa.Xid#getGlobalTransactionId() + */ + public byte[] getGlobalTransactionId() { + return globalTxId; + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/XATest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/XATest.java new file mode 100644 index 00000000000..996c91c38b2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/XATest.java @@ -0,0 +1,2139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Repository; +import javax.jcr.Node; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.InvalidItemStateException; +import javax.jcr.version.VersionException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.NodeType; +import javax.transaction.UserTransaction; +import javax.transaction.RollbackException; +import java.util.StringTokenizer; + +/** + * XATest contains the test cases for the methods + * inside {@link XASessionImpl}. + */ +public class XATest extends AbstractJCRTest { + + /** + * Other superuser. + */ + private Session otherSuperuser; + + /** + * {@inheritDoc} + */ + protected void setUp() throws Exception { + super.setUp(); + + otherSuperuser = getHelper().getSuperuserSession(); + + // clean testroot on second workspace + Session s2 = getHelper().getSuperuserSession(workspaceName); + try { + Node root = s2.getRootNode(); + if (root.hasNode(testPath)) { + // clean test root + Node testRootNode = root.getNode(testPath); + for (NodeIterator children = testRootNode.getNodes(); children.hasNext();) { + children.nextNode().remove(); + } + } else { + // create nodes to testPath + StringTokenizer names = new StringTokenizer(testPath, "/"); + Node currentNode = root; + while (names.hasMoreTokens()) { + String name = names.nextToken(); + if (currentNode.hasNode(name)) { + currentNode = currentNode.getNode(name); + } else { + currentNode = currentNode.addNode(name, testNodeType); + } + } + } + root.save(); + } finally { + s2.logout(); + } + } + + /** + * {@inheritDoc} + */ + protected void tearDown() throws Exception { + if (otherSuperuser != null) { + otherSuperuser.logout(); + otherSuperuser = null; + } + super.tearDown(); + } + + /** + * @see junit.framework.TestCase#runTest() + * + * Make sure that tested repository supports transactions + */ + protected void runTest() throws Throwable { + if (isSupported(Repository.OPTION_TRANSACTIONS_SUPPORTED)) { + super.runTest(); + } + } + + /** + * Test case for + * JCR-2796. + */ + public void testRestore() throws Exception { + Session session = getHelper().getSuperuserSession(); + try { + VersionManager vm = session.getWorkspace().getVersionManager(); + + // make sure that 'testNode' does not exist at the beginning + // of the test + while (session.nodeExists("/testNode")) { + session.getNode("/testNode").remove(); + session.save(); + } + + // 1) create 'testNode' that has a child and a grandchild + Node node = session.getRootNode().addNode("testNode"); + node.addMixin(NodeType.MIX_VERSIONABLE); + node.addNode("child").addNode("grandchild"); + session.save(); + + // 2) check in 'testNode' and give a version-label + Version version = vm.checkin(node.getPath()); + vm.getVersionHistory(node.getPath()).addVersionLabel( + version.getName(), "testLabel", false); + + // 3) do restore by label + UserTransaction utx = new UserTransactionImpl(session); + utx.begin(); + vm.restoreByLabel(node.getPath(), "testLabel", true); + utx.commit(); + + // 4) try to get the grandchild (fails if the restoring has + // been done within a transaction) + assertTrue(node.hasNode("child/grandchild")); + } finally { + session.logout(); + } + } + + /** + * Test case for + * JCR-2712. + */ + public void testVersioningRollbackWithoutPrepare() throws Exception { + Session session = getHelper().getSuperuserSession(); + try { + if (session.getRootNode().hasNode("testNode")) { + session.getRootNode().getNode("testNode").remove(); + session.save(); + } + + UserTransaction utx; + for (int i = 0; i < 50; i++) { + utx = new UserTransactionImpl(session); + utx.begin(); + session.getRootNode().addNode("testNode").addMixin( + NodeType.MIX_VERSIONABLE); + session.save(); + + utx.rollback(); + } + } finally { + session.logout(); + } + } + + /** + * Add a node inside a transaction and commit changes. Make sure + * node exists for other sessions only after commit. + * @throws Exception + */ + public void testAddNodeCommit() throws Exception { + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // add node and save + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // assertion: node exists in this session + try { + superuser.getNodeByUUID(n.getUUID()); + } catch (ItemNotFoundException e) { + fail("New node not visible after save()"); + } + + // assertion: node does not exist in other session + Session otherSuperuser = getHelper().getSuperuserSession(); + + try { + otherSuperuser.getNodeByUUID(n.getUUID()); + fail("Uncommitted node visible for other session"); + } catch (ItemNotFoundException e) { + /* expected */ + } + + // commit + utx.commit(); + + // assertion: node exists in this session + try { + superuser.getNodeByUUID(n.getUUID()); + } catch (ItemNotFoundException e) { + fail("Committed node not visible in this session"); + } + + // assertion: node also exists in other session + try { + otherSuperuser.getNodeByUUID(n.getUUID()); + } catch (ItemNotFoundException e) { + fail("Committed node not visible in other session"); + } + + // logout + otherSuperuser.logout(); + } + + /** + * Set a property inside a transaction and commit changes. Make sure + * property exists for other sessions only after commit. + * @throws Exception + */ + public void testSetPropertyCommit() throws Exception { + // prerequisite: non-existing property + if (testRootNode.hasProperty(propertyName1)) { + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + } + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // set property and save + testRootNode.setProperty(propertyName1, "0"); + testRootNode.save(); + + // assertion: property exists in this session + assertTrue(testRootNode.hasProperty(propertyName1)); + + // assertion: property does not exist in other session + Session otherSuperuser = getHelper().getSuperuserSession(); + Node otherRootNode = otherSuperuser.getRootNode().getNode(testPath); + assertFalse(otherRootNode.hasProperty(propertyName1)); + + // commit + utx.commit(); + + // assertion: property exists in this session + assertTrue(testRootNode.hasProperty(propertyName1)); + + // assertion: property also exists in other session + assertTrue(otherRootNode.hasProperty(propertyName1)); + + // logout + otherSuperuser.logout(); + } + + /** + * @throws Exception + */ + public void testAddAndSetProperty() throws Exception { + // prerequisite: non-existing property + if (testRootNode.hasProperty(propertyName1)) { + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + } + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // 'add' property and save + testRootNode.setProperty(propertyName1, "0"); + testRootNode.save(); + + // 'modify' property and save + testRootNode.setProperty(propertyName1, "1"); + testRootNode.save(); + + // commit + utx.commit(); + + // check property value + Session otherSuperuser = getHelper().getSuperuserSession(); + Node n = (Node) otherSuperuser.getItem(testRootNode.getPath()); + assertEquals(n.getProperty(propertyName1).getString(), "1"); + otherSuperuser.logout(); + } + + /** + * @throws Exception + */ + public void testPropertyIsNew() throws Exception { + // prerequisite: non-existing property + if (testRootNode.hasProperty(propertyName1)) { + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + } + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // 'add' property and save + testRootNode.setProperty(propertyName1, "0"); + + assertTrue("New property must be new.", testRootNode.getProperty(propertyName1).isNew()); + + testRootNode.save(); + + assertFalse("Saved property must not be new.", testRootNode.getProperty(propertyName1).isNew()); + + // commit + utx.commit(); + } + + /** + * @throws Exception + */ + public void testNewNodeIsLocked() throws Exception { + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // add node and save + Node n = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.save(); + + assertFalse("New node must not be locked.", n.isLocked()); + + // commit + utx.commit(); + } + + /** + * @throws Exception + */ + public void testPropertyIsModified() throws Exception { + // prerequisite: existing property + testRootNode.setProperty(propertyName1, "0"); + testRootNode.save(); + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // 'add' property and save + testRootNode.setProperty(propertyName1, "1"); + + assertTrue("Unsaved property must be modified.", testRootNode.getProperty(propertyName1).isModified()); + + testRootNode.save(); + + assertFalse("Saved property must not be modified.", testRootNode.getProperty(propertyName1).isModified()); + + // commit + utx.commit(); + } + + /** + * @throws Exception + */ + public void testDeleteAndAddProperty() throws Exception { + // prerequisite: existing property + testRootNode.setProperty(propertyName1, "0"); + testRootNode.save(); + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // 'delete' property and save + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + + // 'add' property and save + testRootNode.setProperty(propertyName1, "1"); + testRootNode.save(); + + // commit + utx.commit(); + + // check property value + Session otherSuperuser = getHelper().getSuperuserSession(); + Node n = (Node) otherSuperuser.getItem(testRootNode.getPath()); + assertEquals(n.getProperty(propertyName1).getString(), "1"); + otherSuperuser.logout(); + } + + /** + * @throws Exception + */ + public void testModifyAndDeleteProperty() throws Exception { + // prerequisite: existing property + testRootNode.setProperty(propertyName1, "0"); + testRootNode.save(); + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // 'modify' property and save + testRootNode.setProperty(propertyName1, "1"); + testRootNode.save(); + + // 'delete' property and save + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + + // commit + utx.commit(); + + // check property value + Session otherSuperuser = getHelper().getSuperuserSession(); + Node n = (Node) otherSuperuser.getItem(testRootNode.getPath()); + assertFalse("Property must be deleted.", n.hasProperty(propertyName1)); + otherSuperuser.logout(); + } + + /** + * @throws Exception + */ + public void testAddAndDeleteProperty() throws Exception { + // prerequisite: non-existing property + if (testRootNode.hasProperty(propertyName1)) { + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + } + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // 'add' property and save + testRootNode.setProperty(propertyName1, "1"); + testRootNode.save(); + + // 'delete' property and save + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + + // commit + utx.commit(); + + // check property value + Session otherSuperuser = getHelper().getSuperuserSession(); + Node n = (Node) otherSuperuser.getItem(testRootNode.getPath()); + assertFalse("Property must be deleted.", n.hasProperty(propertyName1)); + otherSuperuser.logout(); + } + + /** + * Add a node inside a transaction and rollback changes. + * @throws Exception + */ + public void testAddNodeRollback() throws Exception { + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // add node and save + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // assertion: node exists in this session + String uuid = n.getUUID(); + + try { + superuser.getNodeByUUID(uuid); + } catch (ItemNotFoundException e) { + fail("New node not visible after save()"); + } + + // rollback + utx.rollback(); + + // assertion: node does not exist in this session + try { + superuser.getNodeByUUID(uuid); + fail("Node still visible after rollback()"); + } catch (ItemNotFoundException e) { + /* expected */ + } + } + + /** + * Set a property inside a transaction and rollback changes. + * @throws Exception + */ + public void testSetPropertyRollback() throws Exception { + // prerequisite: non-existing property + if (testRootNode.hasProperty(propertyName1)) { + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + } + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // set property and save + testRootNode.setProperty(propertyName1, "0"); + testRootNode.save(); + + // assertion: property exists in this session + assertTrue(testRootNode.hasProperty(propertyName1)); + + // rollback + utx.rollback(); + + // assertion: property does not exist in this session + assertFalse(testRootNode.hasProperty(propertyName1)); + } + + /** + * Remove a node inside a transaction and rollback changes. Check + * that the node reference may again be used after having rolled + * back changes. + * @throws Exception + */ + public void testRemoveNodeRollback() throws Exception { + // prerequisite: existing node + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.addMixin(mixReferenceable); + testRootNode.save(); + + String uuid = n1.getUUID(); + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // remove node and save + Node n2 = superuser.getNodeByUUID(uuid); + n2.remove(); + testRootNode.save(); + + // assertion: node no longer exists + try { + superuser.getNodeByUUID(uuid); + fail("Removed node still exists after save()"); + } catch (ItemNotFoundException e) { + /* expected */ + } + + // rollback + utx.rollback(); + + // assertion: node exists again + try { + superuser.getNodeByUUID(uuid); + } catch (ItemNotFoundException e) { + fail("Removed node not visible after rollback()"); + } + } + + /** + * Remove a property inside a transaction and rollback changes. + * Check that the property reference may again be used after + * having rolled back changes. + * @throws Exception + */ + public void testRemovePropertyRollback() throws Exception { + // prerequisite: existing property + if (!testRootNode.hasProperty(propertyName1)) { + testRootNode.setProperty(propertyName1, "0"); + testRootNode.save(); + } + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // remove property and save + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + + // assertion: property no longer exists + assertFalse(testRootNode.hasProperty(propertyName1)); + + // rollback + utx.rollback(); + + // assertion: property exists and reference valid + assertTrue(testRootNode.hasProperty(propertyName1)); + } + + /** + * Add reference to some node in one session while removing + * the node in another. + * @throws Exception + */ + public void testAddReference() throws Exception { + // add two nodes, second one referenceable + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + n2.addMixin(mixReferenceable); + testRootNode.save(); + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // start transaction + utx.begin(); + + // add reference and save + n1.setProperty(propertyName1, n2); + testRootNode.save(); + + // remove referenced node in other session + Session otherSuperuser = getHelper().getSuperuserSession(); + Node otherRootNode = otherSuperuser.getRootNode().getNode(testPath); + otherSuperuser.getNodeByUUID(n2.getUUID()).remove(); + otherRootNode.save(); + + // assertion: commit must fail since integrity violated + try { + utx.commit(); + fail("Commit succeeds with violated integrity"); + } catch (RollbackException e) { + /* expected */ + } + + // logout + otherSuperuser.logout(); + } + + /** + * Checks if getReferences() reflects an added reference property that has + * been saved but not yet committed. + *

    + * Spec say: + *

    + * Some level 2 implementations may only return properties that have been + * saved (in a transactional setting this includes both those properties + * that have been saved but not yet committed, as well as properties that + * have been committed). Other level 2 implementations may additionally + * return properties that have been added within the current Session but are + * not yet saved. + *

    + * Jackrabbit does not support the latter, but at least has to support the + * first. + */ + public void testGetReferencesAddedRef() throws Exception { + // create one referenceable node + Node target = testRootNode.addNode(nodeName1); + target.addMixin(mixReferenceable); + // second node, which will later reference the target node + Node n = testRootNode.addNode(nodeName2); + testRootNode.save(); + + UserTransactionImpl tx = new UserTransactionImpl(superuser); + tx.begin(); + try { + // create reference + n.setProperty(propertyName1, target); + testRootNode.save(); + assertTrue("Node.getReferences() must reflect references that have " + + "been saved but not yet committed", target.getReferences().hasNext()); + } finally { + tx.rollback(); + } + } + + /** + * Checks if getReferences() reflects a removed reference property that has + * been saved but not yet committed. + */ + public void testGetReferencesRemovedRef() throws Exception { + // create one referenceable node + Node target = testRootNode.addNode(nodeName1); + target.addMixin(mixReferenceable); + // second node, which reference the target node + Node n = testRootNode.addNode(nodeName2); + // create reference + n.setProperty(propertyName1, target); + testRootNode.save(); + + UserTransactionImpl tx = new UserTransactionImpl(superuser); + tx.begin(); + try { + n.getProperty(propertyName1).remove(); + testRootNode.save(); + assertTrue("Node.getReferences() must reflect references that have " + + "been saved but not yet committed", !target.getReferences().hasNext()); + } finally { + tx.rollback(); + } + } + + /** + * Checks if getReferences() reflects a modified reference property that has + * been saved but not yet committed. + */ + public void testGetReferencesModifiedRef() throws Exception { + // create two referenceable node + Node target1 = testRootNode.addNode(nodeName1); + target1.addMixin(mixReferenceable); + // second node, which reference the target1 node + Node target2 = testRootNode.addNode(nodeName2); + target2.addMixin(mixReferenceable); + Node n = testRootNode.addNode(nodeName3); + // create reference + n.setProperty(propertyName1, target1); + testRootNode.save(); + + UserTransactionImpl tx = new UserTransactionImpl(superuser); + tx.begin(); + try { + // change reference + n.setProperty(propertyName1, target2); + testRootNode.save(); + assertTrue("Node.getReferences() must reflect references that have " + + "been saved but not yet committed", !target1.getReferences().hasNext()); + assertTrue("Node.getReferences() must reflect references that have " + + "been saved but not yet committed", target2.getReferences().hasNext()); + } finally { + tx.rollback(); + } + } + + /** + * Checks if getReferences() reflects a modified reference property that has + * been saved but not yet committed. The old value is a reference, while + * the new value is not. + */ + public void testGetReferencesModifiedRefOldValueReferenceable() throws Exception { + // create one referenceable node + Node target = testRootNode.addNode(nodeName1); + target.addMixin(mixReferenceable); + Node n = testRootNode.addNode(nodeName2); + // create reference + n.setProperty(propertyName1, target); + testRootNode.save(); + + UserTransactionImpl tx = new UserTransactionImpl(superuser); + tx.begin(); + try { + // change reference to a string value + n.setProperty(propertyName1, "foo"); + testRootNode.save(); + assertTrue("Node.getReferences() must reflect references that have " + + "been saved but not yet committed", !target.getReferences().hasNext()); + } finally { + tx.rollback(); + } + } + + /** + * Checks if getReferences() reflects a modified reference property that has + * been saved but not yet committed. The new value is a reference, while + * the old value wasn't. + */ + public void testGetReferencesModifiedRefNewValueReferenceable() throws Exception { + // create one referenceable node + Node target = testRootNode.addNode(nodeName1); + target.addMixin(mixReferenceable); + Node n = testRootNode.addNode(nodeName2); + // create string property + n.setProperty(propertyName1, "foo"); + testRootNode.save(); + + UserTransactionImpl tx = new UserTransactionImpl(superuser); + tx.begin(); + try { + // change string into a reference + n.setProperty(propertyName1, target); + testRootNode.save(); + assertTrue("Node.getReferences() must reflect references that have " + + "been saved but not yet committed", target.getReferences().hasNext()); + } finally { + tx.rollback(); + } + } + + //--------------------------------------------------------------< locking > + + /** + * Test locking a node in one session. Verify that node is not locked + * in other session until commit. + * @throws Exception + */ + public void testLockCommit() throws Exception { + Session other = getHelper().getSuperuserSession(); + try { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // reference node in second session + Node nOther = other.getNodeByUUID(n.getUUID()); + + // verify node is not locked in either session + assertFalse("Node not locked in session 1", n.isLocked()); + assertFalse("Node not locked in session 2", nOther.isLocked()); + + // get user transaction object, start and lock node + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + n.lock(false, true); + + // verify node is locked in first session only + assertTrue("Node locked in session 1", n.isLocked()); + assertFalse("Node not locked in session 2", nOther.isLocked()); + + // commit in first session + utx.commit(); + + // verify node is locked in both sessions + assertTrue("Node locked in session 1", n.isLocked()); + assertTrue("Node locked in session 2", nOther.isLocked()); + } finally { + // logout + other.logout(); + } + } + + /** + * Test locking and unlocking behavior in transaction + * @throws Exception + */ + public void testLockUnlockCommit() throws Exception { + Session other = getHelper().getSuperuserSession(); + try { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // reference node in second session + Node nOther = other.getNodeByUUID(n.getUUID()); + + // verify node is not locked in either session + assertFalse("Node not locked in session 1", n.isLocked()); + assertFalse("Node not locked in session 2", nOther.isLocked()); + + // get user transaction object, start and lock node + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + n.lock(false, true); + + // verify node is locked in first session only + assertTrue("Node locked in session 1", n.isLocked()); + assertFalse("Node not locked in session 2", nOther.isLocked()); + + n.unlock(); + // commit in first session + utx.commit(); + + // verify node is locked in both sessions + assertFalse("Node locked in session 1", n.isLocked()); + assertFalse("Node locked in session 2", nOther.isLocked()); + } finally { + // logout + other.logout(); + } + } + + /** + * Test locking and unlocking behavior in transaction + * (see JCR-2356) + * @throws Exception + */ + public void testCreateLockUnlockInDifferentTransactions() throws Exception { + // create new node and lock it + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + + // add node that is both lockable and referenceable, save + Node rootNode = superuser.getRootNode(); + Node n = rootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + rootNode.save(); + + String uuid = n.getUUID(); + + // commit + utx.commit(); + + // start new Transaction and try to add lock token + utx = new UserTransactionImpl(superuser); + utx.begin(); + + n = superuser.getNodeByUUID(uuid); + // lock this new node + Lock lock = n.lock(true, false); + + // verify node is locked + assertTrue("Node not locked", n.isLocked()); + + String lockToken = lock.getLockToken(); + // assert: session must get a non-null lock token + assertNotNull("session must get a non-null lock token", lockToken); + // assert: session must hold lock token + assertTrue("session must hold lock token", containsLockToken(superuser, lockToken)); + + n.save(); + + superuser.removeLockToken(lockToken); + + String nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + + assertFalse("session must not hold lock token", containsLockToken(superuser, lockToken)); + + // commit + utx.commit(); + + nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + + assertFalse("session must not hold lock token", containsLockToken(superuser, lockToken)); + + // start new Transaction and try to unlock + utx = new UserTransactionImpl(superuser); + utx.begin(); + + n = superuser.getNodeByUUID(uuid); + + // verify node is locked + assertTrue("Node not locked", n.isLocked()); + // assert: session must not hold lock token + assertFalse("session must not hold lock token", containsLockToken(superuser, lockToken)); + + superuser.addLockToken(lockToken); + + // assert: session must not hold lock token + assertTrue("session must hold lock token", containsLockToken(superuser, lockToken)); + + n.unlock(); + + // commit + utx.commit(); + } + + /** + * Test locking a node in one session. Verify that node is not locked + * in session after rollback. + * @throws Exception + */ + public void testLockRollback() throws Exception { + Session other = getHelper().getSuperuserSession(); + try { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // reference node in second session + Node nOther = other.getNodeByUUID(n.getUUID()); + + // verify node is not locked in either session + assertFalse("Node not locked in session 1", n.isLocked()); + assertFalse("Node not locked in session 2", nOther.isLocked()); + + // get user transaction object, start and lock node + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + n.lock(false, true); + + // verify node is locked in first session only + assertTrue("Node locked in session 1", n.isLocked()); + assertFalse("Node not locked in session 2", nOther.isLocked()); + assertFalse("Node not locked in session 2", nOther.hasProperty(jcrLockOwner)); + + // rollback in first session + utx.rollback(); + + // verify node is not locked in either session + assertFalse("Node not locked in session 1", n.isLocked()); + assertFalse("Node not locked in session 2", nOther.isLocked()); + assertFalse("Node not locked in session 2", nOther.hasProperty(jcrlockIsDeep)); + } finally { + // logout + other.logout(); + } + } + + /** + * Test locking a node inside a transaction that has been locked in another + * session, which leads to a failure when committing. + * @throws Exception + */ + public void testLockTwice() throws Exception { + Session other = getHelper().getSuperuserSession(); + try { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // reference node in second session + Node nOther = other.getNodeByUUID(n.getUUID()); + + // verify node is not locked in either session + assertFalse("Node not locked in session 1", n.isLocked()); + assertFalse("Node not locked in session 2", nOther.isLocked()); + + // get user transaction object, start and lock node + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + n.lock(false, true); + + // lock node in non-transactional session, too + nOther.lock(false, true); + + // verify node is locked in both sessions + assertTrue("Node locked in session 1", n.isLocked()); + assertTrue("Node locked in session 2", nOther.isLocked()); + assertTrue("Node locked in session 2", nOther.hasProperty(jcrLockOwner)); + + // assertion: commit must fail since node has already been locked + try { + utx.commit(); + fail("Commit succeeds with double locking"); + } catch (RollbackException e) { + /* expected */ + } + + // verify node is locked in both sessions + assertTrue("Node locked in session 1", n.isLocked()); + assertTrue("Node locked in session 2", nOther.isLocked()); + assertTrue("Node locked in session 2", nOther.hasProperty(jcrlockIsDeep)); + + } finally { + // logout + other.logout(); + } + } + + /** + * Test locking a new node inside a transaction. + * @throws Exception + */ + public void testLockNewNode() throws Exception { + // get user transaction object, start + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // lock this new node + n.lock(false, true); + assertTrue("Node locked in transaction", n.isLocked()); + + // commit + utx.commit(); + + // Check if it is locked in other session + Session other = getHelper().getSuperuserSession(); + Node nOther = other.getNodeByUUID(n.getUUID()); + assertTrue(nOther.isLocked()); + + // Check if it is also locked in other transaction + Session other2 = getHelper().getSuperuserSession(); + // start new Transaction and try to add locktoken + utx = new UserTransactionImpl(other2); + utx.begin(); + + Node nOther2 = other2.getNodeByUUID(n.getUUID()); + assertTrue(nOther2.isLocked()); + + utx.commit(); + + other.logout(); + other2.logout(); + + } + + /** + * Test add and remove lock tokens in a transaction + * @throws Exception + */ + public void testAddRemoveLockToken() throws Exception { + // create new node and lock it + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + + // add node that is both lockable and referenceable, save + Node rootNode = superuser.getRootNode(); + Node n = rootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + rootNode.save(); + + String uuid = n.getUUID(); + + // lock this new node + Lock lock = n.lock(true, false); + String lockToken = lock.getLockToken(); + + // assert: session must get a non-null lock token + assertNotNull("session must get a non-null lock token", lockToken); + + // assert: session must hold lock token + assertTrue("session must hold lock token", containsLockToken(superuser, lockToken)); + + superuser.removeLockToken(lockToken); + + String nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + + // commit + utx.commit(); + + // refresh Lock Info + lock = n.getLock(); + + nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + + Session other = getHelper().getSuperuserSession(); + try { + // start new Transaction and try to add lock token + utx = new UserTransactionImpl(other); + utx.begin(); + + Node otherNode = other.getNodeByUUID(uuid); + assertTrue("Node not locked", otherNode.isLocked()); + try { + otherNode.setProperty(propertyName1, "foo"); + fail("Lock exception should be thrown"); + } catch (LockException e) { + // expected + } + + // add lock token + other.addLockToken(lockToken); + + // refresh Lock Info + lock = otherNode.getLock(); + + // assert: session must hold lock token + assertTrue("session must hold lock token", containsLockToken(other, lock.getLockToken())); + + otherNode.unlock(); + + assertFalse("Node is locked", otherNode.isLocked()); + + otherNode.setProperty(propertyName1, "foo"); + other.save(); + utx.commit(); + } finally { + other.logout(); + } + } + + /** + * Test locking/unlocking a node inside a transaction which should be a + * no-op. + * @throws Exception + */ + public void testLockUnlock() throws Exception { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // verify node is not locked in this session + assertFalse("Node not locked", n.isLocked()); + + // get user transaction object, start and lock node + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + n.lock(false, true); + + // verify node is locked + assertTrue("Node locked", n.isLocked()); + + // unlock node + n.unlock(); + + // commit + utx.commit(); + + // verify node is not locked + assertFalse("Node not locked", n.isLocked()); + } + + /** + * Test correct behaviour of {@link javax.jcr.lock.Lock} inside a + * transaction. + * @throws Exception + */ + public void testLockBehaviour() throws Exception { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // get user transaction object, start and lock node + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + Lock lock = n.lock(false, true); + + // verify lock is live + assertTrue("Lock live", lock.isLive()); + + // rollback + utx.rollback(); + + // verify lock is not live anymore + assertFalse("Lock not live", lock.isLive()); + } + + /** + * Test correct behaviour of {@link javax.jcr.lock.Lock} inside a + * transaction. + * @throws Exception + */ + public void testLockBehaviour2() throws Exception { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + Lock lock = n.lock(false, true); + + // get user transaction object, start + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + + // verify lock is live + assertTrue("Lock live", lock.isLive()); + + // unlock + n.unlock(); + + // verify lock is no longer live + assertFalse("Lock not live", lock.isLive()); + + // rollback + utx.rollback(); + + // verify lock is live again + assertTrue("Lock live", lock.isLive()); + } + + /** + * Test correct behaviour of lock related properties within transaction. + * + * @throws Exception + */ + public void testLockProperties() throws Exception { + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // get user transaction object, start and lock node + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + Lock lock = n.lock(false, true); + + // verify that the lock properties have been created and are neither + // NEW nor MODIFIED. + assertTrue(n.hasProperty(jcrLockOwner)); + Property lockOwner = n.getProperty(jcrLockOwner); + assertFalse(lockOwner.isNew()); + assertFalse(lockOwner.isModified()); + + assertTrue(n.hasProperty(jcrlockIsDeep)); + Property lockIsDeep = n.getProperty(jcrlockIsDeep); + assertFalse(lockIsDeep.isNew()); + assertFalse(lockIsDeep.isModified()); + + // rollback + utx.rollback(); + + // verify that the lock properties have been removed again. + assertFalse(n.hasProperty(jcrLockOwner)); + try { + lockOwner.getPath(); + fail("jcr:lockIsDeep property must have been invalidated."); + } catch (InvalidItemStateException e) { + // success + } + assertFalse(n.hasProperty(jcrlockIsDeep)); + try { + lockIsDeep.getPath(); + fail("jcr:lockIsDeep property must have been invalidated."); + } catch (InvalidItemStateException e) { + // success + } + } + + /** + * Test correct behaviour of lock related properties within transaction. + * + * @throws Exception + */ + public void testLockProperties2() throws Exception { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + Lock lock = n.lock(false, true); + try { + // get user transaction object, start + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + + // verify that the lock properties are present + assertTrue(n.hasProperty(jcrLockOwner)); + assertTrue(n.hasProperty(jcrlockIsDeep)); + + // unlock + n.unlock(); + + // verify that the lock properties have been removed. + assertFalse(n.hasProperty(jcrLockOwner)); + assertFalse(n.hasProperty(jcrlockIsDeep)); + + // rollback + utx.rollback(); + + // verify lock is live again -> properties must be present + assertTrue(n.hasProperty(jcrLockOwner)); + assertTrue(n.hasProperty(jcrlockIsDeep)); + } finally { + n.unlock(); + } + } + + /** + * Test visibility of lock properties by another session. + * + * @throws Exception + */ + public void testLockProperties3() throws Exception { + // add node that is both lockable and referenceable, save + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + testRootNode.save(); + + Lock lock = n.lock(false, true); + + // get user transaction object, start + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + + // unlock + n.unlock(); + + Node n2 = (Node) otherSuperuser.getItem(n.getPath()); + assertTrue(n2.isLocked()); + assertTrue(n2.hasProperty(jcrLockOwner)); + assertTrue(n2.hasProperty(jcrlockIsDeep)); + Lock lock2 = n2.getLock(); + + // complete transaction + utx.commit(); + + // unlock must now be visible to other session + n2.refresh(false); + assertFalse(lock2.isLive()); + assertFalse(n2.isLocked()); + assertFalse(n2.hasProperty(jcrLockOwner)); + assertFalse(n2.hasProperty(jcrlockIsDeep)); + } + + //-----------------------------------------------------------< versioning > + + /** + * Checkin inside tx should not be visible to other users. + */ + public void testCheckin() throws Exception { + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // add node and save + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixVersionable); + testRootNode.save(); + + // reference node in other session + Node nOther = otherSuperuser.getNodeByUUID(n.getUUID()); + + // start transaction + utx.begin(); + + // checkin node + n.checkin(); + + // assert: base versions must differ + if (n.getBaseVersion().getName().equals(nOther.getBaseVersion().getName())) { + fail("Base versions must differ"); + } + + // assert: version must not be visible to other session + try { + nOther.getVersionHistory().getVersion(n.getBaseVersion().getName()); + fail("Version must not be visible to other session."); + } catch (VersionException e) { + // expected. + } + + // commit + utx.commit(); + + // assert: base versions must be equal + assertEquals("Base versions must be equal", + n.getBaseVersion().getName(), nOther.getBaseVersion().getName()); + } + + /** + * Checkin from two sessions simultaneously should throw when committing. + * @throws Exception + */ + public void testConflictingCheckin() throws Exception { + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // add node and save + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixVersionable); + testRootNode.save(); + + // reference node in other session + Node nOther = otherSuperuser.getNodeByUUID(n.getUUID()); + + // start transaction + utx.begin(); + + // checkin node inside tx + n.checkin(); + + // checkin node outside tx + nOther.checkin(); + + // commit + try { + utx.commit(); + fail("Commit failing with modified version history."); + } catch (RollbackException e) { + // expected + } + } + + /** + * Test removed version gets invalid for other users on commit. + */ + public void testRemoveVersion() throws Exception { + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // add node and save + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixVersionable); + testRootNode.save(); + + // reference node in other session + Node nOther = otherSuperuser.getNodeByUUID(n.getUUID()); + + // create two versions, reference first version in other session + n.checkin(); + Version vOther = nOther.getBaseVersion(); + n.checkout(); + n.checkin(); + + // start transaction + utx.begin(); + + // remove version and commit + n.getVersionHistory().removeVersion(vOther.getName()); + + // commit + utx.commit(); + + // assert: version has become invalid + try { + vOther.getPredecessors(); + fail("Removed version still operational."); + } catch (RepositoryException e) { + // expected + } + } + + /** + * Tests a couple of checkin/restore/remove operations on different + * workspaces and different transactions. + * + * @throws Exception + */ + public void testXAVersionsThoroughly() throws Exception { + Session s1 = superuser; + Session s2 = getHelper().getSuperuserSession(workspaceName); + + // add node and save + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.addMixin(mixVersionable); + testRootNode.save(); + + if (!s2.itemExists(testRootNode.getPath())) { + s2.getRootNode().addNode(testRootNode.getName()); + s2.save(); + } + s2.getWorkspace().clone(s1.getWorkspace().getName(), n1.getPath(), n1.getPath(), true); + Node n2 = (Node) s2.getItem(n1.getPath()); + + //log.println("---------------------------------------"); + String phase="init"; + + Version v1_1 = n1.getBaseVersion(); + Version v2_1 = n2.getBaseVersion(); + + check(v1_1, phase, "jcr:rootVersion", 0); + check(v2_1, phase, "jcr:rootVersion", 0); + + //log.println("--------checkout/checkin n1 (uncommitted)----------"); + phase="checkin N1 uncomitted."; + + UserTransaction tx = new UserTransactionImpl(s1); + tx.begin(); + + n1.checkout(); + n1.checkin(); + + Version v1_2 = n1.getBaseVersion(); + + check(v1_1, phase, "jcr:rootVersion", 1); + check(v2_1, phase, "jcr:rootVersion", 0); + check(v1_2, phase, "1.0", 0); + + //log.println("--------checkout/checkin n1 (comitted)----------"); + phase="checkin N1 committed."; + + tx.commit(); + + check(v1_1, phase, "jcr:rootVersion", 1); + check(v2_1, phase, "jcr:rootVersion", 1); + check(v1_2, phase, "1.0", 0); + + //log.println("--------restore n2 (uncommitted) ----------"); + phase="restore N2 uncommitted."; + + tx = new UserTransactionImpl(s2); + tx.begin(); + + n2.restore("1.0", false); + Version v2_2 = n2.getBaseVersion(); + + check(v1_1, phase, "jcr:rootVersion", 1); + check(v2_1, phase, "jcr:rootVersion", 1); + check(v1_2, phase, "1.0", 0); + check(v2_2, phase, "1.0", 0); + + //log.println("--------restore n2 (comitted) ----------"); + phase="restore N2 committed."; + + tx.commit(); + + check(v1_1, phase, "jcr:rootVersion", 1); + check(v2_1, phase, "jcr:rootVersion", 1); + check(v1_2, phase, "1.0", 0); + check(v2_2, phase, "1.0", 0); + + //log.println("--------checkout/checkin n2 (uncommitted) ----------"); + phase="checkin N2 uncommitted."; + + tx = new UserTransactionImpl(s2); + tx.begin(); + + n2.checkout(); + n2.checkin(); + + Version v2_3 = n2.getBaseVersion(); + + check(v1_1, phase, "jcr:rootVersion", 1); + check(v2_1, phase, "jcr:rootVersion", 1); + check(v1_2, phase, "1.0", 0); + check(v2_2, phase, "1.0", 1); + check(v2_3, phase, "1.1", 0); + + //log.println("--------checkout/checkin n2 (committed) ----------"); + phase="checkin N2 committed."; + + tx.commit(); + + check(v1_1, phase, "jcr:rootVersion", 1); + check(v2_1, phase, "jcr:rootVersion", 1); + check(v1_2, phase, "1.0", 1); + check(v2_2, phase, "1.0", 1); + check(v2_3, phase, "1.1", 0); + + //log.println("--------checkout/checkin n1 (uncommitted) ----------"); + phase="checkin N1 uncommitted."; + + tx = new UserTransactionImpl(s1); + tx.begin(); + + n1.checkout(); + n1.checkin(); + + Version v1_3 = n1.getBaseVersion(); + + check(v1_1, phase, "jcr:rootVersion", 1); + check(v2_1, phase, "jcr:rootVersion", 1); + check(v1_2, phase, "1.0", 2); + check(v2_2, phase, "1.0", 1); + check(v2_3, phase, "1.1", 0); + check(v1_3, phase, "1.0.0", 0); + + //log.println("--------checkout/checkin n1 (committed) ----------"); + phase="checkin N1 committed."; + + tx.commit(); + + check(v1_1, phase, "jcr:rootVersion", 1); + check(v2_1, phase, "jcr:rootVersion", 1); + check(v1_2, phase, "1.0", 2); + check(v2_2, phase, "1.0", 2); + check(v2_3, phase, "1.1", 0); + check(v1_3, phase, "1.0.0", 0); + + //log.println("--------remove n1-1.0 (uncommitted) ----------"); + phase="remove N1 1.0 uncommitted."; + + tx = new UserTransactionImpl(s1); + tx.begin(); + + n1.getVersionHistory().removeVersion("1.0"); + + check(v1_1, phase, "jcr:rootVersion", 2); + check(v2_1, phase, "jcr:rootVersion", 1); + check(v1_2, phase, "1.0", -1); + check(v2_2, phase, "1.0", 2); + check(v2_3, phase, "1.1", 0); + check(v1_3, phase, "1.0.0", 0); + + //log.println("--------remove n1-1.0 (committed) ----------"); + phase="remove N1 1.0 committed."; + + tx.commit(); + + check(v1_1, phase, "jcr:rootVersion", 2); + check(v2_1, phase, "jcr:rootVersion", 2); + check(v1_2, phase, "1.0", -1); + check(v2_2, phase, "1.0", -1); + check(v2_3, phase, "1.1", 0); + check(v1_3, phase, "1.0.0", 0); + + //s1.logout(); + s2.logout(); + + } + + /** + * helper method for {@link #testXAVersionsThoroughly()} + */ + private void check(Version v, String phase, String name, int numSucc) { + String vName; + int vSucc = -1; + try { + vName = v.getName(); + //vSucc = v.getProperty("jcr:successors").getValues().length; + vSucc = v.getSuccessors().length; + } catch (RepositoryException e) { + // node is invalid after remove + vName = name; + } + assertEquals(phase + " Version Name", name, vName); + assertEquals(phase + " Num Successors", numSucc, vSucc); + } + + + + /** + * Test new version label becomes available to other sessions on commit. + */ + public void testSetVersionLabel() throws Exception { + final String versionLabel = "myVersion"; + + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser); + + // add node and save + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixVersionable); + testRootNode.save(); + + // reference node in other session + Node nOther = otherSuperuser.getNodeByUUID(n.getUUID()); + + // create another version + Version v = n.checkin(); + + // start transaction + utx.begin(); + + // add new version label + n.getVersionHistory().addVersionLabel(v.getName(), versionLabel, false); + + // assert: version label unknown in other session + try { + nOther.getVersionHistory().getVersionByLabel(versionLabel); + fail("Version label visible outside tx."); + } catch (VersionException e) { + // expected + } + + // commit + utx.commit(); + + // assert: version label known in other session + nOther.getVersionHistory().getVersionByLabel(versionLabel); + } + + /** + * Tests two different Threads for prepare and commit in a Transaction + */ + public void testDistributedThreadAccess() throws Exception { + // get user transaction object + UserTransaction utx = new UserTransactionImpl(superuser, true); + //utx.setTransactionTimeout(50); + // start transaction + utx.begin(); + + // add node and save + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // assertion: node exists in this session + try { + superuser.getNodeByUUID(n.getUUID()); + } catch (ItemNotFoundException e) { + fail("New node not visible after save()"); + } + + // commit + utx.commit(); + + // assertion: node exists in this session + try { + superuser.getNodeByUUID(n.getUUID()); + } catch (ItemNotFoundException e) { + fail("Committed node not visible in this session"); + } + } + + /** + * Tests two different Sessions in one Transaction + * (see JCR-769) + */ + public void testTwoSessionsInOneTransaction() throws Exception { + Session otherSuperuser = getHelper().getSuperuserSession(); + + // get user transaction object + UserTransactionImpl utx = new UserTransactionImpl(superuser, true); + utx.enlistXAResource(otherSuperuser); + + // start transaction + utx.begin(); + + Node rootNode = superuser.getRootNode(); + // add node and save + Node n = rootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixReferenceable); + rootNode.save(); + + // assertion: node exists in this session + try { + superuser.getNodeByUUID(n.getUUID()); + } catch (ItemNotFoundException e) { + fail("New node not visible after save()"); + } + + // assertion: node does exist in other session + try { + otherSuperuser.getNodeByUUID(n.getUUID()); + fail("Uncommitted node visible for other session"); + } catch (ItemNotFoundException e) { + /* expected */ + } + + // add node with other session and save + rootNode = otherSuperuser.getRootNode(); + Node n1 = rootNode.addNode(nodeName2, testNodeType); + n1.addMixin(mixReferenceable); + rootNode.save(); + + // assertion: node exists in this session + try { + otherSuperuser.getNodeByUUID(n1.getUUID()); + } catch (ItemNotFoundException e) { + fail("New node not visible after save()"); + } + + // assertion: node does exist in other session + try { + superuser.getNodeByUUID(n1.getUUID()); + fail("Uncommitted node visible for other session"); + } catch (ItemNotFoundException e) { + /* expected */ + } + + + // commit + utx.commit(); + + // assertion: node exists in this session + try { + superuser.getNodeByUUID(n.getUUID()); + } catch (ItemNotFoundException e) { + fail("Committed node not visible in this session"); + } + + // assertion: node also exists in other session + try { + otherSuperuser.getNodeByUUID(n.getUUID()); + } catch (ItemNotFoundException e) { + fail("Committed node not visible in the other session"); + } + + // assertion: node1 exists in this session + try { + superuser.getNodeByUUID(n1.getUUID()); + } catch (ItemNotFoundException e) { + fail("Committed node not visible in this session"); + } + + // assertion: node1 also exists in other session + try { + otherSuperuser.getNodeByUUID(n1.getUUID()); + } catch (ItemNotFoundException e) { + fail("Committed node not visible in this session"); + } + + // logout + superuser.logout(); + otherSuperuser.logout(); + } + + /** + * Test add lock token and remove node in in a transaction + * (see JCR-2332) + * @throws Exception + */ + public void testAddLockTokenRemoveNode() throws Exception { + // create new node and lock it + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + + // add node that is both lockable and referenceable, save + Node rootNode = superuser.getRootNode(); + Node n = rootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + rootNode.save(); + + String uuid = n.getUUID(); + + // lock this new node + Lock lock = n.lock(true, false); + String lockToken = lock.getLockToken(); + + // assert: session must get a non-null lock token + assertNotNull("session must get a non-null lock token", lockToken); + + // assert: session must hold lock token + assertTrue("session must hold lock token", containsLockToken(superuser, lockToken)); + + superuser.removeLockToken(lockToken); + + String nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + + // commit + utx.commit(); + + // refresh Lock Info + lock = n.getLock(); + + nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + + Session other = getHelper().getSuperuserSession(); + // start new Transaction and try to add lock token unlock the node and then remove it + utx = new UserTransactionImpl(other); + utx.begin(); + + Node otherNode = other.getNodeByUUID(uuid); + assertTrue("Node not locked", otherNode.isLocked()); + // add lock token + other.addLockToken(lockToken); + + // refresh Lock Info + lock = otherNode.getLock(); + + // assert: session must hold lock token + assertTrue("session must hold lock token", containsLockToken(other, lock.getLockToken())); + + otherNode.unlock(); + + assertFalse("Node is locked", otherNode.isLocked()); + + otherNode.remove(); + other.save(); + utx.commit(); + } + + /** + * Tests if it is possible to add-lock a node and unlock-remove it with + * a shared session in different transactions + * (see JCR-2341) + * @throws Exception + */ + public void testAddLockTokenRemoveNode2() throws Exception { + // create new node and lock it + UserTransaction utx = new UserTransactionImpl(superuser); + utx.begin(); + + // add node that is both lockable and referenceable, save + Node rootNode = superuser.getRootNode(); + Node n = rootNode.addNode(nodeName1); + n.addMixin(mixLockable); + n.addMixin(mixReferenceable); + rootNode.save(); + + String uuid = n.getUUID(); + + // lock this new node + Lock lock = n.lock(true, false); + String lockToken = lock.getLockToken(); + + // commit + utx.commit(); + + + // refresh Lock Info + lock = n.getLock(); + + // start new Transaction and try to add lock token unlock the node and then remove it + utx = new UserTransactionImpl(superuser); + utx.begin(); + + Node otherNode = superuser.getNodeByUUID(uuid); + assertTrue("Node not locked", otherNode.isLocked()); + // add lock token + superuser.addLockToken(lockToken); + + // refresh Lock Info + lock = otherNode.getLock(); + + // assert: session must hold lock token + assertTrue("session must hold lock token", containsLockToken(superuser, lockToken)); + + otherNode.unlock(); + + assertFalse("Node is locked", otherNode.isLocked()); + + otherNode.remove(); + superuser.save(); + utx.commit(); + } + + /** + * Test setting the same property multiple times. Exposes an issue where + * the same property instance got reused in subsequent transactions + * (see JCR-1554). + * + * @throws Exception if an error occurs + */ + public void testSetProperty() throws Exception { + final String testNodePath = testPath + "/" + Math.random(); + + Session session = getHelper().getSuperuserSession(); + try { + // Add node + doTransactional(new Operation() { + public void invoke(Session session) throws Exception { + session.getRootNode().addNode(testNodePath); + session.save(); + } + }, session); + + for (int i = 1; i <= 3; i++) { + // Set property "name" to value "value" + doTransactional(new Operation() { + public void invoke(Session session) throws Exception { + Node n = (Node) session.getItem("/" + testNodePath); + n.setProperty("name", "value"); + session.save(); + } + }, session); + } + } finally { + session.logout(); + } + } + + /** + * Test deleting a subnode after creation. Exposes an issue where + * the same node instance got reused in subsequent transactions + * (see JCR-1554). + * + * @throws Exception if an error occurs + */ + public void testDeleteNode() throws Exception { + final String testNodePath = testPath + "/" + Math.random(); + + Session session = getHelper().getSuperuserSession(); + try { + for (int i = 1; i <= 3; i++) { + // Add parent node + doTransactional(new Operation() { + public void invoke(Session session) throws Exception { + session.getRootNode().addNode(testNodePath); + session.save(); + } + }, session); + + // Add child node + doTransactional(new Operation() { + public void invoke(Session session) throws Exception { + session.getRootNode().addNode(testNodePath + "/subnode"); + session.save(); + } + }, session); + + // Remove parent node + doTransactional(new Operation() { + public void invoke(Session session) throws Exception { + session.getRootNode().getNode(testNodePath).remove(); + session.save(); + } + }, session); + } + } finally { + session.logout(); + } + } + + /** + * Operation to invoke on a session scope. + */ + interface Operation { + + /** + * Invoke the operation. + * @param session session to use inside operation + * @throws Exception if an error occurs + */ + void invoke(Session session) throws Exception; + } + + /** + * Wrap a session-scoped operation with a transaction. + * + * @param op operation to invoke + * @param session session to use for the transaction + * @throws Exception if an error occurs + */ + private void doTransactional(Operation op, Session session) throws Exception { + UserTransaction utx = new UserTransactionImpl(session); + utx.begin(); + + op.invoke(session); + + utx.commit(); + } + + /** + * Return a flag indicating whether the indicated session contains + * a specific lock token + */ + private boolean containsLockToken(Session session, String lockToken) { + String[] lt = session.getLockTokens(); + for (int i = 0; i < lt.length; i++) { + if (lt[i].equals(lockToken)) { + return true; + } + } + return false; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cache/ConcurrentCacheTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cache/ConcurrentCacheTest.java new file mode 100644 index 00000000000..c405fee304e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cache/ConcurrentCacheTest.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.core.cache; + +import org.apache.jackrabbit.core.id.NodeId; + +import junit.framework.TestCase; + +/** + * Test cases for the {@link ConcurrentCache} class. + */ +public class ConcurrentCacheTest extends TestCase { + + /** + * Tests a concurrent cache by adding lots of random items to it + * and checking that the excess items have automatically been evicted + * while frequently accessed items are still present. + */ + public void testConcurrentCache() { + NodeId[] ids = new NodeId[1000]; + for (int i = 0; i < ids.length; i++) { + ids[i] = NodeId.randomId(); + } + + ConcurrentCache cache = + new ConcurrentCache("test"); + cache.setMaxMemorySize(ids.length / 2); + + for (int i = 0; i < ids.length; i++) { + for (int j = 0; j < i; j += 3) { + cache.get(ids[j]); + } + cache.put(ids[i], ids[i], 1); + } + + assertTrue(cache.getMemoryUsed() <= ids.length / 2); + + int n = 0; + for (int i = 0; i < ids.length; i++) { + if (cache.containsKey(ids[i])) { + n++; + } + } + + assertTrue(n <= ids.length / 2); + + n = 0; + for (int i = 0; i < ids.length; i += 3) { + if (cache.containsKey(ids[i])) { + n++; + } + } + + assertTrue(cache.getMemoryUsed() > ids.length / 4); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cache/GrowingLRUMapTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cache/GrowingLRUMapTest.java new file mode 100644 index 00000000000..9298c5e25fc --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cache/GrowingLRUMapTest.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.core.cache; + +import junit.framework.TestCase; + +/** + * GrowingLRUMapTest... + */ +public class GrowingLRUMapTest extends TestCase { + + public void testMaxSize() { + int initialSize = 2; + int maxSize = 10; + GrowingLRUMap m = new GrowingLRUMap(initialSize, maxSize); + + for (int i = 0; i < 50; i++) { + m.put("key" + i, "value" + i); + + if (i <= maxSize) { + assertSame("i = " + i, i+1, m.size()); + } else { + assertNotSame("i = " + i, i+1, m.size()); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterDescriptorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterDescriptorTest.java new file mode 100644 index 00000000000..2727bf58486 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterDescriptorTest.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.core.cluster; + +import java.io.File; +import java.io.IOException; +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Tests clustering with a database. + */ +public class ClusterDescriptorTest extends JUnitTest { + + private RepositoryImpl rep1, rep2; + + public void setUp() throws Exception { + deleteAll(); + FileUtils.copyFile( + new File("./src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml"), + new File("./target/descriptorClusterTest/node1/repository.xml")); + FileUtils.copyFile( + new File("./src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml"), + new File("./target/descriptorClusterTest/node2/repository.xml")); + + rep1 = RepositoryImpl.create(RepositoryConfig.create( + new File("./target/descriptorClusterTest/node1"))); + rep2 = RepositoryImpl.create(RepositoryConfig.create( + new File("./target/descriptorClusterTest/node2"))); + } + + public void tearDown() throws Exception { + rep1.shutdown(); + rep2.shutdown(); + deleteAll(); + } + + private static void deleteAll() throws IOException { + FileUtils.deleteDirectory(new File("./target/descriptorClusterTest")); + } + + public void testRepositoryDescriptor() { + String clusterId1 = rep1.getDescriptor(RepositoryImpl.JACKRABBIT_CLUSTER_ID); + String clusterId2 = rep2.getDescriptor(RepositoryImpl.JACKRABBIT_CLUSTER_ID); + + assertNotNull("Cluster descriptor not set for cluster node 1", clusterId1); + assertNotNull("Cluster descriptor not set for cluster node 2", clusterId2); + + assertFalse("Cluster ids should be unique", clusterId1.equals(clusterId2)); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterRecordTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterRecordTest.java new file mode 100644 index 00000000000..b25ab43781f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterRecordTest.java @@ -0,0 +1,330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.util.ArrayList; +import java.util.Collections; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.cluster.SimpleEventListener.LockEvent; +import org.apache.jackrabbit.core.cluster.SimpleEventListener.NamespaceEvent; +import org.apache.jackrabbit.core.cluster.SimpleEventListener.NodeTypeEvent; +import org.apache.jackrabbit.core.cluster.SimpleEventListener.PrivilegeEvent; +import org.apache.jackrabbit.core.cluster.SimpleEventListener.UnlockEvent; +import org.apache.jackrabbit.core.cluster.SimpleEventListener.UpdateEvent; +import org.apache.jackrabbit.core.config.ClusterConfig; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.journal.Journal; +import org.apache.jackrabbit.core.journal.JournalFactory; +import org.apache.jackrabbit.core.journal.MemoryJournal; +import org.apache.jackrabbit.core.journal.MemoryJournal.MemoryRecord; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.nodetype.QNodeTypeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionImpl; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Test cases for cluster record production and consumption. Verifies that + * cluster record serialization and deseralization is correct. + */ +public class ClusterRecordTest extends JUnitTest { + + /** + * Defaut workspace name. + */ + private static final String DEFAULT_WORKSPACE = "default"; + + /** + * Default sync delay: 5 seconds. + */ + private static final long SYNC_DELAY = 5000; + + /** + * Update event factory. + */ + private final UpdateEventFactory factory = UpdateEventFactory.getInstance(); + + /** + * Records shared among multiple memory journals. + */ + private ArrayList records = new ArrayList(); + + /** + * Master. + */ + private ClusterNode master; + + /** + * Slave. + */ + private ClusterNode slave; + + /** + * {@inheritDoc} + */ + @Override + protected void setUp() throws Exception { + master = createClusterNode("master", records); + master.start(); + + slave = createClusterNode("slave", records); + + super.setUp(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void tearDown() throws Exception { + if (master != null) { + master.stop(); + } + if (slave != null) { + slave.stop(); + } + super.tearDown(); + } + + /** + * Test producing and consuming an update. + * @throws Exception + */ + public void testUpdateOperation() throws Exception { + UpdateEvent update = factory.createUpdateOperation(); + + UpdateEventChannel channel = master.createUpdateChannel(DEFAULT_WORKSPACE); + channel.updateCreated(update); + channel.updatePrepared(update); + channel.updateCommitted(update, null); + + SimpleEventListener listener = new SimpleEventListener(); + slave.createUpdateChannel(DEFAULT_WORKSPACE).setListener(listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), update); + } + + /** + * Test producing and consuming an update with a null userId + */ + public void testUpdateOperationWithNullUserId() throws Exception { + UpdateEvent update = factory.createUpdateOperationWithNullUserId(); + + UpdateEventChannel channel = master.createUpdateChannel(DEFAULT_WORKSPACE); + channel.updateCreated(update); + channel.updatePrepared(update); + channel.updateCommitted(update, null); + + SimpleEventListener listener = new SimpleEventListener(); + slave.createUpdateChannel(DEFAULT_WORKSPACE).setListener(listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), update); + } + + /** + * Test producing and consuming a lock operation. + * @throws Exception + */ + public void testLockOperation() throws Exception { + LockEvent event = new LockEvent(NodeId.randomId(), true, "admin"); + + master.createLockChannel(DEFAULT_WORKSPACE).create(event.getNodeId(), + event.isDeep(), event.getUserId()).ended(true); + + SimpleEventListener listener = new SimpleEventListener(); + slave.createLockChannel(DEFAULT_WORKSPACE).setListener(listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), event); + } + + /** + * Test producing and consuming an unlock operation. + * @throws Exception + */ + public void testUnlockOperation() throws Exception { + UnlockEvent event = new UnlockEvent(NodeId.randomId()); + + master.createLockChannel(DEFAULT_WORKSPACE).create(event.getNodeId()).ended(true); + + SimpleEventListener listener = new SimpleEventListener(); + slave.createLockChannel(DEFAULT_WORKSPACE).setListener(listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), event); + } + + /** + * Test producing and consuming a node type registration. + * @throws Exception + */ + public void testNodeTypeRegistration() throws Exception { + QNodeTypeDefinitionBuilder ntd = new QNodeTypeDefinitionBuilder(); + ntd.setName(NameFactoryImpl.getInstance().create("", "test")); + ntd.setSupertypes(new Name[]{NameConstants.NT_BASE}); + + ArrayList list = new ArrayList(); + list.add(ntd.build()); + + NodeTypeEvent event = new NodeTypeEvent(NodeTypeEvent.REGISTER, list); + master.registered(event.getCollection()); + + SimpleEventListener listener = new SimpleEventListener(); + slave.setListener((NodeTypeEventListener) listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), event); + } + + /** + * Test producing and consuming a node type reregistration. + * @throws Exception + */ + public void testNodeTypeReregistration() throws Exception { + QNodeTypeDefinitionBuilder ntd = new QNodeTypeDefinitionBuilder(); + ntd.setName(NameFactoryImpl.getInstance().create("", "test")); + ntd.setSupertypes(new Name[]{NameConstants.NT_BASE}); + + ArrayList list = new ArrayList(); + list.add(ntd.build()); + + NodeTypeEvent event = new NodeTypeEvent(NodeTypeEvent.REREGISTER, list); + master.reregistered(ntd.build()); + + SimpleEventListener listener = new SimpleEventListener(); + slave.setListener((NodeTypeEventListener) listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), event); + } + + /** + * Test producing and consuming a node type unregistration. + * @throws Exception + */ + public void testNodeTypeUnregistration() throws Exception { + Name name = NameFactoryImpl.getInstance().create("", "test"); + + ArrayList list = new ArrayList(); + list.add(name); + + NodeTypeEvent event = new NodeTypeEvent(NodeTypeEvent.UNREGISTER, list); + master.unregistered(list); + + SimpleEventListener listener = new SimpleEventListener(); + slave.setListener((NodeTypeEventListener) listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), event); + } + + /** + * Test producing and consuming a namespace registration. + * @throws Exception + */ + public void testNamespaceRegistration() throws Exception { + NamespaceEvent event = new NamespaceEvent(null, "test", "http://www.test.com"); + + master.remapped(event.getOldPrefix(), event.getNewPrefix(), event.getUri()); + + SimpleEventListener listener = new SimpleEventListener(); + slave.setListener((NamespaceEventListener) listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), event); + } + + /** + * Test producing and consuming a namespace unregistration. + * @throws Exception + */ + public void testNamespaceUnregistration() throws Exception { + NamespaceEvent event = new NamespaceEvent("test", null, null); + + master.remapped(event.getOldPrefix(), event.getNewPrefix(), event.getUri()); + + SimpleEventListener listener = new SimpleEventListener(); + slave.setListener((NamespaceEventListener) listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), event); + } + + /** + * Test producing and consuming a privilege registration. + * @throws Exception + */ + public void testPrivilegeRegistration() throws Exception { + PrivilegeDefinition pdf = new PrivilegeDefinitionImpl(NameFactoryImpl.getInstance().create("", "test"), false, null); + + PrivilegeEvent event = new PrivilegeEvent(Collections.singletonList(pdf)); + master.registeredPrivileges(event.getDefinitions()); + + SimpleEventListener listener = new SimpleEventListener(); + slave.setListener((PrivilegeEventListener) listener); + slave.sync(); + + assertEquals(1, listener.getClusterEvents().size()); + assertEquals(listener.getClusterEvents().get(0), event); + } + + /** + * Create a cluster node, with a memory journal referencing a list of records. + * + * @param id cluster node id + * @param records memory journal's list of records + */ + private ClusterNode createClusterNode( + String id, ArrayList records) throws Exception { + final MemoryJournal journal = new MemoryJournal(); + JournalFactory jf = new JournalFactory() { + public Journal getJournal(NamespaceResolver resolver) + throws RepositoryException { + return journal; + } + }; + ClusterConfig cc = new ClusterConfig(id, SYNC_DELAY, jf); + SimpleClusterContext context = new SimpleClusterContext(cc); + + journal.setRepositoryHome(context.getRepositoryHome()); + journal.init(id, context.getNamespaceResolver()); + if (records != null) { + journal.setRecords(records); + } + + ClusterNode clusterNode = new ClusterNode(); + clusterNode.init(context); + return clusterNode; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterSyncTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterSyncTest.java new file mode 100644 index 00000000000..83fbfa8adcd --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/ClusterSyncTest.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.util.ArrayList; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.cluster.SimpleEventListener.LockEvent; +import org.apache.jackrabbit.core.config.ClusterConfig; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.journal.Journal; +import org.apache.jackrabbit.core.journal.JournalFactory; +import org.apache.jackrabbit.core.journal.MemoryJournal; +import org.apache.jackrabbit.core.journal.Record; +import org.apache.jackrabbit.core.journal.RecordConsumer; +import org.apache.jackrabbit.core.journal.MemoryJournal.MemoryRecord; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.test.JUnitTest; + +import EDU.oswego.cs.dl.util.concurrent.Latch; + +/** + * Test cases for cluster synchronization. + */ +public class ClusterSyncTest extends JUnitTest { + + /** Defaut workspace name. */ + private static final String DEFAULT_WORKSPACE = "default"; + + /** Default sync delay: 5 seconds. */ + private static final long SYNC_DELAY = 5000; + + /** Master node. */ + private ClusterNode master; + + /** Slave node. */ + /* avoid synthetic accessor */ ClusterNode slave; + + /** Records shared among multiple memory journals. */ + private final ArrayList records = new ArrayList(); + + /** + * {@inheritDoc} + */ + @Override + protected void setUp() throws Exception { + master = createClusterNode("master", false); + master.start(); + + slave = createClusterNode("slave", true); + slave.start(); + + super.setUp(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void tearDown() throws Exception { + if (slave != null) { + slave.stop(); + } + if (master != null) { + master.stop(); + } + super.tearDown(); + } + + /** + * Verify that sync() on a cluster node will continue fetching results until no more + * changes are detected. + * + * @throws Exception + */ + public void testSyncAllChanges() throws Exception { + // create channel on master and slave + LockEventChannel channel = master.createLockChannel(DEFAULT_WORKSPACE); + slave.createLockChannel(DEFAULT_WORKSPACE).setListener(new SimpleEventListener()); + + // add blocking consumer to slave, this will block on the first non-empty sync() + BlockingConsumer consumer = new BlockingConsumer(); + slave.getJournal().register(consumer); + + // add first entry + LockEvent event = new LockEvent(NodeId.randomId(), true, "admin"); + channel.create(event.getNodeId(), event.isDeep(), event.getUserId()).ended(true); + + // start a manual sync on the slave and ... + Thread syncOnce = new Thread(new Runnable() { + public void run() { + try { + slave.sync(); + } catch (ClusterException e) { + /* ignore */ + } + } + }); + syncOnce.start(); + + // ... wait until it blocks + consumer.waitUntilBlocked(); + + // add second entry + event = new LockEvent(NodeId.randomId(), true, "admin"); + channel.create(event.getNodeId(), event.isDeep(), event.getUserId()).ended(true); + + // now unblock slave + consumer.unblock(); + + // wait for the sync to finish + syncOnce.join(); + + assertEquals(master.getRevision(), slave.getRevision()); + } + + /** + * Create a cluster node, with a memory journal referencing a list of records. + * + * @param id cluster node id + * @param records memory journal's list of records + * @param disableAutoSync if true background synchronization is disabled + */ + private ClusterNode createClusterNode(String id, boolean disableAutoSync) throws Exception { + final MemoryJournal journal = new MemoryJournal() { + protected boolean syncAgainOnNewRecords() { + return true; + } + }; + JournalFactory jf = new JournalFactory() { + public Journal getJournal(NamespaceResolver resolver) + throws RepositoryException { + return journal; + } + }; + ClusterConfig cc = new ClusterConfig(id, SYNC_DELAY, jf); + SimpleClusterContext context = new SimpleClusterContext(cc); + + journal.setRepositoryHome(context.getRepositoryHome()); + journal.init(id, context.getNamespaceResolver()); + journal.setRecords(records); + + ClusterNode clusterNode = new ClusterNode(); + clusterNode.init(context); + if (disableAutoSync) { + clusterNode.disableAutoSync(); + } + return clusterNode; + } + + /** + * Custom consumer that will block inside the journal's sync() method + * until it is unblocked. + */ + static class BlockingConsumer implements RecordConsumer { + + private final Latch blockLatch = new Latch(); + private final Latch unblockLatch = new Latch(); + private long revision; + + public String getId() { + return "CUSTOM"; + } + + public long getRevision() { + return revision; + } + + public void consume(Record record) { + /* nothing to be done here */ + } + + public void setRevision(long revision) { + blockLatch.release(); + + try { + unblockLatch.acquire(); + } catch (InterruptedException e) { + /* ignore */ + } + this.revision = revision; + } + + public void waitUntilBlocked() throws InterruptedException { + blockLatch.acquire(); + } + + public void unblock() { + unblockLatch.release(); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/DbClusterTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/DbClusterTest.java new file mode 100644 index 00000000000..5c12cc14f03 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/DbClusterTest.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.core.cluster; + +import java.io.File; +import java.io.IOException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Tests clustering with a database. + */ +public class DbClusterTest extends JUnitTest { + + public void setUp() throws Exception { + deleteAll(); + + FileUtils.copyFile( + new File("./src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml"), + new File("./target/dbClusterTest/node1/repository.xml")); + FileUtils.copyFile( + new File("./src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml"), + new File("./target/dbClusterTest/node2/repository.xml")); + } + + public void tearDown() throws Exception { + deleteAll(); + } + + private static void deleteAll() throws IOException { + FileUtils.deleteDirectory(new File("./target/dbClusterTest")); + } + + public void test() throws RepositoryException { + RepositoryImpl rep1 = RepositoryImpl.create(RepositoryConfig.create( + new File("./target/dbClusterTest/node1"))); + RepositoryImpl rep2 = RepositoryImpl.create(RepositoryConfig.create( + new File("./target/dbClusterTest/node2"))); + Session s1 = rep1.login(new SimpleCredentials("admin", "admin".toCharArray())); + Session s2 = rep2.login(new SimpleCredentials("admin", "admin".toCharArray())); + + s1.getRootNode().addNode("test1"); + s2.getRootNode().addNode("test2"); + s1.save(); + s2.save(); + s1.refresh(true); + s2.refresh(true); + + s1.getRootNode().getNode("test2"); + s2.getRootNode().getNode("test1"); + rep1.shutdown(); + rep2.shutdown(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/DbClusterTestJCR3162.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/DbClusterTestJCR3162.java new file mode 100644 index 00000000000..93d36aeab05 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/DbClusterTestJCR3162.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.core.cluster; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.util.UUID; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.query.Query; +import javax.jcr.query.RowIterator; + +import junit.framework.Assert; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Test for JCR3162 + */ +public class DbClusterTestJCR3162 extends JUnitTest { + + private static final SimpleCredentials ADMIN = new SimpleCredentials( + "admin", "admin".toCharArray()); + + private RepositoryImpl rep1; + private RepositoryImpl rep2; + + private String clusterId1 = UUID.randomUUID().toString(); + private String clusterId2 = UUID.randomUUID().toString(); + + private String prevClusterId; + + public void setUp() throws Exception { + deleteAll(); + FileUtils + .copyFile( + new File( + "./src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml"), + new File("./target/dbClusterTest/node1/repository.xml")); + FileUtils + .copyFile( + new File( + "./src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml"), + new File("./target/dbClusterTest/node2/repository.xml")); + + prevClusterId = System.setProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID, clusterId1); + rep1 = RepositoryImpl.create(RepositoryConfig.create(new File( + "./target/dbClusterTest/node1"))); + + System.setProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID, clusterId2); + + } + + public void tearDown() throws Exception { + // revert change to system property + if (prevClusterId == null) { + System.clearProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID); + } + else { + System.setProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID, prevClusterId); + } + + try { + rep1.shutdown(); + if (rep2 != null) { + rep2.shutdown(); + } + } finally { + deleteAll(); + } + } + + private static void deleteAll() throws IOException { + FileUtils.deleteDirectory(new File("./target/dbClusterTest")); + } + + public void test() throws RepositoryException { + int count = 5; + + // 1. create + Session s1 = rep1.login(ADMIN); + Node n = s1.getRootNode().addNode( + "test-cluster-" + System.currentTimeMillis(), + JcrConstants.NT_UNSTRUCTURED); + n.addMixin(JcrConstants.MIX_VERSIONABLE); + for (int i = 0; i < count; i++) { + Node c = n.addNode("child_" + i); + c.addMixin(JcrConstants.MIX_VERSIONABLE); + } + s1.save(); + + // 2. rollback journal revision + resetJournalRev(); + + // 3. sync & verify + // rep1.shutdown(); + + // start #2 with an empty search index + rep2 = RepositoryImpl.create(RepositoryConfig.create(new File( + "./target/dbClusterTest/node2"))); + + // verify + Session s2 = rep2.login(ADMIN); + checkConsistency(s2, "/", s2.getRootNode().getNodes().getSize()); + } + + private void resetJournalRev() { + Connection con = null; + try { + con = DriverManager.getConnection( + "jdbc:h2:./target/dbClusterTest/db", "sa", "sa"); + PreparedStatement prep = con + .prepareStatement("update JOURNAL_LOCAL_REVISIONS set REVISION_ID=0 where JOURNAL_ID=?"); + prep.setString(1, clusterId2); + prep.executeUpdate(); + prep.close(); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Unable to reset revision to 0. " + e.getMessage()); + } finally { + if (con != null) { + try { + con.close(); + } catch (Exception e) { + // e.printStackTrace(); + } + } + } + } + + private void checkConsistency(Session s, String path, long nodes) + throws RepositoryException { + + s.refresh(true); + Node n = s.getNode(path); + + RowIterator result = s + .getWorkspace() + .getQueryManager() + .createQuery( + "SELECT * FROM [" + JcrConstants.NT_BASE + + "] as NODE WHERE ischildnode(NODE, ['" + + n.getPath() + "'])", Query.JCR_SQL2) + .execute().getRows(); + + int foundViaQuery = 0; + while (result.hasNext()) { + result.next(); + foundViaQuery++; + } + + StringBuilder err = new StringBuilder("Path " + n.getPath() + ": "); + for (Node c : JcrUtils.getChildNodes(n)) { + err.append("("); + err.append(c.getPath()); + err.append("|"); + err.append(c.getPrimaryNodeType().getName()); + err.append("),"); + } + Assert.assertEquals(err.toString(), nodes, foundViaQuery); + + for (Node c : JcrUtils.getChildNodes(n)) { + checkConsistency(s, c.getPath(), c.getNodes().getSize()); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/FailUpdateOnJournalExceptionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/FailUpdateOnJournalExceptionTest.java new file mode 100644 index 00000000000..9e704e07ef5 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/FailUpdateOnJournalExceptionTest.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.core.cluster; + +import java.io.File; +import java.io.IOException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * FailUpdateOnJournalExceptionTest checks if + * UpdateEventChannel.updateCreated(Update) throws a ClusterException + * when locking the Journal fails. See JCR-3417 + */ +public class FailUpdateOnJournalExceptionTest extends JUnitTest { + + private RepositoryImpl repo; + + @Override + protected void setUp() throws Exception { + super.setUp(); + deleteAll(); + FileUtils.copyInputStreamToFile( + getClass().getResourceAsStream("repository-with-test-journal.xml"), + new File(getTestDir(), "repository.xml")); + repo = RepositoryImpl.create(RepositoryConfig.create(getTestDir())); + } + + @Override + protected void tearDown() throws Exception { + if (repo != null) { + repo.shutdown(); + } + deleteAll(); + super.tearDown(); + } + + public void testUpdate() throws Exception { + Session s = repo.login(new SimpleCredentials("admin", "admin".toCharArray())); + Node root = s.getRootNode(); + root.addNode("foo"); + s.save(); + root.addNode("bar"); + TestJournal.refuseLock = true; + try { + s.save(); + fail("Session.save() must fail with RepositoryException when Journal cannot be locked."); + } catch (RepositoryException e) { + // expected + } finally { + TestJournal.refuseLock = false; + } + } + + // JCR-3783 + public void testFailedWrite() throws Exception { + Session s = repo.login(new SimpleCredentials("admin", "admin".toCharArray())); + Node root = s.getRootNode(); + root.addNode("foo"); + s.save(); + root.addNode("bar"); + TestJournal.failRecordWrite = true; + try { + s.save(); + fail("Session.save() must fail with RepositoryException when Journal write fails."); + } catch (RepositoryException e) { + // expected + } finally { + TestJournal.failRecordWrite = false; + } + // must succeed after refresh + s.refresh(false); + root.addNode("bar"); + s.save(); + } + + private static void deleteAll() throws IOException { + FileUtils.deleteDirectory(getTestDir()); + } + + private static File getTestDir() throws IOException { + return new File("target", + FailUpdateOnJournalExceptionTest.class.getSimpleName()); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/SimpleClusterContext.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/SimpleClusterContext.java new file mode 100644 index 00000000000..c6134a58883 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/SimpleClusterContext.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.io.File; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.config.ClusterConfig; +import org.apache.jackrabbit.core.nodetype.xml.SimpleNamespaceRegistry; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.RegistryNamespaceResolver; + +/** + * Simple cluster context, providing only limited functionality. + */ +public class SimpleClusterContext implements ClusterContext { + + /** + * Cluster config. + */ + private final ClusterConfig cc; + + /** + * Repository home. + */ + private final File repositoryHome; + + /** + * Namespace resolver. + */ + private final NamespaceResolver nsResolver; + + /** + * Create a new instance of this class. + * + * @param cc cluster config + * @param repositoryHome repository home + */ + public SimpleClusterContext(ClusterConfig cc, File repositoryHome) { + this.cc = cc; + this.repositoryHome = repositoryHome; + + nsResolver = new RegistryNamespaceResolver(new SimpleNamespaceRegistry()); + } + + /** + * Create a new instance of this class. Equivalent to + *

    SimpleClusterContext(cc, null)
    + * + * @param cc cluster config + */ + public SimpleClusterContext(ClusterConfig cc) { + this(cc, null); + } + + //----------------------------------------------------------- ClusterContext + + /** + * {@inheritDoc} + */ + public ClusterConfig getClusterConfig() { + return cc; + } + + /** + * {@inheritDoc} + */ + public NamespaceResolver getNamespaceResolver() { + return nsResolver; + } + + /** + * {@inheritDoc} + */ + public File getRepositoryHome() { + return repositoryHome; + } + + /** + * {@inheritDoc} + */ + public void lockEventsReady(String workspace) throws RepositoryException { + // nothing to be done here + } + + /** + * {@inheritDoc} + */ + public void updateEventsReady(String workspace) throws RepositoryException { + // nothing to be done here + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/SimpleEventListener.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/SimpleEventListener.java new file mode 100644 index 00000000000..33d23b1f3bc --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/SimpleEventListener.java @@ -0,0 +1,685 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +/** + * Simple event listener that can be registered for all cluster event listener + * types and records external events in an array list. + */ +public class SimpleEventListener implements LockEventListener, + NodeTypeEventListener, NamespaceEventListener, PrivilegeEventListener, UpdateEventListener { + + /** + * List of cluster events received. + */ + public List clusterEvents = new ArrayList(); + + //-------------------------------------------------------- LockEventListener + + /** + * {@inheritDoc} + */ + public void externalLock(NodeId nodeId, boolean isDeep, String lockOwner) + throws RepositoryException { + + clusterEvents.add(new LockEvent(nodeId, isDeep, lockOwner)); + } + + /** + * Lock event auxiliary class. + */ + public static class LockEvent { + + /** + * Node id. + */ + private final NodeId nodeId; + + /** + * Deep flag. + */ + private final boolean isDeep; + + /** + * User id. + */ + private final String userId; + + /** + * Create a new instance of this class. + * + * @param nodeId node id + * @param isDeep deep flag + * @param userId user id + */ + public LockEvent(NodeId nodeId, boolean isDeep, String userId) { + this.nodeId = nodeId; + this.isDeep = isDeep; + this.userId = userId; + } + + /** + * Return the node id. + * + * @return the node id + */ + public NodeId getNodeId() { + return nodeId; + } + + /** + * Return a flag indicating whether the lock is deep. + * + * @return true if the lock is deep; + * false otherwise + */ + public boolean isDeep() { + return isDeep; + } + + /** + * Return the user owning the lock. + * + * @return user id + */ + public String getUserId() { + return userId; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return nodeId.hashCode() ^ userId.hashCode(); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof LockEvent) { + LockEvent other = (LockEvent) obj; + return nodeId.equals(other.nodeId) && + isDeep == other.isDeep && + userId.equals(other.userId); + } + return false; + } + } + + /** + * {@inheritDoc} + */ + public void externalUnlock(NodeId nodeId) throws RepositoryException { + clusterEvents.add(new UnlockEvent(nodeId)); + } + + /** + * Unlock event auxiliary class. + */ + public static class UnlockEvent { + + /** + * Node id. + */ + private final NodeId nodeId; + + /** + * Create a new instance of this class. + * + * @param nodeId node id + */ + public UnlockEvent(NodeId nodeId) { + this.nodeId = nodeId; + } + + /** + * Return the node id. + * + * @return node id + */ + public NodeId getNodeId() { + return nodeId; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return nodeId.hashCode(); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof UnlockEvent) { + UnlockEvent other = (UnlockEvent) obj; + return nodeId.equals(other.nodeId); + } + return false; + } + } + + //---------------------------------------------------- NodeTypeEventListener + + /** + * {@inheritDoc} + */ + public void externalRegistered(Collection ntDefs) + throws RepositoryException, InvalidNodeTypeDefException { + + clusterEvents.add(new NodeTypeEvent(NodeTypeEvent.REGISTER, ntDefs)); + } + + /** + * {@inheritDoc} + */ + public void externalReregistered(QNodeTypeDefinition ntDef) + throws NoSuchNodeTypeException, InvalidNodeTypeDefException, + RepositoryException { + + ArrayList ntDefs = new ArrayList(); + ntDefs.add(ntDef); + + clusterEvents.add(new NodeTypeEvent(NodeTypeEvent.REREGISTER, ntDefs)); + } + + /** + * {@inheritDoc} + */ + public void externalUnregistered(Collection ntNames) + throws RepositoryException, NoSuchNodeTypeException { + + clusterEvents.add(new NodeTypeEvent(NodeTypeEvent.UNREGISTER, ntNames)); + } + + /** + * Node type event auxiliary class. + */ + public static class NodeTypeEvent { + + /** + * Operation type: registration. + */ + public static final int REGISTER = NodeTypeRecord.REGISTER; + + /** + * Operation type: re-registration. + */ + public static final int REREGISTER = NodeTypeRecord.REREGISTER; + + /** + * Operation type: unregistration. + */ + public static final int UNREGISTER = NodeTypeRecord.UNREGISTER; + + /** + * Operation. + */ + private int operation; + + /** + * Collection of node type definitions or node type names. + */ + private Collection collection; + + /** + * Create a new instance of this class. + * + * @param operation operation + * @param collection collection of node type definitions or node + * type names + */ + public NodeTypeEvent(int operation, Collection collection) { + this.operation = operation; + this.collection = collection; + } + + /** + * Return the operation. + * + * @return operation + */ + public int getOperation() { + return operation; + } + + /** + * Return the collection. + * + * @return collection + */ + public Collection getCollection() { + return collection; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return operation ^ collection.hashCode(); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof NodeTypeEvent) { + NodeTypeEvent other = (NodeTypeEvent) obj; + return operation == other.operation && + SimpleEventListener.equals(collection, other.collection); + } + return false; + } + } + + //--------------------------------------------------- NamespaceEventListener + + /** + * {@inheritDoc} + */ + public void externalRemap(String oldPrefix, String newPrefix, String uri) + throws RepositoryException { + + clusterEvents.add(new NamespaceEvent(oldPrefix, newPrefix, uri)); + } + + /** + * Namespace event auxiliary class. + */ + public static class NamespaceEvent { + + /** + * Old prefix. + */ + private final String oldPrefix; + + /** + * New prefix. + */ + private final String newPrefix; + + /** + * URI. + */ + private final String uri; + + /** + * Create a new instance of this class. + * + * @param oldPrefix old prefix + * @param newPrefix new prefix + * @param uri URI + */ + public NamespaceEvent(String oldPrefix, String newPrefix, String uri) { + this.oldPrefix = oldPrefix; + this.newPrefix = newPrefix; + this.uri = uri; + } + + /** + * Return the old prefix. + * + * @return old prefix + */ + public String getOldPrefix() { + return oldPrefix; + } + + /** + * Return the new prefix. + * + * @return new prefix + */ + public String getNewPrefix() { + return newPrefix; + } + + /** + * Return the URI. + * + * @return URI + */ + public String getUri() { + return uri; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + int hashCode = 0; + if (oldPrefix != null) { + hashCode ^= oldPrefix.hashCode(); + } + if (newPrefix != null) { + hashCode ^= newPrefix.hashCode(); + } + if (uri != null) { + hashCode ^= uri.hashCode(); + } + return hashCode; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof NamespaceEvent) { + NamespaceEvent other = (NamespaceEvent) obj; + return SimpleEventListener.equals(oldPrefix, other.oldPrefix) && + SimpleEventListener.equals(newPrefix, other.newPrefix) && + SimpleEventListener.equals(uri, other.uri); + } + return false; + } + } + + //---------------------------------------------< PrivilegeEventListener >--- + /** + * {@inheritDoc} + */ + public void externalRegisteredPrivileges(Collection definitions) throws RepositoryException { + clusterEvents.add(new PrivilegeEvent(definitions)); + } + + /** + * privilege event auxiliary class. + */ + public static class PrivilegeEvent { + + /** + * Collection of node type definitions or node type names. + */ + private Collection definitions; + + /** + * Create a new instance of this class. + * + * @param definitions + */ + public PrivilegeEvent(Collection definitions) { + this.definitions = definitions; + } + + /** + * Return the definitions. + * + * @return definitions + */ + public Collection getDefinitions() { + return definitions; + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + return definitions.hashCode(); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof PrivilegeEvent) { + PrivilegeEvent other = (PrivilegeEvent) obj; + return SimpleEventListener.equals(definitions, other.definitions); + } + return false; + } + } + + //------------------------------------------------------ UpdateEventListener + + /** + * {@inheritDoc} + */ + public void externalUpdate(ChangeLog changes, List events, + long timestamp, String userData) + throws RepositoryException { + + clusterEvents.add(new UpdateEvent(changes, events, timestamp, userData)); + + } + + /** + * Update event auxiliary class. + */ + public static class UpdateEvent implements Update { + + /** + * Change log. + */ + private final ChangeLog changes; + + /** + * List of EventStates. + */ + private final List events; + + /** + * Attributes to be stored. + */ + private final transient Map attributes = new HashMap(); + + /** + * Timestamp when the changes in this update event occured. + */ + private final long timestamp; + + /** + * The user data associated with this update. + */ + private final String userData; + + /** + * Create a new instance of this class. + * + * @param changes change log + * @param events list of EventStates + * @param timestamp time when the changes in this event occured. + * @param userData the user data associated with this update. + */ + public UpdateEvent(ChangeLog changes, List events, + long timestamp, String userData) { + this.changes = changes; + this.events = events; + this.timestamp = timestamp; + this.userData = userData; + } + + /** + * Return the change log. + * + * @return the change log + */ + public ChangeLog getChanges() { + return changes; + } + + /** + * Return the list of EventStates + * + * @return list of EventStates + */ + public List getEvents() { + return events; + } + + /** + * {@inheritDoc} + */ + public long getTimestamp() { + return timestamp; + } + + public String getUserData() { + return userData; + } + + /** + * {@inheritDoc} + */ + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + /** + * {@inheritDoc} + */ + public Object getAttribute(String name) { + return attributes.get(name); + } + + /** + * {@inheritDoc} + */ + public int hashCode() { + int h = changes.hashCode() ^ events.hashCode() ^ (int) (timestamp ^ (timestamp >>> 32)); + if (userData != null) { + h = h ^ userData.hashCode(); + } + return h; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof UpdateEvent) { + UpdateEvent other = (UpdateEvent) obj; + return SimpleEventListener.equals(changes, other.changes) && + SimpleEventListener.equals(events, other.events) && + timestamp == other.timestamp && + SimpleEventListener.equals(userData, other.userData); + } + return false; + } + + } + + /** + * Return the collected cluster events. + * + * @return cluster events + */ + public List getClusterEvents() { + return Collections.unmodifiableList(clusterEvents); + } + + /** + * Check whether two objects are equals, allowing null values. + * + * @param o1 object 1 + * @param o2 object 2 + * @return true if they are equal; false otherwise + */ + private static boolean equals(Object o1, Object o2) { + if (o1 == null) { + return o2 == null; + } else { + return o1.equals(o2); + } + } + + /** + * Check whether two collections contain the same elements. Made necessary + * because the Collections.unmodifiableXXX methods do not + * return objects that override equals and hashCode. + * + * @param c1 collection 1 + * @param c2 collection 2 + * @return true if they are equal; false otherwise + */ + private static boolean equals(Collection c1, Collection c2) { + if (c1.size() != c2.size()) { + return false; + } + Iterator iter1 = c1.iterator(); + Iterator iter2 = c2.iterator(); + + while (iter1.hasNext()) { + Object o1 = iter1.next(); + Object o2 = iter2.next(); + if (!o1.equals(o2)) { + return false; + } + } + return true; + } + + /** + * Check whether two changes logs contain the same elements. Not feasible + * by comparing the maps because ItemStates do not override + * {@link Object#equals(Object)}. + * + * @param changes1 change log + * @param changes2 change log + * @return true if the change logs are equals; + * false otherwise. + */ + private static boolean equals(ChangeLog changes1, ChangeLog changes2) { + return equals(changes1.addedStates().iterator(), changes2.addedStates().iterator()) && + equals(changes1.deletedStates().iterator(), changes2.deletedStates().iterator()) && + equals(changes1.modifiedStates().iterator(), changes2.modifiedStates().iterator()); + } + + /** + * Check whether two iterators return the same item states, where "same" + * means having the same ItemId + * @param iter1 first iterator + * @param iter2 second iterator + * @return true if the two iterators are equal; + * false otherwise + */ + private static boolean equals(Iterator iter1, Iterator iter2) { + for (;;) { + if (!iter1.hasNext() && !iter2.hasNext()) { + return true; + } + if (iter1.hasNext() && !iter2.hasNext()) { + return false; + } + if (!iter1.hasNext() && iter2.hasNext()) { + return false; + } + ItemState state1 = (ItemState) iter1.next(); + ItemState state2 = (ItemState) iter2.next(); + return state1.getId().equals(state2.getId()); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/TestAll.java new file mode 100644 index 00000000000..70538ff5f51 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/TestAll.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.core.cluster; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Collects all test classes setting up test data in the workspace. This test + * data is specific to Jackrabbit and will probably not work in other + * implementations. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite(); + + suite.addTestSuite(ClusterRecordTest.class); + suite.addTestSuite(ClusterSyncTest.class); + suite.addTestSuite(DbClusterTest.class); + suite.addTestSuite(DbClusterTestJCR3162.class); + suite.addTestSuite(FailUpdateOnJournalExceptionTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/TestJournal.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/TestJournal.java new file mode 100644 index 00000000000..edee7f932a5 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/TestJournal.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.core.cluster; + +import org.apache.jackrabbit.core.journal.AppendRecord; +import org.apache.jackrabbit.core.journal.DefaultRecordProducer; +import org.apache.jackrabbit.core.journal.JournalException; +import org.apache.jackrabbit.core.journal.MemoryJournal; +import org.apache.jackrabbit.core.journal.RecordProducer; + +/** +* TestJournal extends the MemoryJournal with a static hook to +* refuse lock acquisition. +*/ +public final class TestJournal extends MemoryJournal { + + static boolean refuseLock = false; + + static boolean failRecordWrite = false; + + @Override + protected void doLock() throws JournalException { + if (refuseLock) { + throw new JournalException("lock refused"); + } else { + super.doLock(); + } + } + + @Override + protected RecordProducer createProducer(final String identifier) { + return new DefaultRecordProducer(this, identifier) { + @Override + protected AppendRecord createRecord() throws JournalException { + return new AppendRecord(TestJournal.this, identifier) { + @Override + public void writeString(String s) throws JournalException { + if (failRecordWrite) { + throw new JournalException("write failed"); + } else { + super.writeString(s); + } + } + }; + } + }; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java new file mode 100644 index 00000000000..740c21ed323 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/cluster/UpdateEventFactory.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.cluster; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.jcr.Session; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.cluster.SimpleEventListener.UpdateEvent; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; + +/** + * Simplistic factory that produces update events, consisting of node identifiers, + * property identifiers and event states. + */ +public class UpdateEventFactory { + + /** + * Root node id. + */ + private static final NodeId ROOT_NODE_ID = RepositoryImpl.ROOT_NODE_ID; + + /** + * Default user. + */ + private static final String DEFAULT_USER = "admin"; + + /** + * Instance of this class. + */ + private static final UpdateEventFactory INSTANCE = new UpdateEventFactory(); + + /** + * Default session, used for event state creation. + */ + private final Session session = new ClusterSession(DEFAULT_USER); + + /** + * Name factory. + */ + private NameFactory nameFactory = NameFactoryImpl.getInstance(); + + /** + * Path factory. + */ + private PathFactory pathFactory = PathFactoryImpl.getInstance(); + + /** + * Create a new instance of this class. Private as there is only one + * instance acting as singleton. + */ + private UpdateEventFactory() { + } + + /** + * Return the instance of this class. + * + * @return factory + */ + public static UpdateEventFactory getInstance() { + return INSTANCE; + } + + /** + * Create an update operation. + * + * @return update operation + */ + public UpdateEvent createUpdateOperation() { + NodeState n1 = createNodeState(); + NodeState n2 = createNodeState(); + NodeState n3 = createNodeState(); + PropertyState p1 = createPropertyState(n1.getNodeId(), "{}a"); + PropertyState p2 = createPropertyState(n2.getNodeId(), "{}b"); + + ChangeLog changes = new ChangeLog(); + changes.added(n1); + changes.added(p1); + changes.deleted(p2); + changes.modified(n2); + changes.deleted(n3); + + List events = new ArrayList(); + events.add(createEventState(n1, Event.NODE_ADDED, "{}n1", session)); + events.add(createEventState(p1, n1, Event.PROPERTY_ADDED, session)); + events.add(createEventState(p2, n2, Event.PROPERTY_REMOVED, session)); + events.add(createEventState(n3, Event.NODE_REMOVED, "{}n3", session)); + + return new UpdateEvent(changes, events, System.currentTimeMillis(), "user-data"); + } + + /** + * Create an update operation. + * + * @return update operation + */ + public UpdateEvent createUpdateOperationWithNullUserId() { + NodeState n1 = createNodeState(); + NodeState n2 = createNodeState(); + NodeState n3 = createNodeState(); + PropertyState p1 = createPropertyState(n1.getNodeId(), "{}a"); + PropertyState p2 = createPropertyState(n2.getNodeId(), "{}b"); + + ChangeLog changes = new ChangeLog(); + changes.added(n1); + changes.added(p1); + changes.deleted(p2); + changes.modified(n2); + changes.deleted(n3); + + Session s = new ClusterSession(null); + List events = new ArrayList(); + events.add(createEventState(n1, Event.NODE_ADDED, "{}n1", s)); + events.add(createEventState(p1, n1, Event.PROPERTY_ADDED, s)); + events.add(createEventState(p2, n2, Event.PROPERTY_REMOVED, s)); + events.add(createEventState(n3, Event.NODE_REMOVED, "{}n3", s)); + + return new UpdateEvent(changes, events, System.currentTimeMillis(), "user-data"); + } + + + /** + * Create a node state. + * + * @return node state + */ + protected NodeState createNodeState() { + Name ntName = nameFactory.create("{}testnt"); + NodeState n = new NodeState( + NodeId.randomId(), ntName, + ROOT_NODE_ID, NodeState.STATUS_EXISTING, false); + n.setMixinTypeNames(Collections.EMPTY_SET); + return n; + } + + /** + * Create a property state. + * + * @param parentId parent node id + * @param name property name + * @return property state. + */ + protected PropertyState createPropertyState(NodeId parentId, String name) { + Name propName = nameFactory.create(name); + return new PropertyState( + new PropertyId(parentId, propName), + NodeState.STATUS_EXISTING, false); + } + + /** + * Create an event state for an operation on a node. + * + * @param n node state + * @param type Event.NODE_ADDED or Event.NODE_REMOVED + * @param name node name + * @param session the session that produced the event. + * @return event state + */ + protected EventState createEventState(NodeState n, int type, String name, + Session session) { + Path relPath = pathFactory.create(nameFactory.create(name)); + + switch (type) { + case Event.NODE_ADDED: + return EventState.childNodeAdded( + n.getParentId(), pathFactory.getRootPath(), + n.getNodeId(), relPath, n.getNodeTypeName(), + n.getMixinTypeNames(), session); + case Event.NODE_REMOVED: + return EventState.childNodeRemoved( + n.getParentId(), pathFactory.getRootPath(), + n.getNodeId(), relPath, n.getNodeTypeName(), + n.getMixinTypeNames(), session); + } + return null; + } + + /** + * Create an event state for a property operation. + * + * @param p property state + * @param parent parent node state + * @param type Event.NODE_ADDED or Event.NODE_REMOVED + * @param session the session that produces the event. + * @return event state + */ + protected EventState createEventState(PropertyState p, NodeState parent, int type, + Session session) { + Path relPath = pathFactory.create(p.getName()); + + switch (type) { + case Event.PROPERTY_ADDED: + return EventState.propertyAdded( + p.getParentId(), pathFactory.getRootPath(), relPath, + parent.getNodeTypeName(), parent.getMixinTypeNames(), + session); + case Event.PROPERTY_CHANGED: + return EventState.propertyChanged( + p.getParentId(), pathFactory.getRootPath(), relPath, + parent.getNodeTypeName(), parent.getMixinTypeNames(), + session); + case Event.PROPERTY_REMOVED: + return EventState.propertyRemoved( + p.getParentId(), pathFactory.getRootPath(), relPath, + parent.getNodeTypeName(), parent.getMixinTypeNames(), + session); + } + return null; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/DataSourceConfigTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/DataSourceConfigTest.java new file mode 100644 index 00000000000..506cf2a849d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/DataSourceConfigTest.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.core.config; + +import java.util.Properties; + +import org.apache.jackrabbit.core.config.DataSourceConfig.DataSourceDefinition; + +import junit.framework.TestCase; + +public class DataSourceConfigTest extends TestCase { + + private DataSourceConfig cfg; + + private Properties minimalProps; + + private Properties minimalProps2; + + @Override + public void setUp() { + cfg = new DataSourceConfig(); + minimalProps = new Properties(); + minimalProps.put(DataSourceConfig.DRIVER, "org.apache.derby.jdbc.EmbeddedDriver"); + minimalProps.put(DataSourceConfig.URL, "url"); + minimalProps.put(DataSourceConfig.DB_TYPE, "dbType"); + minimalProps2 = new Properties(); + minimalProps2.put(DataSourceConfig.DRIVER, "org.apache.derby.jdbc.EmbeddedDriver"); + minimalProps2.put(DataSourceConfig.URL, "url2"); + minimalProps2.put(DataSourceConfig.DB_TYPE, "dbType2"); + } + + public void testEmptyConfig() { + assertEquals(0, cfg.getDefinitions().size()); + } + + public void testMinimalRegularConfig() throws ConfigurationException { + cfg.addDataSourceDefinition("ds", minimalProps); + DataSourceDefinition def = cfg.getDefinitions().get(0); + assertEquals("ds", def.getLogicalName()); + assertEquals("org.apache.derby.jdbc.EmbeddedDriver", def.getDriver()); + assertEquals("url", def.getUrl()); + assertEquals("dbType", def.getDbType()); + // check default values: + assertNull(def.getUser()); + assertNull(def.getPassword()); + assertNull(def.getValidationQuery()); + assertEquals(-1, def.getMaxPoolSize()); // unlimited + } + + public void testMultipleDefs() throws ConfigurationException { + cfg.addDataSourceDefinition("ds1", minimalProps); + cfg.addDataSourceDefinition("ds2", minimalProps2); + assertEquals(2, cfg.getDefinitions().size()); + } + + public void testTooMinimalConfig() { + try { + minimalProps.remove(DataSourceConfig.URL); + cfg.addDataSourceDefinition("ds", minimalProps); + fail(); + } catch (ConfigurationException e) { + // expected + } + } + + public void testInvalidProperty() { + try { + minimalProps.put("unknown property", "value"); + cfg.addDataSourceDefinition("ds", minimalProps); + fail(); + } catch (ConfigurationException e) { + // expected + } + } + + public void testUnparseableProperty() { + try { + minimalProps.put(DataSourceConfig.MAX_POOL_SIZE, "no int"); + cfg.addDataSourceDefinition("ds", minimalProps); + fail(); + } catch (ConfigurationException e) { + // expected + } + } + + public void testDuplicateLogicalName() throws ConfigurationException { + cfg.addDataSourceDefinition("ds", minimalProps); + try { + cfg.addDataSourceDefinition("ds", minimalProps2); + fail(); + } catch (ConfigurationException e) { + // expected + } + } + + public void testEmptyLogicalName() throws ConfigurationException { + try { + cfg.addDataSourceDefinition(" ", minimalProps); + fail(); + } catch (ConfigurationException e) { + // expected + } + } + + public void testNullLogicalName() throws ConfigurationException { + try { + cfg.addDataSourceDefinition(null, minimalProps); + fail(); + } catch (ConfigurationException e) { + // expected + } + } + + /** + * It only makes sense to configure driver, url, username, password and dbType for + * a DataSource which is to be obtained from JNDI. + * + * @throws ConfigurationException + */ + public void testConfiguredJNDIConfig() throws ConfigurationException { + minimalProps.put(DataSourceConfig.DRIVER, "javax.naming.InitialContext"); + minimalProps.put(DataSourceConfig.MAX_POOL_SIZE, "10"); + try { + cfg.addDataSourceDefinition("ds", minimalProps); + fail(); + } catch (ConfigurationException e) { + // expected + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/RepositoryConfigTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/RepositoryConfigTest.java new file mode 100644 index 00000000000..a9eed922647 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/RepositoryConfigTest.java @@ -0,0 +1,338 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.config; + + import junit.framework.TestCase; +import org.xml.sax.InputSource; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.input.ClosedInputStream; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Test cases for repository configuration handling. + */ +public class RepositoryConfigTest extends TestCase { + + private static final File DIR = + new File("target", "RepositoryConfigTest"); + + private static final File XML = + new File(DIR, "repository.xml"); + + private RepositoryConfig config; + + /** + * Sets up the test case by creating the repository home directory + * and copying the repository configuration file in place. + */ + protected void setUp() throws Exception { + config = RepositoryConfig.install(DIR); + } + + protected void tearDown() { + FileUtils.deleteQuietly(DIR); + } + + public void testCreateWithRepositoryDirectory() { + try { + RepositoryConfig.create(DIR); + } catch (ConfigurationException e) { + fail("Valid repository directory"); + } + + try { + RepositoryConfig.create(new File(DIR, "invalid-repo-dir")); + fail("Invalid repository directory"); + } catch (ConfigurationException e) { + } + } + + public void testCreateWithRepositoryConfigAndDirectory() { + try { + RepositoryConfig.create(XML, DIR); + } catch (ConfigurationException e) { + fail("Valid repository configuration and directory"); + } + + try { + RepositoryConfig.create(XML, new File(DIR, "invalid-repo-dir")); + fail("Invalid repository directory"); + } catch (ConfigurationException e) { + } + + try { + RepositoryConfig.create(new File(DIR, "invalid.xml"), DIR); + fail("Invalid repository configuration"); + } catch (ConfigurationException e) { + } + } + + /** + * Tests that a file name can be used for the configuration. + */ + public void testRepositoryConfigCreateWithFileName() { + try { + RepositoryConfig.create(XML.getPath(), DIR.getPath()); + } catch (ConfigurationException e) { + fail("Valid configuration file name"); + } + + try { + RepositoryConfig.create( + new File(DIR, "invalid-config-file.xml").getPath(), + DIR.getPath()); + fail("Invalid configuration file name"); + } catch (ConfigurationException e) { + } + } + + /** + * Tests that a URI can be used for the configuration. + */ + public void testRepositoryConfigCreateWithURI() throws URISyntaxException { + try { + RepositoryConfig.create(XML.toURI(), DIR.getPath()); + } catch (ConfigurationException e) { + fail("Valid configuration URI"); + } + + try { + RepositoryConfig.create( + new File(DIR, "invalid-config-file.xml").toURI(), + DIR.getPath()); + fail("Invalid configuration URI"); + } catch (ConfigurationException e) { + } + + try { + RepositoryConfig.create( + new URI("invalid://config/uri"), + DIR.getPath()); + fail("Invalid configuration URI"); + } catch (ConfigurationException e) { + } + } + + /** + * Tests that an input stream can be used for the configuration. + */ + public void testRepositoryConfigCreateWithInputStream() throws IOException { + InputStream input = new FileInputStream(XML); + try { + RepositoryConfig.create(input, DIR.getPath()); + } catch (ConfigurationException e) { + fail("Valid configuration input stream"); + } finally { + input.close(); + } + + try { + RepositoryConfig.create( + new InputStream() { + public int read() throws IOException { + throw new IOException("invalid input stream"); + } + }, + DIR.getPath()); + fail("Invalid configuration input stream"); + } catch (ConfigurationException e) { + } + + try { + RepositoryConfig.create( + new ClosedInputStream(), + DIR.getPath()); + fail("Invalid configuration input stream"); + } catch (ConfigurationException e) { + } + } + + /** + * Tests that an InputSource can be used for the configuration. + */ + public void testRepositoryConfigCreateWithInputSource() throws IOException { + try { + InputSource source = new InputSource(XML.toURI().toString()); + RepositoryConfig.create(source, DIR.getPath()); + } catch (ConfigurationException e) { + fail("Valid configuration input source with file URI"); + } + + InputStream stream = new FileInputStream(XML); + try { + InputSource source = new InputSource(stream); + RepositoryConfig.create(source, DIR.getPath()); + } catch (ConfigurationException e) { + fail("Valid configuration input source with input stream"); + } finally { + stream.close(); + } + } + + /** + * Test that the repository configuration file is correctly parsed. + */ + public void testRepositoryConfig() throws Exception { + assertRepositoryConfiguration(config); + } + + public void testInit() throws Exception { + File workspaces_dir = new File(DIR, "workspaces"); + File workspace_dir = new File(workspaces_dir, "default"); + File workspace_xml = new File(workspace_dir, "workspace.xml"); + assertTrue("Default workspace is created", workspace_xml.exists()); + } + + public void testCreateWorkspaceConfig() throws Exception { + config.createWorkspaceConfig("test-workspace", (StringBuffer) null); + File workspaces_dir = new File(DIR, "workspaces"); + File workspace_dir = new File(workspaces_dir, "test-workspace"); + File workspace_xml = new File(workspace_dir, "workspace.xml"); + assertTrue(workspace_xml.exists()); + } + + public void testCreateDuplicateWorkspaceConfig() throws Exception { + try { + config.createWorkspaceConfig("default", (StringBuffer) null); + fail("No exception thrown when creating a duplicate workspace"); + } catch (ConfigurationException e) { + // test passed + } + } + + public void testRepositoryConfigWithSystemVariables() throws Exception { + final String id = "testvalue"; + final long syncDelay = 11; + + System.setProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID, id); + System.setProperty("cluster.syncDelay", Long.toString(syncDelay)); + try { + InputStream in = getClass().getResourceAsStream( + "/org/apache/jackrabbit/core/cluster/repository.xml"); + RepositoryConfig config = RepositoryConfig.create(in, DIR.getPath()); + + ClusterConfig clusterConfig = config.getClusterConfig(); + assertEquals(id, clusterConfig.getId()); + assertEquals(syncDelay, clusterConfig.getSyncDelay()); + } finally { + System.clearProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID); + System.clearProperty("cluster.syncDelay"); + } + } + + public void testAutomaticClusterNodeIdCreation() throws Exception { + final long syncDelay = 12; + + assertNull( + "This test requires the system property " + ClusterNode.SYSTEM_PROPERTY_NODE_ID + " not to be set; found value: " + + System.getProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID) + " (leftover from broken unit test?)", + System.getProperty(ClusterNode.SYSTEM_PROPERTY_NODE_ID)); + System.setProperty("cluster.syncDelay", Long.toString(syncDelay)); + try { + File file = new File(DIR, "cluster_node.id"); + assertFalse(file.exists()); + + // Check that a new cluster node id is automatically persisted + InputStream in = getClass().getResourceAsStream( + "/org/apache/jackrabbit/core/cluster/repository.xml"); + RepositoryConfig config = RepositoryConfig.create(in, DIR.getPath()); + + assertTrue(file.exists()); + String id = FileUtils.readFileToString(file); + + ClusterConfig clusterConfig = config.getClusterConfig(); + assertEquals(id, clusterConfig.getId()); + assertEquals(syncDelay, clusterConfig.getSyncDelay()); + + // Check that the persisted cluster node id is used when it exists + in = getClass().getResourceAsStream( + "/org/apache/jackrabbit/core/cluster/repository.xml"); + config = RepositoryConfig.create(in, DIR.getPath()); + + assertTrue(file.exists()); + assertEquals(id, FileUtils.readFileToString(file)); + + clusterConfig = config.getClusterConfig(); + assertEquals(id, clusterConfig.getId()); + assertEquals(syncDelay, clusterConfig.getSyncDelay()); + } finally { + System.clearProperty("cluster.syncDelay"); + } + } + + /** + * Test that a RepositoryConfig can be copied into a new instance. + * + * @throws Exception if an unexpected error occurs during the test + */ + public void testCopyConfig() throws Exception + { + RepositoryConfig copyConfig = RepositoryConfig.create(config); + + assertNotNull("Configuration not created properly", copyConfig); + assertRepositoryConfiguration(copyConfig); + } + + private void assertRepositoryConfiguration(RepositoryConfig config) + throws ConfigurationException { + assertEquals(DIR.getPath(), config.getHomeDir()); + assertEquals("default", config.getDefaultWorkspaceName()); + assertEquals( + new File(DIR, "workspaces").getPath(), + new File(config.getWorkspacesConfigRootDir()).getPath()); + assertEquals("Jackrabbit", config.getSecurityConfig().getAppName()); + + // SecurityManagerConfig + SecurityManagerConfig smc = + config.getSecurityConfig().getSecurityManagerConfig(); + assertEquals( + "org.apache.jackrabbit.core.DefaultSecurityManager", + smc.getClassName()); + assertTrue(smc.getParameters().isEmpty()); + assertNotNull(smc.getWorkspaceName()); + + BeanConfig bc = smc.getWorkspaceAccessConfig(); + if (bc != null) { + WorkspaceAccessManager wac = + smc.getWorkspaceAccessConfig().newInstance(WorkspaceAccessManager.class); + assertEquals("org.apache.jackrabbit.core.security.simple.SimpleWorkspaceAccessManager", wac.getClass().getName()); + } + + // AccessManagerConfig + AccessManagerConfig amc = + config.getSecurityConfig().getAccessManagerConfig(); + assertEquals( + "org.apache.jackrabbit.core.security.DefaultAccessManager", + amc.getClassName()); + assertTrue(amc.getParameters().isEmpty()); + + VersioningConfig vc = config.getVersioningConfig(); + assertEquals(new File(DIR, "version"), vc.getHomeDir()); + assertEquals( + "org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager", + vc.getPersistenceManagerConfig().getClassName()); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/SecurityConfigTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/SecurityConfigTest.java new file mode 100644 index 00000000000..5931afff02e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/SecurityConfigTest.java @@ -0,0 +1,460 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.config; + +import org.apache.jackrabbit.core.DefaultSecurityManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.core.security.DefaultAccessManager; +import org.apache.jackrabbit.core.security.JackrabbitSecurityManager; +import org.apache.jackrabbit.core.security.principal.PrincipalProvider; +import org.apache.jackrabbit.core.security.principal.PrincipalProviderRegistry; +import org.apache.jackrabbit.core.security.principal.ProviderRegistryImpl; +import org.apache.jackrabbit.core.security.user.UserManagerImpl; +import org.apache.jackrabbit.core.security.user.UserPerWorkspaceUserManager; +import org.apache.jackrabbit.core.security.authentication.DefaultLoginModule; +import org.apache.jackrabbit.core.security.simple.SimpleAccessManager; +import org.apache.jackrabbit.core.security.simple.SimpleSecurityManager; +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.io.StringReader; +import java.util.Properties; +import java.util.List; +import java.util.ArrayList; + +/** SecurityConfigTest... */ +public class SecurityConfigTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(SecurityConfigTest.class); + + private RepositoryConfigurationParser parser; + + @Override + protected void setUp() throws Exception { + super.setUp(); + parser = new RepositoryConfigurationParser(new Properties()); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testConfig1() throws ConfigurationException { + Element xml = parseXML(new InputSource(new StringReader(CONFIG_1)), true); + SecurityConfig config = parser.parseSecurityConfig(xml); + + assertNotNull(config.getAppName()); + assertEquals("Jackrabbit", config.getAppName()); + + SecurityManagerConfig smc = config.getSecurityManagerConfig(); + assertNotNull(smc); + assertTrue(smc.newInstance(JackrabbitSecurityManager.class) instanceof SimpleSecurityManager); + assertNull(smc.getWorkspaceAccessConfig()); + assertNull(smc.getWorkspaceName()); + + assertNotNull(config.getAccessManagerConfig()); + assertTrue(config.getAccessManagerConfig().newInstance(AccessManager.class) instanceof SimpleAccessManager); + + assertNull(config.getLoginModuleConfig()); + } + + public void testConfig2() throws ConfigurationException { + Element xml = parseXML(new InputSource(new StringReader(CONFIG_2)), true); + SecurityConfig config = parser.parseSecurityConfig(xml); + + assertNotNull(config.getAppName()); + assertEquals("Jackrabbit", config.getAppName()); + + SecurityManagerConfig smc = config.getSecurityManagerConfig(); + assertNotNull(smc); + assertTrue(smc.newInstance(JackrabbitSecurityManager.class) instanceof DefaultSecurityManager); + assertNull(smc.getWorkspaceAccessConfig()); + assertEquals("security", smc.getWorkspaceName()); + + assertNull(smc.getUserManagerConfig()); + + AccessManagerConfig amc = config.getAccessManagerConfig(); + assertNotNull(amc); + assertTrue(amc.newInstance(AccessManager.class) instanceof DefaultAccessManager); + + LoginModuleConfig lmc = config.getLoginModuleConfig(); + assertNotNull(lmc); + assertTrue(lmc.getLoginModule() instanceof DefaultLoginModule); + Properties options = lmc.getParameters(); + assertNotNull(options); + assertEquals("anonymous", options.getProperty("anonymousId")); + assertEquals("admin", options.getProperty("adminId")); + assertEquals("org.apache.jackrabbit.TestPrincipalProvider", options.getProperty("principalProvider")); + } + + public void testConfig3() throws ConfigurationException { + Element xml = parseXML(new InputSource(new StringReader(CONFIG_3)), true); + SecurityConfig config = parser.parseSecurityConfig(xml); + + SecurityManagerConfig smc = config.getSecurityManagerConfig(); + + assertEquals(ItemBasedPrincipal.class, smc.getUserIdClass()); + + UserManagerConfig umc = smc.getUserManagerConfig(); + assertNotNull(umc); + Properties params = umc.getParameters(); + assertNotNull(params); + + assertFalse(params.containsKey(UserManagerImpl.PARAM_COMPATIBLE_JR16)); + assertTrue(Boolean.parseBoolean(params.getProperty(UserManagerImpl.PARAM_AUTO_EXPAND_TREE))); + assertEquals(4, Integer.parseInt(params.getProperty(UserManagerImpl.PARAM_DEFAULT_DEPTH))); + assertEquals(2000, Long.parseLong(params.getProperty(UserManagerImpl.PARAM_AUTO_EXPAND_SIZE))); + } + + public void testUserManagerConfig() throws RepositoryException, UnsupportedRepositoryOperationException { + Element xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_INVALID)), true); + UserManagerConfig umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + try { + umc.getUserManager(UserManagerImpl.class, new Class[] {String.class}, "invalid"); + fail("Nonexisting umgr implementation -> instanciation must fail."); + } catch (ConfigurationException e) { + // success + } + + xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_IMPL)), true); + umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + + // assignable from same class as configured + UserManager um = umc.getUserManager(UserManagerImpl.class, new Class[] { + SessionImpl.class, String.class}, superuser, "admin"); + assertNotNull(um); + assertTrue(um instanceof UserManagerImpl); + assertTrue(um.isAutoSave()); + try { + um.autoSave(false); + fail("must not be allowed"); + } catch (RepositoryException e) { + // success + } + + // derived class expected -> must fail + xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_IMPL)), true); + umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + try { + um = umc.getUserManager(UserPerWorkspaceUserManager.class, new Class[] { + SessionImpl.class, String.class}, superuser, "admin"); + fail("UserManagerImpl is not assignable from derived class"); + } catch (ConfigurationException e) { + // success + } + + // passing invalid parameter types + xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_IMPL)), true); + umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + try { + um = umc.getUserManager(UserManagerImpl.class, new Class[] { + Session.class}, superuser, "admin"); + fail("Invalid parameter types -> must fail."); + } catch (ConfigurationException e) { + // success + } + + // passing wrong arguments + xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_IMPL)), true); + umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + try { + um = umc.getUserManager(UserManagerImpl.class, new Class[] { + SessionImpl.class, String.class}, superuser, 21); + fail("Invalid init args -> must fail."); + } catch (ConfigurationException e) { + // success + } + + xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_DERIVED)), true); + umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + // assignable from defines base class + um = umc.getUserManager(UserManagerImpl.class, new Class[] { + SessionImpl.class, String.class}, superuser, "admin"); + assertNotNull(um); + assertTrue(um instanceof UserPerWorkspaceUserManager); + // but: configured class creates a umgr that requires session.save + assertTrue(um.isAutoSave()); + // changing autosave behavior must succeed. + um.autoSave(false); + + // test authorizable-action configuration + xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_WITH_ACTIONS)), true); + umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + AuthorizableAction[] actions = umc.getAuthorizableActions(); + assertEquals(2, actions.length); + + xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_WITH_INVALID_ACTIONS)), true); + umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + try { + actions = umc.getAuthorizableActions(); + fail("Invalid configuration - must fail"); + } catch (ConfigurationException e) { + // success + } + + xml = parseXML(new InputSource(new StringReader(USER_MANAGER_CONFIG_WITH_INVALID_ACTIONS_2)), true); + umc = parser.parseSecurityConfig(xml).getSecurityManagerConfig().getUserManagerConfig(); + try { + actions = umc.getAuthorizableActions(); + fail("Invalid configuration - must fail"); + } catch (ConfigurationException e) { + // success + } + } + + /** + * + * @throws Exception + */ + public void testPrincipalProviderConfig() throws Exception { + PrincipalProviderRegistry ppr = new ProviderRegistryImpl(null); + + // standard config + Element xml = parseXML(new InputSource(new StringReader(PRINCIPAL_PROVIDER_CONFIG)), true); + LoginModuleConfig lmc = parser.parseSecurityConfig(xml).getLoginModuleConfig(); + PrincipalProvider pp = ppr.registerProvider(lmc.getParameters()); + assertEquals(pp, ppr.getProvider(pp.getClass().getName())); + assertEquals("org.apache.jackrabbit.core.security.principal.FallbackPrincipalProvider", pp.getClass().getName()); + + // config specifying an extra name + xml = parseXML(new InputSource(new StringReader(PRINCIPAL_PROVIDER_CONFIG1)), true); + lmc = parser.parseSecurityConfig(xml).getLoginModuleConfig(); + pp = ppr.registerProvider(lmc.getParameters()); + assertEquals(pp, ppr.getProvider("test")); + assertEquals("org.apache.jackrabbit.core.security.principal.FallbackPrincipalProvider", pp.getClass().getName()); + + // use alternative class config + xml = parseXML(new InputSource(new StringReader(PRINCIPAL_PROVIDER_CONFIG2)), true); + lmc = parser.parseSecurityConfig(xml).getLoginModuleConfig(); + pp = ppr.registerProvider(lmc.getParameters()); + assertEquals(pp, ppr.getProvider("test2")); + assertEquals("org.apache.jackrabbit.core.security.principal.FallbackPrincipalProvider", pp.getClass().getName()); + + // all 3 providers must be registered despite the fact the all configs + // specify the same provider class + assertEquals(3, ppr.getProviders().length); + + } + + public void testInvalidConfig() { + List invalid = new ArrayList(); + invalid.add(new InputSource(new StringReader(INVALID_CONFIG_1))); + invalid.add(new InputSource(new StringReader(INVALID_CONFIG_2))); + invalid.add(new InputSource(new StringReader(INVALID_CONFIG_3))); + + for (Object anInvalid : invalid) { + try { + Element xml = parseXML((InputSource) anInvalid, false); + parser.parseSecurityConfig(xml); + fail("Invalid config -> should fail."); + } catch (ConfigurationException e) { + // ok + } + } + } + + private static Element parseXML(InputSource xml, boolean validate) throws ConfigurationException { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(validate); + DocumentBuilder builder = factory.newDocumentBuilder(); + if (validate) { + builder.setErrorHandler(new ConfigurationErrorHandler()); + } + builder.setEntityResolver(ConfigurationEntityResolver.INSTANCE); + Document document = builder.parse(xml); + return document.getDocumentElement(); + } catch (ParserConfigurationException e) { + throw new ConfigurationException("Unable to create configuration XML parser", e); + } catch (SAXParseException e) { + throw new ConfigurationException("Configuration file syntax error. (Line: " + e.getLineNumber() + " Column: " + e.getColumnNumber() + ")", e); + } catch (SAXException e) { + throw new ConfigurationException("Configuration file syntax error. ", e); + } catch (IOException e) { + throw new ConfigurationException("Configuration file could not be read.", e); + } + } + + private static final String CONFIG_1 = + " " + + " " + + " " + + " "; + + private static final String CONFIG_2 = + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " \n" + + " "; + + private static final String CONFIG_3 = + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " \n" + + " "; + + private static final String INVALID_CONFIG_1 = + " " + + " " + + " "; + + private static final String INVALID_CONFIG_2 = + " " + + " " + + " " + + " "; + private static final String INVALID_CONFIG_3 = + " " + + " " + + " "; + + private static final String USER_MANAGER_CONFIG_INVALID = + " " + + " " + + " " + + " " + + " "; + + private static final String USER_MANAGER_CONFIG_IMPL = + " " + + " " + + " " + + " " + + " "; + + private static final String USER_MANAGER_CONFIG_DERIVED = + " " + + " " + + " " + + " " + + " "; + + private static final String USER_MANAGER_CONFIG_WITH_ACTIONS = + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " "; + + private static final String USER_MANAGER_CONFIG_WITH_INVALID_ACTIONS = + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " "; + + private static final String USER_MANAGER_CONFIG_WITH_INVALID_ACTIONS_2 = + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " "; + + private static final String PRINCIPAL_PROVIDER_CONFIG = + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " \n" + + " "; + + private static final String PRINCIPAL_PROVIDER_CONFIG1 = + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " \n" + + " "; + + private static final String PRINCIPAL_PROVIDER_CONFIG2 = + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " \n" + + " "; +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/TestAll.java new file mode 100644 index 00000000000..27bee4dd4b3 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/TestAll.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.core.config; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Collects all test classes setting up test data in the workspace. This test + * data is specific to Jackrabbit and will probably not work in other + * implementations. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("Setup data for tests"); + + suite.addTestSuite(RepositoryConfigTest.class); + suite.addTestSuite(WorkspaceConfigTest.class); + suite.addTestSuite(DataSourceConfigTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/WorkspaceConfigTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/WorkspaceConfigTest.java new file mode 100644 index 00000000000..9e99d43dd49 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/config/WorkspaceConfigTest.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.core.config; + +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.user.UserImporter; +import org.apache.jackrabbit.core.xml.ProtectedItemImporter; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.List; +import java.util.Properties; + +import junit.framework.TestCase; + +/** + * Test cases for workspace configuration handling. + */ +public class WorkspaceConfigTest extends TestCase { + + private RepositoryConfigurationParser parser; + + protected void setUp() { + Properties variables = new Properties(); + variables.setProperty("wsp.home", "target"); + parser = new RepositoryConfigurationParser(variables); + } + + /** + * Test that a standard workspace configuration file is + * correctly parsed. + * + * @throws Exception on errors + */ + public void testWorkspaceXml() throws Exception { + InputStream xml = getClass().getClassLoader().getResourceAsStream( + "org/apache/jackrabbit/core/config/workspace.xml"); + WorkspaceConfig config = + parser.parseWorkspaceConfig(new InputSource(xml)); + + assertEquals("target", config.getHomeDir()); + assertEquals("default", config.getName()); + + PersistenceManagerConfig pmc = config.getPersistenceManagerConfig(); + assertEquals( + "org.apache.jackrabbit.core.persistence.obj.ObjectPersistenceManager", + pmc.getClassName()); + assertTrue(pmc.getParameters().isEmpty()); + + assertTrue(config.isSearchEnabled()); + + WorkspaceSecurityConfig ws = config.getSecurityConfig(); + if (ws != null) { + BeanConfig ppfConfig = ws.getAccessControlProviderConfig(); + if (ppfConfig != null) { + ppfConfig.newInstance(AccessControlProvider.class); + } + } + } + + public void testImportConfig() throws Exception { + // XML_1 --------------------------------------------------------------- + Element xml = parseXML(new InputSource(new StringReader(XML_1)), true); + ImportConfig config = parser.parseImportConfig(xml); + + List ln = config.getProtectedItemImporters(); + assertEquals(2, ln.size()); + + // XML_2 --------------------------------------------------------------- + xml = parseXML(new InputSource(new StringReader(XML_2)), true); + config = parser.parseImportConfig(xml); + + ln = config.getProtectedItemImporters(); + assertTrue(ln.isEmpty()); + + // XML_3 --------------------------------------------------------------- + xml = parseXML(new InputSource(new StringReader(XML_3)), true); + config = parser.parseImportConfig(xml); + + ln = config.getProtectedItemImporters(); + assertEquals(4, ln.size()); + + // XML_4 --------------------------------------------------------------- + xml = parseXML(new InputSource(new StringReader(XML_4)), true); + config = parser.parseImportConfig(xml); + + ln = config.getProtectedItemImporters(); + assertEquals(2, ln.size()); + + // XML_5 --------------------------------------------------------------- + xml = parseXML(new InputSource(new StringReader(XML_5)), true); + config = parser.parseImportConfig(xml); + + List lp = config.getProtectedItemImporters(); + assertEquals(1, lp.size()); + assertTrue(lp.get(0) instanceof UserImporter); + assertEquals(UserImporter.ImportBehavior.NAME_BESTEFFORT, ((UserImporter)lp.get(0)).getImportBehavior()); + } + + private static Element parseXML(InputSource xml, boolean validate) throws ConfigurationException { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(validate); + DocumentBuilder builder = factory.newDocumentBuilder(); + if (validate) { + builder.setErrorHandler(new ConfigurationErrorHandler()); + } + builder.setEntityResolver(ConfigurationEntityResolver.INSTANCE); + Document document = builder.parse(xml); + return document.getDocumentElement(); + } catch (ParserConfigurationException e) { + throw new ConfigurationException("Unable to create configuration XML parser", e); + } catch (SAXParseException e) { + throw new ConfigurationException("Configuration file syntax error. (Line: " + e.getLineNumber() + " Column: " + e.getColumnNumber() + ")", e); + } catch (SAXException e) { + throw new ConfigurationException("Configuration file syntax error. ", e); + } catch (IOException e) { + throw new ConfigurationException("Configuration file could not be read.", e); + } + } + + + + private static final String XML_1 = + " \n" + + " \n" + + " \n" + + " "; + + private static final String XML_2 = + " \n" + + " "; + + private static final String XML_3 = + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " "; + + private static final String XML_4 = + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " "; + + private static final String XML_5 = + " \n" + + " " + + " " + + " \n" + + " "; +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ConcurrentGcTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ConcurrentGcTest.java new file mode 100644 index 00000000000..1d947c18fa0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ConcurrentGcTest.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.core.data; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import junit.framework.TestCase; +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.data.db.DbDataStore; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests concurrent garbage collection, see JCR-2026 + */ +public class ConcurrentGcTest extends TestCase { + + static final Logger LOG = LoggerFactory.getLogger(ConcurrentGcTest.class); + + private static final String TEST_DIR = "target/ConcurrentGcTest"; + + protected DataStore store; + private Thread gcLoopThread; + + protected Set ids = Collections.synchronizedSet(new HashSet()); + + protected volatile boolean gcLoopStop; + protected volatile Exception gcException; + + public void setUp() throws IOException { + deleteAll(); + } + + public void tearDown() throws IOException { + deleteAll(); + } + + private void deleteAll() throws IOException { + FileUtils.deleteDirectory(new File(TEST_DIR)); + } + + public void testDatabases() throws Exception { +// doTestDatabase( +// "org.h2.Driver", +// "jdbc:h2:" + TEST_DIR + "/db", +// "sa", "sa"); + + // not enabled by default + // doTestDatabase( + // "org.postgresql.Driver", + // "jdbc:postgresql:test", + // "sa", "sa"); + + // not enabled by default + // doTestDatabase( + // "com.mysql.jdbc.Driver", + // "jdbc:postgresql:test", + // "sa", "sa"); + + // fails with a deadlock + // doTestDatabase( + // "org.apache.derby.jdbc.EmbeddedDriver", + // "jdbc:derby:" + TEST_DIR + "/db;create=true", + // "sa", "sa"); + } + + private void doTestDatabase(String driver, String url, String user, String password) throws Exception { + ConnectionFactory pool = new ConnectionFactory(); + try { + DbDataStore store = new DbDataStore(); + store.setConnectionFactory(pool); + + ids.clear(); + + store.setDriver(driver); + store.setUrl(url); + store.setUser(user); + store.setPassword(password); + + store.init("target/test-db-datastore"); + store.setMinRecordLength(0); + doTest(store); + } finally { + pool.close(); + } + } + + public void testFile() throws Exception { + FileDataStore store = new FileDataStore(); + store.setPath(TEST_DIR + "/fs"); + store.init(TEST_DIR + "/fs"); + store.setMinRecordLength(0); + doTest(store); + } + + void doTest(DataStore store) throws Exception { + this.store = store; + + Random r = new Random(); + + concurrentGcLoopStart(); + + int len = 100; + if (getTestScale() > 1) { + len = 1000; + } + + for (int i = 0; i < len && gcException == null; i++) { + LOG.info("test " + i); + byte[] data = new byte[3]; + r.nextBytes(data); + DataRecord rec = store.addRecord(new ByteArrayInputStream(data)); + LOG.debug(" added " + rec.getIdentifier()); + if (r.nextBoolean()) { + LOG.debug(" added " + rec.getIdentifier() + " -> keep reference"); + ids.add(rec.getIdentifier()); + store.getRecord(rec.getIdentifier()); + } + if (r.nextInt(100) == 0) { + LOG.debug("clear i: " + i); + ids.clear(); + } + } + concurrentGcLoopStop(); + store.close(); + } + + private void concurrentGcLoopStart() { + gcLoopStop = false; + gcException = null; + + gcLoopThread = new Thread() { + public void run() { + try { + while (!gcLoopStop) { + if (ids.size() > 0) { + // store.clearInUse(); + long now = System.currentTimeMillis(); + LOG.debug("gc now: " + now); + store.updateModifiedDateOnAccess(now); + for (DataIdentifier id : new ArrayList(ids)) { + LOG.debug(" gc touch " + id); + store.getRecord(id); + } + int count = store.deleteAllOlderThan(now); + LOG.debug("gc now: " + now + " done, deleted: " + count); + } + } + } catch (DataStoreException e) { + gcException = e; + } + } + }; + gcLoopThread.start(); + } + + private void concurrentGcLoopStop() throws Exception { + gcLoopStop = true; + gcLoopThread.join(); + if (gcException != null) { + throw gcException; + } + } + + static int getTestScale() { + return Integer.parseInt(System.getProperty("jackrabbit.test.scale", "1")); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ConsistencyCheckerImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ConsistencyCheckerImplTest.java new file mode 100644 index 00000000000..dd06760a1ac --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ConsistencyCheckerImplTest.java @@ -0,0 +1,512 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.cluster.ClusterException; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.cluster.SimpleClusterContext; +import org.apache.jackrabbit.core.cluster.UpdateEventChannel; +import org.apache.jackrabbit.core.cluster.UpdateEventListener; +import org.apache.jackrabbit.core.config.ClusterConfig; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.journal.Journal; +import org.apache.jackrabbit.core.journal.JournalFactory; +import org.apache.jackrabbit.core.journal.MemoryJournal; +import org.apache.jackrabbit.core.journal.MemoryJournal.MemoryRecord; +import org.apache.jackrabbit.core.observation.EventState; +import org.apache.jackrabbit.core.persistence.bundle.AbstractBundlePersistenceManager; +import org.apache.jackrabbit.core.persistence.bundle.ConsistencyCheckerImpl; +import org.apache.jackrabbit.core.persistence.check.ReportItem; +import org.apache.jackrabbit.core.persistence.util.BLOBStore; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemStateException; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import junit.framework.TestCase; + +public class ConsistencyCheckerImplTest extends TestCase { + + private static final NameFactory nameFactory = NameFactoryImpl.getInstance(); + + /** Default sync delay: 5 seconds. */ + private static final long SYNC_DELAY = 5000; + + private List records = new ArrayList(); + + private ClusterNode master; + private ClusterNode slave; + + @Override + public void setUp() throws Exception { + super.setUp(); + master = createClusterNode("master"); + master.start(); + + slave = createClusterNode("slave"); + slave.start(); + } + + + // Abandoned nodes are nodes that have a link to a parent but that + // parent does not have a link back to the child + public void testFixAbandonedNode() throws RepositoryException, ClusterException { + NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); + NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); + + // node2 has a reference to node 1 as its parent, but node 1 doesn't have + // a corresponding child node entry + bundle2.setParentId(bundle1.getId()); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2)); + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, master.createUpdateChannel("default")); + + // set up cluster event update listener + final TestUpdateEventListener listener = new TestUpdateEventListener(); + final UpdateEventChannel slaveEventChannel = slave.createUpdateChannel("default"); + slaveEventChannel.setListener(listener); + + checker.check(null, false); + + Set reportItems = checker.getReport().getItems(); + assertEquals(1, reportItems.size()); + ReportItem reportItem = reportItems.iterator().next(); + assertEquals(ReportItem.Type.ABANDONED, reportItem.getType()); + assertEquals(bundle2.getId().toString(), reportItem.getNodeId()); + + checker.repair(); + + // node1 should now have a child node entry for node2 + bundle1 = pm.loadBundle(bundle1.getId()); + assertEquals(1, bundle1.getChildNodeEntries().size()); + assertEquals(bundle2.getId(), bundle1.getChildNodeEntries().get(0).getId()); + + slave.sync(); + + // verify events were correctly broadcast to cluster + assertNotNull("Cluster node did not receive update event", listener.changes); + assertTrue("Expected node1 to be modified", listener.changes.isModified(bundle1.getId())); + } + + public void testDoubleCheckAbandonedNode() throws RepositoryException { + NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); + NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); + + // node2 has a reference to node 1 as its parent, but node 1 doesn't have + // a corresponding child node entry + bundle2.setParentId(bundle1.getId()); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2)); + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); + + checker.check(null, false); + + Set reportItems = checker.getReport().getItems(); + assertEquals(1, reportItems.size()); + ReportItem reportItem = reportItems.iterator().next(); + assertEquals(ReportItem.Type.ABANDONED, reportItem.getType()); + assertEquals(bundle2.getId().toString(), reportItem.getNodeId()); + + checker.doubleCheckErrors(); + + assertFalse("Double check removed valid error", checker.getReport().getItems().isEmpty()); + + // fix the error + bundle1.addChildNodeEntry(nameFactory.create("", "test"), bundle2.getId()); + + checker.doubleCheckErrors(); + + assertTrue("Double check didn't remove invalid error", checker.getReport().getItems().isEmpty()); + } + + /* + * There was a bug where when there were multiple abandoned nodes by the same parent + * only one of them was fixed. Hence this separate test case for this scenario. + */ + public void testFixMultipleAbandonedNodesBySameParent() throws RepositoryException { + NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); + NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); + NodePropBundle bundle3 = new NodePropBundle(new NodeId(1, 0)); + + // node2 and node3 have a reference to node1 as its parent, but node1 doesn't have + // corresponding child node entries + bundle2.setParentId(bundle1.getId()); + bundle3.setParentId(bundle1.getId()); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2, bundle3)); + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); + + checker.check(null, false); + checker.repair(); + + // node1 should now have child node entries for node2 and node3 + bundle1 = pm.loadBundle(bundle1.getId()); + assertEquals(2, bundle1.getChildNodeEntries().size()); + assertEquals(bundle2.getId(), bundle1.getChildNodeEntries().get(0).getId()); + assertEquals(bundle3.getId(), bundle1.getChildNodeEntries().get(1).getId()); + } + + // Orphaned nodes are those nodes who's parent does not exist + public void testAddOrphanedNodeToLostAndFound() throws RepositoryException, ClusterException { + final NodeId lostAndFoundId = new NodeId(0, 0); + NodePropBundle lostAndFound = new NodePropBundle(lostAndFoundId); + // lost and found must be of type nt:unstructured + lostAndFound.setNodeTypeName(NameConstants.NT_UNSTRUCTURED); + + final NodeId orphanedId = new NodeId(0, 1); + NodePropBundle orphaned = new NodePropBundle(orphanedId); + // set non-existent parent node id + orphaned.setParentId(new NodeId(1, 0)); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(lostAndFound, orphaned)); + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, lostAndFoundId.toString(), + master.createUpdateChannel("default")); + + // set up cluster event update listener + final TestUpdateEventListener listener = new TestUpdateEventListener(); + final UpdateEventChannel slaveEventChannel = slave.createUpdateChannel("default"); + slaveEventChannel.setListener(listener); + + checker.check(null, false); + + Set reportItems = checker.getReport().getItems(); + assertEquals(1, reportItems.size()); + ReportItem reportItem = reportItems.iterator().next(); + assertEquals(ReportItem.Type.ORPHANED, reportItem.getType()); + assertEquals(orphanedId.toString(), reportItem.getNodeId()); + + checker.repair(); + + // orphan should have been added to lost+found + lostAndFound = pm.loadBundle(lostAndFoundId); + assertEquals(1, lostAndFound.getChildNodeEntries().size()); + assertEquals(orphanedId, lostAndFound.getChildNodeEntries().get(0).getId()); + + orphaned = pm.loadBundle(orphanedId); + assertEquals(lostAndFoundId, orphaned.getParentId()); + + slave.sync(); + + // verify events were correctly broadcast to cluster + assertNotNull("Cluster node did not receive update event", listener.changes); + assertTrue("Expected lostAndFound to be modified", listener.changes.isModified(lostAndFoundId)); + assertTrue("Expected orphan to be modified", listener.changes.isModified(orphanedId)); + } + + public void testEmptyRepo() throws RepositoryException { + MockPersistenceManager pm = new MockPersistenceManager(Collections.emptyList()); + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); + checker.check(null, false); + } + + public void testDoubleCheckOrphanedNode() throws RepositoryException { + NodePropBundle orphaned = new NodePropBundle(new NodeId(0, 1)); + orphaned.setParentId(new NodeId(1, 0)); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(orphaned)); + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); + + checker.check(null, false); + + Set reportItems = checker.getReport().getItems(); + assertEquals(1, reportItems.size()); + ReportItem reportItem = reportItems.iterator().next(); + assertEquals(ReportItem.Type.ORPHANED, reportItem.getType()); + assertEquals(orphaned.getId().toString(), reportItem.getNodeId()); + + checker.doubleCheckErrors(); + + assertFalse("Double check removed valid error", checker.getReport().getItems().isEmpty()); + + // fix the error + NodePropBundle parent = new NodePropBundle(orphaned.getParentId()); + pm.bundles.put(parent.getId(), parent); + + checker.doubleCheckErrors(); + + assertTrue("Double check didn't remove invalid error", checker.getReport().getItems().isEmpty()); + } + + // Disconnected nodes are those nodes for which there are nodes + // that have the node as its child, but the node itself does not + // have those nodes as its parent + public void testFixDisconnectedNode() throws RepositoryException, ClusterException { + NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); + NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); + NodePropBundle bundle3 = new NodePropBundle(new NodeId(1, 0)); + + // node1 has child node3 + bundle1.addChildNodeEntry(nameFactory.create("", "test"), bundle3.getId()); + // node2 also has child node3 + bundle2.addChildNodeEntry(nameFactory.create("", "test"), bundle3.getId()); + // node3 has node2 as parent + bundle3.setParentId(bundle2.getId()); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2, bundle3)); + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, master.createUpdateChannel("default")); + + // set up cluster event update listener + final TestUpdateEventListener listener = new TestUpdateEventListener(); + final UpdateEventChannel slaveEventChannel = slave.createUpdateChannel("default"); + slaveEventChannel.setListener(listener); + + checker.check(null, false); + + Set reportItems = checker.getReport().getItems(); + assertEquals(1, reportItems.size()); + ReportItem reportItem = reportItems.iterator().next(); + assertEquals(ReportItem.Type.DISCONNECTED, reportItem.getType()); + assertEquals(bundle1.getId().toString(), reportItem.getNodeId()); + + checker.repair(); + + bundle1 = pm.loadBundle(bundle1.getId()); + bundle2 = pm.loadBundle(bundle2.getId()); + bundle3 = pm.loadBundle(bundle3.getId()); + + // node3 should have been removed as child node entry of node1 + assertEquals(0, bundle1.getChildNodeEntries().size()); + + // node3 should still be a child of node2 + assertEquals(1, bundle2.getChildNodeEntries().size()); + assertEquals(bundle2.getId(), bundle3.getParentId()); + + slave.sync(); + + // verify events were correctly broadcast to cluster + assertNotNull("Cluster node did not receive update event", listener.changes); + assertTrue("Expected node1 to be modified", listener.changes.isModified(bundle1.getId())); + } + + public void testDoubleCheckDisonnectedNode() throws RepositoryException { + NodePropBundle bundle1 = new NodePropBundle(new NodeId(0, 0)); + NodePropBundle bundle2 = new NodePropBundle(new NodeId(0, 1)); + NodePropBundle bundle3 = new NodePropBundle(new NodeId(1, 0)); + + // node1 has child node3 + bundle1.addChildNodeEntry(nameFactory.create("", "test"), bundle3.getId()); + // node2 also has child node3 + bundle2.addChildNodeEntry(nameFactory.create("", "test"), bundle3.getId()); + // node3 has node2 as parent + bundle3.setParentId(bundle2.getId()); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle1, bundle2, bundle3)); + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); + + checker.check(null, false); + + Set reportItems = checker.getReport().getItems(); + assertEquals(1, reportItems.size()); + ReportItem reportItem = reportItems.iterator().next(); + assertEquals(ReportItem.Type.DISCONNECTED, reportItem.getType()); + assertEquals(bundle1.getId().toString(), reportItem.getNodeId()); + + checker.doubleCheckErrors(); + + assertFalse("Double check removed valid error", checker.getReport().getItems().isEmpty()); + + // fix the error + bundle1.getChildNodeEntries().remove(0); + + checker.doubleCheckErrors(); + + assertTrue("Double check didn't remove invalid error", checker.getReport().getItems().isEmpty()); + } + + public void testFixMissingNode() throws RepositoryException, ClusterException { + NodePropBundle bundle = new NodePropBundle(new NodeId(0, 0)); + bundle.addChildNodeEntry(nameFactory.create("", "test"), new NodeId(0, 1)); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle)); + + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, master.createUpdateChannel("default")); + + // set up cluster event update listener + final TestUpdateEventListener listener = new TestUpdateEventListener(); + final UpdateEventChannel slaveEventChannel = slave.createUpdateChannel("default"); + slaveEventChannel.setListener(listener); + + checker.check(null, false); + + Set reportItems = checker.getReport().getItems(); + assertEquals(1, reportItems.size()); + ReportItem reportItem = reportItems.iterator().next(); + assertEquals(ReportItem.Type.MISSING, reportItem.getType()); + assertEquals(bundle.getId().toString(), reportItem.getNodeId()); + + checker.repair(); + + // node should have no child no entries + assertTrue(bundle.getChildNodeEntries().isEmpty()); + + slave.sync(); + + // verify events were correctly broadcast to cluster + assertNotNull("Cluster node did not receive update event", listener.changes); + assertTrue("Expected node to be modified", listener.changes.isModified(bundle.getId())); + } + + public void testDoubleCheckMissingNode() throws RepositoryException { + NodePropBundle bundle = new NodePropBundle(new NodeId(0, 0)); + final NodeId childNodeId = new NodeId(0, 1); + bundle.addChildNodeEntry(nameFactory.create("", "test"), childNodeId); + + MockPersistenceManager pm = new MockPersistenceManager(Arrays.asList(bundle)); + + ConsistencyCheckerImpl checker = new ConsistencyCheckerImpl(pm, null, null, null); + + checker.check(null, false); + + Set reportItems = checker.getReport().getItems(); + assertEquals(1, reportItems.size()); + ReportItem reportItem = reportItems.iterator().next(); + assertEquals(ReportItem.Type.MISSING, reportItem.getType()); + assertEquals(bundle.getId().toString(), reportItem.getNodeId()); + + checker.doubleCheckErrors(); + + assertFalse("Double check removed valid error", checker.getReport().getItems().isEmpty()); + + // fix the error + NodePropBundle child = new NodePropBundle(childNodeId); + pm.bundles.put(childNodeId, child); + + checker.doubleCheckErrors(); + + assertTrue("Double check didn't remove invalid error", checker.getReport().getItems().isEmpty()); + + } + + private ClusterNode createClusterNode(String id) throws Exception { + final MemoryJournal journal = new MemoryJournal() { + protected boolean syncAgainOnNewRecords() { + return true; + } + }; + JournalFactory jf = new JournalFactory() { + public Journal getJournal(NamespaceResolver resolver) + throws RepositoryException { + return journal; + } + }; + ClusterConfig cc = new ClusterConfig(id, SYNC_DELAY, jf); + SimpleClusterContext context = new SimpleClusterContext(cc); + + journal.setRepositoryHome(context.getRepositoryHome()); + journal.init(id, context.getNamespaceResolver()); + journal.setRecords(records); + + ClusterNode clusterNode = new ClusterNode(); + clusterNode.init(context); + return clusterNode; + } + + private static class MockPersistenceManager extends AbstractBundlePersistenceManager { + + private Map bundles = new LinkedHashMap(); + + private MockPersistenceManager(List bundles) { + for (NodePropBundle bundle : bundles) { + this.bundles.put(bundle.getId(), bundle); + } + } + + public List getAllNodeIds(final NodeId after, final int maxCount) throws ItemStateException, RepositoryException { + List allNodeIds = new ArrayList(); + boolean add = after == null; + for (NodeId nodeId : bundles.keySet()) { + if (add) { + allNodeIds.add(nodeId); + } + if (!add) { + add = nodeId.equals(after); + } + } + return allNodeIds; + } + + @Override + protected NodePropBundle loadBundle(final NodeId id) { + return bundles.get(id); + } + + @Override + public boolean exists(final NodeId id) { + return bundles.containsKey(id); + } + + @Override + protected void evictBundle(final NodeId id) { + } + + @Override + protected void storeBundle(final NodePropBundle bundle) throws ItemStateException { + bundles.put(bundle.getId(), bundle); + } + + @Override + protected void destroyBundle(final NodePropBundle bundle) throws ItemStateException { + bundles.remove(bundle.getId()); + } + + @Override + protected void destroy(final NodeReferences refs) throws ItemStateException { + } + + @Override + protected void store(final NodeReferences refs) throws ItemStateException { + } + + @Override + protected BLOBStore getBlobStore() { + return null; + } + + public NodeReferences loadReferencesTo(final NodeId id) throws NoSuchItemStateException, ItemStateException { + return null; + } + + public boolean existsReferencesTo(final NodeId targetId) throws ItemStateException { + return false; + } + } + + private static class TestUpdateEventListener implements UpdateEventListener { + + private ChangeLog changes; + + @Override + public void externalUpdate(final ChangeLog changes, final List events, final long timestamp, final String userData) throws RepositoryException { + this.changes = changes; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/CopyValueTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/CopyValueTest.java new file mode 100644 index 00000000000..c253af85824 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/CopyValueTest.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.core.data; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import java.io.ByteArrayInputStream; +import java.util.Random; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.ValueFactory; + +/** + * Tests copying binary values from one node to another. + * See also JCR-1351 + * and JCR-1346 + */ +public class CopyValueTest extends AbstractJCRTest { + + static { + // to test without data store, enable the following line: + // System.setProperty("org.jackrabbit.useDataStore", "false"); + } + + /** + * Tests the Workspace.copy() operation with various lengths. + */ + public void testCopyStream() throws Exception { + doTestCopy(100000); + doTestCopy(0); + doTestCopy(1); + doTestCopy(10); + doTestCopy(100); + doTestCopy(1000); + } + + private void doTestCopy(int length) throws Exception { + Node root = superuser.getRootNode(); + if (root.hasNode("testCopy")) { + root.getNode("testCopy").remove(); + superuser.save(); + } + Node testRoot = root.addNode("testCopy"); + Node n = testRoot.addNode("a"); + superuser.save(); + byte[] data = new byte[length + 1]; + ValueFactory vf = superuser.getValueFactory(); + n.setProperty("data", vf.createBinary(new ByteArrayInputStream(data))); + superuser.save(); + data = new byte[length]; + n.setProperty("data", vf.createBinary(new ByteArrayInputStream(data))); + Property p = testRoot.getNode("a").getProperty("data"); + assertEquals(length, p.getLength()); + superuser.getWorkspace().copy("/testCopy/a", "/testCopy/b"); + assertEquals(length, p.getLength()); + p = testRoot.getNode("b").getProperty("data"); + assertEquals(length + 1, p.getLength()); + testRoot.remove(); + superuser.save(); + } + + /** + * Test random operations: + * create, delete, move, and verify node; save and refresh a session. + * The test always runs the same sequence of operations. + */ + public void testRandomOperations() throws Exception { + Random random = new Random(1); + Node root = superuser.getRootNode(); + if (root.hasNode("testRandom")) { + root.getNode("testRandom").remove(); + superuser.save(); + } + Node testRoot = root.addNode("testRandom"); + int len = 1000; + int[] opCounts = new int[6]; + for (int i = 0; i < len; i++) { + String node1 = "node" + random.nextInt(len / 20); + boolean hasNode1 = testRoot.hasNode(node1); + String node2 = "node" + random.nextInt(len / 20); + boolean hasNode2 = testRoot.hasNode(node2); + int op = random.nextInt(6); + switch (op) { + case 0: { + if (hasNode1) { + log(node1 + " remove"); + testRoot.getNode(node1).remove(); + } + opCounts[op]++; + Node n = testRoot.addNode(node1); + int dataLength = Math.abs(Math.min(10000, (int) (10000 * random + .nextGaussian()))); + byte[] data = new byte[dataLength]; + log(node1 + " add len:" + dataLength); + ValueFactory vf = superuser.getValueFactory(); + n.setProperty("data", vf.createBinary(new ByteArrayInputStream(data))); + n.setProperty("len", dataLength); + break; + } + case 1: + opCounts[op]++; + log("save"); + superuser.save(); + break; + case 2: + opCounts[op]++; + log("refresh"); + superuser.refresh(false); + break; + case 3: + if (hasNode1) { + opCounts[op]++; + log(node1 + " remove"); + testRoot.getNode(node1).remove(); + } + break; + case 4: { + if (hasNode1) { + opCounts[op]++; + Node n = testRoot.getNode(node1); + long dataLength = n.getProperty("len").getLong(); + long l = n.getProperty("data").getLength(); + log(node1 + " verify len: " + dataLength); + assertEquals(dataLength, l); + } + break; + } + case 5: + if (hasNode1 && !hasNode2) { + opCounts[op]++; + log(node1 + " copy " + node2); + // todo: why is save required? + superuser.save(); + superuser.getWorkspace().copy("/testRandom/" + node1, + "/testRandom/" + node2); + } + break; + } + } + superuser.save(); + for (int i = 0; i < opCounts.length; i++) { + log(i + ": " + opCounts[i]); + } + testRoot.remove(); + superuser.save(); + } + + private void log(String s) { + // to log operations, enable the following line: + // System.out.println(s); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DBDataStoreTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DBDataStoreTest.java new file mode 100644 index 00000000000..8152d197afa --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DBDataStoreTest.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.util.Random; +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.data.db.DbDataStore; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Test the Database Data Store. + */ +public class DBDataStoreTest extends JUnitTest { + + private DbDataStore store = new DbDataStore(); + + private byte[] data = new byte[1024]; + + private DataIdentifier identifier; + + protected void setUp() throws Exception { + FileUtils.deleteQuietly(new File("target/test-db-datastore")); + + // Initialize the data store + store.setConnectionFactory(new ConnectionFactory()); + store.setUrl("jdbc:derby:target/test-db-datastore/db;create=true"); + store.setDriver("org.apache.derby.jdbc.EmbeddedDriver"); + store.init("target/test-db-datastore"); + + // Initialize random test data + new Random(1234567890).nextBytes(data); + + // Add a test record + identifier = + store.addRecord(new ByteArrayInputStream(data)).getIdentifier(); + } + + protected void tearDown() { + + try { + store.close(); + } catch (DataStoreException expected) { + // ignore + } + } + + public void testGetRecord() throws Exception { + DataRecord record = store.getRecord(identifier); + assertNotNull(record); + assertEquals(identifier, record.getIdentifier()); + assertEquals(data.length, record.getLength()); + + // read the stream twice to make sure that it can be re-read + for (int i = 0; i < 2; i++) { + InputStream stream = record.getStream(); + try { + assertNotNull(stream); + for (int j = 0; j < data.length; j++) { + assertEquals((data[j]) & 0xff, stream.read()); + } + assertEquals(-1, stream.read()); + } finally { + stream.close(); + } + } + } + + public void testDbInputStreamReset() throws Exception { + DataRecord record = store.getRecord(identifier); + InputStream in = record.getStream(); + try { + // test whether mark and reset works + assertTrue(in.markSupported()); + in.mark(data.length); + while (-1 != in.read()) { + // loop + } + assertTrue(in.markSupported()); + try { + in.reset(); + } catch (Exception e) { + fail("Unexpected exception while resetting input stream: " + e.getMessage()); + } + + // test integrity of replayed bytes + byte[] replayedBytes = new byte[data.length]; + int length = in.read(replayedBytes); + assertEquals(length, data.length); + + for (int i = 0; i < data.length; i++) { + log.append(i + " data: " + data[i] + " replayed: " + replayedBytes[i] + "\n"); + assertEquals(data[i], replayedBytes[i]); + } + + assertEquals(-1, in.read()); + + + } finally { + in.close(); + log.flush(); + } + } + + /* + public void testDbInputStreamMarkTwice() throws Exception { + DataRecord record = store.getRecord(identifier); + InputStream in = record.getStream(); + try { + // test whether mark and reset works + assertTrue(in instanceof DbInputStream); + assertTrue(in.markSupported()); + in.mark(data.length); + + // read first 100 bytes + for (int i = 0; i < 100; i++) { + in.read(); + } + + in.mark(data.length - 100); + + // read next 150 bytes + for (int i = 0; i < 150; i++) { + in.read(); + } + + try { + log.append("attempting a reset()\n"); + in.reset(); + } catch (Exception e) { + fail("Unexpected exception while resetting input stream: " + e.getMessage()); + } + + // test integrity of replayed bytes + byte[] replayedBytes = new byte[data.length]; + int length = in.read(replayedBytes); + assertEquals(length, data.length - 100 - 150); + + for (int i = 0; i < length; i++) { + assertEquals(data[i + 100 + 150] & 0xff, replayedBytes[i] & 0xff); + } + + assertTrue(-1 == in.read()); + } finally { + in.close(); + } + } + */ + + public void testConcurrentRead() throws Exception { + InputStream[] streams = new InputStream[10]; + + // retrieve many copies of the same record + for (int i = 0; i < streams.length; i++) { + streams[i] = store.getRecord(identifier).getStream(); + } + + // verify the contents of all the streams, reading them in parallel + for (int i = 0; i < data.length; i++) { + for (int j = 0; j < streams.length; j++) { + assertEquals((data[i]) & 0xff, streams[j].read()); + } + } + + // close all streams + for (int i = 0; i < streams.length; i++) { + assertEquals(-1, streams[i].read()); + streams[i].close(); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DataStoreAPITest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DataStoreAPITest.java new file mode 100644 index 00000000000..c501ec5fcd8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DataStoreAPITest.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.core.data; + +import junit.framework.TestCase; +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.api.JackrabbitRepositoryFactory; +import org.apache.jackrabbit.api.management.DataStoreGarbageCollector; +import org.apache.jackrabbit.api.management.RepositoryManager; +import org.apache.jackrabbit.core.RepositoryFactoryImpl; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.io.File; +import java.io.IOException; +import java.util.Properties; + +/** + * Test data store garbage collection as described in the wiki at + * http://wiki.apache.org/jackrabbit/DataStore + */ +public class DataStoreAPITest extends TestCase { + + private static final String TEST_DIR = "target/repository-datastore-test"; + + /** + * Test data store garbage collection. + */ + public void testDataStoreGarbageCollection() throws RepositoryException { + JackrabbitRepositoryFactory rf = new RepositoryFactoryImpl(); + Properties prop = new Properties(); + prop.setProperty("org.apache.jackrabbit.repository.home", TEST_DIR); + prop.setProperty("org.apache.jackrabbit.repository.conf", TEST_DIR + "/repository.xml"); + JackrabbitRepository rep = (JackrabbitRepository) rf.getRepository(prop); + RepositoryManager rm = rf.getRepositoryManager(rep); + + // need to login to start the repository + Session session = rep.login(); + + DataStoreGarbageCollector gc = rm.createDataStoreGarbageCollector(); + try { + gc.mark(); + gc.sweep(); + } finally { + gc.close(); + } + + session.logout(); + rm.stop(); + } + + public void tearDown() throws IOException { + setUp(); + } + + public void setUp() throws IOException { + FileUtils.deleteDirectory(new File(TEST_DIR)); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DataStoreTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DataStoreTest.java new file mode 100644 index 00000000000..d50c55cde98 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/DataStoreTest.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.data.db.DbDataStore; +import org.apache.jackrabbit.test.JUnitTest; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Random; + +/** + * Tests for the data store. + * Both file data store and database data store are tested, + * with single threaded and multi-threaded tests. + */ +public class DataStoreTest extends JUnitTest { + + private static final boolean TEST_DATABASE = false; + private File testDir = new File(System.getProperty("java.io.tmpdir"), "dataStore"); + + public void setUp() { + testDir.mkdirs(); + } + + public void tearDown() throws IOException { + FileUtils.deleteDirectory(testDir); + } + + public void test() throws Exception { + try { + + if (TEST_DATABASE) { + DbDataStore dds = new DbDataStore(); + String dbPath = (testDir + "/db").replace('\\', '/'); + + // 3 sec + String url = "jdbc:h2:mem:" + dbPath + "/db"; + + // 4 sec + // String url = "jdbc:h2:" + dbPath + "/db"; + + // 26 sec + // String url = "jdbc:derby:" + dbPath + "/db"; + + new File(dbPath).mkdirs(); + dds.setUrl(url + ";create=true"); + dds.setUser("sa"); + dds.setPassword("sa"); + dds.setCopyWhenReading(false); + dds.init(dbPath); + // doTest(dds, 0); + doTestMultiThreaded(dds, 4); + dds.close(); + shutdownDatabase(url); + + FileUtils.deleteDirectory(testDir); + new File(dbPath).mkdirs(); + dds = new DbDataStore(); + dds.setUrl(url + ";create=true"); + dds.setUser("sa"); + dds.setPassword("sa"); + dds.setCopyWhenReading(true); + dds.init(dbPath); + // doTest(dds, 0); + doTestMultiThreaded(dds, 4); + dds.close(); + shutdownDatabase(url); + } + + FileDataStore fds = new FileDataStore(); + fds.init(testDir + "/file"); + doTest(fds, 0); + // doTestMultiThreaded(fds, 4); + fds.close(); + + } catch (Throwable t) { + t.printStackTrace(); + throw new Error(t); + } + } + + public static void main(String... args) throws NoSuchAlgorithmException { + // create and print a "directory-collision", that is, two byte arrays + // where the hash starts with the same bytes + // those values can be used for testDeleteRecordWithParentCollision + HashMap map = new HashMap(); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + ByteBuffer input = ByteBuffer.allocate(8); + byte[] array = input.array(); + for(long x = 0;; x++) { + input.putLong(x).flip(); + long h = ByteBuffer.wrap(digest.digest(array)).getLong(); + Long old = map.put(h & 0xffffffffff000000L, x); + if (old != null) { + System.out.println(Long.toHexString(old) + " " + Long.toHexString(x)); + break; + } + } + } + + public void testDeleteRecordWithParentCollision() throws Exception { + FileDataStore fds = new FileDataStore(); + fds.init(testDir + "/fileDeleteCollision"); + + ByteArrayInputStream c1 = new ByteArrayInputStream(ByteBuffer + .allocate(8).putLong(0x181c7).array()); + ByteArrayInputStream c2 = new ByteArrayInputStream(ByteBuffer + .allocate(8).putLong(0x11fd78).array()); + DataRecord d1 = fds.addRecord(c1); + DataRecord d2 = fds.addRecord(c2); + fds.deleteRecord(d1.getIdentifier()); + DataRecord testRecord = fds.getRecordIfStored(d2.getIdentifier()); + + assertNotNull(testRecord); + assertEquals(d2.getIdentifier(), testRecord.getIdentifier()); + // Check the presence of the parent directory (relies on internal details of the FileDataStore) + File parentDirD1 = new File( + fds.getPath() + System.getProperty("file.separator") + d1.getIdentifier().toString().substring(0, 2)); + assertTrue(parentDirD1.exists()); + } + + public void testDeleteRecordWithoutParentCollision() throws Exception { + FileDataStore fds = new FileDataStore(); + fds.init(testDir + "/fileDelete"); + + String c1 = "idhfigjhehgkdfgk"; + String c2 = "02c60cb75083ceef"; + DataRecord d1 = fds.addRecord(IOUtils.toInputStream(c1)); + DataRecord d2 = fds.addRecord(IOUtils.toInputStream(c2)); + fds.deleteRecord(d1.getIdentifier()); + DataRecord testRecord = fds.getRecordIfStored(d2.getIdentifier()); + + assertNotNull(testRecord); + assertEquals(d2.getIdentifier(), testRecord.getIdentifier()); + // Check the absence of the parent directory (relies on internal details of the FileDataStore) + File parentDirD1 = new File( + fds.getPath() + System.getProperty("file.separator") + d1.getIdentifier().toString().substring(0, 2)); + assertFalse(parentDirD1.exists()); + } + + public void testReference() throws Exception { + byte[] data = new byte[12345]; + new Random(12345).nextBytes(data); + String reference; + + FileDataStore store = new FileDataStore(); + store.init(testDir + "/reference"); + try { + DataRecord record = store.addRecord(new ByteArrayInputStream(data)); + reference = record.getReference(); + + assertReference(data, reference, store); + } finally { + store.close(); + } + + store = new FileDataStore(); + store.init(testDir + "/reference"); + try { + assertReference(data, reference, store); + } finally { + store.close(); + } + } + + private void assertReference( + byte[] expected, String reference, DataStore store) + throws Exception { + DataRecord record = store.getRecordFromReference(reference); + assertNotNull(record); + assertEquals(expected.length, record.getLength()); + + InputStream stream = record.getStream(); + try { + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i] & 0xff, stream.read()); + } + assertEquals(-1, stream.read()); + } finally { + stream.close(); + } + } + + private void shutdownDatabase(String url) { + if (url.startsWith("jdbc:derby:") || url.startsWith("jdbc:hsqldb:")) { + try { + DriverManager.getConnection(url + ";shutdown=true"); + } catch (SQLException e) { + // ignore + } + } + } + + private void doTestMultiThreaded(final DataStore ds, int threadCount) throws Exception { + final Exception[] exception = new Exception[1]; + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + final int x = i; + Thread t = new Thread() { + public void run() { + try { + doTest(ds, x); + } catch (Exception e) { + exception[0] = e; + } + } + }; + threads[i] = t; + t.start(); + } + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + if (exception[0] != null) { + throw exception[0]; + } + } + + void doTest(DataStore ds, int offset) throws Exception { + ArrayList list = new ArrayList(); + HashMap map = new HashMap(); + for (int i = 0; i < 100; i++) { + int size = 100 + i * 10; + RandomInputStream in = new RandomInputStream(size + offset, size); + DataRecord rec = ds.addRecord(in); + list.add(rec); + map.put(rec, new Integer(size)); + } + Random random = new Random(1); + for (int i = 0; i < list.size(); i++) { + int pos = random.nextInt(list.size()); + DataRecord rec = list.get(pos); + int size = map.get(rec); + rec = ds.getRecord(rec.getIdentifier()); + assertEquals(size, rec.getLength()); + InputStream in = rec.getStream(); + RandomInputStream expected = new RandomInputStream(size + offset, size); + if (random.nextBoolean()) { + in = readInputStreamRandomly(in, random); + } + assertEquals(expected, in); + in.close(); + } + } + + InputStream readInputStreamRandomly(InputStream in, Random random) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[8000]; + while (true) { + if (random.nextBoolean()) { + int x = in.read(); + if (x < 0) { + break; + } + out.write(x); + } else { + if (random.nextBoolean()) { + int l = in.read(buffer); + if (l < 0) { + break; + } + out.write(buffer, 0, l); + } else { + int offset = random.nextInt(buffer.length / 2); + int len = random.nextInt(buffer.length / 2); + int l = in.read(buffer, offset, len); + if (l < 0) { + break; + } + out.write(buffer, offset, l); + } + } + } + in.close(); + return new ByteArrayInputStream(out.toByteArray()); + } + + void assertEquals(InputStream a, InputStream b) throws IOException { + while (true) { + int ai = a.read(); + int bi = b.read(); + assertEquals(ai, bi); + if (ai < 0) { + break; + } + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ExportImportTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ExportImportTest.java new file mode 100644 index 00000000000..56d186ad6f7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/ExportImportTest.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +/** + * Test importing and exporting large binary and text objects. + */ +public class ExportImportTest extends AbstractJCRTest { + + /** + * Test importing a large text property with Unicode characters larger than 255. + */ + public void testExportImportText() throws RepositoryException { + doTestExportImportLargeText("Hello \t\r\n!".toCharArray()); + doTestExportImportLargeText("World\f\f\f.".toCharArray()); + doTestExportImportLargeText("Hello\t\n\n.\n".toCharArray()); + doTestExportImportLargeRandomText(100); + doTestExportImportLargeRandomText(10000); + doTestExportImportLargeRandomText(100000); + } + + private void doTestExportImportLargeRandomText(int len) throws RepositoryException { + char[] chars = new char[len]; + Random random = new Random(1); + // The UCS code values 0xd800-0xdfff (UTF-16 surrogates) + // as well as 0xfffe and 0xffff (UCS non-characters) + // should not appear in conforming UTF-8 streams. + // (String.getBytes("UTF-8") only returns 1 byte for 0xd800-0xdfff) + for (int i = 0; i < chars.length; i++) { + chars[i] = (char) random.nextInt(0xd000); + } + doTestExportImportLargeText(chars); + } + + private void doTestExportImportLargeText(char[] chars) throws RepositoryException { + Session session = getHelper().getReadWriteSession(); + try { + Node root = session.getRootNode(); + clean(root); + Node test = root.addNode("testText"); + session.save(); + String s = new String(chars); + test.setProperty("text", s); + session.save(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + session.exportSystemView("/testText", out, false, false); + byte[] output = out.toByteArray(); + Node test2 = root.addNode("testText2"); + Node test3 = root.addNode("testText3"); + session.save(); + session.importXML("/testText2", new ByteArrayInputStream(output), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + session.save(); + session.getWorkspace().importXML("/testText3", new ByteArrayInputStream(output), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + test2 = root.getNode("testText2"); + test2 = test2.getNode("testText"); + test3 = root.getNode("testText3"); + test3 = test3.getNode("testText"); + String s2 = test2.getProperty("text").getString(); + String s3 = test3.getProperty("text").getString(); + assertEquals(s.length(), s2.length()); + assertEquals(s.length(), s3.length()); + assertEquals(s, s2); + assertEquals(s, s3); + clean(root); + } catch (Exception e) { + e.printStackTrace(); + assertFalse(e.getMessage(), true); + } finally { + session.logout(); + } + } + + + /** + * Test a node type with a binary default value + * @throws RepositoryException + */ + public void testExportImportBinary() throws RepositoryException { + doTestExportImportBinary(0); + doTestExportImportBinary(10); + doTestExportImportBinary(10000); + doTestExportImportBinary(100000); + } + + private void doTestExportImportBinary(int len) throws RepositoryException { + Session session = getHelper().getReadWriteSession(); + try { + Node root = session.getRootNode(); + clean(root); + Node test = root.addNode("testBinary"); + session.save(); + byte[] data = new byte[len]; + Random random = new Random(1); + random.nextBytes(data); + ValueFactory vf = session.getValueFactory(); + test.setProperty("data", vf.createBinary(new ByteArrayInputStream(data))); + session.save(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + session.exportSystemView("/testBinary", out, false, false); + byte[] output = out.toByteArray(); + Node test2 = root.addNode("testBinary2"); + Node test3 = root.addNode("testBinary3"); + session.save(); + session.importXML("/testBinary2", new ByteArrayInputStream(output), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + session.save(); + session.getWorkspace().importXML("/testBinary3", new ByteArrayInputStream(output), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + test2 = root.getNode("testBinary2"); + test2 = test2.getNode("testBinary"); + test3 = root.getNode("testBinary3"); + test3 = test3.getNode("testBinary"); + byte[] data2 = readFromStream(test2.getProperty("data").getBinary().getStream()); + byte[] data3 = readFromStream(test3.getProperty("data").getBinary().getStream()); + assertEquals(data.length, data2.length); + assertEquals(data.length, data3.length); + for (int i = 0; i < len; i++) { + assertEquals(data[i], data2[i]); + assertEquals(data[i], data3[i]); + } + clean(root); + } catch (Exception e) { + e.printStackTrace(); + assertFalse(e.getMessage(), true); + } finally { + session.logout(); + } + } + + private byte[] readFromStream(InputStream in) throws IOException { + ByteArrayOutputStream out2 = new ByteArrayOutputStream(); + while (true) { + int x = in.read(); + if (x < 0) { + break; + } + out2.write(x); + } + return out2.toByteArray(); + } + + private void clean(Node root) throws RepositoryException { + while (root.hasNode("testBinary")) { + root.getNode("testBinary").remove(); + } + while (root.hasNode("testBinary2")) { + root.getNode("testBinary2").remove(); + } + while (root.hasNode("testBinary3")) { + root.getNode("testBinary3").remove(); + } + while (root.hasNode("testText")) { + root.getNode("testText").remove(); + } + while (root.hasNode("testText2")) { + root.getNode("testText2").remove(); + } + while (root.hasNode("testText3")) { + root.getNode("testText3").remove(); + } + root.getSession().save(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCConcurrentTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCConcurrentTest.java new file mode 100644 index 00000000000..c0775660118 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCConcurrentTest.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.core.data; + +import org.apache.jackrabbit.api.management.DataStoreGarbageCollector; +import org.apache.jackrabbit.api.management.MarkEventListener; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +/** + * Test case for concurrent garbage collection + */ +public class GCConcurrentTest extends AbstractJCRTest { + + /** logger instance */ + private static final Logger LOG = LoggerFactory.getLogger(GCConcurrentTest.class); + + public void testConcurrentDelete() throws Exception { + Node root = testRootNode; + Session session = root.getSession(); + + final String testNodeName = "testConcurrentDelete"; + node(root, testNodeName); + session.save(); + DataStoreGarbageCollector gc = ((SessionImpl) session).createDataStoreGarbageCollector(); + gc.setPersistenceManagerScan(false); + gc.setMarkEventListener(new MarkEventListener() { + public void beforeScanning(Node n) throws RepositoryException { + if (n.getName().equals(testNodeName)) { + n.remove(); + n.getSession().save(); + } + } + + }); + gc.mark(); + gc.close(); + } + + public void testGC() throws Exception { + Node root = testRootNode; + Session session = root.getSession(); + + GCThread gc = new GCThread(session); + Thread gcThread = new Thread(gc, "Datastore Garbage Collector"); + + int len = 10 * getTestScale(); + boolean started = false; + for (int i = 0; i < len; i++) { + if (!started && i > 5 + len / 100) { + started = true; + gcThread.start(); + } + Node n = node(root, "test" + i); + ValueFactory vf = session.getValueFactory(); + n.setProperty("data", vf.createBinary(randomInputStream(i))); + session.save(); + LOG.debug("saved: " + i); + } + Thread.sleep(10); + for (int i = 0; i < len; i++) { + Node n = root.getNode("test" + i); + Property p = n.getProperty("data"); + InputStream in = p.getBinary().getStream(); + InputStream expected = randomInputStream(i); + checkStreams(expected, in); + n.remove(); + LOG.debug("removed: " + i); + session.save(); + } + Thread.sleep(10); + gc.setStop(true); + Thread.sleep(10); + gcThread.join(); + gc.throwException(); + } + + private void checkStreams(InputStream expected, InputStream in) throws IOException { + while (true) { + int e = expected.read(); + int i = in.read(); + if (e < 0 || i < 0) { + if (e >= 0 || i >= 0) { + fail("expected: " + e + " got: " + i); + } + break; + } else { + assertEquals(e, i); + } + } + expected.close(); + in.close(); + } + + static InputStream randomInputStream(long seed) { + byte[] data = new byte[4096]; + new Random(seed).nextBytes(data); + return new ByteArrayInputStream(data); + } + + static Node node(Node n, String x) throws RepositoryException { + return n.hasNode(x) ? n.getNode(x) : n.addNode(x); + } + + static int getTestScale() { + return Integer.parseInt(System.getProperty("jackrabbit.test.scale", "1")); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCEventListenerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCEventListenerTest.java new file mode 100644 index 00000000000..016a51afcf9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCEventListenerTest.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.core.data; + +import org.apache.jackrabbit.api.management.DataStoreGarbageCollector; +import org.apache.jackrabbit.api.management.MarkEventListener; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.gc.GarbageCollector; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.util.Random; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +/** + * Test the DataStore garbage collector. + * This tests that the EventListener is called while scanning the repository. + * + * @author Thomas Mueller + */ +public class GCEventListenerTest extends AbstractJCRTest implements MarkEventListener { + + /** logger instance */ + private static final Logger LOG = LoggerFactory.getLogger(GCEventListenerTest.class); + + private static final String TEST_NODE_NAME = "testGCEventListener"; + + private boolean gotNullNode; + private boolean gotNode; + private int count; + + public void testEventListener() throws Exception { + doTestEventListener(true); + doTestEventListener(false); + } + + private void doTestEventListener(boolean allowPmScan) throws Exception { + Node root = testRootNode; + Session session = root.getSession(); + if (root.hasNode(TEST_NODE_NAME)) { + root.getNode(TEST_NODE_NAME).remove(); + session.save(); + } + Node test = root.addNode(TEST_NODE_NAME); + Random random = new Random(); + byte[] data = new byte[10000]; + for (int i = 0; i < 10; i++) { + Node n = test.addNode("x" + i); + random.nextBytes(data); + ValueFactory vf = session.getValueFactory(); + n.setProperty("data", vf.createBinary(new ByteArrayInputStream(data))); + session.save(); + if (i % 2 == 0) { + n.remove(); + session.save(); + } + } + session.save(); + SessionImpl si = (SessionImpl) session; + DataStoreGarbageCollector gc = si.createDataStoreGarbageCollector(); + DataStore ds = ((GarbageCollector) gc).getDataStore(); + if (ds != null) { + ds.clearInUse(); + boolean pmScan = gc.isPersistenceManagerScan(); + gc.setPersistenceManagerScan(allowPmScan); + gotNullNode = false; + gotNode = false; + gc.setMarkEventListener(this); + gc.mark(); + if (pmScan && allowPmScan) { + assertTrue("PM scan without null Node", gotNullNode); + assertFalse("PM scan, but got a real node", gotNode); + } else { + assertFalse("Not a PM scan - but got a null Node", gotNullNode); + assertTrue("Not a PM scan - without a real node", gotNode); + } + int deleted = gc.sweep(); + LOG.debug("Deleted " + deleted); + assertTrue("Should delete at least one item", deleted >= 0); + gc.close(); + } + } + + public String getNodeName(Node n) throws RepositoryException { + if (n == null) { + gotNullNode = true; + return String.valueOf(count++); + } else { + gotNode = true; + return n.getPath(); + } + } + + public void beforeScanning(Node n) throws RepositoryException { + String s = getNodeName(n); + if (s != null) { + LOG.debug("scanning " + s); + } + } + + public void done() { + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCSubtreeMoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCSubtreeMoveTest.java new file mode 100644 index 00000000000..97a3836a073 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCSubtreeMoveTest.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.Properties; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.ValueFactory; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.api.JackrabbitRepositoryFactory; +import org.apache.jackrabbit.api.management.MarkEventListener; +import org.apache.jackrabbit.core.RepositoryFactoryImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.gc.GarbageCollector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test case for the scenario where the GC thread traverses the workspace and at + * some point, a subtree that the GC thread did not see yet is moved to a location + * that the thread has already traversed. The GC thread should not ignore binaries + * references by this subtree and eventually delete them. + */ +public class GCSubtreeMoveTest extends TestCase { + + private static final Logger logger = LoggerFactory.getLogger(GCSubtreeMoveTest.class); + + private String testDirectory; + private JackrabbitRepository repository; + private Session sessionGarbageCollector; + private Session sessionMover; + + public void setUp() throws IOException { + testDirectory = "target/" + getClass().getSimpleName() + "/" + getName(); + FileUtils.deleteDirectory(new File(testDirectory)); + } + + public void tearDown() throws IOException { + sessionGarbageCollector.logout(); + sessionMover.logout(); + repository.shutdown(); + + repository = null; + sessionGarbageCollector = null; + sessionMover = null; + + FileUtils.deleteDirectory(new File(testDirectory)); + testDirectory = null; + } + + public void test() { + setupRepository(); + + GarbageCollector garbageCollector = setupGarbageCollector(); + // To make sure even listener for NODE_ADDED is registered in GC. + garbageCollector.setPersistenceManagerScan(false); + + assertEquals(0, getBinaryCount(garbageCollector)); + setupNodes(); + assertEquals(1, getBinaryCount(garbageCollector)); + garbageCollector.getDataStore().clearInUse(); + + garbageCollector.setMarkEventListener(new MarkEventListener() { + + public void beforeScanning(Node node) throws RepositoryException { + String path = node.getPath(); + if (path.startsWith("/node")) { + log("Traversing: " + node.getPath()); + } + + if ("/node1".equals(node.getPath())) { + String from = "/node2/node3"; + String to = "/node0/node3"; + log("Moving " + from + " -> " + to); + sessionMover.move(from, to); + sessionMover.save(); + sleepForFile(); + } + } + }); + + try { + garbageCollector.getDataStore().clearInUse(); + garbageCollector.mark(); + garbageCollector.stopScan(); + sleepForFile(); + int numberOfDeleted = garbageCollector.sweep(); + log("Number of deleted: " + numberOfDeleted); + // Binary data should still be there. + assertEquals(1, getBinaryCount(garbageCollector)); + } catch (RepositoryException e) { + e.printStackTrace(); + failWithException(e); + } finally { + garbageCollector.close(); + } + } + + private void setupNodes() { + try { + Node rootNode = sessionMover.getRootNode(); + rootNode.addNode("node0"); + rootNode.addNode("node1"); + Node node2 = rootNode.addNode("node2"); + Node node3 = node2.addNode("node3"); + Node nodeWithBinary = node3.addNode("node-with-binary"); + ValueFactory vf = sessionGarbageCollector.getValueFactory(); + nodeWithBinary.setProperty("prop", vf.createBinary(new RandomInputStream(10, 1000))); + sessionMover.save(); + sleepForFile(); + } catch (RepositoryException e) { + failWithException(e); + } + } + + private void sleepForFile() { + // Make sure the file is old (access time resolution is 2 seconds) + try { + Thread.sleep(2200); + } catch (InterruptedException ignore) { + } + } + + private void setupRepository() { + JackrabbitRepositoryFactory repositoryFactory = new RepositoryFactoryImpl(); + createRepository(repositoryFactory); + login(); + } + + private void createRepository(JackrabbitRepositoryFactory repositoryFactory) { + Properties prop = new Properties(); + prop.setProperty("org.apache.jackrabbit.repository.home", testDirectory); + prop.setProperty("org.apache.jackrabbit.repository.conf", testDirectory + "/repository.xml"); + try { + repository = (JackrabbitRepository)repositoryFactory.getRepository(prop); + } catch (RepositoryException e) { + failWithException(e); + }; + } + + private void login() { + try { + sessionGarbageCollector = repository.login(new SimpleCredentials("admin", "admin".toCharArray())); + sessionMover = repository.login(new SimpleCredentials("admin", "admin".toCharArray())); + } catch (Exception e) { + failWithException(e); + } + } + + private GarbageCollector setupGarbageCollector() { + try { + return ((SessionImpl) sessionGarbageCollector).createDataStoreGarbageCollector(); + } catch (RepositoryException e) { + failWithException(e); + } + return null; + } + + private void failWithException(Exception e) { + fail("Not expected: " + e.getMessage()); + } + + private int getBinaryCount(GarbageCollector garbageCollector) { + int count = 0; + Iterator it; + try { + it = garbageCollector.getDataStore().getAllIdentifiers(); + while (it.hasNext()) { + it.next(); + count++; + } + } catch (DataStoreException e) { + failWithException(e); + } + log("Binary count: " + count); + return count; + } + + private void log(String message) { + logger.debug(message); + //System.out.println(message); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCThread.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCThread.java new file mode 100644 index 00000000000..0b4a047d6fc --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GCThread.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import org.apache.jackrabbit.api.management.DataStoreGarbageCollector; +import org.apache.jackrabbit.api.management.MarkEventListener; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.gc.GarbageCollector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Helper class that runs data store garbage collection as a background thread. + */ +public class GCThread implements Runnable, MarkEventListener { + + /** logger instance */ + private static final Logger LOG = LoggerFactory.getLogger(GCThread.class); + + private boolean stop; + private Session session; + private Exception exception; + + public GCThread(Session session) { + this.session = session; + } + + public void run() { + + try { + GarbageCollector gc = ((SessionImpl) session) + .createDataStoreGarbageCollector(); + gc.setMarkEventListener(this); + while (!stop) { + LOG.debug("Scanning..."); + gc.mark(); + int count = listIdentifiers(gc); + LOG.debug("Stop; currently " + count + " identifiers"); + gc.stopScan(); + int numDeleted = gc.sweep(); + if (numDeleted > 0) { + LOG.debug("Deleted " + numDeleted + " identifiers"); + } + LOG.debug("Waiting..."); + Thread.sleep(10); + } + gc.close(); + } catch (Exception ex) { + LOG.error("Error scanning", ex); + exception = ex; + } + } + + public void setStop(boolean stop) { + this.stop = stop; + } + + public Exception getException() { + return exception; + } + + private int listIdentifiers(DataStoreGarbageCollector gc) throws DataStoreException { + DataStore ds = ((GarbageCollector) gc).getDataStore(); + Iterator it = ds.getAllIdentifiers(); + int count = 0; + while (it.hasNext()) { + DataIdentifier id = it.next(); + LOG.debug(" " + id); + count++; + } + return count; + } + + public void throwException() throws Exception { + if (exception != null) { + throw exception; + } + } + + public void beforeScanning(Node n) throws RepositoryException { + // nothing to do + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GarbageCollectorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GarbageCollectorTest.java new file mode 100644 index 00000000000..8e6a26efc40 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/GarbageCollectorTest.java @@ -0,0 +1,325 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import org.apache.jackrabbit.api.management.DataStoreGarbageCollector; +import org.apache.jackrabbit.api.management.MarkEventListener; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.gc.GarbageCollector; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import EDU.oswego.cs.dl.util.concurrent.SynchronousChannel; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import javax.jcr.Binary; +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +/** + * Test cases for data store garbage collection. + */ +public class GarbageCollectorTest extends AbstractJCRTest implements ScanEventListener { + + /** logger instance */ + private static final Logger LOG = LoggerFactory.getLogger(GarbageCollectorTest.class); + + public void testCloseSessionWhileRunningGc() throws Exception { + final Session session = getHelper().getReadWriteSession(); + + final DataStoreGarbageCollector gc = ((SessionImpl) session).createDataStoreGarbageCollector(); + gc.setPersistenceManagerScan(false); + final Exception[] ex = new Exception[1]; + gc.setMarkEventListener(new MarkEventListener() { + boolean closed; + + public void beforeScanning(Node n) throws RepositoryException { + closeTest(); + } + + private void closeTest() { + if (closed) { + ex[0] = new Exception("Scanning after the session is closed"); + } + closed = true; + session.logout(); + } + + }); + try { + gc.mark(); + fail("Exception 'session has been closed' expected"); + } catch (RepositoryException e) { + LOG.debug("Expected exception caught: " + e.getMessage()); + } + if (ex[0] != null) { + throw ex[0]; + } + gc.close(); + } + + public void testConcurrentGC() throws Exception { + Node root = testRootNode; + Session session = root.getSession(); + + final SynchronousChannel sync = new SynchronousChannel(); + final Node node = root.addNode("slowBlob"); + final int blobLength = 1000; + final ValueFactory vf = session.getValueFactory(); + new Thread() { + public void run() { + try { + node.setProperty("slowBlob", vf.createBinary(new InputStream() { + int pos; + public int read() throws IOException { + pos++; + if (pos < blobLength) { + return pos % 80 == 0 ? '\n' : '.'; + } else if (pos == blobLength) { + try { + sync.put("x"); + // deleted + sync.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return 'x'; + } + return -1; + } + })); + node.getSession().save(); + sync.put("saved"); + } catch (Exception e) { + e.printStackTrace(); + } + } + }.start(); + assertEquals("x", sync.take()); + DataStoreGarbageCollector gc = ((SessionImpl) session).createDataStoreGarbageCollector(); + gc.setPersistenceManagerScan(false); + gc.mark(); + gc.sweep(); + sync.put("deleted"); + assertEquals("saved", sync.take()); + InputStream in = node.getProperty("slowBlob").getBinary().getStream(); + for (int pos = 1; pos < blobLength; pos++) { + int expected = pos % 80 == 0 ? '\n' : '.'; + assertEquals(expected, in.read()); + } + assertEquals('x', in.read()); + in.close(); + gc.close(); + } + + public void testGC() throws Exception { + Node root = testRootNode; + Session session = root.getSession(); + + deleteMyNodes(); + runGC(session, true); + + root.addNode("node1"); + Node node2 = root.addNode("node2"); + Node n = node2.addNode("nodeWithBlob").addNode("sub"); + ValueFactory vf = session.getValueFactory(); + Binary b = vf.createBinary(new RandomInputStream(20, 1000)); + n.setProperty("test", b); + session.save(); + n = node2.addNode("nodeWithTemporaryBlob"); + n.setProperty("test", vf.createBinary(new RandomInputStream(11, 1000))); + session.save(); + + n.remove(); + session.save(); + + GarbageCollector gc = ((SessionImpl)session).createDataStoreGarbageCollector(); + gc.getDataStore().clearInUse(); + gc.setPersistenceManagerScan(false); + gc.setMarkEventListener(this); + + if (gc.getDataStore() instanceof FileDataStore) { + // make sure the file is old (access time resolution is 2 seconds) + Thread.sleep(2000); + } + + LOG.debug("scanning..."); + gc.mark(); + int count = listIdentifiers(gc); + LOG.debug("stop scanning; currently " + count + " identifiers"); + LOG.debug("deleting..."); + gc.getDataStore().clearInUse(); + assertTrue(gc.sweep() > 0); + int count2 = listIdentifiers(gc); + assertEquals(count - 1, count2); + + // verify the node was moved, and that the binary is still there + n = root.getNode("node1").getNode("nodeWithBlob").getNode("sub"); + b = n.getProperty("test").getValue().getBinary(); + InputStream in = b.getStream(); + InputStream in2 = new RandomInputStream(20, 1000); + verifyInputStream(in, in2); + + deleteMyNodes(); + + gc.close(); + } + + /** + * Test to validate that two GC cannot run simultaneously. One + * exits throwing exception. + */ + public void testSimultaneousRunGC() throws Exception { + Node root = testRootNode; + Session session = root.getSession(); + + GCThread gct1 = new GCThread(session); + GCThread gct2 = new GCThread(session); + Thread gcThread1 = new Thread(gct1, "Datastore Garbage Collector 1"); + Thread gcThread2 = new Thread(gct2, "Datastore Garbage Collector 2"); + // run simultaneous GC + gcThread1.start(); + gcThread2.start(); + Thread.sleep(100); + + gct1.setStop(true); + gct2.setStop(true); + + // allow them to complete + gcThread1.join(); + gcThread2.join(); + + // only one should throw error + int count = (gct1.getException() == null ? 0 : 1) + (gct2.getException() == null ? 0 : 1); + if (count == 0) { + fail("None of the GCs threw an exception"); + } + else { + assertEquals("Only one gc should throw an exception ", 1, count); + } + } + + private void runGC(Session session, boolean all) throws Exception { + GarbageCollector gc = ((SessionImpl)session).createDataStoreGarbageCollector(); + gc.setMarkEventListener(this); + gc.setPersistenceManagerScan(false); + + if (gc.getDataStore() instanceof FileDataStore) { + // make sure the file is old (access time resolution is 2 seconds) + Thread.sleep(2000); + } + gc.mark(); + gc.stopScan(); + if (all) { + gc.getDataStore().clearInUse(); + } + gc.sweep(); + gc.close(); + } + + private static int listIdentifiers(GarbageCollector gc) throws DataStoreException { + LOG.debug("identifiers:"); + int count = 0; + Iterator it = gc.getDataStore().getAllIdentifiers(); + while (it.hasNext()) { + DataIdentifier id = it.next(); + LOG.debug(" " + id); + count++; + } + return count; + } + + public void testTransientObjects() throws Exception { + + Node root = testRootNode; + Session session = root.getSession(); + + deleteMyNodes(); + + Credentials cred = getHelper().getSuperuserCredentials(); + Session s2 = getHelper().getRepository().login(cred); + root = s2.getRootNode(); + Node node2 = root.addNode("node3"); + Node n = node2.addNode("nodeWithBlob"); + ValueFactory vf = session.getValueFactory(); + n.setProperty("test", vf.createBinary(new RandomInputStream(10, 1000))); + + runGC(session, false); + + s2.save(); + + InputStream in = n.getProperty("test").getBinary().getStream(); + InputStream in2 = new RandomInputStream(10, 1000); + verifyInputStream(in, in2); + + deleteMyNodes(); + + s2.logout(); + } + + private static void verifyInputStream(InputStream in, InputStream in2) throws IOException { + while (true) { + int a = in.read(); + int b = in2.read(); + assertEquals(a, b); + if (a < 0) { + break; + } + } + + } + + public void afterScanning(Node n) throws RepositoryException { + if (n != null && n.getPath().startsWith("/testroot/node")) { + String path = n.getPath(); + LOG.debug("scanned: " + path); + } + } + + private void list(Node n) throws RepositoryException { + if (!n.getName().startsWith("jcr:")) { + for (NodeIterator it = n.getNodes(); it.hasNext();) { + list(it.nextNode()); + } + } + } + + public void beforeScanning(Node n) throws RepositoryException { + if (n != null && n.getPath().equals("/testroot/node2")) { + Session session = n.getSession(); + list(session.getRootNode()); + session.move("/testroot/node2/nodeWithBlob", "/testroot/node1/nodeWithBlob"); + session.save(); + LOG.debug("moved /testroot/node2/nodeWithBlob to /testroot/node1"); + } + } + + private void deleteMyNodes() throws RepositoryException { + Node root = testRootNode; + while (root.hasNode("testroot")) { + root.getNode("testroot").remove(); + } + root.getSession().save(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/LazyFileInputStreamTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/LazyFileInputStreamTest.java new file mode 100644 index 00000000000..2406570a9d3 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/LazyFileInputStreamTest.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.core.data; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; + +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Tests the LazyFileInputStream class. + */ +public class LazyFileInputStreamTest extends JUnitTest { + + private static final String TEST_FILE = "target/test.txt"; + + private File file = new File(TEST_FILE); + + public void setUp() { + // Create the test directory + new File(TEST_FILE).getParentFile().mkdirs(); + } + + public void tearDown() { + new File(TEST_FILE).delete(); + } + + private void createFile() throws IOException { + FileOutputStream out = new FileOutputStream(file); + out.write(new byte[1]); + out.close(); + } + + public void test() throws IOException { + + createFile(); + + // test exception if file doesn't exist + try { + LazyFileInputStream in = new LazyFileInputStream(file.getAbsolutePath() + "XX"); + in.close(); + fail(); + } catch (IOException e) { + // expected + } + + // test open / close (without reading) + LazyFileInputStream in = new LazyFileInputStream(file); + in.close(); + + // test reading too much and closing too much + in = new LazyFileInputStream(file); + assertEquals(0, in.read()); + assertEquals(-1, in.read()); + assertEquals(-1, in.read()); + assertEquals(-1, in.read()); + in.close(); + in.close(); + in.close(); + assertEquals(-1, in.read()); + + // test with file name + in = new LazyFileInputStream(file.getAbsolutePath()); + assertEquals(1, in.available()); + assertEquals(0, in.read()); + assertEquals(0, in.available()); + assertEquals(-1, in.read()); + assertEquals(0, in.available()); + in.close(); + + // test markSupported, mark, and reset + in = new LazyFileInputStream(file); + assertFalse(in.markSupported()); + in.mark(1); + assertEquals(0, in.read()); + try { + in.reset(); + fail(); + } catch (IOException e) { + // expected + } + assertEquals(-1, in.read()); + in.close(); + + // test read(byte[]) + in = new LazyFileInputStream(file); + byte[] test = new byte[2]; + assertEquals(1, in.read(test)); + in.close(); + + // test read(byte[],int,int) + in = new LazyFileInputStream(file); + assertEquals(1, in.read(test, 0, 2)); + in.close(); + + // test skip + in = new LazyFileInputStream(file); + assertEquals(2, in.skip(2)); + assertEquals(-1, in.read(test)); + assertEquals(0, in.skip(2)); + in.close(); + + // test with the file descriptor + RandomAccessFile ra = new RandomAccessFile(file, "r"); + in = new LazyFileInputStream(ra.getFD()); + assertEquals(0, in.read()); + assertEquals(-1, in.read()); + in.close(); + ra.close(); + + // test that the file is not opened before reading + in = new LazyFileInputStream(file); + // this should fail in Windows if the file was opened + file.delete(); + + createFile(); + + // test that the file is closed after reading the last byte + in = new LazyFileInputStream(file); + assertEquals(0, in.read()); + assertEquals(-1, in.read()); + // this should fail in Windows if the file was opened + file.delete(); + + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/NodeTypeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/NodeTypeTest.java new file mode 100644 index 00000000000..7fd86e797a2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/NodeTypeTest.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.core.data; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.api.JackrabbitNodeTypeManager; +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test node types with binary default values. + */ +public class NodeTypeTest extends AbstractJCRTest { + + /** + * Test a node type with a binary default value + * @throws RepositoryException + * @throws ParseException + */ + public void testNodeTypesWithBinaryDefaultValue() + throws RepositoryException, IOException, ParseException { + + doTestNodeTypesWithBinaryDefaultValue(0); + doTestNodeTypesWithBinaryDefaultValue(10); + doTestNodeTypesWithBinaryDefaultValue(10000); + } + + public void doTestNodeTypesWithBinaryDefaultValue(int len) + throws RepositoryException, IOException, ParseException { + char[] chars = new char[len]; + for (int i = 0; i < chars.length; i++) { + chars[i] = 'a'; + } + String def = new String(chars); + + String type = "test:foo" + len; + String cnd = + "\n" + + "[" + type + "]\n - value(binary) = '" + def + "' m a"; + JackrabbitNodeTypeManager manager = (JackrabbitNodeTypeManager) + superuser.getWorkspace().getNodeTypeManager(); + if (!manager.hasNodeType(type)) { + Reader cndReader = new InputStreamReader(new ByteArrayInputStream(cnd.getBytes("UTF-8"))); + CndImporter.registerNodeTypes(cndReader, superuser); + } + + Node root = superuser.getRootNode(); + Node node = root.addNode("testfoo" + len, type); + Value value = node.getProperty("value").getValue(); + assertEquals(PropertyType.BINARY, value.getType()); + assertEquals(def, value.getString()); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/OpenFilesTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/OpenFilesTest.java new file mode 100644 index 00000000000..a1d0ecf5e53 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/OpenFilesTest.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.core.data; + +import java.io.ByteArrayInputStream; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test if getting the value from a binary property opens the file. + */ +public class OpenFilesTest extends AbstractJCRTest { + + /** + * Test opening a large number of streams. + */ + public void testStreams() throws RepositoryException { + Session session = getHelper().getReadWriteSession(); + try { + Node test = session.getRootNode().addNode("test"); + ValueFactory vf = session.getValueFactory(); + test.setProperty("data", vf.createBinary(new ByteArrayInputStream(new byte[10 * 1024]))); + session.save(); + for (int i = 0; i < 10000; i++) { + test.getProperty("data").getValue(); + } + } catch (Exception e) { + e.printStackTrace(); + assertFalse(e.getMessage(), true); + } finally { + session.logout(); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/PersistenceManagerIteratorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/PersistenceManagerIteratorTest.java new file mode 100644 index 00000000000..48a0f97b036 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/PersistenceManagerIteratorTest.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.core.data; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.config.WorkspaceConfig; +import org.apache.jackrabbit.core.persistence.PersistenceManager; +import org.apache.jackrabbit.core.persistence.bundle.AbstractBundlePersistenceManager; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.Session; + +/** + * Test AbstractBundlePersistenceManager.getAllNodeIds + */ +public class PersistenceManagerIteratorTest extends AbstractJCRTest { + /** logger instance */ + private static final Logger LOG = LoggerFactory.getLogger(PersistenceManagerIteratorTest.class); + + private void log(String s) { + // System.out.println(s); + LOG.info(s); + } + + public void testGetAllNodeIds() throws Exception { + Node root = testRootNode; + Session session = root.getSession(); + Repository rep = session.getRepository(); + + if (!(rep instanceof RepositoryImpl)) { + log("Test skipped. Required repository class: " + + RepositoryImpl.class + " got: " + rep.getClass()); + return; + } + + RepositoryImpl r = (RepositoryImpl) rep; + RepositoryConfig conf = r.getConfig(); + Collection coll = conf.getWorkspaceConfigs(); + String[] names = new String[coll.size()]; + Iterator wspIt = coll.iterator(); + for (int i = 0; wspIt.hasNext(); i++) { + WorkspaceConfig wsc = wspIt.next(); + names[i] = wsc.getName(); + } + + for (int i = 0; i < names.length && i < 1; i++) { + Session s = getHelper().getSuperuserSession(names[i]); + try { + Method m = r.getClass().getDeclaredMethod("getWorkspaceInfo", new Class[] { String.class }); + m.setAccessible(true); + Object info = m.invoke(r, names[i]); + m = info.getClass().getDeclaredMethod("getPersistenceManager", new Class[0]); + m.setAccessible(true); + PersistenceManager pm = (PersistenceManager) m.invoke(info, new Object[0]); + if (!(pm instanceof AbstractBundlePersistenceManager)) { + log("PM skipped: " + pm.getClass()); + continue; + } + AbstractBundlePersistenceManager apm = (AbstractBundlePersistenceManager) pm; + log("PM: " + pm.getClass().getName()); + + log("All nodes in one step"); + NodeId after = null; + NodeId first = null; + for (NodeId id : apm.getAllNodeIds(null, 0)) { + log(" " + id); + if (first == null) { + // initialize first node id + first = id; + } + if (after != null) { + assertFalse(id.compareTo(after) == 0); + } + after = id; + } + + // start with first + after = first; + log("All nodes using batches"); + while (true) { + log(" bigger than: " + after); + Iterator it = apm.getAllNodeIds(after, 2).iterator(); + if (!it.hasNext()) { + break; + } + while (it.hasNext()) { + NodeId id = it.next(); + log(" " + id); + assertFalse(id.compareTo(after) == 0); + after = id; + } + } + + log("Random access"); + for (int j = 0; j < 50; j++) { + after = NodeId.randomId(); + log(" bigger than: " + after); + for (NodeId id : apm.getAllNodeIds(after, 2)) { + log(" " + id); + assertFalse(id.compareTo(after) == 0); + after = id; + } + } + } finally { + s.logout(); + } + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/TestAll.java new file mode 100644 index 00000000000..8ac2b9f0d76 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/TestAll.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.core.data; + +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the data module. + */ +public class TestAll extends TestCase { + + /** + * Returns a test suite that executes all tests inside this package. + * + * @return a test suite that executes all tests inside this package + */ + public static Test suite() { + TestSuite suite = new ConcurrentTestSuite("Data tests"); + + suite.addTestSuite(ConcurrentGcTest.class); + suite.addTestSuite(CopyValueTest.class); + suite.addTestSuite(DataStoreAPITest.class); + suite.addTestSuite(DataStoreTest.class); + suite.addTestSuite(DBDataStoreTest.class); + suite.addTestSuite(ExportImportTest.class); + suite.addTestSuite(GarbageCollectorTest.class); + suite.addTestSuite(GCConcurrentTest.class); + suite.addTestSuite(GCEventListenerTest.class); + suite.addTestSuite(LazyFileInputStreamTest.class); + suite.addTestSuite(NodeTypeTest.class); + suite.addTestSuite(OpenFilesTest.class); + suite.addTestSuite(PersistenceManagerIteratorTest.class); + suite.addTestSuite(TestTwoGetStreams.class); + suite.addTestSuite(WriteWhileReadingTest.class); + suite.addTestSuite(GCSubtreeMoveTest.class); + + return suite; + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/TestTwoGetStreams.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/TestTwoGetStreams.java new file mode 100644 index 00000000000..b110c397e0c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/TestTwoGetStreams.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Node; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.api.JackrabbitValue; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test concurrent reads to the data store. + */ +public class TestTwoGetStreams extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(TestTwoGetStreams.class); + + private static final int STREAM_LENGTH = 256 * 1024; + + /** + * Test the JackrabbitValue.getContentIdentity feature. + */ + public void testContentIdentity() throws Exception { + Node root = superuser.getRootNode(); + ValueFactory vf = superuser.getValueFactory(); + + long time = System.currentTimeMillis(); + root.setProperty("p1", vf.createBinary(new RandomInputStream(1, STREAM_LENGTH))); + superuser.save(); + long saveOne = System.currentTimeMillis() - time; + + root.setProperty("p2", vf.createBinary(new RandomInputStream(1, STREAM_LENGTH))); + superuser.save(); + + Value v1 = root.getProperty("p1").getValue(); + Value v2 = root.getProperty("p2").getValue(); + if (v1 instanceof JackrabbitValue && v2 instanceof JackrabbitValue) { + JackrabbitValue j1 = (JackrabbitValue) v1; + JackrabbitValue j2 = (JackrabbitValue) v2; + String id1 = j1.getContentIdentity(); + String id2 = j2.getContentIdentity(); + assertNotNull(id1); + assertEquals(id1, id2); + } + + // copying a value should not stream the content + time = System.currentTimeMillis(); + for (int i = 0; i < 100; i++) { + Value v = root.getProperty("p1").getValue(); + root.setProperty("p3", v); + } + superuser.save(); + time = System.currentTimeMillis() - time; + // streaming 1 MB again and again takes about 4.3 seconds + // on my computer; copying the data identifier takes about 16 ms + // here we test if copying 100 objects took less than saving 50 new objects + assertTrue("time: " + time, time < saveOne * 50); + + } + + /** + * Test reading from two concurrently opened streams. + */ + public void testTwoGetStreams() throws Exception { + Node root = superuser.getRootNode(); + ValueFactory vf = superuser.getValueFactory(); + root.setProperty("p1", vf.createBinary(new RandomInputStream(1, STREAM_LENGTH))); + root.setProperty("p2", vf.createBinary(new RandomInputStream(2, STREAM_LENGTH))); + superuser.save(); + + InputStream i1 = root.getProperty("p1").getBinary().getStream(); + InputStream i2 = root.getProperty("p2").getBinary().getStream(); + assertEquals("p1", i1, new RandomInputStream(1, STREAM_LENGTH)); + assertEquals("p2", i2, new RandomInputStream(2, STREAM_LENGTH)); + try { + i1.close(); + } catch (IOException e) { + log.info("Could not close first input stream: ", e); + } + try { + i2.close(); + } catch (IOException e) { + log.info("Could not close second input stream: ", e); + } + } + + /** + * Tests reading concurrently from two different streams. + */ + public void testTwoStreamsConcurrently() throws Exception { + Node root = superuser.getRootNode(); + ValueFactory vf = superuser.getValueFactory(); + root.setProperty("p1", vf.createBinary(new RandomInputStream(1, STREAM_LENGTH))); + root.setProperty("p2", vf.createBinary(new RandomInputStream(1, STREAM_LENGTH))); + superuser.save(); + + InputStream i1 = root.getProperty("p1").getBinary().getStream(); + InputStream i2 = root.getProperty("p2").getBinary().getStream(); + assertEquals("Streams are different", i1, i2); + try { + i1.close(); + } catch (IOException e) { + log.info("Could not close first input stream: ", e); + } + try { + i2.close(); + } catch (IOException e) { + log.info("Could not close second input stream: ", e); + } + } + + /** + * Tests reading concurrently from two different streams coming from the + * same property. + */ + public void testTwoStreamsFromSamePropertyConcurrently() throws Exception { + Node root = superuser.getRootNode(); + ValueFactory vf = superuser.getValueFactory(); + root.setProperty("p1", vf.createBinary(new RandomInputStream(1, STREAM_LENGTH))); + superuser.save(); + + InputStream i1 = root.getProperty("p1").getBinary().getStream(); + InputStream i2 = root.getProperty("p1").getBinary().getStream(); + assertEquals("Streams are different", i1, i2); + try { + i1.close(); + } catch (IOException e) { + log.info("Could not close first input stream: ", e); + } + try { + i2.close(); + } catch (IOException e) { + log.info("Could not close second input stream: ", e); + } + } + + /** + * Asserts that two input streams are equal. + */ + protected void assertEquals(String message, InputStream i1, InputStream i2) { + try { + int b1 = 0, b2 = 0; + int i = 0; + while (b1 != -1 || b2 != -1) { + b1 = i1.read(); + b2 = i2.read(); + assertEquals(message + "; byte #" + i + " mismatch!", b2, b1); + ++i; + } + } catch (Exception e) { + fail("Could not read inputstream! " + e); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/WriteWhileReadingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/WriteWhileReadingTest.java new file mode 100644 index 00000000000..f2539f89f19 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/data/WriteWhileReadingTest.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.core.data; + +import java.io.InputStream; +import javax.jcr.Node; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test that adding an entry works even if the the entry already exists and is still open for reading. + * + * This is a problem for the FileDataStore on Windows, see JCR-2872. + */ +public class WriteWhileReadingTest extends AbstractJCRTest { + + private static final int STREAM_LENGTH = 32 * 1024; + + public void test() throws Exception { + Node root = superuser.getRootNode(); + ValueFactory vf = superuser.getValueFactory(); + + // store a binary in the data store + root.setProperty("p1", vf.createBinary(new RandomInputStream(1, STREAM_LENGTH))); + superuser.save(); + + // read from the binary, but don't close the file + Value v1 = root.getProperty("p1").getValue(); + InputStream in = v1.getBinary().getStream(); + in.read(); + + // store the same content at a different place - + // this will change the last modified date of the file + root.setProperty("p2", vf.createBinary(new RandomInputStream(1, STREAM_LENGTH))); + superuser.save(); + + in.close(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/AbstractFileSystemTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/AbstractFileSystemTest.java new file mode 100644 index 00000000000..5644cb98af6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/AbstractFileSystemTest.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.core.fs; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; + +import junit.framework.TestCase; + +/** + * Tests a file system implementation. + */ +public abstract class AbstractFileSystemTest extends TestCase { + + private FileSystem fs; + private byte[] sampleBytes = new byte[]{(byte)0x12, (byte)0x0F, (byte)0xF0}; + + protected abstract FileSystem getFileSystem() throws Exception; + + protected void setUp() throws Exception { + fs = getFileSystem(); + fs.init(); + } + + protected void tearDown() throws Exception { + fs.close(); + } + + public void testEverything() throws Exception { + String[] list; + + // At beginning the file system should contain only the root folder + assertTrue(fs.exists("/")); + assertTrue(fs.isFolder("/")); + assertFalse(fs.isFile("/")); + assertEquals(0, fs.list("/").length); + + // Create a folder + fs.createFolder("/folder"); + assertTrue(fs.exists("/folder")); + assertTrue(fs.isFolder("/folder")); + assertFalse(fs.isFile("/folder")); + assertEquals(0, fs.list("/folder").length); + list = fs.list("/"); + assertEquals(1, list.length); + assertEquals("folder", list[0]); + + // Create a file inside the folder + createFile("/folder/file", sampleBytes); + assertTrue(fs.exists("/folder/file")); + assertFalse(fs.isFolder("/folder/file")); + assertTrue(fs.isFile("/folder/file")); + list = fs.list("/folder"); + assertEquals(1, list.length); + assertEquals("file", list[0]); + assertEquals(3, fs.length("/folder/file")); + verifyStreamInput(fs.getInputStream("/folder/file"), sampleBytes); + + // Create a subfolder + fs.createFolder("/folder2/subfolder"); + createFile("/folder2/file2", sampleBytes); + assertTrue(fs.exists("/folder2/subfolder")); + assertTrue(fs.isFolder("/folder2/subfolder")); + assertFalse(fs.isFile("/folder2/subfolder")); + assertEquals(0, fs.list("/folder2/subfolder").length); + list = fs.list("/folder2"); + Arrays.sort(list); + assertEquals(2, list.length); + assertEquals("file2", list[0]); + assertEquals("subfolder", list[1]); + list = fs.listFiles("/folder2"); + assertEquals(1, list.length); + assertEquals("file2", list[0]); + list = fs.listFolders("/folder2"); + assertEquals(1, list.length); + assertEquals("subfolder", list[0]); + + // Try to create a file colliding with an existing folder + try { + createFile("/folder2/subfolder", sampleBytes); + fail("FileSystemException expected"); + } catch (FileSystemException e) { + // ok + } + + // Delete the subfolder + fs.deleteFolder("/folder2/subfolder"); + assertFalse(fs.exists("/folder2/subfolder")); + assertFalse(fs.isFolder("/folder2/subfolder")); + assertFalse(fs.isFile("/folder2/subfolder")); + list = fs.list("/folder2"); + assertEquals(1, list.length); + assertEquals("file2", list[0]); + list = fs.listFiles("/folder2"); + assertEquals(1, list.length); + assertEquals("file2", list[0]); + assertEquals(0, fs.listFolders("/folder2").length); + + // Delete the folder + fs.deleteFolder("/folder"); + fs.deleteFolder("/folder2"); + assertFalse(fs.exists("/folder2")); + assertFalse(fs.isFolder("/folder2")); + assertFalse(fs.isFile("/folder2")); + assertFalse(fs.exists("/folder2/file2")); + assertFalse(fs.isFolder("/folder2/file2")); + assertFalse(fs.isFile("/folder2/file2")); + assertEquals(0, fs.list("/").length); + + // Test last modified time stamps + createFile("/file1", sampleBytes); + Thread.sleep(100); + createFile("/file2", sampleBytes); + assertTrue(fs.lastModified("/file1") <= fs.lastModified("/file2")); + + // Try to create a file inside a nonexistent folder + try { + createFile("/missing/file", sampleBytes); + fail("FileSystemException expected"); + } catch (FileSystemException e) { + // ok + } + + // Try to create a folder inside a nonexistent folder + fs.createFolder("/missing/subfolder"); + assertTrue(fs.exists("/missing")); + assertTrue(fs.isFolder("/missing")); + assertFalse(fs.isFile("/missing")); + assertTrue(fs.exists("/missing/subfolder")); + assertTrue(fs.isFolder("/missing/subfolder")); + assertFalse(fs.isFile("/missing/subfolder")); + assertEquals(0, fs.list("/missing/subfolder").length); + list = fs.list("/missing"); + assertEquals(1, list.length); + assertEquals("subfolder", list[0]); + } + + private void verifyStreamInput( + InputStream inputStream, byte[] expectedBytes) throws IOException { + byte[] resultBytes = new byte[3]; + assertEquals(3, inputStream.read(resultBytes)); + inputStream.close(); + + assertEquals(expectedBytes[0], resultBytes[0]); + assertEquals(expectedBytes[1], resultBytes[1]); + assertEquals(expectedBytes[2], resultBytes[2]); + } + + private void createFile(String fileName, byte[] bytes) + throws IOException, FileSystemException { + OutputStream outputStream = fs.getOutputStream(fileName); + outputStream.write(bytes); + outputStream.close(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/TestAll.java new file mode 100644 index 00000000000..79db04fa284 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/TestAll.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.core.fs; + +import org.apache.jackrabbit.core.fs.db.DerbyFileSystemTest; +import org.apache.jackrabbit.core.fs.local.LocalFileSystemTest; +import org.apache.jackrabbit.core.fs.mem.MemoryFileSystemTest; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all test cases for this module. + */ +public class TestAll extends TestCase { + + /** + * Returns a test suite that executes all tests inside this package. + * + * @return a test suite that executes all tests inside this package + */ + public static Test suite() { + TestSuite suite = new TestSuite("FileSystem tests"); + suite.addTestSuite(DerbyFileSystemTest.class); + suite.addTestSuite(LocalFileSystemTest.class); + suite.addTestSuite(MemoryFileSystemTest.class); + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/DerbyFileSystemTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/DerbyFileSystemTest.java new file mode 100644 index 00000000000..522995f623c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/DerbyFileSystemTest.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.core.fs.db; + +import java.io.File; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.fs.AbstractFileSystemTest; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; + +/** + * Tests the Apache Derby file system. + */ +public class DerbyFileSystemTest extends AbstractFileSystemTest { + + private ConnectionFactory conFac; + + private File file; + + protected FileSystem getFileSystem() { + DerbyFileSystem filesystem = new DerbyFileSystem(); + filesystem.setConnectionFactory(conFac); + filesystem.setUrl("jdbc:derby:" + file.getPath() + ";create=true"); + return filesystem; + } + + protected void setUp() throws Exception { + file = File.createTempFile("jackrabbit", "derbyfs"); + file.delete(); + conFac = new ConnectionFactory(); + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + FileUtils.deleteDirectory(file); + conFac.close(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/OracleFileSystemTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/OracleFileSystemTest.java new file mode 100755 index 00000000000..ca9596e8214 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/OracleFileSystemTest.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.core.fs.db; + +import org.apache.jackrabbit.core.fs.AbstractFileSystemTest; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; + +/** + * Tests the creation of an Oracle file system with different tablespace and index tablespace. + * + * @author Edouard Hue + */ +public class OracleFileSystemTest extends AbstractFileSystemTest { + private ConnectionFactory connectionFactory; + + @Override + protected FileSystem getFileSystem() throws Exception { + connectionFactory = new ConnectionFactory(); + final OracleFileSystem fs = new OracleFileSystem(); + fs.setConnectionFactory(connectionFactory); + fs.setUrl(System.getProperty("tests.oracle.url")); + fs.setUser(System.getProperty("tests.oracle.user")); + fs.setPassword(System.getProperty("tests.oracle.password")); + fs.setDriver(System.getProperty("tests.oracle.driver", "oracle.jdbc.driver.OracleDriver")); + fs.setSchemaObjectPrefix(System.getProperty("tests.oracle.schemaObjectPrefix", "")); + fs.setTablespace(System.getProperty("tests.oracle.tablespace")); + fs.setIndexTablespace(System.getProperty("tests.oracle.indexTablespace")); + fs.setSchema("oracle"); + return fs; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/OracleRetrocompatibleFileSystemTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/OracleRetrocompatibleFileSystemTest.java new file mode 100755 index 00000000000..7812c2fee03 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/db/OracleRetrocompatibleFileSystemTest.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.core.fs.db; + +import org.apache.jackrabbit.core.fs.AbstractFileSystemTest; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; + +/** + * Tests the creation of an Oracle file system with no index tablespace specified. + * + * @author Edouard Hue + */ +public class OracleRetrocompatibleFileSystemTest extends AbstractFileSystemTest { + private ConnectionFactory connectionFactory; + + @Override + protected FileSystem getFileSystem() throws Exception { + connectionFactory = new ConnectionFactory(); + final OracleFileSystem fs = new OracleFileSystem(); + fs.setConnectionFactory(connectionFactory); + fs.setUrl(System.getProperty("tests.oracle.url")); + fs.setUser(System.getProperty("tests.oracle.user")); + fs.setPassword(System.getProperty("tests.oracle.password")); + fs.setDriver(System.getProperty("tests.oracle.driver", "oracle.jdbc.driver.OracleDriver")); + fs.setSchemaObjectPrefix(System.getProperty("tests.oracle.schemaObjectPrefix", "")); + fs.setTablespace(System.getProperty("tests.oracle.tablespace")); + fs.setSchema("oracle"); + return fs; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/local/LocalFileSystemTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/local/LocalFileSystemTest.java new file mode 100644 index 00000000000..c59759acd95 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/local/LocalFileSystemTest.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.core.fs.local; + +import java.io.File; +import java.io.IOException; + +import org.apache.jackrabbit.core.fs.AbstractFileSystemTest; +import org.apache.jackrabbit.core.fs.FileSystem; + +/** + * Tests the local file system. + */ +public class LocalFileSystemTest extends AbstractFileSystemTest { + + private String tempDirectory; + + protected FileSystem getFileSystem() { + LocalFileSystem filesystem = new LocalFileSystem(); + filesystem.setPath(tempDirectory); + return filesystem; + } + + protected void setUp() throws Exception { + File file = File.createTempFile("jackrabbit", "localfs"); + tempDirectory = file.getPath(); + file.delete(); + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + delete(new File(tempDirectory)); + } + + private void delete(File file) throws IOException { + File[] files = file.listFiles(); + for (int i = 0; files != null && i < files.length; i++) { + delete(files[i]); + } + file.delete(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystemTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystemTest.java new file mode 100644 index 00000000000..010319f0d1e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/fs/mem/MemoryFileSystemTest.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.core.fs.mem; + +import org.apache.jackrabbit.core.fs.AbstractFileSystemTest; +import org.apache.jackrabbit.core.fs.FileSystem; + +/** + * Tests the in-memory file system. + */ +public class MemoryFileSystemTest extends AbstractFileSystemTest { + + protected FileSystem getFileSystem() { + return new MemoryFileSystem(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/NodeIdFactoryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/NodeIdFactoryTest.java new file mode 100644 index 00000000000..0b030ae645c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/NodeIdFactoryTest.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.core.id; + +import java.io.File; +import java.io.IOException; +import javax.jcr.RepositoryException; +import org.apache.commons.io.FileUtils; +import junit.framework.TestCase; + +public class NodeIdFactoryTest extends TestCase { + + private static final String factoryDir = "target/temp/nodeIdFactory"; + + public void setUp() throws IOException { + System.clearProperty(NodeIdFactory.SEQUENTIAL_NODE_ID); + FileUtils.deleteDirectory(new File(factoryDir)); + } + + public void tearDown() throws IOException { + setUp(); + System.clearProperty(NodeIdFactory.SEQUENTIAL_NODE_ID); + } + + public void testRandomVersusSequential() throws RepositoryException { + NodeIdFactory f = new NodeIdFactory(factoryDir); + f.open(); + NodeId id = f.newNodeId(); + assertTrue(id.getLeastSignificantBits() != 0); + f.close(); + + System.setProperty(NodeIdFactory.SEQUENTIAL_NODE_ID, "true"); + f = new NodeIdFactory(factoryDir); + f.open(); + id = f.newNodeId(); + assertTrue(id.getLeastSignificantBits() == 0); + f.close(); + } + + /** + * Test that the version field is reset (0), and that all other MSB bits are + * 1 at some point. This also tests the LSB bits. + */ + public void testUUIDVersionFieldReset() throws Exception { + System.setProperty(NodeIdFactory.SEQUENTIAL_NODE_ID, "true"); + long msbOr = 0, msbAnd = -1, lsbOr = 0, lsbAnd = -1; + for (int i = 0; i < 0x1f; i++) { + FileUtils.deleteDirectory(new File(factoryDir)); + NodeIdFactory f = new NodeIdFactory(factoryDir); + f.open(); + for (int j = 0; j < 8; j++) { + NodeId x = f.newNodeId(); + msbOr |= x.getMostSignificantBits(); + msbAnd &= x.getMostSignificantBits(); + lsbAnd &= x.getLeastSignificantBits(); + lsbOr |= x.getLeastSignificantBits(); + } + f.close(); + } + assertEquals(0xffffffffffff0fffL, msbOr); + assertEquals(0, msbAnd); + assertEquals(7, lsbOr); + assertEquals(0, lsbAnd); + } + + public void testNormalUsage() throws RepositoryException { + System.setProperty(NodeIdFactory.SEQUENTIAL_NODE_ID, "true"); + NodeIdFactory f = new NodeIdFactory(factoryDir); + f.open(); + assertTrue(f.newNodeId().toString().endsWith("-0000-000000000000")); + f.close(); + f = new NodeIdFactory(factoryDir); + f.open(); + assertTrue(f.newNodeId().toString().endsWith("-0000-000000000001")); + f.close(); + } + + public void testOffset() throws RepositoryException { + System.setProperty(NodeIdFactory.SEQUENTIAL_NODE_ID, "ab/0"); + NodeIdFactory f = new NodeIdFactory(factoryDir); + f.open(); + assertEquals("00000000-0000-00ab-0000-000000000000", f.newNodeId().toString()); + f.close(); + f = new NodeIdFactory(factoryDir); + f.open(); + assertEquals("00000000-0000-00ab-0000-000000000001", f.newNodeId().toString()); + f.close(); + } + + public void testKillRepository() throws RepositoryException { + System.setProperty(NodeIdFactory.SEQUENTIAL_NODE_ID, "true"); + int cacheSize = 8; + for (int i = 1; i < 40; i++) { + File id = new File(factoryDir, "nodeId.properties"); + id.delete(); + NodeIdFactory f = new NodeIdFactory(factoryDir); + f.setCacheSize(cacheSize); + f.open(); + NodeId last = null; + for (int j = 0; j < i; j++) { + last = f.newNodeId(); + } + // don't close the factory - this is the same as killing the process + // f.close(); + f = new NodeIdFactory(factoryDir); + f.setCacheSize(cacheSize); + f.open(); + NodeId n = f.newNodeId(); + assertTrue("now: " + n + " last: " + last, n.compareTo(last) > 0); + long diff = n.getLeastSignificantBits() - last.getLeastSignificantBits(); + assertTrue("diff: " + diff, diff > 0 && diff <= cacheSize); + f.close(); + } + } + + public void testKillWhileSaving() throws RepositoryException { + System.setProperty(NodeIdFactory.SEQUENTIAL_NODE_ID, "true"); + NodeIdFactory f = new NodeIdFactory(factoryDir); + f.open(); + assertTrue(f.newNodeId().toString().endsWith("-0000-000000000000")); + f.close(); + File id = new File(factoryDir, "nodeId.properties"); + assertTrue(id.exists()); + File idTemp = new File(factoryDir, "nodeId.properties.temp"); + id.renameTo(idTemp); + f = new NodeIdFactory(factoryDir); + f.open(); + assertTrue(f.newNodeId().toString().endsWith("-0000-000000000001")); + assertFalse(idTemp.exists()); + f.close(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/NodeIdTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/NodeIdTest.java new file mode 100644 index 00000000000..17c8c716982 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/NodeIdTest.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.core.id; + +import junit.framework.TestCase; + +public class NodeIdTest extends TestCase { + + private static final NodeId[] ids = { + NodeId.randomId(), // random id + new NodeId(0, 0), + new NodeId(-1, -1), + new NodeId("cafebabe-cafe-babe-cafe-babecafebabe") + }; + + public void testDenotesNode() { + for (NodeId id : ids) { + assertTrue(id.denotesNode()); + } + } + + public void testGetMostAndLeastSignificantBits() { + for (NodeId id : ids) { + long msb = id.getMostSignificantBits(); + long lsb = id.getLeastSignificantBits(); + assertEquals(id, new NodeId(msb, lsb)); + } + } + + public void testGetRawBytes() { + for (NodeId id : ids) { + assertEquals(id, new NodeId(id.getRawBytes())); + } + } + + public void testToString() { + for (NodeId id : ids) { + assertEquals(id, new NodeId(id.toString())); + } + } + + public void testCompareTo() { + for (NodeId id : ids) { + assertEquals(0, id.compareTo(id)); + } + + NodeId[] ordered = { + new NodeId(-1, -1), + new NodeId(-1, 0), + new NodeId(0, -1), + new NodeId(0, 0), + new NodeId(0, 1), + new NodeId(1, 0), + new NodeId(1, 1) + }; + for (int i = 0; i < ordered.length; i++) { + for (int j = 0; j < i; j++) { + assertEquals(1, ordered[i].compareTo(ordered[j])); + } + assertEquals(0, ordered[i].compareTo(ordered[i])); + for (int j = i + 1; j < ordered.length; j++) { + assertEquals(-1, ordered[i].compareTo(ordered[j])); + } + } + } + + public void testUuidFormat() { + long maxHigh = 0, maxLow = 0, minHigh = -1L, minLow = -1L; + for (int i = 0; i < 100; i++) { + NodeId id = NodeId.randomId(); + assertUuidFormat(id); + maxHigh |= id.getMostSignificantBits(); + maxLow |= id.getLeastSignificantBits(); + minHigh &= id.getMostSignificantBits(); + minLow &= id.getLeastSignificantBits(); + } + NodeId max = new NodeId(maxHigh, maxLow); + assertEquals("ffffffff-ffff-4fff-bfff-ffffffffffff", max.toString()); + NodeId min = new NodeId(minHigh, minLow); + assertEquals("00000000-0000-4000-8000-000000000000", min.toString()); + } + + private void assertUuidFormat(NodeId id) { + long high = id.getMostSignificantBits(); + long low = id.getLeastSignificantBits(); + long high2 = (high & (~0xf000L)) | 0x4000L; // version 4 (random) + assertEquals(high, high2); + long low2 = (low & 0x3fffffffffffffffL) | 0x8000000000000000L; // variant (Leach-Salz) + assertEquals(low, low2); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/TestAll.java new file mode 100644 index 00000000000..696679823f8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/id/TestAll.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.core.id; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all test cases for the id module. + */ +public class TestAll extends TestCase { + + /** + * Returns a test suite that executes all tests inside this package. + * + * @return a test suite that executes all tests inside this package + */ + public static Test suite() { + TestSuite suite = new TestSuite("Identifier tests"); + + suite.addTestSuite(NodeIdFactoryTest.class); + suite.addTestSuite(NodeIdTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/AxisQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/AxisQueryTest.java new file mode 100644 index 00000000000..6531439ec1e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/AxisQueryTest.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.core.integration; + +import org.apache.jackrabbit.core.query.AbstractQueryTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import java.util.Random; +import java.util.Set; +import java.util.HashSet; +import java.util.regex.Pattern; + +/** + * AxisQueryTest runs random queries on generated test data. The + * amount of test data can be controled by {@link #NUM_LEVELS} and + * {@link #NODES_PER_LEVEL}. The default values create 363 nodes distributed + * over 5 hierarchy levels. + */ +public class AxisQueryTest extends AbstractQueryTest { + + /** + * Number of levels of test data to create. + */ + private static final int NUM_LEVELS = 5; + + /** + * Number of nodes per level + */ + private static final int NODES_PER_LEVEL = 3; + + /** + * Execute random queries for this amount of time. + */ + private static final int RUN_NUM_SECONDS = 10; + + /** + * Controls if query results are checked for their correctness. When the + * number of test nodes increases this becomes expensive and should be + * disabled. + */ + private static final boolean CHECK_RESULT = true; + + /** + * Set to a name that is different from {@link #testPath} when number of + * test nodes is increased. This avoids that the test nodes are removed + * after a test run. The number of test nodes can be controlled by + * {@link #NUM_LEVELS} and {@link #NODES_PER_LEVEL}. + */ + private String relTestLocation; + + /** + * Absolute path to the test location. + */ + private String absTestLocation; + + private Random rand = new Random(); + + protected void setUp() throws Exception { + super.setUp(); + if (relTestLocation == null) { + // use default location + relTestLocation = testPath; + absTestLocation = "/" + relTestLocation; + createNodes(testRootNode, NODES_PER_LEVEL, NUM_LEVELS, 0); + } else { + // customized location + absTestLocation = "/" + relTestLocation; + // only create test data if not yet present + if (!superuser.itemExists(absTestLocation)) { + Node testLocation = superuser.getRootNode().addNode(relTestLocation); + createNodes(testLocation, NODES_PER_LEVEL, NUM_LEVELS, 0); + } + // uncomment to write log messages to system out + //log.setWriter(new PrintWriter(System.out, true)); + } + } + + public void testExecuteQueries() throws RepositoryException { + Node testLocation = superuser.getRootNode().getNode(relTestLocation); + long end = System.currentTimeMillis() + 1000 * RUN_NUM_SECONDS; + while (end > System.currentTimeMillis()) { + StringBuffer statement = new StringBuffer(relTestLocation); + StringBuffer regexp = new StringBuffer(absTestLocation); + int numSteps = rand.nextInt(NUM_LEVELS) + 1; // at least one step + while (numSteps-- > 0) { + String axis = getRandomAxis(); + String name = getRandomName(); + statement.append(axis).append(name); + if (axis.equals("//")) { + regexp.append("/(.*/)?"); + } else { + regexp.append("/"); + } + if (name.equals("*")) { + regexp.append("[^/]*"); + } else { + regexp.append(name); + } + } + long time = System.currentTimeMillis(); + NodeIterator nodes = executeQuery(statement.toString()).getNodes(); + nodes.hasNext(); + time = System.currentTimeMillis() - time; + log.print(statement + "\t" + nodes.getSize() + "\t" + time); + if (CHECK_RESULT) { + Set paths = new HashSet(); + time = System.currentTimeMillis(); + traversalEvaluate(testLocation, + Pattern.compile(regexp.toString()), + paths); + time = System.currentTimeMillis() - time; + log.println("\t" + time); + assertEquals("wrong number of results", paths.size(), nodes.getSize()); + while (nodes.hasNext()) { + String path = nodes.nextNode().getPath(); + assertTrue(path + " is not part of the result set", paths.contains(path)); + } + } else { + log.println(); + } + log.flush(); + } + } + + private String getRandomAxis() { + if (rand.nextBoolean() && rand.nextBoolean()) { + // 25% descendant axis + return "//"; + } else { + // 75% child axis + return "/"; + } + } + + private String getRandomName() { + if (rand.nextBoolean() && rand.nextBoolean()) { + // 25% any name + return "*"; + } else { + // 75% name + return "node" + rand.nextInt(NODES_PER_LEVEL); + } + } + + private void traversalEvaluate(Node n, Pattern pattern, Set matchingPaths) + throws RepositoryException { + if (pattern.matcher(n.getPath()).matches()) { + matchingPaths.add(n.getPath()); + } + for (NodeIterator it = n.getNodes(); it.hasNext(); ) { + traversalEvaluate(it.nextNode(), pattern, matchingPaths); + } + } + + private int createNodes(Node n, int nodesPerLevel, int levels, int count) + throws RepositoryException { + levels--; + for (int i = 0; i < nodesPerLevel; i++) { + Node child = n.addNode("node" + i); + child.setProperty("count", count++); + if (count % 1000 == 0) { + superuser.save(); + log.println("Created " + (count / 1000) + "k nodes"); + } + if (levels > 0) { + count = createNodes(child, nodesPerLevel, levels, count); + } + } + if (levels == 0) { + // final save + superuser.save(); + } + return count; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/CachingHierarchyManagerConsistencyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/CachingHierarchyManagerConsistencyTest.java new file mode 100644 index 00000000000..be40d58c91c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/CachingHierarchyManagerConsistencyTest.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.core.integration; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test case for JCR-3617. + */ +public class CachingHierarchyManagerConsistencyTest extends AbstractJCRTest { + + private static final Logger log = LoggerFactory.getLogger(CachingHierarchyManagerConsistencyTest.class); + + private static final int TEST_DURATION = 10; // seconds + private static final int NUM_LISTENERS = 10; + private static final int ALL_EVENTS = Event.NODE_ADDED | Event.NODE_MOVED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED; + private static final String TEST_PATH = "/my/test/path"; + + public void testObservation() throws Exception { + final List exceptions = new ArrayList(); + Thread writer = new Thread(new Runnable() { + public void run() { + try { + long end = System.currentTimeMillis() + TEST_DURATION * 1000; + Session s = getHelper().getSuperuserSession(); + try { + log.info("Starting to replace nodes"); + int i = 0; + while (System.currentTimeMillis() < end) { + replaceNodes(s, i++); + } + } finally { + s.logout(); + } + } catch (RepositoryException e) { + exceptions.add(e); + } + } + }); + List listeners = new ArrayList(); + for (int i = 0; i < NUM_LISTENERS; i++) { + final Session session = getHelper().getSuperuserSession(); + listeners.add(new EventListener() { + public void onEvent(EventIterator events) { + while (events.hasNext()) { + Event event = events.nextEvent(); + String path = "n/a"; + try { + if (event.getType() == Event.NODE_ADDED + || event.getType() == Event.PROPERTY_ADDED) { + path = event.getPath(); + session.getItem(path); + } + } catch (PathNotFoundException e) { + // ignore + } catch (RepositoryException e) { + log.error(e.toString() + " Unable to get item with path: " + path); + exceptions.add(e); + } + } + } + }); + } + for (EventListener listener : listeners) { + superuser.getWorkspace().getObservationManager().addEventListener( + listener, ALL_EVENTS, "/", true, null, null, false); + } + + writer.start(); + writer.join(); + + for (EventListener listener : listeners) { + superuser.getWorkspace().getObservationManager().removeEventListener(listener); + } + + log.info("" + exceptions.size() + " exception(s) occurred."); + if (!exceptions.isEmpty()) { + throw exceptions.get(0); + } + } + + private void replaceNodes(Session session, int i) throws RepositoryException { + String nodeName = "node-" + (i % 100); + Node root = JcrUtils.getOrCreateByPath(testRoot + TEST_PATH, ntUnstructured, session); + String uuid = UUID.randomUUID().toString(); + if (root.hasNode(nodeName)) { + Node n = root.getNode(nodeName); + uuid = n.getIdentifier(); + n.remove(); + } + Node n = ((NodeImpl) root).addNodeWithUuid(nodeName, ntUnstructured, uuid); + n.addMixin("mix:referenceable"); + n.addNode("foo").addNode("bar"); + n.addNode("qux"); + session.save(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ConcurrentQueriesWithUpdatesTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ConcurrentQueriesWithUpdatesTest.java new file mode 100644 index 00000000000..71ddaf5a144 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ConcurrentQueriesWithUpdatesTest.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.core.integration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +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; + +import org.apache.jackrabbit.core.AbstractConcurrencyTest; + +/** + * ConcurrentQueriesWithUpdatesTest runs concurrent queries that + * stress the hierarchy cache in CachingIndexReader combined with updates that + * modify the hierarchy cache. + */ +public class ConcurrentQueriesWithUpdatesTest extends AbstractConcurrencyTest { + + private static final int NUM_UPDATES = 50; + + private int numNodes; + + @Override + protected void setUp() throws Exception { + super.setUp(); + numNodes = createNodes(testRootNode, 2, 12, 0); // ~4k nodes + superuser.save(); + } + + public void testQueriesWithUpdates() throws Exception { + final List exceptions = Collections.synchronizedList(new ArrayList()); + final AtomicBoolean running = new AtomicBoolean(true); + // track executed queries and do updates at most at the given rate + final BlockingQueue queryExecuted = new LinkedBlockingQueue(); + Thread queries = new Thread(new Runnable() { + public void run() { + try { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + QueryManager qm = session.getWorkspace().getQueryManager(); + while (running.get()) { + Query q = qm.createQuery(testPath + "//element(*, nt:unstructured) order by @jcr:score descending", Query.XPATH); + NodeIterator nodes = q.execute().getNodes(); + assertEquals("wrong result set size", numNodes, nodes.getSize()); + queryExecuted.offer(new Object()); + } + } + }, 5, testRootNode.getPath()); + } catch (RepositoryException e) { + exceptions.add(e); + } + } + }); + queries.start(); + Thread update = new Thread(new Runnable() { + public void run() { + try { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + Random rand = new Random(); + QueryManager qm = session.getWorkspace().getQueryManager(); + for (int i = 0; i < NUM_UPDATES; i++) { + try { + // wait at most 10 seconds + queryExecuted.poll(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // ignore + } + Query q = qm.createQuery(testPath + "//node" + rand.nextInt(numNodes) + " order by @jcr:score descending", Query.XPATH); + NodeIterator nodes = q.execute().getNodes(); + if (nodes.hasNext()) { + Node n = nodes.nextNode(); + n.setProperty("foo", "bar"); + session.save(); + } + } + } + }, 1, testRootNode.getPath()); + } catch (RepositoryException e) { + exceptions.add(e); + } + } + }); + update.start(); + update.join(); + running.set(false); + queries.join(); + } + + private int createNodes(Node n, int nodesPerLevel, int levels, int count) + throws RepositoryException { + levels--; + for (int i = 0; i < nodesPerLevel; i++) { + Node child = n.addNode("node" + count); + count++; + if (count % 1000 == 0) { + superuser.save(); + } + if (levels > 0) { + count = createNodes(child, nodesPerLevel, levels, count); + } + } + return count; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ConcurrentQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ConcurrentQueryTest.java new file mode 100644 index 00000000000..6a7d0c1b2ca --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ConcurrentQueryTest.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.integration; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.NodeIterator; +import javax.jcr.Node; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import java.io.PrintWriter; + +/** + * Runs queries in one thread while another thread is modifying the workspace. + */ +public class ConcurrentQueryTest extends AbstractJCRTest { + + /** + * Number of threads executing queries. + */ + private static final int NUM_READERS = 1; + + /** + * The read sessions executing the queries. + */ + private List readSessions = new ArrayList(); + + /** + * Gets the read sessions for the test cases. + */ + protected void setUp() throws Exception { + super.setUp(); + for (int i = 0; i < NUM_READERS; i++) { + readSessions.add(getHelper().getReadOnlySession()); + } + } + + /** + * Logs out the sessions acquired in setUp(). + */ + protected void tearDown() throws Exception { + super.tearDown(); + for (Session s : readSessions) { + s.logout(); + } + readSessions.clear(); + } + + /** + * Writes 1000 nodes in transactions of 5 nodes to the workspace while + * other threads query the workspace. Query results must always return + * a consistent view of the workspace, that is:
    + * result.numNodes % 5 == 0 + */ + public void testConcurrentQueryWithWrite() throws Exception { + + final List exceptions = Collections.synchronizedList(new ArrayList()); + List readers = new ArrayList(); + String query = "/jcr:root" + testRoot + "//*[@testprop = 'foo']"; + for (Session s : readSessions) { + readers.add(new QueryWorker(s, query, exceptions, log)); + } + + Thread writer = new Thread() { + public void run() { + try { + for (int i = 0; i < 20; i++) { + Node n = testRootNode.addNode("node" + i); + for (int j = 0; j < 10; j++) { + Node n1 = n.addNode("node" + j); + for (int k = 0; k < 5; k++) { + n1.addNode("node" + k).setProperty("testprop", "foo"); + } + testRootNode.save(); + } + } + } catch (RepositoryException e) { + exceptions.add(e); + } + } + }; + + // start the threads + writer.start(); + for (Thread t : readers ) { + t.start(); + } + + // wait for writer thread to finish its work + writer.join(); + + // request readers to finish + for (QueryWorker t : readers) { + t.finish(); + t.join(); + } + + // fail in case of exceptions + if (exceptions.size() > 0) { + fail(exceptions.get(0).toString()); + } + } + + /** + * Deletes 1000 nodes in transactions of 5 nodes while + * other threads query the workspace. Query results must always return + * a consistent view of the workspace, that is:
    + * result.numNodes % 5 == 0 + */ + public void testConcurrentQueryWithDeletes() throws Exception { + + // create 1000 nodes + for (int i = 0; i < 20; i++) { + Node n = testRootNode.addNode("node" + i); + for (int j = 0; j < 10; j++) { + Node n1 = n.addNode("node" + j); + for (int k = 0; k < 5; k++) { + n1.addNode("node" + k).setProperty("testprop", "foo"); + } + } + testRootNode.save(); + } + + final List exceptions = Collections.synchronizedList(new ArrayList()); + List readers = new ArrayList(); + String query = "/jcr:root" + testRoot + "//*[@testprop = 'foo']"; + for (Session s : readSessions) { + readers.add(new QueryWorker(s, query, exceptions, log)); + } + + Thread writer = new Thread() { + public void run() { + try { + for (int i = 0; i < 20; i++) { + Node n = testRootNode.getNode("node" + i); + for (int j = 0; j < 10; j++) { + Node n1 = n.getNode("node" + j); + for (int k = 0; k < 5; k++) { + n1.getNode("node" + k).remove(); + } + testRootNode.save(); + } + } + } catch (Exception e) { + exceptions.add(e); + } + } + }; + + // start the threads + writer.start(); + for (Thread t : readers) { + t.start(); + } + + // wait for writer thread to finish its work + writer.join(); + + // request readers to finish + for (QueryWorker t : readers) { + t.finish(); + t.join(); + } + + // fail in case of exceptions + if (!exceptions.isEmpty()) { + fail(exceptions.get(0).toString()); + } + } + + /** + * Executes queries in a separate thread. + */ + private static final class QueryWorker extends Thread { + + private Session s; + private String query; + private final List exceptions; + private final PrintWriter log; + private boolean finish = false; + private int count; + + QueryWorker(Session s, String query, List exceptions, PrintWriter log) { + this.s = s; + this.query = query; + this.exceptions = exceptions; + this.log = log; + } + + public void run() { + try { + // run the queries + QueryManager qm = s.getWorkspace().getQueryManager(); + Query q = qm.createQuery(query, Query.XPATH); + for (;;) { + long time = System.currentTimeMillis(); + NodeIterator nodes = q.execute().getNodes(); + long size = nodes.getSize(); + if (size == -1) { + while (nodes.hasNext()) { + size++; + nodes.nextNode(); + } + } + time = System.currentTimeMillis() - time; + log.println(getName() + ": num nodes:" + size + + " executed in: " + time + " ms."); + + count++; + if (size % 5 != 0) { + exceptions.add(new Exception("number of result nodes must be divisible by 5, but is: " + size)); + } + // do not consume all cpu power + Thread.sleep(10); + synchronized (this) { + if (finish) { + break; + } + } + } + } catch (Exception e) { + exceptions.add(e); + } + log.println("Executed " + count + " queries"); + } + + public synchronized void finish() { + finish = true; + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java new file mode 100644 index 00000000000..fcb2aae37f8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GQLTest.java @@ -0,0 +1,420 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.integration; + +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.commons.query.GQL; +import org.apache.jackrabbit.core.query.AbstractQueryTest; + +import junit.framework.AssertionFailedError; + +/** + * GQLTest performs tests on {@link GQL}. + */ +public class GQLTest extends AbstractQueryTest { + + private static final String SAMPLE_CONTENT = "the quick brown fox jumps over the lazy dog."; + + public void testPath() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + superuser.save(); + RowIterator rows = GQL.execute(createStatement(""), superuser); + checkResult(rows, new Node[]{n1, n2, n3}); + } + + public void testOrder() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("p", 1); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("p", 2); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty("p", 3); + superuser.save(); + // default: ascending + String stmt = createStatement("order:p"); + RowIterator rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n1, n2, n3}); + // explicit ascending + stmt = createStatement("order:+p"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n1, n2, n3}); + // explicit descending + stmt = createStatement("order:-p"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n3, n2, n1}); + } + + public void testOrderDeep() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("prop", "value"); + n1.addNode("sub").setProperty("p", 1); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("prop", "value"); + n2.addNode("sub").setProperty("p", 2); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty("prop", "value"); + n3.addNode("sub").setProperty("p", 3); + superuser.save(); + // default: ascending + String stmt = createStatement("prop:value order:sub/p"); + RowIterator rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n1, n2, n3}); + // explicit ascending + stmt = createStatement("prop:value order:+sub/p"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n1, n2, n3}); + // explicit descending + stmt = createStatement("prop:value order:-sub/p"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n3, n2, n1}); + } + + public void testLimit() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("p", 1); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("p", 2); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty("p", 3); + superuser.save(); + // only 2 results + String stmt = createStatement("order:p limit:2"); + RowIterator rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n1, n2}); + // range with open start + stmt = createStatement("order:p limit:..2"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n1, n2}); + // range with open end + stmt = createStatement("order:p limit:1.."); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n2, n3}); + // range + stmt = createStatement("order:p limit:1..2"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n2}); + // range with end larger than max results + stmt = createStatement("order:p limit:1..7"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n2, n3}); + // range start larger than end + // end is ignored in that case + stmt = createStatement("order:p limit:2..1"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n3}); + // range with start larger than max results + stmt = createStatement("order:p limit:6..10"); + rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{}); + } + + public void testPhrase() throws RepositoryException { + Node file1 = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + superuser.save(); + String stmt = createStatement("\"quick brown\""); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + } + + /** + * Test for JCR-3157 + */ + public void testApostrophe() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("text", "let's go"); + superuser.save(); + String stmt = createStatement("\"let's go\""); + RowIterator rows = GQL.execute(stmt, superuser); + checkResult(rows, new Node[]{n1}); + } + + public void testExcludeTerm() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("text", "foo"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("text", "bar"); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty("text", "foo bar"); + superuser.save(); + String stmt = createStatement("foo -bar"); + RowIterator rows = GQL.execute(stmt, superuser); + checkResult(rows, new Node[]{n1}); + } + + public void testOptionalTerm() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("text", "apache jackrabbit"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("text", "apache jcr"); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty("text", "jackrabbit jcr"); + superuser.save(); + String stmt = createStatement("apache jackrabbit OR jcr"); + RowIterator rows = GQL.execute(stmt, superuser); + checkResult(rows, new Node[]{n1, n2}); + } + + public void testType() throws RepositoryException { + Node file = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + Node node = testRootNode.addNode("node1"); + node.setProperty("text", SAMPLE_CONTENT); + superuser.save(); + // only nt:resource + String stmt = createStatement("quick type:\"nt:resource\""); + checkResultWithRetries(stmt, null, new Node[]{file.getNode("jcr:content")}); + // only nt:unstructured + stmt = createStatement("quick type:\"nt:unstructured\""); + RowIterator rows = GQL.execute(stmt, superuser); + checkResult(rows, new Node[]{node}); + } + + public void testMixinType() throws RepositoryException { + Node node = testRootNode.addNode("node1"); + node.setProperty("text", SAMPLE_CONTENT); + node.addMixin(mixReferenceable); + superuser.save(); + String stmt = createStatement("quick type:referenceable"); + RowIterator rows = GQL.execute(stmt, superuser); + checkResult(rows, new Node[]{node}); + } + + public void testTypeInheritance() throws RepositoryException { + Node file = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + superuser.save(); + // nt:hierarchyNode and sub types + String stmt = createStatement("quick type:hierarchyNode"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file}); + } + + public void testAutoPrefixType() throws RepositoryException { + Node file = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + Node node = testRootNode.addNode("node1"); + node.setProperty("text", SAMPLE_CONTENT); + superuser.save(); + // only nt:resource + String stmt = createStatement("quick type:resource"); + checkResultWithRetries(stmt, null, new Node[]{file.getNode("jcr:content")}); + // only nt:unstructured + stmt = createStatement("quick type:unstructured"); + RowIterator rows = GQL.execute(stmt, superuser); + checkResult(rows, new Node[]{node}); + } + + public void testQuotedProperty() throws RepositoryException { + Node file1 = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + superuser.save(); + String stmt = createStatement("\"jcr:mimeType\":text/plain"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + } + + public void testAutoPrefix() throws RepositoryException { + Node file1 = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + superuser.save(); + String stmt = createStatement("mimeType:text/plain"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + } + + public void testCommonPathPrefix() throws RepositoryException { + Node file1 = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + Node file2 = addFile(testRootNode, "file2.txt", SAMPLE_CONTENT); + Node file3 = addFile(testRootNode, "file3.txt", SAMPLE_CONTENT); + superuser.save(); + String stmt = createStatement("quick"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1, file2, file3}); + } + + public void testExcerpt() throws RepositoryException { + Node file = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + superuser.save(); + String stmt = createStatement("quick"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file}); + RowIterator rows = GQL.execute(stmt, superuser, "jcr:content"); + assertTrue("Expected result", rows.hasNext()); + String excerpt = rows.nextRow().getValue("rep:excerpt()").getString(); + assertTrue("No excerpt returned", excerpt.startsWith("
    ")); + stmt = createStatement("type:resource quick"); + rows = GQL.execute(stmt, superuser); + assertTrue("Expected result", rows.hasNext()); + excerpt = rows.nextRow().getValue("rep:excerpt()").getString(); + assertTrue("No excerpt returned", excerpt.startsWith("
    ")); + } + + public void testPrefixedValue() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.setProperty("jcr:title", "a"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("jcr:title", "b"); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty("jcr:title", "c"); + superuser.save(); + String stmt = createStatement("order:jcr:title"); + RowIterator rows = GQL.execute(stmt, superuser); + checkResultSequence(rows, new Node[]{n1, n2, n3}); + } + + public void testFilter() throws RepositoryException { + final Node n1 = testRootNode.addNode("node1"); + n1.setProperty("jcr:title", "a"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("jcr:title", "b"); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty("jcr:title", "c"); + superuser.save(); + String stmt = createStatement("order:jcr:title"); + RowIterator rows = GQL.execute(stmt, superuser, null, new GQL.Filter() { + public boolean include(Row row) throws RepositoryException { + return !n1.getPath().equals(row.getValue("jcr:path").getString()); + } + }); + checkResultSequence(rows, new Node[]{n2, n3}); + } + + public void testFilterLimit() throws RepositoryException { + final Node n1 = testRootNode.addNode("node1"); + n1.setProperty("jcr:title", "a"); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("jcr:title", "b"); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty("jcr:title", "c"); + superuser.save(); + String stmt = createStatement("order:jcr:title limit:1"); + RowIterator rows = GQL.execute(stmt, superuser, null, new GQL.Filter() { + public boolean include(Row row) throws RepositoryException { + return !n1.getPath().equals(row.getValue("jcr:path").getString()); + } + }); + checkResultSequence(rows, new Node[]{n2}); + } + + public void testName() throws RepositoryException { + Node file1 = addFile(testRootNode, "file1.txt", SAMPLE_CONTENT); + superuser.save(); + + String stmt = createStatement("\"quick brown\" name:file1.txt"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:file?.txt"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:?ile1.txt"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:file1.tx?"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:file1.???"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:fil*xt"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:*.txt"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:file1.*"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:*"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:fIlE1.*"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{file1}); + + stmt = createStatement("\"quick brown\" name:file2.txt"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{}); + + stmt = createStatement("\"quick brown\" name:file1.t?"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{}); + + stmt = createStatement("\"quick brown\" name:?le1.txt"); + checkResultWithRetries(stmt, "jcr:content", new Node[]{}); + } + + public void XXXtestQueryDestruction() throws RepositoryException { + char[] stmt = createStatement("title:jackrabbit \"apache software\" type:file order:+title limit:10..20").toCharArray(); + for (char c = 0; c < 255; c++) { + for (int i = 0; i < stmt.length; i++) { + char orig = stmt[i]; + stmt[i] = c; + try { + GQL.execute(new String(stmt), superuser); + } finally { + stmt[i] = orig; + } + } + } + } + + protected static Node addFile(Node folder, String name, String contents) + throws RepositoryException { + Node file = folder.addNode(name, "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:encoding", "UTF-8"); + try { + resource.setProperty("jcr:data", new ByteArrayInputStream( + contents.getBytes("UTF-8"))); + } catch (UnsupportedEncodingException e) { + // will never happen + } + return file; + } + + protected String createStatement(String stmt) { + return "path:" + testRoot + " " + stmt; + } + + /** + * Checks if the result contains exactly the nodes. If the + * result does not contain the + * + * @param gql the gql statement. + * @param cpp the common path prefix or null. + * @param nodes the expected nodes in the result set. + * @throws RepositoryException if an error occurs while reading from the result. + */ + protected void checkResultWithRetries(String gql, String cpp, Node[] nodes) + throws RepositoryException { + int retries = 10; + for (int i = 0; i < retries; i++) { + try { + checkResult(GQL.execute(gql, superuser, cpp), nodes); + break; + } catch (AssertionFailedError e) { + if (i + 1 == retries) { + throw e; + } + try { + // sleep for a second and retry + Thread.sleep(1000); + } catch (InterruptedException e1) { + // ignore + } + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GetOrNullTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GetOrNullTest.java new file mode 100644 index 00000000000..b7fc5df0a29 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/GetOrNullTest.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.core.integration; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +/** + * @see JCR-3870 + */ +public class GetOrNullTest extends AbstractJCRTest { + + public static String NAME_EXISTING_PROPERTY = "property1"; + public static String PATH_EXISTING_NODE = "/node1"; + public static String PATH_NON_EXISTING_NODE = "/non-existing-node"; + public static String PATH_EXISTING_PROPERTY = PATH_EXISTING_NODE + "/" + NAME_EXISTING_PROPERTY; + public static String PATH_NON_EXISTING_PROPERTY = PATH_EXISTING_NODE + "/non-existing-property"; + + public void setUp() throws Exception { + super.setUp(); + + Node node = JcrUtils.getOrCreateByPath(PATH_EXISTING_NODE, "nt:unstructured", superuser); + node.setProperty(NAME_EXISTING_PROPERTY, "value"); + superuser.save(); + } + + public void testGetItemOrNullExistingNode() throws RepositoryException { + JackrabbitSession js = (JackrabbitSession) superuser; + Item item = js.getItemOrNull(PATH_EXISTING_NODE); + assertNotNull(item); + assertTrue(item instanceof Node); + assertEquals(item.getPath(), PATH_EXISTING_NODE); + } + + public void testGetItemOrNullNonExistingNode() throws RepositoryException { + JackrabbitSession js = (JackrabbitSession) superuser; + Item item = js.getItemOrNull(PATH_NON_EXISTING_NODE); + assertNull(item); + } + + public void testGetItemOrNullExistingProperty() throws RepositoryException { + JackrabbitSession js = (JackrabbitSession) superuser; + Item item = js.getItemOrNull(PATH_EXISTING_PROPERTY); + assertNotNull(item); + assertTrue(item instanceof Property); + assertEquals(item.getPath(), PATH_EXISTING_PROPERTY); + } + + public void testGetItemOrNullNonExistingProperty() throws RepositoryException { + JackrabbitSession js = (JackrabbitSession) superuser; + Item item = js.getItemOrNull(PATH_NON_EXISTING_PROPERTY); + assertNull(item); + } + + public void testGetNodeOrNullExisting() throws RepositoryException { + JackrabbitSession js = (JackrabbitSession) superuser; + Node node = js.getNodeOrNull(PATH_EXISTING_NODE); + assertNotNull(node); + assertEquals(node.getPath(), PATH_EXISTING_NODE); + } + + public void testGetNodeOrNullNonExisting() throws RepositoryException { + JackrabbitSession js = (JackrabbitSession) superuser; + Node node = js.getNodeOrNull(PATH_NON_EXISTING_NODE); + assertNull(node); + } + + public void testGetPropertyOrNullExisting() throws RepositoryException { + JackrabbitSession js = (JackrabbitSession) superuser; + Property property = js.getPropertyOrNull(PATH_EXISTING_PROPERTY); + assertNotNull(property); + assertEquals(property.getPath(), PATH_EXISTING_PROPERTY); + } + + public void testGetPropertyOrNullNonExisting() throws RepositoryException { + JackrabbitSession js = (JackrabbitSession) superuser; + Property property = js.getPropertyOrNull(PATH_NON_EXISTING_PROPERTY); + assertNull(property); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/InterruptedQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/InterruptedQueryTest.java new file mode 100644 index 00000000000..b197fe88d8e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/InterruptedQueryTest.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.core.integration; + +import java.io.File; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.lucene.util.Constants; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * @see JCR-3469 + */ +public class InterruptedQueryTest { + + private RepositoryImpl repo; + + private Session session; + + @Before + public void setUp() throws Exception { + if (Constants.WINDOWS) { + return; + } + deleteAll(); + FileUtils.copyInputStreamToFile( + getClass().getResourceAsStream("repository-with-SimpleFSDirectory.xml"), + new File(getTestDir(), "repository.xml")); + repo = RepositoryImpl.create(RepositoryConfig.create(getTestDir())); + session = repo.login(new SimpleCredentials("admin", "admin".toCharArray())); + } + + @After + public void tearDown() throws Exception { + if (session != null) { + session.logout(); + } + if (repo != null) { + repo.shutdown(); + } + deleteAll(); + } + + @Test + public void testQuery() throws Exception { + if (Constants.WINDOWS) { + return; + } + for (int i = 0; i < 100; i++) { + session.getRootNode().addNode("node" + i, "nt:unstructured"); + } + session.save(); + final QueryManager qm = session.getWorkspace().getQueryManager(); + final AtomicBoolean stop = new AtomicBoolean(false); + final List exceptions = Collections.synchronizedList( + new ArrayList()); + Thread t = new Thread(new Runnable() { + @Override + public void run() { + while (!stop.get() && exceptions.isEmpty()) { + try { + // execute query + String stmt = "//*[@jcr:primaryType='nt:unstructured']"; + qm.createQuery(stmt, Query.XPATH).execute(); + } catch (RepositoryException e) { + if (Constants.SUN_OS) { + // on Solaris it's OK when the root cause + // of the exception is an InterruptedIOException + // the underlying file is not closed + Throwable t = e; + while (t.getCause() != null) { + t = t.getCause(); + } + if (!(t instanceof InterruptedIOException)) { + exceptions.add(e); + } + } else { + exceptions.add(e); + } + } + } + } + }); + t.start(); + for (int i = 0; i < 200 && t.isAlive(); i++) { + t.interrupt(); + Thread.sleep((long) (100.0 * Math.random())); + } + stop.set(true); + t.join(); + if (!exceptions.isEmpty()) { + throw exceptions.get(0); + } + } + + private static void deleteAll() throws IOException { + FileUtils.deleteDirectory(getTestDir()); + } + + private static File getTestDir() throws IOException { + return new File("target", + InterruptedQueryTest.class.getSimpleName()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ItemSequenceTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ItemSequenceTest.java new file mode 100644 index 00000000000..bd3f85848cd --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/ItemSequenceTest.java @@ -0,0 +1,351 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.jackrabbit.core.integration; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.flat.BTreeManager; +import org.apache.jackrabbit.commons.flat.ItemSequence; +import org.apache.jackrabbit.commons.flat.NodeSequence; +import org.apache.jackrabbit.commons.flat.PropertySequence; +import org.apache.jackrabbit.commons.flat.Rank; +import org.apache.jackrabbit.commons.flat.TreeManager; +import org.apache.jackrabbit.commons.flat.TreeTraverser; +import org.apache.jackrabbit.commons.flat.TreeTraverser.ErrorHandler; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.NodeType; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +public class ItemSequenceTest extends AbstractJCRTest { + private Node testNode; + private final ErrorHandler errorHandler = new ErrorHandler() { + public void call(Item item, RepositoryException exception) { + fail("An exception occurred on " + item + ": " + exception); + } + }; + + @Override + public void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode("ItemSequenceTest", NodeType.NT_UNSTRUCTURED); + superuser.save(); + } + + public void testEmptyNodeSequence() throws RepositoryException { + Comparator order = Rank.comparableComparator(); + TreeManager treeManager = new BTreeManager(testNode, 5, 10, order, true); + NodeSequence nodes = ItemSequence.createNodeSequence(treeManager, errorHandler); + + Iterator nodeIt = nodes.iterator(); + assertTrue(nodeIt.hasNext()); + assertTrue(treeManager.isRoot(nodeIt.next())); + assertFalse(nodeIt.hasNext()); + + checkTreeProperty(testNode, 5, 10, order); + checkOrder(nodes, order); + assertEmpty(nodes); + } + + public void testSingletonNodeSequence() throws RepositoryException { + Comparator order = Rank.comparableComparator(); + TreeManager treeManager = new BTreeManager(testNode, 5, 10, order, true); + NodeSequence nodes = ItemSequence.createNodeSequence(treeManager, errorHandler); + + nodes.addNode("key", NodeType.NT_UNSTRUCTURED); + assertTrue(nodes.hasItem("key")); + + Iterator nodeIt = nodes.iterator(); + assertTrue(nodeIt.hasNext()); + assertEquals("key", nodeIt.next().getName()); + assertFalse(nodeIt.hasNext()); + + checkTreeProperty(testNode, 5, 10, order); + checkOrder(nodes, order); + + nodes.removeNode("key"); + assertEmpty(nodes); + } + + public void testNodeSequence() throws RepositoryException, IOException { + Comparator order = Rank.comparableComparator(); + TreeManager treeManager = new BTreeManager(testNode, 5, 10, order, true); + NodeSequence nodes = ItemSequence.createNodeSequence(treeManager, errorHandler); + + List words = loadWords(); + Collections.shuffle(words); + + addAll(nodes, words); + checkTreeProperty(testNode, 5, 10, order); + checkOrder(nodes, order); + + Collections.shuffle(words); + checkLookup(nodes, words); + + Collections.shuffle(words); + List toRemove = take(words.size()/5, words); + removeAll(nodes, toRemove); + checkNotFound(nodes, toRemove); + checkLookup(nodes, words); + + removeAll(nodes, words); + assertEmpty(nodes); + } + + public void testEmptyPropertySequence() throws RepositoryException { + Comparator order = Rank.comparableComparator(); + TreeManager treeManager = new BTreeManager(testNode, 2, 4, order, true); + PropertySequence properties = ItemSequence.createPropertySequence(treeManager, errorHandler); + + Iterator propertyIt = properties.iterator(); + assertFalse(propertyIt.hasNext()); + assertEmpty(properties); + } + + public void testSingletonPropertySequence() throws RepositoryException { + Comparator order = Rank.comparableComparator(); + TreeManager treeManager = new BTreeManager(testNode, 2, 4, order, true); + PropertySequence properties = ItemSequence.createPropertySequence(treeManager, errorHandler); + + ValueFactory vFactory = testNode.getSession().getValueFactory(); + properties.addProperty("key", vFactory.createValue("key_")); + assertTrue(properties.hasItem("key")); + + Iterator propertyIt = properties.iterator(); + assertTrue(propertyIt.hasNext()); + assertEquals("key", propertyIt.next().getName()); + assertFalse(propertyIt.hasNext()); + + properties.removeProperty("key"); + assertEmpty(properties); + } + + public void testPropertySequence() throws RepositoryException, IOException { + Comparator order = Rank.comparableComparator(); + TreeManager treeManager = new BTreeManager(testNode, 2, 4, order, true); + PropertySequence properties = ItemSequence.createPropertySequence(treeManager, errorHandler); + + List words = loadWords(); + Collections.shuffle(words); + + ValueFactory vFactory = testNode.getSession().getValueFactory(); + addAll(properties, words, vFactory); + checkTreeProperty(testNode, 2, 4, order); + + Collections.shuffle(words); + checkLookup(properties, words); + + Collections.shuffle(words); + List toRemove = take(words.size()/5, words); + removeAll(properties, toRemove); + checkNotFound(properties, toRemove); + checkLookup(properties, words); + + removeAll(properties, words); + assertEmpty(properties); + } + + // -----------------------------------------------------< internal >--- + + private static List loadWords() throws FileNotFoundException, IOException { + InputStreamReader wordList = new InputStreamReader(ItemSequenceTest.class.getResourceAsStream("words.txt")); + BufferedReader wordReader = new BufferedReader(wordList); + + List words = new LinkedList(); + String word = wordReader.readLine(); + while (word != null) { + words.add(word); + word = wordReader.readLine(); + } + return words; + } + + /** + * Remove first count items from list and return them + * in as a separate list. + */ + private static List take(int count, List list) { + List removed = new LinkedList(); + for (int k = 0; k < count; k++) { + removed.add(list.remove(0)); + } + return removed; + } + + private static void addAll(NodeSequence nodes, List words) throws RepositoryException { + for (String name : words) { + nodes.addNode(name, NodeType.NT_UNSTRUCTURED); + } + } + + private static void addAll(PropertySequence properties, List words, ValueFactory vFactory) + throws RepositoryException { + + for (String name : words) { + properties.addProperty(name, vFactory.createValue(name + " value")); + } + } + + private static void checkTreeProperty(Node root, int minChildren, int maxChildren, Comparator order) + throws RepositoryException { + + int depth = -1; + + for (Node node : new TreeTraverser(root)) { + String parentName = node.getName(); + + if (node.hasNodes()) { + int childCount = 0; + for (NodeIterator nodes = node.getNodes(); nodes.hasNext(); ) { + Node child = nodes.nextNode(); + childCount++; + if (!root.isSame(node)) { + assertTrue("Mismatching order: node " + node + " contains child " + child, + order.compare(parentName, child.getName()) <= 0); + } + } + if (!root.isSame(node)) { + assertTrue("Node " + node + " should have at least " + minChildren + " child nodes", + minChildren <= childCount); + } + assertTrue("Node " + node + " should have no more than " + maxChildren + " child nodes", + maxChildren >= childCount); + } + + else { + if (depth == -1) { + depth = node.getDepth(); + } + else { + assertEquals("Node " + node + " has depth " + node.getDepth() + " instead of " + depth, + depth, node.getDepth()); + } + + int propCount = 0; + for (PropertyIterator properties = node.getProperties(); properties.hasNext(); ) { + Property property = properties.nextProperty(); + String propertyName = property.getName(); + if (!JcrConstants.JCR_PRIMARYTYPE.equals(propertyName)) { + propCount++; + assertTrue("Mismatching order: node " + node + " contains property " + property, + order.compare(parentName, propertyName) <= 0); + } + } + + if (propCount > 0) { + assertTrue("Node " + node + " should have at least " + minChildren + " properties", + minChildren <= propCount); + assertTrue("Node" + node + " should have no more than " + maxChildren + " properties", + maxChildren >= propCount); + } + } + + } + } + + private static void checkOrder(NodeSequence nodes, Comparator order) throws RepositoryException { + Node p = null; + for (Node n : nodes) { + if (p != null) { + assertTrue("Mismatching order: node " + p + " should occur before node " + n, + order.compare(p.getName(), n.getName()) < 0); + } + p = n; + } + } + + private static void checkLookup(NodeSequence nodes, List keys) throws RepositoryException { + for (String key : keys) { + assertTrue("Missing key: " + key, nodes.hasItem(key)); + Node node = nodes.getItem(key); + assertEquals("Key " + key + " does not match name of node " + node, key, node.getName()); + } + } + + private static void checkLookup(PropertySequence properties, List keys) throws RepositoryException { + for (String key : keys) { + assertTrue("Missing key: " + key, properties.hasItem(key)); + Property property = properties.getItem(key); + assertEquals("Key " + key + " does not match name of property " + property, key, property.getName()); + } + } + + private static void checkNotFound(NodeSequence nodes, List keys) throws RepositoryException { + for (String key : keys) { + assertFalse("NodeSequence should not contain key " + key, nodes.hasItem(key)); + try { + nodes.getItem(key); + fail("NodeSequence should not contain key " + key); + } + catch (RepositoryException expected) { } + } + } + + private static void checkNotFound(PropertySequence properties, List keys) throws RepositoryException { + for (String key : keys) { + assertFalse("PropertySequence should not contain key " + key, properties.hasItem(key)); + try { + properties.getItem(key); + fail("PropertySequence should not contain key " + key); + } + catch (RepositoryException expected) { } + } + } + + + private static void removeAll(NodeSequence nodes, List words) throws RepositoryException { + for (String name : words) { + nodes.removeNode(name); + } + } + + private static void removeAll(PropertySequence properties, List words) throws RepositoryException { + for (String name : words) { + properties.removeProperty(name); + } + } + + private void assertEmpty(NodeSequence nodes) throws RepositoryException { + for (Node n : nodes) { + if (!n.isSame(testNode)) { + fail("NodeSqeuence should be empty but found " + n.getPath()); + } + } + } + + private void assertEmpty(PropertySequence properties) throws RepositoryException { + for (Property p : properties) { + fail("PropertySqeuence should be empty but found " + p.getPath()); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/JCRAPITest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/JCRAPITest.java new file mode 100644 index 00000000000..fde65fabfdc --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/JCRAPITest.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.core.integration; + +import junit.framework.Test; +import junit.framework.TestCase; +import org.apache.jackrabbit.test.JCRTestSuite; + +/** + * Test suite that includes all test suites from jackrabbit-jcr-tests. + */ +public class JCRAPITest extends TestCase { + + public static Test suite() { + return new JCRTestSuite(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/JCRBenchmark.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/JCRBenchmark.java new file mode 100644 index 00000000000..d9c717cada6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/JCRBenchmark.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.core.integration; + +import junit.framework.Test; +import junit.framework.TestCase; +import org.apache.jackrabbit.benchmark.BenchmarkSuite; + +/** + * Test suite that includes all test suites from jackrabbit-jcr-benchmark. + */ +public class JCRBenchmark extends TestCase { + + public static Test suite() { + return new BenchmarkSuite(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/MassiveRangeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/MassiveRangeTest.java new file mode 100644 index 00000000000..2c898cb18cb --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/MassiveRangeTest.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.core.integration; + +import org.apache.jackrabbit.core.query.AbstractQueryTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +/** + * Tests if a range query with a lot of results does not throw an error. + */ +public class MassiveRangeTest extends AbstractQueryTest { + + /** + * Executes a range query covering 2'000 different property values. + */ + public void testRangeQuery() throws RepositoryException { + int count = 0; + for (int i = 0; i < 20; i++) { + Node child = testRootNode.addNode("node" + i); + for (int j = 0; j < 100; j++) { + Node n = child.addNode("node" + j); + n.setProperty("foo", count++); + } + // save every 100 nodes + testRootNode.save(); + } + + QueryManager qm = superuser.getWorkspace().getQueryManager(); + String stmt = testPath + "//*[@foo >= 0]"; + QueryResult res = qm.createQuery(stmt, Query.XPATH).execute(); + checkResult(res, 2000); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/MassiveWildcardTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/MassiveWildcardTest.java new file mode 100644 index 00000000000..965be41ec04 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/MassiveWildcardTest.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.core.integration; + +import org.apache.jackrabbit.core.query.AbstractQueryTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +/** + * Tests if a wildcard query with a lot of results does not throw an error. + */ +public class MassiveWildcardTest extends AbstractQueryTest { + + /** + * Executes a wildcard query covering 2'000 different property values. + */ + public void testWildcardQuery() throws RepositoryException { + int count = 0; + for (int i = 0; i < 20; i++) { + Node child = testRootNode.addNode("node" + i); + for (int j = 0; j < 100; j++) { + Node n = child.addNode("node" + j); + n.setProperty("foo", "" + count + "foo"); + n.setProperty("bar", "bar" + count++); + } + // save every 100 nodes + testRootNode.save(); + } + + QueryManager qm = superuser.getWorkspace().getQueryManager(); + String stmt = testPath + "//*[jcr:contains(., '*foo')]"; + QueryResult res = qm.createQuery(stmt, Query.XPATH).execute(); + checkResult(res, 2000); + + stmt = testPath + "//*[jcr:contains(., 'bar*')]"; + res = qm.createQuery(stmt, Query.XPATH).execute(); + checkResult(res, 2000); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/NodeImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/NodeImplTest.java new file mode 100644 index 00000000000..1e42128bd36 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/NodeImplTest.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.core.integration; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Integration tests for the Node implementation in Jackrabbit core. + */ +public class NodeImplTest extends AbstractJCRTest { + + private Node node; + + protected void setUp() throws Exception { + super.setUp(); + node = testRootNode.addNode("testNodeImpl", "nt:unstructured"); + testRootNode.save(); + } + + protected void tearDown() throws Exception { + node.remove(); + testRootNode.save(); + super.tearDown(); + } + + /** + * Test case for JCR-1389. + * + * @see JCR-1389 + */ + public void testSetEmptyMultiValueProperty() throws RepositoryException { + Property property = + node.setProperty("test", new Value[0], PropertyType.LONG); + assertEquals( + "JCR-1389: setProperty(name, new Value[0], PropertyType.LONG)" + + " loses property type", + PropertyType.LONG, property.getType()); + } + + /** + * Test case for JCR-1227. + * + * @see JCR-1227 + */ + public void testRestoreEmptyMultiValueProperty() throws Exception { + node.addMixin("mix:versionable"); + node.setProperty("test", new Value[0], PropertyType.LONG); + node.save(); + assertEquals(PropertyType.LONG, node.getProperty("test").getType()); + + Version version = node.checkin(); + assertEquals(PropertyType.LONG, node.getProperty("test").getType()); + + node.restore(version, false); + assertEquals( + "JCR-1227: Restore of empty multivalue property always" + + " changes property type to String", + PropertyType.LONG, node.getProperty("test").getType()); + + node.checkout(); + node.setProperty("test", new Value[0], PropertyType.BOOLEAN); + node.save(); + assertEquals(PropertyType.BOOLEAN, node.getProperty("test").getType()); + + node.restore(version, false); + assertEquals( + "JCR-1227: Restore of empty multivalue property always" + + " changes property type to String", + PropertyType.LONG, node.getProperty("test").getType()); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RepositoryFactoryImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RepositoryFactoryImplTest.java new file mode 100644 index 00000000000..3d87ecd573e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RepositoryFactoryImplTest.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.core.integration; + +import java.io.File; +import java.util.Map; +import java.util.HashMap; +import java.util.Hashtable; + +import javax.jcr.RepositoryException; +import javax.jcr.Repository; +import javax.naming.InitialContext; +import javax.naming.Context; + +import javax.jcr.RepositoryFactory; +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.commons.JndiRepositoryFactory; +import org.apache.jackrabbit.core.RepositoryFactoryImpl; +import org.apache.jackrabbit.core.jndi.provider.DummyInitialContextFactory; +import org.apache.jackrabbit.core.jndi.RegistryHelper; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * RepositoryFactoryImplTest performs tests on + * {@link RepositoryFactoryImpl}. + */ +public class RepositoryFactoryImplTest extends AbstractJCRTest { + + private static final File TARGET = new File("target"); + + private static final File REPO_HOME = new File(TARGET, "repository-factory-test"); + + private static final File REPO_CONF = new File(new File(TARGET, "repository"), "repository.xml"); + + private Repository repo; + + /** + * Makes sure the repository is shutdown. + */ + protected void tearDown() throws Exception { + if (repo instanceof JackrabbitRepository) { + ((JackrabbitRepository) repo).shutdown(); + } + super.tearDown(); + } + + /** + * Checks if a default repository is returned. + * + * @throws RepositoryException if an error occurs. + */ + public void testDefaultRepository() throws RepositoryException { + setProperties(); + repo = JcrUtils.getRepository(); + checkRepository(repo); + } + + /** + * Checks if a repository is returned for the given parameters. + * + * @throws RepositoryException if an error occurs. + */ + public void testWithParameters() throws RepositoryException { + resetProperties(); + repo = JcrUtils.getRepository(getParamters()); + checkRepository(repo); + } + + /** + * Checks if multiple {@link RepositoryFactory#getRepository(Map)} calls + * return the same repository instance. + * + * @throws RepositoryException if an error occurs. + */ + public void testMultipleConnect() throws RepositoryException { + setProperties(); + repo = JcrUtils.getRepository(); + checkRepository(repo); + assertSame(repo, JcrUtils.getRepository()); + } + + /** + * Checks if multiple {@link RepositoryFactory#getRepository(Map)} calls + * return the same repository instance. + * + * @throws RepositoryException if an error occurs. + */ + public void testMultipleConnectWithParameters() throws RepositoryException { + resetProperties(); + repo = JcrUtils.getRepository(getParamters()); + checkRepository(repo); + assertSame(repo, JcrUtils.getRepository(getParamters())); + } + + /** + * Checks if {@link RepositoryFactory#getRepository(Map)} returns a repository + * that can be used even after a previously retrieved repository had been + * {@link JackrabbitRepository#shutdown() shutdown}. + * + * @throws RepositoryException if an error occurs. + */ + public void testShutdown() throws RepositoryException { + setProperties(); + for (int i = 0; i < 2; i++) { + repo = JcrUtils.getRepository(); + checkRepository(repo); + ((JackrabbitRepository) repo).shutdown(); + } + } + + /** + * Checks if a repository can be obtained by specifying JNDI parameters + * on {@link RepositoryFactory#getRepository(Map)}. + * + * @throws Exception if an error occurs. + */ + public void testJNDI() throws Exception { + String name = "jackrabbit-repository"; + Map parameters = new HashMap(); + parameters.put(Context.INITIAL_CONTEXT_FACTORY, DummyInitialContextFactory.class.getName()); + parameters.put(Context.PROVIDER_URL, "localhost"); + InitialContext context = new InitialContext(new Hashtable(parameters)); + RegistryHelper.registerRepository(context, name, + REPO_CONF.getAbsolutePath(), REPO_HOME.getAbsolutePath(), false); + try { + parameters.put(JndiRepositoryFactory.JNDI_NAME, name); + repo = JcrUtils.getRepository(parameters); + checkRepository(repo); + } finally { + RegistryHelper.unregisterRepository(context, name); + } + } + + //-------------------------< internal helper >------------------------------ + + private void checkRepository(Repository r) throws RepositoryException { + r.login(getHelper().getSuperuserCredentials()).logout(); + } + + private static void setProperties() { + System.setProperty(RepositoryFactoryImpl.REPOSITORY_HOME, REPO_HOME.getAbsolutePath()); + System.setProperty(RepositoryFactoryImpl.REPOSITORY_CONF, REPO_CONF.getAbsolutePath()); + } + + private static void resetProperties() { + System.setProperty(RepositoryFactoryImpl.REPOSITORY_HOME, ""); + System.setProperty(RepositoryFactoryImpl.REPOSITORY_CONF, ""); + } + + private static Map getParamters() { + Map params = new HashMap(); + params.put(RepositoryFactoryImpl.REPOSITORY_HOME, REPO_HOME.getAbsolutePath()); + params.put(RepositoryFactoryImpl.REPOSITORY_CONF, REPO_CONF.getAbsolutePath()); + return params; + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RepositoryLockTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RepositoryLockTest.java new file mode 100644 index 00000000000..bf99c4e2c55 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RepositoryLockTest.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.core.integration; + +import java.io.File; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.RepositoryImpl; + +import junit.framework.TestCase; + +/** + * RepositoryLockTest checks if multiple instatiations of a + * repository are prevented and the repository lock file is respected (i.e. not + * deleted). See also JCR-2057. + */ +public class RepositoryLockTest extends TestCase { + + private static final File TARGET = new File("target"); + + private static final File REPO_HOME = new File(TARGET, "repository-lock-test"); + + private static final File REPO_CONF = new File(new File(TARGET, "repository"), "repository.xml"); + + private RepositoryImpl repo; + + /** + * Makes sure the repository is shutdown. + */ + protected void tearDown() throws Exception { + if (repo != null) { + repo.shutdown(); + repo = null; + } + super.tearDown(); + } + + public void testMultipleInstantiation() throws Exception { + RepositoryConfig config = RepositoryConfig.create( + REPO_CONF.getAbsolutePath(), REPO_HOME.getAbsolutePath()); + repo = RepositoryImpl.create(config); + + for (int i = 0; i < 3; i++) { + // try again + try { + repo = RepositoryImpl.create(config); + fail("Multiple instantiation must not be possible"); + } catch (RepositoryException e) { + // expected + } + // check if lock file is still there, see JCR-2057 + assertTrue("repository lock file deleted", new File(REPO_HOME, ".lock").exists()); + } + } +} + + diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RestoreSameNameSiblingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RestoreSameNameSiblingTest.java new file mode 100644 index 00000000000..e0427e9824f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/RestoreSameNameSiblingTest.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.core.integration; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test case for JCR-43. + * + * @see JCR-43 + */ +public class RestoreSameNameSiblingTest extends AbstractJCRTest { + + public void testRestoreSNS() throws RepositoryException { + Session session = getHelper().getSuperuserSession(); + + // - Create a node 'a' with nodetype nt:unstructured + // (defining it's childnodes to show OPV Version behaviour) + Node node = session.getRootNode().addNode("RestoreSameNameSiblingTest"); + try { + // - Create a child node 'b' + node.addNode("test"); + // - Make 'a' versionable (add mixin mix:versionable) + node.addMixin(mixVersionable); + session.save(); + + // - Checkin/Checkout 'a' + Version version = node.checkin(); + node.checkout(); + assertEquals(1, node.getNodes("test").getSize()); + + // - Restore any version of 'a' + node.restore(version, true); + assertEquals(1, node.getNodes("test").getSize()); + } finally { + node.remove(); + session.save(); + session.logout(); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/SessionImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/SessionImplTest.java new file mode 100644 index 00000000000..4b07c03c94d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/SessionImplTest.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.core.integration; + +import java.security.AccessControlException; + +import javax.jcr.Credentials; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * Integration tests for the Session implementation in Jackrabbit core. + */ +public class SessionImplTest extends AbstractJCRTest { + + /** + * JCR-1731: + * Session.checkPermission("/", "add_node") throws PathNotFoundException + * instead of AccessControlException + */ + public void testCheckAddNodePermissionOnRoot() throws RepositoryException { + Session session = getHelper().getReadOnlySession(); + try { + session.checkPermission("/", "add_node"); + } catch (PathNotFoundException e) { + fail("JCR-1731: Session.checkPermission(\"/\", \"add_node\")" + + " throws PathNotFoundException instead of" + + " AccessControlException"); + } catch (AccessControlException e) { + // expected + } finally { + session.logout(); + } + } + + /** + * JCR-1932: Session.getAttributes( ) call always returns an empty array + * + * @see JCR-1932 + */ + public void testSessionAttributes() throws RepositoryException { + SimpleCredentials credentials = + new SimpleCredentials("admin", "admin".toCharArray()); + credentials.setAttribute("test", "attribute"); + Session session = getHelper().getRepository().login(credentials); + try { + String[] names = session.getAttributeNames(); + assertEquals(1, names.length); + assertEquals("test", names[0]); + assertEquals("attribute", session.getAttribute("test")); + } finally { + session.logout(); + } + } + + /** + * JCR-2595: SessionImpl.createSession uses same Subject/LoginContext + * + * @see JCR-2595 + */ + public void testCreateSession() throws RepositoryException, NotExecutableException { + if (!(superuser instanceof SessionImpl)) { + throw new NotExecutableException(); + } + + String currentWsp = superuser.getWorkspace().getName(); + String otherWsp = null; + for (String wsp : superuser.getWorkspace().getAccessibleWorkspaceNames()) { + if (!wsp.equals(currentWsp)) { + otherWsp = wsp; + break; + } + } + + SessionImpl sImpl = (SessionImpl) superuser; + Subject subject = sImpl.getSubject(); + + Session s1 = sImpl.createSession(currentWsp); + try { + assertFalse(s1 == sImpl); + assertFalse(subject == ((SessionImpl) s1).getSubject()); + assertEquals(subject, ((SessionImpl) s1).getSubject()); + assertEquals(currentWsp, s1.getWorkspace().getName()); + } finally { + s1.logout(); + assertFalse(subject.getPrincipals().isEmpty()); + assertFalse(subject.getPublicCredentials().isEmpty()); + } + + + Session s2 = sImpl.createSession(otherWsp); + try { + assertFalse(s2 == sImpl); + assertFalse(subject == ((SessionImpl) s2).getSubject()); + assertEquals(subject, ((SessionImpl) s2).getSubject()); + assertEquals(otherWsp, s2.getWorkspace().getName()); + } finally { + s2.logout(); + assertFalse(subject.getPrincipals().isEmpty()); + assertFalse(subject.getPublicCredentials().isEmpty()); + } + + Session s3 = sImpl.createSession(null); + try { + assertFalse(s3 == sImpl); + assertFalse(subject == ((SessionImpl) s3).getSubject()); + assertEquals(subject, ((SessionImpl) s3).getSubject()); + assertEquals(((RepositoryImpl) sImpl.getRepository()).getConfig().getDefaultWorkspaceName(), s3.getWorkspace().getName()); + } finally { + s3.logout(); + assertFalse(subject.getPrincipals().isEmpty()); + assertFalse(subject.getPublicCredentials().isEmpty()); + } + } + + /** + * JCR-2895 : SessionImpl#getSubject() should return an unmodifiable subject + * + * @see JCR-2895 + */ + public void testGetSubject() { + Subject subject = ((SessionImpl) superuser).getSubject(); + + assertFalse(subject.getPublicCredentials().isEmpty()); + assertFalse(subject.getPublicCredentials(Credentials.class).isEmpty()); + assertFalse(subject.getPrincipals().isEmpty()); + + assertTrue(subject.isReadOnly()); + try { + subject.getPublicCredentials().add(new SimpleCredentials("test", new char[0])); + fail("Subject expected to be readonly"); + } catch (IllegalStateException e) { + // success + } + try { + subject.getPrincipals().add(new PrincipalImpl("test")); + fail("Subject expected to be readonly"); + } catch (IllegalStateException e) { + // success + } + } + + /** + * JCR-3014 Identifier paths for inexistent items throw exception + * + * @see JCR-3014 + */ + public void testCheckNonExistingItem() throws Exception { + String dummyPath = "[" + NodeId.randomId() + "]"; + assertFalse(superuser.itemExists(dummyPath)); + assertFalse(superuser.nodeExists(dummyPath)); + } + + /** + * @see JCR-3885 + */ + public void testSessionHasPermission() throws Exception { + JackrabbitSession js = (JackrabbitSession) superuser; + + assertEquals(superuser.hasPermission("/", Session.ACTION_READ), js.hasPermission("/", new String[] {Session.ACTION_READ})); + assertEquals(superuser.hasPermission("/", Session.ACTION_READ + "," + Session.ACTION_ADD_NODE) , js.hasPermission("/", Session.ACTION_READ, Session.ACTION_ADD_NODE)); + + try { + js.hasPermission("/", new String[0]); + fail(); + } catch (IllegalArgumentException e) { + // success + } + + try { + js.hasPermission("/", new String[] {""}); + fail(); + } catch (IllegalArgumentException e) { + // success + } + + // note: that's a bit unexpected + assertEquals(superuser.hasPermission("/", ",,"), js.hasPermission("/", "", "", "")); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/TreeTraverserTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/TreeTraverserTest.java new file mode 100644 index 00000000000..560548b07c8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/TreeTraverserTest.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.jackrabbit.core.integration; + +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.filterIterator; +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.singleton; +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.transformIterator; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.flat.TreeTraverser; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; +import org.apache.jackrabbit.spi.commons.iterator.Predicate; +import org.apache.jackrabbit.spi.commons.iterator.Transformer; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import junit.framework.AssertionFailedError; + +public class TreeTraverserTest extends AbstractJCRTest { + private Node testNode; + + private final TreeTraverser.ErrorHandler errorHandler = new TreeTraverser.ErrorHandler() { + public void call(Item item, RepositoryException exception) { + throw (AssertionFailedError) new AssertionFailedError().initCause(exception); + } + }; + + private final List nodes = Arrays.asList( + "", + "a", + "a/b", + "a/a", + "a/c", + "b", + "c", + "d", + "d/a", + "d/a/b", + "d/a/b/c", + "d/a/b/c/d", + "d/a/b/c/d/e", + "d/a/b/c/d/e/f", + "e" + ); + + private final Set properties = new HashSet() {{ + add("v"); + add("w"); + add("x"); + add("a/v"); + add("a/w"); + add("a/x"); + add("a/a/a"); + add("d/a/b/c/d/e/f/q"); + }}; + + private final List leaves = new LinkedList(); + + @Override + public void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode("TreeTraverserTest", NodeType.NT_UNSTRUCTURED); + superuser.save(); + + // Determine leave nodes + outer: + for (String node : nodes) { + for (String other : nodes) { + if (other.startsWith(node) && !other.equals(node)) { + continue outer; + } + } + leaves.add(node); + } + } + + public void testTraverseNodesEmpty() throws RepositoryException { + TreeTraverser traverser = new TreeTraverser(testNode, errorHandler, TreeTraverser.InclusionPolicy.ALL); + checkNodes(traverser, singleton(""), testNode.getPath()); + } + + public void testTraverseLeavesEmpty() throws RepositoryException { + TreeTraverser traverser = new TreeTraverser(testNode, errorHandler, TreeTraverser.InclusionPolicy.LEAVES); + checkNodes(traverser, singleton(""), testNode.getPath()); + } + + public void testTraverseNodes() throws RepositoryException { + addNodes(testNode, nodes); + TreeTraverser traverser = new TreeTraverser(testNode, errorHandler, TreeTraverser.InclusionPolicy.ALL); + checkNodes(traverser, nodes.iterator(), testNode.getPath()); + } + + public void testTraverseLeaves() throws RepositoryException { + addNodes(testNode, nodes); + TreeTraverser traverser = new TreeTraverser(testNode, errorHandler, TreeTraverser.InclusionPolicy.LEAVES); + checkNodes(traverser, leaves.iterator(), testNode.getPath()); + } + + public void testTraversePropertiesEmpty() throws RepositoryException { + Iterator nodeIt = TreeTraverser.nodeIterator(testNode); + Iterator propertyIt = TreeTraverser.propertyIterator(nodeIt); + checkProperties(propertyIt, Iterators.empty(), testNode.getPath()); + + addNodes(testNode, nodes); + checkProperties(propertyIt, Iterators.empty(), testNode.getPath()); + } + + public void testTraverseProperties() throws RepositoryException { + addNodes(testNode, nodes); + addProperties(testNode, properties); + Iterator nodeIt = TreeTraverser.nodeIterator(testNode); + Iterator propertyIt = TreeTraverser.propertyIterator(nodeIt); + + checkProperties(propertyIt, properties.iterator(), testNode.getPath()); + } + + // -----------------------------------------------------< internal >--- + + private static void addNodes(Node root, Iterable nodes) throws RepositoryException { + for (String name : nodes) { + if (!"".equals(name)) { + root.addNode(name); + } + } + root.getSession().save(); + } + + private static void addProperties(Node root, Iterable properties) throws RepositoryException { + for (String name : properties) { + int i = name.lastIndexOf('/'); + Node n; + String propName; + if (i <= 0) { + n = root; + propName = name; + } + else { + n = root.getNode(name.substring(0, i)); + propName = name.substring(i + 1); + } + n.setProperty(propName, propName + " value"); + } + root.getSession().save(); + } + + private static void checkNodes(TreeTraverser treeTraverser, Iterator expectedNodes, String pathPrefix) + throws RepositoryException { + + for (Node node : treeTraverser) { + assertTrue("No more nodes. Expected " + node, expectedNodes.hasNext()); + assertEquals(cat(pathPrefix, expectedNodes.next()), node.getPath()); + } + + assertFalse(expectedNodes.hasNext()); + } + + private static void checkProperties(Iterator actualProperties, + Iterator expectedProperties, String pathPrefix) { + + List actualPaths = toList(property2Path(removeIgnored(actualProperties))); + List expectedPaths = toList(cat(pathPrefix, expectedProperties)); + + Collections.sort(actualPaths); + Collections.sort(expectedPaths); + + assertEquals(expectedPaths, actualPaths); + } + + private static Iterator removeIgnored(Iterator properties) { + return filterIterator(properties, new Predicate() { + public boolean evaluate(Property property) { + try { + return !JcrConstants.JCR_PRIMARYTYPE.equals(property.getName()); + } + catch (RepositoryException e) { + throw (AssertionFailedError) new AssertionFailedError().initCause(e); + } + } + }); + } + + private static Iterator property2Path(Iterator properties) { + return transformIterator(properties, new Transformer() { + public String transform(Property property) { + try { + return property.getPath(); + } + catch (RepositoryException e) { + throw (AssertionFailedError) new AssertionFailedError().initCause(e); + } + } + }); + } + + private static List toList(Iterator iterator) { + List list = new LinkedList(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + return list; + } + + private static String cat(String s1, String s2) { + if ("".equals(s2)) { + return s1; + } + else { + return s1 + "/" + s2; + } + } + + private static Iterator cat(final String s, Iterator strings) { + return transformIterator(strings, new Transformer() { + public String transform(String string) { + return cat(s, string); + } + }); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/UtilsGetPathTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/UtilsGetPathTest.java new file mode 100755 index 00000000000..2c7aa3fccaa --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/UtilsGetPathTest.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.core.integration; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * @see JCR-3992 + * and + * JCR-4015 + */ +public class UtilsGetPathTest extends AbstractJCRTest { + + @Test + public void testGetOrCreateByPath1() throws RepositoryException { + String path = testRoot + "/foo"; + Node node = JcrUtils.getOrCreateByPath(path, "nt:unstructured", superuser); + superuser.save(); + assertEquals(path, node.getPath()); + assertTrue(superuser.nodeExists(path)); + + // existing top-level node, two new descendant nodes + String path2 = testRoot + "/foo/a/b"; + Node node2 = JcrUtils.getOrCreateByPath(path2, "nt:unstructured", superuser); + superuser.save(); + assertEquals(path2, node2.getPath()); + assertTrue(superuser.nodeExists(path2)); + } + + @Test + public void testGetOrCreateByPathNoRoot() throws RepositoryException { + String base = testRoot + "/foo"; + Node inter = JcrUtils.getOrCreateByPath(base, "nt:unstructured", superuser); + assertEquals(base, inter.getPath()); + superuser.save(); + + // test what happens if getRootNode() throws + Session mockedSession = Mockito.spy(superuser); + Mockito.when(mockedSession.getRootNode()).thenThrow(new AccessDeniedException("access denied")); + Mockito.when(mockedSession.getNode("/")).thenThrow(new AccessDeniedException("access denied")); + Mockito.when(mockedSession.getItem("/")).thenThrow(new AccessDeniedException("access denied")); + Mockito.when(mockedSession.nodeExists("/")).thenReturn(false); + + Node result = JcrUtils.getOrCreateByPath(base + "/bar", false, null, null, mockedSession, false); + mockedSession.save(); + assertEquals(base + "/bar", result.getPath()); + + // already exists -> nop + Node result2 = JcrUtils.getOrCreateByPath(base + "/bar", false, null, null, mockedSession, false); + mockedSession.save(); + assertEquals(base + "/bar", result2.getPath()); + + // create unique + Node result3 = JcrUtils.getOrCreateByPath(base + "/bar", true, null, null, mockedSession, false); + mockedSession.save(); + assertEquals(base + "/bar0", result3.getPath()); + + // already exists with createUnique == false should pass even when parent isn't readable + Mockito.when(mockedSession.getNode(base)).thenThrow(new AccessDeniedException("access denied")); + Mockito.when(mockedSession.getItem(base)).thenThrow(new AccessDeniedException("access denied")); + Mockito.when(mockedSession.nodeExists(base)).thenReturn(false); + Node result4 = JcrUtils.getOrCreateByPath(base + "/bar", false, null, null, mockedSession, false); + mockedSession.save(); + assertEquals(base + "/bar", result4.getPath()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/VersioningTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/VersioningTest.java new file mode 100644 index 00000000000..714927ca754 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/VersioningTest.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.core.integration; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Versioning tests. + */ +public class VersioningTest extends AbstractJCRTest { + + private Node n1; + private Node n2; + + protected void setUp() throws Exception { + super.setUp(); + + Session s1 = getHelper().getSuperuserSession(); + n1 = s1.getRootNode().addNode("VersioningTest"); + n1.addMixin(mixVersionable); + n1.getSession().save(); + + Session s2 = getHelper().getSuperuserSession(workspaceName); + s2.getWorkspace().clone( + s1.getWorkspace().getName(), n1.getPath(), + "/VersioningTest", true); + n2 = s2.getRootNode().getNode("VersioningTest"); + } + + protected void tearDown() throws Exception { + super.tearDown(); + + Session s1 = n1.getSession(); + n1.remove(); + s1.save(); + s1.logout(); + + Session s2 = n2.getSession(); + n2.remove(); + s2.save(); + s2.logout(); + } + + /** + * Tests that the version tree documented in + * AbstractVersionManager.calculateCheckinVersionName() can be + * constructed and has the expected version names. + *

    + * Note that this test case needs to be modified if the version naming + * algorithm ever gets changed. + */ + public void testVersionGraph() throws Exception { + Version vR = n1.getBaseVersion(); + + Version v10 = n1.checkin(); + n1.checkout(); + Version v11 = n1.checkin(); + n1.checkout(); + Version v12 = n1.checkin(); + n1.checkout(); + Version v13 = n1.checkin(); + n1.checkout(); + Version v14 = n1.checkin(); + n1.checkout(); + Version v15 = n1.checkin(); + n1.checkout(); + Version v16 = n1.checkin(); + + n1.restore(v12, true); + n1.checkout(); + Version v120 = n1.checkin(); + n1.checkout(); + Version v121 = n1.checkin(); + n1.checkout(); + Version v122 = n1.checkin(); + + n1.restore(v12, true); + n1.checkout(); + Version v1200 = n1.checkin(); + + n1.restore(v121, true); + n1.checkout(); + Version v1210 = n1.checkin(); + n1.checkout(); + Version v1211 = n1.checkin(); + + // x.0 versions can be created if the newly created versionable + // node is cloned to another workspace before the first checkin + Version v20 = n2.checkin(); + + // Multiple branches can be merged using multiple workspaces + n2.restore(v122, true); + n1.restore(v16, true); + n1.checkout(); + n1.merge(n2.getSession().getWorkspace().getName(), true); + n1.doneMerge(v122); + Version v17 = n1.checkin(); + + assertEquals("jcr:rootVersion", vR.getName()); + assertPredecessors("", vR); + assertSuccessors("1.0 2.0", vR); + assertEquals("1.0", v10.getName()); + assertPredecessors("jcr:rootVersion", v10); + assertSuccessors("1.1", v10); + assertEquals("1.1", v11.getName()); + assertPredecessors("1.0", v11); + assertSuccessors("1.2", v11); + assertEquals("1.2", v12.getName()); + assertPredecessors("1.1", v12); + assertSuccessors("1.3 1.2.0 1.2.0.0", v12); + assertEquals("1.3", v13.getName()); + assertPredecessors("1.2", v13); + assertSuccessors("1.4", v13); + assertEquals("1.4", v14.getName()); + assertPredecessors("1.3", v14); + assertSuccessors("1.5", v14); + assertEquals("1.5", v15.getName()); + assertPredecessors("1.4", v15); + assertSuccessors("1.6", v15); + assertEquals("1.6", v16.getName()); + assertPredecessors("1.5", v16); + assertSuccessors("1.7", v16); + assertEquals("1.7", v17.getName()); + assertPredecessors("1.6 1.2.2", v17); + assertSuccessors("", v17); + + assertEquals("1.2.0", v120.getName()); + assertPredecessors("1.2", v120); + assertSuccessors("1.2.1", v120); + assertEquals("1.2.1", v121.getName()); + assertPredecessors("1.2.0", v121); + assertSuccessors("1.2.2 1.2.1.0", v121); + assertEquals("1.2.2", v122.getName()); + assertPredecessors("1.2.1", v122); + assertSuccessors("1.7", v122); + + assertEquals("1.2.0.0", v1200.getName()); + assertPredecessors("1.2", v1200); + assertSuccessors("", v1200); + + assertEquals("1.2.1.0", v1210.getName()); + assertPredecessors("1.2.1", v1210); + assertSuccessors("1.2.1.1", v1210); + assertEquals("1.2.1.1", v1211.getName()); + assertPredecessors("1.2.1.0", v1211); + assertSuccessors("", v1211); + + assertEquals("2.0", v20.getName()); + assertPredecessors("jcr:rootVersion", v20); + assertSuccessors("", v20); + } + + private void assertPredecessors(String expected, Version version) + throws Exception { + Set predecessors = new HashSet(); + if (expected.length() > 0) { + predecessors.addAll(Arrays.asList(expected.split(" "))); + } + Version[] versions = version.getPredecessors(); + for (int i = 0; i < versions.length; i++) { + if (!predecessors.remove(versions[i].getName())) { + fail("Version " + version.getName() + + " has an unexpected predessor " + + versions[i].getName()); + } + } + if (!predecessors.isEmpty()) { + fail("Version " + version.getName() + + " does not have all expected predecessors"); + } + } + + private void assertSuccessors(String expected, Version version) + throws Exception { + Set successors = new HashSet(); + if (expected.length() > 0) { + successors.addAll(Arrays.asList(expected.split(" "))); + } + Version[] versions = version.getSuccessors(); + for (int i = 0; i < versions.length; i++) { + if (!successors.remove(versions[i].getName())) { + fail("Version " + version.getName() + + " has an unexpected successor " + + versions[i].getName()); + } + } + if (!successors.isEmpty()) { + fail("Version " + version.getName() + + " does not have all expected successors"); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/WorkspaceInitTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/WorkspaceInitTest.java new file mode 100644 index 00000000000..d3460037aed --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/WorkspaceInitTest.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.core.integration; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.core.query.lucene.SlowQueryHandler; + +/** + * WorkspaceInitTest... + */ +public class WorkspaceInitTest extends AbstractJCRTest { + + protected void setUp() throws Exception { + super.setUp(); + SlowQueryHandler.setInitializationDelay(10 * 1000); + } + + protected void tearDown() throws Exception { + SlowQueryHandler.setInitializationDelay(0); + super.tearDown(); + } + + public void testIdleTime() throws Exception { + // simply access the workspace, which will cause + // initialization of SlowQueryHandler. + List threads = new ArrayList(); + for (int i = 0; i < 10; i++) { + Thread t = new Thread(new Runnable() { + public void run() { + try { + getHelper().getSuperuserSession("wsp-init-test").logout(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + }); + t.start(); + threads.add(t); + } + for (Iterator it = threads.iterator(); it.hasNext(); ) { + ((Thread) it.next()).join(); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/benchmark/ItemStateCacheSyncTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/benchmark/ItemStateCacheSyncTest.java new file mode 100644 index 00000000000..e81e6e27b7f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/benchmark/ItemStateCacheSyncTest.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.core.integration.benchmark; + +import java.io.File; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.TransientRepository; + +/** + * Test case for + * JCR-2546. + * Note that this test takes a long time to finish and does not contain + * normal assertions, so it should only be invoked explicitly instead of + * being included in the normal test suite. + */ +public class ItemStateCacheSyncTest extends TestCase { + + private File directory; + + private Repository repository; + + private Session session; + + private Node root; + + private volatile boolean run; + + private AtomicLong counter = new AtomicLong(); + + protected void setUp() throws Exception { + directory = new File("target", "jackrabbit-sync-test-repo"); + + repository = new TransientRepository(directory); + + session = repository.login( + new SimpleCredentials("admin", "admin".toCharArray())); + + // Add a tree of 100k medium-sized (~10kB) nodes + String[] data = new String[1000]; + Arrays.fill(data, "something"); + root = session.getRootNode(); + for (int i = 0; i < 1000; i++) { + Node a = root.addNode("a" + i); + for (int j = 0; j < 100; j++) { + Node b = a.addNode("b" + j); + b.setProperty("data", data); + } + session.save(); + System.out.println((i + 1) * 1000 + " nodes created"); + } + } + + protected void tearDown() throws Exception { + for (int i = 0; i < 1000; i++) { + root.getNode("a" + i).remove(); + session.save(); + } + session.logout(); + } + + public void testCacheSync() throws Exception { + run = true; + Thread[] threads = new Thread[30]; + for (int i = 0; i < threads.length; i++) { + threads[i] = new Thread(new Lookup()); + threads[i].start(); + } + + // time for all threads to start and caches to warm up + Thread.sleep(3000); + + long start, stop, count; + + counter.set(0); + start = System.currentTimeMillis(); + Thread.sleep(10000); + count = counter.get(); + stop = System.currentTimeMillis(); + System.out.println( + count * 1000 / (stop - start) + " lookups per second"); + + counter.set(0); + start = System.currentTimeMillis(); + int i = 0; + do { + Node node = root.getNode("a" + (i++) % 1000); + for (Node ignore : JcrUtils.getChildNodes(node)) { + } + count = counter.get(); + stop = System.currentTimeMillis(); + } while (stop < start + 10000); + System.out.println( + count * 1000 / (stop - start) + " lookups per second" + + " while traversing " + (i * 1000 * 1000) / (stop - start) + + " nodes per second"); + + run = false; + for (i = 0; i < threads.length; i++) { + threads[i].join(); + } + } + + public class Lookup implements Runnable { + + public void run() { + try { + Session session = repository.login(); + try { + while (run) { + for (int i = 0; i < 100; i++) { + if (session.nodeExists("/a" + i)) { + session.getNode("/a" + 1); + } + counter.incrementAndGet(); + } + } + } finally { + session.logout(); + } + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + + } + + public class Traverse implements ItemVisitor { + + public void visit(Property property) { + } + + public void visit(Node node) throws RepositoryException { + for (Node child : JcrUtils.getChildNodes(node)) { + child.accept(this); + } + } + + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/benchmark/SimpleBench.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/benchmark/SimpleBench.java new file mode 100644 index 00000000000..06fd6180ad1 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/benchmark/SimpleBench.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.integration.benchmark; + +import java.io.File; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.TransientRepository; + +/** + * A simple benchmark application for Jackrabbit. + */ +public class SimpleBench { + + int run; + long start; + Repository repository; + + public static void main(String[] args) throws Exception { + for (int i = 0; i < 5; i++) { + new SimpleBench().test(i); + } + } + + void start() { + start = System.currentTimeMillis(); + } + + void end(String message) { + long time = System.currentTimeMillis() - start; + if (run > 0) { + System.out.println("run: " + run + "; time: " + time + " ms; task: " + message); + } + } + + void test(int run) throws Exception { + this.run = run; + new File("target/jcr.log").delete(); + FileUtils.deleteQuietly(new File("repository")); + + start(); + repository = new TransientRepository(); + Session session = repository.login(new SimpleCredentials("", "".toCharArray())); + if (session.getRootNode().hasNode("test")) { + session.getRootNode().getNode("test").remove(); + session.save(); + } + session.getRootNode().addNode("test"); + session.save(); + end("init"); + Node node = session.getRootNode().getNode("test"); + Node n = null; + int len = run == 0 ? 100 : 1000; + start(); + for (int i = 0; i < len; i++) { + if (i % 100 == 0) { + n = node.addNode("sub" + i); + } + Node x = n.addNode("x" + (i % 100)); + x.setProperty("name", "John"); + x.setProperty("firstName", "Doe"); + session.save(); + } + end("addNodes"); + session.logout(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/DailyIntegrationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/DailyIntegrationTest.java new file mode 100644 index 00000000000..d07a7d54324 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/DailyIntegrationTest.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.core.integration.daily; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import org.apache.jackrabbit.core.ConcurrencyTest; +import org.apache.jackrabbit.core.ConcurrentAddMoveRemoveTest; +import org.apache.jackrabbit.core.ConcurrentCheckinMixedTransactionTest; +import org.apache.jackrabbit.core.ConcurrentLoginTest; +import org.apache.jackrabbit.core.ConcurrentNodeModificationTest; +import org.apache.jackrabbit.core.ConcurrentReadWriteTest; +import org.apache.jackrabbit.core.ConcurrentSaveTest; +import org.apache.jackrabbit.core.ConcurrentVersioningTest; +import org.apache.jackrabbit.core.ConcurrentVersioningWithTransactionsTest; +import org.apache.jackrabbit.core.LockTest; +import org.apache.jackrabbit.core.ReadVersionsWhileModified; +import org.apache.jackrabbit.core.integration.ConcurrentQueriesWithUpdatesTest; +import org.apache.jackrabbit.core.query.lucene.LargeResultSetTest; +import org.apache.jackrabbit.core.lock.ConcurrentLockingTest; +import org.apache.jackrabbit.core.lock.ConcurrentLockingWithTransactionsTest; + +/** + * Contains tests that are run on a daily basis. + */ +public class DailyIntegrationTest extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("Daily integration tests"); + + // random operation test + suite.addTestSuite(RandomOperationTest.class); + + // multi-threading tests + suite.addTestSuite(ConcurrencyTest.class); + suite.addTestSuite(ConcurrentLoginTest.class); + suite.addTestSuite(ConcurrentNodeModificationTest.class); + suite.addTestSuite(ConcurrentReadWriteTest.class); + suite.addTestSuite(ConcurrentSaveTest.class); + suite.addTestSuite(ConcurrentVersioningTest.class); + suite.addTestSuite(ConcurrentVersioningWithTransactionsTest.class); + suite.addTestSuite(ConcurrentCheckinMixedTransactionTest.class); + suite.addTestSuite(ConcurrentAddMoveRemoveTest.class); + suite.addTestSuite(LockTest.class); + suite.addTestSuite(ReadVersionsWhileModified.class); + suite.addTestSuite(ConcurrentLockingTest.class); + suite.addTestSuite(ConcurrentLockingWithTransactionsTest.class); + suite.addTestSuite(LargeResultSetTest.class); + suite.addTestSuite(ConcurrentQueriesWithUpdatesTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/ItemStateHierarchyManagerDeadlockTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/ItemStateHierarchyManagerDeadlockTest.java new file mode 100644 index 00000000000..d0c5b063cd3 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/ItemStateHierarchyManagerDeadlockTest.java @@ -0,0 +1,266 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.integration.daily; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +public class ItemStateHierarchyManagerDeadlockTest extends TestCase { + + public int g_numThreads = 30; + + private int g_numRuns = 30; + + private File repoDescriptor; + + private File repoHome; + + private RepositoryImpl repository; + + public void setUp() throws IOException, RepositoryException { + File baseDir = new File(System.getProperty("basedir", ".")); + File repoBaseDir = new File(baseDir, "target/ItemStateHierarchyManagerDeadlockTest"); + FileUtils.deleteQuietly(repoBaseDir); + + repoDescriptor = new File(repoBaseDir, "repository.xml"); + repoHome = new File(repoBaseDir, "repository"); + repoHome.mkdirs(); + + File repositoryDescriptor = new File(baseDir, "src/test/repository/repository.xml"); + FileUtils.copyFile(repositoryDescriptor, repoDescriptor); + } + + public void tearDown() throws IOException, InterruptedException { + FileUtils.deleteQuietly(repoHome.getParentFile()); + } + + private void startRepository() throws RepositoryException { + repository = + RepositoryImpl.create(RepositoryConfig.create(repoDescriptor.getAbsolutePath(), repoHome + .getAbsolutePath())); + } + + private Session login() throws RepositoryException { + return repository.login(new SimpleCredentials("admin", "admin".toCharArray()), null); + } + + private void stopRepository() throws RepositoryException { + repository.shutdown(); + } + + public void testConcurrentWritingSessions() throws Exception { + + startRepository(); + + for (int i = 0; i < g_numRuns; i++) { + clearInvRootNode(); + createInvRootNode(); + runDeadlockTest(); + try { + System.out.println("*** DONE FOR RUN " + (i + 1) + "/" + g_numRuns + + ". SLEEP 1s before running next one"); + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + stopRepository(); + } + + public void clearInvRootNode() { + System.out.println("Clear test repository InventoryTest."); + Session session = null; + try { + session = login(); + Node root = session.getRootNode(); + try { + Node inv = root.getNode("InventoryTest"); + inv.remove(); + session.save(); + } catch (PathNotFoundException pnfx) { + System.err.println(" The root node is not available."); + } + } catch (Exception e) { + System.err.println("Exception in clear test repository:" + e.getMessage()); + e.printStackTrace(); + } finally { + if (session != null) + session.logout(); + } + } + + public void createInvRootNode() throws RepositoryException { + Session session = null; + try { + session = login(); + getInvRootNode(session); + } catch (Exception e) { + System.err.println("Exception in clear test repository:" + e.getMessage()); + e.printStackTrace(); + } finally { + if (session != null) + session.logout(); + } + } + + private Node getInvRootNode(Session session) throws RepositoryException { + Node root = session.getRootNode(); + Node inventoryRoot; + try { + inventoryRoot = root.getNode("InventoryTest"); + } catch (PathNotFoundException pnfx) { + System.err.println(" The root node is not available. So creating new root Node."); + inventoryRoot = root.addNode("InventoryTest"); + session.save(); + } + + return inventoryRoot; + } + + public void createNodesUnderInvRootNode() { + System.out.println("Start createNodesUnderInvRootNode "); + Session session = null; + try { + session = login(); + Node inventoryRoot = getInvRootNode(session); + for (int num = 0; num < 3; num++) { + Node current = inventoryRoot.addNode("Test" + num + "_" + System.currentTimeMillis()); + current.setProperty("profondeur", 123); + current.setProperty("tree", "1"); + current.setProperty("clientid", 1); + current.setProperty("propId", System.currentTimeMillis()); + current.setProperty("name", "Node " + System.currentTimeMillis()); + current.setProperty("address", "1.22.3.3"); + } + session.save(); + System.out.println("End createNodesUnderInvRootNode "); + } catch (Exception e) { + System.err.println("Exception in createNodesUnderInvRootNode:" + e.getMessage()); + } finally { + if (session != null) + session.logout(); + } + } + + public void retrieveNodesUnderInvRootNode() { + System.out.println("Start retrieveNodesUnderInvRootNode "); + Session session = null; + try { + session = login(); + // start from the bottom of the tree and move up + Node inventoryRoot = getInvRootNode(session); + NodeIterator nodes = inventoryRoot.getNodes(); + while (nodes.hasNext()) { + Node node = nodes.nextNode(); + // System.out.println(" Node: " + node.getName()); + PropertyIterator properties = node.getProperties(); + while (properties.hasNext()) { + Property prop = properties.nextProperty(); + // System.out.println(" Prop: " + prop.getName() + " - " + prop.getString()); + } + } + session.save(); + System.out.println("End retrieveNodesUnderInvRootNode"); + } catch (Exception e) { + System.err.println("Exception in retrieveNodesUnderInvRootNode:" + e.getMessage()); + } finally { + if (session != null) + session.logout(); + } + } + + public void removeNodesUnderInvRootNode() { + System.out.println("Start removeNodesUnderInvRootNode "); + Session session = null; + try { + session = login(); + // start from the bottom of the tree and move up + Node inventoryRoot = getInvRootNode(session); + NodeIterator nodes = inventoryRoot.getNodes(); + int num = 0; + while (nodes.hasNext() && num < 3) { + Node node = nodes.nextNode(); + node.remove(); + num++; + } + session.save(); + System.out.println("End removeNodesUnderInvRootNode"); + } catch (Exception e) { + System.err.println("Exception in removeNodesUnderInvRootNode:" + e.getMessage()); + } finally { + if (session != null) + session.logout(); + } + } + + public void runDeadlockTest() { + long start = System.currentTimeMillis(); + List threads = new ArrayList(); + for (int instanceIndex = 0; instanceIndex < g_numThreads; instanceIndex++) { + final int index = instanceIndex; + Thread t = new Thread(new Runnable() { + + public void run() { + for (int rounds = 0; rounds < 2; rounds++) { + if (index % 2 == 0) { + removeNodesUnderInvRootNode(); + } + createNodesUnderInvRootNode(); + retrieveNodesUnderInvRootNode(); + if (index % 2 != 0) { + removeNodesUnderInvRootNode(); + } + } + } + + }); + threads.add(t); + t.start(); + } + + try { + Iterator it = threads.listIterator(); + while (it.hasNext()) { + Thread t = (Thread) it.next(); + t.join(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("Duration for run: " + (System.currentTimeMillis() - start) / 1000 + "s"); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/RandomOperationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/RandomOperationTest.java new file mode 100644 index 00000000000..54908e673ff --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/daily/RandomOperationTest.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.core.integration.daily; + +import org.apache.jackrabbit.core.AbstractConcurrencyTest; +import org.apache.jackrabbit.core.integration.random.task.VersionOperationsTask; +import org.apache.jackrabbit.core.integration.random.task.ContentOperationsTask; + +import javax.jcr.RepositoryException; + +/** + * RandomOperationTest executes randomly chosen operations using + * multiple threads. Each thread operates on its own subtree to avoid + * conflicting changes. + */ +public class RandomOperationTest extends AbstractConcurrencyTest { + + /** + * Each task is executed with this number of threads. + */ + private static final int NUM_THREADS = 1; + + /** + * Tasks are advised to run for this amount of time. + */ + private static final int RUN_NUM_SECONDS = 60; + + /** + * Number of seconds to wait at most for the tasks to finish their work. + */ + private static final int MAX_WAIT_SECONDS = 60; + + /** + * Number of levels of test data to create per thread + */ + private static final int NUM_LEVELS = 4; + + /** + * Number of nodes per level + */ + private static final int NODES_PER_LEVEL = 3; + + /** + * While creating nodes, save whenever 1000 nodes have been created. + */ + private static final int SAVE_INTERVAL = 1000; + + private long end; + + protected void setUp() throws Exception { + super.setUp(); + end = System.currentTimeMillis() + RUN_NUM_SECONDS * 1000; + } + + public void testRandomContentOperations() throws RepositoryException { + runTask(new ContentOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end), NUM_THREADS); + } + + public void testRandomContentOperationsXA() throws RepositoryException { + ContentOperationsTask task = new ContentOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end); + task.setUseXA(true); + runTask(task, NUM_THREADS); + } + + public void testRandomVersionOperations() throws RepositoryException { + runTask(new VersionOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end), NUM_THREADS); + } + + public void testRandomVersionOperationsXA() throws RepositoryException { + VersionOperationsTask task = new VersionOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end); + task.setUseXA(true); + runTask(task, NUM_THREADS); + } + + public void testContentAndVersionOperations() throws RepositoryException { + runTasks(new Task[]{ + new ContentOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end), + new VersionOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end) + }, NUM_THREADS, end + MAX_WAIT_SECONDS * 1000); + } + + public void testContentAndVersionOperationsXA() throws RepositoryException { + ContentOperationsTask task1 = new ContentOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end); + task1.setUseXA(true); + VersionOperationsTask task2 = new VersionOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end); + task2.setUseXA(true); + runTasks(new Task[]{task1, task2}, NUM_THREADS, end + MAX_WAIT_SECONDS * 1000); + } + + /** + * Test disabled since it violates the "Don't mix concurrent transactional + * and non-transactional writes to a single workspace" guideline formed + * during the concurrency review. + * + * @see Concurrency control + * @see JCR-2000 + */ + public void disabledTestContentAndVersionOperationsXAMixed() + throws RepositoryException { + ContentOperationsTask task1 = new ContentOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end); + ContentOperationsTask task2 = new ContentOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end); + task2.setUseXA(true); + VersionOperationsTask task3 = new VersionOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end); + VersionOperationsTask task4 = new VersionOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, SAVE_INTERVAL, end); + task4.setUseXA(true); + runTasks(new Task[]{task1, task2, task3, task4}, NUM_THREADS, end + MAX_WAIT_SECONDS * 1000); + } + + /** + * Test disabled since it violates the "Don't mix concurrent transactional + * and non-transactional writes to a single workspace" guideline formed + * during the concurrency review. + * + * @see Concurrency control + * @see JCR-2000 + */ + public void disabledTestContentAndVersionOperationsXAMixedShortSaveInterval() + throws RepositoryException { + ContentOperationsTask task1 = new ContentOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, 1, end); + ContentOperationsTask task2 = new ContentOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, 1, end); + task2.setUseXA(true); + VersionOperationsTask task3 = new VersionOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, 1, end); + VersionOperationsTask task4 = new VersionOperationsTask(NUM_LEVELS, NODES_PER_LEVEL, 1, end); + task4.setUseXA(true); + runTasks(new Task[]{task1, task2, task3, task4}, NUM_THREADS, end + MAX_WAIT_SECONDS * 1000); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/AddNode.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/AddNode.java new file mode 100644 index 00000000000..41e9c143eb0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/AddNode.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import javax.jcr.Node; + +/** + * AddNode adds a node with a given name. + */ +public class AddNode extends Operation { + + private static final Logger log = LoggerFactory.getLogger(AddNode.class); + + private final String name; + + public AddNode(Session s, String path, String name) { + super(s, path); + this.name = name; + } + + /** + * Returns the node that was added. + */ + public NodeIterator execute() throws Exception { + Node n = getNode(); + log.info(n.getPath() + "/" + name); + return wrapWithIterator(getNode().addNode(name)); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/AddVersionLabel.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/AddVersionLabel.java new file mode 100644 index 00000000000..8faebc6e505 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/AddVersionLabel.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; + +/** + * AddVersionLabel adds a random 3 character label to the base + * base version of the current node. + */ +public class AddVersionLabel extends VersionOperation { + + private static final Logger log = LoggerFactory.getLogger(AddVersionLabel.class); + + public AddVersionLabel(Session s, String path) { + super(s, path); + } + + /** + * Returns the versionable node. + */ + public NodeIterator execute() throws Exception { + Node n = getNode(); + String name = n.getBaseVersion().getName(); + String label = getRandomText(3); + log.info(n.getPath() + ":" + name + " -> " + label); + n.getVersionHistory().addVersionLabel(name, label, true); + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Checkin.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Checkin.java new file mode 100644 index 00000000000..0a466c32e11 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Checkin.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import javax.jcr.version.Version; + +/** + * Checkin calls checkin on the current node if it is not yet + * checked in. + */ +public class Checkin extends VersionOperation { + + private static final Logger log = LoggerFactory.getLogger(Checkin.class); + + public Checkin(Session s, String path) { + super(s, path); + } + + /** + * Returns the versionable node. + */ + public NodeIterator execute() throws Exception { + Node n = getNode(); + if (n.isCheckedOut()) { + Version v = n.checkin(); + log.info(n.getPath() + ":" + v.getName()); + } + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Checkout.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Checkout.java new file mode 100644 index 00000000000..6a60cc7df56 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Checkout.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; + +/** + * Checkout performs a checkout on the current node if it is not + * yet checked out. + */ +public class Checkout extends VersionOperation { + + private static final Logger log = LoggerFactory.getLogger(Checkout.class); + + public Checkout(Session s, String path) { + super(s, path); + } + + /** + * Returns the versionable node. + */ + public NodeIterator execute() throws Exception { + Node n = getNode(); + if (!n.isCheckedOut()) { + log.info(n.getPath() + ":" + n.getBaseVersion().getName()); + n.checkout(); + } + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/CreateNodes.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/CreateNodes.java new file mode 100644 index 00000000000..0e9f088e110 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/CreateNodes.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * CreateNodes creates a node hierarchy with a given number of + * levels and nodes per level. + */ +public class CreateNodes extends Operation { + + private static final Logger log = LoggerFactory.getLogger(CreateNodes.class); + + private final int numLevels; + + private final int nodesPerLevel; + + private final String[] mixins; + + private final int saveInterval; + + public CreateNodes(Session s, + String path, + int numLevels, + int nodesPerLevel, + String[] mixins, + int saveInterval) { + super(s, path); + this.numLevels = numLevels; + this.nodesPerLevel = nodesPerLevel; + this.mixins = mixins; + this.saveInterval = saveInterval; + } + + /** + * Returns the top of the created node hierarchy. + */ + public NodeIterator execute() throws Exception { + Node n = getNode(); + addMixins(n); + createNodes(n, nodesPerLevel, numLevels, 0); + return wrapWithIterator(n); + } + + private int createNodes(Node n, int nodesPerLevel, int levels, int count) + throws RepositoryException { + levels--; + for (int i = 0; i < nodesPerLevel; i++) { + Node child = n.addNode("node" + i); + count++; + addMixins(child); + log.info("Create node {}", child.getPath()); + if (count % saveInterval == 0) { + getSession().save(); + log.debug("Created " + (count / 1000) + "k nodes"); + } + if (levels > 0) { + count = createNodes(child, nodesPerLevel, levels, count); + } + } + if (levels == 0) { + // final save + getSession().save(); + } + return count; + } + + private void addMixins(Node node) throws RepositoryException { + for (int i = 0; i < mixins.length; i++) { + if (!node.isNodeType(mixins[i])) { + node.addMixin(mixins[i]); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/GetNode.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/GetNode.java new file mode 100644 index 00000000000..d97823dea18 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/GetNode.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.core.integration.random.operation; + +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import javax.jcr.Node; + +/** + * GetNode returns the node at the given path. + */ +public class GetNode extends Operation { + + public GetNode(Session s, String path) { + super(s, path); + } + + /** + * Returns the node at the given path. + */ + public NodeIterator execute() throws Exception { + Node n = getNode(); + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/GetRandomNodes.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/GetRandomNodes.java new file mode 100644 index 00000000000..ada2ba35e2b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/GetRandomNodes.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.core.integration.random.operation; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; + +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * GetRandomNodes randomly returns nodes that were returned by + * the parent operation. + */ +public class GetRandomNodes extends Operation { + + private final Operation op; + + public GetRandomNodes(Session s, Operation op) { + super(s, "/"); + this.op = op; + } + + /** + * Randomly returns node from the parent operation. Please note that the + * returned iterator runs forever!!! + * {@link NodeIterator#hasNext()} will always return true. + */ + public NodeIterator execute() throws Exception { + final List paths = new ArrayList(); + for (NodeIterator it = op.execute(); it.hasNext(); ) { + paths.add(it.nextNode().getPath()); + } + return new NodeIteratorAdapter(new Iterator() { + + public boolean hasNext() { + // runs forever!!! + return true; + } + + public Object next() { + String path = (String) paths.get(getRandom().nextInt(paths.size())); + try { + return getSession().getItem(path); + } catch (RepositoryException e) { + throw new NoSuchElementException(); + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Operation.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Operation.java new file mode 100644 index 00000000000..7232e1d8a7e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Operation.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.core.integration.random.operation; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.Collections; +import java.util.Random; + +/** + * Operation is the common base class for all operations. + */ +public abstract class Operation { + + private final Random rand = new Random(); + + private boolean useTransaction = false; + + private int maxRandomWait = 0; + + private final Session session; + + private final String path; + + public Operation(Session s, String path) { + this.session = s; + this.path = path; + } + + public abstract NodeIterator execute() throws Exception; + + public boolean isUseTransaction() { + return useTransaction; + } + + public void setUseTransaction(boolean useTransaction) { + this.useTransaction = useTransaction; + } + + protected Session getSession() { + return session; + } + + protected String getPath() { + return path; + } + + protected Item getItem() throws RepositoryException { + return session.getItem(path); + } + + protected Node getNode() throws RepositoryException { + return (Node) getItem(); + } + + /** + * Do a random wait. See also {@link #setMaxRandomWait(int)}. + */ + protected void randomWait() throws Exception { + if (maxRandomWait > 0) { + Thread.sleep(rand.nextInt(maxRandomWait)); + } + } + + /** + * @return the maximum number of milliseconds to wait when doing a + * randomized wait. + */ + public int getMaxRandomWait() { + return maxRandomWait; + } + + /** + * @param maxRandomWait the maximum number of milliseconds to wait when + * doing a randomized wait. + */ + public void setMaxRandomWait(int maxRandomWait) { + this.maxRandomWait = maxRandomWait; + } + + /** + * Wraps a single node with a node iterator. + * + * @param node the node to wrap. + * @return a node iterator over the single node. + */ + protected static NodeIterator wrapWithIterator(Node node) { + return new NodeIteratorAdapter(Collections.singletonList(node)); + } + + protected String getRandomText(int numChars) { + StringBuffer tmp = new StringBuffer(numChars); + for (int i = 0; i < numChars; i++) { + char c = (char) (rand.nextInt(('z' + 1) - 'a') + 'a'); + tmp.append(c); + } + return tmp.toString(); + } + + protected Random getRandom() { + return rand; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/OperationFactory.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/OperationFactory.java new file mode 100644 index 00000000000..73178b507af --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/OperationFactory.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.core.integration.random.operation; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.Arrays; + +/** + * OperationFactory hides operation instantiation. + */ +public class OperationFactory { + + private final Random rand = new Random(); + + private final Session session; + + private int nodeCount = 0; + + public OperationFactory(Session s) { + this.session = s; + } + + /** + * Creates a series of random content operations using nodes as + * a source of nodes to operate on. + */ + public Operation randomContentOperations(NodeIterator nodes, + int minOperations, + int maxOperations) + throws Exception { + return new RandomContentOperations(this, session, nodes, + minOperations + rand.nextInt(maxOperations - minOperations)); + } + + /** + * Creates random operations for all the nodes op returns on + * {@link Operation#execute()}. + */ + public Operation randomContentOperation(Operation op) throws Exception { + NodeIterator it = op.execute(); + List ops = new ArrayList(); + while (it.hasNext()) { + Node n = it.nextNode(); + String path = n.getPath(); + switch (rand.nextInt(3)) { // TODO keep in sync with case list + case 0: + ops.add(new AddNode(session, path, "payload" + nodeCount++)); + break; + case 1: + // limit to 10 distinct properties + ops.add(new SetProperty(session, path, "prop" + rand.nextInt(10))); + break; + case 2: + ops.add(new Remove(session, path, "payload")); + break; + } + } + if (ops.size() == 1) { + return (Operation) ops.get(0); + } else { + OperationSequence sequence = new OperationSequence(session, ops); + return sequence; + } + } + + /** + * Creates a series of random version operations using nodes as + * a source of nodes to operate on. + */ + public Operation randomVersionOperations(NodeIterator nodes, + int minOperations, + int maxOperations) + throws Exception { + return new RandomVersionOperations(this, session, nodes, + minOperations + rand.nextInt(maxOperations - minOperations)); + } + + /** + * Creates random operations for all the nodes op returns on + * {@link Operation#execute()}. + */ + public Operation randomVersionOperation(Operation op) throws Exception { + NodeIterator it = op.execute(); + List ops = new ArrayList(); + while (it.hasNext()) { + Node n = it.nextNode(); + String path = n.getPath(); + switch (rand.nextInt(6)) { // TODO keep in sync with case list + case 0: + ops.add(new Checkin(session, path)); + break; + case 1: + ops.add(new Checkout(session, path)); + break; + case 2: + //ops.add(new Restore(session, path)); + break; + case 3: + ops.add(new AddVersionLabel(session, path)); + break; + case 4: + ops.add(new RemoveVersionLabel(session, path)); + break; + case 5: + ops.add(new RemoveVersion(session, path)); + break; + } + } + if (ops.size() == 1) { + return (Operation) ops.get(0); + } else { + OperationSequence sequence = new OperationSequence(session, ops); + return sequence; + } + } + + /** + * Wraps an XA transaction operation around op. + */ + public Operation runInTransaction(Operation op) { + return new XATransaction(session, op); + } + + /** + * Creates a new operation that contains the passed ops. + */ + public Operation runInSequence(Operation[] ops) { + return new OperationSequence(session, Arrays.asList(ops)); + } + + /** + * Creates a save operation on the node with the given path. + */ + public Operation save(String path) { + return new Save(session, path); + } + + /** + * Creates an operation the return the node with the given path. + */ + public Operation getNode(String path) { + return new GetNode(session, path); + } + + /** + * Creates an operation that returns all nodes that are visited by traversing + * the node hierarchy starting at the node with the given path. + */ + public Operation traverseNodes(String path) { + return new TraverseNodes(session, path); + } + + /** + * Creates a node hierarchy. + */ + public Operation createNodes(String path, + int numLevels, + int nodesPerLevel, + String[] mixins, + int saveInterval) { + return new CreateNodes(session, path, numLevels, + nodesPerLevel, mixins, saveInterval); + } + + /** + * Creates an iterator that randomly returns nodes produced by op. + * Please note that the returned iterator always returns true + * for {@link NodeIterator#hasNext()}!!! + */ + public NodeIterator getRandomNodes(Operation op) throws Exception { + return new GetRandomNodes(session, op).execute(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/OperationSequence.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/OperationSequence.java new file mode 100644 index 00000000000..3fb1daea8b7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/OperationSequence.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.core.integration.random.operation; + +import org.apache.commons.collections.iterators.IteratorChain; +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; + +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import java.util.List; + +/** + * OperationSequence wraps other operations and executes the in + * sequence. + */ +public class OperationSequence extends Operation { + + private final Operation[] ops; + + public OperationSequence(Session s, List operations) { + super(s, "/"); + this.ops = (Operation[]) operations.toArray(new Operation[operations.size()]); + } + + public NodeIterator execute() throws Exception { + IteratorChain chain = new IteratorChain(); + for (int i = 0; i < ops.length; i++) { + chain.addIterator(ops[i].execute()); + } + return new NodeIteratorAdapter(chain); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomContentOperations.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomContentOperations.java new file mode 100644 index 00000000000..888b845bac0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomContentOperations.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.core.integration.random.operation; + +import javax.jcr.Session; +import javax.jcr.NodeIterator; + +/** + * RandomContentOperations executes a number of content operations. + */ +public class RandomContentOperations extends RandomOperations { + + public RandomContentOperations(OperationFactory factory, + Session s, + NodeIterator op, + int numOperations) { + super(factory, s, op, numOperations); + } + + public NodeIterator execute() throws Exception { + return super.execute(); + } + + protected Operation getRandomOperation(String path) throws Exception { + OperationFactory f = getFactory(); + return f.randomContentOperation(f.getNode(path)); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomOperations.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomOperations.java new file mode 100644 index 00000000000..81d068884c4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomOperations.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.core.integration.random.operation; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import java.util.ArrayList; +import java.util.List; + +/** + * RandomOperations is an abstract base class for a set of random + * operation. + */ +public abstract class RandomOperations extends Operation { + + private final OperationFactory factory; + + private final NodeIterator nodes; + + private final int numOperations; + + public RandomOperations(OperationFactory factory, + Session s, + NodeIterator nodes, + int numOperations) { + super(s, "/"); + this.factory = factory; + this.nodes = nodes; + this.numOperations = numOperations; + } + + public NodeIterator execute() throws Exception { + List operations = new ArrayList(); + for (int i = 0; i < numOperations && nodes.hasNext(); i++) { + Node n = nodes.nextNode(); + operations.add(getRandomOperation(n.getPath())); + } + return new OperationSequence(getSession(), operations).execute(); + } + + protected int getNumOperations() { + return numOperations; + } + + protected OperationFactory getFactory() { + return factory; + } + + protected abstract Operation getRandomOperation(String path) throws Exception; +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomVersionOperations.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomVersionOperations.java new file mode 100644 index 00000000000..effb01a8dca --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RandomVersionOperations.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.core.integration.random.operation; + +import javax.jcr.Session; +import javax.jcr.NodeIterator; + +/** + * RandomVersionOperations executes a number of version operations. + */ +public class RandomVersionOperations extends RandomOperations { + + public RandomVersionOperations(OperationFactory factory, + Session s, + NodeIterator nodes, + int numOperations) { + super(factory, s, nodes, numOperations); + } + + public NodeIterator execute() throws Exception { + return super.execute(); + } + + protected Operation getRandomOperation(String path) throws Exception { + OperationFactory f = getFactory(); + return f.randomVersionOperation(f.getNode(path)); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Remove.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Remove.java new file mode 100644 index 00000000000..8428bfe738a --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Remove.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; + +/** + * Remove performs a remove of a node if its name starts with a + * given prefix. + */ +public class Remove extends Operation { + + private static final Logger log = LoggerFactory.getLogger(Remove.class); + + private final String namePrefix; + + public Remove(Session s, String path, String namePrefix) { + super(s, path); + this.namePrefix = namePrefix; + } + + public NodeIterator execute() throws Exception { + Node n = getNode(); + for (NodeIterator it = n.getNodes(); it.hasNext(); ) { + Node r = it.nextNode(); + if (r.getName().startsWith(namePrefix)) { + log.info(r.getPath()); + r.remove(); + break; + } + } + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RemoveVersion.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RemoveVersion.java new file mode 100644 index 00000000000..efc971e8b82 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RemoveVersion.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Session; +import javax.jcr.NodeIterator; +import javax.jcr.Node; +import javax.jcr.version.Version; + +/** + * RemoveVersion removes a version. + */ +public class RemoveVersion extends VersionOperation { + + private static final Logger log = LoggerFactory.getLogger(RemoveVersion.class); + + public RemoveVersion(Session s, String path) { + super(s, path); + } + + public NodeIterator execute() throws Exception { + Node n = getNode(); + Version v = getRandomVersion(true); + if (v != null) { + log.info(n.getPath() + ":" + v.getName()); + n.getVersionHistory().removeVersion(v.getName()); + } + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RemoveVersionLabel.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RemoveVersionLabel.java new file mode 100644 index 00000000000..f9a8d88f540 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/RemoveVersionLabel.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Session; +import javax.jcr.NodeIterator; +import javax.jcr.Node; +import javax.jcr.version.VersionHistory; + +/** + * RemoveVersionLabel removes a randomly chosen version label on + * the version history of the current node. + */ +public class RemoveVersionLabel extends VersionOperation { + + private static final Logger log = LoggerFactory.getLogger(RemoveVersionLabel.class); + + public RemoveVersionLabel(Session s, String path) { + super(s, path); + } + + public NodeIterator execute() throws Exception { + Node n = getNode(); + VersionHistory vh = n.getVersionHistory(); + String[] labels = vh.getVersionLabels(); + if (labels.length > 0) { + String label = labels[getRandom().nextInt(labels.length)]; + log.info(n.getPath() + " -> " + label); + vh.removeVersionLabel(label); + } + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Restore.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Restore.java new file mode 100644 index 00000000000..98f3953c445 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Restore.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import javax.jcr.version.Version; + +/** + * Restore... + */ +public class Restore extends VersionOperation { + + private static final Logger log = LoggerFactory.getLogger(Restore.class); + + public Restore(Session s, String path) { + super(s, path); + } + + public NodeIterator execute() throws Exception { + Node n = getNode(); + Version v = getRandomVersion(false); + if (v != null) { + log.info(n.getPath() + ":" + v.getName()); + n.restore(v, true); + } + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Save.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Save.java new file mode 100644 index 00000000000..d9e5507c01a --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/Save.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; + +/** + * Save performs a {@link Node#save()} on the current node. + */ +public class Save extends Operation { + + private static final Logger log = LoggerFactory.getLogger(Save.class); + + public Save(Session s, String path) { + super(s, path); + } + + public NodeIterator execute() throws Exception { + Node n = getNode(); + log.info(n.getPath()); + n.save(); + return wrapWithIterator(n); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/SetProperty.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/SetProperty.java new file mode 100644 index 00000000000..eccff37ba36 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/SetProperty.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.core.integration.random.operation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; + +/** + * SetProperty sets a string property with a random text of length + * 5. + */ +public class SetProperty extends Operation { + + private static final Logger log = LoggerFactory.getLogger(SetProperty.class); + + private final String name; + + public SetProperty(Session s, String path, String name) { + super(s, path); + this.name = name; + } + + public NodeIterator execute() throws Exception { + Node n = getNode(); + String value = getRandomText(5); + String path = n.setProperty(name, value).getPath(); + log.info(path + ": " + value); + return wrapWithIterator(getNode()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/TraverseNodes.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/TraverseNodes.java new file mode 100644 index 00000000000..ca9ff2587a7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/TraverseNodes.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.core.integration.random.operation; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.commons.packaging.FilterContentPackage; +import org.apache.jackrabbit.commons.predicate.IsNodePredicate; + +import javax.jcr.NodeIterator; +import javax.jcr.Session; + +/** + * TraverseNodes traverses a node hierarchy. + */ +public class TraverseNodes extends Operation { + + public TraverseNodes(Session s, String path) { + super(s, path); + } + + public NodeIterator execute() throws Exception { + FilterContentPackage pack = new FilterContentPackage(); + pack.addContent(getPath(), new IsNodePredicate()); + return new NodeIteratorAdapter(pack.getItems(getSession())); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/VersionOperation.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/VersionOperation.java new file mode 100644 index 00000000000..c4a49b01a33 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/VersionOperation.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.core.integration.random.operation; + +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.Version; +import java.util.List; +import java.util.ArrayList; + +/** + * VersionOperation is a base class for all version operations. + */ +public abstract class VersionOperation extends Operation { + + public VersionOperation(Session s, String path) { + super(s, path); + } + + /** + * Returns a randomly chosen version for the current node or + * null if the current node only has a root version. + * + * @param excludeReferenced exclude versions that are still referenced. + * @return randomly chosen version or null. + * @throws RepositoryException if an error occurs while reading from the + * repository. + */ + protected Version getRandomVersion(boolean excludeReferenced) throws RepositoryException { + List allVersions = new ArrayList(); + Node n = getNode(); + for (VersionIterator it = n.getVersionHistory().getAllVersions(); it.hasNext(); ) { + Version v = it.nextVersion(); + if (excludeReferenced) { + // quick check if it is the base version + if (n.getBaseVersion().isSame(v)) { + continue; + } + } + if (v.getPredecessors().length > 0) { + if (!excludeReferenced || !v.getReferences().hasNext()) { + allVersions.add(v); + } + } + } + if (allVersions.size() > 0) { + return (Version) allVersions.get(getRandom().nextInt(allVersions.size())); + } else { + return null; + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/XATransaction.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/XATransaction.java new file mode 100644 index 00000000000..bd9e0d8ea1d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/operation/XATransaction.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.core.integration.random.operation; + +import org.apache.jackrabbit.core.UserTransactionImpl; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import javax.jcr.NodeIterator; +import javax.jcr.Session; + +/** + * XATransaction wraps an operation with a XA transaction. + */ +public class XATransaction extends Operation { + + private static final Logger log = LoggerFactory.getLogger(XATransaction.class); + + private final Operation op; + + public XATransaction(Session s, Operation op) { + super(s, "/"); + this.op = op; + } + + public NodeIterator execute() throws Exception { + UserTransactionImpl tx = new UserTransactionImpl(getSession()); + log.info("begin transaction"); + tx.begin(); + NodeIterator it = op.execute(); + log.info("commit transaction"); + tx.commit(); + return it; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/ContentOperationsTask.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/ContentOperationsTask.java new file mode 100644 index 00000000000..46c12b5e243 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/ContentOperationsTask.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.core.integration.random.task; + +import org.apache.jackrabbit.core.integration.random.operation.Operation; +import org.apache.jackrabbit.core.integration.random.operation.OperationFactory; + +import javax.jcr.NodeIterator; + +/** + * ContentOperationsTask performs random content operations. + */ +public class ContentOperationsTask extends RandomOperationsTask { + + public ContentOperationsTask(int numLevels, int nodesPerLevel, + int saveInterval, long end) { + super(new String[0], numLevels, nodesPerLevel, saveInterval, end); + } + + protected Operation getRandomOperations(OperationFactory f, + NodeIterator randomNodes) + throws Exception { + return f.randomContentOperations(randomNodes, 3, 10); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/RandomOperationsTask.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/RandomOperationsTask.java new file mode 100644 index 00000000000..99c29798c34 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/RandomOperationsTask.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.core.integration.random.task; + +import org.apache.jackrabbit.core.AbstractConcurrencyTest; +import org.apache.jackrabbit.core.integration.random.operation.Operation; +import org.apache.jackrabbit.core.integration.random.operation.OperationFactory; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * ContentOperationsTask base class for random operation tasks. + */ +public abstract class RandomOperationsTask implements AbstractConcurrencyTest.Task { + + private final String[] mixins; + + private final int numLevels; + + private final int nodesPerLevel; + + private final int saveInterval; + + private final long end; + + private boolean useXA = false; + + public RandomOperationsTask(String mixins[], int numLevels, + int nodesPerLevel, int saveInterval, long end) { + this.mixins = mixins; + this.numLevels = numLevels; + this.nodesPerLevel = nodesPerLevel; + this.saveInterval = saveInterval; + this.end = end; + } + + public void execute(Session session, Node test) throws RepositoryException { + try { + OperationFactory f = new OperationFactory(session); + // create nodes + f.createNodes(test.getPath(), numLevels, nodesPerLevel, + mixins, saveInterval).execute(); + // save nodes + f.save(test.getPath()).execute(); + + NodeIterator nodes = f.getRandomNodes(f.traverseNodes(test.getPath())); + while (end > System.currentTimeMillis()) { + Operation op = f.runInSequence(new Operation[]{ + getRandomOperations(f, nodes), + f.save("/") + }); + if (isUseXA()) { + op = f.runInTransaction(op); + } + op.execute(); + } + test.remove(); + session.save(); + } catch (Exception e) { + throw new RepositoryException(e); + } + } + + protected abstract Operation getRandomOperations(OperationFactory f, + NodeIterator randomNodes) + throws Exception; + + public boolean isUseXA() { + return useXA; + } + + public void setUseXA(boolean useXA) { + this.useXA = useXA; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/VersionOperationsTask.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/VersionOperationsTask.java new file mode 100644 index 00000000000..ca55d4a571c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/integration/random/task/VersionOperationsTask.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.core.integration.random.task; + +import org.apache.jackrabbit.core.integration.random.operation.Operation; +import org.apache.jackrabbit.core.integration.random.operation.OperationFactory; + +import javax.jcr.NodeIterator; + +/** + * VersionOperationsTask performs random version operations. + */ +public class VersionOperationsTask extends RandomOperationsTask { + + public VersionOperationsTask(int numLevels, int nodesPerLevel, + int saveInterval, long end) { + super(new String[]{"mix:versionable"}, numLevels, nodesPerLevel, + saveInterval, end); + } + + protected Operation getRandomOperations(OperationFactory f, + NodeIterator randomNodes) + throws Exception { + return f.randomVersionOperations(randomNodes, 3, 10); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/FileJournalTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/FileJournalTest.java new file mode 100644 index 00000000000..a58c559565b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/FileJournalTest.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.journal; + +import java.io.File; + +import javax.jcr.RepositoryException; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.cluster.ClusterNode; +import org.apache.jackrabbit.core.cluster.SimpleClusterContext; +import org.apache.jackrabbit.core.config.ClusterConfig; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Test cases for file journal. + */ +public class FileJournalTest extends JUnitTest { + + /** + * Repository home value. + */ + private static final String REPOSITORY_HOME = "target/repository_for_test"; + + /** + * Default cluster node id. + */ + private static final String CLUSTER_NODE_ID = "node"; + + /** + * Default sync delay: 5 seconds. + */ + private static final long SYNC_DELAY = 5000; + + /** + * Repository home. + */ + private File repositoryHome; + + /** + * Journal directory. + */ + private File journalDirectory; + + /** + * {@inheritDoc} + */ + protected void setUp() throws Exception { + repositoryHome = new File(REPOSITORY_HOME); + repositoryHome.mkdirs(); + FileUtils.cleanDirectory(repositoryHome); + journalDirectory = new File(repositoryHome, "journal"); + + super.setUp(); + } + + /** + * {@inheritDoc} + */ + protected void tearDown() throws Exception { + if (repositoryHome != null) { + FileUtils.deleteDirectory(repositoryHome); + } + super.tearDown(); + } + + /** + * Create a journal with no revision file name. Verify that the journal + * is created nonetheless, with a revision file in the repository home. + * + * @throws Exception + * @see JCR-904 + */ + public void testRevisionIsOptional() throws Exception { + final FileJournal journal = new FileJournal(); + journal.setDirectory(journalDirectory.getPath()); + JournalFactory jf = new JournalFactory() { + public Journal getJournal(NamespaceResolver resolver) { + return journal; + } + }; + ClusterConfig cc = new ClusterConfig(CLUSTER_NODE_ID, SYNC_DELAY, jf); + SimpleClusterContext context = new SimpleClusterContext(cc, repositoryHome); + + journal.setRepositoryHome(repositoryHome); + journal.init(CLUSTER_NODE_ID, context.getNamespaceResolver()); + + ClusterNode clusterNode = new ClusterNode(); + clusterNode.init(context); + + try { + File revisionFile = + new File(repositoryHome, FileJournal.DEFAULT_INSTANCE_FILE_NAME); + assertTrue(revisionFile.exists()); + } finally { + clusterNode.stop(); + } + } + + /** + * Verify that ClusterNode.stop can be invoked even when + * ClusterNode.init throws because of a bad journal class. + * + * @throws Exception + */ + public void testClusterInitIncompleteBadJournalClass() throws Exception { + JournalFactory jf = new JournalFactory() { + public Journal getJournal(NamespaceResolver resolver) + throws RepositoryException { + throw new RepositoryException("Journal not available"); + } + }; + ClusterConfig cc = new ClusterConfig(CLUSTER_NODE_ID, SYNC_DELAY, jf); + SimpleClusterContext context = new SimpleClusterContext(cc); + + ClusterNode clusterNode = new ClusterNode(); + + try { + clusterNode.init(context); + fail("Bad cluster configuration."); + } catch (Exception e) { + } + + clusterNode.stop(); + } + + /** + * Verify that ClusterNode.stop can be invoked even when + * ClusterNode.init throws because the journal can not + * be initialized. Note: this is done by omitting the required argument + * directory. + * + * @throws Exception + */ + public void testClusterInitIncompleteMissingParam() throws Exception { + JournalFactory jf = new JournalFactory() { + public Journal getJournal(NamespaceResolver resolver) + throws RepositoryException { + try { + FileJournal journal = new FileJournal(); + // no setDirectory() call here + journal.init(CLUSTER_NODE_ID, resolver); + return journal; + } catch (JournalException e) { + throw new RepositoryException("Expected failure", e); + } + } + }; + ClusterConfig cc = new ClusterConfig(CLUSTER_NODE_ID, SYNC_DELAY, jf); + SimpleClusterContext context = new SimpleClusterContext(cc); + + ClusterNode clusterNode = new ClusterNode(); + try { + clusterNode.init(context); + fail("Bad cluster configuration."); + } catch (Exception e) { + } + + clusterNode.stop(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/LockableFileRevisionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/LockableFileRevisionTest.java new file mode 100644 index 00000000000..095e121a547 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/LockableFileRevisionTest.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.core.journal; + +import java.io.File; + +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.AbstractConcurrencyTest; + +/** + * LockableFileRevisionTest checks is concurrently locking and + * unlocking a LockableFileRevision works. + */ +public class LockableFileRevisionTest extends AbstractConcurrencyTest { + + private LockableFileRevision rev; + + private File tmp; + + protected void setUp() throws Exception { + super.setUp(); + tmp = File.createTempFile("test", "lock"); + rev = new LockableFileRevision(tmp); + } + + protected void tearDown() throws Exception { + tmp.delete(); + super.tearDown(); + } + + public void testConcurrentLocking() throws Exception { + runTask(new Task() { + public void execute(Session session, Node test) throws + RepositoryException { + for (int i = 0; i < 1000; i++) { + try { + rev.lock(true); + rev.unlock(); + } catch (Exception e) { + throw new RepositoryException(e); + } + } + } + }, 50); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/TestAll.java new file mode 100644 index 00000000000..7e4bd6412c2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/journal/TestAll.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.core.journal; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Collects all test classes setting up test data in the workspace. This test + * data is specific to Jackrabbit and will probably not work in other + * implementations. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite(); + + suite.addTestSuite(FileJournalTest.class); + suite.addTestSuite(LockableFileRevisionTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ConcurrentLockingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ConcurrentLockingTest.java new file mode 100644 index 00000000000..c5d8f7a0627 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ConcurrentLockingTest.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.core.lock; + +import org.apache.jackrabbit.core.AbstractConcurrencyTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.lock.Lock; + +/** + * ConcurrentLockingTest contains test cases that run lock + * operations with concurrent threads. + */ +public class ConcurrentLockingTest extends AbstractConcurrencyTest { + + /** + * The number of threads. + */ + private static final int CONCURRENCY = 10; + + /** + * The total number of operations to execute. E.g. number of lock + * performed by the threads. + */ + private static final int NUM_OPERATIONS = 200; + + public void testConcurrentLockUnlock() throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) throws RepositoryException { + Node n = test.addNode("test"); + n.addMixin(mixLockable); + session.save(); + + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + n.lock(false, true); + n.unlock(); + } + } + }, CONCURRENCY); + } + + public void testConcurrentCreateAndLockUnlock() throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) throws RepositoryException { + // add versionable nodes + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + Node n = test.addNode("test" + i); + n.addMixin(mixLockable); + session.save(); + Lock l = n.lock(false, true); + l.refresh(); + n.unlock(); + } + } + }, CONCURRENCY); + } + + public void testConcurrentLock() throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) throws RepositoryException { + Node n = test.addNode("test"); + n.addMixin(mixLockable); + session.save(); + + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + if (n.isLocked()) { + n.unlock(); + } + n.lock(false, true); + } + } + }, CONCURRENCY); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ConcurrentLockingWithTransactionsTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ConcurrentLockingWithTransactionsTest.java new file mode 100644 index 00000000000..fd4aa994f00 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ConcurrentLockingWithTransactionsTest.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.core.lock; + +import org.apache.jackrabbit.core.state.StaleItemStateException; +import org.apache.jackrabbit.core.AbstractConcurrencyTest; +import org.apache.jackrabbit.core.UserTransactionImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.transaction.UserTransaction; + +/** ConcurrentLockingWithTransactionsTest... */ +public class ConcurrentLockingWithTransactionsTest extends AbstractConcurrencyTest { + + private static Logger log = LoggerFactory.getLogger(ConcurrentLockingWithTransactionsTest.class); + + /** + * The number of threads. + */ + private static final int CONCURRENCY = 100; + + /** + * The total number of operations to execute. E.g. number of locking operations + * performed by the threads. + */ + private static final int NUM_OPERATIONS = 100; + + public void testConcurrentRefreshInTransaction() + throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + int i = 0; + try { + Node n = test.addNode("test"); + n.addMixin(mixLockable); + session.save(); + for (i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + Lock lock = n.lock(false, true); + + final UserTransaction utx = new UserTransactionImpl(test.getSession()); + utx.begin(); + lock.refresh(); + utx.commit(); + + n.unlock(); + } + } catch (Exception e) { + final String threadName = Thread.currentThread().getName(); + final Throwable deepCause = getLevel2Cause(e); + if (deepCause != null && deepCause instanceof StaleItemStateException) { + // ignore + } else { + throw new RepositoryException(threadName + ", i=" + i + ":" + e.getClass().getName(), e); + } + } + } + }, CONCURRENCY); + } + + public void testConcurrentCreateAndLockUnLockInTransaction() + throws RepositoryException { + runTask(new Task() { + public void execute(Session session, Node test) + throws RepositoryException { + // add versionable nodes + for (int i = 0; i < NUM_OPERATIONS / CONCURRENCY; i++) { + try { + final UserTransaction utx = new UserTransactionImpl(test.getSession()); + utx.begin(); + Node n = test.addNode("test" + i); + n.addMixin(mixLockable); + session.save(); + Lock l = n.lock(false, true); + n.unlock(); + utx.commit(); + } catch (Exception e) { + final String threadName = Thread.currentThread().getName(); + final Throwable deepCause = getLevel2Cause(e); + if (deepCause != null && deepCause instanceof StaleItemStateException) { + // ignore + } else { + throw new RepositoryException(threadName + ", i=" + i + ":" + e.getClass().getName(), e); + } + } + } + } + }, CONCURRENCY); + } + + private static Throwable getLevel2Cause(Throwable t) { + Throwable result = null; + try { + result = t.getCause().getCause(); + } catch (NullPointerException npe) { + // ignore, we have no deep cause + } + return result; + + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ExtendedLockingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ExtendedLockingTest.java new file mode 100644 index 00000000000..aa4d72b60b5 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/ExtendedLockingTest.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.core.lock; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.lock.Lock; + +/** + * ExtendedLockingTest... + */ +public class ExtendedLockingTest extends AbstractJCRTest { + + public void testRemoveMixLockableFromLockedNode() throws RepositoryException { + + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + testRootNode.save(); + + Lock l = n.lock(true, true); + + try { + n.removeMixin(mixLockable); + n.save(); + fail("Removing mix:lockable from a locked node must fail."); + } catch (ConstraintViolationException e) { + // success + } finally { + if (n.isLocked()) { + n.unlock(); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/LockTimeoutTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/LockTimeoutTest.java new file mode 100644 index 00000000000..86009a1f997 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/LockTimeoutTest.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.core.lock; + +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.transaction.UserTransaction; +import org.apache.jackrabbit.core.UserTransactionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test lock timeout expiration. + */ +public class LockTimeoutTest extends AbstractJCRTest { + + public void testExpired() throws Exception { + testExpired(false); + testExpired(true); + } + + private void testExpired(boolean xa) throws Exception { + Session s = testRootNode.getSession(); + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixLockable); + s.save(); + + UserTransaction utx = null; + if (xa) { + utx = new UserTransactionImpl(s); + utx.begin(); + } + + javax.jcr.lock.LockManager lm = s.getWorkspace().getLockManager(); + + boolean isDeep; + boolean isSessionScoped; + long timeoutHint; + String ownerInfo; + + isDeep = false; + isSessionScoped = false; + timeoutHint = 1; + ownerInfo = ""; + Session s2 = getHelper().getSuperuserSession(); + + Lock l = lm.lock(n.getPath(), isDeep, isSessionScoped, timeoutHint, ownerInfo); + // this works only for timeout = 1, + // as getSecondsRemaining always returns a positive value + assertEquals(timeoutHint, l.getSecondsRemaining()); + assertTrue(l.isLive()); + + if (xa) { + utx.commit(); + } + + long start = System.currentTimeMillis(); + while (true) { + Thread.sleep(100); + long now = System.currentTimeMillis(); + boolean success; + try { + s2.getNode(n.getPath()).setProperty("x", 1); + s2.save(); + success = true; + } catch (Exception e) { + success = false; + } + long t = now - start; + if (t > timeoutHint + 3000) { + assertTrue(success); + break; + } else if (t < timeoutHint) { + assertFalse(success); + } + } + n.remove(); + s.save(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/TestAll.java new file mode 100644 index 00000000000..84145e482a7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/lock/TestAll.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.core.lock; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package org.apache.jackrabbit.core.lock. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.core.lock tests"); + + suite.addTestSuite(ConcurrentLockingTest.class); + suite.addTestSuite(ConcurrentLockingWithTransactionsTest.class); + suite.addTestSuite(ExtendedLockingTest.class); + suite.addTestSuite(LockTimeoutTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/CyclicNodeTypeRegistrationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/CyclicNodeTypeRegistrationTest.java new file mode 100644 index 00000000000..b9e6a642194 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/CyclicNodeTypeRegistrationTest.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.core.nodetype; + +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.nodetype.QNodeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.QPropertyDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.QNodeTypeDefinitionBuilder; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeTypeManager; +import java.util.Collection; +import java.util.LinkedList; + +/** + */ +public class CyclicNodeTypeRegistrationTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * The node type manager we use for the tests + */ + private NodeTypeManager manager; + + /** + * The node type registry we use for the tests + */ + private NodeTypeRegistry ntreg; + + /** + * The cyclic dependent node type definitions we use for the tests + */ + private Collection ntDefCollection; + + /** + * The name factory + */ + private NameFactory nameFactory; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + //isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + manager = session.getWorkspace().getNodeTypeManager(); + nameFactory = NameFactoryImpl.getInstance(); + + // Get the NodeTypeManager from the Workspace. + // Note that it must be cast from the generic JCR NodeTypeManager to the + // Jackrabbit-specific implementation. + NodeTypeManagerImpl ntmgr = (NodeTypeManagerImpl) session.getWorkspace().getNodeTypeManager(); + + // Acquire the NodeTypeRegistry + ntreg = ntmgr.getNodeTypeRegistry(); + + + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + ntreg = null; + ntDefCollection = null; + nameFactory = null; + super.tearDown(); + } + + /** + * Tests, if it is possible to register node types with simple + * cyclic dependencies. + */ + public void testRegisterCyclicChildNodeTypes() { + /** + * Constructs node types with the following structure: + * [foo] + * + myBarInFoo (bar) + * + * [bar] + * + myFooInBar (foo) + */ + final QNodeTypeDefinitionBuilder foo = new QNodeTypeDefinitionBuilder(); + foo.setName(nameFactory.create("", "foo")); + foo.setSupertypes(new Name[]{NameConstants.NT_BASE}); + + final QNodeTypeDefinitionBuilder bar = new QNodeTypeDefinitionBuilder(); + bar.setName(nameFactory.create("", "bar")); + bar.setSupertypes(new Name[]{NameConstants.NT_BASE}); + + QNodeDefinitionBuilder myBarInFoo = new QNodeDefinitionBuilder(); + myBarInFoo.setRequiredPrimaryTypes(new Name[]{bar.getName()}); + myBarInFoo.setName(nameFactory.create("", "myBarInFoo")); + myBarInFoo.setDeclaringNodeType(foo.getName()); + + QNodeDefinitionBuilder myFooInBar = new QNodeDefinitionBuilder(); + myFooInBar.setRequiredPrimaryTypes(new Name[]{foo.getName()}); + myFooInBar.setName(nameFactory.create("", "myFooInBar")); + myFooInBar.setDeclaringNodeType(bar.getName()); + + foo.setChildNodeDefs(new QNodeDefinition[]{myBarInFoo.build()}); + bar.setChildNodeDefs(new QNodeDefinition[]{myFooInBar.build()}); + ntDefCollection = new LinkedList(); + ntDefCollection.add(foo.build()); + ntDefCollection.add(bar.build()); + + try { + ntreg.registerNodeTypes(ntDefCollection); + } catch (InvalidNodeTypeDefException e) { + assertFalse(e.getMessage(), true); + e.printStackTrace(); + } catch (RepositoryException e) { + assertFalse(e.getMessage(), true); + e.printStackTrace(); + } + boolean allNTsAreRegistered = ntreg.isRegistered(foo.getName()) && ntreg.isRegistered(bar.getName()); + assertTrue(allNTsAreRegistered); + + } + + /** + * A simple check, if a missing node type is found + */ + public void testRegisterSimpleMissingNodeTypes() { + /** + * Constructs node types with the following structure: + * [foo] + * + myNTInFoo (I_am_an_invalid_required_primary_type) + * + */ + final QNodeTypeDefinitionBuilder foo = new QNodeTypeDefinitionBuilder(); + foo.setName(nameFactory.create("", "foo")); + foo.setSupertypes(new Name[]{NameConstants.NT_BASE}); + + + QNodeDefinitionBuilder myBarInFoo = new QNodeDefinitionBuilder(); + myBarInFoo.setRequiredPrimaryTypes(new Name[]{nameFactory.create("", "I_am_an_invalid_required_primary_type")}); + myBarInFoo.setName(nameFactory.create("", "myNTInFoo")); + myBarInFoo.setDeclaringNodeType(foo.getName()); + + foo.setChildNodeDefs(new QNodeDefinition[]{myBarInFoo.build()}); + ntDefCollection = new LinkedList(); + ntDefCollection.add(foo.build()); + + try { + ntreg.registerNodeTypes(ntDefCollection); + assertFalse("Missing node type not found", true); + } catch (InvalidNodeTypeDefException e) { + assertTrue(true); + + } catch (RepositoryException e) { + assertFalse("Wrong Exception thrown on missing node type.", true); + e.printStackTrace(); + } + } + + /** + * Basically a test of a Graffito use case. + */ + public void testRegisterCyclicChildNodeTypesAndSupertypes() { + /** + * Constructs node types with the following structure: + * [Folder] > CmsObject + * + folders (Folder) + * + documents (Document) + * + * [CmsObject] + * + parentFolder (Folder) + * + * [Document] > CmsObject + * - size (long) + * + */ + + final QNodeTypeDefinitionBuilder folder = new QNodeTypeDefinitionBuilder(); + folder.setName(nameFactory.create("", "Folder")); + + final QNodeTypeDefinitionBuilder cmsObject = new QNodeTypeDefinitionBuilder(); + cmsObject.setName(nameFactory.create("", "CmsObject")); + cmsObject.setSupertypes(new Name[]{NameConstants.NT_BASE}); + + QNodeDefinitionBuilder parentFolder = new QNodeDefinitionBuilder(); + parentFolder.setRequiredPrimaryTypes(new Name[]{folder.getName()}); + parentFolder.setName(nameFactory.create("", "parentFolder")); + parentFolder.setDeclaringNodeType(cmsObject.getName()); + cmsObject.setChildNodeDefs(new QNodeDefinition[]{parentFolder.build()}); + + + final QNodeTypeDefinitionBuilder document = new QNodeTypeDefinitionBuilder(); + document.setName(nameFactory.create("", "Document")); + document.setSupertypes(new Name[]{cmsObject.getName()}); + QPropertyDefinitionBuilder sizeProp = new QPropertyDefinitionBuilder(); + sizeProp.setName(nameFactory.create("", "size")); + sizeProp.setRequiredType(PropertyType.LONG); + sizeProp.setDeclaringNodeType(document.getName()); + document.setPropertyDefs(new QPropertyDefinition[]{sizeProp.build()}); + + + folder.setSupertypes(new Name[]{cmsObject.getName()}); + + QNodeDefinitionBuilder folders = new QNodeDefinitionBuilder(); + folders.setRequiredPrimaryTypes(new Name[]{folder.getName()}); + folders.setName(nameFactory.create("", "folders")); + folders.setDeclaringNodeType(folder.getName()); + + QNodeDefinitionBuilder documents = new QNodeDefinitionBuilder(); + documents.setRequiredPrimaryTypes(new Name[]{document.getName()}); + documents.setName(nameFactory.create("", "documents")); + documents.setDeclaringNodeType(folder.getName()); + + folder.setChildNodeDefs(new QNodeDefinition[]{ + folders.build(), documents.build()}); + ntDefCollection = new LinkedList(); + ntDefCollection.add(folder.build()); + ntDefCollection.add(document.build()); + ntDefCollection.add(cmsObject.build()); + + try { + ntreg.registerNodeTypes(ntDefCollection); + } catch (InvalidNodeTypeDefException e) { + assertFalse(e.getMessage(), true); + e.printStackTrace(); + } catch (RepositoryException e) { + assertFalse(e.getMessage(), true); + e.printStackTrace(); + } + boolean allNTsAreRegistered = ntreg.isRegistered(folder.getName()) && ntreg.isRegistered(document.getName()) && ntreg.isRegistered(cmsObject.getName()); + assertTrue(allNTsAreRegistered); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/MixinTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/MixinTest.java new file mode 100644 index 00000000000..072e1b805ba --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/MixinTest.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.core.nodetype; + +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStreamReader; +import java.io.Reader; + +/** + * MixinTest... + */ +public class MixinTest extends AbstractJCRTest { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(MixinTest.class); + + /** Name of the cnd nodetype file for import and namespace registration. */ + private static final String TEST_NODETYPES = "org/apache/jackrabbit/core/nodetype/xml/test_mixin_nodetypes.cnd"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Reader cnd = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(TEST_NODETYPES)); + CndImporter.registerNodeTypes(cnd, superuser); + cnd.close(); + } + + /** + * Test for bug JCR-2778 + * + * @throws Exception + */ + public void testMixinRemovedWithProtectedChildNode() throws Exception { + testRootNode.addMixin("test:mixinNode_protectedchild"); + superuser.save(); + + // remove the mixin type again + testRootNode.removeMixin("test:mixinNode_protectedchild"); + + assertFalse(testRootNode.isNodeType("test:mixinNode_protectedchild")); + superuser.save(); + assertFalse(testRootNode.isNodeType("test:mixinNode_protectedchild")); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/NodeTypesInContentTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/NodeTypesInContentTest.java new file mode 100644 index 00000000000..a763bbee7ba --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/NodeTypesInContentTest.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.core.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.nodetype.xml.NodeTypeReader; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +import javax.jcr.ItemVisitor; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.Node; +import javax.jcr.util.TraversingItemVisitor; +import java.io.InputStream; +import java.util.Arrays; + +/** + * NodeTypesInContentTest... + */ +public class NodeTypesInContentTest extends AbstractJCRTest { + + /** + * custom node type defs defining non-string default values + */ + private static final String TEST_NODETYPES = "org/apache/jackrabbit/core/nodetype/xml/test_nodetypes.xml"; + + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + InputStream xml = getClass().getClassLoader().getResourceAsStream(TEST_NODETYPES); + QNodeTypeDefinition[] ntDefs = NodeTypeReader.read(xml); + NodeTypeRegistry ntReg = ((SessionImpl) superuser).getNodeTypeManager().getNodeTypeRegistry(); + if (!ntReg.isRegistered(ntDefs[0].getName())) { + ntReg.registerNodeTypes(Arrays.asList(ntDefs)); + } + } + + /** + * Test for JCR-1964 + * + * @throws javax.jcr.RepositoryException If an exception occurs. + */ + public void testDefaultValues() throws RepositoryException { + ItemVisitor visitor = new TraversingItemVisitor.Default() { + + public void visit(Property property) throws RepositoryException { + if (JcrConstants.JCR_DEFAULTVALUES.equals(property.getName())) { + int type = property.getType(); + Value[] vs = property.getValues(); + for (int i = 0; i < vs.length; i++) { + assertEquals("Property type must match the value(s) type", type, vs[i].getType()); + } + } + } + }; + + Node start = (Node) superuser.getItem("/jcr:system/jcr:nodeTypes"); + visitor.visit(start); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/TestAll.java new file mode 100644 index 00000000000..043855b6349 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/TestAll.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.core.nodetype; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package org.apache.jackrabbit.core.nodetype. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.core.nodetype tests"); + + suite.addTestSuite(NodeTypesInContentTest.class); + suite.addTestSuite(MixinTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/xml/SimpleNamespaceRegistry.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/xml/SimpleNamespaceRegistry.java new file mode 100644 index 00000000000..66d865d0938 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/xml/SimpleNamespaceRegistry.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.core.nodetype.xml; + +import java.util.Properties; + +import javax.jcr.NamespaceRegistry; + +import org.apache.jackrabbit.spi.Name; + +/** + * Simple utility implementation of the NamespaceRegistry interface. + * Used by the node type formatter test cases. + */ +public class SimpleNamespaceRegistry implements NamespaceRegistry { + + /** Map from namespace prefixes to namespace URIs. */ + private final Properties prefixToURI = new Properties(); + + /** Map from namespace URIs to namespace prefixes. */ + private final Properties uriToPrefix = new Properties(); + + /** + * Creates a simple namespace registry. + */ + public SimpleNamespaceRegistry() { + registerNamespace(Name.NS_JCR_PREFIX, Name.NS_JCR_URI); + registerNamespace(Name.NS_MIX_PREFIX, Name.NS_MIX_URI); + registerNamespace(Name.NS_NT_PREFIX, Name.NS_NT_URI); + registerNamespace(Name.NS_REP_PREFIX, Name.NS_REP_URI); + registerNamespace(Name.NS_EMPTY_PREFIX, Name.NS_EMPTY_PREFIX); + } + + /** {@inheritDoc} */ + public void registerNamespace(String prefix, String uri) { + prefixToURI.put(prefix, uri); + uriToPrefix.put(uri, prefix); + } + + /** {@inheritDoc} */ + public void unregisterNamespace(String prefix) { + if (prefixToURI.contains(prefix)) { + uriToPrefix.remove(prefixToURI.get(prefix)); + prefixToURI.remove(prefix); + } + } + + /** {@inheritDoc} */ + public String[] getPrefixes() { + return (String[]) prefixToURI.keySet().toArray(new String[0]); + } + + /** {@inheritDoc} */ + public String[] getURIs() { + return (String[]) uriToPrefix.keySet().toArray(new String[0]); + } + + /** {@inheritDoc} */ + public String getURI(String prefix) { + return prefixToURI.getProperty(prefix); + } + + /** {@inheritDoc} */ + public String getPrefix(String uri) { + return uriToPrefix.getProperty(uri); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/xml/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/xml/TestAll.java new file mode 100644 index 00000000000..39ea01c5e52 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/nodetype/xml/TestAll.java @@ -0,0 +1,657 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.nodetype.xml; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Arrays; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.version.OnParentVersionAction; + +import junit.framework.AssertionFailedError; + +import org.apache.jackrabbit.api.JackrabbitNodeTypeManager; +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException; +import org.apache.jackrabbit.core.value.InternalValueFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test cases for reading and writing node type definition files. + */ +public class TestAll extends AbstractJCRTest { + + /** The dummy test namespace. */ + private static final String TEST_NAMESPACE = "http://www.apache.org/jackrabbit/test"; + + /** Name of the include test node type definition file. */ + private static final String TEST_NODETYPES = + "org/apache/jackrabbit/core/nodetype/xml/test_nodetypes.xml"; + + /** Name of the xml nodetype file for import and namespace registration. */ + private static final String TEST_NS_XML_NODETYPES = + "test_ns_xml_nodetypes.xml"; + + /** Name of the cnd nodetype file for import and namespace registration. */ + private static final String TEST_NS_CND_NODETYPES = + "test_ns_cnd_nodetypes.cnd"; + + /** Name of the xml nodetype file with same node type name definitions. */ + private static final String TEST_SAME_NT_NAME_XML_NODETYPES = + "test_same_nt_name_xml_nodetypes.xml"; + + /** Name of the cnd nodetype file with same node type name definitions. */ + private static final String TEST_SAME_NT_NAME_CND_NODETYPES = + "test_same_nt_name_cnd_nodetypes.cnd"; + + private static final NameFactory FACTORY = NameFactoryImpl.getInstance(); + + /** Test node types definitions. */ + private QNodeTypeDefinition[] types; + + /** Registry for the test namespaces. */ + private NamespaceRegistry registry; + + /** + * Initializes the node type formatter tests. + * + * @throws Exception on initialization errors + */ + protected void setUp() throws Exception { + super.setUp(); + InputStream xml = + getClass().getClassLoader().getResourceAsStream(TEST_NODETYPES); + + types = NodeTypeReader.read(xml); + + registry = new SimpleNamespaceRegistry(); + registry.registerNamespace("test", TEST_NAMESPACE); + } + + /** + * Returns the named node type definition. If the node type does not + * exist, an assertion failure is generated. + * + * @param name node type name + * @return node type definition + */ + private QNodeTypeDefinition getNodeType(String name) { + Name qname = FACTORY.create(TEST_NAMESPACE, name); + for (int i = 0; i < types.length; i++) { + if (qname.equals(types[i].getName())) { + return types[i]; + } + } + throw new AssertionFailedError("Node type " + name + " does not exist"); + } + + /** + * Returns the named property definition from the named node type + * definition. If either of the definitions do not exist, an assertion + * failure is generated. + *

    + * If the given property name is null, then the residual + * property definition (if one exists) is returned. + * + * @param typeName node type name + * @param propertyName property name, or null + * @return property definition + */ + private QPropertyDefinition getPropDef(String typeName, String propertyName) { + Name name; + if (propertyName != null) { + name = FACTORY.create(TEST_NAMESPACE, propertyName); + } else { + name = NameConstants.ANY_NAME; + } + + QNodeTypeDefinition def = getNodeType(typeName); + QPropertyDefinition[] defs = def.getPropertyDefs(); + for (int i = 0; i < defs.length; i++) { + if (name.equals(defs[i].getName())) { + return defs[i]; + } + } + + throw new AssertionFailedError( + "Property " + propertyName + " does not exist"); + } + + /** + * Returns the string value of the identified property default value. + * + * @param def property definition + * @param index default value index + * @return default value + */ + private String getDefaultValue(QPropertyDefinition def, int index) { + try { + QValue[] values = def.getDefaultValues(); + NamespaceResolver nsResolver = new AdditionalNamespaceResolver(registry); + NamePathResolver resolver = new DefaultNamePathResolver(nsResolver); + ValueFactoryQImpl factory = new ValueFactoryQImpl(InternalValueFactory.getInstance(), resolver); + return factory.createValue(values[index]).getString(); + } catch (RepositoryException e) { + throw new AssertionFailedError(e.getMessage()); + } + } + + /** + * Returns the named child node definition from the named node type + * definition. If either of the definitions do not exist, an assertion + * failure is generated. + * + * @param typeName node type name + * @param nodeName child node name + * @return child node definition + */ + private QNodeDefinition getChildNode(String typeName, String nodeName) { + Name name = FACTORY.create(TEST_NAMESPACE, nodeName); + + QNodeTypeDefinition def = getNodeType(typeName); + QNodeDefinition[] defs = def.getChildNodeDefs(); + for (int i = 0; i < defs.length; i++) { + if (name.equals(defs[i].getName())) { + return defs[i]; + } + } + + throw new AssertionFailedError( + "Child node " + nodeName + " does not exist"); + } + + /** + * Test for reading a node type definition file. The test file + * has already been read during the test setup, so this method + * just verifies the number of node types. + */ + public void testRead() { + assertEquals("number of node types", 6, types.length); + } + + /** Test for the empty node type. */ + public void testEmptyNodeType() { + QNodeTypeDefinition def = getNodeType("emptyNodeType"); + assertNotNull("emptyNodeType exists", def); + assertEquals("emptyNodeType mixin", + false, def.isMixin()); + assertEquals("emptyNodeType hasOrderableChildNodes", + false, def.hasOrderableChildNodes()); + assertEquals("emptyNodeType primaryItemName", + null, def.getPrimaryItemName()); + assertEquals("emptyNodeType childNodeDefs", + 0, def.getChildNodeDefs().length); + assertEquals("emptyNodeType propertyDefs", + 0, def.getPropertyDefs().length); + } + + /** Test for the mixin node type attribute. */ + public void testMixinNodeType() { + QNodeTypeDefinition def = getNodeType("mixinNodeType"); + assertEquals("mixinNodeType mixin", + true, def.isMixin()); + } + + /** Test for the hasOrderableChildNodes node type attribute. */ + public void testOrderedNodeType() { + QNodeTypeDefinition def = getNodeType("orderedNodeType"); + assertEquals("orderedNodeType hasOrderableChildNodes", + true, def.hasOrderableChildNodes()); + } + + /** Test for node type item definitions. */ + public void testItemNodeType() { + QNodeTypeDefinition def = getNodeType("itemNodeType"); + assertEquals("itemNodeType primaryItemName", + FACTORY.create(TEST_NAMESPACE, "emptyItem"), + def.getPrimaryItemName()); + assertEquals("itemNodeType propertyDefs", + 10, def.getPropertyDefs().length); + QPropertyDefinition pdef = getPropDef("itemNodeType", null); + assertTrue("itemNodeType wildcard property", pdef.definesResidual()); + } + + /** Test for namespace registration on node type import. */ + public void testImportXMLNodeTypes() throws Exception { + try { + superuser.getNamespacePrefix("http://ns.example.org/test-namespace2"); + // Ignore test case, node type and namespace already registered + } catch (NamespaceException e1) { + // Namespace testns2 not yet registered + JackrabbitNodeTypeManager ntm = (JackrabbitNodeTypeManager) + superuser.getWorkspace().getNodeTypeManager(); + ntm.registerNodeTypes( + TestAll.class.getResourceAsStream(TEST_NS_XML_NODETYPES), + JackrabbitNodeTypeManager.TEXT_XML); + try { + superuser.getNamespacePrefix("http://ns.example.org/test-namespace2"); + } catch (NamespaceException e2) { + fail("xml test2 namespace not registered"); + } + } + } + + /** Test for same node type name on node type import. */ + public void testInvalidXMLNodeTypes() throws Exception { + JackrabbitNodeTypeManager ntm = (JackrabbitNodeTypeManager) + superuser.getWorkspace().getNodeTypeManager(); + try { + ntm.registerNodeTypes( + TestAll.class.getResourceAsStream(TEST_SAME_NT_NAME_XML_NODETYPES), + JackrabbitNodeTypeManager.TEXT_XML); + fail("Importing multiple node types with the same name must fail"); + } catch (RepositoryException e) { + if (e.getCause() instanceof InvalidNodeTypeDefException) { + // Expected + } else { + throw e; + } + } + } + + /** Test for namespace registration on node type import. */ + public void testImportCNDNodeTypes() throws Exception { + try { + superuser.getNamespacePrefix("http://ns.example.org/test-namespace3"); + // Ignore test case, node type and namespace already registered + } catch (NamespaceException e1) { + Reader cnd = new InputStreamReader(TestAll.class.getResourceAsStream(TEST_NS_CND_NODETYPES)); + CndImporter.registerNodeTypes(cnd, superuser); + cnd.close(); + try { + superuser.getNamespacePrefix("http://ns.example.org/test-namespace3"); + } catch (NamespaceException e2) { + fail("cnd test3 namespace not registered"); + } + } + } + + /** Test for same node type name on node type import. */ + public void testInvalidCNDNodeTypes() throws Exception { + JackrabbitNodeTypeManager ntm = (JackrabbitNodeTypeManager) + superuser.getWorkspace().getNodeTypeManager(); + try { + ntm.registerNodeTypes( + TestAll.class.getResourceAsStream(TEST_SAME_NT_NAME_CND_NODETYPES), + JackrabbitNodeTypeManager.TEXT_X_JCR_CND); + fail("Importing multiple node types with the same name must fail"); + } catch (RepositoryException e) { + if (e.getCause() instanceof InvalidNodeTypeDefException) { + // Expected + } else { + throw e; + } + } + } + + /** Test for the empty item definition. */ + public void testEmptyItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "emptyItem"); + assertEquals("emptyItem autoCreate", + false, def.isAutoCreated()); + assertEquals("emptyItem mandatory", + false, def.isMandatory()); + assertEquals("emptyItem onParentVersion", + OnParentVersionAction.IGNORE, def.getOnParentVersion()); + assertEquals("emptyItem protected", + false, def.isProtected()); + } + + /** Test for the autoCreated item definition attribute. */ + public void testAutoCreateItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "autoCreatedItem"); + assertEquals("autoCreatedItem autoCreated", + true, def.isAutoCreated()); + } + + /** Test for the mandatory item definition attribute. */ + public void testMandatoryItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "mandatoryItem"); + assertEquals("mandatoryItem mandatory", + true, def.isMandatory()); + } + + /** Test for the copy parent version action. */ + public void testCopyItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "copyItem"); + assertEquals("copyItem onParentVersion", + OnParentVersionAction.COPY, def.getOnParentVersion()); + } + + /** Test for the version parent version action. */ + public void testVersionItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "versionItem"); + assertEquals("versionItem onParentVersion", + OnParentVersionAction.VERSION, def.getOnParentVersion()); + } + + /** Test for the initialize parent version action. */ + public void testInitializeItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "initializeItem"); + assertEquals("initializeItem onParentVersion", + OnParentVersionAction.INITIALIZE, def.getOnParentVersion()); + } + + /** Test for the compute parent version action. */ + public void testComputeItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "computeItem"); + assertEquals("computeItem onParentVersion", + OnParentVersionAction.COMPUTE, def.getOnParentVersion()); + } + + /** Test for the abort parent version action. */ + public void testAbortItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "abortItem"); + assertEquals("abortItem onParentVersion", + OnParentVersionAction.ABORT, def.getOnParentVersion()); + } + + /** Test for the protected item definition attribute. */ + public void testProtectedItem() { + QPropertyDefinition def = getPropDef("itemNodeType", "protectedItem"); + assertEquals("protectedItem protected", + true, def.isProtected()); + } + + /** Test for node type property definitions. */ + public void testPropertyNodeType() { + QNodeTypeDefinition def = getNodeType("propertyNodeType"); + assertEquals("propertyNodeType propertyDefs", + 13, def.getPropertyDefs().length); + } + + /** Test for the empty property definition. */ + public void testEmptyProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "emptyProperty"); + assertEquals("emptyProperty requiredType", + PropertyType.UNDEFINED, def.getRequiredType()); + assertEquals("emptyProperty multiple", + false, def.isMultiple()); + assertNull("emptyProperty defaultValues", + def.getDefaultValues()); + assertEquals("emptyProperty valueConstraints", + 0, def.getValueConstraints().length); + } + + /** Test for the binary property definition type. */ + public void testBinaryProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "binaryProperty"); + assertEquals("binaryProperty requiredType", + PropertyType.BINARY, def.getRequiredType()); + assertEquals("binaryProperty valueConstraints", + 1, def.getValueConstraints().length); + assertEquals("binaryProperty valueConstraints[0]", + "[0,)", (def.getValueConstraints())[0].getString()); + assertNull("binaryProperty defaultValues", + def.getDefaultValues()); + } + + /** Test for the boolean property definition type. */ + public void testBooleanProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "booleanProperty"); + assertEquals("booleanProperty requiredType", + PropertyType.BOOLEAN, def.getRequiredType()); + assertEquals("booleanProperty valueConstraints", + 2, def.getValueConstraints().length); + assertEquals("booleanProperty valueConstraints[0]", + "true", (def.getValueConstraints())[0].getString()); + assertEquals("booleanProperty valueConstraints[1]", + "false", (def.getValueConstraints())[1].getString()); + assertEquals("booleanProperty defaultValues", + 1, def.getDefaultValues().length); + assertEquals("booleanProperty defaultValues[0]", + "true", getDefaultValue(def, 0)); + } + + /** Test for the date property definition type. */ + public void testDateProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "dateProperty"); + assertEquals("dateProperty requiredType", + PropertyType.DATE, def.getRequiredType()); + assertEquals("dateProperty valueConstraints", + 1, def.getValueConstraints().length); + assertEquals("dateProperty valueConstraints[0]", + "[2005-01-01T00:00:00.000Z,2006-01-01T00:00:00.000Z)", + (def.getValueConstraints())[0].getString()); + assertEquals("dateProperty defaultValues", + 1, def.getDefaultValues().length); + assertEquals("dateProperty defaultValues[0]", + "2005-01-01T00:00:00.000Z", getDefaultValue(def, 0)); + } + + /** Test for the double property definition type. */ + public void testDoubleProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "doubleProperty"); + assertEquals("doubleProperty requiredType", + PropertyType.DOUBLE, def.getRequiredType()); + assertEquals("doubleProperty valueConstraints", + 3, def.getValueConstraints().length); + assertEquals("doubleProperty valueConstraints[0]", + "[,0.0)", (def.getValueConstraints())[0].getString()); + assertEquals("doubleProperty valueConstraints[1]", + "(1.0,2.0)", (def.getValueConstraints())[1].getString()); + assertEquals("doubleProperty valueConstraints[2]", + "(3.0,]", (def.getValueConstraints())[2].getString()); + assertEquals("doubleProperty defaultValues", + 1, def.getDefaultValues().length); + assertEquals("doubleProperty defaultValues[0]", + "1.5", getDefaultValue(def, 0)); + } + + /** Test for the long property definition type. */ + public void testLongProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "longProperty"); + assertEquals("longProperty requiredType", + PropertyType.LONG, def.getRequiredType()); + assertEquals("longProperty valueConstraints", + 3, def.getValueConstraints().length); + assertEquals("longProperty valueConstraints[0]", + "(-10,0]", (def.getValueConstraints())[0].getString()); + assertEquals("longProperty valueConstraints[1]", + "[1,2]", (def.getValueConstraints())[1].getString()); + assertEquals("longProperty valueConstraints[2]", + "[10,100)", (def.getValueConstraints())[2].getString()); + assertEquals("longProperty defaultValues", + 1, def.getDefaultValues().length); + assertEquals("longProperty defaultValues[0]", + "25", getDefaultValue(def, 0)); + } + + /** Test for the name property definition type. */ + public void testNameProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "nameProperty"); + assertEquals("nameProperty requiredType", + PropertyType.NAME, def.getRequiredType()); + assertEquals("nameProperty valueConstraints", + 1, def.getValueConstraints().length); + assertEquals("nameProperty valueConstraints[0]", + "{http://www.apache.org/jackrabbit/test}testName", + (def.getValueConstraints())[0].getString()); + assertEquals("nameProperty defaultValues", + 1, def.getDefaultValues().length); + assertEquals("nameProperty defaultValues[0]", + "test:testName", getDefaultValue(def, 0)); + } + + /** Test for the path property definition type. */ + public void testPathProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "pathProperty"); + assertEquals("pathProperty requiredType", + PropertyType.PATH, def.getRequiredType()); + assertEquals("pathProperty valueConstraints", + 1, def.getValueConstraints().length); + assertEquals("pathProperty valueConstraints[0]", + "{}\t{http://www.apache.org/jackrabbit/test}testPath", + (def.getValueConstraints())[0].getString()); + assertNull("pathProperty defaultValues", + def.getDefaultValues()); + } + + /** Test for the path property definition type. */ + public void testPathProperty1() { + QPropertyDefinition def = getPropDef("propertyNodeType", "pathProperty1"); + assertEquals("pathProperty requiredType", + PropertyType.PATH, def.getRequiredType()); + assertEquals("pathProperty valueConstraints", + 1, def.getValueConstraints().length); + assertEquals("pathProperty valueConstraints[0]", + "{}\t{http://www.apache.org/jackrabbit/test}testPath\t{}*", + (def.getValueConstraints())[0].getString()); + assertNull("pathProperty defaultValues", + def.getDefaultValues()); + } + + /** Test for the path property definition type. */ + public void testPathProperty2() { + QPropertyDefinition def = getPropDef("propertyNodeType", "pathProperty2"); + assertEquals("pathProperty requiredType", + PropertyType.PATH, def.getRequiredType()); + assertEquals("pathProperty valueConstraints", + 1, def.getValueConstraints().length); + assertEquals("pathProperty valueConstraints[0]", + "{http://www.apache.org/jackrabbit/test}testPath\t{}*", + (def.getValueConstraints())[0].getString()); + assertNull("pathProperty defaultValues", + def.getDefaultValues()); + } + + /** Test for the reference property definition type. */ + public void testReferenceProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "referenceProperty"); + assertEquals("referenceProperty requiredType", + PropertyType.REFERENCE, def.getRequiredType()); + assertEquals("referenceProperty valueConstraints", + 1, def.getValueConstraints().length); + assertEquals("referenceProperty valueConstraints[0]", + "{http://www.jcp.org/jcr/nt/1.0}base", + (def.getValueConstraints())[0].getString()); + assertNull("referenceProperty defaultValues", + def.getDefaultValues()); + } + + /** Test for the string property definition type. */ + public void testStringProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "stringProperty"); + assertEquals("stringProperty requiredType", + PropertyType.STRING, def.getRequiredType()); + assertEquals("stringProperty valueConstraints", + 1, def.getValueConstraints().length); + assertEquals("stringProperty valueConstraints[0]", + "bananas?", + (def.getValueConstraints())[0].getString()); + assertEquals("stringProperty defaultValues", + 2, def.getDefaultValues().length); + assertEquals("stringProperty defaultValues[0]", + "banana", getDefaultValue(def, 0)); + assertEquals("stringProperty defaultValues[1]", + "bananas", getDefaultValue(def, 1)); + } + + /** Test for the multiple property definition attribute. */ + public void testMultipleProperty() { + QPropertyDefinition def = getPropDef("propertyNodeType", "multipleProperty"); + assertEquals("multipleProperty multiple", + true, def.isMultiple()); + } + + /** Test for node type child node definitions. */ + public void testChildNodeType() { + QNodeTypeDefinition def = getNodeType("childNodeType"); + assertEquals("childNodeType childNodeDefs", + 4, def.getChildNodeDefs().length); + } + + /** Test for the empty child node definition. */ + public void testEmptyNode() { + QNodeDefinition def = getChildNode("childNodeType", "emptyNode"); + assertEquals("emptyNode allowsSameNameSiblings", + false, def.allowsSameNameSiblings()); + assertEquals("emptyNode defaultPrimaryType", + null, def.getDefaultPrimaryType()); + } + + /** Test for the allowsSameNameSiblings child node attribute. */ + public void testSiblingNode() { + QNodeDefinition def = getChildNode("childNodeType", "siblingNode"); + assertEquals("siblingNode allowsSameNameSiblings", + true, def.allowsSameNameSiblings()); + } + + /** Test for the defaultPrimaryType child node attribute. */ + public void testDefaultTypeNode() { + QNodeDefinition def = getChildNode("childNodeType", "defaultTypeNode"); + assertEquals("defaultTypeNode defaultPrimaryType", + FACTORY.create(Name.NS_NT_URI, "base"), + def.getDefaultPrimaryType()); + } + + /** Test for the requiredPrimaryTypes child node attributes. */ + public void testRequiredTypeNode() { + QNodeDefinition def = getChildNode("childNodeType", "requiredTypeNode"); + assertEquals("requiredTypeNode requiredPrimaryTypes", + 2, def.getRequiredPrimaryTypes().length); + Name[] types = def.getRequiredPrimaryTypes(); + Arrays.sort(types); + assertEquals("requiredTypeNode requiredPrimaryTypes[0]", + FACTORY.create(Name.NS_NT_URI, "base"), types[0]); + assertEquals("requiredTypeNode requiredPrimaryTypes[1]", + FACTORY.create(Name.NS_NT_URI, "unstructured"), types[1]); + } + + /** + * Test for writing a node type definition file. Writing is tested + * by writing and re-reading the test node types using an internal + * byte array. The resulting node type map is then compared to the + * original test node types. + * + * @throws IOException on IO errors + * @throws RepositoryException on repository errors + */ + public void testWrite() throws IOException, RepositoryException { + try { + ByteArrayOutputStream xml = new ByteArrayOutputStream(); + NodeTypeWriter.write(xml, types, registry); + byte[] bytes = xml.toByteArray(); + QNodeTypeDefinition[] output = + NodeTypeReader.read(new ByteArrayInputStream(bytes)); + assertTrue("write output", Arrays.equals(types, output)); + } catch (InvalidNodeTypeDefException e) { + fail(e.getMessage()); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/MixinTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/MixinTest.java new file mode 100644 index 00000000000..a8bca41871f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/MixinTest.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import org.apache.jackrabbit.test.api.observation.AbstractObservationTest; +import org.apache.jackrabbit.test.api.observation.EventResult; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventListener; + +/** + * Tests if observation event filtering works based on mixin type names. + */ +public class MixinTest extends AbstractObservationTest { + + /** + * Tests event filtering with a single mixin type name. + */ + public void testSingleMixin() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType).addMixin(mixReferenceable); + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName3, testNodeType).addMixin(mixReferenceable); + testRootNode.save(); + + EventResult propertyAddedListener = new EventResult(log); + addEventListener(propertyAddedListener, new String[]{mixReferenceable}, Event.PROPERTY_ADDED); + try { + testRootNode.getNode(nodeName1).setProperty(propertyName1, "test"); + testRootNode.getNode(nodeName2).setProperty(propertyName1, "test"); + testRootNode.getNode(nodeName3).setProperty(propertyName1, "test"); + testRootNode.save(); + + Event[] added = propertyAddedListener.getEvents(DEFAULT_WAIT_TIMEOUT); + checkPropertyAdded(added, new String[]{nodeName1 + "/" + propertyName1, + nodeName3 + "/" + propertyName1}); + } finally { + removeEventListener(propertyAddedListener); + } + } + + /** + * Tests event filtering with multiple mixin type name. + */ + public void testMultipleMixin() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType).addMixin(mixReferenceable); + testRootNode.addNode(nodeName2, testNodeType).addMixin(mixTitle); + testRootNode.addNode(nodeName3, testNodeType).addMixin(mixReferenceable); + testRootNode.save(); + + EventResult propertyAddedListener = new EventResult(log); + addEventListener(propertyAddedListener, new String[]{mixReferenceable, mixTitle}, Event.PROPERTY_ADDED); + try { + testRootNode.getNode(nodeName1).setProperty(propertyName1, "test"); + testRootNode.getNode(nodeName2).setProperty(propertyName1, "test"); + testRootNode.getNode(nodeName3).setProperty(propertyName1, "test"); + testRootNode.save(); + + Event[] added = propertyAddedListener.getEvents(DEFAULT_WAIT_TIMEOUT); + checkPropertyAdded(added, new String[]{nodeName1 + "/" + propertyName1, + nodeName2 + "/" + propertyName1, + nodeName3 + "/" + propertyName1}); + } finally { + removeEventListener(propertyAddedListener); + } + } + + /** + * Tests event filtering on nodes with multiple mixins applied. + */ + public void testMultipleMixinOnNode() throws RepositoryException { + Node node1 = testRootNode.addNode(nodeName1, testNodeType); + node1.addMixin(mixReferenceable); + node1.addMixin(mixTitle); + Node node2 = testRootNode.addNode(nodeName2, testNodeType); + Node node3 = testRootNode.addNode(nodeName3, testNodeType); + node3.addMixin(mixTitle); + node3.addMixin(mixReferenceable); + testRootNode.save(); + + EventResult propertyAddedListener = new EventResult(log); + addEventListener(propertyAddedListener, new String[]{mixReferenceable}, Event.PROPERTY_ADDED); + try { + node1.setProperty(propertyName1, "test"); + node2.setProperty(propertyName1, "test"); + node3.setProperty(propertyName1, "test"); + testRootNode.save(); + + Event[] added = propertyAddedListener.getEvents(DEFAULT_WAIT_TIMEOUT); + checkPropertyAdded(added, new String[]{nodeName1 + "/" + propertyName1, + nodeName3 + "/" + propertyName1}); + } finally { + removeEventListener(propertyAddedListener); + } + } + + /** + * Checks if an event listener registered for a mixin type T also gets + * notifications for an event on a node with a mixin type which is derived + * from T. + */ + public void testDerivedMixin() throws RepositoryException { + Node node1 = testRootNode.addNode(nodeName1, testNodeType); + node1.addMixin(mixVersionable); + + testRootNode.save(); + + EventResult propertyAddedListener = new EventResult(log); + // mix:versionable is derived from mix:referenceable + addEventListener(propertyAddedListener, new String[]{mixReferenceable}, Event.PROPERTY_ADDED); + try { + node1.setProperty(propertyName1, "test"); + testRootNode.save(); + + Event[] added = propertyAddedListener.getEvents(DEFAULT_WAIT_TIMEOUT); + checkPropertyAdded(added, new String[]{nodeName1 + "/" + propertyName1}); + } finally { + removeEventListener(propertyAddedListener); + } + } + + /** + * Registers an EventListener for events of the specified + * type(s). + * + * @param listener the EventListener. + * @param mixins the names of mixin types to filter. + * @param eventType the event types + * @throws RepositoryException if registration fails. + */ + protected void addEventListener(EventListener listener, String[] mixins, int eventType) + throws RepositoryException { + if (obsMgr != null) { + obsMgr.addEventListener(listener, + eventType, + superuser.getRootNode().getPath(), + true, + null, + mixins, + false); + } else { + throw new IllegalStateException("ObservationManager not available."); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/MoveInPlaceTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/MoveInPlaceTest.java new file mode 100644 index 00000000000..c5ad1e73323 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/MoveInPlaceTest.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.core.observation; + +import org.apache.jackrabbit.core.query.AbstractQueryTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; + +/** + * MoveInPlaceTest is a test case for issue JCR-1728 + */ +public class MoveInPlaceTest extends AbstractQueryTest { + + public void testMove() throws RepositoryException { + Node parent = testRootNode.addNode("parent"); + Node child = parent.addNode("child"); + testRootNode.save(); + + Node tmp = testRootNode.addNode("tmp"); + superuser.move(child.getPath(), tmp.getPath() + "/" + child.getName()); + parent.remove(); + superuser.move(tmp.getPath(), testRootNode.getPath() + "/parent"); + testRootNode.save(); + + String stmt = testPath + "/parent/child"; + NodeIterator nodes = executeQuery(stmt).getNodes(); + assertTrue("Result must not be empty", nodes.hasNext()); + assertEquals("Wrong result", testRoot + "/parent/child", nodes.nextNode().getPath()); + assertFalse("More results than expected", nodes.hasNext()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ReorderTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ReorderTest.java new file mode 100644 index 00000000000..f1b0b5b9fbb --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ReorderTest.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.core.observation; + +import org.apache.jackrabbit.test.api.observation.AbstractObservationTest; +import org.apache.jackrabbit.test.api.observation.EventResult; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.observation.Event; + +/** + * Tests implementation specific aspects of the observation manager. + */ +public class ReorderTest extends AbstractObservationTest { + + /** + * Tests if reordering a child node triggers a {@link javax.jcr.observation.Event#NODE_REMOVED} + * and a {@link javax.jcr.observation.Event#NODE_ADDED} event with same name siblings. Furthermore + * a node is removed in the same save scope. + *

    + * Because of the one reorder operation, three nodes change their index. And + * actually four nodes change their context position. The minimal events + * that may be triggered only includes one pair of remove/add node events + * (plus the additional event for the removed node). + */ + public void testNodeReorderComplex() + throws RepositoryException, NotExecutableException { + if (!testRootNode.getDefinition().getDeclaringNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Node at '" + testRoot + "' does not support orderable child nodes."); + } + + /** + * Initial tree: + * + testroot + * + nodename1[1] + * + nodename1[2] + * + nodename2 + * + nodename1[3] + * + nodename1[4] + * + nodename3 + * + * After reorder: + * + testroot + * + nodename1[1] + * + nodename2 + * + nodename1[2] (was 3) + * + nodename1[3] (was 4) + * + nodename1[4] (was 2) + */ + Node n = testRootNode.addNode(nodeName1, testNodeType); + if (!n.getDefinition().allowsSameNameSiblings()) { + throw new NotExecutableException("Node at " + testRoot + " does not allow same name siblings with name " + nodeName1); + } + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.save(); + EventResult addNodeListener = new EventResult(log); + EventResult removeNodeListener = new EventResult(log); + addEventListener(addNodeListener, Event.NODE_ADDED); + addEventListener(removeNodeListener, Event.NODE_REMOVED); + testRootNode.orderBefore(nodeName1 + "[2]", null); + testRootNode.getNode(nodeName3).remove(); + testRootNode.save(); + removeEventListener(addNodeListener); + removeEventListener(removeNodeListener); + Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + // not deterministic, there exist various re-order seqences. the minimal + // is: + // nodename1[2] has been reordered to the end + nodeName3 has been removed + checkNodeAdded(added, new String[]{nodeName1 + "[4]"}, null); + checkNodeRemoved(removed, new String[]{nodeName1 + "[2]", nodeName3}, null); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ShareableNodesTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ShareableNodesTest.java new file mode 100644 index 00000000000..a73944a3cac --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/ShareableNodesTest.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.core.observation; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Workspace; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.test.api.observation.AbstractObservationTest; +import org.apache.jackrabbit.test.api.observation.EventResult; +import org.apache.jackrabbit.core.NodeImpl; + +/** + * ShareableNodesTest... + */ +public class ShareableNodesTest extends AbstractObservationTest { + + public void testAddShareableMixin() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + testRootNode.save(); + + EventResult result = new EventResult(log); + addEventListener(result); + + n1.addMixin(mixShareable); + testRootNode.save(); + + Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + for (int i = 0; i < events.length; i++) { + assertFalse("must not contain node added event", events[i].getType() == Event.NODE_ADDED); + assertFalse("must not contain node removed event", events[i].getType() == Event.NODE_REMOVED); + } + } + + public void testAddShare() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + Node s = n1.addNode(nodeName3); + s.addMixin(mixShareable); + testRootNode.save(); + + EventResult result = new EventResult(log); + addEventListener(result); + + Workspace wsp = superuser.getWorkspace(); + wsp.clone(wsp.getName(), s.getPath(), n2.getPath() + "/" + s.getName(), false); + + checkNodeAdded(result.getEvents(DEFAULT_WAIT_TIMEOUT), + new String[]{nodeName2 + "/" + nodeName3}, + new String[0]); + } + + public void testRemoveShare() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + Node s = n1.addNode(nodeName3); + s.addMixin(mixShareable); + testRootNode.save(); + + Workspace wsp = superuser.getWorkspace(); + wsp.clone(wsp.getName(), s.getPath(), n2.getPath() + "/" + s.getName(), false); + + EventResult result = new EventResult(log); + addEventListener(result); + + removeFromSharedSet(n2.getNode(nodeName3)); + testRootNode.save(); + + checkNodeRemoved(result.getEvents(DEFAULT_WAIT_TIMEOUT), + new String[]{nodeName2 + "/" + nodeName3}, + new String[0]); + } + + protected void removeFromSharedSet(Node node) throws RepositoryException { + node.removeShare(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/TestAll.java new file mode 100644 index 00000000000..ba3d101bcca --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/TestAll.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.core.observation; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all jackrabbit specific testcases for the + * observation module. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("Observation tests"); + + suite.addTestSuite(ReorderTest.class); + suite.addTestSuite(MixinTest.class); + suite.addTestSuite(VersionEventsTest.class); + suite.addTestSuite(MoveInPlaceTest.class); + suite.addTestSuite(ShareableNodesTest.class); + suite.addTestSuite(WarningOnSaveWithNotificationThreadTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/VersionEventsTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/VersionEventsTest.java new file mode 100644 index 00000000000..b0194a19478 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/VersionEventsTest.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.observation; + +import org.apache.jackrabbit.test.api.observation.AbstractObservationTest; +import org.apache.jackrabbit.test.api.observation.EventResult; +import org.apache.jackrabbit.core.UserTransactionImpl; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.ObservationManager; +import javax.jcr.version.Version; +import javax.transaction.UserTransaction; +import java.util.HashSet; +import java.util.Set; + +/** + * VersionEventsTest checks if the correct events are triggered + * for version operations. + */ +public class VersionEventsTest extends AbstractObservationTest { + + /** + * Test if checkin of a node creates two add node events: one for the + * version and one for the frozen node. + */ + public void testCheckin() throws RepositoryException { + // create versionable node + Node n1 = testRootNode.addNode(nodeName1); + n1.addMixin(mixVersionable); + testRootNode.save(); + + EventResult listener = new EventResult(log); + addEventListener(listener, Event.NODE_ADDED); + Version v = n1.checkin(); + removeEventListener(listener); + + Event[] events = listener.getEvents(1000); + Set paths = new HashSet(); + for (int i = 0; i < events.length; i++) { + paths.add(events[i].getPath()); + } + + assertTrue("missing 'node added' event: " + v.getPath(), paths.contains(v.getPath())); + String frozenPath = v.getPath() + "/" + jcrFrozenNode; + assertTrue("missing 'node added' event: " + frozenPath, paths.contains(frozenPath)); + } + + /** + * Test if checkin of a node in an XA transaction creates two add node + * events: one for the version and one for the frozen node. + */ + public void testXACheckin() throws Exception { + // create versionable node + Node n1 = testRootNode.addNode(nodeName1); + n1.addMixin(mixVersionable); + testRootNode.save(); + + EventResult listener = new EventResult(log); + addEventListener(listener, Event.NODE_ADDED); + + // use a transaction + UserTransaction utx = new UserTransactionImpl(superuser); + // start transaction + utx.begin(); + Version v = n1.checkin(); + // commit transaction + utx.commit(); + + removeEventListener(listener); + + Event[] events = listener.getEvents(1000); + Set paths = new HashSet(); + for (int i = 0; i < events.length; i++) { + paths.add(events[i].getPath()); + } + + assertTrue("missing 'node added' event: " + v.getPath(), paths.contains(v.getPath())); + String frozenPath = v.getPath() + "/" + jcrFrozenNode; + assertTrue("missing 'node added' event: " + frozenPath, paths.contains(frozenPath)); + } + + /** + * Test if removing a version triggers two node removed events: one for the + * version and one for the frozen node. + */ + public void testRemoveVersion() throws RepositoryException { + // create versionable node + Node n1 = testRootNode.addNode(nodeName1); + n1.addMixin(mixVersionable); + testRootNode.save(); + + Version v = n1.checkin(); + String versionPath = v.getPath(); + + n1.remove(); + testRootNode.save(); + + EventResult listener = new EventResult(log); + addEventListener(listener, Event.NODE_REMOVED); + v.getContainingHistory().removeVersion(v.getName()); + removeEventListener(listener); + + Event[] events = listener.getEvents(1000); + Set paths = new HashSet(); + for (int i = 0; i < events.length; i++) { + paths.add(events[i].getPath()); + } + + assertTrue("missing 'node removed' event: " + versionPath, paths.contains(versionPath)); + String frozenPath = versionPath + "/" + jcrFrozenNode; + assertTrue("missing 'node removed' event: " + frozenPath, paths.contains(frozenPath)); + } + + /** + * Test if removing a version in an XA transaction triggers two node removed + * events: one for the version and one for the frozen node. + */ + public void testXARemoveVersion() throws Exception { + // create versionable node + Node n1 = testRootNode.addNode(nodeName1); + n1.addMixin(mixVersionable); + testRootNode.save(); + + Version v = n1.checkin(); + String versionPath = v.getPath(); + + n1.remove(); + testRootNode.save(); + + EventResult listener = new EventResult(log); + addEventListener(listener, Event.NODE_REMOVED); + + // use a transaction + UserTransaction utx = new UserTransactionImpl(superuser); + // start transaction + utx.begin(); + v.getContainingHistory().removeVersion(v.getName()); + // commit transaction + utx.commit(); + + removeEventListener(listener); + + Event[] events = listener.getEvents(1000); + Set paths = new HashSet(); + for (int i = 0; i < events.length; i++) { + paths.add(events[i].getPath()); + } + + assertTrue("missing 'node removed' event: " + versionPath, paths.contains(versionPath)); + String frozenPath = versionPath + "/" + jcrFrozenNode; + assertTrue("missing 'node removed' event: " + frozenPath, paths.contains(frozenPath)); + } + + /** + * Test if checkin creates events also on a different workspace than the one + * where the checkin was executed. + */ + public void testCheckinOtherWorkspace() throws RepositoryException { + // create versionable node + Node n1 = testRootNode.addNode(nodeName1); + n1.addMixin(mixVersionable); + testRootNode.save(); + + Session s = getHelper().getReadOnlySession(workspaceName); + try { + EventResult listener = new EventResult(log); + ObservationManager obsMgr = s.getWorkspace().getObservationManager(); + obsMgr.addEventListener(listener, Event.NODE_ADDED, "/", true, + null, null, false); + Version v = n1.checkin(); + obsMgr.removeEventListener(listener); + + Event[] events = listener.getEvents(1000); + Set paths = new HashSet(); + for (int i = 0; i < events.length; i++) { + paths.add(events[i].getPath()); + } + + assertTrue("missing 'node removed': " + v.getPath(), paths.contains(v.getPath())); + String frozenPath = v.getPath() + "/" + jcrFrozenNode; + assertTrue("missing 'node removed': " + frozenPath, paths.contains(frozenPath)); + } finally { + s.logout(); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/WarningOnSaveWithNotificationThreadTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/WarningOnSaveWithNotificationThreadTest.java new file mode 100644 index 00000000000..3e7666112e2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/observation/WarningOnSaveWithNotificationThreadTest.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.core.observation; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; + +import org.apache.jackrabbit.core.Tail; +import org.apache.jackrabbit.test.api.observation.AbstractObservationTest; +import org.apache.jackrabbit.test.api.observation.EventResult; + +/** + * WarningOnSaveWithNotificationThreadTest checks if the repository + * writes a warning to the log file when changes are performed with the event + * notification thread. See JCR-3426. + */ +public class WarningOnSaveWithNotificationThreadTest extends + AbstractObservationTest { + + private static final String MESSAGE = "Save call with event notification thread detected"; + + public void testWarning() throws Exception { + final List exceptions = new ArrayList(); + EventResult result = new EventResult(log) { + @Override + public void onEvent(EventIterator events) { + try { + Session s = getHelper().getSuperuserSession(); + try { + s.getNode(testRoot).addNode("bar"); + s.save(); + } finally { + s.logout(); + } + } catch (RepositoryException e) { + exceptions.add(e); + } + super.onEvent(events); + } + }; + addEventListener(result); + + Tail tail = Tail.start(new File("target", "jcr.log"), MESSAGE); + try { + testRootNode.addNode("foo"); + superuser.save(); + + removeEventListener(result); + result.getEvents(5000); + assertTrue("Warn message expected in log file.", + tail.getLines().iterator().hasNext()); + } finally { + tail.close(); + } + + if (!exceptions.isEmpty()) { + fail(exceptions.get(0).toString()); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/AutoFixCorruptNode.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/AutoFixCorruptNode.java new file mode 100644 index 00000000000..c13b10d7165 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/AutoFixCorruptNode.java @@ -0,0 +1,606 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.UUID; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.TestHelper; +import org.apache.jackrabbit.core.TransientRepository; +import org.apache.jackrabbit.core.persistence.check.ConsistencyReport; + +import junit.framework.TestCase; + +/** + * Tests that a corrupt node is automatically fixed. + */ +public class AutoFixCorruptNode extends TestCase { + + private final String TEST_DIR = "target/temp/" + getClass().getSimpleName(); + + public void setUp() throws Exception { + FileUtils.deleteDirectory(new File(TEST_DIR)); + } + + public void tearDown() throws Exception { + setUp(); + } + + /** + * Unit test for JCR-3069 + */ + public void testAutoFixWithConsistencyCheck() throws Exception { + + // new repository + TransientRepository rep = new TransientRepository(new File(TEST_DIR)); + Session s = openSession(rep, false); + Node root = s.getRootNode(); + + // add nodes /test and /test/missing + Node test = root.addNode("test"); + Node missing = test.addNode("missing"); + missing.addMixin("mix:referenceable"); + UUID id = UUID.fromString(missing.getIdentifier()); + s.save(); + s.logout(); + + destroyBundle(id, "workspaces/default"); + + s = openSession(rep, false); + try { + ConsistencyReport r = TestHelper.checkConsistency(s, false, null); + assertNotNull(r); + assertNotNull(r.getItems()); + assertEquals(1, r.getItems().size()); + assertEquals(test.getIdentifier(), r.getItems().iterator().next() + .getNodeId()); + } finally { + s.logout(); + rep.shutdown(); + FileUtils.deleteDirectory(new File("repository")); + } + } + + public void testOrphan() throws Exception { + + // new repository + TransientRepository rep = new TransientRepository(new File(TEST_DIR)); + Session s = openSession(rep, false); + + try { + Node root = s.getRootNode(); + + Node parent = root.addNode("parent"); + Node test = parent.addNode("test"); + test.addMixin("mix:referenceable"); + + String lost = test.getIdentifier(); + + Node lnf = root.addNode("lost+found"); + lnf.addMixin("mix:referenceable"); + String lnfid = lnf.getIdentifier(); + + s.save(); + + Node brokenNode = parent; + UUID destroy = UUID.fromString(brokenNode.getIdentifier()); + s.logout(); + + destroyBundle(destroy, "workspaces/default"); + + s = openSession(rep, false); + ConsistencyReport report = TestHelper.checkConsistency(s, false, null); + assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty()); + + // now retry with lost+found functionality + ConsistencyReport report2 = TestHelper.checkConsistency(s, true, lnfid); + assertTrue("Report should have reported broken nodes", !report2.getItems().isEmpty()); + s.logout(); + + s = openSession(rep, false); + Node q = s.getNodeByIdentifier(lost); + + // check the node was moved + assertEquals(lnfid, q.getParent().getIdentifier()); + } finally { + s.logout(); + } + } + + public void testMissingVHR() throws Exception { + + // new repository + TransientRepository rep = new TransientRepository(new File(TEST_DIR)); + Session s = openSession(rep, false); + + String oldVersionRecoveryProp = System + .getProperty("org.apache.jackrabbit.version.recovery"); + + try { + Node root = s.getRootNode(); + + Node test = root.addNode("test"); + test.addMixin("mix:versionable"); + + s.save(); + + Node vhr = s.getWorkspace().getVersionManager() + .getVersionHistory(test.getPath()); + + assertNotNull(vhr); + + Node brokenNode = vhr; + String vhrRootVersionId = vhr.getNode("jcr:rootVersion").getIdentifier(); + UUID destroy = UUID.fromString(brokenNode.getIdentifier()); + s.logout(); + + destroyBundle(destroy, "version"); + + s = openSession(rep, false); + + ConsistencyReport report = TestHelper.checkVersionStoreConsistency(s, false, null); + assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty()); + + try { + test = s.getRootNode().getNode("test"); + vhr = s.getWorkspace().getVersionManager() + .getVersionHistory(test.getPath()); + fail("should not get here"); + } catch (Exception ex) { + // expected + } + + s.logout(); + + System.setProperty("org.apache.jackrabbit.version.recovery", "true"); + + s = openSession(rep, false); + + test = s.getRootNode().getNode("test"); + // versioning should be disabled now + assertFalse(test.isNodeType("mix:versionable")); + + try { + // try to enable versioning again + test.addMixin("mix:versionable"); + s.save(); + + fail("enabling versioning succeeded unexpectedly"); + } + catch (Exception e) { + // we expect this to fail + } + + s.logout(); + + // now redo after running fixup on versioning storage + s = openSession(rep, false); + + report = TestHelper.checkVersionStoreConsistency(s, true, null); + assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty()); + int reportitems = report.getItems().size(); + + // problems should now be fixed + report = TestHelper.checkVersionStoreConsistency(s, false, null); + assertTrue("Some problems should have been fixed but are not: " + report, report.getItems().size() < reportitems); + + // get a fresh session + s.logout(); + s = openSession(rep, false); + + test = s.getRootNode().getNode("test"); + // versioning should be disabled now + assertFalse(test.isNodeType("mix:versionable")); + + // try to enable versioning again + test.addMixin("mix:versionable"); + s.save(); + + Node oldRootVersion = s.getNodeByIdentifier(vhrRootVersionId); + try { + String path = oldRootVersion.getPath(); + fail("got path " + path + " for a node believed to be orphaned"); + } + catch (ItemNotFoundException ex) { + // orphaned + } + + Node newRootVersion = s.getWorkspace().getVersionManager() + .getVersionHistory(test.getPath()).getRootVersion(); + assertFalse( + "new root version should be a different node than the one destroyed by the test case", + newRootVersion.getIdentifier().equals(vhrRootVersionId)); + assertNotNull("new root version should have a intact path", + newRootVersion.getPath()); + } finally { + s.logout(); + System.setProperty("org.apache.jackrabbit.version.recovery", + oldVersionRecoveryProp == null ? "" + : oldVersionRecoveryProp); + } + } + + public void testMissingRootVersion() throws Exception { + + // new repository + TransientRepository rep = new TransientRepository(new File(TEST_DIR)); + Session s = openSession(rep, false); + + String oldVersionRecoveryProp = System + .getProperty("org.apache.jackrabbit.version.recovery"); + + try { + Node root = s.getRootNode(); + + // add nodes /test and /test/missing + Node test = root.addNode("test", "nt:file"); + test.addNode("jcr:content", "nt:unstructured"); + test.addMixin("mix:versionable"); + s.save(); + + s.getWorkspace().getVersionManager().checkout(test.getPath()); + s.getWorkspace().getVersionManager().checkin(test.getPath()); + + Node vhr = s.getWorkspace().getVersionManager() + .getVersionHistory(test.getPath()); + + assertNotNull(vhr); + + Node brokenNode = vhr.getNode("jcr:rootVersion"); + String vhrId = vhr.getIdentifier(); + + UUID destroy = UUID.fromString(brokenNode.getIdentifier()); + s.logout(); + + destroyBundle(destroy, "version"); + + s = openSession(rep, false); + + ConsistencyReport report = TestHelper.checkVersionStoreConsistency(s, false, null); + assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty()); + + try { + test = s.getRootNode().getNode("test"); + vhr = s.getWorkspace().getVersionManager() + .getVersionHistory(test.getPath()); + fail("should not get here"); + } catch (Exception ex) { + // expected + } + + s.logout(); + + System.setProperty("org.apache.jackrabbit.version.recovery", "true"); + + s = openSession(rep, false); + + test = s.getRootNode().getNode("test"); + // versioning should be disabled now + assertFalse(test.isNodeType("mix:versionable")); + + try { + // try to enable versioning again + test.addMixin("mix:versionable"); + s.save(); + + fail("enabling versioning succeeded unexpectedly"); + } + catch (Exception e) { + // we expect this to fail + } + + s.logout(); + + // now redo after running fixup on versioning storage + s = openSession(rep, false); + + report = TestHelper.checkVersionStoreConsistency(s, true, null); + assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty()); + int reportitems = report.getItems().size(); + + // problems should now be fixed + report = TestHelper.checkVersionStoreConsistency(s, false, null); + assertTrue("Some problems should have been fixed but are not: " + report, report.getItems().size() < reportitems); + + test = s.getRootNode().getNode("test"); + // versioning should be disabled now + assertFalse(test.isNodeType("mix:versionable")); + // jcr:uuid property should still be present + assertTrue(test.hasProperty("jcr:uuid")); + // ...and have a proper definition + assertNotNull(test.getProperty("jcr:uuid").getDefinition().getName()); + // try to enable versioning again + test.addMixin("mix:versionable"); + s.save(); + + Node oldVHR = s.getNodeByIdentifier(vhrId); + Node newVHR = s.getWorkspace().getVersionManager().getVersionHistory(test.getPath()); + + assertTrue("old and new version history path should be different: " + + oldVHR.getPath() + " vs " + newVHR.getPath(), !oldVHR + .getPath().equals(newVHR.getPath())); + + // name should be same plus suffix + assertTrue(oldVHR.getName().startsWith(newVHR.getName())); + + // try a checkout / checkin + s.getWorkspace().getVersionManager().checkout(test.getPath()); + s.getWorkspace().getVersionManager().checkin(test.getPath()); + + validateDisconnectedVHR(oldVHR); + } finally { + s.logout(); + System.setProperty("org.apache.jackrabbit.version.recovery", + oldVersionRecoveryProp == null ? "" + : oldVersionRecoveryProp); + } + } + + // similar to above, but disconnects version history before damaging the repository + public void testMissingRootVersion2() throws Exception { + + // new repository + TransientRepository rep = new TransientRepository(new File(TEST_DIR)); + Session s = openSession(rep, false); + + String oldVersionRecoveryProp = System + .getProperty("org.apache.jackrabbit.version.recovery"); + + try { + Node root = s.getRootNode(); + + // add nodes /test and /test/missing + Node test = root.addNode("test"); + test.addMixin("mix:versionable"); + + s.save(); + + Node vhr = s.getWorkspace().getVersionManager() + .getVersionHistory(test.getPath()); + + assertNotNull(vhr); + + Node brokenNode = vhr.getNode("jcr:rootVersion"); + String vhrId = vhr.getIdentifier(); + + UUID destroy = UUID.fromString(brokenNode.getIdentifier()); + + // disable versioning + test.removeMixin("mix:versionable"); + s.save(); + + s.logout(); + + + destroyBundle(destroy, "version"); + + s = openSession(rep, false); + + ConsistencyReport report = TestHelper.checkVersionStoreConsistency(s, false, null); + assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty()); + + s.logout(); + + System.setProperty("org.apache.jackrabbit.version.recovery", "true"); + + s = openSession(rep, false); + s.logout(); + + s = openSession(rep, false); + + test = s.getRootNode().getNode("test"); + // versioning should still be disabled + assertFalse(test.isNodeType("mix:versionable")); + + // try to enable versioning again + test.addMixin("mix:versionable"); + s.save(); + + Node oldVHR = s.getNodeByIdentifier(vhrId); + Node newVHR = s.getWorkspace().getVersionManager().getVersionHistory(test.getPath()); + + assertTrue("old and new version history path should be different: " + + oldVHR.getPath() + " vs " + newVHR.getPath(), !oldVHR + .getPath().equals(newVHR.getPath())); + + // name should be same plus suffix + assertTrue(oldVHR.getName().startsWith(newVHR.getName())); + + // try a checkout / checkin + s.getWorkspace().getVersionManager().checkout(test.getPath()); + s.getWorkspace().getVersionManager().checkin(test.getPath()); + + validateDisconnectedVHR(oldVHR); + } finally { + s.logout(); + System.setProperty("org.apache.jackrabbit.version.recovery", + oldVersionRecoveryProp == null ? "" + : oldVersionRecoveryProp); + } + } + + // tests recovery from a broken hierarchy in the version store + public void testBrokenVhrParent() throws Exception { + + // new repository + TransientRepository rep = new TransientRepository(new File(TEST_DIR)); + Session s = openSession(rep, false); + + try { + Node root = s.getRootNode(); + + // add node /test + Node test = root.addNode("test"); + test.addMixin("mix:versionable"); + + s.save(); + + Node vhr = s.getWorkspace().getVersionManager().getVersionHistory(test.getPath()); + + assertNotNull(vhr); + + Node brokenNode = vhr.getParent().getParent(); + + UUID destroy = UUID.fromString(brokenNode.getIdentifier()); + + // disable versioning + test.removeMixin("mix:versionable"); + s.save(); + + s.logout(); + + destroyBundle(destroy, "version"); + + s = openSession(rep, false); + + ConsistencyReport report = TestHelper.checkVersionStoreConsistency(s, true, null); + assertTrue("Report should have reported broken nodes", !report.getItems().isEmpty()); + + s.logout(); + + s = openSession(rep, false); + + test = s.getRootNode().getNode("test"); + // versioning should still be disabled + assertFalse(test.isNodeType("mix:versionable")); + + // try to enable versioning again + test.addMixin("mix:versionable"); + s.save(); + + // try a checkout / checkin + s.getWorkspace().getVersionManager().checkout(test.getPath()); + s.getWorkspace().getVersionManager().checkin(test.getPath()); + } finally { + s.logout(); + } + } + + public void testAutoFix() throws Exception { + + // new repository + TransientRepository rep = new TransientRepository(new File(TEST_DIR)); + Session s = openSession(rep, false); + Node root = s.getRootNode(); + + // add nodes /test and /test/missing + Node test = root.addNode("test"); + Node missing = test.addNode("missing"); + missing.addMixin("mix:referenceable"); + UUID id = UUID.fromString(missing.getIdentifier()); + s.save(); + s.logout(); + + destroyBundle(id, "workspaces/default"); + + // login and try the operation + s = openSession(rep, false); + test = s.getRootNode().getNode("test"); + + // try to add a node with the same name + try { + test.addNode("missing"); + s.save(); + } catch (RepositoryException e) { + // expected + } + + s.logout(); + + s = openSession(rep, true); + test = s.getRootNode().getNode("test"); + // iterate over all child nodes fixes the corruption + NodeIterator it = test.getNodes(); + while (it.hasNext()) { + it.nextNode(); + } + + // try to add a node with the same name + test.addNode("missing"); + s.save(); + + // try to delete the parent node + test.remove(); + s.save(); + + s.logout(); + rep.shutdown(); + + FileUtils.deleteDirectory(new File("repository")); + } + + private void destroyBundle(UUID id, String where) throws SQLException { + Connection conn = DriverManager.getConnection("jdbc:derby:" + TEST_DIR + + "/" + where + "/db"); + String table = where.equals("version") ? "VERSION_BUNDLE" : "DEFAULT_BUNDLE"; + PreparedStatement prep = conn.prepareStatement("delete from " + table + + " where NODE_ID_HI=? and NODE_ID_LO=?"); + prep.setLong(1, id.getMostSignificantBits()); + prep.setLong(2, id.getLeastSignificantBits()); + prep.executeUpdate(); + conn.close(); + } + + private Session openSession(Repository rep, boolean autoFix) + throws RepositoryException { + SimpleCredentials cred = new SimpleCredentials("admin", + "admin".toCharArray()); + if (autoFix) { + cred.setAttribute("org.apache.jackrabbit.autoFixCorruptions", + "true"); + } + return rep.login(cred); + } + + // JCR-4118: check that the old VHR can be retrieved + private void validateDisconnectedVHR(Node oldVHR) throws RepositoryException { + Session s = oldVHR.getSession(); + Node old = s.getNode(oldVHR.getPath()); + assertNotNull("disconnected VHR should be accessible", old); + + assertEquals("nt:versionHistory", old.getPrimaryNodeType().getName()); + NodeIterator ni = old.getNodes(); + while (ni.hasNext()) { + Node n = ni.nextNode(); + String type = n.getPrimaryNodeType().getName(); + assertTrue("node type of VHR child nodes should be nt:version or nt:versionLabels", + "nt:version".equals(type) || "nt:versionLabels".equals(type)); + } + + try { + old.remove(); + s.save(); + fail("removal of node using remove() should throw because it's in the versioning workspace"); + } catch (ConstraintViolationException expected) { + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/PersistenceManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/PersistenceManagerTest.java new file mode 100644 index 00000000000..d314f4898fd --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/PersistenceManagerTest.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.core.persistence; + +import java.io.File; +import java.util.Arrays; + +import javax.jcr.PropertyType; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.NamespaceRegistryImpl; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.fs.mem.MemoryFileSystem; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.persistence.mem.InMemBundlePersistenceManager; +import org.apache.jackrabbit.core.persistence.mem.InMemPersistenceManager; +import org.apache.jackrabbit.core.persistence.obj.ObjectPersistenceManager; +import org.apache.jackrabbit.core.persistence.xml.XMLPersistenceManager; +import org.apache.jackrabbit.core.state.ChangeLog; +import org.apache.jackrabbit.core.state.ItemState; +import org.apache.jackrabbit.core.state.NoSuchItemStateException; +import org.apache.jackrabbit.core.state.NodeReferences; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.core.state.PropertyState; +import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; + +public class PersistenceManagerTest extends TestCase { + + private static final NodeId NODE_ID = NodeId.randomId(); + + private static final NodeId CHILD_ID = NodeId.randomId(); + + private static final Name TEST = + NameFactoryImpl.getInstance().create("", "test"); + + private static final PropertyId PROPERTY_ID = new PropertyId(NODE_ID, TEST); + + private File directory; + + private File database; + + protected void setUp() throws Exception { + directory = File.createTempFile("jackrabbit-persistence-", "-test"); + directory.delete(); + directory.mkdirs(); + + database = File.createTempFile("jackrabbit-persistence-", "-derby"); + database.delete(); + } + + protected void tearDown() throws Exception { + FileUtils.deleteQuietly(database); + FileUtils.deleteQuietly(directory); + } + + public void testInMemPersistenceManager() throws Exception { + assertPersistenceManager(new InMemPersistenceManager()); + } + + public void testInMemBundlePersistenceManager() throws Exception { + assertPersistenceManager(new InMemBundlePersistenceManager()); + } + + public void testXMLPersistenceManager() throws Exception { + assertPersistenceManager(new XMLPersistenceManager()); + } + + public void testObjectPersistenceManager() throws Exception { + assertPersistenceManager(new ObjectPersistenceManager()); + } + + public void testDerbyDatabasePersistenceManager() throws Exception { + org.apache.jackrabbit.core.persistence.db.DerbyPersistenceManager manager = + new org.apache.jackrabbit.core.persistence.db.DerbyPersistenceManager(); + manager.setDriver("org.apache.derby.jdbc.EmbeddedDriver"); + manager.setUrl("jdbc:derby:" + database.getPath() + ";create=true"); + manager.setConnectionFactory(new ConnectionFactory()); + assertPersistenceManager(manager); + } + + public void testDerbyPoolPersistenceManager() throws Exception { + org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager manager = + new org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager(); + manager.setDriver("org.apache.derby.jdbc.EmbeddedDriver"); + manager.setUrl("jdbc:derby:" + database.getPath() + ";create=true"); + manager.setConnectionFactory(new ConnectionFactory()); + assertPersistenceManager(manager); + } + + public void testH2PoolPersistenceManager() throws Exception { + org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager manager = + new org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager(); + manager.setDriver("org.h2.Driver"); + manager.setUrl("jdbc:h2:mem:" + database.getPath()); + manager.setConnectionFactory(new ConnectionFactory()); + assertPersistenceManager(manager); + } + + private void assertPersistenceManager(PersistenceManager manager) + throws Exception { + manager.init(new PMContext( + directory, + new MemoryFileSystem(), + RepositoryImpl.ROOT_NODE_ID, + new NamespaceRegistryImpl(new MemoryFileSystem()), + null, + null, + new RepositoryStatisticsImpl())); + try { + assertCreateNewNode(manager); + assertCreateNewProperty(manager); + assertMissingItemStates(manager); + assertCreateUpdateDelete(manager); + } finally { + manager.close(); + } + } + + private void assertCreateNewNode(PersistenceManager manager) { + NodeState state = manager.createNew(NODE_ID); + assertNotNull(state); + assertEquals(NODE_ID, state.getId()); + assertEquals(NODE_ID, state.getNodeId()); + + assertTrue(state.isNode()); + assertFalse(state.isTransient()); + assertFalse(state.isStale()); + assertFalse(state.isShareable()); + + assertNull(state.getContainer()); + assertEquals(0, state.getModCount()); + assertTrue(state.getChildNodeEntries().isEmpty()); + assertTrue(state.getPropertyNames().isEmpty()); + } + + private void assertCreateNewProperty(PersistenceManager manager) { + PropertyState state = manager.createNew(PROPERTY_ID); + assertNotNull(state); + assertEquals(PROPERTY_ID, state.getId()); + assertEquals(PROPERTY_ID, state.getPropertyId()); + assertEquals(NODE_ID, state.getParentId()); + + assertFalse(state.isNode()); + assertFalse(state.isTransient()); + assertFalse(state.isStale()); + } + + private void assertMissingItemStates(PersistenceManager manager) + throws Exception { + assertFalse(manager.exists(NODE_ID)); + try { + manager.load(NODE_ID); + fail(); + } catch (NoSuchItemStateException expected) { + } + + assertFalse(manager.exists(PROPERTY_ID)); + try { + manager.load(PROPERTY_ID); + fail(); + } catch (NoSuchItemStateException expected) { + } + + assertFalse(manager.existsReferencesTo(NODE_ID)); + try { + manager.loadReferencesTo(NODE_ID); + fail(); + } catch (NoSuchItemStateException expected) { + } + } + + private void assertCreateUpdateDelete(PersistenceManager manager) + throws Exception { + NodeState node = new NodeState( + NODE_ID, TEST, RepositoryImpl.ROOT_NODE_ID, + ItemState.STATUS_NEW, true); + node.addChildNodeEntry(TEST, CHILD_ID); + node.addPropertyName(NameConstants.JCR_PRIMARYTYPE); + node.addPropertyName(TEST); + + NodeState child = new NodeState( + CHILD_ID, TEST, NODE_ID, ItemState.STATUS_NEW, true); + child.addPropertyName(NameConstants.JCR_PRIMARYTYPE); + + PropertyState property = + new PropertyState(PROPERTY_ID, ItemState.STATUS_NEW, true); + property.setType(PropertyType.REFERENCE); + property.setValues( + new InternalValue[] { InternalValue.create(CHILD_ID) }); + + NodeReferences references = new NodeReferences(CHILD_ID); + references.addReference(PROPERTY_ID); + + ChangeLog create = new ChangeLog(); + create.added(node); + create.added(child); + create.added(property); + create.modified(references); + manager.store(create); + + assertTrue(manager.exists(NODE_ID)); + assertTrue(manager.exists(CHILD_ID)); + assertTrue(manager.exists(PROPERTY_ID)); + assertTrue(manager.existsReferencesTo(CHILD_ID)); + + assertEquals(node, manager.load(NODE_ID)); + assertEquals(child, manager.load(CHILD_ID)); + assertEquals(property, manager.load(PROPERTY_ID)); + assertEquals(references, manager.loadReferencesTo(CHILD_ID)); + + references.removeReference(PROPERTY_ID); + node.setStatus(ItemState.STATUS_EXISTING); + ChangeLog update = new ChangeLog(); + update.modified(references); + node.removePropertyName(TEST); + update.deleted(property); + update.modified(node); + manager.store(update); + + assertTrue(manager.exists(NODE_ID)); + assertTrue(manager.exists(CHILD_ID)); + assertFalse(manager.exists(PROPERTY_ID)); + assertFalse(manager.existsReferencesTo(CHILD_ID)); + + assertEquals(node, manager.load(NODE_ID)); + assertEquals(child, manager.load(CHILD_ID)); + + ChangeLog delete = new ChangeLog(); + delete.deleted(child); + delete.deleted(node); + manager.store(delete); + + assertFalse(manager.exists(NODE_ID)); + assertFalse(manager.exists(CHILD_ID)); + assertFalse(manager.exists(PROPERTY_ID)); + assertFalse(manager.existsReferencesTo(CHILD_ID)); + } + + private void assertEquals(NodeState expected, NodeState actual) { + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getNodeId(), actual.getNodeId()); + assertEquals(expected.getNodeTypeName(), actual.getNodeTypeName()); + assertEquals(expected.getMixinTypeNames(), actual.getMixinTypeNames()); + assertEquals(expected.getPropertyNames(), actual.getPropertyNames()); + assertEquals(expected.getChildNodeEntries(), actual.getChildNodeEntries()); + } + + private void assertEquals(PropertyState expected, PropertyState actual) { + assertEquals(expected.getId(), actual.getId()); + assertEquals(expected.getPropertyId(), actual.getPropertyId()); + assertEquals(expected.getType(), actual.getType()); + assertTrue(Arrays.equals(expected.getValues(), actual.getValues())); + } + + private void assertEquals(NodeReferences expected, NodeReferences actual) { + assertEquals(expected.getTargetId(), actual.getTargetId()); + assertEquals(expected.getReferences(), actual.getReferences()); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.java new file mode 100644 index 00000000000..9f457d219ef --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/TestAll.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.core.persistence; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all test cases for this package. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.core.persistence tests"); + + suite.addTestSuite(PersistenceManagerTest.class); + suite.addTestSuite(AutoFixCorruptNode.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/BundleBindingRandomizedTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/BundleBindingRandomizedTest.java new file mode 100644 index 00000000000..a7c174b8dd8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/BundleBindingRandomizedTest.java @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.persistence.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Collections; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +import javax.jcr.PropertyType; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.persistence.util.NodePropBundle.PropertyEntry; +import org.apache.jackrabbit.core.util.StringIndex; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; + +/** + * A randomized test for the BundleBinding writer and reader classes. + */ +public class BundleBindingRandomizedTest extends TestCase { + + private static final NameFactory factory = NameFactoryImpl.getInstance(); + + /** + * Tests serialization of a complex bundle. + */ + public void testRandomBundle() throws Exception { + int seed = 0; + for (int i=0; i<100;) { + Random r = new Random(seed++); + NodePropBundle bundle; + try { + bundle = randomBundle(r); + } catch (IllegalArgumentException e) { + continue; + } + try { + if (tryBundleRoundtrip(bundle)) { + i++; + } + } catch (Exception e) { + throw new Exception( + "Error round-tripping bundle with seed " + seed, e); + + } + } + } + + private static NodePropBundle randomBundle(Random r) { + NodeId id = randomNodeId(r); + NodePropBundle bundle = new NodePropBundle(id); + bundle.setModCount((short) randomSize(r)); + if (r.nextInt(10) > 0) { + bundle.setParentId(randomNodeId(r)); + } + if (r.nextInt(100) > 0) { + bundle.setNodeTypeName(randomName(r)); + } + if (r.nextInt(100) > 0) { + bundle.setMixinTypeNames(randomNameSet(r)); + } + if (r.nextInt(10) > 0) { + bundle.setReferenceable(r.nextBoolean()); + } + if (r.nextInt(10) > 0) { + bundle.setSharedSet(randomNodeIdSet(r)); + } + int count = randomSize(r); + for (int i=0; i 0) { + size = 1; + } else { + size = randomSize(r); + } + InternalValue[] values = new InternalValue[size]; + for (int i = 0; i < size; i++) { + values[i] = randomValue(r); + } + p.setValues(values); + return p; + } + + private static InternalValue randomValue(Random r) { + if (r.nextInt(50) == 0) { + return null; + } + // TODO currently only string values + return InternalValue.create(randomString(r)); + } + + private static int randomSize(Random r) { + if (r.nextInt(20) == 0) { + return 0; + } else if (r.nextInt(20) == 0) { + return 1; + } else if (r.nextInt(20) == 0) { + return r.nextInt(10000); + } + return r.nextInt(5) + 1; + } + + private static Set randomNameSet(Random r) { + if (r.nextInt(100) == 0) { + return null; + } + int size = randomSize(r); + HashSet set = new HashSet(); + for(int i=0; i randomNodeIdSet(Random r) { + if (r.nextInt(100) == 0) { + return null; + } else if (r.nextInt(10) == 0) { + return Collections.emptySet(); + } + int size = randomSize(r); + HashSet set = new HashSet(); + for(int i=0; i strings.length){ + return ""; + } + return strings[idx]; + } + }; + binding = new BundleBinding(null, null, index, index, null); + } + + public void testEmptyBundle() throws Exception { + NodePropBundle bundle = new NodePropBundle(NodeId.randomId()); + bundle.setParentId(new NodeId(1, 2)); + bundle.setNodeTypeName(NameConstants.NT_UNSTRUCTURED); + bundle.setMixinTypeNames(Collections.emptySet()); + bundle.setSharedSet(Collections.emptySet()); + + assertBundleRoundtrip(bundle); + + assertBundleSerialization(bundle, new byte[] { + 2, 0, 0, 1, 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 2, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 0, 0, 0, 0 }); + } + + /** + * Tests serialization of a complex bundle. + */ + public void testComplexBundle() throws Exception { + NodeId id = new NodeId(1, 2); + NodePropBundle bundle = new NodePropBundle(id); + bundle.setParentId(new NodeId(3, 4)); + bundle.setNodeTypeName(NameConstants.NT_UNSTRUCTURED); + bundle.setMixinTypeNames(Collections.singleton( + NameConstants.MIX_CREATED)); + bundle.setReferenceable(true); + bundle.setSharedSet(new HashSet(Arrays.asList( + new NodeId(5, 6), new NodeId(7, 8), new NodeId(9, 10)))); + + PropertyEntry property; + + property = new PropertyEntry( + new PropertyId(id, NameConstants.JCR_CREATED)); + property.setType(PropertyType.DATE); + property.setMultiValued(false); + Calendar date = Calendar.getInstance(); + date.setTimeInMillis(1234567890); + property.setValues(new InternalValue[] { InternalValue.create(date) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, NameConstants.JCR_CREATEDBY)); + property.setType(PropertyType.STRING); + property.setMultiValued(false); + property.setValues( + new InternalValue[] { InternalValue.create("test") }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "binary"))); + property.setType(PropertyType.BINARY); + property.setMultiValued(false); + property.setValues(new InternalValue[] { InternalValue.create( + new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "boolean"))); + property.setType(PropertyType.BOOLEAN); + property.setMultiValued(true); + property.setValues(new InternalValue[] { + InternalValue.create(true), InternalValue.create(false) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "date"))); + property.setType(PropertyType.DATE); + property.setMultiValued(false); + property.setValues(new InternalValue[] { InternalValue.create(date) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "decimal"))); + property.setType(PropertyType.DECIMAL); + property.setMultiValued(false); + property.setValues(new InternalValue[] { + InternalValue.create(new BigDecimal("1234567890.0987654321")) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "double"))); + property.setType(PropertyType.DOUBLE); + property.setMultiValued(true); + property.setValues(new InternalValue[] { + InternalValue.create(1.0), InternalValue.create(Math.PI) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "long"))); + property.setType(PropertyType.LONG); + property.setMultiValued(false); + property.setValues(new InternalValue[] { + InternalValue.create(1234567890) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "name"))); + property.setType(PropertyType.NAME); + property.setMultiValued(false); + property.setValues(new InternalValue[] { + InternalValue.create(NameConstants.JCR_MIMETYPE) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "path"))); + property.setType(PropertyType.PATH); + property.setMultiValued(true); + PathFactory pathFactory = PathFactoryImpl.getInstance(); + Path root = pathFactory.getRootPath(); + Path path = pathFactory.create(root, NameConstants.JCR_SYSTEM, false); + property.setValues(new InternalValue[] { + InternalValue.create(root), InternalValue.create(path) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "reference"))); + property.setType(PropertyType.REFERENCE); + property.setMultiValued(false); + property.setValues(new InternalValue[] { + InternalValue.create(new NodeId(11, 12)) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "string"))); + property.setType(PropertyType.STRING); + property.setMultiValued(false); + property.setValues(new InternalValue[] { + InternalValue.create("test") }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "uri"))); + property.setType(PropertyType.URI); + property.setMultiValued(false); + property.setValues(new InternalValue[] { + InternalValue.create(new URI("http://jackrabbit.apache.org/")) }); + bundle.addProperty(property); + + property = new PropertyEntry( + new PropertyId(id, factory.create("", "weakreference"))); + property.setType(PropertyType.WEAKREFERENCE); + property.setMultiValued(false); + property.setValues(new InternalValue[] { + InternalValue.create(new NodeId(13, 14), true) }); + bundle.addProperty(property); + + bundle.addChildNodeEntry( + NameConstants.JCR_SYSTEM, new NodeId(15, 16)); + bundle.addChildNodeEntry( + NameConstants.JCR_VERSIONSTORAGE, new NodeId(17, 18)); + + assertBundleRoundtrip(bundle); + + assertBundleSerialization(bundle, new byte[] { + 2, 0, 0, 1, 0, 0, 0, 3, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4, -1, -1, -1, -1, + 0, 0, 0, 6, 0, 0, 0, 12, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 73, -106, 2, -46, 0, 0, 0, 6, 0, 0, 0, 16, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 116, 101, 115, 116, 0, 0, + 0, 6, 0, 0, 0, 8, 0, 0, 0, 6, 1, 0, 0, 0, 0, 0, 2, 1, 0, 0, + 0, 0, 6, 0, 0, 0, 13, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 8, 109, 105, 109, 101, 84, 121, 112, 101, 0, 0, 0, 6, + 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 6, 0, 0, 0, 18, + 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 13, + 0, 0, 0, 0, 0, 0, 0, 14, 0, 0, 0, 6, 0, 0, 0, 9, 0, 0, 0, 5, + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 29, 49, 57, 55, 48, 45, 48, 49, + 45, 49, 53, 84, 48, 55, 58, 53, 54, 58, 48, 55, 46, 56, 57, + 48, 43, 48, 49, 58, 48, 48, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, + 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 10, 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 0, 0, 0, 6, 0, 0, 0, 17, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 29, 104, 116, 116, 112, 58, 47, 47, 106, 97, 99, + 107, 114, 97, 98, 98, 105, 116, 46, 97, 112, 97, 99, 104, 101, + 46, 111, 114, 103, 47, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 116, 101, 115, 116, 0, 0, 0, 6, + 0, 0, 0, 11, 0, 0, 0, 4, 1, 0, 0, 0, 0, 0, 2, 63, -16, 0, 0, + 0, 0, 0, 0, 64, 9, 33, -5, 84, 68, 45, 24, 0, 0, 0, 0, 0, 0, + 0, 4, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 29, 49, 57, + 55, 48, 45, 48, 49, 45, 49, 53, 84, 48, 55, 58, 53, 54, 58, + 48, 55, 46, 56, 57, 48, 43, 48, 49, 58, 48, 48, 0, 0, 0, 6, + 0, 0, 0, 10, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 1, 1, 0, 21, 49, + 50, 51, 52, 53, 54, 55, 56, 57, 48, 46, 48, 57, 56, 55, 54, + 53, 52, 51, 50, 49, 0, 0, 0, 6, 0, 0, 0, 14, 0, 0, 0, 8, 1, + 0, 0, 0, 0, 0, 2, 0, 0, 0, 2, 123, 125, 0, 0, 0, 37, 123, + 125, 9, 123, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, + 46, 106, 99, 112, 46, 111, 114, 103, 47, 106, 99, 114, 47, + 49, 46, 48, 125, 115, 121, 115, 116, 101, 109, -1, -1, -1, + -1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 16, + 0, 0, 0, 0, 0, 6, 115, 121, 115, 116, 101, 109, 1, 0, 0, 0, + 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 18, 0, 0, 0, 0, 0, 14, + 118, 101, 114, 115, 105, 111, 110, 83, 116, 111, 114, 97, + 103, 101, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, + 0, 0, 0, 8, 1, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, + 10, 1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 6, 0 }); + } + + /** + * Tests serialization of custom namespaces. + */ + public void testCustomNamespaces() throws Exception { + NodePropBundle bundle = new NodePropBundle(NodeId.randomId()); + bundle.setParentId(NodeId.randomId()); + bundle.setNodeTypeName(NameConstants.NT_UNSTRUCTURED); + bundle.setMixinTypeNames(Collections.emptySet()); + bundle.setSharedSet(Collections.emptySet()); + + bundle.addChildNodeEntry(factory.create("ns1", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns2", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns3", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns4", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns5", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns6", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns7", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns8", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns1", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns1", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns2", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns3", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns1", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns2", "test"), NodeId.randomId()); + bundle.addChildNodeEntry(factory.create("ns3", "test"), NodeId.randomId()); + + assertBundleRoundtrip(bundle); + } + + public void testBooleanSerialization() throws Exception { + assertValueSerialization(InternalValue.create(true)); + assertValueSerialization(InternalValue.create(false)); + } + + /** + * Tests serialization of long values. + */ + public void testLongSerialization() throws Exception { + assertValueSerialization(InternalValue.create(0)); + assertValueSerialization(InternalValue.create(1)); + assertValueSerialization(InternalValue.create(-1)); + assertValueSerialization(InternalValue.create(1234567890)); + assertValueSerialization(InternalValue.create(-1234567890)); + assertValueSerialization(InternalValue.create(Long.MAX_VALUE)); + assertValueSerialization(InternalValue.create(Long.MIN_VALUE)); + } + + public void testDoubleSerialization() throws Exception { + assertValueSerialization(InternalValue.create(0.0)); + assertValueSerialization(InternalValue.create(1.0)); + assertValueSerialization(InternalValue.create(-1.0)); + assertValueSerialization(InternalValue.create(12345.6789)); + assertValueSerialization(InternalValue.create(-12345.6789)); + assertValueSerialization(InternalValue.create(Double.MAX_VALUE)); + assertValueSerialization(InternalValue.create(Double.MIN_VALUE)); + assertValueSerialization(InternalValue.create(Double.POSITIVE_INFINITY)); + assertValueSerialization(InternalValue.create(Double.NEGATIVE_INFINITY)); + assertValueSerialization(InternalValue.create(Double.NaN)); + } + + /** + * Tests serialization of date values. + */ + public void testDateSerialization() throws Exception { + assertDateSerialization("2010-10-10T10:10:10.100Z"); + + // Different kinds of timezone offsets + assertDateSerialization("2010-10-10T10:10:10.100+11:00"); + assertDateSerialization("2010-10-10T10:10:10.100-14:00"); + assertDateSerialization("2010-10-10T10:10:10.100+00:12"); + assertDateSerialization("2010-10-10T10:10:10.100-08:14"); + + // Different timestamp accuracies + assertDateSerialization("2010-10-10T10:10:00.000Z"); + assertDateSerialization("2010-10-10T10:00:00.000Z"); + assertDateSerialization("2010-10-10T00:00:00.000Z"); + + // Dates far from today + assertDateSerialization("1970-01-01T00:00:00.000Z"); + assertDateSerialization("1970-01-01T12:34:56.789-13:45"); + assertDateSerialization("2030-10-10T10:10:10.100+10:10"); + assertDateSerialization("2345-10-10T10:10:10.100Z"); + assertDateSerialization("+9876-10-10T10:10:10.100Z"); + assertDateSerialization("-9876-10-10T10:10:10.100Z"); + } + + /** + * Tests serialization of forbidden date values. + */ + public void testForbiddenDateSerialization() throws Exception { + + try { + // date from really far ahead + assertDateSerialization("20112022-11-11T19:00:00.000+01:00"); + fail("should not be able to serialize future dates"); + } catch (IllegalArgumentException e) { + // expected error + } + } + + /** + * creates a future date that is illegal in v3 using v2, and checks that the + * error is being hadled gracefully + * + * JCR-3083 Degrade gracefully when reading invalid date values + */ + public void testCorruptedBundle() throws Exception { + + // Bundle as described by the BundleDumper: + // version: 1 + // nodeTypeName: #13:#49 + // parentUUID: f22c788c-ab98-47cf-95ab-6c495239807a + // definitionId: 1705077083 + // mixins: - + // property: #1:#509 + // modcount: 1 + // type: Date + // definitionId: 806470580 + // value: string: 20112022-11-11T19:00:00.000+01:00 + // referenceable: false + // childId: 08026c9f-a88a-471b-bcc3-fca2bd82000b #1:linked_products + // modCount: 3 + // + // corrupted value: Date -> 20112022-11-11T19:00:00.000+01:00 + + byte[] corrupted = new byte[] { 1, 0, 0, 13, 0, 0, 0, 49, 1, -14, 44, + 120, -116, -85, -104, 71, -49, -107, -85, 108, 73, 82, 57, + -128, 122, 0, 10, 49, 55, 48, 53, 48, 55, 55, 48, 56, 51, -1, + -1, -1, -1, 0, + 0, 0, 1, 0, 0, 1, -3, 0, 1, 0, 5, 0, 0, 9, 56, 48, 54, 52, 55, + 48, 53, 56, 48, 0, 0, 0, 1, 0, 0, 0, 33, 50, 48, 49, 49, 50, + 48, 50, 50, 45, 49, 49, 45, 49, 49, 84, 49, 57, 58, 48, 48, 58, + 48, 48, 46, 48, 48, 48, 43, 48, 49, 58, 48, 48, -1, -1, -1, -1, + 0, 1, 8, 2, 108, -97, -88, -118, 71, 27, -68, -61, -4, -94, + -67, -126, 0, 11, 0, 0, 0, 1, 0, 15, 108, 105, 110, 107, 101, + 100, 95, 112, 114, 111, 100, 117, 99, 116, 115, 0, 3, 0, 0, 0, + 0, 70, -53, 75, -124 }; + + NodePropBundle result = binding.readBundle(new ByteArrayInputStream( + corrupted), NodeId.randomId()); + + Iterator iterator = result.getPropertyEntries() + .iterator(); + PropertyEntry pe = iterator.next(); + InternalValue iv = pe.getValues()[0]; + assertEquals(PropertyType.DATE, pe.getType()); + assertEquals(PropertyType.DATE, iv.getType()); + assertEquals("20112022-11-11T19:00:00.000+01:00", iv.getString()); + + try { + iv.getDate(); + fail("should not be able to read the property as a DATE"); + } catch (Exception e) { + // expected + } + } + + private void assertDateSerialization(String date) throws Exception { + assertValueSerialization( + InternalValue.valueOf(date, PropertyType.DATE)); + } + + private void assertValueSerialization(InternalValue value) + throws Exception { + NodePropBundle bundle = new NodePropBundle(NodeId.randomId()); + bundle.setParentId(NodeId.randomId()); + bundle.setNodeTypeName(NameConstants.NT_UNSTRUCTURED); + bundle.setMixinTypeNames(Collections.emptySet()); + bundle.setSharedSet(Collections.emptySet()); + + Name name = factory.create("", "test"); + + PropertyEntry property = + new PropertyEntry(new PropertyId(bundle.getId(), name)); + property.setType(value.getType()); + property.setMultiValued(false); + property.setValues(new InternalValue[] { value }); + bundle.addProperty(property); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + binding.writeBundle(buffer, bundle); + byte[] bytes = buffer.toByteArray(); + NodePropBundle result = + binding.readBundle(new ByteArrayInputStream(bytes), bundle.getId()); + + assertEquals(value, result.getPropertyEntry(name).getValues()[0]); + } + + private void assertBundleRoundtrip(NodePropBundle bundle) + throws Exception { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + binding.writeBundle(buffer, bundle); + byte[] bytes = buffer.toByteArray(); + + assertEquals(bundle, binding.readBundle( + new ByteArrayInputStream(bytes), bundle.getId())); + } + + private void assertBundleSerialization(NodePropBundle bundle, byte[] data) + throws Exception { + assertEquals(bundle, binding.readBundle( + new ByteArrayInputStream(data), bundle.getId())); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/HashMapIndexTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/HashMapIndexTest.java new file mode 100644 index 00000000000..47a6a03a0f8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/HashMapIndexTest.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.core.persistence.util; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.core.util.StringIndex; + +public class HashMapIndexTest extends TestCase { + + private StringIndex index; + + private int load; + private int save; + + protected void setUp() throws Exception { + index = new HashMapIndex() { + @Override + protected void load() { + load++; + } + @Override + protected void save() { + save++; + } + }; + load = 0; + save = 0; + } + + public void testIndex() { + assertEquals(0, load); + assertEquals(0, save); + + int test = index.stringToIndex("test"); + assertEquals(1, load); + assertEquals(1, save); + + assertEquals(test, index.stringToIndex("test")); + assertEquals(1, load); + assertEquals(1, save); + + assertEquals("test", index.indexToString(test)); + assertEquals(1, load); + assertEquals(1, save); + + assertNull(index.indexToString(test + 1)); + assertEquals(2, load); + assertEquals(1, save); + + int foo = index.stringToIndex("foo"); + assertTrue(test != foo); + assertEquals(3, load); + assertEquals(2, save); + + assertEquals(foo, index.stringToIndex("foo")); + assertEquals(3, load); + assertEquals(2, save); + + assertEquals("foo", index.indexToString(foo)); + assertEquals(3, load); + assertEquals(2, save); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/NodeCorruptionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/NodeCorruptionTest.java new file mode 100644 index 00000000000..43703c6475e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/NodeCorruptionTest.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.core.persistence.util; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * Tests that a node or a node bundle can not get internally corrupt. + */ +public class NodeCorruptionTest extends AbstractJCRTest { + + public void testCopyMultiSingleValue() throws Exception { + Node root = superuser.getRootNode(); + String nodeName = "testCopyMulti" + System.currentTimeMillis(); + if (root.hasNode(nodeName)) { + root.getNode(nodeName).remove(); + superuser.save(); + } + Node test = root.addNode(nodeName); + test.setProperty("x", "Hi"); + superuser.save(); + + String wsp = superuser.getWorkspace().getName(); + String workspace2 = getAlternativeWorkspaceName(); + if (workspace2 == null) { + throw new NotExecutableException(); + } + Session s2 = getHelper().getSuperuserSession(workspace2); + s2.getWorkspace().clone(wsp, "/" + nodeName, "/" + nodeName, true); + + Node test2 = s2.getRootNode().getNode(nodeName); + test2.setProperty("x", (Value) null); + test2.setProperty("x", new String[]{}); + s2.save(); + + test.update(workspace2); + + try { + Value[] values = test.getProperty("x").getValues(); + assertEquals(0, values.length); + } catch (RepositoryException e) { + // if we get here, it's a bug, as it is a multi-valued property now + // anyway, let's see what happens if we try to read it as a single valued property + test.getProperty("x").getValue(); + // even if that works: it's still a bug + throw e; + } + + } + + private String getAlternativeWorkspaceName() throws RepositoryException { + String altWsp = null; + String[] wsps = superuser.getWorkspace().getAccessibleWorkspaceNames(); + if (wsps.length == 1) { + superuser.getWorkspace().createWorkspace("tmp"); + altWsp = "tmp"; + } else { + for (String name : wsps) { + if (!name.equals(superuser.getWorkspace().getName())) { + altWsp = name; + break; + } + } + } + return altWsp; + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/TestAll.java new file mode 100644 index 00000000000..ce5d0306417 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/persistence/util/TestAll.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.core.persistence.util; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("Persistence utility tests"); + + suite.addTestSuite(HashMapIndexTest.class); + suite.addTestSuite(BundleBindingTest.class); + suite.addTestSuite(NodeCorruptionTest.class); + suite.addTestSuite(BundleBindingRandomizedTest.class); + + return suite; + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/AbstractIndexingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/AbstractIndexingTest.java new file mode 100644 index 00000000000..e9fa81cfaeb --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/AbstractIndexingTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.QueryManager; +import javax.jcr.query.QueryResult; + +import org.apache.jackrabbit.core.TestHelper; +import org.apache.jackrabbit.core.query.lucene.SearchIndex; + +/** + * AbstractIndexingTest is a base class for all indexing + * configuration tests. + */ +public class AbstractIndexingTest extends AbstractQueryTest { + + private static final String WORKSPACE_NAME = "indexing-test"; + + protected Session session; + + protected Node testRootNode; + + protected void setUp() throws Exception { + super.setUp(); + session = getHelper().getSuperuserSession(getWorkspaceName()); + testRootNode = cleanUpTestRoot(session); + // overwrite query manager + qm = session.getWorkspace().getQueryManager(); + } + + protected void tearDown() throws Exception { + if (session != null) { + cleanUpTestRoot(session); + session.logout(); + session = null; + } + testRootNode = null; + super.tearDown(); + } + + protected String getWorkspaceName() { + return WORKSPACE_NAME; + } + + /** + * wait for async text-extraction tasks to finish + */ + protected void waitForTextExtractionTasksToFinish() throws Exception { + TestHelper.waitForTextExtractionTasksToFinish(session); + flushSearchIndex(); + } + + protected void flushSearchIndex() throws RepositoryException { + SearchIndex si = getSearchIndex(); + if (si != null) { + si.flush(); + } + } + + /** + * Returns a reference to the underlying search index. + * + * @return the query handler inside the {@link #qm query manager}. + */ + protected SearchIndex getSearchIndex() { + if (qm instanceof QueryManagerImpl) { + return (SearchIndex) ((QueryManagerImpl) qm).getQueryHandler(); + } + return null; + } + + /** + * Returns a reference to the session's search index. + * + * @return the session's query handler. + */ + protected static SearchIndex getSearchIndex(Session session) + throws RepositoryException { + QueryManager qm = session.getWorkspace().getQueryManager(); + if (qm instanceof QueryManagerImpl) { + return (SearchIndex) ((QueryManagerImpl) qm).getQueryHandler(); + } + return null; + } + + protected QueryResult executeQuery(String statement) + throws RepositoryException { + flushSearchIndex(); + return super.executeQuery(statement); + } + + protected void executeXPathQuery(String xpath, Node[] nodes) + throws RepositoryException { + flushSearchIndex(); + super.executeXPathQuery(xpath, nodes); + } + + protected void executeSQLQuery(String sql, Node[] nodes) + throws RepositoryException { + flushSearchIndex(); + super.executeSQLQuery(sql, nodes); + } + + protected void executeSQL2Query(String statement, Node[] nodes) + throws RepositoryException { + flushSearchIndex(); + super.executeSQL2Query(statement, nodes); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/AbstractQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/AbstractQueryTest.java new file mode 100644 index 00000000000..8b51baa571d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/AbstractQueryTest.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import static javax.jcr.query.Query.JCR_SQL2; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +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 javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Abstract base class for query test cases. + */ +public class AbstractQueryTest extends AbstractJCRTest { + + protected QueryManager qm; + + protected QueryObjectModelFactory qomFactory; + + protected void setUp() throws Exception { + super.setUp(); + qm = superuser.getWorkspace().getQueryManager(); + qomFactory = qm.getQOMFactory(); + } + + protected void tearDown() throws Exception { + qm = null; + qomFactory = null; + super.tearDown(); + } + + /** + * Checks if the result contains a number of hits. + * + * @param result the QueryResult. + * @param hits the number of expected hits. + * @throws RepositoryException if an error occurs while iterating over + * the result nodes. + */ + protected void checkResult(QueryResult result, int hits) + throws RepositoryException { + int count = 0; + log.println("Rows:"); + for (Row row : JcrUtils.getRows(result)) { + log.println(" " + row); + count++; + } + if (count == 0) { + log.println(" NONE"); + } + assertEquals("Wrong hit count.", hits, count); + } + + /** + * Checks if the result contains a number of hits + * and columns. + * + * @param result the QueryResult. + * @param hits the number of expected hits. + * @param columns the number of expected columns. + * @throws RepositoryException if an error occurs while iterating over the + * result nodes. + */ + protected void checkResult(QueryResult result, int hits, int columns) + throws RepositoryException { + checkResult(result, hits); + // now check column count + int count = 0; + log.println("Properties:"); + String[] propNames = result.getColumnNames(); + for (RowIterator it = result.getRows(); it.hasNext(); count++) { + StringBuffer msg = new StringBuffer(); + Value[] values = it.nextRow().getValues(); + for (int i = 0; i < propNames.length; i++) { + msg.append(" ").append(propNames[i]).append(": "); + if (values[i] == null) { + msg.append("null"); + } else { + msg.append(values[i].getString()); + } + } + log.println(msg); + } + if (count == 0) { + log.println(" NONE"); + } + assertEquals("Wrong column count.", columns, count); + } + + /** + * Returns the nodes in it as an array of Nodes. + * @param it the NodeIterator. + * @return the elements of the iterator as an array of Nodes. + */ + protected Node[] toArray(NodeIterator it) { + List nodes = new ArrayList(); + while (it.hasNext()) { + nodes.add(it.nextNode()); + } + return nodes.toArray(new Node[nodes.size()]); + } + + /** + * Executes the xpath query and checks the results against + * the specified nodes. + * @param xpath the xpath query. + * @param nodes the expected result nodes. + * @throws RepositoryException if an error occurs while executing the query + * or checking the result. + */ + protected void executeXPathQuery(String xpath, Node[] nodes) + throws RepositoryException { + QueryResult res = qm.createQuery(xpath, Query.XPATH).execute(); + checkResult(res, nodes); + } + + /** + * Executes the sql query and checks the results against + * the specified nodes. + * @param sql the sql query. + * @param nodes the expected result nodes. + * @throws RepositoryException if an error occurs while executing the query + * or checking the result. + */ + protected void executeSQLQuery(String sql, Node[] nodes) + throws RepositoryException { + QueryResult res = qm.createQuery(sql, Query.SQL).execute(); + checkResult(res, nodes); + } + + /** + * Checks if the result set contains exactly the nodes. + * @param result the query result. + * @param nodes the expected nodes in the result set. + * @throws RepositoryException if an error occurs while reading from the result. + */ + protected void checkResult(QueryResult result, Node[] nodes) + throws RepositoryException { + checkResult(result.getNodes(), nodes); + } + + /** + * Checks if the result contains exactly the nodes. + * @param result the query result. + * @param nodes the expected nodes in the result set. + * @throws RepositoryException if an error occurs while reading from the result. + */ + protected void checkResult(RowIterator result, Node[] nodes) + throws RepositoryException { + checkResult(new NodeIteratorAdapter(result) { + public Object next() throws NoSuchElementException { + Row next = (Row) super.next(); + try { + return superuser.getItem(next.getValue("jcr:path").getString()); + } catch (RepositoryException e) { + throw new NoSuchElementException(); + } + } + }, nodes); + } + + /** + * Checks if the result contains exactly the nodes. + * @param result the query result. + * @param nodes the expected nodes in the result set. + * @throws RepositoryException if an error occurs while reading from the result. + */ + protected void checkResult(NodeIterator result, Node[] nodes) + throws RepositoryException { + // collect paths + Set expectedPaths = new HashSet(); + for (Node n : nodes) { + expectedPaths.add(n.getPath()); + } + Set resultPaths = new HashSet(); + while (result.hasNext()) { + resultPaths.add(result.nextNode().getPath()); + } + // check if all expected are in result + for (Iterator it = expectedPaths.iterator(); it.hasNext();) { + String path = it.next(); + assertTrue(path + " is not part of the result set "+ resultPaths, resultPaths.contains(path)); + } + // check result does not contain more than expected + for (Iterator it = resultPaths.iterator(); it.hasNext();) { + String path = it.next(); + assertTrue(path + " is not expected to be part of the result set " + expectedPaths, expectedPaths.contains(path)); + } + } + + /** + * Checks if the result set contains exactly the nodes in the + * given sequence. + * + * @param result the query result. + * @param nodes the expected nodes in the result set. + * @throws RepositoryException if an error occurs while reading from the result. + */ + protected void checkResultSequence(RowIterator result, Node[] nodes) + throws RepositoryException { + for (int i = 0; i < nodes.length; i++) { + assertTrue("No more results, expected " + nodes[i].getPath(), result.hasNext()); + String path = result.nextRow().getValue("jcr:path").getString(); + assertEquals("Wrong sequence", nodes[i].getPath(), path); + } + assertFalse("No more result expected", result.hasNext()); + } + + /** + * Executes the query specified by statement and returns the + * query result. + * + * @param statement either a SQL or XPath statement. + * @return the query result. + * @throws RepositoryException if an error occurs. + */ + protected QueryResult executeQuery(String statement) + throws RepositoryException { + if (statement.trim().toLowerCase().startsWith("select")) { + return qm.createQuery(statement, Query.SQL).execute(); + } else { + return qm.createQuery(statement, Query.XPATH).execute(); + } + } + + protected QueryResult executeSQL2Query(String statement) + throws RepositoryException { + return qm.createQuery(statement, JCR_SQL2).execute(); + } + + protected void executeSQL2Query(String statement, Node[] nodes) + throws RepositoryException { + QueryResult res = qm.createQuery(statement, JCR_SQL2).execute(); + checkResult(res, nodes); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ChildAxisQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ChildAxisQueryTest.java new file mode 100644 index 00000000000..38eeaf28c4d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ChildAxisQueryTest.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** + * ChildAxisQueryTest tests queries with a child axis in their + * predicates. + */ +public class ChildAxisQueryTest extends AbstractQueryTest { + + /** + * Predicate with child node axis in a relation + */ + public void testRelationQuery() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + n1.setProperty(propertyName1, 1); + Node n2 = testRootNode.addNode(nodeName1); + n2.setProperty(propertyName1, 2); + Node n3 = testRootNode.addNode(nodeName1); + n3.setProperty(propertyName1, 3); + + testRootNode.save(); + + String base = testPath + "[" + nodeName1 + "/@" + propertyName1; + executeXPathQuery(base + " = 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " = 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " = 3]", new Node[]{testRootNode}); + executeXPathQuery(base + " = 4]", new Node[]{}); + executeXPathQuery(base + " > 0]", new Node[]{testRootNode}); + executeXPathQuery(base + " > 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " > 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " > 3]", new Node[]{}); + executeXPathQuery(base + " >= 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " >= 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " >= 3]", new Node[]{testRootNode}); + executeXPathQuery(base + " >= 4]", new Node[]{}); + executeXPathQuery(base + " < 1]", new Node[]{}); + executeXPathQuery(base + " < 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " < 3]", new Node[]{testRootNode}); + executeXPathQuery(base + " < 4]", new Node[]{testRootNode}); + executeXPathQuery(base + " <= 0]", new Node[]{}); + executeXPathQuery(base + " <= 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " <= 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " <= 3]", new Node[]{testRootNode}); + executeXPathQuery(base + " != 0]", new Node[]{testRootNode}); + executeXPathQuery(base + " != 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " != 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " != 3]", new Node[]{testRootNode}); + } + + public void testRelationQueryDeep() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1).addNode(nodeName2); + Node n1 = n.addNode(nodeName3); + n1.setProperty(propertyName1, 1); + Node n2 = n.addNode(nodeName3); + n2.setProperty(propertyName1, 2); + Node n3 = n.addNode(nodeName3); + n3.setProperty(propertyName1, 3); + + testRootNode.save(); + + String base = testPath + "[" + nodeName1 + "/" + nodeName2 + "/" + + nodeName3 + "/@" + propertyName1; + executeXPathQuery(base + " = 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " = 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " = 3]", new Node[]{testRootNode}); + executeXPathQuery(base + " = 4]", new Node[]{}); + executeXPathQuery(base + " > 0]", new Node[]{testRootNode}); + executeXPathQuery(base + " > 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " > 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " > 3]", new Node[]{}); + executeXPathQuery(base + " >= 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " >= 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " >= 3]", new Node[]{testRootNode}); + executeXPathQuery(base + " >= 4]", new Node[]{}); + executeXPathQuery(base + " < 1]", new Node[]{}); + executeXPathQuery(base + " < 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " < 3]", new Node[]{testRootNode}); + executeXPathQuery(base + " < 4]", new Node[]{testRootNode}); + executeXPathQuery(base + " <= 0]", new Node[]{}); + executeXPathQuery(base + " <= 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " <= 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " <= 3]", new Node[]{testRootNode}); + executeXPathQuery(base + " != 0]", new Node[]{testRootNode}); + executeXPathQuery(base + " != 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " != 2]", new Node[]{testRootNode}); + executeXPathQuery(base + " != 3]", new Node[]{testRootNode}); + } + + public void testMultiRelation() throws RepositoryException { + Node level1 = testRootNode.addNode(nodeName1); + level1.setProperty(propertyName1, "foo"); + Node level2 = level1.addNode(nodeName2); + level2.setProperty(propertyName1, "bar"); + Node n1 = level2.addNode(nodeName3); + n1.setProperty(propertyName2, 1); + Node n2 = level2.addNode(nodeName3); + n2.setProperty(propertyName2, 2); + Node n3 = level2.addNode(nodeName3); + n3.setProperty(propertyName2, 3); + + testRootNode.save(); + + String base = testPath + "[" + nodeName1 + "/" + nodeName2 + "/" + + nodeName3 + "/@" + propertyName2; + executeXPathQuery(base + " = 1]", new Node[]{testRootNode}); + executeXPathQuery(base + " = 1 and " + nodeName1 + "/@" + + propertyName1 + " = 'foo' and " + nodeName1 + "/" + nodeName2 + + "/@" + propertyName1 + " = 'bar']", new Node[]{testRootNode}); + executeXPathQuery(base + " = 1 and " + nodeName1 + "/@" + + propertyName1 + " = 'foo' and " + nodeName1 + "/" + nodeName2 + + "/@" + propertyName1 + " = 'bar']", new Node[]{testRootNode}); + executeXPathQuery(base + " = 1 and " + nodeName1 + "/@" + + propertyName1 + " = 'foo' and " + nodeName2 + + "/@" + propertyName1 + " = 'bar']", new Node[]{}); + } + + public void testLike() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + n1.setProperty(propertyName1, "foo"); + Node n2 = testRootNode.addNode(nodeName1); + n2.setProperty(propertyName1, "foobar"); + Node n3 = testRootNode.addNode(nodeName1); + n3.setProperty(propertyName1, "foo bar"); + + testRootNode.save(); + + String base = testPath + "[jcr:like(" + nodeName1 + "/@" + propertyName1; + executeXPathQuery(base + ", 'fo_')]", new Node[]{testRootNode}); + executeXPathQuery(base + ", 'foo_ar')]", new Node[]{testRootNode}); + executeXPathQuery(base + ", 'foo %')]", new Node[]{testRootNode}); + executeXPathQuery(base + ", 'f_oba')]", new Node[]{}); + } + + public void testContains() throws RepositoryException { + Node level1 = testRootNode.addNode(nodeName1); + level1.setProperty(propertyName1, "The quick brown fox jumps over the lazy dog."); + Node level2 = level1.addNode(nodeName2); + level2.setProperty(propertyName1, "Franz jagt im total verwahrlosten Taxi quer durch Bayern."); + Node n1 = level2.addNode(nodeName3); + n1.setProperty(propertyName2, 1); + Node n2 = level2.addNode(nodeName3); + n2.setProperty(propertyName2, 2); + Node n3 = level2.addNode(nodeName3); + n3.setProperty(propertyName2, 3); + + testRootNode.save(); + + String base = testPath + "[jcr:contains("; + executeXPathQuery(base + nodeName1 + "/@" + propertyName1 + ", 'lazy')" + + " and " + nodeName1 + "/" + nodeName2 + "/" + nodeName3 + "/@" + propertyName2 + " = 2]", + new Node[]{testRootNode}); + executeXPathQuery(base + nodeName1 + "/" + nodeName2 + "/@" + propertyName1 + ", 'franz')" + + " and " + nodeName1 + "/" + nodeName2 + "/" + nodeName3 + "/@" + propertyName2 + " = 3]", + new Node[]{testRootNode}); + executeXPathQuery(base + nodeName1 + ", 'lazy')" + + " and " + nodeName1 + "/" + nodeName2 + "/" + nodeName3 + "/@" + propertyName2 + " = 1]", + new Node[]{testRootNode}); + executeXPathQuery(base + nodeName1 + "/" + nodeName2 + ", 'franz')" + + " and " + nodeName1 + "/" + nodeName2 + "/" + nodeName3 + "/@" + propertyName2 + " = 1]", + new Node[]{testRootNode}); + } + + public void testStarNameTest() throws RepositoryException { + Node level1 = testRootNode.addNode(nodeName1); + level1.setProperty(propertyName1, "The quick brown fox jumps over the lazy dog."); + Node level2 = level1.addNode(nodeName2); + level2.setProperty(propertyName1, "Franz jagt im total verwahrlosten Taxi quer durch Bayern."); + Node n1 = level2.addNode(nodeName3); + n1.setProperty(propertyName2, 1); + Node n2 = level2.addNode(nodeName3); + n2.setProperty(propertyName2, 2); + Node n3 = level2.addNode(nodeName4); + n3.setProperty(propertyName2, 3); + + testRootNode.save(); + + String base = testPath + "[jcr:contains("; + executeXPathQuery(base + nodeName1 + "/@" + propertyName1 + ", 'lazy')" + + " and " + nodeName1 + "/" + nodeName2 + "/" + nodeName3 + "/@" + propertyName2 + " = 3]", + new Node[]{}); + executeXPathQuery(base + nodeName1 + "/@" + propertyName1 + ", 'lazy')" + + " and " + nodeName1 + "/" + nodeName2 + "/*/@" + propertyName2 + " = 3]", + new Node[]{testRootNode}); + + executeXPathQuery(base + "*/@" + propertyName1 + ", 'lazy')]", + new Node[]{testRootNode}); + executeXPathQuery(base + nodeName1 + "/*, 'franz')]", + new Node[]{testRootNode}); + executeXPathQuery(base + "*/*, 'franz')]", + new Node[]{testRootNode}); + executeXPathQuery(base + "*/*, 'lazy')]", + new Node[]{}); + } + + public void testSimpleQuery() throws Exception { + Node foo = testRootNode.addNode("foo"); + testRootNode.addNode("bar"); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE jcr:path LIKE '"+testRoot+"/foo'"; + executeSQLQuery(sql, new Node[] {foo}); + } + + /** + * JCR-3337 + */ + public void testNotIsDescendantNodeQuery() throws Exception { + Node foo = testRootNode.addNode("foo"); + testRootNode.getSession().save(); + String sql = "SELECT a.* FROM [nt:base] as a WHERE isdescendantnode(a,'" + + testRootNode.getPath() + + "') and not isdescendantnode(a,'" + + foo.getPath() + "')"; + executeSQL2Query(sql, new Node[] {foo}); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/DerefTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/DerefTest.java new file mode 100644 index 00000000000..93e60d2c3f3 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/DerefTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.version.Version; + +/** + * Tests the jcr:deref() function. + */ +public class DerefTest extends AbstractQueryTest { + + /** + * Test nodes. + */ + private Node andrew, bill, carl, daren, eric, frank; + + /** + * Test nodes. + */ + private Node sun, microsoft, ibm; + + /** + * Sets up the following structure: + *

    +     *   + people
    +     *      + andrew (worksfor -> company/sun)
    +     *         + bill (worksfor -> company/ibm)
    +     *      + carl (worksfor -> company/microsoft)
    +     *         + daren (worksfor -> company/ibm)
    +     *            + eric (worksfor -> company/sun)
    +     *      + frank (worksfor -> company/microsoft)
    +     *   + company
    +     *      + sun (eotm -> andrew)
    +     *      + microsoft (eotm -> carl)
    +     *      + ibm (eotm -> daren)
    +     * 
    + */ + protected void setUp() throws Exception { + super.setUp(); + + Node people = testRootNode.addNode("people"); + Node company = testRootNode.addNode("company"); + + sun = company.addNode("sun"); + sun.addMixin(mixReferenceable); + sun.setProperty("ceo", "McNealy"); + microsoft = company.addNode("microsoft"); + microsoft.addMixin(mixReferenceable); + microsoft.setProperty("ceo", "Ballmer"); + ibm = company.addNode("ibm"); + ibm.addMixin(mixReferenceable); + ibm.setProperty("ceo", "Palmisano"); + + andrew = people.addNode("andrew"); + andrew.addMixin(mixReferenceable); + andrew.setProperty("worksfor", sun); + bill = andrew.addNode("bill"); + bill.setProperty("worksfor", ibm); + carl = people.addNode("carl"); + carl.addMixin(mixReferenceable); + carl.setProperty("worksfor", microsoft); + daren = carl.addNode("daren"); + daren.addMixin(mixReferenceable); + daren.setProperty("worksfor", ibm); + eric = daren.addNode("eric"); + eric.setProperty("worksfor", sun); + frank = people.addNode("frank"); + frank.setProperty("worksfor", microsoft); + + // Employees of the month + sun.setProperty("eotm", andrew); + microsoft.setProperty("eotm", carl); + ibm.setProperty("eotm", daren); + + testRootNode.save(); + } + + protected void tearDown() throws Exception { + andrew = null; + bill = null; + carl = null; + daren = null; + eric = null; + frank = null; + sun = null; + microsoft = null; + ibm = null; + super.tearDown(); + } + + /** + * Tests various XPath queries with jcr:deref() function. + */ + public void testDeref() throws RepositoryException { + executeXPathQuery(testPath + "/people/jcr:deref(@worksfor, '*')", + new Node[]{}); + + executeXPathQuery(testPath + "/people/*/jcr:deref(@worksfor, '*')", + new Node[]{sun, microsoft}); + + executeXPathQuery(testPath + "/people/*/*/jcr:deref(@worksfor, '*')", + new Node[]{ibm}); + + executeXPathQuery(testPath + "/people//jcr:deref(@worksfor, '*')", + new Node[]{sun, ibm, microsoft}); + + executeXPathQuery(testPath + "/people/carl//jcr:deref(@worksfor, '*')", + new Node[]{sun, ibm}); + + executeXPathQuery(testPath + "/people//jcr:deref(@worksfor, 'sun')", + new Node[]{sun}); + + executeXPathQuery(testPath + "/people//jcr:deref(@worksfor, '*')[@ceo = 'McNealy']", + new Node[]{sun}); + + executeXPathQuery(testPath + "/people/*/jcr:deref(@worksfor, '*')[jcr:contains(.,'ballmer')]", + new Node[]{microsoft}); + } + + public void testDerefInPredicate() throws RepositoryException { + executeXPathQuery(testPath + "/people//*[jcr:deref(@worksfor, '*')/@ceo='McNealy']", + new Node[]{andrew, eric}); + + executeXPathQuery("//*[people/jcr:deref(@worksfor, '*')/@ceo='McNealy']", + new Node[]{testRootNode}); + +// executeXPathQuery("//*[jcr:contains(people/jcr:deref(@worksfor, '*'),'ballmer')]", +// new Node[]{testRootNode}); + } + + public void testRewrite() throws RepositoryException { + executeXPathQuery("//*[people/jcr:deref(@worksfor, '*')/@foo=1]", + new Node[]{}); + } + + /** + * Checks if jcr:deref works when dereferencing into the version storage. + */ + public void testDerefToVersionNode() throws RepositoryException { + Node referenced = testRootNode.addNode(nodeName1); + referenced.addMixin(mixVersionable); + testRootNode.save(); + + Version version = referenced.checkin(); + Node referencedVersionNode = version.getNode(jcrFrozenNode); + Node referencer = testRootNode.addNode(nodeName2); + referencer.setProperty(propertyName1, referencedVersionNode); + testRootNode.save(); + + String query = "/" + testRoot + "/*[@" + propertyName1 + + "]/jcr:deref(@" + propertyName1 + ",'*')"; + QueryManager qm = superuser.getWorkspace().getQueryManager(); + Query q = qm.createQuery(query, Query.XPATH); + QueryResult qr = q.execute(); + NodeIterator ni = qr.getNodes(); + assertEquals("Must find one result in query", 1, ni.getSize()); + while (ni.hasNext()) { + Node node = (Node) ni.next(); + assertTrue(node.getProperty("jcr:frozenUuid").getString().equals(referenced.getUUID())); + } + } + + /** + * Tests various XPath queries with multiple jcr:deref() function. + */ + public void testMultipleDeref() throws RepositoryException { + executeXPathQuery(testPath + "/people/frank/jcr:deref(@worksfor, '*')/jcr:deref(@eotm, '*')", + new Node[]{carl}); + executeXPathQuery(testPath + "/people/frank/jcr:deref(@worksfor, '*')/jcr:deref(@eotm, '*')[@jcr:uuid]", + new Node[]{carl}); + executeXPathQuery(testPath + "/people/frank/jcr:deref(@worksfor, '*')[@jcr:uuid]/jcr:deref(@eotm, '*')[@jcr:uuid]", + new Node[]{carl}); + executeXPathQuery(testPath + "/people/frank/jcr:deref(@worksfor, '*')[@jcr:uuid]/jcr:deref(@eotm, '*')", + new Node[]{carl}); + + executeXPathQuery(testPath + "/people//jcr:deref(@worksfor, '*')/jcr:deref(@eotm, '*')", + new Node[]{andrew, carl, daren}); + executeXPathQuery(testPath + "/people//jcr:deref(@worksfor, '*')/jcr:deref(@eotm, '*')[@jcr:uuid]", + new Node[]{andrew, carl, daren}); + executeXPathQuery(testPath + "/people//jcr:deref(@worksfor, '*')[@jcr:uuid]/jcr:deref(@eotm, '*')[@jcr:uuid]", + new Node[]{andrew, carl, daren}); + executeXPathQuery(testPath + "/people//jcr:deref(@worksfor, '*')[@jcr:uuid]/jcr:deref(@eotm, '*')", + new Node[]{andrew, carl, daren}); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/DescendantSelfAxisTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/DescendantSelfAxisTest.java new file mode 100644 index 00000000000..405d42b38f0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/DescendantSelfAxisTest.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.core.query; + +import static org.apache.jackrabbit.JcrConstants.*; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +public class DescendantSelfAxisTest extends AbstractQueryTest { + + /** + * JCR-3407 CaseTermQuery #rewrite behavior changes + */ + public void testCaseTermQueryNPE() throws RepositoryException { + String xpathNPE = "//element(*,nt:unstructured)[fn:lower-case(@jcr:language)='en']//element(*,nt:unstructured)[@jcr:message]/(@jcr:key|@jcr:message)"; + executeXPathQuery(xpathNPE, new Node[] {}); + } + + /** + * JCR-3401 Wrong results when querying with a DescendantSelfAxisQuery + */ + public void testNodeName() throws RepositoryException { + String name = "testNodeName" + System.currentTimeMillis(); + + Node foo = testRootNode.addNode("foo", NT_UNSTRUCTURED); + foo.addNode("branch1", NT_FOLDER).addNode(name, NT_FOLDER); + foo.addNode("branch2", NT_FOLDER).addNode(name, NT_FOLDER); + Node bar = testRootNode.addNode(name, NT_UNSTRUCTURED); + + testRootNode.getSession().save(); + + executeXPathQuery("//element(*, nt:unstructured)[fn:name() = '" + name + + "']", new Node[] { bar }); + executeXPathQuery("//element(" + name + ", nt:unstructured)", + new Node[] { bar }); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ExcerptTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ExcerptTest.java new file mode 100644 index 00000000000..3574fd54855 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ExcerptTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +/** + * ExcerptTest checks if HTML excerpts are created correctly. The + * test cases assume the following implementation details: + *
      + *
    • An excerpt is enclosed with a <div> element
    • + *
    • A fragment is enclosed with a <span> element
    • + *
    • Terms are highlighted with a <strong> element
    • + *
    • The maximum number of fragment created is three
    • + *
    • The maximum excerpt length is 150 characters
    • + *
    • A fragment contains at most 75 characters (excluding '... ') before the first term is highlighted
    • + *
    • At least the following sentence separators are recognized: '.', '!' and '?'
    • + *
    • If there is additial text after the fragment end ' ...' is appended to the fragment
    • + *
    • If the fragment starts within a sentence, then the fragment is prefixed with '... '
    • + *
    + */ +public class ExcerptTest extends AbstractQueryTest { + + private static final String EXCERPT_START = "
    "; + + private static final String EXCERPT_END = "
    "; + + public void testHightlightFirstWord() throws RepositoryException { + checkExcerpt("jackrabbit bla bla bla", + "jackrabbit bla bla bla", + "jackrabbit"); + } + + public void testHightlightLastWord() throws RepositoryException { + checkExcerpt("bla bla bla jackrabbit", + "bla bla bla jackrabbit", + "jackrabbit"); + } + + public void testHightlightWordBetween() throws RepositoryException { + checkExcerpt("bla bla jackrabbit bla bla", + "bla bla jackrabbit bla bla", + "jackrabbit"); + } + + public void testMoreTextDotsAtEnd() throws RepositoryException { + checkExcerpt("bla bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla", + "bla bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla ...", + "jackrabbit"); + } + + public void testMoreTextDotsAtStart() throws RepositoryException { + checkExcerpt("bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla jackrabbit bla bla bla bla", + "... bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla jackrabbit bla bla bla bla", + "jackrabbit"); + } + + public void testMoreTextDotsAtStartAndEnd() throws RepositoryException { + checkExcerpt("bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla", + "... bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla ...", + "jackrabbit"); + } + + public void testPunctuationStartsFragment() throws RepositoryException { + checkExcerpt("bla bla bla bla bla bla bla bla. bla bla bla bla bla bla bla bla bla bla bla bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla", + "bla bla bla bla bla bla bla bla bla bla bla bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla", + "jackrabbit"); + } + + public void testPunctuationStartsFragmentEndsWithDots() throws RepositoryException { + checkExcerpt("bla bla bla bla bla bla bla bla. bla bla bla bla bla bla bla bla bla bla bla bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla", + "bla bla bla bla bla bla bla bla bla bla bla bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla ...", + "jackrabbit"); + } + + public void testHighlightMultipleTerms() throws RepositoryException { + checkExcerpt("bla bla bla apache jackrabbit bla bla bla", + "bla bla bla apache jackrabbit bla bla bla", + "apache jackrabbit"); + } + + public void testPreferPhrase() throws RepositoryException { + checkExcerpt("bla apache bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla apache jackrabbit bla bla bla", + "... bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla apache jackrabbit bla bla bla
    bla apache bla jackrabbit bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla ...", + "apache jackrabbit"); + } + + /** + * Verifies character encoding on a node property that does not contain any + * excerpt info + */ + public void testEncodeIllegalCharsNoHighlights() throws RepositoryException { + String text = "bla bla bla"; + String excerpt = createExcerpt("bla <strong>bla</strong> bla"); + Node n = testRootNode.addNode(nodeName1); + n.setProperty("text", text); + n.setProperty("other", "foo"); + superuser.save(); + + String stmt = getStatement("foo"); + QueryResult result = executeQuery(stmt); + RowIterator rows = result.getRows(); + assertEquals(1, rows.getSize()); + String ex = rows.nextRow().getValue("rep:excerpt(text)").getString(); + assertEquals("Expected " + excerpt + ", but got ", excerpt, ex); + } + + /** + * Verifies character encoding on a node property that contains excerpt info + */ + public void testEncodeIllegalCharsHighlights() throws RepositoryException { + checkExcerpt("bla bla foo", + "bla <strong>bla</strong> foo", + "foo"); + } + + /** + * test for https://issues.apache.org/jira/browse/JCR-3077 + * + * when given a quoted phrase, the excerpt should evaluate it whole as a + * token (not break is down) + * + */ + public void testQuotedPhrase() throws RepositoryException { + checkExcerpt("one two three four", + "one two three four", "\"two three\""); + } + + /** + * Verifies excerpt generation on a node property that does not contain any + * excerpt info for a quoted phrase + */ + public void testQuotedPhraseNoMatch() throws RepositoryException { + String text = "one two three four"; + String excerpt = createExcerpt("one two three four"); + String terms = "\"five six\""; + + Node n = testRootNode.addNode(nodeName1); + n.setProperty("text", text); + n.setProperty("other", terms); + superuser.save(); + + String stmt = getStatement(terms); + QueryResult result = executeQuery(stmt); + RowIterator rows = result.getRows(); + assertEquals(1, rows.getSize()); + String ex = rows.nextRow().getValue("rep:excerpt(text)").getString(); + assertEquals("Expected " + excerpt + ", but got ", excerpt, ex); + } + + /** + * + * Verifies excerpt generation on a node property that contains the exact + * quoted phrase but with scrambled words. + * + * More clearly it actually checks that the order of tokens is respected for + * a quoted phrase. + */ + public void testQuotedPhraseNoMatchScrambled() throws RepositoryException { + String text = "one two three four"; + String excerpt = createExcerpt("one two three four"); + String terms = "\"three two\""; + + Node n = testRootNode.addNode(nodeName1); + n.setProperty("text", text); + n.setProperty("other", terms); + superuser.save(); + + String stmt = getStatement(terms); + QueryResult result = executeQuery(stmt); + RowIterator rows = result.getRows(); + assertEquals(1, rows.getSize()); + String ex = rows.nextRow().getValue("rep:excerpt(text)").getString(); + assertEquals("Expected " + excerpt + ", but got ", excerpt, ex); + } + + /** + * Verifies excerpt generation on a node property that does not contain the + * exact quoted phrase, but contains fragments of it. + * + */ + public void testQuotedPhraseNoMatchGap() throws RepositoryException { + String text = "one two three four"; + String excerpt = createExcerpt("one two three four"); + String terms = "\"two four\""; + + Node n = testRootNode.addNode(nodeName1); + n.setProperty("text", text); + n.setProperty("other", terms); + superuser.save(); + + String stmt = getStatement(terms); + QueryResult result = executeQuery(stmt); + RowIterator rows = result.getRows(); + assertEquals(1, rows.getSize()); + String ex = rows.nextRow().getValue("rep:excerpt(text)").getString(); + assertEquals("Expected " + excerpt + ", but got ", excerpt, ex); + } + + /** + * test for https://issues.apache.org/jira/browse/JCR-3077 + * + * JA search acts as a PhraseQuery, thanks to LUCENE-2458. so it should be + * covered by the QuotedTest search. + * + */ + public void testHighlightJa() throws RepositoryException { + + // http://translate.google.com/#auto|en|%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%88 + String jContent = "\u30b3\u30fe\u30c6\u30f3\u30c8"; + // http://translate.google.com/#auto|en|%E3%83%86%E3%82%B9%E3%83%88 + String jTest = "\u30c6\u30b9\u30c8"; + + String content = "some text with japanese: " + jContent + " (content)" + + " and " + jTest + " (test)."; + + // expected excerpt; note this may change if excerpt providers change + String expectedExcerpt = "some text with japanese: " + jContent + + " (content) and " + jTest + " (test)."; + checkExcerpt(content, expectedExcerpt, jTest); + } + + /** + * test for https://issues.apache.org/jira/browse/JCR-3428 + * + * when given an incomplete fulltext search token, the excerpt should + * highlight the entire matching token + * + */ + public void testEagerMatch() throws RepositoryException { + checkExcerpt("lorem ipsum dolor sit amet", + "lorem ipsum dolor sit amet", "ipsu*"); + } + + /** + * @see #testEagerMatch() + */ + public void testEagerMatch2() throws RepositoryException { + checkExcerpt("lorem ipsum dolor sit amet", + "lorem ipsum dolor sit amet", + "lorem ipsu*"); + } + + /** + * @see #testEagerMatch() + */ + public void testEagerMatch3() throws RepositoryException { + checkExcerpt("lorem ipsum dolor sit amet", + "lorem ipsum dolor sit amet", "ipsu* dolor"); + } + + private void checkExcerpt(String text, String fragmentText, String terms) + throws RepositoryException { + String excerpt = createExcerpt(fragmentText); + createTestData(text); + String stmt = getStatement(terms); + QueryResult result = executeQuery(stmt); + RowIterator rows = result.getRows(); + assertEquals(1, rows.getSize()); + assertEquals(excerpt, getExcerpt(rows.nextRow())); + } + + private String getStatement(String terms) { + return testPath + "/*[jcr:contains(., '"+ terms + "')]/rep:excerpt(.)"; + } + + private void createTestData(String text) throws RepositoryException { + Node n = testRootNode.addNode(nodeName1); + n.setProperty("text", text); + superuser.save(); + } + + private String getExcerpt(Row row) throws RepositoryException { + Value v = row.getValue("rep:excerpt(.)"); + if (v != null) { + return v.getString(); + } else { + return null; + } + } + + private String createExcerpt(String fragments) { + return EXCERPT_START + fragments + EXCERPT_END; + } + + /** + *

    + * Test when there are multiple tokens that match the fulltext search (which + * will generate multiple lucene terms for the highlighter to use) in the + * repository but not all of them are present in the current property. + *

    + * + */ + public void testMatchMultipleNonMatchingTokens() throws RepositoryException { + String text = "lorem ipsum"; + String fragmentText = "lorem ipsum"; + String terms = "ipsu*"; + + String excerpt = createExcerpt(fragmentText); + + // here we'll add more matching garbage data so we have more tokens + // passed to the highlighter + Node parent = testRootNode.addNode(nodeName1); + Node n = parent.addNode("test"); + n.setProperty("text", text); + testRootNode.addNode(nodeName2).setProperty("foo", "ipsuFoo"); + testRootNode.addNode(nodeName3).setProperty("bar", "ipsuBar"); + superuser.save(); + // -- + String stmt = testPath + "/" + nodeName1 + "//*[jcr:contains(., '" + + terms + "')]/rep:excerpt(.)"; + QueryResult result = executeQuery(stmt); + RowIterator rows = result.getRows(); + assertEquals(1, rows.getSize()); + assertEquals(excerpt, getExcerpt(rows.nextRow())); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.java new file mode 100644 index 00000000000..281c00309c0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FnNameQueryTest.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.core.query; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; + +/** + * FnNameQueryTest tests queries with fn:name() functions. + */ +public class FnNameQueryTest extends AbstractQueryTest { + + public void testSimple() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + n1.setProperty(propertyName1, 1); + Node n2 = testRootNode.addNode(nodeName2); + n2.setProperty(propertyName1, 2); + Node n3 = testRootNode.addNode(nodeName3); + n3.setProperty(propertyName1, 3); + + superuser.save(); + + String base = testPath + "/*[@" + propertyName1; + executeXPathQuery(base + " = 1 and fn:name() = '" + nodeName1 + "']", + new Node[]{n1}); + executeXPathQuery(base + " = 1 and fn:name() = '" + nodeName2 + "']", + new Node[]{}); + executeXPathQuery(base + " > 0 and fn:name() = '" + nodeName2 + "']", + new Node[]{n2}); + executeXPathQuery(base + " > 0 and (fn:name() = '" + nodeName1 + + "' or fn:name() = '" + nodeName2 + "')]", new Node[]{n1, n2}); + executeXPathQuery(base + " > 0 and not(fn:name() = '" + nodeName1 + "')]", + new Node[]{n2, n3}); + } + + public void testWithSpace() throws RepositoryException { + Node n1 = testRootNode.addNode("My Documents"); + n1.setProperty(propertyName1, 1); + + superuser.save(); + + String base = testPath + "/*[@" + propertyName1; + executeXPathQuery(base + " = 1 and fn:name() = 'My Documents']", + new Node[]{}); + executeXPathQuery(base + " = 1 and fn:name() = 'My_x0020_Documents']", + new Node[]{n1}); + } + + public void testLikeWithWildcard() throws RepositoryException { + Node n1 = testRootNode.addNode("Foo"); + n1.setProperty(propertyName1, 1); + + superuser.save(); + + String prefix = testPath + "/*[@" + propertyName1 + " = 1 and jcr:like(fn:name(), '"; + String suffix = "')]"; + executeXPathQuery(prefix + "F%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "Fo%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "Foo%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "Fooo%" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%o" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%oo" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%Foo" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%Foo%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%oo%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%" + suffix, new Node[]{n1}); + + executeXPathQuery(prefix + "F__" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "Fo_" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "F_o" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "Foo_" + suffix, new Node[]{}); + } + + public void testLikeWithWildcardAndLowerCase() + throws RepositoryException { + Node n1 = testRootNode.addNode("Foo"); + n1.setProperty(propertyName1, 1); + + superuser.save(); + + String prefix = testPath + "/*[@" + propertyName1 + " = 1 and jcr:like(fn:lower-case(fn:name()), '"; + String suffix = "')]"; + + executeXPathQuery(prefix + "f%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "fo%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "foo%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "fooo%" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%o" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%oo" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%foo" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%foo%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "%oo%" + suffix, new Node[]{n1}); + + executeXPathQuery(prefix + "f__" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "fo_" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "f_o" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "_oo" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "foo_" + suffix, new Node[]{}); + + // all non-matching + executeXPathQuery(prefix + "F%" + suffix, new Node[]{}); + executeXPathQuery(prefix + "fO%" + suffix, new Node[]{}); + executeXPathQuery(prefix + "foO%" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%O" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%Oo" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%Foo" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%FOO%" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%oO%" + suffix, new Node[]{}); + + executeXPathQuery(prefix + "F__" + suffix, new Node[]{}); + executeXPathQuery(prefix + "fO_" + suffix, new Node[]{}); + executeXPathQuery(prefix + "F_o" + suffix, new Node[]{}); + executeXPathQuery(prefix + "_oO" + suffix, new Node[]{}); + } + + public void testLikeWithPrefix() throws RepositoryException { + Node n1 = testRootNode.addNode("jcr:content"); + n1.setProperty(propertyName1, 1); + + superuser.save(); + + String prefix = testPath + "/*[@" + propertyName1 + " = 1 and jcr:like(fn:name(), '"; + String suffix = "')]"; + + executeXPathQuery(prefix + "jcr:%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "jcr:c%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "jcr:%ten%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "jcr:c_nt%" + suffix, new Node[]{n1}); + executeXPathQuery(prefix + "jcr:%nt" + suffix, new Node[]{n1}); + + // non-matching + executeXPathQuery(prefix + "invalid:content" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%:content" + suffix, new Node[]{}); + executeXPathQuery(prefix + "%:%" + suffix, new Node[]{}); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FulltextQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FulltextQueryTest.java new file mode 100644 index 00000000000..b4c3c7a1383 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FulltextQueryTest.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; + +import org.apache.jackrabbit.commons.iterator.RowIterable; + +/** + * Performs tests with the CONTAINS function. + */ +public class FulltextQueryTest extends AbstractQueryTest { + + public void testFulltextSimpleSQL1() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE jcr:path LIKE '" + testRoot + "/%" + + "' AND CONTAINS(., 'fox')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + public void testFulltextSimpleSQL2() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE \"jcr:path\" = '" + testRoot + "/foo" + + "' AND CONTAINS(., 'fox')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + public void testFulltextMultiWordSQL() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"test text"}); + n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"other text"}); + n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%" + + "' AND CONTAINS(., 'fox test')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + public void testFulltextPhraseSQL() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"test text"}); + n.setProperty("mytext", new String[]{"the quick brown jumps fox over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"other text"}); + n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%" + + "' AND CONTAINS(., 'text \"fox jumps\"')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + public void testFulltextExcludeSQL() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"test text"}); + n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"other text"}); + n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + superuser.getRootNode().save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%" + + "' AND CONTAINS(., 'text ''fox jumps'' -other')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + public void testFulltextOrSQL() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"test text"}); + n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"other text"}); + n.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%" + + "' AND CONTAINS(., '''fox jumps'' test OR other')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 2); + } + + public void testFulltextIntercapSQL() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"tEst text"}); + n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"Other text"}); + n.setProperty("mytext", new String[]{"the quick brown FOX jumPs over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE \"jcr:path\" LIKE '" + testRoot + "/%" + + "' AND CONTAINS(., '''fox juMps'' Test OR otheR')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 2); + } + + public void testContainsStarSQL() throws RepositoryException { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"tEst text"}); + n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"The quick brown Fox jumps over the lazy dog."}); + n.setProperty("mytext", new String[]{"text text"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE jcr:path LIKE '" + testRoot + "/%" + + "' AND CONTAINS(., 'fox jumps')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + checkResult(q.execute(), 2); + } + + public void testContainsStarXPath() throws RepositoryException { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"tEst text"}); + n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"The quick brown Fox jumps over the lazy dog."}); + n.setProperty("mytext", new String[]{"text text"}); + + testRootNode.save(); + + String sql = "/jcr:root" + testRoot + "/element(*, nt:unstructured)" + + "[jcr:contains(., 'quick fox')]"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.XPATH); + checkResult(q.execute(), 2); + } + + public void testContainsPropScopeSQL() throws RepositoryException { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"tEst text"}); + n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"The quick brown Fox jumps over the lazy dog."}); + n.setProperty("mytext", new String[]{"text text"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE jcr:path LIKE '" + testRoot + "/%" + + "' AND CONTAINS(title, 'fox jumps')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + checkResult(q.execute(), 1); + } + + public void testContainsPropScopeXPath() throws RepositoryException { + Node n = testRootNode.addNode("node1"); + n.setProperty("title", new String[]{"tEst text"}); + n.setProperty("mytext", new String[]{"The quick brown Fox jumps over the lazy dog."}); + + n = testRootNode.addNode("node2"); + n.setProperty("title", new String[]{"The quick brown Fox jumps over the lazy dog."}); + n.setProperty("mytext", new String[]{"text text"}); + + testRootNode.save(); + + String sql = "/jcr:root" + testRoot + "/element(*, nt:unstructured)" + + "[jcr:contains(@title, 'quick fox')]"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.XPATH); + checkResult(q.execute(), 1); + } + + public void testWildcard() throws RepositoryException { + String content = "The quick brown Fox jumps over the lazy dog."; + + // single * wildcard + executeContainsQuery("qu*", content, true); + executeContainsQuery("qu*ck", content, true); + executeContainsQuery("quick*", content, true); + executeContainsQuery("*quick", content, true); + executeContainsQuery("qu*Ck", content, true); + + // multiple * wildcard + executeContainsQuery("*o*", content, true); + executeContainsQuery("*ump*", content, true); + executeContainsQuery("qu**ck", content, true); + executeContainsQuery("q***u**c*k", content, true); + executeContainsQuery("*uMp*", content, true); + + // single ? wildcard + executeContainsQuery("quic?", content, true); + executeContainsQuery("?uick", content, true); + executeContainsQuery("qu?ck", content, true); + executeContainsQuery("qu?cK", content, true); + + // multiple ? wildcard + executeContainsQuery("q??ck", content, true); + executeContainsQuery("?uic?", content, true); + executeContainsQuery("??iCk", content, true); + + // no matches + executeContainsQuery("*ab*", content, false); + executeContainsQuery("q***j**c*k", content, false); + + } + + public void testMultiByte() throws RepositoryException { + String content = "some text with multi byte \u7530\u4e2d characters."; + + executeContainsQuery("\u7530\u4e2d*", content, true); + } + + public void testPredefinedEntityReference() throws RepositoryException { + String content = "Max&Moritz"; + + executeContainsQuery("max&moritz", content, true); + } + + public void testColonInContains() throws RepositoryException { + executeContainsQuery("foo:bar", "foo:bar", true); + } + + public void testMultipleOrExpressions() throws RepositoryException { + Node n = testRootNode.addNode("node1"); + n.setProperty("prop1", "foo"); + n.setProperty("prop2", "bar"); + n.setProperty("prop3", "baz"); + + n = testRootNode.addNode("node2"); + n.setProperty("prop1", "bar"); + n.setProperty("prop2", "foo"); + n.setProperty("prop3", "baz"); + + n = testRootNode.addNode("node3"); + n.setProperty("prop1", "bar"); + n.setProperty("prop2", "baz"); + n.setProperty("prop3", "foo"); + + superuser.save(); + + TreeSet r1 = new TreeSet(); + QueryResult result = qm.createQuery(testPath + "/*[jcr:contains(@prop1, 'foo') or jcr:contains(@prop2, 'foo') or jcr:contains(@prop3, 'foo')] order by @jcr:score descending", Query.XPATH).execute(); + for (Row r : new RowIterable(result.getRows())) { + r1.add(r.getPath() + ":" + (int) (r.getScore() * 1000)); + } + + TreeSet r2 = new TreeSet(); + result = qm.createQuery(testPath + "/*[jcr:contains(@prop3, 'foo') or jcr:contains(@prop1, 'foo') or jcr:contains(@prop2, 'foo')] order by @jcr:score descending", Query.XPATH).execute(); + for (Row r : new RowIterable(result.getRows())) { + r2.add(r.getPath() + ":" + (int) (r.getScore() * 1000)); + } + + TreeSet r3 = new TreeSet(); + result = qm.createQuery(testPath + "/*[jcr:contains(@prop2, 'foo') or jcr:contains(@prop3, 'foo') or jcr:contains(@prop1, 'foo')] order by @jcr:score descending", Query.XPATH).execute(); + for (Row r : new RowIterable(result.getRows())) { + r3.add(r.getPath() + ":" + (int) (r.getScore() * 1000)); + } + + assertEquals(r1, r2); + assertEquals(r1, r3); + } + + /** + * Executes a query and checks if the query matched the test node. + * + * @param statement the query statement. + * @param content the content for the test node. + * @param match if the query matches the node. + * @throws RepositoryException if an error occurs. + */ + private void executeContainsQuery(String statement, + String content, + boolean match) throws RepositoryException { + while (testRootNode.hasNode(nodeName1)) { + testRootNode.getNode(nodeName1).remove(); + } + testRootNode.addNode(nodeName1).setProperty("text", content); + testRootNode.save(); + + assertContainsQuery(statement, match); + } + + private void assertContainsQuery(String statement, boolean match) + throws InvalidQueryException, RepositoryException { + StringBuffer stmt = new StringBuffer(); + stmt.append("/jcr:root").append(testRoot).append("/*"); + stmt.append("[jcr:contains(., '").append(statement); + stmt.append("')]"); + + Query q = superuser.getWorkspace().getQueryManager().createQuery(stmt.toString(), Query.XPATH); + checkResult(q.execute(), match ? 1 : 0); + + stmt = new StringBuffer(); + stmt.append("SELECT * FROM nt:base "); + stmt.append("WHERE jcr:path LIKE '").append(testRoot).append("/%' "); + stmt.append("AND CONTAINS(., '").append(statement).append("')"); + + q = superuser.getWorkspace().getQueryManager().createQuery(stmt.toString(), Query.SQL); + checkResult(q.execute(), match ? 1 : 0); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FulltextSQL2QueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FulltextSQL2QueryTest.java new file mode 100644 index 00000000000..77a207234d4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/FulltextSQL2QueryTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +/** + * Performs tests with the CONTAINS function in JCR_SQL2 queries. + */ +public class FulltextSQL2QueryTest extends AbstractQueryTest { + + public void testFulltextSimpleSQL() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM [nt:unstructured]" + + " WHERE ISCHILDNODE([" + testRoot + "])" + + " AND CONTAINS(mytext, 'fox')"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + public void testFulltextBindVariableSQL() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM [nt:unstructured]" + + " WHERE ISCHILDNODE([" + testRoot + "])" + + " AND CONTAINS(mytext, $searchExpression)"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.JCR_SQL2); + assertEquals("Expected exactly 1 bind variable", 1, q.getBindVariableNames().length); + assertEquals("searchExpression", q.getBindVariableNames()[0]); + + q.bindValue("searchExpression", superuser.getValueFactory().createValue("fox")); + QueryResult result = q.execute(); + checkResult(result, 1); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/JoinTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/JoinTest.java new file mode 100644 index 00000000000..a2138673430 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/JoinTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +/** + * Test case for JOIN queries with JCR_SQL2 + */ +public class JoinTest extends AbstractQueryTest { + + private Node node; + + @Override + protected void setUp() throws Exception { + super.setUp(); + node = testRootNode.addNode("jointest", "nt:unstructured"); + + Node n1a = node.addNode("n1a", "nt:unstructured"); + n1a.addMixin(NodeType.MIX_REFERENCEABLE); + n1a.setProperty("type", "parent"); + + Node n1b = node.addNode("n1b", "nt:unstructured"); + n1b.addMixin(NodeType.MIX_REFERENCEABLE); + n1b.setProperty("type", "parent"); + n1b.setProperty("testJoinWithOR4", "testJoinWithOR4"); + + Node n1c = node.addNode("n1c", "nt:unstructured"); + n1c.addMixin(NodeType.MIX_REFERENCEABLE); + n1c.setProperty("type", "parent"); + + Node n3 = node.addNode("node3", "nt:unstructured"); + n3.addMixin(NodeType.MIX_REFERENCEABLE); + n3.setProperty("testref", new String[] { n1a.getIdentifier() }, + PropertyType.REFERENCE); + n3.setProperty("type", "child"); + n3.setProperty("testJoinWithOR4", "testJoinWithOR4"); + + Node n4 = node.addNode("node4", "nt:unstructured"); + n4.addMixin(NodeType.MIX_REFERENCEABLE); + n4.setProperty("testref", new String[] { n1b.getIdentifier() }, + PropertyType.REFERENCE); + n4.setProperty("type", "child"); + + Node n5 = node.addNode("node5", "nt:unstructured"); + n5.addMixin(NodeType.MIX_REFERENCEABLE); + n5.setProperty("testref", new String[] { n1c.getIdentifier() }, + PropertyType.REFERENCE); + n5.setProperty("type", "child"); + + Node parent2 = testRootNode + .addNode("jointest_other", "nt:unstructured"); + parent2.setProperty("p", "abc"); + + Node p2n1 = parent2.addNode("p2n1", "nt:unstructured"); + p2n1.setProperty("p", "abc"); + + Node p2n2 = parent2.addNode("p2n2", "nt:unstructured"); + p2n2.setProperty("p", "xyz"); + + testRootNode.getSession().save(); + } + + @Override + protected void tearDown() throws Exception { + node.remove(); + testRootNode.getSession().save(); + super.tearDown(); + } + + /** + * Test case for JCR-2718 + */ + public void testMultiValuedReferenceJoin() throws Exception { + String join = "SELECT a.*, b.*" + + " FROM [nt:unstructured] AS a" + + " INNER JOIN [nt:unstructured] AS b ON a.[jcr:uuid] = b.testref"; + checkResult(qm.createQuery(join, Query.JCR_SQL2).execute(), 3); + } + + /** + * Test case for JCR-2852 + */ + public void testJoinWithOR() throws Exception { + + String join = "SELECT a.*, b.*" + + " FROM [nt:unstructured] AS a" + + " INNER JOIN [nt:unstructured] AS b ON a.[jcr:uuid] = b.testref WHERE " + + "a.[jcr:primaryType] IS NOT NULL OR b.[jcr:primaryType] IS NOT NULL"; + checkResult(qm.createQuery(join, Query.JCR_SQL2).execute(), 3); + } + + public void testJoinWithOR2() throws Exception { + + StringBuilder join = new StringBuilder( + "SELECT a.* FROM [nt:unstructured] AS a"); + join.append(" INNER JOIN [nt:unstructured] AS b ON ISCHILDNODE(b, a) "); + join.append(" WHERE "); + join.append(" a.[p] = 'abc' OR b.[p] = 'abc' "); + checkResult(qm.createQuery(join.toString(), Query.JCR_SQL2).execute(), + 3); + } + + public void testJoinWithOR3() throws Exception { + StringBuilder join = new StringBuilder( + "SELECT a.* FROM [nt:unstructured] AS a"); + join.append(" INNER JOIN [nt:unstructured] AS b ON ISCHILDNODE(b, a) "); + join.append(" WHERE "); + join.append(" ( CONTAINS(b.*, 'abc' ) OR CONTAINS(a.*, 'abc') ) "); + join.append(" AND "); + join.append(" NAME(b) = 'p2n2' "); + checkResult(qm.createQuery(join.toString(), Query.JCR_SQL2).execute(), + 1); + } + + public void testJoinWithOR4() throws Exception { + + StringBuilder join = new StringBuilder( + "SELECT a.* FROM [nt:unstructured] AS a"); + join.append(" INNER JOIN [nt:unstructured] AS b ON b.[jcr:uuid] = a.testref "); + join.append(" WHERE "); + join.append(" a.type = 'child' "); + join.append(" AND ("); + join.append(" CONTAINS(a.*, 'testJoinWithOR4') "); + join.append(" OR "); + join.append(" b.type = 'parent' "); + join.append(" AND "); + join.append(" CONTAINS(b.*, 'testJoinWithOR4') "); + join.append(" )"); + + Query q = qm.createQuery(join.toString(), Query.JCR_SQL2); + QueryResult result = q.execute(); + checkResult(result, 2); + + } + + public void testJoinWithOR5() throws Exception { + + StringBuilder join = new StringBuilder( + "SELECT a.* FROM [nt:unstructured] AS a"); + join.append(" INNER JOIN [nt:unstructured] AS b ON b.[jcr:uuid] = a.testref "); + join.append(" WHERE "); + join.append(" a.type = 'child' AND CONTAINS(a.*, 'testJoinWithOR4') "); + join.append(" OR "); + join.append(" b.type = 'parent' AND CONTAINS(b.*, 'testJoinWithOR4') "); + + checkResult(qm.createQuery(join.toString(), Query.JCR_SQL2).execute(), + 2); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LazyResultSetQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LazyResultSetQueryTest.java new file mode 100644 index 00000000000..e4c18bc92cf --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LazyResultSetQueryTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; + +import org.junit.Ignore; + +/** + * + * This is meant to test the limits of queries and lazy fetching of nodes + * + * @see JCR-3477 + */ +public class LazyResultSetQueryTest extends AbstractQueryTest { + + @Ignore("JCR-3477") + public void testResultSet() throws RepositoryException { + int count = createNodes(testRootNode, 10, 5, 0); + testRootNode.getSession().save(); + + String stmt = testPath + "//*[@" + jcrPrimaryType + "]"; + // + " order by @jcr:score descending"; + + readResult(executeQuery(stmt), count); + } + + protected void tearDown() throws Exception { + int count = 0; + for (NodeIterator it = testRootNode.getNodes(); it.hasNext();) { + it.nextNode().remove(); + count++; + if (count % 10000 == 0) { + testRootNode.getSession().save(); + } + } + testRootNode.getSession().save(); + super.tearDown(); + } + + private void readResult(QueryResult result, int count) + throws RepositoryException { + for (RowIterator rows = result.getRows(); rows.hasNext();) { + rows.nextRow(); + count--; + } + assertEquals(0, count); + } + + private int createNodes(Node n, int nodesPerLevel, int levels, int count) + throws RepositoryException { + levels--; + for (int i = 0; i < nodesPerLevel; i++) { + Node child = n.addNode("node" + i); + count++; + if (count % 10000 == 0) { + n.getSession().save(); + } + if (levels > 0) { + count = createNodes(child, nodesPerLevel, levels, count); + } + } + return count; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LimitAndOffsetTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LimitAndOffsetTest.java new file mode 100644 index 00000000000..8448bb4932f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LimitAndOffsetTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +import org.apache.jackrabbit.api.query.JackrabbitQueryResult; + +public class LimitAndOffsetTest extends AbstractQueryTest { + + private Node node1; + private Node node2; + private Node node3; + + private Query query; + + protected void setUp() throws Exception { + super.setUp(); + + node1 = testRootNode.addNode("foo"); + node1.setProperty("name", "1"); + node2 = testRootNode.addNode("bar"); + node2.setProperty("name", "2"); + node3 = testRootNode.addNode("baz"); + node3.setProperty("name", "3"); + + testRootNode.getSession().save(); + + query = qm.createQuery("/jcr:root" + testRoot + "/* order by @name", + Query.XPATH); + } + + protected void tearDown() throws Exception { + node1 = null; + node2 = null; + node3 = null; + query = null; + super.tearDown(); + } + + protected void checkResult(QueryResult result, Node[] expectedNodes) throws RepositoryException { + assertEquals(expectedNodes.length, result.getNodes().getSize()); + } + + public void testLimit() throws Exception { + query.setLimit(1); + QueryResult result = query.execute(); + checkResult(result, new Node[] { node1 }); + + query.setLimit(2); + result = query.execute(); + checkResult(result, new Node[] { node1, node2 }); + + query.setLimit(3); + result = query.execute(); + checkResult(result, new Node[] { node1, node2, node3 }); + } + + public void testOffset() throws Exception { + query.setOffset(0); + QueryResult result = query.execute(); + checkResult(result, new Node[] { node1, node2, node3 }); + + query.setOffset(1); + result = query.execute(); + checkResult(result, new Node[] { node2, node3 }); + + query.setOffset(2); + result = query.execute(); + checkResult(result, new Node[] { node3 }); + } + + public void testOffsetAndLimit() throws Exception { + query.setOffset(0); + query.setLimit(1); + QueryResult result = query.execute(); + checkResult(result, new Node[] { node1 }); + + query.setOffset(1); + query.setLimit(1); + result = query.execute(); + checkResult(result, new Node[] { node2 }); + + query.setOffset(1); + query.setLimit(2); + result = query.execute(); + checkResult(result, new Node[] { node2, node3 }); + + query.setOffset(0); + query.setLimit(2); + result = query.execute(); + checkResult(result, new Node[] { node1, node2 }); + + // Added for JCR-1323 + query.setOffset(0); + query.setLimit(4); + result = query.execute(); + checkResult(result, new Node[] { node1, node2, node3 }); + } + + public void testOffsetAndSkip() throws Exception { + query.setOffset(1); + QueryResult result = query.execute(); + NodeIterator nodes = result.getNodes(); + nodes.skip(1); + assertTrue(node3.isSame(nodes.nextNode())); + } + + public void testOffsetAndLimitWithGetSize() throws Exception { + query.setOffset(1); + QueryResult result = query.execute(); + NodeIterator nodes = result.getNodes(); + assertEquals(2, nodes.getSize()); + if (result instanceof JackrabbitQueryResult) { + assertEquals(3, ((JackrabbitQueryResult) result).getTotalSize()); + } + + // JCR-2684: offset higher than total result => size == 0 + query.setOffset(10); + result = query.execute(); + nodes = result.getNodes(); + assertFalse(nodes.hasNext()); + assertEquals(0, nodes.getSize()); + if (result instanceof JackrabbitQueryResult) { + assertEquals(3, ((JackrabbitQueryResult) result).getTotalSize()); + } + + query.setOffset(1); + query.setLimit(1); + result = query.execute(); + nodes = result.getNodes(); + assertEquals(1, nodes.getSize()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LimitedAccessQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LimitedAccessQueryTest.java new file mode 100644 index 00000000000..308c12c54f8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/LimitedAccessQueryTest.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.core.query; + +import java.security.Principal; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.query.Query; + +import org.apache.jackrabbit.core.NodeImplTest; + +/** + * LimitedAccessQueryTest tests queries that include nodes that are + * outside their access. + */ +public class LimitedAccessQueryTest extends AbstractQueryTest { + + private Session readOnly; + private Principal principal; + + private Node a; + private Node b; + + protected void setUp() throws Exception { + super.setUp(); + + a = testRootNode.addNode("a", "nt:unstructured"); + a.setProperty("p", 1); + b = testRootNode.addNode("b", "nt:unstructured"); + b.setProperty("p", 1); + superuser.save(); + + principal = NodeImplTest.getReadOnlyPrincipal(getHelper()); + NodeImplTest.changeReadPermission(principal, a, false); + superuser.save(); + + readOnly = getHelper().getReadOnlySession(); + + // preliminary tests + try { + readOnly.getNodeByIdentifier(a.getIdentifier()); + fail("Access to the node '" + a.getPath() + "' has to be denied."); + } catch (ItemNotFoundException e) { + // good acl + } + + try { + readOnly.getNodeByIdentifier(b.getIdentifier()); + } catch (ItemNotFoundException e) { + fail(e.getMessage()); + } + + } + + protected void tearDown() throws Exception { + readOnly.logout(); + NodeImplTest.changeReadPermission(principal, a, true); + super.tearDown(); + } + + /** + * this test is for the DescendantSelfAxisQuery class. + * + * see JCR-3001 + * + * @throws Exception + */ + @SuppressWarnings("deprecation") + public void testDescendantSelfAxisQuery() throws Exception { + String xpath = "/" + testRootNode.getPath() + "//*"; + checkResult( + readOnly.getWorkspace().getQueryManager() + .createQuery(xpath, Query.XPATH).execute(), + new Node[] { b }); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/MixinTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/MixinTest.java new file mode 100644 index 00000000000..b2c22869b3e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/MixinTest.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.core.query; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.commons.cnd.CndImporter; + +/** + * Tests if mixin types are queried correctly when using element test: element() + */ +public class MixinTest extends AbstractQueryTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + + NodeTypeManager manager = superuser.getWorkspace().getNodeTypeManager(); + if (!manager.hasNodeType("test:mimeType")) { + String cnd = + "\n" + + "[test:mimeType] > mix:mimeType mixin"; + + Reader cndReader = new InputStreamReader(new ByteArrayInputStream(cnd.getBytes())); + CndImporter.registerNodeTypes(cndReader, superuser); + } + } + + public void testBuiltInMixin() throws RepositoryException { + // nt:resource is mix:mimeType by its node type definition + Node n1 = testRootNode.addNode("n1", "nt:resource"); + n1.setProperty("jcr:data", new ByteArrayInputStream("hello world".getBytes())); + n1.setProperty("jcr:lastModified", Calendar.getInstance()); + n1.setProperty("jcr:mimeType", "application/octet-stream"); + + // assign mix:referenceable to arbitrary node + Node n2 = testRootNode.addNode("n2"); + n2.addMixin("mix:mimeType"); + + // make node referenceable using a mixin that extends from mix:mimeType + Node n3 = testRootNode.addNode("n3"); + n3.addMixin("test:mimeType"); + + testRootNode.save(); + + String query = testPath + "//element(*, mix:mimeType)"; + executeXPathQuery(query, new Node[]{n1, n2, n3}); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/OrderByTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/OrderByTest.java new file mode 100644 index 00000000000..45eefd9900b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/OrderByTest.java @@ -0,0 +1,315 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; + +/** + * Tests queries with order by. + */ +public class OrderByTest extends AbstractQueryTest { + + public void testOrderByScore() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("text", "aaa"); + n1.setProperty("value", 3); + n2.setProperty("text", "bbb"); + n2.setProperty("value", 2); + n3.setProperty("text", "ccc"); + n3.setProperty("value", 2); + + testRootNode.save(); + + String sql = "SELECT value FROM nt:unstructured WHERE " + + "jcr:path LIKE '" + testRoot + "/%' ORDER BY jcr:score, value"; + Query q = qm.createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 3); + + String xpath = "/" + testRoot + "/*[@jcr:primaryType='nt:unstructured'] order by jcr:score(), @value"; + q = qm.createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 3); + } + + /** + * Test for JCR-2906 + */ + public void testOrderByMVP() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + Node n4 = testRootNode.addNode("node4"); + Node n5 = testRootNode.addNode("node5"); + + n1.setProperty("extra", new String[] { "12345" }); + n1.setProperty("text", new String[] { "ccc" }); + + n2.setProperty("text", new String[] { "eee", "bbb" }); + n3.setProperty("text", new String[] { "aaa" }); + n4.setProperty("text", new String[] { "bbb", "aaa" }); + n5.setProperty("text", new String[] { "eee", "aaa" }); + + testRootNode.getSession().save(); + + String sql = "SELECT value FROM nt:unstructured WHERE " + + "jcr:path LIKE '" + testRoot + "/%' ORDER BY text"; + checkResultSequence(executeQuery(sql).getRows(), new Node[] { n3, n4, + n1, n5, n2 }); + + String xpath = "/" + + testRoot + + "/*[@jcr:primaryType='nt:unstructured'] order by jcr:score(), @text"; + checkResultSequence(executeQuery(xpath).getRows(), new Node[] { n3, n4, + n1, n5, n2 }); + } + + public void testOrderByUpperCase() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("text", "Amundsen"); + n2.setProperty("text", "barents"); + n3.setProperty("text", "Wegener"); + + testRootNode.save(); + + String xpath = "/" + testRoot + "/*[@jcr:primaryType='nt:unstructured'] order by fn:upper-case(@text)"; + Query q = qm.createQuery(xpath, Query.XPATH); + QueryResult result = q.execute(); + checkResult(result, new Node[]{n1, n2, n3}); + } + + public void testOrderByLowerCase() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("text", "Amundsen"); + n2.setProperty("text", "barents"); + n3.setProperty("text", "Wegener"); + + testRootNode.save(); + + String xpath = "/" + testRoot + "/*[@jcr:primaryType='nt:unstructured'] order by fn:lower-case(@text)"; + Query q = qm.createQuery(xpath, Query.XPATH); + QueryResult result = q.execute(); + checkResult(result, new Node[]{n1, n2, n3}); + } + + public void testChildAxisString() throws RepositoryException { + checkChildAxis(new Value[]{getValue("a"), getValue("b"), getValue("c")}); + } + + public void testChildAxisLong() throws RepositoryException { + checkChildAxis(new Value[]{getValue(1), getValue(2), getValue(3)}); + } + + public void testChildAxisDouble() throws RepositoryException { + checkChildAxis(new Value[]{getValue(1.0), getValue(2.0), getValue(3.0)}); + } + + public void testChildAxisBoolean() throws RepositoryException { + checkChildAxis(new Value[]{getValue(false), getValue(true)}); + } + + public void testChildAxisCalendar() throws RepositoryException { + Calendar c1 = Calendar.getInstance(); + Calendar c2 = Calendar.getInstance(); + c2.add(Calendar.MINUTE, 1); + Calendar c3 = Calendar.getInstance(); + c3.add(Calendar.MINUTE, 2); + checkChildAxis(new Value[]{getValue(c1), getValue(c2), getValue(c3)}); + } + + public void testChildAxisName() throws RepositoryException { + checkChildAxis(new Value[]{getNameValue("a"), getNameValue("b"), getNameValue("c")}); + } + + public void testChildAxisPath() throws RepositoryException { + checkChildAxis(new Value[]{getPathValue("a"), getPathValue("b"), getPathValue("c")}); + } + + public void testChildAxisDeep() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.addNode("a").addNode("b"); // no property + Node n2 = testRootNode.addNode("node2"); + n2.addNode("a").addNode("b").addNode("c").setProperty("prop", "a"); + Node n3 = testRootNode.addNode("node3"); + n3.addNode("a").addNode("b").addNode("c").setProperty("prop", "b"); + testRootNode.save(); + + List expected = Arrays.asList(new String[]{n1.getPath(), n2.getPath(), n3.getPath()}); + String xpath = testPath + "/* order by a/b/c/@prop"; + assertEquals(expected, collectPaths(executeQuery(xpath))); + + // descending + Collections.reverse(expected); + xpath = testPath + "/* order by a/b/c/@prop descending"; + assertEquals(expected, collectPaths(executeQuery(xpath))); + } + + public void testChildAxisNoValue() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + n1.addNode("child").setProperty("prop", "a"); + Node n2 = testRootNode.addNode("node2"); + n2.addNode("child"); + testRootNode.save(); + + List expected = Arrays.asList(new String[]{n2.getPath(), n1.getPath()}); + String xpath = testPath + "/* order by child/@prop"; + assertEquals(expected, collectPaths(executeQuery(xpath))); + + // descending + Collections.reverse(expected); + xpath = testPath + "/* order by child/@prop descending"; + assertEquals(expected, collectPaths(executeQuery(xpath))); + + // reverse order in content + n1.getNode("child").getProperty("prop").remove(); + n2.getNode("child").setProperty("prop", "a"); + testRootNode.save(); + + Collections.reverse(expected); + assertEquals(expected, collectPaths(executeQuery(xpath))); + } + + public void testChildAxisMixedTypes() throws RepositoryException { + // when differing types are used then the class name of the type + // is used for comparison: + // java.lang.Double < java.lang.Integer + checkChildAxis(new Value[]{getValue(2.0), getValue(1)}); + } + + //------------------------------< helper >---------------------------------- + + private Value getValue(String value) throws RepositoryException { + return superuser.getValueFactory().createValue(value); + } + + private Value getValue(long value) throws RepositoryException { + return superuser.getValueFactory().createValue(value); + } + + private Value getValue(double value) throws RepositoryException { + return superuser.getValueFactory().createValue(value); + } + + private Value getValue(boolean value) throws RepositoryException { + return superuser.getValueFactory().createValue(value); + } + + private Value getValue(Calendar value) throws RepositoryException { + return superuser.getValueFactory().createValue(value); + } + + private Value getNameValue(String value) throws RepositoryException { + return superuser.getValueFactory().createValue(value, PropertyType.NAME); + } + + private Value getPathValue(String value) throws RepositoryException { + return superuser.getValueFactory().createValue(value, PropertyType.PATH); + } + + /** + * Checks if order by with a relative path works on the the passed values. + * The values are expected to be in ascending order. + * + * @param values the values in ascending order. + * @throws RepositoryException if an error occurs. + */ + private void checkChildAxis(Value[] values) throws RepositoryException { + // child/prop is part of the test indexing configuration, + // this will use SimpleScoreDocComparator internally + checkChildAxis(values, "child", "property"); + cleanUpTestRoot(superuser); + // c/p is not in the indexing configuration, + // this will use RelPathScoreDocComparator internally + checkChildAxis(values, "c", "p"); + } + + /** + * Checks if order by with a relative path works on the the passed values. + * The values are expected to be in ascending order. + * + * @param values the values in ascending order. + * @param child the name of the child node. + * @param property the name of the property. + * @throws RepositoryException if an error occurs. + */ + private void checkChildAxis(Value[] values, String child, String property) + throws RepositoryException { + List vals = new ArrayList(); + // add initial value null -> property not set + // inexistent property is always less than any property value set + vals.add(null); + vals.addAll(Arrays.asList(values)); + + List expected = new ArrayList(); + for (int i = 0; i < vals.size(); i++) { + Node n = testRootNode.addNode("node" + i); + expected.add(n.getPath()); + Node c = n.addNode(child); + if (vals.get(i) != null) { + c.setProperty(property, (Value) vals.get(i)); + } + } + testRootNode.save(); + + String xpath = testPath + "/* order by " + child + "/@" + property; + assertEquals(expected, collectPaths(executeQuery(xpath))); + + // descending + Collections.reverse(expected); + xpath += " descending"; + assertEquals(expected, collectPaths(executeQuery(xpath))); + + Collections.reverse(vals); + for (int i = 0; i < vals.size(); i++) { + Node c = testRootNode.getNode("node" + i).getNode(child); + c.setProperty(property, (Value) vals.get(i)); + } + testRootNode.save(); + + Collections.reverse(expected); + assertEquals(expected, collectPaths(executeQuery(xpath))); + } + + private static List collectPaths(QueryResult result) + throws RepositoryException { + List paths = new ArrayList(); + for (NodeIterator it = result.getNodes(); it.hasNext(); ) { + paths.add(it.nextNode().getPath()); + } + return paths; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ParentNodeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ParentNodeTest.java new file mode 100644 index 00000000000..712da14a230 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ParentNodeTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +/** + * QueryResultTest tests various methods on the + * NodeIterator returned by a QueryResult. + */ +public class ParentNodeTest extends AbstractQueryTest { + + protected void setUp() throws Exception { + super.setUp(); + + // creates the following test structure: + // + base (foo1=bar1) + // + child (foo2=bar2) + // + child2 (foo2=bar2) + // + child3 (foo2=bar2) + Node base = testRootNode.addNode("base"); + base.setProperty("foo1", "bar1"); + + base.addNode("child").setProperty("foo2", "bar2"); + base.addNode("child2").setProperty("foo2", "bar2"); + base.addNode("child3").setProperty("foo2", "bar2"); + + superuser.save(); + } + + public void testParentInPath() throws RepositoryException { + String stmt = testPath + "//child/..[@foo1]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertEquals("Wrong size of NodeIterator in result", + 1, result.getNodes().getSize()); + + assertEquals("base", result.getNodes().nextNode().getName()); + } + + public void testParentInAttribute1() throws RepositoryException { + String stmt = testPath + "//child[../@foo1]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertTrue("Wrong size of NodeIterator in result", result.getNodes().getSize() > 0); + + assertEquals("child", result.getNodes().nextNode().getName()); + } + + public void testParentInAttribute2() throws RepositoryException { + String stmt = testPath + "//child[../child/@foo2]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertTrue("Wrong size of NodeIterator in result", result.getNodes().getSize() > 0); + + assertEquals("child", result.getNodes().nextNode().getName()); + } + + public void testParentInAttribute3() throws RepositoryException { + String stmt = testPath + "//child[../../base/@foo1]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertTrue("Wrong size of NodeIterator in result", result.getNodes().getSize() > 0); + + assertEquals("child", result.getNodes().nextNode().getName()); + } + + public void testParentInAttribute4() throws RepositoryException { + String stmt = testPath + "//child[../@foo1 = 'bar1']"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertTrue("Wrong size of NodeIterator in result", result.getNodes().getSize() > 0); + + assertEquals("child", result.getNodes().nextNode().getName()); + } + + public void testParentAttributeWithDescendant() throws RepositoryException { + String stmt = testPath + "//base[../base/@foo1 = 'bar1']/child"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertTrue("Wrong size of NodeIterator in result", result.getNodes().getSize() > 0); + + assertEquals("child", result.getNodes().nextNode().getName()); + } + + public void testParentWithAnd() throws RepositoryException { + String stmt = testPath + "//child[../@foo1 = 'bar1 and @foo2']"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertEquals("Wrong size of NodeIterator in result", 0, result.getNodes().getSize()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/PathQueryNodeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/PathQueryNodeTest.java new file mode 100644 index 00000000000..e7e7fe409e8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/PathQueryNodeTest.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.core.query; + +import java.util.Arrays; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.query.DefaultQueryNodeFactory; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.xpath.XPathQueryBuilder; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; + +public class PathQueryNodeTest extends TestCase { + + private static final DefaultQueryNodeFactory QUERY_NODE_FACTORY = new DefaultQueryNodeFactory( + Arrays.asList(new Name[] { NameConstants.NT_NODETYPE })); + + private static final NameResolver JCR_RESOLVER = new DefaultNamePathResolver(new NamespaceResolver() { + + public String getPrefix(String uri) { + throw new UnsupportedOperationException(); + } + + public String getURI(String prefix) { + if (Name.NS_JCR_PREFIX.equals(prefix)) + return Name.NS_JCR_URI; + if (Name.NS_NT_PREFIX.equals(prefix)) + return Name.NS_NT_URI; + return ""; + } + }); + + public void testNeedsSystemTree() throws Exception { + QueryRootNode queryRootNode = XPathQueryBuilder.createQuery("/jcr:root/*", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertTrue(queryRootNode.needsSystemTree()); + + queryRootNode = XPathQueryBuilder.createQuery("/jcr:root/test/*", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertFalse(queryRootNode.needsSystemTree()); + + queryRootNode = XPathQueryBuilder.createQuery("*", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertTrue(queryRootNode.needsSystemTree()); + + queryRootNode = XPathQueryBuilder.createQuery("jcr:system/*", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertTrue(queryRootNode.needsSystemTree()); + + queryRootNode = XPathQueryBuilder.createQuery("test//*", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertFalse(queryRootNode.needsSystemTree()); + + queryRootNode = XPathQueryBuilder.createQuery("//test/*", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertTrue(queryRootNode.needsSystemTree()); + } + + public void testNeedsSystemTreeForAllNodesByNodeType() throws Exception { + QueryRootNode queryRootNode = XPathQueryBuilder.createQuery("//element(*, nt:resource)", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertFalse(queryRootNode.needsSystemTree()); + + queryRootNode = XPathQueryBuilder.createQuery("//element(*, nt:resource)[@jcr:test = 'foo']", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertFalse(queryRootNode.needsSystemTree()); + + queryRootNode = XPathQueryBuilder.createQuery("//element(*, nt:nodeType)", JCR_RESOLVER, QUERY_NODE_FACTORY); + assertTrue(queryRootNode.needsSystemTree()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/QueryResultTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/QueryResultTest.java new file mode 100644 index 00000000000..f4f63a89939 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/QueryResultTest.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.core.query; + +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import java.util.NoSuchElementException; + +/** + * QueryResultTest tests various methods on the + * NodeIterator returned by a QueryResult. + */ +public class QueryResultTest extends AbstractQueryTest { + + private static int INITIAL_NODE_NUM = 55; + + protected void setUp() throws Exception { + super.setUp(); + for (int i = 0; i < INITIAL_NODE_NUM; i++) { + testRootNode.addNode("node" + i).setProperty(propertyName1, i); + } + testRootNode.save(); + } + + public void testGetSize() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + for (int i = 0; i < 10; i++) { + String stmt = testPath + "/*[@" + propertyName1 + " < 1000]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertEquals("Wrong size of NodeIterator in result", + INITIAL_NODE_NUM - i, result.getNodes().getSize()); + // remove node for the next iteration + testRootNode.getNode("node" + i).remove(); + testRootNode.save(); + } + } + + public void testGetSizeOrderByScore() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + for (int i = 0; i < 10; i++) { + String stmt = testPath + "/*[@" + propertyName1 + " < 1000] order by jcr:score()"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertEquals("Wrong size of NodeIterator in result", + INITIAL_NODE_NUM - i, result.getNodes().getSize()); + // remove node for the next iteration + testRootNode.getNode("node" + i).remove(); + testRootNode.save(); + } + } + + public void testIteratorNext() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + for (int i = 0; i < 10; i++) { + String stmt = testPath + "/*[@" + propertyName1 + " < 1000]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + int size = 0; + for (NodeIterator it = result.getNodes(); it.hasNext(); ) { + it.nextNode(); + size++; + } + assertEquals("Wrong size of NodeIterator in result", + INITIAL_NODE_NUM - i, size); + // remove node for the next iteration + testRootNode.getNode("node" + i).remove(); + testRootNode.save(); + } + } + + public void testIteratorNextOrderByScore() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + for (int i = 0; i < 10; i++) { + String stmt = testPath + "/*[@" + propertyName1 + " < 1000] order by jcr:score()"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + int size = 0; + for (NodeIterator it = result.getNodes(); it.hasNext(); ) { + it.nextNode(); + size++; + } + assertEquals("Wrong size of NodeIterator in result", + INITIAL_NODE_NUM - i, size); + // remove node for the next iteration + testRootNode.getNode("node" + i).remove(); + testRootNode.save(); + } + } + + public void testSkip() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + for (int i = 0; i < 10; i++) { + String stmt = testPath + "/*[@" + propertyName1 + " < 1000]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + for (int j = 0; j < INITIAL_NODE_NUM - i; j++) { + // skip to each node in the result + NodeIterator it = result.getNodes(); + it.skip(j); + long propValue = it.nextNode().getProperty(propertyName1).getLong(); + // expected = number of skipped nodes + number of deleted nodes + long expected = j + i; + assertEquals("Wrong node after skip()", expected, propValue); + } + try { + NodeIterator it = result.getNodes(); + it.skip(it.getSize() + 1); + fail("must throw NoSuchElementException"); + } catch (NoSuchElementException e) { + // correct + } + // remove node for the next iteration + testRootNode.getNode("node" + i).remove(); + testRootNode.save(); + } + } + + public void testSkipOrderByProperty() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + for (int i = 0; i < 10; i++) { + String stmt = testPath + "/*[@" + propertyName1 + + " < 1000] order by @" + propertyName1; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + for (int j = 0; j < INITIAL_NODE_NUM - i; j++) { + // skip to each node in the result + NodeIterator it = result.getNodes(); + it.skip(j); + long propValue = it.nextNode().getProperty(propertyName1).getLong(); + // expected = number of skipped nodes + number of deleted nodes + long expected = j + i; + assertEquals("Wrong node after skip()", expected, propValue); + } + try { + NodeIterator it = result.getNodes(); + it.skip(it.getSize() + 1); + fail("must throw NoSuchElementException"); + } catch (NoSuchElementException e) { + // correct + } + // remove node for the next iteration + testRootNode.getNode("node" + i).remove(); + testRootNode.save(); + } + } + + public void testGetPosition() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + for (int i = 0; i < 10; i++) { + String stmt = testPath + "/*[@" + propertyName1 + " < 1000]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + NodeIterator it = result.getNodes(); + assertEquals("Wrong position", 0, it.getPosition()); + int count = 0; + while (it.hasNext()) { + long position = it.getPosition(); + it.nextNode(); + assertEquals("Wrong position", count++, position); + } + try { + it.next(); + fail("must throw NoSuchElementException"); + } catch (Exception e) { + // correct + } + // remove node for the next iteration + testRootNode.getNode("node" + i).remove(); + testRootNode.save(); + } + } + + public void testGetPositionOrderBy() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + for (int i = 0; i < 10; i++) { + String stmt = testPath + "/*[@" + propertyName1 + " < 1000] order by jcr:score()"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + NodeIterator it = result.getNodes(); + assertEquals("Wrong position", 0, it.getPosition()); + int count = 0; + while (it.hasNext()) { + long position = it.getPosition(); + it.nextNode(); + assertEquals("Wrong position", count++, position); + } + try { + it.next(); + fail("must throw NoSuchElementException"); + } catch (Exception e) { + // correct + } + // remove node for the next iteration + testRootNode.getNode("node" + i).remove(); + testRootNode.save(); + } + } + + public void testPositionEmptyResult() throws RepositoryException { + QueryManager qm = superuser.getWorkspace().getQueryManager(); + String stmt = testPath + "/*[@" + propertyName1 + " > 1000]"; + QueryResult result = qm.createQuery(stmt, Query.XPATH).execute(); + assertEquals("Wrong position", 0, result.getNodes().getPosition()); + assertEquals("Wrong position", 0, result.getRows().getPosition()); + stmt += " order by jcr:score()"; + result = qm.createQuery(stmt, Query.XPATH).execute(); + assertEquals("Wrong position", 0, result.getNodes().getPosition()); + assertEquals("Wrong position", 0, result.getRows().getPosition()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2NodeLocalNameTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2NodeLocalNameTest.java new file mode 100644 index 00000000000..b9f5e17df1c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2NodeLocalNameTest.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.core.query; + +import javax.jcr.Node; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Test case for Node LocalName queries with JCR_SQL2 + * + * Inspired by JCR-2956 + */ +public class SQL2NodeLocalNameTest extends AbstractQueryTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + testRootNode.addNode("SQL2NodeLocalNameTest_node1"); + testRootNode.addNode("SQL2NodeLocalNameTest_node2"); + testRootNode.getSession().save(); + } + + @Override + protected void tearDown() throws Exception { + for (Node c : JcrUtils.getChildNodes(testRootNode)) { + testRootNode.getSession().removeItem(c.getPath()); + } + superuser.save(); + super.tearDown(); + } + + public void testEq() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE localname(NODE) = 'SQL2NodeLocalNameTest_node1' "), + 1); + } + + public void testLikeEnd() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE localname(NODE) like 'SQL2NodeLocalNameTest%' "), + 2); + } + + public void testLikeMiddle() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE localname(NODE) like '%NodeLocalNameTest%' "), + 2); + } + + public void testLikeNoHits() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE localname(NODE) like 'XXX_NodeLocalNameTest_XXX' "), + 0); + } + + /** + * funny enough, this will return the root node + */ + public void testLikeEmpty() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE localname(NODE) like '' "), + 1); + } + + public void testLikeEmptyAsChild() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE ischildnode(NODE, [" + + testRootNode.getPath() + + "]) AND localname(NODE) like '' "), 0); + } + + public void testLikeAllAsChild() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE ischildnode(NODE, [" + + testRootNode.getPath() + + "]) AND localname(NODE) like '%' "), 2); + } + + /** + * test for JCR-3159 + */ + public void testLowerLocalName() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE LOWER(localname(NODE)) like 'sql2nodelocalnametest%'"), + 2); + } + + /** + * test for JCR-3159 + */ + public void testUpperLocalName() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE UPPER(localname(NODE)) like 'SQL2NODELOCALNAMETEST%'"), + 2); + } + + /** + * test for JCR-3159 + */ + public void testLowerName() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE LOWER(name(NODE)) like 'sql2nodelocalnametest%'"), + 2); + } + + /** + * test for JCR-3398 + */ + public void testLowerLocalNameOrContains() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE LOWER(localname(NODE)) like 'sql2nodelocalnametest%' OR contains(NODE.*, 'sql2nodelocalnametest')"), + 2); + } + + /** + * test for JCR-3398 + */ + public void testUpperLocalNameOrContains() throws Exception { + checkResult( + executeSQL2Query("SELECT * FROM [nt:base] as NODE WHERE UPPER(localname(NODE)) like 'SQL2NODELOCALNAMETEST%' OR contains(NODE.*, 'SQL2NODELOCALNAMETEST')"), + 2); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OffsetLimitTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OffsetLimitTest.java new file mode 100644 index 00000000000..081e5114cc0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OffsetLimitTest.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.core.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Test case for queries with JCR_SQL2 having offset and limit clauses + * + * Inspired by JCR-2830 + * + */ +public class SQL2OffsetLimitTest extends AbstractQueryTest { + + private final List c = Arrays.asList("a", "b", "c", "d", "e"); + + @Override + protected void setUp() throws Exception { + super.setUp(); + for (String s : c) { + testRootNode.addNode(s); + } + testRootNode.getSession().save(); + } + + @Override + protected void tearDown() throws Exception { + for (Node c : JcrUtils.getChildNodes(testRootNode)) { + testRootNode.getSession().removeItem(c.getPath()); + } + testRootNode.getSession().save(); + super.tearDown(); + } + + private Query newQuery() throws Exception { + return qm.createQuery("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) order by [jcr:score] ", Query.JCR_SQL2); + } + + public void testNoConstraints() throws Exception { + List expected = new ArrayList(c); + Query q = newQuery(); + + List out = qrToPaths(q.execute()); + assertEquals(c.size(), out.size()); + for (String s : out) { + assertTrue(expected.remove(s)); + } + assertTrue(expected.isEmpty()); + } + + public void testLimitEqSize() throws Exception { + List expected = new ArrayList(c); + Query q = newQuery(); + q.setOffset(0); + q.setLimit(c.size()); + + List out = qrToPaths(q.execute()); + assertEquals(c.size(), out.size()); + for (String s : out) { + assertTrue(expected.remove(s)); + } + assertTrue(expected.isEmpty()); + } + + public void testLimitGtSize() throws Exception { + List expected = new ArrayList(c); + Query q = newQuery(); + q.setOffset(0); + q.setLimit(c.size() * 2); + + List out = qrToPaths(q.execute()); + assertEquals(c.size(), out.size()); + for (String s : out) { + assertTrue(expected.remove(s)); + } + assertTrue(expected.isEmpty()); + } + + public void testOffsetEqSize() throws Exception { + Query q = newQuery(); + q.setOffset(c.size() - 1); + List out = qrToPaths(q.execute()); + assertEquals(1, out.size()); + } + + public void testOffsetGtSize() throws Exception { + Query q = newQuery(); + q.setOffset(c.size() * 2); + List out = qrToPaths(q.execute()); + assertTrue(out.isEmpty()); + } + + public void testSimplePagination() throws Exception { + List expected = new ArrayList(c); + Query q = newQuery(); + + for (int i = 0; i < c.size(); i++) { + q.setOffset(i); + q.setLimit(1); + List out = qrToPaths(q.execute()); + assertEquals(1, out.size()); + assertTrue(expected.remove(out.get(0))); + } + assertTrue(expected.isEmpty()); + } + + public void test2BigPages() throws Exception { + List expected = new ArrayList(c); + Query q = newQuery(); + + int p1 = (int) (c.size() * 0.8); + int p2 = c.size() - p1; + + q.setOffset(0); + q.setLimit(p1); + List out1 = qrToPaths(q.execute()); + assertEquals(p1, out1.size()); + for (String s : out1) { + assertTrue(expected.remove(s)); + } + + q.setOffset(p1); + q.setLimit(p2); + List out2 = qrToPaths(q.execute()); + assertEquals(p2, out2.size()); + for (String s : out2) { + assertTrue(expected.remove(s)); + } + + assertTrue(expected.isEmpty()); + } + + private List qrToPaths(QueryResult qr) throws RepositoryException { + List ret = new ArrayList(); + for (Row row : JcrUtils.getRows(qr)) { + Node n = row.getNode(); + ret.add(n.getPath().replace(n.getParent().getPath() + "/", "")); + } + return ret; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OrderByTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OrderByTest.java new file mode 100644 index 00000000000..7889b87737f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OrderByTest.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Tests queries with order by. + */ +public class SQL2OrderByTest extends AbstractQueryTest { + + // TODO order by aggregate test? + // TODO enable the test once the native sort is properly handled. + + static { + // To see JCR-2959 in action, enable the following + // System.setProperty(QueryEngine.NATIVE_SORT_SYSTEM_PROPERTY, "true"); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + for (Node c : JcrUtils.getChildNodes(testRootNode)) { + testRootNode.getSession().removeItem(c.getPath()); + } + testRootNode.getSession().save(); + super.tearDown(); + } + + public void testOrderByScore() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("text", "aaa"); + n1.setProperty("value", 3); + testRootNode.getSession().save(); + + n2.setProperty("text", "bbb"); + n2.setProperty("value", 2); + testRootNode.getSession().save(); + + n3.setProperty("text", "ccc"); + n3.setProperty("value", 2); + testRootNode.getSession().save(); + + QueryResult qr = executeSQL2Query("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY SCORE()"); + RowIterator rows = qr.getRows(); + + long size = rows.getSize(); + assertTrue(size == 3 || size == -1); + size = 0; + + double score = Double.MIN_VALUE; + while (rows.hasNext()) { + double nextScore = rows.nextRow().getScore(); + assertTrue(nextScore >= score); + score = nextScore; + size++; + } + assertEquals(3, size); + } + + /** + * SQL2 Test for JCR-2906 + */ + public void testOrderByMVP() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + Node n4 = testRootNode.addNode("node4"); + Node n5 = testRootNode.addNode("node5"); + + n1.setProperty("text", new String[] { "ccc" }); + n2.setProperty("text", new String[] { "eee", "bbb" }); + n3.setProperty("text", new String[] { "aaa" }); + n4.setProperty("text", new String[] { "bbb", "aaa" }); + n5.setProperty("text", new String[] { "eee", "aaa" }); + + testRootNode.getSession().save(); + + String sql = "SELECT value FROM [nt:unstructured] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY text"; + + checkSeq(executeSQL2Query(sql), new Node[] { n3, n4, n1, n5, n2 }); + } + + public void testOrderByVal() throws RepositoryException { + + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("value", 3); + n2.setProperty("value", 1); + n3.setProperty("value", 2); + + testRootNode.getSession().save(); + + QueryResult qr = executeSQL2Query("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY [value]"); + checkSeq(qr, new Node[] { n2, n3, n1 }); + } + + public void testOrderByValDesc() throws RepositoryException { + + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("value", 3); + n2.setProperty("value", 1); + n3.setProperty("value", 2); + + testRootNode.getSession().save(); + + QueryResult qr = executeSQL2Query("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY [value] desc"); + checkSeq(qr, new Node[] { n1, n3, n2 }); + } + + public void testOrderByValMult() throws RepositoryException { + + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("value", 2); + n1.setProperty("text", "b"); + + n2.setProperty("value", 1); + n2.setProperty("text", "x"); + + n3.setProperty("value", 2); + n3.setProperty("text", "a"); + + testRootNode.getSession().save(); + + QueryResult qr = executeSQL2Query("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY [value], [text]"); + checkSeq(qr, new Node[] { n2, n3, n1 }); + } + + public void testOrderByValMultDesc() throws RepositoryException { + + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("value", 2); + n1.setProperty("text", "b"); + + n2.setProperty("value", 1); + n2.setProperty("text", "x"); + + n3.setProperty("value", 2); + n3.setProperty("text", "a"); + + testRootNode.getSession().save(); + + QueryResult qr = executeSQL2Query("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY [value] desc, [text] desc"); + checkSeq(qr, new Node[] { n1, n3, n2 }); + } + + public void testOrderByValMultMix() throws RepositoryException { + + Node n1 = testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + + n1.setProperty("value", 2); + n1.setProperty("text", "b"); + + n2.setProperty("value", 1); + n2.setProperty("text", "x"); + + n3.setProperty("value", 2); + n3.setProperty("text", "a"); + + testRootNode.getSession().save(); + + QueryResult qr = executeSQL2Query("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY [value], [text] desc"); + checkSeq(qr, new Node[] { n2, n1, n3 }); + } + + public void testOrderByFnc() throws RepositoryException { + + Node n1 = testRootNode.addNode("node1", "nt:unstructured"); + Node n2 = testRootNode.addNode("node2", "nt:unstructured"); + Node n3 = testRootNode.addNode("node3", "nt:unstructured"); + + n1.setProperty("value", "aaa"); + n2.setProperty("value", "a"); + n3.setProperty("value", "aa"); + + testRootNode.getSession().save(); + + QueryResult qr = executeSQL2Query("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY LENGTH([value])"); + checkSeq(qr, new Node[] { n2, n3, n1 }); + } + + public void testOrderByFncDesc() throws RepositoryException { + + Node n1 = testRootNode.addNode("node1", "nt:unstructured"); + Node n2 = testRootNode.addNode("node2", "nt:unstructured"); + Node n3 = testRootNode.addNode("node3", "nt:unstructured"); + + n1.setProperty("value", "aaa"); + n2.setProperty("value", "a"); + n3.setProperty("value", "aa"); + + testRootNode.getSession().save(); + + QueryResult qr = executeSQL2Query("SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) ORDER BY LENGTH([value]) desc"); + checkSeq(qr, new Node[] { n1, n3, n2 }); + } + + private void checkSeq(QueryResult qr, Node[] nodes) + throws RepositoryException { + NodeIterator ni = qr.getNodes(); + for (Node n : nodes) { + assertTrue(ni.hasNext()); + assertEquals(n.getPath(), ni.nextNode().getPath()); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OuterJoinTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OuterJoinTest.java new file mode 100644 index 00000000000..3a461d8d832 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2OuterJoinTest.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.core.query; + +import static javax.jcr.query.Query.JCR_SQL2; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.io.Reader; + +import javax.jcr.Node; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.commons.cnd.CndImporter; + +/** + * Test case for OUTER JOIN queries with JCR_SQL2 + * + * Inspired by JCR-2933 + * + */ +public class SQL2OuterJoinTest extends AbstractQueryTest { + + private Node n2; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + NodeTypeManager manager = superuser.getWorkspace().getNodeTypeManager(); + if (!manager.hasNodeType("test:SamplePage")) { + StringBuilder defs = new StringBuilder(); + defs.append("[test:SamplePage]\n"); + defs.append(" - n1prop1\n"); + defs.append(" + * (nt:base) = nt:unstructured \n"); + defs.append("[test:SampleContent]\n"); + defs.append(" - n2prop1"); + Reader cndReader = new InputStreamReader(new ByteArrayInputStream( + defs.toString().getBytes())); + CndImporter.registerNodeTypes(cndReader, superuser); + } + + Node n1 = testRootNode.addNode("node1", "test:SamplePage"); + n1.setProperty("n1prop1", "page1"); + n2 = n1.addNode("node2", "test:SampleContent"); + n2.setProperty("n2prop1", "content1"); + testRootNode.getSession().save(); + } + + @Override + protected void tearDown() throws Exception { + for (Node c : JcrUtils.getChildNodes(testRootNode)) { + testRootNode.getSession().removeItem(c.getPath()); + } + testRootNode.getSession().save(); + super.tearDown(); + } + + public void testOuterJoin() throws Exception { + StringBuilder join = new StringBuilder(); + join.append(" Select * from [test:SamplePage] as page left outer join [test:SampleContent] as content on ISDESCENDANTNODE(content,page)"); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), 1); + } + + /** + * Test case for JCR-2933 + * + * Outer Join that works OOTB + */ + public void testOuterJoinWithCondition() throws Exception { + StringBuilder join = new StringBuilder(); + join.append(" Select * from [test:SamplePage] as page left outer join [test:SampleContent] as content on ISDESCENDANTNODE(content,page) where page.n1prop1 = 'page1' and content.n2prop1 = 'content1' "); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), 1); + } + + /** + * Test case for JCR-2933 + * + * Outer Join that does not work on missing where clause, hence the jira + * issue + */ + public void testOuterJoinMissingProperty() throws Exception { + StringBuilder join = new StringBuilder(); + join.append(" Select * from [test:SamplePage] as page left outer join [test:SampleContent] as content on ISDESCENDANTNODE(content,page) where page.n1prop1 = 'page1' and content.n2prop1 = 'XXX' "); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), 0); + } + + /** + * Test case for JCR-2933 + * + * Outer Join that does not work on missing child node, no WHERE condition + */ + public void testOuterJoinMissingNode() throws Exception { + testRootNode.getSession().removeItem(n2.getPath()); + testRootNode.getSession().save(); + + StringBuilder join = new StringBuilder(); + join.append(" Select * from [test:SamplePage] as page left outer join [test:SampleContent] as content on ISDESCENDANTNODE(content,page)"); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), 1); + } + + public void testOuterJoinMissingNodeWithCondition() throws Exception { + testRootNode.getSession().removeItem(n2.getPath()); + testRootNode.getSession().save(); + + StringBuilder join = new StringBuilder(); + join.append(" Select * from [test:SamplePage] as page left outer join [test:SampleContent] as content on ISDESCENDANTNODE(content,page) where page.n1prop1 = 'page1' and content.n2prop1 = 'XXX' "); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), 0); + } + + public void testOuterJoinExtraNode() throws Exception { + Node n3 = testRootNode.addNode("node3", "test:SamplePage"); + n3.setProperty("n1prop1", "page1"); + testRootNode.getSession().save(); + + StringBuilder join = new StringBuilder(); + join.append(" Select * from [test:SamplePage] as page left outer join [test:SampleContent] as content on ISDESCENDANTNODE(content,page)"); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), 2); + } + + public void testOuterJoinExtraNodeWithCondition() throws Exception { + Node n3 = testRootNode.addNode("node3", "test:SamplePage"); + n3.setProperty("n1prop1", "page1"); + testRootNode.getSession().save(); + + StringBuilder join = new StringBuilder(); + join.append(" Select * from [test:SamplePage] as page left outer join [test:SampleContent] as content on ISDESCENDANTNODE(content,page) where page.n1prop1 = 'page1' and content.n2prop1 = 'XXX' "); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), 0); + } + + public void testOuterJoinDoubleJoinSplit() throws Exception { + Node n3 = testRootNode.addNode("node3", "test:SamplePage"); + n3.setProperty("n1prop1", "page2"); + + Node n4 = n3.addNode("node2", "test:SampleContent"); + n4.setProperty("n2prop1", "content1"); + testRootNode.getSession().save(); + + StringBuilder join = new StringBuilder(); + join.append("Select * from [test:SamplePage] as page left outer join [test:SampleContent] as content on ISDESCENDANTNODE(content,page) where (page.n1prop1 = 'page1' and content.n2prop1 = 'content1') or (page.n1prop1 = 'page2' and content.n2prop1 = 'content1') "); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), 2); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2PathEscapingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2PathEscapingTest.java new file mode 100644 index 00000000000..078ede2754f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2PathEscapingTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.qom.Column; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.Source; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Test case for path escaping + * + * Inspired by JCR-2939 + * + */ +public class SQL2PathEscapingTest extends AbstractQueryTest { + + private Node n1; + + @Override + protected void setUp() throws Exception { + super.setUp(); + n1 = testRootNode.addNode("a b"); + n1.addNode("x"); + n1.addNode("y"); + testRootNode.getSession().save(); + } + + @Override + protected void tearDown() throws Exception { + for (Node c : JcrUtils.getChildNodes(testRootNode)) { + testRootNode.getSession().removeItem(c.getPath()); + } + testRootNode.getSession().save(); + super.tearDown(); + } + + /** + * not escaping the spaced path will not get you anything + * + * @throws Exception + */ + public void testGetChildrenNoEscaping() throws Exception { + StringBuilder select = new StringBuilder(); + select.append("select * from [nt:base] AS selector where ISCHILDNODE(selector, [" + + n1.getPath() + "]) "); + checkResult(executeSQL2Query(select.toString()), 0); + } + + /** + * + * escaping the entire path should work + * + * @throws Exception + * + */ + public void testGetChildrenEscapedFull() throws Exception { + StringBuilder select = new StringBuilder(); + select.append("select * from [nt:base] AS selector where ISCHILDNODE(selector, ['" + + n1.getPath() + "']) "); + checkResult(executeSQL2Query(select.toString()), 2); + } + + /** + * escaping just the spaced path should work too + * + * @throws Exception + */ + public void testGetChildrenEscapedNode() throws Exception { + String path = n1.getParent().getPath() + "/'" + "a b" + "'"; + StringBuilder select = new StringBuilder(); + select.append("select * from [nt:base] AS selector where ISCHILDNODE(selector, [" + + path + "]) "); + checkResult(executeSQL2Query(select.toString()), 2); + } + + /** + * will build a query directly via the api using a spaced path + * + * @throws Exception + */ + public void testGetChildrenApiDirect() throws Exception { + QueryObjectModelFactory qomf = qm.getQOMFactory(); + Source source1 = qomf.selector(NodeType.NT_BASE, "selector"); + Column[] columns = new Column[] { qomf.column("selector", null, null) }; + Constraint constraint2 = qomf.childNode("selector", n1.getPath()); + QueryObjectModel qom = qomf.createQuery(source1, constraint2, null, + columns); + checkResult(qom.execute(), 2); + } + + /** + * the statement behind the api should be consistent, and return a similar + * query. in our case it should escape the paths that have spaces in them by + * enclosing them in single quotes + * + * @throws Exception + */ + public void testGetChildrenApiStatement() throws Exception { + + QueryObjectModelFactory qomf = qm.getQOMFactory(); + Source source1 = qomf.selector(NodeType.NT_BASE, "selector"); + Column[] columns = new Column[] { qomf.column("selector", null, null) }; + Constraint constraint2 = qomf.childNode("selector", n1.getPath()); + QueryObjectModel qom = qomf.createQuery(source1, constraint2, null, + columns); + checkResult(executeSQL2Query(qom.getStatement()), 2); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2QueryResultTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2QueryResultTest.java new file mode 100644 index 00000000000..b8e4f604267 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2QueryResultTest.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.core.query; + +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.commons.cnd.CndImporter; + +/** + * QueryResultTest tests various methods on the + * NodeIterator returned by a QueryResult. + */ +public class SQL2QueryResultTest extends AbstractQueryTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + NodeTypeManager manager = superuser.getWorkspace().getNodeTypeManager(); + if (!manager.hasNodeType("test:RTypeTest")) { + StringBuilder defs = new StringBuilder(); + defs.append("[test:RTypeTest]\n"); + defs.append(" - prop1\n"); + defs.append(" - prop2\n"); + Reader cndReader = new InputStreamReader(new ByteArrayInputStream( + defs.toString().getBytes())); + CndImporter.registerNodeTypes(cndReader, superuser); + } + + Node n1 = testRootNode.addNode("node1", "test:RTypeTest"); + n1.setProperty("prop1", "p1"); + n1.setProperty("prop2", "p2"); + + testRootNode.getSession().save(); + } + + /** + * Checks returned columns in a 'select *' vs 'select property' situation. + * + * see JCR-2954 + * + */ + public void testSQL2SelectColums() throws RepositoryException { + + executeAndCheckColumns("select * from [test:RTypeTest]", 3, + "test:RTypeTest.prop1", "test:RTypeTest.jcr:primaryType", + "test:RTypeTest.prop2"); + + executeAndCheckColumns("select * from [test:RTypeTest] as test", 3, + "test.jcr:primaryType", "test.prop1", "test.prop2"); + + executeAndCheckColumns("select test.* from [test:RTypeTest] as test", + 3, "test.jcr:primaryType", "test.prop1", "test.prop2"); + + executeAndCheckColumns("select prop1 from [test:RTypeTest]", 1, "prop1"); + + executeAndCheckColumns( + "select prop1 as newProp1 from [test:RTypeTest]", 1, "newProp1"); + + executeAndCheckColumns("select prop1 from [test:RTypeTest] as test", 1, + "prop1"); + + executeAndCheckColumns( + "select prop1 as newProp1 from [test:RTypeTest] as test", 1, + "newProp1"); + + executeAndCheckColumns( + "select test.prop1 from [test:RTypeTest] as test", 1, + "test.prop1"); + + executeAndCheckColumns( + "select test.prop1 as newProp1 from [test:RTypeTest] as test", + 1, "newProp1"); + + } + + private void executeAndCheckColumns(String sql2, int expected, + String... cols) throws RepositoryException { + QueryResult r = executeSQL2Query(sql2); + assertEquals( + "Got more columns than expected: " + + Arrays.toString(r.getColumnNames()), expected, + r.getColumnNames().length); + if (expected > 0) { + assertEquals(expected, cols.length); + List expectedCols = new ArrayList( + Arrays.asList(cols)); + expectedCols.removeAll(new ArrayList(Arrays.asList(r + .getColumnNames()))); + assertTrue("Got unexpected columns: " + expectedCols, + expectedCols.isEmpty()); + for (Row row : JcrUtils.getRows(r)) { + assertNotNull(row.getValues()); + assertEquals(expected, row.getValues().length); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2TooManyClausesTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2TooManyClausesTest.java new file mode 100644 index 00000000000..184841f1966 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQL2TooManyClausesTest.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.core.query; + +import static javax.jcr.query.Query.JCR_SQL2; + +import javax.jcr.Node; + +/** + * test for the usecases where there could be more than 1024 boolean clauses on + * a query and the QueryEngine (usually) has to implement some sort of batching + * + * @see {@link org.apache.lucene.search.BooleanQuery$TooManyClauses + * BooleanQuery#TooManyClauses} + */ +public class SQL2TooManyClausesTest extends AbstractQueryTest { + + private Node a; + + private Node boys; + + private Node girls; + + private final int nodes = 3300; + + @Override + protected void setUp() throws Exception { + super.setUp(); + a = testRootNode.addNode("teacher", "nt:unstructured"); + boys = a.addNode("boys", "nt:unstructured"); + girls = a.addNode("girls", "nt:unstructured"); + + for (int i = 0; i < nodes; i++) { + if (i % 2 == 0) { + Node n = getOrCreateParent(boys, i).addNode("student" + i, + "nt:unstructured"); + n.setProperty("sex", "m"); + } else { + Node n = getOrCreateParent(girls, i).addNode("student" + i, + "nt:unstructured"); + n.setProperty("sex", "f"); + } + if (i % 100 == 0) { + superuser.save(); + } + } + superuser.save(); + } + + private Node getOrCreateParent(Node node, int index) throws Exception { + String indexAsString = index + ""; + int len = indexAsString.length(); + Node n = node; + for (int i = 0; i < 3; i++) { + String name = "0"; + if (i < len) { + name = indexAsString.charAt(i) + ""; + } + n = n.addNode(name); + } + return n; + } + + /** + * JCR-3108 + */ + public void testISDESCENDANTNODE() throws Exception { + StringBuilder join = new StringBuilder(); + join.append("SELECT * FROM [nt:unstructured] as students WHERE "); + join.append(" (ISDESCENDANTNODE([" + boys.getPath() + + "]) OR ISDESCENDANTNODE([" + girls.getPath() + "])) "); + join.append(" AND ( CONTAINS(students.sex,'m') OR CONTAINS(students.sex,'f') )"); + checkResult(qm.createQuery(join.toString(), JCR_SQL2).execute(), nodes); + + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQLTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQLTest.java new file mode 100644 index 00000000000..048c66129ba --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SQLTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +/** + */ +public class SQLTest extends AbstractQueryTest { + + + public void testSimpleQuery1() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("bla", new String[]{"bla"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE bla='bla' " + + "AND jcr:path LIKE '" + testRoot + "/%'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + public void testFulltextSimple() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE contains(., 'fox') " + + "AND jcr:path LIKE '" + testRoot + "/%'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + /** + * I'm not sure what this test is supposed to verify. It only works because + * the sql parser is not strict enough and the column values are never + * actually checked + */ + public void _testFulltextComplex() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("mytext", new String[]{"the quick brown fox jumps over the lazy dog."}); + + testRootNode.save(); + + String sql = "SELECT foo.mytext, bla.foo FROM nt:unstructured WHERE " + + "contains(., 'fox') AND NOT contains(., 'bla') " + + "AND jcr:path LIKE '" + testRoot + "/%'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + +} diff --git a/src/test/org/apache/jackrabbit/core/search/SelectClauseTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SelectClauseTest.java similarity index 89% rename from src/test/org/apache/jackrabbit/core/search/SelectClauseTest.java rename to jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SelectClauseTest.java index 3094d660632..5a5ce18be4b 100644 --- a/src/test/org/apache/jackrabbit/core/search/SelectClauseTest.java +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SelectClauseTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.core.search; +package org.apache.jackrabbit.core.query; import javax.jcr.Node; import javax.jcr.RepositoryException; diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ShareableNodeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ShareableNodeTest.java new file mode 100644 index 00000000000..af4cf12fc72 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/ShareableNodeTest.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.core.query; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; + +/** + * ShareableNodeTest performs query tests with shareable nodes. + */ +public class ShareableNodeTest extends AbstractQueryTest { + + public void testPathConstraint() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + Node s = n1.addNode(nodeName3); + s.setProperty(propertyName1, "value"); + s.addMixin(mixShareable); + Node n4 = s.addNode(nodeName4); + n4.setProperty(propertyName2, "value"); + testRootNode.save(); + + Workspace wsp = superuser.getWorkspace(); + wsp.clone(wsp.getName(), s.getPath(), n2.getPath() + "/" + s.getName(), false); + + String stmt = testPath + "/" + nodeName1 + "/*[@" + propertyName1 + "='value']"; + NodeIterator nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "/" + nodeName2 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "//*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "//*[@" + propertyName2 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", n4.isSame(nodes.nextNode())); + + // remove a node from the shared set + s.removeShare(); + testRootNode.save(); + + s = n2.getNode(nodeName3); + + stmt = testPath + "/" + nodeName1 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "/" + nodeName2 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "//*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + // remove remaining node from the shared set + s.removeShare(); + testRootNode.save(); + + stmt = testPath + "/" + nodeName1 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "/" + nodeName2 + "/*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "//*[@" + propertyName1 + "='value']"; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + } + + public void testName() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + Node s = n1.addNode(nodeName3); + s.addMixin(mixShareable); + testRootNode.save(); + + Workspace wsp = superuser.getWorkspace(); + wsp.clone(wsp.getName(), s.getPath(), n2.getPath() + "/" + nodeName4, false); + + String stmt = testPath + "//" + nodeName3; + NodeIterator nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + stmt = testPath + "//" + nodeName4; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + // remove a node from the shared set + s.removeShare(); + testRootNode.save(); + + s = n2.getNode(nodeName4); + + stmt = testPath + "//" + nodeName3; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "//" + nodeName4; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 1, nodes.getSize()); + // spec does not say which path must be returned -> use isSame() + assertTrue("wrong node", s.isSame(nodes.nextNode())); + + // remove remaining node from the shared set + s.removeShare(); + testRootNode.save(); + + stmt = testPath + "//" + nodeName3; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + + stmt = testPath + "//" + nodeName4; + nodes = executeQuery(stmt).getNodes(); + assertEquals("wrong result size", 0, nodes.getSize()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SimilarQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SimilarQueryTest.java new file mode 100644 index 00000000000..8ec1758c88f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SimilarQueryTest.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.core.query; + +import javax.jcr.RepositoryException; + +/** + * SimilarQueryTest checks rep:similar function. + */ +public class SimilarQueryTest extends AbstractQueryTest { + + public void testSimilar() throws RepositoryException { + executeQuery("//*[rep:similar(., '" + testRootNode.getPath() + "')]"); + executeQuery("//*[rep:similar(node, '" + testRootNode.getPath() + "')]"); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SimpleQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SimpleQueryTest.java new file mode 100644 index 00000000000..3855c96e391 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SimpleQueryTest.java @@ -0,0 +1,562 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import javax.jcr.Node; +import javax.jcr.Value; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +import java.math.BigDecimal; +import java.util.Calendar; + +/** + * Performs various query test cases. + */ +public class SimpleQueryTest extends AbstractQueryTest { + + + public void testSimpleQuerySQL1() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("bla", new String[]{"bla"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base" + + " WHERE jcr:path LIKE '" + testRoot + "/foo'" + + " AND bla = 'bla'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + } + + public void testSimpleQuerySQL2() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("bla", new String[]{"bla"}); + Node bla = testRootNode.addNode("bla"); + bla.setProperty("bla", new String[]{"bla"}); + + superuser.getRootNode().save(); + + String sql = "SELECT * FROM nt:file" + + " WHERE jcr:path LIKE '" + testRoot + "/%'" + + " AND bla = 'bla'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 0); + } + + public void testSimpleQuerySQL3() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("bla", new String[]{"bla"}); + Node bla = testRootNode.addNode("bla"); + bla.setProperty("bla", new String[]{"bla"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured" + + " WHERE jcr:path LIKE '" + testRoot + "/%'" + + " AND bla = 'bla'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 2); + } + + public void testSimpleQuerySQL4() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("bla", new String[]{"bla"}); + Node bla = testRootNode.addNode("bla"); + bla.setProperty("bla", new String[]{"bla"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE jcr:path LIKE '" + testRoot + "/%'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 2); + } + + public void testDateField1() throws Exception { + Node n = testRootNode.addNode("marcel"); + Calendar marcel = Calendar.getInstance(); + marcel.set(1976, 4, 20, 15, 40); + n.setProperty("birth", new Value[]{superuser.getValueFactory().createValue(marcel)}); + + n = testRootNode.addNode("vanessa"); + Calendar vanessa = Calendar.getInstance(); + vanessa.set(1975, 4, 10, 13, 30); + n.setProperty("birth", new Value[]{superuser.getValueFactory().createValue(vanessa)}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND birth > TIMESTAMP '1976-01-01T00:00:00.000+01:00'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND birth > TIMESTAMP '1975-01-01T00:00:00.000+01:00'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 2); + } + + public void testDoubleField() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("value", new Value[]{superuser.getValueFactory().createValue(1.9928375d)}); + n = testRootNode.addNode("node2"); + n.setProperty("value", new Value[]{superuser.getValueFactory().createValue(0.0d)}); + n = testRootNode.addNode("node3"); + n.setProperty("value", new Value[]{superuser.getValueFactory().createValue(-1.42982475d)}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value > 0.1e-0"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value > -0.1"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 2); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value > -1.5"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 3); + } + + public void testBigDecimalField() throws Exception { + Node n1 = testRootNode.addNode("node1"); + n1.setProperty( + "value", + superuser.getValueFactory().createValue( + new BigDecimal(1.9928375d))); + Node n2 = testRootNode.addNode("node2"); + n2.setProperty("value", + superuser.getValueFactory().createValue(new BigDecimal(0.0d))); + Node n3 = testRootNode.addNode("node3"); + n3.setProperty( + "value", + superuser.getValueFactory().createValue( + new BigDecimal(-1.42982475d))); + testRootNode.getSession().save(); + + String baseSql2 = "SELECT * FROM [nt:base] WHERE ISCHILDNODE([" + + testRoot + "]) AND value"; + checkResult(qm.createQuery(baseSql2 + " > 0.1", Query.JCR_SQL2) + .execute(), new Node[] { n1 }); + checkResult( + qm.createQuery(baseSql2 + " = CAST('0' AS DECIMAL)", + Query.JCR_SQL2).execute(), new Node[] { n2 }); + checkResult( + qm.createQuery(baseSql2 + " = CAST(0 AS DECIMAL)", + Query.JCR_SQL2).execute(), new Node[] { n2 }); + checkResult(qm.createQuery(baseSql2 + " > -0.1", Query.JCR_SQL2) + .execute(), new Node[] { n1, n2 }); + checkResult(qm.createQuery(baseSql2 + " > -1.5", Query.JCR_SQL2) + .execute(), new Node[] { n1, n2, n3 }); + } + + public void testLongField() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("value", new Value[]{superuser.getValueFactory().createValue(1)}); + n = testRootNode.addNode("node2"); + n.setProperty("value", new Value[]{superuser.getValueFactory().createValue(0)}); + n = testRootNode.addNode("node3"); + n.setProperty("value", new Value[]{superuser.getValueFactory().createValue(-1)}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value > 0"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value > -1"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 2); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value > -2"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 3); + } + + public void testLikePattern() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("value", new String[]{"king"}); + n = testRootNode.addNode("node2"); + n.setProperty("value", new String[]{"ping"}); + n = testRootNode.addNode("node3"); + n.setProperty("value", new String[]{"ching"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'ping'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE '_ing'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 2); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE '%ing'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 3); + } + + public void testLikePatternBetween() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("value", new String[]{"ping"}); + n = testRootNode.addNode("node2"); + n.setProperty("value", new String[]{"pong"}); + n = testRootNode.addNode("node3"); + n.setProperty("value", new String[]{"puung"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'ping'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'p_ng'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 2); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'p%ng'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 3); + } + + public void testLikePatternEnd() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("value", new String[]{"bli"}); + n = testRootNode.addNode("node2"); + n.setProperty("value", new String[]{"bla"}); + n = testRootNode.addNode("node3"); + n.setProperty("value", new String[]{"blub"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'bli'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'bl_'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 2); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'bl%'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 3); + } + + public void testLikePatternEscaped() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("value", new String[]{"foo\\_bar"}); + n = testRootNode.addNode("node2"); + n.setProperty("value", new String[]{"foobar"}); + n = testRootNode.addNode("node3"); + n.setProperty("value", new String[]{"foo_bar"}); + n = testRootNode.addNode("node4"); + n.setProperty("value", new String[]{"foolbar"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'foo\\_bar' ESCAPE '\\'"; // matches node3 + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'foo_bar'"; // matches node3 and node4 + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 2); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'foo%bar'"; // matches all nodes + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 4); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'foo\\\\\\_bar' ESCAPE '\\'"; // matches node1 + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'foo\\_bar'"; // matches node1 + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 1); + } + + public void testLikeWithLineTerminator() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("value", new String[]{"foo\nbar"}); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'foo%bar'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value LIKE 'foo_bar'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 1); + } + + public void testNotEqual() throws Exception { + Node n = testRootNode.addNode("node1"); + n.setProperty("value", new String[]{"foo"}); + n = testRootNode.addNode("node2"); + n.setProperty("value", new String[]{"bar"}); + n = testRootNode.addNode("node3"); + n.setProperty("value", new String[]{"foobar"}); + + testRootNode.save(); + + String jcrql = "SELECT * FROM nt:base WHERE jcr:path LIKE '" + testRoot + "/%' AND value <> 'bar'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(jcrql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 2); + + } + + public void testIsNull() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("mytext", "the quick brown fox jumps over the lazy dog."); + Node bar = testRootNode.addNode("bar"); + bar.setProperty("text", "the quick brown fox jumps over the lazy dog."); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE mytext is null and jcr:path LIKE '" + + testRoot + "/%'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + String xpath = "/" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and fn:not(@mytext)]"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 1); + + xpath = "/jcr:root" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and fn:not(@mytext)]"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 1); + } + + public void testIsNotNull() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("mytext", "the quick brown fox jumps over the lazy dog."); + Node bar = testRootNode.addNode("bar"); + bar.setProperty("text", "the quick brown fox jumps over the lazy dog."); + // documents which field name is not exactly "mytext" should not match (JCR-1051) + bar.setProperty("mytextwhichstartswithmytext", "the quick brown fox jumps over the lazy dog."); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE mytext is not null"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + String xpath = "//*[@jcr:primaryType='nt:unstructured' and @mytext]"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 1); + } + + public void testNegativeNumber() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("number", -10); + Node bar = testRootNode.addNode("bar"); + bar.setProperty("number", -20); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE number = -10"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + String xpath = "//*[@jcr:primaryType='nt:unstructured' and @number = -10]"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 1); + + sql = "SELECT * FROM nt:unstructured WHERE number <= -10"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, 2); + + xpath = "//*[@jcr:primaryType='nt:unstructured' and @number <= -10]"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 2); + } + + public void testQuotes() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("foo", "bar'bar"); + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE foo = 'bar''bar'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, 1); + + String xpath = "//*[@jcr:primaryType='nt:unstructured' and @foo ='bar''bar']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 1); + + xpath = "//*[@jcr:primaryType='nt:unstructured' and jcr:like(@foo,'%ar''ba%')]"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 1); + + xpath = "//*[@jcr:primaryType='nt:unstructured' and jcr:like(@foo,\"%ar'ba%\")]"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, 1); + } + + public void testGeneralComparison() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("text", new String[]{"foo", "bar"}); // mvp + Node bar = testRootNode.addNode("bar"); + bar.setProperty("text", new String[]{"foo"}); // mvp with one value + Node bla = testRootNode.addNode("bla"); + bla.setProperty("text", "foo"); // svp + Node blu = testRootNode.addNode("blu"); + blu.setProperty("text", "bar"); // svp + + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE 'foo' IN text " + + "and jcr:path LIKE '" + testRoot + "/%'"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, new Node[]{foo, bar, bla}); + + String xpath = "/" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and @text = 'foo']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{foo, bar, bla}); + + xpath = "/jcr:root" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and @text = 'foo']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{foo, bar, bla}); + + //---------------------------------------------------------------------- + + // the text = 'foo' is now also a general comparison + sql = "SELECT * FROM nt:unstructured WHERE text = 'foo' " + + "and jcr:path LIKE '" + testRoot + "/%'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, new Node[]{foo, bar, bla}); + + xpath = "/" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and @text eq 'foo']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{bar, bla}); + + xpath = "/jcr:root" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and @text eq 'foo']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{bar, bla}); + + //---------------------------------------------------------------------- + + sql = "SELECT * FROM nt:unstructured WHERE 'bar' NOT IN text " + + "and jcr:path LIKE '" + testRoot + "/%'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, new Node[]{foo, bar, bla}); + + xpath = "/" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and @text != 'bar']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{foo, bar, bla}); + + xpath = "/jcr:root" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and @text != 'bar']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{foo, bar, bla}); + + //---------------------------------------------------------------------- + + sql = "SELECT * FROM nt:unstructured WHERE 'foo' NOT IN text " + + "and jcr:path LIKE '" + testRoot + "/%'"; + q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + result = q.execute(); + checkResult(result, new Node[]{foo, blu}); + + xpath = "/" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and @text != 'foo']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{foo, blu}); + + xpath = "/jcr:root" + testRoot + "/*[@jcr:primaryType='nt:unstructured' and @text != 'foo']"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{foo, blu}); + } + + public void testLogicalExpression() throws Exception { + Node foo = testRootNode.addNode("foo"); + foo.setProperty("a", 1); + foo.setProperty("b", 2); + foo.setProperty("c", 3); + Node bar = testRootNode.addNode("bar"); + bar.setProperty("a", 0); + bar.setProperty("b", 2); + bar.setProperty("c", 0); + Node bla = testRootNode.addNode("bla"); + bla.setProperty("a", 1); + bla.setProperty("b", 0); + bla.setProperty("c", 3); + testRootNode.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE a=1 and b=2 or c=3"; + Query q = superuser.getWorkspace().getQueryManager().createQuery(sql, Query.SQL); + QueryResult result = q.execute(); + checkResult(result, new Node[]{foo, bla}); + + String xpath = "//*[@a=1 and @b=2 or @c=3] "; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, Query.XPATH); + result = q.execute(); + checkResult(result, new Node[]{foo, bla}); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SkipDeletedNodesTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SkipDeletedNodesTest.java new file mode 100644 index 00000000000..b59866229ee --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SkipDeletedNodesTest.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.InvalidItemStateException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.query.QueryManager; + +/** + * Tests if the NodeIterator returned by {@link javax.jcr.query.QueryResult#getNodes()} + * skips Nodes that have been deleted in the meantime. + */ +public class SkipDeletedNodesTest extends AbstractQueryTest { + + private Session s2; + + private QueryManager qm; + + protected void setUp() throws Exception { + super.setUp(); + s2 = getHelper().getSuperuserSession(); + qm = s2.getWorkspace().getQueryManager(); + } + + protected void tearDown() throws Exception { + try { + if (s2 != null) { + s2.logout(); + s2 = null; + } + } finally { + qm = null; + super.tearDown(); + } + } + + /** + * Executes a query with one session and removes a node from that query + * result with another session. + */ + public void testRemoveFirstNode() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + testRootNode.addNode("node2"); + testRootNode.addNode("node3"); + testRootNode.save(); + + // query the workspace for all three nodes + String stmt = testPath + "/*"; + QueryResult res = qm.createQuery(stmt, Query.XPATH).execute(); + + // now remove the first node + n1.remove(); + testRootNode.save(); + + // iterate over nodes + log.println("Result nodes:"); + int count = 0; + for (NodeIterator it = res.getNodes(); it.hasNext(); ) { + assertEquals("Wrong value for getPosition().", count++, it.getPosition()); + try { + log.println(it.nextNode().getPath()); + } catch (InvalidItemStateException e) { + // this is allowed + log.println("Invalid: "); + } + } + } + + /** + * Executes a query with one session and removes a node from that query + * result with another session. + */ + public void testRemoveSomeNode() throws RepositoryException { + testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + testRootNode.addNode("node3"); + testRootNode.save(); + + // query the workspace for all three nodes + String stmt = testPath + "/*"; + QueryResult res = qm.createQuery(stmt, Query.XPATH).execute(); + + // now remove the second node + n2.remove(); + testRootNode.save(); + + // iterate over nodes + int count = 0; + log.println("Result nodes:"); + for (NodeIterator it = res.getNodes(); it.hasNext(); ) { + assertEquals("Wrong value for getPosition().", count++, it.getPosition()); + try { + log.println(it.nextNode().getPath()); + } catch (InvalidItemStateException e) { + // this is allowed + log.println("Invalid: "); + } + } + } + + /** + * Executes a query with one session and removes a node from that query + * result with another session. + */ + public void testRemoveLastNode() throws RepositoryException { + testRootNode.addNode("node1"); + testRootNode.addNode("node2"); + Node n3 = testRootNode.addNode("node3"); + testRootNode.save(); + + // query the workspace for all three nodes + String stmt = testPath + "/*"; + QueryResult res = qm.createQuery(stmt, Query.XPATH).execute(); + + // now remove the last node + n3.remove(); + testRootNode.save(); + + // iterate over nodes + int count = 0; + log.println("Result nodes:"); + for (NodeIterator it = res.getNodes(); it.hasNext(); ) { + assertEquals("Wrong value for getPosition().", count++, it.getPosition()); + try { + log.println(it.nextNode().getPath()); + } catch (InvalidItemStateException e) { + // this is allowed + log.println("Invalid: "); + } + } + } + + /** + * Executes a query with one session and removes a node from that query + * result with another session. + *

    This test is different from the other tests that it removes the + * node after another session has called hasNext() to retrieve the node + * that gets deleted. + */ + public void testRemoveFirstNodeAfterHasNext() throws RepositoryException { + Node n1 = testRootNode.addNode("node1"); + testRootNode.addNode("node2"); + testRootNode.addNode("node3"); + testRootNode.save(); + + // query the workspace for all three nodes + String stmt = testPath + "/*"; + QueryResult res = qm.createQuery(stmt, Query.XPATH).execute(); + + NodeIterator it = res.getNodes(); + it.hasNext(); + + // now remove the first node + n1.remove(); + testRootNode.save(); + + // iterate over nodes + int count = 0; + log.println("Result nodes:"); + while (it.hasNext()) { + assertEquals("Wrong value for getPosition().", count++, it.getPosition()); + try { + log.println(it.nextNode().getPath()); + } catch (InvalidItemStateException e) { + // this is allowed + log.println("Invalid: "); + } + } + } + + /** + * Executes a query with one session and removes a node from that query + * result with another session. + *

    This test is different from the other tests that it removes the + * node after another session has called hasNext() to retrieve the node + * that gets deleted. + */ + public void testRemoveSomeNodeAfterHasNext() throws RepositoryException { + testRootNode.addNode("node1"); + Node n2 = testRootNode.addNode("node2"); + testRootNode.addNode("node3"); + testRootNode.save(); + + // query the workspace for all three nodes + String stmt = testPath + "/*"; + QueryResult res = qm.createQuery(stmt, Query.XPATH).execute(); + + NodeIterator it = res.getNodes(); + it.hasNext(); + + // now remove the second node + n2.remove(); + testRootNode.save(); + + // iterate over nodes + int count = 0; + log.println("Result nodes:"); + while (it.hasNext()) { + assertEquals("Wrong value for getPosition().", count++, it.getPosition()); + try { + log.println(it.nextNode().getPath()); + } catch (InvalidItemStateException e) { + // this is allowed + log.println("Invalid: "); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SkipDeniedNodesTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SkipDeniedNodesTest.java new file mode 100644 index 00000000000..aa977b522b7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/SkipDeniedNodesTest.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.core.query; + +import java.security.Principal; + +import javax.jcr.AccessDeniedException; +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; +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.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; + +/** + * SkipDeniedNodesTest checks if nodes are correctly skipped + * when a session does not have access to it. + */ +public class SkipDeniedNodesTest extends AbstractAccessControlTest { + + private Session anonymous; + + private Node n1; + private Node n2; + private Node n3; + private Node n4; + + protected void setUp() throws Exception { + super.setUp(); + anonymous = getHelper().getReadOnlySession(); + + n1 = testRootNode.addNode(nodeName1); + n1.setProperty(propertyName1, "a"); + n2 = testRootNode.addNode(nodeName2); + n2.setProperty(propertyName1, "b"); + n3 = testRootNode.addNode(nodeName3); + n3.setProperty(propertyName1, "c"); + n4 = testRootNode.addNode(nodeName4); + n4.setProperty(propertyName1, "d"); + superuser.save(); + + JackrabbitAccessControlList acl = getACL(n2.getPath()); + acl.addEntry(getPrincipal(anonymous), privilegesFromName(Privilege.JCR_READ) , false); + + acMgr.setPolicy(n2.getPath(), acl); + superuser.save(); + } + + protected void tearDown() throws Exception { + anonymous.logout(); + super.tearDown(); + } + + public void testSkipNodes() throws RepositoryException { + Node testNode = (Node) anonymous.getItem(testRoot); + checkSequence(testNode.getNodes(), + new String[]{n1.getPath(), n3.getPath(), n4.getPath()}); + + QueryManager qm = anonymous.getWorkspace().getQueryManager(); + String ntName = n1.getPrimaryNodeType().getName(); + String stmt = testPath + "/element(*, " + ntName + ") order by @" + propertyName1; + QueryImpl query = (QueryImpl) qm.createQuery(stmt, Query.XPATH); + query.setOffset(0); + query.setLimit(2); + checkSequence(query.execute().getNodes(), new String[]{n1.getPath(), n3.getPath()}); + + query = (QueryImpl) qm.createQuery(stmt, Query.XPATH); + query.setOffset(2); + query.setLimit(2); + checkSequence(query.execute().getNodes(), new String[]{n4.getPath()}); + } + + private void checkSequence(NodeIterator nodes, String[] paths) + throws RepositoryException { + for (int i = 0; i < paths.length; i++) { + assertTrue("No more nodes, expected: " + paths[i], nodes.hasNext()); + assertEquals("Wrong sequence", paths[i], nodes.nextNode().getPath()); + } + assertFalse("No more nodes expected", nodes.hasNext()); + } + + private static Principal getPrincipal(Session session) throws NotExecutableException { + UserManager userMgr; + if (!(session instanceof JackrabbitSession)) { + throw new NotExecutableException(); + } + try { + userMgr = ((JackrabbitSession) session).getUserManager(); + User user = (User) userMgr.getAuthorizable(session.getUserID()); + if (user == null) { + throw new NotExecutableException("cannot get user for userID : " + session.getUserID()); + } + return user.getPrincipal(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + } + + private JackrabbitAccessControlList getACL(String path) throws RepositoryException, AccessDeniedException, NotExecutableException { + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + while (it.hasNext()) { + AccessControlPolicy acp = it.nextAccessControlPolicy(); + if (acp instanceof JackrabbitAccessControlList) { + return (JackrabbitAccessControlList) acp; + } + } + throw new NotExecutableException("No JackrabbitAccessControlList found at " + path + " ."); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TestAll.java new file mode 100644 index 00000000000..aa1b6af5aa3 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TestAll.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.core.query; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +/** + * Test suite that includes all testcases for the Search module. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new ConcurrentTestSuite("Search tests"); + + suite.addTestSuite(SimpleQueryTest.class); + suite.addTestSuite(FulltextQueryTest.class); + suite.addTestSuite(SelectClauseTest.class); + suite.addTestSuite(SQLTest.class); + suite.addTestSuite(JoinTest.class); + suite.addTestSuite(OrderByTest.class); + suite.addTestSuite(XPathAxisTest.class); + suite.addTestSuite(SkipDeletedNodesTest.class); + suite.addTestSuite(SkipDeniedNodesTest.class); + suite.addTestSuite(MixinTest.class); + suite.addTestSuite(DerefTest.class); + suite.addTestSuite(VersionStoreQueryTest.class); + suite.addTestSuite(UpperLowerCaseQueryTest.class); + suite.addTestSuite(ChildAxisQueryTest.class); + suite.addTestSuite(QueryResultTest.class); + suite.addTestSuite(FnNameQueryTest.class); + suite.addTestSuite(PathQueryNodeTest.class); + suite.addTestSuite(ExcerptTest.class); + suite.addTestSuite(ShareableNodeTest.class); + suite.addTestSuite(ParentNodeTest.class); + suite.addTestSuite(SimilarQueryTest.class); + suite.addTestSuite(FulltextSQL2QueryTest.class); + suite.addTestSuite(LimitAndOffsetTest.class); + suite.addTestSuite(SQL2NodeLocalNameTest.class); + suite.addTestSuite(SQL2OuterJoinTest.class); + suite.addTestSuite(SQL2PathEscapingTest.class); + suite.addTestSuite(SQL2QueryResultTest.class); + suite.addTestSuite(LimitedAccessQueryTest.class); + suite.addTestSuite(SQL2OffsetLimitTest.class); + suite.addTestSuite(SQL2OrderByTest.class); + suite.addTestSuite(DescendantSelfAxisTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TextExtractorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TextExtractorTest.java new file mode 100644 index 00000000000..cdfec6637ab --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/TextExtractorTest.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.core.query; + +import javax.jcr.Node; +import java.io.File; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.BufferedInputStream; +import java.net.URLConnection; +import java.util.Calendar; + +/** + * TextExtractorTest implements a file / folder import from the + * local file system. + */ +public class TextExtractorTest extends AbstractQueryTest { + + private static final String TEST_FOLDER = "test-data"; + + private int fileCount = 0; + + public void testImport() throws Exception { + File sourceFolder = new File(TEST_FOLDER); + // only run if there is test data + if (!sourceFolder.exists()) { + return; + } + long time = System.currentTimeMillis(); + addContents(sourceFolder, + testRootNode.addNode(sourceFolder.getName(), "nt:folder")); + superuser.save(); + time = System.currentTimeMillis() - time; + log.println("Imported " + fileCount + " files in " + time + " ms."); + } + + /** + * Recursively adds files and folders to the workspace. + */ + private void addContents(File folder, Node n) throws Exception { + String[] names = folder.list(); + for (int i = 0; i < names.length; i++) { + File f = new File(folder, names[i]); + if (f.canRead()) { + if (f.isDirectory()) { + log.println("Added folder: " + f.getAbsolutePath()); + addContents(f, n.addNode(names[i], "nt:folder")); + } else { + addFile(n, f); + log.println("Added file: " + f.getAbsolutePath()); + // save after 100 files + if (++fileCount % 100 == 0) { + n.getSession().save(); + } + } + } + } + } + + /** + * Repeatedly update a file in the workspace and force text extraction + * on it. + */ + public void testRepeatedUpdate() throws Exception { + File testFile = new File("test.pdf"); + if (!testFile.exists()) { + return; + } + Node resource = addFile(testRootNode, testFile).getNode("jcr:content"); + superuser.save(); + for (int i = 0; i < 10; i++) { + // kick start text extractor + executeXPathQuery(testPath, new Node[]{testRootNode}); + InputStream in = new BufferedInputStream(new FileInputStream(testFile)); + try { + resource.setProperty("jcr:data", in); + } finally { + in.close(); + } + log.println("updating resource..."); + superuser.save(); + } + } + + private static Node addFile(Node folder, File f) throws Exception { + String mimeType = URLConnection.guessContentTypeFromName(f.getName()); + if (mimeType == null) { + mimeType = "application/octet-stream"; + } + Node file = folder.addNode(f.getName(), "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + InputStream in = new BufferedInputStream(new FileInputStream(f)); + try { + resource.setProperty("jcr:data", in); + resource.setProperty("jcr:mimeType", mimeType); + Calendar lastModified = Calendar.getInstance(); + lastModified.setTimeInMillis(f.lastModified()); + resource.setProperty("jcr:lastModified", lastModified); + } finally { + in.close(); + } + return file; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/UpperLowerCaseQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/UpperLowerCaseQueryTest.java new file mode 100644 index 00000000000..6b66ff77df8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/UpperLowerCaseQueryTest.java @@ -0,0 +1,321 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query; + +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.QueryResult; + +import org.apache.jackrabbit.commons.query.qom.Operator; + +/** + * UpperLowerCaseQueryTest tests the functions fn:lower-case() and + * fn:upper-case() in XPath, LOWER() and UPPER() in SQL and UpperCase and + * LowerCase in JQOM. + */ +public class UpperLowerCaseQueryTest extends AbstractQueryTest { + + public void testEqualsGeneralComparison() throws RepositoryException { + check(new String[]{"foo", "Foo", "fOO", "FOO", "fooBar", "fo", "fooo"}, + Operator.EQ, + "foo", + new boolean[]{true, true, true, true, false, false, false}); + check(new String[]{"foo"}, Operator.EQ, "", new boolean[]{false}); + check(new String[]{""}, Operator.EQ, "", new boolean[]{true}); + } + + public void testGreaterThanGeneralComparison() throws RepositoryException { + // check edges + check(new String[]{"foo", "FOO", "FoO", "fOo", "FON", "fon", "fo", "FO"}, + Operator.GT, + "foo", + new boolean[]{false, false, false, false, false, false, false, false}); + check(new String[]{"foo ", "FOOa", "FoOO", "fOo1", "FOp", "foP", "fp", "g", "G"}, + Operator.GT, + "foo", + new boolean[]{true, true, true, true, true, true, true, true, true}); + // check combinations + check(new String[]{"foo", "fooo", "FooO", "fo", "FON", "fon"}, + Operator.GT, + "foo", + new boolean[]{false, true, true, false, false, false}); + } + + public void testLessThanGeneralComparison() throws RepositoryException { + // check edges + check(new String[]{"foo", "FOO", "FoO", "fOo", "foOo", "foo ", "fooa", "fop"}, + Operator.LT, + "foo", + new boolean[]{false, false, false, false, false, false, false, false}); + check(new String[]{"fo", "FOn", "FoN", "fO", "FO1", "fn", "fN", "E", "e"}, + Operator.LT, + "foo", + new boolean[]{true, true, true, true, true, true, true, true, true}); + // check combinations + check(new String[]{"foo", "fooo", "FooO", "fo", "FON", "fon"}, + Operator.LT, + "foo", + new boolean[]{false, false, false, true, true, true}); + } + + public void testGreaterEqualsGeneralComparison() throws RepositoryException { + // check edges + check(new String[]{"fo", "FO", "Fon", "fONo", "FON", "fO", "fo", "FO"}, + Operator.GE, + "foo", + new boolean[]{false, false, false, false, false, false, false, false}); + check(new String[]{"foo", "FoO", "FoOO", "fOo1", "FOp", "foP", "fp", "g", "G"}, + Operator.GE, + "foo", + new boolean[]{true, true, true, true, true, true, true, true, true}); + // check combinations + check(new String[]{"foo", "fooo", "FOo", "fo", "FON", "fon"}, + Operator.GE, + "foo", + new boolean[]{true, true, true, false, false, false}); + } + + public void testLessEqualsGeneralComparison() throws RepositoryException { + // check edges + check(new String[]{"fooo", "FOoo", "Fop", "fOpo", "FOP", "fOo ", "fp", "G"}, + Operator.LE, + "foo", + new boolean[]{false, false, false, false, false, false, false, false}); + check(new String[]{"foo", "FoO", "Foo", "fOn", "FO", "fo", "f", "E", "e"}, + Operator.LE, + "foo", + new boolean[]{true, true, true, true, true, true, true, true, true}); + // check combinations + check(new String[]{"foo", "fo", "FOo", "fop", "FOP", "fooo"}, + Operator.LE, + "foo", + new boolean[]{true, true, true, false, false, false}); + } + + public void testNotEqualsGeneralComparison() throws RepositoryException { + // check edges + check(new String[]{"fooo", "FOoo", "Fop", "fOpo", "FOP", "fOo ", "fp", "G", ""}, + Operator.NE, + "foo", + new boolean[]{true, true, true, true, true, true, true, true, true}); + check(new String[]{"foo", "FoO", "Foo", "foO", "FOO"}, + Operator.NE, + "foo", + new boolean[]{false, false, false, false, false}); + // check combinations + check(new String[]{"foo", "fo", "FOo", "fop", "FOP", "fooo"}, + Operator.NE, + "foo", + new boolean[]{false, true, false, true, true, true}); + } + + public void testLikeComparison() throws RepositoryException { + check(new String[]{"foo", "Foo", "fOO", "FO "}, + Operator.LIKE, + "fo_", + new boolean[]{true, true, true, true}); + check(new String[]{"foo", "Foo", "fOO", "FOO"}, + Operator.LIKE, + "f_o", + new boolean[]{true, true, true, true}); + check(new String[]{"foo", "Foo", "fOO", " OO"}, + Operator.LIKE, + "_oo", + new boolean[]{true, true, true, true}); + check(new String[]{"foo", "Foa", "fOO", "FO", "foRm", "fPo", "fno", "FPo", "Fno"}, + Operator.LIKE, + "fo%", + new boolean[]{true, true, true, true, true, false, false, false, false}); + } + + public void testLikeComparisonRandom() throws RepositoryException { + String abcd = "abcd"; + Random random = new Random(); + for (int i = 0; i < 50; i++) { + String pattern = ""; + pattern += getRandomChar(abcd, random); + pattern += getRandomChar(abcd, random); + + // create 10 random values with 4 characters + String[] values = new String[10]; + boolean[] matches = new boolean[10]; + for (int n = 0; n < 10; n++) { + // at least the first character always matches + String value = String.valueOf(pattern.charAt(0)); + for (int r = 1; r < 4; r++) { + char c = getRandomChar(abcd, random); + if (random.nextBoolean()) { + c = Character.toUpperCase(c); + } + value += c; + } + matches[n] = value.toLowerCase().startsWith(pattern); + values[n] = value; + } + pattern += "%"; + check(values, Operator.LIKE, pattern, matches); + } + } + + public void testRangeWithEmptyString() throws RepositoryException { + check(new String[]{" ", "a", "A", "1", "3", "!", "@"}, + Operator.GT, + "", + new boolean[]{true, true, true, true, true, true, true}); + check(new String[]{"", "a", "A", "1", "3", "!", "@"}, + Operator.GE, + "", + new boolean[]{true, true, true, true, true, true, true}); + check(new String[]{"", "a", "A", "1", "3", "!", "@"}, + Operator.LT, + "", + new boolean[]{false, false, false, false, false, false, false}); + check(new String[]{"", "a", "A", "1", "3", "!", "@"}, + Operator.LE, + "", + new boolean[]{true, false, false, false, false, false, false}); + } + + public void testInvalidQuery() throws RepositoryException { + try { + executeXPathQuery("//*[fn:lower-case(@foo) = 123]", new Node[]{}); + fail("must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // correct + } + + try { + executeSQLQuery("select * from nt:base where LOWER(foo) = 123", new Node[]{}); + fail("must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // correct + } + } + + public void testWrongCaseNeverMatches() throws RepositoryException { + Node n = testRootNode.addNode("node"); + n.setProperty("foo", "Bar"); + testRootNode.save(); + executeXPathQuery(testPath + "/*[jcr:like(fn:lower-case(@foo), 'BA%')]", new Node[]{}); + } + + //----------------------------< internal >---------------------------------- + + private void check(String[] values, Operator operator, String queryTerm, boolean[] matches) + throws RepositoryException { + if (values.length != matches.length) { + throw new IllegalArgumentException("values and matches must have same length"); + } + // create log message + StringBuffer logMsg = new StringBuffer(); + logMsg.append("queryTerm: ").append(queryTerm); + logMsg.append(" values: "); + String separator = ""; + for (int i = 0; i < values.length; i++) { + logMsg.append(separator); + separator = ", "; + if (matches[i]) { + logMsg.append("+"); + } else { + logMsg.append("-"); + } + logMsg.append(values[i]); + } + log.println(logMsg.toString()); + for (NodeIterator it = testRootNode.getNodes(); it.hasNext();) { + it.nextNode().remove(); + } + Set matchingNodes = new HashSet(); + for (int i = 0; i < values.length; i++) { + Node n = testRootNode.addNode("node" + i); + n.setProperty(propertyName1, values[i]); + if (matches[i]) { + matchingNodes.add(n); + } + } + testRootNode.save(); + + Node[] nodes = matchingNodes.toArray(new Node[matchingNodes.size()]); + + // run queries with lower-case + String xpath = operator.formatXpath( + "fn:lower-case(@" + propertyName1 + ")", + "'" + queryTerm.toLowerCase() + "'"); + executeXPathQuery(testPath + "/*[" + xpath + "]", nodes); + + String sql = "select * from nt:base where " + + "jcr:path like '" + testRoot + "/%' and " + + operator.formatSql( + "LOWER(" + propertyName1 + ")", + "'" + queryTerm.toLowerCase() + "'"); + executeSQLQuery(sql, nodes); + + QueryResult result = qomFactory.createQuery( + qomFactory.selector(testNodeType, "s"), + qomFactory.and( + qomFactory.childNode("s", testRoot), + qomFactory.comparison( + qomFactory.lowerCase( + qomFactory.propertyValue("s", propertyName1)), + operator.toString(), + qomFactory.literal( + superuser.getValueFactory().createValue( + queryTerm.toLowerCase())) + ) + ), null, null).execute(); + checkResult(result, nodes); + + // run queries with upper-case + xpath = operator.formatXpath( + "fn:upper-case(@" + propertyName1 + ")", + "'" + queryTerm.toUpperCase() + "'"); + executeXPathQuery(testPath + "/*[" + xpath + "]", nodes); + + sql = "select * from nt:base where " + + "jcr:path like '" + testRoot + "/%' and " + + operator.formatSql( + "UPPER(" + propertyName1 + ")", + "'" + queryTerm.toUpperCase() + "'"); + executeSQLQuery(sql, nodes); + + result = qomFactory.createQuery( + qomFactory.selector(testNodeType, "s"), + qomFactory.and( + qomFactory.childNode("s", testRoot), + operator.comparison( + qomFactory, + qomFactory.upperCase( + qomFactory.propertyValue("s", propertyName1)), + qomFactory.literal( + superuser.getValueFactory().createValue( + queryTerm.toUpperCase())) + ) + ), null, null).execute(); + checkResult(result, nodes); + } + + private char getRandomChar(String pool, Random random) { + return pool.charAt(random.nextInt(pool.length())); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/VersionStoreQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/VersionStoreQueryTest.java new file mode 100644 index 00000000000..d505b7aaf1a --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/VersionStoreQueryTest.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.core.query; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import java.util.Calendar; + +/** + * VersionStoreQueryTest tests queries against the version store. + */ +public class VersionStoreQueryTest extends AbstractQueryTest { + + /** + * Tests if after each checkin an additional node is returned in a result + * of a query. + */ + public void testCheckin() throws RepositoryException { + // current time + Calendar c = Calendar.getInstance(); + Node n1 = testRootNode.addNode(nodeName1); + n1.setProperty(propertyName1, c); + n1.addMixin(mixVersionable); + testRootNode.save(); + + String statement = "//*[@" + propertyName1 + "=xs:dateTime('" + + n1.getProperty(propertyName1).getString() + "')]"; + + Node v1 = n1.checkin().getNode(jcrFrozenNode); + + executeXPathQuery(statement, new Node[]{n1,v1}); + + n1.checkout(); + Node v2 = n1.checkin().getNode(jcrFrozenNode); + + executeXPathQuery(statement, new Node[]{n1,v1,v2}); + + n1.checkout(); + Node v3 = n1.checkin().getNode(jcrFrozenNode); + + executeXPathQuery(statement, new Node[]{n1,v1,v2,v3}); + } +} diff --git a/src/test/org/apache/jackrabbit/core/search/XPathAxisTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/XPathAxisTest.java similarity index 85% rename from src/test/org/apache/jackrabbit/core/search/XPathAxisTest.java rename to jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/XPathAxisTest.java index a051cda9052..3907f524120 100644 --- a/src/test/org/apache/jackrabbit/core/search/XPathAxisTest.java +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/XPathAxisTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.core.search; +package org.apache.jackrabbit.core.query; import javax.jcr.RepositoryException; import javax.jcr.Node; @@ -42,6 +42,16 @@ protected void setUp() throws Exception { testRootNode.save(); } + protected void tearDown() throws Exception { + n1 = null; + n2 = null; + n11 = null; + n12 = null; + n21 = null; + n22 = null; + super.tearDown(); + } + public void testChildAxisRoot() throws RepositoryException { String xpath = "/*"; executeXPathQuery(xpath, new Node[]{superuser.getRootNode()}); @@ -188,4 +198,12 @@ public void testIndex3Descendant() throws RepositoryException { executeXPathQuery(xpath, new Node[0]); } + public void testRootQuery() throws RepositoryException { + // JCR-1987 + executeXPathQuery("/jcr:root[@foo = 'does-not-exist']", new Node[0]); + Node rootNode = superuser.getRootNode(); + executeXPathQuery("/jcr:root", new Node[]{rootNode}); + executeXPathQuery("/jcr:root[@" + jcrPrimaryType + "='" + + rootNode.getPrimaryNodeType().getName() + "']", new Node[]{rootNode}); + } } diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/BlockingParser.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/BlockingParser.java new file mode 100644 index 00000000000..f201f94bb7e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/BlockingParser.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.EmptyParser; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.sax.XHTMLContentHandler; +import org.w3c.dom.Document; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class BlockingParser extends EmptyParser { + + public static final MediaType TYPE = MediaType.application("x-blocked"); + + /** + * Flag for blocking text extraction. + */ + private static volatile boolean blocked = false; + + /** + * Waits until text extraction is no longer blocked. + */ + private synchronized static void waitIfBlocked() { + try { + while (blocked) { + BlockingParser.class.wait(); + } + } catch (InterruptedException e) { + throw new RuntimeException("Text extraction block interrupted", e); + } + } + + /** + * Blocks text extraction. + */ + static synchronized void block() { + blocked = true; + } + + /** + * Unblocks text extraction. + */ + static synchronized void unblock() { + blocked = false; + BlockingParser.class.notifyAll(); + } + + @Override + public Set getSupportedTypes(ParseContext context) { + return Collections.singleton(TYPE); + } + + @Override + public void parse( + InputStream stream, ContentHandler handler, + Metadata metadata, ParseContext context) + throws SAXException { + waitIfBlocked(); + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + Document doc = dbf.newDocumentBuilder().parse(new InputSource(stream)); + String contents = doc.getDocumentElement().getTextContent(); + XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata); + xhtml.startDocument(); + xhtml.element("p", contents); + xhtml.endDocument(); + } catch (ParserConfigurationException ex) { + throw new SAXException(ex); + } catch (IOException ex) { + throw new SAXException(ex); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/ChainedTermEnumTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/ChainedTermEnumTest.java new file mode 100644 index 00000000000..6278315f4d9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/ChainedTermEnumTest.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.core.query.lucene; + +import junit.framework.TestCase; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.Term; +import org.apache.lucene.index.TermEnum; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.RAMDirectory; +import org.apache.lucene.util.Version; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * ChainedTermEnumTest implements a test for JCR-2410. + */ +public class ChainedTermEnumTest extends TestCase { + + public void testEnum() throws Exception { + Collection enums = new ArrayList(); + enums.add(createTermEnum("a", 2)); + enums.add(createTermEnum("b", 1)); + enums.add(createTermEnum("c", 0)); + enums.add(createTermEnum("d", 2)); + TermEnum terms = new IndexMigration.ChainedTermEnum(enums); + List expected = new ArrayList(); + expected.addAll(Arrays.asList("a0", "a1", "b0", "d0", "d1")); + List result = new ArrayList(); + do { + Term t = terms.term(); + if (t != null) { + result.add(t.text()); + } + } while (terms.next()); + assertEquals(expected, result); + } + + protected TermEnum createTermEnum(String prefix, int numTerms) + throws IOException { + Directory dir = new RAMDirectory(); + IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig( + Version.LUCENE_36, new StandardAnalyzer(Version.LUCENE_36))); + try { + for (int i = 0; i < numTerms; i++) { + Document doc = new Document(); + doc.add(new Field("field", true, prefix + i, Field.Store.NO, + Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + writer.addDocument(doc); + } + } finally { + writer.close(); + } + IndexReader reader = IndexReader.open(dir); + try { + TermEnum terms = reader.terms(); + if (terms.term() == null) { + // position at first term + terms.next(); + } + return terms; + } finally { + reader.close(); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/ComparableArrayTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/ComparableArrayTest.java new file mode 100644 index 00000000000..49507d6b796 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/ComparableArrayTest.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.core.query.lucene; + +import static junit.framework.Assert.assertEquals; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.query.lucene.SharedFieldCache.ComparableArray; +import org.junit.Test; + +public class ComparableArrayTest { + + /** + * Test for JCR-2906 to make sure the SharedFieldCache arranges the entries + * properly and keeps the internal array creation efficient. + */ + @Test + public void testInsert() throws RepositoryException { + ComparableArray ca = new ComparableArray("a", 1); + assertEquals("a", ca.toString()); + assertEquals(1, ca.getOffset()); + + // insert before + ca.insert("b", 0); + assertEquals("[b, a]", ca.toString()); + assertEquals(0, ca.getOffset()); + + // insert after + ca.insert("c", 3); + assertEquals("[b, a, null, c]", ca.toString()); + assertEquals(0, ca.getOffset()); + + // insert inside + ca.insert("d", 2); + assertEquals("[b, a, d, c]", ca.toString()); + assertEquals(0, ca.getOffset()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/DecimalConvertTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/DecimalConvertTest.java new file mode 100644 index 00000000000..5df6752fd77 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/DecimalConvertTest.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.core.query.lucene; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Random; + +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Tests converting BigDecimal to String and back. + */ +public class DecimalConvertTest extends JUnitTest { + + public void testCommon() { + // System.out.println(DecimalField.decimalToString(new BigDecimal(0))); + // System.out.println(DecimalField.decimalToString(new BigDecimal(2))); + // System.out.println(DecimalField.decimalToString(new BigDecimal(120))); + // System.out.println(DecimalField.decimalToString(new BigDecimal(-1))); + + ArrayList list = new ArrayList(); + list.add(BigDecimal.ZERO); + list.add(BigDecimal.ONE); + list.add(BigDecimal.TEN); + list.add(BigDecimal.ONE.scaleByPowerOfTen(1)); + list.add(new BigDecimal("100")); + list.add(new BigDecimal("1000")); + list.add(new BigDecimal("0.1")); + list.add(new BigDecimal("0.01")); + list.add(new BigDecimal("0.001")); + list.add(new BigDecimal("1.1")); + list.add(new BigDecimal("0.09")); + list.add(new BigDecimal("9.9")); + list.add(new BigDecimal("9.99")); + list.add(new BigDecimal("99")); + list.add(new BigDecimal("99.0")); + list.add(new BigDecimal("101")); + list.add(new BigDecimal("1000.0")); + list.add(new BigDecimal("-1.23E-10")); + testWithList(list); + } + + public void testMinimumScale() { + if (true) { // JCR-3834 + return; + } + final BigDecimal d1 = new BigDecimal(BigInteger.ONE, Integer.MIN_VALUE); + final BigDecimal d2 = new BigDecimal(BigInteger.ONE, Integer.MIN_VALUE+1); + final String s1 = DecimalField.decimalToString(d1); + final String s2 = DecimalField.decimalToString(d2); + assertEquals(Math.signum(d1.compareTo(d2)), Math.signum(s1.compareTo(s2))); + } + + public void testRandomized() { + if (true) { // JCR-3834 + return; + } + ArrayList list = new ArrayList(); + list.add(BigDecimal.ZERO); + list.add(BigDecimal.ONE); + list.add(BigDecimal.TEN); + list.add(new BigDecimal(BigInteger.ONE, Integer.MAX_VALUE)); + list.add(new BigDecimal(BigInteger.ONE, Integer.MIN_VALUE)); + Random random = new Random(1); + // a few regular values + for (int i = 0; i < 10000; i++) { + list.add(new BigDecimal(i)); + } + for (int i = 0; i < 100; i++) { + list.add(new BigDecimal(random.nextDouble())); + } + // scale -10 .. 10 + for (int i = 0; i < 1000; i++) { + int scale = random.nextInt(20) - 10; + BigInteger value = BigInteger.valueOf(random.nextLong()); + list.add(new BigDecimal(value, scale)); + } + // extremely small and large values + for (int i = 0; i < 100; i++) { + int scale = random.nextInt(2000) - 1000; + BigInteger value = new BigInteger(1000, random); + list.add(new BigDecimal(value, scale)); + } + testWithList(list); + } + + private void testWithList(ArrayList list) { + // add negative values + for (BigDecimal d : new ArrayList(list)) { + list.add(d.negate()); + } + Collections.sort(list); + BigDecimal lastDecimal = null; + String lastString = null; + for (BigDecimal d : list) { + String s = DecimalField.decimalToString(d); + if (lastDecimal != null) { + int compDecimal = (int) Math.signum(lastDecimal.compareTo(d)); + int compString = (int) Math.signum(lastString.compareTo(s)); + if (compDecimal != compString) { + assertEquals(compDecimal, compString); + } + } + BigDecimal test = DecimalField.stringToDecimal(s); + if (test.compareTo(d) != 0) { + assertEquals(d + "<>" + test.toPlainString(), 0, test.compareTo(d)); + } + lastDecimal = d; + lastString = s; + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IDFieldTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IDFieldTest.java new file mode 100644 index 00000000000..ad10a907b94 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IDFieldTest.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.core.query.lucene; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.Field.TermVector; + +/** + * IDFieldTest... + */ +public class IDFieldTest extends TestCase { + + public void testPerformance() { + NodeId id = NodeId.randomId(); + long time = System.currentTimeMillis(); + for (int i = 0; i < 1000 * 1000; i++) { + new IDField(id); + } + time = System.currentTimeMillis() - time; + System.out.println("IDField: " + time + " ms."); + + for (int i = 0; i < 50; i++) { + createNodes(id.toString(), i % 2 == 0); + } + } + + private void createNodes(String id, boolean useNewWay) { + + long time = System.currentTimeMillis(); + for (int i = 0; i < 1000 * 1000; i++) { + if (useNewWay) { + new Field(FieldNames.UUID, false, id, Field.Store.YES, + Field.Index.NOT_ANALYZED_NO_NORMS, TermVector.NO); + } else { + new Field(FieldNames.UUID, id, Field.Store.YES, + Field.Index.NOT_ANALYZED_NO_NORMS, TermVector.NO); + } + + } + time = System.currentTimeMillis() - time; + System.out.println(String.format("Field: %2s ms. new way? %b", time, + useNewWay)); + + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexFormatVersionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexFormatVersionTest.java new file mode 100644 index 00000000000..467f4e69528 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexFormatVersionTest.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.core.query.lucene; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.query.AbstractIndexingTest; + +/** + * IndexFormatVersionTest checks if the various index format + * versions are correctly read from the index. + */ +public class IndexFormatVersionTest extends AbstractIndexingTest { + + public void testVersionOne() throws RepositoryException { + checkIndexFormatVersion("index-format-v1", IndexFormatVersion.V1); + } + + public void testVersionTwo() throws RepositoryException { + checkIndexFormatVersion("index-format-v2", IndexFormatVersion.V2); + } + + public void testVersionThree() throws RepositoryException { + checkIndexFormatVersion("index-format-v3", IndexFormatVersion.V3); + } + + private void checkIndexFormatVersion(String wspName, + IndexFormatVersion version) + throws RepositoryException { + Session session = getHelper().getSuperuserSession(wspName); + try { + SearchIndex index = getSearchIndex(session); + assertEquals("Wrong index format", version.getVersion(), + index.getIndexFormatVersion().getVersion()); + } finally { + session.logout(); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexInfosTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexInfosTest.java new file mode 100644 index 00000000000..31926f14ea1 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexInfosTest.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.core.query.lucene; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; + +import junit.framework.TestCase; + +/** + * IndexInfosTest check if IndexInfos can deal with + * invalid info files. See also JCR-3299. + */ +public class IndexInfosTest extends TestCase { + + private static final File TEST_DIR = new File(new File("target"), "indexInfosTest"); + + private Directory dir; + + @Override + protected void setUp() throws Exception { + super.setUp(); + FileUtils.deleteDirectory(TEST_DIR); + TEST_DIR.mkdirs(); + dir = FSDirectory.open(TEST_DIR); + } + + @Override + protected void tearDown() throws Exception { + dir.close(); + FileUtils.deleteDirectory(TEST_DIR); + super.tearDown(); + } + + public void testEmptyIndexesFile() throws IOException { + // creates initial generation of infos + IndexInfos infos = new IndexInfos(dir, "indexes"); + long initialGeneration = infos.getGeneration(); + + // create second generation + infos.addName("index1", 23432); + infos.write(); + + // replace second generation file with an empty one + String fileName = infos.getFileName(); + dir.deleteFile(fileName); + dir.createOutput(fileName).close(); + + new IndexHistory(dir, Integer.MAX_VALUE); // must succeed + + // read index infos again + infos = new IndexInfos(dir, "indexes"); + assertEquals("must read initial generation", initialGeneration, infos.getGeneration()); + + // create new generation + infos.addName("index1", 39854); + infos.write(); // must succeed + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexMigrationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexMigrationTest.java new file mode 100644 index 00000000000..a87e0ed18ed --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexMigrationTest.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.core.query.lucene; + +import junit.framework.TestCase; +import org.apache.jackrabbit.core.query.lucene.directory.DirectoryManager; +import org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.search.Similarity; +import org.apache.lucene.store.RAMDirectory; +import org.apache.lucene.util.Version; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * IndexMigrationTest contains a test case for JCR-2393. + */ +public class IndexMigrationTest extends TestCase { + + /** + * Cannot use \uFFFF because of LUCENE-1221. + */ + private static final char SEP_CHAR = '\uFFFE'; + + public void testMigration() throws Exception { + List docs = new ArrayList(); + docs.add(createDocument("ab", "a")); + docs.add(createDocument("a", "b")); + docs.add(createDocument("abcd", "c")); + docs.add(createDocument("abc", "d")); + + DirectoryManager dirMgr = new RAMDirectoryManager(); + + PersistentIndex idx = new PersistentIndex("index", + new StandardAnalyzer(Version.LUCENE_36), Similarity.getDefault(), + new DocNumberCache(100), + new IndexingQueue(new IndexingQueueStore(new RAMDirectory())), + dirMgr, 0); + idx.addDocuments(docs.toArray(new Document[docs.size()])); + idx.commit(); + + IndexMigration.migrate(idx, dirMgr, SEP_CHAR); + } + + protected static String createNamedValue14(String name, String value) { + return name + SEP_CHAR + value; + } + + protected static Document createDocument(String name, String value) { + Document doc = new Document(); + doc.add(new Field(FieldNames.UUID, false, UUID.randomUUID().toString(), + Field.Store.YES, Field.Index.NO, Field.TermVector.NO)); + doc.add(new Field(FieldNames.PROPERTIES, false, createNamedValue14( + name, value), Field.Store.NO, Field.Index.NOT_ANALYZED, + Field.TermVector.NO)); + doc.add(new Field(FieldNames.FULLTEXT_PREFIX + ":" + name, true, value, + Field.Store.NO, Field.Index.ANALYZED, + Field.TermVector.WITH_POSITIONS_OFFSETS)); + return doc; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingAggregateTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingAggregateTest.java new file mode 100644 index 00000000000..6152b7d5ad1 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingAggregateTest.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.core.query.lucene; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.Query; + +import org.apache.jackrabbit.core.query.AbstractIndexingTest; + +import java.io.ByteArrayOutputStream; +import java.io.Writer; +import java.io.OutputStreamWriter; +import java.io.ByteArrayInputStream; +import java.util.Calendar; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; + + +/** + * IndexingAggregateTest checks if the nt:file nt:resource + * aggregate defined in workspace indexing-test works properly. + */ +public class IndexingAggregateTest extends AbstractIndexingTest { + + public void testNtFileAggregate() throws Exception { + String sqlBase = "SELECT * FROM nt:file" + + " WHERE jcr:path LIKE '" + testRoot + "/%" + + "' AND CONTAINS"; + String sqlCat = sqlBase + "(., 'cat')"; + String sqlDog = sqlBase + "(., 'dog')"; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + writer.write("the quick brown fox jumps over the lazy dog."); + writer.flush(); + + Node file = testRootNode.addNode("myFile", "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", new ByteArrayInputStream(out.toByteArray())); + + testRootNode.save(); + + executeSQLQuery(sqlDog, new Node[]{file}); + + // update jcr:data + out.reset(); + writer.write("the quick brown fox jumps over the lazy cat."); + writer.flush(); + resource.setProperty("jcr:data", new ByteArrayInputStream(out.toByteArray())); + testRootNode.save(); + + executeSQLQuery(sqlCat, new Node[]{file}); + + // replace jcr:content with unstructured + resource.remove(); + Node unstrContent = file.addNode("jcr:content", "nt:unstructured"); + Node foo = unstrContent.addNode("foo"); + foo.setProperty("text", "the quick brown fox jumps over the lazy dog."); + testRootNode.save(); + + executeSQLQuery(sqlDog, new Node[]{file}); + + // remove foo + foo.remove(); + testRootNode.save(); + + executeSQLQuery(sqlDog, new Node[]{}); + + // replace jcr:content again with resource + unstrContent.remove(); + resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", new ByteArrayInputStream(out.toByteArray())); + testRootNode.save(); + + executeSQLQuery(sqlCat, new Node[]{file}); + } + + public void testContentLastModified() throws RepositoryException { + List expected = new ArrayList(); + long time = System.currentTimeMillis(); + for (int i = 0; i < 10; i++) { + expected.add(addFile(testRootNode, "file" + i, time)); + time += 1000; + } + testRootNode.save(); + + String stmt = testPath + "/* order by jcr:content/@jcr:lastModified"; + Query q = qm.createQuery(stmt, Query.XPATH); + checkResultSequence(q.execute().getRows(), (Node[]) expected.toArray(new Node[expected.size()])); + + // descending + stmt = testPath + "/* order by jcr:content/@jcr:lastModified descending"; + q = qm.createQuery(stmt, Query.XPATH); + Collections.reverse(expected); + checkResultSequence(q.execute().getRows(), (Node[]) expected.toArray(new Node[expected.size()])); + + // reverse order in content + for (Iterator it = expected.iterator(); it.hasNext(); ) { + Node file = (Node) it.next(); + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(time); + file.getNode("jcr:content").setProperty("jcr:lastModified", cal); + time -= 1000; + } + testRootNode.save(); + + stmt = testPath + "/* order by jcr:content/@jcr:lastModified descending"; + q = qm.createQuery(stmt, Query.XPATH); + checkResultSequence(q.execute().getRows(), (Node[]) expected.toArray(new Node[expected.size()])); + } + + public void disabled_testPerformance() throws RepositoryException { + createNodes(testRootNode, 10, 4, 0, new NodeCreationCallback() { + public void nodeCreated(Node node, int count) throws + RepositoryException { + node.addNode("child").setProperty("property", "value" + count); + // save once in a while + if (count % 1000 == 0) { + session.save(); + System.out.println("added " + count + " nodes so far."); + } + } + }); + session.save(); + + String xpath = testPath + "//*[child/@property] order by child/@property"; + for (int i = 0; i < 3; i++) { + long time = System.currentTimeMillis(); + Query query = qm.createQuery(xpath, Query.XPATH); + query.setLimit(20); + query.execute().getNodes().getSize(); + time = System.currentTimeMillis() - time; + System.out.println("executed query in " + time + " ms."); + } + } + + private static Node addFile(Node folder, String name, long lastModified) + throws RepositoryException { + Node file = folder.addNode(name, "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(lastModified); + resource.setProperty("jcr:lastModified", cal); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", new ByteArrayInputStream("test".getBytes())); + return file; + } + + private int createNodes(Node n, int nodesPerLevel, int levels, + int count, NodeCreationCallback callback) + throws RepositoryException { + levels--; + for (int i = 0; i < nodesPerLevel; i++) { + Node child = n.addNode("node" + i); + count++; + callback.nodeCreated(child, count); + if (levels > 0) { + count = createNodes(child, nodesPerLevel, levels, count, callback); + } + } + return count; + } + + private static interface NodeCreationCallback { + + public void nodeCreated(Node node, int count) throws RepositoryException; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationImplTest.java new file mode 100644 index 00000000000..81728140558 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingConfigurationImplTest.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.core.query.lucene; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Node; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.query.AbstractIndexingTest; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * IndexingConfigurationImplTest... + */ +public class IndexingConfigurationImplTest extends AbstractIndexingTest { + + private static final Name FOO = NameFactoryImpl.getInstance().create("", "foo"); + + private NodeState nState; + private Node n; + + @Override + protected void setUp() throws Exception { + super.setUp(); + n = testRootNode.addNode(nodeName1, ntUnstructured); + n.addMixin(mixReferenceable); + n.addMixin(mixTitle); + session.save(); + nState = (NodeState) getSearchIndex().getContext().getItemStateManager().getItemState( + new NodeId(n.getIdentifier())); + } + + public void testMatchAllNoPrefix() throws Exception { + IndexingConfiguration config = createConfig("config1"); + assertFalse(config.isIndexed(nState, NameConstants.JCR_DATA)); + assertTrue(config.isIndexed(nState, FOO)); + } + + public void testRegexpInPrefix() throws Exception { + IndexingConfiguration config = createConfig("config2"); + assertTrue(config.isIndexed(nState, NameConstants.JCR_DATA)); + assertTrue(config.isIndexed(nState, FOO)); + } + + public void testMatchAllJCRPrefix() throws Exception { + IndexingConfiguration config = createConfig("config3"); + assertTrue(config.isIndexed(nState, NameConstants.JCR_DATA)); + assertFalse(config.isIndexed(nState, FOO)); + } + + public void testAddNodeTypeToRegistry() throws Exception { + IndexingConfiguration config = createConfig("config4"); + // add node type + NodeTypeManager ntMgr = session.getWorkspace().getNodeTypeManager(); + String baseName = "indexingTextNodeType"; + int i = 0; + String nt; + do { + nt = baseName + "_" + i++; + } while (ntMgr.hasNodeType(nt)); + // register node type + NodeTypeTemplate ntTemplate = ntMgr.createNodeTypeTemplate(); + ntTemplate.setName(nt); + ntTemplate.setDeclaredSuperTypeNames(new String[]{ntUnstructured}); + ntMgr.registerNodeType(ntTemplate, false); + // create node + Node n = testRootNode.addNode(nodeName2, nt); + session.save(); + // get state + NodeState state = (NodeState) getSearchIndex().getContext().getItemStateManager().getItemState( + new NodeId(n.getIdentifier())); + assertTrue(config.isIndexed(state, FOO)); + assertFalse(config.isIncludedInNodeScopeIndex(state, FOO)); + } + + public void testIndexRuleMixin() throws Exception{ + IndexingConfiguration config = createConfig("config5"); + assertTrue(config.isIndexed(nState, NameConstants.JCR_TITLE)); + assertFalse(config.isIndexed(nState, NameConstants.JCR_DESCRIPTION)); + assertTrue(config.isIndexed(nState, NameConstants.JCR_UUID)); // from mixReferenceable ... should be indexed + } + + //----------------------------< internal >---------------------------------- + protected IndexingConfiguration createConfig(String name) throws Exception { + IndexingConfiguration config = new IndexingConfigurationImpl(); + config.init(loadConfig(name), getSearchIndex().getContext(), + getSearchIndex().getNamespaceMappings()); + return config; + } + + protected Element loadConfig(String name) + throws ParserConfigurationException, IOException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new IndexingConfigurationEntityResolver()); + InputStream in = getClass().getResourceAsStream("indexing_" + name + ".xml"); + return builder.parse(in).getDocumentElement(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingQueueTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingQueueTest.java new file mode 100644 index 00000000000..f06af8cc364 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingQueueTest.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.core.query.lucene; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; + +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.TestHelper; +import org.apache.jackrabbit.core.fs.local.FileUtil; +import org.apache.jackrabbit.core.query.AbstractIndexingTest; + +/** + * IndexingQueueTest checks if the indexing queue properly indexes + * nodes in a background thread when text extraction takes more than 10 ms. See + * the workspace.xml file for the indexing-test workspace. + */ +public class IndexingQueueTest extends AbstractIndexingTest { + + private static final File TEMP_DIR = + new File(System.getProperty("java.io.tmpdir")); + + private static final String TESTCONTENT = "\nThe quick brown fox jumps over the lazy dog."; + + public void testQueue() throws Exception { + SearchIndex index = getSearchIndex(); + IndexingQueue queue = index.getIndex().getIndexingQueue(); + + BlockingParser.block(); + assertEquals(0, queue.getNumPendingDocuments()); + + Node resource = testRootNode.addNode(nodeName1, "nt:resource"); + resource.setProperty("jcr:data", TESTCONTENT, PropertyType.BINARY); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:mimeType", BlockingParser.TYPE.toString()); + session.save(); + + index.getIndex().getVolatileIndex().commit(); + assertEquals(1, queue.getNumPendingDocuments()); + + Query q = qm.createQuery(testPath + "/*[jcr:contains(., 'fox')]", Query.XPATH); + NodeIterator nodes = q.execute().getNodes(); + assertFalse(nodes.hasNext()); + + BlockingParser.unblock(); + waitForTextExtractionTasksToFinish(); + assertEquals(0, queue.getNumPendingDocuments()); + + q = qm.createQuery(testPath + "/*[jcr:contains(., 'fox')]", Query.XPATH); + nodes = q.execute().getNodes(); + assertTrue(nodes.hasNext()); + } + + public void testInitialIndex() throws Exception { + BlockingParser.block(); + File indexDir = new File(getSearchIndex().getPath()); + + // fill workspace + Node testFolder = testRootNode.addNode("folder", "nt:folder"); + int num = createFiles(testFolder, 10, 2, 0); + session.save(); + + // shutdown workspace + RepositoryImpl repo = (RepositoryImpl) session.getRepository(); + session.logout(); + session = null; + superuser.logout(); + superuser = null; + TestHelper.shutdownWorkspace(getWorkspaceName(), repo); + + // delete index + try { + FileUtil.delete(indexDir); + } catch (IOException e) { + fail("Unable to delete index directory"); + } + + int initialNumExtractorFiles = getNumExtractorFiles(); + + BlockingParser.unblock(); + Thread t = new Thread(new Runnable() { + public void run() { + try { + session = getHelper().getSuperuserSession(getWorkspaceName()); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + }); + t.start(); + + while (t.isAlive()) { + // there must not be more than 20 extractor files, because: + // - initial index creation checks indexing queue every 10 nodes + // - there is an aggregate definition on the workspace that causes + // 2 extractor jobs per nt:resource + // => 2 * 10 = 20 + int numFiles = getNumExtractorFiles() - initialNumExtractorFiles; + assertTrue(numFiles <= 20); + Thread.sleep(50); + } + + qm = session.getWorkspace().getQueryManager(); + waitForTextExtractionTasksToFinish(); + + String stmt = testPath + "//element(*, nt:resource)[jcr:contains(., 'fox')] order by @jcr:score descending"; + Query q = qm.createQuery(stmt, Query.XPATH); + assertEquals(num, q.execute().getNodes().getSize()); + } + + /* + * Test case for JCR-2082 + */ + public void testReaderUpToDate() throws Exception { + BlockingParser.block(); + SearchIndex index = getSearchIndex(); + File indexDir = new File(index.getPath()); + + // shutdown workspace + RepositoryImpl repo = (RepositoryImpl) session.getRepository(); + session.logout(); + session = null; + superuser.logout(); + superuser = null; + TestHelper.shutdownWorkspace(getWorkspaceName(), repo); + + // delete index + try { + FileUtil.delete(indexDir); + } catch (IOException e) { + fail("Unable to delete index directory"); + } + + BlockingParser.unblock(); + // start workspace again by getting a session + session = getHelper().getSuperuserSession(getWorkspaceName()); + + qm = session.getWorkspace().getQueryManager(); + + Query q = qm.createQuery(testPath, Query.XPATH); + assertEquals(1, getSize(q.execute().getNodes())); + } + + private int createFiles( + Node folder, int filesPerLevel, int levels, int count) + throws RepositoryException { + levels--; + for (int i = 0; i < filesPerLevel; i++) { + // create files + Node file = folder.addNode("file" + i, "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:data", TESTCONTENT, PropertyType.BINARY); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:mimeType", BlockingParser.TYPE.toString()); + count++; + } + if (levels > 0) { + for (int i = 0; i < filesPerLevel; i++) { + // create files + Node subFolder = folder.addNode("folder" + i, "nt:folder"); + count = createFiles(subFolder, filesPerLevel, levels, count); + } + } + return count; + } + + private int getNumExtractorFiles() throws IOException { + return TEMP_DIR.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.startsWith("extractor"); + } + }).length; + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingRuleTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingRuleTest.java new file mode 100644 index 00000000000..3cac486536e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/IndexingRuleTest.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.core.query.lucene; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.core.query.AbstractIndexingTest; + + +/** + * IndexingRuleTest performs indexing rule tests. + */ +public class IndexingRuleTest extends AbstractIndexingTest { + + private static final String NT_UNSTRUCTURED = "nt:unstructured"; + + private static final String TEXT = "the quick brown fox jumps over the lazy dog"; + + public void testRegexp() throws RepositoryException { + Node node1 = testRootNode.addNode(nodeName1, NT_UNSTRUCTURED); + node1.setProperty("rule", "regexp"); + node1.setProperty("Text", "foo"); + Node node2 = testRootNode.addNode(nodeName2, NT_UNSTRUCTURED); + node2.setProperty("rule", "regexp"); + node2.setProperty("OtherText", "foo"); + Node node3 = testRootNode.addNode(nodeName3, NT_UNSTRUCTURED); + node3.setProperty("rule", "regexp"); + node3.setProperty("Textle", "foo"); + testRootNode.save(); + String stmt = "/jcr:root" + testRootNode.getPath() + + "/*[jcr:contains(., 'foo')]"; + checkResult(executeQuery(stmt), new Node[]{node1, node2}); + } + + public void testBoost() throws RepositoryException { + Node node1 = testRootNode.addNode(nodeName1, NT_UNSTRUCTURED); + node1.setProperty("rule", "boost1"); + node1.setProperty("text", TEXT); + Node node2 = testRootNode.addNode(nodeName2, NT_UNSTRUCTURED); + node2.setProperty("rule", "boost2"); + node2.setProperty("text", TEXT); + Node node3 = testRootNode.addNode(nodeName3, NT_UNSTRUCTURED); + node3.setProperty("rule", "boost3"); + node3.setProperty("text", TEXT); + testRootNode.getSession().save(); + String stmt = "/jcr:root" + + testRootNode.getPath() + + "/*[jcr:contains(@text, 'quick')] order by @jcr:score descending"; + executeXPathQuery(stmt, new Node[] { node3, node2, node1 }); + } + + public void testNodeScopeIndex() throws RepositoryException { + Node node1 = testRootNode.addNode(nodeName1, NT_UNSTRUCTURED); + node1.setProperty("rule", "nsiTrue"); + node1.setProperty("text", TEXT); + Node node2 = testRootNode.addNode(nodeName2, NT_UNSTRUCTURED); + node2.setProperty("rule", "nsiFalse"); + node2.setProperty("text", TEXT); + testRootNode.save(); + String stmt = "/jcr:root" + testRootNode.getPath() + + "/*[jcr:contains(., 'quick')]"; + checkResult(executeQuery(stmt), new Node[]{node1}); + } + + public void testNodeType() throws RepositoryException { + // assumes there is an index-rule for nt:hierarchyNode that + // does not include the property jcr:created + Node node1 = testRootNode.addNode(nodeName1, "nt:folder"); + testRootNode.save(); + String stmt = "/jcr:root" + testRootNode.getPath() + + "/*[@" + jcrCreated + " = xs:dateTime('" + + node1.getProperty(jcrCreated).getString() + "')]"; + checkResult(executeQuery(stmt), new Node[]{}); + } + + public void testUseInExcerpt() throws RepositoryException { + Node node = testRootNode.addNode(nodeName1, NT_UNSTRUCTURED); + node.setProperty("rule", "excerpt"); + node.setProperty("title", "Apache Jackrabbit"); + // the value below is for testing https://issues.apache.org/jira/browse/JCR-3610 + node.setProperty("foo", "markup"); + node.setProperty("text", "Jackrabbit is a JCR implementation"); + testRootNode.save(); + String stmt = "/jcr:root" + testRootNode.getPath() + + "/*[jcr:contains(., 'jackrabbit implementation')]/rep:excerpt(.)"; + RowIterator rows = executeQuery(stmt).getRows(); + assertTrue("No results returned", rows.hasNext()); + Value excerpt = rows.nextRow().getValue("rep:excerpt(.)"); + assertNotNull("No excerpt created", excerpt); + assertTrue("Title must not be present in excerpt", + excerpt.getString().indexOf("Apache") == -1); + assertTrue("Missing highlight", + excerpt.getString().indexOf("implementation") != -1); + + stmt = "/jcr:root" + testRootNode.getPath() + + "/*[jcr:contains(., 'apache')]/rep:excerpt(.)"; + rows = executeQuery(stmt).getRows(); + assertTrue("No results returned", rows.hasNext()); + excerpt = rows.nextRow().getValue("rep:excerpt(.)"); + assertNotNull("No excerpt created", excerpt); + assertTrue("Title must not be present in excerpt", + excerpt.getString().indexOf("Apache") == -1); + } + + public void testExcerptOnExcludedProperty() throws RepositoryException { + Node node = testRootNode.addNode(nodeName1, NT_UNSTRUCTURED); + node.setProperty("rule", "excerpt"); + node.setProperty("title", TEXT); + testRootNode.save(); + String stmt = "/jcr:root" + testRootNode.getPath() + + "/*[jcr:contains(., 'quick')]/rep:excerpt(.)"; + QueryResult result = executeQuery(stmt); + checkResult(result, new Node[]{node}); + Value excerpt = result.getRows().nextRow().getValue("rep:excerpt(.)"); + assertNotNull("No excerpt created", excerpt); + } + + public void testUseInExcerptWithAggregate() throws RepositoryException { + Node node = testRootNode.addNode(nodeName1, NT_UNSTRUCTURED); + node.setProperty("rule", "excerpt"); + node.setProperty("title", "Apache Jackrabbit"); + node.setProperty("text", "Jackrabbit is a JCR implementation"); + Node aggregated = node.addNode("aggregated-node", NT_UNSTRUCTURED); + aggregated.setProperty("rule", "excerpt"); + aggregated.setProperty("title", "Apache Jackrabbit"); + aggregated.setProperty("text", "Jackrabbit is a JCR implementation"); + testRootNode.save(); + + String stmt = "/jcr:root" + testRootNode.getPath() + + "/*[jcr:contains(., 'jackrabbit')]/rep:excerpt(.)"; + RowIterator rows = executeQuery(stmt).getRows(); + assertTrue("No results returned", rows.hasNext()); + Value excerpt; + while (rows.hasNext()) { + excerpt = rows.nextRow().getValue("rep:excerpt(.)"); + assertNotNull("No excerpt created", excerpt); + assertTrue("Title must not be present in excerpt", + excerpt.getString().indexOf("Apache") == -1); + int idx = 0; + int numHighlights = 0; + for (;;) { + idx = excerpt.getString().indexOf("", idx); + if (idx == -1) { + break; + } + numHighlights++; + int endIdx = excerpt.getString().indexOf("", idx); + assertEquals("wrong highlight", "Jackrabbit", + excerpt.getString().substring(idx + "".length(), endIdx)); + idx = endIdx; + } + assertTrue("Missing highlight", numHighlights > 0); + } + + stmt = "/jcr:root" + testRootNode.getPath() + + "/*[jcr:contains(., 'apache')]/rep:excerpt(.)"; + rows = executeQuery(stmt).getRows(); + assertTrue("No results returned", rows.hasNext()); + excerpt = rows.nextRow().getValue("rep:excerpt(.)"); + assertNotNull("No excerpt created", excerpt); + assertTrue("Title must not be present in excerpt", + excerpt.getString().indexOf("Apache") == -1); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/LargeResultSetTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/LargeResultSetTest.java new file mode 100644 index 00000000000..b980dd0c999 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/LargeResultSetTest.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.core.query.lucene; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.core.query.AbstractIndexingTest; + +/** + * LargeResultSetTest... + * + * TODO what does this test actually do? + */ +public class LargeResultSetTest extends AbstractIndexingTest { + + public void testResultSet() throws RepositoryException { + int count = createNodes(testRootNode, 10, 5, 0); + session.save(); + + SearchIndex index = getSearchIndex(); + int resultFetchSize = index.getResultFetchSize(); + try { + String stmt = testPath + "//*[@" + jcrPrimaryType + "] order by @jcr:score descending"; + + // with result fetch size Integer.MAX_VALUE + readResult(executeQuery(stmt), count); + + // with result fetch size 100 + index.setResultFetchSize(100); + readResult(executeQuery(stmt), count); + + // with 100 limit + Query query = qm.createQuery(stmt, Query.XPATH); + query.setLimit(100); + readResult(query.execute(), 100); + } finally { + index.setResultFetchSize(resultFetchSize); + } + } + + protected void tearDown() throws Exception { + int count = 0; + for (NodeIterator it = testRootNode.getNodes(); it.hasNext();) { + it.nextNode().remove(); + count++; + if (count % 10000 == 0) { + session.save(); + } + } + session.save(); + super.tearDown(); + } + + /* + * use default ws + */ + protected String getWorkspaceName() { + return null; + } + + private void readResult(QueryResult result, int count) throws RepositoryException { + for (RowIterator rows = result.getRows(); rows.hasNext(); ) { + rows.nextRow(); + count--; + } + assertEquals(0, count); + } + + private int createNodes(Node n, int nodesPerLevel, int levels, int count) + throws RepositoryException { + levels--; + for (int i = 0; i < nodesPerLevel; i++) { + Node child = n.addNode("node" + i); + count++; + if (count % 10000 == 0) { + session.save(); + } + if (levels > 0) { + count = createNodes(child, nodesPerLevel, levels, count); + } + } + return count; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/LazyTextExtractorFieldTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/LazyTextExtractorFieldTest.java new file mode 100644 index 00000000000..4904f77444e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/LazyTextExtractorFieldTest.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.core.query.lucene; + +import org.apache.jackrabbit.core.data.RandomInputStream; +import org.apache.jackrabbit.core.query.AbstractIndexingTest; +import org.apache.jackrabbit.core.query.lucene.LazyTextExtractorField.ParsingTask; +import org.apache.jackrabbit.core.value.InternalValue; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.parser.Parser; + +public class LazyTextExtractorFieldTest extends AbstractIndexingTest { + + /** + * @see JCR-3296 + * Indexing ignored file types creates some garbage + */ + public void testEmptyParser() throws Exception { + + InternalValue val = InternalValue + .create(new RandomInputStream(1, 1024)); + + Metadata metadata = new Metadata(); + metadata.set(Metadata.CONTENT_TYPE, "application/java-archive"); + metadata.set(Metadata.CONTENT_ENCODING, "UTF-8"); + + Parser p = getSearchIndex().getParser(); + + ParsingTask task = new ParsingTask(p, val, metadata, Integer.MAX_VALUE) { + public void setExtractedText(String value) { + assertEquals("", value); + } + }; + task.run(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SQL2IndexingAggregateTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SQL2IndexingAggregateTest.java new file mode 100644 index 00000000000..ab00e7ec8c2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SQL2IndexingAggregateTest.java @@ -0,0 +1,370 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import static javax.jcr.query.Query.JCR_SQL2; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import javax.jcr.Node; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.io.input.NullInputStream; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.query.AbstractIndexingTest; + +/** + * SQL2IndexingAggregateTest checks if aggregation rules defined in + * workspace indexing-test work properly. + * + * See src/test/repository/workspaces/indexing-test/indexing-configuration.xml + */ +public class SQL2IndexingAggregateTest extends AbstractIndexingTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + for (Node c : JcrUtils.getChildNodes(testRootNode)) { + testRootNode.getSession().removeItem(c.getPath()); + } + testRootNode.getSession().save(); + } + + /** + * + * this test is very similar to + * {@link SQL2IndexingAggregateTest#testNtFileAggregate() + * testNtFileAggregate} but checks embedded index aggregates. + * + * The aggregation hierarchy is defined in + * src/test/repository/workspaces/indexing-test/indexing-configuration.xml + * + * basically a folder aggregates other folders and files that aggregate a + * stream of content. + * + * see JCR-2989 + * + * nt:folder: recursive="true" recursiveLimit="10" + * + */ + @SuppressWarnings("unchecked") + public void testDeepHierarchy() throws Exception { + + // this parameter IS the 'recursiveLimit' defined in the index + // config file + int definedRecursiveLimit = 10; + int levelsDeep = 14; + + List allNodes = new ArrayList(); + List updatedNodes = new ArrayList(); + List staleNodes = new ArrayList(); + + String sqlBase = "SELECT * FROM [nt:folder] as f WHERE "; + String sqlCat = sqlBase + " CONTAINS (f.*, 'cat')"; + String sqlDog = sqlBase + " CONTAINS (f.*, 'dog')"; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + writer.write("the quick brown fox jumps over the lazy dog."); + writer.flush(); + + Node folderRoot = testRootNode.addNode("myFolder", "nt:folder"); + Node folderChild = folderRoot; + allNodes.add(folderChild); + + for (int i = 0; i < levelsDeep; i++) { + folderChild.addNode("0" + i + "-dummy", "nt:folder"); + folderChild = folderChild.addNode("0" + i, "nt:folder"); + allNodes.add(folderChild); + + // -2 because: + // 1 because 'i' starts at 0, + // + + // 1 because we are talking about same node type aggregation levels + // extra to the current node that is updated + if (i > levelsDeep - definedRecursiveLimit - 2) { + updatedNodes.add(folderChild); + } + } + staleNodes.addAll(CollectionUtils.disjunction(allNodes, updatedNodes)); + + Node file = folderChild.addNode("myFile", "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", session.getValueFactory() + .createBinary(new ByteArrayInputStream(out.toByteArray()))); + + testRootNode.getSession().save(); + + // because of the optimizations, the first save is expected to update + // ALL nodes + // executeSQL2Query(sqlDog, allNodes.toArray(new Node[] {})); + + // update jcr:data + out.reset(); + writer.write("the quick brown fox jumps over the lazy cat."); + writer.flush(); + resource.setProperty("jcr:data", session.getValueFactory() + .createBinary(new ByteArrayInputStream(out.toByteArray()))); + testRootNode.getSession().save(); + executeSQL2Query(sqlDog, staleNodes.toArray(new Node[] {})); + executeSQL2Query(sqlCat, updatedNodes.toArray(new Node[] {})); + + // replace jcr:content with unstructured + resource.remove(); + Node unstrContent = file.addNode("jcr:content", "nt:unstructured"); + Node foo = unstrContent.addNode("foo"); + foo.setProperty("text", "the quick brown fox jumps over the lazy dog."); + testRootNode.getSession().save(); + executeSQL2Query(sqlDog, allNodes.toArray(new Node[] {})); + executeSQL2Query(sqlCat, new Node[] {}); + + // remove foo + foo.remove(); + testRootNode.getSession().save(); + executeSQL2Query(sqlDog, staleNodes.toArray(new Node[] {})); + executeSQL2Query(sqlCat, new Node[] {}); + + } + + /** + * simple index aggregation from jcr:content to nt:file + * + * The aggregation hierarchy is defined in + * src/test/repository/workspaces/indexing-test/indexing-configuration.xml + * + */ + public void testNtFileAggregate() throws Exception { + + String sqlBase = "SELECT * FROM [nt:file] as f" + + " WHERE ISCHILDNODE([" + testRoot + "])"; + String sqlCat = sqlBase + " AND CONTAINS (f.*, 'cat')"; + String sqlDog = sqlBase + " AND CONTAINS (f.*, 'dog')"; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + writer.write("the quick brown fox jumps over the lazy dog."); + writer.flush(); + + Node file = testRootNode.addNode("myFile", "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", session.getValueFactory() + .createBinary(new ByteArrayInputStream(out.toByteArray()))); + testRootNode.getSession().save(); + executeSQL2Query(sqlDog, new Node[] { file }); + + // update jcr:data + out.reset(); + writer.write("the quick brown fox jumps over the lazy cat."); + writer.flush(); + resource.setProperty("jcr:data", session.getValueFactory() + .createBinary(new ByteArrayInputStream(out.toByteArray()))); + testRootNode.getSession().save(); + executeSQL2Query(sqlDog, new Node[] {}); + executeSQL2Query(sqlCat, new Node[] { file }); + + // replace jcr:content with unstructured + resource.remove(); + Node unstrContent = file.addNode("jcr:content", "nt:unstructured"); + Node foo = unstrContent.addNode("foo"); + foo.setProperty("text", "the quick brown fox jumps over the lazy dog."); + testRootNode.getSession().save(); + executeSQL2Query(sqlDog, new Node[] { file }); + executeSQL2Query(sqlCat, new Node[] {}); + + // remove foo + foo.remove(); + testRootNode.getSession().save(); + executeSQL2Query(sqlDog, new Node[] {}); + executeSQL2Query(sqlCat, new Node[] {}); + + // replace jcr:content again with resource + unstrContent.remove(); + resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", session.getValueFactory() + .createBinary(new ByteArrayInputStream(out.toByteArray()))); + testRootNode.getSession().save(); + executeSQL2Query(sqlDog, new Node[] {}); + executeSQL2Query(sqlCat, new Node[] { file }); + + } + + /** + * JCR-3160 - Session#move doesn't trigger rebuild of parent node + * aggregation + */ + public void testAggregateMove() throws Exception { + + String sql = "SELECT * FROM [nt:folder] as f" + + " WHERE ISDESCENDANTNODE([" + testRoot + "])" + + " AND CONTAINS (f.*, 'dog')"; + + Node folderA = testRootNode.addNode("folderA", "nt:folder"); + Node folderB = testRootNode.addNode("folderB", "nt:folder"); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + writer.write("the quick brown fox jumps over the lazy dog."); + writer.flush(); + + Node file = folderA.addNode("myFile", "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", session.getValueFactory() + .createBinary(new ByteArrayInputStream(out.toByteArray()))); + + testRootNode.getSession().save(); + executeSQL2Query(sql, new Node[] { folderA }); + + testRootNode.getSession().move(file.getPath(), + folderB.getPath() + "/myFile"); + testRootNode.getSession().save(); + executeSQL2Query(sql, new Node[] { folderB }); + } + + /** + * By default, the recursive aggregation is turned off. + * + * The aggregation hierarchy is defined in + * src/test/repository/workspaces/indexing-test/indexing-configuration.xml + */ + public void testDefaultRecursiveAggregation() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + writer.write("the quick brown fox jumps over the lazy dog."); + writer.flush(); + + Node parent = testRootNode.addNode( + "testDefaultRecursiveAggregation_parent", + JcrConstants.NT_UNSTRUCTURED); + + Node child = parent.addNode("testDefaultRecursiveAggregation_child", + JcrConstants.NT_UNSTRUCTURED); + child.setProperty("type", "testnode"); + child.setProperty("jcr:encoding", "UTF-8"); + child.setProperty("jcr:mimeType", "text/plain"); + child.setProperty( + "jcr:data", + session.getValueFactory().createBinary( + new ByteArrayInputStream(out.toByteArray()))); + testRootNode.getSession().save(); + + String sqlBase = "SELECT * FROM [nt:unstructured] as u WHERE CONTAINS (u.*, 'dog') "; + String sqlParent = sqlBase + " AND ISCHILDNODE([" + testRoot + "])"; + String sqlChild = sqlBase + " AND ISCHILDNODE([" + parent.getPath() + + "])"; + + executeSQL2Query(sqlParent, new Node[] {}); + executeSQL2Query(sqlChild, new Node[] { child }); + } + + /** + * Tests that even if there is a rule to include same type node aggregates + * by property name, the recursive flag will still ignore them. + * + * It should issue a log warning, though. + * + * The aggregation hierarchy is defined in + * src/test/repository/workspaces/indexing-test/indexing-configuration.xml + */ + public void testRecursiveAggregationExclusion() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + writer.write("the quick brown fox jumps over the lazy dog."); + writer.flush(); + + Node parent = testRootNode.addNode( + "testDefaultRecursiveAggregation_parent", + JcrConstants.NT_UNSTRUCTURED); + + Node child = parent.addNode("aggregated-node", + JcrConstants.NT_UNSTRUCTURED); + child.setProperty("type", "testnode"); + child.setProperty("jcr:encoding", "UTF-8"); + child.setProperty("jcr:mimeType", "text/plain"); + child.setProperty( + "jcr:data", + session.getValueFactory().createBinary( + new ByteArrayInputStream(out.toByteArray()))); + testRootNode.getSession().save(); + + String sqlBase = "SELECT * FROM [nt:unstructured] as u WHERE CONTAINS (u.*, 'dog') "; + String sqlParent = sqlBase + " AND ISCHILDNODE([" + testRoot + "])"; + String sqlChild = sqlBase + " AND ISCHILDNODE([" + parent.getPath() + + "])"; + executeSQL2Query(sqlParent, new Node[] {}); + executeSQL2Query(sqlChild, new Node[] { child }); + } + + /** + * checks that while text extraction runs in the background, the node is + * available for query on any of its properties + * + * see JCR-2980 + * + */ + public void testAsyncIndexQuery() throws Exception { + + Node n = testRootNode.addNode("justnode", JcrConstants.NT_UNSTRUCTURED); + n.setProperty("type", "testnode"); + n.setProperty("jcr:encoding", "UTF-8"); + n.setProperty("jcr:mimeType", "text/plain"); + n.setProperty( + "jcr:data", + session.getValueFactory().createBinary( + new NullInputStream(1024 * 40))); + testRootNode.getSession().save(); + + String sql = "SELECT * FROM [nt:unstructured] as f " + + " WHERE ISCHILDNODE([" + testRoot + + "]) and type = 'testnode' "; + checkResult(qm.createQuery(sql, JCR_SQL2).execute(), 1); + } + + public void testAggregatedProperty() throws Exception { + + Node parent = testRootNode.addNode("parent", + JcrConstants.NT_UNSTRUCTURED); + Node child = parent.addNode("child", JcrConstants.NT_UNSTRUCTURED); + child.setProperty("property", + "the quick brown fox jumps over the lazy dog."); + testRootNode.getSession().save(); + + String sql = "SELECT * FROM [nt:unstructured] as u WHERE CONTAINS (u.*, 'dog') "; + executeSQL2Query(sql, new Node[] { parent, child }); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SQL2IndexingAggregateTest2.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SQL2IndexingAggregateTest2.java new file mode 100644 index 00000000000..8e81ca7921f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SQL2IndexingAggregateTest2.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import javax.jcr.Node; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.query.AbstractIndexingTest; + +/** + * SQL2IndexingAggregateTest2 checks if aggregation rules defined + * in workspace indexing-test-2 work properly. + * + * See src/test/repository/workspaces/indexing-test-2/indexing-configuration.xml + */ +public class SQL2IndexingAggregateTest2 extends AbstractIndexingTest { + + protected static final String WORKSPACE_NAME_NEW = "indexing-test-2"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + for (Node c : JcrUtils.getChildNodes(testRootNode)) { + testRootNode.getSession().removeItem(c.getPath()); + } + testRootNode.getSession().save(); + } + + @Override + protected String getWorkspaceName() { + return WORKSPACE_NAME_NEW; + } + + /** + * recursive="true" recursiveLimit="-1" + * + * The aggregation hierarchy is defined in + * src/test/repository/workspaces/indexing-test-2/indexing-configuration.xml + * + * see JCR-2989 + */ + public void testNegativeRecursiveAggregationLimit() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + writer.write("the quick brown fox jumps over the lazy dog."); + writer.flush(); + + Node parent = testRootNode.addNode( + "testDefaultRecursiveAggregation_parent", + JcrConstants.NT_UNSTRUCTURED); + + Node child = parent.addNode("testDefaultRecursiveAggregation_child", + JcrConstants.NT_UNSTRUCTURED); + child.setProperty("type", "testnode"); + child.setProperty("jcr:encoding", "UTF-8"); + child.setProperty("jcr:mimeType", "text/plain"); + child.setProperty( + "jcr:data", + session.getValueFactory().createBinary( + new ByteArrayInputStream(out.toByteArray()))); + testRootNode.getSession().save(); + + String sqlBase = "SELECT * FROM [nt:unstructured] as u WHERE CONTAINS (u.*, 'dog') "; + String sqlParent = sqlBase + " AND ISCHILDNODE([" + testRoot + "])"; + String sqlChild = sqlBase + " AND ISCHILDNODE([" + parent.getPath() + + "])"; + + executeSQL2Query(sqlParent, new Node[] {}); + executeSQL2Query(sqlChild, new Node[] { child }); + } + + /** + * this should traverse the *entire* hierarchy to aggregate indexes + * + * see JCR-2989 + */ + public void testUnlimitedRecursiveAggregation() throws Exception { + + long levelsDeep = AggregateRuleImpl.RECURSIVE_AGGREGATION_LIMIT_DEFAULT + 10; + List expectedNodes = new ArrayList(); + + String sqlBase = "SELECT * FROM [nt:folder] as f WHERE "; + String sqlCat = sqlBase + " CONTAINS (f.*, 'cat')"; + String sqlDog = sqlBase + " CONTAINS (f.*, 'dog')"; + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + writer.write("the quick brown fox jumps over the lazy dog."); + writer.flush(); + + Node folderRoot = testRootNode.addNode("myFolder", "nt:folder"); + Node folderChild = folderRoot; + expectedNodes.add(folderChild); + + for (int i = 0; i < levelsDeep; i++) { + folderChild.addNode("0" + i + "-dummy", "nt:folder"); + folderChild = folderChild.addNode("0" + i, "nt:folder"); + expectedNodes.add(folderChild); + } + + Node file = folderChild.addNode("myFile", "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + resource.setProperty("jcr:data", session.getValueFactory() + .createBinary(new ByteArrayInputStream(out.toByteArray()))); + + testRootNode.getSession().save(); + waitForTextExtractionTasksToFinish(); + executeSQL2Query(sqlDog, expectedNodes.toArray(new Node[] {})); + + // update jcr:data + out.reset(); + writer.write("the quick brown fox jumps over the lazy cat."); + writer.flush(); + resource.setProperty("jcr:data", session.getValueFactory() + .createBinary(new ByteArrayInputStream(out.toByteArray()))); + testRootNode.getSession().save(); + waitForTextExtractionTasksToFinish(); + executeSQL2Query(sqlDog, new Node[] {}); + executeSQL2Query(sqlCat, expectedNodes.toArray(new Node[] {})); + + // replace jcr:content with unstructured + resource.remove(); + Node unstrContent = file.addNode("jcr:content", "nt:unstructured"); + Node foo = unstrContent.addNode("foo"); + foo.setProperty("text", "the quick brown fox jumps over the lazy dog."); + testRootNode.getSession().save(); + waitForTextExtractionTasksToFinish(); + executeSQL2Query(sqlDog, expectedNodes.toArray(new Node[] {})); + executeSQL2Query(sqlCat, new Node[] {}); + + // remove foo + foo.remove(); + testRootNode.getSession().save(); + waitForTextExtractionTasksToFinish(); + executeSQL2Query(sqlDog, new Node[] {}); + executeSQL2Query(sqlCat, new Node[] {}); + + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SearchIndexConsistencyCheckTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SearchIndexConsistencyCheckTest.java new file mode 100644 index 00000000000..426a81a3f02 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SearchIndexConsistencyCheckTest.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.query.lucene; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.SearchManager; +import org.apache.jackrabbit.core.TestHelper; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.query.lucene.hits.AbstractHitCollector; +import org.apache.jackrabbit.core.state.NodeState; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; + +public class SearchIndexConsistencyCheckTest extends AbstractJCRTest { + + private boolean keepRunning; + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + public void testIndexMissesNode() throws Exception { + + Session s = getHelper().getSuperuserSession(); + SearchManager searchManager = TestHelper.getSearchManager(s); + SearchIndex searchIndex = (SearchIndex) searchManager.getQueryHandler(); + + Node foo = testRootNode.addNode("foo"); + testRootNode.getSession().save(); + NodeId fooId = new NodeId(foo.getIdentifier()); + + Iterator remove = Collections.singletonList(fooId).iterator(); + Iterator add = Collections.emptyList().iterator(); + + searchIndex.updateNodes(remove, add); + + ConsistencyCheck consistencyCheck = searchIndex.runConsistencyCheck(); + List errors = consistencyCheck.getErrors(); + assertEquals("Expected 1 index consistency error", 1, errors.size()); + + ConsistencyCheckError error = errors.iterator().next(); + assertEquals("Different node was reported to be missing", error.id, fooId); + + consistencyCheck.repair(false); + + assertTrue("Index was not repaired properly", searchIndexContainsNode(searchIndex, fooId)); + + assertTrue("Consistency check still reports errors", searchIndex.runConsistencyCheck().getErrors().isEmpty()); + } + + public void testMissingNodeDoubleCheck() throws Exception { + Session s = getHelper().getSuperuserSession(); + SearchManager searchManager = TestHelper.getSearchManager(s); + SearchIndex searchIndex = (SearchIndex) searchManager.getQueryHandler(); + + Node foo = testRootNode.addNode("foo"); + testRootNode.getSession().save(); + NodeId fooId = new NodeId(foo.getIdentifier()); + + Iterator remove = Collections.singletonList(fooId).iterator(); + Iterator add = Collections.emptyList().iterator(); + + searchIndex.updateNodes(remove, add); + + ConsistencyCheck consistencyCheck = searchIndex.runConsistencyCheck(); + List errors = consistencyCheck.getErrors(); + assertEquals("Expected 1 index consistency error", 1, errors.size()); + + // now add foo to the index again so that double check finds a false positive + remove = Collections.emptyList().iterator(); + add = Collections.singletonList(new NodeState(fooId, null, null, 1, false)).iterator(); + + searchIndex.updateNodes(remove, add); + + consistencyCheck.doubleCheckErrors(); + + assertTrue("Consistency double check of missing node failed", consistencyCheck.getErrors().isEmpty()); + + } + + public void testIndexContainsUnknownNode() throws Exception { + + Session s = getHelper().getSuperuserSession(); + SearchManager searchManager = TestHelper.getSearchManager(s); + SearchIndex searchIndex = (SearchIndex) searchManager.getQueryHandler(); + + NodeId nodeId = new NodeId(0, 0); + NodeState nodeState = new NodeState(nodeId, null, null, 1, false); + + Iterator remove = Collections.emptyList().iterator(); + Iterator add = Collections.singletonList(nodeState).iterator(); + + searchIndex.updateNodes(remove, add); + + ConsistencyCheck consistencyCheck = searchIndex.runConsistencyCheck(); + List errors = consistencyCheck.getErrors(); + assertEquals("Expected 1 index consistency error", 1, errors.size()); + + ConsistencyCheckError error = errors.iterator().next(); + assertEquals("Different node was reported to be unknown", error.id, nodeId); + + consistencyCheck.repair(false); + + assertFalse("Index was not repaired properly", searchIndexContainsNode(searchIndex, nodeId)); + + assertTrue("Consistency check still reports errors", searchIndex.runConsistencyCheck().getErrors().isEmpty()); + } + + public void testUnknownNodeDoubleCheck() throws Exception { + + Session s = getHelper().getSuperuserSession(); + SearchManager searchManager = TestHelper.getSearchManager(s); + SearchIndex searchIndex = (SearchIndex) searchManager.getQueryHandler(); + + NodeId nodeId = new NodeId(0, 0); + NodeState nodeState = new NodeState(nodeId, null, null, 1, false); + + Iterator remove = Collections.emptyList().iterator(); + Iterator add = Collections.singletonList(nodeState).iterator(); + + searchIndex.updateNodes(remove, add); + + ConsistencyCheck consistencyCheck = searchIndex.runConsistencyCheck(); + List errors = consistencyCheck.getErrors(); + assertEquals("Expected 1 index consistency error", 1, errors.size()); + + // now remove the unknown node from the index again so that double check finds a false positive + remove = Collections.singletonList(nodeId).iterator(); + add = Collections.emptyList().iterator(); + + searchIndex.updateNodes(remove, add); + + consistencyCheck.doubleCheckErrors(); + + assertTrue("Consistency double check of deleted node failed", consistencyCheck.getErrors().isEmpty()); + } + + public void testIndexMissesAncestor() throws Exception { + Session s = getHelper().getSuperuserSession(); + SearchManager searchManager = TestHelper.getSearchManager(s); + SearchIndex searchIndex = (SearchIndex) searchManager.getQueryHandler(); + + Node foo = testRootNode.addNode("foo"); + Node bar = foo.addNode("bar"); + testRootNode.getSession().save(); + NodeId fooId = new NodeId(foo.getIdentifier()); + NodeId barId = new NodeId(bar.getIdentifier()); + + Iterator remove = Collections.singletonList(fooId).iterator(); + Iterator add = Collections.emptyList().iterator(); + + searchIndex.updateNodes(remove, add); + + ConsistencyCheck consistencyCheck = searchIndex.runConsistencyCheck(); + List errors = consistencyCheck.getErrors(); + + assertEquals("Expected 2 index consistency errors", 2, errors.size()); + + assertEquals("Different node was reported to have missing parent", errors.get(0).id, barId); + assertEquals("Different node was reported to be missing", errors.get(1).id, fooId); + + consistencyCheck.repair(false); + + assertTrue("Index was not repaired properly", searchIndexContainsNode(searchIndex, fooId)); + + assertTrue("Consistency check still reports errors", searchIndex.runConsistencyCheck().getErrors().isEmpty()); + } + + public void testMissingAncestorDoubleCheck() throws Exception { + + Session s = getHelper().getSuperuserSession(); + SearchManager searchManager = TestHelper.getSearchManager(s); + SearchIndex searchIndex = (SearchIndex) searchManager.getQueryHandler(); + + Node foo = testRootNode.addNode("foo"); + foo.addNode("bar"); + testRootNode.getSession().save(); + NodeId fooId = new NodeId(foo.getIdentifier()); + + Iterator remove = Collections.singletonList(fooId).iterator(); + Iterator add = Collections.emptyList().iterator(); + + searchIndex.updateNodes(remove, add); + + ConsistencyCheck consistencyCheck = searchIndex.runConsistencyCheck(); + List errors = consistencyCheck.getErrors(); + + assertEquals("Expected 2 index consistency errors", 2, errors.size()); + + remove = Collections.emptyList().iterator(); + add = Collections.singletonList(new NodeState(fooId, null, null, 1, true)).iterator(); + + searchIndex.updateNodes(remove, add); + + consistencyCheck.doubleCheckErrors(); + + assertTrue("Consistency double check of missing ancestor failed", consistencyCheck.getErrors().isEmpty()); + } + + public void testIndexContainsMultipleEntries() throws Exception { + Session s = getHelper().getSuperuserSession(); + SearchManager searchManager = TestHelper.getSearchManager(s); + SearchIndex searchIndex = (SearchIndex) searchManager.getQueryHandler(); + + Node foo = testRootNode.addNode("foo"); + testRootNode.getSession().save(); + NodeId fooId = new NodeId(foo.getIdentifier()); + + NodeState nodeState = new NodeState(fooId, null, null, 1, false); + Iterator remove = Collections.emptyList().iterator(); + Iterator add = Arrays.asList(nodeState).iterator(); + + searchIndex.updateNodes(remove, add); + + searchIndex.flush(); + + remove = Collections.emptyList().iterator(); + add = Arrays.asList(nodeState).iterator(); + + searchIndex.updateNodes(remove, add); + + ConsistencyCheck consistencyCheck = searchIndex.runConsistencyCheck(); + List errors = consistencyCheck.getErrors(); + + assertEquals("Expected 1 index consistency error", 1, errors.size()); + assertEquals("Different node was reported to be duplicate", errors.get(0).id, fooId); + + consistencyCheck.doubleCheckErrors(); + errors = consistencyCheck.getErrors(); + + assertEquals("Expected 1 index consistency error after double check", 1, errors.size()); + assertEquals("Different node was reported to be duplicate after double check", errors.get(0).id, fooId); + + consistencyCheck.repair(false); + + assertTrue("Index was not repaired properly", searchIndexContainsNode(searchIndex, fooId)); + + consistencyCheck.doubleCheckErrors(); + errors = consistencyCheck.getErrors(); + + assertTrue("Consistency double check of multiple entries failed", errors.isEmpty()); + assertTrue("Consistency check still finds errors", searchIndex.runConsistencyCheck().getErrors().isEmpty()); + + } + + /** + * Stress test on the double check mechanism + */ + public void testDoubleCheckStressTest() throws Exception { + Thread t = new Thread(new Runnable() { + private Session s = getHelper().getReadWriteSession(); + @Override + public void run() { + while (keepRunning) { + try { + Node foo = s.getRootNode().getNode(testPath).addNode("foo"); + s.save(); + foo.remove(); + s.save(); + } catch (RepositoryException e) { + System.out.println(e); + } + } + } + }); + + Session s = getHelper().getSuperuserSession(); + SearchManager searchManager = TestHelper.getSearchManager(s); + SearchIndex searchIndex = (SearchIndex) searchManager.getQueryHandler(); + + keepRunning = true; + try { + t.start(); + Thread.sleep(100); + for (int i = 100; i > 0; i--) { + final ConsistencyCheck consistencyCheck = searchIndex.runConsistencyCheck(); + consistencyCheck.doubleCheckErrors(); + final List errors = consistencyCheck.getErrors(); + assertTrue(errors.isEmpty()); + } + } finally { + keepRunning = false; + } + + } + + private boolean searchIndexContainsNode(SearchIndex searchIndex, NodeId nodeId) throws IOException { + final List docs = new ArrayList(1); + final IndexReader reader = searchIndex.getIndexReader(); + try { + IndexSearcher searcher = new IndexSearcher(reader); + try { + Query q = new TermQuery(new Term(FieldNames.UUID, nodeId.toString())); + searcher.search(q, new AbstractHitCollector() { + @Override + protected void collect(final int doc, final float score) { + docs.add(doc); + } + }); + } finally { + searcher.close(); + } + } finally { + Util.closeOrRelease(reader); + } + return !docs.isEmpty(); + + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SearchIndexTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SearchIndexTest.java new file mode 100644 index 00000000000..68ad0dea98d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SearchIndexTest.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.core.query.lucene; + +import junit.framework.TestCase; + +public class SearchIndexTest extends TestCase { + + /** + * @see JCR-3236 + */ + public void testSetAnalyzer() { + String[] analyzers = { + "org.apache.lucene.analysis.SimpleAnalyzer", + "org.apache.lucene.analysis.StopAnalyzer", + "org.apache.lucene.analysis.standard.StandardAnalyzer" }; + SearchIndex index = new SearchIndex(); + for (String analyzer : analyzers) { + index.setAnalyzer(analyzer); + assertEquals(analyzer, index.getAnalyzer()); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SlowQueryHandler.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SlowQueryHandler.java new file mode 100644 index 00000000000..4f2f6b337e4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SlowQueryHandler.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.core.query.lucene; + +import java.io.IOException; + +import org.apache.jackrabbit.core.id.NodeId; + +/** + * SlowQueryHandler implements a dummy query handler for testing + * purpose. + */ +public class SlowQueryHandler extends SearchIndex { + + private static long INITIALIZATION_DELAY = 0; + + protected void doInit() throws IOException { + // sleep for 10 seconds then try to read from the item state manager + // the repository.xml is configured with a 5 second maxIdleTime + try { + Thread.sleep(INITIALIZATION_DELAY); + } catch (InterruptedException e) { + // ignore + } + getContext().getItemStateManager().hasItemState(NodeId.randomId()); + super.doInit(); + } + + public static void setInitializationDelay(long delay) { + INITIALIZATION_DELAY = delay; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SynonymProviderTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SynonymProviderTest.java new file mode 100644 index 00000000000..cb4feb3b929 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/SynonymProviderTest.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.core.query.lucene; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; + +import org.apache.jackrabbit.core.query.AbstractQueryTest; + +/** + * SynonymProviderTest contains test cases for the + * PropertiesSynonymProvider class. + * This test assumes that the following synonyms are defined: + *
      + *
    • quick <-> fast
    • + *
    • sluggish <-> lazy
    • + *
    • ASF <-> Apache Software Foundation
    • + *
    + */ +public class SynonymProviderTest extends AbstractQueryTest { + + public void testSynonyms() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1); + n.setProperty(propertyName1, "The quick brown fox jumps over the lazy dog."); + testRootNode.save(); + executeXPathQuery(testPath + "//*[jcr:contains(., '~fast')]", new Node[]{n}); + executeXPathQuery(testPath + "//*[jcr:contains(., '~Fast')]", new Node[]{n}); + executeXPathQuery(testPath + "//*[jcr:contains(., '~quick')]", new Node[]{n}); + executeXPathQuery(testPath + "//*[jcr:contains(., '~sluggish')]", new Node[]{n}); + executeXPathQuery(testPath + "//*[jcr:contains(., '~sluGGish')]", new Node[]{n}); + executeXPathQuery(testPath + "//*[jcr:contains(., '~lazy')]", new Node[]{n}); + // check term which is not in the synonym provider + executeXPathQuery(testPath + "//*[jcr:contains(., '~brown')]", new Node[]{n}); + } + + public void testPhrase() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1); + n.setProperty(propertyName1, "Licensed to the Apache Software Foundation ..."); + testRootNode.save(); + executeXPathQuery(testPath + "//*[jcr:contains(., '~ASF')]", new Node[]{n}); + executeXPathQuery(testPath + "//*[jcr:contains(., '~asf')]", new Node[]{n}); + executeXPathQuery(testPath + "//*[jcr:contains(., 'asf')]", new Node[]{}); + } + + public void disabled_testReload() throws RepositoryException, InterruptedException { + for (int i = 0; i < 60; i++) { + Thread.sleep(1 * 1000); + executeXPathQuery(testPath + "//*[jcr:contains(., '~asf')]", new Node[]{}); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/TestAll.java new file mode 100644 index 00000000000..fffd285b1c6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/TestAll.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.core.query.lucene; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.apache.jackrabbit.core.query.lucene.hits.ArrayHitsTest; +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +/** + * Test suite that includes all testcases for the Search module. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new ConcurrentTestSuite("Search tests"); + + suite.addTestSuite(IndexingQueueTest.class); + suite.addTestSuite(DecimalConvertTest.class); + suite.addTestSuite(IndexingAggregateTest.class); + suite.addTestSuite(IndexMigrationTest.class); + suite.addTestSuite(ChainedTermEnumTest.class); + suite.addTestSuite(IndexingConfigurationImplTest.class); + suite.addTestSuite(SQL2IndexingAggregateTest.class); + suite.addTestSuite(SQL2IndexingAggregateTest2.class); + suite.addTestSuite(LazyTextExtractorFieldTest.class); + suite.addTestSuite(IndexInfosTest.class); + suite.addTestSuite(IndexingRuleTest.class); + suite.addTestSuite(TextExtractionQueryTest.class); + suite.addTestSuite(ArrayHitsTest.class); + suite.addTestSuite(IndexFormatVersionTest.class); + suite.addTestSuite(SynonymProviderTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/TextExtractionQueryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/TextExtractionQueryTest.java new file mode 100644 index 00000000000..692d7839a72 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/TextExtractionQueryTest.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.core.query.lucene; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; + +import org.apache.jackrabbit.core.query.AbstractIndexingTest; +import org.apache.jackrabbit.core.query.FulltextQueryTest; + +public class TextExtractionQueryTest extends AbstractIndexingTest { + + public void testFileContains() throws Exception { + assertFileContains("test.txt", "text/plain", + "AE502DBEA2C411DEBD340AD156D89593"); + assertFileContains("test.rtf", "application/rtf", "quick brown fox"); + } + + public void testNtFile() throws RepositoryException, IOException { + Node file = testRootNode.addNode(nodeName1, "nt:file"); + Node resource = file.addNode("jcr:content", "nt:resource"); + resource.setProperty("jcr:encoding", "UTF-8"); + resource.setProperty("jcr:mimeType", "text/plain"); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(data, "UTF-8"); + writer.write("The quick brown fox jumps over the lazy dog."); + writer.close(); + resource.setProperty("jcr:data", new ByteArrayInputStream(data.toByteArray())); + resource.setProperty("jcr:lastModified", Calendar.getInstance()); + + testRootNode.save(); + String xpath = testPath + "/*[jcr:contains(jcr:content, 'lazy')]"; + executeXPathQuery(xpath, new Node[]{file}); + } + + private void assertFileContains(String name, String type, + String... statements) throws Exception { + while (testRootNode.hasNode(nodeName1)) { + testRootNode.getNode(nodeName1).remove(); + } + Node resource = testRootNode.addNode(nodeName1, NodeType.NT_RESOURCE); + resource.setProperty("jcr:mimeType", type); + InputStream stream = FulltextQueryTest.class.getResourceAsStream(name); + try { + resource.setProperty("jcr:data", stream); + } finally { + stream.close(); + } + testRootNode.save(); + flushSearchIndex(); + for (String statement : statements) { + assertContainsQuery(statement, true); + } + } + + private void assertContainsQuery(String statement, boolean match) + throws InvalidQueryException, RepositoryException { + StringBuffer stmt = new StringBuffer(); + stmt.append("/jcr:root").append(testRoot).append("/*"); + stmt.append("[jcr:contains(., '").append(statement); + stmt.append("')]"); + + Query q = qm.createQuery(stmt.toString(), Query.XPATH); + checkResult(q.execute(), match ? 1 : 0); + + stmt = new StringBuffer(); + stmt.append("SELECT * FROM nt:base "); + stmt.append("WHERE jcr:path LIKE '").append(testRoot).append("/%' "); + stmt.append("AND CONTAINS(., '").append(statement).append("')"); + + q = qm.createQuery(stmt.toString(), Query.SQL); + checkResult(q.execute(), match ? 1 : 0); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/UtilTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/UtilTest.java new file mode 100644 index 00000000000..f3a2e49e9b4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/UtilTest.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.core.query.lucene; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import junit.framework.TestCase; +import org.apache.jackrabbit.value.ValueFactoryImpl; +import org.junit.Test; + +public class UtilTest extends TestCase { + + public void testComparableContract(){ + //The test data needs to be greater than 32 in length to trigger TimSort + //The data was obtained by running multiple runs with random sequence + //and then reverse engineered from it :) + Integer[] data = {null,25,21,5,null,23,10,19,10,null,null,10,24,null,10,null,7,11, + null,7,null,14,26,0,6,19,null,5,null,4,28,19,5,28,18,14,12,16,14,15}; + List testData = createValueArrayList(data); + Collections.sort(testData, new ValueArrayComparator()); + } + + private static List createValueArrayList(Integer[] data){ + List result = new ArrayList(data.length); + for(Integer i : data){ + Value[] r = null; + if(i != null){ + r = new Value[]{ValueFactoryImpl.getInstance().createValue(i.longValue())}; + } + result.add(r); + } + return result; + } + + private static class ValueArrayComparator implements Comparator { + @Override + public int compare(Value[] a, Value[] b) { + try { + return Util.compare(a, b); + } catch (RepositoryException e) { + throw new RuntimeException("Unable to compare values " + + Arrays.toString(a) + " and " + Arrays.toString(b), e); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/DirectoryManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/DirectoryManagerTest.java new file mode 100644 index 00000000000..5651aaef3a9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/DirectoryManagerTest.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.core.query.lucene.directory; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.io.File; + +import org.apache.jackrabbit.core.query.lucene.SearchIndex; +import org.apache.lucene.store.Directory; + +import junit.framework.TestCase; + +/** + * DirectoryManagerTest performs tests on directory manager + * implementations. + */ +public class DirectoryManagerTest extends TestCase { + + private static final Collection IMPLEMENTATIONS = Arrays.asList( + new Class[]{FSDirectoryManager.class, RAMDirectoryManager.class}); + + private static final SearchIndex INDEX = new SearchIndex(); + + private static final String TEST = "test"; + + private static final String RENAMED = "renamed"; + + static { + INDEX.setPath(new File(new File("target"), "directory-factory-test").getAbsolutePath()); + } + + protected void tearDown() throws Exception { + new File(INDEX.getPath(), TEST).delete(); + new File(INDEX.getPath(), RENAMED).delete(); + } + + public void testHasDirectory() throws Exception { + execute(new Callable(){ + public void call(DirectoryManager directoryManager) throws Exception { + Directory dir = directoryManager.getDirectory(TEST); + assertTrue(directoryManager.hasDirectory(TEST)); + dir.close(); + } + }); + } + + public void testDelete() throws Exception { + execute(new Callable(){ + public void call(DirectoryManager directoryManager) throws Exception { + directoryManager.getDirectory(TEST).close(); + directoryManager.delete(TEST); + assertFalse(directoryManager.hasDirectory(TEST)); + } + }); + } + + public void testGetDirectoryNames() throws Exception { + execute(new Callable(){ + public void call(DirectoryManager directoryManager) throws Exception { + directoryManager.getDirectory(TEST).close(); + assertTrue(Arrays.asList(directoryManager.getDirectoryNames()).contains(TEST)); + } + }); + } + + public void testRename() throws Exception { + execute(new Callable(){ + public void call(DirectoryManager directoryManager) throws Exception { + directoryManager.getDirectory(TEST).close(); + directoryManager.rename(TEST, RENAMED); + assertTrue(directoryManager.hasDirectory(RENAMED)); + assertFalse(directoryManager.hasDirectory(TEST)); + } + }); + } + + private void execute(Callable callable) throws Exception { + for (Iterator it = IMPLEMENTATIONS.iterator(); it.hasNext(); ) { + Class clazz = (Class) it.next(); + DirectoryManager dirMgr = (DirectoryManager) clazz.newInstance(); + dirMgr.init(INDEX); + try { + callable.call(dirMgr); + } finally { + dirMgr.dispose(); + } + } + } + + private interface Callable { + + public void call(DirectoryManager directoryManager) throws Exception; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/IndexInputStreamTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/IndexInputStreamTest.java new file mode 100644 index 00000000000..cb23cba1b80 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/IndexInputStreamTest.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.core.query.lucene.directory; + +import java.io.IOException; +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.util.Random; + +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.RAMDirectory; +import org.apache.lucene.store.IndexOutput; + +import junit.framework.TestCase; + +/** + * IndexInputStreamTest performs tests on {@link IndexInputStream}. + */ +public class IndexInputStreamTest extends TestCase { + + public void testIndexInputStream() throws IOException { + checkStream(0, 0); + checkStream(0, 128); + checkStream(128, 0); + checkStream(128, 128); + checkStream(127, 128); + checkStream(129, 128); + checkStream(300, 128); + } + + private void checkStream(int size, int buffer) throws IOException { + Random rand = new Random(); + byte[] data = new byte[size]; + rand.nextBytes(data); + Directory dir = new RAMDirectory(); + IndexOutput out = dir.createOutput("test"); + out.writeBytes(data, data.length); + out.close(); + InputStream in = new IndexInputStream(dir.openInput("test")); + if (buffer != 0) { + in = new BufferedInputStream(in, buffer); + } + byte[] buf = new byte[3]; + int len; + int pos = 0; + while ((len = in.read(buf)) > -1) { + for (int i = 0; i < len; i++, pos++) { + assertEquals(data[pos], buf[i]); + } + } + in.close(); + // assert length + assertEquals(data.length, pos); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/IndexOutputStreamTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/IndexOutputStreamTest.java new file mode 100644 index 00000000000..f611748858b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/IndexOutputStreamTest.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.core.query.lucene.directory; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.BufferedOutputStream; +import java.util.Random; + +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.RAMDirectory; +import org.apache.lucene.store.IndexInput; + +import junit.framework.TestCase; + +/** + * IndexOutputStreamTest performs tests on {@link IndexOutputStream}. + */ +public class IndexOutputStreamTest extends TestCase { + + public void testIndexOutputStream() throws IOException { + checkStream(0, 0); + checkStream(0, 128); + checkStream(128, 0); + checkStream(127, 128); + checkStream(128, 128); + checkStream(129, 128); + checkStream(300, 128); + } + + private void checkStream(int size, int buffer) throws IOException { + Random rand = new Random(); + byte[] data = new byte[size]; + rand.nextBytes(data); + Directory dir = new RAMDirectory(); + OutputStream out = new IndexOutputStream(dir.createOutput("test")); + if (buffer != 0) { + out = new BufferedOutputStream(out, buffer); + } + out.write(data); + out.close(); + + byte[] buf = new byte[3]; + int pos = 0; + IndexInput in = dir.openInput("test"); + for (;;) { + int len = (int) Math.min(buf.length, in.length() - pos); + in.readBytes(buf, 0, len); + for (int i = 0; i < len; i++, pos++) { + assertEquals(data[pos], buf[i]); + } + if (len == 0) { + // EOF + break; + } + } + in.close(); + + // assert length + assertEquals(data.length, pos); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/TestAll.java new file mode 100644 index 00000000000..dd010537b55 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/directory/TestAll.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.core.query.lucene.directory; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the directory module. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("Directory tests"); + + suite.addTestSuite(IndexInputStreamTest.class); + suite.addTestSuite(IndexOutputStreamTest.class); + suite.addTestSuite(DirectoryManagerTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/hits/ArrayHitsTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/hits/ArrayHitsTest.java new file mode 100644 index 00000000000..9cbfec36bbb --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/query/lucene/hits/ArrayHitsTest.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.core.query.lucene.hits; + +import junit.framework.TestCase; + +public class ArrayHitsTest extends TestCase { + + public void testSkipToDocumentNumberGreaterThanLastMatch() throws Exception { + ArrayHits hits = new ArrayHits(); + hits.set(1); + int doc = hits.skipTo(2); + assertEquals(-1, doc); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/AbstractRetentionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/AbstractRetentionTest.java new file mode 100644 index 00000000000..63d7ad678ff --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/AbstractRetentionTest.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.core.retention; + +import javax.jcr.RepositoryException; +import javax.jcr.retention.RetentionPolicy; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AbstractAccessControlTest... + */ +public abstract class AbstractRetentionTest extends org.apache.jackrabbit.test.api.retention.AbstractRetentionTest { + + @Override + protected RetentionPolicy getApplicableRetentionPolicy() throws NotExecutableException, RepositoryException { + return getApplicableRetentionPolicy("retentionPolicyName"); + } + + protected RetentionPolicy getApplicableRetentionPolicy(String jcrName) throws NotExecutableException, RepositoryException { + // TODO: move to repositoryStub/helper and adjust accordingly + return RetentionPolicyImpl.createRetentionPolicy(jcrName, superuser); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/HoldTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/HoldTest.java new file mode 100644 index 00000000000..b618720bcdb --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/HoldTest.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.core.retention; + +import javax.jcr.RepositoryException; +import javax.jcr.retention.Hold; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation specific hold tests. + */ +public class HoldTest extends AbstractRetentionTest { + + private static Logger log = LoggerFactory.getLogger(HoldTest.class); + + @Override + protected void tearDown() throws Exception { + try { + superuser.refresh(false); + Hold[] holds = retentionMgr.getHolds(testNodePath); + for (Hold hold : holds) { + retentionMgr.removeHold(testNodePath, hold); + } + superuser.save(); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + + super.tearDown(); + } + + public void testAddHoldTwice() throws RepositoryException, NotExecutableException { + Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + + try { + retentionMgr.addHold(testNodePath, getHoldName(), true); + fail("cannot add the same hold twice"); + } catch (RepositoryException e) { + // success + } + + superuser.save(); + try { + retentionMgr.addHold(testNodePath, getHoldName(), true); + fail("cannot add the same hold twice"); + } catch (RepositoryException e) { + // success + } + } + + public void testRemoveInvalidHold() throws RepositoryException, NotExecutableException { + final Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + + try { + Hold invalidH = new Hold() { + public boolean isDeep() throws RepositoryException { + return h.isDeep(); + } + public String getName() throws RepositoryException { + return h.getName(); + } + }; + retentionMgr.removeHold(testNodePath, invalidH); + fail("An invalid hold impl. should not be removable."); + + } catch (RepositoryException e) { + // success + } + } + + public void testRemoveInvalidHold2() throws RepositoryException, NotExecutableException { + final Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + + try { + Hold invalidH = new Hold() { + public boolean isDeep() throws RepositoryException { + return h.isDeep(); + } + public String getName() throws RepositoryException { + return "anyName"; + } + }; + retentionMgr.removeHold(testNodePath, invalidH); + fail("An invalid hold impl. should not be removable."); + + } catch (RepositoryException e) { + // success + } + } + + public void testRemoveInvalidHold3() throws RepositoryException, NotExecutableException { + final Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + + try { + Hold invalidH = new Hold() { + public boolean isDeep() throws RepositoryException { + return !h.isDeep(); + } + public String getName() throws RepositoryException { + return h.getName(); + } + }; + retentionMgr.removeHold(testNodePath, invalidH); + fail("An invalid hold impl. should not be removable."); + + } catch (RepositoryException e) { + // success + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/RetentionPolicyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/RetentionPolicyTest.java new file mode 100644 index 00000000000..30b2fc7aa3b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/RetentionPolicyTest.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.core.retention; + +import javax.jcr.retention.RetentionPolicy; + +import javax.jcr.RepositoryException; + + +/** + * RetentionPolicyTest... + */ +public class RetentionPolicyTest extends AbstractRetentionTest { + + public void testSetInvalidRetentionPolicy() { + try { + RetentionPolicy invalidRPolicy = new RetentionPolicy() { + public String getName() throws RepositoryException { + return "anyName"; + } + }; + retentionMgr.setRetentionPolicy(testNodePath, invalidRPolicy); + fail("Setting an invalid retention policy must fail."); + } catch (RepositoryException e) { + // success + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/TestAll.java new file mode 100644 index 00000000000..43b5db4ec18 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/retention/TestAll.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.core.retention; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package org.apache.jackrabbit.core.retention. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.core.retention tests"); + + suite.addTestSuite(HoldTest.class); + suite.addTestSuite(RetentionPolicyTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/AccessManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/AccessManagerTest.java new file mode 100644 index 00000000000..33003ff6612 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/AccessManagerTest.java @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.ItemImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NoSuchWorkspaceException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.List; + +/** + * AccessManagerTest... + */ +public class AccessManagerTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(AccessManagerTest.class); + + private static AccessManager getAccessManager(Session session) throws NotExecutableException { + if (session instanceof SessionImpl) { + return ((SessionImpl) session).getAccessManager(); + } else { + throw new NotExecutableException(); + } + } + + private static ItemId getItemId(Item item) throws NotExecutableException { + if (item instanceof ItemImpl) { + return ((ItemImpl)item).getId(); + } else { + throw new NotExecutableException(); + } + } + + // TODO: add tests for new methods + // TODO: add specific tests for 'AC-read/modify' privileges + + public void testCheckPermissionReadOnlySession() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + AccessManager acMgr = getAccessManager(s); + + NodeId id = (NodeId) getItemId(s.getItem(testRootNode.getPath())); + + acMgr.checkPermission(id, AccessManager.READ); + try { + acMgr.checkPermission(id, AccessManager.WRITE); + fail(); + } catch (AccessDeniedException e) { + // success + } + + try { + acMgr.checkPermission(id, AccessManager.WRITE | AccessManager.REMOVE); + fail(); + } catch (AccessDeniedException e) { + // success + } + } finally { + s.logout(); + } + } + + /** + */ + public void testCheckPermissionWithNoPermissionFlag() throws RepositoryException, NotExecutableException { + AccessManager acMgr = getAccessManager(superuser); + + NodeId id = (NodeId) getItemId(superuser.getItem(testRootNode.getPath())); + // NOTE: backwards compatibility. + // for deprecated method: invalid perm-flags will be ignored + acMgr.checkPermission(id, AccessManager.READ - 1); + } + + public void testCheckPermissionWithInvalidPermission() throws RepositoryException, NotExecutableException { + AccessManager acMgr = getAccessManager(superuser); + + NodeId id = (NodeId) getItemId(superuser.getItem(testRootNode.getPath())); + // NOTE: backwards compatibility. + // for deprecated method: invalid perm-flags will be ignored + acMgr.checkPermission(id, AccessManager.READ | AccessManager.WRITE | AccessManager.REMOVE + 1); + } + + public void testCheckPermissionWithUnknowId() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + NodeId id = NodeId.randomId(); + try { + AccessManager acMgr = getAccessManager(s); + acMgr.checkPermission(id, AccessManager.READ); + fail("AccessManager.checkPermission should throw ItemNotFoundException with a random (unknown) item id."); + } catch (ItemNotFoundException e) { + // ok + } finally { + s.logout(); + } + } + + public void testIsGranted() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + AccessManager acMgr = getAccessManager(s); + + NodeId id = (NodeId) getItemId(s.getItem(testRootNode.getPath())); + assertTrue(acMgr.isGranted(id, AccessManager.READ)); + assertFalse(acMgr.isGranted(id, AccessManager.WRITE)); + assertFalse(acMgr.isGranted(id, AccessManager.WRITE | AccessManager.REMOVE)); + } finally { + s.logout(); + } + } + + public void testIsGrantedOnProperty() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + AccessManager acMgr = getAccessManager(s); + + PropertyId id = (PropertyId) getItemId(testRootNode.getProperty(jcrPrimaryType)); + + assertTrue(acMgr.isGranted(id, AccessManager.READ)); + assertFalse(acMgr.isGranted(id, AccessManager.WRITE)); + assertFalse(acMgr.isGranted(id, AccessManager.WRITE | AccessManager.REMOVE)); + } finally { + s.logout(); + } + } + + public void testIsGrantedOnNewNode() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadWriteSession(); + try { + AccessManager acMgr = getAccessManager(s); + + Node newNode = ((Node) s.getItem(testRoot)).addNode(nodeName2, testNodeType); + NodeId id = (NodeId) getItemId(newNode); + + assertTrue(acMgr.isGranted(id, AccessManager.READ)); + assertTrue(acMgr.isGranted(id, AccessManager.WRITE)); + assertTrue(acMgr.isGranted(id, AccessManager.WRITE | AccessManager.REMOVE)); + } finally { + s.logout(); + } + } + + public void testCanAccess() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + String wspName = s.getWorkspace().getName(); + + assertTrue(getAccessManager(s).canAccess(wspName)); + } finally { + s.logout(); + } + } + + public void testCanAccessAllAvailable() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + String[] wspNames = s.getWorkspace().getAccessibleWorkspaceNames(); + for (String wspName : wspNames) { + assertTrue(getAccessManager(s).canAccess(wspName)); + } + } finally { + s.logout(); + } + } + + public void testCanAccessDeniedWorkspace() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + Set allAccessibles = new HashSet(Arrays.asList(superuser.getWorkspace().getAccessibleWorkspaceNames())); + Set sWorkspaceNames = new HashSet(Arrays.asList(s.getWorkspace().getAccessibleWorkspaceNames())); + + if (!allAccessibles.removeAll(sWorkspaceNames) || allAccessibles.isEmpty()) { + throw new NotExecutableException("No workspace name found that exists but is not accessible for ReadOnly session."); + } + + String notAccessibleName = allAccessibles.iterator().next(); + assertFalse(getAccessManager(s).canAccess(notAccessibleName)); + } finally { + s.logout(); + } + } + + public void testCanAccessNotExistingWorkspace() throws RepositoryException, NotExecutableException { + Session s = getHelper().getReadOnlySession(); + try { + List all = Arrays.asList(s.getWorkspace().getAccessibleWorkspaceNames()); + String testName = "anyWorkspace"; + int i = 0; + while (all.contains(testName)) { + testName = "anyWorkspace" + i; + i++; + } + assertFalse(getAccessManager(s).canAccess(testName)); + } catch (NoSuchWorkspaceException e) { + // fine as well. + } finally { + s.logout(); + } + } + + public void testIsGrantedWithRelativePath() throws NotExecutableException { + AccessManager acMgr = getAccessManager(superuser); + Path p = PathFactoryImpl.getInstance().create(NameConstants.JCR_DATA); + try { + acMgr.isGranted(p, Permission.READ); + fail("calling AccessManager.isGranted(Path, int) with relative path must fail."); + } catch (RepositoryException e) { + // success + } + + try { + acMgr.isGranted(p, NameConstants.JCR_CREATED, Permission.READ); + fail("calling AccessManager.isGranted(Path, int) with relative path must fail."); + } catch (RepositoryException e) { + // success + } + } + + public void testIsGrantedPathToNonExistingItem() throws NotExecutableException, RepositoryException { + AccessManager acMgr = getAccessManager(superuser); + Path p = PathFactoryImpl.getInstance().getRootPath(); + + // existing node-path + assertTrue(acMgr.isGranted(p, Permission.ALL)); + // not existing property: + assertTrue(acMgr.isGranted(p, NameConstants.JCR_CREATED, Permission.ALL)); + // existing property + assertTrue(acMgr.isGranted(p, NameConstants.JCR_PRIMARYTYPE, Permission.ALL)); + } + + public void testIsGrantedReadOnlySession() throws NotExecutableException, RepositoryException { + Session s = getHelper().getReadOnlySession(); + try { + AccessManager acMgr = getAccessManager(s); + Path p = PathFactoryImpl.getInstance().getRootPath(); + + // existing node-path + assertTrue(acMgr.isGranted(p, Permission.READ)); + // not existing property: + assertTrue(acMgr.isGranted(p, NameConstants.JCR_CREATED, Permission.READ)); + + // existing node-path + assertFalse(acMgr.isGranted(p, Permission.ALL)); + // not existing property: + assertFalse(acMgr.isGranted(p, NameConstants.JCR_CREATED, Permission.ALL)); + } finally { + s.logout(); + } + } + + public void testCanReadPathId() throws Exception { + Session s = getHelper().getReadOnlySession(); + try { + AccessManager acMgr = getAccessManager(s); + + ItemId id = ((NodeImpl) testRootNode).getId(); + Path path = ((NodeImpl) testRootNode).getPrimaryPath(); + assertTrue(acMgr.canRead(null, id)); + assertTrue(acMgr.canRead(path, null)); + assertTrue(acMgr.canRead(path, id)); + + id = ((PropertyImpl) testRootNode.getProperty(jcrPrimaryType)).getId(); + path = ((PropertyImpl) testRootNode.getProperty(jcrPrimaryType)).getPrimaryPath(); + assertTrue(acMgr.canRead(null, id)); + assertTrue(acMgr.canRead(path, null)); + assertTrue(acMgr.canRead(path, id)); + + } finally { + s.logout(); + } + + } + + public void testCanReadNewId() throws Exception { + Session s = getHelper().getReadOnlySession(); + try { + NodeImpl n = (NodeImpl) testRootNode.addNode(nodeName1); + PropertyImpl p = (PropertyImpl) n.setProperty(propertyName1, "somevalue"); + try { + AccessManager acMgr = getAccessManager(s); + acMgr.canRead(null, n.getId()); + fail("expected repositoryexception"); + } catch (RepositoryException e) { + // success + } + try { + AccessManager acMgr = getAccessManager(s); + acMgr.canRead(null, p.getId()); + fail("expected repositoryexception"); + } catch (RepositoryException e) { + // success + } + } finally { + s.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/TestAll.java new file mode 100644 index 00000000000..5b40e78434b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/TestAll.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.core.security; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all security test. + * + * @return a Test suite that executes all security test. + */ + public static Test suite() { + TestSuite suite = new TestSuite("core.security tests"); + + suite.addTestSuite(AccessManagerTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/TestPrincipal.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/TestPrincipal.java new file mode 100644 index 00000000000..aadf2c1c68d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/TestPrincipal.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.core.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.Principal; + +/** + * TestPrincipal... + * + * // TODO retrieve principal-Impl and test-name from Configuration + */ +public class TestPrincipal implements Principal { + + private static Logger log = LoggerFactory.getLogger(TestPrincipal.class); + + private final String name; + + public TestPrincipal(String name) { + this.name = name; + } + public String getName() { + return name; + } + + public int hashCode() { + return name == null ? 0 : name.hashCode(); + } + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Principal) { + String otherName = ((Principal)obj).getName(); + return (name == null) ? otherName == null : name.equals(otherName); + } + return false; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentialsTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentialsTest.java new file mode 100644 index 00000000000..de71bc122ae --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/CryptedSimpleCredentialsTest.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.core.security.authentication; + +import javax.jcr.SimpleCredentials; +import java.security.NoSuchAlgorithmException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; +import org.apache.jackrabbit.util.Text; + +/** + * CryptedSimpleCredentialsTest... + */ +public class CryptedSimpleCredentialsTest extends TestCase { + + private final String userID = "anyUserID"; + private final String pw = "somePw"; + + private SimpleCredentials sCreds; + private List cCreds = new ArrayList(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + sCreds = new SimpleCredentials(userID, pw.toCharArray()); + // build crypted credentials from the simple credentials + CryptedSimpleCredentials cc = new CryptedSimpleCredentials(sCreds); + cCreds.add(cc); + // build from uid/pw + cCreds.add(new CryptedSimpleCredentials(userID, pw)); + // build from uid and crypted pw + cCreds.add(new CryptedSimpleCredentials(userID, cc.getPassword())); + } + + public void testSimpleMatch() throws NoSuchAlgorithmException, UnsupportedEncodingException { + for (CryptedSimpleCredentials cc : cCreds) { + assertTrue(cc.matches(sCreds)); + } + } + + public void testUserIDMatchesCaseInsensitive() throws Exception { + String uid = userID.toUpperCase(); + for (CryptedSimpleCredentials cc : cCreds) { + assertTrue(cc.matches(new SimpleCredentials(uid, pw.toCharArray()))); + } + + uid = userID.toLowerCase(); + for (CryptedSimpleCredentials cc : cCreds) { + assertTrue(cc.matches(new SimpleCredentials(uid, pw.toCharArray()))); + } + } + + public void testGetUserID() { + for (CryptedSimpleCredentials cc : cCreds) { + assertEquals(userID, cc.getUserID()); + } + } + + public void testGetPassword() throws NoSuchAlgorithmException, UnsupportedEncodingException { + // build crypted credentials from the simple credentials + CryptedSimpleCredentials cc = new CryptedSimpleCredentials(userID, pw); + assertFalse(pw.equals(cc.getPassword())); + + // build from uid and crypted pw + CryptedSimpleCredentials cc2 = new CryptedSimpleCredentials(userID, cc.getPassword()); + assertFalse(pw.equals(cc2.getPassword())); + + assertEquals(cc.getPassword(), cc2.getPassword()); + + CryptedSimpleCredentials cc3 = new CryptedSimpleCredentials(sCreds); + assertFalse(pw.equals(cc3.getPassword())); + assertFalse(cc.getPassword().equals(cc3.getPassword())); + } + + public void testGetPassword2() throws NoSuchAlgorithmException, UnsupportedEncodingException { + CryptedSimpleCredentials prev = cCreds.get(0); + // build crypted credentials from the uid and the crypted pw contained + // in simple credentials -> simple-c-password must be treated plain-text + SimpleCredentials sc = new SimpleCredentials(userID, prev.getPassword().toCharArray()); + CryptedSimpleCredentials diff = new CryptedSimpleCredentials(sc); + + assertFalse(prev.getPassword().equals(diff.getPassword())); + assertFalse(String.valueOf(sc.getPassword()).equals(diff.getPassword())); + } + + public void testGetAlgorithm() { + CryptedSimpleCredentials prev = null; + for (CryptedSimpleCredentials cc : cCreds) { + assertNotNull(cc.getAlgorithm()); + if (prev != null) { + assertEquals(prev.getAlgorithm(), cc.getAlgorithm()); + } + prev = cc; + } + } + + public void testPasswordMatch() throws NoSuchAlgorithmException, UnsupportedEncodingException { + // simple credentials containing the crypted pw must not match. + SimpleCredentials sc = new SimpleCredentials(userID, cCreds.get(0).getPassword().toCharArray()); + for (CryptedSimpleCredentials cc : cCreds) { + assertFalse(cc.matches(sc)); + } + + // simple credentials containing different pw must not match. + SimpleCredentials sc2 = new SimpleCredentials(userID, "otherPw".toCharArray()); + for (CryptedSimpleCredentials cc : cCreds) { + assertFalse(cc.matches(sc2)); + } + + // simple credentials with pw in digested form must not match. + SimpleCredentials sc3 = new SimpleCredentials(userID, "{unknown}somePw".toCharArray()); + for (CryptedSimpleCredentials cc : cCreds) { + assertFalse(cc.matches(sc3)); + } + + // simple credentials with pw with different digest must not match + SimpleCredentials sc4 = new SimpleCredentials(userID, ("{md5}"+Text.digest("md5", pw.getBytes("UTF-8"))).toCharArray()); + for (CryptedSimpleCredentials cc : cCreds) { + assertFalse(cc.matches(sc4)); + } + } + + public void testUserIdMatch() throws NoSuchAlgorithmException, UnsupportedEncodingException { + // simple credentials containing a different uid must not match + SimpleCredentials sc = new SimpleCredentials("another", pw.toCharArray()); + for (CryptedSimpleCredentials cc : cCreds) { + assertFalse(cc.matches(sc)); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/DefaultLoginModuleTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/DefaultLoginModuleTest.java new file mode 100644 index 00000000000..8380ab939f9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/DefaultLoginModuleTest.java @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authentication; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.ConfigurationEntityResolver; +import org.apache.jackrabbit.core.config.ConfigurationErrorHandler; +import org.apache.jackrabbit.core.config.ConfigurationException; +import org.apache.jackrabbit.core.config.LoginModuleConfig; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.config.RepositoryConfigurationParser; +import org.apache.jackrabbit.core.security.authentication.token.TokenBasedAuthentication; +import org.apache.jackrabbit.core.security.principal.FallbackPrincipalProvider; +import org.apache.jackrabbit.core.security.principal.ProviderRegistryImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.StringReader; +import java.util.Properties; + +/** + * DefaultLoginModuleTest... + */ +public class DefaultLoginModuleTest extends AbstractJCRTest { + + private static final String DEFAULT_CONFIG = + "" + + "\n" + + " \n" + + " \n" + + "" + + ""; + + private static final String DISABLE_TOKEN_CONFIG = + "" + + "\n" + + " \n" + + " \n" + + " \n" + + "" + + ""; + + private SimpleCredentials simpleCredentials = new SimpleCredentials("admin", "admin".toCharArray()); + private Session securitySession; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + RepositoryConfig rc = ((RepositoryImpl) superuser.getRepository()).getConfig(); + String workspaceName = rc.getSecurityConfig().getSecurityManagerConfig().getWorkspaceName(); + if (workspaceName == null) { + workspaceName = rc.getDefaultWorkspaceName(); + } + securitySession = getHelper().getSuperuserSession(workspaceName); + } + + @Override + protected void cleanUp() throws Exception { + if (securitySession != null && securitySession.isLive()) { + securitySession.logout(); + } + super.cleanUp(); + } + + public void testSimpleCredentialsLogin() throws Exception { + AuthContext ac = getAuthContext(simpleCredentials, DEFAULT_CONFIG); + ac.login(); + ac.logout(); + } + + public void testSimpleCredentialsLoginLogout() throws Exception { + AuthContext ac = getAuthContext(simpleCredentials, DEFAULT_CONFIG); + ac.login(); + + Subject subject = ac.getSubject(); + assertFalse(subject.getPrincipals().isEmpty()); + assertFalse(subject.getPublicCredentials().isEmpty()); + assertFalse(subject.getPublicCredentials(SimpleCredentials.class).isEmpty()); + + ac.logout(); + assertTrue(subject.getPrincipals().isEmpty()); + assertTrue(subject.getPublicCredentials().isEmpty()); + assertTrue(subject.getPublicCredentials(SimpleCredentials.class).isEmpty()); + } + + public void testTokenCredentialsLoginLogout() throws Exception { + simpleCredentials.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + try { + // login with simple credentials forcing token creation. + AuthContext ac = getAuthContext(simpleCredentials, DEFAULT_CONFIG); + ac.login(); + + Subject subject = ac.getSubject(); + + assertFalse(subject.getPrincipals().isEmpty()); + assertFalse(subject.getPublicCredentials().isEmpty()); + assertFalse(subject.getPublicCredentials(SimpleCredentials.class).isEmpty()); + assertFalse(subject.getPublicCredentials(TokenCredentials.class).isEmpty()); + assertEquals(2, subject.getPublicCredentials(Credentials.class).size()); + + TokenCredentials tokenCredentials = subject.getPublicCredentials(TokenCredentials.class).iterator().next(); + + ac.logout(); + + // second login with token credentials + ac = getAuthContext(tokenCredentials, DEFAULT_CONFIG); + ac.login(); + + subject = ac.getSubject(); + + assertFalse(subject.getPrincipals().isEmpty()); + assertFalse(subject.getPublicCredentials().isEmpty()); + assertFalse(subject.getPublicCredentials(SimpleCredentials.class).isEmpty()); + assertFalse(subject.getPublicCredentials(TokenCredentials.class).isEmpty()); + assertEquals(2, subject.getPublicCredentials(Credentials.class).size()); + + ac.logout(); + assertTrue(subject.getPrincipals().isEmpty()); + assertTrue(subject.getPublicCredentials().isEmpty()); + assertTrue(subject.getPublicCredentials(SimpleCredentials.class).isEmpty()); + assertTrue(subject.getPublicCredentials(TokenCredentials.class).isEmpty()); + } finally { + simpleCredentials.removeAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE); + } + } + + public void testDisabledTokenCredentials() throws Exception { + simpleCredentials.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + try { + AuthContext ac = getAuthContext(simpleCredentials, DISABLE_TOKEN_CONFIG); + ac.login(); + + Subject subject = ac.getSubject(); + + assertFalse(subject.getPrincipals().isEmpty()); + assertFalse(subject.getPublicCredentials().isEmpty()); + assertFalse(subject.getPublicCredentials(SimpleCredentials.class).isEmpty()); + assertTrue(subject.getPublicCredentials(TokenCredentials.class).isEmpty()); + assertEquals(1, subject.getPublicCredentials(Credentials.class).size()); + + ac.logout(); + } finally { + simpleCredentials.removeAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE); + } + } + + public void testDisabledTokenCredentials2() throws Exception { + simpleCredentials.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + try { + AuthContext ac = getAuthContext(simpleCredentials, DEFAULT_CONFIG); + ac.login(); + Subject subj = ac.getSubject(); + assertFalse(subj.getPublicCredentials(SimpleCredentials.class).isEmpty()); + assertFalse(subj.getPublicCredentials(TokenCredentials.class).isEmpty()); + + TokenCredentials tokenCredentials = subj.getPublicCredentials(TokenCredentials.class).iterator().next(); + ac.logout(); + + // test login with token credentials + ac = getAuthContext(tokenCredentials, DEFAULT_CONFIG); + ac.login(); + ac.logout(); + + // test login with token credentials if token-auth is disabled. + try { + ac = getAuthContext(tokenCredentials, DISABLE_TOKEN_CONFIG); + ac.login(); + ac.logout(); + fail(); + } catch (LoginException e) { + // success + } + + } finally { + simpleCredentials.removeAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE); + } + } + + public void testTokenConfigurationWithJaas() throws Exception { + // define the location of the JAAS configuration + System.setProperty( + "java.security.auth.login.config", + "target/test-classes/jaas.config"); + + simpleCredentials.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + try { + AuthContext ac = getJAASAuthContext(simpleCredentials, "defaultLoginModuleTest"); + ac.login(); + + Subject subject = ac.getSubject(); + + assertFalse(subject.getPrincipals().isEmpty()); + assertFalse(subject.getPublicCredentials().isEmpty()); + assertFalse(subject.getPublicCredentials(SimpleCredentials.class).isEmpty()); + + assertTrue(subject.getPublicCredentials(TokenCredentials.class).isEmpty()); + + assertEquals(1, subject.getPublicCredentials(Credentials.class).size()); + + ac.logout(); + } finally { + simpleCredentials.removeAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE); + } + } + + private AuthContext getAuthContext(Credentials creds, String config) throws RepositoryException { + CallbackHandler ch = new CallbackHandlerImpl(creds, + securitySession, new ProviderRegistryImpl(new FallbackPrincipalProvider()), + "admin", "anonymous"); + return new LocalAuthContext(getLoginModuleConfig(config), ch, null); + } + + private AuthContext getJAASAuthContext(Credentials creds, String appName) { + CallbackHandler ch = new CallbackHandlerImpl(creds, + securitySession, new ProviderRegistryImpl(new FallbackPrincipalProvider()), + "admin", "anonymous"); + return new JAASAuthContext(appName, ch, null); + } + + private static LoginModuleConfig getLoginModuleConfig(String config) throws ConfigurationException { + return new RepositoryConfigurationParser(new Properties()).parseLoginModuleConfig(parseXML(new InputSource(new StringReader(config)), false)); + } + + private static Element parseXML(InputSource xml, boolean validate) throws ConfigurationException { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(validate); + DocumentBuilder builder = factory.newDocumentBuilder(); + if (validate) { + builder.setErrorHandler(new ConfigurationErrorHandler()); + } + builder.setEntityResolver(ConfigurationEntityResolver.INSTANCE); + Document document = builder.parse(xml); + return document.getDocumentElement(); + } catch (ParserConfigurationException e) { + throw new ConfigurationException("Unable to create configuration XML parser", e); + } catch (SAXParseException e) { + throw new ConfigurationException("Configuration file syntax error. (Line: " + e.getLineNumber() + " Column: " + e.getColumnNumber() + ")", e); + } catch (SAXException e) { + throw new ConfigurationException("Configuration file syntax error. ", e); + } catch (IOException e) { + throw new ConfigurationException("Configuration file could not be read.", e); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/GuestLoginTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/GuestLoginTest.java new file mode 100644 index 00000000000..270c61f1387 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/GuestLoginTest.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.core.security.authentication; + +import javax.jcr.GuestCredentials; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** GuestLoginTest... */ +public class GuestLoginTest extends AbstractJCRTest { + + private Session guest; + + protected void setUp() throws Exception { + super.setUp(); + guest = getHelper().getRepository().login(new GuestCredentials()); + } + + protected void tearDown() throws Exception { + if (guest != null) { + guest.logout(); + } + super.tearDown(); + } + + /** + * Implementation specific test: userID must never be null. + * + * @throws RepositoryException + */ + public void testUserID() throws RepositoryException { + assertNotNull(guest.getUserID()); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/LoginModuleTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/LoginModuleTest.java new file mode 100644 index 00000000000..c76199b1810 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/LoginModuleTest.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.core.security.authentication; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.core.security.TestPrincipal; +import org.apache.jackrabbit.core.security.principal.FallbackPrincipalProvider; +import org.apache.jackrabbit.core.security.principal.ProviderRegistryImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * LoginModuleTest checks if multiple login modules are properly + * handled. More specifically, this test case sets up a configuration with + * two login modules: + *
      + *
    • module 1: required. This module will always authenticate successfully
    • + *
    • module 2: sufficient. This module will always indicate that it should be ignored.
    • + *
    + * See also JCR-2671. + */ +public class LoginModuleTest extends AbstractJCRTest { + + private static final String APP_NAME = LoginModuleTest.class.getName(); + + public void testMultipleModules() throws Exception { + + CallbackHandler ch = new CallbackHandlerImpl(new SimpleCredentials("user", "pass".toCharArray()), + superuser, new ProviderRegistryImpl(new FallbackPrincipalProvider()), + "admin", "anonymous"); + LoginContext context = new LoginContext( + APP_NAME, new Subject(), ch, new TestConfiguration()); + context.login(); + assertFalse("no principal set", context.getSubject().getPrincipals().isEmpty()); + } + + static class TestConfiguration extends Configuration { + + @Override + public void refresh() { + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return new AppConfigurationEntry[] { + new TestAppConfigurationEntry(AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, false), + new TestAppConfigurationEntry(AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, true) + }; + } + } + + static class TestAppConfigurationEntry extends AppConfigurationEntry { + + private static final Map IGNORE = new HashMap(); + + private static final Map EMPTY = Collections.emptyMap(); + + static { + IGNORE.put("ignore", "true"); + } + + public TestAppConfigurationEntry(LoginModuleControlFlag controlFlag, + boolean ignore) { + super(TestLoginModule.class.getName(), controlFlag, ignore ? IGNORE : EMPTY); + } + } + + public static class TestLoginModule extends AbstractLoginModule { + + private boolean ignore = false; + + @Override + protected void doInit(CallbackHandler callbackHandler, + Session session, + Map options) throws LoginException { + if (options.containsKey("ignore")) { + ignore = true; + } + } + + @Override + protected boolean impersonate(Principal principal, + Credentials credentials) + throws RepositoryException, LoginException { + return false; + } + + @Override + protected Authentication getAuthentication(Principal principal, + Credentials creds) + throws RepositoryException { + if (ignore) { + return null; + } else { + return new Authentication() { + public boolean canHandle(Credentials credentials) { + return true; + } + + public boolean authenticate(Credentials credentials) + throws RepositoryException { + return true; + } + }; + } + } + + @Override + protected Principal getPrincipal(Credentials credentials) { + if (ignore) { + return null; + } else { + return new TestPrincipal(((SimpleCredentials) credentials).getUserID()); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/NullLoginTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/NullLoginTest.java new file mode 100644 index 00000000000..207b4689b7c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/NullLoginTest.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.core.security.authentication; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** NullLoginTest... */ +public class NullLoginTest extends AbstractJCRTest { + + public void testNullLogin() throws RepositoryException { + Session s = getHelper().getRepository().login(); + Session s2 = getHelper().getRepository().login(null, null); + try { + assertNotNull(s.getUserID()); + assertEquals(s.getUserID(), s2.getUserID()); + assertEquals(s.getWorkspace().getName(), s2.getWorkspace().getName()); + } finally { + s.logout(); + s2.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/SimpleCredentialsAuthenticationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/SimpleCredentialsAuthenticationTest.java new file mode 100644 index 00000000000..7917a0ff091 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/SimpleCredentialsAuthenticationTest.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.core.security.authentication; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +import java.security.Principal; +import java.util.Iterator; + +/** + * SimpleCredentialsAuthenticationTest... + */ +public class SimpleCredentialsAuthenticationTest extends AbstractJCRTest { + + private static final Credentials creds = new Credentials(){}; + private static final SimpleCredentials simpleAA = new SimpleCredentials("a", "a".toCharArray()); + private static final SimpleCredentials simpleBB = new SimpleCredentials("b", "b".toCharArray()); + private static final SimpleCredentials simpleAB = new SimpleCredentials("a", "b".toCharArray()); + private static final SimpleCredentials simpleNull = new SimpleCredentials(null, new char[0]); + private static final SimpleCredentials simpleEmpty = new SimpleCredentials("", new char[0]); + + private User user; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (superuser instanceof JackrabbitSession) { + String userID = superuser.getUserID(); + user = (User) ((JackrabbitSession) superuser).getUserManager().getAuthorizable(userID); + } else { + throw new NotExecutableException(); + } + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testCanHandle() throws RepositoryException { + Authentication a = new SimpleCredentialsAuthentication(user); + assertFalse(a.canHandle(null)); + assertFalse(a.canHandle(creds)); + assertTrue(a.canHandle(simpleEmpty)); + assertTrue(a.canHandle(simpleNull)); + + + a = new SimpleCredentialsAuthentication(new DummyUserImpl(null)); + assertFalse(a.canHandle(null)); + assertFalse(a.canHandle(creds)); + assertFalse(a.canHandle(simpleEmpty)); + + a = new SimpleCredentialsAuthentication(new DummyUserImpl(creds)); + assertFalse(a.canHandle(null)); + assertFalse(a.canHandle(creds)); + assertFalse(a.canHandle(simpleEmpty)); + + a = new SimpleCredentialsAuthentication(new DummyUserImpl(simpleAA)); + assertFalse(a.canHandle(null)); + assertFalse(a.canHandle(creds)); + assertTrue(a.canHandle(simpleEmpty)); + assertTrue(a.canHandle(simpleNull)); + assertTrue(a.canHandle(simpleAB)); + } + + public void testAuthenticate() throws RepositoryException { + Authentication a = new SimpleCredentialsAuthentication(user); + assertFalse(a.authenticate(simpleEmpty)); + assertFalse(a.authenticate(simpleNull)); + assertFalse(a.authenticate(simpleAA)); + + a = new SimpleCredentialsAuthentication(new DummyUserImpl(null)); + assertFalse(a.authenticate(simpleEmpty)); + assertFalse(a.authenticate(simpleAA)); + + a = new SimpleCredentialsAuthentication(new DummyUserImpl(creds)); + assertFalse(a.authenticate(simpleEmpty)); + assertFalse(a.authenticate(simpleAA)); + + a = new SimpleCredentialsAuthentication(new DummyUserImpl(simpleAA)); + assertFalse(a.authenticate(simpleEmpty)); + assertFalse(a.authenticate(simpleBB)); + assertFalse(a.authenticate(simpleAB)); + assertTrue(a.authenticate(simpleAA)); + } + + //-------------------------------------------------------------------------- + /** + * Internal class used for tests. + */ + private class DummyUserImpl implements User { + + private final Credentials creds; + + private DummyUserImpl(Credentials creds) { + this.creds = creds; + } + + public boolean isAdmin() { + return false; + } + + public boolean isSystemUser() { + return false; + } + + public Credentials getCredentials() throws RepositoryException { + return creds; + } + + public Impersonation getImpersonation() throws RepositoryException { + return null; + } + + public void changePassword(String password) throws RepositoryException { + } + + public void changePassword(String password, String oldPassword) throws RepositoryException { + } + + public void disable(String reason) throws RepositoryException { + } + + public boolean isDisabled() throws RepositoryException { + return false; + } + + public String getDisabledReason() throws RepositoryException { + return null; + } + + public String getID() throws RepositoryException { + return null; + } + + public boolean isGroup() { + return false; + } + + public Principal getPrincipal() throws RepositoryException { + return null; + } + + public Iterator declaredMemberOf() throws RepositoryException { + return null; + } + + public Iterator memberOf() throws RepositoryException { + return null; + } + + public void remove() throws RepositoryException { + } + + public Iterator getPropertyNames() throws RepositoryException { + return null; + } + + public Iterator getPropertyNames(String relPath) throws RepositoryException { + return null; + } + + public boolean hasProperty(String name) throws RepositoryException { + return false; + } + + public void setProperty(String name, Value value) throws RepositoryException { + } + + public void setProperty(String name, Value[] value) throws RepositoryException { + } + + public Value[] getProperty(String name) throws RepositoryException { + return new Value[0]; + } + + public boolean removeProperty(String name) throws RepositoryException { + return false; + } + + public String getPath() { + return null; + } + } +} + diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/TestAll.java new file mode 100644 index 00000000000..7bb7cfae0ed --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/TestAll.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.core.security.authentication; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** TestAll... */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("core.security.authentication tests"); + + suite.addTestSuite(GuestLoginTest.class); + suite.addTestSuite(NullLoginTest.class); + suite.addTestSuite(SimpleCredentialsAuthenticationTest.class); + suite.addTestSuite(CryptedSimpleCredentialsTest.class); + suite.addTestSuite(LoginModuleTest.class); + suite.addTestSuite(DefaultLoginModuleTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/CompatTokenProviderTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/CompatTokenProviderTest.java new file mode 100644 index 00000000000..1528b61991e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/CompatTokenProviderTest.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.core.security.authentication.token; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +public class CompatTokenProviderTest extends AbstractJCRTest { + + private User testuser; + private String userId; + + private SessionImpl session; + private CompatTokenProvider tokenProvider; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (superuser instanceof SessionImpl) { + UserManager umgr = ((SessionImpl) superuser).getUserManager(); + if (!umgr.isAutoSave()) { + umgr.autoSave(true); + } + String uid = "test"; + while (umgr.getAuthorizable(uid) != null) { + uid += "_"; + } + + testuser = umgr.createUser(uid, uid); + userId = testuser.getID(); + } else { + throw new NotExecutableException(); + } + + if (superuser.nodeExists(((ItemBasedPrincipal) testuser.getPrincipal()).getPath())) { + session = (SessionImpl) superuser; + } else { + session = (SessionImpl) getHelper().getSuperuserSession("security"); + } + tokenProvider = new CompatTokenProvider((SessionImpl) session, TokenBasedAuthentication.TOKEN_EXPIRATION); + } + + @Override + protected void tearDown() throws Exception { + try { + testuser.remove(); + session.logout(); + } finally { + super.tearDown(); + } + } + + public void testCreateTokenFromCredentials() throws Exception { + TokenInfo info = tokenProvider.createToken(testuser, new SimpleCredentials(userId, new char[0])); + assertTokenInfo(info); + } + + public void testCreateTokenIsCaseInsensitive() throws Exception { + String upperCaseUserId = userId.toUpperCase(); + TokenInfo info = tokenProvider.createToken(testuser, new SimpleCredentials(upperCaseUserId, new char[0])); + assertTokenInfo(info); + } + + public void testTokenNode() throws Exception { + 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(publicAttributes); + attributes.putAll(privateAttributes); + + SimpleCredentials sc = new SimpleCredentials(userId, userId.toCharArray()); + for (String s : attributes.keySet()) { + sc.setAttribute(s, attributes.get(s)); + } + + TokenInfo info = tokenProvider.createToken(testuser, sc); + Node tokenNode = getTokenNode(info); + Property prop = tokenNode.getProperty(".token.key"); + assertNotNull(prop); + assertEquals(PropertyType.STRING, prop.getType()); + assertFalse(prop.getDefinition().isProtected()); + + prop = tokenNode.getProperty(".token.exp"); + assertNotNull(prop); + assertEquals(PropertyType.DATE, prop.getType()); + assertFalse(prop.getDefinition().isProtected()); + + for (String key : privateAttributes.keySet()) { + assertEquals(privateAttributes.get(key), tokenNode.getProperty(key).getString()); + } + + for (String key : publicAttributes.keySet()) { + assertEquals(publicAttributes.get(key), tokenNode.getProperty(key).getString()); + } + } + + public void testGetTokenInfoFromInvalidToken() throws Exception { + List invalid = new ArrayList(); + invalid.add("/invalid"); + invalid.add(UUID.randomUUID().toString()); + + try { + for (String token : invalid) { + TokenInfo info = tokenProvider.getTokenInfo(token); + assertNull(info); + } + } catch (Exception e) { + // success + } + } + + public void testGetTokenInfo() throws Exception { + String token = tokenProvider.createToken(testuser, new SimpleCredentials(userId, userId.toCharArray())).getToken(); + TokenInfo info = tokenProvider.getTokenInfo(token); + assertTokenInfo(info); + } + + public void testIsExpired() throws Exception { + TokenInfo info = tokenProvider.createToken(testuser, new SimpleCredentials(userId, userId.toCharArray())); + + long loginTime = waitForSystemTimeIncrement(System.currentTimeMillis()); + assertFalse(info.isExpired(loginTime)); + assertTrue(info.isExpired(loginTime + TokenBasedAuthentication.TOKEN_EXPIRATION)); + } + + public void testReset() throws Exception { + TokenInfo info = tokenProvider.createToken(testuser, new SimpleCredentials(userId, userId.toCharArray())); + long expTime = getTokenNode(info).getProperty(".token.exp").getLong(); + + long loginTime = System.currentTimeMillis(); + assertFalse(info.resetExpiration(loginTime)); + assertTrue(info.resetExpiration(loginTime + TokenBasedAuthentication.TOKEN_EXPIRATION / 2)); + long expTime2 = getTokenNode(info).getProperty(".token.exp").getLong(); + assertFalse(expTime == expTime2); + } + + //-------------------------------------------------------------------------- + private static void assertTokenInfo(TokenInfo info) { + assertNotNull(info); + assertNotNull(info.getToken()); + assertFalse(info.isExpired(new Date().getTime())); + } + + private Node getTokenNode(TokenInfo info) throws RepositoryException { + return CompatTokenProvider.getTokenNode(info.getToken(), session); + } + + private static long waitForSystemTimeIncrement(long old){ + while (old == System.currentTimeMillis()) { + // wait for system timer to move + } + return System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TestAll.java new file mode 100644 index 00000000000..670399661f2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TestAll.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.core.security.authentication.token; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all test cases for package org.apache.jackrabbit.core.security.authentication.token. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.core.security.authentication.token tests"); + + suite.addTestSuite(TokenBasedAuthenticationCompatTest.class); + suite.addTestSuite(TokenBasedAuthenticationTest.class); + suite.addTestSuite(TokenBasedLoginTest.class); + suite.addTestSuite(TokenProviderTest.class); + suite.addTestSuite(CompatTokenProviderTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthenticationCompatTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthenticationCompatTest.java new file mode 100644 index 00000000000..e6242aab563 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthenticationCompatTest.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.core.security.authentication.token; + +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; +import javax.jcr.Credentials; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * TokenBasedAuthenticationCompatTest... + */ +public class TokenBasedAuthenticationCompatTest extends AbstractJCRTest { + + SessionImpl adminSession; + User testUser; + + Node tokenNode; + String token; + + TokenBasedAuthentication nullTokenAuth; + TokenBasedAuthentication validTokenAuth; + + TokenCredentials tokenCreds; + Credentials simpleCreds = new SimpleCredentials("uid", "pw".toCharArray()); + Credentials creds = new Credentials() {}; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + System.setProperty(TokenBasedAuthentication.PARAM_COMPAT, Boolean.TRUE.toString()); + + adminSession = (SessionImpl) getHelper().getSuperuserSession("security"); + testUser = adminSession.getUserManager().createUser(UUID.randomUUID().toString(), "pw"); + adminSession.save(); + + SimpleCredentials sc = new SimpleCredentials(testUser.getID(), "pw".toCharArray()); + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + + CompatTokenProvider tp = new CompatTokenProvider(adminSession, TokenBasedAuthentication.TOKEN_EXPIRATION); + TokenInfo ti = tp.createToken(testUser, sc); + tokenNode = CompatTokenProvider.getTokenNode(ti.getToken(), adminSession); + + token = ti.getToken(); + + nullTokenAuth = new TokenBasedAuthentication(null, -1, adminSession); + validTokenAuth = new TokenBasedAuthentication(token, 7200, adminSession); + + tokenCreds = new TokenCredentials(token); + } + + @Override + protected void tearDown() throws Exception { + System.setProperty(TokenBasedAuthentication.PARAM_COMPAT, Boolean.FALSE.toString()); + super.tearDown(); + } + + private TokenBasedAuthentication expiredToken() throws RepositoryException, LockException, ConstraintViolationException, VersionException { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(new Date().getTime() - 100); + tokenNode.setProperty(".token.exp", cal); + adminSession.save(); + return new TokenBasedAuthentication(token, TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + } + + public void testCanHandle() throws RepositoryException { + assertTrue(validTokenAuth.canHandle(tokenCreds)); + assertFalse(nullTokenAuth.canHandle(tokenCreds)); + + assertFalse(validTokenAuth.canHandle(simpleCreds)); + assertFalse(nullTokenAuth.canHandle(simpleCreds)); + + assertFalse(validTokenAuth.canHandle(creds)); + assertFalse(nullTokenAuth.canHandle(creds)); + + TokenBasedAuthentication expiredToken = expiredToken(); + assertTrue(expiredToken.canHandle(tokenCreds)); + } + + public void testExpiry() throws RepositoryException { + assertTrue(validTokenAuth.authenticate(tokenCreds)); + + TokenBasedAuthentication expiredToken = expiredToken(); + assertFalse(expiredToken.authenticate(tokenCreds)); + } + + public void testRemoval() throws RepositoryException { + String identifier = tokenNode.getIdentifier(); + + TokenBasedAuthentication expiredToken = expiredToken(); + assertFalse(expiredToken.authenticate(tokenCreds)); + + try { + adminSession.getNodeByIdentifier(identifier); + fail("expired token node should be removed."); + } catch (ItemNotFoundException e) { + // success + } + } + + public void testInvalidCredentials() throws RepositoryException { + try { + validTokenAuth.authenticate(creds); + fail("RepositoryException expected"); + } catch (RepositoryException e) { + // success + } + + try { + assertFalse(validTokenAuth.authenticate(simpleCreds)); + fail("RepositoryException expected"); + } catch (RepositoryException e) { + // success + } + } + + public void testAttributes() throws RepositoryException { + tokenNode.setProperty(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".any", "correct"); + adminSession.save(); + TokenBasedAuthentication auth = new TokenBasedAuthentication(tokenNode.getIdentifier(), TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + + assertFalse(auth.authenticate(tokenCreds)); + + tokenCreds.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + ".any", "wrong"); + assertFalse(auth.authenticate(tokenCreds)); + + tokenCreds.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".any", "correct"); + assertTrue(auth.authenticate(tokenCreds)); + + // add informative property + tokenNode.setProperty("noMatchRequired", "abc"); + adminSession.save(); + auth = new TokenBasedAuthentication(tokenNode.getIdentifier(), TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + + assertTrue(auth.authenticate(tokenCreds)); + } + + public void testUpdateAttributes() throws RepositoryException { + tokenNode.setProperty(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".any", "correct"); + tokenNode.setProperty("informative","value"); + adminSession.save(); + + // token credentials must be updated to contain the additional attribute + // present on the token node. + TokenBasedAuthentication auth = new TokenBasedAuthentication(tokenNode.getIdentifier(), TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + tokenCreds.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + ".any", "correct"); + assertTrue(auth.authenticate(tokenCreds)); + assertEquals("value", tokenCreds.getAttribute("informative")); + + // additional informative property present on credentials upon subsequent + // authentication -> the node must not be updated + auth = new TokenBasedAuthentication(tokenNode.getIdentifier(), TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + tokenCreds.setAttribute("informative2", "value2"); + assertTrue(auth.authenticate(tokenCreds)); + assertFalse(tokenNode.hasProperty("informative2")); + + // modified informative property present on credentials upon subsequent + // authentication -> the node must not be updated + auth = new TokenBasedAuthentication(tokenNode.getIdentifier(), TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + tokenCreds.setAttribute("informative", "otherValue"); + assertTrue(auth.authenticate(tokenCreds)); + assertTrue(tokenNode.hasProperty("informative")); + assertEquals("value", tokenNode.getProperty("informative").getString()); + + // additional mandatory property on the credentials upon subsequent + // authentication -> must be ignored + auth = new TokenBasedAuthentication(tokenNode.getIdentifier(), TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + tokenCreds.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".toIgnore", "ignore"); + assertTrue(auth.authenticate(tokenCreds)); + assertFalse(tokenNode.hasProperty(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".toIgnore")); + } + + public void testIsTokenBasedLogin() { + assertFalse(TokenBasedAuthentication.isTokenBasedLogin(simpleCreds)); + assertFalse(TokenBasedAuthentication.isTokenBasedLogin(creds)); + + assertTrue(TokenBasedAuthentication.isTokenBasedLogin(tokenCreds)); + } + + public void testIsMandatoryAttribute() { + assertFalse(TokenBasedAuthentication.isMandatoryAttribute("noMatchRequired")); + + assertTrue(TokenBasedAuthentication.isMandatoryAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + ".exp")); + assertTrue(TokenBasedAuthentication.isMandatoryAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + ".custom")); + assertTrue(TokenBasedAuthentication.isMandatoryAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + "_custom")); + assertTrue(TokenBasedAuthentication.isMandatoryAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + "custom")); + } + + public void testDoCreateToken() { + assertFalse(TokenBasedAuthentication.doCreateToken(creds)); + assertFalse(TokenBasedAuthentication.doCreateToken(simpleCreds)); + assertFalse(TokenBasedAuthentication.doCreateToken(tokenCreds)); + + SimpleCredentials sc = new SimpleCredentials("uid", "pw".toCharArray()); + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, null); + + assertFalse(TokenBasedAuthentication.doCreateToken(sc)); + + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, "somevalue"); + assertFalse(TokenBasedAuthentication.doCreateToken(sc)); + + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + assertTrue(TokenBasedAuthentication.doCreateToken(sc)); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthenticationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthenticationTest.java new file mode 100644 index 00000000000..e9d4c5ab9b2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedAuthenticationTest.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authentication.token; + +import java.util.UUID; +import javax.jcr.Credentials; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * TokenBasedAuthenticationOakTest... + */ +public class TokenBasedAuthenticationTest extends AbstractJCRTest { + + private SessionImpl adminSession; + private User testUser; + + private String token; + private Node tokenNode; + private TokenCredentials tokenCreds; + + private String expiredToken; + private Node expiredNode; + private TokenCredentials expiredCreds; + + + private TokenBasedAuthentication nullTokenAuth; + private TokenBasedAuthentication validTokenAuth; + + private Credentials simpleCreds = new SimpleCredentials("uid", "pw".toCharArray()); + private Credentials creds = new Credentials() {}; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + adminSession = (SessionImpl) getHelper().getSuperuserSession("security"); + testUser = adminSession.getUserManager().createUser(UUID.randomUUID().toString(), "pw"); + adminSession.save(); + + SimpleCredentials sc = new SimpleCredentials(testUser.getID(), "pw".toCharArray()); + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE+".any", "correct"); + sc.setAttribute("informative", "value"); + + TokenProvider tp = new TokenProvider(adminSession, TokenBasedAuthentication.TOKEN_EXPIRATION); + TokenInfo ti = tp.createToken(testUser, sc); + tokenCreds = ti.getCredentials(); + token = tokenCreds.getToken(); + tokenNode = TokenProvider.getTokenNode(token, adminSession); + + long ttl = 1; // 1ms expiration + tp = new TokenProvider(adminSession, ttl); + TokenInfo expired = tp.createToken(testUser, sc); + expiredCreds = expired.getCredentials(); + expiredToken = expiredCreds.getToken(); + long tokenWillExpireAfter = System.currentTimeMillis() + ttl; + expiredNode = TokenProvider.getTokenNode(expiredToken, adminSession); + + nullTokenAuth = new TokenBasedAuthentication(null, -1, adminSession); + validTokenAuth = new TokenBasedAuthentication(token, 7200, adminSession); + + while (System.currentTimeMillis() <= tokenWillExpireAfter) { + // wait until the token is actually expired + } + } + + @Override + protected void tearDown() throws Exception { + try { + testUser.remove(); + adminSession.save(); + adminSession.logout(); + } finally { + super.tearDown(); + } + } + + private TokenBasedAuthentication createAuthenticationForExpiredToken() throws RepositoryException, LockException, ConstraintViolationException, VersionException { + return new TokenBasedAuthentication(expiredToken, TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + } + + private TokenBasedAuthentication createAuthentication() throws RepositoryException { + return new TokenBasedAuthentication(token, TokenBasedAuthentication.TOKEN_EXPIRATION, adminSession); + } + + public void testCanHandle() throws RepositoryException { + assertTrue(validTokenAuth.canHandle(tokenCreds)); + assertFalse(nullTokenAuth.canHandle(tokenCreds)); + + assertFalse(validTokenAuth.canHandle(simpleCreds)); + assertFalse(nullTokenAuth.canHandle(simpleCreds)); + + assertFalse(validTokenAuth.canHandle(creds)); + assertFalse(nullTokenAuth.canHandle(creds)); + } + + public void testCanHandleExpiredToken() throws RepositoryException { + TokenBasedAuthentication expiredToken = createAuthenticationForExpiredToken(); + assertTrue(expiredToken.canHandle(expiredCreds)); + } + + public void testExpiry() throws RepositoryException { + TokenBasedAuthentication expiredToken = createAuthenticationForExpiredToken(); + assertFalse(expiredToken.authenticate(expiredCreds)); + } + + public void testRemoval() throws RepositoryException { + String identifier = expiredNode.getIdentifier(); + + TokenBasedAuthentication expiredToken = createAuthenticationForExpiredToken(); + assertFalse(expiredToken.authenticate(expiredCreds)); + + try { + superuser.getNodeByIdentifier(identifier); + fail("expired token node should be removed."); + } catch (ItemNotFoundException e) { + // success + } + } + + public void testInvalidCredentials() throws RepositoryException { + try { + validTokenAuth.authenticate(creds); + fail("RepositoryException expected"); + } catch (RepositoryException e) { + // success + } + + try { + assertFalse(validTokenAuth.authenticate(simpleCreds)); + fail("RepositoryException expected"); + } catch (RepositoryException e) { + // success + } + } + + public void testAttributes() throws RepositoryException { + TokenBasedAuthentication auth = createAuthentication(); + assertFalse(auth.authenticate(new TokenCredentials(token))); + + TokenCredentials tc = new TokenCredentials(token); + tc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".any", "wrong"); + assertFalse(auth.authenticate(tc)); + + tc = new TokenCredentials(token); + tc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".any", "correct"); + assertTrue(auth.authenticate(tokenCreds)); + } + + public void testUpdateAttributes() throws RepositoryException { + // token credentials must be updated to contain the additional attribute + // present on the token node. + TokenBasedAuthentication auth = createAuthentication(); + + TokenCredentials tc = new TokenCredentials(token); + tc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".any", "correct"); + + assertTrue(auth.authenticate(tc)); + assertEquals("value", tc.getAttribute("informative")); + + // additional informative property present on credentials upon subsequent + // authentication -> the node must not be updated + auth = createAuthentication(); + tc.setAttribute("informative2", "value2"); + assertTrue(auth.authenticate(tc)); + assertFalse(tokenNode.hasProperty("informative2")); + + // modified informative property present on credentials upon subsequent + // authentication -> the node must not be updated + auth = createAuthentication(); + tc.setAttribute("informative", "otherValue"); + assertTrue(auth.authenticate(tc)); + assertTrue(tokenNode.hasProperty("informative")); + assertEquals("value", tokenNode.getProperty("informative").getString()); + + // additional mandatory property on the credentials upon subsequent + // authentication -> must be ignored + auth = createAuthentication(); + tc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".toIgnore", "ignore"); + assertTrue(auth.authenticate(tokenCreds)); + assertFalse(tokenNode.hasProperty(TokenBasedAuthentication.TOKEN_ATTRIBUTE +".toIgnore")); + } + + public void testIsTokenBasedLogin() { + assertFalse(TokenBasedAuthentication.isTokenBasedLogin(simpleCreds)); + assertFalse(TokenBasedAuthentication.isTokenBasedLogin(creds)); + + assertTrue(TokenBasedAuthentication.isTokenBasedLogin(tokenCreds)); + } + + public void testIsMandatoryAttribute() { + assertFalse(TokenBasedAuthentication.isMandatoryAttribute("noMatchRequired")); + + assertTrue(TokenBasedAuthentication.isMandatoryAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + ".exp")); + assertTrue(TokenBasedAuthentication.isMandatoryAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + ".custom")); + assertTrue(TokenBasedAuthentication.isMandatoryAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + "_custom")); + assertTrue(TokenBasedAuthentication.isMandatoryAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE + "custom")); + } + + public void testDoCreateToken() { + assertFalse(TokenBasedAuthentication.doCreateToken(creds)); + assertFalse(TokenBasedAuthentication.doCreateToken(simpleCreds)); + assertFalse(TokenBasedAuthentication.doCreateToken(tokenCreds)); + + SimpleCredentials sc = new SimpleCredentials("uid", "pw".toCharArray()); + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, null); + + assertFalse(TokenBasedAuthentication.doCreateToken(sc)); + + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, "somevalue"); + assertFalse(TokenBasedAuthentication.doCreateToken(sc)); + + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + assertTrue(TokenBasedAuthentication.doCreateToken(sc)); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedLoginTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedLoginTest.java new file mode 100644 index 00000000000..78d8194e3ea --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenBasedLoginTest.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authentication.token; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.authentication.token.TokenCredentials; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; + +import javax.jcr.LoginException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * TokenBasedLoginTest... + */ +public class TokenBasedLoginTest extends AbstractJCRTest { + + private static final String TOKENS_NAME = ".tokens"; + private static final String TOKEN_ATTRIBUTE = TokenBasedAuthentication.TOKEN_ATTRIBUTE; + + private User testuser; + private String testuserPath; + private SimpleCredentials creds; + + private boolean doSave; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (superuser instanceof JackrabbitSession) { + UserManager umgr = ((JackrabbitSession) superuser).getUserManager(); + String uid = "test"; + while (umgr.getAuthorizable(uid) != null) { + uid += "_"; + } + + testuser = umgr.createUser(uid, uid); + Principal p = testuser.getPrincipal(); + if (p instanceof ItemBasedPrincipal) { + testuserPath = ((ItemBasedPrincipal) p).getPath(); + } else { + throw new NotExecutableException(); + } + + creds = new SimpleCredentials(uid, uid.toCharArray()); + + if (!umgr.isAutoSave()) { + doSave = true; + superuser.save(); + } + } else { + throw new NotExecutableException(); + } + } + + @Override + protected void tearDown() throws Exception { + if (testuser != null) { + testuser.remove(); + if (doSave) { + superuser.save(); + } + } + super.tearDown(); + } + + public void testLogin() throws RepositoryException { + Repository repo = getHelper().getRepository(); + + // make sure regular simple login works. + Session s = repo.login(creds); + s.logout(); + + // test if token creation works. + creds.setAttribute(TOKEN_ATTRIBUTE, ""); + // an additional attribute that must match + creds.setAttribute(TOKEN_ATTRIBUTE + ".any", "any"); + // an attribute just for info purposes + creds.setAttribute("attr", "attr"); + + String token = null; + + s = repo.login(creds); + try { + // token credentials must be created + Set tokenCreds = ((SessionImpl) s).getSubject().getPublicCredentials(TokenCredentials.class); + assertFalse(tokenCreds.isEmpty()); + assertEquals(1, tokenCreds.size()); + + TokenCredentials tc = tokenCreds.iterator().next(); + token = tc.getToken(); + + // original simple credentials: token attribute should be updated + assertNotNull(creds.getAttribute(TOKEN_ATTRIBUTE)); + assertFalse("".equals(creds.getAttribute(TOKEN_ATTRIBUTE))); + + // simple credentials must also be present on the subject + Set scs = ((SessionImpl) s).getSubject().getPublicCredentials(SimpleCredentials.class); + assertFalse(scs.isEmpty()); + assertEquals(1, scs.size()); + SimpleCredentials sc = scs.iterator().next(); + assertNotNull(sc.getAttribute(TOKEN_ATTRIBUTE)); + assertFalse("".equals(sc.getAttribute(TOKEN_ATTRIBUTE))); + + // test if session attributes only exposed non-mandatory attributes + assertNull(s.getAttribute(TOKEN_ATTRIBUTE)); + for (String attrName : tc.getAttributeNames()) { + if (TokenBasedAuthentication.isMandatoryAttribute(attrName)) { + assertNull(s.getAttribute(attrName)); + } else { + assertEquals(tc.getAttribute(attrName), s.getAttribute(attrName)); + } + } + + // only test node characteristics if user-node resided within the same + // workspace as 'superuser' has been created for. + if (superuser.nodeExists(testuserPath)) { + Node userNode = superuser.getNode(testuserPath); + + assertTrue(userNode.hasNode(TOKENS_NAME)); + + Node tNode = userNode.getNode(TOKENS_NAME); + assertTrue(tNode.hasNodes()); + + Node ttNode = tNode.getNodes().nextNode(); + assertTrue(ttNode.hasProperty("attr")); + assertEquals("attr", ttNode.getProperty("attr").getString()); + + assertTrue(ttNode.hasProperty(TOKEN_ATTRIBUTE + ".any")); + assertEquals("any", ttNode.getProperty(TOKEN_ATTRIBUTE + ".any").getString()); + + String id = ttNode.getIdentifier(); + assertTrue(token.startsWith(id)); + } + + } finally { + s.logout(); + } + + // login with token only must succeed as well. + TokenCredentials tokenOnly = new TokenCredentials(token); + tokenOnly.setAttribute(TOKEN_ATTRIBUTE + ".any", "any"); + + s = repo.login(tokenOnly); + try { + assertEquals(creds.getUserID(), s.getUserID()); + + Set tokenCreds = ((SessionImpl) s).getSubject().getPublicCredentials(TokenCredentials.class); + assertFalse(tokenCreds.isEmpty()); + assertEquals(1, tokenCreds.size()); + + TokenCredentials tc = tokenCreds.iterator().next(); + String tk = tc.getToken(); + assertEquals(token, tk); + + assertNull(s.getAttribute(TOKEN_ATTRIBUTE)); + for (String attrName : tc.getAttributeNames()) { + if (TokenBasedAuthentication.isMandatoryAttribute(attrName)) { + assertNull(s.getAttribute(attrName)); + } else { + assertEquals(tc.getAttribute(attrName), s.getAttribute(attrName)); + } + } + + } finally { + s.logout(); + } + + // the non-mandatory attribute may have any value if present with the creds. + tokenOnly.setAttribute("attr", "another"); + s = repo.login(tokenOnly); + try { + assertEquals(creds.getUserID(), s.getUserID()); + } finally { + s.logout(); + tokenOnly.removeAttribute("attr"); + } + + // login with token but wrong mandatory attribute + tokenOnly.setAttribute(TOKEN_ATTRIBUTE + ".any", "another"); + try { + s = repo.login(tokenOnly); + s.logout(); + fail("The additional mandatory attr doesn't match. login must fail."); + } catch (LoginException e) { + // success + } + + // login with token but missing the mandatory attribute + tokenOnly.removeAttribute(TOKEN_ATTRIBUTE + ".any"); + try { + s = repo.login(tokenOnly); + s.logout(); + fail("The additional mandatory attr is missing. login must fail."); + } catch (LoginException e) { + // success + } + } + + /** + * Tests concurrent login on the Repository including token creation. + * Test copied and slightly adjusted from org.apache.jackrabbit.core.ConcurrentLoginTest + */ + public void testConcurrentLogin() throws RepositoryException, NotExecutableException { + final Exception[] exception = new Exception[1]; + List testRunner = new ArrayList(); + for (int i = 0; i < 10; i++) { + testRunner.add(new Thread(new Runnable() { + public void run() { + for (int i = 0; i < 100; i++) { + try { + SimpleCredentials sc = new SimpleCredentials(testuser.getID(), testuser.getID().toCharArray()); + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + + Session s = getHelper().getRepository().login(sc); + try { + Set tcs = ((SessionImpl) s).getSubject().getPublicCredentials(TokenCredentials.class); + assertFalse(tcs.isEmpty()); + } finally { + s.logout(); + } + } catch (Exception e) { + exception[0] = e; + break; + } + } + } + })); + } + + // start threads + for (Object aTestRunner : testRunner) { + ((Thread) aTestRunner).start(); + } + + // join threads + for (Object aTestRunner : testRunner) { + try { + ((Thread) aTestRunner).join(); + } catch (InterruptedException e) { + fail(e.toString()); + } + } + + if (exception[0] != null) { + fail(exception[0].toString()); + } + } + + /** + * Tests concurrent login of 3 different users on the Repository including + * token creation. + * Test copied and slightly adjusted from org.apache.jackrabbit.core.ConcurrentLoginTest + */ + public void testConcurrentLoginOfDifferentUsers() throws RepositoryException, NotExecutableException { + final Exception[] exception = new Exception[1]; + List testRunner = new ArrayList(); + for (int i = 0; i < 10; i++) { + testRunner.add(new Thread(new Runnable() { + public void run() { + for (int i = 0; i < 100; i++) { + try { + SimpleCredentials c; + double rand = 3 * Math.random(); + int index = (int) Math.floor(rand); + switch (index) { + case 0: + c = new SimpleCredentials(testuser.getID(), testuser.getID().toCharArray()); + break; + case 1: + c = new SimpleCredentials(getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + RepositoryStub.PROP_SUPERUSER_NAME), getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + RepositoryStub.PROP_SUPERUSER_PWD).toCharArray()); + break; + default: + c = new SimpleCredentials(getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + RepositoryStub.PROP_READONLY_NAME), getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + RepositoryStub.PROP_READONLY_PWD).toCharArray()); + break; + } + c.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + Session s = getHelper().getRepository().login(c); + try { + Set tcs = ((SessionImpl) s).getSubject().getPublicCredentials(TokenCredentials.class); + assertFalse(tcs.isEmpty()); + } finally { + s.logout(); + } + } catch (Exception e) { + exception[0] = e; + break; + } + } + } + })); + } + + // start threads + for (Object aTestRunner : testRunner) { + ((Thread) aTestRunner).start(); + } + + // join threads + for (Object aTestRunner : testRunner) { + try { + ((Thread) aTestRunner).join(); + } catch (InterruptedException e) { + fail(e.toString()); + } + } + + if (exception[0] != null) { + fail(exception[0].toString()); + } + } + + /** + * Tests concurrent login on the Repository including token creation. + * Test copied and slightly adjusted from org.apache.jackrabbit.core.ConcurrentLoginTest + */ + public void testConcurrentLoginDifferentWorkspaces() throws RepositoryException, NotExecutableException { + final String testID = testuser.getID(); + + // check if test is executable + // - multiple workspaces must be present + final List wspNames = Arrays.asList(superuser.getWorkspace().getAccessibleWorkspaceNames()); + if (wspNames.size() <= 1) { + throw new NotExecutableException(); + } + // - testuser must be present for all workspaces + for (String wspName : wspNames) { + JackrabbitSession s = null; + try { + s = (JackrabbitSession) getHelper().getSuperuserSession(wspName); + if (s.getUserManager().getAuthorizable(testID) == null) { + throw new NotExecutableException(); + } + } finally { + if (s != null) { + s.logout(); + } + } + } + + final Exception[] exception = new Exception[1]; + List testRunner = new ArrayList(); + for (int i = 0; i < 10; i++) { + testRunner.add(new Thread(new Runnable() { + public void run() { + for (int i = 0; i < 100; i++) { + try { + double rand = wspNames.size() * Math.random(); + int index = (int) Math.floor(rand); + String wspName = wspNames.get(index); + + SimpleCredentials sc = new SimpleCredentials(testID, testID.toCharArray()); + sc.setAttribute(TokenBasedAuthentication.TOKEN_ATTRIBUTE, ""); + + Session s = getHelper().getRepository().login(sc, wspName); + try { + Set tcs = ((SessionImpl) s).getSubject().getPublicCredentials(TokenCredentials.class); + assertFalse(tcs.isEmpty()); + } finally { + s.logout(); + } + + } catch (Exception e) { + exception[0] = e; + break; + } + } + } + })); + } + + // start threads + for (Object aTestRunner : testRunner) { + ((Thread) aTestRunner).start(); + } + + // join threads + for (Object aTestRunner : testRunner) { + try { + ((Thread) aTestRunner).join(); + } catch (InterruptedException e) { + fail(e.toString()); + } + } + + if (exception[0] != null) { + fail(exception[0].toString()); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenProviderTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenProviderTest.java new file mode 100644 index 00000000000..0beccb4070d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authentication/token/TokenProviderTest.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.core.security.authentication.token; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +public class TokenProviderTest extends AbstractJCRTest { + + private User testuser; + private String userId; + + private SessionImpl session; + private TokenProvider tokenProvider; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (superuser instanceof SessionImpl) { + UserManager umgr = ((SessionImpl) superuser).getUserManager(); + if (!umgr.isAutoSave()) { + umgr.autoSave(true); + } + String uid = "test"; + while (umgr.getAuthorizable(uid) != null) { + uid += "_"; + } + + testuser = umgr.createUser(uid, uid); + userId = testuser.getID(); + } else { + throw new NotExecutableException(); + } + + if (superuser.nodeExists(((ItemBasedPrincipal) testuser.getPrincipal()).getPath())) { + session = (SessionImpl) superuser; + } else { + session = (SessionImpl) getHelper().getSuperuserSession("security"); + } + tokenProvider = new TokenProvider((SessionImpl) session, TokenBasedAuthentication.TOKEN_EXPIRATION); + } + + @Override + protected void tearDown() throws Exception { + try { + testuser.remove(); + session.logout(); + } finally { + super.tearDown(); + } + } + + public void testCreateTokenFromInvalidCredentials() throws Exception { + assertNull(tokenProvider.createToken(testuser, new SimpleCredentials("unknownUserId", new char[0]))); + } + + public void testCreateTokenFromCredentials() throws Exception { + TokenInfo info = tokenProvider.createToken(testuser, new SimpleCredentials(userId, new char[0])); + assertTokenInfo(info); + } + + public void testCreateTokenIsCaseInsensitive() throws Exception { + String upperCaseUserId = userId.toUpperCase(); + TokenInfo info = tokenProvider.createToken(testuser, new SimpleCredentials(upperCaseUserId, new char[0])); + assertTokenInfo(info); + } + + public void testTokenNode() throws Exception { + 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(publicAttributes); + attributes.putAll(privateAttributes); + + SimpleCredentials sc = new SimpleCredentials(userId, userId.toCharArray()); + for (String s : attributes.keySet()) { + sc.setAttribute(s, attributes.get(s)); + } + + TokenInfo info = tokenProvider.createToken(testuser, sc); + Node tokenNode = getTokenNode(info); + Property prop = tokenNode.getProperty("rep:token.key"); + assertNotNull(prop); + assertEquals(PropertyType.STRING, prop.getType()); + assertTrue(prop.getDefinition().isProtected()); + + prop = tokenNode.getProperty("rep:token.exp"); + assertNotNull(prop); + assertEquals(PropertyType.DATE, prop.getType()); + assertTrue(prop.getDefinition().isProtected()); + + for (String key : privateAttributes.keySet()) { + assertEquals(privateAttributes.get(key), tokenNode.getProperty(key).getString()); + } + + for (String key : publicAttributes.keySet()) { + assertEquals(publicAttributes.get(key), tokenNode.getProperty(key).getString()); + } + } + + public void testGetTokenInfoFromInvalidToken() throws Exception { + List invalid = new ArrayList(); + invalid.add("/invalid"); + invalid.add(UUID.randomUUID().toString()); + + try { + for (String token : invalid) { + TokenInfo info = tokenProvider.getTokenInfo(token); + assertNull(info); + } + } catch (Exception e) { + // success + } + } + + public void testGetTokenInfo() throws Exception { + String token = tokenProvider.createToken(testuser, new SimpleCredentials(userId, userId.toCharArray())).getToken(); + TokenInfo info = tokenProvider.getTokenInfo(token); + assertTokenInfo(info); + } + + public void testIsExpired() throws Exception { + TokenInfo info = tokenProvider.createToken(testuser, new SimpleCredentials(userId, userId.toCharArray())); + + long loginTime = waitForSystemTimeIncrement(System.currentTimeMillis()); + assertFalse(info.isExpired(loginTime)); + assertTrue(info.isExpired(loginTime + TokenBasedAuthentication.TOKEN_EXPIRATION)); + } + + public void testReset() throws Exception { + TokenInfo info = tokenProvider.createToken(testuser, new SimpleCredentials(userId, userId.toCharArray())); + long expTime = getTokenNode(info).getProperty("rep:token.exp").getLong(); + + long loginTime = waitForSystemTimeIncrement(System.currentTimeMillis()); + assertFalse(info.resetExpiration(loginTime)); + assertFalse(info.resetExpiration(loginTime + TokenBasedAuthentication.TOKEN_EXPIRATION)); + + assertTrue(info.resetExpiration(loginTime + TokenBasedAuthentication.TOKEN_EXPIRATION / 2)); + long expTime2 = getTokenNode(info).getProperty("rep:token.exp").getLong(); + assertFalse(expTime == expTime2); + } + + //-------------------------------------------------------------------------- + private static void assertTokenInfo(TokenInfo info) { + assertNotNull(info); + assertNotNull(info.getToken()); + assertFalse(info.isExpired(new Date().getTime())); + } + + private Node getTokenNode(TokenInfo info) throws RepositoryException { + return TokenProvider.getTokenNode(info.getToken(), session); + } + + private static long waitForSystemTimeIncrement(long old){ + while (old == System.currentTimeMillis()) { + // wait for system timer to move + } + return System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractACLTemplateTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractACLTemplateTest.java new file mode 100644 index 00000000000..1af903466f8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractACLTemplateTest.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import java.security.Principal; +import java.util.Collections; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.core.security.TestPrincipal; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; + +/** + * AbstractACLTemplateTest... + */ +public abstract class AbstractACLTemplateTest extends AbstractAccessControlTest { + + protected Principal testPrincipal; + protected PrincipalManager principalMgr; + protected PrivilegeManagerImpl privilegeMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!(superuser instanceof JackrabbitSession)) { + throw new NotExecutableException(); + } + + principalMgr = ((JackrabbitSession) superuser).getPrincipalManager(); + PrincipalIterator it = principalMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_NOT_GROUP); + if (it.hasNext()) { + testPrincipal = it.nextPrincipal(); + } else { + throw new NotExecutableException(); + } + privilegeMgr = (PrivilegeManagerImpl) ((JackrabbitWorkspace) superuser.getWorkspace()).getPrivilegeManager(); + } + + protected void assertSamePrivileges(Privilege[] privs1, Privilege[] privs2) throws AccessControlException { + assertEquals(privilegeMgr.getBits(privs1), privilegeMgr.getBits(privs2)); + } + + protected abstract String getTestPath(); + + protected abstract JackrabbitAccessControlList createEmptyTemplate(String path) throws RepositoryException; + + protected abstract Principal getSecondPrincipal() throws Exception; + + public void testEmptyTemplate() throws RepositoryException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + + assertNotNull(pt.getAccessControlEntries()); + assertTrue(pt.getAccessControlEntries().length == 0); + assertTrue(pt.size() == pt.getAccessControlEntries().length); + assertTrue(pt.isEmpty()); + } + + public void testGetPath() throws RepositoryException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + assertEquals(getTestPath(), pt.getPath()); + } + + public void testAddInvalidEntry() throws RepositoryException, NotExecutableException { + Principal unknownPrincipal; + if (!principalMgr.hasPrincipal("an unknown principal")) { + unknownPrincipal = new TestPrincipal("an unknown principal"); + } else { + throw new NotExecutableException(); + } + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + try { + pt.addAccessControlEntry(unknownPrincipal, privilegesFromName(Privilege.JCR_READ)); + fail("Adding an ACE with an unknown principal should fail"); + } catch (AccessControlException e) { + // success + } + } + + public void testAddInvalidEntry2() throws RepositoryException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + try { + pt.addAccessControlEntry(testPrincipal, new Privilege[0]); + fail("Adding an ACE with invalid privileges should fail"); + } catch (AccessControlException e) { + // success + } + } + + public void testRemoveInvalidEntry() throws RepositoryException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + try { + pt.removeAccessControlEntry(new JackrabbitAccessControlEntry() { + public boolean isAllow() { + return false; + } + public String[] getRestrictionNames() { + return new String[0]; + } + public Value getRestriction(String restrictionName) { + return null; + } + + public Value[] getRestrictions(String restrictionName) throws RepositoryException { + return null; + } + + public Principal getPrincipal() { + return testPrincipal; + } + + public Privilege[] getPrivileges() { + try { + return privilegesFromName(Privilege.JCR_READ); + } catch (Exception e) { + return new Privilege[0]; + } + } + }); + fail("Passing an unknown ACE should fail"); + } catch (AccessControlException e) { + // success + } + } + + public void testRemoveInvalidEntry2() throws RepositoryException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + try { + pt.removeAccessControlEntry(new JackrabbitAccessControlEntry() { + public boolean isAllow() { + return false; + } + public int getPrivilegeBits() { + return 0; + } + public String[] getRestrictionNames() { + return new String[0]; + } + public Value getRestriction(String restrictionName) { + return null; + } + + public Value[] getRestrictions(String restrictionName) throws RepositoryException { + return null; + } + + public Principal getPrincipal() { + return testPrincipal; + } + public Privilege[] getPrivileges() { + return new Privilege[0]; + } + }); + fail("Passing a ACE with invalid privileges should fail"); + } catch (AccessControlException e) { + // success + } + } + + public void testAddEntry() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + Privilege[] privs = privilegesFromName(Privilege.JCR_READ); + assertTrue(pt.addEntry(testPrincipal, privs, true, Collections.emptyMap())); + } + + public void testAddEntryTwice() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + Privilege[] privs = privilegesFromName(Privilege.JCR_READ); + + pt.addEntry(testPrincipal, privs, true, Collections.emptyMap()); + assertFalse(pt.addEntry(testPrincipal, privs, true, Collections.emptyMap())); + } + + public void testEffect() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + Privilege[] read = privilegesFromName(Privilege.JCR_READ); + Privilege[] modProp = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + + pt.addAccessControlEntry(testPrincipal, read); + + // add deny entry for mod_props + assertTrue(pt.addEntry(testPrincipal, modProp, false, null)); + + // test net-effect + PrivilegeBits allows = PrivilegeBits.getInstance(); + PrivilegeBits denies = PrivilegeBits.getInstance(); + AccessControlEntry[] entries = pt.getAccessControlEntries(); + for (AccessControlEntry ace : entries) { + if (testPrincipal.equals(ace.getPrincipal()) && ace instanceof JackrabbitAccessControlEntry) { + PrivilegeBits entryBits = privilegeMgr.getBits(ace.getPrivileges()); + if (((JackrabbitAccessControlEntry) ace).isAllow()) { + allows.addDifference(entryBits, denies); + } else { + denies.addDifference(entryBits, allows); + } + } + } + assertEquals(privilegeMgr.getBits(read), allows); + assertEquals(privilegeMgr.getBits(modProp), denies); + } + + public void testEffect2() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + pt.addEntry(testPrincipal, privilegesFromName(Privilege.JCR_READ), true, Collections.emptyMap()); + + // same entry but with revers 'isAllow' flag + assertTrue(pt.addEntry(testPrincipal, privilegesFromName(Privilege.JCR_READ), false, Collections.emptyMap())); + + // test net-effect + PrivilegeBits allows = PrivilegeBits.getInstance(); + PrivilegeBits denies = PrivilegeBits.getInstance(); + AccessControlEntry[] entries = pt.getAccessControlEntries(); + for (AccessControlEntry ace : entries) { + if (testPrincipal.equals(ace.getPrincipal()) && ace instanceof JackrabbitAccessControlEntry) { + PrivilegeBits entryBits = privilegeMgr.getBits(ace.getPrivileges()); + if (((JackrabbitAccessControlEntry) ace).isAllow()) { + allows.addDifference(entryBits, denies); + } else { + denies.addDifference(entryBits, allows); + } + } + } + + assertTrue(allows.isEmpty()); + assertEquals(privilegeMgr.getBits(privilegesFromName(Privilege.JCR_READ)), denies); + } + + public void testRemoveEntry() throws RepositoryException, + NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + pt.addAccessControlEntry(testPrincipal, privilegesFromName(Privilege.JCR_READ)); + pt.removeAccessControlEntry(pt.getAccessControlEntries()[0]); + } + + public void testRemoveNonExisting() throws RepositoryException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + try { + pt.removeAccessControlEntry(new AccessControlEntry() { + public Principal getPrincipal() { + return testPrincipal; + } + public Privilege[] getPrivileges() { + return new Privilege[0]; + } + }); + fail("Attemt to remove a non-existing, custom ACE must throw AccessControlException."); + } catch (AccessControlException e) { + // success + } + } + + public void testReorder() throws Exception { + Privilege[] read = privilegesFromName(Privilege.JCR_READ); + Privilege[] write = privilegesFromName(Privilege.JCR_WRITE); + + Principal p2 = getSecondPrincipal(); + + AbstractACLTemplate acl = (AbstractACLTemplate) createEmptyTemplate(getTestPath()); + acl.addAccessControlEntry(testPrincipal, read); + acl.addEntry(testPrincipal, write, false); + acl.addAccessControlEntry(p2, write); + + AccessControlEntry[] entries = acl.getAccessControlEntries(); + + assertEquals(3, entries.length); + AccessControlEntry aReadTP = entries[0]; + AccessControlEntry dWriteTP = entries[1]; + AccessControlEntry aWriteP2 = entries[2]; + + // reorder aWriteP2 to the first position + acl.orderBefore(aWriteP2, aReadTP); + assertEquals(0, acl.getEntries().indexOf(aWriteP2)); + assertEquals(1, acl.getEntries().indexOf(aReadTP)); + assertEquals(2, acl.getEntries().indexOf(dWriteTP)); + + // reorder aReadTP to the end of the list + acl.orderBefore(aReadTP, null); + assertEquals(0, acl.getEntries().indexOf(aWriteP2)); + assertEquals(1, acl.getEntries().indexOf(dWriteTP)); + assertEquals(2, acl.getEntries().indexOf(aReadTP)); + } + + public void testReorderInvalidElements() throws Exception { + Privilege[] read = privilegesFromName(Privilege.JCR_READ); + Privilege[] write = privilegesFromName(Privilege.JCR_WRITE); + + Principal p2 = getSecondPrincipal(); + + AbstractACLTemplate acl = (AbstractACLTemplate) createEmptyTemplate(getTestPath()); + acl.addAccessControlEntry(testPrincipal, read); + acl.addAccessControlEntry(p2, write); + + AbstractACLTemplate acl2 = (AbstractACLTemplate) createEmptyTemplate(getTestPath()); + acl2.addEntry(testPrincipal, write, false); + + AccessControlEntry invalid = acl2.getEntries().get(0); + try { + acl.orderBefore(invalid, acl.getEntries().get(0)); + fail("src entry not contained in list -> reorder should fail."); + } catch (AccessControlException e) { + // success + } + try { + acl.orderBefore(acl.getEntries().get(0), invalid); + fail("dest entry not contained in list -> reorder should fail."); + } catch (AccessControlException e) { + // success + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEffectivePolicyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEffectivePolicyTest.java new file mode 100644 index 00000000000..f950b648e4c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEffectivePolicyTest.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.core.security.authorization; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +/** + * AbstractEffectivePolicyTest... + */ +public abstract class AbstractEffectivePolicyTest extends AbstractEvaluationTest { + + protected String path; + protected String childNPath; + protected String childNPath2; + protected String siblingPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create some nodes below the test root in order to apply ac-stuff + Node node = testRootNode.addNode(nodeName1, testNodeType); + Node cn1 = node.addNode(nodeName2, testNodeType); + Property cp1 = node.setProperty(propertyName1, "anyValue"); + Node cn2 = node.addNode(nodeName3, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + superuser.save(); + + path = node.getPath(); + childNPath = cn1.getPath(); + childNPath2 = cn2.getPath(); + siblingPath = n2.getPath(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEntryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEntryTest.java new file mode 100644 index 00000000000..e89b116c5f8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEntryTest.java @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; + +/** + * AbstractEntryTest... + */ +public abstract class AbstractEntryTest extends AbstractAccessControlTest { + + protected Principal testPrincipal; + + @Override + protected void setUp() throws Exception { + super.setUp(); + testPrincipal = new Principal() { + public String getName() { + return "TestPrincipal"; + } + }; + } + + protected JackrabbitAccessControlEntry createEntry(String[] privilegeNames, boolean isAllow) + throws RepositoryException, NotExecutableException { + Privilege[] privs = privilegesFromNames(privilegeNames); + return createEntry(testPrincipal, privs, isAllow); + } + + protected abstract JackrabbitAccessControlEntry createEntry(Principal principal, Privilege[] privileges, boolean isAllow) + throws RepositoryException; + + protected abstract JackrabbitAccessControlEntry createEntry(Principal principal, Privilege[] privileges, boolean isAllow, Map restrictions) + throws RepositoryException; + + protected abstract JackrabbitAccessControlEntry createEntryFromBase(JackrabbitAccessControlEntry base, Privilege[] privileges, boolean isAllow) throws RepositoryException, NotExecutableException; + + protected abstract Map getTestRestrictions() throws RepositoryException; + + public void testIsAllow() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlEntry tmpl = createEntry(new String[] {Privilege.JCR_READ}, true); + assertTrue(tmpl.isAllow()); + + tmpl = createEntry(new String[] {Privilege.JCR_READ}, false); + assertFalse(tmpl.isAllow()); + } + + public void testGetPrincipal() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlEntry tmpl = createEntry(new String[] {Privilege.JCR_READ}, true); + assertNotNull(tmpl.getPrincipal()); + assertEquals(testPrincipal.getName(), tmpl.getPrincipal().getName()); + assertSame(testPrincipal, tmpl.getPrincipal()); + } + + public void testGetPrivilegeBits() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlEntry tmpl = createEntry(new String[] {Privilege.JCR_READ}, true); + + assertEquals(1, tmpl.getPrivileges().length); + assertEquals(getAccessControlManager(superuser).privilegeFromName(Privilege.JCR_READ), + tmpl.getPrivileges()[0]); + + tmpl = createEntry(new String[] {PrivilegeRegistry.REP_WRITE}, true); + assertEquals(getAccessControlManager(superuser).privilegeFromName(PrivilegeRegistry.REP_WRITE), + tmpl.getPrivileges()[0]); + } + + public void testGetPrivileges() throws RepositoryException, NotExecutableException { + PrivilegeManagerImpl privMgr = (PrivilegeManagerImpl) ((JackrabbitWorkspace) superuser.getWorkspace()).getPrivilegeManager(); + JackrabbitAccessControlEntry entry = createEntry(new String[] {Privilege.JCR_READ}, true); + + Privilege[] privs = entry.getPrivileges(); + assertNotNull(privs); + assertEquals(1, privs.length); + assertEquals(privs[0], acMgr.privilegeFromName(Privilege.JCR_READ)); + assertEquals(privMgr.getBits(privs), privMgr.getBits(entry.getPrivileges())); + + entry = createEntry(new String[] {PrivilegeRegistry.REP_WRITE}, true); + privs = entry.getPrivileges(); + assertNotNull(privs); + assertEquals(1, privs.length); + assertEquals(privs[0], acMgr.privilegeFromName(PrivilegeRegistry.REP_WRITE)); + assertEquals(privMgr.getBits(privs), privMgr.getBits(entry.getPrivileges())); + + entry = createEntry(new String[] {Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_REMOVE_CHILD_NODES}, true); + privs = entry.getPrivileges(); + assertNotNull(privs); + assertEquals(2, privs.length); + + Privilege[] param = privilegesFromNames(new String[] { + Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_REMOVE_CHILD_NODES + }); + assertEquals(Arrays.asList(param), Arrays.asList(privs)); + assertEquals(privMgr.getBits(privs), privMgr.getBits(entry.getPrivileges())); + } + + public void testEquals() throws RepositoryException, NotExecutableException { + + Map equalAces = new HashMap(); + + JackrabbitAccessControlEntry ace = createEntry(new String[] {Privilege.JCR_ALL}, true); + // create same entry again + equalAces.put(ace, createEntry(new String[] {Privilege.JCR_ALL}, true)); + + // create entry with declared aggregate privileges + Privilege[] declaredAllPrivs = acMgr.privilegeFromName(Privilege.JCR_ALL).getDeclaredAggregatePrivileges(); + equalAces.put(ace, createEntry(testPrincipal, declaredAllPrivs, true)); + + // create entry with aggregate privileges + Privilege[] aggregateAllPrivs = acMgr.privilegeFromName(Privilege.JCR_ALL).getAggregatePrivileges(); + equalAces.put(ace, createEntry(testPrincipal, aggregateAllPrivs, true)); + + // create entry with different privilege order + List reordered = new ArrayList(Arrays.asList(aggregateAllPrivs)); + reordered.add(reordered.remove(0)); + equalAces.put(createEntry(testPrincipal, reordered.toArray(new Privilege[reordered.size()]), true), + createEntry(testPrincipal, aggregateAllPrivs, true)); + + // even if entries are build with aggregated or declared aggregate privileges + equalAces.put(createEntry(testPrincipal, declaredAllPrivs, true), + createEntry(testPrincipal, aggregateAllPrivs, true)); + + for (AccessControlEntry entry : equalAces.keySet()) { + assertEquals(entry, equalAces.get(entry)); + } + } + + public void testNotEquals() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlEntry ace = createEntry(new String[] {Privilege.JCR_ALL}, true); + List otherAces = new ArrayList(); + + try { + // ACE template with different principal + Principal princ = new Principal() { + public String getName() { + return "a name"; + } + }; + Privilege[] privs = new Privilege[] { + acMgr.privilegeFromName(Privilege.JCR_ALL) + }; + otherAces.add(createEntry(princ, privs, true)); + } catch (RepositoryException e) { + } + + // ACE template with different privileges + try { + otherAces.add(createEntry(new String[] {Privilege.JCR_READ}, true)); + } catch (RepositoryException e) { + } + // ACE template with different 'allow' flag + try { + otherAces.add(createEntry(new String[] {Privilege.JCR_ALL}, false)); + } catch (RepositoryException e) { + } + // ACE template with different privileges and 'allows + try { + otherAces.add(createEntry(new String[] {PrivilegeRegistry.REP_WRITE}, false)); + } catch (RepositoryException e) { + } + + // other ace impl + final Privilege[] privs = new Privilege[] { + acMgr.privilegeFromName(Privilege.JCR_ALL) + }; + + JackrabbitAccessControlEntry pe = new JackrabbitAccessControlEntry() { + public boolean isAllow() { + return true; + } + public String[] getRestrictionNames() { + return new String[0]; + } + public Value getRestriction(String restrictionName) { + return null; + } + + public Value[] getRestrictions(String restrictionName) throws RepositoryException { + return null; + } + + public Principal getPrincipal() { + return testPrincipal; + } + public Privilege[] getPrivileges() { + return privs; + } + }; + otherAces.add(pe); + + for (JackrabbitAccessControlEntry otherAce : otherAces) { + assertFalse(ace.equals(otherAce)); + } + } + + public void testHashCode() throws RepositoryException, NotExecutableException { + + Map equivalent = new HashMap(); + JackrabbitAccessControlEntry ace = createEntry(new String[] {Privilege.JCR_ALL}, true); + // create same entry again + equivalent.put(ace, createEntry(new String[] {Privilege.JCR_ALL}, true)); + // create entry with declared aggregate privileges + Privilege[] declaredAllPrivs = acMgr.privilegeFromName(Privilege.JCR_ALL).getDeclaredAggregatePrivileges(); + equivalent.put(ace, createEntry(testPrincipal, declaredAllPrivs, true)); + // create entry with aggregate privileges + Privilege[] aggregateAllPrivs = acMgr.privilegeFromName(Privilege.JCR_ALL).getAggregatePrivileges(); + equivalent.put(ace, createEntry(testPrincipal, aggregateAllPrivs, true)); + // create entry with different privilege order + List reordered = new ArrayList(Arrays.asList(aggregateAllPrivs)); + reordered.add(reordered.remove(0)); + equivalent.put(createEntry(testPrincipal, reordered.toArray(new Privilege[reordered.size()]), true), + createEntry(testPrincipal, aggregateAllPrivs, true)); + // even if entries are build with aggregated or declared aggregate privileges + equivalent.put(createEntry(testPrincipal, declaredAllPrivs, true), + createEntry(testPrincipal, aggregateAllPrivs, true)); + + for (AccessControlEntry entry : equivalent.keySet()) { + assertEquals(entry.hashCode(), equivalent.get(entry).hashCode()); + } + + // and the opposite: + List otherAces = new ArrayList(); + try { + // ACE template with different principal + Principal princ = new Principal() { + public String getName() { + return "a name"; + } + }; + Privilege[] privs = new Privilege[] { + acMgr.privilegeFromName(Privilege.JCR_ALL) + }; + otherAces.add(createEntry(princ, privs, true)); + } catch (RepositoryException e) { + } + // ACE template with different privileges + try { + otherAces.add(createEntry(new String[] {Privilege.JCR_READ}, true)); + } catch (RepositoryException e) { + } + // ACE template with different 'allow' flag + try { + otherAces.add(createEntry(new String[] {Privilege.JCR_ALL}, false)); + } catch (RepositoryException e) { + } + // ACE template with different privileges and 'allows + try { + otherAces.add(createEntry(new String[] {PrivilegeRegistry.REP_WRITE}, false)); + } catch (RepositoryException e) { + } + // other ace impl + final Privilege[] privs = new Privilege[] { + acMgr.privilegeFromName(Privilege.JCR_ALL) + }; + JackrabbitAccessControlEntry pe = new JackrabbitAccessControlEntry() { + public boolean isAllow() { + return true; + } + public String[] getRestrictionNames() { + return new String[0]; + } + public Value getRestriction(String restrictionName) { + return null; + } + + public Value[] getRestrictions(String restrictionName) throws RepositoryException { + return null; + } + + public Principal getPrincipal() { + return testPrincipal; + } + public Privilege[] getPrivileges() { + return privs; + } + }; + otherAces.add(pe); + + for (JackrabbitAccessControlEntry otherAce : otherAces) { + assertFalse(ace.hashCode() == otherAce.hashCode()); + } + + } + + public void testNullPrincipal() throws RepositoryException { + try { + Privilege[] privs = new Privilege[] { + acMgr.privilegeFromName(Privilege.JCR_ALL) + }; + createEntry(null, privs, true); + fail("Principal must not be null"); + } catch (Exception e) { + // success + } + } + + public void testInvalidPrivilege() throws RepositoryException, + NotExecutableException { + Privilege invalidPriv = new Privilege() { + public String getName() { + return ""; + } + public boolean isAbstract() { + return false; + } + public boolean isAggregate() { + return false; + } + public Privilege[] getDeclaredAggregatePrivileges() { + return new Privilege[0]; + } + public Privilege[] getAggregatePrivileges() { + return new Privilege[0]; + } + }; + try { + Privilege[] privs = new Privilege[] {invalidPriv, privilegesFromName(Privilege.JCR_READ)[0]}; + createEntry(testPrincipal, privs, true); + fail("Principal must not be null"); + } catch (AccessControlException e) { + // success + } + } + + public void testCreateFromBase() throws RepositoryException, NotExecutableException { + Map testRestrictions = getTestRestrictions(); + JackrabbitAccessControlEntry base = createEntry(testPrincipal, privilegesFromName(Privilege.JCR_READ), false, testRestrictions); + assertEquals(testPrincipal, base.getPrincipal()); + assertTrue(Arrays.equals(privilegesFromName(Privilege.JCR_READ), base.getPrivileges())); + assertFalse(base.isAllow()); + + Map baseRestrictions = new HashMap(); + for (String name : base.getRestrictionNames()) { + baseRestrictions.put(name, base.getRestriction(name)); + } + assertEquals(testRestrictions, baseRestrictions); + + + JackrabbitAccessControlEntry entry = createEntryFromBase(base, privilegesFromName(Privilege.JCR_WRITE), true); + assertEquals(testPrincipal, entry.getPrincipal()); + assertTrue(Arrays.equals(privilegesFromName(Privilege.JCR_WRITE), entry.getPrivileges())); + assertTrue(entry.isAllow()); + + Map entryRestrictions = new HashMap(); + for (String name : entry.getRestrictionNames()) { + entryRestrictions.put(name, entry.getRestriction(name)); + } + assertEquals(testRestrictions, entryRestrictions); + + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEvaluationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEvaluationTest.java new file mode 100644 index 00000000000..9780103db33 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractEvaluationTest.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.core.security.authorization; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +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.api.security.user.Authorizable; +import org.apache.jackrabbit.core.security.TestPrincipal; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** AbstractEvaluationTest... */ +public abstract class AbstractEvaluationTest extends AbstractAccessControlTest { + + private static Logger log = LoggerFactory.getLogger(AbstractEvaluationTest.class); + + private String uid; + protected User testUser; + protected Credentials creds; + + protected Group testGroup; + + private Session testSession; + private AccessControlManager testAccessControlManager; + private Node trn; + private Set toClear = new HashSet(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!isExecutable()) { + superuser.logout(); + throw new NotExecutableException(); + } + + try { + UserManager uMgr = getUserManager(superuser); + // create the testUser + uid = "testUser" + UUID.randomUUID(); + creds = new SimpleCredentials(uid, uid.toCharArray()); + + testUser = uMgr.createUser(uid, uid); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + } catch (Exception e) { + superuser.logout(); + throw e; + } + } + + @Override + protected void tearDown() throws Exception { + try { + for (String path : toClear) { + try { + AccessControlPolicy[] policies = acMgr.getPolicies(path); + for (AccessControlPolicy policy : policies) { + acMgr.removePolicy(path, policy); + } + superuser.save(); + } catch (RepositoryException e) { + // log error and ignore + log.debug(e.getMessage()); + } + } + + if (testSession != null && testSession.isLive()) { + testSession.logout(); + } + if (testGroup != null && testUser != null) { + if (testGroup.isDeclaredMember(testUser)) { + testGroup.removeMember(testUser); + } + testGroup.remove(); + } + if (uid != null) { + Authorizable a = getUserManager(superuser).getAuthorizable(uid); + if (a != null) { + a.remove(); + } + } + if (!getUserManager(superuser).isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + } finally { + super.tearDown(); + } + } + + protected static UserManager getUserManager(Session session) throws + NotExecutableException { + if (!(session instanceof JackrabbitSession)) { + throw new NotExecutableException(); + } + try { + return ((JackrabbitSession) session).getUserManager(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + } + + protected Session getTestSession() throws RepositoryException { + if (testSession == null) { + testSession = getHelper().getRepository().login(creds); + } + return testSession; + } + + protected AccessControlManager getTestACManager() throws NotExecutableException, RepositoryException { + if (testAccessControlManager == null) { + testAccessControlManager = getAccessControlManager(getTestSession()); + } + return testAccessControlManager; + } + + protected Group getTestGroup() throws RepositoryException, NotExecutableException { + if (testGroup == null) { + // create the testGroup + Principal principal = new TestPrincipal("testGroup" + UUID.randomUUID()); + UserManager umgr = getUserManager(superuser); + testGroup = umgr.createGroup(principal); + testGroup.addMember(testUser); + if (!umgr.isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + } + return testGroup; + } + + protected Node getTestNode() throws RepositoryException { + if (trn == null) { + trn = (Node) getTestSession().getItem(testRootNode.getPath()); + } + return trn; + } + + protected abstract boolean isExecutable(); + + protected void checkReadOnly(String path) throws RepositoryException, NotExecutableException { + Privilege[] privs = getTestACManager().getPrivileges(path); + assertTrue(privs.length == 1); + assertEquals(privilegesFromName(Privilege.JCR_READ)[0], privs[0]); + } + + protected abstract JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws RepositoryException, NotExecutableException; + protected abstract Map getRestrictions(Session session, String path) throws RepositoryException, NotExecutableException; + + protected JackrabbitAccessControlList modifyPrivileges(String path, String privilege, boolean isAllow) throws NotExecutableException, RepositoryException { + return modifyPrivileges(path, testUser.getPrincipal(), privilegesFromName(privilege), isAllow, getRestrictions(superuser, path)); + } + + protected JackrabbitAccessControlList modifyPrivileges(String path, Principal principal, Privilege[] privileges, boolean isAllow, Map restrictions) throws NotExecutableException, RepositoryException { + JackrabbitAccessControlList tmpl = getPolicy(acMgr, path, principal); + tmpl.addEntry(principal, privileges, isAllow, restrictions); + + acMgr.setPolicy(tmpl.getPath(), tmpl); + superuser.save(); + + // remember for clean up during tearDown + toClear.add(tmpl.getPath()); + return tmpl; + } + + protected JackrabbitAccessControlList givePrivileges(String nPath, Privilege[] privileges, + Map restrictions) + throws NotExecutableException, RepositoryException { + return modifyPrivileges(nPath, testUser.getPrincipal(), privileges, true, restrictions); + } + + protected JackrabbitAccessControlList givePrivileges(String nPath, Principal principal, + Privilege[] privileges, Map restrictions) + throws NotExecutableException, RepositoryException { + return modifyPrivileges(nPath, principal, privileges, true, restrictions); + } + + protected JackrabbitAccessControlList withdrawPrivileges(String nPath, Privilege[] privileges, Map restrictions) + throws NotExecutableException, RepositoryException { + return modifyPrivileges(nPath, testUser.getPrincipal(), privileges, false, restrictions); + } + + protected JackrabbitAccessControlList withdrawPrivileges(String nPath, Principal principal, Privilege[] privileges, Map restrictions) + throws NotExecutableException, RepositoryException { + return modifyPrivileges(nPath, principal, privileges, false, restrictions); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractLockManagementTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractLockManagementTest.java new file mode 100644 index 00000000000..30cbccaa90d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractLockManagementTest.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.core.security.authorization; + +import javax.jcr.security.Privilege; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockManager; + +/** AbstractVersionAccessTest... */ +public abstract class AbstractLockManagementTest extends AbstractEvaluationTest { + + private Node createLockableNode(Node parent) throws RepositoryException, NotExecutableException { + Node n = parent.addNode(nodeName1); + if (!n.isNodeType(mixLockable)) { + if (n.canAddMixin(mixLockable)) { + n.addMixin(mixLockable); + } else { + throw new NotExecutableException(); + } + parent.save(); + } + return n; + } + + private Node createLockedNode(Node parent) throws RepositoryException, NotExecutableException { + Node n = createLockableNode(parent); + // create a deep, session scoped lock + n.lock(true, true); + return n; + } + + public void testReadLockContent() throws RepositoryException, NotExecutableException { + Node n = createLockedNode(testRootNode); + Node childN = n.addNode(nodeName2); + modifyPrivileges(n.getPath(), Privilege.JCR_READ, false); + modifyPrivileges(childN.getPath(), Privilege.JCR_READ, true); + + Node childN2 = (Node) getTestSession().getItem(childN.getPath()); + try { + childN2.getLock(); + fail("TestUser doesn't have permission to read the jcr:lockIsDeep property."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testLock2() throws RepositoryException, NotExecutableException { + Node n = createLockableNode(testRootNode); + + modifyPrivileges(n.getPath(), PrivilegeRegistry.REP_WRITE, false); + modifyPrivileges(n.getPath(), Privilege.JCR_LOCK_MANAGEMENT, true); + + Node n2 = getTestNode().getNode(nodeName1); + + // all lock operations must succeed + Lock l = n2.lock(true, true); + l.refresh(); + n2.unlock(); + } + + public void testLock3() throws RepositoryException, NotExecutableException { + Node n = createLockableNode(testRootNode); + + Node trn = getTestNode(); + modifyPrivileges(trn.getPath(), Privilege.JCR_READ, true); + modifyPrivileges(trn.getPath(), PrivilegeRegistry.REP_WRITE, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_LOCK_MANAGEMENT, true); + + Node n2 = trn.getNode(n.getName()); + n2.lock(true, true); + Lock l = n2.getLock(); + + // withdraw lock-mgmt -> must not be able to refresh the lock or + // unlock the node + modifyPrivileges(trn.getPath(), Privilege.JCR_LOCK_MANAGEMENT, false); + + try { + l.refresh(); + fail("TestUser doesn't have permission to refresh the lock."); + } catch (AccessDeniedException e) { + // success + } + try { + n2.unlock(); + fail("TestUser doesn't have permission to unlock the node."); + } catch (AccessDeniedException e) { + // success + } + + // make sure the lock can be removed upon session.logout. + modifyPrivileges(trn.getPath(), Privilege.JCR_LOCK_MANAGEMENT, true); + } + + public void testLock4() throws RepositoryException, NotExecutableException { + Node n = createLockableNode(testRootNode); + + Node trn = getTestNode(); + modifyPrivileges(trn.getPath(), Privilege.JCR_READ, true); + modifyPrivileges(trn.getPath(), PrivilegeRegistry.REP_WRITE, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_LOCK_MANAGEMENT, true); + + Node n2 = trn.getNode(n.getName()); + n2.lock(true, true); + Lock l = n2.getLock(); + String lt = l.getLockToken(); + + // withdraw lock-mgmt -> logout of session must still remove the lock + modifyPrivileges(trn.getPath(), Privilege.JCR_LOCK_MANAGEMENT, false); + + getTestSession().logout(); + boolean isLocked = n.isLocked(); + assertFalse(isLocked); + } + + public void testLockBreaking() throws RepositoryException, NotExecutableException { + String locktoken = null; + LockManager sulm = superuser.getWorkspace().getLockManager(); + String lockedpath = null; + + try { + Node trn = getTestNode(); + modifyPrivileges(trn.getPath(), Privilege.JCR_READ, true); + modifyPrivileges(trn.getPath(), PrivilegeRegistry.REP_WRITE, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_LOCK_MANAGEMENT, true); + + Session lockingSession = trn.getSession(); + + assertFalse("super user and test user should have different user ids: " + lockingSession.getUserID() + " vs " + superuser.getUserID(), + lockingSession.getUserID().equals(superuser.getUserID())); + + trn.addNode("locktest", "nt:unstructured"); + trn.addMixin("mix:lockable"); + lockingSession.save(); + + // let the "other" user lock the node + LockManager oulm = lockingSession.getWorkspace().getLockManager(); + Lock l = oulm.lock(trn.getPath(), true, false, Long.MAX_VALUE, null); + lockedpath = trn.getPath(); + locktoken = l.getLockToken(); + lockingSession.logout(); + + // transfer the lock token to the super user and try the unlock + + Node lockednode = superuser.getNode(lockedpath); + assertTrue(lockednode.isLocked()); + Lock sl = sulm.getLock(lockedpath); + assertNotNull(sl.getLockToken()); + sulm.addLockToken(sl.getLockToken()); + sulm.unlock(lockedpath); + locktoken = null; + } + finally { + if (locktoken != null && lockedpath != null) { + sulm.addLockToken(locktoken); + sulm.unlock(lockedpath); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractNodeTypeManagementTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractNodeTypeManagementTest.java new file mode 100644 index 00000000000..ad5d38bca8a --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractNodeTypeManagementTest.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import javax.jcr.security.Privilege; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** AbstractNodeTypeManagementTest... */ +public abstract class AbstractNodeTypeManagementTest extends AbstractEvaluationTest { + + private Node childNode; + private String mixinName; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Node child = testRootNode.addNode(nodeName2); + if (child.isNodeType(mixReferenceable) || !child.canAddMixin(mixReferenceable)) { + throw new NotExecutableException(); + } + superuser.save(); + + mixinName = getTestSession().getNamespacePrefix(NS_MIX_URI) + ":referenceable"; + childNode = getTestSession().getNode(child.getPath()); + } + + public void testCanAddMixin() throws RepositoryException, NotExecutableException { + checkReadOnly(childNode.getPath()); + + assertFalse(childNode.canAddMixin(mixinName)); + + modifyPrivileges(childNode.getPath(), Privilege.JCR_NODE_TYPE_MANAGEMENT, true); + assertTrue(childNode.canAddMixin(mixinName)); + + modifyPrivileges(childNode.getPath(), Privilege.JCR_NODE_TYPE_MANAGEMENT, false); + assertFalse(childNode.canAddMixin(mixinName)); + } + + public void testAddMixin() throws RepositoryException, NotExecutableException { + checkReadOnly(childNode.getPath()); + + try { + childNode.addMixin(mixinName); + childNode.save(); + fail("TestSession does not have sufficient privileges to add a mixin type."); + } catch (AccessDeniedException e) { + // success + } + + modifyPrivileges(childNode.getPath(), Privilege.JCR_NODE_TYPE_MANAGEMENT, true); + childNode.addMixin(mixinName); + childNode.save(); + } + + public void testRemoveMixin() throws RepositoryException, NotExecutableException { + ((Node) superuser.getItem(childNode.getPath())).addMixin(mixinName); + superuser.save(); + + checkReadOnly(childNode.getPath()); + + try { + childNode.removeMixin(mixinName); + childNode.save(); + fail("TestSession does not have sufficient privileges to remove a mixin type."); + } catch (AccessDeniedException e) { + // success + } + + modifyPrivileges(childNode.getPath(), Privilege.JCR_NODE_TYPE_MANAGEMENT, true); + childNode.removeMixin(mixinName); + childNode.save(); + } + + public void testSetPrimaryType() throws RepositoryException, NotExecutableException { + Node child = (Node) superuser.getItem(childNode.getPath()); + String ntName = child.getPrimaryNodeType().getName(); + + String changedNtName = "nt:folder"; + child.setPrimaryType(changedNtName); + child.save(); + + try { + checkReadOnly(childNode.getPath()); + + try { + childNode.setPrimaryType(ntName); + childNode.save(); + fail("TestSession does not have sufficient privileges to change the primary type."); + } catch (AccessDeniedException e) { + // success + getTestSession().refresh(false); // TODO: see JCR-1916 + } + + modifyPrivileges(childNode.getPath(), Privilege.JCR_NODE_TYPE_MANAGEMENT, true); + childNode.setPrimaryType(ntName); + childNode.save(); + + } finally { + if (!ntName.equals(child.getPrimaryNodeType().getName())) { + child.setPrimaryType(ntName); + child.save(); + } + } + } + + /** + * Test difference between common jcr:write privilege an rep:write privilege + * that includes the ability to set the primary node type upon child node + * creation. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testAddNode() throws RepositoryException, NotExecutableException { + checkReadOnly(childNode.getPath()); + + // with simple write privilege a child node can be added BUT no + // node type must be specified. + modifyPrivileges(childNode.getPath(), Privilege.JCR_WRITE, true); + addChildNode(false); + try { + addChildNode(true); + fail("Missing privilege jcr:nodeTypeManagement."); + } catch (AccessDeniedException e) { + // success + } + + // adding jcr:nodeTypeManagement privilege will allow to use any + // variant of Node.addNode. + modifyPrivileges(childNode.getPath(), Privilege.JCR_NODE_TYPE_MANAGEMENT, true); + addChildNode(false); + addChildNode(true); + } + + private void addChildNode(boolean specifyNodeType) throws RepositoryException { + Node n = null; + try { + n = (specifyNodeType) ? childNode.addNode(nodeName3, testNodeType) : childNode.addNode(nodeName3); + } finally { + if (n != null) { + n.remove(); + childNode.save(); + } + } + } + + public void testCopy() throws RepositoryException, NotExecutableException { + Workspace wsp = getTestSession().getWorkspace(); + String parentPath = childNode.getParent().getPath(); + String srcPath = childNode.getPath(); + String destPath = parentPath + "/"+ nodeName3; + + checkReadOnly(parentPath); + try { + wsp.copy(srcPath, destPath); + fail("Missing write privilege."); + } catch (AccessDeniedException e) { + // success + } + + // with simple write privilege copying a node is not allowed. + modifyPrivileges(parentPath, Privilege.JCR_WRITE, true); + try { + wsp.copy(srcPath, destPath); + fail("Missing privilege jcr:nodeTypeManagement."); + } catch (AccessDeniedException e) { + // success + } + + // adding jcr:nodeTypeManagement privilege will grant permission to copy. + modifyPrivileges(parentPath, PrivilegeRegistry.REP_WRITE, true); + wsp.copy(srcPath, destPath); + } + + public void testWorkspaceMove() throws RepositoryException, NotExecutableException { + Workspace wsp = getTestSession().getWorkspace(); + String parentPath = childNode.getParent().getPath(); + String srcPath = childNode.getPath(); + String destPath = parentPath + "/"+ nodeName3; + + checkReadOnly(parentPath); + try { + wsp.move(srcPath, destPath); + fail("Missing write privilege."); + } catch (AccessDeniedException e) { + // success + } + + // with simple write privilege moving a node is not allowed. + modifyPrivileges(parentPath, Privilege.JCR_WRITE, true); + try { + wsp.move(srcPath, destPath); + fail("Missing privilege jcr:nodeTypeManagement."); + } catch (AccessDeniedException e) { + // success + } + + // adding jcr:nodeTypeManagement privilege will grant permission to move. + modifyPrivileges(parentPath, PrivilegeRegistry.REP_WRITE, true); + wsp.move(srcPath, destPath); + } + + public void testSessionMove() throws RepositoryException, NotExecutableException { + Session s = getTestSession(); + String parentPath = childNode.getParent().getPath(); + String srcPath = childNode.getPath(); + String destPath = parentPath + "/"+ nodeName3; + + checkReadOnly(parentPath); + try { + s.move(srcPath, destPath); + s.save(); + fail("Missing write privilege."); + } catch (AccessDeniedException e) { + // success + } + + // with simple write privilege moving a node is not allowed. + modifyPrivileges(parentPath, Privilege.JCR_WRITE, true); + try { + s.move(srcPath, destPath); + s.save(); + fail("Missing privilege jcr:nodeTypeManagement."); + } catch (AccessDeniedException e) { + // success + } + + // adding jcr:nodeTypeManagement privilege will grant permission to move. + modifyPrivileges(parentPath, PrivilegeRegistry.REP_WRITE, true); + s.move(srcPath, destPath); + s.save(); + } + + public void testSessionImportXML() throws RepositoryException, NotExecutableException, IOException { + Session s = getTestSession(); + String parentPath = childNode.getPath(); + + checkReadOnly(parentPath); + try { + s.importXML(parentPath, getXmlForImport(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + s.save(); + fail("Missing write privilege."); + } catch (AccessDeniedException e) { + // success + } finally { + s.refresh(false); + } + + // with simple write privilege moving a node is not allowed. + modifyPrivileges(parentPath, Privilege.JCR_WRITE, true); + try { + s.importXML(parentPath, getXmlForImport(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + s.save(); + fail("Missing privilege jcr:nodeTypeManagement."); + } catch (AccessDeniedException e) { + // success + } finally { + s.refresh(false); + } + + // adding jcr:nodeTypeManagement privilege will grant permission to move. + modifyPrivileges(parentPath, PrivilegeRegistry.REP_WRITE, true); + s.importXML(parentPath, getXmlForImport(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + s.save(); + } + + public void testWorkspaceImportXML() throws RepositoryException, NotExecutableException, IOException { + Workspace wsp = getTestSession().getWorkspace(); + String parentPath = childNode.getPath(); + + checkReadOnly(parentPath); + try { + wsp.importXML(parentPath, getXmlForImport(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + fail("Missing write privilege."); + } catch (AccessDeniedException e) { + // success + } + + // with simple write privilege moving a node is not allowed. + modifyPrivileges(parentPath, Privilege.JCR_WRITE, true); + try { + wsp.importXML(parentPath, getXmlForImport(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + fail("Missing privilege jcr:nodeTypeManagement."); + } catch (AccessDeniedException e) { + // success + } + + // adding jcr:nodeTypeManagement privilege will grant permission to move. + modifyPrivileges(parentPath, PrivilegeRegistry.REP_WRITE, true); + wsp.importXML(parentPath, getXmlForImport(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + } + + /** + * Simple XML for testing permissions upon import. + * + * @return + */ + private InputStream getXmlForImport() { + String xml = "" + + "" + + " " + + " " + testNodeType + "" + + " " + + ""; + return new ByteArrayInputStream(xml.getBytes()); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractRepositoryOperationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractRepositoryOperationTest.java new file mode 100644 index 00000000000..c8de108ec81 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractRepositoryOperationTest.java @@ -0,0 +1,559 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.AccessManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * AbstractRepositoryOperationTest... + */ +public abstract class AbstractRepositoryOperationTest extends AbstractEvaluationTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + try { + for (AccessControlPolicy policy : acMgr.getPolicies(null)) { + acMgr.removePolicy(null, policy); + } + superuser.save(); + } finally { + super.tearDown(); + } + } + + private Workspace getTestWorkspace() throws RepositoryException { + return getTestSession().getWorkspace(); + } + + private void assertDefaultPrivileges(Name privName) throws Exception { + Privilege[] privs = privilegesFromName(privName.toString()); + // admin must be allowed + assertTrue(superuser.getAccessControlManager().hasPrivileges(null, privs)); + // test user must not be allowed + assertFalse(getTestACManager().hasPrivileges(null, privs)); + } + + private void assertPrivilege(Name privName, boolean isAllow) throws Exception { + Privilege[] privs = privilegesFromName(privName.toString()); + assertEquals(isAllow, getTestACManager().hasPrivileges(null, privs)); + } + + private void assertPermission(int permission, boolean isAllow) throws Exception { + AccessManager acMgr = ((SessionImpl) getTestSession()).getAccessManager(); + try { + acMgr.checkRepositoryPermission(permission); + if (!isAllow) { + fail(); + } + } catch (AccessDeniedException e) { + if (isAllow) { + fail(); + } + } + } + + private String getNewWorkspaceName(Workspace wsp) throws RepositoryException { + List awn = Arrays.asList(wsp.getAccessibleWorkspaceNames()); + String workspaceName = "new"; + int i = 0; + while (awn.contains(workspaceName)) { + workspaceName = "new_" + i++; + } + return workspaceName; + } + + private String getNewNamespacePrefix(Workspace wsp) throws RepositoryException { + String prefix = "prefix"; + List pfcs = Arrays.asList(wsp.getNamespaceRegistry().getPrefixes()); + int i = 0; + while (pfcs.contains(prefix)) { + prefix = "prefix" + i++; + } + return prefix; + } + + private String getNewNamespaceURI(Workspace wsp) throws RepositoryException { + String uri = "http://jackrabbit.apache.org/uri"; + List uris = Arrays.asList(wsp.getNamespaceRegistry().getURIs()); + int i = 0; + while (uris.contains(uri)) { + uri = "http://jackrabbit.apache.org/uri_" + i++; + } + return uri; + } + + private String getNewPrivilegeName(Workspace wsp) throws RepositoryException, NotExecutableException { + String privName = null; + AccessControlManager acMgr = wsp.getSession().getAccessControlManager(); + for (int i = 0; i < 100; i++) { + try { + Privilege p = acMgr.privilegeFromName(privName); + privName = "privilege-" + i; + } catch (Exception e) { + break; + } + } + + if (privName == null) { + throw new NotExecutableException("failed to define new privilege name."); + } + return privName; + } + + public void testWorkspaceCreation() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_WORKSPACE_MANAGEMENT); + + String wspName = getNewWorkspaceName(superuser.getWorkspace()); + try { + getTestWorkspace().createWorkspace(wspName); + fail("Workspace creation should be denied."); + } catch (AccessDeniedException e) { + // success + } + + wspName = getNewWorkspaceName(superuser.getWorkspace()); + try { + Workspace wsp = getTestWorkspace(); + wsp.createWorkspace(wspName, wsp.getName()); + fail("Workspace creation should be denied."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testWorkspaceCreationWithPrivilege() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_WORKSPACE_MANAGEMENT); + assertPermission(Permission.WORKSPACE_MNGMT, false); + + modifyPrivileges(null, NameConstants.JCR_WORKSPACE_MANAGEMENT.toString(), true); + // assert that permission have changed: + assertPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT, true); + assertPermission(Permission.WORKSPACE_MNGMT, true); + + try { + Workspace testWsp = getTestWorkspace(); + testWsp.createWorkspace(getNewWorkspaceName(superuser.getWorkspace())); + } finally { + modifyPrivileges(null, NameConstants.JCR_WORKSPACE_MANAGEMENT.toString(), false); + } + + assertPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT, false); + assertPermission(Permission.WORKSPACE_MNGMT, false); + } + + public void testWorkspaceDeletion() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_WORKSPACE_MANAGEMENT); + assertPermission(Permission.WORKSPACE_MNGMT, false); + + Workspace wsp = superuser.getWorkspace(); + String workspaceName = getNewWorkspaceName(wsp); + + wsp.createWorkspace(workspaceName); + try { + Workspace testWsp = getTestWorkspace(); + List wspNames = Arrays.asList(testWsp.getAccessibleWorkspaceNames()); + if (wspNames.contains(workspaceName)) { + testWsp.deleteWorkspace(workspaceName); + fail("Workspace deletion should be denied."); + } + } catch (AccessDeniedException e) { + // success + } finally { + // clean up (not supported by jackrabbit-core) + try { + superuser.getWorkspace().deleteWorkspace(workspaceName); + } catch (Exception e) { + // workspace removal is not supported by jackrabbit-core. + } + } + } + + public void testRegisterNodeType() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT); + assertPermission(Permission.NODE_TYPE_DEF_MNGMT, false); + + Workspace testWsp = getTestWorkspace(); + NodeTypeManager ntm = testWsp.getNodeTypeManager(); + NodeTypeTemplate ntd = ntm.createNodeTypeTemplate(); + ntd.setName("testNodeType"); + ntd.setMixin(true); + + try { + ntm.registerNodeType(ntd, true); + fail("Node type registration should be denied."); + } catch (AccessDeniedException e) { + // success + } + try { + ntm.registerNodeType(ntd, false); + fail("Node type registration should be denied."); + } catch (AccessDeniedException e) { + // success + } + + NodeTypeTemplate[] ntds = new NodeTypeTemplate[2]; + ntds[0] = ntd; + ntds[1] = ntm.createNodeTypeTemplate(); + ntds[1].setName("anotherNodeType"); + ntds[1].setDeclaredSuperTypeNames(new String[] {"nt:file"}); + try { + ntm.registerNodeTypes(ntds, true); + fail("Node type registration should be denied."); + } catch (AccessDeniedException e) { + // success + } + + try { + ntm.registerNodeTypes(ntds, false); + fail("Node type registration should be denied."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testRegisterNodeTypeWithPrivilege() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT); + assertPermission(Permission.NODE_TYPE_DEF_MNGMT, false); + + modifyPrivileges(null, NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString(), true); + assertPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT, true); + assertPermission(Permission.NODE_TYPE_DEF_MNGMT, true); + + try { + Workspace testWsp = getTestWorkspace(); + NodeTypeManager ntm = testWsp.getNodeTypeManager(); + NodeTypeTemplate ntd = ntm.createNodeTypeTemplate(); + ntd.setName("testNodeType"); + ntd.setMixin(true); + ntm.registerNodeType(ntd, true); + + NodeTypeTemplate[] ntds = new NodeTypeTemplate[2]; + ntds[0] = ntd; + ntds[1] = ntm.createNodeTypeTemplate(); + ntds[1].setName("anotherNodeType"); + ntds[1].setDeclaredSuperTypeNames(new String[] {"nt:file"}); + ntm.registerNodeTypes(ntds, true); + } finally { + modifyPrivileges(null, NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString(), false); + } + + assertPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT, false); + assertPermission(Permission.NODE_TYPE_DEF_MNGMT, false); + } + + public void testUnRegisterNodeType() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT); + assertPermission(Permission.NODE_TYPE_DEF_MNGMT, false); + + NodeTypeManager ntm = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeTemplate ntd = ntm.createNodeTypeTemplate(); + ntd.setName("testNodeType"); + ntd.setMixin(true); + ntm.registerNodeType(ntd, true); + + Workspace testWsp = getTestWorkspace(); + try { + try { + NodeTypeManager testNtm = testWsp.getNodeTypeManager(); + testNtm.unregisterNodeType(ntd.getName()); + fail("Namespace unregistration should be denied."); + } catch (AccessDeniedException e) { + // success + } + try { + NodeTypeManager testNtm = testWsp.getNodeTypeManager(); + testNtm.unregisterNodeTypes(new String[] {ntd.getName()}); + fail("Namespace unregistration should be denied."); + } catch (AccessDeniedException e) { + // success + } + } finally { + // clean up (not supported by jackrabbit-core) + try { + ntm.unregisterNodeType(ntd.getName()); + } catch (Exception e) { + // ns unregistration is not supported by jackrabbit-core. + } + } + + } + + public void testRegisterNamespace() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_NAMESPACE_MANAGEMENT); + assertPermission(Permission.NODE_TYPE_DEF_MNGMT, false); + + try { + Workspace testWsp = getTestWorkspace(); + testWsp.getNamespaceRegistry().registerNamespace(getNewNamespacePrefix(testWsp), getNewNamespaceURI(testWsp)); + fail("Namespace registration should be denied."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testRegisterNamespaceWithPrivilege() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_NAMESPACE_MANAGEMENT); + assertPermission(Permission.NAMESPACE_MNGMT, false); + + modifyPrivileges(null, NameConstants.JCR_NAMESPACE_MANAGEMENT.toString(), true); + assertPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT, true); + assertPermission(Permission.NAMESPACE_MNGMT, true); + + try { + Workspace testWsp = getTestWorkspace(); + testWsp.getNamespaceRegistry().registerNamespace(getNewNamespacePrefix(testWsp), getNewNamespaceURI(testWsp)); + } finally { + modifyPrivileges(null, NameConstants.JCR_NAMESPACE_MANAGEMENT.toString(), false); + } + + assertPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT, false); + assertPermission(Permission.NAMESPACE_MNGMT, false); + } + + public void testUnregisterNamespace() throws Exception { + assertDefaultPrivileges(NameConstants.JCR_NAMESPACE_MANAGEMENT); + assertPermission(Permission.NAMESPACE_MNGMT, false); + + Workspace wsp = superuser.getWorkspace(); + String pfx = getNewNamespacePrefix(wsp); + wsp.getNamespaceRegistry().registerNamespace(pfx, getNewNamespaceURI(wsp)); + + try { + Workspace testWsp = getTestWorkspace(); + testWsp.getNamespaceRegistry().unregisterNamespace(pfx); + fail("Namespace unregistration should be denied."); + } catch (AccessDeniedException e) { + // success + } finally { + // clean up (not supported by jackrabbit-core) + try { + superuser.getWorkspace().getNamespaceRegistry().unregisterNamespace(pfx); + } catch (Exception e) { + // ns unregistration is not supported by jackrabbit-core. + } + } + } + + public void testRegisterPrivilege() throws Exception { + assertDefaultPrivileges(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT_NAME); + assertPermission(Permission.PRIVILEGE_MNGMT, false); + + try { + Workspace testWsp = getTestWorkspace(); + ((JackrabbitWorkspace) testWsp).getPrivilegeManager().registerPrivilege(getNewPrivilegeName(testWsp), false, new String[0]); + fail("Privilege registration should be denied."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testRegisterPrivilegeWithPrivilege() throws Exception { + assertDefaultPrivileges(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT_NAME); + assertPermission(Permission.PRIVILEGE_MNGMT, false); + + modifyPrivileges(null, PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT_NAME.toString(), true); + assertPrivilege(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT_NAME, true); + assertPermission(Permission.PRIVILEGE_MNGMT, true); + + try { + Workspace testWsp = getTestWorkspace(); + ((JackrabbitWorkspace) testWsp).getPrivilegeManager().registerPrivilege(getNewPrivilegeName(testWsp), false, new String[0]); } finally { + modifyPrivileges(null, PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT_NAME.toString(), false); + } + + assertPrivilege(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT_NAME, false); + assertPermission(Permission.PRIVILEGE_MNGMT, false); + } + + public void testRepoPolicyAPI() throws Exception { + try { + // initial state: no repo level policy + AccessControlPolicy[] policies = acMgr.getPolicies(null); + assertNotNull(policies); + assertEquals(0, policies.length); + + AccessControlPolicy[] effective = acMgr.getEffectivePolicies(null); + assertNotNull(effective); + assertEquals(0, effective.length); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(null); + assertNotNull(it); + assertTrue(it.hasNext()); + AccessControlPolicy acp = it.nextAccessControlPolicy(); + assertNotNull(acp); + assertTrue(acp instanceof JackrabbitAccessControlPolicy); + + // modify the repo level policy + modifyPrivileges(null, NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString(), false); + modifyPrivileges(null, NameConstants.JCR_NAMESPACE_MANAGEMENT.toString(), true); + + AccessControlPolicy[] plcs = acMgr.getPolicies(null); + assertNotNull(plcs); + assertEquals(1, plcs.length); + assertTrue(plcs[0] instanceof AccessControlList); + + AccessControlList acl = (AccessControlList) plcs[0]; + AccessControlEntry[] aces = acl.getAccessControlEntries(); + assertNotNull(aces); + assertEquals(2, aces.length); + + assertPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT, true); + assertPermission(Permission.NAMESPACE_MNGMT, true); + + assertPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT, false); + assertPermission(Permission.NODE_TYPE_DEF_MNGMT, false); + + effective = acMgr.getEffectivePolicies(null); + assertNotNull(effective); + assertEquals(1, effective.length); + assertTrue(effective[0] instanceof AccessControlList); + + acl = (AccessControlList) effective[0]; + aces = acl.getAccessControlEntries(); + assertNotNull(aces); + assertEquals(2, aces.length); + + // change the policy: removing the second entry in the access control list + acl = (AccessControlList) acMgr.getPolicies(null)[0]; + AccessControlEntry toRemove = acl.getAccessControlEntries()[1]; + acl.removeAccessControlEntry(toRemove); + acMgr.setPolicy(null, acl); + superuser.save(); + + acl = (AccessControlList) acMgr.getPolicies(null)[0]; + aces = acl.getAccessControlEntries(); + assertNotNull(aces); + assertEquals(1, aces.length); + + assertPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT, false); + assertPermission(Permission.NAMESPACE_MNGMT, false); + + assertPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT, false); + assertPermission(Permission.NODE_TYPE_DEF_MNGMT, false); + + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(); + } finally { + // remove it again + for (AccessControlPolicy plc : acMgr.getPolicies(null)) { + acMgr.removePolicy(null, plc); + } + superuser.save(); + + // back to initial state: no repo level policy + AccessControlPolicy[] policies = acMgr.getPolicies(null); + assertNotNull(policies); + assertEquals(0, policies.length); + + AccessControlPolicy[] effective = acMgr.getEffectivePolicies(null); + assertNotNull(effective); + assertEquals(0, effective.length); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(null); + assertNotNull(it); + assertTrue(it.hasNext()); + AccessControlPolicy acp = it.nextAccessControlPolicy(); + assertNotNull(acp); + assertTrue(acp instanceof JackrabbitAccessControlPolicy); + } + } + + public void testGetEffectivePoliciesByPrincipal() throws Exception { + if (!(acMgr instanceof JackrabbitAccessControlManager)) { + throw new NotExecutableException(); + } + JackrabbitAccessControlManager jAcMgr = (JackrabbitAccessControlManager) acMgr; + Set principalSet = Collections.singleton(testUser.getPrincipal()); + + try { + // initial state: no repo level policy + AccessControlPolicy[] policies = acMgr.getPolicies(null); + assertNotNull(policies); + assertEquals(0, policies.length); + + AccessControlPolicy[] effective = jAcMgr.getEffectivePolicies(principalSet); + assertNotNull(effective); + assertEquals(0, effective.length); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(null); + assertTrue(it.hasNext()); + + // modify the repo level policy + modifyPrivileges(null, NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString(), false); + modifyPrivileges(null, NameConstants.JCR_NAMESPACE_MANAGEMENT.toString(), true); + + // verify that the effective policies for the given principal set + // is properly calculated. + AccessControlPolicy[] eff = jAcMgr.getEffectivePolicies(principalSet); + assertNotNull(eff); + assertEquals(1, eff.length); + assertTrue(eff[0] instanceof AccessControlList); + + AccessControlList acl = (AccessControlList) eff[0]; + AccessControlEntry[] aces = acl.getAccessControlEntries(); + assertNotNull(aces); + assertEquals(2, aces.length); + for (AccessControlEntry ace : aces) { + assertEquals(testUser.getPrincipal(), ace.getPrincipal()); + } + + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(); + } finally { + // remove it again + for (AccessControlPolicy plc : acMgr.getPolicies(null)) { + acMgr.removePolicy(null, plc); + } + superuser.save(); + + // back to initial state: no repo level policy + AccessControlPolicy[] policies = acMgr.getPolicies(null); + assertNotNull(policies); + assertEquals(0, policies.length); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractVersionManagementTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractVersionManagementTest.java new file mode 100644 index 00000000000..7b90dfdf355 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractVersionManagementTest.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.core.security.authorization; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.Privilege; +import javax.jcr.version.Version; + +/** AbstractVersionAccessTest... */ +public abstract class AbstractVersionManagementTest extends AbstractEvaluationTest { + + private static Logger log = LoggerFactory.getLogger(AbstractVersionManagementTest.class); + + protected Node createVersionableNode(Node parent) throws RepositoryException, NotExecutableException { + Node n = parent.addNode(nodeName1); + if (n.canAddMixin(mixVersionable)) { + n.addMixin(mixVersionable); + } else { + throw new NotExecutableException(); + } + parent.save(); + return n; + } + + public void testAddMixVersionable() throws RepositoryException, NotExecutableException { + Node trn = getTestNode(); + modifyPrivileges(trn.getPath(), PrivilegeRegistry.REP_WRITE, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_VERSION_MANAGEMENT, false); + Node n = trn.addNode(nodeName1); + try { + if (n.canAddMixin(mixVersionable)) { + n.addMixin(mixVersionable); + } else { + throw new NotExecutableException(); + } + trn.save(); + fail("Test session does not have write permission in the version storage -> adding mixin must fail."); + } catch (AccessDeniedException e) { + // success + log.debug(e.getMessage()); + // ... but autocreated versionable node properties must not be present + assertFalse(n.isNodeType(mixVersionable)); + assertFalse(n.hasProperty("jcr:isCheckedOut")); + assertFalse(n.hasProperty(jcrVersionHistory)); + } + } + + public void testAddMixVersionable2() throws RepositoryException, NotExecutableException { + Node trn = getTestNode(); + modifyPrivileges(trn.getPath(), PrivilegeRegistry.REP_WRITE, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_NODE_TYPE_MANAGEMENT, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_VERSION_MANAGEMENT, true); + + Node n = createVersionableNode(trn); + n.checkin(); + n.checkout(); + } + + public void testWriteVersionStore() throws RepositoryException, NotExecutableException { + Node trn = getTestNode(); + modifyPrivileges(trn.getPath(), PrivilegeRegistry.REP_WRITE, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_VERSION_MANAGEMENT, false); + + Node n = createVersionableNode(testRootNode); + try { + Node n2 = trn.getNode(nodeName1); + n2.checkin(); + fail("No write permission in the version storage."); + } catch (AccessDeniedException e) { + // success + log.debug(e.getMessage()); + // ... but the property must not be modified nor indicating + // checkedIn status + Property p = n.getProperty("jcr:isCheckedOut"); + assertFalse(p.isModified()); + assertTrue(n.getProperty("jcr:isCheckedOut").getValue().getBoolean()); + } + } + + public void testRemoveVersion() throws RepositoryException, NotExecutableException { + Node trn = getTestNode(); + Node n = createVersionableNode(testRootNode); + modifyPrivileges(trn.getPath(), Privilege.JCR_VERSION_MANAGEMENT, true); + + // test session should now be able to create versionable nodes, checkout + // and checkin them, read the version/v-histories. + + Node testNode = trn.getNode(nodeName1); + Version v = testNode.checkin(); + testNode.checkout(); + testNode.checkin(); + + // remove ability to edit version information + // -> VersionHistory.removeVersion must not be allowed. + modifyPrivileges(trn.getPath(), Privilege.JCR_VERSION_MANAGEMENT, false); + try { + testNode.getVersionHistory().removeVersion(v.getName()); + fail("TestSession without remove privilege on the v-storage must not be able to remove a version."); + } catch (AccessDeniedException e) { + // success + log.debug(e.getMessage()); + } + } + + public void testRemoveVersion2() throws RepositoryException, NotExecutableException { + Node trn = getTestNode(); + Node n = createVersionableNode(testRootNode); + modifyPrivileges(trn.getPath(), Privilege.JCR_VERSION_MANAGEMENT, true); + + Node testNode = trn.getNode(nodeName1); + Version v = testNode.checkin(); + testNode.checkout(); + testNode.checkin(); + + // -> VersionHistory.removeVersion must not be allowed. + try { + testNode.getVersionHistory().removeVersion(v.getName()); + fail("TestSession without remove privilege on the v-storage must not be able to remove a version."); + } catch (AccessDeniedException e) { + // success + log.debug(e.getMessage()); + } + } + + public void testRemoveVersion3() throws RepositoryException, NotExecutableException { + Node trn = getTestNode(); + Node n = createVersionableNode(testRootNode); + + String path = getTestSession().getRootNode().getPath(); + JackrabbitAccessControlList tmpl = getPolicy(acMgr, path, testUser.getPrincipal()); + AccessControlEntry entry; + try { + // NOTE: don't use 'modifyPrivileges' in order not to have the + // root-policy cleared on tear-down. + tmpl.addEntry(testUser.getPrincipal(), privilegesFromName(Privilege.JCR_VERSION_MANAGEMENT), true, getRestrictions(superuser, path)); + acMgr.setPolicy(tmpl.getPath(), tmpl); + superuser.save(); + + Node testNode = trn.getNode(nodeName1); + Version v = testNode.checkin(); + testNode.checkout(); + testNode.checkin(); + + // -> VersionHistory.removeVersion must be allowed + testNode.getVersionHistory().removeVersion(v.getName()); + } finally { + // revert privilege modification (manually remove the ACE added) + AccessControlEntry[] entries = tmpl.getAccessControlEntries(); + for (AccessControlEntry entry1 : entries) { + if (entry1.getPrincipal().equals(testUser.getPrincipal())) { + tmpl.removeAccessControlEntry(entry1); + } + } + acMgr.setPolicy(tmpl.getPath(), tmpl); + superuser.save(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractWriteTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractWriteTest.java new file mode 100644 index 00000000000..340a785850f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/AbstractWriteTest.java @@ -0,0 +1,1320 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.jackrabbit.api.JackrabbitNode; +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.core.UserTransactionImpl; +import org.apache.jackrabbit.test.JUnitTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.observation.EventResult; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.observation.Event; +import javax.jcr.observation.ObservationManager; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import javax.transaction.UserTransaction; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; + +/** + * AbstractEvaluationTest... + */ +public abstract class AbstractWriteTest extends AbstractEvaluationTest { + + protected static final long DEFAULT_WAIT_TIMEOUT = 5000; + + protected String path; + protected String childNPath; + protected String childNPath2; + protected String childPPath; + protected String childchildPPath; + protected String siblingPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create some nodes below the test root in order to apply ac-stuff + Node node = testRootNode.addNode(nodeName1, testNodeType); + Node cn1 = node.addNode(nodeName2, testNodeType); + Property cp1 = node.setProperty(propertyName1, "anyValue"); + Node cn2 = node.addNode(nodeName3, testNodeType); + + Property ccp1 = cn1.setProperty(propertyName1, "childNodeProperty"); + + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + superuser.save(); + + path = node.getPath(); + childNPath = cn1.getPath(); + childNPath2 = cn2.getPath(); + childPPath = cp1.getPath(); + childchildPPath = ccp1.getPath(); + siblingPath = n2.getPath(); + } + + public void testGrantedPermissions() throws RepositoryException, AccessDeniedException, NotExecutableException { + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + // give 'testUser' ADD_CHILD_NODES|MODIFY_PROPERTIES privileges at 'path' + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_MODIFY_PROPERTIES + }); + givePrivileges(path, privileges, getRestrictions(superuser, path)); + /* + testuser must now have + - ADD_NODE permission for child node + - SET_PROPERTY permission for child props + - REMOVE permission for child-props + - READ-only permission for the node at 'path' + + testuser must not have + - REMOVE permission for child node + */ + Session testSession = getTestSession(); + String nonExChildPath = path + "/anyItem"; + assertTrue(testSession.hasPermission(nonExChildPath, "read,add_node,set_property")); + assertFalse(testSession.hasPermission(nonExChildPath, "remove")); + + Node testN = testSession.getNode(path); + + // must be allowed to add child node + testN.addNode(nodeName3); + testSession.save(); + + // must be allowed to remove child-property + testSession.getProperty(childPPath).remove(); + testSession.save(); + + // must be allowed to set child property again + testN.setProperty(Text.getName(childPPath), "othervalue"); + testSession.save(); + + // must not be allowed to remove child nodes + try { + testSession.getNode(childNPath).remove(); + testSession.save(); + fail("test-user is not allowed to remove a node below " + path); + } catch (AccessDeniedException e) { + // success + } + + // must have read-only access on 'testN' and it's sibling + assertTrue(testSession.hasPermission(path, "read")); + assertFalse(testSession.hasPermission(path, "add_node,set_property,remove")); + checkReadOnly(siblingPath); + } + + public void testDeniedPermission() throws RepositoryException, NotExecutableException, InterruptedException { + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + // withdraw READ privilege to 'testUser' at 'path' + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + withdrawPrivileges(childNPath, privileges, getRestrictions(superuser, childNPath)); + /* + testuser must now have + - READ-only permission at path + - READ-only permission for the child-props of path + + testuser must not have + - any permission on child-node and all its subtree + */ + + // must still have read-access to path, ... + Session testSession = getTestSession(); + assertTrue(testSession.hasPermission(path, "read")); + Node n = testSession.getNode(path); + // ... siblings of childN + testSession.getNode(childNPath2); + // ... and props of path + assertTrue(n.getProperties().hasNext()); + + //testSession must not have access to 'childNPath' + assertFalse(testSession.itemExists(childNPath)); + try { + testSession.getNode(childNPath); + fail("Read access has been denied -> cannot retrieve child node."); + } catch (PathNotFoundException e) { + // ok. + } + /* + -> must not have access to subtree below 'childNPath' + */ + assertFalse(testSession.itemExists(childchildPPath)); + try { + testSession.getItem(childchildPPath); + fail("Read access has been denied -> cannot retrieve prop below child node."); + } catch (PathNotFoundException e) { + // ok. + } + } + + public void testAccessControlRead() throws NotExecutableException, RepositoryException { + AccessControlManager testAcMgr = getTestACManager(); + checkReadOnly(path); + + // re-grant READ in order to have an ACL-node + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + JackrabbitAccessControlList tmpl = givePrivileges(path, privileges, getRestrictions(superuser, path)); + // make sure the 'rep:policy' node has been created. + assertTrue(superuser.itemExists(tmpl.getPath() + "/rep:policy")); + + Session testSession = getTestSession(); + /* + Testuser must still have READ-only access only and must not be + allowed to view the acl-node that has been created. + */ + assertFalse(testAcMgr.hasPrivileges(path, privilegesFromName(Privilege.JCR_READ_ACCESS_CONTROL))); + assertFalse(testSession.itemExists(path + "/rep:policy")); + + Node n = testSession.getNode(tmpl.getPath()); + assertFalse(n.hasNode("rep:policy")); + try { + n.getNode("rep:policy"); + fail("Accessing the rep:policy node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // ok. + } + + /* Finally the test user must not be allowed to remove the policy. */ + try { + testAcMgr.removePolicy(path, new AccessControlPolicy() {}); + fail("Test user must not be allowed to remove the access control policy."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testAccessControlModification() throws RepositoryException, NotExecutableException { + AccessControlManager testAcMgr = getTestACManager(); + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + Session testSession = getTestSession(); + + // give 'testUser' ADD_CHILD_NODES|MODIFY_PROPERTIES| REMOVE_CHILD_NODES privileges at 'path' + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_REMOVE_CHILD_NODES, + Privilege.JCR_MODIFY_PROPERTIES + }); + JackrabbitAccessControlList tmpl = givePrivileges(path, privileges, getRestrictions(superuser, path)); + /* + testuser must not have + - permission to view AC items + - permission to modify AC items + */ + + // make sure the 'rep:policy' node has been created. + assertTrue(superuser.itemExists(tmpl.getPath() + "/rep:policy")); + // the policy node however must not be visible to the test-user + assertFalse(testSession.itemExists(tmpl.getPath() + "/rep:policy")); + try { + testAcMgr.getPolicies(tmpl.getPath()); + fail("test user must not have READ_AC privilege."); + } catch (AccessDeniedException e) { + // success + } + try { + testAcMgr.getEffectivePolicies(tmpl.getPath()); + fail("test user must not have READ_AC privilege."); + } catch (AccessDeniedException e) { + // success + } + try { + testAcMgr.getEffectivePolicies(path); + fail("test user must not have READ_AC privilege."); + } catch (AccessDeniedException e) { + // success + } + try { + testAcMgr.removePolicy(tmpl.getPath(), new AccessControlPolicy() {}); + fail("test user must not have MODIFY_AC privilege."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testWithDrawRead() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + // give 'testUser' READ_AC|MODIFY_AC privileges at 'path' + Privilege[] grPrivs = privilegesFromName(PrivilegeRegistry.REP_WRITE); + givePrivileges(path, grPrivs, getRestrictions(superuser, path)); + // withdraw the READ privilege + Privilege[] dnPrivs = privilegesFromName(Privilege.JCR_READ); + withdrawPrivileges(path, dnPrivs, getRestrictions(superuser, path)); + + // test if login as testuser -> item at path must not exist. + Session s = null; + try { + s = getHelper().getRepository().login(creds); + assertFalse(s.itemExists(path)); + } finally { + if (s != null) { + s.logout(); + } + } + } + + public void testEventGeneration() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + Session testSession = getTestSession(); + + // withdraw the READ privilege + Privilege[] dnPrivs = privilegesFromName(Privilege.JCR_READ); + withdrawPrivileges(path, dnPrivs, getRestrictions(superuser, path)); + + // testUser registers a event listener for 'path + ObservationManager obsMgr = testSession.getWorkspace().getObservationManager(); + EventResult listener = new EventResult(((JUnitTest) this).log); + try { + obsMgr.addEventListener(listener, Event.NODE_REMOVED, path, true, new String[0], new String[0], true); + + // superuser removes the node with childNPath in order to provoke + // events being generated + superuser.getItem(childNPath).remove(); + superuser.save(); + + obsMgr.removeEventListener(listener); + // since the testUser does not have read-permission on the removed + // node, no corresponding event must be generated. + Event[] evts = listener.getEvents(DEFAULT_WAIT_TIMEOUT); + for (Event evt : evts) { + if (evt.getType() == Event.NODE_REMOVED && + evt.getPath().equals(childNPath)) { + fail("TestUser does not have READ permission below " + path + " -> events below must not show up."); + } + } + } finally { + obsMgr.removeEventListener(listener); + } + } + + public void testInheritance() throws RepositoryException, NotExecutableException { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + // give 'modify_properties' and 'remove_node' privilege on 'path' + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_REMOVE_NODE, Privilege.JCR_MODIFY_PROPERTIES}); + givePrivileges(path, privileges, getRestrictions(superuser, path)); + // give 'add-child-nodes', remove_child_nodes' on 'childNPath' + privileges = privilegesFromNames(new String[] { + Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_REMOVE_CHILD_NODES}); + givePrivileges(childNPath, privileges, getRestrictions(superuser, childNPath)); + + /* + since evaluation respects inheritance through the node + hierarchy, the following privileges must now be given at 'childNPath': + - jcr:read + - jcr:modifyProperties + - jcr:addChildNodes + - jcr:removeChildNodes + - jcr:removeNode + */ + Privilege[] expectedPrivileges = privilegesFromNames(new String[] { + Privilege.JCR_READ, + Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_REMOVE_CHILD_NODES, + Privilege.JCR_REMOVE_NODE, + Privilege.JCR_MODIFY_PROPERTIES + }); + assertTrue(testAcMgr.hasPrivileges(childNPath, expectedPrivileges)); + + /* + ... permissions granted at childNPath: + - read + - set-property + + BUT NOT: + - add-node + - remove. + */ + String aActions = javax.jcr.Session.ACTION_SET_PROPERTY + "," + javax.jcr.Session.ACTION_READ; + assertTrue(testSession.hasPermission(childNPath, aActions)); + String dActions = javax.jcr.Session.ACTION_REMOVE + "," + javax.jcr.Session.ACTION_ADD_NODE; + assertFalse(testSession.hasPermission(childNPath, dActions)); + + /* + ... permissions granted at any child item of child-path: + - read + - set-property + - add-node + - remove + */ + String nonExistingItemPath = childNPath + "/anyItem"; + assertTrue(testSession.hasPermission(nonExistingItemPath, aActions + "," + dActions)); + + /* try adding a new child node -> must succeed. */ + Node childN = testSession.getNode(childNPath); + String testPath = childN.addNode(nodeName2).getPath(); + + /* test privileges on the 'new' child node */ + assertTrue(testAcMgr.hasPrivileges(testPath, expectedPrivileges)); + + /* repeat test after save. */ + testSession.save(); + assertTrue(testAcMgr.hasPrivileges(testPath, expectedPrivileges)); + } + + public void testRemovePermission() throws NotExecutableException, RepositoryException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + Session testSession = getTestSession(); + + Privilege[] rmChildNodes = privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES); + + // add 'remove_child_nodes' privilege at 'path' + givePrivileges(path, rmChildNodes, getRestrictions(superuser, path)); + /* + expected result: + - neither node at path nor at childNPath can be removed since + REMOVE_NODE privilege is missing. + */ + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_REMOVE)); + assertFalse(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + } + + public void testRemovePermission2() throws NotExecutableException, RepositoryException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + Session testSession = getTestSession(); + + Privilege[] rmChildNodes = privilegesFromName(Privilege.JCR_REMOVE_NODE); + + // add 'remove_node' privilege at 'path' + givePrivileges(path, rmChildNodes, getRestrictions(superuser, path)); + /* + expected result: + - neither node at path nor at childNPath can be removed permission + due to missing remove_child_nodes privilege. + */ + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_REMOVE)); + assertFalse(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + } + + public void testRemovePermission3() throws NotExecutableException, RepositoryException { + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + Session testSession = getTestSession(); + + Privilege[] privs = privilegesFromNames(new String[] { + Privilege.JCR_REMOVE_CHILD_NODES, Privilege.JCR_REMOVE_NODE + }); + // add 'remove_node' and 'remove_child_nodes' privilege at 'path' + givePrivileges(path, privs, getRestrictions(superuser, path)); + /* + expected result: + - missing remove permission at path since REMOVE_CHILD_NODES present + at path only applies for nodes below. REMOVE_CHILD_NODES must + be present at the parent instead (which isn't) + - remove permission is however granted at childNPath. + - privileges: both at path and at childNPath 'remove_node' and + 'remove_child_nodes' are present. + */ + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_REMOVE)); + assertTrue(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + + assertTrue(testAcMgr.hasPrivileges(path, privs)); + assertTrue(testAcMgr.hasPrivileges(childNPath, privs)); + } + + public void testRemovePermission4() throws NotExecutableException, RepositoryException { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Privilege[] rmChildNodes = privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES); + Privilege[] rmNode = privilegesFromName(Privilege.JCR_REMOVE_NODE); + + // add 'remove_child_nodes' privilege at 'path'... + givePrivileges(path, rmChildNodes, getRestrictions(superuser, path)); + // ... and add 'remove_node' privilege at 'childNPath' + givePrivileges(childNPath, rmNode, getRestrictions(superuser, childNPath)); + /* + expected result: + - remove not allowed for node at path + - remove-permission present for node at childNPath + - both remove_node and remove_childNodes privilege present at childNPath + */ + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_REMOVE)); + assertTrue(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + assertTrue(testAcMgr.hasPrivileges(childNPath, new Privilege[] {rmChildNodes[0], rmNode[0]})); + } + + public void testRemovePermission5() throws NotExecutableException, RepositoryException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Privilege[] rmNode = privilegesFromName(Privilege.JCR_REMOVE_NODE); + + // add 'remove_node' privilege at 'childNPath' + givePrivileges(childNPath, rmNode, getRestrictions(superuser, childNPath)); + /* + expected result: + - node at childNPath can't be removed since REMOVE_CHILD_NODES is missing. + */ + assertFalse(getTestSession().hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + } + + public void testRemovePermission6() throws NotExecutableException, RepositoryException { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Privilege[] privs = privilegesFromNames(new String[] { + Privilege.JCR_REMOVE_CHILD_NODES, Privilege.JCR_REMOVE_NODE + }); + Privilege[] rmNode = privilegesFromName(Privilege.JCR_REMOVE_NODE); + + // add 'remove_child_nodes' and 'remove_node' privilege at 'path' + givePrivileges(path, privs, getRestrictions(superuser, path)); + // ... but deny 'remove_node' at childNPath + withdrawPrivileges(childNPath, rmNode, getRestrictions(superuser, childNPath)); + /* + expected result: + - neither node at path nor at childNPath could be removed. + - no remove_node privilege at childNPath + - read, remove_child_nodes privilege at childNPath + */ + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_REMOVE)); + assertFalse(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + assertTrue(testAcMgr.hasPrivileges(childNPath, privilegesFromNames(new String[] {Privilege.JCR_READ, Privilege.JCR_REMOVE_CHILD_NODES}))); + assertFalse(testAcMgr.hasPrivileges(childNPath, privilegesFromName(Privilege.JCR_REMOVE_NODE))); + } + + public void testRemovePermission7() throws NotExecutableException, RepositoryException { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Privilege[] rmChildNodes = privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES); + Privilege[] rmNode = privilegesFromName(Privilege.JCR_REMOVE_NODE); + + // deny 'remove_child_nodes' at 'path' + withdrawPrivileges(path, rmChildNodes, getRestrictions(superuser, path)); + // ... but allow 'remove_node' at childNPath + givePrivileges(childNPath, rmNode, getRestrictions(superuser, childNPath)); + /* + expected result: + - node at childNPath can't be removed. + */ + assertFalse(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + + // additionally add remove_child_nodes privilege at 'childNPath' + givePrivileges(childNPath, rmChildNodes, getRestrictions(superuser, childNPath)); + /* + expected result: + - node at childNPath still can't be removed. + - but both privileges (remove_node, remove_child_nodes) are present. + */ + assertFalse(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + assertTrue(testAcMgr.hasPrivileges(childNPath, new Privilege[] {rmChildNodes[0], rmNode[0]})); + } + + public void testRemovePermission8() throws NotExecutableException, RepositoryException { + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Privilege[] rmChildNodes = privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES); + Privilege[] rmNode = privilegesFromName(Privilege.JCR_REMOVE_NODE); + + // add 'remove_child_nodes' at 'path + givePrivileges(path, rmChildNodes, getRestrictions(superuser, path)); + // deny 'remove_node' at 'path' + withdrawPrivileges(path, rmNode, getRestrictions(superuser, path)); + // and allow 'remove_node' at childNPath + givePrivileges(childNPath, rmNode, getRestrictions(superuser, childNPath)); + /* + expected result: + - remove permission must be granted at childNPath + */ + assertTrue(getTestSession().hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + assertTrue(testAcMgr.hasPrivileges(childNPath, new Privilege[] {rmChildNodes[0], rmNode[0]})); + } + + public void testSessionMove() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + Session testSession = getTestSession(); + + String destPath = path + "/" + nodeName1; + + // give 'add_child_nodes' and 'nt-management' privilege + // -> not sufficient privileges for a move + givePrivileges(path, privilegesFromNames(new String[] {Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_NODE_TYPE_MANAGEMENT}), getRestrictions(superuser, path)); + try { + testSession.move(childNPath, destPath); + testSession.save(); + fail("Move requires add and remove permission."); + } catch (AccessDeniedException e) { + // success. + } + + // add 'remove_child_nodes' at 'path + // -> not sufficient for a move since 'remove_node' privilege is missing + // on the move-target + givePrivileges(path, privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES), getRestrictions(superuser, path)); + try { + testSession.move(childNPath, destPath); + testSession.save(); + fail("Move requires add and remove permission."); + } catch (AccessDeniedException e) { + // success. + } + + // allow 'remove_node' at childNPath + // -> now move must succeed + givePrivileges(childNPath, privilegesFromName(Privilege.JCR_REMOVE_NODE), getRestrictions(superuser, childNPath)); + testSession.move(childNPath, destPath); + testSession.save(); + + // withdraw 'add_child_nodes' privilege on former src-parent + // -> moving child-node back must fail + withdrawPrivileges(path, privilegesFromName(Privilege.JCR_ADD_CHILD_NODES), getRestrictions(superuser, path)); + try { + testSession.move(destPath, childNPath); + testSession.save(); + fail("Move requires add and remove permission."); + } catch (AccessDeniedException e) { + // success. + } + } + + public void testWorkspaceMove() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + Session testSession = getTestSession(); + + String destPath = path + "/" + nodeName1; + + // give 'add_child_nodes', 'nt-mgmt' privilege + // -> not sufficient privileges for a move. + givePrivileges(path, privilegesFromNames(new String[] {Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_NODE_TYPE_MANAGEMENT}), getRestrictions(superuser, path)); + try { + testSession.getWorkspace().move(childNPath, destPath); + fail("Move requires add and remove permission."); + } catch (AccessDeniedException e) { + // success. + } + + // add 'remove_child_nodes' at 'path + // -> no sufficient for a move since 'remove_node' privilege is missing + // on the move-target + givePrivileges(path, privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES), getRestrictions(superuser, path)); + try { + testSession.getWorkspace().move(childNPath, destPath); + fail("Move requires add and remove permission."); + } catch (AccessDeniedException e) { + // success. + } + + // allow 'remove_node' at childNPath + // -> now move must succeed + givePrivileges(childNPath, privilegesFromName(Privilege.JCR_REMOVE_NODE), + getRestrictions(superuser, childNPath)); + testSession.getWorkspace().move(childNPath, destPath); + + // withdraw 'add_child_nodes' privilege on former src-parent + // -> moving child-node back must fail + withdrawPrivileges(path, privilegesFromName(Privilege.JCR_ADD_CHILD_NODES), getRestrictions(superuser, path)); + try { + testSession.getWorkspace().move(destPath, childNPath); + fail("Move requires add and remove permission."); + } catch (AccessDeniedException e) { + // success. + } + } + + public void testGroupPermissions() throws NotExecutableException, RepositoryException { + Group testGroup = getTestGroup(); + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + /* add privileges for the Group the test-user is member of */ + Privilege[] privileges = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + givePrivileges(path, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + + /* testuser must get the permissions/privileges inherited from + the group it is member of. + */ + String actions = javax.jcr.Session.ACTION_SET_PROPERTY + "," + javax.jcr.Session.ACTION_READ; + + assertTrue(getTestSession().hasPermission(path, actions)); + Privilege[] privs = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + assertTrue(testAcMgr.hasPrivileges(path, privs)); + } + + public void testMixedUserGroupPermissions() throws NotExecutableException, RepositoryException { + Group testGroup = getTestGroup(); + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + /* explicitly withdraw MODIFY_PROPERTIES for the user */ + Privilege[] privileges = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + withdrawPrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* give MODIFY_PROPERTIES privilege for a Group the test-user is member of */ + givePrivileges(path, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* + since user-permissions overrule the group permissions, testuser must + not have set_property action / modify_properties privilege. + */ + String actions = javax.jcr.Session.ACTION_SET_PROPERTY; + assertFalse(getTestSession().hasPermission(path, actions)); + assertFalse(testAcMgr.hasPrivileges(path, privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES))); + } + + public void testInheritanceAndMixedUserGroupPermissions() throws RepositoryException, NotExecutableException { + Group testGroup = getTestGroup(); + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + Privilege[] privileges = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + + /* give MODIFY_PROPERTIES privilege for testGroup at 'path' */ + givePrivileges(path, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + + /* withdraw MODIFY_PROPERTIES for the user at 'path' */ + withdrawPrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + + /* + since user-permissions overrule the group permissions, testuser must + not have set_property action / modify_properties privilege. + */ + assertFalse(testAcMgr.hasPrivileges(path, privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES))); + + /* + give MODIFY_PROPERTIES privilege for everyone at 'childNPath' + -> user-privileges still overrule group privileges + */ + givePrivileges(childNPath, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + assertFalse(testAcMgr.hasPrivileges(childNPath, privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES))); + } + + public void testNewNodes() throws RepositoryException, NotExecutableException { + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + /* create some new nodes below 'path' */ + Node n = superuser.getNode(path); + for (int i = 0; i < 5; i++) { + n = n.addNode(nodeName2, testNodeType); + } + superuser.save(); + + /* make sure the same privileges/permissions are granted as at path. */ + String childPath = n.getPath(); + Privilege[] privs = testAcMgr.getPrivileges(childPath); + PrivilegeManagerImpl privilegeMgr = (PrivilegeManagerImpl) ((JackrabbitWorkspace) getTestSession().getWorkspace()).getPrivilegeManager(); + assertEquals(privilegeMgr.getBits(privilegesFromName(Privilege.JCR_READ)), + privilegeMgr.getBits(privs)); + getTestSession().checkPermission(childPath, javax.jcr.Session.ACTION_READ); + } + + public void testNonExistingItem() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on the root node and below + */ + Session testSession = getTestSession(); + String rootPath = testSession.getRootNode().getPath(); + checkReadOnly(rootPath); + testSession.checkPermission(rootPath + "nonExistingItem", javax.jcr.Session.ACTION_READ); + } + + public void testACItemsAreProtected() throws NotExecutableException, RepositoryException { + // search for a rep:policy node + Node policyNode = findPolicyNode(superuser.getRootNode()); + if (policyNode == null) { + throw new NotExecutableException("no policy node found."); + } + + assertTrue("The rep:Policy node must be protected", policyNode.getDefinition().isProtected()); + try { + policyNode.remove(); + fail("rep:Policy node must be protected."); + } catch (ConstraintViolationException e) { + // success + } + + for (NodeIterator it = policyNode.getNodes(); it.hasNext();) { + Node n = it.nextNode(); + if (n.isNodeType("rep:ACE")) { + try { + n.remove(); + fail("ACE node must be protected."); + } catch (ConstraintViolationException e) { + // success + } + break; + } + } + + try { + policyNode.setProperty("test", "anyvalue"); + fail("rep:policy node must be protected."); + } catch (ConstraintViolationException e) { + // success + } + try { + policyNode.addNode("test", "rep:ACE"); + fail("rep:policy node must be protected."); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * the ADD_CHILD_NODES privileges assigned on a node to a specific principal + * grants the corresponding user the permission to add nodes below the + * target node but not 'at' the target node. + * + * @throws RepositoryException If an error occurs. + * @throws NotExecutableException If the test cannot be executed. + */ + public void testAddChildNodePrivilege() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + /* create a child node below node at 'path' */ + Node n = superuser.getNode(path); + n = n.addNode(nodeName2, testNodeType); + superuser.save(); + + /* add 'add_child_nodes' privilege for testSession at path. */ + Privilege[] privileges = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + givePrivileges(path, privileges, getRestrictions(superuser, path)); + + /* test permissions. expected result: + - testSession cannot add child-nodes at 'path' + - testSession can add child-nodes below path + */ + Session testSession = getTestSession(); + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_ADD_NODE)); + assertTrue(testSession.hasPermission(path+"/anychild", javax.jcr.Session.ACTION_ADD_NODE)); + String childPath = n.getPath(); + assertTrue(testSession.hasPermission(childPath, javax.jcr.Session.ACTION_ADD_NODE)); + } + + public void testAclReferingToRemovedPrincipal() throws + NotExecutableException, RepositoryException { + + JackrabbitAccessControlList acl = givePrivileges(path, privilegesFromName(PrivilegeRegistry.REP_WRITE), getRestrictions(superuser, path)); + String acPath = acl.getPath(); + + // remove the test user + testUser.remove(); + if (!getUserManager(superuser).isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + testUser = null; + + // try to retrieve the acl again + Session s = getHelper().getSuperuserSession(); + try { + AccessControlManager acMgr = getAccessControlManager(s); + acMgr.getPolicies(acPath); + } finally { + s.logout(); + } + } + + public void testSingleDenyAfterAllAllowed() throws + NotExecutableException, RepositoryException { + + /* add 'all' privilege for testSession at path. */ + Privilege[] allPrivileges = privilegesFromName(Privilege.JCR_ALL); + givePrivileges(path, allPrivileges, getRestrictions(superuser, path)); + + /* deny a single privilege */ + Privilege[] lockPrivileges = privilegesFromName(Privilege.JCR_LOCK_MANAGEMENT); + withdrawPrivileges(path, lockPrivileges, getRestrictions(superuser, path)); + + /* test permissions. expected result: + - testSession cannot lock at 'path' + - testSession doesn't have ALL privilege at path + */ + Session testSession = getTestSession(); + AccessControlManager acMgr = testSession.getAccessControlManager(); + + assertFalse(acMgr.hasPrivileges(path, allPrivileges)); + assertFalse(acMgr.hasPrivileges(path, lockPrivileges)); + + List remainingprivs = new ArrayList(Arrays.asList(allPrivileges[0].getAggregatePrivileges())); + remainingprivs.remove(lockPrivileges[0]); + assertTrue(acMgr.hasPrivileges(path, remainingprivs.toArray(new Privilege[remainingprivs.size()]))); + } + + public void testReorder() throws RepositoryException, NotExecutableException { + Session testSession = getTestSession(); + Node n = testSession.getNode(path); + try { + if (!n.getPrimaryNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Reordering child nodes is not supported.."); + } + + n.orderBefore(Text.getName(childNPath), Text.getName(childNPath2)); + testSession.save(); + fail("test session must not be allowed to reorder nodes."); + } catch (AccessDeniedException e) { + // success. + } + + // give 'add_child_nodes' and 'nt-management' privilege + // -> not sufficient privileges for a reorder + givePrivileges(path, privilegesFromNames(new String[] {Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_NODE_TYPE_MANAGEMENT}), getRestrictions(superuser, path)); + try { + n.orderBefore(Text.getName(childNPath), Text.getName(childNPath2)); + testSession.save(); + fail("test session must not be allowed to reorder nodes."); + } catch (AccessDeniedException e) { + // success. + } + + // add 'remove_child_nodes' at 'path + // -> reorder must now succeed + givePrivileges(path, privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES), getRestrictions(superuser, path)); + n.orderBefore(Text.getName(childNPath), Text.getName(childNPath2)); + testSession.save(); + } + + public void testRename() throws RepositoryException, NotExecutableException { + Session testSession = getTestSession(); + Node child = testSession.getNode(childNPath); + try { + ((JackrabbitNode) child).rename("rename"); + testSession.save(); + fail("test session must not be allowed to rename nodes."); + } catch (AccessDeniedException e) { + // success. + } + + // give 'add_child_nodes' and 'nt-management' privilege + // -> not sufficient privileges for a renaming of the child + givePrivileges(path, privilegesFromNames(new String[] {Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_NODE_TYPE_MANAGEMENT}), getRestrictions(superuser, path)); + try { + ((JackrabbitNode) child).rename("rename"); + testSession.save(); + fail("test session must not be allowed to rename nodes."); + } catch (AccessDeniedException e) { + // success. + } + + // add 'remove_child_nodes' at 'path + // -> rename of child must now succeed + givePrivileges(path, privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES), getRestrictions(superuser, path)); + ((JackrabbitNode) child).rename("rename"); + testSession.save(); + } + + /** + * Test case for JCR-2420 + * + * @throws Exception + * @see JCR-2420 + */ + public void testRemovalJCR242() throws Exception { + Privilege[] allPriv = privilegesFromNames(new String[] {Privilege.JCR_ALL}); + + /* grant ALL privilege for testUser at 'path' */ + givePrivileges(path, testUser.getPrincipal(), allPriv, getRestrictions(superuser, path)); + /* grant ALL privilege for testUser at 'childNPath' */ + givePrivileges(childNPath, testUser.getPrincipal(), allPriv, getRestrictions(superuser, childNPath)); + + Session testSession = getTestSession(); + AccessControlManager acMgr = testSession.getAccessControlManager(); + + assertTrue(acMgr.hasPrivileges(path, allPriv)); + assertTrue(acMgr.hasPrivileges(childNPath, allPriv)); + + assertTrue(testSession.hasPermission(childNPath, Session.ACTION_REMOVE)); + + Node child = testSession.getNode(childNPath); + child.remove(); + testSession.save(); + } + + /** + * Test the rep:glob restriction + * + * @throws Exception + */ + public void testGlobRestriction() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node child = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + String childchildPath = child.getPath(); + + Privilege[] write = privilegesFromName(PrivilegeRegistry.REP_WRITE); + String writeActions = Session.ACTION_ADD_NODE +","+Session.ACTION_REMOVE +","+ Session.ACTION_SET_PROPERTY; + + + // permissions defined @ path + // restriction: grants write priv to all nodeName3 children + Map restrictions = new HashMap(getRestrictions(superuser, path)); + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/*"+nodeName3)); + givePrivileges(path, write, restrictions); + + assertFalse(testAcMgr.hasPrivileges(path, write)); + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_SET_PROPERTY)); + + assertFalse(testAcMgr.hasPrivileges(childNPath, write)); + assertFalse(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_SET_PROPERTY)); + + assertTrue(testAcMgr.hasPrivileges(childNPath2, write)); + assertTrue(testSession.hasPermission(childNPath2, Session.ACTION_SET_PROPERTY)); + assertFalse(testSession.hasPermission(childNPath2, writeActions)); // removal req. rmchildnode privilege on parent. + + assertTrue(testAcMgr.hasPrivileges(childchildPath, write)); + } + + /** + * Test the rep:glob restriction + * + * @throws Exception + */ + public void testGlobRestriction2() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node child = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + String childchildPath = child.getPath(); + + Privilege[] write = privilegesFromName(PrivilegeRegistry.REP_WRITE); + Privilege[] addNode = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + Privilege[] rmNode = privilegesFromName(Privilege.JCR_REMOVE_NODE); + + Map restrictions = new HashMap(getRestrictions(superuser, path)); + + // permissions defined @ path + // restriction: grants write-priv to nodeName3 grand-children but not direct nodeName3 children. + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/*/"+nodeName3)); + givePrivileges(path, write, restrictions); + + assertFalse(testAcMgr.hasPrivileges(path, write)); + assertFalse(testAcMgr.hasPrivileges(path, rmNode)); + + assertFalse(testAcMgr.hasPrivileges(childNPath, addNode)); + + assertFalse(testAcMgr.hasPrivileges(childNPath2, write)); + + assertTrue(testAcMgr.hasPrivileges(childchildPath, write)); + } + + /** + * Test the rep:glob restriction + * + * @throws Exception + */ + public void testGlobRestriction3() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node child = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + String childchildPath = child.getPath(); + + Privilege[] write = privilegesFromName(PrivilegeRegistry.REP_WRITE); + Privilege[] addNode = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + String writeActions = Session.ACTION_ADD_NODE +","+Session.ACTION_REMOVE +","+ Session.ACTION_SET_PROPERTY; + + Map restrictions = new HashMap(getRestrictions(superuser, path)); + + // permissions defined @ path + // restriction: allows write to nodeName3 children + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/*/"+nodeName3)); + givePrivileges(path, write, restrictions); + // and grant add-node only at path (no glob restriction) + givePrivileges(path, addNode, getRestrictions(superuser, path)); + + assertFalse(testAcMgr.hasPrivileges(path, write)); + assertTrue(testAcMgr.hasPrivileges(path, addNode)); + + assertFalse(testAcMgr.hasPrivileges(childNPath, write)); + assertTrue(testAcMgr.hasPrivileges(childNPath, addNode)); + + assertFalse(testAcMgr.hasPrivileges(childNPath2, write)); + assertTrue(testAcMgr.hasPrivileges(childchildPath, write)); + } + + /** + * Test the rep:glob restriction + * + * @throws Exception + */ + public void testGlobRestriction4() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node child = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + String childchildPath = child.getPath(); + + Privilege[] write = privilegesFromName(PrivilegeRegistry.REP_WRITE); + Privilege[] addNode = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + + Map restrictions = new HashMap(getRestrictions(superuser, path)); + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/*"+nodeName3)); + givePrivileges(path, write, restrictions); + + withdrawPrivileges(childNPath2, addNode, getRestrictions(superuser, childNPath2)); + + assertFalse(testAcMgr.hasPrivileges(path, write)); + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_REMOVE)); + + assertFalse(testAcMgr.hasPrivileges(childNPath, write)); + assertFalse(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_REMOVE)); + + assertFalse(testAcMgr.hasPrivileges(childNPath2, write)); + + assertTrue(testAcMgr.hasPrivileges(childchildPath, write)); + } + + + /** + * Test the rep:glob restriction + * + * @throws Exception + */ + public void testCancelInheritanceRestriction() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Privilege[] write = privilegesFromName(PrivilegeRegistry.REP_WRITE); + Privilege[] addNode = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + + Map restrictions = new HashMap(getRestrictions(superuser, path)); + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("")); + givePrivileges(path, write, restrictions); + + assertTrue(testAcMgr.hasPrivileges(path, write)); + assertTrue(testSession.hasPermission(path, Session.ACTION_SET_PROPERTY)); + + assertFalse(testAcMgr.hasPrivileges(childNPath, write)); + assertFalse(testSession.hasPermission(childNPath, Session.ACTION_SET_PROPERTY)); + + assertFalse(testAcMgr.hasPrivileges(childNPath2, write)); + assertFalse(testSession.hasPermission(childNPath2, Session.ACTION_SET_PROPERTY)); + } + + /** + * Tests if it is possible to create/read nodes with a non-admin session + * within a transaction. + * + * @throws Exception + * @see JCR-2999 + */ + public void testTransaction() throws Exception { + + // make sure testUser has all privileges + Privilege[] privileges = privilegesFromName(Privilege.JCR_ALL); + givePrivileges(path, privileges, getRestrictions(superuser, path)); + + // create new node and lock it + Session s = getTestSession(); + UserTransaction utx = new UserTransactionImpl(s); + utx.begin(); + + // add node and save it + Node n = s.getNode(childNPath); + if (n.hasNode(nodeName1)) { + Node c = n.getNode(nodeName1); + c.remove(); + s.save(); + } + + // create node and save + Node n2 = n.addNode(nodeName1); + s.save(); // -> node is NEW -> no failure + + // create child node + Node n3 = n2.addNode(nodeName2); + s.save(); // -> used to fail because n2 was saved (EXISTING) but not committed. + + n3.remove(); + n2.remove(); + + // recreate n2 // -> another area where ItemManager#getItem is called in the ItemSaveOperation + n2 = n.addNode(nodeName1); + s.save(); + + // set a property on a child node of an uncommited parent + n2.setProperty(propertyName1, "testSetProperty"); + s.save(); // -> used to fail because PropertyImpl#getParent called from PropertyImpl#checkSetValue + // was checking read permission on the not yet commited parent + + // commit + utx.commit(); + } + + private static Node findPolicyNode(Node start) throws RepositoryException { + Node policyNode = null; + if (start.isNodeType("rep:Policy")) { + policyNode = start; + } + for (NodeIterator it = start.getNodes(); it.hasNext() && policyNode == null;) { + Node n = it.nextNode(); + if (!"jcr:system".equals(n.getName())) { + policyNode = findPolicyNode(n); + } + } + return policyNode; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/CustomPrivilegeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/CustomPrivilegeTest.java new file mode 100644 index 00000000000..6e39183deb7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/CustomPrivilegeTest.java @@ -0,0 +1,404 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionWriter; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.security.Privilege; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * CustomPrivilegeTest... + */ +public class CustomPrivilegeTest extends AbstractJCRTest { + + private NameResolver resolver; + + private FileSystem fs; + private PrivilegeRegistry privilegeRegistry; + + @Override + protected void setUp() throws Exception { + super.setUp(); + resolver = ((SessionImpl) superuser); + + // setup the custom privilege file with cyclic references + fs = ((RepositoryImpl) superuser.getRepository()).getConfig().getFileSystem(); + FileSystemResource resource = new FileSystemResource(fs, "/privileges/custom_privileges.xml"); + if (!resource.exists()) { + resource.makeParentDirs(); + } + + privilegeRegistry = new PrivilegeRegistry(superuser.getWorkspace().getNamespaceRegistry(), fs); + } + + @Override + protected void tearDown() throws Exception { + try { + if (fs.exists("/privileges") && fs.isFolder("/privileges")) { + fs.deleteFolder("/privileges"); + } + } finally { + super.tearDown(); + } + } + + private static void assertPrivilege(PrivilegeRegistry registry, NameResolver resolver, PrivilegeDefinition def) throws RepositoryException { + PrivilegeManagerImpl pmgr = new PrivilegeManagerImpl(registry, resolver); + Privilege p = pmgr.getPrivilege(resolver.getJCRName(def.getName())); + + assertNotNull(p); + + assertEquals(def.isAbstract(), p.isAbstract()); + Set danames = def.getDeclaredAggregateNames(); + assertEquals(danames.size() > 0, p.isAggregate()); + assertEquals(danames.size(), p.getDeclaredAggregatePrivileges().length); + } + + private static Set createNameSet(Name... names) { + Set set = new HashSet(); + set.addAll(Arrays.asList(names)); + return set; + } + + public void testInvalidCustomDefinitions() throws RepositoryException, FileSystemException, IOException { + // setup the custom privilege file with cyclic references + FileSystem fs = ((RepositoryImpl) superuser.getRepository()).getConfig().getFileSystem(); + FileSystemResource resource = new FileSystemResource(fs, "/privileges/custom_privileges.xml"); + if (!resource.exists()) { + resource.makeParentDirs(); + } + StringBuilder sb = new StringBuilder(); + sb.append(""); + + Writer writer = new OutputStreamWriter(resource.getOutputStream(), "utf-8"); + writer.write(sb.toString()); + writer.flush(); + writer.close(); + + try { + new PrivilegeRegistry(superuser.getWorkspace().getNamespaceRegistry(), fs); + fail("Invalid names must be detected upon registry startup."); + } catch (RepositoryException e) { + // success + } finally { + fs.deleteFolder("/privileges"); + } + } + + public void testCustomDefinitionsWithCyclicReferences() throws RepositoryException, FileSystemException, IOException { + // setup the custom privilege file with cyclic references + FileSystem fs = ((RepositoryImpl) superuser.getRepository()).getConfig().getFileSystem(); + FileSystemResource resource = new FileSystemResource(fs, "/privileges/custom_privileges.xml"); + if (!resource.exists()) { + resource.makeParentDirs(); + } + + NameFactory nf = NameFactoryImpl.getInstance(); + Name test = nf.create(Name.NS_DEFAULT_URI, "test"); + Name test2 = nf.create(Name.NS_DEFAULT_URI, "test2"); + Name test3 = nf.create(Name.NS_DEFAULT_URI, "test3"); + Name test4 = nf.create(Name.NS_DEFAULT_URI, "test4"); + Name test5 = nf.create(Name.NS_DEFAULT_URI, "test5"); + + OutputStream out = resource.getOutputStream(); + try { + List defs = new ArrayList(); + defs.add(new PrivilegeDefinitionImpl(test, false, Collections.singleton(test2))); + defs.add(new PrivilegeDefinitionImpl(test4, true, Collections.singleton(test5))); + defs.add(new PrivilegeDefinitionImpl(test5, false, Collections.singleton(test3))); + defs.add(new PrivilegeDefinitionImpl(test3, false, Collections.singleton(test))); + defs.add(new PrivilegeDefinitionImpl(test2, false, Collections.singleton(test4))); + PrivilegeDefinitionWriter pdw = new PrivilegeDefinitionWriter("text/xml"); + pdw.writeDefinitions(out, defs.toArray(new PrivilegeDefinition[defs.size()]), Collections.emptyMap()); + + new PrivilegeRegistry(superuser.getWorkspace().getNamespaceRegistry(), fs); + fail("Cyclic definitions must be detected upon registry startup."); + } catch (RepositoryException e) { + // success + } finally { + out.close(); + fs.deleteFolder("/privileges"); + } + } + + public void testCustomEquivalentDefinitions() throws RepositoryException, FileSystemException, IOException { + // setup the custom privilege file with cyclic references + FileSystem fs = ((RepositoryImpl) superuser.getRepository()).getConfig().getFileSystem(); + FileSystemResource resource = new FileSystemResource(fs, "/privileges/custom_privileges.xml"); + if (!resource.exists()) { + resource.makeParentDirs(); + } + + NameFactory nf = NameFactoryImpl.getInstance(); + Name test = nf.create(Name.NS_DEFAULT_URI, "test"); + Name test2 = nf.create(Name.NS_DEFAULT_URI, "test2"); + Name test3 = nf.create(Name.NS_DEFAULT_URI, "test3"); + Name test4 = nf.create(Name.NS_DEFAULT_URI, "test4"); + Name test5 = nf.create(Name.NS_DEFAULT_URI, "test5"); + Name test6 = nf.create(Name.NS_DEFAULT_URI, "test6"); + + OutputStream out = resource.getOutputStream(); + try { + List defs = new ArrayList(); + defs.add(new PrivilegeDefinitionImpl(test, false, createNameSet(test2, test3))); + defs.add(new PrivilegeDefinitionImpl(test2, true, Collections.singleton(test4))); + defs.add(new PrivilegeDefinitionImpl(test3, true, Collections.singleton(test5))); + defs.add(new PrivilegeDefinitionImpl(test4, true, Collections.emptySet())); + defs.add(new PrivilegeDefinitionImpl(test5, true, Collections.emptySet())); + + // the equivalent definition to 'test' + defs.add(new PrivilegeDefinitionImpl(test6, false, createNameSet(test2, test5))); + + PrivilegeDefinitionWriter pdw = new PrivilegeDefinitionWriter("text/xml"); + pdw.writeDefinitions(out, defs.toArray(new PrivilegeDefinition[defs.size()]), Collections.emptyMap()); + + new PrivilegeRegistry(superuser.getWorkspace().getNamespaceRegistry(), fs); + fail("Equivalent definitions must be detected upon registry startup."); + } catch (RepositoryException e) { + // success + } finally { + out.close(); + fs.deleteFolder("/privileges"); + } + } + + public void testRegisterBuiltInPrivilege() throws RepositoryException, IllegalNameException, FileSystemException { + Map> builtIns = new HashMap>(); + builtIns.put(NameConstants.JCR_READ, Collections.emptySet()); + builtIns.put(NameConstants.JCR_LIFECYCLE_MANAGEMENT, Collections.singleton(NameConstants.JCR_ADD_CHILD_NODES)); + builtIns.put(PrivilegeRegistry.REP_WRITE_NAME, Collections.emptySet()); + builtIns.put(NameConstants.JCR_ALL, Collections.emptySet()); + + for (Name builtInName : builtIns.keySet()) { + try { + privilegeRegistry.registerDefinition(builtInName, false, builtIns.get(builtInName)); + fail("Privilege name already in use -> Exception expected"); + } catch (RepositoryException e) { + // success + } + } + } + + public void testRegisterInvalidNewAggregate() throws RepositoryException, IllegalNameException, FileSystemException { + Map> newAggregates = new HashMap>(); + // same as jcr:read + newAggregates.put(resolver.getQName("jcr:newAggregate"), Collections.singleton(NameConstants.JCR_READ)); + // aggregated combining built-in and an unknown privilege + newAggregates.put(resolver.getQName("jcr:newAggregate"), createNameSet(NameConstants.JCR_READ, resolver.getQName("unknownPrivilege"))); + // aggregate containing unknown privilege + newAggregates.put(resolver.getQName("newAggregate"), createNameSet(resolver.getQName("unknownPrivilege"))); + // custom aggregated contains itself + newAggregates.put(resolver.getQName("newAggregate"), createNameSet(resolver.getQName("newAggregate"))); + // same as rep:write + newAggregates.put(resolver.getQName("repWriteAggregate"), createNameSet(NameConstants.JCR_MODIFY_PROPERTIES, NameConstants.JCR_ADD_CHILD_NODES, NameConstants.JCR_NODE_TYPE_MANAGEMENT, NameConstants.JCR_REMOVE_CHILD_NODES,NameConstants.JCR_REMOVE_NODE)); + // aggregated combining built-in and unknown custom + newAggregates.put(resolver.getQName("newAggregate"), createNameSet(NameConstants.JCR_READ, resolver.getQName("unknownPrivilege"))); + + for (Name name : newAggregates.keySet()) { + try { + privilegeRegistry.registerDefinition(name, true, newAggregates.get(name)); + fail("New aggregate referring to unknown Privilege -> Exception expected"); + } catch (RepositoryException e) { + // success + } + } + } + + public void testRegisterInvalidNewAggregate2() throws RepositoryException, FileSystemException { + Map> newCustomPrivs = new LinkedHashMap>(); + newCustomPrivs.put(resolver.getQName("new"), Collections.emptySet()); + newCustomPrivs.put(resolver.getQName("new2"), Collections.emptySet()); + Set decl = new HashSet(); + decl.add(resolver.getQName("new")); + decl.add(resolver.getQName("new2")); + newCustomPrivs.put(resolver.getQName("new3"), decl); + + for (Name name : newCustomPrivs.keySet()) { + boolean isAbstract = true; + Set aggrNames = newCustomPrivs.get(name); + privilegeRegistry.registerDefinition(name, isAbstract, aggrNames); + } + + Map> newAggregates = new HashMap>(); + // other illegal aggregates already represented by registered definition. + newAggregates.put(resolver.getQName("newA2"), Collections.singleton(resolver.getQName("new"))); + newAggregates.put(resolver.getQName("newA3"), Collections.singleton(resolver.getQName("new2"))); + + for (Name name : newAggregates.keySet()) { + boolean isAbstract = false; + Set aggrNames = newAggregates.get(name); + + try { + privilegeRegistry.registerDefinition(name, isAbstract, aggrNames); + fail("Invalid aggregation in definition '"+ name.toString()+"' : Exception expected"); + } catch (RepositoryException e) { + // success + } + } + } + + public void testRegisterCustomPrivileges() throws RepositoryException, FileSystemException { + Map> newCustomPrivs = new HashMap>(); + newCustomPrivs.put(resolver.getQName("new"), Collections.emptySet()); + newCustomPrivs.put(resolver.getQName("test:new"), Collections.emptySet()); + + for (Name name : newCustomPrivs.keySet()) { + boolean isAbstract = true; + Set aggrNames = newCustomPrivs.get(name); + + privilegeRegistry.registerDefinition(name, isAbstract, aggrNames); + + // validate definition + PrivilegeDefinition definition = privilegeRegistry.get(name); + assertNotNull(definition); + assertEquals(name, definition.getName()); + assertTrue(definition.isAbstract()); + assertTrue(definition.getDeclaredAggregateNames().isEmpty()); + assertEquals(aggrNames.size(), definition.getDeclaredAggregateNames().size()); + for (Name n : aggrNames) { + assertTrue(definition.getDeclaredAggregateNames().contains(n)); + } + + Set allAgg = privilegeRegistry.get(NameConstants.JCR_ALL).getDeclaredAggregateNames(); + assertTrue(allAgg.contains(name)); + + // re-read the filesystem resource and check if definition is correct + PrivilegeRegistry registry = new PrivilegeRegistry(superuser.getWorkspace().getNamespaceRegistry(), fs); + PrivilegeDefinition def = registry.get(name); + assertEquals(isAbstract, def.isAbstract()); + assertEquals(aggrNames.size(), def.getDeclaredAggregateNames().size()); + for (Name n : aggrNames) { + assertTrue(def.getDeclaredAggregateNames().contains(n)); + } + + assertPrivilege(privilegeRegistry, (SessionImpl) superuser, definition); + } + + Map> newAggregates = new HashMap>(); + // a new aggregate of custom privileges + newAggregates.put(resolver.getQName("newA2"), createNameSet(resolver.getQName("test:new"), resolver.getQName("new"))); + // a new aggregate of custom and built-in privilege + newAggregates.put(resolver.getQName("newA1"), createNameSet(resolver.getQName("new"), NameConstants.JCR_READ)); + // aggregating built-in privileges + newAggregates.put(resolver.getQName("aggrBuiltIn"), createNameSet(NameConstants.JCR_MODIFY_PROPERTIES, NameConstants.JCR_READ)); + + for (Name name : newAggregates.keySet()) { + boolean isAbstract = false; + Set aggrNames = newAggregates.get(name); + privilegeRegistry.registerDefinition(name, isAbstract, aggrNames); + PrivilegeDefinition definition = privilegeRegistry.get(name); + + assertNotNull(definition); + assertEquals(name, definition.getName()); + assertFalse(definition.isAbstract()); + assertFalse(definition.getDeclaredAggregateNames().isEmpty()); + assertEquals(aggrNames.size(), definition.getDeclaredAggregateNames().size()); + for (Name n : aggrNames) { + assertTrue(definition.getDeclaredAggregateNames().contains(n)); + } + + Set allAgg = privilegeRegistry.get(NameConstants.JCR_ALL).getDeclaredAggregateNames(); + assertTrue(allAgg.contains(name)); + + // re-read the filesystem resource and check if definition is correct + PrivilegeRegistry registry = new PrivilegeRegistry(superuser.getWorkspace().getNamespaceRegistry(), fs); + PrivilegeDefinition def = registry.get(name); + assertEquals(isAbstract, def.isAbstract()); + assertEquals(isAbstract, def.isAbstract()); + assertEquals(aggrNames.size(), def.getDeclaredAggregateNames().size()); + for (Name n : aggrNames) { + assertTrue(def.getDeclaredAggregateNames().contains(n)); + } + + assertPrivilege(registry, (SessionImpl) superuser, def); + } + } + + public void testCustomPrivilege() throws RepositoryException, FileSystemException { + boolean isAbstract = false; + Name name = ((SessionImpl) superuser).getQName("test"); + privilegeRegistry.registerDefinition(name, isAbstract, Collections.emptySet()); + + PrivilegeManagerImpl pm = new PrivilegeManagerImpl(privilegeRegistry, resolver); + String privName = resolver.getJCRName(name); + + Privilege priv = pm.getPrivilege(privName); + assertEquals(privName, priv.getName()); + assertEquals(isAbstract, priv.isAbstract()); + assertFalse(priv.isAggregate()); + assertFalse(pm.getBits(priv).isEmpty()); + + Privilege jcrWrite = pm.getPrivilege(Privilege.JCR_WRITE); + assertFalse(pm.getBits(jcrWrite).equals(pm.getBits(priv, jcrWrite))); + } + + public void testRegister100CustomPrivileges() throws RepositoryException, FileSystemException { + PrivilegeBits previous = privilegeRegistry.getBits(privilegeRegistry.get(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT_NAME)).unmodifiable(); + for (int i = 0; i < 100; i++) { + boolean isAbstract = true; + Name name = ((SessionImpl) superuser).getQName("test"+i); + privilegeRegistry.registerDefinition(name, isAbstract, Collections.emptySet()); + PrivilegeDefinition definition = privilegeRegistry.get(name); + + assertNotNull(definition); + assertEquals(name, definition.getName()); + + PrivilegeBits modifiable = privilegeRegistry.getBits(definition); + PrivilegeBits bits = modifiable.unmodifiable(); + assertNotNull(bits); + assertFalse(bits.isEmpty()); + assertEquals(modifiable, bits); + + assertFalse(previous.equals(bits)); + assertEquals(previous.nextBits(), bits); + + PrivilegeDefinition all = privilegeRegistry.get(NameConstants.JCR_ALL); + assertTrue(all.getDeclaredAggregateNames().contains(name)); + assertTrue(privilegeRegistry.getBits(all).includes(bits)); + + previous = bits; + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/GlobPatternTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/GlobPatternTest.java new file mode 100644 index 00000000000..d392f74abd2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/GlobPatternTest.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.core.security.authorization; + +import org.apache.jackrabbit.test.JUnitTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * GlobPatternTest... + */ +public class GlobPatternTest extends JUnitTest { + + private static Logger log = LoggerFactory.getLogger(GlobPatternTest.class); + + private static void assertMatch(GlobPattern gp, String testPath, Boolean expectedResult) { + Boolean match = Boolean.valueOf(gp.matches(testPath)); + assertEquals("Pattern : " + gp + "; TestPath : " + testPath, expectedResult, match); + } + + public void testMatchesNoMetaChar() { + GlobPattern gp = GlobPattern.create("/a/b/c"); + + Map tests = new HashMap(); + tests.put("/a/b/c", true); + tests.put("/a/b/c/d", true); + tests.put("/a/b/c/d/e", true); + tests.put("/a/b/c/d/e/f", true); + + tests.put("/", false); + tests.put("/a", false); + tests.put("/b/c", false); + for (String toTest : tests.keySet()) { + assertTrue(tests.get(toTest) == gp.matches(toTest)); + } + } + + public void testMatchesWildcardAll() { + + Map tests = new HashMap(); + + // restriction "*" matches /foo, all siblings of foo and foo's and the siblings' descendants + GlobPattern gp = GlobPattern.create("/a/b/c", "*"); + // matching + tests.put("/a/b/c", true); // foo itself + tests.put("/a/b/c/d", true); // child of foo + tests.put("/a/b/c/d/e", true); // child of foo + tests.put("/a/b/c/d/e/f", true); // child of foo + tests.put("/a/b/cde", true); // sibling + tests.put("/a/b/cde/e/f", true); // child of the sibling + // not-matching + tests.put("/", false); + tests.put("/a", false); + tests.put("/b/c", false); + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // restriction "*cat" matches all siblings and descendants of /foo that have a name ending with cat + gp = GlobPattern.create("/a/b/c", "*e"); + tests = new HashMap(); + // matching + tests.put("/a/b/c/e", true); // descendant with name segment 'e' + tests.put("/a/b/c/d/e", true); // descendant with name segment 'e' + tests.put("/a/b/c/gge", true); // descendant with name segment ending with 'e' + tests.put("/a/b/c/d/gge", true); // descendant with name segment ending with 'e' + tests.put("/a/b/ce", true); // sibling whose name ends with 'e' + tests.put("/a/b/chee", true); // sibling whose name ends with 'e' + tests.put("/a/b/cd/e", true); // descendant of sibling named 'e' + tests.put("/a/b/cd/f/e", true); // descendant of sibling named 'e' + tests.put("/a/b/cd/e", true); // descendant of sibling with name ending with 'e' + tests.put("/a/b/cd/f/e", true); // descendant of sibling with name ending with 'e' + // not-matching + tests.put("/", false); + tests.put("/a", false); + tests.put("/b/c", false); + tests.put("/a/b/c", false); + tests.put("/a/b/c/d", false); + tests.put("/a/b/c/d/e/f", false); + tests.put("/a/b/c/d/f/e/f", false); + tests.put("/a/b/c/d/f/efg", false); + tests.put("/a/b/c/d/f/f", false); + tests.put("/a/b/c/e/f", false); + tests.put("/a/b/ce/", false); + tests.put("/a/b/ceg", false); + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // restriction "*/cat" matches all descendants of /foo and foo's siblings that have a name segment "cat" + gp = GlobPattern.create("/a/b/c", "*/e"); + tests = new HashMap(); + // matching + tests.put("/a/b/c/e", true); // descendant with name segment 'e' + tests.put("/a/b/c/d/e", true); // descendant with name segment 'e' + tests.put("/a/b/cd/e", true); // descendant of sibling named 'e' + tests.put("/a/b/cd/f/e", true); // descendant of sibling named 'e' + // not-matching + tests.put("/", false); + tests.put("/a", false); + tests.put("/b/c", false); + tests.put("/a/b/c", false); + tests.put("/a/b/c/d", false); + tests.put("/a/b/c/d/e/f", false); + tests.put("/a/b/c/d/f/e/f", false); + tests.put("/a/b/c/d/f/efg", false); + tests.put("/a/b/c/d/f/f", false); + tests.put("/a/b/c/e/f", false); + tests.put("/a/b/ce/", false); + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // matches target path '/a/b/c/e', all siblings whose name starts with e + // and child nodes of either. + gp = GlobPattern.create("/a/b/c/e", "*"); + tests = new HashMap(); + // matching + tests.put("/a/b/c/e/f/g/h", true); + tests.put("/a/b/c/e/d/e/f", true); + tests.put("/a/b/c/e/d/e/f", true); + tests.put("/a/b/c/e", true); + tests.put("/a/b/c/e/", true); + tests.put("/a/b/c/ef", true); + tests.put("/a/b/c/ef/g", true); + // not-matching + tests.put("/a/b/ce/f/g/h", false); + tests.put("/a/b/ce/d/e/f", false); + tests.put("/a/b/c", false); + tests.put("/a/b/c/d", false); + tests.put("/a/b/c/d/e/f", false); + tests.put("/a/b/c/d/f/f", false); + tests.put("/a/b/c/d/f/e/f", false); + tests.put("/a/b/cee/d/e/f", false); + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // all descendants of '/a/b/c/e' + gp = GlobPattern.create("/a/b/c/e", "/*"); + tests = new HashMap(); + // matching + tests.put("/a/b/c/e/f/g/h", true); + tests.put("/a/b/c/e/d/e/f", true); + // not-matching + tests.put("/a/b/c/e", false); // not matching node path + tests.put("/a/b/c/e/", false); // not matching node path + / + tests.put("/a/b/c/ef", false); // not matching siblings of node path + tests.put("/a/b/ce/f/g/h", false); + tests.put("/a/b/ce/d/e/f", false); + tests.put("/a/b/c", false); + tests.put("/a/b/c/d", false); + tests.put("/a/b/c/d/e", false); + tests.put("/a/b/c/d/e/f", false); + tests.put("/a/b/c/d/f/f", false); + tests.put("/a/b/c/d/f/e/f", false); + tests.put("/a/b/cee/d/e/f", false); + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // all descendants of '/a/b/ce' + gp = GlobPattern.create("/a/b/c", "e/*"); + tests = new HashMap(); + // not-matching + tests.put("/a/b/ce/f/g/h", true); + tests.put("/a/b/ce/d/e/f", true); + // not-matching + tests.put("/a/b/c", false); + tests.put("/a/b/ce", false); + tests.put("/a/b/c/d", false); + tests.put("/a/b/c/d/e", false); + tests.put("/a/b/c/d/e/f", false); + tests.put("/a/b/c/d/f/f", false); + tests.put("/a/b/c/d/f/e/f", false); + tests.put("/a/b/cee/d/e/f", false); + tests.put("/a/b/ce/", false); // missing * after ce/ + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // all descendants of '/' + gp = GlobPattern.create("/", "*"); + tests = new HashMap(); + // matching + tests.put("/a", true); + tests.put("/b/", true); + tests.put("/c/d", true); + tests.put("/a/b/ce/", true); + tests.put("/a/b/ce/f/g/h", true); + tests.put("/a/b/ce/d/e/f", true); + // not-matching + tests.put("/", false); + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // restriction "*e/*" matches all descendants of /foo that have an intermediate segment ending with 'e' + gp = GlobPattern.create("/a/b/c", "*e/*"); + tests = new HashMap(); + // matching + tests.put("/a/b/ceeeeeee/f/g/h", true); + tests.put("/a/b/cde/d/e/f", true); + tests.put("/a/b/c/d/e/f", true); + tests.put("/a/b/ced/d/e/f", true); + // not-matching + tests.put("/a/b/ce/", false); // ignore trailing / in test path + tests.put("/a/b/c/d/e/", false); // ignore trailing / in test path + tests.put("/a/b/c/d", false); // missing *e/* + tests.put("/a/b/c/d/e", false); // missing /* + tests.put("/a/b/c/d/f/f", false); // missing *e + tests.put("/a/b/c/ed/f/f", false); // missing e/ + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // restriction /*cat matches all children of /a/b/c whose path ends with "cat" + gp = GlobPattern.create("/a/b/c", "/*cat"); + tests = new HashMap(); + // matching + tests.put("/a/b/c/cat", true); + tests.put("/a/b/c/acat", true); + tests.put("/a/b/c/f/cat", true); + tests.put("/a/b/c/f/acat", true); + // not-matching + tests.put("/a/b/c/d", false); + tests.put("/a/b/c/d/cat/e", false); // cat only intermediate segment + tests.put("/a/b/c/d/acat/e", false); // cat only intermediate segment + tests.put("/a/b/c/d/cata/e", false); // cat only intermediate segment + tests.put("/a/b/c/d/cate", false); + tests.put("/a/b/cat", false); // siblings do no match + tests.put("/a/b/cat/ed/f/f", false); // ... nor do siblings' children + tests.put("/a/b/ced/cat", false); // ... nor do siblings' children + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // restriction /*/cat matches all non-direct descendants of /foo named "cat" + gp = GlobPattern.create("/a/b/c", "/*/cat"); + tests = new HashMap(); + // matching + tests.put("/a/b/c/a/cat", true); + tests.put("/a/b/c/d/e/f/cat", true); + // not-matching + tests.put("/a/b/c/cat", false); + tests.put("/a/b/c/cate", false); + tests.put("/a/b/c/acat", false); + tests.put("/a/b/c/cat/d", false); + tests.put("/a/b/c/d/acat", false); + tests.put("/a/b/c/d/cate", false); + tests.put("/a/b/c/d/cat/e", false); // cat only intermediate segment + tests.put("/a/b/c/d/acat/e", false); // cat only intermediate segment + tests.put("/a/b/c/d/cata/e", false); // cat only intermediate segment + tests.put("/a/b/cat", false); // siblings do no match + tests.put("/a/b/cat/ed/f/f", false); // ... nor do siblings' children + tests.put("/a/b/ced/cat", false); // ... nor do siblings' children + tests.put("/a/b/ced/f/cat", false); // ... nor do siblings' children + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + + // restriction /cat* matches all descendant paths of /foo that have the + // direct foo-descendant segment starting with "cat" + gp = GlobPattern.create("/a/b/c", "/cat*"); + tests = new HashMap(); + // matching + tests.put("/a/b/c/cat", true); + tests.put("/a/b/c/cats", true); + tests.put("/a/b/c/cat/s", true); + tests.put("/a/b/c/cats/d/e/f", true); + // not-matching + tests.put("/a/b/c/d/cat", false); + tests.put("/a/b/c/d/cats", false); + tests.put("/a/b/c/d/e/cat", false); + tests.put("/a/b/c/d/e/cats", false); + tests.put("/a/b/c/acat", false); + tests.put("/a/b/c/d/acat", false); + tests.put("/a/b/c/d/cat/e", false); + tests.put("/a/b/c/d/acat/e", false); + tests.put("/a/b/c/d/cata/e", false); + tests.put("/a/b/cat", false); // siblings do no match + tests.put("/a/b/cat/ed/f/f", false); // ... nor do siblings' children + tests.put("/a/b/ced/cat", false); // ... nor do siblings' children + tests.put("/a/b/ced/f/cat", false); // ... nor do siblings' children + + for (String testPath : tests.keySet()) { + assertMatch(gp, testPath, tests.get(testPath)); + } + } + + public void testEmptyRestriction() { + GlobPattern gp = GlobPattern.create("/", ""); + Map tests = new HashMap(); + tests.put("/", true); + + tests.put("/a/b/c/d", false); + tests.put("/a/b/c/d/e", false); + tests.put("/a/b/c/d/e/f", false); + tests.put("/a/b/c", false); + tests.put("/a", false); + tests.put("/a/b/cde", false); + + for (String toTest : tests.keySet()) { + assertTrue(gp + " : " + toTest, tests.get(toTest) == gp.matches(toTest)); + } + + gp = GlobPattern.create("/a/b/c", ""); + + tests = new HashMap(); + tests.put("/a/b/c", true); + + tests.put("/a/b/c/d", false); + tests.put("/a/b/c/d/e", false); + tests.put("/a/b/c/d/e/f", false); + tests.put("/", false); + tests.put("/a", false); + tests.put("/a/b/cde", false); + + for (String toTest : tests.keySet()) { + assertTrue(gp + " : " + toTest, tests.get(toTest) == gp.matches(toTest)); + } + } + + public void testMatchesItem() { + // TODO + } + + public void testEquals() { + GlobPattern gp1 = GlobPattern.create("/a/b/c"); + GlobPattern gp2 = GlobPattern.create("/a/b/c"); + assertEquals(gp1, gp2); + + gp1 = GlobPattern.create("/a/b/c"); + gp2 = GlobPattern.create("/a/b/c/d"); + assertFalse(gp1.equals(gp2)); + + gp1 = GlobPattern.create("/a/b/c", null); + gp2 = GlobPattern.create("/a/b/c", ""); + assertFalse(gp1.equals(gp2)); + + gp1 = GlobPattern.create("/a/b/c", ""); + gp2 = GlobPattern.create("/a/b/c", ""); + assertEquals(gp1, gp2); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/JackrabbitAccessControlListTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/JackrabbitAccessControlListTest.java new file mode 100644 index 00000000000..fb8f9371606 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/JackrabbitAccessControlListTest.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * JackrabbitAccessControlListTest... + */ +public class JackrabbitAccessControlListTest extends AbstractAccessControlTest { + + private JackrabbitAccessControlList templ; + private PrivilegeManagerImpl privilegeMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(n.getPath()); + while (it.hasNext() && templ == null) { + AccessControlPolicy p = it.nextAccessControlPolicy(); + if (p instanceof JackrabbitAccessControlList) { + templ = (JackrabbitAccessControlList) p; + } + } + if (templ == null) { + superuser.logout(); + throw new NotExecutableException("No JackrabbitAccessControlList to test."); + } + + privilegeMgr = (PrivilegeManagerImpl) ((JackrabbitWorkspace) superuser.getWorkspace()).getPrivilegeManager(); + } + + @Override + protected void tearDown() throws Exception { + // make sure transient ac-changes are reverted. + superuser.refresh(false); + super.tearDown(); + } + + private Principal getValidPrincipal() throws NotExecutableException, RepositoryException { + if (!(superuser instanceof JackrabbitSession)) { + throw new NotExecutableException(); + } + + PrincipalManager pMgr = ((JackrabbitSession) superuser).getPrincipalManager(); + PrincipalIterator it = pMgr.getPrincipals(PrincipalManager.SEARCH_TYPE_NOT_GROUP); + if (it.hasNext()) { + return it.nextPrincipal(); + } else { + throw new NotExecutableException(); + } + } + + public void testGetRestrictionNames() throws RepositoryException { + assertNotNull(templ.getRestrictionNames()); + } + + public void testGetRestrictionType() throws RepositoryException { + String[] names = templ.getRestrictionNames(); + for (String name : names) { + int type = templ.getRestrictionType(name); + assertTrue(type > PropertyType.UNDEFINED); + } + } + + public void testIsEmpty() throws RepositoryException { + if (templ.isEmpty()) { + assertEquals(0, templ.getAccessControlEntries().length); + } else { + assertTrue(templ.getAccessControlEntries().length > 0); + } + } + + public void testSize() { + if (templ.isEmpty()) { + assertEquals(0, templ.size()); + } else { + assertTrue(templ.size() > 0); + } + } + + public void testAddEntry() throws NotExecutableException, RepositoryException { + Principal princ = getValidPrincipal(); + Privilege[] priv = privilegesFromName(Privilege.JCR_ALL); + + List entriesBefore = Arrays.asList(templ.getAccessControlEntries()); + if (templ.addEntry(princ, priv, true, Collections.emptyMap())) { + AccessControlEntry[] entries = templ.getAccessControlEntries(); + if (entries.length == 0) { + fail("GrantPrivileges was successful -> at least 1 entry for principal."); + } + PrivilegeBits allows = PrivilegeBits.getInstance(); + for (AccessControlEntry en : entries) { + PrivilegeBits bits = privilegeMgr.getBits(en.getPrivileges()); + if (en instanceof JackrabbitAccessControlEntry && ((JackrabbitAccessControlEntry) en).isAllow()) { + allows.add(bits); + } + } + assertEquals(privilegeMgr.getBits(priv), allows); + } else { + AccessControlEntry[] entries = templ.getAccessControlEntries(); + assertEquals("Grant ALL not successful -> entries must not have changed.", entriesBefore, Arrays.asList(entries)); + } + } + + public void testAddEntry2() throws NotExecutableException, RepositoryException { + Principal princ = getValidPrincipal(); + Privilege[] privs = privilegesFromName(PrivilegeRegistry.REP_WRITE); + + templ.addEntry(princ, privs, true, Collections.emptyMap()); + AccessControlEntry[] entries = templ.getAccessControlEntries(); + assertTrue("GrantPrivileges was successful -> at least 1 entry for principal.", entries.length > 0); + + PrivilegeBits allows = PrivilegeBits.getInstance(); + for (AccessControlEntry en : entries) { + PrivilegeBits bits = privilegeMgr.getBits(en.getPrivileges()); + if (en instanceof JackrabbitAccessControlEntry && ((JackrabbitAccessControlEntry) en).isAllow()) { + allows.add(bits); + } + } + assertTrue("After successfully granting WRITE, the entries must reflect this", allows.includes(privilegeMgr.getBits(privs))); + } + + public void testAllowWriteDenyRemove() throws NotExecutableException, RepositoryException { + Principal princ = getValidPrincipal(); + Privilege[] grPriv = privilegesFromName(PrivilegeRegistry.REP_WRITE); + Privilege[] dePriv = privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES); + + templ.addEntry(princ, grPriv, true, Collections.emptyMap()); + templ.addEntry(princ, dePriv, false, Collections.emptyMap()); + + Set allows = new HashSet(); + Set denies = new HashSet(); + AccessControlEntry[] entries = templ.getAccessControlEntries(); + for (AccessControlEntry en : entries) { + if (princ.equals(en.getPrincipal()) && en instanceof JackrabbitAccessControlEntry) { + JackrabbitAccessControlEntry ace = (JackrabbitAccessControlEntry) en; + Privilege[] privs = ace.getPrivileges(); + if (ace.isAllow()) { + allows.addAll(Arrays.asList(privs)); + } else { + denies.addAll(Arrays.asList(privs)); + } + } + } + + String[] expected = new String[] {Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_REMOVE_NODE, Privilege.JCR_MODIFY_PROPERTIES, Privilege.JCR_NODE_TYPE_MANAGEMENT}; + assertEquals(expected.length, allows.size()); + for (String name : expected) { + assertTrue(allows.contains(acMgr.privilegeFromName(name))); + } + + assertEquals(1, denies.size()); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_REMOVE_CHILD_NODES), denies.iterator().next()); + } + + public void testRemoveEntry() throws NotExecutableException, RepositoryException { + Principal princ = getValidPrincipal(); + Privilege[] grPriv = privilegesFromName(PrivilegeRegistry.REP_WRITE); + + templ.addEntry(princ, grPriv, true, Collections.emptyMap()); + AccessControlEntry[] entries = templ.getAccessControlEntries(); + int length = entries.length; + assertTrue("Grant was both successful -> at least 1 entry.", length > 0); + for (AccessControlEntry entry : entries) { + templ.removeAccessControlEntry(entry); + length = length - 1; + assertEquals(length, templ.size()); + assertEquals(length, templ.getAccessControlEntries().length); + } + + assertTrue(templ.isEmpty()); + assertEquals(0, templ.size()); + assertEquals(0, templ.getAccessControlEntries().length); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PermissionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PermissionTest.java new file mode 100644 index 00000000000..6c62b9de4f3 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PermissionTest.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.core.security.authorization; + +import junit.framework.TestCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * PermissionTest... + */ +public class PermissionTest extends TestCase { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(PermissionTest.class); + + public void testPermissions() { + assertEquals(0, Permission.NONE); + assertEquals(1, Permission.READ); + assertEquals(2, Permission.SET_PROPERTY); + assertEquals(4, Permission.ADD_NODE); + assertEquals(8, Permission.REMOVE_NODE); + assertEquals(16, Permission.REMOVE_PROPERTY); + assertEquals(32, Permission.READ_AC); + assertEquals(64, Permission.MODIFY_AC); + assertEquals(128, Permission.NODE_TYPE_MNGMT); + assertEquals(256, Permission.VERSION_MNGMT); + assertEquals(512, Permission.LOCK_MNGMT); + assertEquals(1024, Permission.LIFECYCLE_MNGMT); + assertEquals(2048, Permission.RETENTION_MNGMT); + assertEquals(4096, Permission.MODIFY_CHILD_NODE_COLLECTION); + assertEquals(8192, Permission.NODE_TYPE_DEF_MNGMT); + assertEquals(16384, Permission.NAMESPACE_MNGMT); + assertEquals(32768, Permission.WORKSPACE_MNGMT); + assertEquals(65536, Permission.PRIVILEGE_MNGMT); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeBitsTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeBitsTest.java new file mode 100644 index 00000000000..a7573583a85 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeBitsTest.java @@ -0,0 +1,516 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.jackrabbit.test.JUnitTest; + +/** + * PrivilegeBitsTest... + */ +public class PrivilegeBitsTest extends JUnitTest { + + private static final PrivilegeBits READ_PRIVILEGE_BITS = PrivilegeBits.getInstance(1); + + private static final long[] LONGS = new long[] {1, 2, 13, 199, 512, Long.MAX_VALUE/2, Long.MAX_VALUE-1, Long.MAX_VALUE}; + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + //-----------------------------------------------------------< internal >--- + + public void testLongValue() { + // empty + assertEquals(PrivilegeRegistry.NO_PRIVILEGE, PrivilegeBits.EMPTY.longValue()); + + // long based privilege bits + for (long l : LONGS) { + assertEquals(l, PrivilegeBits.getInstance(l).longValue()); + } + + // long based privilege bits + PrivilegeBits pb = READ_PRIVILEGE_BITS; + long l = pb.longValue(); + while (l < Long.MAX_VALUE/2) { + l = l << 1; + pb = pb.nextBits(); + assertEquals(l, pb.longValue()); + } + + // other privilege bits: long value not available. + for (int i = 0; i < 10; i++) { + pb = pb.nextBits(); + assertEquals(0, pb.longValue()); + } + + // modifiable privilege bits + pb = READ_PRIVILEGE_BITS; + for (int i = 0; i < 100; i++) { + PrivilegeBits modifiable = PrivilegeBits.getInstance(pb); + assertEquals(pb.longValue(), modifiable.longValue()); + pb = pb.nextBits(); + } + } + + public void testNextBits() { + // empty + assertSame(PrivilegeBits.EMPTY, PrivilegeBits.EMPTY.nextBits()); + + // long based privilege bits + PrivilegeBits pb = READ_PRIVILEGE_BITS; + long l = pb.longValue(); + while (l < Long.MAX_VALUE/2) { + l = l << 1; + pb = pb.nextBits(); + assertEquals(l, pb.longValue()); + } + + // other privilege bits: long value not available. + for (int i = 0; i < 10; i++) { + PrivilegeBits nxt = pb.nextBits(); + assertEquals(nxt, pb.nextBits()); + assertFalse(pb.equals(nxt)); + pb = nxt; + } + + // modifiable privilege bits + pb = READ_PRIVILEGE_BITS; + for (int i = 0; i < 100; i++) { + PrivilegeBits modifiable = PrivilegeBits.getInstance(pb); + try { + modifiable.nextBits(); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + pb = pb.nextBits(); + } + } + + + public void testUnmodifiable() { + assertSame(PrivilegeBits.EMPTY, PrivilegeBits.EMPTY.unmodifiable()); + + // other privilege bits + PrivilegeBits pb = READ_PRIVILEGE_BITS; + PrivilegeBits mod = PrivilegeBits.getInstance(pb); + + for (int i = 0; i < 100; i++) { + PrivilegeBits nxt = pb.nextBits(); + assertSame(nxt, nxt.unmodifiable()); + assertEquals(nxt, nxt.unmodifiable()); + + mod.add(nxt); + assertNotSame(mod, mod.unmodifiable()); + assertEquals(mod, mod.unmodifiable()); + + pb = nxt; + } + } + + //---------------------------------------------------------------< test >--- + + public void testIncludesRead() { + // empty + assertFalse(PrivilegeBits.EMPTY.includesRead()); + + // other privilege bits + PrivilegeBits pb = READ_PRIVILEGE_BITS; + assertTrue(pb.includesRead()); + assertTrue(PrivilegeBits.getInstance(pb).includesRead()); + + PrivilegeBits mod = PrivilegeBits.getInstance(); + for (int i = 0; i < 100; i++) { + mod.add(pb); + assertTrue(mod.includesRead()); + + pb = pb.nextBits(); + assertFalse(pb.toString(), pb.includesRead()); + assertFalse(PrivilegeBits.getInstance(pb).includesRead()); + + PrivilegeBits modifiable = PrivilegeBits.getInstance(pb); + modifiable.add(READ_PRIVILEGE_BITS); + assertTrue(modifiable.includesRead()); + } + } + + public void testIncludes() { + // empty + assertTrue(PrivilegeBits.EMPTY.includes(PrivilegeBits.EMPTY)); + + // other privilege bits + PrivilegeBits pb = READ_PRIVILEGE_BITS; + PrivilegeBits mod = PrivilegeBits.getInstance(); + + for (int i = 0; i < 100; i++) { + + assertFalse(PrivilegeBits.EMPTY.includes(pb)); + assertTrue(pb.includes(PrivilegeBits.EMPTY)); + + mod.add(pb); + assertTrue(mod.includes(pb)); + + PrivilegeBits nxt = pb.nextBits(); + assertTrue(nxt.includes(nxt)); + assertTrue(nxt.includes(PrivilegeBits.getInstance(nxt))); + + assertFalse(pb + " should not include " + nxt, pb.includes(nxt)); + assertFalse(nxt + " should not include " + pb, nxt.includes(pb)); + assertFalse(mod.includes(nxt)); + assertFalse(nxt.includes(mod)); + + pb = nxt; + } + } + + public void testIsEmpty() { + // empty + assertTrue(PrivilegeBits.EMPTY.isEmpty()); + + // any other bits should not be empty + PrivilegeBits pb = READ_PRIVILEGE_BITS; + PrivilegeBits mod = PrivilegeBits.getInstance(pb); + for (int i = 0; i < 100; i++) { + assertFalse(pb.isEmpty()); + assertFalse(PrivilegeBits.getInstance(pb).isEmpty()); + + pb = pb.nextBits(); + mod.add(pb); + assertFalse(mod.isEmpty()); + + PrivilegeBits tmp = PrivilegeBits.getInstance(pb); + tmp.diff(pb); + assertTrue(tmp.toString(), tmp.isEmpty()); + } + } + + //----------------------------------------------------------------< mod >--- + + public void testAdd() { + // empty + try { + PrivilegeBits.EMPTY.add(PrivilegeBits.EMPTY); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + // other privilege bits + PrivilegeBits pb = READ_PRIVILEGE_BITS; + PrivilegeBits mod = PrivilegeBits.getInstance(pb); + + for (int i = 0; i < 100; i++) { + try { + pb.add(PrivilegeBits.EMPTY); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + try { + pb.add(mod); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + PrivilegeBits nxt = pb.nextBits(); + try { + pb.add(nxt); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + long before = mod.longValue(); + long nxtLong = nxt.longValue(); + + mod.add(nxt); + if (nxt.longValue() != 0) { + assertEquals(before | nxtLong, mod.longValue()); + } + assertTrue(mod.includes(nxt)); + + PrivilegeBits tmp = PrivilegeBits.getInstance(pb); + assertTrue(tmp.includes(pb)); + assertFalse(tmp.includes(nxt)); + if (READ_PRIVILEGE_BITS.equals(pb)) { + assertTrue(tmp.includesRead()); + } else { + assertFalse(tmp.includesRead()); + } + tmp.add(nxt); + assertTrue(tmp.includes(pb) && tmp.includes(nxt)); + if (READ_PRIVILEGE_BITS.equals(pb)) { + assertTrue(tmp.includesRead()); + assertTrue(tmp.includes(READ_PRIVILEGE_BITS)); + } else { + assertFalse(tmp.toString(), tmp.includesRead()); + assertFalse(tmp.includes(READ_PRIVILEGE_BITS)); + } + tmp.add(READ_PRIVILEGE_BITS); + assertTrue(tmp.includesRead()); + assertTrue(tmp.includes(READ_PRIVILEGE_BITS)); + + pb = nxt; + } + } + + public void testDiff() { + // empty + try { + PrivilegeBits.EMPTY.diff(PrivilegeBits.EMPTY); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + // other privilege bits + PrivilegeBits pb = READ_PRIVILEGE_BITS; + PrivilegeBits mod = PrivilegeBits.getInstance(pb); + + for (int i = 0; i < 100; i++) { + PrivilegeBits nxt = pb.nextBits(); + try { + pb.diff(nxt); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + try { + pb.diff(mod); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + PrivilegeBits before = PrivilegeBits.getInstance(mod); + mod.diff(nxt); + assertEquals(before, mod); + mod.add(nxt); + assertFalse(before.equals(mod)); + mod.diff(nxt); + assertEquals(before, mod); + mod.add(nxt); + + // diff with same pb must leave original bits empty + PrivilegeBits tmp = PrivilegeBits.getInstance(pb); + tmp.add(nxt); + tmp.add(READ_PRIVILEGE_BITS); + tmp.diff(tmp); + assertEquals(nxt.toString(), PrivilegeBits.EMPTY, tmp); + + tmp = PrivilegeBits.getInstance(pb); + tmp.add(nxt); + tmp.add(READ_PRIVILEGE_BITS); + tmp.diff(PrivilegeBits.getInstance(tmp)); + assertEquals(nxt.toString(), PrivilegeBits.EMPTY, tmp); + + // diff without intersection -> leave privilege unmodified. + tmp = PrivilegeBits.getInstance(pb); + tmp.diff(nxt); + assertEquals(PrivilegeBits.getInstance(pb), tmp); + + // diff with intersection -> privilege must be modified accordingly. + tmp = PrivilegeBits.getInstance(nxt); + tmp.add(READ_PRIVILEGE_BITS); + assertTrue(tmp.includes(READ_PRIVILEGE_BITS)); + assertTrue(tmp.includes(nxt)); + tmp.diff(nxt); + assertEquals(READ_PRIVILEGE_BITS, tmp); + assertTrue(tmp.includes(READ_PRIVILEGE_BITS)); + assertFalse(tmp.includes(nxt)); + + tmp = PrivilegeBits.getInstance(pb); + tmp.add(READ_PRIVILEGE_BITS); + PrivilegeBits tmp2 = PrivilegeBits.getInstance(pb); + tmp2.add(nxt); + PrivilegeBits tmp3 = PrivilegeBits.getInstance(tmp2); + assertEquals(tmp2, tmp3); + tmp.diff(tmp2); + if (READ_PRIVILEGE_BITS.equals(pb)) { + assertEquals(PrivilegeBits.EMPTY, tmp); + } else { + assertEquals(READ_PRIVILEGE_BITS, tmp); + } + // but pb passed to the diff call must not be modified. + assertEquals(tmp3, tmp2); + + pb = nxt; + } + } + + public void testAddDifference() { + // empty + try { + PrivilegeBits.EMPTY.addDifference(PrivilegeBits.EMPTY, PrivilegeBits.EMPTY); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + // other privilege bits + PrivilegeBits pb = READ_PRIVILEGE_BITS; + PrivilegeBits mod = PrivilegeBits.getInstance(pb); + + for (int i = 0; i < 100; i++) { + PrivilegeBits nxt = pb.nextBits(); + try { + pb.addDifference(nxt, mod); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + try { + pb.addDifference(nxt, READ_PRIVILEGE_BITS); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + PrivilegeBits tmp = PrivilegeBits.getInstance(mod); + tmp.addDifference(nxt, READ_PRIVILEGE_BITS); + mod.add(nxt); + assertEquals(mod, tmp); // since there is diff(nxt, read) which results in nxt + + if (!pb.equals(READ_PRIVILEGE_BITS)) { + tmp = PrivilegeBits.getInstance(nxt); + PrivilegeBits mod2 = PrivilegeBits.getInstance(mod); + tmp.addDifference(mod2, READ_PRIVILEGE_BITS); + assertFalse(nxt.equals(tmp)); // tmp should be modified by addDifference call. + assertEquals(mod2, mod); // mod2 should not be modified here + assertTrue(tmp.includes(pb)); + assertFalse(tmp.includes(READ_PRIVILEGE_BITS)); + assertFalse(tmp.includes(mod)); + } + + tmp = PrivilegeBits.getInstance(nxt); + PrivilegeBits mod2 = PrivilegeBits.getInstance(mod); + tmp.addDifference(READ_PRIVILEGE_BITS, mod2); + assertEquals(nxt, tmp); // tmp not modified by addDifference call. + assertEquals(mod2, mod); // mod2 should not be modified here + assertFalse(tmp.includes(pb)); + assertFalse(tmp.includes(READ_PRIVILEGE_BITS)); + assertFalse(tmp.includes(mod)); + + tmp = PrivilegeBits.getInstance(nxt); + tmp.addDifference(READ_PRIVILEGE_BITS, READ_PRIVILEGE_BITS); + assertEquals(nxt, tmp); // tmp not modified by addDifference call. + assertFalse(tmp.includes(READ_PRIVILEGE_BITS)); + + tmp = PrivilegeBits.getInstance(mod); + tmp.addDifference(READ_PRIVILEGE_BITS, READ_PRIVILEGE_BITS); + assertEquals(mod, tmp); // tmp not modified by addDifference call. + assertTrue(tmp.includes(READ_PRIVILEGE_BITS)); + + pb = nxt; + } + } + + //------------------------------------------------------------< general >--- + public void testGetInstance() { + PrivilegeBits pb = PrivilegeBits.getInstance(); + assertEquals(PrivilegeBits.EMPTY, pb); + assertNotSame(PrivilegeBits.EMPTY, pb); + assertNotSame(pb, pb.unmodifiable()); + pb.add(READ_PRIVILEGE_BITS); + pb.addDifference(READ_PRIVILEGE_BITS, READ_PRIVILEGE_BITS); + pb.diff(READ_PRIVILEGE_BITS); + + pb = PrivilegeBits.getInstance(PrivilegeBits.EMPTY); + assertEquals(PrivilegeBits.EMPTY, pb); + assertNotSame(PrivilegeBits.EMPTY, pb); + assertNotSame(pb, pb.unmodifiable()); + pb.add(READ_PRIVILEGE_BITS); + pb.addDifference(READ_PRIVILEGE_BITS, READ_PRIVILEGE_BITS); + pb.diff(READ_PRIVILEGE_BITS); + + pb = PrivilegeBits.getInstance(READ_PRIVILEGE_BITS); + assertEquals(READ_PRIVILEGE_BITS, pb); + assertNotSame(READ_PRIVILEGE_BITS, pb); + assertNotSame(pb, pb.unmodifiable()); + pb.add(READ_PRIVILEGE_BITS); + pb.addDifference(READ_PRIVILEGE_BITS, PrivilegeBits.EMPTY); + pb.diff(READ_PRIVILEGE_BITS); + + pb = PrivilegeBits.getInstance(PrivilegeRegistry.NO_PRIVILEGE); + assertEquals(pb, PrivilegeBits.EMPTY); + assertSame(pb, PrivilegeBits.EMPTY); + assertSame(pb, pb.unmodifiable()); + try { + pb.add(READ_PRIVILEGE_BITS); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + try { + pb.addDifference(READ_PRIVILEGE_BITS, READ_PRIVILEGE_BITS); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + try { + pb.diff(READ_PRIVILEGE_BITS); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + + try { + PrivilegeBits.getInstance(-1); + fail(); + } catch (IllegalArgumentException e) { + // success. + } + + PrivilegeBits bts = PrivilegeBits.getInstance(PrivilegeRegistry.NO_PRIVILEGE); + assertSame(PrivilegeBits.EMPTY, bts); + + for (long l : LONGS) { + pb = PrivilegeBits.getInstance(l); + assertEquals(pb, PrivilegeBits.getInstance(l)); + assertSame(pb, pb.unmodifiable()); + + assertEquals(pb, PrivilegeBits.getInstance(pb)); + assertEquals(PrivilegeBits.getInstance(pb), pb); + assertNotSame(pb, PrivilegeBits.getInstance(pb)); + + try { + pb.add(READ_PRIVILEGE_BITS); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + try { + pb.addDifference(READ_PRIVILEGE_BITS, READ_PRIVILEGE_BITS); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + try { + pb.diff(READ_PRIVILEGE_BITS); + fail("UnsupportedOperation expected"); + } catch (UnsupportedOperationException e) { + // success + } + } + + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeManagerImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeManagerImplTest.java new file mode 100644 index 00000000000..b79e8932d02 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeManagerImplTest.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.core.security.authorization; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManagerTest; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * PrivilegeManagerTest... + */ +public class PrivilegeManagerImplTest extends PrivilegeManagerTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!(privilegeMgr instanceof PrivilegeManagerImpl)) { + throw new NotExecutableException("PrivilegeManagerImpl expected"); + } + } + + private PrivilegeManagerImpl getPrivilegeManagerImpl() { + return (PrivilegeManagerImpl) privilegeMgr; + } + + private Privilege[] privilegesFromNames(String[] privNames) + throws RepositoryException { + Privilege[] privs = new Privilege[privNames.length]; + for (int i = 0; i < privNames.length; i++) { + privs[i] = privilegeMgr.getPrivilege(privNames[i]); + } + return privs; + } + + private void assertPrivilege(Privilege priv, String name, boolean isAggregate, boolean isAbstract) throws NamespaceException, IllegalNameException { + assertNotNull(priv); + assertPrivilegeName(name, priv.getName()); + assertEquals(isAggregate, priv.isAggregate()); + assertEquals(isAbstract, priv.isAbstract()); + } + + private void assertPrivilegeName(String name, String name2) throws NamespaceException, IllegalNameException { + if (!((SessionImpl) superuser).getQName(name).equals(((SessionImpl) superuser).getQName(name2))) { + fail(); + } + } + + public void testGetRegisteredPrivileges() throws RepositoryException { + Privilege[] registered = privilegeMgr.getRegisteredPrivileges(); + Set set = new HashSet(); + Privilege all = privilegeMgr.getPrivilege(Privilege.JCR_ALL); + set.add(all); + set.addAll(Arrays.asList(all.getAggregatePrivileges())); + + for (Privilege p : registered) { + assertTrue(set.remove(p)); + } + assertTrue(set.isEmpty()); + } + + public void testGetPrivilege() throws RepositoryException { + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_READ), Privilege.JCR_READ, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_ADD_CHILD_NODES), Privilege.JCR_ADD_CHILD_NODES, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_REMOVE_CHILD_NODES), Privilege.JCR_REMOVE_CHILD_NODES, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_MODIFY_PROPERTIES), Privilege.JCR_MODIFY_PROPERTIES, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_REMOVE_NODE), Privilege.JCR_REMOVE_NODE, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_READ_ACCESS_CONTROL), Privilege.JCR_READ_ACCESS_CONTROL, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_MODIFY_ACCESS_CONTROL), Privilege.JCR_MODIFY_ACCESS_CONTROL, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_LIFECYCLE_MANAGEMENT), Privilege.JCR_LIFECYCLE_MANAGEMENT, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_LOCK_MANAGEMENT), Privilege.JCR_LOCK_MANAGEMENT, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_NODE_TYPE_MANAGEMENT), Privilege.JCR_NODE_TYPE_MANAGEMENT, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_RETENTION_MANAGEMENT), Privilege.JCR_RETENTION_MANAGEMENT, false, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT), Privilege.JCR_VERSION_MANAGEMENT, false, false); + + // repo-level operation privileges + assertPrivilege(privilegeMgr.getPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT.toString()), NameConstants.JCR_NAMESPACE_MANAGEMENT.toString() , false, false); + assertPrivilege(privilegeMgr.getPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString()), NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString(), false, false); + assertPrivilege(privilegeMgr.getPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT.toString()), NameConstants.JCR_WORKSPACE_MANAGEMENT.toString(), false, false); + + // aggregates + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_ALL), Privilege.JCR_ALL, true, false); + assertPrivilege(privilegeMgr.getPrivilege(Privilege.JCR_WRITE), Privilege.JCR_WRITE, true, false); + assertPrivilege(privilegeMgr.getPrivilege(PrivilegeRegistry.REP_WRITE), PrivilegeRegistry.REP_WRITE, true, false); + } + + public void testGetBits() throws RepositoryException { + Privilege p1 = privilegeMgr.getPrivilege(Privilege.JCR_ADD_CHILD_NODES); + Privilege p2 = privilegeMgr.getPrivilege(Privilege.JCR_REMOVE_CHILD_NODES); + Privilege[] privs = new Privilege[] {p1, p2}; + + PrivilegeBits bits = getPrivilegeManagerImpl().getBits(privs); + assertFalse(bits.isEmpty()); + PrivilegeBits other = PrivilegeBits.getInstance(getPrivilegeManagerImpl().getBits(p1)); + other.add(getPrivilegeManagerImpl().getBits(p2)); + assertEquals(bits, other); + } + + public void testGetBitsFromCustomPrivilege() throws AccessControlException { + Privilege p = buildCustomPrivilege(Privilege.JCR_READ, null); + try { + getPrivilegeManagerImpl().getBits(p); + fail("Retrieving bits from unknown privilege should fail."); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsFromCustomAggregatePrivilege() throws RepositoryException { + Privilege p = buildCustomPrivilege("anyName", privilegeMgr.getPrivilege(Privilege.JCR_WRITE)); + try { + getPrivilegeManagerImpl().getBits(p); + fail("Retrieving bits from unknown privilege should fail."); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsFromNull() { + try { + getPrivilegeManagerImpl().getBits((Privilege) null); + fail("Should throw AccessControlException"); + } catch (AccessControlException e) { + // ok + } + + try { + getPrivilegeManagerImpl().getBits((Privilege[]) null); + fail("Should throw AccessControlException"); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsFromEmptyArray() throws AccessControlException { + try { + getPrivilegeManagerImpl().getBits(new Privilege[0]); + fail("Should throw AccessControlException"); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsFromArrayContainingNull() throws RepositoryException { + try { + getPrivilegeManagerImpl().getBits(privilegeMgr.getPrivilege(Privilege.JCR_READ), null); + fail("Should throw AccessControlException"); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsWithInvalidPrivilege() { + Privilege p = buildCustomPrivilege("anyName", null); + try { + getPrivilegeManagerImpl().getBits(p); + fail(); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetPrivilegesFromBits() throws RepositoryException { + Set pvs = getPrivilegeManagerImpl().getPrivileges(getPrivilegeManagerImpl().getBits(privilegesFromNames(new String[] {Privilege.JCR_READ_ACCESS_CONTROL}))); + + assertTrue(pvs != null); + assertTrue(pvs.size() == 1); + assertSamePrivilegeName(pvs.iterator().next().getName(), Privilege.JCR_READ_ACCESS_CONTROL); + } + + public void testGetPrivilegesFromBits2() throws RepositoryException { + String[] names = new String[] { + Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_REMOVE_CHILD_NODES, + Privilege.JCR_REMOVE_NODE, + Privilege.JCR_MODIFY_PROPERTIES + }; + PrivilegeBits writeBits = getPrivilegeManagerImpl().getBits(privilegesFromNames(names)); + Set pvs = getPrivilegeManagerImpl().getPrivileges(writeBits); + + assertTrue(pvs != null); + assertTrue(pvs.size() == 1); + Privilege p = pvs.iterator().next(); + assertSamePrivilegeName(p.getName(), Privilege.JCR_WRITE); + assertTrue(p.isAggregate()); + assertTrue(p.getDeclaredAggregatePrivileges().length == names.length); + } + + public void testGetPrivilegesFromBits3() throws RepositoryException { + String[] names = new String[] { + PrivilegeRegistry.REP_WRITE + }; + PrivilegeBits writeBits = getPrivilegeManagerImpl().getBits(privilegesFromNames(names)); + Set pvs = getPrivilegeManagerImpl().getPrivileges(writeBits); + + assertTrue(pvs != null); + assertTrue(pvs.size() == 1); + Privilege p = pvs.iterator().next(); + assertSamePrivilegeName(p.getName(), PrivilegeRegistry.REP_WRITE); + assertTrue(p.isAggregate()); + + names = new String[] { + PrivilegeRegistry.REP_WRITE, + Privilege.JCR_WRITE + }; + writeBits = getPrivilegeManagerImpl().getBits(privilegesFromNames(names)); + pvs = getPrivilegeManagerImpl().getPrivileges(writeBits); + + assertTrue(pvs != null); + assertTrue(pvs.size() == 1); + p = pvs.iterator().next(); + assertSamePrivilegeName(p.getName(), PrivilegeRegistry.REP_WRITE); + assertTrue(p.isAggregate()); + assertTrue(p.getDeclaredAggregatePrivileges().length == names.length); + } + + public void testGetPrivilegesFromBits4() throws RepositoryException { + String[] names = new String[] { + PrivilegeRegistry.REP_WRITE, + Privilege.JCR_LIFECYCLE_MANAGEMENT + }; + PrivilegeBits writeBits = getPrivilegeManagerImpl().getBits(privilegesFromNames(names)); + Set pvs = getPrivilegeManagerImpl().getPrivileges(writeBits); + + assertTrue(pvs != null); + assertTrue(pvs.size() == 2); + } + + public void testRegisterPrivilegeAsNonAdmin() throws RepositoryException { + Session s = getHelper().getReadOnlySession(); + try { + ((JackrabbitWorkspace) s.getWorkspace()).getPrivilegeManager().registerPrivilege("test", true, new String[0]); + fail("Only admin is allowed to register privileges."); + } catch (AccessDeniedException e) { + // success + } finally { + s.logout(); + } + } + + private Privilege buildCustomPrivilege(final String name, final Privilege declaredAggr) { + return new Privilege() { + + public String getName() { + return name; + } + public boolean isAbstract() { + return false; + } + public boolean isAggregate() { + return declaredAggr != null; + } + public Privilege[] getDeclaredAggregatePrivileges() { + return (declaredAggr == null) ? new Privilege[0] : new Privilege[] {declaredAggr}; + } + public Privilege[] getAggregatePrivileges() { + return (declaredAggr == null) ? new Privilege[0] : declaredAggr.getAggregatePrivileges(); + } + }; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeRegistryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeRegistryTest.java new file mode 100644 index 00000000000..52e76d74753 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/PrivilegeRegistryTest.java @@ -0,0 +1,456 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * PrivilegeRegistryTest... + */ +public class PrivilegeRegistryTest extends AbstractJCRTest { + + private NameResolver resolver; + private PrivilegeRegistry privilegeRegistry; + + @Override + protected void setUp() throws Exception { + super.setUp(); + resolver = ((SessionImpl) superuser); + privilegeRegistry = new PrivilegeRegistry(resolver); + } + + public void testGetAll() throws RepositoryException { + + PrivilegeDefinition[] defs = privilegeRegistry.getAll(); + + List l = new ArrayList(Arrays.asList(defs)); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_READ))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_ADD_CHILD_NODES))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_REMOVE_CHILD_NODES))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_MODIFY_PROPERTIES))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_REMOVE_NODE))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_READ_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_MODIFY_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_WRITE))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_ALL))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_LIFECYCLE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_LOCK_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_NODE_TYPE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_RETENTION_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_VERSION_MANAGEMENT))); + // including repo-level operation privileges + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_NAMESPACE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.get(NameConstants.JCR_WORKSPACE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.get(resolver.getQName(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT)))); + // and aggregates + assertTrue(l.remove(privilegeRegistry.get(resolver.getQName(PrivilegeRegistry.REP_WRITE)))); + + assertTrue(l.isEmpty()); + } + + + public void testGet() throws RepositoryException { + + for (PrivilegeDefinition def : privilegeRegistry.getAll()) { + + PrivilegeDefinition d = privilegeRegistry.get(def.getName()); + assertEquals(def, d); + + assertNotNull(d.getName()); + assertEquals(def.getName(), d.getName()); + + assertFalse(d.isAbstract()); + assertEquals(def.isAbstract(), d.isAbstract()); + + assertNotNull(d.getDeclaredAggregateNames()); + assertTrue(def.getDeclaredAggregateNames().containsAll(d.getDeclaredAggregateNames())); + assertTrue(d.getDeclaredAggregateNames().containsAll(def.getDeclaredAggregateNames())); + + assertFalse(privilegeRegistry.getBits(d).isEmpty()); + } + } + + public void testAggregates() throws RepositoryException { + + for (PrivilegeDefinition def : privilegeRegistry.getAll()) { + if (def.getDeclaredAggregateNames().isEmpty()) { + continue; // ignore non aggregate + } + + for (Name n : def.getDeclaredAggregateNames()) { + PrivilegeDefinition d = privilegeRegistry.get(n); + assertNotNull(d); + Name[] names = privilegeRegistry.getNames(privilegeRegistry.getBits(d)); + assertNotNull(names); + assertEquals(1, names.length); + assertEquals(d.getName(), names[0]); + } + } + } + + public void testPrivilegeDefinition() throws RepositoryException { + + for (PrivilegeDefinition def : privilegeRegistry.getAll()) { + assertNotNull(def.getName()); + assertFalse(def.isAbstract()); + assertNotNull(def.getDeclaredAggregateNames()); + assertFalse(privilegeRegistry.getBits(def).isEmpty()); + } + } + + public void testJcrAll() throws RepositoryException { + PrivilegeDefinition p = privilegeRegistry.get(NameConstants.JCR_ALL); + assertEquals(p.getName(), NameConstants.JCR_ALL); + assertFalse(p.getDeclaredAggregateNames().isEmpty()); + assertFalse(p.isAbstract()); + + Set l = new HashSet(p.getDeclaredAggregateNames()); + assertTrue(l.remove(NameConstants.JCR_READ)); + assertTrue(l.remove(NameConstants.JCR_WRITE)); + assertTrue(l.remove(resolver.getQName(PrivilegeRegistry.REP_WRITE))); + assertTrue(l.remove(NameConstants.JCR_READ_ACCESS_CONTROL)); + assertTrue(l.remove(NameConstants.JCR_MODIFY_ACCESS_CONTROL)); + assertTrue(l.remove(NameConstants.JCR_LIFECYCLE_MANAGEMENT)); + assertTrue(l.remove(NameConstants.JCR_LOCK_MANAGEMENT)); + assertTrue(l.remove(NameConstants.JCR_NODE_TYPE_MANAGEMENT)); + assertTrue(l.remove(NameConstants.JCR_RETENTION_MANAGEMENT)); + assertTrue(l.remove(NameConstants.JCR_VERSION_MANAGEMENT)); + // including repo-level operation privileges + assertTrue(l.remove(NameConstants.JCR_NAMESPACE_MANAGEMENT)); + assertTrue(l.remove(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT)); + assertTrue(l.remove(NameConstants.JCR_WORKSPACE_MANAGEMENT)); + assertTrue(l.remove(resolver.getQName(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT))); + assertTrue(l.isEmpty()); + } + + public void testJcrWrite() throws RepositoryException { + Name rw = resolver.getQName(PrivilegeRegistry.REP_WRITE); + PrivilegeDefinition p = privilegeRegistry.get(rw); + + assertEquals(p.getName(), rw); + assertFalse(p.getDeclaredAggregateNames().isEmpty()); + assertFalse(p.isAbstract()); + + Set l = new HashSet(p.getDeclaredAggregateNames()); + assertTrue(l.remove(NameConstants.JCR_WRITE)); + assertTrue(l.remove(NameConstants.JCR_NODE_TYPE_MANAGEMENT)); + assertTrue(l.isEmpty()); + } + + public void testRepWrite() throws RepositoryException { + PrivilegeDefinition p = privilegeRegistry.get(NameConstants.JCR_WRITE); + assertEquals(p.getName(), NameConstants.JCR_WRITE); + assertFalse(p.getDeclaredAggregateNames().isEmpty()); + assertFalse(p.isAbstract()); + + Set l = new HashSet(p.getDeclaredAggregateNames()); + assertTrue(l.remove(NameConstants.JCR_MODIFY_PROPERTIES)); + assertTrue(l.remove(NameConstants.JCR_ADD_CHILD_NODES)); + assertTrue(l.remove(NameConstants.JCR_REMOVE_CHILD_NODES)); + assertTrue(l.remove(NameConstants.JCR_REMOVE_NODE)); + assertTrue(l.isEmpty()); + } + + private void assertSamePrivilegeName(String expected, String present) throws RepositoryException { + assertEquals("Privilege names are not the same", resolver.getQName(expected), resolver.getQName(present)); + } + + private Privilege[] privilegesFromNames(String[] privNames) + throws RepositoryException { + Privilege[] privs = new Privilege[privNames.length]; + for (int i = 0; i < privNames.length; i++) { + privs[i] = privilegeRegistry.getPrivilege(privNames[i]); + } + return privs; + } + + public void testRegisteredPrivileges() throws RepositoryException { + Privilege[] ps = privilegeRegistry.getRegisteredPrivileges(); + + List l = new ArrayList(Arrays.asList(ps)); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_READ))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_ADD_CHILD_NODES))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_REMOVE_CHILD_NODES))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_MODIFY_PROPERTIES))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_REMOVE_NODE))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_READ_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_MODIFY_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_WRITE))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_ALL))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_LIFECYCLE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_LOCK_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_NODE_TYPE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_RETENTION_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(PrivilegeRegistry.REP_WRITE))); + // including repo-level operation privileges + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT))); + assertTrue(l.isEmpty()); + } + + public void testAllPrivilege() throws RepositoryException { + Privilege p = privilegeRegistry.getPrivilege(Privilege.JCR_ALL); + assertSamePrivilegeName(p.getName(), Privilege.JCR_ALL); + assertTrue(p.isAggregate()); + assertFalse(p.isAbstract()); + + List l = new ArrayList(Arrays.asList(p.getAggregatePrivileges())); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_READ))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_ADD_CHILD_NODES))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_REMOVE_CHILD_NODES))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_MODIFY_PROPERTIES))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_REMOVE_NODE))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_READ_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_MODIFY_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_LIFECYCLE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_LOCK_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_NODE_TYPE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_RETENTION_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_WRITE))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(PrivilegeRegistry.REP_WRITE))); + // including repo-level operation privileges + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT))); + assertTrue(l.isEmpty()); + + l = new ArrayList(Arrays.asList(p.getDeclaredAggregatePrivileges())); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_READ))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_WRITE))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(PrivilegeRegistry.REP_WRITE))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(PrivilegeRegistry.REP_PRIVILEGE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_READ_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_MODIFY_ACCESS_CONTROL))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_LIFECYCLE_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_LOCK_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_RETENTION_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_VERSION_MANAGEMENT))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(Privilege.JCR_NODE_TYPE_MANAGEMENT))); + // including repo-level operation privileges + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_NAMESPACE_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_NODE_TYPE_DEFINITION_MANAGEMENT.toString()))); + assertTrue(l.remove(privilegeRegistry.getPrivilege(NameConstants.JCR_WORKSPACE_MANAGEMENT.toString()))); + assertTrue(l.isEmpty()); + } + + public void testGetBits() throws RepositoryException { + Privilege p1 = privilegeRegistry.getPrivilege(Privilege.JCR_ADD_CHILD_NODES); + Privilege p2 = privilegeRegistry.getPrivilege(Privilege.JCR_REMOVE_CHILD_NODES); + Privilege[] privs = new Privilege[] {p1, p2}; + + int bits = PrivilegeRegistry.getBits(privs); + assertTrue(bits > PrivilegeRegistry.NO_PRIVILEGE); + assertTrue(bits == (PrivilegeRegistry.getBits(new Privilege[] {p1}) | + PrivilegeRegistry.getBits(new Privilege[] {p2}))); + } + + public void testGetBitsFromInvalidPrivilege() throws AccessControlException { + Privilege p = buildUnregisteredPrivilege(Privilege.JCR_READ, null); + try { + PrivilegeRegistry.getBits(new Privilege[] {p}); + fail("Retrieving bits from unknown privilege should fail."); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsFromInvalidAggregatePrivilege() throws RepositoryException { + Privilege p = buildUnregisteredPrivilege("anyName", privilegeRegistry.getPrivilege(Privilege.JCR_WRITE)); + try { + PrivilegeRegistry.getBits(new Privilege[] {p}); + fail("Retrieving bits from unknown privilege should fail."); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsFromNull() { + try { + PrivilegeRegistry.getBits((Privilege[]) null); + fail("Should throw AccessControlException"); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsFromEmptyArray() { + try { + PrivilegeRegistry.getBits(new Privilege[0]); + fail("Should throw AccessControlException"); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetBitsWithInvalidPrivilege() { + Privilege p = buildUnregisteredPrivilege("anyName", null); + try { + PrivilegeRegistry.getBits(new Privilege[] {p}); + fail(); + } catch (AccessControlException e) { + // ok + } + } + + public void testGetPrivilegesFromBits() throws RepositoryException { + int bits = PrivilegeRegistry.getBits(privilegesFromNames(new String[] {Privilege.JCR_READ_ACCESS_CONTROL})); + Privilege[] pvs = privilegeRegistry.getPrivileges(bits); + + assertTrue(pvs != null); + assertTrue(pvs.length == 1); + assertSamePrivilegeName(pvs[0].getName(), Privilege.JCR_READ_ACCESS_CONTROL); + } + + public void testGetPrivilegesFromBits2() throws RepositoryException { + String[] names = new String[] { + Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_REMOVE_CHILD_NODES, + Privilege.JCR_REMOVE_NODE, + Privilege.JCR_MODIFY_PROPERTIES + }; + int writeBits = PrivilegeRegistry.getBits(privilegesFromNames(names)); + Privilege[] pvs = privilegeRegistry.getPrivileges(writeBits); + + assertTrue(pvs != null); + assertTrue(pvs.length == 1); + assertSamePrivilegeName(pvs[0].getName(), Privilege.JCR_WRITE); + assertTrue(pvs[0].isAggregate()); + assertTrue(pvs[0].getDeclaredAggregatePrivileges().length == names.length); + } + + public void testGetPrivilegesFromBits3() throws RepositoryException { + String[] names = new String[] { + PrivilegeRegistry.REP_WRITE + }; + int writeBits = PrivilegeRegistry.getBits(privilegesFromNames(names)); + Privilege[] pvs = privilegeRegistry.getPrivileges(writeBits); + + assertTrue(pvs != null); + assertTrue(pvs.length == 1); + assertSamePrivilegeName(pvs[0].getName(), PrivilegeRegistry.REP_WRITE); + assertTrue(pvs[0].isAggregate()); + + names = new String[] { + PrivilegeRegistry.REP_WRITE, + Privilege.JCR_WRITE + }; + writeBits = PrivilegeRegistry.getBits(privilegesFromNames(names)); + pvs = privilegeRegistry.getPrivileges(writeBits); + + assertTrue(pvs != null); + assertTrue(pvs.length == 1); + assertSamePrivilegeName(pvs[0].getName(), PrivilegeRegistry.REP_WRITE); + assertTrue(pvs[0].isAggregate()); + assertTrue(pvs[0].getDeclaredAggregatePrivileges().length == names.length); + } + + public void testGetPrivilegesFromBits4() throws RepositoryException { + String[] names = new String[] { + PrivilegeRegistry.REP_WRITE, + Privilege.JCR_LIFECYCLE_MANAGEMENT + }; + int writeBits = PrivilegeRegistry.getBits(privilegesFromNames(names)); + Privilege[] pvs = privilegeRegistry.getPrivileges(writeBits); + + assertTrue(pvs != null); + assertTrue(pvs.length == 2); + } + + public void testGetPrivilegeFromName() throws AccessControlException, RepositoryException { + Privilege p = privilegeRegistry.getPrivilege(Privilege.JCR_READ); + + assertTrue(p != null); + assertSamePrivilegeName(Privilege.JCR_READ, p.getName()); + assertFalse(p.isAggregate()); + + p = privilegeRegistry.getPrivilege(Privilege.JCR_WRITE); + + assertTrue(p != null); + assertSamePrivilegeName(p.getName(), Privilege.JCR_WRITE); + assertTrue(p.isAggregate()); + } + + public void testGetPrivilegesFromInvalidName() throws RepositoryException { + try { + privilegeRegistry.getPrivilege("unknown"); + fail("invalid privilege name"); + } catch (AccessControlException e) { + // OK + } + } + + public void testGetPrivilegesFromEmptyNames() { + try { + privilegeRegistry.getPrivilege(""); + fail("invalid privilege name array"); + } catch (AccessControlException e) { + // OK + } catch (RepositoryException e) { + // OK + } + } + + public void testGetPrivilegesFromNullNames() { + try { + privilegeRegistry.getPrivilege(null); + fail("invalid privilege name (null)"); + } catch (Exception e) { + // OK + } + } + + private Privilege buildUnregisteredPrivilege(final String name, final Privilege declaredAggr) { + return new Privilege() { + + public String getName() { + return name; + } + public boolean isAbstract() { + return false; + } + public boolean isAggregate() { + return declaredAggr != null; + } + public Privilege[] getDeclaredAggregatePrivileges() { + return (declaredAggr == null) ? new Privilege[0] : new Privilege[] {declaredAggr}; + } + public Privilege[] getAggregatePrivileges() { + return (declaredAggr == null) ? new Privilege[0] : declaredAggr.getAggregatePrivileges(); + } + }; + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/TestAll.java new file mode 100644 index 00000000000..7d965b10864 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/TestAll.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.core.security.authorization; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("core.security.authorization tests"); + + suite.addTestSuite(PrivilegeRegistryTest.class); + suite.addTestSuite(PrivilegeManagerImplTest.class); + suite.addTestSuite(CustomPrivilegeTest.class); + suite.addTestSuite(PrivilegeBitsTest.class); + suite.addTestSuite(JackrabbitAccessControlListTest.class); + suite.addTestSuite(GlobPatternTest.class); + suite.addTestSuite(PermissionTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLEditorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLEditorTest.java new file mode 100644 index 00000000000..cc2084894c6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLEditorTest.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.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.security.authorization.AbstractEvaluationTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.version.VersionException; +import java.security.Principal; +import java.util.Map; + +/** + * ACLEditorTest... + */ +public class ACLEditorTest extends AbstractEvaluationTest { + + private String testPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Node node = testRootNode.addNode(nodeName1, "nt:unstructured"); + Node pseudoPolicy = node.addNode("rep:policy", "rep:ACL"); + superuser.save(); + + testPath = node.getPath(); + } + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + + @Override + protected Map getRestrictions(Session session, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(session, path); + } + + public void testNodeNotRepAccessControllable() throws RepositoryException, LockException, ConstraintViolationException, NoSuchNodeTypeException, ItemExistsException, VersionException { + AccessControlPolicy[] plcs = acMgr.getPolicies(testPath); + assertNotNull(plcs); + assertEquals(0, plcs.length); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(testPath); + assertNotNull(it); + assertEquals(0, it.getSize()); + } + + public void testNodeNotRepAccessControllableAddMixin() throws RepositoryException, LockException, ConstraintViolationException, NoSuchNodeTypeException, ItemExistsException, VersionException { + superuser.getNode(testPath).addMixin("rep:AccessControllable"); + superuser.save(); + + AccessControlPolicy[] plcs = acMgr.getPolicies(testPath); + assertNotNull(plcs); + assertEquals(1, plcs.length); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(testPath); + assertNotNull(it); + assertEquals(0, it.getSize()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplateEntryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplateEntryTest.java new file mode 100644 index 00000000000..09dd992b001 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplateEntryTest.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.core.security.authorization.acl; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.api.security.authorization.PrivilegeManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractEntryTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * EntryTest... + */ +public class ACLTemplateEntryTest extends AbstractEntryTest { + + private ACLTemplate acl; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + SessionImpl s = (SessionImpl) superuser; + PrivilegeManager privMgr = ((JackrabbitWorkspace) superuser.getWorkspace()).getPrivilegeManager(); + + acl = new ACLTemplate(testPath, s.getPrincipalManager(), privMgr, s.getValueFactory(), s, false); + } + + @Override + protected JackrabbitAccessControlEntry createEntry(Principal principal, Privilege[] privileges, boolean isAllow) + throws RepositoryException { + return acl.createEntry(principal, privileges, isAllow, Collections.emptyMap()); + } + + @Override + protected JackrabbitAccessControlEntry createEntry(Principal principal, Privilege[] privileges, boolean isAllow, Map restrictions) throws RepositoryException { + return acl.createEntry(principal, privileges, isAllow, restrictions); + } + + @Override + protected JackrabbitAccessControlEntry createEntryFromBase(JackrabbitAccessControlEntry base, Privilege[] privileges, boolean isAllow) throws RepositoryException, NotExecutableException { + if (base instanceof ACLTemplate.Entry) { + return acl.createEntry((ACLTemplate.Entry) base, privileges, isAllow); + } else { + throw new NotExecutableException(); + } + } + + @Override + protected Map getTestRestrictions() throws RepositoryException { + String restrName = ((SessionImpl) superuser).getJCRName(ACLTemplate.P_GLOB); + return Collections.singletonMap(restrName, superuser.getValueFactory().createValue("/.*")); + } + + public void testRestrictions() throws RepositoryException { + // test if restrictions with expanded name are properly resolved + Map restrictions = new HashMap(); + restrictions.put(ACLTemplate.P_GLOB.toString(), superuser.getValueFactory().createValue("*/test")); + + Privilege[] privs = new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_ALL)}; + ACLTemplate.Entry ace = acl.createEntry(testPrincipal, privs, true, restrictions); + + Value v = ace.getRestriction(ACLTemplate.P_GLOB.toString()); + Value v2 = ace.getRestriction(((SessionImpl) superuser).getJCRName(ACLTemplate.P_GLOB)); + assertEquals(v, v2); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplateTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplateTest.java new file mode 100644 index 00000000000..2dd3ebc2788 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ACLTemplateTest.java @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractACLTemplateTest; +import org.apache.jackrabbit.core.security.authorization.PrivilegeBits; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +/** + * ACLTemplateTest... + */ +public class ACLTemplateTest extends AbstractACLTemplateTest { + + private Map emptyRestrictions = Collections.emptyMap(); + + @Override + protected String getTestPath() { + return "/ab/c/d"; + } + + @Override + protected JackrabbitAccessControlList createEmptyTemplate(String path) throws RepositoryException { + SessionImpl sImpl = (SessionImpl) superuser; + return new ACLTemplate(path, principalMgr, privilegeMgr, sImpl.getValueFactory(), sImpl, false); + } + + @Override + protected Principal getSecondPrincipal() throws Exception { + return principalMgr.getEveryone(); + } + + public void testMultipleEntryEffect() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + pt.addEntry(testPrincipal, privileges, true, emptyRestrictions); + + // new entry extends privileges. + privileges = privilegesFromNames(new String[] { + Privilege.JCR_READ, + Privilege.JCR_ADD_CHILD_NODES}); + assertTrue(pt.addEntry(testPrincipal, + privileges, + true, emptyRestrictions)); + + // net-effect: only a single allow-entry with both privileges + assertTrue(pt.size() == 1); + assertSamePrivileges(privileges, pt.getAccessControlEntries()[0].getPrivileges()); + + // adding just ADD_CHILD_NODES -> must not remove READ privilege + Privilege[] achPrivs = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + assertFalse(pt.addEntry(testPrincipal, achPrivs, true, emptyRestrictions)); + // net-effect: only a single allow-entry with add_child_nodes + read privilege + assertTrue(pt.size() == 1); + assertSamePrivileges(privileges, pt.getAccessControlEntries()[0].getPrivileges()); + + // revoke the 'READ' privilege + privileges = privilegesFromName(Privilege.JCR_READ); + assertTrue(pt.addEntry(testPrincipal, privileges, false, emptyRestrictions)); + // net-effect: 2 entries one allowing ADD_CHILD_NODES, the other denying READ + assertTrue(pt.size() == 2); + assertSamePrivileges(privilegesFromName(Privilege.JCR_ADD_CHILD_NODES), + pt.getAccessControlEntries()[0].getPrivileges()); + assertSamePrivileges(privilegesFromName(Privilege.JCR_READ), + pt.getAccessControlEntries()[1].getPrivileges()); + + // remove the deny-READ entry + pt.removeAccessControlEntry(pt.getAccessControlEntries()[1]); + assertTrue(pt.size() == 1); + assertSamePrivileges(privilegesFromName(Privilege.JCR_ADD_CHILD_NODES), + pt.getAccessControlEntries()[0].getPrivileges()); + + // remove the allow-ADD_CHILD_NODES entry + pt.removeAccessControlEntry(pt.getAccessControlEntries()[0]); + assertTrue(pt.isEmpty()); + } + + public void testMultipleEntryEffect2() throws RepositoryException, NotExecutableException { + Privilege repwrite = getAccessControlManager(superuser).privilegeFromName(PrivilegeRegistry.REP_WRITE); + + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + pt.addAccessControlEntry(testPrincipal, new Privilege[] {repwrite}); + + // add deny entry for mod_props + Privilege modProperties = getAccessControlManager(superuser).privilegeFromName(Privilege.JCR_MODIFY_PROPERTIES); + assertTrue(pt.addEntry(testPrincipal, new Privilege[] {modProperties}, false, null)); + + // net-effect: 2 entries with the allow entry being adjusted + assertTrue(pt.size() == 2); + AccessControlEntry[] entries = pt.getAccessControlEntries(); + for (AccessControlEntry entry1 : entries) { + ACLTemplate.Entry entry = (ACLTemplate.Entry) entry1; + PrivilegeBits privs = entry.getPrivilegeBits(); + if (entry.isAllow()) { + Privilege[] result = privilegesFromNames(new String[] { + Privilege.JCR_ADD_CHILD_NODES, + Privilege.JCR_NODE_TYPE_MANAGEMENT, + Privilege.JCR_REMOVE_CHILD_NODES, + Privilege.JCR_REMOVE_NODE}); + PrivilegeBits bits = privilegeMgr.getBits(result); + assertEquals(privs, bits); + } else { + assertEquals(privs, privilegeMgr.getBits(modProperties)); + } + } + } + + public void testMultiplePrincipals() throws RepositoryException, NotExecutableException { + PrincipalManager pMgr = ((JackrabbitSession) superuser).getPrincipalManager(); + Principal everyone = pMgr.getEveryone(); + Principal grPrincipal = null; + PrincipalIterator it = pMgr.findPrincipals("", PrincipalManager.SEARCH_TYPE_GROUP); + while (it.hasNext()) { + GroupPrincipal gr = (GroupPrincipal) it.nextPrincipal(); + if (!everyone.equals(gr)) { + grPrincipal = gr; + } + } + if (grPrincipal == null || grPrincipal.equals(everyone)) { + throw new NotExecutableException(); + } + Privilege[] privs = privilegesFromName(Privilege.JCR_READ); + + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + pt.addAccessControlEntry(testPrincipal, privs); + assertFalse(pt.addAccessControlEntry(testPrincipal, privs)); + + // add same privileges for another principal -> must modify as well. + assertTrue(pt.addAccessControlEntry(everyone, privs)); + // .. 2 entries must be present. + assertTrue(pt.getAccessControlEntries().length == 2); + } + + public void testSetEntryForGroupPrincipal() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + Privilege[] privs = privilegesFromName(Privilege.JCR_READ); + GroupPrincipal grPrincipal = (GroupPrincipal) principalMgr.getEveryone(); + + // adding allow-entry must succeed + assertTrue(pt.addAccessControlEntry(grPrincipal, privs)); + + // adding deny-entry must succeed + pt.addEntry(grPrincipal, privs, false, null); + } + + public void testRevokeEffect() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + + pt.addEntry(testPrincipal, privileges, true, emptyRestrictions); + + // same entry but with revers 'isAllow' flag + assertTrue(pt.addEntry(testPrincipal, privileges, false, emptyRestrictions)); + + // net-effect: only a single deny-read entry + assertEquals(1, pt.size()); + assertSamePrivileges(privileges, pt.getAccessControlEntries()[0].getPrivileges()); + } + + public void testUpdateEntry() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + + Privilege[] readPriv = privilegesFromName(Privilege.JCR_READ); + Privilege[] writePriv = privilegesFromName(Privilege.JCR_WRITE); + + Principal principal2 = principalMgr.getEveryone(); + + pt.addEntry(testPrincipal, readPriv, true, emptyRestrictions); + pt.addEntry(principal2, readPriv, true, emptyRestrictions); + pt.addEntry(testPrincipal, writePriv, false, emptyRestrictions); + + // adding an entry that should update the existing allow-entry for everyone. + pt.addEntry(principal2, writePriv, true, emptyRestrictions); + + AccessControlEntry[] entries = pt.getAccessControlEntries(); + assertEquals(3, entries.length); + JackrabbitAccessControlEntry princ2AllowEntry = (JackrabbitAccessControlEntry) entries[1]; + assertEquals(principal2, princ2AllowEntry.getPrincipal()); + assertTrue(princ2AllowEntry.isAllow()); + assertSamePrivileges(new Privilege[] {readPriv[0], writePriv[0]}, princ2AllowEntry.getPrivileges()); + } + + public void testUpdateComplementaryEntry() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + + Privilege[] readPriv = privilegesFromName(Privilege.JCR_READ); + Privilege[] writePriv = privilegesFromName(Privilege.JCR_WRITE); + Principal principal2 = principalMgr.getEveryone(); + + pt.addEntry(testPrincipal, readPriv, true, emptyRestrictions); + pt.addEntry(principal2, readPriv, true, emptyRestrictions); + pt.addEntry(testPrincipal, writePriv, false, emptyRestrictions); + pt.addEntry(principal2, writePriv, true, emptyRestrictions); + // entry complementary to the first entry + // -> must remove the allow-READ entry and update the deny-WRITE entry. + pt.addEntry(testPrincipal, readPriv, false, emptyRestrictions); + + AccessControlEntry[] entries = pt.getAccessControlEntries(); + + assertEquals(2, entries.length); + + JackrabbitAccessControlEntry first = (JackrabbitAccessControlEntry) entries[0]; + assertEquals(principal2, first.getPrincipal()); + + JackrabbitAccessControlEntry second = (JackrabbitAccessControlEntry) entries[1]; + assertEquals(testPrincipal, second.getPrincipal()); + assertFalse(second.isAllow()); + assertSamePrivileges(new Privilege[] {readPriv[0], writePriv[0]}, second.getPrivileges()); + } + + public void testTwoEntriesPerPrincipal() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + + Privilege[] readPriv = privilegesFromName(Privilege.JCR_READ); + Privilege[] writePriv = privilegesFromName(Privilege.JCR_WRITE); + Privilege[] acReadPriv = privilegesFromName(Privilege.JCR_READ_ACCESS_CONTROL); + + pt.addEntry(testPrincipal, readPriv, true, emptyRestrictions); + pt.addEntry(testPrincipal, writePriv, true, emptyRestrictions); + pt.addEntry(testPrincipal, acReadPriv, true, emptyRestrictions); + + pt.addEntry(testPrincipal, readPriv, false, emptyRestrictions); + pt.addEntry(new PrincipalImpl(testPrincipal.getName()), readPriv, false, emptyRestrictions); + pt.addEntry(new Principal() { + public String getName() { + return testPrincipal.getName(); + } + }, readPriv, false, emptyRestrictions); + + AccessControlEntry[] entries = pt.getAccessControlEntries(); + assertEquals(2, entries.length); + } + + /** + * Test if new entries get appended at the end of the list. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testNewEntriesAppendedAtEnd() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + + Privilege[] readPriv = privilegesFromName(Privilege.JCR_READ); + Privilege[] writePriv = privilegesFromName(Privilege.JCR_WRITE); + + pt.addEntry(testPrincipal, readPriv, true, emptyRestrictions); + pt.addEntry(principalMgr.getEveryone(), readPriv, true, emptyRestrictions); + pt.addEntry(testPrincipal, writePriv, false, emptyRestrictions); + + AccessControlEntry[] entries = pt.getAccessControlEntries(); + + assertEquals(3, entries.length); + + JackrabbitAccessControlEntry last = (JackrabbitAccessControlEntry) entries[2]; + assertEquals(testPrincipal, last.getPrincipal()); + assertEquals(false, last.isAllow()); + assertEquals(writePriv[0], last.getPrivileges()[0]); + } + + public void testRestrictions() throws RepositoryException, NotExecutableException { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + + String restrName = ((SessionImpl) superuser).getJCRName(ACLTemplate.P_GLOB); + + String[] names = pt.getRestrictionNames(); + assertNotNull(names); + assertEquals(1, names.length); + assertEquals(restrName, names[0]); + assertEquals(PropertyType.STRING, pt.getRestrictionType(names[0])); + + Privilege[] writePriv = privilegesFromName(Privilege.JCR_WRITE); + + // add entry without restr. -> must succeed + assertTrue(pt.addAccessControlEntry(testPrincipal, writePriv)); + assertEquals(1, pt.getAccessControlEntries().length); + + // ... again -> no modification. + assertFalse(pt.addAccessControlEntry(testPrincipal, writePriv)); + assertEquals(1, pt.getAccessControlEntries().length); + + // ... again using different method -> no modification. + assertFalse(pt.addEntry(testPrincipal, writePriv, true)); + assertEquals(1, pt.getAccessControlEntries().length); + + // ... complementary entry -> must modify the acl + assertTrue(pt.addEntry(testPrincipal, writePriv, false)); + assertEquals(1, pt.getAccessControlEntries().length); + + // add an entry with a restrictions: + Map restrictions = Collections.singletonMap(restrName, superuser.getValueFactory().createValue("/.*")); + assertTrue(pt.addEntry(testPrincipal, writePriv, false, restrictions)); + assertEquals(2, pt.getAccessControlEntries().length); + + // ... same again -> no modification. + assertFalse(pt.addEntry(testPrincipal, writePriv, false, restrictions)); + assertEquals(2, pt.getAccessControlEntries().length); + + // ... complementary entry -> must modify the acl. + assertTrue(pt.addEntry(testPrincipal, writePriv, true, restrictions)); + assertEquals(2, pt.getAccessControlEntries().length); + } + + public void testInsertionOrder() throws Exception { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + + Privilege[] readPriv = privilegesFromName(Privilege.JCR_READ); + Privilege[] writePriv = privilegesFromName(Privilege.JCR_WRITE); + Privilege[] addNodePriv = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + + String restrName = ((SessionImpl) superuser).getJCRName(ACLTemplate.P_GLOB); + Map restrictions = Collections.singletonMap(restrName, superuser.getValueFactory().createValue("/.*")); + + pt.addEntry(testPrincipal, readPriv, true, emptyRestrictions); + pt.addEntry(testPrincipal, writePriv, false, emptyRestrictions); + pt.addEntry(testPrincipal, addNodePriv, true, restrictions); + + AccessControlEntry[] entries = pt.getAccessControlEntries(); + assertTrue(Arrays.equals(readPriv, entries[0].getPrivileges())); + assertTrue(Arrays.equals(writePriv, entries[1].getPrivileges())); + assertTrue(Arrays.equals(addNodePriv, entries[2].getPrivileges())); + } + + public void testInsertionOrder2() throws Exception { + JackrabbitAccessControlList pt = createEmptyTemplate(getTestPath()); + + Privilege[] readPriv = privilegesFromName(Privilege.JCR_READ); + Privilege[] writePriv = privilegesFromName(Privilege.JCR_WRITE); + Privilege[] addNodePriv = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + + String restrName = ((SessionImpl) superuser).getJCRName(ACLTemplate.P_GLOB); + Map restrictions = Collections.singletonMap(restrName, superuser.getValueFactory().createValue("/.*")); + + pt.addEntry(testPrincipal, readPriv, true, emptyRestrictions); + pt.addEntry(testPrincipal, addNodePriv, true, restrictions); + pt.addEntry(testPrincipal, writePriv, false, emptyRestrictions); + + AccessControlEntry[] entries = pt.getAccessControlEntries(); + assertTrue(Arrays.equals(readPriv, entries[0].getPrivileges())); + assertTrue(Arrays.equals(addNodePriv, entries[1].getPrivileges())); + assertTrue(Arrays.equals(writePriv, entries[2].getPrivileges())); + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/AcReadWriteTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/AcReadWriteTest.java new file mode 100644 index 00000000000..f04036f337d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/AcReadWriteTest.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.security.authorization.AbstractEvaluationTest; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * ACWriteTest... + */ +public class AcReadWriteTest extends AbstractEvaluationTest { + + protected String path; + protected String childNPath; + protected String childNPath2; + protected String childPPath; + protected String childchildPPath; + protected String siblingPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create some nodes below the test root in order to apply ac-stuff + Node node = testRootNode.addNode(nodeName1, testNodeType); + Node cn1 = node.addNode(nodeName2, testNodeType); + Property cp1 = node.setProperty(propertyName1, "anyValue"); + Node cn2 = node.addNode(nodeName3, testNodeType); + + Property ccp1 = cn1.setProperty(propertyName1, "childNodeProperty"); + + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + superuser.save(); + + path = node.getPath(); + childNPath = cn1.getPath(); + childNPath2 = cn2.getPath(); + childPPath = cp1.getPath(); + childchildPPath = ccp1.getPath(); + siblingPath = n2.getPath(); + } + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) { + return Collections.emptyMap(); + } + + + public void testAccessControlPrivileges() throws RepositoryException, NotExecutableException { + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + /* grant 'testUser' rep:write, rep:readAccessControl and + rep:modifyAccessControl privileges at 'path' */ + Privilege[] privileges = privilegesFromNames(new String[] { + PrivilegeRegistry.REP_WRITE, + Privilege.JCR_READ_ACCESS_CONTROL, + Privilege.JCR_MODIFY_ACCESS_CONTROL + }); + JackrabbitAccessControlList tmpl = givePrivileges(path, privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + /* + testuser must have + - permission to view AC items + - permission to modify AC items + */ + // the policy node however must be visible to the test-user + assertTrue(testSession.itemExists(tmpl.getPath() + "/rep:policy")); + + testAcMgr.getPolicies(tmpl.getPath()); + testAcMgr.removePolicy(tmpl.getPath(), tmpl); + } + + /** + * Test if a new applicable policy can be applied within a individual + * subtree where AC-modification is allowed. + * + * @throws RepositoryException + * @throws NotExecutableException + * @see JCR-2869 + */ + public void testSetNewPolicy() throws RepositoryException, NotExecutableException { + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + /* grant 'testUser' rep:write, rep:readAccessControl and + rep:modifyAccessControl privileges at 'path' */ + Privilege[] privileges = privilegesFromNames(new String[] { + PrivilegeRegistry.REP_WRITE, + Privilege.JCR_READ_ACCESS_CONTROL, + Privilege.JCR_MODIFY_ACCESS_CONTROL + }); + JackrabbitAccessControlList tmpl = givePrivileges(path, privileges, getRestrictions(superuser, path)); + + AccessControlManager testAcMgr = getTestACManager(); + /* + testuser must be allowed to set a new policy at a child node. + */ + AccessControlPolicyIterator it = testAcMgr.getApplicablePolicies(childNPath); + while (it.hasNext()) { + AccessControlPolicy plc = it.nextAccessControlPolicy(); + testAcMgr.setPolicy(childNPath, plc); + testAcMgr.removePolicy(childNPath, plc); + } + } + + public void testSetModifiedPolicy() throws RepositoryException, NotExecutableException { + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + /* grant 'testUser' rep:write, rep:readAccessControl and + rep:modifyAccessControl privileges at 'path' */ + Privilege[] privileges = privilegesFromNames(new String[] { + PrivilegeRegistry.REP_WRITE, + Privilege.JCR_READ_ACCESS_CONTROL, + Privilege.JCR_MODIFY_ACCESS_CONTROL + }); + JackrabbitAccessControlList tmpl = givePrivileges(path, privileges, getRestrictions(superuser, path)); + + /* + testuser must be allowed to set (modified) policy at target node. + */ + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + AccessControlPolicy[] policies = testAcMgr.getPolicies(path); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof AccessControlList); + + AccessControlList acl = (AccessControlList) policies[0]; + if (acl.addAccessControlEntry(testUser.getPrincipal(), new Privilege[] {testAcMgr.privilegeFromName(Privilege.JCR_LOCK_MANAGEMENT)})) { + testAcMgr.setPolicy(path, acl); + testSession.save(); + } + } + + public void testRetrievePrivilegesOnAcNodes() throws NotExecutableException, RepositoryException { + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + // give 'testUser' jcr:readAccessControl privileges at 'path' + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_READ_ACCESS_CONTROL + }); + JackrabbitAccessControlList tmpl = givePrivileges(path, privileges, getRestrictions(superuser, path)); + + /* + testuser must be allowed to read ac-content at target node. + */ + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + assertTrue(testAcMgr.hasPrivileges(path, privileges)); + + AccessControlPolicy[] policies = testAcMgr.getPolicies(path); + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + String aclNodePath = null; + Node n = superuser.getNode(path); + for (NodeIterator itr = n.getNodes(); itr.hasNext();) { + Node child = itr.nextNode(); + if (child.isNodeType("rep:Policy")) { + aclNodePath = child.getPath(); + } + } + + if (aclNodePath == null) { + fail("Expected node at " + path + " to have an ACL child node."); + } + + assertTrue(testAcMgr.hasPrivileges(aclNodePath, privileges)); + assertTrue(testSession.hasPermission(aclNodePath, Session.ACTION_READ)); + + for (NodeIterator aceNodes = superuser.getNode(aclNodePath).getNodes(); aceNodes.hasNext();) { + String aceNodePath = aceNodes.nextNode().getPath(); + assertTrue(testAcMgr.hasPrivileges(aceNodePath, privileges)); + assertTrue(testSession.hasPermission(aceNodePath, Session.ACTION_READ)); + } + } + + public void testReadAccessControl() throws NotExecutableException, RepositoryException { + /* precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + /* give 'testUser' jcr:readAccessControl privileges at subtree below + path excluding the node at path itself. */ + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_READ_ACCESS_CONTROL + }); + Map restrictions = new HashMap(getRestrictions(superuser, path)); + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/" + nodeName2)); + JackrabbitAccessControlList tmpl = givePrivileges(path, privileges, restrictions); + + /* + testuser must not be allowed to read AC content at the target node; + however, retrieving potential AC content at 'childPath' is granted. + */ + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + assertFalse(testAcMgr.hasPrivileges(path, privileges)); + try { + testAcMgr.getPolicies(path); + fail("AccessDeniedException expected"); + } catch (AccessDeniedException e) { + // success. + } + + assertTrue(testAcMgr.hasPrivileges(childNPath, privileges)); + assertEquals(0, testAcMgr.getPolicies(childNPath).length); + + /* similarly reading the corresponding AC items at 'path' must be forbidden */ + String aclNodePath = null; + Node n = superuser.getNode(path); + for (NodeIterator itr = n.getNodes(); itr.hasNext();) { + Node child = itr.nextNode(); + if (child.isNodeType("rep:Policy")) { + aclNodePath = child.getPath(); + } + } + if (aclNodePath == null) { + fail("Expected node at " + path + " to have an ACL child node."); + } + + assertFalse(testSession.nodeExists(aclNodePath)); + + for (NodeIterator aceNodes = superuser.getNode(aclNodePath).getNodes(); aceNodes.hasNext();) { + Node aceNode = aceNodes.nextNode(); + String aceNodePath = aceNode.getPath(); + assertFalse(testSession.nodeExists(aceNodePath)); + + for (PropertyIterator it = aceNode.getProperties(); it.hasNext();) { + assertFalse(testSession.propertyExists(it.nextProperty().getPath())); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EffectivePolicyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EffectivePolicyTest.java new file mode 100644 index 00000000000..8f274b9bd8a --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EffectivePolicyTest.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.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractEffectivePolicyTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * EffectivePolicyTest... + */ +public class EffectivePolicyTest extends AbstractEffectivePolicyTest { + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) { + return Collections.emptyMap(); + } + + public void testEffectivePoliciesByPath() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + // give 'testUser' READ_AC privileges at 'path' + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_READ_ACCESS_CONTROL, + }); + + givePrivileges(path, privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + assertFalse(testAcMgr.hasPrivileges("/", privileges)); + assertTrue(testAcMgr.hasPrivileges(path, privileges)); + + // since read-ac access is denied on the root that by default is + // access controlled, getEffectivePolicies must fail due to missing + // permissions to view all the effective policies. + try { + testAcMgr.getEffectivePolicies(path); + fail(); + } catch (AccessDeniedException e) { + // success + } + + // ... and same on childNPath. + try { + testAcMgr.getEffectivePolicies(childNPath); + fail(); + } catch (AccessDeniedException e) { + // success + } + } + + public void testGetEffectivePoliciesByPrincipal() throws Exception { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + // give 'testUser' READ_AC privileges at 'path' + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_READ_ACCESS_CONTROL, + }); + + givePrivileges(path, privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + // effective policies for testPrinicpal only on path -> must succeed. + ((JackrabbitAccessControlManager) testAcMgr).getEffectivePolicies(Collections.singleton(testUser.getPrincipal())); + + // effective policies for a combination of principals -> must fail since + // policy for 'everyone' at root node cannot be read by testuser + Set principals = ((SessionImpl) testSession).getSubject().getPrincipals(); + try { + ((JackrabbitAccessControlManager) testAcMgr).getEffectivePolicies(principals); + fail(); + } catch (AccessDeniedException e) { + // success + } + + withdrawPrivileges(childNPath, privileges, getRestrictions(superuser, childNPath)); + + // the effective policies included the allowed acl at 'path' and + // the denied acl at 'childNPath' -> must fail + try { + ((JackrabbitAccessControlManager) testAcMgr).getEffectivePolicies(Collections.singleton(testUser.getPrincipal())); + fail(); + } catch (AccessDeniedException e) { + // success + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EntryCollectorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EntryCollectorTest.java new file mode 100644 index 00000000000..237ac71a882 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EntryCollectorTest.java @@ -0,0 +1,431 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import java.security.Principal; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlList; +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.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +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.core.security.TestPrincipal; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; + +/** + * EntryCollectorTest... + */ +public class EntryCollectorTest extends AbstractAccessControlTest { + + private Group testGroup; + private User testUser; + + private String path; + private String childNPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create some nodes below the test root in order to apply ac-stuff + Node node = testRootNode.addNode(nodeName1, testNodeType); + Node cn1 = node.addNode(nodeName2, testNodeType); + superuser.save(); + + path = node.getPath(); + childNPath = cn1.getPath(); + + // create the testGroup + UserManager umgr = getUserManager(superuser); + + Principal groupPrincipal = new TestPrincipal("testGroup" + UUID.randomUUID()); + testGroup = umgr.createGroup(groupPrincipal); + testUser = umgr.createUser("testUser" + UUID.randomUUID(), "pw"); + if (!umgr.isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + } + + @Override + protected void tearDown() throws Exception { + try { + if (testGroup != null) { + testGroup.remove(); + if (!getUserManager(superuser).isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + } + if (testUser != null) { + testUser.remove(); + if (!getUserManager(superuser).isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + } + } finally { + super.tearDown(); + } + } + + private static UserManager getUserManager(Session session) throws + NotExecutableException { + if (!(session instanceof JackrabbitSession)) { + throw new NotExecutableException(); + } + try { + return ((JackrabbitSession) session).getUserManager(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + } + + private ACLTemplate getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, + AccessDeniedException, NotExecutableException { + // try applicable (new) ACLs first + AccessControlPolicyIterator itr = acM.getApplicablePolicies(path); + while (itr.hasNext()) { + AccessControlPolicy policy = itr.nextAccessControlPolicy(); + if (policy instanceof ACLTemplate) { + return (ACLTemplate) policy; + } + } + // try if there is an acl that has been set before: + AccessControlPolicy[] pcls = acM.getPolicies(path); + for (AccessControlPolicy policy : pcls) { + if (policy instanceof ACLTemplate) { + return (ACLTemplate) policy; + } + } + // no applicable or existing ACLTemplate to edit -> not executable. + throw new NotExecutableException(); + } + + private ACLTemplate modifyPrivileges(String path, Principal principal, Privilege[] privileges, boolean isAllow) throws NotExecutableException, RepositoryException { + ACLTemplate tmpl = getPolicy(acMgr, path, principal); + tmpl.addEntry(principal, privileges, isAllow); + + acMgr.setPolicy(tmpl.getPath(), tmpl); + superuser.save(); + + return tmpl; + } + + private static void verifyACEs(AccessControlPolicy[] policies, + String policyPath, int numberOfAce) throws RepositoryException { + JackrabbitAccessControlList acl = null; + for (AccessControlPolicy p : policies) { + if (p instanceof JackrabbitAccessControlList) { + if (policyPath.equals(((JackrabbitAccessControlList) p).getPath())) { + acl = (JackrabbitAccessControlList) p; + } + } + } + + if (acl == null) { + fail("No Jackrabbit ACL found at " + policyPath); + } else { + assertEquals(numberOfAce, acl.getAccessControlEntries().length); + } + } + + public void testCache() throws Exception { + + // --- test1 : add an ACE at path -------------------------------------- + modifyPrivileges(path, testGroup.getPrincipal(), privilegesFromName(Privilege.JCR_READ), true); + AccessControlPolicy[] plcs = acMgr.getEffectivePolicies(path); + AccessControlPolicy[] plcs2 = acMgr.getEffectivePolicies(childNPath); + // effective policies must be the equal on path and childPath + assertTrue(Arrays.equals(plcs, plcs2)); + // the policy at 'path' must contain a single ACE + verifyACEs(plcs2, path, 1); + + // --- test2: modify the policy at 'path' ------------------------------ + modifyPrivileges(path, testGroup.getPrincipal(), privilegesFromName(Privilege.JCR_WRITE), false); + plcs = acMgr.getEffectivePolicies(path); + plcs2 = acMgr.getEffectivePolicies(childNPath); + // effective policies must be the equal on path and childNPath + assertTrue(Arrays.equals(plcs, plcs2)); + verifyACEs(plcs2, path, 2); + + // --- test3: add an policy at childNPath ------------------------------ + modifyPrivileges(childNPath, testGroup.getPrincipal(), privilegesFromName(Privilege.JCR_ADD_CHILD_NODES), true); + plcs = acMgr.getEffectivePolicies(path); + plcs2 = acMgr.getEffectivePolicies(childNPath); + assertFalse(Arrays.equals(plcs, plcs2)); + verifyACEs(plcs2, path, 2); + verifyACEs(plcs2, childNPath, 1); + + // --- test4: modify policy at childNPath ------------------------------ + modifyPrivileges(childNPath, testGroup.getPrincipal(), privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES), true); + plcs = acMgr.getEffectivePolicies(path); + plcs2 = acMgr.getEffectivePolicies(childNPath); + assertFalse(Arrays.equals(plcs, plcs2)); + verifyACEs(plcs2, path, 2); + // still a single ACE at childNPath. but privileges must be adjusted + verifyACEs(plcs2, childNPath, 1); + AccessControlList acl = null; + for (AccessControlPolicy p : plcs2) { + if (p instanceof JackrabbitAccessControlList && childNPath.equals(((JackrabbitAccessControlList) p).getPath())) { + acl = (AccessControlList) p; + } + } + Privilege[] privs = privilegesFromNames(new String[] {Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_REMOVE_CHILD_NODES}); + assertEquals(privs, acl.getAccessControlEntries()[0].getPrivileges()); + + // --- test4: remove policy at childNPath ------------------------------ + acMgr.removePolicy(childNPath, acMgr.getPolicies(childNPath)[0]); + superuser.save(); + + plcs = acMgr.getEffectivePolicies(path); + AccessControlPolicy[] plcs3 = acMgr.getEffectivePolicies(childNPath); + + assertTrue(Arrays.equals(plcs, plcs3)); + assertFalse(Arrays.equals(plcs2, plcs3)); + + for (AccessControlPolicy p : plcs3) { + if (p instanceof JackrabbitAccessControlList) { + if (childNPath.equals(((JackrabbitAccessControlList) p).getPath())) { + fail("Policy at path has been removed."); + } + } + } + verifyACEs(plcs, path, 2); + } + + /** + * Asserts that the given privilege sets are equal, regardless of ordering. + */ + private void assertEquals(Privilege[] expected, Privilege[] actual) { + assertEquals(getPrivilegeNames(expected), getPrivilegeNames(actual)); + } + + private Set getPrivilegeNames(Privilege[] privileges) { + Set names = new HashSet(); + for (Privilege privilege : privileges) { + names.add(privilege.getName()); + } + return names; + } + + public void testPermissions() throws Exception { + Session superuser2 = getHelper().getSuperuserSession(); + try { + JackrabbitAccessControlManager acM = (JackrabbitAccessControlManager) acMgr; + JackrabbitAccessControlManager acM2 = (JackrabbitAccessControlManager) superuser2.getAccessControlManager(); + Set principals = Collections.singleton(testGroup.getPrincipal()); + + // --- test1 : add an ACE at path ---------------------------------- + Privilege[] privs = privilegesFromName(Privilege.JCR_LOCK_MANAGEMENT); + modifyPrivileges(path, testGroup.getPrincipal(), privs, true); + + assertTrue(acM.hasPrivileges(path, principals, privs)); + assertTrue(acM2.hasPrivileges(path, principals, privs)); + + assertTrue(acM.hasPrivileges(childNPath, principals, privs)); + assertTrue(acM2.hasPrivileges(childNPath, principals, privs)); + + // --- test2: modify the policy at 'path' ------------------------------ + modifyPrivileges(path, testGroup.getPrincipal(), privilegesFromName(Privilege.JCR_WRITE), true); + + privs = privilegesFromNames(new String[] { + Privilege.JCR_LOCK_MANAGEMENT, + Privilege.JCR_WRITE}); + assertTrue(acM.hasPrivileges(path, principals, privs)); + assertTrue(acM2.hasPrivileges(path, principals, privs)); + + assertTrue(acM.hasPrivileges(childNPath, principals, privs)); + assertTrue(acM2.hasPrivileges(childNPath, principals, privs)); + + // --- test3: add an policy at childNPath ------------------------------ + modifyPrivileges(childNPath, testGroup.getPrincipal(), + privilegesFromName(Privilege.JCR_ADD_CHILD_NODES), false); + + privs = privilegesFromNames(new String[] { + Privilege.JCR_LOCK_MANAGEMENT, + Privilege.JCR_WRITE}); + assertTrue(acM.hasPrivileges(path, principals, privs)); + assertTrue(acM2.hasPrivileges(path, principals, privs)); + + privs = privilegesFromNames(new String[] { + Privilege.JCR_LOCK_MANAGEMENT, + Privilege.JCR_MODIFY_PROPERTIES, + Privilege.JCR_REMOVE_CHILD_NODES, + Privilege.JCR_REMOVE_NODE}); + assertTrue(acM.hasPrivileges(childNPath, principals, privs)); + assertTrue(acM2.hasPrivileges(childNPath, principals, privs)); + + + // --- test4: modify policy at childNPath -------------------------- + modifyPrivileges(childNPath, testGroup.getPrincipal(), + privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES), false); + + privs = privilegesFromNames(new String[] { + Privilege.JCR_LOCK_MANAGEMENT, + Privilege.JCR_WRITE}); + assertTrue(acM.hasPrivileges(path, principals, privs)); + assertTrue(acM2.hasPrivileges(path, principals, privs)); + + privs = privilegesFromNames(new String[] { + Privilege.JCR_LOCK_MANAGEMENT, + Privilege.JCR_MODIFY_PROPERTIES, + Privilege.JCR_REMOVE_NODE}); + assertTrue(acM.hasPrivileges(childNPath, principals, privs)); + assertTrue(acM2.hasPrivileges(childNPath, principals, privs)); + + // --- test4: remove policy at childNPath -------------------------- + acMgr.removePolicy(childNPath, acMgr.getPolicies(childNPath)[0]); + superuser.save(); + + privs = privilegesFromNames(new String[] { + Privilege.JCR_LOCK_MANAGEMENT, + Privilege.JCR_WRITE}); + + assertTrue(acM.hasPrivileges(path, principals, privs)); + assertTrue(acM2.hasPrivileges(path, principals, privs)); + + assertTrue(acM.hasPrivileges(childNPath, principals, privs)); + assertTrue(acM2.hasPrivileges(childNPath, principals, privs)); + + } finally { + superuser2.logout(); + } + } + + static interface TestInvokation { + public void runTest() throws Exception; + } + + private void runTestUnderLoad(TestInvokation ti) throws Exception { + + JcrTestThread t[] = new JcrTestThread[4]; + + for (int i = 0; i < t.length; i++) { + t[i] = new JcrTestThread(); + } + + try { + for (int i = 0; i < t.length; i++) { + t[i].start(); + } + ti.runTest(); + } + finally { + for (int i = 0; i < t.length; i++) { + t[i].stopMe(); + t[i].join(); + Throwable th = t[i].getLastExc(); + if (th != null) { + fail("failure in load thread: " + th); + } + } + } + } + + public void testCacheUnderLoad() throws Exception { + runTestUnderLoad(new TestInvokation() { + public void runTest() throws Exception { + testCache(); + } + }); + } + + public void testPermissionsUnderLoad() throws Exception { + runTestUnderLoad(new TestInvokation() { + public void runTest() throws Exception { + testPermissions(); + } + }); + } + + /** + * Test code that that walks the repository. + */ + private class JcrTestThread extends Thread { + + private boolean stopme = false; + private Throwable lastErr; + + @Override + public void run() { + while (!this.stopme) { + Session session = null; + try { + session = getHelper().getReadOnlySession(); + walk(session.getRootNode()); + } + catch (RepositoryException ex) { + // ignored + } catch (Throwable ex) { + lastErr = ex; + } + finally { + if (session != null) { + session.logout(); + session = null; + } + } + } + } + + public void stopMe() { + this.stopme = true; + } + + public Throwable getLastExc() { + return lastErr; + } + + private void walk(Node node) { + if (stopme) { + return; + } + + try { + if ("/jcr:system".equals(node.getPath())) { + // do not descend into an non-interesting subtree + return; + } + + NodeIterator ni = node.getNodes(); + while (ni.hasNext()) { + walk(ni.nextNode()); + } + } catch (RepositoryException ex) { + // ignore + } catch (Throwable ex) { + lastErr = ex; + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EntryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EntryTest.java new file mode 100644 index 00000000000..684c031c127 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EntryTest.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.core.security.authorization.acl; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.AbstractEvaluationTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * EntryTest... + */ +public class EntryTest extends AbstractEvaluationTest { + + private String testPath; + private JackrabbitAccessControlList acl; + + protected void setUp() throws Exception { + super.setUp(); + testPath = testRootNode.getPath(); + } + + @Override + protected void tearDown() throws Exception { + try { + acMgr.removePolicy(testPath, acl); + superuser.save(); + } finally { + super.tearDown(); + } + } + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) { + return Collections.emptyMap(); + } + + public void testIsLocal() throws NotExecutableException, RepositoryException { + acl = getPolicy(acMgr, testPath, testUser.getPrincipal()); + modifyPrivileges(testPath, Privilege.JCR_READ, true); + + NodeImpl aclNode = (NodeImpl) superuser.getNode(acl.getPath() + "/rep:policy"); + List entries = Entry.readEntries(aclNode, testRootNode.getPath()); + assertTrue(!entries.isEmpty()); + assertEquals(1, entries.size()); + + Entry entry = entries.iterator().next(); + // false since acl has been created from path only -> no id + assertTrue(entry.isLocal(((NodeImpl) testRootNode).getNodeId())); + // false since internal id is null -> will never match. + assertFalse(entry.isLocal(NodeId.randomId())); + } + + public void testRestrictions() throws RepositoryException, NotExecutableException { + // test if restrictions with expanded name are properly resolved + Map restrictions = new HashMap(); + restrictions.put(ACLTemplate.P_GLOB.toString(), superuser.getValueFactory().createValue("*/test")); + + acl = getPolicy(acMgr, testPath, testUser.getPrincipal()); + acl.addEntry(testUser.getPrincipal(), new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_ALL)}, true, restrictions); + acMgr.setPolicy(testPath, acl); + superuser.save(); + + Map toMatch = new HashMap(); + toMatch.put(acl.getPath(), false); + toMatch.put(acl.getPath() + "test", false); + + toMatch.put(acl.getPath() + "/test", true); + toMatch.put(acl.getPath() + "/something/test", true); + toMatch.put(acl.getPath() + "de/test", true); + + NodeImpl aclNode = (NodeImpl) superuser.getNode(acl.getPath() + "/rep:policy"); + List entries = Entry.readEntries(aclNode, testRootNode.getPath()); + assertTrue(!entries.isEmpty()); + assertEquals(1, entries.size()); + + Entry entry = entries.iterator().next(); + for (String str : toMatch.keySet()) { + assertEquals("Path to match : " + str, toMatch.get(str).booleanValue(), entry.matches(str)); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EvaluationUtil.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EvaluationUtil.java new file mode 100644 index 00000000000..3aa291a64eb --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/EvaluationUtil.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.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; + +/** + * EvaluationTest... + */ +final class EvaluationUtil { + + static boolean isExecutable(AccessControlManager acMgr) { + try { + AccessControlPolicy[] rootPolicies = acMgr.getPolicies("/"); + if (rootPolicies.length > 0 && rootPolicies[0] instanceof ACLTemplate) { + return true; + } + } catch (RepositoryException e) { + // ignore + } + return false; + } + + static JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, + AccessDeniedException, NotExecutableException { + // try applicable (new) ACLs first + AccessControlPolicyIterator itr = acM.getApplicablePolicies(path); + while (itr.hasNext()) { + AccessControlPolicy policy = itr.nextAccessControlPolicy(); + if (policy instanceof ACLTemplate) { + return (ACLTemplate) policy; + } + } + // try if there is an acl that has been set before: + AccessControlPolicy[] pcls = acM.getPolicies(path); + for (AccessControlPolicy policy : pcls) { + if (policy instanceof ACLTemplate) { + return (ACLTemplate) policy; + } + } + // no applicable or existing ACLTemplate to edit -> not executable. + throw new NotExecutableException(); + } + + static Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return Collections.emptyMap(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/LockTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/LockTest.java new file mode 100644 index 00000000000..05f9565f8b0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/LockTest.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.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.security.authorization.AbstractLockManagementTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import java.security.Principal; +import java.util.Map; + +/** + * LockTest... + */ +public class LockTest extends AbstractLockManagementTest { + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws + RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/MoveTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/MoveTest.java new file mode 100644 index 00000000000..a56cedef9b4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/MoveTest.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.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.security.authorization.AbstractEvaluationTest; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * MoveTest... + */ +public class MoveTest extends AbstractEvaluationTest { + + private String path; + private String childNPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create some nodes below the test root in order to apply ac-stuff + Node node = testRootNode.addNode(nodeName1, testNodeType); + Node cn1 = node.addNode(nodeName2, testNodeType); + superuser.save(); + + path = node.getPath(); + childNPath = cn1.getPath(); + } + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) { + return Collections.emptyMap(); + } + + public void testMoveAccessControlledNode() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node node3 = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + + String node3Path = node3.getPath(); + Privilege[] privileges = privilegesFromName(NameConstants.JCR_READ.toString()); + + // permissions defined @ childNode + // -> revoke read permission + withdrawPrivileges(childNPath, privileges, getRestrictions(superuser, childNPath)); + + assertFalse(testSession.nodeExists(childNPath)); + assertFalse(testAcMgr.hasPrivileges(childNPath, privileges)); + assertFalse(testSession.nodeExists(node3Path)); + assertFalse(testAcMgr.hasPrivileges(node3Path, privileges)); + + // move the ancestor node + String movedChildNPath = path + "/movedNode"; + String movedNode3Path = movedChildNPath + "/" + nodeName3; + + superuser.move(childNPath, movedChildNPath); + superuser.save(); + + // expected behavior: + // the AC-content present on childNode is still enforced both on + // the node itself and on the subtree. + assertFalse(testSession.nodeExists(movedChildNPath)); + assertFalse(testAcMgr.hasPrivileges(movedChildNPath, privileges)); + assertFalse(testSession.nodeExists(movedNode3Path)); + assertFalse(testAcMgr.hasPrivileges(movedNode3Path, privileges)); + } + + public void testMoveAccessControlledNodeInSubtree() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node node3 = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + + String node3Path = node3.getPath(); + Privilege[] privileges = privilegesFromName(NameConstants.JCR_READ.toString()); + + // permissions defined @ node3Path + // -> revoke read permission + withdrawPrivileges(node3Path, privileges, getRestrictions(superuser, node3Path)); + + assertFalse(testSession.nodeExists(node3Path)); + assertFalse(testAcMgr.hasPrivileges(node3Path, privileges)); + + // move the ancestor node + String movedChildNPath = path + "/movedNode"; + String movedNode3Path = movedChildNPath + "/" + nodeName3; + + superuser.move(childNPath, movedChildNPath); + superuser.save(); + + // expected behavior: + // the AC-content present on node3 is still enforced + assertFalse(testSession.nodeExists(movedNode3Path)); + assertFalse(testAcMgr.hasPrivileges(movedNode3Path, privileges)); + } + + public void testMoveWithDifferentEffectiveAc() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node node3 = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + + String node3Path = node3.getPath(); + Privilege[] privileges = privilegesFromName(NameConstants.JCR_READ.toString()); + + // @path read is denied, @childNode its allowed again + withdrawPrivileges(path, privileges, getRestrictions(superuser, path)); + givePrivileges(childNPath, privileges, getRestrictions(superuser, childNPath)); + + assertTrue(testSession.nodeExists(node3Path)); + assertTrue(testAcMgr.hasPrivileges(node3Path, privileges)); + + // move the ancestor node + String movedPath = path + "/movedNode"; + + superuser.move(node3Path, movedPath); + superuser.save(); + + // expected behavior: + // due to move node3 should not e visible any more + assertFalse(testSession.nodeExists(movedPath)); + assertFalse(testAcMgr.hasPrivileges(movedPath, privileges)); + } + + public void testMoveNodeWithGlobRestriction() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node node3 = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + + String node3Path = node3.getPath(); + Privilege[] privileges = privilegesFromName(NameConstants.JCR_READ.toString()); + + // permissions defined @ path + // restriction: remove read priv to nodeName3 node + Map restrictions = new HashMap(getRestrictions(superuser, childNPath)); + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/"+nodeName3)); + withdrawPrivileges(childNPath, privileges, restrictions); + + assertFalse(testSession.nodeExists(node3Path)); + assertFalse(testAcMgr.hasPrivileges(node3Path, privileges)); + + String movedChildNPath = path + "/movedNode"; + String movedNode3Path = movedChildNPath + "/" + node3.getName(); + + superuser.move(childNPath, movedChildNPath); + superuser.save(); + + assertFalse(testSession.nodeExists(movedNode3Path)); + assertFalse(testAcMgr.hasPrivileges(movedNode3Path, privileges)); + } + + public void testMoveNodeWithGlobRestriction2() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Node node3 = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + + Privilege[] privileges = privilegesFromName(NameConstants.JCR_READ.toString()); + + // permissions defined @ path + // restriction: remove read priv to nodeName3 node + Map restrictions = new HashMap(getRestrictions(superuser, childNPath)); + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/"+nodeName3)); + withdrawPrivileges(childNPath, privileges, restrictions); + + // don't fill the per-session read-cache by calling Session.nodeExists + assertFalse(testAcMgr.hasPrivileges(node3.getPath(), privileges)); + + String movedChildNPath = path + "/movedNode"; + String movedNode3Path = movedChildNPath + "/" + node3.getName(); + + superuser.move(childNPath, movedChildNPath); + superuser.save(); + + assertFalse(testSession.nodeExists(movedNode3Path)); + assertFalse(testAcMgr.hasPrivileges(movedNode3Path, privileges)); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/NodeTypeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/NodeTypeTest.java new file mode 100644 index 00000000000..28ade400b9b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/NodeTypeTest.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.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.security.authorization.AbstractNodeTypeManagementTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import java.security.Principal; +import java.util.Map; + +/** + * NodeTypeTest... + */ +public class NodeTypeTest extends AbstractNodeTypeManagementTest { + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws + RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + @Override + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ReadNodeTypeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ReadNodeTypeTest.java new file mode 100644 index 00000000000..3dc12fbe789 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ReadNodeTypeTest.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.core.security.authorization.acl; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.security.authorization.AbstractEvaluationTest; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.test.NotExecutableException; + +public class ReadNodeTypeTest extends AbstractEvaluationTest { + + private String path; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create some nodes below the test root in order to apply ac-stuff + Node node = testRootNode.addNode(nodeName1, testNodeType); + node.addMixin(NodeType.MIX_LOCKABLE); + superuser.save(); + + path = node.getPath(); + } + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) { + return Collections.emptyMap(); + } + + /** + * @see OAK-2441 + */ + public void testNodeGetPrimaryType() throws Exception { + Map rest = new HashMap(getRestrictions(superuser, path)); + rest.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/jcr:*")); + + withdrawPrivileges(path, privilegesFromName(Privilege.JCR_READ), rest); + + Session testSession = getTestSession(); + Node n = testSession.getNode(path); + + assertFalse(testSession.propertyExists(path + '/' + JcrConstants.JCR_PRIMARYTYPE)); + assertFalse(n.hasProperty(JcrConstants.JCR_PRIMARYTYPE)); + + NodeType primary = n.getPrimaryNodeType(); + } + + /** + * @see OAK-2441 + */ + public void testNodeGetMixinTypes() throws Exception { + Session testSession = getTestSession(); + assertTrue(testSession.propertyExists(path + '/' + JcrConstants.JCR_MIXINTYPES)); + + Map rest = new HashMap(getRestrictions(superuser, path)); + rest.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("/jcr:*")); + + withdrawPrivileges(path, privilegesFromName(Privilege.JCR_READ), rest); + + int noMixins = superuser.getNode(path).getMixinNodeTypes().length; + + Node n = testSession.getNode(path); + assertFalse(testSession.propertyExists(path + '/' + JcrConstants.JCR_MIXINTYPES)); + assertFalse(n.hasProperty(JcrConstants.JCR_MIXINTYPES)); + + NodeType[] mixins = n.getMixinNodeTypes(); + assertEquals(noMixins, mixins.length); + } + + public void testNodeGetMixinTypesWithTransientModifications() throws Exception { + int noMixins = superuser.getNode(path).getMixinNodeTypes().length; + + Node node = superuser.getNode(path); + node.addMixin(NodeType.MIX_CREATED); + + NodeType[] mixins = node.getMixinNodeTypes(); + assertEquals(noMixins+1, mixins.length); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ReadTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ReadTest.java new file mode 100644 index 00000000000..dcd187330a7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/ReadTest.java @@ -0,0 +1,503 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.core.security.authorization.AbstractEvaluationTest; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.test.NotExecutableException; +import org.junit.Test; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * ReadTest... + */ +public class ReadTest extends AbstractEvaluationTest { + + private String path; + private String childNPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create some nodes below the test root in order to apply ac-stuff + Node node = testRootNode.addNode(nodeName1, testNodeType); + Node cn1 = node.addNode(nodeName2, testNodeType); + superuser.save(); + + path = node.getPath(); + childNPath = cn1.getPath(); + } + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) { + return Collections.emptyMap(); + } + + public void testReadDenied() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + + /* deny READ privilege for testUser at 'path' */ + withdrawPrivileges(path, privileges, getRestrictions(superuser, path)); + /* + allow READ privilege for testUser at 'childNPath' + */ + givePrivileges(childNPath, privileges, getRestrictions(superuser, childNPath)); + + + Session testSession = getTestSession(); + + assertFalse(testSession.nodeExists(path)); + assertTrue(testSession.nodeExists(childNPath)); + Node n = testSession.getNode(childNPath); + n.getDefinition(); + } + + public void testDenyUserAllowGroup() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + + /* + deny READ privilege for testUser at 'path' + */ + withdrawPrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* + allow READ privilege for group at 'path' + */ + givePrivileges(path, group, privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + assertFalse(testSession.nodeExists(path)); + } + + public void testAllowGroupDenyUser() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + + /* + allow READ privilege for group at 'path' + */ + givePrivileges(path, group, privileges, getRestrictions(superuser, path)); + /* + deny READ privilege for testUser at 'path' + */ + withdrawPrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + assertFalse(testSession.nodeExists(path)); + } + + public void testAllowUserDenyGroup() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + + /* + allow READ privilege for testUser at 'path' + */ + givePrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* + deny READ privilege for group at 'path' + */ + withdrawPrivileges(path, group, privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + assertTrue(testSession.nodeExists(path)); + } + + public void testDenyGroupAllowUser() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + + /* + deny READ privilege for group at 'path' + */ + withdrawPrivileges(path, group, privileges, getRestrictions(superuser, path)); + + /* + allow READ privilege for testUser at 'path' + */ + givePrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + assertTrue(testSession.nodeExists(path)); + } + + public void testDenyGroupAllowEveryone() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + Principal everyone = ((JackrabbitSession) superuser).getPrincipalManager().getEveryone(); + + /* + deny READ privilege for group at 'path' + */ + withdrawPrivileges(path, group, privileges, getRestrictions(superuser, path)); + + /* + allow READ privilege for everyone at 'path' + */ + givePrivileges(path, everyone, privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + assertTrue(testSession.nodeExists(path)); + } + + public void testAllowEveryoneDenyGroup() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + Principal everyone = ((JackrabbitSession) superuser).getPrincipalManager().getEveryone(); + + /* + allow READ privilege for everyone at 'path' + */ + givePrivileges(path, everyone, privileges, getRestrictions(superuser, path)); + + /* + deny READ privilege for group at 'path' + */ + withdrawPrivileges(path, group, privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + assertFalse(testSession.nodeExists(path)); + } + + public void testDenyGroupPathAllowEveryoneChildPath() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + Principal everyone = ((JackrabbitSession) superuser).getPrincipalManager().getEveryone(); + + /* + deny READ privilege for group at 'path' + */ + withdrawPrivileges(path, group, privileges, getRestrictions(superuser, path)); + + /* + allow READ privilege for everyone at 'childNPath' + */ + givePrivileges(path, everyone, privileges, getRestrictions(superuser, childNPath)); + + Session testSession = getTestSession(); + assertTrue(testSession.nodeExists(childNPath)); + } + + public void testAllowEveryonePathDenyGroupChildPath() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + Principal everyone = ((JackrabbitSession) superuser).getPrincipalManager().getEveryone(); + + /* + allow READ privilege for everyone at 'path' + */ + givePrivileges(path, everyone, privileges, getRestrictions(superuser, path)); + + /* + deny READ privilege for group at 'childNPath' + */ + withdrawPrivileges(path, group, privileges, getRestrictions(superuser, childNPath)); + + Session testSession = getTestSession(); + assertFalse(testSession.nodeExists(childNPath)); + } + + public void testAllowUserPathDenyGroupChildPath() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + + /* + allow READ privilege for testUser at 'path' + */ + givePrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* + deny READ privilege for group at 'childPath' + */ + withdrawPrivileges(path, group, privileges, getRestrictions(superuser, childNPath)); + + Session testSession = getTestSession(); + assertTrue(testSession.nodeExists(childNPath)); + } + + public void testDenyGroupPathAllowUserChildPath() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + + /* + deny READ privilege for group at 'path' + */ + withdrawPrivileges(path, group, privileges, getRestrictions(superuser, path)); + + /* + allow READ privilege for testUser at 'childNPath' + */ + givePrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, childNPath)); + + Session testSession = getTestSession(); + assertTrue(testSession.nodeExists(childNPath)); + } + + public void testDenyUserPathAllowGroupChildPath() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + + /* + deny READ privilege for testUser at 'path' + */ + withdrawPrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* + allow READ privilege for group at 'childNPath' + */ + givePrivileges(path, group, privileges, getRestrictions(superuser, childNPath)); + + Session testSession = getTestSession(); + assertFalse(testSession.nodeExists(childNPath)); + } + + public void testAllowGroupPathDenyUserChildPath() throws Exception { + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + Principal group = getTestGroup().getPrincipal(); + + /* + allow READ privilege for everyone at 'path' + */ + givePrivileges(path, group, privileges, getRestrictions(superuser, path)); + /* + deny READ privilege for testUser at 'childNPath' + */ + withdrawPrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, childNPath)); + + Session testSession = getTestSession(); + assertFalse(testSession.nodeExists(childNPath)); + } + + public void testGlobRestriction() throws Exception { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + ValueFactory vf = superuser.getValueFactory(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Privilege[] read = privilegesFromName(Privilege.JCR_READ); + + Map restrictions = new HashMap(getRestrictions(superuser, path)); + restrictions.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("*/"+jcrPrimaryType)); + + withdrawPrivileges(path, read, restrictions); + + assertTrue(testAcMgr.hasPrivileges(path, read)); + assertTrue(testSession.hasPermission(path, javax.jcr.Session.ACTION_READ)); + testSession.getNode(path); + + assertTrue(testAcMgr.hasPrivileges(childNPath, read)); + assertTrue(testSession.hasPermission(childNPath, javax.jcr.Session.ACTION_READ)); + testSession.getNode(childNPath); + + String propPath = path + "/" + jcrPrimaryType; + assertFalse(testSession.hasPermission(propPath, javax.jcr.Session.ACTION_READ)); + assertFalse(testSession.propertyExists(propPath)); + + propPath = childNPath + "/" + jcrPrimaryType; + assertFalse(testSession.hasPermission(propPath, javax.jcr.Session.ACTION_READ)); + assertFalse(testSession.propertyExists(propPath)); + } + + /** + * @see OAK-2412 + */ + @Test + public void testEmptyGlobRestriction()throws Exception{ + Node grandchild = superuser.getNode(childNPath).addNode("child"); + String ccPath = grandchild.getPath(); + superuser.save(); + + // first deny access to 'path' (read-access is granted in the test setup) + Privilege[] read = privilegesFromName(Privilege.JCR_READ); + withdrawPrivileges(path, read, Collections.EMPTY_MAP); + + Session testSession = getTestSession(); + assertFalse(testSession.nodeExists(path)); + assertFalse(canGetNode(testSession, path)); + assertFalse(testSession.nodeExists(childNPath)); + assertFalse(canGetNode(testSession, childNPath)); + assertFalse(testSession.nodeExists(ccPath)); + assertFalse(canGetNode(testSession, ccPath)); + assertFalse(testSession.propertyExists(childNPath + '/' + JcrConstants.JCR_PRIMARYTYPE)); + + Map emptyStringRestriction = new HashMap(getRestrictions(superuser, childNPath)); + emptyStringRestriction.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("")); + + givePrivileges(childNPath, read, emptyStringRestriction); + assertFalse(testSession.nodeExists(path)); + assertFalse(canGetNode(testSession, path)); + assertTrue(testSession.nodeExists(childNPath)); + assertTrue(canGetNode(testSession, childNPath)); + assertFalse(testSession.nodeExists(ccPath)); + assertFalse(canGetNode(testSession, ccPath)); + assertFalse(testSession.propertyExists(childNPath + '/' + JcrConstants.JCR_PRIMARYTYPE)); + + givePrivileges(ccPath, read, Collections.EMPTY_MAP); + assertTrue(testSession.nodeExists(ccPath)); + assertTrue(canGetNode(testSession, ccPath)); + assertTrue(testSession.propertyExists(ccPath + '/' + JcrConstants.JCR_PRIMARYTYPE)); + } + + /** + * @see OAK-2412 + */ + @Test + public void testEmptyGlobRestriction2()throws Exception{ + Node grandchild = superuser.getNode(childNPath).addNode("child"); + String ccPath = grandchild.getPath(); + superuser.save(); + + // first deny access to 'path' (read-access is granted in the test setup) + Privilege[] read = privilegesFromName(Privilege.JCR_READ); + withdrawPrivileges(path, read, Collections.EMPTY_MAP); + + Session testSession = getTestSession(); + assertFalse(testSession.nodeExists(path)); + assertFalse(canGetNode(testSession, path)); + assertFalse(testSession.nodeExists(childNPath)); + assertFalse(canGetNode(testSession, childNPath)); + assertFalse(testSession.nodeExists(ccPath)); + assertFalse(canGetNode(testSession, ccPath)); + assertFalse(testSession.propertyExists(childNPath + '/' + JcrConstants.JCR_PRIMARYTYPE)); + + Map emptyStringRestriction = new HashMap(getRestrictions(superuser, path)); + emptyStringRestriction.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("")); + + givePrivileges(path, read, emptyStringRestriction); + assertTrue(testSession.nodeExists(path)); + assertTrue(canGetNode(testSession, path)); + assertFalse(testSession.nodeExists(childNPath)); + assertFalse(canGetNode(testSession, childNPath)); + assertFalse(testSession.nodeExists(ccPath)); + assertFalse(canGetNode(testSession, ccPath)); + assertFalse(testSession.propertyExists(childNPath + '/' + JcrConstants.JCR_PRIMARYTYPE)); + } + + /** + * @see OAK-2412 + */ + @Test + public void testEmptyGlobRestriction3()throws Exception{ + Node child2 = superuser.getNode(path).addNode("child2"); + String childNPath2 = child2.getPath(); + superuser.save(); + + try { + Group group1 = getTestGroup(); + Group group2 = getUserManager(superuser).createGroup("group2"); + group2.addMember(testUser); + Group group3 = getUserManager(superuser).createGroup("group3"); + superuser.save(); + + assertTrue(group1.isDeclaredMember(testUser)); + assertTrue(group2.isDeclaredMember(testUser)); + assertFalse(group3.isDeclaredMember(testUser)); + + Privilege[] read = privilegesFromName(Privilege.JCR_READ); + + withdrawPrivileges(path, group1.getPrincipal(), read, Collections.EMPTY_MAP); + Map emptyStringRestriction = new HashMap(getRestrictions(superuser, path)); + emptyStringRestriction.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("")); + givePrivileges(path, group1.getPrincipal(), read, emptyStringRestriction); + + withdrawPrivileges(childNPath, group2.getPrincipal(), read, Collections.EMPTY_MAP); + emptyStringRestriction = new HashMap(getRestrictions(superuser, childNPath)); + emptyStringRestriction.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("")); + givePrivileges(childNPath, group2.getPrincipal(), read, emptyStringRestriction); + + withdrawPrivileges(childNPath2, group3.getPrincipal(), read, Collections.EMPTY_MAP); + emptyStringRestriction = new HashMap(getRestrictions(superuser, childNPath2)); + emptyStringRestriction.put(AccessControlConstants.P_GLOB.toString(), vf.createValue("")); + givePrivileges(childNPath2, group3.getPrincipal(), read, emptyStringRestriction); + + // NOTE: test-session is created here and is expected to reflect the + // group membership changes made above. + Session testSession = getTestSession(); + assertTrue(testSession.nodeExists(path)); + assertTrue(testSession.nodeExists(childNPath)); + assertFalse(testSession.nodeExists(childNPath2)); + } finally { + Authorizable g2 = getUserManager(superuser).getAuthorizable("group2"); + if (g2 != null) { + g2.remove(); + } + Authorizable g3 = getUserManager(superuser).getAuthorizable("group3"); + if (g3 != null) { + g3.remove(); + } + superuser.save(); + } + } + + private static boolean canGetNode(Session session, String nodePath) throws RepositoryException { + try { + session.getNode(nodePath); + return true; + } catch (PathNotFoundException e) { + return false; + } + } + + public void testRemoveMixin() throws Exception { + Node n = superuser.getNode(path); + + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + + withdrawPrivileges(path, privileges, getRestrictions(superuser, path)); + + assertTrue(n.hasNode("rep:policy")); + assertTrue(n.isNodeType("rep:AccessControllable")); + + n.removeMixin("rep:AccessControllable"); + + superuser.save(); + assertFalse(n.hasNode("rep:policy")); + assertFalse(n.isNodeType("rep:AccessControllable")); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/RepositoryOperationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/RepositoryOperationTest.java new file mode 100644 index 00000000000..8c9392afbb2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/RepositoryOperationTest.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.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.security.authorization.AbstractRepositoryOperationTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import java.security.Principal; +import java.util.Map; + +/** + * RepositoryOperationTest... + */ +public class RepositoryOperationTest extends AbstractRepositoryOperationTest { + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws + RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + @Override + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/RestrictionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/RestrictionTest.java new file mode 100644 index 00000000000..125c70f4128 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/RestrictionTest.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.core.security.authorization.acl; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils; +import org.apache.jackrabbit.core.security.authorization.AbstractEvaluationTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.value.StringValue; + +/** + * ReadTest... + */ +public class RestrictionTest extends AbstractEvaluationTest { + + private String path_root; + private String path_a; + private String path_b; + private String path_c; + private String path_d; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create some nodes below the test root in order to apply ac-stuff + Node a = testRootNode.addNode("a", testNodeType); + Node b = a.addNode("b", testNodeType); + Node c = b.addNode("c", testNodeType); + Node d = c.addNode("d", testNodeType); + superuser.save(); + + path_root = testRootNode.getPath(); + path_a = a.getPath(); + path_b = b.getPath(); + path_c = c.getPath(); + path_d = d.getPath(); + } + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) { + return Collections.emptyMap(); + } + + private void addEntry(String path, boolean grant, String restriction, String... privilegeNames) throws Exception { + if (restriction.length() > 0) { + Map rs = new HashMap(); + rs.put("rep:glob", new StringValue(restriction)); + modifyPrivileges(path, testUser.getPrincipal(), AccessControlUtils.privilegesFromNames(acMgr, privilegeNames), grant, rs); + } else { + modifyPrivileges(path, testUser.getPrincipal(), AccessControlUtils.privilegesFromNames(acMgr, privilegeNames), grant, Collections.emptyMap()); + } + } + + /** + * Tests if the restriction are active at the proper place + */ + public void testHasPermissionWithRestrictions() throws Exception { + // create permissions + // allow rep:write /testroot + // deny jcr:removeNode /testroot/a glob=*/c + // allow jcr:removeNode /testroot/a glob=*/b + // allow jcr:removeNode /testroot/a glob=*/c/* + + addEntry(path_root, true, "", Privilege.JCR_READ, Privilege.JCR_WRITE); + addEntry(path_a, false, "*/c", Privilege.JCR_REMOVE_NODE); + addEntry(path_a, true, "*/b", Privilege.JCR_REMOVE_NODE); + addEntry(path_a, true, "*/c/*", Privilege.JCR_REMOVE_NODE); + + Session testSession = getTestSession(); + try { + AccessControlManager acMgr = getAccessControlManager(testSession); + + assertFalse("user should not have remove node on /a/b/c", + acMgr.hasPrivileges(path_c, AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_REMOVE_NODE))); + assertTrue("user should have remove node on /a/b", + acMgr.hasPrivileges(path_b, AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_REMOVE_NODE))); + assertTrue("user should have remove node on /a/b/c/d", + acMgr.hasPrivileges(path_d, AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_REMOVE_NODE))); + + // should be able to remove /a/b/c/d + testSession.getNode(path_d).remove(); + testSession.save(); + + try { + testSession.getNode(path_c).remove(); + testSession.save(); + fail("removing node on /a/b/c should fail"); + } catch (RepositoryException e) { + // all ok + } + } finally { + testSession.logout(); + } + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/TestAll.java new file mode 100644 index 00000000000..fcf16c39bc7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/TestAll.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.core.security.authorization.acl; + +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new ConcurrentTestSuite("security.authorization.acl tests"); + + suite.addTestSuite(ACLTemplateTest.class); + suite.addTestSuite(ACLTemplateEntryTest.class); + suite.addTestSuite(EntryTest.class); + suite.addTestSuite(EntryCollectorTest.class); + + suite.addTestSuite(ReadTest.class); + suite.addTestSuite(WriteTest.class); + suite.addTestSuite(AcReadWriteTest.class); + suite.addTestSuite(LockTest.class); + suite.addTestSuite(VersionTest.class); + suite.addTestSuite(NodeTypeTest.class); + suite.addTestSuite(EffectivePolicyTest.class); + suite.addTestSuite(ACLEditorTest.class); + suite.addTestSuite(RepositoryOperationTest.class); + suite.addTestSuite(MoveTest.class); + suite.addTestSuite(RestrictionTest.class); + + return suite; + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/VersionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/VersionTest.java new file mode 100644 index 00000000000..99a38174881 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/VersionTest.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.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.security.authorization.AbstractVersionManagementTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import java.security.Principal; +import java.util.Map; + +/** + * VersionTest... + */ +public class VersionTest extends AbstractVersionManagementTest { + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws + RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + @Override + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/WriteTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/WriteTest.java new file mode 100644 index 00000000000..cd31be27faf --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/acl/WriteTest.java @@ -0,0 +1,556 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.authorization.acl; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractWriteTest; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.core.security.TestPrincipal; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import javax.jcr.security.AccessControlEntry; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +/** + * EvaluationTest... + */ +public class WriteTest extends AbstractWriteTest { + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable(acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) { + return Collections.emptyMap(); + } + + public void testAccessControlModification2() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + // give 'testUser' READ_AC|MODIFY_AC privileges at 'path' + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_READ_ACCESS_CONTROL, + Privilege.JCR_MODIFY_ACCESS_CONTROL + }); + JackrabbitAccessControlList tmpl = givePrivileges(path, privileges, getRestrictions(superuser, path)); + /* + testuser must + - still have the inherited READ permission. + - must have permission to view AC items at 'path' (and below) + - must have permission to modify AC items at 'path' + + testuser must not have + - permission to view AC items outside of the tree defined by path. + */ + + // make sure the 'rep:policy' node has been created. + assertTrue(superuser.itemExists(tmpl.getPath() + "/rep:policy")); + + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + // test: MODIFY_AC granted at 'path' + assertTrue(testAcMgr.hasPrivileges(path, privilegesFromName(Privilege.JCR_MODIFY_ACCESS_CONTROL))); + + // test if testuser can READ access control on the path and on the + // entire subtree that gets the policy inherited. + AccessControlPolicy[] policies = testAcMgr.getPolicies(path); + testAcMgr.getPolicies(childNPath); + + // test: READ_AC privilege does not apply outside of the tree. + try { + testAcMgr.getPolicies(siblingPath); + fail("READ_AC privilege must not apply outside of the tree it has applied to."); + } catch (AccessDeniedException e) { + // success + } + + // test: MODIFY_AC privilege does not apply outside of the tree. + try { + testAcMgr.setPolicy(siblingPath, policies[0]); + fail("MODIFY_AC privilege must not apply outside of the tree it has applied to."); + } catch (AccessDeniedException e) { + // success + } + + // test if testuser can modify AC-items + // 1) add an ac-entry + ACLTemplate acl = (ACLTemplate) policies[0]; + acl.addAccessControlEntry(testUser.getPrincipal(), privilegesFromName(PrivilegeRegistry.REP_WRITE)); + testAcMgr.setPolicy(path, acl); + testSession.save(); + + assertTrue(testAcMgr.hasPrivileges(path, + privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES))); + + // 2) remove the policy + testAcMgr.removePolicy(path, policies[0]); + testSession.save(); + + // Finally: testuser removed the policy that granted him permission + // to modify the AC content. Since testuser removed the policy, it's + // privileges must be gone again... + try { + testAcMgr.getEffectivePolicies(childNPath); + fail("READ_AC privilege has been revoked -> must throw again."); + } catch (AccessDeniedException e) { + // success + } + // ... and since the ACE is stored with the policy all right except + // READ must be gone. + checkReadOnly(path); + } + + public void testRemovePermission9() throws NotExecutableException, RepositoryException { + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + Privilege[] rmChildNodes = privilegesFromName(Privilege.JCR_REMOVE_CHILD_NODES); + Privilege[] rmNode = privilegesFromName(Privilege.JCR_REMOVE_NODE); + + // add 'remove_child_nodes' at 'path and allow 'remove_node' at childNPath + givePrivileges(path, rmChildNodes, getRestrictions(superuser, path)); + givePrivileges(childNPath, rmNode, getRestrictions(superuser, childNPath)); + /* + expected result: + - rep:policy node can still not be remove for it is access-control + content that requires jcr:modifyAccessControl privilege instead. + */ + String policyPath = childNPath + "/rep:policy"; + assertFalse(getTestSession().hasPermission(policyPath, javax.jcr.Session.ACTION_REMOVE)); + assertTrue(testAcMgr.hasPrivileges(policyPath, new Privilege[] {rmChildNodes[0], rmNode[0]})); + } + + public void testApplicablePolicies() throws RepositoryException { + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(childNPath); + assertTrue(it.hasNext()); + + // the same should be true, if the rep:AccessControllable mixin has + // been manually added + Node n = (Node) superuser.getItem(childNPath); + n.addMixin(((SessionImpl) superuser).getJCRName(AccessControlConstants.NT_REP_ACCESS_CONTROLLABLE)); + it = acMgr.getApplicablePolicies(childNPath); + assertTrue(it.hasNext()); + } + + public void testInheritance2() throws RepositoryException, NotExecutableException { + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + checkReadOnly(childNPath); + + // give jcr:write privilege on 'path' and withdraw them on 'childNPath' + Privilege[] privileges = privilegesFromNames(new String[] {Privilege.JCR_WRITE}); + givePrivileges(path, privileges, getRestrictions(superuser, path)); + withdrawPrivileges(childNPath, privileges, getRestrictions(superuser, path)); + + /* + since evaluation respects inheritance through the node + hierarchy, the jcr:write privilege must not be granted at childNPath + */ + assertFalse(testAcMgr.hasPrivileges(childNPath, privileges)); + + /* + ... same for permissions at 'childNPath' + */ + String actions = Session.ACTION_SET_PROPERTY + "," + Session.ACTION_REMOVE + "," + Session.ACTION_ADD_NODE; + + String nonExistingItemPath = childNPath + "/anyItem"; + assertFalse(testSession.hasPermission(nonExistingItemPath, actions)); + + // yet another level in the hierarchy + Node grandChild = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + String gcPath = grandChild.getPath(); + + // grant write privilege again + givePrivileges(gcPath, privileges, getRestrictions(superuser, path)); + assertTrue(testAcMgr.hasPrivileges(gcPath, privileges)); + assertTrue(testSession.hasPermission(gcPath + "/anyProp", Session.ACTION_SET_PROPERTY)); + // however: removing the grand-child nodes must not be allowed as + // remove_child_node privilege is missing on the direct ancestor. + assertFalse(testSession.hasPermission(gcPath, Session.ACTION_REMOVE)); + } + + public void testInheritedGroupPermissions() throws NotExecutableException, RepositoryException { + Group testGroup = getTestGroup(); + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + Privilege[] privileges = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + + /* give MODIFY_PROPERTIES privilege for testGroup at 'path' */ + givePrivileges(path, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* + withdraw MODIFY_PROPERTIES privilege for everyone at 'childNPath' + */ + withdrawPrivileges(childNPath, EveryonePrincipal.getInstance(), privileges, getRestrictions(superuser, path)); + + // result at 'child path' must be deny + assertFalse(testAcMgr.hasPrivileges(childNPath, privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES))); + } + + public void testInheritedGroupPermissions2() throws NotExecutableException, RepositoryException { + Group testGroup = getTestGroup(); + AccessControlManager testAcMgr = getTestACManager(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + Privilege[] privileges = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + + // NOTE: same as testInheritedGroupPermissions above but using + // everyone on path, testgroup on childpath -> result must be the same + + /* give MODIFY_PROPERTIES privilege for everyone at 'path' */ + givePrivileges(path, EveryonePrincipal.getInstance(), privileges, getRestrictions(superuser, path)); + /* + withdraw MODIFY_PROPERTIES privilege for testGroup at 'childNPath' + */ + withdrawPrivileges(childNPath, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + + // result at 'child path' must be deny + assertFalse(testAcMgr.hasPrivileges(childNPath, privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES))); + } + + public void testMultipleGroupPermissionsOnNode() throws NotExecutableException, RepositoryException { + Group testGroup = getTestGroup(); + + /* create a second group the test user is member of */ + Principal principal = new TestPrincipal("testGroup" + UUID.randomUUID()); + UserManager umgr = getUserManager(superuser); + Group group2 = umgr.createGroup(principal); + try { + group2.addMember(testUser); + if (!umgr.isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + + /* add privileges for the Group the test-user is member of */ + Privilege[] privileges = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + givePrivileges(path, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + + withdrawPrivileges(path, group2.getPrincipal(), privileges, getRestrictions(superuser, path)); + + /* + testuser must get the permissions/privileges inherited from + the group it is member of. + the denial of group2 must succeed + */ + String actions = javax.jcr.Session.ACTION_SET_PROPERTY + "," + javax.jcr.Session.ACTION_READ; + + AccessControlManager testAcMgr = getTestACManager(); + + assertFalse(getTestSession().hasPermission(path, actions)); + Privilege[] privs = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + assertFalse(testAcMgr.hasPrivileges(path, privs)); + } finally { + group2.remove(); + } + } + + public void testMultipleGroupPermissionsOnNode2() throws NotExecutableException, RepositoryException { + Group testGroup = getTestGroup(); + + /* create a second group the test user is member of */ + Principal principal = new TestPrincipal("testGroup" + UUID.randomUUID()); + UserManager umgr = getUserManager(superuser); + Group group2 = umgr.createGroup(principal); + + try { + group2.addMember(testUser); + if (!umgr.isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + + /* add privileges for the Group the test-user is member of */ + Privilege[] privileges = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + withdrawPrivileges(path, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + + givePrivileges(path, group2.getPrincipal(), privileges, getRestrictions(superuser, path)); + + /* + testuser must get the permissions/privileges inherited from + the group it is member of. + granting permissions for group2 must be effective + */ + String actions = javax.jcr.Session.ACTION_SET_PROPERTY + "," + javax.jcr.Session.ACTION_READ; + + AccessControlManager testAcMgr = getTestACManager(); + assertTrue(getTestSession().hasPermission(path, actions)); + Privilege[] privs = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + assertTrue(testAcMgr.hasPrivileges(path, privs)); + } finally { + group2.remove(); + } + } + + public void testReorderGroupPermissions() throws NotExecutableException, RepositoryException { + Group testGroup = getTestGroup(); + + /* create a second group the test user is member of */ + Principal principal = new TestPrincipal("testGroup" + UUID.randomUUID()); + UserManager umgr = getUserManager(superuser); + Group group2 = umgr.createGroup(principal); + + try { + group2.addMember(testUser); + if (!umgr.isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + + /* add privileges for the Group the test-user is member of */ + Privilege[] privileges = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + withdrawPrivileges(path, testGroup.getPrincipal(), privileges, getRestrictions(superuser, path)); + givePrivileges(path, group2.getPrincipal(), privileges, getRestrictions(superuser, path)); + + /* + testuser must get the permissions/privileges inherited from + the group it is member of. + granting permissions for group2 must be effective + */ + String actions = javax.jcr.Session.ACTION_SET_PROPERTY + "," + javax.jcr.Session.ACTION_READ; + + AccessControlManager testAcMgr = getTestACManager(); + assertTrue(getTestSession().hasPermission(path, actions)); + Privilege[] privs = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + assertTrue(testAcMgr.hasPrivileges(path, privs)); + + // reorder the ACEs + AccessControlEntry srcEntry = null; + AccessControlEntry destEntry = null; + JackrabbitAccessControlList acl = (JackrabbitAccessControlList) acMgr.getPolicies(path)[0]; + for (AccessControlEntry entry : acl.getAccessControlEntries()) { + Principal princ = entry.getPrincipal(); + if (testGroup.getPrincipal().equals(princ)) { + destEntry = entry; + } else if (group2.getPrincipal().equals(princ)) { + srcEntry = entry; + } + + } + + acl.orderBefore(srcEntry, destEntry); + acMgr.setPolicy(path, acl); + superuser.save(); + + /* after reordering the permissions must be denied */ + assertFalse(getTestSession().hasPermission(path, actions)); + assertFalse(testAcMgr.hasPrivileges(path, privs)); + + } finally { + group2.remove(); + } + } + + public void testWriteIfReadingParentIsDenied() throws Exception { + Privilege[] privileges = privilegesFromNames(new String[] {Privilege.JCR_READ, Privilege.JCR_WRITE}); + + /* deny READ/WRITE privilege for testUser at 'path' */ + withdrawPrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* + allow READ/WRITE privilege for testUser at 'childNPath' + */ + givePrivileges(childNPath, testUser.getPrincipal(), privileges, getRestrictions(superuser, childNPath)); + + + Session testSession = getTestSession(); + + assertFalse(testSession.nodeExists(path)); + + // reading the node and it's definition must succeed. + assertTrue(testSession.nodeExists(childNPath)); + Node n = testSession.getNode(childNPath); + + n.addNode("someChild"); + n.save(); + } + + public void testRemoveNodeWithPolicy() throws Exception { + Privilege[] privileges = privilegesFromNames(new String[] {Privilege.JCR_READ, Privilege.JCR_WRITE}); + + /* allow READ/WRITE privilege for testUser at 'path' */ + givePrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* allow READ/WRITE privilege for testUser at 'childPath' */ + givePrivileges(childNPath, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + + assertTrue(testSession.nodeExists(childNPath)); + assertTrue(testSession.hasPermission(childNPath, Session.ACTION_REMOVE)); + + Node n = testSession.getNode(childNPath); + + // removing the child node must succeed as both remove-node and + // remove-child-nodes are granted to testsession. + // the policy node underneath childNPath should silently be removed + // as the editing session has no knowledge about it's existence. + n.remove(); + testSession.save(); + } + + public void testRemoveNodeWithInvisibleChild() throws Exception { + Privilege[] privileges = privilegesFromNames(new String[] {Privilege.JCR_READ, Privilege.JCR_WRITE}); + + Node invisible = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + + /* allow READ/WRITE privilege for testUser at 'path' */ + givePrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* deny READ privilege at invisible node. (removal is still granted) */ + withdrawPrivileges(invisible.getPath(), testUser.getPrincipal(), + privilegesFromNames(new String[] {Privilege.JCR_READ}), + getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + + assertTrue(testSession.nodeExists(childNPath)); + assertTrue(testSession.hasPermission(childNPath, Session.ACTION_REMOVE)); + + Node n = testSession.getNode(childNPath); + + // removing the child node must succeed as both remove-node and + // remove-child-nodes are granted to testsession. + // the policy node underneath childNPath should silently be removed + // as the editing session has no knowledge about it's existence. + n.remove(); + testSession.save(); + } + + public void testRemoveNodeWithInvisibleNonRemovableChild() throws Exception { + Privilege[] privileges = privilegesFromNames(new String[] {Privilege.JCR_READ, Privilege.JCR_WRITE}); + + Node invisible = superuser.getNode(childNPath).addNode(nodeName3); + superuser.save(); + + /* allow READ/WRITE privilege for testUser at 'path' */ + givePrivileges(path, testUser.getPrincipal(), privileges, getRestrictions(superuser, path)); + /* deny READ privilege at invisible node. (removal is still granted) */ + withdrawPrivileges(invisible.getPath(), testUser.getPrincipal(), + privileges, + getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + + assertTrue(testSession.nodeExists(childNPath)); + assertTrue(testSession.hasPermission(childNPath, Session.ACTION_REMOVE)); + + Node n = testSession.getNode(childNPath); + + // removing the child node must fail as a hidden child node cannot + // be removed. + try { + n.remove(); + testSession.save(); + fail(); + } catch (AccessDeniedException e) { + // success + } + } + + // https://issues.apache.org/jira/browse/JCR-3131 + public void testEmptySaveNoRootAccess() throws RepositoryException, NotExecutableException { + + Session s = getTestSession(); + s.save(); + + Privilege[] read = privilegesFromName(Privilege.JCR_READ); + + try { + JackrabbitAccessControlList tmpl = getPolicy(acMgr, "/", testUser.getPrincipal()); + tmpl.addEntry(testUser.getPrincipal(), read, false, getRestrictions(superuser, path)); + acMgr.setPolicy(tmpl.getPath(), tmpl); + superuser.save(); + + // empty save operation + s.save(); + } + finally { + // undo revocation of read privilege + JackrabbitAccessControlList tmpl = getPolicy(acMgr, "/", testUser.getPrincipal()); + tmpl.addEntry(testUser.getPrincipal(), read, true, getRestrictions(superuser, path)); + acMgr.setPolicy(tmpl.getPath(), tmpl); + superuser.save(); + } + } + + public void testReorderPolicyNode() throws RepositoryException, NotExecutableException { + Session testSession = getTestSession(); + Node n = testSession.getNode(path); + try { + if (!n.getPrimaryNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Reordering child nodes is not supported.."); + } + + n.orderBefore(Text.getName(childNPath), Text.getName(childNPath2)); + testSession.save(); + fail("test session must not be allowed to reorder nodes."); + } catch (AccessDeniedException e) { + // success. + } + + // grant all privileges + givePrivileges(path, privilegesFromNames(new String[] {Privilege.JCR_ALL}), getRestrictions(superuser, path)); + + n.orderBefore("rep:policy", Text.getName(childNPath2)); + testSession.save(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/combined/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/combined/TestAll.java new file mode 100644 index 00000000000..9198db3743c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/combined/TestAll.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.core.security.authorization.combined; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +/** + * Test suite + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("security.authorization.combined tests"); + + suite.addTestSuite(WriteTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/combined/WriteTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/combined/WriteTest.java new file mode 100644 index 00000000000..34df09b8850 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/combined/WriteTest.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.core.security.authorization.combined; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; + +/** + * EvaluationTest... + */ +public class WriteTest extends org.apache.jackrabbit.core.security.authorization.acl.WriteTest { + + private static Logger log = LoggerFactory.getLogger(WriteTest.class); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // simple test to check if proper provider is present: + try { + getPrincipalBasedPolicy(acMgr, path, testUser.getPrincipal()); + } catch (Exception e) { + superuser.logout(); + throw e; + } + } + + @Override + protected boolean isExecutable() { + try { + AccessControlPolicy[] rootPolicies = acMgr.getPolicies("/"); + if (rootPolicies.length > 0) { + return true; + } + if (acMgr.getApplicablePolicies("/").hasNext()) { + return true; + } + } catch (RepositoryException e) { + // ignore + } + return false; + } + + private JackrabbitAccessControlList getPrincipalBasedPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + if (acM instanceof JackrabbitAccessControlManager) { + AccessControlPolicy[] tmpls = ((JackrabbitAccessControlManager) acM).getApplicablePolicies(principal); + for (AccessControlPolicy tmpl : tmpls) { + if (tmpl instanceof JackrabbitAccessControlList) { + return (JackrabbitAccessControlList) tmpl; + } + } + } + throw new NotExecutableException(); + } + + private JackrabbitAccessControlList givePrivileges(String nPath, + Principal principal, + Privilege[] privileges, + Map restrictions, + boolean nodeBased) throws NotExecutableException, RepositoryException { + if (nodeBased) { + return givePrivileges(nPath, principal, privileges, getRestrictions(superuser, nPath)); + } else { + JackrabbitAccessControlList tmpl = getPrincipalBasedPolicy(acMgr, nPath, principal); + tmpl.addEntry(principal, privileges, true, restrictions); + acMgr.setPolicy(tmpl.getPath(), tmpl); + superuser.save(); + return tmpl; + } + } + + private JackrabbitAccessControlList withdrawPrivileges(String nPath, + Principal principal, + Privilege[] privileges, + Map restrictions, + boolean nodeBased) throws NotExecutableException, RepositoryException { + if (nodeBased) { + return withdrawPrivileges(nPath, principal, privileges, getRestrictions(superuser, nPath)); + } else { + JackrabbitAccessControlList tmpl = getPrincipalBasedPolicy(acMgr, nPath, principal); + tmpl.addEntry(principal, privileges, false, restrictions); + acMgr.setPolicy(tmpl.getPath(), tmpl); + superuser.save(); + return tmpl; + } + } + + private Map getPrincipalBasedRestrictions(String path) throws RepositoryException, NotExecutableException { + if (superuser instanceof SessionImpl) { + Map restr = new HashMap(); + restr.put("rep:nodePath", superuser.getValueFactory().createValue(path, PropertyType.PATH)); + return restr; + } else { + throw new NotExecutableException(); + } + } + + public void testCombinedPolicies() throws RepositoryException, NotExecutableException { + Group testGroup = getTestGroup(); + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + Privilege[] readPrivs = privilegesFromName(Privilege.JCR_READ); + // nodebased: remove READ privilege for 'testUser' at 'path' + withdrawPrivileges(path, readPrivs, getRestrictions(superuser, path)); + // principalbased: add READ privilege for 'testGroup' + givePrivileges(path, testGroup.getPrincipal(), readPrivs, getPrincipalBasedRestrictions(path), false); + /* + expected result: + - nodebased wins over principalbased -> READ is denied + */ + assertFalse(testSession.itemExists(path)); + assertFalse(testSession.hasPermission(path, javax.jcr.Session.ACTION_READ)); + assertFalse(testAcMgr.hasPrivileges(path, readPrivs)); + + // remove the nodebased policy + JackrabbitAccessControlList policy = getPolicy(acMgr, path, testUser.getPrincipal()); + acMgr.removePolicy(policy.getPath(), policy); + superuser.save(); + + /* + expected result: + - READ privilege is present again. + */ + assertTrue(testSession.itemExists(path)); + assertTrue(testSession.hasPermission(path, javax.jcr.Session.ACTION_READ)); + assertTrue(testAcMgr.hasPrivileges(path, readPrivs)); + + // nodebased: add WRITE privilege for 'testUser' at 'path' + Privilege[] wrtPrivileges = privilegesFromName(PrivilegeRegistry.REP_WRITE); + givePrivileges(path, wrtPrivileges, getRestrictions(superuser, path)); + // userbased: deny MODIFY_PROPERTIES privileges for 'testUser' + Privilege[] modPropPrivs = privilegesFromName(Privilege.JCR_MODIFY_PROPERTIES); + withdrawPrivileges(path, testUser.getPrincipal(), modPropPrivs, getPrincipalBasedRestrictions(path), false); + /* + expected result: + - MODIFY_PROPERTIES privilege still present + */ + assertTrue(testSession.hasPermission(path+"/anyproperty", javax.jcr.Session.ACTION_SET_PROPERTY)); + assertTrue(testAcMgr.hasPrivileges(path, wrtPrivileges)); + + // nodebased: deny MODIFY_PROPERTIES privileges for 'testUser' + // on a child node. + withdrawPrivileges(childNPath, testUser.getPrincipal(), modPropPrivs, getRestrictions(superuser, childNPath)); + /* + expected result: + - MODIFY_PROPERTIES privilege still present at 'path' + - no-MODIFY_PROPERTIES privilege at 'childNPath' + */ + assertTrue(testSession.hasPermission(path+"/anyproperty", javax.jcr.Session.ACTION_SET_PROPERTY)); + assertTrue(testAcMgr.hasPrivileges(path, modPropPrivs)); + + assertFalse(testSession.hasPermission(childNPath+"/anyproperty", javax.jcr.Session.ACTION_SET_PROPERTY)); + assertFalse(testAcMgr.hasPrivileges(childNPath, modPropPrivs)); + } + + public void testCanReadOnCombinedPolicies() throws RepositoryException, NotExecutableException { + Group testGroup = getTestGroup(); + Session testSession = getTestSession(); + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + Privilege[] readPrivs = privilegesFromName(Privilege.JCR_READ); + // nodebased: remove READ privilege for 'testUser' at 'path' + withdrawPrivileges(path, readPrivs, getRestrictions(superuser, path)); + // principalbased: add READ privilege for 'testGroup' + givePrivileges(path, testGroup.getPrincipal(), readPrivs, getPrincipalBasedRestrictions(path), false); + /* + expected result: + - nodebased wins over principalbased -> READ is denied + */ + NodeId nodeId = ((NodeImpl) superuser.getNode(path)).getNodeId(); + assertFalse(((SessionImpl) testSession).getAccessManager().canRead(null, nodeId)); + + /* allow again on child N -> should be allowed */ + givePrivileges(childNPath, testGroup.getPrincipal(), readPrivs, getPrincipalBasedRestrictions(path), false); + + NodeId childId = ((NodeImpl) superuser.getNode(childNPath)).getNodeId(); + assertTrue(((SessionImpl) testSession).getAccessManager().canRead(null, childId)); + + // remove the nodebased policy + JackrabbitAccessControlList policy = getPolicy(acMgr, path, testUser.getPrincipal()); + acMgr.removePolicy(policy.getPath(), policy); + superuser.save(); + + /* + expected result: + - READ privilege is present again. + */ + assertTrue(((SessionImpl) testSession).getAccessManager().canRead(null, nodeId)); + + // nodebased: remove READ privilege for 'testUser' at 'path' + givePrivileges(path, readPrivs, getRestrictions(superuser, path)); + // principalbased: add READ privilege for 'testGroup' + withdrawPrivileges(path, testGroup.getPrincipal(), readPrivs, getPrincipalBasedRestrictions(path), false); + /* + expected result: + - nodebased wins over principalbased -> READ is allowed + */ + assertTrue(((SessionImpl) testSession).getAccessManager().canRead(null, nodeId)); + + /* allow again on child N -> should be allowed */ + withdrawPrivileges(childNPath, testGroup.getPrincipal(), readPrivs, getPrincipalBasedRestrictions(path), false); + assertFalse(((SessionImpl) testSession).getAccessManager().canRead(null, childId)); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLTemplateTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLTemplateTest.java new file mode 100644 index 00000000000..3aef6031449 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/ACLTemplateTest.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractACLTemplateTest; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.util.Arrays; +import java.util.List; +import java.security.Principal; + +/** + * ACLTemplateTest... + */ +public class ACLTemplateTest extends AbstractACLTemplateTest { + + private String testPath = "/rep:accessControl/users/test"; + + @Override + protected String getTestPath() { + return testPath; + } + + @Override + protected JackrabbitAccessControlList createEmptyTemplate(String testPath) + throws RepositoryException { + return new ACLTemplate(testPrincipal, testPath, (SessionImpl) superuser, superuser.getValueFactory()); + } + + @Override + protected Principal getSecondPrincipal() throws Exception { + return testPrincipal; + } + + public void testGetRestrictionNames() throws RepositoryException { + List names = Arrays.asList(createEmptyTemplate(getTestPath()).getRestrictionNames()); + + assertEquals(2, names.size()); + NameResolver resolver = (NameResolver) superuser; + assertTrue(names.contains(resolver.getJCRName(ACLTemplate.P_NODE_PATH))); + assertTrue(names.contains(resolver.getJCRName(ACLTemplate.P_GLOB))); + } + + public void testGetRestrictionTypes() throws RepositoryException { + JackrabbitAccessControlList acl = createEmptyTemplate(getTestPath()); + + NameResolver resolver = (NameResolver) superuser; + assertEquals(PropertyType.PATH, acl.getRestrictionType(resolver.getJCRName(ACLTemplate.P_NODE_PATH))); + assertEquals(PropertyType.STRING, acl.getRestrictionType(resolver.getJCRName(ACLTemplate.P_GLOB))); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EffectivePolicyTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EffectivePolicyTest.java new file mode 100644 index 00000000000..efaa3e98942 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EffectivePolicyTest.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractEffectivePolicyTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; + +/** + * EffectivePolicyTest... + */ +public class EffectivePolicyTest extends AbstractEffectivePolicyTest { + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable((SessionImpl) superuser, acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } + + public void testGetEffectivePoliciesByPrincipal() throws Exception { + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_READ_ACCESS_CONTROL, + }); + + JackrabbitAccessControlManager jacMgr = (JackrabbitAccessControlManager) acMgr; + + Principal everyone = ((SessionImpl) superuser).getPrincipalManager().getEveryone(); + AccessControlPolicy[] acp = jacMgr.getEffectivePolicies(Collections.singleton(everyone)); + assertNotNull(acp); + assertEquals(1, acp.length); + assertTrue(acp[0] instanceof JackrabbitAccessControlPolicy); + + JackrabbitAccessControlPolicy jacp = (JackrabbitAccessControlPolicy) acp[0]; + + assertFalse(jacMgr.hasPrivileges(jacp.getPath(), Collections.singleton(testUser.getPrincipal()), privileges)); + assertFalse(jacMgr.hasPrivileges(jacp.getPath(), Collections.singleton(everyone), privileges)); + + + acp = jacMgr.getApplicablePolicies(testUser.getPrincipal()); + if (acp.length == 0) { + acp = jacMgr.getPolicies(testUser.getPrincipal()); + } + + assertNotNull(acp); + assertEquals(1, acp.length); + assertTrue(acp[0] instanceof JackrabbitAccessControlList); + + // let testuser read the ACL defined for 'testUser' principal. + JackrabbitAccessControlList acl = (JackrabbitAccessControlList) acp[0]; + acl.addEntry(testUser.getPrincipal(), privileges, true, getRestrictions(superuser, acl.getPath())); + jacMgr.setPolicy(acl.getPath(), acl); + superuser.save(); + + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + // effective policies for testPrinicpal only on path -> must succeed. + ((JackrabbitAccessControlManager) testAcMgr).getEffectivePolicies(Collections.singleton(testUser.getPrincipal())); + + // effective policies for a combination of principals -> must fail + try { + ((JackrabbitAccessControlManager) testAcMgr).getEffectivePolicies(((SessionImpl) testSession).getSubject().getPrincipals()); + fail(); + } catch (AccessDeniedException e) { + // success + } + } + + public void testEffectivePoliciesByPath() throws RepositoryException, NotExecutableException { + /* + precondition: + testuser must have READ-only permission on test-node and below + */ + checkReadOnly(path); + + // give 'testUser' READ_AC privileges at 'path' + Privilege[] privileges = privilegesFromNames(new String[] { + Privilege.JCR_READ_ACCESS_CONTROL, + }); + + givePrivileges(path, privileges, getRestrictions(superuser, path)); + + Session testSession = getTestSession(); + AccessControlManager testAcMgr = getTestACManager(); + + assertTrue(testAcMgr.hasPrivileges(path, privileges)); + + // reading the policies stored at 'path' must succeed. + // however, principalbased-ac stores ac information in a separate tree. + // no policy must be present at 'path'. + AccessControlPolicy[] policies = testAcMgr.getPolicies(path); + assertNotNull(policies); + assertEquals(0, policies.length); + + // since read-ac access denied on the acl storing node itself obtaining + // the effective policy for 'path' must fail. + try { + testAcMgr.getEffectivePolicies(path); + fail(); + } catch (AccessDeniedException e) { + // success + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EntryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EntryTest.java new file mode 100644 index 00000000000..7bda0648903 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EntryTest.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractEntryTest; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.value.BooleanValue; +import org.apache.jackrabbit.value.StringValue; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * EntryTest... + */ +public class EntryTest extends AbstractEntryTest { + + private Map restrictions; + private ACLTemplate acl; + + private String nodePath; + private String glob; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (superuser instanceof NameResolver) { + NameResolver resolver = (NameResolver) superuser; + nodePath = resolver.getJCRName(ACLTemplate.P_NODE_PATH); + glob = resolver.getJCRName(ACLTemplate.P_GLOB); + } else { + throw new NotExecutableException(); + } + + restrictions = new HashMap(2); + restrictions.put(nodePath, superuser.getValueFactory().createValue("/a/b/c/d", PropertyType.PATH)); + restrictions.put(glob, superuser.getValueFactory().createValue("*")); + acl = new ACLTemplate(testPrincipal, testPath, (SessionImpl) superuser, superuser.getValueFactory()); + } + + @Override + protected JackrabbitAccessControlEntry createEntry(Principal principal, Privilege[] privileges, boolean isAllow) + throws RepositoryException { + return (JackrabbitAccessControlEntry) acl.createEntry(principal, privileges, isAllow, restrictions); + } + + @Override + protected JackrabbitAccessControlEntry createEntry(Principal principal, Privilege[] privileges, boolean isAllow, Map restrictions) + throws RepositoryException { + return (JackrabbitAccessControlEntry) acl.createEntry(principal, privileges, isAllow, restrictions); + } + + @Override + protected JackrabbitAccessControlEntry createEntryFromBase(JackrabbitAccessControlEntry base, Privilege[] privileges, boolean isAllow) throws RepositoryException, NotExecutableException { + Map restr = new HashMap(); + for (String name : base.getRestrictionNames()) { + restr.put(name, base.getRestriction(name)); + } + return (JackrabbitAccessControlEntry) acl.createEntry(base.getPrincipal(), privileges, isAllow, restr); + } + + @Override + protected Map getTestRestrictions() throws RepositoryException { + return restrictions; + } + + public void testNodePathMustNotBeNull() throws RepositoryException, NotExecutableException { + try { + Privilege[] privs = privilegesFromName(Privilege.JCR_ALL); + createEntry(testPrincipal, privs, true, Collections.emptyMap()); + fail("NodePath cannot not be null"); + } catch (AccessControlException e) { + // success + } + } + + public void testGetNodePath() throws RepositoryException, NotExecutableException { + Privilege[] privs = privilegesFromName(Privilege.JCR_ALL); + JackrabbitAccessControlEntry pe = createEntry(testPrincipal, privs, true); + + assertEquals(restrictions.get(nodePath), pe.getRestriction(nodePath)); + assertEquals(PropertyType.PATH, pe.getRestriction(nodePath).getType()); + } + + public void testGetGlob() throws RepositoryException, NotExecutableException { + Privilege[] privs = privilegesFromName(Privilege.JCR_ALL); + + JackrabbitAccessControlEntry pe = createEntry(testPrincipal, privs, true); + + assertEquals(restrictions.get(glob), pe.getRestriction(glob)); + assertEquals(PropertyType.STRING, pe.getRestriction(glob).getType()); + + Map restr = new HashMap(); + restr.put(nodePath, restrictions.get(nodePath)); + pe = createEntry(testPrincipal, privs, true, restr); + assertNull(pe.getRestriction(glob)); + + restr = new HashMap(); + restr.put(nodePath, restrictions.get(nodePath)); + restr.put(glob, new StringValue("")); + + pe = createEntry(testPrincipal, privs, true, restr); + assertEquals("", pe.getRestriction(glob).getString()); + + restr = new HashMap(); + restr.put(nodePath, restrictions.get(nodePath)); + restr.put(glob, new BooleanValue(true)); + assertEquals(PropertyType.STRING, pe.getRestriction(glob).getType()); + } + + public void testTypeConversion() throws RepositoryException, NotExecutableException { + // ACLTemplate impl tries to convert the property types if the don't + // match the required ones. + Privilege[] privs = privilegesFromName(Privilege.JCR_ALL); + + Map restr = new HashMap(); + restr.put(nodePath, new StringValue("/a/b/c/d")); + JackrabbitAccessControlEntry pe = createEntry(testPrincipal, privs, true, restr); + + assertEquals("/a/b/c/d", pe.getRestriction(nodePath).getString()); + assertEquals(PropertyType.PATH, pe.getRestriction(nodePath).getType()); + + restr = new HashMap(); + restr.put(nodePath, restrictions.get(nodePath)); + restr.put(glob, new BooleanValue(true)); + pe = createEntry(testPrincipal, privs, true, restr); + + assertEquals(true, pe.getRestriction(glob).getBoolean()); + assertEquals(PropertyType.STRING, pe.getRestriction(glob).getType()); + } + + public void testMatches() throws RepositoryException { + Privilege[] privs = new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_ALL)}; + ACLTemplate.Entry ace = (ACLTemplate.Entry) createEntry(testPrincipal, privs, true); + + String nPath = restrictions.get(nodePath).getString(); + List toMatch = new ArrayList(); + toMatch.add(nPath + "/any"); + toMatch.add(nPath + "/anyother"); + toMatch.add(nPath + "/f/g/h"); + toMatch.add(nPath); + for (String str : toMatch) { + assertTrue("Restrictions should match " + str, ace.matches(str)); + } + + List notToMatch = new ArrayList(); + notToMatch.add(null); + notToMatch.add(""); + notToMatch.add("/"); + notToMatch.add("/a/b/c/"); + for (String str : notToMatch) { + assertFalse("Restrictions shouldn't match " + str, ace.matches(str)); + } + } + + public void testRestrictions() throws RepositoryException { + // test if restrictions with expanded name are properly resolved + Map restrictions = new HashMap(); + restrictions.put(ACLTemplate.P_GLOB.toString(), superuser.getValueFactory().createValue("*/test")); + restrictions.put(ACLTemplate.P_NODE_PATH.toString(), superuser.getValueFactory().createValue("/a/b/c")); + + Privilege[] privs = new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_ALL)}; + ACLTemplate.Entry ace = (ACLTemplate.Entry) createEntry(testPrincipal, privs, true, restrictions); + + Value v = ace.getRestriction(ACLTemplate.P_GLOB.toString()); + Value v2 = ace.getRestriction(glob); + assertEquals(v, v2); + + v = ace.getRestriction(ACLTemplate.P_NODE_PATH.toString()); + v2 = ace.getRestriction(nodePath); + assertEquals(v, v2); + + Map toMatch = new HashMap(); + toMatch.put("/a/b/c", false); + toMatch.put("/a/b/ctest", false); + toMatch.put("/a/b/c/test", true); + toMatch.put("/a/b/c/something/test", true); + toMatch.put("/a/b/cde/test", true); + + for (String str : toMatch.keySet()) { + assertEquals("Path to match : " + str, toMatch.get(str).booleanValue(), ace.matches(str)); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EvaluationUtil.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EvaluationUtil.java new file mode 100644 index 00000000000..f8a1a01b286 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/EvaluationUtil.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; + +/** + * EvaluationTest... + */ +class EvaluationUtil { + + static boolean isExecutable(SessionImpl s, AccessControlManager acMgr) { + if (acMgr instanceof JackrabbitAccessControlManager) { + for (Principal princ : s.getSubject().getPrincipals()) { + try { + AccessControlPolicy[] policies = ((JackrabbitAccessControlManager) acMgr).getApplicablePolicies(princ); + for (AccessControlPolicy policy : policies) { + if (policy instanceof ACLTemplate) { + return true; + } + } + policies = ((JackrabbitAccessControlManager) acMgr).getPolicies(princ); + for (AccessControlPolicy policy : policies) { + if (policy instanceof ACLTemplate) { + return true; + } + } + } catch (RepositoryException e) { + // ignore + } + } + } + return false; + } + + static JackrabbitAccessControlList getPolicy(AccessControlManager acM, + String path, + Principal principal) + throws RepositoryException, AccessDeniedException, NotExecutableException { + if (acM instanceof JackrabbitAccessControlManager && path != null) { + // first try applicable policies + AccessControlPolicy[] policies = ((JackrabbitAccessControlManager) acM).getApplicablePolicies(principal); + for (AccessControlPolicy policy : policies) { + if (policy instanceof ACLTemplate) { + return (ACLTemplate) policy; + } + } + + // second existing policies + policies = ((JackrabbitAccessControlManager) acM).getPolicies(principal); + for (AccessControlPolicy policy : policies) { + if (policy instanceof ACLTemplate) { + return (ACLTemplate) policy; + } + } + } + throw new NotExecutableException(); + } + + static Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + if (s instanceof SessionImpl && path != null) { + Map restr = new HashMap(); + restr.put(((SessionImpl) s).getJCRName(ACLTemplate.P_NODE_PATH), s.getValueFactory().createValue(path, PropertyType.PATH)); + return restr; + } else { + throw new NotExecutableException(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/LockTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/LockTest.java new file mode 100644 index 00000000000..2b6b37d18a6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/LockTest.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractLockManagementTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import java.security.Principal; +import java.util.Map; + +/** + * LockTest... + */ +public class LockTest extends AbstractLockManagementTest { + + protected boolean isExecutable() { + return EvaluationUtil.isExecutable((SessionImpl) superuser, acMgr); + } + + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws + RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/NodeTypeTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/NodeTypeTest.java new file mode 100644 index 00000000000..08c7b12ded3 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/NodeTypeTest.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractNodeTypeManagementTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import java.security.Principal; +import java.util.Map; + +/** + * NodeTypeTest... + */ +public class NodeTypeTest extends AbstractNodeTypeManagementTest { + + protected boolean isExecutable() { + return EvaluationUtil.isExecutable((SessionImpl) superuser, acMgr); + } + + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws + RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/RepositoryOperationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/RepositoryOperationTest.java new file mode 100644 index 00000000000..5f040e7e505 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/RepositoryOperationTest.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractRepositoryOperationTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import java.security.Principal; +import java.util.Map; + +/** + * RepositoryOperationTest... + */ +public class RepositoryOperationTest extends AbstractRepositoryOperationTest { + + protected boolean isExecutable() { + return EvaluationUtil.isExecutable((SessionImpl) superuser, acMgr); + } + + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws + RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/TestAll.java new file mode 100644 index 00000000000..0d4ad49ad55 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/TestAll.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.core.security.authorization.principalbased; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("security.authorization.principalbased tests"); + + suite.addTestSuite(ACLTemplateTest.class); + suite.addTestSuite(EntryTest.class); + + suite.addTestSuite(WriteTest.class); + suite.addTestSuite(LockTest.class); + suite.addTestSuite(VersionTest.class); + suite.addTestSuite(NodeTypeTest.class); + suite.addTestSuite(EffectivePolicyTest.class); + suite.addTestSuite(RepositoryOperationTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/VersionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/VersionTest.java new file mode 100644 index 00000000000..556ea0b521e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/VersionTest.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractVersionManagementTest; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Map; + +/** + * VersionTest... + */ +public class VersionTest extends AbstractVersionManagementTest { + + private static Logger log = LoggerFactory.getLogger(VersionTest.class); + + private static String VERSION_STORAGE_PATH = "/jcr:system/jcr:versionStorage"; + + protected boolean isExecutable() { + return EvaluationUtil.isExecutable((SessionImpl) superuser, acMgr); + } + + protected JackrabbitAccessControlList getPolicy(AccessControlManager acMgr, String path, Principal princ) throws + RepositoryException, NotExecutableException { + return EvaluationUtil.getPolicy(acMgr, path, princ); + } + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } + + public void testReadVersionInfo() throws RepositoryException, NotExecutableException { + Node n = createVersionableNode(testRootNode); + modifyPrivileges(VERSION_STORAGE_PATH, Privilege.JCR_READ, false); + + Node n2 = (Node) getTestSession().getItem(n.getPath()); + try { + n2.getVersionHistory(); + fail(); + } catch (AccessDeniedException e) { + // success + } catch (ItemNotFoundException e) { + // success as well + } + try { + n2.getBaseVersion(); + fail(); + } catch (AccessDeniedException e) { + // success + } catch (ItemNotFoundException e) { + // success as well + } + } + + public void testReadVersionInfo2() throws RepositoryException, NotExecutableException { + Node n = createVersionableNode(testRootNode); + modifyPrivileges(VERSION_STORAGE_PATH, Privilege.JCR_READ, true); + + Node n2 = (Node) getTestSession().getItem(n.getPath()); + n2.getVersionHistory(); + n2.getBaseVersion(); + } + + public void testReadVersionInfo3() throws RepositoryException, NotExecutableException { + Node trn = getTestNode(); + modifyPrivileges(trn.getPath(), PrivilegeRegistry.REP_WRITE, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_NODE_TYPE_MANAGEMENT, true); + modifyPrivileges(trn.getPath(), Privilege.JCR_VERSION_MANAGEMENT, true); + modifyPrivileges(VERSION_STORAGE_PATH, Privilege.JCR_READ, false); + + Node n = createVersionableNode(trn); + assertTrue(n.isNodeType(mixVersionable)); + assertFalse(n.isModified()); + + try { + n.getVersionHistory(); + n.getBaseVersion(); + fail("No READ permission in the version storage"); + } catch (AccessDeniedException e) { + // success + log.debug(e.getMessage()); + } catch (ItemNotFoundException e) { + // success + log.debug(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/WriteTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/WriteTest.java new file mode 100644 index 00000000000..736c8601eb6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/authorization/principalbased/WriteTest.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.core.security.authorization.principalbased; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.authorization.AbstractWriteTest; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.core.security.TestPrincipal; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import java.security.Principal; +import java.util.Map; + +/** + * EvaluationTest... + */ +public class WriteTest extends AbstractWriteTest { + + @Override + protected boolean isExecutable() { + return EvaluationUtil.isExecutable((SessionImpl) superuser, acMgr); + } + + @Override + protected JackrabbitAccessControlList getPolicy(AccessControlManager acM, String path, Principal principal) throws RepositoryException, AccessDeniedException, NotExecutableException { + return EvaluationUtil.getPolicy(acM, path, principal); + } + + @Override + protected Map getRestrictions(Session s, String path) throws RepositoryException, NotExecutableException { + return EvaluationUtil.getRestrictions(s, path); + } + + public void testAutocreatedProperties() throws RepositoryException, NotExecutableException { + givePrivileges(path, testUser.getPrincipal(), privilegesFromName(PrivilegeRegistry.REP_WRITE), getRestrictions(superuser, path)); + + // test user is not allowed to READ the protected property jcr:created. + Map restr = getRestrictions(superuser, path); + restr.put(((SessionImpl) superuser).getJCRName(ACLTemplate.P_GLOB), superuser.getValueFactory().createValue("/afolder/jcr:created")); + withdrawPrivileges(path, testUser.getPrincipal(), privilegesFromName(Privilege.JCR_READ), restr); + + // still: adding a nt:folder node should be possible + Node n = getTestSession().getNode(path); + Node folder = n.addNode("afolder", "nt:folder"); + + assertFalse(folder.hasProperty("jcr:created")); + } + + public void testEditor() throws NotExecutableException, RepositoryException { + UserManager uMgr = getUserManager(superuser); + User u = null; + try { + u = uMgr.createUser("t", "t"); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + + Principal p = u.getPrincipal(); + + JackrabbitAccessControlManager acMgr = (JackrabbitAccessControlManager) getAccessControlManager(superuser); + JackrabbitAccessControlPolicy[] acls = acMgr.getApplicablePolicies(p); + + assertEquals(1, acls.length); + assertTrue(acls[0] instanceof ACLTemplate); + + // access again + acls = acMgr.getApplicablePolicies(p); + + assertEquals(1, acls.length); + assertEquals(1, acMgr.getApplicablePolicies(acls[0].getPath()).getSize()); + + assertEquals(0, acMgr.getPolicies(p).length); + assertEquals(0, acMgr.getPolicies(acls[0].getPath()).length); + + acMgr.setPolicy(acls[0].getPath(), acls[0]); + + assertEquals(0, acMgr.getApplicablePolicies(p).length); + assertEquals(1, acMgr.getPolicies(p).length); + assertEquals(1, acMgr.getPolicies(acls[0].getPath()).length); + } finally { + superuser.refresh(false); + if (u != null) { + u.remove(); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + } + } + } + + public void testEditor2() throws NotExecutableException, RepositoryException { + UserManager uMgr = getUserManager(superuser); + User u = null; + User u2 = null; + try { + u = uMgr.createUser("t", "t"); + u2 = uMgr.createUser("tt", "tt", new TestPrincipal("tt"), "t/tt"); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + + Principal p = u.getPrincipal(); + Principal p2 = u2.getPrincipal(); + + if (p instanceof ItemBasedPrincipal && p2 instanceof ItemBasedPrincipal && + Text.isDescendant(((ItemBasedPrincipal) p).getPath(), ((ItemBasedPrincipal) p2).getPath())) { + + JackrabbitAccessControlManager acMgr = (JackrabbitAccessControlManager) getAccessControlManager(superuser); + + JackrabbitAccessControlPolicy[] acls = acMgr.getApplicablePolicies(p2); + acMgr.setPolicy(acls[0].getPath(), acls[0]); + + acls = acMgr.getApplicablePolicies(p); + String path = acls[0].getPath(); + + Node n = superuser.getNode(path); + assertEquals("rep:PrincipalAccessControl", n.getPrimaryNodeType().getName()); + } else { + throw new NotExecutableException(); + } + } finally { + superuser.refresh(false); + if (u2 != null) u2.remove(); + if (u != null) u.remove(); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + } + + } + + /** + * Test for bug JCR-2621 + * + * @throws Exception + */ + public void testPrincipalNameDiffersFromID() throws Exception { + UserManager uMgr = getUserManager(superuser); + User u = null; + try { + // create a user with different uid vs principal name + u = uMgr.createUser("t@foo.org", "t", new PrincipalImpl("t"), null); + if (!uMgr.isAutoSave()) { + superuser.save(); + } + + Principal principal = u.getPrincipal(); + JackrabbitAccessControlList acl = getPolicy(acMgr, path, principal); + acl.addEntry(principal, privilegesFromName(Privilege.JCR_READ), true, getRestrictions(superuser, path)); + acMgr.setPolicy(acl.getPath(), acl); + + AccessControlPolicy[] plcs = acMgr.getPolicies(acl.getPath()); + assertEquals(1, plcs.length); + acl = (JackrabbitAccessControlList) plcs[0]; + acl.addEntry(principal, privilegesFromName(Privilege.JCR_WRITE), true, getRestrictions(superuser, path)); + acMgr.setPolicy(acl.getPath(), acl); + + } finally { + if (u != null) { + u.remove(); + } + } + } + + /** + * Test for bug JCR-2621 + * + * @throws Exception + */ + public void testNotItemBasedPrincipal() throws Exception { + try { + Principal everyone = ((JackrabbitSession) superuser).getPrincipalManager().getEveryone(); + JackrabbitAccessControlList acl = getPolicy(acMgr, path, everyone); + acl.addEntry(everyone, privilegesFromName(Privilege.JCR_READ), true, getRestrictions(superuser, path)); + acMgr.setPolicy(acl.getPath(), acl); + + AccessControlPolicy[] plcs = acMgr.getPolicies(acl.getPath()); + assertEquals(1, plcs.length); + acl = (JackrabbitAccessControlList) plcs[0]; + acl.addEntry(everyone, privilegesFromName(Privilege.JCR_WRITE), true, getRestrictions(superuser, path)); + acMgr.setPolicy(acl.getPath(), acl); + } finally { + // revert all kind of transient modifications + superuser.refresh(false); + } + } + + public void testInvalidPrincipal() throws Exception { + PrincipalManager pMgr = ((JackrabbitSession) superuser).getPrincipalManager(); + String unknown = "unknown"; + while (pMgr.hasPrincipal(unknown)) { + unknown = unknown + "_"; + } + Principal principal = new PrincipalImpl(unknown); + + if (acMgr instanceof JackrabbitAccessControlManager) { + // first try applicable policies + try { + AccessControlPolicy[] policies = ((JackrabbitAccessControlManager) acMgr).getApplicablePolicies(principal); + assertNotNull(policies); + assertEquals(0, policies.length); + } catch (AccessControlException e) { + // success + } + + // second existing policies + try { + AccessControlPolicy[] policies = ((JackrabbitAccessControlManager) acMgr).getPolicies(principal); + assertNotNull(policies); + assertEquals(0, policies.length); + } catch (AccessControlException e) { + // success + } + } else { + throw new NotExecutableException(); + } + } + + // TODO: add specific tests with other restrictions +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalProviderTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalProviderTest.java new file mode 100644 index 00000000000..f507a051d74 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/AbstractPrincipalProviderTest.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.core.security.principal; + +import junit.framework.TestCase; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.security.Principal; +import java.util.Properties; + +/** + * AbstractPrincipalProviderTest... + */ +public class AbstractPrincipalProviderTest extends TestCase { + + public void testNegativeCacheEntries() throws RepositoryException, NotExecutableException { + String unknownName = "UnknownPrincipal"; + + PrincipalProvider caching = new DummyProvider(); + Properties options = new Properties(); + options.setProperty(DefaultPrincipalProvider.NEGATIVE_ENTRY_KEY, "true"); + caching.init(options); + + // accessing from wrapper must not throw! as negative entry is expected + // to be in the cache (default behavior of the DefaultPrincipalProvider) + assertNull(caching.getPrincipal(unknownName)); + assertNull(caching.getPrincipal(unknownName)); + + PrincipalProvider throwing = new DummyProvider(); + options = new Properties(); + options.setProperty(DefaultPrincipalProvider.NEGATIVE_ENTRY_KEY, "false"); + throwing.init(options); + + // however: the noNegativeCacheProvider configured NOT to cache null-results + // is expected to call 'providePrincipal' for each call to 'getPrincipal' + // with a principalName that doesn't exist. + assertNull(throwing.getPrincipal(unknownName)); + try { + throwing.getPrincipal(unknownName); + fail("exception expected"); + } catch (UnsupportedOperationException e) { + // success + } + } + + private class DummyProvider extends AbstractPrincipalProvider { + + private boolean first = true; + @Override + protected Principal providePrincipal(String principalName) { + if (first) { + first = false; + return null; + } else { + throw new UnsupportedOperationException(); + } + } + public PrincipalIterator findPrincipals(String simpleFilter) { + throw new UnsupportedOperationException(); + } + + public PrincipalIterator findPrincipals(String simpleFilter, int searchType) { + throw new UnsupportedOperationException(); + } + + public PrincipalIterator getPrincipals(int searchType) { + throw new UnsupportedOperationException(); + } + + public PrincipalIterator getGroupMembership(Principal principal) { + throw new UnsupportedOperationException(); + } + + public boolean canReadPrincipal(Session session, Principal principalToRead) { + return true; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/EveryonePrincipalTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/EveryonePrincipalTest.java new file mode 100644 index 00000000000..8df5750fb30 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/EveryonePrincipalTest.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.core.security.principal; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import java.security.Principal; + +/** + * EveryonePrincipalTest... + */ +public class EveryonePrincipalTest extends AbstractJCRTest { + + public void testEveryonePrincipal() { + Principal everyone = EveryonePrincipal.getInstance(); + + assertEquals(EveryonePrincipal.NAME, everyone.getName()); + assertEquals(everyone, EveryonePrincipal.getInstance()); + } + + public void testEveryonePrincipal2() { + Principal everyone = EveryonePrincipal.getInstance(); + + Principal someotherEveryone = new Principal() { + public String getName() { + return EveryonePrincipal.NAME; + } + }; + + assertFalse(everyone.equals(someotherEveryone)); + } + + public void testEveryonePrincipal3() { + Principal everyone = EveryonePrincipal.getInstance(); + + Principal someotherEveryone = new 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(); + } + }; + + assertEquals(someotherEveryone, everyone); + assertEquals(everyone, someotherEveryone); + } + + public void testEveryonePrincipal4() throws NotExecutableException, RepositoryException { + Principal everyone = EveryonePrincipal.getInstance(); + + Group everyoneGroup = null; + try { + everyoneGroup = getUserManager(superuser).createGroup(EveryonePrincipal.NAME); + superuser.save(); + + assertEquals(everyoneGroup.getPrincipal(), everyone); + assertEquals(everyone, everyoneGroup.getPrincipal()); + + } finally { + if (everyoneGroup != null) { + everyoneGroup.remove(); + superuser.save(); + } + } + } + + private 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()); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/PrincipalManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/PrincipalManagerTest.java new file mode 100644 index 00000000000..9b66fc0ba07 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/PrincipalManagerTest.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.core.security.principal; + +import java.security.Principal; +import java.util.Properties; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; +import org.apache.jackrabbit.api.security.principal.JackrabbitPrincipal; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.mockito.Mockito; + + +/** + * PrincipalManagerTest... + */ +public class PrincipalManagerTest extends AbstractJCRTest { + + private static final String TESTGROUP_NAME = "org.apache.jackrabbit.core.security.principal.PrincipalManagerTest.testgroup"; + private static final GroupPrincipal TESTGROUP = Mockito.mock(GroupPrincipal.class); + + private static class CustomPrincipalProvider extends AbstractPrincipalProvider { + + protected Principal providePrincipal(String principalName) { + return TESTGROUP_NAME.equals(principalName) ? TESTGROUP : null; + } + + public PrincipalIterator findPrincipals(String simpleFilter) { + throw new UnsupportedOperationException(); + } + + public PrincipalIterator findPrincipals(String simpleFilter, int searchType) { + throw new UnsupportedOperationException(); + } + + public PrincipalIterator getPrincipals(int searchType) { + throw new UnsupportedOperationException(); + } + + public PrincipalIterator getGroupMembership(Principal principal) { + throw new UnsupportedOperationException(); + } + + public boolean canReadPrincipal(Session session, Principal principalToRead) { + return true; + } + } + + /** + * Test if a group which is not item based will be wrapped by a JackrabbitPrincipal implementation. + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testJackrabbitPrincipal() throws NotExecutableException, RepositoryException { + + final PrincipalProvider testProvider = new CustomPrincipalProvider(); + testProvider.init(new Properties()); + PrincipalManagerImpl principalManager = new PrincipalManagerImpl(superuser, new PrincipalProvider[] { testProvider }); + Principal principalFromManager = principalManager.getPrincipal(TESTGROUP_NAME); + assertTrue(principalFromManager instanceof JackrabbitPrincipal); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/TestAll.java new file mode 100644 index 00000000000..8420fcbc9b4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/principal/TestAll.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.core.security.principal; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("core.security.principal tests"); + + suite.addTestSuite(AbstractPrincipalProviderTest.class); + suite.addTestSuite(EveryonePrincipalTest.class); + suite.addTestSuite(PrincipalManagerTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/simple/SimpleSecurityManagerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/simple/SimpleSecurityManagerTest.java new file mode 100644 index 00000000000..53d730f1068 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/simple/SimpleSecurityManagerTest.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.core.security.simple; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import junit.framework.TestCase; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.junit.Test; + +public class SimpleSecurityManagerTest extends TestCase { + + private Repository repository; + + @Override + public void setUp() throws RepositoryException { + String file = "src/test/resources/org/apache/jackrabbit/core/security/simple/simple_repository.xml"; + RepositoryConfig config = RepositoryConfig.create(file, "target/simple_repository"); + repository = RepositoryImpl.create(config); + } + + /** + * @see JCR-3697 + */ + @Test + public void testRemove() throws RepositoryException { + Session s = repository.login(new SimpleCredentials("admin", "admin".toCharArray())); + Node n = s.getRootNode().addNode(("a")); + s.save(); + + n.remove(); + s.save(); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/simple/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/simple/TestAll.java new file mode 100644 index 00000000000..1c3fcd2edbc --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/simple/TestAll.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.core.security.simple; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("core.security.simple tests"); + + suite.addTestSuite(SimpleSecurityManagerTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AdministratorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AdministratorTest.java new file mode 100644 index 00000000000..b26ac51731a --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AdministratorTest.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.util.Properties; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AdministratorTest... + */ +public class AdministratorTest extends AbstractUserTest { + + private String adminId; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!(userMgr instanceof UserManagerImpl)) { + throw new NotExecutableException(); + } + adminId = superuser.getUserID(); + if (!((UserManagerImpl) userMgr).isAdminId(adminId)) { + throw new NotExecutableException(); + } + } + + public void testGetPrincipal() throws RepositoryException { + Authorizable admin = userMgr.getAuthorizable(adminId); + assertNotNull(admin); + assertFalse(admin.isGroup()); + assertTrue(admin.getPrincipal() instanceof AdminPrincipal); + } + + public void testRemoveSelf() throws RepositoryException, NotExecutableException { + Authorizable admin = userMgr.getAuthorizable(adminId); + if (admin == null) { + throw new NotExecutableException(); + } + try { + admin.remove(); + fail("The Administrator should not be allowed to remove the own authorizable."); + } catch (RepositoryException e) { + // success + } + } + + /** + * Test if the administrator is recreated upon login if the corresponding + * node gets removed. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testRemoveAdminNode() throws RepositoryException, NotExecutableException { + Authorizable admin = userMgr.getAuthorizable(adminId); + + if (admin == null || !(admin instanceof AuthorizableImpl)) { + throw new NotExecutableException(); + } + + // access the node corresponding to the admin user and remove it + NodeImpl adminNode = ((AuthorizableImpl) admin).getNode(); + Session s = adminNode.getSession(); + adminNode.remove(); + // use session obtained from the node as usermgr may point to a dedicated + // system workspace different from the superusers workspace. + s.save(); + + // after removing the node the admin user doesn't exist any more + assertNull(userMgr.getAuthorizable(adminId)); + + // login must succeed as system user mgr recreates the admin user + Session s2 = getHelper().getSuperuserSession(); + try { + admin = userMgr.getAuthorizable(adminId); + assertNotNull(admin); + assertNotNull(getUserManager(s2).getAuthorizable(adminId)); + } finally { + s2.logout(); + } + } + + /** + * Test for collisions that would prevent from recreate the admin user. + * - an intermediate rep:AuthorizableFolder node with the same name + */ + public void testAdminNodeCollidingWithAuthorizableFolder() throws RepositoryException, NotExecutableException { + Authorizable admin = userMgr.getAuthorizable(adminId); + + if (admin == null || !(admin instanceof AuthorizableImpl)) { + throw new NotExecutableException(); + } + + // access the node corresponding to the admin user and remove it + NodeImpl adminNode = ((AuthorizableImpl) admin).getNode(); + String adminPath = adminNode.getPath(); + String adminNodeName = adminNode.getName(); + Node parentNode = adminNode.getParent(); + + Session s = adminNode.getSession(); + adminNode.remove(); + // use session obtained from the node as usermgr may point to a dedicated + // system workspace different from the superusers workspace. + s.save(); + + Session s2 = null; + String collidingPath = null; + try { + // now create a colliding node: + Node n = parentNode.addNode(adminNodeName, "rep:AuthorizableFolder"); + collidingPath = n.getPath(); + s.save(); + + // force recreation of admin user. + s2 = getHelper().getSuperuserSession(); + + admin = userMgr.getAuthorizable(adminId); + assertNotNull(admin); + assertEquals(adminNodeName, ((AuthorizableImpl) admin).getNode().getName()); + assertFalse(adminPath.equals(((AuthorizableImpl) admin).getNode().getPath())); + + } finally { + if (s2 != null) { + s2.logout(); + } + // remove the extra folder and the admin user (created underneath) again. + if (collidingPath != null) { + s.getNode(collidingPath).remove(); + s.save(); + } + } + } + + /** + * Test for collisions that would prevent from recreate the admin user. + * - a colliding node somewhere else with the same jcr:uuid. + * + * Test if creation of the administrator user forces the removal of some + * other node in the repository that by change happens to have the same + * jcr:uuid and thus inhibits the creation of the admininstrator user. + */ + public void testAdminNodeCollidingWithRandomNode() throws RepositoryException, NotExecutableException { + Authorizable admin = userMgr.getAuthorizable(adminId); + + if (admin == null || !(admin instanceof AuthorizableImpl)) { + throw new NotExecutableException(); + } + + // access the node corresponding to the admin user and remove it + NodeImpl adminNode = ((AuthorizableImpl) admin).getNode(); + NodeId nid = adminNode.getNodeId(); + + Session s = adminNode.getSession(); + adminNode.remove(); + // use session obtained from the node as usermgr may point to a dedicated + // system workspace different from the superusers workspace. + s.save(); + + Session s2 = null; + String collidingPath = null; + try { + // create a colliding node outside of the user tree + NameResolver nr = (SessionImpl) s; + // NOTE: testRootNode will not be present if users are stored in a distinct wsp. + // therefore use root node as start... + NodeImpl tr = (NodeImpl) s.getRootNode(); + Node n = tr.addNode(nr.getQName("tmpNode"), nr.getQName(testNodeType), nid); + collidingPath = n.getPath(); + s.save(); + + // force recreation of admin user. + s2 = getHelper().getSuperuserSession(); + + admin = userMgr.getAuthorizable(adminId); + assertNotNull(admin); + // the colliding node must have been removed. + assertFalse(s2.nodeExists(collidingPath)); + + } finally { + if (s2 != null) { + s2.logout(); + } + if (collidingPath != null && s.nodeExists(collidingPath)) { + s.getNode(collidingPath).remove(); + s.save(); + } + } + } + + /** + * Reconfiguration of the user-root-path will result in node collision + * upon initialization of the built-in repository users. Test if the + * UserManagerImpl in this case removes the colliding admin-user node. + */ + public void testChangeUserRootPath() throws RepositoryException, NotExecutableException { + Authorizable admin = userMgr.getAuthorizable(adminId); + + if (admin == null || !(admin instanceof AuthorizableImpl)) { + throw new NotExecutableException(); + } + + // access the node corresponding to the admin user and remove it + NodeImpl adminNode = ((AuthorizableImpl) admin).getNode(); + + Session s = adminNode.getSession(); + adminNode.remove(); + // use session obtained from the node as usermgr may point to a dedicated + // system workspace different from the superusers workspace. + s.save(); + + Session s2 = null; + String collidingPath = null; + try { + // create a colliding user node outside of the user tree + Properties props = new Properties(); + props.setProperty("usersPath", "/testPath"); + UserManager um = new UserManagerImpl((SessionImpl) s, adminId, props); + User collidingUser = um.createUser(adminId, adminId); + collidingPath = ((AuthorizableImpl) collidingUser).getNode().getPath(); + s.save(); + + // force recreation of admin user. + s2 = getHelper().getSuperuserSession(); + + admin = userMgr.getAuthorizable(adminId); + assertNotNull(admin); + // the colliding node must have been removed. + assertFalse(s2.nodeExists(collidingPath)); + + } finally { + if (s2 != null) { + s2.logout(); + } + if (collidingPath != null && s.nodeExists(collidingPath)) { + s.getNode(collidingPath).remove(); + s.save(); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java new file mode 100644 index 00000000000..f61095fb8d1 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableActionTest.java @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.core.security.user.action.AbstractAuthorizableAction; +import org.apache.jackrabbit.core.security.user.action.AccessControlAction; +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; +import org.apache.jackrabbit.core.security.user.action.ClearMembershipAction; +import org.apache.jackrabbit.core.security.user.action.PasswordValidationAction; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import java.util.ArrayList; +import java.util.List; + +/** + * AuthorizableActionTest... + */ +public class AuthorizableActionTest extends AbstractUserTest { + + private UserManagerImpl impl; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + impl = (UserManagerImpl) userMgr; + } + + @Override + protected void tearDown() throws Exception { + // reset the actions + setActions(null); + + super.tearDown(); + } + + private void setActions(AuthorizableAction action) { + AuthorizableAction[] actions = (action == null) ? + new AuthorizableAction[0] : + new AuthorizableAction[] {action}; + impl.setAuthorizableActions(actions); + } + + public void testAccessControlAction() throws Exception { + AccessControlAction action = new AccessControlAction(); + action.setUserPrivilegeNames("jcr:all"); + action.setGroupPrivilegeNames("jcr:read"); + + User u = null; + Group gr = null; + try { + setActions(action); + + String uid = getTestPrincipal().getName(); + u = impl.createUser(uid, buildPassword(uid)); + save(superuser); + assertAcAction(u, impl); + + String grId = getTestPrincipal().getName(); + gr = impl.createGroup(grId); + save(superuser); + assertAcAction(gr, impl); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(e.getMessage()); + } finally { + if (u != null) { + u.remove(); + } + if (gr != null) { + gr.remove(); + } + save(superuser); + } + } + + private static void assertAcAction(Authorizable a, UserManagerImpl umgr) throws RepositoryException, NotExecutableException { + Session s = umgr.getSession(); + AccessControlManager acMgr = s.getAccessControlManager(); + boolean hasACL = false; + AccessControlPolicyIterator it = acMgr.getApplicablePolicies("/"); + while (it.hasNext()) { + if (it.nextAccessControlPolicy() instanceof AccessControlList) { + hasACL = true; + break; + } + } + + if (!hasACL) { + for (AccessControlPolicy p : acMgr.getPolicies("/")) { + if (p instanceof AccessControlList) { + hasACL = true; + break; + } + } + } + + if (!hasACL) { + throw new NotExecutableException("No ACLs in workspace containing users."); + } + + String path = a.getPath(); + assertEquals(1, acMgr.getPolicies(path).length); + assertTrue(acMgr.getPolicies(path)[0] instanceof AccessControlList); + } + + public void testClearMembershipAction() throws Exception { + User u = null; + Group gr = null; + try { + setActions(new ClearMembershipAction()); + + String uid = getTestPrincipal().getName(); + u = impl.createUser(uid, buildPassword(uid)); + + String grId = getTestPrincipal().getName(); + gr = impl.createGroup(grId); + gr.addMember(u); + + save(superuser); + + assertTrue(gr.isMember(u)); + + u.remove(); + u = null; + + assertFalse(gr.isMember(u)); + } finally { + if (u != null) { + u.remove(); + } + if (gr != null) { + gr.remove(); + } + save(superuser); + } + } + + public void testPasswordAction() throws Exception { + User u = null; + + try { + TestAction action = new TestAction(); + setActions(action); + + String uid = getTestPrincipal().getName(); + u = impl.createUser(uid, buildPassword(uid)); + + u.changePassword("pw1"); + assertEquals(1, action.called); + + u.changePassword("pw2", "pw1"); + assertEquals(2, action.called); + } finally { + if (u != null) { + u.remove(); + } + save(superuser); + } + } + + public void testPasswordValidationAction() throws Exception { + User u = null; + + try { + String uid = getTestPrincipal().getName(); + u = impl.createUser(uid, buildPassword(uid)); + + PasswordValidationAction pwAction = new PasswordValidationAction(); + pwAction.setConstraint("^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z]).*"); + setActions(pwAction); + + List invalid = new ArrayList(); + invalid.add("pw1"); + invalid.add("only6C"); + invalid.add("12345678"); + invalid.add("WITHOUTLOWERCASE"); + invalid.add("withoutuppercase"); + + for (String pw : invalid) { + try { + u.changePassword(pw); + 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) { + u.changePassword(pw); + } + + } finally { + if (u != null) { + u.remove(); + } + save(superuser); + } + } + + public void testPasswordValidationActionIgnoresHashedPwStringOnCreate() throws Exception { + User u = null; + + try { + PasswordValidationAction pwAction = new PasswordValidationAction(); + pwAction.setConstraint("^.*(?=.{8,})(?=.*[a-z])(?=.*[A-Z]).*"); + setActions(pwAction); + + String uid = getTestPrincipal().getName(); + String hashed = PasswordUtility.buildPasswordHash("DWkej32H"); + u = impl.createUser(uid, hashed); + + } finally { + if (u != null) { + u.remove(); + } + save(superuser); + } + } + + public void testPasswordValidationActionOnChange() throws Exception { + User u = null; + + try { + String uid = getTestPrincipal().getName(); + u = impl.createUser(uid, buildPassword(uid)); + + PasswordValidationAction pwAction = new PasswordValidationAction(); + pwAction.setConstraint("abc"); + setActions(pwAction); + + String hashed = PasswordUtility.buildPasswordHash("abc"); + u.changePassword(hashed); + + fail("Password change must always enforce password validation."); + + } catch (ConstraintViolationException e) { + // success + } finally { + if (u != null) { + u.remove(); + } + save(superuser); + } + } + + //-------------------------------------------------------------------------- + private class TestAction extends AbstractAuthorizableAction { + + private int called = 0; + + @Override + public void onPasswordChange(User user, String newPassword, Session session) throws RepositoryException { + called++; + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableImplTest.java new file mode 100644 index 00000000000..91b345a2391 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/AuthorizableImplTest.java @@ -0,0 +1,476 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.core.NodeImpl; +import org.apache.jackrabbit.core.PropertyImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.value.StringValue; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RangeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.nodetype.ConstraintViolationException; +import java.util.ArrayList; +import java.util.List; +import java.util.Iterator; +import java.util.HashSet; +import java.util.Set; +import java.security.Principal; +import java.util.UUID; + +/** + * AuthorizableImplTest... + */ +public class AuthorizableImplTest extends AbstractUserTest { + + private List protectedUserProps = new ArrayList(); + private List protectedGroupProps = new ArrayList(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (superuser instanceof SessionImpl) { + NameResolver resolver = (SessionImpl) superuser; + protectedUserProps.add(resolver.getJCRName(UserConstants.P_PASSWORD)); + protectedUserProps.add(resolver.getJCRName(UserConstants.P_IMPERSONATORS)); + protectedUserProps.add(resolver.getJCRName(UserConstants.P_PRINCIPAL_NAME)); + protectedUserProps.add(resolver.getJCRName(UserConstants.P_DISABLED)); + + protectedGroupProps.add(resolver.getJCRName(UserConstants.P_MEMBERS)); + protectedGroupProps.add(resolver.getJCRName(UserConstants.P_PRINCIPAL_NAME)); + } else { + throw new NotExecutableException(); + } + } + + private static void checkProtected(Property prop) throws RepositoryException { + assertTrue(prop.getDefinition().isProtected()); + } + + public void testRemoveAdmin() { + String adminID = superuser.getUserID(); + try { + Authorizable admin = userMgr.getAuthorizable(adminID); + admin.remove(); + fail("The admin user cannot be removed."); + } catch (RepositoryException e) { + // OK superuser cannot be removed. not even by the superuser itself. + } + } + + public void testSetSpecialProperties() throws NotExecutableException, RepositoryException { + Value v = superuser.getValueFactory().createValue("any_value"); + + User u = getTestUser(superuser); + for (String pName : protectedUserProps) { + try { + u.setProperty(pName, v); + save(superuser); + fail("changing the '" + pName + "' property on a User should fail."); + } catch (RepositoryException e) { + // success + } + } + + Group g = getTestGroup(superuser); + for (String pName : protectedGroupProps) { + try { + g.setProperty(pName, v); + save(superuser); + fail("changing the '" + pName + "' property on a Group should fail."); + } catch (RepositoryException e) { + // success + } + } + } + + public void testRemoveSpecialProperties() throws NotExecutableException, RepositoryException { + User u = getTestUser(superuser); + for (String pName : protectedUserProps) { + try { + u.removeProperty(pName); + save(superuser); + fail("removing the '" + pName + "' property on a User should fail."); + } catch (RepositoryException e) { + // success + } + } + Group g = getTestGroup(superuser); + for (String pName : protectedGroupProps) { + try { + g.removeProperty(pName); + save(superuser); + fail("removing the '" + pName + "' property on a Group should fail."); + } catch (RepositoryException e) { + // success + } + } + } + + public void testProtectedUserProperties() throws NotExecutableException, RepositoryException { + UserImpl user = (UserImpl) getTestUser(superuser); + NodeImpl n = user.getNode(); + + checkProtected(n.getProperty(UserConstants.P_PASSWORD)); + if (n.hasProperty(UserConstants.P_PRINCIPAL_NAME)) { + checkProtected(n.getProperty(UserConstants.P_PRINCIPAL_NAME)); + } + if (n.hasProperty(UserConstants.P_IMPERSONATORS)) { + checkProtected(n.getProperty(UserConstants.P_IMPERSONATORS)); + } + } + + public void testProtectedGroupProperties() throws NotExecutableException, RepositoryException { + GroupImpl gr = (GroupImpl) getTestGroup(superuser); + NodeImpl n = gr.getNode(); + + if (n.hasProperty(UserConstants.P_PRINCIPAL_NAME)) { + checkProtected(n.getProperty(UserConstants.P_PRINCIPAL_NAME)); + } + if (n.hasProperty(UserConstants.P_MEMBERS)) { + checkProtected(n.getProperty(UserConstants.P_MEMBERS)); + } + } + + public void testMembersPropertyType() throws NotExecutableException, RepositoryException { + GroupImpl gr = (GroupImpl) getTestGroup(superuser); + NodeImpl n = gr.getNode(); + + if (!n.hasProperty(UserConstants.P_MEMBERS)) { + gr.addMember(getTestUser(superuser)); + } + + Property p = n.getProperty(UserConstants.P_MEMBERS); + for (Value v : p.getValues()) { + assertEquals(PropertyType.WEAKREFERENCE, v.getType()); + } + } + + public void testMemberOfRangeIterator() throws NotExecutableException, RepositoryException { + Authorizable auth = null; + Group group = null; + + try { + auth = userMgr.createUser(getTestPrincipal().getName(), "pw"); + group = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + Iteratorgroups = auth.declaredMemberOf(); + assertTrue(groups instanceof RangeIterator); + assertEquals(0, ((RangeIterator) groups).getSize()); + groups = auth.memberOf(); + assertTrue(groups instanceof RangeIterator); + assertEquals(0, ((RangeIterator) groups).getSize()); + + group.addMember(auth); + groups = auth.declaredMemberOf(); + assertTrue(groups instanceof RangeIterator); + assertEquals(1, ((RangeIterator) groups).getSize()); + + groups = auth.memberOf(); + assertTrue(groups instanceof RangeIterator); + assertEquals(1, ((RangeIterator) groups).getSize()); + + } finally { + if (auth != null) { + auth.remove(); + } + if (group != null) { + group.remove(); + } + save(superuser); + } + } + + public void testSetSpecialPropertiesDirectly() throws NotExecutableException, RepositoryException { + AuthorizableImpl user = (AuthorizableImpl) getTestUser(superuser); + NodeImpl n = user.getNode(); + try { + String pName = user.getPrincipalName(); + n.setProperty(UserConstants.P_PRINCIPAL_NAME, new StringValue("any-value")); + + // should have failed => change value back. + n.setProperty(UserConstants.P_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.P_IMPERSONATORS, + new Value[] {new StringValue(imperson)}, + PropertyType.STRING); + fail("Attempt to change protected property rep:impersonators should fail."); + } catch (ConstraintViolationException e) { + // ok. + } + } + + public void testRemoveSpecialUserPropertiesDirectly() throws RepositoryException, NotExecutableException { + AuthorizableImpl g = (AuthorizableImpl) getTestUser(superuser); + NodeImpl n = g.getNode(); + try { + n.getProperty(UserConstants.P_PASSWORD).remove(); + fail("Attempt to remove protected property rep:password should fail."); + } catch (ConstraintViolationException e) { + // ok. + } + try { + if (n.hasProperty(UserConstants.P_PRINCIPAL_NAME)) { + n.getProperty(UserConstants.P_PRINCIPAL_NAME).remove(); + fail("Attempt to remove protected property rep:principalName should fail."); + } + } catch (ConstraintViolationException e) { + // ok. + } + } + + public void testRemoveSpecialGroupPropertiesDirectly() throws RepositoryException, NotExecutableException { + AuthorizableImpl g = (AuthorizableImpl) getTestGroup(superuser); + NodeImpl n = g.getNode(); + try { + if (n.hasProperty(UserConstants.P_PRINCIPAL_NAME)) { + n.getProperty(UserConstants.P_PRINCIPAL_NAME).remove(); + fail("Attempt to remove protected property rep:principalName should fail."); + } + } catch (ConstraintViolationException e) { + // ok. + } + try { + if (n.hasProperty(UserConstants.P_MEMBERS)) { + n.getProperty(UserConstants.P_MEMBERS).remove(); + fail("Attempt to remove protected property rep:members should fail."); + } + } catch (ConstraintViolationException e) { + // ok. + } + } + + public void testUserGetProperties() throws RepositoryException, NotExecutableException { + AuthorizableImpl user = (AuthorizableImpl) getTestUser(superuser); + NodeImpl n = user.getNode(); + + for (PropertyIterator it = n.getProperties(); it.hasNext();) { + PropertyImpl p = (PropertyImpl) 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())); + } + } + } + + public void testGroupGetProperties() throws RepositoryException, NotExecutableException { + AuthorizableImpl group = (AuthorizableImpl) getTestGroup(superuser); + NodeImpl n = group.getNode(); + + for (PropertyIterator it = n.getProperties(); it.hasNext();) { + PropertyImpl p = (PropertyImpl) it.nextProperty(); + if (p.getDefinition().isProtected()) { + assertFalse(group.hasProperty(p.getName())); + assertNull(group.getProperty(p.getName())); + } else { + // authorizable defined property + assertTrue(group.hasProperty(p.getName())); + assertNotNull(group.getProperty(p.getName())); + } + } + } + + public void testSingleToMultiValued() throws Exception { + AuthorizableImpl user = (AuthorizableImpl) 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(); + } + } + } + + public void testMultiValuedToSingle() throws Exception { + AuthorizableImpl user = (AuthorizableImpl) 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(); + } + } + } + + public void testObjectMethods() throws Exception { + final AuthorizableImpl user = (AuthorizableImpl) getTestUser(superuser); + AuthorizableImpl user2 = (AuthorizableImpl) 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)); + } + + public void testGetPath() throws Exception { + AuthorizableImpl user = (AuthorizableImpl) getTestUser(superuser); + try { + assertEquals(user.getNode().getPath(), user.getPath()); + } catch (UnsupportedRepositoryOperationException e) { + // ok. + } + + } + + /** + * this is a very specialized test for JCR-3654 + * @throws Exception + */ + public void testMembershipCacheTraversal() throws Exception { + UserManager uMgr = getUserManager(superuser); + //uMgr.autoSave(true); + User u = uMgr.createUser("any_principal" + UUID.randomUUID(), "foobar"); + + // create group with mixin properties + GroupImpl gr = (GroupImpl) uMgr.createGroup("any_group" + UUID.randomUUID()); + for (int i=0; i<100; i++) { + User testUser = uMgr.createUser("any_principal" + UUID.randomUUID(), "foobar"); + gr.addMember(testUser); + } + + gr.addMember(u); + NodeImpl n = gr.getNode(); + n.addMixin("mix:title"); + superuser.save(); + + // removing the authorizable node forces a traversal to collect the memberships + //u = (User) uMgr.getAuthorizable(u.getID()); + //superuser.getNode(u.getPath()).remove(); + u.remove(); + Iterator grp = u.declaredMemberOf(); + assertTrue("User need to be member of group", grp.hasNext()); + Group result = grp.next(); + assertEquals("User needs to be member of group", gr.getID(), result.getID()); + + Iterator auths = gr.getDeclaredMembers(); + int i = 0; + while (auths.hasNext()) { + auths.next(); + i++; + } + assertEquals("Group needs to have 100 members", 100, i); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/DefaultPrincipalProviderTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/DefaultPrincipalProviderTest.java new file mode 100644 index 00000000000..df02d32784e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/DefaultPrincipalProviderTest.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.security.Principal; +import java.util.Properties; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.core.security.TestPrincipal; +import org.apache.jackrabbit.core.security.principal.DefaultPrincipalProvider; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.core.security.principal.PrincipalProvider; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * DefaultPrincipalProviderTest... + */ +public class DefaultPrincipalProviderTest extends AbstractUserTest { + + private PrincipalProvider principalProvider; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!(userMgr instanceof UserManagerImpl)) { + throw new NotExecutableException(); + } + + UserManagerImpl umgr = (UserManagerImpl) userMgr; + // Workaround for testing cache behaviour that relies on observation: + // - retrieve session attached to the userManager implementation. + // - using superuser will not work if users are stored in a different workspace. + Authorizable a = umgr.getAuthorizable(getPrincipalSetFromSession(superuser).iterator().next()); + Session s = ((AuthorizableImpl) a).getNode().getSession(); + principalProvider = new DefaultPrincipalProvider(s, umgr); + principalProvider.init(new Properties()); + } + + @Override + protected void tearDown() throws Exception { + principalProvider.close(); + + super.tearDown(); + } + + // TODO: add tests for 'findPrincipals' + + public void testUnknownUserMemberShip() throws RepositoryException { + Principal userPrincipal = getTestPrincipal(); + + PrincipalIterator pit = principalProvider.getGroupMembership(userPrincipal); + + // unknown user must be in 'everyone' group but nothing else + assertTrue(pit.hasNext()); + assertEquals(EveryonePrincipal.getInstance(), pit.nextPrincipal()); + assertFalse(pit.hasNext()); + } + + public void testInheritedMemberShip() throws RepositoryException, NotExecutableException { + Principal up = getTestPrincipal(); + + User u = null; + Group gr1 = null; + Group gr2 = null; + try { + u = userMgr.createUser(up.getName(), buildPassword(up)); + gr1 = userMgr.createGroup(getTestPrincipal()); + gr2 = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + + gr1.addMember(gr2); + gr2.addMember(u); + save(superuser); + + PrincipalIterator it = principalProvider.getGroupMembership(u.getPrincipal()); + while (it.hasNext()) { + Principal p = it.nextPrincipal(); + if (p.equals(gr1.getPrincipal())) { + // success return + return; + } + } + + fail("User principal " + up.getName() + " must have inherited group membership for " + gr1.getPrincipal().getName()); + + } finally { + if (gr2 != null && u != null) gr2.removeMember(u); + if (gr1 != null && gr2 != null) gr1.removeMember(gr2); + + if (gr1 != null) gr1.remove(); + if (gr2 != null) gr2.remove(); + if (u != null) u.remove(); + save(superuser); + } + } + + /** + * + * @throws Exception + */ + public void testCacheDoesntContainTestPrincipalImpl() throws Exception { + Set principals = getPrincipalSetFromSession(superuser); + for (Principal p : principals) { + Principal testPrinc = new TestPrincipal(p.getName()); + principalProvider.getGroupMembership(testPrinc); + Principal fromProvider = principalProvider.getPrincipal(p.getName()); + + assertNotSame(testPrinc, fromProvider); + assertFalse(fromProvider instanceof TestPrincipal); + } + } + + /** + * Test if cache is properly updated. + * + * @throws Exception + */ + public void testPrincipalCache() throws Exception { + Principal testPrincipal = getTestPrincipal(); + String testName = testPrincipal.getName(); + + assertNull(principalProvider.getPrincipal(testName)); + + // create a user with the given principal name -> cache must be updated. + Authorizable a = userMgr.createUser(testName, "pw"); + save(superuser); + try { + assertNotNull(principalProvider.getPrincipal(testName)); + } finally { + a.remove(); + save(superuser); + } + + // after removal -> entry must be removed from the cache. + assertNull(principalProvider.getPrincipal(testName)); + + // create a group with that name + a = userMgr.createGroup(testPrincipal); + save(superuser); + try { + Principal p = principalProvider.getPrincipal(testName); + assertNotNull(p); + assertTrue(p instanceof GroupPrincipal); + } finally { + a.remove(); + save(superuser); + } + + // recreate user again without filling cache with 'null' value + a = userMgr.createUser(testName, "pw"); + save(superuser); + try { + Principal p = principalProvider.getPrincipal(testName); + assertNotNull(p); + assertFalse(p instanceof GroupPrincipal); + } finally { + a.remove(); + save(superuser); + } + } + + public void testEveryonePrincipal() throws Exception { + Principal p = principalProvider.getPrincipal(EveryonePrincipal.NAME); + assertNotNull(p); + assertEquals(EveryonePrincipal.getInstance(), p); + + PrincipalIterator pit = principalProvider.findPrincipals(EveryonePrincipal.NAME); + assertNotNull(pit); + if (pit.getSize() == -1) { + assertTrue(pit.hasNext()); + assertEquals(EveryonePrincipal.getInstance(), pit.nextPrincipal()); + assertFalse(pit.hasNext()); + } else { + assertEquals(1, pit.getSize()); + assertEquals(EveryonePrincipal.getInstance(), pit.nextPrincipal()); + } + } + + public void testEveryonePrincipal2() throws Exception { + Group g = null; + try { + g = userMgr.createGroup(EveryonePrincipal.NAME); + save(superuser); + + Principal p = principalProvider.getPrincipal(EveryonePrincipal.NAME); + assertNotNull(p); + assertEquals(EveryonePrincipal.getInstance(), p); + + PrincipalIterator pit = principalProvider.findPrincipals(EveryonePrincipal.NAME); + assertNotNull(pit); + if (pit.getSize() == -1) { + assertTrue(pit.hasNext()); + assertEquals(EveryonePrincipal.getInstance(), pit.nextPrincipal()); + assertFalse(pit.hasNext()); + } else { + assertEquals(1, pit.getSize()); + assertEquals(EveryonePrincipal.getInstance(), pit.nextPrincipal()); + } + + } finally { + if (g != null) { + g.remove(); + save(superuser); + } + } + } + + /** + * Test for: Principal assiocated with Group does not update members + * @see JCR-3552 + */ + public void testGroupMembership() throws Exception { + Group g = null; + User u = null; + Principal up = getTestPrincipal(); + try { + // create a group and user, add the user to the group and assert membership + g = userMgr.createGroup(getTestPrincipal()); + u = userMgr.createUser(up.getName(), buildPassword(up)); + save(superuser); + g.addMember(u); + save(superuser); + + Principal groupPrincipal = principalProvider.getPrincipal(g.getPrincipal().getName()); + assertTrue(groupPrincipal instanceof GroupPrincipal); + assertTrue(((GroupPrincipal) groupPrincipal).isMember(u.getPrincipal())); + + // remove the user from the group and assert the user is no longer a member of the group + g.removeMember(u); + save(superuser); + + groupPrincipal = principalProvider.getPrincipal(g.getPrincipal().getName()); + assertFalse(((GroupPrincipal) groupPrincipal).isMember(u.getPrincipal())); + } finally { + if (null != g) { g.remove(); } + if (null != u) { u.remove(); } + save(superuser); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupAdministratorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupAdministratorTest.java new file mode 100644 index 00000000000..ec78da73fc3 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupAdministratorTest.java @@ -0,0 +1,495 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import java.security.Principal; +import java.util.Iterator; + +/** + * GroupAdministratorTest... + */ +public class GroupAdministratorTest extends AbstractUserTest { + + // group-admin + private String uID; + private Session uSession; + + private String otherUID; + private String otherUID2; + private String grID; + + private String groupsPath; + + private Group groupAdmin; + + protected void setUp() throws Exception { + super.setUp(); + + // create a first user + Principal p = getTestPrincipal(); + UserImpl pUser = (UserImpl) userMgr.createUser(p.getName(), buildPassword(p)); + save(superuser); + otherUID = pUser.getID(); + + // create a second user and make it group-admin + p = getTestPrincipal(); + String pw = buildPassword(p); + Credentials creds = buildCredentials(p.getName(), pw); + User user = userMgr.createUser(p.getName(), pw); + save(superuser); + uID = user.getID(); + + // make other user a group-administrator: + Authorizable grAdmin = userMgr.getAuthorizable(UserConstants.GROUP_ADMIN_GROUP_NAME); + if (grAdmin == null || !grAdmin.isGroup()) { + throw new NotExecutableException("Cannot execute test. No group-administrator group found."); + } + groupAdmin = (Group) grAdmin; + groupAdmin.addMember(user); + save(superuser); + grID = groupAdmin.getID(); + + // create a session for the group-admin user. + uSession = getHelper().getRepository().login(creds); + + groupsPath = (userMgr instanceof UserManagerImpl) ? ((UserManagerImpl) userMgr).getGroupsPath() : UserConstants.GROUPS_PATH; + } + + protected void tearDown() throws Exception { + try { + if (uSession != null) { + uSession.logout(); + } + } finally { + // remove group member ship + groupAdmin.removeMember(userMgr.getAuthorizable(uID)); + + // remove all users that have been created + Authorizable a = userMgr.getAuthorizable(otherUID); + if (a != null) { + a.remove(); + } + save(superuser); + } + super.tearDown(); + } + + private String getYetAnotherID() throws RepositoryException, NotExecutableException { + if (otherUID2 == null) { + // create a third user + Principal p = getTestPrincipal(); + otherUID2 = userMgr.createUser(p.getName(), buildPassword(p)).getID(); + save(superuser); + } + return otherUID2; + } + + public void testIsGroupAdmin() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Group gr = (Group) umgr.getAuthorizable(grID); + + assertTrue(gr.isMember(umgr.getAuthorizable(uID))); + } + + public void testCreateUser() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + UserImpl u = null; + // create a new user -> must succeed and user must be create below 'other' + try { + Principal p = getTestPrincipal(); + u = (UserImpl) umgr.createUser(p.getName(), buildPassword(p)); + save(uSession); + fail("Group administrator should not be allowed to create a new user."); + } catch (AccessDeniedException e) { + // success + } finally { + if (u != null) { + u.remove(); + save(uSession); + } + } + } + + public void testRemoveSelf() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + + Authorizable himself = umgr.getAuthorizable(uID); + try { + himself.remove(); + save(uSession); + fail("A GroupAdministrator should not be allowed to remove the own authorizable."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testRemoveGroupAdmin() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + + Authorizable groupAdmin = umgr.getAuthorizable(grID); + try { + groupAdmin.remove(); + save(uSession); + fail("A GroupAdministrator should not be allowed to remove the group admin."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testCreateGroup() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Group testGroup = null; + try { + testGroup = umgr.createGroup(getTestPrincipal()); + save(uSession); + assertTrue(Text.isDescendant(groupsPath, ((GroupImpl)testGroup).getNode().getPath())); + } finally { + if (testGroup != null) { + testGroup.remove(); + save(uSession); + } + } + } + + public void testCreateGroupWithIntermediatePath() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Group testGroup = null; + try { + testGroup = umgr.createGroup(getTestPrincipal(), "/any/intermediate/path"); + save(uSession); + assertTrue(Text.isDescendant(groupsPath + "/any/intermediate/path", ((GroupImpl)testGroup).getNode().getPath())); + } finally { + if (testGroup != null) { + testGroup.remove(); + save(uSession); + } + } + } + + public void testAddToGroup() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Authorizable cU = umgr.getAuthorizable(getYetAnotherID()); + Group gr = (Group) umgr.getAuthorizable(grID); + + // adding and removing the test user as member of a group must succeed. + try { + assertTrue("Modifying group membership requires GroupAdmin membership.",gr.addMember(cU)); + save(uSession); + } finally { + gr.removeMember(cU); + save(uSession); + } + } + + public void testAddToGroup2() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Authorizable cU = umgr.getAuthorizable(getYetAnotherID()); + + Group gr = (Group) umgr.getAuthorizable(groupAdmin.getID()); + assertTrue(gr.addMember(cU)); + save(uSession); + assertTrue(gr.removeMember(cU)); + save(uSession); + } + + public void testAddMembersToCreatedGroup() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Group testGroup = null; + User self = (User) umgr.getAuthorizable(uID); + try { + // let groupadmin create a new group + testGroup = umgr.createGroup(getTestPrincipal(), "/a/b/c/d"); + save(uSession); + + // editing session adds itself to that group -> must succeed. + assertTrue(testGroup.addMember(self)); + save(uSession); + + // add child-user to test group + Authorizable testUser = umgr.getAuthorizable(getYetAnotherID()); + assertFalse(testGroup.isMember(testUser)); + assertTrue(testGroup.addMember(testUser)); + save(uSession); + } finally { + if (testGroup != null) { + for (Iterator it = testGroup.getDeclaredMembers(); it.hasNext();) { + testGroup.removeMember(it.next()); + } + testGroup.remove(); + save(uSession); + } + } + } + + public void testAddMembersUserAdmins() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Authorizable auth = umgr.getAuthorizable(UserConstants.USER_ADMIN_GROUP_NAME); + if (auth == null || !auth.isGroup()) { + throw new NotExecutableException("Cannot execute test. No User-Admin group found."); + } + Group userAdmin = (Group) auth; + Group testGroup = null; + User self = (User) umgr.getAuthorizable(uID); + try { + + userAdmin.addMember(self); + save(uSession); + + userAdmin.removeMember(self); + save(uSession); + fail("Group admin cannot add member to user-admins"); + } catch (AccessDeniedException e) { + // success + } + + try { + // let groupadmin create a new group + testGroup = umgr.createGroup(getTestPrincipal(), "/a/b/c/d"); + save(uSession); + + userAdmin.addMember(testGroup); + save(uSession); + userAdmin.removeMember(testGroup); + save(uSession); + fail("Group admin cannot add member to user-admins"); + } catch (AccessDeniedException e) { + // success + } finally { + if (testGroup != null) { + testGroup.remove(); + save(uSession); + } + } + } + + public void testAddOtherUserToGroup() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + + Authorizable pU = umgr.getAuthorizable(otherUID); + Group gr = (Group) umgr.getAuthorizable(groupAdmin.getID()); + + try { + assertTrue(gr.addMember(pU)); + save(uSession); + } finally { + gr.removeMember(pU); + save(uSession); + } + } + + public void testAddOwnAuthorizableAsGroupAdmin() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + + Authorizable user = umgr.getAuthorizable(uID); + Group gr = (Group) umgr.getAuthorizable(groupAdmin.getID()); + + // user is already group-admin -> adding must return false. + // but should not throw exception. + assertFalse(gr.addMember(user)); + } + + public void testRemoveMembershipForOwnAuthorizable() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + + Authorizable user = umgr.getAuthorizable(uID); + Group gr = (Group) umgr.getAuthorizable(groupAdmin.getID()); + + // removing himself from group. should succeed. + assertTrue(gr.removeMember(user)); + } + + public void testAddOwnAuthorizableToForeignGroup() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Authorizable self = umgr.getAuthorizable(uID); + + Group gr = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + try { + assertTrue(((Group) umgr.getAuthorizable(gr.getID())).addMember(self)); + save(uSession); + assertTrue(((Group) umgr.getAuthorizable(gr.getID())).removeMember(self)); + save(uSession); + } finally { + gr.remove(); + save(superuser); + } + } + + public void testRemoveMembersOfForeignGroup() throws RepositoryException, NotExecutableException { + Group nGr = null; + User nUs = null; + User nUs2 = null; + + try { + // let superuser create a group and a user a make user member of group + nGr = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + Principal p = getTestPrincipal(); + nUs = userMgr.createUser(p.getName(), buildPassword(p)); + save(superuser); + + p = getTestPrincipal(); + nUs2 = userMgr.createUser(p.getName(), buildPassword(p)); + save(superuser); + nGr.addMember(nUs); + nGr.addMember(nUs2); + save(superuser); + + UserManager umgr = getUserManager(uSession); + Group gr = (Group) umgr.getAuthorizable(nGr.getID()); + + // removing any member must fail unless the testuser is user-admin + Iterator it = gr.getMembers(); + if (it.hasNext()) { + Authorizable auth = it.next(); + + String msg = "GroupAdmin must be able to modify group membership."; + assertTrue(msg, gr.removeMember(auth)); + save(uSession); + } else { + fail("Must contain members...."); + } + + } catch (AccessDeniedException e) { + // fine as well. + } finally { + // let superuser remove authorizables again + if (nGr != null) { + nGr.removeMember(nUs); + nGr.removeMember(nUs2); + nGr.remove(); + } + if (nUs != null) nUs.remove(); + if (nUs2 != null) nUs2.remove(); + save(superuser); + } + } + + public void testRemoveAllMembersOfForeignGroup() throws RepositoryException, NotExecutableException { + Group nGr = null; + User nUs = null; + + try { + // let superuser create a group and a user a make user member of group + nGr = userMgr.createGroup(getTestPrincipal()); + save(superuser); + Principal p = getTestPrincipal(); + nUs = userMgr.createUser(p.getName(), buildPassword(p)); + nGr.addMember(nUs); + save(superuser); + + UserManager umgr = getUserManager(uSession); + Group gr = (Group) umgr.getAuthorizable(nGr.getID()); + + // since only 1 single member -> removal rather than modification. + // since uSession is not user-admin this must fail. + for (Iterator it = gr.getMembers(); it.hasNext();) { + Authorizable auth = it.next(); + + String msg = "GroupAdmin must be able to remove a member of another group."; + assertTrue(msg, gr.removeMember(auth)); + save(uSession); + } + } catch (AccessDeniedException e) { + // fine as well. + } finally { + // let superuser remove authorizables again + if (nGr != null && nUs != null) nGr.removeMember(nUs); + if (nGr != null) nGr.remove(); + if (nUs != null) nUs.remove(); + save(superuser); + } + } + + public void testImpersonationOfOtherUser() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Principal selfPrinc = umgr.getAuthorizable(uID).getPrincipal(); + + User child = (User) umgr.getAuthorizable(getYetAnotherID()); + Impersonation impers = child.getImpersonation(); + assertFalse(impers.allows(buildSubject(selfPrinc))); + try { + assertFalse(impers.grantImpersonation(selfPrinc)); + save(uSession); + } catch (AccessDeniedException e) { + // ok. + } + assertFalse(impers.allows(buildSubject(selfPrinc))); + + User parent = (User) umgr.getAuthorizable(otherUID); + impers = parent.getImpersonation(); + assertFalse(impers.allows(buildSubject(selfPrinc))); + try { + assertFalse(impers.grantImpersonation(selfPrinc)); + save(uSession); + } catch (AccessDeniedException e) { + // ok. + } + assertFalse(impers.allows(buildSubject(selfPrinc))); + } + + public void testPersisted() throws NotExecutableException, RepositoryException { + UserManager umgr = getUserManager(uSession); + Group gr = null; + try { + Principal p = getTestPrincipal(); + gr = umgr.createGroup(p); + save(uSession); + + // must be visible for the user-mgr attached to another session. + Authorizable az = userMgr.getAuthorizable(gr.getID()); + assertNotNull(az); + assertEquals(gr.getID(), az.getID()); + } finally { + if (gr != null) { + gr.remove(); + save(uSession); + } + } + } + + public void testAddCustomNodeToGroupAdminNode() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(uSession); + Node groupAdminNode = ((AuthorizableImpl) umgr.getAuthorizable(grID)).getNode(); + Session s = groupAdminNode.getSession(); + + Node n = groupAdminNode.addNode(nodeName1, ntUnstructured); + save(uSession); + + n.setProperty(propertyName1, s.getValueFactory().createValue("anyValue")); + save(uSession); + + n.remove(); + save(uSession); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupImplTest.java new file mode 100644 index 00000000000..0e2ae6e52b7 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/GroupImplTest.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.core.security.user; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import java.security.Principal; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * GroupImplTest... + */ +public class GroupImplTest extends AbstractUserTest { + + public void testEveryoneGroup() throws RepositoryException, NotExecutableException { + Group g = null; + try { + g = userMgr.createGroup(EveryonePrincipal.NAME); + save(superuser); + + assertEquals(EveryonePrincipal.NAME, g.getPrincipal().getName()); + assertEquals(EveryonePrincipal.getInstance(), g.getPrincipal()); + + assertTrue(g.isDeclaredMember(getTestUser(superuser))); + assertTrue(g.isMember(getTestUser(superuser))); + + Iterator it = g.getDeclaredMembers(); + assertTrue(it.hasNext()); + Set members = new HashSet(); + while (it.hasNext()) { + members.add(it.next()); + } + + it = g.getMembers(); + assertTrue(it.hasNext()); + while (it.hasNext()) { + assertTrue(members.contains(it.next())); + } + + assertFalse(g.addMember(getTestUser(superuser))); + assertFalse(g.removeMember(getTestUser(superuser))); + + PrincipalManager pMgr = ((JackrabbitSession) superuser).getPrincipalManager(); + Principal everyone = pMgr.getEveryone(); + + assertTrue(everyone instanceof ItemBasedPrincipal); + assertEquals(everyone, EveryonePrincipal.getInstance()); + + } finally { + if (g != null) { + g.remove(); + save(superuser); + } + } + } + + public void testEveryoneGroup2() throws RepositoryException, NotExecutableException { + Group g = null; + Group g2 = null; + try { + g = userMgr.createGroup(EveryonePrincipal.NAME); + g2 = userMgr.createGroup("testGroup"); + save(superuser); + + assertFalse(g.addMember(g2)); + assertFalse(g.removeMember(g2)); + + assertFalse(g2.addMember(g)); + assertFalse(g2.removeMember(g)); + + } finally { + if (g != null) { + g.remove(); + } + if (g2 != null) { + g2.remove(); + } + save(superuser); + } + } + + public void testEveryoneGroupPrincipal() throws Exception { + Group g = null; + try { + g = userMgr.createGroup(EveryonePrincipal.NAME); + save(superuser); + + GroupPrincipal principal = (GroupPrincipal) g.getPrincipal(); + assertTrue(principal.isMember(new Principal() { + + public String getName() { + return "test"; + } + })); + + assertFalse(principal.isMember(principal)); + + } finally { + if (g != null) { + g.remove(); + save(superuser); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/ImpersonationImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/ImpersonationImplTest.java new file mode 100644 index 00000000000..9389ccd08e9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/ImpersonationImplTest.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.core.security.user; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.api.security.user.Impersonation; +import org.apache.jackrabbit.core.security.SystemPrincipal; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.security.Principal; + +/** + * ImpersonationImplTest... + */ +public class ImpersonationImplTest extends AbstractUserTest { + + private Credentials creds; + private String uID; + private Session uSession; + private UserManager uMgr; + + private String otherUID; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create a first user and retrieve the UserManager from the session + // created for that new user. + Principal p = getTestPrincipal(); + String pw = buildPassword(p); + creds = buildCredentials(p.getName(), pw); + + UserImpl u = (UserImpl) userMgr.createUser(p.getName(), pw); + save(superuser); + + uID = u.getID(); + uSession = getHelper().getRepository().login(creds); + uMgr = getUserManager(uSession); + + // create a second user 'below' the first user. + p = getTestPrincipal(); + pw = buildPassword(p); + + User u2 = userMgr.createUser(p.getName(), pw); + save(superuser); + + otherUID = u2.getID(); + } + + @Override + protected void tearDown() throws Exception { + try { + uSession.logout(); + } finally { + Authorizable a = userMgr.getAuthorizable(uID); + if (a != null) { + a.remove(); + } + a = userMgr.getAuthorizable(otherUID); + if (a != null) { + a.remove(); + } + save(superuser); + } + super.tearDown(); + } + + public void testModifyOwnImpersonation() throws RepositoryException, NotExecutableException { + User u = (User) uMgr.getAuthorizable(uID); + + if (!uSession.hasPermission(((UserImpl) u).getNode().getPath(), "set_property")) { + throw new NotExecutableException("Users should be able to modify their properties -> Check repository config."); + } + + Principal otherP = uMgr.getAuthorizable(otherUID).getPrincipal(); + + Impersonation impers = u.getImpersonation(); + assertFalse(impers.allows(buildSubject(otherP))); + + assertTrue(impers.grantImpersonation(otherP)); + save(uSession); + + assertTrue(impers.allows(buildSubject(otherP))); + + assertTrue(impers.revokeImpersonation(otherP)); + save(uSession); + + assertFalse(impers.allows(buildSubject(otherP))); + } + + public void testModifyOthersImpersonators() throws RepositoryException { + Principal p = uMgr.getAuthorizable(uID).getPrincipal(); + + User other = (User) uMgr.getAuthorizable(otherUID); + try { + boolean success = other.getImpersonation().grantImpersonation(p); + // omit save call + assertFalse("A simple user may not add itself as impersonator to another user.",success); + } catch (AccessDeniedException e) { + // fine as well -> access denied. + } + assertFalse("A simple user may not add itself as impersonator to another user.", other.getImpersonation().allows(buildSubject(p))); + } + + public void testAdminPrincipalAsImpersonator() throws RepositoryException, NotExecutableException { + String adminId = superuser.getUserID(); + Authorizable a = userMgr.getAuthorizable(adminId); + if (a == null || a.isGroup() || !((User) a).isAdmin()) { + throw new NotExecutableException(adminId + " is not administators ID"); + } + + Principal adminPrincipal = new AdminPrincipal(adminId); + + // admin cannot be add/remove to set of impersonators of 'u' but is + // always allowed to impersonate that user. + User u = (User) userMgr.getAuthorizable(uID); + Impersonation impersonation = u.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) a).getImpersonation(); + + assertFalse(adminImpersonation.grantImpersonation(adminPrincipal)); + assertFalse(adminImpersonation.revokeImpersonation(adminPrincipal)); + assertTrue(impersonation.allows(buildSubject(adminPrincipal))); + } + + public void testSystemPrincipalAsImpersonator() throws RepositoryException { + Principal systemPrincipal = new SystemPrincipal(); + assertNull(userMgr.getAuthorizable(systemPrincipal)); + + // system cannot be add/remove to set of impersonators of 'u' nor + // should it be allowed to impersonate a given user... + User u = (User) userMgr.getAuthorizable(uID); + Impersonation impersonation = u.getImpersonation(); + + assertFalse(impersonation.grantImpersonation(systemPrincipal)); + assertFalse(impersonation.revokeImpersonation(systemPrincipal)); + assertFalse(impersonation.allows(buildSubject(systemPrincipal))); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/IndexNodeResolverTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/IndexNodeResolverTest.java new file mode 100644 index 00000000000..9d32c953264 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/IndexNodeResolverTest.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.core.security.user; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.NodeIterator; + +/** IndexNodeResolver... */ +public class IndexNodeResolverTest extends NodeResolverTest { + + private static Logger log = LoggerFactory.getLogger(IndexNodeResolver.class); + + @Override + protected NodeResolver createNodeResolver(SessionImpl session) throws RepositoryException, NotExecutableException { + return new IndexNodeResolver(session, session); + } + + + /** + * If query value contains backslash the non-exact findNodes method should + * return the desired result. + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testFindNodesNonExact() throws NotExecutableException, RepositoryException { + UserImpl currentUser = getCurrentUser(); + Value vs = superuser.getValueFactory().createValue("value \\, containing backslash"); + currentUser.setProperty(propertyName1, vs); + save(); + + Name propName = ((SessionImpl) superuser).getQName(propertyName1); + try { + NodeResolver nr = createNodeResolver(currentUser.getNode().getSession()); + + NodeIterator result = nr.findNodes(propName, "value \\, containing backslash", UserConstants.NT_REP_USER, false); + assertTrue("expected result", result.hasNext()); + assertEquals(currentUser.getNode().getPath(), result.nextNode().getPath()); + assertFalse("expected no more results", result.hasNext()); + } finally { + currentUser.removeProperty(propertyName1); + save(); + } + } + + public void testFindNodesNonExactWithApostrophe() + throws NotExecutableException, RepositoryException { + UserImpl currentUser = getCurrentUser(); + Value vs = superuser.getValueFactory().createValue("value ' with apostrophe"); + currentUser.setProperty(propertyName1, vs); + save(); + + Name propName = ((SessionImpl) superuser).getQName(propertyName1); + try { + NodeResolver nr = createNodeResolver(currentUser.getNode().getSession()); + + NodeIterator result = nr.findNodes(propName, "value ' with apostrophe", UserConstants.NT_REP_USER, false); + assertTrue("expected result", result.hasNext()); + assertEquals(currentUser.getNode().getPath(), result.nextNode().getPath()); + assertFalse("expected no more results", result.hasNext()); + } finally { + currentUser.removeProperty(propertyName1); + save(); + } + } + + + public void testFindNodesExactWithApostrophe() + throws NotExecutableException, RepositoryException { + UserImpl currentUser = getCurrentUser(); + Value vs = superuser.getValueFactory().createValue("value ' with apostrophe"); + currentUser.setProperty(propertyName1, vs); + save(); + + Name propName = ((SessionImpl) superuser).getQName(propertyName1); + try { + NodeResolver nr = createNodeResolver(currentUser.getNode().getSession()); + + NodeIterator result = nr.findNodes(propName, "value ' with apostrophe", UserConstants.NT_REP_USER, true); + assertTrue("expected result", result.hasNext()); + assertEquals(currentUser.getNode().getPath(), result.nextNode().getPath()); + assertFalse("expected no more results", result.hasNext()); + } finally { + currentUser.removeProperty(propertyName1); + save(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/MembershipCachePerfTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/MembershipCachePerfTest.java new file mode 100644 index 00000000000..6ff5218d464 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/MembershipCachePerfTest.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.io.File; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +import org.apache.commons.io.FileUtils; +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.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Performance test for JCR-3892. + */ +public class MembershipCachePerfTest extends JUnitTest { + + private static final String TEST_USER_PREFIX = "MembershipCacheTestUser-"; + private static final String TEST_GROUP_PREFIX = "MembershipCacheTestGroup-"; + private static final String REPO_HOME = new File("target", + MembershipCachePerfTest.class.getSimpleName()).getPath(); + private static final int NUM_USERS = 10000; + private static final int NUM_USERS_PER_GROUP = 5000; + private static final int NUM_GROUPS = 300; + private static final int NUM_READERS = 8; + private static final int NUM_WRITERS = 8; + + private static final int TIME_TEST = 20000; + private static final int TIME_RAMP_UP = 1000; + + + private RepositoryImpl repo; + private JackrabbitSession session; + private UserManager userMgr; + private MembershipCache cache; + + @Override + protected void setUp() throws Exception { + super.setUp(); + FileUtils.deleteDirectory(new File(REPO_HOME)); + RepositoryConfig config = RepositoryConfig.create( + getClass().getResourceAsStream("repository-membersplit.xml"), REPO_HOME); + repo = RepositoryImpl.create(config); + session = createSession(); + userMgr = session.getUserManager(); + cache = ((UserManagerImpl) userMgr).getMembershipCache(); + boolean autoSave = userMgr.isAutoSave(); + userMgr.autoSave(false); + // create test users and groups + System.out.printf("Creating %d users...\n", NUM_USERS); + List users = new ArrayList(); + for (int i = 0; i < NUM_USERS; i++) { + users.add(userMgr.createUser(TEST_USER_PREFIX + i, "secret")); + } + System.out.printf("Creating %d groups...\n", NUM_GROUPS); + for (int i = 0; i < NUM_GROUPS; i++) { + Group g = userMgr.createGroup(TEST_GROUP_PREFIX + i); + for (int j=0; j exceptions = Collections.synchronizedList(new ArrayList()); + List readers = new ArrayList(); + Stats readerStats = new Stats(); + for (int i = 0; i < NUM_READERS; i++) { + Reader r = new Reader(createSession(), readerStats, exceptions); + readers.add(r); + } + + List writers = new ArrayList(); + Stats writerStats = new Stats(); + for (int i = 0; i < NUM_WRITERS; i++) { + Writer w = new Writer(createSession(), writerStats, exceptions); + writers.add(w); + } + + Node test = session.getRootNode().addNode("test", "nt:unstructured"); + session.save(); + + for (Reader r : readers) { + r.start(); + } + + // invalidate stats after ramp-up + Thread.sleep(TIME_RAMP_UP); + cache.clear(); + readerStats.clear(); + + // start writers + for (Writer w : writers) { + w.start(); + } + + long endTime = System.currentTimeMillis() + TIME_TEST; + while (System.currentTimeMillis() < endTime) { + Thread.sleep(1000); + System.out.printf("running...current cache size: %d\n", cache.getSize()); + } + + for (Reader r : readers) { + r.setRunning(false); + } + for (Writer w : writers) { + w.setRunning(false); + } + + for (Reader r : readers) { + r.join(); + } + for (Writer w : writers) { + w.join(); + } + + test.remove(); + session.save(); + + System.out.printf("-----------------------------------------------\n"); + System.out.printf("Test time: %d, Ramp-up time %d\n", TIME_TEST, TIME_RAMP_UP); + System.out.printf("Number of users: %d\n", NUM_USERS); + System.out.printf("Avg number of users/group: %d\n", NUM_USERS_PER_GROUP); + System.out.printf("Number of groups: %d\n", NUM_GROUPS); + System.out.printf("Number of readers: %d\n", NUM_READERS); + System.out.printf("Number of writers: %d\n", NUM_WRITERS); + System.out.printf("Cache size: %d\n", cache.getSize()); + System.out.printf("Time to get memberships:\n"); + readerStats.printResults(System.out); + System.out.printf("-----------------------------------------------\n"); + System.out.printf("Time to alter memberships:\n"); + writerStats.printResults(System.out); + System.out.printf("-----------------------------------------------\n"); + + for (Exception e : exceptions) { + throw e; + } + logger.info("cache size: " + cache.getSize()); + } + + private JackrabbitSession createSession() throws RepositoryException { + return (JackrabbitSession) repo.login( + new SimpleCredentials("admin", "admin".toCharArray())); + } + + private static final class Reader extends Thread { + + private final JackrabbitSession session; + private final UserManager userMgr; + private final Stats stats; + private final Random random = new Random(); + private final List exceptions; + + private boolean running = true; + + public Reader(JackrabbitSession s, + Stats stats, + List exceptions) + throws RepositoryException { + this.session = s; + this.userMgr = s.getUserManager(); + this.stats = stats; + this.exceptions = exceptions; + } + + public void setRunning(boolean running) { + this.running = running; + } + + public void run() { + try { + while (running) { + int idx = random.nextInt(NUM_USERS); + Authorizable user = userMgr.getAuthorizable(TEST_USER_PREFIX + idx); + long time = System.nanoTime(); + user.memberOf(); + stats.logTime(System.nanoTime() - time); + } + } catch (RepositoryException e) { + exceptions.add(e); + } finally { + session.logout(); + } + } + } + + private static final class Writer extends Thread { + + private final JackrabbitSession session; + private final UserManager userMgr; + private final Stats stats; + private final Random random = new Random(); + private final List exceptions; + + private boolean running = true; + + public Writer(JackrabbitSession s, + Stats stats, + List exceptions) + throws RepositoryException { + this.session = s; + this.stats = stats; + this.userMgr = s.getUserManager(); + userMgr.autoSave(false); + this.exceptions = exceptions; + } + + public void setRunning(boolean running) { + this.running = running; + } + + public void run() { + try { + while (running) { + int userIdx = random.nextInt(NUM_USERS); + int groupIdx = random.nextInt(NUM_GROUPS); + User user = (User) userMgr.getAuthorizable(TEST_USER_PREFIX + userIdx); + Group group = (Group) userMgr.getAuthorizable(TEST_GROUP_PREFIX + groupIdx); + + do { + long time = System.nanoTime(); + try { + if (group.isDeclaredMember(user)) { + group.removeMember(user); + } else { + group.addMember(user); + } + session.save(); + stats.logTime(System.nanoTime() - time); + break; + } catch (InvalidItemStateException e) { + // concurrent writing...try again + session.refresh(false); + } + } while (running); + Thread.sleep(10); + } + } catch (RepositoryException e) { + exceptions.add(e); + } catch (InterruptedException e) { + exceptions.add(e); + } finally { + session.logout(); + } + } + } + + private static final class Stats { + + private AtomicLong[] buckets = new AtomicLong[20]; + + public Stats() { + for (int i = 0; i < buckets.length; i++) { + buckets[i] = new AtomicLong(); + } + } + + void logTime(long nanos) { + if (nanos == 0) { + buckets[0].incrementAndGet(); + } else { + buckets[(int) Math.log10(nanos)].incrementAndGet(); + } + } + + void clear() { + for (AtomicLong b: buckets) { + b.set(0); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + String separator = ""; + for (AtomicLong bucket : buckets) { + sb.append(separator); + sb.append(bucket.get()); + separator = ","; + } + return sb.toString(); + } + + public void printResults(PrintStream out) { + long total = 0; + long last = 0; + for (int power = 0; power 0) { + last = power; + } + } + if (last == 0) { + last = buckets.length - 1; + } + + String[] units = {"ns", "10ns", "100ns", "1us", "10us", "100us", "1ms", "10ms", "100ms"}; + for (int power = 0; power<=last; power++) { + long value = buckets[power].get(); + String unit = power < units.length ? units[power] : Math.pow(10, power-units.length) + "s"; + out.printf("%-6s: %2.2f%% (%d)\n", unit, 100.0 * (double) value / (double) total, value); + } + } + + } + + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/MembershipCacheTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/MembershipCacheTest.java new file mode 100644 index 00000000000..c66b7525f2f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/MembershipCacheTest.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.api.JackrabbitSession; +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.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Performance test for JCR-3658. + */ +public class MembershipCacheTest extends JUnitTest { + + private static final String TEST_USER_PREFIX = "MembershipCacheTestUser-"; + private static final String REPO_HOME = new File("target", + MembershipCacheTest.class.getSimpleName()).getPath(); + private static final int NUM_USERS = 100; + private static final int NUM_GROUPS = 8; + private static final int NUM_READERS = 8; + private RepositoryImpl repo; + private JackrabbitSession session; + private UserManager userMgr; + private MembershipCache cache; + + @Override + protected void setUp() throws Exception { + super.setUp(); + FileUtils.deleteDirectory(new File(REPO_HOME)); + RepositoryConfig config = RepositoryConfig.create( + getClass().getResourceAsStream("repository.xml"), REPO_HOME); + repo = RepositoryImpl.create(config); + session = createSession(); + userMgr = session.getUserManager(); + cache = ((UserManagerImpl) userMgr).getMembershipCache(); + boolean autoSave = userMgr.isAutoSave(); + userMgr.autoSave(false); + // create test users and groups + List users = new ArrayList(); + for (int i = 0; i < NUM_USERS; i++) { + users.add(userMgr.createUser(TEST_USER_PREFIX + i, "secret")); + } + for (int i = 0; i < NUM_GROUPS; i++) { + Group g = userMgr.createGroup("MembershipCacheTestGroup-" + i); + for (User u : users) { + g.addMember(u); + } + } + session.save(); + userMgr.autoSave(autoSave); + logger.info("Initial cache size: " + cache.getSize()); + } + + @Override + protected void tearDown() throws Exception { + boolean autoSave = userMgr.isAutoSave(); + userMgr.autoSave(false); + for (int i = 0; i < NUM_USERS; i++) { + userMgr.getAuthorizable(TEST_USER_PREFIX + i).remove(); + } + for (int i = 0; i < NUM_GROUPS; i++) { + userMgr.getAuthorizable("MembershipCacheTestGroup-" + i).remove(); + } + session.save(); + userMgr.autoSave(autoSave); + userMgr = null; + cache = null; + session.logout(); + repo.shutdown(); + repo = null; + FileUtils.deleteDirectory(new File(REPO_HOME)); + super.tearDown(); + } + + public void testConcurrency() throws Exception { + Stats stats = new Stats(); + List exceptions = Collections.synchronizedList(new ArrayList()); + List readers = new ArrayList(); + for (int i = 0; i < NUM_READERS; i++) { + Reader r = new Reader(createSession(), stats, exceptions); + r.addUser(TEST_USER_PREFIX + 0); + readers.add(r); + } + Node test = session.getRootNode().addNode("test", "nt:unstructured"); + session.save(); + for (Reader r : readers) { + r.start(); + } + for (int i = 1; i < NUM_USERS; i++) { + test.addNode("node-" + i); + session.save(); + for (Reader r : readers) { + r.addUser(TEST_USER_PREFIX + i); + } + } + for (Reader r : readers) { + r.join(); + } + test.remove(); + session.save(); + System.out.println(stats); + for (Exception e : exceptions) { + throw e; + } + } + + public void testRun75() throws Exception { + for (int i = 0; i < 75; i++) { + testConcurrency(); + cache.clear(); + } + } + + private JackrabbitSession createSession() throws RepositoryException { + return (JackrabbitSession) repo.login( + new SimpleCredentials("admin", "admin".toCharArray())); + } + + private static final class Reader extends Thread { + + private final JackrabbitSession session; + private final UserManager userMgr; + private final Stats stats; + private final List knownUsers = new ArrayList(); + private final Random random = new Random(); + private final List exceptions; + + public Reader(JackrabbitSession s, + Stats stats, + List exceptions) + throws RepositoryException { + this.session = s; + this.userMgr = s.getUserManager(); + this.stats = stats; + this.exceptions = exceptions; + } + + void addUser(String user) { + synchronized (knownUsers) { + knownUsers.add(user); + } + } + + public void run() { + try { + while (knownUsers.size() < NUM_USERS) { + Object idOrUser; + int idx; + synchronized (knownUsers) { + idx = random.nextInt(knownUsers.size()); + idOrUser = knownUsers.get(idx); + } + User user; + if (idOrUser instanceof String) { + user = (User) userMgr.getAuthorizable((String) idOrUser); + synchronized (knownUsers) { + knownUsers.set(idx, user); + } + } else { + user = (User) idOrUser; + } + long time = System.nanoTime(); + user.memberOf(); + stats.logTime(System.nanoTime() - time); + } + } catch (RepositoryException e) { + exceptions.add(e); + } finally { + session.logout(); + } + } + } + + private static final class Stats { + + private AtomicLong[] buckets = new AtomicLong[20]; + + public Stats() { + for (int i = 0; i < buckets.length; i++) { + buckets[i] = new AtomicLong(); + } + } + + void logTime(long nanos) { + if (nanos == 0) { + buckets[0].incrementAndGet(); + } else { + buckets[(int) Math.log10(nanos)].incrementAndGet(); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + String separator = ""; + for (AtomicLong bucket : buckets) { + sb.append(separator); + sb.append(bucket.get()); + separator = ","; + } + return sb.toString(); + } + } + + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NodeCreationTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NodeCreationTest.java new file mode 100644 index 00000000000..8773eb67e05 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NodeCreationTest.java @@ -0,0 +1,409 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.commons.collections.map.ListOrderedMap; +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.core.NodeImpl; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.TestPrincipal; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * NodeCreationTest... + */ +public class NodeCreationTest extends AbstractUserTest { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(NodeCreationTest.class); + + private SessionImpl s; + private UserManagerImpl uMgr; + private final List toRemove = new ArrayList(); + + private String usersPath; + private String groupsPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + String workspaceName = ((RepositoryImpl) superuser.getRepository()).getConfig().getSecurityConfig().getSecurityManagerConfig().getWorkspaceName(); + s = (SessionImpl) ((SessionImpl) superuser).createSession(workspaceName); + + usersPath = ((UserManagerImpl) userMgr).getUsersPath(); + groupsPath = ((UserManagerImpl) userMgr).getGroupsPath(); + } + + @Override + protected void tearDown() throws Exception { + try { + for (NodeImpl node : toRemove) { + uMgr.removeProtectedItem(node, (NodeImpl) node.getParent()); + save(s); + } + } finally { + s.logout(); + } + super.tearDown(); + } + + private void createUserManager(int depth, boolean expandTree, long size) throws RepositoryException { + Properties props = new Properties(); + props.put(UserManagerImpl.PARAM_DEFAULT_DEPTH, depth); + props.put(UserManagerImpl.PARAM_AUTO_EXPAND_TREE, expandTree); + props.put(UserManagerImpl.PARAM_AUTO_EXPAND_SIZE, size); + props.put(UserManagerImpl.PARAM_GROUPS_PATH, groupsPath); + props.put(UserManagerImpl.PARAM_USERS_PATH, usersPath); + + uMgr = new UserManagerImpl(s, "admin", props); + } + + + public void testRemoveTree() throws RepositoryException, NotExecutableException { + UserImpl u = (UserImpl) userMgr.createUser("z", "z"); + save(superuser); + UserImpl u2 = (UserImpl) userMgr.createUser("zz", "zz"); + save(superuser); + + assertEquals(usersPath + "/z/zz/z", u.getNode().getPath()); + + try { + NodeImpl folder = (NodeImpl) u.getNode().getParent().getParent(); + ((UserManagerImpl) userMgr).removeProtectedItem(folder, (NodeImpl) folder.getParent()); + save(superuser); + } finally { + boolean fail = false; + if (userMgr.getAuthorizable("z") != null) { + fail = true; + u.remove(); + save(superuser); + } + if (userMgr.getAuthorizable("zz") != null) { + fail = true; + u2.remove(); + save(superuser); + } + if (fail) { + fail("Removing the top authorizable folder must remove all users contained."); + } + } + } + + /** + * If auto-expand is false all users must be created on the second level. + */ + public void testDefault() throws RepositoryException, NotExecutableException { + createUserManager(2, false, 1); + + UserImpl u = (UserImpl) uMgr.createUser("z", "z"); + save(s); + + // remember the z-folder for later removal + toRemove.add((NodeImpl) u.getNode().getParent().getParent()); + assertEquals(usersPath + "/z/zz/z", u.getNode().getPath()); + + Map m = new ListOrderedMap(); + m.put("zz", "/z/zz/zz"); + m.put("zzz", "/z/zz/zzz"); + m.put("zzzz", "/z/zz/zzzz"); + m.put("zh", "/z/zh/zh"); + m.put("zHzh", "/z/zH/zHzh"); + m.put("z_Hz", "/z/z_/z_Hz"); + m.put("z\u00cfrich", "/z/z\u00cf/z\u00cfrich"); + + for (String uid : m.keySet()) { + u = (UserImpl) uMgr.createUser(uid, uid); + save(s); + assertEquals(usersPath + m.get(uid), u.getNode().getPath()); + } + } + + /** + * Having 3 default levels -> test uids again. + * + * @throws RepositoryException + */ + public void testChangedDefaultLevel() throws RepositoryException, NotExecutableException { + createUserManager(3, false, 1); + + UserImpl u = (UserImpl) uMgr.createUser("z", "z"); + save(s); + + // remember the z-folder for later removal + toRemove.add((NodeImpl) u.getNode().getParent().getParent().getParent()); + assertEquals(usersPath + "/z/zz/zzz/z", u.getNode().getPath()); + + Map m = new ListOrderedMap(); + m.put("zz", "/z/zz/zzz/zz"); + m.put("zzz", "/z/zz/zzz/zzz"); + m.put("zzzz", "/z/zz/zzz/zzzz"); + m.put("zH", "/z/zH/zHH/zH"); + m.put("zHzh", "/z/zH/zHz/zHzh"); + m.put("z_Hz", "/z/z_/z_H/z_Hz"); + m.put("z\u00cfrich", "/z/z\u00cf/z\u00cfr/z\u00cfrich"); + + for (String uid : m.keySet()) { + u = (UserImpl) uMgr.createUser(uid, uid); + save(s); + + assertEquals(usersPath + m.get(uid), u.getNode().getPath()); + + Authorizable az = uMgr.getAuthorizable(uid); + assertNotNull(az); + } + } + + public void testIllegalChars() throws RepositoryException, NotExecutableException { + createUserManager(2, true, 2); + + UserImpl u = (UserImpl) uMgr.createUser("z", "z"); + save(s); + + // remember the z-folder for later removal + toRemove.add((NodeImpl) u.getNode().getParent().getParent()); + + String zu = Text.escapeIllegalJcrChars("z*"); + String zur = Text.escapeIllegalJcrChars("z*r"); + + Map m = new ListOrderedMap(); + // test illegal JCR chars in uid + // on level 2 + m.put("z*rich", "/z/" + zu + "/" + Text.escapeIllegalJcrChars("z*rich")); + m.put("z*riq", "/z/" + zu + "/" + Text.escapeIllegalJcrChars("z*riq")); + m.put("z*", "/z/" + zu + "/" + zu); // still on level 2 (too short for 3) + // on level 3 + m.put("z*rik", "/z/" + zu + "/" + zur + "/" + Text.escapeIllegalJcrChars("z*rik")); + m.put("z*.ri", "/z/" + zu + "/" + Text.escapeIllegalJcrChars("z*.") + "/" + Text.escapeIllegalJcrChars("z*.ri")); + + for (String uid : m.keySet()) { + u = (UserImpl) uMgr.createUser(uid, uid); + save(s); + assertEquals(usersPath + m.get(uid), u.getNode().getPath()); + + Authorizable ath = uMgr.getAuthorizable(uid); + assertNotNull("User with id " + uid + " must exist.", ath); + assertFalse("User with id " + uid + " must not be a group.", ath.isGroup()); + } + + // test for groups as well + GroupImpl gr = (GroupImpl) uMgr.createGroup(new TestPrincipal("z[x]")); + save(s); + // remember the z-folder for later removal + toRemove.add((NodeImpl) gr.getNode().getParent().getParent()); + + assertEquals("z[x]", gr.getID()); + String expectedPath = groupsPath + "/z/" + Text.escapeIllegalJcrChars("z[") + "/" + Text.escapeIllegalJcrChars("z[x]"); + assertEquals(expectedPath, gr.getNode().getPath()); + + Authorizable ath = uMgr.getAuthorizable(gr.getID()); + assertNotNull(ath); + assertTrue(ath.isGroup()); + + // test if conflicting authorizables are detected. + try { + uMgr.createUser("z[x]", "z[x]"); + save(s); + fail("A group \"z[x]\" already exists."); + } catch (AuthorizableExistsException e) { + // success + } + + try { + uMgr.createGroup(new TestPrincipal("z*rik")); + save(s); + fail("A user \"z*rik\" already exists"); + } catch (AuthorizableExistsException e) { + // success + } + } + + /** + * If auto-expand is true users must be distributed over more than default-depth + * levels if max-size is reached. + * In addition the special cases must be respected (see DefaultIdResolver). + * + * @throws RepositoryException + */ + public void testAutoExpand() throws RepositoryException, NotExecutableException { + createUserManager(2, true, 5); + + UserImpl u = (UserImpl) uMgr.createUser("z", "z"); + save(s); + + // remember the z-folder for later removal + toRemove.add((NodeImpl) u.getNode().getParent().getParent()); + assertEquals(usersPath + "/z/zz/z", u.getNode().getPath()); + + Map m = new ListOrderedMap(); + m.put("zz", "/z/zz/zz"); + // zzz -> potential conflict: must be added to 3rd level + m.put("zzz", "/z/zz/zzz/zzz"); + + // more users -> added to 2nd level until max-size (5) is reached. + m.put("zzABC", "/z/zz/zzABC"); + m.put("zzzh", "/z/zz/zzzh"); + + // max-size on level 2 (zz) is reached -> added to 3rd level. + m.put("zzzzZ", "/z/zz/zzz/zzzzZ"); + m.put("zzh", "/z/zz/zzh/zzh"); + m.put("zzXyzzz", "/z/zz/zzX/zzXyzzz"); + + // zzzz, zzza -> potential conflicts on the 3rd level + // -> must be added to 4th level + m.put("zzzz", "/z/zz/zzz/zzzz/zzzz"); + m.put("zzza", "/z/zz/zzz/zzza/zzza"); + + + // zA -> to short for 3rd -> must be inserted at the 2nd level. + m.put("zA", "/z/zA/zA"); + + for (String uid : m.keySet()) { + u = (UserImpl) uMgr.createUser(uid, uid); + save(s); + assertEquals(usersPath + m.get(uid), u.getNode().getPath()); + } + } + + /** + * Test special case of turning autoexpandtree option on having colliding + * authorizables already present a leve N: In this case auto-expansion must + * be aborted at that level and the authorizable will be create at level N + * ignoring that max-size has been reached. + * + * @throws RepositoryException + */ + public void testConflictUponChangingAutoExpandFlag() throws RepositoryException, NotExecutableException { + createUserManager(2, false, 1); + + UserImpl u = (UserImpl) uMgr.createUser("zzz", "zzz"); + save(s); + + // remember the z-folder for later removal + toRemove.add((NodeImpl) u.getNode().getParent().getParent()); + + assertEquals(usersPath + "/z/zz/zzz", u.getNode().getPath()); + + // now create a second user manager that has auto-expand-tree enabled + createUserManager(2, true, 1); + + + Map m = new ListOrderedMap(); + // upon creation of any a new user 'zzzA' an additional intermediate + // folder would be created if there wasn't the colliding authorizable + // 'zzz' -> autoexpansion is aborted. + m.put("zzzA", "/z/zz/zzzA"); + // this is also true for 'zzzzz' and zzzBsl + m.put("zzzzz", "/z/zz/zzzzz"); + m.put("zzzBsl", "/z/zz/zzzBsl"); + + // on other levels the expansion must still work as expected. + // - zzBsl -> zz is completed -> create zzB -> insert zzBsl user + // - zzBslrich -> zz, zzB are completed -> create zzBs -> insert zzBslrich user + m.put("zzBsl", "/z/zz/zzB/zzBsl"); + m.put("zzBslrich", "/z/zz/zzB/zzBs/zzBslrich"); + + for (String uid : m.keySet()) { + u = (UserImpl) uMgr.createUser(uid, uid); + save(s); + + assertEquals(usersPath + m.get(uid), u.getNode().getPath()); + assertNotNull(uMgr.getAuthorizable(uid)); + } + } + + /** + * Find by ID must succeed. + * + * @throws RepositoryException + */ + public void testFindById() throws RepositoryException, NotExecutableException { + createUserManager(2, true, 2); + + UserImpl u = (UserImpl) uMgr.createUser("z", "z"); + save(s); + + // remember the z-folder for later removal + toRemove.add((NodeImpl) u.getNode().getParent().getParent()); + assertEquals(usersPath + "/z/zz/z", u.getNode().getPath()); + + Map m = new ListOrderedMap(); + // potential conflicting uid + m.put("zzz", "/z/zz/zzz/zzz"); + // max-size (2) is reached + m.put("zzzuerich", "/z/zz/zzz/zzzuerich"); + m.put("zzuerich", "/z/zz/zzu/zzuerich"); + // too short for expanded folders + m.put("zz", "/z/zz/zz"); + + for (String uid : m.keySet()) { + u = (UserImpl) uMgr.createUser(uid, uid); + save(s); + + assertEquals(usersPath + m.get(uid), u.getNode().getPath()); + + User us = (User) uMgr.getAuthorizable(uid); + assertNotNull(us); + assertEquals(uid, us.getID()); + } + } + + public void testIdIsCaseSensitive() throws RepositoryException, NotExecutableException { + createUserManager(2, true, 2); + + UserImpl u = (UserImpl) uMgr.createUser("ZuRiCh", "z"); + save(s); + + // remember the z-folder for later removal + toRemove.add((NodeImpl) u.getNode().getParent().getParent()); + + assertEquals("ZuRiCh", u.getID()); + } + + public void testUUIDIsBuildCaseInsensitive() throws RepositoryException, NotExecutableException { + createUserManager(2, true, 2); + + UserImpl u = (UserImpl) uMgr.createUser("ZuRiCh", "z"); + save(s); + + // remember the z-folder for later removal + toRemove.add((NodeImpl) u.getNode().getParent().getParent()); + + try { + User u2 = uMgr.createUser("zurich", "z"); + fail("uuid is built from insensitive userID -> must conflict"); + } catch (AuthorizableExistsException e) { + // success + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NodeResolverTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NodeResolverTest.java new file mode 100644 index 00000000000..b31770d48d5 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NodeResolverTest.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +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.UserManager; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import java.util.Iterator; + +/** NodeResolverTest... */ +public abstract class NodeResolverTest extends AbstractJCRTest { + + NodeResolver nodeResolver; + UserManager umgr; + String usersPath = UserConstants.USERS_PATH; + String groupsPath = UserConstants.GROUPS_PATH; + String authorizablesPath = UserConstants.AUTHORIZABLES_PATH; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + nodeResolver = createNodeResolver(superuser); + if (!(superuser instanceof JackrabbitSession)) { + throw new NotExecutableException(); + } + + umgr = ((JackrabbitSession) superuser).getUserManager(); + if (umgr instanceof UserManagerImpl) { + UserManagerImpl uImpl = (UserManagerImpl) umgr; + usersPath = uImpl.getUsersPath(); + groupsPath = uImpl.getGroupsPath(); + + authorizablesPath = usersPath; + while (!Text.isDescendant(authorizablesPath, groupsPath)) { + authorizablesPath = Text.getRelativeParent(authorizablesPath, 1); + } + } + } + + protected UserImpl getCurrentUser() throws NotExecutableException, RepositoryException { + String uid = superuser.getUserID(); + if (uid != null) { + Authorizable auth = umgr.getAuthorizable(uid); + if (auth != null && auth instanceof UserImpl) { + return (UserImpl) auth; + } + } + // unable to retrieve current user + throw new NotExecutableException(); + } + + protected void save() throws RepositoryException { + if (!umgr.isAutoSave() && superuser.hasPendingChanges()) { + superuser.save(); + } + } + + protected abstract NodeResolver createNodeResolver(SessionImpl session) throws RepositoryException, NotExecutableException; + + protected NodeResolver createNodeResolver(Session session) throws NotExecutableException, RepositoryException { + if (!(session instanceof SessionImpl)) { + throw new NotExecutableException(); + } + + NodeResolver resolver = createNodeResolver((SessionImpl) session); + UserManager umr = ((SessionImpl) session).getUserManager(); + if (umr instanceof UserManagerImpl) { + UserManagerImpl uImpl = (UserManagerImpl) umr; + resolver.setSearchRoots(uImpl.getUsersPath(), uImpl.getGroupsPath()); + } + return resolver; + } + + public void testFindNode() throws NotExecutableException, RepositoryException { + UserImpl currentUser = getCurrentUser(); + + NodeResolver nr = createNodeResolver(currentUser.getNode().getSession()); + + Node result = nr.findNode(currentUser.getNode().getQName(), UserConstants.NT_REP_USER); + assertNotNull(result); + assertTrue(currentUser.getNode().isSame(result)); + + result = nr.findNode(currentUser.getNode().getQName(), UserConstants.NT_REP_AUTHORIZABLE); + assertNotNull(result); + assertTrue(currentUser.getNode().isSame(result)); + + result = nr.findNode(currentUser.getNode().getQName(), UserConstants.NT_REP_GROUP); + assertNull(result); + + Iterator it = currentUser.memberOf(); + while (it.hasNext()) { + GroupImpl gr = (GroupImpl) it.next(); + + result = nr.findNode(gr.getNode().getQName(), UserConstants.NT_REP_GROUP); + assertNotNull(result); + assertTrue(gr.getNode().isSame(result)); + + result = nr.findNode(gr.getNode().getQName(), UserConstants.NT_REP_AUTHORIZABLE); + assertNotNull(result); + assertTrue(gr.getNode().isSame(result)); + + result = nr.findNode(gr.getNode().getQName(), UserConstants.NT_REP_USER); + assertNull(result); + } + } + + public void testFindNodeByPrincipalName() throws NotExecutableException, RepositoryException { + UserImpl currentUser = getCurrentUser(); + + NodeResolver nr = createNodeResolver(currentUser.getNode().getSession()); + + Node result = nr.findNode(UserConstants.P_PRINCIPAL_NAME, currentUser.getPrincipal().getName(), UserConstants.NT_REP_USER); + assertNotNull(result); + assertTrue(currentUser.getNode().isSame(result)); + + Iterator it = currentUser.memberOf(); + while (it.hasNext()) { + GroupImpl gr = (GroupImpl) it.next(); + + result = nr.findNode(UserConstants.P_PRINCIPAL_NAME, gr.getPrincipal().getName(), UserConstants.NT_REP_GROUP); + assertNotNull(result); + assertTrue(gr.getNode().isSame(result)); + + result = nr.findNode(UserConstants.P_PRINCIPAL_NAME, gr.getPrincipal().getName(), UserConstants.NT_REP_AUTHORIZABLE_FOLDER); + assertNull(result); + } + } + + public void testFindNodeByMultiValueProp() throws NotExecutableException, RepositoryException { + UserImpl currentUser = getCurrentUser(); + + Value[] vs = new Value[] { + superuser.getValueFactory().createValue("blub"), + superuser.getValueFactory().createValue("blib") + }; + currentUser.setProperty(propertyName1, vs); + save(); + + NodeResolver nr = createNodeResolver(currentUser.getNode().getSession()); + + Node result = nr.findNode(((SessionImpl) superuser).getQName(propertyName1), + "blib", UserConstants.NT_REP_USER); + assertNotNull(result); + assertTrue(currentUser.getNode().isSame(result)); + + currentUser.removeProperty(propertyName1); + save(); + } + + public void testFindNodeWithNonExistingSearchRoot() throws NotExecutableException, RepositoryException { + String searchRoot = nodeResolver.getSearchRoot(UserConstants.NT_REP_AUTHORIZABLE_FOLDER); + SessionImpl sImpl = (SessionImpl) superuser; + + if (sImpl.nodeExists(searchRoot)) { + throw new NotExecutableException(); + } + Node result = nodeResolver.findNode(sImpl.getQName(UserConstants.GROUP_ADMIN_GROUP_NAME), UserConstants.NT_REP_AUTHORIZABLE); + assertNull(result); + } + + public void testFindNodes() throws NotExecutableException, RepositoryException { + Value[] vs = new Value[] { + superuser.getValueFactory().createValue("blub"), + superuser.getValueFactory().createValue("blib") + }; + + UserImpl currentUser = getCurrentUser(); + currentUser.setProperty(propertyName1, vs); + + int expResultSize = 1; + Iterator it = currentUser.memberOf(); + while (it.hasNext()) { + GroupImpl gr = (GroupImpl) it.next(); + gr.setProperty(propertyName1, vs); + expResultSize++; + } + save(); + + Name propName = ((SessionImpl) superuser).getQName(propertyName1); + + try { + NodeResolver nr = createNodeResolver(currentUser.getNode().getSession()); + + NodeIterator result = nr.findNodes(propName, "blub", UserConstants.NT_REP_USER, false); + assertTrue("expected result", result.hasNext()); + assertEquals(currentUser.getNode().getPath(), result.nextNode().getPath()); + assertFalse("expected no more results", result.hasNext()); + + result = nr.findNodes(propName, "blub", UserConstants.NT_REP_AUTHORIZABLE, false); + assertEquals(expResultSize, getSize(result)); + + } finally { + currentUser.removeProperty(propertyName1); + it = currentUser.memberOf(); + while (it.hasNext()) { + GroupImpl gr = (GroupImpl) it.next(); + gr.removeProperty(propertyName1); + } + save(); + } + } + + /** + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testFindNodesByRelPathProperties() throws NotExecutableException, RepositoryException { + Value[] vs = new Value[] { + superuser.getValueFactory().createValue("blub"), + superuser.getValueFactory().createValue("blib") + }; + + String relPath = "relPath/" + propertyName1; + String relPath2 = "another/" + propertyName1; + String relPath3 = "relPath/relPath/" + propertyName1; + UserImpl currentUser = getCurrentUser(); + currentUser.setProperty(relPath, vs); + currentUser.setProperty(relPath2, vs); + currentUser.setProperty(relPath3, vs); + save(); + + Path relQPath = ((SessionImpl) superuser).getQPath(relPath); + Path relQName = ((SessionImpl) superuser).getQPath(propertyName1); + + try { + NodeResolver nr = createNodeResolver(currentUser.getNode().getSession()); + // 1) findNodes(QPath..... + // relPath : "prop1" -> should find the currentuserNode + NodeIterator result = nr.findNodes(relQName, "blub", UserManager.SEARCH_TYPE_USER, false, Long.MAX_VALUE); + assertTrue("expected result", result.hasNext()); + Node n = result.nextNode(); + assertEquals(currentUser.getNode().getPath(), n.getPath()); + assertFalse("expected no more results", result.hasNext()); + + // relPath : "relPath/prop1" -> should find the currentuserNode + result = nr.findNodes(relQPath, "blub", UserManager.SEARCH_TYPE_USER, false, Long.MAX_VALUE); + assertTrue("expected result", result.hasNext()); + assertEquals(currentUser.getNode().getPath(), result.nextNode().getPath()); + assertFalse("expected no more results", result.hasNext()); + + // 2) findNodes(Name.......) + // search by Name -> should not find deep property + Name propName = ((SessionImpl) superuser).getQName(propertyName1); + result = nr.findNodes(propName, "blub", UserConstants.NT_REP_USER, false); + assertFalse("should not find result", result.hasNext()); + + } finally { + currentUser.removeProperty(relPath); + currentUser.removeProperty(relPath2); + currentUser.removeProperty(relPath3); + save(); + } + } + + + public void testFindNodesWithNonExistingSearchRoot() throws NotExecutableException, RepositoryException { + String searchRoot = nodeResolver.getSearchRoot(UserConstants.NT_REP_AUTHORIZABLE); + if (superuser.nodeExists(searchRoot)) { + throw new NotExecutableException(); + } + + NodeIterator result = nodeResolver.findNodes(UserConstants.P_PRINCIPAL_NAME, "anyValue", UserConstants.NT_REP_AUTHORIZABLE, true); + assertNotNull(result); + assertFalse(result.hasNext()); + } + + public void testGetSearchRoot() { + String searchRoot = nodeResolver.getSearchRoot(UserConstants.NT_REP_AUTHORIZABLE); + assertNotNull(searchRoot); + assertEquals(authorizablesPath, searchRoot); + + searchRoot = nodeResolver.getSearchRoot(UserConstants.NT_REP_GROUP); + assertNotNull(searchRoot); + assertEquals(groupsPath, searchRoot); + + searchRoot = nodeResolver.getSearchRoot(UserConstants.NT_REP_USER); + assertNotNull(searchRoot); + assertEquals(usersPath, searchRoot); + } + + public void testGetSearchRootDefault() { + String searchRoot = nodeResolver.getSearchRoot(UserConstants.NT_REP_AUTHORIZABLE_FOLDER); + assertNotNull(searchRoot); + assertEquals(authorizablesPath, searchRoot); + + searchRoot = nodeResolver.getSearchRoot(NameConstants.NT_UNSTRUCTURED); + assertNotNull(searchRoot); + assertEquals(authorizablesPath, searchRoot); + } + + public void testGetNamePathResolver() { + assertNotNull(nodeResolver.getNamePathResolver()); + } + + public void testGetSession() { + assertNotNull(nodeResolver.getSession()); + } + + public void testFindNodeEscape() throws RepositoryException { + Name n = NameFactoryImpl.getInstance().create("", + "someone" + "@apache.org"); + nodeResolver.findNode(n, UserConstants.NT_REP_USER); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NotUserAdministratorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NotUserAdministratorTest.java new file mode 100644 index 00000000000..279347ad633 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/NotUserAdministratorTest.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.core.security.SecurityConstants; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import java.security.Principal; + +/** + * NotUserAdministratorTest... + */ +public class NotUserAdministratorTest extends AbstractUserTest { + + // test user that is NOT user admin + private String uID; + private Session uSession; + private UserManager uMgr; + + protected void setUp() throws Exception { + super.setUp(); + + // create a first user and retrieve the UserManager from the session + // created for that new user. + Principal p = getTestPrincipal(); + String pw = buildPassword(p); + + UserImpl u = (UserImpl) userMgr.createUser(p.getName(), pw); + save(superuser); + + uID = u.getID(); + + // create a session for the other user. + uSession = getHelper().getRepository().login(new SimpleCredentials(uID, pw.toCharArray())); + uMgr = getUserManager(uSession); + } + + protected void tearDown() throws Exception { + try { + if (uSession != null) { + uSession.logout(); + } + } finally { + Authorizable a = userMgr.getAuthorizable(uID); + if (a != null) { + a.remove(); + save(superuser); + } + } + super.tearDown(); + } + + public void testCreateUser() throws NotExecutableException { + try { + Principal p = getTestPrincipal(); + User u = uMgr.createUser(p.getName(), buildPassword(p)); + save(uSession); + + fail("A non-UserAdmin should not be allowed to create a new User."); + + // clean-up: let superuser remove the user created by fault. + userMgr.getAuthorizable(u.getID()).remove(); + } catch (AuthorizableExistsException e) { + // should never get here. + fail(e.getMessage()); + } catch (RepositoryException e) { + // success + } + } + + public void testCreateUserWithItermediatePath() throws NotExecutableException { + try { + Principal p = getTestPrincipal(); + User u = uMgr.createUser(p.getName(), buildPassword(p), p, "/any/intermediate/path"); + save(uSession); + + fail("A non-UserAdmin should not be allowed to create a new User."); + + // clean-up: let superuser remove the user created by fault. + userMgr.getAuthorizable(u.getID()).remove(); + } catch (AuthorizableExistsException e) { + // should never get here. + fail(e.getMessage()); + } catch (RepositoryException e) { + // success + } + } + + public void testRemoveOwnAuthorizable() throws RepositoryException, NotExecutableException { + Authorizable himself = uMgr.getAuthorizable(uID); + try { + himself.remove(); + save(uSession); + + fail("A user should not be allowed to remove him/herself."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testRemoveUser() throws RepositoryException, NotExecutableException { + // let superuser create another user. + Principal p = getTestPrincipal(); + String user2ID = userMgr.createUser(p.getName(), buildPassword(p)).getID(); + save(superuser); + + try { + Authorizable a = uMgr.getAuthorizable(user2ID); + a.remove(); + save(uSession); + + fail("A non-administrator user should not be allowed to remove another user."); + } catch (AccessDeniedException e) { + // success + } + + // let superuser do clean up. + Authorizable user2 = userMgr.getAuthorizable(user2ID); + if (user2 != null) { + user2.remove(); + save(superuser); + } + } + + public void testRemoveOtherUser() throws RepositoryException, NotExecutableException { + // let superuser create another user. + Principal p = getTestPrincipal(); + String user2ID = userMgr.createUser(p.getName(), buildPassword(p), p, "/any/intermediate/path").getID(); + save(superuser); + + try { + Authorizable a = uMgr.getAuthorizable(user2ID); + a.remove(); + save(uSession); + + fail("A non-administrator user should not be allowed to remove another user."); + } catch (AccessDeniedException e) { + // success + } + + // let superuser do clean up. + Authorizable user2 = userMgr.getAuthorizable(user2ID); + if (user2 != null) { + user2.remove(); + save(superuser); + } + } + + public void testModifyImpersonationOfAnotherUser() throws RepositoryException, NotExecutableException { + // let superuser create another user. + Principal p = getTestPrincipal(); + String user2ID = userMgr.createUser(p.getName(), buildPassword(p)).getID(); + save(superuser); + + try { + Authorizable a = uMgr.getAuthorizable(user2ID); + + Impersonation impers = ((User) a).getImpersonation(); + Principal himselfP = uMgr.getAuthorizable(uID).getPrincipal(); + assertFalse(impers.allows(buildSubject(himselfP))); + impers.grantImpersonation(himselfP); + save(uSession); + + fail("A non-administrator user should not be allowed modify Impersonation of another user."); + } catch (AccessDeniedException e) { + // success + } + + // let superuser do clean up. + Authorizable user2 = userMgr.getAuthorizable(user2ID); + if (user2 != null) { + user2.remove(); + save(superuser); + } + } + + public void testAddToGroup() throws NotExecutableException, RepositoryException { + Authorizable auth = uMgr.getAuthorizable(SecurityConstants.ADMINISTRATORS_NAME); + if (auth == null || !auth.isGroup()) { + throw new NotExecutableException("Couldn't find 'administrators' group"); + } + + Group gr = (Group) auth; + try { + auth = uMgr.getAuthorizable(uID); + gr.addMember(auth); + save(uSession); + + fail("a common user should not be allowed to modify any groups."); + } catch (AccessDeniedException e) { + // success + } finally { + if (gr.removeMember(auth)) { + save(uSession); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.java new file mode 100644 index 00000000000..fed9c5b94eb --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/PasswordUtilityTest.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.core.security.user; + +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.test.JUnitTest; + +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * PasswordUtilityTest... + */ +public class PasswordUtilityTest extends JUnitTest { + + 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 + } + } + } + + 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)); + } + } + } + + 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 + } + } + + } + + public void testIsPlainTextPassword() throws Exception { + for (String pw : PLAIN_PWDS) { + assertTrue(pw + " should be plain text.", PasswordUtility.isPlainTextPassword(pw)); + } + } + + public void testIsPlainTextForNull() throws Exception { + assertTrue(PasswordUtility.isPlainTextPassword(null)); + } + + public void testIsPlainTextForPwHash() throws Exception { + for (String pwHash : HASHED_PWDS.values()) { + assertFalse(pwHash + " should not be plain text.", PasswordUtility.isPlainTextPassword(pwHash)); + } + } + + 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, SecurityConstants.DEFAULT_DIGEST, 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)); + } + + 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; + } + } + + public void testExtractAlgorithmFromPlainPw() throws Exception { + for (String pw : PLAIN_PWDS) { + assertNull(pw + " is no pw-hash -> no algorithm expected.", PasswordUtility.extractAlgorithm(pw)); + } + } + + public void testExtractAlgorithmFromNull() throws Exception { + assertNull("null pw -> no algorithm expected.", PasswordUtility.extractAlgorithm(null)); + } + + public void testExtractAlgorithmFromPwHash() throws Exception { + for (String pwHash : HASHED_PWDS.values()) { + String algorithm = PasswordUtility.extractAlgorithm(pwHash); + assertNotNull(pwHash + " is pw-hash -> algorithm expected.", algorithm); + assertEquals("Wrong algorithm extracted from " + pwHash, PasswordUtility.DEFAULT_ALGORITHM, algorithm); + } + + String pwHash = PasswordUtility.buildPasswordHash("pw", SecurityConstants.DEFAULT_DIGEST, 4, 50); + assertEquals(SecurityConstants.DEFAULT_DIGEST, PasswordUtility.extractAlgorithm(pwHash)); + + pwHash = PasswordUtility.buildPasswordHash("pw", "md5", 0, 5); + assertEquals("md5", PasswordUtility.extractAlgorithm(pwHash)); + + pwHash = PasswordUtility.buildPasswordHash("pw", "md5", -1, -1); + assertEquals("md5", PasswordUtility.extractAlgorithm(pwHash)); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.java new file mode 100644 index 00000000000..f1dc824a98b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TestAll.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.core.security.user; + +import org.apache.jackrabbit.test.ConcurrentTestSuite; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all test in this package. + * + * @return a Test suite that executes all test in this package. + */ + public static Test suite() { + TestSuite suite = new ConcurrentTestSuite("core.security.user tests"); + + suite.addTestSuite(UserManagerImplTest.class); + suite.addTestSuite(AuthorizableImplTest.class); + suite.addTestSuite(UserImplTest.class); + suite.addTestSuite(GroupImplTest.class); + suite.addTestSuite(ImpersonationImplTest.class); + suite.addTestSuite(AuthorizableActionTest.class); + + suite.addTestSuite(UserAdministratorTest.class); + suite.addTestSuite(NotUserAdministratorTest.class); + suite.addTestSuite(GroupAdministratorTest.class); + suite.addTestSuite(AdministratorTest.class); + + suite.addTestSuite(IndexNodeResolverTest.class); + suite.addTestSuite(TraversingNodeResolverTest.class); + + suite.addTestSuite(NodeCreationTest.class); + + suite.addTestSuite(UserImporterTest.class); + + suite.addTestSuite(UserAccessControlProviderTest.class); + suite.addTestSuite(DefaultPrincipalProviderTest.class); + + suite.addTestSuite(PasswordUtilityTest.class); + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TraversingNodeResolverTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TraversingNodeResolverTest.java new file mode 100644 index 00000000000..c6d56ef98c8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/TraversingNodeResolverTest.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.core.security.user; + +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; + +/** TraversingNodeResolverTest... */ +public class TraversingNodeResolverTest extends NodeResolverTest { + + private static Logger log = LoggerFactory.getLogger(TraversingNodeResolverTest.class); + + @Override + protected NodeResolver createNodeResolver(SessionImpl session) throws RepositoryException, NotExecutableException { + return new TraversingNodeResolver(session, session); + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserAccessControlProviderTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserAccessControlProviderTest.java new file mode 100644 index 00000000000..c1a0476b306 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserAccessControlProviderTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.core.security.authorization.CompiledPermissions; +import org.apache.jackrabbit.core.security.authorization.Permission; +import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import java.security.Principal; +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; + +/** + * UserAccessControlProviderTest... + */ +public class UserAccessControlProviderTest extends AbstractUserTest { + + private Session s; + private AccessControlProvider provider; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + RepositoryImpl repo = (RepositoryImpl) superuser.getRepository(); + String wspName = repo.getConfig().getSecurityConfig().getSecurityManagerConfig().getWorkspaceName(); + + s = getHelper().getSuperuserSession(wspName); + provider = new UserAccessControlProvider(); + provider.init(s, Collections.emptyMap()); + } + + @Override + protected void cleanUp() throws Exception { + if (provider != null) { + provider.close(); + } + if (s != null) { + s.logout(); + } + super.cleanUp(); + } + + private Set getAnonymousPrincipals() throws RepositoryException { + SessionImpl s = ((SessionImpl) getHelper().getRepository().login(new SimpleCredentials(SecurityConstants.ANONYMOUS_ID, "".toCharArray()))); + try { + return new HashSet(s.getSubject().getPrincipals()); + } finally { + s.logout(); + } + } + + /** + * @see JCR-2630 + */ + public void testNoNodeForPrincipal() throws RepositoryException { + final Principal testPrincipal = getTestPrincipal(); + String path = "/home/users/t/" + testPrincipal.getName(); + while (s.nodeExists(path)) { + path += "_"; + } + final String principalPath = path; + + List> principalSets = new ArrayList>(); + principalSets.add(Collections.singleton(testPrincipal)); + principalSets.add(Collections.singleton(new ItemBasedPrincipal() { + public String getPath() { + return principalPath; + } + public String getName() { + return testPrincipal.getName(); + } + })); + + Path rootPath = ((SessionImpl) s).getQPath("/"); + for (Set principals : principalSets) { + CompiledPermissions cp = provider.compilePermissions(principals); + + assertFalse(cp.canReadAll()); + assertFalse(cp.grants(rootPath, Permission.READ)); + assertTrue(cp.getPrivilegeSet(rootPath).isEmpty()); + assertSame(CompiledPermissions.NO_PERMISSION, cp); + } + } + + public void testNodeRemovedForPrincipal() throws RepositoryException, NotExecutableException { + Principal testPrincipal = getTestPrincipal(); + final User u = getUserManager(superuser).createUser(testPrincipal.getName(), "pw"); + save(superuser); + + Path rootPath = ((SessionImpl) s).getQPath("/"); + CompiledPermissions cp = null; + try { + Set principals = Collections.singleton(u.getPrincipal()); + cp = provider.compilePermissions(principals); + + assertTrue(cp.canReadAll()); + assertTrue(cp.grants(rootPath, Permission.READ)); + assertNotSame(CompiledPermissions.NO_PERMISSION, cp); + } finally { + + // remove the user to assert that the path doesn't point to an + // existing node any more -> userNode cannot be resolved any more -> permissions denied. + u.remove(); + save(superuser); + + if (cp != null) { + assertFalse(cp.canReadAll()); + assertFalse(cp.grants(rootPath, Permission.READ)); + assertTrue(cp.getPrivilegeSet(rootPath).isEmpty()); + } + } + } + + public void testAnonymousDefaultAccess() throws Exception { + Set anonymousPrincipals = getAnonymousPrincipals(); + + assertTrue(provider.canAccessRoot(anonymousPrincipals)); + + CompiledPermissions cp = provider.compilePermissions(anonymousPrincipals); + assertTrue(cp.canReadAll()); + assertFalse(CompiledPermissions.NO_PERMISSION.equals(cp)); + } + + public void testAnonymousAccessDenied() throws Exception { + Map config = new HashMap(); + config.put(UserAccessControlProvider.PARAM_ANONYMOUS_ACCESS, "false"); + + AccessControlProvider p2 = new UserAccessControlProvider(); + try { + p2.init(s, config); + + Set anonymousPrincipals = getAnonymousPrincipals(); + + assertFalse(p2.canAccessRoot(anonymousPrincipals)); + + CompiledPermissions cp = p2.compilePermissions(anonymousPrincipals); + try { + assertEquals(CompiledPermissions.NO_PERMISSION, cp); + assertFalse(cp.canReadAll()); + assertFalse(cp.grants(((NodeImpl) s.getRootNode()).getPrimaryPath(), Permission.READ)); + } finally { + cp.close(); + } + } finally { + p2.close(); + } + } + + public void testAnonymousAccessDenied2() throws Exception { + Map config = new HashMap(); + config.put(UserAccessControlProvider.PARAM_ANONYMOUS_ACCESS, "false"); + config.put(UserAccessControlProvider.PARAM_ANONYMOUS_ID, "abc"); + + AccessControlProvider p2 = new UserAccessControlProvider(); + try { + p2.init(s, config); + + Principal princ = new Principal() { + public String getName() { + return "abc"; + } + }; + Set anonymousPrincipals = Collections.singleton(princ); + + assertFalse(p2.canAccessRoot(anonymousPrincipals)); + + CompiledPermissions cp = p2.compilePermissions(anonymousPrincipals); + try { + assertEquals(CompiledPermissions.NO_PERMISSION, cp); + assertFalse(cp.canReadAll()); + assertFalse(cp.grants(((NodeImpl) s.getRootNode()).getPrimaryPath(), Permission.READ)); + } finally { + cp.close(); + } + } finally { + p2.close(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserAdministratorTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserAdministratorTest.java new file mode 100644 index 00000000000..ee5f269ce48 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserAdministratorTest.java @@ -0,0 +1,443 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +import org.apache.jackrabbit.api.security.user.Authorizable; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.Impersonation; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.security.Principal; +import java.util.Iterator; +import java.util.Set; +import java.util.Map; +import java.util.HashMap; + +/** + * UserAdministratorTest... + */ +public class UserAdministratorTest extends AbstractUserTest { + + // a test user + private String uID; + + // a test user being member of the user-admin group + private String otherUID; + private Session otherSession; + + // the user-admin group + private Group uAdministrators; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create a first user and retrieve the UserManager from the session + // created for that new user. + Principal p = getTestPrincipal(); + UserImpl u = (UserImpl) userMgr.createUser(p.getName(), buildPassword(p)); + save(superuser); + + uID = u.getID(); + + // create a second user + p = getTestPrincipal(); + String pw = buildPassword(p); + Credentials otherCreds = buildCredentials(p.getName(), pw); + User other = userMgr.createUser(p.getName(), pw); + save(superuser); + + otherUID = other.getID(); + + // make other user a user-administrator: + Authorizable ua = userMgr.getAuthorizable(UserConstants.USER_ADMIN_GROUP_NAME); + if (ua == null || !ua.isGroup()) { + throw new NotExecutableException("Cannot execute test. No user-administrator group found."); + } + uAdministrators = (Group) ua; + uAdministrators.addMember(other); + + // create a session for the other user. + otherSession = getHelper().getRepository().login(otherCreds); + } + + @Override + protected void tearDown() throws Exception { + try { + if (otherSession != null) { + otherSession.logout(); + } + } finally { + Authorizable a = userMgr.getAuthorizable(otherUID); + if (a != null) { + for (Iterator it = a.memberOf(); it.hasNext();) { + Group gr = it.next(); + if (!gr.getPrincipal().equals(EveryonePrincipal.getInstance())) { + gr.removeMember(a); + } + } + a.remove(); + } + a = userMgr.getAuthorizable(uID); + if (a != null) { + a.remove(); + } + save(superuser); + } + super.tearDown(); + } + + private Group getGroupAdminGroup(UserManager uMgr) throws RepositoryException, NotExecutableException { + Authorizable auth = uMgr.getAuthorizable(UserConstants.GROUP_ADMIN_GROUP_NAME); + if (auth == null || !auth.isGroup()) { + throw new NotExecutableException(); + } + return (Group) auth; + } + + public void testIsUserAdministrator() throws RepositoryException, NotExecutableException { + Set principals = getPrincipalSetFromSession(otherSession); + boolean isUserAdmin = false; + for (Iterator it = principals.iterator(); it.hasNext() && !isUserAdmin;) { + isUserAdmin = UserConstants.USER_ADMIN_GROUP_NAME.equals(it.next().getName()); + } + assertTrue(isUserAdmin); + } + + public void testCreateUser() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + UserImpl u = null; + // create a new user -> must succeed. + try { + Principal p = getTestPrincipal(); + u = (UserImpl) umgr.createUser(p.getName(), buildPassword(p)); + save(otherSession); + } finally { + if (u != null) { + u.remove(); + save(otherSession); + } + } + } + + public void testCreateUserWithIntermediatePath() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + UserImpl u = null; + + // create a new user with intermediate-path + // -> must succeed and user must be created + // -> intermediate path must be part of the nodes path + Principal p = getTestPrincipal(); + String usersPath = ((UserManagerImpl) umgr).getUsersPath(); + Map m = new HashMap(); + m.put("/some/intermediate/path", usersPath + "/some/intermediate/path/" + p.getName()); + m.put("some/intermediate/path", usersPath + "/some/intermediate/path/" + p.getName()); + m.put("/", usersPath + "/" + p.getName()); + m.put("", usersPath + "/" + p.getName()); + m.put(usersPath + "/some/intermediate/path", usersPath + "/some/intermediate/path/" + p.getName()); + + for (String intermediatePath : m.keySet()) { + try { + u = (UserImpl) umgr.createUser(p.getName(), buildPassword(p), p, intermediatePath); + save(otherSession); + + String expPath = m.get(intermediatePath); + assertEquals(expPath, u.getNode().getPath()); + } finally { + if (u != null) { + u.remove(); + save(otherSession); + } + } + } + } + + public void testCreateNestedUsers() throws NotExecutableException, RepositoryException { + UserManager umgr = getUserManager(otherSession); + UserImpl u = null; + + String invalidIntermediatePath = ((UserImpl) umgr.getAuthorizable(otherUID)).getNode().getPath(); + try { + Principal p = getTestPrincipal(); + u = (UserImpl) umgr.createUser(p.getName(), buildPassword(p), p, invalidIntermediatePath); + save(otherSession); + + fail("An attempt to create a user below an existing user must fail."); + } catch (RepositoryException e) { + // success + } finally { + if (u != null) { + u.remove(); + save(otherSession); + } + } + } + + public void testRemoveHimSelf() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + + Authorizable himself = umgr.getAuthorizable(otherUID); + try { + himself.remove(); + save(otherSession); + + fail("A UserAdministrator should not be allowed to remove himself."); + } catch (AccessDeniedException e) { + // success + } + } + + /** + * A member of 'usermanagers' must be able to remove another user. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testRemoveAnotherUser() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + + Authorizable user = umgr.getAuthorizable(uID); + user.remove(); + save(otherSession); + } + + public void testModifyImpersonationOfUser() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + Principal otherP = umgr.getAuthorizable(otherUID).getPrincipal(); + + // modify impersonation of new user + User u = null; + try { + Principal p = getTestPrincipal(); + u = umgr.createUser(p.getName(), buildPassword(p)); + save(otherSession); + + Impersonation impers = u.getImpersonation(); + assertFalse(impers.allows(buildSubject(otherP))); + + assertTrue(impers.grantImpersonation(otherP)); + save(otherSession); + + assertTrue(impers.allows(buildSubject(otherP))); + } finally { + // impersonation get removed while removing the user u. + if (u != null) { + u.remove(); + save(otherSession); + } + } + + // modify impersonation of another user + u = (User) umgr.getAuthorizable(uID); + Impersonation uImpl = u.getImpersonation(); + + if (!uImpl.allows(buildSubject(otherP))) { + // ... trying to modify 'impersonators of another user must succeed + assertTrue(uImpl.grantImpersonation(otherP)); + save(otherSession); + + assertTrue(uImpl.allows(buildSubject(otherP))); + + uImpl.revokeImpersonation(otherP); + save(otherSession); + } else { + throw new NotExecutableException("Cannot execute test. OtherP can already impersonate UID-user."); + } + } + + public void testModifyGroupForHimSelf() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + + User userHimSelf = (User) umgr.getAuthorizable(otherUID); + Group gr = getGroupAdminGroup(umgr); + try { + assertFalse(gr.addMember(userHimSelf)); + // conditional save call omitted. + } catch (RepositoryException e) { + // success as well. + } finally { + // clean up using the superuser + if (getGroupAdminGroup(userMgr).removeMember(userMgr.getAuthorizable(otherUID))) { + save(superuser); + } + } + } + + public void testModifyGroup() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + + User parentUser = (User) umgr.getAuthorizable(uID); + if (parentUser == null) { + throw new NotExecutableException(); + } else { + Group gr = getGroupAdminGroup(umgr); + try { + assertFalse("A UserAdmin must not be allowed to modify group memberships", gr.addMember(parentUser)); + } catch (RepositoryException e) { + // success + } + } + + Principal cp = getTestPrincipal(); + User childU = null; + try { + childU = umgr.createUser(cp.getName(), buildPassword(cp)); + save(otherSession); + + Group gr = getGroupAdminGroup(umgr); + try { + assertFalse("A UserAdmin must not be allowed to modify group " + + "memberships", gr.addMember(childU)); + // con-save call omitted. + } catch (RepositoryException e) { + // success + } + } finally { + if (childU != null) { + childU.remove(); + } + } + } + + public void testCreateGroup() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + String grId = null; + try { + Group testGroup = umgr.createGroup(getTestPrincipal()); + save(otherSession); + grId = testGroup.getID(); + + fail("UserAdmin should not be allowed to create a new Group."); + + } catch (RepositoryException e) { + // success. + } finally { + // let superuser clean up + if (grId != null) { + Authorizable gr = userMgr.getAuthorizable(grId); + if (gr != null) { + gr.remove(); + save(superuser); + } + } + } + } + + public void testCreateGroupWithIntermediatePath() throws RepositoryException, NotExecutableException { + UserManager umgr = getUserManager(otherSession); + String grId = null; + try { + Group testGroup = umgr.createGroup(getTestPrincipal(), "/any/intermediate/path"); + save(otherSession); + grId = testGroup.getID(); + + fail("UserAdmin should not be allowed to create a new Group with intermediate path."); + + } catch (RepositoryException e) { + // success. + } finally { + // let superuser clean up + if (grId != null) { + Authorizable gr = userMgr.getAuthorizable(grId); + if (gr != null) { + gr.remove(); + save(superuser); + } + } + } + } + + public void testRemoveGroup() throws NotExecutableException, RepositoryException { + UserManager umgr = getUserManager(otherSession); + Group g = null; + try { + g = userMgr.createGroup(getTestPrincipal()); + save(superuser); + + umgr.getAuthorizable(g.getID()).remove(); + save(otherSession); + + fail("UserAdmin should not be allowed to remove a Group."); + } catch (RepositoryException e) { + // success. + } finally { + if (g != null) { + g.remove(); + save(superuser); + } + } + } + + public void testAddToGroup() throws NotExecutableException, RepositoryException { + UserManager umgr = getUserManager(otherSession); + Group gr = getGroupAdminGroup(umgr); + + Authorizable auth = umgr.getAuthorizable(uID); + try { + assertFalse(gr.addMember(auth)); + // omit conditional save call. + } catch (AccessDeniedException e) { + // success as well. + } + + auth = umgr.getAuthorizable(otherUID); + try { + assertFalse(gr.addMember(auth)); + // omit conditional save call. + } catch (AccessDeniedException e) { + // success as well. + } + + // not even user-admin group + gr = (Group) umgr.getAuthorizable(uAdministrators.getID()); + auth = umgr.getAuthorizable(otherUID); + try { + assertFalse(gr.addMember(auth)); + // omit cond-save call. + } catch (AccessDeniedException e) { + // success + } + } + + public void testPersisted() throws NotExecutableException, RepositoryException { + UserManager umgr = getUserManager(otherSession); + UserImpl u = null; + // create a new user -> must succeed. + try { + Principal p = getTestPrincipal(); + u = (UserImpl) umgr.createUser(p.getName(), buildPassword(p)); + save(otherSession); + + Authorizable az = userMgr.getAuthorizable(u.getID()); + assertNotNull(az); + assertEquals(u.getID(), az.getID()); + } finally { + if (u != null) { + u.remove(); + save(otherSession); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.java new file mode 100644 index 00000000000..77b6f346a1b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImplTest.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.core.security.user; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.authentication.CryptedSimpleCredentials; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.value.StringValue; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; + +/** + * UserImplTest... + */ +public class UserImplTest extends AbstractUserTest { + + private String uID; + private Credentials creds; + private Session uSession; + private UserManager uMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Principal p = getTestPrincipal(); + String pw = buildPassword(p); + creds = new SimpleCredentials(p.getName(), pw.toCharArray()); + + User u = userMgr.createUser(p.getName(), pw); + save(superuser); + + uID = u.getID(); + uSession = getHelper().getRepository().login(creds); + uMgr = getUserManager(uSession); + } + + @Override + protected void tearDown() throws Exception { + try { + userMgr.getAuthorizable(uID).remove(); + save(superuser); + } finally { + uSession.logout(); + } + super.tearDown(); + } + + public void testUserImplHasCryptedSimplCredentials() throws RepositoryException, NotExecutableException { + User user = getTestUser(superuser); + Credentials creds = user.getCredentials(); + assertNotNull(creds); + + assertTrue(creds instanceof CryptedSimpleCredentials); + assertEquals(((CryptedSimpleCredentials) creds).getUserID(), user.getID()); + } + + public void testIsUser() throws RepositoryException { + Authorizable auth = uMgr.getAuthorizable(uID); + assertFalse(auth.isGroup()); + } + + public void testUserCanModifyItsOwnProperties() throws RepositoryException, NotExecutableException { + User u = (User) uMgr.getAuthorizable(uID); + if (u == null) { + fail("User " +uID+ "hast not been removed and must be visible to the Session created with its credentials."); + } + + if (!uSession.hasPermission(((UserImpl) u).getNode().getPath(), "set_property")) { + throw new NotExecutableException("Users should be able to modify their properties -> Check repository config."); + } + + // single valued properties + u.setProperty("Email", new StringValue("tu@security.test")); + save(uSession); + + assertNotNull(u.getProperty("Email")); + assertEquals("tu@security.test", u.getProperty("Email")[0].getString()); + + u.removeProperty("Email"); + save(uSession); + + assertNull(u.getProperty("Email")); + + // multivalued properties + u.setProperty(propertyName1, new Value[] {uSession.getValueFactory().createValue("anyValue")}); + save(uSession); + + assertNotNull(u.getProperty(propertyName1)); + + u.removeProperty(propertyName1); + save(uSession); + + assertNull(u.getProperty(propertyName1)); + } + + public void testCredentials() throws RepositoryException, NoSuchAlgorithmException, UnsupportedEncodingException { + User u = (User) userMgr.getAuthorizable(uID); + + Credentials uc = u.getCredentials(); + assertTrue(uc instanceof CryptedSimpleCredentials); + assertTrue(((CryptedSimpleCredentials) uc).matches((SimpleCredentials) creds)); + } + + public void testChangePassword() throws RepositoryException, NotExecutableException, NoSuchAlgorithmException, UnsupportedEncodingException { + User u = (User) userMgr.getAuthorizable(uID); + + String sha1Hash = "{" +SecurityConstants.DEFAULT_DIGEST+ "}" + Text.digest(SecurityConstants.DEFAULT_DIGEST, "abc".getBytes()); + String md5Hash = "{md5}" + Text.digest("md5", "abc".getBytes()); + + // valid passwords and the corresponding match + Map pwds = new HashMap(); + // plain text passwords + pwds.put("abc", "abc"); + pwds.put("{a}password", "{a}password"); + // passwords with hash-like char-sequence -> must still be hashed. + pwds.put(sha1Hash, sha1Hash); + pwds.put(md5Hash, md5Hash); + pwds.put("{"+SecurityConstants.DEFAULT_DIGEST+"}any", "{"+SecurityConstants.DEFAULT_DIGEST+"}any"); + pwds.put("{"+SecurityConstants.DEFAULT_DIGEST+"}", "{"+SecurityConstants.DEFAULT_DIGEST+"}"); + + for (String pw : pwds.keySet()) { + u.changePassword(pw); + + String plain = pwds.get(pw); + SimpleCredentials sc = new SimpleCredentials(u.getID(), plain.toCharArray()); + CryptedSimpleCredentials cc = (CryptedSimpleCredentials) u.getCredentials(); + + assertTrue(cc.matches(sc)); + } + + // valid passwords, non-matching plain text + MapnoMatch = new HashMap(); + noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}", ""); + noMatch.put("{"+SecurityConstants.DEFAULT_DIGEST+"}any", "any"); + noMatch.put(sha1Hash, "abc"); + noMatch.put(md5Hash, "abc"); + + for (String pw : noMatch.keySet()) { + u.changePassword(pw); + + String plain = noMatch.get(pw); + SimpleCredentials sc = new SimpleCredentials(u.getID(), plain.toCharArray()); + CryptedSimpleCredentials cc = (CryptedSimpleCredentials) u.getCredentials(); + + assertFalse(pw, cc.matches(sc)); + } + } + + public void testChangePasswordNull() throws RepositoryException { + User u = (User) userMgr.getAuthorizable(uID); + + // invalid 'null' pw string + try { + u.changePassword(null); + fail("invalid pw null"); + } catch (Exception e) { + // success + } + } + + public void testLoginWithCryptedCredentials() throws RepositoryException { + User u = (User) uMgr.getAuthorizable(uID); + + Credentials creds = u.getCredentials(); + assertTrue(creds instanceof CryptedSimpleCredentials); + + try { + Session s = getHelper().getRepository().login(u.getCredentials()); + s.logout(); + fail("Login using CryptedSimpleCredentials must fail."); + } catch (LoginException e) { + // success + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImporterTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImporterTest.java new file mode 100644 index 00000000000..fc395505c4d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserImporterTest.java @@ -0,0 +1,1827 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +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.AuthorizableExistsException; +import org.apache.jackrabbit.api.security.user.Group; +import org.apache.jackrabbit.api.security.user.Impersonation; +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.commons.xml.ParsingContentHandler; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.config.ImportConfig; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.core.security.user.UserImporter.ImportBehavior; +import org.apache.jackrabbit.core.security.user.action.AccessControlAction; +import org.apache.jackrabbit.core.security.user.action.AuthorizableAction; +import org.apache.jackrabbit.core.util.ReferenceChangeTracker; +import org.apache.jackrabbit.core.xml.ImportHandler; +import org.apache.jackrabbit.core.xml.ProtectedNodeImporter; +import org.apache.jackrabbit.core.xml.SessionImporter; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.InvalidItemStateException; +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.Workspace; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.retention.RetentionManager; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; +import javax.jcr.version.VersionException; +import javax.security.auth.Subject; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.AccessControlException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +/** + * UserImporterTest... + */ +public class UserImporterTest extends AbstractJCRTest { + + private SessionImpl sImpl; + private UserManagerImpl umgr; + + private String groupIdToRemove; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!(superuser instanceof SessionImpl)) { + throw new NotExecutableException("SessionImpl expected."); + } + + sImpl = (SessionImpl) superuser; + + UserManager umgr = sImpl.getUserManager(); + if (umgr.isAutoSave()) { + try { + umgr.autoSave(false); + } catch (RepositoryException e) { + // user manager cannot have the autosave behavior changed + // -> test not executable + throw new NotExecutableException("Expected user manager that can have its autosave behavior changed to false."); + } + } + this.umgr = (UserManagerImpl) umgr; + // avoid collision with testing a-folders that may have been created + // with another test (but not removed as user/groups got removed) + String path = this.umgr.getUsersPath() + "/t"; + if (sImpl.nodeExists(path)) { + sImpl.getNode(path).remove(); + } + path = this.umgr.getGroupsPath() + "/g"; + if (sImpl.nodeExists(path)) { + sImpl.getNode(path).remove(); + } + sImpl.save(); + + // make sure the target node for group-import exists + Authorizable administrators = umgr.getAuthorizable(SecurityConstants.ADMINISTRATORS_NAME); + if (administrators == null) { + groupIdToRemove = umgr.createGroup(new PrincipalImpl(SecurityConstants.ADMINISTRATORS_NAME)).getID(); + sImpl.save(); + } else if (!administrators.isGroup()) { + throw new NotExecutableException("Expected " + administrators.getID() + " to be a group."); + } + } + + @Override + protected void tearDown() throws Exception { + sImpl.refresh(false); + + if (umgr != null) { + String path = umgr.getUsersPath() + "/t"; + if (sImpl.nodeExists(path)) { + sImpl.getNode(path).remove(); + } + path = umgr.getGroupsPath() + "/g"; + if (sImpl.nodeExists(path)) { + sImpl.getNode(path).remove(); + } + if (groupIdToRemove != null) { + Authorizable a = umgr.getAuthorizable(groupIdToRemove); + if (a != null) { + a.remove(); + } + } + sImpl.save(); + } + super.tearDown(); + } + + private static UserImporter createImporter() { + return new UserImporter(); + } + + public void testWorkspaceImport() { + assertFalse("WorkspaceImport is not supported -> init should return false.", + createImporter().init(sImpl, sImpl, true, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new ReferenceChangeTracker())); } + + public void testUUIDBehaviorCreateNew() { + assertFalse("ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW is not supported -> init should return false.", + createImporter().init(sImpl, sImpl, false, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW, new ReferenceChangeTracker())); + } + + public void testDifferentUserManagerImpl() { + assertFalse("Only UserManagerImpl supported", + createImporter().init(new DummySession(), sImpl, false, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new ReferenceChangeTracker())); + } + + public void testInitTwice() { + UserImporter userImporter = createImporter(); + assertTrue(userImporter.init(sImpl, sImpl, false, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new ReferenceChangeTracker())); + try { + userImporter.init(sImpl, sImpl, false, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new ReferenceChangeTracker()); + fail("calling init twice must fail"); + } catch (IllegalStateException e) { + // success + } + } + + public void testImportUser() throws RepositoryException, IOException, SAXException, NotExecutableException { + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " t" + + " disabledUser" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + assertTrue(target.isModified()); + assertTrue(sImpl.hasPendingChanges()); + + Authorizable newUser = umgr.getAuthorizable("t"); + assertNotNull(newUser); + assertFalse(newUser.isGroup()); + assertEquals("t", newUser.getPrincipal().getName()); + assertEquals("t", newUser.getID()); + assertTrue(((User) newUser).isDisabled()); + assertEquals("disabledUser", ((User) newUser).getDisabledReason()); + + NodeImpl n = ((UserImpl) newUser).getNode(); + assertTrue(n.isNew()); + assertTrue(n.getParent().isSame(target)); + + assertEquals("t", n.getName()); + assertEquals("t", n.getProperty(UserConstants.P_PRINCIPAL_NAME).getString()); + assertEquals("{sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375", n.getProperty(UserConstants.P_PASSWORD).getString()); + assertEquals("disabledUser", n.getProperty(UserConstants.P_DISABLED).getString()); + + // saving changes of the import -> must succeed. add mandatory + // props should have been created. + sImpl.save(); + + } finally { + sImpl.refresh(false); + if (target.hasNode("t")) { + target.getNode("t").remove(); + sImpl.save(); + } + } + } + + public void testImportGroup() throws RepositoryException, IOException, SAXException, NotExecutableException { + String xml = "\n" + + "" + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " g" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + + assertTrue(target.isModified()); + assertTrue(sImpl.hasPendingChanges()); + + Authorizable newGroup = umgr.getAuthorizable("g"); + assertNotNull(newGroup); + assertTrue(newGroup.isGroup()); + assertEquals("g", newGroup.getPrincipal().getName()); + assertEquals("g", newGroup.getID()); + + NodeImpl n = ((GroupImpl) newGroup).getNode(); + assertTrue(n.isNew()); + assertTrue(n.getParent().isSame(target)); + + assertEquals("g", n.getName()); + assertEquals("g", n.getProperty(UserConstants.P_PRINCIPAL_NAME).getString()); + + // saving changes of the import -> must succeed. add mandatory + // props should have been created. + sImpl.save(); + + } finally { + sImpl.refresh(false); + if (target.hasNode("g")) { + target.getNode("g").remove(); + sImpl.save(); + } + } + } + + public void testImportGroupIntoUsersTree() throws RepositoryException, IOException, SAXException, NotExecutableException { + String xml = "\n" + + "" + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " g" + + ""; + + /* + importing a group below the users-path: + - nonProtected node rep:Group must be created. + - protected properties are ignored + - UserManager.getAuthorizable must return null. + - saving changes must fail with ConstraintViolationEx. + */ + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + assertTrue(target.isModified()); + assertTrue(sImpl.hasPendingChanges()); + + Authorizable newGroup = umgr.getAuthorizable("g"); + assertNull(newGroup); + + assertTrue(target.hasNode("g")); + assertFalse(target.hasProperty("g/rep:principalName")); + + // saving changes of the import -> must fail as mandatory prop is missing + try { + sImpl.save(); + fail("Import must be incomplete. Saving changes must fail."); + } catch (ConstraintViolationException e) { + // success + } + + } finally { + sImpl.refresh(false); + if (target.hasNode("g")) { + target.getNode("g").remove(); + sImpl.save(); + } + } + } + + public void testImportAuthorizableId() throws IOException, RepositoryException, SAXException, NotExecutableException { + // importing an authorizable with an jcr:uuid that doesn't match the + // hash of the given ID -> getAuthorizable(String id) will not find the + // authorizable. + //String calculatedUUID = "e358efa4-89f5-3062-b10d-d7316b65649e"; + String mismatchUUID = "a358efa4-89f5-3062-b10d-d7316b65649e"; + + String xml = "" + + "" + + " rep:User" + + " " +mismatchUUID+ "" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " t"; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + assertTrue(target.isModified()); + assertTrue(sImpl.hasPendingChanges()); + + // node must be present: + assertTrue(target.hasNode("t")); + NodeImpl n = (NodeImpl) target.getNode("t"); + assertEquals(mismatchUUID, n.getUUID()); + + // but UserManager.getAuthorizable(String) will not find the + // authorizable + Authorizable newUser = umgr.getAuthorizable("t"); + assertNull(newUser); + + } finally { + sImpl.refresh(false); + } + + } + + public void testExistingPrincipal() throws RepositoryException, NotExecutableException, IOException, SAXException { + Principal existing = null; + for (Principal p : sImpl.getSubject().getPrincipals()) { + if (umgr.getAuthorizable(p) != null) { + existing = p; + break; + } + } + if (existing == null) { + throw new NotExecutableException(); + } + + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " "+ existing.getName() +"" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + fail("Import must detect conflicting principals."); + } catch (SAXException e) { + // success + } finally { + sImpl.refresh(false); + } + } + + + public void testConflictingPrincipalsWithinImport() throws IOException, RepositoryException, NotExecutableException { + String xml = "\n" + + "" + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " g" + + "" + + "" + + " rep:Group" + + " 0120a4f9-196a-3f9e-b9f5-23f31f914da7" + + " g" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + fail("Import must detect conflicting principals."); + } catch (SAXException e) { + // success + } finally { + sImpl.refresh(false); + } + } + + public void testMultiValuedPrincipalName() throws RepositoryException, IOException, SAXException, NotExecutableException { + String xml = "\n" + + "" + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " gg2g"; + + /* + importing a group with a multi-valued rep:principalName property + - nonProtected node rep:Group must be created. + - property rep:principalName must be created regularly without being protected + - saving changes must fail with ConstraintViolationEx. as the protected + mandatory property rep:principalName is missing + */ + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + + assertTrue(target.isModified()); + assertTrue(sImpl.hasPendingChanges()); + + Authorizable newGroup = umgr.getAuthorizable("g"); + assertNotNull(newGroup); + + assertTrue(target.hasNode("g")); + assertTrue(target.hasProperty("g/rep:principalName")); + assertFalse(target.getProperty("g/rep:principalName").getDefinition().isProtected()); + + // saving changes of the import -> must fail as mandatory prop is missing + try { + sImpl.save(); + fail("Import must be incomplete. Saving changes must fail."); + } catch (ConstraintViolationException e) { + // success + } + + } finally { + sImpl.refresh(false); + if (target.hasNode("g")) { + target.getNode("g").remove(); + sImpl.save(); + } + } + } + + public void testPlainTextPassword() throws RepositoryException, IOException, SAXException, NotExecutableException { + String plainPw = "myPassword"; + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " "+plainPw+"" + + " t" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + assertTrue(target.isModified()); + assertTrue(sImpl.hasPendingChanges()); + + Authorizable newUser = umgr.getAuthorizable("t"); + NodeImpl n = ((UserImpl) newUser).getNode(); + + String pwValue = n.getProperty(UserConstants.P_PASSWORD).getString(); + assertFalse(plainPw.equals(pwValue)); + assertTrue(pwValue.startsWith("{sha1}")); + + } finally { + sImpl.refresh(false); + } + } + + public void testMultiValuedPassword() throws RepositoryException, IOException, SAXException, NotExecutableException { + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375{sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " t" + + ""; + /* + importing a user with a multi-valued rep:password property + - nonProtected node rep:User must be created. + - property rep:password must be created regularly without being protected + - saving changes must fail with ConstraintViolationEx. as the protected + mandatory property rep:password is missing + */ + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + assertTrue(target.isModified()); + assertTrue(sImpl.hasPendingChanges()); + + Authorizable newUser = umgr.getAuthorizable("t"); + assertNotNull(newUser); + + assertTrue(target.hasNode("t")); + assertTrue(target.hasProperty("t/rep:password")); + assertFalse(target.getProperty("t/rep:password").getDefinition().isProtected()); + + // saving changes of the import -> must fail as mandatory prop is missing + try { + sImpl.save(); + fail("Import must be incomplete. Saving changes must fail."); + } catch (ConstraintViolationException e) { + // success + } + + } finally { + sImpl.refresh(false); + if (target.hasNode("t")) { + target.getNode("t").remove(); + sImpl.save(); + } + } + } + + public void testIncompleteUser() throws RepositoryException, IOException, SAXException, NotExecutableException { + List incompleteXml = new ArrayList(); + incompleteXml.add("\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + ""); + incompleteXml.add("\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " t" + + ""); + + for (String xml : incompleteXml) { + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + // saving changes of the import -> must fail as mandatory prop is missing + try { + sImpl.save(); + fail("Import must be incomplete. Saving changes must fail."); + } catch (ConstraintViolationException e) { + // success + } + } finally { + sImpl.refresh(false); + if (target.hasNode("t")) { + target.getNode("t").remove(); + sImpl.save(); + } + } + } + } + + public void testIncompleteGroup() throws IOException, RepositoryException, SAXException, NotExecutableException { + String xml = "" + + "" + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + ""; + + /* + importing a group without rep:principalName property + - saving changes must fail with ConstraintViolationEx. + */ + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + // saving changes of the import -> must fail as mandatory prop is missing + try { + sImpl.save(); + fail("Import must be incomplete. Saving changes must fail."); + } catch (ConstraintViolationException e) { + // success + } + + } finally { + sImpl.refresh(false); + if (target.hasNode("g")) { + target.getNode("g").remove(); + sImpl.save(); + } + } + } + + public void testImportWithIntermediatePath() throws IOException, RepositoryException, SAXException, NotExecutableException { + String xml = "\n" + + "" + + " rep:AuthorizableFolder" + + " d5433be9-68d0-4fba-bf96-efc29f461993" + + "" + + " rep:AuthorizableFolder" + + " d87354a4-037e-4756-a8fb-deb2eb7c5149" + + "" + + " rep:AuthorizableFolder" + + " 24263272-b789-4568-957a-3bcaf99dbab3" + + "" + + " rep:User" + + " 0b8854ad-38f0-36c6-9807-928d28195609" + + " {sha1}4358694eeb098c6708ae914a10562ce722bbbc34" + + " t3" + + "" + + "" + + "" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + assertTrue(target.isModified()); + assertTrue(sImpl.hasPendingChanges()); + + Authorizable newUser = umgr.getAuthorizable("t3"); + assertNotNull(newUser); + assertFalse(newUser.isGroup()); + assertEquals("t3", newUser.getPrincipal().getName()); + assertEquals("t3", newUser.getID()); + + NodeImpl n = ((UserImpl) newUser).getNode(); + assertTrue(n.isNew()); + + Node parent = n.getParent(); + assertFalse(n.isSame(target)); + assertTrue(((NodeImpl) parent).isNodeType(UserConstants.NT_REP_AUTHORIZABLE_FOLDER)); + assertFalse(parent.getDefinition().isProtected()); + + assertTrue(target.hasNode("some")); + assertTrue(target.hasNode("some/intermediate/path")); + + } finally { + sImpl.refresh(false); + } + } + + public void testImportNewMembers() throws IOException, RepositoryException, SAXException, NotExecutableException { + String xml = "" + + "" + + "" + + " rep:AuthorizableFolder" + + "" + + "" + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " g" + + "" + + "" + + " rep:Group" + + " 0120a4f9-196a-3f9e-b9f5-23f31f914da7" + + " g1" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + "" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + + Group g = (Group) umgr.getAuthorizable("g"); + assertNotNull(g); + Group g1 = (Group) umgr.getAuthorizable("g1"); + assertNotNull(g1); + + NodeImpl n = ((AuthorizableImpl) g1).getNode(); + assertTrue(n.hasProperty(UserConstants.P_MEMBERS) || n.hasNode(UserConstants.N_MEMBERS)); + + // getWeakReferences only works upon save. + sImpl.save(); + + assertTrue(g1.isMember(g)); + + } finally { + sImpl.refresh(false); + if (target.hasNode("gFolder")) { + target.getNode("gFolder").remove(); + } + sImpl.save(); + } + } + + public void testImportNewMembersReverseOrder() throws IOException, RepositoryException, SAXException, NotExecutableException { + // group is imported before the member + String xml = "" + + "" + + " rep:AuthorizableFolder" + + " " + + " rep:Group" + + " 0120a4f9-196a-3f9e-b9f5-23f31f914da7" + + " g1" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " " + + " " + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " g" + + " " + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + + Group g = (Group) umgr.getAuthorizable("g"); + assertNotNull(g); + Group g1 = (Group) umgr.getAuthorizable("g1"); + assertNotNull(g1); + + NodeImpl n = ((AuthorizableImpl) g1).getNode(); + assertTrue(n.hasProperty(UserConstants.P_MEMBERS) || n.hasNode(UserConstants.N_MEMBERS)); + + // getWeakReferences only works upon save. + sImpl.save(); + + assertTrue(g1.isMember(g)); + + } finally { + sImpl.refresh(false); + if (target.hasNode("gFolder")) { + target.getNode("gFolder").remove(); + } + sImpl.save(); + } + } + + public void testImportMembers() throws RepositoryException, IOException, SAXException, NotExecutableException { + Authorizable admin = umgr.getAuthorizable("admin"); + if (admin == null) { + throw new NotExecutableException(); + } + + String uuid = ((AuthorizableImpl) admin).getNode().getUUID(); + String xml = "" + + "" + + " rep:AuthorizableFolder" + + " " + + " rep:Group" + + " 0120a4f9-196a-3f9e-b9f5-23f31f914da7" + + " g1" + + " " +uuid+ "" + + " " + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + + Group g1 = (Group) umgr.getAuthorizable("g1"); + assertNotNull(g1); + + // getWeakReferences only works upon save. + sImpl.save(); + + assertTrue(g1.isMember(admin)); + + boolean found = false; + for (Iterator it = admin.declaredMemberOf(); it.hasNext() && !found;) { + found = "g1".equals(it.next().getID()); + } + assertTrue(found); + + } finally { + sImpl.refresh(false); + target.getNode("gFolder").remove(); + sImpl.save(); + } + } + + public void testImportNonExistingMemberIgnore() throws IOException, RepositoryException, SAXException, NotExecutableException { + Node n = testRootNode.addNode(nodeName1, ntUnstructured); + n.addMixin(mixReferenceable); + + List invalid = new ArrayList(); + invalid.add(UUID.randomUUID().toString()); // random uuid + invalid.add(n.getUUID()); // uuid of non-authorizable node + + for (String id : invalid) { + String xml = "" + + "" + + " rep:AuthorizableFolder" + + "rep:Group" + + " 0120a4f9-196a-3f9e-b9f5-23f31f914da7" + + " g1" + + " " +id+ "" + + "" + + ""; + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + // there should be no exception during import, + // but invalid members must be ignored. + doImport(target, xml, UserImporter.ImportBehavior.IGNORE); + Authorizable a = umgr.getAuthorizable("g1"); + if (a.isGroup()) { + assertNotDeclaredMember((Group) a, id); + } else { + fail("'g1' was not imported as Group."); + } + } finally { + sImpl.refresh(false); + } + } + } + + public void testImportNonExistingMemberAbort() throws IOException, RepositoryException, SAXException, NotExecutableException { + Node n = testRootNode.addNode(nodeName1, ntUnstructured); + n.addMixin(mixReferenceable); + + List invalid = new ArrayList(); + invalid.add(UUID.randomUUID().toString()); // random uuid + invalid.add(n.getUUID()); // uuid of non-authorizable node + + for (String id : invalid) { + String xml = "" + + "" + + " rep:AuthorizableFolder" + + "rep:Group" + + " 0120a4f9-196a-3f9e-b9f5-23f31f914da7" + + " g1" + + " " +id+ "" + + "" + + ""; + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml, UserImporter.ImportBehavior.ABORT); + // import behavior ABORT -> should throw. + fail("importing invalid members -> must throw."); + } catch (SAXException e) { + // success as well + } finally { + sImpl.refresh(false); + } + } + } + + public void testImportNonExistingMemberBestEffort() throws IOException, RepositoryException, SAXException, NotExecutableException { + if (umgr.hasMemberSplitSize()) { + throw new NotExecutableException(); + } + + Node n = testRootNode.addNode(nodeName1, ntUnstructured); + n.addMixin(mixReferenceable); + + List invalid = new ArrayList(); + invalid.add(UUID.randomUUID().toString()); // random uuid + invalid.add(n.getUUID()); // uuid of non-authorizable node + + for (String id : invalid) { + String xml = "" + + "" + + " rep:AuthorizableFolder" + + "rep:Group" + + " 0120a4f9-196a-3f9e-b9f5-23f31f914da7" + + " g1" + + " " +id+ "" + + "" + + ""; + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + // BESTEFFORT behavior -> must import non-existing members. + doImport(target, xml, UserImporter.ImportBehavior.BESTEFFORT); + Authorizable a = umgr.getAuthorizable("g1"); + if (a.isGroup()) { + // the rep:members property must contain the invalid value + boolean found = false; + NodeImpl grNode = ((AuthorizableImpl) a).getNode(); + for (Value memberValue : grNode.getProperty(UserConstants.P_MEMBERS).getValues()) { + assertEquals(PropertyType.WEAKREFERENCE, memberValue.getType()); + if (id.equals(memberValue.getString())) { + found = true; + break; + } + } + assertTrue("ImportBehavior.BESTEFFORT must import non-existing members.",found); + + // declared members must not list the invalid entry. + assertNotDeclaredMember((Group) a, id); + } else { + fail("'g1' was not imported as Group."); + } + } finally { + sImpl.refresh(false); + } + } + } + + public void testImportNonExistingMemberBestEffort2() throws IOException, RepositoryException, SAXException, NotExecutableException { + + String g1Id = "0120a4f9-196a-3f9e-b9f5-23f31f914da7"; + String nonExistingId = "b2f5ff47-4366-31b6-a533-d8dc3614845d"; // groupId of 'g' group. + if (umgr.getAuthorizable("g") != null || umgr.hasMemberSplitSize()) { + throw new NotExecutableException(); + } + + String xml = "" + + "" + + " rep:AuthorizableFolder" + + "rep:Group" + + " " + g1Id + "" + + " g1" + + " " +nonExistingId+ "" + + "" + + ""; + + String xml2 = "" + + " " + + " rep:Group" + + " " + nonExistingId + "" + + " g" + + " " + g1Id + "" + + " "; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + // BESTEFFORT behavior -> must import non-existing members. + doImport(target, xml, UserImporter.ImportBehavior.BESTEFFORT); + Authorizable g1 = umgr.getAuthorizable("g1"); + if (g1.isGroup()) { + // the rep:members property must contain the invalid value + boolean found = false; + NodeImpl grNode = ((AuthorizableImpl) g1).getNode(); + for (Value memberValue : grNode.getProperty(UserConstants.P_MEMBERS).getValues()) { + assertEquals(PropertyType.WEAKREFERENCE, memberValue.getType()); + if (nonExistingId.equals(memberValue.getString())) { + found = true; + break; + } + } + assertTrue("ImportBehavior.BESTEFFORT must import non-existing members.",found); + } else { + fail("'g1' was not imported as Group."); + } + + /* + now try to import the 'g' group that has a circular group + membership references. + expected: + - group is imported + - circular membership is ignored + - g is member of g1 + - g1 isn't member of g + */ + target = (NodeImpl) target.getNode("gFolder"); + doImport(target, xml2, UserImporter.ImportBehavior.BESTEFFORT); + + Authorizable g = umgr.getAuthorizable("g"); + assertNotNull(g); + if (g.isGroup()) { + assertNotDeclaredMember((Group) g, g1Id); + } else { + fail("'g' was not imported as Group."); + } + + } finally { + sImpl.refresh(false); + } + } + + public void testImportSelfAsGroupIgnore() throws Exception { + + String invalidId = "0120a4f9-196a-3f9e-b9f5-23f31f914da7"; // uuid of the group itself + String xml = "" + + "" + + " rep:AuthorizableFolder" + + "rep:Group" + + " "+invalidId+"" + + " g1" + + " " +invalidId+ "" + + "" + + ""; + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml, UserImporter.ImportBehavior.IGNORE); + // no exception during import -> member must have been ignored though. + Authorizable a = umgr.getAuthorizable("g1"); + if (a.isGroup()) { + assertNotDeclaredMember((Group) a, invalidId); + } else { + fail("'g1' was not imported as Group."); + } + } finally { + sImpl.refresh(false); + } + } + + public void testImportSelfAsGroupAbort() throws Exception { + + String invalidId = "0120a4f9-196a-3f9e-b9f5-23f31f914da7"; // uuid of the group itself + String xml = "" + + "" + + " rep:AuthorizableFolder" + + "rep:Group" + + " "+invalidId+"" + + " g1" + + " " +invalidId+ "" + + "" + + ""; + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml, UserImporter.ImportBehavior.ABORT); + fail("Importing self as group with ImportBehavior.ABORT must fail."); + } catch (SAXException e) { + // success. + }finally { + sImpl.refresh(false); + } + } + + public void testImportImpersonation() throws IOException, RepositoryException, SAXException, NotExecutableException { + String xml = "" + + "" + + " rep:AuthorizableFolder" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " t" + + " g" + + "" + + "" + + " rep:User" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " g" + + "" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + Authorizable newUser = umgr.getAuthorizable("t"); + assertNotNull(newUser); + + Authorizable u2 = umgr.getAuthorizable("g"); + assertNotNull(u2); + + Subject subj = new Subject(); + subj.getPrincipals().add(u2.getPrincipal()); + + Impersonation imp = ((User) newUser).getImpersonation(); + assertTrue(imp.allows(subj)); + + } finally { + sImpl.refresh(false); + } + } + + public void testImportInvalidImpersonationIgnore() throws IOException, RepositoryException, SAXException, NotExecutableException { + List invalid = new ArrayList(); + invalid.add("anybody"); // an non-existing princ-name + invalid.add("administrators"); // a group + invalid.add("t"); // principal of the user itself. + + for (String principalName : invalid) { + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " t" +principalName+ "" + + ""; + Subject subj = new Subject(); + subj.getPrincipals().add(new PrincipalImpl(principalName)); + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml, UserImporter.ImportBehavior.IGNORE); + // no exception during import: no impersonation must be granted + // for the invalid principal name + Authorizable a = umgr.getAuthorizable("t"); + if (!a.isGroup()) { + Impersonation imp = ((User)a).getImpersonation(); + Subject s = new Subject(); + s.getPrincipals().add(new PrincipalImpl(principalName)); + assertFalse(imp.allows(s)); + for (PrincipalIterator it = imp.getImpersonators(); it.hasNext();) { + assertFalse(principalName.equals(it.nextPrincipal().getName())); + } + } else { + fail("Importing 't' didn't create a User."); + } + } finally { + sImpl.refresh(false); + } + } + } + + public void testImportInvalidImpersonationAbort() throws IOException, RepositoryException, SAXException, NotExecutableException { + List invalid = new ArrayList(); + invalid.add("anybody"); // an non-existing princ-name + invalid.add("administrators"); // a group + invalid.add("t"); // principal of the user itself. + + for (String principalName : invalid) { + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " t" +principalName+ "" + + ""; + Subject subj = new Subject(); + subj.getPrincipals().add(new PrincipalImpl(principalName)); + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml, UserImporter.ImportBehavior.ABORT); + fail("UserImporter.ImportBehavior.ABORT -> importing invalid impersonators must throw."); + } catch (SAXException e) { + // success + } finally { + sImpl.refresh(false); + } + } + } + + public void testImportUuidCollisionRemoveExisting() throws RepositoryException, IOException, SAXException { + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " t" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + // re-import should succeed if UUID-behavior is set accordingly + doImport(target, xml, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING, UserImporter.ImportBehavior.BESTEFFORT); + + // saving changes of the import -> must succeed. add mandatory + // props should have been created. + sImpl.save(); + + } finally { + sImpl.refresh(false); + if (target.hasNode("t")) { + target.getNode("t").remove(); + sImpl.save(); + } + } + } + + /** + * Same as {@link #testImportUuidCollisionRemoveExisting} with the single + * difference that the inital import is saved before being overwritten. + * + * @throws RepositoryException + * @throws IOException + * @throws SAXException + */ + public void testImportUuidCollisionRemoveExisting2() throws RepositoryException, IOException, SAXException { + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " t" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + sImpl.save(); + + // re-import should succeed if UUID-behavior is set accordingly + doImport(target, xml, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING, UserImporter.ImportBehavior.BESTEFFORT); + + // saving changes of the import -> must succeed. add mandatory + // props should have been created. + sImpl.save(); + + } finally { + sImpl.refresh(false); + if (target.hasNode("t")) { + target.getNode("t").remove(); + sImpl.save(); + } + } + } + + public void testImportUuidCollisionThrow() throws RepositoryException, IOException, SAXException { + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " t" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + doImport(target, xml, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, UserImporter.ImportBehavior.BESTEFFORT); + fail("UUID collision must be handled according to the uuid behavior."); + + } catch (SAXException e) { + assertTrue(e.getException() instanceof ItemExistsException); + // success. + } finally { + sImpl.refresh(false); + if (target.hasNode("t")) { + target.getNode("t").remove(); + sImpl.save(); + } + } + } + + public void testImportGroupMembersFromNodes() throws RepositoryException, IOException, SAXException { + String xml = "rep:AuthorizableFolder2010-08-17T18:22:20.086+02:00adminrep:AuthorizableFolder2010-08-17T18:22:20.086+02:00adminrep:Group08429aec-6f09-30db-8c83-1a2a57fc760c" + + "2010-08-17T18:22:20.086+02:00adminshrimpsrep:Membersrep:Membersrep:Membersc46335eb-267e-3e1c-9e5b-017acb4cd79921232f29-7a57-35a7-8389-4a0e4a801fc3rep:Membersa468b64f-b1df-377c-b325-20d97aaa1ad9294de355-7d9d-30b3-92d8-a1e6aab028cff08910b6-41c8-3cb9-a648-1dddd14b132drep:Membersrep:Membersd53bedf9-ebb8-3117-a8b8-162d32b4bee21795fa1a-3d20-3a64-996e-eaaeb520a01ea0d499c7-5105-3663-8611-a32779a571049ea4d671-8ed1-399a-8401-59487a14d00arep:Membersa9bcf1e4-d7b9-3a22-a297-5c812d938889dc3a8f16-70d6-3bea-a9b7-b65048a0ac40rep:Members9ec299fd-3461-3f1a-9749-92a76f2516eb16d5d24f-5b09-3199-9bd4-e5f57bf11237536931d8-0dec-318c-b3db-9612bdd004d4"; + + List createdUsers = new LinkedList(); + NodeImpl groupsNode = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + String[] users = {"angi", "adi", "hansi", "lisi", "luzi", "susi", "pipi", "hari", "gabi", "eddi", + "debbi", "cati", "admin", "anonymous"}; + + for (String user : users) { + if (umgr.getAuthorizable(user) == null) { + umgr.createUser(user, user); + createdUsers.add(user); + } + } + if (!umgr.isAutoSave()) { + sImpl.save(); + } + + doImport(groupsNode, xml); + if (!umgr.isAutoSave()) { + sImpl.save(); + } + + Authorizable aShrimps = umgr.getAuthorizable("shrimps"); + assertNotNull(aShrimps); + assertTrue(aShrimps.isGroup()); + + Group gShrimps = (Group) aShrimps; + for (String user : users) { + assertTrue(user + " should be member of " + gShrimps, gShrimps.isMember(umgr.getAuthorizable(user))); + } + + + } finally { + sImpl.refresh(false); + for (String user : createdUsers) { + Authorizable a = umgr.getAuthorizable(user); + if (a != null && !a.isGroup()) { + a.remove(); + } + } + if (!umgr.isAutoSave()) { + sImpl.save(); + } + for (NodeIterator it = groupsNode.getNodes(); it.hasNext(); ) { + it.nextNode().remove(); + } + if (!umgr.isAutoSave()) { + sImpl.save(); + } + } + } + + public void testImportGroupMembersFromNodesBestEffort() throws RepositoryException, IOException, SAXException { + String xml = "rep:AuthorizableFolder2010-08-17T18:22:20.086+02:00adminrep:AuthorizableFolder2010-08-17T18:22:20.086+02:00adminrep:Group08429aec-6f09-30db-8c83-1a2a57fc760c" + + "2010-08-17T18:22:20.086+02:00adminshrimpsrep:Membersrep:Membersrep:Membersc46335eb-267e-3e1c-9e5b-017acb4cd79921232f29-7a57-35a7-8389-4a0e4a801fc3rep:Membersa468b64f-b1df-377c-b325-20d97aaa1ad9294de355-7d9d-30b3-92d8-a1e6aab028cff08910b6-41c8-3cb9-a648-1dddd14b132drep:Membersrep:Membersd53bedf9-ebb8-3117-a8b8-162d32b4bee21795fa1a-3d20-3a64-996e-eaaeb520a01ea0d499c7-5105-3663-8611-a32779a571049ea4d671-8ed1-399a-8401-59487a14d00arep:Membersa9bcf1e4-d7b9-3a22-a297-5c812d938889dc3a8f16-70d6-3bea-a9b7-b65048a0ac40rep:Members9ec299fd-3461-3f1a-9749-92a76f2516eb16d5d24f-5b09-3199-9bd4-e5f57bf11237536931d8-0dec-318c-b3db-9612bdd004d4"; + + List createdUsers = new LinkedList(); + NodeImpl groupsNode = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + String[] users = {"angi", "adi", "hansi", "lisi", "luzi", "susi", "pipi", "hari", "gabi", "eddi", + "debbi", "cati", "admin", "anonymous"}; + + doImport(groupsNode, xml, UserImporter.ImportBehavior.BESTEFFORT); + if (!umgr.isAutoSave()) { + sImpl.save(); + } + + for (String user : users) { + if (umgr.getAuthorizable(user) == null) { + umgr.createUser(user, user); + createdUsers.add(user); + } + } + if (!umgr.isAutoSave()) { + sImpl.save(); + } + + Authorizable aShrimps = umgr.getAuthorizable("shrimps"); + assertNotNull(aShrimps); + assertTrue(aShrimps.isGroup()); + + Group gShrimps = (Group) aShrimps; + for (String user : users) { + assertTrue(user + " should be member of " + gShrimps, gShrimps.isMember(umgr.getAuthorizable(user))); + } + + + } finally { + sImpl.refresh(false); + for (String user : createdUsers) { + Authorizable a = umgr.getAuthorizable(user); + if (a != null && !a.isGroup()) { + a.remove(); + } + } + if (!umgr.isAutoSave()) { + sImpl.save(); + } + for (NodeIterator it = groupsNode.getNodes(); it.hasNext(); ) { + it.nextNode().remove(); + } + if (!umgr.isAutoSave()) { + sImpl.save(); + } + } + } + + public void testActionExecutionForUser() throws Exception { + TestAction testAction = new TestAction(); + + umgr.setAuthorizableActions(new AuthorizableAction[] {testAction}); + + // import user + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " pw" + + " tPrincipal" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + assertEquals(testAction.id, "t"); + assertEquals(testAction.pw, "pw"); + } finally { + sImpl.refresh(false); + } + } + + public void testActionExecutionForGroup() throws Exception { + TestAction testAction = new TestAction(); + + umgr.setAuthorizableActions(new AuthorizableAction[] {testAction}); + + // import group + String xml = "\n" + + "" + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " gPrincipal" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + assertEquals(testAction.id, "g"); + assertNull(testAction.pw); + } finally { + sImpl.refresh(false); + } + } + + public void testAccessControlActionExecutionForUser() throws Exception { + AccessControlAction a1 = new AccessControlAction(); + a1.setUserPrivilegeNames(Privilege.JCR_ALL); + + umgr.setAuthorizableActions(new AuthorizableAction[] {a1}); + + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + " tPrincipal" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + Authorizable a = umgr.getAuthorizable("t"); + assertNotNull(a); + assertFalse(a.isGroup()); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(a.getPath()); + assertNotNull(policies); + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof AccessControlList); + + AccessControlEntry[] aces = ((AccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(1, aces.length); + assertEquals("tPrincipal", aces[0].getPrincipal().getName()); + + } finally { + sImpl.refresh(false); + } + } + + public void testAccessControlActionExecutionForUser2() throws Exception { + AccessControlAction a1 = new AccessControlAction(); + a1.setUserPrivilegeNames(Privilege.JCR_ALL); + + umgr.setAuthorizableActions(new AuthorizableAction[] {a1}); + + String xml = "\n" + + "" + + " rep:User" + + " e358efa4-89f5-3062-b10d-d7316b65649e" + + " tPrincipal" + + " {sha1}8efd86fb78a56a5145ed7739dcb00c78581c5375" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getUsersPath()); + try { + doImport(target, xml); + + Authorizable a = umgr.getAuthorizable("t"); + assertNotNull(a); + assertFalse(a.isGroup()); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(a.getPath()); + assertNotNull(policies); + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof AccessControlList); + + AccessControlEntry[] aces = ((AccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(1, aces.length); + assertEquals("tPrincipal", aces[0].getPrincipal().getName()); + + } finally { + sImpl.refresh(false); + } + } + + public void testAccessControlActionExecutionForGroup() throws Exception { + AccessControlAction a1 = new AccessControlAction(); + a1.setGroupPrivilegeNames(Privilege.JCR_READ); + + umgr.setAuthorizableActions(new AuthorizableAction[] {a1}); + + String xml = "\n" + + "" + + " rep:Group" + + " b2f5ff47-4366-31b6-a533-d8dc3614845d" + + " gPrincipal" + + ""; + + NodeImpl target = (NodeImpl) sImpl.getNode(umgr.getGroupsPath()); + try { + doImport(target, xml); + + Authorizable a = umgr.getAuthorizable("g"); + assertNotNull(a); + assertTrue(a.isGroup()); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(a.getPath()); + assertNotNull(policies); + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof AccessControlList); + + AccessControlEntry[] aces = ((AccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(1, aces.length); + assertEquals("gPrincipal", aces[0].getPrincipal().getName()); + + } finally { + sImpl.refresh(false); + } + } + + //-------------------------------------------------------------------------- + + private void doImport(NodeImpl target, String xml) throws IOException, SAXException, RepositoryException { + InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8")); + SessionImporter importer = new SessionImporter(target, sImpl, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + } + + private void doImport(NodeImpl target, String xml, int importBehavior) throws IOException, SAXException, RepositoryException { + doImport(target, xml, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, importBehavior); + } + + private void doImport(NodeImpl target, String xml, int uuidBehavior, int importBehavior) throws IOException, SAXException, RepositoryException { + InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8")); + SessionImporter importer = new SessionImporter(target, sImpl, + uuidBehavior, new PseudoConfig(importBehavior)); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + } + + private static void assertNotDeclaredMember(Group gr, String potentialID) throws RepositoryException { + // declared members must not list the invalid entry. + Iterator it = gr.getDeclaredMembers(); + while (it.hasNext()) { + AuthorizableImpl member = (AuthorizableImpl) it.next(); + assertFalse(potentialID.equals(member.getNode().getIdentifier())); + } + } + + //-------------------------------------------------------------------------- + private final class PseudoConfig extends ImportConfig { + + private final UserImporter userImporter; + + private PseudoConfig() { + this.userImporter = createImporter(); + } + + private PseudoConfig(int importBehavior) { + this.userImporter = createImporter(); + (userImporter).setImportBehavior( + ImportBehavior.nameFromValue(importBehavior)); + } + + @Override + public List getProtectedItemImporters() { + return Collections.singletonList(userImporter); + } + + } + + private final class TestAction implements AuthorizableAction { + private String id; + private String pw; + + public void onCreate(Group group, Session session) throws RepositoryException { + id = group.getID(); + } + public void onCreate(User user, String password, Session session) throws RepositoryException { + id = user.getID(); + pw = password; + } + public void onRemove(Authorizable authorizable, Session session) throws RepositoryException { + // ignore + } + public void onPasswordChange(User user, String newPassword, Session session) throws RepositoryException { + pw = newPassword; + } + } + + private final class DummySession implements JackrabbitSession { + + private DummySession() { + } + + @Override + public boolean hasPermission(String absPath, String... actions) throws RepositoryException { + return false; + } + + public PrincipalManager getPrincipalManager() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public UserManager getUserManager() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + return new UserManager() { + + public Authorizable getAuthorizable(String id) throws RepositoryException { + return null; + } + + public T getAuthorizable(String id, Class authorizableClass) throws RepositoryException { + return null; + } + + public Authorizable getAuthorizable(Principal principal) throws RepositoryException { + return null; + } + + public Authorizable getAuthorizableByPath(String path) throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public Iterator findAuthorizables(String relPath, String value) throws RepositoryException { + return null; + } + + public Iterator findAuthorizables(String relPath, String value, int searchType) throws RepositoryException { + return null; + } + + public Iterator findAuthorizables(Query query) throws RepositoryException { + return null; + } + + public User createUser(String userID, String password) throws AuthorizableExistsException, RepositoryException { + return null; + } + + public User createUser(String userID, String password, Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException { + return null; + } + + public User createSystemUser(String userID, String intermediatePath) throws AuthorizableExistsException, RepositoryException { + return null; + } + + public Group createGroup(String groupID) throws AuthorizableExistsException, RepositoryException { + return null; + } + + public Group createGroup(Principal principal) throws AuthorizableExistsException, RepositoryException { + return null; + } + + public Group createGroup(Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException { + return null; + } + + public Group createGroup(String groupID, Principal principal, String intermediatePath) throws AuthorizableExistsException, RepositoryException { + return null; + } + + public boolean isAutoSave() { + return true; + } + + public void autoSave(boolean enable) throws UnsupportedRepositoryOperationException, RepositoryException { + } + }; + } + + public Item getItemOrNull(String absPath) throws RepositoryException { + return null; + } + + public Property getPropertyOrNull(String absPath) throws RepositoryException { + return null; + } + + public Node getNodeOrNull(String absPath) throws RepositoryException { + return null; + } + + public Repository getRepository() { + return null; + } + + public String getUserID() { + return null; + } + + public String[] getAttributeNames() { + return new String[0]; + } + + public Object getAttribute(String name) { + return null; + } + + public Workspace getWorkspace() { + return null; + } + + public Node getRootNode() throws RepositoryException { + return null; + } + + public Session impersonate(Credentials credentials) throws LoginException, RepositoryException { + return null; + } + + public Node getNodeByUUID(String uuid) throws ItemNotFoundException, RepositoryException { + return null; + } + + public Node getNodeByIdentifier(String id) throws ItemNotFoundException, RepositoryException { + return null; + } + + public Item getItem(String absPath) throws PathNotFoundException, RepositoryException { + return null; + } + + public Node getNode(String absPath) throws PathNotFoundException, RepositoryException { + return null; + } + + public Property getProperty(String absPath) throws PathNotFoundException, RepositoryException { + return null; + } + + public boolean itemExists(String absPath) throws RepositoryException { + return false; + } + + public boolean nodeExists(String absPath) throws RepositoryException { + return false; + } + + public boolean propertyExists(String absPath) throws RepositoryException { + return false; + } + + public void move(String srcAbsPath, String destAbsPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException { + } + + public void removeItem(String absPath) throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException { + } + + public void save() throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, ConstraintViolationException, InvalidItemStateException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException { + } + + public void refresh(boolean keepChanges) throws RepositoryException { + } + + public boolean hasPendingChanges() throws RepositoryException { + return false; + } + + public ValueFactory getValueFactory() throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public boolean hasPermission(String absPath, String actions) throws RepositoryException { + return false; + } + + public void checkPermission(String absPath, String actions) throws AccessControlException, RepositoryException { + } + + public boolean hasCapability(String methodName, Object target, Object[] arguments) throws RepositoryException { + return false; + } + + public ContentHandler getImportContentHandler(String parentAbsPath, int uuidBehavior) throws PathNotFoundException, ConstraintViolationException, VersionException, LockException, RepositoryException { + return null; + } + + public void importXML(String parentAbsPath, InputStream in, int uuidBehavior) throws IOException, PathNotFoundException, ItemExistsException, ConstraintViolationException, VersionException, InvalidSerializedDataException, LockException, RepositoryException { + } + + public void exportSystemView(String absPath, ContentHandler contentHandler, boolean skipBinary, boolean noRecurse) throws PathNotFoundException, SAXException, RepositoryException { + } + + public void exportSystemView(String absPath, OutputStream out, boolean skipBinary, boolean noRecurse) throws IOException, PathNotFoundException, RepositoryException { + } + + public void exportDocumentView(String absPath, ContentHandler contentHandler, boolean skipBinary, boolean noRecurse) throws PathNotFoundException, SAXException, RepositoryException { + } + + public void exportDocumentView(String absPath, OutputStream out, boolean skipBinary, boolean noRecurse) throws IOException, PathNotFoundException, RepositoryException { + } + + public void setNamespacePrefix(String prefix, String uri) throws NamespaceException, RepositoryException { + } + + public String[] getNamespacePrefixes() throws RepositoryException { + return new String[0]; + } + + public String getNamespaceURI(String prefix) throws NamespaceException, RepositoryException { + return null; + } + + public String getNamespacePrefix(String uri) throws NamespaceException, RepositoryException { + return null; + } + + public void logout() { + + } + + public boolean isLive() { + return false; + } + + public void addLockToken(String lt) { + + } + + public String[] getLockTokens() { + return new String[0]; + } + + public void removeLockToken(String lt) { + + } + + public AccessControlManager getAccessControlManager() throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public RetentionManager getRetentionManager() throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserManagerImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserManagerImplTest.java new file mode 100644 index 00000000000..0b8d3968bfa --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/security/user/UserManagerImplTest.java @@ -0,0 +1,857 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.security.user; + +import org.apache.jackrabbit.api.security.user.AbstractUserTest; +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.core.SessionImpl; +import org.apache.jackrabbit.core.security.TestPrincipal; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +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 java.util.Set; + +/** + * UserManagerImplTest... + */ +public class UserManagerImplTest extends AbstractUserTest { + + private String pPrincipalName; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!(userMgr instanceof UserManagerImpl)) { + throw new NotExecutableException("UserManagerImpl expected -> cannot perform test."); + } + NameResolver resolver = (SessionImpl) superuser; + pPrincipalName = resolver.getJCRName(UserConstants.P_PRINCIPAL_NAME); + } + + private String getTestUserId(Principal p) throws RepositoryException { + String hint = "UID" + p.getName(); + String userId = hint; + int i = 0; + while (userMgr.getAuthorizable(userId) != null) { + userId = hint + i++; + } + return userId; + } + + public void testPrincipalNameEqualsUserID() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + User u = null; + try { + u = userMgr.createUser(p.getName(), buildPassword(p)); + save(superuser); + + String msg = "Implementation specific: User.getID() must return the userID pass to createUser."; + assertEquals(msg, u.getID(), p.getName()); + } finally { + if (u != null) { + u.remove(); + save(superuser); + } + } + } + + public void testUserIDFromSession() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + User u = null; + Session uSession = null; + try { + String uid = p.getName(); + String pw = buildPassword(p); + u = userMgr.createUser(uid, pw); + save(superuser); + + 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(); + save(superuser); + } + } + } + + public void testCreateUserIdDifferentFromPrincipalName() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestUserId(p); + String pw = buildPassword(uid); + + User u = null; + Session uSession = null; + try { + u = userMgr.createUser(uid, pw, p, null); + save(superuser); + + 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(); + save(superuser); + } + } + } + + public void testCreateGroupWithInvalidIdOrPrincipal() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + Map fail = new HashMap(); + fail.put(uid, null); + fail.put(uid, new TestPrincipal("")); + 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+"' should fail"); + } catch (IllegalArgumentException e) { + // success + } finally { + if (g != null) { + g.remove(); + save(superuser); + } + } + } + } + + 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(); + } + } + + } + + public void testCreateGroupWithId() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + Group gr = null; + try { + + // assert group creation with exact ID + gr = userMgr.createGroup(uid); + save(superuser); + assertEquals("Expect group with exact ID", uid, gr.getID()); + + } finally { + if (gr != null) { + gr.remove(); + save(superuser); + } + } + } + + public void testCreateGroupWithIdAndPrincipal() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + Group gr = null; + try { + + // assert group creation with exact ID + gr = userMgr.createGroup(uid, p, null); + save(superuser); + + 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(); + save(superuser); + } + } + } + + public void testCreateGroupIdDifferentFromPrincipalName() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestUserId(p); + + assertFalse(uid.equals(p.getName())); + + Group g = null; + try { + g = userMgr.createGroup(uid, p, null); + save(superuser); + + String msg = "Creating a Group with principal-name distinct from Principal-name must succeed as long as both are unique."; + assertEquals(msg, g.getID(), uid); + assertEquals(msg, p.getName(), g.getPrincipal().getName()); + assertFalse(msg, g.getID().equals(g.getPrincipal().getName())); + + } finally { + if (g != null) { + g.remove(); + save(superuser); + } + } + } + + public void testCreatingGroupWithPrincipalMatchingExistingUserId() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestUserId(p); + + User u = null; + Group gr = null; + try { + u = userMgr.createUser(uid, buildPassword(uid), p, null); + save(superuser); + gr = userMgr.createGroup(new TestPrincipal(uid)); + save(superuser); + + String msg = "Creating a Group with a principal-name that exists as UserID -> must create new GroupID but keep PrincipalName."; + assertFalse(msg, gr.getID().equals(gr.getPrincipal().getName())); + assertFalse(msg, gr.getID().equals(uid)); + assertFalse(msg, gr.getID().equals(u.getID())); + assertEquals(msg, uid, gr.getPrincipal().getName()); + } finally { + if (u != null) { + u.remove(); + save(superuser); + } + if (gr != null) { + gr.remove(); + save(superuser); + } + } + } + + public void testCreateGroupWithExistingPrincipal() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + User u = null; + try { + // create a user with the given ID + u = userMgr.createUser(uid, buildPassword(uid), p, null); + save(superuser); + + // 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(); + save(superuser); + } + } + + } finally { + if (u != null) { + u.remove(); + save(superuser); + } + } + } + + public void testCreateGroupWithExistingPrincipal2() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestUserId(p); + + assertFalse(uid.equals(p.getName())); + + User u = null; + try { + // create a user with the given ID + u = userMgr.createUser(uid, buildPassword(uid), p, null); + save(superuser); + + // 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(); + save(superuser); + } + } + + } finally { + if (u != null) { + u.remove(); + save(superuser); + } + } + } + + public void testCreateGroupWithExistingPrincipal3() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestUserId(p); + + assertFalse(uid.equals(p.getName())); + + User u = null; + try { + // create a user with the given ID + u = userMgr.createUser(uid, buildPassword(uid), p, null); + save(superuser); + + // assert AuthorizableExistsException for principal that is already in use + Group gr = null; + try { + gr = userMgr.createGroup(getTestPrincipal().getName(), p, null); + fail("Principal " + p.getName() + " is already in use -> must throw AuthorizableExistsException."); + } catch (AuthorizableExistsException aee) { + // expected this + } finally { + if (gr != null) { + gr.remove(); + save(superuser); + } + } + + } finally { + if (u != null) { + u.remove(); + save(superuser); + } + } + } + + public void testCreateGroupWithExistingUserID() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestUserId(p); + + User u = null; + try { + // create a user with the given ID + u = userMgr.createUser(uid, buildPassword(uid), p, null); + save(superuser); + + // 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(); + save(superuser); + } + } + + } finally { + if (u != null) { + u.remove(); + save(superuser); + } + } + } + + public void testCreateGroupWithExistingGroupID() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestUserId(p); + + Group g = null; + try { + // create a user with the given ID + g = userMgr.createGroup(uid); + save(superuser); + + // 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(); + save(superuser); + } + } + + } finally { + if (g != null) { + g.remove(); + save(superuser); + } + } + } + + public void testCreateGroupWithExistingGroupID2() throws RepositoryException, NotExecutableException { + Principal p = getTestPrincipal(); + String uid = getTestUserId(p); + + Group g = null; + try { + // create a user with the given ID + g = userMgr.createGroup(uid, p, null); + save(superuser); + + // assert AuthorizableExistsException for id that is already in use + Group gr = null; + try { + gr = userMgr.createGroup(uid, getTestPrincipal(), null); + fail("ID " + uid + " is already in use -> must throw AuthorizableExistsException."); + } catch (AuthorizableExistsException aee) { + // expected this + } finally { + if (gr != null) { + gr.remove(); + save(superuser); + } + } + + } finally { + if (g != null) { + g.remove(); + save(superuser); + } + } + } + + public void testFindAuthorizable() throws RepositoryException, NotExecutableException { + Authorizable auth; + Set principals = getPrincipalSetFromSession(superuser); + for (Principal p : principals) { + auth = userMgr.getAuthorizable(p); + + if (auth != null) { + if (!auth.isGroup() && auth.hasProperty(pPrincipalName)) { + String val = auth.getProperty(pPrincipalName)[0].getString(); + Iterator users = userMgr.findAuthorizables(pPrincipalName, val); + + // the result must contain 1 authorizable + assertTrue(users.hasNext()); + Authorizable first = users.next(); + assertEquals(first.getID(), val); + + // since id is unique -> there should be no more users in + // the iterator left + assertFalse(users.hasNext()); + } + } + } + } + + 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")}); + save(superuser); + + 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(); + save(superuser); + } + } + } + + 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")); + save(superuser); + + // 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(); + save(superuser); + } + } + } + + public void testFindUser() throws RepositoryException, NotExecutableException { + User u = null; + try { + Principal p = getTestPrincipal(); + String uid = "UID" + p.getName(); + u = userMgr.createUser(uid, buildPassword(uid), p, null); + save(superuser); + + boolean found = false; + Iterator it = userMgr.findAuthorizables(pPrincipalName, 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(pPrincipalName, 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(pPrincipalName, p.getName(), UserManager.SEARCH_TYPE_GROUP); + assertFalse(it.hasNext()); + + it = userMgr.findAuthorizables(pPrincipalName, 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(); + save(superuser); + } + } + } + + public void testFindGroup() throws RepositoryException, NotExecutableException { + Group gr = null; + try { + Principal p = getTestPrincipal(); + gr = userMgr.createGroup(p); + save(superuser); + + boolean found = false; + Iterator it = userMgr.findAuthorizables(pPrincipalName, 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(pPrincipalName, 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(pPrincipalName, p.getName(), UserManager.SEARCH_TYPE_USER); + assertFalse(it.hasNext()); + + it = userMgr.findAuthorizables(pPrincipalName, 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(); + save(superuser); + } + } + } + + public void testFindAllUsers() throws RepositoryException { + Iterator it = userMgr.findAuthorizables(pPrincipalName, null, UserManager.SEARCH_TYPE_USER); + while (it.hasNext()) { + assertFalse(it.next().isGroup()); + } + } + + public void testFindAllGroups() throws RepositoryException { + Iterator it = userMgr.findAuthorizables(pPrincipalName, null, UserManager.SEARCH_TYPE_GROUP); + while (it.hasNext()) { + assertTrue(it.next().isGroup()); + } + } + + public void testNewUserCanLogin() throws RepositoryException, NotExecutableException { + String uid = getTestPrincipal().getName(); + String pw = buildPassword(uid); + + User u = null; + Session s = null; + try { + u = userMgr.createUser(uid, pw); + save(superuser); + + Credentials creds = new SimpleCredentials(uid, pw.toCharArray()); + s = superuser.getRepository().login(creds); + } finally { + if (u != null) { + u.remove(); + save(superuser); + } + if (s != null) { + s.logout(); + } + } + } + + public void testUnknownUserLogin() throws RepositoryException { + String uid = getTestPrincipal().getName(); + 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. + } + } + + 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(); + } + } + } + + 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(); + } + } + } + } + + /** + * Implementation specific test: user(/groups) cannot be nested. + * @throws RepositoryException + */ + public void testEnforceAuthorizableFolderHierarchy() throws RepositoryException { + AuthorizableImpl authImpl = (AuthorizableImpl) userMgr.getAuthorizable(superuser.getUserID()); + Node userNode = authImpl.getNode(); + SessionImpl sImpl = (SessionImpl) userNode.getSession(); + + Node folder = userNode.addNode("folder", sImpl.getJCRName(UserConstants.NT_REP_AUTHORIZABLE_FOLDER)); + String path = folder.getPath(); + try { + // authNode - authFolder -> create User + Authorizable a = null; + try { + Principal p = getTestPrincipal(); + a = userMgr.createUser(p.getName(), p.getName(), p, path); + fail("Users may not be nested."); + } catch (RepositoryException e) { + // success + } finally { + if (a != null) { + a.remove(); + } + } + } finally { + if (sImpl.nodeExists(path)) { + folder.remove(); + sImpl.save(); + } + } + + Node someContent = userNode.addNode("mystuff", "nt:unstructured"); + path = someContent.getPath(); + try { + // authNode - anyNode -> create User + Authorizable a = null; + try { + Principal p = getTestPrincipal(); + a = userMgr.createUser(p.getName(), p.getName(), p, someContent.getPath()); + fail("Users may not be nested."); + } catch (RepositoryException e) { + // success + } finally { + if (a != null) { + a.remove(); + a = null; + } + } + + // authNode - anyNode - authFolder -> create User + if (!sImpl.nodeExists(path)) { + someContent = userNode.addNode("mystuff", "nt:unstructured"); + } + folder = someContent.addNode("folder", sImpl.getJCRName(UserConstants.NT_REP_AUTHORIZABLE_FOLDER)); + sImpl.save(); // this time save node structure + try { + Principal p = getTestPrincipal(); + a = userMgr.createUser(p.getName(), p.getName(), p, folder.getPath()); + fail("Users may not be nested."); + } catch (RepositoryException e) { + // success + } finally { + if (a != null) { + a.remove(); + } + } + } finally { + if (sImpl.nodeExists(path)) { + someContent.remove(); + sImpl.save(); + } + } + } + + public void testCreateWithRelativePath() throws Exception { + Principal p = getTestPrincipal(); + String uid = p.getName(); + + String usersPath = ((UserManagerImpl) userMgr).getUsersPath(); + + List invalid = new ArrayList(); + invalid.add("../../path"); + invalid.add(usersPath + "/../test"); + + for (String path : invalid) { + try { + User user = userMgr.createUser(uid, buildPassword(uid), p, path); + save(superuser); + + fail("intermediate path may not point outside of the user tree."); + user.remove(); + save(superuser); + + } catch (Exception e) { + // success + assertNull(userMgr.getAuthorizable(uid)); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/AbstractISMLockingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/AbstractISMLockingTest.java new file mode 100644 index 00000000000..1e46c05318e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/AbstractISMLockingTest.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.core.state; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.core.id.ItemId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.core.state.ISMLocking.ReadLock; +import org.apache.jackrabbit.core.state.ISMLocking.WriteLock; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * AbstractISMLockingTest contains test cases for the ISMLocking requirements. + */ +public abstract class AbstractISMLockingTest extends TestCase { + + /** + * The {@link ISMLocking} instance under test. + */ + protected ISMLocking locking; + + /** + * Test node state. + */ + protected NodeState state; + + /** + * Node references instance targeting {@link #state}. + */ + protected NodeReferences refs; + + /** + * List of change logs, each with a different modification for {@link #state}. + */ + protected List logs; + + protected void setUp() throws Exception { + super.setUp(); + locking = createISMLocking(); + NodeId id = NodeId.randomId(); + state = new NodeState(id, NameConstants.NT_BASE, null, ItemState.STATUS_EXISTING, true); + refs = new NodeReferences(state.getNodeId()); + logs = new ArrayList(); + ChangeLog log = new ChangeLog(); + log.added(state); + logs.add(log); + log = new ChangeLog(); + log.deleted(state); + logs.add(log); + log = new ChangeLog(); + log.modified(state); + logs.add(log); + log = new ChangeLog(); + log.modified(refs); + logs.add(log); + } + + /** + * @return an {@link ISMLocking} instance which is exercised by the tests + */ + public abstract ISMLocking createISMLocking(); + + /** + * Checks the following requirement:

    While a read lock is held for a given item with id + * I an implementation must ensure that no write lock is issued for a change log that + * contains a reference to an item with id I. + * + * @throws InterruptedException on interruption; this will err the test + */ + public void testReadBlocksWrite() throws InterruptedException { + ReadLock rLock = locking.acquireReadLock(state.getId()); + for (ChangeLog changeLog : logs) { + verifyBlocked(startWriterThread(locking, changeLog)); + } + rLock.release(); + } + + /** + * Checks the following requirement:

    While a write lock is held for a given change log + * C an implementation must ensure that no read lock is issued for an item that is contained + * in C, unless the current thread is the owner of the write lock!

    The "unless" + * clause is tested by {@link #testWriteBlocksRead_notIfSameThread()} test. + * + * @throws InterruptedException on interruption; this will err the test + */ + public void testWriteBlocksRead() throws InterruptedException { + for (ChangeLog changeLog : logs) { + WriteLock wLock = locking.acquireWriteLock(changeLog); + verifyBlocked(startReaderThread(locking, state.getId())); + wLock.release(); + } + } + + public void testWriteBlocksRead_notIfSameThread() throws InterruptedException { + for (final ChangeLog changeLog : logs) { + Thread t = new Thread(new Runnable() { + public void run() { + try { + WriteLock wLock = locking.acquireWriteLock(changeLog); + locking.acquireReadLock(state.getId()).release(); + wLock.release(); + } catch (InterruptedException e) { + } + } + }); + t.start(); + verifyNotBlocked(t); + } + } + + /** + * Checks the following requirement:

    While a write lock is held for a given change log C + * an implementation must ensure that no write lock is issued for a change log C' that intersects + * with C. That is both change logs contain a reference to the same item. Please note that an + * implementation is free to block requests entirely for additional write lock while a write lock is + * active. It is not a requirement to support concurrent write locks. + * + * @throws InterruptedException on interruption; this will err the test + */ + public void testIntersectingWrites() throws InterruptedException { + ChangeLog cl = new ChangeLog(); + cl.added(state); + WriteLock wLock = locking.acquireWriteLock(cl); + for (ChangeLog changeLog : logs) { + verifyBlocked(startWriterThread(locking, changeLog)); + } + wLock.release(); + } + + /** + * Checks if a downgraded write lock allows other threads to read again. + * + * @throws InterruptedException on interruption; this will err the test + */ + public void testDowngrade() throws InterruptedException { + for (ChangeLog changeLog : logs) { + WriteLock wLock = locking.acquireWriteLock(changeLog); + verifyBlocked(startReaderThread(locking, state.getId())); + ReadLock rLock = wLock.downgrade(); + verifyNotBlocked(startReaderThread(locking, state.getId())); + rLock.release(); + } + } + + // ------------------------------< utilities >------------------------------- + + /** + * Creates and starts a thread that acquires and releases the write lock of the given + * ISMLocking for the given changelog. The thread's interrupted status is set if it was + * interrupted during the acquire-release sequence and could therefore not finish it. + * + * @param lock the ISMLocking to use + * @param changeLog the ChangeLog to use + * @return a thread that has been started + */ + protected final Thread startWriterThread(final ISMLocking lock, final ChangeLog changeLog) { + Thread t = new Thread(new Runnable() { + + public void run() { + try { + lock.acquireWriteLock(changeLog).release(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + t.start(); + return t; + } + + /** + * Creates and starts an thread that acquires and releases the read lock of the given + * ISMLocking for the given id. The thread's interrupted status is set if it was interrupted + * during the acquire-release sequence and could therefore not finish it. + * + * @param lock the ISMLocking to use + * @param id the id to use + * @return a thread that has been started + */ + protected final Thread startReaderThread(final ISMLocking lock, final ItemId id) { + Thread t = new Thread(new Runnable() { + + public void run() { + try { + lock.acquireReadLock(id).release(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + t.start(); + return t; + } + + /** + * Verifies that the given thread is blocked. Then it interrupts the thread and waits a certain amount of + * time for it to complete. (If it doesn't complete within that time then the test that calls this method + * fails). + * + * @param other a started thread + * @throws InterruptedException on interruption + */ + protected final void verifyBlocked(final Thread other) throws InterruptedException { + Thread.sleep(100); + assertTrue(other.isAlive()); + other.interrupt(); + other.join(100); + assertFalse(other.isAlive()); + } + + /** + * Verifies that the given thread is not blocked and runs to completion within a certain amount of time + * and without interruption. (If it doesn't complete within that time without interruption then the test + * that calls this method fails). + * + * @param other a started thread + * @throws InterruptedException on interruption + */ + protected final void verifyNotBlocked(final Thread other) throws InterruptedException { + other.join(1000); + assertFalse(other.isInterrupted()); + assertFalse(other.isAlive()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/ChangeLogTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/ChangeLogTest.java new file mode 100644 index 00000000000..a68d9fa30e2 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/ChangeLogTest.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.core.state; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.core.id.PropertyId; +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.NameFactory; + +/** + * ChangeLogTest contains the test cases for the methods + * inside {@link org.apache.jackrabbit.core.state.ChangeLog}. + */ +public class ChangeLogTest extends AbstractJCRTest { + + private NameFactory factory; + + protected void setUp() throws Exception { + super.setUp(); + factory = NameFactoryImpl.getInstance(); + } + + /** + * Add an item state and then delete it. Make sure there is no + * entry in either the added nor the removed states + */ + public void testAddDelete() throws Exception { + PropertyId id = new PropertyId(NodeId.randomId(), factory.create("", "a")); + ItemState state = new PropertyState(id, ItemState.STATUS_NEW, false); + + ChangeLog log = new ChangeLog(); + + log.added(state); + log.deleted(state); + + assertFalse("State not in added collection", + log.addedStates().iterator().hasNext()); + assertFalse("State not in deleted collection", + log.deletedStates().iterator().hasNext()); + } + + /** + * Add an item state and then modify it. Make sure the entry is still + * in the added states. + */ + public void testAddModify() throws Exception { + PropertyId id = new PropertyId(NodeId.randomId(), factory.create("", "a")); + ItemState state = new PropertyState(id, ItemState.STATUS_NEW, false); + + ChangeLog log = new ChangeLog(); + + log.added(state); + log.modified(state); + + assertTrue("State still in added collection", + log.addedStates().iterator().hasNext()); + assertFalse("State not in modified collection", + log.modifiedStates().iterator().hasNext()); + } + + /** + * Add some item states. Retrieve them again and make sure the order is + * preserved. + */ + public void testPreserveOrder() throws Exception { + ItemState[] states = new ItemState[10]; + for (int i = 0; i < states.length; i++) { + PropertyId id = new PropertyId(NodeId.randomId(), factory.create("", "a" + i)); + states[i] = new PropertyState(id, ItemState.STATUS_NEW, false); + } + + ChangeLog log = new ChangeLog(); + + for (int i = 0; i < states.length; i++) { + log.added(states[i]); + } + + int i = 0; + for (ItemState state : log.addedStates()) { + assertTrue("Added states preserve order.", + state.equals(states[i++])); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/DefaultISMLockingDeadlockTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/DefaultISMLockingDeadlockTest.java new file mode 100644 index 00000000000..09e74ddd217 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/DefaultISMLockingDeadlockTest.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.core.state; + +import org.apache.jackrabbit.core.state.DefaultISMLocking; +import org.apache.jackrabbit.core.state.ISMLocking; +import org.apache.jackrabbit.core.state.ISMLocking.ReadLock; +import org.apache.jackrabbit.core.state.ISMLocking.WriteLock; +import org.apache.jackrabbit.test.JUnitTest; + +/** + * Tests the DefaultISMLocking class. + */ +public class DefaultISMLockingDeadlockTest extends JUnitTest { + + public void test() throws InterruptedException { + final ISMLocking lock = new DefaultISMLocking(); + WriteLock w1 = lock.acquireWriteLock(null); + ReadLock r1 = w1.downgrade(); + final InterruptedException[] ex = new InterruptedException[1]; + Thread thread = new Thread() { + public void run() { + try { + lock.acquireWriteLock(null).release(); + } catch (InterruptedException e) { + ex[0] = e; + } + } + }; + thread.start(); + Thread.sleep(100); + lock.acquireReadLock(null).release(); + r1.release(); + thread.join(); + if (ex[0] != null) { + throw ex[0]; + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/DefaultISMLockingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/DefaultISMLockingTest.java new file mode 100644 index 00000000000..96f2bf1a6e9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/DefaultISMLockingTest.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.core.state; + +/** + * DefaultISMLockingTest executes the test cases implemented in + * {@link AbstractISMLockingTest}. + */ +public class DefaultISMLockingTest extends AbstractISMLockingTest { + + public ISMLocking createISMLocking() { + return new DefaultISMLocking(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/FineGrainedISMLockingTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/FineGrainedISMLockingTest.java new file mode 100644 index 00000000000..2d22fd377a9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/FineGrainedISMLockingTest.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.core.state; + +/** + * FineGrainedISMLockingTest executes the test cases implemented in + * {@link AbstractISMLockingTest}. + */ +public class FineGrainedISMLockingTest extends AbstractISMLockingTest { + + public ISMLocking createISMLocking() { + return new FineGrainedISMLocking(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NameSetTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NameSetTest.java new file mode 100644 index 00000000000..335c714ad47 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NameSetTest.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.core.state; + +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; + +import junit.framework.TestCase; + +/** + * NameSetTest checks if equals and hashCode semantics are correct + * on {@link NameSet}. + */ +public class NameSetTest extends TestCase { + + private static final NameFactory FACTORY = NameFactoryImpl.getInstance(); + + private static final String NAME_STRING = "{}foo"; + + public void testEquals() { + NameSet set1 = new NameSet(); + NameSet set2 = new NameSet(); + assertEquals(set1, set2); + + set1.add(FACTORY.create(NAME_STRING)); + set2.add(FACTORY.create(NAME_STRING)); + assertEquals(set1, set2); + } + + public void testHashCode() { + NameSet set1 = new NameSet(); + NameSet set2 = new NameSet(); + assertEquals(set1.hashCode(), set2.hashCode()); + + set1.add(FACTORY.create(NAME_STRING)); + set2.add(FACTORY.create(NAME_STRING)); + assertEquals(set1.hashCode(), set2.hashCode()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java new file mode 100644 index 00000000000..f93794589f4 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/NodeStateMergerTest.java @@ -0,0 +1,978 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.state; + +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * NodeStateMergerTest... + */ +public class NodeStateMergerTest extends AbstractJCRTest { + + /** Name of the cnd nodetype file for import and namespace registration. */ + private static final String TEST_NODETYPES = "org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd"; + + private List testMixins = new ArrayList(); + + private Node testNode; + + private Session sessionB; + private Node testNodeB; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Reader cnd = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(TEST_NODETYPES)); + CndImporter.registerNodeTypes(cnd, superuser); + cnd.close(); + + NodeTypeIterator it = superuser.getWorkspace().getNodeTypeManager().getMixinNodeTypes(); + while (it.hasNext()) { + NodeType nt = it.nextNodeType(); + if (nt.getName().startsWith("test:")) { + testMixins.add(nt.getName()); + } + } + + testNode = testRootNode.addNode(nodeName1, "nt:unstructured"); + superuser.save(); + + sessionB = getHelper().getSuperuserSession(); + testNodeB = sessionB.getNode(testNode.getPath()); + } + + @Override + protected void tearDown() throws Exception { + if (sessionB != null) { + sessionB.logout(); + } + super.tearDown(); + } + + /** + * Add the same property with both sessions but with different values. + * + * @throws RepositoryException + */ + public void testAddSamePropertiesWithDifferentValues() throws RepositoryException { + assertFalse(testNode.hasProperty(propertyName2)); + assertFalse(testNodeB.hasProperty(propertyName2)); + + testNode.setProperty(propertyName2, "value"); + + testNodeB.setProperty(propertyName2, "otherValue"); + sessionB.save(); + + superuser.save(); + + assertEquals("value", testNode.getProperty(propertyName2).getString()); + testNodeB.refresh(false); + assertEquals("value", testNodeB.getProperty(propertyName2).getString()); + } + + /** + * Modify the same property with both sessions but with different values. + * + * @throws RepositoryException + */ + public void testModifySamePropertiesWithDifferentValues() throws RepositoryException { + testNode.setProperty(propertyName2, "test"); + superuser.save(); + testNodeB.refresh(false); + + assertTrue(testNodeB.hasProperty(propertyName2)); + + try { + testNode.setProperty(propertyName2, "value"); + + testNodeB.setProperty(propertyName2, "otherValue"); + sessionB.save(); + + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + //------------------ Tests adding jcr:mixinType property in the session1 --- + /** + * Both node don't have any mixins assigned yet. Test if adding the same + * mixin node with both sessions works. + * + * @throws RepositoryException + */ + public void testAddSameMixinToSessionsAB() throws RepositoryException { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addMixin(mixReferenceable); + + testNodeB.addMixin(mixReferenceable); + sessionB.save(); + + superuser.save(); + } + + /** + * Same as {@link #testAddSameMixinToSessionsAB} but in addition + * adding non-conflicting properties defined by this mixin. The properties + * must be merged silently. + * + * @throws RepositoryException + */ + public void testAddSameMixinToSessionsAB2() throws RepositoryException { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addMixin("test:mixinProp_1"); + testNode.setProperty("test:prop_double", 124); + + testNodeB.addMixin("test:mixinProp_1"); + testNodeB.setProperty("test:prop_string", "abc"); + sessionB.save(); + + superuser.save(); + + assertEquals("abc", testNode.getProperty("test:prop_string").getString()); + testNodeB.refresh(false); + assertEquals(124, testNodeB.getProperty("test:prop_double").getLong()); + } + + /** + * Same as {@link #testAddSameMixinToSessionsAB} but in addition + * a property defined by this mixin with different value. Value of overlayed + * property (from sessionB) must be merged into sessionA. + * + * @throws RepositoryException + */ + public void testAddSameMixinToSessionsAB3() throws RepositoryException { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addMixin("test:mixinProp_1"); + testNode.setProperty("test:prop_double", 124); + + testNodeB.addMixin("test:mixinProp_1"); + testNodeB.setProperty("test:prop_double", 134); + sessionB.save(); + + superuser.save(); + + assertEquals(124, testNode.getProperty("test:prop_double").getLong()); + testNodeB.refresh(false); + assertEquals(124, testNodeB.getProperty("test:prop_double").getLong()); + } + + /** + * Same as {@link #testAddSameMixinToSessionsAB3} having additional + * modifications in the overlayed state (modifications by session B). + * + * @throws RepositoryException + */ + public void testAddSameMixinToSessionsAB4() throws RepositoryException { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addMixin("test:mixinProp_1"); + testNode.setProperty("test:prop_double", 124); + + testNodeB.addMixin("test:mixinProp_1"); + testNodeB.setProperty("test:prop_double", 134); + testNodeB.setProperty("more", "yes"); + sessionB.save(); + + superuser.save(); + + assertEquals(124, testNode.getProperty("test:prop_double").getLong()); + testNodeB.refresh(false); + assertEquals(124, testNodeB.getProperty("test:prop_double").getLong()); + } + + /** + * Test adding mixin(s) to sessionA while the overlayed + * (attached to testNode2, sessionB) doesn't have any mixins defined. + * The changes should be merged as there is no conflict. + * + * @throws RepositoryException + */ + public void testMixinAddedInSessionA() throws RepositoryException { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + for (String mixin : testMixins) { + testNode.addMixin(mixin); + } + + testNodeB.addNode(nodeName1, "nt:unstructured"); + testNodeB.setProperty(propertyName1, "anyValue"); + sessionB.save(); + + superuser.save(); + + assertTrue(testNode.hasNode(nodeName1)); + assertTrue(testNode.hasProperty(propertyName1)); + + testNodeB.refresh(false); + for (String mixin : testMixins) { + assertTrue(testNodeB.isNodeType(mixin)); + } + } + + /** + * Test adding mixin(s) to sessionA, while the other sessionB + * doesn't have any mixins defined. The changes should be merged as there + * is no conflict. + * + * @throws RepositoryException + */ + public void testMixinAddedInSessionA2() throws RepositoryException { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addMixin("test:mixinProp_1"); + + testNodeB.setProperty("test:prop_double", sessionB.getValueFactory().createValue(false)); + sessionB.save(); + + superuser.save(); + assertTrue(testNode.isNodeType("test:mixinProp_1")); + assertEquals(PropertyType.BOOLEAN, testNode.getProperty("test:prop_double").getType()); + + testNodeB.refresh(false); + assertTrue(testNodeB.isNodeType("test:mixinProp_1")); + assertEquals(PropertyType.BOOLEAN, testNodeB.getProperty("test:prop_double").getType()); + assertEquals(testNode.getProperty("test:prop_double").getString(), testNodeB.getProperty("test:prop_double").getString()); + assertEquals("nt:unstructured", testNode.getProperty("test:prop_double").getDefinition().getDeclaringNodeType().getName()); + } + + /** + * Test adding mixin(s) to sessionA while sessionB doesn't add new mixins + * but has new child items that collide with the items defined by the new mixin. + * The merge should in this case fail. + * + * @throws RepositoryException + */ + public void testAddedInSessionAConflictsWithChildItemsInSessionB() throws RepositoryException { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addMixin("test:mixinProp_5"); // has an autocreated property + + testNodeB.setProperty("test:prop_long_p", "conflict"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + assertFalse(testNodeB.isNodeType("test:mixinProp_5")); + testNodeB.refresh(false); + assertEquals("conflict", testNodeB.getProperty("test:prop_long_p").getString()); + } + } + + /** + * Similar to {@link #testAddedInSessionAConflictsWithChildItemsInSessionB} + * but adding mix:referenceable in the SessionA while SessionB + * gets a property jcr:uuid (with different type). + * + * @throws RepositoryException + */ +// TODO: uncomment once JCR-2779 is fixed +// public void testAddedReferenceableSessionAConflictsWithPropInSessionB() throws RepositoryException { +// assertFalse(testNode.hasProperty("jcr:mixinTypes")); +// +// testNode.addMixin(mixReferenceable); +// +// Property p = testNode2.setProperty("jcr:uuid", session2.getValueFactory().createValue(false)); +// assertTrue(testNode2.hasProperty("jcr:uuid")); +// assertTrue(p.isNew()); +// session2.save(); +// +// assertTrue(testNode2.hasProperty("jcr:uuid")); +// assertTrue(p.isNew()); +// try { +// superuser.save(); +// fail(); +// } catch (InvalidItemStateException e) { +// assertTrue(testNode.isNodeType(mixReferenceable)); +// assertEquals(PropertyType.STRING, testNode.getProperty("jcr:uuid").getType()); +// } +// } + + /** + * Same as {@link #testAddedInSessionAConflictsWithChildItemsInSessionB} + * but in addition the overlayed state gets an addition child node. + * + * @throws RepositoryException + */ +// TODO: uncomment once JCR-2779 is fixed +// public void testAddedReferenceableSessionAConflictsWithPropInSessionB2() throws RepositoryException { +// assertFalse(testNode.hasProperty("jcr:mixinTypes")); +// +// testNode.addMixin(mixReferenceable); +// +// testNode2.setProperty("jcr:uuid", session2.getValueFactory().createValue(false)); +// testNode2.addNode("test"); +// session2.save(); +// +// assertTrue(testNode2.hasProperty("jcr:uuid")); +// assertTrue(testNode2.getProperty("jcr:uuid").isNew()); +// try { +// superuser.save(); +// fail(); +// } catch (InvalidItemStateException e) { +// assertTrue(testNode.isNodeType(mixReferenceable)); +// assertEquals(PropertyType.STRING, testNode.getProperty("jcr:uuid").getType()); +// } +// } + + /** + * Adding different node types in the SessionA and SessionB: + * Not handled and thus the merge must fail. + * + * @throws RepositoryException + */ + public void testDifferentMixinAddedInSessionAB() throws Exception { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addMixin("test:mixinProp_1"); + + testNodeB.addMixin("test:mixinProp_3"); + sessionB.save(); + + try { + superuser.save(); + fail("Different mixin types added both in SessionA and SessionB. InvalidItemStateException expected."); + } catch (InvalidItemStateException e) { + // expected + assertFalse(testNode.isNodeType("test:mixinProp_3")); + } + } + + //---------------- Tests removing jcr:mixinType property in the SessionA --- + /** + * Remove the jcr:mixinType property by removing all mixin types. Merge + * to changes made in the overlayed state should fail. + * @throws Exception + */ + public void testMixinRemovedInSessionA() throws Exception { + for (int i = 1; i<=5; i++) { + testNode.addMixin("test:mixinProp_" + i); + } + superuser.save(); + testNodeB.refresh(false); + + // remove all mixin types + for (NodeType mixin : testNode.getMixinNodeTypes()) { + testNode.removeMixin(mixin.getName()); + } + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNodeB.addNode(nodeName1, "nt:unstructured"); + testNodeB.setProperty(propertyName1, "anyValue"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + /** + * Same as {@link #testMixinRemovedInSessionA} with different mixin types. + * @throws Exception + */ + public void testMixinRemovedInSessionA2() throws Exception { + for (int i = 1; i<=4; i++) { + testNode.addMixin("test:mixinNode_" + i); + } + superuser.save(); + testNodeB.refresh(false); + + // remove all mixin types + for (NodeType mixin : testNode.getMixinNodeTypes()) { + testNode.removeMixin(mixin.getName()); + } + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNodeB.addNode(nodeName1, "nt:unstructured"); + testNodeB.setProperty(propertyName1, "anyValue"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + //--------------- Tests modifying jcr:mixinType property in the SessionA --- + /** + * Modify the existing mixin property in the SessionA change without adding + * conflicting modifications to SessionB. + * + * @throws Exception + */ + public void testMixinModifiedInSessionA() throws Exception { + testNode.addMixin("test:mixinProp_5"); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // modify the mixin types + testNode.addMixin("test:mixinProp_1"); + + testNodeB.addNode(nodeName1, "nt:unstructured"); + testNodeB.setProperty(propertyName1, "anyValue"); + sessionB.save(); + + superuser.save(); + + assertTrue(testNode.hasProperty(propertyName1)); + assertTrue(testNode.hasNode(nodeName1)); + + assertTrue(testNodeB.isNodeType("test:mixinProp_1")); + assertTrue(Arrays.asList(testNodeB.getMixinNodeTypes()).contains(sessionB.getWorkspace().getNodeTypeManager().getNodeType("test:mixinProp_1"))); + } + + public void testMixinModifiedInSessionAB() throws Exception { + testNode.addMixin("test:mixinProp_5"); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // modify the mixin types + testNode.addMixin("test:mixinProp_1"); + + testNodeB.addMixin("test:mixinProp_1"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + public void testMixinModifiedInSessionAB2() throws Exception { + testNode.addMixin("test:mixinProp_5"); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // modify the mixin types + testNode.addMixin("test:mixinProp_1"); + + testNodeB.removeMixin("test:mixinProp_5"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + assertTrue(testNode.hasProperty("jcr:mixinTypes")); + assertTrue(testNode.isNodeType("test:mixinProp_1")); + assertTrue(testNode.isNodeType("test:mixinProp_5")); + assertEquals(2, testNode.getMixinNodeTypes().length); + } + } + + public void testMixinModifiedInSessionAB3() throws Exception { + testNode.addMixin("test:mixinProp_5"); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // modify the mixin types + testNode.addMixin("test:mixinProp_1"); + testNode.setProperty(propertyName1, "value"); + + testNodeB.removeMixin("test:mixinProp_5"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + assertTrue(testNode.hasProperty("jcr:mixinTypes")); + assertTrue(testNode.isNodeType("test:mixinProp_1")); + assertTrue(testNode.isNodeType("test:mixinProp_5")); + assertEquals(2, testNode.getMixinNodeTypes().length); + } + } + + public void testMixinModifiedInSessionAB4() throws Exception { + testNode.addMixin("test:mixinProp_5"); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // modify the mixin types + testNode.addMixin("test:mixinProp_1"); + + testNodeB.addMixin("test:mixinProp_2"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + //-------------------------- Tests adding jcr:mixinType only in SessionB --- + /** + * No jcr:mixinTypes property present in the SessionA but was added + * to the overlayed state while other changes were made to the SessionA. + * @throws Exception + */ + public void testMixinAddedInSessionB() throws Exception { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addNode(nodeName2); + + testNodeB.addMixin("test:mixinProp_1"); + sessionB.save(); + + superuser.save(); + + assertTrue(testNode.isNodeType("test:mixinProp_1")); + assertTrue(testNode.hasProperty("jcr:mixinTypes")); + + assertTrue(testNodeB.hasNode(nodeName2)); + } + + /** + * Same as {@link #testMixinAddedInSessionB} but having 2 SessionA modifications. + * @throws Exception + */ + public void testMixinAddedInSessionB2() throws Exception { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addNode(nodeName2); + testNode.setProperty(propertyName1, "value"); + + testNodeB.addMixin("test:mixinProp_1"); + sessionB.save(); + + superuser.save(); + + assertTrue(testNode.isNodeType("test:mixinProp_1")); + assertTrue(testNode.hasProperty("jcr:mixinTypes")); + + assertTrue(testNodeB.hasNode(nodeName2)); + } + + /** + * Add the mixin property in SessionB by adding a single mixin + * and create a conflicting item in the SessionA -> merge must fail. + * @throws Exception + */ + public void testMixinAddedInSessionBWithConflictingChanges() throws Exception { + assertFalse(testNode.hasProperty("jcr:mixinTypes")); + + testNode.addNode(nodeName2); + testNode.setProperty("test:prop_long_p", "value"); + + testNodeB.addMixin("test:mixinProp_5"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + //----------------------- Test jcr:mixinTypes removed in SessionB ---------- + /** + * Test if removing the (existing) jcr:mixinTypes property in the overlayed + * state (by removing all mixins) is not merged to SessionA changes. + * @throws Exception + */ + public void testMixinRemovedInSessionB() throws Exception { + for (int i = 1; i<=5; i++) { + testNode.addMixin("test:mixinProp_" + i); + } + superuser.save(); + testNodeB.refresh(false); + + testNode.addNode(nodeName1, "nt:unstructured"); + testNode.setProperty(propertyName1, "anyValue"); + + // remove all mixin types + for (NodeType mixin : testNodeB.getMixinNodeTypes()) { + testNodeB.removeMixin(mixin.getName()); + } + assertFalse(testNodeB.hasProperty("jcr:mixinTypes")); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + //----------------------- Test mixin modification (add-only) in SessionB --- + /** + * Existing mixins are modified in SessionB but not in the + * SessionA, where the net effect is 'add-only' in the SessionA. + * Changes should be merge if there are no conflicting SessionA + * modifications. + * + * @throws Exception + */ + public void testMixinModifiedAddInSessionB() throws Exception { + for (int i = 1; i<=5; i++) { + testNode.addMixin("test:mixinProp_" + i); + } + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // non-conflicting modification in SessionA + testNode.setProperty(propertyName1, "value"); + + testNodeB.addMixin("test:mixinNode_1"); + sessionB.save(); + + superuser.save(); + + assertTrue(testNode.isNodeType("test:mixinNode_1")); + List mx = Arrays.asList(testNode.getMixinNodeTypes()); + assertTrue(mx.contains(superuser.getWorkspace().getNodeTypeManager().getNodeType("test:mixinNode_1"))); + + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty(propertyName1)); + } + + /** + * Same as {@link #testMixinModifiedAddInSessionB} with different + * SessionA modifications. + * + * @throws Exception + */ + public void testMixinModifiedAddInSessionB2() throws Exception { + for (int i = 1; i<=5; i++) { + testNode.addMixin("test:mixinProp_" + i); + } + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // non-conflicting modification in SessionA + testNode.setProperty(propertyName1, "value"); + testNode.addNode(nodeName2); + + testNodeB.addMixin("test:mixinNode_1"); + sessionB.save(); + + superuser.save(); + assertTrue(testNode.isNodeType("test:mixinNode_1")); + List mx = Arrays.asList(testNode.getMixinNodeTypes()); + assertTrue(mx.contains(superuser.getWorkspace().getNodeTypeManager().getNodeType("test:mixinNode_1"))); + + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty(propertyName1)); + assertTrue(testNodeB.hasNode(nodeName2)); + } + + /** + * Test if the merge of add-only mixin modification in the overlayed stated + * is aborted if there are conflicting SessionA changes present. + * @throws Exception + */ + public void testMixinModifiedAddInSessionBWithConflictingChanges() throws Exception { + testNode.addMixin(mixReferenceable); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // conflicting modification in SessionA + testNode.setProperty(propertyName1, "value"); + testNode.addNode("test:child_1"); + + testNodeB.addMixin("test:mixinNode_1"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + /** + * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges} + * with different SessionA modifications. + * + * @throws Exception + */ + public void testMixinModifiedAddInSessionBWithConflictingChanges2() throws Exception { + testNode.addMixin(mixReferenceable); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // conflicting modification in SessionA + testNode.addNode("test:child_1"); + + testNodeB.addMixin("test:mixinNode_1"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + /** + * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges} + * with different mixin and SessionA modifications. + * + * @throws Exception + */ + public void testMixinModifiedAddInSessionBWithConflictingChanges3() throws Exception { + testNode.addMixin(mixReferenceable); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // conflicting modification in SessionA + testNode.setProperty("test:prop_long_p", "non-long-value"); + + testNodeB.addMixin("test:mixinProp_5"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + /** + * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges} + * with different mixin and other kind of modifications. + * + * @throws Exception + */ + public void testMixinModifiedAddInSessionBWithConflictingChanges4() throws Exception { + testNode.addMixin(mixReferenceable); + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // conflicting modification in session1 + testNode.setProperty("test:prop_long_p", "non-long-value"); + testNode.addNode("test:child_1"); + + testNodeB.addMixin("test:mixinProp_5"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + /** + * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges} + * but using mix:referencable as mixin to force the modification in the + * overlayed state. + * + * @throws Exception + */ +// TODO: uncomment once JCR-2779 is fixed +// public void testMixinModifiedReferenceableInSessionBConflicting() throws RepositoryException { +// testNode.addMixin("test:mixinProp_1"); +// superuser.save(); +// assertTrue(testNode2.hasProperty("jcr:mixinTypes")); +// +// testNode.setProperty("jcr:uuid", superuser.getValueFactory().createValue(false)); +// +// testNode2.addMixin(mixReferenceable); +// session2.save(); +// +// assertTrue(testNode.hasProperty("jcr:uuid")); +// assertTrue(testNode.getProperty("jcr:uuid").isNew()); +// try { +// superuser.save(); +// fail(); +// } catch (InvalidItemStateException e) { +// assertFalse(testNode.isNodeType(mixReferenceable)); +// assertEquals(PropertyType.BOOLEAN, testNode.getProperty("jcr:uuid").getType()); +// +// assertTrue(testNode2.isNodeType(mixReferenceable)); +// assertEquals(PropertyType.STRING, testNode2.getProperty("jcr:uuid").getType()); +// } +// } + + /** + * Same as {@link #testMixinModifiedAddInSessionBWithConflictingChanges} + * but using mix:referencable as mixin to force the modification in the + * overlayed state. + * + * @throws Exception + */ +// TODO: uncomment once JCR-2779 is fixed +// public void testMixinModifiedReferenceableInSessionBConflicting2() throws RepositoryException { +// testNode.addMixin("test:mixinProp_1"); +// superuser.save(); +// assertTrue(testNode2.hasProperty("jcr:mixinTypes")); +// +// testNode.setProperty("jcr:uuid", superuser.getValueFactory().createValue(false)); +// testNode.addNode(nodeName2); +// +// testNode2.addMixin(mixReferenceable); +// session2.save(); +// +// assertTrue(testNode.hasProperty("jcr:uuid")); +// assertTrue(testNode.getProperty("jcr:uuid").isNew()); +// try { +// superuser.save(); +// fail(); +// } catch (InvalidItemStateException e) { +// assertFalse(testNode.isNodeType(mixReferenceable)); +// assertEquals(PropertyType.BOOLEAN, testNode.getProperty("jcr:uuid").getType()); +// +// assertTrue(testNode2.isNodeType(mixReferenceable)); +// assertEquals(PropertyType.STRING, testNode2.getProperty("jcr:uuid").getType()); +// } +// } + + //-------------------- Test mixin modification (remove-only) in SessionB --- + /** + * Test if merge fails if some mixin removal occurred in the SessionB. + * + * @throws Exception + */ + public void testMixinModifiedRemovedInSessionB() throws Exception { + for (String mixin : testMixins) { + testNode.addMixin(mixin); + } + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + + // modification in session1 + testNode.setProperty(propertyName1, "value"); + + // mixin-removal in the session2 + testNodeB.removeMixin("test:mixinProp_1"); + testNodeB.removeMixin("test:mixinProp_2"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + /** + * Same as {@link #testMixinModifiedRemovedInSessionB} but with different + * SessionA modifications. + * + * @throws Exception + */ + public void testMixinModifiedRemovedInSessionB2() throws Exception { + for (String mixin : testMixins) { + testNode.addMixin(mixin); + } + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + + // modification in SessionA + testNode.setProperty(propertyName1, "value"); + testNode.addNode(nodeName2); + + // mixin-removal in SessionB + testNodeB.removeMixin("test:mixinProp_1"); + testNodeB.removeMixin("test:mixinProp_2"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } + + //---------------------------- Test other mixin modification in SessionB --- + /** + * Test if merge fails if the mixins of the overlayed state (sessionB) were + * modified in a combination of add and removal of mixin. + * + * @throws Exception + */ + public void testMixinModifiedInSessionB() throws Exception { + for (String mixin : testMixins) { + testNode.addMixin(mixin); + } + superuser.save(); + testNodeB.refresh(false); + assertTrue(testNodeB.hasProperty("jcr:mixinTypes")); + + // modification in SessionA + testNode.setProperty(propertyName1, "value"); + + // mixin-removal in the SessionB + testNodeB.addMixin(mixReferenceable); + testNodeB.removeMixin("test:mixinProp_2"); + sessionB.save(); + + try { + superuser.save(); + fail(); + } catch (InvalidItemStateException e) { + // expected + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/TestAll.java new file mode 100644 index 00000000000..fd66c0efd25 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/state/TestAll.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.core.state; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the State module. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("State tests"); + + suite.addTestSuite(ChangeLogTest.class); + suite.addTestSuite(DefaultISMLockingTest.class); + suite.addTestSuite(DefaultISMLockingDeadlockTest.class); + suite.addTestSuite(FineGrainedISMLockingTest.class); + suite.addTestSuite(NameSetTest.class); + suite.addTestSuite(NodeStateMergerTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/stats/QueryStatCoreTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/stats/QueryStatCoreTest.java new file mode 100644 index 00000000000..5d14c15668f --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/stats/QueryStatCoreTest.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.core.stats; + +import static javax.jcr.query.Query.JCR_SQL2; + +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.jackrabbit.stats.QueryStatCore; +import org.apache.jackrabbit.stats.QueryStatImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Query stats test cases. + */ +public class QueryStatCoreTest extends AbstractJCRTest { + + private QueryStatCore queryStat; + + private AtomicLong token = new AtomicLong(System.currentTimeMillis()); + + protected void setUp() throws Exception { + super.setUp(); + queryStat = new QueryStatImpl(); + queryStat.setEnabled(true); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + private void runRandomQuery() throws Exception { + String sql = "SELECT * FROM [nt:unstructured] as t where CONTAINS(t, '" + + token.getAndIncrement() + "') "; + queryStat.logQuery(JCR_SQL2, sql, 5); + } + + public void testPopularQuery() throws Exception { + + int size = 15; + queryStat.setPopularQueriesQueueSize(size); + + // test clear + runRandomQuery(); + queryStat.clearPopularQueriesQueue(); + assertEquals(0, queryStat.getPopularQueries().length); + + // test run one + queryStat.clearPopularQueriesQueue(); + runRandomQuery(); + assertEquals(1, queryStat.getPopularQueries().length); + + // run more than max size + queryStat.clearPopularQueriesQueue(); + for (int i = 0; i < size + 5; i++) { + runRandomQuery(); + } + assertEquals(size, queryStat.getPopularQueries().length); + + // test shrink + int newSize = 5; + queryStat.setPopularQueriesQueueSize(newSize); + assertEquals(newSize, queryStat.getPopularQueries().length); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/stats/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/stats/TestAll.java new file mode 100644 index 00000000000..426ee300f3b --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/stats/TestAll.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.core.stats; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all test cases for the stats module. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("Stats tests"); + + suite.addTestSuite(QueryStatCoreTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/CooperativeFileLockTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/CooperativeFileLockTest.java new file mode 100644 index 00000000000..0e912b1a433 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/CooperativeFileLockTest.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.core.util; + +import java.io.File; +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; + +/** + * Tests the cooperative file lock mechanism. + */ +public class CooperativeFileLockTest extends TestCase { + + private static final String TEST_DIRECTORY = "target/tmp/testCooperativeFileLock"; + + public void tearDown() throws IOException { + setUp(); + } + + public void setUp() throws IOException { + FileUtils.deleteQuietly(new File(TEST_DIRECTORY)); + } + + public void testFileLock() throws RepositoryException { + int testRuns = 1; + for (int i = 0; i < testRuns; i++) { + new File(TEST_DIRECTORY).mkdirs(); + CooperativeFileLock l1 = new CooperativeFileLock(); + l1.init(TEST_DIRECTORY); + l1.acquire(); + CooperativeFileLock l2 = new CooperativeFileLock(); + l2.init(TEST_DIRECTORY); + try { + l2.acquire(); + fail(); + } catch (Exception e) { + // expected + } + l1.release(); + l2.acquire(); + l2.release(); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/DOMWalkerTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/DOMWalkerTest.java new file mode 100644 index 00000000000..16daa31ac4e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/DOMWalkerTest.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.core.util; + +import java.io.ByteArrayInputStream; + +import junit.framework.TestCase; + +/** + * Unit tests for the {@link DOMWalker} class. + */ +public class DOMWalkerTest extends TestCase { + + /** + * JCR-1755: + * ClassCastException when registering custom node by XML file + */ + public void testIterateTopLevelElements() throws Exception { + DOMWalker walker = new DOMWalker( + new ByteArrayInputStream("".getBytes("UTF-8"))); + try { + while (walker.iterateElements("nodeType")) { + // do nothing + } + } catch (ClassCastException e) { + fail("JCR-1755: ClassCastException when registering" + + " custom node by XML file"); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/RepositoryLockTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/RepositoryLockTest.java new file mode 100644 index 00000000000..b3115a87f2d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/RepositoryLockTest.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.core.util; + +import java.io.File; +import java.io.IOException; + +import javax.jcr.RepositoryException; + +import junit.framework.TestCase; + +/** + * Unit tests for the {@link RepositoryLock} class. + */ +public class RepositoryLockTest extends TestCase { + + /** + * The temporary directory used for testing. + */ + private File directory; + + /** + * Sets up the temporary directory used for testing. + */ + protected void setUp() throws IOException { + directory = File.createTempFile("RepositoryLock", "Test"); + directory.delete(); + directory.mkdir(); + } + + /** + * Deletes the temporary directory used for testing. + */ + protected void tearDown() { + delete(directory); + } + + /** + * Recursively deletes the given file or directory. + * + * @param file file or directory to be deleted + */ + private void delete(File file) { + File[] files = file.listFiles(); + for (int i = 0; files != null && i < files.length; i++) { + delete(files[i]); + } + file.delete(); + } + + /** + * Tests that when an acquired lock is released, the lock file is + * automatically removed. + * + * @throws RepositoryException if an error occurs + */ + public void testNoFilesLeftBehind() throws RepositoryException { + RepositoryLock lock = new RepositoryLock(directory.getPath()); + lock.acquire(); + lock.release(); + assertEquals( + "Some files left behind by a lock", + 0, directory.listFiles().length); + } + + /** + * Tests that locking is exclusive within a single JVM. + * + * @throws RepositoryException if an error occurs + */ + public void testTwoLocks() throws RepositoryException { + RepositoryLock lockA = new RepositoryLock(directory.getPath()); + RepositoryLock lockB = new RepositoryLock(directory.getPath()); + lockA.acquire(); + try { + lockB.acquire(); + fail("Can acquire an already acquired lock"); + } catch (RepositoryException e) { + } + lockA.release(); + try { + lockB.acquire(); + } catch (RepositoryException e) { + fail("Can not acquire a released lock"); + } + lockB.release(); + } + + /** + * Tests that the canonical path is used for locking. + * + * @see https://issues.apache.org/jira/browse/JCR-933 + * @throws RepositoryException + */ + public void testCanonicalPath() throws RepositoryException { + RepositoryLock lockA = new RepositoryLock(directory.getPath()); + lockA.acquire(); + try { + File parent = new File(directory, ".."); + RepositoryLock lockB = new RepositoryLock( + new File(parent, directory.getName()).getPath()); + lockB.acquire(); + fail("Can acquire an already acquired lock using a different path"); + } catch (RepositoryException e) { + } + lockA.release(); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/TestAll.java new file mode 100644 index 00000000000..45f5adff4dd --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/TestAll.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.core.util; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the util module. + */ +public class TestAll extends TestCase { + + /** + * Returns a test suite that executes all tests inside this package. + * + * @return a test suite that executes all tests inside this package + */ + public static Test suite() { + TestSuite suite = new TestSuite("Utility tests"); + suite.addTestSuite(RepositoryLockTest.class); + suite.addTestSuite(CooperativeFileLockTest.class); + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/db/ConnectionFactoryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/db/ConnectionFactoryTest.java new file mode 100644 index 00000000000..46fbf6bcff8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/db/ConnectionFactoryTest.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.core.util.db; + +import java.sql.Connection; +import java.util.Properties; + +import javax.jcr.RepositoryException; +import javax.sql.DataSource; + +import junit.framework.TestCase; + +import org.apache.commons.dbcp.BasicDataSource; +import org.apache.derby.iapi.jdbc.EngineConnection; +import org.apache.jackrabbit.core.config.ConfigurationException; +import org.apache.jackrabbit.core.config.DataSourceConfig; + +/** + * + */ +public class ConnectionFactoryTest extends TestCase { + + private ConnectionFactory connectionFactory; + + private static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver"; + + private static final String DERBY_URL = "jdbc:derby:target/connection-factory-test/db;create=true"; + + private static final String MYSQL_URL = "jdbc:mysql://localhost:3306/dbName?autoReconnect=true"; + + private static final String MSSQL_URL_1 = "jdbc:jtds:sqlserver://localhost:2433/dbName"; + + private static final String MSSQL_URL_2 = "jdbc:sqlserver://localhost:1433;databaseName=dbName"; + + private static final String ORACLE_URL = "jdbc:oracle:thin:@localhost:1521:xe"; + + private static final String H2_URL = "jdbc:h2:tcp://localhost/dbName"; + + @Override + public void setUp() { + System.setProperty("derby.stream.error.file", "target/derby-connectionfactorytest.log"); + connectionFactory = new ConnectionFactory(); + } + + public void testGetDataSource_defaults_Derby() throws Exception { + DataSource ds1 = connectionFactory.getDataSource(DRIVER, DERBY_URL, "user", "password"); + assertTrue(ds1 instanceof BasicDataSource); + BasicDataSource ds = (BasicDataSource) ds1; + assertPoolDefaults(ds, "values(1)", -1); + } + + public void testGuessValidationQuery_MYSQL() throws Exception { + DataSource ds1 = connectionFactory.getDataSource(DRIVER, MYSQL_URL, "user", "password"); + assertEquals("select 1", ((BasicDataSource) ds1).getValidationQuery()); + } + + public void testGuessValidationQuery_MSSQL() throws Exception { + DataSource ds1 = connectionFactory.getDataSource(DRIVER, MSSQL_URL_1, "user", "password"); + assertEquals("select 1", ((BasicDataSource) ds1).getValidationQuery()); + DataSource ds2 = connectionFactory.getDataSource(DRIVER, MSSQL_URL_2, "user", "password"); + assertEquals("select 1", ((BasicDataSource) ds2).getValidationQuery()); + } + + public void testGuessValidationQuery_ORACLE() throws Exception { + DataSource ds1 = connectionFactory.getDataSource(DRIVER, ORACLE_URL, "user", "password"); + assertEquals("select 'validationQuery' from dual", ((BasicDataSource) ds1).getValidationQuery()); + } + + public void testGuessValidationQuery_H2() throws Exception { + DataSource ds1 = connectionFactory.getDataSource(DRIVER, H2_URL, "user", "password"); + assertEquals("select 1", ((BasicDataSource) ds1).getValidationQuery()); + } + + public void testRegisterDataSources_defaultValues() throws Exception { + BasicDataSource ds = registerAndGet(DERBY_URL, "overwrite", -1); + assertPoolDefaults(ds, "overwrite", -1); + } + + public void testRegisterDataSources_noValidationQuery() throws Exception { + BasicDataSource ds = registerAndGet(MYSQL_URL, "", -1); + assertEquals("select 1", ds.getValidationQuery()); + } + + public void testGetDatabaseType() throws Exception { + String name = register(MYSQL_URL, "", -1); + assertEquals("dbType", connectionFactory.getDataBaseType(name)); + } + + public void testGetDataSource_identity() throws Exception { + DataSource ds1 = connectionFactory.getDataSource(DRIVER, DERBY_URL, "user", "password"); + DataSource ds2 = connectionFactory.getDataSource(DRIVER, DERBY_URL, "user", "password"); + assertSame(ds1, ds2); + } + + public void testGetDataSource_identity_differentPasswords() throws Exception { + DataSource ds1 = connectionFactory.getDataSource(DRIVER, DERBY_URL, "user", "password"); + DataSource ds2 = connectionFactory.getDataSource(DRIVER, DERBY_URL, "user", "password2"); + assertSame(ds1, ds2); + } + + public void testGetDataSource_noIdentity() throws Exception { + DataSource ds1 = connectionFactory.getDataSource(DRIVER, DERBY_URL, "user", "password"); + DataSource ds2 = connectionFactory.getDataSource(DRIVER, DERBY_URL, "user2", "password"); + assertNotSame(ds1, ds2); + } + + public void testUnwrap() throws Exception { + DataSource ds = connectionFactory.getDataSource(DRIVER, DERBY_URL, "user", "password"); + Connection wrappedCon = ds.getConnection(); + assertNotNull(wrappedCon); + Connection con = ConnectionFactory.unwrap(wrappedCon); + assertTrue(con instanceof EngineConnection); + } + + public void testClose() throws Exception { + connectionFactory.close(); + try { + connectionFactory.getDataBaseType("logicalName"); + fail("could retrieve after close"); + } catch (IllegalStateException expected) { + } + try { + connectionFactory.getDataSource("logicalName"); + fail("could retrieve after close"); + } catch (IllegalStateException expected) { + } + try { + connectionFactory.getDataSource(DRIVER, DERBY_URL, "user", "password"); + fail("could retrieve after close"); + } catch (IllegalStateException expected) { + } + } + + private void assertPoolDefaults(BasicDataSource ds, String validationQuery, int maxCons) { + assertEquals(maxCons, ds.getMaxActive()); + assertEquals(validationQuery, ds.getValidationQuery()); + assertTrue(ds.getDefaultAutoCommit()); + assertFalse(ds.getTestOnBorrow()); + assertTrue(ds.getTestWhileIdle()); + assertEquals(600000, ds.getTimeBetweenEvictionRunsMillis()); + assertTrue(ds.isPoolPreparedStatements()); + assertEquals(-1, ds.getMaxOpenPreparedStatements()); + } + + private BasicDataSource registerAndGet(String url, String validationQuery, int maxCons) throws Exception { + final String name = register(url, validationQuery, maxCons); + DataSource ds = connectionFactory.getDataSource(name); + assertTrue(ds instanceof BasicDataSource); + return (BasicDataSource) ds; + } + + private String register(String url, String validationQuery, int maxCons) throws ConfigurationException, + RepositoryException { + final String name = "some random name to not interfere with integration tests..."; + DataSourceConfig dsc = new DataSourceConfig(); + Properties props = new Properties(); + props.put(DataSourceConfig.DRIVER, DRIVER); + props.put(DataSourceConfig.URL, url); + props.put(DataSourceConfig.DB_TYPE, "dbType"); + props.put(DataSourceConfig.MAX_POOL_SIZE, Integer.toString(maxCons)); + props.put(DataSourceConfig.VALIDATION_QUERY, validationQuery); + dsc.addDataSourceDefinition(name, props); + connectionFactory.registerDataSources(dsc); + return name; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/db/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/db/TestAll.java new file mode 100644 index 00000000000..2b25459eca9 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/util/db/TestAll.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.core.util.db; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the util module. + */ +public class TestAll extends TestCase { + + /** + * Returns a test suite that executes all tests inside this package. + * + * @return a test suite that executes all tests inside this package + */ + public static Test suite() { + TestSuite suite = new TestSuite("Database utility tests"); + suite.addTestSuite(ConnectionFactoryTest.class); + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/BinaryValueTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/BinaryValueTest.java new file mode 100644 index 00000000000..f9d509bf660 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/BinaryValueTest.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.core.value; + +import java.io.ByteArrayInputStream; +import java.util.Random; + +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.Binary; +import javax.jcr.Node; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * BinaryValueTest check if multiple executions of: + *

      + *
    • get Binary from property
    • + *
    • read from Binary
    • + *
    • dispose Binary
    • + *
    + * do not throw an exception. + *

    + * See also JCR-2238. + */ +public class BinaryValueTest extends AbstractJCRTest { + + public void testDispose10() throws Exception { + checkDispose(10, false); + } + + public void testDispose10k() throws Exception { + checkDispose(10 * 1024, false); + } + + public void testDispose10Save() throws Exception { + checkDispose(10, true); + } + + public void testDispose10kSave() throws Exception { + checkDispose(10 * 1024, true); + } + + protected void checkDispose(int length, boolean save) throws Exception { + Property prop = setProperty(testRootNode.addNode(nodeName1), length); + if (save) { + superuser.save(); + } + checkProperty(prop); + } + + protected Property setProperty(Node node, int length) throws RepositoryException { + Random rand = new Random(); + byte[] data = new byte[length]; + rand.nextBytes(data); + + Binary b = vf.createBinary(new ByteArrayInputStream(data)); + //System.out.println(b.getClass() + ": " + System.identityHashCode(b)); + try { + return node.setProperty(propertyName1, b); + } finally { + b.dispose(); + } + } + + protected void checkProperty(Property prop) throws Exception { + for (int i = 0; i < 3; i++) { + Binary b = prop.getBinary(); + try { + //System.out.println(b.getClass() + ": " + System.identityHashCode(b)); + b.read(new byte[1], 0); + } finally { + b.dispose(); + } + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/InternalValueFactoryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/InternalValueFactoryTest.java new file mode 100644 index 00000000000..6c5140f7610 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/InternalValueFactoryTest.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.core.value; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.QValueFactoryTest; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * InternalValueFactoryTest... + */ +public class InternalValueFactoryTest extends QValueFactoryTest { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(InternalValueFactoryTest.class); + + protected void setUp() throws Exception { + factory = InternalValueFactory.getInstance(); + rootPath = PathFactoryImpl.getInstance().getRootPath(); + testName = NameFactoryImpl.getInstance().create(Name.NS_JCR_URI, "data"); + reference = NodeId.randomId().toString(); + } + + protected void tearDown() throws Exception { + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/InternalValueTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/InternalValueTest.java new file mode 100644 index 00000000000..a12a2b70654 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/InternalValueTest.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.core.value; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValueTest; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * InternalValueFactoryTest... + */ +public class InternalValueTest extends QValueTest { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(InternalValueTest.class); + + protected void setUp() throws Exception { + factory = InternalValueFactory.getInstance(); + rootPath = PathFactoryImpl.getInstance().getRootPath(); + testName = NameFactoryImpl.getInstance().create(Name.NS_JCR_URI, "data"); + reference = NodeId.randomId().toString(); + } + + protected void tearDown() throws Exception { + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/PathTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/PathTest.java new file mode 100644 index 00000000000..f1c49b034b6 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/PathTest.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.core.value; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Binary; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * PathTest... + */ +public class PathTest extends AbstractJCRTest { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(PathTest.class); + + Property prop; + + @Override + protected void setUp() throws Exception { + super.setUp(); + prop = testRootNode.setProperty(propertyName1, "/a/b/c", PropertyType.PATH); + } + + public void testGetBinary() throws RepositoryException, IOException { + Binary binary = prop.getBinary(); + byte[] bytes = new byte[(int) binary.getSize()]; + binary.read(bytes, 0); + binary.dispose(); + + assertEquals(prop.getString(), new String(bytes, "UTF-8")); + } + + public void testGetBinaryFromValue() throws RepositoryException, IOException { + Value v = superuser.getValueFactory().createValue("/a/b/c", PropertyType.PATH); + Binary binary = v.getBinary(); + + byte[] bytes = new byte[(int) binary.getSize()]; + binary.read(bytes, 0); + binary.dispose(); + + assertEquals(prop.getString(), new String(bytes, "UTF-8")); + } + + public void testGetStream() throws RepositoryException, IOException { + InputStream in = prop.getStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + IOUtils.copy(in, out); + IOUtils.closeQuietly(in); + + assertEquals(prop.getString(), new String(out.toByteArray(), "UTF-8")); + } + + public void testGetStreamFromValue() throws RepositoryException, IOException { + Value v = superuser.getValueFactory().createValue("/a/b/c", PropertyType.PATH); + InputStream in = v.getStream(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + IOUtils.copy(in, out); + IOUtils.closeQuietly(in); + + assertEquals(prop.getString(), new String(out.toByteArray(), "UTF-8")); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/ReferenceBinaryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/ReferenceBinaryTest.java new file mode 100644 index 00000000000..d77c78bbb9d --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/ReferenceBinaryTest.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.core.value; + +import javax.jcr.Binary; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.api.ReferenceBinary; +import org.apache.jackrabbit.commons.jackrabbit.SimpleReferenceBinary; +import org.apache.jackrabbit.core.data.RandomInputStream; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Testcase for JCR-3534. + */ +public class ReferenceBinaryTest extends AbstractJCRTest { + + private static final int STREAM_LENGTH = 256 * 1024; + + public void testReferenceBinaryExchangeWithSharedRepository() throws Exception { + + Session firstSession = superuser; + + // create a binary + Binary b = vf.createBinary(new RandomInputStream(1, STREAM_LENGTH)); + + ReferenceBinary referenceBinary = null; + if (b instanceof ReferenceBinary) { + referenceBinary = (ReferenceBinary) b; + } + + assertNotNull(referenceBinary); + + assertNotNull(referenceBinary.getReference()); + + // in the current test the message is exchanged via repository which is shared as well + // put the reference message value in a property on a node + String newNode = "sample_" + System.nanoTime(); + firstSession.getRootNode().addNode(newNode).setProperty("reference", referenceBinary.getReference()); + + // save the first session + firstSession.save(); + + // get a second session over the same repository / ds + Session secondSession = getHelper().getRepository().login(new SimpleCredentials("admin", "admin".toCharArray())); + + // read the binary referenced by the referencing binary + String reference = secondSession.getRootNode().getNode(newNode).getProperty("reference").getString(); + + ReferenceBinary ref = new SimpleReferenceBinary(reference); + + assertEquals(b, secondSession.getValueFactory().createValue(ref).getBinary()); + + } + +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/TestAll.java new file mode 100644 index 00000000000..68a5de9214e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/value/TestAll.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.core.value; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package org.apache.jackrabbit.core.value. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.core.value tests"); + + suite.addTestSuite(BinaryValueTest.class); + suite.addTestSuite(InternalValueFactoryTest.class); + suite.addTestSuite(InternalValueTest.class); + suite.addTestSuite(PathTest.class); + suite.addTestSuite(ReferenceBinaryTest.class); + + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/CheckinRemoveVersionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/CheckinRemoveVersionTest.java new file mode 100644 index 00000000000..eec47cf950e --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/CheckinRemoveVersionTest.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.core.version; + +import org.apache.jackrabbit.core.UserTransactionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Node; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.version.Version; + +/** + * Test case for JCR-1481 + */ +public class CheckinRemoveVersionTest extends AbstractJCRTest { + + public void testCheckinRemoveVersionWithXA() throws Exception { + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + testRootNode.save(); + UserTransactionImpl tx = new UserTransactionImpl(superuser); + tx.begin(); + try { + Version v10 = n.checkin(); + assertTrue("Version.getReferences() must return base version", v10.getReferences().hasNext()); + try { + n.getVersionHistory().removeVersion(v10.getName()); + fail("VersionHistory.removeVersion() must throw ReferentialIntegrityException when" + + " version is still referenced."); + } catch (ReferentialIntegrityException e) { + // expected + } + } finally { + tx.rollback(); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/CopyFrozenUuidTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/CopyFrozenUuidTest.java new file mode 100644 index 00000000000..6cd3f114dfd --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/CopyFrozenUuidTest.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.core.version; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Tests the case when a node already has a manual set JcrConstants.JCR_FROZENUUID property and is versioned. + * The manual set frozenUuid will overwrite the one that is automatically assigned by the VersionManager, which should not happen + */ +public class CopyFrozenUuidTest extends AbstractJCRTest { + + public void testCopyFrozenUuidProperty() throws Exception { + Node firstNode = testRootNode.addNode(nodeName1); + firstNode.setPrimaryType(JcrConstants.NT_UNSTRUCTURED); + firstNode.addMixin(JcrConstants.MIX_VERSIONABLE); + firstNode.getSession().save(); + + // create version for the node + Version firstNodeVersion = firstNode.checkin(); + firstNode.checkout(); + + Node secondNode = testRootNode.addNode(nodeName2); + secondNode.setPrimaryType(JcrConstants.NT_UNSTRUCTURED); + secondNode.addMixin(JcrConstants.MIX_VERSIONABLE); + Property firstNodeVersionFrozenUuid = firstNodeVersion.getFrozenNode().getProperty(JcrConstants.JCR_FROZENUUID); + secondNode.setProperty(JcrConstants.JCR_FROZENUUID, firstNodeVersionFrozenUuid.getValue()); + secondNode.getSession().save(); + + // create version of the second node + Version secondNodeVersion = secondNode.checkin(); + secondNode.checkout(); + + // frozenUuid from the second node version node should not be the same as the one from the first node version + Property secondBodeVersionFrozenUuid = secondNodeVersion.getFrozenNode().getProperty(JcrConstants.JCR_FROZENUUID); + assertFalse(JcrConstants.JCR_FROZENUUID + " should not be the same for two different versions of different nodes! ", + secondBodeVersionFrozenUuid.getValue().equals(firstNodeVersionFrozenUuid.getValue())); + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/InternalVersionHistoryImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/InternalVersionHistoryImplTest.java new file mode 100644 index 00000000000..dc21e35ee9c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/InternalVersionHistoryImplTest.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.core.version; + +import junit.framework.TestCase; + +import java.util.Calendar; + +/** + * InternalVersionHistoryImplTest executes tests for + * {@link InternalVersionHistoryImpl}. + */ +public class InternalVersionHistoryImplTest extends TestCase { + + /** + * Checks if {@link InternalVersionHistoryImpl#getCurrentTime()} returns + * monotonically increasing Calendar instances. + */ + public void testGetCurrentTime() { + Calendar last = InternalVersionHistoryImpl.getCurrentTime(); + for (int i = 0; i < 100; i++) { + Calendar next = InternalVersionHistoryImpl.getCurrentTime(); + assertTrue("InternalVersionHistoryImpl.getCurrentTime() not monotonically increasing", + last.getTimeInMillis() < next.getTimeInMillis()); + last = next; + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveAndAddVersionLabelXATest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveAndAddVersionLabelXATest.java new file mode 100644 index 00000000000..63377a258c8 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveAndAddVersionLabelXATest.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.core.version; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.core.UserTransactionImpl; + +import javax.jcr.Node; +import javax.transaction.UserTransaction; + +/** + * RemoveAndAddVersionLabelXATest implements a test case for + * JCR-1587. + */ +public class RemoveAndAddVersionLabelXATest extends AbstractJCRTest { + + public void testVersionLabel() throws Exception { + UserTransaction tx = new UserTransactionImpl(superuser); + tx.begin(); + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + testRootNode.save(); + String v1 = n.checkin().getName(); + n.checkout(); + String v2 = n.checkin().getName(); + n.getVersionHistory().addVersionLabel(v2, "label", false); + tx.commit(); + + tx = new UserTransactionImpl(superuser); + tx.begin(); + n.restore(v1, false); + n.getVersionHistory().removeVersion(v2); + n.checkout(); + v2 = n.checkin().getName(); + n.getVersionHistory().addVersionLabel(v2, "label", false); + tx.commit(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveOrphanVersionHistoryTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveOrphanVersionHistoryTest.java new file mode 100644 index 00000000000..6b94b095757 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveOrphanVersionHistoryTest.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Workspace; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test case for JCR-134. + */ +public class RemoveOrphanVersionHistoryTest extends AbstractJCRTest { + + /** + * Test orphan version history cleaning in a single workspace. + * @throws RepositoryException if an error occurs. + */ + public void testRemoveOrphanVersionHistory() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + testRootNode.save(); + Session session = n.getSession(); + VersionHistory vh = n.getVersionHistory(); + String vhUuid = vh.getUUID(); + assertExists(session, vhUuid); + + // First version + Version v10 = n.checkin(); + n.checkout(); + + // Second version + Version v11 = n.checkin(); + n.checkout(); + + // Remove node + n.remove(); + testRootNode.save(); + assertExists(session, vhUuid); + + // Remove the first version + vh.removeVersion(v10.getName()); + assertExists(session, vhUuid); + + // Remove the second and last version + vh.removeVersion(v11.getName()); + + try { + session.getNodeByUUID(vhUuid); + fail("Orphan version history must have been removed"); + } catch (ItemNotFoundException e) { + // Expected + } + } + + /** + * Test orphan version history cleaning in multiple workspace. + * @throws RepositoryException if an error occurs. + */ + public void testWorkspaceRemoveOrphanVersionHistory() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + testRootNode.save(); + Session session = n.getSession(); + VersionHistory vh = n.getVersionHistory(); + String vhUuid = vh.getUUID(); + assertExists(session, vhUuid); + + // First version + Version v10 = n.checkin(); + n.checkout(); + + Workspace defaultWorkspace = n.getSession().getWorkspace(); + Session otherWsSession = n.getSession().getRepository().login(new SimpleCredentials("superuser", "".toCharArray()), workspaceName); + // Clone the node in another workspace + otherWsSession.getWorkspace().clone(defaultWorkspace.getName(), n.getPath(), n.getPath(), false); + Node otherWsRootNode = otherWsSession.getRootNode(); + Node clonedNode = otherWsRootNode.getNode(n.getPath().substring(1)); + // Ensure that version histories are the same + assertEquals(vhUuid, clonedNode.getVersionHistory().getUUID()); + + Version v11 = clonedNode.checkin(); + clonedNode.checkout(); + + // Remove node + n.remove(); + testRootNode.save(); + assertExists(session, vhUuid); + assertExists(otherWsSession, vhUuid); + + // Remove the first version + vh.removeVersion(v10.getName()); + assertExists(session, vhUuid); + assertExists(otherWsSession, vhUuid); + + // Remove cloned node + clonedNode.remove(); + otherWsRootNode.save(); + assertExists(session, vhUuid); + assertExists(otherWsSession, vhUuid); + + // Remove the last version + vh.removeVersion(v11.getName()); + + try { + session.getNodeByUUID(vhUuid); + fail("Orphan version history must have been removed from the default workspace"); + } catch (ItemNotFoundException e) { + // Expected + } + + try { + otherWsSession.getNodeByUUID(vhUuid); + fail("Orphan version history must have been removed from the other workspace"); + } catch (ItemNotFoundException e) { + // Expected + } + } + + /** + * Test that an emptied version history that is still being referenced + * from another workspace does not get removed. + * + * @throws RepositoryException if an error occurs. + */ + public void testEmptyNonOrphanVersionHistory() throws RepositoryException { + Session session = testRootNode.getSession(); + + // Create versionable test node + Node node = testRootNode.addNode(nodeName1); + node.addMixin(mixVersionable); + session.save(); + + VersionHistory history = node.getVersionHistory(); + String uuid = history.getUUID(); + + // Create version 1.0 + Version v10 = node.checkin(); + + // Remove the test node + node.checkout(); + node.remove(); + session.save(); + + Session otherSession = getHelper().getReadWriteSession(workspaceName); + try { + // create a reference to the version history in another workspace + Node otherRoot = otherSession.getRootNode(); + Property reference = otherRoot.setProperty( + "RemoveOrphanVersionTest", uuid, PropertyType.REFERENCE); + otherSession.save(); + + // Now remove the contents of the version history + history.removeVersion(v10.getName()); + + // Check that the version history still exists! + try { + session.getNodeByUUID(uuid); + } catch (ItemNotFoundException e) { + fail("Referenced empty version history must note be removed"); + } + + // Cleanup + reference.remove(); + otherSession.save(); + } finally { + otherSession.logout(); + } + } + + /** + * Assert that a node exists in a session. + * @param session the session. + * @param uuid the node's UUID. + * @throws RepositoryException if an error occurs. + */ + protected void assertExists(Session session, String uuid) throws RepositoryException + { + try { + session.getNodeByUUID(uuid); + } catch (ItemNotFoundException e) { + fail("Unknown uuid: " + uuid); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveVersionLabelTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveVersionLabelTest.java new file mode 100644 index 00000000000..04766a338ce --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveVersionLabelTest.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.core.version; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +/** + * Test case for JCR-1475. + */ +public class RemoveVersionLabelTest extends AbstractJCRTest { + + public void testRemoveVersionLabel() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + testRootNode.save(); + Version v10 = n.checkin(); + n.checkout(); + n.checkin(); + VersionHistory vh = n.getVersionHistory(); + vh.addVersionLabel(v10.getName(), "test", true); + // the next call must not fail + vh.removeVersion(v10.getName()); + // now the label must be gone + String[] labels = vh.getVersionLabels(); + assertEquals("Label of a removed version must be removed as well", 0, labels.length); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveVersionTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveVersionTest.java new file mode 100644 index 00000000000..69916d8809a --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RemoveVersionTest.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.version; + +import java.util.LinkedList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; +import javax.transaction.UserTransaction; + +import org.apache.jackrabbit.core.UserTransactionImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Tests if removing of versions works correctly + */ +public class RemoveVersionTest extends AbstractJCRTest { + + /** + * Removes a version in 1 transaction and tries to commit afterwards the + * versionable node using a 2nd transaction. + * + * Tests error reported in JCR-2613 + * + * @throws Exception if an error occurs + */ + public void testRemoveVersionAndCheckinXA() throws Exception { + UserTransaction tx = new UserTransactionImpl(superuser); + tx.begin(); + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + n.addMixin(mixReferenceable); + testRootNode.save(); + String uuid = n.getUUID(); + // create two versions + String v1 = n.checkin().getName(); + n.checkout(); + n.checkin(); + n.checkout(); + tx.commit(); + + tx = new UserTransactionImpl(superuser); + tx.begin(); + // remove one version + n = superuser.getNodeByUUID(uuid); + n.getVersionHistory().removeVersion(v1); + n.save(); + tx.commit(); + + // new session + Session session = getHelper().getSuperuserSession(); + tx = new UserTransactionImpl(session); + tx.begin(); + n = session.getNodeByUUID(uuid); + n.checkin(); + tx.commit(); + } + + /** + * Removes a version in 1 transaction and tries to commit afterwards the + * versionable node using a 2nd transaction. Uses the JCR2.0 API. + * + * Tests error reported in JCR-2613 + * + * @throws Exception if an error occurs + */ + public void testRemoveVersionAndCheckinXA_JCR2() throws Exception { + UserTransaction tx = new UserTransactionImpl(superuser); + tx.begin(); + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + superuser.save(); + + // create two versions + String path = n.getPath(); + String v1 = superuser.getWorkspace().getVersionManager().checkpoint(path).getName(); + String v2 = superuser.getWorkspace().getVersionManager().checkpoint(path).getName(); + tx.commit(); + + tx = new UserTransactionImpl(superuser); + tx.begin(); + // remove one version + superuser.getWorkspace().getVersionManager().getVersionHistory(path).removeVersion(v1); + tx.commit(); + + // new session + Session session = getHelper().getSuperuserSession(); + tx = new UserTransactionImpl(session); + tx.begin(); + session.getWorkspace().getVersionManager().checkin(path); + tx.commit(); + } + + /** + * Creates 3 versions and removes them afterwards. Checks if version history + * was purged, too. + * + * Tests error reported in JCR-2601 + * + * @throws Exception if an error occurs + */ + public void testRemoveAllVersions() throws Exception { + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + superuser.save(); + String path = n.getPath(); + + // create some versions + VersionManager mgr = superuser.getWorkspace().getVersionManager(); + mgr.checkpoint(path); // v1.0 + mgr.checkpoint(path); // v1.1 + mgr.checkpoint(path); // v1.2 + + // get version history + VersionHistory vh = mgr.getVersionHistory(path); + String id = vh.getIdentifier(); + + // remove versionable node + n.remove(); + superuser.save(); + + // get the names of the versions + List names = new LinkedList(); + VersionIterator vit = vh.getAllVersions(); + while (vit.hasNext()) { + Version v = vit.nextVersion(); + if (!v.getName().equals("jcr:rootVersion")) { + names.add(v.getName()); + } + } + assertEquals("Number of versions", 3, names.size()); + + // remove all versions + for (String name: names) { + vh.removeVersion(name); + } + + // assert that version history is gone + try { + superuser.getNodeByIdentifier(id); + fail("Version history not removed after last version was removed."); + } catch (RepositoryException e) { + // ok + } + } +} \ No newline at end of file diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RestoreNodeWithSNSTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RestoreNodeWithSNSTest.java new file mode 100755 index 00000000000..ec3b1a9c70c --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RestoreNodeWithSNSTest.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.core.version; + +import javax.jcr.Node; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test case for JCR-2930 + */ +public class RestoreNodeWithSNSTest extends AbstractJCRTest { + + public void testRestoreWithSNS() throws Exception { + + int childCount = 5; + + // create a test node with /childCount/ children with the same name + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + for (int i = 0; i < childCount; i++) { + Node child = n.addNode(nodeName2); + child.setProperty("name", nodeName2 + i); + } + testRootNode.getSession().save(); + + // check the number of children + assertEquals(childCount, n.getNodes().getSize()); + + VersionManager vm = testRootNode.getSession().getWorkspace() + .getVersionManager(); + vm.checkin(n.getPath()); + + // modify one child + vm.checkout(n.getPath()); + n.getNode(nodeName2).setProperty("name", "modified"); + testRootNode.getSession().save(); + + // check the number of children again + assertEquals(childCount, n.getNodes().getSize()); + + // restore base versiob + Version baseVersion = vm.getBaseVersion(n.getPath()); + vm.restore(baseVersion, true); + + n.getSession().refresh(false); + + // check the number of children again + assertEquals(childCount, n.getNodes().getSize()); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RestoreTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RestoreTest.java new file mode 100644 index 00000000000..8679ab2a9a5 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/RestoreTest.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.core.version; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.core.UserTransactionImpl; + +import javax.jcr.Node; +import javax.jcr.version.Version; + +/** + * Test case for JCR-1476. + */ +public class RestoreTest extends AbstractJCRTest { + + public void testRestoreWithXA() throws Exception { + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + testRootNode.save(); + UserTransactionImpl tx = new UserTransactionImpl(superuser); + tx.begin(); + Version v10 = n.checkin(); + String versionName = v10.getName(); + n.restore(v10, true); + assertEquals("Wrong version restored", versionName, n.getBaseVersion().getName()); + tx.commit(); + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/TestAll.java new file mode 100644 index 00000000000..f3f68009ea0 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/TestAll.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.core.version; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all jackrabbit core version tests. + */ +public class TestAll extends TestCase { + + /** + * Returns a test suite that executes all tests inside this package. + * + * @return a test suite that executes all tests inside this package + */ + public static Test suite() { + TestSuite suite = new TestSuite("Version tests"); + suite.addTestSuite(CheckinRemoveVersionTest.class); + suite.addTestSuite(CopyFrozenUuidTest.class); + suite.addTestSuite(InternalVersionHistoryImplTest.class); + suite.addTestSuite(RemoveVersionLabelTest.class); + suite.addTestSuite(RestoreTest.class); + suite.addTestSuite(RestoreNodeWithSNSTest.class); + suite.addTestSuite(VersionIteratorImplTest.class); + return suite; + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/VersionIteratorImplTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/VersionIteratorImplTest.java new file mode 100644 index 00000000000..edb824ef063 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/version/VersionIteratorImplTest.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.core.version; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.spi.Name; + +public class VersionIteratorImplTest extends TestCase { + + private static final int VERSION_COUNT = 10000; + + private final class DummyInternalVersion implements InternalVersion { + + private final InternalVersion[] successors; + private NodeId id; + + public DummyInternalVersion(InternalVersion[] successors, NodeId id) { + this.successors = successors; + this.id = id; + } + + public List getSuccessors() { + return Arrays.asList(successors); + } + + public NodeId getId() { + return id; + } + + public Calendar getCreated() {return null;} + public InternalFrozenNode getFrozenNode() {return null;} + public NodeId getFrozenNodeId() {return null;} + public Name[] getLabels() {return null;} + public Name getName() {return null;} + public InternalVersion[] getPredecessors() {return null;} + public InternalVersionHistory getVersionHistory() {return null;} + public boolean hasLabel(Name label) {return false;} + public boolean isMoreRecent(InternalVersion v) {return false;} + public boolean isRootVersion() {return false;} + public InternalVersionItem getParent() {return null;} + public InternalVersion getLinearSuccessor(InternalVersion baseVersion) { return null; } + public InternalVersion getLinearPredecessor() { return null; } + } + + public void testVersionIterator() throws Exception { + + InternalVersion version = new DummyInternalVersion(new InternalVersion[] {}, NodeId.randomId()); + for (int i = 1; i < VERSION_COUNT; i++) { + version = new DummyInternalVersion(new InternalVersion[] {version}, NodeId.randomId()); + } + + try { + VersionIteratorImpl versionIteratorImpl = new VersionIteratorImpl(null, version); + assertEquals(VERSION_COUNT, versionIteratorImpl.getSize()); + } catch (StackOverflowError e) { + fail("Should be able to handle " + VERSION_COUNT + " versions."); + } + + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/AccessControlImporterTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/AccessControlImporterTest.java new file mode 100644 index 00000000000..a0889ca2725 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/AccessControlImporterTest.java @@ -0,0 +1,780 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.xml; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.commons.xml.ParsingContentHandler; +import org.apache.jackrabbit.core.NodeImpl; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.core.config.ImportConfig; +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.authorization.AccessControlConstants; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.core.security.principal.PrincipalImpl; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.xml.sax.SAXException; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * AccessControlImporterTest: Testing import of resource based + * ACLs. + */ +public class AccessControlImporterTest extends AbstractJCRTest { + + private static final String XML_POLICY_TREE = "" + + "" + + "" + + "nt:unstructured" + + "" + + "" + + "rep:AccessControllable" + + "mix:versionable" + + "" + + "" + + "0a0ca2e9-ab98-4433-a12b-d57283765207" + + "" + + "" + + "35d0d137-a3a4-4af3-8cdd-ce565ea6bdc9" + + "" + + "" + + "true" + + "" + + "" + + "35d0d137-a3a4-4af3-8cdd-ce565ea6bdc9" + + "" + + "" + + "428c9ef2-78e5-4f1c-95d3-16b4ce72d815" + + "" + + "" + + "" + + "rep:ACL" + + "" + + "" + + "" + + "rep:GrantACE" + + "" + + "" + + "everyone" + + "" + + "" + + "jcr:write" + + "" + + "" + + "" + + ""; + + private static final String XML_POLICY_TREE_2 = "" + + "" + + "" + + "rep:ACL" + + "" + + "" + + "" + + "rep:GrantACE" + + "" + + "" + + "everyone" + + "" + + "" + + "jcr:write" + + "" + + "" + + ""; + + private static final String XML_POLICY_TREE_3 = "" + + "" + + "" + + "rep:ACL" + + "" + + "" + + "" + + "rep:GrantACE" + + "" + + "" + + "everyone" + + "" + + "" + + "jcr:write" + + "" + + "" + + "" + + "" + + "rep:GrantACE" + + "" + + "" + + "admin" + + "" + + "" + + "jcr:write" + + "" + + "" + + ""; + + private static final String XML_POLICY_TREE_4 = "" + + "" + + "" + + "rep:ACL" + + "" + + "" + + "" + + "rep:GrantACE" + + "" + + "" + + "unknownprincipal" + + "" + + "" + + "jcr:write" + + "" + + "" + + "" + + "" + + "rep:GrantACE" + + "" + + "" + + "admin" + + "" + + "" + + "jcr:write" + + "" + + "" + + ""; + + private static final String XML_POLICY_TREE_5 = "" + + "" + + "" + + "rep:ACL" + + "" + + "" + + "" + + "rep:GrantACE" + + "" + + "" + + "admin" + + "" + + "" + + "jcr:write" + + "" + + "" + + ""; + + private static final String XML_REPO_POLICY_TREE = "" + + "" + + "" + + "rep:ACL" + + "" + + "" + + "" + + "rep:GrantACE" + + "" + + "" + + "admin" + + "" + + "" + + "jcr:workspaceManagement" + + "" + + "" + + ""; + + private static final String XML_AC_TREE = "rep:AccessControlrep:AccessControlrep:AccessControlrep:PrincipalAccessControlrep:ACLrep:GrantACE*/administratorsjcr:allrep:AccessControlrep:AccessControlrep:PrincipalAccessControlrep:PrincipalAccessControlrep:PrincipalAccessControl"; + + private static final String XML_POLICY_ONLY = "nt:unstructuredrep:AccessControllablemix:versionable0a0ca2e9-ab98-4433-a12b-d5728376520735d0d137-a3a4-4af3-8cdd-ce565ea6bdc9true35d0d137-a3a4-4af3-8cdd-ce565ea6bdc9428c9ef2-78e5-4f1c-95d3-16b4ce72d815rep:ACL"; + + + private SessionImpl sImpl; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!(superuser instanceof SessionImpl)) { + throw new NotExecutableException("SessionImpl expected"); + } + sImpl = (SessionImpl) superuser; + + // make sure the repository provides resource based policies. + AccessControlPolicyIterator it = sImpl.getAccessControlManager().getApplicablePolicies("/"); + if (!it.hasNext()) { + AccessControlPolicy[] pcs = sImpl.getAccessControlManager().getPolicies("/"); + if (pcs == null || pcs.length == 0) { + throw new NotExecutableException(); + } + + } // ok resource based acl + } + + private NodeImpl createPolicyNode(NodeImpl target) throws Exception { + try { + InputStream in = new ByteArrayInputStream(XML_POLICY_ONLY.getBytes("UTF-8")); + + SessionImporter importer = new SessionImporter(target, sImpl, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + return (NodeImpl) target.getNode("test/rep:policy"); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } finally { + superuser.refresh(false); + if (superuser.nodeExists("/test")) { + NodeIterator it = superuser.getRootNode().getNodes("test"); + while (it.hasNext()) { + it.nextNode().remove(); + } + } + superuser.save(); + } + } + + private static ProtectedNodeImporter createImporter() { + return new AccessControlImporter(); + } + + public void testWorkspaceImport() throws Exception { + boolean isWorkspaceImport = true; + ProtectedNodeImporter protectedImporter = new AccessControlImporter(); + protectedImporter.init(sImpl, sImpl, isWorkspaceImport, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, null); + + NodeImpl n = createPolicyNode((NodeImpl) testRootNode); + assertFalse(protectedImporter.start(n)); + } + + public void testNonProtectedNode() throws Exception { + if (!testRootNode.getDefinition().isProtected()) { + ProtectedNodeImporter piImporter = createImporter(); + piImporter.init(sImpl, sImpl, false, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, null); + assertFalse(piImporter.start((NodeImpl) testRootNode)); + } else { + throw new NotExecutableException(); + } + } + + public void testUnsupportedProtectedNode() throws Exception { + Node n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + + ProtectedNodeImporter piImporter = createImporter(); + piImporter.init(sImpl, sImpl, false, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, null); + assertFalse(piImporter.start((NodeImpl) n)); + } + + /** + * Imports a resource-based ACL containing a single entry. + * + * @throws Exception + */ + public void testImportACL() throws Exception { + NodeImpl target = (NodeImpl) testRootNode; + try { + + InputStream in = new ByteArrayInputStream(XML_POLICY_TREE.getBytes("UTF-8")); + SessionImporter importer = new SessionImporter(target, sImpl, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + assertTrue(target.hasNode("test")); + String path = target.getNode("test").getPath(); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(path); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(1, entries.length); + + AccessControlEntry entry = entries[0]; + assertEquals("everyone", entry.getPrincipal().getName()); + assertEquals(1, entry.getPrivileges().length); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + if(entry instanceof JackrabbitAccessControlEntry) { + assertTrue(((JackrabbitAccessControlEntry) entry).isAllow()); + } + + } finally { + superuser.refresh(false); + } + } + + /** + * Imports a resource-based ACL containing a single entry. + * + * @throws Exception + */ + public void testImportACLOnly() throws Exception { + try { + NodeImpl target = (NodeImpl) testRootNode.addNode(nodeName1); + target.addMixin("rep:AccessControllable"); + + InputStream in = new ByteArrayInputStream(XML_POLICY_TREE_3.getBytes("UTF-8")); + SessionImporter importer = new SessionImporter(target, sImpl, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + String path = target.getPath(); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(path); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(2, entries.length); + + AccessControlEntry entry = entries[0]; + assertEquals("everyone", entry.getPrincipal().getName()); + assertEquals(1, entry.getPrivileges().length); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + entry = entries[1]; + assertEquals("admin", entry.getPrincipal().getName()); + assertEquals(1, entry.getPrivileges().length); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + if(entry instanceof JackrabbitAccessControlEntry) { + assertTrue(((JackrabbitAccessControlEntry) entry).isAllow()); + } + } finally { + superuser.refresh(false); + } + } + + /** + * Imports a resource-based ACL containing a single entry. + * + * @throws Exception + */ + public void testImportACLRemoveACE() throws Exception { + try { + NodeImpl target = (NodeImpl) testRootNode.addNode(nodeName1); + target.addMixin("rep:AccessControllable"); + + InputStream in = new ByteArrayInputStream(XML_POLICY_TREE_3.getBytes("UTF-8")); + SessionImporter importer = new SessionImporter(target, sImpl, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + in = new ByteArrayInputStream(XML_POLICY_TREE_5.getBytes("UTF-8")); + importer = new SessionImporter(target, sImpl, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new PseudoConfig()); + ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + String path = target.getPath(); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(path); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(1, entries.length); + + AccessControlEntry entry = entries[0]; + assertEquals("admin", entry.getPrincipal().getName()); + assertEquals(1, entry.getPrivileges().length); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + if(entry instanceof JackrabbitAccessControlEntry) { + assertTrue(((JackrabbitAccessControlEntry) entry).isAllow()); + } + } finally { + superuser.refresh(false); + } + } + + /** + * Imports a resource-based ACL containing a single entry. + * + * @throws Exception + */ + public void testImportACLUnknown() throws Exception { + try { + NodeImpl target = (NodeImpl) testRootNode.addNode(nodeName1); + target.addMixin("rep:AccessControllable"); + + InputStream in = new ByteArrayInputStream(XML_POLICY_TREE_4.getBytes("UTF-8")); + SessionImporter importer = new SessionImporter(target, sImpl, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + String path = target.getPath(); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(path); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(2, entries.length); + + AccessControlEntry entry = entries[0]; + assertEquals("unknownprincipal", entry.getPrincipal().getName()); + assertEquals(1, entry.getPrivileges().length); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + entry = entries[1]; + assertEquals("admin", entry.getPrincipal().getName()); + assertEquals(1, entry.getPrivileges().length); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + if(entry instanceof JackrabbitAccessControlEntry) { + assertTrue(((JackrabbitAccessControlEntry) entry).isAllow()); + } + } finally { + superuser.refresh(false); + } + } + + /** + * Imports a resource-based ACL containing a single entry. + * + * @throws Exception + */ + public void testImportACLUnknownFail() throws Exception { + try { + NodeImpl target = (NodeImpl) testRootNode.addNode(nodeName1); + target.addMixin("rep:AccessControllable"); + + InputStream in = new ByteArrayInputStream(XML_POLICY_TREE_4.getBytes("UTF-8")); + PseudoConfig config = new PseudoConfig(); + ((AccessControlImporter) config.getProtectedItemImporters().get(0)).setImportBehavior("default"); + SessionImporter importer = new SessionImporter(target, sImpl, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, config); + ImportHandler ih = new ImportHandler(importer, sImpl); + try { + new ParsingContentHandler(ih).parse(in); + fail("importing unknown principal should fail based on configuration."); + } catch (Exception e) { + // ok + } + } finally { + superuser.refresh(false); + } + } + + /** + * Imports a resource-based ACL containing a single entry for a policy that + * already exists. + * + * @throws Exception + */ + public void testImportPolicyExists() throws Exception { + // this test does not work anymore, since the normal behavior is replace + // all ACEs for an import. maybe control this behavior via uuid-flag. + if (true) { + return; + } + + NodeImpl target = (NodeImpl) testRootNode; + target = (NodeImpl) target.addNode("test", "test:sameNameSibsFalseChildNodeDefinition"); + AccessControlManager acMgr = sImpl.getAccessControlManager(); + for (AccessControlPolicyIterator it = acMgr.getApplicablePolicies(target.getPath()); it.hasNext();) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + if (policy instanceof AccessControlList) { + Privilege[] privs = new Privilege[] {acMgr.privilegeFromName(Privilege.JCR_LOCK_MANAGEMENT)}; + ((AccessControlList) policy).addAccessControlEntry(sImpl.getPrincipalManager().getEveryone(), privs); + acMgr.setPolicy(target.getPath(), policy); + } + } + + try { + + InputStream in = new ByteArrayInputStream(XML_POLICY_TREE_2.getBytes("UTF-8")); + SessionImporter importer = new SessionImporter(target, sImpl, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + AccessControlPolicy[] policies = acMgr.getPolicies(target.getPath()); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(1, entries.length); + + AccessControlEntry entry = entries[0]; + assertEquals("everyone", entry.getPrincipal().getName()); + List privs = Arrays.asList(entry.getPrivileges()); + assertEquals(2, privs.size()); + assertTrue(privs.contains(acMgr.privilegeFromName(Privilege.JCR_WRITE)) && + privs.contains(acMgr.privilegeFromName(Privilege.JCR_LOCK_MANAGEMENT))); + + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + if(entry instanceof JackrabbitAccessControlEntry) { + assertTrue(((JackrabbitAccessControlEntry) entry).isAllow()); + } + + } finally { + superuser.refresh(false); + } + } + + /** + * Imports an empty resource-based ACL for a policy that already exists. + * + * @throws Exception + */ + public void testImportEmptyExistingPolicy() throws Exception { + NodeImpl target = (NodeImpl) testRootNode; + target = (NodeImpl) target.addNode("test", "test:sameNameSibsFalseChildNodeDefinition"); + AccessControlManager acMgr = sImpl.getAccessControlManager(); + for (AccessControlPolicyIterator it = acMgr.getApplicablePolicies(target.getPath()); it.hasNext();) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + if (policy instanceof AccessControlList) { + acMgr.setPolicy(target.getPath(), policy); + } + } + + try { + InputStream in = new ByteArrayInputStream(XML_POLICY_ONLY.getBytes("UTF-8")); + + SessionImporter importer = new SessionImporter(target, sImpl, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + AccessControlPolicy[] policies = acMgr.getPolicies(target.getPath()); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(0, entries.length); + + } finally { + superuser.refresh(false); + } + } + + /** + * Repo level acl must be imported underneath the root node. + * + * @throws Exception + */ + public void testImportRepoACLAtRoot() throws Exception { + NodeImpl target = (NodeImpl) sImpl.getRootNode(); + AccessControlManager acMgr = sImpl.getAccessControlManager(); + try { + // need to add mixin. in contrast to only using JCR API to retrieve + // and set the policies the protected item import only is called if + // the node to be imported is defined to be protected. however, if + // the root node doesn't have the mixin assigned the defining node + // type of the imported policy nodes will be rep:root (unstructured) + // and the items will not be detected as being protected. + target.addMixin("rep:RepoAccessControllable"); + InputStream in = new ByteArrayInputStream(XML_REPO_POLICY_TREE.getBytes("UTF-8")); + + SessionImporter importer = new SessionImporter(target, sImpl, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + AccessControlPolicy[] policies = acMgr.getPolicies(null); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(1, entries.length); + assertEquals(1, entries[0].getPrivileges().length); + assertEquals(acMgr.privilegeFromName("jcr:workspaceManagement"), entries[0].getPrivileges()[0]); + + assertTrue(target.hasNode("rep:repoPolicy")); + assertTrue(target.hasNode("rep:repoPolicy/allow")); + + // clean up again + acMgr.removePolicy(null, policies[0]); + assertFalse(target.hasNode("rep:repoPolicy")); + assertFalse(target.hasNode("rep:repoPolicy/allow")); + + } finally { + superuser.refresh(false); + } + } + + /** + * Make sure repo-level acl is not imported below any other node than the + * root node. + * + * @throws Exception + */ + public void testImportRepoACLAtTestNode() throws Exception { + NodeImpl target = (NodeImpl) testRootNode.addNode("test"); + target.addMixin("rep:RepoAccessControllable"); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + try { + InputStream in = new ByteArrayInputStream(XML_REPO_POLICY_TREE.getBytes("UTF-8")); + + SessionImporter importer = new SessionImporter(target, sImpl, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + AccessControlPolicy[] policies = acMgr.getPolicies(null); + assertEquals(0, policies.length); + + assertTrue(target.hasNode("rep:repoPolicy")); + assertFalse(target.hasNode("rep:repoPolicy/allow0")); + + Node n = target.getNode("rep:repoPolicy"); + assertEquals("rep:RepoAccessControllable", n.getDefinition().getDeclaringNodeType().getName()); + } finally { + superuser.refresh(false); + } + } + + /** + * Imports a principal-based ACL containing a single entry mist fail with + * the default configuration. + * + * @throws Exception + */ + public void testImportPrincipalBasedACL() throws Exception { + JackrabbitAccessControlManager acMgr = (JackrabbitAccessControlManager) sImpl.getAccessControlManager(); + if (acMgr.getApplicablePolicies(EveryonePrincipal.getInstance()).length > 0 || + acMgr.getPolicies(EveryonePrincipal.getInstance()).length > 0) { + // test expects that only resource-based acl is supported + throw new NotExecutableException(); + } + + PrincipalManager pmgr = sImpl.getPrincipalManager(); + if (!pmgr.hasPrincipal(SecurityConstants.ADMINISTRATORS_NAME)) { + UserManager umgr = sImpl.getUserManager(); + umgr.createGroup(new PrincipalImpl(SecurityConstants.ADMINISTRATORS_NAME)); + if (!umgr.isAutoSave()) { + sImpl.save(); + } + if (pmgr.hasPrincipal(SecurityConstants.ADMINISTRATORS_NAME)) { + throw new NotExecutableException(); + } + } + + + NodeImpl target; + NodeImpl root = (NodeImpl) sImpl.getRootNode(); + if (!root.hasNode(AccessControlConstants.N_ACCESSCONTROL)) { + target = root.addNode(AccessControlConstants.N_ACCESSCONTROL, AccessControlConstants.NT_REP_ACCESS_CONTROL, null); + } else { + target = root.getNode(AccessControlConstants.N_ACCESSCONTROL); + if (!target.isNodeType(AccessControlConstants.NT_REP_ACCESS_CONTROL)) { + target.setPrimaryType(sImpl.getJCRName(AccessControlConstants.NT_REP_ACCESS_CONTROL)); + } + } + try { + + InputStream in = new ByteArrayInputStream(XML_AC_TREE.getBytes("UTF-8")); + + SessionImporter importer = new SessionImporter(target, sImpl, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, new PseudoConfig()); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + fail("Default config only allows resource-based ACL -> protected import must fail"); + + } catch (SAXException e) { + if (e.getException() instanceof ConstraintViolationException) { + // success + } else { + throw e; + } + } finally { + superuser.refresh(false); + } + } + + /** + * With the default importer that isn't able to deal with ACEs the + * policy will be created but any ACEs will be ignored. + * + * @throws Exception + */ + public void testImportWithDefaultImporter() throws Exception { + NodeImpl target = (NodeImpl) testRootNode; + try { + + InputStream in = new ByteArrayInputStream(XML_POLICY_TREE.getBytes("UTF-8")); + + SessionImporter importer = new SessionImporter(target, sImpl, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, null); + ImportHandler ih = new ImportHandler(importer, sImpl); + new ParsingContentHandler(ih).parse(in); + + assertTrue(target.hasNode("test")); + String path = target.getNode("test").getPath(); + + AccessControlManager acMgr = sImpl.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(path); + + assertEquals(1, policies.length); + assertTrue(policies[0] instanceof JackrabbitAccessControlList); + + AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries(); + assertEquals(0, entries.length); + + } finally { + superuser.refresh(false); + } + } + + private final class PseudoConfig extends ImportConfig { + + private final ProtectedNodeImporter aci; + + private PseudoConfig() { + this.aci = createImporter(); + } + + @Override + public List getProtectedItemImporters() { + return Collections.singletonList(aci); + } + } +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/DocumentViewTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/DocumentViewTest.java new file mode 100644 index 00000000000..32eadda6d49 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/DocumentViewTest.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.core.xml; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import org.apache.jackrabbit.api.JackrabbitNodeTypeManager; +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Jackrabbit-specific test cases for the document view XML format. + * + * @see org.apache.jackrabbit.test.api.ExportDocViewTest + * @see org.apache.jackrabbit.test.api.DocumentViewImportTest + */ +public class DocumentViewTest extends AbstractJCRTest { + + /** + * Sets up the test fixture. + * + * @throws Exception if an unexpected error occurs + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + JackrabbitNodeTypeManager manager = (JackrabbitNodeTypeManager) + superuser.getWorkspace().getNodeTypeManager(); + try { + manager.getNodeType("DocViewMultiValueTest"); + } catch (NoSuchNodeTypeException e) { + String cnd = "[DocViewMultiValueTest] - test (boolean) multiple"; + Reader cndReader = new InputStreamReader(new ByteArrayInputStream(cnd.getBytes("UTF-8"))); + CndImporter.registerNodeTypes(cndReader, superuser); + } + } + + /** + * Tears down the test fixture. + * + * @throws Exception if an unexpected error occurs + */ + @Override + protected void tearDown() throws Exception { + // TODO: Unregister the MultiValueTestType node type once Jackrabbit + // supports node type removal. + super.tearDown(); + } + + /** + * Test case for + * JCR-369: + * IllegalNameException when importing document view with two mixins. + * + * @throws Exception if an unexpected error occurs + */ + public void testTwoMixins() throws Exception { + try { + String xml = ""; + InputStream input = new ByteArrayInputStream(xml.getBytes("UTF-8")); + superuser.importXML( + "/", input, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + } catch (ValueFormatException e) { + fail("JCR-369: IllegalNameException when importing document view" + + " with two mixins"); + } + } + + /** + * Test case for + * JCR-325: + * docview roundtripping does not work with multivalue non-string properties + * + * @throws Exception if an unexpected error occurs + */ + public void testMultiValue() throws Exception { + String message = "JCR-325: docview roundtripping does not work with" + + " multivalue non-string properties"; + + Node root = superuser.getRootNode(); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + Node node = root.addNode("multi-value-test", "DocViewMultiValueTest"); + node.setProperty("test", new String[] {"true", "false"}); + superuser.exportDocumentView("/multi-value-test", buffer, true, true); + superuser.refresh(false); // Discard the transient multi-value-test node + + superuser.importXML( + "/", new ByteArrayInputStream(buffer.toByteArray()), + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + try { + Property property = root.getProperty("multi-value-test/test"); + assertTrue(message, property.isMultiple()); + assertEquals(message, property.getValues().length, 2); + assertTrue(message, property.getValues()[0].getBoolean()); + assertFalse(message, property.getValues()[1].getBoolean()); + } catch (PathNotFoundException e) { + fail(message); + } + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/TestAll.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/TestAll.java new file mode 100644 index 00000000000..bbf1b4f7cd5 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/TestAll.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.core.xml; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite for Jackrabbit-specific XML format test cases. + */ +public class TestAll extends TestCase { + + /** + * Returns a {@link Test} suite that executes all tests inside this + * package. + * + * @return test suite + */ + public static Test suite() { + TestSuite suite = new TestSuite("XML format test cases"); + suite.addTestSuite(DocumentViewTest.class); + suite.addTestSuite(AccessControlImporterTest.class); + return suite; + } + +} diff --git a/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/WorkspaceImporterTest.java b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/WorkspaceImporterTest.java new file mode 100644 index 00000000000..d65079c4b29 --- /dev/null +++ b/jackrabbit-core/src/test/java/org/apache/jackrabbit/core/xml/WorkspaceImporterTest.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.core.xml; + +import java.io.ByteArrayInputStream; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.core.id.NodeId; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test cases for the {@link WorkspaceImporter} class. + */ +public class WorkspaceImporterTest extends AbstractJCRTest { + + private Node root; + + protected void setUp() throws Exception { + super.setUp(); + root = superuser.getRootNode().addNode("WorkspaceImporterTest"); + superuser.save(); + } + + protected void tearDown() throws Exception { + root.remove(); + superuser.save(); + super.tearDown(); + } + + /** + * Tests that an XML document with an internal reference is correctly + * imported. This functionality got broken by JCR-569. + * + * @throws Exception if an unexpected error occurs + */ + public void testReferenceImport() throws Exception { + try { + NodeId id = NodeId.randomId(); + String xml = + "" + + "" + + "nt:unstructured" + + "" + + "" + + "nt:unstructured" + + "" + + "mix:referenceable" + + "" + + "" + id + "" + + "" + + "" + + "" + + "nt:unstructured" + + "" + + "" + id + "" + + "" + + ""; + superuser.getWorkspace().importXML( + root.getPath(), + new ByteArrayInputStream(xml.getBytes("UTF-8")), + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + + Node b = root.getNode("a/b"); + Node c = root.getNode("a/c"); + assertTrue( + "Imported reference points to the correct node", + b.isSame(c.getProperty("ref").getNode())); + } catch (PathNotFoundException e) { + fail("Imported node or property not found"); + } catch (RepositoryException e) { + fail("Failed to import an XML document with an internal reference"); + } + } + +} diff --git a/jackrabbit-core/src/test/repository-descriptor-overlay/repository.xml b/jackrabbit-core/src/test/repository-descriptor-overlay/repository.xml new file mode 100644 index 00000000000..93876b89d58 --- /dev/null +++ b/jackrabbit-core/src/test/repository-descriptor-overlay/repository.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/default/workspace.xml b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/default/workspace.xml new file mode 100644 index 00000000000..2f9e45d8b35 --- /dev/null +++ b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/default/workspace.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v1/workspace.xml b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v1/workspace.xml new file mode 100644 index 00000000000..b23783ab1ee --- /dev/null +++ b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v1/workspace.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v2/workspace.xml b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v2/workspace.xml new file mode 100644 index 00000000000..03f62c3f564 --- /dev/null +++ b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v2/workspace.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v3/workspace.xml b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v3/workspace.xml new file mode 100644 index 00000000000..6bab0c393ab --- /dev/null +++ b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/index-format-v3/workspace.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/indexing-test/workspace.xml b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/indexing-test/workspace.xml new file mode 100644 index 00000000000..abd59d21a55 --- /dev/null +++ b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/indexing-test/workspace.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/security/workspace.xml b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/security/workspace.xml new file mode 100644 index 00000000000..7fabb8f649e --- /dev/null +++ b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/security/workspace.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/wsp-init-test/workspace.xml b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/wsp-init-test/workspace.xml new file mode 100644 index 00000000000..301e2a927f5 --- /dev/null +++ b/jackrabbit-core/src/test/repository-descriptor-overlay/workspaces/wsp-init-test/workspace.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/repository.xml b/jackrabbit-core/src/test/repository/repository.xml new file mode 100644 index 00000000000..c3e80fa0e1a --- /dev/null +++ b/jackrabbit-core/src/test/repository/repository.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/default/synonyms.properties b/jackrabbit-core/src/test/repository/workspaces/default/synonyms.properties new file mode 100644 index 00000000000..74c6885d01d --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/default/synonyms.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 a synonym definition file for PropertiesSynonymProvider +ASF=Apache Software Foundation +quick=fast +sluggish=lazy diff --git a/jackrabbit-core/src/test/repository/workspaces/default/workspace.xml b/jackrabbit-core/src/test/repository/workspaces/default/workspace.xml new file mode 100644 index 00000000000..035b4c86386 --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/default/workspace.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/_2.cfs b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/_2.cfs new file mode 100644 index 00000000000..3794bf7accc Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/_2.cfs differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/deletable b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/deletable new file mode 100644 index 00000000000..593f4708db8 Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/deletable differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/segments b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/segments new file mode 100644 index 00000000000..ffe36ef8046 Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/_0/segments differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/indexes b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/indexes new file mode 100644 index 00000000000..97ab9543bce Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/index/indexes differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/5a/9a/d0fcc7f542bbb435bcb9ed30a2e2.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/5a/9a/d0fcc7f542bbb435bcb9ed30a2e2.n new file mode 100644 index 00000000000..28a85bfdd08 Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/5a/9a/d0fcc7f542bbb435bcb9ed30a2e2.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/ca/fe/babecafebabecafebabecafebabe.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/ca/fe/babecafebabecafebabecafebabe.n new file mode 100644 index 00000000000..1bbc79475ec Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/ca/fe/babecafebabecafebabecafebabe.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/de/ad/beefcafebabecafebabecafebabe.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/de/ad/beefcafebabecafebabecafebabe.n new file mode 100644 index 00000000000..ef273502a0a Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/items/de/ad/beefcafebabecafebabecafebabe.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/names.properties b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/names.properties new file mode 100644 index 00000000000..3174948c22b --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/names.properties @@ -0,0 +1,5 @@ +#string index +#Wed Apr 02 18:14:08 CEST 2008 +root=1 +system=0 +unstructured=2 diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/namespaces.properties b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/namespaces.properties new file mode 100644 index 00000000000..3aea0505b38 --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/namespaces.properties @@ -0,0 +1,6 @@ +#string index +#Mon Apr 07 10:28:09 CEST 2008 +http\://www.jcp.org/jcr/1.0=1 +internal=0 +http\://www.jcp.org/jcr/nt/1.0=2 +=3 diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v1/workspace.xml b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/workspace.xml new file mode 100644 index 00000000000..84ebb470358 --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v1/workspace.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/_0.cfs b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/_0.cfs new file mode 100644 index 00000000000..dd6b7ece032 Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/_0.cfs differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments.gen b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments.gen new file mode 100644 index 00000000000..e9fa6008bec Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments.gen differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments_1 b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments_1 new file mode 100644 index 00000000000..64960a808df Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments_1 differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments_3 b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments_3 new file mode 100644 index 00000000000..4da6ab859c7 Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/_0/segments_3 differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/indexes b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/indexes new file mode 100644 index 00000000000..97ab9543bce Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/index/indexes differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/c9/bb/26c0edf0408b8ab22e88c1edc593.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/c9/bb/26c0edf0408b8ab22e88c1edc593.n new file mode 100644 index 00000000000..28a85bfdd08 Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/c9/bb/26c0edf0408b8ab22e88c1edc593.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/ca/fe/babecafebabecafebabecafebabe.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/ca/fe/babecafebabecafebabecafebabe.n new file mode 100644 index 00000000000..ad765789cde Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/ca/fe/babecafebabecafebabecafebabe.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/de/ad/beefcafebabecafebabecafebabe.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/de/ad/beefcafebabecafebabecafebabe.n new file mode 100644 index 00000000000..ef273502a0a Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/items/de/ad/beefcafebabecafebabecafebabe.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/names.properties b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/names.properties new file mode 100644 index 00000000000..5bc963019bc --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/names.properties @@ -0,0 +1,5 @@ +#string index +#Wed Apr 02 18:21:04 CEST 2008 +root=1 +system=0 +unstructured=2 diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/namespaces.properties b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/namespaces.properties new file mode 100644 index 00000000000..95b3d95e14b --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/namespaces.properties @@ -0,0 +1,6 @@ +#string index +#Mon Apr 07 10:40:50 CEST 2008 +http\://www.jcp.org/jcr/1.0=1 +internal=0 +http\://www.jcp.org/jcr/nt/1.0=2 +=3 diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v2/workspace.xml b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/workspace.xml new file mode 100644 index 00000000000..19e4f3c837a --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v2/workspace.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/c9/bb/26c0edf0408b8ab22e88c1edc593.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/c9/bb/26c0edf0408b8ab22e88c1edc593.n new file mode 100644 index 00000000000..28a85bfdd08 Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/c9/bb/26c0edf0408b8ab22e88c1edc593.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/ca/fe/babecafebabecafebabecafebabe.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/ca/fe/babecafebabecafebabecafebabe.n new file mode 100644 index 00000000000..ad765789cde Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/ca/fe/babecafebabecafebabecafebabe.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/de/ad/beefcafebabecafebabecafebabe.n b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/de/ad/beefcafebabecafebabecafebabe.n new file mode 100644 index 00000000000..ef273502a0a Binary files /dev/null and b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/items/de/ad/beefcafebabecafebabecafebabe.n differ diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v3/names.properties b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/names.properties new file mode 100644 index 00000000000..5bc963019bc --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/names.properties @@ -0,0 +1,5 @@ +#string index +#Wed Apr 02 18:21:04 CEST 2008 +root=1 +system=0 +unstructured=2 diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v3/namespaces.properties b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/namespaces.properties new file mode 100644 index 00000000000..95b3d95e14b --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/namespaces.properties @@ -0,0 +1,6 @@ +#string index +#Mon Apr 07 10:40:50 CEST 2008 +http\://www.jcp.org/jcr/1.0=1 +internal=0 +http\://www.jcp.org/jcr/nt/1.0=2 +=3 diff --git a/jackrabbit-core/src/test/repository/workspaces/index-format-v3/workspace.xml b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/workspace.xml new file mode 100644 index 00000000000..e3874cb1e81 --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/index-format-v3/workspace.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/indexing-test-2/indexing-configuration.xml b/jackrabbit-core/src/test/repository/workspaces/indexing-test-2/indexing-configuration.xml new file mode 100644 index 00000000000..e99d0b520a5 --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/indexing-test-2/indexing-configuration.xml @@ -0,0 +1,26 @@ + + + + + + + + + + * + * + + + + jcr:content + jcr:content/* + jcr:content/jcr:lastModified + + + + aggregated-node + child/property + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/indexing-test-2/workspace.xml b/jackrabbit-core/src/test/repository/workspaces/indexing-test-2/workspace.xml new file mode 100644 index 00000000000..cafcc6de345 --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/indexing-test-2/workspace.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/indexing-test/indexing-configuration.xml b/jackrabbit-core/src/test/repository/workspaces/indexing-test/indexing-configuration.xml new file mode 100644 index 00000000000..eb7f91d6a88 --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/indexing-test/indexing-configuration.xml @@ -0,0 +1,66 @@ + + + + + + + text + + + + + text + + + + + text + + + + + text + + + + + text + + + + .*Text + + + + title + foo + text + + + + Text + + + + + + + + * + * + + + + jcr:content + jcr:content/* + jcr:content/jcr:lastModified + + + + aggregated-node + child/property + + + \ No newline at end of file diff --git a/jackrabbit-core/src/test/repository/workspaces/indexing-test/workspace.xml b/jackrabbit-core/src/test/repository/workspaces/indexing-test/workspace.xml new file mode 100644 index 00000000000..a57db9532ed --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/indexing-test/workspace.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/security/workspace.xml b/jackrabbit-core/src/test/repository/workspaces/security/workspace.xml new file mode 100644 index 00000000000..92383909f2a --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/security/workspace.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/repository/workspaces/wsp-init-test/workspace.xml b/jackrabbit-core/src/test/repository/workspaces/wsp-init-test/workspace.xml new file mode 100644 index 00000000000..f3fe718c89e --- /dev/null +++ b/jackrabbit-core/src/test/repository/workspaces/wsp-init-test/workspace.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/META-INF/services/org.apache.tika.parser.Parser b/jackrabbit-core/src/test/resources/META-INF/services/org.apache.tika.parser.Parser new file mode 100644 index 00000000000..6127bb69d7e --- /dev/null +++ b/jackrabbit-core/src/test/resources/META-INF/services/org.apache.tika.parser.Parser @@ -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.core.query.lucene.BlockingParser diff --git a/jackrabbit-core/src/test/resources/cnd-reader-test-input.cnd b/jackrabbit-core/src/test/resources/cnd-reader-test-input.cnd new file mode 100644 index 00000000000..93001610ae6 --- /dev/null +++ b/jackrabbit-core/src/test/resources/cnd-reader-test-input.cnd @@ -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. + */ + + + +[ex:NodeType] > ex:ParentNodeType1, ex:ParentNodeType2 + orderable mixin abstract noquery + - ex:property (long) = '1', '2' primary mandatory autocreated protected multiple queryops '=, <>' nofulltext noqueryorder version < '[1,10]' + + ex:node (ex:RequiredNodeType1, ex:RequiredNodeType2) = ex:RequiredNodeType1 mandatory autocreated protected multiple version + +[ex:AnotherNodeType] > ex:NodeType + - * (string) = 'a residual property' multiple + + * (ex:RequiredNodeType1) multiple + +[ex:Reference] + - ex:ref (reference) mandatory protected < 'ex:ref' + +[ex:Name] + - ex:name (name) < 'ex:name' + +[ex:Path] + - ex:path (path) < 'ex:a/ex:b' + +[ex:Empty] \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/jaas.config b/jackrabbit-core/src/test/resources/jaas.config new file mode 100644 index 00000000000..be96179bfe5 --- /dev/null +++ b/jackrabbit-core/src/test/resources/jaas.config @@ -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. + */ +defaultLoginModuleTest { + org.apache.jackrabbit.core.security.authentication.DefaultLoginModule required + disableTokenAuth="true" + tokenExpiration="25"; +}; diff --git a/jackrabbit-core/src/test/resources/logback-test.xml b/jackrabbit-core/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..2b7579c25f1 --- /dev/null +++ b/jackrabbit-core/src/test/resources/logback-test.xml @@ -0,0 +1,49 @@ + + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + testclass + junit + + + + target/surefire-reports/${testclass}.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/api/test_mixin_nodetypes.cnd b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/api/test_mixin_nodetypes.cnd new file mode 100644 index 00000000000..55f8658f1d5 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/api/test_mixin_nodetypes.cnd @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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:A] mixin + - test:propA (STRING) + +[test:AA] > test:A + mixin + - test:propAA (STRING) diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml new file mode 100644 index 00000000000..6b73992ebbc --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository-h2.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository-with-test-journal.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository-with-test-journal.xml new file mode 100644 index 00000000000..e8f10de1013 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository-with-test-journal.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository.xml new file mode 100644 index 00000000000..d6da33e08a1 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/cluster/repository.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/config/workspace.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/config/workspace.xml new file mode 100644 index 00000000000..6f96ce6b960 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/config/workspace.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/integration/repository-with-SimpleFSDirectory.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/integration/repository-with-SimpleFSDirectory.xml new file mode 100644 index 00000000000..9e1b5df3578 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/integration/repository-with-SimpleFSDirectory.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/integration/words.txt b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/integration/words.txt new file mode 100644 index 00000000000..fd9c33b2368 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/integration/words.txt @@ -0,0 +1,518 @@ +a +able +about +above +access +achieve +achieved +actual +actually +adding +addition +additional +address +adds +again +all +allow +allowed +allows +along +also +an +analogous +analyze +and +another +any +anymore +applicable +application +apply +approach +arbitrary +are +argument +arguments +arithmetic +around +art +article +articles +as +ascii +ask +at +authors +automatically +available +base +basically +be +becomes +before +behind +belong +better +blog +boiled +both +bound +bounds +branches +but +by +calculus +call +callable +called +calling +calls +came +can +cannot +carried +carry +case +cases +casted +casts +certain +church +closest +code +collection +comes +comment +compile +compiler +complain +complains +composes +concretes +constraint +constructing +contract +conversion +conversions +convert +converted +convertible +converting +converts +correct +correspond +corresponding +could +create +credits +current +decided +define +defines +defining +degree +depth +depths +designing +despite +detail +details +determine +did +difficulties +directly +discuss +discussed +disparate +dispatch +dispatched +dispatcher +do +does +don't +done +double +down +dropped +due +each +earlier +easily +easy +eating +editor +effect +effectively +either +element +employed +employers +emulate +enable +encode +encoded +encoding +encodings +end +entire +equal +erases +erasure +error +evaluate +even +every +exactly +existential +expands +expected +explain +explicitly +express +extension +fact +failed +far +finally +find +finds +fine +first +fit +fixed +follow +following +follows +for +form +formatted +former +from +full +function +further +general +generalization +generalize +generalized +generic +generics +gentle +give +given +goals +got +guess +had +handle +handling +has +have +having +helps +henry +here +hidden +higher +how +however +i +idea +if +implement +implementation +implemented +implicit +implicitly +impression +in +indeed +individual +induction +infix +information +inherent +initial +inside +instance +instances +instantiated +instead +int +integer +interested +into +ints +involved +is +issue +issues +it +iteratively +its +itself +java +jcr +just +keeps +kind +kinds +lambda +last +later +latter +lattice +least +leaves +legal +lets +library +like +limitations +limited +limits +line +lines +list +lists +look +looks +matching +matter +may +me +means +mechanism +mention +mentioned +meta +method +methods +might +mimic +mind +more +much +multiplication +my +natural +need +new +next +nicely +no +nodes +not +noted +now +nth +null +number +numbers +numerals +nutshell +objects +of +on +once +one +only +open +operations +operator +operators +or +order +original +originally +other +outputs +over +overloading +page +paper +parameter +parameters +pass +passed +passes +passing +pattern +picture +pimp +place +playing +plays +plus +pointing +polymorphic +pose +possible +post +posted +pre +preparation +previous +print +probably +problem +problematic +programming +properties +proposed +provided +puzzle +random +rather +recently +recursive +related +reports +represent +representing +requirement +respective +responsible +restrict +restricted +restriction +result +return +returns +reuse +revisited +right +rights +safe +safety +same +saved +say +scala +scale +scarify +scenes +scope +searches +second +sections +see +seems +select +serve +shortcoming +shots +should +show +showed +shows +signature +similar +similarly +simpler +simplified +simply +since +so +solution +some +source +special +specialized +specific +specifying +stands +step +still +string +strings +structural +structure +structures +stumbled +stupid +subsequently +succeed +successor +such +supply +switch +syntax +system +t +take +taken +takes +target +technique +terms +that +the +their +them +themselves +then +theory +there +these +things +think +third +this +thus +time +to +topic +towards +treated +tree +trick +tried +trouble +try +two +type +typed +types +under +understand +understanding +until +up +upper +us +use +used +using +value +values +version +view +want +wanted +ware +was +way +ways +we +well +what +when +where +which +while +whole +whose +why +will +with +without +work +works +wrapped +write +wrote +yet +you +zero \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/nodetypes.dtd b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/nodetypes.dtd new file mode 100644 index 00000000000..357db601101 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/nodetypes.dtd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_mixin_nodetypes.cnd b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_mixin_nodetypes.cnd new file mode 100644 index 00000000000..6d68bbc2718 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_mixin_nodetypes.cnd @@ -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. + */ + + +[test:mixinNode_protectedchild] mixin + + test:protectedchild (nt:unstructured) = nt:unstructured protected autocreated diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd new file mode 100644 index 00000000000..2a21000e665 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodestatemerger_nodetypes.cnd @@ -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. + */ + + +[test:mixinProp_1] mixin + - test:prop_double (double) + - test:prop_string (string) + +[test:mixinProp_2] mixin + - test:prop_name_p (name) protected + +[test:mixinProp_3] mixin + - test:prop_boolean_p (boolean) = 'true' protected autocreated + +[test:mixinProp_4] mixin + - * (string) + +[test:mixinProp_5] mixin + - test:prop_long_p (long) = '123' autocreated + + + +[test:mixinNode_1] mixin + + test:child_1 (nt:unstructured) = nt:unstructured + +[test:mixinNode_2] mixin + + test:child_2 (nt:unstructured) = nt:unstructured protected + +[test:mixinNode_3] mixin + + test:child_3 (nt:unstructured) = nt:unstructured protected autocreated + +[test:mixinNode_4] mixin + + * (nt:unstructured) = nt:unstructured \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodetypes.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodetypes.xml new file mode 100644 index 00000000000..e4dcf493437 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_nodetypes.xml @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + "defvalue" + + + + + + + + + + + + + + + + + [0,) + + + + + true + false + + + true + + + + + + [2005-01-01T00:00:00.000Z,2006-01-01T00:00:00.000Z) + + + + 2005-01-01T00:00:00.000Z + + + + + [,0.0) + (1.0,2.0) + (3.0,] + + + 1.5 + + + + + (-10,0] + [1,2] + [10,100) + + + 25 + + + + + test:testName + + + test:testName + + + + + /test:testPath + + + + + /test:testPath/* + + + + + test:testPath/* + + + + + nt:base + + + + + bananas? + + + banana + bananas + + + + + + + + + + + + nt:base + nt:unstructured + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_ns_cnd_nodetypes.cnd b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_ns_cnd_nodetypes.cnd new file mode 100644 index 00000000000..ed705bb8510 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_ns_cnd_nodetypes.cnd @@ -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. + */ + + + + +[testns3:emptyNodeType] > nt:base + +[testns4:mixinNodeType] + mixin + +[testns3:orderableNodeType] > nt:base + orderable + +[testns4:itemNodeType] > nt:base + - testns4:myDouble (double) ignore + - testns4:myBool (boolean) protected diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_ns_xml_nodetypes.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_ns_xml_nodetypes.xml new file mode 100644 index 00000000000..ab56701aed4 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_ns_xml_nodetypes.xml @@ -0,0 +1,223 @@ + + + + + + + nt:base + + + + + + + + nt:base + + + + + + + nt:base + + + + + + + + + + + + + + + + nt:base + + + + + [0,) + + + + + true + false + + + true + + + + + + [2005-01-01T00:00:00.000Z,2006-01-01T00:00:00.000Z) + + + + 2005-01-01T00:00:00.000Z + + + + + [,0.0) + (1.0,2.0) + (3.0,] + + + 1.5 + + + + + (-10,0] + [1,2] + [10,100) + + + 25 + + + + + testns1:testName + + + testns1:testName + + + + + /testns1:testPath + + + + + bananas? + + + banana + bananas + + + + + + + + nt:base + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_same_nt_name_cnd_nodetypes.cnd b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_same_nt_name_cnd_nodetypes.cnd new file mode 100644 index 00000000000..8f6566b13b0 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_same_nt_name_cnd_nodetypes.cnd @@ -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. + */ + + + +[testns6:sameNodeType] > nt:base + +[testns6:sameNodeType] > nt:unstructured diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_same_nt_name_xml_nodetypes.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_same_nt_name_xml_nodetypes.xml new file mode 100644 index 00000000000..e63f2d6bf55 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/nodetype/xml/test_same_nt_name_xml_nodetypes.xml @@ -0,0 +1,39 @@ + + + + + + + nt:base + + + + + + nt:unstructured + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config1.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config1.xml new file mode 100644 index 00000000000..3c97f998eaa --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config1.xml @@ -0,0 +1,26 @@ + + + + + + + .* + + + \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config2.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config2.xml new file mode 100644 index 00000000000..a38673ffe12 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config2.xml @@ -0,0 +1,26 @@ + + + + + + + .*:.* + + + \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config3.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config3.xml new file mode 100644 index 00000000000..8a43cc04765 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config3.xml @@ -0,0 +1,26 @@ + + + + + + + jcr:.* + + + \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config4.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config4.xml new file mode 100644 index 00000000000..b561ded2139 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config4.xml @@ -0,0 +1,27 @@ + + + + + + + foo + .*:.* + + + \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config5.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config5.xml new file mode 100644 index 00000000000..a71509a9c92 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/lucene/indexing_config5.xml @@ -0,0 +1,28 @@ + + + + + + + jcr:title + + + + \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/test.rtf b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/test.rtf new file mode 100644 index 00000000000..e91b1005eb5 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/test.rtf @@ -0,0 +1,157 @@ +{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff31507\deff0\stshfdbch31506\stshfloch31506\stshfhich31506\stshfbi31507\deflang2055\deflangfe2055\themelang2055\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f34\fbidi \froman\fcharset1\fprq2{\*\panose 02040503050406030204}Cambria Math;} +{\f37\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria;} +{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} +{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;} +{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f39\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f40\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\f42\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f43\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f44\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f45\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\f46\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f47\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f409\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\f410\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;} +{\f412\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\f413\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\f416\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\fhimajor\f31528\fbidi \froman\fcharset238\fprq2 Cambria CE;}{\fhimajor\f31529\fbidi \froman\fcharset204\fprq2 Cambria Cyr;}{\fhimajor\f31531\fbidi \froman\fcharset161\fprq2 Cambria Greek;}{\fhimajor\f31532\fbidi \froman\fcharset162\fprq2 Cambria Tur;} +{\fhimajor\f31535\fbidi \froman\fcharset186\fprq2 Cambria Baltic;}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} +{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} +{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} +{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} +{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} +{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;} +{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;} +{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} +{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} +{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0; +\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128; +\red192\green192\blue192;}{\*\defchp \f31506\fs22\lang2055\langfe1033\langfenp1033 }{\*\defpap \ql \li0\ri0\sa200\sl276\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{ +\ql \li0\ri0\sa200\sl276\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \f31506\fs22\lang2055\langfe1033\cgrid\langnp2055\langfenp1033 +\snext0 \sqformat \spriority0 \styrsid15293873 Normal;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tscellwidthfts0\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa200\sl276\slmult1 +\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 \f31506\fs22\lang2055\langfe1033\cgrid\langnp2055\langfenp1033 \snext11 \ssemihidden \sunhideused \sqformat Normal Table;}} +{\*\rsidtbl \rsid11742246\rsid15293873}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\author mreutegg}{\operator mreutegg}{\creatim\yr2008\mo10\dy23\hr10\min7} +{\revtim\yr2008\mo10\dy23\hr10\min8}{\version1}{\edmins0}{\nofpages1}{\nofwords6}{\nofchars39}{\*\company Day Software AG}{\nofcharsws44}{\vern32895}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}} +\paperw11906\paperh16838\margl1417\margr1417\margt1417\margb1134\gutter0\ltrsect +\deftab708\widowctrl\ftnbj\aenddoc\hyphhotz425\trackmoves1\trackformatting1\donotembedsysfont1\relyonvml0\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0 +\showxmlerrors1\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace180\dgvspace180\dghorigin1417\dgvorigin1417\dghshow1\dgvshow1 +\jexpand\viewkind1\viewscale140\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct +\asianbrkrule\rsidroot11742246\newtblstyruls\nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat \fet0 +{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\headery708\footery708\colsx708\endnhere\sectlinegrid360\sectdefaultcl\sectrsid15293873\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}} +{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}} +{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9 +\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\ql \li0\ri0\sa200\sl276\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs22\alang1025 \ltrch\fcs0 +\f31506\fs22\lang2055\langfe1033\cgrid\langnp2055\langfenp1033 {\rtlch\fcs1 \af31507 \ltrch\fcs0 \lang1033\langfe1033\langnp1033\insrsid11742246\charrsid11742246 The quick brown fox jumps over the lazy dog.}{\rtlch\fcs1 \af31507 \ltrch\fcs0 +\lang1033\langfe1033\langnp1033\insrsid15293873\charrsid11742246 +\par }{\*\themedata 504b030414000600080000002100828abc13fa0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb6ac3301045f785fe83d0b6d8 +72ba28a5d8cea249777d2cd20f18e4b12d6a8f843409c9df77ecb850ba082d74231062ce997b55ae8fe3a00e1893f354e9555e6885647de3a8abf4fbee29bbd7 +2a3150038327acf409935ed7d757e5ee14302999a654e99e393c18936c8f23a4dc072479697d1c81e51a3b13c07e4087e6b628ee8cf5c4489cf1c4d075f92a0b +44d7a07a83c82f308ac7b0a0f0fbf90c2480980b58abc733615aa2d210c2e02cb04430076a7ee833dfb6ce62e3ed7e14693e8317d8cd0433bf5c60f53fea2fe7 +065bd80facb647e9e25c7fc421fd2ddb526b2e9373fed4bb902e182e97b7b461e6bfad3f010000ffff0300504b030414000600080000002100a5d6a7e7c00000 +00360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4fc7060abb08 +84a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b63095120f88d94fbc +52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462a1a82fe353 +bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f7468656d652f7468 +656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b4b0d592c9c +070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b4757e8d3f7 +29e245eb2b260a0238fd010000ffff0300504b03041400060008000000210096b5ade296060000501b0000160000007468656d652f7468656d652f7468656d65 +312e786d6cec594f6fdb3614bf0fd87720746f6327761a07758ad8b19b2d4d1bc46e871e698996d850a240d2497d1bdae38001c3ba618715d86d87615b8116d8 +a5fb34d93a6c1dd0afb0475292c5585e9236d88aad3e2412f9e3fbff1e1fa9abd7eec70c1d1221294fda5efd72cd4324f1794093b0eddd1ef62fad79482a9c04 +98f184b4bd2991deb58df7dfbb8ad755446282607d22d771db8b944ad79796a40fc3585ee62949606ecc458c15bc8a702910f808e8c66c69b9565b5d8a314d3c +94e018c8de1a8fa94fd05093f43672e23d06af89927ac06762a049136785c10607758d9053d965021d62d6f6804fc08f86e4bef210c352c144dbab999fb7b471 +7509af678b985ab0b6b4ae6f7ed9ba6c4170b06c788a705430adf71bad2b5b057d03606a1ed7ebf5babd7a41cf00b0ef83a6569632cd467faddec9699640f671 +9e76b7d6ac355c7c89feca9cccad4ea7d36c65b258a206641f1b73f8b5da6a6373d9c11b90c537e7f08dce66b7bbeae00dc8e257e7f0fd2badd5868b37a088d1 +e4600ead1ddaef67d40bc898b3ed4af81ac0d76a197c86826828a24bb318f3442d8ab518dfe3a20f000d6458d104a9694ac6d88728eee2782428d60cf03ac1a5 +193be4cbb921cd0b495fd054b5bd0f530c1931a3f7eaf9f7af9e3f45c70f9e1d3ff8e9f8e1c3e3073f5a42ceaa6d9c84e5552fbffdeccfc71fa33f9e7ef3f2d1 +17d57859c6fffac327bffcfc793510d26726ce8b2f9ffcf6ecc98baf3efdfdbb4715f04d814765f890c644a29be408edf3181433567125272371be15c308d3f2 +8acd249438c19a4b05fd9e8a1cf4cd296699771c393ac4b5e01d01e5a30a787d72cf1178108989a2159c77a2d801ee72ce3a5c545a6147f32a99793849c26ae6 +6252c6ed637c58c5bb8b13c7bfbd490a75330f4b47f16e441c31f7184e140e494214d273fc80900aedee52ead87597fa824b3e56e82e451d4c2b4d32a423279a +668bb6690c7e9956e90cfe766cb37b077538abd27a8b1cba48c80acc2a841f12e698f13a9e281c57911ce298950d7e03aba84ac8c154f8655c4f2af074481847 +bd804859b5e696007d4b4edfc150b12addbecba6b18b148a1e54d1bc81392f23b7f84137c2715a851dd0242a633f900710a218ed715505dfe56e86e877f0034e +16bafb0e258ebb4faf06b769e888340b103d3311da9750aa9d0a1cd3e4efca31a3508f6d0c5c5c398602f8e2ebc71591f5b616e24dd893aa3261fb44f95d843b +5974bb5c04f4edafb95b7892ec1108f3f98de75dc97d5772bdff7cc95d94cf672db4b3da0a6557f70db629362d72bcb0431e53c6066acac80d699a6409fb44d0 +8741bdce9c0e4971624a2378cceaba830b05366b90e0ea23aaa241845368b0eb9e2612ca8c742851ca251ceccc70256d8d87265dd96361531f186c3d9058edf2 +c00eafe8e1fc5c509031bb4d680e9f39a3154de0accc56ae644441edd76156d7429d995bdd88664a9dc3ad50197c38af1a0c16d684060441db02565e85f3b966 +0d0713cc48a0ed6ef7dedc2dc60b17e92219e180643ed27acffba86e9c94c78ab90980d8a9f0913ee49d62b512b79626fb06dccee2a432bbc60276b9f7dec44b +7904cfbca4f3f6443ab2a49c9c2c41476dafd55c6e7ac8c769db1bc399161ee314bc2e75cf8759081743be1236ec4f4d6693e5336fb672c5dc24a8c33585b5fb +9cc24e1d4885545b58463634cc5416022cd19cacfccb4d30eb45296023fd35a458598360f8d7a4003bbaae25e331f155d9d9a5116d3bfb9a95523e51440ca2e0 +088dd844ec6370bf0e55d027a012ae264c45d02f708fa6ad6da6dce29c255df9f6cae0ec38666984b372ab5334cf640b37795cc860de4ae2816e95b21be5ceaf +8a49f90b52a51cc6ff3355f47e0237052b81f6800fd7b802239daf6d8f0b1571a8426944fdbe80c6c1d40e8816b88b8569082ab84c36ff0539d4ff6dce591a26 +ade1c0a7f669880485fd484582903d284b26fa4e2156cff62e4b9265844c4495c495a9157b440e091bea1ab8aaf7760f4510eaa69a6465c0e04ec69ffb9e65d0 +28d44d4e39df9c1a52ecbd3607fee9cec7263328e5d661d3d0e4f62f44acd855ed7ab33cdf7bcb8ae889599bd5c8b3029895b6825696f6af29c239b75a5bb1e6 +345e6ee6c28117e73586c1a2214ae1be07e93fb0ff51e133fb65426fa843be0fb515c187064d0cc206a2fa926d3c902e907670048d931db4c1a44959d366ad93 +b65abe595f70a75bf03d616c2dd959fc7d4e6317cd99cbcec9c58b34766661c7d6766ca1a9c1b327531486c6f941c638c67cd22a7f75e2a37be0e82db8df9f30 +254d30c1372581a1f51c983c80e4b71ccdd28dbf000000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d652f74 +68656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d363f24 +51eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e3198 +720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d9850528 +a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100828abc13fa0000001c0200001300000000000000000000000000 +000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b000000000000000000000000 +002b0100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000140200007468 +656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d001400060008000000210096b5ade296060000501b000016000000000000000000 +00000000d10200007468656d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b010000270000000000 +00000000000000009b0900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000960a00000000} +{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d +617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 +6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 +656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} +{\*\latentstyles\lsdstimax267\lsdlockeddef0\lsdsemihiddendef1\lsdunhideuseddef1\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal; +\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4; +\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9; +\lsdpriority39 \lsdlocked0 toc 1;\lsdpriority39 \lsdlocked0 toc 2;\lsdpriority39 \lsdlocked0 toc 3;\lsdpriority39 \lsdlocked0 toc 4;\lsdpriority39 \lsdlocked0 toc 5;\lsdpriority39 \lsdlocked0 toc 6;\lsdpriority39 \lsdlocked0 toc 7; +\lsdpriority39 \lsdlocked0 toc 8;\lsdpriority39 \lsdlocked0 toc 9;\lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdpriority1 \lsdlocked0 Default Paragraph Font; +\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority22 \lsdlocked0 Strong;\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority59 \lsdlocked0 Table Grid;\lsdunhideused0 \lsdlocked0 Placeholder Text;\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority60 \lsdlocked0 Light Shading;\lsdsemihidden0 \lsdunhideused0 \lsdpriority61 \lsdlocked0 Light List;\lsdsemihidden0 \lsdunhideused0 \lsdpriority62 \lsdlocked0 Light Grid; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority65 \lsdlocked0 Medium List 1; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority66 \lsdlocked0 Medium List 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority68 \lsdlocked0 Medium Grid 2; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority70 \lsdlocked0 Dark List;\lsdsemihidden0 \lsdunhideused0 \lsdpriority71 \lsdlocked0 Colorful Shading; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority72 \lsdlocked0 Colorful List;\lsdsemihidden0 \lsdunhideused0 \lsdpriority73 \lsdlocked0 Colorful Grid;\lsdsemihidden0 \lsdunhideused0 \lsdpriority60 \lsdlocked0 Light Shading Accent 1; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdunhideused0 \lsdlocked0 Revision; +\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority71 \lsdlocked0 Colorful Shading Accent 1; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdsemihidden0 \lsdunhideused0 \lsdpriority60 \lsdlocked0 Light Shading Accent 2; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority66 \lsdlocked0 Medium List 2 Accent 2; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority72 \lsdlocked0 Colorful List Accent 2; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdsemihidden0 \lsdunhideused0 \lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority61 \lsdlocked0 Light List Accent 3; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority62 \lsdlocked0 Light Grid Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority70 \lsdlocked0 Dark List Accent 3; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdsemihidden0 \lsdunhideused0 \lsdpriority73 \lsdlocked0 Colorful Grid Accent 3; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority62 \lsdlocked0 Light Grid Accent 4; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority65 \lsdlocked0 Medium List 1 Accent 4; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority71 \lsdlocked0 Colorful Shading Accent 4; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdsemihidden0 \lsdunhideused0 \lsdpriority60 \lsdlocked0 Light Shading Accent 5; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority62 \lsdlocked0 Light Grid Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority71 \lsdlocked0 Colorful Shading Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority72 \lsdlocked0 Colorful List Accent 5; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdsemihidden0 \lsdunhideused0 \lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority61 \lsdlocked0 Light List Accent 6; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority62 \lsdlocked0 Light Grid Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority66 \lsdlocked0 Medium List 2 Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority70 \lsdlocked0 Dark List Accent 6; +\lsdsemihidden0 \lsdunhideused0 \lsdpriority71 \lsdlocked0 Colorful Shading Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdsemihidden0 \lsdunhideused0 \lsdpriority73 \lsdlocked0 Colorful Grid Accent 6; +\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; +\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference; +\lsdsemihidden0 \lsdunhideused0 \lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdpriority37 \lsdlocked0 Bibliography;\lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;}}{\*\datastore 010500000200000018000000 +4d73786d6c322e534158584d4c5265616465722e352e3000000000000000000000060000 +d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffffec69d9888b8b3d4c859eaf6cd158be0f0000000000000000000000003068 +2a89e634c901feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 +0000000000000000000000000000000000000000000000000105000000000000}} \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/test.txt b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/test.txt new file mode 100644 index 00000000000..d46b70733f2 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/query/test.txt @@ -0,0 +1,3 @@ +This is a test of Jackrabbit full text indexing. The following +unique token should be included in the search index once this +file has been indexed: AE502DBEA2C411DEBD340AD156D89593 diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/repository-oracle-compat.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/repository-oracle-compat.xml new file mode 100755 index 00000000000..a6ffa4edf8d --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/repository-oracle-compat.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/repository-oracle.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/repository-oracle.xml new file mode 100755 index 00000000000..8fc776db220 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/repository-oracle.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/simple/simple_repository.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/simple/simple_repository.xml new file mode 100644 index 00000000000..8c12f980a38 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/simple/simple_repository.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/user/repository-membersplit.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/user/repository-membersplit.xml new file mode 100644 index 00000000000..55789a03446 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/user/repository-membersplit.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/user/repository.xml b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/user/repository.xml new file mode 100644 index 00000000000..2756b205250 --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/jackrabbit/core/security/user/repository.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/org/apache/tika/mime/custom-mimetypes.xml b/jackrabbit-core/src/test/resources/org/apache/tika/mime/custom-mimetypes.xml new file mode 100644 index 00000000000..fd6c1de897b --- /dev/null +++ b/jackrabbit-core/src/test/resources/org/apache/tika/mime/custom-mimetypes.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/jackrabbit-core/src/test/resources/repositoryHelperPool.properties b/jackrabbit-core/src/test/resources/repositoryHelperPool.properties new file mode 100644 index 00000000000..b217a503b7e --- /dev/null +++ b/jackrabbit-core/src/test/resources/repositoryHelperPool.properties @@ -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. + +# first helper uses default values +helper.0.= + +# second helper overwrites repository home +helper.1.org.apache.jackrabbit.repository.home=target/repository-2 diff --git a/jackrabbit-core/src/test/resources/repositoryStubImpl.properties b/jackrabbit-core/src/test/resources/repositoryStubImpl.properties new file mode 100644 index 00000000000..5503b4bf25f --- /dev/null +++ b/jackrabbit-core/src/test/resources/repositoryStubImpl.properties @@ -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. + +# Stub implementation class +javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.core.JackrabbitRepositoryStub diff --git a/jackrabbit-core/src/test/resources/xsd-converter-model-output.cnd b/jackrabbit-core/src/test/resources/xsd-converter-model-output.cnd new file mode 100644 index 00000000000..84e8945b8a6 --- /dev/null +++ b/jackrabbit-core/src/test/resources/xsd-converter-model-output.cnd @@ -0,0 +1,45 @@ +[bookType] + - id + - available (Boolean) + - isbn mandatory + + title (titleType) = titleType mandatory + + authors (authorsType) = authorsType mandatory + + characters (charactersType) = charactersType mandatory + +[authorType] + - id + - name mandatory + - born (Date) mandatory + - dead (Date) + +[xs:anyType] + orderable + - * (undefined) + + * + + jcr:xmltext (jcr:Xmltext) = jcr:Xmltext multiple + +[authorsType] + orderable + + author (authorType) = authorType multiple + +[characterType] + - id + - name mandatory + - born (Date) mandatory + - qualification mandatory + +[jcr:Xmltext] + - jcr:xmlcharacters + +[titleType] + - lang + - jcr:xmlContent + +[libraryType] + orderable + + book (bookType) = bookType mandatory multiple + +[charactersType] + orderable + + character (characterType) = characterType multiple + diff --git a/jackrabbit-core/src/test/resources/xsd-converter-test-input.xsd b/jackrabbit-core/src/test/resources/xsd-converter-test-input.xsd new file mode 100644 index 00000000000..a2a4d395cc5 --- /dev/null +++ b/jackrabbit-core/src/test/resources/xsd-converter-test-input.xsd @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-data/pom.xml b/jackrabbit-data/pom.xml new file mode 100644 index 00000000000..8fd2a4b10ba --- /dev/null +++ b/jackrabbit-data/pom.xml @@ -0,0 +1,116 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-data + Jackrabbit Data + Jackrabbit DataStore Implentations + bundle + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.rat + apache-rat-plugin + + + .checkstyle + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + + + + + + + javax.jcr + jcr + + + org.osgi + org.osgi.annotation + provided + + + org.apache.jackrabbit + jackrabbit-api + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + commons-io + commons-io + + + commons-dbcp + commons-dbcp + 1.3 + true + + + org.apache.derby + derby + true + + + org.slf4j + jcl-over-slf4j + + + + + junit + junit + test + + + org.slf4j + slf4j-log4j12 + test + + + \ No newline at end of file diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/ConfigurationException.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/ConfigurationException.java new file mode 100644 index 00000000000..2e10f7d7126 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/ConfigurationException.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.core.config; + +import javax.jcr.RepositoryException; + +/** + * Exception class used for configuration errors. + */ +public class ConfigurationException extends RepositoryException { + + /** + * Creates a configuration exception. + * + * @param message configuration message + */ + public ConfigurationException(String message) { + super(message); + } + + /** + * Creates a configuration exception that is caused by another exception. + * + * @param message configuration error message + * @param cause root cause of the configuration error + */ + public ConfigurationException(String message, Exception cause) { + super(message, cause); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/DataSourceConfig.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/DataSourceConfig.java new file mode 100644 index 00000000000..6df55b73e75 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/DataSourceConfig.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.core.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import javax.naming.Context; + +/** + * This class contains list of definitions for {@code DataSource} instances. + */ +public class DataSourceConfig { + + public static final String DRIVER = "driver"; + + public static final String URL = "url"; + + public static final String USER = "user"; + + public static final String PASSWORD = "password"; + + public static final String DB_TYPE = "databaseType"; + + public static final String VALIDATION_QUERY = "validationQuery"; + + public static final String MAX_POOL_SIZE = "maxPoolSize"; + + private final List defs = new ArrayList(); + + /** + * Adds a DataSourceDefinition from the given properties. + * + * @param props the properties (key and values must be strings) + * @throws ConfigurationException on error + */ + public void addDataSourceDefinition(String name, Properties props) throws ConfigurationException { + DataSourceDefinition def = new DataSourceDefinition(name, props); + for (DataSourceDefinition existing : defs) { + if (existing.getLogicalName().equals(def.getLogicalName())) { + throw new ConfigurationException("Duplicate logicalName for a DataSource: " + + def.getLogicalName()); + } + } + defs.add(def); + } + + /** + * @return the unmodifiable list of the current {@link DataSourceDefinition}s + */ + public List getDefinitions() { + return Collections.unmodifiableList(defs); + } + + /** + * The definition of a DataSource. + */ + public static final class DataSourceDefinition { + + private static final List allPropNames = + Arrays.asList(DRIVER, URL, USER, PASSWORD, DB_TYPE, VALIDATION_QUERY, MAX_POOL_SIZE); + + private static final List allJndiPropNames = + Arrays.asList(DRIVER, URL, USER, PASSWORD, DB_TYPE); + + private final String logicalName; + + private final String driver; + + private final String url; + + private final String user; + + private final String password; + + private final String dbType; + + private final String validationQuery; + + private final int maxPoolSize; + + /** + * Creates a DataSourceDefinition from the given properties and + * throws a {@link ConfigurationException} when the set of properties does not + * satisfy some validity constraints. + * + * @param name the logical name of the data source + * @param props the properties (string keys and values) + * @throws ConfigurationException on error + */ + public DataSourceDefinition(String name, Properties props) throws ConfigurationException { + this.logicalName = name; + this.driver = (String) props.getProperty(DRIVER); + this.url = (String) props.getProperty(URL); + this.user = (String) props.getProperty(USER); + this.password = (String) props.getProperty(PASSWORD); + this.dbType = (String) props.getProperty(DB_TYPE); + this.validationQuery = (String) props.getProperty(VALIDATION_QUERY); + try { + this.maxPoolSize = Integer.parseInt((String) props.getProperty(MAX_POOL_SIZE, "-1")); + } catch (NumberFormatException e) { + throw new ConfigurationException("failed to parse " + MAX_POOL_SIZE + + " property for DataSource " + logicalName); + } + verify(props); + } + + private void verify(Properties props) throws ConfigurationException { + // Check required properties + if (logicalName == null || "".equals(logicalName.trim())) { + throw new ConfigurationException("DataSource logical name must not be null or empty"); + } + if (driver == null || "".equals(driver)) { + throw new ConfigurationException("DataSource driver must not be null or empty"); + } + if (url == null || "".equals(url)) { + throw new ConfigurationException("DataSource URL must not be null or empty"); + } + if (dbType == null || "".equals(dbType)) { + throw new ConfigurationException("DataSource databaseType must not be null or empty"); + } + // Check unknown properties + for (Object propName : props.keySet()) { + if (!allPropNames.contains((String) propName)) { + throw new ConfigurationException("Unknown DataSource property: " + propName); + } + } + // Check JNDI config: + if (isJndiConfig()) { + for (Object propName : props.keySet()) { + if (!allJndiPropNames.contains((String) propName)) { + throw new ConfigurationException("Property " + propName + + " is not allowed for a DataSource obtained through JNDI" + + ", DataSource logicalName = " + logicalName); + } + } + } + } + + private boolean isJndiConfig() throws ConfigurationException { + Class driverClass = null; + try { + if (driver.length() > 0) { + driverClass = Class.forName(driver); + } + } catch (ClassNotFoundException e) { + throw new ConfigurationException("Could not load JDBC driver class " + driver, e); + } + return driverClass != null && Context.class.isAssignableFrom(driverClass); + } + + /** + * @return the logicalName + */ + public String getLogicalName() { + return logicalName; + } + + /** + * @return the driver + */ + public String getDriver() { + return driver; + } + + /** + * @return the url + */ + public String getUrl() { + return url; + } + + /** + * @return the user + */ + public String getUser() { + return user; + } + + /** + * @return the dbType + */ + public String getDbType() { + return dbType; + } + + /** + * @return the password + */ + public String getPassword() { + return password; + } + + /** + * @return the validationQuery + */ + public String getValidationQuery() { + return validationQuery; + } + + /** + * @return the maxPoolSize + */ + public int getMaxPoolSize() { + return maxPoolSize; + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/package-info.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/package-info.java new file mode 100755 index 00000000000..0db1e22b55c --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/config/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.core.config; diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractBackend.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractBackend.java new file mode 100644 index 00000000000..3258962cb96 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractBackend.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.core.data; + +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; + +import org.apache.jackrabbit.core.data.util.NamedThreadFactory; + +/** + * Abstract Backend which has a reference to the underlying {@link CachingDataStore} and is + * maintaining the lifecycle of the internal asynchronous write executor. + */ +public abstract class AbstractBackend implements Backend { + + /** + * {@link CachingDataStore} instance using this backend. + */ + private CachingDataStore dataStore; + + /** + * path of repository home dir. + */ + private String homeDir; + + /** + * path of config property file. + */ + private String config; + + /** + * The pool size of asynchronous write pooling executor. + */ + private int asyncWritePoolSize = 10; + + /** + * Asynchronous write pooling executor. + */ + private volatile Executor asyncWriteExecutor; + + /** + * Returns the pool size of the asynchronous write pool executor. + * @return the pool size of the asynchronous write pool executor + */ + public int getAsyncWritePoolSize() { + return asyncWritePoolSize; + } + + /** + * Sets the pool size of the asynchronous write pool executor. + * @param asyncWritePoolSize pool size of the async write pool executor + */ + public void setAsyncWritePoolSize(int asyncWritePoolSize) { + this.asyncWritePoolSize = asyncWritePoolSize; + } + + /** + * {@inheritDoc} + */ + @Override + public void init(CachingDataStore dataStore, String homeDir, String config) throws DataStoreException { + this.dataStore = dataStore; + this.homeDir = homeDir; + this.config = config; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws DataStoreException { + Executor asyncExecutor = getAsyncWriteExecutor(); + + if (asyncExecutor != null && asyncExecutor instanceof ExecutorService) { + ((ExecutorService) asyncExecutor).shutdownNow(); + } + } + + /** + * Returns the {@link CachingDataStore} instance using this backend. + * @return the {@link CachingDataStore} instance using this backend + */ + protected CachingDataStore getDataStore() { + return dataStore; + } + + /** + * Sets the {@link CachingDataStore} instance using this backend. + * @param dataStore the {@link CachingDataStore} instance using this backend + */ + protected void setDataStore(CachingDataStore dataStore) { + this.dataStore = dataStore; + } + + /** + * Returns path of repository home dir. + * @return path of repository home dir + */ + protected String getHomeDir() { + return homeDir; + } + + /** + * Sets path of repository home dir. + * @param homeDir path of repository home dir + */ + protected void setHomeDir(String homeDir) { + this.homeDir = homeDir; + } + + /** + * Returns path of config property file. + * @return path of config property file + */ + protected String getConfig() { + return config; + } + + /** + * Sets path of config property file. + * @param config path of config property file + */ + protected void setConfig(String config) { + this.config = config; + } + + /** + * Returns Executor used to execute asynchronous write or touch jobs. + * @return Executor used to execute asynchronous write or touch jobs + */ + protected Executor getAsyncWriteExecutor() { + Executor executor = asyncWriteExecutor; + + if (executor == null) { + synchronized (this) { + executor = asyncWriteExecutor; + if (executor == null) { + asyncWriteExecutor = executor = createAsyncWriteExecutor(); + } + } + } + + return executor; + } + + /** + * Creates an {@link Executor}. + * This method is invoked during the initialization for asynchronous write/touch job executions. + * @return an {@link Executor} + */ + protected Executor createAsyncWriteExecutor() { + Executor asyncExecutor; + + if (dataStore.getAsyncUploadLimit() > 0 && getAsyncWritePoolSize() > 0) { + asyncExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(getAsyncWritePoolSize(), + new NamedThreadFactory(getClass().getSimpleName() + "-write-worker")); + } else { + asyncExecutor = new ImmediateExecutor(); + } + + return asyncExecutor; + } + + /** + * This class implements {@link Executor} interface to run {@code command} right away, + * resulting in non-asynchronous mode executions. + */ + private class ImmediateExecutor implements Executor { + @Override + public void execute(Runnable command) { + command.run(); + } + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractDataRecord.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractDataRecord.java new file mode 100644 index 00000000000..3c7f7d9edf1 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractDataRecord.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.core.data; + + +/** + * Abstract data record base class. This base class contains only + * a reference to the data identifier of the record and implements + * the standard {@link Object} equality, hash code, and string + * representation methods based on the identifier. + */ +public abstract class AbstractDataRecord implements DataRecord { + + /** + * The data store that contains this record. + */ + private final AbstractDataStore store; + + /** + * The binary identifier; + */ + private final DataIdentifier identifier; + + /** + * Creates a data record with the given identifier. + * + * @param identifier data identifier + */ + public AbstractDataRecord( + AbstractDataStore store, DataIdentifier identifier) { + this.store = store; + this.identifier = identifier; + } + + /** + * Returns the data identifier. + * + * @return data identifier + */ + public DataIdentifier getIdentifier() { + return identifier; + } + + public String getReference() { + return store.getReferenceFromIdentifier(identifier); + } + + /** + * Returns the string representation of the data identifier. + * + * @return string representation + */ + public String toString() { + return identifier.toString(); + } + + /** + * Checks if the given object is a data record with the same identifier + * as this one. + * + * @param object other object + * @return true if the other object is a data record and has + * the same identifier as this one, false otherwise + */ + public boolean equals(Object object) { + return (object instanceof DataRecord) + && identifier.equals(((DataRecord) object).getIdentifier()); + } + + /** + * Returns the hash code of the data identifier. + * + * @return hash code + */ + public int hashCode() { + return identifier.hashCode(); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractDataStore.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractDataStore.java new file mode 100644 index 00000000000..21f852beb6a --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AbstractDataStore.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.core.data; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public abstract class AbstractDataStore implements DataStore { + + private static Logger LOG = LoggerFactory.getLogger(AbstractDataStore.class); + + private static final String ALGORITHM = "HmacSHA1"; + + /** + * Array of hexadecimal digits. + */ + private static final char[] HEX = "0123456789abcdef".toCharArray(); + + /** + * The digest algorithm used to uniquely identify records. + */ + protected String DIGEST = System.getProperty("ds.digest.algorithm", "SHA-256"); + + /** + * Cached copy of the reference key of this data store. Initialized in + * {@link #getReferenceKey()} when the key is first accessed. + */ + private byte[] referenceKey = null; + + //---------------------------------------------------------< DataStore >-- + + public DataRecord getRecord(DataIdentifier identifier) + throws DataStoreException { + DataRecord record = getRecordIfStored(identifier); + if (record != null) { + return record; + } else { + throw new DataStoreException( + "Record " + identifier + " does not exist"); + } + } + + public DataRecord getRecordFromReference(String reference) + throws DataStoreException { + if (reference != null) { + int colon = reference.indexOf(':'); + if (colon != -1) { + DataIdentifier identifier = + new DataIdentifier(reference.substring(0, colon)); + if (reference.equals(getReferenceFromIdentifier(identifier))) { + return getRecordIfStored(identifier); + } + } + } + return null; + } + + //---------------------------------------------------------< protected >-- + + /** + * Returns the hex encoding of the given bytes. + * + * @param value value to be encoded + * @return encoded value + */ + protected static String encodeHexString(byte[] value) { + char[] buffer = new char[value.length * 2]; + for (int i = 0; i < value.length; i++) { + buffer[2 * i] = HEX[(value[i] >> 4) & 0x0f]; + buffer[2 * i + 1] = HEX[value[i] & 0x0f]; + } + return new String(buffer); + } + + protected String getReferenceFromIdentifier(DataIdentifier identifier) { + try { + String id = identifier.toString(); + + Mac mac = Mac.getInstance(ALGORITHM); + mac.init(new SecretKeySpec(getReferenceKey(), ALGORITHM)); + byte[] hash = mac.doFinal(id.getBytes(StandardCharsets.UTF_8)); + + return id + ':' + encodeHexString(hash); + } catch (Exception e) { + LOG.error("Failed to hash identifier using MAC (Message Authentication Code) algorithm.", e); + } + return null; + } + + /** + * Returns the reference key of this data store. If one does not already + * exist, it is automatically created in an implementation-specific way. + * The default implementation simply creates a temporary random key that's + * valid only until the data store gets restarted. Subclasses can override + * and/or decorate this method to support a more persistent reference key. + *

    + * This method is called only once during the lifetime of a data store + * instance and the return value is cached in memory, so it's no problem + * if the implementation is slow. + * + * @return reference key + * @throws DataStoreException if the key is not available + */ + protected byte[] getOrCreateReferenceKey() throws DataStoreException { + byte[] referenceKeyValue = new byte[256]; + new SecureRandom().nextBytes(referenceKeyValue); + return referenceKeyValue; + } + + //-----------------------------------------------------------< private >-- + + /** + * Returns the reference key of this data store. Synchronized to + * control concurrent access to the cached {@link #referenceKey} value. + * + * @return reference key + * @throws DataStoreException if the key is not available + */ + private synchronized byte[] getReferenceKey() throws DataStoreException { + if (referenceKey == null) { + referenceKey = getOrCreateReferenceKey(); + } + return referenceKey; + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncTouchCallback.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncTouchCallback.java new file mode 100644 index 00000000000..2084000b277 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncTouchCallback.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.core.data; +/** + * This interface defines callback methods to reflect the status of asynchronous + * touch. + */ +public interface AsyncTouchCallback { + + + /** + * Callback method for successful asynchronous touch. + */ + public void onSuccess(AsyncTouchResult result); + + /** + * Callback method for failed asynchronous touch. + */ + public void onFailure(AsyncTouchResult result); + + /** + * Callback method for aborted asynchronous touch. + */ + public void onAbort(AsyncTouchResult result); + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncTouchResult.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncTouchResult.java new file mode 100644 index 00000000000..014db6553a2 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncTouchResult.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.core.data; + +/** + * + * The class holds the result of asynchronous touch to {@link Backend} + */ +public class AsyncTouchResult { + /** + * {@link DataIdentifier} on which asynchronous touch is initiated. + */ + private final DataIdentifier identifier; + /** + * Any {@link Exception} which is raised in asynchronously touch. + */ + private Exception exception; + + public AsyncTouchResult(DataIdentifier identifier) { + super(); + this.identifier = identifier; + } + + public DataIdentifier getIdentifier() { + return identifier; + } + + public Exception getException() { + return exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCache.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCache.java new file mode 100644 index 00000000000..2eeeb9e6d9b --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCache.java @@ -0,0 +1,351 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class holds all in progress uploads. This class contains two data + * structures, one is {@link #asyncUploadMap} which is {@link Map} + * of file path vs lastModified of upload. The second {@link #toBeDeleted} is + * {@link Set} of upload which is marked for delete, while it is already + * in progress. Before starting an asynchronous upload, it requires to invoke + * {@link #add(String)} to add entry to {@link #asyncUploadMap}. After + * asynchronous upload completes, it requires to invoke + * {@link #remove(String)} to remove entry from + * {@link #asyncUploadMap} Any modification to this class are immediately + * persisted to local file system. {@link #asyncUploadMap} is persisted to / + * {@link homeDir}/ {@link #PENDIND_UPLOAD_FILE}. {@link #toBeDeleted} is + * persisted to / {@link homeDir}/ {@link #TO_BE_DELETED_UPLOAD_FILE}. The / + * {@link homeDir} refer to ${rep.home}. + */ +public class AsyncUploadCache { + private static final Logger LOG = LoggerFactory.getLogger(AsyncUploadCache.class); + + /** + * {@link Map} of fileName Vs lastModified to store asynchronous upload. + */ + Map asyncUploadMap = new HashMap(); + + /** + * {@link Set} of fileName which are mark for delete during asynchronous + * Upload. + */ + Set toBeDeleted = new HashSet(); + + String path; + + String homeDir; + + int asyncUploadLimit; + + private File pendingUploads; + + private File toBeDeletedUploads; + + private static final String PENDIND_UPLOAD_FILE = "async-pending-uploads.ser"; + + private static final String TO_BE_DELETED_UPLOAD_FILE = "async-tobedeleted-uploads.ser"; + + /** + * This methods checks if file can be added to {@link #asyncUploadMap}. If + * yes it adds to {@link #asyncUploadMap} and + * {@link #serializeAsyncUploadMap()} the {@link #asyncUploadMap} to disk. + * + * @return {@link AsyncUploadCacheResult} if successfully added to + * asynchronous uploads it sets + * {@link AsyncUploadCacheResult#setAsyncUpload(boolean)} to true + * else sets to false. + */ + public synchronized AsyncUploadCacheResult add(String fileName) + throws IOException { + AsyncUploadCacheResult result = new AsyncUploadCacheResult(); + if (asyncUploadMap.entrySet().size() >= asyncUploadLimit) { + LOG.info( + "Async write limit [{}] reached. File [{}] not added to async write cache.", + asyncUploadLimit, fileName); + LOG.debug("AsyncUploadCache size=[{}] and entries =[{}]", + asyncUploadMap.size(), asyncUploadMap.keySet()); + result.setAsyncUpload(false); + } else { + long startTime = System.currentTimeMillis(); + if (toBeDeleted.remove(fileName)) { + serializeToBeDeleted(); + } + asyncUploadMap.put(fileName, System.currentTimeMillis()); + serializeAsyncUploadMap(); + LOG.debug("added file [{}] to asyncUploadMap upoad took [{}] sec", + fileName, ((System.currentTimeMillis() - startTime) / 1000)); + LOG.debug("AsyncUploadCache size=[{}] and entries =[{}]", + asyncUploadMap.size(), asyncUploadMap.keySet()); + result.setAsyncUpload(true); + } + return result; + } + + /** + * This methods removes file (if found) from {@link #asyncUploadMap}. If + * file is found, it immediately serializes the {@link #asyncUploadMap} to + * disk. This method sets + * {@link AsyncUploadCacheResult#setRequiresDelete(boolean)} to true, if + * asynchronous upload found to be in {@link #toBeDeleted} set i.e. marked + * for delete. + */ + public synchronized AsyncUploadCacheResult remove(String fileName) + throws IOException { + long startTime = System.currentTimeMillis(); + Long retVal = asyncUploadMap.remove(fileName); + if (retVal != null) { + serializeAsyncUploadMap(); + LOG.debug("removed file [{}] from asyncUploadMap took [{}] sec", + fileName, ((System.currentTimeMillis() - startTime) / 1000)); + LOG.debug("AsyncUploadCache size=[{}] and entries =[{}]", + asyncUploadMap.size(), asyncUploadMap.keySet()); + } else { + LOG.debug("cannot removed file [{}] from asyncUploadMap took [{}] sec. File not found.", + fileName, ((System.currentTimeMillis() - startTime) / 1000)); + LOG.debug("AsyncUploadCache size=[{}] and entries =[{}]", + asyncUploadMap.size(), asyncUploadMap.keySet()); + } + AsyncUploadCacheResult result = new AsyncUploadCacheResult(); + result.setRequiresDelete(toBeDeleted.contains(fileName)); + return result; + } + + /** + * This methods returns the in progress asynchronous uploads which are not + * marked for delete. + */ + public synchronized Set getAll() { + Set retVal = new HashSet(); + retVal.addAll(asyncUploadMap.keySet()); + retVal.removeAll(toBeDeleted); + return retVal; + } + + /** + * This methos checks if asynchronous upload is in progress for @param + * fileName. If @param touch is true, the lastModified is updated to current + * time. + */ + public synchronized boolean hasEntry(String fileName, boolean touch) + throws IOException { + boolean contains = asyncUploadMap.containsKey(fileName) + && !toBeDeleted.contains(fileName); + if (touch && contains) { + long timeStamp = System.currentTimeMillis(); + asyncUploadMap.put(fileName, timeStamp); + serializeAsyncUploadMap(); + } + return contains; + } + + /** + * Returns lastModified from {@link #asyncUploadMap} if found else returns + * 0. + */ + public synchronized long getLastModified(String fileName) { + return asyncUploadMap.get(fileName) != null + && !toBeDeleted.contains(fileName) + ? asyncUploadMap.get(fileName) + : 0; + } + + /** + * This methods deletes asynchronous upload for @param fileName if there + * exists asynchronous upload for @param fileName. + */ + public synchronized void delete(String fileName) throws IOException { + boolean serialize = false; + if (toBeDeleted.remove(fileName)) { + serialize = true; + } + if (asyncUploadMap.containsKey(fileName) && toBeDeleted.add(fileName)) { + serialize = true; + } + if (serialize) { + serializeToBeDeleted(); + } + } + + /** + * Delete in progress asynchronous uploads which are older than @param min. + * This method leverage lastModified stored in {@link #asyncUploadMap} + */ + public synchronized Set deleteOlderThan(long min) + throws IOException { + min = min - 1000; + LOG.info("deleteOlderThan min [{}]", min); + Set deleteSet = new HashSet(); + for (Map.Entry entry : asyncUploadMap.entrySet()) { + if (entry.getValue() < min) { + deleteSet.add(entry.getKey()); + } + } + if (deleteSet.size() > 0) { + LOG.debug("deleteOlderThan set [{}]", deleteSet); + toBeDeleted.addAll(deleteSet); + serializeToBeDeleted(); + } + return deleteSet; + } + + /** + * @param homeDir + * home directory of repository. + * @param path + * path of the {@link LocalCache} + * @param asyncUploadLimit + * the maximum number of asynchronous uploads + */ + public synchronized void init(String homeDir, String path, + int asyncUploadLimit) throws IOException, ClassNotFoundException { + this.homeDir = homeDir; + this.path = path; + this.asyncUploadLimit = asyncUploadLimit; + LOG.info( + "AsynWriteCache:homeDir=[{}], path=[{}], asyncUploadLimit=[{}].", + new Object[] { homeDir, path, asyncUploadLimit }); + pendingUploads = new File(homeDir + "/" + PENDIND_UPLOAD_FILE); + toBeDeletedUploads = new File(homeDir + "/" + TO_BE_DELETED_UPLOAD_FILE); + if (pendingUploads.exists()) { + deserializeAsyncUploadMap(); + } else { + pendingUploads.createNewFile(); + asyncUploadMap = new HashMap(); + serializeAsyncUploadMap(); + } + + if (toBeDeletedUploads.exists()) { + deserializeToBeDeleted(); + } else { + toBeDeletedUploads.createNewFile(); + asyncUploadMap = new HashMap(); + serializeToBeDeleted(); + } + } + + /** + * Reset the {@link AsyncUploadCache} to empty {@link #asyncUploadMap} and + * {@link #toBeDeleted} + */ + public synchronized void reset() throws IOException { + if (!pendingUploads.exists()) { + pendingUploads.createNewFile(); + } + pendingUploads.createNewFile(); + asyncUploadMap = new HashMap(); + serializeAsyncUploadMap(); + + if (!toBeDeletedUploads.exists()) { + toBeDeletedUploads.createNewFile(); + } + toBeDeletedUploads.createNewFile(); + toBeDeleted = new HashSet(); + serializeToBeDeleted(); + } + + /** + * Serialize {@link #asyncUploadMap} to local file system. + */ + private synchronized void serializeAsyncUploadMap() throws IOException { + + // use buffering + OutputStream fos = new FileOutputStream(pendingUploads); + OutputStream buffer = new BufferedOutputStream(fos); + ObjectOutput output = new ObjectOutputStream(buffer); + try { + output.writeObject(asyncUploadMap); + output.flush(); + } finally { + output.close(); + IOUtils.closeQuietly(buffer); + + } + } + + /** + * Deserialize {@link #asyncUploadMap} from local file system. + */ + private synchronized void deserializeAsyncUploadMap() throws IOException, + ClassNotFoundException { + // use buffering + InputStream fis = new FileInputStream(pendingUploads); + InputStream buffer = new BufferedInputStream(fis); + ObjectInput input = new ObjectInputStream(buffer); + try { + asyncUploadMap = (Map) input.readObject(); + } finally { + input.close(); + IOUtils.closeQuietly(buffer); + } + } + + /** + * Serialize {@link #toBeDeleted} to local file system. + */ + private synchronized void serializeToBeDeleted() throws IOException { + + // use buffering + OutputStream fos = new FileOutputStream(toBeDeletedUploads); + OutputStream buffer = new BufferedOutputStream(fos); + ObjectOutput output = new ObjectOutputStream(buffer); + try { + output.writeObject(toBeDeleted); + output.flush(); + } finally { + output.close(); + IOUtils.closeQuietly(buffer); + } + } + + /** + * Deserialize {@link #toBeDeleted} from local file system. + */ + private synchronized void deserializeToBeDeleted() throws IOException, + ClassNotFoundException { + // use buffering + InputStream fis = new FileInputStream(toBeDeletedUploads); + InputStream buffer = new BufferedInputStream(fis); + ObjectInput input = new ObjectInputStream(buffer); + try { + toBeDeleted = (Set) input.readObject(); + } finally { + input.close(); + IOUtils.closeQuietly(buffer); + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCacheResult.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCacheResult.java new file mode 100644 index 00000000000..fb5613fc3f2 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCacheResult.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.core.data; + +import java.io.File; + +/** + * This class holds result of asynchronous upload from {@link AsyncUploadCache} + */ +public class AsyncUploadCacheResult { + + /** + * flag to indicate that asynchronous upload can be started on file. + */ + private boolean asyncUpload; + + /** + * flag to indicate that cached file requires to be deleted. It is + * applicable in case where file marked for delete before asynchronous + * upload completes. + */ + private boolean requiresDelete; + + private File file; + + /** + * Flag to denote that asynchronous upload can be started on file. + */ + public boolean canAsyncUpload() { + return asyncUpload; + } + + public void setAsyncUpload(boolean asyncUpload) { + this.asyncUpload = asyncUpload; + } + + /** + * Flag to indicate that record to be deleted from {@link DataStore}. + */ + public boolean doRequiresDelete() { + return requiresDelete; + } + + public void setRequiresDelete(boolean requiresDelete) { + this.requiresDelete = requiresDelete; + } + + public File getFile() { + return file; + } + + public void setFile(File file) { + this.file = file; + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCallback.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCallback.java new file mode 100644 index 00000000000..af18c032499 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadCallback.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.core.data; + +/** + * This interface defines callback methods to reflect the status of asynchronous + * upload. + */ +public interface AsyncUploadCallback { + + /** + * Callback method for successful asynchronous upload. + */ + public void onSuccess(AsyncUploadResult result); + + /** + * Callback method for failed asynchronous upload. + */ + public void onFailure(AsyncUploadResult result); + + /** + * Callback method for aborted asynchronous upload. + */ + public void onAbort(AsyncUploadResult result); +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadResult.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadResult.java new file mode 100644 index 00000000000..bdbbb4c1f29 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/AsyncUploadResult.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.core.data; + +import java.io.File; + +/** + * + * The class holds the result of asynchronous upload to {@link Backend} + */ +public class AsyncUploadResult { + /** + * {@link DataIdentifier} on which asynchronous upload is initiated. + */ + private final DataIdentifier identifier; + + /** + * {@link File} which is asynchronously uploaded. + */ + private final File file; + + /** + * Any {@link Exception} which is raised in asynchronously upload. + */ + private Exception exception; + + public AsyncUploadResult(DataIdentifier identifier, File file) { + super(); + this.identifier = identifier; + this.file = file; + } + + public DataIdentifier getIdentifier() { + return identifier; + } + + public File getFile() { + return file; + } + + public Exception getException() { + return exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/Backend.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/Backend.java new file mode 100644 index 00000000000..840b52ae700 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/Backend.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.core.data; + +import java.io.File; +import java.io.InputStream; +import java.util.Iterator; +import java.util.Set; + +/** + * The interface defines the backend which can be plugged into + * {@link CachingDataStore}. + */ +public interface Backend { + + /** + * This method initialize backend with the configuration. + * + * @param store + * {@link CachingDataStore} + * @param homeDir + * path of repository home dir. + * @param config + * path of config property file. + * @throws DataStoreException + */ + void init(CachingDataStore store, String homeDir, String config) + throws DataStoreException; + + /** + * Return inputstream of record identified by identifier. + * + * @param identifier + * identifier of record. + * @return inputstream of the record. + * @throws DataStoreException + * if record not found or any error. + */ + InputStream read(DataIdentifier identifier) throws DataStoreException; + + /** + * Return length of record identified by identifier. + * + * @param identifier + * identifier of record. + * @return length of the record. + * @throws DataStoreException + * if record not found or any error. + */ + long getLength(DataIdentifier identifier) throws DataStoreException; + + /** + * Return lastModified of record identified by identifier. + * + * @param identifier + * identifier of record. + * @return lastModified of the record. + * @throws DataStoreException + * if record not found or any error. + */ + long getLastModified(DataIdentifier identifier) throws DataStoreException; + + /** + * Stores file to backend with identifier used as key. If key pre-exists, it + * updates the timestamp of the key. + * + * @param identifier + * key of the file + * @param file + * file that would be stored in backend. + * @throws DataStoreException + * for any error. + */ + void write(DataIdentifier identifier, File file) throws DataStoreException; + + /** + * Write file to backend in asynchronous mode. + * + * @param identifier + * @param file + * @param callback + * Callback interface to called after upload succeed or failed. + * @throws DataStoreException + */ + void writeAsync(DataIdentifier identifier, File file, + AsyncUploadCallback callback) throws DataStoreException; + + /** + * Returns identifiers of all records that exists in backend. + * + * @return iterator consisting of all identifiers + * @throws DataStoreException + */ + Iterator getAllIdentifiers() throws DataStoreException; + + /** + * This method check the existence of record in backend. Return true if + * records exists else false. This method also touch record identified by + * identifier if touch is true. + * + * @param identifier + * @throws DataStoreException + */ + boolean exists(DataIdentifier identifier, boolean touch) + throws DataStoreException; + + /** + * This method check the existence of record in backend. + * + * @param identifier + * identifier to be checked. + * @return true if records exists else false. + * @throws DataStoreException + */ + boolean exists(DataIdentifier identifier) throws DataStoreException; + + /** + * Update the lastModified of record if it's lastModified < minModifiedDate. + * + * @param identifier + * @param minModifiedDate + * @throws DataStoreException + */ + void touch(final DataIdentifier identifier, long minModifiedDate) + throws DataStoreException; + + /** + * Update the lastModified of record if it's lastModified < minModifiedDate + * asynchronously. Result of update is passed using appropriate + * {@link AsyncTouchCallback} methods. If identifier's lastModified > + * minModified {@link AsyncTouchCallback#onAbort(AsyncTouchResult)} is + * called. Any exception is communicated through + * {@link AsyncTouchCallback#onFailure(AsyncTouchResult)} . On successful + * update of lastModified, + * {@link AsyncTouchCallback#onSuccess(AsyncTouchResult)} + * is invoked. + * + * @param identifier + * @param minModifiedDate + * @param callback + * @throws DataStoreException + */ + void touchAsync(final DataIdentifier identifier, long minModifiedDate, + final AsyncTouchCallback callback) throws DataStoreException; + + /** + * Close backend and release resources like database connection if any. + * + * @throws DataStoreException + */ + void close() throws DataStoreException; + + /** + * Delete all records which are older than timestamp. + * + * @param timestamp + * @return {@link Set} of identifiers which are deleted. + * @throws DataStoreException + */ + Set deleteAllOlderThan(long timestamp) + throws DataStoreException; + + /** + * Delete record identified by identifier. No-op if identifier not found. + * + * @param identifier + * @throws DataStoreException + */ + void deleteRecord(DataIdentifier identifier) throws DataStoreException; +} + diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingDataRecord.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingDataRecord.java new file mode 100644 index 00000000000..881911709ae --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingDataRecord.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.core.data; + +import java.io.InputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * CachingDataRecord which stores reference to {@link CachingDataStore}. This + * class doesn't store any references to attributes but attributes are fetched + * on demand from {@link CachingDataStore}. + */ +public class CachingDataRecord extends AbstractDataRecord { + + private static final Logger LOG = LoggerFactory.getLogger(CachingDataRecord.class); + + private final CachingDataStore store; + + public CachingDataRecord(CachingDataStore store, DataIdentifier identifier) { + super(store, identifier); + this.store = store; + } + + @Override + public long getLastModified() { + try { + return store.getLastModified(getIdentifier()); + } catch (DataStoreException dse) { + LOG.info("exception in getLastModified for identifier [" + + getIdentifier() + "]. returning 0.", dse); + return 0; + } + } + + @Override + public long getLength() throws DataStoreException { + return store.getLength(getIdentifier()); + } + + @Override + public InputStream getStream() throws DataStoreException { + return store.getStream(getIdentifier()); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingDataStore.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingDataStore.java new file mode 100644 index 00000000000..a95c2893c45 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingDataStore.java @@ -0,0 +1,1399 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.core.data; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import javax.jcr.RepositoryException; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.data.util.NamedThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A caching data store that consists of {@link LocalCache} and {@link Backend}. + * {@link Backend} is single source of truth. All methods first try to fetch + * information from {@link LocalCache}. If record is not available in + * {@link LocalCache}, then it is fetched from {@link Backend} and saved to + * {@link LocalCache} for further access. This class is designed to work without + * {@link LocalCache} and then all information is fetched from {@link Backend}. + * To disable {@link LocalCache} set {@link #setCacheSize(long)} to 0. * + * Configuration: + * + *

    + * <DataStore class="org.apache.jackrabbit.aws.ext.ds.CachingDataStore">
    + * 
    + *     <param name="{@link #setPath(String) path}" value="/data/datastore"/>
    + *     <param name="{@link #setConfig(String) config}" value="${rep.home}/backend.properties"/>
    + *     <param name="{@link #setCacheSize(long) cacheSize}" value="68719476736"/>
    + *     <param name="{@link #setSecret(String) secret}" value="123456"/>
    + *     <param name="{@link #setCachePurgeTrigFactor(double)}" value="0.95d"/>
    + *     <param name="{@link #setCachePurgeResizeFactor(double) cacheSize}" value="0.85d"/>
    + *     <param name="{@link #setMinRecordLength(int) minRecordLength}" value="1024"/>
    + *     <param name="{@link #setContinueOnAsyncUploadFailure(boolean) continueOnAsyncUploadFailure}" value="false"/>
    + *     <param name="{@link #setConcurrentUploadsThreads(int) concurrentUploadsThreads}" value="10"/>
    + *     <param name="{@link #setAsyncUploadLimit(int) asyncUploadLimit}" value="100"/>
    + *     <param name="{@link #setUploadRetries(int) uploadRetries}" value="3"/>
    + *     <param name="{@link #setTouchAsync(boolean) touchAsync}" value="false"/>
    + *     <param name="{@link #setProactiveCaching(boolean) proactiveCaching}" value="true"/>
    + *     <param name="{@link #setRecLengthCacheSize(int) recLengthCacheSize}" value="200"/>
    + * </DataStore>
    + * 
    + */ +public abstract class CachingDataStore extends AbstractDataStore implements + MultiDataStoreAware, AsyncUploadCallback, AsyncTouchCallback { + + /** + * Logger instance. + */ + private static final Logger LOG = LoggerFactory.getLogger(CachingDataStore.class); + + private static final String DS_STORE = ".DS_Store"; + + /** + * Name of the directory used for temporary files. Must be at least 3 + * characters. + */ + private static final String TMP = "tmp"; + + /** + * All data identifiers that are currently in use are in this set until they + * are garbage collected. + */ + protected Map> inUse = Collections.synchronizedMap(new WeakHashMap>()); + + /** + * In memory map to hold failed asynchronous upload {@link DataIdentifier} + * and its retry count. Once if all retries are exhausted or file is + * successfully uploaded, then corresponding entry is flushed from the map. + * As all failed uploads are synchronously uploaded at startup, this map + * is not required to be persisted. + */ + protected final Map uploadRetryMap = new ConcurrentHashMap(5); + + /** + * In memory map to hold in-progress asynchronous touch. Once touch is + * successful corresponding entry is flushed from the map. + */ + protected final Map asyncTouchCache = new ConcurrentHashMap(5); + + /** + * In memory map to hold in-progress asynchronous downloads. Once + * download is finished corresponding entry is flushed from the map. + */ + protected final Map asyncDownloadCache = new ConcurrentHashMap(5); + + /** + * In memory cache to hold {@link DataRecord#getLength()} against + * {@link DataIdentifier} + */ + protected Map recLenCache = null; + + protected Backend backend; + + /** + * The minimum size of an object that should be stored in this data store. + */ + private int minRecordLength = 16 * 1024; + + private String path; + + private File directory; + + private File tmpDir; + + private String secret; + + /** + * Flag to indicate if lastModified is updated asynchronously. + */ + private boolean touchAsync = false; + + /** + * Flag to indicate that binary content will be cached proactively and + * asynchronously when binary metadata is retrieved from {@link Backend}. + */ + private boolean proactiveCaching = true; + + /** + * The optional backend configuration. + */ + private String config; + + /** + * The minimum modified date. If a file is accessed (read or write) with a + * modified date older than this value, the modified date is updated to the + * current time. + */ + private long minModifiedDate; + + /** + * Cache purge trigger factor. Cache will undergo in auto-purge mode if + * cache current size is greater than cachePurgeTrigFactor * cacheSize + */ + private double cachePurgeTrigFactor = 0.95d; + + /** + * Cache resize factor. After auto-purge mode, cache current size would just + * greater than cachePurgeResizeFactor * cacheSize cacheSize + */ + private double cachePurgeResizeFactor = 0.85d; + + /** + * The number of bytes in the cache. The default value is 64 GB. + */ + private long cacheSize = 64L * 1024 * 1024 * 1024; + + /** + * The number of retries for failed upload. + */ + private int uploadRetries = 3; + + /** + * The local file system cache. + */ + private LocalCache cache; + + /** + * Caching holding pending uploads + */ + private AsyncUploadCache asyncWriteCache; + + /** + * {@link ExecutorService} to asynchronous downloads + */ + private ExecutorService downloadExecService; + + protected abstract Backend createBackend(); + + protected abstract String getMarkerFile(); + + /** + * In {@link #init(String)},it resumes all incomplete asynchronous upload + * from {@link AsyncUploadCache} and uploads them concurrently in multiple + * threads. It throws {@link RepositoryException}, if file is not found in + * local cache for that asynchronous upload. As far as code is concerned, it + * is only possible when somebody has removed files from local cache + * manually. If there is an exception and user want to proceed with + * inconsistencies, set parameter continueOnAsyncUploadFailure to true in + * repository.xml. This will ignore {@link RepositoryException} and log all + * missing files and proceed after resetting {@link AsyncUploadCache} . + */ + private boolean continueOnAsyncUploadFailure; + + /** + * The {@link #init(String)} methods checks for {@link #getMarkerFile()} and + * if it doesn't exists migrates all files from fileystem to {@link Backend} + * . This parameter governs number of threads which will upload files + * concurrently to {@link Backend}. + */ + private int concurrentUploadsThreads = 10; + + /** + * This parameter limits the number of asynchronous uploads slots to + * {@link Backend}. Once this limit is reached, further uploads to + * {@link Backend} are synchronous, till one of asynchronous uploads + * completes and make asynchronous uploads slot available. To disable + * asynchronous upload, set {@link #asyncUploadLimit} parameter to 0 in + * repository.xml. By default it is 100 + */ + private int asyncUploadLimit = 100; + + /** + * Size of {@link #recLenCache}. Each entry consumes of approx 140 bytes. + * Default total memory consumption of {@link #recLenCache} 28KB. + */ + private int recLengthCacheSize = 200; + + /** + * Initialized the data store. If the path is not set, <repository + * home>/repository/datastore is used. This directory is automatically + * created if it does not yet exist. During first initialization, it upload + * all files from local datastore to backed and local datastore act as a + * local cache. + */ + @Override + public void init(String homeDir) throws RepositoryException { + try { + if (path == null) { + path = homeDir + "/repository/datastore"; + } + // create tmp inside path + tmpDir = new File(path, "tmp"); + LOG.info("path=[{}], tmpPath=[{}]", path, tmpDir.getAbsolutePath()); + directory = new File(path); + mkdirs(directory); + mkdirs(new File(homeDir)); + + if (!mkdirs(tmpDir)) { + FileUtils.cleanDirectory(tmpDir); + LOG.info("tmp=[{}] cleaned.", tmpDir.getPath()); + } + boolean asyncWriteCacheInitStatus = true; + try { + asyncWriteCache = new AsyncUploadCache(); + asyncWriteCache.init(homeDir, path, asyncUploadLimit); + } catch (Exception e) { + LOG.warn("Failed to initialize asyncWriteCache", e); + asyncWriteCacheInitStatus = false; + } + backend = createBackend(); + backend.init(this, path, config); + String markerFileName = getMarkerFile(); + if (markerFileName != null && !"".equals(markerFileName.trim())) { + // create marker file in homeDir to avoid deletion in cache + // cleanup. + File markerFile = new File(homeDir, markerFileName); + if (!markerFile.exists()) { + LOG.info("load files from local cache"); + uploadFilesFromCache(); + try { + markerFile.createNewFile(); + } catch (IOException e) { + throw new DataStoreException( + "Could not create marker file " + + markerFile.getAbsolutePath(), e); + } + } else { + LOG.info("marker file = [{}] exists ", + markerFile.getAbsolutePath()); + if (!asyncWriteCacheInitStatus) { + LOG.info("Initialization of asyncWriteCache failed. " + + "Re-loading all files from local cache"); + uploadFilesFromCache(); + asyncWriteCache.reset(); + } + } + } else { + throw new DataStoreException("Failed to intialized DataStore." + + " MarkerFileName is null or empty. "); + } + // upload any leftover async uploads to backend during last shutdown + Set fileList = asyncWriteCache.getAll(); + if (fileList != null && !fileList.isEmpty()) { + List errorFiles = new ArrayList(); + LOG.info("Uploading [{}] and size=[{}] from AsyncUploadCache.", + fileList, fileList.size()); + long totalSize = 0; + List files = new ArrayList(fileList.size()); + for (String fileName : fileList) { + File f = new File(path, fileName); + if (!f.exists()) { + errorFiles.add(fileName); + LOG.error( + "Cannot upload pending file [{}]. File doesn't exist.", + f.getAbsolutePath()); + } else { + totalSize += f.length(); + files.add(new File(path, fileName)); + } + } + new FilesUploader(files, totalSize, concurrentUploadsThreads, + true).upload(); + if (!continueOnAsyncUploadFailure && errorFiles.size() > 0) { + LOG.error( + "Pending uploads of files [{}] failed. Files do not exist in Local cache.", + errorFiles); + LOG.error("To continue set [continueOnAsyncUploadFailure] " + + "to true in Datastore configuration in " + + "repository.xml. There would be inconsistent data " + + "in repository due the missing files. "); + throw new RepositoryException( + "Cannot upload async uploads from local cache. Files not found."); + } else { + if (errorFiles.size() > 0) { + LOG.error( + "Pending uploads of files [{}] failed. Files do" + + " not exist in Local cache. Continuing as " + + "[continueOnAsyncUploadFailure] is set to true.", + errorFiles); + } + LOG.info("Reseting AsyncWrite Cache list."); + asyncWriteCache.reset(); + } + } + downloadExecService = Executors.newFixedThreadPool(5, + new NamedThreadFactory("backend-file-download-worker")); + cache = new LocalCache(path, tmpDir.getAbsolutePath(), cacheSize, + cachePurgeTrigFactor, cachePurgeResizeFactor, asyncWriteCache); + /* + * Initialize LRU cache of size {@link #recLengthCacheSize} + */ + recLenCache = Collections.synchronizedMap(new LinkedHashMap( + recLengthCacheSize, 0.75f, true) { + + private static final long serialVersionUID = -8752749075395630485L; + + @Override + protected boolean removeEldestEntry( + Map.Entry eldest) { + if (size() > recLengthCacheSize) { + LOG.trace("evicted from recLengthCache [{}]", + eldest.getKey()); + return true; + } + return false; + } + }); + } catch (Exception e) { + throw new RepositoryException(e); + } + } + + /** + * Creates a new data record in {@link Backend}. The stream is first + * consumed and the contents are saved in a temporary file and the {@link #DIGEST} + * message digest of the stream is calculated. If a record with the same + * {@link #DIGEST} digest (and length) is found then it is returned. Otherwise new + * record is created in {@link Backend} and the temporary file is moved in + * place to {@link LocalCache}. + * + * @param input + * binary stream + * @return {@link CachingDataRecord} + * @throws DataStoreException + * if the record could not be created. + */ + @Override + public DataRecord addRecord(InputStream input) throws DataStoreException { + File temporary = null; + long startTime = System.currentTimeMillis(); + long length = 0; + try { + temporary = newTemporaryFile(); + DataIdentifier tempId = new DataIdentifier(temporary.getName()); + usesIdentifier(tempId); + // Copy the stream to the temporary file and calculate the + // stream length and the message digest of the stream + MessageDigest digest = MessageDigest.getInstance(DIGEST); + OutputStream output = new DigestOutputStream(new FileOutputStream( + temporary), digest); + try { + length = IOUtils.copyLarge(input, output); + } finally { + output.close(); + } + long currTime = System.currentTimeMillis(); + DataIdentifier identifier = new DataIdentifier( + encodeHexString(digest.digest())); + LOG.debug("Digest of [{}], length =[{}] took [{}]ms ", + new Object[] { identifier, length, (currTime - startTime) }); + String fileName = getFileName(identifier); + AsyncUploadCacheResult result = null; + synchronized (this) { + usesIdentifier(identifier); + // check if async upload is already in progress + if (!asyncWriteCache.hasEntry(fileName, true)) { + result = cache.store(fileName, temporary, true); + } + } + LOG.debug("storing [{}] in localCache took [{}] ms", identifier, + (System.currentTimeMillis() - currTime)); + if (result != null) { + if (result.canAsyncUpload()) { + backend.writeAsync(identifier, result.getFile(), this); + } else { + backend.write(identifier, result.getFile()); + } + } + // this will also make sure that + // tempId is not garbage collected until here + inUse.remove(tempId); + LOG.debug("addRecord [{}] of length [{}] took [{}]ms.", + new Object[] { identifier, length, + (System.currentTimeMillis() - startTime) }); + return new CachingDataRecord(this, identifier); + } catch (NoSuchAlgorithmException e) { + throw new DataStoreException(DIGEST + " not available", e); + } catch (IOException e) { + throw new DataStoreException("Could not add record", e); + } finally { + if (temporary != null) { + // try to delete - but it's not a big deal if we can't + temporary.delete(); + } + } + } + + @Override + public DataRecord getRecord(DataIdentifier identifier) + throws DataStoreException { + String fileName = getFileName(identifier); + try { + if (getLength(identifier) > -1) { + LOG.trace("getRecord: [{}] retrieved using getLength", + identifier); + if (minModifiedDate > 0) { + touchInternal(identifier); + } + usesIdentifier(identifier); + return new CachingDataRecord(this, identifier); + } else if (asyncWriteCache.hasEntry(fileName, minModifiedDate > 0)) { + LOG.trace("getRecord: [{}] retrieved from asyncUploadmap", + identifier); + usesIdentifier(identifier); + return new CachingDataRecord(this, identifier); + } + } catch (IOException ioe) { + throw new DataStoreException("error in getting record [" + + identifier + "]", ioe); + } + throw new DataStoreException("Record not found: " + identifier); + } + + /** + * Get a data record for the given identifier or null it data record doesn't + * exist in {@link Backend} + * + * @param identifier identifier of record. + * @return the {@link CachingDataRecord} or null. + */ + @Override + public DataRecord getRecordIfStored(DataIdentifier identifier) + throws DataStoreException { + String fileName = getFileName(identifier); + try { + if (asyncWriteCache.hasEntry(fileName, minModifiedDate > 0)) { + LOG.trace( + "getRecordIfStored: [{}] retrieved from asyncuploadmap", + identifier); + usesIdentifier(identifier); + return new CachingDataRecord(this, identifier); + } else if (recLenCache.containsKey(identifier)) { + LOG.trace( + "getRecordIfStored: [{}] retrieved using recLenCache", + identifier); + if (minModifiedDate > 0) { + touchInternal(identifier); + } + usesIdentifier(identifier); + return new CachingDataRecord(this, identifier); + } else { + try { + long length = backend.getLength(identifier); + LOG.debug( + "getRecordIfStored :[{}] retrieved from backend", + identifier); + recLenCache.put(identifier, length); + if (minModifiedDate > 0) { + touchInternal(identifier); + } + usesIdentifier(identifier); + return new CachingDataRecord(this, identifier); + } catch (DataStoreException ignore) { + LOG.warn(" getRecordIfStored: [{}] not found", identifier); + } + + } + } catch (IOException ioe) { + throw new DataStoreException(ioe); + } + return null; + } + + @Override + public void updateModifiedDateOnAccess(long before) { + LOG.info("minModifiedDate set to [{}]", before); + minModifiedDate = before; + } + + /** + * Retrieves all identifiers from {@link Backend}. + */ + @Override + public Iterator getAllIdentifiers() + throws DataStoreException { + Set ids = new HashSet(); + for (String fileName : asyncWriteCache.getAll()) { + ids.add(getIdentifier(fileName)); + } + Iterator itr = backend.getAllIdentifiers(); + while (itr.hasNext()) { + ids.add(itr.next()); + } + return ids.iterator(); + } + + /** + * This method deletes record from {@link Backend} and then from + * {@link LocalCache} + */ + @Override + public void deleteRecord(DataIdentifier identifier) + throws DataStoreException { + String fileName = getFileName(identifier); + synchronized (this) { + try { + // order is important here + recLenCache.remove(identifier); + asyncWriteCache.delete(fileName); + backend.deleteRecord(identifier); + cache.delete(fileName); + } catch (IOException ioe) { + throw new DataStoreException(ioe); + } + } + } + + @Override + public synchronized int deleteAllOlderThan(long min) + throws DataStoreException { + Set diSet = backend.deleteAllOlderThan(min); + + // remove entries from local cache + for (DataIdentifier identifier : diSet) { + recLenCache.remove(identifier); + cache.delete(getFileName(identifier)); + } + try { + for (String fileName : asyncWriteCache.deleteOlderThan(min)) { + diSet.add(getIdentifier(fileName)); + } + } catch (IOException e) { + throw new DataStoreException(e); + } + LOG.info( + "deleteAllOlderThan exit. Deleted [{}]records. Number of records deleted [{}]", + diSet, diSet.size()); + return diSet.size(); + } + + /** + * Get stream of record from {@link LocalCache}. If record is not available + * in {@link LocalCache}, this method fetches record from {@link Backend} + * and stores it to {@link LocalCache}. Stream is then returned from cached + * record. + */ + InputStream getStream(DataIdentifier identifier) throws DataStoreException { + InputStream in = null; + try { + String fileName = getFileName(identifier); + InputStream cached = cache.getIfStored(fileName); + if (cached != null) { + return cached; + } + in = backend.read(identifier); + return cache.store(fileName, in); + } catch (IOException e) { + throw new DataStoreException("IO Exception: " + identifier, e); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Return lastModified of record from {@link Backend} assuming + * {@link Backend} as a single source of truth. + */ + public long getLastModified(DataIdentifier identifier) + throws DataStoreException { + String fileName = getFileName(identifier); + long lastModified = asyncWriteCache.getLastModified(fileName); + if (lastModified != 0) { + LOG.trace( + "identifier [{}], lastModified=[{}] retrireved from AsyncUploadCache ", + identifier, lastModified); + + } else if (asyncTouchCache.get(identifier) != null) { + lastModified = asyncTouchCache.get(identifier); + LOG.trace( + "identifier [{}], lastModified=[{}] retrireved from asyncTouchCache ", + identifier, lastModified); + } else { + lastModified = backend.getLastModified(identifier); + LOG.debug( + "identifier [{}], lastModified=[{}] retrireved from backend ", + identifier, lastModified); + asyncDownload(identifier); + } + return lastModified; + } + + /** + * Return the length of record from {@link LocalCache} if available, + * otherwise retrieve it from {@link Backend}. + */ + public long getLength(final DataIdentifier identifier) + throws DataStoreException { + String fileName = getFileName(identifier); + + Long length = recLenCache.get(identifier); + if (length != null) { + LOG.trace(" identifier [{}] length fetched from recLengthCache", + identifier); + return length; + } else if ((length = cache.getFileLength(fileName)) != null) { + LOG.trace(" identifier [{}] length fetched from local cache", + identifier); + recLenCache.put(identifier, length); + return length; + } else { + length = backend.getLength(identifier); + LOG.debug(" identifier [{}] length fetched from backend", + identifier); + recLenCache.put(identifier, length); + asyncDownload(identifier); + return length; + } + } + + @Override + protected byte[] getOrCreateReferenceKey() throws DataStoreException { + return secret.getBytes(StandardCharsets.UTF_8); + } + + public Set getPendingUploads() { + return asyncWriteCache.getAll(); + } + + + public void deleteFromCache(DataIdentifier identifier) + throws DataStoreException { + try { + // order is important here + recLenCache.remove(identifier); + String fileName = getFileName(identifier); + asyncWriteCache.delete(fileName); + cache.delete(fileName); + } catch (IOException ioe) { + throw new DataStoreException(ioe); + } + } + + @Override + public void onSuccess(AsyncUploadResult result) { + DataIdentifier identifier = result.getIdentifier(); + File file = result.getFile(); + String fileName = getFileName(identifier); + try { + LOG.debug("Upload completed for [{}]", identifier); + // remove from failed upload map if any. + uploadRetryMap.remove(identifier); + AsyncUploadCacheResult cachedResult = asyncWriteCache.remove(fileName); + if (cachedResult.doRequiresDelete()) { + // added record already marked for delete + deleteRecord(identifier); + } else { + // async upload took lot of time. + // getRecord to touch if required. + getRecord(identifier); + } + } catch (IOException ie) { + LOG.warn("Cannot remove pending file upload. Dataidentifer [ " + + identifier + "], file [" + file.getAbsolutePath() + "]", ie); + } catch (DataStoreException dse) { + LOG.warn("Cannot remove pending file upload. Dataidentifer [ " + + identifier + "], file [" + file.getAbsolutePath() + "]", dse); + } + } + + @Override + public void onFailure(AsyncUploadResult result) { + DataIdentifier identifier = result.getIdentifier(); + File file = result.getFile(); + String fileName = getFileName(identifier); + if (result.getException() != null) { + LOG.warn("Async Upload failed. Dataidentifer [ " + identifier + + "], file [" + file.getAbsolutePath() + "]", + result.getException()); + } else { + LOG.warn("Async Upload failed. Dataidentifer [ " + identifier + + "], file [" + file.getAbsolutePath() + "]"); + } + // Retry failed upload upto uploadRetries times. + try { + if (asyncWriteCache.hasEntry(fileName, false)) { + synchronized (uploadRetryMap) { + Integer retry = uploadRetryMap.get(identifier); + if (retry == null) { + retry = new Integer(1); + } else { + retry++; + } + if (retry <= uploadRetries) { + uploadRetryMap.put(identifier, retry); + LOG.info( + "Retrying [{}] times failed upload for dataidentifer {}", + retry, identifier); + try { + backend.writeAsync(identifier, file, this); + } catch (DataStoreException e) { + LOG.warn("exception", e); + } + } else { + LOG.info("Retries [{}] exhausted for dataidentifer {}.", + (retry - 1), identifier); + uploadRetryMap.remove(identifier); + } + } + } + } catch (IOException ie) { + LOG.warn("Cannot retry failed async file upload. Dataidentifer [ " + + identifier + "], file [" + file.getAbsolutePath() + "]", ie); + } + } + + @Override + public void onAbort(AsyncUploadResult result) { + DataIdentifier identifier = result.getIdentifier(); + File file = result.getFile(); + String fileName = getFileName(identifier); + try { + // remove from failed upload map if any. + uploadRetryMap.remove(identifier); + asyncWriteCache.remove(fileName); + LOG.info( + "Async Upload Aborted. Dataidentifer [{}], file [{}] removed from AsyncCache.", + identifier, file.getAbsolutePath()); + } catch (IOException ie) { + LOG.warn("Cannot remove pending file upload. Dataidentifer [ " + + identifier + "], file [" + file.getAbsolutePath() + "]", ie); + } + } + + + @Override + public void onSuccess(AsyncTouchResult result) { + asyncTouchCache.remove(result.getIdentifier()); + LOG.debug(" Async Touch succeed. Removed [{}] from asyncTouchCache", + result.getIdentifier()); + + } + + @Override + public void onFailure(AsyncTouchResult result) { + LOG.warn(" Async Touch failed. Not removing [{}] from asyncTouchCache", + result.getIdentifier()); + if (result.getException() != null) { + LOG.debug(" Async Touch failed. exception", result.getException()); + } + } + + @Override + public void onAbort(AsyncTouchResult result) { + asyncTouchCache.remove(result.getIdentifier()); + LOG.debug(" Async Touch aborted. Removed [{}] from asyncTouchCache", + result.getIdentifier()); + } + + /** + * Method to confirm that identifier can be deleted from {@link Backend} + * + * @param identifier + * @return + */ + public boolean confirmDelete(DataIdentifier identifier) { + if (isInUse(identifier)) { + LOG.debug("identifier [{}] is inUse confirmDelete= false ", + identifier); + return false; + } + + String fileName = getFileName(identifier); + long lastModified = asyncWriteCache.getLastModified(fileName); + if (lastModified != 0) { + LOG.debug( + "identifier [{}] is asyncWriteCache map confirmDelete= false ", + identifier); + return false; + + } + if (asyncTouchCache.get(identifier) != null) { + LOG.debug( + "identifier [{}] is asyncTouchCache confirmDelete = false ", + identifier); + return false; + } + + return true; + } + + /** + * Internal method to touch identifier in @link {@link Backend}. if + * {@link #touchAsync}, the record is updated asynchronously. + * + * @param identifier + * @throws DataStoreException + */ + private void touchInternal(DataIdentifier identifier) + throws DataStoreException { + + if (touchAsync) { + Long lastModified = asyncTouchCache.put(identifier, + System.currentTimeMillis()); + + if (lastModified == null) { + LOG.debug("Async touching [{}] ", identifier); + backend.touchAsync(identifier, minModifiedDate, this); + } else { + LOG.debug( "Touched in asyncTouchMap [{}]", identifier); + } + + } else { + backend.touch(identifier, minModifiedDate); + } + } + + /** + * Invoke {@link #getStream(DataIdentifier)} asynchronously to cache binary + * asynchronously. + */ + private void asyncDownload(final DataIdentifier identifier) { + if (proactiveCaching + && cacheSize != 0 + && asyncDownloadCache.put(identifier, System.currentTimeMillis()) == null) { + downloadExecService.execute(new Runnable() { + @Override + public void run() { + long startTime = System.currentTimeMillis(); + InputStream input = null; + try { + LOG.trace("Async download [{}] started.", identifier); + input = getStream(identifier); + } catch (RepositoryException re) { + // ignore exception + } finally { + asyncDownloadCache.remove(identifier); + IOUtils.closeQuietly(input); + LOG.debug("Async download [{}] completed in [{}] ms.", + identifier, + (System.currentTimeMillis() - startTime)); + } + } + }); + } + } + + /** + * Returns a unique temporary file to be used for creating a new data + * record. + */ + private File newTemporaryFile() throws IOException { + return File.createTempFile(TMP, null, tmpDir); + } + + /** + * Load files from {@link LocalCache} to {@link Backend}. + */ + private void uploadFilesFromCache() throws RepositoryException { + ArrayList files = new ArrayList(); + listRecursive(files, directory); + long totalSize = 0; + for (File f : files) { + totalSize += f.length(); + } + if (files.size() > 0) { + if (concurrentUploadsThreads > 1) { + new FilesUploader(files, totalSize, concurrentUploadsThreads, + false).upload(); + } else { + uploadFilesInSingleThread(files, totalSize); + } + } + } + + private void uploadFilesInSingleThread(List files, long totalSize) + throws RepositoryException { + long startTime = System.currentTimeMillis(); + LOG.info("Upload: [{}] files in single thread.", files.size()); + long currentCount = 0; + long currentSize = 0; + long time = System.currentTimeMillis(); + for (File f : files) { + String name = f.getName(); + LOG.debug("upload file [{}] ", name); + if (!name.startsWith(TMP) && !name.endsWith(DS_STORE) + && f.length() > 0) { + uploadFileToBackEnd(f, false); + } + currentSize += f.length(); + currentCount++; + long now = System.currentTimeMillis(); + if (now > time + 5000) { + LOG.info("Uploaded: [{}/{}] files, [{}/{}] size data", + new Object[] { currentCount, files.size(), currentSize, + totalSize }); + time = now; + } + } + long endTime = System.currentTimeMillis(); + LOG.info( + "Uploaded: [{}/{}] files, [{}/{}] size data, time taken = [{}] sec", + new Object[] { currentCount, files.size(), currentSize, totalSize, + ((endTime - startTime) / 1000) }); + } + + /** + * Traverse recursively and populate list with files. + */ + private static void listRecursive(List list, File file) { + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) { + listRecursive(list, f); + } else { + list.add(f); + } + } + } + } + + /** + * Upload file from {@link LocalCache} to {@link Backend}. + * + * @param f + * file to uploaded. + * @throws DataStoreException + */ + private void uploadFileToBackEnd(File f, boolean updateAsyncUploadCache) + throws DataStoreException { + try { + DataIdentifier identifier = new DataIdentifier(f.getName()); + usesIdentifier(identifier); + if (!backend.exists(identifier)) { + backend.write(identifier, f); + } + if (updateAsyncUploadCache) { + String fileName = getFileName(identifier); + asyncWriteCache.remove(fileName); + } + LOG.debug("uploaded [{}]", f.getName()); + } catch (IOException ioe) { + throw new DataStoreException(ioe); + } + } + + /** + * Derive file name from identifier. + */ + private static String getFileName(DataIdentifier identifier) { + String name = identifier.toString(); + return getFileName(name); + } + + private static String getFileName(String name) { + return name.substring(0, 2) + "/" + name.substring(2, 4) + "/" + + name.substring(4, 6) + "/" + name; + } + + private static DataIdentifier getIdentifier(String fileName) { + return new DataIdentifier( + fileName.substring(fileName.lastIndexOf("/") + 1)); + } + + private void usesIdentifier(DataIdentifier identifier) { + inUse.put(identifier, new WeakReference(identifier)); + } + + private static boolean mkdirs(File dir) throws IOException { + if (dir.exists()) { + if (dir.isFile()) { + throw new IOException("Can not create a directory " + + "because a file exists with the same name: " + + dir.getAbsolutePath()); + } + return false; + } + boolean created = dir.mkdirs(); + if (!created) { + throw new IOException("Could not create directory: " + + dir.getAbsolutePath()); + } + return created; + } + + @Override + public void clearInUse() { + inUse.clear(); + } + + public boolean isInUse(DataIdentifier identifier) { + return inUse.containsKey(identifier); + } + + @Override + public void close() throws DataStoreException { + cache.close(); + backend.close(); + downloadExecService.shutdown(); + } + + /** + * Setter for configuration based secret + * + * @param secret + * the secret used to sign reference binaries + */ + public void setSecret(String secret) { + this.secret = secret; + } + + /** + * Set the minimum object length. + * + * @param minRecordLength + * the length + */ + public void setMinRecordLength(int minRecordLength) { + this.minRecordLength = minRecordLength; + } + + /** + * Return mininum object length. + */ + @Override + public int getMinRecordLength() { + return minRecordLength; + } + + /** + * Return path of configuration properties. + * + * @return path of configuration properties. + */ + public String getConfig() { + return config; + } + + /** + * Set the configuration properties path. + * + * @param config + * path of configuration properties. + */ + public void setConfig(String config) { + this.config = config; + } + + /** + * @return size of {@link LocalCache}. + */ + public long getCacheSize() { + return cacheSize; + } + + /** + * Set size of {@link LocalCache}. + * + * @param cacheSize + * size of {@link LocalCache}. + */ + public void setCacheSize(long cacheSize) { + this.cacheSize = cacheSize; + } + + /** + * @return path of {@link LocalCache}. + */ + public String getPath() { + return path; + } + + /** + * Set path of {@link LocalCache}. + * + * @param path + * of {@link LocalCache}. + */ + public void setPath(String path) { + this.path = path; + } + + /** + * @return Purge trigger factor of {@link LocalCache}. + */ + public double getCachePurgeTrigFactor() { + return cachePurgeTrigFactor; + } + + /** + * Set purge trigger factor of {@link LocalCache}. + * + * @param cachePurgeTrigFactor + * purge trigger factor. + */ + public void setCachePurgeTrigFactor(double cachePurgeTrigFactor) { + this.cachePurgeTrigFactor = cachePurgeTrigFactor; + } + + /** + * @return Purge resize factor of {@link LocalCache}. + */ + public double getCachePurgeResizeFactor() { + return cachePurgeResizeFactor; + } + + /** + * Set purge resize factor of {@link LocalCache}. + * + * @param cachePurgeResizeFactor + * purge resize factor. + */ + public void setCachePurgeResizeFactor(double cachePurgeResizeFactor) { + this.cachePurgeResizeFactor = cachePurgeResizeFactor; + } + + public int getConcurrentUploadsThreads() { + return concurrentUploadsThreads; + } + + public void setConcurrentUploadsThreads(int concurrentUploadsThreads) { + this.concurrentUploadsThreads = concurrentUploadsThreads; + } + + public int getAsyncUploadLimit() { + return asyncUploadLimit; + } + + public void setAsyncUploadLimit(int asyncUploadLimit) { + this.asyncUploadLimit = asyncUploadLimit; + } + + public boolean isContinueOnAsyncUploadFailure() { + return continueOnAsyncUploadFailure; + } + + public void setContinueOnAsyncUploadFailure( + boolean continueOnAsyncUploadFailure) { + this.continueOnAsyncUploadFailure = continueOnAsyncUploadFailure; + } + + public int getUploadRetries() { + return uploadRetries; + } + + public void setUploadRetries(int uploadRetries) { + this.uploadRetries = uploadRetries; + } + + public void setTouchAsync(boolean touchAsync) { + this.touchAsync = touchAsync; + } + + public void setProactiveCaching(boolean proactiveCaching) { + this.proactiveCaching = proactiveCaching; + } + + public void setRecLengthCacheSize(int recLengthCacheSize) { + this.recLengthCacheSize = recLengthCacheSize; + } + + public Backend getBackend() { + return backend; + } + + /** + * This class initiates files upload in multiple threads to backend. + */ + private class FilesUploader { + final List files; + + final long totalSize; + + volatile AtomicInteger currentCount = new AtomicInteger(); + + volatile AtomicLong currentSize = new AtomicLong(); + + volatile AtomicBoolean exceptionRaised = new AtomicBoolean(); + + DataStoreException exception; + + final int threads; + + final boolean updateAsyncCache; + + FilesUploader(List files, long totalSize, int threads, + boolean updateAsyncCache) { + super(); + this.files = files; + this.threads = threads; + this.totalSize = totalSize; + this.updateAsyncCache = updateAsyncCache; + } + + void addCurrentCount(int delta) { + currentCount.addAndGet(delta); + } + + void addCurrentSize(long delta) { + currentSize.addAndGet(delta); + } + + synchronized void setException(DataStoreException exception) { + exceptionRaised.getAndSet(true); + this.exception = exception; + } + + boolean isExceptionRaised() { + return exceptionRaised.get(); + } + + void logProgress() { + LOG.info("Uploaded: [{}/{}] files, [{}/{}] size data", + new Object[] { currentCount, files.size(), currentSize, + totalSize }); + } + + void upload() throws DataStoreException { + long startTime = System.currentTimeMillis(); + LOG.info(" Uploading [{}] using [{}] threads.", files.size(), threads); + ExecutorService executor = Executors.newFixedThreadPool(threads, + new NamedThreadFactory("backend-file-upload-worker")); + int partitionSize = files.size() / (threads); + int startIndex = 0; + int endIndex = partitionSize; + for (int i = 1; i <= threads; i++) { + List partitionFileList = Collections.unmodifiableList(files.subList( + startIndex, endIndex)); + FileUploaderThread fut = new FileUploaderThread( + partitionFileList, startIndex, endIndex, this, + updateAsyncCache); + executor.execute(fut); + + startIndex = endIndex; + if (i == (threads - 1)) { + endIndex = files.size(); + } else { + endIndex = startIndex + partitionSize; + } + } + // This will make the executor accept no new threads + // and finish all existing threads in the queue + executor.shutdown(); + + try { + // Wait until all threads are finish + while (!isExceptionRaised() + && !executor.awaitTermination(15, TimeUnit.SECONDS)) { + logProgress(); + } + } catch (InterruptedException ie) { + + } + long endTime = System.currentTimeMillis(); + LOG.info( + "Uploaded: [{}/{}] files, [{}/{}] size data, time taken = [{}] sec", + new Object[] { currentCount, files.size(), currentSize, + totalSize, ((endTime - startTime) / 1000) }); + if (isExceptionRaised()) { + executor.shutdownNow(); // Cancel currently executing tasks + throw exception; + } + } + + } + + /** + * This class implements {@link Runnable} interface and uploads list of + * files from startIndex to endIndex to {@link Backend} + */ + private class FileUploaderThread implements Runnable { + final List files; + + final FilesUploader filesUploader; + + final int startIndex; + + final int endIndex; + + final boolean updateAsyncCache; + + FileUploaderThread(List files, int startIndex, int endIndex, + FilesUploader controller, boolean updateAsyncCache) { + super(); + this.files = files; + this.filesUploader = controller; + this.startIndex = startIndex; + this.endIndex = endIndex; + this.updateAsyncCache = updateAsyncCache; + } + + public void run() { + long time = System.currentTimeMillis(); + LOG.debug( + "Thread [{}] : Uploading files from startIndex [{}] to endIndex [{}] both inclusive.", + new Object[] { Thread.currentThread().getName(), startIndex, + (endIndex - 1) }); + int uploadCount = 0; + long uploadSize = 0; + try { + for (File f : files) { + + if (filesUploader.isExceptionRaised()) { + break; + } + String name = f.getName(); + LOG.debug("upload file [{}] ",name); + if (!name.startsWith(TMP) && !name.endsWith(DS_STORE) + && f.length() > 0) { + uploadFileToBackEnd(f, updateAsyncCache); + } + uploadCount++; + uploadSize += f.length(); + // update upload status at every 15 seconds. + long now = System.currentTimeMillis(); + if (now > time + 15000) { + filesUploader.addCurrentCount(uploadCount); + filesUploader.addCurrentSize(uploadSize); + uploadCount = 0; + uploadSize = 0; + time = now; + } + } + // update final state. + filesUploader.addCurrentCount(uploadCount); + filesUploader.addCurrentSize(uploadSize); + } catch (DataStoreException e) { + if (!filesUploader.isExceptionRaised()) { + filesUploader.setException(e); + } + } + + } + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingFDS.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingFDS.java new file mode 100644 index 00000000000..1cfc50e137f --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/CachingFDS.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. + */ + +/** + * {@link CachingDataStore} with {@link FSBackend}. It is performant + * {@link DataStore} when {@link FSBackend} is hosted on network storage + * (SAN or NAS). It leverages all caching capabilites of + * {@link CachingDataStore}. + */ +package org.apache.jackrabbit.core.data; + +import java.util.Properties; + +public class CachingFDS extends CachingDataStore { + private Properties properties; + + @Override + protected Backend createBackend() { + FSBackend backend = new FSBackend(); + if (properties != null) { + backend.setProperties(properties); + } + return backend; + } + + @Override + protected String getMarkerFile() { + return "fs.init.done"; + } + + /** + * Properties required to configure the S3Backend + */ + public void setProperties(Properties properties) { + this.properties = properties; + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataIdentifier.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataIdentifier.java new file mode 100644 index 00000000000..0c0b36a20d1 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataIdentifier.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.core.data; + +import java.io.Serializable; + +/** + * Opaque data identifier used to identify records in a data store. + * All identifiers must be serializable and implement the standard + * object equality and hash code methods. + */ +public class DataIdentifier implements Serializable { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = -9197191401131100016L; + + /** + * Data identifier. + */ + private final String identifier; + + /** + * Creates a data identifier from the given string. + * + * @param identifier data identifier + */ + public DataIdentifier(String identifier) { + this.identifier = identifier; + } + + //-------------------------------------------------------------< Object > + + /** + * Returns the identifier string. + * + * @return identifier string + */ + public String toString() { + return identifier; + } + + /** + * Checks if the given object is a data identifier and has the same + * string representation as this one. + * + * @param object other object + * @return true if the given object is the same identifier, + * false otherwise + */ + public boolean equals(Object object) { + return (object instanceof DataIdentifier) + && identifier.equals(object.toString()); + } + + /** + * Returns the hash code of the identifier string. + * + * @return hash code + */ + public int hashCode() { + return identifier.hashCode(); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataRecord.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataRecord.java new file mode 100644 index 00000000000..022b485a82d --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataRecord.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.core.data; + +import java.io.InputStream; + +/** + * Immutable data record that consists of a binary stream. + */ +public interface DataRecord { + + /** + * Returns the identifier of this record. + * + * @return data identifier + */ + DataIdentifier getIdentifier(); + + /** + * Returns a secure reference to this binary, or {@code null} if no such + * reference is available. + * + * @return binary reference, or {@code null} + */ + String getReference(); + + /** + * Returns the length of the binary stream in this record. + * + * @return length of the binary stream + * @throws DataStoreException if the record could not be accessed + */ + long getLength() throws DataStoreException; + + /** + * Returns the the binary stream in this record. + * + * @return binary stream + * @throws DataStoreException if the record could not be accessed + */ + InputStream getStream() throws DataStoreException; + + /** + * Returns the last modified of the record. + * + * @return last modified time of the binary stream + */ + long getLastModified(); +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStore.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStore.java new file mode 100644 index 00000000000..d714cf7b0a1 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStore.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.core.data; + +import java.io.InputStream; +import java.util.Iterator; + +import javax.jcr.RepositoryException; + +/** + * Append-only store for binary streams. A data store consists of a number + * of identifiable data records that each contain a distinct binary stream. + * New binary streams can be added to the data store, but existing streams + * are never removed or modified. + *

    + * A data store should be fully thread-safe, i.e. it should be possible to + * add and access data records concurrently. Optimally even separate processes + * should be able to concurrently access the data store with zero interprocess + * synchronization. + */ +public interface DataStore { + + /** + * Check if a record for the given identifier exists, and return it if yes. + * If no record exists, this method returns null. + * + * @param identifier data identifier + * @return the record if found, and null if not + * @throws DataStoreException if the data store could not be accessed + */ + DataRecord getRecordIfStored(DataIdentifier identifier) + throws DataStoreException; + + /** + * Returns the identified data record. The given identifier should be + * the identifier of a previously saved data record. Since records are + * never removed, there should never be cases where the identified record + * is not found. Abnormal cases like that are treated as errors and + * handled by throwing an exception. + * + * @param identifier data identifier + * @return identified data record + * @throws DataStoreException if the data store could not be accessed, + * or if the given identifier is invalid + */ + DataRecord getRecord(DataIdentifier identifier) throws DataStoreException; + + /** + * Returns the record that matches the given binary reference. + * Returns {@code null} if the reference is invalid, for example if it + * points to a record that does not exist. + * + * @param reference binary reference + * @return matching record, or {@code null} + * @throws DataStoreException if the data store could not be accessed + */ + DataRecord getRecordFromReference(String reference) + throws DataStoreException; + + /** + * Creates a new data record. The given binary stream is consumed and + * a binary record containing the consumed stream is created and returned. + * If the same stream already exists in another record, then that record + * is returned instead of creating a new one. + *

    + * The given stream is consumed and not closed by this + * method. It is the responsibility of the caller to close the stream. + * A typical call pattern would be: + *

    +     *     InputStream stream = ...;
    +     *     try {
    +     *         record = store.addRecord(stream);
    +     *     } finally {
    +     *         stream.close();
    +     *     }
    +     * 
    + * + * @param stream binary stream + * @return data record that contains the given stream + * @throws DataStoreException if the data store could not be accessed + */ + DataRecord addRecord(InputStream stream) throws DataStoreException; + + /** + * From now on, update the modified date of an object even when accessing it. + * Usually, the modified date is only updated when creating a new object, + * or when a new link is added to an existing object. When this setting is enabled, + * even getLength() will update the modified date. + * + * @param before - update the modified date to the current time if it is older than this value + */ + void updateModifiedDateOnAccess(long before); + + /** + * Delete objects that have a modified date older than the specified date. + * + * @param min the minimum time + * @return the number of data records deleted + * @throws DataStoreException + */ + int deleteAllOlderThan(long min) throws DataStoreException; + + /** + * Get all identifiers. + * + * @return an iterator over all DataIdentifier objects + * @throws DataStoreException if the list could not be read + */ + Iterator getAllIdentifiers() throws DataStoreException; + + /** + * Initialized the data store + * + * @param homeDir the home directory of the repository + * @throws RepositoryException + */ + void init(String homeDir) throws RepositoryException; + + /** + * Get the minimum size of an object that should be stored in this data store. + * Depending on the overhead and configuration, each store may return a different value. + * + * @return the minimum size in bytes + */ + int getMinRecordLength(); + + /** + * Close the data store + * + * @throws DataStoreException if a problem occurred + */ + void close() throws DataStoreException; + + /** + * Clear the in-use list. This is only used for testing to make the the garbage collection + * think that objects are no longer in use. + */ + void clearInUse(); + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStoreException.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStoreException.java new file mode 100644 index 00000000000..4b173ac84ac --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStoreException.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.core.data; + +import javax.jcr.RepositoryException; + +/** + * Exception thrown by the Data Store module. + */ +public class DataStoreException extends RepositoryException { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detailed message. + */ + public DataStoreException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detailed message. + * @param cause root failure cause + */ + public DataStoreException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new instance of this class with the specified root cause. + * + * @param rootCause root failure cause + */ + public DataStoreException(Throwable rootCause) { + super(rootCause); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStoreFactory.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStoreFactory.java new file mode 100644 index 00000000000..390f4c5eceb --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/DataStoreFactory.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.core.data; + +import javax.jcr.RepositoryException; + + +/** + * Factory interface for creating {@link DataStore} instances. Used + * to decouple the repository internals from the repository configuration + * mechanism. + * + * @since Jackrabbit 1.5 + * @see JCR-1438 + */ +public interface DataStoreFactory { + + /** + * Creates, initializes, and returns a {@link DataStore} instance + * for use by the repository. Note that no information is passed from + * the client, so all required configuration information must be + * encapsulated in the factory. + * + * @return initialized data store + * @throws RepositoryException if the data store can not be created + */ + DataStore getDataStore() throws RepositoryException; + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FSBackend.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FSBackend.java new file mode 100644 index 00000000000..1437178d556 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FSBackend.java @@ -0,0 +1,477 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * File system {@link Backend} used with {@link CachingDataStore}. + * The file system can be network storage. + */ +package org.apache.jackrabbit.core.data; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FSBackend extends AbstractBackend { + + private Properties properties; + + private String fsPath; + + File fsPathDir; + + public static final String FS_BACKEND_PATH = "fsBackendPath"; + + /** + * Logger instance. + */ + private static final Logger LOG = LoggerFactory.getLogger(FSBackend.class); + + /** + * The maximum last modified time resolution of the file system. + */ + private static final int ACCESS_TIME_RESOLUTION = 2000; + + @Override + public void init(CachingDataStore store, String homeDir, String config) + throws DataStoreException { + super.init(store, homeDir, config); + Properties initProps = null; + // Check is configuration is already provided. That takes precedence + // over config provided via file based config + if (this.properties != null) { + initProps = this.properties; + } else { + initProps = new Properties(); + InputStream in = null; + try { + in = new FileInputStream(config); + initProps.load(in); + } catch (IOException e) { + throw new DataStoreException( + "Could not initialize FSBackend from " + config, e); + } finally { + IOUtils.closeQuietly(in); + } + this.properties = initProps; + } + init(store, homeDir, initProps); + + } + + public void init(CachingDataStore store, String homeDir, Properties prop) + throws DataStoreException { + setDataStore(store); + setHomeDir(homeDir); + this.fsPath = prop.getProperty(FS_BACKEND_PATH); + if (this.fsPath == null || "".equals(this.fsPath)) { + throw new DataStoreException("Could not initialize FSBackend from " + + getConfig() + ". [" + FS_BACKEND_PATH + "] property not found."); + } + fsPathDir = new File(this.fsPath); + if (fsPathDir.exists() && fsPathDir.isFile()) { + throw new DataStoreException("Can not create a directory " + + "because a file exists with the same name: " + this.fsPath); + } + if (!fsPathDir.exists()) { + boolean created = fsPathDir.mkdirs(); + if (!created) { + throw new DataStoreException("Could not create directory: " + + fsPathDir.getAbsolutePath()); + } + } + } + + @Override + public InputStream read(DataIdentifier identifier) + throws DataStoreException { + File file = getFile(identifier); + try { + return new LazyFileInputStream(file); + } catch (IOException e) { + throw new DataStoreException("Error opening input stream of " + + file.getAbsolutePath(), e); + } + } + + @Override + public long getLength(DataIdentifier identifier) throws DataStoreException { + File file = getFile(identifier); + if (file.isFile()) { + return file.length(); + } + throw new DataStoreException("Could not length of dataIdentifier [" + + identifier + "]"); + } + + @Override + public long getLastModified(DataIdentifier identifier) + throws DataStoreException { + long start = System.currentTimeMillis(); + File f = getFile(identifier); + if (f.isFile()) { + return getLastModified(f); + } + LOG.info("getLastModified:Identifier [{}] not found. Took [{}] ms.", + identifier, (System.currentTimeMillis() - start)); + throw new DataStoreException("Identifier [" + identifier + + "] not found."); + } + + @Override + public void write(DataIdentifier identifier, File src) + throws DataStoreException { + File dest = getFile(identifier); + synchronized (this) { + if (dest.exists()) { + long now = System.currentTimeMillis(); + if (getLastModified(dest) < now + ACCESS_TIME_RESOLUTION) { + setLastModified(dest, now + ACCESS_TIME_RESOLUTION); + } + } else { + try { + FileUtils.copyFile(src, dest); + } catch (IOException ioe) { + LOG.error("failed to copy [{}] to [{}]", + src.getAbsolutePath(), dest.getAbsolutePath()); + throw new DataStoreException("Not able to write file [" + + identifier + "]", ioe); + } + } + } + + } + + @Override + public void writeAsync(final DataIdentifier identifier, final File src, + final AsyncUploadCallback callback) + throws DataStoreException { + if (callback == null) { + throw new IllegalArgumentException( + "callback parameter cannot be null in asyncUpload"); + } + getAsyncWriteExecutor().execute(new Runnable() { + @Override + public void run() { + try { + write(identifier, src); + callback.onSuccess(new AsyncUploadResult(identifier, src)); + } catch (DataStoreException dse) { + AsyncUploadResult res = new AsyncUploadResult(identifier, + src); + res.setException(dse); + callback.onFailure(res); + } + + } + }); + } + + @Override + public Iterator getAllIdentifiers() + throws DataStoreException { + ArrayList files = new ArrayList(); + for (File file : fsPathDir.listFiles()) { + if (file.isDirectory()) { // skip top-level files + listRecursive(files, file); + } + } + + ArrayList identifiers = new ArrayList(); + for (File f : files) { + String name = f.getName(); + identifiers.add(new DataIdentifier(name)); + } + LOG.debug("Found " + identifiers.size() + " identifiers."); + return identifiers.iterator(); + } + + @Override + public boolean exists(DataIdentifier identifier, boolean touch) + throws DataStoreException { + File file = getFile(identifier); + if (file.isFile()) { + if (touch) { + long now = System.currentTimeMillis(); + setLastModified(file, now + ACCESS_TIME_RESOLUTION); + } + return true; + } + return false; + } + + @Override + public boolean exists(DataIdentifier identifier) throws DataStoreException { + return exists(identifier, false); + } + + @Override + public void touch(DataIdentifier identifier, long minModifiedDate) + throws DataStoreException { + File file = getFile(identifier); + long now = System.currentTimeMillis(); + if (minModifiedDate > 0 && minModifiedDate > getLastModified(file)) { + setLastModified(file, now + ACCESS_TIME_RESOLUTION); + } + } + + @Override + public void touchAsync(final DataIdentifier identifier, + final long minModifiedDate, + final AsyncTouchCallback callback) + throws DataStoreException { + try { + if (callback == null) { + throw new IllegalArgumentException( + "callback parameter cannot be null in touchAsync"); + } + Thread.currentThread().setContextClassLoader( + getClass().getClassLoader()); + + getAsyncWriteExecutor().execute(new Runnable() { + @Override + public void run() { + try { + touch(identifier, minModifiedDate); + callback.onSuccess(new AsyncTouchResult(identifier)); + } catch (DataStoreException e) { + AsyncTouchResult result = new AsyncTouchResult( + identifier); + result.setException(e); + callback.onFailure(result); + } + } + }); + } catch (Exception e) { + if (callback != null) { + callback.onAbort(new AsyncTouchResult(identifier)); + } + throw new DataStoreException("Cannot touch the record " + + identifier.toString(), e); + } + + } + + @Override + public Set deleteAllOlderThan(long min) + throws DataStoreException { + Set deleteIdSet = new HashSet(30); + for (File file : fsPathDir.listFiles()) { + if (file.isDirectory()) { // skip top-level files + deleteOlderRecursive(file, min, deleteIdSet); + } + } + return deleteIdSet; + } + + @Override + public void deleteRecord(DataIdentifier identifier) + throws DataStoreException { + File file = getFile(identifier); + synchronized (this) { + if (file.exists()) { + if (file.delete()) { + deleteEmptyParentDirs(file); + } else { + LOG.warn("Failed to delete file " + file.getAbsolutePath()); + } + } + } + } + + /** + * Properties used to configure the backend. If provided explicitly before + * init is invoked then these take precedence + * @param properties to configure S3Backend + */ + public void setProperties(Properties properties) { + this.properties = properties; + } + + /** + * Returns the identified file. This method implements the pattern used to + * avoid problems with too many files in a single directory. + *

    + * No sanity checks are performed on the given identifier. + * @param identifier data identifier + * @return identified file + */ + private File getFile(DataIdentifier identifier) { + String string = identifier.toString(); + File file = this.fsPathDir; + file = new File(file, string.substring(0, 2)); + file = new File(file, string.substring(2, 4)); + file = new File(file, string.substring(4, 6)); + return new File(file, string); + } + + /** + * Set the last modified date of a file, if the file is writable. + * @param file the file + * @param time the new last modified date + * @throws DataStoreException if the file is writable but modifying the date + * fails + */ + private static void setLastModified(File file, long time) + throws DataStoreException { + if (!file.setLastModified(time)) { + if (!file.canWrite()) { + // if we can't write to the file, so garbage collection will + // also not delete it + // (read only files or file systems) + return; + } + try { + // workaround for Windows: if the file is already open for + // reading + // (in this or another process), then setting the last modified + // date + // doesn't work - see also JCR-2872 + RandomAccessFile r = new RandomAccessFile(file, "rw"); + try { + r.setLength(r.length()); + } finally { + r.close(); + } + } catch (IOException e) { + throw new DataStoreException( + "An IO Exception occurred while trying to set the last modified date: " + + file.getAbsolutePath(), e); + } + } + } + + /** + * Get the last modified date of a file. + * @param file the file + * @return the last modified date + * @throws DataStoreException if reading fails + */ + private static long getLastModified(File file) throws DataStoreException { + long lastModified = file.lastModified(); + if (lastModified == 0) { + throw new DataStoreException( + "Failed to read record modified date: " + + file.getAbsolutePath()); + } + return lastModified; + } + + private void listRecursive(List list, File file) { + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) { + listRecursive(list, f); + } else { + list.add(f); + } + } + } + } + + private void deleteEmptyParentDirs(File file) { + File parent = file.getParentFile(); + try { + // Only iterate & delete if parent directory of the blob file is + // child + // of the base directory and if it is empty + while (FileUtils.directoryContains(fsPathDir, parent)) { + String[] entries = parent.list(); + if (entries == null) { + LOG.warn("Failed to list directory {}", + parent.getAbsolutePath()); + break; + } + if (entries.length > 0) { + break; + } + boolean deleted = parent.delete(); + LOG.debug("Deleted parent [{}] of file [{}]: {}", new Object[] { + parent, file.getAbsolutePath(), deleted }); + parent = parent.getParentFile(); + } + } catch (IOException e) { + LOG.warn("Error in parents deletion for " + file.getAbsoluteFile(), + e); + } + } + + private void deleteOlderRecursive(File file, long min, + Set deleteIdSet) throws DataStoreException { + if (file.isFile() && file.exists() && file.canWrite()) { + synchronized (this) { + long lastModified; + try { + lastModified = getLastModified(file); + } catch (DataStoreException e) { + LOG.warn( + "Failed to read modification date; file not deleted", e); + // don't delete the file, since the lastModified date is + // uncertain + lastModified = min; + } + if (lastModified < min) { + DataIdentifier id = new DataIdentifier(file.getName()); + if (getDataStore().confirmDelete(id)) { + getDataStore().deleteFromCache(id); + if (LOG.isInfoEnabled()) { + LOG.info("Deleting old file " + + file.getAbsolutePath() + " modified: " + + new Timestamp(lastModified).toString() + + " length: " + file.length()); + } + if (file.delete()) { + deleteIdSet.add(id); + } else { + LOG.warn("Failed to delete old file " + + file.getAbsolutePath()); + } + } + } + } + } else if (file.isDirectory()) { + File[] list = file.listFiles(); + if (list != null) { + for (File f : list) { + deleteOlderRecursive(f, min, deleteIdSet); + } + } + + // JCR-1396: FileDataStore Garbage Collector and empty directories + // Automatic removal of empty directories (but not the root!) + synchronized (this) { + list = file.listFiles(); + if (list != null && list.length == 0) { + file.delete(); + } + } + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FileDataRecord.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FileDataRecord.java new file mode 100644 index 00000000000..df0161db101 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FileDataRecord.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.core.data; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + + +/** + * Data record that is based on a normal file. + */ +public class FileDataRecord extends AbstractDataRecord { + + /** + * The file that contains the binary stream. + */ + private final File file; + + /** + * Creates a data record based on the given identifier and file. + * + * @param identifier data identifier + * @param file file that contains the binary stream + */ + public FileDataRecord( + AbstractDataStore store, DataIdentifier identifier, File file) { + super(store, identifier); + assert file.isFile(); + this.file = file; + } + + /** + * {@inheritDoc} + */ + public long getLength() { + return file.length(); + } + + /** + * {@inheritDoc} + */ + public InputStream getStream() throws DataStoreException { + try { + return new LazyFileInputStream(file); + } catch (IOException e) { + throw new DataStoreException("Error opening input stream of " + file.getAbsolutePath(), e); + } + } + + /** + * {@inheritDoc} + */ + public long getLastModified() { + return file.lastModified(); + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FileDataStore.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FileDataStore.java new file mode 100644 index 00000000000..3bd11ee1758 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/FileDataStore.java @@ -0,0 +1,502 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.lang.ref.WeakReference; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple file-based data store. Data records are stored as normal files + * named using a message digest of the contained binary stream. + * + * Configuration: + *

    + * <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
    + *     <param name="{@link #setPath(String) path}" value="/data/datastore"/>
    + *     <param name="{@link #setMinRecordLength(int) minRecordLength}" value="1024"/>
    + * </DataStore>
    + * 
    + *

    + * If the directory is not set, the directory <repository home>/repository/datastore is used. + *

    + * A three level directory structure is used to avoid placing too many + * files in a single directory. The chosen structure is designed to scale + * up to billions of distinct records. + *

    + * This implementation relies on the underlying file system to support + * atomic O(1) move operations with {@link File#renameTo(File)}. + */ +public class FileDataStore extends AbstractDataStore + implements MultiDataStoreAware { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(FileDataStore.class); + + /** + * The default value for the minimum object size. + */ + private static final int DEFAULT_MIN_RECORD_LENGTH = 100; + + /** + * The maximum last modified time resolution of the file system. + */ + private static final int ACCESS_TIME_RESOLUTION = 2000; + + /** + * Name of the directory used for temporary files. + * Must be at least 3 characters. + */ + private static final String TMP = "tmp"; + + /** + * The minimum modified date. If a file is accessed (read or write) with a modified date + * older than this value, the modified date is updated to the current time. + */ + private volatile long minModifiedDate; + + /** + * The directory that contains all the data record files. The structure + * of content within this directory is controlled by this class. + */ + private File directory; + + /** + * The name of the directory that contains all the data record files. The structure + * of content within this directory is controlled by this class. + */ + private String path; + + /** + * The minimum size of an object that should be stored in this data store. + */ + private int minRecordLength = DEFAULT_MIN_RECORD_LENGTH; + + /** + * All data identifiers that are currently in use are in this set until they are garbage collected. + */ + protected Map> inUse = + Collections.synchronizedMap(new WeakHashMap>()); + + /** + * Initialized the data store. + * If the path is not set, <repository home>/repository/datastore is used. + * This directory is automatically created if it does not yet exist. + * + * @param homeDir + */ + public void init(String homeDir) { + if (path == null) { + path = homeDir + "/repository/datastore"; + } + directory = new File(path); + directory.mkdirs(); + } + + /** + * Get a data record for the given identifier. + * + * @param identifier the identifier + * @return the data record or null + */ + public DataRecord getRecordIfStored(DataIdentifier identifier) throws DataStoreException { + File file = getFile(identifier); + if (!file.exists()) { + return null; + } + if (minModifiedDate != 0) { + // only check when running garbage collection + synchronized (this) { + if (getLastModified(file) < minModifiedDate) { + setLastModified(file, System.currentTimeMillis() + ACCESS_TIME_RESOLUTION); + } + } + } + usesIdentifier(identifier); + return new FileDataRecord(this, identifier, file); + } + + private void usesIdentifier(DataIdentifier identifier) { + inUse.put(identifier, new WeakReference(identifier)); + } + + /** + * Creates a new data record. + * The stream is first consumed and the contents are saved in a temporary file + * and the {@link #DIGEST} message digest of the stream is calculated. If a + * record with the same {@link #DIGEST} digest (and length) is found then it is + * returned. Otherwise the temporary file is moved in place to become + * the new data record that gets returned. + * + * @param input binary stream + * @return data record that contains the given stream + * @throws DataStoreException if the record could not be created + */ + public DataRecord addRecord(InputStream input) throws DataStoreException { + File temporary = null; + try { + temporary = newTemporaryFile(); + DataIdentifier tempId = new DataIdentifier(temporary.getName()); + usesIdentifier(tempId); + // Copy the stream to the temporary file and calculate the + // stream length and the message digest of the stream + long length = 0; + MessageDigest digest = MessageDigest.getInstance(DIGEST); + OutputStream output = new DigestOutputStream( + new FileOutputStream(temporary), digest); + try { + length = IOUtils.copyLarge(input, output); + } finally { + output.close(); + } + DataIdentifier identifier = + new DataIdentifier(encodeHexString(digest.digest())); + File file; + + synchronized (this) { + // Check if the same record already exists, or + // move the temporary file in place if needed + usesIdentifier(identifier); + file = getFile(identifier); + if (!file.exists()) { + File parent = file.getParentFile(); + parent.mkdirs(); + if (temporary.renameTo(file)) { + // no longer need to delete the temporary file + temporary = null; + } else { + throw new IOException( + "Can not rename " + temporary.getAbsolutePath() + + " to " + file.getAbsolutePath() + + " (media read only?)"); + } + } else { + long now = System.currentTimeMillis(); + if (getLastModified(file) < now + ACCESS_TIME_RESOLUTION) { + setLastModified(file, now + ACCESS_TIME_RESOLUTION); + } + } + if (file.length() != length) { + // Sanity checks on the record file. These should never fail, + // but better safe than sorry... + if (!file.isFile()) { + throw new IOException("Not a file: " + file); + } + throw new IOException(DIGEST + " collision: " + file); + } + } + // this will also make sure that + // tempId is not garbage collected until here + inUse.remove(tempId); + return new FileDataRecord(this, identifier, file); + } catch (NoSuchAlgorithmException e) { + throw new DataStoreException(DIGEST + " not available", e); + } catch (IOException e) { + throw new DataStoreException("Could not add record", e); + } finally { + if (temporary != null) { + temporary.delete(); + } + } + } + + /** + * Returns the identified file. This method implements the pattern + * used to avoid problems with too many files in a single directory. + *

    + * No sanity checks are performed on the given identifier. + * + * @param identifier data identifier + * @return identified file + */ + private File getFile(DataIdentifier identifier) { + usesIdentifier(identifier); + String string = identifier.toString(); + File file = directory; + file = new File(file, string.substring(0, 2)); + file = new File(file, string.substring(2, 4)); + file = new File(file, string.substring(4, 6)); + return new File(file, string); + } + + /** + * Returns a unique temporary file to be used for creating a new + * data record. + * + * @return temporary file + * @throws IOException + */ + private File newTemporaryFile() throws IOException { + // the directory is already created in the init method + return File.createTempFile(TMP, null, directory); + } + + public void updateModifiedDateOnAccess(long before) { + minModifiedDate = before; + } + + public void deleteRecord(DataIdentifier identifier) + throws DataStoreException { + File file = getFile(identifier); + synchronized (this) { + if (file.exists()) { + if (file.delete()) { + deleteEmptyParentDirs(file); + } else { + log.warn("Failed to delete file " + file.getAbsolutePath()); + } + } + } + } + + private void deleteEmptyParentDirs(File file) { + File parent = file.getParentFile(); + try { + // Only iterate & delete if parent directory of the blob file is child + // of the base directory and if it is empty + while (FileUtils.directoryContains(directory, parent)) { + String[] entries = parent.list(); + if (entries == null) { + log.warn("Failed to list directory {}", parent.getAbsolutePath()); + break; + } + if (entries.length > 0) { + break; + } + boolean deleted = parent.delete(); + log.debug("Deleted parent [{}] of file [{}]: {}", + new Object[]{parent, file.getAbsolutePath(), deleted}); + parent = parent.getParentFile(); + } + } catch (IOException e) { + log.warn("Error in parents deletion for " + file.getAbsoluteFile(), e); + } + } + + public int deleteAllOlderThan(long min) { + int count = 0; + for (File file : directory.listFiles()) { + if (file.isDirectory()) { // skip top-level files + count += deleteOlderRecursive(file, min); + } + } + return count; + } + + private int deleteOlderRecursive(File file, long min) { + int count = 0; + if (file.isFile() && file.exists() && file.canWrite()) { + synchronized (this) { + long lastModified; + try { + lastModified = getLastModified(file); + } catch (DataStoreException e) { + log.warn("Failed to read modification date; file not deleted", e); + // don't delete the file, since the lastModified date is uncertain + lastModified = min; + } + if (lastModified < min) { + DataIdentifier id = new DataIdentifier(file.getName()); + if (!inUse.containsKey(id)) { + if (log.isInfoEnabled()) { + log.info("Deleting old file " + file.getAbsolutePath() + + " modified: " + new Timestamp(lastModified).toString() + + " length: " + file.length()); + } + if (!file.delete()) { + log.warn("Failed to delete old file " + file.getAbsolutePath()); + } + count++; + } + } + } + } else if (file.isDirectory()) { + File[] list = file.listFiles(); + if (list != null) { + for (File f: list) { + count += deleteOlderRecursive(f, min); + } + } + + // JCR-1396: FileDataStore Garbage Collector and empty directories + // Automatic removal of empty directories (but not the root!) + synchronized (this) { + list = file.listFiles(); + if (list != null && list.length == 0) { + file.delete(); + } + } + } + return count; + } + + private void listRecursive(List list, File file) { + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) { + listRecursive(list, f); + } else { + list.add(f); + } + } + } + } + + public Iterator getAllIdentifiers() { + ArrayList files = new ArrayList(); + for (File file : directory.listFiles()) { + if (file.isDirectory()) { // skip top-level files + listRecursive(files, file); + } + } + + ArrayList identifiers = new ArrayList(); + for (File f: files) { + String name = f.getName(); + identifiers.add(new DataIdentifier(name)); + } + log.debug("Found " + identifiers.size() + " identifiers."); + return identifiers.iterator(); + } + + public void clearInUse() { + inUse.clear(); + } + + /** + * Get the name of the directory where this data store keeps the files. + * + * @return the full path name + */ + public String getPath() { + return path; + } + + /** + * Set the name of the directory where this data store keeps the files. + * + * @param directoryName the path name + */ + public void setPath(String directoryName) { + this.path = directoryName; + } + + public int getMinRecordLength() { + return minRecordLength; + } + + /** + * Set the minimum object length. + * + * @param minRecordLength the length + */ + public void setMinRecordLength(int minRecordLength) { + this.minRecordLength = minRecordLength; + } + + public void close() { + // nothing to do + } + + //---------------------------------------------------------< protected >-- + + @Override + protected byte[] getOrCreateReferenceKey() throws DataStoreException { + File file = new File(directory, "reference.key"); + try { + if (file.exists()) { + return FileUtils.readFileToByteArray(file); + } else { + byte[] key = super.getOrCreateReferenceKey(); + FileUtils.writeByteArrayToFile(file, key); + return key; + } + } catch (IOException e) { + throw new DataStoreException( + "Unable to access reference key file " + file.getPath(), e); + } + } + + //-----------------------------------------------------------< private >-- + + /** + * Get the last modified date of a file. + * + * @param file the file + * @return the last modified date + * @throws DataStoreException if reading fails + */ + private static long getLastModified(File file) throws DataStoreException { + long lastModified = file.lastModified(); + if (lastModified == 0) { + throw new DataStoreException("Failed to read record modified date: " + file.getAbsolutePath()); + } + return lastModified; + } + + /** + * Set the last modified date of a file, if the file is writable. + * + * @param file the file + * @param time the new last modified date + * @throws DataStoreException if the file is writable but modifying the date fails + */ + private static void setLastModified(File file, long time) throws DataStoreException { + if (!file.setLastModified(time)) { + if (!file.canWrite()) { + // if we can't write to the file, so garbage collection will also not delete it + // (read only files or file systems) + return; + } + try { + // workaround for Windows: if the file is already open for reading + // (in this or another process), then setting the last modified date + // doesn't work - see also JCR-2872 + RandomAccessFile r = new RandomAccessFile(file, "rw"); + try { + r.setLength(r.length()); + } finally { + r.close(); + } + } catch (IOException e) { + throw new DataStoreException("An IO Exception occurred while trying to set the last modified date: " + file.getAbsolutePath(), e); + } + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LazyFileInputStream.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LazyFileInputStream.java new file mode 100644 index 00000000000..a9c0489b045 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LazyFileInputStream.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.core.data; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.apache.commons.io.input.AutoCloseInputStream; + +/** + * This input stream delays opening the file until the first byte is read, and + * closes and discards the underlying stream as soon as the end of input has + * been reached or when the stream is explicitly closed. + */ +public class LazyFileInputStream extends AutoCloseInputStream { + + /** + * The file descriptor to use. + */ + protected final FileDescriptor fd; + + /** + * The file to read from. + */ + protected final File file; + + /** + * True if the input stream was opened. It is also set to true if the stream + * was closed without reading (to avoid opening the file after the stream + * was closed). + */ + protected boolean opened; + + /** + * Creates a new LazyFileInputStream for the given file. If the + * file is unreadable, a FileNotFoundException is thrown. + * The file is not opened until the first byte is read from the stream. + * + * @param file the file + * @throws java.io.FileNotFoundException + */ + public LazyFileInputStream(File file) + throws FileNotFoundException { + super(null); + if (!file.canRead()) { + throw new FileNotFoundException(file.getPath()); + } + this.file = file; + this.fd = null; + } + + /** + * Creates a new LazyFileInputStream for the given file + * descriptor. + * The file is not opened until the first byte is read from the stream. + * + * @param fd + */ + public LazyFileInputStream(FileDescriptor fd) { + super(null); + this.file = null; + this.fd = fd; + } + + /** + * Creates a new LazyFileInputStream for the given file. If the + * file is unreadable, a FileNotFoundException is thrown. + * + * @param name + * @throws java.io.FileNotFoundException + */ + public LazyFileInputStream(String name) throws FileNotFoundException { + this(new File(name)); + } + + /** + * Open the stream if required. + * + * @throws java.io.IOException + */ + protected void open() throws IOException { + if (!opened) { + opened = true; + if (fd != null) { + in = new FileInputStream(fd); + } else { + in = new FileInputStream(file); + } + } + } + + public int read() throws IOException { + open(); + return super.read(); + } + + public int available() throws IOException { + open(); + return super.available(); + } + + public void close() throws IOException { + // make sure the file is not opened afterwards + opened = true; + + // only close the file if it was in fact opened + if (in != null) { + super.close(); + } + } + + public synchronized void reset() throws IOException { + open(); + super.reset(); + } + + public boolean markSupported() { + try { + open(); + } catch (IOException e) { + throw new IllegalStateException(e.toString()); + } + return super.markSupported(); + } + + public synchronized void mark(int readlimit) { + try { + open(); + } catch (IOException e) { + throw new IllegalStateException(e.toString()); + } + super.mark(readlimit); + } + + public long skip(long n) throws IOException { + open(); + return super.skip(n); + } + + public int read(byte[] b) throws IOException { + open(); + return super.read(b, 0, b.length); + } + + public int read(byte[] b, int off, int len) throws IOException { + open(); + return super.read(b, off, len); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LocalCache.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LocalCache.java new file mode 100644 index 00000000000..ab1abb3f0ab --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LocalCache.java @@ -0,0 +1,665 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.core.data; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.util.TransientFileFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class implements a LRU cache used by {@link CachingDataStore}. If cache + * size exceeds limit, this cache goes in purge mode. In purge mode any + * operation to cache is no-op. After purge cache size would be less than + * cachePurgeResizeFactor * maximum size. + */ +public class LocalCache { + + /** + * Logger instance. + */ + static final Logger LOG = LoggerFactory.getLogger(LocalCache.class); + + /** + * The file names of the files that need to be deleted. + */ + final Set toBeDeleted = new HashSet(); + + /** + * The filename Vs file size LRU cache. + */ + LRUCache cache; + + /** + * The directory where the files are created. + */ + private final File directory; + + /** + * The directory where tmp files are created. + */ + private final File tmp; + + /** + * If true cache is in purgeMode and not available. All operation would be + * no-op. + */ + private volatile boolean purgeMode; + + private AsyncUploadCache asyncUploadCache; + + private AtomicLong cacheMissCounter = new AtomicLong(); + + private AtomicLong cacheMissDuration = new AtomicLong(); + + + /** + * Build LRU cache of files located at 'path'. It uses lastModified property + * of file to build LRU cache. If cache size exceeds limit size, this cache + * goes in purge mode. In purge mode any operation to cache is no-op. + * + * @param path file system path + * @param tmpPath temporary directory used by cache. + * @param maxSizeInBytes maximum size of cache. + * @param cachePurgeTrigFactor factor which triggers cache to purge mode. + * That is if current size exceed (cachePurgeTrigFactor * maxSizeInBytes), the + * cache will go in auto-purge mode. + * @param cachePurgeResizeFactor after cache purge size of cache will be + * just less (cachePurgeResizeFactor * maxSizeInBytes). + * @param asyncUploadCache {@link AsyncUploadCache} + */ + public LocalCache(String path, String tmpPath, long maxSizeInBytes, double cachePurgeTrigFactor, + double cachePurgeResizeFactor, AsyncUploadCache asyncUploadCache) { + directory = new File(path); + tmp = new File(tmpPath); + LOG.info( + "cachePurgeTrigFactor =[{}], cachePurgeResizeFactor =[{}], " + + "cachePurgeTrigFactorSize =[{}], cachePurgeResizeFactorSize =[{}]", + new Object[] { cachePurgeTrigFactor, cachePurgeResizeFactor, + (cachePurgeTrigFactor * maxSizeInBytes), + (cachePurgeResizeFactor * maxSizeInBytes) }); + cache = new LRUCache(maxSizeInBytes, cachePurgeTrigFactor, cachePurgeResizeFactor); + this.asyncUploadCache = asyncUploadCache; + new Thread(new CacheBuildJob()).start(); + } + + /** + * Store an item in the cache and return the input stream. If cache is in + * purgeMode or file doesn't exists, inputstream from a + * {@link TransientFileFactory#createTransientFile(String, String, File)} is + * returned. Otherwise inputStream from cached file is returned. This method + * doesn't close the incoming inputstream. + * + * @param fileName the key of cache. + * @param in {@link InputStream} + * @return the (new) input stream. + */ + public InputStream store(String fileName, final InputStream in) + throws IOException { + fileName = fileName.replace("\\", "/"); + File f = getFile(fileName); + long length = 0; + if (!f.exists() || isInPurgeMode()) { + OutputStream out = null; + File transFile = null; + try { + TransientFileFactory tff = TransientFileFactory.getInstance(); + transFile = tff.createTransientFile("s3-", "tmp", tmp); + out = new BufferedOutputStream(new FileOutputStream(transFile)); + length = IOUtils.copyLarge(in, out); + } finally { + IOUtils.closeQuietly(out); + } + // rename the file to local fs cache + if (canAdmitFile(length) + && (f.getParentFile().exists() || f.getParentFile().mkdirs()) + && transFile.renameTo(f) && f.exists()) { + if (transFile.exists() && transFile.delete()) { + LOG.info("tmp file [{}] not deleted successfully", + transFile.getAbsolutePath()); + } + transFile = null; + LOG.debug( + "file [{}] doesn't exists. adding to local cache using inputstream.", + fileName); + cache.put(fileName, f.length()); + } else { + LOG.debug( + "file [{}] doesn't exists. returning transient file [{}].", + fileName, transFile.getAbsolutePath()); + f = transFile; + } + } else { + f.setLastModified(System.currentTimeMillis()); + LOG.debug( + "file [{}] exists. adding to local cache using inputstream.", + fileName); + cache.put(fileName, f.length()); + } + tryPurge(); + return new LazyFileInputStream(f); + } + + /** + * Store an item along with file in cache. Cache size is increased by + * {@link File#length()} If file already exists in cache, + * {@link File#setLastModified(long)} is updated with current time. + * + * @param fileName the key of cache. + * @param src file to be added to cache. + */ + public File store(String fileName, final File src) { + try { + return store(fileName, src, false).getFile(); + } catch (IOException ioe) { + LOG.warn("Exception in addding file [" + fileName + "] to local cache.", ioe); + } + return null; + } + + /** + * This method add file to {@link LocalCache} and tries that file can be + * added to {@link AsyncUploadCache}. If file is added to + * {@link AsyncUploadCache} successfully, it sets + * {@link AsyncUploadCacheResult#setAsyncUpload(boolean)} to true. + * + * @param fileName name of the file. + * @param src source file. + * @param tryForAsyncUpload If true it tries to add fileName to + * {@link AsyncUploadCache} + * @return {@link AsyncUploadCacheResult}. This method sets + * {@link AsyncUploadCacheResult#setAsyncUpload(boolean)} to true, if + * fileName is added to {@link AsyncUploadCache} successfully else + * it sets {@link AsyncUploadCacheResult#setAsyncUpload(boolean)} to + * false. {@link AsyncUploadCacheResult#getFile()} contains cached + * file, if it is added to {@link LocalCache} or original file. + * @throws IOException + */ + public AsyncUploadCacheResult store(String fileName, File src, + boolean tryForAsyncUpload) throws IOException { + fileName = fileName.replace("\\", "/"); + File dest = getFile(fileName); + File parent = dest.getParentFile(); + AsyncUploadCacheResult result = new AsyncUploadCacheResult(); + result.setFile(src); + result.setAsyncUpload(false); + boolean destExists = false; + if ((destExists = dest.exists()) + || (src.exists() && !dest.exists() && !src.equals(dest) + && canAdmitFile(src.length()) + && (parent.exists() || parent.mkdirs()) && (src.renameTo(dest)))) { + if (destExists) { + dest.setLastModified(System.currentTimeMillis()); + } + LOG.debug("file [{}] moved to [{}] ", src.getAbsolutePath(), dest.getAbsolutePath()); + LOG.debug( + "file [{}] exists= [{}] added to local cache, isLastModified [{}]", + new Object[] { dest.getAbsolutePath(), dest.exists(), + destExists }); + + cache.put(fileName, dest.length()); + result.setFile(dest); + if (tryForAsyncUpload) { + result.setAsyncUpload(asyncUploadCache.add(fileName).canAsyncUpload()); + } + } else { + LOG.info("file [{}] exists= [{}] not added to local cache.", + fileName, destExists); + } + tryPurge(); + return result; + } + /** + * Return the inputstream from from cache, or null if not in the cache. + * + * @param fileName name of file. + * @return stream or null. + */ + public InputStream getIfStored(String fileName) throws IOException { + File file = getFileIfStored(fileName); + return file == null ? null : new LazyFileInputStream(file); + } + + public File getFileIfStored(String fileName) throws IOException { + fileName = fileName.replace("\\", "/"); + File f = getFile(fileName); + long diff = (System.currentTimeMillis() - cacheMissDuration.get()) / 1000; + // logged at 5 minute interval minimum + if (diff > 5 * 60) { + LOG.info("local cache misses [{}] in [{}] sec", new Object[] { + cacheMissCounter.getAndSet(0), diff }); + cacheMissDuration.set(System.currentTimeMillis()); + } + + // return file in purge mode = true and file present in asyncUploadCache + // as asyncUploadCache's files will be not be deleted in cache purge. + if (!f.exists() || (isInPurgeMode() && !asyncUploadCache.hasEntry(fileName, false))) { + LOG.debug( + "getFileIfStored returned: purgeMode=[{}], file=[{}] exists=[{}]", + new Object[] { isInPurgeMode(), f.getAbsolutePath(), f.exists() }); + cacheMissCounter.incrementAndGet(); + return null; + } else { + // touch entry in LRU caches + f.setLastModified(System.currentTimeMillis()); + cache.get(fileName); + return f; + } + } + + /** + * Delete file from cache. Size of cache is reduced by file length. The + * method is no-op if file doesn't exist in cache. + * + * @param fileName file name that need to be removed from cache. + */ + public void delete(String fileName) { + if (isInPurgeMode()) { + LOG.debug("purgeMode true :delete returned"); + return; + } + fileName = fileName.replace("\\", "/"); + cache.remove(fileName); + } + + /** + * Returns length of file if exists in cache else returns null. + * @param fileName name of the file. + */ + public Long getFileLength(String fileName) { + Long length = null; + try { + length = cache.get(fileName); + if( length == null ) { + File f = getFileIfStored(fileName); + if (f != null) { + length = f.length(); + } + } + } catch (IOException ignore) { + + } + return length; + } + + /** + * Close the cache. Cache maintain set of files which it was not able to + * delete successfully. This method will an attempt to delete all + * unsuccessful delete files. + */ + public void close() { + LOG.debug("close"); + deleteOldFiles(); + } + + /** + * Check if cache can admit file of given length. + * @param length of the file. + * @return true if yes else return false. + */ + private boolean canAdmitFile(final long length) { + //order is important here + boolean value = !isInPurgeMode() && (cache.canAdmitFile(length)); + if (!value) { + LOG.debug("cannot admit file of length=[{}] and currentSizeInBytes=[{}] ", + length, cache.currentSizeInBytes); + } + return value; + } + + /** + * Return true if cache is in purge mode else return false. + */ + synchronized boolean isInPurgeMode() { + return purgeMode; + } + + /** + * Set purge mode. If set to true all cache operation will be no-op. If set + * to false, all operations to cache are available. + * + * @param purgeMode purge mode + */ + synchronized void setPurgeMode(final boolean purgeMode) { + this.purgeMode = purgeMode; + } + + File getFile(final String fileName) { + return new File(directory, fileName); + } + + private void deleteOldFiles() { + int initialSize = toBeDeleted.size(); + int count = 0; + for (String fileName : new ArrayList(toBeDeleted)) { + fileName = fileName.replace("\\", "/"); + if( cache.remove(fileName) != null) { + count++; + } + } + LOG.info("deleted [{}]/[{}] files.", count, initialSize); + } + + /** + * This method tries to delete a file. If it is not able to delete file due + * to any reason, it add it toBeDeleted list. + * + * @param fileName name of the file which will be deleted. + * @return true if this method deletes file successfuly else return false. + */ + boolean tryDelete(final String fileName) { + LOG.debug("try deleting file [{}]", fileName); + File f = getFile(fileName); + if (f.exists() && f.delete()) { + LOG.info("File [{}] deleted successfully", f.getAbsolutePath()); + toBeDeleted.remove(fileName); + while (true) { + f = f.getParentFile(); + if (f.equals(directory) || f.list().length > 0) { + break; + } + // delete empty parent folders (except the main directory) + f.delete(); + } + return true; + } else if (f.exists()) { + LOG.info("not able to delete file [{}]", f.getAbsolutePath()); + toBeDeleted.add(fileName); + return false; + } + return true; + } + + static int maxSizeElements(final long bytes) { + // after a CQ installation, the average item in + // the data store is about 52 KB + int count = (int) (bytes / 65535); + count = Math.max(1024, count); + count = Math.min(64 * 1024, count); + return count; + } + + /** + * This method tries purging of local cache. It checks if local cache + * has exceeded the defined limit then it triggers purge cache job in a + * seperate thread. + */ + synchronized void tryPurge() { + if (!isInPurgeMode() + && cache.currentSizeInBytes > cache.cachePurgeTrigSize) { + setPurgeMode(true); + LOG.info( + "cache.entries = [{}], currentSizeInBytes=[{}] exceeds cachePurgeTrigSize=[{}]", + new Object[] { cache.size(), cache.currentSizeInBytes, + cache.cachePurgeTrigSize }); + new Thread(new PurgeJob()).start(); + } else { + LOG.debug( + "currentSizeInBytes=[{}],cachePurgeTrigSize=[{}], isInPurgeMode =[{}]", + new Object[] { cache.currentSizeInBytes, + cache.cachePurgeTrigSize, isInPurgeMode() }); + } + } + + /** + * A LRU based extension {@link LinkedHashMap}. The key is file name and + * value is length of file. + */ + private class LRUCache extends LinkedHashMap { + private static final long serialVersionUID = 1L; + + volatile long currentSizeInBytes; + + final long maxSizeInBytes; + + final long cachePurgeResize; + + final long cachePurgeTrigSize; + + LRUCache(final long maxSizeInBytes, + final double cachePurgeTrigFactor, + final double cachePurgeResizeFactor) { + super(maxSizeElements(maxSizeInBytes), (float) 0.75, true); + this.maxSizeInBytes = maxSizeInBytes; + this.cachePurgeTrigSize = new Double(cachePurgeTrigFactor + * maxSizeInBytes).longValue(); + this.cachePurgeResize = new Double(cachePurgeResizeFactor + * maxSizeInBytes).longValue(); + } + + /** + * Overridden {@link Map#remove(Object)} to delete corresponding file + * from file system. + */ + @Override + public synchronized Long remove(final Object key) { + String fileName = (String) key; + fileName = fileName.replace("\\", "/"); + try { + // not removing file from local cache, if there is in progress + // async upload on it. + if (asyncUploadCache.hasEntry(fileName, false)) { + LOG.info( + "AsyncUploadCache upload contains file [{}]. Not removing it from LocalCache.", + fileName); + return null; + } + } catch (IOException e) { + LOG.debug("error: ", e); + return null; + } + Long flength = null; + if (tryDelete(fileName)) { + flength = super.remove(key); + if (flength != null) { + LOG.debug("cache entry [{}], with size [{}] removed.", + fileName, flength); + currentSizeInBytes -= flength.longValue(); + } + } else if (!getFile(fileName).exists()) { + // second attempt. remove from cache if file doesn't exists + flength = super.remove(key); + if (flength != null) { + LOG.debug( + "file not exists. cache entry [{}], with size [{}] removed.", + fileName, flength); + currentSizeInBytes -= flength.longValue(); + } + } else { + LOG.info("not able to remove cache entry [{}], size [{}]", key, + super.get(key)); + } + return flength; + } + + @Override + public Long put(final String fileName, final Long value) { + if( isInPurgeMode()) { + LOG.debug("cache is purge mode: put is no-op"); + return null; + } + synchronized (this) { + Long oldValue = cache.get(fileName); + if (oldValue == null) { + long flength = value.longValue(); + currentSizeInBytes += flength; + return super.put(fileName.replace("\\", "/"), value); + } + toBeDeleted.remove(fileName); + return oldValue; + } + } + + @Override + public Long get(Object key) { + if( isInPurgeMode()) { + LOG.debug("cache is purge mode: get is no-op"); + return null; + } + synchronized (this) { + return super.get(key); + } + } + + /** + * This method check if cache can admit file of given length. + * @param length length of file. + * @return true if cache size + length is less than maxSize. + */ + synchronized boolean canAdmitFile(final long length) { + return cache.currentSizeInBytes + length < cache.maxSizeInBytes; + } + } + + /** + * This class performs purging of local cache. It implements + * {@link Runnable} and should be invoked in a separate thread. + */ + private class PurgeJob implements Runnable { + public PurgeJob() { + // TODO Auto-generated constructor stub + } + + /** + * This method purges local cache till its size is less than + * cacheResizefactor * maxSize + */ + @Override + public void run() { + try { + synchronized (cache) { + // first try to delete toBeDeleted files + int initialSize = cache.size(); + LOG.info(" cache purge job started. initial cache entries = [{}]", initialSize); + for (String fileName : new ArrayList(toBeDeleted)) { + cache.remove(fileName); + } + int skipCount = 0; + Iterator> itr = cache.entrySet().iterator(); + while (itr.hasNext()) { + Map.Entry entry = itr.next(); + if (entry.getKey() != null) { + if (cache.currentSizeInBytes > cache.cachePurgeResize) { + if (cache.remove(entry.getKey()) != null) { + itr = cache.entrySet().iterator(); + for (int i = 0; i < skipCount && itr.hasNext(); i++) { + itr.next(); + } + } else { + skipCount++; + } + } else { + break; + } + } + } + LOG.info( + " cache purge job completed: cleaned [{}] files and currentSizeInBytes = [{}]", + (initialSize - cache.size()), cache.currentSizeInBytes); + } + } catch (Exception e) { + LOG.error("error in purge jobs:", e); + } finally { + setPurgeMode(false); + } + } + } + + /** + * This class implements {@link Runnable} interface to build LRU cache + * asynchronously. + */ + private class CacheBuildJob implements Runnable { + + + public void run() { + long startTime = System.currentTimeMillis(); + ArrayList allFiles = new ArrayList(); + Iterator it = FileUtils.iterateFiles(directory, null, true); + while (it.hasNext()) { + File f = it.next(); + allFiles.add(f); + } + long t1 = System.currentTimeMillis(); + LOG.debug("Time taken to recursive [{}] took [{}] sec", + allFiles.size(), ((t1 - startTime) / 1000)); + + String dataStorePath = directory.getAbsolutePath(); + // convert to java path format + dataStorePath = dataStorePath.replace("\\", "/"); + LOG.info("directoryPath = " + dataStorePath); + + String tmpPath = tmp.getAbsolutePath(); + tmpPath = tmpPath.replace("\\", "/"); + LOG.debug("tmp path [{}]", tmpPath); + long time = System.currentTimeMillis(); + int count = 0; + for (File f : allFiles) { + if (f.exists()) { + count++; + String name = f.getPath(); + String filePath = f.getAbsolutePath(); + // convert to java path format + name = name.replace("\\", "/"); + filePath = filePath.replace("\\", "/"); + // skipped any temp file + if(filePath.startsWith(tmpPath) ) { + LOG.info ("tmp file [{}] skipped ", filePath); + continue; + } + if (filePath.startsWith(dataStorePath)) { + name = filePath.substring(dataStorePath.length()); + } + if (name.startsWith("/") || name.startsWith("\\")) { + name = name.substring(1); + } + store(name, f); + long now = System.currentTimeMillis(); + if (now > time + 10000) { + LOG.info("Processed {" + (count) + "}/{" + allFiles.size() + "}"); + time = now; + } + } + } + LOG.debug( + "Processed [{}]/[{}], currentSizeInBytes = [{}], maxSizeInBytes = [{}], cache.filecount = [{}]", + new Object[] { count, allFiles.size(), + cache.currentSizeInBytes, cache.maxSizeInBytes, + cache.size() }); + long t3 = System.currentTimeMillis(); + LOG.info("Time to build cache of [{}] files took [{}] sec", + allFiles.size(), ((t3 - startTime) / 1000)); + } + } +} + diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java new file mode 100644 index 00000000000..aea8016a481 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java @@ -0,0 +1,722 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Iterator; +import java.util.concurrent.locks.ReentrantLock; + +import javax.jcr.RepositoryException; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.FileSystemResource; +import org.apache.jackrabbit.core.fs.local.LocalFileSystem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A MultiDataStore can handle two independent DataStores. + *

    + * Attention: You will lost the global single instance mechanism ! + *

    + * It can be used if you have two storage systems. One for fast access and a + * other one like a archive DataStore on a slower storage system. All Files will + * be added to the primary DataStore. On read operations first the primary + * dataStore will be used and if no Record is found the archive DataStore will + * be used. The GarabageCollector will only remove files from the archive + * DataStore. + *

    + * The internal MoveDataTask will be started automatically and could be + * configured with the following properties. + *

    + * The Configuration: + * + *

    + * <DataStore class="org.apache.jackrabbit.core.data.MultiDataStore">
    + *     <param name="{@link #setMaxAge(int) maxAge}" value="60"/>
    + *     <param name="{@link #setMoveDataTaskSleep(int) moveDataTaskSleep}" value="604800"/>
    + *     <param name="{@link #setMoveDataTaskFirstRunHourOfDay(int) moveDataTaskFirstRunHourOfDay}" value="1"/>
    + *     <param name="{@link #setSleepBetweenRecords(long) sleepBetweenRecords}" value="100"/>
    + *     <param name="{@link #setDelayedDelete(boolean) delayedDelete}" value="false"/>
    + *     <param name="{@link #setDelayedDeleteSleep(long) delayedDeleteSleep}" value="86400"/>
    + *     <param name="primary" value="org.apache.jackrabbit.core.data.db.DbDataStore">
    + *        <param .../>
    + *     </param>
    + *     <param name="archive" value="org.apache.jackrabbit.core.data.FileDataStore">
    + *        <param .../>
    + *     </param>
    + * </DataStore>
    + * 
    + * + *
      + *
    • maxAge: defines how many days the content will reside in the + * primary data store. DataRecords that have been added before this time span + * will be moved to the archive data store. (default = 60)
    • + *
    • moveDataTaskSleep: specifies the sleep time of the + * moveDataTaskThread in seconds. (default = 60 * 60 * 24 * 7, which equals 7 + * days)
    • + *
    • moveDataTaskNextRunHourOfDay: specifies the hour at which + * the moveDataTaskThread initiates its first run (default = 1 + * which means 01:00 at night)
    • + *
    • sleepBetweenRecords: specifies the delay in milliseconds + * between scanning data records (default = 100)
    • + *
    • delayedDelete: its possible to delay the delete operation on + * the primary data store. The DataIdentifiers will be written to a temporary + * file. The file will be processed after a defined sleep (see + * delayedDeleteSleep) It's useful if you like to create a snapshot + * of the primary data store backend in the meantime before the data will be + * deleted. (default = false)
    • + *
    • delayedDeleteSleep: specifies the sleep time of the + * delayedDeleteTaskThread in seconds. (default = 60 * 60 * 24, which equals 1 + * day). This means the delayed delete from the primary data store will be + * processed after one day.
    • + *
    + */ +public class MultiDataStore implements DataStore { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(MultiDataStore.class); + + private DataStore primaryDataStore; + private DataStore archiveDataStore; + + /** + * Max Age in days. + */ + private int maxAge = 60; + + /** + * ReentrantLock that is used while the MoveDataTask is running. + */ + private ReentrantLock moveDataTaskLock = new ReentrantLock(); + private boolean moveDataTaskRunning = false; + private Thread moveDataTaskThread; + + /** + * The sleep time in seconds of the MoveDataTask, 7 day default. + */ + private int moveDataTaskSleep = 60 * 60 * 24 * 7; + + /** + * Indicates when the next run of the move task is scheduled. The first run + * is scheduled by default at 01:00 hours. + */ + private Calendar moveDataTaskNextRun = Calendar.getInstance(); + + /** + * Its possible to delay the delete operation on the primary data store + * while move task is running. The delete will be executed after defined + * delayDeleteSleep. + */ + private boolean delayedDelete = false; + + /** + * The sleep time in seconds to delay remove operation on the primary data + * store, 1 day default. + */ + private long delayedDeleteSleep = 60 * 60 * 24; + + /** + * File that holds the data identifiers if delayDelete is enabled. + */ + private FileSystemResource identifiersToDeleteFile = null; + + private Thread deleteDelayedIdentifiersTaskThread; + + /** + * Name of the file which holds the identifiers if deleayed delete is + * enabled + */ + private final String IDENTIFIERS_TO_DELETE_FILE_KEY = "identifiersToDelete"; + + /** + * The delay time in milliseconds between scanning data records, 100 + * default. + */ + private long sleepBetweenRecords = 100; + + { + if (moveDataTaskNextRun.get(Calendar.HOUR_OF_DAY) >= 1) { + moveDataTaskNextRun.add(Calendar.DAY_OF_MONTH, 1); + } + moveDataTaskNextRun.set(Calendar.HOUR_OF_DAY, 1); + moveDataTaskNextRun.set(Calendar.MINUTE, 0); + moveDataTaskNextRun.set(Calendar.SECOND, 0); + moveDataTaskNextRun.set(Calendar.MILLISECOND, 0); + } + + /** + * Setter for the primary dataStore + * + * @param dataStore + */ + public void setPrimaryDataStore(DataStore dataStore) { + this.primaryDataStore = dataStore; + } + + /** + * Setter for the archive dataStore + * + * @param dataStore + */ + public void setArchiveDataStore(DataStore dataStore) { + this.archiveDataStore = dataStore; + } + + /** + * Check if a record for the given identifier exists in the primary data + * store. If not found there it will be returned from the archive data + * store. If no record exists, this method returns null. + * + * @param identifier + * data identifier + * @return the record if found, and null if not + */ + public DataRecord getRecordIfStored(DataIdentifier identifier) throws DataStoreException { + if (moveDataTaskRunning) { + moveDataTaskLock.lock(); + } + try { + DataRecord dataRecord = primaryDataStore.getRecordIfStored(identifier); + if (dataRecord == null) { + dataRecord = archiveDataStore.getRecordIfStored(identifier); + } + return dataRecord; + } finally { + if (moveDataTaskRunning) { + moveDataTaskLock.unlock(); + } + } + } + + /** + * Returns the identified data record from the primary data store. If not + * found there it will be returned from the archive data store. The given + * identifier should be the identifier of a previously saved data record. + * Since records are never removed, there should never be cases where the + * identified record is not found. Abnormal cases like that are treated as + * errors and handled by throwing an exception. + * + * @param identifier + * data identifier + * @return identified data record + * @throws DataStoreException + * if the data store could not be accessed, or if the given + * identifier is invalid + */ + public DataRecord getRecord(DataIdentifier identifier) throws DataStoreException { + if (moveDataTaskRunning) { + moveDataTaskLock.lock(); + } + try { + return primaryDataStore.getRecord(identifier); + } catch (DataStoreException e) { + return archiveDataStore.getRecord(identifier); + } finally { + if (moveDataTaskRunning) { + moveDataTaskLock.unlock(); + } + } + } + + /** + * Creates a new data record in the primary data store. The given binary + * stream is consumed and a binary record containing the consumed stream is + * created and returned. If the same stream already exists in another + * record, then that record is returned instead of creating a new one. + *

    + * The given stream is consumed and not closed by this + * method. It is the responsibility of the caller to close the stream. A + * typical call pattern would be: + * + *

    +     *     InputStream stream = ...;
    +     *     try {
    +     *         record = store.addRecord(stream);
    +     *     } finally {
    +     *         stream.close();
    +     *     }
    +     * 
    + * + * @param stream + * binary stream + * @return data record that contains the given stream + * @throws DataStoreException + * if the data store could not be accessed + */ + public DataRecord addRecord(InputStream stream) throws DataStoreException { + return primaryDataStore.addRecord(stream); + } + + /** + * From now on, update the modified date of an object even when accessing it + * in the archive data store. Usually, the modified date is only updated + * when creating a new object, or when a new link is added to an existing + * object. When this setting is enabled, even getLength() will update the + * modified date. + * + * @param before + * - update the modified date to the current time if it is older + * than this value + */ + public void updateModifiedDateOnAccess(long before) { + archiveDataStore.updateModifiedDateOnAccess(before); + } + + /** + * Delete objects that have a modified date older than the specified date + * from the archive data store. + * + * @param min + * the minimum time + * @return the number of data records deleted + * @throws DataStoreException + */ + public int deleteAllOlderThan(long min) throws DataStoreException { + return archiveDataStore.deleteAllOlderThan(min); + } + + /** + * Get all identifiers from the archive data store. + * + * @return an iterator over all DataIdentifier objects + * @throws DataStoreException + * if the list could not be read + */ + public Iterator getAllIdentifiers() throws DataStoreException { + return archiveDataStore.getAllIdentifiers(); + } + + public DataRecord getRecordFromReference(String reference) + throws DataStoreException { + DataRecord record = primaryDataStore.getRecordFromReference(reference); + if (record == null) { + record = archiveDataStore.getRecordFromReference(reference); + } + return record; + } + + /** + * {@inheritDoc} + */ + public void init(String homeDir) throws RepositoryException { + if (delayedDelete) { + // First initialize the identifiersToDeleteFile + LocalFileSystem fileSystem = new LocalFileSystem(); + fileSystem.setRoot(new File(homeDir)); + identifiersToDeleteFile = new FileSystemResource(fileSystem, FileSystem.SEPARATOR + + IDENTIFIERS_TO_DELETE_FILE_KEY); + } + moveDataTaskThread = new Thread(new MoveDataTask(), + "Jackrabbit-MulitDataStore-MoveDataTaskThread"); + moveDataTaskThread.setDaemon(true); + moveDataTaskThread.start(); + log.info("MultiDataStore-MoveDataTask thread started; first run scheduled at " + + moveDataTaskNextRun.getTime()); + if (delayedDelete) { + try { + // Run on startup the DeleteDelayedIdentifiersTask only if the + // file exists and modify date is older than the + // delayedDeleteSleep timeout ... + if (identifiersToDeleteFile != null + && identifiersToDeleteFile.exists() + && (identifiersToDeleteFile.lastModified() + (delayedDeleteSleep * 1000)) < System + .currentTimeMillis()) { + deleteDelayedIdentifiersTaskThread = new Thread( + //Start immediately ... + new DeleteDelayedIdentifiersTask(0L), + "Jackrabbit-MultiDataStore-DeleteDelayedIdentifiersTaskThread"); + deleteDelayedIdentifiersTaskThread.setDaemon(true); + deleteDelayedIdentifiersTaskThread.start(); + log.info("Old entries in the " + IDENTIFIERS_TO_DELETE_FILE_KEY + + " File found. DeleteDelayedIdentifiersTask-Thread started now."); + } + } catch (FileSystemException e) { + throw new RepositoryException("I/O error while reading from '" + + identifiersToDeleteFile.getPath() + "'", e); + } + } + } + + /** + * Get the minimum size of an object that should be stored in the primary + * data store. + * + * @return the minimum size in bytes + */ + public int getMinRecordLength() { + return primaryDataStore.getMinRecordLength(); + } + + /** + * {@inheritDoc} + */ + public void close() throws DataStoreException { + DataStoreException lastException = null; + // 1. close the primary data store + try { + primaryDataStore.close(); + } catch (DataStoreException e) { + lastException = e; + } + // 2. close the archive data store + try { + archiveDataStore.close(); + } catch (DataStoreException e) { + if (lastException != null) { + lastException = new DataStoreException(lastException); + } + } + // 3. if moveDataTaskThread is running interrupt it + try { + if (moveDataTaskRunning) { + moveDataTaskThread.interrupt(); + } + } catch (Exception e) { + if (lastException != null) { + lastException = new DataStoreException(lastException); + } + } + // 4. if deleteDelayedIdentifiersTaskThread is running interrupt it + try { + if (deleteDelayedIdentifiersTaskThread != null + && deleteDelayedIdentifiersTaskThread.isAlive()) { + deleteDelayedIdentifiersTaskThread.interrupt(); + } + } catch (Exception e) { + if (lastException != null) { + lastException = new DataStoreException(lastException); + } + } + if (lastException != null) { + throw lastException; + } + } + + /** + * {@inheritDoc} + */ + public void clearInUse() { + archiveDataStore.clearInUse(); + } + + public int getMaxAge() { + return maxAge; + } + + public void setMaxAge(int maxAge) { + this.maxAge = maxAge; + } + + public int getMoveDataTaskSleep() { + return moveDataTaskSleep; + } + + public int getMoveDataTaskFirstRunHourOfDay() { + return moveDataTaskNextRun.get(Calendar.HOUR_OF_DAY); + } + + public void setMoveDataTaskSleep(int sleep) { + this.moveDataTaskSleep = sleep; + } + + public void setMoveDataTaskFirstRunHourOfDay(int hourOfDay) { + moveDataTaskNextRun = Calendar.getInstance(); + if (moveDataTaskNextRun.get(Calendar.HOUR_OF_DAY) >= hourOfDay) { + moveDataTaskNextRun.add(Calendar.DAY_OF_MONTH, 1); + } + moveDataTaskNextRun.set(Calendar.HOUR_OF_DAY, hourOfDay); + moveDataTaskNextRun.set(Calendar.MINUTE, 0); + moveDataTaskNextRun.set(Calendar.SECOND, 0); + moveDataTaskNextRun.set(Calendar.MILLISECOND, 0); + } + + public void setSleepBetweenRecords(long millis) { + this.sleepBetweenRecords = millis; + } + + public long getSleepBetweenRecords() { + return sleepBetweenRecords; + } + + public boolean isDelayedDelete() { + return delayedDelete; + } + + public void setDelayedDelete(boolean delayedDelete) { + this.delayedDelete = delayedDelete; + } + + public long getDelayedDeleteSleep() { + return delayedDeleteSleep; + } + + public void setDelayedDeleteSleep(long delayedDeleteSleep) { + this.delayedDeleteSleep = delayedDeleteSleep; + } + + /** + * Writes the given DataIdentifier to the delayedDeletedFile. + * + * @param identifier + * @return boolean true if it was successful otherwise false + */ + private boolean writeDelayedDataIdentifier(DataIdentifier identifier) { + BufferedWriter writer = null; + try { + File identifierFile = new File( + ((LocalFileSystem) identifiersToDeleteFile.getFileSystem()).getPath(), + identifiersToDeleteFile.getPath()); + writer = new BufferedWriter(new FileWriter(identifierFile, true)); + writer.write(identifier.toString()); + return true; + } catch (Exception e) { + log.warn("I/O error while saving DataIdentifier (stacktrace on DEBUG log level) to '" + + identifiersToDeleteFile.getPath() + "': " + e.getMessage()); + log.debug("Root cause: ", e); + return false; + } finally { + IOUtils.closeQuietly(writer); + } + } + + /** + * Purges the delayedDeletedFile. + * + * @return boolean true if it was successful otherwise false + */ + private boolean purgeDelayedDeleteFile() { + BufferedWriter writer = null; + try { + writer = new BufferedWriter(new OutputStreamWriter( + identifiersToDeleteFile.getOutputStream())); + writer.write(""); + return true; + } catch (Exception e) { + log.warn("I/O error while purging (stacktrace on DEBUG log level) the " + + IDENTIFIERS_TO_DELETE_FILE_KEY + " file '" + + identifiersToDeleteFile.getPath() + "': " + e.getMessage()); + log.debug("Root cause: ", e); + return false; + } finally { + IOUtils.closeQuietly(writer); + } + } + + /** + * Class for maintaining the MultiDataStore. It will be used to move the + * content of the primary data store to the archive data store. + */ + public class MoveDataTask implements Runnable { + + /** + * {@inheritDoc} + */ + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + log.info("Next move-data task run scheduled at " + + moveDataTaskNextRun.getTime()); + long sleepTime = moveDataTaskNextRun.getTimeInMillis() + - System.currentTimeMillis(); + if (sleepTime > 0) { + Thread.sleep(sleepTime); + } + moveDataTaskRunning = true; + moveOutdatedData(); + moveDataTaskRunning = false; + moveDataTaskNextRun.add(Calendar.SECOND, moveDataTaskSleep); + if (delayedDelete) { + if (deleteDelayedIdentifiersTaskThread != null + && deleteDelayedIdentifiersTaskThread.isAlive()) { + log.warn("The DeleteDelayedIdentifiersTask-Thread is already running."); + } else { + deleteDelayedIdentifiersTaskThread = new Thread( + new DeleteDelayedIdentifiersTask(delayedDeleteSleep), + "Jackrabbit-MultiDataStore-DeleteDelayedIdentifiersTaskThread"); + deleteDelayedIdentifiersTaskThread.setDaemon(true); + deleteDelayedIdentifiersTaskThread.start(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + log.warn("Interrupted: stopping move-data task."); + } + + /** + * Moves outdated data from primary to archive data store + */ + protected void moveOutdatedData() { + try { + long now = System.currentTimeMillis(); + long maxAgeMilli = 1000L * 60 * 60 * 24 * maxAge; + log.debug("Collecting all Identifiers from PrimaryDataStore..."); + Iterator allIdentifiers = primaryDataStore.getAllIdentifiers(); + int moved = 0; + while (allIdentifiers.hasNext()) { + DataIdentifier identifier = allIdentifiers.next(); + DataRecord dataRecord = primaryDataStore.getRecord(identifier); + if ((dataRecord.getLastModified() + maxAgeMilli) < now) { + try { + moveDataTaskLock.lock(); + if (delayedDelete) { + // first write it to the file and then add it to + // the archive data store ... + if (writeDelayedDataIdentifier(identifier)) { + archiveDataStore.addRecord(dataRecord.getStream()); + moved++; + } + } else { + // first add it and then delete it .. not really + // atomic ... + archiveDataStore.addRecord(dataRecord.getStream()); + ((MultiDataStoreAware) primaryDataStore).deleteRecord(identifier); + moved++; + } + if (moved % 100 == 0) { + log.debug("Moving DataRecord's... ({})", moved); + } + } catch (DataStoreException e) { + log.error("Failed to move DataRecord. DataIdentifier: " + identifier, e); + } finally { + moveDataTaskLock.unlock(); + } + } + // Give other threads time to use the MultiDataStore while + // MoveDataTask is running.. + Thread.sleep(sleepBetweenRecords); + } + if (delayedDelete) { + log.info("Moved " + + moved + + " DataRecords to the archive data store. The DataRecords in the primary data store will be removed in " + + delayedDeleteSleep + " seconds."); + } else { + log.info("Moved " + moved + " DataRecords to the archive data store."); + } + } catch (Exception e) { + log.warn("Failed to run move-data task.", e); + } + } + } + + /** + * Class to clean up the delayed DataRecords from the primary data store. + */ + public class DeleteDelayedIdentifiersTask implements Runnable { + + boolean run = true; + private long sleepTime = 0L; + + /** + * Constructor + * @param sleep how long this DeleteDelayedIdentifiersTask should sleep in seconds. + */ + public DeleteDelayedIdentifiersTask(long sleep) { + this.sleepTime = (sleep * 1000L); + } + + @Override + public void run() { + if (moveDataTaskRunning) { + log.warn("It's not supported to run the DeleteDelayedIdentifiersTask while the MoveDataTask is running."); + return; + } + while (run && !Thread.currentThread().isInterrupted()) { + if (sleepTime > 0) { + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + log.info("Start to delete DataRecords from the primary data store."); + BufferedReader reader = null; + ArrayList problemIdentifiers = new ArrayList(); + try { + int deleted = 0; + reader = new BufferedReader(new InputStreamReader( + identifiersToDeleteFile.getInputStream())); + while (true) { + String s = reader.readLine(); + if (s == null || s.equals("")) { + break; + } + DataIdentifier identifier = new DataIdentifier(s); + try { + moveDataTaskLock.lock(); + ((MultiDataStoreAware) primaryDataStore).deleteRecord(identifier); + deleted++; + } catch (DataStoreException e) { + log.error("Failed to delete DataRecord. DataIdentifier: " + identifier, + e); + problemIdentifiers.add(identifier); + } finally { + moveDataTaskLock.unlock(); + } + // Give other threads time to use the MultiDataStore + // while + // DeleteDelayedIdentifiersTask is running.. + Thread.sleep(sleepBetweenRecords); + } + log.info("Deleted " + deleted + " DataRecords from the primary data store."); + if (problemIdentifiers.isEmpty()) { + try { + identifiersToDeleteFile.delete(); + } catch (FileSystemException e) { + log.warn("Unable to delete the " + IDENTIFIERS_TO_DELETE_FILE_KEY + + " File."); + if (!purgeDelayedDeleteFile()) { + log.error("Unable to purge the " + IDENTIFIERS_TO_DELETE_FILE_KEY + + " File."); + } + } + } else { + if (purgeDelayedDeleteFile()) { + for (int x = 0; x < problemIdentifiers.size(); x++) { + writeDelayedDataIdentifier(problemIdentifiers.get(x)); + } + } + } + } catch (InterruptedException e) { + log.warn("Interrupted: stopping delayed-delete task."); + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.warn("Failed to run delayed-delete task.", e); + } finally { + IOUtils.closeQuietly(reader); + run = false; + } + } + } + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java new file mode 100644 index 00000000000..b4bb5c20ecc --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.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.core.data; + +import org.apache.jackrabbit.core.data.MultiDataStore.MoveDataTask; + +/** + * To use a DataStore within a MultiDataStore it must implement this + * MultiDataStoreAware Interface. It extends a DataStore to delete a single + * DataRecord. + */ +public interface MultiDataStoreAware { + + /** + * Deletes a single DataRecord based on the given identifier. Delete will + * only be used by the {@link MoveDataTask}. + * + * @param identifier + * data identifier + * @throws DataStoreException + * if the data store could not be accessed, or if the given + * identifier is invalid + */ + void deleteRecord(DataIdentifier identifier) throws DataStoreException; + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/ScanEventListener.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/ScanEventListener.java new file mode 100644 index 00000000000..f442f6f7d30 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/ScanEventListener.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.core.data; + +import org.apache.jackrabbit.api.management.MarkEventListener; + +/** + * The listener interface for receiving garbage collection scan events. + */ +public interface ScanEventListener extends MarkEventListener { + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataRecord.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataRecord.java new file mode 100644 index 00000000000..417b3ced777 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataRecord.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.core.data.db; + +import org.apache.jackrabbit.core.data.AbstractDataRecord; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataStoreException; + +import java.io.BufferedInputStream; +import java.io.InputStream; + +/** + * Data record that is stored in a database + */ +public class DbDataRecord extends AbstractDataRecord { + + protected final DbDataStore store; + protected final long length; + protected long lastModified; + + /** + * Creates a data record based on the given identifier and length. + * + * @param identifier data identifier + * @param length the length + * @param lastModified + */ + public DbDataRecord(DbDataStore store, DataIdentifier identifier, long length, long lastModified) { + super(store, identifier); + this.store = store; + this.length = length; + this.lastModified = lastModified; + } + + /** + * {@inheritDoc} + */ + public long getLength() throws DataStoreException { + lastModified = store.touch(getIdentifier(), lastModified); + return length; + } + + /** + * {@inheritDoc} + */ + public InputStream getStream() throws DataStoreException { + lastModified = store.touch(getIdentifier(), lastModified); + return new BufferedInputStream(new DbInputStream(store, getIdentifier())); + } + + /** + * {@inheritDoc} + */ + public long getLastModified() { + return lastModified; + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java new file mode 100644 index 00000000000..be80f83e3cb --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java @@ -0,0 +1,1014 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data.db; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.CountingInputStream; +import org.apache.jackrabbit.core.data.AbstractDataStore; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataRecord; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.data.MultiDataStoreAware; +import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DatabaseAware; +import org.apache.jackrabbit.core.util.db.DbUtility; +import org.apache.jackrabbit.core.util.db.StreamWrapper; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.WeakReference; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; +import java.util.WeakHashMap; + +import javax.jcr.RepositoryException; +import javax.sql.DataSource; + +/** + * A data store implementation that stores the records in a database using JDBC. + * + * Configuration: + *
    + * <DataStore class="org.apache.jackrabbit.core.data.db.DbDataStore">
    + *     <param name="{@link #setUrl(String) url}" value="jdbc:postgresql:test"/>
    + *     <param name="{@link #setUser(String) user}" value="sa"/>
    + *     <param name="{@link #setPassword(String) password}" value="sa"/>
    + *     <param name="{@link #setDatabaseType(String) databaseType}" value="postgresql"/>
    + *     <param name="{@link #setDriver(String) driver}" value="org.postgresql.Driver"/>
    + *     <param name="{@link #setMinRecordLength(int) minRecordLength}" value="1024"/>
    + *     <param name="{@link #setMaxConnections(int) maxConnections}" value="2"/>
    + *     <param name="{@link #setCopyWhenReading(boolean) copyWhenReading}" value="true"/>
    + *     <param name="{@link #setTablePrefix(String) tablePrefix}" value=""/>
    + *     <param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value=""/>
    + *     <param name="{@link #setSchemaCheckEnabled(boolean) schemaCheckEnabled}" value="true"/>
    + * </DataStore>
    + * 
    + *

    + * Only URL, user name and password usually need to be set. + * The remaining settings are generated using the database URL sub-protocol from the + * database type resource file. + *

    + * JNDI can be used to get the connection. In this case, use the javax.naming.InitialContext as the driver, + * and the JNDI name as the URL. If the user and password are configured in the JNDI resource, + * they should not be configured here. Example JNDI settings: + *

    + * <param name="driver" value="javax.naming.InitialContext" />
    + * <param name="url" value="java:comp/env/jdbc/Test" />
    + * 
    + *

    + * For Microsoft SQL Server 2005, there is a problem reading large BLOBs. You will need to use + * the JDBC driver version 1.2 or newer, and append ;responseBuffering=adaptive to the database URL. + * Don't append ;selectMethod=cursor, otherwise it can still run out of memory. + * Example database URL: jdbc:sqlserver://localhost:4220;DatabaseName=test;responseBuffering=adaptive + *

    + * By default, the data is copied to a temp file when reading, to avoid problems when reading multiple + * blobs at the same time. + *

    + * The tablePrefix can be used to specify a schema and / or catalog name: + * <param name="tablePrefix" value="ds."> + */ +public class DbDataStore extends AbstractDataStore + implements DatabaseAware, MultiDataStoreAware { + + /** + * The default value for the minimum object size. + */ + public static final int DEFAULT_MIN_RECORD_LENGTH = 100; + + /** + * Write to a temporary file to get the length (slow, but always works). + * This is the default setting. + */ + public static final String STORE_TEMP_FILE = "tempFile"; + + /** + * Call PreparedStatement.setBinaryStream(..., -1) + */ + public static final String STORE_SIZE_MINUS_ONE = "-1"; + + /** + * Call PreparedStatement.setBinaryStream(..., Integer.MAX_VALUE) + */ + public static final String STORE_SIZE_MAX = "max"; + + /** + * The digest algorithm used to uniquely identify records. + */ + protected static final String DIGEST = System.getProperty("ds.digest.algorithm", "SHA-256"); + + /** + * The prefix used for temporary objects. + */ + protected static final String TEMP_PREFIX = "TEMP_"; + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(DbDataStore.class); + + /** + * The minimum modified date. If a file is accessed (read or write) with a modified date + * older than this value, the modified date is updated to the current time. + */ + protected long minModifiedDate; + + /** + * The database URL used. + */ + protected String url; + + /** + * The database driver. + */ + protected String driver; + + /** + * The user name. + */ + protected String user; + + /** + * The password + */ + protected String password; + + /** + * The database type used. + */ + protected String databaseType; + + /** + * The minimum size of an object that should be stored in this data store. + */ + protected int minRecordLength = DEFAULT_MIN_RECORD_LENGTH; + + /** + * The prefix for the datastore table, empty by default. + */ + protected String tablePrefix = ""; + + /** + * The prefix of the table names. By default it is empty. + */ + protected String schemaObjectPrefix = ""; + + /** + * Whether the schema check must be done during initialization. + */ + private boolean schemaCheckEnabled = true; + + /** + * The logical name of the DataSource to use. + */ + protected String dataSourceName; + + /** + * This is the property 'table' + * in the [databaseType].properties file, initialized with the default value. + */ + protected String tableSQL = "DATASTORE"; + + /** + * This is the property 'createTable' + * in the [databaseType].properties file, initialized with the default value. + */ + protected String createTableSQL = + "CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA BLOB)"; + + /** + * This is the property 'insertTemp' + * in the [databaseType].properties file, initialized with the default value. + */ + protected String insertTempSQL = + "INSERT INTO ${tablePrefix}${table} VALUES(?, 0, ?, NULL)"; + + /** + * This is the property 'updateData' + * in the [databaseType].properties file, initialized with the default value. + */ + protected String updateDataSQL = + "UPDATE ${tablePrefix}${table} SET DATA=? WHERE ID=?"; + + /** + * This is the property 'updateLastModified' + * in the [databaseType].properties file, initialized with the default value. + */ + protected String updateLastModifiedSQL = + "UPDATE ${tablePrefix}${table} SET LAST_MODIFIED=? WHERE ID=? AND LAST_MODIFIED> inUse = + Collections.synchronizedMap(new WeakHashMap>()); + + /** + * The temporary identifiers that are currently in use. + */ + protected List temporaryInUse = Collections.synchronizedList(new ArrayList()); + + /** + * The {@link ConnectionHelper} set in the {@link #init(String)} method. + * */ + protected ConnectionHelper conHelper; + + /** + * The repositories {@link ConnectionFactory}. + */ + private ConnectionFactory connectionFactory; + + public void setConnectionFactory(ConnectionFactory connnectionFactory) { + this.connectionFactory = connnectionFactory; + } + + public DataRecord addRecord(InputStream stream) throws DataStoreException { + InputStream fileInput = null; + String tempId = null; + ResultSet rs = null; + try { + long tempModified; + while (true) { + try { + tempModified = System.currentTimeMillis(); + String id = UUID.randomUUID().toString(); + tempId = TEMP_PREFIX + id; + temporaryInUse.add(tempId); + // SELECT LENGTH, LAST_MODIFIED FROM DATASTORE WHERE ID=? + rs = conHelper.query(selectMetaSQL, tempId); + boolean hasNext = rs.next(); + DbUtility.close(rs); + rs = null; + if (hasNext) { + // re-try in the very, very unlikely event that the row already exists + continue; + } + // INSERT INTO DATASTORE VALUES(?, 0, ?, NULL) + conHelper.exec(insertTempSQL, tempId, tempModified); + break; + } catch (Exception e) { + throw convert("Can not insert new record", e); + } finally { + DbUtility.close(rs); + // prevent that rs.close() is called again + rs = null; + } + } + MessageDigest digest = getDigest(); + DigestInputStream dIn = new DigestInputStream(stream, digest); + CountingInputStream in = new CountingInputStream(dIn); + StreamWrapper wrapper; + if (STORE_SIZE_MINUS_ONE.equals(storeStream)) { + wrapper = new StreamWrapper(in, -1); + } else if (STORE_SIZE_MAX.equals(storeStream)) { + wrapper = new StreamWrapper(in, Integer.MAX_VALUE); + } else if (STORE_TEMP_FILE.equals(storeStream)) { + File temp = moveToTempFile(in); + long length = temp.length(); + wrapper = new StreamWrapper(new ResettableTempFileInputStream(temp), length); + } else { + throw new DataStoreException("Unsupported stream store algorithm: " + storeStream); + } + // UPDATE DATASTORE SET DATA=? WHERE ID=? + conHelper.exec(updateDataSQL, wrapper, tempId); + long length = in.getByteCount(); + DataIdentifier identifier = + new DataIdentifier(encodeHexString(digest.digest())); + usesIdentifier(identifier); + String id = identifier.toString(); + long newModified; + while (true) { + newModified = System.currentTimeMillis(); + if (checkExisting(tempId, length, identifier)) { + touch(identifier, newModified); + conHelper.exec(deleteSQL, tempId); + break; + } + try { + // UPDATE DATASTORE SET ID=?, LENGTH=?, LAST_MODIFIED=? + // WHERE ID=? AND LAST_MODIFIED=? + int count = conHelper.update(updateSQL, + id, length, newModified, tempId, tempModified); + // If update count is 0, the last modified time of the + // temporary row was changed - which means we need to + // re-try using a new last modified date (a later one) + // because we need to ensure the new last modified date + // is _newer_ than the old (otherwise the garbage + // collection could delete rows) + if (count != 0) { + // update was successful + break; + } + } catch (SQLException e) { + // duplicate key (the row already exists) - repeat + // we use exception handling for flow control here, which is bad, + // but the alternative is to use UPDATE ... WHERE ... (SELECT ...) + // which could cause a deadlock in some databases - also, + // duplicate key will only occur if somebody else concurrently + // added the same record (which is very unlikely) + } + // SELECT LENGTH, LAST_MODIFIED FROM DATASTORE WHERE ID=? + rs = conHelper.query(selectMetaSQL, tempId); + if (!rs.next()) { + // the row was deleted, which is unexpected / not allowed + String msg = + DIGEST + " temporary entry deleted: " + + " id=" + tempId + " length=" + length; + log.error(msg); + throw new DataStoreException(msg); + } + tempModified = rs.getLong(2); + DbUtility.close(rs); + rs = null; + } + usesIdentifier(identifier); + DbDataRecord record = new DbDataRecord(this, identifier, length, newModified); + return record; + } catch (Exception e) { + throw convert("Can not insert new record", e); + } finally { + if (tempId != null) { + temporaryInUse.remove(tempId); + } + DbUtility.close(rs); + if (fileInput != null) { + try { + fileInput.close(); + } catch (IOException e) { + throw convert("Can not close temporary file", e); + } + } + } + } + + /** + * Check if a row with this ID already exists. + * + * @return true if the row exists and the length matches + * @throw DataStoreException if a row exists, but the length is different + */ + private boolean checkExisting(String tempId, long length, DataIdentifier identifier) throws DataStoreException, SQLException { + String id = identifier.toString(); + // SELECT LENGTH, LAST_MODIFIED FROM DATASTORE WHERE ID=? + ResultSet rs = null; + try { + rs = conHelper.query(selectMetaSQL, id); + if (rs.next()) { + long oldLength = rs.getLong(1); + long lastModified = rs.getLong(2); + if (oldLength != length) { + String msg = + DIGEST + " collision: temp=" + tempId + + " id=" + id + " length=" + length + + " oldLength=" + oldLength; + log.error(msg); + throw new DataStoreException(msg); + } + DbUtility.close(rs); + rs = null; + touch(identifier, lastModified); + // row already exists + conHelper.exec(deleteSQL, tempId); + return true; + } + } finally { + DbUtility.close(rs); + } + return false; + } + + /** + * Creates a temp file and copies the data there. + * The input stream is closed afterwards. + * + * @param in the input stream + * @return the file + * @throws IOException + */ + private File moveToTempFile(InputStream in) throws IOException { + File temp = File.createTempFile("dbRecord", null); + writeToFileAndClose(in, temp); + return temp; + } + + private void writeToFileAndClose(InputStream in, File file) throws IOException { + OutputStream out = new FileOutputStream(file); + try { + IOUtils.copy(in, out); + } finally { + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(in); + } + } + + public synchronized void deleteRecord(DataIdentifier identifier) throws DataStoreException { + try { + conHelper.exec(deleteSQL, identifier.toString()); + } catch (Exception e) { + throw convert("Can not delete record", e); + } + } + + public synchronized int deleteAllOlderThan(long min) throws DataStoreException { + try { + ArrayList touch = new ArrayList(); + ArrayList ids = new ArrayList(inUse.keySet()); + for (DataIdentifier identifier: ids) { + if (identifier != null) { + touch.add(identifier.toString()); + } + } + touch.addAll(temporaryInUse); + for (String key : touch) { + updateLastModifiedDate(key, 0); + } + // DELETE FROM DATASTORE WHERE LAST_MODIFIED getAllIdentifiers() throws DataStoreException { + ArrayList list = new ArrayList(); + ResultSet rs = null; + try { + // SELECT ID FROM DATASTORE + rs = conHelper.query(selectAllSQL); + while (rs.next()) { + String id = rs.getString(1); + if (!id.startsWith(TEMP_PREFIX)) { + DataIdentifier identifier = new DataIdentifier(id); + list.add(identifier); + } + } + log.debug("Found " + list.size() + " identifiers."); + return list.iterator(); + } catch (Exception e) { + throw convert("Can not read records", e); + } finally { + DbUtility.close(rs); + } + } + + public int getMinRecordLength() { + return minRecordLength; + } + + /** + * Set the minimum object length. + * The maximum value is around 32000. + * + * @param minRecordLength the length + */ + public void setMinRecordLength(int minRecordLength) { + this.minRecordLength = minRecordLength; + } + + public DataRecord getRecordIfStored(DataIdentifier identifier) throws DataStoreException { + usesIdentifier(identifier); + ResultSet rs = null; + try { + String id = identifier.toString(); + // SELECT LENGTH, LAST_MODIFIED FROM DATASTORE WHERE ID = ? + rs = conHelper.query(selectMetaSQL, id); + if (!rs.next()) { + return null; + } + long length = rs.getLong(1); + long lastModified = rs.getLong(2); + DbUtility.close(rs); + rs = null; + lastModified = touch(identifier, lastModified); + return new DbDataRecord(this, identifier, length, lastModified); + } catch (Exception e) { + throw convert("Can not read identifier " + identifier, e); + } finally { + DbUtility.close(rs); + } + } + + /** + * Open the input stream. This method sets those fields of the caller + * that need to be closed once the input stream is read. + * + * @param inputStream the database input stream object + * @param identifier data identifier + * @throws DataStoreException if the data store could not be accessed, + * or if the given identifier is invalid + */ + InputStream openStream(DbInputStream inputStream, DataIdentifier identifier) throws DataStoreException { + ResultSet rs = null; + try { + // SELECT ID, DATA FROM DATASTORE WHERE ID = ? + rs = conHelper.query(selectDataSQL, identifier.toString()); + if (!rs.next()) { + throw new DataStoreException("Record not found: " + identifier); + } + InputStream stream = rs.getBinaryStream(2); + if (stream == null) { + stream = new ByteArrayInputStream(new byte[0]); + DbUtility.close(rs); + } else if (copyWhenReading) { + // If we copy while reading, create a temp file and close the stream + File temp = moveToTempFile(stream); + stream = new BufferedInputStream(new TempFileInputStream(temp)); + DbUtility.close(rs); + } else { + stream = new BufferedInputStream(stream); + inputStream.setResultSet(rs); + } + return stream; + } catch (Exception e) { + DbUtility.close(rs); + throw convert("Retrieving database resource ", e); + } + } + + public synchronized void init(String homeDir) throws DataStoreException { + try { + initDatabaseType(); + + conHelper = createConnectionHelper(getDataSource()); + + if (isSchemaCheckEnabled()) { + createCheckSchemaOperation().run(); + } + } catch (Exception e) { + throw convert("Can not init data store, driver=" + driver + " url=" + url + " user=" + user + + " schemaObjectPrefix=" + schemaObjectPrefix + " tableSQL=" + tableSQL + " createTableSQL=" + createTableSQL, e); + } + } + + private DataSource getDataSource() throws Exception { + if (getDataSourceName() == null || "".equals(getDataSourceName())) { + return connectionFactory.getDataSource(getDriver(), getUrl(), getUser(), getPassword()); + } else { + return connectionFactory.getDataSource(dataSourceName); + } + } + + /** + * This method is called from the {@link #init(String)} method of this class and returns a + * {@link ConnectionHelper} instance which is assigned to the {@code conHelper} field. Subclasses may + * override it to return a specialized connection helper. + * + * @param dataSrc the {@link DataSource} of this persistence manager + * @return a {@link ConnectionHelper} + * @throws Exception on error + */ + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + return new ConnectionHelper(dataSrc, false); + } + + /** + * This method is called from {@link #init(String)} after the + * {@link #createConnectionHelper(DataSource)} method, and returns a default {@link CheckSchemaOperation}. + * + * @return a new {@link CheckSchemaOperation} instance + */ + protected final CheckSchemaOperation createCheckSchemaOperation() { + String tableName = tablePrefix + schemaObjectPrefix + tableSQL; + return new CheckSchemaOperation(conHelper, new ByteArrayInputStream(createTableSQL.getBytes()), tableName); + } + + protected void initDatabaseType() throws DataStoreException { + boolean failIfNotFound = false; + if (databaseType == null) { + if (dataSourceName != null) { + try { + databaseType = connectionFactory.getDataBaseType(dataSourceName); + } catch (RepositoryException e) { + throw new DataStoreException(e); + } + } else { + if (!url.startsWith("jdbc:")) { + return; + } + int start = "jdbc:".length(); + int end = url.indexOf(':', start); + databaseType = url.substring(start, end); + } + } else { + failIfNotFound = true; + } + + InputStream in = + DbDataStore.class.getResourceAsStream(databaseType + ".properties"); + if (in == null) { + if (failIfNotFound) { + String msg = + "Configuration error: The resource '" + databaseType + + ".properties' could not be found;" + + " Please verify the databaseType property"; + log.debug(msg); + throw new DataStoreException(msg); + } else { + return; + } + } + Properties prop = new Properties(); + try { + try { + prop.load(in); + } finally { + in.close(); + } + } catch (IOException e) { + String msg = "Configuration error: Could not read properties '" + databaseType + ".properties'"; + log.debug(msg); + throw new DataStoreException(msg, e); + } + if (driver == null) { + driver = getProperty(prop, "driver", driver); + } + tableSQL = getProperty(prop, "table", tableSQL); + createTableSQL = getProperty(prop, "createTable", createTableSQL); + insertTempSQL = getProperty(prop, "insertTemp", insertTempSQL); + updateDataSQL = getProperty(prop, "updateData", updateDataSQL); + updateLastModifiedSQL = getProperty(prop, "updateLastModified", updateLastModifiedSQL); + updateSQL = getProperty(prop, "update", updateSQL); + deleteSQL = getProperty(prop, "delete", deleteSQL); + deleteOlderSQL = getProperty(prop, "deleteOlder", deleteOlderSQL); + selectMetaSQL = getProperty(prop, "selectMeta", selectMetaSQL); + selectAllSQL = getProperty(prop, "selectAll", selectAllSQL); + selectDataSQL = getProperty(prop, "selectData", selectDataSQL); + storeStream = getProperty(prop, "storeStream", storeStream); + if (!STORE_SIZE_MINUS_ONE.equals(storeStream) + && !STORE_TEMP_FILE.equals(storeStream) + && !STORE_SIZE_MAX.equals(storeStream)) { + String msg = "Unsupported Stream store mechanism: " + storeStream + + " supported are: " + STORE_SIZE_MINUS_ONE + ", " + + STORE_TEMP_FILE + ", " + STORE_SIZE_MAX; + log.debug(msg); + throw new DataStoreException(msg); + } + } + + /** + * Get the expanded property value. The following placeholders are supported: + * ${table}: the table name (the default is DATASTORE) and + * ${tablePrefix}: tablePrefix plus schemaObjectPrefix as set in the configuration + * + * @param prop the properties object + * @param key the key + * @param defaultValue the default value + * @return the property value (placeholders are replaced) + */ + protected String getProperty(Properties prop, String key, String defaultValue) { + String sql = prop.getProperty(key, defaultValue); + sql = Text.replace(sql, "${table}", tableSQL).trim(); + sql = Text.replace(sql, "${tablePrefix}", tablePrefix + schemaObjectPrefix).trim(); + return sql; + } + + /** + * Convert an exception to a data store exception. + * + * @param cause the message + * @param e the root cause + * @return the data store exception + */ + protected DataStoreException convert(String cause, Exception e) { + log.warn(cause, e); + if (e instanceof DataStoreException) { + return (DataStoreException) e; + } else { + return new DataStoreException(cause, e); + } + } + + public void updateModifiedDateOnAccess(long before) { + log.debug("Update modifiedDate on access before " + before); + minModifiedDate = before; + } + + /** + * Update the modified date of an entry if required. + * + * @param identifier the entry identifier + * @param lastModified the current last modified date + * @return the new modified date + */ + long touch(DataIdentifier identifier, long lastModified) throws DataStoreException { + usesIdentifier(identifier); + return updateLastModifiedDate(identifier.toString(), lastModified); + } + + private long updateLastModifiedDate(String key, long lastModified) throws DataStoreException { + if (lastModified < minModifiedDate) { + long now = System.currentTimeMillis(); + try { + // UPDATE DATASTORE SET LAST_MODIFIED = ? WHERE ID = ? AND LAST_MODIFIED < ? + conHelper.update(updateLastModifiedSQL, now, key, now); + return now; + } catch (Exception e) { + throw convert("Can not update lastModified", e); + } + } + return lastModified; + } + + /** + * Get the database type (if set). + * @return the database type + */ + public String getDatabaseType() { + return databaseType; + } + + /** + * Set the database type. By default the sub-protocol of the JDBC database URL is used if it is not set. + * It must match the resource file [databaseType].properties. Example: mysql. + * + * @param databaseType + */ + public void setDatabaseType(String databaseType) { + this.databaseType = databaseType; + } + + /** + * Get the database driver + * + * @return the driver + */ + public String getDriver() { + return driver; + } + + /** + * Set the database driver class name. + * If not set, the default driver class name for the database type is used, + * as set in the [databaseType].properties resource; key 'driver'. + * + * @param driver + */ + public void setDriver(String driver) { + this.driver = driver; + } + + /** + * Get the password. + * + * @return the password + */ + public String getPassword() { + return password; + } + + /** + * Set the password. + * + * @param password + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Get the database URL. + * + * @return the URL + */ + public String getUrl() { + return url; + } + + /** + * Set the database URL. + * Example: jdbc:postgresql:test + * + * @param url + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * Get the user name. + * + * @return the user name + */ + public String getUser() { + return user; + } + + /** + * Set the user name. + * + * @param user + */ + public void setUser(String user) { + this.user = user; + } + + /** + * @return whether the schema check is enabled + */ + public final boolean isSchemaCheckEnabled() { + return schemaCheckEnabled; + } + + /** + * @param enabled set whether the schema check is enabled + */ + public final void setSchemaCheckEnabled(boolean enabled) { + schemaCheckEnabled = enabled; + } + + public synchronized void close() throws DataStoreException { + // nothing to do + } + + protected void usesIdentifier(DataIdentifier identifier) { + inUse.put(identifier, new WeakReference(identifier)); + } + + public void clearInUse() { + inUse.clear(); + } + + protected synchronized MessageDigest getDigest() throws DataStoreException { + try { + return MessageDigest.getInstance(DIGEST); + } catch (NoSuchAlgorithmException e) { + throw convert("No such algorithm: " + DIGEST, e); + } + } + + /** + * Get the maximum number of concurrent connections. + * + * @deprecated + * @return the maximum number of connections. + */ + public int getMaxConnections() { + return -1; + } + + /** + * Set the maximum number of concurrent connections in the pool. + * At least 3 connections are required if the garbage collection process is used. + * + *@deprecated + * @param maxConnections the new value + */ + public void setMaxConnections(int maxConnections) { + // no effect + } + + /** + * Is a stream copied to a temporary file before returning? + * + * @return the setting + */ + public boolean getCopyWhenReading() { + return copyWhenReading; + } + + /** + * The the copy setting. If enabled, + * a stream is always copied to a temporary file when reading a stream. + * + * @param copyWhenReading the new setting + */ + public void setCopyWhenReading(boolean copyWhenReading) { + this.copyWhenReading = copyWhenReading; + } + + /** + * Get the table prefix. + * + * @return the table prefix. + */ + public String getTablePrefix() { + return tablePrefix; + } + + /** + * Set the new table prefix. The default is empty. + * The table name is constructed like this: + * ${tablePrefix}${schemaObjectPrefix}${tableName} + * + * @param tablePrefix the new value + */ + public void setTablePrefix(String tablePrefix) { + this.tablePrefix = tablePrefix; + } + + /** + * Get the schema prefix. + * + * @return the schema object prefix + */ + public String getSchemaObjectPrefix() { + return schemaObjectPrefix; + } + + /** + * Set the schema object prefix. The default is empty. + * The table name is constructed like this: + * ${tablePrefix}${schemaObjectPrefix}${tableName} + * + * @param schemaObjectPrefix the new prefix + */ + public void setSchemaObjectPrefix(String schemaObjectPrefix) { + this.schemaObjectPrefix = schemaObjectPrefix; + } + + public String getDataSourceName() { + return dataSourceName; + } + + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbInputStream.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbInputStream.java new file mode 100644 index 00000000000..3bc2f0c50b4 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbInputStream.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.core.data.db; + +import java.io.EOFException; +import java.io.IOException; +import java.sql.ResultSet; + +import org.apache.commons.io.input.AutoCloseInputStream; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.util.db.DbUtility; + +/** + * This class represents an input stream backed by a database. The database + * objects are only acquired when reading from the stream, and stay open until + * the stream is closed, fully read, or garbage collected. + *

    + * This class does not support mark/reset. It is always to be wrapped + * using a BufferedInputStream. + */ +public class DbInputStream extends AutoCloseInputStream { + + protected DbDataStore store; + protected DataIdentifier identifier; + protected boolean endOfStream; + + protected ResultSet rs; + + /** + * Create a database input stream for the given identifier. + * Database access is delayed until the first byte is read from the stream. + * + * @param store the database data store + * @param identifier the data identifier + */ + protected DbInputStream(DbDataStore store, DataIdentifier identifier) { + super(null); + this.store = store; + this.identifier = identifier; + } + + /** + * Open the stream if required. + * + * @throws IOException + */ + protected void openStream() throws IOException { + if (endOfStream) { + throw new EOFException(); + } + if (in == null) { + try { + in = store.openStream(this, identifier); + } catch (DataStoreException e) { + IOException e2 = new IOException(e.getMessage()); + e2.initCause(e); + throw e2; + } + } + } + + /** + * {@inheritDoc} + * When the stream is consumed, the database objects held by the instance are closed. + */ + public int read() throws IOException { + if (endOfStream) { + return -1; + } + openStream(); + int c = in.read(); + if (c == -1) { + endOfStream = true; + close(); + } + return c; + } + + /** + * {@inheritDoc} + * When the stream is consumed, the database objects held by the instance are closed. + */ + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + /** + * {@inheritDoc} + * When the stream is consumed, the database objects held by the instance are closed. + */ + public int read(byte[] b, int off, int len) throws IOException { + if (endOfStream) { + return -1; + } + openStream(); + int c = in.read(b, off, len); + if (c == -1) { + endOfStream = true; + close(); + } + return c; + } + + /** + * {@inheritDoc} + * When the stream is consumed, the database objects held by the instance are closed. + */ + public void close() throws IOException { + if (in != null) { + in.close(); + in = null; + // some additional database objects + // may need to be closed + if (rs != null) { + DbUtility.close(rs); + rs = null; + } + } + } + + /** + * {@inheritDoc} + */ + public long skip(long n) throws IOException { + if (endOfStream) { + return -1; + } + openStream(); + return in.skip(n); + } + + /** + * {@inheritDoc} + */ + public int available() throws IOException { + if (endOfStream) { + return 0; + } + openStream(); + return in.available(); + } + + /** + * This method does nothing. + */ + public void mark(int readlimit) { + // do nothing + } + + /** + * This method does nothing. + */ + public void reset() throws IOException { + // do nothing + } + + /** + * Check whether mark and reset are supported. + * + * @return false + */ + public boolean markSupported() { + return false; + } + + /** + * Set the result set of this input stream. This object must be closed once + * the stream is closed. + * + * @param rs the result set + */ + void setResultSet(ResultSet rs) { + this.rs = rs; + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DerbyDataStore.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DerbyDataStore.java new file mode 100644 index 00000000000..c1a8ff665da --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DerbyDataStore.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.core.data.db; + +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.util.db.ConnectionHelper; +import org.apache.jackrabbit.core.util.db.DerbyConnectionHelper; + +/** + * The Derby data store closes the database when the data store is closed + * (embedded databases only). + */ +public class DerbyDataStore extends DbDataStore { + + /** + * {@inheritDoc} + */ + @Override + protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception { + return new DerbyConnectionHelper(dataSrc, false); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void close() throws DataStoreException { + super.close(); + try { + ((DerbyConnectionHelper) conHelper).shutDown(getDriver()); + } catch (SQLException e) { + throw new DataStoreException(e); + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/ResettableTempFileInputStream.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/ResettableTempFileInputStream.java new file mode 100644 index 00000000000..203c6dd8b68 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/ResettableTempFileInputStream.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.core.data.db; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.channels.FileChannel; + +/** + * TempFileInputStream that can be reset in order to allow the + * ConnectionHelper to retry SQL execution in case of failure. + */ +public class ResettableTempFileInputStream extends TempFileInputStream { + + private final FileChannel fileChannel; + private long mark = 0; + + public ResettableTempFileInputStream(final File file) throws FileNotFoundException { + this(new FileInputStream(file), file); + } + + private ResettableTempFileInputStream(final FileInputStream in, final File file) { + super(in, file); + this.fileChannel = in.getChannel(); + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public synchronized void mark(int readlimit) { + try { + mark = fileChannel.position(); + } catch (IOException ex) { + mark = -1; + } + } + + @Override + public synchronized void reset() throws IOException { + if (mark == -1) { + throw new IOException("Mark failed"); + } + fileChannel.position(mark); + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/TempFileInputStream.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/TempFileInputStream.java new file mode 100644 index 00000000000..5419e2c4ca4 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/TempFileInputStream.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.core.data.db; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilterInputStream; +import java.io.IOException; + +import org.apache.commons.io.input.ClosedInputStream; + +/** + * An input stream from a temporary file. The file is deleted when the stream is + * closed or garbage collected. + */ +public class TempFileInputStream extends FilterInputStream { + + private final File file; + + public TempFileInputStream(File file) throws FileNotFoundException { + this(new FileInputStream(file), file); + } + + protected TempFileInputStream(FileInputStream in, File file) { + super(in); + this.file = file; + } + + @Override + public void close() throws IOException { + in.close(); + in = new ClosedInputStream(); + file.delete(); + } + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/package-info.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/package-info.java new file mode 100755 index 00000000000..26277ba6fb6 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.6") +package org.apache.jackrabbit.core.data.db; diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/package-info.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/package-info.java new file mode 100755 index 00000000000..bd387e0679d --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.14.0") +package org.apache.jackrabbit.core.data; diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/util/NamedThreadFactory.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/util/NamedThreadFactory.java new file mode 100644 index 00000000000..e176db72433 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/util/NamedThreadFactory.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.core.data.util; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class extends {@link ThreadFactory} to creates named threads. + */ +public class NamedThreadFactory implements ThreadFactory { + + private AtomicInteger threadCount = new AtomicInteger(1); + + String threadPrefixName; + + public NamedThreadFactory(String threadPrefixName) { + super(); + this.threadPrefixName = threadPrefixName; + } + + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setContextClassLoader(getClass().getClassLoader()); + thread.setName(threadPrefixName + "-" + threadCount.getAndIncrement()); + return thread; + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/util/package-info.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/util/package-info.java new file mode 100755 index 00000000000..e1d33779c4e --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/util/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.core.data.util; diff --git a/src/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java similarity index 81% rename from src/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java rename to jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java index e73a859fa19..bf3bbdf334c 100644 --- a/src/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -19,6 +19,7 @@ import java.io.InputStream; import java.io.OutputStream; + /** * A BasedFileSystem represents a 'file system in a file system'. */ @@ -58,7 +59,11 @@ public BasedFileSystem(FileSystem fsBase, String relRootPath) { protected String buildBasePath(String path) { if (path.startsWith(SEPARATOR)) { - return basePath + path; + if (path.length() == 1) { + return basePath; + } else { + return basePath + path; + } } else { return basePath + SEPARATOR + path; } @@ -82,13 +87,6 @@ public void close() throws FileSystemException { // do nothing; base file system should be closed explicitly } - /** - * {@inheritDoc} - */ - public void copy(String srcPath, String destPath) throws FileSystemException { - fsBase.copy(buildBasePath(srcPath), buildBasePath(destPath)); - } - /** * {@inheritDoc} */ @@ -131,14 +129,6 @@ public OutputStream getOutputStream(String filePath) throws FileSystemException return fsBase.getOutputStream(buildBasePath(filePath)); } - /** - * {@inheritDoc} - */ - public RandomAccessOutputStream getRandomAccessOutputStream(String filePath) - throws FileSystemException { - return fsBase.getRandomAccessOutputStream(buildBasePath(filePath)); - } - /** * {@inheritDoc} */ @@ -174,13 +164,6 @@ public long length(String filePath) throws FileSystemException { return fsBase.length(buildBasePath(filePath)); } - /** - * {@inheritDoc} - */ - public void touch(String filePath) throws FileSystemException { - fsBase.touch(buildBasePath(filePath)); - } - /** * {@inheritDoc} */ @@ -201,11 +184,4 @@ public String[] listFiles(String folderPath) throws FileSystemException { public String[] listFolders(String folderPath) throws FileSystemException { return fsBase.listFolders(buildBasePath(folderPath)); } - - /** - * {@inheritDoc} - */ - public void move(String srcPath, String destPath) throws FileSystemException { - fsBase.move(buildBasePath(srcPath), buildBasePath(destPath)); - } } diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystem.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystem.java new file mode 100644 index 00000000000..babc0f873b9 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystem.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.fs; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The FileSystem interface is an abstraction of a virtual + * file system. The similarities of its method names with with the methods + * of the java.io.File class are intentional. + *
    + * Implementations of this interface expose a file system-like resource. + * File system-like resources include WebDAV-enabled servers, local file systems, + * and so forth. + */ +public interface FileSystem { + + /** + * File separator + */ + String SEPARATOR = "/"; + + /** + * File separator character + */ + char SEPARATOR_CHAR = '/'; + + /** + * Initialize the file system + * + * @throws FileSystemException if the file system initialization fails + */ + void init() throws FileSystemException; + + /** + * Close the file system. After calling this method, the file system is no + * longer accessible. + * + * @throws FileSystemException + */ + void close() throws FileSystemException; + + /** + * Returns an input stream of the contents of the file denoted by this path. + * + * @param filePath the path of the file. + * @return an input stream of the contents of the file. + * @throws FileSystemException if the file does not exist + * or if it cannot be read from + */ + InputStream getInputStream(String filePath) throws FileSystemException; + + /** + * Returns an output stream for writing bytes to the file denoted by this path. + * The file will be created if it doesn't exist. If the file exists, its contents + * will be overwritten. + * + * @param filePath the path of the file. + * @return an output stream for writing bytes to the file. + * @throws FileSystemException if the file cannot be written to or created + */ + OutputStream getOutputStream(String filePath) throws FileSystemException; + + /** + * Creates the folder named by this path, including any necessary but + * nonexistent parent folders. Note that if this operation fails it + * may have succeeded in creating some of the necessary parent folders. + * + * @param folderPath the path of the folder to be created. + * @throws FileSystemException if a file system entry denoted by path + * already exists or if another error occurs. + */ + void createFolder(String folderPath) throws FileSystemException; + + /** + * Tests whether the file system entry denoted by this path exists. + * + * @param path the path of a file system entry. + * @return true if the file system entry at path exists; false otherwise. + * @throws FileSystemException + */ + boolean exists(String path) throws FileSystemException; + + /** + * Tests whether the file system entry denoted by this path exists and + * is a file. + * + * @param path the path of a file system entry. + * @return true if the file system entry at path is a file; false otherwise. + * @throws FileSystemException + */ + boolean isFile(String path) throws FileSystemException; + + /** + * Tests whether the file system entry denoted by this path exists and + * is a folder. + * + * @param path the path of a file system entry. + * @return true if the file system entry at path is a folder; false otherwise. + * @throws FileSystemException + */ + boolean isFolder(String path) throws FileSystemException; + + /** + * Tests whether the file system entry denoted by this path has child entries. + * + * @param path the path of a file system entry. + * @return true if the file system entry at path has child entries; false otherwise. + * @throws FileSystemException + */ + boolean hasChildren(String path) throws FileSystemException; + + /** + * Returns the length of the file denoted by this path. + * + * @param filePath the path of the file. + * @return The length, in bytes, of the file denoted by this path, + * or -1L if the length can't be determined. + * @throws FileSystemException if the path does not denote an existing file. + */ + long length(String filePath) throws FileSystemException; + + /** + * Returns the time that the file system entry denoted by this path + * was last modified. + * + * @param path the path of a file system entry. + * @return A long value representing the time the file system entry was + * last modified, measured in milliseconds since the epoch + * (00:00:00 GMT, January 1, 1970), or 0L if the modification + * time can't be determined. + * @throws FileSystemException if the file system entry does not exist. + */ + long lastModified(String path) throws FileSystemException; + + /** + * Returns an array of strings naming the files and folders + * in the folder denoted by this path. + * + * @param folderPath the path of the folder whose contents is to be listed. + * @return an array of strings naming the files and folders + * in the folder denoted by this path. + * @throws FileSystemException if this path does not denote a folder or if + * another error occurs. + */ + String[] list(String folderPath) throws FileSystemException; + + /** + * Returns an array of strings naming the files in the folder + * denoted by this path. + * + * @param folderPath the path of the folder whose contents is to be listed. + * @return an array of strings naming the files in the folder + * denoted by this path. + * @throws FileSystemException if this path does not denote a folder or if + * another error occurs. + */ + String[] listFiles(String folderPath) throws FileSystemException; + + /** + * Returns an array of strings naming the folders in the folder + * denoted by this path. + * + * @param folderPath the path of the folder whose contents is to be listed. + * @return an array of strings naming the folders in the folder + * denoted by this path. + * @throws FileSystemException if this path does not denote a folder or if + * another error occurs. + */ + String[] listFolders(String folderPath) throws FileSystemException; + + /** + * Deletes the file denoted by this path. + * + * @param filePath the path of the file to be deleted. + * @throws FileSystemException if this path does not denote a file or if + * another error occurs. + */ + void deleteFile(String filePath) throws FileSystemException; + + /** + * Deletes the folder denoted by this path. Any contents of this folder + * (folders and files) will be deleted recursively. + * + * @param folderPath the path of the folder to be deleted. + * @throws FileSystemException if this path does not denote a folder or if + * another error occurs. + */ + void deleteFolder(String folderPath) throws FileSystemException; + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemException.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemException.java new file mode 100644 index 00000000000..620eeeb9aa9 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemException.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.core.fs; + +/** + * The FileSystemException signals an error within a file system + * operation. FileSystemExceptions are thrown by {@link FileSystem} + * implementations. + */ +public class FileSystemException extends Exception { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public FileSystemException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public FileSystemException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemFactory.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemFactory.java new file mode 100644 index 00000000000..012ab7d5332 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemFactory.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.core.fs; + +import javax.jcr.RepositoryException; + + +/** + * Factory interface for creating {@link FileSystem} instances. Used + * to decouple the repository internals from the repository configuration + * mechanism. + */ +public interface FileSystemFactory { + + /** + * Creates, initializes, and returns a {@link FileSystem} instance + * for use by the repository. Note that no information is passed from + * the client, so all required configuration information must be + * encapsulated in the factory. + * + * @return initialized file system + * @throws RepositoryException if the file system can not be created + */ + FileSystem getFileSystem() throws RepositoryException; + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemPathUtil.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemPathUtil.java new file mode 100644 index 00000000000..af056fb37b9 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemPathUtil.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.core.fs; + +import java.io.ByteArrayOutputStream; +import java.util.BitSet; + + +/** + * Utility class for handling paths in a file system. + */ +public final class FileSystemPathUtil { + + /** + * Array of lowercase hexadecimal characters used in creating hex escapes. + */ + private static final char[] HEX_TABLE = "0123456789abcdef".toCharArray(); + + /** + * The escape character used to mark hex escape sequences. + */ + private static final char ESCAPE_CHAR = '%'; + + /** + * The list of characters that are not encoded by the escapeName(String) + * and unescape(String) methods. They contains the characters + * which can safely be used in file names: + */ + public static final BitSet SAFE_NAMECHARS; + + /** + * The list of characters that are not encoded by the escapePath(String) + * and unescape(String) methods. They contains the characters + * which can safely be used in file paths: + */ + public static final BitSet SAFE_PATHCHARS; + + static { + // build list of valid name characters + SAFE_NAMECHARS = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + SAFE_NAMECHARS.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + SAFE_NAMECHARS.set(i); + } + for (i = '0'; i <= '9'; i++) { + SAFE_NAMECHARS.set(i); + } + SAFE_NAMECHARS.set('-'); + SAFE_NAMECHARS.set('_'); + SAFE_NAMECHARS.set('.'); + + // build list of valid path characters (includes name characters) + SAFE_PATHCHARS = (BitSet) SAFE_NAMECHARS.clone(); + SAFE_PATHCHARS.set(FileSystem.SEPARATOR_CHAR); + } + + /** + * private constructor + */ + private FileSystemPathUtil() { + } + + /** + * Escapes the given string using URL encoding for all bytes not included + * in the given set of safe characters. + * + * @param s the string to escape + * @param safeChars set of safe characters (bytes) + * @return escaped string + */ + private static String escape(String s, BitSet safeChars) { + byte[] bytes = s.getBytes(); + StringBuilder out = new StringBuilder(bytes.length); + for (int i = 0; i < bytes.length; i++) { + int c = bytes[i] & 0xff; + if (safeChars.get(c) && c != ESCAPE_CHAR) { + out.append((char) c); + } else { + out.append(ESCAPE_CHAR); + out.append(HEX_TABLE[(c >> 4) & 0x0f]); + out.append(HEX_TABLE[(c) & 0x0f]); + } + } + return out.toString(); + } + + /** + * Encodes the specified path. Same as + * {@link #escapeName(String)} except that the separator + * character / is regarded as a legal path character + * that needs no escaping. + * + * @param path the path to encode. + * @return the escaped path + */ + public static String escapePath(String path) { + return escape(path, SAFE_PATHCHARS); + } + + /** + * Encodes the specified name. Same as + * {@link #escapePath(String)} except that the separator character + * / is regarded as an illegal character that needs + * escaping. + * + * @param name the name to encode. + * @return the escaped name + */ + public static String escapeName(String name) { + return escape(name, SAFE_NAMECHARS); + } + + /** + * Decodes the specified path/name. + * + * @param pathOrName the escaped path/name + * @return the unescaped path/name + */ + public static String unescape(String pathOrName) { + ByteArrayOutputStream out = new ByteArrayOutputStream(pathOrName.length()); + for (int i = 0; i < pathOrName.length(); i++) { + char c = pathOrName.charAt(i); + if (c == ESCAPE_CHAR) { + try { + out.write(Integer.parseInt(pathOrName.substring(i + 1, i + 3), 16)); + } catch (NumberFormatException e) { + IllegalArgumentException iae = new IllegalArgumentException("Failed to unescape escape sequence"); + iae.initCause(e); + throw iae; + } + i += 2; + } else { + out.write(c); + } + } + return new String(out.toByteArray()); + } + + /** + * Tests whether the specified path represents the root path, i.e. "/". + * + * @param path path to test + * @return true if the specified path represents the root path; false otherwise. + */ + public static boolean denotesRoot(String path) { + return path.equals(FileSystem.SEPARATOR); + } + + /** + * Checks if path is a valid path. + * + * @param path the path to be checked + * @throws FileSystemException If path is not a valid path + */ + public static void checkFormat(String path) throws FileSystemException { + if (path == null) { + throw new FileSystemException("null path"); + } + + // path must be absolute, i.e. starting with '/' + if (!path.startsWith(FileSystem.SEPARATOR)) { + throw new FileSystemException("not an absolute path: " + path); + } + + // trailing '/' is not allowed (except for root path) + if (path.endsWith(FileSystem.SEPARATOR) && path.length() > 1) { + throw new FileSystemException("malformed path: " + path); + } + + String[] names = path.split(FileSystem.SEPARATOR); + for (int i = 1; i < names.length; i++) { + // name must not be empty + if (names[i].length() == 0) { + throw new FileSystemException("empty name: " + path); + } + // leading/trailing whitespace is not allowed + String trimmed = names[i].trim(); + if (!trimmed.equals(names[i])) { + throw new FileSystemException("illegal leading or trailing whitespace in name: " + path); + } + } + } + + /** + * Returns the parent directory of the specified path. + * + * @param path a file system path denoting a directory or a file. + * @return the parent directory. + */ + public static String getParentDir(String path) { + int pos = path.lastIndexOf(FileSystem.SEPARATOR_CHAR); + if (pos > 0) { + return path.substring(0, pos); + } + return FileSystem.SEPARATOR; + } + + /** + * Returns the name of the specified path. + * + * @param path a file system path denoting a directory or a file. + * @return the name. + */ + public static String getName(String path) { + int pos = path.lastIndexOf(FileSystem.SEPARATOR_CHAR); + if (pos != -1) { + return path.substring(pos + 1); + } + return path; + } + +} diff --git a/src/java/org/apache/jackrabbit/core/fs/FileSystemResource.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemResource.java similarity index 83% rename from src/java/org/apache/jackrabbit/core/fs/FileSystemResource.java rename to jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemResource.java index ba1a46849f4..b429bf73feb 100644 --- a/src/java/org/apache/jackrabbit/core/fs/FileSystemResource.java +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemResource.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -20,6 +20,9 @@ import java.io.InputStream; import java.io.OutputStream; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.fs.FileSystemPathUtil; + /** * A FileSystemResource represents a resource (i.e. file) in a * FileSystem. @@ -30,6 +33,11 @@ public class FileSystemResource { protected final String path; + static { + // preload FileSystemPathUtil to prevent classloader issues during shutdown + FileSystemPathUtil.class.hashCode(); + } + /** * Creates a new FileSystemResource * @@ -99,7 +107,7 @@ public synchronized void makeParentDirs() throws FileSystemException { /** * Deletes this resource. - * Same as {@link #delete(false)}. + * Same as {@link #delete(boolean)} called with {@code false}. * * @see FileSystem#deleteFile */ @@ -154,16 +162,9 @@ public InputStream getInputStream() throws FileSystemException { public void spool(OutputStream out) throws FileSystemException, IOException { InputStream in = fs.getInputStream(path); try { - byte[] buffer = new byte[8192]; - int read = 0; - while ((read = in.read(buffer)) > 0) { - out.write(buffer, 0, read); - } + IOUtils.copy(in, out); } finally { - try { - in.close(); - } catch (IOException ioe) { - } + IOUtils.closeQuietly(in); } } @@ -174,14 +175,6 @@ public OutputStream getOutputStream() throws FileSystemException { return fs.getOutputStream(path); } - /** - * @see FileSystem#getRandomAccessOutputStream - */ - public RandomAccessOutputStream getRandomAccessOutputStream() - throws FileSystemException { - return fs.getRandomAccessOutputStream(path); - } - /** * @see FileSystem#lastModified */ @@ -196,20 +189,6 @@ public long length() throws FileSystemException { return fs.length(path); } - /** - * @see FileSystem#touch - */ - public void touch() throws FileSystemException { - fs.touch(path); - } - - /** - * @see FileSystem#move - */ - public void move(String destPath) throws FileSystemException { - fs.move(path, destPath); - } - //-------------------------------------------< java.lang.Object overrides > /** * Returns the path string of this resource. This is just the @@ -232,4 +211,16 @@ public boolean equals(Object obj) { } return false; } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + } diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/RandomAccessOutputStream.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/RandomAccessOutputStream.java new file mode 100644 index 00000000000..fd7c7bedd31 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/RandomAccessOutputStream.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.core.fs; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Extends the regular java.io.OutputStream with a random + * access facility. Multiple write() operations can be + * positioned off sequence with the {@link #seek} method. + * + * @deprecated this class should no longer be used + */ +public abstract class RandomAccessOutputStream extends OutputStream { + + /** + * Sets the current position in the resource where the next write + * will occur. + * + * @param position the new position in the resource. + * @throws IOException if an error occurs while seeking to the position. + */ + public abstract void seek(long position) throws IOException; +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/FileUtil.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/FileUtil.java new file mode 100644 index 00000000000..3fe4323198b --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/FileUtil.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.core.fs.local; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; + +/** + * Static utility methods for recursively copying and deleting files and + * directories. + */ +public final class FileUtil { + + /** + * private constructor + */ + private FileUtil() { + } + + /** + * Recursively copies the given file or directory to the + * given destination. + * + * @param src source file or directory + * @param dest destination file or directory + * @throws IOException if the file or directory cannot be copied + */ + public static void copy(File src, File dest) throws IOException { + if (!src.canRead()) { + throw new IOException(src.getPath() + " can't be read from."); + } + if (src.isDirectory()) { + // src is a folder + if (dest.isFile()) { + throw new IOException("can't copy a folder to a file"); + } + if (!dest.exists()) { + dest.mkdirs(); + } + if (!dest.canWrite()) { + throw new IOException("can't write to " + dest.getPath()); + } + File[] children = src.listFiles(); + for (int i = 0; i < children.length; i++) { + copy(children[i], new File(dest, children[i].getName())); + } + } else { + // src is a file + File destParent; + if (dest.isDirectory()) { + // dest is a folder + destParent = dest; + dest = new File(destParent, src.getName()); + } else { + destParent = dest.getParentFile(); + } + if (!destParent.canWrite()) { + throw new IOException("can't write to " + destParent.getPath()); + } + + FileUtils.copyFile(src, dest); + } + } + + /** + * Recursively deletes the given file or directory. + * + * @param f file or directory + * @throws IOException if the file or directory cannot be deleted + */ + public static void delete(File f) throws IOException { + if (f.isDirectory()) { + // it's a folder, list children first + File[] children = f.listFiles(); + for (int i = 0; i < children.length; i++) { + delete(children[i]); + } + } + if (!f.delete()) { + throw new IOException("Unable to delete " + f.getPath()); + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/HandleMonitor.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/HandleMonitor.java new file mode 100644 index 00000000000..c6a6577ab87 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/HandleMonitor.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.core.fs.local; + +import org.apache.jackrabbit.util.LazyFileInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.HashSet; + +/** + * This Class implements a very simple open handle monitor for the local + * file system. This is usefull, if the list of open handles, referenced by + * an open FileInputStream() should be tracked. This can cause problems on + * windows filesystems where open files cannot be deleted. + */ +public class HandleMonitor { + + /** + * The default logger + */ + private static Logger log = LoggerFactory.getLogger(HandleMonitor.class); + + /** + * the map of open handles (key=File, value=Handle) + */ + private HashMap openHandles = new HashMap(); + + /** + * Opens a file and returns an InputStream + * + * @param file + * @return + * @throws FileNotFoundException + */ + public InputStream open(File file) throws FileNotFoundException { + Handle handle = getHandle(file); + InputStream in = handle.open(); + return in; + } + + /** + * Checks, if the file is open + * @param file + * @return + */ + public boolean isOpen(File file) { + return openHandles.containsKey(file); + } + + /** + * Closes a file + * @param file + */ + private void close(File file) { + openHandles.remove(file); + } + + /** + * Returns the handle for a file. + * @param file + * @return + */ + private Handle getHandle(File file) { + Handle handle = openHandles.get(file); + if (handle == null) { + handle = new Handle(file); + openHandles.put(file, handle); + } + return handle; + } + + /** + * Dumps the contents of this monitor + */ + public void dump() { + log.info("Number of open files: " + openHandles.size()); + for (File file : openHandles.keySet()) { + Handle handle = openHandles.get(file); + handle.dump(); + } + } + + /** + * Dumps the information for a file + * @param file + */ + public void dump(File file) { + Handle handle = openHandles.get(file); + if (handle != null) { + handle.dump(true); + } + } + + /** + * Class representing all open handles to a file + */ + private class Handle { + + /** + * the file of this handle + */ + private File file; + + /** + * all open streams of this handle + */ + private HashSet streams = new HashSet(); + + /** + * Creates a new handle for a file + * @param file + */ + private Handle(File file) { + this.file = file; + } + + /** + * opens a stream for this handle + * @return + * @throws FileNotFoundException + */ + private InputStream open() throws FileNotFoundException { + Handle.MonitoredInputStream in = new Handle.MonitoredInputStream(file); + streams.add(in); + return in; + } + + /** + * Closes a stream + * @param in + */ + private void close(MonitoredInputStream in) { + streams.remove(in); + if (streams.isEmpty()) { + HandleMonitor.this.close(file); + } + } + + /** + * Dumps this handle + */ + private void dump() { + dump(false); + } + + /** + * Dumps this handle + */ + private void dump(boolean detailed) { + if (detailed) { + log.info("- " + file.getPath() + ", " + streams.size()); + for (Handle.MonitoredInputStream in : streams) { + in.dump(); + } + } else { + log.info("- " + file.getPath() + ", " + streams.size()); + } + } + + /** + * Delegating input stream that registers/unregisters itself from the + * handle. + */ + private class MonitoredInputStream extends LazyFileInputStream { + + /** + * throwable of the time, the stream was created + */ + private final Throwable throwable = new Exception(); + + /** + * {@inheritDoc} + */ + private MonitoredInputStream(File file) throws FileNotFoundException { + super(file); + } + + /** + * dumps this stream + */ + private void dump() { + log.info("- opened by : ", throwable); + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + // remove myself from the set + Handle.this.close(this); + super.close(); + } + + } + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/LocalFileSystem.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/LocalFileSystem.java new file mode 100644 index 00000000000..d5d32aeb758 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/LocalFileSystem.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.fs.local; + +import org.apache.jackrabbit.core.fs.FileSystem; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.apache.jackrabbit.core.fs.local.FileUtil; +import org.apache.jackrabbit.core.fs.local.HandleMonitor; +import org.apache.jackrabbit.util.LazyFileInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A LocalFileSystem ... + */ +public class LocalFileSystem implements FileSystem { + + private static Logger log = LoggerFactory.getLogger(LocalFileSystem.class); + + private File root; + + private HandleMonitor monitor; + + /** + * Default constructor + */ + public LocalFileSystem() { + } + + public String getPath() { + if (root != null) { + return root.getPath(); + } else { + return null; + } + } + + /** + * Sets the path to the root directory of this local filesystem. please note + * that this method can be called via reflection during initialization and + * must not be altered. + * + * @param rootPath the path to the root directory + */ + public void setPath(String rootPath) { + setRoot(new File(osPath(rootPath))); + } + + public void setRoot(File root) { + this.root = root; + } + + /** + * Enables/Disables the use of the handle monitor. + * + * @param enable + */ + public void setEnableHandleMonitor(String enable) { + setEnableHandleMonitor(Boolean.valueOf(enable).booleanValue()); + } + + /** + * Enables/Disables the use of the handle monitor. + * + * @param enable flag + */ + public void setEnableHandleMonitor(boolean enable) { + if (enable && monitor == null) { + monitor = new HandleMonitor(); + } + if (!enable && monitor != null) { + monitor = null; + } + } + + /** + * Returns true if use of the handle monitor is currently + * enabled, otherwise returns false. + * + * @see #setEnableHandleMonitor(boolean) + */ + public String getEnableHandleMonitor() { + return monitor == null ? "false" : "true"; + } + + private String osPath(String genericPath) { + if (File.separator.equals(SEPARATOR)) { + return genericPath; + } + return genericPath.replace(SEPARATOR_CHAR, File.separatorChar); + } + + //-------------------------------------------< java.lang.Object overrides > + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof LocalFileSystem) { + LocalFileSystem other = (LocalFileSystem) obj; + if (root == null) { + return other.root == null; + } else { + return root.equals(other.root); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //-----------------------------------------------------------< FileSystem > + /** + * {@inheritDoc} + */ + public void init() throws FileSystemException { + if (root == null) { + String msg = "root directory not set"; + log.debug(msg); + throw new FileSystemException(msg); + } + + if (root.exists()) { + if (!root.isDirectory()) { + String msg = "path does not denote a folder"; + log.debug(msg); + throw new FileSystemException(msg); + } + } else { + if (!root.mkdirs()) { + String msg = "failed to create root"; + log.debug(msg); + throw new FileSystemException(msg); + } + } + log.info("LocalFileSystem initialized at path " + root.getPath()); + if (monitor != null) { + log.info("LocalFileSystem using handle monitor"); + } + } + + /** + * {@inheritDoc} + */ + public void close() throws FileSystemException { + root = null; + } + + /** + * {@inheritDoc} + */ + public void createFolder(String folderPath) throws FileSystemException { + File f = new File(root, osPath(folderPath)); + if (f.exists()) { + String msg = f.getPath() + " already exists"; + log.debug(msg); + throw new FileSystemException(msg); + } + if (!f.mkdirs()) { + String msg = "failed to create folder " + f.getPath(); + log.debug(msg); + throw new FileSystemException(msg); + } + } + + /** + * {@inheritDoc} + */ + public void deleteFile(String filePath) throws FileSystemException { + File f = new File(root, osPath(filePath)); + if (!f.isFile()) { + String msg = f.getPath() + " does not denote an existing file"; + throw new FileSystemException(msg); + } + try { + FileUtil.delete(f); + } catch (IOException ioe) { + String msg = "failed to delete " + f.getPath(); + if (monitor != null && monitor.isOpen(f)) { + log.error("Unable to delete. There are still open streams."); + monitor.dump(f); + } + + throw new FileSystemException(msg, ioe); + } + } + + /** + * {@inheritDoc} + */ + public void deleteFolder(String folderPath) throws FileSystemException { + File f = new File(root, osPath(folderPath)); + if (!f.isDirectory()) { + String msg = f.getPath() + " does not denote an existing folder"; + log.debug(msg); + throw new FileSystemException(msg); + } + try { + FileUtil.delete(f); + } catch (IOException ioe) { + String msg = "failed to delete " + f.getPath(); + log.debug(msg); + throw new FileSystemException(msg, ioe); + } + } + + /** + * {@inheritDoc} + */ + public boolean exists(String path) throws FileSystemException { + File f = new File(root, osPath(path)); + return f.exists(); + } + + /** + * {@inheritDoc} + */ + public InputStream getInputStream(String filePath) + throws FileSystemException { + File f = new File(root, osPath(filePath)); + try { + if (monitor == null) { + return new LazyFileInputStream(f); + } else { + return monitor.open(f); + } + } catch (FileNotFoundException fnfe) { + String msg = f.getPath() + " does not denote an existing file"; + log.debug(msg); + throw new FileSystemException(msg, fnfe); + } + } + + /** + * {@inheritDoc} + */ + public OutputStream getOutputStream(String filePath) + throws FileSystemException { + File f = new File(root, osPath(filePath)); + try { + return new FileOutputStream(f); + } catch (FileNotFoundException fnfe) { + String msg = "failed to get output stream for " + f.getPath(); + log.debug(msg); + throw new FileSystemException(msg, fnfe); + } + } + + /** + * {@inheritDoc} + */ + public boolean hasChildren(String path) throws FileSystemException { + File f = new File(root, osPath(path)); + if (!f.exists()) { + String msg = f.getPath() + " does not exist"; + log.debug(msg); + throw new FileSystemException(msg); + } + if (f.isFile()) { + return false; + } + return (f.list().length > 0); + } + + /** + * {@inheritDoc} + */ + public boolean isFile(String path) throws FileSystemException { + File f = new File(root, osPath(path)); + return f.isFile(); + } + + /** + * {@inheritDoc} + */ + public boolean isFolder(String path) throws FileSystemException { + File f = new File(root, osPath(path)); + return f.isDirectory(); + } + + /** + * {@inheritDoc} + */ + public long lastModified(String path) throws FileSystemException { + File f = new File(root, osPath(path)); + return f.lastModified(); + } + + /** + * {@inheritDoc} + */ + public long length(String filePath) throws FileSystemException { + File f = new File(root, osPath(filePath)); + if (!f.exists()) { + return -1; + } + return f.length(); + } + + /** + * {@inheritDoc} + */ + public String[] list(String folderPath) throws FileSystemException { + File f = new File(root, osPath(folderPath)); + String[] entries = f.list(); + if (entries == null) { + String msg = folderPath + " does not denote a folder"; + log.debug(msg); + throw new FileSystemException(msg); + } + return entries; + } + + /** + * {@inheritDoc} + */ + public String[] listFiles(String folderPath) throws FileSystemException { + File folder = new File(root, osPath(folderPath)); + File[] files = folder.listFiles(new FileFilter() { + public boolean accept(File f) { + return f.isFile(); + } + }); + if (files == null) { + String msg = folderPath + " does not denote a folder"; + log.debug(msg); + throw new FileSystemException(msg); + } + String[] entries = new String[files.length]; + for (int i = 0; i < files.length; i++) { + entries[i] = files[i].getName(); + } + return entries; + } + + /** + * {@inheritDoc} + */ + public String[] listFolders(String folderPath) throws FileSystemException { + File file = new File(root, osPath(folderPath)); + File[] folders = file.listFiles(new FileFilter() { + public boolean accept(File f) { + return f.isDirectory(); + } + }); + if (folders == null) { + String msg = folderPath + " does not denote a folder"; + log.debug(msg); + throw new FileSystemException(msg); + } + String[] entries = new String[folders.length]; + for (int i = 0; i < folders.length; i++) { + entries[i] = folders[i].getName(); + } + return entries; + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/package-info.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/package-info.java new file mode 100755 index 00000000000..ba4a9b106c7 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/local/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.core.fs.local; diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/package-info.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/package-info.java new file mode 100755 index 00000000000..15a8337e03b --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.core.fs; diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/CheckSchemaOperation.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/CheckSchemaOperation.java new file mode 100644 index 00000000000..ab69ec6116b --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/CheckSchemaOperation.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.core.util.db; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.util.Text; + +/** + * An operation which synchronously checks the DB schema in the {@link #run()} method. The + * {@link #addVariableReplacement(String, String)} method return the instance to enable method chaining. + */ +public class CheckSchemaOperation { + + public static final String SCHEMA_OBJECT_PREFIX_VARIABLE = "${schemaObjectPrefix}"; + + public static final String TABLE_SPACE_VARIABLE = "${tableSpace}"; + + private final ConnectionHelper conHelper; + + private final InputStream ddl; + + private final String table; + + private final Map varReplacement = new HashMap(); + + /** + * @param connectionhelper the connection helper + * @param ddlStream the stream of the DDL to use to create the schema if necessary (closed by the + * {@link #run()} method) + * @param tableName the name of the table to use for the schema-existence-check + */ + public CheckSchemaOperation(ConnectionHelper connectionhelper, InputStream ddlStream, String tableName) { + conHelper = connectionhelper; + ddl = ddlStream; + table = tableName; + } + + /** + * Adds a variable replacement mapping. + * + * @param var the variable + * @param replacement the replacement value + * @return this + */ + public CheckSchemaOperation addVariableReplacement(String var, String replacement) { + varReplacement.put(var, replacement); + return this; + } + + /** + * Checks if the required schema objects exist and creates them if they don't exist yet. + * + * @throws SQLException if an error occurs + * @throws IOException if an error occurs + */ + public void run() throws SQLException, IOException { + try { + if (!conHelper.tableExists(table)) { + BufferedReader reader = new BufferedReader(new InputStreamReader(ddl)); + String sql = reader.readLine(); + while (sql != null) { + // Skip comments and empty lines + if (!sql.startsWith("#") && sql.length() > 0) { + // replace prefix variable + sql = replace(sql); + // execute sql stmt + conHelper.exec(sql); + } + // read next sql stmt + sql = reader.readLine(); + } + } + } finally { + IOUtils.closeQuietly(ddl); + } + } + + /** + * Applies the variable replacement to the given string. + * + * @param sql the string in which to replace variables + * @return the new string + */ + private String replace(String sql) { + String result = sql; + for (Map.Entry entry : varReplacement.entrySet()) { + result = Text.replace(result, entry.getKey(), entry.getValue()).trim(); + } + return result; + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ConnectionFactory.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ConnectionFactory.java new file mode 100644 index 00000000000..c0830c3fe31 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ConnectionFactory.java @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.util.db; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.sql.DataSource; + +import org.apache.commons.dbcp.BasicDataSource; +import org.apache.commons.dbcp.DelegatingConnection; +import org.apache.commons.pool.impl.GenericObjectPool; +import org.apache.jackrabbit.core.config.DataSourceConfig; +import org.apache.jackrabbit.core.config.DataSourceConfig.DataSourceDefinition; +import org.apache.jackrabbit.core.util.db.DataSourceWrapper; +import org.apache.jackrabbit.util.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory for new database connections. + * Supported are regular JDBC drivers, as well as + * JNDI resources. + * + * FIXME: the registry currently is ClassLoader wide. I.e., if you start two repositories + * then you share the registered datasources... + */ +public final class ConnectionFactory { + + private static final Logger log = LoggerFactory.getLogger(ConnectionFactory.class); + + /** + * The lock to protect the fields of this class. + */ + private final Object lock = new Object(); + + /** + * The data sources without logical name. The keys in the map are based on driver-url-user combination. + */ + private final Map keyToDataSource = new HashMap(); + + /** + * The configured data sources with logical name. The keys in the map are the logical name. + */ + private final Map nameToDataSource = new HashMap(); + + /** + * The configured data source defs. The keys in the map are the logical name. + */ + private final Map nameToDataSourceDef = new HashMap(); + + /** + * The list of data sources created by this factory. + */ + private final List created = new ArrayList(); + + private boolean closed = false; + + /** + * Registers a number of data sources. + * + * @param dsc the {@link DataSourceConfig} which contains the configuration + */ + public void registerDataSources(DataSourceConfig dsc) throws RepositoryException { + synchronized (lock) { + sanityCheck(); + for (DataSourceDefinition def : dsc.getDefinitions()) { + Class driverClass = getDriverClass(def.getDriver()); + if (driverClass != null + && Context.class.isAssignableFrom(driverClass)) { + DataSource ds = getJndiDataSource((Class) driverClass, def.getUrl()); + nameToDataSource.put(def.getLogicalName(), ds); + nameToDataSourceDef.put(def.getLogicalName(), def); + } else { + BasicDataSource bds = + getDriverDataSource(driverClass, def.getUrl(), def.getUser(), def.getPassword()); + if (def.getMaxPoolSize() > 0) { + bds.setMaxActive(def.getMaxPoolSize()); + } + if (def.getValidationQuery() != null && !"".equals(def.getValidationQuery().trim())) { + bds.setValidationQuery(def.getValidationQuery()); + } + nameToDataSource.put(def.getLogicalName(), bds); + nameToDataSourceDef.put(def.getLogicalName(), def); + } + } + } + } + + /** + * Retrieves a configured data source by logical name. + * + * @param logicalName the name of the {@code DataSource} + * @return a {@code DataSource} + * @throws RepositoryException if there is no {@code DataSource} with the given name + */ + public DataSource getDataSource(String logicalName) throws RepositoryException { + synchronized (lock) { + sanityCheck(); + DataSource ds = nameToDataSource.get(logicalName); + if (ds == null) { + throw new RepositoryException("DataSource with logicalName " + logicalName + + " has not been configured"); + } + return ds; + } + } + + /** + * @param logicalName the name of the {@code DataSource} + * @return the configured database type + * @throws RepositoryException if there is no {@code DataSource} with the given name + */ + public String getDataBaseType(String logicalName) throws RepositoryException { + synchronized (lock) { + sanityCheck(); + DataSourceDefinition def = nameToDataSourceDef.get(logicalName); + if (def == null) { + throw new RepositoryException("DataSource with logicalName " + logicalName + + " has not been configured"); + } + return def.getDbType(); + } + } + + /** + * Retrieve a {@code DataSource} for the specified properties. + * This can be a JNDI Data Source as well. To do that, + * the driver class name must reference a {@code javax.naming.Context} class + * (for example {@code javax.naming.InitialContext}), and the URL must be the JNDI URL + * (for example {@code java:comp/env/jdbc/Test}). + * + * @param driver the JDBC driver or the Context class + * @param url the database URL + * @param user the user name + * @param password the password + * @return the {@code DataSource} + * @throws RepositoryException if the driver could not be loaded + * @throws SQLException if the connection could not be established + */ + public DataSource getDataSource(String driver, String url, String user, String password) + throws RepositoryException, SQLException { + final String key = driver + url + user; + synchronized(lock) { + sanityCheck(); + DataSource ds = keyToDataSource.get(key); + if (ds == null) { + ds = createDataSource( + driver, url, user, Base64.decodeIfEncoded(password)); + keyToDataSource.put(key, ds); + } + return ds; + } + } + + /** + * + */ + public void close() { + synchronized(lock) { + sanityCheck(); + for (BasicDataSource ds : created) { + try { + ds.close(); + } catch (SQLException e) { + log.error("failed to close " + ds, e); + } + } + keyToDataSource.clear(); + nameToDataSource.clear(); + nameToDataSourceDef.clear(); + created.clear(); + closed = true; + } + } + + /** + * Needed for pre-10R2 Oracle blob support....:( + * + * This method actually assumes that we are using commons DBCP 1.2.2. + * + * @param con the commons-DBCP {@code DelegatingConnection} to unwrap + * @return the unwrapped connection + */ + public static Connection unwrap(Connection con) throws SQLException { + if (con instanceof DelegatingConnection) { + return ((DelegatingConnection)con).getInnermostDelegate(); + } else { + throw new SQLException("failed to unwrap connection of class " + con.getClass().getName() + + ", expected it to be a " + DelegatingConnection.class.getName()); + } + } + + private void sanityCheck() { + if (closed) { + throw new IllegalStateException("this factory has already been closed"); + } + } + + /** + * Create a new pooling data source or finds an existing JNDI data source (depends on driver). + * + * @param driver + * @param url + * @param user + * @param password + * @return + * @throws RepositoryException + */ + private DataSource createDataSource(String driver, String url, String user, String password) + throws RepositoryException { + Class driverClass = getDriverClass(driver); + if (driverClass != null + && Context.class.isAssignableFrom(driverClass)) { + @SuppressWarnings("unchecked") + DataSource database = getJndiDataSource((Class) driverClass, url); + if (user == null && password == null) { + return database; + } else { + return new DataSourceWrapper(database, user, password); + } + } else { + return getDriverDataSource(driverClass, url, user, password); + } + } + + /** + * Loads and returns the given JDBC driver (or JNDI context) class. + * Returns null if a class name is not given. + * + * @param driver driver class name + * @return driver class, or null + * @throws RepositoryException if the class can not be loaded + */ + private Class getDriverClass(String driver) + throws RepositoryException { + try { + if (driver != null && driver.length() > 0) { + return Class.forName(driver); + } else { + return null; + } + } catch (ClassNotFoundException e) { + throw new RepositoryException( + "Could not load JDBC driver class " + driver, e); + } + } + + /** + * Returns the JDBC {@link DataSource} bound to the given name in + * the JNDI {@link Context} identified by the given class. + * + * @param contextClass class that is instantiated to get the JNDI context + * @param name name of the DataSource within the JNDI context + * @return the DataSource bound in JNDI + * @throws RepositoryException if the JNDI context can not be accessed, + * or if the named DataSource is not found + */ + private DataSource getJndiDataSource( + Class contextClass, String name) + throws RepositoryException { + try { + Object object = contextClass.newInstance().lookup(name); + if (object instanceof DataSource) { + return (DataSource) object; + } else { + throw new RepositoryException( + "Object " + object + " with JNDI name " + + name + " is not a JDBC DataSource"); + } + } catch (InstantiationException e) { + throw new RepositoryException( + "Invalid JNDI context: " + contextClass.getName(), e); + } catch (IllegalAccessException e) { + throw new RepositoryException( + "Invalid JNDI context: " + contextClass.getName(), e); + } catch (NamingException e) { + throw new RepositoryException( + "JNDI name not found: " + name, e); + } + } + + /** + * Creates and returns a pooling JDBC {@link DataSource} for accessing + * the database identified by the given driver class and JDBC + * connection URL. The driver class can be null if + * a specific driver has not been configured. + * + * @param driverClass the JDBC driver class, or null + * @param url the JDBC connection URL + * @return pooling DataSource for accessing the specified database + */ + private BasicDataSource getDriverDataSource( + Class driverClass, String url, String user, String password) { + BasicDataSource ds = new BasicDataSource(); + created.add(ds); + + if (driverClass != null) { + Driver instance = null; + try { + // Workaround for Apache Derby: + // The JDBC specification recommends the Class.forName + // method without the .newInstance() method call, + // but it is required after a Derby 'shutdown' + instance = (Driver) driverClass.newInstance(); + } catch (Throwable e) { + // Ignore exceptions as there's no requirement for + // a JDBC driver class to have a public default constructor + } + if (instance != null) { + if (instance.jdbcCompliant()) { + // JCR-3445 At the moment the PostgreSQL isn't compliant because it doesn't implement this method... + ds.setValidationQueryTimeout(3); + } + } + ds.setDriverClassName(driverClass.getName()); + } + + ds.setUrl(url); + ds.setUsername(user); + ds.setPassword(password); + ds.setDefaultAutoCommit(true); + ds.setTestOnBorrow(false); + ds.setTestWhileIdle(true); + ds.setTimeBetweenEvictionRunsMillis(600000); // 10 Minutes + ds.setMinEvictableIdleTimeMillis(60000); // 1 Minute + ds.setMaxActive(-1); // unlimited + ds.setMaxIdle(GenericObjectPool.DEFAULT_MAX_IDLE + 10); + ds.setValidationQuery(guessValidationQuery(url)); + ds.setAccessToUnderlyingConnectionAllowed(true); + ds.setPoolPreparedStatements(true); + ds.setMaxOpenPreparedStatements(-1); // unlimited + return ds; + } + + private String guessValidationQuery(String url) { + if (url.contains("derby")) { + return "values(1)"; + } else if (url.contains("mysql")) { + return "select 1"; + } else if (url.contains("sqlserver") || url.contains("jtds")) { + return "select 1"; + } else if (url.contains("oracle")) { + return "select 'validationQuery' from dual"; + } else if (url.contains("postgresql")) { + return "select 1"; + } else if (url.contains("h2")) { + return "select 1"; + } else if (url.contains("db2")) { + return "values(1)"; + } + log.warn("Failed to guess validation query for URL " + url); + return null; + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ConnectionHelper.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ConnectionHelper.java new file mode 100644 index 00000000000..4610e81d7cd --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ConnectionHelper.java @@ -0,0 +1,608 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.util.db; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.apache.jackrabbit.data.core.TransactionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class provides convenience methods to execute SQL statements. They can be either executed in isolation + * or within the context of a JDBC transaction; the so-called batch mode (use the {@link #startBatch()} + * and {@link #endBatch(boolean)} methods for this). + * + *

    + * + * This class contains logic to retry execution of SQL statements. If this helper is not in batch mode + * and if a statement fails due to an {@code SQLException}, then it is retried. If the {@code block} argument + * of the constructor call was {@code false} then it is retried only once. Otherwise the statement is retried + * until either it succeeds or the thread is interrupted. This clearly assumes that the only cause of {@code + * SQLExceptions} is faulty {@code Connections} which are restored eventually.
    Note: + * This retry logic only applies to the following methods: + *

      + *
    • {@link #exec(String, Object...)}
    • + *
    • {@link #update(String, Object[])}
    • + *
    • {@link #exec(String, Object[], boolean, int)}
    • + *
    + * + *

    + * + * This class is not thread-safe and if it is to be used by multiple threads then the clients must make sure + * that access to this class is properly synchronized. + * + *

    + * + * Implementation note: The {@code Connection} that is retrieved from the {@code DataSource} + * in {@link #getConnection(boolean)} may be broken. This is so because if an internal {@code DataSource} is used, + * then this is a commons-dbcp {@code DataSource} with a testWhileIdle validation strategy (see + * the {@code ConnectionFactory} class). Furthermore, if it is a {@code DataSource} obtained through JNDI then we + * can make no assumptions about the validation strategy. This means that our retry logic must either assume that + * the SQL it tries to execute can do so without errors (i.e., the statement is valid), or it must implement its + * own validation strategy to apply. Currently, the former is in place. + */ +public class ConnectionHelper { + + static Logger log = LoggerFactory.getLogger(ConnectionHelper.class); + + private static final int RETRIES = 1; + + private static final int SLEEP_BETWEEN_RETRIES_MS = 100; + + final boolean blockOnConnectionLoss; + + private final boolean checkTablesWithUserName; + + protected final DataSource dataSource; + + private Map batchConnectionMap = Collections.synchronizedMap(new HashMap()); + + /** + * The default fetchSize is '0'. This means the fetchSize Hint will be ignored + */ + private int fetchSize = 0; + + /** + * @param dataSrc the {@link DataSource} on which this instance acts + * @param block whether the helper should transparently block on DB connection loss (otherwise it retries + * once and if that fails throws exception) + */ + public ConnectionHelper(DataSource dataSrc, boolean block) { + dataSource = dataSrc; + checkTablesWithUserName = false; + blockOnConnectionLoss = block; + } + + /** + * @param dataSrc the {@link DataSource} on which this instance acts + * @param checkWithUserName whether the username is to be used for the {@link #tableExists(String)} method + * @param block whether the helper should transparently block on DB connection loss (otherwise it throws exceptions) + */ + protected ConnectionHelper(DataSource dataSrc, boolean checkWithUserName, boolean block) { + dataSource = dataSrc; + checkTablesWithUserName = checkWithUserName; + blockOnConnectionLoss = block; + } + + /** + * @param dataSrc the {@link DataSource} on which this instance acts + * @param checkWithUserName whether the username is to be used for the {@link #tableExists(String)} method + * @param block whether the helper should transparently block on DB connection loss (otherwise it throws exceptions) + * @param fetchSize the fetchSize that will be used per default + */ + protected ConnectionHelper(DataSource dataSrc, boolean checkWithUserName, boolean block, int fetchSize) { + dataSource = dataSrc; + checkTablesWithUserName = checkWithUserName; + blockOnConnectionLoss = block; + this.fetchSize = fetchSize; + } + + /** + * A utility method that makes sure that identifier does only consist of characters that are + * allowed in names on the target database. Illegal characters will be escaped as necessary. + * + * This method is not affected by the + * + * @param identifier the identifier to convert to a db specific identifier + * @return the db-normalized form of the given identifier + * @throws SQLException if an error occurs + */ + public final String prepareDbIdentifier(String identifier) throws SQLException { + if (identifier == null) { + return null; + } + String legalChars = "ABCDEFGHIJKLMNOPQRSTUVWXZY0123456789_"; + legalChars += getExtraNameCharacters(); + String id = identifier.toUpperCase(); + StringBuilder escaped = new StringBuilder(); + for (int i = 0; i < id.length(); i++) { + char c = id.charAt(i); + if (legalChars.indexOf(c) == -1) { + replaceCharacter(escaped, c); + } else { + escaped.append(c); + } + } + return escaped.toString(); + } + + /** + * Called from {@link #prepareDbIdentifier(String)}. Default implementation replaces the illegal + * characters with their hexadecimal encoding. + * + * @param escaped the escaped db identifier + * @param c the character to replace + */ + protected void replaceCharacter(StringBuilder escaped, char c) { + escaped.append("_x"); + String hex = Integer.toHexString(c); + escaped.append("0000".toCharArray(), 0, 4 - hex.length()); + escaped.append(hex); + escaped.append("_"); + } + + /** + * Returns true if we are currently in a batch mode, false otherwise. + * + * @return true if the current thread or the active transaction is running in batch mode, false otherwise. + */ + protected boolean inBatchMode() { + return getTransactionAwareBatchConnection() != null; + } + + /** + * The default implementation returns the {@code extraNameCharacters} provided by the databases metadata. + * + * @return the additional characters for identifiers supported by the db + * @throws SQLException on error + */ + private String getExtraNameCharacters() throws SQLException { + Connection con = dataSource.getConnection(); + try { + DatabaseMetaData metaData = con.getMetaData(); + return metaData.getExtraNameCharacters(); + } finally { + DbUtility.close(con, null, null); + } + } + + /** + * Checks whether the given table exists in the database. + * + * @param tableName the name of the table + * @return whether the given table exists + * @throws SQLException on error + */ + public final boolean tableExists(String tableName) throws SQLException { + Connection con = dataSource.getConnection(); + ResultSet rs = null; + boolean schemaExists = false; + String name = tableName; + try { + DatabaseMetaData metaData = con.getMetaData(); + if (metaData.storesLowerCaseIdentifiers()) { + name = tableName.toLowerCase(); + } else if (metaData.storesUpperCaseIdentifiers()) { + name = tableName.toUpperCase(); + } + String userName = null; + if (checkTablesWithUserName) { + userName = metaData.getUserName(); + } + rs = metaData.getTables(null, userName, name, null); + schemaExists = rs.next(); + } finally { + DbUtility.close(con, null, rs); + } + return schemaExists; + } + + /** + * Starts the batch mode. If an {@link SQLException} is thrown, then the batch mode is not started. + *

    + * Important: clients that call this method must make sure that + * {@link #endBatch(boolean)} is called eventually. + * + * @throws SQLException on error + */ + public final void startBatch() throws SQLException { + if (inBatchMode()) { + throw new SQLException("already in batch mode"); + } + Connection batchConnection = null; + try { + batchConnection = getConnection(false); + batchConnection.setAutoCommit(false); + setTransactionAwareBatchConnection(batchConnection); + } catch (SQLException e) { + removeTransactionAwareBatchConnection(); + // Strive for failure atomicity + if (batchConnection != null) { + DbUtility.close(batchConnection, null, null); + } + throw e; + } + } + + /** + * This method always ends the batch mode. + * + * @param commit whether the changes in the batch should be committed or rolled back + * @throws SQLException if the commit or rollback of the underlying JDBC Connection threw an {@code + * SQLException} + */ + public final void endBatch(boolean commit) throws SQLException { + if (!inBatchMode()) { + throw new SQLException("not in batch mode"); + } + Connection batchConnection = getTransactionAwareBatchConnection(); + try { + if (commit) { + batchConnection.commit(); + } else { + batchConnection.rollback(); + } + } finally { + removeTransactionAwareBatchConnection(); + if (batchConnection != null) { + DbUtility.close(batchConnection, null, null); + } + } + } + + /** + * Executes a general SQL statement and immediately closes all resources. + * + * Note: We use a Statement if there are no parameters to avoid a problem on + * the Oracle 10g JDBC driver w.r.t. :NEW and :OLD keywords that triggers ORA-17041. + * + * @param sql an SQL statement string + * @param params the parameters for the SQL statement + * @throws SQLException on error + */ + public final void exec(final String sql, final Object... params) throws SQLException { + new RetryManager(params) { + + @Override + protected Void call() throws SQLException { + reallyExec(sql, params); + return null; + } + + }.doTry(); + } + + void reallyExec(String sql, Object... params) throws SQLException { + Connection con = null; + Statement stmt = null; + boolean inBatchMode = inBatchMode(); + long start = System.currentTimeMillis(); + try { + con = getConnection(inBatchMode); + if (params == null || params.length == 0) { + stmt = con.createStatement(); + stmt.execute(sql); + } else { + PreparedStatement p = con.prepareStatement(sql); + stmt = p; + execute(p, params); + } + } finally { + closeResources(con, stmt, null, inBatchMode); + long duration = System.currentTimeMillis() - start; + log.debug("SQL-Execution [{}] took [{}] ms.", sql, duration); + } + } + + /** + * Executes an update or delete statement and returns the update count. + * + * @param sql an SQL statement string + * @param params the parameters for the SQL statement + * @return the update count + * @throws SQLException on error + */ + public final int update(final String sql, final Object... params) throws SQLException { + return new RetryManager(params) { + + @Override + protected Integer call() throws SQLException { + return reallyUpdate(sql, params); + } + + }.doTry(); + } + + int reallyUpdate(String sql, Object... params) throws SQLException { + Connection con = null; + PreparedStatement stmt = null; + boolean inBatchMode = inBatchMode(); + long start = System.currentTimeMillis(); + try { + con = getConnection(inBatchMode); + stmt = con.prepareStatement(sql); + return execute(stmt, params).getUpdateCount(); + } finally { + closeResources(con, stmt, null, inBatchMode); + log.debug("SQL-Execution [{}] took [{}] ms.", sql, (System.currentTimeMillis() - start) ); + } + } + + /** + * Executes a SQL query and returns the {@link ResultSet}. The + * returned {@link ResultSet} should be closed by clients. + * + * @param sql an SQL statement string + * @param params the parameters for the SQL statement + * @return a {@link ResultSet} + */ + public final ResultSet query(String sql, Object... params) throws SQLException { + return exec(sql, params, false, 0); + } + + /** + * Executes a general SQL statement and returns the {@link ResultSet} of the executed statement. The + * returned {@link ResultSet} should be closed by clients. + * + * @param sql an SQL statement string + * @param params the parameters for the SQL statement + * @param returnGeneratedKeys whether generated keys should be returned + * @param maxRows the maximum number of rows in a potential {@link ResultSet} (0 means no limit) + * @return a {@link ResultSet} + * @throws SQLException on error + */ + public final ResultSet exec(final String sql, final Object[] params, final boolean returnGeneratedKeys, + final int maxRows) throws SQLException { + return new RetryManager(params) { + + @Override + protected ResultSet call() throws SQLException { + return reallyExec(sql, params, returnGeneratedKeys, maxRows); + } + + }.doTry(); + } + + ResultSet reallyExec(String sql, Object[] params, boolean returnGeneratedKeys, int maxRows) + throws SQLException { + Connection con = null; + PreparedStatement stmt = null; + ResultSet rs = null; + boolean inBatchMode = inBatchMode(); + long start = System.currentTimeMillis(); + try { + con = getConnection(inBatchMode); + if (returnGeneratedKeys) { + stmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + } else { + stmt = con.prepareStatement(sql); + } + stmt.setMaxRows(maxRows); + int currentFetchSize = this.fetchSize; + if (0 < maxRows && maxRows < currentFetchSize) { + currentFetchSize = maxRows; // JCR-3090 + } + stmt.setFetchSize(currentFetchSize); + execute(stmt, params); + if (returnGeneratedKeys) { + rs = stmt.getGeneratedKeys(); + } else { + rs = stmt.getResultSet(); + } + // Don't wrap null + if (rs == null) { + closeResources(con, stmt, rs, inBatchMode); + return null; + } + if (inBatchMode) { + return ResultSetWrapper.newInstance(null, stmt, rs); + } else { + return ResultSetWrapper.newInstance(con, stmt, rs); + } + } catch (SQLException e) { + closeResources(con, stmt, rs, inBatchMode); + throw e; + } finally { + log.debug("SQL-Execution [{}] took [{}] ms.", sql, (System.currentTimeMillis() - start) ); + } + } + + /** + * Gets a connection based on the {@code batchMode} state of this helper. The connection should be closed + * by a call to {@link #closeResources(Connection, Statement, ResultSet, boolean)} which also takes the {@code + * batchMode} state into account. + * + * @param inBatchMode indicates if we are in a batchMode + * @return a {@code Connection} to use, based on the batch mode state + * @throws SQLException on error + */ + protected final Connection getConnection(boolean inBatchMode) throws SQLException { + if (inBatchMode) { + return getTransactionAwareBatchConnection(); + } else { + Connection con = dataSource.getConnection(); + // JCR-1013: Setter may fail unnecessarily on a managed connection + if (!con.getAutoCommit()) { + con.setAutoCommit(true); + } + return con; + } + } + + /** + * Returns the Batch Connection. + * + * @return Connection + */ + private Connection getTransactionAwareBatchConnection() { + Object threadId = TransactionContext.getCurrentThreadId(); + return batchConnectionMap.get(threadId); + } + + /** + * Stores the given Connection to the batchConnectionMap. + * If we are running in a XA Environment the globalTransactionId will be used as Key. + * In Non-XA Environment the ThreadName is used. + * + * @param batchConnection + */ + private void setTransactionAwareBatchConnection(Connection batchConnection) { + Object threadId = TransactionContext.getCurrentThreadId(); + batchConnectionMap.put(threadId, batchConnection); + } + + /** + * Removes the Batch Connection from the batchConnectionMap + */ + private void removeTransactionAwareBatchConnection() { + Object threadId = TransactionContext.getCurrentThreadId(); + batchConnectionMap.remove(threadId); + } + + /** + * Closes the given resources given the {@code batchMode} state. + * + * @param con the {@code Connection} obtained through the {@link #getConnection(boolean)} method + * @param stmt a {@code Statement} + * @param rs a {@code ResultSet} + * @param inBatchMode indicates if we are in a batchMode + */ + protected final void closeResources(Connection con, Statement stmt, ResultSet rs, boolean inBatchMode) { + if (inBatchMode) { + DbUtility.close(null, stmt, rs); + } else { + DbUtility.close(con, stmt, rs); + } + } + + /** + * This method is used by all methods of this class that execute SQL statements. This default + * implementation sets all parameters and unwraps {@link StreamWrapper} instances. Subclasses may override + * this method to do something special with the parameters. E.g., the {@link Oracle10R1ConnectionHelper} + * overrides it in order to add special blob handling. + * + * @param stmt the {@link PreparedStatement} to execute + * @param params the parameters + * @return the executed statement + * @throws SQLException on error + */ + protected PreparedStatement execute(PreparedStatement stmt, Object[] params) throws SQLException { + for (int i = 0; params != null && i < params.length; i++) { + Object p = params[i]; + if (p instanceof StreamWrapper) { + StreamWrapper wrapper = (StreamWrapper) p; + stmt.setBinaryStream(i + 1, wrapper.getStream(), (int) wrapper.getSize()); + } else { + stmt.setObject(i + 1, p); + } + } + stmt.execute(); + return stmt; + } + + /** + * This class encapsulates the logic to retry a method invocation if it threw an SQLException. + * The RetryManager must cleanup the Params it will get. + * + * @param the return type of the method which is retried if it failed + */ + public abstract class RetryManager { + + private Object[] params; + + public RetryManager(Object[] params) { + this.params = params; + } + + public final T doTry() throws SQLException { + try { + if (inBatchMode()) { + return call(); + } else { + boolean sleepInterrupted = false; + int failures = 0; + SQLException lastException = null; + while (!sleepInterrupted && (blockOnConnectionLoss || failures <= RETRIES)) { + try { + return call(); + } catch (SQLException e) { + lastException = e; + } + log.error("Failed to execute SQL (stacktrace on DEBUG log level): " + lastException); + log.debug("Failed to execute SQL", lastException); + if (!resetParamResources()) { + log.warn("Could not reset parameters: not retrying SQL call"); + break; + } + failures++; + if (blockOnConnectionLoss || failures <= RETRIES) { // if we're going to try again + try { + Thread.sleep(SLEEP_BETWEEN_RETRIES_MS); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + sleepInterrupted = true; + log.error("Interrupted: canceling retry"); + } + } + } + throw lastException; + } + } finally { + cleanupParamResources(); + } + } + + protected abstract T call() throws SQLException; + + /** + * Cleans up the Parameter resources that are not automatically closed or deleted. + */ + protected void cleanupParamResources() { + for (int i = 0; params != null && i < params.length; i++) { + Object p = params[i]; + if (p instanceof StreamWrapper) { + StreamWrapper wrapper = (StreamWrapper) p; + wrapper.closeStream(); + } + } + } + + protected boolean resetParamResources() { + for (int i = 0; params != null && i < params.length; i++) { + Object p = params[i]; + if (p instanceof StreamWrapper) { + StreamWrapper wrapper = (StreamWrapper) p; + if(!wrapper.resetStream()) { + return false; + } + } + } + return true; + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DataSourceWrapper.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DataSourceWrapper.java new file mode 100644 index 00000000000..3902d74b536 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DataSourceWrapper.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.core.util.db; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +/** + * This class delegates all calls to the corresponding method on the wrapped {@code DataSource} except for the {@link #getConnection()} method, + * which delegates to {@code DataSource#getConnection(String, String)} with the username and password + * which are given on construction. + */ +public class DataSourceWrapper implements DataSource { + + private final DataSource dataSource; + + private final String username; + + private final String password; + + /** + * @param dataSource the {@code DataSource} to wrap + * @param username the username to use + * @param password the password to use + */ + public DataSourceWrapper(DataSource dataSource, String username, String password) { + this.dataSource = dataSource; + this.username = username; + this.password = password; + } + + /** + * Java 6 method. + * + * {@inheritDoc} + */ + public boolean isWrapperFor(Class arg0) throws SQLException { + throw new UnsupportedOperationException("Java 6 method not supported"); + } + + /** + * Java 6 method. + * + * {@inheritDoc} + */ + public T unwrap(Class arg0) throws SQLException { + throw new UnsupportedOperationException("Java 6 method not supported"); + } + + /** + * Unsupported Java 7 method. + * + * @see JCR-3167 + */ + public Logger getParentLogger() { + throw new UnsupportedOperationException("Java 7 method not supported"); + } + + /** + * {@inheritDoc} + */ + public Connection getConnection() throws SQLException { + return dataSource.getConnection(username, password); + } + + /** + * {@inheritDoc} + */ + public Connection getConnection(String username, String password) throws SQLException { + return dataSource.getConnection(username, password); + } + + /** + * {@inheritDoc} + */ + public PrintWriter getLogWriter() throws SQLException { + return dataSource.getLogWriter(); + } + + /** + * {@inheritDoc} + */ + public int getLoginTimeout() throws SQLException { + return dataSource.getLoginTimeout(); + } + + /** + * {@inheritDoc} + */ + public void setLogWriter(PrintWriter out) throws SQLException { + dataSource.setLogWriter(out); + } + + /** + * {@inheritDoc} + */ + public void setLoginTimeout(int seconds) throws SQLException { + dataSource.setLoginTimeout(seconds); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DatabaseAware.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DatabaseAware.java new file mode 100644 index 00000000000..a686c881209 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DatabaseAware.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.core.util.db; + +/** + * Bean components (i.e., classes that appear in the repository descriptor) that implement this interface will + * get the repositories {@link ConnectionFactory} instance injected just after construction and before + * initialization. + */ +public interface DatabaseAware { + + /** + * @param connectionFactory + */ + void setConnectionFactory(ConnectionFactory connectionFactory); +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DbUtility.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DbUtility.java new file mode 100644 index 00000000000..90c9ee20269 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DbUtility.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.core.util.db; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class contains some database utility methods. + */ +public final class DbUtility { + + private static final Logger LOG = LoggerFactory.getLogger(DbUtility.class); + + /** + * Private constructor for utility class pattern. + */ + private DbUtility() { + } + + /** + * This is a utility method which closes the given resources without throwing exceptions. Any exceptions + * encountered are logged instead. + * + * @param rs the {@link ResultSet} to close, may be null + */ + public static void close(ResultSet rs) { + close(null, null, rs); + } + + /** + * This is a utility method which closes the given resources without throwing exceptions. Any exceptions + * encountered are logged instead. + * + * @param con the {@link Connection} to close, may be null + * @param stmt the {@link Statement} to close, may be null + * @param rs the {@link ResultSet} to close, may be null + */ + public static void close(Connection con, Statement stmt, ResultSet rs) { + try { + if (rs != null) { + rs.close(); + } + } catch (SQLException e) { + logException("failed to close ResultSet", e); + } finally { + try { + if (stmt != null) { + stmt.close(); + } + } catch (SQLException e) { + logException("failed to close Statement", e); + } finally { + try { + if (con != null && !con.isClosed()) { + con.close(); + } + } catch (SQLException e) { + logException("failed to close Connection", e); + } + } + } + } + + /** + * Logs an SQL exception on error level, and debug level (more detail). + * + * @param message the message + * @param e the exception + */ + public static void logException(String message, SQLException e) { + if (message != null) { + LOG.error(message); + } + LOG.error(" Reason: " + e.getMessage()); + LOG.error(" State/Code: " + e.getSQLState() + "/" + e.getErrorCode()); + LOG.debug(" dump:", e); + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DerbyConnectionHelper.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DerbyConnectionHelper.java new file mode 100644 index 00000000000..76f333f6b26 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/DerbyConnectionHelper.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.core.util.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + */ +public final class DerbyConnectionHelper extends ConnectionHelper { + + /** name of the embedded driver */ + public static final String DERBY_EMBEDDED_DRIVER = "org.apache.derby.jdbc.EmbeddedDriver"; + + private static Logger log = LoggerFactory.getLogger(DerbyConnectionHelper.class); + + /** + * @param dataSrc the {@code DataSource} on which this helper acts + * @param block whether to block on connection loss until the db is up again + */ + public DerbyConnectionHelper(DataSource dataSrc, boolean block) { + super(dataSrc, block); + } + + /** + * Shuts the embedded Derby database down. + * + * @param driver the driver + * @throws SQLException on failure + */ + public void shutDown(String driver) throws SQLException { + // check for embedded driver + if (!DERBY_EMBEDDED_DRIVER.equals(driver)) { + return; + } + + // prepare connection url for issuing shutdown command + String url = null; + Connection con = null; + + try { + con = dataSource.getConnection(); + try { + url = con.getMetaData().getURL(); + } catch (SQLException e) { + // JCR-1557: embedded derby db probably already shut down; + // this happens when configuring multiple FS/PM instances + // to use the same embedded derby db instance. + log.debug("failed to retrieve connection url: embedded db probably already shut down", e); + return; + } + // we have to reset the connection to 'autoCommit=true' before closing it; + // otherwise Derby would mysteriously complain about some pending uncommitted + // changes which can't possibly be true. + // @todo further investigate + con.setAutoCommit(true); + } + finally { + DbUtility.close(con, null, null); + } + int pos = url.lastIndexOf(';'); + if (pos != -1) { + // strip any attributes from connection url + url = url.substring(0, pos); + } + url += ";shutdown=true"; + + // now it's safe to shutdown the embedded Derby database + try { + DriverManager.getConnection(url); + } catch (SQLException e) { + // a shutdown command always raises a SQLException + log.info(e.getMessage()); + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/Oracle10R1ConnectionHelper.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/Oracle10R1ConnectionHelper.java new file mode 100644 index 00000000000..f4efdb7cd02 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/Oracle10R1ConnectionHelper.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.core.util.db; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.sql.Blob; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The connection helper for Oracle databases of version up to 10.1. It has special blob handling. + */ +public final class Oracle10R1ConnectionHelper extends OracleConnectionHelper { + + /** + * the default logger + */ + private static Logger log = LoggerFactory.getLogger(Oracle10R1ConnectionHelper.class); + + private Class blobClass; + + private Integer durationSessionConstant; + + private Integer modeReadWriteConstant; + + /** + * @param dataSrc the {@code DataSource} on which this helper acts + * @param block whether to block on connection loss until the db is up again + */ + public Oracle10R1ConnectionHelper(DataSource dataSrc, boolean block) { + super(dataSrc, block); + } + + /** + * Retrieve the oracle.sql.BLOB class via reflection, and initialize the values for the + * DURATION_SESSION and MODE_READWRITE constants defined there. + */ + @Override + public void init() throws Exception { + super.init(); + // initialize oracle.sql.BLOB class & constants + + // use the Connection object for using the exact same + // class loader that the Oracle driver was loaded with + Connection con = null; + try { + con = dataSource.getConnection(); + blobClass = con.getClass().getClassLoader().loadClass("oracle.sql.BLOB"); + durationSessionConstant = new Integer(blobClass.getField("DURATION_SESSION").getInt(null)); + modeReadWriteConstant = new Integer(blobClass.getField("MODE_READWRITE").getInt(null)); + } finally { + if (con != null) { + DbUtility.close(con, null, null); + } + } + } + + /** + * Wraps any input-stream parameters in temporary blobs and frees these again after the statement + * has been executed. + * + * {@inheritDoc} + */ + @Override + protected PreparedStatement execute(PreparedStatement stmt, Object[] params) throws SQLException { + List tmpBlobs = new ArrayList(); + try { + for (int i = 0; params != null && i < params.length; i++) { + Object p = params[i]; + if (p instanceof StreamWrapper) { + StreamWrapper wrapper = (StreamWrapper) p; + Blob tmp = createTemporaryBlob(stmt.getConnection(), wrapper.getStream()); + tmpBlobs.add(tmp); + stmt.setBlob(i + 1, tmp); + } else if (p instanceof InputStream) { + Blob tmp = createTemporaryBlob(stmt.getConnection(), (InputStream) p); + tmpBlobs.add(tmp); + stmt.setBlob(i + 1, tmp); + } else { + stmt.setObject(i + 1, p); + } + } + stmt.execute(); + return stmt; + } catch (Exception e) { + throw new SQLException(e.getMessage()); + } finally { + for (Blob blob : tmpBlobs) { + try { + freeTemporaryBlob(blob); + } catch (Exception e) { + log.warn("Could not close temporary blob", e); + } + } + } + } + + /** + * Creates a temporary oracle.sql.BLOB instance via reflection and spools the contents of the specified + * stream. + */ + private Blob createTemporaryBlob(Connection con, InputStream in) throws Exception { + /* + * BLOB blob = BLOB.createTemporary(con, false, BLOB.DURATION_SESSION); + * blob.open(BLOB.MODE_READWRITE); OutputStream out = blob.getBinaryOutputStream(); ... out.flush(); + * out.close(); blob.close(); return blob; + */ + Method createTemporary = + blobClass.getMethod("createTemporary", new Class[]{Connection.class, Boolean.TYPE, Integer.TYPE}); + Object blob = + createTemporary.invoke(null, new Object[]{ConnectionFactory.unwrap(con), Boolean.FALSE, + durationSessionConstant}); + Method open = blobClass.getMethod("open", new Class[]{Integer.TYPE}); + open.invoke(blob, new Object[]{modeReadWriteConstant}); + Method getBinaryOutputStream = blobClass.getMethod("getBinaryOutputStream", new Class[0]); + OutputStream out = (OutputStream) getBinaryOutputStream.invoke(blob); + try { + IOUtils.copy(in, out); + } finally { + try { + out.flush(); + } catch (IOException ioe) { + } + out.close(); + } + Method close = blobClass.getMethod("close", new Class[0]); + close.invoke(blob); + return (Blob) blob; + } + + /** + * Frees a temporary oracle.sql.BLOB instance via reflection. + */ + private void freeTemporaryBlob(Blob blob) throws Exception { + // blob.freeTemporary(); + Method freeTemporary = blobClass.getMethod("freeTemporary", new Class[0]); + freeTemporary.invoke(blob); + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/OracleConnectionHelper.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/OracleConnectionHelper.java new file mode 100644 index 00000000000..e9baeab60aa --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/OracleConnectionHelper.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.core.util.db; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The connection helper for Oracle databases of version 10.2 and later. + */ +public class OracleConnectionHelper extends ConnectionHelper { + + /** + * the default logger + */ + private static Logger log = LoggerFactory.getLogger(OracleConnectionHelper.class); + + /** + * @param dataSrc the {@code DataSource} on which this helper acts + * @param block whether to block on connection loss until the db is up again + */ + public OracleConnectionHelper(DataSource dataSrc, boolean block) { + super(dataSrc, true, block); + } + + /** + * Initializes the helper: checks for valid driver version. + * Subclasses that override this method should still call it! + * + * @throws Exception on error + */ + public void init() throws Exception { + // check driver version + Connection connection = dataSource.getConnection(); + try { + DatabaseMetaData metaData = connection.getMetaData(); + if (metaData.getDriverMajorVersion() < 10) { + // Oracle drivers prior to version 10 only support + // writing BLOBs up to 32k in size... + log.warn("Unsupported driver version detected: " + + metaData.getDriverName() + + " v" + metaData.getDriverVersion()); + } + } catch (SQLException e) { + log.warn("Can not retrieve driver version", e); + } finally { + DbUtility.close(connection, null, null); + } + } + + /** + * Since Oracle only supports table names up to 30 characters in + * length illegal characters are simply replaced with "_" rather than + * escaping them with "_x0000_". + * + * {@inheritDoc} + */ + @Override + protected final void replaceCharacter(StringBuilder escaped, char c) { + escaped.append("_"); + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/PostgreSQLConnectionHelper.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/PostgreSQLConnectionHelper.java new file mode 100644 index 00000000000..872d1990df7 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/PostgreSQLConnectionHelper.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.core.util.db; + +import javax.sql.DataSource; + + +/** + * The connection helper for PSQL databases. It has special fetch size handling. + */ +public final class PostgreSQLConnectionHelper extends ConnectionHelper { + + /** + * @param dataSrc the {@code DataSource} on which this helper acts + * @param block whether to block on connection loss until the db is up again + */ + public PostgreSQLConnectionHelper(DataSource dataSrc, boolean block) { + super(dataSrc, false, block, 10000); + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ResultSetWrapper.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ResultSetWrapper.java new file mode 100644 index 00000000000..2b171466879 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/ResultSetWrapper.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.core.util.db; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; + + +/** + * This is a dynamic proxy in order to support both Java 5 and 6. + */ +public final class ResultSetWrapper implements InvocationHandler { + + private final Connection connection; + + private final Statement statement; + + private final ResultSet resultSet; + + /** + * Creates a new {@code ResultSet} proxy which closes the given {@code Connection} and + * {@code Statement} if it is closed. + * + * @param con the associated {@code Connection} + * @param stmt the associated {@code Statement} + * @param rs the {@code ResultSet} which backs the proxy + * @return a {@code ResultSet} proxy + */ + public static final ResultSet newInstance(Connection con, Statement stmt, ResultSet rs) { + ResultSetWrapper proxy = new ResultSetWrapper(con, stmt, rs); + return (ResultSet) Proxy.newProxyInstance(rs.getClass().getClassLoader(), + new Class[]{ResultSet.class}, proxy); + } + + private ResultSetWrapper(Connection con, Statement stmt, ResultSet rs) { + connection = con; + statement = stmt; + resultSet = rs; + } + + /** + * {@inheritDoc} + */ + public Object invoke(Object proxy, Method m, Object[] args) throws Throwable { + if ("close".equals(m.getName())) { + DbUtility.close(connection, statement, resultSet); + return null; + } else { + return m.invoke(resultSet, args); + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/StreamWrapper.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/StreamWrapper.java new file mode 100644 index 00000000000..ba38415c6dd --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/StreamWrapper.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.core.util.db; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; + +import org.apache.commons.io.input.CloseShieldInputStream; +import org.apache.jackrabbit.core.util.db.ConnectionHelper.RetryManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StreamWrapper { + + private final Logger log = LoggerFactory.getLogger(StreamWrapper.class); + + private MarkDetectingInputStream stream; + private final long size; + + /** + * Creates a wrapper for the given InputStream that can + * safely be passed as a parameter to the {@link ConnectionHelper#exec(String, Object...)}, + * {@link ConnectionHelper#exec(String, Object[], boolean, int)} and + * {@link ConnectionHelper#update(String, Object[])} methods. + * + * @param in the InputStream to wrap + * @param size the size of the input stream + */ + public StreamWrapper(InputStream in, long size) { + this.stream = new MarkDetectingInputStream(in); + this.size = size; + } + + public InputStream getStream() { + return new CloseShieldInputStream(stream); + } + + public long getSize() { + return size; + } + + public void closeStream() { + try { + stream.close(); + } catch (IOException e) { + log.error("Error while closing stream", e); + } + } + + /** + * Resets the internal InputStream that it could be re-read.
    + * Is used from {@link ConnectionHelper.RetryManager} if a {@link SQLException} has occurred.
    + * It relies on the assumption that the InputStream was not marked anywhere + * during reading. + * + * @return returns true if it was able to reset the Stream + */ + public boolean resetStream() { + try { + if (!stream.isMarked()) { + stream.reset(); + return true; + } else { + log.warn("Cannot reset stream to the beginning because it was marked."); + return false; + } + } catch (IOException e) { + return false; + } + } + + private static class MarkDetectingInputStream extends FilterInputStream { + + private boolean marked; + + protected MarkDetectingInputStream(final InputStream in) { + super(in); + } + + @Override + public synchronized void mark(final int readlimit) { + super.mark(readlimit); + marked = true; + } + + private boolean isMarked() { + return marked; + } + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/package-info.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/package-info.java new file mode 100755 index 00000000000..775639ff17b --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/util/db/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.core.util.db; diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/InternalXAResource.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/InternalXAResource.java new file mode 100644 index 00000000000..c68d7d2503f --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/InternalXAResource.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.data.core; + + + +/** + * Interface implemented by resources that provide XA functionality. + */ +public interface InternalXAResource { + + /** + * Associate this resource with a transaction. All further operations on + * the object should be interpreted as part of this transaction and changes + * recorded in some attribute of the transaction context. + * @param tx transaction context, if null disassociate + */ + void associate(TransactionContext tx); + + /** + * Invoked before one of the {@link #prepare}, {@link #commit} or + * {@link #rollback} method is called. + * @param tx transaction context + */ + void beforeOperation(TransactionContext tx); + + /** + * Prepare transaction. The transaction is identified by a transaction + * context. + * @param tx transaction context + * @throws TransactionException if an error occurs + */ + void prepare(TransactionContext tx) throws TransactionException; + + /** + * Commit transaction. The transaction is identified by a transaction + * context. If the method throws, other resources get their changes + * rolled back. + * @param tx transaction context + * @throws TransactionException if an error occurs + */ + void commit(TransactionContext tx) throws TransactionException; + + /** + * Rollback transaction. The transaction is identified by a transaction + * context. + * @param tx transaction context. + */ + void rollback(TransactionContext tx) throws TransactionException; + + /** + * Invoked after one of the {@link #prepare}, {@link #commit} or + * {@link #rollback} method has been called. + * @param tx transaction context + */ + void afterOperation(TransactionContext tx); + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/TransactionContext.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/TransactionContext.java new file mode 100644 index 00000000000..054a1301a4d --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/TransactionContext.java @@ -0,0 +1,400 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.data.core; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.Xid; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents the transaction on behalf of the component that wants to + * explicitly demarcate transaction boundaries. After having been prepared, + * schedules a task that rolls back the transaction if some time passes without + * any further action. This will guarantee that global objects locked by one + * of the resources' {@link InternalXAResource#prepare} method, are eventually + * unlocked. + */ +public class TransactionContext { + + /** + * Logger instance. + */ + private static final Logger log = LoggerFactory.getLogger(TransactionContext.class); + + private static final int STATUS_PREPARING = 1; + private static final int STATUS_PREPARED = 2; + private static final int STATUS_COMMITTING = 3; + private static final int STATUS_COMMITTED = 4; + private static final int STATUS_ROLLING_BACK = 5; + private static final int STATUS_ROLLED_BACK = 6; + + /** + * The per thread associated Xid + */ + private static final ThreadLocal CURRENT_XID = new ThreadLocal(); + + /** + * Transactional resources. + */ + private final InternalXAResource[] resources; + + /** + * The Xid + */ + private final Xid xid; + + /** + * Transaction attributes. + */ + private final Map attributes = new HashMap(); + + /** + * Status. + */ + private int status; + + /** + * Flag indicating whether the association is currently suspended. + */ + private boolean suspended; + + /** + * Create a new instance of this class. + * + * @param xid associated xid + * @param resources transactional resources + */ + public TransactionContext(Xid xid, InternalXAResource[] resources) { + this.xid = xid; + this.resources = resources; + } + + /** + * Set an attribute on this transaction. If the value specified is + * null, it is semantically equivalent to + * {@link #removeAttribute}. + * + * @param name attribute name + * @param value attribute value + */ + public void setAttribute(String name, Object value) { + if (value == null) { + removeAttribute(name); + } + attributes.put(name, value); + } + + /** + * Return an attribute value on this transaction. + * + * @param name attribute name + * @return attribute value, null if no attribute with that + * name exists + */ + public Object getAttribute(String name) { + return attributes.get(name); + } + + /** + * Remove an attribute on this transaction. + * + * @param name attribute name + */ + public void removeAttribute(String name) { + attributes.remove(name); + } + + /** + * Prepare the transaction identified by this context. Prepares changes on + * all resources. If some resource reports an error on prepare, + * automatically rollback changes on all other resources. Throw exception + * at the end if errors were found. + * + * @throws XAException if an error occurs + */ + public synchronized void prepare() throws XAException { + bindCurrentXid(); + status = STATUS_PREPARING; + beforeOperation(); + + TransactionException txe = null; + for (int i = 0; i < resources.length; i++) { + try { + resources[i].prepare(this); + } catch (TransactionException e) { + txe = e; + break; + } catch (Exception e) { + txe = new TransactionException("Error while preparing resource " + resources, e); + break; + } + } + + afterOperation(); + status = STATUS_PREPARED; + + if (txe != null) { + // force immediate rollback on error. + try { + rollback(); + } catch (XAException e) { + /* ignore */ + } + XAException e = new XAException(XAException.XA_RBOTHER); + e.initCause(txe); + throw e; + } + } + + /** + * Commit the transaction identified by this context. Commits changes on + * all resources. If some resource reports an error on commit, + * automatically rollback changes on all other resources. Throw + * exception at the end if some commit failed. + * + * @throws XAException if an error occurs + */ + public synchronized void commit() throws XAException { + if (status == STATUS_ROLLED_BACK) { + throw new XAException(XAException.XA_HEURRB); + } + + boolean heuristicCommit = false; + bindCurrentXid(); + status = STATUS_COMMITTING; + beforeOperation(); + + TransactionException txe = null; + for (int i = 0; i < resources.length; i++) { + InternalXAResource resource = resources[i]; + if (txe != null) { + try { + resource.rollback(this); + } catch (Exception e) { + log.warn("Unable to rollback changes on " + resource, e); + } + } else { + try { + resource.commit(this); + heuristicCommit = true; + } catch (TransactionException e) { + txe = e; + } catch (Exception e) { + txe = new TransactionException("Error while committing resource " + resource, e); + } + } + } + afterOperation(); + status = STATUS_COMMITTED; + + cleanCurrentXid(); + + if (txe != null) { + XAException e = null; + if (heuristicCommit) { + e = new XAException(XAException.XA_HEURMIX); + } else { + e = new XAException(XAException.XA_HEURRB); + } + e.initCause(txe); + throw e; + } + } + + /** + * Rollback the transaction identified by this context. Rolls back changes + * on all resources. Throws exception at the end if errors were found. + * @throws XAException if an error occurs + */ + public synchronized void rollback() throws XAException { + if (status == STATUS_ROLLED_BACK) { + throw new XAException(XAException.XA_RBOTHER); + } + bindCurrentXid(); + status = STATUS_ROLLING_BACK; + beforeOperation(); + + int errors = 0; + for (int i = 0; i < resources.length; i++) { + InternalXAResource resource = resources[i]; + try { + resource.rollback(this); + } catch (Exception e) { + log.warn("Unable to rollback changes on " + resource, e); + errors++; + } + } + afterOperation(); + status = STATUS_ROLLED_BACK; + + cleanCurrentXid(); + + if (errors != 0) { + throw new XAException(XAException.XA_RBOTHER); + } + } + + /** + * Invoke all of the registered resources' {@link InternalXAResource#beforeOperation} + * methods. + */ + private void beforeOperation() { + for (int i = 0; i < resources.length; i++) { + resources[i].beforeOperation(this); + } + } + + /** + * Invoke all of the registered resources' {@link InternalXAResource#afterOperation} + * methods. + */ + private void afterOperation() { + for (int i = 0; i < resources.length; i++) { + resources[i].afterOperation(this); + } + } + + /** + * Return a flag indicating whether the association is suspended. + * + * @return true if the association is suspended; + * false otherwise + */ + public boolean isSuspended() { + return suspended; + } + + /** + * Set a flag indicating whether the association is suspended. + * + * @param suspended flag whether that the association is suspended. + */ + public void setSuspended(boolean suspended) { + this.suspended = suspended; + } + + /** + * Helper Method to bind the {@link Xid} associated with this {@link TransactionContext} + * to the {@link #CURRENT_XID} ThreadLocal. + */ + private void bindCurrentXid() { + CURRENT_XID.set(xid); + } + + /** + * Helper Method to clean the {@link Xid} associated with this {@link TransactionContext} + * from the {@link #CURRENT_XID} ThreadLocal. + */ + private void cleanCurrentXid() { + CURRENT_XID.set(null); + } + + /** + * Returns the {@link Xid} bind to the {@link #CURRENT_XID} ThreadLocal + * @return current Xid or null + */ + private static Xid getCurrentXid() { + return CURRENT_XID.get(); + } + + /** + * Returns the current thread identifier. The identifier is either the + * current thread instance or the global transaction identifier wrapped + * in a {@link XidWrapper}, when running under a transaction. + * + * @return current thread identifier + */ + public static Object getCurrentThreadId() { + Xid xid = TransactionContext.getCurrentXid(); + if (xid != null) { + return new XidWrapper(xid.getGlobalTransactionId()); + } else { + return Thread.currentThread(); + } + } + + /** + * Compares the given thread identifiers for equality. + * + * @see #getCurrentThreadId() + */ + public static boolean isSameThreadId(Object a, Object b) { + if (a == b) { + return true; + } else if (a != null) { + return a.equals(b); + } else { + return false; + } + } + + /** + * Wrapper around a global transaction id (byte[]) + * that handles hashCode and equals in a proper way. + */ + private static class XidWrapper { + + private static final char[] HEX = "0123456789abcdef".toCharArray(); + + private byte[] gtid; + + public XidWrapper(byte[] gtid) { + this.gtid = gtid; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof XidWrapper)) { + return false; + } + return Arrays.equals((byte[]) gtid, ((XidWrapper)other).gtid); + } + + @Override + public int hashCode() { + return Arrays.hashCode(gtid); + } + + @Override + public String toString() { + return encodeHexString(gtid); + } + + /** + * Returns the hex encoding of the given bytes. + * + * @param value value to be encoded + * @return encoded value + */ + private static String encodeHexString(byte[] value) { + char[] buffer = new char[value.length * 2]; + for (int i = 0; i < value.length; i++) { + buffer[2 * i] = HEX[(value[i] >> 4) & 0x0f]; + buffer[2 * i + 1] = HEX[value[i] & 0x0f]; + } + return new String(buffer); + } + + } + +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/TransactionException.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/TransactionException.java new file mode 100644 index 00000000000..f2746f6579a --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/TransactionException.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.data.core; + +/** + * TransactionException is thrown when some operation inside the transaction + * fails. + */ +public class TransactionException extends Exception { + + /** + * Creates an instance of this class. Takes a detail message as parameter. + * + * @param message message + */ + public TransactionException(String message) { + super(message); + } + + /** + * Creates an instance of this class. Takes a message and a root throwable + * as parameter. + * + * @param message message + * @param rootCause root throwable + */ + public TransactionException(String message, Throwable rootCause) { + super(message, rootCause); + } +} diff --git a/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/package-info.java b/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/package-info.java new file mode 100755 index 00000000000..523e8c2da42 --- /dev/null +++ b/jackrabbit-data/src/main/java/org/apache/jackrabbit/data/core/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.data.core; diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/azure.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/azure.properties new file mode 100644 index 00000000000..9a9a81937ac --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/azure.properties @@ -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. + +driver=com.microsoft.sqlserver.jdbc.SQLServerDriver +createTable=CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA IMAGE) diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/db2.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/db2.properties new file mode 100644 index 00000000000..f3f56158b76 --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/db2.properties @@ -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. + +driver=COM.ibm.db2.jdbc.net.DB2Driver +createTable=CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY NOT NULL, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA BLOB(1000M)) diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/derby.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/derby.properties new file mode 100644 index 00000000000..04d42dcfeed --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/derby.properties @@ -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. + +# Tested with Apache Derby 10.3.1.4 on Windows XP (2007-12-11) +driver=org.apache.derby.jdbc.EmbeddedDriver \ No newline at end of file diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/h2.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/h2.properties new file mode 100644 index 00000000000..bf9f1c929d9 --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/h2.properties @@ -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. + +# Tested with H2 1.0.63 on Windows XP (2007-12-11) +driver=org.h2.Driver +storeStream=-1 \ No newline at end of file diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/ingres.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/ingres.properties new file mode 100644 index 00000000000..0b08ca70495 --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/ingres.properties @@ -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. + +driver=com.ingres.jdbc.IngresDriver +createTable=CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY NOT NULL, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA LONG BYTE) diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/mssql.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/mssql.properties new file mode 100644 index 00000000000..9a9a81937ac --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/mssql.properties @@ -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. + +driver=com.microsoft.sqlserver.jdbc.SQLServerDriver +createTable=CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA IMAGE) diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/mysql.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/mysql.properties new file mode 100644 index 00000000000..34654c6be07 --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/mysql.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Tested with MySQL 5.0.27-community-nt on Windows XP (2007-12-11) +# currently, the objects must fit in memory +driver=com.mysql.jdbc.Driver +createTable=CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA BLOB(2147483647)) diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/oracle.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/oracle.properties new file mode 100644 index 00000000000..a0af7920f68 --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/oracle.properties @@ -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. + +# Tested with Oracle Database 10g Release 10.2.0.1.0 on Windows XP (2008-04-29) +driver=oracle.jdbc.OracleDriver +createTable=CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY, LENGTH NUMBER, LAST_MODIFIED NUMBER, DATA BLOB) diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/postgresql.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/postgresql.properties new file mode 100644 index 00000000000..953adaa7370 --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/postgresql.properties @@ -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. + +# Tested with PostgreSQL 8.2.4 on Windows XP (2007-12-11) +# currently, the objects must fit in memory +driver=org.postgresql.Driver +table=datastore +createTable=CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA BYTEA) diff --git a/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/sqlserver.properties b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/sqlserver.properties new file mode 100644 index 00000000000..11d0902a538 --- /dev/null +++ b/jackrabbit-data/src/main/resources/org/apache/jackrabbit/core/data/db/sqlserver.properties @@ -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. + +# Tested with Microsoft SQL Server 2005 4 on Windows XP (2007-12-11) +driver=com.microsoft.sqlserver.jdbc.SQLServerDriver +createTable=CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA IMAGE) diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/InMemoryBackend.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/InMemoryBackend.java new file mode 100644 index 00000000000..5360a74258b --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/InMemoryBackend.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jackrabbit.core.data; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * An in-memory backend implementation used to speed up testing. + */ +public class InMemoryBackend implements Backend { + + private HashMap data = new HashMap(); + + private HashMap timeMap = new HashMap(); + + private CachingDataStore store; + + private Properties properties; + + @Override + public void init(CachingDataStore store, String homeDir, String config) + throws DataStoreException { + // ignore + log("init"); + this.store = store; + } + + @Override + public void close() { + // ignore + log("close"); + } + + @Override + public boolean exists(final DataIdentifier identifier) { + log("exists " + identifier); + return data.containsKey(identifier); + } + + @Override + public Iterator getAllIdentifiers() + throws DataStoreException { + log("getAllIdentifiers"); + return data.keySet().iterator(); + } + + @Override + public InputStream read(final DataIdentifier identifier) + throws DataStoreException { + log("read " + identifier); + return new ByteArrayInputStream(data.get(identifier)); + } + + @Override + public void writeAsync(final DataIdentifier identifier, final File file, + final AsyncUploadCallback callback) throws DataStoreException { + this.write(identifier, file, true, callback); + } + + @Override + public void write(final DataIdentifier identifier, final File file) + throws DataStoreException { + this.write(identifier, file, false, null); + } + + @Override + public long getLastModified(final DataIdentifier identifier) + throws DataStoreException { + log("getLastModified " + identifier); + return timeMap.get(identifier); + } + + @Override + public void deleteRecord(final DataIdentifier identifier) + throws DataStoreException { + timeMap.remove(identifier); + data.remove(identifier); + } + + @Override + public Set deleteAllOlderThan(final long min) throws DataStoreException { + log("deleteAllOlderThan " + min); + Set tobeDeleted = new HashSet(); + for (Map.Entry entry : timeMap.entrySet()) { + DataIdentifier identifier = entry.getKey(); + long timestamp = entry.getValue(); + if (timestamp < min && !store.isInUse(identifier) + && store.confirmDelete(identifier)) { + store.deleteFromCache(identifier); + tobeDeleted.add(identifier); + } + } + for (DataIdentifier identifier : tobeDeleted) { + timeMap.remove(identifier); + data.remove(identifier); + } + return tobeDeleted; + } + + @Override + public long getLength(final DataIdentifier identifier) + throws DataStoreException { + try { + return data.get(identifier).length; + } catch (Exception e) { + throw new DataStoreException(e); + } + } + + @Override + public boolean exists(final DataIdentifier identifier, final boolean touch) + throws DataStoreException { + boolean retVal = data.containsKey(identifier); + if (retVal && touch) { + timeMap.put(identifier, System.currentTimeMillis()); + } + return retVal; + } + + @Override + public void touch(DataIdentifier identifier, long minModifiedDate) { + timeMap.put(identifier, System.currentTimeMillis()); + } + + @Override + public void touchAsync(DataIdentifier identifier, long minModifiedDate, + AsyncTouchCallback callback) { + timeMap.put(identifier, System.currentTimeMillis()); + callback.onSuccess(new AsyncTouchResult(identifier)); + } + + private void write(final DataIdentifier identifier, final File file, + final boolean async, final AsyncUploadCallback callback) + throws DataStoreException { + log("write " + identifier + " " + file.length()); + byte[] buffer = new byte[(int) file.length()]; + try { + if (async && callback == null) { + throw new IllegalArgumentException( + "callback parameter cannot be null"); + } + DataInputStream din = new DataInputStream(new FileInputStream(file)); + din.readFully(buffer); + din.close(); + data.put(identifier, buffer); + timeMap.put(identifier, System.currentTimeMillis()); + } catch (IOException e) { + if (async) { + callback.onAbort(new AsyncUploadResult(identifier, file)); + } + throw new DataStoreException(e); + } + if (async) { + callback.onSuccess(new AsyncUploadResult(identifier, file)); + } + } + + /** + * Properties used to configure the backend. If provided explicitly before + * init is invoked then these take precedence + * + * @param properties to configure S3Backend + */ + public void setProperties(Properties properties) { + this.properties = properties; + } + + /** + * Log a message if logging is enabled. + * + * @param message + * the message + */ + private void log(final String message) { + // System.out.println(message); + } +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/InMemoryDataStore.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/InMemoryDataStore.java new file mode 100644 index 00000000000..b3c87e01d42 --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/InMemoryDataStore.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.core.data; + +import java.util.Properties; + +import org.apache.jackrabbit.core.data.Backend; +import org.apache.jackrabbit.core.data.CachingDataStore; + +/** + * A caching data store that uses the in-memory backend. + */ +public class InMemoryDataStore extends CachingDataStore { + + private Properties properties; + + @Override + protected Backend createBackend() { + InMemoryBackend backend = new InMemoryBackend(); + if (properties != null) { + backend.setProperties(properties); + } + return backend; + } + + @Override + protected String getMarkerFile() { + return "mem.init.done"; + } + + /** + * Properties required to configure the S3Backend + */ + public void setProperties(Properties properties) { + this.properties = properties; + } +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/RandomInputStream.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/RandomInputStream.java new file mode 100644 index 00000000000..e2037671a31 --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/RandomInputStream.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.core.data; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An input stream that returns pseudo-random bytes. + */ +public class RandomInputStream extends InputStream { + + private static final long MUL = 0x5DEECE66DL; + private static final long ADD = 0xBL; + private static final long MASK = (1L << 48) - 1; + private static final int DEFAULT_MAX_READ_BLOCK_SIZE = 15; + + private final long initialSeed; + private final long len; + private long markedState; + private long pos; + private long markedPos; + private long state; + private int maxReadBlockSize; + + public String toString() { + return "new RandomInputStream(" + initialSeed + ", " + len + ")"; + } + + public RandomInputStream(long seed, long len) { + this(seed, len, DEFAULT_MAX_READ_BLOCK_SIZE); + } + + public static void compareStreams(InputStream a, InputStream b) throws IOException { + a = new BufferedInputStream(a); + b = new BufferedInputStream(b); + long pos = 0; + while (true) { + int x = a.read(); + int y = b.read(); + if (x == -1 || y == -1) { + if (x == y) { + break; + } + } + if (x != y) { + throw new IOException("Incorrect byte at position " + pos + ": x=" + x + " y=" + y); + } + } + } + + public RandomInputStream(long seed, long len, int maxReadBlockSize) { + this.initialSeed = seed; + this.len = len; + this.maxReadBlockSize = maxReadBlockSize; + setSeed(seed); + reset(); + } + + public long skip(long n) { + n = getReadBlock(n); + if (n == 0) { + return -1; + } + pos += n; + return n; + } + + private int getReadBlock(long n) { + if (n > (len - pos)) { + n = (len - pos); + } + if (n > maxReadBlockSize) { + n = maxReadBlockSize; + } else if (n < 0) { + n = 0; + } + return (int) n; + } + + public int read(byte[] b, int off, int len) { + if (pos >= this.len) { + return -1; + } + len = getReadBlock(len); + if (len == 0) { + return -1; + } + for (int i = 0; i < len; i++) { + b[off + i] = (byte) (next() & 255); + } + pos += len; + return len; + } + + public int read(byte[] b) { + return read(b, 0, b.length); + } + + public void close() { + pos = len; + } + + private void setSeed(long seed) { + markedState = (seed ^ MUL) & MASK; + } + + private int next() { + state = (state * MUL + ADD) & MASK; + return (int) (state >>> (48 - 32)); + } + + public void reset() { + pos = markedPos; + state = markedState; + } + + public int read() { + if (pos >= len) { + return -1; + } + pos++; + return next() & 255; + } + + public boolean markSupported() { + return true; + } + + public void mark(int readlimit) { + markedPos = pos; + markedState = state; + } + +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCachingFDS.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCachingFDS.java new file mode 100644 index 00000000000..5d44385df4e --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCachingFDS.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.core.data; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Properties; + +import javax.jcr.RepositoryException; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestCachingFDS extends TestFileDataStore { + + protected static final Logger LOG = LoggerFactory.getLogger(TestCachingFDS.class); + + private static final String PENDIND_UPLOAD_FILE = "async-pending-uploads.ser"; + + private static final String TO_BE_DELETED_UPLOAD_FILE = "async-tobedeleted-uploads.ser"; + + protected DataStore createDataStore() throws RepositoryException { + CachingFDS cacheFDS = new CachingFDS(); + Properties props = loadProperties("/fs.properties"); + String pathValue = props.getProperty(FSBackend.FS_BACKEND_PATH); + if (pathValue != null && !"".equals(pathValue.trim())) { + fsPath = pathValue + "/cachingFds" + "-" + + String.valueOf(randomGen.nextInt(100000)) + "-" + + String.valueOf(randomGen.nextInt(100000)); + } else { + fsPath = dataStoreDir + "/cachingFds"; + } + props.setProperty(FSBackend.FS_BACKEND_PATH, fsPath); + LOG.info("fsBackendPath [{}] set.", fsPath); + cacheFDS.setProperties(props); + cacheFDS.setSecret("12345"); + // disable asynchronous writing in testing. + cacheFDS.setAsyncUploadLimit(0); + cacheFDS.init(dataStoreDir); + return cacheFDS; + } + + /** + * Test robustness of {@link AsyncUploadCache} corruption. + */ + public void testAsyncUploadCacheCorruption() { + try { + ds = createDataStore(); + File pendingUploads = new File(dataStoreDir + "/" + + PENDIND_UPLOAD_FILE); + FileOutputStream fos = new FileOutputStream(pendingUploads); + IOUtils.write("garbage-data", fos); + fos.close(); + + File tobeDeletedFile = new File(dataStoreDir + "/" + + TO_BE_DELETED_UPLOAD_FILE); + fos = new FileOutputStream(tobeDeletedFile); + IOUtils.write("garbage-data", fos); + fos.close(); + ds.close(); + + doAddRecordTest(); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCachingFDSCacheOff.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCachingFDSCacheOff.java new file mode 100644 index 00000000000..4ea4e43fe88 --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCachingFDSCacheOff.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.core.data; + +import java.util.Properties; + +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestCachingFDSCacheOff extends TestFileDataStore { + + protected static final Logger LOG = LoggerFactory.getLogger(TestCachingFDS.class); + + protected DataStore createDataStore() throws RepositoryException { + CachingFDS cacheFDS = new CachingFDS(); + Properties props = loadProperties("/fs.properties"); + String pathValue = props.getProperty(FSBackend.FS_BACKEND_PATH); + if (pathValue != null && !"".equals(pathValue.trim())) { + fsPath = pathValue + "/cachingFds" + "-" + + String.valueOf(randomGen.nextInt(100000)) + "-" + + String.valueOf(randomGen.nextInt(100000)); + } else { + fsPath = dataStoreDir + "/cachingFDS"; + } + props.setProperty(FSBackend.FS_BACKEND_PATH, fsPath); + cacheFDS.setProperties(props); + cacheFDS.setSecret("12345"); + // disable asynchronous writing in testing. + cacheFDS.setAsyncUploadLimit(0); + cacheFDS.setCacheSize(0); + cacheFDS.init(dataStoreDir); + return cacheFDS; + } +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCaseBase.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCaseBase.java new file mode 100644 index 00000000000..dad1e093806 --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestCaseBase.java @@ -0,0 +1,682 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.Random; + +import javax.jcr.RepositoryException; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test base class which covers all scenarios. + */ +public abstract class TestCaseBase extends TestCase { + + /** + * Logger + */ + protected static final Logger LOG = LoggerFactory.getLogger(TestCaseBase.class); + + /** + * temp directory + */ + private static final String TEST_DIR = "target/temp"; + + /** + * Constant describing aws properties file path. + */ + public static final String CONFIG = "config"; + + /** + * length of record to be added + */ + protected int dataLength = 123456; + + /** + * datastore directory path + */ + protected String dataStoreDir; + + protected DataStore ds; + + /** + * Random number generator to populate data + */ + protected Random randomGen = new Random(); + + /** + * Delete temporary directory. + */ + @Override + protected void setUp() throws Exception { + dataStoreDir = TEST_DIR + "-" + String.valueOf(randomGen.nextInt(9999)) + + "-" + String.valueOf(randomGen.nextInt(9999)); + // delete directory if it exists + File directory = new File(dataStoreDir); + if (directory.exists()) { + boolean delSuccessFul = FileUtils.deleteQuietly(directory); + int retry = 2, count = 0; + while (!delSuccessFul && count <= retry) { + // try once more + delSuccessFul = FileUtils.deleteQuietly(new File(dataStoreDir)); + count++; + } + LOG.info("setup : directory [" + dataStoreDir + "] deleted [" + + delSuccessFul + "]"); + } + } + + @Override + protected void tearDown() { + boolean delSuccessFul = FileUtils.deleteQuietly(new File(dataStoreDir)); + int retry = 2, count = 0; + while (!delSuccessFul && count <= retry) { + // try once more + delSuccessFul = FileUtils.deleteQuietly(new File(dataStoreDir)); + count++; + } + LOG.info("tearDown : directory [" + dataStoreDir + "] deleted [" + + delSuccessFul + "]"); + } + /** + * Testcase to validate {@link DataStore#addRecord(InputStream)} API. + */ + public void testAddRecord() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#addRecord, testDir=" + dataStoreDir); + doAddRecordTest(); + LOG.info("Testcase: " + this.getClass().getName() + + "#addRecord finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } + + /** + * Testcase to validate {@link DataStore#getRecord(DataIdentifier)} API. + */ + public void testGetRecord() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testGetRecord, testDir=" + dataStoreDir); + doGetRecordTest(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testGetRecord finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + } + } + + /** + * Testcase to validate {@link DataStore#getAllIdentifiers()} API. + */ + public void testGetAllIdentifiers() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testGetAllIdentifiers, testDir=" + dataStoreDir); + doGetAllIdentifiersTest(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testGetAllIdentifiers finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } + + /** + * Testcase to validate {@link DataStore#updateModifiedDateOnAccess(long)} + * API. + */ + public void testUpdateLastModifiedOnAccess() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testUpdateLastModifiedOnAccess, testDir=" + dataStoreDir); + doUpdateLastModifiedOnAccessTest(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testUpdateLastModifiedOnAccess finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + } + } + + /** + * Testcase to validate + * {@link MultiDataStoreAware#deleteRecord(DataIdentifier)}.API. + */ + public void testDeleteRecord() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testDeleteRecord, testDir=" + dataStoreDir); + doDeleteRecordTest(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testDeleteRecord finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } + + /** + * Testcase to validate {@link DataStore#deleteAllOlderThan(long)} API. + */ + public void testDeleteAllOlderThan() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testDeleteAllOlderThan, testDir=" + dataStoreDir); + doDeleteAllOlderThan(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testDeleteAllOlderThan finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } + + /** + * Testcase to validate {@link DataStore#getRecordFromReference(String)} + */ + public void testReference() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testReference, testDir=" + dataStoreDir); + doReferenceTest(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testReference finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } + + /** + * Testcase to validate mixed scenario use of {@link DataStore}. + */ + public void testSingleThread() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testSingleThread, testDir=" + dataStoreDir); + doTestSingleThread(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testSingleThread finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } + + /** + * Testcase to validate mixed scenario use of {@link DataStore} in + * multi-threaded concurrent environment. + */ + public void testMultiThreaded() { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testMultiThreaded, testDir=" + dataStoreDir); + doTestMultiThreaded(); + LOG.info("Testcase: " + this.getClass().getName() + + "#testMultiThreaded finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + + } + + protected abstract DataStore createDataStore() throws RepositoryException ; + + /** + * Test {@link DataStore#addRecord(InputStream)} and assert length of added + * record. + */ + protected void doAddRecordTest() throws Exception { + ds = createDataStore(); + byte[] data = new byte[dataLength]; + randomGen.nextBytes(data); + DataRecord rec = ds.addRecord(new ByteArrayInputStream(data)); + assertEquals(data.length, rec.getLength()); + assertRecord(data, rec); + ds.close(); + } + + /** + * Test {@link DataStore#getRecord(DataIdentifier)} and assert length and + * inputstream. + */ + protected void doGetRecordTest() throws Exception { + ds = createDataStore(); + byte[] data = new byte[dataLength]; + randomGen.nextBytes(data); + DataRecord rec = ds.addRecord(new ByteArrayInputStream(data)); + rec = ds.getRecord(rec.getIdentifier()); + assertEquals(data.length, rec.getLength()); + assertRecord(data, rec); + ds.close(); + } + + /** + * Test {@link MultiDataStoreAware#deleteRecord(DataIdentifier)}. + */ + protected void doDeleteRecordTest() throws Exception { + ds = createDataStore(); + Random random = randomGen; + byte[] data1 = new byte[dataLength]; + random.nextBytes(data1); + DataRecord rec1 = ds.addRecord(new ByteArrayInputStream(data1)); + + byte[] data2 = new byte[dataLength]; + random.nextBytes(data2); + DataRecord rec2 = ds.addRecord(new ByteArrayInputStream(data2)); + + byte[] data3 = new byte[dataLength]; + random.nextBytes(data3); + DataRecord rec3 = ds.addRecord(new ByteArrayInputStream(data3)); + + ((MultiDataStoreAware)ds).deleteRecord(rec2.getIdentifier()); + + assertNull("rec2 should be null", + ds.getRecordIfStored(rec2.getIdentifier())); + assertEquals(new ByteArrayInputStream(data1), + ds.getRecord(rec1.getIdentifier()).getStream()); + assertEquals(new ByteArrayInputStream(data3), + ds.getRecord(rec3.getIdentifier()).getStream()); + ds.close(); + } + + /** + * Test {@link DataStore#getAllIdentifiers()} and asserts all identifiers + * are returned. + */ + protected void doGetAllIdentifiersTest() throws Exception { + ds = createDataStore(); + List list = new ArrayList(); + Random random = randomGen; + byte[] data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec = ds.addRecord(new ByteArrayInputStream(data)); + list.add(rec.getIdentifier()); + + data = new byte[dataLength]; + random.nextBytes(data); + rec = ds.addRecord(new ByteArrayInputStream(data)); + list.add(rec.getIdentifier()); + + data = new byte[dataLength]; + random.nextBytes(data); + rec = ds.addRecord(new ByteArrayInputStream(data)); + list.add(rec.getIdentifier()); + + Iterator itr = ds.getAllIdentifiers(); + while (itr.hasNext()) { + assertTrue("record found on list", list.remove(itr.next())); + } + assertEquals(0, list.size()); + ds.close(); + } + + /** + * Asserts that timestamp of all records accessed after + * {@link DataStore#updateModifiedDateOnAccess(long)} invocation. + */ + protected void doUpdateLastModifiedOnAccessTest() throws Exception { + ds = createDataStore(); + Random random = randomGen; + byte[] data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec1 = ds.addRecord(new ByteArrayInputStream(data)); + + data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec2 = ds.addRecord(new ByteArrayInputStream(data)); + LOG.debug("rec2 timestamp=" + rec2.getLastModified()); + + // sleep for some time to ensure that async upload completes in backend. + sleep(6000); + long updateTime = System.currentTimeMillis(); + LOG.debug("updateTime=" + updateTime); + ds.updateModifiedDateOnAccess(updateTime); + + // sleep to workaround System.currentTimeMillis granularity. + sleep(3000); + data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec3 = ds.addRecord(new ByteArrayInputStream(data)); + + data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec4 = ds.addRecord(new ByteArrayInputStream(data)); + + rec1 = ds.getRecord(rec1.getIdentifier()); + + assertEquals("rec1 touched", true, rec1.getLastModified() > updateTime); + LOG.debug("rec2 timestamp=" + rec2.getLastModified()); + assertEquals("rec2 not touched", true, + rec2.getLastModified() < updateTime); + assertEquals("rec3 touched", true, rec3.getLastModified() > updateTime); + assertEquals("rec4 touched", true, rec4.getLastModified() > updateTime); + ds.close(); + + } + + /** + * Asserts that {@link DataStore#deleteAllOlderThan(long)} only deleted + * records older than argument passed. + */ + protected void doDeleteAllOlderThan() throws Exception { + ds = createDataStore(); + Random random = randomGen; + byte[] data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec1 = ds.addRecord(new ByteArrayInputStream(data)); + + data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec2 = ds.addRecord(new ByteArrayInputStream(data)); + + // sleep for some time to ensure that async upload completes in backend. + sleep(10000); + long updateTime = System.currentTimeMillis(); + ds.updateModifiedDateOnAccess(updateTime); + + // sleep to workaround System.currentTimeMillis granularity. + sleep(3000); + data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec3 = ds.addRecord(new ByteArrayInputStream(data)); + + data = new byte[dataLength]; + random.nextBytes(data); + DataRecord rec4 = ds.addRecord(new ByteArrayInputStream(data)); + + rec1 = ds.getRecord(rec1.getIdentifier()); + ds.clearInUse(); + assertEquals("only rec2 should be deleted", 1, + ds.deleteAllOlderThan(updateTime)); + assertNull("rec2 should be null", + ds.getRecordIfStored(rec2.getIdentifier())); + + Iterator itr = ds.getAllIdentifiers(); + List list = new ArrayList(); + list.add(rec1.getIdentifier()); + list.add(rec3.getIdentifier()); + list.add(rec4.getIdentifier()); + while (itr.hasNext()) { + assertTrue("record found on list", list.remove(itr.next())); + } + + assertEquals("touched records found", 0, list.size()); + assertEquals("rec1 touched", true, rec1.getLastModified() > updateTime); + assertEquals("rec3 touched", true, rec3.getLastModified() > updateTime); + assertEquals("rec4 touched", true, rec4.getLastModified() > updateTime); + ds.close(); + } + + /** + * Test if record can be accessed via + * {@link DataStore#getRecordFromReference(String)} + */ + protected void doReferenceTest() throws Exception { + ds = createDataStore(); + byte[] data = new byte[dataLength]; + randomGen.nextBytes(data); + String reference; + DataRecord record = ds.addRecord(new ByteArrayInputStream(data)); + reference = record.getReference(); + assertReference(data, reference, ds); + ds.close(); + } + + /** + * Method to validate mixed scenario use of {@link DataStore}. + */ + protected void doTestSingleThread() throws Exception { + ds = createDataStore(); + doTestMultiThreaded(ds, 1); + ds.close(); + } + + /** + * Method to validate mixed scenario use of {@link DataStore} in + * multi-threaded concurrent environment. + */ + protected void doTestMultiThreaded() throws Exception { + ds = createDataStore(); + doTestMultiThreaded(ds, 4); + ds.close(); + } + + /** + * Method to assert record with byte array. + */ + protected void assertRecord(byte[] expected, DataRecord record) + throws DataStoreException, IOException { + InputStream stream = record.getStream(); + try { + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i] & 0xff, stream.read()); + } + assertEquals(-1, stream.read()); + } finally { + stream.close(); + } + } + + /** + * Method to run {@link TestCaseBase#doTest(DataStore, int)} in multiple + * concurrent threads. + */ + protected void doTestMultiThreaded(final DataStore ds, int threadCount) + throws Exception { + final Exception[] exception = new Exception[1]; + Thread[] threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) { + final int x = i; + Thread t = new Thread() { + public void run() { + try { + doTest(ds, x); + } catch (Exception e) { + exception[0] = e; + } + } + }; + threads[i] = t; + t.start(); + } + for (int i = 0; i < threadCount; i++) { + threads[i].join(); + } + if (exception[0] != null) { + throw exception[0]; + } + } + + /** + * Assert randomly read stream from record. + */ + void doTest(DataStore ds, int offset) throws Exception { + ArrayList list = new ArrayList(); + HashMap map = new HashMap(); + for (int i = 0; i < 10; i++) { + int size = 100000 - (i * 100); + RandomInputStream in = new RandomInputStream(size + offset, size); + DataRecord rec = ds.addRecord(in); + list.add(rec); + map.put(rec, new Integer(size)); + } + Random random = new Random(1); + for (int i = 0; i < list.size(); i++) { + int pos = random.nextInt(list.size()); + DataRecord rec = list.get(pos); + int size = map.get(rec); + rec = ds.getRecord(rec.getIdentifier()); + assertEquals(size, rec.getLength()); + RandomInputStream expected = new RandomInputStream(size + offset, + size); + InputStream in = rec.getStream(); + // Workaround for race condition that can happen with low cache size relative to the test + // read immediately + byte[] buffer = new byte[1]; + in.read(buffer); + in = new SequenceInputStream(new ByteArrayInputStream(buffer), in); + + if (random.nextBoolean()) { + in = readInputStreamRandomly(in, random); + } + assertEquals(expected, in); + } + } + + InputStream readInputStreamRandomly(InputStream in, Random random) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buffer = new byte[8000]; + while (true) { + if (random.nextBoolean()) { + int x = in.read(); + if (x < 0) { + break; + } + out.write(x); + } else { + if (random.nextBoolean()) { + int l = in.read(buffer); + if (l < 0) { + break; + } + out.write(buffer, 0, l); + } else { + int offset = random.nextInt(buffer.length / 2); + int len = random.nextInt(buffer.length / 2); + int l = in.read(buffer, offset, len); + if (l < 0) { + break; + } + out.write(buffer, offset, l); + } + } + } + in.close(); + return new ByteArrayInputStream(out.toByteArray()); + } + + /** + * Assert two inputstream + */ + protected void assertEquals(InputStream a, InputStream b) + throws IOException { + try { + assertTrue("binary not equal", + org.apache.commons.io.IOUtils.contentEquals(a, b)); + } finally { + try { + a.close(); + } catch (Exception ignore) { + } + try { + b.close(); + } catch (Exception ignore) { + } + } + } + + /** + * Assert inputstream read from reference. + */ + protected void assertReference(byte[] expected, String reference, + DataStore store) throws Exception { + DataRecord record = store.getRecordFromReference(reference); + assertNotNull(record); + assertEquals(expected.length, record.getLength()); + + InputStream stream = record.getStream(); + try { + assertTrue("binary not equal", + org.apache.commons.io.IOUtils.contentEquals( + new ByteArrayInputStream(expected), stream)); + } finally { + stream.close(); + } + } + + /** + * Utility method to stop execution for duration time. + * + * @param duration + * time in milli seconds + */ + protected void sleep(long duration) { + long expected = System.currentTimeMillis() + duration; + while (System.currentTimeMillis() < expected) { + try { + Thread.sleep(1); + } catch (InterruptedException ie) { + + } + } + } + + /** + * Return {@link Properties} from class resource. Return empty + * {@link Properties} if not found. + */ + protected Properties loadProperties(String resource) { + Properties configProp = new Properties(); + try { + configProp.load(this.getClass().getResourceAsStream(resource)); + } catch (Exception ignore) { + + } + return configProp; + } +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestFileDataStore.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestFileDataStore.java new file mode 100644 index 00000000000..facc025fc7d --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestFileDataStore.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.core.data; + +import java.io.File; +import java.util.Properties; + +import javax.jcr.RepositoryException; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test cases to test {@link FileDataStore} + */ +public class TestFileDataStore extends TestCaseBase { + + protected static final Logger LOG = LoggerFactory.getLogger(TestFileDataStore.class); + + String fsPath; + + @Override + protected DataStore createDataStore() throws RepositoryException { + FileDataStore fds = new FileDataStore(); + Properties props = loadProperties("/fs.properties"); + String pathValue = props.getProperty(FSBackend.FS_BACKEND_PATH); + if (pathValue != null && !"".equals(pathValue.trim())) { + fsPath = pathValue + "/fds" + "-" + + String.valueOf(randomGen.nextInt(100000)) + "-" + + String.valueOf(randomGen.nextInt(100000)); + } else { + fsPath = dataStoreDir + "/repository/datastore"; + } + LOG.info("path [{}] set.", fsPath); + fds.setPath(fsPath); + fds.init(dataStoreDir); + return fds; + } + + @Override + protected void tearDown() { + LOG.info("cleaning fsPath [{}]", fsPath); + File f = new File(fsPath); + try { + for (int i = 0; i < 4 && f.exists(); i++) { + FileUtils.deleteQuietly(f); + Thread.sleep(2000); + } + } catch (Exception ignore) { + + } + super.tearDown(); + } +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestInMemDs.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestInMemDs.java new file mode 100644 index 00000000000..d63b2b62bda --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestInMemDs.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.core.data; + +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test {@link CachingDataStore} with InMemoryBackend and local cache on. + */ +public class TestInMemDs extends TestCaseBase { + + protected static final Logger LOG = LoggerFactory.getLogger(TestInMemDs.class); + + @Override + protected DataStore createDataStore() throws RepositoryException { + InMemoryDataStore inMemDS = new InMemoryDataStore(); + inMemDS.setProperties(null); + inMemDS.init(dataStoreDir); + inMemDS.setSecret("12345"); + return inMemDS; + } + + +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestInMemDsCacheOff.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestInMemDsCacheOff.java new file mode 100644 index 00000000000..1b5f9568003 --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestInMemDsCacheOff.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.core.data; + +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test {@link CachingDataStore} with InMemoryBackend and local cache off. + */ + +public class TestInMemDsCacheOff extends TestCaseBase { + + protected static final Logger LOG = LoggerFactory.getLogger(TestInMemDsCacheOff.class); + @Override + protected DataStore createDataStore() throws RepositoryException { + InMemoryDataStore inMemDS = new InMemoryDataStore(); + inMemDS.setProperties(null); + inMemDS.init(dataStoreDir); + inMemDS.setSecret("12345"); + inMemDS.setCacheSize(0); + return inMemDS; + } +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestLocalCache.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestLocalCache.java new file mode 100644 index 00000000000..c376c67726e --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/TestLocalCache.java @@ -0,0 +1,404 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.core.data; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.core.data.util.NamedThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Testcase to test local cache. + */ +public class TestLocalCache extends TestCase { + + private static final String CACHE_DIR = "target/cache"; + + private static final String TEMP_DIR = "target/temp"; + + private static final String TARGET_DIR = "target"; + + protected String cacheDirPath; + + protected String tempDirPath; + + /** + * Random number generator to populate data + */ + protected Random randomGen = new Random(); + + private static final Logger LOG = LoggerFactory.getLogger(TestLocalCache.class); + + @Override + protected void setUp() { + try { + cacheDirPath = CACHE_DIR + "-" + + String.valueOf(randomGen.nextInt(9999)) + "-" + + String.valueOf(randomGen.nextInt(9999)); + File cachedir = new File(cacheDirPath); + for (int i = 0; i < 4 && cachedir.exists(); i++) { + FileUtils.deleteQuietly(cachedir); + Thread.sleep(1000); + } + cachedir.mkdirs(); + + tempDirPath = TEMP_DIR + "-" + + String.valueOf(randomGen.nextInt(9999)) + "-" + + String.valueOf(randomGen.nextInt(9999)); + File tempdir = new File(tempDirPath); + for (int i = 0; i < 4 && tempdir.exists(); i++) { + FileUtils.deleteQuietly(tempdir); + Thread.sleep(1000); + } + tempdir.mkdirs(); + } catch (Exception e) { + LOG.error("error:", e); + fail(); + } + } + + @Override + protected void tearDown() throws Exception { + File cachedir = new File(cacheDirPath); + for (int i = 0; i < 4 && cachedir.exists(); i++) { + FileUtils.deleteQuietly(cachedir); + Thread.sleep(1000); + } + + File tempdir = new File(tempDirPath); + for (int i = 0; i < 4 && tempdir.exists(); i++) { + FileUtils.deleteQuietly(tempdir); + Thread.sleep(1000); + } + } + + /** + * Test to validate store retrieve in cache. + */ + public void testStoreRetrieve() { + try { + AsyncUploadCache pendingFiles = new AsyncUploadCache(); + pendingFiles.init(tempDirPath, cacheDirPath, 100); + pendingFiles.reset(); + LocalCache cache = new LocalCache(cacheDirPath, tempDirPath, 400, + 0.95, 0.70, pendingFiles); + Random random = new Random(12345); + byte[] data = new byte[100]; + Map byteMap = new HashMap(); + random.nextBytes(data); + byteMap.put("a1", data); + + data = new byte[100]; + random.nextBytes(data); + byteMap.put("a2", data); + + data = new byte[100]; + random.nextBytes(data); + byteMap.put("a3", data); + + cache.store("a1", new ByteArrayInputStream(byteMap.get("a1"))); + cache.store("a2", new ByteArrayInputStream(byteMap.get("a2"))); + cache.store("a3", new ByteArrayInputStream(byteMap.get("a3"))); + InputStream result = cache.getIfStored("a1"); + assertEquals(new ByteArrayInputStream(byteMap.get("a1")), result); + IOUtils.closeQuietly(result); + result = cache.getIfStored("a2"); + assertEquals(new ByteArrayInputStream(byteMap.get("a2")), result); + IOUtils.closeQuietly(result); + result = cache.getIfStored("a3"); + assertEquals(new ByteArrayInputStream(byteMap.get("a3")), result); + IOUtils.closeQuietly(result); + } catch (Exception e) { + LOG.error("error:", e); + fail(); + } + } + + /** + * Test to verify cache's purging if cache current size exceeds + * cachePurgeTrigFactor * size. + */ + public void testAutoPurge() { + try { + AsyncUploadCache pendingFiles = new AsyncUploadCache(); + pendingFiles.init(tempDirPath, cacheDirPath, 100); + pendingFiles.reset(); + LocalCache cache = new LocalCache(cacheDirPath, tempDirPath, 400, + 0.95, 0.70, pendingFiles); + Random random = new Random(12345); + byte[] data = new byte[100]; + Map byteMap = new HashMap(); + random.nextBytes(data); + byteMap.put("a1", data); + + data = new byte[100]; + random.nextBytes(data); + byteMap.put("a2", data); + + data = new byte[100]; + random.nextBytes(data); + byteMap.put("a3", data); + + data = new byte[100]; + random.nextBytes(data); + byteMap.put("a4", data); + + cache.store("a1", new ByteArrayInputStream(byteMap.get("a1"))); + cache.store("a2", new ByteArrayInputStream(byteMap.get("a2"))); + cache.store("a3", new ByteArrayInputStream(byteMap.get("a3"))); + + InputStream result = cache.getIfStored("a1"); + assertEquals(new ByteArrayInputStream(byteMap.get("a1")), result); + IOUtils.closeQuietly(result); + result = cache.getIfStored("a2"); + assertEquals(new ByteArrayInputStream(byteMap.get("a2")), result); + IOUtils.closeQuietly(result); + result = cache.getIfStored("a3"); + assertEquals(new ByteArrayInputStream(byteMap.get("a3")), result); + IOUtils.closeQuietly(result); + + data = new byte[90]; + random.nextBytes(data); + byteMap.put("a4", data); + // storing a4 should purge cache + cache.store("a4", new ByteArrayInputStream(byteMap.get("a4"))); + do { + Thread.sleep(1000); + } while (cache.isInPurgeMode()); + + result = cache.getIfStored("a1"); + assertNull("a1 should be null", result); + IOUtils.closeQuietly(result); + + result = cache.getIfStored("a2"); + assertNull("a2 should be null", result); + IOUtils.closeQuietly(result); + + result = cache.getIfStored("a3"); + assertEquals(new ByteArrayInputStream(byteMap.get("a3")), result); + IOUtils.closeQuietly(result); + + result = cache.getIfStored("a4"); + assertEquals(new ByteArrayInputStream(byteMap.get("a4")), result); + IOUtils.closeQuietly(result); + + data = new byte[100]; + random.nextBytes(data); + byteMap.put("a5", data); + cache.store("a5", new ByteArrayInputStream(byteMap.get("a5"))); + result = cache.getIfStored("a3"); + assertEquals(new ByteArrayInputStream(byteMap.get("a3")), result); + IOUtils.closeQuietly(result); + } catch (Exception e) { + LOG.error("error:", e); + fail(); + } + } + + /** + * Test to verify cache's purging if cache current size exceeds + * cachePurgeTrigFactor * size. + */ + public void testAutoPurgeWithPendingUpload() { + try { + AsyncUploadCache pendingFiles = new AsyncUploadCache(); + pendingFiles.init(tempDirPath, cacheDirPath, 100); + pendingFiles.reset(); + LocalCache cache = new LocalCache(cacheDirPath, tempDirPath, 400, + 0.95, 0.70, pendingFiles); + Random random = new Random(12345); + byte[] data = new byte[125]; + Map byteMap = new HashMap(); + random.nextBytes(data); + byteMap.put("a1", data); + + data = new byte[125]; + random.nextBytes(data); + byteMap.put("a2", data); + + data = new byte[125]; + random.nextBytes(data); + byteMap.put("a3", data); + + data = new byte[100]; + random.nextBytes(data); + byteMap.put("a4", data); + File tempDir = new File(tempDirPath); + File f = File.createTempFile("test", "tmp", tempDir); + FileOutputStream fos = new FileOutputStream(f); + fos.write(byteMap.get("a1")); + fos.close(); + AsyncUploadCacheResult result = cache.store("a1", f, true); + assertTrue("should be able to add to pending upload", + result.canAsyncUpload()); + + f = File.createTempFile("test", "tmp", tempDir); + fos = new FileOutputStream(f); + fos.write(byteMap.get("a2")); + fos.close(); + result = cache.store("a2", f, true); + assertTrue("should be able to add to pending upload", + result.canAsyncUpload()); + + f = File.createTempFile("test", "tmp", tempDir); + fos = new FileOutputStream(f); + fos.write(byteMap.get("a3")); + fos.close(); + result = cache.store("a3", f, true); + assertTrue("should be able to add to pending upload", + result.canAsyncUpload()); + + InputStream inp = cache.getIfStored("a1"); + assertEquals(new ByteArrayInputStream(byteMap.get("a1")), inp); + IOUtils.closeQuietly(inp); + inp = cache.getIfStored("a2"); + assertEquals(new ByteArrayInputStream(byteMap.get("a2")), inp); + IOUtils.closeQuietly(inp); + inp = cache.getIfStored("a3"); + assertEquals(new ByteArrayInputStream(byteMap.get("a3")), inp); + IOUtils.closeQuietly(inp); + + data = new byte[90]; + random.nextBytes(data); + byteMap.put("a4", data); + + f = File.createTempFile("test", "tmp", tempDir); + fos = new FileOutputStream(f); + fos.write(byteMap.get("a4")); + fos.close(); + + result = cache.store("a4", f, true); + assertFalse("should not be able to add to pending upload", + result.canAsyncUpload()); + Thread.sleep(1000); + + inp = cache.getIfStored("a1"); + assertEquals(new ByteArrayInputStream(byteMap.get("a1")), inp); + IOUtils.closeQuietly(inp); + inp = cache.getIfStored("a2"); + assertEquals(new ByteArrayInputStream(byteMap.get("a2")), inp); + IOUtils.closeQuietly(inp); + inp = cache.getIfStored("a3"); + assertEquals(new ByteArrayInputStream(byteMap.get("a3")), inp); + IOUtils.closeQuietly(inp); + inp = cache.getIfStored("a4"); + assertNull("a4 should be null", inp); + } catch (Exception e) { + LOG.error("error:", e); + fail(); + } + } + + /** + * Test concurrent {@link LocalCache} initialization with storing + * {@link LocalCache} + */ + public void testConcurrentInitWithStore() { + try { + AsyncUploadCache pendingFiles = new AsyncUploadCache(); + pendingFiles.init(tempDirPath, cacheDirPath, 100); + pendingFiles.reset(); + LocalCache cache = new LocalCache(cacheDirPath, tempDirPath, + 10000000, 0.95, 0.70, pendingFiles); + Random random = new Random(12345); + int fileUploads = 1000; + Map byteMap = new HashMap( + fileUploads); + byte[] data; + for (int i = 0; i < fileUploads; i++) { + data = new byte[100]; + random.nextBytes(data); + String key = "a" + i; + byteMap.put(key, data); + cache.store(key, new ByteArrayInputStream(byteMap.get(key))); + } + cache.close(); + + ExecutorService executor = Executors.newFixedThreadPool(10, + new NamedThreadFactory("localcache-store-worker")); + cache = new LocalCache(cacheDirPath, tempDirPath, 10000000, 0.95, + 0.70, pendingFiles); + executor.execute(new StoreWorker(cache, byteMap)); + executor.shutdown(); + while (!executor.awaitTermination(15, TimeUnit.SECONDS)) { + } + } catch (Exception e) { + LOG.error("error:", e); + fail(); + } + } + + private class StoreWorker implements Runnable { + Map byteMap; + + LocalCache cache; + + Random random; + + private StoreWorker(LocalCache cache, Map byteMap) { + this.byteMap = byteMap; + this.cache = cache; + random = new Random(byteMap.size()); + } + + public void run() { + try { + for (int i = 0; i < 100; i++) { + String key = "a" + random.nextInt(byteMap.size()); + LOG.debug("key=" + key); + cache.store(key, new ByteArrayInputStream(byteMap.get(key))); + } + } catch (Exception e) { + LOG.error("error:", e); + fail(); + } + } + } + + /** + * Assert two inputstream + */ + protected void assertEquals(InputStream a, InputStream b) + throws IOException { + while (true) { + int ai = a.read(); + int bi = b.read(); + assertEquals(ai, bi); + if (ai < 0) { + break; + } + } + IOUtils.closeQuietly(a); + IOUtils.closeQuietly(b); + } + +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/db/ResettableTempFileInputStreamTest.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/db/ResettableTempFileInputStreamTest.java new file mode 100644 index 00000000000..154a7ee2a63 --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/db/ResettableTempFileInputStreamTest.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.core.data.db; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import junit.framework.TestCase; +import static org.junit.Assume.assumeTrue; + +public class ResettableTempFileInputStreamTest extends TestCase { + + public void testResetStreamAllowsReadAgain() throws Exception { + final File tmp = createTemporaryFileWithContents(new byte[1]); + ResettableTempFileInputStream in = null; + try { + in = new ResettableTempFileInputStream(tmp); + assertEquals(0, in.read()); + assertEquals(-1, in.read()); + in.reset(); + assertEquals(0, in.read()); + assertEquals(-1, in.read()); + } finally { + IOUtils.closeQuietly(in); + } + } + + public void testMarkStreamAllowsReadFromMark() throws Exception { + final File tmp = createTemporaryFileWithContents(createTestByteArray()); + ResettableTempFileInputStream in = null; + try { + in = new ResettableTempFileInputStream(tmp); + assumeTrue(in.read(new byte[5]) == 5); + in.mark(Integer.MAX_VALUE); + assumeTrue(in.read(new byte[5]) == 5); + in.reset(); + assertEquals(5, in.read()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private File createTemporaryFileWithContents(byte[] data) throws IOException { + final File tmp = File.createTempFile("test", ".bin"); + FileUtils.writeByteArrayToFile(tmp, data); + return tmp; + } + + private byte[] createTestByteArray() { + byte[] bytes = new byte[10]; + for (int i = 0; i < 10; i++) { + bytes[i] = (byte) i; + } + return bytes; + } +} diff --git a/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/db/TempFileInputStreamTest.java b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/db/TempFileInputStreamTest.java new file mode 100644 index 00000000000..7e0bfbcaf95 --- /dev/null +++ b/jackrabbit-data/src/test/java/org/apache/jackrabbit/core/data/db/TempFileInputStreamTest.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.core.data.db; + +import java.io.File; + +import junit.framework.TestCase; + +public class TempFileInputStreamTest extends TestCase { + + private File tmp; + private TempFileInputStream in; + + + @Override + protected void setUp() throws Exception { + super.setUp(); + tmp = File.createTempFile("test", ".bin"); + in = new TempFileInputStream(tmp); + } + + public void testFileIsDeletedWhenStreamIsClosed() throws Exception { + assertTrue(tmp.exists()); + in.close(); + assertFalse(tmp.exists()); + } + +} diff --git a/jackrabbit-data/src/test/resources/fs.properties b/jackrabbit-data/src/test/resources/fs.properties new file mode 100644 index 00000000000..9c314cb8206 --- /dev/null +++ b/jackrabbit-data/src/test/resources/fs.properties @@ -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. +# +fsBackendPath= diff --git a/jackrabbit-data/src/test/resources/log4j.properties b/jackrabbit-data/src/test/resources/log4j.properties new file mode 100644 index 00000000000..86450359ab6 --- /dev/null +++ b/jackrabbit-data/src/test/resources/log4j.properties @@ -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. +# + +# this is the log4j configuration for the JCR API tests +log4j.rootLogger=INFO, file + +#log4j.logger.org.apache.jackrabbit.test=DEBUG + +# 'file' is set to be a FileAppender. +log4j.appender.file=org.apache.log4j.FileAppender +log4j.appender.file.File=target/debug.log + +# 'file' uses PatternLayout. +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n diff --git a/jackrabbit-jca/HEADER.txt b/jackrabbit-jca/HEADER.txt new file mode 100644 index 00000000000..ae6f28c4a1c --- /dev/null +++ b/jackrabbit-jca/HEADER.txt @@ -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. + */ diff --git a/jackrabbit-jca/README.txt b/jackrabbit-jca/README.txt new file mode 100644 index 00000000000..85438e31053 --- /dev/null +++ b/jackrabbit-jca/README.txt @@ -0,0 +1,11 @@ +========================================== +Welcome to Jackrabbit JCA Resource Adapter +========================================== + +This is the JCA Resource Adapter component of the Apache Jackrabbit project. +This component packages the Jackrabbit content repository as a JCA 1.0 resource +adapter. The packaged adapter can be deployed on a wide range of application +servers. See the "deploy" directory for example deployment configurations. + +The Jackrabbit content repository embedded in the JCA package is started when +first accessed and shut down when the adapter is un- or re-deployed. diff --git a/jackrabbit-jca/deploy/geronimo/geronimo-ra.xml b/jackrabbit-jca/deploy/geronimo/geronimo-ra.xml new file mode 100644 index 00000000000..93540386895 --- /dev/null +++ b/jackrabbit-jca/deploy/geronimo/geronimo-ra.xml @@ -0,0 +1,57 @@ + + + + + + + org.apache.jackrabbit + jackrabbit-jca + 1.0 + rar + + + + javax.jcr + jcr + 2.0 + jar + + + + + + + + javax.jcr.Repository + + jackrabbit + jcr-jackrabbit://jackrabbit + + + + 10 + 0 + + + + + + + + + diff --git a/jackrabbit-jca/deploy/jboss/jcr-ds.xml b/jackrabbit-jca/deploy/jboss/jcr-ds.xml new file mode 100644 index 00000000000..4803e7003c9 --- /dev/null +++ b/jackrabbit-jca/deploy/jboss/jcr-ds.xml @@ -0,0 +1,27 @@ + + + + + jcr/local + + jackrabbit-jca.rar + javax.jcr.Repository + jcr-jackrabbit://jackrabbit + true + + diff --git a/jackrabbit-jca/deploy/weblogic/weblogic-ra.xml b/jackrabbit-jca/deploy/weblogic/weblogic-ra.xml new file mode 100644 index 00000000000..471803083e8 --- /dev/null +++ b/jackrabbit-jca/deploy/weblogic/weblogic-ra.xml @@ -0,0 +1,52 @@ + + + + + jackrabbitRA + + true + + true + + + + + javax.jcr.Repository + + + jackrabbit + + + 2 + 10 + 1 + true + 60 + + + + RepositoryURI + jcr-jackrabbit://jackrabbit + + + + + + + + diff --git a/jackrabbit-jca/pom.xml b/jackrabbit-jca/pom.xml new file mode 100644 index 00000000000..3b3f898a307 --- /dev/null +++ b/jackrabbit-jca/pom.xml @@ -0,0 +1,138 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-jca + rar + Jackrabbit JCA Resource Adapter + + A resource adapter for Jackrabbit as specified by JCA 1.0 and 1.5. + + + + + + maven-jar-plugin + + + package-jca-jar + + jar + + + test + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/${project.build.finalName}.jar + jar + + + + + + + + maven-surefire-plugin + + + + derby.system.durability + test + + + derby.stream.error.file + target/derby.log + + + + + + + + + + javax.jcr + jcr + + + org.apache.geronimo.specs + geronimo-j2ee-connector_1.5_spec + provided + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + + + org.apache.tika + tika-parsers + + + commons-logging + commons-logging + + + + + org.slf4j + jcl-over-slf4j + + + ch.qos.logback + logback-classic + + + junit + junit + test + + + + diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/AnonymousConnection.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/AnonymousConnection.java new file mode 100644 index 00000000000..02b277578d4 --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/AnonymousConnection.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jca; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import javax.security.auth.Subject; +import javax.resource.spi.ConnectionRequestInfo; +import javax.resource.spi.ManagedConnection; +import javax.resource.spi.ConnectionEventListener; +import javax.resource.spi.LocalTransaction; +import javax.resource.spi.ManagedConnectionMetaData; +import javax.resource.ResourceException; +import java.io.PrintWriter; + +/** + * Implements a ManagedConnection for an anonymous user, + * where no ConnectionRequestInfo has been specified. + * + * @see JCAManagedConnectionFactory#createManagedConnection + */ +public class AnonymousConnection implements ManagedConnection, XAResource { + + /** + * Default transaction timeout, in seconds. + */ + private static final int DEFAULT_TX_TIMEOUT = 5; + + /** + * Timeout explicitely set. + */ + private int timeout; + + /** + * Log writer. + */ + private PrintWriter logWriter; + + //------------------------------------------------------- ManagedConnection + + /** + * {@inheritDoc} + */ + public XAResource getXAResource() throws ResourceException { + return this; + } + + /** + * {@inheritDoc} + */ + public void cleanup() throws ResourceException { + } + + /** + * {@inheritDoc} + */ + public void destroy() throws ResourceException { + } + + /** + * {@inheritDoc} + */ + public void setLogWriter(PrintWriter logWriter) throws ResourceException { + this.logWriter = logWriter; + } + + /** + * {@inheritDoc} + */ + public PrintWriter getLogWriter() throws ResourceException { + return logWriter; + } + + /** + * {@inheritDoc} + */ + public void addConnectionEventListener(ConnectionEventListener listener) { + // ignored + } + + /** + * {@inheritDoc} + */ + public void removeConnectionEventListener(ConnectionEventListener listener) { + // ignored + } + + //--------------------------------------------------------- not implemented + + public Object getConnection(Subject subject, ConnectionRequestInfo cri) + throws ResourceException { + + String msg = "No connection allowed for anonymous user."; + throw new UnsupportedOperationException(msg); + } + + public void associateConnection(Object o) throws ResourceException { + String msg = "Associating a connection not supported."; + throw new UnsupportedOperationException(msg); + } + + public LocalTransaction getLocalTransaction() throws ResourceException { + String msg = "Local transactions not supported."; + throw new UnsupportedOperationException(msg); + } + + public ManagedConnectionMetaData getMetaData() throws ResourceException { + String msg = "Retrieving meta data not supported."; + throw new UnsupportedOperationException(msg); + } + + //-------------------------------------------------------------- XAResource + + /** + * {@inheritDoc} + */ + public Xid[] recover(int flags) throws XAException { + return new Xid[0]; + } + + /** + * {@inheritDoc} + */ + public int getTransactionTimeout() throws XAException { + return timeout == 0 ? DEFAULT_TX_TIMEOUT : timeout; + } + + /** + * {@inheritDoc} + */ + public boolean setTransactionTimeout(int timeout) throws XAException { + this.timeout = timeout; + return true; + } + + /** + * {@inheritDoc} + */ + public boolean isSameRM(XAResource xares) throws XAException { + return xares instanceof AnonymousConnection; + } + + //--------------------------------------------------------- not implemented + + public void start(Xid xid, int flags) throws XAException { + throw new XAException(XAException.XAER_RMFAIL); + } + + public void end(Xid xid, int flags) throws XAException { + throw new XAException(XAException.XAER_NOTA); + } + + public void forget(Xid xid) throws XAException { + throw new XAException(XAException.XAER_NOTA); + } + + public int prepare(Xid xid) throws XAException { + throw new XAException(XAException.XAER_NOTA); + } + + public void commit(Xid xid, boolean arg1) throws XAException { + throw new XAException(XAException.XAER_NOTA); + } + + public void rollback(Xid xid) throws XAException { + throw new XAException(XAException.XAER_NOTA); + } +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAConnectionManager.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAConnectionManager.java new file mode 100644 index 00000000000..b65791ffdb3 --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAConnectionManager.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.jca; + +import javax.resource.ResourceException; +import javax.resource.spi.ConnectionManager; +import javax.resource.spi.ConnectionRequestInfo; +import javax.resource.spi.ManagedConnection; +import javax.resource.spi.ManagedConnectionFactory; + +/** + * This class implements the default connection manager. + */ +public final class JCAConnectionManager implements ConnectionManager { + + private static final long serialVersionUID = 1479445982219812432L; + + /** + * The method allocateConnection gets called by the resource adapter's + * connection factory instance. + */ + public Object allocateConnection(ManagedConnectionFactory mcf, ConnectionRequestInfo cri) + throws ResourceException { + ManagedConnection mc = mcf.createManagedConnection(null, cri); + return mc.getConnection(null, cri); + } +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAConnectionRequestInfo.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAConnectionRequestInfo.java new file mode 100644 index 00000000000..23375a54370 --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAConnectionRequestInfo.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jca; + +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import javax.resource.spi.ConnectionRequestInfo; +import java.util.HashMap; +import java.util.Map; + +/** + * This class encapsulates the credentials for creating a + * session from the repository. + */ +public final class JCAConnectionRequestInfo implements ConnectionRequestInfo { + + /** + * Credentials. + */ + private final Credentials creds; + + /** + * Workspace. + */ + private final String workspace; + + /** + * Construct the request info. + */ + public JCAConnectionRequestInfo(JCAConnectionRequestInfo cri) { + this(cri.creds, cri.workspace); + } + + /** + * Construct the request info. + */ + public JCAConnectionRequestInfo(Credentials creds, String workspace) { + this.creds = creds; + this.workspace = workspace; + } + + /** + * Return the workspace. + */ + public String getWorkspace() { + return workspace; + } + + /** + * Return the credentials. + */ + public Credentials getCredentials() { + return creds; + } + + /** + * Return the hash code. + */ + public int hashCode() { + int hash1 = workspace != null ? workspace.hashCode() : 0; + int hash2 = creds != null ? computeCredsHashCode(creds) : 0; + return hash1 ^ hash2; + } + + /** + * Return true if equals. + */ + public boolean equals(Object o) { + if (o == this) { + return true; + } else if (o instanceof JCAConnectionRequestInfo) { + return equals((JCAConnectionRequestInfo) o); + } else { + return false; + } + } + + /** + * Return true if equals. + */ + private boolean equals(JCAConnectionRequestInfo o) { + return equals(workspace, o.workspace) + && equals(creds, o.creds); + } + + /** + * Return true if equals. + */ + private boolean equals(Object o1, Object o2) { + if (o1 == o2) { + return true; + } else if ((o1 != null) && (o2 != null)) { + return o1.equals(o2); + } else { + return false; + } + } + + /** + * Return true if equals. + */ + private boolean equals(char[] o1, char[] o2) { + if (o1 == o2) { + return true; + } else if ((o1 != null) && (o2 != null)) { + return equals(new String(o1), new String(o2)); + } else { + return false; + } + } + + /** + * Return true if equals. + */ + private boolean equals(Credentials o1, Credentials o2) { + if (o1 == o2) { + return true; + } else if ((o1 != null) && (o2 != null)) { + if ((o1 instanceof SimpleCredentials) && (o2 instanceof SimpleCredentials)) { + return equals((SimpleCredentials) o1, (SimpleCredentials) o2); + } else { + return o1.equals(o2); + } + } else { + return false; + } + } + + /** + * This method compares two simple credentials. + */ + private boolean equals(SimpleCredentials o1, SimpleCredentials o2) { + if (!equals(o1.getUserID(), o2.getUserID())) { + return false; + } + + if (!equals(o1.getPassword(), o2.getPassword())) { + return false; + } + + Map m1 = getAttributeMap(o1); + Map m2 = getAttributeMap(o2); + return m1.equals(m2); + } + + /** + * Return the credentials attributes. + */ + private Map getAttributeMap(SimpleCredentials creds) { + HashMap map = new HashMap(); + String[] keys = creds.getAttributeNames(); + + for (int i = 0; i < keys.length; i++) { + map.put(keys[i], creds.getAttribute(keys[i])); + } + + return map; + } + + /** + * Returns Credentials instance hash code. Handles instances of + * SimpleCredentials in a special way. + */ + private int computeCredsHashCode(Credentials c) { + if (c instanceof SimpleCredentials) { + return computeSimpleCredsHashCode((SimpleCredentials) c); + } + return c.hashCode(); + } + + /** + * Computes hash code of a SimpleCredentials instance. Ignores its own + * hashCode() method because it's not overridden in SimpleCredentials. + */ + private int computeSimpleCredsHashCode(SimpleCredentials c) { + String userID = c.getUserID(); + char[] password = c.getPassword(); + Map m = getAttributeMap(c); + final int prime = 31; + int result = 1; + result = prime * result + ((userID == null) ? 0 : userID.hashCode()); + for (int i = 0; i < password.length; i++) { + result = prime * result + password[i]; + } + result = prime * result + ((m == null) ? 0 : m.hashCode()); + return result; + } + + @Override + public String toString() { + return "workspace (" + workspace + ") creds (" + creds + ")"; + } +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAManagedConnection.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAManagedConnection.java new file mode 100644 index 00000000000..c7f6bb491c7 --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAManagedConnection.java @@ -0,0 +1,546 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jca; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.resource.ResourceException; +import javax.resource.spi.ConnectionEvent; +import javax.resource.spi.ConnectionEventListener; +import javax.resource.spi.ConnectionRequestInfo; +import javax.resource.spi.LocalTransaction; +import javax.resource.spi.ManagedConnection; +import javax.resource.spi.ManagedConnectionMetaData; +import javax.security.auth.Subject; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import java.io.PrintWriter; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class implements the managed connection for + * this resource adapter. + */ +public class JCAManagedConnection + implements ManagedConnection, ManagedConnectionMetaData { + + /** + * The LocalTransactionAdapter wraps the internal XAResource and uses the XA Method's to + * fulfill the LocalTransaction calls. + */ + private static class LocalTransactionAdapter implements javax.resource.spi.LocalTransaction { + + /** + * Internal {@link Xid} implementation. + */ + class XidImpl implements Xid { + + private final byte[] globalTxId; + + public XidImpl(byte[] globalTxId) { + this.globalTxId = globalTxId; + } + + /** + * {@inheritDoc} + */ + public int getFormatId() { + return 0; + } + + /** + * {@inheritDoc} + */ + public byte[] getBranchQualifier() { + return new byte[0]; + } + + /** + * {@inheritDoc} + */ + public byte[] getGlobalTransactionId() { + return globalTxId; + } + } + + /** + * Global static counter for the internal Xid's + */ + private static AtomicInteger globalCounter = new AtomicInteger(); + + private XAResource resource; + private Xid xid; + + public LocalTransactionAdapter(XAResource xaResource) { + this.resource = xaResource; + } + + /** + * {@inheritDoc} + */ + @Override + public void begin() throws ResourceException { + try { + this.xid = new XidImpl(intToByteArray(globalCounter.getAndIncrement())); + resource.start(xid, XAResource.TMNOFLAGS); + } catch (XAException e) { + throw new ResourceException(e.getMessage()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void commit() throws ResourceException { + try { + resource.end(xid, XAResource.TMSUCCESS); + resource.commit(xid, true); + } catch (XAException e) { + throw new ResourceException(e.getMessage()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void rollback() throws ResourceException { + try { + resource.end(xid, XAResource.TMFAIL); + resource.rollback(xid); + } catch (XAException e) { + throw new ResourceException(e.getMessage()); + } + } + + /** + * Converts the given int (global transaction id) to a byte[] + * + * @param value + * @return byte[] + */ + private static byte[] intToByteArray(int value) { + byte[] b = new byte[4]; + for (int i = 0; i < 4; i++) { + int offset = (b.length - 1 - i) * 8; + b[i] = (byte) ((value >>> offset) & 0xFF); + } + return b; + } + } + + /** + * Managed connection factory. + */ + private final JCAManagedConnectionFactory mcf; + + /** + * Connection request info. + */ + private final JCAConnectionRequestInfo cri; + + /** + * Session instance. + */ + private Session session; + + /** + * XAResource instance. + */ + private XAResource xaResource; + + /** + * Listeners. + */ + private final LinkedList listeners; + + /** + * Handles. + */ + private final LinkedList handles; + + /** + * Log writer. + */ + private PrintWriter logWriter; + + private LocalTransactionAdapter localTransactionAdapter; + + /** + * Construct the managed connection. + */ + public JCAManagedConnection( + JCAManagedConnectionFactory mcf, JCAConnectionRequestInfo cri) + throws ResourceException { + this.mcf = mcf; + this.cri = cri; + this.session = openSession(); + this.listeners = new LinkedList(); + this.handles = new LinkedList(); + if (this.mcf.getBindSessionToTransaction().booleanValue()) { + this.xaResource = new TransactionBoundXAResource(this, (XAResource) session); + } else { + this.xaResource = (XAResource) session; + } + this.localTransactionAdapter = new LocalTransactionAdapter(xaResource); + } + + /** + * Create a new session. + */ + private Session openSession() throws ResourceException { + try { + Session session = mcf.getRepository().login( + cri.getCredentials(), cri.getWorkspace()); + log("Created session (" + session + ")"); + return session; + } catch (RepositoryException e) { + log("Failed to create session", e); + ResourceException exception = new ResourceException( + "Failed to create session: " + e.getMessage()); + exception.initCause(e); + throw exception; + } + } + + /** + * Return the managed connection factory. + */ + public JCAManagedConnectionFactory getManagedConnectionFactory() { + return mcf; + } + + /** + * Return the connection request info. + */ + public JCAConnectionRequestInfo getConnectionRequestInfo() { + return cri; + } + + /** + * Get the log writer. + */ + public PrintWriter getLogWriter() { + return logWriter; + } + + /** + * Set the log writer. + */ + public void setLogWriter(PrintWriter logWriter) + throws ResourceException { + this.logWriter = logWriter; + } + + /** + * Creates a new connection handle for the underlying physical + * connection represented by the ManagedConnection instance. + */ + public Object getConnection(Subject subject, ConnectionRequestInfo cri) + throws ResourceException { + JCASessionHandle handle = new JCASessionHandle(this); + addHandle(handle); + return handle; + } + + /** + * Destroys the physical connection to the underlying resource manager. + */ + public void destroy() + throws ResourceException { + this.session.logout(); + this.handles.clear(); + } + + /** + * Application server calls this method to force any cleanup on + * the ManagedConnection instance. + */ + public void cleanup() + throws ResourceException { + synchronized (handles) { + this.session.logout(); + this.session = openSession(); + this.handles.clear(); + if (this.mcf.getBindSessionToTransaction().booleanValue() && (this.xaResource instanceof TransactionBoundXAResource)) { + ((TransactionBoundXAResource) this.xaResource).rebind((XAResource) session); + } else { + this.xaResource = (XAResource) session; + } + } + } + + /** + * Used by the container to change the association of an + * application-level connection handle with a ManagedConneciton instance. + */ + public void associateConnection(Object connection) + throws ResourceException { + JCASessionHandle handle = (JCASessionHandle) connection; + if (handle.getManagedConnection() != this) { + handle.getManagedConnection().removeHandle(handle); + handle.setManagedConnection(this); + addHandle(handle); + } + } + + /** + * Returns an javax.transaction.xa.XAresource instance. + */ + public XAResource getXAResource() + throws ResourceException { + return this.xaResource; + } + + /** + * Returns an javax.resource.spi.LocalTransaction instance. + */ + public LocalTransaction getLocalTransaction() + throws ResourceException { + return localTransactionAdapter; + } + + /** + * Gets the metadata information for this connection's underlying + * EIS resource manager instance. + */ + public ManagedConnectionMetaData getMetaData() + throws ResourceException { + return this; + } + + /** + * Close the handle. + */ + public void closeHandle(JCASessionHandle handle) { + if (handle != null) { + removeHandle(handle); + sendClosedEvent(handle); + } + } + + /** + * Return the session. + */ + public Session getSession(JCASessionHandle handle) { + synchronized (handles) { + if ((handles.size() > 0) && (handles.get(0) == handle)) { + return session; + } else { + throw new java.lang.IllegalStateException("Inactive logical session handle called"); + } + } + } + + private String getDescriptor(String key) throws ResourceException { + try { + return mcf.getRepository().getDescriptor(key); + } catch (RepositoryException e) { + log("Failed to access the repository", e); + ResourceException exception = new ResourceException( + "Failed to access the repository: " + e.getMessage()); + exception.initCause(e); + throw exception; + } + } + + /** + * Return the product name. + */ + public String getEISProductName() throws ResourceException { + return getDescriptor(Repository.REP_NAME_DESC); + } + + /** + * Return the product version. + */ + public String getEISProductVersion() throws ResourceException { + return getDescriptor(Repository.REP_VERSION_DESC); + } + + /** + * Return number of max connections. + */ + public int getMaxConnections() + throws ResourceException { + return Integer.MAX_VALUE; + } + + /** + * Return the user name. + */ + public String getUserName() + throws ResourceException { + return session.getUserID(); + } + + /** + * Log a message. + */ + public void log(String message) { + log(message, null); + } + + /** + * Log a message. + */ + public void log(String message, Throwable exception) { + if (logWriter != null) { + logWriter.println(message); + + if (exception != null) { + exception.printStackTrace(logWriter); + } + } + } + + /** + * Adds a listener. + */ + public void addConnectionEventListener(ConnectionEventListener listener) { + synchronized (listeners) { + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + } + + /** + * Remove a listener. + */ + public void removeConnectionEventListener(ConnectionEventListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + /** + * Send event. + */ + private void sendEvent(ConnectionEvent event) { + synchronized (listeners) { + for (Iterator i = listeners.iterator(); i.hasNext();) { + ConnectionEventListener listener = i.next(); + + switch (event.getId()) { + case ConnectionEvent.CONNECTION_CLOSED: + listener.connectionClosed(event); + break; + case ConnectionEvent.CONNECTION_ERROR_OCCURRED: + listener.connectionErrorOccurred(event); + break; + case ConnectionEvent.LOCAL_TRANSACTION_COMMITTED: + listener.localTransactionCommitted(event); + break; + case ConnectionEvent.LOCAL_TRANSACTION_ROLLEDBACK: + listener.localTransactionRolledback(event); + break; + case ConnectionEvent.LOCAL_TRANSACTION_STARTED: + listener.localTransactionStarted(event); + break; + default: + // Unknown event, skip + } + } + } + } + + /** + * Send event. + */ + private void sendEvent(int type, Object handle, Exception cause) { + ConnectionEvent event = new ConnectionEvent(this, type, cause); + if (handle != null) { + event.setConnectionHandle(handle); + } + + sendEvent(event); + } + + /** + * Send connection closed event. + */ + private void sendClosedEvent(JCASessionHandle handle) { + sendEvent(ConnectionEvent.CONNECTION_CLOSED, handle, null); + } + + /** + * Send connection error event. + */ + public void sendrrorEvent(JCASessionHandle handle, Exception cause) { + sendEvent(ConnectionEvent.CONNECTION_ERROR_OCCURRED, handle, cause); + } + + /** + * Send transaction committed event. + */ + public void sendTxCommittedEvent(JCASessionHandle handle) { + sendEvent(ConnectionEvent.LOCAL_TRANSACTION_COMMITTED, handle, null); + } + + /** + * Send transaction rolledback event. + */ + public void sendTxRolledbackEvent(JCASessionHandle handle) { + sendEvent(ConnectionEvent.LOCAL_TRANSACTION_ROLLEDBACK, handle, null); + } + + /** + * Send transaction started event. + */ + public void sendTxStartedEvent(JCASessionHandle handle) { + sendEvent(ConnectionEvent.LOCAL_TRANSACTION_STARTED, handle, null); + } + + /** + * Add a session handle. + */ + private void addHandle(JCASessionHandle handle) { + synchronized (handles) { + handles.addFirst(handle); + } + } + + /** + * Remove a session handle. + */ + private void removeHandle(JCASessionHandle handle) { + synchronized (handles) { + handles.remove(handle); + } + } + + /** + * Release handles. + */ + void closeHandles() { + synchronized (handles) { + JCASessionHandle[] handlesArray = new JCASessionHandle[handles + .size()]; + handles.toArray(handlesArray); + for (int i = 0; i < handlesArray.length; i++) { + this.closeHandle(handlesArray[i]); + } + } + } +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAManagedConnectionFactory.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAManagedConnectionFactory.java new file mode 100644 index 00000000000..10f80553dfa --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAManagedConnectionFactory.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jca; + +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.resource.ResourceException; +import javax.resource.spi.ConnectionManager; +import javax.resource.spi.ConnectionRequestInfo; +import javax.resource.spi.ManagedConnection; +import javax.resource.spi.ManagedConnectionFactory; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Implements the JCA ManagedConnectionFactory contract. + */ +public class JCAManagedConnectionFactory + implements ManagedConnectionFactory { + + private static final long serialVersionUID = 4775747072955577131L; + + /** + * Repository parameters. + */ + private final Map parameters = new HashMap(); + + /** + * Key for the repository home + */ + final static String HOMEDIR_KEY = "org.apache.jackrabbit.repository.home"; + /** + * Key for the repository config file + */ + final static String CONFIGFILE_KEY = "org.apache.jackrabbit.repository.conf"; + + /** + * Flag indicating whether the session should be bound to the + * transaction lifecycle. + * In other words, if this flag is true the handle + * will be closed when the transaction ends. + */ + private Boolean bindSessionToTransaction = Boolean.TRUE; + + /** + * Flag indicating whether the Repository should start + * immediately or lazy on first access. Per default true so the Repository will + * start immediately if this JCAManagedConnectionFactory will be initialized. + */ + private Boolean startRepositoryImmediately = Boolean.TRUE; + + /** + * Repository. + */ + private transient Repository repository; + + /** + * Log writer. + */ + private transient PrintWriter logWriter; + + /** + * Return the repository URI. + */ + public String getRepositoryURI() { + return parameters.get(JcrUtils.REPOSITORY_URI); + } + + /** + * Set the repository URI. + */ + public void setRepositoryURI(String uri) { + parameters.put(JcrUtils.REPOSITORY_URI, uri); + } + + /** + * Return the repository home directory. + */ + public String getHomeDir() { + return parameters.get(HOMEDIR_KEY); + } + + /** + * Set the repository home directory. + */ + public void setHomeDir(String home) { + parameters.put(HOMEDIR_KEY, home); + } + + /** + * Return the repository configuration file. + */ + public String getConfigFile() { + return parameters.get(CONFIGFILE_KEY); + } + + /** + * Set the repository configuration file. + */ + public void setConfigFile(String conf) { + parameters.put(CONFIGFILE_KEY, conf); + } + + /** + * Get the log writer. + */ + public PrintWriter getLogWriter() { + return logWriter; + } + + /** + * Set the log writer. + */ + public void setLogWriter(PrintWriter logWriter) + throws ResourceException { + this.logWriter = logWriter; + } + + /** + * Creates a Connection Factory instance. + */ + public Object createConnectionFactory() + throws ResourceException { + return createConnectionFactory(new JCAConnectionManager()); + } + + /** + * Creates a Connection Factory instance. + */ + public Object createConnectionFactory(ConnectionManager cm) + throws ResourceException { + if (startRepositoryImmediately) { + createRepository(); + } + JCARepositoryHandle handle = new JCARepositoryHandle(this, cm); + log("Created repository handle (" + handle + ")"); + return handle; + } + + /** + * {@inheritDoc} + *

    + * Creates a new physical connection to the underlying EIS resource manager. + *

    + * WebSphere 5.1.1 will try to recover an XA resource on startup, regardless + * whether it was committed or rolled back. On this occasion, cri + * will be null. In order to be interoperable, we return an + * anonymous connection, whose XA resource is recoverable-only. + */ + public ManagedConnection createManagedConnection(Subject subject, ConnectionRequestInfo cri) + throws ResourceException { + + if (cri == null) { + return new AnonymousConnection(); + } + return createManagedConnection((JCAConnectionRequestInfo) cri); + } + + /** + * Creates a new physical connection to the underlying EIS resource manager. + */ + private ManagedConnection createManagedConnection(JCAConnectionRequestInfo cri) + throws ResourceException { + return new JCAManagedConnection(this, cri); + } + + /** + * Returns a matched connection from the candidate set of connections. + */ + @SuppressWarnings("rawtypes") + public ManagedConnection matchManagedConnections( + Set set, Subject subject, ConnectionRequestInfo cri) + throws ResourceException { + for (Object connection : set) { + if (connection instanceof JCAManagedConnection) { + JCAManagedConnection mc = (JCAManagedConnection) connection; + if (equals(mc.getManagedConnectionFactory())) { + JCAConnectionRequestInfo otherCri = mc.getConnectionRequestInfo(); + if (cri == otherCri || (cri != null && cri.equals(otherCri))) { + return mc; + } + } + } + } + return null; + } + + /** + * Create/startup the repository. + */ + @SuppressWarnings("deprecation") + private void createRepository() throws ResourceException { + if (repository == null) { + try { + JCARepositoryManager mgr = JCARepositoryManager.getInstance(); + repository = mgr.createRepository(parameters); + log("Created repository (" + repository + ")"); + } catch (RepositoryException e) { + log("Failed to create repository", e); + ResourceException exception = new ResourceException( + "Failed to create repository: " + e.getMessage()); + exception.setLinkedException(e); + throw exception; + } + } + } + + /** + * Return the repository, automatically creating it if needed. + */ + @SuppressWarnings("deprecation") + public Repository getRepository() throws RepositoryException { + if (repository == null) { + synchronized (this) { + try { + createRepository(); + } catch (ResourceException e) { + throw (RepositoryException) e.getLinkedException(); + } + } + } + return repository; + } + + /** + * Log a message. + */ + public void log(String message) { + log(message, null); + } + + /** + * Log a message. + */ + public void log(String message, Throwable exception) { + if (logWriter != null) { + logWriter.println(message); + + if (exception != null) { + exception.printStackTrace(logWriter); + } + } + } + + /** + * Return the hash code. + */ + public int hashCode() { + return parameters.hashCode(); + } + + /** + * Return true if equals. + */ + public boolean equals(Object o) { + if (o == this) { + return true; + } else if (o instanceof JCAManagedConnectionFactory) { + return equals((JCAManagedConnectionFactory) o); + } else { + return false; + } + } + + /** + * Return true if equals. + */ + private boolean equals(JCAManagedConnectionFactory o) { + return parameters.equals(o.parameters); + } + + /** + * Shutdown the repository. + */ + protected void finalize() { + JCARepositoryManager mgr = JCARepositoryManager.getInstance(); + mgr.autoShutdownRepository(parameters); + } + + public Boolean getBindSessionToTransaction() { + return bindSessionToTransaction; + } + + public void setBindSessionToTransaction(Boolean bindSessionToTransaction) { + this.bindSessionToTransaction = bindSessionToTransaction; + } + + public Boolean getStartRepositoryImmediately() { + return startRepositoryImmediately; + } + + public void setStartRepositoryImmediately(Boolean startRepositoryImmediately) { + this.startRepositoryImmediately = startRepositoryImmediately; + } + +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCARepositoryHandle.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCARepositoryHandle.java new file mode 100644 index 00000000000..ff724b875cc --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCARepositoryHandle.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.jca; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.naming.Reference; +import javax.resource.Referenceable; +import javax.resource.ResourceException; +import javax.resource.spi.ConnectionManager; + +import org.apache.jackrabbit.commons.repository.ProxyRepository; +import org.apache.jackrabbit.commons.repository.RepositoryFactory; + +import java.io.Serializable; + +/** + * This class implements the JCA implementation of repository. + */ +public final class JCARepositoryHandle extends ProxyRepository + implements Referenceable, Serializable { + + private static final long serialVersionUID = 1235867375647927916L; + + /** + * Managed connection factory. + */ + private final JCAManagedConnectionFactory mcf; + + /** + * Connection manager. + */ + private final ConnectionManager cm; + + /** + * Reference. + */ + private Reference reference; + + /** + * Construct the repository. + */ + public JCARepositoryHandle( + JCAManagedConnectionFactory mcf, ConnectionManager cm) { + super(new JCARepositoryFactory(mcf)); + this.mcf = mcf; + this.cm = cm; + } + + /** + * Creates a new session. + */ + @SuppressWarnings("deprecation") + public Session login(Credentials creds, String workspace) + throws RepositoryException { + try { + return (Session) cm.allocateConnection( + mcf, new JCAConnectionRequestInfo(creds, workspace)); + } catch (ResourceException e) { + Throwable cause = e.getCause(); + if (cause == null) { + cause = e.getLinkedException(); + } + if (cause instanceof LoginException) { + throw (LoginException) cause; + } else if (cause instanceof NoSuchWorkspaceException) { + throw (NoSuchWorkspaceException) cause; + } else if (cause instanceof RepositoryException) { + throw (RepositoryException) cause; + } else if (cause != null) { + throw new RepositoryException(cause); + } else { + throw new RepositoryException(e); + } + } + } + + /** + * Return the reference. + */ + public Reference getReference() { + return reference; + } + + /** + * Set the reference. + */ + public void setReference(Reference reference) { + this.reference = reference; + } + + private static class JCARepositoryFactory + implements RepositoryFactory, Serializable { + + private static final long serialVersionUID = 5364039431121341634L; + + private final JCAManagedConnectionFactory mcf; + + public JCARepositoryFactory(JCAManagedConnectionFactory mcf) { + this.mcf = mcf; + } + + public Repository getRepository() throws RepositoryException { + return mcf.getRepository(); + } + + } + +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCARepositoryManager.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCARepositoryManager.java new file mode 100644 index 00000000000..4323abc6e2c --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCARepositoryManager.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.jca; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * This class implements the repository manager. + */ +public final class JCARepositoryManager { + + /** The config file prefix that signifies the file is to be loaded from the classpath. */ + public static final String CLASSPATH_CONFIG_PREFIX = "classpath:"; + + /** + * Instance of manager. + */ + private static final JCARepositoryManager INSTANCE = + new JCARepositoryManager(); + + /** + * References. + */ + private final Map, Repository> repositories = + new HashMap, Repository>(); + + /** + * Flag indicating that the life cycle + * of the resource is not managed by the + * application server + */ + private boolean autoShutdown = true; + + /** + * Construct the manager. + */ + private JCARepositoryManager() { + } + + /** + * Create repository. + * + * @param parameters repository parameters + * @return repository instance + */ + public synchronized Repository createRepository( + Map parameters) throws RepositoryException { + Repository repository = repositories.get(parameters); + if (repository == null) { + if (parameters.containsKey(JcrUtils.REPOSITORY_URI)) { + repository = JcrUtils.getRepository(parameters); + } else { + repository = createNonTransientRepository(parameters); + } + repositories.put(parameters, repository); + } + return repository; + } + + /** + * Creates a non transient Repository + * + * @param parameters + * @return Repository + * @throws RepositoryException + */ + private Repository createNonTransientRepository( + Map parameters) throws RepositoryException { + RepositoryConfig config = null; + + String configFile = parameters.get(JCAManagedConnectionFactory.CONFIGFILE_KEY); + String homeDir = parameters.get(JCAManagedConnectionFactory.HOMEDIR_KEY); + + if (configFile != null && configFile.startsWith(CLASSPATH_CONFIG_PREFIX)) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = this.getClass().getClassLoader(); + } + + InputStream configInputStream = cl.getResourceAsStream( + configFile.substring(CLASSPATH_CONFIG_PREFIX.length())); + try { + config = RepositoryConfig.create(configInputStream, homeDir); + } finally { + if (configInputStream != null) { + try { + configInputStream.close(); + } catch (IOException e) { + // ignore + } + } + } + } else if (configFile != null) { + config = RepositoryConfig.create(configFile, homeDir); + } else { + config = RepositoryConfig.create(new File(homeDir)); + } + return RepositoryImpl.create(config); + } + + /** + * Shutdown all the repositories. + */ + public synchronized void shutdown() { + for (Repository repository : repositories.values()) { + if (repository instanceof JackrabbitRepository) { + ((JackrabbitRepository) repository).shutdown(); + } + } + repositories.clear(); + } + + /** + * Return the instance. + */ + public static JCARepositoryManager getInstance() { + return INSTANCE; + } + + public boolean isAutoShutdown() { + return autoShutdown; + } + + public void setAutoShutdown(boolean autoShutdown) { + this.autoShutdown = autoShutdown; + } + + /** + * Try to shutdown the repository only if + * {@link JCARepositoryManager#autoShutdown} is true. + */ + public synchronized void autoShutdownRepository( + Map parameters) { + if (this.isAutoShutdown()) { + Repository repository = repositories.get(parameters); + if (repository instanceof JackrabbitRepository) { + ((JackrabbitRepository) repository).shutdown(); + } + } + } + +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAResourceAdapter.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAResourceAdapter.java new file mode 100644 index 00000000000..2bb10bf8f29 --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCAResourceAdapter.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.jca; + +import java.io.Serializable; + +import javax.resource.ResourceException; +import javax.resource.spi.ActivationSpec; +import javax.resource.spi.BootstrapContext; +import javax.resource.spi.ResourceAdapter; +import javax.resource.spi.ResourceAdapterInternalException; +import javax.resource.spi.endpoint.MessageEndpointFactory; +import javax.transaction.xa.XAResource; + + +/** + * JCR ResourceAdapter. + */ +public class JCAResourceAdapter implements ResourceAdapter, Serializable { + + private static final long serialVersionUID = 7335723888000232035L; + + private final XAResource[] xaResources = new XAResource[0]; + + /** + * Notify the RepositoryManager that the lifecycle is managed by + * the container + */ + public void start(BootstrapContext ctx) throws ResourceAdapterInternalException { + JCARepositoryManager.getInstance().setAutoShutdown(false); + } + + /** + * Shutdown jackrabbit repositories + */ + public void stop() { + JCARepositoryManager.getInstance().shutdown(); + } + + public void endpointActivation(MessageEndpointFactory mef, ActivationSpec as) throws ResourceException { + } + + public void endpointDeactivation(MessageEndpointFactory mef, ActivationSpec as) { + } + + public XAResource[] getXAResources(ActivationSpec[] as) throws ResourceException { + return xaResources; + } + +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCASessionHandle.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCASessionHandle.java new file mode 100644 index 00000000000..5c69f366e9c --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/JCASessionHandle.java @@ -0,0 +1,440 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jca; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.AccessControlException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.InvalidItemStateException; +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +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.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.retention.RetentionManager; +import javax.jcr.security.AccessControlManager; +import javax.jcr.version.VersionException; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * This class implements the JCA implementation of session. + */ +public final class JCASessionHandle implements Session, XAResource { + + /** + * Managed connection. + */ + private JCAManagedConnection mc; + + /** + * Construct a new session. + */ + public JCASessionHandle(JCAManagedConnection mc) { + this.mc = mc; + } + + /** + * Return the managed connection. + */ + public JCAManagedConnection getManagedConnection() { + return mc; + } + + /** + * Set the managed connection. + */ + public void setManagedConnection(JCAManagedConnection mc) { + this.mc = mc; + } + + /** + * Return the session. + */ + private Session getSession() { + return mc.getSession(this); + } + + /** + * Return the repository. + */ + public Repository getRepository() { + return getSession().getRepository(); + } + + /** + * Return the user id. + */ + public String getUserID() { + return getSession().getUserID(); + } + + /** + * Return the attribute. + */ + public Object getAttribute(String name) { + return getSession().getAttribute(name); + } + + /** + * Return the attribute names. + */ + public String[] getAttributeNames() { + return getSession().getAttributeNames(); + } + + /** + * Return the workspace. + */ + public Workspace getWorkspace() { + return getSession().getWorkspace(); + } + + /** + * Impersonate another user. + */ + public Session impersonate(Credentials cred) + throws LoginException, RepositoryException { + throw new RepositoryException("impersonate(..) not supported in managed environment"); + } + + /** + * Return the root node. + */ + public Node getRootNode() + throws RepositoryException { + return getSession().getRootNode(); + } + + /** + * Return node by UUID. + */ + @SuppressWarnings("deprecation") + public Node getNodeByUUID(String uuid) + throws ItemNotFoundException, RepositoryException { + return getSession().getNodeByUUID(uuid); + } + + /** + * Return the item. + */ + public Item getItem(String arg0) + throws PathNotFoundException, RepositoryException { + return getSession().getItem(arg0); + } + + /** + * Return true if item exists. + */ + public boolean itemExists(String arg0) + throws RepositoryException { + return getSession().itemExists(arg0); + } + + /** + * Move the item. + */ + public void move(String arg0, String arg1) + throws ItemExistsException, PathNotFoundException, VersionException, + ConstraintViolationException, LockException, RepositoryException { + getSession().move(arg0, arg1); + } + + /** + * Save the session. + */ + public void save() + throws AccessDeniedException, ItemExistsException, + ConstraintViolationException, InvalidItemStateException, VersionException, + LockException, NoSuchNodeTypeException, RepositoryException { + getSession().save(); + } + + /** + * Refresh the session. + */ + public void refresh(boolean arg0) + throws RepositoryException { + getSession().refresh(arg0); + } + + /** + * Return true if it has pending changes. + */ + public boolean hasPendingChanges() + throws RepositoryException { + return getSession().hasPendingChanges(); + } + + /** + * Return the value factory. + */ + public ValueFactory getValueFactory() + throws UnsupportedRepositoryOperationException, RepositoryException { + return getSession().getValueFactory(); + } + + /** + * Check permission. + */ + public void checkPermission(String arg0, String arg1) + throws AccessControlException, RepositoryException { + getSession().checkPermission(arg0, arg1); + } + + /** + * Return the import content handler. + */ + public ContentHandler getImportContentHandler(String arg0, int arg1) + throws PathNotFoundException, ConstraintViolationException, VersionException, + LockException, RepositoryException { + return getSession().getImportContentHandler(arg0, arg1); + } + + /** + * Import XML content. + */ + public void importXML(String arg0, InputStream arg1, int arg2) + throws IOException, PathNotFoundException, ItemExistsException, + ConstraintViolationException, VersionException, InvalidSerializedDataException, + LockException, RepositoryException { + getSession().importXML(arg0, arg1, arg2); + } + + /** + * Export system view. + */ + public void exportSystemView(String arg0, ContentHandler arg1, boolean arg2, boolean arg3) + throws PathNotFoundException, SAXException, RepositoryException { + getSession().exportSystemView(arg0, arg1, arg2, arg3); + } + + /** + * Export system view. + */ + public void exportSystemView(String arg0, OutputStream arg1, boolean arg2, boolean arg3) + throws IOException, PathNotFoundException, RepositoryException { + getSession().exportSystemView(arg0, arg1, arg2, arg3); + } + + /** + * Export document view. + */ + public void exportDocumentView(String arg0, ContentHandler arg1, boolean arg2, boolean arg3) + throws PathNotFoundException, SAXException, RepositoryException { + getSession().exportDocumentView(arg0, arg1, arg2, arg3); + } + + /** + * Export document view. + */ + public void exportDocumentView(String arg0, OutputStream arg1, boolean arg2, boolean arg3) + throws IOException, PathNotFoundException, RepositoryException { + getSession().exportDocumentView(arg0, arg1, arg2, arg3); + } + + /** + * Set namespace prefix. + */ + public void setNamespacePrefix(String arg0, String arg1) + throws NamespaceException, RepositoryException { + getSession().setNamespacePrefix(arg0, arg1); + } + + /** + * Return namespace prefixes. + */ + public String[] getNamespacePrefixes() + throws RepositoryException { + return getSession().getNamespacePrefixes(); + } + + /** + * Return namespace URI. + */ + public String getNamespaceURI(String arg0) + throws NamespaceException, RepositoryException { + return getSession().getNamespaceURI(arg0); + } + + /** + * Return namespace prefix. + */ + public String getNamespacePrefix(String arg0) + throws NamespaceException, RepositoryException { + return getSession().getNamespacePrefix(arg0); + } + + /** + * Logout the session. + */ + public void logout() { + mc.closeHandle(this); + } + + /** + * Return true if session is live. + */ + public boolean isLive() { + return getSession().isLive(); + } + + /** + * Add lock token. + */ + @SuppressWarnings("deprecation") + public void addLockToken(String arg0) { + getSession().addLockToken(arg0); + } + + /** + * Return the lock tokens. + */ + @SuppressWarnings("deprecation") + public String[] getLockTokens() { + return getSession().getLockTokens(); + } + + /** + * Remove lock token. + */ + @SuppressWarnings("deprecation") + public void removeLockToken(String arg0) { + getSession().removeLockToken(arg0); + } + + public AccessControlManager getAccessControlManager() + throws RepositoryException { + return getSession().getAccessControlManager(); + } + + public Node getNode(String arg0) throws RepositoryException { + return getSession().getNode(arg0); + } + + public Node getNodeByIdentifier(String arg0) throws RepositoryException { + return getSession().getNodeByIdentifier(arg0); + } + + public Property getProperty(String arg0) throws RepositoryException { + return getSession().getProperty(arg0); + } + + public RetentionManager getRetentionManager() + throws RepositoryException { + return getSession().getRetentionManager(); + } + + public boolean hasCapability(String arg0, Object arg1, Object[] arg2) + throws RepositoryException { + return getSession().hasCapability(arg0, arg1, arg2); + } + + public boolean hasPermission(String arg0, String arg1) + throws RepositoryException { + return getSession().hasPermission(arg0, arg1); + } + + public boolean nodeExists(String path) throws RepositoryException { + return getSession().nodeExists(path); + } + + public boolean propertyExists(String path) throws RepositoryException { + return getSession().propertyExists(path); + } + + public void removeItem(String path) throws RepositoryException { + getSession().removeItem(path); + } + + //--------------------------------------------------------< XAResource >-- + + private XAResource getXAResource() throws XAException { + Session session = getSession(); + if (session instanceof XAResource) { + return (XAResource) session; + } else { + throw new XAException( + "XA transactions are not supported with " + session); + } + } + + public void start(Xid xid, int flags) throws XAException { + getXAResource().start(xid, flags); + } + + public void end(Xid xid, int flags) throws XAException { + getXAResource().end(xid, flags); + } + + public int prepare(Xid xid) throws XAException { + return getXAResource().prepare(xid); + } + + public void rollback(Xid xid) throws XAException { + getXAResource().rollback(xid); + } + + public void commit(Xid xid, boolean onePhase) throws XAException { + getXAResource().commit(xid, onePhase); + } + + public void forget(Xid xid) throws XAException { + getXAResource().forget(xid); + } + + public Xid[] recover(int flag) throws XAException { + return getXAResource().recover(flag); + } + + public boolean isSameRM(XAResource xares) throws XAException { + if (xares instanceof JCASessionHandle) { + xares = ((JCASessionHandle) xares).getXAResource(); + } + return getXAResource().isSameRM(xares); + } + + public int getTransactionTimeout() throws XAException { + return getXAResource().getTransactionTimeout(); + } + + public boolean setTransactionTimeout(int seconds) throws XAException { + return getXAResource().setTransactionTimeout(seconds); + } + +} diff --git a/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/TransactionBoundXAResource.java b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/TransactionBoundXAResource.java new file mode 100644 index 00000000000..92700ffe8b2 --- /dev/null +++ b/jackrabbit-jca/src/main/java/org/apache/jackrabbit/jca/TransactionBoundXAResource.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.jca; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +public class TransactionBoundXAResource implements XAResource { + + private XAResource xaResource; + + private JCAManagedConnection connection; + + private boolean ending; + + public TransactionBoundXAResource(JCAManagedConnection connection, + XAResource xaResource) { + super(); + this.xaResource = xaResource; + this.connection = connection; + } + + /** + * There is a one-to-one Relation between this TransactionBoundXAResource + * and the JCAManagedConnection so the used XAResource must be in sync when it is changed in the + * JCAManagedConnection. + * @param res + */ + protected void rebind(XAResource res) { + this.xaResource = res; + } + + public void commit(Xid arg0, boolean arg1) throws XAException { + xaResource.commit(arg0, arg1); + } + + public void end(Xid arg0, int arg1) throws XAException { + if (!ending) { + this.ending = true; + try { + xaResource.end(arg0, arg1); + } finally { + if(arg1 != XAResource.TMSUSPEND) { + this.connection.closeHandles(); + } + } + // reuse the XAResource + this.ending = false; + } + } + + public void forget(Xid arg0) throws XAException { + xaResource.forget(arg0); + } + + public int getTransactionTimeout() throws XAException { + return xaResource.getTransactionTimeout(); + } + + public boolean isSameRM(XAResource arg0) throws XAException { + return xaResource.isSameRM(arg0); + } + + public int prepare(Xid arg0) throws XAException { + return xaResource.prepare(arg0); + } + + public Xid[] recover(int arg0) throws XAException { + return xaResource.recover(arg0); + } + + public void rollback(Xid arg0) throws XAException { + xaResource.rollback(arg0); + } + + public boolean setTransactionTimeout(int arg0) throws XAException { + return xaResource.setTransactionTimeout(arg0); + } + + public void start(Xid arg0, int arg1) throws XAException { + xaResource.start(arg0, arg1); + } + +} diff --git a/jackrabbit-jca/src/main/rar/META-INF/LICENSE b/jackrabbit-jca/src/main/rar/META-INF/LICENSE new file mode 100644 index 00000000000..82ea7256201 --- /dev/null +++ b/jackrabbit-jca/src/main/rar/META-INF/LICENSE @@ -0,0 +1,1625 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +APACHE JACKRABBIT SUBCOMPONENTS + +Apache Jackrabbit includes parts with separate copyright notices and license +terms. Your use of these subcomponents is subject to the terms and conditions +of the following licenses: + +XPath parser (jackrabbit-spi-commons) + + XPath 2.0/XQuery 1.0 Parser: + http://www.w3.org/2002/11/xquery-xpath-applets/xgrammar.zip + + Copyright (C) 2002 World Wide Web Consortium, (Massachusetts Institute of + Technology, European Research Consortium for Informatics and Mathematics, + Keio University). All Rights Reserved. + + This work is distributed under the W3C(R) Software License in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + W3C(R) SOFTWARE NOTICE AND LICENSE + http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + + This work (and included software, documentation such as READMEs, or + other related items) is being provided by the copyright holders under + the following license. By obtaining, using and/or copying this work, + you (the licensee) agree that you have read, understood, and will comply + with the following terms and conditions. + + Permission to copy, modify, and distribute this software and its + documentation, with or without modification, for any purpose and + without fee or royalty is hereby granted, provided that you include + the following on ALL copies of the software and documentation or + portions thereof, including modifications: + + 1. The full text of this NOTICE in a location viewable to users + of the redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, + or terms and conditions. If none exist, the W3C Software Short + Notice should be included (hypertext is preferred, text is + permitted) within the body of any redistributed or derivative code. + + 3. Notice of any changes or modifications to the files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT + HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR + DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, + TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and + any associated documentation will at all times remain with + copyright holders. + +PDFBox libraries (pdfbox, jempbox, fontbox) + + Copyright (c) 2002-2007, www.pdfbox.org + Copyright (c) 2006-2007, www.jempbox.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of pdfbox; nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Adobe Font Metrics (AFM) for PDF Core 14 Fonts + + This file and the 14 PostScript(R) AFM files it accompanies may be used, + copied, and distributed for any purpose and without charge, with or without + modification, provided that all copyright notices are retained; that the + AFM files are not distributed without this file; that all modifications + to this file or any of the AFM files are prominently noted in the modified + file(s); and that this paragraph is not modified. Adobe Systems has no + responsibility or obligation to support the use of the AFM files. + +CMaps for PDF Fonts (http://www.adobe.com/devnet/font/#pcfi and +ftp://ftp.oreilly.com/pub/examples/nutshell/cjkv/adobe/) + + Copyright 1990-2001 Adobe Systems Incorporated. + All Rights Reserved. + + Patents Pending + + NOTICE: All information contained herein is the property + of Adobe Systems Incorporated. + + Permission is granted for redistribution of this file + provided this copyright notice is maintained intact and + that the contents of this file are not altered in any + way from its original form. + + PostScript and Display PostScript are trademarks of + Adobe Systems Incorporated which may be registered in + certain jurisdictions. + +Glyphlist (http://www.adobe.com/devnet/opentype/archives/glyph.html) + + Copyright (c) 1997,1998,2002,2007 Adobe Systems Incorporated + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this documentation file to use, copy, publish, distribute, + sublicense, and/or sell copies of the documentation, and to permit + others to do the same, provided that: + - No modification, editing or other alteration of this document is + allowed; and + - The above copyright notice and this permission notice shall be + included in all copies of the documentation. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this documentation file, to create their own derivative works + from the content of this document to use, copy, publish, distribute, + sublicense, and/or sell the derivative works, and to permit others to do + the same, provided that the derived work is not represented as being a + copy or version of this document. + + Adobe shall not be liable to any party for any loss of revenue or profit + or for indirect, incidental, special, consequential, or other similar + damages, whether based on tort (including without limitation negligence + or strict liability), contract or other legal or equitable grounds even + if Adobe has been advised or had reason to know of the possibility of + such damages. The Adobe materials are provided on an "AS IS" basis. + Adobe specifically disclaims all express, statutory, or implied + warranties relating to the Adobe materials, including but not limited to + those concerning merchantability or fitness for a particular purpose or + non-infringement of any third party rights regarding the Adobe + materials. + +The International Components for Unicode (http://site.icu-project.org/) + + Copyright (c) 1995-2009 International Business Machines Corporation + and others + + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, and/or sell copies of the Software, and to permit persons + to whom the Software is furnished to do so, provided that the above + copyright notice(s) and this permission notice appear in all copies + of the Software and that both the above copyright notice(s) and this + permission notice appear in supporting documentation. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE + BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, + OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + SOFTWARE. + + Except as contained in this notice, the name of a copyright holder shall + not be used in advertising or otherwise to promote the sale, use or other + dealings in this Software without prior written authorization of the + copyright holder. + +MIME type information from file-4.26.tar.gz (http://www.darwinsys.com/file/) + + Copyright (c) Ian F. Darwin 1986, 1987, 1989, 1990, 1991, 1992, 1994, 1995. + Software written by Ian F. Darwin and others; + maintained 1994- Christos Zoulas. + + This software is not subject to any export provision of the United States + Department of Commerce, and may be exported to any country or planet. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice immediately at the beginning of the file, without modification, + this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Metadata extractor library (metadata-extractor) + + This is public domain software - that is, you can do whatever you want + with it, and include it software that is licensed under the GNU or the + BSD license, or whatever other licence you choose, including proprietary + closed source licenses. I do ask that you leave this header in tact. + + If you make modifications to this code that you think would benefit the + wider community, please send me a copy and I'll post it on my site. + + If you make use of this code, I'd appreciate hearing about it. + metadata_extractor [at] drewnoakes [dot] com + Latest version of this software kept at + http://drewnoakes.com/ + +ASM bytecode manipulation library (asm) + + Copyright (c) 2000-2005 INRIA, France Telecom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +SLF4J libraries (slf4j-api, log4j-over-slf4j, jcl-over-slf4j) + + Copyright (c) 2004-2008 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Logback library (logback-core, logback-classic) + + Logback: the reliable, generic, fast and flexible logging framework. + Copyright (C) 1999-2009, QOS.ch. All rights reserved. + + This program and the accompanying materials are dual-licensed under + either the terms of the Eclipse Public License v1.0 as published by + the Eclipse Foundation + + or (per the licensee's choosing) + + under the terms of the GNU Lesser General Public License version 2.1 + as published by the Free Software Foundation. + +XML API library, org.w3c classes (xml-apis) + + DOM Java Language Binding: + http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/java-binding.html + + W3C IPR SOFTWARE NOTICE + Copyright (C) 2000 World Wide Web Consortium, (Massachusetts Institute of + Technology, Institut National de Recherche en Informatique et en + Automatique, Keio University). All Rights Reserved. + + The DOM bindings are published under the W3C Software Copyright Notice + and License. The software license requires "Notice of any changes or + modifications to the W3C files, including the date changes were made." + Consequently, modified versions of the DOM bindings must document that + they do not conform to the W3C standard; in the case of the IDL binding, + the pragma prefix can no longer be 'w3c.org'; in the case of the Java + binding, the package names can no longer be in the 'org.w3c' package. + + Note: The original version of the W3C Software Copyright Notice and + License could be found at + http://www.w3.org/Consortium/Legal/copyright-software-19980720 + + Copyright (C) 1994-2000 World Wide Web Consortium, (Massachusetts + Institute of Technology, Institut National de Recherche en Informatique + et en Automatique, Keio University). All Rights Reserved. + http://www.w3.org/Consortium/Legal/ + + This W3C work (including software, documents, or other related items) is + being provided by the copyright holders under the following license. By + obtaining, using and/or copying this work, you (the licensee) agree that + you have read, understood, and will comply with the following terms and + conditions: + + Permission to use, copy, and modify this software and its documentation, + with or without modification, for any purpose and without fee or royalty + is hereby granted, provided that you include the following on ALL copies + of the software and documentation or portions thereof, including + modifications, that you make: + + 1. The full text of this NOTICE in a location viewable to users of the + redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, or + terms and conditions. If none exist, a short notice of the following + form (hypertext is preferred, text is permitted) should be used + within the body of any redistributed or derivative code: + "Copyright (C) [$date-of-software] World Wide Web Consortium, + (Massachusetts Institute of Technology, Institut National de + Recherche en Informatique et en Automatique, Keio University). + All Rights Reserved. http://www.w3.org/Consortium/Legal/" + + 3. Notice of any changes or modifications to the W3C files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS + MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT + NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR + PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE + ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and any + associated documentation will at all times remain with copyright holders. + +XML API library, org.xml.sax classes (xml-apis) + + SAX2 is Free! + + I hereby abandon any property rights to SAX 2.0 (the Simple API for + XML), and release all of the SAX 2.0 source code, compiled code, and + documentation contained in this distribution into the Public Domain. + SAX comes with NO WARRANTY or guarantee of fitness for any purpose. + + David Megginson, david@megginson.com + 2000-05-05 + +Concurrent library (concurrent-1.3.4.jar) + + http://g.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html + + All classes are released to the public domain and may be used for any + purpose whatsoever without permission or acknowledgment. Portions of + the CopyOnWriteArrayList and ConcurrentReaderHashMap classes are adapted + from Sun JDK source code. These are copyright of Sun Microsystems, Inc, + and are used with their kind permission, as described in this license: + + TECHNOLOGY LICENSE FROM SUN MICROSYSTEMS, INC. TO DOUG LEA + + Whereas Doug Lea desires to utlized certain Java Software technologies + in the util.concurrent technology; and Whereas Sun Microsystems, Inc. + ("Sun") desires that Doug Lea utilize certain Java Software technologies + in the util.concurrent technology; + + Therefore the parties agree as follows, effective May 31, 2002: + + "Java Software technologies" means + + classes/java/util/ArrayList.java, and + classes/java/util/HashMap.java. + + The Java Software technologies are Copyright (c) 1994-2000 Sun + Microsystems, Inc. All rights reserved. + + Sun hereby grants Doug Lea a non-exclusive, worldwide, non-transferrable + license to use, reproduce, create derivate works of, and distribute the + Java Software and derivative works thereof in source and binary forms + as part of a larger work, and to sublicense the right to use, reproduce + and distribute the Java Software and Doug Lea's derivative works as the + part of larger works through multiple tiers of sublicensees provided that + the following conditions are met: + + -Neither the name of or trademarks of Sun may be used to endorse or + promote products including or derived from the Java Software technology + without specific prior written permission; and + -Redistributions of source or binary code must contain the above + copyright notice, this notice and and the following disclaimers: + + This software is provided "AS IS," without a warranty of any kind. + ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + MICROSYSTEMS, INC. AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES + SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING + THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN MICROSYSTEMS, INC. + OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, + HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF + THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN MICROSYSTEMS, INC. + HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + You acknowledge that Software is not designed,licensed or intended for + use in the design, construction, operation or maintenance of any nuclear + facility. + +Office Open XML schemas (ooxml-schemas-1.0.jar) + + The Office Open XML schema definitions used by Apache POI are + a part of the Office Open XML ECMA Specification (ECMA-376, [1]). + As defined in section 9.4 of the ECMA bylaws [2], this specification + is available to all interested parties without restriction: + + 9.4 All documents when approved shall be made available to + all interested parties without restriction. + + Furthermore, both Microsoft and Adobe have granted patent licenses + to this work [3,4,5]. + + [1] http://www.ecma-international.org/publications/standards/Ecma-376.htm + [2] http://www.ecma-international.org/memento/Ecmabylaws.htm + [3] http://www.microsoft.com/interop/osp/ + [4] http://www.ecma-international.org/publications/files/ECMA-ST/Ecma%20PATENT/ECMA-376%20Edition%201%20Microsoft%20Patent%20Declaration.pdf + [5] http://www.ecma-international.org/publications/files/ECMA-ST/Ecma%20PATENT/ga-2006-191.pdf + +DOM4J library (dom4j-1.6.1.jar) + + Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved. + + Redistribution and use of this software and associated documentation + ("Software"), with or without modification, are permitted provided + that the following conditions are met: + + 1. Redistributions of source code must retain copyright + statements and notices. Redistributions must also contain a + copy of this document. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + + 3. The name "DOM4J" must not be used to endorse or promote + products derived from this Software without prior written + permission of MetaStuff, Ltd. For written permission, + please contact dom4j-info@metastuff.com. + + 4. Products derived from this Software may not be called "DOM4J" + nor may "DOM4J" appear in their names without prior written + permission of MetaStuff, Ltd. DOM4J is a registered + trademark of MetaStuff, Ltd. + + 5. Due credit should be given to the DOM4J Project - + http://www.dom4j.org + + THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + +Unicode conversion code in Lucene Java (lucene-core) + + Copyright 2001-2004 Unicode, Inc. + + Disclaimer + + This source code is provided as is by Unicode, Inc. No claims are + made as to fitness for any particular purpose. No warranties of any + kind are expressed or implied. The recipient agrees to determine + applicability of information provided. If this file has been + purchased on magnetic or optical media from Unicode, Inc., the + sole remedy for any claim will be exchange of defective media + within 90 days of receipt. + + Limitations on Rights to Redistribute This Code + + Unicode, Inc. hereby grants the right to freely use the information + supplied in this file in the creation of products supporting the + Unicode Standard, and to make copies of this file in any form + for internal or external distribution as long as this notice + remains attached. + +Array utility code in Lucene Java (lucene-core) + + PSF LICENSE AGREEMENT FOR PYTHON 2.4 + ------------------------------------ + + 1. This LICENSE AGREEMENT is between the Python Software Foundation + ("PSF"), and the Individual or Organization ("Licensee") accessing and + otherwise using Python 2.4 software in source or binary form and its + associated documentation. + + 2. Subject to the terms and conditions of this License Agreement, PSF + hereby grants Licensee a nonexclusive, royalty-free, world-wide + license to reproduce, analyze, test, perform and/or display publicly, + prepare derivative works, distribute, and otherwise use Python 2.4 + alone or in any derivative version, provided, however, that PSF's + License Agreement and PSF's notice of copyright, i.e., "Copyright (c) + 2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved" + are retained in Python 2.4 alone or in any derivative version prepared + by Licensee. + + 3. In the event Licensee prepares a derivative work that is based on + or incorporates Python 2.4 or any part thereof, and wants to make + the derivative work available to others as provided herein, then + Licensee hereby agrees to include in any such work a brief summary of + the changes made to Python 2.4. + + 4. PSF is making Python 2.4 available to Licensee on an "AS IS" + basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.4 WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON + 2.4 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS + A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.4, + OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 7. Nothing in this License Agreement shall be deemed to create any + relationship of agency, partnership, or joint venture between PSF and + Licensee. This License Agreement does not grant permission to use PSF + trademarks or trade name in a trademark sense to endorse or promote + products or services of Licensee, or any third party. + + 8. By copying, installing or otherwise using Python 2.4, Licensee + agrees to be bound by the terms and conditions of this License + Agreement. + + BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 + ------------------------------------------- + + BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + + 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an + office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the + Individual or Organization ("Licensee") accessing and otherwise using + this software in source or binary form and its associated + documentation ("the Software"). + + 2. Subject to the terms and conditions of this BeOpen Python License + Agreement, BeOpen hereby grants Licensee a non-exclusive, + royalty-free, world-wide license to reproduce, analyze, test, perform + and/or display publicly, prepare derivative works, distribute, and + otherwise use the Software alone or in any derivative version, + provided, however, that the BeOpen Python License is retained in the + Software, alone or in any derivative version prepared by Licensee. + + 3. BeOpen is making the Software available to Licensee on an "AS IS" + basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE + SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS + AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY + DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 5. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 6. This License Agreement shall be governed by and interpreted in all + respects by the law of the State of California, excluding conflict of + law provisions. Nothing in this License Agreement shall be deemed to + create any relationship of agency, partnership, or joint venture + between BeOpen and Licensee. This License Agreement does not grant + permission to use BeOpen trademarks or trade names in a trademark + sense to endorse or promote products or services of Licensee, or any + third party. As an exception, the "BeOpen Python" logos available at + http://www.pythonlabs.com/logos.html may be used according to the + permissions granted on that web page. + + 7. By copying, installing or otherwise using the software, Licensee + agrees to be bound by the terms and conditions of this License + Agreement. + + CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 + --------------------------------------- + + 1. This LICENSE AGREEMENT is between the Corporation for National + Research Initiatives, having an office at 1895 Preston White Drive, + Reston, VA 20191 ("CNRI"), and the Individual or Organization + ("Licensee") accessing and otherwise using Python 1.6.1 software in + source or binary form and its associated documentation. + + 2. Subject to the terms and conditions of this License Agreement, CNRI + hereby grants Licensee a nonexclusive, royalty-free, world-wide + license to reproduce, analyze, test, perform and/or display publicly, + prepare derivative works, distribute, and otherwise use Python 1.6.1 + alone or in any derivative version, provided, however, that CNRI's + License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) + 1995-2001 Corporation for National Research Initiatives; All Rights + Reserved" are retained in Python 1.6.1 alone or in any derivative + version prepared by Licensee. Alternately, in lieu of CNRI's License + Agreement, Licensee may substitute the following text (omitting the + quotes): "Python 1.6.1 is made available subject to the terms and + conditions in CNRI's License Agreement. This Agreement together with + Python 1.6.1 may be located on the Internet using the following + unique, persistent identifier (known as a handle): 1895.22/1013. This + Agreement may also be obtained from a proxy server on the Internet + using the following URL: http://hdl.handle.net/1895.22/1013". + + 3. In the event Licensee prepares a derivative work that is based on + or incorporates Python 1.6.1 or any part thereof, and wants to make + the derivative work available to others as provided herein, then + Licensee hereby agrees to include in any such work a brief summary of + the changes made to Python 1.6.1. + + 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" + basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON + 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS + A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, + OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 7. This License Agreement shall be governed by the federal + intellectual property law of the United States, including without + limitation the federal copyright law, and, to the extent such + U.S. federal law does not apply, by the law of the Commonwealth of + Virginia, excluding Virginia's conflict of law provisions. + Notwithstanding the foregoing, with regard to derivative works based + on Python 1.6.1 that incorporate non-separable material that was + previously distributed under the GNU General Public License (GPL), the + law of the Commonwealth of Virginia shall govern this License + Agreement only as to issues arising under or with respect to + Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this + License Agreement shall be deemed to create any relationship of + agency, partnership, or joint venture between CNRI and Licensee. This + License Agreement does not grant permission to use CNRI trademarks or + trade name in a trademark sense to endorse or promote products or + services of Licensee, or any third party. + + 8. By clicking on the "ACCEPT" button where indicated, or by copying, + installing or otherwise using Python 1.6.1, Licensee agrees to be + bound by the terms and conditions of this License Agreement. + + ACCEPT + + + CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 + -------------------------------------------------- + + Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, + The Netherlands. All rights reserved. + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the name of Stichting Mathematisch + Centrum or CWI not be used in advertising or publicity pertaining to + distribution of the software without specific, written prior + permission. + + STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE + FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +AspectJ runtime library (aspectjrt) + + Eclipse Public License - v 1.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF + THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial code and + documentation distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + + i) changes to the Program, and + + ii) additions to the Program; + + where such changes and/or additions to the Program originate from and + are distributed by that particular Contributor. A Contribution + 'originates' from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include additions to the Program which: (i) are + separate modules of software distributed in conjunction with the + Program under their own license agreement, and (ii) are not derivative + works of the Program. + + "Contributor" means any person or entity that distributes the Program. + + "Licensed Patents " mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone or + when combined with the Program. + + "Program" means the Contributions distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement, + including all Contributors. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free copyright license to + reproduce, prepare derivative works of, publicly display, publicly + perform, distribute and sublicense the Contribution of such + Contributor, if any, and such derivative works, in source code and + object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free patent license under + Licensed Patents to make, use, sell, offer to sell, import and + otherwise transfer the Contribution of such Contributor, if any, in + source code and object code form. This patent license shall apply to + the combination of the Contribution and the Program if, at the time + the Contribution is added by the Contributor, such addition of the + Contribution causes such combination to be covered by the Licensed + Patents. The patent license shall not apply to any other combinations + which include the Contribution. No hardware per se is licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. Each + Contributor disclaims any liability to Recipient for claims brought by + any other entity based on infringement of intellectual property rights + or otherwise. As a condition to exercising the rights and licenses + granted hereunder, each Recipient hereby assumes sole responsibility + to secure any other intellectual property rights needed, if any. For + example, if a third party patent license is required to allow Recipient + to distribute the Program, it is Recipient's responsibility to acquire + that license before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + + 3. REQUIREMENTS + + A Contributor may choose to distribute the Program in object code form + under its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + + i) effectively disclaims on behalf of all Contributors all warranties + and conditions, express and implied, including warranties or + conditions of title and non-infringement, and implied warranties + or conditions of merchantability and fitness for a particular + purpose; + + ii) effectively excludes on behalf of all Contributors all liability + for damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + + iii) states that any provisions which differ from this Agreement are + offered by that Contributor alone and not by any other party; and + + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a + reasonable manner on or through a medium customarily used for + software exchange. + + When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the + Program. + + Contributors may not remove or alter any copyright notices contained + within the Program. + + Each Contributor must identify itself as the originator of its + Contribution, if any, in a manner that reasonably allows subsequent + Recipients to identify the originator of the Contribution. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product offering + should do so in a manner which does not create potential liability for + other Contributors. Therefore, if a Contributor includes the Program in + a commercial product offering, such Contributor ("Commercial Contributor") + hereby agrees to defend and indemnify every other Contributor + ("Indemnified Contributor") against any losses, damages and costs + (collectively "Losses") arising from claims, lawsuits and other legal + actions brought by a third party against the Indemnified Contributor to + the extent caused by the acts or omissions of such Commercial Contributor + in connection with its distribution of the Program in a commercial + product offering. The obligations in this section do not apply to any + claims or Losses relating to any actual or alleged intellectual property + infringement. In order to qualify, an Indemnified Contributor must: + a) promptly notify the Commercial Contributor in writing of such claim, + and b) allow the Commercial Contributor to control, and cooperate with + the Commercial Contributor in, the defense and any related settlement + negotiations. The Indemnified Contributor may participate in any such + claim at its own expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance claims, + or offers warranties related to Product X, those performance claims and + warranties are such Commercial Contributor's responsibility alone. Under + this section, the Commercial Contributor would have to defend claims + against the other Contributors related to those performance claims and + warranties, and if a court requires any other Contributor to pay any + damages as a result, the Commercial Contributor must pay those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED + ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER + EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR + CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A + PARTICULAR PURPOSE. Each Recipient is solely responsible for determining + the appropriateness of using and distributing the Program and assumes all + risks associated with its exercise of rights under this Agreement , + including but not limited to the risks and costs of program errors, + compliance with applicable laws, damage to or loss of data, programs or + equipment, and unavailability or interruption of operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR + ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING + WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR + DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED + HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further action + by the parties hereto, such provision shall be reformed to the minimum + extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity (including + a cross-claim or counterclaim in a lawsuit) alleging that the Program + itself (excluding combinations of the Program with other software or + hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it fails + to comply with any of the material terms or conditions of this Agreement + and does not cure such failure in a reasonable period of time after + becoming aware of such noncompliance. If all Recipient's rights under + this Agreement terminate, Recipient agrees to cease use and distribution + of the Program as soon as reasonably practicable. However, Recipient's + obligations under this Agreement and any licenses granted by Recipient + relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and may + only be modified in the following manner. The Agreement Steward reserves + the right to publish new versions (including revisions) of this Agreement + from time to time. No one other than the Agreement Steward has the right + to modify this Agreement. The Eclipse Foundation is the initial Agreement + Steward. The Eclipse Foundation may assign the responsibility to serve as + the Agreement Steward to a suitable separate entity. Each new version of + the Agreement will be given a distinguishing version number. The Program + (including Contributions) may always be distributed subject to the version + of the Agreement under which it was received. In addition, after a new + version of the Agreement is published, Contributor may elect to distribute + the Program (including its Contributions) under the new version. Except as + expressly stated in Sections 2(a) and 2(b) above, Recipient receives no + rights or licenses to the intellectual property of any Contributor under + this Agreement, whether expressly, by implication, estoppel or otherwise. + All rights in the Program not expressly granted under this Agreement + are reserved. + + This Agreement is governed by the laws of the State of New York and the + intellectual property laws of the United States of America. No party to + this Agreement will bring a legal action under this Agreement more than + one year after the cause of action arose. Each party waives its rights to + a jury trial in any resulting litigation. + +juniversalchardet library (juniversalchardet) + + MOZILLA PUBLIC LICENSE + Version 1.1 + + --------------- + + 1. Definitions. + + 1.0.1. "Commercial Use" means distribution or otherwise making the + Covered Code available to a third party. + + 1.1. "Contributor" means each entity that creates or contributes to + the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Code, prior Modifications used by a Contributor, and the Modifications + made by that particular Contributor. + + 1.3. "Covered Code" means the Original Code or Modifications or the + combination of the Original Code and Modifications, in each case + including portions thereof. + + 1.4. "Electronic Distribution Mechanism" means a mechanism generally + accepted in the software development community for the electronic + transfer of data. + + 1.5. "Executable" means Covered Code in any form other than Source + Code. + + 1.6. "Initial Developer" means the individual or entity identified + as the Initial Developer in the Source Code notice required by Exhibit + A. + + 1.7. "Larger Work" means a work which combines Covered Code or + portions thereof with code not governed by the terms of this License. + + 1.8. "License" means this document. + + 1.8.1. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed herein. + + 1.9. "Modifications" means any addition to or deletion from the + substance or structure of either the Original Code or any previous + Modifications. When Covered Code is released as a series of files, a + Modification is: + A. Any addition to or deletion from the contents of a file + containing Original Code or previous Modifications. + + B. Any new file that contains any part of the Original Code or + previous Modifications. + + 1.10. "Original Code" means Source Code of computer software code + which is described in the Source Code notice required by Exhibit A as + Original Code, and which, at the time of its release under this + License is not already Covered Code governed by this License. + + 1.10.1. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, process, + and apparatus claims, in any patent Licensable by grantor. + + 1.11. "Source Code" means the preferred form of the Covered Code for + making modifications to it, including all modules it contains, plus + any associated interface definition files, scripts used to control + compilation and installation of an Executable, or source code + differential comparisons against either the Original Code or another + well known, available Covered Code of the Contributor's choice. The + Source Code can be in a compressed or archival form, provided the + appropriate decompression or de-archiving software is widely available + for no charge. + + 1.12. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms of, this + License or a future version of this License issued under Section 6.1. + For legal entities, "You" includes any entity which controls, is + controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, + to cause the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty percent + (50%) of the outstanding shares or beneficial ownership of such + entity. + + 2. Source Code License. + + 2.1. The Initial Developer Grant. + The Initial Developer hereby grants You a world-wide, royalty-free, + non-exclusive license, subject to third party intellectual property + claims: + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer to use, reproduce, + modify, display, perform, sublicense and distribute the Original + Code (or portions thereof) with or without Modifications, and/or + as part of a Larger Work; and + + (b) under Patents Claims infringed by the making, using or + selling of Original Code, to make, have made, use, practice, + sell, and offer for sale, and/or otherwise dispose of the + Original Code (or portions thereof). + + (c) the licenses granted in this Section 2.1(a) and (b) are + effective on the date Initial Developer first distributes + Original Code under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: 1) for code that You delete from the Original Code; 2) + separate from the Original Code; or 3) for infringements caused + by: i) the modification of the Original Code or ii) the + combination of the Original Code with other software or devices. + + 2.2. Contributor Grant. + Subject to third party intellectual property claims, each Contributor + hereby grants You a world-wide, royalty-free, non-exclusive license + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor, to use, reproduce, modify, + display, perform, sublicense and distribute the Modifications + created by such Contributor (or portions thereof) either on an + unmodified basis, with other Modifications, as Covered Code + and/or as part of a Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either alone + and/or in combination with its Contributor Version (or portions + of such combination), to make, use, sell, offer for sale, have + made, and/or otherwise dispose of: 1) Modifications made by that + Contributor (or portions thereof); and 2) the combination of + Modifications made by that Contributor with its Contributor + Version (or portions of such combination). + + (c) the licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first makes Commercial Use of + the Covered Code. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: 1) for any code that Contributor has deleted from the + Contributor Version; 2) separate from the Contributor Version; + 3) for infringements caused by: i) third party modifications of + Contributor Version or ii) the combination of Modifications made + by that Contributor with other software (except as part of the + Contributor Version) or other devices; or 4) under Patent Claims + infringed by Covered Code in the absence of Modifications made by + that Contributor. + + 3. Distribution Obligations. + + 3.1. Application of License. + The Modifications which You create or to which You contribute are + governed by the terms of this License, including without limitation + Section 2.2. The Source Code version of Covered Code may be + distributed only under the terms of this License or a future version + of this License released under Section 6.1, and You must include a + copy of this License with every copy of the Source Code You + distribute. You may not offer or impose any terms on any Source Code + version that alters or restricts the applicable version of this + License or the recipients' rights hereunder. However, You may include + an additional document offering the additional rights described in + Section 3.5. + + 3.2. Availability of Source Code. + Any Modification which You create or to which You contribute must be + made available in Source Code form under the terms of this License + either on the same media as an Executable version or via an accepted + Electronic Distribution Mechanism to anyone to whom you made an + Executable version available; and if made available via Electronic + Distribution Mechanism, must remain available for at least twelve (12) + months after the date it initially became available, or at least six + (6) months after a subsequent version of that particular Modification + has been made available to such recipients. You are responsible for + ensuring that the Source Code version remains available even if the + Electronic Distribution Mechanism is maintained by a third party. + + 3.3. Description of Modifications. + You must cause all Covered Code to which You contribute to contain a + file documenting the changes You made to create that Covered Code and + the date of any change. You must include a prominent statement that + the Modification is derived, directly or indirectly, from Original + Code provided by the Initial Developer and including the name of the + Initial Developer in (a) the Source Code, and (b) in any notice in an + Executable version or related documentation in which You describe the + origin or ownership of the Covered Code. + + 3.4. Intellectual Property Matters + (a) Third Party Claims. + If Contributor has knowledge that a license under a third party's + intellectual property rights is required to exercise the rights + granted by such Contributor under Sections 2.1 or 2.2, + Contributor must include a text file with the Source Code + distribution titled "LEGAL" which describes the claim and the + party making the claim in sufficient detail that a recipient will + know whom to contact. If Contributor obtains such knowledge after + the Modification is made available as described in Section 3.2, + Contributor shall promptly modify the LEGAL file in all copies + Contributor makes available thereafter and shall take other steps + (such as notifying appropriate mailing lists or newsgroups) + reasonably calculated to inform those who received the Covered + Code that new knowledge has been obtained. + + (b) Contributor APIs. + If Contributor's Modifications include an application programming + interface and Contributor has knowledge of patent licenses which + are reasonably necessary to implement that API, Contributor must + also include this information in the LEGAL file. + + (c) Representations. + Contributor represents that, except as disclosed pursuant to + Section 3.4(a) above, Contributor believes that Contributor's + Modifications are Contributor's original creation(s) and/or + Contributor has sufficient rights to grant the rights conveyed by + this License. + + 3.5. Required Notices. + You must duplicate the notice in Exhibit A in each file of the Source + Code. If it is not possible to put such notice in a particular Source + Code file due to its structure, then You must include such notice in a + location (such as a relevant directory) where a user would be likely + to look for such a notice. If You created one or more Modification(s) + You may add your name as a Contributor to the notice described in + Exhibit A. You must also duplicate this License in any documentation + for the Source Code where You describe recipients' rights or ownership + rights relating to Covered Code. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Code. However, You + may do so only on Your own behalf, and not on behalf of the Initial + Developer or any Contributor. You must make it absolutely clear than + any such warranty, support, indemnity or liability obligation is + offered by You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, + support, indemnity or liability terms You offer. + + 3.6. Distribution of Executable Versions. + You may distribute Covered Code in Executable form only if the + requirements of Section 3.1-3.5 have been met for that Covered Code, + and if You include a notice stating that the Source Code version of + the Covered Code is available under the terms of this License, + including a description of how and where You have fulfilled the + obligations of Section 3.2. The notice must be conspicuously included + in any notice in an Executable version, related documentation or + collateral in which You describe recipients' rights relating to the + Covered Code. You may distribute the Executable version of Covered + Code or ownership rights under a license of Your choice, which may + contain terms different from this License, provided that You are in + compliance with the terms of this License and that the license for the + Executable version does not attempt to limit or alter the recipient's + rights in the Source Code version from the rights set forth in this + License. If You distribute the Executable version under a different + license You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial + Developer or any Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by + the Initial Developer or such Contributor as a result of any such + terms You offer. + + 3.7. Larger Works. + You may create a Larger Work by combining Covered Code with other code + not governed by the terms of this License and distribute the Larger + Work as a single product. In such a case, You must make sure the + requirements of this License are fulfilled for the Covered Code. + + 4. Inability to Comply Due to Statute or Regulation. + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Code due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description + must be included in the LEGAL file described in Section 3.4 and must + be included with all distributions of the Source Code. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + + 5. Application of this License. + + This License applies to code to which the Initial Developer has + attached the notice in Exhibit A and to related Covered Code. + + 6. Versions of the License. + + 6.1. New Versions. + Netscape Communications Corporation ("Netscape") may publish revised + and/or new versions of the License from time to time. Each version + will be given a distinguishing version number. + + 6.2. Effect of New Versions. + Once Covered Code has been published under a particular version of the + License, You may always continue to use it under the terms of that + version. You may also choose to use such Covered Code under the terms + of any subsequent version of the License published by Netscape. No one + other than Netscape has the right to modify the terms applicable to + Covered Code created under this License. + + 6.3. Derivative Works. + If You create or use a modified version of this License (which you may + only do in order to apply it to code which is not already Covered Code + governed by this License), You must (a) rename Your license so that + the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", + "MPL", "NPL" or any confusingly similar phrase do not appear in your + license (except to note that your license differs from this License) + and (b) otherwise make it clear that Your version of the license + contains terms which differ from the Mozilla Public License and + Netscape Public License. (Filling in the name of the Initial + Developer, Original Code or Contributor in the notice described in + Exhibit A shall not of themselves be deemed to be modifications of + this License.) + + 7. DISCLAIMER OF WARRANTY. + + COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, + WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF + DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. + THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE + IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE + COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + + 8. TERMINATION. + + 8.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. All + sublicenses to the Covered Code which are properly granted shall + survive any termination of this License. Provisions which, by their + nature, must remain in effect beyond the termination of this License + shall survive. + + 8.2. If You initiate litigation by asserting a patent infringement + claim (excluding declatory judgment actions) against Initial Developer + or a Contributor (the Initial Developer or Contributor against whom + You file such action is referred to as "Participant") alleging that: + + (a) such Participant's Contributor Version directly or indirectly + infringes any patent, then any and all rights granted by such + Participant to You under Sections 2.1 and/or 2.2 of this License + shall, upon 60 days notice from Participant terminate prospectively, + unless if within 60 days after receipt of notice You either: (i) + agree in writing to pay Participant a mutually agreeable reasonable + royalty for Your past and future use of Modifications made by such + Participant, or (ii) withdraw Your litigation claim with respect to + the Contributor Version against such Participant. If within 60 days + of notice, a reasonable royalty and payment arrangement are not + mutually agreed upon in writing by the parties or the litigation claim + is not withdrawn, the rights granted by Participant to You under + Sections 2.1 and/or 2.2 automatically terminate at the expiration of + the 60 day notice period specified above. + + (b) any software, hardware, or device, other than such Participant's + Contributor Version, directly or indirectly infringes any patent, then + any rights granted to You by such Participant under Sections 2.1(b) + and 2.2(b) are revoked effective as of the date You first made, used, + sold, distributed, or had made, Modifications made by that + Participant. + + 8.3. If You assert a patent infringement claim against Participant + alleging that such Participant's Contributor Version directly or + indirectly infringes any patent where such claim is resolved (such as + by license or settlement) prior to the initiation of patent + infringement litigation, then the reasonable value of the licenses + granted by such Participant under Sections 2.1 or 2.2 shall be taken + into account in determining the amount or value of any payment or + license. + + 8.4. In the event of termination under Sections 8.1 or 8.2 above, + all end user license agreements (excluding distributors and resellers) + which have been validly granted by You or any distributor hereunder + prior to termination shall survive termination. + + 9. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL + DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, + OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR + ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY + CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, + WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO + THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + + 10. U.S. GOVERNMENT END USERS. + + The Covered Code is a "commercial item," as that term is defined in + 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer + software" and "commercial computer software documentation," as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 + C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), + all U.S. Government End Users acquire Covered Code with only those + rights set forth herein. + + 11. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed by + California law provisions (except to the extent applicable law, if + any, provides otherwise), excluding its conflict-of-law provisions. + With respect to disputes in which at least one party is a citizen of, + or an entity chartered or registered to do business in the United + States of America, any litigation relating to this License shall be + subject to the jurisdiction of the Federal Courts of the Northern + District of California, with venue lying in Santa Clara County, + California, with the losing party responsible for costs, including + without limitation, court costs and reasonable attorneys' fees and + expenses. The application of the United Nations Convention on + Contracts for the International Sale of Goods is expressly excluded. + Any law or regulation which provides that the language of a contract + shall be construed against the drafter shall not apply to this + License. + + 12. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or indirectly, + out of its utilization of rights under this License and You agree to + work with Initial Developer and Contributors to distribute such + responsibility on an equitable basis. Nothing herein is intended or + shall be deemed to constitute any admission of liability. + + 13. MULTIPLE-LICENSED CODE. + + Initial Developer may designate portions of the Covered Code as + "Multiple-Licensed". "Multiple-Licensed" means that the Initial + Developer permits you to utilize portions of the Covered Code under + Your choice of the NPL or the alternative licenses, if any, specified + by the Initial Developer in the file described in Exhibit A. + + EXHIBIT A -Mozilla Public License. + + ``The contents of this file are subject to the Mozilla Public License + Version 1.1 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + http://www.mozilla.org/MPL/ + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + License for the specific language governing rights and limitations + under the License. + + The Original Code is ______________________________________. + + The Initial Developer of the Original Code is ________________________. + Portions created by ______________________ are Copyright (C) ______ + _______________________. All Rights Reserved. + + Contributor(s): ______________________________________. + + Alternatively, the contents of this file may be used under the terms + of the _____ license (the "[___] License"), in which case the + provisions of [______] License are applicable instead of those + above. If you wish to allow use of your version of this file only + under the terms of the [____] License and not to allow others to use + your version of this file under the MPL, indicate your decision by + deleting the provisions above and replace them with the notice and + other provisions required by the [___] License. If you do not delete + the provisions above, a recipient may use your version of this file + under either the MPL or the [___] License." + + [NOTE: The text of this Exhibit A may differ slightly from the text of + the notices in the Source Code files of the Original Code. You should + use the text of this Exhibit A rather than the text found in the + Original Code Source Code for Your Modifications.] diff --git a/jackrabbit-jca/src/main/rar/META-INF/NOTICE b/jackrabbit-jca/src/main/rar/META-INF/NOTICE new file mode 100644 index 00000000000..362dbf5a9f7 --- /dev/null +++ b/jackrabbit-jca/src/main/rar/META-INF/NOTICE @@ -0,0 +1,41 @@ +Apache Jackrabbit +Copyright 2010 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Based on source code originally developed by +Day Software (http://www.day.com/). + +This product includes software from the following contributions: + +Original BZip2 classes contributed by Keiron Liddle +, Aftex Software to the Apache Ant project + +Original Tar classes from contributors of the Apache Ant project + +Original Zip classes from contributors of the Apache Ant project + +Original CPIO classes contributed by Markus Kuss and the jRPM project +(jrpm.sourceforge.net) + +Portions of Derby were originally developed by International Business +Machines Corporation and are licensed to the Apache Software Foundation +under the "Software Grant and Corporate Contribution License Agreement", +informally known as the "Derby CLA". The following copyright notice(s) +were affixed to portions of the code with which this file is now or was +at one time distributed and are placed here unaltered. + + (C) Copyright 1997,2004 International Business Machines Corporation. + All rights reserved. + + (C) Copyright IBM Corp. 2003. + +The JDBC apis for small devices and JDBC3 (under java/stubs/jsr169 and +java/stubs/jdbc3) were produced by trimming sources supplied by the +Apache Harmony project. The following notice covers the Harmony sources: + + Portions of Harmony were originally developed by + Intel Corporation and are licensed to the Apache Software + Foundation under the "Software Grant and Corporate Contribution + License Agreement", informally known as the "Intel Harmony CLA". diff --git a/jackrabbit-jca/src/main/rar/META-INF/ra.xml b/jackrabbit-jca/src/main/rar/META-INF/ra.xml new file mode 100644 index 00000000000..90d30d35f43 --- /dev/null +++ b/jackrabbit-jca/src/main/rar/META-INF/ra.xml @@ -0,0 +1,72 @@ + + + + + Apache Jackrabbit JCR Adapter + The Apache Software Foundation + JCR Adapter + 1.0 + + + + org.apache.jackrabbit.jca.JCAResourceAdapter + + + + + org.apache.jackrabbit.jca.JCAManagedConnectionFactory + + + + RepositoryURI + java.lang.String + + + HomeDir + java.lang.String + + + ConfigFile + java.lang.String + + + + javax.jcr.Repository + + + org.apache.jackrabbit.jca.JCARepositoryHandle + + + + javax.jcr.Session + + + org.apache.jackrabbit.jca.JCASessionHandle + + + + XATransaction + false + + + + diff --git a/jackrabbit-jca/src/main/rar10/META-INF/ra.xml b/jackrabbit-jca/src/main/rar10/META-INF/ra.xml new file mode 100644 index 00000000000..8c3dca6575e --- /dev/null +++ b/jackrabbit-jca/src/main/rar10/META-INF/ra.xml @@ -0,0 +1,70 @@ + + + + + Apache Jackrabbit JCR Adapter + The Apache Software Foundation + 1.0 + JCR Adapter + 1.0 + + + Apache License, Version 2.0 + false + + + + + org.apache.jackrabbit.jca.JCAManagedConnectionFactory + + + + javax.jcr.Repository + + + org.apache.jackrabbit.jca.JCARepositoryHandle + + + + javax.jcr.Session + + + org.apache.jackrabbit.jca.JCASessionHandle + + + XATransaction + + + RepositoryURI + java.lang.String + + + HomeDir + java.lang.String + + + ConfigFile + java.lang.String + + + false + + + diff --git a/jackrabbit-jca/src/main/resources/logback.xml b/jackrabbit-jca/src/main/resources/logback.xml new file mode 100644 index 00000000000..f3742d41796 --- /dev/null +++ b/jackrabbit-jca/src/main/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/AbstractTestCase.java b/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/AbstractTestCase.java new file mode 100644 index 00000000000..6b4ed86d012 --- /dev/null +++ b/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/AbstractTestCase.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.jca.test; + +import junit.framework.TestCase; +import org.apache.jackrabbit.jca.JCAManagedConnectionFactory; + +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import java.io.File; + +/** + * This implements the abstract test case. + */ +public abstract class AbstractTestCase + extends TestCase { + + /** + * Repository home directory. + */ + public static final String JCR_HOME_DIR = "target/repository"; + + /** + * Repository configuration file. + */ + public static final String JCR_CONFIG_FILE = + "classpath:org/apache/jackrabbit/core/repository.xml"; + + /** + * Default credentials. + */ + public static final Credentials JCR_SUPERUSER = + new SimpleCredentials("admin", "admin".toCharArray()); + + /** + * Anonymous credentials. + */ + public static final Credentials JCR_ANONUSER = + new SimpleCredentials("anonymous", new char[0]); + + /** + * Repository workspace. + */ + public static final String JCR_WORKSPACE = "default"; + + /** + * Managed connection factory. + */ + protected JCAManagedConnectionFactory mcf; + + /** + * Setup the test. + */ + protected void setUp() throws Exception { + File home = new File(JCR_HOME_DIR); + if (!home.exists()) { + home.mkdirs(); + } + + // Construct the managed connection factory + this.mcf = new JCAManagedConnectionFactory(); + this.mcf.setHomeDir(JCR_HOME_DIR); + this.mcf.setConfigFile(JCR_CONFIG_FILE); + } + +} diff --git a/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/ConnectionFactoryTest.java b/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/ConnectionFactoryTest.java new file mode 100644 index 00000000000..d0d2fcfae52 --- /dev/null +++ b/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/ConnectionFactoryTest.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.jca.test; + +import java.io.Serializable; +import java.util.HashSet; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.naming.Referenceable; +import javax.resource.spi.ManagedConnection; +import javax.transaction.xa.XAResource; + +import org.apache.jackrabbit.jca.JCAConnectionRequestInfo; +import org.apache.jackrabbit.jca.JCARepositoryHandle; +import org.apache.jackrabbit.jca.JCASessionHandle; + +/** + * This case executes tests on the connection factory. + */ +public final class ConnectionFactoryTest + extends AbstractTestCase { + + /** + * Test the connection factory allocation. + */ + public void testAllocation() throws Exception { + + // Create the connection factory + Object cf = mcf.createConnectionFactory(); + assertTrue(cf instanceof JCARepositoryHandle); + Repository repository = (Repository) cf; + + // Open a new session + Session session = repository.login(JCR_SUPERUSER); + assertTrue(session != null); + assertTrue(session instanceof JCASessionHandle); + + // Logout session + session.logout(); + } + + /** + * Test the connection matching. + */ + public void testMatching() throws Exception { + + // Create connection request infos + JCAConnectionRequestInfo cri1 = new JCAConnectionRequestInfo(JCR_SUPERUSER, JCR_WORKSPACE); + JCAConnectionRequestInfo cri2 = new JCAConnectionRequestInfo(JCR_ANONUSER, JCR_WORKSPACE); + + // Check if not same + assertNotSame(cri1, cri2); + + // Create the connection factory + mcf.createConnectionFactory(); + + // Allocate connections + ManagedConnection mc1 = mcf.createManagedConnection(null, cri1); + ManagedConnection mc2 = mcf.createManagedConnection(null, cri2); + + // Check if not same + assertTrue(mc1 != mc2); + + // Create a sef of connections + HashSet connectionSet = new HashSet(); + connectionSet.add(mc1); + connectionSet.add(mc2); + + // Match the first connection + JCAConnectionRequestInfo cri3 = new JCAConnectionRequestInfo(cri1); + assertTrue((cri1 != cri3) && cri1.equals(cri3)); + ManagedConnection mc3 = mcf.matchManagedConnections(connectionSet, null, cri3); + assertTrue(mc1 == mc3); + + // Match the second connection + JCAConnectionRequestInfo cri4 = new JCAConnectionRequestInfo(cri2); + assertTrue((cri2 != cri4) && cri2.equals(cri4)); + ManagedConnection mc4 = mcf.matchManagedConnections(connectionSet, null, cri4); + assertTrue(mc2 == mc4); + } + + /** + * Test if the connection factory is serializable. + */ + public void testSerializable() throws Exception { + + // Create the connection factory + Object cf = mcf.createConnectionFactory(); + + // Check if serializable and referenceable + assertTrue(cf != null); + assertTrue(cf instanceof Serializable); + assertTrue(cf instanceof Referenceable); + } + + /** + * Test if the session supports transactions + */ + public void testTransactionSupport() throws Exception { + // Create the connection factory + Object cf = mcf.createConnectionFactory(); + assertTrue(cf instanceof JCARepositoryHandle); + Repository repository = (Repository) cf; + + // Open a session + Session session = repository.login(JCR_SUPERUSER); + assertTrue(session instanceof XAResource); + session.logout(); + } + + /** + * Tests if a NoSuchWorkspaceException is thrown if a wrong workspace name is given to login + */ + public void testExceptionHandling() throws Exception { + Object cf = mcf.createConnectionFactory(); + Repository repository = (Repository) cf; + try { + repository.login(JCR_SUPERUSER, "xxx"); + } catch (Exception e) { + assertTrue(e instanceof NoSuchWorkspaceException); + } + } + +} diff --git a/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/ConnectionRequestInfoTest.java b/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/ConnectionRequestInfoTest.java new file mode 100644 index 00000000000..58fd25c2d87 --- /dev/null +++ b/jackrabbit-jca/src/test/java/org/apache/jackrabbit/jca/test/ConnectionRequestInfoTest.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.jca.test; + +import org.apache.jackrabbit.jca.JCAConnectionRequestInfo; + +import javax.jcr.SimpleCredentials; + +import java.util.HashMap; + +import junit.framework.TestCase; + +/** + * This case executes tests on the connection request info. + */ +public final class ConnectionRequestInfoTest + extends TestCase { + + private SimpleCredentials creds1 = new SimpleCredentials("user", "password".toCharArray()); + private SimpleCredentials creds2 = new SimpleCredentials("user", "password".toCharArray()); + private SimpleCredentials creds3 = new SimpleCredentials("another_user", "password".toCharArray()); + private JCAConnectionRequestInfo info1 = new JCAConnectionRequestInfo(creds1, "default"); + private JCAConnectionRequestInfo info2 = new JCAConnectionRequestInfo(creds2, "default"); + private JCAConnectionRequestInfo info3 = new JCAConnectionRequestInfo(creds3, "default"); + + /** + * Test the JCAConnectionRequestInfo equals() method. + */ + public void testEquals() throws Exception { + assertEquals("Object must be equal to itself", info1, info1); + assertEquals("Infos with the same auth data must be equal", info1, info2); + assertTrue("Infos with different auth data must not be equal", !info1.equals(info3)); + } + + /** + * Test the JCAConnectionRequestInfo hashCode() method. + */ + public void testHashCode() throws Exception { + assertEquals("Object must be equal to itself", info1.hashCode(), info1.hashCode()); + assertEquals("Infos with the same auth data must have same hashCode", info1.hashCode(), info2.hashCode()); + assertTrue("Infos with different auth data must not have same hashCode", info1.hashCode() != info3.hashCode()); + } + + /** + * Tests that JCAConnectionRequestInfo works as a HashMap key correctly. + */ + public void testPutToHashMap() throws Exception { + HashMap map = new HashMap(); + map.put(info1, new Object()); + assertTrue("Map must contain the info", map.containsKey(info2)); + } +} diff --git a/jackrabbit-jca/src/test/resources/logback-test.xml b/jackrabbit-jca/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..ac54c9f79b6 --- /dev/null +++ b/jackrabbit-jca/src/test/resources/logback-test.xml @@ -0,0 +1,33 @@ + + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + + + diff --git a/jackrabbit-jcr-client/README.txt b/jackrabbit-jcr-client/README.txt new file mode 100644 index 00000000000..c835b80ab79 --- /dev/null +++ b/jackrabbit-jcr-client/README.txt @@ -0,0 +1,6 @@ +================================= +Welcome to Jackrabbit JCR Client +================================= + +This is the Jackrabbit JCR Client component of the Apache Jackrabbit project. + diff --git a/jackrabbit-jcr-client/pom.xml b/jackrabbit-jcr-client/pom.xml new file mode 100644 index 00000000000..07bd4323bd6 --- /dev/null +++ b/jackrabbit-jcr-client/pom.xml @@ -0,0 +1,138 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-jcr-client + Jackrabbit JCR Client + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + org.apache.rat + apache-rat-plugin + + + repository/** + repository.xml + derby.log + + + + + + + + + integration-test + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + **/*Test.java + + target + once + ${test.opts} + + + + + + + + + + javax.jcr + jcr + + + org.slf4j + slf4j-api + + + + org.apache.jackrabbit + jackrabbit-jcr2spi + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-spi2dav + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-spi2jcr + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-spi-commons + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-spi + ${project.version} + test + + + junit + junit + test + + + ch.qos.logback + logback-classic + test + + + diff --git a/jackrabbit-jcr-client/src/main/java/org/apache/jackrabbit/client/RepositoryFactoryImpl.java b/jackrabbit-jcr-client/src/main/java/org/apache/jackrabbit/client/RepositoryFactoryImpl.java new file mode 100644 index 00000000000..a8923f4adb4 --- /dev/null +++ b/jackrabbit-jcr-client/src/main/java/org/apache/jackrabbit/client/RepositoryFactoryImpl.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.client; + +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +/** + * This {@link RepositoryFactory} implementations is capable of creating any + * repository which is covered by the Apache Jackrabbit project. It does so by + * delegating back to secondary RepositoryFactory implementations. The + * parameters passed to the {@link #getRepository(Map)} method determine which + * secondary RepositoryFactory this factory delegates to. + */ +public class RepositoryFactoryImpl implements RepositoryFactory { + + /** + * When this key parameter is present, this factory delegates to + * {@code org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory} + */ + public static final String PARAM_REPOSITORY_SERVICE_FACTORY = "org.apache.jackrabbit.spi.RepositoryServiceFactory"; + + /** + * When this key parameter is present, this factory delegates to + * {@code org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory} + */ + public static final String PARAM_REPOSITORY_CONFIG = "org.apache.jackrabbit.jcr2spi.RepositoryConfig"; + + /** + * Creates a JCR repository from the given parameters. + * If either {@link #PARAM_REPOSITORY_SERVICE_FACTORY} or + * {@link #PARAM_REPOSITORY_CONFIG} is present, this factory delegates + * to {@code org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory}. + * Otherwise it delegates to + * {@code org.apache.jackrabbit.core.RepositoryFactoryImpl}. + * + * @see RepositoryFactory#getRepository(java.util.Map) + */ + public Repository getRepository(@SuppressWarnings("rawtypes") Map parameters) throws RepositoryException { + String repositoryFactoryName = parameters != null && ( + parameters.containsKey(PARAM_REPOSITORY_SERVICE_FACTORY) || + parameters.containsKey(PARAM_REPOSITORY_CONFIG)) + ? "org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory" + : "org.apache.jackrabbit.core.RepositoryFactoryImpl"; + + Object repositoryFactory; + try { + Class repositoryFactoryClass = Class.forName(repositoryFactoryName, true, + Thread.currentThread().getContextClassLoader()); + + repositoryFactory = repositoryFactoryClass.newInstance(); + } + catch (Exception e) { + throw new RepositoryException(e); + } + + if (repositoryFactory instanceof RepositoryFactory) { + return ((RepositoryFactory) repositoryFactory).getRepository(parameters); + } + else { + throw new RepositoryException(repositoryFactory + " is not a RepositoryFactory"); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-client/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory b/jackrabbit-jcr-client/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory new file mode 100644 index 00000000000..a743537418a --- /dev/null +++ b/jackrabbit-jcr-client/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory @@ -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. + +# +# This file lists the repository factory implementation for jackrabbit-jcr-client +# + +org.apache.jackrabbit.client.RepositoryFactoryImpl + diff --git a/jackrabbit-jcr-client/src/test/java/org/apache/jackrabbit/client/RepositoryFactoryImplTest.java b/jackrabbit-jcr-client/src/test/java/org/apache/jackrabbit/client/RepositoryFactoryImplTest.java new file mode 100644 index 00000000000..1d6f414dd77 --- /dev/null +++ b/jackrabbit-jcr-client/src/test/java/org/apache/jackrabbit/client/RepositoryFactoryImplTest.java @@ -0,0 +1,544 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.client; + +import java.io.InputStream; +import java.net.ConnectException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.MergeException; +import javax.jcr.NamespaceException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PathNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.version.VersionException; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.jcr2spi.config.CacheBehaviour; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.logging.Slf4jLogWriterProvider; +import org.apache.jackrabbit.webdav.DavException; + +public class RepositoryFactoryImplTest extends TestCase { + private final RepositoryFactory factory = new RepositoryFactoryImpl(); + + public void testGetRepositoryFromServiceFactory() throws RepositoryException { + Map parameters = Collections.singletonMap( + "org.apache.jackrabbit.spi.RepositoryServiceFactory", + RepositoryServiceFactoryImpl.INSTANCE); + + Repository repo = factory.getRepository(parameters); + assertNotNull(repo); + } + + public void testGetRepositoryFromRepositoryConfig() throws RepositoryException { + Map parameters = Collections.singletonMap( + "org.apache.jackrabbit.jcr2spi.RepositoryConfig", + RepositoryConfigImpl.INSTANCE); + + Repository repo = factory.getRepository(parameters); + assertNotNull(repo); + } + + public void testGetRepositoryWithLogger() throws RepositoryException { + List lwprovider = new ArrayList(); + lwprovider.add(null); + lwprovider.add(true); + lwprovider.add(new Slf4jLogWriterProvider()); + + Map params = new HashMap(); + params.put("org.apache.jackrabbit.jcr2spi.RepositoryConfig", RepositoryConfigImpl.INSTANCE); + + for (Object aLwprovider : lwprovider) { + params.put("org.apache.jackrabbit.spi.commons.logging.LogWriterProvider", aLwprovider); + Repository repo = factory.getRepository(params); + assertNotNull(repo); + } + } + + public void testGetDefaultRepository() throws RepositoryException { + Repository repo = factory.getRepository(null); + assertNotNull(repo); + assertEquals("Jackrabbit", repo.getDescriptor(Repository.REP_NAME_DESC)); + } + + public void testGetSpi2jcrRepository() throws RepositoryException { + Repository coreRepo = factory.getRepository(null); + + HashMap parameters = new HashMap(); + parameters.put("org.apache.jackrabbit.spi.RepositoryServiceFactory", + "org.apache.jackrabbit.spi2jcr.Spi2jcrRepositoryServiceFactory"); + parameters.put("org.apache.jackrabbit.spi2jcr.Repository", coreRepo); + + Repository jcr2spiRepo = factory.getRepository(parameters); + assertNotNull(jcr2spiRepo); + assertEquals("Jackrabbit", jcr2spiRepo.getDescriptor(Repository.REP_NAME_DESC)); + } + + public void testGetSpi2davRepository() throws RepositoryException { + Map parameters = new HashMap(); + + parameters.put("org.apache.jackrabbit.spi.RepositoryServiceFactory", + "org.apache.jackrabbit.spi2dav.Spi2davRepositoryServiceFactory"); + parameters.put("org.apache.jackrabbit.spi2dav.uri", + "http://localhost/"); + + try { + Repository repo = factory.getRepository(parameters); + assertNotNull(repo); + } catch (RepositoryException e) { + // If there is no jcr server on localhost, one of the below + // exceptions will be thrown. Since this indicates that the + // factory is working correctly, it is safe to ignore them. + if (!(ConnectException.class.isInstance(e.getCause()) || + DavException.class.isInstance(e.getCause()))) { + throw e; + } + } + } + + public void testGetSpi2davexRepository() throws RepositoryException { + Map parameters = Collections.singletonMap( + "org.apache.jackrabbit.spi.RepositoryServiceFactory", + "org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory"); + + try { + Repository repo = factory.getRepository(parameters); + assertNotNull(repo); + } catch (RepositoryException e) { + // If there is no jcr server on localhost, one of the below + // exceptions will be thrown. Since this indicates that the + // factory is working correctly, it is safe to ignore them. + if (!(ConnectException.class.isInstance(e.getCause()) || + DavException.class.isInstance(e.getCause()))) { + throw e; + } + } + } + + public void testGetRepositoryUnknownParams() throws RepositoryException { + Repository repo = factory.getRepository(Collections.emptyMap()); + assertNull(repo); + } + + // -----------------------------------------------------< private >--- + + /** + * Dummy RepositoryServiceFactory + */ + private static final class RepositoryServiceFactoryImpl implements RepositoryServiceFactory { + public static final RepositoryServiceFactory INSTANCE = new RepositoryServiceFactoryImpl(); + + private RepositoryServiceFactoryImpl() { + super(); + } + + public RepositoryService createRepositoryService(Map parameters) throws RepositoryException { + return RepositoryServiceImpl.INSTANCE; + } + } + + /** + * Dummy RepositoryConfig + */ + private static final class RepositoryConfigImpl implements RepositoryConfig { + public static final RepositoryConfig INSTANCE = new RepositoryConfigImpl(); + + private RepositoryConfigImpl() { + super(); + } + + public CacheBehaviour getCacheBehaviour() { + return CacheBehaviour.INVALIDATE; + } + + public int getItemCacheSize() { + return 1234; + } + + public int getPollTimeout() { + return 1234; + } + + @Override + public T getConfiguration(String name, T defaultValue) { + return null; + } + + public RepositoryService getRepositoryService() throws RepositoryException { + return RepositoryServiceImpl.INSTANCE; + } + + } + + /** + * Dummy RepositoryService + */ + private static final class RepositoryServiceImpl implements RepositoryService { + + public static final RepositoryService INSTANCE = new RepositoryServiceImpl(); + + private RepositoryServiceImpl() { + super(); + } + + public IdFactory getIdFactory() throws RepositoryException { + return null; + } + + public NameFactory getNameFactory() throws RepositoryException { + return null; + } + + public PathFactory getPathFactory() throws RepositoryException { + return null; + } + + public QValueFactory getQValueFactory() throws RepositoryException { + return null; + } + + public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) throws RepositoryException { + return null; + } + + public Map getRepositoryDescriptors() throws RepositoryException { + return Collections.emptyMap(); + } + + public SessionInfo obtain(Credentials credentials, String workspaceName) throws LoginException, NoSuchWorkspaceException, RepositoryException { + return null; + } + + public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName) throws LoginException, NoSuchWorkspaceException, RepositoryException { + return null; + } + + public SessionInfo impersonate(SessionInfo sessionInfo, Credentials credentials) throws LoginException, RepositoryException { + return null; + } + + public void dispose(SessionInfo sessionInfo) throws RepositoryException { + // empty + } + + public String[] getWorkspaceNames(SessionInfo sessionInfo) throws RepositoryException { + return new String[0]; + } + + public boolean isGranted(SessionInfo sessionInfo, ItemId itemId, String[] actions) throws RepositoryException { + return false; + } + + @Override + public PrivilegeDefinition[] getPrivilegeDefinitions(SessionInfo sessionInfo) throws RepositoryException { + return new PrivilegeDefinition[0]; + } + + @Override + public Name[] getPrivilegeNames(SessionInfo sessionInfo, NodeId id) throws RepositoryException { + return new Name[0]; + } + + @Override + public PrivilegeDefinition[] getSupportedPrivileges(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return new PrivilegeDefinition[0]; + } + + public QNodeDefinition getNodeDefinition(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return null; + } + + public QPropertyDefinition getPropertyDefinition(SessionInfo sessionInfo, PropertyId propertyId) throws RepositoryException { + return null; + } + + public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws ItemNotFoundException, RepositoryException { + return null; + } + + public Iterator getItemInfos(SessionInfo sessionInfo, ItemId itemId) throws ItemNotFoundException, RepositoryException { + return null; + } + + public Iterator getChildInfos(SessionInfo sessionInfo, NodeId parentId) throws ItemNotFoundException, RepositoryException { + return null; + } + + public Iterator getReferences(SessionInfo sessionInfo, NodeId nodeId, Name propertyName, boolean weakReferences) throws ItemNotFoundException, RepositoryException { + return null; + } + + public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId) throws ItemNotFoundException, RepositoryException { + return null; + } + + public Batch createBatch(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException { + return null; + } + + public void submit(Batch batch) throws PathNotFoundException, ItemNotFoundException, NoSuchNodeTypeException, ValueFormatException, VersionException, LockException, ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + // empty + } + + @Override + public Tree createTree(SessionInfo sessionInfo, Batch batch, Name nodeName, Name primaryTypeName, String uniqueId) throws RepositoryException { + return null; + } + + public void importXml(SessionInfo sessionInfo, NodeId parentId, InputStream xmlStream, int uuidBehaviour) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + // empty + } + + public void move(SessionInfo sessionInfo, NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + // empty + } + + public void copy(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException { + // empty + } + + public void update(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName) throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException { + // empty + } + + public void clone(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException { + // empty + } + + public LockInfo getLockInfo(SessionInfo sessionInfo, NodeId nodeId) throws AccessDeniedException, RepositoryException { + return null; + } + + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, boolean sessionScoped) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + return null; + } + + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, boolean sessionScoped, long timeoutHint, String ownerHint) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + return null; + } + + public void refreshLock(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + // empty + } + + public void unlock(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + // empty + } + + public NodeId checkin(SessionInfo sessionInfo, NodeId nodeId) throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException { + return null; + } + + public void checkout(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + // empty + } + + public void checkout(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + // empty + } + + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public void removeVersion(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId) throws ReferentialIntegrityException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + // empty + } + + public void restore(SessionInfo sessionInfo, NodeId nodeId, NodeId versionId, boolean removeExisting) throws VersionException, PathNotFoundException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + // empty + } + + public void restore(SessionInfo sessionInfo, NodeId[] versionIds, boolean removeExisting) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException { + // empty + } + + public Iterator merge(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName, boolean bestEffort) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + return null; + } + + public Iterator merge(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName, boolean bestEffort, boolean isShallow) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + return null; + } + + public void resolveMergeConflict(SessionInfo sessionInfo, NodeId nodeId, NodeId[] mergeFailedIds, NodeId[] predecessorIds) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + // empty + } + + public void addVersionLabel(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId, Name label, boolean moveLabel) throws VersionException, RepositoryException { + // empty + } + + public void removeVersionLabel(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId, Name label) throws VersionException, RepositoryException { + // empty + } + + public NodeId createActivity(SessionInfo sessionInfo, String title) throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public void removeActivity(SessionInfo sessionInfo, NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + // empty + } + + public Iterator mergeActivity(SessionInfo sessionInfo, NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public NodeId createConfiguration(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public String[] getSupportedQueryLanguages(SessionInfo sessionInfo) throws RepositoryException { + return new String[0]; + } + + public String[] checkQueryStatement(SessionInfo sessionInfo, String statement, String language, Map namespaces) throws InvalidQueryException, RepositoryException { + return new String[0]; + } + + public QueryInfo executeQuery(SessionInfo sessionInfo, String statement, String language, Map namespaces, long limit, long offset, Map values) throws RepositoryException { + return null; + } + + public EventFilter createEventFilter(SessionInfo sessionInfo, int eventTypes, Path absPath, boolean isDeep, String[] uuid, Name[] nodeTypeName, boolean noLocal) throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public Subscription createSubscription(SessionInfo sessionInfo, EventFilter[] filters) throws UnsupportedRepositoryOperationException, RepositoryException { + return null; + } + + public void updateEventFilters(Subscription subscription, EventFilter[] filters) throws RepositoryException { + // empty + } + + public EventBundle[] getEvents(Subscription subscription, long timeout) throws RepositoryException, InterruptedException { + return new EventBundle[0]; + } + + public EventBundle getEvents(SessionInfo sessionInfo, EventFilter filter, long after) throws RepositoryException, UnsupportedRepositoryOperationException { + return null; + } + + public void dispose(Subscription subscription) throws RepositoryException { + // empty + } + + public Map getRegisteredNamespaces(SessionInfo sessionInfo) throws RepositoryException { + return null; + } + + public String getNamespaceURI(SessionInfo sessionInfo, String prefix) throws NamespaceException, RepositoryException { + return null; + } + + public String getNamespacePrefix(SessionInfo sessionInfo, String uri) throws NamespaceException, RepositoryException { + return null; + } + + public void registerNamespace(SessionInfo sessionInfo, String prefix, String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + // empty + } + + public void unregisterNamespace(SessionInfo sessionInfo, String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + // empty + } + + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo) throws RepositoryException { + return null; + } + + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo, Name[] nodetypeNames) throws RepositoryException { + return null; + } + + public void registerNodeTypes(SessionInfo sessionInfo, QNodeTypeDefinition[] nodeTypeDefinitions, boolean allowUpdate) throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, UnsupportedRepositoryOperationException, RepositoryException { + // empty + } + + public void unregisterNodeTypes(SessionInfo sessionInfo, Name[] nodeTypeNames) throws UnsupportedRepositoryOperationException, NoSuchNodeTypeException, RepositoryException { + // empty + } + + public void createWorkspace(SessionInfo sessionInfo, String name, String srcWorkspaceName) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException { + // empty + } + + public void deleteWorkspace(SessionInfo sessionInfo, String name) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException { + // empty + } + } + +} diff --git a/jackrabbit-jcr-client/src/test/java/org/apache/jackrabbit/client/RepositoryFactoryTest.java b/jackrabbit-jcr-client/src/test/java/org/apache/jackrabbit/client/RepositoryFactoryTest.java new file mode 100644 index 00000000000..03ecd7fed35 --- /dev/null +++ b/jackrabbit-jcr-client/src/test/java/org/apache/jackrabbit/client/RepositoryFactoryTest.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.client; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import junit.framework.TestCase; + +import java.util.Iterator; +import java.util.ServiceLoader; + +import javax.jcr.RepositoryFactory; + +/** + * RepositoryFactoryTest... + */ +public class RepositoryFactoryTest extends TestCase { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(RepositoryFactoryTest.class); + + public void testGetFactory() { + Iterator it = ServiceLoader.load(RepositoryFactory.class).iterator(); + if (it.hasNext()) { + RepositoryFactory rf = (RepositoryFactory) it.next(); + assertTrue(rf instanceof RepositoryFactoryImpl); + } else { + fail(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/README.txt b/jackrabbit-jcr-commons/README.txt new file mode 100644 index 00000000000..046f2a54174 --- /dev/null +++ b/jackrabbit-jcr-commons/README.txt @@ -0,0 +1,7 @@ +================================= +Welcome to Jackrabbit JCR Commons +================================= + +This is the JCR Commons component of the Apache Jackrabbit project. +This component contains a number of general-purpose classes for +use with the JCR API. diff --git a/jackrabbit-jcr-commons/pom.xml b/jackrabbit-jcr-commons/pom.xml new file mode 100644 index 00000000000..2a8ae6809ac --- /dev/null +++ b/jackrabbit-jcr-commons/pom.xml @@ -0,0 +1,104 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-jcr-commons + Jackrabbit JCR Commons + General purpose classes for use with the JCR API + bundle + + + + + org.apache.felix + maven-bundle-plugin + true + + + + org.apache.jackrabbit.util.Base64 + + + org.apache.jackrabbit.api.security.user;version="[2.2,3)";resolution:=optional, + * + + + + + + + + + + javax.jcr + jcr + + + + org.apache.jackrabbit + jackrabbit-api + ${project.version} + true + + + org.osgi + org.osgi.annotation + provided + + + junit + junit + test + + + cglib + cglib + test + + + com.googlecode.json-simple + json-simple + 1.1.1 + test + + + simple-jndi + simple-jndi + 0.11.4.1 + test + + + + diff --git a/jackrabbit-jcr-commons/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-jcr-commons/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/JcrConstants.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/JcrConstants.java new file mode 100644 index 00000000000..3b15c90d876 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/JcrConstants.java @@ -0,0 +1,311 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit; + +import javax.jcr.Session; + +/** + * This Interface defines some of the item names that are defined in the + * jcr spec 1.0, using the default prefixes 'jcr', 'nt' and 'mix'. Please note + * that those prefixes can by redefined by an application using the + * {@link Session#setNamespacePrefix(String, String)} method. As a result, the + * constants may not refer to the respective items. + */ +public interface JcrConstants { + /** + * jcr:autoCreated + */ + public static final String JCR_AUTOCREATED = "jcr:autoCreated"; + /** + * jcr:baseVersion + */ + public static final String JCR_BASEVERSION = "jcr:baseVersion"; + /** + * jcr:child + */ + public static final String JCR_CHILD = "jcr:child"; + /** + * jcr:childNodeDefinition + */ + public static final String JCR_CHILDNODEDEFINITION = "jcr:childNodeDefinition"; + /** + * jcr:content + */ + public static final String JCR_CONTENT = "jcr:content"; + /** + * jcr:created + */ + public static final String JCR_CREATED = "jcr:created"; + /** + * jcr:data + */ + public static final String JCR_DATA = "jcr:data"; + /** + * jcr:defaultPrimaryType + */ + public static final String JCR_DEFAULTPRIMARYTYPE = "jcr:defaultPrimaryType"; + /** + * jcr:defaultValues + */ + public static final String JCR_DEFAULTVALUES = "jcr:defaultValues"; + /** + * jcr:encoding + */ + public static final String JCR_ENCODING = "jcr:encoding"; + /** + * jcr:frozenMixinTypes + */ + public static final String JCR_FROZENMIXINTYPES = "jcr:frozenMixinTypes"; + /** + * jcr:frozenNode + */ + public static final String JCR_FROZENNODE = "jcr:frozenNode"; + /** + * jcr:frozenPrimaryType + */ + public static final String JCR_FROZENPRIMARYTYPE = "jcr:frozenPrimaryType"; + /** + * jcr:frozenUuid + */ + public static final String JCR_FROZENUUID = "jcr:frozenUuid"; + /** + * jcr:hasOrderableChildNodes + */ + public static final String JCR_HASORDERABLECHILDNODES = "jcr:hasOrderableChildNodes"; + /** + * jcr:isCheckedOut + */ + public static final String JCR_ISCHECKEDOUT = "jcr:isCheckedOut"; + /** + * jcr:isMixin + */ + public static final String JCR_ISMIXIN = "jcr:isMixin"; + /** + * jcr:language + */ + public static final String JCR_LANGUAGE = "jcr:language"; + /** + * jcr:lastModified + */ + public static final String JCR_LASTMODIFIED = "jcr:lastModified"; + /** + * jcr:lockIsDeep + */ + public static final String JCR_LOCKISDEEP = "jcr:lockIsDeep"; + /** + * jcr:lockOwner + */ + public static final String JCR_LOCKOWNER = "jcr:lockOwner"; + /** + * jcr:mandatory + */ + public static final String JCR_MANDATORY = "jcr:mandatory"; + /** + * jcr:mergeFailed + */ + public static final String JCR_MERGEFAILED = "jcr:mergeFailed"; + /** + * jcr:mimeType + */ + public static final String JCR_MIMETYPE = "jcr:mimeType"; + /** + * jcr:mixinTypes + */ + public static final String JCR_MIXINTYPES = "jcr:mixinTypes"; + /** + * jcr:multiple + */ + public static final String JCR_MULTIPLE = "jcr:multiple"; + /** + * jcr:name + */ + public static final String JCR_NAME = "jcr:name"; + /** + * jcr:nodeTypeName + */ + public static final String JCR_NODETYPENAME = "jcr:nodeTypeName"; + /** + * jcr:onParentVersion + */ + public static final String JCR_ONPARENTVERSION = "jcr:onParentVersion"; + /** + * jcr:predecessors + */ + public static final String JCR_PREDECESSORS = "jcr:predecessors"; + /** + * jcr:primaryItemName + */ + public static final String JCR_PRIMARYITEMNAME = "jcr:primaryItemName"; + /** + * jcr:primaryType + */ + public static final String JCR_PRIMARYTYPE = "jcr:primaryType"; + /** + * jcr:propertyDefinition + */ + public static final String JCR_PROPERTYDEFINITION = "jcr:propertyDefinition"; + /** + * jcr:protected + */ + public static final String JCR_PROTECTED = "jcr:protected"; + /** + * jcr:requiredPrimaryTypes + */ + public static final String JCR_REQUIREDPRIMARYTYPES = "jcr:requiredPrimaryTypes"; + /** + * jcr:requiredType + */ + public static final String JCR_REQUIREDTYPE = "jcr:requiredType"; + /** + * jcr:rootVersion + */ + public static final String JCR_ROOTVERSION = "jcr:rootVersion"; + /** + * jcr:sameNameSiblings + */ + public static final String JCR_SAMENAMESIBLINGS = "jcr:sameNameSiblings"; + /** + * jcr:statement + */ + public static final String JCR_STATEMENT = "jcr:statement"; + /** + * jcr:successors + */ + public static final String JCR_SUCCESSORS = "jcr:successors"; + /** + * jcr:supertypes + */ + public static final String JCR_SUPERTYPES = "jcr:supertypes"; + /** + * jcr:system + */ + public static final String JCR_SYSTEM = "jcr:system"; + /** + * jcr:uuid + */ + public static final String JCR_UUID = "jcr:uuid"; + /** + * jcr:valueConstraints + */ + public static final String JCR_VALUECONSTRAINTS = "jcr:valueConstraints"; + /** + * jcr:versionHistory + */ + public static final String JCR_VERSIONHISTORY = "jcr:versionHistory"; + /** + * jcr:versionLabels + */ + public static final String JCR_VERSIONLABELS = "jcr:versionLabels"; + /** + * jcr:versionStorage + */ + public static final String JCR_VERSIONSTORAGE = "jcr:versionStorage"; + /** + * jcr:versionableUuid + */ + public static final String JCR_VERSIONABLEUUID = "jcr:versionableUuid"; + + /** + * Pseudo property jcr:path used with query results + */ + public static final String JCR_PATH = "jcr:path"; + /** + * Pseudo property jcr:score used with query results + */ + public static final String JCR_SCORE = "jcr:score"; + + /** + * mix:lockable + */ + public static final String MIX_LOCKABLE = "mix:lockable"; + /** + * mix:referenceable + */ + public static final String MIX_REFERENCEABLE = "mix:referenceable"; + /** + * mix:versionable + */ + public static final String MIX_VERSIONABLE = "mix:versionable"; + /** + * mix:shareable + */ + public static final String MIX_SHAREABLE = "mix:shareable"; + /** + * nt:base + */ + public static final String NT_BASE = "nt:base"; + /** + * nt:childNodeDefinition + */ + public static final String NT_CHILDNODEDEFINITION = "nt:childNodeDefinition"; + /** + * nt:file + */ + public static final String NT_FILE = "nt:file"; + /** + * nt:folder + */ + public static final String NT_FOLDER = "nt:folder"; + /** + * nt:frozenNode + */ + public static final String NT_FROZENNODE = "nt:frozenNode"; + /** + * nt:hierarchyNode + */ + public static final String NT_HIERARCHYNODE = "nt:hierarchyNode"; + /** + * nt:linkedFile + */ + public static final String NT_LINKEDFILE = "nt:linkedFile"; + /** + * nt:nodeType + */ + public static final String NT_NODETYPE = "nt:nodeType"; + /** + * nt:propertyDefinition + */ + public static final String NT_PROPERTYDEFINITION = "nt:propertyDefinition"; + /** + * nt:query + */ + public static final String NT_QUERY = "nt:query"; + /** + * nt:resource + */ + public static final String NT_RESOURCE = "nt:resource"; + /** + * nt:unstructured + */ + public static final String NT_UNSTRUCTURED = "nt:unstructured"; + /** + * nt:version + */ + public static final String NT_VERSION = "nt:version"; + /** + * nt:versionHistory + */ + public static final String NT_VERSIONHISTORY = "nt:versionHistory"; + /** + * nt:versionLabels + */ + public static final String NT_VERSIONLABELS = "nt:versionLabels"; + /** + * nt:versionedChild + */ + public static final String NT_VERSIONEDCHILD = "nt:versionedChild"; +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractItem.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractItem.java new file mode 100644 index 00000000000..e3a1f7642e4 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractItem.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.commons; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +/** + * Abstract base class for implementing the JCR {@link Item} interface. + *

    + * {@link Item} methods without a default implementation: + *

      + *
    • {@link Item#accept(javax.jcr.ItemVisitor)}
    • + *
    • {@link Item#getName()}
    • + *
    • {@link Item#getParent()}
    • + *
    • {@link Item#getPath()}
    • + *
    • {@link Item#getSession()}
    • + *
    • {@link Item#isModified()}
    • + *
    • {@link Item#isNew()}
    • + *
    • {@link Item#isNode()}
    • + *
    • {@link Item#isSame(Item)}
    • + *
    • {@link Item#refresh(boolean)}
    • + *
    • {@link Item#remove()}
    • + *
    • {@link Item#save()}
    • + *
    + */ +public abstract class AbstractItem implements Item { + + /** + * Returns the ancestor of this item at the given depth. + *

    + * The default implementation handles the root node at depth zero and + * this item at depth equal to the depth of this item as special cases, + * and uses {@link javax.jcr.Session#getItem(String)} to retrieve other + * ancestors based on the ancestor path calculated from the path of this + * node as returned by {@link Item#getPath()}. + * + * @param depth depth of the returned ancestor item + * @return ancestor item + * @throws ItemNotFoundException if the given depth is negative or greater + * than the depth of this item + * @throws AccessDeniedException if access to the ancestor item is denied + * @throws RepositoryException if an error occurs + */ + public Item getAncestor(int depth) + throws ItemNotFoundException, AccessDeniedException, + RepositoryException { + if (depth < 0) { + throw new ItemNotFoundException( + this + ": Invalid ancestor depth (" + depth + ")"); + } else if (depth == 0) { + return getSession().getRootNode(); + } + + String path = getPath(); + int slash = 0; + for (int i = 0; i < depth - 1; i++) { + slash = path.indexOf('/', slash + 1); + if (slash == -1) { + throw new ItemNotFoundException( + this + ": Invalid ancestor depth (" + depth + ")"); + } + } + slash = path.indexOf('/', slash + 1); + if (slash == -1) { + return this; + } + + try { + return getSession().getItem(path.substring(0, slash)); + } catch (ItemNotFoundException e) { + throw new AccessDeniedException( + this + ": Ancestor access denied (" + depth + ")"); + } + } + + /** + * Returns the depth of this item. + *

    + * The default implementation determines the depth by counting the + * slashes in the path returned by {@link Item#getPath()}. + * + * @return depth of this item + * @throws RepositoryException if an error occurs + */ + public int getDepth() throws RepositoryException { + String path = getPath(); + if (path.length() == 1) { + return 0; + } else { + int depth = 1; + int slash = path.indexOf('/', 1); + while (slash != -1) { + depth++; + slash = path.indexOf('/', slash + 1); + } + return depth; + } + } + + //--------------------------------------------------------------< Object > + + /** + * Returns a string representation of this item. + *

    + * The default implementation returns the path of this item and falls + * back to the {@link Object#toString()} implementation if the item path + * can not be retrieved. + * + * @return string representation of this item + */ + public String toString() { + try { + return getPath(); + } catch (RepositoryException e) { + return super.toString(); + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractNode.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractNode.java new file mode 100644 index 00000000000..abacc332c58 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractNode.java @@ -0,0 +1,765 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons; + +import java.io.InputStream; +import java.util.Calendar; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +/** + * Abstract base class for implementing the JCR {@link Node} interface. + *

    + * {@link Item} methods without a default implementation: + *

      + *
    • {@link Item#accept(javax.jcr.ItemVisitor)}
    • + *
    • {@link Item#getName()}
    • + *
    • {@link Item#getParent()}
    • + *
    • {@link Item#getSession()}
    • + *
    • {@link Item#isModified()}
    • + *
    • {@link Item#isNew()}
    • + *
    • {@link Item#isSame(Item)}
    • + *
    • {@link Item#refresh(boolean)}
    • + *
    • {@link Item#remove()}
    • + *
    • {@link Item#save()}
    • + *
    + *

    + * {@link Node} methods without a default implementation: + *

      + *
    • {@link Node#addMixin(String)}
    • + *
    • {@link Node#addNode(String)}
    • + *
    • {@link Node#addNode(String, String)}
    • + *
    • {@link Node#canAddMixin(String)}
    • + *
    • {@link Node#cancelMerge(Version)}
    • + *
    • {@link Node#checkin()}
    • + *
    • {@link Node#checkout()}
    • + *
    • {@link Node#doneMerge(Version)}
    • + *
    • {@link Node#getBaseVersion()}
    • + *
    • {@link Node#getCorrespondingNodePath(String)}
    • + *
    • {@link Node#getDefinition()}
    • + *
    • {@link Node#getIndex()}
    • + *
    • {@link Node#getLock()}
    • + *
    • {@link Node#getNode(String)}
    • + *
    • {@link Node#getNodes()}
    • + *
    • {@link Node#getNodes(String)}
    • + *
    • {@link Node#getPrimaryItem()}
    • + *
    • {@link Node#getProperties()}
    • + *
    • {@link Node#getProperties(String)}
    • + *
    • {@link Node#getReferences()}
    • + *
    • {@link Node#lock(boolean, boolean)}
    • + *
    • {@link Node#merge(String, boolean)}
    • + *
    • {@link Node#orderBefore(String, String)}
    • + *
    • {@link Node#removeMixin(String)}
    • + *
    • {@link Node#restore(Version, String, boolean)}
    • + *
    • {@link Node#setProperty(String, Value)}
    • + *
    • {@link Node#setProperty(String, Value[])}
    • + *
    • {@link Node#unlock()}
    • + *
    • {@link Node#update(String)}
    • + *
    + */ +public abstract class AbstractNode extends AbstractItem implements Node { + + //----------------------------------------------------------------< Item > + + /** + * Accepts the given item visitor. + *

    + * The default implementation calls {@link ItemVisitor#visit(Node)} on + * the given visitor with this node as the argument. + * + * @param visitor item visitor + * @throws RepositoryException if an error occurs + */ + public void accept(ItemVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + + /** + * Returns the path of this node. + *

    + * The default implementation recursively calls this method on the + * parent node and appends the name and optionally the index of this + * node to construct the full path. Returns "/" if the parent node is + * not available (i.e. this is the root node). + * + * @return node path + * @throws RepositoryException if an error occurs + */ + public String getPath() throws RepositoryException { + try { + StringBuffer buffer = new StringBuffer(getParent().getPath()); + if (buffer.length() > 1) { + buffer.append('/'); + } + buffer.append(getName()); + int index = getIndex(); + if (index != 1) { + buffer.append('['); + buffer.append(index); + buffer.append(']'); + } + return buffer.toString(); + } catch (ItemNotFoundException e) { + return "/"; + } + } + + /** + * Returns true. + * + * @return true + */ + public boolean isNode() { + return true; + } + + //----------------------------------------------------------------< Node > + + /** + * Returns the declared mixin node types of this node. + *

    + * The default implementation uses the values of the + * jcr:mixinTypes property to look up the mixin node types + * from the {@link NodeTypeManager} of the current workspace. + * + * @return mixin node types + * @throws RepositoryException if an error occurs + */ + public NodeType[] getMixinNodeTypes() throws RepositoryException { + try { + NodeTypeManager manager = + getSession().getWorkspace().getNodeTypeManager(); + Property property = getProperty(getName("jcr:mixinTypes")); + Value[] values = property.getValues(); + NodeType[] types = new NodeType[values.length]; + for (int i = 0; i < values.length; i++) { + types[i] = manager.getNodeType(values[i].getString()); + } + return types; + } catch (PathNotFoundException e) { + // jcr:mixinTypes does not exist, i.e. no mixin types on this node + return new NodeType[0]; + } + } + + /** + * Returns the primary node type of this node. + *

    + * The default implementation uses the value of the + * jcr:primaryType property to look up the primary + * node type from the {@link NodeTypeManager} of the current workspace. + * + * @return primary node type + * @throws RepositoryException if an error occurs + */ + public NodeType getPrimaryNodeType() throws RepositoryException { + NodeTypeManager manager = + getSession().getWorkspace().getNodeTypeManager(); + Property property = getProperty(getName("jcr:primaryType")); + return manager.getNodeType(property.getString()); + } + + /** + * Returns the property at the given relative path from this node. + *

    + * The default implementation looks up the parent node of the given + * relative path and iterates through the properties of that node to + * find and return the identified property. + * + * @param relPath relative path of the property + * @return property + * @throws PathNotFoundException if the property is not found + * @throws RepositoryException if an error occurs + */ + public Property getProperty(String relPath) + throws PathNotFoundException, RepositoryException { + // Corner case, remove any "/." self references at the end of the path + while (relPath.endsWith("/.")) { + relPath = relPath.substring(0, relPath.length() - 2); + } + + // Find the parent node of the identified property + Node node = this; + int slash = relPath.lastIndexOf('/'); + if (slash == 0) { + node = getSession().getRootNode(); + relPath = relPath.substring(1); + } else if (slash > 0) { + node = getNode(relPath.substring(0, slash)); + relPath = relPath.substring(slash + 1); + } + + // Look for the named property. Must iterate and re-check for the name + // since the client could have used an invalid path like "./a|b". + PropertyIterator properties = node.getProperties(relPath); + while (properties.hasNext()) { + Property property = (Property) properties.next(); + if (relPath.equals(property.getName())) { + return property; + } + } + + throw new PathNotFoundException("Property not found: " + relPath); + } + + /** + * Returns the UUID of this node. + *

    + * The default implementation checks if this node is referenceable (i.e. of + * type mix:referenceable) and returns the contents of the + * jcr:uuid property if it is. + * + * @return node UUID + * @throws UnsupportedRepositoryOperationException + * if this node is not referenceable + * @throws RepositoryException if an error occurs + */ + public String getUUID() + throws UnsupportedRepositoryOperationException, RepositoryException { + if (isNodeType(getName("mix:referenceable"))) { + return getProperty(getName("jcr:uuid")).getString(); + } else { + throw new UnsupportedRepositoryOperationException( + "This node is not referenceable: " + getPath()); + } + } + + /** + * Returns the version history of this node. + *

    + * The default implementation returns the containing version history of + * the base version of this node. + * + * @return version history + * @throws RepositoryException if an error occurs + */ + public VersionHistory getVersionHistory() throws RepositoryException { + return getBaseVersion().getContainingHistory(); + } + + /** + * Checks whether a node at the given relative path exists. + *

    + * The default implementation looks up the node using + * {@link Node#getNode(String)} and returns true if + * a {@link PathNotFoundException} is not thrown. + * + * @param relPath relative path + * @return true if a node exists at the given path, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean hasNode(String relPath) throws RepositoryException { + try { + getNode(relPath); + return true; + } catch (PathNotFoundException e) { + return false; + } + } + + /** + * Checks if this node has one or more properties. + *

    + * The default implementation calls {@link Node#getNodes()} and returns + * true iff returned iterator has at least one element. + * + * @return true if this node has child nodes, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean hasNodes() throws RepositoryException { + return getNodes().hasNext(); + } + + /** + * Checks if this node has one or more properties. + *

    + * The default implementation calls {@link Node#getProperties()} and + * returns true iff returned iterator has at least one element. + *

    + * Note that in normal circumstances (i.e. no weird access controls) this + * method will always return true since all nodes always have + * at least the jcr:primaryType property. + * + * @return true if this node has properties, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean hasProperties() throws RepositoryException { + return getProperties().hasNext(); + } + + /** + * Checks whether a property at the given relative path exists. + *

    + * The default implementation looks up the property using + * {@link Node#getProperty(String)} and returns true if + * a {@link PathNotFoundException} is not thrown. + * + * @param relPath relative path + * @return true if a property exists at the given path, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean hasProperty(String relPath) throws RepositoryException { + try { + getProperty(relPath); + return true; + } catch (PathNotFoundException e) { + return false; + } + } + + /** + * Checks if this node holds a lock. + *

    + * The default implementation calls {@link Node#getLock()} and returns + * true iff the holding node of the lock is the same as this + * node. + * + * @return true if this node holds a lock, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean holdsLock() throws RepositoryException { + try { + return isSame(getLock().getNode()); + } catch (LockException e) { + return false; + } + } + + /** + * Checks whether this node is checked out. + *

    + * The default implementation checks the jcr:isCheckedOut + * property if this node is versionable, and recursively calls this method + * on the parent node if this node is not versionable. A non-versionable + * root node always returns true from this method. + * + * @return true if this node is checked out, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean isCheckedOut() throws RepositoryException { + if (isNodeType(getName("jcr:versionable"))) { + // This node is versionable, check the jcr:isCheckedOut property + return getProperty(getName("jcr:isCheckedOut")).getBoolean(); + } else { + try { + // This node is not versionable, is the parent checked out? + return getParent().isCheckedOut(); + } catch (ItemNotFoundException e) { + // This node is the root node, always checked out + return true; + } + } + } + + /** + * Checks if this node is locked. + *

    + * The default implementation calls {@link Node#getLock()} and returns + * true iff a {@link LockException} is not thrown. + * + * @return true if this node is locked, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean isLocked() throws RepositoryException { + try { + getLock(); + return true; + } catch (LockException e) { + return false; + } + } + + /** + * Checks whether this node is of the given type. + *

    + * The default implementation iterates through the primary and mixin + * types and all the supertypes of this node, returning true + * if a type with the given name is encountered. Returns false + * if none of the types matches. + * + * @param name type name + * @return true if this node is of the given type, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean isNodeType(String name) throws RepositoryException { + NodeType type = getPrimaryNodeType(); + if (name.equals(type.getName())) { + return true; + } + NodeType[] supertypes = type.getSupertypes(); + for (int i = 0; i < supertypes.length; i++) { + if (name.equals(supertypes[i].getName())) { + return true; + } + } + + NodeType[] mixins = getMixinNodeTypes(); + for (int i = 0; i < mixins.length; i++) { + if (name.equals(mixins[i].getName())) { + return true; + } + supertypes = mixins[i].getSupertypes(); + for (int j = 0; j < supertypes.length; j++) { + if (name.equals(supertypes[j].getName())) { + return true; + } + } + } + + return false; + } + + /** + * Restores this node to the version with the given name. + *

    + * The default implement retrieves the named {@link Version} from the + * associated {@link VersionHistory} and forwards the call to the + * {@link Node#restore(Version, boolean)} method. + * + * @param versionName version name + * @param removeExisting passed through + * @throws RepositoryException if an error occurs + */ + public void restore(String versionName, boolean removeExisting) + throws RepositoryException { + restore(getVersionHistory().getVersion(versionName), removeExisting); + } + + /** + * Restores this node to the given version. + *

    + * The default implementation forwards the call to the + * {@link Node#restore(Version, String, boolean)} method using the + * relative path ".". + * + * @param version passed through + * @param removeExisting passed through + * @throws RepositoryException if an error occurs + */ + public void restore(Version version, boolean removeExisting) + throws RepositoryException { + restore(version, ".", removeExisting); + } + + /** + * Restores this node to the version with the given label. + *

    + * The default implement retrieves the labeled {@link Version} from the + * associated {@link VersionHistory} and forwards the call to the + * {@link Node#restore(Version, boolean)} method. + * + * @param versionLabel version label + * @param removeExisting passed through + * @throws RepositoryException if an error occurs + */ + public void restoreByLabel(String versionLabel, boolean removeExisting) + throws RepositoryException { + restore(getVersionHistory().getVersionByLabel(versionLabel), + removeExisting); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instances from + * the given string values and forwards the call to the + * {@link Node#setProperty(String, Value[])} method. + * + * @param name property name + * @param strings string values + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, String[] strings) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + Value[] values = new Value[strings.length]; + for (int i = 0; i < strings.length; i++) { + values[i] = factory.createValue(strings[i]); + } + return setProperty(name, values); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instance from + * the given string value and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value string value + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, String value) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + return setProperty(name, factory.createValue(value)); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instance from + * the given binary value and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value binary value + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, InputStream value) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + return setProperty(name, factory.createValue(value)); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instance from + * the given boolean value and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value boolean value + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, boolean value) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + return setProperty(name, factory.createValue(value)); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instance from + * the given double value and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value double value + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, double value) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + return setProperty(name, factory.createValue(value)); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instance from + * the given long value and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value long value + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, long value) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + return setProperty(name, factory.createValue(value)); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instance from + * the given date value and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value date value + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, Calendar value) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + return setProperty(name, factory.createValue(value)); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instance from + * the given reference value and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value reference value + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, Node value) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + return setProperty(name, factory.createValue(value)); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to convert the given value to the given + * type and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value property value + * @param type property type + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, Value value, int type) + throws RepositoryException { + if (value.getType() != type) { + ValueFactory factory = getSession().getValueFactory(); + value = factory.createValue(value.getString(), type); + } + return setProperty(name, value); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to convert the given values to the given + * type and forwards the call to the + * {@link Node#setProperty(String, Value[])} method. + * + * @param name property name + * @param values property values + * @param type property type + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, Value[] values, int type) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + Value[] converted = new Value[values.length]; + for (int i = 0; i < values.length; i++) { + if (values[i].getType() != type) { + converted[i] = factory.createValue(values[i].getString(), type); + } else { + converted[i] = values[i]; + } + } + return setProperty(name, converted); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create {@link Value} instances of the + * given type from the given string values and forwards the call to the + * {@link Node#setProperty(String, Value[])} method. + * + * @param name property name + * @param strings string values + * @param type property type + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, String[] strings, int type) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + Value[] values = new Value[strings.length]; + for (int i = 0; i < strings.length; i++) { + values[i] = factory.createValue(strings[i], type); + } + return setProperty(name, values); + } + + /** + * Sets the value of the named property. + *

    + * The default implementation uses the {@link ValueFactory} of the + * current {@link Session} to create a {@link Value} instance of the + * given type from the given string value and forwards the call to the + * {@link Node#setProperty(String, Value)} method. + * + * @param name property name + * @param value string value + * @param type property type + * @return modified property + * @throws RepositoryException if an error occurs + */ + public Property setProperty(String name, String value, int type) + throws RepositoryException { + ValueFactory factory = getSession().getValueFactory(); + return setProperty(name, factory.createValue(value, type)); + } + + //-------------------------------------------------------------< private > + + /** + * Returns the prefixed JCR name for the namespace URI and local name + * using the current namespace mappings. + * + * @param uri namespace URI + * @param name namespace-local name + * @return prefixed JCR name + * @throws RepositoryException if an error occurs + */ + private String getName(String name) throws RepositoryException { + return new NamespaceHelper(getSession()).getJcrName(name); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractProperty.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractProperty.java new file mode 100644 index 00000000000..69f020e31cc --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractProperty.java @@ -0,0 +1,561 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; + +import javax.jcr.Item; +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.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +/** + * Abstract base class for implementing the JCR {@link Property} interface. + *

    + * {@link Item} methods without a default implementation: + *

      + *
    • {@link Item#getName()}
    • + *
    • {@link Item#getParent()}
    • + *
    • {@link Item#getSession()}
    • + *
    • {@link Item#isModified()}
    • + *
    • {@link Item#isNew()}
    • + *
    • {@link Item#isSame(Item)}
    • + *
    • {@link Item#refresh(boolean)}
    • + *
    • {@link Item#save()}
    • + *
    + *

    + * {@link Property} methods without a default implementation: + *

      + *
    • {@link Property#getDefinition()}
    • + *
    • {@link Property#getValue()}
    • + *
    • {@link Property#getValues()}
    • + *
    + *

    + * NOTE: Many of the default method implementations in + * this base class rely on the parent node being accessible through the + * {@link Item#getParent()} call. It is possible (though unlikely) that + * access controls deny access to a containing node even though a property + * is accessible. In such cases the default method implementations in this + * class will not work. + */ +public abstract class AbstractProperty extends AbstractItem + implements Item, Property { + + //----------------------------------------------------------------< Item > + + /** + * Accepts the given item visitor. + *

    + * The default implementation calls {@link ItemVisitor#visit(Property)} + * on the given visitor with this property as the argument. + * + * @param visitor item visitor + * @throws RepositoryException if an error occurs + */ + public void accept(ItemVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + + /** + * Returns the path of this property. + *

    + * The default implementation constructs the path from the path of the + * parent node and the name of this property. + * + * @return property path + * @throws RepositoryException if an error occurs + */ + public String getPath() throws RepositoryException { + StringBuffer buffer = new StringBuffer(getParent().getPath()); + if (buffer.length() > 1) { + buffer.append('/'); + } + buffer.append(getName()); + return buffer.toString(); + } + + /** + * Returns false. + * + * @return false + */ + public boolean isNode() { + return false; + } + + /** + * Removes this property. + *

    + * The default implementation calls {@link Node#setProperty(String, Value)} + * with a null value on the parent node. + * + * @throws RepositoryException if an error occurs + */ + public void remove() throws RepositoryException { + getParent().setProperty(getName(), (Value) null); + } + + //------------------------------------------------------------< Property > + + /** + * Returns the boolean value of this property. + *

    + * The default implementation forwards the method call to the + * {@link Value} instance returned by the generic + * {@link Property#getValue()} method. + * + * @return boolean value + * @throws RepositoryException if an error occurs + */ + public boolean getBoolean() throws RepositoryException { + return getValue().getBoolean(); + } + + /** + * Returns the date value of this property. + *

    + * The default implementation forwards the method call to the + * {@link Value} instance returned by the generic + * {@link Property#getValue()} method. + * + * @return date value + * @throws RepositoryException if an error occurs + */ + public Calendar getDate() throws RepositoryException { + return getValue().getDate(); + } + + /** + * Returns the double value of this property. + *

    + * The default implementation forwards the method call to the + * {@link Value} instance returned by the generic + * {@link Property#getValue()} method. + * + * @return double value + * @throws RepositoryException if an error occurs + */ + public double getDouble() throws RepositoryException { + return getValue().getDouble(); + } + + /** + * Returns the length of the value of this property. + *

    + * The default implementation measures the length of the {@link Value} + * instance returned by the generic {@link Property#getValue()} method. + * + * @return length of the property value + * @throws RepositoryException if an error occurs + */ + public long getLength() throws RepositoryException { + return getLength(getValue()); + } + + /** + * Returns the lengths of the values of this property. + *

    + * The default implementation measures the lengths of the {@link Value} + * instances returned by the generic {@link Property#getValues()} method. + * + * @return lengths of the property values + * @throws RepositoryException if an error occurs + */ + 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; + } + + /** + * Returns the long value of this property. + *

    + * The default implementation forwards the method call to the + * {@link Value} instance returned by the generic + * {@link Property#getValue()} method. + * + * @return long value + * @throws RepositoryException if an error occurs + */ + public long getLong() throws RepositoryException { + return getValue().getLong(); + } + + /** + * If this property is of type REFERENCE, + * WEAKREFERENCE or PATH (or convertible to one of + * these types) this method returns the Node to which this + * property refers. + *

    + * If this property is of type PATH and it contains a relative + * path, it is interpreted relative to the parent node of this property. For + * example "." refers to the parent node itself, + * ".." to the parent of the parent node and "foo" + * to a sibling node of this property. + * + * @return the referenced Node + * @throws ValueFormatException if this property cannot be converted to a + * referring type (REFERENCE, WEAKREFERENCE or + * PATH), if the property is multi-valued or if this property + * is a referring type but is currently part of the frozen state of a + * version in version storage. + * @throws ItemNotFoundException If this property is of type + * PATH or WEAKREFERENCE and no target node + * accessible by the current Session exists in this workspace. + * Note that this applies even if the property is a PATHS and a + * property exists at the specified location. To dereference to a + * target property (as opposed to a target node), the method + * Property.getProperty is used. + * @throws RepositoryException if another error occurs. + */ + public Node getNode() throws ValueFormatException, RepositoryException { + String value = getString(); + + switch (getType()) { + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + return getSession().getNodeByIdentifier(value); + + case PropertyType.PATH: + try { + return (value.startsWith("/")) ? getSession().getNode(value) : getParent().getNode(value); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(value); + } + + case PropertyType.NAME: + try { + return getParent().getNode(value); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(value); + } + + case PropertyType.STRING: + try { + // interpret as identifier + Value refValue = getSession().getValueFactory().createValue(value, PropertyType.REFERENCE); + 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 = getSession().getValueFactory().createValue(value, PropertyType.PATH); + try { + return (value.startsWith("/")) ? getSession().getNode(pathValue.getString()) : getParent().getNode(pathValue.getString()); + } catch (PathNotFoundException e1) { + throw new ItemNotFoundException(pathValue.getString()); + } + } + + default: + throw new ValueFormatException("Property value cannot be converted to a PATH, REFERENCE or WEAKREFERENCE: " + value); + } + } + + /** + * If this property is of type PATH (or convertible to this + * type) this method returns the Property to which this + * property refers. + *

    + * If this property contains a relative path, it is interpreted relative to + * the parent node of this property. Therefore, when resolving such a + * relative path, the segment "." refers to + * the parent node itself, ".." to the parent of the parent + * node and "foo" to a sibling property of this property or + * this property itself. + *

    + * For example, if this property is located at + * /a/b/c and it has a value of "../d" then this + * method will return the property at /a/d if such exists. + *

    + * If this property is multi-valued, this method throws a + * ValueFormatException. + *

    + * If this property cannot be converted to a PATH then a + * ValueFormatException is thrown. + *

    + * If this property is currently part of the frozen state of a version in + * version storage, this method will throw a ValueFormatException. + * + * @return the referenced property + * @throws ValueFormatException if this property cannot be converted to a + * PATH, if the property is multi-valued or if this property is + * a referring type but is currently part of the frozen state of a version + * in version storage. + * @throws ItemNotFoundException If no property accessible by the current + * Session exists in this workspace at the specified path. Note + * that this applies even if a node exists at the specified location. + * To dereference to a target node, the method Property.getNode + * is used. + * @throws RepositoryException if another error occurs. + */ + public Property getProperty() throws RepositoryException { + String value = getString(); + switch (getType()) { + case PropertyType.PATH: + try { + return (value.startsWith("/")) ? getSession().getProperty(value) : getParent().getProperty(value); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(value); + } + + case PropertyType.NAME: + try { + return getParent().getProperty(value); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(value); + } + + default: + try { + String path = getSession().getValueFactory().createValue(value, PropertyType.PATH).getString(); + return (path.startsWith("/")) ? getSession().getProperty(path) : getParent().getProperty(path); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(value); + } + } + } + + /** + * Returns the binary value of this property. + *

    + * The default implementation forwards the method call to the + * {@link Value} instance returned by the generic + * {@link Property#getValue()} method. + * + * @return binary value + * @throws RepositoryException if an error occurs + */ + public InputStream getStream() throws RepositoryException { + return getValue().getStream(); + } + + /** + * Returns the string value of this property. + *

    + * The default implementation forwards the method call to the + * {@link Value} instance returned by the generic + * {@link Property#getValue()} method. + * + * @return string value + * @throws RepositoryException if an error occurs + */ + public String getString() throws RepositoryException { + return getValue().getString(); + } + + /** + * Returns the type of this property. + *

    + * The default implementation forwards the method call to the + * {@link Value} instance returned by the generic + * {@link Property#getValue()} method. + * + * @return property type + * @throws RepositoryException if an error occurs + */ + public int getType() throws RepositoryException { + return getValue().getType(); + } + + /** + * Sets the value of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, Value)} method of the parent node + * using the name of this property. + * + * @param value passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(Value value) throws RepositoryException { + getParent().setProperty(getName(), value); + } + + /** + * Sets the values of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, Value[])} method of the parent node + * using the name of this property. + * + * @param values passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(Value[] values) throws RepositoryException { + getParent().setProperty(getName(), values); + } + + /** + * Sets the value of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, String)} method of the parent node + * using the name of this property. + * + * @param value passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(String value) throws RepositoryException { + getParent().setProperty(getName(), value); + } + + /** + * Sets the values of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, String[])} method of the parent node + * using the name of this property. + * + * @param values passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(String[] values) throws RepositoryException { + getParent().setProperty(getName(), values); + } + + /** + * Sets the value of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, InputStream)} method of the parent node + * using the name of this property. + * + * @param value passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(InputStream value) throws RepositoryException { + getParent().setProperty(getName(), value); + } + + /** + * Sets the value of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, long)} method of the parent node + * using the name of this property. + * + * @param value passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(long value) throws RepositoryException { + getParent().setProperty(getName(), value); + } + + /** + * Sets the value of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, double)} method of the parent node + * using the name of this property. + * + * @param value passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(double value) throws RepositoryException { + getParent().setProperty(getName(), value); + } + + /** + * Sets the value of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, Calendar)} method of the parent node + * using the name of this property. + * + * @param value passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(Calendar value) throws RepositoryException { + getParent().setProperty(getName(), value); + } + + /** + * Sets the value of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, boolean)} method of the parent node + * using the name of this property. + * + * @param value passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(boolean value) throws RepositoryException { + getParent().setProperty(getName(), value); + } + + /** + * Sets the value of this property. + *

    + * The default implementation forwards the call to the + * {@link Node#setProperty(String, Node)} method of the parent node + * using the name of this property. + * + * @param value passed through + * @throws RepositoryException if an error occurs + */ + public void setValue(Node value) throws RepositoryException { + getParent().setProperty(getName(), value); + } + + //-------------------------------------------------------------< private > + + /** + * Returns the length of the given value. + * + * @param value value + * @return length of the value + * @throws RepositoryException if an error occurs + */ + private long getLength(Value value) throws RepositoryException { + if (value.getType() != PropertyType.BINARY) { + return value.getString().length(); + } else { + try { + InputStream stream = value.getStream(); + try { + long length = 0; + byte[] buffer = new byte[4096]; + int n = stream.read(buffer); + while (n != -1) { + length += n; + n = stream.read(buffer); + } + return length; + } finally { + stream.close(); + } + } catch (IOException e) { + throw new RepositoryException( + "Failed to count the length of a binary value", e); + } + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractRepository.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractRepository.java new file mode 100644 index 00000000000..5cfe09627a8 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractRepository.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.commons; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Abstract base class for implementing the JCR {@link Repository} interface. + *

    + * This class implements the three utility login methods by calling the + * {@link Repository#login(Credentials, String)} method with null + * arguments as specified in the JCR API. + */ +public abstract class AbstractRepository implements Repository { + + /** + * The set of standard descriptor keys defined in the + * {@link Repository} interface. + */ + private static final Set STANDARD_KEYS = new HashSet() {{ + add(Repository.IDENTIFIER_STABILITY); + add(Repository.LEVEL_1_SUPPORTED); + add(Repository.LEVEL_2_SUPPORTED); + add(Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_INHERITANCE); + add(Repository.NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_PROPERTY_TYPES); + add(Repository.NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPORTED); + add(Repository.OPTION_ACCESS_CONTROL_SUPPORTED); + add(Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED); + add(Repository.OPTION_LIFECYCLE_SUPPORTED); + add(Repository.OPTION_LOCKING_SUPPORTED); + add(Repository.OPTION_OBSERVATION_SUPPORTED); + add(Repository.OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED); + add(Repository.OPTION_QUERY_SQL_SUPPORTED); + add(Repository.OPTION_RETENTION_SUPPORTED); + add(Repository.OPTION_SHAREABLE_NODES_SUPPORTED); + add(Repository.OPTION_SIMPLE_VERSIONING_SUPPORTED); + add(Repository.OPTION_TRANSACTIONS_SUPPORTED); + add(Repository.OPTION_UNFILED_CONTENT_SUPPORTED); + add(Repository.OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED); + add(Repository.OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED); + add(Repository.OPTION_VERSIONING_SUPPORTED); + add(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED); + add(Repository.OPTION_XML_EXPORT_SUPPORTED); + add(Repository.OPTION_XML_IMPORT_SUPPORTED); + add(Repository.OPTION_ACTIVITIES_SUPPORTED); + add(Repository.OPTION_BASELINES_SUPPORTED); + + add(Repository.QUERY_FULL_TEXT_SEARCH_SUPPORTED); + add(Repository.QUERY_JOINS); + add(Repository.QUERY_LANGUAGES); + add(Repository.QUERY_STORED_QUERIES_SUPPORTED); + add(Repository.QUERY_XPATH_DOC_ORDER); + add(Repository.QUERY_XPATH_POS_INDEX); + add(Repository.REP_NAME_DESC); + add(Repository.REP_VENDOR_DESC); + add(Repository.REP_VENDOR_URL_DESC); + add(Repository.SPEC_NAME_DESC); + add(Repository.SPEC_VERSION_DESC); + add(Repository.WRITE_SUPPORTED); + }}; + + /** + * Returns true if the given key identifies a standard descriptor. + * + * @param key descriptor key + * @return true if the key identifies a standard descriptor, + * false otherwise + */ + public boolean isStandardDescriptor(String key) { + return STANDARD_KEYS.contains(key); + } + + /** + * This implementation directly delegates to {@link #login(javax.jcr.Credentials, String)} + * not supporting any attributes. + * + * @param credentials the credentials of the user + * @param workspaceName the name of a workspace + * @param attributes implementation-specific session attributes + * @return a valid session for the user to access the repository. + * @throws javax.jcr.LoginException + * @throws javax.jcr.NoSuchWorkspaceException + * @throws RepositoryException + */ + public Session login( + Credentials credentials, String workspaceName, Map attributes) + throws LoginException, NoSuchWorkspaceException, RepositoryException { + return login(credentials, workspaceName); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * null arguments. + * + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login() throws RepositoryException { + return login(null, null); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * the given credentials and a null workspace name. + * + * @param credentials login credentials + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login(Credentials credentials) throws RepositoryException { + return login(credentials, null); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * null credentials and the given workspace name. + * + * @param workspace workspace name + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login(String workspace) throws RepositoryException { + return login(null, workspace); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractSession.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractSession.java new file mode 100644 index 00000000000..e4b125cbfb8 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractSession.java @@ -0,0 +1,561 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Credentials; +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.Item; +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.Workspace; + +import org.apache.jackrabbit.commons.xml.DocumentViewExporter; +import org.apache.jackrabbit.commons.xml.Exporter; +import org.apache.jackrabbit.commons.xml.ParsingContentHandler; +import org.apache.jackrabbit.commons.xml.SystemViewExporter; +import org.apache.jackrabbit.commons.xml.ToXmlContentHandler; +import org.apache.jackrabbit.util.XMLChar; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * Abstract base class for implementing the JCR {@link Session} interface. + */ +public abstract class AbstractSession implements Session { + + /** + * Local namespace mappings. 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(); + + /** + * Clears the local namespace mappings. Subclasses that for example + * want to participate in a session pools should remember to call + * super.logout() when overriding this method to avoid + * namespace mappings to be carried over to a new session. + */ + public void logout() { + synchronized (namespaces) { + namespaces.clear(); + } + } + + //------------------------------------------------< Namespace handling >-- + + /** + * Returns the namespace prefix mapped to the given URI. The mapping is + * added to the set of session-local namespace mappings unless it already + * exists there. + *

    + * This behaviour is based on JSR 283 (JCR 2.0), but remains backwards + * compatible with JCR 1.0. + * + * @param uri namespace URI + * @return namespace prefix + * @throws NamespaceException if the namespace is not found + * @throws RepositoryException if a repository error occurs + */ + public String getNamespacePrefix(String uri) + throws NamespaceException, 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; + } + + namespaces.put(prefix, uri); + return prefix; + } + } + + /** + * Returns the namespace URI mapped to the given prefix. The mapping is + * added to the set of session-local namespace mappings unless it already + * exists there. + *

    + * This behaviour is based on JSR 283 (JCR 2.0), but remains backwards + * compatible with JCR 1.0. + * + * @param prefix namespace prefix + * @return namespace URI + * @throws NamespaceException if the namespace is not found + * @throws RepositoryException if a repository error occurs + */ + public String getNamespaceURI(String prefix) + throws NamespaceException, 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); + } + // Add the mapping to the local set, we already know that + // the prefix is not taken + namespaces.put(prefix, uri); + } + + return uri; + } + } + + /** + * Returns the prefixes of all known namespace mappings. All global + * mappings not already included in the local set of namespace mappings + * are added there. + *

    + * This behaviour is based on JSR 283 (JCR 2.0), but remains backwards + * compatible with JCR 1.0. + * + * @return namespace prefixes + * @throws RepositoryException if a repository error occurs + */ + public String[] getNamespacePrefixes() + throws RepositoryException { + for (String uri : getWorkspace().getNamespaceRegistry().getURIs()) { + getNamespacePrefix(uri); + } + + synchronized (namespaces) { + return namespaces.keySet().toArray(new String[namespaces.size()]); + } + } + + /** + * Modifies the session local namespace mappings to contain the given + * prefix to URI mapping. + *

    + * This behaviour is based on JSR 283 (JCR 2.0), but remains backwards + * compatible with JCR 1.0. + * + * @param prefix namespace prefix + * @param uri namespace URI + * @throws NamespaceException if the mapping is illegal + * @throws RepositoryException if a repository error occurs + */ + public void setNamespacePrefix(String prefix, String uri) + throws NamespaceException, 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.length() == 0) { + throw new NamespaceException( + "Empty prefix is reserved and can not be remapped"); + } else if (uri.length() == 0) { + throw new NamespaceException( + "Default namespace is reserved and can not be remapped"); + } else if (prefix.toLowerCase().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); + } + } + + //---------------------------------------------< XML export and import >-- + + /** + * Generates a document view export using a {@link DocumentViewExporter} + * instance. + * + * @param path of the node to be exported + * @param handler handler for the SAX events of the export + * @param skipBinary whether binary values should be skipped + * @param noRecurse whether to export just the identified node + * @throws PathNotFoundException if a node at the given path does not exist + * @throws SAXException if the SAX event handler failed + * @throws RepositoryException if another error occurs + */ + public void exportDocumentView( + String path, ContentHandler handler, + boolean skipBinary, boolean noRecurse) + throws PathNotFoundException, SAXException, RepositoryException { + export(path, new DocumentViewExporter( + this, handler, !noRecurse, !skipBinary)); + } + + /** + * Generates a system view export using a {@link SystemViewExporter} + * instance. + * + * @param path of the node to be exported + * @param handler handler for the SAX events of the export + * @param skipBinary whether binary values should be skipped + * @param noRecurse whether to export just the identified node + * @throws PathNotFoundException if a node at the given path does not exist + * @throws SAXException if the SAX event handler failed + * @throws RepositoryException if another error occurs + */ + public void exportSystemView( + String path, ContentHandler handler, + boolean skipBinary, boolean noRecurse) + throws PathNotFoundException, SAXException, RepositoryException { + export(path, new SystemViewExporter( + this, handler, !noRecurse, !skipBinary)); + } + + /** + * Calls {@link Session#exportDocumentView(String, ContentHandler, boolean, boolean)} + * with the given arguments and a {@link ContentHandler} that serializes + * SAX events to the given output stream. + * + * @param absPath passed through + * @param out output stream to which the SAX events are serialized + * @param skipBinary passed through + * @param noRecurse passed through + * @throws IOException if the SAX serialization failed + * @throws RepositoryException if another error occurs + */ + public void exportDocumentView( + String absPath, OutputStream out, + boolean skipBinary, boolean noRecurse) + throws IOException, RepositoryException { + try { + ContentHandler handler = new ToXmlContentHandler(out); + exportDocumentView(absPath, handler, skipBinary, noRecurse); + } catch (SAXException e) { + Exception exception = e.getException(); + if (exception instanceof RepositoryException) { + throw (RepositoryException) exception; + } else if (exception instanceof IOException) { + throw (IOException) exception; + } else { + throw new RepositoryException( + "Error serializing document view XML", e); + } + } + } + + /** + * Calls {@link Session#exportSystemView(String, ContentHandler, boolean, boolean)} + * with the given arguments and a {@link ContentHandler} that serializes + * SAX events to the given output stream. + * + * @param absPath passed through + * @param out output stream to which the SAX events are serialized + * @param skipBinary passed through + * @param noRecurse passed through + * @throws IOException if the SAX serialization failed + * @throws RepositoryException if another error occurs + */ + public void exportSystemView( + String absPath, OutputStream out, + boolean skipBinary, boolean noRecurse) + throws IOException, RepositoryException { + try { + ContentHandler handler = new ToXmlContentHandler(out); + exportSystemView(absPath, handler, skipBinary, noRecurse); + } catch (SAXException e) { + Exception exception = e.getException(); + if (exception instanceof RepositoryException) { + throw (RepositoryException) exception; + } else if (exception instanceof IOException) { + throw (IOException) exception; + } else { + throw new RepositoryException( + "Error serializing system view XML", e); + } + } + } + + /** + * Parses the given input stream as an XML document and processes the + * SAX events using the {@link ContentHandler} returned by + * {@link Session#getImportContentHandler(String, int)}. + * + * @param parentAbsPath passed through + * @param in input stream to be parsed as XML and imported + * @param uuidBehavior passed through + * @throws IOException if an I/O error occurs + * @throws InvalidSerializedDataException if an XML parsing error occurs + * @throws RepositoryException if a repository error occurs + */ + public void importXML( + String parentAbsPath, InputStream in, int uuidBehavior) + throws IOException, InvalidSerializedDataException, + RepositoryException { + try { + ContentHandler handler = + getImportContentHandler(parentAbsPath, uuidBehavior); + new ParsingContentHandler(handler).parse(in); + } catch (SAXException e) { + Throwable exception = e.getException(); + if (exception instanceof RepositoryException) { + throw (RepositoryException) exception; + } else if (exception instanceof IOException) { + throw (IOException) exception; + } else { + throw new InvalidSerializedDataException("XML parse error", e); + } + } finally { + // JCR-2903 + if (in != null) { + try { in.close(); } catch (IOException ignore) {} + } + } + } + + //-----------------------------------------------------< Item handling >-- + + private String toRelativePath(String absPath) throws PathNotFoundException { + if (absPath.startsWith("/") && absPath.length() > 1) { + return absPath.substring(1); + } else { + throw new PathNotFoundException("Not an absolute path: " + absPath); + } + } + + /** + * Returns the node or property at the given path. + *

    + * The default implementation: + *

      + *
    • Returns the root node if the given path is "/" + *
    • Delegates to {@link Session#getNodeByIdentifier(String)} for identifier + * paths + *
    • Throws a {@link PathNotFoundException} if the given path does not + * start with a slash. + *
    • Calls {@link Node#getNode(String)} on the root node with the part of + * the given path after the first slash + *
    • Calls {@link Node#getProperty(String)} similarly in case the above + * call fails with a {@link PathNotFoundException} + *
    + * + * @see Session#getItem(String) + * @param absPath + * absolute path + * @return the node or property with the given path + * @throws PathNotFoundException + * if the given path is invalid or not found + * @throws RepositoryException + * if another error occurs + */ + public Item getItem(String absPath) throws PathNotFoundException, RepositoryException { + Node root = getRootNode(); + if (absPath.equals("/")) { + return root; + } else if (absPath.startsWith("[") && absPath.endsWith("]")) { + return getNodeByIdentifier(absPath.substring(1, absPath.length() - 1)); + } else { + String relPath = toRelativePath(absPath); + if (root.hasNode(relPath)) { + return root.getNode(relPath); + } else { + return root.getProperty(relPath); + } + } + } + + /** + * Calls {@link #getItem(String)} with the given path and returns + * true if the call succeeds. Returns false + * if a {@link PathNotFoundException} was thrown. Other exceptions are + * passed through. + * + * @see Session#itemExists(String) + * @param absPath absolute path + * @return true if an item exists at the given path, + * false otherwise + * @throws RepositoryException if an error occurs + */ + public boolean itemExists(String absPath) throws RepositoryException { + if (absPath.equals("/")) { + return true; + } else { + Node root = getRootNode(); + String relPath = toRelativePath(absPath); + return root.hasNode(relPath) || root.hasProperty(relPath); + } + } + + /** + * Removes the identified item. Implemented by calling + * {@link Item#remove()} on the item removed by {@link #getItem(String)}. + * + * @see Session#removeItem(String) + * @param absPath An absolute path of the item to be removed + * @throws RepositoryException if the item can not be removed + */ + public void removeItem(String absPath) throws RepositoryException { + getItem(absPath).remove(); + } + + /** + * Returns the node with the given absolute path. + * + * @see Session#getNode(String) + * @param absPath absolute path + * @return node at the given path + * @throws RepositoryException if the node can not be accessed + */ + public Node getNode(String absPath) throws RepositoryException { + Node root = getRootNode(); + if (absPath.equals("/")) { + return root; + } else { + return root.getNode(toRelativePath(absPath)); + } + } + + /** + * Checks whether a node with the given absolute path exists. + * + * @see Session#nodeExists(String) + * @param absPath absolute path + * @return true if a node with the given path exists, + * false otherwise + * @throws RepositoryException if the path is invalid + */ + public boolean nodeExists(String absPath) throws RepositoryException { + if (absPath.equals("/")) { + return true; + } else { + return getRootNode().hasNode(toRelativePath(absPath)); + } + } + + /** + * Returns the property with the given absolute path. + * + * @see Session#getProperty(String) + * @param absPath absolute path + * @return node at the given path + * @throws RepositoryException if the property can not be accessed + */ + public Property getProperty(String absPath) throws RepositoryException { + if (absPath.equals("/")) { + throw new RepositoryException("The root node is not a property"); + } else { + return getRootNode().getProperty(toRelativePath(absPath)); + } + } + + /** + * Checks whether a property with the given absolute path exists. + * + * @see Session#propertyExists(String) + * @param absPath absolute path + * @return true if a property with the given path exists, + * false otherwise + * @throws RepositoryException if the path is invalid + */ + public boolean propertyExists(String absPath) throws RepositoryException { + if (absPath.equals("/")) { + return false; + } else { + return getRootNode().hasProperty(toRelativePath(absPath)); + } + } + + //--------------------------------------------------< Session handling >-- + + /** + * Logs in the same workspace with the given credentials. + *

    + * The default implementation: + *

      + *
    • Retrieves the {@link Repository} instance using + * {@link Session#getRepository()} + *
    • Retrieves the current workspace using {@link Session#getWorkspace()} + *
    • Retrieves the name of the current workspace using + * {@link Workspace#getName()} + *
    • Calls {@link Repository#login(Credentials, String)} on the + * retrieved repository with the given credentials and the retrieved + * workspace name. + *
    + * + * @param credentials login credentials + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session impersonate(Credentials credentials) + throws RepositoryException { + return getRepository().login(credentials, getWorkspace().getName()); + } + + //-------------------------------------------------------------< private > + + /** + * Exports content at the given path using the given exporter. + * + * @param path of the node to be exported + * @param exporter document or system view exporter + * @throws SAXException if the SAX event handler failed + * @throws RepositoryException if another error occurs + */ + private synchronized void export(String path, Exporter exporter) + throws PathNotFoundException, SAXException, RepositoryException { + Item item = getItem(path); + if (item.isNode()) { + exporter.export((Node) item); + } else { + throw new PathNotFoundException( + "XML export is not defined for properties: " + path); + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractWorkspace.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractWorkspace.java new file mode 100644 index 00000000000..bff3406b02c --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/AbstractWorkspace.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.commons; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; + +import org.apache.jackrabbit.commons.xml.ParsingContentHandler; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * Abstract base class for implementing the JCR {@link Workspace} interface. + */ +public abstract class AbstractWorkspace implements Workspace { + + /** + * Parses the given input stream as an XML document and processes the + * SAX events using the {@link ContentHandler} returned by + * {@link Workspace#getImportContentHandler(String, int)}. + * + * @param parentAbsPath passed through + * @param in input stream to be parsed as XML and imported + * @param uuidBehavior passed through + * @throws IOException if an I/O error occurs + * @throws InvalidSerializedDataException if an XML parsing error occurs + * @throws RepositoryException if a repository error occurs + */ + public void importXML( + String parentAbsPath, InputStream in, int uuidBehavior) + throws IOException, InvalidSerializedDataException, + RepositoryException { + try { + ContentHandler handler = + getImportContentHandler(parentAbsPath, uuidBehavior); + new ParsingContentHandler(handler).parse(in); + } catch (SAXException e) { + Throwable exception = e.getException(); + if (exception instanceof RepositoryException) { + throw (RepositoryException) exception; + } else if (exception instanceof IOException) { + throw (IOException) exception; + } else { + throw new InvalidSerializedDataException("XML parse error", e); + } + } finally { + // JCR-2903 + if (in != null) { + try { in.close(); } catch (IOException ignore) {} + } + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/GenericRepositoryFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/GenericRepositoryFactory.java new file mode 100644 index 00000000000..cd4d007f4de --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/GenericRepositoryFactory.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.commons; + +/** + * Renamed to {@link JndiRepositoryFactory}. This class will be removed + * in Jackrabbit 2.0. Please use the {@link JcrUtils#REPOSITORY_URI} constant + * instead of the {@link #URI} constant in this class. + */ +@Deprecated +public class GenericRepositoryFactory extends JndiRepositoryFactory { + + /** + * Please use {@link JcrUtils#REPOSITORY_URI} instead. + */ + public static final String URI = JcrUtils.REPOSITORY_URI; + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/ItemNameMatcher.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/ItemNameMatcher.java new file mode 100644 index 00000000000..aba12084ecd --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/ItemNameMatcher.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.commons; + +import java.util.StringTokenizer; + +/** + * Utility for name matching such as required for {@link javax.jcr.Node#getNodes(String)}, + * {@link javax.jcr.Node#getNodes(String[])}, {@link javax.jcr.Node#getProperties(String)} + * and {@link javax.jcr.Node#getProperties(String[])}. + */ +public final class ItemNameMatcher { + + private static final char WILDCARD_CHAR = '*'; + private static final String OR = "|"; + + private ItemNameMatcher() { } + + /** + * Matches the name pattern against the specified name. + *

    + * The pattern may be a full name or a partial name with one or more + * wildcard characters ("*"), or a disjunction (using the "|" character + * to represent logical OR) of these. For example, + *

    + * {@code "jcr:*|foo:bar"} + *

    + * would match + *

    + * {@code "foo:bar"}, but also {@code "jcr:whatever"}. + *

    +     * The EBNF for pattern is:
    +     *
    +     * namePattern ::= disjunct {'|' disjunct}
    +     * disjunct ::= name [':' name]
    +     * name ::= '*' |
    +     *          ['*'] fragment {'*' fragment}['*']
    +     * fragment ::= char {char}
    +     * char ::= nonspace | ' '
    +     * nonspace ::= (* Any Unicode character except:
    +     *               '/', ':', '[', ']', '*',
    +     *               ''', '"', '|' or any whitespace
    +     *               character *)
    +     * 
    + * Note that leading and trailing whitespace around a pattern is ignored. + * + * @param name the name to test the pattern with + * @param pattern the pattern to be matched against the name + * @return true if the specified name matches the pattern + * @see javax.jcr.Node#getNodes(String) + */ + public static boolean matches(String name, String pattern) { + StringTokenizer st = new StringTokenizer(pattern, OR, false); + while (st.hasMoreTokens()) { + // remove leading & trailing whitespace from token + String token = st.nextToken().trim(); + if (internalMatches(name, token, 0, 0)) { + return true; + } + } + return false; + } + + /** + * Matches the {@code nameGlob} strings in the passed array against + * the specified name. + *

    + * A glob may be a full name or a partial name with one or more + * wildcard characters ("{@code *}"). + *

    + * Note that unlike in the case of the {@link #matches(String, String)} + * leading and trailing whitespace around a glob is not ignored. + * + * @param name the name to test the pattern with + * @param nameGlobs an array of globbing strings + * @return true if the specified name matches any of the globs + * @see javax.jcr.Node#getNodes(String[]) + */ + public static boolean matches(String name, String[] nameGlobs) { + for (String nameGlob : nameGlobs) { + // use globbing string as-is. Don't trim any leading/trailing whitespace + if (internalMatches(name, nameGlob, 0, 0)) { + return true; + } + } + return false; + } + + //------------------------------------------< private >--- + + /** + * Internal helper used to recursively match the pattern + * + * @param s The string to be tested + * @param pattern The pattern + * @param sOff offset within {@code s} + * @param pOff offset within {@code pattern}. + * @return true if {@code 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) { + if (sOff >= sLen) { + return true; + } else if (s.charAt(sOff) == '[') { + // check for subscript notation (e.g. "whatever[1]") + // the entire pattern matched up to the subscript: + // -> ignore the subscript + return true; + } else { + return false; + } + } + if (sOff >= sLen && pattern.charAt(pOff) != WILDCARD_CHAR) { + return false; + } + + // check for a '*' as the next pattern char; this is handled by + // a recursive call for each postfix of the name. + if (pattern.charAt(pOff) == WILDCARD_CHAR) { + 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) { + if (pattern.charAt(pOff) != s.charAt(sOff)) { + return false; + } + } + pOff++; + sOff++; + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JcrUtils.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JcrUtils.java new file mode 100644 index 00000000000..93a775bc100 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JcrUtils.java @@ -0,0 +1,1944 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons; + +import static java.net.URLDecoder.decode; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.StringTokenizer; + +import javax.jcr.Binary; +import javax.jcr.Item; +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.RepositoryFactory; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.EventListenerIterator; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.version.Version; +import javax.jcr.version.VersionIterator; + +/** + * Collection of static utility methods for use with the JCR API. + * + * @since Apache Jackrabbit 2.0 + */ +public class JcrUtils { + + /** + * The repository URI parameter name used by the + * {@link #getRepository(String)} method. All {@link RepositoryFactory} + * implementations that want to support this repository access convention + * should implement processing of this parameter. + *

    + * Client applications are recommended to use the + * {@link #getRepository(String)} method instead of directly referencing + * this constant unless they explicitly want to pass also other + * {@link RepositoryFactory} parameters through the + * {@link #getRepository(Map)} method. + */ + public static final String REPOSITORY_URI = "org.apache.jackrabbit.repository.uri"; + + /** + * A pre-allocated empty array of values. + * + * @since Apache Jackrabbit 2.3 + */ + public static final Value[] NO_VALUES = new Value[0]; + + /** + * Private constructor to prevent instantiation of this class. + */ + private JcrUtils() { + } + + /** + * Returns the default repository of the current environment. + * Implemented by calling {@link #getRepository(Map)} with a + * null parameter map. + * + * @see RepositoryFactory#getRepository(Map) + * @return default repository + * @throws RepositoryException if a default repository is not available + * or can not be accessed + */ + public static Repository getRepository() throws RepositoryException { + return getRepository((Map) null); + } + + /** + * Looks up the available {@link RepositoryFactory repository factories} + * and returns the {@link Repository repository} that one of the factories + * returns for the given settings. + *

    + * Note that unlike {@link RepositoryFactory#getRepository(Map)} this + * method will throw an exception instead of returning null + * if the given parameters can not be interpreted. + * + * @param parameters repository settings + * @return repository reference + * @throws RepositoryException if the repository can not be accessed, + * or if an appropriate repository factory + * is not available + */ + public static Repository getRepository(Map parameters) + throws RepositoryException { + String newline = System.getProperty("line.separator"); + + // Prepare the potential error message (JCR-2459) + StringBuilder log = new StringBuilder("Unable to access a repository"); + if (parameters != null) { + log.append(" with the following settings:"); + for (Map.Entry entry : parameters.entrySet()) { + log.append(newline); + log.append(" "); + log.append(entry.getKey()); + log.append(": "); + log.append(entry.getValue()); + } + } else { + log.append(" with the default settings."); + } + + // Use the query part of a repository URI as additional parameters + if (parameters != null + && parameters.containsKey(JcrUtils.REPOSITORY_URI)) { + String uri = parameters.get(JcrUtils.REPOSITORY_URI); + try { + URI u = new URI(uri); + String query = u.getRawQuery(); + if (query != null) { + Map copy = new HashMap(parameters); + for (String entry : query.split("&")) { + int i = entry.indexOf('='); + if (i != -1) { + copy.put( + decode(entry.substring(0, i), "UTF-8"), + decode(entry.substring(i + 1), "UTF-8")); + } else { + copy.put( + decode(entry, "UTF-8"), + Boolean.TRUE.toString()); + } + } + copy.put( + JcrUtils.REPOSITORY_URI, + new URI(u.getScheme(), u.getRawAuthority(), + u.getRawPath(), null, u.getRawFragment() + ).toASCIIString()); + parameters = copy; + } + } catch (URISyntaxException e) { + log.append(newline); + log.append("Note that the given repository URI was invalid:"); + log.append(newline); + log.append(" ").append(uri); + log.append(newline); + log.append(" ").append(e.getMessage()); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException("UTF-8 is not supported!", e); + } + } + + // Iterate through the available RepositoryFactories, with logging + log.append(newline); + log.append("The following RepositoryFactory classes were consulted:"); + Iterator iterator = + ServiceLoader.load(RepositoryFactory.class).iterator(); + while (iterator.hasNext()) { + RepositoryFactory factory = iterator.next(); + log.append(newline); + log.append(" "); + log.append(factory.getClass().getName()); + try { + Repository repository = factory.getRepository(parameters); + if (repository != null) { + // We found the requested repository! Return it + // and just ignore the error message being built. + return repository; + } else { + log.append(": declined"); + } + } catch (Exception e) { + log.append(": failed"); + for (Throwable c = e; c != null; c = c.getCause()) { + log.append(newline); + log.append(" because of "); + log.append(c.getClass().getSimpleName()); + log.append(": "); + log.append(c.getMessage()); + } + } + } + log.append(newline); + log.append( + "Perhaps the repository you are trying" + + " to access is not available at the moment."); + + // No matching repository found. Throw an exception with the + // detailed information we gathered during the above process. + throw new RepositoryException(log.toString()); + } + + /** + * Returns the repository identified by the given URI. This feature + * is implemented by calling the {@link #getRepository(Map)} method + * with the {@link #REPOSITORY_URI} parameter set to the given URI. + * Any query parameters are moved from the URI to the parameter map. + *

    + * See the documentation of the repository implementation you want + * to use for whether it supports this repository URI convention and + * for what the repository URI should look like. For example, + * Jackrabbit 2.0 supports the following types of repository URIs: + *

    + *
    http(s)://...
    + *
    + * A remote repository connection using SPI2DAVex with the given URL. + * See the jackrabbit-jcr2dav component for more details. + *
    + *
    file://...
    + *
    + * An embedded Jackrabbit repository located in the given directory. + * See the jackrabbit-core component for more details. + *
    + *
    jndi:...
    + *
    + * JNDI lookup for the named repository. See the + * {@link JndiRepositoryFactory} class for more details. + *
    + *
    + * + * @param uri repository URI + * @return repository instance + * @throws RepositoryException if the repository can not be accessed, + * or if the given URI is unknown or invalid + */ + public static Repository getRepository(String uri) + throws RepositoryException { + Map parameters = new HashMap(); + parameters.put(JcrUtils.REPOSITORY_URI, uri); + return getRepository(parameters); + } + + /** + * Returns an {@link Iterable} over the shared set of the given node. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getSharedSet() + * @param node shared node + * @return nodes in the shared set + * @throws RepositoryException if the {@link Node#getSharedSet()} call fails + */ + public static Iterable getSharedSet(final Node node) + throws RepositoryException { + final NodeIterator iterator = node.getSharedSet(); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getSharedSet(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over the children of the given node. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getNodes() + * @param node parent node + * @return child nodes + * @throws RepositoryException if the {@link Node#getNodes()} call fails + */ + public static Iterable getChildNodes(final Node node) + throws RepositoryException { + final NodeIterator iterator = node.getNodes(); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getNodes(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over those children of the given node + * that match the given name pattern. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getNodes(String) + * @param node parent node + * @param pattern node name pattern + * @return matching child nodes + * @throws RepositoryException + * if the {@link Node#getNodes(String)} call fails + */ + public static Iterable getChildNodes( + final Node node, final String pattern) throws RepositoryException { + final NodeIterator iterator = node.getNodes(pattern); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getNodes(pattern); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over those children of the given node + * that match the given name patterns. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getNodes(String[]) + * @param node parent node + * @param globs node name pattern + * @return matching child nodes + * @throws RepositoryException + * if the {@link Node#getNodes(String[])} call fails + */ + public static Iterable getChildNodes( + final Node node, final String[] globs) throws RepositoryException { + final NodeIterator iterator = node.getNodes(globs); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getNodes(globs); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over the properties of the given node. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getProperties() + * @param node node + * @return properties of the node + * @throws RepositoryException + * if the {@link Node#getProperties()} call fails + */ + public static Iterable getProperties(final Node node) + throws RepositoryException { + final PropertyIterator iterator = node.getProperties(); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getProperties(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over those properties of the + * given node that match the given name pattern. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getProperties(String) + * @param node node + * @param pattern property name pattern + * @return matching properties of the node + * @throws RepositoryException + * if the {@link Node#getProperties(String)} call fails + */ + public static Iterable getProperties( + final Node node, final String pattern) throws RepositoryException { + final PropertyIterator iterator = node.getProperties(pattern); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getProperties(pattern); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over those properties of the + * given node that match the given name patterns. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getProperties(String[]) + * @param node node + * @param globs property name globs + * @return matching properties of the node + * @throws RepositoryException + * if the {@link Node#getProperties(String[])} call fails + */ + public static Iterable getProperties( + final Node node, final String[] globs) throws RepositoryException { + final PropertyIterator iterator = node.getProperties(globs); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getProperties(globs); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over references to the given node. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getReferences() + * @param node reference target + * @return references that point to the given node + * @throws RepositoryException + * if the {@link Node#getReferences()} call fails + */ + public static Iterable getReferences(final Node node) + throws RepositoryException { + final PropertyIterator iterator = node.getReferences(); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getReferences(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over those references to the + * given node that have the given name. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getReferences(String) + * @param node reference target + * @param name reference property name + * @return references with the given name that point to the given node + * @throws RepositoryException + * if the {@link Node#getReferences(String)} call fails + */ + public static Iterable getReferences( + final Node node, final String name) throws RepositoryException { + final PropertyIterator iterator = node.getReferences(name); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getReferences(name); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over weak references to the given node. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getWeakReferences() + * @param node reference target + * @return weak references that point to the given node + * @throws RepositoryException + * if the {@link Node#getWeakReferences()} call fails + */ + public static Iterable getWeakReferences(final Node node) + throws RepositoryException { + final PropertyIterator iterator = node.getWeakReferences(); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getWeakReferences(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over those weak references to the + * given node that have the given name. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see Node#getWeakReferences(String) + * @param node reference target + * @param name reference property name + * @return weak references with the given name that point to the given node + * @throws RepositoryException + * if the {@link Node#getWeakReferences(String)} call fails + */ + public static Iterable getWeakReferences( + final Node node, final String name) throws RepositoryException { + final PropertyIterator iterator = node.getWeakReferences(name); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return node.getWeakReferences(name); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over nodes in the given query result. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see QueryResult#getNodes() + * @param result query result + * @return nodes in the query result + * @throws RepositoryException + * if the {@link QueryResult#getNodes()} call fails + */ + public static Iterable getNodes(final QueryResult result) + throws RepositoryException { + final NodeIterator iterator = result.getNodes(); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return result.getNodes(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Returns an {@link Iterable} over nodes in the given query result. + *

    + * The first iterator is acquired directly during this method call to + * allow a possible {@link RepositoryException} to be thrown as-is. + * Further iterators are acquired lazily when needed, with possible + * {@link RepositoryException}s wrapped into {@link RuntimeException}s. + * + * @see QueryResult#getRows() + * @param result query result + * @return rows in the query result + * @throws RepositoryException + * if the {@link QueryResult#getRows()} call fails + */ + public static Iterable getRows(final QueryResult result) + throws RepositoryException { + final RowIterator iterator = result.getRows(); + return new Iterable() { + private boolean first = true; + @Override @SuppressWarnings("unchecked") + public synchronized Iterator iterator() { + if (first) { + first = false; + return iterator; + } else { + try { + return result.getRows(); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } + }; + } + + /** + * Transform any type of {@link Iterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param type + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + public static Iterable in(final Iterator iterator) { + return new Iterable() { + private boolean stale = false; + + @Override + public synchronized Iterator iterator() { + if (stale) { + throw new IllegalStateException("Cannot reuse Iterable intended for single use"); + } + + stale = true; + return iterator; + } + }; + } + + /** + * Transform an {@link AccessControlPolicyIterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable in(AccessControlPolicyIterator iterator) { + return in((Iterator) iterator); + } + + /** + * Transform an {@link EventIterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable in(EventIterator iterator) { + return in((Iterator) iterator); + } + + /** + * Transform an {@link EventListenerIterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable in(EventListenerIterator iterator) { + return in((Iterator) iterator); + } + + /** + * Transform an {@link NodeIterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable in(NodeIterator iterator) { + return in((Iterator) iterator); + } + + /** + * Transform an {@link NodeTypeIterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable in(NodeTypeIterator iterator) { + return in((Iterator) iterator); + } + + /** + * Transform an {@link PropertyIterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable in(PropertyIterator iterator) { + return in((Iterator) iterator); + } + + /** + * Transform an {@link RowIterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable in(RowIterator iterator) { + return in((Iterator) iterator); + } + + /** + * Transform an {@link VersionIterator} into an {@link Iterable} + * for single use in a Java 5 for-each loop. + *

    + * While general purpose Iterables tend to be reusable, + * this wrapper Iterable consumes the argument + * Iterator, leaving it in a non-reusable state. The returned + * Iterable will throw an IllegalStateException if + * its iterator() method is invoked a second time. + * + * @param iterator + * The input Iterator + * @return The wrapping Iterable + */ + @SuppressWarnings("unchecked") + public static Iterable in(VersionIterator iterator) { + return in((Iterator) iterator); + } + + /** + * Returns the named child of the given node, creating the child if + * it does not already exist. If the child node gets added, then its + * type will be determined by the child node definitions associated + * with the parent node. The caller is expected to take care of saving + * or discarding any transient changes. + * + * @see Node#getNode(String) + * @see Node#addNode(String) + * @param parent parent node + * @param name name of the child node + * @return the child node + * @throws RepositoryException if the child node can not be + * accessed or created + */ + public static Node getOrAddNode(Node parent, String name) + throws RepositoryException { + return getOrAddNode(parent, name, null); + } + + /** + * Returns the named child of the given node, creating the child if + * it does not already exist. If the child node gets added, then it + * is created with the given node type. The caller is expected to take + * care of saving or discarding any transient changes. + * + * @see Node#getNode(String) + * @see Node#addNode(String, String) + * @see Node#isNodeType(String) + * @param parent parent node + * @param name name of the child node + * @param type type of the child node or {@code null}, + * ignored if the child already exists + * @return the child node + * @throws RepositoryException if the child node can not be accessed + * or created + */ + public static Node getOrAddNode(Node parent, String name, String type) + throws RepositoryException { + if (parent.hasNode(name)) { + return parent.getNode(name); + } else if (type != null) { + return parent.addNode(name, type); + } else { + return parent.addNode(name); + } + } + + /** + * Returns the named child of the given node, creating it as an + * nt:folder node if it does not already exist. The caller is expected + * to take care of saving or discarding any transient changes. + *

    + * Note that the type of the returned node is not guaranteed + * to match nt:folder in case the node already existed. The caller can + * use an explicit {@link Node#isNodeType(String)} check if needed, or + * simply use a data-first approach and not worry about the node type + * until a constraint violation is encountered. + * + * @param parent parent node + * @param name name of the child node + * @return the child node + * @throws RepositoryException if the child node can not be accessed + * or created + */ + public static Node getOrAddFolder(Node parent, String name) + throws RepositoryException { + return getOrAddNode(parent, name, NodeType.NT_FOLDER); + } + + /** + * Creates or updates the named child of the given node. If the child + * does not already exist, then it is created using the nt:file node type. + * This file child node is returned from this method. + *

    + * If the file node does not already contain a jcr:content child, then + * one is created using the nt:resource node type. The following + * properties are set on the jcr:content node: + *

    + *
    jcr:mimeType
    + *
    media type
    + *
    jcr:encoding (optional)
    + *
    charset parameter of the media type, if any
    + *
    jcr:lastModified
    + *
    current time
    + *
    jcr:data
    + *
    binary content
    + *
    + *

    + * Note that the types of the returned node or the jcr:content child are + * not guaranteed to match nt:file and nt:resource in case the + * nodes already existed. The caller can use an explicit + * {@link Node#isNodeType(String)} check if needed, or simply use a + * data-first approach and not worry about the node type until a constraint + * violation is encountered. + *

    + * The given binary content stream is closed by this method. + * + * @param parent parent node + * @param name name of the file + * @param mime media type of the file + * @param data binary content of the file + * @return the child node + * @throws RepositoryException if the child node can not be created + * or updated + */ + public static Node putFile( + Node parent, String name, String mime, InputStream data) + throws RepositoryException { + return putFile(parent, name, mime, data, Calendar.getInstance()); + } + + /** + * Creates or updates the named child of the given node. If the child + * does not already exist, then it is created using the nt:file node type. + * This file child node is returned from this method. + *

    + * If the file node does not already contain a jcr:content child, then + * one is created using the nt:resource node type. The following + * properties are set on the jcr:content node: + *

    + *
    jcr:mimeType
    + *
    media type
    + *
    jcr:encoding (optional)
    + *
    charset parameter of the media type, if any
    + *
    jcr:lastModified
    + *
    date of last modification
    + *
    jcr:data
    + *
    binary content
    + *
    + *

    + * Note that the types of the returned node or the jcr:content child are + * not guaranteed to match nt:file and nt:resource in case the + * nodes already existed. The caller can use an explicit + * {@link Node#isNodeType(String)} check if needed, or simply use a + * data-first approach and not worry about the node type until a constraint + * violation is encountered. + *

    + * The given binary content stream is closed by this method. + * + * @param parent parent node + * @param name name of the file + * @param mime media type of the file + * @param data binary content of the file + * @param date date of last modification + * @return the child node + * @throws RepositoryException if the child node can not be created + * or updated + */ + public static Node putFile( + Node parent, String name, String mime, + InputStream data, Calendar date) throws RepositoryException { + Binary binary = parent.getSession().getValueFactory().createBinary(data); + try { + Node file = getOrAddNode(parent, name, NodeType.NT_FILE); + Node content = getOrAddNode(file, Node.JCR_CONTENT, NodeType.NT_RESOURCE); + + content.setProperty(Property.JCR_MIMETYPE, mime); + String[] parameters = mime.split(";"); + for (int i = 1; i < parameters.length; i++) { + int equals = parameters[i].indexOf('='); + if (equals != -1) { + String parameter = parameters[i].substring(0, equals); + if ("charset".equalsIgnoreCase(parameter.trim())) { + content.setProperty( + Property.JCR_ENCODING, + parameters[i].substring(equals + 1).trim()); + } + } + } + + content.setProperty(Property.JCR_LAST_MODIFIED, date); + content.setProperty(Property.JCR_DATA, binary); + return file; + } finally { + binary.dispose(); + } + } + + /** + * Returns a stream for reading the contents of the file stored at the + * given node. This method works with both on nt:file and nt:resource and + * on any other similar node types, as it only looks for the jcr:data + * property or a jcr:content child node. + *

    + * The returned stream contains a reference to the underlying + * {@link Binary} value instance that will be disposed when the stream + * is closed. It is the responsibility of the caller to close the stream + * once it is no longer needed. + * + * @since Apache Jackrabbit 2.3 + * @param node node to be read + * @return stream for reading the file contents + * @throws RepositoryException if the file can not be accessed + */ + public static InputStream readFile(Node node) throws RepositoryException { + if (node.hasProperty(Property.JCR_DATA)) { + Property data = node.getProperty(Property.JCR_DATA); + final Binary binary = data.getBinary(); + return new FilterInputStream(binary.getStream()) { + @Override + public void close() throws IOException { + super.close(); + binary.dispose(); + } + }; + } else if (node.hasNode(Node.JCR_CONTENT)) { + return readFile(node.getNode(Node.JCR_CONTENT)); + } else { + throw new RepositoryException( + "Unable to read file node: " + node.getPath()); + } + } + + /** + * Writes the contents of file stored at the given node to the given + * stream. Similar file handling logic is used as in the + * {@link #readFile(Node)} method. + * + * @since Apache Jackrabbit 2.3 + * @param node node to be read + * @param output to which the file contents are written + * @throws RepositoryException if the file can not be accessed + * @throws IOException if the file can not be read or written + */ + public static void readFile(Node node, OutputStream output) + throws RepositoryException, IOException { + InputStream input = readFile(node); + try { + byte[] buffer = new byte[16 * 1024]; + int n = input.read(buffer); + while (n != -1) { + output.write(buffer, 0, n); + n = input.read(buffer); + } + } finally { + input.close(); + } + } + + /** + * Returns the last modified date of the given file node. The value is + * read from the jcr:lastModified property of this node or alternatively + * from a jcr:content child node. + * + * @since Apache Jackrabbit 2.3 + * @param node file node + * @return last modified date, or null if not available + * @throws RepositoryException if the last modified date can not be accessed + */ + public static Calendar getLastModified(Node node) throws RepositoryException { + if (node.hasProperty(Property.JCR_LAST_MODIFIED)) { + return node.getProperty(Property.JCR_LAST_MODIFIED).getDate(); + } else if (node.hasNode(Node.JCR_CONTENT)) { + return getLastModified(node.getNode(Node.JCR_CONTENT)); + } else { + return null; + } + } + + /** + * Sets the last modified date of the given file node. The value is + * written to the jcr:lastModified property of a jcr:content child node + * or this node if such a child does not exist. + * + * @since Apache Jackrabbit 2.3 + * @param node file node + * @param date modified date + * @throws RepositoryException if the last modified date can not be set + */ + public static void setLastModified(Node node, Calendar date) throws RepositoryException { + if (node.hasNode(Node.JCR_CONTENT)) { + setLastModified(node.getNode(Node.JCR_CONTENT), date); + } else { + node.setProperty(Property.JCR_LAST_MODIFIED, date); + } + } + + /** + * Returns a string representation of the given item. The returned string + * is designed to be easily readable while providing maximum amount of + * information for logging and debugging purposes. + *

    + * The returned string is not meant to be parsed and the exact contents + * can change in future releases. The current string representation of + * a node is "/path/to/node [type]" and the representation of a property is + * "@name = value(s)". Binary values are expressed like "<123 bytes>" + * and other values as their standard binary representation. Multi-valued + * properties have their values listed in like "[ v1, v2, v3, ... ]". No + * more than the three first values are included. Long string values are + * truncated. + * + * @param item given node or property + * @return string representation of the given item + */ + public static String toString(Item item) { + StringBuilder builder = new StringBuilder(); + try { + if (item.isNode()) { + builder.append(item.getPath()); + builder.append(" ["); + builder.append(((Node) item).getPrimaryNodeType().getName()); + builder.append("]"); + } else { + builder.append("@"); + builder.append(item.getName()); + builder.append(" = "); + Property property = (Property) item; + if (property.isMultiple()) { + builder.append("[ "); + Value[] values = property.getValues(); + for (int i = 0; i < values.length && i < 3; i++) { + if (i > 0) { + builder.append(", "); + } + append(builder, values[i]); + } + if (values.length >= 3) { + builder.append(", ..."); + } + builder.append(" ]"); + } else { + append(builder, property.getValue()); + } + } + } catch (RepositoryException e) { + builder.append("!!! "); + builder.append(e.getMessage()); + builder.append(" !!!"); + } + return builder.toString(); + } + + /** + * Private helper method that adds the string representation of the given + * value to the given {@link StringBuilder}. Used by the + * {{@link #toString(Item)} method. + */ + private static void append(StringBuilder builder, Value value) + throws RepositoryException { + if (value.getType() == PropertyType.BINARY) { + Binary binary = value.getBinary(); + try { + builder.append("<"); + builder.append(binary.getSize()); + builder.append(" bytes>"); + } finally { + binary.dispose(); + } + } else { + String string = value.getString(); + if (string.length() > 40) { + builder.append(string.substring(0, 37)); + builder.append("..."); + } else { + builder.append(string); + } + } + } + + private static final List PROPERTY_TYPES_NAMES = new ArrayList(); + private static final Map PROPERTY_TYPES = new HashMap(); + static { + for (int i = PropertyType.UNDEFINED; i <= PropertyType.DECIMAL; i++) { + String typeName = PropertyType.nameFromValue(i); + PROPERTY_TYPES_NAMES.add(typeName); + PROPERTY_TYPES.put(typeName.toLowerCase(), i); + } + } + + /** + * Returns the numeric constant value of the property type with + * the specified name. This method is like + * {@link PropertyType#valueFromName(String)}, but the name lookup + * is case insensitive. + * + * @since Apache Jackrabbit 2.3 + * @param name name of the property type (case insensitive) + * @return property type constant + * @throws IllegalArgumentException if the given name is not a valid + * property type name + */ + public static int getPropertyType(String name) + throws IllegalArgumentException { + Integer type = PROPERTY_TYPES.get(name.toLowerCase()); + if (type != null) { + return type; + } else { + throw new IllegalArgumentException("Unknown property type: " + name); + } + } + + /** + * Return the property type names including or excluding 'undefined' depending + * on the specified flag. + * + * @param includeUndefined If true the returned array will contain the name + * of the 'undefined' property type. + * @return array of property type names. + */ + public static String[] getPropertyTypeNames(boolean includeUndefined) { + if (includeUndefined) { + return PROPERTY_TYPES_NAMES.toArray(new String[PROPERTY_TYPES_NAMES.size()]); + } else { + String[] typeNames = new String[PROPERTY_TYPES_NAMES.size()-1]; + int i = 0; + for (String name : PROPERTY_TYPES_NAMES) { + if (!PropertyType.TYPENAME_UNDEFINED.equals(name)) { + typeNames[i++] = name; + } + } + return typeNames; + } + } + + /** + * Creates or gets the {@link javax.jcr.Node Node} at the given Path. + * In case it has to create the Node all non-existent intermediate path-elements + * will be created with the given NodeType. + * + *

    + * Changes made are not saved by this method, so session.save() + * has to be called to persist them. + * + * @param absolutePath absolute path to create + * @param nodeType to use for creation of nodes If null the node type + * is determined by the child node definitions of the parent node. + * @param session to use + * @return the Node at path + * @throws RepositoryException in case of exception accessing the Repository + */ + public static Node getOrCreateByPath(String absolutePath, String nodeType, Session session) + throws RepositoryException { + return getOrCreateByPath(absolutePath, false, nodeType, nodeType, session, false); + } + + /** + * Creates or gets the {@link javax.jcr.Node Node} at the given Path. + * In case it has to create the Node all non-existent intermediate path-elements + * will be created with the given intermediate node type and the returned node + * will be created with the given nodeType. + * + * @param absolutePath absolute path to create + * @param intermediateNodeType to use for creation of intermediate nodes. If null the node type + * is determined by the child node definitions of the parent node. + * @param nodeType to use for creation of the final node. If null the node type + * is determined by the child node definitions of the parent node. + * @param session to use + * @param autoSave Should save be called when a new node is created? + * @return the Node at absolutePath + * @throws RepositoryException in case of exception accessing the Repository + */ + public static Node getOrCreateByPath(String absolutePath, + String intermediateNodeType, + String nodeType, Session session, + boolean autoSave) + throws RepositoryException { + return getOrCreateByPath(absolutePath, false, intermediateNodeType, nodeType, session, autoSave); + } + + /** + * Creates a {@link javax.jcr.Node Node} at the given Path. In case it has + * to create the Node all non-existent intermediate path-elements will be + * created with the given intermediate node type and the returned node will + * be created with the given nodeType. + * + *

    + * If the path points to an existing node, the leaf node name will be + * regarded as a name hint and a unique node name will be created by + * appending a number to the given name (eg. /some/path/foobar2). + * Please note that the uniqueness check is not an atomic JCR operation, + * so it is possible that you get a {@link RepositoryException} (path + * already exists) if another concurrent session created the same node in + * the meantime. + * + *

    + * Changes made are not saved by this method, so session.save() + * has to be called to persist them. + * + * @param pathHint + * path to create + * @param nodeType + * to use for creation of nodes. . If null the node type + * is determined by the child node definitions of the parent node. + * @param session + * to use + * @return the newly created Node + * @throws RepositoryException + * in case of exception accessing the Repository + */ + public static Node getOrCreateUniqueByPath(String pathHint, String nodeType, Session session) + throws RepositoryException { + return getOrCreateByPath(pathHint, true, nodeType, nodeType, session, false); + } + + /** + * Creates or gets the {@link javax.jcr.Node Node} at the given Path. In + * case it has to create the Node all non-existent intermediate + * path-elements will be created with the given intermediate node type and + * the returned node will be created with the given nodeType. + * + *

    + * If the parameter createUniqueLeaf is set, it will not get + * an existing node but rather try to create a unique node by appending a + * number to the last path element (leaf node). Please note that the + * uniqueness check is not an atomic JCR operation, so it is possible + * that you get a {@link RepositoryException} (path already exists) if + * another concurrent session created the same node in the meantime. + * + * @param absolutePath + * absolute path to create + * @param createUniqueLeaf + * whether the leaf of the path should be regarded as a name hint + * and a unique node name should be created by appending a number + * to the given name (eg. /some/path/foobar2) + * @param intermediateNodeType + * to use for creation of intermediate nodes. If null the node type + * is determined by the child node definitions of the parent node. + * @param nodeType + * to use for creation of the final node. If null the node type + * is determined by the child node definitions of the parent node. + * @param session + * to use + * @param autoSave + * Should save be called when a new node is created? + * @return the Node at absolutePath + * @throws RepositoryException + * in case of exception accessing the Repository + */ + public static Node getOrCreateByPath(String absolutePath, + boolean createUniqueLeaf, + String intermediateNodeType, + String nodeType, Session session, + boolean autoSave) + throws RepositoryException { + if (absolutePath == null || absolutePath.length() == 0 || "/".equals(absolutePath)) { + // path denotes root node + return session.getRootNode(); + } else if (!absolutePath.startsWith("/")) { + throw new IllegalArgumentException("not an absolute path: " + absolutePath); + } else if (session.nodeExists(absolutePath) && !createUniqueLeaf) { + return session.getNode(absolutePath); + } else { + // find deepest existing parent node + String path = absolutePath; + int currentIndex = path.lastIndexOf('/'); + String existingPath = null; + while (currentIndex > 0 && existingPath == null) { + path = path.substring(0, currentIndex); + if (session.nodeExists(path)) { + existingPath = path; + } else { + currentIndex = path.lastIndexOf('/'); + } + } + // create path relative to the root node + return getOrCreateByPath(existingPath == null ? session.getRootNode() : session.getNode(existingPath), + absolutePath.substring(currentIndex + 1), createUniqueLeaf, intermediateNodeType, nodeType, autoSave); + } + } + + /** + * Creates or gets the {@link javax.jcr.Node node} at the given path. In + * case it has to create the node, nodes for all non-existent intermediate + * path-elements will be created with the given intermediate node type and + * the returned node will be created with the given nodeType. + * Note: When the given path contains parent elements this method might + * create multiple nodes at leaf position (e.g "a/../b" will create the + * child nodes "a" and "b" on the current node). + * + *

    + * If the node name points to an existing node, the node name will be + * regarded as a name hint and a unique node name will be created by + * appending a number to the given name (eg. /some/path/foobar2). + * Please note that the uniqueness check is not an atomic JCR operation, + * so it is possible that you get a {@link RepositoryException} (path + * already exists) if another concurrent session created the same node in + * the meantime. + * + *

    + * Changes made are not saved by this method, so session.save() + * has to be called to persist them. + * + * @param parent + * existing parent node for the new node + * @param nodeNameHint + * name hint for the new node + * @param nodeType + * to use for creation of the node. If null the node type + * is determined by the child node definitions of the parent node. + * @return the newly created Node + * @throws RepositoryException + * in case of exception accessing the Repository + */ + public static Node getOrCreateUniqueByPath(Node parent, + String nodeNameHint, + String nodeType) + throws RepositoryException { + return getOrCreateByPath(parent, nodeNameHint, true, nodeType, nodeType, false); + } + + /** + * Creates or gets the {@link javax.jcr.Node node} at the given path + * relative to the baseNode. In case it has to create the node, nodes for + * all non-existent intermediate path-elements will be created with the given + * intermediate node type and the returned node will be created with the + * given nodeType. Note: When the given path contains parent elements + * this method might create multiple nodes at leaf position (e.g "a/../b" + * will create the child nodes "a" and "b" on the current node). + *

    + * If the parameter createUniqueLeaf is set, it will not get + * an existing node but rather try to create a unique node by appending a + * number to the last path element (leaf node). Please note that the + * uniqueness check is not an atomic JCR operation, so it is possible + * that you get a {@link RepositoryException} (path already exists) if + * another concurrent session created the same node in the meantime. + * + * @param baseNode + * existing node that should be the base for the relative path + * @param path + * relative path to create + * @param createUniqueLeaf + * whether the leaf of the path should be regarded as a name hint + * and a unique node name should be created by appending a number + * to the given name (eg. /some/path/foobar2) + * @param intermediateNodeType + * to use for creation of intermediate nodes. If null the node type + * is determined by the child node definitions of the parent node. + * @param nodeType + * to use for creation of the final node. If null the node type + * is determined by the child node definitions of the parent node. + * @param autoSave + * Should save be called when a new node is created? + * @return the Node at path + * @throws RepositoryException + * in case of exception accessing the Repository + */ + public static Node getOrCreateByPath(Node baseNode, + String path, + boolean createUniqueLeaf, + String intermediateNodeType, + String nodeType, + boolean autoSave) + throws RepositoryException { + + if (!createUniqueLeaf && baseNode.hasNode(path)) { + // node at path already exists, quicker way + return baseNode.getNode(path); + } + + // find the parent that exists + // we can start from the deepest child in tree + String fullPath = baseNode.getPath().equals("/") ? "/" + path : baseNode.getPath() + "/" + path; + int currentIndex = fullPath.lastIndexOf('/'); + String temp = fullPath; + String existingPath = null; + while (currentIndex > 0) { + temp = temp.substring(0, currentIndex); + // break when first existing parent is found + if (baseNode.getSession().itemExists(temp)) { + existingPath = temp; + break; + } + currentIndex = temp.lastIndexOf('/'); + } + + if (existingPath != null) { + baseNode = baseNode.getSession().getNode(existingPath); + path = fullPath.substring(existingPath.length() + 1); + } + + Node node = baseNode; + int pos = path.lastIndexOf('/'); + + // intermediate path elements + if (pos != -1) { + final StringTokenizer st = new StringTokenizer(path.substring(0, pos), "/"); + while (st.hasMoreTokens()) { + final String token = st.nextToken(); + if (!node.hasNode(token)) { + try { + if (intermediateNodeType != null) { + node.addNode(token, intermediateNodeType); + } else { + node.addNode(token); + } + if (autoSave) { + node.getSession().save(); + } + } catch (RepositoryException e) { + // we ignore this as this folder might be created from a different task + node.refresh(false); + } + } + node = node.getNode(token); + } + path = path.substring(pos + 1); + } + + // last path element (path = leaf node name) + if (!node.hasNode(path)) { + if (nodeType != null) { + node.addNode(path, nodeType); + } else { + node.addNode(path); + } + if (autoSave) { + node.getSession().save(); + } + } else if (createUniqueLeaf) { + // leaf node already exists, create new unique name + String leafNodeName; + int i = 0; + do { + leafNodeName = path + String.valueOf(i); + i++; + } while (node.hasNode(leafNodeName)); + + Node leaf; + if (nodeType != null) { + leaf = node.addNode(leafNodeName, nodeType); + } else { + leaf = node.addNode(leafNodeName); + } + if (autoSave) { + node.getSession().save(); + } + return leaf; + } + + return node.getNode(path); + } + + /** + * Get the node at relPath from baseNode or null if no such node exists. + * + * @param baseNode existing node that should be the base for the relative path + * @param relPath relative path to the node to get + * @return the node at relPath from baseNode or null if no such node exists. + * @throws RepositoryException in case of exception accessing the Repository + */ + public static Node getNodeIfExists(Node baseNode, String relPath) throws RepositoryException { + try { + return baseNode.getNode(relPath); + } catch (PathNotFoundException e) { + return null; + } + } + + /** + * Gets the node at absPath or null if no such node exists. + * + * @param absPath the absolute path to the node to return + * @param session to use + * @return the node at absPath or null if no such node exists. + * @throws RepositoryException in case of exception accessing the Repository + */ + public static Node getNodeIfExists(String absPath, Session session) throws RepositoryException { + try { + return session.getNode(absPath); + } catch (PathNotFoundException e) { + return null; + } + } + + /** + * Returns the string property value at relPath from baseNode or defaultValue + * if no such property exists. + * + * @param baseNode existing node that should be the base for the relative path + * @param relPath relative path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the string property value at relPath from baseNode or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static String getStringProperty(Node baseNode, String relPath, String defaultValue) throws RepositoryException { + try { + return baseNode.getProperty(relPath).getString(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the long property value at relPath from baseNode or defaultValue + * if no such property exists. + * + * @param baseNode existing node that should be the base for the relative path + * @param relPath relative path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the long property value at relPath from baseNode or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static long getLongProperty(Node baseNode, String relPath, long defaultValue) throws RepositoryException { + try { + return baseNode.getProperty(relPath).getLong(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the double property value at relPath from baseNode or defaultValue + * if no such property exists. + * + * @param baseNode existing node that should be the base for the relative path + * @param relPath relative path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the double property value at relPath from baseNode or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static double getDoubleProperty(Node baseNode, String relPath, double defaultValue) throws RepositoryException { + try { + return baseNode.getProperty(relPath).getDouble(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the boolean property value at relPath from baseNode or defaultValue + * if no such property exists. + * + * @param baseNode existing node that should be the base for the relative path + * @param relPath relative path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the boolean property value at relPath from baseNode or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static boolean getBooleanProperty(Node baseNode, String relPath, boolean defaultValue) throws RepositoryException { + try { + return baseNode.getProperty(relPath).getBoolean(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the date property value at relPath from baseNode or defaultValue + * if no such property exists. + * + * @param baseNode existing node that should be the base for the relative path + * @param relPath relative path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the date property value at relPath from baseNode or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static Calendar getDateProperty(Node baseNode, String relPath, Calendar defaultValue) throws RepositoryException { + try { + return baseNode.getProperty(relPath).getDate(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the decimal property value at relPath from baseNode or defaultValue + * if no such property exists. + * + * @param baseNode existing node that should be the base for the relative path + * @param relPath relative path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the decimal property value at relPath from baseNode or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static BigDecimal getDecimalProperty(Node baseNode, String relPath, BigDecimal defaultValue) throws RepositoryException { + try { + return baseNode.getProperty(relPath).getDecimal(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the binary property value at relPath from baseNode or defaultValue + * if no such property exists. + * + * @param baseNode existing node that should be the base for the relative path + * @param relPath relative path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the binary property value at relPath from baseNode or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static Binary getBinaryProperty(Node baseNode, String relPath, Binary defaultValue) throws RepositoryException { + try { + return baseNode.getProperty(relPath).getBinary(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the string property value at absPath or defaultValue + * if no such property exists. + * + * @param session to use + * @param absPath absolute path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the string property value at absPath or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static String getStringProperty(Session session, String absPath, String defaultValue) throws RepositoryException { + try { + return session.getProperty(absPath).getString(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the long property value at absPath or defaultValue + * if no such property exists. + * + * @param session to use + * @param absPath absolute path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the long property value at absPath or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static long getLongProperty(Session session, String absPath, long defaultValue) throws RepositoryException { + try { + return session.getProperty(absPath).getLong(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the double property value at absPath or defaultValue + * if no such property exists. + * + * @param session to use + * @param absPath absolute path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the double property value at absPath or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static double getDoubleProperty(Session session, String absPath, double defaultValue) throws RepositoryException { + try { + return session.getProperty(absPath).getDouble(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the boolean property value at absPath or defaultValue + * if no such property exists. + * + * @param session to use + * @param absPath absolute path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the boolean property value at absPath or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static boolean getBooleanProperty(Session session, String absPath, boolean defaultValue) throws RepositoryException { + try { + return session.getProperty(absPath).getBoolean(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the date property value at absPath or defaultValue + * if no such property exists. + * + * @param session to use + * @param absPath absolute path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the date property value at absPath or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static Calendar getDateProperty(Session session, String absPath, Calendar defaultValue) throws RepositoryException { + try { + return session.getProperty(absPath).getDate(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the decimal property value at absPath or defaultValue + * if no such property exists. + * + * @param session to use + * @param absPath absolute path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the decimal property value at absPath or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static BigDecimal getDecimalProperty(Session session, String absPath, BigDecimal defaultValue) throws RepositoryException { + try { + return session.getProperty(absPath).getDecimal(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } + + /** + * Returns the binary property value at absPath or defaultValue + * if no such property exists. + * + * @param session to use + * @param absPath absolute path to the property to get + * @param defaultValue default value to return when the property does not exist + * @return the binary property value at absPath or defaultValue + * if no such property exists + * @throws RepositoryException in case of exception accessing the Repository + */ + public static Binary getBinaryProperty(Session session, String absPath, Binary defaultValue) throws RepositoryException { + try { + return session.getProperty(absPath).getBinary(); + } catch (PathNotFoundException e) { + return defaultValue; + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JndiRepositoryFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JndiRepositoryFactory.java new file mode 100644 index 00000000000..92274df6f3f --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JndiRepositoryFactory.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.commons; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Hashtable; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +/** + * JNDI-based JCR repository factory. This factory looks up {@link Repository} + * instances from JNDI directories based on the following parameters: + *

    + *
    {@link #JNDI_NAME org.apache.jackrabbit.repository.jndi.name}
    + *
    + * The value of this parameter is used as a JNDI name for looking + * up the repository. + *
    + *
    {@link JcrUtils#REPOSITORY_URI org.apache.jackrabbit.repository.uri}
    + *
    + * If the URI scheme is "jndi", then the remainder of the URI is used + * as a JNDI name for looking up the repository. + *
    + *
    + *

    + * All the other repository parameters are passed as the environment of the + * {@link InitialContext initial JNDI context}. + *

    + * Clients should normally only use this class through the Java Service + * Provider mechanism. See the getRepository utility methods in + * {@link JcrUtils} for an easy way to do that. + * + * @since Apache Jackrabbit 2.0 + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class JndiRepositoryFactory implements RepositoryFactory { + + /** + * The JNDI name parameter name. + */ + public static final String JNDI_NAME = + "org.apache.jackrabbit.repository.jndi.name"; + + public Repository getRepository(Map parameters) + throws RepositoryException { + if (parameters == null) { + return null; // no default JNDI repository + } else { + Hashtable environment = new Hashtable(parameters); + if (environment.containsKey(JNDI_NAME)) { + String name = environment.remove(JNDI_NAME).toString(); + return getRepository(name, environment); + } else if (environment.containsKey(JcrUtils.REPOSITORY_URI)) { + Object parameter = environment.remove(JcrUtils.REPOSITORY_URI); + try { + URI uri = new URI(parameter.toString().trim()); + if ("jndi".equalsIgnoreCase(uri.getScheme())) { + return getRepository(uri, environment); + } else { + return null; // not a jndi: URI + } + } catch (URISyntaxException e) { + return null; // not a valid URI + } + } else { + return null; // unknown parameters + } + } + } + + private Repository getRepository(URI uri, Hashtable environment) + throws RepositoryException { + String name; + if (uri.isOpaque()) { + name = uri.getSchemeSpecificPart(); + } else { + name = uri.getPath(); + if (name == null) { + name = ""; + } else if (name.startsWith("/")) { + name = name.substring(1); + } + String authority = uri.getAuthority(); + if (authority != null && authority.length() > 0) { + environment = new Hashtable(environment); + environment.put(Context.INITIAL_CONTEXT_FACTORY, authority); + } + } + return getRepository(name, environment); + } + + private Repository getRepository(String name, Hashtable environment) + throws RepositoryException { + try { + Object value = new InitialContext(environment).lookup(name); + if (value instanceof Repository) { + return (Repository) value; + } else { + throw new RepositoryException( + "Invalid repository object " + value + + " found at " + name + " in JNDI environment " + + environment); + } + } catch (NamingException e) { + throw new RepositoryException( + "Failed to look up " + name + + " from JNDI environment " + environment, e); + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/NamespaceHelper.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/NamespaceHelper.java new file mode 100644 index 00000000000..a5581185c5c --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/NamespaceHelper.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.util.XMLChar; + +/** + * Helper class for working with JCR namespaces. + * + * @since Jackrabbit JCR Commons 1.5 + */ +public class NamespaceHelper { + + /** + * The jcr namespace URI. + */ + public static final String JCR = "http://www.jcp.org/jcr/1.0"; + + /** + * The nt namespace URI. + */ + public static final String NT = "http://www.jcp.org/jcr/nt/1.0"; + + /** + * The mix namespace URI. + */ + public static final String MIX = "http://www.jcp.org/jcr/mix/1.0"; + + /** + * Current session. + */ + private final Session session; + + /** + * Creates a namespace helper for the given session. + * + * @param session current session + */ + public NamespaceHelper(Session session) { + this.session = session; + } + + /** + * Returns a map containing all prefix to namespace URI mappings of + * the current session. The returned map is newly allocated and can + * can be freely modified by the caller. + * + * @see Session#getNamespacePrefixes() + * @return namespace mappings + * @throws RepositoryException if the namespaces could not be retrieved + */ + public Map getNamespaces() throws RepositoryException { + Map namespaces = new HashMap(); + String[] prefixes = session.getNamespacePrefixes(); + for (String prefixe : prefixes) { + namespaces.put(prefixe, session.getNamespaceURI(prefixe)); + } + return namespaces; + } + + /** + * Returns the prefix mapped to the given namespace URI in the current + * session, or null if the namespace does not exist. + * + * @see Session#getNamespacePrefix(String) + * @param uri namespace URI + * @return namespace prefix, or null + * @throws RepositoryException if the namespace could not be retrieved + */ + public String getPrefix(String uri) throws RepositoryException { + try { + return session.getNamespacePrefix(uri); + } catch (NamespaceException e) { + return null; + } + } + + /** + * Returns the namespace URI mapped to the given prefix in the current + * session, or null if the namespace does not exist. + * + * @see Session#getNamespaceURI(String) + * @param prefix namespace prefix + * @return namespace prefix, or null + * @throws RepositoryException if the namespace could not be retrieved + */ + public String getURI(String prefix) throws RepositoryException { + try { + return session.getNamespaceURI(prefix); + } catch (NamespaceException e) { + return null; + } + } + + /** + * Returns the prefixed JCR name for the given namespace URI and local + * name in the current session. + * + * @param uri namespace URI + * @param name local name + * @return prefixed JCR name + * @throws NamespaceException if the namespace does not exist + * @throws RepositoryException if the namespace could not be retrieved + */ + public String getJcrName(String uri, String name) + throws NamespaceException, RepositoryException { + if (uri != null && uri.length() > 0) { + return session.getNamespacePrefix(uri) + ":" + name; + } else { + return name; + } + } + + /** + * Replaces the standard jcr, nt, or + * mix prefix in the given name with the prefix + * mapped to that namespace in the current session. + *

    + * The purpose of this method is to make it easier to write + * namespace-aware code that uses names in the standard JCR namespaces. + * For example: + *

    +     *     node.getProperty(helper.getName("jcr:data"));
    +     * 
    + * + * @param name prefixed name using the standard JCR prefixes + * @return prefixed name using the current session namespace mappings + * @throws IllegalArgumentException if the prefix is unknown + * @throws RepositoryException if the namespace could not be retrieved + */ + public String getJcrName(String name) + throws IllegalArgumentException, RepositoryException { + String standardPrefix; + String currentPrefix; + + if (name.startsWith("jcr:")) { + standardPrefix = "jcr"; + currentPrefix = session.getNamespacePrefix(JCR); + } else if (name.startsWith("nt:")) { + standardPrefix = "nt"; + currentPrefix = session.getNamespacePrefix(NT); + } else if (name.startsWith("mix:")) { + standardPrefix = "mix"; + currentPrefix = session.getNamespacePrefix(MIX); + } else { + throw new IllegalArgumentException("Unknown prefix: " + name); + } + + if (currentPrefix.equals(standardPrefix)) { + return name; + } else { + return currentPrefix + name.substring(standardPrefix.length()); + } + } + + /** + * Safely registers the given namespace. If the namespace already exists, + * then the prefix mapped to the namespace in the current session is + * returned. Otherwise the namespace is registered to the namespace + * registry. If the given prefix is already registered for some other + * namespace or otherwise invalid, then another prefix is automatically + * generated. After the namespace has been registered, the prefix mapped + * to it in the current session is returned. + * + * @see NamespaceRegistry#registerNamespace(String, String) + * @param prefix namespace prefix + * @param uri namespace URI + * @return namespace prefix in the current session + * @throws RepositoryException if the namespace could not be registered + */ + public String registerNamespace(String prefix, String uri) + throws RepositoryException { + NamespaceRegistry registry = + session.getWorkspace().getNamespaceRegistry(); + try { + // Check if the namespace is registered + registry.getPrefix(uri); + } catch (NamespaceException e1) { + // Replace troublesome prefix hints + if (prefix == null || prefix.length() == 0 + || prefix.toLowerCase().startsWith("xml") + || !XMLChar.isValidNCName(prefix)) { + prefix = "ns"; // ns, ns2, ns3, ns4, ... + } + + // Loop until an unused prefix is found + try { + String base = prefix; + for (int i = 2; true; i++) { + registry.getURI(prefix); + prefix = base + i; + } + } catch (NamespaceException e2) { + // Exit the loop + } + + // Register the namespace + registry.registerNamespace(prefix, uri); + } + + return session.getNamespacePrefix(uri); + } + + /** + * Safely registers all namespaces in the given map from + * prefixes to namespace URIs. + * + * @param namespaces namespace mappings + * @throws RepositoryException if the namespaces could not be registered + */ + public void registerNamespaces(Map namespaces) throws RepositoryException { + for (Map.Entry stringStringEntry : namespaces.entrySet()) { + Map.Entry entry = stringStringEntry; + registerNamespace(entry.getKey(), entry.getValue()); + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/SimpleValueFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/SimpleValueFactory.java new file mode 100644 index 00000000000..f2fc81b28b6 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/SimpleValueFactory.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.commons; + +import javax.jcr.ValueFormatException; + +import org.apache.jackrabbit.value.AbstractValueFactory; + +/** + * Simple value factory implementation for use mainly in testing. + * Complex value types such as names, paths and references are kept + * just as strings, and no format checks nor any namespace prefix + * updates are made. + * + * @since Apache Jackrabbit 2.3 + */ +public class SimpleValueFactory extends AbstractValueFactory { + + @Override + protected void checkPathFormat(String pathValue) + throws ValueFormatException { + } + + @Override + protected void checkNameFormat(String nameValue) + throws ValueFormatException { + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CndImporter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CndImporter.java new file mode 100644 index 00000000000..2d160836224 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CndImporter.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.commons.cnd; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFactory; +import javax.jcr.Workspace; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinitionTemplate; + +import static org.apache.jackrabbit.JcrConstants.NT_BASE; + +/** + * Utility class for importing compact node type definitions. + * @see CompactNodeTypeDefReader + * @see TemplateBuilderFactory + */ +public final class CndImporter { + + private CndImporter() { + super(); + } + + /** + * Shortcut for + *
    +     *   registerNodeTypes(cnd, "cnd input stream", wsp.getNodeTypeManager(),
    +     *          wsp.getNamespaceRegistry(), session.getValueFactory(), false);
    +     * 
    + * where wsp is the workspace of the session passed. + * @see #registerNodeTypes(Reader, String, NodeTypeManager, NamespaceRegistry, ValueFactory, boolean) + * @param cnd + * @param session the session to use for registering the node types + * @return the registered node types + * + * @throws InvalidNodeTypeDefinitionException + * @throws NodeTypeExistsException + * @throws UnsupportedRepositoryOperationException + * @throws ParseException + * @throws RepositoryException + * @throws IOException + */ + public static NodeType[] registerNodeTypes(Reader cnd, Session session) + throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, + UnsupportedRepositoryOperationException, ParseException, RepositoryException, IOException { + + Workspace wsp = session.getWorkspace(); + return registerNodeTypes(cnd, "cnd input stream", wsp.getNodeTypeManager(), wsp.getNamespaceRegistry(), + session.getValueFactory(), false); + } + + /** + * Shortcut for + *
    +     *   registerNodeTypes(cnd, "cnd input stream", wsp.getNodeTypeManager(),
    +     *          wsp.getNamespaceRegistry(), session.getValueFactory(), reregisterExisting);
    +     * 
    + * where wsp is the workspace of the session passed. + * @see #registerNodeTypes(Reader, String, NodeTypeManager, NamespaceRegistry, ValueFactory, boolean) + * @param cnd + * @param session the session to use for registering the node types + * @param reregisterExisting true if existing node types should be re-registered + * with those present in the cnd. false otherwise. + * @return the registered node types + * + * @throws InvalidNodeTypeDefinitionException + * @throws NodeTypeExistsException + * @throws UnsupportedRepositoryOperationException + * @throws ParseException + * @throws RepositoryException + * @throws IOException + */ + public static NodeType[] registerNodeTypes(Reader cnd, Session session, boolean reregisterExisting) + throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, + UnsupportedRepositoryOperationException, ParseException, RepositoryException, IOException { + + Workspace wsp = session.getWorkspace(); + return registerNodeTypes(cnd, "cnd input stream", wsp.getNodeTypeManager(), wsp.getNamespaceRegistry(), + session.getValueFactory(), reregisterExisting); + } + + /** + * Registers nodetypes in cnd format. + * @param cnd a reader to the cnd. The reader is closed on return. + * @param systemId a informative id of the given cnd input. + * @param nodeTypeManager the {@link NodeTypeManager} used for creating and registering the + * {@link NodeTypeTemplate}s, {@link NodeDefinitionTemplate}s and {@link PropertyDefinitionTemplate}s + * defined in the cnd. + * @param namespaceRegistry the {@link NamespaceRegistry} used for registering namespaces defined in + * the cnd. + * @param valueFactory the {@link ValueFactory} used to create + * {@link PropertyDefinitionTemplate#setDefaultValues(javax.jcr.Value[]) default value(s)}. + * @param reregisterExisting true if existing node types should be re-registered + * with those present in the cnd. false otherwise. + * @return the registered node types + * + * @throws ParseException if the cnd cannot be parsed + * @throws InvalidNodeTypeDefinitionException if a NodeTypeDefinition is invalid. + * @throws NodeTypeExistsException if reregisterExisting is false and a + * NodeTypeDefinition specifies a node type name that is already registered. + * @throws UnsupportedRepositoryOperationException if the NodeTypeManager does not + * support node type registration. + * @throws IOException if closing the cnd reader fails + * @throws RepositoryException if another error occurs. + */ + public static NodeType[] registerNodeTypes(Reader cnd, String systemId, NodeTypeManager nodeTypeManager, + NamespaceRegistry namespaceRegistry, ValueFactory valueFactory, boolean reregisterExisting) + throws ParseException, InvalidNodeTypeDefinitionException, NodeTypeExistsException, + UnsupportedRepositoryOperationException, RepositoryException, IOException { + + try { + DefinitionBuilderFactory factory = + new TemplateBuilderFactory(nodeTypeManager, valueFactory, namespaceRegistry); + + CompactNodeTypeDefReader cndReader = + new CompactNodeTypeDefReader(cnd, systemId, factory); + + Map templates = new HashMap(); + for (NodeTypeTemplate template : cndReader.getNodeTypeDefinitions()) { + templates.put(template.getName(), template); + } + + List toRegister = new ArrayList(templates.size()); + for (NodeTypeTemplate ntt : templates.values()) { + if (reregisterExisting || !nodeTypeManager.hasNodeType(ntt.getName())) { + ensureNtBase(ntt, templates, nodeTypeManager); + toRegister.add(ntt); + } + } + NodeTypeIterator registered = nodeTypeManager.registerNodeTypes( + toRegister.toArray(new NodeTypeTemplate[toRegister.size()]), true); + return toArray(registered); + } + finally { + cnd.close(); + } + } + + private static void ensureNtBase(NodeTypeTemplate ntt, Map templates, + NodeTypeManager nodeTypeManager) throws RepositoryException { + if (!ntt.isMixin() && !NT_BASE.equals(ntt.getName())) { + String[] supertypes = ntt.getDeclaredSupertypeNames(); + if (supertypes.length == 0) { + ntt.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 std = templates.get(name); + if (std == null) { + std = nodeTypeManager.getNodeType(name); + } + if (std != null && !std.isMixin()) { + needsNtBase = false; + } + } + if (needsNtBase) { + String[] withNtBase = new String[supertypes.length + 1]; + withNtBase[0] = NT_BASE; + System.arraycopy(supertypes, 0, withNtBase, 1, supertypes.length); + ntt.setDeclaredSuperTypeNames(withNtBase); + } + } + } + } + + // -----------------------------------------------------< private >--- + + private static NodeType[] toArray(NodeTypeIterator nodeTypes) { + ArrayList nts = new ArrayList(); + + while (nodeTypes.hasNext()) { + nts.add(nodeTypes.nextNodeType()); + } + + return nts.toArray(new NodeType[nts.size()]); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CompactNodeTypeDefReader.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CompactNodeTypeDefReader.java new file mode 100644 index 00000000000..de4fdbf1307 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CompactNodeTypeDefReader.java @@ -0,0 +1,762 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.cnd; + +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory.AbstractNodeDefinitionBuilder; +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory.AbstractNodeTypeDefinitionBuilder; +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory.AbstractPropertyDefinitionBuilder; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.version.OnParentVersionAction; +import java.io.Reader; +import java.util.LinkedList; +import java.util.List; + +/** + * CompactNodeTypeDefReader. Parses node type definitions written in the compact + * node type definition format and provides a list of type definition + * objects that can then be used to register node types. + *

    + * The CompactNodeTypeDefReader is parameterizable in the type of the node type + * definition T and the type of the namespace mapping N + * which the parser should build. For types T and N the + * parser's constructor takes a {@link DefinitionBuilderFactory} for + * T and N. + *

    + * The EBNF grammar of the compact node type definition:
    + *

    + * Cnd ::= {NamespaceMapping | NodeTypeDef}
    + * NamespaceMapping ::= '<' Prefix '=' Uri '>'
    + * Prefix ::= String
    + * Uri ::= String
    + * NodeTypeDef ::= NodeTypeName [Supertypes]
    + *                 [NodeTypeAttribute {NodeTypeAttribute}]
    + *                 {PropertyDef | ChildNodeDef}
    + * NodeTypeName ::= '[' String ']'
    + * Supertypes ::= '>' (StringList | '?')
    + * NodeTypeAttribute ::= Orderable | Mixin | Abstract | Query |
    + *                       PrimaryItem
    + * Orderable ::= ('orderable' | 'ord' | 'o') ['?']
    + * Mixin ::= ('mixin' | 'mix' | 'm') ['?']
    + * Abstract ::= ('abstract' | 'abs' | 'a') ['?']
    + * Query ::= ('noquery' | 'nq') | ('query' | 'q' )
    + * PrimaryItem ::= ('primaryitem'| '!')(String | '?')
    + * PropertyDef ::= PropertyName [PropertyType] [DefaultValues]
    + *                 [PropertyAttribute {PropertyAttribute}]
    + *                 [ValueConstraints]
    + * PropertyName ::= '-' String
    + * PropertyType ::= '(' ('STRING' | 'BINARY' | 'LONG' | 'DOUBLE' |
    + *                       'BOOLEAN' | 'DATE' | 'NAME' | 'PATH' |
    + *                       'REFERENCE' | 'WEAKREFERENCE' |
    + *                       'DECIMAL' | 'URI' | 'UNDEFINED' | '*' |
    + *                       '?') ')'
    + * DefaultValues ::= '=' (StringList | '?')
    + * ValueConstraints ::= '<' (StringList | '?')
    + * ChildNodeDef ::= NodeName [RequiredTypes] [DefaultType]
    + *                  [NodeAttribute {NodeAttribute}]
    + * NodeName ::= '+' String
    + * RequiredTypes ::= '(' (StringList | '?') ')'
    + * DefaultType ::= '=' (String | '?')
    + * PropertyAttribute ::= Autocreated | Mandatory | Protected |
    + *                       Opv | Multiple | QueryOps | NoFullText |
    + *                       NoQueryOrder
    + * NodeAttribute ::= Autocreated | Mandatory | Protected |
    + *                   Opv | Sns
    + * Autocreated ::= ('autocreated' | 'aut' | 'a' )['?']
    + * Mandatory ::= ('mandatory' | 'man' | 'm') ['?']
    + * Protected ::= ('protected' | 'pro' | 'p') ['?']
    + * Opv ::= 'COPY' | 'VERSION' | 'INITIALIZE' | 'COMPUTE' |
    + *         'IGNORE' | 'ABORT' | ('OPV' '?')
    + * Multiple ::= ('multiple' | 'mul' | '*') ['?']
    + * QueryOps ::= ('queryops' | 'qop')
    + *              (('''Operator {','Operator}''') | '?')
    + * Operator ::= '=' | '<>' | '<' | '<=' | '>' | '>=' | 'LIKE'
    + * NoFullText ::= ('nofulltext' | 'nof') ['?']
    + * NoQueryOrder ::= ('noqueryorder' | 'nqord') ['?']
    + * Sns ::= ('sns' | '*') ['?']
    + * StringList ::= String {',' String}
    + * String ::= QuotedString | UnquotedString
    + * QuotedString ::= SingleQuotedString | DoubleQuotedString
    + * SingleQuotedString ::= ''' UnquotedString '''
    + * DoubleQuotedString ::= '"' UnquotedString '"'
    + * UnquotedString ::= XmlChar {XmlChar}
    + * XmlChar ::= see 3.2.2 Local Names
    + * 
    + * + * @param + * @param + */ +public class CompactNodeTypeDefReader { + + /** + * the list of parsed QNodeTypeDefinition + */ + private final List nodeTypeDefs = new LinkedList(); + + /** + * the underlying lexer + */ + private final Lexer lexer; + + /** + * the current token + */ + private String currentToken; + + /** + * The builder for QNodeTypeDefinitions + */ + private final DefinitionBuilderFactory factory; + + /** + * Creates a new CND reader and parses the given stream. + * + * @param r a reader to the CND + * @param systemId a informative id of the given stream + * @param factory builder for creating new definitions and handling namespaces + * @throws ParseException if an error occurs + */ + public CompactNodeTypeDefReader(Reader r, String systemId, + DefinitionBuilderFactory factory) throws ParseException { + + this(r, systemId, null, factory); + } + + /** + * Creates a new CND reader and parses the given stream. + * + * @param r a reader to the CND + * @param systemId a informative id of the given stream + * @param nsMapping default namespace mapping to use + * @param factory builder for creating new definitions and handling namespaces + * @throws ParseException if an error occurs + */ + public CompactNodeTypeDefReader(Reader r, String systemId, N nsMapping, + DefinitionBuilderFactory factory) throws ParseException { + + super(); + + this.factory = factory; + lexer = new Lexer(r, systemId); + if (nsMapping != null) { + factory.setNamespaceMapping(nsMapping); + } + + nextToken(); + parse(); + } + + /** + * Returns the previously assigned system id + * + * @return the system id + */ + public String getSystemId() { + return lexer.getSystemId(); + } + + /** + * Returns the list of parsed node type definitions definitions. + * + * @return a collection of node type definition objects + */ + public List getNodeTypeDefinitions() { + return nodeTypeDefs; + } + + /** + * Returns the namespace mapping. + * + * @return + */ + public N getNamespaceMapping() { + return factory.getNamespaceMapping(); + } + + /** + * Parses the definition + * + * @throws ParseException if an error during parsing occurs + */ + private void parse() throws ParseException { + while (!currentTokenEquals(Lexer.EOF)) { + if (!doNameSpace()) { + break; + } + } + try { + while (!currentTokenEquals(Lexer.EOF)) { + AbstractNodeTypeDefinitionBuilder ntd = factory.newNodeTypeDefinitionBuilder(); + ntd.setOrderableChildNodes(false); + ntd.setMixin(false); + ntd.setAbstract(false); + ntd.setQueryable(true); + doNodeTypeName(ntd); + doSuperTypes(ntd); + doOptions(ntd); + doItemDefs(ntd); + nodeTypeDefs.add(ntd.build()); + } + } catch (RepositoryException e) { + lexer.fail(e); + } + } + + + /** + * processes the namespace declaration + * + * @return true if a namespace was parsed + * @throws ParseException if an error during parsing occurs + */ + private boolean doNameSpace() throws ParseException { + if (!currentTokenEquals('<')) { + return false; + } + nextToken(); + String prefix = currentToken; + nextToken(); + if (!currentTokenEquals('=')) { + lexer.fail("Missing = in namespace decl."); + } + nextToken(); + String uri = currentToken; + nextToken(); + if (!currentTokenEquals('>')) { + lexer.fail("Missing > in namespace decl."); + } + try { + factory.setNamespace(prefix, uri); + } catch (RepositoryException e) { + lexer.fail("Error setting namespace mapping " + currentToken, e); + } + nextToken(); + return true; + } + + /** + * processes the nodetype name + * + * @param ntd nodetype definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doNodeTypeName(AbstractNodeTypeDefinitionBuilder ntd) throws ParseException { + if (!currentTokenEquals(Lexer.BEGIN_NODE_TYPE_NAME)) { + lexer.fail("Missing '" + Lexer.BEGIN_NODE_TYPE_NAME + "' delimiter for beginning of node type name"); + } + nextToken(); + try { + ntd.setName(currentToken); + } catch (RepositoryException e) { + lexer.fail("Error setting node type name " + currentToken, e); + } + + nextToken(); + if (!currentTokenEquals(Lexer.END_NODE_TYPE_NAME)) { + lexer.fail("Missing '" + Lexer.END_NODE_TYPE_NAME + "' delimiter for end of node type name, found " + currentToken); + } + nextToken(); + } + + /** + * processes the superclasses + * + * @param ntd nodetype definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doSuperTypes(AbstractNodeTypeDefinitionBuilder ntd) throws ParseException { + + if (currentTokenEquals(Lexer.EXTENDS)) + do { + nextToken(); + try { + ntd.addSupertype(currentToken); + } catch (RepositoryException e) { + lexer.fail("Error setting super type of " + ntd.getName() + " to " + currentToken, e); + } + nextToken(); + } while (currentTokenEquals(Lexer.LIST_DELIMITER)); + } + + /** + * processes the options + * + * @param ntd nodetype definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doOptions(AbstractNodeTypeDefinitionBuilder ntd) throws ParseException { + + boolean hasOption = true; + try { + while (hasOption) { + if (currentTokenEquals(Lexer.ORDERABLE)) { + nextToken(); + ntd.setOrderableChildNodes(true); + } else if (currentTokenEquals(Lexer.MIXIN)) { + nextToken(); + ntd.setMixin(true); + } else if (currentTokenEquals(Lexer.ABSTRACT)) { + nextToken(); + ntd.setAbstract(true); + } else if (currentTokenEquals(Lexer.NOQUERY)) { + nextToken(); + ntd.setQueryable(false); + } else if (currentTokenEquals(Lexer.QUERY)) { + nextToken(); + ntd.setQueryable(true); + } else if (currentTokenEquals(Lexer.PRIMARYITEM)) { + nextToken(); + ntd.setPrimaryItemName(currentToken); + nextToken(); + } else { + hasOption = false; + } + } + } catch (RepositoryException e) { + lexer.fail("Error setting option of " + ntd.getName() + " to " + currentToken, e); + } + } + + /** + * processes the item definitions + * + * @param ntd nodetype definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doItemDefs(AbstractNodeTypeDefinitionBuilder ntd) throws ParseException { + while (currentTokenEquals(Lexer.PROPERTY_DEFINITION) || currentTokenEquals(Lexer.CHILD_NODE_DEFINITION)) { + if (currentTokenEquals(Lexer.PROPERTY_DEFINITION)) { + try { + AbstractPropertyDefinitionBuilder pd = ntd.newPropertyDefinitionBuilder(); + try { + pd.setAutoCreated(false); + pd.setDeclaringNodeType(ntd.getName()); + pd.setMandatory(false); + pd.setMultiple(false); + pd.setOnParentVersion(OnParentVersionAction.COPY); + pd.setProtected(false); + pd.setRequiredType(PropertyType.STRING); + pd.setFullTextSearchable(true); + pd.setQueryOrderable(true); + } catch (RepositoryException e) { + lexer.fail("Error setting property definitions of " + pd.getName() + " to " + currentToken, e); + } + nextToken(); + doPropertyDefinition(pd, ntd); + pd.build(); + } catch (RepositoryException e) { + lexer.fail("Error building property definition for " + ntd.getName(), e); + } + + } else if (currentTokenEquals(Lexer.CHILD_NODE_DEFINITION)) { + try { + AbstractNodeDefinitionBuilder nd = ntd.newNodeDefinitionBuilder(); + try { + nd.setAllowsSameNameSiblings(false); + nd.setAutoCreated(false); + nd.setDeclaringNodeType(ntd.getName()); + nd.setMandatory(false); + nd.setOnParentVersion(OnParentVersionAction.COPY); + nd.setProtected(false); + } catch (RepositoryException e) { + lexer.fail("Error setting node definitions of " + nd.getName() + " to " + currentToken, e); + } + + nextToken(); + doChildNodeDefinition(nd, ntd); + nd.build(); + } catch (RepositoryException e) { + lexer.fail("Error building node definition for " + ntd.getName(), e); + } + } + } + } + + /** + * processes the property definition + * + * @param pd property definition builder + * @param ntd declaring nodetype definition builder + * @throws ParseException if an error during parsing occur + */ + private void doPropertyDefinition(AbstractPropertyDefinitionBuilder pd, AbstractNodeTypeDefinitionBuilder ntd) + throws ParseException { + + try { + pd.setName(currentToken); + } catch (RepositoryException e) { + lexer.fail("Invalid property name '" + currentToken, e); + } + nextToken(); + doPropertyType(pd); + doPropertyDefaultValue(pd); + doPropertyAttributes(pd, ntd); + doPropertyValueConstraints(pd); + } + + /** + * processes the property type + * + * @param pd property definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doPropertyType(AbstractPropertyDefinitionBuilder pd) throws ParseException { + + if (!currentTokenEquals(Lexer.BEGIN_TYPE)) { + return; + } + nextToken(); + try { + if (currentTokenEquals(Lexer.STRING)) { + pd.setRequiredType(PropertyType.STRING); + } else if (currentTokenEquals(Lexer.BINARY)) { + pd.setRequiredType(PropertyType.BINARY); + } else if (currentTokenEquals(Lexer.LONG)) { + pd.setRequiredType(PropertyType.LONG); + } else if (currentTokenEquals(Lexer.DECIMAL)) { + pd.setRequiredType(PropertyType.DECIMAL); + } else if (currentTokenEquals(Lexer.DOUBLE)) { + pd.setRequiredType(PropertyType.DOUBLE); + } else if (currentTokenEquals(Lexer.BOOLEAN)) { + pd.setRequiredType(PropertyType.BOOLEAN); + } else if (currentTokenEquals(Lexer.DATE)) { + pd.setRequiredType(PropertyType.DATE); + } else if (currentTokenEquals(Lexer.NAME)) { + pd.setRequiredType(PropertyType.NAME); + } else if (currentTokenEquals(Lexer.PATH)) { + pd.setRequiredType(PropertyType.PATH); + } else if (currentTokenEquals(Lexer.URI)) { + pd.setRequiredType(PropertyType.URI); + } else if (currentTokenEquals(Lexer.REFERENCE)) { + pd.setRequiredType(PropertyType.REFERENCE); + } else if (currentTokenEquals(Lexer.WEAKREFERENCE)) { + pd.setRequiredType(PropertyType.WEAKREFERENCE); + } else if (currentTokenEquals(Lexer.UNDEFINED)) { + pd.setRequiredType(PropertyType.UNDEFINED); + } else { + lexer.fail("Unkown property type '" + currentToken + "' specified"); + } + } catch (RepositoryException e) { + lexer.fail("Error setting property type of " + pd.getName() + " to " + currentToken, e); + } + nextToken(); + if (!currentTokenEquals(Lexer.END_TYPE)) { + lexer.fail("Missing '" + Lexer.END_TYPE + "' delimiter for end of property type"); + } + nextToken(); + } + + /** + * processes the property attributes + * + * @param pd property definition builder + * @param ntd declaring nodetype definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doPropertyAttributes(AbstractPropertyDefinitionBuilder pd, + AbstractNodeTypeDefinitionBuilder ntd) throws ParseException { + + try { + while (currentTokenEquals(Lexer.PROP_ATTRIBUTE)) { + if (currentTokenEquals(Lexer.PRIMARY)) { + ntd.setPrimaryItemName(pd.getName()); + } else if (currentTokenEquals(Lexer.AUTOCREATED)) { + pd.setAutoCreated(true); + } else if (currentTokenEquals(Lexer.MANDATORY)) { + pd.setMandatory(true); + } else if (currentTokenEquals(Lexer.PROTECTED)) { + pd.setProtected(true); + } else if (currentTokenEquals(Lexer.MULTIPLE)) { + pd.setMultiple(true); + } else if (currentTokenEquals(Lexer.COPY)) { + pd.setOnParentVersion(OnParentVersionAction.COPY); + } else if (currentTokenEquals(Lexer.VERSION)) { + pd.setOnParentVersion(OnParentVersionAction.VERSION); + } else if (currentTokenEquals(Lexer.INITIALIZE)) { + pd.setOnParentVersion(OnParentVersionAction.INITIALIZE); + } else if (currentTokenEquals(Lexer.COMPUTE)) { + pd.setOnParentVersion(OnParentVersionAction.COMPUTE); + } else if (currentTokenEquals(Lexer.IGNORE)) { + pd.setOnParentVersion(OnParentVersionAction.IGNORE); + } else if (currentTokenEquals(Lexer.ABORT)) { + pd.setOnParentVersion(OnParentVersionAction.ABORT); + } else if (currentTokenEquals(Lexer.NOFULLTEXT)) { + pd.setFullTextSearchable(false); + } else if (currentTokenEquals(Lexer.NOQUERYORDER)) { + pd.setQueryOrderable(false); + } else if (currentTokenEquals(Lexer.QUERYOPS)) { + doPropertyQueryOperators(pd); + } + nextToken(); + } + } catch (RepositoryException e) { + lexer.fail("Error setting property attribute of " + pd.getName() + " to " + currentToken, e); + } + } + + /** + * processes the property query operators + * + * @param pd the property definition builder + * @throws ParseException if an error occurs + */ + private void doPropertyQueryOperators(AbstractPropertyDefinitionBuilder pd) + throws ParseException { + if (!currentTokenEquals(Lexer.QUERYOPS)) { + return; + } + nextToken(); + + String[] ops = currentToken.split(","); + List queryOps = new LinkedList(); + for (String op : ops) { + String s = op.trim(); + if (s.equals(Lexer.QUEROPS_EQUAL)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO); + } else if (s.equals(Lexer.QUEROPS_NOTEQUAL)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO); + } else if (s.equals(Lexer.QUEROPS_LESSTHAN)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN); + } else if (s.equals(Lexer.QUEROPS_LESSTHANOREQUAL)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO); + } else if (s.equals(Lexer.QUEROPS_GREATERTHAN)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN); + } else if (s.equals(Lexer.QUEROPS_GREATERTHANOREQUAL)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO); + } else if (s.equals(Lexer.QUEROPS_LIKE)) { + queryOps.add(QueryObjectModelConstants.JCR_OPERATOR_LIKE); + } else { + lexer.fail("'" + s + "' is not a valid query operator"); + } + } + try { + pd.setAvailableQueryOperators(queryOps.toArray(new String[queryOps.size()])); + } catch (RepositoryException e) { + lexer.fail("Error query operators for " + pd.getName() + " to " + currentToken, e); + } + } + + /** + * processes the property default values + * + * @param pd property definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doPropertyDefaultValue(AbstractPropertyDefinitionBuilder pd) + throws ParseException { + + if (!currentTokenEquals(Lexer.DEFAULT)) { + return; + } + + do { + nextToken(); + try { + pd.addDefaultValues(currentToken); + } catch (RepositoryException e) { + lexer.fail("Error adding default value for " + pd.getName() + " to " + currentToken, e); + } + nextToken(); + } while (currentTokenEquals(Lexer.LIST_DELIMITER)); + } + + /** + * processes the property value constraints + * + * @param pd property definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doPropertyValueConstraints(AbstractPropertyDefinitionBuilder pd) + throws ParseException { + + if (!currentTokenEquals(Lexer.CONSTRAINT)) { + return; + } + + do { + nextToken(); + try { + pd.addValueConstraint(currentToken); + } catch (RepositoryException e) { + lexer.fail("Error adding value constraint for " + pd.getName() + " to " + currentToken, e); + } + nextToken(); + } while (currentTokenEquals(Lexer.LIST_DELIMITER)); + } + + /** + * processes the childnode definition + * + * @param nd node definition builder + * @param ntd declaring nodetype definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doChildNodeDefinition(AbstractNodeDefinitionBuilder nd, + AbstractNodeTypeDefinitionBuilder ntd) + throws ParseException { + + try { + nd.setName(currentToken); + } catch (RepositoryException e) { + lexer.fail("Invalid child node name '" + currentToken, e); + } + nextToken(); + doChildNodeRequiredTypes(nd); + doChildNodeDefaultType(nd); + doChildNodeAttributes(nd, ntd); + } + + /** + * processes the childnode required types + * + * @param nd node definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doChildNodeRequiredTypes(AbstractNodeDefinitionBuilder nd) + throws ParseException { + + if (!currentTokenEquals(Lexer.BEGIN_TYPE)) { + return; + } + + do { + nextToken(); + try { + nd.addRequiredPrimaryType(currentToken); + } catch (RepositoryException e) { + lexer.fail("Error setting required primary type of " + nd.getName() + " to " + currentToken, e); + } + nextToken(); + } while (currentTokenEquals(Lexer.LIST_DELIMITER)); + nextToken(); + } + + /** + * processes the childnode default types + * + * @param nd node definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doChildNodeDefaultType(AbstractNodeDefinitionBuilder nd) + throws ParseException { + + if (!currentTokenEquals(Lexer.DEFAULT)) { + return; + } + nextToken(); + try { + nd.setDefaultPrimaryType(currentToken); + } catch (RepositoryException e) { + lexer.fail("Error setting default primary type of " + nd.getName() + " to " + currentToken, e); + } + nextToken(); + } + + /** + * processes the childnode attributes + * + * @param nd node definition builder + * @param ntd declaring nodetype definition builder + * @throws ParseException if an error during parsing occurs + */ + private void doChildNodeAttributes(AbstractNodeDefinitionBuilder nd, + AbstractNodeTypeDefinitionBuilder ntd) + throws ParseException { + + try { + while (currentTokenEquals(Lexer.NODE_ATTRIBUTE)) { + if (currentTokenEquals(Lexer.PRIMARY)) { + ntd.setPrimaryItemName(nd.getName()); + } else if (currentTokenEquals(Lexer.AUTOCREATED)) { + nd.setAutoCreated(true); + } else if (currentTokenEquals(Lexer.MANDATORY)) { + nd.setMandatory(true); + } else if (currentTokenEquals(Lexer.PROTECTED)) { + nd.setProtected(true); + } else if (currentTokenEquals(Lexer.SNS)) { + nd.setAllowsSameNameSiblings(true); + } else if (currentTokenEquals(Lexer.COPY)) { + nd.setOnParentVersion(OnParentVersionAction.COPY); + } else if (currentTokenEquals(Lexer.VERSION)) { + nd.setOnParentVersion(OnParentVersionAction.VERSION); + } else if (currentTokenEquals(Lexer.INITIALIZE)) { + nd.setOnParentVersion(OnParentVersionAction.INITIALIZE); + } else if (currentTokenEquals(Lexer.COMPUTE)) { + nd.setOnParentVersion(OnParentVersionAction.COMPUTE); + } else if (currentTokenEquals(Lexer.IGNORE)) { + nd.setOnParentVersion(OnParentVersionAction.IGNORE); + } else if (currentTokenEquals(Lexer.ABORT)) { + nd.setOnParentVersion(OnParentVersionAction.ABORT); + } + nextToken(); + } + } catch (RepositoryException e) { + lexer.fail("Error setting child node attribute of " + nd.getName() + " to " + currentToken, e); + } + } + + /** + * Gets the next token from the underlying lexer. + * + * @throws ParseException if the lexer fails to get the next token. + * @see Lexer#getNextToken() + */ + private void nextToken() throws ParseException { + currentToken = lexer.getNextToken(); + } + + /** + * Checks if the {@link #currentToken} is semantically equal to the given + * argument ignoring the case. + * + * @param s the tokens to compare with + * @return true if equals; false otherwise. + */ + private boolean currentTokenEquals(String[] s) { + for (String value : s) { + if (currentToken.equalsIgnoreCase(value)) { + return true; + } + } + return false; + } + + /** + * Checks if the {@link #currentToken} is semantically equal to the given + * argument. + * + * @param c the tokens to compare with + * @return true if equals; false otherwise. + */ + private boolean currentTokenEquals(char c) { + return currentToken.length() == 1 && currentToken.charAt(0) == c; + } + + /** + * Checks if the {@link #currentToken} is semantically equal to the given + * argument. + * + * @param s the tokens to compare with + * @return true if equals; false otherwise. + */ + private boolean currentTokenEquals(String s) { + return currentToken.equals(s); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CompactNodeTypeDefWriter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CompactNodeTypeDefWriter.java new file mode 100644 index 00000000000..c01c6477c72 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/CompactNodeTypeDefWriter.java @@ -0,0 +1,555 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.cnd; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.version.OnParentVersionAction; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Prints node type defs in a compact notation + * Print Format: + * <ex = "http://apache.org/jackrabbit/example"> + * [ex:NodeType] > ex:ParentType1, ex:ParentType2 + * orderable mixin + * - ex:property (STRING) = 'default1', 'default2' + * primary mandatory autocreated protected multiple VERSION + * < 'constraint1', 'constraint2' + * + ex:node (ex:reqType1, ex:reqType2) = ex:defaultType + * mandatory autocreated protected multiple VERSION + */ +public class CompactNodeTypeDefWriter { + + /** + * the indention string + */ + private static final String INDENT = " "; + + private static final String ANY = "*"; + + /** + * Helper to retrieve the namespace URI for a given prefix. + */ + private final NamespaceMapping nsMapping; + + /** + * the underlying writer + */ + private Writer out; + + /** + * special writer used for namespaces + */ + private Writer nsWriter; + + /** + * namespaces(prefixes) that are used + */ + private final Set usedNamespaces = new HashSet(); + + /** + * Creates a new nodetype writer based on a session + * + * @param out the underlying writer + * @param session repository session + * @param includeNS if true all used namespace decl. are also + * written to the writer + */ + public CompactNodeTypeDefWriter(Writer out, final Session session, boolean includeNS) { + this(out, new DefaultNamespaceMapping(session), includeNS); + } + + /** + * Creates a new nodetype writer based on a session + * + * @param out the underlying writer + * @param nsMapping the mapping from prefix to namespace URI. + * @param includeNS if true all used namespace decl. are also + * written to the writer + */ + public CompactNodeTypeDefWriter(Writer out, NamespaceMapping nsMapping, boolean includeNS) { + this.nsMapping = nsMapping; + if (includeNS) { + this.out = new StringWriter(); + this.nsWriter = out; + } else { + this.out = out; + this.nsWriter = null; + } + } + + /** + * Writes the given list of QNodeTypeDefinition to the output writer including the + * used namespaces. + * + * @param defs collection of definitions + * @param session session + * @param out output writer + * @throws java.io.IOException if an I/O error occurs + */ + public static void write(Collection defs, + Session session, Writer out) throws IOException { + CompactNodeTypeDefWriter w = new CompactNodeTypeDefWriter(out, session, true); + for (NodeTypeDefinition def : defs) { + w.write(def); + } + w.close(); + } + + /** + * Writes the given list of QNodeTypeDefinition to the output writer + * including the used namespaces. + * + * @param defs collection of definitions + * @param nsMapping the mapping from prefix to namespace URI. + * @param out output writer + * @throws java.io.IOException if an I/O error occurs + */ + public static void write(Collection defs, + NamespaceMapping nsMapping, Writer out) throws IOException { + CompactNodeTypeDefWriter w = new CompactNodeTypeDefWriter(out, nsMapping, true); + for (NodeTypeDefinition def : defs) { + w.write(def); + } + w.close(); + } + + /** + * Write one NodeTypeDefinition to this writer + * + * @param ntd node type definition + * @throws IOException if an I/O error occurs + */ + public void write(NodeTypeDefinition ntd) throws IOException { + writeName(ntd); + writeSupertypes(ntd); + writeOptions(ntd); + PropertyDefinition[] pdefs = ntd.getDeclaredPropertyDefinitions(); + if (pdefs != null) { + for (PropertyDefinition pd : pdefs) { + writePropDef(pd); + } + } + NodeDefinition[] ndefs = ntd.getDeclaredChildNodeDefinitions(); + if (ndefs != null) { + for (NodeDefinition nd : ndefs) { + writeNodeDef(nd); + } + } + out.write("\n\n"); + } + + /** + * Write a namespace declaration to this writer. Note, that this method + * has no effect if there is no extra namespace write present. + * + * @param prefix namespace prefix + * @throws IOException if an I/O error occurs + */ + public void writeNamespaceDeclaration(String prefix) throws IOException { + if (nsWriter != null && !usedNamespaces.contains(prefix)) { + usedNamespaces.add(prefix); + nsWriter.write("<'"); + nsWriter.write(prefix); + nsWriter.write("'='"); + nsWriter.write(escape(nsMapping.getNamespaceURI(prefix))); + nsWriter.write("'>\n"); + } + } + + /** + * Flushes all pending write operations and Closes this writer. please note, + * that the underlying writer remains open. + * + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + if (nsWriter != null) { + nsWriter.write("\n"); + out.close(); + nsWriter.write(((StringWriter) out).getBuffer().toString()); + out = nsWriter; + nsWriter = null; + } + out.flush(); + out = null; + } + + //------------------------------------------------------------< private >--- + /** + * write name + * @param ntd node type definition + * @throws IOException if an I/O error occurs + */ + private void writeName(NodeTypeDefinition ntd) throws IOException { + out.write(Lexer.BEGIN_NODE_TYPE_NAME); + writeJcrName(ntd.getName()); + out.write(Lexer.END_NODE_TYPE_NAME); + } + + /** + * Write the super type definitions. + * + * @param ntd node type definition + * @throws IOException if an I/O error occurs + */ + private void writeSupertypes(NodeTypeDefinition ntd) throws IOException { + // get ordered list of supertypes, omitting nt:base + TreeSet supertypes = new TreeSet(); + for (String name : ntd.getDeclaredSupertypeNames()) { + if (!name.equals(JcrConstants.NT_BASE)) { + supertypes.add(name); + } + } + if (!supertypes.isEmpty()) { + String delim = " " + Lexer.EXTENDS + " "; + for (String name : supertypes) { + out.write(delim); + writeJcrName(name); + delim = ", "; + } + } + } + + /** + * Write the options + * + * @param ntd node type definition + * @throws IOException if an I/O error occurs + */ + private void writeOptions(NodeTypeDefinition ntd) throws IOException { + List options = new LinkedList(); + if (ntd.isAbstract()) { + options.add(Lexer.ABSTRACT[0]); + } + if (ntd.hasOrderableChildNodes()) { + options.add(Lexer.ORDERABLE[0]); + } + if (ntd.isMixin()) { + options.add(Lexer.MIXIN[0]); + } + if (!ntd.isQueryable()) { + options.add(Lexer.NOQUERY[0]); + } + String pin = ntd.getPrimaryItemName(); + if (pin != null) { + options.add(Lexer.PRIMARYITEM[0]); + } + for (int i = 0; i < options.size(); i++) { + if (i == 0) { + out.write("\n" + INDENT); + } else { + out.write(" "); + } + out.write(options.get(i)); + } + + if (pin != null) { + out.write(" "); + writeJcrName(pin); + } + } + + /** + * Write a property definition + * + * @param pd property definition + * @throws IOException if an I/O error occurs + */ + private void writePropDef(PropertyDefinition pd) throws IOException { + out.write("\n" + INDENT + Lexer.PROPERTY_DEFINITION + " "); + + writeJcrName(pd.getName()); + out.write(" "); + out.write(Lexer.BEGIN_TYPE); + out.write(PropertyType.nameFromValue(pd.getRequiredType()).toLowerCase()); + out.write(Lexer.END_TYPE); + writeDefaultValues(pd.getDefaultValues()); + if (pd.isMandatory()) { + out.write(" "); + out.write(Lexer.MANDATORY[0]); + } + if (pd.isAutoCreated()) { + out.write(" "); + out.write(Lexer.AUTOCREATED[0]); + } + if (pd.isProtected()) { + out.write(" "); + out.write(Lexer.PROTECTED[0]); + } + if (pd.isMultiple()) { + out.write(" "); + out.write(Lexer.MULTIPLE[0]); + } + if (pd.getOnParentVersion() != OnParentVersionAction.COPY) { + out.write(" "); + out.write(OnParentVersionAction.nameFromValue(pd.getOnParentVersion()).toLowerCase()); + } + if (!pd.isFullTextSearchable()) { + out.write(" "); + out.write(Lexer.NOFULLTEXT[0]); + } + if (!pd.isQueryOrderable()) { + out.write(" "); + out.write(Lexer.NOQUERYORDER[0]); + } + String[] qops = pd.getAvailableQueryOperators(); + if (qops != null && qops.length > 0) { + List opts = new ArrayList(Arrays.asList(qops)); + List defaultOps = Arrays.asList(Operator.getAllQueryOperators()); + if (!opts.containsAll(defaultOps)) { + out.write(" "); + out.write(Lexer.QUERYOPS[0]); + out.write(" '"); + String delim = ""; + for (String opt: opts) { + out.write(delim); + delim= ", "; + if (opt.equals(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO)) { + out.write(Lexer.QUEROPS_EQUAL); + } else if (opt.equals(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO)) { + out.write(Lexer.QUEROPS_NOTEQUAL); + } else if (opt.equals(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN)) { + out.write(Lexer.QUEROPS_GREATERTHAN); + } else if (opt.equals(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO)) { + out.write(Lexer.QUEROPS_GREATERTHANOREQUAL); + } else if (opt.equals(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN)) { + out.write(Lexer.QUEROPS_LESSTHAN); + } else if (opt.equals(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO)) { + out.write(Lexer.QUEROPS_LESSTHANOREQUAL); + } else if (opt.equals(QueryObjectModelConstants.JCR_OPERATOR_LIKE)) { + out.write(Lexer.QUEROPS_LIKE); + } + } + out.write("'"); + } + } + writeValueConstraints(pd.getValueConstraints(), pd.getRequiredType()); + } + + /** + * Write default values + * + * @param dva default value + * @throws IOException if an I/O error occurs + */ + private void writeDefaultValues(Value[] dva) throws IOException { + if (dva != null && dva.length > 0) { + String delim = " = '"; + for (Value value : dva) { + out.write(delim); + try { + out.write(escape(value.getString())); + } catch (RepositoryException e) { + out.write(escape(value.toString())); + } + out.write("'"); + delim = ", '"; + } + } + } + + /** + * Write the value constraints + * + * @param constraints the value constraints + * @param type value type + * @throws IOException if an I/O error occurs + */ + private void writeValueConstraints(String[] constraints, int type) throws IOException { + if (constraints != null && constraints.length > 0) { + out.write(" "); + out.write(Lexer.CONSTRAINT); + out.write(" '"); + out.write(escape(constraints[0])); + out.write("'"); + for (int i = 1; i < constraints.length; i++) { + out.write(", '"); + out.write(escape(constraints[i])); + out.write("'"); + } + } + } + + /** + * Write a child node definition + * + * @param nd node definition + * @throws IOException if an I/O error occurs + */ + private void writeNodeDef(NodeDefinition nd) throws IOException { + out.write("\n" + INDENT + Lexer.CHILD_NODE_DEFINITION + " "); + + writeJcrName(nd.getName()); + writeRequiredTypes(nd.getRequiredPrimaryTypeNames()); + writeDefaultType(nd.getDefaultPrimaryTypeName()); + if (nd.isMandatory()) { + out.write(" "); + out.write(Lexer.MANDATORY[0]); + } + if (nd.isAutoCreated()) { + out.write(" "); + out.write(Lexer.AUTOCREATED[0]); + } + if (nd.isProtected()) { + out.write(" "); + out.write(Lexer.PROTECTED[0]); + } + if (nd.allowsSameNameSiblings()) { + out.write(" "); + out.write(Lexer.MULTIPLE[0]); + } + if (nd.getOnParentVersion() != OnParentVersionAction.COPY) { + out.write(" "); + out.write(OnParentVersionAction.nameFromValue(nd.getOnParentVersion()).toLowerCase()); + } + } + + /** + * write required types + * @param reqTypes required type names + * @throws IOException if an I/O error occurs + */ + private void writeRequiredTypes(String[] reqTypes) throws IOException { + if (reqTypes != null && reqTypes.length > 0) { + String delim = " " + Lexer.BEGIN_TYPE; + for (String reqType : reqTypes) { + out.write(delim); + writeJcrName(reqType); + delim = ", "; + } + out.write(Lexer.END_TYPE); + } + } + + /** + * write default types + * @param defType default type name + * @throws IOException if an I/O error occurs + */ + private void writeDefaultType(String defType) throws IOException { + if (defType != null && !defType.equals("*")) { + out.write(" = "); + writeJcrName(defType); + } + } + + /** + * Write the name and updated the namespace declarations if needed. + * + * @param name name to write + * @throws IOException if an I/O error occurs + */ + private void writeJcrName(String name) throws IOException { + if (name == null) { + return; + } + + String prefix = Text.getNamespacePrefix(name); + if (!prefix.equals(NamespaceRegistry.PREFIX_EMPTY)) { + // update namespace declaration + writeNamespaceDeclaration(prefix); + prefix += ":"; + } + + String localName = Text.getLocalName(name); + String encLocalName = (ANY.equals(localName)) ? ANY : ISO9075.encode(Text.getLocalName(name)); + String resolvedName = prefix + encLocalName; + + // check for '-' and '+' + boolean quotesNeeded = (name.indexOf('-') >= 0 || name.indexOf('+') >= 0); + if (quotesNeeded) { + out.write("'"); + out.write(resolvedName); + out.write("'"); + } else { + out.write(resolvedName); + } + } + + /** + * escape + * @param s string + * @return the escaped string + */ + private String escape(String s) { + StringBuffer sb = new StringBuffer(s); + for (int i = 0; i < sb.length(); i++) { + if (sb.charAt(i) == '\\') { + sb.insert(i, '\\'); + i++; + } else if (sb.charAt(i) == '\'') { + sb.insert(i, '\''); + i++; + } + } + return sb.toString(); + } + + /** + * Map namespace prefixes such as present in a qualified JCR name to + * the corresponding namespace URI. + */ + public interface NamespaceMapping { + + String getNamespaceURI(String prefix); + } + + /** + * Default implementation using Session to determine + * the namespace URI. + */ + private static class DefaultNamespaceMapping implements NamespaceMapping { + + private final Session session; + + private DefaultNamespaceMapping(Session session) { + this.session = session; + } + + public String getNamespaceURI(String prefix) { + try { + return session.getNamespaceURI(prefix); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/DefinitionBuilderFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/DefinitionBuilderFactory.java new file mode 100644 index 00000000000..0a4e8ddfc14 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/DefinitionBuilderFactory.java @@ -0,0 +1,405 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.cnd; + +import javax.jcr.RepositoryException; +import javax.jcr.PropertyType; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.PropertyDefinition; + +/** + * Builder for node type definitions, node definitions and property definitions. + * @param type of the node type definition + * @param type of the namespace mapping + */ +public abstract class DefinitionBuilderFactory { + + /** + * Create a new instance of a {@link AbstractNodeTypeDefinitionBuilder} + * @return + * @throws RepositoryException + */ + public abstract AbstractNodeTypeDefinitionBuilder newNodeTypeDefinitionBuilder() + throws RepositoryException; + + /** + * Set the namespace mapping to use for the node type definition being built + * @param nsMapping + */ + public abstract void setNamespaceMapping(N nsMapping); + + /** + * @return the namespace mapping used for the node type definition being built + */ + public abstract N getNamespaceMapping(); + + /** + * Add a mapping to the namespace map + * @param prefix + * @param uri + * @throws RepositoryException + */ + public abstract void setNamespace(String prefix, String uri) throws RepositoryException; + + /** + * Builder for a node type definition of type T. + * @param + */ + public static abstract class AbstractNodeTypeDefinitionBuilder { + + /** See {@link #setName(String)} */ + protected String name; + + /** See {@link #setMixin(boolean)} */ + protected boolean isMixin; + + /** See {@link #setOrderableChildNodes(boolean)} */ + protected boolean isOrderable; + + /** See {@link #setAbstract(boolean)} */ + protected boolean isAbstract; + + /** See {@link #setQueryable(boolean)} */ + protected boolean queryable; + + /** + * Set the name of the node type definition being built + * @param name + * @throws RepositoryException if the name is not valid + * @see NodeTypeDefinition#getName() + */ + public void setName(String name) throws RepositoryException { + this.name = name; + } + + /** + * Returns the name of the node type definition being built + * @return + */ + public String getName() { + return name; + } + + /** + * Add the given name to the set of supertypes of the node type definition + * being built + * @param name name of the the supertype + * @throws RepositoryException if the name is not valid + * @see NodeTypeDefinition#getDeclaredSupertypeNames() + */ + public abstract void addSupertype(String name) throws RepositoryException; + + /** + * @param isMixin true if building a mixin node type + * definition; false otherwise. + * @throws RepositoryException + * @see NodeTypeDefinition#isMixin() + */ + public void setMixin(boolean isMixin) throws RepositoryException { + this.isMixin = isMixin; + } + + /** + * @param isOrderable true if building a node type having + * orderable child nodes; false otherwise. + * @throws RepositoryException + * @see NodeTypeDefinition#hasOrderableChildNodes() + */ + public void setOrderableChildNodes(boolean isOrderable) throws RepositoryException { + this.isOrderable = isOrderable; + } + + /** + * @param name the name of the primary item. + * @throws RepositoryException + * @see NodeTypeDefinition#getPrimaryItemName() + */ + public abstract void setPrimaryItemName(String name) throws RepositoryException; + + /** + * @param isAbstract true if building a node type that is abstract. + * @throws RepositoryException + * @see NodeTypeDefinition#isAbstract() + */ + public void setAbstract(boolean isAbstract) throws RepositoryException { + this.isAbstract = isAbstract; + } + + /** + * @param queryable true if building a node type that is queryable + * @throws RepositoryException + * @see NodeTypeDefinition#isQueryable() + */ + public void setQueryable(boolean queryable) throws RepositoryException { + this.queryable = queryable; + } + + /** + * Create a new instance of a {@link DefinitionBuilderFactory.AbstractPropertyDefinitionBuilder} + * which can be used to add property definitions to the node type definition being built. + * @return + * @throws RepositoryException + */ + public abstract AbstractPropertyDefinitionBuilder newPropertyDefinitionBuilder() + throws RepositoryException; + + /** + * Create a new instance fo a {@link DefinitionBuilderFactory.AbstractNodeDefinitionBuilder} + * which can be used to add child node definitions to the node type definition being built. + * @return + * @throws RepositoryException + */ + public abstract AbstractNodeDefinitionBuilder newNodeDefinitionBuilder() throws RepositoryException; + + /** + * Build this node type definition + * @return + * @throws RepositoryException + */ + public abstract T build() throws RepositoryException; + } + + /** + * Builder for item definitions of type T + * @param + */ + public static abstract class AbstractItemDefinitionBuilder { + + /** See {@link #setName(String)} */ + protected String name; + + /** See {@link #setAutoCreated(boolean)} */ + protected boolean autocreate; + + /** See {@link #setOnParentVersion(int)} */ + protected int onParent; + + /** See {@link #setProtected(boolean)} */ + protected boolean isProtected; + + /** See {@link #setMandatory(boolean)} */ + protected boolean isMandatory; + + /** + * @param name the name of the child item definition being build + * @throws RepositoryException + * @see ItemDefinition#getName() + */ + public void setName(String name) throws RepositoryException { + this.name = name; + } + + /** + * Name of the child item definition being built + * @return + */ + public String getName() { + return name; + } + + /** + * @param name the name of the declaring node type. + * @throws RepositoryException + * @see ItemDefinition#getDeclaringNodeType() + */ + public abstract void setDeclaringNodeType(String name) throws RepositoryException; + + /** + * @param autocreate true if building a 'autocreate' child item + * definition, false otherwise. + * @throws RepositoryException + * @see ItemDefinition#isAutoCreated() + */ + public void setAutoCreated(boolean autocreate) throws RepositoryException { + this.autocreate = autocreate; + } + + /** + * @param onParent the 'onParentVersion' attribute of the child item definition being built + * @throws RepositoryException + * @see ItemDefinition#getOnParentVersion() + */ + public void setOnParentVersion(int onParent) throws RepositoryException { + this.onParent = onParent; + } + + /** + * @param isProtected true if building a 'protected' child + * item definition, false otherwise. + * @throws RepositoryException + * @see ItemDefinition#isProtected() + */ + public void setProtected(boolean isProtected) throws RepositoryException { + this.isProtected = isProtected; + } + + /** + * @param isMandatory true if building a 'mandatory' child + * item definition, false otherwise. + * @throws RepositoryException + */ + public void setMandatory(boolean isMandatory) throws RepositoryException { + this.isMandatory = isMandatory; + } + + /** + * Build this item definition an add it to its parent node type definition + * @throws RepositoryException + */ + public abstract void build() throws RepositoryException; + } + + /** + * Builder for property definitions of type T + * @param + */ + public static abstract class AbstractPropertyDefinitionBuilder extends AbstractItemDefinitionBuilder { + + private static final String[] ALL_OPERATORS = new String[]{ + QueryObjectModelConstants.JCR_OPERATOR_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, + QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO + }; + + /** See {@link #setRequiredType(int)} */ + protected int requiredType = PropertyType.UNDEFINED; + + /** See {@link #setMultiple(boolean)} */ + protected boolean isMultiple = false; + + /** See {@link #setFullTextSearchable(boolean)} */ + protected boolean fullTextSearchable = true; + + /** See {@link #setQueryOrderable(boolean)} */ + protected boolean queryOrderable = true; + + /** See {@link #setAvailableQueryOperators(String[])} */ + protected String[] queryOperators = ALL_OPERATORS; + + /** + * @param type the required type of the property definition being built. + * @throws RepositoryException + * @see PropertyDefinition#getRequiredType() + */ + public void setRequiredType(int type) throws RepositoryException { + this.requiredType = type; + } + + /** + * The required type of the property definition being built. + * @return + */ + public int getRequiredType() { + return requiredType; + } + + /** + * @param constraint add a value constraint to the list of value constraints of the property + * definition being built. + * @throws RepositoryException + * @see PropertyDefinition#getValueConstraints() + */ + public abstract void addValueConstraint(String constraint) throws RepositoryException; + + /** + * @param value add a default value to the list of default values of the property definition + * being built. + * @throws RepositoryException + * @see PropertyDefinition#getDefaultValues() + */ + public abstract void addDefaultValues(String value) throws RepositoryException; + + /** + * @param isMultiple true if building a 'multiple' property definition. + * @throws RepositoryException + * @see PropertyDefinition#isMultiple() + */ + public void setMultiple(boolean isMultiple) throws RepositoryException { + this.isMultiple = isMultiple; + } + + /** + * @param fullTextSearchable true if building a + * 'fulltext searchable' property definition + * @throws RepositoryException + * @see PropertyDefinition#isFullTextSearchable() + */ + public void setFullTextSearchable(boolean fullTextSearchable) throws RepositoryException { + this.fullTextSearchable = fullTextSearchable; + } + + /** + * @param queryOrderable true if the property is orderable in a query + * @throws RepositoryException + * @see PropertyDefinition#isQueryOrderable() + */ + public void setQueryOrderable(boolean queryOrderable) throws RepositoryException { + this.queryOrderable = queryOrderable; + } + + /** + * @param queryOperators the query operators of the property + * @throws RepositoryException + * @see PropertyDefinition#getAvailableQueryOperators() + */ + public void setAvailableQueryOperators(String[] queryOperators) throws RepositoryException { + if (queryOperators == null) { + throw new NullPointerException("queryOperators"); + } + this.queryOperators = queryOperators; + } + } + + /** + * Builder for child node definitions of type T + * @param + */ + public static abstract class AbstractNodeDefinitionBuilder extends AbstractItemDefinitionBuilder { + protected boolean allowSns; + + /** + * @param name the name of the default primary type of the node definition being built. + * @throws RepositoryException + */ + public abstract void setDefaultPrimaryType(String name) throws RepositoryException; + + /** + * @param name add a required primary type to the list of names of the required primary types of + * the node definition being built. + * @throws RepositoryException + */ + public abstract void addRequiredPrimaryType(String name) throws RepositoryException; + + /** + * @param allowSns true if building a node definition with same name siblings, false otherwise. + * @throws RepositoryException + */ + public void setAllowsSameNameSiblings(boolean allowSns) throws RepositoryException { + this.allowSns = allowSns; + } + + } + +} + + diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/Lexer.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/Lexer.java new file mode 100644 index 00000000000..8fcbec9ec08 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/Lexer.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.commons.cnd; + +import java.io.IOException; +import java.io.Reader; +import java.io.StreamTokenizer; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Lexer of the CND definition. + */ +public class Lexer { + public static final char SINGLE_QUOTE = '\''; + public static final char DOUBLE_QUOTE = '\"'; + public static final char BEGIN_NODE_TYPE_NAME = '['; + public static final char END_NODE_TYPE_NAME = ']'; + public static final char EXTENDS = '>'; + public static final char LIST_DELIMITER = ','; + public static final char PROPERTY_DEFINITION = '-'; + public static final char CHILD_NODE_DEFINITION = '+'; + public static final char BEGIN_TYPE = '('; + public static final char END_TYPE = ')'; + public static final char DEFAULT = '='; + public static final char CONSTRAINT = '<'; + + public static final String[] ORDERABLE = new String[] {"orderable", "ord", "o"}; + public static final String[] MIXIN = new String[]{"mixin", "mix", "m"}; + public static final String[] ABSTRACT = new String[]{"abstract", "abs", "a"}; + public static final String[] NOQUERY = new String[]{"noquery", "nq"}; + public static final String[] QUERY = new String[]{"query", "q"}; + public static final String[] PRIMARYITEM = new String[]{"primaryitem", "!"}; + + public static final String[] PRIMARY = new String[]{"primary", "pri", "!"}; + public static final String[] AUTOCREATED = new String[]{"autocreated", "aut", "a"}; + public static final String[] MANDATORY = new String[]{"mandatory", "man", "m"}; + public static final String[] PROTECTED = new String[]{"protected", "pro", "p"}; + public static final String[] MULTIPLE = new String[]{"multiple", "mul", "*"}; + public static final String[] SNS = new String[]{"sns", "*", "multiple"}; + public static final String[] QUERYOPS = new String[]{"queryops", "qop"}; + public static final String[] NOFULLTEXT = new String[]{"nofulltext", "nof"}; + public static final String[] NOQUERYORDER = new String[]{"noqueryorder", "nqord"}; + + public static final String[] COPY = new String[]{"COPY"}; + public static final String[] VERSION = new String[]{"VERSION"}; + public static final String[] INITIALIZE = new String[]{"INITIALIZE"}; + public static final String[] COMPUTE = new String[]{"COMPUTE"}; + public static final String[] IGNORE = new String[]{"IGNORE"}; + public static final String[] ABORT = new String[]{"ABORT"}; + + public static final String[] PROP_ATTRIBUTE; + public static final String[] NODE_ATTRIBUTE; + static { + ArrayList attr = new ArrayList(); + attr.addAll(Arrays.asList(PRIMARY)); + attr.addAll(Arrays.asList(AUTOCREATED)); + attr.addAll(Arrays.asList(MANDATORY)); + attr.addAll(Arrays.asList(PROTECTED)); + attr.addAll(Arrays.asList(MULTIPLE)); + attr.addAll(Arrays.asList(QUERYOPS)); + attr.addAll(Arrays.asList(NOFULLTEXT)); + attr.addAll(Arrays.asList(NOQUERYORDER)); + attr.addAll(Arrays.asList(COPY)); + attr.addAll(Arrays.asList(VERSION)); + attr.addAll(Arrays.asList(INITIALIZE)); + attr.addAll(Arrays.asList(COMPUTE)); + attr.addAll(Arrays.asList(IGNORE)); + attr.addAll(Arrays.asList(ABORT)); + PROP_ATTRIBUTE = attr.toArray(new String[attr.size()]); + attr = new ArrayList(); + attr.addAll(Arrays.asList(PRIMARY)); + attr.addAll(Arrays.asList(AUTOCREATED)); + attr.addAll(Arrays.asList(MANDATORY)); + attr.addAll(Arrays.asList(PROTECTED)); + attr.addAll(Arrays.asList(SNS)); + attr.addAll(Arrays.asList(COPY)); + attr.addAll(Arrays.asList(VERSION)); + attr.addAll(Arrays.asList(INITIALIZE)); + attr.addAll(Arrays.asList(COMPUTE)); + attr.addAll(Arrays.asList(IGNORE)); + attr.addAll(Arrays.asList(ABORT)); + NODE_ATTRIBUTE = attr.toArray(new String[attr.size()]); + } + + public static final String QUEROPS_EQUAL = "="; + public static final String QUEROPS_NOTEQUAL = "<>"; + public static final String QUEROPS_LESSTHAN = "<"; + public static final String QUEROPS_LESSTHANOREQUAL = "<="; + public static final String QUEROPS_GREATERTHAN = ">"; + public static final String QUEROPS_GREATERTHANOREQUAL = ">="; + public static final String QUEROPS_LIKE = "LIKE"; + + public static final String[] STRING = {"STRING"}; + public static final String[] BINARY = {"BINARY"}; + public static final String[] LONG = {"LONG"}; + public static final String[] DOUBLE = {"DOUBLE"}; + public static final String[] BOOLEAN = {"BOOLEAN"}; + public static final String[] DATE = {"DATE"}; + public static final String[] NAME = {"NAME"}; + public static final String[] PATH = {"PATH"}; + public static final String[] REFERENCE = {"REFERENCE"}; + public static final String[] WEAKREFERENCE = {"WEAKREFERENCE"}; + public static final String[] URI = {"URI"}; + public static final String[] DECIMAL = {"DECIMAL"}; + + public static final String[] UNDEFINED = new String[]{"UNDEFINED", "*"}; + + public static final String EOF = "eof"; + + private final StreamTokenizer st; + + private final String systemId; + + /** + * Creates an unitialized lexer on top of the given reader. + * @param r the reader + * @param systemId informational systemid of the given stream + */ + public Lexer(Reader r, String systemId) { + this.systemId = systemId; + st = new StreamTokenizer(r); + + st.eolIsSignificant(false); + + st.lowerCaseMode(false); + + st.slashSlashComments(true); + st.slashStarComments(true); + + st.wordChars('a', 'z'); + st.wordChars('A', 'Z'); + st.wordChars(':', ':'); + st.wordChars('_', '_'); + + st.quoteChar(SINGLE_QUOTE); + st.quoteChar(DOUBLE_QUOTE); + + st.ordinaryChar(BEGIN_NODE_TYPE_NAME); + st.ordinaryChar(END_NODE_TYPE_NAME); + st.ordinaryChar(EXTENDS); + st.ordinaryChar(LIST_DELIMITER); + st.ordinaryChar(PROPERTY_DEFINITION); + st.ordinaryChar(CHILD_NODE_DEFINITION); + st.ordinaryChar(BEGIN_TYPE); + st.ordinaryChar(END_TYPE); + st.ordinaryChar(DEFAULT); + st.ordinaryChar(CONSTRAINT); + } + + /** + * getNextToken + * + * @return the next token + * @throws ParseException if an error during parsing occurs + */ + public String getNextToken() throws ParseException { + try { + int tokenType = st.nextToken(); + if (tokenType == StreamTokenizer.TT_EOF) { + return EOF; + } else if (tokenType == StreamTokenizer.TT_WORD + || tokenType == SINGLE_QUOTE + || tokenType == DOUBLE_QUOTE) { + return st.sval; + } else if (tokenType == StreamTokenizer.TT_NUMBER) { + return String.valueOf(st.nval); + } else { + return new String(new char[] {(char) tokenType}); + } + } catch (IOException e) { + fail("IOException while attempting to read input stream", e); + return null; + } + } + + /** + * Returns the system id + * @return the system id + */ + public String getSystemId() { + return systemId; + } + + public int getLineNumber() { + return st.lineno(); + } + + /** + * Creates a failure exception including the current line number and systemid. + * @param message message + * @throws ParseException the created exception + */ + public void fail(String message) throws ParseException { + throw new ParseException(message, getLineNumber(), -1, systemId); + } + + /** + * Creates a failure exception including the current line number and systemid. + * @param message message + * @param e root cause + * @throws ParseException the created exception + */ + public void fail(String message, Throwable e) throws ParseException { + throw new ParseException(message, e, getLineNumber(), -1, systemId); + } + + /** + * Creates a failure exception including the current line number and systemid. + * @param e root cause + * @throws ParseException the created exception + */ + public void fail(Throwable e) throws ParseException { + throw new ParseException(e, getLineNumber(), -1, systemId); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/ParseException.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/ParseException.java new file mode 100644 index 00000000000..ecce9df6c60 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/ParseException.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.commons.cnd; + +/** + * ParseException + */ +public class ParseException extends Exception { + + /** + * the line number where the error occurred + */ + private final int lineNumber; + + /** + * the column number where the error occurred + */ + private final int colNumber; + + /** + * the systemid of the source that produced the error + */ + private final String systemId; + + /** + * Constructs a new instance of this class with null as its + * detail message. + * @param lineNumber line number + * @param colNumber columns number + * @param systemId system id + */ + public ParseException(int lineNumber, int colNumber, String systemId) { + super(); + this.lineNumber = lineNumber; + this.colNumber = colNumber; + this.systemId = systemId; + } + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param lineNumber line number + * @param colNumber columns number + * @param systemId system id + */ + public ParseException(String message, int lineNumber, int colNumber, String systemId) { + super(message); + this.lineNumber = lineNumber; + this.colNumber = colNumber; + this.systemId = systemId; + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param lineNumber line number + * @param colNumber columns number + * @param systemId system id + * @param rootCause root failure cause + */ + public ParseException(String message, Throwable rootCause, int lineNumber, int colNumber, String systemId) { + super(message, rootCause); + this.lineNumber = lineNumber; + this.colNumber = colNumber; + this.systemId = systemId; + } + + /** + * Constructs a new instance of this class with the specified root cause. + * + * @param lineNumber line number + * @param colNumber columns number + * @param systemId system id + * @param rootCause root failure cause + */ + public ParseException(Throwable rootCause, int lineNumber, int colNumber, String systemId) { + super(rootCause); + this.lineNumber = lineNumber; + this.colNumber = colNumber; + this.systemId = systemId; + } + + /** + * {@inheritDoc} + */ + public String getMessage() { + String message = super.getMessage(); + StringBuffer b = new StringBuffer(message == null ? "" : message); + String delim = " ("; + if (systemId != null && !systemId.equals("")) { + b.append(delim); + b.append(systemId); + delim = ", "; + } + if (lineNumber >= 0) { + b.append(delim); + b.append("line "); + b.append(lineNumber); + delim = ", "; + } + if (colNumber >= 0) { + b.append(delim); + b.append("col "); + b.append(colNumber); + delim = ", "; + } + if (delim.equals(", ")) { + b.append(")"); + } + return b.toString(); + } + + /** + * {@inheritDoc} + */ + public String toString() { + return super.toString(); // + " (" + systemId + ", line " + lineNumber +", col " + colNumber +")"; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/TemplateBuilderFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/TemplateBuilderFactory.java new file mode 100644 index 00000000000..724190eb7c3 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/TemplateBuilderFactory.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.cnd; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinitionTemplate; + + +/** + * This implementation of {@link DefinitionBuilderFactory} can be used with + * the {@link CompactNodeTypeDefReader} to produce node type definitions of type + * {@link NodeTypeTemplate} and a namespace map of type {@link NamespaceRegistry}. + * It uses {@link NodeTypeTemplateBuilder} for building node type definitions, + * {@link PropertyDefinitionTemplateBuilder} for building property definitions, and + * {@link NodeDefinitionTemplateBuilder} for building node definitions. + */ +public class TemplateBuilderFactory extends DefinitionBuilderFactory { + + private final NodeTypeManager nodeTypeManager; + private final ValueFactory valueFactory; + private NamespaceRegistry namespaceRegistry; + + public TemplateBuilderFactory(NodeTypeManager nodeTypeManager, ValueFactory valueFactory, + NamespaceRegistry namespaceRegistry) { + + this.nodeTypeManager = nodeTypeManager; + this.valueFactory = valueFactory; + this.namespaceRegistry = namespaceRegistry; + } + + /** + * Creates a new TemplateBuilderFactory for the specified + * Session. This is equivalent to + * {@link #TemplateBuilderFactory(NodeTypeManager, ValueFactory, NamespaceRegistry)} + * where all parameters are obtained from the given session object and + * the workspace associated with it. + * + * @param session The repository session. + * @throws RepositoryException If an error occurs. + */ + public TemplateBuilderFactory(Session session) throws RepositoryException { + this(session.getWorkspace().getNodeTypeManager(), session.getValueFactory(), session.getWorkspace().getNamespaceRegistry()); + } + + @Override + public AbstractNodeTypeDefinitionBuilder newNodeTypeDefinitionBuilder() + throws UnsupportedRepositoryOperationException, RepositoryException { + + return new NodeTypeTemplateBuilder(); + } + + @Override + public void setNamespaceMapping(NamespaceRegistry namespaceRegistry) { + this.namespaceRegistry = namespaceRegistry; + } + + @Override + public NamespaceRegistry getNamespaceMapping() { + return namespaceRegistry; + } + + @Override + public void setNamespace(String prefix, String uri) { + try { + namespaceRegistry.registerNamespace(prefix, uri); + } + catch (RepositoryException e) { + // ignore + } + } + + public class NodeTypeTemplateBuilder extends AbstractNodeTypeDefinitionBuilder { + private final NodeTypeTemplate template; + private final List supertypes = new ArrayList(); + + public NodeTypeTemplateBuilder() throws UnsupportedRepositoryOperationException, RepositoryException { + super(); + template = nodeTypeManager.createNodeTypeTemplate(); + } + + @Override + public AbstractNodeDefinitionBuilder newNodeDefinitionBuilder() + throws UnsupportedRepositoryOperationException, RepositoryException { + + return new NodeDefinitionTemplateBuilder(this); + } + + @Override + public AbstractPropertyDefinitionBuilder newPropertyDefinitionBuilder() + throws UnsupportedRepositoryOperationException, RepositoryException { + + return new PropertyDefinitionTemplateBuilder(this); + } + + @Override + public NodeTypeTemplate build() throws ConstraintViolationException { + template.setMixin(super.isMixin); + template.setOrderableChildNodes(super.isOrderable); + template.setAbstract(super.isAbstract); + template.setQueryable(super.queryable); + template.setDeclaredSuperTypeNames(supertypes.toArray(new String[supertypes.size()])); + return template; + } + + @Override + public void setName(String name) throws RepositoryException { + super.setName(name); + template.setName(name); + } + + @Override + public void addSupertype(String name) { + supertypes.add(name); + } + + @Override + public void setPrimaryItemName(String name) throws ConstraintViolationException { + template.setPrimaryItemName(name); + } + + } + + public class PropertyDefinitionTemplateBuilder extends + AbstractPropertyDefinitionBuilder { + + private final NodeTypeTemplateBuilder ntd; + private final PropertyDefinitionTemplate template; + private final List values = new ArrayList(); + private final List constraints = new ArrayList(); + + public PropertyDefinitionTemplateBuilder(NodeTypeTemplateBuilder ntd) + throws UnsupportedRepositoryOperationException, RepositoryException { + + super(); + this.ntd = ntd; + template = nodeTypeManager.createPropertyDefinitionTemplate(); + } + + @Override + public void setName(String name) throws RepositoryException { + super.setName(name); + template.setName(name); + } + + @Override + public void addDefaultValues(String value) throws ValueFormatException { + values.add(valueFactory.createValue(value, getRequiredType())); + } + + @Override + public void addValueConstraint(String constraint) { + constraints.add(constraint); + } + + @Override + public void setDeclaringNodeType(String name) { + // empty + } + + @Override + public void build() throws IllegalStateException { + template.setAutoCreated(super.autocreate); + template.setMandatory(super.isMandatory); + template.setOnParentVersion(super.onParent); + template.setProtected(super.isProtected); + template.setRequiredType(super.requiredType); + template.setValueConstraints(constraints.toArray(new String[constraints.size()])); + template.setDefaultValues(values.toArray(new Value[values.size()])); + template.setMultiple(super.isMultiple); + template.setAvailableQueryOperators(super.queryOperators); + template.setFullTextSearchable(super.fullTextSearchable); + template.setQueryOrderable(super.queryOrderable); + + @SuppressWarnings("unchecked") + List templates = ntd.template.getPropertyDefinitionTemplates(); + templates.add(template); + } + + } + + public class NodeDefinitionTemplateBuilder extends AbstractNodeDefinitionBuilder { + private final NodeTypeTemplateBuilder ntd; + private final NodeDefinitionTemplate template; + private final List requiredPrimaryTypes = new ArrayList(); + + public NodeDefinitionTemplateBuilder(NodeTypeTemplateBuilder ntd) + throws UnsupportedRepositoryOperationException, RepositoryException { + + super(); + this.ntd = ntd; + template = nodeTypeManager.createNodeDefinitionTemplate(); + } + + @Override + public void setName(String name) throws RepositoryException { + super.setName(name); + template.setName(name); + } + + @Override + public void addRequiredPrimaryType(String name) { + requiredPrimaryTypes.add(name); + } + + @Override + public void setDefaultPrimaryType(String name) throws ConstraintViolationException { + template.setDefaultPrimaryTypeName(name); + } + + @Override + public void setDeclaringNodeType(String name) { + // empty + } + + @Override + public void build() throws ConstraintViolationException { + template.setAutoCreated(super.autocreate); + template.setMandatory(super.isMandatory); + template.setOnParentVersion(super.onParent); + template.setProtected(super.isProtected); + template.setRequiredPrimaryTypeNames(requiredPrimaryTypes + .toArray(new String[requiredPrimaryTypes.size()])); + template.setSameNameSiblings(super.allowSns); + + @SuppressWarnings("unchecked") + List templates = ntd.template.getNodeDefinitionTemplates(); + templates.add(template); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/package-info.java new file mode 100644 index 00000000000..f7dc093472f --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/cnd/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.3") +package org.apache.jackrabbit.commons.cnd; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/BTreeManager.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/BTreeManager.java new file mode 100644 index 00000000000..4662cb5968b --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/BTreeManager.java @@ -0,0 +1,456 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.jackrabbit.commons.flat; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.iterator.FilterIterator; +import org.apache.jackrabbit.commons.predicate.Predicate; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * This {@link TreeManager} implementation provides B+-tree like behavior. That + * is items of a sequence (i.e. {@link NodeSequence} or {@link PropertySequence}) + * are mapped to a sub-tree in JCR in a way such that only leave nodes carry + * actual values, the sub-tree is always balanced and ordered. This + * implementation does in contrast to a full B+-tree implementation not + * join nodes after deletions. This does not affect the order of items and also + * leaves the tree balanced wrt. its depths. It might however result in a sparse + * tree. That is, the tree might get unbalanced wrt. its weights. + *

    + * The nodes in the JCR sub tree are arranged such that any node named x + * only contains child nodes with names greater or equal to x. + * The implementation keeps the child nodes in the sub tree ordered if the + * respective node type supports ordering of child nodes. + * Ordering is always wrt. to a {@link Comparator} on the respective keys. + * For lexical order this arrangement corresponds to how words are arranged in a multi + * volume encyclopedia. + *

    + * Example usage: + *

    + * // Create a new TreeManager instance rooted at node. Splitting of nodes takes place
    + * // when the number of children of a node exceeds 40 and is done such that each new
    + * // node has at least 40 child nodes. The keys are ordered according to the natural
    + * // order of java.lang.String.
    + * TreeManager treeManager = new BTreeManager(node, 20, 40, Rank.<String>comparableComparator(), true);
    + *
    + * // Create a new NodeSequence with that tree manager
    + * NodeSequence nodes = ItemSequence.createNodeSequence(treeManager);
    + *
    + * // Add nodes with key "jcr" and "day"
    + * nodes.addNode("jcr", NodeType.NT_UNSTRUCTURED);
    + * nodes.addNode("day", NodeType.NT_UNSTRUCTURED);
    + *
    + * // Iterate over the node in the sequence.
    + * // Prints "day jcr "
    + * for (Node n : nodes) {
    + *     System.out.print(n.getName() + " ");
    + * }
    + *
    + * // Retrieve node with key "jcr"
    + * Node n = nodes.getItem("jcr");
    + *
    + * // Remove node with key "day"
    + * nodes.removeNode("day");
    + * 
    + */ +public class BTreeManager implements TreeManager { + private final Node root; + private final int minChildren; + private final int maxChildren; + private final Comparator order; + private final boolean autoSave; + private final Comparator itemOrder; + + private final Set ignoredProperties = new HashSet(Arrays.asList( + JcrConstants.JCR_PRIMARYTYPE, + JcrConstants.JCR_MIXINTYPES)); + + /** + * Create a new {@link BTreeManager} rooted at Node root. + * + * @param root the root of the JCR sub-tree where the items of the sequence + * are stored. + * @param minChildren minimal number of children for a node after splitting. + * @param maxChildren maximal number of children for a node after which + * splitting occurs. + * @param order order according to which the keys are stored + * @param autoSave determines whether the current session is saved after + * add/delete operations. + * @throws RepositoryException + */ + public BTreeManager(Node root, int minChildren, int maxChildren, Comparator order, boolean autoSave) + throws RepositoryException { + super(); + + if (root == null) { + throw new IllegalArgumentException("root must not be null"); + } + if (minChildren <= 0) { + throw new IllegalArgumentException("minChildren must be positive"); + } + if (2 * minChildren > maxChildren) { + throw new IllegalArgumentException("maxChildren must be at least twice minChildren"); + } + if (order == null) { + throw new IllegalArgumentException("order must not be null"); + } + + this.root = root; + this.minChildren = minChildren; + this.maxChildren = maxChildren; + this.order = order; + this.autoSave = autoSave; + this.itemOrder = new Comparator() { + public int compare(Item i1, Item i2) { + try { + return BTreeManager.this.order.compare(i1.getName(), i2.getName()); + } + catch (RepositoryException e) { + throw new WrappedRepositoryException(e); + } + } + }; + } + + /** + * Properties to ignore. The default set contains {@link JcrConstants#JCR_PRIMARYTYPE} + * and {@link JcrConstants#JCR_MIXINTYPES}. + * + * @return + */ + public Set getIgnoredProperties() { + return ignoredProperties; + } + + /** + * This implementations splits node when its number of child + * nodes exceeds the maximum number specified in the constructor. Splitting + * is done such that after the split each of the new child nodes contains at + * least as many nodes as specified in the constructor. + * + * @see org.apache.jackrabbit.commons.flat.TreeManager#split(org.apache.jackrabbit.commons.flat.ItemSequence, + * javax.jcr.Node, javax.jcr.Node) + */ + @SuppressWarnings("deprecation") + public void split(ItemSequence itemSequence, Node node, Node cause) throws RepositoryException { + SizedIterator childNodes = getNodes(node); + int count = (int) childNodes.getSize(); + if (count >= 0 && count <= maxChildren) { + return; + } + + split(node, new Rank(childNodes, Node.class, count, itemOrder), itemSequence); + } + + /** + * This implementations splits node when its number of + * properties exceeds the maximum number specified in the constructor. + * Splitting is done such that after the split each of the new child nodes + * contains at least as many nodes as specified in the constructor. + * + * @see org.apache.jackrabbit.commons.flat.TreeManager#split(org.apache.jackrabbit.commons.flat.ItemSequence, + * javax.jcr.Node, javax.jcr.Property) + */ + @SuppressWarnings("deprecation") + public void split(ItemSequence itemSequence, Node node, Property cause) throws RepositoryException { + SizedIterator properties = getProperties(node); + int count = (int) properties.getSize(); + if (count >= 0 && count <= maxChildren) { + return; + } + + split(node, new Rank(properties, Property.class, count, itemOrder), itemSequence); + } + + /** + * This implementation does not actually join any nodes. It does however + * delete node if {@link #getNodes(Node)} returns an empty + * iterator. It does further recursively delete any parent of + * node which does not have any child node. + * + * @see org.apache.jackrabbit.commons.flat.TreeManager#join(org.apache.jackrabbit.commons.flat.ItemSequence, + * javax.jcr.Node, javax.jcr.Node) + */ + @SuppressWarnings("deprecation") + public void join(ItemSequence itemSequence, Node node, Node cause) throws RepositoryException { + SizedIterator nodes = getNodes(node); + long count = nodes.getSize(); + if (count < 0) { + for (count = 0; nodes.hasNext(); count++) { + nodes.next(); + } + } + + if (count == 0) { + removeRec(node); + } + } + + /** + * This implementation does not actually join any nodes. It does however + * delete node if {@link #getProperties(Node)} returns an empty + * iterator. It does further recursively delete any parent of + * node which does not have any child node. + * + * @see org.apache.jackrabbit.commons.flat.TreeManager#join(org.apache.jackrabbit.commons.flat.ItemSequence, + * javax.jcr.Node, javax.jcr.Property) + */ + @SuppressWarnings("deprecation") + public void join(ItemSequence itemSequence, Node node, Property cause) throws RepositoryException { + SizedIterator properties = getProperties(node); + long count = properties.getSize(); + if (count < 0) { + for (count = 0; properties.hasNext(); count++) { + properties.next(); + } + } + + if (count == 0) { + removeRec(node); + } + } + + public Node getRoot() { + return root; + } + + public boolean isRoot(Node node) throws RepositoryException { + return node.isSame(root); + } + + /** + * Returns !node.hasNodes() + * @see org.apache.jackrabbit.commons.flat.TreeManager#isLeaf(javax.jcr.Node) + */ + public boolean isLeaf(Node node) throws RepositoryException { + return !node.hasNodes(); + } + + public Comparator getOrder() { + return order; + } + + public boolean getAutoSave() { + return autoSave; + } + + // -----------------------------------------------------< internal >--- + + /** + * Returns a {@link SizedIterator} of the child nodes of node. + */ + @SuppressWarnings("deprecation") + protected SizedIterator getNodes(Node node) throws RepositoryException { + NodeIterator nodes = node.getNodes(); + return getSizedIterator(convert(nodes), nodes.getSize()); + } + + /** + * Returns a {@link SizedIterator} of the properties of node + * which excludes the jcr.primaryType property. + */ + @SuppressWarnings("deprecation") + protected SizedIterator getProperties(Node node) throws RepositoryException { + final PropertyIterator properties = node.getProperties(); + + long size = properties.getSize(); + for (Iterator ignored = ignoredProperties.iterator(); size > 0 && ignored.hasNext(); ) { + if (node.hasProperty(ignored.next())) { + size--; + } + } + + return getSizedIterator(filterProperties(convert(properties)), size); + } + + /** + * Creates and return an intermediate node for the given name + * as child node of parent. + */ + protected Node createIntermediateNode(Node parent, String name) throws RepositoryException { + return parent.addNode(name); + } + + /** + * Move node to the new parent. + */ + protected void move(Node node, Node parent) throws RepositoryException { + String oldPath = node.getPath(); + String newPath = parent.getPath() + "/" + node.getName(); + node.getSession().move(oldPath, newPath); + } + + /** + * Move property to the new parent. + */ + protected void move(Property property, Node parent) throws RepositoryException { + parent.setProperty(property.getName(), property.getValue()); + property.remove(); + } + + /** + * Wraps iterator into a {@link SizedIterator} given a + * size. The value of the size parameter must + * correctly reflect the number of items in iterator. + */ + @SuppressWarnings("deprecation") + protected final SizedIterator getSizedIterator(final Iterator iterator, final long size) { + return new SizedIterator() { + public boolean hasNext() { + return iterator.hasNext(); + } + + public T next() { + return iterator.next(); + } + + public void remove() { + iterator.remove(); + } + + public long getSize() { + return size; + } + }; + } + + // -----------------------------------------------------< internal >--- + + @SuppressWarnings("unchecked") + private static Iterator convert(PropertyIterator it) { + return it; + } + + @SuppressWarnings("unchecked") + private static Iterator convert(NodeIterator it) { + return it; + } + + private void split(Node node, Rank ranking, ItemSequence itemSequence) throws RepositoryException { + if (ranking.size() <= maxChildren) { + return; + } + + try { + Node grandParent; + if (isRoot(node)) { + grandParent = node; + } + else { + grandParent = node.getParent(); + + // leave first minChildren items where they are + ranking.take(minChildren); + } + + // move remaining items to new parents + for (int k = ranking.size() / minChildren; k > 0; k--) { + T item = ranking.take(1).next(); + String key = item.getName(); + + Node newParent; + if (grandParent.getPrimaryNodeType().hasOrderableChildNodes()) { + Node dest = itemSequence.getSuccessor(grandParent, key); + newParent = createIntermediateNode(grandParent, key); + grandParent.orderBefore(key, dest == null ? null : dest.getName()); + } + else { + newParent = createIntermediateNode(grandParent, key); + } + + move(item, newParent); + + int c = k > 1 ? minChildren - 1 : ranking.size(); + Iterator remaining = ranking.take(c); + + // If ordered, ranking returns an ordered iterator. So order will be correct here + while (remaining.hasNext()) { + move(remaining.next(), newParent); + } + } + + // If we did not reach root yet, recursively split the parent + if (!node.isSame(root)) { + split(itemSequence, grandParent, (Node) null); + } + } + catch (WrappedRepositoryException e) { + throw e.wrapped(); + } + } + + private void move(T item, Node parent) throws RepositoryException { + if (item.isNode()) { + move((Node) item, parent); + } + else { + move((Property) item, parent); + } + } + + private void removeRec(Node node) throws RepositoryException { + Node n = node; + while (!n.hasNodes() && !isRoot(n)) { + Node d = n; + n = n.getParent(); + d.remove(); + } + } + + /** + * Filtering ignored properties from the given properties. + */ + private Iterator filterProperties(Iterator properties) { + return new FilterIterator(properties, new Predicate() { + public boolean evaluate(Object object) { + try { + Property p = (Property) object; + return !ignoredProperties.contains(p.getName()); + } + catch (RepositoryException ignore) { + return true; + } + } + }); + } + + private static class WrappedRepositoryException extends RuntimeException { + private final RepositoryException wrapped; + + public WrappedRepositoryException(RepositoryException e) { + super(); + this.wrapped = e; + } + + public RepositoryException wrapped() { + return wrapped; + } + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/FilterIterator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/FilterIterator.java new file mode 100644 index 00000000000..8b18b423ae9 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/FilterIterator.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.commons.flat; + +import org.apache.jackrabbit.commons.predicate.Predicate; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Iterator filtering out items which do not match a given predicate. + * @param + * @deprecated use {@link org.apache.jackrabbit.commons.iterator.FilterIterator} + */ +public class FilterIterator extends org.apache.jackrabbit.commons.iterator.FilterIterator { + + /** + * Create a new filtered iterator based on the given iterator. + * + * @param tIterator iterator to filter + * @param predicate only item matching this predicate are included + */ + public FilterIterator(Iterator tIterator, Predicate predicate) { + super(tIterator, predicate); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/ItemSequence.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/ItemSequence.java new file mode 100644 index 00000000000..74963462841 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/ItemSequence.java @@ -0,0 +1,526 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.jackrabbit.commons.flat; + +import org.apache.jackrabbit.commons.flat.TreeTraverser.ErrorHandler; +import org.apache.jackrabbit.commons.flat.TreeTraverser.InclusionPolicy; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.Set; + +/** + *

    + * This class serves as main entry point for obtaining sequences of {@link Node} + * s and {@link Property}s. It provides factory methods for creating + * {@link NodeSequence}s and {@link PropertySequence}s. + *

    + * + *

    + * NodeSequence and PropertySequence instances provide a flat representation of + * a JCR hierarchy rooted at a certain node. They allow iterating over all + * items, retrieving items by key, checking whether a given key is mapped, + * adding new items and removing existing items. + *

    + * + *

    + * The specifics of the mapping from the flat representation to the JCR + * hierarchy are delegated to a {@link TreeManager}. Particularly the + * TreeManager specifies the order of the items when retrieved as sequence and + * when and how to add and remove intermediate nodes when new items are inserted + * or removed. + *

    + * + *

    + * An {@link ErrorHandler} is used to handle exceptions which occur while + * traversing the hierarchy. + *

    + * + * @see TreeTraverser + * @see NodeSequence + * @see PropertySequence + * @see TreeManager + */ +public abstract class ItemSequence { + + /** + * The {@link TreeManager} instance managing the mapping between the + * sequence view and the JCR hierarchy. + */ + protected final TreeManager treeManager; + + /** + * The {@link ErrorHandler} instance used for handling exceptions occurring + * while traversing the hierarchy. + */ + protected final ErrorHandler errorHandler; + + /** + * @see TreeManager#getRoot() + */ + protected final Node root; + + /** + * @see TreeManager#getOrder() + */ + protected final Comparator order; + + /** + * @see TreeManager#getAutoSave() + */ + protected final boolean autoSave; + + /** + * Create a new {@link ItemSequence} instance. + * + * @param treeManager The {@link TreeManager} for managing the mapping + * between the sequence view and the JCR hierarchy. + * @param errorHandler The {@link ErrorHandler} for handling exceptions + * occurring while traversing the hierarchy. + * @throws IllegalArgumentException If either treeManager is + * null or {@link TreeManager#getRoot()} return + * null or {@link TreeManager#getOrder()} return + * null. + */ + protected ItemSequence(TreeManager treeManager, ErrorHandler errorHandler) { + super(); + + if (treeManager == null) { + throw new IllegalArgumentException("tree manager must not be null"); + } + if (treeManager.getRoot() == null) { + throw new IllegalArgumentException("root must not be null"); + } + if (treeManager.getOrder() == null) { + throw new IllegalArgumentException("order must not be null"); + } + + this.treeManager = treeManager; + this.errorHandler = errorHandler; + this.root = treeManager.getRoot(); + this.order = treeManager.getOrder(); + this.autoSave = treeManager.getAutoSave(); + } + + /** + * Create a new {@link NodeSequence} instance. + * + * @param treeManager The {@link TreeManager} for managing the mapping + * between the sequence view and the JCR hierarchy. + * @param errorHandler The {@link ErrorHandler} for handling exceptions + * occurring while + * @return + */ + public static NodeSequence createNodeSequence(TreeManager treeManager, ErrorHandler errorHandler) { + return new NodeSequenceImpl(treeManager, errorHandler); + } + + /** + * Create a new {@link NodeSequence} instance. + * + * @param treeManager The {@link TreeManager} for managing the mapping + * between the sequence view and the JCR hierarchy. + * @return + */ + public static NodeSequence createNodeSequence(TreeManager treeManager) { + return new NodeSequenceImpl(treeManager, ErrorHandler.IGNORE); + } + + /** + * Create a new {@link PropertySequence} instance. + * + * @param treeManager The {@link TreeManager} for managing the mapping + * between the sequence view and the JCR hierarchy. + * @param errorHandler The {@link ErrorHandler} for handling exceptions + * occurring while + * @return + */ + public static PropertySequence createPropertySequence(TreeManager treeManager, ErrorHandler errorHandler) { + return new PropertySequenceImpl(treeManager, errorHandler); + } + + /** + * Create a new {@link PropertySequence} instance. + * + * @param treeManager The {@link TreeManager} for managing the mapping + * between the sequence view and the JCR hierarchy. + * @return + */ + public static PropertySequence createPropertySequence(TreeManager treeManager) { + return new PropertySequenceImpl(treeManager, ErrorHandler.IGNORE); + } + + /** + * Create a new {@link NodeSequence} instance with the same parameterization + * as this instance. + * + * @return + */ + public NodeSequence getNodeSequence() { + return new NodeSequenceImpl(treeManager, errorHandler); + } + + /** + * Create a new {@link PropertySequence} instance with the same + * parametrization as this instance. + * + * @return + */ + public PropertySequence getPropertySequence() { + return new PropertySequenceImpl(treeManager, errorHandler); + } + + // -----------------------------------------------------< internal >--- + + /** + * Returns the parent node for the given key. When the key is not present in + * this sequence already, the returned node is the node that would contain + * that key if it where present. + */ + protected abstract Node getParent(String key) throws RepositoryException; + + /** + * Returns the predecessor node for the given + * key. That is the node + * whose key directly precedes the passed key in the order + * determined by {@link TreeManager#getOrder()}. There are two cases: + *
      + *
    • A node with the given key is mapped: then that node is + * returned.
    • + *
    • A node with the given key is not mapped: the the node + * where that would contain that key if present is returned.
    • + *
    + */ + protected final Node getPredecessor(String key) throws RepositoryException { + Node p = root; + Node n; + while ((n = getPredecessor(p, key)) != null) { + p = n; + } + + return p; + } + + /** + * Returns the direct predecessor of key amongst + * node's child nodes wrt. to {@link TreeManager#getOrder()}. + * Returns null if either node has no child nodes + * or node is a leaf (see {@link TreeManager#isLeaf(Node)}) or + * key is smaller than all the keys of all child nodes of + * node. + */ + protected final Node getPredecessor(Node node, String key) throws RepositoryException { + if (!node.hasNodes() || treeManager.isLeaf(node)) { + return null; + } + + // Shortcut for exact match + try { + return node.getNode(key); + } + catch (PathNotFoundException ignore) { } + + // Search for direct predecessor of key in the nodes children + // todo performance: for ordered nodes use binary search + NodeIterator childNodes = node.getNodes(); + Node p = null; + while (childNodes.hasNext()) { + Node n = childNodes.nextNode(); + String childKey = n.getName(); + if (order.compare(key, childKey) > 0 && (p == null || order.compare(childKey, p.getName()) > 0)) { + p = n; + } + } + + return p; + } + + /** + * Returns the successor node for the given + * key. That is the node + * whose key directly succeeds the passed key in the order + * determined by {@link TreeManager#getOrder()}. There are two cases: + *
      + *
    • A node with the given key is mapped: then that node is + * returned.
    • + *
    • A node with the given key is not mapped: the the node + * where that would contain that key if present is returned.
    • + *
    + */ + protected final Node getSuccessor(Node node, String key) throws RepositoryException { + if (!node.hasNodes() || treeManager.isLeaf(node)) { + return null; + } + + // Shortcut for exact match + try { + return node.getNode(key); + } + catch (PathNotFoundException ignore) { } + + // Search for direct successor of key in the nodes children + // todo performance: for ordered nodes use binary search + NodeIterator childNodes = node.getNodes(); + Node s = null; + while (childNodes.hasNext()) { + Node n = childNodes.nextNode(); + String childKey = n.getName(); + if (order.compare(key, childKey) < 0 && (s == null || order.compare(childKey, s.getName()) < 0)) { + s = n; + } + } + + return s; + } + + /** + * Returns the node with the minimal key wrt. {@link TreeManager#getOrder()}. + * For the empty sequence this is {@link TreeManager#getRoot()}. + */ + protected final Node getMinimal() throws RepositoryException { + Node p = null; + Node n = root; + while ((n = getMinimal(n)) != null) { + p = n; + } + + return p; + } + + /** + * Returns the node amongst the child nodes of node whose key + * is minimal wrt. {@link TreeManager#getOrder()}. Returns null + * id either node has no child nodes or node is a + * leaf (see {@link TreeManager#isLeaf(Node)}). + */ + protected final Node getMinimal(Node node) throws RepositoryException { + if (!node.hasNodes() || treeManager.isLeaf(node)) { + return null; + } + + // Search for minimal key in the nodes children + // todo performance: for ordered nodes use binary search + NodeIterator childNodes = node.getNodes(); + Node p = childNodes.nextNode(); + String minKey = p.getName(); + while (childNodes.hasNext()) { + Node n = childNodes.nextNode(); + if (order.compare(n.getName(), minKey) < 0) { + p = n; + minKey = p.getName(); + } + } + + return p; + } + + /** + * Rename the path of the node with the minimal key. That is, assuming + * node is the node with the minimal key (see + * {@link #getMinimal()}), this method renames every segment of the path of + * node up to {@link TreeManager#getRoot()} to key + * . Note: If node is not the node with the minimal + * key, the behavior of this method is not specified. + */ + protected final void renamePath(Node node, String key) throws RepositoryException { + if (!treeManager.isRoot(node)) { + Node p = node.getParent(); + renamePath(p, key); + Session s = node.getSession(); + s.move(node.getPath(), p.getPath() + "/" + key); + + // If orderable, move to first child node + if (p.getPrimaryNodeType().hasOrderableChildNodes()) { + p.orderBefore(key, p.getNodes().nextNode().getName()); + } + } + } + + protected static class NodeSequenceImpl extends ItemSequence implements NodeSequence { + private final InclusionPolicy inclusionPolicy = new InclusionPolicy() { + public boolean include(Node node) { + try { + return treeManager.isLeaf(node); + } + catch (RepositoryException e) { + return false; + } + } + }; + + public NodeSequenceImpl(TreeManager treeManager, ErrorHandler errorHandler) { + super(treeManager, errorHandler); + } + + public Iterator iterator() { + return TreeTraverser.nodeIterator(root, errorHandler, inclusionPolicy); + } + + public Node getItem(String key) throws RepositoryException { + return getParent(key).getNode(key); + } + + public boolean hasItem(String key) throws RepositoryException { + return getParent(key).hasNode(key); + } + + public Node addNode(String key, String primaryNodeTypeName) throws RepositoryException { + Node parent = getOrCreateParent(key); + if (parent.hasNode(key)) { + throw new ItemExistsException(key); + } + + Node n; + if (parent.getPrimaryNodeType().hasOrderableChildNodes()) { + Node dest = getSuccessor(parent, key); + n = parent.addNode(key, primaryNodeTypeName); + parent.orderBefore(key, dest == null ? null : dest.getName()); + } + else { + n = parent.addNode(key, primaryNodeTypeName); + } + + treeManager.split(this, parent, n); + + if (autoSave) { + parent.getSession().save(); + } + + return n; + } + + public void removeNode(String key) throws RepositoryException { + Node parent = getParent(key); + Node n = parent.getNode(key); + n.remove(); + treeManager.join(this, parent, n); + + if (autoSave) { + parent.getSession().save(); + } + } + + @Override + public Node getParent(String key) throws RepositoryException { + Node p = getPredecessor(key); + if (treeManager.isLeaf(p) && !treeManager.isRoot(p)) { + return p.getParent(); + } + else { + return p; + } + } + + private Node getOrCreateParent(String key) throws RepositoryException { + Node p = getParent(key); + if (treeManager.isRoot(p)) { + Node min = getMinimal(); + if (min != null) { + p = min.getParent(); + renamePath(p, key); + } + } + return p; + } + + } + + protected static class PropertySequenceImpl extends ItemSequence implements PropertySequence { + private final InclusionPolicy inclusionPolicy = new InclusionPolicy() { + private final Set ignoredProperties = treeManager.getIgnoredProperties(); + public boolean include(Property property) { + try { + return !ignoredProperties.contains(property.getName()); + } + catch (RepositoryException e) { + return false; + } + } + }; + + public PropertySequenceImpl(TreeManager treeManager, ErrorHandler errorHandler) { + super(treeManager, errorHandler); + } + + public Iterator iterator() { + return TreeTraverser.propertyIterator(getNodeSequence().iterator(), errorHandler, inclusionPolicy); + } + + public Property getItem(String key) throws RepositoryException { + return getParent(key).getProperty(key); + } + + public boolean hasItem(String key) throws RepositoryException { + return getParent(key).hasProperty(key); + } + + public Property addProperty(String key, Value value) throws RepositoryException { + Node parent = getOrCreateParent(key); + if (parent.hasProperty(key)) { + throw new ItemExistsException(key); + } + + Property p = parent.setProperty(key, value); + treeManager.split(this, parent, p); + + if (autoSave) { + p.getSession().save(); + } + + return p; + } + + public void removeProperty(String key) throws RepositoryException { + Node parent = getParent(key); + Property p = parent.getProperty(key); + p.remove(); + treeManager.join(this, parent, p); + + if (autoSave) { + parent.getSession().save(); + } + } + + @Override + public Node getParent(String key) throws RepositoryException { + return getPredecessor(key); + } + + private Node getOrCreateParent(String key) throws RepositoryException { + Node p = getParent(key); + if (treeManager.isRoot(p)) { + Node min = getMinimal(); + if (min != null) { + p = min; + renamePath(p, key); + } + } + return p; + } + + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/NodeSequence.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/NodeSequence.java new file mode 100644 index 00000000000..253fc01c48b --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/NodeSequence.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.commons.flat; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** + * Extension of {@link Sequence Sequence<Node>} which provides methods for + * adding and removing nodes by key. + */ +public interface NodeSequence extends Sequence { + + /** + * Add a with the given key and primary node type name. + * + * @param key key of the node to add + * @param primaryNodeTypeName primary node type of the node to add + * @return the newly added node + * @throws RepositoryException + */ + Node addNode(String key, String primaryNodeTypeName) throws RepositoryException; + + /** + * Remove the node with the given key. + * + * @param key The key of the node to remove + * @throws RepositoryException If there is no node with such a key or + * another error occurs. + */ + void removeNode(String key) throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/PropertySequence.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/PropertySequence.java new file mode 100644 index 00000000000..f9e575d1727 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/PropertySequence.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.commons.flat; + +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * Extension of {@link Sequence Sequence<Property>} which provides methods + * for adding and removing properties by key. + */ +public interface PropertySequence extends Sequence { + + /** + * Add a property with the given key and value. + * + * @param key key of the property to add + * @param value value of the property to add + * @return the newly added property + * @throws RepositoryException + */ + Property addProperty(String key, Value value) throws RepositoryException; + + /** + * Remove the property with the given key. + * + * @param key The key of the property to remove + * @throws RepositoryException If there is no property with such a key or + * another error occurs. + */ + void removeProperty(String key) throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/Rank.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/Rank.java new file mode 100644 index 00000000000..0fed132dd17 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/Rank.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.commons.flat; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; + +/** + *

    + * This class does efficient ranking of values of type T wrt. to a + * {@link Comparator} for T. After creating an instance of + * Rank, the {@link #take(int)} method returns the next + * k smallest values. That is, each of these values is smaller than + * every value not yet retrieved. The order of the values returned by + * take is not specified in general. However if the values are in + * increasing order, the values returned by take will also be in + * increasing order. + *

    + *

    + * Note: The values may not contain duplicates or the behavior + * of take is not defined. + *

    + * + * @param Type of values in this Rank. + */ +public class Rank { + private final T[] values; + private final Comparator order; + private int first; + + /** + * Create a new instance of Rank for a given array of + * values and a given order. The + * values are manipulated in place, no copying is performed. + * + * @param values values for ranking. Duplicates are not allowed. + * @param order Ordering for ranking + */ + public Rank(T[] values, Comparator order) { + super(); + this.values = values; + this.order = order; + } + + /** + * Create a new instance of Rank for a given collection of + * values and a given order. The + * values are copied into an internal array before they are + * manipulated. + * + * @param values values for ranking. Duplicates are not allowed. + * @param componentType type evidence for the values + * @param order Ordering for ranking + */ + public Rank(Collection values, Class componentType, Comparator order) { + super(); + this.values = toArray(values, componentType); + this.order = order; + } + + /** + * Create a new instance of Rank for the first + * count values in a a given iterator of values + * and a given order. The values are copied into + * an internal array before they are manipulated. + * + * @param values values for ranking. Duplicates are not allowed. + * @param componentType type evidence for the values + * @param count Number of items to include. -1 for all. + * @param order Ordering for ranking + */ + public Rank(Iterator values, Class componentType, int count, Comparator order) { + super(); + this.order = order; + + if (count >= 0) { + this.values = createArray(count, componentType); + for (int k = 0; k < count; k++) { + this.values[k] = values.next(); + } + } + else { + List l = new LinkedList(); + while (values.hasNext()) { + l.add(values.next()); + } + this.values = toArray(l, componentType); + } + } + + /** + * Create a new instance of Rank for a given array of + * values. The order is determined by the natural ordering of + * the values (i.e. through {@link Comparable}). The values are + * manipulated in place, no copying is performed. + * + * @param extends Comparable<S> + * @param values values for ranking. Duplicates are not allowed. + * @return A new instance of Rank. + */ + public static > Rank rank(S[] values) { + return new Rank(values, Rank.comparableComparator()); + } + + /** + * Create a new instance of Rank for a given collection of + * values. The order is determined by the natural ordering of + * the values (i.e. through {@link Comparable}). The values are + * copied into an internal array before they are manipulated. + * + * @param extends Comparable<S> + * @param values values for ranking. Duplicates are not allowed. + * @param componentType type evidence for the values + * @return A new instance of Rank. + */ + public static > Rank rank(Collection values, Class componentType) { + return new Rank(values, componentType, Rank.comparableComparator()); + } + + /** + * Create a new instance of Rank for the first + * count values in a a given iterator of values. + * The order is determined by the natural ordering of the values (i.e. + * through {@link Comparable}). The values are copied into an + * internal array before they are manipulated. + * + * @param extends Comparable<S> + * @param values values for ranking. Duplicates are not allowed. + * @param componentType type evidence for the values + * @param count Number of items to include. -1 for all. + * @return A new instance of Rank. + */ + public static > Rank rank(Iterator values, Class componentType, int count) { + return new Rank(values, componentType, count, Rank.comparableComparator()); + } + + /** + * Utility method for creating a {@link Comparator} of T from a + * {@link Comparable} of type T. + * + * @param extends Comparable<T> + * @return Comparator whose order is defined by T. + */ + public static > Comparator comparableComparator() { + return new Comparator() { + public int compare(T c1, T c2) { + return c1.compareTo(c2); + } + }; + } + + public Comparator getOrder() { + return order; + } + + /** + * Returns the n-th smallest values remaining in this + * Rank. + * + * @param n Number of values to return + * @return An iterator containing the next n smallest values. + * @throws NoSuchElementException if this Rank has not enough + * remaining elements or when n is negative. + */ + public Iterator take(int n) { + if (n < 0 || n + first > values.length) { + throw new NoSuchElementException(); + } + + if (n > 0) { + take(n, first, values.length - 1); + first += n; + return Arrays.asList(values).subList(first - n, first).iterator(); + } else { + return Collections.emptySet().iterator(); + } + } + + /** + * Returns the number of remaining items in the Rank. + * + * @return number of remaining items. + */ + public int size() { + return values.length - first; + } + + // -----------------------------------------------------< internal >--- + + /** + * Rearrange {@link #values} such that each of the n first + * values starting at from is smaller that all the remaining + * items up to to. + */ + private void take(int n, int from, int to) { + // Shortcut for all values + if (n >= to - from + 1) { + return; + } + + // Choosing the n-th value as pivot results in correct partitioning after one pass + // for already ordered values. + int pivot = from + n - 1; + int lo = from; + int hi = to; + + // Partition values around pivot + while (lo < hi) { + // Find values to swap around the pivot + while (order.compare(values[lo], values[pivot]) < 0) { + lo++; + } + while (order.compare(values[hi], values[pivot]) > 0) { + hi--; + } + if (lo < hi) { + // Swap values and keep track of pivot position in case the pivot itself is swapped + if (lo == pivot) { + pivot = hi; + } else if (hi == pivot) { + pivot = lo; + } + swap(lo, hi); + lo++; + hi--; + } + } + + // Actual number of values taken + int nn = pivot + 1 - from; + if (nn > n) { // Recurse: take first n elements from first partition + take(n, from, pivot); + } + else if (nn < n) { // Recurse: take first n - nn elements from second partition + take(n - nn, pivot + 1, to); + } + // else done + } + + private void swap(int lo, int hi) { + T t1 = values[lo]; + T t2 = values[hi]; + if (order.compare(t1, t2) == 0) { + throw new IllegalStateException("Detected duplicates " + t1); + } + values[lo] = t2; + values[hi] = t1; + } + + // -----------------------------------------------------< utility >--- + + private static S[] toArray(Collection collection, Class componentType) { + return collection.toArray(createArray(collection.size(), componentType)); + } + + @SuppressWarnings("unchecked") + private static S[] createArray(int size, Class componentType) { + return (S[]) Array.newInstance(componentType, size); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/Sequence.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/Sequence.java new file mode 100644 index 00000000000..5404d569a09 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/Sequence.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.commons.flat; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import java.util.Iterator; + +/** + * Interface for accessing JCR {@link Item}s sequentially through an + * {@link Iterator} or looking them up through a key. + * + * @param extends <Item> + */ +public interface Sequence extends Iterable { + + /** + * Iterator for the {@link Item}s in this sequence. The order of the items + * is implementation specific. + * + * @see java.lang.Iterable#iterator() + */ + Iterator iterator(); + + /** + * Retrieve an {@link Item} from this sequence by its key. If + * the sequence does not contain the key this method throws an + * {@link ItemNotFoundException}. + * + * @param key The key of the item to retrieve. Must not be + * null. + * @return The item belonging to key. + * @throws ItemNotFoundException + * @throws RepositoryException + */ + T getItem(String key) throws AccessDeniedException, PathNotFoundException, ItemNotFoundException, + RepositoryException; + + /** + * Determine whether this sequence contains a specific key. + * + * @param key The key to look up. + * @return true if this sequence contains key. + * False otherwise. + * @throws RepositoryException + */ + boolean hasItem(String key) throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/SizedIterator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/SizedIterator.java new file mode 100644 index 00000000000..da083efcd24 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/SizedIterator.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.commons.flat; + +/** + * SizedIterator extends {@link java.util.Iterator} with a + * getSize method. + * + * @param the type of elements of this iterator + * @deprecated use {@link org.apache.jackrabbit.commons.iterator.SizedIterator} + */ +public interface SizedIterator extends org.apache.jackrabbit.commons.iterator.SizedIterator { +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/TreeManager.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/TreeManager.java new file mode 100644 index 00000000000..d52a40cb81c --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/TreeManager.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.jackrabbit.commons.flat; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import java.util.Comparator; +import java.util.Set; + +/** + * TreeManager instances are responsible for the mapping between sequence views + * of {@link Node}s and {@link Property}s and an hierarchical JCR + * representation. They are passed to the various factory methods in + * {@link ItemSequence} to parameterize the behavior of {@link NodeSequence}s + * and {@link PropertySequence}s. + * + * @see NodeSequence + * @see PropertySequence + */ +public interface TreeManager { + + /** + * @return the root node of the JCR sub-tree where the items of the sequence + * are be mapped to. + */ + Node getRoot(); + + /** + * Determined whether the given node is the root node of the + * JCR sub-tree. + * + * @param node Node to test for root + * @return getRoot().isSame(node). + * @throws RepositoryException + */ + boolean isRoot(Node node) throws RepositoryException; + + /** + * Determines whether the given node is a leaf. Leaf nodes are + * the nodes which are actually part of a {@link NodeSequence} or the + * parents of the properties of a {@link PropertySequence}. + * + * @param node Node to test for leaf + * @return true if node is a leaf node, + * false otherwise. + * @throws RepositoryException + */ + boolean isLeaf(Node node) throws RepositoryException; + + /** + * Properties to ignore. + * @return + */ + public Set getIgnoredProperties(); + + /** + * {@link Comparator} used for establishing the order of the keys in the + * sequence. + * + * @return a Comparator<String> instance + */ + Comparator getOrder(); + + /** + * After the node cause has been inserted into the sequence + * itemSequence, the implementation of this method may decide + * to split the parent node of cause into two or + * more new nodes. Splitting must be done such that the overall order of the + * keys in this sequence obeys the order given by {@link #getOrder()} as + * much as possible. + * + * @param itemSequence the {@link ItemSequence} where the new node + * cause has been inserted. + * @param node the parent node of the newly inserted node + * @param cause the newly inserted node or null if not known. + * @throws RepositoryException + */ + void split(ItemSequence itemSequence, Node node, Node cause) throws RepositoryException; + + /** + * After the property cause has been inserted into the sequence + * itemSequence, the implementation of this method may decide + * to split the parent node of cause into two or + * more new nodes. Splitting must be done such that the overall order of the + * keys in this sequence obeys the order given by {@link #getOrder()} as + * much as possible. + * + * @param itemSequence the {@link ItemSequence} where the new property + * cause has been inserted. + * @param node the parent node of the newly inserted property + * @param cause the newly inserted property or null if not + * known. + * @throws RepositoryException + */ + void split(ItemSequence itemSequence, Node node, Property cause) throws RepositoryException; + + /** + * After the node cause has been deleted from the sequence + * itemSequence, the implementation of this method may decide + * to join the parent node of cause with some + * other nodes. Joining must be done such that the overall order of the keys + * in this sequence obeys the order given by {@link #getOrder()} as much as + * possible. + * + * @param itemSequence the {@link ItemSequence} where the node + * cause has been deleted from. + * @param node the parent node from which cause has been + * deleted. + * @param cause the deleted node or null if not known. + * Note: cause might be stale. + * @throws RepositoryException + */ + void join(ItemSequence itemSequence, Node node, Node cause) throws RepositoryException; + + /** + * After the property cause has been deleted from the sequence + * itemSequence, the implementation of this method may decide + * to join the parent node of cause with some + * other nodes. Joining must be done such that the overall order of the keys + * in this sequence obeys the order given by {@link #getOrder()} as much as + * possible. + * + * @param itemSequence the {@link ItemSequence} where the property + * cause has been deleted from. + * @param node the parent node from which cause has been + * deleted. + * @param cause the deleted property or null if not known. + * Note: cause might be stale. + * @throws RepositoryException + */ + void join(ItemSequence itemSequence, Node node, Property cause) throws RepositoryException; + + /** + * Whether to automatically save changes of the current session occurring + * from adding/removing nodes and properties. + * + * @return true if changes should be automatically saved, + * false otherwiese. + */ + boolean getAutoSave(); +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/TreeTraverser.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/TreeTraverser.java new file mode 100644 index 00000000000..8d6be07fc1a --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/TreeTraverser.java @@ -0,0 +1,348 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.jackrabbit.commons.flat; + +import static org.apache.jackrabbit.commons.iterator.LazyIteratorChain.chain; + +import org.apache.jackrabbit.commons.iterator.FilterIterator; +import org.apache.jackrabbit.commons.predicate.Predicate; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import java.util.Collections; +import java.util.Iterator; + +/** + *

    + * Utility class for traversing the {@link Item}s of a JCR hierarchy rooted at a + * specific {@link Node}. + *

    + * + *

    + * This class provides an {@link Iterator} of JCR items either through its + * implementation of {@link Iterable} or through various static factory methods. + * The iterators return its elements in pre-order. That is, each node occurs + * before its child nodes are traversed. The order in which child nodes are + * traversed is determined by the underlying JCR implementation. Generally the + * order is not specified unless a {@link Node} has orderable child nodes. + *

    + * + *

    + * Whether a specific node is included is determined by an + * {@link #inclusionPolicy}. Error occurring while traversing are delegated to + * an {@link #errorHandler}. + *

    + */ +public final class TreeTraverser implements Iterable { + private final Node root; + private final ErrorHandler errorHandler; + private final InclusionPolicy inclusionPolicy; + + /** + * Create a new instance of a TreeTraverser rooted at node. + * + * @param root The root node of the sub-tree to traverse + * @param errorHandler Handler for errors while traversing + * @param inclusionPolicy Inclusion policy to determine which nodes to + * include + */ + public TreeTraverser(Node root, ErrorHandler errorHandler, InclusionPolicy inclusionPolicy) { + super(); + this.root = root; + this.errorHandler = errorHandler == null? ErrorHandler.IGNORE : errorHandler; + this.inclusionPolicy = inclusionPolicy; + } + + /** + * Create a new instance of a TreeTraverser rooted at node. + * + * @param root The root node of the sub-tree to traverse + */ + public TreeTraverser(Node root) { + this(root, ErrorHandler.IGNORE, InclusionPolicy.ALL); + } + + /** + * Error handler for handling {@link RepositoryException}s occurring on + * traversal. The predefined {@link #IGNORE} error handler can be used to + * ignore all exceptions. + */ + public interface ErrorHandler { + + /** + * Predefined error handler which ignores all exceptions. + */ + public static ErrorHandler IGNORE = new ErrorHandler() { + public void call(Item item, RepositoryException exception) { /* ignore */ } + }; + + /** + * This call back method is called whenever an error occurs while + * traversing. + * + * @param item The item which was the target of an operation which + * failed and caused the exception. + * @param exception The exception which occurred. + */ + void call(Item item, RepositoryException exception); + } + + /** + * Inclusion policy to determine which items to include when traversing. + * There a two predefined inclusion policies: + *
      + *
    • {@link #ALL} includes all items.
    • + *
    • {@link #LEAVES} includes only leave nodes. A leaf node is a node + * which does not have child nodes.
    • + *
    + */ + public interface InclusionPolicy { + + /** + * This inclusions policy includes all items. + */ + public static InclusionPolicy ALL = new InclusionPolicy() { + public boolean include(Item item) { + return true; + } + }; + + /** + * This inclusion policy includes leave nodes only. A leaf + * node is a node which does not have child nodes. + */ + public static InclusionPolicy LEAVES = new InclusionPolicy() { + public boolean include(Node node) { + try { + return !node.hasNodes(); + } + catch (RepositoryException e) { + return false; + } + } + }; + + /** + * Call back method to determine whether to include a given item. + * + * @param item The item under consideration + * @return true when item should be included. + * false otherwise. + */ + boolean include(T item); + } + + /** + * Create an iterator for the nodes of the sub-tree rooted at + * root. + * + * @param root root node of the sub-tree to traverse + * @param errorHandler handler for exceptions occurring on traversal + * @param inclusionPolicy inclusion policy to determine which nodes to + * include + * @return iterator of {@link Node} + */ + public static Iterator nodeIterator(Node root, ErrorHandler errorHandler, + InclusionPolicy inclusionPolicy) { + + return new TreeTraverser(root, errorHandler, inclusionPolicy).iterator(); + } + + /** + * Create an iterator for the nodes of the sub-tree rooted at + * root. Exceptions occurring on traversal are ignored. + * + * @param root root node of the sub-tree to traverse + * @return iterator of {@link Node} + */ + public static Iterator nodeIterator(Node root) { + return nodeIterator(root, ErrorHandler.IGNORE, InclusionPolicy.ALL); + } + + /** + * Create an iterator of the properties for a given iterator of nodes. The + * order of the returned properties is only specified so far that if node + * n1 occurs before node n2 in the iterator of + * nodes, then any property of n1 will occur before any + * property of n2. + * + * @param nodes nodes whose properties to chain + * @param errorHandler handler for exceptions occurring on traversal + * @param inclusionPolicy inclusion policy to determine properties items to include + * + * @return iterator of {@link Property} + */ + public static Iterator propertyIterator(Iterator nodes, ErrorHandler errorHandler, + InclusionPolicy inclusionPolicy) { + + return filter(chain(propertyIterators(nodes, errorHandler)), inclusionPolicy); + } + + + /** + * Create an iterator of the properties for a given iterator of nodes. The + * order of the returned properties is only specified so far that if node + * n1 occurs before node n2 in the iterator of + * nodes, then any property of n1 will occur before any + * property of n2. Exceptions occurring on traversal are + * ignored. + * + * @param nodes nodes whose properties to chain + * @return iterator of {@link Property} + */ + public static Iterator propertyIterator(Iterator nodes) { + return propertyIterator(nodes, ErrorHandler.IGNORE, InclusionPolicy.ALL); + } + + /** + * Create an iterator of the properties of all nodes of the sub-tree rooted + * at root. + * + * @param root root node of the sub-tree to traverse + * @param errorHandler handler for exceptions occurring on traversal + * @param inclusionPolicy inclusion policy to determine which items to + * include + * @return iterator of {@link Property} + */ + public static Iterator propertyIterator(Node root, ErrorHandler errorHandler, + InclusionPolicy inclusionPolicy) { + + return propertyIterator(nodeIterator(root, errorHandler, inclusionPolicy), errorHandler, + inclusionPolicy); + } + + /** + * Create an iterator of the properties of all nodes of the sub-tree rooted + * at root. Exceptions occurring on traversal are ignored. + * + * @param root root node of the sub-tree to traverse + * @return iterator of {@link Property} + */ + public static Iterator propertyIterator(Node root) { + return propertyIterator(root, ErrorHandler.IGNORE, InclusionPolicy.ALL); + } + + /** + * Returns an iterator of {@link Node} for this instance. + * + * @see TreeTraverser#TreeTraverser(Node, ErrorHandler, InclusionPolicy) + * @see java.lang.Iterable#iterator() + */ + public Iterator iterator() { + return iterator(root); + } + + // -----------------------------------------------------< internal >--- + + /** + * Returns an iterator of the nodes of the sub-tree rooted at + * node. + */ + @SuppressWarnings("unchecked") + private Iterator iterator(Node node) { + if (inclusionPolicy.include(node)) { + return chain(singleton(node), chain(childIterators(node))); + } + else { + return chain(childIterators(node)); + } + } + + /** + * Returns an iterator of iterators of the child nodes of node. + */ + private Iterator> childIterators(Node node) { + try { + final NodeIterator childNodes = node.getNodes(); + return new Iterator>() { + public boolean hasNext() { + return childNodes.hasNext(); + } + public Iterator next() { + return iterator(childNodes.nextNode()); + } + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } catch (RepositoryException e) { + errorHandler.call(node, e); + return empty(); + } + } + + /** + * Returns an iterator of all properties of all nodes. For node + * n1 occurring before node n2 in + * nodes, any property of n1 will occur before any + * property of n2 in the iterator. + */ + private static Iterator> propertyIterators(final Iterator nodes, + final ErrorHandler errorHandler) { + + return new Iterator>() { + public boolean hasNext() { + return nodes.hasNext(); + } + + @SuppressWarnings("unchecked") + public Iterator next() { + Node n = nodes.next(); + try { + return n.getProperties(); + } catch (RepositoryException e) { + errorHandler.call(n, e); + return empty(); + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + // -----------------------------------------------------< utility >--- + + private static Iterator empty() { + return Collections.emptySet().iterator(); + } + + private Iterator singleton(T value) { + return Collections.singleton(value).iterator(); + } + + /** + * Filtering items not matching the inclusionPolicy from + * iterator. + */ + private static Iterator filter(final Iterator iterator, + final InclusionPolicy inclusionPolicy) { + + return new FilterIterator(iterator, new Predicate() { + @SuppressWarnings("unchecked") + public boolean evaluate(Object object) { + return inclusionPolicy.include((T) object); + } + }); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/package-info.java new file mode 100644 index 00000000000..11d63cedca5 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/flat/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.3") +package org.apache.jackrabbit.commons.flat; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/AbstractLazyIterator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/AbstractLazyIterator.java new file mode 100644 index 00000000000..289ecf0b8cc --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/AbstractLazyIterator.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.commons.iterator; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * {@code AbstractLazyIterator} provides basic iteration methods for a lazy loading iterator that does not support + * remove. Implementing classes only need to implement the {@link #getNext()} method which must return the next item + * in the iteration or {@code null} if the iteration as reached its end. + */ +public abstract class AbstractLazyIterator implements Iterator { + + private boolean fetchNext = true; + + private T next; + + protected AbstractLazyIterator() { + } + + @Override + public boolean hasNext() { + if (fetchNext) { + next = getNext(); + fetchNext = false; + } + return next != null; + } + + @Override + public T next() { + if (fetchNext) { + next = getNext(); + } else { + fetchNext = true; + } + if (next == null) { + throw new NoSuchElementException(); + } + return next; + } + + /** + * Returns the next element of this iteration or {@code null} if the iteration has finished. + * @return the next element. + */ + abstract protected T getNext(); + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/AccessControlPolicyIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/AccessControlPolicyIteratorAdapter.java new file mode 100644 index 00000000000..2e9d0086cc8 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/AccessControlPolicyIteratorAdapter.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.commons.iterator; + +import javax.jcr.RangeIterator; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Adapter class for turning {@link RangeIterator}s or {@link Iterator}s + * into {@link AccessControlPolicyIteratorAdapter}s. + */ +public class AccessControlPolicyIteratorAdapter extends RangeIteratorDecorator + implements AccessControlPolicyIterator { + + /** + * Static instance of an empty {@link AccessControlPolicyIteratorAdapter}. + */ + public static final AccessControlPolicyIterator EMPTY = + new AccessControlPolicyIteratorAdapter(RangeIteratorAdapter.EMPTY); + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link AccessControlPolicy access control policies}. + */ + public AccessControlPolicyIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link Iterator}. + * + * @param iterator iterator of {@link AccessControlPolicy access control policies}. + */ + public AccessControlPolicyIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + /** + * Creates an iterator for the given collection. + * + * @param collection collection of {@link AccessControlPolicy} objects. + */ + public AccessControlPolicyIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //----------------------------------------< AccessControlPolicyIterator >--- + /** + * Returns the next policy. + * + * @return next policy. + * @throws NoSuchElementException if there is no next policy. + */ + public AccessControlPolicy nextAccessControlPolicy() throws NoSuchElementException { + return (AccessControlPolicy) next(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/EventIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/EventIteratorAdapter.java new file mode 100644 index 00000000000..b569aaa1c64 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/EventIteratorAdapter.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.commons.iterator; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; + +/** + * Adapter class for turning {@link RangeIterator}s or {@link Iterator}s + * into {@link EventIterator}s. + */ +public class EventIteratorAdapter extends RangeIteratorDecorator + implements EventIterator { + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link Event}s + */ + public EventIteratorAdapter(EventIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link Event}s + */ + public EventIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link Iterator}. + * + * @param iterator iterator of {@link Event}s. + */ + public EventIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + /** + * Creates an iterator for the given collection. + * + * @param collection collection of {@link Event}s + */ + public EventIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //-------------------------------------------------------< EventIterator > + + /** + * Returns the next event. + * + * @return next event + * @throws NoSuchElementException if there is no next event + */ + public Event nextEvent() throws NoSuchElementException { + return (Event) next(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/EventListenerIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/EventListenerIteratorAdapter.java new file mode 100644 index 00000000000..9a54f457118 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/EventListenerIteratorAdapter.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.commons.iterator; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.EventListenerIterator; + +/** + * Adapter class for turning {@link RangeIterator}s or {@link Iterator}s + * into {@link EventListenerIterator}s. + */ +public class EventListenerIteratorAdapter extends RangeIteratorDecorator + implements EventListenerIterator { + + /** + * Static instance of an empty {@link EventListenerIterator}. + */ + public static final EventListenerIterator EMPTY = + new EventListenerIteratorAdapter(RangeIteratorAdapter.EMPTY); + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link EventListener}s + */ + public EventListenerIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link Iterator}. + * + * @param iterator iterator of {@link EventListener}s + */ + public EventListenerIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + /** + * Creates an iterator for the given collection. + * + * @param collection collection of {@link EventListener}s + */ + public EventListenerIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //-----------------------------------------------< EventListenerIterator > + + /** + * Returns the next event listener. + * + * @return next event listener + * @throws NoSuchElementException if there is no next event listener + */ + public EventListener nextEventListener() { + return (EventListener) next(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilterIterator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilterIterator.java new file mode 100644 index 00000000000..d613fdb29d1 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilterIterator.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.commons.iterator; + +import org.apache.jackrabbit.commons.predicate.Predicate; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Iterator filtering out items which do not match a given predicate. + * @param + */ +public class FilterIterator implements Iterator { + private final Iterator iterator; + private final Predicate predicate; + + private T next = null; + + /** + * Create a new filtered iterator based on the given iterator. + * + * @param iterator iterator to filter + * @param predicate only item matching this predicate are included + */ + public FilterIterator(Iterator iterator, Predicate predicate) { + super(); + this.iterator = iterator; + this.predicate = predicate; + } + + public boolean hasNext() { + while (next == null && iterator.hasNext()) { + T e = iterator.next(); + if (predicate.evaluate(e)) { + next = e; + } + } + + return next != null; + } + + public T next() { + if (hasNext()) { + T e = next; + next = null; + return e; + } + else { + throw new NoSuchElementException(); + } + } + + /** + * @throws UnsupportedOperationException always + * @see java.util.Iterator#remove() + */ + public void remove() { + throw new UnsupportedOperationException(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilteredRangeIterator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilteredRangeIterator.java new file mode 100644 index 00000000000..596d18aeba1 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilteredRangeIterator.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.iterator; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; + +import org.apache.jackrabbit.commons.predicate.Predicate; + +/** + * Filtering decorator for iterators. + */ +public class FilteredRangeIterator implements RangeIterator { + + /** + * The underlying iterator. + */ + private final Iterator iterator; + + /** + * Predicate used for filtering. + */ + private final Predicate predicate; + + /** + * Buffer of pre-fetched objects from the underlying iterator. + */ + private final Object[] buffer; + + /** + * Current position within the buffer. Guaranteed to be within the + * range [0, {@link #bufferSize}]. + */ + private int bufferPosition = 0; + + /** + * Number of pre-fetched objects in the buffer. + */ + private int bufferSize = 0; + + /** + * The zero-based position of the first element in the buffer. + */ + private long position = 0; + + /** + * The number of filtered items up to the end of the buffer. + */ + private long size = 0; + + + /** + * Creates a new filtered iterator. + * + * @param iterator underlying iterator + * @param predicate + * @param bufferSize + */ + public FilteredRangeIterator( + Iterator iterator, Predicate predicate, int bufferSize) { + this.iterator = iterator; + this.predicate = predicate; + this.buffer = new Object[bufferSize]; + } + + /** + * Creates a new filtered iterator with the default pre-fetch buffer size. + * + * @param iterator underlying iterator + * @param predicate predicate used for filtering + */ + public FilteredRangeIterator(Iterator iterator, Predicate predicate) { + this(iterator, predicate, 1000); + } + + /** + * Creates a pre-fetching decorator for the given iterator. This + * constructor is mostly useful when it is desirable to use the + * pre-fetching feature to automatically + * + * + * @param iterator underlying iterator + */ + public FilteredRangeIterator(Iterator iterator) { + this(iterator, Predicate.TRUE, 1000); + } + + /** + * Pre-fetches more items into the buffer. + */ + private void fetch() { + if (bufferPosition == bufferSize) { + position += bufferSize; + bufferPosition = 0; + bufferSize = 0; + while (bufferSize < buffer.length && iterator.hasNext()) { + Object object = iterator.next(); + if (predicate.evaluate(object)) { + buffer[bufferSize++] = object; + } + } + size += bufferSize; + } + } + + /** + * Returns the zero-based position of the next element in this iterator. + * + * @return position of the next element + */ + public long getPosition() { + return position + bufferPosition; + } + + /** + * Returns the total number of elements in this iterator, or -1 if that + * number is unknown. + * + * @return number of elements in this iterator, or -1 + */ + public long getSize() { + fetch(); + if (iterator.hasNext()) { + return -1; // still unknown + } else { + return size; + } + } + + /** + * Skips the next n elements. + * + * @param n number of elements to skip + * @throws IllegalArgumentException if n is negative + * @throws NoSuchElementException if there are not enough elements to skip + */ + public void skip(long n) + throws IllegalArgumentException, NoSuchElementException { + if (n < 0) { + throw new IllegalArgumentException(); + } + while (n > 0) { + fetch(); + if (bufferPosition < bufferSize) { + long m = Math.min(n, bufferSize - bufferPosition); + bufferPosition += m; + n -= m; + } else { + throw new NoSuchElementException(); + } + } + } + + /** + * Checks whether there are more elements in this iterator. + * + * @return true if there are more elements, + * false otherwise + */ + public boolean hasNext() { + fetch(); + return bufferPosition < bufferSize; + } + + /** + * Returns the next element in this iterator. + * + * @return next element + * @throws NoSuchElementException if there are no more elements + */ + public Object next() throws NoSuchElementException { + fetch(); + if (bufferPosition < bufferSize) { + return buffer[bufferPosition++]; + } else { + throw new NoSuchElementException(); + } + } + + /** + * Not supported. + */ + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilteringNodeIterator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilteringNodeIterator.java new file mode 100644 index 00000000000..083d82e4ffb --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FilteringNodeIterator.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.commons.iterator; + +import org.apache.jackrabbit.commons.predicate.Predicate; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import java.util.NoSuchElementException; + +/** + * A wrapper around a NodeIterator filtering out nodes from the base iterator + * that don't match the specified {@link Predicate filter}. Due to the nature + * of the filter mechanism the {@link #getSize() size} if the iterator may + * shrink upon iteration. + */ +public class FilteringNodeIterator implements NodeIterator { + + protected final NodeIterator base; + protected final Predicate filter; + + private Node next; + private long position; + + public FilteringNodeIterator(NodeIterator base, Predicate filter) { + this.base = base; + this.filter = filter; + next = seekNext(); + } + + //-----------------------------------------------------------< Iterator >--- + /** + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() { + return next != null; + } + + /** + * @see java.util.Iterator#next() + */ + public Object next() { + return nextNode(); + } + + /** + * @see java.util.Iterator#remove() + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + //-------------------------------------------------------< NodeIterator >--- + /** + * @see javax.jcr.NodeIterator#nextNode() + */ + public Node nextNode() { + Node n = next; + if (n == null) { + throw new NoSuchElementException(); + } + next = seekNext(); + position++; + return n; + } + + //------------------------------------------------------< RangeIterator >--- + /** + * @see javax.jcr.RangeIterator#skip(long) + */ + public void skip(long skipNum) { + while (skipNum-- > 0) { + next(); + } + } + + /** + * @see javax.jcr.RangeIterator#getSize() + */ + public long getSize() { + return -1; + } + + /** + * @see javax.jcr.RangeIterator#getPosition() + */ + public long getPosition() { + return position; + } + + //-------------------------------------------------------------------------- + /** + * + * @return + */ + protected Node seekNext() { + Node n = null; + while (n == null && base.hasNext()) { + Node nextRes = base.nextNode(); + if (filter.evaluate(nextRes)) { + n = nextRes; + } + } + return n; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FrozenNodeIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FrozenNodeIteratorAdapter.java new file mode 100644 index 00000000000..93c43a2b400 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/FrozenNodeIteratorAdapter.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.commons.iterator; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionIterator; + +import javax.jcr.version.Version; + +/** + * Implements a node iterator that takes a version iterator and returns the + * frozen nodes of the underlying versions. + */ +public class FrozenNodeIteratorAdapter extends RangeIteratorAdapter implements NodeIterator { + + public FrozenNodeIteratorAdapter(VersionIterator iterator) { + super(iterator); + } + + /** + * {@inheritDoc} + * + * @return the next frozen node. + */ + public Node nextNode() { + try { + return ((Version) next()).getFrozenNode(); + } catch (RepositoryException e) { + throw (IllegalStateException) new IllegalStateException(e.toString()).initCause(e); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/LazyIteratorChain.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/LazyIteratorChain.java new file mode 100644 index 00000000000..a9b242fb94a --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/LazyIteratorChain.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.commons.iterator; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * This class implements the concatenation of iterators. The implementation + * is lazy in the sense that advancing of any iterator is deferred as much + * as possible. Specifically no iterator is fully unwrapped at one single + * point of time. + * + * @param type of values iterating over + */ +public class LazyIteratorChain implements Iterator { + private final Iterator> iterators; + private Iterator currentIterator; + private Boolean hasNext; + + /** + * Returns the concatenation of all iterators in iterators. + * + * @param + * @param iterators + * @return + */ + public static Iterator chain(Iterator> iterators) { + return new LazyIteratorChain(iterators); + } + + /** + * Returns the concatenation of all iterators in iterators. + * + * @param + * @param iterators + * @return + */ + public static Iterator chain(Iterator... iterators) { + return new LazyIteratorChain(iterators); + } + + public LazyIteratorChain(Iterator> iterators) { + super(); + this.iterators = iterators; + } + + public LazyIteratorChain(Iterator... iterators) { + super(); + this.iterators = Arrays.asList(iterators).iterator(); + } + + public boolean hasNext() { + // Memoizing the result of hasNext is crucial to performance when recursively + // traversing tree structures. + if (hasNext == null) { + while ((currentIterator == null || !currentIterator.hasNext()) && iterators.hasNext()) { + currentIterator = iterators.next(); + } + hasNext = currentIterator != null && currentIterator.hasNext(); + } + return hasNext; + } + + public T next() { + if (hasNext()) { + hasNext = null; + return currentIterator.next(); + } + else { + throw new NoSuchElementException(); + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeIterable.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeIterable.java new file mode 100644 index 00000000000..6f83a318fa5 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeIterable.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.commons.iterator; + +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Adapter class that adapts a {@link NodeIterator} instance to an + * {@link Iterable} instance that always returns the same underlying + * iterator. + * + * @since Apache Jackrabbit 2.0 + * @deprecated - Use {@link JcrUtils#in(NodeIterator)} instead + */ +@Deprecated +public class NodeIterable implements Iterable { + + /** + * The node iterator being adapted. + */ + private final NodeIterator iterator; + + /** + * Creates an iterable adapter for the given node iterator. + * + * @param iterator the node iterator to be adapted + */ + public NodeIterable(NodeIterator iterator) { + this.iterator = iterator; + } + + /** + * Returns the node iterator. + * + * @return node iterator + */ + @SuppressWarnings("unchecked") + public Iterator iterator() { + return iterator; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeIteratorAdapter.java new file mode 100644 index 00000000000..30caabb388a --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeIteratorAdapter.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.commons.iterator; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RangeIterator; + +/** + * Adapter class for turning {@link RangeIterator}s or {@link Iterator}s + * into {@link NodeIterator}s. + */ +public class NodeIteratorAdapter extends RangeIteratorDecorator + implements NodeIterator { + + /** + * Static instance of an empty {@link NodeIterator}. + */ + public static final NodeIterator EMPTY = + new NodeIteratorAdapter(RangeIteratorAdapter.EMPTY); + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link Node}s + */ + public NodeIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link Iterator}. + * + * @param iterator iterator of {@link Node}s + */ + public NodeIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + public NodeIteratorAdapter(Iterator iterator, long size) { + super(new RangeIteratorAdapter(iterator, size)); + } + + /** + * Creates an iterator for the given collection. + * + * @param collection collection of {@link Node}s + */ + public NodeIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //--------------------------------------------------------< NodeIterator > + + /** + * Returns the next node. + * + * @return next node + * @throws NoSuchElementException if there is no next node + */ + public Node nextNode() throws NoSuchElementException { + return (Node) next(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeTypeIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeTypeIteratorAdapter.java new file mode 100644 index 00000000000..b4968d5c1e6 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/NodeTypeIteratorAdapter.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.commons.iterator; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; + +/** + * Adapter class for turning {@link RangeIterator}s or {@link Iterator}s + * into {@link NodeTypeIterator}s. + */ +public class NodeTypeIteratorAdapter extends RangeIteratorDecorator + implements NodeTypeIterator { + + /** + * Static instance of an empty {@link NodeTypeIterator}. + */ + public static final NodeTypeIterator EMPTY = + new NodeTypeIteratorAdapter(RangeIteratorAdapter.EMPTY); + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link NodeType}s + */ + public NodeTypeIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link Iterator}. + * + * @param iterator iterator of {@link NodeType}s + */ + public NodeTypeIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + /** + * Creates an iterator for the given collection. + * + * @param collection collection of {@link NodeType}s + */ + public NodeTypeIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //----------------------------------------------------< NodeTypeIterator > + + /** + * Returns the next node type. + * + * @return next node type + * @throws NoSuchElementException if there is no next node type + */ + public NodeType nextNodeType() throws NoSuchElementException { + return (NodeType) next(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/PropertyIterable.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/PropertyIterable.java new file mode 100644 index 00000000000..b66cb0db193 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/PropertyIterable.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.commons.iterator; + +import java.util.Iterator; + +import javax.jcr.Property; +import javax.jcr.PropertyIterator; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Adapter class that adapts a {@link PropertyIterator} instance to an + * {@link Iterable} instance that always returns the same underlying + * iterator. + * + * @since Apache Jackrabbit 2.0 + * @deprecated - Use {@link JcrUtils#in(PropertyIterator)} instead + */ +@Deprecated +public class PropertyIterable implements Iterable { + + /** + * The property iterator being adapted. + */ + private final PropertyIterator iterator; + + /** + * Creates an iterable adapter for the given property iterator. + * + * @param iterator the property iterator to be adapted + */ + public PropertyIterable(PropertyIterator iterator) { + this.iterator = iterator; + } + + /** + * Returns the property iterator. + * + * @return property iterator + */ + @SuppressWarnings("unchecked") + public Iterator iterator() { + return iterator; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/PropertyIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/PropertyIteratorAdapter.java new file mode 100644 index 00000000000..fa964ffd1fc --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/PropertyIteratorAdapter.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.commons.iterator; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RangeIterator; + +/** + * Adapter class for turning {@link RangeIterator}s or {@link Iterator}s + * into {@link PropertyIterator}s. + */ +public class PropertyIteratorAdapter extends RangeIteratorDecorator + implements PropertyIterator { + + /** + * Static instance of an empty {@link PropertyIterator}. + */ + public static final PropertyIterator EMPTY = + new PropertyIteratorAdapter(RangeIteratorAdapter.EMPTY); + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link Property} instances + */ + public PropertyIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link Iterator}. + * + * @param iterator iterator of {@link Property} instances + */ + public PropertyIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + public PropertyIteratorAdapter(Iterator iterator, long size) { + super(new RangeIteratorAdapter(iterator, size)); + } + + /** + * Creates an iterator for the given collection. + * + * @param collection collection of {@link Property} instances + */ + public PropertyIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //----------------------------------------------------< PropertyIterator > + + /** + * Returns the next property. + * + * @return next property + * @throws NoSuchElementException if there is no next property + */ + public Property nextProperty() { + return (Property) next(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RangeIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RangeIteratorAdapter.java new file mode 100644 index 00000000000..f0f5a2f9a1e --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RangeIteratorAdapter.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.commons.iterator; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; + +/** + * Adapter for turning normal {@link Iterator}s into {@link RangeIterator}s. + * This helper class is used by the adapter classes in this package to + * implement the JCR iterator interfaces on top of normal Java iterators. + */ +public class RangeIteratorAdapter implements RangeIterator { + + /** + * Static instance of an empty {@link RangeIterator}. + */ + public static final RangeIterator EMPTY = + new RangeIteratorAdapter(Collections.EMPTY_LIST); + + /** + * The adapted iterator instance. + */ + private final Iterator iterator; + + /** + * Number of elements in the adapted iterator, or -1 if unknown. + */ + private long size; + + /** + * Current position of the iterator. + */ + private long position; + + /** + * Creates an adapter for the given iterator of the given size. + * + * @param iterator adapted iterator + * @param size size of the iterator, or -1 if unknown + */ + public RangeIteratorAdapter(Iterator iterator, long size) { + this.iterator = iterator; + this.size = size; + this.position = 0; + } + + /** + * Creates an adapter for the given iterator of unknown size. + * + * @param iterator adapted iterator + */ + public RangeIteratorAdapter(Iterator iterator) { + this(iterator, -1); + } + + /** + * Creates a {@link RangeIterator} for the given collection. + * + * @param collection the collection to iterate + */ + public RangeIteratorAdapter(Collection collection) { + this(collection.iterator(), collection.size()); + } + + //-------------------------------------------------------< RangeIterator > + + /** + * Returns the current position of the iterator. + * + * @return iterator position + */ + public long getPosition() { + return position; + } + + /** + * Returns the size of the iterator. + * + * @return iterator size, or -1 if unknown + */ + public long getSize() { + return size; + } + + /** + * Skips the given number of elements. + * + * @param n number of elements to skip + * @throws IllegalArgumentException if n is negative + * @throws NoSuchElementException if skipped past the last element + */ + public void skip(long n) + throws IllegalArgumentException, NoSuchElementException { + if (n < 0) { + throw new IllegalArgumentException("skip(" + n + ")"); + } + for (long i = 0; i < n; i++) { + next(); + } + } + + //------------------------------------------------------------< Iterator > + + /** + * Checks if this iterator has more elements. If there are no more + * elements and the size of the iterator is unknown, then the size is + * set to the current position. + * + * @return true if this iterator has more elements, + * false otherwise + */ + public boolean hasNext() { + if (iterator.hasNext()) { + return true; + } else { + if (size == -1) { + size = position; + } + return false; + } + } + + /** + * Returns the next element in this iterator and advances the iterator + * position. If there are no more elements and the size of the iterator + * is unknown, then the size is set to the current position. + * + * @return next element + * @throws NoSuchElementException if there are no more elements + */ + public Object next() throws NoSuchElementException { + try { + Object next = iterator.next(); + position++; + return next; + } catch (NoSuchElementException e) { + if (size == -1) { + size = position; + } + throw e; + } + } + + /** + * Removes the previously retrieved element. Decreases the current + * position and size of this iterator. + * + * @throws UnsupportedOperationException if removes are not permitted + * @throws IllegalStateException if there is no previous element to remove + */ + public void remove() + throws UnsupportedOperationException, IllegalStateException { + iterator.remove(); + position--; + if (size != -1) { + size--; + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RangeIteratorDecorator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RangeIteratorDecorator.java new file mode 100644 index 00000000000..6c46aa311ca --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RangeIteratorDecorator.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.commons.iterator; + +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; + +/** + * Base class for decorating {@link RangeIterator}s. + */ +public class RangeIteratorDecorator implements RangeIterator { + + /** + * The decorated iterator. + */ + private final RangeIterator iterator; + + /** + * Creates a decorated iterator. Protected since this class is only + * useful when subclassed. + * + * @param iterator the iterator to be decorated + */ + protected RangeIteratorDecorator(RangeIterator iterator) { + this.iterator = iterator; + } + + //-------------------------------------------------------< RangeIterator > + + /** + * Delegated to the underlying iterator. + * + * @return iterator position + */ + public long getPosition() { + return iterator.getPosition(); + } + + /** + * Delegated to the underlying iterator. + * + * @return iterator size + */ + public long getSize() { + return iterator.getSize(); + } + + /** + * Delegated to the underlying iterator. + * + * @param n number of elements to skip + * @throws NoSuchElementException if skipped past the last element + */ + public void skip(long n) throws NoSuchElementException { + iterator.skip(n); + } + + //------------------------------------------------------------< Iterator > + + /** + * Delegated to the underlying iterator. + * + * @return true if the iterator has more elements, + * false otherwise + */ + public boolean hasNext() { + return iterator.hasNext(); + } + + /** + * Delegated to the underlying iterator. + * + * @return next element + * @throws NoSuchElementException if there are no more elements + */ + public Object next() throws NoSuchElementException { + return iterator.next(); + } + + /** + * Delegated to the underlying iterator. + * + * @throws UnsupportedOperationException if the operation is not supported + * @throws IllegalStateException if there is no element to be removed + */ + public void remove() + throws UnsupportedOperationException, IllegalStateException { + iterator.remove(); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RowIterable.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RowIterable.java new file mode 100644 index 00000000000..849cb416a79 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RowIterable.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.commons.iterator; + +import java.util.Iterator; + +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Adapter class that adapts a {@link RowIterator} instance to an + * {@link Iterable} instance that always returns the same underlying + * iterator. + * + * @since Apache Jackrabbit 2.0 + * @deprecated - Use {@link JcrUtils#in(RowIterator)} instead + */ +@Deprecated +public class RowIterable implements Iterable { + + /** + * The row iterator being adapted. + */ + private final RowIterator iterator; + + /** + * Creates an iterable adapter for the given row iterator. + * + * @param iterator the row iterator to be adapted + */ + public RowIterable(RowIterator iterator) { + this.iterator = iterator; + } + + /** + * Returns the row iterator. + * + * @return row iterator + */ + @SuppressWarnings("unchecked") + public Iterator iterator() { + return iterator; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RowIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RowIteratorAdapter.java new file mode 100644 index 00000000000..829a12abb56 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/RowIteratorAdapter.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.commons.iterator; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +/** + * Adapter class for turning {@link RangeIterator}s or {@link Iterator}s + * into {@link RowIterator}s. + */ +public class RowIteratorAdapter extends RangeIteratorDecorator + implements RowIterator { + + /** + * Static instance of an empty {@link RowIterator}. + */ + public static final RowIterator EMPTY = + new RowIteratorAdapter(RangeIteratorAdapter.EMPTY); + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link Row}s + */ + public RowIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link Iterator}. + * + * @param iterator iterator of {@link Row}s + */ + public RowIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + /** + * Creates an iterator for the given collection. + * + * @param collection collection of {@link Row}s + */ + public RowIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //---------------------------------------------------------< RowIterator > + + /** + * Returns the next row. + * + * @return next row + * @throws NoSuchElementException if there is no next row + */ + public Row nextRow() { + return (Row) next(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/SizedIterator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/SizedIterator.java new file mode 100644 index 00000000000..fd283328b92 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/SizedIterator.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.commons.iterator; + +import java.util.Iterator; + +/** + * SizedIterator extends {@link Iterator} with a + * getSize method. + * + * @param the type of elements of this iterator + */ +public interface SizedIterator extends Iterator { + + /** + * The number of elements of this iterator or -1 if not known. + * + * @return number of elements. + */ + long getSize(); +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/VersionIteratorAdapter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/VersionIteratorAdapter.java new file mode 100644 index 00000000000..5b083472f71 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/VersionIteratorAdapter.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.commons.iterator; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; +import javax.jcr.version.Version; +import javax.jcr.version.VersionIterator; + +/** + * Adapter class for turning {@link RangeIterator}s or {@link Iterator}s + * into {@link VersionIterator}s. + */ +public class VersionIteratorAdapter extends RangeIteratorDecorator + implements VersionIterator { + + /** + * Static instance of an empty {@link VersionIterator}. + */ + public static final VersionIterator EMPTY = + new VersionIteratorAdapter(RangeIteratorAdapter.EMPTY); + + /** + * Creates an adapter for the given {@link RangeIterator}. + * + * @param iterator iterator of {@link Version}s + */ + public VersionIteratorAdapter(RangeIterator iterator) { + super(iterator); + } + + /** + * Creates an adapter for the given {@link Iterator}. + * + * @param iterator iterator of {@link Version}s + */ + public VersionIteratorAdapter(Iterator iterator) { + super(new RangeIteratorAdapter(iterator)); + } + + /** + * Creates an iterator for the given collection. + * + * @param collection collection of {@link Version}s + */ + public VersionIteratorAdapter(Collection collection) { + super(new RangeIteratorAdapter(collection)); + } + + //-----------------------------------------------------< VersionIterator > + + /** + * Returns the next version. + * + * @return next version + * @throws NoSuchElementException if there is no next version + */ + public Version nextVersion() { + return (Version) next(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/package-info.java new file mode 100644 index 00000000000..409352ad565 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/iterator/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.commons.iterator; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/SimpleReferenceBinary.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/SimpleReferenceBinary.java new file mode 100644 index 00000000000..e8644440270 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/SimpleReferenceBinary.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.commons.jackrabbit; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.api.ReferenceBinary; +import org.apache.jackrabbit.api.ReferenceBinaryException; + +public class SimpleReferenceBinary implements ReferenceBinary { + + private final String reference; + + public SimpleReferenceBinary(String reference) { + this.reference = reference; + } + + //---------------------------------------------------< ReferenceBinary >-- + + @Override + public String getReference() { + return reference; + } + + //------------------------------------------------------------< Binary >-- + + @Override + public InputStream getStream() throws RepositoryException { + throw new ReferenceBinaryException( + "Broken binary reference: " + reference); + } + + @Override + public int read(byte[] b, long position) + throws IOException, RepositoryException { + throw new ReferenceBinaryException( + "Broken binary reference: " + reference); + } + + @Override + public long getSize() throws RepositoryException { + throw new ReferenceBinaryException( + "Broken binary reference: " + reference); + } + + @Override + public void dispose() { + // ignore + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/authorization/AccessControlUtils.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/authorization/AccessControlUtils.java new file mode 100644 index 00000000000..fb6859b02e8 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/authorization/AccessControlUtils.java @@ -0,0 +1,391 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.commons.jackrabbit.authorization; + +import java.security.Principal; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlEntry; +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.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.principal.PrincipalManager; + +/** + * This class provides common access control related utilities. + */ +public class AccessControlUtils { + + /** + * Retrieves the {@link Privilege}s from the specified privilege names. + * + * @param session The editing session. + * @param privilegeNames The privilege names. + * @return An array of privileges. + * @throws RepositoryException If an error occurs or if {@code privilegeNames} + * contains an unknown/invalid privilege name. + */ + public static Privilege[] privilegesFromNames(Session session, String... privilegeNames) throws RepositoryException { + return privilegesFromNames(session.getAccessControlManager(), privilegeNames); + } + + /** + * Retrieves the {@link Privilege}s from the specified privilege names. + * + * @param accessControlManager The access control manager. + * @param privilegeNames The privilege names. + * @return An array of privileges. + * @throws RepositoryException If an error occurs or if {@code privilegeNames} + * contains an unknown/invalid privilege name. + */ + public static Privilege[] privilegesFromNames(AccessControlManager accessControlManager, String... privilegeNames) throws RepositoryException { + Set privileges = new HashSet(privilegeNames.length); + for (String privName : privilegeNames) { + privileges.add(accessControlManager.privilegeFromName(privName)); + } + return privileges.toArray(new Privilege[privileges.size()]); + } + + /** + * Retrieves the names of the specified privileges. + * + * @param privileges One or more privileges. + * @return The names of the specified privileges. + */ + public static String[] namesFromPrivileges(Privilege... privileges) { + if (privileges == null || privileges.length == 0) { + return new String[0]; + } else { + String[] names = new String[privileges.length]; + for (int i = 0; i < privileges.length; i++) { + names[i] = privileges[i].getName(); + } + return names; + } + } + + /** + * Utility that combines {@link AccessControlManager#getApplicablePolicies(String)} + * and {@link AccessControlManager#getPolicies(String)} to retrieve + * a modifiable {@code JackrabbitAccessControlList} for the given path.
    + * + * Note that the policy must be {@link AccessControlManager#setPolicy(String, + * javax.jcr.security.AccessControlPolicy) reapplied} + * and the changes must be saved in order to make the AC modifications take + * effect. + * + * @param session The editing session. + * @param absPath The absolute path of the target node. + * @return A modifiable access control list or null if there is none. + * @throws RepositoryException If an error occurs. + */ + public static JackrabbitAccessControlList getAccessControlList(Session session, String absPath) throws RepositoryException { + AccessControlManager acMgr = session.getAccessControlManager(); + return getAccessControlList(acMgr, absPath); + } + + /** + * Utility that combines {@link AccessControlManager#getApplicablePolicies(String)} + * and {@link AccessControlManager#getPolicies(String)} to retrieve + * a modifiable {@code JackrabbitAccessControlList} for the given path.
    + * + * Note that the policy must be {@link AccessControlManager#setPolicy(String, + * javax.jcr.security.AccessControlPolicy) reapplied} + * and the changes must be saved in order to make the AC modifications take + * effect. + * + * @param accessControlManager The {@code AccessControlManager} . + * @param absPath The absolute path of the target node. + * @return A modifiable access control list or null if there is none. + * @throws RepositoryException If an error occurs. + */ + public static JackrabbitAccessControlList getAccessControlList(AccessControlManager accessControlManager, String absPath) throws RepositoryException { + // try applicable (new) ACLs + AccessControlPolicyIterator itr = accessControlManager.getApplicablePolicies(absPath); + while (itr.hasNext()) { + AccessControlPolicy policy = itr.nextAccessControlPolicy(); + if (policy instanceof JackrabbitAccessControlList) { + return (JackrabbitAccessControlList) policy; + } + } + + // try if there is an acl that has been set before + AccessControlPolicy[] pcls = accessControlManager.getPolicies(absPath); + for (AccessControlPolicy policy : pcls) { + if (policy instanceof JackrabbitAccessControlList) { + return (JackrabbitAccessControlList) policy; + } + } + + // no policy found + return null; + } + + /** + * A utility method to add a new access control entry.
    + * Please note, that calling {@link javax.jcr.Session#save()} is required + * in order to persist the changes. + * + * @param session The editing session. + * @param absPath The absolute path of the target node. + * @param principal The principal to grant/deny privileges to. + * @param privilegeNames The names of the privileges to grant or deny. + * @param isAllow {@code true} to grant; {@code false} otherwise. + * @return {@code true} if the node's ACL was modified and the session has + * pending changes. + * @throws RepositoryException If an error occurs. + */ + public static boolean addAccessControlEntry(Session session, String absPath, + Principal principal, String[] privilegeNames, + boolean isAllow) throws RepositoryException { + return addAccessControlEntry(session, absPath, principal, privilegesFromNames(session, privilegeNames), isAllow); + } + + /** + * A utility method to add a new access control entry. Please note, that + * a call to {@link javax.jcr.Session#save()} is required in order + * to persist the changes. + * + * @param session The editing session + * @param absPath The absolute path of the target node. + * @param principal The principal to grant/deny privileges to. + * @param privileges The privileges to grant or deny + * @param isAllow {@code true} to grant; {@code false} otherwise; + * @return {@code true} if the node's ACL was modified and the session has + * pending changes. + * @throws RepositoryException If an error occurs. + */ + public static boolean addAccessControlEntry(Session session, String absPath, + Principal principal, Privilege[] privileges, + boolean isAllow) throws RepositoryException { + JackrabbitAccessControlList acl = getAccessControlList(session, absPath); + if (acl != null) { + if (acl.addEntry(principal, privileges, isAllow)) { + session.getAccessControlManager().setPolicy(absPath, acl); + return true; + } // else: not modified + } // else: no acl found. + + return false; + } + + /** + * Utility to grant jcr:all privilege to the everyone group principal. + * Please note, that {@link javax.jcr.Session#save()} is required in order + * to persist the changes. + * + * @param session The editing session. + * @param absPath The absolute path of the target node + * @return {@code true} if the node's access control list was modified; + * {@code false} otherwise; + * @throws RepositoryException If an error occurs. + */ + public static boolean grantAllToEveryone(Session session, String absPath) throws RepositoryException { + Principal everyone = getEveryonePrincipal(session); + Privilege[] privileges = privilegesFromNames(session, Privilege.JCR_ALL); + return addAccessControlEntry(session, absPath, everyone, privileges, true); + } + + /** + * Utility to deny jcr:all privilege to the everyone group principal. + * Please note, that {@link javax.jcr.Session#save()} is required in order + * to persist the changes. + * + * @param session The editing session. + * @param absPath The absolute path of the target node + * @return {@code true} if the node's access control list was modified; + * {@code false} otherwise; + * @throws RepositoryException If an error occurs. + */ + public static boolean denyAllToEveryone(Session session, String absPath) throws RepositoryException { + Principal everyone = getEveryonePrincipal(session); + Privilege[] privileges = privilegesFromNames(session, Privilege.JCR_ALL); + return addAccessControlEntry(session, absPath, everyone, privileges, false); + } + + /** + * Allow certain privileges on a given node for a given principal. + * + *

    To activate the ACL change, session.save() must be called.

    + * + * @param node node to set the resource-based ACL entry on; underlying session is used to write the ACL + * @param principalName Name of the principal for which the ACL entry should apply + * @param privileges list of privileges to set by name (see {@link javax.jcr.security.Privilege}) + * @return {@code true} if the node's ACL was modified and the session has pending changes. + * @throws RepositoryException If an unexpected repository error occurs + */ + public static boolean allow(Node node, String principalName, String... privileges) throws RepositoryException { + return addAccessControlEntry( + node.getSession(), + node.getPath(), + getPrincipal(node.getSession(), principalName), + privileges, + true // allow + ); + } + + /** + * Deny certain privileges on a node for a given principal. + * + *

    To activate the ACL change, session.save() must be called.

    + * + * @param node node to set the resource-based ACL entry on; underlying session is used to write the ACL + * @param principalName Name of the principal for which the ACL entry should apply + * @param privileges list of privileges to set by name (see {@link javax.jcr.security.Privilege}) + * @return {@code true} if the node's ACL was modified and the session has pending changes. + * @throws RepositoryException If an unexpected repository error occurs + */ + public static boolean deny(Node node, String principalName, String... privileges) throws RepositoryException { + return addAccessControlEntry( + node.getSession(), + node.getPath(), + getPrincipal(node.getSession(), principalName), + privileges, + false // deny + ); + } + + /** + * Removes all ACL entries for a principal at a given absolute path. If the specified + * {@code principalName} is {@code null} the policy will be removed altogether. + *

    Modifications only take effect upon {@code Session.save()}.

    + * + * @param session The editing session. + * @param absPath Absolute path of an existing node from which to remove ACL entries (or the policy) + * @param principalName Name of the principal whose entries should be removed; + * use {@code null} to clear the policy. + * @return {@code true} if the policy has been modified; {@code false} otherwise. + * @throws RepositoryException If an unexpected repository error occurs + */ + public static boolean clear(Session session, String absPath, String principalName) throws RepositoryException { + AccessControlManager acm = session.getAccessControlManager(); + JackrabbitAccessControlList acl = null; + // only clear if there is an existing acl (no need to retrieve applicable policies) + AccessControlPolicy[] pcls = acm.getPolicies(absPath); + for (AccessControlPolicy policy : pcls) { + if (policy instanceof JackrabbitAccessControlList) { + acl = (JackrabbitAccessControlList) policy; + } + } + if (acl != null) { + if (principalName == null) { + acm.removePolicy(absPath, acl); + return true; + } else { + Principal principal = getPrincipal(session, principalName); + if (principal == null) { + return false; + } + boolean removedEntries = false; + // remove all existing entries for principal + for (AccessControlEntry ace : acl.getAccessControlEntries()) { + if (ace.getPrincipal().equals(principal)) { + acl.removeAccessControlEntry(ace); + removedEntries = true; + } + } + if (removedEntries) { + acm.setPolicy(absPath, acl); + return true; + } + } + } + return false; + } + + /** + * Removes all ACL entries for a principal on a given node. + * + *

    Modification to the policy only take effect upon {@code Session.save()} must be called.

    + * + * @param node node from which to remove ACL entries; underlying session is used to write the changes + * @param principalName Name of the principal whose entries should be removed; use {@code null} to clear the policy altogether. + * @return {@code true} if the node's ACL was modified, {@code false} otherwise. + * @throws RepositoryException If an unexpected repository error occurs + */ + public static boolean clear(Node node, String principalName) throws RepositoryException { + return clear(node.getSession(), node.getPath(), principalName); + } + + /** + * Removes the access control list at a given node. + *

    To persist the modifications, {@code Session.save()} must be called.

    + * + * @param node node from which to remove the ACL; underlying session is used to write the changes + * @return {@code true} if the node's ACL was removed, {@code false} otherwise. + * @throws RepositoryException If an unexpected repository error occurs + */ + public static boolean clear(Node node) throws RepositoryException { + return clear(node, null); + } + + /** + * Removes the access control list at the specified absolute path. + *

    To persist the modification, session.save() must be called.

    + * + * @param session The editing session. + * @param absPath An absolute path of a valid node accessible to the editing session from which to remove the ACL. + * @return {@code true} if the node's ACL got removed, {@code false} otherwise. + * @throws RepositoryException If an unexpected repository error occurs + */ + public static boolean clear(Session session, String absPath) throws RepositoryException { + return clear(session, absPath, null); + } + + /** + * Retrieves the principal with the specified {@code principalName}. Shortcut + * for calling {@link PrincipalManager#getPrincipal(String)}. + * + * @param session The editing session which must be a {@code JackrabbitSession}. + * @param principalName The name of the principal. + * @return The principal with the specified name or {@code null} if no such principal exists. + * @throws RepositoryException If an error occurs or if the session is not a {@code JackrabbitSession}. + */ + public static Principal getPrincipal(Session session, String principalName) throws RepositoryException { + if (session instanceof JackrabbitSession) { + return ((JackrabbitSession) session).getPrincipalManager().getPrincipal(principalName); + } else { + throw new UnsupportedOperationException("Failed to retrieve principal: JackrabbitSession expected."); + } + } + + /** + * Shortcut for calling {@link PrincipalManager#getEveryone()}. + * + * @param session The editing session which must be a {@code JackrabbitSession}. + * @return The group principal presenting everyone. + * @throws RepositoryException If an error occurs or if the session is not a {@code JackrabbitSession}. + */ + public static Principal getEveryonePrincipal(Session session) throws RepositoryException { + if (session instanceof JackrabbitSession) { + return ((JackrabbitSession) session).getPrincipalManager().getEveryone(); + } else { + throw new UnsupportedOperationException("Failed to retrieve everyone principal: JackrabbitSession expected."); + } + } +} + diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/authorization/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/authorization/package-info.java new file mode 100755 index 00000000000..dece4db4392 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/authorization/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.commons.jackrabbit.authorization; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/package-info.java new file mode 100755 index 00000000000..e0ec3c40190 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/package-info.java @@ -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. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.commons.jackrabbit; + diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/user/AuthorizableQueryManager.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/user/AuthorizableQueryManager.java new file mode 100644 index 00000000000..4d076359a3f --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/user/AuthorizableQueryManager.java @@ -0,0 +1,749 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.jackrabbit.user; + +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.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.commons.json.JsonHandler; +import org.apache.jackrabbit.commons.json.JsonParser; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; + +/** + * This class handles the translation of queries for users and groups from a + * JSON format to the query model of Jackrabbit's user groups search + * (see {@link org.apache.jackrabbit.api.security.user.UserManager#findAuthorizables(org.apache.jackrabbit.api.security.user.Query) + * UserManager#findAuthorizables(Query)}). + * + * The JSON query format is defined as follows: + *
    +{
    +  ( selector: "authorizable" | "user" | "group" )?        // Defaults to "authorizable", see QueryBuilder#setSelector()
    +
    +  (
    +    scope:                                                // See QueryBuilder#setScope()
    +    {
    +      groupName: /* group name (String) * /
    +      ( declaredOnly: true | false )                      // Defaults to true
    +    }
    +  ) ?                                                     // Defaults to all
    +
    +  ( condition: [ CONJUNCTION+ ] ) ?                       // Defaults to a 'true' condition, see QueryBuilder#setCondition()
    +
    +  (
    +    order | sort:                                         // See QueryBuilder#setOrder()
    +    {
    +      property: /* relative path (String) * /
    +      ( direction: "asc" | "desc" )                       // Defaults to "asc"
    +      ( ignoreCase: true | false )                        // Defaults to "true", see QueryBuilder#setSortOrder()
    +    }
    +  ) ?                                                     // Defaults to document order
    +
    +  (
    +    limit:                                                // See QueryBuilder#setLimit()
    +    {
    +      offset: /* Positive Integer * /                     // Takes precedence over bound if both are given
    +      bound:  /* String, Number, Boolean * /
    +      max:    /* Positive Integer or -1 * /               // Defaults to no limit (-1)
    +    }
    +  ) ?                                                     // Defaults to all
    +}
    +
    +CONJUNCTION ::= COMPOUND | PRIMITIVE
    +COMPOUND    ::= [ PRIMITIVE+ ]
    +PRIMITIVE   ::= { ATOM | NEGATION }
    +NEGATION    ::= not: { ATOM }                             // See QueryBuilder#not()
    +ATOM        ::= named: /* pattern * /                     // Users, groups of that name. See QueryBuilder#nameMatches()
    +            |   exists: /* relative path * /              // See QueryBuilder#exists()
    +            |   impersonates: /* authorizable name * /    // See QueryBuilder#impersonates()
    +            |   RELOP:
    +                {
    +                  property: /* relative path * /
    +                  value: /* String, Number, Boolean * /   // According to the type of the property
    +                }
    +            |   like:                                     // See QueryBuilder#like()
    +                {
    +                  property: /* relative path * /
    +                  pattern: /* pattern * /
    +                }
    +            |   contains:                                 // See QueryBuilder#contains()
    +                {
    +                  property: /* relative path * /
    +                  expression: /* search expression * /
    +                }
    +RELOP       ::= neq | eq | lt | le | gt | ge              // See QueryBuilder#neq(), QueryBuilder#eq(), ...
    +
    + * + *
      + *
    • A relative path refers to a property or a child node of an user or a group. Property names need to be + * prefixed with the at (@) character. Invalid JCR characters need proper escaping. The current path is denoted + * by a dot (.).
    • + *
    • In a 'pattern' the percent character (%) represents any string of zero or more characters and the underscore + * character (_) represents any single character. Any literal use of these characters and the backslash + * character (\) must be escaped with a backslash character. The pattern is matched against + * Authorizable#getID() and Authorizable#getPrincipal().
    • + *
    • The syntax of 'expression' is [-]value { [OR] [-]value }.
    • + *
    + */ +public class AuthorizableQueryManager { + + /** + * Constant defining the default maximal size of the result set. + */ + public static final int MAX_RESULT_COUNT = 2000; + + private final UserManager userManager; + private final ValueFactory valueFactory; + + public AuthorizableQueryManager(UserManager userManager, ValueFactory valueFactory) { + this.userManager = userManager; + this.valueFactory = valueFactory; + } + + public Iterator execute(final String query) throws RepositoryException, IOException { + try { + return userManager.findAuthorizables(new Query() { + public void build(QueryBuilder builder) { + try { + // Must request more than MAX_RESULT_COUNT records explicitly + builder.setLimit(0, MAX_RESULT_COUNT); + new QueryTranslator(builder).translate(query); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + }); + } catch (IllegalArgumentException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else { + throw e; + } + } + } + + //------------------------------------------------------------< private >--- + + private class QueryTranslator implements JsonHandler { + private final QueryBuilder queryBuilder; + private final Stack handlers = new Stack(); + + public QueryTranslator(QueryBuilder queryBuilder) { + this.queryBuilder = queryBuilder; + handlers.push(new HandlerBase() { + @Override + public void object() { + handlers.push(new ClausesHandler()); + } + }); + } + + public void translate(String query) throws IOException { + new JsonParser(this).parse(query); + if (handlers.size() != 1) { + throw new IOException("Missing closing parenthesis"); + } + } + + public void object() throws IOException { + handlers.peek().object(); + } + + public void endObject() throws IOException { + handlers.peek().endObject(); + } + + public void array() throws IOException { + handlers.peek().array(); + } + + public void endArray() throws IOException { + handlers.peek().endArray(); + } + + public void key(String s) throws IOException { + handlers.peek().key(s); + } + + public void value(String s) throws IOException { + handlers.peek().value(s); + } + + public void value(boolean b) throws IOException { + handlers.peek().value(b); + } + + public void value(long l) throws IOException { + handlers.peek().value(l); + } + + public void value(double v) throws IOException { + handlers.peek().value(v); + } + + private Value valueFor(String s) { + return valueFactory.createValue(s); + } + + private Value valueFor(boolean b) { + return valueFactory.createValue(b); + } + + private Value valueFor(long l) { + return valueFactory.createValue(l); + } + + private Value valueFor(double v) { + return valueFactory.createValue(v); + } + + //----------------------------------------------------< HandlerBase >--- + + private class HandlerBase implements JsonHandler { + + public void object() throws IOException { + throw new IOException("Syntax error: '{'"); + } + + public void endObject() throws IOException { + throw new IOException("Syntax error: '}'"); + } + + public void array() throws IOException { + throw new IOException("Syntax error: '['"); + } + + public void endArray() throws IOException { + throw new IOException("Syntax error: ']'"); + } + + public void key(String s) throws IOException { + throw new IOException("Syntax error: key '" + s + '\''); + } + + public void value(String s) throws IOException { + throw new IOException("Syntax error: string '" + s + '\''); + } + + public void value(boolean b) throws IOException { + throw new IOException("Syntax error: boolean '" + b + '\''); + } + + public void value(long l) throws IOException { + throw new IOException("Syntax error: long '" + l + '\''); + } + + public void value(double v) throws IOException { + throw new IOException("Syntax error: double '" + v + '\''); + } + } + + //-------------------------------------------------< ClausesHandler >--- + + private class ClausesHandler extends HandlerBase { + private String currentKey; + + @Override + public void object() throws IOException { + handlers.push(handlerFor(currentKey)); + } + + @Override + public void endObject() throws IOException { + handlers.pop(); + } + + @Override + public void array() throws IOException { + handlers.push(handlerFor(currentKey)); + } + + @Override + public void endArray() throws IOException { + handlers.pop(); + } + + @Override + public void key(String s) throws IOException { + currentKey = s; + } + + @Override + public void value(String s) throws IOException { + if ("selector".equals(currentKey)) { + queryBuilder.setSelector(selectorFor(s)); + } else { + throw new IOException("String value '" + s + "' is invalid for '" + currentKey + '\''); + } + } + + private Class selectorFor(String selector) throws IOException { + if ("user".equals(selector)) { + return User.class; + } else if ("group".equals(selector)) { + return Group.class; + } else if ("authorizable".equals(selector)) { + return Authorizable.class; + } else { + throw new IOException("Invalid selector '" + selector + '\''); + } + } + + private JsonHandler handlerFor(String key) throws IOException { + if ("scope".equals(key)) { + return new ScopeHandler(); + } else if ("condition".equals(key)) { + return new ConditionHandler(); + } else if ("order".equals(key) || "sort".equals(key)) { + return new OrderHandler(); + } else if ("limit".equals(key)) { + return new LimitHandler(); + } else { + throw new IOException("Invalid clause '" + key + '\''); + } + } + } + + //---------------------------------------------------< ScopeHandler >--- + + private class ScopeHandler extends HandlerBase { + private String currentKey; + private String groupName; + private Boolean declaredOnly; + + @Override + public void endObject() throws IOException { + if (groupName == null) { + throw new IOException("Missing groupName"); + } else { + queryBuilder.setScope(groupName, declaredOnly == null ? true : declaredOnly); + } + handlers.pop(); + } + + @Override + public void key(String s) throws IOException { + currentKey = s; + } + + @Override + public void value(String s) throws IOException { + if ("groupName".equals(currentKey)) { + groupName = s; + } else { + throw new IOException("Unexpected: '" + currentKey + ':' + s + '\''); + } + } + + @Override + public void value(boolean b) throws IOException { + if ("declaredOnly".equals(currentKey)) { + declaredOnly = b; + } else { + throw new IOException("Unexpected: '" + currentKey + ':' + b + '\''); + } + } + } + + //-----------------------------------------------< ConditionHandler >--- + + private class ConditionHandler extends HandlerBase { + private final List memberHandlers = new ArrayList(); + + @Override + public void object() throws IOException { + PrimitiveHandler memberHandler = new PrimitiveHandler(); + memberHandlers.add(memberHandler); + handlers.push(memberHandler); + } + + @Override + public void array() throws IOException { + CompoundHandler memberHandler = new CompoundHandler(); + memberHandlers.add(memberHandler); + handlers.push(memberHandler); + } + + @Override + public void endArray() throws IOException { + if (memberHandlers.isEmpty()) { + throw new IOException("Empty search term"); + } + + Iterator memberHandler = memberHandlers.iterator(); + T condition = memberHandler.next().getCondition(); + while (memberHandler.hasNext()) { + condition = queryBuilder.and(condition, memberHandler.next().getCondition()); + } + + queryBuilder.setCondition(condition); + + handlers.pop(); + } + + } + + //--------------------------------------------------< ConditionBase >--- + + private abstract class ConditionBase extends HandlerBase { + public abstract T getCondition(); + } + + //------------------------------------------------< CompoundHandler >--- + + private class CompoundHandler extends ConditionBase { + private final List memberHandlers = new ArrayList(); + + @Override + public void object() throws IOException { + PrimitiveHandler memberHandler = new PrimitiveHandler(); + memberHandlers.add(memberHandler); + handlers.push(memberHandler); + } + + @Override + public void endArray() throws IOException { + if (memberHandlers.isEmpty()) { + throw new IOException("Empty search term"); + } + + handlers.pop(); + } + + @Override + public T getCondition() { + Iterator memberHandler = memberHandlers.iterator(); + T condition = memberHandler.next().getCondition(); + while (memberHandler.hasNext()) { + condition = queryBuilder.or(condition, memberHandler.next().getCondition()); + } + + return condition; + } + } + + //-----------------------------------------------< PrimitiveHandler >--- + + private class PrimitiveHandler extends ConditionBase { + private String currentKey; + private ConditionBase relOp; + private ConditionBase not; + private T condition; + + @Override + public void object() throws IOException { + if (hasCondition()) { + throw new IOException("Condition on '" + currentKey + "' not allowed since another " + + "condition is already set"); + } + + if ("not".equals(currentKey)) { + not = new PrimitiveHandler(); + handlers.push(not); + } else { + relOp = new RelOpHandler(currentKey); + handlers.push(relOp); + } + } + + @Override + public void endObject() throws IOException { + if (!hasCondition()) { + throw new IOException("Missing term"); + } + + if (relOp != null) { + condition = relOp.getCondition(); + } else if (condition == null) { + condition = queryBuilder.not(not.getCondition()); + } + + handlers.pop(); + } + + @Override + public void key(String s) throws IOException { + currentKey = s; + } + + @Override + public void value(String s) throws IOException { + if (hasCondition()) { + throw new IOException("Condition on '" + currentKey + "' not allowed since another " + + "condition is already set"); + } + + if ("named".equals(currentKey)) { + condition = queryBuilder.nameMatches(s); + } else if ("exists".equals(currentKey)) { + condition = queryBuilder.exists(s); + } else if ("impersonates".equals(currentKey)) { + condition = queryBuilder.impersonates(s); + } else { + throw new IOException("Invalid condition '" + currentKey + '\''); + } + } + + private boolean hasCondition() { + return condition != null || relOp != null || not != null; + } + + @Override + public T getCondition() { + return condition; + } + } + + //---------------------------------------------------< RelOpHandler >--- + + private class RelOpHandler extends ConditionBase { + private final String op; + + private String currentKey; + private String property; + private String pattern; + private String expression; + private Value value; + private T condition; + + public RelOpHandler(String op) { + this.op = op; + } + + @Override + public void endObject() throws IOException { + if (property == null) { + throw new IOException("Property not set for condition '" + op + '\''); + } + + if ("like".equals(op)) { + if (pattern == null) { + throw new IOException("Pattern not set for 'like' condition"); + } + condition = queryBuilder.like(property, pattern); + } else if ("contains".equals(op)) { + if (expression == null) { + throw new IOException("Expression not set for 'contains' condition"); + } + condition = queryBuilder.contains(property, expression); + } else { + if (value == null) { + throw new IOException("Value not set for '" + op + "' condition"); + } + + if ("eq".equals(op)) { + condition = queryBuilder.eq(property, value); + } else if ("neq".equals(op)) { + condition = queryBuilder.neq(property, value); + } else if ("lt".equals(op)) { + condition = queryBuilder.lt(property, value); + } else if ("le".equals(op)) { + condition = queryBuilder.le(property, value); + } else if ("ge".equals(op)) { + condition = queryBuilder.ge(property, value); + } else if ("gt".equals(op)) { + condition = queryBuilder.gt(property, value); + } else { + throw new IOException("Invalid condition: '" + op + '\''); + } + } + + handlers.pop(); + } + + @Override + public void key(String s) throws IOException { + currentKey = s; + } + + @Override + public void value(String s) throws IOException { + if ("property".equals(currentKey)) { + property = s; + } else if ("pattern".equals(currentKey)) { + pattern = s; + } else if ("expression".equals(currentKey)) { + expression = s; + } else if ("value".equals(currentKey)) { + value = valueFor(s); + } else { + throw new IOException("Expected one of 'property', 'pattern', 'expression', 'value' " + + "but found '" + currentKey + '\''); + } + } + + @Override + public void value(boolean b) throws IOException { + if ("value".equals(currentKey)) { + value = valueFor(b); + } else { + throw new IOException("Expected 'value', found '" + currentKey + '\''); + } + } + + @Override + public void value(long l) throws IOException { + if ("value".equals(currentKey)) { + value = valueFor(l); + } else { + throw new IOException("Expected 'value', found '" + currentKey + '\''); + } + } + + @Override + public void value(double v) throws IOException { + if ("value".equals(currentKey)) { + value = valueFor(v); + } else { + throw new IOException("Expected 'value', found '" + currentKey + '\''); + } + } + + @Override + public T getCondition() { + return condition; + } + } + + //---------------------------------------------------< OrderHandler >--- + + private class OrderHandler extends HandlerBase { + private String currentKey; + private String property; + private QueryBuilder.Direction direction; + private boolean ignoreCase = true; + + @Override + public void endObject() throws IOException { + if (property == null) { + throw new IOException("Missing property"); + } else { + queryBuilder.setSortOrder(property, + direction == null ? QueryBuilder.Direction.ASCENDING : direction, + ignoreCase); + } + handlers.pop(); + } + + @Override + public void key(String s) throws IOException { + currentKey = s; + } + + @Override + public void value(String s) throws IOException { + if ("property".equals(currentKey)) { + property = s; + } else if ("direction".equals(currentKey)) { + direction = directionFor(s); + } else if ("ignoreCase".equals(currentKey)) { + ignoreCase = Boolean.valueOf(s); + } else { + throw new IOException("Unexpected: '" + currentKey + ':' + s + '\''); + } + } + + private QueryBuilder.Direction directionFor(String direction) throws IOException { + if ("asc".equals(direction)) { + return QueryBuilder.Direction.ASCENDING; + } else if ("desc".equals(direction)) { + return QueryBuilder.Direction.DESCENDING; + } else { + throw new IOException("Invalid direction '" + direction + '\''); + } + } + } + + //---------------------------------------------------< LimitHandler >--- + + private class LimitHandler extends HandlerBase { + private String currentKey; + private Long offset; + private Value bound; + private Long max; + + @Override + public void endObject() throws IOException { + if (offset != null) { + queryBuilder.setLimit(offset, max == null ? -1 : max); + } else if (bound != null) { + queryBuilder.setLimit(bound, max == null ? -1 : max); + } else { + throw new IOException("Missing bound or offset"); + } + handlers.pop(); + } + + @Override + public void key(String s) throws IOException { + currentKey = s; + } + + @Override + public void value(String s) throws IOException { + if ("bound".equals(currentKey)) { + bound = valueFor(s); + } else { + throw new IOException("Unexpected: '" + currentKey + ':' + s + '\''); + } + } + + @Override + public void value(boolean b) throws IOException { + if ("bound".equals(currentKey)) { + bound = valueFor(b); + } else { + throw new IOException("Unexpected: '" + currentKey + ':' + b + '\''); + } + } + + @Override + public void value(long l) throws IOException { + if ("bound".equals(currentKey)) { + bound = valueFor(l); + } else if ("offset".equals(currentKey)) { + offset = l; + } else if ("max".equals(currentKey)) { + max = l; + } else { + throw new IOException("Unexpected: '" + currentKey + ':' + l + '\''); + } + } + + @Override + public void value(double v) throws IOException { + if ("bound".equals(currentKey)) { + bound = valueFor(v); + } else { + throw new IOException("Unexpected: '" + currentKey + ':' + v + '\''); + } + } + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/user/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/user/package-info.java new file mode 100644 index 00000000000..6c5e120f0e2 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/jackrabbit/user/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.3") +package org.apache.jackrabbit.commons.jackrabbit.user; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonHandler.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonHandler.java new file mode 100644 index 00000000000..0f55db30c47 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonHandler.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.commons.json; + +import java.io.IOException; + +/** + * The JSONHandler interface receives notifications from the + * JsonParser. + */ +public interface JsonHandler { + + /** + * Receive notification about the start of an JSON object. + * + * @throws IOException If an error occurs. + */ + void object() throws IOException; + + /** + * Receive notification about the end of an JSON object. + * + * @throws IOException If an error occurs. + */ + void endObject() throws IOException; + + /** + * Receive notification about the start of an JSON array. + * + * @throws IOException If an error occurs. + */ + void array() throws IOException; + + /** + * Receive notification about the end of an JSON array. + * + * @throws IOException If an error occurs. + */ + void endArray() throws IOException; + + /** + * Receive notification about the given JSON key. + * + * @param key The key. + * @throws IOException If an error occurs. + */ + void key(String key) throws IOException; + + /** + * Receive notification about the given JSON String value. + * + * @param value The value. + * @throws IOException If an error occurs. + */ + void value(String value) throws IOException; + + /** + * Receive notification about the given JSON boolean value. + * + * @param value The value. + * @throws IOException If an error occurs. + */ + void value(boolean value) throws IOException; + + /** + * Receive notification about the given JSON number value (long). + * + * @param value The value. + * @throws IOException If an error occurs. + */ + void value(long value) throws IOException; + + /** + * Receive notification about the given JSON number value (double). + * + * @param value The value. + * @throws IOException If an error occurs. + */ + void value(double value) throws IOException; +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonParser.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonParser.java new file mode 100644 index 00000000000..529d48690b6 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonParser.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.json; + +import java.io.BufferedReader; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.util.Stack; + +/** + * JsonParser parses and validates the JSON object passed upon + * {@link #parse(String)} or {@link #parse(InputStream, String)} and notifies + * the specified JsonHandler + */ +public class JsonParser { + + private static final String NULL = "null"; + private static final int EOF = -1; + + private static final int KEY_START = 1; + private static final int VALUE_START = 2; + private static final int VALUE = 4; + + private static final Integer OBJECT = new Integer(8); + private static final Integer ARRAY = new Integer(32); + + /* the handler */ + private final JsonHandler handler; + + /** + * Create a new JSONParser with the specified JSONHandler. + * + * @param jsonHandler A JSONHandler + */ + public JsonParser(JsonHandler jsonHandler) { + handler = jsonHandler; + } + + /** + * + * @param str String to be parsed + * @throws IOException If an error occurs. + */ + public void parse(String str) throws IOException { + parse(new BufferedReader(new StringReader(str))); + } + + /** + * + * @param input InputStream to be parsed. + * @param charSetName Name of the charset to be used. + * @throws IOException If an error occurs. + */ + public void parse(InputStream input, String charSetName) throws IOException { + parse(new BufferedReader(new InputStreamReader(input, charSetName))); + } + + /** + * + * @param reader The reader + * @throws IOException If an error occurs. + */ + public void parse(Reader reader) throws IOException { + + //StringBuffer key = new StringBuffer(); + StringBuffer value = new StringBuffer(); + + int state; + Stack complexVStack = new Stack(); + + int next = reader.read(); + if (next == '{') { + handler.object(); + complexVStack.push(OBJECT); + state = KEY_START; + next = readIgnoreWhitespace(reader); + } else { + throw new IOException("JSON object must start with a '{'"); + } + + + while (next != EOF) { + switch (state) { + + case KEY_START: + if (next == '"') { + String key = nextString(reader, '\"'); + next = readIgnoreWhitespace(reader); + if (next == ':') { + handler.key(key); + state = VALUE_START; + } else { + throw new IOException("Key-Value pairs must be separated by ':'"); + } + next = readIgnoreWhitespace(reader); + } else if (next == '}') { + // empty object + state = VALUE; + } else { + throw new IOException("Key must be in String format (double quotes)"); + } + break; + + case VALUE_START: + if (next == '[') { + handler.array(); + complexVStack.push(ARRAY); + // status still value_start + next = readIgnoreWhitespace(reader); + } else if (next == '{') { + handler.object(); + complexVStack.push(OBJECT); + state = KEY_START; + next = readIgnoreWhitespace(reader); + } else if (next == '\"') { + handler.value(nextString(reader, '\"')); + next = readIgnoreWhitespace(reader); + if (!(next == ',' || next == ']' || next == '}')) { + throw new IOException("Invalid json format"); + } + } else { + // start of boolean/long/double/null value + // will be notified as key-value pair + state = VALUE; + } + break; + + case VALUE: + if (next == '"') { + throw new IOException("Invalid json format"); + } else if (next == ',') { + state = (complexVStack.peek() == OBJECT) ? KEY_START : VALUE_START; + value = resetValue(value); + next = readIgnoreWhitespace(reader); + } else if (next == ']') { + if (complexVStack.pop() != ARRAY) { + throw new IOException("Invalid json format: Unexpected array termination."); + } + value = resetValue(value); + handler.endArray(); + + next = readIgnoreWhitespace(reader); + if (!(next == ',' || next == '}' || next == ']')) { + throw new IOException("Invalid json format"); + } + } else if (next == '}') { + if (complexVStack.pop() != OBJECT) { + throw new IOException("Invalid json format: Unexpected object termination."); + } + value = resetValue(value); + handler.endObject(); + + next = readIgnoreWhitespace(reader); + if (!(next == ',' || next == '}' || next == ']' || next == EOF)) { + throw new IOException("Invalid json format"); + } + } else { + // simple value + value.append((char) next); + next = reader.read(); + } + break; + } + } + + // EOF reached -> minimal validation check + if (value.length() != 0) { + throw new IOException("Invalid json format"); + } + } + + /** + * Return the characters up to the next close quote character. + * Backslash processing is done. The formal JSON format does not + * allow strings in single quotes, but an implementation is allowed to + * accept them. + * + * @param r The reader. + * @param quote The quoting character, either + * " (double quote) or + * ' (single quote). + * @return A String. + * @throws IOException Unterminated string. + */ + private static String nextString(Reader r, char quote) throws IOException { + int c; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = r.read(); + switch (c) { + case EOF: + case '\n': + case '\r': + throw new IOException("Unterminated string"); + case '\\': + c = r.read(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + sb.append((char)Integer.parseInt(next(r, 4), 16)); + break; + case 'x' : + sb.append((char) Integer.parseInt(next(r, 2), 16)); + break; + default: + sb.append((char) c); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append((char) c); + } + } + } + + private static String next(Reader r, int n) throws IOException { + StringBuffer b = new StringBuffer(n); + while (n-- > 0) { + int c = r.read(); + if (c < 0) { + throw new EOFException(); + } + b.append((char) c); + } + return b.toString(); + } + + /** + * Get the next char in the string, skipping whitespace. + * + * @param reader The reader + * @return A character, or -1 if there are no more characters. + * @throws IOException If an error occurs. + */ + private static int readIgnoreWhitespace(Reader reader) throws IOException { + int next; + do { + next = reader.read(); + } while (next == ' ' || next == '\n' || next == '\r' || next == '\t'); + return next; + } + + private StringBuffer resetValue(StringBuffer value) throws IOException { + if (value != null && value.length() > 0) { + String v = value.toString(); + if (NULL.equals(v)) { + handler.value(null); + } else if (v.equalsIgnoreCase("true")) { + handler.value(true); + } else if (v.equalsIgnoreCase("false")) { + handler.value(false); + } else if (v.indexOf('.') > -1) { + double d = Double.parseDouble(v); + handler.value(d); + } else { + long l = Long.parseLong(v); + handler.value(l); + } + } + return new StringBuffer(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonUtil.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonUtil.java new file mode 100644 index 00000000000..e6805802715 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/JsonUtil.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.commons.json; + +/** + * JSON utilities. + */ +public class JsonUtil { + + /** + * Generate a valid JSON string from the given str. + * + * @param str A String + * @return JSON string surrounded by double quotes. + * @see RFC 4627 + */ + public static String getJsonString(String str) { + if (str == null || str.length() == 0) { + return "\"\""; + } + + int len = str.length(); + StringBuffer sb = new StringBuffer(len + 2); + // leading quote + sb.append('"'); + // append passed string escaping characters as required + for (int i = 0; i < len; i++) { + char c = str.charAt(i); + switch (c) { + // reverse solidus and double quote + case '\\': + case '"': + sb.append('\\').append(c); + break; + // tab, line breaking chars and backspace + 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; + // other control characters and 'unescaped' + default: + if (c < 32) { + // control characters except those already covered above. + String uc = Integer.toHexString(c); + sb.append("\\u"); + int uLen = uc.length(); + while (uLen++ < 4) { + sb.append('0'); + } + sb.append(uc); + } else { + // unescaped = %x20-21 / %x23-5B / %x5D-10FFFF + sb.append(c); + } + } + } + // trailing quote + sb.append('"'); + return sb.toString(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/package-info.java new file mode 100644 index 00000000000..4a113ffa027 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/json/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.2") +package org.apache.jackrabbit.commons.json; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/EventTracker.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/EventTracker.java new file mode 100644 index 00000000000..ad0304a35c9 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/EventTracker.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.commons.observation; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; + +/** + * Event decorator that tracks whether user and date information is being + * accessed from external events or without checking for externality. + * + * @see ListenerTracker + */ +class EventTracker implements Event { + + /** The enclosing listener tracker */ + private final ListenerTracker listener; + + protected final Event event; + + protected final AtomicBoolean externalAccessed = + new AtomicBoolean(); + + public EventTracker(ListenerTracker listenerTracker, Event event) { + listener = listenerTracker; + this.event = event; + } + + private void userInfoAccessed() { + if (!externalAccessed.get() + && !listener.userInfoAccessedWithoutExternalsCheck.getAndSet(true)) { + listener.warn("Event listener " + listener + " is trying" + + " to access user information of event " + event + + " without checking whether the event is external."); + } + if (eventIsExternal() + && !listener.userInfoAccessedFromExternalEvent.getAndSet(true)) { + listener.warn("Event listener " + listener + " is trying" + + " to access user information of external event " + + event + "."); + } + } + + private void dateInfoAccessed() { + if (!externalAccessed.get() + && !listener.dateAccessedWithoutExternalsCheck.getAndSet(true)) { + listener.warn("Event listener " + listener + " is trying" + + " to access date information of event " + event + + " without checking whether the event is external."); + } + if (eventIsExternal() + && !listener.dateAccessedFromExternalEvent.getAndSet(true)) { + listener.warn("Event listener " + listener + " is trying" + + " to access date information of external event " + + event + "."); + } + } + + protected boolean eventIsExternal() { + return false; + } + + @Override + public String toString() { + return event.toString(); + } + + @Override + public int hashCode() { + return event.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (other instanceof EventTracker) { + return event.equals(other); + } else { + return false; + } + } + + //---------------------------------------------------------< Event >-- + + @Override + public int getType() { + return event.getType(); + } + + @Override + public String getPath() throws RepositoryException { + return event.getPath(); + } + + @Override + public String getUserID() { + userInfoAccessed(); + return event.getUserID(); + } + + @Override + public String getIdentifier() throws RepositoryException { + return event.getIdentifier(); + } + + @Override + public Map getInfo() throws RepositoryException { + return event.getInfo(); + } + + @Override + public String getUserData() throws RepositoryException { + userInfoAccessed(); + return event.getUserData(); + } + + @Override + public long getDate() throws RepositoryException { + dateInfoAccessed(); + return event.getDate(); + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/JackrabbitEventTracker.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/JackrabbitEventTracker.java new file mode 100644 index 00000000000..928408b9cbf --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/JackrabbitEventTracker.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.commons.observation; + +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.api.observation.JackrabbitEvent; + +class JackrabbitEventTracker extends EventTracker + implements JackrabbitEvent { + + public JackrabbitEventTracker(ListenerTracker listener, Event event) { + super(listener, event); + } + + @Override + protected boolean eventIsExternal() { + return ((JackrabbitEvent) event).isExternal(); + } + + //----------------------------------------------------< JackrabbitEvent>-- + + @Override + public boolean isExternal() { + externalAccessed.set(true); + return eventIsExternal(); + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/ListenerTracker.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/ListenerTracker.java new file mode 100644 index 00000000000..437efb17ef0 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/ListenerTracker.java @@ -0,0 +1,428 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.commons.observation; + +import static java.lang.System.currentTimeMillis; +import static java.lang.System.nanoTime; +import static org.apache.jackrabbit.stats.TimeSeriesStatsUtil.asCompositeData; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import javax.management.openmbean.CompositeData; + +import org.apache.jackrabbit.api.jmx.EventListenerMBean; +import org.apache.jackrabbit.api.observation.JackrabbitEvent; +import org.apache.jackrabbit.commons.iterator.EventIteratorAdapter; +import org.apache.jackrabbit.stats.TimeSeriesMax; +import org.apache.jackrabbit.stats.TimeSeriesRecorder; + +/** + * Tracks event deliveries to an event listener and the way the listener + * processes the events. The collected information is made available through + * the {@link EventListenerMBean} interface. + */ +public class ListenerTracker { + + private final EventListener listener; + + private final int eventTypes; + + private final String absPath; + + private final boolean isDeep; + + private final String[] uuid; + + private final String[] nodeTypeName; + + private final boolean noLocal; + + protected final Exception initStackTrace = + new Exception("The event listener was registered here:"); + + private final long startTime = currentTimeMillis(); + + private final AtomicLong eventDeliveries = new AtomicLong(); + + private final AtomicLong eventsDelivered = new AtomicLong(); + + private final AtomicLong eventDeliveryTime = new AtomicLong(); + + private final AtomicLong headTimestamp = new AtomicLong(); + + private final TimeSeriesMax queueLength = new TimeSeriesMax(); + + private final TimeSeriesRecorder eventCount = new TimeSeriesRecorder(true); + + private final TimeSeriesRecorder eventConsumerTime = new TimeSeriesRecorder(true); + + private final TimeSeriesRecorder eventProducerTime = new TimeSeriesRecorder(true); + + final AtomicBoolean userInfoAccessedWithoutExternalsCheck = + new AtomicBoolean(); + + final AtomicBoolean userInfoAccessedFromExternalEvent = + new AtomicBoolean(); + + final AtomicBoolean dateAccessedWithoutExternalsCheck = + new AtomicBoolean(); + + final AtomicBoolean dateAccessedFromExternalEvent = + new AtomicBoolean(); + + public ListenerTracker( + EventListener listener, int eventTypes, String absPath, boolean isDeep, String[] uuid, + String[] nodeTypeName, boolean noLocal) { + this.listener = listener; + this.eventTypes = eventTypes; + this.absPath = absPath; + this.isDeep = isDeep; + this.uuid = copy(uuid); + this.nodeTypeName = copy(nodeTypeName); + this.noLocal = noLocal; + } + + /** + * Called to log a deprecation warning about the detected behavior of + * the decorated listener. Subclasses should override this method that + * by default does nothing. + * + * @param message warning message + */ + protected void warn(String message) { + // do nothing + } + + /** + * Called just before the {@link EventListener#onEvent(EventIterator)} + * method is called. The default implementation of this method does + * nothing, but subclasses can override it to add custom processing. + */ + protected void beforeEventDelivery() { + // do nothing + } + + /** + * Called just after the {@link EventListener#onEvent(EventIterator)} + * method has been called (even if the call threw an exception). The + * default implementation of this method does nothing, but subclasses + * can override it to add custom processing. + */ + protected void afterEventDelivery() { + // do nothing + } + + /** + * Applications should call this to report the current queue length. + * @param length + */ + public void recordQueueLength(long length) { + queueLength.recordValue(length); + } + + /** + * Applications should call this to report the current queue length when an + * item is removed from the queue. + * + * @param length the length of the queue after the item was removed. + * @param headTimestamp the time in milliseconds when the head item was + * created and put into the queue. + */ + public void recordQueueLength(long length, long headTimestamp) { + queueLength.recordValue(length); + this.headTimestamp.set(length == 0 ? 0 : headTimestamp); + } + + /** + * Records the number of measured values over the past second and resets + * the counter. This method should be scheduled to be called once per + * second. + */ + public void recordOneSecond() { + queueLength.recordOneSecond(); + eventCount.recordOneSecond(); + eventConsumerTime.recordOneSecond(); + eventProducerTime.recordOneSecond(); + } + + /** + * Record additional producer time spent outside of the listeners, e.g. + * before {@code onEvent()} is called. + * + * @param time the amount of time. + * @param unit the time unit. + */ + public void recordProducerTime(long time, TimeUnit unit) { + eventProducerTime.getCounter().addAndGet(unit.toNanos(time)); + } + + public EventListener getTrackedListener() { + return new EventListener() { + @Override + public void onEvent(EventIterator events) { + eventDeliveries.incrementAndGet(); + final long start = nanoTime(); + try { + beforeEventDelivery(); + listener.onEvent(new EventIteratorAdapter(events) { + long t0 = start; + + private void recordTime(TimeSeriesRecorder recorder) { + recorder.getCounter().addAndGet(-(t0 - (t0 = nanoTime()))); + } + + @Override + public Object next() { + recordTime(eventConsumerTime); + eventsDelivered.incrementAndGet(); + eventCount.getCounter().incrementAndGet(); + Object object = super.next(); + if (object instanceof JackrabbitEvent) { + object = new JackrabbitEventTracker( + ListenerTracker.this, + (JackrabbitEvent) object); + } else if (object instanceof Event) { + object = new EventTracker( + ListenerTracker.this, (Event) object); + } + recordTime(eventProducerTime); + return object; + } + + @Override + public boolean hasNext() { + recordTime(eventConsumerTime); + boolean result = super.hasNext(); + recordTime(eventProducerTime); + return result; + } + }); + } finally { + afterEventDelivery(); + eventDeliveryTime.addAndGet(nanoTime() - start); + } + } + + @Override + public String toString() { + return ListenerTracker.this.toString(); + } + }; + } + + public EventListenerMBean getListenerMBean() { + return new EventListenerMBean() { + @Override + public String getClassName() { + return listener.getClass().getName(); + } + @Override + public String getToString() { + return listener.toString(); + } + @Override + public String getInitStackTrace() { + StringWriter writer = new StringWriter(); + initStackTrace.printStackTrace(new PrintWriter(writer)); + return writer.toString(); + } + @Override + public int getEventTypes() { + return eventTypes; + } + @Override + public String getAbsPath() { + return absPath; + } + @Override + public boolean isDeep() { + return isDeep; + } + @Override + public String[] getUuid() { + return copy(uuid); + } + @Override + public String[] getNodeTypeName() { + return copy(nodeTypeName); + } + @Override + public boolean isNoLocal() { + return noLocal; + } + @Override + public long getEventDeliveries() { + return eventDeliveries.get(); + } + @Override + public long getEventDeliveriesPerHour() { + return TimeUnit.HOURS.toMillis(getEventDeliveries()) + / Math.max(currentTimeMillis() - startTime, 1); + } + @Override + public long getMicrosecondsPerEventDelivery() { + return TimeUnit.NANOSECONDS.toMicros(eventDeliveryTime.get()) + / Math.max(getEventDeliveries(), 1); + } + @Override + public long getEventsDelivered() { + return eventsDelivered.get(); + } + @Override + public long getEventsDeliveredPerHour() { + return TimeUnit.HOURS.toMillis(getEventsDelivered()) + / Math.max(currentTimeMillis() - startTime, 1); + } + @Override + public long getMicrosecondsPerEventDelivered() { + return TimeUnit.NANOSECONDS.toMicros(eventDeliveryTime.get()) + / Math.max(getEventsDelivered(), 1); + } + @Override + public double getRatioOfTimeSpentProcessingEvents() { + double timeSpentProcessingEvents = + TimeUnit.NANOSECONDS.toMillis(eventDeliveryTime.get()); + return timeSpentProcessingEvents + / Math.max(currentTimeMillis() - startTime, 1); + } + @Override + public double getEventConsumerTimeRatio() { + double consumerTime = sum(eventConsumerTime); + double producerTime = sum(eventProducerTime); + return consumerTime / Math.max(consumerTime + producerTime, 1); + } + @Override + public boolean isUserInfoAccessedWithoutExternalsCheck() { + return userInfoAccessedWithoutExternalsCheck.get(); + } + @Override + public synchronized boolean isUserInfoAccessedFromExternalEvent() { + return userInfoAccessedFromExternalEvent.get(); + } + @Override + public synchronized boolean isDateAccessedWithoutExternalsCheck() { + return dateAccessedWithoutExternalsCheck.get(); + } + @Override + public synchronized boolean isDateAccessedFromExternalEvent() { + return dateAccessedFromExternalEvent.get(); + } + @Override + public long getQueueBacklogMillis() { + long t = headTimestamp.get(); + if (t > 0) { + t = currentTimeMillis() - t; + } + return t; + } + @Override + public CompositeData getQueueLength() { + return asCompositeData(queueLength, "queueLength"); + } + @Override + public CompositeData getEventCount() { + return asCompositeData(eventCount, "eventCount"); + } + @Override + public CompositeData getEventConsumerTime() { + return asCompositeData(eventConsumerTime, "eventConsumerTime"); + } + @Override + public CompositeData getEventProducerTime() { + return asCompositeData(eventProducerTime, "eventProducerTime"); + } + }; + } + + //------------------------------------------------------------< Object >-- + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (absPath != null) { + builder.append(absPath); + } + if (isDeep) { + builder.append("//*"); + } else { + builder.append("/*"); + } + builder.append('['); + builder.append(Integer.toBinaryString(eventTypes)); + builder.append('b'); + if (uuid != null) { + for (String id : uuid) { + builder.append(", "); + builder.append(id); + } + } + if (nodeTypeName != null) { + for (String name : nodeTypeName) { + builder.append(", "); + builder.append(name); + } + } + if (noLocal) { + builder.append(", no local"); + } + builder.append("]@"); + builder.append(listener.getClass().getName()); + return builder.toString(); + } + + //-----------------------------------------------------------< private >-- + + private static String[] copy(String[] array) { + if (array != null && array.length > 0) { + String[] copy = new String[array.length]; + System.arraycopy(array, 0, copy, 0, array.length); + return copy; + } else { + return array; + } + } + + private static long sum(TimeSeriesRecorder timeSeries) { + long missingValue = timeSeries.getMissingValue(); + long sum = 0; + sum += sum(timeSeries.getValuePerSecond(), missingValue); + sum += sum(timeSeries.getValuePerMinute(), missingValue); + sum += sum(timeSeries.getValuePerHour(), missingValue); + sum += sum(timeSeries.getValuePerWeek(), missingValue); + return sum; + } + + private static long sum(long[] values, long missing) { + long sum = 0; + for (long v : values) { + if (v != missing) { + sum += v; + } + } + return sum; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/package-info.java new file mode 100755 index 00000000000..a606e73a2a6 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/observation/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.commons.observation; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/package-info.java new file mode 100644 index 00000000000..42b3b486607 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.commons; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/ContentPackage.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/ContentPackage.java new file mode 100644 index 00000000000..dea1379023c --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/ContentPackage.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.commons.packaging; + +import java.util.Iterator; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +public interface ContentPackage { + + Iterator getItems(Session session) throws RepositoryException; +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/ContentPackageExporter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/ContentPackageExporter.java new file mode 100644 index 00000000000..ef4ff5d20df --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/ContentPackageExporter.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.commons.packaging; + +import java.io.OutputStream; + +import javax.jcr.RepositoryException; + +public interface ContentPackageExporter { + + void export(ContentPackage description, OutputStream out) + throws RepositoryException; +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/FilterContentPackage.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/FilterContentPackage.java new file mode 100644 index 00000000000..a33df632319 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/FilterContentPackage.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.commons.packaging; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.commons.predicate.Predicate; + +public class FilterContentPackage implements ContentPackage { + + protected final List content = new ArrayList(); + + protected boolean includeProperties = false; + + public void addContent(String path, Predicate filterList) { + this.content.add(new Content(new String[] {path}, filterList)); + } + + public void addContent(String[] paths, Predicate filterList) { + this.content.add(new Content(paths, filterList)); + } + + /** + * @see org.apache.jackrabbit.commons.packaging.ContentPackage#getItems(javax.jcr.Session) + */ + public Iterator getItems(Session session) + throws RepositoryException { + return new FilteringIterator(session, new ArrayList(this.content), this.includeProperties); + } + + protected static class Content { + protected final String[] paths; + protected final Predicate filterList; + + public Content(String[] paths, Predicate filterList) { + this.paths = paths; + this.filterList = filterList; + } + } + + public static class FilteringIterator implements Iterator { + + /** The content we will iterate over. */ + protected final List content; + + /** + * Filter that defines which items are included + */ + protected Predicate includeFilter; + + protected int contentIndex, pathIndex; + + protected Item nextItem; + + protected Node lastNode; + + protected final Session session; + + protected final List nodeIteratorStack = new ArrayList(); + + protected final boolean includeProperties; + + protected PropertyIterator propertyIterator; + + /** + * Creates a new tree walker that uses the given filter as include and + * traversal filter. + * + * @param session The session. + * @param contentList The list of content objects. + * @param includeProperties Should properties be included. + */ + public FilteringIterator(final Session session, + final List contentList, + final boolean includeProperties) { + this.content = contentList; + this.session = session; + this.includeProperties = includeProperties; + } + + /** + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() { + if ( this.nextItem != null ) { + return true; + } + try { + return this.checkForNextNode(); + } catch (RepositoryException e) { + // if any error occurs, we stop iterating + return false; + } + } + + protected boolean checkForNextNode() throws RepositoryException { + if ( this.propertyIterator != null ) { + if ( this.propertyIterator.hasNext() ) { + this.nextItem = this.propertyIterator.nextProperty(); + return true; + } + this.propertyIterator = null; + } else if ( this.includeProperties && this.lastNode != null ) { + if ( this.lastNode.hasProperties() ) { + this.propertyIterator = this.lastNode.getProperties(); + this.propertyIterator.hasNext(); + this.nextItem = this.propertyIterator.nextProperty(); + return true; + } + } + if ( this.lastNode != null ) { + + if ( this.lastNode.hasNodes() ) { + final NodeIterator iter = this.lastNode.getNodes(); + this.nodeIteratorStack.add(iter); + } + while ( this.nodeIteratorStack.size() > 0 ) { + final NodeIterator iter = (NodeIterator)this.nodeIteratorStack.get(this.nodeIteratorStack.size() - 1); + if ( iter.hasNext() ) { + do { + final Node contextNode = iter.nextNode(); + if ( this.includeFilter.evaluate(contextNode) ) { + this.lastNode = contextNode; + this.nextItem = contextNode; + return true; + } + } while ( iter.hasNext() ); + } + this.nodeIteratorStack.remove(iter); + } + this.pathIndex++; + this.lastNode = null; + } + while ( this.contentIndex < this.content.size() ) { + final Content content = (Content)this.content.get(this.contentIndex); + this.includeFilter = content.filterList; + while ( this.pathIndex < content.paths.length ) { + final String path = content.paths[this.pathIndex]; + this.pathIndex++; + final Node contextNode = (Node)this.session.getItem(path); + if ( this.includeFilter.evaluate(contextNode) ) { + this.lastNode = contextNode; + this.nextItem = contextNode; + return true; + } + } + this.contentIndex++; + this.pathIndex = 0; + } + + return false; + } + + /** + * @see java.util.Iterator#next() + */ + public Object next() { + if ( this.hasNext() ) { + final Item result = nextItem; + this.nextItem = null; + return result; + } + throw new NoSuchElementException("No more elements available"); + } + + /** + * @see java.util.Iterator#remove() + */ + public void remove() { + throw new UnsupportedOperationException("Remove is not supported."); + } + } + + public boolean isIncludeProperties() { + return includeProperties; + } + + public void setIncludeProperties(boolean includeProperties) { + this.includeProperties = includeProperties; + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/package-info.java new file mode 100644 index 00000000000..d3270605e1a --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/packaging/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.2.1") +package org.apache.jackrabbit.commons.packaging; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/DeclaringTypePredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/DeclaringTypePredicate.java new file mode 100644 index 00000000000..3c63e0440df --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/DeclaringTypePredicate.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.commons.predicate; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +/** + * Filter that checks the declared type of an item + * + */ +public class DeclaringTypePredicate extends DepthPredicate { + + /** + * The nodetype to check + */ + protected final String nodeType; + + /** + * indicates if only props should be checked + */ + protected final boolean propsOnly; + + /** + * Creates a new filter for the given nodetype and flags. + * @param nodeType the nodetype name to check + * @param propsOnly if true only properties are checked + * @param minDepth the minimal depth + * @param maxDepth the maximal depth + */ + public DeclaringTypePredicate(String nodeType, boolean propsOnly, + int minDepth, int maxDepth) { + super(minDepth, maxDepth); + this.nodeType = nodeType; + this.propsOnly = propsOnly; + } + + /** + * Creates a new filter for the given nodetype and flags + * @param nodeType the nodetype name to check + * @param propsOnly if true only properties are checked + */ + public DeclaringTypePredicate(String nodeType, boolean propsOnly) { + this(nodeType, propsOnly, 0, Integer.MAX_VALUE); + } + + /** + * Matches if the declaring nodetype of the item is equal to the one + * specified in this filter. If the item is a node and propsOnly + * flag is true it returns false. + * @see org.apache.jackrabbit.commons.predicate.DepthPredicate#matches(javax.jcr.Item) + */ + @Override + protected boolean matches(Item item) throws RepositoryException { + if (item.isNode()) { + return !propsOnly && ((Node) item).getDefinition().getDeclaringNodeType().getName().equals(nodeType); + } + return ((Property) item).getDefinition().getDeclaringNodeType().getName().equals(nodeType); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/DepthPredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/DepthPredicate.java new file mode 100644 index 00000000000..a7a993e1e02 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/DepthPredicate.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.commons.predicate; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +/** + * Implements a filter that filters item according to their (passed) depth. + * + */ +public class DepthPredicate implements Predicate { + + /** + * The minimal depth + */ + protected final int minDepth; + + /** + * The maximal depth + */ + protected final int maxDepth; + + /** + * Creates a new depth filter for the given depths. + * @param minDepth the minimal depth + * @param maxDepth the maximal depth + */ + public DepthPredicate(int minDepth, int maxDepth) { + this.minDepth = minDepth; + this.maxDepth = maxDepth; + } + + /** + * Matches if the given depth is greater or equal the minimum depth and + * less or equal the maximum depth and if the call to {@link #matches(Item)} + * returns true. + * @see Predicate#evaluate(java.lang.Object) + */ + public boolean evaluate(Object item) { + if ( item instanceof Item ) { + try { + final int depth = ((Item)item).getDepth(); + return depth >= minDepth && depth <= maxDepth && matches((Item)item); + } catch (RepositoryException re) { + return false; + } + } + return false; + } + + /** + * Returns true. Subclasses can override to implement something + * useful that is dependant of the depth. + * + * @param item the item to match + * @return true if the item matches; false otherwise. + * @throws RepositoryException if an error occurs. + */ + protected boolean matches(Item item) throws RepositoryException { + return true; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/IsMandatoryPredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/IsMandatoryPredicate.java new file mode 100644 index 00000000000..b840b76b9a7 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/IsMandatoryPredicate.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.commons.predicate; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +/** + * IsMandatoryFilter... + * + */ +public class IsMandatoryPredicate extends DepthPredicate { + + protected final boolean isMandatory; + + public IsMandatoryPredicate() { + this(true); + } + + public IsMandatoryPredicate(boolean isMandatory, int minDepth, int maxDepth) { + super(minDepth, maxDepth); + this.isMandatory = isMandatory; + } + + public IsMandatoryPredicate(boolean isMandatory) { + this(isMandatory, 0, Integer.MAX_VALUE); + } + + /** + * @see org.apache.jackrabbit.commons.predicate.DepthPredicate#matches(javax.jcr.Item) + */ + @Override + protected boolean matches(Item item) throws RepositoryException { + if (item.isNode()) { + return ((Node) item).getDefinition().isMandatory() == isMandatory; + } + return ((Property) item).getDefinition().isMandatory() == isMandatory; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/IsNodePredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/IsNodePredicate.java new file mode 100644 index 00000000000..1a919f030b0 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/IsNodePredicate.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.commons.predicate; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +/** + * Item filter that checks if an item is a node. + * + */ +public class IsNodePredicate extends DepthPredicate { + + /** + * Polarity of this filter + */ + protected final boolean isNode; + + /** + * Default constructor. + */ + public IsNodePredicate() { + this(true); + } + + /** + * Creates a new node item filter. + * + * @param polarity the polarity of this filter. if true it matches + * nodes, if false it matches properties. + * @param minDepth the minimum depth + * @param maxDepth the maximum depth + * + * @see DepthPredicate + */ + public IsNodePredicate(boolean polarity, int minDepth, int maxDepth) { + super(minDepth, maxDepth); + isNode = polarity; + } + + /** + * Creates a new node item filter + * @param polarity the polarity of this filter. if true it matches + * nodes, if false it matches properties. + */ + public IsNodePredicate(boolean polarity) { + this(polarity, 0, Integer.MAX_VALUE); + } + + /** + * Returns true if the item is a node and the polarity is + * positive (true). + * @see org.apache.jackrabbit.commons.predicate.DepthPredicate#matches(javax.jcr.Item) + */ + @Override + protected boolean matches(Item item) throws RepositoryException { + return item.isNode() == isNode; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NamePredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NamePredicate.java new file mode 100644 index 00000000000..7ea7db39c98 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NamePredicate.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.commons.predicate; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +/** + * Filters items according to their names. + * + */ +public class NamePredicate extends DepthPredicate { + + /** + * The name to filter on + */ + protected final String name; + + /** + * Creates a new name filter with the given name and depths + * @param name the name to filter on + * @param minDepth the minimal depth + * @param maxDepth the maximal depth + */ + public NamePredicate(String name, int minDepth, int maxDepth) { + super(minDepth, maxDepth); + this.name = name; + } + + /** + * Creates a new name filter with the given name. + * @param name the name to filter on + */ + public NamePredicate(String name) { + this(name, 0, Integer.MAX_VALUE); + } + + /** + * Returns true if the name of the given item is equal to + * the configured name. + * @see org.apache.jackrabbit.commons.predicate.DepthPredicate#matches(javax.jcr.Item) + */ + @Override + protected boolean matches(Item item) throws RepositoryException { + return item.getName().equals(name); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NodeTypePredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NodeTypePredicate.java new file mode 100644 index 00000000000..8223dc8fcd5 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NodeTypePredicate.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.commons.predicate; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** + * Filters on the node type of a node. + * + */ +public class NodeTypePredicate extends DepthPredicate { + + /** + * the nodetype to filter on + */ + protected final String nodeType; + + /** + * indicates if supertypes should be respected + */ + protected final boolean respectSupertype; + + /** + * Creates a new node type filter. + * @param nodeType the node type to filter on + * @param respectSupertype indicates if supertype should be respected + * @param minDepth the minimal depth + * @param maxDepth the maximal depth + */ + public NodeTypePredicate(String nodeType, boolean respectSupertype, + int minDepth, int maxDepth) { + super(minDepth, maxDepth); + this.nodeType = nodeType; + this.respectSupertype = respectSupertype; + } + + /** + * Creates a new node type filter. + * @param nodeType the node type to filter on + * @param respectSupertype indicates if supertype should be respected + */ + public NodeTypePredicate(String nodeType, boolean respectSupertype) { + this(nodeType, respectSupertype, 0, Integer.MAX_VALUE); + } + + /** + * Returns true if the item is a node and if the configured + * nodetype is equal to the primary type of the node. if supertypes are + * respected it also returns true if the items nodetype + * extends from the configured node type (Node.isNodeType() check). + * @see org.apache.jackrabbit.commons.predicate.DepthPredicate#matches(javax.jcr.Item) + */ + @Override + protected boolean matches(Item item) throws RepositoryException { + if (item.isNode()) { + if (respectSupertype) { + try { + return ((Node) item).isNodeType(nodeType); + } catch (RepositoryException e) { + // ignore + return false; + } + } + return ((Node) item).getPrimaryNodeType().getName().equals(nodeType); + } + return false; + + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NtFilePredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NtFilePredicate.java new file mode 100644 index 00000000000..00e426d01a8 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/NtFilePredicate.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.commons.predicate; + +import javax.jcr.Item; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +/** + * The nt file item filter matches all properties that are defined my the + * nt:file or nt:resource nodetype. the later only, if the respective nodes + * name is 'jcr:content'. + * + * Additionally the properties 'jcr:encoding' can be configured to be excluded. + * + */ +public class NtFilePredicate implements Predicate { + + public static final String NT_FILE = "nt:file"; + public static final String NT_HIERARCHYNODE = "nt:hierarchyNode"; + public static final String NT_RESOURCE = "nt:resource"; + public static final String JCR_CONTENT = "jcr:content"; + public static final String JCR_ENCODING = "jcr:encoding"; + public static final String JCR_MIMETYPE = "jcr:mimeType"; + public static final String JCR_PRIMARY_TYPE = "jcr:primaryType"; + + /** + * indicates if the jcr:encoding property is to be excluded from this filter. + */ + protected final boolean ignoreEncoding; + + /** + * indicates if the jcr:mimeType property is to be excluded from this filter. + */ + protected final boolean ignoreMimeType; + + public NtFilePredicate() { + this(false, false); + } + + public NtFilePredicate(boolean ignoreEncoding, boolean ignoreMimeType) { + this.ignoreEncoding = ignoreEncoding; + this.ignoreMimeType = ignoreMimeType; + } + + /** + * Returns the ignore encoding flag. + * @return the ignore encoding flag. + */ + public boolean isIgnoreEncoding() { + return ignoreEncoding; + } + + /** + * Returns the ignore mime type flag. + * @return the ignore mime type flag. + */ + public boolean isIgnoreMimeType() { + return ignoreMimeType; + } + + /** + * @return true if the item is a nt:file or nt:resource property + * @see org.apache.jackrabbit.commons.predicate.Predicate#evaluate(java.lang.Object) + */ + public boolean evaluate(Object item) { + if ( item instanceof Item ) { + if (!((Item)item).isNode()) { + try { + Property prop = (Property) item; + String dnt = prop.getDefinition().getDeclaringNodeType().getName(); + // exclude all nt:file props + if (dnt.equals(NT_FILE) || dnt.equals(NT_HIERARCHYNODE)) { + return true; + } + if (ignoreEncoding && prop.getName().equals(JCR_ENCODING)) { + return false; + } + if (ignoreMimeType && prop.getName().equals(JCR_MIMETYPE)) { + return false; + } + // exclude nt:resource props, if parent is 'jcr:content' + if (prop.getParent().getName().equals(JCR_CONTENT)) { + if (dnt.equals(NT_RESOURCE)) { + return true; + } + // exclude primary type if nt:resource + /* + if (prop.getName().equals(JCR_PRIMARY_TYPE) + && prop.getValue().getString().equals(NT_RESOURCE)) { + return true; + } + */ + } + } catch (RepositoryException re) { + return false; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/PathPredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/PathPredicate.java new file mode 100644 index 00000000000..f253e53e688 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/PathPredicate.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.commons.predicate; + +import java.util.regex.Pattern; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +/** + * The path filter provides hierarchical filtering. + * + */ +public class PathPredicate implements Predicate { + + /** + * the internal regex pattern + */ + protected final Pattern regex; + + /** + * Creates a new default path filter + *
    +     * | Pattern | Matches
    +     * | /foo    | exactly "/foo"
    +     * | /foo.*  | all paths starting with "foo."
    +     * | foo.*   | all files starting with "foo."
    +     * | /foo/*  | all direct children of /foo
    +     * | /foo/** | all children of /foo
    +     * 
    + * @param pattern the pattern + */ + public PathPredicate(String pattern) { + String suffix = ""; + String prefix = ""; + if (pattern.endsWith("/**")) { + suffix = "/.*"; + pattern = pattern.substring(0, pattern.length() - 3); + } else if (pattern.endsWith("*")) { + suffix = "[^/]*$"; + pattern = pattern.substring(0, pattern.length() - 1); + } + if (pattern.charAt(0) != '/') { + prefix = "^.*/"; + } + pattern = prefix + pattern.replaceAll("\\.", "\\\\.") + suffix; + regex = Pattern.compile(pattern); + } + + /** + * @see Predicate#evaluate(java.lang.Object) + */ + public boolean evaluate(Object item) { + if ( item instanceof Item ) { + try { + return regex.matcher(((Item)item).getPath()).matches(); + } catch (RepositoryException re) { + return false; + } + } + return false; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/Predicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/Predicate.java new file mode 100644 index 00000000000..e9ecbb9ae57 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/Predicate.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.commons.predicate; + +/** + * Interface for object predicates, i.e. functions that evaluate a given + * object to a boolean value. + */ +public interface Predicate { + + /** + * Evaluates the predicate for the given object. + * + * @param object some object + * @return predicate result + */ + boolean evaluate(Object object); + + /** + * Constant predicate that returns true for all objects. + */ + Predicate TRUE = new Predicate() { + public boolean evaluate(Object object) { + return true; + } + }; + + /** + * Constant predicate that returns false for all objects. + */ + Predicate FALSE = new Predicate() { + public boolean evaluate(Object object) { + return false; + } + }; + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/Predicates.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/Predicates.java new file mode 100644 index 00000000000..006613c84a8 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/Predicates.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.commons.predicate; + +/** + * Static utility class to help working with {@link Predicate}s. + * + * @since Apache Jackrabbit 2.2 + */ +public class Predicates { + + /** + * Creates an AND predicate over all the given component predicates. + * All the component predicates must evaluate to true + * for the AND predicate to do so. + * + * @param predicates component predicates + * @return AND predicate + */ + public static Predicate and(final Predicate... predicates) { + return new Predicate() { + public boolean evaluate(Object object) { + for (Predicate predicate : predicates) { + if (!predicate.evaluate(object)) { + return false; + } + } + return true; + } + }; + } + + /** + * Creates an OR predicate over all the given component predicates. + * At least one of the component predicates must evaluate to + * true for the OR predicate to do so. + * + * @param predicates component predicates + * @return OR predicate + */ + public static Predicate or(final Predicate... predicates) { + return new Predicate() { + public boolean evaluate(Object object) { + for (Predicate predicate : predicates) { + if (predicate.evaluate(object)) { + return true; + } + } + return false; + } + }; + } + + /** + * Creates a NOT predicate for the given component predicate. + * The NOT predicate evaluates to true when the component + * predicate doesn't, and vice versa. + * + * @param predicate component predicate + * @return NOT predicate + */ + public static Predicate not(final Predicate predicate) { + return new Predicate() { + public boolean evaluate(Object object) { + return !predicate.evaluate(object); + } + }; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/RowPredicate.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/RowPredicate.java new file mode 100644 index 00000000000..fe55d875a5b --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/RowPredicate.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.commons.predicate; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.Row; + +/** + * Predicate for checking whether a given object is a {@link Row} and + * optionally whether it contains a given selector. Subclasses can extend + * this class to provide more complex checking of the row or the selected + * node. + * + * @since Apache Jackrabbit 2.2 + */ +public class RowPredicate implements Predicate { + + /** + * Selector name, or null. + */ + private final String selectorName; + + /** + * Creates a row predicate that checks the existence of the given + * selector (if given). + * + * @param selectorName selector name, or null + */ + public RowPredicate(String selectorName) { + this.selectorName = selectorName; + } + + /** + * Creates a row predicate. + */ + public RowPredicate() { + this(null); + } + + /** + * Checks whether the given object is a {@link Row} and calls the + * protected {@link #evaluate(Row)} method to evaluate the row. + */ + public boolean evaluate(Object object) { + if (object instanceof Row) { + try { + return evaluate((Row) object); + } catch (RepositoryException e) { + throw new RuntimeException("Failed to evaluate " + object, e); + } + } else { + return false; + } + } + + /** + * Evaluates the given row. If a selector name is specified, then + * the corresponding node in this row is evaluated by calling the + * protected {@link #evaluate(Node)} method. + */ + protected boolean evaluate(Row row) throws RepositoryException { + if (selectorName != null) { + return evaluate(row.getNode(selectorName)); + } else { + return true; + } + } + + /** + * Evaluates the given node. The default implementation always + * returns true. + */ + protected boolean evaluate(Node node) throws RepositoryException { + return true; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/package-info.java new file mode 100644 index 00000000000..5e49a549c9a --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/predicate/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.2") +package org.apache.jackrabbit.commons.predicate; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java new file mode 100755 index 00000000000..9773f76f50e --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/GQL.java @@ -0,0 +1,1376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RangeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.commons.iterator.RowIteratorAdapter; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.util.Text; + +/** + * GQL is a simple fulltext query language, which supports field + * prefixes similar to Lucene or Google queries. + *

    + * GQL basically consists of a list of query terms that are optionally prefixed + * with a property name. E.g.: title:jackrabbit. When a property + * prefix is omitted, GQL will perform a fulltext search on all indexed + * properties of a node. There are a number of pseudo properties that have + * special meaning: + *

      + *
    • path: only search nodes below this path. If you + * specify more than one term with a path prefix, only the last one will be + * considered.
    • + *
    • type: only return nodes of the given node types. This + * includes primary as well as mixin types. You may specify multiple comma + * separated node types. GQL will return nodes that are of any of the specified + * types.
    • + *
    • order: order the result by the given properties. You + * may specify multiple comma separated property names. To order the result in + * descending order simply prefix the property name with a minus. E.g.: + * order:-name. Using a plus sign will return the result in + * ascending order, which is also the default.
    • + *
    • limit: limits the number of results using an + * interval. E.g.: limit:10..20 Please note that the interval is + * zero based, start is inclusive and end is exclusive. You may also specify an + * open interval: limit:10.. or limit:..20 If the dots + * are omitted and only one value is specified GQL will return at most this + * number of results. E.g. limit:10 (will return the first 10 + * results)
    • + *
    • name: a constraint on the name of the returned nodes. + * The following wild cards are allowed: '*', matching any character sequence of + * length 0..n; '?', matching any single character.
    • + *
    + *

    + * Property name + *

    + * Instead of a property name you may also specify a relative path to a + * property. E.g.: "jcr:content/jcr:mimeType":text/plain + *

    + * Double quotes + *

    + * The property name as well as the value may enclosed in double quotes. For + * certain use cases this is required. E.g. if you want to search for a phrase: + * title:"apache jackrabbit". Similarly you need to enclose the + * property name if it contains a colon: "jcr:title":apache, + * otherwise the first colon is interpreted as the separator between the + * property name and the value. This also means that a value that contains + * a colon does not need to be enclosed in double quotes. + *

    + * Escaping + *

    + * To imply double-quotes("), backslash(\), and colon(:) literally you can + * escape them with a backslash. E.g. similar to example above in quoting for colon, + * "jcr:title":apache is equiavalent to jcr\:title:apache. + *

    + * Auto prefixes + *

    + * When a property, node or node type name does not have a namespace prefix GQL + * will guess the prefix by looking up item and node type definitions in the + * node type manager. If it finds a definition with a local name that matches + * it will automatically assign the prefix that is in the definition. This means + * that you can write: 'type:file' and GQL will return nodes that are + * of node type nt:file. Similarly you can write: + * order:lastModified and your result nodes will be sorted by their + * jcr:lastModified property value. + *

    + * Common path prefix + *

    + * For certain queries it is useful to specify a common path prefix for the + * GQL query statement. See {@link #execute(String, Session, String)}. E.g. if + * you are searching for file nodes with matches in their resource node. The + * common path prefix is prepended to every term (except to pseudo properties) + * before the query is executed. This means you can write: + * 'type:file jackrabbit' and call execute with three parameters, + * where the third parameter is jcr:content. GQL will return + * nt:file nodes with jcr:content nodes that contain + * matches for jackrabbit. + *

    + * Excerpts + *

    + * To get an excerpt for the current row in the result set simply call + * {@link Row#getValue(String) Row.getValue("rep:excerpt()");}. Please note + * that this is feature is Jackrabbit specific and will not work with other + * implementations! + *

    + * Parser callbacks + *

    + * You can get callbacks for each field and query term pair using the method + * {@link #parse(String, Session, ParserCallback)}. This may be useful when you + * want to do some transformation on the GQL before it is actually executed. + */ +public final class GQL { + + /** + * Constant for path keyword. + */ + private static final String PATH = "path"; + + /** + * Constant for type keyword. + */ + private static final String TYPE = "type"; + + /** + * Constant for order keyword. + */ + private static final String ORDER = "order"; + + /** + * Constant for limit keyword. + */ + private static final String LIMIT = "limit"; + + /** + * Constant for name keyword. + */ + private static final String NAME = "name"; + + /** + * Constant for OR operator. + */ + private static final String OR = "OR"; + + /** + * Constant for jcr:mixinTypes property name. + */ + private static final String JCR_MIXIN_TYPES = "jcr:mixinTypes"; + + /** + * Constant for jcr:primaryType property name. + */ + private static final String JCR_PRIMARY_TYPE = "jcr:primaryType"; + + /** + * Constant for jcr:root. + */ + private static final String JCR_ROOT = "jcr:root"; + + /** + * Constant for jcr:contains function name. + */ + private static final String JCR_CONTAINS = "jcr:contains"; + + /** + * Constant for jcr:score pseudo property name. + */ + private static final String JCR_SCORE = "jcr:score"; + + /** + * Constant for descending order modifier. + */ + private static final String DESCENDING = "descending"; + + /** + * Constant for rep:excerpt function name. (Jackrabbit + * specific). + */ + private static final String REP_EXCERPT = "rep:excerpt"; + + /** + * A pseudo-property for native xpath conditions. + */ + private static final String NATIVE_XPATH = "jcr:nativeXPath"; + + /** + * The GQL query statement. + */ + private final String statement; + + /** + * The session that will exeucte the query. + */ + private final Session session; + + /** + * List that contains all {@link PropertyExpression}s. + */ + private final List conditions = new ArrayList(); + + /** + * An optional common path prefix for the GQL query. + */ + private final String commonPathPrefix; + + /** + * An optional filter that may include/exclude result rows. + */ + private final Filter filter; + + /** + * Maps local names of node types to prefixed names. + */ + private Map ntNames; + + /** + * Maps local names of child node definitions to prefixed child node names. + */ + private Map childNodeNames; + + /** + * Maps local names of property definitions to prefixed property names. + */ + private Map propertyNames; + + /** + * The path constraint. Defaults to: //* + */ + private String pathConstraint = "//*"; + + /** + * The optional type constraints. + */ + private OptionalExpression typeConstraints = null; + + /** + * The order by expression. Defaults to: @jcr:score descending + */ + private Expression orderBy = new OrderByExpression(); + + /** + * A result offset. + */ + private int offset = 0; + + /** + * The number of results to return at most. + */ + private int numResults = Integer.MAX_VALUE; + + /** + * Constructs a GQL. + * + * @param statement the GQL query. + * @param session the session that will execute the query. + * @param commonPathPrefix a common path prefix for the GQL query. + * @param filter an optional filter that may include/exclude result rows. + */ + private GQL(String statement, Session session, + String commonPathPrefix, Filter filter) { + this.statement = statement; + this.session = session; + this.commonPathPrefix = commonPathPrefix; + this.filter = filter; + } + + /** + * Executes the GQL query and returns the result as a row iterator. + * + * @param statement the GQL query. + * @param session the session that will execute the query. + * @return the result. + */ + public static RowIterator execute(String statement, + Session session) { + return execute(statement, session, null); + } + + /** + * Executes the GQL query and returns the result as a row iterator. + * + * @param statement the GQL query. + * @param session the session that will execute the query. + * @param commonPathPrefix a common path prefix for the GQL query. + * @return the result. + */ + public static RowIterator execute(String statement, + Session session, + String commonPathPrefix) { + return execute(statement, session, commonPathPrefix, null); + } + + /** + * Executes the GQL query and returns the result as a row iterator. + * + * @param statement the GQL query. + * @param session the session that will execute the query. + * @param commonPathPrefix a common path prefix for the GQL query. + * @param filter an optional filter that may include/exclude result rows. + * @return the result. + */ + public static RowIterator execute(String statement, + Session session, + String commonPathPrefix, + Filter filter) { + GQL query = new GQL(statement, session, commonPathPrefix, filter); + return query.execute(); + } + + /** + * Executes the GQL query and returns the result as a row iterator. + * + * @param jcrQuery the native JCR query. + * @param jcrQueryLanguage the JCR query language + * @param session the session that will execute the query. + * @param commonPathPrefix a common path prefix for the GQL query. + * @param filter an optional filter that may include/exclude result rows. + * @return the result. + */ + public static RowIterator executeXPath(String jcrQuery, + String jcrQueryLanguage, + Session session, + String commonPathPrefix, + Filter filter) { + GQL query = new GQL("", session, commonPathPrefix, filter); + return query.executeJcrQuery(jcrQuery, jcrQueryLanguage); + } + + /** + * Translate the GQL query to XPath. + * + * @param statement the GQL query. + * @param session the session that will execute the query. + * @param commonPathPrefix a common path prefix for the GQL query. + * @return the xpath statement. + */ + public static String translateToXPath(String statement, + Session session, + String commonPathPrefix) throws RepositoryException { + GQL query = new GQL(statement, session, commonPathPrefix, null); + return query.translateStatement(); + } + + /** + * Parses the given statement and generates callbacks for each + * GQL term parsed. + * + * @param statement the GQL statement. + * @param session the current session to resolve namespace prefixes. + * @param callback the callback handler. + * @throws RepositoryException if an error occurs while parsing. + */ + public static void parse(String statement, + Session session, + ParserCallback callback) + throws RepositoryException { + GQL query = new GQL(statement, session, null, null); + query.parse(callback); + } + + /** + * Defines a filter for query result rows. + */ + public interface Filter { + + /** + * Returns true if the given row should be + * included in the result. + * + * @param row the row to check. + * @return true if the row should be included, + * false otherwise. + * @throws RepositoryException if an error occurs while reading from the + * repository. + */ + public boolean include(Row row) throws RepositoryException; + } + + /** + * Defines a callback interface that may be implemented by client code to + * get a callback for each GQL term that is parsed. + */ + public interface ParserCallback { + + /** + * A GQL term was parsed. + * + * @param property the name of the property or an empty string if the + * term is not prefixed. + * @param value the value of the term. + * @param optional whether this term is prefixed with an OR operator. + * @throws RepositoryException if an error occurs while processing the + * term. + */ + public void term(String property, String value, boolean optional) + throws RepositoryException; + } + + //-----------------------------< internal >--------------------------------- + + /** + * Executes the GQL query and returns the result as a row iterator. + * + * @return the result. + */ + private RowIterator execute() { + String xpath; + try { + xpath = translateStatement(); + } catch (RepositoryException e) { + // in case of error return empty result + return RowIteratorAdapter.EMPTY; + } + return executeJcrQuery(xpath, Query.XPATH); + } + + private RowIterator executeJcrQuery(String jcrQuery, String jcrQueryLanguage) { + try { + QueryManager qm = session.getWorkspace().getQueryManager(); + RowIterator nodes = qm.createQuery(jcrQuery, jcrQueryLanguage).execute().getRows(); + if (filter != null) { + nodes = new FilteredRowIterator(nodes); + } + if (offset > 0) { + try { + nodes.skip(offset); + } catch (NoSuchElementException e) { + return RowIteratorAdapter.EMPTY; + } + } + if (numResults == Integer.MAX_VALUE) { + return new RowIterAdapter(nodes, nodes.getSize()); + } + List resultRows = new ArrayList(); + while (numResults-- > 0 && nodes.hasNext()) { + resultRows.add(nodes.nextRow()); + } + return new RowIterAdapter(resultRows, resultRows.size()); + } catch (RepositoryException e) { + // in case of error return empty result + return RowIteratorAdapter.EMPTY; + } + } + + /** + * Translates the GQL query into a XPath statement. + * + * @return the XPath equivalent. + * @throws RepositoryException if an error occurs while translating the query. + */ + private String translateStatement() throws RepositoryException { + parse(new ParserCallback() { + public void term(String property, String value, boolean optional) + throws RepositoryException { + pushExpression(property, value, optional); + } + }); + StringBuffer stmt = new StringBuffer(); + // path constraint + stmt.append(pathConstraint); + // predicate + RequiredExpression predicate = new RequiredExpression(); + if (typeConstraints != null) { + predicate.addOperand(typeConstraints); + } + for (Expression condition : conditions) { + predicate.addOperand(condition); + } + if (predicate.getSize() > 0) { + stmt.append("["); + } + predicate.toString(stmt); + if (predicate.getSize() > 0) { + stmt.append("]"); + } + stmt.append(" "); + // order by + orderBy.toString(stmt); + return stmt.toString(); + } + + /** + * Resolves and collects all node types that match ntName. + * + * @param ntName the name of a node type (optionally without prefix). + * @throws RepositoryException if an error occurs while reading from the + * node type manager. + */ + private void collectNodeTypes(String ntName) + throws RepositoryException { + NodeTypeManager ntMgr = session.getWorkspace().getNodeTypeManager(); + + String[] resolvedNames = resolveNodeTypeName(ntName); + + // now resolve node type hierarchy + for (String resolvedName : resolvedNames) { + try { + NodeType base = ntMgr.getNodeType(resolvedName); + if (base.isMixin()) { + // search for nodes where jcr:mixinTypes is set to this mixin + addTypeConstraint(new MixinComparision(resolvedName)); + } else { + // search for nodes where jcr:primaryType is set to this type + addTypeConstraint(new PrimaryTypeComparision(resolvedName)); + } + + // now search for all node types that are derived from base + NodeTypeIterator allTypes = ntMgr.getAllNodeTypes(); + while (allTypes.hasNext()) { + NodeType nt = allTypes.nextNodeType(); + NodeType[] superTypes = nt.getSupertypes(); + if (Arrays.asList(superTypes).contains(base)) { + if (nt.isMixin()) { + addTypeConstraint(new MixinComparision(nt.getName())); + } else { + addTypeConstraint(new PrimaryTypeComparision(nt.getName())); + } + } + } + } catch (NoSuchNodeTypeException e) { + // add anyway -> will not match anything + addTypeConstraint(new PrimaryTypeComparision(resolvedName)); + } + } + } + + /** + * Adds an expression to the {@link #typeConstraints}. + * + * @param expr the expression to add. + */ + private void addTypeConstraint(Expression expr) { + if (typeConstraints == null) { + typeConstraints = new OptionalExpression(); + } + typeConstraints.addOperand(expr); + } + + /** + * Resolves the given ntName and returns all node type names + * where the local name matches ntName. + * + * @param ntName the name of a node type (optionally without prefix). + * @return the matching node type names. + * @throws RepositoryException if an error occurs while reading from the + * node type manager. + */ + private String[] resolveNodeTypeName(String ntName) + throws RepositoryException { + String[] names; + if (isPrefixed(ntName)) { + names = new String[]{ntName}; + } else { + if (ntNames == null) { + NodeTypeManager ntMgr = session.getWorkspace().getNodeTypeManager(); + ntNames = new HashMap(); + NodeTypeIterator it = ntMgr.getAllNodeTypes(); + while (it.hasNext()) { + String name = it.nextNodeType().getName(); + String localName = name; + int idx = name.indexOf(':'); + if (idx != -1) { + localName = name.substring(idx + 1); + } + String[] nts = ntNames.get(localName); + if (nts == null) { + nts = new String[]{name}; + } else { + String[] tmp = new String[nts.length + 1]; + System.arraycopy(nts, 0, tmp, 0, nts.length); + tmp[nts.length] = name; + nts = tmp; + } + ntNames.put(localName, nts); + } + } + names = ntNames.get(ntName); + if (names == null) { + names = new String[]{ntName}; + } + } + return names; + } + + /** + * Resolves the given property name. If the name has a prefix then the name + * is returned immediately as is. Otherwise the node type manager is + * searched for a property definition that defines a named property with + * a local name that matches the provided name. If such a match + * is found the name of the property definition is returned. + * + * @param name the name of a property (optionally without a prefix). + * @return the resolved property name. + * @throws RepositoryException if an error occurs while reading from the + * node type manager. + */ + private String resolvePropertyName(String name) + throws RepositoryException { + if (isPrefixed(name)) { + return name; + } + if (propertyNames == null) { + propertyNames = new HashMap(); + if (session != null) { + NodeTypeManager ntMgr = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator it = ntMgr.getAllNodeTypes(); + while (it.hasNext()) { + NodeType nt = it.nextNodeType(); + PropertyDefinition[] defs = nt.getDeclaredPropertyDefinitions(); + for (PropertyDefinition def : defs) { + String pn = def.getName(); + if (!pn.equals("*")) { + String localName = pn; + int idx = pn.indexOf(':'); + if (idx != -1) { + localName = pn.substring(idx + 1); + } + propertyNames.put(localName, pn); + } + } + } + } + } + String pn = propertyNames.get(name); + if (pn != null) { + return pn; + } else { + return name; + } + } + + /** + * Resolves the given node name. If the name has a prefix then the name + * is returned immediately as is. Otherwise the node type manager is + * searched for a node definition that defines a named child node with + * a local name that matches the provided name. If such a match + * is found the name of the node definition is returned. + * + * @param name the name of a node (optionally without a prefix). + * @return the resolved node name. + * @throws RepositoryException if an error occurs while reading from the + * node type manager. + */ + private String resolveChildNodeName(String name) + throws RepositoryException { + if (isPrefixed(name)) { + return name; + } + if (childNodeNames == null) { + childNodeNames = new HashMap(); + NodeTypeManager ntMgr = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator it = ntMgr.getAllNodeTypes(); + while (it.hasNext()) { + NodeType nt = it.nextNodeType(); + NodeDefinition[] defs = nt.getDeclaredChildNodeDefinitions(); + for (NodeDefinition def : defs) { + String cnn = def.getName(); + if (!cnn.equals("*")) { + String localName = cnn; + int idx = cnn.indexOf(':'); + if (idx != -1) { + localName = cnn.substring(idx + 1); + } + childNodeNames.put(localName, cnn); + } + } + } + } + String cnn = childNodeNames.get(name); + if (cnn != null) { + return cnn; + } else { + return name; + } + } + + /** + * @param name a JCR name. + * @return true if name contains a namespace + * prefix; false otherwise. + */ + private static boolean isPrefixed(String name) { + return name.indexOf(':') != -1; + } + + /** + * Parses the GQL query statement. + * + * @param callback the parser callback. + * @throws RepositoryException if an error occurs while reading from the + * repository. + */ + private void parse(ParserCallback callback) throws RepositoryException { + char[] stmt = new char[statement.length() + 1]; + statement.getChars(0, statement.length(), stmt, 0); + stmt[statement.length()] = ' '; + StringBuffer property = new StringBuffer(); + StringBuffer value = new StringBuffer(); + boolean quoted = false; + boolean escaped = false; + boolean optional = false; + for (char c : stmt) { + switch (c) { + case ' ': + if (quoted) { + value.append(c); + } else { + if (value.length() > 0) { + String p = property.toString(); + String v = value.toString(); + if (v.equals(OR) && p.length() == 0) { + optional = true; + } else { + callback.term(p, v, optional); + optional = false; + } + property.setLength(0); + value.setLength(0); + } + } + break; + case ':': + if (quoted || escaped) { + value.append(c); + } else { + if (property.length() == 0) { + // turn value into property name + property.append(value); + value.setLength(0); + } else { + // colon in value + value.append(c); + } + } + break; + case '"': + if (escaped) { + value.append(c); + } else { + quoted = !quoted; + } + break; + case '\\': + if (escaped) { + value.append(c); + } + escaped = !escaped; + break; + // noise + case '*': + case '?': + if (property.toString().equals(NAME)) { + // allow wild cards in name + value.append(c); + break; + } + case '~': + case '^': + case '[': + case ']': + case '{': + case '}': + case '!': + break; + default: + value.append(c); + } + if (c != '\\') { + escaped = false; + } + } + } + + /** + * Pushes an expression. + * + * @param property the property name of the currently parsed expression. + * @param value the value of the currently parsed expression. + * @param optional whether the previous token was the OR operator. + * @throws RepositoryException if an error occurs while reading from the + * node type manager. + */ + private void pushExpression(String property, + String value, + boolean optional) throws RepositoryException { + if (property.equals(PATH)) { + String path; + if (value.startsWith("/")) { + path = "/" + JCR_ROOT + value; + } else { + path = value; + } + pathConstraint = ISO9075.encodePath(path) + "//*"; + } else if (property.equals(TYPE)) { + String[] nts = Text.explode(value, ','); + if (nts.length > 0) { + for (String nt : nts) { + collectNodeTypes(nt); + } + } + } else if (property.equals(ORDER)) { + orderBy = new OrderByExpression(value); + } else if (property.equals(LIMIT)) { + int idx = value.indexOf(".."); + if (idx != -1) { + String lower = value.substring(0, idx); + String uppper = value.substring(idx + "..".length()); + if (lower.length() > 0) { + try { + offset = Integer.parseInt(lower); + } catch (NumberFormatException e) { + // ignore + } + } + if (uppper.length() > 0) { + try { + numResults = Integer.parseInt(uppper) - offset; + if (numResults < 0) { + numResults = Integer.MAX_VALUE; + } + } catch (NumberFormatException e) { + // ignore + } + } + } else { + // numResults only? + try { + numResults = Integer.parseInt(value); + } catch (NumberFormatException e) { + // ignore + } + } + } else { + Expression expr; + if (property.equals(NAME)) { + expr = new NameExpression(value); + } else { + expr = new ContainsExpression(property, value); + } + + if (optional) { + Expression last = conditions.get(conditions.size() - 1); + if (last instanceof OptionalExpression) { + ((OptionalExpression) last).addOperand(expr); + } else { + OptionalExpression op = new OptionalExpression(); + op.addOperand(last); + op.addOperand(expr); + conditions.set(conditions.size() - 1, op); + } + } else { + conditions.add(expr); + } + } + } + + /** + * Checks if the value is prohibited (prefixed with a dash) + * and returns the value without the prefix. + * + * @param value the value to check. + * @return the un-prefixed value. + */ + private static String checkProhibited(String value) { + if (value.startsWith("-")) { + return value.substring(1); + } else { + return value; + } + } + + /** + * An expression in GQL. + */ + private interface Expression { + + void toString(StringBuffer buffer) throws RepositoryException; + } + + /** + * Base class for all property expressions. + */ + private abstract class PropertyExpression implements Expression { + + protected final String property; + + protected final String value; + + PropertyExpression(String property, String value) { + this.property = property; + this.value = value; + } + } + + /** + * Base class for mixin and primary type expressions. + */ + private abstract class ValueComparison extends PropertyExpression { + + ValueComparison(String property, String value) { + super(property, value); + } + + public void toString(StringBuffer buffer) + throws RepositoryException { + buffer.append("@"); + buffer.append(ISO9075.encode(resolvePropertyName(property))); + buffer.append("='").append(value).append("'"); + } + } + + /** + * A mixin type comparison. + */ + private class MixinComparision extends ValueComparison { + + MixinComparision(String value) { + super(JCR_MIXIN_TYPES, value); + } + } + + /** + * A primary type comparision. + */ + private class PrimaryTypeComparision extends ValueComparison { + + PrimaryTypeComparision(String value) { + super(JCR_PRIMARY_TYPE, value); + } + } + + /** + * A name expression. + */ + private static class NameExpression implements Expression { + + private final String value; + + NameExpression(String value) { + String tmp = value; + tmp = tmp.replaceAll("'", "''"); + tmp = tmp.replaceAll("\\*", "\\%"); + tmp = tmp.replaceAll("\\?", "\\_"); + tmp = tmp.toLowerCase(); + this.value = tmp; + } + + public void toString(StringBuffer buffer) + throws RepositoryException { + buffer.append("jcr:like(fn:lower-case(fn:name()), '"); + buffer.append(value); + buffer.append("')"); + } + } + + /** + * A single contains expression. + */ + private final class ContainsExpression extends PropertyExpression { + + private boolean prohibited = false; + + ContainsExpression(String property, String value) { + super(property, checkProhibited(value.toLowerCase())); + this.prohibited = value.startsWith("-"); + } + + public void toString(StringBuffer buffer) + throws RepositoryException { + if (property.equals(NATIVE_XPATH)) { + buffer.append(value); + return; + } + if (prohibited) { + buffer.append("not("); + } + buffer.append(JCR_CONTAINS).append("("); + if (property.length() == 0) { + // node scope + if (commonPathPrefix == null) { + buffer.append("."); + } else { + buffer.append(ISO9075.encodePath(commonPathPrefix)); + } + } else { + // property scope + String[] parts = Text.explode(property, '/'); + if (commonPathPrefix != null) { + buffer.append(ISO9075.encodePath(commonPathPrefix)); + buffer.append("/"); + } + String slash = ""; + for (int i = 0; i < parts.length; i++) { + if (i == parts.length - 1) { + if (!parts[i].equals(".")) { + // last part + buffer.append(slash); + buffer.append("@"); + buffer.append(ISO9075.encode( + resolvePropertyName(parts[i]))); + } + } else { + buffer.append(slash); + buffer.append(ISO9075.encode( + resolveChildNodeName(parts[i]))); + } + slash = "/"; + } + } + buffer.append(", '"); + // properly escape apostrophe. See JCR-3157 + String escapedValue = value.replaceAll("'", "\\\\''"); + if (value.indexOf(' ') != -1) { + // phrase + buffer.append('"').append(escapedValue).append('"'); + } else { + buffer.append(escapedValue); + } + buffer.append("')"); + if (prohibited) { + buffer.append(")"); + } + } + } + + /** + * Base class for n-ary expression. + */ + private abstract class NAryExpression implements Expression { + + private final List operands = new ArrayList(); + + public void toString(StringBuffer buffer) + throws RepositoryException { + if (operands.size() > 1) { + buffer.append("("); + } + String op = ""; + for (Expression expr : operands) { + buffer.append(op); + expr.toString(buffer); + op = getOperation(); + } + if (operands.size() > 1) { + buffer.append(")"); + } + } + + void addOperand(Expression expr) { + operands.add(expr); + } + + int getSize() { + return operands.size(); + } + + protected abstract String getOperation(); + } + + /** + * An expression that requires all its operands to match. + */ + private class RequiredExpression extends NAryExpression { + + protected String getOperation() { + return " and "; + } + } + + /** + * An expression where at least one operand must match. + */ + private class OptionalExpression extends NAryExpression { + + protected String getOperation() { + return " or "; + } + } + + /** + * An order by expression. + */ + private class OrderByExpression implements Expression { + + private final String value; + + OrderByExpression() { + this.value = ""; + } + + OrderByExpression(String value) { + this.value = value; + } + + public void toString(StringBuffer buffer) + throws RepositoryException { + int start = buffer.length(); + buffer.append("order by "); + List names = new ArrayList(Arrays.asList(Text.explode(value, ','))); + int length = buffer.length(); + String comma = ""; + for (String name : names) { + boolean asc; + if (name.equals("-")) { + // no order by at all + buffer.delete(start, buffer.length()); + return; + } + if (name.startsWith("-")) { + name = name.substring(1); + asc = false; + } else if (name.startsWith("+")) { + name = name.substring(1); + asc = true; + } else { + // default is ascending + asc = true; + } + if (name.length() > 0) { + buffer.append(comma); + name = createPropertyName(resolvePropertyName(name)); + buffer.append(name); + if (!asc) { + buffer.append(" ").append(DESCENDING); + } + comma = ", "; + } + } + if (buffer.length() == length) { + // no order by + defaultOrderBy(buffer); + } + } + + private String createPropertyName(String name) { + if (name.contains("/")) { + String[] labels = name.split("/"); + + name = ""; + for (int i = 0; i < labels.length; i++) { + String label = ISO9075.encode(labels[i]); + if (i < (labels.length - 1)) { + name += label + "/"; + } else { + name += "@" + label; + } + } + return name; + } else { + return "@" + ISO9075.encode(name); + } + } + + private void defaultOrderBy(StringBuffer buffer) { + buffer.append("@").append(JCR_SCORE).append(" ").append(DESCENDING); + } + } + + /** + * A row adapter for rep:excerpt values in the result. + */ + private static final class RowAdapter implements Row { + + private final Row row; + + private final String excerptPath; + + private RowAdapter(Row row, String excerptPath) { + this.row = row; + this.excerptPath = excerptPath; + } + + public Value[] getValues() throws RepositoryException { + return row.getValues(); + } + + public Value getValue(String propertyName) throws ItemNotFoundException, RepositoryException { + if (propertyName.startsWith(REP_EXCERPT)) { + propertyName = REP_EXCERPT + "(" + excerptPath + ")"; + } + return row.getValue(propertyName); + } + + public Node getNode() throws RepositoryException { + return row.getNode(); + } + + public Node getNode(String selectorName) throws RepositoryException { + return row.getNode(selectorName); + } + + public String getPath() throws RepositoryException { + return row.getPath(); + } + + public String getPath(String selectorName) throws RepositoryException { + return row.getPath(selectorName); + } + + public double getScore() throws RepositoryException { + return row.getScore(); + } + + public double getScore(String selectorName) throws RepositoryException { + return row.getScore(selectorName); + } + } + + /** + * Customized row iterator adapter, which returns {@link RowAdapter}. + */ + private final class RowIterAdapter extends RowIteratorAdapter { + + private final long size; + + public RowIterAdapter(RangeIterator rangeIterator, long size) { + super(rangeIterator); + this.size = size; + } + + public RowIterAdapter(Collection collection, long size) { + super(collection); + this.size = size; + } + + public Row nextRow() { + Row next = super.nextRow(); + if (commonPathPrefix != null) { + next = new RowAdapter(next, commonPathPrefix); + } else { + next = new RowAdapter(next, "."); + } + return next; + } + + public long getSize() { + return size; + } + } + + /** + * A row iterator that applies a {@link GQL#filter} on the underlying rows. + */ + private final class FilteredRowIterator implements RowIterator { + + /** + * The underlying rows. + */ + private final RowIterator rows; + + /** + * The next row to return or null if there is none. + */ + private Row next; + + /** + * The current position. + */ + private long position = 0; + + /** + * Filters the given rows. + * + * @param rows the rows to filter. + */ + public FilteredRowIterator(RowIterator rows) { + this.rows = rows; + fetchNext(); + } + + /** + * {@inheritDoc} + */ + public void skip(long skipNum) { + while (skipNum-- > 0 && hasNext()) { + nextRow(); + } + } + + /** + * {@inheritDoc} + */ + public long getSize() { + return -1; + } + + /** + * {@inheritDoc} + */ + public long getPosition() { + return position; + } + + /** + * {@inheritDoc} + */ + public Object next() { + return nextRow(); + } + + /** + * {@inheritDoc} + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Row nextRow() { + if (next == null) { + throw new NoSuchElementException(); + } else { + try { + return next; + } finally { + position++; + fetchNext(); + } + } + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + return next != null; + } + + /** + * Fetches the next {@link Row}. + */ + private void fetchNext() { + next = null; + while (next == null && rows.hasNext()) { + Row r = rows.nextRow(); + try { + if (filter.include(r)) { + next = r; + return; + } + } catch (RepositoryException e) { + // ignore + } + } + } + } +} + diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryObjectModelBuilder.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryObjectModelBuilder.java new file mode 100644 index 00000000000..d551cc64329 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryObjectModelBuilder.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.commons.query; + +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.ValueFactory; +import javax.jcr.RepositoryException; + +/** + * QueryObjectModelBuilder defines an interface for building a + * query object model from a string based query statement and vice versa. + */ +public interface QueryObjectModelBuilder { + + /** + * Creates a new query object model from the given statement + * using the passed QOM and value factory. + * + * @param statement the query statement. + * @param qf the query object model factory. + * @param vf the value factory. + * @return the query object model for the given statement. + * @throws InvalidQueryException if the statement is invalid. + * @throws RepositoryException if another error occurs. + */ + public QueryObjectModel createQueryObjectModel(String statement, + QueryObjectModelFactory qf, + ValueFactory vf) + throws InvalidQueryException, RepositoryException; + + /** + * Returns true if this QOM builder can handle a statement in + * language. + * + * @param language the language of a query statement to build a QOM. + * @return true if this builder can handle + * language; false otherwise. + */ + boolean canHandle(String language); + + /** + * Returns the set of query languages supported by this builder. + * + * @return String array containing the names of the supported languages. + */ + String[] getSupportedLanguages(); + + /** + * Creates a String representation of the query object model in the syntax + * this QueryObjectModelBuilder can handle. + * + * @param qom the query object model. + * @return a String representation of the QOM. + * @throws InvalidQueryException if the query object model cannot be + * converted into a String representation due + * to restrictions in this syntax. + */ + String toString(QueryObjectModel qom) + throws InvalidQueryException; +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryObjectModelBuilderRegistry.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryObjectModelBuilderRegistry.java new file mode 100644 index 00000000000..f99da511602 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/QueryObjectModelBuilderRegistry.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.commons.query; + +import java.util.List; +import java.util.ServiceLoader; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Arrays; +import java.util.Collections; + +import javax.jcr.query.InvalidQueryException; + +/** + * Implements a central access to QueryObjectModelBuilder instances. + */ +public class QueryObjectModelBuilderRegistry { + + /** + * List of QueryObjectModelBuilder instances known to the classloader. + */ + private static final List BUILDERS = new ArrayList(); + + /** + * Set of languages known to the registered builders. + */ + private static final Set LANGUAGES; + + static { + Set languages = new HashSet(); + Iterator it = ServiceLoader.load(QueryObjectModelBuilder.class, + QueryObjectModelBuilder.class.getClassLoader()).iterator(); + while (it.hasNext()) { + QueryObjectModelBuilder builder = it.next(); + BUILDERS.add(builder); + languages.addAll(Arrays.asList(builder.getSupportedLanguages())); + } + LANGUAGES = Collections.unmodifiableSet(languages); + } + + /** + * Returns the QueryObjectModelBuilder for + * language. + * + * @param language the language of the query statement. + * @return the QueryObjectModelBuilder for + * language. + * @throws InvalidQueryException if there is no query object model builder + * for language. + */ + public static QueryObjectModelBuilder getQueryObjectModelBuilder(String language) + throws InvalidQueryException { + for (QueryObjectModelBuilder builder : BUILDERS) { + if (builder.canHandle(language)) { + return builder; + } + } + throw new InvalidQueryException("Unsupported language: " + language); + } + + /** + * Returns the set of query languages supported by all registered + * {@link QueryObjectModelBuilder} implementations. + * + * @return String array containing the names of the supported languages. + */ + public static String[] getSupportedLanguages() { + return LANGUAGES.toArray(new String[LANGUAGES.size()]); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/package-info.java new file mode 100644 index 00000000000..c62d59ceced --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.3.1") +package org.apache.jackrabbit.commons.query; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/JoinType.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/JoinType.java new file mode 100644 index 00000000000..298ee64a68e --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/JoinType.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.commons.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; + + private 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, 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 + */ + 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 + * @throws RepositoryException if the given name is unknown + */ + public static JoinType getJoinTypeByName(String name) + throws RepositoryException { + for (JoinType type : JoinType.values()) { + if (type.name.equals(name)) { + return type; + } + } + throw new RepositoryException("Unknown join type name: " + name); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/OperandEvaluator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/OperandEvaluator.java new file mode 100644 index 00000000000..8be348d7693 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/OperandEvaluator.java @@ -0,0 +1,507 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.query.qom; + +import java.util.Locale; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +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.query.Row; +import javax.jcr.query.qom.BindVariableValue; +import javax.jcr.query.qom.FullTextSearchScore; +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.Operand; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.StaticOperand; +import javax.jcr.query.qom.UpperCase; + +/** + * Evaluator of QOM {@link Operand operands}. This class evaluates operands + * in the context of a {@link ValueFactory value factory}, a set of bind + * variables and possibly a query result row. + */ +public class OperandEvaluator { + + /** Value factory */ + private final ValueFactory factory; + + /** Bind variables */ + private final Map variables; + + /** The locale to use in upper- and lower-case conversion. */ + private final Locale locale; + + /** + * Creates an operand evaluator for the given value factory and set of + * bind variables. Upper- and lower-case conversions are performed using + * the given locale. + * + * @param factory value factory + * @param variables bind variables + * @param locale locale to use in upper- and lower-case conversions + */ + public OperandEvaluator( + ValueFactory factory, Map variables, Locale locale) { + this.factory = factory; + this.variables = variables; + this.locale = locale; + } + + /** + * Creates an operand evaluator for the given value factory and set of + * bind variables. Upper- and lower-case conversions are performed using + * the {@link Locale#ENGLISH}. + * + * @param factory value factory + * @param variables bind variables + */ + public OperandEvaluator( + ValueFactory factory, Map variables) { + this(factory, variables, Locale.ENGLISH); + } + + /** + * Returns the value of the given static operand + * ({@link Literal literal} or {@link BindVariableValue bind variable}) + * casted to the given type. + * + * @param operand static operand to be evaluated + * @param type expected value type + * @return evaluated value, casted to the given type + * @throws RepositoryException if a named bind variable is not found, + * if the operand type is unknown, or + * if the type conversion fails + */ + public Value getValue(StaticOperand operand, int type) + throws RepositoryException { + Value value = getValue(operand); + if (type == PropertyType.UNDEFINED || type == value.getType()) { + return value; + } if (type == PropertyType.LONG) { + return factory.createValue(value.getLong()); + } if (type == PropertyType.DOUBLE) { + return factory.createValue(value.getDouble()); + } if (type == PropertyType.DATE) { + return factory.createValue(value.getDate()); + } else { + return factory.createValue(value.getString(), type); + } + } + + /** + * Returns the value of the given static operand + * ({@link Literal literal} or {@link BindVariableValue bind variable}). + * + * @param operand static operand to be evaluated + * @return evaluated value + * @throws RepositoryException if a named bind variable is not found, + * or if the operand type is unknown + */ + public Value getValue(StaticOperand operand) throws RepositoryException { + if (operand instanceof Literal) { + Literal literal = (Literal) operand; + return literal.getLiteralValue(); + } else if (operand instanceof BindVariableValue) { + BindVariableValue bvv = (BindVariableValue) operand; + Value value = variables.get(bvv.getBindVariableName()); + if (value != null) { + return value; + } else { + throw new RepositoryException( + "Unknown bind variable: " + bvv.getBindVariableName()); + } + } else { + throw new UnsupportedRepositoryOperationException( + "Unknown static operand type: " + operand); + } + } + + /** + * Returns the value of the given operand in the context of the given row. + * This is a convenience method that uses a somewhat lossy best-effort + * mapping to evaluate multi-valued operands to a single value. Use the + * {@link #getValues(Operand, Row)} method for more accurate results. + * + * @param operand operand to be evaluated + * @param row query result row + * @return evaluated value + * @throws RepositoryException if the operand can't be evaluated + */ + public Value getValue(Operand operand, Row row) throws RepositoryException { + Value[] values = getValues(operand, row); + if (values.length == 1) { + return values[0]; + } else { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + builder.append(' '); + } + builder.append(values[i].getString()); + } + return factory.createValue(builder.toString()); + } + } + + /** + * Evaluates the given operand in the context of the given row. + * + * @param operand operand to be evaluated + * @param row query result row + * @return values of the operand at the given row + * @throws RepositoryException if the operand can't be evaluated + */ + public Value[] getValues(Operand operand, Row row) + throws RepositoryException { + if (operand instanceof StaticOperand) { + StaticOperand so = (StaticOperand) operand; + return new Value[] { getValue(so) }; + } else if (operand instanceof FullTextSearchScore) { + FullTextSearchScore ftss = (FullTextSearchScore) operand; + double score = row.getScore(ftss.getSelectorName()); + return new Value[] { factory.createValue(score) }; + } else if (operand instanceof NodeName) { + NodeName nn = (NodeName) operand; + String name = row.getNode(nn.getSelectorName()).getName(); + // root node + if ("".equals(name)) { + return new Value[] { factory.createValue(name, + PropertyType.STRING) }; + } + return new Value[] { factory.createValue(name, PropertyType.NAME) }; + } else if (operand instanceof Length) { + return getLengthValues((Length) operand, row); + } else if (operand instanceof LowerCase) { + return getLowerCaseValues((LowerCase) operand, row); + } else if (operand instanceof UpperCase) { + return getUpperCaseValues((UpperCase) operand, row); + } else if (operand instanceof NodeLocalName) { + return getNodeLocalNameValues((NodeLocalName) operand, row); + } else if (operand instanceof PropertyValue) { + return getPropertyValues((PropertyValue) operand, row); + } else { + throw new UnsupportedRepositoryOperationException( + "Unknown operand type: " + operand); + } + } + + /** + * Evaluates the given operand in the context of the given node. + * + * @param operand operand to be evaluated + * @param node node + * @return values of the operand at the given node + * @throws RepositoryException if the operand can't be evaluated + */ + public Value[] getValues(Operand operand, Node node) + throws RepositoryException { + if (operand instanceof StaticOperand) { + StaticOperand so = (StaticOperand) operand; + return new Value[] { getValue(so) }; + } + if (operand instanceof FullTextSearchScore) { + final double defaultScore = 0.0; + return new Value[] { factory.createValue(defaultScore) }; + } + if (operand instanceof NodeName) { + String name = node.getName(); + // root node + if ("".equals(name)) { + return new Value[] { factory.createValue(name, + PropertyType.STRING) }; + } + return new Value[] { factory.createValue(name, PropertyType.NAME) }; + } + if (operand instanceof Length) { + return getLengthValues((Length) operand, node); + } + if (operand instanceof LowerCase) { + return getLowerCaseValues((LowerCase) operand, node); + } + if (operand instanceof UpperCase) { + return getUpperCaseValues((UpperCase) operand, node); + } + if (operand instanceof NodeLocalName) { + return getNodeLocalNameValues((NodeLocalName) operand, node); + } + if (operand instanceof PropertyValue) { + return getPropertyValues((PropertyValue) operand, node); + } + throw new UnsupportedRepositoryOperationException( + "Unknown operand type: " + operand); + } + + /** + * Returns the values of the given value length operand at the given row. + * + * @see #getProperty(PropertyValue, Row) + * @param operand value length operand + * @param row row + * @return values of the operand at the given row + * @throws RepositoryException if the operand can't be evaluated + */ + private Value[] getLengthValues(Length operand, Row row) + throws RepositoryException { + Property property = getProperty(operand.getPropertyValue(), row); + if (property == null) { + return new Value[0]; + } else if (property.isMultiple()) { + long[] lengths = property.getLengths(); + Value[] values = new Value[lengths.length]; + for (int i = 0; i < lengths.length; i++) { + values[i] = factory.createValue(lengths[i]); + } + return values; + } else { + long length = property.getLength(); + return new Value[] { factory.createValue(length) }; + } + } + + /** + * Returns the values of the given value length operand for the given node. + * + * @see #getProperty(PropertyValue, Node) + * @param operand value length operand + * @param node node + * @return values of the operand for the given node + * @throws RepositoryException if the operand can't be evaluated + */ + private Value[] getLengthValues(Length operand, Node n) + throws RepositoryException { + Property property = getProperty(operand.getPropertyValue(), n); + if (property == null) { + return new Value[0]; + } + if (property.isMultiple()) { + long[] lengths = property.getLengths(); + Value[] values = new Value[lengths.length]; + for (int i = 0; i < lengths.length; i++) { + values[i] = factory.createValue(lengths[i]); + } + return values; + } + long length = property.getLength(); + return new Value[] { factory.createValue(length) }; + } + + /** + * Returns the values of the given lower case operand at the given row. + * + * @param operand lower case operand + * @param row row + * @return values of the operand at the given row + * @throws RepositoryException if the operand can't be evaluated + */ + private Value[] getLowerCaseValues(LowerCase operand, Row row) + throws RepositoryException { + Value[] values = getValues(operand.getOperand(), row); + for (int i = 0; i < values.length; i++) { + String value = values[i].getString(); + String lower = value.toLowerCase(locale); + if (!value.equals(lower)) { + values[i] = factory.createValue(lower); + } + } + return values; + } + + /** + * Returns the values of the given lower case operand for the given node. + * + * @param operand lower case operand + * @param node node + * @return values of the operand for the given node + * @throws RepositoryException if the operand can't be evaluated + */ + private Value[] getLowerCaseValues(LowerCase operand, Node node) + throws RepositoryException { + Value[] values = getValues(operand.getOperand(), node); + for (int i = 0; i < values.length; i++) { + String value = values[i].getString(); + String lower = value.toLowerCase(locale); + if (!value.equals(lower)) { + values[i] = factory.createValue(lower); + } + } + return values; + } + + /** + * Returns the values of the given upper case operand at the given row. + * + * @param operand upper case operand + * @param row row + * @return values of the operand at the given row + * @throws RepositoryException if the operand can't be evaluated + */ + private Value[] getUpperCaseValues(UpperCase operand, Row row) + throws RepositoryException { + Value[] values = getValues(operand.getOperand(), row); + for (int i = 0; i < values.length; i++) { + String value = values[i].getString(); + String upper = value.toUpperCase(locale); + if (!value.equals(upper)) { + values[i] = factory.createValue(upper); + } + } + return values; + } + + /** + * Returns the values of the given upper case operand for the given node. + * + * @param operand upper case operand + * @param node node + * @return values of the operand for the given node + * @throws RepositoryException if the operand can't be evaluated + */ + private Value[] getUpperCaseValues(UpperCase operand, Node node) + throws RepositoryException { + Value[] values = getValues(operand.getOperand(), node); + for (int i = 0; i < values.length; i++) { + String value = values[i].getString(); + String upper = value.toUpperCase(locale); + if (!value.equals(upper)) { + values[i] = factory.createValue(upper); + } + } + return values; + } + + /** + * Returns the value of the given local name operand at the given row. + * + * @param operand local name operand + * @param row row + * @return value of the operand at the given row + * @throws RepositoryException if the operand can't be evaluated + */ + private Value[] getNodeLocalNameValues(NodeLocalName operand, Row row) + throws RepositoryException { + return getNodeLocalNameValues(operand, + row.getNode(operand.getSelectorName())); + } + + /** + * Returns the value of the given local name operand for the given node. + * + * @param operand + * local name operand + * @param node + * node + * @return value of the operand for the given node + * @throws RepositoryException + */ + private Value[] getNodeLocalNameValues(NodeLocalName operand, Node node) + throws RepositoryException { + String name = node.getName(); + + // root node has no local name + if ("".equals(name)) { + return new Value[] { factory.createValue("", PropertyType.STRING) }; + } + int colon = name.indexOf(':'); + if (colon != -1) { + name = name.substring(colon + 1); + } + return new Value[] { factory.createValue(name, PropertyType.NAME) }; + } + + /** + * Returns the values of the given property value operand at the given row. + * + * @see #getProperty(PropertyValue, Row) + * @param operand property value operand + * @param row row + * @return values of the operand at the given row + * @throws RepositoryException if the operand can't be evaluated + */ + private Value[] getPropertyValues(PropertyValue operand, Row row) + throws RepositoryException { + Property property = getProperty(operand, row); + if (property == null) { + return new Value[0]; + } else if (property.isMultiple()) { + return property.getValues(); + } else { + return new Value[] { property.getValue() }; + } + } + + private Value[] getPropertyValues(PropertyValue operand, Node node) + throws RepositoryException { + Property property = getProperty(operand, node); + if (property == null) { + return new Value[0]; + } else if (property.isMultiple()) { + return property.getValues(); + } else { + return new Value[] { property.getValue() }; + } + } + + /** + * Returns the identified property from the given row. This method + * is used by both the {@link #getValue(Length, Row)} and the + * {@link #getValue(PropertyValue, Row)} methods to access properties. + * + * @param operand property value operand + * @param row row + * @return the identified property, + * or null if the property does not exist + * @throws RepositoryException if the property can't be accessed + */ + private Property getProperty(PropertyValue operand, Row row) + throws RepositoryException { + return getProperty(operand, row.getNode(operand.getSelectorName())); + } + + /** + * Returns the identified property from the given node. + * + * Can return null is the property doesn't exist or it is not + * accessible. + * + * @param operand + * @param node + * @return identified property + * @throws RepositoryException + */ + private Property getProperty(PropertyValue operand, Node node) + throws RepositoryException { + if (node == null) { + return null; + } + try { + return node.getProperty(operand.getPropertyName()); + } catch (PathNotFoundException e) { + return null; + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/Operator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/Operator.java new file mode 100644 index 00000000000..59ec0871a73 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/Operator.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.commons.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; + + private Operator(String name, String op) { + this(name, op, op); + } + + private 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, a op b or + * jcr:like(a, b) for {@link #LIKE} + */ + public String formatXpath(String a, String b) { + if (this == LIKE) { + return "jcr:like(" + a + ", " + b + ")"; + } else { + 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, 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 + */ + 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 + * @throws RepositoryException if the given name is unknown + */ + public static Operator getOperatorByName(String name) + throws RepositoryException { + for (Operator operator : Operator.values()) { + if (operator.name.equals(name)) { + return operator; + } + } + throw new RepositoryException("Unknown operator name: " + name); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/Order.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/Order.java new file mode 100644 index 00000000000..d7c21e90881 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/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.commons.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; + + private 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 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/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/package-info.java new file mode 100644 index 00000000000..c878ae32204 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/qom/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.3.1") +package org.apache.jackrabbit.commons.query.qom; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/Parser.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/Parser.java new file mode 100644 index 00000000000..d2a00ecdd2b --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/Parser.java @@ -0,0 +1,1005 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.query.sql2; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.BindVariableValue; +import javax.jcr.query.qom.Column; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.Literal; +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.Selector; +import javax.jcr.query.qom.Source; +import javax.jcr.query.qom.StaticOperand; + +import org.apache.jackrabbit.commons.query.qom.JoinType; +import org.apache.jackrabbit.commons.query.qom.Operator; + +/** + * The SQL2 parser can convert a JCR-SQL2 query to a QueryObjectModel. + */ +public class Parser { + + // 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 Value 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, allowNumberLiterals = true; + + private QueryObjectModelFactory factory; + private ValueFactory valueFactory; + + /** + * Create a new parser. A parser can be re-used, but it is not thread safe. + * + * @param factory the query object model factory + * @param valueFactory the value factory + */ + public Parser(QueryObjectModelFactory factory, ValueFactory valueFactory) { + this.factory = factory; + this.valueFactory = valueFactory; + } + + /** + * Parse a JCR-SQL2 query and return the query object model + * + * @param query the query string + * @return the query object model + * @throws RepositoryException if parsing failed + */ + public QueryObjectModel createQueryObjectModel(String query) throws RepositoryException { + initialize(query); + selectors = new ArrayList(); + expected = new ArrayList(); + bindVariables = new HashMap(); + read(); + read("SELECT"); + int columnParseIndex = parseIndex; + ArrayList list = parseColumns(); + read("FROM"); + Source source = parseSource(); + Column[] columnArray = resolveColumns(columnParseIndex, list); + Constraint constraint = null; + if (readIf("WHERE")) { + constraint = parseConstraint(); + } + Ordering[] orderings = null; + if (readIf("ORDER")) { + read("BY"); + orderings = parseOrder(); + } + if (currentToken.length() > 0) { + throw getSyntaxError(""); + } + return factory.createQuery(source, constraint, orderings, columnArray); + } + + private Selector parseSelector() throws RepositoryException { + String nodeTypeName = readName(); + if (readIf("AS")) { + String selectorName = readName(); + return factory.selector(nodeTypeName, selectorName); + } else { + return factory.selector(nodeTypeName, nodeTypeName); + } + } + + private String readName() throws RepositoryException { + if (readIf("[")) { + if (currentTokenType == VALUE) { + Value value = readString(); + read("]"); + return value.getString(); + } 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 Source parseSource() throws RepositoryException { + Selector selector = parseSelector(); + selectors.add(selector); + Source source = selector; + while (true) { + JoinType type; + if (readIf("RIGHT")) { + read("OUTER"); + type = JoinType.RIGHT; + } else if (readIf("LEFT")) { + read("OUTER"); + type = JoinType.LEFT; + } else if (readIf("INNER")) { + type = JoinType.INNER; + } else { + break; + } + read("JOIN"); + selector = parseSelector(); + selectors.add(selector); + read("ON"); + JoinCondition on = parseJoinCondition(); + source = type.join(factory, source, selector, on); + } + return source; + } + + private JoinCondition parseJoinCondition() throws RepositoryException { + boolean identifier = currentTokenType == IDENTIFIER; + String name = readName(); + JoinCondition c; + if (identifier && readIf("(")) { + if ("ISSAMENODE".equalsIgnoreCase(name)) { + String selector1 = readName(); + read(","); + String selector2 = readName(); + if (readIf(",")) { + c = factory.sameNodeJoinCondition(selector1, selector2, readPath()); + } else { + 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 Constraint parseConstraint() throws RepositoryException { + Constraint a = parseAnd(); + while (readIf("OR")) { + a = factory.or(a, parseAnd()); + } + return a; + } + + private Constraint parseAnd() throws RepositoryException { + Constraint a = parseCondition(); + while (readIf("AND")) { + a = factory.and(a, parseCondition()); + } + return a; + } + + private Constraint parseCondition() throws RepositoryException { + Constraint a; + if (readIf("NOT")) { + a = factory.not(parseConstraint()); + } else if (readIf("(")) { + a = parseConstraint(); + read(")"); + } else if (currentTokenType == IDENTIFIER) { + String identifier = readName(); + if (readIf("(")) { + a = parseConditionFuntionIf(identifier); + if (a == null) { + DynamicOperand op = parseExpressionFunction(identifier); + a = parseCondition(op); + } + } else if (readIf(".")) { + a = parseCondition(factory.propertyValue(identifier, readName())); + } else { + a = parseCondition(factory.propertyValue(getOnlySelectorName(identifier), identifier)); + } + } else if ("[".equals(currentToken)) { + String name = readName(); + if (readIf(".")) { + a = parseCondition(factory.propertyValue(name, readName())); + } else { + a = parseCondition(factory.propertyValue(getOnlySelectorName(name), name)); + } + } else { + throw getSyntaxError(); + } + return a; + } + + private Constraint parseCondition(DynamicOperand left) throws RepositoryException { + Constraint c; + if (readIf("=")) { + c = Operator.EQ.comparison(factory, left, parseStaticOperand()); + } else if (readIf("<>")) { + c = Operator.NE.comparison(factory, left, parseStaticOperand()); + } else if (readIf("<")) { + c = Operator.LT.comparison(factory, left, parseStaticOperand()); + } else if (readIf(">")) { + c = Operator.GT.comparison(factory, left, parseStaticOperand()); + } else if (readIf("<=")) { + c = Operator.LE.comparison(factory, left, parseStaticOperand()); + } else if (readIf(">=")) { + c = Operator.GE.comparison(factory, left, parseStaticOperand()); + } else if (readIf("LIKE")) { + c = Operator.LIKE.comparison(factory, left, parseStaticOperand()); + } else if (readIf("IS")) { + boolean not = readIf("NOT"); + read("NULL"); + if (!(left instanceof PropertyValue)) { + throw getSyntaxError("propertyName (NOT NULL is only supported for properties)"); + } + PropertyValue p = (PropertyValue) left; + c = getPropertyExistence(p); + if (!not) { + c = factory.not(c); + } + } else if (readIf("NOT")) { + if (readIf("IS")) { + read("NULL"); + if (!(left instanceof PropertyValue)) { + throw new RepositoryException( + "Only property values can be tested for NOT IS NULL; got: " + + left.getClass().getName()); + } + PropertyValue pv = (PropertyValue) left; + c = getPropertyExistence(pv); + } else { + read("LIKE"); + c = factory.not(Operator.LIKE.comparison( + factory, left, parseStaticOperand())); + } + } else { + throw getSyntaxError(); + } + return c; + } + + private PropertyExistence getPropertyExistence(PropertyValue p) throws InvalidQueryException, RepositoryException { + return factory.propertyExistence(p.getSelectorName(), p.getPropertyName()); + } + + private Constraint parseConditionFuntionIf(String functionName) throws RepositoryException { + Constraint c; + if ("CONTAINS".equalsIgnoreCase(functionName)) { + 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), name, + parseStaticOperand()); + } + } else if ("ISSAMENODE".equalsIgnoreCase(functionName)) { + String name = readName(); + if (readIf(",")) { + c = factory.sameNode(name, readPath()); + } else { + c = factory.sameNode(getOnlySelectorName(name), name); + } + } else if ("ISCHILDNODE".equalsIgnoreCase(functionName)) { + String name = readName(); + if (readIf(",")) { + c = factory.childNode(name, readPath()); + } else { + c = factory.childNode(getOnlySelectorName(name), name); + } + } else if ("ISDESCENDANTNODE".equalsIgnoreCase(functionName)) { + String name = readName(); + if (readIf(",")) { + c = factory.descendantNode(name, readPath()); + } else { + c = factory.descendantNode(getOnlySelectorName(name), name); + } + } else { + return null; + } + read(")"); + return c; + } + + private String readPath() throws RepositoryException { + return readName(); + } + + private DynamicOperand parseDynamicOperand() throws RepositoryException { + boolean identifier = currentTokenType == IDENTIFIER; + String name = readName(); + if (identifier && readIf("(")) { + return parseExpressionFunction(name); + } else { + return parsePropertyValue(name); + } + } + + private DynamicOperand parseExpressionFunction(String functionName) throws RepositoryException { + DynamicOperand op; + if ("LENGTH".equalsIgnoreCase(functionName)) { + op = factory.length(parsePropertyValue(readName())); + } else if ("NAME".equalsIgnoreCase(functionName)) { + if (isToken(")")) { + op = factory.nodeName(getOnlySelectorName("NAME()")); + } else { + op = factory.nodeName(readName()); + } + } else if ("LOCALNAME".equalsIgnoreCase(functionName)) { + if (isToken(")")) { + op = factory.nodeLocalName(getOnlySelectorName("LOCALNAME()")); + } else { + op = factory.nodeLocalName(readName()); + } + } else if ("SCORE".equalsIgnoreCase(functionName)) { + if (isToken(")")) { + op = factory.fullTextSearchScore(getOnlySelectorName("SCORE()")); + } else { + op = factory.fullTextSearchScore(readName()); + } + } else if ("LOWER".equalsIgnoreCase(functionName)) { + op = factory.lowerCase(parseDynamicOperand()); + } else if ("UPPER".equalsIgnoreCase(functionName)) { + op = factory.upperCase(parseDynamicOperand()); + } else { + throw getSyntaxError("LENGTH, NAME, LOCALNAME, SCORE, LOWER, UPPER, or CAST"); + } + read(")"); + return op; + } + + private PropertyValue parsePropertyValue(String name) throws RepositoryException { + if (readIf(".")) { + return factory.propertyValue(name, readName()); + } else { + return factory.propertyValue(getOnlySelectorName(name), name); + } + } + + private StaticOperand parseStaticOperand() throws RepositoryException { + if (currentTokenType == PLUS) { + read(); + } else if (currentTokenType == MINUS) { + read(); + if (currentTokenType != VALUE) { + throw getSyntaxError("number"); + } + int valueType = currentValue.getType(); + switch (valueType) { + case PropertyType.LONG: + currentValue = valueFactory.createValue(-currentValue.getLong()); + break; + case PropertyType.DOUBLE: + currentValue = valueFactory.createValue(-currentValue.getDouble()); + break; + case PropertyType.BOOLEAN: + currentValue = valueFactory.createValue(!currentValue.getBoolean()); + break; + case PropertyType.DECIMAL: + currentValue = valueFactory.createValue(currentValue.getDecimal().negate()); + break; + default: + throw getSyntaxError("Illegal operation: -" + currentValue); + } + } + if (currentTokenType == VALUE) { + Literal literal = getUncastLiteral(currentValue); + read(); + return literal; + } else if (currentTokenType == PARAMETER) { + read(); + String name = readName(); + if (readIf(":")) { + name = name + ":" + readName(); + } + BindVariableValue var = bindVariables.get(name); + if (var == null) { + var = factory.bindVariable(name); + bindVariables.put(name, var); + } + return var; + } else if (readIf("TRUE")) { + Literal literal = getUncastLiteral(valueFactory.createValue(true)); + return literal; + } else if (readIf("FALSE")) { + Literal literal = getUncastLiteral(valueFactory.createValue(false)); + return literal; + } else if (readIf("CAST")) { + read("("); + StaticOperand op = parseStaticOperand(); + if (!(op instanceof Literal)) { + throw getSyntaxError("literal"); + } + Literal literal = (Literal) op; + Value value = literal.getLiteralValue(); + read("AS"); + value = parseCastAs(value); + read(")"); + // CastLiteral + literal = factory.literal(value); + return literal; + } else { + throw getSyntaxError("static operand"); + } + } + + /** + * Create a literal from a parsed value. + * + * @param value the original value + * @return the literal + */ + private Literal getUncastLiteral(Value value) throws RepositoryException { + return factory.literal(value); + } + + private Value parseCastAs(Value value) throws RepositoryException { + if (readIf("STRING")) { + return valueFactory.createValue(value.getString()); + } else if(readIf("BINARY")) { + return valueFactory.createValue(value.getBinary()); + } else if(readIf("DATE")) { + return valueFactory.createValue(value.getDate()); + } else if(readIf("LONG")) { + return valueFactory.createValue(value.getLong()); + } else if(readIf("DOUBLE")) { + return valueFactory.createValue(value.getDouble()); + } else if(readIf("DECIMAL")) { + return valueFactory.createValue(value.getDecimal()); + } else if(readIf("BOOLEAN")) { + return valueFactory.createValue(value.getBoolean()); + } else if(readIf("NAME")) { + return valueFactory.createValue(value.getString(), PropertyType.NAME); + } else if(readIf("PATH")) { + return valueFactory.createValue(value.getString(), PropertyType.PATH); + } else if(readIf("REFERENCE")) { + return valueFactory.createValue(value.getString(), PropertyType.REFERENCE); + } else if(readIf("WEAKREFERENCE")) { + return valueFactory.createValue(value.getString(), PropertyType.WEAKREFERENCE); + } else if(readIf("URI")) { + return valueFactory.createValue(value.getString(), PropertyType.URI); + } else { + throw getSyntaxError("data type (STRING|BINARY|...)"); + } + } + + private Ordering[] parseOrder() throws RepositoryException { + ArrayList orderList = new ArrayList(); + do { + Ordering ordering; + DynamicOperand op = parseDynamicOperand(); + if (readIf("DESC")) { + ordering = factory.descending(op); + } else { + readIf("ASC"); + ordering = factory.ascending(op); + } + orderList.add(ordering); + } while (readIf(",")); + Ordering[] orderings = new Ordering[orderList.size()]; + orderList.toArray(orderings); + return orderings; + } + + private ArrayList parseColumns() throws RepositoryException { + 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 { + column.columnName = column.selectorName + "." + + column.propertyName; + } + } + } else { + if (readIf("AS")) { + column.columnName = readName(); + } + } + list.add(column); + } while (readIf(",")); + } + return list; + } + + private Column[] resolveColumns(int columnParseIndex, ArrayList list) throws RepositoryException { + int oldParseIndex = parseIndex; + // set the parse index to the column list, to get a more meaningful error message + // if something is wrong + this.parseIndex = columnParseIndex; + try { + ArrayList columns = new ArrayList(); + for (ColumnOrWildcard c : list) { + if (c.propertyName == null) { + for (Selector selector : selectors) { + if (c.selectorName == null + || c.selectorName + .equals(selector.getSelectorName())) { + Column column = factory.column(selector + .getSelectorName(), null, null); + columns.add(column); + } + } + } else { + Column 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.propertyName, c.columnName); + } else { + column = factory.column(getOnlySelectorName(c.propertyName), c.propertyName, c.propertyName); + } + columns.add(column); + } + } + Column[] array = new Column[columns.size()]; + columns.toArray(array); + return array; + } finally { + this.parseIndex = oldParseIndex; + } + } + + private boolean readIf(String token) throws RepositoryException { + 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 RepositoryException { + if (!expected.equalsIgnoreCase(currentToken) || currentTokenQuoted) { + throw getSyntaxError(expected); + } + read(); + } + + private String readAny() throws RepositoryException { + if (currentTokenType == END) { + throw getSyntaxError("a token"); + } + String s; + if (currentTokenType == VALUE) { + s = currentValue.getString(); + } else { + s = currentToken; + } + read(); + return s; + } + + private Value readString() throws RepositoryException { + if (currentTokenType != VALUE) { + throw getSyntaxError("string value"); + } + Value value = currentValue; + read(); + return value; + } + + private void addExpected(String token) { + if (expected != null) { + expected.add(token); + } + } + + private void initialize(String query) throws InvalidQueryException { + 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 InvalidQueryException { + if (i >= len) { + parseIndex = startLoop; + throw getSyntaxError(); + } + } + + private void read() throws RepositoryException { + 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) { + break; + } + i++; + } + currentToken = statement.substring(start, i); + if (currentToken.length() == 0) { + 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 = valueFactory.createValue(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, '\"'); + return; + case CHAR_END: + currentToken = ""; + currentTokenType = END; + parseIndex = i; + return; + default: + throw getSyntaxError(); + } + } + + private void readString(int i, char end) throws RepositoryException { + 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 = valueFactory.createValue(result); + parseIndex = i; + currentTokenType = VALUE; + } + + private void checkLiterals(boolean text) throws InvalidQueryException { + if (text && !allowTextLiterals || (!text && !allowNumberLiterals)) { + throw getSyntaxError("bind variable (literals of this type not allowed)"); + } + } + + private void readDecimal(int start, int i) throws RepositoryException { + 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(); + } + do { + i++; // go until the first non-number + } while (types[i] == CHAR_VALUE); + } + parseIndex = i; + String sub = statement.substring(start, i); + BigDecimal bd; + try { + bd = new BigDecimal(sub); + } catch (NumberFormatException e) { + throw new InvalidQueryException("Data conversion error converting " + sub + " to BigDecimal: " + e); + } + checkLiterals(false); + + currentValue = valueFactory.createValue(bd); + currentTokenType = VALUE; + } + + private InvalidQueryException getSyntaxError() { + if (expected == null || expected.size() == 0) { + 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 InvalidQueryException getSyntaxError(String expected) { + int index = Math.min(parseIndex, statement.length() - 1); + String query = statement.substring(0, index) + "(*)" + statement.substring(index).trim(); + if (expected != null) { + query += "; expected: " + expected; + } + return new InvalidQueryException("Query:\n" + query); + } + + /** + * 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. + * + * @param name the property name + * @return the selector name + */ + private String getOnlySelectorName(String propertyName) throws RepositoryException { + if (selectors.size() > 1) { + throw getSyntaxError("Need to specify the selector name for \"" + propertyName + "\" because the query contains more than one selector."); + } + return selectors.get(0).getSelectorName(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/QOMFormatter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/QOMFormatter.java new file mode 100644 index 00000000000..34577f84b02 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/QOMFormatter.java @@ -0,0 +1,572 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.query.sql2; + +import java.util.BitSet; +import java.util.Arrays; + +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.Source; +import javax.jcr.query.qom.Selector; +import javax.jcr.query.qom.Column; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Ordering; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.EquiJoinCondition; +import javax.jcr.query.qom.ChildNodeJoinCondition; +import javax.jcr.query.qom.DescendantNodeJoinCondition; +import javax.jcr.query.qom.SameNodeJoinCondition; +import javax.jcr.query.qom.And; +import javax.jcr.query.qom.ChildNode; +import javax.jcr.query.qom.Comparison; +import javax.jcr.query.qom.DescendantNode; +import javax.jcr.query.qom.FullTextSearch; +import javax.jcr.query.qom.Not; +import javax.jcr.query.qom.Or; +import javax.jcr.query.qom.PropertyExistence; +import javax.jcr.query.qom.SameNode; +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.StaticOperand; +import javax.jcr.query.qom.BindVariableValue; +import javax.jcr.query.qom.Literal; +import javax.jcr.query.qom.FullTextSearchScore; +import javax.jcr.query.qom.Length; +import javax.jcr.query.qom.LowerCase; +import javax.jcr.query.qom.NodeLocalName; +import javax.jcr.query.qom.NodeName; +import javax.jcr.query.qom.PropertyValue; +import javax.jcr.query.qom.UpperCase; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +/** + * QOMFormatter implements a formatter that translates a query + * object model into a JCR_SQL2 string statement. + */ +public class QOMFormatter implements QueryObjectModelConstants { + + /** + * BitSet of valid SQL identifier start characters. + */ + private static final BitSet IDENTIFIER_START = new BitSet(); + + /** + * BitSet of valid SQL identifier body characters. + */ + private static final BitSet IDENTIFIER_PART_OR_UNDERSCORE = new BitSet(); + + static { + for (char c = 'a'; c <= 'z'; c++) { + IDENTIFIER_START.set(c); + } + for (char c = 'A'; c <= 'Z'; c++) { + IDENTIFIER_START.set(c); + } + IDENTIFIER_PART_OR_UNDERSCORE.or(IDENTIFIER_START); + for (char c = '0'; c <= '9'; c++) { + IDENTIFIER_PART_OR_UNDERSCORE.set(c); + } + IDENTIFIER_PART_OR_UNDERSCORE.set('_'); + } + + /** + * The query object model to format. + */ + private final QueryObjectModel qom; + + /** + * The JCR_SQL2 statement. + */ + private final StringBuilder sb = new StringBuilder(); + + /** + * Private constructor. + * + * @param qom the query object model to format. + */ + private QOMFormatter(QueryObjectModel qom) { + this.qom = qom; + } + + /** + * Formats the given qom as a JCR_SQL2 query statement. + * + * @param qom the query object model to translate. + * @return the JCR_SQL2 statement. + * @throws RepositoryException if an error occurs while formatting the qom. + */ + public static String format(QueryObjectModel qom) + throws RepositoryException { + return new QOMFormatter(qom).format(); + } + + private String format() throws RepositoryException { + append("SELECT "); + append(qom.getColumns()); + append(" FROM "); + append(qom.getSource()); + Constraint c = qom.getConstraint(); + if (c != null) { + append(" WHERE "); + append(c); + } + Ordering[] orderings = qom.getOrderings(); + if (orderings.length > 0) { + append(" ORDER BY "); + append(orderings); + } + return sb.toString(); + } + + private void append(Ordering[] orderings) { + String comma = ""; + for (Ordering ordering : orderings) { + append(comma); + comma = ", "; + append(ordering.getOperand()); + if (JCR_ORDER_DESCENDING.equals(ordering.getOrder())) { + append(" DESC"); + } + } + } + + private void append(Constraint c) + throws RepositoryException { + if (c instanceof And) { + append((And) c); + } else if (c instanceof ChildNode) { + append((ChildNode) c); + } else if (c instanceof Comparison) { + append((Comparison) c); + } else if (c instanceof DescendantNode) { + append((DescendantNode) c); + } else if (c instanceof FullTextSearch) { + append((FullTextSearch) c); + } else if (c instanceof Not) { + append((Not) c); + } else if (c instanceof Or) { + append((Or) c); + } else if (c instanceof PropertyExistence) { + append((PropertyExistence) c); + } else { + append((SameNode) c); + } + } + + private void append(And constraint) + throws RepositoryException { + String and = ""; + for (Constraint c : Arrays.asList( + constraint.getConstraint1(), + constraint.getConstraint2())) { + append(and); + and = " AND "; + boolean paren = c instanceof Or || c instanceof Not; + if (paren) { + append("("); + } + append(c); + if (paren) { + append(")"); + } + } + } + + private void append(ChildNode constraint) { + append("ISCHILDNODE("); + appendName(constraint.getSelectorName()); + append(", "); + appendPath(constraint.getParentPath()); + append(")"); + } + + private void append(Comparison constraint) + throws RepositoryException { + append(constraint.getOperand1()); + append(" "); + appendOperator(constraint.getOperator()); + append(" "); + append(constraint.getOperand2()); + } + + private void append(StaticOperand operand) + throws RepositoryException { + if (operand instanceof BindVariableValue) { + append((BindVariableValue) operand); + } else { + append((Literal) operand); + } + } + + private void append(BindVariableValue value) { + append("$"); + append(value.getBindVariableName()); + } + + private void append(Literal value) + throws RepositoryException { + Value v = value.getLiteralValue(); + switch (v.getType()) { + case PropertyType.BINARY: + appendCastLiteral(v.getString(), "BINARY"); + break; + case PropertyType.BOOLEAN: + append(v.getString()); + break; + case PropertyType.DATE: + appendCastLiteral(v.getString(), "DATE"); + break; + case PropertyType.DECIMAL: + appendCastLiteral(v.getString(), "DECIMAL"); + break; + case PropertyType.DOUBLE: + appendCastLiteral(v.getString(), "DOUBLE"); + break; + case PropertyType.LONG: + appendCastLiteral(v.getString(), "LONG"); + break; + case PropertyType.NAME: + appendCastLiteral(v.getString(), "NAME"); + break; + case PropertyType.PATH: + appendCastLiteral(v.getString(), "PATH"); + break; + case PropertyType.REFERENCE: + appendCastLiteral(v.getString(), "REFERENCE"); + break; + case PropertyType.STRING: + appendStringLiteral(v.getString()); + break; + case PropertyType.URI: + appendCastLiteral(v.getString(), "URI"); + break; + case PropertyType.WEAKREFERENCE: + appendCastLiteral(v.getString(), "WEAKREFERENCE"); + break; + } + } + + private void appendCastLiteral(String value, String propertyType) { + append("CAST("); + appendStringLiteral(value); + append(" AS "); + append(propertyType); + append(")"); + } + + private void appendStringLiteral(String value) { + append("'"); + append(value.replaceAll("'", "''")); + append("'"); + } + + private void appendOperator(String operator) { + if (JCR_OPERATOR_EQUAL_TO.equals(operator)) { + append("="); + } else if (JCR_OPERATOR_GREATER_THAN.equals(operator)) { + append(">"); + } else if (JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO.equals(operator)) { + append(">="); + } else if (JCR_OPERATOR_LESS_THAN.equals(operator)) { + append("<"); + } else if (JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO.equals(operator)) { + append("<="); + } else if (JCR_OPERATOR_LIKE.equals(operator)) { + append("LIKE"); + } else { + append("<>"); + } + } + + private void append(DynamicOperand operand) { + if (operand instanceof FullTextSearchScore) { + append((FullTextSearchScore) operand); + } else if (operand instanceof Length) { + append((Length) operand); + } else if (operand instanceof LowerCase) { + append((LowerCase) operand); + } else if (operand instanceof NodeLocalName) { + append((NodeLocalName) operand); + } else if (operand instanceof NodeName) { + append((NodeName) operand); + } else if (operand instanceof PropertyValue) { + append((PropertyValue) operand); + } else { + append((UpperCase) operand); + } + } + + private void append(FullTextSearchScore operand) { + append("SCORE("); + appendName(operand.getSelectorName()); + append(")"); + } + + private void append(Length operand) { + append("LENGTH("); + append(operand.getPropertyValue()); + append(")"); + } + + private void append(LowerCase operand) { + append("LOWER("); + append(operand.getOperand()); + append(")"); + } + + private void append(NodeLocalName operand) { + append("LOCALNAME("); + appendName(operand.getSelectorName()); + append(")"); + } + + private void append(NodeName operand) { + append("NAME("); + appendName(operand.getSelectorName()); + append(")"); + } + + private void append(PropertyValue operand) { + appendName(operand.getSelectorName()); + append("."); + appendName(operand.getPropertyName()); + } + + private void append(UpperCase operand) { + append("UPPER("); + append(operand.getOperand()); + append(")"); + } + + private void append(DescendantNode constraint) { + append("ISDESCENDANTNODE("); + appendName(constraint.getSelectorName()); + append(", "); + appendPath(constraint.getAncestorPath()); + append(")"); + } + + private void append(FullTextSearch constraint) throws RepositoryException { + append("CONTAINS("); + appendName(constraint.getSelectorName()); + append("."); + String propName = constraint.getPropertyName(); + if (propName == null) { + append("*"); + } else { + appendName(propName); + } + append(", "); + append(constraint.getFullTextSearchExpression()); + append(")"); + } + + private void append(Not constraint) throws RepositoryException { + append("NOT "); + Constraint c = constraint.getConstraint(); + boolean paren = c instanceof And || c instanceof Or; + if (paren) { + append("("); + } + append(c); + if (paren) { + append(")"); + } + } + + private void append(Or constraint) throws RepositoryException { + append(constraint.getConstraint1()); + append(" OR "); + append(constraint.getConstraint2()); + } + + private void append(PropertyExistence constraint) { + appendName(constraint.getSelectorName()); + append("."); + appendName(constraint.getPropertyName()); + append(" IS NOT NULL"); + } + + private void append(SameNode constraint) { + append("ISSAMENODE("); + appendName(constraint.getSelectorName()); + append(", "); + appendPath(constraint.getPath()); + append(")"); + } + + private void append(Column[] columns) { + if (columns.length == 0) { + append("*"); + } else { + String comma = ""; + for (Column c : columns) { + append(comma); + comma = ", "; + appendName(c.getSelectorName()); + append("."); + String propName = c.getPropertyName(); + if (propName != null) { + appendName(propName); + if (c.getColumnName() != null) { + append(" AS "); + appendName(c.getColumnName()); + } + } else { + append("*"); + } + } + } + } + + private void append(Source source) { + if (source instanceof Join) { + append((Join) source); + } else { + append((Selector) source); + } + } + + private void append(Join join) { + append(join.getLeft()); + append(" "); + appendJoinType(join.getJoinType()); + append(" JOIN "); + append(join.getRight()); + append(" ON "); + append(join.getJoinCondition()); + } + + private void append(JoinCondition joinCondition) { + if (joinCondition instanceof EquiJoinCondition) { + append((EquiJoinCondition) joinCondition); + } else if (joinCondition instanceof ChildNodeJoinCondition) { + append((ChildNodeJoinCondition) joinCondition); + } else if (joinCondition instanceof DescendantNodeJoinCondition) { + append((DescendantNodeJoinCondition) joinCondition); + } else { + append((SameNodeJoinCondition) joinCondition); + } + } + + private void append(EquiJoinCondition condition) { + appendName(condition.getSelector1Name()); + append("."); + appendName(condition.getProperty1Name()); + append(" = "); + appendName(condition.getSelector2Name()); + append("."); + appendName(condition.getProperty2Name()); + } + + private void append(ChildNodeJoinCondition condition) { + append("ISCHILDNODE("); + appendName(condition.getChildSelectorName()); + append(", "); + appendName(condition.getParentSelectorName()); + append(")"); + } + + private void append(DescendantNodeJoinCondition condition) { + append("ISDESCENDANTNODE("); + appendName(condition.getDescendantSelectorName()); + append(", "); + appendName(condition.getAncestorSelectorName()); + append(")"); + } + + private void append(SameNodeJoinCondition condition) { + append("ISSAMENODE("); + appendName(condition.getSelector1Name()); + append(", "); + appendName(condition.getSelector2Name()); + if (condition.getSelector2Path() != null) { + append(", "); + appendPath(condition.getSelector2Path()); + } + append(")"); + } + + private void appendPath(String path) { + if (isSimpleName(path)) { + append(path); + } else { + boolean needQuotes = path.contains(" "); + append("["); + if (needQuotes) { + append("'"); + } + append(path); + if (needQuotes) { + append("'"); + } + append("]"); + } + } + + private void appendJoinType(String joinType) { + if (joinType.equals(JCR_JOIN_TYPE_INNER)) { + append("INNER"); + } else if (joinType.equals(JCR_JOIN_TYPE_LEFT_OUTER)) { + append("LEFT OUTER"); + } else { + append("RIGHT OUTER"); + } + } + + private void append(Selector selector) { + appendName(selector.getNodeTypeName()); + if (!selector.getSelectorName().equals(selector.getNodeTypeName())) { + append(" AS "); + appendName(selector.getSelectorName()); + } + } + + private void appendName(String name) { + if (isSimpleName(name)) { + append(name); + } else { + append("["); + append(name); + append("]"); + } + } + + private static boolean isSimpleName(String name) { + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (i == 0) { + if (!IDENTIFIER_START.get(c)) { + return false; + } + } else { + if (!IDENTIFIER_PART_OR_UNDERSCORE.get(c)) { + return false; + } + } + } + return true; + } + + private void append(String s) { + sb.append(s); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/SQL2QOMBuilder.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/SQL2QOMBuilder.java new file mode 100644 index 00000000000..0417eefdd21 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/SQL2QOMBuilder.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.commons.query.sql2; + +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.ValueFactory; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.commons.query.QueryObjectModelBuilder; + +/** + * SQL2QOMBuilder implements QOM builder that understands + * {@link Query#JCR_SQL2} and {@link Query#JCR_JQOM}. JCR_JQOM + * might be surprising, but JSR 283 says that the serialization format of + * JCR_JQOM is JCR_SQL2. This is important when + * a JQOM is stored on a node as a serialized String and a language property + * set to JCR_JQOM. + */ +public class SQL2QOMBuilder implements QueryObjectModelBuilder { + + /** + * Supports {@link Query#JCR_JQOM} and {@link Query#JCR_SQL2}. + */ + private static final List SUPPORTED = new ArrayList( + Arrays.asList(Query.JCR_JQOM, Query.JCR_SQL2)); + + /** + * {@inheritDoc} + */ + public QueryObjectModel createQueryObjectModel(String statement, + QueryObjectModelFactory qf, + ValueFactory vf) + throws InvalidQueryException, RepositoryException { + return new Parser(qf, vf).createQueryObjectModel(statement); + } + + /** + * {@inheritDoc} + */ + public boolean canHandle(String language) { + return SUPPORTED.contains(language); + } + + /** + * {@inheritDoc} + */ + public String[] getSupportedLanguages() { + return SUPPORTED.toArray(new String[SUPPORTED.size()]); + } + + /** + * {@inheritDoc} + */ + public String toString(QueryObjectModel qom) + throws InvalidQueryException { + try { + return QOMFormatter.format(qom); + } catch (RepositoryException e) { + throw new InvalidQueryException(e.getMessage(), e); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/package-info.java new file mode 100644 index 00000000000..54558913465 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/query/sql2/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.3.1") +package org.apache.jackrabbit.commons.query.sql2; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/EmptyRepository.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/EmptyRepository.java new file mode 100644 index 00000000000..429bbcf9e92 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/EmptyRepository.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.commons.repository; + +import javax.jcr.Credentials; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.commons.AbstractRepository; + +/** + * An empty repository with no descriptors and no workspaces. This class + * can be used as a dummy sentinel in cases where a proper content repository + * is not available. + * + * @since 1.4 + */ +public class EmptyRepository extends AbstractRepository { + + /** + * Returns null since this repository contains no descriptors. + * + * @param key descriptor key + * @return null + */ + public String getDescriptor(String key) { + return null; + } + + /** + * Returns null since this repository contains no descriptors. + * + * @param key descriptor key + * @return null + */ + public Value getDescriptorValue(String key) { + return null; + } + + /** + * Returns null since this repository contains no descriptors. + * + * @param key descriptor key + * @return null + */ + public Value[] getDescriptorValues(String key) { + return null; + } + + /** + * Returns false since this repository contains no descriptors. + * + * @param key descriptor key + * @return false + */ + public boolean isSingleValueDescriptor(String key) { + return false; + } + + /** + * Returns an empty array since this repository contains no descriptors. + * + * @return empty array + */ + public String[] getDescriptorKeys() { + return new String[0]; + } + + /** + * Throws an exception since this repository contains no workspaces. + * + * @return nothing + * @throws NoSuchWorkspaceException always thrown + */ + public Session login(Credentials credentials, String workspace) + throws NoSuchWorkspaceException { + throw new NoSuchWorkspaceException("Empty repository"); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/JNDIRepository.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/JNDIRepository.java new file mode 100644 index 00000000000..c8eff32952b --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/JNDIRepository.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.commons.repository; + +import javax.naming.Context; + +/** + * Proxy for a repository bound in JNDI. The configured repository is + * looked up from JNDI lazily during each method call. Thus the JNDI entry + * does not need to exist when this class is instantiated. The JNDI entry + * can also be replaced with another repository during the lifetime of an + * instance of this class. + * + * @since 1.4 + */ +public class JNDIRepository extends ProxyRepository { + + /** + * Creates a proxy for a repository in the given JNDI location. + * + * @param context JNDI context + * @param name JNDI name of the proxied repository + */ + public JNDIRepository(Context context, String name) { + super(new JNDIRepositoryFactory(context, name)); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/JNDIRepositoryFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/JNDIRepositoryFactory.java new file mode 100644 index 00000000000..15646532644 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/JNDIRepositoryFactory.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.commons.repository; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.naming.Context; +import javax.naming.NamingException; + +/** + * Factory that looks up a repository from JNDI. + * + * @since 1.4 + */ +public class JNDIRepositoryFactory implements RepositoryFactory { + + /** + * JNDI context from which to look up the repository. + */ + private final Context context; + + /** + * JNDI name of the repository. + */ + private final String name; + + /** + * Creates a factory for looking up a repository from JNDI. + * + * @param context JNDI context + * @param name JNDI name of the repository + */ + public JNDIRepositoryFactory(Context context, String name) { + this.context = context; + this.name = name; + } + + /** + * Looks up and returns the configured repository. + * + * @return repository instance + * @throws RepositoryException if the repository can not be found + */ + public Repository getRepository() throws RepositoryException { + try { + Object repository = context.lookup(name); + if (repository instanceof Repository) { + return (Repository) repository; + } else if (repository == null) { + throw new RepositoryException( + "Repository not found: The JNDI entry " + + name + " is null"); + } else { + throw new RepositoryException( + "Invalid repository: The JNDI entry " + + name + " is an instance of " + + repository.getClass().getName()); + } + } catch (NamingException e) { + throw new RepositoryException( + "Repository not found: The JNDI entry " + name + + " could not be looked up", e); + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/ProxyRepository.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/ProxyRepository.java new file mode 100644 index 00000000000..4279d3edfee --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/ProxyRepository.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.repository; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.commons.AbstractRepository; +import org.apache.jackrabbit.commons.JcrUtils; + +/** + * Repository that proxies all method calls to another repository. + * The other repository is accessed lazily using a + * {@link RepositoryFactory repository factory}. + * + * @since 1.4 + */ +public class ProxyRepository extends AbstractRepository { + + /** + * Factory for accessing the proxied repository. + */ + private final RepositoryFactory factory; + + /** + * Repository access parameters. Used if an explicit repository + * factory has not been configured. + */ + private final Map parameters = + new HashMap(); + + /** + * Creates a proxy for the repository (or repositories) accessible + * through the given factory. + * + * @param factory repository factory + */ + public ProxyRepository(RepositoryFactory factory) { + this.factory = factory; + } + + /** + * Creates a proxy for the repository (or repositories) accessible + * using the given repository parameters. + * + * @param parameters repository parameters + */ + public ProxyRepository(Map parameters) { + this.factory = null; + this.parameters.putAll(parameters); + } + + /** + * Creates a proxy for the repository accessible using the given + * repository URI. + * + * @param uri repository URI + */ + public ProxyRepository(String uri) { + this.factory = null; + this.parameters.put(JcrUtils.REPOSITORY_URI, uri); + } + + /** + * Protected constructor for subclasses that want to override the + * {@link #getRepository()} method. + */ + protected ProxyRepository() { + this.factory = null; + } + + /** + * Returns the proxied repository. Subclasses can override this + * method to implement custom repository access mechanisms. + * + * @return repository + * @throws RepositoryException if the repository can not be accessed + */ + protected Repository getRepository() throws RepositoryException { + if (factory != null) { + return factory.getRepository(); + } else { + return JcrUtils.getRepository(parameters); + } + } + + /** + * Returns the descriptor keys of the proxied repository, or an empty + * array if the proxied repository can not be accessed. + * + * @return descriptor keys (possibly empty) + */ + public String[] getDescriptorKeys() { + try { + return getRepository().getDescriptorKeys(); + } catch (RepositoryException e) { + return new String[0]; + } + } + + /** + * Checks whether the given key identifies a valid single-valued + * descriptor key in the proxied repository. Returns false + * if the proxied repository can not be accessed. + * + * @return true if the key identifies a valid single-valued + * descriptor in the proxied repository, + * false otherwise + */ + public boolean isSingleValueDescriptor(String key) { + try { + return getRepository().isSingleValueDescriptor(key); + } catch (RepositoryException e) { + return false; + } + } + + /** + * Returns the descriptor with the given key from the proxied repository. + * Returns null if the descriptor does not exist or if the + * proxied repository can not be accessed. + * + * @param key descriptor key + * @return descriptor value, or null + */ + public String getDescriptor(String key) { + try { + return getRepository().getDescriptor(key); + } catch (RepositoryException e) { + return null; + } + } + + /** + * Returns the value of the descriptor with the given key from the proxied + * repository. Returns null if the descriptor does not exist + * or if the proxied repository can not be accessed. + * + * @param key descriptor key + * @return descriptor value, or null + */ + public Value getDescriptorValue(String key) { + try { + return getRepository().getDescriptorValue(key); + } catch (RepositoryException e) { + return null; + } + } + + /** + * Returns the values of the descriptor with the given key from the proxied + * repository. Returns null if the descriptor does not exist + * or if the proxied repository can not be accessed. + * + * @param key descriptor key + * @return descriptor values, or null + */ + public Value[] getDescriptorValues(String key) { + try { + return getRepository().getDescriptorValues(key); + } catch (RepositoryException e) { + return null; + } + } + + /** + * Logs in to the proxied repository and returns the resulting session. + *

    + * Note that the {@link Session#getRepository()} method of the resulting + * session will return the proxied repository, not this repository proxy! + * + * @throws RepositoryException if the proxied repository can not be + * accessed, or if the login in the proxied + * repository fails + */ + public Session login(Credentials credentials, String workspace) + throws RepositoryException { + return getRepository().login(credentials, workspace); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/RepositoryFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/RepositoryFactory.java new file mode 100644 index 00000000000..5feb024cdaa --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/RepositoryFactory.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.commons.repository; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +/** + * Factory interface for JCR content repositories. + * + * @since 1.4 + */ +public interface RepositoryFactory { + + /** + * Returns a content repository. + * + * @return content repository + * @throws RepositoryException if a repository is not available + */ + Repository getRepository() throws RepositoryException; + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/SingletonRepositoryFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/SingletonRepositoryFactory.java new file mode 100644 index 00000000000..e47dbd566cf --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/SingletonRepositoryFactory.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.commons.repository; + +import javax.jcr.Repository; + +/** + * Repository factory that always returns the same configured repository. + */ +public class SingletonRepositoryFactory implements RepositoryFactory { + + /** + * Singleton repository instance. + */ + private final Repository repository; + + /** + * Creates a repository factory that always returns the given repository. + * + * @param repository singleton repository instance. + */ + public SingletonRepositoryFactory(Repository repository) { + this.repository = repository; + } + + /** + * Returns the configured repository instance. + * + * @return singleton repository instance + */ + public Repository getRepository() { + return repository; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/package-info.java new file mode 100644 index 00000000000..7b19d66493d --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/repository/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.2") +package org.apache.jackrabbit.commons.repository; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/visitor/FilteringItemVisitor.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/visitor/FilteringItemVisitor.java new file mode 100644 index 00000000000..cf09d550be7 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/visitor/FilteringItemVisitor.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.commons.visitor; + +import java.util.LinkedList; + +import javax.jcr.Item; +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.commons.predicate.Predicate; + +public abstract class FilteringItemVisitor implements ItemVisitor { + + /** + * Predicate that defines which items are included. + */ + protected Predicate includePredicate = Predicate.TRUE; + + /** + * Predicate that defines which items are traversed. + */ + protected Predicate traversalPredicate = Predicate.TRUE; + + /** + * Do we want to walk all properties of nodes? + * The default is false. + */ + protected boolean walkProperties = false; + + /** + * indicates if traversal should be done in a breadth-first + * manner rather than depth-first (which is the default) + */ + protected boolean breadthFirst = false; + + /** + * the 0-based level up to which the hierarchy should be traversed + * (if it's -1, the hierarchy will be traversed until there are no + * more children of the current item) + */ + protected int maxLevel = -1; + + /** + * queues used to implement breadth-first traversal + */ + protected LinkedList currentQueue; + protected LinkedList nextQueue; + + /** + * used to track hierarchy level of item currently being processed + */ + protected int currentLevel; + + public void setMaxLevel(final int ml) { + this.maxLevel = ml; + } + + public void setBreadthFirst(final boolean flag) { + if ( this.breadthFirst != flag ) { + this.breadthFirst = flag; + if (breadthFirst) { + this.currentQueue = new LinkedList(); + this.nextQueue = new LinkedList(); + } else { + this.currentQueue = null; + this.nextQueue = null; + } + + } + } + public void setWalkProperties(final boolean flag) { + this.walkProperties = flag; + } + + public void setIncludePredicate(final Predicate ip) { + this.includePredicate = ip; + } + + public void setTraversalPredicate(final Predicate tp) { + this.traversalPredicate = tp; + } + + /** + * Implement this method to add behaviour performed before a + * Property is visited. + * + * @param property the Property that is accepting this visitor. + * @param level hierarchy level of this property (the root node starts at level 0) + * @throws RepositoryException if an error occurrs + */ + protected abstract void entering(Property property, int level) + throws RepositoryException; + + /** + * Implement this method to add behaviour performed before a + * Node is visited. + * + * @param node the Node that is accepting this visitor. + * @param level hierarchy level of this node (the root node starts at level 0) + * @throws RepositoryException if an error occurrs + */ + protected abstract void entering(Node node, int level) + throws RepositoryException; + + /** + * Implement this method to add behaviour performed after a + * Property is visited. + * + * @param property the Property that is accepting this visitor. + * @param level hierarchy level of this property (the root node starts at level 0) + * @throws RepositoryException if an error occurrs + */ + protected abstract void leaving(Property property, int level) + throws RepositoryException; + + /** + * Implement this method to add behaviour performed after a + * Node is visited. + * + * @param node the Node that is accepting this visitor. + * @param level hierarchy level of this node (the root node starts at level 0) + * @throws RepositoryException if an error occurrs + */ + protected abstract void leaving(Node node, int level) + throws RepositoryException; + + /** + * Called when the Visitor is passed to a Property. + *

    + * It calls TraversingItemVisitor.entering(Property, int) followed by + * TraversingItemVisitor.leaving(Property, int). Implement these abstract methods to + * specify behaviour on 'arrival at' and 'after leaving' the Property. + *

    + *

    + * If this method throws, the visiting process is aborted. + * + * @param property the Property that is accepting this visitor. + * @throws RepositoryException if an error occurrs + */ + public void visit(Property property) throws RepositoryException { + if ( this.walkProperties && this.includePredicate.evaluate(property) ) { + entering(property, currentLevel); + leaving(property, currentLevel); + } + } + + /** + * Called when the Visitor is passed to a Node. + *

    + * It calls TraversingItemVisitor.entering(Node, int) followed by + * TraversingItemVisitor.leaving(Node, int). Implement these abstract methods to + * specify behaviour on 'arrival at' and 'after leaving' the Node. + *

    + * If this method throws, the visiting process is aborted. + * + * @param node the Node that is accepting this visitor. + * @throws RepositoryException if an error occurrs + */ + public void visit(Node node) + throws RepositoryException { + if ( this.traversalPredicate.evaluate(node) ) { + if ( this.includePredicate == this.traversalPredicate || this.includePredicate.evaluate(node) ) { + try { + if (!breadthFirst) { + // depth-first traversal + entering(node, currentLevel); + if (maxLevel == -1 || currentLevel < maxLevel) { + currentLevel++; + if ( this.walkProperties ) { + PropertyIterator propIter = node.getProperties(); + while (propIter.hasNext()) { + propIter.nextProperty().accept(this); + } + } + NodeIterator nodeIter = node.getNodes(); + while (nodeIter.hasNext()) { + nodeIter.nextNode().accept(this); + } + currentLevel--; + } + leaving(node, currentLevel); + } else { + // breadth-first traversal + entering(node, currentLevel); + leaving(node, currentLevel); + + if (maxLevel == -1 || currentLevel < maxLevel) { + if ( this.walkProperties ) { + PropertyIterator propIter = node.getProperties(); + while (propIter.hasNext()) { + nextQueue.addLast(propIter.nextProperty()); + } + } + NodeIterator nodeIter = node.getNodes(); + while (nodeIter.hasNext()) { + nextQueue.addLast(nodeIter.nextNode()); + } + } + + while (!currentQueue.isEmpty() || !nextQueue.isEmpty()) { + if (currentQueue.isEmpty()) { + currentLevel++; + currentQueue = nextQueue; + nextQueue = new LinkedList(); + } + Item e = (Item) currentQueue.removeFirst(); + e.accept(this); + } + currentLevel = 0; + } + } catch (RepositoryException re) { + currentLevel = 0; + throw re; + } + } + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/visitor/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/visitor/package-info.java new file mode 100644 index 00000000000..5f5bbafaaa3 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/visitor/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.2") +package org.apache.jackrabbit.commons.visitor; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/AtomFeedConstants.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/AtomFeedConstants.java new file mode 100644 index 00000000000..c642504f07b --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/AtomFeedConstants.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.commons.webdav; + +import javax.xml.namespace.QName; + +/** + * AtomFeedConstants provides string constants for Atom feed + * (RFC 4287) resources. + */ +public interface AtomFeedConstants { + + /** + * Namespace URI for RFC 4287 elements. + */ + public static final String NS_URI = "http://www.w3.org/2005/Atom"; + + public static final String MEDIATYPE = "application/atom+xml"; + + public static final String XML_AUTHOR = "author"; + public static final String XML_CONTENT = "content"; + public static final String XML_ENTRY = "entry"; + public static final String XML_FEED = "feed"; + public static final String XML_ID = "id"; + public static final String XML_LINK = "link"; + public static final String XML_NAME = "name"; + public static final String XML_TITLE = "title"; + public static final String XML_UPDATED = "updated"; + + public static final QName N_AUTHOR = new QName(NS_URI, XML_AUTHOR); + public static final QName N_CONTENT = new QName(NS_URI, XML_CONTENT); + public static final QName N_ENTRY = new QName(NS_URI, XML_ENTRY); + public static final QName N_FEED = new QName(NS_URI, XML_FEED); + public static final QName N_ID = new QName(NS_URI, XML_ID); + public static final QName N_LINK = new QName(NS_URI, XML_LINK); + public static final QName N_NAME = new QName(NS_URI, XML_NAME); + public static final QName N_TITLE = new QName(NS_URI, XML_TITLE); + public static final QName N_UPDATED = new QName(NS_URI, XML_UPDATED); +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/EventUtil.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/EventUtil.java new file mode 100644 index 00000000000..07eaba38bec --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/EventUtil.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.commons.webdav; + +import javax.jcr.observation.Event; +import java.util.HashMap; +import java.util.Map; + +/** + * EventConstants... + */ +public abstract class EventUtil { + + + /** + * Element representing the 'nodeadded' event type. + * @see javax.jcr.observation.Event#NODE_ADDED + */ + public static final String EVENT_NODEADDED = "nodeadded"; + + /** + * Element representing the 'noderemoved' event type. + * @see javax.jcr.observation.Event#NODE_REMOVED + */ + public static final String EVENT_NODEREMOVED = "noderemoved"; + + /** + * Element representing the 'propertyadded' event type. + * @see javax.jcr.observation.Event#PROPERTY_ADDED + */ + public static final String EVENT_PROPERTYADDED = "propertyadded"; + + /** + * Element representing the 'propertyremoved' event type. + * @see javax.jcr.observation.Event#PROPERTY_REMOVED + */ + public static final String EVENT_PROPERTYREMOVED = "propertyremoved"; + + /** + * Element representing the 'propertychanged' event type. + * @see javax.jcr.observation.Event#PROPERTY_CHANGED + */ + public static final String EVENT_PROPERTYCHANGED = "propertychanged"; + + /** + * Element representing the 'nodemoved' event type. + * @see javax.jcr.observation.Event#NODE_MOVED + */ + public static final String EVENT_NODEMOVED = "nodemoved"; + + /** + * Element representing the 'persist' event type. + * @see javax.jcr.observation.Event#PERSIST + */ + public static final String EVENT_PERSIST = "persist"; + + /** + * String array listing the xml local names of all type of jcr events. + */ + public static final String[] EVENT_ALL = new String[] { + EVENT_NODEADDED, + EVENT_NODEREMOVED, + EVENT_PROPERTYADDED, + EVENT_PROPERTYREMOVED, + EVENT_PROPERTYCHANGED, + EVENT_NODEMOVED, + EVENT_PERSIST}; + + private static Map NAME_TO_JCR = new HashMap(); + static { + NAME_TO_JCR.put(EVENT_NODEADDED, Event.NODE_ADDED); + NAME_TO_JCR.put(EVENT_NODEREMOVED, Event.NODE_REMOVED); + NAME_TO_JCR.put(EVENT_PROPERTYADDED, Event.PROPERTY_ADDED); + NAME_TO_JCR.put(EVENT_PROPERTYREMOVED, Event.PROPERTY_REMOVED); + NAME_TO_JCR.put(EVENT_PROPERTYCHANGED, Event.PROPERTY_CHANGED); + NAME_TO_JCR.put(EVENT_NODEMOVED, Event.NODE_MOVED); + NAME_TO_JCR.put(EVENT_PERSIST, Event.PERSIST); + + } + + /** + * Tests if the specified eventName can be mapped to a JCR event type. + * + * @param eventName + * @return true if the specified eventName can be mapped to a JCR event type. + */ + public static boolean isValidEventName(String eventName) { + return NAME_TO_JCR.containsKey(eventName); + } + + /** + * Static utility method to convert the localName of a EventType + * as present in the Xml body into the corresponding JCR event constant defined by + * {@link javax.jcr.observation.Event}. + * + * @param eventName + * @return Any of the event types defined by {@link Event} or null. + * @throws IllegalArgumentException if the specified evenName is invalid. + */ + public static int getJcrEventType(String eventName) { + if (NAME_TO_JCR.containsKey(eventName)) { + return NAME_TO_JCR.get(eventName); + } else { + throw new IllegalArgumentException("Invalid eventName : " + eventName); + } + } + + /** + * Static utility method to retrieve a String representation of the type + * defined by a {@link javax.jcr.observation.Event JCR event}. + * + * @param jcrEventType + * @return Event name of the given JCR event type. + * @throws IllegalArgumentException if the given int does not represent a + * valid type constants as defined by {@link Event}.
    + * Valid values are + *

      + *
    • {@link Event#NODE_ADDED}
    • + *
    • {@link Event#NODE_REMOVED}
    • + *
    • {@link Event#PROPERTY_ADDED}
    • + *
    • {@link Event#PROPERTY_REMOVED}
    • + *
    • {@link Event#PROPERTY_CHANGED}
    • + *
    • {@link Event#NODE_MOVED}
    • + *
    • {@link Event#PERSIST}
    • + *
    + */ + public static String getEventName(int jcrEventType) { + String eventName; + switch (jcrEventType) { + case Event.NODE_ADDED: + eventName = EVENT_NODEADDED; + break; + case Event.NODE_REMOVED: + eventName = EVENT_NODEREMOVED; + break; + case Event.PROPERTY_ADDED: + eventName = EVENT_PROPERTYADDED; + break; + case Event.PROPERTY_CHANGED: + eventName = EVENT_PROPERTYCHANGED; + break; + case Event.PROPERTY_REMOVED: + eventName = EVENT_PROPERTYREMOVED; + break; + case Event.NODE_MOVED: + eventName = EVENT_NODEMOVED; + break; + case Event.PERSIST: + eventName = EVENT_PERSIST; + break; + default: // no default + throw new IllegalArgumentException("Invalid JCR event type: " + jcrEventType); + } + return eventName; + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/JcrRemotingConstants.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/JcrRemotingConstants.java new file mode 100644 index 00000000000..cfaa36450eb --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/JcrRemotingConstants.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.commons.webdav; + +/** + * JcrRemotingConstants provides string constants for WebDAV + * resources representing repository items. + */ +public interface JcrRemotingConstants { + + /** + * Namespace prefix used for Jackrabbit specific WebDAV extensions related + * to JCR remoting. + * @see #NS_URI + */ + public static final String NS_PREFIX = "dcr"; + + /** + * Namespace uri used for Jackrabbit specific WebDAV extensions related + * to JCR remoting. + * @see #NS_PREFIX + */ + public static final String NS_URI = "http://www.day.com/jcr/webdav/1.0"; + + /** + * The resource path of the root-item-resource. + */ + public static final String ROOT_ITEM_PATH = "/"; + /** + * Placeholder resource path for the JCR root node. + */ + public static final String ROOT_ITEM_RESOURCEPATH = "/jcr:root"; + + /** + * The version storage item resource path. + */ + public static final String VERSIONSTORAGE_PATH = "/jcr:system/jcr:versionStorage"; + + public static final String IMPORT_UUID_BEHAVIOR = "ImportUUIDBehavior"; + + // xml element names + public static final String XML_PRIMARYNODETYPE = "primarynodetype"; + public static final String XML_VALUE = "value"; + /** + * 'type' attribute for the {@link #XML_VALUE value} element, reflecting the + * {@link javax.jcr.PropertyType type} of the value being transported. + */ + public static final String ATTR_VALUE_TYPE = "type"; + public static final String XML_LENGTH = "length"; + public static final String XML_EXCLUSIVE_SESSION_SCOPED = "exclusive-session-scoped"; + + // xml elements used to reflect the workspaces ns-registry + public static final String XML_NAMESPACE = "namespace"; + public static final String XML_PREFIX = "prefix"; + public static final String XML_URI = "uri"; + + // xml elements used for repository-descriptors report + public static final String XML_DESCRIPTOR = "descriptor"; + public static final String XML_DESCRIPTORKEY = "descriptorkey"; + public static final String XML_DESCRIPTORVALUE = "descriptorvalue"; + + // xml elements used for node type registration + public static final String XML_CND = "cnd"; + public static final String XML_ALLOWUPDATE = "allowupdate"; + public static final String XML_NODETYPENAME = "nodetypename"; + + /** + * The 'removeexisting' element is not defined by RFC 3253. If it is present + * in the UPDATE request body, uuid conflicts should be solved by removing + * the existing nodes. + * + * @see javax.jcr.Node#restore(javax.jcr.version.Version, boolean) + * @see javax.jcr.Workspace#restore(javax.jcr.version.Version[], boolean) + */ + public static final String XML_REMOVEEXISTING = "removeexisting"; + + /** + * The 'relpath' element is not defined by RFC 3253. If it is present + * in the UPDATE request body, the server is forced to used the text contained + * as 'relPath' argument for the {@link javax.jcr.Node#restore(javax.jcr.version.Version, String, boolean) + * Node.restore} call. + * + * @see javax.jcr.Node#restore(javax.jcr.version.Version, String, boolean) + */ + public static final String XML_RELPATH = "relpath"; + + // oroperty local name of the workspace for which the repository session has been created. + public static final String JCR_WORKSPACE_NAME_LN = "workspaceName"; + + // general property local names + public static final String JCR_NAME_LN = "name"; + public static final String JCR_PATH_LN = "path"; + public static final String JCR_DEPTH_LN = "depth"; + public static final String JCR_PARENT_LN = "parent"; + public static final String JCR_ISNEW_LN = "isnew"; + public static final String JCR_ISMODIFIED_LN = "ismodified"; + public static final String JCR_DEFINITION_LN = "definition"; + public static final String JCR_SELECTOR_NAME_LN = "selectorName"; + + // property local names used for resources representing jcr-nodes + public static final String JCR_PRIMARYNODETYPE_LN = XML_PRIMARYNODETYPE; + public static final String JCR_MIXINNODETYPES_LN = "mixinnodetypes"; + public static final String JCR_INDEX_LN = "index"; + public static final String JCR_REFERENCES_LN = "references"; + /** + * @since JCR 2.0 + */ + public static final String JCR_WEAK_REFERENCES_LN = "weakreferences"; + public static final String JCR_UUID_LN = "uuid"; + public static final String JCR_PRIMARYITEM_LN = "primaryitem"; + + // property local names used for resources representing jcr-properties + public static final String JCR_TYPE_LN = "type"; + public static final String JCR_VALUE_LN = "value"; + public static final String JCR_VALUES_LN = "values"; + public static final String JCR_LENGTH_LN = "length"; + public static final String JCR_LENGTHS_LN = "lengths"; + public static final String JCR_GET_STRING_LN = "getstring"; + + public static final String JCR_NAMESPACES_LN = "namespaces"; + public static final String JCR_NODETYPES_CND_LN = "nodetypes-cnd"; + + // property local names used for resource representing a version history + public static final String JCR_VERSIONABLEUUID_LN = "versionableuuid"; + + // property local names related to query + public static final String JCR_QUERY_RESULT_LN = "search-result-property"; + + // name of the xml element containing the result columns. + public static final String XML_QUERY_RESULT_COLUMN = "column"; + + public static final String REPORT_EXPORT_VIEW = "exportview"; + public static final String REPORT_PRIVILEGES = "privileges"; + public static final String REPORT_LOCATE_BY_UUID = "locate-by-uuid"; + public static final String REPORT_LOCATE_CORRESPONDING_NODE = "locate-corresponding-node"; + public static final String REPORT_NODETYPES = "nodetypes"; + public static final String REPORT_REGISTERED_NAMESPACES = "registerednamespaces"; + public static final String REPORT_REPOSITORY_DESCRIPTORS = "repositorydescriptors"; + + /** + * RFC 5988 relation type for user data + *

    + * Used to transport JCR User Data inside an HTTP request. + *

    + * Example: + * + *

    +     * Link: <data:,my%20user%data>, rel="RELATION_USER_DATA"
    +     * 
    + */ + public static final String RELATION_USER_DATA = NS_URI + "/user-data"; + + /** + * RFC 5988 relation type for remote session identification + *

    + * Used to transport an identifier for the remote session. + *

    + * Example: + * + *

    +     * Link: <urn:uuid:96d3c6fe-1073-11e1-a3c0-00059a3c7a00>, rel="RELATION_REMOTE_SESSION_ID"
    +     * 
    + */ + public static final String RELATION_REMOTE_SESSION_ID = NS_URI + "/session-id"; +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/JcrValueType.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/JcrValueType.java new file mode 100644 index 00000000000..3594cfaf9c5 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/JcrValueType.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.commons.webdav; + +import javax.jcr.PropertyType; +import java.util.HashMap; +import java.util.Map; + +/** JcrValueType... */ +public class JcrValueType { + + /** + * Fragment for build the content type of request entities representing + * a JCR-value. The fragment must be completed as follows: + *
    +     * jcr-value/ + Value.getType().toLowerCase()
    +     * 
    + * + * resulting in the following types: + *
    +     * jcr-value/binary
    +     * jcr-value/boolean
    +     * jcr-value/date
    +     * jcr-value/decimal
    +     * jcr-value/double
    +     * jcr-value/long
    +     * jcr-value/name
    +     * jcr-value/path
    +     * jcr-value/reference
    +     * jcr-value/string
    +     * jcr-value/undefined
    +     * jcr-value/uri
    +     * jcr-value/weakreference
    +     * 
    + */ + private static final String VALUE_CONTENT_TYPE_FRAGMENT = "jcr-value/"; + + /** + * Hardcoded lookup from content type as created by {@link #contentTypeFromType(int)}. + * Reason: As of JCR 2.0 there is no trivial rule to obtain the the TYPENAME + * constant from a lower-cased string: WeakReference uses camel-case and URI + * is all upper case... + */ + private static final Map TYPE_LOOKUP = new HashMap(); + static { + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.BINARY), PropertyType.BINARY); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.BOOLEAN), PropertyType.BOOLEAN); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.DATE), PropertyType.DATE); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.DECIMAL), PropertyType.DECIMAL); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.DOUBLE), PropertyType.DOUBLE); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.LONG), PropertyType.LONG); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.NAME), PropertyType.NAME); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.PATH), PropertyType.PATH); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.REFERENCE), PropertyType.REFERENCE); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.STRING), PropertyType.STRING); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.UNDEFINED), PropertyType.UNDEFINED); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.URI), PropertyType.URI); + TYPE_LOOKUP.put(contentTypeFromType(PropertyType.WEAKREFERENCE), PropertyType.WEAKREFERENCE); + } + + public static String contentTypeFromType(int propertyType) { + return VALUE_CONTENT_TYPE_FRAGMENT + PropertyType.nameFromValue(propertyType).toLowerCase(); + } + + public static int typeFromContentType(String contentType) { + if (contentType != null) { + // remove charset if present + int pos = contentType.indexOf(';'); + String ct = (pos == -1) ? contentType : contentType.substring(0, pos); + if (TYPE_LOOKUP.containsKey(ct)) { + return TYPE_LOOKUP.get(ct); + } + } + + // some invalid content type argument that does not match any of the + // strings created by contentTypeFromType(int propertyType) + // -> Fallback to UNDEFINED + return PropertyType.UNDEFINED; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/NodeTypeConstants.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/NodeTypeConstants.java new file mode 100644 index 00000000000..c34851649b9 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/NodeTypeConstants.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.commons.webdav; + +/** + * NodeTypeConstants used to represent nodetype definitions in + * Xml. + * + * @see javax.jcr.nodetype.NodeType + */ +public interface NodeTypeConstants { + + public static final String XML_NODETYPENAME = "nodetypename"; + + public static final String XML_REPORT_ALLNODETYPES = "all-nodetypes"; + public static final String XML_REPORT_MIXINNODETYPES = "mixin-nodetypes"; + public static final String XML_REPORT_PRIMARYNODETYPES = "primary-nodetypes"; + public static final String XML_NODETYPES = "nodetypes"; + public static final String XML_NODETYPE = "nodetype"; + + //------< copied from org.apache.jackrabbit.core.nodetype.xml.Constants >--- + + /** Name of the node type definition root element. */ + String NODETYPES_ELEMENT = "nodeTypes"; + + /** Name of the node type definition element. */ + String NODETYPE_ELEMENT = "nodeType"; + + /** Name of the child node definition element. */ + String CHILDNODEDEFINITION_ELEMENT = "childNodeDefinition"; + + /** Name of the property definition element. */ + String PROPERTYDEFINITION_ELEMENT = "propertyDefinition"; + + /** Name of the isMixin attribute. */ + String ISMIXIN_ATTRIBUTE = "isMixin"; + + /** Name of the hasOrderableChildNodes attribute. */ + String HASORDERABLECHILDNODES_ATTRIBUTE = "hasOrderableChildNodes"; + + /** + * Name of the isAbstract attribute. + * @since JCR 2.0 + */ + String ISABSTRACT_ATTRIBUTE = "isAbstract"; + + /** + * Name of the isQueryable attribute. + * @since JCR 2.0 + */ + String ISQUERYABLE_ATTRIBUTE = "isQueryable"; + + /** Name of the primary item name attribute. */ + String PRIMARYITEMNAME_ATTRIBUTE = "primaryItemName"; + + /** Name of the supertypes element. */ + String SUPERTYPES_ELEMENT = "supertypes"; + + /** Name of the supertype element. */ + String SUPERTYPE_ELEMENT = "supertype"; + + /** Name of the name attribute. */ + String NAME_ATTRIBUTE = "name"; + + /** Name of the autoCreated attribute. */ + String AUTOCREATED_ATTRIBUTE = "autoCreated"; + + /** Name of the mandatory attribute. */ + String MANDATORY_ATTRIBUTE = "mandatory"; + + /** Name of the onParentVersion attribute. */ + String ONPARENTVERSION_ATTRIBUTE = "onParentVersion"; + + /** Name of the protected attribute. */ + String PROTECTED_ATTRIBUTE = "protected"; + + /** Name of the required type attribute. */ + String REQUIREDTYPE_ATTRIBUTE = "requiredType"; + + /** Name of the value constraints element. */ + String VALUECONSTRAINTS_ELEMENT = "valueConstraints"; + + /** Name of the value constraint element. */ + String VALUECONSTRAINT_ELEMENT = "valueConstraint"; + + /** Name of the default values element. */ + String DEFAULTVALUES_ELEMENT = "defaultValues"; + + /** Name of the default value element. */ + String DEFAULTVALUE_ELEMENT = "defaultValue"; + + /** Name of the multiple attribute. */ + String MULTIPLE_ATTRIBUTE = "multiple"; + + /** Name of the required primary types element. */ + String REQUIREDPRIMARYTYPES_ELEMENT = "requiredPrimaryTypes"; + + /** Name of the required primary type element. */ + String REQUIREDPRIMARYTYPE_ELEMENT = "requiredPrimaryType"; + + /** Name of the default primary type attribute. */ + String DEFAULTPRIMARYTYPE_ATTRIBUTE = "defaultPrimaryType"; + + /** Name of the sameNameSiblings attribute. */ + String SAMENAMESIBLINGS_ATTRIBUTE = "sameNameSiblings"; + + /** + * Name of the availableQueryOperators element. + * @since JCR 2.0 + */ + String AVAILABLE_QUERY_OPERATORS_ELEMENT = "availableQueryOperators"; + + /** + * Name of the availableQueryOperator element. + * @since JCR 2.0 + */ + String AVAILABLE_QUERY_OPERATOR_ELEMENT = "availableQueryOperator"; + + /** + * Name of the fullTextSearchable attribute. + * @since JCR 2.0 + */ + String FULL_TEXT_SEARCHABLE_ATTRIBUTE = "fullTextSearchable"; + + /** + * Name of the queryOrderable attribute. + * @since JCR 2.0 + */ + String QUERY_ORDERABLE_ATTRIBUTE = "queryOrderable"; + + //----------< attr. not defined by copied from o.a.j.core.n.x.Constants >--- + /** + * Name of the declaring nodetype. This value is not needed during + * discovery of nodetype definitions. However if the definition of an item is + * retrieved (instead of being calculated on the client), this information is + * needed + */ + String DECLARINGNODETYPE_ATTRIBUTE = "declaringNodeType"; +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/NodeTypeUtil.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/NodeTypeUtil.java new file mode 100644 index 00000000000..43c15a4ff9f --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/NodeTypeUtil.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.commons.webdav; + +import org.apache.jackrabbit.util.XMLUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Text; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * NodeTypeUtil... + */ +public class NodeTypeUtil implements NodeTypeConstants { + + public static Element ntNameToXml(String nodeTypeName, Document document) { + Element ntElem = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + XML_NODETYPE); + Element nameElem = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + XML_NODETYPENAME); + Text txt = document.createTextNode(nodeTypeName); + nameElem.appendChild(txt); + ntElem.appendChild(nameElem); + return ntElem; + } + + public static Collection ntNamesFromXml(Object propValue) { + // assume property has be built from xml + if (propValue instanceof List) { + return retrieveNodeTypeNames(((List)propValue)); + } else if (propValue instanceof Element) { + List l = Collections.singletonList((Element) propValue); + return retrieveNodeTypeNames(l); + } else { + // Property value cannot be parsed into node type names. + return Collections.emptySet(); + } + } + + private static Set retrieveNodeTypeNames(List elementList) { + Set nodetypeNames = new HashSet(); + for (Object content : elementList) { + if (!(content instanceof Element)) { + continue; + } + Element el = (Element) content; + if (XML_NODETYPE.equals(el.getLocalName()) && JcrRemotingConstants.NS_URI.equals(el.getNamespaceURI())) { + String nodetypeName = XMLUtil.getChildText(el, XML_NODETYPENAME, JcrRemotingConstants.NS_URI); + if (nodetypeName != null && !"".equals(nodetypeName)) { + nodetypeNames.add(nodetypeName); + } + } // else: 'dcr:nodetype' element expected -> ignoring element + } + return nodetypeNames; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/QueryUtil.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/QueryUtil.java new file mode 100644 index 00000000000..19c378d4c87 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/QueryUtil.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.commons.webdav; + +import org.apache.jackrabbit.util.XMLUtil; +import org.apache.jackrabbit.value.ValueHelper; +import org.w3c.dom.Element; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; +import java.util.List; + +/** + * QueryUtil... + */ +public class QueryUtil implements JcrRemotingConstants { + + public static void parseResultPropertyValue(Object propValue, + List columnNames, + List selectorNames, + List values, + ValueFactory valueFactory) + throws ValueFormatException, RepositoryException { + if (propValue instanceof List) { + for (Object o : ((List) propValue)) { + if (o instanceof Element) { + parseColumnElement((Element) o, columnNames, selectorNames, values, valueFactory); + } + } + } else if (propValue instanceof Element) { + parseColumnElement((Element) propValue, columnNames, selectorNames, values, valueFactory); + } else { + throw new IllegalArgumentException("SearchResultProperty requires a list of 'dcr:column' xml elements."); + } + } + + private static void parseColumnElement(Element columnElement, + List columnNames, + List selectorNames, + List values, + ValueFactory valueFactory) + throws ValueFormatException, RepositoryException { + if (!XML_QUERY_RESULT_COLUMN.equals(columnElement.getLocalName()) && NS_URI.equals(columnElement.getNamespaceURI())) { + // dcr:column element expected within search result -> can't parse + return; + } + + columnNames.add(XMLUtil.getChildText(columnElement, JCR_NAME_LN, NS_URI)); + selectorNames.add(XMLUtil.getChildText(columnElement, JCR_SELECTOR_NAME_LN, NS_URI)); + + Value jcrValue = null; + Element valueElement = XMLUtil.getChildElement(columnElement, JCR_VALUE_LN, NS_URI); + if (valueElement != null) { + String text = XMLUtil.getText(valueElement); + if (text != null) { + String typeStr = XMLUtil.getAttribute(valueElement, ATTR_VALUE_TYPE, NS_URI); + jcrValue = ValueHelper.deserialize( + text, PropertyType.valueFromName(typeStr), true, valueFactory); + } + } + values.add(jcrValue); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/ValueUtil.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/ValueUtil.java new file mode 100644 index 00000000000..7e93873d704 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/ValueUtil.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.commons.webdav; + +import org.apache.jackrabbit.util.XMLUtil; +import org.apache.jackrabbit.value.ValueHelper; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Text; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; +import java.util.ArrayList; +import java.util.List; + +/** + * ValuesUtil... + */ +public class ValueUtil { + + public static Element valueToXml(Value jcrValue, Document document) throws RepositoryException { + + String type = PropertyType.nameFromValue(jcrValue.getType()); + String serializedValue = ValueHelper.serialize(jcrValue, true); + + Element xmlValue = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + JcrRemotingConstants.XML_VALUE); + + Text txt = document.createTextNode(serializedValue); + xmlValue.appendChild(txt); + + Attr attr = document.createAttributeNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + JcrRemotingConstants.ATTR_VALUE_TYPE); + attr.setValue(type); + xmlValue.setAttributeNodeNS(attr); + + return xmlValue; + } + + public static Value[] valuesFromXml(Object propValue, int defaultType, ValueFactory valueFactory) throws RepositoryException { + Value[] jcrValues; + // retrieve jcr-values from child 'value'-element(s) + List valueElements = new ArrayList(); + if (propValue == null) { + jcrValues = new Value[0]; + } else { /* not null propValue */ + if (isValueElement(propValue)) { + valueElements.add((Element) propValue); + } else if (propValue instanceof List) { + for (Object el : ((List) propValue)) { + /* make sure, only Elements with name 'value' are used for + * the 'value' field. any other content (other elements, text, + * comment etc.) is ignored. NO bad-request/conflict error is + * thrown. + */ + if (isValueElement(el)) { + valueElements.add((Element) el); + } + } + } + /* fill the 'value' with the valid 'value' elements found before */ + jcrValues = new Value[valueElements.size()]; + int i = 0; + for (Element element : valueElements) { + jcrValues[i] = getJcrValue(element, defaultType, valueFactory); + i++; + } + } + return jcrValues; + } + + private static boolean isValueElement(Object obj) { + return obj instanceof Element && JcrRemotingConstants.XML_VALUE.equals(((Element)obj).getLocalName()); + } + + /** + * + * @param valueElement + * @param defaultType + * @return + * @throws javax.jcr.ValueFormatException + * @throws javax.jcr.RepositoryException + */ + private static Value getJcrValue(Element valueElement, int defaultType, + ValueFactory valueFactory) + throws ValueFormatException, RepositoryException { + if (valueElement == null) { + return null; + } + // make sure the value is never 'null' + String value = XMLUtil.getText(valueElement, ""); + String typeStr = XMLUtil.getAttribute(valueElement, JcrRemotingConstants.ATTR_VALUE_TYPE, JcrRemotingConstants.NS_URI); + int type = (typeStr == null) ? defaultType : PropertyType.valueFromName(typeStr); + // deserialize value ->> see #valueToXml where values are serialized + return ValueHelper.deserialize(value, type, true, valueFactory); + } + + public static long[] lengthsFromXml(Object propValue) throws RepositoryException { + long[] lengths; + // retrieve jcr-values from child 'value'-element(s) + List lengthElements = new ArrayList(); + if (propValue == null) { + lengths = new long[0]; + } else { /* not null propValue */ + if (isLengthElement(propValue)) { + lengthElements.add((Element) propValue); + } else if (propValue instanceof List) { + for (Object el : ((List) propValue)) { + /* make sure, only Elements with name 'value' are used for + * the 'value' field. any other content (other elements, text, + * comment etc.) is ignored. NO bad-request/conflict error is + * thrown. + */ + if (isLengthElement(el)) { + lengthElements.add((Element) el); + } + } + } + /* fill the 'value' with the valid 'value' elements found before */ + lengths = new long[lengthElements.size()]; + int i = 0; + for (Element element : lengthElements) { + lengths[i] = Long.parseLong(XMLUtil.getText(element, "0")); + i++; + } + } + return lengths; + } + + private static boolean isLengthElement(Object obj) { + return obj instanceof Element && JcrRemotingConstants.XML_LENGTH.equals(((Element)obj).getLocalName()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/package-info.java new file mode 100644 index 00000000000..c22f88c84cb --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/webdav/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.5.0") +package org.apache.jackrabbit.commons.webdav; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/DefaultContentHandler.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/DefaultContentHandler.java new file mode 100644 index 00000000000..155eeee8e1e --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/DefaultContentHandler.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.xml; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Adapter class for exposing a {@link ContentHandler} instance as + * {@link DefaultHandler} object. + * + * @since Jackrabbit JCR Commons 1.5 + */ +public class DefaultContentHandler extends DefaultHandler { + + /** + * The adapted content handler instance. + */ + private final ContentHandler handler; + + /** + * Creates a {@link DefaultHandler} adapter for the given content + * handler. + * + * @param handler content handler + */ + public DefaultContentHandler(ContentHandler handler) { + this.handler = handler; + } + + //------------------------------------------------------< ContentHandler > + + /** + * Delegated to {@link #handler}. + * + * @param ch passed through + * @param start passed through + * @param length passed through + * @throws SAXException if an error occurs + */ + public void characters(char[] ch, int start, int length) + throws SAXException { + handler.characters(ch, start, length); + } + + /** + * Delegated to {@link #handler}. + * + * @throws SAXException if an error occurs + */ + public void endDocument() throws SAXException { + handler.endDocument(); + } + + /** + * Delegated to {@link #handler}. + * + * @param namespaceURI passed through + * @param localName passed through + * @param qName passed through + * @throws SAXException if an error occurs + */ + public void endElement( + String namespaceURI, String localName, String qName) + throws SAXException { + handler.endElement(namespaceURI, localName, qName); + } + + /** + * Delegated to {@link #handler}. + * + * @param prefix passed through + * @throws SAXException if an error occurs + */ + public void endPrefixMapping(String prefix) throws SAXException { + handler.endPrefixMapping(prefix); + } + + /** + * Delegated to {@link #handler}. + * + * @param ch passed through + * @param start passed through + * @param length passed through + * @throws SAXException if an error occurs + */ + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + handler.ignorableWhitespace(ch, start, length); + } + + /** + * Delegated to {@link #handler}. + * + * @param target passed through + * @param data passed through + * @throws SAXException if an error occurs + */ + public void processingInstruction(String target, String data) + throws SAXException { + handler.processingInstruction(target, data); + } + + /** + * Delegated to {@link #handler}. + * + * @param locator passed through + */ + public void setDocumentLocator(Locator locator) { + handler.setDocumentLocator(locator); + } + + /** + * Delegated to {@link #handler}. + * + * @param name passed through + * @throws SAXException if an error occurs + */ + public void skippedEntity(String name) throws SAXException { + handler.skippedEntity(name); + } + + /** + * Delegated to {@link #handler}. + * + * @throws SAXException if an error occurs + */ + public void startDocument() throws SAXException { + handler.startDocument(); + } + + /** + * Delegated to {@link #handler}. + * + * @param namespaceURI passed through + * @param localName passed through + * @param qName passed through + * @param atts passed through + * @throws SAXException if an error occurs + */ + public void startElement( + String namespaceURI, String localName, String qName, + Attributes atts) throws SAXException { + handler.startElement(namespaceURI, localName, qName, atts); + } + + /** + * Delegated to {@link #handler}. + * + * @param prefix passed through + * @param uri passed through + * @throws SAXException if an error occurs + */ + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + handler.startPrefixMapping(prefix, uri); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/DocumentViewExporter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/DocumentViewExporter.java new file mode 100644 index 00000000000..f7dc6007430 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/DocumentViewExporter.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.commons.xml; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.commons.NamespaceHelper; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.value.ValueHelper; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * Document view exporter. + * + * @since Jackrabbit JCR Commons 1.5 + */ +public class DocumentViewExporter extends Exporter { + + /** + * Creates a document view exporter. + * + * @param session current session + * @param handler SAX event handler for the export + * @param recurse whether to recursively export the whole subtree + * @param binary whether to export binary values + */ + public DocumentViewExporter( + Session session, ContentHandler handler, + boolean recurse, boolean binary) { + super(session, handler, recurse, binary); + } + + /** + * Exports the given node either as XML characters (if it's an + * xml:text node) or as an XML element with properties + * mapped to XML attributes. + */ + protected void exportNode(String uri, String local, Node node) + throws RepositoryException, SAXException { + if (NamespaceHelper.JCR.equals(uri) && "xmltext".equals(local)) { + try { + // assume jcr:xmlcharacters is single-valued + Property property = + node.getProperty(helper.getJcrName("jcr:xmlcharacters")); + char[] ch = property.getString().toCharArray(); + characters(ch, 0, ch.length); + } catch (PathNotFoundException e) { + // jcr:xmlcharacters not found, ignore this node + } + } else { + // attributes (properties) + exportProperties(node); + + // encode node name to make sure it's a valid xml name + String encoded = ISO9075.encode(local); + startElement(uri, encoded); + exportNodes(node); + endElement(uri, encoded); + } + } + + /** + * Maps the given single-valued property to an XML attribute. + */ + protected void exportProperty(String uri, String local, Value value) + throws RepositoryException { + // TODO: Serialized names and paths should use XML namespace mappings + String attribute = ValueHelper.serialize(value, false); + addAttribute(uri, ISO9075.encode(local), attribute); + } + + /** + * Does nothing. Multi-valued properties are skipped for the time being + * until a way of properly handling/detecting multi-valued properties + * on re-import is found. Skipping multi-valued properties entirely is + * legal according to "6.4.2.5 Multi-value Properties" of the JSR 170 + * specification. + * + * @see JCR-325 + */ + protected void exportProperty( + String uri, String local, int type, Value[] values) { + // TODO: proper multi-value serialization support + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/Exporter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/Exporter.java new file mode 100644 index 00000000000..63ca19e81f5 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/Exporter.java @@ -0,0 +1,560 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.xml; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.commons.NamespaceHelper; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Abstract base class for document and system view exporters. This class + * takes care of all the details related to namespace mappings, shareable + * nodes, recursive exports, binary values, and so on while leaving the + * decisions about what kind of SAX events to generate to subclasses. + *

    + * A subclass should only need to implement the abstract methods of this + * class to produce a fully functional exporter. + * + * @since Jackrabbit JCR Commons 1.5 + */ +public abstract class Exporter { + + /** + * Attributes of the next element. This single instance is reused for + * all elements by simply clearing it after each element has been emitted. + */ + private final AttributesImpl attributes = new AttributesImpl(); + + /** + * Stack of namespace mappings. + */ + private final LinkedList stack = new LinkedList(); + + /** + * The UUID strings of all shareable nodes already exported. + */ + private final Set shareables = new HashSet(); + + /** + * Whether the current node is a shareable node that has already been + * exported. + */ + private boolean share = false; + + /** + * Current session. + */ + private final Session session; + + /** + * Namespace helper. + */ + protected final NamespaceHelper helper; + + /** + * SAX event handler to which the export events are sent. + */ + private final ContentHandler handler; + + /** + * Whether to export the subtree or just the given node. + */ + private final boolean recurse; + + /** + * Whether to export binary values. + */ + private final boolean binary; + + /** + * Creates an exporter instance. + * + * @param session current session + * @param handler SAX event handler + * @param recurse whether the export should be recursive + * @param binary whether the export should include binary values + */ + protected Exporter( + Session session, ContentHandler handler, + boolean recurse, boolean binary) { + this.session = session; + this.helper = new NamespaceHelper(session); + this.handler = handler; + this.recurse = recurse; + this.binary = binary; + stack.add(new HashMap()); + } + + /** + * Exports the given node by preparing the export and calling the + * abstract {@link #exportNode(String, String, Node)} method to give + * control of the export format to a subclass. + *

    + * This method should be called only once for an exporter instance. + * + * @param node node to be exported + * @throws SAXException if a SAX error occurs + * @throws RepositoryException if a repository error occurs + */ + public void export(Node node) throws RepositoryException, SAXException { + handler.startDocument(); + + String[] prefixes = session.getNamespacePrefixes(); + for (int i = 0; i < prefixes.length; i++) { + if (prefixes[i].length() > 0 && !prefixes[i].equals("xml") ) { + addNamespace(prefixes[i], session.getNamespaceURI(prefixes[i])); + } + } + + exportNode(node); + + handler.endDocument(); + } + + /** + * Called to export the given node. The node name (or jcr:root + * if the node is the root node) is given as an explicit pair of the + * resolved namespace URI and local part of the name. + *

    + * The implementation of this method should call the methods + * {@link #exportProperties(Node)} and {@link #exportProperties(Node)} + * to respectively export the properties and child nodes of the given node. + * Those methods will call back to the implementations of this method and + * the abstract property export methods so the subclass can decide what + * SAX events to emit for each exported item. + * + * @param uri node namespace + * @param local node name + * @param node node + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + protected abstract void exportNode(String uri, String local, Node node) + throws RepositoryException, SAXException; + + /** + * Called by {@link #exportProperties(Node)} to process a single-valued + * property. + * + * @param uri property namespace + * @param local property name + * @param value property value + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + protected abstract void exportProperty( + String uri, String local, Value value) + throws RepositoryException, SAXException; + + /** + * Called by {@link #exportProperties(Node)} to process a multivalued + * property. + * + * @param uri property namespace + * @param local property name + * @param type property type + * @param values property values + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + protected abstract void exportProperty( + String uri, String local, int type, Value[] values) + throws RepositoryException, SAXException; + + /** + * Called by {@link #exportNode(String, String, Node)} to recursively + * call {@link #exportNode(String, String, Node)} for each child node. + * Does nothing if this exporter is not recursive. + * + * @param node parent node + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + protected void exportNodes(Node node) + throws RepositoryException, SAXException { + if (recurse && !share) { + NodeIterator iterator = node.getNodes(); + while (iterator.hasNext()) { + Node child = iterator.nextNode(); + exportNode(child); + } + } + } + + /** + * Processes all properties of the given node by calling the abstract + * {@link #exportProperty(String, String, Value)} and + * {@link #exportProperty(String, String, int, Value[])} methods for + * each property depending on whether the property is single- or + * multivalued. + *

    + * The first properties to be processed are jcr:primaryType, + * jcr:mixinTypes, and jcr:uuid, and then the + * remaining properties ordered by their names. + *

    + * If the node is a shareable node that has already been encountered by + * this event generator, then only a jcr:primaryType property + * with the fixed value "nt:share" and the jcr:uuid property + * of the shareable node are exported. + * + * @see JCR-1084 + * @param node node + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + protected void exportProperties(Node node) + throws RepositoryException, SAXException { + // if node is shareable and has already been serialized, change its + // type to nt:share and process only the properties jcr:primaryType + // and jcr:uuid (mix:shareable is referenceable, so jcr:uuid exists) + if (share) { + ValueFactory factory = session.getValueFactory(); + exportProperty( + NamespaceHelper.JCR, "primaryType", + factory.createValue( + helper.getJcrName("nt:share"), PropertyType.NAME)); + exportProperty( + NamespaceHelper.JCR, "uuid", + factory.createValue(node.getUUID())); + } else { + // Standard behaviour: return all properties (sorted, see JCR-1084) + SortedMap properties = getProperties(node); + + // serialize jcr:primaryType, jcr:mixinTypes & jcr:uuid first: + exportProperty(properties, helper.getJcrName("jcr:primaryType")); + exportProperty(properties, helper.getJcrName("jcr:mixinTypes")); + exportProperty(properties, helper.getJcrName("jcr:uuid")); + + // serialize remaining properties + Iterator iterator = properties.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String name = (String) entry.getKey(); + exportProperty(name, (Property) entry.getValue()); + } + } + } + + /** + * Utility method for exporting the given node. Parses the node name + * (or jcr:root if given the root node) and calls + * {@link #exportNode(String, String, Node)} with the resolved namespace + * URI and the local part of the name. + * + * @param node node + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + private void exportNode(Node node) + throws RepositoryException, SAXException { + share = node.isNodeType(helper.getJcrName("mix:shareable")) + && !shareables.add(node.getUUID()); + + if (node.getDepth() == 0) { + exportNode(NamespaceHelper.JCR, "root", node); + } else { + String name = node.getName(); + int colon = name.indexOf(':'); + if (colon == -1) { + exportNode("", name, node); + } else { + String uri = session.getNamespaceURI(name.substring(0, colon)); + exportNode(uri, name.substring(colon + 1), node); + } + } + } + + /** + * Returns a sorted map of the properties of the given node. + * + * @param node JCR node + * @return sorted map (keyed by name) of properties + * @throws RepositoryException if a repository error occurs + */ + private SortedMap getProperties(Node node) throws RepositoryException { + SortedMap properties = new TreeMap(); + PropertyIterator iterator = node.getProperties(); + while (iterator.hasNext()) { + Property property = iterator.nextProperty(); + properties.put(property.getName(), property); + } + return properties; + } + + /** + * Utility method for processing the named property from the given + * map of properties. If the property exists, it is removed from the + * given map and passed to {@link #exportProperty(Property)}. + * The property is ignored if it does not exist. + * + * @param properties map of properties + * @param name property name + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + private void exportProperty(Map properties, String name) + throws RepositoryException, SAXException { + Property property = (Property) properties.remove(name); + if (property != null) { + exportProperty(name, property); + } + } + + /** + * Utility method for processing the given property. Calls either + * {@link #exportProperty(Value)} or {@link #exportProperty(int, Value[])} + * depending on whether the the property is single- or multivalued. + * + * @param name property name + * @param property property + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + private void exportProperty(String name, Property property) + throws RepositoryException, SAXException { + String uri = ""; + String local = name; + int colon = name.indexOf(':'); + if (colon != -1) { + uri = session.getNamespaceURI(name.substring(0, colon)); + local = name.substring(colon + 1); + } + + int type = property.getType(); + if (type != PropertyType.BINARY || binary) { + if (property.isMultiple()) { + exportProperty(uri, local, type, property.getValues()); + } else { + exportProperty(uri, local, property.getValue()); + } + } else { + ValueFactory factory = session.getValueFactory(); + Value value = factory.createValue("", PropertyType.BINARY); + if (property.isMultiple()) { + exportProperty(uri, local, type, new Value[] { value }); + } else { + exportProperty(uri, local, value); + } + } + } + + //---------------------------------------------< XML handling methods >-- + + /** + * Emits a characters event with the given character content. + * + * @param ch character array + * @param start start offset within the array + * @param length number of characters to emit + * @throws SAXException if a SAX error occurs + */ + protected void characters(char[] ch, int start, int length) + throws SAXException { + handler.characters(ch, start, length); + } + + /** + * Adds the given attribute to be included in the next element. + * + * @param uri namespace URI of the attribute + * @param local local name of the attribute + * @param value attribute value + * @throws RepositoryException if a repository error occurs + */ + protected void addAttribute(String uri, String local, String value) + throws RepositoryException { + attributes.addAttribute( + uri, local, getXMLName(uri, local), "CDATA", value); + } + + /** + * Emits the start element event for an element with the given name. + * All the attributes added using + * {@link #addAttribute(String, String, String)} are included in the + * element along with any new namespace mappings. The namespace stack + * is extended for potential child elements. + * + * @param uri namespace URI or the element + * @param local local name of the element + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + protected void startElement(String uri, String local) + throws SAXException, RepositoryException { + // Prefixed name is generated before namespace handling so that a + // potential new prefix mapping gets included as a xmlns attribute + String name = getXMLName(uri, local); + + // Add namespace mappings + Map namespaces = (Map) stack.getFirst(); + Iterator iterator = namespaces.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String namespace = (String) entry.getKey(); + String prefix = (String) entry.getValue(); + handler.startPrefixMapping(prefix, namespace); + attributes.addAttribute( + "http://www.w3.org/2000/xmlns/", prefix, "xmlns:" + prefix, + "CDATA", namespace); + } + + // Emit the start element event, and clear things for the next element + handler.startElement(uri, local, name, attributes); + attributes.clear(); + stack.addFirst(new HashMap()); + } + + /** + * Emits the end element event for an element with the given name. + * The namespace stack and mappings are automatically updated. + * + * @param uri namespace URI or the element + * @param local local name of the element + * @throws RepositoryException if a repository error occurs + * @throws SAXException if a SAX error occurs + */ + protected void endElement(String uri, String local) + throws SAXException, RepositoryException { + stack.removeFirst(); + handler.endElement(uri, local, getXMLName(uri, local)); + + Map namespaces = (Map) stack.getFirst(); + Iterator iterator = namespaces.values().iterator(); + while (iterator.hasNext()) { + handler.endPrefixMapping((String) iterator.next()); + } + namespaces.clear(); + } + + /** + * Returns a prefixed XML name for the given namespace URI and local + * name. If a prefix mapping for the namespace URI is not yet available, + * it is created based on the namespace mappings of the current JCR + * session. + * + * @param uri namespace URI + * @param local local name + * @return prefixed XML name + * @throws RepositoryException if a JCR namespace mapping is not available + */ + protected String getXMLName(String uri, String local) + throws RepositoryException { + if (uri.length() == 0) { + return local; + } else { + String prefix = getPrefix(uri); + if (prefix == null) { + prefix = getUniquePrefix(session.getNamespacePrefix(uri)); + ((Map) stack.getFirst()).put(uri, prefix); + } + return prefix + ":" + local; + } + } + + /** + * Adds the given namespace to the export. A unique prefix based on + * the given prefix hint is mapped to the given namespace URI. If the + * namespace is already mapped, then the existing prefix is returned. + * + * @param hint prefix hint + * @param uri namespace URI + * @return registered prefix + */ + protected String addNamespace(String hint, String uri) { + String prefix = getPrefix(uri); + if (prefix == null) { + prefix = getUniquePrefix(hint); + ((Map) stack.getFirst()).put(uri, prefix); + } + return prefix; + } + + /** + * Returns the namespace prefix mapped to the given URI. Returns + * null if the namespace URI is not registered. + * + * @param uri namespace URI + * @return prefix mapped to the URI, or null + */ + private String getPrefix(String uri) { + Iterator iterator = stack.iterator(); + while (iterator.hasNext()) { + String prefix = (String) ((Map) iterator.next()).get(uri); + if (prefix != null) { + return prefix; + } + } + return null; + } + + /** + * Returns a unique namespace prefix based on the given hint. + * We need prefixes to be unique within the current namespace + * stack as otherwise for example a previously added attribute + * to the current element might incorrectly be using a prefix + * that is being redefined in this element. + * + * @param hint prefix hint + * @return unique prefix + */ + private String getUniquePrefix(String hint) { + String prefix = hint; + for (int i = 2; prefixExists(prefix); i++) { + prefix = hint + i; + } + return prefix; + } + + /** + * Checks whether the given prefix is already mapped within the + * current namespace stack. + * + * @param prefix namespace prefix + * @return true if the prefix is mapped, + * false otherwise + */ + private boolean prefixExists(String prefix) { + Iterator iterator = stack.iterator(); + while (iterator.hasNext()) { + if (((Map) iterator.next()).containsValue(prefix)) { + return true; + } + } + return false; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ParsingContentHandler.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ParsingContentHandler.java new file mode 100644 index 00000000000..e7da202b758 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ParsingContentHandler.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.commons.xml; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Utility class that decorates a {@link ContentHandler} instance with + * simple XML parsing capability. + * + * @since Jackrabbit JCR Commons 1.5 + */ +public class ParsingContentHandler extends DefaultContentHandler { + + /** + * Creates a {@link DefaultHandler} adapter for the given content + * handler. + * + * @param handler content handler + */ + public ParsingContentHandler(ContentHandler handler) { + super(handler); + } + + /** + * Utility method that parses the given input stream using this handler. + * The parser is namespace-aware and will not resolve external entity + * references. + * + * @param in XML input stream + * @throws IOException if an I/O error occurs + * @throws SAXException if an XML parsing error occurs + */ + public void parse(InputStream in) throws IOException, SAXException { + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.newSAXParser().parse(new InputSource(in), this); + } catch (ParserConfigurationException e) { + throw new SAXException("SAX parser configuration error", e); + } + } + + /** + * Returns an empty stream to prevent the XML parser from attempting + * to resolve external entity references. + */ + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException { + return new InputSource(new ByteArrayInputStream(new byte[0])); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ProxyContentHandler.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ProxyContentHandler.java new file mode 100644 index 00000000000..6ef5d0eb79a --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ProxyContentHandler.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.commons.xml; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * A proxy content handler that passes all SAX events as-is to the + * proxied handler. + *

    + * As a convenience this class inherits the {@link DefaultHandler} class + * instead of just the {@link ContentHandler} interface. This makes it + * possible to use this class as an adapter when using methods like + * {@link javax.xml.parsers.SAXParser#parse(String, DefaultHandler)} that + * expect a DefaultHandler instance instead of a ContentHandler. + */ +public class ProxyContentHandler extends DefaultHandler { + + /** + * The proxied content handler. This is a protected, non-final field + * so that subclasses can access the proxied handler or even replace + * it they want. + */ + protected ContentHandler handler; + + /** + * Creates a proxy for the given content handler. + * + * @param handler content handler to be proxied + */ + public ProxyContentHandler(ContentHandler handler) { + this.handler = handler; + } + + //------------------------------------------------------< ContentHandler > + + /** + * Delegated to {@link #handler}. + * + * @param ch passed through + * @param start passed through + * @param length passed through + * @throws SAXException if an error occurs + */ + public void characters(char[] ch, int start, int length) + throws SAXException { + handler.characters(ch, start, length); + } + + /** + * Delegated to {@link #handler}. + * + * @throws SAXException if an error occurs + */ + public void endDocument() throws SAXException { + handler.endDocument(); + } + + /** + * Delegated to {@link #handler}. + * + * @param namespaceURI passed through + * @param localName passed through + * @param qName passed through + * @throws SAXException if an error occurs + */ + public void endElement( + String namespaceURI, String localName, String qName) + throws SAXException { + handler.endElement(namespaceURI, localName, qName); + } + + /** + * Delegated to {@link #handler}. + * + * @param prefix passed through + * @throws SAXException if an error occurs + */ + public void endPrefixMapping(String prefix) throws SAXException { + handler.endPrefixMapping(prefix); + } + + /** + * Delegated to {@link #handler}. + * + * @param ch passed through + * @param start passed through + * @param length passed through + * @throws SAXException if an error occurs + */ + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + handler.ignorableWhitespace(ch, start, length); + } + + /** + * Delegated to {@link #handler}. + * + * @param target passed through + * @param data passed through + * @throws SAXException if an error occurs + */ + public void processingInstruction(String target, String data) + throws SAXException { + handler.processingInstruction(target, data); + } + + /** + * Delegated to {@link #handler}. + * + * @param locator passed through + */ + public void setDocumentLocator(Locator locator) { + handler.setDocumentLocator(locator); + } + + /** + * Delegated to {@link #handler}. + * + * @param name passed through + * @throws SAXException if an error occurs + */ + public void skippedEntity(String name) throws SAXException { + handler.skippedEntity(name); + } + + /** + * Delegated to {@link #handler}. + * + * @throws SAXException if an error occurs + */ + public void startDocument() throws SAXException { + handler.startDocument(); + } + + /** + * Delegated to {@link #handler}. + * + * @param namespaceURI passed through + * @param localName passed through + * @param qName passed through + * @param atts passed through + * @throws SAXException if an error occurs + */ + public void startElement( + String namespaceURI, String localName, String qName, + Attributes atts) throws SAXException { + handler.startElement(namespaceURI, localName, qName, atts); + } + + /** + * Delegated to {@link #handler}. + * + * @param prefix passed through + * @param uri passed through + * @throws SAXException if an error occurs + */ + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + handler.startPrefixMapping(prefix, uri); + } + + //--------------------------------------------------------------< Object > + + public String toString() { + return handler.toString(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/SerializingContentHandler.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/SerializingContentHandler.java new file mode 100644 index 00000000000..1e7e1fe8bee --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/SerializingContentHandler.java @@ -0,0 +1,441 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.xml; + +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; +import org.xml.sax.helpers.DefaultHandler; + +/** + * A {@link ContentHandler} that serializes SAX events to a given + * {@link Result} instance. The JAXP {@link SAXTransformerFactory} + * facility is used for the serialization. + *

    + * This class explicitly ensures that all namespace prefixes are also + * present as xmlns attributes in the serialized XML document. This avoids + * problems with Xalan's serialization behaviour which was (at least during + * JDK 1.4) to ignore namespaces if they were not present as xmlns attributes. + *

    + * NOTE: The code in this class was originally written for Apache Cocoon and + * is included with some modifications here in Apache Jackrabbit. See the + * org.apache.cocoon.serialization.AbstractTextSerializer class in the + * cocoon-pipeline-impl component for the original code. + * + * @since Jackrabbit JCR Commons 1.5 + */ +public class SerializingContentHandler extends DefaultContentHandler { + + /** + * The character encoding used for serialization (UTF-8). + * The encoding is fixed to make the text/xml content type safer to use. + * + * @see JCR-1621 + */ + public static final String ENCODING = "UTF-8"; + + /** The URI for xml namespaces */ + private static final String XML = "http://www.w3.org/XML/1998/namespace"; + + /** + * The factory used to create serializing SAX transformers. + */ + private static final SAXTransformerFactory FACTORY = + // Note that the cast from below is strictly speaking only valid when + // the factory instance supports the SAXTransformerFactory.FEATURE + // feature. But since this class would be useless without this feature, + // it's no problem to fail with a ClassCastException here and prevent + // this class from even being loaded. AFAIK all common JAXP + // implementations do support this feature. + (SAXTransformerFactory) TransformerFactory.newInstance(); + + /** + * Flag that indicates whether we need to work around the issue of + * the serializer not automatically generating the required xmlns + * attributes for the namespaces used in the document. + */ + private static final boolean NEEDS_XMLNS_ATTRIBUTES = + needsXmlnsAttributes(); + + /** + * Probes the available XML serializer for xmlns support. Used to set + * the value of the {@link #NEEDS_XMLNS_ATTRIBUTES} flag. + * + * @return whether the XML serializer needs explicit xmlns attributes + */ + private static boolean needsXmlnsAttributes() { + try { + StringWriter writer = new StringWriter(); + TransformerHandler probe = FACTORY.newTransformerHandler(); + probe.setResult(new StreamResult(writer)); + probe.startDocument(); + probe.startPrefixMapping("p", "uri"); + probe.startElement("uri", "e", "p:e", new AttributesImpl()); + probe.endElement("uri", "e", "p:e"); + probe.endPrefixMapping("p"); + probe.endDocument(); + return writer.toString().indexOf("xmlns") == -1; + } catch (Exception e) { + throw new UnsupportedOperationException("XML serialization fails"); + } + } + + /** + * Creates a serializing content handler that writes to the given stream. + * + * @param output serialization target + * @return serializing content handler + * @throws SAXException if the content handler could not be initialized + */ + public static DefaultHandler getSerializer(OutputStream output) + throws SAXException { + return getSerializer(new StreamResult(output)); + } + + /** + * Creates a serializing content handler that writes to the given writer. + * + * @param writer serialization target + * @return serializing content handler + * @throws SAXException if the content handler could not be initialized + */ + public static DefaultHandler getSerializer(Writer writer) + throws SAXException { + return getSerializer(new StreamResult(writer)); + } + + /** + * Creates a serializing content handler that writes to the given result. + * + * @param result serialization target + * @return serializing content handler + * @throws SAXException if the content handler could not be initialized + */ + public static DefaultHandler getSerializer(Result result) + throws SAXException { + try { + TransformerHandler handler = FACTORY.newTransformerHandler(); + handler.setResult(result); + + // Specify the output properties to avoid surprises especially in + // character encoding or the output method (might be html for some + // documents!) + Transformer transformer = handler.getTransformer(); + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + transformer.setOutputProperty(OutputKeys.ENCODING, ENCODING); + transformer.setOutputProperty(OutputKeys.INDENT, "no"); + + if (NEEDS_XMLNS_ATTRIBUTES) { + // The serializer does not output xmlns declarations, + // so we need to do it explicitly with this wrapper + return new SerializingContentHandler(handler); + } else { + return new DefaultContentHandler(handler); + } + } catch (TransformerConfigurationException e) { + throw new SAXException("Failed to initialize XML serializer", e); + } + } + + /** + * The prefixes of startPrefixMapping() declarations for the coming element. + */ + private List prefixList = new ArrayList(); + + /** + * The URIs of startPrefixMapping() declarations for the coming element. + */ + private List uriList = new ArrayList(); + + /** + * Maps of URI<->prefix mappings. Used to work around a bug in the Xalan + * serializer. + */ + private Map uriToPrefixMap = new HashMap(); + private Map prefixToUriMap = new HashMap(); + + /** + * True if there has been some startPrefixMapping() for the coming element. + */ + private boolean hasMappings = false; + + /** + * Stack of the prefixes of explicitly generated prefix mapping calls + * per each element level. An entry is appended at the beginning of each + * {@link #startElement(String, String, String, Attributes)} call and + * removed at the end of each {@link #endElement(String, String, String)} + * call. By default the entry for each element is null to + * avoid losing performance, but whenever the code detects a new prefix + * mapping that needs to be registered, the null entry is + * replaced with a list of explicitly registered prefixes for that node. + * When that element is closed, the listed prefixes get unmapped. + * + * @see #checkPrefixMapping(String, String) + * @see JCR-1767 + */ + private final List addedPrefixMappings = new ArrayList(); + + private SerializingContentHandler(ContentHandler handler) { + super(handler); + } + + public void startDocument() throws SAXException { + // Cleanup + this.uriToPrefixMap.clear(); + this.prefixToUriMap.clear(); + clearMappings(); + super.startDocument(); + } + + /** + * Track mappings to be able to add xmlns: attributes + * in startElement(). + */ + public void startPrefixMapping(String prefix, String uri) throws SAXException { + // Store the mappings to reconstitute xmlns:attributes + // except prefixes starting with "xml": these are reserved + // VG: (uri != null) fixes NPE in startElement + if (uri != null && !prefix.startsWith("xml")) { + this.hasMappings = true; + this.prefixList.add(prefix); + this.uriList.add(uri); + + // append the prefix colon now, in order to save concatenations later, but + // only for non-empty prefixes. + if (prefix.length() > 0) { + this.uriToPrefixMap.put(uri, prefix + ":"); + } else { + this.uriToPrefixMap.put(uri, prefix); + } + + this.prefixToUriMap.put(prefix, uri); + } + super.startPrefixMapping(prefix, uri); + } + + /** + * Checks whether a prefix mapping already exists for the given namespace + * and generates the required {@link #startPrefixMapping(String, String)} + * call if the mapping is not found. By default the registered prefix + * is taken from the given qualified name, but a different prefix is + * automatically selected if that prefix is already used. + * + * @see JCR-1767 + * @param uri namespace URI + * @param qname element name with the prefix, or null + * @throws SAXException if the prefix mapping can not be added + */ + private void checkPrefixMapping(String uri, String qname) + throws SAXException { + // Only add the prefix mapping if the URI is not already known + if (uri != null && uri.length() > 0 && !uri.startsWith("xml") + && !uriToPrefixMap.containsKey(uri)) { + // Get the prefix + String prefix = "ns"; + if (qname != null && qname.length() > 0) { + int colon = qname.indexOf(':'); + if (colon != -1) { + prefix = qname.substring(0, colon); + } + } + + // Make sure that the prefix is unique + String base = prefix; + for (int i = 2; prefixToUriMap.containsKey(prefix); i++) { + prefix = base + i; + } + + int last = addedPrefixMappings.size() - 1; + List prefixes = (List) addedPrefixMappings.get(last); + if (prefixes == null) { + prefixes = new ArrayList(); + addedPrefixMappings.set(last, prefixes); + } + prefixes.add(prefix); + + startPrefixMapping(prefix, uri); + } + } + + /** + * Ensure all namespace declarations are present as xmlns: attributes + * and add those needed before calling superclass. This is a workaround for a Xalan bug + * (at least in version 2.0.1) : org.apache.xalan.serialize.SerializerToXML + * ignores start/endPrefixMapping(). + */ + public void startElement( + String eltUri, String eltLocalName, String eltQName, Attributes attrs) + throws SAXException { + // JCR-1767: Generate extra prefix mapping calls where needed + addedPrefixMappings.add(null); + checkPrefixMapping(eltUri, eltQName); + for (int i = 0; i < attrs.getLength(); i++) { + checkPrefixMapping(attrs.getURI(i), attrs.getQName(i)); + } + + // try to restore the qName. The map already contains the colon + if (null != eltUri && eltUri.length() != 0 && this.uriToPrefixMap.containsKey(eltUri)) { + eltQName = this.uriToPrefixMap.get(eltUri) + eltLocalName; + } + if (this.hasMappings) { + // Add xmlns* attributes where needed + + // New Attributes if we have to add some. + AttributesImpl newAttrs = null; + + int mappingCount = this.prefixList.size(); + int attrCount = attrs.getLength(); + + for (int mapping = 0; mapping < mappingCount; mapping++) { + + // Build infos for this namespace + String uri = (String) this.uriList.get(mapping); + String prefix = (String) this.prefixList.get(mapping); + String qName = prefix.equals("") ? "xmlns" : ("xmlns:" + prefix); + + // Search for the corresponding xmlns* attribute + boolean found = false; + for (int attr = 0; attr < attrCount; attr++) { + if (qName.equals(attrs.getQName(attr))) { + // Check if mapping and attribute URI match + if (!uri.equals(attrs.getValue(attr))) { + throw new SAXException("URI in prefix mapping and attribute do not match"); + } + found = true; + break; + } + } + + if (!found) { + // Need to add this namespace + if (newAttrs == null) { + // Need to test if attrs is empty or we go into an infinite loop... + // Well know SAX bug which I spent 3 hours to remind of :-( + if (attrCount == 0) { + newAttrs = new AttributesImpl(); + } else { + newAttrs = new AttributesImpl(attrs); + } + } + + if (prefix.equals("")) { + newAttrs.addAttribute(XML, qName, qName, "CDATA", uri); + } else { + newAttrs.addAttribute(XML, prefix, qName, "CDATA", uri); + } + } + } // end for mapping + + // Cleanup for the next element + clearMappings(); + + // Start element with new attributes, if any + super.startElement(eltUri, eltLocalName, eltQName, newAttrs == null ? attrs : newAttrs); + } else { + // Normal job + super.startElement(eltUri, eltLocalName, eltQName, attrs); + } + } + + + /** + * Receive notification of the end of an element. + * Try to restore the element qName. + */ + public void endElement(String eltUri, String eltLocalName, String eltQName) throws SAXException { + // try to restore the qName. The map already contains the colon + if (null != eltUri && eltUri.length() != 0 && this.uriToPrefixMap.containsKey(eltUri)) { + eltQName = this.uriToPrefixMap.get(eltUri) + eltLocalName; + } + + super.endElement(eltUri, eltLocalName, eltQName); + + // JCR-1767: Generate extra prefix un-mapping calls where needed + int last = addedPrefixMappings.size() - 1; + List prefixes = (List) addedPrefixMappings.remove(last); + if (prefixes != null) { + Iterator iterator = prefixes.iterator(); + while (iterator.hasNext()) { + endPrefixMapping((String) iterator.next()); + } + } + } + + /** + * End the scope of a prefix-URI mapping: + * remove entry from mapping tables. + */ + public void endPrefixMapping(String prefix) throws SAXException { + // remove mappings for xalan-bug-workaround. + // Unfortunately, we're not passed the uri, but the prefix here, + // so we need to maintain maps in both directions. + if (this.prefixToUriMap.containsKey(prefix)) { + this.uriToPrefixMap.remove(this.prefixToUriMap.get(prefix)); + this.prefixToUriMap.remove(prefix); + } + + if (hasMappings) { + // most of the time, start/endPrefixMapping calls have an element event between them, + // which will clear the hasMapping flag and so this code will only be executed in the + // rather rare occasion when there are start/endPrefixMapping calls with no element + // event in between. If we wouldn't remove the items from the prefixList and uriList here, + // the namespace would be incorrectly declared on the next element following the + // endPrefixMapping call. + int pos = prefixList.lastIndexOf(prefix); + if (pos != -1) { + prefixList.remove(pos); + uriList.remove(pos); + } + } + + super.endPrefixMapping(prefix); + } + + public void endDocument() throws SAXException { + // Cleanup + this.uriToPrefixMap.clear(); + this.prefixToUriMap.clear(); + clearMappings(); + super.endDocument(); + } + + private void clearMappings() { + this.hasMappings = false; + this.prefixList.clear(); + this.uriList.clear(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/SystemViewExporter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/SystemViewExporter.java new file mode 100644 index 00000000000..540d15250e9 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/SystemViewExporter.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.commons.xml; + +import java.io.IOException; +import java.io.Writer; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.value.ValueHelper; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * System view exporter. + * + * @since Jackrabbit JCR Commons 1.5 + */ +public class SystemViewExporter extends Exporter { + + /** + * The sv namespace URI. + */ + private static final String SV = "http://www.jcp.org/jcr/sv/1.0"; + + /** + * The xs namespace URI. + */ + private static final String XS = "http://www.w3.org/2001/XMLSchema"; + + /** + * The xsi namespace URI. + */ + private static final String XSI = + "http://www.w3.org/2001/XMLSchema-instance"; + + /** + * Creates a system view exporter. + * + * @param session current session + * @param handler SAX event handler for the export + * @param recurse whether to recursively export the whole subtree + * @param binary whether to export binary values + */ + public SystemViewExporter( + Session session, ContentHandler handler, + boolean recurse, boolean binary) { + super(session, handler, recurse, binary); + addNamespace("sv", SV); + } + + /** + * Exports the given node as an sv:node element. + */ + protected void exportNode(String uri, String local, Node node) + throws RepositoryException, SAXException { + addAttribute(SV, "name", getXMLName(uri, local)); + startElement(SV, "node"); + exportProperties(node); + exportNodes(node); + endElement(SV, "node"); + } + + /** + * Calls {@link #exportProperty(String, String, int, Value[])}. + */ + protected void exportProperty(String uri, String local, Value value) + throws RepositoryException, SAXException { + // start property element + addAttribute(SV, "name", getXMLName(uri, local)); + addAttribute(SV, "type", PropertyType.nameFromValue(value.getType())); + startElement(SV, "property"); + // value + exportValue(value); + endElement(SV, "property"); + } + + /** + * Exports the given property as an sv:property element. + */ + protected void exportProperty( + String uri, String local, int type, Value[] values) + throws RepositoryException, SAXException { + // start property element + addAttribute(SV, "name", getXMLName(uri, local)); + addAttribute(SV, "type", PropertyType.nameFromValue(type)); + addAttribute(SV, "multiple", Boolean.TRUE.toString()); + startElement(SV, "property"); + // values + for (int i = 0; i < values.length; i++) { + exportValue(values[i]); + } + endElement(SV, "property"); + } + + /** + * Exports the given value as an sv:value element. + * + * @param value value to be exported + */ + private void exportValue(Value value) + throws RepositoryException, SAXException { + try { + boolean binary = mustSendBinary(value); + if (binary) { + addNamespace("xs", XS); + addNamespace("xsi", XSI); + addAttribute(XSI, "type", getXMLName(XS, "base64Binary")); + } + startElement(SV, "value"); + ValueHelper.serialize(value, false, binary, new Writer() { + public void write(char[] cbuf, int off, int len) + throws IOException { + try { + SystemViewExporter.this.characters(cbuf, off, len); + } catch (Exception e) { + IOException exception = new IOException(); + exception.initCause(e); + throw exception; + } + } + public void close() { + } + public void flush() { + } + }); + endElement(SV, "value"); + } catch (IOException e) { + // check if the exception wraps a SAXException + // (see Writer.write(char[], int, int) above) + if (e.getCause() instanceof SAXException) { + throw (SAXException) e.getCause(); + } else { + throw new RepositoryException(e); + } + } + } + + /** + * Utility method for determining whether a non-binary value should + * still be exported in base64 encoding to avoid emitting invalid XML + * characters. + * + * @param value value to be exported + * @return whether the value should be treated as a binary + * @throws RepositoryException if a repository error occurs + */ + private boolean mustSendBinary(Value value) throws RepositoryException { + if (value.getType() != PropertyType.BINARY) { + String string = value.getString(); + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + if (c >= 0 && c < 32 && c != '\n' && c != '\t') { + return true; + } + } + } + return false; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ToXmlContentHandler.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ToXmlContentHandler.java new file mode 100644 index 00000000000..a053da27a33 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/ToXmlContentHandler.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.xml; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Simple XML serializer. This content handler serializes the received + * SAX events as XML to a given {@link Writer} or {@link OutputStream}. + * The serialization assumes that the incoming SAX events are well-formed, + * i.e. that all elements are properly nested, that element and attribute + * names are valid and that no invalid XML characters are included. Assuming + * these preconditions are met, the result will be a well-formed XML stream. + *

    + * This serializer does not have any special support for namespaces. For + * example, namespace prefixes are declared in the resulting XML stream + * if and only if the corresponding "xmlns" attributes are explicitly + * included in the {@link Attributes} instances passed in + * {@link #startElement(String, String, String, Attributes)} calls. + *

    + * As a convenience this class inherits the {@link DefaultHandler} class + * instead of just the {@link ContentHandler} interface. This makes it + * easier to pass instances of this class to methods like + * {@link javax.xml.parsers.SAXParser#parse(String, DefaultHandler)} that + * expect a DefaultHandler instance instead of a ContentHandler. + */ +public class ToXmlContentHandler extends DefaultHandler { + + /** + * The XML stream. + */ + private final Writer writer; + + /** + * The data part of the <?xml?> processing instruction included + * at the beginning of the XML stream. + */ + private final String declaration; + + /** + * Flag variable that is used to track whether a start tag has had it's + * closing ">" appended. Set to true by the + * {@link #startElement(String, String, String, Attributes)} method that + * does not output the closing ">". If this flag is still set + * when the {#link {@link #endElement(String, String, String)}} method + * is called, then the method knows that the element is empty and can + * close it with "/>". Any other SAX event will cause the open start tag + * to be closed with a normal ">". + * + * @see #closeStartTagIfOpen() + */ + private boolean startTagIsOpen = false; + + //--------------------------------------------------------< constructors > + + /** + * Creates an XML serializer that writes the serialized XML stream + * to the given output stream using the given character encoding. + * + * @param stream XML output stream + * @param encoding character encoding + * @throws UnsupportedEncodingException if the encoding is not supported + */ + public ToXmlContentHandler(OutputStream stream, String encoding) + throws UnsupportedEncodingException { + this.writer = new OutputStreamWriter(stream, encoding); + this.declaration = "version=\"1.0\" encoding=\"" + encoding + "\""; + } + + /** + * Creates an XML serializer that writes the serialized XML stream + * to the given output stream using the UTF-8 character encoding. + * + * @param stream XML output stream + */ + public ToXmlContentHandler(OutputStream stream) { + this.writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8); + this.declaration = "version=\"1.0\" encoding=\"UTF-8\""; + } + + /** + * Creates an XML serializer that writes the serialized XML stream + * to the given writer. + * + * @param writer XML output stream + */ + public ToXmlContentHandler(Writer writer) { + this.writer = writer; + this.declaration = "version=\"1.0\""; + } + + /** + * Creates an XML serializer that writes the serialized XML stream + * to an internal buffer. Use the {@link #toString()} method to access + * the serialized XML document. + */ + public ToXmlContentHandler() { + this(new StringWriter()); + } + + //-------------------------------------------------------------< private > + + private void write(char[] ch, int start, int length, boolean attribute) + throws SAXException { + for (int i = start; i < start + length; i++) { + try { + if (ch[i] == '>') { + writer.write(">"); + } else if (ch[i] == '<') { + writer.write("<"); + } else if (ch[i] == '&') { + writer.write("&"); + } else if (attribute && ch[i] == '"') { + writer.write("""); + } else if (attribute && ch[i] == '\'') { + writer.write("'"); + } else { + writer.write(ch[i]); + } + } catch (IOException e) { + throw new SAXException( + "Failed to output XML character: " + ch[i], e); + } + } + } + + private void closeStartTagIfOpen() throws SAXException { + if (startTagIsOpen) { + try { + writer.write(">"); + } catch (IOException e) { + throw new SAXException( + "Failed to output XML bracket: >", e); + } + startTagIsOpen = false; + } + } + + //------------------------------------------------------< ContentHandler > + + /** + * Starts the XML serialization by outputting the <?xml?> header. + */ + public void startDocument() throws SAXException { + processingInstruction("xml", declaration); + } + + /** + * Ends the XML serialization by flushing the output stream. + */ + public void endDocument() throws SAXException { + try { + writer.flush(); + } catch (IOException e) { + throw new SAXException("Failed to flush XML output", e); + } + } + + /** + * Serializes a processing instruction. + */ + public void processingInstruction(String target, String data) + throws SAXException { + closeStartTagIfOpen(); + try { + writer.write(""); + } catch (IOException e) { + throw new SAXException( + "Failed to output XML processing instruction: " + target, e); + } + } + + /** + * Outputs the specified start tag with the given attributes. + */ + public void startElement( + String namespaceURI, String localName, String qName, + Attributes atts) throws SAXException { + closeStartTagIfOpen(); + try { + writer.write("<"); + writer.write(qName); + for (int i = 0; i < atts.getLength(); i++) { + writer.write(" "); + writer.write(atts.getQName(i)); + writer.write("=\""); + char[] ch = atts.getValue(i).toCharArray(); + write(ch, 0, ch.length, true); + writer.write("\""); + } + startTagIsOpen = true; + } catch (IOException e) { + throw new SAXException( + "Failed to output XML end tag: " + qName, e); + } + } + + /** + * Escapes and outputs the given characters. + */ + public void characters(char[] ch, int start, int length) + throws SAXException { + closeStartTagIfOpen(); + write(ch, start, length, false); + } + + /** + * Escapes and outputs the given characters. + */ + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + characters(ch, start, length); + } + + /** + * Outputs the specified end tag. + */ + public void endElement( + String namespaceURI, String localName, String qName) + throws SAXException { + try { + if (startTagIsOpen) { + writer.write("/>"); + startTagIsOpen = false; + } else { + writer.write(""); + } + } catch (IOException e) { + throw new SAXException( + "Failed to output XML end tag: " + qName, e); + } + } + + //--------------------------------------------------------------< Object > + + /** + * Returns the serialized XML document (assuming the default no-argument + * constructor was used). + * + * @return serialized XML document + */ + public String toString() { + return writer.toString(); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/XmlnsContentHandler.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/XmlnsContentHandler.java new file mode 100644 index 00000000000..e2813ce10bc --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/XmlnsContentHandler.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.commons.xml; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Content handler proxy that adds explicit "xmlns" attributes for all + * namespace mappings introduced through + * {@link #startPrefixMapping(String, String)} calls. + */ +public class XmlnsContentHandler extends ProxyContentHandler { + + /** + * Namespace of the "xmlns" attributes. + */ + private static final String XMLNS_NAMESPACE = + "http://www.w3.org/2000/xmlns/"; + + /** + * Namespace mappings recorded for the next element. + */ + private final LinkedHashMap namespaces = new LinkedHashMap(); + + public XmlnsContentHandler(ContentHandler handler) { + super(handler); + } + + //------------------------------------------------------< ContentHandler > + + /** + * Records the namespace mapping and passes the call to the proxied + * content handler. + */ + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + namespaces.put(prefix, uri); + super.startPrefixMapping(prefix, uri); + } + + /** + * Adds the recorded namespace mappings (if any) as "xmlns" attributes + * before passing the call on to the proxied content handler. + */ + public void startElement( + String namespaceURI, String localName, String qName, + Attributes atts) throws SAXException { + if (!namespaces.isEmpty()) { + AttributesImpl attributes = new AttributesImpl(atts); + Iterator iterator = namespaces.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String prefix = (String) entry.getKey(); + String uri = (String) entry.getValue(); + if (prefix.length() == 0) { + attributes.addAttribute( + XMLNS_NAMESPACE, "xmlns", "xmlns", + "CDATA", uri); + } else { + attributes.addAttribute( + XMLNS_NAMESPACE, prefix, "xmlns:" + prefix, + "CDATA", uri); + } + } + atts = attributes; + namespaces.clear(); + } + super.startElement(namespaceURI, localName, qName, atts); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/package-info.java new file mode 100644 index 00000000000..28b80f00b30 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/xml/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.2") +package org.apache.jackrabbit.commons.xml; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/package-info.java new file mode 100644 index 00000000000..26f32fe40f9 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.2") +package org.apache.jackrabbit; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatCore.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatCore.java new file mode 100644 index 00000000000..71e2fa051f1 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatCore.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.stats; + +import org.apache.jackrabbit.api.stats.QueryStat; + +/** + * Extends external facing {@link QueryStat} with some internal operations. + */ +public interface QueryStatCore extends QueryStat { + + /** + * Logs the call of each query ran on the repository. + * + * @param language + * the query language, see + * {@link javax.jcr.query.QueryManager#getSupportedQueryLanguages()} + * @param statement + * the query + * @param durationMs + * time in ms + */ + void logQuery(final String language, final String statement, long durationMs); +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoComparator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoComparator.java new file mode 100644 index 00000000000..7a14e93d59b --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoComparator.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.stats; + +import java.util.Comparator; + +import org.apache.jackrabbit.api.stats.QueryStatDto; + +/** + * QueryStatDto comparator by duration + * + */ +public class QueryStatDtoComparator implements Comparator { + public int compare(QueryStatDto o1, QueryStatDto o2) { + return new Long(o1.getDuration()).compareTo(o2.getDuration()); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoImpl.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoImpl.java new file mode 100644 index 00000000000..06323aa3464 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoImpl.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.stats; + +import java.util.Calendar; +import java.util.Date; + +import org.apache.jackrabbit.api.stats.QueryStatDto; + +/** + * Object that holds statistical info about a query. + * + */ +public class QueryStatDtoImpl implements QueryStatDto { + + private static final long serialVersionUID = 1L; + + /** + * lazy, computed at call time + */ + private long position; + + /** + * the time that the query was created + */ + private final Date creationTime; + + /** + * run duration in ms + */ + private final long durationMs; + + /** + * query language + */ + private final String language; + + /** + * query statement + */ + private final String statement; + + /** + * used in popular queries list + */ + private int occurrenceCount = 1; + + public QueryStatDtoImpl(final String language, final String statement, + long durationMs) { + this.durationMs = durationMs; + this.language = language; + this.statement = statement; + + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(System.currentTimeMillis() - durationMs); + this.creationTime = c.getTime(); + } + + public long getDuration() { + return durationMs; + } + + public String getLanguage() { + return language; + } + + public String getStatement() { + return statement; + } + + public String getCreationTime() { + return creationTime.toString(); + } + + public long getPosition() { + return position; + } + + public void setPosition(long position) { + this.position = position; + } + + @Override + public String toString() { + return "QueryStat [creationTime=" + creationTime + ", duration=" + + durationMs + ", position " + position + ", language=" + + language + ", statement=" + statement + "]"; + } + + public int getOccurrenceCount() { + return occurrenceCount; + } + + public void setOccurrenceCount(int occurrenceCount) { + this.occurrenceCount = occurrenceCount; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((language == null) ? 0 : language.hashCode()); + result = prime * result + + ((statement == null) ? 0 : statement.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; + QueryStatDtoImpl other = (QueryStatDtoImpl) obj; + if (language == null) { + if (other.language != null) + return false; + } else if (!language.equals(other.language)) + return false; + if (statement == null) { + if (other.statement != null) + return false; + } else if (!statement.equals(other.statement)) + return false; + return true; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoOccurrenceComparator.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoOccurrenceComparator.java new file mode 100644 index 00000000000..dcaf5a6ed9b --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatDtoOccurrenceComparator.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.stats; + +import java.util.Comparator; + +/** + * QueryStatDto comparator by occurrence count + * + * used by the popular queries queue + * + */ +public class QueryStatDtoOccurrenceComparator implements + Comparator { + public int compare(QueryStatDtoImpl o1, QueryStatDtoImpl o2) { + return new Integer(o1.getOccurrenceCount()).compareTo(o2 + .getOccurrenceCount()); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatImpl.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatImpl.java new file mode 100644 index 00000000000..fb235000c8f --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/QueryStatImpl.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.stats; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.concurrent.PriorityBlockingQueue; + +import org.apache.jackrabbit.api.stats.QueryStatDto; + +/** + * Default {@link QueryStatCore} implementation + * + */ +public class QueryStatImpl implements QueryStatCore { + + private final static Comparator comparator = new QueryStatDtoComparator(); + + private final BoundedPriorityBlockingQueue slowQueries = new BoundedPriorityBlockingQueue( + 15, comparator); + + private final static Comparator comparatorOccurrence = new QueryStatDtoOccurrenceComparator(); + + /** + * the real queue size will be bigger than the desired number of popular + * queries by POPULAR_QUEUE_MULTIPLIER times + */ + private static final int POPULAR_QUEUE_MULTIPLIER = 5; + + private final BoundedPriorityBlockingQueue popularQueries = new BoundedPriorityBlockingQueue( + 15 * POPULAR_QUEUE_MULTIPLIER, comparatorOccurrence); + + private static final class BoundedPriorityBlockingQueue extends + PriorityBlockingQueue { + + private static final long serialVersionUID = 1L; + private int maxSize; + + public BoundedPriorityBlockingQueue(int maxSize, + Comparator comparator) { + super(maxSize + 1, comparator); + this.maxSize = maxSize; + } + + @Override + public boolean offer(E e) { + boolean s = super.offer(e); + if (!s) { + return false; + } + if (size() > maxSize) { + poll(); + } + return true; + } + + public synchronized void setMaxSize(int maxSize) { + if (maxSize < this.maxSize) { + // shrink the queue + int delta = super.size() - maxSize; + for (int i = 0; i < delta; i++) { + E t = poll(); + if (t == null) { + break; + } + } + } + this.maxSize = maxSize; + } + + public int getMaxSize() { + return maxSize; + } + } + + private boolean enabled = false; + + public QueryStatImpl() { + } + + public int getSlowQueriesQueueSize() { + return slowQueries.getMaxSize(); + } + + public void setSlowQueriesQueueSize(int size) { + slowQueries.setMaxSize(size); + } + + public boolean isEnabled() { + return enabled; + } + + public synchronized void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void logQuery(final String language, final String statement, + long durationMs) { + if (!enabled) { + return; + } + final QueryStatDtoImpl qs = new QueryStatDtoImpl(language, statement, + durationMs); + slowQueries.offer(qs); + + synchronized (popularQueries) { + Iterator iterator = popularQueries.iterator(); + while (iterator.hasNext()) { + QueryStatDtoImpl qsdi = iterator.next(); + if (qsdi.equals(qs)) { + qs.setOccurrenceCount(qsdi.getOccurrenceCount() + 1); + iterator.remove(); + break; + } + } + popularQueries.offer(qs); + } + } + + public void clearSlowQueriesQueue() { + slowQueries.clear(); + } + + public QueryStatDto[] getSlowQueries() { + QueryStatDto[] top = slowQueries.toArray(new QueryStatDto[slowQueries + .size()]); + Arrays.sort(top, Collections.reverseOrder(comparator)); + for (int i = 0; i < top.length; i++) { + top[i].setPosition(i + 1); + } + return top; + } + + public QueryStatDto[] getPopularQueries() { + QueryStatDtoImpl[] top; + int size = 0; + int maxSize = 0; + synchronized (popularQueries) { + top = popularQueries.toArray(new QueryStatDtoImpl[popularQueries + .size()]); + size = popularQueries.size(); + maxSize = popularQueries.getMaxSize(); + } + Arrays.sort(top, Collections.reverseOrder(comparatorOccurrence)); + int retSize = Math.min(size, maxSize / POPULAR_QUEUE_MULTIPLIER); + QueryStatDto[] retval = new QueryStatDto[retSize]; + for (int i = 0; i < retSize; i++) { + retval[i] = top[i]; + retval[i].setPosition(i + 1); + } + return retval; + } + + public int getPopularQueriesQueueSize() { + return popularQueries.getMaxSize() / POPULAR_QUEUE_MULTIPLIER; + } + + public void setPopularQueriesQueueSize(int size) { + popularQueries.setMaxSize(size * POPULAR_QUEUE_MULTIPLIER); + } + + public void clearPopularQueriesQueue() { + popularQueries.clear(); + } + + public void reset() { + clearSlowQueriesQueue(); + clearPopularQueriesQueue(); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/RepositoryStatisticsImpl.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/RepositoryStatisticsImpl.java new file mode 100644 index 00000000000..68195793726 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/RepositoryStatisticsImpl.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.stats; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.jackrabbit.api.stats.RepositoryStatistics; +import org.apache.jackrabbit.api.stats.TimeSeries; + +public class RepositoryStatisticsImpl implements + Iterable>, RepositoryStatistics { + + private final Map recorders = + new HashMap(); + + private final Map avg = + new HashMap(); + + public RepositoryStatisticsImpl() { + getOrCreateRecorder(Type.SESSION_COUNT); + getOrCreateRecorder(Type.SESSION_LOGIN_COUNTER); + + createAvg(Type.SESSION_READ_COUNTER, Type.SESSION_READ_DURATION, + Type.SESSION_READ_AVERAGE); + createAvg(Type.SESSION_WRITE_COUNTER, Type.SESSION_WRITE_DURATION, + Type.SESSION_WRITE_AVERAGE); + createAvg(Type.BUNDLE_CACHE_MISS_COUNTER, + Type.BUNDLE_CACHE_MISS_DURATION, Type.BUNDLE_CACHE_MISS_AVERAGE); + createAvg(Type.BUNDLE_WRITE_COUNTER, Type.BUNDLE_WRITE_DURATION, + Type.BUNDLE_WRITE_AVERAGE); + createAvg(Type.QUERY_COUNT, Type.QUERY_DURATION, + Type.QUERY_AVERAGE); + createAvg(Type.OBSERVATION_EVENT_COUNTER, Type.OBSERVATION_EVENT_DURATION, + Type.OBSERVATION_EVENT_AVERAGE); + } + + private void createAvg(Type count, Type duration, Type avgTs) { + avg.put(avgTs.name(), new TimeSeriesAverage(getOrCreateRecorder(duration), + getOrCreateRecorder(count))); + } + + public RepositoryStatisticsImpl(ScheduledExecutorService executor) { + this(); + executor.scheduleAtFixedRate(new Runnable() { + public void run() { + recordOneSecond(); + } + }, 1, 1, TimeUnit.SECONDS); + } + + public synchronized Iterator> iterator() { + Map map = new TreeMap(); + map.putAll(recorders); + map.putAll(avg); + return map.entrySet().iterator(); + } + + public AtomicLong getCounter(Type type) { + return getOrCreateRecorder(type).getCounter(); + } + + public AtomicLong getCounter(String type, boolean resetValueEachSecond) { + return getOrCreateRecorder(type, resetValueEachSecond).getCounter(); + } + + public TimeSeries getTimeSeries(Type type) { + return getTimeSeries(type.name(), type.isResetValueEachSecond()); + } + + public TimeSeries getTimeSeries(String type, boolean resetValueEachSecond) { + if (avg.containsKey(type)) { + return avg.get(type); + } + return getOrCreateRecorder(type, resetValueEachSecond); + } + + private synchronized TimeSeriesRecorder getOrCreateRecorder(Type type) { + return getOrCreateRecorder(type.name(), type.isResetValueEachSecond()); + } + + private synchronized TimeSeriesRecorder getOrCreateRecorder(String type, boolean resetValueEachSecond) { + TimeSeriesRecorder recorder = recorders.get(type); + if (recorder == null) { + recorder = new TimeSeriesRecorder(resetValueEachSecond); + recorders.put(type, recorder); + } + return recorder; + } + + private synchronized void recordOneSecond() { + for (TimeSeriesRecorder recorder : recorders.values()) { + recorder.recordOneSecond(); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesAverage.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesAverage.java new file mode 100644 index 00000000000..e71e5906af8 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesAverage.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.stats; + +import org.apache.jackrabbit.api.stats.TimeSeries; + +/** + * Time series of the average calculated by dividing a measured + * value by the counter of events during the measurement period. + */ +public class TimeSeriesAverage implements TimeSeries { + + /** Value */ + private final TimeSeries value; + + /** Value */ + private final TimeSeries counter; + + /** The value used to encode missing values */ + private final long missingValue; + + /** + * Same as {@link #TimeSeriesAverage(TimeSeries, TimeSeries, long)} passing 0 for the 3rd argument. + * @param value {@code TimeSeries} of values + * @param counter {@code TimeSeries} of counts + */ + public TimeSeriesAverage(TimeSeries value, TimeSeries counter) { + this(value, counter, 0); + } + + /** + * @param value {@code TimeSeries} of values + * @param counter {@code TimeSeries} of counts + * @param missingValue The value used to encode missing values + */ + public TimeSeriesAverage(TimeSeries value, TimeSeries counter, long missingValue) { + this.value = value; + this.counter = counter; + this.missingValue = missingValue; + } + + //----------------------------------------------------------< TimeSeries > + + @Override + public long getMissingValue() { + return missingValue; + } + + @Override + public long[] getValuePerSecond() { + long[] values = value.getValuePerSecond(); + long[] counts = counter.getValuePerSecond(); + return divide(values, counts); + } + + @Override + public long[] getValuePerMinute() { + long[] values = value.getValuePerMinute(); + long[] counts = counter.getValuePerMinute(); + return divide(values, counts); + } + + @Override + public synchronized long[] getValuePerHour() { + long[] values = value.getValuePerHour(); + long[] counts = counter.getValuePerHour(); + return divide(values, counts); + } + + @Override + public synchronized long[] getValuePerWeek() { + long[] values = value.getValuePerWeek(); + long[] counts = counter.getValuePerWeek(); + return divide(values, counts); + } + + //-------------------------------------------------------------< private > + + /** + * Per-entry division of two arrays. + * + * @param v array + * @param c array + * @return result of division + */ + private long[] divide(long[] v, long[] c) { + long[] avg = new long[v.length]; + for (int i = 0; i < v.length; i++) { + if (c[i] == 0 || v[i] == value.getMissingValue() || c[i] == counter.getMissingValue()) { + avg[i] = missingValue; + } else { + avg[i] = v[i] / c[i]; + } + } + return avg; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesMax.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesMax.java new file mode 100644 index 00000000000..8a71ade5a48 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesMax.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.jackrabbit.stats; + +import static java.util.Arrays.fill; + +import org.apache.jackrabbit.api.stats.TimeSeries; + +/** + * Time series of the maximum value recorded in a period + */ +public class TimeSeriesMax implements TimeSeries { + private final MaxValue max; + private final long missingValue; + private final long[] perSecond; + private final long[] perMinute; + private final long[] perHour; + private final long[] perWeek; + + /** Current second (index in {@link #perSecond}) */ + private int seconds; + + /** Current minute (index in {@link #perMinute}) */ + private int minutes; + + /** Current hour (index in {@link #perHour}) */ + private int hours; + + /** Current week (index in {@link #perWeek}) */ + private int weeks; + + public TimeSeriesMax() { + this(0); + } + + public TimeSeriesMax(long missingValue) { + this.missingValue = missingValue; + max = new MaxValue(missingValue); + perSecond = newArray(60, missingValue); + perMinute = newArray(60, missingValue); + perHour = newArray(7 * 24, missingValue); + perWeek = newArray(3 * 52, missingValue); + } + + private static long[] newArray(int size, long value) { + long[] array = new long[size]; + fill(array, value); + return array; + } + + public void recordValue(long value) { + max.setIfMaximal(value); + } + + /** + * Records the number of measured values over the past second and resets + * the counter. This method should be scheduled to be called once per + * second. + */ + public synchronized void recordOneSecond() { + perSecond[seconds++] = max.getAndSetValue(missingValue); + if (seconds == perSecond.length) { + seconds = 0; + perMinute[minutes++] = max(perSecond); + } + if (minutes == perMinute.length) { + minutes = 0; + perHour[hours++] = max(perMinute); + } + if (hours == perHour.length) { + hours = 0; + perWeek[weeks++] = max(perHour); + } + if (weeks == perWeek.length) { + weeks = 0; + } + } + + @Override + public long getMissingValue() { + return missingValue; + } + + @Override + public synchronized long[] getValuePerSecond() { + return cyclicCopyFrom(perSecond, seconds); + } + + @Override + public synchronized long[] getValuePerMinute() { + return cyclicCopyFrom(perMinute, minutes); + } + + @Override + public synchronized long[] getValuePerHour() { + return cyclicCopyFrom(perHour, hours); + } + + @Override + public synchronized long[] getValuePerWeek() { + return cyclicCopyFrom(perWeek, weeks); + } + + /** + * Returns the maximum of all entries in the given array. + */ + private long max(long[] array) { + long max = missingValue; + for (long v : array) { + if (max == missingValue) { + max = v; + } else if (v != missingValue) { + max = Math.max(max, v); + } + } + return max; + } + + /** + * Returns a copy of the given cyclical array, with the element at + * the given position as the first element of the returned array. + * + * @param array cyclical array + * @param pos position of the first element + * @return copy of the array + */ + private static long[] cyclicCopyFrom(long[] array, int pos) { + long[] reverse = new long[array.length]; + for (int i = 0; i < array.length; i++) { + reverse[i] = array[(pos + i) % array.length]; + } + return reverse; + } + + private class MaxValue { + private long max; + + public MaxValue(long max) { + this.max = max; + } + + public synchronized long getAndSetValue(long value) { + long v = max; + max = value; + return v; + } + + public synchronized void setIfMaximal(long value) { + if (max == missingValue) { + max = value; + } else if (value != missingValue) { + max = Math.max(max, value); + } + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesRecorder.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesRecorder.java new file mode 100755 index 00000000000..7fb288967cd --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesRecorder.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.stats; + +import static java.lang.Math.round; + +import static java.util.Arrays.fill; + +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.jackrabbit.api.stats.RepositoryStatistics.Type; +import org.apache.jackrabbit.api.stats.TimeSeries; + +/** + * Recorder of a time series. An instance of this class records (and clears) + * the state of a given {@link AtomicLong} counter once every second and + * exposes the collected time series through the {@link TimeSeries} + * interface. + */ +public class TimeSeriesRecorder implements TimeSeries { + + /** Value */ + private final AtomicLong counter; + + /** Whether to reset value each second */ + private final boolean resetValueEachSecond; + + /** The value used to encode missing values */ + private final long missingValue; + + /** Measured value per second over the last minute. */ + private final long[] valuePerSecond; + + /** Measured value per minute over the last hour. */ + private final long[] valuePerMinute; + + /** Measured value per hour over the last week. */ + private final long[] valuePerHour; + + /** Measured value per week over the last three years. */ + private final long[] valuePerWeek; + + /** Current second (index in {@link #valuePerSecond}) */ + private int seconds; + + /** Current minute (index in {@link #valuePerMinute}) */ + private int minutes; + + /** Current hour (index in {@link #valuePerHour}) */ + private int hours; + + /** Current week (index in {@link #valuePerWeek}) */ + private int weeks; + + public TimeSeriesRecorder(Type type) { + this(type.isResetValueEachSecond()); + } + + /** + * Same as {@link #TimeSeriesRecorder(boolean, long)} passing long for the 2nd argument + * @param resetValueEachSecond Whether to reset value each second + */ + public TimeSeriesRecorder(boolean resetValueEachSecond) { + this(resetValueEachSecond, 0); + } + + /** + * @param resetValueEachSecond Whether to reset value each second + * @param missingValue The value used to encode missing values + */ + public TimeSeriesRecorder(boolean resetValueEachSecond, long missingValue) { + this.resetValueEachSecond = resetValueEachSecond; + this.missingValue = missingValue; + counter = new AtomicLong(missingValue); + valuePerSecond = newArray(60, missingValue); + valuePerMinute = newArray(60, missingValue); + valuePerHour = newArray(7 * 24, missingValue); + valuePerWeek = newArray(3 * 52, missingValue); + } + + private static long[] newArray(int size, long value) { + long[] array = new long[size]; + fill(array, value); + return array; + } + + /** + * Returns the {@link AtomicLong} instance used to measure the value for + * the time series. + * + * @return value + */ + public AtomicLong getCounter() { + return counter; + } + + /** + * Records the number of measured values over the past second and resets + * the counter. This method should be scheduled to be called once per + * second. + */ + public synchronized void recordOneSecond() { + if (resetValueEachSecond) { + valuePerSecond[seconds++] = counter.getAndSet(missingValue); + } else { + valuePerSecond[seconds++] = counter.get(); + } + if (seconds == valuePerSecond.length) { + seconds = 0; + valuePerMinute[minutes++] = aggregate(valuePerSecond); + } + if (minutes == valuePerMinute.length) { + minutes = 0; + valuePerHour[hours++] = aggregate(valuePerMinute); + } + if (hours == valuePerHour.length) { + hours = 0; + valuePerWeek[weeks++] = aggregate(valuePerHour); + } + if (weeks == valuePerWeek.length) { + weeks = 0; + } + } + + //----------------------------------------------------------< TimeSeries > + + + @Override + public long getMissingValue() { + return missingValue; + } + + @Override + public synchronized long[] getValuePerSecond() { + return cyclicCopyFrom(valuePerSecond, seconds); + } + + @Override + public synchronized long[] getValuePerMinute() { + return cyclicCopyFrom(valuePerMinute, minutes); + } + + @Override + public synchronized long[] getValuePerHour() { + return cyclicCopyFrom(valuePerHour, hours); + } + + @Override + public synchronized long[] getValuePerWeek() { + return cyclicCopyFrom(valuePerWeek, weeks); + } + + //-------------------------------------------------------------< private > + + /** + * Returns the sum of all entries in the given array. + * + * @param array array to be summed + * @return sum of entries + */ + private long aggregate(long[] array) { + long sum = 0; + int count = 0; + for (long value : array) { + if (value != missingValue) { + count++; + sum += value; + } + } + if (count == 0) { + return missingValue; + } else if (resetValueEachSecond) { + return sum; + } else { + return round((double) sum / count); + } + } + + /** + * Returns a copy of the given cyclical array, with the element at + * the given position as the first element of the returned array. + * + * @param array cyclical array + * @param pos position of the first element + * @return copy of the array + */ + private static long[] cyclicCopyFrom(long[] array, int pos) { + long[] reverse = new long[array.length]; + for (int i = 0; i < array.length; i++) { + reverse[i] = array[(pos + i) % array.length]; + } + return reverse; + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesStatsUtil.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesStatsUtil.java new file mode 100644 index 00000000000..afc950f04c6 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/TimeSeriesStatsUtil.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.stats; + +import javax.management.openmbean.ArrayType; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; + +import org.apache.jackrabbit.api.stats.TimeSeries; + +/** + * Utility class for retrieving {@link javax.management.openmbean.CompositeData} for + * {@link org.apache.jackrabbit.api.stats.TimeSeries}. + */ +public final class TimeSeriesStatsUtil { + public static final String[] ITEM_NAMES = new String[] {"per second", "per minute", "per hour", "per week"}; + + private TimeSeriesStatsUtil() { + } + + public static CompositeData asCompositeData(TimeSeries timeSeries, String name) { + try { + long[][] values = new long[][] {timeSeries.getValuePerSecond(), timeSeries.getValuePerMinute(), + timeSeries.getValuePerHour(), timeSeries.getValuePerWeek()}; + return new CompositeDataSupport(getCompositeType(name), ITEM_NAMES, values); + } catch (Exception e) { + throw new IllegalArgumentException("Error creating CompositeData instance from TimeSeries", e); + } + } + + private static CompositeType getCompositeType(String name) throws OpenDataException { + ArrayType longArrayType = new ArrayType(SimpleType.LONG, true); + OpenType[] itemTypes = new OpenType[] {longArrayType, longArrayType, longArrayType, longArrayType}; + return new CompositeType(name, name + " time series", ITEM_NAMES, ITEM_NAMES, itemTypes); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/jmx/QueryStatManager.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/jmx/QueryStatManager.java new file mode 100644 index 00000000000..d1b7bf30b79 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/jmx/QueryStatManager.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.stats.jmx; + +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; +import javax.management.openmbean.TabularData; +import javax.management.openmbean.TabularDataSupport; +import javax.management.openmbean.TabularType; + +import org.apache.jackrabbit.api.jmx.QueryStatManagerMBean; +import org.apache.jackrabbit.api.stats.QueryStat; +import org.apache.jackrabbit.api.stats.QueryStatDto; + +/** + * The QueryStatManagerMBean default implementation + * + */ +public class QueryStatManager implements QueryStatManagerMBean { + + private final QueryStat queryStat; + + public QueryStatManager(final QueryStat queryStat) { + this.queryStat = queryStat; + } + + public boolean isEnabled() { + return this.queryStat.isEnabled(); + } + + public void enable() { + this.queryStat.setEnabled(true); + } + + public void disable() { + this.queryStat.setEnabled(false); + } + + public void reset() { + this.queryStat.reset(); + } + + public int getSlowQueriesQueueSize() { + return queryStat.getSlowQueriesQueueSize(); + } + + public void setSlowQueriesQueueSize(int size) { + this.queryStat.setSlowQueriesQueueSize(size); + } + + public void clearSlowQueriesQueue() { + this.queryStat.clearSlowQueriesQueue(); + } + + public int getPopularQueriesQueueSize() { + return queryStat.getPopularQueriesQueueSize(); + } + + public void setPopularQueriesQueueSize(int size) { + queryStat.setPopularQueriesQueueSize(size); + } + + public void clearPopularQueriesQueue() { + queryStat.clearPopularQueriesQueue(); + } + + public TabularData getSlowQueries() { + return asTabularData(queryStat.getSlowQueries()); + } + + public TabularData getPopularQueries() { + return asTabularData(queryStat.getPopularQueries()); + } + + private TabularData asTabularData(QueryStatDto[] data) { + TabularDataSupport tds = null; + try { + CompositeType ct = QueryStatCompositeTypeFactory.getCompositeType(); + + TabularType tt = new TabularType(QueryStatDto.class.getName(), + "Query History", ct, QueryStatCompositeTypeFactory.index); + tds = new TabularDataSupport(tt); + + for (QueryStatDto q : data) { + tds.put(new CompositeDataSupport(ct, + QueryStatCompositeTypeFactory.names, + QueryStatCompositeTypeFactory.getValues(q))); + } + return tds; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + private static class QueryStatCompositeTypeFactory { + + private final static String[] index = { "position" }; + + private final static String[] names = { "position", "duration", + "occurrenceCount", "language", "statement", "creationTime" }; + + private final static String[] descriptions = { "position", "duration", + "occurrenceCount", "language", "statement", "creationTime" }; + + private final static OpenType[] types = { SimpleType.LONG, + SimpleType.LONG, SimpleType.INTEGER, SimpleType.STRING, + SimpleType.STRING, SimpleType.STRING }; + + public static CompositeType getCompositeType() throws OpenDataException { + return new CompositeType(QueryStat.class.getName(), + QueryStat.class.getName(), names, descriptions, types); + } + + public static Object[] getValues(QueryStatDto q) { + return new Object[] { q.getPosition(), q.getDuration(), + q.getOccurrenceCount(), q.getLanguage(), q.getStatement(), + q.getCreationTime() }; + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/jmx/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/jmx/package-info.java new file mode 100755 index 00000000000..e997ff9968e --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/jmx/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.stats.jmx; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/package-info.java new file mode 100644 index 00000000000..5a8b92718ee --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/stats/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.7.5") +package org.apache.jackrabbit.stats; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Base64.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Base64.java new file mode 100644 index 00000000000..f6708899252 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Base64.java @@ -0,0 +1,380 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.io.BufferedWriter; +import java.nio.charset.StandardCharsets; + +/** + * Base64 provides Base64 encoding/decoding of strings and streams. + */ +public class Base64 { + + // charset used for base64 encoded data (7-bit ASCII) + private static final String CHARSET = "US-ASCII"; + + // encoding table (the 64 valid base64 characters) + private static final char[] BASE64CHARS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + + // decoding table (used to lookup original 6-bit with base64 character + // as table index) + private static final byte[] DECODETABLE = new byte[128]; + + static { + // initialize decoding table + for (int i = 0; i < DECODETABLE.length; i++) { + DECODETABLE[i] = 0x7f; + } + // build decoding table + for (int i = 0; i < BASE64CHARS.length; i++) { + DECODETABLE[BASE64CHARS[i]] = (byte) i; + } + } + + // pad character + private static final char BASE64PAD = '='; + + /** + * empty private constructor + */ + private Base64() { + } + + /** + * Base64-decodes or -encodes (see {@link #decodeOrEncode(String)} + * all the given arguments and prints the results on separate lines + * in standard output. + * + * @since Apache Jackrabbit 2.3 + * @param args command line arguments to be decoded or encoded + */ + public static void main(String[] args) { + for (String arg : args) { + System.out.println(decodeOrEncode(arg)); + } + } + + /** + * Base64-decodes or -encodes the given string, depending on whether + * or not it contains a "{base64}" prefix. If the string gets encoded, + * the "{base64}" prefix is added to it. + * + * @since Apache Jackrabbit 2.3 + * @param data string to be decoded or encoded + * @return decoded or encoded string + */ + public static String decodeOrEncode(String data) { + if (data.startsWith("{base64}")) { + return decode(data.substring("{base64}".length())); + } else { + return "{base64}" + encode(data); + } + } + + /** + * Decodes a base64-encoded string marked by a "{base64}" prefix. + * If the prefix is not found, then the string is returned as-is. + * If the given string is null, then null + * is returned. + * + * @since Apache Jackrabbit 2.3 + * @param data string to be decoded, can be null + * @return the given string, possibly decoded + */ + public static String decodeIfEncoded(String data) { + if (data != null && data.startsWith("{base64}")) { + return decode(data.substring("{base64}".length())); + } else { + return data; + } + } + + /** + * Calculates the size (i.e. number of bytes) of the base64 encoded output + * given the length (i.e. number of bytes) of the data to be encoded. + * + * @param dataLength length (i.e. number of bytes) of the data to be encoded + * @return size (i.e. number of bytes) of the base64 encoded output + */ + public static long calcEncodedLength(long dataLength) { + long encLen = dataLength * 4 / 3; + encLen += (encLen + 4) % 4; + return encLen; + } + + /** + * Pessimistically guesses the size (i.e. number of bytes) of the decoded + * output given the length (i.e. number of bytes) of the base64 encoded + * data. + * + * @param encLength length (i.e. number of bytes) of the base64 encoded data + * @return size (i.e. number of bytes) of the decoded output + */ + public static long guessDecodedLength(long encLength) { + long decLen = encLength * 3 / 4; + return decLen + 3; + } + + /** + * Outputs base64 representation of the specified stream data to a + * Writer. + * + * @param in stream data to be encoded + * @param writer writer to output the encoded data + * @throws java.io.IOException if an i/o error occurs + */ + public static void encode(InputStream in, Writer writer) + throws IOException { + // encode stream data in chunks; + // chunksize must be a multiple of 3 in order + // to avoid padding within output + byte[] buffer = new byte[9 * 1024]; + int read; + while ((read = in.read(buffer)) > 0) { + encode(buffer, 0, read, writer); + } + } + + /** + * Outputs base64 representation of the specified stream data to an + * OutputStream. + * + * @param in stream data to be encoded + * @param out stream where the encoded data should be written to + * @throws java.io.IOException if an i/o error occurs + */ + public static void encode(InputStream in, OutputStream out) + throws IOException { + Writer writer = new BufferedWriter(new OutputStreamWriter(out, CHARSET)); + try { + encode(in, writer); + } finally { + try { + writer.flush(); + } catch (IOException ignore) { + } + } + } + + /** + * Outputs base64 representation of the specified data to a + * Writer. + * + * @param data data to be encoded + * @param off offset within data at which to start encoding + * @param len length of data to encode + * @param writer writer to output the encoded data + * @throws java.io.IOException if an i/o error occurs + */ + public static void encode(byte[] data, int off, int len, Writer writer) + throws IOException { + if (len == 0) { + return; + } + if (len < 0 || off >= data.length + || len + off > data.length) { + throw new IllegalArgumentException(); + } + char[] enc = new char[4]; + while (len >= 3) { + int i = ((data[off] & 0xff) << 16) + + ((data[off + 1] & 0xff) << 8) + + (data[off + 2] & 0xff); + enc[0] = BASE64CHARS[i >> 18]; + enc[1] = BASE64CHARS[(i >> 12) & 0x3f]; + enc[2] = BASE64CHARS[(i >> 6) & 0x3f]; + enc[3] = BASE64CHARS[i & 0x3f]; + writer.write(enc, 0, 4); + off += 3; + len -= 3; + } + // add padding if necessary + if (len == 1) { + int i = data[off] & 0xff; + enc[0] = BASE64CHARS[i >> 2]; + enc[1] = BASE64CHARS[(i << 4) & 0x3f]; + enc[2] = BASE64PAD; + enc[3] = BASE64PAD; + writer.write(enc, 0, 4); + } else if (len == 2) { + int i = ((data[off] & 0xff) << 8) + (data[off + 1] & 0xff); + enc[0] = BASE64CHARS[i >> 10]; + enc[1] = BASE64CHARS[(i >> 4) & 0x3f]; + enc[2] = BASE64CHARS[(i << 2) & 0x3f]; + enc[3] = BASE64PAD; + writer.write(enc, 0, 4); + } + } + + /** + * Returns the base64 representation of UTF-8 encoded string. + * + * @since Apache Jackrabbit 2.3 + * @param data the string to be encoded + * @return base64-encoding of the string + */ + public static String encode(String data) { + try { + StringWriter buffer = new StringWriter(); + byte[] b = data.getBytes(StandardCharsets.UTF_8); + encode(b, 0, b.length, buffer); + return buffer.toString(); + } catch (IOException e) { // should never happen + throw new RuntimeException( + "Unable to encode base64 data: " + data, e); + } + } + + /** + * Decodes a base64-encoded string using the UTF-8 character encoding. + * The given string is returned as-is if it doesn't contain a valid + * base64 encoding. + * + * @since Apache Jackrabbit 2.3 + * @param data the base64-encoded data to be decoded + * @return decoded string + */ + public static String decode(String data) { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + decode(data, buffer); + return new String(buffer.toByteArray(), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return data; + } catch (IOException e) { // should never happen + throw new RuntimeException( + "Unable to decode base64 data: " + data, e); + } + } + + /** + * Decode base64 encoded data. + * + * @param reader reader for the base64 encoded data to be decoded + * @param out stream where the decoded data should be written to + * @throws java.io.IOException if an i/o error occurs + */ + public static void decode(Reader reader, OutputStream out) + throws IOException { + char[] chunk = new char[8192]; + int read; + while ((read = reader.read(chunk)) > -1) { + decode(chunk, 0, read, out); + } + } + + /** + * Decode base64 encoded data. The data read from the inputstream is + * assumed to be of charset "US-ASCII". + * + * @param in inputstream of the base64 encoded data to be decoded + * @param out stream where the decoded data should be written to + * @throws java.io.IOException if an i/o error occurs + */ + public static void decode(InputStream in, OutputStream out) + throws IOException { + decode(new InputStreamReader(in, CHARSET), out); + } + + /** + * Decode base64 encoded data. + * + * @param data the base64 encoded data to be decoded + * @param out stream where the decoded data should be written to + * @throws java.io.IOException if an i/o error occurs + */ + public static void decode(String data, OutputStream out) + throws IOException { + char[] chars = data.toCharArray(); + decode(chars, 0, chars.length, out); + } + + /** + * Decode base64 encoded data. + * + * @param chars the base64 encoded data to be decoded + * @param out stream where the decoded data should be written to + * @throws java.io.IOException if an i/o error occurs + */ + public static void decode(char[] chars, OutputStream out) + throws IOException { + decode(chars, 0, chars.length, out); + } + + /** + * Decode base64 encoded data. + * + * @param chars the base64 encoded data to be decoded + * @param off offset within data at which to start decoding + * @param len length of data to decode + * @param out stream where the decoded data should be written to + * @throws java.io.IOException if an i/o error occurs + */ + public static void decode(char[] chars, int off, int len, OutputStream out) + throws IOException { + if (len == 0) { + return; + } + if (len < 0 || off >= chars.length + || len + off > chars.length) { + throw new IllegalArgumentException(); + } + char[] chunk = new char[4]; + byte[] dec = new byte[3]; + int posChunk = 0; + // decode in chunks of 4 characters + for (int i = off; i < (off + len); i++) { + char c = chars[i]; + if (c < DECODETABLE.length && DECODETABLE[c] != 0x7f + || c == BASE64PAD) { + chunk[posChunk++] = c; + if (posChunk == chunk.length) { + int b0 = DECODETABLE[chunk[0]]; + int b1 = DECODETABLE[chunk[1]]; + int b2 = DECODETABLE[chunk[2]]; + int b3 = DECODETABLE[chunk[3]]; + if (chunk[3] == BASE64PAD && chunk[2] == BASE64PAD) { + dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); + out.write(dec, 0, 1); + } else if (chunk[3] == BASE64PAD) { + dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); + dec[1] = (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf); + out.write(dec, 0, 2); + } else { + dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); + dec[1] = (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf); + dec[2] = (byte) (b2 << 6 & 0xc0 | b3 & 0x3f); + out.write(dec, 0, 3); + } + posChunk = 0; + } + } else if (!Character.isWhitespace(c)) { + throw new IllegalArgumentException("specified data is not base64 encoded"); + } + } + } +} diff --git a/src/java/org/apache/jackrabbit/core/util/ChildrenCollector.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ChildrenCollector.java similarity index 82% rename from src/java/org/apache/jackrabbit/core/util/ChildrenCollector.java rename to jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ChildrenCollector.java index fe2965f883d..3a84e8cd806 100644 --- a/src/java/org/apache/jackrabbit/core/util/ChildrenCollector.java +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ChildrenCollector.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.core.util; +package org.apache.jackrabbit.util; import javax.jcr.Node; import javax.jcr.Property; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ChildrenCollectorFilter.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ChildrenCollectorFilter.java new file mode 100644 index 00000000000..e6c5e8dccf5 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ChildrenCollectorFilter.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.util.TraversingItemVisitor; + +import org.apache.jackrabbit.commons.ItemNameMatcher; +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +/** + * ChildrenCollectorFilter is a utility class + * which can be used to 'collect' child items of a + * node whose names match a certain pattern. It implements the + * ItemVisitor interface. + */ +public class ChildrenCollectorFilter extends TraversingItemVisitor.Default { + static final char WILDCARD_CHAR = '*'; + static final String OR = "|"; + + private final Collection children; + private final boolean collectNodes; + private final boolean collectProperties; + // namePattern and nameGlobs fields are used mutually exclusive + private final String namePattern; + private final String[] nameGlobs; + + /** + * Constructs a ChildrenCollectorFilter + * + * @param namePattern the pattern which should be applied to the names + * of the children + * @param children where the matching children should be added + * @param collectNodes true, if child nodes should be collected; otherwise false + * @param collectProperties true, if child properties should be collected; otherwise false + * @param maxLevel number of hierarchy levels to traverse + * (e.g. 1 for direct children only, 2 for children and their children, and so on) + */ + public ChildrenCollectorFilter( + String namePattern, Collection children, + boolean collectNodes, boolean collectProperties, int maxLevel) { + super(false, maxLevel); + this.namePattern = namePattern; + this.nameGlobs = null; + this.children = children; + this.collectNodes = collectNodes; + this.collectProperties = collectProperties; + } + + /** + * Constructs a ChildrenCollectorFilter + * + * @param nameGlobs an array of globbing strings which should be + * applied to the names of the children + * @param children where the matching children should be added + * @param collectNodes true, if child nodes should be collected; otherwise false + * @param collectProperties true, if child properties should be collected; otherwise false + * @param maxLevel number of hierarchy levels to traverse + * (e.g. 1 for direct children only, 2 for children and their children, and so on) + */ + public ChildrenCollectorFilter( + String[] nameGlobs, Collection children, + boolean collectNodes, boolean collectProperties, int maxLevel) { + super(false, maxLevel); + this.nameGlobs = nameGlobs; + this.namePattern = null; + this.children = children; + this.collectNodes = collectNodes; + this.collectProperties = collectProperties; + } + + public static NodeIterator collectChildNodes( + Node node, String namePattern) throws RepositoryException { + Collection nodes = new ArrayList(); + node.accept(new ChildrenCollectorFilter( + namePattern, nodes, true, false, 1)); + return new NodeIteratorAdapter(nodes); + } + + public static NodeIterator collectChildNodes( + Node node, String[] nameGlobs) throws RepositoryException { + Collection nodes = new ArrayList(); + node.accept(new ChildrenCollectorFilter( + nameGlobs, nodes, true, false, 1)); + return new NodeIteratorAdapter(nodes); + } + + public static PropertyIterator collectProperties( + Node node, String namePattern) throws RepositoryException { + Collection properties = Collections.emptySet(); + PropertyIterator pit = node.getProperties(); + while (pit.hasNext()) { + Property p = pit.nextProperty(); + if (matches(p.getName(), namePattern)) { + properties = addToCollection(properties, p); + } + } + return new PropertyIteratorAdapter(properties); + } + + public static PropertyIterator collectProperties(Node node, String[] nameGlobs) throws RepositoryException { + Collection properties = Collections.emptySet(); + PropertyIterator pit = node.getProperties(); + while (pit.hasNext()) { + Property p = pit.nextProperty(); + if (matches(p.getName(), nameGlobs)) { + properties = addToCollection(properties, p); + } + } + return new PropertyIteratorAdapter(properties); + } + + /** + * {@inheritDoc} + */ + @Override + protected void entering(Node node, int level) + throws RepositoryException { + if (level > 0 && collectNodes) { + if (namePattern != null) { + if (matches(node.getName(), namePattern)) { + children.add(node); + } + } else { + if (matches(node.getName(), nameGlobs)) { + children.add(node); + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void entering(Property property, int level) + throws RepositoryException { + if (level > 0 && collectProperties) { + if (namePattern != null) { + if (matches(property.getName(), namePattern)) { + children.add(property); + } + } else { + if (matches(property.getName(), nameGlobs)) { + children.add(property); + } + } + } + } + + /** + * Same as {@link ItemNameMatcher#matches(String, String)}. + * + * @see javax.jcr.Node#getNodes(String) + */ + public static boolean matches(String name, String pattern) { + return ItemNameMatcher.matches(name, pattern); + } + + /** + * Same as {@link ItemNameMatcher#matches(String, String)}. + * + * @see javax.jcr.Node#getNodes(String) + */ + public static boolean matches(String name, String[] nameGlobs) { + return ItemNameMatcher.matches(name, nameGlobs); + } + + private static Collection addToCollection(Collection c, Item p) { + Collection nc = c; + if (c.isEmpty()) { + nc = Collections.singleton(p); + } else if (c.size() == 1) { + nc = new ArrayList(c); + nc.add(p); + } else { + nc.add(p); + } + + return nc; + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ISO8601.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ISO8601.java new file mode 100644 index 00000000000..6a12356d314 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ISO8601.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.util; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +/** + * The ISO8601 utility class provides helper methods + * to deal with date/time formatting using a specific ISO8601-compliant + * format (see ISO 8601). + *

    + * The currently supported format is: + *

    + *   ±YYYY-MM-DDThh:mm:ss.SSSTZD
    + * 
    + * where: + *
    + *   ±YYYY = four-digit year with optional sign where values <= 0 are
    + *           denoting years BCE and values > 0 are denoting years CE,
    + *           e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE,
    + *           0001 denotes the year 1 CE, and so on...
    + *   MM    = two-digit month (01=January, etc.)
    + *   DD    = two-digit day of month (01 through 31)
    + *   hh    = two digits of hour (00 through 23) (am/pm NOT allowed)
    + *   mm    = two digits of minute (00 through 59)
    + *   ss    = two digits of second (00 through 59)
    + *   SSS   = three digits of milliseconds (000 through 999)
    + *   TZD   = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC
    + *           in the form of +hh:mm or -hh:mm
    + * 
    + */ +public final class ISO8601 { + + /** + * Flyweight instances of known time zones. + */ + private static final Map TZS = + new HashMap(); + + static { + TimeZone gmt = TimeZone.getTimeZone("GMT"); + TZS.put("Z", gmt); + TZS.put("+00:00", gmt); + TZS.put("-00:00", gmt); + + // http://en.wikipedia.org/wiki/List_of_UTC_time_offsets + String[] tzs = { + "-12:00", "-11:00", "-10:00", "-09:30", "-09:00", "-08:00", + "-07:00", "-06:00", "-05:00", "-04:30", "-04:00", "-03:30", + "-03:00", "-02:00", "-01:00", "+01:00", "+02:00", "+03:00", + "+03:30", "+04:00", "+04:30", "+05:00", "+05:30", "+05:45", + "+06:00", "+06:30", "+07:00", "+08:00", "+08:45", "+09:00", + "+09:30", "+10:00", "+10:30", "+11:00", "+11:30", "+12:00", + "+12:45", "+13:00", "+14:00" }; + for (String tz : tzs) { + TZS.put(tz, TimeZone.getTimeZone("GMT" + tz)); + } + } + + /** + * Parses an ISO8601-compliant date/time string. + * + * @param text the date/time string to be parsed + * @return a Calendar, or null if the input could + * not be parsed + * @throws IllegalArgumentException if a null argument is passed + */ + public static Calendar parse(String text) { + if (text == null) { + throw new IllegalArgumentException("argument can not be null"); + } + + // check optional leading sign + char sign; + int start; + if (text.startsWith("-")) { + sign = '-'; + start = 1; + } else if (text.startsWith("+")) { + sign = '+'; + start = 1; + } else { + sign = '+'; // no sign specified, implied '+' + start = 0; + } + + /** + * the expected format of the remainder of the string is: + * YYYY-MM-DDThh:mm:ss.SSSTZD + * + * note that we cannot use java.text.SimpleDateFormat for + * parsing because it can't handle years <= 0 and TZD's + */ + + int year, month, day, hour, min, sec, ms; + TimeZone tz; + try { + // year (YYYY) + year = Integer.parseInt(text.substring(start, start + 4)); + start += 4; + // delimiter '-' + if (text.charAt(start) != '-') { + return null; + } + start++; + // month (MM) + month = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter '-' + if (text.charAt(start) != '-') { + return null; + } + start++; + // day (DD) + day = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter 'T' + if (text.charAt(start) != 'T') { + return null; + } + start++; + // hour (hh) + hour = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (text.charAt(start) != ':') { + return null; + } + start++; + // minute (mm) + min = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (text.charAt(start) != ':') { + return null; + } + start++; + // second (ss) + sec = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter '.' + if (text.charAt(start) != '.') { + return null; + } + start++; + // millisecond (SSS) + ms = Integer.parseInt(text.substring(start, start + 3)); + start += 3; + // time zone designator (Z or +00:00 or -00:00) + String tzid = text.substring(start); + tz = TZS.get(tzid); + if (tz == null) { + // offset to UTC specified in the format +00:00/-00:00 + tzid = "GMT" + tzid; + tz = TimeZone.getTimeZone(tzid); + // verify id of returned time zone (getTimeZone defaults to "GMT") + if (!tz.getID().equals(tzid)) { + // invalid time zone + return null; + } + } + } catch (IndexOutOfBoundsException e) { + return null; + } catch (NumberFormatException e) { + return null; + } + + // initialize Calendar object + Calendar cal = Calendar.getInstance(tz); + cal.setLenient(false); + // year and era + if (sign == '-' || year == 0) { + // not CE, need to set era (BCE) and adjust year + cal.set(Calendar.YEAR, year + 1); + cal.set(Calendar.ERA, GregorianCalendar.BC); + } else { + cal.set(Calendar.YEAR, year); + cal.set(Calendar.ERA, GregorianCalendar.AD); + } + // month (0-based!) + cal.set(Calendar.MONTH, month - 1); + // day of month + cal.set(Calendar.DAY_OF_MONTH, day); + // hour + cal.set(Calendar.HOUR_OF_DAY, hour); + // minute + cal.set(Calendar.MINUTE, min); + // second + cal.set(Calendar.SECOND, sec); + // millisecond + cal.set(Calendar.MILLISECOND, ms); + + try { + /** + * the following call will trigger an IllegalArgumentException + * if any of the set values are illegal or out of range + */ + cal.getTime(); + /** + * in addition check the validity of the year + */ + getYear(cal); + } catch (IllegalArgumentException e) { + return null; + } + + return cal; + } + + /** + * Formats a Calendar value into an ISO8601-compliant + * date/time string. + * + * @param cal the time value to be formatted into a date/time string. + * @return the formatted date/time string. + * @throws IllegalArgumentException if a null argument is passed + * or the calendar cannot be represented as defined by ISO 8601 (i.e. year + * with more than four digits). + */ + public static String format(Calendar cal) throws IllegalArgumentException { + if (cal == null) { + throw new IllegalArgumentException("argument can not be null"); + } + + /** + * the format of the date/time string is: + * YYYY-MM-DDThh:mm:ss.SSSTZD + * + * note that we cannot use java.text.SimpleDateFormat for + * formatting because it can't handle years <= 0 and TZD's + */ + StringBuilder buf = new StringBuilder(); + // year ([-]YYYY) + appendZeroPaddedInt(buf, getYear(cal), 4); + buf.append('-'); + // month (MM) + appendZeroPaddedInt(buf, cal.get(Calendar.MONTH) + 1, 2); + buf.append('-'); + // day (DD) + appendZeroPaddedInt(buf, cal.get(Calendar.DAY_OF_MONTH), 2); + buf.append('T'); + // hour (hh) + appendZeroPaddedInt(buf, cal.get(Calendar.HOUR_OF_DAY), 2); + buf.append(':'); + // minute (mm) + appendZeroPaddedInt(buf, cal.get(Calendar.MINUTE), 2); + buf.append(':'); + // second (ss) + appendZeroPaddedInt(buf, cal.get(Calendar.SECOND), 2); + buf.append('.'); + // millisecond (SSS) + appendZeroPaddedInt(buf, cal.get(Calendar.MILLISECOND), 3); + // time zone designator (Z or +00:00 or -00:00) + TimeZone tz = cal.getTimeZone(); + // determine offset of timezone from UTC (incl. daylight saving) + int offset = tz.getOffset(cal.getTimeInMillis()); + if (offset != 0) { + int hours = Math.abs((offset / (60 * 1000)) / 60); + int minutes = Math.abs((offset / (60 * 1000)) % 60); + buf.append(offset < 0 ? '-' : '+'); + appendZeroPaddedInt(buf, hours, 2); + buf.append(':'); + appendZeroPaddedInt(buf, minutes, 2); + } else { + buf.append('Z'); + } + return buf.toString(); + } + + /** + * Returns the astronomical year of the given calendar. + * + * @param cal a calendar instance. + * @return the astronomical year. + * @throws IllegalArgumentException if calendar cannot be represented as + * defined by ISO 8601 (i.e. year with more + * than four digits). + */ + public static int getYear(Calendar cal) throws IllegalArgumentException { + // determine era and adjust year if necessary + int year = cal.get(Calendar.YEAR); + if (cal.isSet(Calendar.ERA) + && cal.get(Calendar.ERA) == GregorianCalendar.BC) { + /** + * calculate year using astronomical system: + * year n BCE => astronomical year -n + 1 + */ + year = 0 - year + 1; + } + + if (year > 9999 || year < -9999) { + throw new IllegalArgumentException("Calendar has more than four " + + "year digits, cannot be formatted as ISO8601: " + year); + } + return year; + } + + /** + * Appends a zero-padded number to the given string buffer. + *

    + * This is an internal helper method which doesn't perform any + * validation on the given arguments. + * + * @param buf String buffer to append to + * @param n number to append + * @param precision number of digits to append + */ + private static void appendZeroPaddedInt(StringBuilder buf, int n, int precision) { + if (n < 0) { + buf.append('-'); + n = -n; + } + + for (int exp = precision - 1; exp > 0; exp--) { + if (n < Math.pow(10, exp)) { + buf.append('0'); + } else { + break; + } + } + buf.append(n); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ISO9075.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ISO9075.java new file mode 100644 index 00000000000..f836ea109e7 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/ISO9075.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Implements the encode and decode routines as specified for XML name to SQL + * identifier conversion in ISO 9075-14:2003.
    + * If a character c is not valid at a certain position in an XML 1.0 + * NCName it is encoded in the form: '_x' + hexValueOf(c) + '_'. + */ +public class ISO9075 { + + /** Hidden constructor. */ + private ISO9075() { } + + /** Pattern on an encoded character */ + private static final Pattern ENCODE_PATTERN = Pattern.compile("_x\\p{XDigit}{4}_"); + + /** Padding characters */ + private static final char[] PADDING = new char[] {'0', '0', '0'}; + + /** All the possible hex digits */ + private static final String HEX_DIGITS = "0123456789abcdefABCDEF"; + + /** + * Encodes name as specified in ISO 9075. + * @param name the String to encode. + * @return the encoded String or name if it does + * not need encoding. + */ + public static String encode(String name) { + // quick check for root node name + if (name.length() == 0) { + return name; + } + if (XMLChar.isValidName(name) && name.indexOf("_x") < 0) { + // already valid + return name; + } else { + // encode + StringBuffer encoded = new StringBuffer(); + for (int i = 0; i < name.length(); i++) { + if (i == 0) { + // first character of name + if (XMLChar.isNameStart(name.charAt(i))) { + if (needsEscaping(name, i)) { + // '_x' must be encoded + encode('_', encoded); + } else { + encoded.append(name.charAt(i)); + } + } else { + // not valid as first character -> encode + encode(name.charAt(i), encoded); + } + } else if (!XMLChar.isName(name.charAt(i))) { + encode(name.charAt(i), encoded); + } else { + if (needsEscaping(name, i)) { + // '_x' must be encoded + encode('_', encoded); + } else { + encoded.append(name.charAt(i)); + } + } + } + return encoded.toString(); + } + } + + /** + * Encodes path as specified in ISO 9075. Please note that + * the character '[' is not encoded but rather interpreted as + * the start of an index in a path segment. + * + * @param path the String to encode. + * @return the encoded String. + */ + public static String encodePath(String path) { + String[] names = Text.explode(path, '/', true); + StringBuffer encoded = new StringBuffer(path.length()); + for (int i = 0; i < names.length; i++) { + // detect index + String index = null; + int idx = names[i].indexOf('['); + if (idx != -1) { + index = names[i].substring(idx); + names[i] = names[i].substring(0, idx); + } + encoded.append(encode(names[i])); + if (index != null) { + encoded.append(index); + } + if (i < names.length - 1) { + encoded.append('/'); + } + } + return encoded.toString(); + } + + /** + * Decodes the name. + * @param name the String to decode. + * @return the decoded String. + */ + public static String decode(String name) { + // quick check + if (name.indexOf("_x") < 0) { + // not encoded + return name; + } + StringBuffer decoded = new StringBuffer(); + Matcher m = ENCODE_PATTERN.matcher(name); + while (m.find()) { + char ch = (char) Integer.parseInt(m.group().substring(2, 6), 16); + if (ch == '$' || ch == '\\') { + m.appendReplacement(decoded, "\\" + ch); + } else { + m.appendReplacement(decoded, Character.toString(ch)); + } + } + m.appendTail(decoded); + return decoded.toString(); + } + + //-------------------------< internal >------------------------------------- + + /** + * Encodes the character c as a String in the following form: + * "_x" + hex value of c + "_". Where the hex value has + * four digits if the character with possibly leading zeros. + *

    + * Example: ' ' (the space character) is encoded to: _x0020_ + * @param c the character to encode + * @param b the encoded character is appended to StringBuffer + * b. + */ + private static void encode(char c, StringBuffer b) { + b.append("_x"); + String hex = Integer.toHexString(c); + b.append(PADDING, 0, 4 - hex.length()); + b.append(hex); + b.append("_"); + } + + /** + * Returns true if name.charAt(location) is the underscore + * character and the following character sequence is 'xHHHH_' where H + * is a hex digit. + * @param name the name to check. + * @param location the location to look at. + * @throws ArrayIndexOutOfBoundsException if location > name.length() + */ + private static boolean needsEscaping(String name, int location) + throws ArrayIndexOutOfBoundsException { + if (name.charAt(location) == '_' && name.length() >= location + 6) { + return name.charAt(location + 1) == 'x' + && HEX_DIGITS.indexOf(name.charAt(location + 2)) != -1 + && HEX_DIGITS.indexOf(name.charAt(location + 3)) != -1 + && HEX_DIGITS.indexOf(name.charAt(location + 4)) != -1 + && HEX_DIGITS.indexOf(name.charAt(location + 5)) != -1; + } else { + return false; + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LazyFileInputStream.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LazyFileInputStream.java new file mode 100644 index 00000000000..fc555bf5f2e --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LazyFileInputStream.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * This Class implements an InputStream that provides the same functionality + * as a FileInputStream but opens the file by the first file access. + */ +public class LazyFileInputStream extends InputStream { + + /** + * the underlying input stream + */ + private FileInputStream in; + + /** + * FileDescriptor to use + */ + private FileDescriptor fd; + + /** + * File to use + */ + private File file; + + /** + * Creates a new LazyFileInputStream for the given file. If the + * file is unreadably, a FileNotFoundException is thrown. + * + * @param file + * @throws java.io.FileNotFoundException + */ + public LazyFileInputStream(File file) + throws FileNotFoundException { + // check if we can read from the file + if (!file.canRead()) { + throw new FileNotFoundException(file.getPath()); + } + this.file = file; + } + + /** + * Creates a new LazyFileInputStream for the given file + * desciptor. + * + * @param fdObj + */ + public LazyFileInputStream(FileDescriptor fdObj) { + this.fd = fdObj; + } + + /** + * Creates a new LazyFileInputStream for the given file. If the + * file is unreadably, a FileNotFoundException is thrown. + * + * @param name + * @throws java.io.FileNotFoundException + */ + public LazyFileInputStream(String name) throws FileNotFoundException { + this(new File(name)); + } + + /** + * Opens the underlying file input stream in neccessairy. + * @throws java.io.IOException + */ + public void open() throws IOException { + if (in == null) { + if (file != null) { + in = new FileInputStream(file); + } else if (fd != null) { + in = new FileInputStream(fd); + } else { + throw new IOException("Stream already closed."); + } + } + } + + /** + * {@inheritDoc} + */ + public int read() throws IOException { + open(); + return in.read(); + } + + /** + * {@inheritDoc} + */ + public int available() throws IOException { + open(); + return in.available(); + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + if (in != null) { + in.close(); + } + in = null; + file = null; + fd = null; + } + + /** + * {@inheritDoc} + */ + public synchronized void reset() throws IOException { + open(); + in.reset(); + } + + /** + * {@inheritDoc} + */ + public boolean markSupported() { + try { + open(); + return in.markSupported(); + } catch (IOException e) { + throw new IllegalStateException(e.toString()); + } + } + + /** + * {@inheritDoc} + */ + public synchronized void mark(int readlimit) { + try { + open(); + in.mark(readlimit); + } catch (IOException e) { + throw new IllegalStateException(e.toString()); + } + } + + /** + * {@inheritDoc} + */ + public long skip(long n) throws IOException { + open(); + return in.skip(n); + } + + /** + * {@inheritDoc} + */ + public int read(byte[] b) throws IOException { + open(); + return in.read(b); + } + + /** + * {@inheritDoc} + */ + public int read(byte[] b, int off, int len) throws IOException { + open(); + return in.read(b, off, len); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Locked.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Locked.java new file mode 100644 index 00000000000..c7dabb83856 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Locked.java @@ -0,0 +1,318 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.lock.LockManager; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; + +/** + * Locked is a utility to synchronize modifications on a lockable + * node. The modification is applied while the lock on the node is held, thus + * ensuring that the modification will never fail with an {@link + * javax.jcr.InvalidItemStateException}. This utility can be used with any + * JCR Repository, not just Jackrabbit. + *

    + * The following example shows how this utility can be used to implement + * a persistent counter: + *

    + * Node counter = ...;
    + * long nextValue = ((Long) new Locked() {
    + *     protected Object run(Node counter) throws RepositoryException {
    + *         Property seqProp = counter.getProperty("value");
    + *         long value = seqProp.getLong();
    + *         seqProp.setValue(++value);
    + *         seqProp.save();
    + *         return new Long(value);
    + *     }
    + * }.with(counter, false)).longValue();
    + * 
    + * If you specify a timeout you need to check the return value + * whether the run method could be executed within the timeout + * period: + *
    + * Node counter = ...;
    + * Object ret = new Locked() {
    + *     protected Object run(Node counter) throws RepositoryException {
    + *         Property seqProp = counter.getProperty("value");
    + *         long value = seqProp.getLong();
    + *         seqProp.setValue(++value);
    + *         seqProp.save();
    + *         return new Long(value);
    + *     }
    + * }.with(counter, false);
    + * if (ret == Locked.TIMED_OUT) {
    + *     // do whatever you think is appropriate in this case
    + * } else {
    + *     // get the value
    + *     long nextValue = ((Long) ret).longValue();
    + * }
    + * 
    + */ +public abstract class Locked { + + /** The mixin namespace */ + private static final String MIX = "http://www.jcp.org/jcr/mix/1.0"; + + /** + * Object returned when timeout is reached without being able to call + * {@link #run} while holding the lock. + */ + public static final Object TIMED_OUT = new Object(); + + /** + * Executes {@link #run} while the lock on lockable is held. + * This method will block until {@link #run} is executed while holding the + * lock on node lockable. + * + * @param lockable a lockable node. + * @param isDeep true if lockable will be locked + * deep. + * @return the object returned by {@link #run}. + * @throws IllegalArgumentException if lockable is not + * mix:lockable. + * @throws RepositoryException if {@link #run} throws an exception. + * @throws InterruptedException if this thread is interrupted while waiting + * for the lock on node lockable. + */ + public Object with(Node lockable, boolean isDeep) + throws RepositoryException, InterruptedException { + return with(lockable, isDeep, true); + } + + /** + * Executes {@link #run} while the lock on lockable is held. + * This method will block until {@link #run} is executed while holding the + * lock on node lockable. + * + * @param lockable a lockable node. + * @param isDeep true if lockable will be locked + * deep. + * @param isSessionScoped true if the lock is session scoped. + * @return the object returned by {@link #run}. + * @throws IllegalArgumentException if lockable is not + * mix:lockable. + * @throws RepositoryException if {@link #run} throws an exception. + * @throws InterruptedException if this thread is interrupted while waiting + * for the lock on node lockable. + */ + public Object with(Node lockable, boolean isDeep, boolean isSessionScoped) + throws RepositoryException, InterruptedException { + return with(lockable, isDeep, Long.MAX_VALUE, isSessionScoped); + } + + /** + * Executes the method {@link #run} within the scope of a lock held on + * lockable. + * + * @param lockable the node where the lock is obtained from. + * @param isDeep true if lockable will be locked + * deep. + * @param timeout time in milliseconds to wait at most to acquire the lock. + * @return the object returned by {@link #run} or {@link #TIMED_OUT} if the + * lock on lockable could not be acquired within the + * specified timeout. + * @throws IllegalArgumentException if timeout is negative or + * lockable is not + * mix:lockable. + * @throws RepositoryException if {@link #run} throws an exception. + * @throws UnsupportedRepositoryOperationException + * if this repository does not support + * locking. + * @throws InterruptedException if this thread is interrupted while + * waiting for the lock on node + * lockable. + */ + public Object with(Node lockable, boolean isDeep, long timeout) + throws UnsupportedRepositoryOperationException, RepositoryException, InterruptedException { + return with(lockable, isDeep, timeout, true); + } + + /** + * Executes the method {@link #run} within the scope of a lock held on + * lockable. + * + * @param lockable the node where the lock is obtained from. + * @param isDeep true if lockable will be locked + * deep. + * @param timeout time in milliseconds to wait at most to acquire the lock. + * @param isSessionScoped true if the lock is session scoped. + * @return the object returned by {@link #run} or {@link #TIMED_OUT} if the + * lock on lockable could not be acquired within the + * specified timeout. + * @throws IllegalArgumentException if timeout is negative or + * lockable is not + * mix:lockable. + * @throws RepositoryException if {@link #run} throws an exception. + * @throws UnsupportedRepositoryOperationException + * if this repository does not support + * locking. + * @throws InterruptedException if this thread is interrupted while + * waiting for the lock on node + * lockable. + */ + public Object with(Node lockable, boolean isDeep, long timeout, boolean isSessionScoped) + throws UnsupportedRepositoryOperationException, RepositoryException, InterruptedException { + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be >= 0"); + } + + Session session = lockable.getSession(); + + EventListener listener = null; + try { + // check whether the lockable can be locked at all + String mix = session.getNamespacePrefix(MIX); + if (!lockable.isNodeType(mix + ":lockable")) { + throw new IllegalArgumentException("Node is not lockable"); + } + + Lock lock = tryLock(lockable, isDeep, timeout, isSessionScoped); + if (lock != null) { + return runAndUnlock(lock); + } + + if (timeout == 0) { + return TIMED_OUT; + } + + long timelimit; + if (timeout == Long.MAX_VALUE) { + timelimit = Long.MAX_VALUE; + } else { + timelimit = System.currentTimeMillis() + timeout; + } + + // node is locked by other session -> register event listener if possible + if (isObservationSupported(session)) { + ObservationManager om = session.getWorkspace().getObservationManager(); + listener = new EventListener() { + public void onEvent(EventIterator events) { + synchronized (Locked.this) { + Locked.this.notify(); + } + } + }; + om.addEventListener(listener, Event.PROPERTY_REMOVED, + lockable.getPath(), false, null, null, true); + } + + // now keep trying to acquire the lock + // using 'this' as a monitor allows the event listener to notify + // the current thread when the lockable node is possibly unlocked + for (; ;) { + synchronized (this) { + lock = tryLock(lockable, isDeep, timeout, isSessionScoped); + if (lock != null) { + return runAndUnlock(lock); + } else { + // check timeout + if (System.currentTimeMillis() > timelimit) { + return TIMED_OUT; + } + if (listener != null) { + // event listener *should* wake us up, however + // there is a chance that removal of the lockOwner + // property is notified before the node is acutally + // unlocked. therefore we use a safety net to wait + // at most 1000 millis. + this.wait(Math.min(1000, timeout)); + } else { + // repository does not support observation + // wait at most 50 millis then retry + this.wait(Math.min(50, timeout)); + } + } + } + } + } finally { + if (listener != null) { + session.getWorkspace().getObservationManager().removeEventListener(listener); + } + } + } + + /** + * This method is executed while holding the lock. + * @param node The Node on which the lock is placed. + * @return an object which is then returned by {@link #with with()}. + * @throws RepositoryException if an error occurs. + */ + protected abstract Object run(Node node) throws RepositoryException; + + /** + * Executes {@link #run} and unlocks the lockable node in any case, even + * when an exception is thrown. + * + * @param lock The Lock to unlock in any case before returning. + * + * @return the object returned by {@link #run}. + * @throws RepositoryException if an error occurs. + */ + private Object runAndUnlock(Lock lock) throws RepositoryException { + Node node = lock.getNode(); + try { + return run(node); + } finally { + node.getSession().getWorkspace().getLockManager().unlock(node.getPath()); + } + } + + /** + * Tries to acquire a session scoped lock on lockable. + * + * @param lockable the lockable node + * @param isDeep true if the lock should be deep + * @param timeout time in milliseconds to wait at most to acquire the lock. + * @param isSessionScoped true if the lock is session scoped. + * @return The Lock or null if the + * lockable cannot be locked. + * @throws UnsupportedRepositoryOperationException + * if this repository does not support locking. + * @throws RepositoryException if an error occurs + */ + private static Lock tryLock(Node lockable, boolean isDeep, long timeout, boolean isSessionScoped) + throws UnsupportedRepositoryOperationException, RepositoryException { + try { + LockManager lm = lockable.getSession().getWorkspace().getLockManager(); + return lm.lock(lockable.getPath(), isDeep, isSessionScoped, timeout, null); + } catch (LockException e) { + // locked by some other session + } + return null; + } + + /** + * Returns true if the repository supports observation. + * + * @param s a session of the repository. + * @return true if the repository supports observation. + */ + private static boolean isObservationSupported(Session s) { + return "true".equalsIgnoreCase(s.getRepository().getDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED)); + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LockedWrapper.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LockedWrapper.java new file mode 100644 index 00000000000..860c69ce19d --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/LockedWrapper.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; + +/** + * LockedWrapper is a wrapper class to {@link Locked} which adds + * generics support and wraps the Locked.TIMED_OUT object into a + * {@link LockException}. + */ +public abstract class LockedWrapper extends Locked { + + /* + * (non-Javadoc) + * + * @see org.apache.jackrabbit.util.Locked#with(javax.jcr.Node, boolean) + */ + @Override + @SuppressWarnings("unchecked") + public T with(Node lockable, boolean isDeep) throws RepositoryException, + InterruptedException { + return (T) super.with(lockable, isDeep); + } + + /* + * (non-Javadoc) + * + * @see org.apache.jackrabbit.util.Locked#with(javax.jcr.Node, boolean, + * boolean) + */ + @Override + @SuppressWarnings("unchecked") + public T with(Node lockable, boolean isDeep, boolean isSessionScoped) + throws RepositoryException, InterruptedException { + return (T) super.with(lockable, isDeep, isSessionScoped); + } + + /* + * (non-Javadoc) + * + * @see org.apache.jackrabbit.util.Locked#with(javax.jcr.Node, boolean, + * long) + */ + @Override + @SuppressWarnings("unchecked") + public T with(Node lockable, boolean isDeep, long timeout) + throws UnsupportedRepositoryOperationException, + RepositoryException, InterruptedException { + + Object r = super.with(lockable, isDeep, timeout); + if (r == Locked.TIMED_OUT) { + throw new LockException("Node locked."); + } + return (T) r; + } + + /* + * (non-Javadoc) + * + * @see org.apache.jackrabbit.util.Locked#with(javax.jcr.Node, boolean, + * long, boolean) + */ + @Override + @SuppressWarnings("unchecked") + public Object with(Node lockable, boolean isDeep, long timeout, + boolean isSessionScoped) + throws UnsupportedRepositoryOperationException, + RepositoryException, InterruptedException { + + Object r = super.with(lockable, isDeep, timeout, isSessionScoped); + if (r == Locked.TIMED_OUT) { + throw new LockException("Node locked."); + } + return (T) r; + } + + /** + * This method is executed while holding the lock. + * + * @param node + * The Node on which the lock is placed. + * @return an object which is then returned by {@link #with with()}. + * @throws RepositoryException + * if an error occurs. + */ + @Override + protected abstract T run(Node node) throws RepositoryException; + +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Text.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Text.java new file mode 100644 index 00000000000..0b860bc904f --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Text.java @@ -0,0 +1,815 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Properties; + +/** + * This Class provides some text related utilities + */ +public class Text { + + /** + * Hidden constructor. + */ + private Text() { + } + + /** + * used for the md5 + */ + public static final char[] hexTable = "0123456789abcdef".toCharArray(); + + /** + * Calculate an MD5 hash of the string given. + * + * @param data the data to encode + * @param enc the character encoding to use + * @return a hex encoded string of the md5 digested input + */ + public static String md5(String data, String enc) + throws UnsupportedEncodingException { + try { + return digest("MD5", data.getBytes(enc)); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("MD5 digest not available???"); + } + } + + /** + * Calculate an MD5 hash of the string given using 'utf-8' encoding. + * + * @param data the data to encode + * @return a hex encoded string of the md5 digested input + */ + public static String md5(String data) { + try { + return md5(data, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new InternalError("UTF8 digest not available???"); + } + } + + /** + * Digest the plain string using the given algorithm. + * + * @param algorithm The alogrithm for the digest. This algorithm must be + * supported by the MessageDigest class. + * @param data The plain text String to be digested. + * @param enc The character encoding to use + * @return The digested plain text String represented as Hex digits. + * @throws java.security.NoSuchAlgorithmException if the desired algorithm is not supported by + * the MessageDigest class. + * @throws java.io.UnsupportedEncodingException if the encoding is not supported + */ + public static String digest(String algorithm, String data, String enc) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + + return digest(algorithm, data.getBytes(enc)); + } + + /** + * Digest the plain string using the given algorithm. + * + * @param algorithm The algorithm for the digest. This algorithm must be + * supported by the MessageDigest class. + * @param data the data to digest with the given algorithm + * @return The digested plain text String represented as Hex digits. + * @throws java.security.NoSuchAlgorithmException if the desired algorithm is not supported by + * the MessageDigest class. + */ + public static String digest(String algorithm, byte[] data) + throws NoSuchAlgorithmException { + + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] digest = md.digest(data); + StringBuilder res = new StringBuilder(digest.length * 2); + for (byte b : digest) { + res.append(hexTable[(b >> 4) & 15]); + res.append(hexTable[b & 15]); + } + return res.toString(); + } + + /** + * returns an array of strings decomposed of the original string, split at + * every occurrence of 'ch'. if 2 'ch' follow each other with no intermediate + * characters, empty "" entries are avoided. + * + * @param str the string to decompose + * @param ch the character to use a split pattern + * @return an array of strings + */ + public static String[] explode(String str, int ch) { + return explode(str, ch, false); + } + + /** + * returns an array of strings decomposed of the original string, split at + * every occurrence of 'ch'. + * + * @param str the string to decompose + * @param ch the character to use a split pattern + * @param respectEmpty if true, empty elements are generated + * @return an array of strings + */ + public static String[] explode(String str, int ch, boolean respectEmpty) { + if (str == null || str.length() == 0) { + return new String[0]; + } + + ArrayList strings = new ArrayList(); + int pos; + int lastpos = 0; + + // add snipples + while ((pos = str.indexOf(ch, lastpos)) >= 0) { + if (pos - lastpos > 0 || respectEmpty) { + strings.add(str.substring(lastpos, pos)); + } + lastpos = pos + 1; + } + // add rest + if (lastpos < str.length()) { + strings.add(str.substring(lastpos)); + } else if (respectEmpty && lastpos == str.length()) { + strings.add(""); + } + + // return string array + return strings.toArray(new String[strings.size()]); + } + + /** + * Concatenates all strings in the string array using the specified delimiter. + * @param arr + * @param delim + * @return the concatenated string + */ + public static String implode(String[] arr, String delim) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + buf.append(delim); + } + buf.append(arr[i]); + } + return buf.toString(); + } + + /** + * Replaces all occurrences of oldString in text + * with newString. + * + * @param text + * @param oldString old substring to be replaced with newString + * @param newString new substring to replace occurrences of oldString + * @return a string + */ + public static String replace(String text, String oldString, String newString) { + if (text == null || oldString == null || newString == null) { + throw new IllegalArgumentException("null argument"); + } + int pos = text.indexOf(oldString); + if (pos == -1) { + return text; + } + int lastPos = 0; + StringBuilder sb = new StringBuilder(text.length()); + while (pos != -1) { + sb.append(text.substring(lastPos, pos)); + sb.append(newString); + lastPos = pos + oldString.length(); + pos = text.indexOf(oldString, lastPos); + } + if (lastPos < text.length()) { + sb.append(text.substring(lastPos)); + } + return sb.toString(); + } + + /** + * Replaces XML characters in the given string that might need escaping + * as XML text or attribute + * + * @param text text to be escaped + * @return a string + */ + public static String encodeIllegalXMLCharacters(String text) { + return encodeMarkupCharacters(text, false); + } + + /** + * Replaces HTML characters in the given string that might need escaping + * as HTML text or attribute + * + * @param text text to be escaped + * @return a string + */ + public static String encodeIllegalHTMLCharacters(String text) { + return encodeMarkupCharacters(text, true); + } + + private static String encodeMarkupCharacters(String text, boolean isHtml) { + if (text == null) { + throw new IllegalArgumentException("null argument"); + } + StringBuilder buf = null; + int length = text.length(); + int pos = 0; + for (int i = 0; i < length; i++) { + int ch = text.charAt(i); + switch (ch) { + case '<': + case '>': + case '&': + case '"': + case '\'': + if (buf == null) { + buf = new StringBuilder(); + } + if (i > 0) { + buf.append(text.substring(pos, i)); + } + pos = i + 1; + break; + default: + continue; + } + if (ch == '<') { + buf.append("<"); + } else if (ch == '>') { + buf.append(">"); + } else if (ch == '&') { + buf.append("&"); + } else if (ch == '"') { + buf.append("""); + } else if (ch == '\'') { + buf.append(isHtml ? "'" : "'"); + } + } + if (buf == null) { + return text; + } else { + if (pos < length) { + buf.append(text.substring(pos)); + } + return buf.toString(); + } + } + + /** + * The list of characters that are not encoded by the escape() + * and unescape() METHODS. They contains the characters as + * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax': + *

    + *

    +     * unreserved  = alphanum | mark
    +     * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
    +     * 
    + */ + public static BitSet URISave; + + /** + * Same as {@link #URISave} but also contains the '/' + */ + public static BitSet URISaveEx; + + static { + URISave = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + URISave.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + URISave.set(i); + } + for (i = '0'; i <= '9'; i++) { + URISave.set(i); + } + URISave.set('-'); + URISave.set('_'); + URISave.set('.'); + URISave.set('!'); + URISave.set('~'); + URISave.set('*'); + URISave.set('\''); + URISave.set('('); + URISave.set(')'); + + URISaveEx = (BitSet) URISave.clone(); + URISaveEx.set('/'); + } + + /** + * Does an URL encoding of the string using the + * escape character. The characters that don't need encoding + * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' + * RFC 2396, but without the escape character. + * + * @param string the string to encode. + * @param escape the escape character. + * @return the escaped string + * @throws NullPointerException if string is null. + */ + public static String escape(String string, char escape) { + return escape(string, escape, false); + } + + /** + * Does an URL encoding of the string using the + * escape character. The characters that don't need encoding + * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' + * RFC 2396, but without the escape character. If isPath is + * true, additionally the slash '/' is ignored, too. + * + * @param string the string to encode. + * @param escape the escape character. + * @param isPath if true, the string is treated as path + * @return the escaped string + * @throws NullPointerException if string is null. + */ + public static String escape(String string, char escape, boolean isPath) { + BitSet validChars = isPath ? URISaveEx : URISave; + byte[] bytes = string.getBytes(StandardCharsets.UTF_8); + StringBuilder out = new StringBuilder(bytes.length); + for (byte aByte : bytes) { + int c = aByte & 0xff; + if (validChars.get(c) && c != escape) { + out.append((char) c); + } else { + out.append(escape); + out.append(hexTable[(c >> 4) & 0x0f]); + out.append(hexTable[(c) & 0x0f]); + } + } + return out.toString(); + } + + /** + * Does a URL encoding of the string. The characters that + * don't need encoding are those defined 'unreserved' in section 2.3 of + * the 'URI generic syntax' RFC 2396. + * + * @param string the string to encode + * @return the escaped string + * @throws NullPointerException if string is null. + */ + public static String escape(String string) { + return escape(string, '%'); + } + + /** + * Does a URL encoding of the path. The characters that + * don't need encoding are those defined 'unreserved' in section 2.3 of + * the 'URI generic syntax' RFC 2396. In contrast to the + * {@link #escape(String)} method, not the entire path string is escaped, + * but every individual part (i.e. the slashes are not escaped). + * + * @param path the path to encode + * @return the escaped path + * @throws NullPointerException if path is null. + */ + public static String escapePath(String path) { + return escape(path, '%', true); + } + + /** + * Does a URL decoding of the string using the + * escape character. Please note that in opposite to the + * {@link java.net.URLDecoder} it does not transform the + into spaces. + * + * @param string the string to decode + * @param escape the escape character + * @return the decoded string + * @throws NullPointerException if string is null. + * @throws IllegalArgumentException if the 2 characters following the escape + * character do not represent a hex-number + * or if not enough characters follow an + * escape character + */ + public static String unescape(String string, char escape) { + byte[] utf8 = string.getBytes(StandardCharsets.UTF_8); + + // Check whether escape occurs at invalid position + if ((utf8.length >= 1 && utf8[utf8.length - 1] == escape) || + (utf8.length >= 2 && utf8[utf8.length - 2] == escape)) { + throw new IllegalArgumentException("Premature end of escape sequence at end of input"); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(utf8.length); + for (int k = 0; k < utf8.length; k++) { + byte b = utf8[k]; + if (b == escape) { + out.write((decodeDigit(utf8[++k]) << 4) + decodeDigit(utf8[++k])); + } + else { + out.write(b); + } + } + + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + + /** + * Does a URL decoding of the string. Please note that in + * opposite to the {@link java.net.URLDecoder} it does not transform the + + * into spaces. + * + * @param string the string to decode + * @return the decoded string + * @throws NullPointerException if string is null. + * @throws ArrayIndexOutOfBoundsException if not enough character follow an + * escape character + * @throws IllegalArgumentException if the 2 characters following the escape + * character do not represent a hex-number. + */ + public static String unescape(String string) { + return unescape(string, '%'); + } + + /** + * Escapes all illegal JCR name characters of a string. + * The encoding is loosely modeled after URI encoding, but only encodes + * the characters it absolutely needs to in order to make the resulting + * string a valid JCR name. + * Use {@link #unescapeIllegalJcrChars(String)} for decoding. + *

    + * QName EBNF: + *

    +     * simplename ::= onecharsimplename | twocharsimplename | threeormorecharname
    +     * onecharsimplename ::= (* Any Unicode character except: '.', '/', ':', '[', ']', '*', '|' or any whitespace character *)
    +     * twocharsimplename ::= '.' onecharsimplename | onecharsimplename '.' | onecharsimplename onecharsimplename
    +     * threeormorecharname ::= nonspace string nonspace
    +     * string ::= char | string char
    +     * char ::= nonspace | ' '
    +     * nonspace ::= (* Any Unicode character except: '/', ':', '[', ']', '*', '|' or any whitespace character *)
    +     * 
    + * + * @param name the name to escape + * @return the escaped name + */ + public static String escapeIllegalJcrChars(String name) { + return escapeIllegalChars(name, "%/:[]*|\t\r\n"); + } + + /** + * Escapes all illegal JCR 1.0 name characters of a string. + * Use {@link #unescapeIllegalJcrChars(String)} for decoding. + *

    + * QName EBNF: + *

    +     * simplename ::= onecharsimplename | twocharsimplename | threeormorecharname
    +     * onecharsimplename ::= (* Any Unicode character except: '.', '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace character *)
    +     * twocharsimplename ::= '.' onecharsimplename | onecharsimplename '.' | onecharsimplename onecharsimplename
    +     * threeormorecharname ::= nonspace string nonspace
    +     * string ::= char | string char
    +     * char ::= nonspace | ' '
    +     * nonspace ::= (* Any Unicode character except: '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace character *)
    +     * 
    + * + * @since Apache Jackrabbit 2.3.2 and 2.2.10 + * @see JCR-3128 + * @param name the name to escape + * @return the escaped name + */ + public static String escapeIllegalJcr10Chars(String name) { + return escapeIllegalChars(name, "%/:[]*'\"|\t\r\n"); + } + + private static String escapeIllegalChars(String name, String illegal) { + StringBuilder buffer = new StringBuilder(name.length() * 2); + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + if (illegal.indexOf(ch) != -1 + || (ch == '.' && name.length() < 3) + || (ch == ' ' && (i == 0 || i == name.length() - 1))) { + buffer.append('%'); + buffer.append(Character.toUpperCase(Character.forDigit(ch / 16, 16))); + buffer.append(Character.toUpperCase(Character.forDigit(ch % 16, 16))); + } else { + buffer.append(ch); + } + } + return buffer.toString(); + } + + /** + * Escapes illegal XPath search characters at the end of a string. + *

    Example:
    + * A search string like 'test?' will run into a ParseException + * documented in http://issues.apache.org/jira/browse/JCR-1248 + * + * @param s the string to encode + * @return the escaped string + */ + public static String escapeIllegalXpathSearchChars(String s) { + StringBuilder sb = new StringBuilder(); + sb.append(s.substring(0, (s.length() - 1))); + char c = s.charAt(s.length() - 1); + // NOTE: keep this in sync with _ESCAPED_CHAR below! + if (c == '!' || c == '(' || c == ':' || c == '^' + || c == '[' || c == ']' || c == '{' || c == '}' || c == '?') { + sb.append('\\'); + } + sb.append(c); + return sb.toString(); + } + + /** + * Unescapes previously escaped jcr chars. + *

    + * Please note, that this does not exactly the same as the url related + * {@link #unescape(String)}, since it handles the byte-encoding + * differently. + * + * @param name the name to unescape + * @return the unescaped name + */ + public static String unescapeIllegalJcrChars(String name) { + StringBuilder buffer = new StringBuilder(name.length()); + int i = name.indexOf('%'); + while (i > -1 && i + 2 < name.length()) { + buffer.append(name.toCharArray(), 0, i); + int a = Character.digit(name.charAt(i + 1), 16); + int b = Character.digit(name.charAt(i + 2), 16); + if (a > -1 && b > -1) { + buffer.append((char) (a * 16 + b)); + name = name.substring(i + 3); + } else { + buffer.append('%'); + name = name.substring(i + 1); + } + i = name.indexOf('%'); + } + buffer.append(name); + return buffer.toString(); + } + + /** + * Returns the name part of the path. If the given path is already a name + * (i.e. contains no slashes) it is returned. + * + * @param path the path + * @return the name part or null if path is null. + */ + public static String getName(String path) { + return getName(path, '/'); + } + + /** + * Returns the name part of the path, delimited by the given delim. + * If the given path is already a name (i.e. contains no delim + * characters) it is returned. + * + * @param path the path + * @param delim the delimiter + * @return the name part or null if path is null. + */ + public static String getName(String path, char delim) { + return path == null + ? null + : path.substring(path.lastIndexOf(delim) + 1); + } + + /** + * Same as {@link #getName(String)} but adding the possibility + * to pass paths that end with a trailing '/' + * + * @see #getName(String) + */ + public static String getName(String path, boolean ignoreTrailingSlash) { + if (ignoreTrailingSlash && path != null && path.endsWith("/") && path.length() > 1) { + path = path.substring(0, path.length()-1); + } + return getName(path); + } + + /** + * Returns the namespace prefix of the given qname. If the + * prefix is missing, an empty string is returned. Please note, that this + * method does not validate the name or prefix. + *

    + * The qname has the format: qname := [prefix ':'] local; + * + * @param qname a qualified name + * @return the prefix of the name or "". + * + * @see #getLocalName(String) + * + * @throws NullPointerException if qname is null + */ + public static String getNamespacePrefix(String qname) { + int pos = qname.indexOf(':'); + return pos >=0 ? qname.substring(0, pos) : ""; + } + + /** + * Returns the local name of the given qname. Please note, that + * this method does not validate the name. + *

    + * The qname has the format: qname := [prefix ':'] local; + * + * @param qname a qualified name + * @return the localname + * + * @see #getNamespacePrefix(String) + * + * @throws NullPointerException if qname is null + */ + public static String getLocalName(String qname) { + int pos = qname.indexOf(':'); + return pos >=0 ? qname.substring(pos+1) : qname; + } + + /** + * Determines, if two paths denote hierarchical siblins. + * + * @param p1 first path + * @param p2 second path + * @return true if on same level, false otherwise + */ + public static boolean isSibling(String p1, String p2) { + int pos1 = p1.lastIndexOf('/'); + int pos2 = p2.lastIndexOf('/'); + return (pos1 == pos2 && pos1 >= 0 && p1.regionMatches(0, p2, 0, pos1)); + } + + /** + * Determines if the descendant path is hierarchical a + * descendant of path. + * + * @param path the current path + * @param descendant the potential descendant + * @return true if the descendant is a descendant; + * false otherwise. + */ + public static boolean isDescendant(String path, String descendant) { + String pattern = path.endsWith("/") ? path : path + "/"; + return !pattern.equals(descendant) && + descendant.startsWith(pattern); + } + + /** + * Determines if the descendant path is hierarchical a + * descendant of path or equal to it. + * + * @param path the path to check + * @param descendant the potential descendant + * @return true if the descendant is a descendant + * or equal; false otherwise. + */ + public static boolean isDescendantOrEqual(String path, String descendant) { + if (path.equals(descendant)) { + return true; + } else { + String pattern = path.endsWith("/") ? path : path + "/"; + return descendant.startsWith(pattern); + } + } + + /** + * Returns the nth relative parent of the path, where n=level. + *

    Example:
    + * + * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar" + * + * + * @param path the path of the page + * @param level the level of the parent + */ + public static String getRelativeParent(String path, int level) { + int idx = path.length(); + while (level > 0) { + idx = path.lastIndexOf('/', idx - 1); + if (idx < 0) { + return ""; + } + level--; + } + return (idx == 0) ? "/" : path.substring(0, idx); + } + + /** + * Same as {@link #getRelativeParent(String, int)} but adding the possibility + * to pass paths that end with a trailing '/' + * + * @see #getRelativeParent(String, int) + */ + public static String getRelativeParent(String path, int level, boolean ignoreTrailingSlash) { + if (ignoreTrailingSlash && path.endsWith("/") && path.length() > 1) { + path = path.substring(0, path.length()-1); + } + return getRelativeParent(path, level); + } + + /** + * Returns the nth absolute parent of the path, where n=level. + *

    Example:
    + * + * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar" + * + * + * @param path the path of the page + * @param level the level of the parent + */ + public static String getAbsoluteParent(String path, int level) { + int idx = 0; + int len = path.length(); + while (level >= 0 && idx < len) { + idx = path.indexOf('/', idx + 1); + if (idx < 0) { + idx = len; + } + level--; + } + return level >= 0 ? "" : path.substring(0, idx); + } + + /** + * Performs variable replacement on the given string value. + * Each ${...} sequence within the given value is replaced + * with the value of the named parser variable. If a variable is not found + * in the properties an IllegalArgumentException is thrown unless + * ignoreMissing is true. In the later case, the + * missing variable is replaced by the empty string. + * + * @param value the original value + * @param ignoreMissing if true, missing variables are replaced + * by the empty string. + * @return value after variable replacements + * @throws IllegalArgumentException if the replacement of a referenced + * variable is not found + */ + public static String replaceVariables(Properties variables, String value, + boolean ignoreMissing) + throws IllegalArgumentException { + StringBuilder result = new StringBuilder(); + + // Value: + // +--+-+--------+-+-----------------+ + // | |p|--> |q|--> | + // +--+-+--------+-+-----------------+ + int p = 0, q = value.indexOf("${"); // Find first ${ + while (q != -1) { + result.append(value.substring(p, q)); // Text before ${ + p = q; + q = value.indexOf("}", q + 2); // Find } + if (q != -1) { + String variable = value.substring(p + 2, q); + String replacement = variables.getProperty(variable); + if (replacement == null) { + if (ignoreMissing) { + replacement = ""; + } else { + throw new IllegalArgumentException( + "Replacement not found for ${" + variable + "}."); + } + } + result.append(replacement); + p = q + 1; + q = value.indexOf("${", p); // Find next ${ + } + } + result.append(value.substring(p, value.length())); // Trailing text + + return result.toString(); + } + + private static byte decodeDigit(byte b) { + if (b >= 0x30 && b <= 0x39) { + return (byte) (b - 0x30); + } + else if (b >= 0x41 && b <= 0x46) { + return (byte) (b - 0x37); + } + else if (b >= 0x61 && b <= 0x66) { + return (byte) (b - 0x57); + } + else { + throw new IllegalArgumentException("Escape sequence is not hexadecimal: " + (char)b); + } + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Timer.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Timer.java new file mode 100644 index 00000000000..97744e2fbe9 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/Timer.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.util; + +import java.util.TimerTask; + +/** + * Timer wraps the standard Java {@link java.util.Timer} class + * and implements a guaranteed shutdown of the background thread running + * in the Timer instance after a certain {@link #IDLE_TIME}. + */ +public class Timer { + + /** + * Idle time in milliseconds. When a timer instance is idle for this amount + * of time the underlying timer is canceled. + */ + static final int IDLE_TIME = 3 * 1000; + + /** + * The interval at which the idle checker task runs. + */ + static final int CHECKER_INTERVAL = 1000; + + /** + * The timer implementation we us internally. + */ + private java.util.Timer delegatee; + + /** + * Indicates whether the timer thread should run as deamon. + */ + private final boolean runAsDeamon; + + /** + * The number of currently scheduled tasks. If this value drops to zero + * the internal {@link java.util.Timer} instance is canceled. Whenever + * this value increases from zero to one a new {@link java.util.Timer} + * instance is created and started. + */ + private int numScheduledTasks = 0; + + /** + * The time when the last task was scheduled. + */ + private long lastTaskScheduled; + + /** + * Creates a new Timer instance. + * + * @param isDeamon if true the background thread wil run as + * deamon. + */ + public Timer(boolean isDeamon) { + runAsDeamon = isDeamon; + } + + /** + * Schedules the specified task for repeated fixed-delay execution, + * beginning after the specified delay. Subsequent executions take place + * at approximately regular intervals separated by the specified period. + * + * @param task task to be scheduled. + * @param delay delay in milliseconds before task is to be executed. + * @param period time in milliseconds between successive task executions. + * @throws IllegalArgumentException if delay is negative, or + * delay + System.currentTimeMillis() is negative. + * @throws IllegalStateException if task was already scheduled or + * cancelled, timer was cancelled, or timer thread terminated. + * @see java.util.Timer#schedule(java.util.TimerTask, long, long) + */ + public void schedule(Task task, long delay, long period) { + if (delay < 0) + throw new IllegalArgumentException("Negative delay."); + if (period <= 0) + throw new IllegalArgumentException("Non-positive period."); + synchronized (this) { + if (delegatee == null) { + delegatee = new java.util.Timer(runAsDeamon); + // run idle checker every second + Task idleChecker = new IdleCheckerTask(); + idleChecker.setTimer(this); + delegatee.schedule(idleChecker, IDLE_TIME, CHECKER_INTERVAL); + } + delegatee.schedule(task, delay, period); + task.setTimer(this); + numScheduledTasks++; + lastTaskScheduled = System.currentTimeMillis(); + } + } + + /** + * Terminates this timer, discarding any currently scheduled tasks. + * Does not interfere with a currently executing task (if it exists). + * Once a timer has been terminated, its execution thread terminates + * gracefully, and no more tasks may be scheduled on it. + * + *

    Note that calling this method from within the run method of a + * timer task that was invoked by this timer absolutely guarantees that + * the ongoing task execution is the last task execution that will ever + * be performed by this timer. + * + *

    This method may be called repeatedly; the second and subsequent + * calls have no effect. + */ + public void cancel() { + synchronized (this) { + if (delegatee != null) { + delegatee.cancel(); + numScheduledTasks = 0; + delegatee = null; + } + } + } + + /** + * @return true if this timer has a running backround thread + * for scheduled tasks. This method is only for test purposes. + */ + boolean isRunning() { + synchronized (this) { + return delegatee != null; + } + } + + /** + * Notifies this Timer that a task has been canceled. + */ + private void taskCanceled() { + synchronized (this) { + --numScheduledTasks; + } + } + + /** + * Extends the TimerTask with callback hooks to this + * Timer implementation. + */ + public static abstract class Task extends TimerTask { + + /** + * The Timer instance where this Task is + * scheduled on. + */ + private Timer timer; + + /** + * Sets the timer instance where this task is scheduled on. + * @param timer the timer instance. + */ + private void setTimer(Timer timer) { + this.timer = timer; + } + + /** + * {@inheritDoc} + */ + public final boolean cancel() { + if (timer != null) { + timer.taskCanceled(); + timer = null; + } + return super.cancel(); + } + } + + /** + * Checks if the enclosing timer had been idle for at least + * {@link Timer#IDLE_TIME} and cancels it in that case. + */ + private class IdleCheckerTask extends Task { + + public void run() { + synchronized (Timer.this) { + if (numScheduledTasks == 0 && + System.currentTimeMillis() > lastTaskScheduled + IDLE_TIME) { + if (delegatee != null) { + delegatee.cancel(); + delegatee = null; + } + } + } + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/TransientFileFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/TransientFileFactory.java new file mode 100644 index 00000000000..e823fbf746a --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/TransientFileFactory.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.util; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Collections; + +/** + * The TransientFileFactory utility class can be used to create + * transient files, i.e. temporary files that are automatically + * removed once the associated File object is reclaimed by the + * garbage collector. + *

    + * File deletion is handled by a low-priority background thread. + *

    + */ +public class TransientFileFactory { + + /** + * The singleton factory instance + */ + private static TransientFileFactory INSTANCE; + + /** + * Queue where MoribundFileReference instances will be enqueued + * once the associated target File objects have been gc'ed. + */ + private final ReferenceQueue phantomRefQueue = new ReferenceQueue(); + + /** + * Collection of MoribundFileReference instances currently + * being tracked. + */ + private final Collection trackedRefs = + Collections.synchronizedList(new ArrayList()); + + /** + * The reaper thread responsible for removing files awaiting deletion + */ + private final ReaperThread reaper; + + /** + * Shutdown hook which removes all files awaiting deletion + */ + private static Thread shutdownHook = null; + + /** + * Returns the singleton TransientFileFactory instance. + */ + public static TransientFileFactory getInstance() { + synchronized (TransientFileFactory.class) { + if (INSTANCE == null) { + INSTANCE = new TransientFileFactory(); + } + return INSTANCE; + } + } + + /** + * Hidden constructor. + */ + private TransientFileFactory() { + // instantiate & start low priority reaper thread + reaper = new ReaperThread("Transient File Reaper"); + reaper.setPriority(Thread.MIN_PRIORITY); + reaper.setDaemon(true); + reaper.start(); + // register shutdownhook for final cleaning up + try { + shutdownHook = new Thread() { + public void run() { + doShutdown(); + } + }; + Runtime.getRuntime().addShutdownHook(shutdownHook); + } catch (IllegalStateException e) { + // can't register shutdownhook because + // jvm shutdown sequence has already begun, + // silently ignore... + } + } + + //------------------------------------------------------< factory methods > + /** + * Same as {@link File#createTempFile(String, String, File)} except that + * the newly-created file will be automatically deleted once the + * returned File object has been gc'ed. + * + * @param prefix The prefix string to be used in generating the file's + * name; must be at least three characters long + * @param suffix The suffix string to be used in generating the file's + * name; may be null, in which case the + * suffix ".tmp" will be used + * @param directory The directory in which the file is to be created, or + * null if the default temporary-file + * directory is to be used + * @return the newly-created empty file + * @throws IOException If a file could not be created + */ + public File createTransientFile(String prefix, String suffix, File directory) + throws IOException { + File f = File.createTempFile(prefix, suffix, directory); + trackedRefs.add(new MoribundFileReference(f, phantomRefQueue)); + return f; + } + + /** + * Shuts this factory down removing all temp files and removes shutdown hook. + *

    + * Warning!!! + *

    + * This should be called by a web-application IF it is unloaded + * AND IF jackrabbit-jcr-commons.jar had been loaded by + * the webapp classloader. This must be called after all repositories had + * been stopped, so use with great care! + *

    + * See http://issues.apache.org/jira/browse/JCR-1636 for details. + */ + public static void shutdown() { + getInstance().doShutdown(); + } + + /** + * Actually shuts factory down removing all temp files. This happens when + * VM shutdown hook works or when explicitly requested. + * Shutdown hook is removed. + */ + private synchronized void doShutdown() { + // synchronize on the list before iterating over it in order + // to avoid ConcurrentModificationException (JCR-549) + // @see java.lang.util.Collections.synchronizedList(java.util.List) + synchronized(trackedRefs) { + for (Iterator it = trackedRefs.iterator(); it.hasNext();) { + it.next().delete(); + } + + } + if (shutdownHook != null) { + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } catch (IllegalStateException e) { + // can't unregister shutdownhook because + // jvm shutdown sequence has already begun, + // silently ignore... + } + shutdownHook = null; + } + reaper.stopWorking(); + } + + //--------------------------------------------------------< inner classes > + /** + * The reaper thread that will remove the files that are ready for deletion. + */ + private class ReaperThread extends Thread { + + private volatile boolean stopping = false; + + ReaperThread(String name) { + super(name); + } + + /** + * Run the reaper thread that will delete files as their associated + * marker objects are reclaimed by the garbage collector. + */ + public void run() { + while (!stopping) { + MoribundFileReference fileRef = null; + try { + // wait until a MoribundFileReference is ready for deletion + fileRef = (MoribundFileReference) phantomRefQueue.remove(); + } catch (InterruptedException e) { + if (stopping) { + break; + } + } catch (Exception e) { + // silently ignore... + continue; + } + // delete target + fileRef.delete(); + fileRef.clear(); + trackedRefs.remove(fileRef); + } + } + + /** + * Stops the reaper thread. + */ + public void stopWorking() { + stopping = true; + interrupt(); + } + } + + /** + * Tracker object for a file pending deletion. + */ + private static class MoribundFileReference extends PhantomReference { + + /** + * The full path to the file being tracked. + */ + private final String path; + + /** + * Constructs an instance of this class from the supplied parameters. + * + * @param file The file to be tracked. + * @param queue The queue on to which the tracker will be pushed. + */ + MoribundFileReference(File file, ReferenceQueue queue) { + super(file, queue); + this.path = file.getPath(); + } + + /** + * Deletes the file associated with this instance. + * + * @return true if the file was deleted successfully; + * false otherwise. + */ + boolean delete() { + return new File(path).delete(); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/WeakIdentityCollection.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/WeakIdentityCollection.java new file mode 100644 index 00000000000..da24d4a3dd8 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/WeakIdentityCollection.java @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.BitSet; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.lang.ref.ReferenceQueue; + +/** + * WeakIdentityCollection implements a Collection with weak values. + * Equality of elements is tested using the == operator. + *

    + * This collection does not hide the fact that the garbage collector will remove + * a mapping at some point in time. Thus, the {@link java.util.Iterator} returned + * by this collection might return null values. The same applies + * to the method {@link #toArray()} in both its variants. + */ +public class WeakIdentityCollection implements Collection { + + /** + * The weak references. + */ + private transient WeakRef[] elementData; + + /** + * The current number of elements in {@link #elementData}. + */ + private int size; + + /** + * The reference queue to poll for references that point to unreachable + * objects. + */ + private final ReferenceQueue refQueue = new ReferenceQueue(); + + /** + * BitSet where a set bit indicates that the slot at this position in + * {@link #elementData} is empty and can be reused. + */ + private final BitSet emptySlots = new BitSet(); + + /** + * Creates a new WeakIdentityCollection. + * + * @param initialCapacity the initial capacity. + */ + public WeakIdentityCollection(int initialCapacity) { + if (initialCapacity < 0) { + throw new IllegalArgumentException("Illegal Capacity: " + + initialCapacity); + } + this.elementData = new WeakRef[initialCapacity]; + } + + /** + * Returns the current size of this collection. + * + * @return the current size of this collection. + */ + public int size() { + return size; + } + + /** + * Returns true if this collection is empty. + * + * @return true if this collection is empty. + */ + public boolean isEmpty() { + return size == 0; + } + + /** + * Releases all references held by this collection. + */ + public void clear() { + for (int i = 0; i < size; i++) { + elementData[i] = null; + } + size = 0; + emptySlots.clear(); + } + + /** + * Adds object o to this collection. + * + * @param o the object to add. + * @return always true as this collection allows duplicates. + * @throws NullPointerException if o is null. + */ + public boolean add(Object o) { + if (o == null) { + throw new NullPointerException("Object must not be null"); + } + // poll refQueue for a slot we can reuse + WeakRef ref = (WeakRef) refQueue.poll(); + if (ref != null) { + elementData[ref.index] = new WeakRef(o, ref.index); + cleanQueue(); + } else if (!emptySlots.isEmpty()) { + int idx = emptySlots.nextSetBit(0); + elementData[idx] = new WeakRef(o, idx); + emptySlots.clear(idx); + } else { + ensureCapacity(size + 1); + elementData[size++] = new WeakRef(o, size - 1); + } + return true; + } + + /** + * Returns true if this collection contains o. + * + * @param o element whose presence in this collection is to be tested. + * @return true if this collection contains the specified + * element + */ + public boolean contains(Object o) { + for (int i = 0; i < size; i++) { + if (elementData[i].get() == o) { + return true; + } + } + return false; + } + + /** + * Removes the object o from this collection if it is present. + * + * @param o the object to remove. + * @return true if this collection changed as a result of the + * call. + */ + public boolean remove(Object o) { + for (int i = 0; i < size; i++) { + if (elementData[i].get() == o) { + emptySlots.set(i); + // overwrite entry with dummy ref + elementData[i] = new WeakRef(null, i); + return true; + } + } + return false; + } + + /** + * @throws UnsupportedOperationException always. + */ + public boolean addAll(Collection c) { + throw new UnsupportedOperationException("addAll"); + } + + /** + * @throws UnsupportedOperationException always. + */ + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException("containsAll"); + } + + /** + * @throws UnsupportedOperationException always. + */ + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException("removeAll"); + } + + /** + * @throws UnsupportedOperationException always. + */ + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException("retainAll"); + } + + /** + * Returns an {@link java.util.Iterator} over the elements of this + * collection. The returned iterator is not fail-fast. That is, it does + * not throw a {@link java.util.ConcurrentModificationException} if this + * collection is modified while iterating over the collection. + * + * @return an {@link java.util.Iterator} over the elements of this + * collection. + */ + public Iterator iterator() { + return new Iter(); + } + + /** + * Returns an array containing all of the elements in this collection. The + * returned array may contain null elements! + * + * @return an array containing all of the elements in this collection. + */ + public Object[] toArray() { + Object[] result = new Object[size]; + for (int i = 0; i < result.length; i++) { + result[i] = elementData[i].get(); + } + return result; + } + + /** + * The returned array may contain null elements! + * {@inheritDoc} + */ + public Object[] toArray(Object a[]) { + if (a.length < size) { + a = (Object[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size); + } + + for (int i = 0; i < size; i++) { + a[i] = elementData[i].get(); + } + + if (a.length > size) { + a[size] = null; + } + + return a; + } + + /** + * Ensures that the internal {@link #elementData} has + * minCapacity. + * + * @param minCapacity the minimal capacity to ensure. + */ + private void ensureCapacity(int minCapacity) { + int oldCapacity = elementData.length; + if (minCapacity > oldCapacity) { + Object oldData[] = elementData; + int newCapacity = (oldCapacity * 3)/2 + 1; + if (newCapacity < minCapacity) + newCapacity = minCapacity; + elementData = new WeakRef[newCapacity]; + System.arraycopy(oldData, 0, elementData, 0, size); + } + } + + /** + * Polls the reference queue until no reference is available anymore. + */ + private void cleanQueue() { + WeakRef ref; + while ((ref = (WeakRef) refQueue.poll()) != null) { + emptySlots.set(ref.index); + } + } + + /** + * Iterator implementation for this collecation. + */ + private final class Iter implements Iterator { + + /** + * current index. + */ + private int index; + + /** + * The current element data. + */ + private Reference[] elements = elementData; + + /** + * The current size of this collection. + */ + private int size = WeakIdentityCollection.this.size; + + /** + * @throws UnsupportedOperationException always. + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + /** + * @inheritDoc + */ + public boolean hasNext() { + return index < size; + } + + /** + * @inheritDoc + */ + public Object next() { + if (index >= size) { + throw new NoSuchElementException(); + } + return elements[index++].get(); + } + } + + /** + * Weak reference with index value that points to the slot in {@link + * WeakIdentityCollection#elementData}. + */ + private final class WeakRef extends WeakReference { + + /** + * The index of this weak reference. + */ + private final int index; + + /** + * Creates a new WeakRef. + * + * @param referent object the new weak reference will refer to. + * @param index the index of this weak reference. + */ + public WeakRef(Object referent, int index) { + super(referent, refQueue); + this.index = index; + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/XMLChar.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/XMLChar.java new file mode 100644 index 00000000000..d19e90cb32d --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/XMLChar.java @@ -0,0 +1,1025 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.util; + +// Note: This file is a copy from org.apache.xerces.util. +// See http://issues.apache.org/jira/browse/JCR-367 + +import java.util.Arrays; + +/** + * This class defines the basic XML character properties. The data + * in this class can be used to verify that a character is a valid + * XML character or if the character is a space, name start, or name + * character. + *

    + * A series of convenience methods are supplied to ease the burden + * of the developer. Because inlining the checks can improve per + * character performance, the tables of character properties are + * public. Using the character as an index into the CHARS + * array and applying the appropriate mask flag (e.g. + * MASK_VALID), yields the same results as calling the + * convenience methods. There is one exception: check the comments + * for the isValid method for details. + * + * @author Glenn Marcy, IBM + * @author Andy Clark, IBM + * @author Eric Ye, IBM + * @author Arnaud Le Hors, IBM + * @author Michael Glavassevich, IBM + * @author Rahul Srivastava, Sun Microsystems Inc. + * + * @version $Id$ + */ +public class XMLChar { + + // + // Constants + // + + /** Character flags. */ + private static final byte[] CHARS = new byte[1 << 16]; + + /** Valid character mask. */ + public static final int MASK_VALID = 0x01; + + /** Space character mask. */ + public static final int MASK_SPACE = 0x02; + + /** Name start character mask. */ + public static final int MASK_NAME_START = 0x04; + + /** Name character mask. */ + public static final int MASK_NAME = 0x08; + + /** Pubid character mask. */ + public static final int MASK_PUBID = 0x10; + + /** + * Content character mask. Special characters are those that can + * be considered the start of markup, such as '<' and '&'. + * The various newline characters are considered special as well. + * All other valid XML characters can be considered content. + *

    + * This is an optimization for the inner loop of character scanning. + */ + public static final int MASK_CONTENT = 0x20; + + /** NCName start character mask. */ + public static final int MASK_NCNAME_START = 0x40; + + /** NCName character mask. */ + public static final int MASK_NCNAME = 0x80; + + // + // Static initialization + // + + static { + + // Initializing the Character Flag Array + // Code generated by: XMLCharGenerator. + + CHARS[9] = 35; + CHARS[10] = 19; + CHARS[13] = 19; + CHARS[32] = 51; + CHARS[33] = 49; + CHARS[34] = 33; + Arrays.fill(CHARS, 35, 38, (byte) 49 ); // Fill 3 of value (byte) 49 + CHARS[38] = 1; + Arrays.fill(CHARS, 39, 45, (byte) 49 ); // Fill 6 of value (byte) 49 + Arrays.fill(CHARS, 45, 47, (byte) -71 ); // Fill 2 of value (byte) -71 + CHARS[47] = 49; + Arrays.fill(CHARS, 48, 58, (byte) -71 ); // Fill 10 of value (byte) -71 + CHARS[58] = 61; + CHARS[59] = 49; + CHARS[60] = 1; + CHARS[61] = 49; + CHARS[62] = 33; + Arrays.fill(CHARS, 63, 65, (byte) 49 ); // Fill 2 of value (byte) 49 + Arrays.fill(CHARS, 65, 91, (byte) -3 ); // Fill 26 of value (byte) -3 + Arrays.fill(CHARS, 91, 93, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[93] = 1; + CHARS[94] = 33; + CHARS[95] = -3; + CHARS[96] = 33; + Arrays.fill(CHARS, 97, 123, (byte) -3 ); // Fill 26 of value (byte) -3 + Arrays.fill(CHARS, 123, 183, (byte) 33 ); // Fill 60 of value (byte) 33 + CHARS[183] = -87; + Arrays.fill(CHARS, 184, 192, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 192, 215, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[215] = 33; + Arrays.fill(CHARS, 216, 247, (byte) -19 ); // Fill 31 of value (byte) -19 + CHARS[247] = 33; + Arrays.fill(CHARS, 248, 306, (byte) -19 ); // Fill 58 of value (byte) -19 + Arrays.fill(CHARS, 306, 308, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 308, 319, (byte) -19 ); // Fill 11 of value (byte) -19 + Arrays.fill(CHARS, 319, 321, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 321, 329, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[329] = 33; + Arrays.fill(CHARS, 330, 383, (byte) -19 ); // Fill 53 of value (byte) -19 + CHARS[383] = 33; + Arrays.fill(CHARS, 384, 452, (byte) -19 ); // Fill 68 of value (byte) -19 + Arrays.fill(CHARS, 452, 461, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 461, 497, (byte) -19 ); // Fill 36 of value (byte) -19 + Arrays.fill(CHARS, 497, 500, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 500, 502, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 502, 506, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 506, 536, (byte) -19 ); // Fill 30 of value (byte) -19 + Arrays.fill(CHARS, 536, 592, (byte) 33 ); // Fill 56 of value (byte) 33 + Arrays.fill(CHARS, 592, 681, (byte) -19 ); // Fill 89 of value (byte) -19 + Arrays.fill(CHARS, 681, 699, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 699, 706, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 706, 720, (byte) 33 ); // Fill 14 of value (byte) 33 + Arrays.fill(CHARS, 720, 722, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 722, 768, (byte) 33 ); // Fill 46 of value (byte) 33 + Arrays.fill(CHARS, 768, 838, (byte) -87 ); // Fill 70 of value (byte) -87 + Arrays.fill(CHARS, 838, 864, (byte) 33 ); // Fill 26 of value (byte) 33 + Arrays.fill(CHARS, 864, 866, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 866, 902, (byte) 33 ); // Fill 36 of value (byte) 33 + CHARS[902] = -19; + CHARS[903] = -87; + Arrays.fill(CHARS, 904, 907, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[907] = 33; + CHARS[908] = -19; + CHARS[909] = 33; + Arrays.fill(CHARS, 910, 930, (byte) -19 ); // Fill 20 of value (byte) -19 + CHARS[930] = 33; + Arrays.fill(CHARS, 931, 975, (byte) -19 ); // Fill 44 of value (byte) -19 + CHARS[975] = 33; + Arrays.fill(CHARS, 976, 983, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 983, 986, (byte) 33 ); // Fill 3 of value (byte) 33 + CHARS[986] = -19; + CHARS[987] = 33; + CHARS[988] = -19; + CHARS[989] = 33; + CHARS[990] = -19; + CHARS[991] = 33; + CHARS[992] = -19; + CHARS[993] = 33; + Arrays.fill(CHARS, 994, 1012, (byte) -19 ); // Fill 18 of value (byte) -19 + Arrays.fill(CHARS, 1012, 1025, (byte) 33 ); // Fill 13 of value (byte) 33 + Arrays.fill(CHARS, 1025, 1037, (byte) -19 ); // Fill 12 of value (byte) -19 + CHARS[1037] = 33; + Arrays.fill(CHARS, 1038, 1104, (byte) -19 ); // Fill 66 of value (byte) -19 + CHARS[1104] = 33; + Arrays.fill(CHARS, 1105, 1117, (byte) -19 ); // Fill 12 of value (byte) -19 + CHARS[1117] = 33; + Arrays.fill(CHARS, 1118, 1154, (byte) -19 ); // Fill 36 of value (byte) -19 + CHARS[1154] = 33; + Arrays.fill(CHARS, 1155, 1159, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 1159, 1168, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 1168, 1221, (byte) -19 ); // Fill 53 of value (byte) -19 + Arrays.fill(CHARS, 1221, 1223, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1223, 1225, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1225, 1227, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1227, 1229, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1229, 1232, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 1232, 1260, (byte) -19 ); // Fill 28 of value (byte) -19 + Arrays.fill(CHARS, 1260, 1262, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1262, 1270, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 1270, 1272, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1272, 1274, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1274, 1329, (byte) 33 ); // Fill 55 of value (byte) 33 + Arrays.fill(CHARS, 1329, 1367, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 1367, 1369, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[1369] = -19; + Arrays.fill(CHARS, 1370, 1377, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 1377, 1415, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 1415, 1425, (byte) 33 ); // Fill 10 of value (byte) 33 + Arrays.fill(CHARS, 1425, 1442, (byte) -87 ); // Fill 17 of value (byte) -87 + CHARS[1442] = 33; + Arrays.fill(CHARS, 1443, 1466, (byte) -87 ); // Fill 23 of value (byte) -87 + CHARS[1466] = 33; + Arrays.fill(CHARS, 1467, 1470, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[1470] = 33; + CHARS[1471] = -87; + CHARS[1472] = 33; + Arrays.fill(CHARS, 1473, 1475, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[1475] = 33; + CHARS[1476] = -87; + Arrays.fill(CHARS, 1477, 1488, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 1488, 1515, (byte) -19 ); // Fill 27 of value (byte) -19 + Arrays.fill(CHARS, 1515, 1520, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 1520, 1523, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 1523, 1569, (byte) 33 ); // Fill 46 of value (byte) 33 + Arrays.fill(CHARS, 1569, 1595, (byte) -19 ); // Fill 26 of value (byte) -19 + Arrays.fill(CHARS, 1595, 1600, (byte) 33 ); // Fill 5 of value (byte) 33 + CHARS[1600] = -87; + Arrays.fill(CHARS, 1601, 1611, (byte) -19 ); // Fill 10 of value (byte) -19 + Arrays.fill(CHARS, 1611, 1619, (byte) -87 ); // Fill 8 of value (byte) -87 + Arrays.fill(CHARS, 1619, 1632, (byte) 33 ); // Fill 13 of value (byte) 33 + Arrays.fill(CHARS, 1632, 1642, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 1642, 1648, (byte) 33 ); // Fill 6 of value (byte) 33 + CHARS[1648] = -87; + Arrays.fill(CHARS, 1649, 1720, (byte) -19 ); // Fill 71 of value (byte) -19 + Arrays.fill(CHARS, 1720, 1722, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1722, 1727, (byte) -19 ); // Fill 5 of value (byte) -19 + CHARS[1727] = 33; + Arrays.fill(CHARS, 1728, 1743, (byte) -19 ); // Fill 15 of value (byte) -19 + CHARS[1743] = 33; + Arrays.fill(CHARS, 1744, 1748, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[1748] = 33; + CHARS[1749] = -19; + Arrays.fill(CHARS, 1750, 1765, (byte) -87 ); // Fill 15 of value (byte) -87 + Arrays.fill(CHARS, 1765, 1767, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1767, 1769, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[1769] = 33; + Arrays.fill(CHARS, 1770, 1774, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 1774, 1776, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1776, 1786, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 1786, 2305, (byte) 33 ); // Fill 519 of value (byte) 33 + Arrays.fill(CHARS, 2305, 2308, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2308] = 33; + Arrays.fill(CHARS, 2309, 2362, (byte) -19 ); // Fill 53 of value (byte) -19 + Arrays.fill(CHARS, 2362, 2364, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2364] = -87; + CHARS[2365] = -19; + Arrays.fill(CHARS, 2366, 2382, (byte) -87 ); // Fill 16 of value (byte) -87 + Arrays.fill(CHARS, 2382, 2385, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2385, 2389, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 2389, 2392, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2392, 2402, (byte) -19 ); // Fill 10 of value (byte) -19 + Arrays.fill(CHARS, 2402, 2404, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2404, 2406, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2406, 2416, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2416, 2433, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 2433, 2436, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2436] = 33; + Arrays.fill(CHARS, 2437, 2445, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 2445, 2447, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2447, 2449, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2449, 2451, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2451, 2473, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2473] = 33; + Arrays.fill(CHARS, 2474, 2481, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2481] = 33; + CHARS[2482] = -19; + Arrays.fill(CHARS, 2483, 2486, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2486, 2490, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2490, 2492, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2492] = -87; + CHARS[2493] = 33; + Arrays.fill(CHARS, 2494, 2501, (byte) -87 ); // Fill 7 of value (byte) -87 + Arrays.fill(CHARS, 2501, 2503, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2503, 2505, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2505, 2507, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2507, 2510, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2510, 2519, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[2519] = -87; + Arrays.fill(CHARS, 2520, 2524, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2524, 2526, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2526] = 33; + Arrays.fill(CHARS, 2527, 2530, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2530, 2532, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2532, 2534, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2534, 2544, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2544, 2546, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2546, 2562, (byte) 33 ); // Fill 16 of value (byte) 33 + CHARS[2562] = -87; + Arrays.fill(CHARS, 2563, 2565, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2565, 2571, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 2571, 2575, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2575, 2577, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2577, 2579, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2579, 2601, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2601] = 33; + Arrays.fill(CHARS, 2602, 2609, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2609] = 33; + Arrays.fill(CHARS, 2610, 2612, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2612] = 33; + Arrays.fill(CHARS, 2613, 2615, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2615] = 33; + Arrays.fill(CHARS, 2616, 2618, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2618, 2620, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2620] = -87; + CHARS[2621] = 33; + Arrays.fill(CHARS, 2622, 2627, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 2627, 2631, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2631, 2633, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2633, 2635, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2635, 2638, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2638, 2649, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 2649, 2653, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[2653] = 33; + CHARS[2654] = -19; + Arrays.fill(CHARS, 2655, 2662, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 2662, 2674, (byte) -87 ); // Fill 12 of value (byte) -87 + Arrays.fill(CHARS, 2674, 2677, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2677, 2689, (byte) 33 ); // Fill 12 of value (byte) 33 + Arrays.fill(CHARS, 2689, 2692, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2692] = 33; + Arrays.fill(CHARS, 2693, 2700, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2700] = 33; + CHARS[2701] = -19; + CHARS[2702] = 33; + Arrays.fill(CHARS, 2703, 2706, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[2706] = 33; + Arrays.fill(CHARS, 2707, 2729, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2729] = 33; + Arrays.fill(CHARS, 2730, 2737, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2737] = 33; + Arrays.fill(CHARS, 2738, 2740, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2740] = 33; + Arrays.fill(CHARS, 2741, 2746, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 2746, 2748, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2748] = -87; + CHARS[2749] = -19; + Arrays.fill(CHARS, 2750, 2758, (byte) -87 ); // Fill 8 of value (byte) -87 + CHARS[2758] = 33; + Arrays.fill(CHARS, 2759, 2762, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2762] = 33; + Arrays.fill(CHARS, 2763, 2766, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2766, 2784, (byte) 33 ); // Fill 18 of value (byte) 33 + CHARS[2784] = -19; + Arrays.fill(CHARS, 2785, 2790, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 2790, 2800, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2800, 2817, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 2817, 2820, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2820] = 33; + Arrays.fill(CHARS, 2821, 2829, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 2829, 2831, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2831, 2833, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2833, 2835, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2835, 2857, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2857] = 33; + Arrays.fill(CHARS, 2858, 2865, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2865] = 33; + Arrays.fill(CHARS, 2866, 2868, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2868, 2870, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2870, 2874, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2874, 2876, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2876] = -87; + CHARS[2877] = -19; + Arrays.fill(CHARS, 2878, 2884, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 2884, 2887, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2887, 2889, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2889, 2891, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2891, 2894, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2894, 2902, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 2902, 2904, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2904, 2908, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2908, 2910, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2910] = 33; + Arrays.fill(CHARS, 2911, 2914, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2914, 2918, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2918, 2928, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2928, 2946, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 2946, 2948, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[2948] = 33; + Arrays.fill(CHARS, 2949, 2955, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 2955, 2958, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2958, 2961, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[2961] = 33; + Arrays.fill(CHARS, 2962, 2966, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2966, 2969, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2969, 2971, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2971] = 33; + CHARS[2972] = -19; + CHARS[2973] = 33; + Arrays.fill(CHARS, 2974, 2976, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2976, 2979, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2979, 2981, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2981, 2984, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2984, 2987, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2987, 2990, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2990, 2998, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[2998] = 33; + Arrays.fill(CHARS, 2999, 3002, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 3002, 3006, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3006, 3011, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 3011, 3014, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 3014, 3017, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3017] = 33; + Arrays.fill(CHARS, 3018, 3022, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3022, 3031, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[3031] = -87; + Arrays.fill(CHARS, 3032, 3047, (byte) 33 ); // Fill 15 of value (byte) 33 + Arrays.fill(CHARS, 3047, 3056, (byte) -87 ); // Fill 9 of value (byte) -87 + Arrays.fill(CHARS, 3056, 3073, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 3073, 3076, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3076] = 33; + Arrays.fill(CHARS, 3077, 3085, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3085] = 33; + Arrays.fill(CHARS, 3086, 3089, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3089] = 33; + Arrays.fill(CHARS, 3090, 3113, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3113] = 33; + Arrays.fill(CHARS, 3114, 3124, (byte) -19 ); // Fill 10 of value (byte) -19 + CHARS[3124] = 33; + Arrays.fill(CHARS, 3125, 3130, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 3130, 3134, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3134, 3141, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[3141] = 33; + Arrays.fill(CHARS, 3142, 3145, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3145] = 33; + Arrays.fill(CHARS, 3146, 3150, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3150, 3157, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3157, 3159, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3159, 3168, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 3168, 3170, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3170, 3174, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3174, 3184, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3184, 3202, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 3202, 3204, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3204] = 33; + Arrays.fill(CHARS, 3205, 3213, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3213] = 33; + Arrays.fill(CHARS, 3214, 3217, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3217] = 33; + Arrays.fill(CHARS, 3218, 3241, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3241] = 33; + Arrays.fill(CHARS, 3242, 3252, (byte) -19 ); // Fill 10 of value (byte) -19 + CHARS[3252] = 33; + Arrays.fill(CHARS, 3253, 3258, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 3258, 3262, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3262, 3269, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[3269] = 33; + Arrays.fill(CHARS, 3270, 3273, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3273] = 33; + Arrays.fill(CHARS, 3274, 3278, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3278, 3285, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3285, 3287, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3287, 3294, (byte) 33 ); // Fill 7 of value (byte) 33 + CHARS[3294] = -19; + CHARS[3295] = 33; + Arrays.fill(CHARS, 3296, 3298, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3298, 3302, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3302, 3312, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3312, 3330, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 3330, 3332, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3332] = 33; + Arrays.fill(CHARS, 3333, 3341, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3341] = 33; + Arrays.fill(CHARS, 3342, 3345, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3345] = 33; + Arrays.fill(CHARS, 3346, 3369, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3369] = 33; + Arrays.fill(CHARS, 3370, 3386, (byte) -19 ); // Fill 16 of value (byte) -19 + Arrays.fill(CHARS, 3386, 3390, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3390, 3396, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3396, 3398, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3398, 3401, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3401] = 33; + Arrays.fill(CHARS, 3402, 3406, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3406, 3415, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[3415] = -87; + Arrays.fill(CHARS, 3416, 3424, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 3424, 3426, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3426, 3430, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3430, 3440, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3440, 3585, (byte) 33 ); // Fill 145 of value (byte) 33 + Arrays.fill(CHARS, 3585, 3631, (byte) -19 ); // Fill 46 of value (byte) -19 + CHARS[3631] = 33; + CHARS[3632] = -19; + CHARS[3633] = -87; + Arrays.fill(CHARS, 3634, 3636, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3636, 3643, (byte) -87 ); // Fill 7 of value (byte) -87 + Arrays.fill(CHARS, 3643, 3648, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 3648, 3654, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 3654, 3663, (byte) -87 ); // Fill 9 of value (byte) -87 + CHARS[3663] = 33; + Arrays.fill(CHARS, 3664, 3674, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3674, 3713, (byte) 33 ); // Fill 39 of value (byte) 33 + Arrays.fill(CHARS, 3713, 3715, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3715] = 33; + CHARS[3716] = -19; + Arrays.fill(CHARS, 3717, 3719, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3719, 3721, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3721] = 33; + CHARS[3722] = -19; + Arrays.fill(CHARS, 3723, 3725, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[3725] = -19; + Arrays.fill(CHARS, 3726, 3732, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 3732, 3736, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[3736] = 33; + Arrays.fill(CHARS, 3737, 3744, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[3744] = 33; + Arrays.fill(CHARS, 3745, 3748, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3748] = 33; + CHARS[3749] = -19; + CHARS[3750] = 33; + CHARS[3751] = -19; + Arrays.fill(CHARS, 3752, 3754, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3754, 3756, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3756] = 33; + Arrays.fill(CHARS, 3757, 3759, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3759] = 33; + CHARS[3760] = -19; + CHARS[3761] = -87; + Arrays.fill(CHARS, 3762, 3764, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3764, 3770, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[3770] = 33; + Arrays.fill(CHARS, 3771, 3773, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3773] = -19; + Arrays.fill(CHARS, 3774, 3776, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3776, 3781, (byte) -19 ); // Fill 5 of value (byte) -19 + CHARS[3781] = 33; + CHARS[3782] = -87; + CHARS[3783] = 33; + Arrays.fill(CHARS, 3784, 3790, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3790, 3792, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3792, 3802, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3802, 3864, (byte) 33 ); // Fill 62 of value (byte) 33 + Arrays.fill(CHARS, 3864, 3866, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3866, 3872, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 3872, 3882, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3882, 3893, (byte) 33 ); // Fill 11 of value (byte) 33 + CHARS[3893] = -87; + CHARS[3894] = 33; + CHARS[3895] = -87; + CHARS[3896] = 33; + CHARS[3897] = -87; + Arrays.fill(CHARS, 3898, 3902, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3902, 3904, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3904, 3912, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3912] = 33; + Arrays.fill(CHARS, 3913, 3946, (byte) -19 ); // Fill 33 of value (byte) -19 + Arrays.fill(CHARS, 3946, 3953, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3953, 3973, (byte) -87 ); // Fill 20 of value (byte) -87 + CHARS[3973] = 33; + Arrays.fill(CHARS, 3974, 3980, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3980, 3984, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3984, 3990, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[3990] = 33; + CHARS[3991] = -87; + CHARS[3992] = 33; + Arrays.fill(CHARS, 3993, 4014, (byte) -87 ); // Fill 21 of value (byte) -87 + Arrays.fill(CHARS, 4014, 4017, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4017, 4024, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[4024] = 33; + CHARS[4025] = -87; + Arrays.fill(CHARS, 4026, 4256, (byte) 33 ); // Fill 230 of value (byte) 33 + Arrays.fill(CHARS, 4256, 4294, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 4294, 4304, (byte) 33 ); // Fill 10 of value (byte) 33 + Arrays.fill(CHARS, 4304, 4343, (byte) -19 ); // Fill 39 of value (byte) -19 + Arrays.fill(CHARS, 4343, 4352, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[4352] = -19; + CHARS[4353] = 33; + Arrays.fill(CHARS, 4354, 4356, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4356] = 33; + Arrays.fill(CHARS, 4357, 4360, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[4360] = 33; + CHARS[4361] = -19; + CHARS[4362] = 33; + Arrays.fill(CHARS, 4363, 4365, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4365] = 33; + Arrays.fill(CHARS, 4366, 4371, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 4371, 4412, (byte) 33 ); // Fill 41 of value (byte) 33 + CHARS[4412] = -19; + CHARS[4413] = 33; + CHARS[4414] = -19; + CHARS[4415] = 33; + CHARS[4416] = -19; + Arrays.fill(CHARS, 4417, 4428, (byte) 33 ); // Fill 11 of value (byte) 33 + CHARS[4428] = -19; + CHARS[4429] = 33; + CHARS[4430] = -19; + CHARS[4431] = 33; + CHARS[4432] = -19; + Arrays.fill(CHARS, 4433, 4436, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4436, 4438, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4438, 4441, (byte) 33 ); // Fill 3 of value (byte) 33 + CHARS[4441] = -19; + Arrays.fill(CHARS, 4442, 4447, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 4447, 4450, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[4450] = 33; + CHARS[4451] = -19; + CHARS[4452] = 33; + CHARS[4453] = -19; + CHARS[4454] = 33; + CHARS[4455] = -19; + CHARS[4456] = 33; + CHARS[4457] = -19; + Arrays.fill(CHARS, 4458, 4461, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4461, 4463, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4463, 4466, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4466, 4468, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4468] = 33; + CHARS[4469] = -19; + Arrays.fill(CHARS, 4470, 4510, (byte) 33 ); // Fill 40 of value (byte) 33 + CHARS[4510] = -19; + Arrays.fill(CHARS, 4511, 4520, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[4520] = -19; + Arrays.fill(CHARS, 4521, 4523, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[4523] = -19; + Arrays.fill(CHARS, 4524, 4526, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 4526, 4528, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4528, 4535, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 4535, 4537, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4537] = 33; + CHARS[4538] = -19; + CHARS[4539] = 33; + Arrays.fill(CHARS, 4540, 4547, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 4547, 4587, (byte) 33 ); // Fill 40 of value (byte) 33 + CHARS[4587] = -19; + Arrays.fill(CHARS, 4588, 4592, (byte) 33 ); // Fill 4 of value (byte) 33 + CHARS[4592] = -19; + Arrays.fill(CHARS, 4593, 4601, (byte) 33 ); // Fill 8 of value (byte) 33 + CHARS[4601] = -19; + Arrays.fill(CHARS, 4602, 7680, (byte) 33 ); // Fill 3078 of value (byte) 33 + Arrays.fill(CHARS, 7680, 7836, (byte) -19 ); // Fill 156 of value (byte) -19 + Arrays.fill(CHARS, 7836, 7840, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 7840, 7930, (byte) -19 ); // Fill 90 of value (byte) -19 + Arrays.fill(CHARS, 7930, 7936, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 7936, 7958, (byte) -19 ); // Fill 22 of value (byte) -19 + Arrays.fill(CHARS, 7958, 7960, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 7960, 7966, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 7966, 7968, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 7968, 8006, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 8006, 8008, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8008, 8014, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 8014, 8016, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8016, 8024, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[8024] = 33; + CHARS[8025] = -19; + CHARS[8026] = 33; + CHARS[8027] = -19; + CHARS[8028] = 33; + CHARS[8029] = -19; + CHARS[8030] = 33; + Arrays.fill(CHARS, 8031, 8062, (byte) -19 ); // Fill 31 of value (byte) -19 + Arrays.fill(CHARS, 8062, 8064, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8064, 8117, (byte) -19 ); // Fill 53 of value (byte) -19 + CHARS[8117] = 33; + Arrays.fill(CHARS, 8118, 8125, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[8125] = 33; + CHARS[8126] = -19; + Arrays.fill(CHARS, 8127, 8130, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8130, 8133, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[8133] = 33; + Arrays.fill(CHARS, 8134, 8141, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 8141, 8144, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8144, 8148, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 8148, 8150, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8150, 8156, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 8156, 8160, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 8160, 8173, (byte) -19 ); // Fill 13 of value (byte) -19 + Arrays.fill(CHARS, 8173, 8178, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 8178, 8181, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[8181] = 33; + Arrays.fill(CHARS, 8182, 8189, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 8189, 8400, (byte) 33 ); // Fill 211 of value (byte) 33 + Arrays.fill(CHARS, 8400, 8413, (byte) -87 ); // Fill 13 of value (byte) -87 + Arrays.fill(CHARS, 8413, 8417, (byte) 33 ); // Fill 4 of value (byte) 33 + CHARS[8417] = -87; + Arrays.fill(CHARS, 8418, 8486, (byte) 33 ); // Fill 68 of value (byte) 33 + CHARS[8486] = -19; + Arrays.fill(CHARS, 8487, 8490, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8490, 8492, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 8492, 8494, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[8494] = -19; + Arrays.fill(CHARS, 8495, 8576, (byte) 33 ); // Fill 81 of value (byte) 33 + Arrays.fill(CHARS, 8576, 8579, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 8579, 12293, (byte) 33 ); // Fill 3714 of value (byte) 33 + CHARS[12293] = -87; + CHARS[12294] = 33; + CHARS[12295] = -19; + Arrays.fill(CHARS, 12296, 12321, (byte) 33 ); // Fill 25 of value (byte) 33 + Arrays.fill(CHARS, 12321, 12330, (byte) -19 ); // Fill 9 of value (byte) -19 + Arrays.fill(CHARS, 12330, 12336, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[12336] = 33; + Arrays.fill(CHARS, 12337, 12342, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 12342, 12353, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 12353, 12437, (byte) -19 ); // Fill 84 of value (byte) -19 + Arrays.fill(CHARS, 12437, 12441, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 12441, 12443, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 12443, 12445, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 12445, 12447, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 12447, 12449, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 12449, 12539, (byte) -19 ); // Fill 90 of value (byte) -19 + CHARS[12539] = 33; + Arrays.fill(CHARS, 12540, 12543, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 12543, 12549, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 12549, 12589, (byte) -19 ); // Fill 40 of value (byte) -19 + Arrays.fill(CHARS, 12589, 19968, (byte) 33 ); // Fill 7379 of value (byte) 33 + Arrays.fill(CHARS, 19968, 40870, (byte) -19 ); // Fill 20902 of value (byte) -19 + Arrays.fill(CHARS, 40870, 44032, (byte) 33 ); // Fill 3162 of value (byte) 33 + Arrays.fill(CHARS, 44032, 55204, (byte) -19 ); // Fill 11172 of value (byte) -19 + Arrays.fill(CHARS, 55204, 55296, (byte) 33 ); // Fill 92 of value (byte) 33 + Arrays.fill(CHARS, 57344, 65534, (byte) 33 ); // Fill 8190 of value (byte) 33 + + } // () + + // + // Public static methods + // + + /** + * Returns true if the specified character is a supplemental character. + * + * @param c The character to check. + */ + public static boolean isSupplemental(int c) { + return (c >= 0x10000 && c <= 0x10FFFF); + } + + /** + * Returns true the supplemental character corresponding to the given + * surrogates. + * + * @param h The high surrogate. + * @param l The low surrogate. + */ + public static int supplemental(char h, char l) { + return (h - 0xD800) * 0x400 + (l - 0xDC00) + 0x10000; + } + + /** + * Returns the high surrogate of a supplemental character + * + * @param c The supplemental character to "split". + */ + public static char highSurrogate(int c) { + return (char) (((c - 0x00010000) >> 10) + 0xD800); + } + + /** + * Returns the low surrogate of a supplemental character + * + * @param c The supplemental character to "split". + */ + public static char lowSurrogate(int c) { + return (char) (((c - 0x00010000) & 0x3FF) + 0xDC00); + } + + /** + * Returns whether the given character is a high surrogate + * + * @param c The character to check. + */ + public static boolean isHighSurrogate(int c) { + return (0xD800 <= c && c <= 0xDBFF); + } + + /** + * Returns whether the given character is a low surrogate + * + * @param c The character to check. + */ + public static boolean isLowSurrogate(int c) { + return (0xDC00 <= c && c <= 0xDFFF); + } + + + /** + * Returns true if the specified character is valid. This method + * also checks the surrogate character range from 0x10000 to 0x10FFFF. + *

    + * If the program chooses to apply the mask directly to the + * CHARS array, then they are responsible for checking + * the surrogate character range. + * + * @param c The character to check. + */ + public static boolean isValid(int c) { + return (c < 0x10000 && (CHARS[c] & MASK_VALID) != 0) || + (0x10000 <= c && c <= 0x10FFFF); + } // isValid(int):boolean + + /** + * Returns true if the specified character is invalid. + * + * @param c The character to check. + */ + public static boolean isInvalid(int c) { + return !isValid(c); + } // isInvalid(int):boolean + + /** + * Returns true if the specified character can be considered content. + * + * @param c The character to check. + */ + public static boolean isContent(int c) { + return (c < 0x10000 && (CHARS[c] & MASK_CONTENT) != 0) || + (0x10000 <= c && c <= 0x10FFFF); + } // isContent(int):boolean + + /** + * Returns true if the specified character can be considered markup. + * Markup characters include '<', '&', and '%'. + * + * @param c The character to check. + */ + public static boolean isMarkup(int c) { + return c == '<' || c == '&' || c == '%'; + } // isMarkup(int):boolean + + /** + * Returns true if the specified character is a space character + * as defined by production [3] in the XML 1.0 specification. + * + * @param c The character to check. + */ + public static boolean isSpace(int c) { + return c <= 0x20 && (CHARS[c] & MASK_SPACE) != 0; + } // isSpace(int):boolean + + /** + * Returns true if the specified character is a valid name start + * character as defined by production [5] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isNameStart(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NAME_START) != 0; + } // isNameStart(int):boolean + + /** + * Returns true if the specified character is a valid name + * character as defined by production [4] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isName(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NAME) != 0; + } // isName(int):boolean + + /** + * Returns true if the specified character is a valid NCName start + * character as defined by production [4] in Namespaces in XML + * recommendation. + * + * @param c The character to check. + */ + public static boolean isNCNameStart(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NCNAME_START) != 0; + } // isNCNameStart(int):boolean + + /** + * Returns true if the specified character is a valid NCName + * character as defined by production [5] in Namespaces in XML + * recommendation. + * + * @param c The character to check. + */ + public static boolean isNCName(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NCNAME) != 0; + } // isNCName(int):boolean + + /** + * Returns true if the specified character is a valid Pubid + * character as defined by production [13] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isPubid(int c) { + return c < 0x10000 && (CHARS[c] & MASK_PUBID) != 0; + } // isPubid(int):boolean + + /* + * [5] Name ::= (Letter | '_' | ':') (NameChar)* + */ + /** + * Check to see if a string is a valid Name according to [5] + * in the XML 1.0 Recommendation + * + * @param name string to check + * @return true if name is a valid Name + */ + public static boolean isValidName(String name) { + if (name.length() == 0) + return false; + char ch = name.charAt(0); + if( isNameStart(ch) == false) + return false; + for (int i = 1; i < name.length(); i++ ) { + ch = name.charAt(i); + if( isName( ch ) == false ){ + return false; + } + } + return true; + } // isValidName(String):boolean + + + /* + * from the namespace rec + * [4] NCName ::= (Letter | '_') (NCNameChar)* + */ + /** + * Check to see if a string is a valid NCName according to [4] + * from the XML Namespaces 1.0 Recommendation + * + * @param ncName string to check + * @return true if name is a valid NCName + */ + public static boolean isValidNCName(String ncName) { + if (ncName.length() == 0) + return false; + char ch = ncName.charAt(0); + if( isNCNameStart(ch) == false) + return false; + for (int i = 1; i < ncName.length(); i++ ) { + ch = ncName.charAt(i); + if( isNCName( ch ) == false ){ + return false; + } + } + return true; + } // isValidNCName(String):boolean + + /* + * [7] Nmtoken ::= (NameChar)+ + */ + /** + * Check to see if a string is a valid Nmtoken according to [7] + * in the XML 1.0 Recommendation + * + * @param nmtoken string to check + * @return true if nmtoken is a valid Nmtoken + */ + public static boolean isValidNmtoken(String nmtoken) { + if (nmtoken.length() == 0) + return false; + for (int i = 0; i < nmtoken.length(); i++ ) { + char ch = nmtoken.charAt(i); + if( ! isName( ch ) ){ + return false; + } + } + return true; + } // isValidName(String):boolean + + + + + + // encodings + + /** + * Returns true if the encoding name is a valid IANA encoding. + * This method does not verify that there is a decoder available + * for this encoding, only that the characters are valid for an + * IANA encoding name. + * + * @param ianaEncoding The IANA encoding name. + */ + public static boolean isValidIANAEncoding(String ianaEncoding) { + if (ianaEncoding != null) { + int length = ianaEncoding.length(); + if (length > 0) { + char c = ianaEncoding.charAt(0); + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + for (int i = 1; i < length; i++) { + c = ianaEncoding.charAt(i); + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && + (c < '0' || c > '9') && c != '.' && c != '_' && + c != '-') { + return false; + } + } + return true; + } + } + } + return false; + } // isValidIANAEncoding(String):boolean + + /** + * Returns true if the encoding name is a valid Java encoding. + * This method does not verify that there is a decoder available + * for this encoding, only that the characters are valid for an + * Java encoding name. + * + * @param javaEncoding The Java encoding name. + */ + public static boolean isValidJavaEncoding(String javaEncoding) { + if (javaEncoding != null) { + int length = javaEncoding.length(); + if (length > 0) { + for (int i = 1; i < length; i++) { + char c = javaEncoding.charAt(i); + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && + (c < '0' || c > '9') && c != '.' && c != '_' && + c != '-') { + return false; + } + } + return true; + } + } + return false; + } // isValidIANAEncoding(String):boolean + + +} // class XMLChar diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/XMLUtil.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/XMLUtil.java new file mode 100644 index 00000000000..1435a5d6c5e --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/XMLUtil.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.util; + +import org.w3c.dom.Attr; +import org.w3c.dom.CharacterData; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * XMLUtil... + */ +public class XMLUtil { + + /** + * @param node + * @return true if the given node is of type text or CDATA. + */ + public static boolean isText(Node node) { + int ntype = node.getNodeType(); + return ntype == Node.TEXT_NODE || ntype == Node.CDATA_SECTION_NODE; + } + + /** + * Concatenates the values of all child nodes of type 'Text' or 'CDATA'/ + * + * @param element + * @return String representing the value of all Text and CDATA child nodes or + * null if the length of the resulting String is 0. + * @see #isText(org.w3c.dom.Node) + */ + public static String getText(Element element) { + StringBuilder content = new StringBuilder(); + if (element != null) { + NodeList nodes = element.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + Node child = nodes.item(i); + if (isText(child)) { + // cast to super class that contains Text and CData + content.append(((CharacterData) child).getData()); + } + } + } + return (content.length()==0) ? null : content.toString(); + } + + /** + * Same as {@link #getText(Element)} except that 'defaultValue' is returned + * instead of null, if the element does not contain any text. + * + * @param element + * @param defaultValue + * @return the text contained in the specified element or + * defaultValue if the element does not contain any text. + */ + public static String getText(Element element, String defaultValue) { + String txt = getText(element); + return (txt == null) ? defaultValue : txt; + } + + /** + * Calls {@link #getText(Element)} on the first child element that matches + * the given local name and namespace. + * + * @param parent + * @param childLocalName + * @param childNamespaceURI + * @return text contained in the first child that matches the given local name + * and namespace or null. + * @see #getText(Element) + */ + public static String getChildText(Element parent, String childLocalName, String childNamespaceURI) { + Element child = getChildElement(parent, childLocalName, childNamespaceURI); + return (child == null) ? null : getText(child); + } + + /** + * Returns the first child element that matches the given local name and + * namespace. If no child element is present or no child element matches, + * null is returned. + * + * @param parent + * @param childLocalName + * @param childNamespaceURI + * @return first child element matching the specified names or null. + */ + public static Element getChildElement(Node parent, String childLocalName, String childNamespaceURI) { + if (parent != null) { + NodeList children = parent.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node child = children.item(i); + if (child.getNodeType() == Node.ELEMENT_NODE && childLocalName.equals(child.getLocalName()) && childNamespaceURI.equals(child.getNamespaceURI())) { + return (Element)child; + } + } + } + return null; + } + + /** + * Returns the value of the named attribute of the current element. + * + * @param parent + * @param localName attribute local name or 'nodeName' if no namespace is + * specified. + * @param namespaceURI or null + * @return attribute value, or null if not found + */ + public static String getAttribute(Element parent, String localName, String namespaceURI) { + if (parent == null) { + return null; + } + Attr attribute; + if (namespaceURI == null) { + attribute = parent.getAttributeNode(localName); + } else { + attribute = parent.getAttributeNodeNS(namespaceURI, localName); + } + if (attribute != null) { + return attribute.getValue(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/package-info.java new file mode 100644 index 00000000000..c1a03a6d6e1 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/util/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4") +package org.apache.jackrabbit.util; diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/AbstractValueFactory.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/AbstractValueFactory.java new file mode 100644 index 00000000000..d9fa5e79d47 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/AbstractValueFactory.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.value; + +import java.io.InputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Calendar; + +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; + +/** + * This class implements the ValueFactory interface. + * + * @see javax.jcr.Session#getValueFactory() + */ +public abstract class AbstractValueFactory implements ValueFactory { + + /** + * Constructs a ValueFactory object. + */ + protected AbstractValueFactory() { + } + + /** + * Checks the format of the given string representing a path value. + * Implementations must assert that the given value is a valid jcr path. + * + * @param pathValue + * @throws javax.jcr.ValueFormatException If the given pathValue + * isn't a valid jcr path. + */ + protected abstract void checkPathFormat(String pathValue) throws ValueFormatException; + + /** + * Checks the format of the given string representing a name value. + * Implementations must assert that the given value is a valid jcr name. + * + * @param nameValue + * @throws javax.jcr.ValueFormatException If the given pathValue + * isn't a valid jcr name. + */ + protected abstract void checkNameFormat(String nameValue) throws ValueFormatException; + + //---------------------------------------------------------< ValueFactory > + /** + * {@inheritDoc} + */ + public Value createValue(boolean value) { + return new BooleanValue(value); + } + + /** + * {@inheritDoc} + */ + public Value createValue(Calendar value) { + return new DateValue(value); + } + + /** + * {@inheritDoc} + */ + public Value createValue(double value) { + return new DoubleValue(value); + } + + /** + * {@inheritDoc} + */ + public Value createValue(InputStream value) { + try { + return new BinaryValue(value); + } finally { + // JCR-2903 + try { value.close(); } catch (IOException ignore) {} + } + } + + /** + * {@inheritDoc} + */ + public Value createValue(long value) { + return new LongValue(value); + } + + /** + * {@inheritDoc} + */ + public Value createValue(Node value) throws RepositoryException { + return createValue(value, false); + } + + /** + * {@inheritDoc} + */ + public Value createValue(String value) { + return new StringValue(value); + } + + /** + * {@inheritDoc} + */ + public Value createValue(String value, int type) + throws ValueFormatException { + Value val; + switch (type) { + case PropertyType.STRING: + val = new StringValue(value); + break; + case PropertyType.BOOLEAN: + val = BooleanValue.valueOf(value); + break; + case PropertyType.DOUBLE: + val = DoubleValue.valueOf(value); + break; + case PropertyType.LONG: + val = LongValue.valueOf(value); + break; + case PropertyType.DECIMAL: + val = DecimalValue.valueOf(value); + break; + case PropertyType.DATE: + val = DateValue.valueOf(value); + break; + case PropertyType.NAME: + checkNameFormat(value); + val = NameValue.valueOf(value); + break; + case PropertyType.PATH: + checkPathFormat(value); + val = PathValue.valueOf(value); + break; + case PropertyType.URI: + val = URIValue.valueOf(value); + break; + case PropertyType.REFERENCE: + val = ReferenceValue.valueOf(value); + break; + case PropertyType.WEAKREFERENCE: + val = WeakReferenceValue.valueOf(value); + break; + case PropertyType.BINARY: + val = new BinaryValue(value); + break; + default: + throw new IllegalArgumentException("Invalid type constant: " + type); + } + return val; + } + + /** + * {@inheritDoc} + */ + public Binary createBinary(InputStream stream) throws RepositoryException { + try { + return new BinaryImpl(stream); + } catch (IOException e) { + throw new RepositoryException("failed to create Binary instance", e); + } finally { + // JCR-2903 + try { stream.close(); } catch (IOException ignore) {} + } + } + + /** + * {@inheritDoc} + */ + public Value createValue(Binary value) { + return new BinaryValue(value); + } + + /** + * {@inheritDoc} + */ + public Value createValue(BigDecimal value) { + return new DecimalValue(value); + } + + /** + * {@inheritDoc} + */ + public Value createValue(Node node, boolean weak) + throws RepositoryException { + if (weak) { + return new WeakReferenceValue(node); + } else { + return new ReferenceValue(node); + } + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BaseValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BaseValue.java new file mode 100644 index 00000000000..cc0ea260de7 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BaseValue.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.value; + +import org.apache.jackrabbit.util.ISO8601; + +import javax.jcr.Binary; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Calendar; + +/** + * This class is the superclass of the type-specific + * classes implementing the Value interfaces. + * + * @see javax.jcr.Value + * @see StringValue + * @see LongValue + * @see DoubleValue + * @see DecimalValue + * @see BooleanValue + * @see DateValue + * @see BinaryValue + * @see NameValue + * @see PathValue + * @see URIValue + * @see ReferenceValue + * @see WeakReferenceValue + */ +public abstract class BaseValue implements Value { + + protected static final String DEFAULT_ENCODING = "UTF-8"; + + protected final int type; + + protected InputStream stream = null; + + /** + * Package-private default constructor. + * + * @param type The type of this value. + */ + BaseValue(int type) { + this.type = type; + } + + /** + * Returns the internal string representation of this value without modifying + * the value state. + * + * @return the internal string representation + * @throws javax.jcr.ValueFormatException if the value can not be represented as a + * String or if the value is + * null. + * @throws javax.jcr.RepositoryException if another error occurs. + */ + protected abstract String getInternalString() + throws ValueFormatException, RepositoryException; + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public int getType() { + return type; + } + + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + Calendar cal = ISO8601.parse(getInternalString()); + if (cal == null) { + throw new ValueFormatException("not a valid date format"); + } else { + return cal; + } + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + try { + return Long.parseLong(getInternalString()); + } catch (NumberFormatException e) { + throw new ValueFormatException("conversion to long failed", e); + } + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + return Boolean.valueOf(getInternalString()); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + try { + return Double.parseDouble(getInternalString()); + } catch (NumberFormatException e) { + throw new ValueFormatException("conversion to double failed", e); + } + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + try { + return new BigDecimal(getInternalString()); + } catch (NumberFormatException e) { + throw new ValueFormatException("conversion to Decimal failed", e); + } + } + + /** + * {@inheritDoc} + */ + public InputStream getStream() + throws IllegalStateException, RepositoryException { + if (stream != null) { + return stream; + } + + try { + // convert via string + stream = new ByteArrayInputStream(getInternalString().getBytes(DEFAULT_ENCODING)); + return stream; + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(DEFAULT_ENCODING + + " not supported on this platform", e); + } + } + + /** + * {@inheritDoc} + */ + public Binary getBinary() + throws ValueFormatException, IllegalStateException, + RepositoryException { + try { + // convert via string + return new BinaryImpl(new ByteArrayInputStream(getInternalString().getBytes(DEFAULT_ENCODING))); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(DEFAULT_ENCODING + + " not supported on this platform", e); + } catch (IOException e) { + throw new RepositoryException("failed to create Binary instance", e); + } + } + + /** + * {@inheritDoc} + */ + public String getString() + throws ValueFormatException, IllegalStateException, + RepositoryException { + return getInternalString(); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BinaryImpl.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BinaryImpl.java new file mode 100644 index 00000000000..d79411e55b0 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BinaryImpl.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.value; + +import org.apache.jackrabbit.util.TransientFileFactory; + +import javax.jcr.Binary; +import javax.jcr.RepositoryException; +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.RandomAccessFile; +import java.io.FileNotFoundException; +import java.io.ByteArrayInputStream; + +/** + * BinaryImpl implements the Binary interface. + */ +public class BinaryImpl implements Binary { + + /** + * empty array + */ + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * max size for keeping tmp data in memory + */ + private static final int MAX_BUFFER_SIZE = 0x10000; + + /** + * underlying tmp file + */ + private final File tmpFile; + + /** + * buffer for small-sized data + */ + private byte[] buffer = EMPTY_BYTE_ARRAY; + + /** + * Creates a new BinaryImpl instance from an + * InputStream. The contents of the stream is spooled + * to a temporary file or to a byte buffer if its size is smaller than + * {@link #MAX_BUFFER_SIZE}. + *

    + * @param in stream to be represented as a BLOBFileValue instance + * @throws IOException if an error occurs while reading from the stream or + * writing to the temporary file + */ + public BinaryImpl(InputStream in) throws IOException { + byte[] spoolBuffer = new byte[0x2000]; + int read; + int len = 0; + OutputStream out = null; + File spoolFile = null; + try { + while ((read = in.read(spoolBuffer)) > 0) { + if (out != null) { + // spool to temp file + out.write(spoolBuffer, 0, read); + len += read; + } else if (len + read > MAX_BUFFER_SIZE) { + // threshold for keeping data in memory exceeded; + // create temp file and spool buffer contents + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + spoolFile = fileFactory.createTransientFile("bin", null, null); + out = new FileOutputStream(spoolFile); + out.write(buffer, 0, len); + out.write(spoolBuffer, 0, read); + buffer = null; + len += read; + } else { + // reallocate new buffer and spool old buffer contents + byte[] newBuffer = new byte[len + read]; + System.arraycopy(buffer, 0, newBuffer, 0, len); + System.arraycopy(spoolBuffer, 0, newBuffer, len, read); + buffer = newBuffer; + len += read; + } + } + } finally { + if (out != null) { + out.close(); + } + } + + // init fields + tmpFile = spoolFile; + } + + /** + * Creates a new BinaryImpl instance from a + * byte[] array. + * + * @param buffer byte array to be represented as a BinaryImpl + * instance + */ + public BinaryImpl(byte[] buffer) { + if (buffer == null) { + throw new IllegalArgumentException("buffer must be non-null"); + } + this.buffer = buffer; + tmpFile = null; + } + + /** + * {@inheritDoc} + */ + public InputStream getStream() throws RepositoryException { + if (tmpFile != null) { + try { + // this instance is backed by a temp file + return new FileInputStream(tmpFile); + } catch (FileNotFoundException e) { + throw new RepositoryException("already disposed"); + } + } else { + // this instance is backed by an in-memory buffer + return new ByteArrayInputStream(buffer); + } + } + + /** + * {@inheritDoc} + */ + public int read(byte[] b, long position) throws IOException, RepositoryException { + if (tmpFile != null) { + // this instance is backed by a temp file + RandomAccessFile raf = new RandomAccessFile(tmpFile, "r"); + try { + raf.seek(position); + return raf.read(b); + } finally { + raf.close(); + } + } else { + // this instance is backed by an in-memory buffer + int length = Math.min(b.length, buffer.length - (int) position); + if (length > 0) { + System.arraycopy(buffer, (int) position, b, 0, length); + return length; + } else { + return -1; + } + } + } + + /** + * {@inheritDoc} + */ + public long getSize() throws RepositoryException { + if (tmpFile != null) { + // this instance is backed by a temp file + if (tmpFile.exists()) { + return tmpFile.length(); + } else { + return -1; + } + } else { + // this instance is backed by an in-memory buffer + return buffer.length; + } + } + + /** + * {@inheritDoc} + */ + public void dispose() { + if (tmpFile != null) { + // this instance is backed by a temp file + tmpFile.delete(); + } else { + // this instance is backed by an in-memory buffer + buffer = EMPTY_BYTE_ARRAY; + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BinaryValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BinaryValue.java new file mode 100644 index 00000000000..4d056157f3e --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BinaryValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.Binary; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +/** + * A BinaryValue provides an implementation + * of the Value interface representing a binary value. + */ +public class BinaryValue extends BaseValue { + + public static final int TYPE = PropertyType.BINARY; + + private Binary bin = null; + private String text = null; + + /** + * Constructs a BinaryValue object based on a string. + * + * @param text the string this BinaryValue should represent + */ + public BinaryValue(String text) { + super(TYPE); + this.text = text; + } + + /** + * Constructs a BinaryValue object based on a Binary. + * + * @param bin the bytes this BinaryValue should represent + */ + public BinaryValue(Binary bin) { + super(TYPE); + this.bin = bin; + } + + /** + * Constructs a BinaryValue object based on a stream. + * + * @param stream the stream this BinaryValue should represent + */ + public BinaryValue(InputStream stream) { + super(TYPE); + try { + bin = new BinaryImpl(stream); + } catch (IOException e) { + throw new IllegalArgumentException("specified stream cannot be read", e); + } + } + + /** + * Constructs a BinaryValue object based on a byte array. + * + * @param data the bytes this BinaryValue should represent + */ + public BinaryValue(byte[] data) { + super(TYPE); + bin = new BinaryImpl(data); + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a BinaryValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BinaryValue) { + BinaryValue other = (BinaryValue) obj; + if (text == other.text && stream == other.stream + && bin == other.bin) { + return true; + } + return (text != null && text.equals(other.text)) + || (bin != null && bin.equals(other.bin)); + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * Gets the string representation of this binary value. + * + * @return string representation of this binary value. + * + * @throws javax.jcr.ValueFormatException + * @throws javax.jcr.RepositoryException if another error occurs + */ + public String getInternalString() + throws ValueFormatException, RepositoryException { + // build text value if necessary + if (text == null) { + try { + byte[] bytes = new byte[(int) bin.getSize()]; + bin.read(bytes, 0); + text = new String(bytes, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(DEFAULT_ENCODING + + " not supported on this platform", e); + } catch (IOException e) { + throw new RepositoryException("failed to retrieve binary data", e); + } + } + + return text; + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public InputStream getStream() + throws IllegalStateException, RepositoryException { + if (stream == null) { + if (bin != null) { + stream = bin.getStream(); + } else { + try { + stream = new ByteArrayInputStream(text.getBytes(DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(DEFAULT_ENCODING + + " not supported on this platform", e); + } + } + } + + return stream; + } + + /** + * {@inheritDoc} + */ + public Binary getBinary() + throws ValueFormatException, IllegalStateException, + RepositoryException { + + if (bin == null) { + try { + bin = new BinaryImpl(new ByteArrayInputStream(text.getBytes(DEFAULT_ENCODING))); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(DEFAULT_ENCODING + + " not supported on this platform", e); + } catch (IOException e) { + throw new RepositoryException("failed to retrieve binary data", e); + } + } + + return bin; + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BooleanValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BooleanValue.java new file mode 100644 index 00000000000..1f1e269cd62 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/BooleanValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.math.BigDecimal; + +/** + * A BooleanValue provides an implementation + * of the Value interface representing a boolean value. + */ +public class BooleanValue extends BaseValue { + + public static final int TYPE = PropertyType.BOOLEAN; + + private final Boolean bool; + + /** + * Constructs a BooleanValue object representing a boolean. + * + * @param bool the boolean this BooleanValue should represent + */ + public BooleanValue(Boolean bool) { + super(TYPE); + this.bool = bool; + } + + /** + * Constructs a BooleanValue object representing a boolean. + * + * @param bool the boolean this BooleanValue should represent + */ + public BooleanValue(boolean bool) { + super(TYPE); + this.bool = bool; + } + + /** + * Returns a new BooleanValue initialized to the value + * represented by the specified String. + * + * @param s the string to be parsed. + * @return a newly constructed BooleanValue representing the + * the specified value. + */ + public static BooleanValue valueOf(String s) { + return new BooleanValue(Boolean.valueOf(s)); + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a BooleanValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BooleanValue) { + BooleanValue other = (BooleanValue) obj; + if (bool == other.bool) { + return true; + } else if (bool != null && other.bool != null) { + return bool.equals(other.bool); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (bool != null) { + return bool.toString(); + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to date failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to long failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (bool != null) { + return bool; + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to double failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to Decimal failed: inconvertible types"); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DateValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DateValue.java new file mode 100644 index 00000000000..ea790f7a6f7 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DateValue.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.value; + +import org.apache.jackrabbit.util.ISO8601; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.math.BigDecimal; + +/** + * A DateValue provides an implementation + * of the Value interface representing a date value. + */ +public class DateValue extends BaseValue { + + public static final int TYPE = PropertyType.DATE; + + private final Calendar date; + + /** + * Constructs a DateValue object representing a date. + * + * @param date the date this DateValue should represent + * @throws IllegalArgumentException if the given date cannot be represented + * as defined by ISO 8601. + */ + public DateValue(Calendar date) throws IllegalArgumentException { + super(TYPE); + this.date = date; + ISO8601.getYear(date); + } + + /** + * Returns a new DateValue initialized to the value + * represented by the specified String. + *

    + * The specified String must be a ISO8601-compliant date/time + * string. + * + * @param s the string to be parsed. + * @return a newly constructed DateValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String is not a valid + * ISO8601-compliant date/time string. + * @see ISO8601 + */ + public static DateValue valueOf(String s) throws ValueFormatException { + Calendar cal = ISO8601.parse(s); + if (cal != null) { + return new DateValue(cal); + } else { + throw new ValueFormatException("not a valid date format: " + s); + } + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a DateValue object that + * represents the same value as this object. + *

    + * The value comparison is performed using the ISO 8601 string + * representation of the dates, since the native Calendar.equals() + * method may produce false negatives (see JSR-598). + *

    + * Note that the comparison still returns false when comparing the + * same time in different time zones, but that seems to be the intent + * of JSR 170. Compare the Value.getDate().getTime() values if you need + * an exact time comparison in UTC. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof DateValue) { + DateValue other = (DateValue) obj; + if (date == other.date) { + return true; + } else if (date != null && other.date != null) { + return ISO8601.format(date).equals(ISO8601.format(other.date)); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (date != null) { + return ISO8601.format(date); + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (date != null) { + return (Calendar) date.clone(); + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (date != null) { + return date.getTimeInMillis(); + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (date != null) { + throw new ValueFormatException("cannot convert date to boolean"); + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (date != null) { + long ms = date.getTimeInMillis(); + if (ms <= Double.MAX_VALUE) { + return ms; + } + throw new ValueFormatException("conversion from date to double failed: inconvertible types"); + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (date != null) { + return new BigDecimal(date.getTimeInMillis()); + } else { + throw new ValueFormatException("empty value"); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DecimalValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DecimalValue.java new file mode 100644 index 00000000000..03051f60c14 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DecimalValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.util.Date; +import java.math.BigDecimal; + +/** + * A DecimalValue provides an implementation + * of the Value interface representing a DECIMAL value. + */ +public class DecimalValue extends BaseValue { + + public static final int TYPE = PropertyType.DECIMAL; + + private final BigDecimal number; + + /** + * Constructs a DecimalValue object representing a decimal. + * + * @param number the decimal this DecimalValue should represent + */ + public DecimalValue(BigDecimal number) { + super(TYPE); + this.number = number; + } + + /** + * Returns a new DecimalValue initialized to the value + * represented by the specified String. + * + * @param s the string to be parsed. + * @return a newly constructed DecimalValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String does not + * contain a parsable decimal. + */ + public static DecimalValue valueOf(String s) throws ValueFormatException { + try { + return new DecimalValue(new BigDecimal(s)); + } catch (NumberFormatException e) { + throw new ValueFormatException("not a valid decimal format: " + s, e); + } + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a DecimalValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof DecimalValue) { + DecimalValue other = (DecimalValue) obj; + if (number == other.number) { + return true; + } else if (number != null && other.number != null) { + return number.compareTo(other.number) == 0; + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (number != null) { + return number.toString(); + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (number != null) { + // loosing timezone information... + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date(number.longValue())); + return cal; + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (number != null) { + return number.longValue(); + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to boolean failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (number != null) { + return number.doubleValue(); + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (number != null) { + return number; + } else { + throw new ValueFormatException("empty value"); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DoubleValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DoubleValue.java new file mode 100644 index 00000000000..2a158ee9c43 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/DoubleValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.util.Date; +import java.math.BigDecimal; + +/** + * A DoubleValue provides an implementation + * of the Value interface representing a double value. + */ +public class DoubleValue extends BaseValue { + + public static final int TYPE = PropertyType.DOUBLE; + + private final Double dblNumber; + + /** + * Constructs a DoubleValue object representing a double. + * + * @param dblNumber the double this DoubleValue should represent + */ + public DoubleValue(Double dblNumber) { + super(TYPE); + this.dblNumber = dblNumber; + } + + /** + * Constructs a DoubleValue object representing a double. + * + * @param dbl the double this DoubleValue should represent + */ + public DoubleValue(double dbl) { + super(TYPE); + this.dblNumber = dbl; + } + + /** + * Returns a new DoubleValue initialized to the value + * represented by the specified String. + * + * @param s the string to be parsed. + * @return a newly constructed DoubleValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String does not + * contain a parsable double. + */ + public static DoubleValue valueOf(String s) throws ValueFormatException { + try { + return new DoubleValue(Double.parseDouble(s)); + } catch (NumberFormatException e) { + throw new ValueFormatException("not a valid double format: " + s, e); + } + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a DoubleValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof DoubleValue) { + DoubleValue other = (DoubleValue) obj; + if (dblNumber == other.dblNumber) { + return true; + } else if (dblNumber != null && other.dblNumber != null) { + return dblNumber.equals(other.dblNumber); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (dblNumber != null) { + return dblNumber.toString(); + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (dblNumber != null) { + // loosing timezone information... + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date(dblNumber.longValue())); + return cal; + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (dblNumber != null) { + return dblNumber.longValue(); + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to boolean failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (dblNumber != null) { + return dblNumber; + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (dblNumber != null) { + return new BigDecimal(dblNumber); + } else { + throw new ValueFormatException("empty value"); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/LongValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/LongValue.java new file mode 100644 index 00000000000..5255151b651 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/LongValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.util.Date; +import java.math.BigDecimal; + +/** + * A LongValue provides an implementation + * of the Value interface representing a long value. + */ +public class LongValue extends BaseValue { + + public static final int TYPE = PropertyType.LONG; + + private final Long lNumber; + + /** + * Constructs a LongValue object representing a long. + * + * @param lNumber the long this LongValue should represent + */ + public LongValue(Long lNumber) { + super(TYPE); + this.lNumber = lNumber; + } + + /** + * Constructs a LongValue object representing a long. + * + * @param l the long this LongValue should represent + */ + public LongValue(long l) { + super(TYPE); + this.lNumber = l; + } + + /** + * Returns a new LongValue initialized to the value + * represented by the specified String. + * + * @param s the string to be parsed. + * @return a newly constructed LongValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String does not + * contain a parsable long. + */ + public static LongValue valueOf(String s) throws ValueFormatException { + try { + return new LongValue(Long.parseLong(s)); + } catch (NumberFormatException e) { + throw new ValueFormatException("not a valid long format: " + s, e); + } + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a LongValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof LongValue) { + LongValue other = (LongValue) obj; + if (lNumber == other.lNumber) { + return true; + } else if (lNumber != null && other.lNumber != null) { + return lNumber.equals(other.lNumber); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (lNumber != null) { + return lNumber.toString(); + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (lNumber != null) { + // loosing timezone information... + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date(lNumber)); + return cal; + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (lNumber != null) { + return lNumber; + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to boolean failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (lNumber != null) { + return lNumber.doubleValue(); + } else { + throw new ValueFormatException("empty value"); + } + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + if (lNumber != null) { + return new BigDecimal(lNumber); + } else { + throw new ValueFormatException("empty value"); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/NameValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/NameValue.java new file mode 100644 index 00000000000..a1b7518f3ed --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/NameValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.math.BigDecimal; + +/** + * A NameValue provides an implementation + * of the Value interface representing a NAME value + * (a string that is namespace-qualified). + */ +public class NameValue extends BaseValue { + + public static final int TYPE = PropertyType.NAME; + + private final String name; + + /** + * Returns a new NameValue initialized to the value + * represented by the specified String. + *

    + * The specified String must be a valid JCR name. + * + * @param s the string to be parsed. + * @return a newly constructed NameValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String is not a valid + * name. + */ + public static NameValue valueOf(String s) throws ValueFormatException { + if (s != null) { + return new NameValue(s); + } else { + throw new ValueFormatException("not a valid name format: " + s); + } + } + + + /** + * Protected constructor creating a NameValue object + * without validating the name. + * + * @param name the name this NameValue should represent + * @see #valueOf + */ + protected NameValue(String name) { + super(TYPE); + this.name = name; + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a NameValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof NameValue) { + NameValue other = (NameValue) obj; + if (name == other.name) { + return true; + } else if (name != null && other.name != null) { + return name.equals(other.name); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (name != null) { + return name; + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to date failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to long failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to boolean failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to double failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to Decimal failed: inconvertible types"); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/PathValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/PathValue.java new file mode 100644 index 00000000000..8d7b310b859 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/PathValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.math.BigDecimal; + +/** + * A PathValue provides an implementation + * of the Value interface representing a PATH value + * (an absolute or relative workspace path). + */ +public class PathValue extends BaseValue { + + public static final int TYPE = PropertyType.PATH; + + private final String path; + + /** + * Returns a new PathValue initialized to the value + * represented by the specified String. + *

    + * The specified String must be a valid absolute or relative + * path. + * + * @param s the string to be parsed. + * @return a newly constructed PathValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String is not a valid + * absolute or relative path. + */ + public static PathValue valueOf(String s) throws ValueFormatException { + if (s != null) { + return new PathValue(s); + } else { + throw new ValueFormatException("not a valid path format: " + s); + } + } + + /** + * Protected constructor creating a PathValue object + * without validating the path. + * + * @param path the path this PathValue should represent + * @see #valueOf + */ + protected PathValue(String path) { + super(TYPE); + this.path = path; + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a PathValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof PathValue) { + PathValue other = (PathValue) obj; + if (path == other.path) { + return true; + } else if (path != null && other.path != null) { + return path.equals(other.path); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (path != null) { + return path; + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to date failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to long failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to boolean failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to double failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to Decimal failed: inconvertible types"); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ReferenceValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ReferenceValue.java new file mode 100644 index 00000000000..0ffd86cfb09 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ReferenceValue.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.value; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.util.UUID; +import java.math.BigDecimal; + +/** + * A ReferenceValue provides an implementation + * of the Value interface representing a REFERENCE value + * (a UUID of an existing node). + */ +public class ReferenceValue extends BaseValue { + + public static final int TYPE = PropertyType.REFERENCE; + + private final String uuid; + + /** + * Constructs a ReferenceValue object representing the UUID of + * an existing node. + * + * @param target the node to be referenced + * @throws IllegalArgumentException If target is nonreferenceable. + * @throws javax.jcr.RepositoryException If another error occurs. + */ + public ReferenceValue(Node target) throws RepositoryException { + super(TYPE); + try { + this.uuid = target.getUUID(); + } catch (UnsupportedRepositoryOperationException ure) { + throw new IllegalArgumentException("target is nonreferenceable."); + } + } + + /** + * Returns a new ReferenceValue initialized to the value + * represented by the specified String. + *

    + * The specified String must denote the UUID of an existing + * node. + * + * @param s the string to be parsed. + * @return a newly constructed ReferenceValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String is not a valid + * not a valid UUID format. + */ + public static ReferenceValue valueOf(String s) throws ValueFormatException { + if (s != null) { + try { + UUID.fromString(s); + } catch (IllegalArgumentException iae) { + throw new ValueFormatException("not a valid UUID format: " + s); + } + return new ReferenceValue(s); + } else { + throw new ValueFormatException("not a valid UUID format: " + s); + } + } + + /** + * Protected constructor creating a ReferenceValue object + * without validating the UUID format. + * + * @param uuid the UUID of the node to be referenced + * @see #valueOf + */ + protected ReferenceValue(String uuid) { + super(TYPE); + this.uuid = uuid; + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a ReferenceValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ReferenceValue) { + ReferenceValue other = (ReferenceValue) obj; + if (uuid == other.uuid) { + return true; + } else if (uuid != null && other.uuid != null) { + return uuid.equals(other.uuid); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (uuid != null) { + return uuid; + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to date failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to long failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to boolean failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to double failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to Decimal failed: inconvertible types"); + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/StringValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/StringValue.java new file mode 100644 index 00000000000..f8eae38f3ce --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/StringValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.ValueFormatException; + +/** + * A StringValue provides an implementation + * of the Value interface representing a string value. + */ +public class StringValue extends BaseValue { + + public static final int TYPE = PropertyType.STRING; + + private final String text; + + /** + * Constructs a StringValue object representing a string. + * + * @param text the string this StringValue should represent + */ + public StringValue(String text) { + super(TYPE); + this.text = text; + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a StringValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof StringValue) { + StringValue other = (StringValue) obj; + if (text == other.text) { + return true; + } else if (text != null && other.text != null) { + return text.equals(other.text); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (text != null) { + return text; + } else { + throw new ValueFormatException("empty value"); + } + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/URIValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/URIValue.java new file mode 100644 index 00000000000..be919e840d2 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/URIValue.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.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.net.URI; +import java.net.URISyntaxException; +import java.math.BigDecimal; + +/** + * A URIValue provides an implementation + * of the Value interface representing a URI value + * (an absolute or relative workspace path). + */ +public class URIValue extends BaseValue { + + public static final int TYPE = PropertyType.URI; + + private final URI uri; + + /** + * Returns a new URIValue initialized to the value + * represented by the specified String. + *

    + * The specified String must be a valid URI. + * + * @param s the string to be parsed. + * @return a newly constructed URIValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String is not a valid URI. + */ + public static URIValue valueOf(String s) throws ValueFormatException { + if (s != null) { + try { + return new URIValue(new URI(s)); + } catch (URISyntaxException e) { + throw new ValueFormatException(e.getMessage()); + } + } else { + throw new ValueFormatException("not a valid uri format: " + s); + } + } + + /** + * Returns a new URIValue initialized to the value of the + * specified URI. + * + * @param uri the path this URIValue should represent + * @see #valueOf + */ + public URIValue(URI uri) { + super(TYPE); + this.uri = uri; + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a URIValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof URIValue) { + URIValue other = (URIValue) obj; + if (uri == other.uri) { + return true; + } else if (uri != null && other.uri != null) { + return uri.equals(other.uri); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (uri != null) { + return uri.toString(); + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to date failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to long failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to boolean failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to double failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to Decimal failed: inconvertible types"); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ValueFactoryImpl.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ValueFactoryImpl.java new file mode 100644 index 00000000000..20c3e3a5094 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ValueFactoryImpl.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.value; + +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; + +/** + * Simple extension of the AbstractValueFactory that omits any + * validation checks for path and name values. + * + * @see javax.jcr.Session#getValueFactory() + */ +public class ValueFactoryImpl extends AbstractValueFactory { + + private static final ValueFactory valueFactory = new ValueFactoryImpl(); + + /** + * Constructs a ValueFactory object. + */ + protected ValueFactoryImpl() { + } + + /** + * + */ + public static ValueFactory getInstance() { + return valueFactory; + } + + /** + * @see AbstractValueFactory#checkPathFormat(String) + */ + protected void checkPathFormat(String pathValue) throws ValueFormatException { + // ignore + } + + /** + * @see AbstractValueFactory#checkNameFormat(String) + */ + protected void checkNameFormat(String nameValue) throws ValueFormatException { + // ignore + } +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ValueHelper.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ValueHelper.java new file mode 100644 index 00000000000..17d202bb3cd --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/ValueHelper.java @@ -0,0 +1,903 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.value; + +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.UNDEFINED; +import static javax.jcr.PropertyType.WEAKREFERENCE; + +import org.apache.jackrabbit.util.Base64; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.util.TransientFileFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.ValueFactory; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.FilterInputStream; +import java.io.OutputStream; +import java.io.BufferedOutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * The ValueHelper class provides several Value + * related utility methods. + */ +public class ValueHelper { + + /** + * empty private constructor + */ + private ValueHelper() { + } + + private static final Map> SUPPORTED_CONVERSIONS = new HashMap>(); + static { + SUPPORTED_CONVERSIONS.put(DATE, immutableSetOf(STRING, BINARY, DOUBLE, DECIMAL, LONG)); + SUPPORTED_CONVERSIONS.put(DOUBLE, immutableSetOf(STRING, BINARY, DECIMAL, DATE, LONG)); + SUPPORTED_CONVERSIONS.put(DECIMAL, immutableSetOf(STRING, BINARY, DOUBLE, DATE, LONG)); + SUPPORTED_CONVERSIONS.put(LONG, immutableSetOf(STRING, BINARY, DECIMAL, DATE, DOUBLE)); + SUPPORTED_CONVERSIONS.put(BOOLEAN, immutableSetOf(STRING, BINARY)); + SUPPORTED_CONVERSIONS.put(NAME, immutableSetOf(STRING, BINARY, PATH, PropertyType.URI)); + SUPPORTED_CONVERSIONS.put(PATH, immutableSetOf(STRING, BINARY, NAME, PropertyType.URI)); + SUPPORTED_CONVERSIONS.put(PropertyType.URI, immutableSetOf(STRING, BINARY, NAME, PATH)); + SUPPORTED_CONVERSIONS.put(REFERENCE, immutableSetOf(STRING, BINARY, WEAKREFERENCE)); + SUPPORTED_CONVERSIONS.put(WEAKREFERENCE, immutableSetOf(STRING, BINARY, REFERENCE)); + } + + private static Set immutableSetOf(int... types) { + Set t = new HashSet(); + for (int type : types) { + t.add(type); + } + return Collections.unmodifiableSet(t); + } + + public static boolean isSupportedConversion(int fromType, int toType) { + if (fromType == toType) { + return true; + } else if (STRING == fromType || BINARY == fromType) { + return true; + } else { + return SUPPORTED_CONVERSIONS.containsKey(fromType) && SUPPORTED_CONVERSIONS.get(fromType).contains(toType); + } + } + + public static void checkSupportedConversion(int fromType, int toType) throws ValueFormatException { + if (!isSupportedConversion(fromType, toType)) { + throw new ValueFormatException("Unsupported conversion from '" + PropertyType.nameFromValue(fromType) + "' to '" + PropertyType.nameFromValue(toType) + '\''); + } + } + + /** + * @param srcValue + * @param targetType + * @param factory + * @throws ValueFormatException + * @throws IllegalArgumentException + * @see #convert(Value, int, ValueFactory) + */ + public static Value convert(String srcValue, int targetType, ValueFactory factory) + throws ValueFormatException, IllegalArgumentException { + if (srcValue == null) { + return null; + } else { + return factory.createValue(srcValue, targetType); + } + } + + /** + * @param srcValue + * @param targetType + * @param factory + * @throws ValueFormatException + * @throws IllegalArgumentException + */ + public static Value convert(InputStream srcValue, int targetType, ValueFactory factory) + throws ValueFormatException, IllegalArgumentException { + if (srcValue == null) { + return null; + } else { + return convert(factory.createValue(srcValue), targetType, factory); + } + } + + /** + * Same as {@link #convert(String[], int, ValueFactory)} using + * ValueFactoryImpl. + * + * @param srcValues + * @param targetType + * @throws ValueFormatException + * @throws IllegalArgumentException + * @see #convert(Value, int, ValueFactory) + */ + public static Value[] convert(String[] srcValues, int targetType, ValueFactory factory) + throws ValueFormatException, IllegalArgumentException { + if (srcValues == null) { + return null; + } + Value[] newValues = new Value[srcValues.length]; + for (int i = 0; i < srcValues.length; i++) { + newValues[i] = convert(srcValues[i], targetType, factory); + } + return newValues; + } + + /** + * @param srcValues + * @param targetType + * @throws ValueFormatException + * @throws IllegalArgumentException + * @see #convert(Value, int, ValueFactory) + */ + public static Value[] convert(InputStream[] srcValues, int targetType, + ValueFactory factory) + throws ValueFormatException, IllegalArgumentException { + if (srcValues == null) { + return null; + } + Value[] newValues = new Value[srcValues.length]; + for (int i = 0; i < srcValues.length; i++) { + newValues[i] = convert(srcValues[i], targetType, factory); + } + return newValues; + } + + /** + * @param srcValues + * @param targetType + * @param factory + * @throws ValueFormatException + * @throws IllegalArgumentException + * @see #convert(Value, int, ValueFactory) + */ + public static Value[] convert(Value[] srcValues, int targetType, + ValueFactory factory) + throws ValueFormatException, IllegalArgumentException { + if (srcValues == null) { + return null; + } + + Value[] newValues = new Value[srcValues.length]; + int srcValueType = PropertyType.UNDEFINED; + for (int i = 0; i < srcValues.length; i++) { + if (srcValues[i] == null) { + newValues[i] = null; + continue; + } + // check type of values + if (srcValueType == PropertyType.UNDEFINED) { + srcValueType = srcValues[i].getType(); + } else if (srcValueType != srcValues[i].getType()) { + // inhomogeneous types + String msg = "inhomogeneous type of values"; + throw new ValueFormatException(msg); + } + + newValues[i] = convert(srcValues[i], targetType, factory); + } + return newValues; + } + + /** + * Converts the given value to a value of the specified target type. + * The conversion is performed according to the rules described in + * "3.6.4 Property Type Conversion" in the JSR 283 specification. + * + * @param srcValue + * @param targetType + * @param factory + * @throws ValueFormatException + * @throws IllegalStateException + * @throws IllegalArgumentException + */ + public static Value convert(Value srcValue, int targetType, ValueFactory factory) + throws ValueFormatException, IllegalStateException, + IllegalArgumentException { + if (srcValue == null) { + return null; + } + + Value val; + int srcType = srcValue.getType(); + + if (srcType == targetType) { + // no conversion needed, return original value + return srcValue; + } + + switch (targetType) { + case PropertyType.STRING: + // convert to STRING + try { + val = factory.createValue(srcValue.getString()); + } catch (RepositoryException re) { + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType), re); + } + break; + + case PropertyType.BINARY: + // convert to BINARY + try { + val = factory.createValue(srcValue.getBinary()); + } catch (RepositoryException re) { + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType), re); + } + break; + + case PropertyType.BOOLEAN: + // convert to BOOLEAN + try { + val = factory.createValue(srcValue.getBoolean()); + } catch (RepositoryException re) { + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType), re); + } + break; + + case PropertyType.DATE: + // convert to DATE + try { + val = factory.createValue(srcValue.getDate()); + } catch (RepositoryException re) { + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType), re); + } + break; + + case PropertyType.DOUBLE: + // convert to DOUBLE + try { + val = factory.createValue(srcValue.getDouble()); + } catch (RepositoryException re) { + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType), re); + } + break; + + case PropertyType.LONG: + // convert to LONG + try { + val = factory.createValue(srcValue.getLong()); + } catch (RepositoryException re) { + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType), re); + } + break; + + case PropertyType.DECIMAL: + // convert to DECIMAL + try { + val = factory.createValue(srcValue.getDecimal()); + } catch (RepositoryException re) { + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType), re); + } + break; + + case PropertyType.PATH: + // convert to PATH + switch (srcType) { + case PropertyType.PATH: + // no conversion needed, return original value + // (redundant code, just here for the sake of clarity) + return srcValue; + + case PropertyType.BINARY: + case PropertyType.STRING: + case PropertyType.NAME: // a name is always also a relative path + // try conversion via string + String path; + try { + // get string value + path = srcValue.getString(); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to PATH value", + re); + } + // the following call will throw ValueFormatException + // if p is not a valid PATH + val = factory.createValue(path, targetType); + break; + + case PropertyType.URI: + URI uri; + try { + uri = URI.create(srcValue.getString()); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to PATH value", + re); + } + if (uri.isAbsolute()) { + // uri contains scheme... + throw new ValueFormatException("failed to convert URI value to PATH value"); + } + String p = uri.getPath(); + + if (p.startsWith("./")) { + p = p.substring(2); + } + + // the following call will throw ValueFormatException + // if p is not a valid PATH + val = factory.createValue(p, targetType); + break; + + case PropertyType.BOOLEAN: + case PropertyType.DATE: + case PropertyType.DOUBLE: + case PropertyType.DECIMAL: + case PropertyType.LONG: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType)); + + default: + throw new IllegalArgumentException("not a valid type constant: " + srcType); + } + break; + + case PropertyType.NAME: + // convert to NAME + switch (srcType) { + case PropertyType.NAME: + // no conversion needed, return original value + // (redundant code, just here for the sake of clarity) + return srcValue; + + case PropertyType.BINARY: + case PropertyType.STRING: + case PropertyType.PATH: // path might be a name (relative path of length 1) + // try conversion via string + String name; + try { + // get string value + name = srcValue.getString(); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to NAME value", + re); + } + // the following call will throw ValueFormatException + // if p is not a valid NAME + val = factory.createValue(name, targetType); + break; + + case PropertyType.URI: + URI uri; + try { + uri = URI.create(srcValue.getString()); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to NAME value", + re); + } + if (uri.isAbsolute()) { + // uri contains scheme... + throw new ValueFormatException("failed to convert URI value to NAME value"); + } + String p = uri.getPath(); + + if (p.startsWith("./")) { + p = p.substring(2); + } + + // the following call will throw ValueFormatException + // if p is not a valid NAME + val = factory.createValue(p, targetType); + break; + + case PropertyType.BOOLEAN: + case PropertyType.DATE: + case PropertyType.DOUBLE: + case PropertyType.DECIMAL: + case PropertyType.LONG: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType)); + + default: + throw new IllegalArgumentException("not a valid type constant: " + srcType); + } + break; + + case PropertyType.REFERENCE: + // convert to REFERENCE + switch (srcType) { + case PropertyType.REFERENCE: + // no conversion needed, return original value + // (redundant code, just here for the sake of clarity) + return srcValue; + + case PropertyType.BINARY: + case PropertyType.STRING: + case PropertyType.WEAKREFERENCE: + // try conversion via string + String uuid; + try { + // get string value + uuid = srcValue.getString(); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to REFERENCE value", re); + } + val = factory.createValue(uuid, targetType); + break; + + case PropertyType.BOOLEAN: + case PropertyType.DATE: + case PropertyType.DOUBLE: + case PropertyType.LONG: + case PropertyType.DECIMAL: + case PropertyType.PATH: + case PropertyType.URI: + case PropertyType.NAME: + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType)); + + default: + throw new IllegalArgumentException("not a valid type constant: " + srcType); + } + break; + + case PropertyType.WEAKREFERENCE: + // convert to WEAKREFERENCE + switch (srcType) { + case PropertyType.WEAKREFERENCE: + // no conversion needed, return original value + // (redundant code, just here for the sake of clarity) + return srcValue; + + case PropertyType.BINARY: + case PropertyType.STRING: + case PropertyType.REFERENCE: + // try conversion via string + String uuid; + try { + // get string value + uuid = srcValue.getString(); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to WEAKREFERENCE value", re); + } + val = factory.createValue(uuid, targetType); + break; + + case PropertyType.BOOLEAN: + case PropertyType.DATE: + case PropertyType.DOUBLE: + case PropertyType.LONG: + case PropertyType.DECIMAL: + case PropertyType.URI: + case PropertyType.PATH: + case PropertyType.NAME: + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType)); + + default: + throw new IllegalArgumentException("not a valid type constant: " + srcType); + } + break; + + case PropertyType.URI: + // convert to URI + switch (srcType) { + case PropertyType.URI: + // no conversion needed, return original value + // (redundant code, just here for the sake of clarity) + return srcValue; + + case PropertyType.BINARY: + case PropertyType.STRING: + // try conversion via string + String uuid; + try { + // get string value + uuid = srcValue.getString(); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to URI value", re); + } + val = factory.createValue(uuid, targetType); + break; + + case PropertyType.NAME: + String name; + try { + // get string value + name = srcValue.getString(); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to URI value", re); + } + // prefix name with "./" (jsr 283 spec 3.6.4.8) + val = factory.createValue("./" + name, targetType); + break; + + case PropertyType.PATH: + String path; + try { + // get string value + path = srcValue.getString(); + } catch (RepositoryException re) { + // should never happen + throw new ValueFormatException("failed to convert source value to URI value", re); + } + if (!path.startsWith("/")) { + // prefix non-absolute path with "./" (jsr 283 spec 3.6.4.9) + path = "./" + path; + } + val = factory.createValue(path, targetType); + break; + + case PropertyType.BOOLEAN: + case PropertyType.DATE: + case PropertyType.DOUBLE: + case PropertyType.LONG: + case PropertyType.DECIMAL: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + throw new ValueFormatException("conversion failed: " + + PropertyType.nameFromValue(srcType) + " to " + + PropertyType.nameFromValue(targetType)); + + default: + throw new IllegalArgumentException("not a valid type constant: " + srcType); + } + break; + + default: + throw new IllegalArgumentException("not a valid type constant: " + targetType); + } + + return val; + } + + /** + * + * @param srcValue + * @param factory + * @throws IllegalStateException + */ + public static Value copy(Value srcValue, ValueFactory factory) + throws IllegalStateException { + if (srcValue == null) { + return null; + } + + Value newVal = null; + try { + switch (srcValue.getType()) { + case PropertyType.BINARY: + newVal = factory.createValue(srcValue.getStream()); + break; + + case PropertyType.BOOLEAN: + newVal = factory.createValue(srcValue.getBoolean()); + break; + + case PropertyType.DATE: + newVal = factory.createValue(srcValue.getDate()); + break; + + case PropertyType.DOUBLE: + newVal = factory.createValue(srcValue.getDouble()); + break; + + case PropertyType.LONG: + newVal = factory.createValue(srcValue.getLong()); + break; + + case PropertyType.DECIMAL: + newVal = factory.createValue(srcValue.getDecimal()); + break; + + case PropertyType.PATH: + case PropertyType.NAME: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + case PropertyType.URI: + newVal = factory.createValue(srcValue.getString(), srcValue.getType()); + break; + + case PropertyType.STRING: + newVal = factory.createValue(srcValue.getString()); + break; + } + } catch (RepositoryException re) { + // should never get here + } + return newVal; + } + + /** + * @param srcValues + * @param factory + * @throws IllegalStateException + */ + public static Value[] copy(Value[] srcValues, ValueFactory factory) + throws IllegalStateException { + if (srcValues == null) { + return null; + } + + Value[] newValues = new Value[srcValues.length]; + for (int i = 0; i < srcValues.length; i++) { + newValues[i] = copy(srcValues[i], factory); + } + return newValues; + } + + /** + * Serializes the given value to a String. The serialization + * format is the same as used by Document & System View XML, i.e. + * binary values will be Base64-encoded whereas for all others + * {@link Value#getString()} will be used. + * + * @param value the value to be serialized + * @param encodeBlanks if true space characters will be encoded + * as "_x0020_" within he output string. + * @return a string representation of the given value. + * @throws IllegalStateException if the given value is in an illegal state + * @throws RepositoryException if an error occured during the serialization. + */ + public static String serialize(Value value, boolean encodeBlanks) + throws IllegalStateException, RepositoryException { + StringWriter writer = new StringWriter(); + try { + serialize(value, encodeBlanks, false, writer); + } catch (IOException ioe) { + throw new RepositoryException("failed to serialize value", + ioe); + } + return writer.toString(); + } + + /** + * Outputs the serialized value to a Writer. The serialization + * format is the same as used by Document & System View XML, i.e. + * binary values will be Base64-encoded whereas for all others + * {@link Value#getString()} will be used for serialization. + * + * @param value the value to be serialized + * @param encodeBlanks if true space characters will be encoded + * as "_x0020_" within he output string. + * @param enforceBase64 if true, base64 encoding will always be used + * @param writer writer to output the encoded data + * @throws IllegalStateException if the given value is in an illegal state + * @throws IOException if an i/o error occured during the + * serialization + * @throws RepositoryException if an error occured during the serialization. + */ + public static void serialize(Value value, boolean encodeBlanks, boolean enforceBase64, + Writer writer) + throws IllegalStateException, IOException, RepositoryException { + if (value.getType() == PropertyType.BINARY) { + // binary data, base64 encoding required; + // the encodeBlanks flag can be ignored since base64-encoded + // data cannot contain space characters + InputStream in = value.getStream(); + try { + Base64.encode(in, writer); + // no need to close StringWriter + //writer.close(); + } finally { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } else { + String textVal = value.getString(); + if (enforceBase64) { + byte bytes[] = textVal.getBytes(StandardCharsets.UTF_8); + Base64.encode(bytes, 0, bytes.length, writer); + } + else { + if (encodeBlanks) { + // enocde blanks in string + textVal = Text.replace(textVal, " ", "_x0020_"); + } + writer.write(textVal); + } + } + } + + /** + * Deserializes the given string to a Value of the given type. + * + * @param value string to be deserialized + * @param type type of value + * @param decodeBlanks if true "_x0020_" + * character sequences will be decoded to single space + * characters each. + * @param factory ValueFactory used to build the Value object. + * @return the deserialized Value + * @throws ValueFormatException if the string data is not of the required + * format + * @throws RepositoryException if an error occured during the + * deserialization. + */ + public static Value deserialize(String value, int type, boolean decodeBlanks, + ValueFactory factory) + throws ValueFormatException, RepositoryException { + if (type == PropertyType.BINARY) { + // base64 encoded binary value; + // the encodeBlanks flag can be ignored since base64-encoded + // data cannot contain encoded space characters + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + Base64.decode(value, baos); + // no need to close ByteArrayOutputStream + //baos.close(); + } catch (IOException ioe) { + throw new RepositoryException("failed to decode binary value", + ioe); + } + // NOTE: for performance reasons the BinaryValue is created directly + // from the byte-array. This is inconsistent with the other calls, + // that delegate the value creation to the ValueFactory. + return new BinaryValue(baos.toByteArray()); + } else { + if (decodeBlanks) { + // decode encoded blanks in value + value = Text.replace(value, "_x0020_", " "); + } + return convert(value, type, factory); + } + } + + /** + * Deserializes the string data read from the given reader to a + * Value of the given type. + * + * @param reader reader for the string data to be deserialized + * @param type type of value + * @param decodeBlanks if true "_x0020_" + * character sequences will be decoded to single space + * characters each. + * @param factory ValueFactory used to build the Value object. + * @return the deserialized Value + * @throws IOException if an i/o error occured during the + * serialization + * @throws ValueFormatException if the string data is not of the required + * format + * @throws RepositoryException if an error occured during the + * deserialization. + */ + public static Value deserialize(Reader reader, int type, + boolean decodeBlanks, ValueFactory factory) + throws IOException, ValueFormatException, RepositoryException { + if (type == PropertyType.BINARY) { + // base64 encoded binary value; + // the encodeBlanks flag can be ignored since base64-encoded + // data cannot contain encoded space characters + + // decode to temp file + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + final File tmpFile = fileFactory.createTransientFile("bin", null, null); + OutputStream out = new BufferedOutputStream(new FileOutputStream(tmpFile)); + try { + Base64.decode(reader, out); + } finally { + out.close(); + } + + // create an InputStream that keeps a hard reference to the temp file + // in order to prevent its automatic deletion once the associated + // File object is reclaimed by the garbage collector; + // pass InputStream wrapper to ValueFactory, that creates a BinaryValue. + return factory.createValue(new FilterInputStream(new FileInputStream(tmpFile)) { + + public void close() throws IOException { + in.close(); + // temp file can now safely be removed + tmpFile.delete(); + } + }); +/* + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Base64.decode(reader, baos); + // no need to close ByteArrayOutputStream + //baos.close(); + return new BinaryValue(baos.toByteArray()); +*/ + } else { + char[] chunk = new char[8192]; + int read; + StringBuilder buf = new StringBuilder(); + while ((read = reader.read(chunk)) > -1) { + buf.append(chunk, 0, read); + } + String value = buf.toString(); + if (decodeBlanks) { + // decode encoded blanks in value + value = Text.replace(value, "_x0020_", " "); + } + return convert(value, type, factory); + } + } + + /** + * Determine the {@link javax.jcr.PropertyType} of the passed values if all are of + * the same type. + * + * @param values array of values of the same type + * @return {@link javax.jcr.PropertyType#UNDEFINED} if {@code values} is empty, + * {@code values[0].getType()} otherwise. + * @throws javax.jcr.ValueFormatException if not all {@code values} are of the same type + */ + public static int getType(Value[] values) throws ValueFormatException { + int type = UNDEFINED; + for (Value value : values) { + if (value != null) { + if (type == UNDEFINED) { + type = value.getType(); + } else if (value.getType() != type) { + throw new ValueFormatException( + "All values of a multi-valued property must be of the same type"); + } + } + } + return type; + } + +} diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/WeakReferenceValue.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/WeakReferenceValue.java new file mode 100644 index 00000000000..f971813c052 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/WeakReferenceValue.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.value; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.util.UUID; +import java.math.BigDecimal; + +/** + * A WeakReferenceValue provides an implementation + * of the Value interface representing a WEAKREFERENCE value + * (a UUID of an existing node). + */ +public class WeakReferenceValue extends BaseValue { + + public static final int TYPE = PropertyType.WEAKREFERENCE; + + private final String uuid; + + /** + * Constructs a ReferenceValue object representing the UUID of + * an existing node. + * + * @param target the node to be referenced + * @throws IllegalArgumentException If target is nonreferenceable. + * @throws javax.jcr.RepositoryException If another error occurs. + */ + public WeakReferenceValue(Node target) throws RepositoryException { + super(TYPE); + try { + this.uuid = target.getUUID(); + } catch (UnsupportedRepositoryOperationException ure) { + throw new IllegalArgumentException("target is nonreferenceable."); + } + } + + /** + * Returns a new ReferenceValue initialized to the value + * represented by the specified String. + *

    + * The specified String must denote the UUID of an existing + * node. + * + * @param s the string to be parsed. + * @return a newly constructed ReferenceValue representing the + * the specified value. + * @throws javax.jcr.ValueFormatException If the String is not a valid + * not a valid UUID format. + */ + public static WeakReferenceValue valueOf(String s) throws ValueFormatException { + if (s != null) { + try { + UUID.fromString(s); + } catch (IllegalArgumentException iae) { + throw new ValueFormatException("not a valid UUID format: " + s); + } + return new WeakReferenceValue(s); + } else { + throw new ValueFormatException("not a valid UUID format: " + s); + } + } + + /** + * Protected constructor creating a ReferenceValue object + * without validating the UUID format. + * + * @param uuid the UUID of the node to be referenced + * @see #valueOf + */ + protected WeakReferenceValue(String uuid) { + super(TYPE); + this.uuid = uuid; + } + + /** + * Indicates whether some other object is "equal to" this one. + *

    + * The result is true if and only if the argument is not + * null and is a ReferenceValue object that + * represents the same value as this object. + * + * @param obj the reference object with which to compare. + * @return true if this object is the same as the obj + * argument; false otherwise. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof WeakReferenceValue) { + WeakReferenceValue other = (WeakReferenceValue) obj; + if (uuid == other.uuid) { + return true; + } else if (uuid != null && other.uuid != null) { + return uuid.equals(other.uuid); + } + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //------------------------------------------------------------< BaseValue > + /** + * {@inheritDoc} + */ + protected String getInternalString() throws ValueFormatException { + if (uuid != null) { + return uuid; + } else { + throw new ValueFormatException("empty value"); + } + } + + //----------------------------------------------------------------< Value > + /** + * {@inheritDoc} + */ + public Calendar getDate() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to date failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public long getLong() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to long failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public boolean getBoolean() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to boolean failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public double getDouble() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to double failed: inconvertible types"); + } + + /** + * {@inheritDoc} + */ + public BigDecimal getDecimal() + throws ValueFormatException, IllegalStateException, + RepositoryException { + throw new ValueFormatException("conversion to Decimal failed: inconvertible types"); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/package-info.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/package-info.java new file mode 100644 index 00000000000..7859d285b32 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/value/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.2.1") +package org.apache.jackrabbit.value; diff --git a/jackrabbit-jcr-commons/src/main/javadoc/org/apache/jackrabbit/uuid/package.html b/jackrabbit-jcr-commons/src/main/javadoc/org/apache/jackrabbit/uuid/package.html new file mode 100644 index 00000000000..507eb0ef016 --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/javadoc/org/apache/jackrabbit/uuid/package.html @@ -0,0 +1,28 @@ + + +

    + This package contains a UUID (Universally Unique Identifier) version 4 + generator implementation copied from from the + Jakarta Commons-Id project. +

    +

    + TODO: Replace with commons-id.jar as soon as official release is + available. +

    + diff --git a/jackrabbit-jcr-commons/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory b/jackrabbit-jcr-commons/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory new file mode 100644 index 00000000000..de7de92cdd0 --- /dev/null +++ b/jackrabbit-jcr-commons/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.commons.JndiRepositoryFactory diff --git a/jackrabbit-jcr-commons/src/main/resources/META-INF/services/org.apache.jackrabbit.commons.query.QueryObjectModelBuilder b/jackrabbit-jcr-commons/src/main/resources/META-INF/services/org.apache.jackrabbit.commons.query.QueryObjectModelBuilder new file mode 100644 index 00000000000..e9a676a82af --- /dev/null +++ b/jackrabbit-jcr-commons/src/main/resources/META-INF/services/org.apache.jackrabbit.commons.query.QueryObjectModelBuilder @@ -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. + +# +# This file lists all available query language implementations that are shipped +# with Jackrabbit and are based on the JCR 2.0 QueryObjectModel. +# + +org.apache.jackrabbit.commons.query.sql2.SQL2QOMBuilder diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/AbstractRepositoryTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/AbstractRepositoryTest.java new file mode 100644 index 00000000000..5aecae67828 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/AbstractRepositoryTest.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +/** + * Test cases for the {@link AbstractRepositoryTest} class. + */ +public class AbstractRepositoryTest extends MockCase { + + /** + * Tests the {@link AbstractRepository#login()} method. + * + * @throws RepositoryException if an error occurs + */ + public void testLogin() throws RepositoryException { + Repository repository = (Repository) record(AbstractRepository.class); + repository.login(null, null); + + replay(); + repository.login(); + + verify(); + } + + /** + * Tests the {@link AbstractRepository#login(Credentials)} method. + * + * @throws RepositoryException if an error occurs + */ + public void testLoginWithCredentials() throws RepositoryException { + Credentials credentials = new SimpleCredentials("", "".toCharArray()); + + Repository repository = (Repository) record(AbstractRepository.class); + repository.login(credentials, null); + + replay(); + repository.login(credentials); + + verify(); + } + + /** + * Tests the {@link AbstractRepository#login(Credentials)} method with + * null credentials. + * + * @throws RepositoryException if an error occurs + */ + public void testLoginWithNullCredentials() throws RepositoryException { + Repository repository = (Repository) record(AbstractRepository.class); + repository.login(null, null); + + replay(); + repository.login((Credentials) null); + + verify(); + } + + /** + * Tests the {@link AbstractRepository#login(String)} method. + * + * @throws RepositoryException if an error occurs + */ + public void testLoginWithWorkspace() throws RepositoryException { + Repository repository = (Repository) record(AbstractRepository.class); + repository.login(null, "workspace"); + + replay(); + repository.login("workspace"); + + verify(); + } + + /** + * Tests the {@link AbstractRepository#login(String)} method with a + * null workspace name. + * + * @throws RepositoryException if an error occurs + */ + public void testLoginWithNullWorkspace() throws RepositoryException { + Repository repository = (Repository) record(AbstractRepository.class); + repository.login(null, null); + + replay(); + repository.login((String) null); + + verify(); + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/JcrUtilsTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/JcrUtilsTest.java new file mode 100644 index 00000000000..007bef3d112 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/JcrUtilsTest.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.commons; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.naming.InitialContext; + +public class JcrUtilsTest extends MockCase { + + public void testGetRepository() throws Exception { + Object repository = record(AbstractRepository.class); + + Hashtable environment = new Hashtable(); + environment.put( + "java.naming.factory.initial", + "org.osjava.sj.memory.MemoryContextFactory"); + environment.put("org.osjava.sj.jndi.shared", "true"); + InitialContext context = new InitialContext(environment); + context.bind("repository", repository); + + // Test lookup with a traditional map of parameters + Map parameters = new HashMap(); + parameters.put( + "org.apache.jackrabbit.repository.jndi.name", "repository"); + parameters.put( + "java.naming.factory.initial", + "org.osjava.sj.memory.MemoryContextFactory"); + parameters.put("org.osjava.sj.jndi.shared", "true"); + assertTrue(repository == JcrUtils.getRepository(parameters)); + + // Test lookup with URI query parameters + assertTrue(repository == JcrUtils.getRepository( + "jndi://x" + + "?org.apache.jackrabbit.repository.jndi.name=repository" + + "&org.osjava.sj.jndi.shared=true" + + "&java.naming.factory.initial" + + "=org.osjava.sj.memory.MemoryContextFactory")); + + // Test lookup with the custom JNDI URI format (JCR-2771) + assertTrue(repository == JcrUtils.getRepository( + "jndi://org.osjava.sj.memory.MemoryContextFactory/repository" + + "?org.osjava.sj.jndi.shared=true")); + + try { + JcrUtils.getRepository( + "jndi://org.osjava.sj.memory.MemoryContextFactory/missing"); + fail("Repository lookup failure should throw an exception"); + } catch (RepositoryException expected) { + } + } + + public void testGetPropertyType() { + assertEquals(PropertyType.BINARY, JcrUtils.getPropertyType( + PropertyType.TYPENAME_BINARY)); + assertEquals(PropertyType.BOOLEAN, JcrUtils.getPropertyType( + PropertyType.TYPENAME_BOOLEAN.toLowerCase())); + assertEquals(PropertyType.DATE, JcrUtils.getPropertyType( + PropertyType.TYPENAME_DATE.toUpperCase())); + } + + public void testIn() { + Iterable iterable = JcrUtils.in(Arrays.asList("A").iterator()); + Iterator iterator = iterable.iterator(); + assertNotNull(iterator); + assertTrue(iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse(iterator.hasNext()); + + try { + iterable.iterator(); + fail("Second execution of Iterable.iterator() should throw an exception"); + } catch (IllegalStateException expected) { + } + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/MockCase.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/MockCase.java new file mode 100644 index 00000000000..c73967f0a26 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/MockCase.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.commons; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedList; + +import junit.framework.TestCase; + +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +/** + * Simple {@link TestCase} base class for mock-testing the abstract + * JCR implementation classes in this package. + */ +public class MockCase extends TestCase implements MethodInterceptor { + + /** + * Mock test state. Set to true when the mock methods + * are being recorded, and to false when the methods are + * being played back. + */ + private boolean recording; + + /** + * The recorded mock call sequence. List of {@link MethodCall} objects. + */ + private LinkedList calls; + + /** + * The abstract base class being tested. + */ + private Class base; + + /** + * Creates a mocked proxy object for the given abstract base class and + * starts recording calls to the object. + * + * @param base the abstract base class being tested + * @return mocked proxy object + */ + protected Object record(Class base) { + this.recording = true; + this.calls = new LinkedList(); + this.base = base; + return Enhancer.create(base, base.getInterfaces(), this); + } + + /** + * Switches the test state to replaying and verifying the recorded + * call sequence. + */ + protected void replay() { + recording = false; + } + + /** + * Verifies that all recorded method calls were executed during + * the verification phase. + */ + protected void verify() { + assertTrue(calls.isEmpty()); + } + + //---------------------------------------------------< MethodInterceptor > + + /** + * Intercepts a method call to the mocked proxy object. Passes the + * call through if the abstract base class being tested implements the + * method, and records or verifies the method call otherwise. + * + * @param object the object on which the method is being called + * @param method the method being called + * @param args method arguments + * @param proxy proxy for re-invoking the called method + * @return method return value + * @throws Throwable if an error occurs + */ + public Object intercept( + Object object, Method method, Object[] args, MethodProxy proxy) + throws Throwable { + try { + base.getDeclaredMethod(method.getName(), method.getParameterTypes()); + return proxy.invokeSuper(object, args); + } catch (NoSuchMethodException e) { + if (recording) { + calls.addLast(new MethodCall(method, args)); + } else { + assertFalse(calls.isEmpty()); + MethodCall call = (MethodCall) calls.removeFirst(); + call.assertCall(method, args); + } + return null; + } + } + + //----------------------------------------------------------< MethodCall > + + /** + * Record of a method call. + */ + private static class MethodCall { + + /** + * The method that was called. + */ + private final Method method; + + /** + * The arguments that were passed to the method. + */ + private final Object[] args; + + /** + * Creates a new method call record. + * + * @param method the method that was called + * @param args the arguments that were passed to the method + */ + public MethodCall(Method method, Object[] args) { + this.method = method; + this.args = args; + } + + /** + * Verifies that the given method is the same as was recorded. + * + * @param method the method that was called + * @param args the arguments that were passed to the method + */ + public void assertCall(Method method, Object[] args) { + assertEquals(this.method, method); + assertTrue(Arrays.equals(this.args, args)); + } + + } + +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/SimpleValueFactoryTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/SimpleValueFactoryTest.java new file mode 100644 index 00000000000..3c794a53c85 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/SimpleValueFactoryTest.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.commons; + +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.TimeZone; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; + +import junit.framework.TestCase; + +/** + * Test cases for the {@link SimpleValueFactory} class. + */ +public class SimpleValueFactoryTest extends TestCase { + + private final ValueFactory factory = new SimpleValueFactory(); + + public void testBoolean() throws RepositoryException { + Value value = factory.createValue(true); + + assertEquals(PropertyType.BOOLEAN, value.getType()); + assertEquals(value, factory.createValue(true)); + + assertTrue(value.getBoolean()); + try { value.getDate(); fail(); } catch (ValueFormatException e) {} + try { value.getDecimal(); fail(); } catch (ValueFormatException e) {} + try { value.getDouble(); fail(); } catch (ValueFormatException e) {} + try { value.getLong(); fail(); } catch (ValueFormatException e) {} + assertEquals(Boolean.TRUE.toString(), value.getString()); + + // TODO: binary representation + } + + public void testDate() throws RepositoryException { + Calendar a = Calendar.getInstance(); + a.setTimeInMillis(1234567890); + a.setTimeZone(TimeZone.getTimeZone("GMT")); + Value value = factory.createValue(a); + + assertEquals(PropertyType.DATE, value.getType()); + assertEquals(value, factory.createValue(a)); + + try { value.getBoolean(); fail(); } catch (ValueFormatException e) {} + assertEquals(a, value.getDate()); + assertEquals(new BigDecimal(a.getTimeInMillis()), value.getDecimal()); + assertEquals((double) a.getTimeInMillis(), value.getDouble()); + assertEquals(a.getTimeInMillis(), value.getLong()); + assertEquals("1970-01-15T06:56:07.890Z", value.getString()); + + // TODO: binary representation + } + + public void testDecimal() throws RepositoryException { + BigDecimal a = new BigDecimal(1234567890); + Value value = factory.createValue(a); + + assertEquals(PropertyType.DECIMAL, value.getType()); + assertEquals(value, factory.createValue(a)); + + try { value.getBoolean(); fail(); } catch (ValueFormatException e) {} + assertEquals(a.longValue(), value.getDate().getTimeInMillis()); + assertEquals(a, value.getDecimal()); + assertEquals(a.doubleValue(), value.getDouble()); + assertEquals(a.longValue(), value.getLong()); + assertEquals(a.toString(), value.getString()); + + // TODO: binary representation + } + + public void testDouble() throws RepositoryException { + double a = 123456789.0; + Value value = factory.createValue(a); + + assertEquals(PropertyType.DOUBLE, value.getType()); + assertEquals(value, factory.createValue(a)); + + try { value.getBoolean(); fail(); } catch (ValueFormatException e) {} + assertEquals((long) a, value.getDate().getTimeInMillis()); + assertEquals(new BigDecimal(a), value.getDecimal()); + assertEquals(a, value.getDouble()); + assertEquals((long) a, value.getLong()); + assertEquals(Double.toString(a), value.getString()); + + // TODO: binary representation + } + + public void testLong() throws RepositoryException { + long a = 1234567890; + Value value = factory.createValue(a); + + assertEquals(PropertyType.LONG, value.getType()); + assertEquals(value, factory.createValue(a)); + + try { value.getBoolean(); fail(); } catch (ValueFormatException e) {} + assertEquals(a, value.getDate().getTimeInMillis()); + assertEquals(new BigDecimal(a), value.getDecimal()); + assertEquals((double) a, value.getDouble()); + assertEquals(a, value.getLong()); + assertEquals(Long.toString(a), value.getString()); + + // TODO: binary representation + } + + public void testString() throws RepositoryException { + String a = "test"; + Value value = factory.createValue(a); + + assertEquals(PropertyType.STRING, value.getType()); + assertEquals(value, factory.createValue(a)); + + assertFalse(value.getBoolean()); + try { value.getDate(); fail(); } catch (ValueFormatException e) {} + try { value.getDecimal(); fail(); } catch (ValueFormatException e) {} + try { value.getDouble(); fail(); } catch (ValueFormatException e) {} + try { value.getLong(); fail(); } catch (ValueFormatException e) {} + assertEquals(a, value.getString()); + + // TODO: binary representation + } + +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/flat/RankTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/flat/RankTest.java new file mode 100644 index 00000000000..faafb8d32e7 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/flat/RankTest.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.jackrabbit.commons.flat; + +import org.apache.jackrabbit.commons.flat.Rank; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.Set; + +import junit.framework.TestCase; + +public class RankTest extends TestCase { + private static final Random rnd = new Random(); + + public void testEmpty() { + Rank r = Rank.rank(new Integer[0]); + assertFalse(r.take(0).hasNext()); + assertEquals(0, r.size()); + try { + r.take(1); + fail("Excepted " + NoSuchElementException.class.getName()); + } + catch (NoSuchElementException ignore) { } + } + + public void testSingleton() { + Rank r = Rank.rank(new Integer[] {42}); + assertFalse(r.take(0).hasNext()); + assertEquals(1, r.size()); + + Iterator it = r.take(1); + assertTrue(it.hasNext()); + assertEquals(0, r.size()); + assertEquals(42, it.next().intValue()); + + assertFalse(r.take(0).hasNext()); + + try { + r.take(1); + fail("Excepted " + NoSuchElementException.class.getName()); + } + catch (NoSuchElementException ignore) { } + } + + public void testRank() { + for (int n = 1; n <= 2000; n++) { + testRank(n); + } + } + + public void testGetAll() { + int n = 100; + List values = createValues(n); + Rank r = Rank.rank(values, Integer.class); + + try { + r.take(n + 1); + fail("Expected " + NoSuchElementException.class.getName()); + } + catch (NoSuchElementException ignore) { } + + assertEquals(n, r.size()); + + Iterator it = r.take(n); + while (it.hasNext()) { + assertTrue(values.remove(it.next())); + } + + assertTrue(values.isEmpty()); + assertEquals(0, r.size()); + } + + public void testGetSingles() { + int n = 100; + List values = createValues(n); + Rank r = Rank.rank(values, Integer.class); + + List sorted = new ArrayList(); + Iterator it; + while (r.size() > 0) { + it = r.take(1); + sorted.add(it.next()); + assertFalse(it.hasNext()); + } + + assertTrue(sorted.containsAll(values)); + assertTrue(values.containsAll(sorted)); + + Comparator order = r.getOrder(); + checkOrdered(sorted.iterator(), order); + } + + public void testOrdered() { + int n = 1000000; + Integer[] values = new Integer[n]; + for (int k = 0; k < n; k++) { + values[k] = k; + } + + Rank r = Rank.rank(values); + while (r.size() > 0) { + int k = Math.min(rnd.nextInt(n/10), r.size()); + checkOrdered(r.take(k), r.getOrder()); + } + } + + // -----------------------------------------------------< internal >--- + + private static List createValues(int count) { + Set ints = new HashSet(); + for (int k = 0; k < count; k++) { + ints.add(rnd.nextInt()); + } + + List intList = new LinkedList(ints); + Collections.shuffle(intList); + return intList; + } + + private void checkOrdered(Iterator it, Comparator order) { + T prev = it.next(); + while (it.hasNext()) { + T next = it.next(); + assertTrue(order.compare(prev, next) < 0); + prev = next; + } + } + + private static void testRank(int count) { + List ints = createValues(count); + Rank r = Rank.rank(ints, Integer.class); + Set all = new HashSet(ints); + Set previous = new HashSet(); + while (r.size() > 0) { + int n = Math.min(rnd.nextInt(count + 1), r.size()); + Iterator it = r.take(n); + Set actual = new HashSet(); + while (it.hasNext()) { + Integer i = it.next(); + assertTrue(actual.add(i)); + assertTrue(all.remove(i)); + } + + checkOrdering(previous, actual, r.getOrder()); + previous = actual; + } + + assertTrue(all.isEmpty()); + } + + private static void checkOrdering(Set previous, Set actual, + Comparator order) { + + for (Iterator pIt = previous.iterator(); pIt.hasNext(); ) { + Integer p = pIt.next(); + for(Iterator aIt = actual.iterator(); aIt.hasNext(); ) { + Integer a = aIt.next(); + assertTrue(order.compare(p, a) < 0); + } + } + } + +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/iterator/FilteredRangeIteratorTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/iterator/FilteredRangeIteratorTest.java new file mode 100644 index 00000000000..cb484c07cff --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/iterator/FilteredRangeIteratorTest.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.commons.iterator; + +import java.util.Arrays; +import java.util.List; + +import javax.jcr.RangeIterator; + +import org.apache.jackrabbit.commons.predicate.Predicate; + +import junit.framework.TestCase; + +/** + * Test cases for the {@link FilteredRangeIterator} class. + * + * @see JCR-2722 + */ +public class FilteredRangeIteratorTest extends TestCase { + + private static final List LIST = Arrays.asList("x", "y", "z"); + + public void testMatchAll() { + RangeIterator iterator = new FilteredRangeIterator(LIST.iterator()); + assertEquals(3, iterator.getSize()); + + assertEquals(0, iterator.getPosition()); + assertTrue(iterator.hasNext()); + assertEquals("x", iterator.next()); + + assertEquals(1, iterator.getPosition()); + assertTrue(iterator.hasNext()); + assertEquals("y", iterator.next()); + + assertEquals(2, iterator.getPosition()); + assertTrue(iterator.hasNext()); + assertEquals("z", iterator.next()); + + assertEquals(3, iterator.getPosition()); + assertFalse(iterator.hasNext()); + } + + public void testMatchNone() { + RangeIterator iterator = + new FilteredRangeIterator(LIST.iterator(), Predicate.FALSE); + assertEquals(0, iterator.getSize()); + assertEquals(0, iterator.getPosition()); + assertFalse(iterator.hasNext()); + } + + public void testSkip() { + RangeIterator iterator = new FilteredRangeIterator(LIST.iterator()); + assertEquals(3, iterator.getSize()); + + assertEquals(0, iterator.getPosition()); + assertTrue(iterator.hasNext()); + assertEquals("x", iterator.next()); + + iterator.skip(1); + + assertEquals(2, iterator.getPosition()); + assertTrue(iterator.hasNext()); + assertEquals("z", iterator.next()); + + assertEquals(3, iterator.getPosition()); + assertFalse(iterator.hasNext()); + } + +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/JsonParserTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/JsonParserTest.java new file mode 100644 index 00000000000..4958070746b --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/JsonParserTest.java @@ -0,0 +1,582 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.json; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +/** + * JSONParserTest... + */ +public class JsonParserTest extends TestCase { + + private final JsonHandler handler = new DummyJsonHandler(); + + protected void setUp() throws Exception { + super.setUp(); + } + + public void testParser() throws Exception { + // TODO validate output. use specific handler ext. + JsonParser parser = new JsonParser(handler); + parser.parse(getObj().toString()); + } + + public void testParseBooleanValue() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("boolean", true); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("boolean", key); + } + public void value(String value) { + fail(); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + assertEquals(true, value); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testParseLongValue() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("long", 123456); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("long", key); + } + public void value(String value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + public void value(long value) { + assertEquals(123456, value); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testDoubleValue() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("double", 1235674.342424); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("double", key); + } + public void value(String value) { + fail(); + } + public void value(long value) { + fail(); + } + public void value(boolean value) { + fail(); + } + public void value(double value) { + assertEquals(new Double(1235674.342424), new Double(value)); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testStringValue() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("string", "abc"); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("string", key); + } + public void value(String value) { + assertEquals("abc", value); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testStringWithQuoteValue() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("string", "abc\"abc"); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("string", key); + } + public void value(String value) { + assertEquals("abc\"abc", value); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testStringWithBackSlashValue() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("string", "abc\\abc"); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("string", key); + } + public void value(String value) { + assertEquals("abc\\abc", value); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testStringWithBackSlashValue2() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("string", "\'abc\\\\x\\'abc"); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("string", key); + } + public void value(String value) { + assertEquals("\'abc\\\\x\\'abc", value); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testStringWithUnicode() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("string", "abc\u2345ab\u00EB\u0633c"); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("string", key); + } + public void value(String value) { + assertEquals("abc\u2345ab\u00EB\u0633c", value); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testStringWithUnicode2() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("string", "\u00EB"); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("string", key); + } + public void value(String value) { + assertEquals("\u00EB", value); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testStringWithReturn() throws Exception { + ArrayList l = new ArrayList(); + l.add("abc\ndef"); + l.add("abc\rdef"); + l.add("abc\n\rdef"); + l.add("abc\tdef"); + l.add("abc\bdef"); + l.add("abc\n\\\tdef"); + l.add("abc\f\u3456\b\\def"); + + for (Iterator it = l.iterator(); it.hasNext();) { + final String expValue = it.next().toString(); + JSONObject obj = new JSONObject(); + obj.put("string", expValue); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("string", key); + } + public void value(String value) { + assertEquals(expValue, value); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + } + + public void testNullValue() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("null", null); + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals("null", key); + } + public void value(String value) { + assertNull(value); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testArray() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("array", Arrays.asList(new String[] {"a", "b", "c"})); + + JsonHandler handler = new DummyJsonHandler() { + boolean arrayStarted = false; + int index = 0; + + public void key(String key) { + assertEquals("array", key); + } + public void array() { + assertFalse(arrayStarted); + arrayStarted = true; + } + public void endArray() { + assertTrue(arrayStarted); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + public void value(String value) { + assertTrue(arrayStarted); + switch (index) { + case 0: assertEquals("a", value); break; + case 1: assertEquals("b", value); break; + case 2: assertEquals("c", value); break; + default: fail(); + } + index++; + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testLongArray() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("longarray", Arrays.asList(new Long[] {new Long(123), new Long(3456), new Long(45367)})); + + JsonHandler handler = new DummyJsonHandler() { + boolean arrayStarted = false; + int index = 0; + + public void key(String key) { + assertEquals("longarray", key); + } + public void array() { + assertFalse(arrayStarted); + arrayStarted = true; + } + public void endArray() { + assertTrue(arrayStarted); + } + public void value(long value) { + assertTrue(arrayStarted); + switch (index) { + case 0: assertEquals(123, value); break; + case 1: assertEquals(3456, value); break; + case 2: assertEquals(45367, value); break; + default: fail(); + } + index++; + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + public void value(String value) { + fail(); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testParser2() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("obj1", getSimpleObj("bla")); + obj.put("obj2", getSimpleObj("blu")); + obj.put("obj3", getSimpleObj("bli")); + + // TODO validate output. use specific handler ext. + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testParser4() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("arr1", getSimpleArray(new String[] {"s", "t", "r"})); + obj.put("arr2", getSimpleArray(new String[] {"s", "t", "r"})); + obj.put("arr3", getSimpleArray(new String[] {"s", "t", "r"})); + obj.put("arr4", getSimpleArray(new String[] {"s", "t", "r"})); + + // TODO validate output. use specific handler ext. + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testParser5() throws Exception { + JSONObject obj = new JSONObject(); + obj.put("arr1", getSimpleArray(new JSONObject[] { getSimpleObj(new Integer(1)), getSimpleObj("abc"), getSimpleObj(Boolean.TRUE) })); + obj.put("objvalue", getSimpleObj( getSimpleArray(new Object[] {"a", new Double(2.3), Boolean.FALSE}))); + obj.put("arrarr", getSimpleArray( new Object[] {getSimpleArray(new Object[] {"a", new Double(2.3), Boolean.FALSE})})); + obj.put("simplv", Boolean.TRUE); + + // TODO validate output. use specific handler ext. + JsonParser parser = new JsonParser(handler); + parser.parse(obj.toString()); + } + + public void testParser6() throws Exception { + final String expKey = "prop1"; + final String expValue = "Any string containing comma, period. question mark?"; + + JsonHandler handler = new DummyJsonHandler() { + public void key(String key) { + assertEquals(expKey, key); + } + + public void value(String value) { + assertEquals(expValue, value); + } + }; + + String str = "{\"" +expKey+ "\":\""+expValue+"\"}"; + + JsonParser parser = new JsonParser(handler); + parser.parse(str); + } + + public void testParseEmptyObject() throws Exception { + JsonHandler handler = new DummyJsonHandler() { + private int objectCnt = 0; + public void object() { + objectCnt++; + } + public void endObject() { + assertEquals(1, objectCnt); + } + public void array() { + fail(); + } + public void endArray() { + fail(); + } + public void key(String key) { + fail(); + } + public void value(String value) { + fail(); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + }; + JsonParser parser = new JsonParser(handler); + parser.parse("{}"); + } + + public void testParseEmptyObjectValue() throws Exception { + + List l = new ArrayList(); + l.add("{\"a\":{},\"b\":{},\"c\":{}}"); + l.add("{\"a\":{\"b\":{\"c\":{}}}}"); + l.add("{\"a\":{},\"b\":{\"c\":{}}}"); + l.add("{\"a\":{\"b\":{},\"c\":{}}}"); + + for (Iterator it = l.iterator(); it.hasNext();) { + JsonHandler handler = new DummyJsonHandler() { + private int objectCnt = 0; + public void object() { + objectCnt++; + } + public void endObject() { + assertFalse(objectCnt > 4); + } + public void array() { + fail(); + } + public void endArray() { + fail(); + } + public void value(String value) { + fail(); + } + public void value(long value) { + fail(); + } + public void value(double value) { + fail(); + } + public void value(boolean value) { + fail(); + } + }; + + JsonParser parser = new JsonParser(handler); + parser.parse(it.next().toString()); + } + } + + private static JSONObject getObj() { + JSONObject obj = new JSONObject(); + obj.put("boolean", true); + obj.put("long", 1); + obj.put("double", 1235674.342424); + obj.put("array", Arrays.asList(new String[] {"a", "b", "c"})); + obj.put("longarray", Arrays.asList(new Long[] {new Long(123), new Long(3456), new Long(45367)})); + obj.put("string", "abc"); + obj.put("string1", "123.456"); + return obj; + } + + private static JSONObject getSimpleObj(Object value) { + JSONObject obj = new JSONObject(); + obj.put("v", value); + return obj; + } + + private static JSONArray getSimpleArray(Object[] values) { + JSONArray arr = new JSONArray(); + for (int i = 0; i < values.length; i++) { + arr.add(values[i]); + } + return arr; + } + + //-------------------------------------------------------------------------- + /** + * Dummy handler impl that does nothing. + */ + private class DummyJsonHandler implements JsonHandler { + + public void object() { + } + public void endObject() { + } + public void array() { + } + public void endArray() { + } + public void key(String key) { + } + public void value(String value) { + } + public void value(long value) { + } + public void value(double value) { + } + public void value(boolean value) { + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/JsonUtilTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/JsonUtilTest.java new file mode 100644 index 00000000000..a61e668c7c0 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/JsonUtilTest.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.commons.json; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * JSONUtilTest... + */ +public class JsonUtilTest extends TestCase { + + public void testGetJsonString() { + char DQ = '"'; + + Map m = new HashMap(); + m.put("abc", "abc"); + m.put("a \"b\" c", "a \\\"b\\\" c"); + m.put("a\tb\rc\nd\fe\b", "a\\tb\\rc\\nd\\fe\\b"); + m.put("\\abc", "\\\\abc"); + m.put("abc", "abc"); + + // non-printable ascii other than those treated (\t,\r,\n) + m.put(String.valueOf((char) 7), "\\u0007"); + m.put(String.valueOf((char) 30), "\\u001e"); + + /* chinese */ + m.put("\u4e00a\u4e8cb\u4e09c", "\u4e00a\u4e8cb\u4e09c"); + /* arabic */ + m.put("\u062c\u062f\u064a\u062f", "\u062c\u062f\u064a\u062f"); + /* U+00D1 a U+00E7 b U+0416 c */ + m.put("\u00d1a\u00e7b\u0416c", "\u00d1a\u00e7b\u0416c"); + /* U+00E2 a U+00E8 b U+00F8 c U+00FC d */ + m.put("\u00e2a\u00e8b\u00f8c\u00fcd", "\u00e2a\u00e8b\u00f8c\u00fcd"); + + for (Map.Entry t : m.entrySet()) { + String input = t.getKey(); + String output = DQ + t.getValue() + DQ; + assertEquals(output, JsonUtil.getJsonString(input)); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/TestAll.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/TestAll.java new file mode 100644 index 00000000000..1001573847f --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/json/TestAll.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.commons.json; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package org.apache.jackrabbit.json. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + + TestSuite suite = new TestSuite("org.apache.jackrabbit.json tests"); + + suite.addTestSuite(JsonUtilTest.class); + suite.addTestSuite(JsonParserTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/query/GQLTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/query/GQLTest.java new file mode 100755 index 00000000000..e3b2681f5ac --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/query/GQLTest.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.commons.query; + +import javax.jcr.RepositoryException; + +import junit.framework.TestCase; + +public class GQLTest extends TestCase { + + public void testGQL() throws RepositoryException { + assertEquals( + "//*[jcr:like(fn:lower-case(fn:name()), 'test')] ", + GQL.translateToXPath( + "order:- " + + "name:test", null, "assets")); + assertEquals( + "//*[1=1] order by @jcr:score descending", + GQL.translateToXPath( + "\"jcr:nativeXPath\":\"1=1\"", null, "assets")); + assertEquals( + "//*[(jcr:contains(assets/@a, '1') and 1=1)] ", + GQL.translateToXPath( + "order:- " + + "a: 1 " + + "\"jcr:nativeXPath\":\"1=1\"", null, "assets")); + + } + + public void testEscaping() throws RepositoryException { + //simple things work + assertEquals("//*[jcr:contains(assets/@a, 'b')] order by @jcr:score descending", + GQL.translateToXPath("a:b", null, "assets")); + + //backslash is ignored (same as earlier) and only ", \ and : are escaped + assertEquals("//*[jcr:contains(assets/@a, 'b')] order by @jcr:score descending", + GQL.translateToXPath("a:b\\", null, "assets")); + assertEquals("//*[jcr:contains(assets, 'ab')] order by @jcr:score descending", + GQL.translateToXPath("a\\b", null, "assets")); + assertEquals("//*[jcr:contains(assets/@a, 'b')] order by @jcr:score descending", + GQL.translateToXPath("a:\\b", null, "assets")); + + //backward compatibility of quoted ":" + assertEquals("//*[jcr:contains(assets/@a, '1:1')] order by @jcr:score descending", + GQL.translateToXPath("a:\"1:1\"", null, "assets")); + + //escaping ":" + assertEquals("//*[jcr:contains(assets/@a, '1:1')] order by @jcr:score descending", + GQL.translateToXPath("a:\"1\\:1\"", null, "assets")); + + assertEquals("//*[jcr:contains(assets/@a, '1:1')] order by @jcr:score descending", + GQL.translateToXPath("a:1\\:1", null, "assets")); + + assertEquals("//*[jcr:contains(assets/@a:, '1')] order by @jcr:score descending", + GQL.translateToXPath("a\\::1", null, "assets")); + + //escaping \ + assertEquals("//*[jcr:contains(assets/@a, '1\\1')] order by @jcr:score descending", + GQL.translateToXPath("a:\"1\\\\1\"", null, "assets")); + + assertEquals("//*[jcr:contains(assets/@a, '1\\1')] order by @jcr:score descending", + GQL.translateToXPath("a:1\\\\1", null, "assets")); + + assertEquals("//*[jcr:contains(assets/@a_x005c_, '1')] order by @jcr:score descending", + GQL.translateToXPath("a\\\\:1", null, "assets")); + + + //escaping " + assertEquals("//*[jcr:contains(assets/@a, '1\"1')] order by @jcr:score descending", + GQL.translateToXPath("a:\"1\\\"1\"", null, "assets")); + + assertEquals("//*[jcr:contains(assets/@a, '1\"1')] order by @jcr:score descending", + GQL.translateToXPath("a:1\\\"1", null, "assets")); + + assertEquals("//*[jcr:contains(assets/@a_x0022_, '1')] order by @jcr:score descending", + GQL.translateToXPath("a\\\":1", null, "assets")); + + } + +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/ParsingContentHandlerTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/ParsingContentHandlerTest.java new file mode 100644 index 00000000000..29ae9cb477f --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/ParsingContentHandlerTest.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.commons.xml; + +import java.io.ByteArrayInputStream; +import java.io.StringWriter; + +import junit.framework.TestCase; + +import org.xml.sax.ContentHandler; +import org.xml.sax.helpers.DefaultHandler; + +public class ParsingContentHandlerTest extends TestCase { + + public void testParsingContentHandler() throws Exception { + String source = + "abcxyz"; + StringWriter writer = new StringWriter(); + + ContentHandler handler = + SerializingContentHandler.getSerializer(writer); + new ParsingContentHandler(handler).parse( + new ByteArrayInputStream(source.getBytes("UTF-8"))); + + String xml = writer.toString(); + assertContains(xml, ""); + assertContains(xml, ""); + assertContains(xml, ""); + assertContains(xml, "xyz"); + assertContains(xml, ""); + } + + /** + * Test case for JCR-1355. + * + * @see https://issues.apache.org/jira/browse/JCR-1355 + */ + public void testExternalEntities() { + try { + String source = + ""; + new ParsingContentHandler(new DefaultHandler()).parse( + new ByteArrayInputStream(source.getBytes("UTF-8"))); + } catch (Exception e) { + fail("JCR-1355: XML import should not access external entities"); + } + } + + private void assertContains(String haystack, String needle) { + if (haystack.indexOf(needle) == -1) { + fail("'" + haystack + "' does not contain '" + needle+ "'"); + } + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/SerializingContentHandlerTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/SerializingContentHandlerTest.java new file mode 100644 index 00000000000..fe81b7b17a9 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/SerializingContentHandlerTest.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.commons.xml; + +import java.io.StringWriter; + +import junit.framework.TestCase; + +import org.xml.sax.ContentHandler; +import org.xml.sax.helpers.AttributesImpl; + +public class SerializingContentHandlerTest extends TestCase { + + public void testSerializingContentHandler() throws Exception { + StringWriter writer = new StringWriter(); + + ContentHandler handler = + SerializingContentHandler.getSerializer(writer); + handler.startDocument(); + handler.startPrefixMapping("p", "uri"); + handler.startElement("uri", "a", "p:a", new AttributesImpl()); + AttributesImpl attributes = new AttributesImpl(); + attributes.addAttribute("uri", "foo", "p:foo", "CDATA", "bar"); + handler.startElement(null, "b", "b", attributes); + handler.characters("abc".toCharArray(), 0, 3); + handler.endElement(null, "b", "b"); + handler.startElement(null, "c", "c", new AttributesImpl()); + handler.endElement(null, "c", "c"); + handler.characters("xyz".toCharArray(), 0, 3); + handler.endElement("uri", "a", "p:a"); + handler.endPrefixMapping("p"); + handler.endDocument(); + + String xml = writer.toString(); + assertContains(xml, ""); + assertContains(xml, ""); + assertContains(xml, ""); + assertContains(xml, "xyz"); + assertContains(xml, ""); + } + + /** + * Test case for JCR-1767. + * + * @see JCR-1767 + */ + public void testNoPrefixMappingCalls() throws Exception { + StringWriter writer = new StringWriter(); + + ContentHandler handler = + SerializingContentHandler.getSerializer(writer); + handler.startDocument(); + handler.startElement("uri", "a", "p:a", new AttributesImpl()); + AttributesImpl attributes = new AttributesImpl(); + attributes.addAttribute("uri", "foo", "p:foo", "CDATA", "bar"); + handler.startElement(null, "b", "b", attributes); + handler.characters("abc".toCharArray(), 0, 3); + handler.endElement(null, "b", "b"); + handler.startElement(null, "c", "c", new AttributesImpl()); + handler.endElement(null, "c", "c"); + handler.characters("xyz".toCharArray(), 0, 3); + handler.endElement("uri", "a", "p:a"); + handler.endDocument(); + + String xml = writer.toString(); + assertContains(xml, ""); + assertContains(xml, ""); + assertContains(xml, ""); + assertContains(xml, "xyz"); + assertContains(xml, ""); + } + + private void assertContains(String haystack, String needle) { + if (haystack.indexOf(needle) == -1) { + fail("'" + haystack + "' does not contain '" + needle+ "'"); + } + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/ToXmlContentHandlerTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/ToXmlContentHandlerTest.java new file mode 100644 index 00000000000..b2bd9b56acd --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/ToXmlContentHandlerTest.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.commons.xml; + +import junit.framework.TestCase; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +public class ToXmlContentHandlerTest extends TestCase { + + public void testMinimalDocument() throws SAXException { + ContentHandler handler = new ToXmlContentHandler(); + handler.startDocument(); + handler.startElement("", "test", "test", new AttributesImpl()); + handler.endElement("", "test", "test"); + handler.endDocument(); + assertEquals("", handler.toString()); + } + + public void testAttribute() throws SAXException { + ContentHandler handler = new ToXmlContentHandler(); + handler.startDocument(); + AttributesImpl attributes = new AttributesImpl(); + attributes.addAttribute("", "foo", "foo", "CDATA", "bar"); + handler.startElement("", "test", "test", attributes); + handler.endElement("", "test", "test"); + handler.endDocument(); + assertEquals( + "", + handler.toString()); + } + + public void testAttributeOrder() throws SAXException { + ContentHandler handler; + AttributesImpl attributes; + + handler = new ToXmlContentHandler(); + handler.startDocument(); + attributes = new AttributesImpl(); + attributes.addAttribute("", "foo", "foo", "CDATA", "A"); + attributes.addAttribute("", "bar", "bar", "CDATA", "B"); + handler.startElement("", "test", "test", attributes); + handler.endElement("", "test", "test"); + handler.endDocument(); + assertEquals( + "", + handler.toString()); + + handler = new ToXmlContentHandler(); + handler.startDocument(); + attributes = new AttributesImpl(); + attributes.addAttribute("", "bar", "bar", "CDATA", "B"); + attributes.addAttribute("", "foo", "foo", "CDATA", "A"); + handler.startElement("", "test", "test", attributes); + handler.endElement("", "test", "test"); + handler.endDocument(); + assertEquals( + "", + handler.toString()); + } + + public void testChildElements() throws SAXException { + ContentHandler handler = new ToXmlContentHandler(); + handler.startDocument(); + handler.startElement("", "test", "test", new AttributesImpl()); + handler.startElement("", "foo", "foo", new AttributesImpl()); + handler.endElement("", "foo", "foo"); + handler.startElement("", "bar", "bar", new AttributesImpl()); + handler.endElement("", "bar", "bar"); + handler.endElement("", "test", "test"); + handler.endDocument(); + assertEquals( + "", + handler.toString()); + } + + public void testCharacters() throws SAXException { + ContentHandler handler = new ToXmlContentHandler(); + handler.startDocument(); + handler.startElement("", "test", "test", new AttributesImpl()); + handler.characters("foo".toCharArray(), 0, 3); + handler.endElement("", "test", "test"); + handler.endDocument(); + assertEquals( + "foo", + handler.toString()); + } + + public void testIgnorableWhitespace() throws SAXException { + ContentHandler handler = new ToXmlContentHandler(); + handler.startDocument(); + handler.ignorableWhitespace("\n".toCharArray(), 0, 1); + handler.startElement("", "test", "test", new AttributesImpl()); + handler.ignorableWhitespace("\n".toCharArray(), 0, 1); + handler.endElement("", "test", "test"); + handler.ignorableWhitespace("\n".toCharArray(), 0, 1); + handler.endDocument(); + assertEquals( + "\n\n\n", + handler.toString()); + } + + public void testProcessingInstruction() throws SAXException { + ContentHandler handler = new ToXmlContentHandler(); + handler.startDocument(); + handler.processingInstruction("foo", "abc=\"xyz\""); + handler.startElement("", "test", "test", new AttributesImpl()); + handler.processingInstruction("bar", null); + handler.endElement("", "test", "test"); + handler.endDocument(); + assertEquals( + "" + + "", + handler.toString()); + } + + public void testComplexDocument() throws SAXException { + ContentHandler handler = new ToXmlContentHandler(); + handler.startDocument(); + handler.ignorableWhitespace("\n".toCharArray(), 0, 1); + handler.processingInstruction("foo", "abc=\"xyz\""); + handler.ignorableWhitespace("\n".toCharArray(), 0, 1); + AttributesImpl attributes = new AttributesImpl(); + attributes.addAttribute("", "version", "version", "CDATA", "1.0"); + attributes.addAttribute( + "http://www.w3.org/2000/xmlns/", "xmlns", "xmlns", + "CDATA", "http://x.y.z/"); + attributes.addAttribute( + "http://www.w3.org/2000/xmlns/", "xmlns", "xmlns:abc", + "CDATA", "http://a.b.c/"); + handler.startElement("", "test", "test", attributes); + handler.ignorableWhitespace("\n ".toCharArray(), 0, 3); + handler.characters("abc\n".toCharArray(), 0, 4); + handler.characters(" ".toCharArray(), 0, 2); + attributes = new AttributesImpl(); + attributes.addAttribute("", "escape", "escape", "CDATA", "\"'<>&"); + handler.startElement("http://a.b.c/", "foo", "abc:foo", attributes); + handler.characters("def".toCharArray(), 0, 3); + handler.endElement("http://a.b.c/", "foo", "abc:foo"); + handler.ignorableWhitespace("\n ".toCharArray(), 0, 3); + char[] ch = "".toCharArray(); + handler.characters(ch, 0, ch.length); + handler.characters("\n".toCharArray(), 0, 1); + handler.endElement("", "test", "test"); + handler.characters("\n".toCharArray(), 0, 1); + handler.endDocument(); + assertEquals( + "\n" + + "\n" + + "\n" + + " abc\n" + + " " + + "def\n" + + " <bar a=\"&amp;\" b=''/>\n" + + "\n", + handler.toString()); + } + +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/XmlnsContentHandlerTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/XmlnsContentHandlerTest.java new file mode 100644 index 00000000000..20e1d0ffc3d --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/xml/XmlnsContentHandlerTest.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.commons.xml; + +import junit.framework.TestCase; + +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +public class XmlnsContentHandlerTest extends TestCase { + + public void testXmlns() throws SAXException { + ContentHandler handler = + new XmlnsContentHandler(new ToXmlContentHandler()); + handler.startDocument(); + handler.startPrefixMapping("foo", "http://x.y.z/"); + handler.startPrefixMapping("bar", "http://a.b.c/"); + handler.startElement("", "test", "test", new AttributesImpl()); + handler.startPrefixMapping("foo", "http://a.b.c/"); + handler.startElement("", "tset", "tset", new AttributesImpl()); + handler.endElement("", "tset", "tset"); + handler.endPrefixMapping("foo"); + handler.startElement("", "tset", "tset", new AttributesImpl()); + handler.endElement("", "tset", "tset"); + handler.endElement("", "test", "test"); + handler.endPrefixMapping("bar"); + handler.endPrefixMapping("foo"); + handler.endDocument(); + assertEquals( + "" + + "" + + "" + + "", + handler.toString()); + } + +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/RepositoryStatisticsImplTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/RepositoryStatisticsImplTest.java new file mode 100644 index 00000000000..15fc029fcbd --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/RepositoryStatisticsImplTest.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.stats; + +import java.util.Iterator; +import java.util.Map.Entry; + +import junit.framework.TestCase; +import org.apache.jackrabbit.api.stats.TimeSeries; + +public class RepositoryStatisticsImplTest extends TestCase { + + private static final int DEFAULT_NUMBER_OF_ELEMENTS = 20; + + public void testDefaultIterator() { + RepositoryStatisticsImpl repositoryStatistics = new RepositoryStatisticsImpl(); + + Iterator> iterator = repositoryStatistics.iterator(); + int count = 0; + while (iterator.hasNext()) { + count++; + iterator.next(); + } + assertEquals(DEFAULT_NUMBER_OF_ELEMENTS, count); + } + + public void testIteratorWithSingleCustomType() { + RepositoryStatisticsImpl repositoryStatistics = new RepositoryStatisticsImpl(); + String type = "customType"; + repositoryStatistics.getCounter(type, false); + + Iterator> iterator = repositoryStatistics.iterator(); + int count = 0; + boolean customTypeExists = false; + while (iterator.hasNext()) { + count++; + Entry entry = iterator.next(); + if (type.equals(entry.getKey())) { + customTypeExists = true; + } + } + assertEquals(DEFAULT_NUMBER_OF_ELEMENTS + 1, count); + assertTrue(customTypeExists); + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesAverageTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesAverageTest.java new file mode 100644 index 00000000000..7bb820286d4 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesAverageTest.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.stats; + +import java.util.concurrent.atomic.AtomicLong; + +import junit.framework.TestCase; + +public class TimeSeriesAverageTest extends TestCase { + private TimeSeriesAverage avg; + + public void testAverage() { + TimeSeriesRecorder values = new TimeSeriesRecorder(true); + TimeSeriesRecorder counts = new TimeSeriesRecorder(true); + avg = new TimeSeriesAverage(values, counts); + AtomicLong value = values.getCounter(); + AtomicLong count = counts.getCounter(); + + // initial values + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // no changes in first second + values.recordOneSecond(); + counts.recordOneSecond(); + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // 2 seconds + value.set(42); + count.set(2); + values.recordOneSecond(); + counts.recordOneSecond(); + assertValues(avg.getValuePerSecond(), 21); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // no changes in 3rd second + values.recordOneSecond(); + counts.recordOneSecond(); + assertValues(avg.getValuePerSecond(), 0, 21); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // one minute later + for (int i = 0; i < 60; i++) { + values.recordOneSecond(); + counts.recordOneSecond(); + } + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute(), 21); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // another minute later + for (int i = 0; i < 60; i++) { + values.recordOneSecond(); + counts.recordOneSecond(); + } + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute(), 0, 21); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // one hour + for (int i = 0; i < 60 * 60; i++) { + values.recordOneSecond(); + counts.recordOneSecond(); + } + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour(), 21); + assertValues(avg.getValuePerWeek()); + + // one week + for (int i = 0; i < 7 * 24 * 60 * 60; i++) { + values.recordOneSecond(); + counts.recordOneSecond(); + } + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek(), 21); + } + + public void testAverageWithMissing() { + for (long m : new long[]{-42, 42}) { + TimeSeriesRecorder values = new TimeSeriesRecorder(true); + TimeSeriesRecorder counts = new TimeSeriesRecorder(true); + avg = new TimeSeriesAverage(values, counts, m); + AtomicLong value = values.getCounter(); + AtomicLong count = counts.getCounter(); + + // initial values + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // no changes in first second + values.recordOneSecond(); + counts.recordOneSecond(); + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // 2 seconds + value.set(42); + count.set(2); + values.recordOneSecond(); + counts.recordOneSecond(); + assertValues(avg.getValuePerSecond(), 21); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // no changes in 3rd second + values.recordOneSecond(); + counts.recordOneSecond(); + assertValues(avg.getValuePerSecond(), avg.getMissingValue(), 21); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // Division by 0 reported as missing + value.set(1); + count.set(0); + values.recordOneSecond(); + counts.recordOneSecond(); + + // one minute later + for (int i = 0; i < 60; i++) { + values.recordOneSecond(); + counts.recordOneSecond(); + } + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute(), 21); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // another minute later + for (int i = 0; i < 60; i++) { + values.recordOneSecond(); + counts.recordOneSecond(); + } + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute(), avg.getMissingValue(), 21); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek()); + + // one hour + for (int i = 0; i < 60 * 60; i++) { + values.recordOneSecond(); + counts.recordOneSecond(); + } + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour(), 21); + assertValues(avg.getValuePerWeek()); + + // one week + for (int i = 0; i < 7 * 24 * 60 * 60; i++) { + values.recordOneSecond(); + counts.recordOneSecond(); + } + assertValues(avg.getValuePerSecond()); + assertValues(avg.getValuePerMinute()); + assertValues(avg.getValuePerHour()); + assertValues(avg.getValuePerWeek(), 21); + } + } + + private void assertValues(long[] values, long... expected) { + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], values[values.length - i - 1]); + } + for (int i = expected.length; i < values.length; i++) { + assertEquals(avg.getMissingValue(), values[values.length - i - 1]); + } + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesMaxTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesMaxTest.java new file mode 100644 index 00000000000..3558834c19b --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesMaxTest.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.stats; + +import junit.framework.TestCase; + +public class TimeSeriesMaxTest extends TestCase { + private TimeSeriesMax max; + + public void testMax() { + max = new TimeSeriesMax(); + + // initial values + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // no changes in first second + max.recordOneSecond(); + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // 2 seconds + max.recordValue(42); + max.recordOneSecond(); + assertValues(max.getValuePerSecond(), 42); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // no changes in 3rd second + max.recordOneSecond(); + assertValues(max.getValuePerSecond(), 0, 42); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // 4th second + max.recordValue(99); + max.recordOneSecond(); + assertValues(max.getValuePerSecond(), 99, 0, 42); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // one minute later + for (int i = 0; i < 60; i++) { + max.recordOneSecond(); + } + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute(), 99); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // another minute later + for (int i = 0; i < 60; i++) { + max.recordOneSecond(); + } + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute(), 0, 99); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // one hour + for (int i = 0; i < 60 * 60; i++) { + max.recordOneSecond(); + } + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour(), 99); + assertValues(max.getValuePerWeek()); + + // one week + for (int i = 0; i < 7 * 24 * 60 * 60; i++) { + max.recordOneSecond(); + } + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek(), 99); + } + + public void testMaxWithMissing() { + for (long m : new long[]{-42, 42}) { + max = new TimeSeriesMax(m); + + // initial values + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // no changes in first second + max.recordOneSecond(); + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // 2 seconds + max.recordValue(42); + max.recordOneSecond(); + assertValues(max.getValuePerSecond(), 42); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // no changes in 3rd second + max.recordOneSecond(); + assertValues(max.getValuePerSecond(), max.getMissingValue(), 42); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // 4th second + max.recordValue(99); + max.recordOneSecond(); + assertValues(max.getValuePerSecond(), 99, max.getMissingValue(), 42); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // one minute later + for (int i = 0; i < 60; i++) { + max.recordOneSecond(); + } + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute(), 99); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // another minute later + for (int i = 0; i < 60; i++) { + max.recordOneSecond(); + } + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute(), max.getMissingValue(), 99); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek()); + + // one hour + for (int i = 0; i < 60 * 60; i++) { + max.recordOneSecond(); + } + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour(), 99); + assertValues(max.getValuePerWeek()); + + // one week + for (int i = 0; i < 7 * 24 * 60 * 60; i++) { + max.recordOneSecond(); + } + assertValues(max.getValuePerSecond()); + assertValues(max.getValuePerMinute()); + assertValues(max.getValuePerHour()); + assertValues(max.getValuePerWeek(), 99); + } + } + + private void assertValues(long[] values, long... expected) { + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], values[values.length - i - 1]); + } + for (int i = expected.length; i < values.length; i++) { + assertEquals(max.getMissingValue(), values[values.length - i - 1]); + } + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesRecorderTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesRecorderTest.java new file mode 100644 index 00000000000..31416a981d2 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/stats/TimeSeriesRecorderTest.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.stats; + +import java.util.concurrent.atomic.AtomicLong; + +import junit.framework.TestCase; +import org.apache.jackrabbit.api.stats.RepositoryStatistics; + +public class TimeSeriesRecorderTest extends TestCase { + + private TimeSeriesRecorder recorder; + + public void testCounter() { + recorder = new TimeSeriesRecorder(RepositoryStatistics.Type.SESSION_READ_COUNTER); + AtomicLong counter = recorder.getCounter(); + + // initial values + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // no changes in first second + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // one increment in second + counter.incrementAndGet(); + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond(), 1); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // two increments in second + counter.incrementAndGet(); + counter.incrementAndGet(); + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond(), 2, 1); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // no changes in a second + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond(), 0, 2, 1); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // ten increments in a second + counter.addAndGet(10); + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond(), 10, 0, 2, 1); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // one minute + for (int i = 0; i < 60; i++) { + recorder.recordOneSecond(); + } + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute(), 13); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // second minute + for (int i = 0; i < 60; i++) { + recorder.recordOneSecond(); + } + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute(), 0, 13); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // one hour + for (int i = 0; i < 60 * 60; i++) { + recorder.recordOneSecond(); + } + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour(), 13); + assertValues(recorder.getValuePerWeek()); + + // one week + for (int i = 0; i < 7 * 24 * 60 * 60; i++) { + recorder.recordOneSecond(); + } + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek(), 13); + } + + public void testCounterWithMissing() { + for (long m : new long[]{-42, 42}) { + recorder = new TimeSeriesRecorder(true, m); + AtomicLong counter = recorder.getCounter(); + + // initial values + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // no changes in first second + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // one increment in second + counter.set(0); + counter.incrementAndGet(); + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond(), 1); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // two increments in second + counter.set(0); + counter.incrementAndGet(); + counter.incrementAndGet(); + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond(), 2, 1); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // no changes in a second + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond(), recorder.getMissingValue(), 2, 1); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // ten increments in a second + counter.set(0); + counter.addAndGet(10); + recorder.recordOneSecond(); + assertValues(recorder.getValuePerSecond(), 10, recorder.getMissingValue(), 2, 1); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // one minute + for (int i = 0; i < 60; i++) { + recorder.recordOneSecond(); + } + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute(), 13); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // second minute + for (int i = 0; i < 60; i++) { + recorder.recordOneSecond(); + } + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute(), recorder.getMissingValue(), 13); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek()); + + // one hour + for (int i = 0; i < 60 * 60; i++) { + recorder.recordOneSecond(); + } + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour(), 13); + assertValues(recorder.getValuePerWeek()); + + // one week + for (int i = 0; i < 7 * 24 * 60 * 60; i++) { + recorder.recordOneSecond(); + } + assertValues(recorder.getValuePerSecond()); + assertValues(recorder.getValuePerMinute()); + assertValues(recorder.getValuePerHour()); + assertValues(recorder.getValuePerWeek(), 13); + } + } + + private void assertValues(long[] values, long... expected) { + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], values[values.length - i - 1]); + } + for (int i = expected.length; i < values.length; i++) { + assertEquals(recorder.getMissingValue(), values[values.length - i - 1]); + } + } + +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/Base64Test.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/Base64Test.java new file mode 100644 index 00000000000..ac1b461d80b --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/Base64Test.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.util; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.util.Random; +import java.util.Arrays; + +/** + * Test cases for Base64 encode / decode. + */ +public class Base64Test extends TestCase { + + private Random _random = new Random(); + + /** + * @return Returns the _random. + */ + protected Random getRandom() { + return this._random; + } + + /** + * Tests that whitespace characters are ignored within base64 data. + */ + public void testWhitespace() throws Exception { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + Base64.decode(" d G\tV \tzdA\n= =\n", buffer); + byte[] data = buffer.toByteArray(); + assertEquals("test", new String(data, "US-ASCII")); + } + + /** + * Tests that base 64 encoding/decoding round trips are lossless. + */ + public void testBase64() throws Exception { + base64RoundTrip(new byte[0]); + + for (int i = 0; i < 10000; i++) { + base64RoundTrip(); + } + } + + public void testBase64Streaming() throws Exception { + byte[] data = new byte[0x100000]; + getRandom().nextBytes(data); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Base64.encode(new ByteArrayInputStream(data), baos); + byte[] encData = baos.toByteArray(); + + baos = new ByteArrayOutputStream(); + Base64.decode(new ByteArrayInputStream(encData), baos); + byte[] decData = baos.toByteArray(); + assertTrue(Arrays.equals(data, decData)); + } + + private void base64RoundTrip() throws Exception { + int len = getRandom().nextInt(0x1000); + byte[] b1 = new byte[len]; + getRandom().nextBytes(b1); + + base64RoundTrip(b1); + } + + private void base64RoundTrip(byte[] ba) throws Exception { + StringWriter sw = new StringWriter(); + Base64.encode(ba, 0, ba.length, sw); + String s = sw.toString(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Base64.decode(s, baos); + byte[] ba2 = baos.toByteArray(); + assertNotNull(ba2); + assertEquals(ba.length, ba2.length); + + for (int i = 0; i < ba.length; i++) { + assertEquals(ba[i], ba2[i]); + } + } + + public void testDecodeOrEncode() throws IOException { + assertEquals("", Base64.decodeOrEncode(Base64.decodeOrEncode(""))); + assertEquals("test", Base64.decodeOrEncode(Base64.decodeOrEncode("test"))); + assertEquals("{base64}dGVzdA==", Base64.decodeOrEncode("test")); + assertEquals("test", Base64.decodeOrEncode("{base64}dGVzdA==")); + } + + public void testDecodeIfEncoded() throws IOException { + assertEquals(null, Base64.decodeIfEncoded(null)); + assertEquals("", Base64.decodeIfEncoded("")); + assertEquals("", Base64.decodeIfEncoded("{base64}")); + assertEquals("test", Base64.decodeIfEncoded("test")); + assertEquals("test", Base64.decodeIfEncoded("{base64}dGVzdA==")); + } + + public void testStringEncodeDecode() throws IOException { + assertEquals("", Base64.decode(Base64.encode(""))); + assertEquals("test", Base64.decode(Base64.encode("test"))); + assertEquals("dGVzdA==", Base64.encode("test")); + assertEquals("test", Base64.decode("dGVzdA==")); + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/ISO9075Test.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/ISO9075Test.java new file mode 100644 index 00000000000..6a98eb926ed --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/ISO9075Test.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.util; + +import junit.framework.TestCase; + +/** + * Test cases for ISO9075 encode / decode. + */ +public class ISO9075Test extends TestCase { + + public void testSpecExamples() { + assertEquals("My_x0020_Documents", ISO9075.encode("My Documents")); + assertEquals("_x0031_234id", ISO9075.encode("1234id")); + assertEquals("My_Documents", ISO9075.encode("My_Documents")); + assertEquals("My_x005f_x0020Documents", ISO9075.encode("My_x0020Documents")); + assertEquals("My_x005f_x0020_Documents", ISO9075.encode("My_x0020_Documents")); + assertEquals("My_x005f_x0020_", ISO9075.encode("My_x0020_")); + assertEquals("My_x002", ISO9075.encode("My_x002")); + assertEquals("My_x005f_x0020", ISO9075.encode("My_x0020")); + assertEquals("My_", ISO9075.encode("My_")); + assertEquals("My_x005f_x0020_x0020_Documents", ISO9075.encode("My_x0020 Documents")); + } + + public void testMatcherEscapes() { + assertEquals( + "StringWith$inside", ISO9075.decode("StringWith$inside")); + assertEquals( + "StringWith$inside", ISO9075.decode("StringWith_x0024_inside")); + assertEquals( + "StringWith_x0024_inside", ISO9075.encode("StringWith$inside")); + assertEquals( + "StringWith\\inside", ISO9075.decode("StringWith\\inside")); + assertEquals( + "StringWith\\inside", ISO9075.decode("StringWith_x005c_inside")); + assertEquals( + "StringWith_x005c_inside", ISO9075.encode("StringWith\\inside")); + } + + public void testPath() { + assertEquals("foo/bar", ISO9075.encodePath("foo/bar")); + assertEquals("/foo/bar", ISO9075.encodePath("/foo/bar")); + assertEquals("/foo/bar/", ISO9075.encodePath("/foo/bar/")); + assertEquals("/foo/bar[3]", ISO9075.encodePath("/foo/bar[3]")); + assertEquals("home/My_x0020_Documents", ISO9075.encodePath("home/My Documents")); + assertEquals("year/_x0032_007", ISO9075.encodePath("year/2007")); + assertEquals("year/_x0032_007[2]", ISO9075.encodePath("year/2007[2]")); + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/JcrUtilsTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/JcrUtilsTest.java new file mode 100644 index 00000000000..a8b0d9b8fd9 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/JcrUtilsTest.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.util; + +import junit.framework.TestCase; +import org.apache.jackrabbit.commons.JcrUtils; + +import javax.jcr.PropertyType; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * JcrUtilsTest... + */ +public class JcrUtilsTest extends TestCase { + + public void testGetPropertyTypeNames() { + String[] names = JcrUtils.getPropertyTypeNames(true); + assertEquals(13, names.length); + Set nameSet = new HashSet(Arrays.asList(names)); + assertEquals(13, nameSet.size()); + assertTrue(nameSet.contains(PropertyType.TYPENAME_UNDEFINED)); + + names = JcrUtils.getPropertyTypeNames(false); + assertEquals(12, names.length); + nameSet = new HashSet(Arrays.asList(names)); + assertEquals(12, nameSet.size()); + assertFalse(nameSet.contains(PropertyType.TYPENAME_UNDEFINED)); + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/TextTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/TextTest.java new file mode 100644 index 00000000000..1375aff26ff --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/TextTest.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.util; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import junit.framework.TestCase; + +/** + * Test cases for the Text utility class. + */ +public class TextTest extends TestCase { + + private void checkEscape(String name) { + String escaped = Text.escapeIllegalJcrChars(name); + assertEquals(name, Text.unescapeIllegalJcrChars(escaped)); + } + + public void testEscape() { + // Handling of normal names + checkEscape("foobar"); + // Handling of short names + checkEscape("x"); + checkEscape("xx"); + checkEscape("xxx"); + // Handling of spaces + checkEscape("x x"); + checkEscape("x x x"); + checkEscape(" "); + checkEscape(" x"); + checkEscape("x "); + checkEscape(" "); + checkEscape(" "); + checkEscape(" x "); + // Handling of dots within the first two characters + checkEscape("."); + checkEscape(".."); + checkEscape(".x"); + checkEscape("x."); + checkEscape(". "); + checkEscape(" ."); + // Handling of the special characters + checkEscape("%/:[]*'\"|\t\r\n"); + } + + public void testIsDescendant() { + String parent = "/"; + List descendants = new ArrayList(); + descendants.add("/a"); + descendants.add("/a/b"); + for (Iterator it = descendants.iterator(); it.hasNext();) { + String desc = it.next().toString(); + assertTrue(desc + " must be descendant of " + parent, Text.isDescendant(parent, desc)); + } + List nonDescendants = new ArrayList(); + nonDescendants.add("/"); + nonDescendants.add("a"); + for (Iterator it = nonDescendants.iterator(); it.hasNext();) { + String nonDesc = it.next().toString(); + assertFalse(nonDesc + " isn't a descendant of " + parent,Text.isDescendant(parent, nonDesc)); + } + + parent = "/a/b"; + descendants = new ArrayList(); + descendants.add("/a/b/c"); + descendants.add("/a/b/c/"); + for (Iterator it = descendants.iterator(); it.hasNext();) { + String desc = it.next().toString(); + assertTrue(desc + " must be descendant of " + parent, Text.isDescendant(parent, desc)); + } + nonDescendants = new ArrayList(); + nonDescendants.add("/"); + nonDescendants.add("/a"); + nonDescendants.add("/a/b"); + nonDescendants.add("/a/b/"); + nonDescendants.add("/d"); + nonDescendants.add("/d/b"); + for (Iterator it = nonDescendants.iterator(); it.hasNext();) { + String nonDesc = it.next().toString(); + assertFalse(nonDesc + " isn't a descendant of " + parent, Text.isDescendant(parent, nonDesc)); + } + } + + public void testGetName() { + List l = new ArrayList(); + l.add(new String[] {"/a/b/c", "c"}); + l.add(new String[] {"c", "c"}); + l.add(new String[] {null, null}); + l.add(new String[] {"", ""}); + l.add(new String[] {"/", ""}); + l.add(new String[] {"http://jackrabbit.apache.org/jackrabbit", "jackrabbit"}); + l.add(new String[] {"http://jackrabbit.apache.org/jackrabbit/", ""}); + + for (Iterator it = l.iterator(); it.hasNext();) { + String[] strs = (String[]) it.next(); + assertEquals(strs[1], Text.getName(strs[0])); + } + + // Text.getName(String path, boolean ignoreTrailingSlash) + l = new ArrayList(); + l.add(new String[] {"http://jackrabbit.apache.org/jackrabbit", "jackrabbit"}); + l.add(new String[] {"http://jackrabbit.apache.org/jackrabbit/", "jackrabbit"}); + + for (Iterator it = l.iterator(); it.hasNext();) { + String[] strs = (String[]) it.next(); + assertEquals(strs[1], Text.getName(strs[0], true)); + } + + // Text.getName(String path, char delim) + l = new ArrayList(); + l.add(new String[] {"/a=b/c", "b/c"}); + l.add(new String[] {"c", "c"}); + l.add(new String[] {null, null}); + l.add(new String[] {"", ""}); + l.add(new String[] {"http:/=jackrabbit.apache.org/jackrabbit", "jackrabbit.apache.org/jackrabbit"}); + l.add(new String[] {"http:=//jackrabbit.apache.org/jackrabbit/", "//jackrabbit.apache.org/jackrabbit/"}); + + for (Iterator it = l.iterator(); it.hasNext();) { + String[] strs = (String[]) it.next(); + assertEquals(strs[1], Text.getName(strs[0], '=')); + } + } + + public void testUrlEscape() { + String testString = "\u4e2d\u56fd\u7684\u7f51\u9875 $% \u20acuro %$ "; + + String escaped = testString + .replaceAll("%", "%25") + .replaceAll(" ", "%20"); + String unescaped = Text.unescape(escaped); + assertEquals(testString, unescaped); + + escaped = Text.escape(testString); + unescaped = Text.unescape(escaped); + assertEquals(testString, unescaped); + + assertEquals("%", Text.unescape("%25")); + assertEquals("", Text.unescape("")); + assertEquals("\u4e2d\u56fd\u7684\u7f51\u9875", Text.unescape("\u4e2d\u56fd\u7684\u7f51\u9875")); + } + + /** + * @see JCR-1926 + */ + public void testUnescapeWithInvalidInput() { + assertInvalidUnescape("%"); // too short + assertInvalidUnescape("anything%"); // too short + assertInvalidUnescape("%%"); // too short + assertInvalidUnescape("anything%%"); // too short + assertInvalidUnescape("%1"); // too short + assertInvalidUnescape("anything%1"); // too short + assertInvalidUnescape("%%%"); // not a number + assertInvalidUnescape("%ag"); // not a number + assertInvalidUnescape("anything%%%"); // not a number + assertInvalidUnescape("anything%ag"); // not a number + assertInvalidUnescape("anything%%%anything"); // not a number + assertInvalidUnescape("anything%aganything"); // not a number + } + + private void assertInvalidUnescape(String string) { + try { + Text.unescape(string); + fail("Text.unescape(" + string + ") should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } catch (RuntimeException unexpected) { + fail("Text.unescape(" + string + "): " + unexpected.getMessage()); + } + } + + public void testEscapeIllegalJcr10Chars() throws Exception { + // single and double quote are illegal in JCR 1.0 + assertEquals("local%27name", Text.escapeIllegalJcr10Chars("local'name")); + assertEquals("local%22name", Text.escapeIllegalJcr10Chars("local\"name")); + } + + public void testEscapeIllegalJcrChars() throws Exception { + // single and double quote are valid since JCR 2.0 + assertEquals("local'name", Text.escapeIllegalJcrChars("local'name")); + assertEquals("local\"name", Text.escapeIllegalJcrChars("local\"name")); + } + + public void testEscapeXML() { + assertEquals("&<>'"", Text.encodeIllegalXMLCharacters("&<>'\"")); + } + + public void testEscapeHTML() { + assertEquals("&<>'"", Text.encodeIllegalHTMLCharacters("&<>'\"")); + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/TimerTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/TimerTest.java new file mode 100644 index 00000000000..98a445a72d3 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/util/TimerTest.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.util; + +import junit.framework.TestCase; + +/** + * TimerTest checks if the internal thread of a timer is stopped + * after {@link Timer#IDLE_TIME} elapsed. + */ +public class TimerTest extends TestCase { + + private Timer timer; + + private DummyTask task; + + protected void setUp() throws Exception { + super.setUp(); + timer = new Timer(true); + task = new DummyTask(); + } + + protected void tearDown() throws Exception { + timer.cancel(); + super.tearDown(); + } + + public void testInitiallyNotRunning() { + assertTrue("Timer must not be running without a scheduled task", !timer.isRunning()); + } + + public void testIsRunning() { + timer.schedule(task, 0, Integer.MAX_VALUE); + assertTrue("Timer must be running with a scheduled task", timer.isRunning()); + } + + public void testLongDelay() throws InterruptedException { + int testDelay = Timer.IDLE_TIME + 1000; + timer.schedule(task, testDelay, Integer.MAX_VALUE); + Thread.sleep(testDelay); + assertTrue("Timer must be running with a scheduled task", timer.isRunning()); + } + + public void testIdle() throws InterruptedException { + timer.schedule(task, 0, Integer.MAX_VALUE); + task.waitUntilRun(); + task.cancel(); + assertTrue("Timer must be running while idle", timer.isRunning()); + Thread.sleep(Timer.IDLE_TIME + 2 * Timer.CHECKER_INTERVAL); + assertTrue("Timer must not be running after idle time elapsed", !timer.isRunning()); + } + + private static class DummyTask extends Timer.Task { + + private boolean run = false; + + public void run() { + synchronized (this) { + run = true; + notifyAll(); + } + } + + public void waitUntilRun() throws InterruptedException { + synchronized (this) { + while (!run) { + wait(); + } + } + } + } +} diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/value/BinaryValueTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/value/BinaryValueTest.java new file mode 100644 index 00000000000..32b2aad0438 --- /dev/null +++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/value/BinaryValueTest.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.value; + +import junit.framework.TestCase; + +/** + * Test cases for binary values. + */ +public class BinaryValueTest extends TestCase { + + private static final byte[] DATA = "abc".getBytes(); + + public void testBinaryValueEquals() throws Exception { + assertFalse(new BinaryValue(DATA).equals(null)); + assertFalse(new BinaryValue(DATA).equals(new Object())); + + // TODO: JCR-320 Binary value equality + // assertTrue(new BinaryValue(DATA).equals(new BinaryValue(DATA))); + // assertTrue(new BinaryValue(DATA).equals( + // new BinaryValue(new ByteArrayInputStream(DATA)))); + // assertTrue(new BinaryValue(new ByteArrayInputStream(DATA)).equals( + // new BinaryValue(DATA))); + // assertTrue(new BinaryValue(DATA).equals( + // new BinaryValue(new String(DATA)))); + // assertTrue(new BinaryValue(new String(DATA)).equals( + // new BinaryValue(DATA))); + } + +} diff --git a/jackrabbit-jcr-rmi/HEADER.txt b/jackrabbit-jcr-rmi/HEADER.txt new file mode 100644 index 00000000000..ae6f28c4a1c --- /dev/null +++ b/jackrabbit-jcr-rmi/HEADER.txt @@ -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. + */ diff --git a/jackrabbit-jcr-rmi/README.txt b/jackrabbit-jcr-rmi/README.txt new file mode 100644 index 00000000000..837df5bb00d --- /dev/null +++ b/jackrabbit-jcr-rmi/README.txt @@ -0,0 +1,12 @@ +=================================================== +Apache Jackrabbit JCR-RMI +=================================================== + +JCR-RMI is a transparent Remote Method Invocation (RMI) layer for JCR. +The layer makes it possible to remotely access JCR content repositories. + +Note that due to changes in the way the RMI client is built, versions since +2.16 will not be able to connect to older versions of the server. If +compatibility with older servers is needed, just use the client from a +recent 2.14 release. + diff --git a/jackrabbit-jcr-rmi/checkstyle.xml b/jackrabbit-jcr-rmi/checkstyle.xml new file mode 100644 index 00000000000..33678aa8981 --- /dev/null +++ b/jackrabbit-jcr-rmi/checkstyle.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-jcr-rmi/pom.xml b/jackrabbit-jcr-rmi/pom.xml new file mode 100644 index 00000000000..ea6e9a17b6e --- /dev/null +++ b/jackrabbit-jcr-rmi/pom.xml @@ -0,0 +1,572 @@ + + + + + + 4.0.0 + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + + + + + jackrabbit-jcr-rmi + bundle + + Jackrabbit JCR-RMI + + JCR-RMI is a transparent Remote Method Invocation (RMI) layer for the + Content Repository for Java Technology API (JCR). The layer makes it + possible to remotely access JCR content repositories. + + + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + jcr,jackrabbit + http://jackrabbit.apache.org/ + + org.apache.jackrabbit.rmi.*;version=3.1.0 + + + + javax.transaction.xa;resolution:=optional,* + + + + jackrabbit-jcr-commons;inline=org/apache/jackrabbit/commons/iterator/RangeIteratorAdapter* + + + + + + org.apache.rat + apache-rat-plugin + + + src/main/javadoc/**/*.uxf + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + org.apache.maven.plugins + + + maven-antrun-plugin + + [1.6,) + + run + + + + + + + + + + + + + + + + + integrationTesting + + + + maven-surefire-plugin + + ${test.opts} + true + + + jackrabbit.test.integration + true + + + known.issues + +org.apache.jackrabbit.test.api.ShareableNodeTest +org.apache.jackrabbit.test.api.SetValueValueFormatExceptionTest#testNodeNotReferenceable + +org.apache.jackrabbit.test.api.NameTest#testExpandedNameValue +org.apache.jackrabbit.test.api.NameTest#testExpandedNameValueProperty + +org.apache.jackrabbit.test.api.NodeRemoveMixinTest#testNotAssigned +org.apache.jackrabbit.test.api.ValueFactoryTest#testValueFormatException + +org.apache.jackrabbit.test.api.GetWeakReferencesTest#testMultiValues +org.apache.jackrabbit.test.api.GetWeakReferencesTest#testSingleValue + +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testEmptyNodeDefinitionTemplate +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testEmptyNodeTypeTemplate +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testEmptyPropertyDefinitionTemplate +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testInvalidJCRNames +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testNewNodeTypeTemplate +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testNodeDefinitionTemplate +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testNonEmptyNodeTypeTemplate +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testPropertyDefinitionTemplate +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testRegisterNodeType +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testRegisterNodeTypes +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testResidualNames +org.apache.jackrabbit.test.api.nodetype.NodeTypeCreationTest#testSetDefaultValues + +org.apache.jackrabbit.test.api.observation.GetUserDataTest#testSave +org.apache.jackrabbit.test.api.observation.GetUserDataTest#testVersioning +org.apache.jackrabbit.test.api.observation.GetUserDataTest#testWorkspaceOperation +org.apache.jackrabbit.test.api.observation.NodeReorderTest#testNodeReorderMove + +org.apache.jackrabbit.test.api.PathTest#testResolvedIdentifierBasedPropertyValue + +org.apache.jackrabbit.test.api.query.CreateQueryTest#testUnknownQueryLanguage +org.apache.jackrabbit.test.api.query.DerefQueryLevel1Test#testDerefMultiPropWithNodeStar +org.apache.jackrabbit.test.api.query.DerefQueryLevel1Test#testDerefMultiPropWithNodeTest +org.apache.jackrabbit.test.api.query.DerefQueryLevel1Test#testDerefSinglePropWithNodeStar +org.apache.jackrabbit.test.api.query.DerefQueryLevel1Test#testDerefSinglePropWithNodeTest +org.apache.jackrabbit.test.api.query.ElementTest#testElementTest +org.apache.jackrabbit.test.api.query.ElementTest#testElementTestAnyNode +org.apache.jackrabbit.test.api.query.ElementTest#testElementTestAnyNodeNtBase +org.apache.jackrabbit.test.api.query.ElementTest#testElementTestAnyNodeSomeNT +org.apache.jackrabbit.test.api.query.ElementTest#testElementTestNameTest +org.apache.jackrabbit.test.api.query.ElementTest#testElementTestNameTestNtBase +org.apache.jackrabbit.test.api.query.ElementTest#testElementTestNameTestSomeNT +org.apache.jackrabbit.test.api.query.ElementTest#testElementTestNameTestSomeNTWithSNS +org.apache.jackrabbit.test.api.query.GetLanguageTest#testGetLanguage +org.apache.jackrabbit.test.api.query.GetLanguageTest#testJCRQOM +org.apache.jackrabbit.test.api.query.GetLanguageTest#testJCRSQL2 +org.apache.jackrabbit.test.api.query.GetLanguageTest#testSQL +org.apache.jackrabbit.test.api.query.GetPersistentQueryPathLevel1Test#testGetStoredQueryPath +org.apache.jackrabbit.test.api.query.GetPersistentQueryPathTest#testGetPersistentQueryPath +org.apache.jackrabbit.test.api.query.GetPropertyNamesTest#testGetPropertyNames +org.apache.jackrabbit.test.api.query.GetStatementTest#testGetStatement +org.apache.jackrabbit.test.api.query.GetSupportedQueryLanguagesTest#testGetSupportedQueryLanguages +org.apache.jackrabbit.test.api.query.OrderByDateTest#testDateOrder +org.apache.jackrabbit.test.api.query.OrderByDateTest#testDateOrderMillis +org.apache.jackrabbit.test.api.query.OrderByDateTest#testDateOrderNegativeTimeZone +org.apache.jackrabbit.test.api.query.OrderByDateTest#testDateOrderPositiveTimeZone +org.apache.jackrabbit.test.api.query.OrderByDecimalTest#testDecimal +org.apache.jackrabbit.test.api.query.OrderByDoubleTest#testDoubleOrder1 +org.apache.jackrabbit.test.api.query.OrderByDoubleTest#testDoubleOrder2 +org.apache.jackrabbit.test.api.query.OrderByLengthTest#testLength +org.apache.jackrabbit.test.api.query.OrderByLocalNameTest#testLocalName +org.apache.jackrabbit.test.api.query.OrderByLongTest#testIntegerOrder +org.apache.jackrabbit.test.api.query.OrderByLowerCaseTest#testLowerCase +org.apache.jackrabbit.test.api.query.OrderByMultiTypeTest#testMultipleOrder +org.apache.jackrabbit.test.api.query.OrderByNameTest#testName +org.apache.jackrabbit.test.api.query.OrderByStringTest#testStringOrder +org.apache.jackrabbit.test.api.query.OrderByUpperCaseTest#testLowerCase +org.apache.jackrabbit.test.api.query.OrderByURITest#testURI +org.apache.jackrabbit.test.api.query.PredicatesTest#testAnd +org.apache.jackrabbit.test.api.query.PredicatesTest#testCombinedAnd +org.apache.jackrabbit.test.api.query.PredicatesTest#testCombinedOr +org.apache.jackrabbit.test.api.query.PredicatesTest#testEquality +org.apache.jackrabbit.test.api.query.PredicatesTest#testOr +org.apache.jackrabbit.test.api.query.qom.AndConstraintTest#testAnd +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testBindVariableNames +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testBoolean +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testDate +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testDecimal +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testDouble +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testIllegalArgumentException +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testLong +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testName +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testPath +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testReference +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testString +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testURI +org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest#testWeakReference +org.apache.jackrabbit.test.api.query.qom.ChildNodeJoinConditionTest#testInnerJoin +org.apache.jackrabbit.test.api.query.qom.ChildNodeJoinConditionTest#testLeftOuterJoin +org.apache.jackrabbit.test.api.query.qom.ChildNodeJoinConditionTest#testRightOuterJoin +org.apache.jackrabbit.test.api.query.qom.ChildNodeTest#testChildNode +org.apache.jackrabbit.test.api.query.qom.ChildNodeTest#testChildNodes +org.apache.jackrabbit.test.api.query.qom.ChildNodeTest#testChildNodesDoNotMatchSelector +org.apache.jackrabbit.test.api.query.qom.ChildNodeTest#testNotASelectorName +org.apache.jackrabbit.test.api.query.qom.ChildNodeTest#testPathDoesNotExist +org.apache.jackrabbit.test.api.query.qom.ChildNodeTest#testRelativePath +org.apache.jackrabbit.test.api.query.qom.ChildNodeTest#testSyntacticallyInvalidPath +org.apache.jackrabbit.test.api.query.qom.ColumnTest#testColumnNames +org.apache.jackrabbit.test.api.query.qom.ColumnTest#testExpandColumnsForNodeType +org.apache.jackrabbit.test.api.query.qom.ColumnTest#testMultiColumn +org.apache.jackrabbit.test.api.query.qom.DescendantNodeJoinConditionTest#testInnerJoin +org.apache.jackrabbit.test.api.query.qom.DescendantNodeJoinConditionTest#testLeftOuterJoin +org.apache.jackrabbit.test.api.query.qom.DescendantNodeJoinConditionTest#testRightOuterJoin +org.apache.jackrabbit.test.api.query.qom.DescendantNodeTest#testDescendantNode +org.apache.jackrabbit.test.api.query.qom.DescendantNodeTest#testDescendantNodes +org.apache.jackrabbit.test.api.query.qom.DescendantNodeTest#testDescendantNodesDoNotMatchSelector +org.apache.jackrabbit.test.api.query.qom.DescendantNodeTest#testNotASelectorName +org.apache.jackrabbit.test.api.query.qom.DescendantNodeTest#testPathDoesNotExist +org.apache.jackrabbit.test.api.query.qom.DescendantNodeTest#testRelativePath +org.apache.jackrabbit.test.api.query.qom.DescendantNodeTest#testSyntacticallyInvalidPath +org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testInnerJoin1 +org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testInnerJoin2 +org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testLeftOuterJoin1 +org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testLeftOuterJoin2 +org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testRightOuterJoin1 +org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testRightOuterJoin2 +org.apache.jackrabbit.test.api.query.qom.FullTextSearchScoreTest#testConstraint +org.apache.jackrabbit.test.api.query.qom.FullTextSearchScoreTest#testOrdering +org.apache.jackrabbit.test.api.query.qom.GetQueryTest#testGetQuery +org.apache.jackrabbit.test.api.query.qom.GetQueryTest#testInvalidQueryException +org.apache.jackrabbit.test.api.query.qom.LengthTest#testBinaryLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testBooleanLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testDateLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testDecimalLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testDoubleLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthBinaryLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthBooleanLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthDateLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthDecimalLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthDoubleLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthNameLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthPathLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthReferenceLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthStringLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthURILiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLengthWeakReferenceLiteral +org.apache.jackrabbit.test.api.query.qom.LengthTest#testLongLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testNameLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testPathLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testReferenceLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testStringLength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testURILength +org.apache.jackrabbit.test.api.query.qom.LengthTest#testWeakReferenceLength +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testBinaryLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testBooleanLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testDateLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testDecimalLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testDoubleLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testEqualTo +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testGreaterThan +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testGreaterThanOrEqualTo +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testLessThan +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testLessThanOrEqualTo +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testLike +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testLongLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testNameLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testNotEqualTo +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testPathLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testReferenceLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testStringLiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testStringLiteralInvalidName +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testURILiteral +org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testWeakReferenceLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testBinaryLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testBooleanLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testDateLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testDecimalLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testDoubleLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testEqualTo +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testLongLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testNameLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testNotEqualTo +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testPathLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testReferenceLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testStringLiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testStringLiteralInvalidName +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testURILiteral +org.apache.jackrabbit.test.api.query.qom.NodeNameTest#testWeakReferenceLiteral +org.apache.jackrabbit.test.api.query.qom.NotConstraintTest#testNot +org.apache.jackrabbit.test.api.query.qom.OrConstraintTest#testOr +org.apache.jackrabbit.test.api.query.qom.OrderingTest#testMultipleSelectors +org.apache.jackrabbit.test.api.query.qom.PropertyExistenceTest#testPropertyExistence +org.apache.jackrabbit.test.api.query.qom.PropertyValueTest#testPropertyExistence +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testAnd +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testBindVariableValue +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testChildNode +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testChildNodeJoinCondition +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testChildNodeWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testColumn +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testColumnAllProperties +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testColumnWithColumnName +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testColumnWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testComparison +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testCreateQuery +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testCreateQueryFromSource +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testCreateQueryFromSourceWithConstraint +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testCreateQueryFromSourceWithConstraintAndOrdering +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testCreateQueryFromSourceWithConstraintOrderingAndColumn +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testCreateQueryWithConstraint +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testCreateQueryWithConstraintAndOrdering +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testCreateQueryWithConstraintOrderingAndColumn +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testDescendantNode +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testDescendantNodeJoinCondition +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testDescendantNodeWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testEquiJoinCondition +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testFullTextSearch +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testFullTextSearchAllProperties +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testFullTextSearchScore +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testFullTextSearchScoreWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testFullTextSearchWithBindVariableValue +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testJoin +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testLength +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testLiteral +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testLowerCase +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testNodeLocalName +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testNodeLocalNameWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testNodeName +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testNodeNameWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testNot +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testOr +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testOrderingAscending +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testOrderingDescending +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testPropertyExistence +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testPropertyExistenceWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testPropertyValue +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testPropertyValueWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testSameNode +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testSameNodeJoinCondition +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testSameNodeJoinConditionWithPath +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testSameNodeWithSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testSelector +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testSelectorWithName +org.apache.jackrabbit.test.api.query.qom.QueryObjectModelFactoryTest#testUpperCase +org.apache.jackrabbit.test.api.query.qom.RowTest#testGetNode +org.apache.jackrabbit.test.api.query.qom.RowTest#testGetNodeWithSelector +org.apache.jackrabbit.test.api.query.qom.RowTest#testGetPath +org.apache.jackrabbit.test.api.query.qom.RowTest#testGetPathWithSelector +org.apache.jackrabbit.test.api.query.qom.RowTest#testGetScore +org.apache.jackrabbit.test.api.query.qom.RowTest#testGetScoreWithSelector +org.apache.jackrabbit.test.api.query.qom.RowTest#testGetValue +org.apache.jackrabbit.test.api.query.qom.RowTest#testGetValues +org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testInnerJoin +org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testInnerJoinWithPath +org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testLeftOuterJoin +org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testLeftOuterJoinWithPath +org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testRightOuterJoin +org.apache.jackrabbit.test.api.query.qom.SameNodeJoinConditionTest#testRightOuterJoinWithPath +org.apache.jackrabbit.test.api.query.qom.SameNodeTest#testChildNodesDoNotMatchSelector +org.apache.jackrabbit.test.api.query.qom.SameNodeTest#testNotASelectorName +org.apache.jackrabbit.test.api.query.qom.SameNodeTest#testPathDoesNotExist +org.apache.jackrabbit.test.api.query.qom.SameNodeTest#testRelativePath +org.apache.jackrabbit.test.api.query.qom.SameNodeTest#testSameNode +org.apache.jackrabbit.test.api.query.qom.SameNodeTest#testSyntacticallyInvalidPath +org.apache.jackrabbit.test.api.query.qom.SelectorTest#testDuplicateNodeType +org.apache.jackrabbit.test.api.query.qom.SelectorTest#testSelector +org.apache.jackrabbit.test.api.query.qom.SelectorTest#testSyntacticallyInvalidName +org.apache.jackrabbit.test.api.query.qom.SelectorTest#testUnknownNodeType +org.apache.jackrabbit.test.api.query.qom.UpperLowerCaseTest#testLength +org.apache.jackrabbit.test.api.query.qom.UpperLowerCaseTest#testLowerCaseTwice +org.apache.jackrabbit.test.api.query.qom.UpperLowerCaseTest#testLowerUpperCase +org.apache.jackrabbit.test.api.query.qom.UpperLowerCaseTest#testNodeLocalName +org.apache.jackrabbit.test.api.query.qom.UpperLowerCaseTest#testNodeName +org.apache.jackrabbit.test.api.query.qom.UpperLowerCaseTest#testPropertyValue +org.apache.jackrabbit.test.api.query.qom.UpperLowerCaseTest#testUpperCaseTwice +org.apache.jackrabbit.test.api.query.qom.UpperLowerCaseTest#testUpperLowerCase +org.apache.jackrabbit.test.api.query.QueryResultNodeIteratorTest#testGetPosition +org.apache.jackrabbit.test.api.query.QueryResultNodeIteratorTest#testGetPositionEmptyIterator +org.apache.jackrabbit.test.api.query.QueryResultNodeIteratorTest#testGetSize +org.apache.jackrabbit.test.api.query.QueryResultNodeIteratorTest#testNoSuchElementException +org.apache.jackrabbit.test.api.query.QueryResultNodeIteratorTest#testSkip +org.apache.jackrabbit.test.api.query.SetLimitTest#testSetLimit +org.apache.jackrabbit.test.api.query.SetOffsetTest#testSetOffset +org.apache.jackrabbit.test.api.query.SimpleSelectionTest#testSingleProperty +org.apache.jackrabbit.test.api.query.SQLJcrPathTest#testJcrPath +org.apache.jackrabbit.test.api.query.SQLJoinTest#testJoin +org.apache.jackrabbit.test.api.query.SQLJoinTest#testJoinFilterPrimaryType +org.apache.jackrabbit.test.api.query.SQLJoinTest#testJoinNtBase +org.apache.jackrabbit.test.api.query.SQLJoinTest#testJoinSNS +org.apache.jackrabbit.test.api.query.SQLOrderByTest#testOrderByAscending +org.apache.jackrabbit.test.api.query.SQLOrderByTest#testOrderByDefault +org.apache.jackrabbit.test.api.query.SQLOrderByTest#testOrderByDescending +org.apache.jackrabbit.test.api.query.SQLPathTest#testChildAxisLeaf +org.apache.jackrabbit.test.api.query.SQLPathTest#testChildAxisRoot +org.apache.jackrabbit.test.api.query.SQLPathTest#testChildAxisTestRoot +org.apache.jackrabbit.test.api.query.SQLPathTest#testDescendantLeaf +org.apache.jackrabbit.test.api.query.SQLPathTest#testDescendantSelfTestRoot +org.apache.jackrabbit.test.api.query.SQLPathTest#testDescendantTestRoot +org.apache.jackrabbit.test.api.query.SQLQueryLevel2Test#testFullTextSearch +org.apache.jackrabbit.test.api.query.SQLQueryLevel2Test#testMultiValueSearch +org.apache.jackrabbit.test.api.query.SQLQueryLevel2Test#testPathColumn +org.apache.jackrabbit.test.api.query.SQLQueryLevel2Test#testRange +org.apache.jackrabbit.test.api.query.SQLQueryLevel2Test#testScoreColumn +org.apache.jackrabbit.test.api.query.TextNodeTest#testTextNodeTest +org.apache.jackrabbit.test.api.query.TextNodeTest#testTextNodeTestContains +org.apache.jackrabbit.test.api.query.TextNodeTest#testTextNodeTestMultiNodes +org.apache.jackrabbit.test.api.query.TextNodeTest#testTextNodeTestWithPosition +org.apache.jackrabbit.test.api.query.XPathDocOrderTest#testDocOrderFirstFunction +org.apache.jackrabbit.test.api.query.XPathDocOrderTest#testDocOrderLastFunction +org.apache.jackrabbit.test.api.query.XPathDocOrderTest#testDocOrderPositionFunction +org.apache.jackrabbit.test.api.query.XPathDocOrderTest#testDocOrderPositionIndex +org.apache.jackrabbit.test.api.query.XPathJcrPathTest#testJcrPath +org.apache.jackrabbit.test.api.query.XPathOrderByTest#testOrderBy +org.apache.jackrabbit.test.api.query.XPathOrderByTest#testOrderByAscending +org.apache.jackrabbit.test.api.query.XPathOrderByTest#testOrderByDescending +org.apache.jackrabbit.test.api.query.XPathPosIndexTest#testDocOrderIndexedNotation +org.apache.jackrabbit.test.api.query.XPathQueryLevel2Test#testFullTextSearch +org.apache.jackrabbit.test.api.query.XPathQueryLevel2Test#testMultiValueSearch +org.apache.jackrabbit.test.api.query.XPathQueryLevel2Test#testPathColumn +org.apache.jackrabbit.test.api.query.XPathQueryLevel2Test#testRange +org.apache.jackrabbit.test.api.query.XPathQueryLevel2Test#testScoreColumn + +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testSetIllegalPolicy +org.apache.jackrabbit.test.api.security.RSessionAccessControlPolicyTest#testSetInvalidPolicy + +org.apache.jackrabbit.test.api.SerializationTest#testLockExceptionSessionWithHandler +org.apache.jackrabbit.test.api.SerializationTest#testLockExceptionWorkspaceWithHandler +org.apache.jackrabbit.test.api.SerializationTest#testVersioningExceptionFileChildSessionContentHandler +org.apache.jackrabbit.test.api.SerializationTest#testVersioningExceptionFileChildWorkspaceContentHandler +org.apache.jackrabbit.test.api.SerializationTest#testVersioningExceptionFileParentSessionContentHandler +org.apache.jackrabbit.test.api.SerializationTest#testVersioningExceptionFileParentWorkspaceContentHandler + +org.apache.jackrabbit.test.api.SessionTest#testHasCapability + +org.apache.jackrabbit.test.api.version.CopyTest#testCopy +org.apache.jackrabbit.test.api.version.VersionHistoryTest#testGetAllFrozenNodes + +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testSetPolicy +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testSetAllPolicies +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testGetPolicyAfterSet +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testResetPolicy +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testSetPolicyIsTransient +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testGetPolicyAfterSave +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testNodeIsModifiedAfterSecondSetPolicy +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testNodeIsModifiedAfterSetPolicy +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testRemovePolicy +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testRemovePolicyIsTransient +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testNodeIsModifiedAfterRemovePolicy +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testSetPolicyOnNewNode +org.apache.jackrabbit.test.api.security.AccessControlPolicyTest#testRemoveTransientlyAddedPolicy +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntry +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAggregatePrivilege +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAggregatedPrivilegesSeparately +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddPrivilegesPresentInEntries +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryAndSetPolicy +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryIsTransient +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryInvalidPrincipal +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryEmptyPrivilegeArray +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryInvalidPrivilege +org.apache.jackrabbit.test.api.security.AccessControlListTest#testRemoveAddedAccessControlEntry +org.apache.jackrabbit.test.api.security.AccessControlListTest#testRemoveAccessControlEntryAndSetPolicy +org.apache.jackrabbit.test.api.security.AccessControlListTest#testRemoveAccessControlEntryIsTransient +org.apache.jackrabbit.test.api.security.AccessControlListTest#testRemoveIllegalAccessControlEntry +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryTwice +org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryAgain +org.apache.jackrabbit.test.api.security.AccessControlListTest#testExtendPrivileges +org.apache.jackrabbit.test.api.security.RSessionAccessControlPolicyTest#testSetPolicy + +org.apache.jackrabbit.test.api.observation.EventJournalTest + + + + derby.system.durability + test + + + derby.storage.fileSyncTransactionLog + true + + + derby.stream.error.file + target/derby.log + + + + + + + + + + + + + + + + javax.jcr + jcr + + + org.slf4j + slf4j-api + + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + provided + + + + + org.apache.jackrabbit + jackrabbit-api + ${project.version} + provided + + + + + junit + junit + test + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-jcr-tests + ${project.version} + test + + + ch.qos.logback + logback-classic + test + + + + diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/BrokenRemoteRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/BrokenRemoteRepository.java new file mode 100644 index 00000000000..9201f1a7ba8 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/BrokenRemoteRepository.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.Credentials; +import javax.jcr.Value; + +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.remote.RemoteSession; + +/** + * Dummy remote repository instance that throws a {@link RemoteException} + * whenever any method is invoked. Used as a sentinel object by the + * {@link SafeClientRepository} class. + */ +public class BrokenRemoteRepository implements RemoteRepository { + + /** + * The remote exception thrown by methods of this instance. + */ + private final RemoteException exception; + + /** + * Creates a remote repository whose methods throw the given + * exception. + * + * @param exception remote exception + */ + public BrokenRemoteRepository(RemoteException exception) { + this.exception = exception; + } + + /** + * Creates a remote repository whose methods trow a remote + * exception with the given message. + * + * @param message exception message + */ + public BrokenRemoteRepository(String message) { + this(new RemoteException(message)); + } + + /** + * Creates a remote repository whose methods throw a remote exception. + */ + public BrokenRemoteRepository() { + this(new RemoteException()); + } + + //----------------------------------------------------< RemoteRepository > + + /** + * Throws a {@link RemoteException}. + * + * @param key ignored + * @return nothing + * @throws RemoteException always thrown + */ + public String getDescriptor(String key) throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @return nothing + * @throws RemoteException always thrown + */ + public String[] getDescriptorKeys() throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @return nothing + * @throws RemoteException always thrown + */ + public RemoteSession login() throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @param workspace ignored + * @return nothing + * @throws RemoteException always thrown + */ + public RemoteSession login(String workspace) throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @param credentials ignored + * @return nothing + * @throws RemoteException always thrown + */ + public RemoteSession login(Credentials credentials) throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @param workspace ignored + * @param credentials ignored + * @return nothing + * @throws RemoteException always thrown + */ + public RemoteSession login(Credentials credentials, String workspace) + throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @param key ignored + * @return nothing + * @throws RemoteException always thrown + */ + public Value getDescriptorValue(String key) throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @param key ignored + * @return nothing + * @throws RemoteException always thrown + */ + public Value[] getDescriptorValues(String key) throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @param key ignored + * @return nothing + * @throws RemoteException always thrown + */ + public boolean isSingleValueDescriptor(String key) throws RemoteException { + throw exception; + } + + /** + * Throws a {@link RemoteException}. + * + * @param key ignored + * @return nothing + * @throws RemoteException always thrown + */ + public boolean isStandardDescriptor(String key) throws RemoteException { + throw exception; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientAdapterFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientAdapterFactory.java new file mode 100644 index 00000000000..14959c91ead --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientAdapterFactory.java @@ -0,0 +1,457 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.security.Principal; +import java.util.Iterator; + +import javax.jcr.Item; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.observation.ObservationManager; +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 javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.rmi.client.iterator.ClientNodeIterator; +import org.apache.jackrabbit.rmi.client.iterator.ClientNodeTypeIterator; +import org.apache.jackrabbit.rmi.client.iterator.ClientPropertyIterator; +import org.apache.jackrabbit.rmi.client.iterator.ClientRowIterator; +import org.apache.jackrabbit.rmi.client.iterator.ClientVersionIterator; +import org.apache.jackrabbit.rmi.client.principal.ClientGroup; +import org.apache.jackrabbit.rmi.client.principal.ClientPrincipal; +import org.apache.jackrabbit.rmi.client.principal.ClientPrincipalIterator; +import org.apache.jackrabbit.rmi.client.security.ClientAccessControlEntry; +import org.apache.jackrabbit.rmi.client.security.ClientAccessControlList; +import org.apache.jackrabbit.rmi.client.security.ClientAccessControlManager; +import org.apache.jackrabbit.rmi.client.security.ClientAccessControlPolicy; +import org.apache.jackrabbit.rmi.client.security.ClientAccessControlPolicyIterator; +import org.apache.jackrabbit.rmi.client.security.ClientPrivilege; +import org.apache.jackrabbit.rmi.remote.RemoteItem; +import org.apache.jackrabbit.rmi.remote.RemoteItemDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteLock; +import org.apache.jackrabbit.rmi.remote.RemoteLockManager; +import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; +import org.apache.jackrabbit.rmi.remote.RemoteObservationManager; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteQuery; +import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; +import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.remote.RemoteRow; +import org.apache.jackrabbit.rmi.remote.RemoteSession; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; +import org.apache.jackrabbit.rmi.remote.RemoteVersionManager; +import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; +import org.apache.jackrabbit.rmi.remote.RemoteXASession; +import org.apache.jackrabbit.rmi.remote.principal.RemoteGroup; +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlEntry; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlList; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlPolicy; +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; + +/** + * Default implementation of the + * {@link org.apache.jackrabbit.rmi.client.LocalAdapterFactory LocalAdapterFactory} + * interface. This factory uses the client adapters defined in this + * package as the default adapter implementations. Subclasses can + * easily override or extend the default adapters by implementing the + * corresponding factory methods. + */ +public class ClientAdapterFactory implements LocalAdapterFactory { + + /** + * Creates and returns a {@link ClientRepository ClientRepository} + * instance. + * + * {@inheritDoc} + */ + public Repository getRepository(RemoteRepository remote) { + return new ClientRepository(remote, this); + } + + /** + * Creates and returns a {@link ClientSession ClientSession} instance. + * In case the remote session is transaction enabled, the returned session + * will be transaction enabled too through the {@link ClientXASession}. + * + * {@inheritDoc} + */ + public Session getSession(Repository repository, RemoteSession remote) { + if (remote instanceof RemoteXASession) { + return new ClientXASession( + repository, (RemoteXASession) remote, this); + } else { + return new ClientSession(repository, remote, this); + } + } + + /** + * Creates and returns a {@link ClientWorkspace ClientWorkspace} instance. + * + * {@inheritDoc} + */ + public Workspace getWorkspace(Session session, RemoteWorkspace remote) { + return new ClientWorkspace(session, remote, this); + } + + /** + * Creates and returns a + * {@link ClientObservationManager ClientObservationManager} instance. + * + * {@inheritDoc} + */ + public ObservationManager getObservationManager(Workspace workspace, + RemoteObservationManager remote) { + return new ClientObservationManager(workspace, remote); + } + + /** + * Creates and returns a + * {@link ClientNamespaceRegistry ClientClientNamespaceRegistry} instance. + * + * {@inheritDoc} + */ + public NamespaceRegistry getNamespaceRegistry( + RemoteNamespaceRegistry remote) { + return new ClientNamespaceRegistry(remote, this); + } + + /** + * Creates and returns a + * {@link ClientNodeTypeManager ClienNodeTypeManager} instance. + * + * {@inheritDoc} + */ + public NodeTypeManager getNodeTypeManager(RemoteNodeTypeManager remote) { + return new ClientNodeTypeManager(remote, this); + } + + /** + * Creates and returns a {@link ClientItem ClientItem} instance. + * + * {@inheritDoc} + */ + public Item getItem(Session session, RemoteItem remote) { + return new ClientItem(session, remote, this); + } + + /** + * Creates and returns a {@link ClientProperty ClientProperty} instance. + * + * {@inheritDoc} + */ + public Property getProperty(Session session, RemoteProperty remote) { + return new ClientProperty(session, remote, this); + } + + /** + * Creates and returns a {@link ClientNode ClientNode} instance. + * + * {@inheritDoc} + */ + public Node getNode(Session session, RemoteNode remote) { + return new ClientNode(session, remote, this); + } + + /** + * Creates and returns a {@link ClientVersion ClientVersion} instance. + * + * {@inheritDoc} + */ + public Version getVersion(Session session, RemoteVersion remote) { + return new ClientVersion(session, remote, this); + } + + /** + * Creates and returns a {@link ClientVersionHistory ClientVersionHistory} + * instance. + * + * {@inheritDoc} + */ + public VersionHistory getVersionHistory(Session session, RemoteVersionHistory remote) { + return new ClientVersionHistory(session, remote, this); + } + + /** + * Creates and returns a {@link ClientNodeType ClientNodeType} instance. + * + * {@inheritDoc} + */ + public NodeType getNodeType(RemoteNodeType remote) { + return new ClientNodeType(remote, this); + } + + /** + * Creates and returns a {@link ClientItemDefinition ClientItemDefinition} instance. + * + * {@inheritDoc} + */ + public ItemDefinition getItemDef(RemoteItemDefinition remote) { + return new ClientItemDefinition(remote, this); + } + + /** + * Creates and returns a {@link ClientNodeDefinition ClientNodeDefinition} instance. + * + * {@inheritDoc} + */ + public NodeDefinition getNodeDef(RemoteNodeDefinition remote) { + return new ClientNodeDefinition(remote, this); + } + + /** + * Creates and returns a {@link ClientPropertyDefinition ClientPropertyDefinition} + * instance. + * + * {@inheritDoc} + */ + public PropertyDefinition getPropertyDef(RemotePropertyDefinition remote) { + return new ClientPropertyDefinition(remote, this); + } + + /** + * Creates and returns a {@link ClientLock ClientLock} instance. + * + * {@inheritDoc} + */ + public Lock getLock(Session session, RemoteLock remote) { + return new ClientLock(session, remote, this); + } + + /** + * Creates and returns a {@link ClientQueryManager ClientQueryManager} instance. + * + * {@inheritDoc} + */ + public QueryManager getQueryManager( + Session session, RemoteQueryManager remote) { + return new ClientQueryManager(session, remote, this); + } + + /** + * Creates and returns a {@link ClientQuery ClientQuery} instance. + * + * {@inheritDoc} + */ + public Query getQuery(Session session, RemoteQuery remote) { + return new ClientQuery(session, remote, this); + } + + /** + * Creates and returns a {@link ClientQueryResult ClientQueryResult} instance. + * + * {@inheritDoc} + */ + public QueryResult getQueryResult( + Session session, RemoteQueryResult remote) { + return new ClientQueryResult(session, remote, this); + } + + /** + * Creates and returns a {@link ClientRow ClientRow} instance. + * + * {@inheritDoc} + */ + public Row getRow(Session session, RemoteRow remote) { + return new ClientRow(session, remote, this); + } + + /** + * Creates and returns a {@link ClientNodeIterator} instance. + * {@inheritDoc} + */ + public NodeIterator getNodeIterator( + Session session, RemoteIterator remote) { + return new ClientNodeIterator(remote, session, this); + } + + /** + * Creates and returns a {@link ClientPropertyIterator} instance. + * {@inheritDoc} + */ + public PropertyIterator getPropertyIterator( + Session session, RemoteIterator remote) { + return new ClientPropertyIterator(remote, session, this); + } + + /** + * Creates and returns a {@link ClientVersionIterator} instance. + * {@inheritDoc} + */ + public VersionIterator getVersionIterator( + Session session, RemoteIterator remote) { + return new ClientVersionIterator(remote, session, this); + } + + /** + * Creates and returns a {@link ClientNodeTypeIterator} instance. + * {@inheritDoc} + */ + public NodeTypeIterator getNodeTypeIterator(RemoteIterator remote) { + return new ClientNodeTypeIterator(remote, this); + } + + /** + * Creates and returns a {@link ClientRowIterator} instance. + * {@inheritDoc} + */ + public RowIterator getRowIterator(Session session, RemoteIterator remote) { + return new ClientRowIterator(session, remote, this); + } + + public LockManager getLockManager( + Session session, RemoteLockManager remote) { + return new ClientLockManager(session, remote, this); + } + + public VersionManager getVersionManager( + Session session, RemoteVersionManager remote) { + return new ClientVersionManager(session, remote, this); + } + + /** + * {@inheritDoc} + */ + public AccessControlManager getAccessControlManager( + RemoteAccessControlManager remote) { + return new ClientAccessControlManager(remote, this); + } + + /** + * {@inheritDoc} + */ + public AccessControlPolicy getAccessControlPolicy( + RemoteAccessControlPolicy remote) { + if (remote instanceof RemoteAccessControlList) { + return new ClientAccessControlList( + (RemoteAccessControlList) remote, this); + } + return new ClientAccessControlPolicy(remote, this); + } + + /** + * {@inheritDoc} + */ + public AccessControlPolicy[] getAccessControlPolicy( + RemoteAccessControlPolicy[] remote) { + final AccessControlPolicy[] local = new AccessControlPolicy[remote.length]; + for (int i = 0; i < local.length; i++) { + local[i] = getAccessControlPolicy(remote[i]); + } + return local; + } + + /** + * {@inheritDoc} + */ + public AccessControlPolicyIterator getAccessControlPolicyIterator( + RemoteIterator remote) { + return new ClientAccessControlPolicyIterator(remote, this); + } + + /** + * {@inheritDoc} + */ + public AccessControlEntry getAccessControlEntry( + RemoteAccessControlEntry remote) { + return new ClientAccessControlEntry(remote, this); + } + + /** + * {@inheritDoc} + */ + public AccessControlEntry[] getAccessControlEntry( + RemoteAccessControlEntry[] remote) { + final AccessControlEntry[] local = new AccessControlEntry[remote.length]; + for (int i = 0; i < local.length; i++) { + local[i] = getAccessControlEntry(remote[i]); + } + return local; + } + + /** + * {@inheritDoc} + */ + public Principal getPrincipal(RemotePrincipal remote) { + if (remote instanceof RemoteGroup) { + return new ClientGroup(remote, this); + } + return new ClientPrincipal(remote); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + public Iterator getPrincipalIterator(RemoteIterator remote) { + return new ClientPrincipalIterator(remote, this); + } + + /** + * {@inheritDoc} + */ + public Privilege getPrivilege(RemotePrivilege remote) { + return new ClientPrivilege(remote, this); + } + + /** + * {@inheritDoc} + */ + public Privilege[] getPrivilege(RemotePrivilege[] remote) { + final Privilege[] local = new Privilege[remote.length]; + for (int i = 0; i < local.length; i++) { + local[i] = getPrivilege(remote[i]); + } + return local; + } + +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientItem.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientItem.java similarity index 87% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientItem.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientItem.java index cf4981dbb83..cb5bbe5fcbe 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientItem.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientItem.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -30,14 +30,13 @@ /** * Local adapter for the JCR-RMI * {@link org.apache.jackrabbit.rmi.remote.RemoteItem RemoteItem} - * inteface. This class makes a remote item locally available using + * interface. This class makes a remote item locally available using * the JCR {@link javax.jcr.Item Item} interface. Used mainly as the * base class for the * {@link org.apache.jackrabbit.rmi.client.ClientProperty ClientProperty} * and * {@link org.apache.jackrabbit.rmi.client.ClientNode ClientNode} adapters. * - * @author Jukka Zitting * @see javax.jcr.Item * @see org.apache.jackrabbit.rmi.remote.RemoteItem */ @@ -158,21 +157,15 @@ public boolean isModified() { * * @see Item#getPath() */ - public boolean isSame(Item item) { + public boolean isSame(Item item) throws RepositoryException { if (item == null) { return false; } else if (equals(item)) { return true; - } else if ((item instanceof Property) && !(this instanceof Property)) { - return false; - } else if ((item instanceof Node) && !(this instanceof Node)) { - return false; + } else if (isNode() == item.isNode()) { + return getPath().equals(item.getPath()); } else { - try { - return getPath().equals(item.getPath()); - } catch (RepositoryException ex) { - throw new RuntimeException(ex); - } + return false; } } diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientItemDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientItemDefinition.java new file mode 100644 index 00000000000..c758f227198 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientItemDefinition.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.rmi.remote.RemoteItemDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteItemDefinition RemoteItemDefinition} + * interface. This class makes a remote item definition locally available using + * the JCR {@link javax.jcr.nodetype.ItemDefinition ItemDef} interface. Used mainly + * as the base class for the + * {@link org.apache.jackrabbit.rmi.client.ClientPropertyDefinition ClientPropertyDefinition} + * and + * {@link org.apache.jackrabbit.rmi.client.ClientNodeDefinition ClientNodeDefinition} adapters. + * + * @see javax.jcr.nodetype.ItemDefinition + * @see org.apache.jackrabbit.rmi.remote.RemoteItemDefinition + */ +public class ClientItemDefinition extends ClientObject implements ItemDefinition { + + /** The adapted remote item definition. */ + private RemoteItemDefinition remote; + + /** + * Creates a local adapter for the given remote item definition. + * + * @param remote remote item definition + * @param factory local adapter factory + */ + public ClientItemDefinition(RemoteItemDefinition remote, LocalAdapterFactory factory) { + super(factory); + this.remote = remote; + } + + /** {@inheritDoc} */ + public NodeType getDeclaringNodeType() { + try { + RemoteNodeType nt = remote.getDeclaringNodeType(); + if (nt == null) { + return null; + } else { + return getFactory().getNodeType(nt); + } + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String getName() { + try { + return remote.getName(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isAutoCreated() { + try { + return remote.isAutoCreated(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isMandatory() { + try { + return remote.isMandatory(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public int getOnParentVersion() { + try { + return remote.getOnParentVersion(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isProtected() { + try { + return remote.isProtected(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientLock.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientLock.java new file mode 100644 index 00000000000..c4abc32c272 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientLock.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; + +import org.apache.jackrabbit.rmi.remote.RemoteLock; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteLock RemoteLock} + * interface. This class makes a remote lock locally available using + * the JCR {@link javax.jcr.lock.Lock Lock} interface. + * + * @see javax.jcr.lock.Lock + * @see org.apache.jackrabbit.rmi.remote.RemoteLock + */ +public class ClientLock extends ClientObject implements Lock { + + /** Current session. */ + private Session session; + + /** The adapted remote lock. */ + private RemoteLock remote; + + /** + * Creates a local adapter for the given remote lock. + * + * @param session current session + * @param remote remote lock + * @param factory local adapter factory + */ + public ClientLock(Session session, RemoteLock remote, LocalAdapterFactory factory) { + super(factory); + this.session = session; + this.remote = remote; + } + + /** {@inheritDoc} */ + public Node getNode() { + try { + return getNode(session, remote.getNode()); + } catch (RemoteException e) { + throw new RemoteRuntimeException(e); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + + /** {@inheritDoc} */ + public String getLockOwner() { + try { + return remote.getLockOwner(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isDeep() { + try { + return remote.isDeep(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String getLockToken() { + try { + return remote.getLockToken(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isLive() throws RepositoryException { + try { + return remote.isLive(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public void refresh() throws RepositoryException { + try { + remote.refresh(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isSessionScoped() { + try { + return remote.isSessionScoped(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public long getSecondsRemaining() throws RepositoryException { + try { + return remote.getSecondsRemaining(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isLockOwningSession() { + try { + return remote.isLockOwningSession(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientLockManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientLockManager.java new file mode 100644 index 00000000000..a8dd98135c7 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientLockManager.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockManager; + +import org.apache.jackrabbit.rmi.remote.RemoteLockManager; + +public class ClientLockManager extends ClientObject implements LockManager { + + /** The current session. */ + private Session session; + + private RemoteLockManager remote; + + public ClientLockManager( + Session session, RemoteLockManager remote, + LocalAdapterFactory factory) { + super(factory); + this.session = session; + this.remote = remote; + } + + public String[] getLockTokens() throws RepositoryException { + try { + return remote.getLockTokens(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public void addLockToken(String lockToken) throws RepositoryException { + try { + remote.addLockToken(lockToken); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public void removeLockToken(String lockToken) throws RepositoryException { + try { + remote.removeLockToken(lockToken); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public Lock getLock(String absPath) throws RepositoryException { + try { + return getFactory().getLock(session, remote.getLock(absPath)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public boolean holdsLock(String absPath) throws RepositoryException { + try { + return remote.holdsLock(absPath); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public boolean isLocked(String absPath) throws RepositoryException { + try { + return remote.isLocked(absPath); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public Lock lock( + String absPath, boolean isDeep, boolean isSessionScoped, + long timeoutHint, String ownerInfo) throws RepositoryException { + try { + return getFactory().getLock(session, remote.lock( + absPath, isDeep, isSessionScoped, timeoutHint, ownerInfo)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public void unlock(String absPath) throws RepositoryException { + try { + remote.unlock(absPath); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNamespaceRegistry.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNamespaceRegistry.java similarity index 86% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNamespaceRegistry.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNamespaceRegistry.java index 992011b8158..9d6b21eaa2f 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientNamespaceRegistry.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNamespaceRegistry.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -26,11 +26,10 @@ /** * Local adapter for the JCR-RMI * {@link org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry RemoteNamespaceRegistry} - * inteface. This class makes a remote namespace registry locally available + * interface. This class makes a remote namespace registry locally available * using the JCR {@link javax.jcr.NamespaceRegistry NamespaceRegistry} * interface. * - * @author Jukka Zitting * @see javax.jcr.NamespaceRegistry * @see org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry */ diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNode.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNode.java new file mode 100644 index 00000000000..09f2f2b1049 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNode.java @@ -0,0 +1,798 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.rmi.RemoteException; +import java.util.Calendar; + +import javax.jcr.Binary; +import javax.jcr.Item; +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.lock.Lock; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.rmi.remote.RemoteLock; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteNode RemoteNode} + * interface. This class makes a remote node locally available using + * the JCR {@link javax.jcr.Node Node} interface. + * + * @see javax.jcr.Node + * @see org.apache.jackrabbit.rmi.remote.RemoteNode + */ +public class ClientNode extends ClientItem implements Node { + + /** The adapted remote node. */ + private RemoteNode remote; + + /** + * Creates a local adapter for the given remote node. + * + * @param session current session + * @param remote remote node + * @param factory local adapter factory + */ + public ClientNode( + Session session, RemoteNode remote, LocalAdapterFactory factory) { + super(session, remote, factory); + this.remote = remote; + } + + /** + * Returns true without contacting the remote node. + * + * {@inheritDoc} + */ + public boolean isNode() { + return true; + } + + /** + * Calls the {@link ItemVisitor#visit(Node) ItemVisitor.visit(Node)} + * method of the given visitor. Does not contact the remote node, but + * the visitor may invoke other methods that do contact the remote node. + * + * {@inheritDoc} + */ + public void accept(ItemVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + + /** {@inheritDoc} */ + public Node addNode(String path) throws RepositoryException { + try { + return getNode(getSession(), remote.addNode(path)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node addNode(String path, String type) throws RepositoryException { + try { + RemoteNode node = remote.addNode(path, type); + return getNode(getSession(), node); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void orderBefore(String src, String dst) throws RepositoryException { + try { + remote.orderBefore(src, dst); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, Value value) + throws RepositoryException { + try { + if (value == null) { + remote.setProperty(name, value); + return null; + } else { + RemoteProperty property = remote.setProperty( + name, SerialValueFactory.makeSerialValue(value)); + return getFactory().getProperty(getSession(), property); + } + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, Value[] values) + throws RepositoryException { + try { + if (values == null) { + remote.setProperty(name, values); + return null; + } else { + Value[] serials = SerialValueFactory.makeSerialValueArray(values); + RemoteProperty property = remote.setProperty(name, serials); + return getFactory().getProperty(getSession(), property); + } + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, String[] strings) + throws RepositoryException { + try { + if (strings == null) { + remote.setProperty(name, (Value[]) null); + return null; + } else { + Value[] serials = SerialValueFactory.makeSerialValueArray(strings); + RemoteProperty property = remote.setProperty(name, serials); + return getFactory().getProperty(getSession(), property); + } + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, String value) + throws RepositoryException { + if (value == null) { + return setProperty(name, (Value) null); + } else { + return setProperty(name, getSession().getValueFactory().createValue(value)); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, InputStream value) + throws RepositoryException { + if (value == null) { + return setProperty(name, (Value) null); + } else { + return setProperty(name, getSession().getValueFactory().createValue(value)); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, boolean value) + throws RepositoryException { + return setProperty(name, getSession().getValueFactory().createValue(value)); + } + + /** {@inheritDoc} */ + public Property setProperty(String name, double value) + throws RepositoryException { + return setProperty(name, getSession().getValueFactory().createValue(value)); + } + + /** {@inheritDoc} */ + public Property setProperty(String name, long value) + throws RepositoryException { + return setProperty(name, getSession().getValueFactory().createValue(value)); + } + + /** {@inheritDoc} */ + public Property setProperty(String name, Calendar value) + throws RepositoryException { + if (value == null) { + return setProperty(name, (Value) null); + } else { + return setProperty(name, getSession().getValueFactory().createValue(value)); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, Node value) + throws RepositoryException { + if (value == null) { + return setProperty(name, (Value) null); + } else { + return setProperty(name, getSession().getValueFactory().createValue(value)); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, Binary value) + throws RepositoryException { + if (value == null) { + return setProperty(name, (Value) null); + } else { + return setProperty( + name, getSession().getValueFactory().createValue(value)); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, BigDecimal value) + throws RepositoryException { + if (value == null) { + return setProperty(name, (Value) null); + } else { + return setProperty( + name, getSession().getValueFactory().createValue(value)); + } + } + + /** {@inheritDoc} */ + public Node getNode(String path) throws RepositoryException { + try { + return getNode(getSession(), remote.getNode(path)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeIterator getNodes() throws RepositoryException { + try { + return getFactory().getNodeIterator(getSession(), remote.getNodes()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeIterator getNodes(String pattern) throws RepositoryException { + try { + return getFactory().getNodeIterator(getSession(), remote.getNodes(pattern)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeIterator getNodes(String[] globs) throws RepositoryException { + try { + return getFactory().getNodeIterator(getSession(), remote.getNodes(globs)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property getProperty(String path) throws RepositoryException { + try { + RemoteProperty property = remote.getProperty(path); + return getFactory().getProperty(getSession(), property); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyIterator getProperties() throws RepositoryException { + try { + return getFactory().getPropertyIterator(getSession(), remote.getProperties()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyIterator getProperties(String pattern) + throws RepositoryException { + try { + return getFactory().getPropertyIterator(getSession(), remote.getProperties(pattern)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyIterator getProperties(String[] globs) + throws RepositoryException { + try { + return getFactory().getPropertyIterator(getSession(), remote.getProperties(globs)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Item getPrimaryItem() throws RepositoryException { + try { + return getItem(getSession(), remote.getPrimaryItem()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getIdentifier() throws RepositoryException { + try { + return remote.getIdentifier(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getUUID() throws RepositoryException { + try { + return remote.getUUID(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyIterator getReferences() throws RepositoryException { + try { + return getFactory().getPropertyIterator(getSession(), remote.getReferences()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyIterator getReferences(String name) + throws RepositoryException { + try { + return getFactory().getPropertyIterator(getSession(), remote.getReferences(name)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasNode(String path) throws RepositoryException { + try { + return remote.hasNode(path); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasProperty(String path) throws RepositoryException { + try { + return remote.hasProperty(path); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasNodes() throws RepositoryException { + try { + return remote.hasNodes(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasProperties() throws RepositoryException { + try { + return remote.hasProperties(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeType getPrimaryNodeType() throws RepositoryException { + try { + return getFactory().getNodeType(remote.getPrimaryNodeType()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeType[] getMixinNodeTypes() throws RepositoryException { + try { + return getNodeTypeArray(remote.getMixinNodeTypes()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isNodeType(String type) throws RepositoryException { + try { + return remote.isNodeType(type); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void addMixin(String name) throws RepositoryException { + try { + remote.addMixin(name); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeMixin(String name) throws RepositoryException { + try { + remote.removeMixin(name); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canAddMixin(String name) throws RepositoryException { + try { + return remote.canAddMixin(name); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeDefinition getDefinition() throws RepositoryException { + try { + return getFactory().getNodeDef(remote.getDefinition()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Version checkin() throws RepositoryException { + try { + return getFactory().getVersion(getSession(), remote.checkin()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void checkout() throws RepositoryException { + try { + remote.checkout(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void update(String workspace) throws RepositoryException { + try { + remote.update(workspace); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeIterator merge(String workspace, boolean bestEffort) + throws RepositoryException { + try { + return getFactory().getNodeIterator(getSession(), remote.merge(workspace, bestEffort)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void cancelMerge(Version version) throws RepositoryException { + try { + remote.cancelMerge(version.getUUID()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void doneMerge(Version version) throws RepositoryException { + try { + remote.doneMerge(version.getUUID()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getCorrespondingNodePath(String workspace) + throws RepositoryException { + try { + return remote.getCorrespondingNodePath(workspace); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public int getIndex() throws RepositoryException { + try { + return remote.getIndex(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void restore(String version, boolean removeExisting) + throws RepositoryException { + try { + remote.restore(version, removeExisting); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void restore(Version version, boolean removeExisting) + throws RepositoryException { + try { + remote.restoreByUUID(version.getUUID(), removeExisting); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void restore(Version version, String path, boolean removeExisting) + throws RepositoryException { + try { + remote.restore(version.getUUID(), path, removeExisting); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void restoreByLabel(String label, boolean removeExisting) + throws RepositoryException { + try { + remote.restoreByLabel(label, removeExisting); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, String[] strings, int type) + throws RepositoryException { + try { + if (strings == null) { + remote.setProperty(name, (Value[]) null); + return null; + } else { + Value[] serials = SerialValueFactory.makeSerialValueArray(strings); + RemoteProperty property = remote.setProperty(name, serials, type); + return getFactory().getProperty(getSession(), property); + } + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, Value[] values, int type) + throws RepositoryException { + try { + if (values != null) { + values = SerialValueFactory.makeSerialValueArray(values); + } + RemoteProperty property = remote.setProperty(name, values, type); + if (property != null) { + return getFactory().getProperty(getSession(), property); + } else { + return null; + } + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, Value value, int type) + throws RepositoryException { + try { + if (value != null) { + value = SerialValueFactory.makeSerialValue(value); + } + RemoteProperty property = remote.setProperty(name, value, type); + if (property != null) { + return getFactory().getProperty(getSession(), property); + } else { + return null; + } + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property setProperty(String name, String string, int type) + throws RepositoryException { + Value value = null; + if (string != null) { + value = getSession().getValueFactory().createValue(string); + } + return setProperty(name, value, type); + } + + /** {@inheritDoc} */ + public boolean isCheckedOut() throws RepositoryException { + try { + return remote.isCheckedOut(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public VersionHistory getVersionHistory() throws RepositoryException { + try { + return getFactory().getVersionHistory(getSession(), remote.getVersionHistory()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Version getBaseVersion() throws RepositoryException { + try { + return getFactory().getVersion(getSession(), remote.getBaseVersion()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Lock lock(boolean isDeep, boolean isSessionScoped) + throws RepositoryException { + try { + RemoteLock lock = remote.lock(isDeep, isSessionScoped); + return getFactory().getLock(getSession(), lock); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Lock getLock() throws RepositoryException { + try { + return getFactory().getLock(getSession(), remote.getLock()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void unlock() throws RepositoryException { + try { + remote.unlock(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean holdsLock() throws RepositoryException { + try { + return remote.holdsLock(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isLocked() throws RepositoryException { + try { + return remote.isLocked(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void followLifecycleTransition(String transition) + throws RepositoryException { + try { + remote.followLifecycleTransition(transition); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getAllowedLifecycleTransistions() + throws RepositoryException { + try { + return remote.getAllowedLifecycleTransistions(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeIterator getSharedSet() throws RepositoryException { + try { + return getFactory().getNodeIterator(getSession(), remote.getSharedSet()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyIterator getWeakReferences() throws RepositoryException { + try { + return getFactory().getPropertyIterator(getSession(), remote.getWeakReferences()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyIterator getWeakReferences(String name) + throws RepositoryException { + try { + return getFactory().getPropertyIterator(getSession(), remote.getWeakReferences(name)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeShare() throws RepositoryException { + try { + remote.removeShare(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeSharedSet() throws RepositoryException { + try { + remote.removeSharedSet(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void setPrimaryType(String nodeTypeName) + throws RepositoryException { + try { + remote.setPrimaryType(nodeTypeName); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeDefinition.java new file mode 100644 index 00000000000..1412f227a6d --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeDefinition.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition RemoteNodeDefinition} + * interface. This class makes a remote node definition locally available using + * the JCR {@link javax.jcr.nodetype.NodeDefinition NodeDef} interface. + * + * @see javax.jcr.nodetype.NodeDefinition + * @see org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition + */ +public class ClientNodeDefinition extends ClientItemDefinition implements NodeDefinition { + + /** The adapted remote node definition. */ + private RemoteNodeDefinition remote; + + /** + * Creates a local adapter for the given remote node definition. + * + * @param remote remote node definition + * @param factory local adapter factory + */ + public ClientNodeDefinition(RemoteNodeDefinition remote, LocalAdapterFactory factory) { + super(remote, factory); + this.remote = remote; + } + + /** {@inheritDoc} */ + public NodeType[] getRequiredPrimaryTypes() { + try { + return getNodeTypeArray(remote.getRequiredPrimaryTypes()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public NodeType getDefaultPrimaryType() { + try { + RemoteNodeType nt = remote.getDefaultPrimaryType(); + if (nt == null) { + return null; + } else { + return getFactory().getNodeType(nt); + } + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean allowsSameNameSiblings() { + try { + return remote.allowsSameNameSiblings(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String getDefaultPrimaryTypeName() { + try { + return remote.getDefaultPrimaryTypeName(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getRequiredPrimaryTypeNames() { + try { + return remote.getRequiredPrimaryTypeNames(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeType.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeType.java new file mode 100644 index 00000000000..5f23c047c35 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeType.java @@ -0,0 +1,315 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} + * interface. This class makes a remote node type locally available using + * the JCR {@link javax.jcr.nodetype.NodeType NodeType} interface. + * + * @see javax.jcr.nodetype.NodeType + * @see org.apache.jackrabbit.rmi.remote.RemoteNodeType + */ +public class ClientNodeType extends ClientObject implements NodeType { + + /** The adapted remote node type. */ + private RemoteNodeType remote; + + /** + * Creates a local adapter for the given remote node type. + * + * @param remote remote node type + * @param factory local adapter factory + */ + public ClientNodeType(RemoteNodeType remote, LocalAdapterFactory factory) { + super(factory); + this.remote = remote; + } + + /** + * Utility method for creating an array of local node definition + * adapters for an array of remote node definitions. The node + * definition adapters are created using the local adapter factory. + *

    + * A null input is treated as an empty array. + * + * @param remotes remote node definitions + * @return local node definition array + */ + private NodeDefinition[] getNodeDefArray(RemoteNodeDefinition[] remotes) { + if (remotes != null) { + NodeDefinition[] defs = new NodeDefinition[remotes.length]; + for (int i = 0; i < remotes.length; i++) { + defs[i] = getFactory().getNodeDef(remotes[i]); + } + return defs; + } else { + return new NodeDefinition[0]; // for safety + } + } + + /** + * Utility method for creating an array of local property definition + * adapters for an array of remote property definitions. The property + * definition adapters are created using the local adapter factory. + *

    + * A null input is treated as an empty array. + * + * @param remotes remote property definitions + * @return local property definition array + */ + protected PropertyDefinition[] getPropertyDefArray( + RemotePropertyDefinition[] remotes) { + if (remotes != null) { + PropertyDefinition[] defs = new PropertyDefinition[remotes.length]; + for (int i = 0; i < remotes.length; i++) { + defs[i] = getFactory().getPropertyDef(remotes[i]); + } + return defs; + } else { + return new PropertyDefinition[0]; // for safety + } + } + + /** {@inheritDoc} */ + public String getName() { + try { + return remote.getName(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isMixin() { + try { + return remote.isMixin(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasOrderableChildNodes() { + try { + return remote.hasOrderableChildNodes(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public NodeType[] getSupertypes() { + try { + return getNodeTypeArray(remote.getSupertypes()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public NodeType[] getDeclaredSupertypes() { + try { + return getNodeTypeArray(remote.getDeclaredSupertypes()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isNodeType(String type) { + try { + return remote.isNodeType(type); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyDefinition[] getPropertyDefinitions() { + try { + return getPropertyDefArray(remote.getPropertyDefs()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyDefinition[] getDeclaredPropertyDefinitions() { + try { + return getPropertyDefArray(remote.getDeclaredPropertyDefs()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public NodeDefinition[] getChildNodeDefinitions() { + try { + return getNodeDefArray(remote.getChildNodeDefs()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public NodeDefinition[] getDeclaredChildNodeDefinitions() { + try { + return getNodeDefArray(remote.getDeclaredChildNodeDefs()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canSetProperty(String name, Value value) { + try { + return remote.canSetProperty( + name, SerialValueFactory.makeSerialValue(value)); + } catch (RepositoryException e) { + throw new RuntimeException("Unable to serialize value", e); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canSetProperty(String name, Value[] values) { + try { + Value[] serials = SerialValueFactory.makeSerialValueArray(values); + return remote.canSetProperty(name, serials); + } catch (RepositoryException e) { + throw new RuntimeException("Unable to serialize values", e); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canAddChildNode(String name) { + try { + return remote.canAddChildNode(name); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canAddChildNode(String name, String type) { + try { + return remote.canAddChildNode(name, type); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canRemoveItem(String name) { + try { + return remote.canRemoveItem(name); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String getPrimaryItemName() { + try { + return remote.getPrimaryItemName(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canRemoveNode(String nodeName) { + try { + return remote.canRemoveNode(nodeName); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canRemoveProperty(String propertyName) { + try { + return remote.canRemoveProperty(propertyName); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public NodeTypeIterator getDeclaredSubtypes() { + try { + return getFactory().getNodeTypeIterator(remote.getDeclaredSubtypes()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public NodeTypeIterator getSubtypes() { + try { + return getFactory().getNodeTypeIterator(remote.getSubtypes()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getDeclaredSupertypeNames() { + try { + return remote.getDeclaredSupertypeNames(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isAbstract() { + try { + return remote.isAbstract(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isQueryable() { + try { + return remote.isQueryable(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeTypeManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeTypeManager.java new file mode 100644 index 00000000000..c1d9e438c64 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientNodeTypeManager.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +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.PropertyDefinitionTemplate; + +import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager RemoteNodeTypeManager} + * interface. This class makes a remote node type manager locally available + * using the JCR {@link javax.jcr.nodetype.NodeTypeManager NodeTypeManager} + * interface. + * + * @see javax.jcr.nodetype.NodeTypeManager + * @see org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager + */ +public class ClientNodeTypeManager extends ClientObject + implements NodeTypeManager { + + /** The adapted remote node type manager. */ + private RemoteNodeTypeManager remote; + + /** + * Creates a local adapter for the given remote node type manager. + * + * @param remote remote node type manager + * @param factory local adapter factory + */ + public ClientNodeTypeManager( + RemoteNodeTypeManager remote, LocalAdapterFactory factory) { + super(factory); + this.remote = remote; + } + + /** {@inheritDoc} */ + public NodeType getNodeType(String name) throws RepositoryException { + try { + return getFactory().getNodeType(remote.getNodeType(name)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeTypeIterator getAllNodeTypes() throws RepositoryException { + try { + return getFactory().getNodeTypeIterator(remote.getAllNodeTypes()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeTypeIterator getPrimaryNodeTypes() throws RepositoryException { + try { + return getFactory().getNodeTypeIterator(remote.getPrimaryNodeTypes()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeTypeIterator getMixinNodeTypes() throws RepositoryException { + try { + return getFactory().getNodeTypeIterator(remote.getMixinNodeTypes()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public NodeDefinitionTemplate createNodeDefinitionTemplate() + throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public NodeTypeTemplate createNodeTypeTemplate() + throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public NodeTypeTemplate createNodeTypeTemplate(NodeTypeDefinition ntd) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public PropertyDefinitionTemplate createPropertyDefinitionTemplate() + throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public boolean hasNodeType(String name) throws RepositoryException { + try { + return remote.hasNodeType(name); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public NodeType registerNodeType( + NodeTypeDefinition ntd, boolean allowUpdate) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public NodeTypeIterator registerNodeTypes( + NodeTypeDefinition[] ntds, boolean allowUpdate) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public void unregisterNodeType(String name) throws RepositoryException { + unregisterNodeTypes(new String[] { name }); + } + + public void unregisterNodeTypes(String[] names) throws RepositoryException { + try { + remote.unregisterNodeTypes(names); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientObject.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientObject.java new file mode 100644 index 00000000000..fbb0634fbe3 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientObject.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.rmi.client; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.rmi.remote.RemoteItem; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; + +/** + * Base class for client adapter objects. The only purpose of + * this class is to centralize the handling of the + * local adapter factory used by the client adapters to + * instantiate new adapters. + */ +public class ClientObject { + + /** Local adapter factory. */ + private LocalAdapterFactory factory; + + /** + * Creates a basic client adapter that uses the given factory + * to create new adapters. + * + * @param factory local adapter factory + */ + protected ClientObject(LocalAdapterFactory factory) { + this.factory = factory; + } + + /** + * Returns the local adapter factory used to create new adapters. + * + * @return local adapter factory + */ + protected LocalAdapterFactory getFactory() { + return factory; + } + + /** + * Utility method to create a local adapter for a remote item. + * This method introspects the remote reference to determine + * whether to instantiate a {@link javax.jcr.Property}, + * a {@link Node Node}, or an {@link Item Item} adapter using + * the local adapter factory. + *

    + * If the remote item is a {@link RemoteNode}, this method delegates + * to {@link #getNode(Session, RemoteNode)}. + * + * @param session current session + * @param remote remote item + * @return local property, node, or item adapter + */ + protected Item getItem(Session session, RemoteItem remote) { + if (remote instanceof RemoteProperty) { + return factory.getProperty(session, (RemoteProperty) remote); + } else if (remote instanceof RemoteNode) { + return getNode(session, (RemoteNode) remote); + } else { + return factory.getItem(session, remote); + } + } + + /** + * Utility method to create a local adapter for a remote node. + * This method introspects the remote reference to determine + * whether to instantiate a {@link Node Node}, + * a {@link javax.jcr.version.VersionHistory VersionHistory}, or a + * {@link javax.jcr.version.Version Version} adapter using + * the local adapter factory. + * + * @param session current session + * @param remote remote node + * @return local node, version, or version history adapter + */ + protected Node getNode(Session session, RemoteNode remote) { + if (remote instanceof RemoteVersion) { + return factory.getVersion(session, (RemoteVersion) remote); + } else if (remote instanceof RemoteVersionHistory) { + return factory.getVersionHistory(session, (RemoteVersionHistory) remote); + } else { + return factory.getNode(session, remote); + } + } + + /** + * Utility method for creating an array of local node type adapters + * for an array of remote node types. The node type adapters are created + * using the local adapter factory. + *

    + * A null input is treated as an empty array. + * + * @param remotes remote node types + * @return local node type array + */ + protected NodeType[] getNodeTypeArray(RemoteNodeType[] remotes) { + if (remotes != null) { + NodeType[] types = new NodeType[remotes.length]; + for (int i = 0; i < remotes.length; i++) { + types[i] = factory.getNodeType(remotes[i]); + } + return types; + } else { + return new NodeType[0]; // for safety + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientObservationManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientObservationManager.java new file mode 100644 index 00000000000..d1b57006ae8 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientObservationManager.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Workspace; +import javax.jcr.observation.EventJournal; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.EventListenerIterator; +import javax.jcr.observation.ObservationManager; + +import org.apache.jackrabbit.rmi.iterator.ArrayEventListenerIterator; +import org.apache.jackrabbit.rmi.observation.ClientEventPoll; +import org.apache.jackrabbit.rmi.remote.RemoteObservationManager; + +/** + * The ClientObservationManager class + *

    + * This class uses an instance of the + * {@link org.apache.jackrabbit.rmi.observation.ClientEventPoll} class for the + * actual registration and event dispatching. + *

    + * This class does not require the + * {@link org.apache.jackrabbit.rmi.client.LocalAdapterFactory} and consequently + * calls the base class constructor with a null factory. + *

    + * See the {@link org.apache.jackrabbit.rmi.observation observation} + * package comment for a description on how event listener registration and + * notification is implemented. + * + * @see org.apache.jackrabbit.rmi.observation.ClientEventPoll + */ +public class ClientObservationManager extends ClientObject implements + ObservationManager { + + /** The remote observation manager */ + private final RemoteObservationManager remote; + + /** The Workspace to which this observation manager belongs. */ + private final Workspace workspace; + + /** The ClientEventPoll class internally used for event dispatching */ + private ClientEventPoll poller; + + /** + * Creates an instance of this class talking to the given remote observation + * manager. + * + * @param remote The {@link RemoteObservationManager} backing this + * client-side observation manager. + * @param workspace The Workspace instance to which this + * observation manager belongs. + */ + public ClientObservationManager(Workspace workspace, + RemoteObservationManager remote) { + super(null); + this.remote = remote; + this.workspace = workspace; + } + + /** {@inheritDoc} */ + public void addEventListener(EventListener listener, int eventTypes, + String absPath, boolean isDeep, String[] uuid, + String[] nodeTypeName, boolean noLocal) + throws RepositoryException { + try { + long listenerId = getClientEventPoll().addListener(listener); + remote.addEventListener(listenerId, eventTypes, absPath, + isDeep, uuid, nodeTypeName, noLocal); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeEventListener(EventListener listener) + throws RepositoryException { + try { + long id = getClientEventPoll().removeListener(listener); + remote.removeEventListener(id); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public EventListenerIterator getRegisteredEventListeners() { + EventListener[] listeners = (poller != null) + ? poller.getListeners() + : new EventListener[0]; + return new ArrayEventListenerIterator(listeners); + } + + public EventJournal getEventJournal() throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public EventJournal getEventJournal( + int eventTypes, String absPath, boolean isDeep, + String[] uuid, String[] nodeTypeName) throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public void setUserData(String userData) throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + //---------- internal ------------------------------------------------------ + + /** + * Returns the {@link ClientEventPoll} instance used by this (client-side) + * observation manager. This method creates the instance on the first call + * and starts the poller thread to wait for remote events. + * + * @return poller instance + */ + private synchronized ClientEventPoll getClientEventPoll() { + if (poller == null) { + poller = new ClientEventPoll(remote, workspace.getSession()); + poller.start(); + } + return poller; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientProperty.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientProperty.java new file mode 100644 index 00000000000..fd565456e8b --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientProperty.java @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.rmi.RemoteException; +import java.util.Calendar; + +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.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteProperty RemoteProperty} + * interface. This class makes a remote property locally available using + * the JCR {@link javax.jcr.Property Property} interface. + * + * @see javax.jcr.Property + * @see org.apache.jackrabbit.rmi.remote.RemoteProperty + */ +public class ClientProperty extends ClientItem implements Property { + + /** The adapted remote property. */ + private RemoteProperty remote; + + /** + * Creates a local adapter for the given remote property. + * + * @param session current session + * @param remote remote property + * @param factory local adapter factory + */ + public ClientProperty( + Session session, RemoteProperty remote, + LocalAdapterFactory factory) { + super(session, remote, factory); + this.remote = remote; + } + + /** + * Calls the {@link ItemVisitor#visit(Property) ItemVisitor.visit(Property} + * method of the given visitor. Does not contact the remote property, but + * the visitor may invoke other methods that do contact the remote property. + * + * {@inheritDoc} + */ + public void accept(ItemVisitor visitor) throws RepositoryException { + visitor.visit(this); + } + + /** + * Returns the boolean value of this property. Implemented as + * getValue().getBoolean(). + * + * {@inheritDoc} + */ + public boolean getBoolean() throws RepositoryException { + return getValue().getBoolean(); + } + + /** + * Returns the date value of this property. Implemented as + * getValue().getDate(). + * + * {@inheritDoc} + */ + public Calendar getDate() throws RepositoryException { + return getValue().getDate(); + } + + /** + * Returns the double value of this property. Implemented as + * getValue().getDouble(). + * + * {@inheritDoc} + */ + public double getDouble() throws RepositoryException { + return getValue().getDouble(); + } + + /** + * Returns the long value of this property. Implemented as + * getValue().getLong(). + * + * {@inheritDoc} + */ + public long getLong() throws RepositoryException { + return getValue().getLong(); + } + + /** + * Returns the binary value of this property. Implemented as + * getValue().getBinary(). + * + * {@inheritDoc} + */ + public Binary getBinary() throws RepositoryException { + return getValue().getBinary(); + } + + /** + * Returns the decimal value of this property. Implemented as + * getValue().getDecimal(). + * + * {@inheritDoc} + */ + public BigDecimal getDecimal() throws RepositoryException { + return getValue().getDecimal(); + } + + /** + * Returns the binary value of this property. Implemented as + * getValue().getStream(). + * + * {@inheritDoc} + */ + @SuppressWarnings("deprecation") + public InputStream getStream() throws RepositoryException { + return getValue().getStream(); + } + + /** + * Returns the string value of this property. Implemented as + * getValue().getString(). + * + * {@inheritDoc} + */ + public String getString() throws RepositoryException { + return getValue().getString(); + } + + /** {@inheritDoc} */ + public Value getValue() throws RepositoryException { + try { + return remote.getValue(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Value[] getValues() throws RepositoryException { + try { + return remote.getValues(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** + * Sets the boolean value of this property. Implemented as + * setValue(new BooleanValue(value)). + * + * {@inheritDoc} + */ + public void setValue(boolean value) throws RepositoryException { + setValue(getSession().getValueFactory().createValue(value)); + } + + /** + * Sets the date value of this property. Implemented as + * setValue(new DateValue(value)). + * + * {@inheritDoc} + */ + public void setValue(Calendar value) throws RepositoryException { + if (value == null) { + setValue((Value) null); + } else { + setValue(getSession().getValueFactory().createValue(value)); + } + } + + /** + * Sets the double value of this property. Implemented as + * setValue(new DoubleValue(value)). + * + * {@inheritDoc} + */ + public void setValue(double value) throws RepositoryException { + setValue(getSession().getValueFactory().createValue(value)); + } + + /** + * Sets the binary value of this property. Implemented as + * setValue(new BinaryValue(value)). + * + * {@inheritDoc} + */ + public void setValue(InputStream value) throws RepositoryException { + if (value == null) { + setValue((Value) null); + } else { + ValueFactory factory = getSession().getValueFactory(); + Binary binary = factory.createBinary(value); + try { + setValue(factory.createValue(binary)); + } finally { + binary.dispose(); + } + } + } + + /** + * Sets the long value of this property. Implemented as + * setValue(new LongValue(value)). + * + * {@inheritDoc} + */ + public void setValue(long value) throws RepositoryException { + setValue(getSession().getValueFactory().createValue(value)); + } + + /** + * Sets the binary value of this property. + * + * {@inheritDoc} + */ + public void setValue(Binary value) throws RepositoryException { + setValue(getSession().getValueFactory().createValue(value)); + } + + /** + * Sets the decimal value of this property. + * + * {@inheritDoc} + */ + public void setValue(BigDecimal value) throws RepositoryException { + setValue(getSession().getValueFactory().createValue(value)); + } + + + /** + * Sets the reference value of this property. Implemented as + * setValue(new ReferenceValue(value)). + * + * {@inheritDoc} + */ + public void setValue(Node value) throws RepositoryException { + if (value == null) { + setValue((Value) null); + } else { + setValue(getSession().getValueFactory().createValue(value)); + } + } + + /** + * Sets the string value of this property. Implemented as + * setValue(new StringValue(value)). + * + * {@inheritDoc} + */ + public void setValue(String value) throws RepositoryException { + if (value == null) { + setValue((Value) null); + } else { + setValue(getSession().getValueFactory().createValue(value)); + } + } + + /** + * {@inheritDoc} + */ + public void setValue(String[] strings) throws RepositoryException { + try { + Value[] values = null; + if (strings != null) { + values = SerialValueFactory.makeSerialValueArray(strings); + } + remote.setValue(values); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void setValue(Value value) throws RepositoryException { + try { + if (value != null) { + value = SerialValueFactory.makeSerialValue(value); + } + remote.setValue(value); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void setValue(Value[] values) throws RepositoryException { + try { + if (values != null) { + values = SerialValueFactory.makeSerialValueArray(values); + } + remote.setValue(values); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** + * Returns the reference value of this property. Implemented by + * converting the reference value to an UUID string and using the + * current session to look up the referenced node. + * + * {@inheritDoc} + */ + public Node getNode() throws RepositoryException { + String value = getString(); + + switch (getType()) { + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + return getSession().getNodeByIdentifier(value); + + case PropertyType.PATH: + try { + if (value.startsWith("/")) { + return getSession().getNode(value); + } else { + return getParent().getNode(value); + } + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(value); + } + + case PropertyType.NAME: + try { + return getParent().getNode(value); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(value); + } + + case PropertyType.STRING: + try { + // interpret as identifier + Value refValue = getSession().getValueFactory().createValue(value, PropertyType.REFERENCE); + 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 = getSession().getValueFactory().createValue(value, PropertyType.PATH); + boolean absolute = value.startsWith("/"); + try { + return (absolute) ? getSession().getNode(pathValue.getString()) : getParent().getNode(pathValue.getString()); + } catch (PathNotFoundException e1) { + throw new ItemNotFoundException(pathValue.getString()); + } + } + + default: + throw new ValueFormatException("Property value cannot be converted to a PATH, REFERENCE or WEAKREFERENCE: " + value); + } + } + + /** {@inheritDoc} */ + public Property getProperty() throws RepositoryException { + if (getType() != PropertyType.PATH && getType() != PropertyType.NAME) { + throw new ValueFormatException("Not a path property"); + } else { + String value = getString(); + try { + if (value.startsWith("/")) { + return getSession().getProperty(value); + } else { + return getParent().getProperty(value); + } + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(value); + } + } + } + + /** {@inheritDoc} */ + public long getLength() throws RepositoryException { + try { + return remote.getLength(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public long[] getLengths() throws RepositoryException { + try { + return remote.getLengths(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public PropertyDefinition getDefinition() throws RepositoryException { + try { + return getFactory().getPropertyDef(remote.getDefinition()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public int getType() throws RepositoryException { + try { + return remote.getType(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isMultiple() throws RepositoryException { + // TODO: Direct remote call for this? + return getDefinition().isMultiple(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientPropertyDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientPropertyDefinition.java new file mode 100644 index 00000000000..15955b321f4 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientPropertyDefinition.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.Value; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition RemotePropertyDefinition} + * interface. This class makes a remote property definition locally available + * using the JCR {@link javax.jcr.nodetype.PropertyDefinition PropertyDef} interface. + * + * @see javax.jcr.nodetype.PropertyDefinition + * @see org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition + */ +public class ClientPropertyDefinition extends ClientItemDefinition implements PropertyDefinition { + + /** The adapted remote property. */ + private RemotePropertyDefinition remote; + + /** + * Creates a local adapter for the given remote property definition. + * + * @param remote remote property definition + * @param factory local adapter factory + */ + public ClientPropertyDefinition( + RemotePropertyDefinition remote, LocalAdapterFactory factory) { + super(remote, factory); + this.remote = remote; + } + + /** {@inheritDoc} */ + public int getRequiredType() { + try { + return remote.getRequiredType(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getValueConstraints() { + try { + return remote.getValueConstraints(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public Value[] getDefaultValues() { + try { + return remote.getDefaultValues(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isMultiple() { + try { + return remote.isMultiple(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getAvailableQueryOperators() { + try { + return remote.getAvailableQueryOperators(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isFullTextSearchable() { + try { + return remote.isFullTextSearchable(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isQueryOrderable() { + try { + return remote.isQueryOrderable(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQuery.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQuery.java new file mode 100644 index 00000000000..0de4fde8995 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQuery.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.Value; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +import org.apache.jackrabbit.rmi.remote.RemoteQuery; + +/** + * Local adapter for the JCR-RMI + * {@link RemoteQuery RemoteQuery} + * interface. This class makes a remote query locally available using + * the JCR {@link Query Query} interface. + * + * @see javax.jcr.query.Query Query + * @see org.apache.jackrabbit.rmi.remote.RemoteQuery + */ +public class ClientQuery extends ClientObject implements Query { + + /** The current session */ + private Session session; + + /** The adapted remote query manager. */ + private RemoteQuery remote; + + /** + * Creates a client adapter for the given query. + * + * @param session current session + * @param remote remote query + * @param factory adapter factory + */ + public ClientQuery( + Session session, RemoteQuery remote, LocalAdapterFactory factory) { + super(factory); + this.session = session; + this.remote = remote; + } + + /** {@inheritDoc} */ + public QueryResult execute() throws RepositoryException { + try { + return getFactory().getQueryResult(session, remote.execute()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getStatement() { + try { + return remote.getStatement(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String getLanguage() { + try { + return remote.getLanguage(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String getStoredQueryPath() throws RepositoryException { + try { + return remote.getStoredQueryPath(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node storeAsNode(String absPath) throws RepositoryException { + try { + return getNode(session, remote.storeAsNode(absPath)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void bindValue(String varName, Value value) + throws RepositoryException { + try { + remote.bindValue(varName, value); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getBindVariableNames() throws RepositoryException { + try { + return remote.getBindVariableNames(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void setLimit(long limit) { + try { + remote.setLimit(limit); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public void setOffset(long offset) { + try { + remote.setOffset(offset); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQueryManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQueryManager.java similarity index 78% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQueryManager.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQueryManager.java index 9074ff25f35..a6919992e78 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/client/ClientQueryManager.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQueryManager.java @@ -1,93 +1,97 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.client; - -import java.rmi.RemoteException; - -import javax.jcr.Node; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; - -import org.apache.jackrabbit.rmi.remote.RemoteQuery; -import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; - -/** - * Local adapter for the JCR-RMI {@link RemoteQueryManager RemoteQueryManager} - * inteface. This class makes a remote query manager locally available using - * the JCR {@link QueryManager QueryManager} interface. - * - * @author Philipp Koch - * @see javax.jcr.query.QueryManager QueryManager - * @see org.apache.jackrabbit.rmi.remote.RemoteQueryManager - */ -public class ClientQueryManager extends ClientObject implements QueryManager { - - /** The current session */ - private Session session; - - /** The adapted remote query manager. */ - private RemoteQueryManager remote; - - /** - * Creates a client adapter for the given remote query manager. - * - * @param session current session - * @param remote remote query manager - * @param factory adapter factory - */ - public ClientQueryManager( - Session session, RemoteQueryManager remote, - LocalAdapterFactory factory) { - super(factory); - this.session = session; - this.remote = remote; - } - - /** {@inheritDoc} */ - public Query createQuery(String statement, String language) - throws RepositoryException { - try { - RemoteQuery query = remote.createQuery(statement, language); - return getFactory().getQuery(session, query); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public Query getQuery(Node node) throws RepositoryException { - try { - // TODO fix this remote node dereferencing hack - RemoteQuery query = remote.getQuery(node.getPath()); - return getFactory().getQuery(session, query); - } catch (RemoteException ex) { - throw new RemoteRepositoryException(ex); - } - } - - /** {@inheritDoc} */ - public String[] getSupportedQueryLanguages() { - try { - return remote.getSupportedQueryLanguages(); - } catch (RemoteException ex) { - throw new RemoteRuntimeException(ex); - } - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.rmi.remote.RemoteQuery; +import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; + +/** + * Local adapter for the JCR-RMI {@link RemoteQueryManager RemoteQueryManager} + * interface. This class makes a remote query manager locally available using + * the JCR {@link QueryManager QueryManager} interface. + * + * @see javax.jcr.query.QueryManager QueryManager + * @see org.apache.jackrabbit.rmi.remote.RemoteQueryManager + */ +public class ClientQueryManager extends ClientObject implements QueryManager { + + /** The current session */ + private Session session; + + /** The adapted remote query manager. */ + private RemoteQueryManager remote; + + /** + * Creates a client adapter for the given remote query manager. + * + * @param session current session + * @param remote remote query manager + * @param factory adapter factory + */ + public ClientQueryManager( + Session session, RemoteQueryManager remote, + LocalAdapterFactory factory) { + super(factory); + this.session = session; + this.remote = remote; + } + + /** {@inheritDoc} */ + public Query createQuery(String statement, String language) + throws RepositoryException { + try { + RemoteQuery query = remote.createQuery(statement, language); + return getFactory().getQuery(session, query); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Query getQuery(Node node) throws RepositoryException { + try { + // TODO fix this remote node dereferencing hack + RemoteQuery query = remote.getQuery(node.getPath()); + return getFactory().getQuery(session, query); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getSupportedQueryLanguages() throws RepositoryException { + try { + return remote.getSupportedQueryLanguages(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + public QueryObjectModelFactory getQOMFactory() { + throw new RuntimeException("TODO: JCR-3206"); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQueryResult.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQueryResult.java new file mode 100644 index 00000000000..dd2ef4b1d5e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientQueryResult.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; + +/** + * Local adapter for the JCR-RMI + * {@link RemoteQueryResult RemoteQueryResult} + * interface. This class makes a remote query result locally available using + * the JCR {@link QueryResult QueryResult} interface. + * + * @see javax.jcr.query.QueryResult QueryResult + * @see org.apache.jackrabbit.rmi.remote.RemoteQueryResult + */ +public class ClientQueryResult extends ClientObject implements QueryResult { + + /** The current session */ + private Session session; + + /** The adapted remote query result. */ + private RemoteQueryResult remote; + + /** + * Creates a client adapter for the given remote query result. + * + * @param session current session + * @param remote remote query result + * @param factory adapter factory + */ + public ClientQueryResult( + Session session, RemoteQueryResult remote, + LocalAdapterFactory factory) { + super(factory); + this.session = session; + this.remote = remote; + } + + /** {@inheritDoc} */ + public String[] getColumnNames() throws RepositoryException { + try { + return remote.getColumnNames(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RowIterator getRows() throws RepositoryException { + try { + return getFactory().getRowIterator(session, remote.getRows()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeIterator getNodes() throws RepositoryException { + try { + return getFactory().getNodeIterator(session, remote.getNodes()); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public String[] getSelectorNames() throws RepositoryException { + try { + return remote.getSelectorNames(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRepository.java new file mode 100644 index 00000000000..f292298dc88 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRepository.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.rmi.client; + +import java.rmi.RemoteException; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.remote.RemoteSession; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteRepository RemoteRepository} + * interface. This class makes a remote repository locally available using + * the JCR {@link javax.jcr.Repository Repository} interface. + * + * @see javax.jcr.Repository + * @see org.apache.jackrabbit.rmi.remote.RemoteRepository + */ +public class ClientRepository implements Repository { + + /** + * The set of standard descriptor keys defined in the + * {@link Repository} interface. + */ + private static final Set STANDARD_KEYS = new HashSet() {{ + add(Repository.IDENTIFIER_STABILITY); + add(Repository.LEVEL_1_SUPPORTED); + add(Repository.LEVEL_2_SUPPORTED); + add(Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_INHERITANCE); + add(Repository.NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_PROPERTY_TYPES); + add(Repository.NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPORTED); + add(Repository.OPTION_ACCESS_CONTROL_SUPPORTED); + add(Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED); + add(Repository.OPTION_LIFECYCLE_SUPPORTED); + add(Repository.OPTION_LOCKING_SUPPORTED); + add(Repository.OPTION_OBSERVATION_SUPPORTED); + add(Repository.OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED); + add(Repository.OPTION_QUERY_SQL_SUPPORTED); + add(Repository.OPTION_RETENTION_SUPPORTED); + add(Repository.OPTION_SHAREABLE_NODES_SUPPORTED); + add(Repository.OPTION_SIMPLE_VERSIONING_SUPPORTED); + add(Repository.OPTION_TRANSACTIONS_SUPPORTED); + add(Repository.OPTION_UNFILED_CONTENT_SUPPORTED); + add(Repository.OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED); + add(Repository.OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED); + add(Repository.OPTION_VERSIONING_SUPPORTED); + add(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED); + add(Repository.OPTION_XML_EXPORT_SUPPORTED); + add(Repository.OPTION_XML_IMPORT_SUPPORTED); + add(Repository.OPTION_ACTIVITIES_SUPPORTED); + add(Repository.OPTION_BASELINES_SUPPORTED); + + add(Repository.QUERY_FULL_TEXT_SEARCH_SUPPORTED); + add(Repository.QUERY_JOINS); + add(Repository.QUERY_LANGUAGES); + add(Repository.QUERY_STORED_QUERIES_SUPPORTED); + add(Repository.QUERY_XPATH_DOC_ORDER); + add(Repository.QUERY_XPATH_POS_INDEX); + add(Repository.REP_NAME_DESC); + add(Repository.REP_VENDOR_DESC); + add(Repository.REP_VENDOR_URL_DESC); + add(Repository.SPEC_NAME_DESC); + add(Repository.SPEC_VERSION_DESC); + add(Repository.WRITE_SUPPORTED); + }}; + + /** The adapted remote repository. */ + private final RemoteRepository remote; + + /** Local adapter factory. */ + private final LocalAdapterFactory factory; + + /** + * Creates a client adapter for the given remote repository. + * + * @param remote remote repository + * @param factory local adapter factory + */ + public ClientRepository( + RemoteRepository remote, LocalAdapterFactory factory) { + this.remote = remote; + this.factory = factory; + } + + /** {@inheritDoc} */ + public String getDescriptor(String name) { + try { + return remote.getDescriptor(name); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public Value getDescriptorValue(String key) { + String descriptor = getDescriptor(key); + if (descriptor != null) { + return SerialValueFactory.getInstance().createValue(descriptor); + } else { + return null; + } + } + + /** {@inheritDoc} */ + public Value[] getDescriptorValues(String key) { + try { + return remote.getDescriptorValues(key); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getDescriptorKeys() { + try { + return remote.getDescriptorKeys(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isSingleValueDescriptor(String key) { + return getDescriptor(key) != null; + } + + /** {@inheritDoc} */ + public Session login(Credentials credentials, String workspace) + throws RepositoryException { + try { + RemoteSession session = remote.login(credentials, workspace); + return factory.getSession(this, session); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** + * Returns true if the given key identifies a standard descriptor. + * + * @param key descriptor key + * @return true if the key identifies a standard descriptor, + * false otherwise + */ + public boolean isStandardDescriptor(String key) { + return STANDARD_KEYS.contains(key); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * null arguments. + * + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login() throws RepositoryException { + return login(null, null); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * the given credentials and a null workspace name. + * + * @param credentials login credentials + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login(Credentials credentials) throws RepositoryException { + return login(credentials, null); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * null credentials and the given workspace name. + * + * @param workspace workspace name + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login(String workspace) throws RepositoryException { + return login(null, workspace); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRepositoryFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRepositoryFactory.java new file mode 100644 index 00000000000..9aa41e5fd2e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRepositoryFactory.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.rmi.client; + +import java.net.MalformedURLException; +import java.rmi.Naming; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.util.Hashtable; + +import javax.jcr.Repository; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.spi.ObjectFactory; + +import org.apache.jackrabbit.rmi.remote.RemoteRepository; + +/** + * Object factory for JCR-RMI clients. This factory can be used either + * directly or as a JNDI object factory. + * + * @see ClientRepository + */ +public class ClientRepositoryFactory implements ObjectFactory { + + /** + * The JNDI parameter name for configuring the RMI URL of + * a remote repository. + */ + public static final String URL_PARAMETER = "url"; + + /** + * Local adapter factory. + */ + private LocalAdapterFactory factory; + + /** + * Creates a JCR-RMI client factory with the default adapter factory. + */ + public ClientRepositoryFactory() { + this(new ClientAdapterFactory()); + } + + /** + * Creates a JCR-RMI client factory with the given adapter factory. + * + * @param factory local adapter factory + */ + public ClientRepositoryFactory(LocalAdapterFactory factory) { + this.factory = factory; + } + + /** + * Returns a client wrapper for a remote content repository. The remote + * repository is looked up from the RMI registry using the given URL by + * the returned {@link SafeClientRepository} instance. + *

    + * The current implementation of this method will not throw any of the + * declared exceptions (because of the {@link SafeClientRepository} being + * used), but the throws clauses are kept for backwards compatibility and + * potential future use. Clients should be prepared to handle exceptions + * from this method. + * + * @param url the RMI URL of the remote repository + * @return repository client + * @throws MalformedURLException if the given URL is malfored + * @throws NotBoundException if the given URL points to nothing + * @throws ClassCastException if the given URL points to something unknown + * @throws RemoteException if the remote repository can not be accessed + */ + public Repository getRepository(final String url) + throws MalformedURLException, NotBoundException, + ClassCastException, RemoteException { + return new SafeClientRepository(factory) { + + protected RemoteRepository getRemoteRepository() + throws RemoteException { + try { + return (RemoteRepository) Naming.lookup(url); + } catch (MalformedURLException e) { + throw new RemoteException("Malformed URL: " + url, e); + } catch (NotBoundException e) { + throw new RemoteException("No target found: " + url, e); + } catch (ClassCastException e) { + throw new RemoteException("Unknown target: " + url, e); + } + } + + }; + } + + /** + * JNDI factory method for creating JCR-RMI clients. Creates a lazy + * client repository instance that uses the reference parameter "url" + * as the RMI URL where the remote repository is looked up when accessed. + * + * @param object reference parameters + * @param name unused + * @param context unused + * @param environment unused + * @return repository client + */ + public Object getObjectInstance( + Object object, Name name, Context context, Hashtable environment) { + if (object instanceof Reference) { + Reference reference = (Reference) object; + RefAddr url = reference.get(URL_PARAMETER); + if (url != null && url.getContent() != null) { + try { + return getRepository(url.getContent().toString()); + } catch (Exception e) { + return null; + } + } + } + return null; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRow.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRow.java new file mode 100644 index 00000000000..253b3c451b0 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientRow.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.query.Row; + +import org.apache.jackrabbit.rmi.remote.RemoteRow; + +/** + * Local adapter for the JCR-RMI {@link RemoteRow RemoteRow} + * interface. This class makes a remote query row locally available using + * the JCR {@link Row Row} interface. + * + * @see javax.jcr.query.Row Row + * @see org.apache.jackrabbit.rmi.remote.RemoteRow + */ +public class ClientRow extends ClientObject implements Row { + + /** Current session. */ + private Session session; + + /** The remote query row. */ + private RemoteRow remote; + + /** + * Creates a client adapter for the given remote query row. + * + * @param remote remote query row + */ + public ClientRow(Session session, RemoteRow remote, + LocalAdapterFactory factory) { + super(factory); + this.session = session; + this.remote = remote; + } + + /** {@inheritDoc} */ + public Value[] getValues() throws RepositoryException { + try { + return remote.getValues(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Value getValue(String s) throws RepositoryException { + try { + return remote.getValue(s); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node getNode() throws RepositoryException { + try { + return getFactory().getNode(session, remote.getNode()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node getNode(String selectorName) throws RepositoryException { + try { + return getFactory().getNode(session, remote.getNode(selectorName)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getPath() throws RepositoryException { + try { + return remote.getPath(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getPath(String selectorName) throws RepositoryException { + try { + return remote.getPath(selectorName); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public double getScore() throws RepositoryException { + try { + return remote.getScore(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public double getScore(String selectorName) throws RepositoryException { + try { + return remote.getScore(selectorName); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientSession.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientSession.java new file mode 100644 index 00000000000..909ad2a6545 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientSession.java @@ -0,0 +1,587 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.rmi.RemoteException; +import java.security.AccessControlException; + +import javax.jcr.Credentials; +import javax.jcr.Item; +import javax.jcr.Node; +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.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.stream.StreamSource; + +import org.apache.jackrabbit.rmi.remote.RemoteSession; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteSession RemoteSession} + * interface. This class makes a remote session locally available using + * the JCR {@link javax.jcr.Session Session} interface. + * + * @see javax.jcr.Session + * @see org.apache.jackrabbit.rmi.remote.RemoteSession + */ +public class ClientSession extends ClientObject implements Session { + + /** + * Logger instance. + */ + private static final Logger log = + LoggerFactory.getLogger(ClientSession.class); + + /** The current repository. */ + private final Repository repository; + + /** + * Flag indicating whether the session is to be considered live of not. + * This flag is initially set to true and reset to + * false by the {@link #logout()} method. The {@link #isLive()} + * method first checks this flag before asking the remote session. + */ + private boolean live = true; + + /** The adapted remote session. */ + protected final RemoteSession remote; + + /** + * The adapted workspace of this session. This field is set on the first + * call to the {@link #getWorkspace()} method assuming, that a workspace + * instance is not changing during the lifetime of a session, that is, + * each call to the server-side Session.getWorkspace() allways + * returns the same object. + */ + private Workspace workspace; + + /** + * Creates a client adapter for the given remote session. + * + * @param repository current repository + * @param remote remote repository + * @param factory local adapter factory + */ + public ClientSession(Repository repository, RemoteSession remote, + LocalAdapterFactory factory) { + super(factory); + this.repository = repository; + this.remote = remote; + } + + /** + * Returns the current repository without contacting the remote session. + * + * {@inheritDoc} + */ + public Repository getRepository() { + return repository; + } + + /** {@inheritDoc} */ + public String getUserID() { + try { + return remote.getUserID(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public Object getAttribute(String name) { + try { + return remote.getAttribute(name); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getAttributeNames() { + try { + return remote.getAttributeNames(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public Workspace getWorkspace() { + if (workspace == null) { + try { + workspace = + getFactory().getWorkspace(this, remote.getWorkspace()); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + return workspace; + } + + /** {@inheritDoc} */ + public Session impersonate(Credentials credentials) + throws RepositoryException { + try { + RemoteSession session = remote.impersonate(credentials); + return getFactory().getSession(repository, session); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node getRootNode() throws RepositoryException { + try { + return getNode(this, remote.getRootNode()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node getNodeByIdentifier(String id) throws RepositoryException { + try { + return getNode(this, remote.getNodeByIdentifier(id)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node getNodeByUUID(String uuid) throws RepositoryException { + try { + return getNode(this, remote.getNodeByUUID(uuid)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Item getItem(String path) throws RepositoryException { + try { + return getItem(this, remote.getItem(path)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node getNode(String path) throws RepositoryException { + try { + return getNode(this, remote.getNode(path)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Property getProperty(String path) throws RepositoryException { + try { + return (Property) getItem(this, remote.getProperty(path)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean itemExists(String path) throws RepositoryException { + try { + return remote.itemExists(path); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean nodeExists(String path) throws RepositoryException { + try { + return remote.nodeExists(path); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public boolean propertyExists(String path) throws RepositoryException { + try { + return remote.propertyExists(path); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public void removeItem(String path) throws RepositoryException { + try { + remote.removeItem(path); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void move(String from, String to) throws RepositoryException { + try { + remote.move(from, to); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void save() throws RepositoryException { + try { + remote.save(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void refresh(boolean keepChanges) throws RepositoryException { + try { + remote.refresh(keepChanges); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasPendingChanges() throws RepositoryException { + try { + return remote.hasPendingChanges(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** + * Returns the {@link SerialValueFactory#getInstance()}. + * + * {@inheritDoc} + */ + public ValueFactory getValueFactory() { + return SerialValueFactory.getInstance(); + } + + /** {@inheritDoc} */ + public void checkPermission(String path, String actions) + throws AccessControlException, RepositoryException { + if (!hasPermission(path, actions)) { + throw new AccessControlException( + "No permission for " + actions + " on " + path); + } + } + + /** {@inheritDoc} */ + public boolean hasPermission(String path, String actions) + throws RepositoryException { + try { + return remote.hasPermission(path, actions); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public void importXML(String path, InputStream xml, int mode) + throws IOException, RepositoryException { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] bytes = new byte[4096]; + for (int n = xml.read(bytes); n != -1; n = xml.read(bytes)) { + buffer.write(bytes, 0, n); + } + remote.importXML(path, buffer.toByteArray(), mode); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } finally { + // JCR-2903 + try { xml.close(); } catch (IOException ignore) {} + } + } + + /** {@inheritDoc} */ + public ContentHandler getImportContentHandler( + final String path, final int mode) throws RepositoryException { + getItem(path); // Check that the path exists + try { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ContentHandler handler = + SerializingContentHandler.getSerializer(buffer); + return new DefaultContentHandler(handler) { + public void endDocument() throws SAXException { + super.endDocument(); + try { + remote.importXML(path, buffer.toByteArray(), mode); + } catch (Exception e) { + throw new SAXException("XML import failed", e); + } + } + }; + } catch (SAXException e) { + throw new RepositoryException("XML serialization failed", e); + } + } + + /** {@inheritDoc} */ + public void setNamespacePrefix(String prefix, String uri) + throws RepositoryException { + try { + remote.setNamespacePrefix(prefix, uri); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getNamespacePrefixes() throws RepositoryException { + try { + return remote.getNamespacePrefixes(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getNamespaceURI(String prefix) throws RepositoryException { + try { + return remote.getNamespaceURI(prefix); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getNamespacePrefix(String uri) throws RepositoryException { + try { + return remote.getNamespacePrefix(uri); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void logout() { + + // ignore if we are not alive any more. + if (!isLive()) { + return; + } + + try { + remote.logout(); + } catch (RemoteException ex) { + // JCRRMI-3: Just log a warning, the connection is dead anyway + log.warn("Remote logout failed", ex); + } finally { + // mark "dead" + live = false; + } + } + + /** {@inheritDoc} */ + public void addLockToken(String name) { + try { + remote.addLockToken(name); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getLockTokens() { + try { + return remote.getLockTokens(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public void removeLockToken(String name) { + try { + remote.removeLockToken(name); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** + * Exports the XML system view of the specified repository location + * to the given XML content handler. This method first requests the + * raw XML data from the remote session, and then uses an identity + * transformation to feed the data to the given XML content handler. + * Possible IO and transformer exceptions are thrown as SAXExceptions. + * + * {@inheritDoc} + */ + public void exportSystemView( + String path, ContentHandler handler, + boolean binaryAsLink, boolean noRecurse) + throws SAXException, RepositoryException { + try { + byte[] xml = remote.exportSystemView(path, binaryAsLink, noRecurse); + + Source source = new StreamSource(new ByteArrayInputStream(xml)); + Result result = new SAXResult(handler); + + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(); + transformer.transform(source, result); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } catch (IOException ex) { + throw new SAXException(ex); + } catch (TransformerConfigurationException ex) { + throw new SAXException(ex); + } catch (TransformerException ex) { + throw new SAXException(ex); + } + } + + /** + * Exports the XML system view of the specified repository location + * to the given output stream. This method first requests the + * raw XML data from the remote session, and then writes the data to + * the output stream. + * + * {@inheritDoc} + */ + public void exportSystemView( + String path, OutputStream output, + boolean binaryAsLink, boolean noRecurse) + throws IOException, RepositoryException { + try { + byte[] xml = remote.exportSystemView(path, binaryAsLink, noRecurse); + output.write(xml); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** + * Exports the XML document view of the specified repository location + * to the given XML content handler. This method first requests the + * raw XML data from the remote session, and then uses an identity + * transformation to feed the data to the given XML content handler. + * Possible IO and transformer exceptions are thrown as SAXExceptions. + * + * {@inheritDoc} + */ + public void exportDocumentView( + String path, ContentHandler handler, + boolean binaryAsLink, boolean noRecurse) + throws SAXException, RepositoryException { + try { + byte[] xml = remote.exportDocumentView(path, binaryAsLink, noRecurse); + + Source source = new StreamSource(new ByteArrayInputStream(xml)); + Result result = new SAXResult(handler); + + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(); + transformer.transform(source, result); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } catch (IOException ex) { + throw new SAXException(ex); + } catch (TransformerConfigurationException ex) { + throw new SAXException(ex); + } catch (TransformerException ex) { + throw new SAXException(ex); + } + } + + /** + * Exports the XML document view of the specified repository location + * to the given output stream. This method first requests the + * raw XML data from the remote session, and then writes the data to + * the output stream. + * + * {@inheritDoc} + */ + public void exportDocumentView( + String path, OutputStream output, + boolean binaryAsLink, boolean noRecurse) + throws IOException, RepositoryException { + try { + byte[] xml = remote.exportDocumentView(path, binaryAsLink, noRecurse); + output.write(xml); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** + * {@inheritDoc} + */ + public boolean isLive() { + try { + return live && remote.isLive(); + } catch (RemoteException e) { + log.warn("Failed to test remote session state", e); + return false; + } + } + + public AccessControlManager getAccessControlManager() + throws UnsupportedRepositoryOperationException, RepositoryException { + try { + return getFactory().getAccessControlManager( + remote.getAccessControlManager()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public RetentionManager getRetentionManager() + throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + + public boolean hasCapability( + String methodName, Object target, Object[] arguments) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException("TODO: JCR-3206"); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersion.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersion.java new file mode 100644 index 00000000000..edce561b40b --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersion.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.rmi.client; + +import java.rmi.RemoteException; +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.rmi.remote.RemoteVersion; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteVersion RemoteVersion} + * interface. This class makes a remote version locally available using + * the JCR {@link javax.jcr.version.Version Version} interface. + * + * @see javax.jcr.version.Version + * @see org.apache.jackrabbit.rmi.remote.RemoteVersion + */ +public class ClientVersion extends ClientNode implements Version { + + /** The adapted remote version. */ + private RemoteVersion remote; + + /** + * Creates a local adapter for the given remote version. + * + * @param session current session + * @param remote remote version + * @param factory local adapter factory + */ + public ClientVersion(Session session, RemoteVersion remote, + LocalAdapterFactory factory) { + super(session, remote, factory); + this.remote = remote; + } + + /** + * Utility method for creating a version array for an array + * of remote versions. The versions in the returned array + * are created using the local adapter factory. + *

    + * A null input is treated as an empty array. + * + * @param remotes remote versions + * @return local version array + */ + private Version[] getVersionArray(RemoteVersion[] remotes) { + if (remotes != null) { + Version[] versions = new Version[remotes.length]; + for (int i = 0; i < remotes.length; i++) { + versions[i] = getFactory().getVersion(getSession(), remotes[i]); + } + return versions; + } else { + return new Version[0]; // for safety + } + } + + + /** {@inheritDoc} */ + public Calendar getCreated() throws RepositoryException { + try { + return remote.getCreated(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Version[] getSuccessors() throws RepositoryException { + try { + return getVersionArray(remote.getSuccessors()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Version[] getPredecessors() throws RepositoryException { + try { + return getVersionArray(remote.getPredecessors()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public VersionHistory getContainingHistory() throws RepositoryException { + try { + return getFactory().getVersionHistory(getSession(), remote.getContainingHistory()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Node getFrozenNode() throws RepositoryException { + try { + return getFactory().getNode(getSession(), remote.getFrozenNode()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Version getLinearPredecessor() throws RepositoryException { + try { + RemoteVersion linearPredecessor = remote.getLinearPredecessor(); + if (linearPredecessor == null) { + return null; + } else { + return getFactory().getVersion(getSession(), linearPredecessor); + } + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Version getLinearSuccessor() throws RepositoryException { + try { + RemoteVersion linearSuccessor = remote.getLinearSuccessor(); + if (linearSuccessor == null) { + return null; + } else { + return getFactory().getVersion(getSession(), linearSuccessor); + } + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersionHistory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersionHistory.java new file mode 100644 index 00000000000..5d855cf5b9d --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersionHistory.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; + +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteVersionHistory RemoteVersionHistory} + * interface. This class makes a remote version history locally available using + * the JCR {@link javax.jcr.version.VersionHistory VersionHistory} interface. + * + * @see javax.jcr.version.VersionHistory + * @see org.apache.jackrabbit.rmi.remote.RemoteVersionHistory + */ +public class ClientVersionHistory extends ClientNode implements VersionHistory { + + /** The adapted remote version history. */ + private RemoteVersionHistory remote; + + /** + * Creates a local adapter for the given remote version history. + * + * @param session current session + * @param remote remote version history + * @param factory local adapter factory + */ + public ClientVersionHistory(Session session, RemoteVersionHistory remote, + LocalAdapterFactory factory) { + super(session, remote, factory); + this.remote = remote; + } + + /** {@inheritDoc} */ + public Version getRootVersion() throws RepositoryException { + try { + return getFactory().getVersion(getSession(), remote.getRootVersion()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public VersionIterator getAllVersions() throws RepositoryException { + try { + return getFactory().getVersionIterator( + getSession(), remote.getAllVersions()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Version getVersion(String versionName) throws VersionException, + RepositoryException { + try { + return getFactory().getVersion(getSession(), remote.getVersion(versionName)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Version getVersionByLabel(String label) throws RepositoryException { + try { + return getFactory().getVersion(getSession(), remote.getVersionByLabel(label)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void addVersionLabel(String versionName, String label, + boolean moveLabel) throws VersionException, RepositoryException { + try { + remote.addVersionLabel(versionName, label, moveLabel); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeVersionLabel(String label) + throws VersionException, RepositoryException { + try { + remote.removeVersionLabel(label); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasVersionLabel(String label) throws RepositoryException { + try { + return remote.hasVersionLabel(label); + } catch (RemoteException ex) { + // grok the exception and assume label is missing + return false; + } + } + + /** {@inheritDoc} */ + public boolean hasVersionLabel(Version version, String label) + throws VersionException, RepositoryException { + try { + String versionIdentifier = version.getIdentifier(); + return remote.hasVersionLabel(versionIdentifier, label); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getVersionLabels() throws RepositoryException { + try { + return remote.getVersionLabels(); + } catch (RemoteException ex) { + // grok the exception and return an empty array + return new String[0]; + } + } + + /** {@inheritDoc} */ + public String[] getVersionLabels(Version version) + throws VersionException, RepositoryException { + try { + String versionIdentifier = version.getIdentifier(); + return remote.getVersionLabels(versionIdentifier); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeVersion(String versionName) + throws UnsupportedRepositoryOperationException, VersionException, + RepositoryException { + try { + remote.removeVersion(versionName); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} + * @deprecated As of JCR 2.0, {@link #getVersionableIdentifier} should be + * used instead. + */ + public String getVersionableUUID() throws RepositoryException { + try { + return remote.getVersionableUUID(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeIterator getAllFrozenNodes() throws RepositoryException { + try { + return getFactory().getNodeIterator(getSession(), remote.getAllFrozenNodes()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeIterator getAllLinearFrozenNodes() throws RepositoryException { + try { + return getFactory().getNodeIterator(getSession(), remote.getAllLinearFrozenNodes()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public VersionIterator getAllLinearVersions() throws RepositoryException { + try { + return getFactory().getVersionIterator(getSession(), remote.getAllLinearVersions()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getVersionableIdentifier() throws RepositoryException { + try { + return remote.getVersionableIdentifier(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersionManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersionManager.java new file mode 100644 index 00000000000..07f9dedbdf3 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientVersionManager.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteVersionManager; + +public class ClientVersionManager extends ClientObject + implements VersionManager { + + /** The current session. */ + private Session session; + + private RemoteVersionManager remote; + + public ClientVersionManager( + Session session, RemoteVersionManager remote, + LocalAdapterFactory factory) { + super(factory); + this.session = session; + this.remote = remote; + } + + /** {@inheritDoc} */ + public void cancelMerge(String absPath, Version version) + throws RepositoryException { + try { + remote.cancelMerge(absPath, version.getIdentifier()); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public Version checkin(String absPath) throws RepositoryException { + try { + return getFactory().getVersion(session, remote.checkin(absPath)); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public void checkout(String absPath) throws RepositoryException { + try { + remote.checkout(absPath); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public Version checkpoint(String absPath) throws RepositoryException { + try { + return getFactory().getVersion(session, remote.checkpoint(absPath)); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public Node createActivity(String title) + throws RepositoryException { + try { + return getFactory().getNode(session, remote.createActivity(title)); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public Node createConfiguration(String absPath) + throws RepositoryException { + try { + return getFactory().getNode( + session, remote.createConfiguration(absPath)); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public void doneMerge(String absPath, Version version) + throws RepositoryException { + try { + remote.doneMerge(absPath, version.getIdentifier()); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public Node getActivity() throws RepositoryException { + try { + RemoteNode activity = remote.getActivity(); + if (activity == null) { + return null; + } else { + return getFactory().getNode(session, activity); + } + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public Version getBaseVersion(String absPath) throws RepositoryException { + try { + return getFactory().getVersion( + session, remote.getBaseVersion(absPath)); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public VersionHistory getVersionHistory(String absPath) + throws RepositoryException { + try { + return getFactory().getVersionHistory( + session, remote.getVersionHistory(absPath)); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public boolean isCheckedOut(String absPath) throws RepositoryException { + try { + return remote.isCheckedOut(absPath); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public NodeIterator merge(Node activityNode) throws RepositoryException { + try { + RemoteIterator iterator = remote.merge(activityNode.getIdentifier()); + return getFactory().getNodeIterator(session, iterator); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public NodeIterator merge( + String absPath, String srcWorkspace, boolean bestEffort) + throws RepositoryException { + try { + return getFactory().getNodeIterator( + session, remote.merge(absPath, srcWorkspace, bestEffort)); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public NodeIterator merge( + String absPath, String srcWorkspace, boolean bestEffort, + boolean isShallow) throws RepositoryException { + try { + return getFactory().getNodeIterator(session, remote.merge( + absPath, srcWorkspace, bestEffort, isShallow)); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public void removeActivity(Node activityNode) throws RepositoryException { + try { + remote.removeActivity(activityNode.getIdentifier()); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public void restore(Version[] versions, boolean removeExisting) + throws RepositoryException { + try { + String[] versionIdentifiers = new String[versions.length]; + for (int i = 0; i < versions.length; i++) { + versionIdentifiers[i] = versions[i].getIdentifier(); + } + remote.restore(versionIdentifiers, removeExisting); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public void restore(Version version, boolean removeExisting) + throws RepositoryException { + try { + remote.restore(version.getIdentifier(), removeExisting); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public void restore( + String absPath, String versionName, boolean removeExisting) + throws RepositoryException { + try { + remote.restore(absPath, versionName, removeExisting); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public void restore(String absPath, Version version, boolean removeExisting) + throws RepositoryException { + try { + remote.restoreVI(absPath, version.getIdentifier(), removeExisting); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public void restoreByLabel( + String absPath, String versionLabel, boolean removeExisting) + throws RepositoryException { + try { + remote.restoreByLabel(absPath, versionLabel, removeExisting); + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + + /** {@inheritDoc} */ + public Node setActivity(Node activity) throws RepositoryException { + try { + RemoteNode remoteActivity; + if (activity == null) { + remoteActivity = remote.setActivity(null); + } else { + remoteActivity = remote.setActivity(activity.getIdentifier()); + } + if (remoteActivity == null) { + return null; + } else { + return getFactory().getNode(session, remoteActivity); + } + } catch (RemoteException e) { + throw new RemoteRepositoryException(e); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientWorkspace.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientWorkspace.java new file mode 100644 index 00000000000..89f83f41f72 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientWorkspace.java @@ -0,0 +1,297 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.rmi.RemoteException; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +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.Version; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; +import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; +import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; +import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +/** + * Local adapter for the JCR-RMI {@link RemoteWorkspace RemoteWorkspace} + * interface. This class makes a remote workspace locally available using + * the JCR {@link Workspace Workspace} interface. + * + * @see javax.jcr.Workspace + * @see org.apache.jackrabbit.rmi.remote.RemoteWorkspace + */ +public class ClientWorkspace extends ClientObject implements Workspace { + + /** The current session. */ + private Session session; + + /** The adapted remote workspace. */ + private RemoteWorkspace remote; + + /** + * The adapted observation manager of this workspace. This field is set on + * the first call to the {@link #getObservationManager()()} method assuming, + * that the observation manager instance is not changing during the lifetime + * of a workspace instance, that is, each call to the server-side + * Workspace.getObservationManager() allways returns the same + * object. + */ + private ObservationManager observationManager; + + private LockManager lockManager; + + private VersionManager versionManager; + + /** + * Creates a client adapter for the given remote workspace. + * + * @param session current session + * @param remote remote workspace + * @param factory local adapter factory + */ + public ClientWorkspace( + Session session, RemoteWorkspace remote, + LocalAdapterFactory factory) { + super(factory); + this.session = session; + this.remote = remote; + } + + /** + * Returns the current session without contacting the remote workspace. + * + * {@inheritDoc} + */ + public Session getSession() { + return session; + } + + /** {@inheritDoc} */ + public String getName() { + try { + return remote.getName(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public void copy(String from, String to) throws RepositoryException { + try { + remote.copy(from, to); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void copy(String workspace, String from, String to) + throws RepositoryException { + try { + remote.copy(workspace, from, to); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void move(String from, String to) throws RepositoryException { + try { + remote.move(from, to); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public QueryManager getQueryManager() throws RepositoryException { + try { + RemoteQueryManager manager = remote.getQueryManager(); + return getFactory().getQueryManager(session, manager); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NamespaceRegistry getNamespaceRegistry() throws RepositoryException { + try { + RemoteNamespaceRegistry registry = remote.getNamespaceRegistry(); + return getFactory().getNamespaceRegistry(registry); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public NodeTypeManager getNodeTypeManager() throws RepositoryException { + try { + RemoteNodeTypeManager manager = remote.getNodeTypeManager(); + return getFactory().getNodeTypeManager(manager); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public ObservationManager getObservationManager() + throws RepositoryException { + if (observationManager == null) { + try { + observationManager = + getFactory(). + getObservationManager(this, remote.getObservationManager()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + return observationManager; + } + + /** {@inheritDoc} */ + public void clone( + String workspace, String src, String dst, boolean removeExisting) + throws RepositoryException { + try { + remote.clone(workspace, src, dst, removeExisting); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getAccessibleWorkspaceNames() throws RepositoryException { + try { + return remote.getAccessibleWorkspaceNames(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public ContentHandler getImportContentHandler( + final String path, final int mode) throws RepositoryException { + getSession().getItem(path); // Check that the path exists + try { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + return new DefaultContentHandler( + SerializingContentHandler.getSerializer(buffer)) { + public void endDocument() throws SAXException { + super.endDocument(); + try { + remote.importXML(path, buffer.toByteArray(), mode); + } catch (Exception e) { + throw new SAXException("XML import failed", e); + } + } + }; + } catch (SAXException e) { + throw new RepositoryException("XML serialization failed", e); + } + } + + /** {@inheritDoc} */ + public void importXML(String path, InputStream xml, int uuidBehaviour) + throws IOException, RepositoryException { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] bytes = new byte[4096]; + for (int n = xml.read(bytes); n != -1; n = xml.read(bytes)) { + buffer.write(bytes, 0, n); + } + remote.importXML(path, buffer.toByteArray(), uuidBehaviour); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } finally { + // JCR-2903 + try { xml.close(); } catch (IOException ignore) {} + } + } + + /** {@inheritDoc} */ + public void restore(Version[] versions, boolean removeExisting) + throws RepositoryException { + getVersionManager().restore(versions, removeExisting); + } + + public void createWorkspace(String name) throws RepositoryException { + try { + remote.createWorkspace(name, null); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public void createWorkspace(String name, String srcWorkspace) + throws RepositoryException { + try { + remote.createWorkspace(name, srcWorkspace); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + public void deleteWorkspace(String name) throws RepositoryException { + try { + remote.deleteWorkspace(name); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public LockManager getLockManager() throws RepositoryException { + if (lockManager == null) { + try { + lockManager = getFactory().getLockManager( + session, remote.getLockManager()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + return lockManager; + } + + public VersionManager getVersionManager() throws RepositoryException { + if (versionManager == null) { + try { + versionManager = getFactory().getVersionManager( + session, remote.getVersionManager()); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + return versionManager; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientXASession.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientXASession.java new file mode 100644 index 00000000000..63a3bedb2b1 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/ClientXASession.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import javax.jcr.Repository; + +import org.apache.jackrabbit.rmi.remote.RemoteXASession; +import org.apache.jackrabbit.rmi.remote.SerializableXid; + +/** + * Local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteXASession RemoteXASession} + * interface. + * + * @since 1.4 + */ +public class ClientXASession extends ClientSession implements XAResource { + + /** + * The adapted remote transaction enabled session. + */ + private RemoteXASession remote; + + /** + * Creates a client adapter for the given remote session which is + * transaction enabled. + */ + public ClientXASession( + Repository repository, RemoteXASession remote, + LocalAdapterFactory factory) { + super(repository, remote, factory); + this.remote = remote; + } + + /** + * Returns true if the given object is a local + * adapter that refers to the same remote XA resource. + * + * @see http://blogs.sun.com/fkieviet/entry/j2ee_jca_resource_adapters_the + */ + public boolean isSameRM(XAResource xares) throws XAException { + return xares instanceof ClientXASession + && remote == ((ClientXASession) xares).remote; + } + + private XAException getXAException(RemoteException e) { + XAException exception = new XAException("Remote operation failed"); + exception.initCause(e); + return exception; + } + + public void commit(Xid xid, boolean onePhase) throws XAException { + try { + remote.commit(new SerializableXid(xid), onePhase); + } catch (RemoteException e) { + throw getXAException(e); + } + } + + public void end(Xid xid, int flags) throws XAException { + try { + remote.end(new SerializableXid(xid), flags); + } catch (RemoteException e) { + throw getXAException(e); + } + } + + public void forget(Xid xid) throws XAException { + try { + remote.forget(new SerializableXid(xid)); + } catch (RemoteException e) { + throw getXAException(e); + } + } + + public int getTransactionTimeout() throws XAException { + try { + return remote.getTransactionTimeout(); + } catch (RemoteException e) { + throw getXAException(e); + } + } + + public int prepare(Xid xid) throws XAException { + try { + return remote.prepare(new SerializableXid(xid)); + } catch (RemoteException e) { + throw getXAException(e); + } + } + + public Xid[] recover(int flag) throws XAException { + try { + return remote.recover(flag); + } catch (RemoteException e) { + throw getXAException(e); + } + } + + public void rollback(Xid xid) throws XAException { + try { + remote.rollback(new SerializableXid(xid)); + } catch (RemoteException e) { + throw getXAException(e); + } + } + + public boolean setTransactionTimeout(int seconds) throws XAException { + try { + return remote.setTransactionTimeout(seconds); + } catch (RemoteException e) { + throw getXAException(e); + } + } + + public void start(Xid xid, int flags) throws XAException { + try { + remote.start(new SerializableXid(xid), flags); + } catch (RemoteException e) { + throw getXAException(e); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/DefaultContentHandler.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/DefaultContentHandler.java new file mode 100644 index 00000000000..d6f3947b215 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/DefaultContentHandler.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Adapter class for exposing a {@link ContentHandler} instance as + * {@link DefaultHandler} object. + */ +class DefaultContentHandler extends DefaultHandler { + + /** + * The adapted content handler instance. + */ + private final ContentHandler handler; + + /** + * Creates a {@link DefaultHandler} adapter for the given content + * handler. + * + * @param handler content handler + */ + public DefaultContentHandler(ContentHandler handler) { + this.handler = handler; + } + + //------------------------------------------------------< ContentHandler > + + /** + * Delegated to {@link #handler}. + * + * @param ch passed through + * @param start passed through + * @param length passed through + * @throws SAXException if an error occurs + */ + public void characters(char[] ch, int start, int length) + throws SAXException { + handler.characters(ch, start, length); + } + + /** + * Delegated to {@link #handler}. + * + * @throws SAXException if an error occurs + */ + public void endDocument() throws SAXException { + handler.endDocument(); + } + + /** + * Delegated to {@link #handler}. + * + * @param namespaceURI passed through + * @param localName passed through + * @param qName passed through + * @throws SAXException if an error occurs + */ + public void endElement( + String namespaceURI, String localName, String qName) + throws SAXException { + handler.endElement(namespaceURI, localName, qName); + } + + /** + * Delegated to {@link #handler}. + * + * @param prefix passed through + * @throws SAXException if an error occurs + */ + public void endPrefixMapping(String prefix) throws SAXException { + handler.endPrefixMapping(prefix); + } + + /** + * Delegated to {@link #handler}. + * + * @param ch passed through + * @param start passed through + * @param length passed through + * @throws SAXException if an error occurs + */ + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + handler.ignorableWhitespace(ch, start, length); + } + + /** + * Delegated to {@link #handler}. + * + * @param target passed through + * @param data passed through + * @throws SAXException if an error occurs + */ + public void processingInstruction(String target, String data) + throws SAXException { + handler.processingInstruction(target, data); + } + + /** + * Delegated to {@link #handler}. + * + * @param locator passed through + */ + public void setDocumentLocator(Locator locator) { + handler.setDocumentLocator(locator); + } + + /** + * Delegated to {@link #handler}. + * + * @param name passed through + * @throws SAXException if an error occurs + */ + public void skippedEntity(String name) throws SAXException { + handler.skippedEntity(name); + } + + /** + * Delegated to {@link #handler}. + * + * @throws SAXException if an error occurs + */ + public void startDocument() throws SAXException { + handler.startDocument(); + } + + /** + * Delegated to {@link #handler}. + * + * @param namespaceURI passed through + * @param localName passed through + * @param qName passed through + * @param atts passed through + * @throws SAXException if an error occurs + */ + public void startElement( + String namespaceURI, String localName, String qName, + Attributes atts) throws SAXException { + handler.startElement(namespaceURI, localName, qName, atts); + } + + /** + * Delegated to {@link #handler}. + * + * @param prefix passed through + * @param uri passed through + * @throws SAXException if an error occurs + */ + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + handler.startPrefixMapping(prefix, uri); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/LocalAdapterFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/LocalAdapterFactory.java new file mode 100644 index 00000000000..a2f55edbb3d --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/LocalAdapterFactory.java @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.security.Principal; +import java.util.Iterator; + +import javax.jcr.Item; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.observation.ObservationManager; +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 javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.rmi.remote.RemoteItem; +import org.apache.jackrabbit.rmi.remote.RemoteItemDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteLock; +import org.apache.jackrabbit.rmi.remote.RemoteLockManager; +import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; +import org.apache.jackrabbit.rmi.remote.RemoteObservationManager; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteQuery; +import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; +import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.remote.RemoteRow; +import org.apache.jackrabbit.rmi.remote.RemoteSession; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; +import org.apache.jackrabbit.rmi.remote.RemoteVersionManager; +import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; +import org.apache.jackrabbit.rmi.remote.principal.RemoteGroup; +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlEntry; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlPolicy; +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; + +/** + * Factory interface for creating local adapters for remote references. + * This interface defines how remote JCR-RMI references are adapted + * back to the normal JCR interfaces. The adaption mechanism can be + * modified (for example to add extra features) by changing the + * local adapter factory used by the repository client. + *

    + * Note that the + * {@link org.apache.jackrabbit.rmi.client.ClientObject ClientObject} + * base class provides a number of utility methods designed to work with + * a local adapter factory. Adapter implementations may want to inherit + * that functionality by subclassing from ClientObject. + * + * @see org.apache.jackrabbit.rmi.server.RemoteAdapterFactory + * @see org.apache.jackrabbit.rmi.client.ClientAdapterFactory + * @see org.apache.jackrabbit.rmi.client.ClientObject + */ +public interface LocalAdapterFactory { + + /** + * Factory method for creating a local adapter for a remote repository. + * + * @param remote remote repository + * @return local repository adapter + */ + Repository getRepository(RemoteRepository remote); + + /** + * Factory method for creating a local adapter for a remote session. + * + * @param repository current repository + * @param remote remote session + * @return local session adapter + */ + Session getSession(Repository repository, RemoteSession remote); + + /** + * Factory method for creating a local adapter for a remote workspace. + * + * @param session current session + * @param remote remote workspace + * @return local workspace adapter + */ + Workspace getWorkspace(Session session, RemoteWorkspace remote); + + /** + * Factory method for creating a local adapter for a remote observation + * manager. + * + * @param workspace current workspace + * @param remote remote observation manager + * @return local observation manager adapter + */ + ObservationManager getObservationManager(Workspace workspace, + RemoteObservationManager remote); + + /** + * Factory method for creating a local adapter for a remote namespace + * registry. + * + * @param remote remote namespace registry + * @return local namespace registry adapter + */ + NamespaceRegistry getNamespaceRegistry(RemoteNamespaceRegistry remote); + + /** + * Factory method for creating a local adapter for a remote node type + * manager. + * + * @param remote remote node type manager + * @return local node type manager adapter + */ + NodeTypeManager getNodeTypeManager(RemoteNodeTypeManager remote); + + /** + * Factory method for creating a local adapter for a remote item. + * Note that before calling this method, the client may want to + * introspect the remote item reference to determine whether to use the + * {@link #getNode(Session, RemoteNode) getNode} or + * {@link #getProperty(Session, RemoteProperty) getProperty} method + * instead, as the adapter returned by this method will only cover + * the basic {@link Item Item} interface. + * + * @param session current session + * @param remote remote item + * @return local item adapter + */ + Item getItem(Session session, RemoteItem remote); + + /** + * Factory method for creating a local adapter for a remote property. + * + * @param session current session + * @param remote remote property + * @return local property adapter + */ + Property getProperty(Session session, RemoteProperty remote); + + /** + * Factory method for creating a local adapter for a remote node. + * + * @param session current session + * @param remote remote node + * @return local node adapter + */ + Node getNode(Session session, RemoteNode remote); + + /** + * Factory method for creating a local adapter for a remote version. + * + * @param session current session + * @param remote remote version + * @return local version adapter + */ + Version getVersion(Session session, RemoteVersion remote); + + /** + * Factory method for creating a local adapter for a remote version history. + * + * @param session current session + * @param remote remote version history + * @return local version history adapter + */ + VersionHistory getVersionHistory(Session session, RemoteVersionHistory remote); + + /** + * Factory method for creating a local adapter for a remote node type. + * + * @param remote remote node type + * @return local node type adapter + */ + NodeType getNodeType(RemoteNodeType remote); + + /** + * Factory method for creating a local adapter for a remote item + * definition. Note that before calling this method, the client may want to + * introspect the remote item definition to determine whether to use the + * {@link #getNodeDef(RemoteNodeDefinition) getNodeDef} or + * {@link #getPropertyDef(RemotePropertyDefinition) getPropertyDef} method + * instead, as the adapter returned by this method will only cover + * the {@link ItemDefinition ItemDef} base interface. + * + * @param remote remote item definition + * @return local item definition adapter + */ + ItemDefinition getItemDef(RemoteItemDefinition remote); + + /** + * Factory method for creating a local adapter for a remote node + * definition. + * + * @param remote remote node definition + * @return local node definition adapter + */ + NodeDefinition getNodeDef(RemoteNodeDefinition remote); + + /** + * Factory method for creating a local adapter for a remote property + * definition. + * + * @param remote remote property definition + * @return local property definition adapter + */ + PropertyDefinition getPropertyDef(RemotePropertyDefinition remote); + + /** + * Factory method for creating a local adapter for a remote lock. + * + * @param session current session + * @param remote remote lock + * @return local lock adapter + */ + Lock getLock(Session session, RemoteLock remote); + + /** + * Factory method for creating a local adapter for a remote query manager. + * + * @param session current session + * @param remote remote query manager + * @return local query manager adapter + */ + QueryManager getQueryManager(Session session, RemoteQueryManager remote); + + /** + * Factory method for creating a local adapter for a remote query. + * + * @param session current session + * @param remote remote query + * @return local query adapter + */ + Query getQuery(Session session, RemoteQuery remote); + + /** + * Factory method for creating a local adapter for a remote query result. + * + * @param session current session + * @param remote remote query result + * @return local query result adapter + */ + QueryResult getQueryResult(Session session, RemoteQueryResult remote); + + /** + * Factory method for creating a local adapter for a remote query row. + * + * @param session current session + * @param remote remote query row + * @return local query row adapter + */ + Row getRow(Session session, RemoteRow remote); + + /** + * Factory method for creating a local adapter for a remote node iterator. + * + * @param session current session + * @param remote remote node iterator + * @return local node iterator adapter + */ + NodeIterator getNodeIterator(Session session, RemoteIterator remote); + + /** + * Factory method for creating a local adapter for a remote property iterator. + * + * @param session current session + * @param remote remote property iterator + * @return local property iterator adapter + */ + PropertyIterator getPropertyIterator(Session session, RemoteIterator remote); + + /** + * Factory method for creating a local adapter for a remote version iterator. + * + * @param session current session + * @param remote remote version iterator + * @return local version iterator adapter + */ + VersionIterator getVersionIterator(Session session, RemoteIterator remote); + + /** + * Factory method for creating a local adapter for a remote + * node type iterator. + * + * @param remote remote node type iterator + * @return local node type iterator adapter + */ + NodeTypeIterator getNodeTypeIterator(RemoteIterator remote); + + /** + * Factory method for creating a local adapter for a remote row iterator. + * + * @param session current session + * @param remote remote row iterator + * @return local row iterator adapter + */ + RowIterator getRowIterator(Session session, RemoteIterator remote); + + LockManager getLockManager(Session session, RemoteLockManager lockManager); + + VersionManager getVersionManager( + Session session, RemoteVersionManager versionManager); + + /** + * Factory method for creating a local adapter for a remote access control + * manager + * + * @param remote remote access control manager + * @return local access control manager + */ + AccessControlManager getAccessControlManager( + RemoteAccessControlManager remote); + + /** + * Factory method for creating a local adapter for a remote access control + * policy + * + * @param remote remote access control policy + * @return local access control policy + */ + AccessControlPolicy getAccessControlPolicy(RemoteAccessControlPolicy remote); + + /** + * Factory method for creating an array of local adapter for an array of + * remote access control policies + * + * @param remote array of remote access control policies + * @return array of local access control policies + */ + AccessControlPolicy[] getAccessControlPolicy( + RemoteAccessControlPolicy[] remote); + + /** + * Factory method for creating a local adapter for a remote access control + * policy iterator + * + * @param remote access control policy iterator + * @return local access control policy iterator + */ + AccessControlPolicyIterator getAccessControlPolicyIterator( + RemoteIterator remote); + + /** + * Factory method for creating a local adapter for a remote access control + * entry + * + * @param remote remote access control entry + * @return local access control entry + */ + AccessControlEntry getAccessControlEntry(RemoteAccessControlEntry remote); + + /** + * Factory method for creating an array of local adapter for an array of + * remote access control entry + * + * @param remote array of remote access control entry + * @return local array of access control entry + */ + AccessControlEntry[] getAccessControlEntry(RemoteAccessControlEntry[] remote); + + /** + * Factory method for creating a local adapter for a remote principal. + *

    + * If remote is a {@link RemoteGroup} the + * principal returned implements the org.apache.jackrabbit.api.security.principal.GroupPrincipal + * interface. + * + * @param remote principal + * @return local principal + */ + Principal getPrincipal(RemotePrincipal remote); + + /** + * Factory method for creating a local adapter for a remote principal + * iterator. + *

    + * Each entry in the remote iterator which is a + * {@link RemoteGroup} will be + * provided as a principal implementing the + * org.apache.jackrabbit.api.security.principal.GroupPrincipal interface. + * + * @param remote remote principal iterator + * @return local principal iterator + */ + Iterator getPrincipalIterator(RemoteIterator remote); + + /** + * Factory method for creating a local adapter for a remote privilege + * + * @param remote remote privilege + * @return local privilege + */ + Privilege getPrivilege(RemotePrivilege remote); + + /** + * Factory method for creating an array of local adapter for an array of + * remote privilege + * + * @param remote array of remote privilege + * @return array of local privilege + */ + Privilege[] getPrivilege(RemotePrivilege[] remote); + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/RemoteRepositoryException.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/RemoteRepositoryException.java new file mode 100644 index 00000000000..ab5919a8fea --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/RemoteRepositoryException.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +/** + * JCR-RMI remote exception. Used by the JCR-RMI client to wrap RMI errors + * into RepositoryExceptions to avoid breaking the JCR interfaces. + *

    + * Note that if a RemoteException is received by call with no declared + * exceptions, then the RemoteException is wrapped into a + * RemoteRuntimeException. + */ +public class RemoteRepositoryException extends RepositoryException { + + /** + * Creates a RemoteRepositoryException based on the given RemoteException. + * + * @param ex the remote exception + */ + public RemoteRepositoryException(RemoteException ex) { + super(ex); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/RemoteRuntimeException.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/RemoteRuntimeException.java new file mode 100644 index 00000000000..72ad8080f50 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/RemoteRuntimeException.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.rmi.client; + +import java.rmi.RemoteException; + +/** + * JCR-RMI remote runtime exception. Used by the JCR-RMI client to wrap + * RMI errors into RuntimeExceptions to avoid breaking the JCR interfaces. + *

    + * Note that if a RemoteException is received by call that declares to + * throw RepositoryExceptions, then the RemoteException is wrapped into + * a RemoteRepositoryException. + */ +public class RemoteRuntimeException extends RuntimeException { + + /** + * Creates a RemoteRuntimeException based on the given RemoteException. + * + * @param ex the remote exception + */ + public RemoteRuntimeException(RemoteException ex) { + super(ex); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/SafeClientRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/SafeClientRepository.java new file mode 100644 index 00000000000..4b6acafb0b4 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/SafeClientRepository.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.rmi.client; + +import java.rmi.RemoteException; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.remote.RemoteSession; + +/** + * A "safe" local adapter for the JCR-RMI + * {@link org.apache.jackrabbit.rmi.remote.RemoteRepository RemoteRepository} + * interface. This class uses an abstract factory method for loading + * (and reloading) the remote repository instance that is made locally + * available through the JCR {@link Repository} interface. If the remote + * reference breaks (a RemoteException is thrown by a remote call), then + * this adapter attempts to reload the remote reference once before failing. + * + * @see javax.jcr.Repository + * @see org.apache.jackrabbit.rmi.remote.RemoteRepository + */ +public abstract class SafeClientRepository extends ClientObject + implements Repository { + + /** The adapted remote repository. */ + private RemoteRepository remote; + + /** + * Creates a client adapter for the given remote repository. + * + * @param factory local adapter factory + */ + public SafeClientRepository(LocalAdapterFactory factory) { + super(factory); + try { + remote = getRemoteRepository(true); + } catch (RemoteException e) { + remote = new BrokenRemoteRepository(e); + } + } + + /** + * Abstract factory class for getting the remote repository. + * + * @return remote repository + * @throws RemoteException if the remote repository could not be accessed + */ + protected abstract RemoteRepository getRemoteRepository() + throws RemoteException; + + /** + * Method to obtain the remote remote repository. + * If initialize is true and a RepositoryException will be thrown no {@link BrokenRemoteRepository} + * will be created. + * + * @return remote repository + * @throws RemoteException if the remote repository could not be accessed + */ + protected RemoteRepository getRemoteRepository(boolean initialize) + throws RemoteException { + if (initialize) { + try { + return getRemoteRepository(); + } catch (RemoteException e) { + throw new RemoteRuntimeException(e); + } + } else { + return getRemoteRepository(); + } + } + + /** {@inheritDoc} */ + public synchronized String getDescriptor(String name) { + try { + return remote.getDescriptor(name); + } catch (RemoteException e1) { + try { + remote = getRemoteRepository(false); + return remote.getDescriptor(name); + } catch (RemoteException e2) { + remote = new BrokenRemoteRepository(e2); + throw new RemoteRuntimeException(e2); + } + } + } + + /** {@inheritDoc} */ + public synchronized String[] getDescriptorKeys() { + try { + return remote.getDescriptorKeys(); + } catch (RemoteException e1) { + try { + remote = getRemoteRepository(false); + return remote.getDescriptorKeys(); + } catch (RemoteException e2) { + remote = new BrokenRemoteRepository(e2); + throw new RemoteRuntimeException(e2); + } + } + } + + private synchronized RemoteSession remoteLogin( + Credentials credentials, String workspace) + throws RepositoryException { + try { + return remote.login(credentials, workspace); + } catch (RemoteException e1) { + try { + remote = getRemoteRepository(false); + return remote.login(credentials, workspace); + } catch (RemoteException e2) { + remote = new BrokenRemoteRepository(e2); + throw new RemoteRepositoryException(e2); + } + } + } + + /** {@inheritDoc} */ + public Session login(Credentials credentials, String workspace) + throws RepositoryException { + RemoteSession session = remoteLogin(credentials, workspace); + return getFactory().getSession(this, session); + } + + /** {@inheritDoc} */ + public Session login(String workspace) throws RepositoryException { + return login(null, workspace); + } + + /** {@inheritDoc} */ + public Session login(Credentials credentials) throws RepositoryException { + return login(credentials, null); + } + + /** {@inheritDoc} */ + public Session login() throws RepositoryException { + return login(null, null); + } + + /** {@inheritDoc} */ + public synchronized Value getDescriptorValue(String key) { + try { + return remote.getDescriptorValue(key); + } catch (RemoteException e1) { + try { + remote = getRemoteRepository(false); + return remote.getDescriptorValue(key); + } catch (RemoteException e2) { + remote = new BrokenRemoteRepository(e2); + throw new RemoteRuntimeException(e2); + } + } + } + + /** {@inheritDoc} */ + public synchronized Value[] getDescriptorValues(String key) { + try { + return remote.getDescriptorValues(key); + } catch (RemoteException e1) { + try { + remote = getRemoteRepository(false); + return remote.getDescriptorValues(key); + } catch (RemoteException e2) { + remote = new BrokenRemoteRepository(e2); + throw new RemoteRuntimeException(e2); + } + } + } + + /** {@inheritDoc} */ + public synchronized boolean isSingleValueDescriptor(String key) { + try { + return remote.isSingleValueDescriptor(key); + } catch (RemoteException e1) { + try { + remote = getRemoteRepository(false); + return remote.isSingleValueDescriptor(key); + } catch (RemoteException e2) { + remote = new BrokenRemoteRepository(e2); + throw new RemoteRuntimeException(e2); + } + } + } + + /** {@inheritDoc} */ + public synchronized boolean isStandardDescriptor(String key) { + try { + return remote.isStandardDescriptor(key); + } catch (RemoteException e1) { + try { + remote = getRemoteRepository(false); + return remote.isStandardDescriptor(key); + } catch (RemoteException e2) { + remote = new BrokenRemoteRepository(e2); + throw new RemoteRuntimeException(e2); + } + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/SerializingContentHandler.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/SerializingContentHandler.java new file mode 100644 index 00000000000..7076210af2e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/SerializingContentHandler.java @@ -0,0 +1,439 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.client; + +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; + +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; +import org.xml.sax.helpers.DefaultHandler; + +/** + * A {@link ContentHandler} that serializes SAX events to a given + * {@link Result} instance. The JAXP {@link SAXTransformerFactory} + * facility is used for the serialization. + *

    + * This class explicitly ensures that all namespace prefixes are also + * present as xmlns attributes in the serialized XML document. This avoids + * problems with Xalan's serialization behaviour which was (at least during + * JDK 1.4) to ignore namespaces if they were not present as xmlns attributes. + *

    + * NOTE: The code in this class was originally written for Apache Cocoon and + * is included with some modifications here in Apache Jackrabbit. See the + * org.apache.cocoon.serialization.AbstractTextSerializer class in the + * cocoon-pipeline-impl component for the original code. + */ +class SerializingContentHandler extends DefaultContentHandler { + + /** + * The character encoding used for serialization (UTF-8). + * The encoding is fixed to make the text/xml content type safer to use. + * + * @see https://issues.apache.org/jira/browse/JCR-1621 + */ + public static final String ENCODING = "UTF-8"; + + /** The URI for xml namespaces */ + private static final String XML = "http://www.w3.org/XML/1998/namespace"; + + /** + * The factory used to create serializing SAX transformers. + */ + private static final SAXTransformerFactory FACTORY = + // Note that the cast from below is strictly speaking only valid when + // the factory instance supports the SAXTransformerFactory.FEATURE + // feature. But since this class would be useless without this feature, + // it's no problem to fail with a ClassCastException here and prevent + // this class from even being loaded. AFAIK all common JAXP + // implementations do support this feature. + (SAXTransformerFactory) TransformerFactory.newInstance(); + + /** + * Flag that indicates whether we need to work around the issue of + * the serializer not automatically generating the required xmlns + * attributes for the namespaces used in the document. + */ + private static final boolean NEEDS_XMLNS_ATTRIBUTES = + needsXmlnsAttributes(); + + /** + * Probes the available XML serializer for xmlns support. Used to set + * the value of the {@link #NEEDS_XMLNS_ATTRIBUTES} flag. + * + * @return whether the XML serializer needs explicit xmlns attributes + */ + private static boolean needsXmlnsAttributes() { + try { + StringWriter writer = new StringWriter(); + TransformerHandler probe = FACTORY.newTransformerHandler(); + probe.setResult(new StreamResult(writer)); + probe.startDocument(); + probe.startPrefixMapping("p", "uri"); + probe.startElement("uri", "e", "p:e", new AttributesImpl()); + probe.endElement("uri", "e", "p:e"); + probe.endPrefixMapping("p"); + probe.endDocument(); + return writer.toString().indexOf("xmlns") == -1; + } catch (Exception e) { + throw new UnsupportedOperationException("XML serialization fails"); + } + } + + /** + * Creates a serializing content handler that writes to the given stream. + * + * @param stream serialization target + * @return serializing content handler + * @throws SAXException if the content handler could not be initialized + */ + public static DefaultHandler getSerializer(OutputStream output) + throws SAXException { + return getSerializer(new StreamResult(output)); + } + + /** + * Creates a serializing content handler that writes to the given writer. + * + * @param writer serialization target + * @return serializing content handler + * @throws SAXException if the content handler could not be initialized + */ + public static DefaultHandler getSerializer(Writer writer) + throws SAXException { + return getSerializer(new StreamResult(writer)); + } + + /** + * Creates a serializing content handler that writes to the given result. + * + * @param result serialization target + * @return serializing content handler + * @throws SAXException if the content handler could not be initialized + */ + public static DefaultHandler getSerializer(Result result) + throws SAXException { + try { + TransformerHandler handler = FACTORY.newTransformerHandler(); + handler.setResult(result); + + // Specify the output properties to avoid surprises especially in + // character encoding or the output method (might be html for some + // documents!) + Transformer transformer = handler.getTransformer(); + transformer.setOutputProperty(OutputKeys.METHOD, "xml"); + transformer.setOutputProperty(OutputKeys.ENCODING, ENCODING); + transformer.setOutputProperty(OutputKeys.INDENT, "no"); + + if (NEEDS_XMLNS_ATTRIBUTES) { + // The serializer does not output xmlns declarations, + // so we need to do it explicitly with this wrapper + return new SerializingContentHandler(handler); + } else { + return new DefaultContentHandler(handler); + } + } catch (TransformerConfigurationException e) { + throw new SAXException("Failed to initialize XML serializer", e); + } + } + + /** + * The prefixes of startPrefixMapping() declarations for the coming element. + */ + private List prefixList = new ArrayList(); + + /** + * The URIs of startPrefixMapping() declarations for the coming element. + */ + private List uriList = new ArrayList(); + + /** + * Maps of URI<->prefix mappings. Used to work around a bug in the Xalan + * serializer. + */ + private Map uriToPrefixMap = new HashMap(); + private Map prefixToUriMap = new HashMap(); + + /** + * True if there has been some startPrefixMapping() for the coming element. + */ + private boolean hasMappings = false; + + /** + * Stack of the prefixes of explicitly generated prefix mapping calls + * per each element level. An entry is appended at the beginning of each + * {@link #startElement(String, String, String, Attributes)} call and + * removed at the end of each {@link #endElement(String, String, String)} + * call. By default the entry for each element is null to + * avoid losing performance, but whenever the code detects a new prefix + * mapping that needs to be registered, the null entry is + * replaced with a list of explicitly registered prefixes for that node. + * When that element is closed, the listed prefixes get unmapped. + * + * @see #checkPrefixMapping(String, String) + * @see JCR-1767 + */ + private final List addedPrefixMappings = new ArrayList(); + + private SerializingContentHandler(ContentHandler handler) { + super(handler); + } + + public void startDocument() throws SAXException { + // Cleanup + this.uriToPrefixMap.clear(); + this.prefixToUriMap.clear(); + clearMappings(); + super.startDocument(); + } + + /** + * Track mappings to be able to add xmlns: attributes + * in startElement(). + */ + public void startPrefixMapping(String prefix, String uri) throws SAXException { + // Store the mappings to reconstitute xmlns:attributes + // except prefixes starting with "xml": these are reserved + // VG: (uri != null) fixes NPE in startElement + if (uri != null && !prefix.startsWith("xml")) { + this.hasMappings = true; + this.prefixList.add(prefix); + this.uriList.add(uri); + + // append the prefix colon now, in order to save concatenations later, but + // only for non-empty prefixes. + if (prefix.length() > 0) { + this.uriToPrefixMap.put(uri, prefix + ":"); + } else { + this.uriToPrefixMap.put(uri, prefix); + } + + this.prefixToUriMap.put(prefix, uri); + } + super.startPrefixMapping(prefix, uri); + } + + /** + * Checks whether a prefix mapping already exists for the given namespace + * and generates the required {@link #startPrefixMapping(String, String)} + * call if the mapping is not found. By default the registered prefix + * is taken from the given qualified name, but a different prefix is + * automatically selected if that prefix is already used. + * + * @see JCR-1767 + * @param uri namespace URI + * @param qname element name with the prefix, or null + * @throws SAXException if the prefix mapping can not be added + */ + private void checkPrefixMapping(String uri, String qname) + throws SAXException { + // Only add the prefix mapping if the URI is not already known + if (uri != null && uri.length() > 0 && !uri.startsWith("xml") + && !uriToPrefixMap.containsKey(uri)) { + // Get the prefix + String prefix = "ns"; + if (qname != null && qname.length() > 0) { + int colon = qname.indexOf(':'); + if (colon != -1) { + prefix = qname.substring(0, colon); + } + } + + // Make sure that the prefix is unique + String base = prefix; + for (int i = 2; prefixToUriMap.containsKey(prefix); i++) { + prefix = base + i; + } + + int last = addedPrefixMappings.size() - 1; + List prefixes = (List) addedPrefixMappings.get(last); + if (prefixes == null) { + prefixes = new ArrayList(); + addedPrefixMappings.set(last, prefixes); + } + prefixes.add(prefix); + + startPrefixMapping(prefix, uri); + } + } + + /** + * Ensure all namespace declarations are present as xmlns: attributes + * and add those needed before calling superclass. This is a workaround for a Xalan bug + * (at least in version 2.0.1) : org.apache.xalan.serialize.SerializerToXML + * ignores start/endPrefixMapping(). + */ + public void startElement( + String eltUri, String eltLocalName, String eltQName, Attributes attrs) + throws SAXException { + // JCR-1767: Generate extra prefix mapping calls where needed + addedPrefixMappings.add(null); + checkPrefixMapping(eltUri, eltQName); + for (int i = 0; i < attrs.getLength(); i++) { + checkPrefixMapping(attrs.getURI(i), attrs.getQName(i)); + } + + // try to restore the qName. The map already contains the colon + if (null != eltUri && eltUri.length() != 0 && this.uriToPrefixMap.containsKey(eltUri)) { + eltQName = this.uriToPrefixMap.get(eltUri) + eltLocalName; + } + if (this.hasMappings) { + // Add xmlns* attributes where needed + + // New Attributes if we have to add some. + AttributesImpl newAttrs = null; + + int mappingCount = this.prefixList.size(); + int attrCount = attrs.getLength(); + + for (int mapping = 0; mapping < mappingCount; mapping++) { + + // Build infos for this namespace + String uri = (String) this.uriList.get(mapping); + String prefix = (String) this.prefixList.get(mapping); + String qName = prefix.equals("") ? "xmlns" : ("xmlns:" + prefix); + + // Search for the corresponding xmlns* attribute + boolean found = false; + for (int attr = 0; attr < attrCount; attr++) { + if (qName.equals(attrs.getQName(attr))) { + // Check if mapping and attribute URI match + if (!uri.equals(attrs.getValue(attr))) { + throw new SAXException("URI in prefix mapping and attribute do not match"); + } + found = true; + break; + } + } + + if (!found) { + // Need to add this namespace + if (newAttrs == null) { + // Need to test if attrs is empty or we go into an infinite loop... + // Well know SAX bug which I spent 3 hours to remind of :-( + if (attrCount == 0) { + newAttrs = new AttributesImpl(); + } else { + newAttrs = new AttributesImpl(attrs); + } + } + + if (prefix.equals("")) { + newAttrs.addAttribute(XML, qName, qName, "CDATA", uri); + } else { + newAttrs.addAttribute(XML, prefix, qName, "CDATA", uri); + } + } + } // end for mapping + + // Cleanup for the next element + clearMappings(); + + // Start element with new attributes, if any + super.startElement(eltUri, eltLocalName, eltQName, newAttrs == null ? attrs : newAttrs); + } else { + // Normal job + super.startElement(eltUri, eltLocalName, eltQName, attrs); + } + } + + + /** + * Receive notification of the end of an element. + * Try to restore the element qName. + */ + public void endElement(String eltUri, String eltLocalName, String eltQName) throws SAXException { + // try to restore the qName. The map already contains the colon + if (null != eltUri && eltUri.length() != 0 && this.uriToPrefixMap.containsKey(eltUri)) { + eltQName = this.uriToPrefixMap.get(eltUri) + eltLocalName; + } + + super.endElement(eltUri, eltLocalName, eltQName); + + // JCR-1767: Generate extra prefix un-mapping calls where needed + int last = addedPrefixMappings.size() - 1; + List prefixes = (List) addedPrefixMappings.remove(last); + if (prefixes != null) { + Iterator iterator = prefixes.iterator(); + while (iterator.hasNext()) { + endPrefixMapping((String) iterator.next()); + } + } + } + + /** + * End the scope of a prefix-URI mapping: + * remove entry from mapping tables. + */ + public void endPrefixMapping(String prefix) throws SAXException { + // remove mappings for xalan-bug-workaround. + // Unfortunately, we're not passed the uri, but the prefix here, + // so we need to maintain maps in both directions. + if (this.prefixToUriMap.containsKey(prefix)) { + this.uriToPrefixMap.remove(this.prefixToUriMap.get(prefix)); + this.prefixToUriMap.remove(prefix); + } + + if (hasMappings) { + // most of the time, start/endPrefixMapping calls have an element event between them, + // which will clear the hasMapping flag and so this code will only be executed in the + // rather rare occasion when there are start/endPrefixMapping calls with no element + // event in between. If we wouldn't remove the items from the prefixList and uriList here, + // the namespace would be incorrectly declared on the next element following the + // endPrefixMapping call. + int pos = prefixList.lastIndexOf(prefix); + if (pos != -1) { + prefixList.remove(pos); + uriList.remove(pos); + } + } + + super.endPrefixMapping(prefix); + } + + public void endDocument() throws SAXException { + // Cleanup + this.uriToPrefixMap.clear(); + this.prefixToUriMap.clear(); + clearMappings(); + super.endDocument(); + } + + private void clearMappings() { + this.hasMappings = false; + this.prefixList.clear(); + this.uriList.clear(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientIterator.java new file mode 100644 index 00000000000..113f66bfbd4 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientIterator.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.rmi.client.iterator; + +import java.rmi.RemoteException; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; + +import org.apache.jackrabbit.rmi.client.ClientObject; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.RemoteRuntimeException; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; + +/** + * A buffering local adapter for the JCR-RMI {@link RemoteIterator} + * interface. This class makes the remote iterator locally available + * using the JCR {@link RangeIterator} interface. The element arrays + * returned by the remote iterator are buffered locally. + *

    + * See the subclasses for type-specific versions of this abstract class. + */ +public abstract class ClientIterator extends ClientObject + implements RangeIterator { + + /** The adapted remote iterator. */ + private final RemoteIterator remote; + + /** + * The cached number of elements in the iterator, -1 if the iterator + * size is unknown, or -2 if the size has not been retrieved from the + * remote iterator. + */ + private long size; + + /** The position of the buffer within the iteration. */ + private long positionOfBuffer; + + /** The position within the buffer of the iteration. */ + private int positionInBuffer; + + /** + * The element buffer. Set to null when the end of the + * iteration has been reached. + */ + private Object[] buffer; + + /** + * Creates a local adapter for the given remote iterator. The element + * buffer is initially empty. + * + * @param remote remote iterator + * @param factory local adapter factory + */ + public ClientIterator(RemoteIterator remote, LocalAdapterFactory factory) { + super(factory); + this.remote = remote; + this.size = -2; + this.positionOfBuffer = 0; + this.positionInBuffer = 0; + this.buffer = new Object[0]; + } + + /** + * Returns the current position within the iterator. + * + * @return current position + * @see RangeIterator#getPosition() + */ + public long getPosition() { + return positionOfBuffer + positionInBuffer; + } + + /** + * Returns the size (the total number of elements) of this iteration. + * Returns -1 if the size is unknown. + *

    + * To minimize the number of remote method calls, the size is retrieved + * when this method is first called and cached for subsequent invocations. + * + * @return number of elements in the iteration, or -1 + * @throws RemoteRuntimeException on RMI errors + * @see RangeIterator#getSize() + */ + public long getSize() throws RemoteRuntimeException { + if (size == -2) { + try { + size = remote.getSize(); + } catch (RemoteException e) { + throw new RemoteRuntimeException(e); + } + } + return size; + } + + /** + * Skips the given number of elements in this iteration. + *

    + * The elements in the local element buffer are skipped first, and + * a remote skip method call is made only if more elements are being + * skipped than remain in the local buffer. + * + * @param skipNum the number of elements to skip + * @throws NoSuchElementException if skipped past the last element + * @throws RemoteRuntimeException on RMI errors + * @see RangeIterator#skip(long) + */ + public void skip(long skipNum) + throws NoSuchElementException, RemoteRuntimeException { + if (skipNum < 0) { + throw new IllegalArgumentException("Negative skip is not allowed"); + } else if (buffer == null && skipNum > 0) { + throw new NoSuchElementException("Skipped past the last element"); + } else if (positionInBuffer + skipNum <= buffer.length) { + positionInBuffer += skipNum; + } else { + try { + skipNum -= buffer.length - positionInBuffer; + remote.skip(skipNum); + positionInBuffer = buffer.length; + positionOfBuffer += skipNum; + } catch (RemoteException e) { + throw new RemoteRuntimeException(e); + } catch (NoSuchElementException e) { + buffer = null; // End of iterator reached + throw e; + } + } + } + + /** + * Advances the element buffer if there are no more elements in it. The + * element buffer is set to null if the end of the iteration + * has been reached. + * + * @throws RemoteException on RMI errors + */ + private void advance() throws RemoteException { + if (buffer != null && positionInBuffer == buffer.length) { + positionOfBuffer += buffer.length; + positionInBuffer = 0; + buffer = remote.nextObjects(); + if (buffer == null) { + size = positionOfBuffer; + } + } + } + + /** + * Checks if there are more elements in this iteration. + * + * @return true if there are more elements, + * false otherwise + * @throws RemoteRuntimeException on RMI errors + */ + public boolean hasNext() throws RemoteRuntimeException { + try { + advance(); + return buffer != null; + } catch (RemoteException e) { + throw new RemoteRuntimeException(e); + } + } + + /** + * Returns a local adapter for the given remote object. This abstract + * method is used by {@link #next()} to convert the remote references + * returned by the remote iterator to local adapters. + *

    + * Subclasses should implement this method to use the local adapter + * factory to create local adapters of the specific element type. + * + * @param remote remote object + * @return local adapter + */ + protected abstract Object getObject(Object remote); + + /** + * Returns the next element in this iteration. + * + * @return next element + * @throws NoSuchElementException if there are no more elements + * @throws RemoteRuntimeException on RMI errors + */ + public Object next() throws NoSuchElementException, RemoteRuntimeException { + try { + advance(); + if (buffer == null) { + throw new NoSuchElementException("End of iterator reached"); + } else { + return getObject(buffer[positionInBuffer++]); + } + } catch (RemoteException e) { + throw new RemoteRuntimeException(e); + } + } + + /** + * Not supported. + * + * @throws UnsupportedOperationException always thrown + */ + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientNodeIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientNodeIterator.java new file mode 100644 index 00000000000..a8e228cbf26 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientNodeIterator.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.rmi.client.iterator; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteNode; + +/** + * A ClientIterator for iterating remote nodes. + */ +public class ClientNodeIterator extends ClientIterator implements NodeIterator { + + /** The current session. */ + private final Session session; + + /** + * Creates a ClientNodeIterator instance. + * + * @param iterator remote iterator + * @param session current session + * @param factory local adapter factory + */ + public ClientNodeIterator( + RemoteIterator iterator, Session session, + LocalAdapterFactory factory) { + super(iterator, factory); + this.session = session; + } + + /** + * Creates and returns a local adapter for the given remote node. + * + * @param remote remote referecne + * @return local adapter + * @see ClientIterator#getObject(Object) + */ + protected Object getObject(Object remote) { + return getNode(session, (RemoteNode) remote); + } + + /** + * Returns the next node in this iteration. + * + * @return next node + * @see NodeIterator#nextNode() + */ + public Node nextNode() { + return (Node) next(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientNodeTypeIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientNodeTypeIterator.java new file mode 100644 index 00000000000..3bc09cab013 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientNodeTypeIterator.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.rmi.client.iterator; + +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; + +/** + * A ClientIterator for iterating remote node types. + */ +public class ClientNodeTypeIterator extends ClientIterator + implements NodeTypeIterator { + + /** + * Creates a ClientNodeTypeIterator instance. + * + * @param iterator remote iterator + * @param factory local adapter factory + */ + public ClientNodeTypeIterator( + RemoteIterator iterator, LocalAdapterFactory factory) { + super(iterator, factory); + } + + /** + * Creates and returns a local adapter for the given remote node. + * + * @param remote remote referecne + * @return local adapter + * @see ClientIterator#getObject(Object) + */ + protected Object getObject(Object remote) { + return getFactory().getNodeType((RemoteNodeType) remote); + } + + /** + * Returns the next node type in this iteration. + * + * @return next node type + * @see NodeTypeIterator#nextNodeType() + */ + public NodeType nextNodeType() { + return (NodeType) next(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientPropertyIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientPropertyIterator.java new file mode 100644 index 00000000000..a180f508dd0 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientPropertyIterator.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.rmi.client.iterator; + +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.Session; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; + +/** + * A ClientIterator for iterating remote properties. + */ +public class ClientPropertyIterator extends ClientIterator + implements PropertyIterator { + + /** The current session. */ + private final Session session; + + /** + * Creates a ClientPropertyIterator instance. + * + * @param iterator remote iterator + * @param session current session + * @param factory local adapter factory + */ + public ClientPropertyIterator( + RemoteIterator iterator, Session session, + LocalAdapterFactory factory) { + super(iterator, factory); + this.session = session; + } + + /** + * Creates and returns a local adapter for the given remote property. + * + * @param remote remote referecne + * @return local adapter + * @see ClientIterator#getObject(Object) + */ + protected Object getObject(Object remote) { + return getFactory().getProperty(session, (RemoteProperty) remote); + } + + /** + * Returns the next property in this iteration. + * + * @return next property + * @see PropertyIterator#nextProperty() + */ + public Property nextProperty() { + return (Property) next(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientRowIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientRowIterator.java new file mode 100644 index 00000000000..7014fbeaa09 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientRowIterator.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.rmi.client.iterator; + +import javax.jcr.Session; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteRow; + +/** + * A ClientIterator for iterating remote rows. + */ +public class ClientRowIterator extends ClientIterator implements RowIterator { + + /** Current session. */ + private Session session; + + /** + * Creates a ClientRowIterator instance. + * + * @param iterator remote iterator + * @param factory local adapter factory + */ + public ClientRowIterator(Session session, + RemoteIterator iterator, LocalAdapterFactory factory) { + super(iterator, factory); + this.session = session; + } + + /** + * Creates and returns a local adapter for the given remote row. + * + * @param remote remote reference + * @return local adapter + * @see ClientIterator#getObject(Object) + */ + protected Object getObject(Object remote) { + return getFactory().getRow(session, (RemoteRow) remote); + } + + /** + * Returns the next row in this iteration. + * + * @return next row + * @see RowIterator#nextRow() + */ + public Row nextRow() { + return (Row) next(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientVersionIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientVersionIterator.java new file mode 100644 index 00000000000..44832ef8db1 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/iterator/ClientVersionIterator.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.rmi.client.iterator; + +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionIterator; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; + +/** + * A ClientIterator for iterating remote versions. + */ +public class ClientVersionIterator extends ClientIterator + implements VersionIterator { + + /** The current session. */ + private final Session session; + + /** + * Creates a ClientVersionIterator instance. + * + * @param iterator remote iterator + * @param session current session + * @param factory local adapter factory + */ + public ClientVersionIterator( + RemoteIterator iterator, Session session, + LocalAdapterFactory factory) { + super(iterator, factory); + this.session = session; + } + + /** + * Creates and returns a local adapter for the given remote version. + * + * @param remote remote referecne + * @return local adapter + * @see ClientIterator#getObject(Object) + */ + protected Object getObject(Object remote) { + return getFactory().getVersion(session, (RemoteVersion) remote); + } + + /** + * Returns the next version in this iteration. + * + * @return next version + * @see VersionIterator#nextVersion() + */ + public Version nextVersion() { + return (Version) next(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientGroup.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientGroup.java new file mode 100644 index 00000000000..393ca7bf7bb --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientGroup.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.rmi.client.principal; + +import java.rmi.RemoteException; +import java.security.Principal; +import java.security.acl.Group; +import java.util.Enumeration; +import java.util.Iterator; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.RemoteRuntimeException; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.principal.RemoteGroup; +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; + +/** + * Local adapter for the JCR-RMI {@link RemoteGroup RemoteGroup} interface. This + * class makes a remote group locally available using the Java {@link Group} + * interface. + * + * @see Group + * @see RemoteGroup + */ +public class ClientGroup extends ClientPrincipal implements Group { + + private final LocalAdapterFactory factory; + + public ClientGroup(final RemotePrincipal p, + final LocalAdapterFactory factory) { + super(p); + this.factory = factory; + } + + /** + * @return false - this method is not implemented yet + */ + public boolean addMember(Principal user) { + // no support for adding member here + return false; + } + + public boolean isMember(Principal member) { + try { + return ((RemoteGroup) getRemotePrincipal()).isMember(member.getName()); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + public Enumeration members() { + try { + final RemoteIterator remote = ((RemoteGroup) getRemotePrincipal()).members(); + final Iterator pi = factory.getPrincipalIterator(remote); + return new Enumeration() { + public boolean hasMoreElements() { + return pi.hasNext(); + } + + public Principal nextElement() { + return pi.next(); + } + }; + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + /** + * @return false - this method is not implemented yet + */ + public boolean removeMember(Principal user) { + // no support for removing member here + return false; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientPrincipal.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientPrincipal.java new file mode 100644 index 00000000000..266c0810688 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientPrincipal.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.rmi.client.principal; + +import java.rmi.RemoteException; +import java.security.Principal; + +import org.apache.jackrabbit.rmi.client.RemoteRuntimeException; +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; + +/** + * Local adapter for the JCR-RMI {@link RemotePrincipal RemotePrincipal} + * interface. This class makes a remote principal locally available using the + * Java {@link Principal} interface. + * + * @see Principal + * @see RemotePrincipal + */ +public class ClientPrincipal implements Principal { + + private final RemotePrincipal p; + + public ClientPrincipal(final RemotePrincipal p) { + this.p = p; + } + + /** {@inheritDoc} */ + public String getName() { + try { + return p.getName(); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + /** + * Returns the {@link RemotePrincipal} encapsulated in this instance. + *

    + * NOTE: This method is intended to only be used in the JCR RMI + * implementation to be able to "send back" remote principals to the server + * for implementation of the remote JCR API. + * + * @return the {@link RemotePrincipal} encapsulated in this instance. + */ + public final RemotePrincipal getRemotePrincipal() { + return p; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientPrincipalIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientPrincipalIterator.java new file mode 100644 index 00000000000..207e113808f --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/principal/ClientPrincipalIterator.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.rmi.client.principal; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.iterator.ClientIterator; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; + +/** + * A ClientIterator for iterating remote principals + */ +public class ClientPrincipalIterator extends ClientIterator { + + public ClientPrincipalIterator(final RemoteIterator iterator, + final LocalAdapterFactory factory) { + super(iterator, factory); + } + + /** {@inheritDoc} */ + @Override + protected Object getObject(Object remote) { + return getFactory().getPrincipal((RemotePrincipal) remote); + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlEntry.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlEntry.java new file mode 100644 index 00000000000..3554cdb1a11 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlEntry.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.rmi.client.security; + +import java.rmi.RemoteException; +import java.security.Principal; + +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.rmi.client.ClientObject; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.RemoteRuntimeException; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlEntry; + +/** + * Local adapter for the JCR-RMI {@link RemoteAccessControlEntry + * RemoteAccessControlEntry} interface. This class makes a remote + * AccessControlEntry locally available using the JCR {@link AccessControlEntry + * AccessControlEntry} interface. + * + * @see javax.jcr.security.AccessControlEntry + * @see org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlEntry + */ +public class ClientAccessControlEntry extends ClientObject implements + AccessControlEntry { + + private final RemoteAccessControlEntry race; + + public ClientAccessControlEntry(final RemoteAccessControlEntry race, + final LocalAdapterFactory factory) { + super(factory); + this.race = race; + } + + /** {@inheritDoc} */ + public Principal getPrincipal() { + try { + return getFactory().getPrincipal(race.getPrincipal()); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + /** {@inheritDoc} */ + public Privilege[] getPrivileges() { + try { + return getFactory().getPrivilege(race.getPrivileges()); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + RemoteAccessControlEntry getRemoteAccessControlEntry() { + return race; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlList.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlList.java new file mode 100644 index 00000000000..98674442199 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlList.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.rmi.client.security; + +import java.rmi.RemoteException; +import java.security.Principal; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.RemoteRepositoryException; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlList; + +/** + * Local adapter for the JCR-RMI {@link RemoteAccessControlList + * RemoteAccessControlList} interface. This class makes a remote + * AccessControlList locally available using the JCR {@link AccessControlList + * AccessControlList} interface. + * + * @see javax.jcr.security.AccessControlList + * @see org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlList + */ +public class ClientAccessControlList extends ClientAccessControlPolicy + implements AccessControlList { + + public ClientAccessControlList(final RemoteAccessControlList racl, + final LocalAdapterFactory factory) { + super(racl, factory); + } + + /** + * @throws UnsupportedRepositoryOperationException This method is not + * implemented yet + */ + public boolean addAccessControlEntry(Principal principal, + Privilege[] privileges) + throws UnsupportedRepositoryOperationException { + // TODO: implement client side of the story + throw new UnsupportedRepositoryOperationException( + "addAccessControlEntry"); + } + + /** {@inheritDoc} */ + public AccessControlEntry[] getAccessControlEntries() + throws RepositoryException { + try { + return getFactory().getAccessControlEntry( + ((RemoteAccessControlList) getRemoteAccessControlPolicy()).getAccessControlEntries()); + } catch (RemoteException re) { + throw new RemoteRepositoryException(re); + } + } + + /** + * @throws UnsupportedRepositoryOperationException This method is not + * implemented yet + */ + public void removeAccessControlEntry(AccessControlEntry ace) + throws UnsupportedRepositoryOperationException { + // TODO: implement client side of the story + throw new UnsupportedRepositoryOperationException( + "removeAccessControlEntry"); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlManager.java new file mode 100644 index 00000000000..ef8996ff9c6 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlManager.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.rmi.client.security; + +import java.rmi.RemoteException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import org.apache.jackrabbit.rmi.client.ClientObject; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.RemoteRepositoryException; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager; + +/** + * Local adapter for the JCR-RMI {@link RemoteAccessControlManager + * RemoteAccessControlManager} interface. This class makes a remote + * AccessControlManager locally available using the JCR + * {@link AccessControlManager AccessControlManager} interface. + * + * @see javax.jcr.security.AccessControlManager + * @see org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager + */ +public class ClientAccessControlManager extends ClientObject implements + AccessControlManager { + + private final RemoteAccessControlManager racm; + + public ClientAccessControlManager(final RemoteAccessControlManager racm, + final LocalAdapterFactory factory) { + super(factory); + this.racm = racm; + } + + /** {@inheritDoc} */ + public AccessControlPolicyIterator getApplicablePolicies(String absPath) + throws RepositoryException { + try { + return getFactory().getAccessControlPolicyIterator( + racm.getApplicablePolicies(absPath)); + } catch (RemoteException re) { + throw new RemoteRepositoryException(re); + } + } + + /** {@inheritDoc} */ + public AccessControlPolicy[] getEffectivePolicies(String absPath) + throws PathNotFoundException, AccessDeniedException, + RepositoryException { + try { + return getFactory().getAccessControlPolicy( + racm.getEffectivePolicies(absPath)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public AccessControlPolicy[] getPolicies(String absPath) + throws PathNotFoundException, AccessDeniedException, + RepositoryException { + try { + return getFactory().getAccessControlPolicy( + racm.getPolicies(absPath)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Privilege[] getPrivileges(String absPath) + throws PathNotFoundException, RepositoryException { + try { + return getFactory().getPrivilege(racm.getPrivileges(absPath)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Privilege[] getSupportedPrivileges(String absPath) + throws PathNotFoundException, RepositoryException { + try { + return getFactory().getPrivilege( + racm.getSupportedPrivileges(absPath)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasPrivileges(String absPath, Privilege[] privileges) + throws PathNotFoundException, RepositoryException { + String[] privNames = new String[privileges.length]; + for (int i = 0; i < privNames.length; i++) { + privNames[i] = privileges[i].getName(); + } + + try { + return racm.hasPrivileges(absPath, privNames); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + + } + + /** {@inheritDoc} */ + public Privilege privilegeFromName(String privilegeName) + throws AccessControlException, RepositoryException { + try { + return getFactory().getPrivilege( + racm.privilegeFromName(privilegeName)); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** + * @throws UnsupportedRepositoryOperationException This method is not + * implemented yet + */ + public void removePolicy(String absPath, AccessControlPolicy policy) + throws UnsupportedRepositoryOperationException { + // TODO: implement client side of the story + throw new UnsupportedRepositoryOperationException("removePolicy"); + } + + /** + * @throws UnsupportedRepositoryOperationException This method is not + * implemented yet + */ + public void setPolicy(String absPath, AccessControlPolicy policy) + throws UnsupportedRepositoryOperationException { + // TODO: implement client side of the story + throw new UnsupportedRepositoryOperationException("setPolicy"); + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlPolicy.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlPolicy.java new file mode 100644 index 00000000000..e68b5f55de3 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlPolicy.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.rmi.client.security; + +import javax.jcr.security.AccessControlPolicy; +import org.apache.jackrabbit.rmi.client.ClientObject; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlPolicy; + +/** + * Local adapter for the JCR-RMI {@link RemoteAccessControlPolicy + * RemoteAccessControlPolicy} interface. This class makes a remote + * AccessControlPolicy locally available using the JCR + * {@link AccessControlPolicy AccessControlPolicy} interface. + * + * @see javax.jcr.security.AccessControlPolicy + * @see org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlPolicy + */ +public class ClientAccessControlPolicy extends ClientObject implements + AccessControlPolicy { + + private final RemoteAccessControlPolicy racp; + + public ClientAccessControlPolicy(final RemoteAccessControlPolicy racp, + final LocalAdapterFactory factory) { + super(factory); + this.racp = racp; + } + + RemoteAccessControlPolicy getRemoteAccessControlPolicy() { + return racp; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlPolicyIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlPolicyIterator.java new file mode 100644 index 00000000000..fe43e1d5424 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientAccessControlPolicyIterator.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.rmi.client.security; + +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.iterator.ClientIterator; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlList; + +/** + * A ClientIterator for iterating remote access control policies. + */ +public class ClientAccessControlPolicyIterator extends ClientIterator implements + AccessControlPolicyIterator { + + public ClientAccessControlPolicyIterator(RemoteIterator iterator, + LocalAdapterFactory factory) { + super(iterator, factory); + } + + /** {@inheritDoc} */ + protected Object getObject(Object remote) { + return getFactory().getAccessControlPolicy( + (RemoteAccessControlList) remote); + } + + /** {@inheritDoc} */ + public AccessControlPolicy nextAccessControlPolicy() { + return (AccessControlPolicy) next(); + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientPrivilege.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientPrivilege.java new file mode 100644 index 00000000000..b8a6450cdd5 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/client/security/ClientPrivilege.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.rmi.client.security; + +import java.rmi.RemoteException; + +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.rmi.client.ClientObject; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.RemoteRuntimeException; +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; + +/** + * Local adapter for the JCR-RMI {@link RemotePrivilege RemotePrivilege} + * interface. This class makes a remote Privilege locally available using the + * JCR {@link Privilege Privilege} interface. + * + * @see javax.jcr.security.Privilege + * @see org.apache.jackrabbit.rmi.remote.security.RemotePrivilege + */ +public class ClientPrivilege extends ClientObject implements Privilege { + + private final RemotePrivilege rp; + + public ClientPrivilege(final RemotePrivilege rp, + final LocalAdapterFactory factory) { + super(factory); + this.rp = rp; + } + + /** {@inheritDoc} */ + public Privilege[] getAggregatePrivileges() { + try { + return getFactory().getPrivilege(rp.getAggregatePrivileges()); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + /** {@inheritDoc} */ + public Privilege[] getDeclaredAggregatePrivileges() { + try { + return getFactory().getPrivilege( + rp.getDeclaredAggregatePrivileges()); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + /** {@inheritDoc} */ + public String getName() { + try { + return rp.getName(); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + /** {@inheritDoc} */ + public boolean isAbstract() { + try { + return rp.isAbstract(); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + /** {@inheritDoc} */ + public boolean isAggregate() { + try { + return rp.isAggregate(); + } catch (RemoteException re) { + throw new RemoteRuntimeException(re); + } + } + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } else if (obj == null) { + return false; + } + + if (obj instanceof Privilege) { + return getName().equals(((Privilege) obj).getName()); + } + + return false; + } + + public int hashCode() { + return getName().hashCode(); + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayEventIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayEventIterator.java new file mode 100644 index 00000000000..543f620d9d7 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayEventIterator.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.rmi.iterator; + +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; + +/** + * Array implementation of the JCR + * {@link javax.jcr.observation.EventIterator EventIterator} interface. + * This class is used by the JCR-RMI client adapters to convert + * node arrays to iterators. + */ +public class ArrayEventIterator extends ArrayIterator implements EventIterator { + + /** + * Creates an iterator for the given array of events. + * + * @param nodes the nodes to iterate + */ + public ArrayEventIterator(Event[] nodes) { + super(nodes); + } + + /** {@inheritDoc} */ + public Event nextEvent() { + return (Event) next(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayEventListenerIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayEventListenerIterator.java new file mode 100644 index 00000000000..8a497982ead --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayEventListenerIterator.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.rmi.iterator; + +import javax.jcr.observation.EventListener; +import javax.jcr.observation.EventListenerIterator; + +/** + * Array implementation of the JCR + * {@link javax.jcr.observation.EventListenerIterator EventListenerIterator} interface. + * This class is used by the JCR-RMI client adapters to convert + * listener arrays to iterators. + */ +public class ArrayEventListenerIterator extends ArrayIterator + implements EventListenerIterator { + + /** + * Creates an iterator for the given array of listeners. + * + * @param listeners the listeners to iterate + */ + public ArrayEventListenerIterator(EventListener[] listeners) { + super(listeners); + } + + /** {@inheritDoc} */ + public EventListener nextEventListener() { + return (EventListener) next(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayIterator.java new file mode 100644 index 00000000000..f06ca8c7f62 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/iterator/ArrayIterator.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.rmi.iterator; + +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; + +/** + * Array implementation of the JCR + * {@link javax.jcr.RangeIterator RangeIterator} interface. This class + * implements the RangeIterator functionality for an underlying array + * of objects. Used as the base class for the type-specific iterator + * classes defined in this package. + */ +public class ArrayIterator implements RangeIterator { + + /** The current iterator position. */ + private int position; + + /** The underlying array of objects. */ + private Object[] array; + + /** + * Creates an iterator for the given array of objects. + * + * @param array the objects to iterate + */ + public ArrayIterator(Object[] array) { + this.position = 0; + this.array = array; + } + + /** {@inheritDoc} */ + public boolean hasNext() { + return (position < array.length); + } + + /** {@inheritDoc} */ + public Object next() { + return array[position++]; + } + + /** {@inheritDoc} */ + public void remove() { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + public void skip(long items) { + position += items; + if (position > array.length) { + position = array.length; + throw new NoSuchElementException( + "Skipped past the last element of the iterator"); + } + } + + /** {@inheritDoc} */ + public long getSize() { + return array.length; + } + + /** {@inheritDoc} */ + public long getPosition() { + return position; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/jackrabbit/JackrabbitClientAdapterFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/jackrabbit/JackrabbitClientAdapterFactory.java new file mode 100644 index 00000000000..972172d9cc4 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/jackrabbit/JackrabbitClientAdapterFactory.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.rmi.jackrabbit; + +import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; + +/** + * @deprecated Use the normal {@link ClientAdapterFactory} instead + */ +public class JackrabbitClientAdapterFactory extends ClientAdapterFactory { +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/ClientEventPoll.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/ClientEventPoll.java new file mode 100644 index 00000000000..7c3dca80819 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/ClientEventPoll.java @@ -0,0 +1,342 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.observation; + +import java.rmi.RemoteException; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; + +import org.apache.jackrabbit.rmi.client.RemoteRepositoryException; +import org.apache.jackrabbit.rmi.client.RemoteRuntimeException; +import org.apache.jackrabbit.rmi.iterator.ArrayEventIterator; +import org.apache.jackrabbit.rmi.remote.RemoteEventCollection; +import org.apache.jackrabbit.rmi.remote.RemoteObservationManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The ClientEventPoll class is the registry for client-side + * event listeners on behalf of the + * {@link org.apache.jackrabbit.rmi.client.ClientObservationManager} class. In + * addition this class extends the java.lang.Thread class able + * to be run in a separate thread to constantly poll the server-side observation + * manager for new events. + *

    + * Notes: + *

      + *
    1. Only one instance of this class should be instantiated for each instance + * of a {@link org.apache.jackrabbit.rmi.remote.RemoteObservationManager} class. + *
    2. EventListeners registered with this class must properly + * implement the Object.hashCode() and Object.equals() + * contracts for them to be handled correctly by this class. + *
    + * + * @see #run() + */ +public class ClientEventPoll extends Thread { + + /** logger */ + private static final Logger log = + LoggerFactory.getLogger(ClientEventPoll.class); + + /** + * The time in milliseconds the {@link #run()} method should be waiting + * for remote events. + * @see #run() + */ + private static final long POLL_TIMEOUT = 5000; + + /** The thread name */ + private static final String THREAD_NAME = "Client Event Poller"; + + /** The primitive unique identifier generator. */ + private static long counter = 0; + + /** The {@link RemoteObservationManager} called for the new events. */ + private final RemoteObservationManager remote; + + /** + * The Session checked by the {@link #run} method whether it + * is still alive or the thread should terminate. + */ + private final Session session; + + /** The map of locally registered listeners indexed by the unique identifier */ + private Map listenerMap = new HashMap(); + + /** The map of unique identifieres indexed by the registered listeners */ + private Map idMap = new HashMap(); + + /** + * Flag indicating whether the {@link #run()} method should terminate. + * @see #run() + */ + private boolean running = true; + + /** + * Creates an instance of this class talking to the given + * {@link RemoteObservationManager}. + * + * @param remote The remote observation manager which is asked for new + * events. This must not be null. + * @param session The Session which is asked whether it is + * alive by the {@link #run()} method. This must not be null. + * + * @throws NullPointerException if remote or session + * is null. + */ + public ClientEventPoll(RemoteObservationManager remote, Session session) + throws NullPointerException { + super(THREAD_NAME); + + // check remote and session + if (remote == null) { + throw new NullPointerException("remote"); + } + if (session == null) { + throw new NullPointerException("session"); + } + + this.remote = remote; + this.session = session; + } + + /** + * Registers the given local listener with this instance and returns the + * unique identifier assigned to it. + * + * @param listener The EventListener to register. + * + * @return The unique identifier assigned to the newly registered event + * listener. + */ + public synchronized long addListener(EventListener listener) { + Long id = new Long(counter++); + listenerMap.put(id, listener); + idMap.put(listener, id); + return id.longValue(); + } + + /** + * Unregisters the given local listener from this instance and returns the + * unique identifier assigned to it. + * + * @param listener The EventListener to unregister. + * + * @return The unique identifier assigned to the unregistered event listener + * or -1 if the listener was not registered. + */ + public synchronized long removeListener(EventListener listener) { + Long key = (Long) idMap.remove(listener); + if (key != null) { + listenerMap.remove(key); + return key.longValue(); + } + + return -1; + } + + /** + * Returns an array of the registered event listeners. + * + * @return registered event listeners + */ + public synchronized EventListener[] getListeners() { + return (EventListener[]) listenerMap.values().toArray( + new EventListener[(listenerMap.size())]); + } + + /** + * Indicates to the {@link #run()} method, that asking for events should + * be terminated. + * + * @see #run() + */ + public void terminate() { + this.running = false; + } + + //---------- Thread overwrite --------------------------------------------- + + /** + * Checks for remote events and dispatches them to the locally registered + * event listeners. This is how this method works: + *
      + *
    1. Continue with next step if {@link #terminate()} has not been called + * yet and the session is still alive. + *
    2. Call the {@link RemoteObservationManager#getNextEvent(long)} method + * waiting for a specified time (5 seconds). + *
    3. If no event was received in the specified time go back to step #1. + *
    4. Extract the unique listener identifier from the remote event and + * find it in the list of locally registered event listeners. Go back to + * step #1 if no such listener exists. + *
    5. Convert the remote event list to an EventIterator and + * call the EventListener.onEvent() method. + *
    6. Go back to step #1. + *
    + */ + public void run() { + while (running && session.isLive()) { + try { + // ask for an event waiting at most POLL_TIMEOUT milliseconds + RemoteEventCollection remoteEvent = remote.getNextEvent(POLL_TIMEOUT); + + // poll time out, check running and ask again + if (remoteEvent == null) { + continue; + } + + // extract the listener id from the remote event and find + // the locally registered event listener + Long id = new Long(remoteEvent.getListenerId()); + EventListener listener = (EventListener) listenerMap.get(id); + + // if the listener is not registered (anymore), the event is + // silently ignored, running is checked and the server asked again + if (listener == null) { + continue; + } + + // otherwise convert the remote events into an EventIterator + // and the listener is called + RemoteEventCollection.RemoteEvent[] remoteEvents = remoteEvent.getEvents(); + EventIterator events = toEvents(remoteEvents); + try { + listener.onEvent(events); + } catch (Exception e) { + log.error("Unexpected failure of Listener " + listener, e); + } + + } catch (RemoteException re) { + log.error("Problem handling event. Looking for next one.", re); + } + } + } + + //---------- internal ----------------------------------------------------- + + /** + * Converts an array of {@link RemoteEventCollection.RemoteEvent} instances to an + * instance of EventIterator suitable to be sent to the + * event listener. + * + * @param remoteEvents array of remote events + * @return event iterator + * @throws RemoteException on RMI errors + */ + private EventIterator toEvents(RemoteEventCollection.RemoteEvent[] remoteEvents) + throws RemoteException { + Event[] events = new Event[remoteEvents.length]; + for (int i = 0; i < events.length; i++) { + events[i] = new JCREvent(remoteEvents[i]); + } + return new ArrayEventIterator(events); + } + + /** + * The JCREvent class is a simple implementation of the JCR + * Event interface to be sent to the locally registered + * event listeners. + * + * @author Felix Meschberger + */ + private static class JCREvent implements Event { + + /** The adapted remote event. */ + private final RemoteEventCollection.RemoteEvent remote; + + /** + * Creates an instance of this class from the contents of the given + * remoteEvent. + * + * @param remoteEvent The {@link RemoteEventCollection.RemoteEvent} instance + * providing the data for this event. + */ + private JCREvent(RemoteEventCollection.RemoteEvent remoteEvent) { + remote = remoteEvent; + } + + /** {@inheritDoc} */ + public int getType() { + try { + return remote.getType(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public String getPath() throws RepositoryException { + try { + return remote.getPath(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getUserID() { + try { + return remote.getUserID(); + } catch (RemoteException ex) { + throw new RemoteRuntimeException(ex); + } + } + + /** {@inheritDoc} */ + public long getDate() throws RepositoryException { + try { + return remote.getDate(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getIdentifier() throws RepositoryException { + try { + return remote.getIdentifier(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Map getInfo() throws RepositoryException { + try { + return remote.getInfo(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getUserData() throws RepositoryException { + try { + return remote.getUserData(); + } catch (RemoteException ex) { + throw new RemoteRepositoryException(ex); + } + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/Queue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/Queue.java new file mode 100644 index 00000000000..c0f22230264 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/Queue.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.rmi.observation; + +import java.util.LinkedList; + +/** + * The Queue class is a very simple queue assuming that there is + * at least one consumer and potentially multiple producers. This class poses + * no restrictions on the size of the queue. + */ +public class Queue { + + /** The linked list implementing the queue of data */ + private final LinkedList queue; + + /** + * Creates an instance of this queue. + */ + public Queue() { + queue = new LinkedList(); + } + + /** + * Appends the given object to the end of the queue. + *

    + * After appending the element, the queue is notified such that threads + * waiting to retrieve an element from the queue are woken up. + * + * @param object the object to be added + */ + public void put(Object object) { + synchronized (queue) { + queue.addLast(object); + queue.notifyAll(); + } + } + + /** + * Returns the first element from the queue. If the queue is currently empty + * the method waits at most the given number of milliseconds. + * + * @param timeout The maximum number of milliseconds to wait for an entry in + * the queue if the queue is empty. If zero, the method waits forever + * for an element. + * + * @return The first element of the queue or null if the method + * timed out waiting for an entry. + * + * @throws InterruptedException Is thrown if the current thread is + * interrupted while waiting for the queue to get at least one entry. + */ + public Object get(long timeout) throws InterruptedException { + synchronized (queue) { + // wait for data if the queue is empty + if (queue.isEmpty()) { + queue.wait(timeout); + } + + // return null if queue is (still) empty + if (queue.isEmpty()) { + return null; + } + + // return first if queue has content now + return queue.removeFirst(); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/ServerEventListenerProxy.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/ServerEventListenerProxy.java new file mode 100644 index 00000000000..ad4a88267cd --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/observation/ServerEventListenerProxy.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.rmi.observation; + +import java.rmi.RemoteException; + +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; + +import org.apache.jackrabbit.rmi.remote.RemoteEventCollection; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The ServerEventListenerProxy class is the server-side event + * listener proxy registered on behalf of a client-side event listener identified + * with the unique identifier. + *

    + * The term Server in this class indicates, that this is a server-side + * class. In contrast to the classes in the + * {@link org.apache.jackrabbit.rmi.server} package, this class neither extends + * the {@link org.apache.jackrabbit.rmi.server.ServerObject} class nor does it + * implement any of the remote interfaces in the + * {@link org.apache.jackrabbit.rmi.remote} package because it only is + * instantiated to be used on the server side. + *

    + * See the package overview for an explanation of the mechanisms implemented for + * event dispatching. + */ +public class ServerEventListenerProxy implements EventListener { + + /** logger */ + private static final Logger log = + LoggerFactory.getLogger(ServerEventListenerProxy.class); + + /** The factory used to convert event iterators to remote events */ + private final RemoteAdapterFactory factory; + + /** + * The unique indentifier of the client-side event listener on whose + * behalf this listener proxy is registered. + */ + private final long listenerId; + + /** + * The queue to which remote events are queue for them to be picked up + * by calls to the + * {@link org.apache.jackrabbit.rmi.remote.RemoteObservationManager#getNextEvent(long)} + * method. + */ + private final Queue queue; + + /** + * Creates a new instance of this listener proxy. + * + * @param factory The {@link RemoteAdapterFactory} used to convert the + * {@link EventIterator} instances to {@link RemoteEventCollection} objects. + * @param listenerId The unique identifier of the client-side event listener + * on whose behalf this proxy works. + * @param queue The sink to which events to be dispatched to the client are + * queued to be picked up. + */ + public ServerEventListenerProxy(RemoteAdapterFactory factory, + long listenerId, Queue queue) { + this.factory = factory; + this.listenerId = listenerId; + this.queue = queue; + } + + /** + * Converts the {@link javax.jcr.observation.Event} instances in the given + * iterator to an instance of {@link RemoteEventCollection} for them to be dispatched + * to the client-side event listener. + * + * @param events The {@link javax.jcr.observation.Event Events} to be + * dispatched. + */ + public void onEvent(EventIterator events) { + try { + RemoteEventCollection remoteEvent = factory.getRemoteEvent(listenerId, events); + queue.put(remoteEvent); + } catch (RemoteException re) { + Throwable t = (re.getCause() == null) ? re : re.getCause(); + log.error("Problem creating remote event for " + listenerId, t); + } + } + + //---------- Object overwrite ---------------------------------------------- + + /** + * Returns the client-side listener identifier as its hash code. + * + * @return hash code + */ + public int hashCode() { + return (int) listenerId; + } + + /** + * Returns true if obj is either the same as this + * or a proxy for the same client-side listener, which is identicated by the + * same listener identifier. + * + * @param obj The object to compare to. + * + * @return true if obj is the same or a proxy for + * the same client-side listener. + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof ServerEventListenerProxy) { + return listenerId == ((ServerEventListenerProxy) obj).listenerId; + } else { + return false; + } + } + + /** + * Returns the a string representation of this instance, which is an + * indication of this class's name and the unique identifier of the real + * event listener. + * + * @return string representation + */ + public String toString() { + return "EventListenerProxy: listenerId=" + listenerId; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/ArrayIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/ArrayIterator.java new file mode 100644 index 00000000000..1c58e43a679 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/ArrayIterator.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.rmi.remote; + +import java.io.Serializable; +import java.util.NoSuchElementException; + +/** + * A simple array-based remote iterator. Used when the iteration is + * short enough for all the elements to be sent over the network in + * one go. + */ +public class ArrayIterator implements RemoteIterator, Serializable { + + /** + * The elements in this iterator. Set to null when + * all elements have been iterated. + */ + private Object[] elements; + + /** + * The position of this iterator. Set to the size of the iterator + * when all elements have been iterated. + */ + private int position; + + /** + * Creates an array-based remote iterator from the given array + * of remote references or serializable objects. + * + * @param elements elements of the iteration + */ + public ArrayIterator(Object[] elements) { + this.elements = elements; + this.position = 0; + } + + /** + * Returns the size of the iterator. + * + * @return length of the iterator + * @see RemoteIterator#getSize() + */ + public long getSize() { + if (elements == null) { + return position; + } else { + return elements.length; + } + } + + /** + * Skips the first items elements in the array. + * {@inheritDoc} + */ + public void skip(long items) + throws IllegalArgumentException, NoSuchElementException { + if (items < 0) { + throw new IllegalArgumentException("Negative skip is not allowed"); + } else if (elements == null || items > elements.length) { + throw new NoSuchElementException("Skipped past the last element"); + } else if (items == elements.length) { + position = elements.length; + elements = null; + } else { + Object[] tmp = new Object[elements.length - (int) items]; + System.arraycopy(elements, (int) items, tmp, 0, tmp.length); + elements = tmp; + position += items; + } + } + + /** + * Returns the underlying array. + * {@inheritDoc} + */ + public Object[] nextObjects() throws IllegalArgumentException { + if (elements == null) { + return null; + } else { + Object[] tmp = elements; + position += elements.length; + elements = null; + return tmp; + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/BufferIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/BufferIterator.java new file mode 100644 index 00000000000..542596eeb71 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/BufferIterator.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.remote; + +import java.io.Serializable; +import java.rmi.RemoteException; +import java.util.NoSuchElementException; + +/** + * A buffered remote iterator. Used to transfer a remote iterator reference + * along with a buffer of the first few iterator elements in one network + * transmission. + */ +public class BufferIterator implements RemoteIterator, Serializable { + + /** The element buffer. Set to null when the iterator ends. */ + private Object[] buffer; + + /** Cached size of the iterator. */ + private final long size; + + /** Remote iterator reference. */ + private final RemoteIterator remote; + + /** + * Creates a new buffered remote iterator. + * + * @param buffer first elements in the iterator + * @param size total iterator size + * @param remote reference to the remaining iterator + */ + public BufferIterator(Object[] buffer, long size, RemoteIterator remote) { + this.buffer = buffer; + this.size = size; + this.remote = remote; + } + + /** + * Returns the cached size of the iterator. + * + * @return iterator size, or -1 if unknown + * @see RemoteIterator#getSize() + */ + public long getSize() { + return size; + } + + /** + * Skips the given number of elements. First discards elements from the + * element buffer and only then contacts the remote iterator. + * + * @param items number of items to skip + * @throws IllegalArgumentException if items is negative + * @throws NoSuchElementException if skipped past the last element + * @throws RemoteException on RMI errors + * @see RemoteIterator#skip(long) + */ + public void skip(long items) + throws IllegalArgumentException, NoSuchElementException, + RemoteException { + if (items < 0) { + throw new IllegalArgumentException("Negative skip is not allowed"); + } else if (buffer == null && items > 0) { + throw new NoSuchElementException("Skipped past the last element"); + } else if (items > buffer.length) { + remote.skip(items - buffer.length); + buffer = remote.nextObjects(); + } else { + Object[] tmp = new Object[buffer.length - (int) items]; + System.arraycopy(buffer, (int) items, tmp, 0, tmp.length); + buffer = tmp; + } + } + + /** + * Returns the currently buffered elements and fills in the buffer + * with next elements. + * + * @return buffered elements, or null if the iterator has ended + * @throws RemoteException on RMI errors + * @see RemoteIterator#nextObjects() + */ + public Object[] nextObjects() throws RemoteException { + if (buffer == null) { + return null; + } else { + Object[] tmp = buffer; + buffer = remote.nextObjects(); + return tmp; + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteEventCollection.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteEventCollection.java new file mode 100644 index 00000000000..9c9eca1d8c4 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteEventCollection.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.Map; + +import javax.jcr.RepositoryException; + +/** + * The RemoteEventCollection class serves as a container for + * notifications sent to registered event listeners. Instances of this class are + * created by the server-side event listener proxies and sent to the client-side + * event poller. On the client-side the enclosed list of events is then sent to + * the listener identified by the contained listener identifier. + */ +public interface RemoteEventCollection extends Remote { + + /** + * Returns unique identifier of the client-side listener to which the + * enclosed events should be sent. + * + * @return unique listener identifier + * @throws RemoteException on RMI errors + */ + long getListenerId() throws RemoteException; + + /** + * Returns the list of events to be sent to the client-side listener + * identified by {@link #getListenerId()}. + * + * @return list of events + * @throws RemoteException on RMI errors + */ + RemoteEvent[] getEvents() throws RemoteException; + + /** + * The RemoteEvent class provides an encapsulation of single + * events in an event list sent to a registered listener. + */ + public static interface RemoteEvent extends Remote { + /** + * Remote version of the + * {@link javax.jcr.observation.Event#getType() Event.getType()} method. + * + * @return the type of this event. + * @throws RemoteException on RMI errors + */ + int getType() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.observation.Event#getPath() Event.getPath()} method. + * + * @return the absolute path associated with this event or + * null. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getPath() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.observation.Event#getUserID() Event.getUserID()} method. + * + * @return the user ID. + * @throws RemoteException on RMI errors + */ + String getUserID() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.observation.Event#getIdentifier() Event.getIdentifier()} method. + * + * @return the identifier associated with this event or null. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getIdentifier() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.observation.Event#getInfo() Event.getInfo()} method. + * + * @return A Map containing parameter information for instances + * of a NODE_MOVED event. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + Map getInfo() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.observation.Event#getUserData() Event.getUserData()} method. + * + * @return The user data string. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getUserData() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.observation.Event#getDate() Event.getDate()} method. + * + * @return the date when the change was persisted that caused this event. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + long getDate() throws RepositoryException, RemoteException; + } +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteItem.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteItem.java similarity index 90% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteItem.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteItem.java index f22a67e199c..bbf98380c39 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteItem.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteItem.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -31,11 +31,10 @@ * to a corresponding Item method. The remote object will simply forward * the method call to the underlying Item instance. Argument and return * values, as well as possible exceptions, are copied over the network. - * Compex return values (Items and Nodes) are returned as remote references - * to the corresponding remote interfaces. RMI errors are signalled with + * Complex return values (Items and Nodes) are returned as remote references + * to the corresponding remote interfaces. RMI errors are signaled with * RemoteExceptions. * - * @author Jukka Zitting * @see javax.jcr.Item * @see org.apache.jackrabbit.rmi.client.ClientItem * @see org.apache.jackrabbit.rmi.server.ServerItem diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteItemDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteItemDefinition.java new file mode 100644 index 00000000000..3ab6e0f17d6 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteItemDefinition.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +/** + * Remote version of the JCR {@link javax.jcr.nodetype.ItemDefinition ItemDef} + * interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.ServerItemDefinition ServerItemDefinition} and + * {@link org.apache.jackrabbit.rmi.client.ClientItemDefinition ClientItemDefinition} + * adapter base classes to provide transparent RMI access to remote item + * definitions. + *

    + * The methods in this interface are documented only with a reference + * to a corresponding ItemDef method. The remote object will simply forward + * the method call to the underlying ItemDef instance. Argument and return + * values, as well as possible exceptions, are copied over the network. + * Complex {@link javax.jcr.nodetype.NodeType NodeType} return values + * are returned as remote references to the + * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} + * interface. RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.nodetype.ItemDefinition + * @see org.apache.jackrabbit.rmi.client.ClientItemDefinition + * @see org.apache.jackrabbit.rmi.server.ServerItemDefinition + */ +public interface RemoteItemDefinition extends Remote { + + /** + * Remote version of the + * {@link javax.jcr.nodetype.ItemDefinition#getDeclaringNodeType() ItemDef.getDeclaringNodeType()} + * method. + * + * @return declaring node type + * @throws RemoteException on RMI errors + */ + RemoteNodeType getDeclaringNodeType() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.ItemDefinition#getName() ItemDef.getName()} method. + * + * @return item name + * @throws RemoteException on RMI errors + */ + String getName() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.ItemDefinition#isAutoCreated() ItemDef.isAutoCreate()} + * method. + * + * @return true if the item is automatically created, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isAutoCreated() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.ItemDefinition#isMandatory() ItemDef.isMandatory()} + * method. + * + * @return true if the item is mandatory, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isMandatory() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.ItemDefinition#getOnParentVersion() ItemDef.getOnParentVersion()} + * method. + * + * @return parent version behaviour + * @throws RemoteException on RMI errors + */ + int getOnParentVersion() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.ItemDefinition#isProtected() ItemDef.isProtected()} + * method. + * + * @return true if the item is protected, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isProtected() throws RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteIterator.java new file mode 100644 index 00000000000..fa6b69f4d1d --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteIterator.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.util.NoSuchElementException; + +/** + * Remote version of the JCR {@link javax.jcr.RangeIterator} interface. + * Used by the {@link org.apache.jackrabbit.rmi.server.iterator.ServerIterator} and + * {@link org.apache.jackrabbit.rmi.client.iterator.ClientIterator} classes to + * provide transparent RMI access to remote iterators. + *

    + * This interface allows both the client and server side to control the + * amount of buffering used to increase performance. + */ +public interface RemoteIterator extends Remote { + + /** + * Returns the size of the iteration, or -1 if the + * size is unknown. + * + * @return size of the iteration, or -1 if unknown + * @throws RemoteException on RMI errors + * @see javax.jcr.RangeIterator#getSize() + */ + long getSize() throws RemoteException; + + /** + * Skips the given number of elements in this iteration. + * + * @param items number of elements to skip + * @throws NoSuchElementException if skipped past the last element + * @throws RemoteException on RMI errors + * @see javax.jcr.RangeIterator#skip(long) + */ + void skip(long items) throws NoSuchElementException, RemoteException; + + /** + * Returns an array of remote references to the next elements in this + * iterator. Returns null if the end of this iteration has + * been reached. + *

    + * To reduce the amount of remote method calls, this method returns an + * array of one or more elements in this iteration. + * + * @return array of remote references, or null + * @throws IllegalArgumentException if maxItems is not positive + * @throws RemoteException on RMI errors + * @see java.util.Iterator#next() + */ + Object[] nextObjects() throws IllegalArgumentException, RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteLock.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteLock.java new file mode 100644 index 00000000000..014c69cafd7 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteLock.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +/** + * Remote version of the JCR {@link javax.jcr.lock.Lock} interface. + * Used by the {@link org.apache.jackrabbit.rmi.server.ServerLock ServerLock} + * and {@link org.apache.jackrabbit.rmi.client.ClientLock ClientLock} + * adapter classes to provide transparent RMI access to remote locks. + *

    + * The methods in this interface are documented only with a reference + * to a corresponding Lock method. The remote object will simply forward + * the method call to the underlying Lock instance. Return values and + * possible exceptions are copied over the network. RMI errors are signaled + * with RemoteExceptions. + * + * @see javax.jcr.lock.Lock + * @see org.apache.jackrabbit.rmi.client.ClientLock + * @see org.apache.jackrabbit.rmi.server.ServerLock + */ +public interface RemoteLock extends Remote { + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#getNode() Lock.getNode()} method. + * + * @return remote node + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + * @since JCR-RMI 2.0 + */ + RemoteNode getNode() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#getLockOwner() Lock.getLockOwner()} method. + * + * @return lock owner + * @throws RemoteException on RMI errors + */ + String getLockOwner() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#isDeep() Lock.isDeep()} method. + * + * @return true if the lock is deep, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isDeep() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#getLockToken() Lock.getLockToken()} method. + * + * @return lock token + * @throws RemoteException on RMI errors + */ + String getLockToken() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#isLive() Lock.isLive()} method. + * + * @return true if the lock is live, + * false otherwise + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + boolean isLive() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#refresh() Lock.refresh()} method. + * + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void refresh() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#isSessionScoped()} () Lock.isSessionScoped()} method. + * + * @return true if the lock is live, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isSessionScoped() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#getSecondsRemaining()} () Lock.getSecondsRemaining()} method. + * + * @return the number of seconds remaining until this lock times out. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + long getSecondsRemaining() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.lock.Lock#isLockOwningSession()} () Lock.isLockOwningSession()} method. + * + * @return a boolean. + * @throws RemoteException on RMI errors + */ + boolean isLockOwningSession() throws RemoteException; + + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteLockManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteLockManager.java new file mode 100644 index 00000000000..caf3f387cba --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteLockManager.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +public interface RemoteLockManager extends Remote { + + String[] getLockTokens() throws RepositoryException, RemoteException; + + void addLockToken(String lockToken) + throws RepositoryException, RemoteException; + + void removeLockToken(String lockToken) + throws RepositoryException, RemoteException; + + RemoteLock getLock(String absPath) + throws RepositoryException, RemoteException; + + boolean holdsLock(String absPath) + throws RepositoryException, RemoteException; + + boolean isLocked(String absPath) + throws RepositoryException, RemoteException; + + RemoteLock lock( + String absPath, boolean isDeep, boolean isSessionScoped, + long timeoutHint, String ownerInfo) + throws RepositoryException, RemoteException; + + void unlock(String absPath) throws RepositoryException, RemoteException; + +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNamespaceRegistry.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNamespaceRegistry.java similarity index 88% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNamespaceRegistry.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNamespaceRegistry.java index a62765e662d..b442f2033ed 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNamespaceRegistry.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNamespaceRegistry.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -34,9 +34,8 @@ * to a corresponding NamespaceRegistry method. The remote object will * simply forward the method call to the underlying NamespaceRegistry instance. * Argument and return values, as well as possible exceptions, are copied - * over the network. RMI errors are signalled with RemoteExceptions. + * over the network. RMI errors are signaled with RemoteExceptions. * - * @author Jukka Zitting * @see javax.jcr.NamespaceRegistry * @see org.apache.jackrabbit.rmi.client.ClientNamespaceRegistry * @see org.apache.jackrabbit.rmi.server.ServerNamespaceRegistry diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNode.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNode.java similarity index 76% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNode.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNode.java index 9badf231884..e95a0473d7e 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNode.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNode.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,7 +21,6 @@ import javax.jcr.RepositoryException; import javax.jcr.Value; - /** * Remote version of the JCR {@link javax.jcr.Node Node} interface. * Used by the {@link org.apache.jackrabbit.rmi.server.ServerNode ServerNode} @@ -32,9 +31,9 @@ * to a corresponding Node method. The remote object will simply forward * the method call to the underlying Node instance. Argument and return * values, as well as possible exceptions, are copied over the network. - * Compex return values (like Nodes and Properties) are returned as remote + * Complex return values (like Nodes and Properties) are returned as remote * references to the corresponding remote interfaces. Iterator values - * are transmitted as object arrays. RMI errors are signalled with + * are transmitted as object arrays. RMI errors are signaled with * RemoteExceptions. *

    * Note that only two generic setProperty methods are included in this @@ -43,11 +42,9 @@ * and calling the generic setProperty methods. Note also that the * Value objects must be serializable and implemented using classes * available on both the client and server side. The - * {@link org.apache.jackrabbit.rmi.remote.SerialValue SerialValue} - * decorator utility provides a convenient way to satisfy these - * requirements. + * {@link org.apache.jackrabbit.rmi.value.SerialValueFactory SerialValueFactory} + * class provides two convenience methods to satisfy these requirements. * - * @author Jukka Zitting * @see javax.jcr.Node * @see org.apache.jackrabbit.rmi.client.ClientNode * @see org.apache.jackrabbit.rmi.server.ServerNode @@ -100,8 +97,7 @@ RemoteProperty getProperty(String path) * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteProperty[] getProperties() - throws RepositoryException, RemoteException; + RemoteIterator getProperties() throws RepositoryException, RemoteException; /** * Remote version of the @@ -113,7 +109,20 @@ RemoteProperty[] getProperties() * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteProperty[] getProperties(String pattern) + RemoteIterator getProperties(String pattern) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#getProperties(String[]) Node.getProperties(String[])} + * method. + * + * @param globs property name globs + * @return matching node properties + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteIterator getProperties(String[] globs) throws RepositoryException, RemoteException; /** @@ -126,6 +135,16 @@ RemoteProperty[] getProperties(String pattern) */ RemoteItem getPrimaryItem() throws RepositoryException, RemoteException; + /** + * Remote version of the + * {@link javax.jcr.Node#getIdentifier() Node.getIdentifier()} method. + * + * @return node identifier + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getIdentifier() throws RepositoryException, RemoteException; + /** * Remote version of the * {@link javax.jcr.Node#getUUID() Node.getUUID()} method. @@ -144,8 +163,18 @@ RemoteProperty[] getProperties(String pattern) * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteProperty[] getReferences() - throws RepositoryException, RemoteException; + RemoteIterator getReferences() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#getReferences(String) Node.getReferences(String)} method. + * + * @param name reference property name + * @return reference properties + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteIterator getReferences(String name) throws RepositoryException, RemoteException; /** * Remote version of the @@ -155,7 +184,7 @@ RemoteProperty[] getReferences() * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteNode[] getNodes() throws RepositoryException, RemoteException; + RemoteIterator getNodes() throws RepositoryException, RemoteException; /** * Remote version of the @@ -166,7 +195,19 @@ RemoteProperty[] getReferences() * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteNode[] getNodes(String pattern) + RemoteIterator getNodes(String pattern) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#getNodes(String[]) Node.getNodes(String[])} method. + * + * @param globs node name globs + * @return matching child nodes + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteIterator getNodes(String[] globs) throws RepositoryException, RemoteException; /** @@ -290,6 +331,21 @@ void orderBefore(String src, String dst) RemoteProperty setProperty(String name, Value value) throws RepositoryException, RemoteException; + /** + * Remote version of the + * {@link javax.jcr.Node#setProperty(String,Value,int) Node.setProperty(String,Value)} + * method. + * + * @param name property name + * @param value property value + * @param type property type + * @return property + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteProperty setProperty(String name, Value value, int type) + throws RepositoryException, RemoteException; + /** * Remote version of the * {@link javax.jcr.Node#setProperty(String,Value[]) Node.setProperty(String,Value[])} @@ -347,7 +403,7 @@ boolean canAddMixin(String name) * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteNodeDef getDefinition() throws RepositoryException, RemoteException; + RemoteNodeDefinition getDefinition() throws RepositoryException, RemoteException; /** * Remote version of the @@ -385,10 +441,11 @@ boolean canAddMixin(String name) * * @param workspace source workspace name * @param bestEffort best effort flag + * @return nodes that failed to merge * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - void merge(String workspace, boolean bestEffort) + RemoteIterator merge(String workspace, boolean bestEffort) throws RepositoryException, RemoteException; /** @@ -595,4 +652,92 @@ RemoteLock lock(boolean isDeep, boolean isSessionScoped) */ RemoteLock getLock() throws RepositoryException, RemoteException; + /** + * Remote version of the + * {@link javax.jcr.Node#getSharedSet() Node.getSharedSet()} method. + * + * @return a NodeIterator. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteIterator getSharedSet() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#followLifecycleTransition(String) Node.followLifecycleTransition(String)} + * method. + * + * @param transition a state transition + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void followLifecycleTransition(String transition) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#getAllowedLifecycleTransistions() Node.getAllowedLifecycleTransistions()} + * method. + * + * @return a String array. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String[] getAllowedLifecycleTransistions() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#getWeakReferences() Node.getWeakReferences()} + * method. + * + * @return A PropertyIterator. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteIterator getWeakReferences() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#getWeakReferences(String) Node.getWeakReferences(String)} + * method. + * + * @param name name of referring WEAKREFERENCE properties to be + * returned; if null then all referring + * WEAKREFERENCEs are returned. + * @return A PropertyIterator. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteIterator getWeakReferences(String name) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#removeShare() Node.removeShare()} + * method. + * + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void removeShare() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#removeSharedSet() Node.removeSharedSet()} + * method. + * + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void removeSharedSet() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Node#setPrimaryType(String) Node.setPrimaryType(String)} + * method. + * + * @param nodeTypeName the node type name + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void setPrimaryType(String nodeTypeName) throws RepositoryException, RemoteException; + } diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeDefinition.java new file mode 100644 index 00000000000..a4c14c29857 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeDefinition.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.rmi.remote; + +import java.rmi.RemoteException; + +/** + * Remote version of the JCR {@link javax.jcr.nodetype.NodeDefinition NodeDefinition} + * interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.ServerNodeDefinition ServerNodeDefinition} and + * {@link org.apache.jackrabbit.rmi.client.ClientNodeDefinition ClientNodeDefinition} + * adapters to provide transparent RMI access to remote node definitions. + *

    + * The methods in this interface are documented only with a reference + * to a corresponding NodeDef method. The remote object will simply forward + * the method call to the underlying NodeDef instance. Return values + * and possible exceptions are copied over the network. Complex + * {@link javax.jcr.nodetype.NodeType NodeType} return values + * are returned as remote references to the + * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} + * interface. RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.nodetype.NodeDefinition + * @see org.apache.jackrabbit.rmi.client.ClientNodeDefinition + * @see org.apache.jackrabbit.rmi.server.ServerNodeDefinition + */ +public interface RemoteNodeDefinition extends RemoteItemDefinition { + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeDefinition#getRequiredPrimaryTypes() NodeDef.getRequiredPrimaryTypes()} + * method. + * + * @return required primary node types + * @throws RemoteException on RMI errors + */ + RemoteNodeType[] getRequiredPrimaryTypes() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeDefinition#getDefaultPrimaryType() NodeDef.getDefaultPrimaryType()} + * method. + * + * @return default primary node type + * @throws RemoteException on RMI errors + */ + RemoteNodeType getDefaultPrimaryType() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeDefinition#allowsSameNameSiblings() NodeDef.allowSameNameSibs()} + * method. + * + * @return true if same name siblings are allowed, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean allowsSameNameSiblings() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeDefinition#getDefaultPrimaryTypeName() NodeDef.getDefaultPrimaryTypeName()} + * method. + * + * @return a String + * @throws RemoteException on RMI errors + */ + String getDefaultPrimaryTypeName() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeDefinition#getRequiredPrimaryTypeNames() NodeDef.getRequiredPrimaryTypeNames()} + * method. + * + * @return a String array + * @throws RemoteException on RMI errors + */ + String[] getRequiredPrimaryTypeNames() throws RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeType.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeType.java new file mode 100644 index 00000000000..8f4c834ce87 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeType.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.Value; + +/** + * Remote version of the JCR {@link javax.jcr.nodetype.NodeType NodeType} + * interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.ServerNodeType ServerNodeType} and + * {@link org.apache.jackrabbit.rmi.client.ClientNodeType ClientNodeType} + * adapters to provide transparent RMI access to remote node types. + *

    + * The methods in this interface are documented only with a reference + * to a corresponding NodeType method. The remote object will simply forward + * the method call to the underlying NodeType instance. Return values + * and possible exceptions are copied over the network. Complex return + * values (like NodeTypes and PropertyDefs) are returned as remote + * references to the corresponding remote interfaces. RMI errors are + * signaled with RemoteExceptions. + * + * @see javax.jcr.nodetype.NodeType + * @see org.apache.jackrabbit.rmi.client.ClientNodeType + * @see org.apache.jackrabbit.rmi.server.ServerNodeType + */ +public interface RemoteNodeType extends Remote { + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getName() NodeType.getName()} method. + * + * @return node type name + * @throws RemoteException on RMI errors + */ + String getName() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#isMixin() NodeType.isMixin()} method. + * + * @return true if this is a mixin type, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isMixin() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#isAbstract() NodeType.isAbstract()} method. + * + * @return true if this is an abstract type, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isAbstract() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#hasOrderableChildNodes() NodeType.hasOrderableChildNodes()} + * method. + * + * @return true if nodes of this type has orderable + * child nodes, false otherwise + * @throws RemoteException on RMI errors + */ + boolean hasOrderableChildNodes() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getSupertypes() NodeType.getSupertypes()} + * method. + * + * @return supertypes + * @throws RemoteException on RMI errors + */ + RemoteNodeType[] getSupertypes() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getDeclaredSupertypes() NodeType.getDeclaredSupertypes()} + * method. + * + * @return declared supertypes + * @throws RemoteException on RMI errors + */ + RemoteNodeType[] getDeclaredSupertypes() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#isNodeType(String) NodeType.isNodeType(String)} + * method. + * + * @param type node type name + * @return true if this node type is or extends the + * given node type, false otherwise + * @throws RemoteException on RMI errors + */ + boolean isNodeType(String type) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getPropertyDefinitions() NodeType.getPropertyDefinitions()} + * method. + * + * @return property definitions + * @throws RemoteException on RMI errors + */ + RemotePropertyDefinition[] getPropertyDefs() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getDeclaredPropertyDefinitions() NodeType.getDeclaredPropertyDefinitions()} + * method. + * + * @return declared property definitions + * @throws RemoteException on RMI errors + */ + RemotePropertyDefinition[] getDeclaredPropertyDefs() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getChildNodeDefinitions() NodeType.getChildNodeDefinitions()} + * method. + * + * @return child node definitions + * @throws RemoteException on RMI errors + */ + RemoteNodeDefinition[] getChildNodeDefs() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getDeclaredChildNodeDefinitions() NodeType.getDeclaredChildNodeDefinitions()} + * method. + * + * @return declared child node definitions + * @throws RemoteException on RMI errors + */ + RemoteNodeDefinition[] getDeclaredChildNodeDefs() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#canSetProperty(String,Value) NodeType.canSetProperty(String,Value)} + * method. + * + * @param name property name + * @param value property value + * @return true if the property can be set, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean canSetProperty(String name, Value value) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#canSetProperty(String,Value[]) NodeType.canSetProperty(String,Value[])} + * method. + * + * @param name property name + * @param values property values + * @return true if the property can be set, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean canSetProperty(String name, Value[] values) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#canAddChildNode(String) NodeType.canAddChildNode(String)} + * method. + * + * @param name child node name + * @return true if the child node can be added, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean canAddChildNode(String name) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#canAddChildNode(String,String) NodeType.canAddChildNode(String,String)} + * method. + * + * @param name child node name + * @param type child node type + * @return true if the child node can be added, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean canAddChildNode(String name, String type) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#canRemoveItem(String) NodeType.canRemoveItem(String)} + * method. + * + * @param name item name + * @return true if the item can be removed, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean canRemoveItem(String name) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getPrimaryItemName() NodeType.getPrimaryItemName()} + * method. + * + * @return primary item name + * @throws RemoteException on RMI errors + */ + String getPrimaryItemName() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#canRemoveNode(String) NodeType.canRemoveNode()} + * method. + * + * @return boolean + * @throws RemoteException on RMI errors + */ + boolean canRemoveNode(String nodeName) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#canRemoveProperty(String) NodeType.canRemoveProperty()} + * method. + * + * @return boolean + * @throws RemoteException on RMI errors + */ + boolean canRemoveProperty(String propertyName) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getDeclaredSupertypeNames() NodeType.getDeclaredSupertypeNames()} + * method. + * + * @return a String[] + * @throws RemoteException on RMI errors + */ + String[] getDeclaredSupertypeNames() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#isQueryable() NodeType.isQueryable()} + * method. + * + * @return boolean + * @throws RemoteException on RMI errors + */ + boolean isQueryable() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getDeclaredSubtypes() NodeType.getDeclaredSubtypes()} + * method. + * + * @return RemoteIterator + * @throws RemoteException on RMI errors + */ + RemoteIterator getDeclaredSubtypes() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.NodeType#getSubtypes() NodeType.getSubtypes()} + * method. + * + * @return RemoteIterator + * @throws RemoteException on RMI errors + */ + RemoteIterator getSubtypes() throws RemoteException; + +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeTypeManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeTypeManager.java similarity index 80% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeTypeManager.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeTypeManager.java index 81b04729b13..6f21d595143 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteNodeTypeManager.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteNodeTypeManager.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -38,9 +38,8 @@ * remote references to the * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} * interface. Iterator values are transmitted as object arrays. RMI errors - * are signalled with RemoteExceptions. + * are signaled with RemoteExceptions. * - * @author Jukka Zitting * @see javax.jcr.nodetype.NodeTypeManager * @see org.apache.jackrabbit.rmi.client.ClientNodeTypeManager * @see org.apache.jackrabbit.rmi.server.ServerNodeTypeManager @@ -69,7 +68,7 @@ RemoteNodeType getNodeType(String name) * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteNodeType[] getAllNodeTypes() + RemoteIterator getAllNodeTypes() throws RepositoryException, RemoteException; /** @@ -81,7 +80,7 @@ RemoteNodeType[] getAllNodeTypes() * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteNodeType[] getPrimaryNodeTypes() + RemoteIterator getPrimaryNodeTypes() throws RepositoryException, RemoteException; /** @@ -93,7 +92,13 @@ RemoteNodeType[] getPrimaryNodeTypes() * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemoteNodeType[] getMixinNodeTypes() + RemoteIterator getMixinNodeTypes() + throws RepositoryException, RemoteException; + + boolean hasNodeType(String name) + throws RepositoryException, RemoteException; + + void unregisterNodeTypes(String[] names) throws RepositoryException, RemoteException; } diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteObservationManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteObservationManager.java new file mode 100644 index 00000000000..fcd36000053 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteObservationManager.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +/** + * Remote version of the JCR {@link javax.jcr.observation.ObservationManager ObservationManager} + * interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.ServerObservationManager ServerObservationManager} + * and + * {@link org.apache.jackrabbit.rmi.client.ClientObservationManager ClientObservationManager} + * adapter base classes to provide transparent RMI access to remote observation + * managers. + *

    + * See the observation + * package comment for a description on how event listener registration and + * notification is implemented. + * + * @see javax.jcr.observation.ObservationManager + * @see org.apache.jackrabbit.rmi.client.ClientObservationManager + * @see org.apache.jackrabbit.rmi.server.ServerObservationManager + */ +public interface RemoteObservationManager extends Remote { + + /** + * Remote version of the + * {@link javax.jcr.observation.ObservationManager#addEventListener(javax.jcr.observation.EventListener, int, String, boolean, String[], String[], boolean) ObservationManager.addEventListener()} + * method. See class comment for an explanation on how the + * listenerId is used. + * + * @param listenerId The identification of the listener on the client + * side to which events will be directed. + * @param eventTypes The mask of event types to be sent to this listener. + * @param absPath The root item defining a subtree for which events are to + * be delivered. + * @param isDeep true if the events from the complete subtree + * rooted at absPath are to be sent or only for the item + * at the given path. + * @param uuid An optional list of node UUIDs for which events are to be + * sent. If null this parameter is ignored. + * @param nodeTypeName An optional list of node type names for which events + * are to be sent. If null this parameter is ignored. + * @param noLocal true if only events are to be sent which do + * not originate from the session to which this instance belongs. + * + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void addEventListener(long listenerId, int eventTypes, + String absPath, boolean isDeep, String[] uuid, + String[] nodeTypeName, boolean noLocal) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.observation.ObservationManager#removeEventListener(javax.jcr.observation.EventListener) ObservationManager.removeEventListener()} + * method. See class comment for an explanation on how the + * listenerId is used. + * + * @param listenerId The identification of the listener on the client + * side to which events will be directed. + * + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void removeEventListener(long listenerId) + throws RepositoryException, RemoteException; + + /** + * Returns the next event to be dispatched to registered event listeners. If + * no event is available, this method blocks until one is available or until + * the given timeout expires. + * + * @param timeout The time in milliseconds to wait for the next event + * available to be dispatched. If negative or zero, this method waits + * for ever. + * + * @return The {@link RemoteEventCollection} to be dispatched. null is + * returned if the method timed out waiting for an event to be + * dispatched. + * + * @throws RemoteException on RMI errors + */ + RemoteEventCollection getNextEvent(long timeout) + throws RemoteException; +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteProperty.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteProperty.java similarity index 82% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteProperty.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteProperty.java index 7f6725ec77f..b760627b8a5 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteProperty.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteProperty.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -32,21 +32,19 @@ * to a corresponding Property method. The remote object will simply forward * the method call to the underlying Property instance. Argument and return * values, as well as possible exceptions, are copied over the network. - * Compex {@link javax.jcr.nodetype.PropertyDef PropertyDef} return values + * Complex {@link javax.jcr.nodetype.PropertyDefinition PropertyDef} return values * are returned as remote references to the corresponding - * {@link org.apache.jackrabbit.rmi.remote.RemotePropertyDef RemotePropertyDef} - * interface. RMI errors are signalled with RemoteExceptions. + * {@link org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition RemotePropertyDefinition} + * interface. RMI errors are signaled with RemoteExceptions. *

    * Note that only the generic getValue and setValue methods are included * in this interface. Clients should implement the type-specific value - * getters and setters wrapping using the generic methods. Note also that - * the Value objects must be serializable and implemented using classes + * getters and setters wrapping using the generic methods. Note also that the + * Value objects must be serializable and implemented using classes * available on both the client and server side. The - * {@link org.apache.jackrabbit.rmi.remote.SerialValue SerialValue} - * decorator utility provides a convenient way to satisfy these - * requirements. + * {@link org.apache.jackrabbit.rmi.value.SerialValueFactory SerialValueFactory} + * class provides two convenience methods to satisfy these requirements. * - * @author Jukka Zitting * @see javax.jcr.Property * @see org.apache.jackrabbit.rmi.client.ClientProperty * @see org.apache.jackrabbit.rmi.server.ServerProperty @@ -126,7 +124,7 @@ public interface RemoteProperty extends RemoteItem { * @throws RepositoryException on repository errors * @throws RemoteException on RMI errors */ - RemotePropertyDef getDefinition() + RemotePropertyDefinition getDefinition() throws RepositoryException, RemoteException; /** diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemotePropertyDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemotePropertyDefinition.java new file mode 100644 index 00000000000..3219b156953 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemotePropertyDefinition.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.rmi.remote; + +import java.rmi.RemoteException; + +import javax.jcr.Value; + +/** + * Remote version of the JCR {@link javax.jcr.nodetype.PropertyDefinition PropertyDefinition} + * interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.ServerPropertyDefinition ServerPropertyDefinition} + * and + * {@link org.apache.jackrabbit.rmi.client.ClientPropertyDefinition ClientPropertyDefinition} + * adapters to provide transparent RMI access to remote property definitions. + *

    + * The methods in this interface are documented only with a reference + * to a corresponding PropertyDef method. The remote object will simply + * forward the method call to the underlying PropertyDef instance. Return + * values and possible exceptions are copied over the network. RMI errors + * are signaled with RemoteExceptions. + *

    + * Note that the returned Value objects must be serializable and implemented + * using classes available on both the client and server side. The + * {@link org.apache.jackrabbit.rmi.value.SerialValueFactory SerialValueFactory} + * class provides two convenience methods to satisfy this requirement. + * + * @see javax.jcr.nodetype.PropertyDefinition + * @see org.apache.jackrabbit.rmi.client.ClientPropertyDefinition + * @see org.apache.jackrabbit.rmi.server.ServerPropertyDefinition + */ +public interface RemotePropertyDefinition extends RemoteItemDefinition { + + /** + * Remote version of the + * {@link javax.jcr.nodetype.PropertyDefinition#getRequiredType() PropertyDefinition.getRequiredType()} + * method. + * + * @return required type + * @throws RemoteException on RMI errors + */ + int getRequiredType() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.PropertyDefinition#getValueConstraints() PropertyDefinition.getValueConstraints()} + * method. + * + * @return value constraints + * @throws RemoteException on RMI errors + */ + String[] getValueConstraints() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.PropertyDefinition#getDefaultValues() PropertyDefinition.getDefaultValues()} + * method. + * + * @return default values + * @throws RemoteException on RMI errors + */ + Value[] getDefaultValues() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.PropertyDefinition#isMultiple() PropertyDefinition.isMultiple()} + * method. + * + * @return true if the property is multi-valued, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isMultiple() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.PropertyDefinition#getAvailableQueryOperators() PropertyDefinition.getAvailableQueryOperators()} + * method. + * + * @return a String[] + * @throws RemoteException on RMI errors + */ + String[] getAvailableQueryOperators() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.PropertyDefinition#isFullTextSearchable() PropertyDefinition.isFullTextSearchable()} + * method. + * + * @return a boolean + * @throws RemoteException on RMI errors + */ + boolean isFullTextSearchable() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.nodetype.PropertyDefinition#isQueryOrderable() PropertyDefinition.isQueryOrderable()} + * method. + * + * @return a boolean + * @throws RemoteException on RMI errors + */ + boolean isQueryOrderable() throws RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQuery.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQuery.java new file mode 100644 index 00000000000..0a9c37c6c0d --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQuery.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * Remote version of the JCR {@link javax.jcr.query.Query Query} interface. + * Used by the {@link org.apache.jackrabbit.rmi.server.ServerQuery ServerQuery} + * and {@link org.apache.jackrabbit.rmi.client.ClientQuery ClientQuery} + * adapter base classes to provide transparent RMI access to remote items. + *

    + * RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.query.Query + * @see org.apache.jackrabbit.rmi.client.ClientQuery + * @see org.apache.jackrabbit.rmi.server.ServerQuery + */ +public interface RemoteQuery extends Remote { + + /** + * @see javax.jcr.query.Query#execute() + * + * @return a QueryResult + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteQueryResult execute() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Query#setLimit(long) + * + * @param limit a long + * @throws RemoteException on RMI errors + */ + void setLimit(long limit) throws RemoteException; + + /** + * @see javax.jcr.query.Query#setOffset(long) + * + * @param offset a long + * @throws RemoteException on RMI errors + */ + void setOffset(long offset) throws RemoteException; + + /** + * @see javax.jcr.query.Query#getStatement() + * + * @return the query statement. + * @throws RemoteException on RMI errors + */ + String getStatement() throws RemoteException; + + /** + * @see javax.jcr.query.Query#getLanguage() + * + * @return the query language. + * @throws RemoteException on RMI errors + */ + String getLanguage() throws RemoteException; + + /** + * @see javax.jcr.query.Query#getStoredQueryPath() + * + * @return path of the node representing this query. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getStoredQueryPath() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Query#storeAsNode(String) + * + * @param absPath path at which to persist this query. + * @return stored node + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteNode storeAsNode(String absPath) throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Query#bindValue(String, Value) + * + * @param varName name of variable in query + * @param value value to bind + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void bindValue(String varName, Value value) throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Query#getBindVariableNames() + * + * @return the names of the bind variables in this query. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + public String[] getBindVariableNames() throws RepositoryException, RemoteException; +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQueryManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQueryManager.java similarity index 76% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQueryManager.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQueryManager.java index 50c420345ec..da672ec3ed7 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteQueryManager.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQueryManager.java @@ -1,71 +1,73 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.rmi.remote; - -import java.rmi.Remote; -import java.rmi.RemoteException; - -import javax.jcr.RepositoryException; - -/** - * Remote version of the JCR {@link javax.jcr.query.QueryManager QueryManager} interface. - * Used by the {@link org.apache.jackrabbit.rmi.server.ServerQueryManager ServerQueryManager} - * and {@link org.apache.jackrabbit.rmi.client.ClientQueryManager ClientQueryManager} - * adapter base classes to provide transparent RMI access to remote items. - *

    - * RMI errors are signalled with RemoteExceptions. - * - * @author Philipp Koch - * @see javax.jcr.query.QueryManager - * @see org.apache.jackrabbit.rmi.client.ClientQueryManager - * @see org.apache.jackrabbit.rmi.server.ServerQueryManager - */ -public interface RemoteQueryManager extends Remote { - - /** - * @see javax.jcr.query.QueryManager#createQuery - * - * @param statement query statement - * @param language query language - * @return query - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteQuery createQuery(String statement, String language) - throws RepositoryException, RemoteException; - - /** - * @see javax.jcr.query.QueryManager#getQuery - * - * @param absPath node path of a persisted query (that is, a node of type nt:query). - * @return a Query object. - * @throws RepositoryException on repository errors - * @throws RemoteException on RMI errors - */ - RemoteQuery getQuery(String absPath) - throws RepositoryException, RemoteException; - - /** - * @see javax.jcr.query.QueryManager#getSupportedQueryLanguages - * - * See {@link Query}. - * @return An string array. - * @throws RemoteException on RMI errors - */ - String[] getSupportedQueryLanguages() throws RemoteException; - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; + +/** + * Remote version of the JCR {@link javax.jcr.query.QueryManager QueryManager} interface. + * Used by the {@link org.apache.jackrabbit.rmi.server.ServerQueryManager ServerQueryManager} + * and {@link org.apache.jackrabbit.rmi.client.ClientQueryManager ClientQueryManager} + * adapter base classes to provide transparent RMI access to remote items. + *

    + * RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.query.QueryManager + * @see org.apache.jackrabbit.rmi.client.ClientQueryManager + * @see org.apache.jackrabbit.rmi.server.ServerQueryManager + */ +public interface RemoteQueryManager extends Remote { + + /** + * @see javax.jcr.query.QueryManager#createQuery + * + * @param statement query statement + * @param language query language + * @return query + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteQuery createQuery(String statement, String language) + throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.QueryManager#getQuery + * + * @param absPath node path of a persisted query (that is, a node of type nt:query). + * @return a Query object. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteQuery getQuery(String absPath) + throws RepositoryException, RemoteException; + + /** + * See {@link Query}. + * + * @see javax.jcr.query.QueryManager#getSupportedQueryLanguages + * @return An string array. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String[] getSupportedQueryLanguages() + throws RepositoryException, RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQueryResult.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQueryResult.java new file mode 100644 index 00000000000..5fe8691c5e9 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteQueryResult.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +/** + * Remote version of the JCR {@link javax.jcr.query.QueryResult QueryResult} interface. + * Used by the {@link org.apache.jackrabbit.rmi.server.ServerQueryResult ServerQueryResult} + * and {@link org.apache.jackrabbit.rmi.client.ClientQueryResult ClientQueryResult} + * adapter base classes to provide transparent RMI access to remote items. + *

    + * RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.query.QueryResult + * @see org.apache.jackrabbit.rmi.client.ClientQueryResult + * @see org.apache.jackrabbit.rmi.server.ServerQueryResult + */ +public interface RemoteQueryResult extends Remote { + /** + * @see javax.jcr.query.QueryResult#getColumnNames() + * + * @return a PropertyIterator + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String[] getColumnNames() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.QueryResult#getRows() + * + * @return a RowIterator + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteIterator getRows() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.QueryResult#getNodes() + * + * @return a remote node iterator + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteIterator getNodes() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.QueryResult#getSelectorNames() + * + * @return a String array holding the selector names. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + public String[] getSelectorNames() throws RepositoryException, RemoteException; +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteRepository.java new file mode 100644 index 00000000000..93b5ac6be20 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteRepository.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * Remote version of the JCR {@link javax.jcr.Repository Repository} interface. + * Used by the + * {@link org.apache.jackrabbit.rmi.server.ServerRepository ServerRepository} + * and + * {@link org.apache.jackrabbit.rmi.client.ClientRepository ClientRepository} + * adapters to provide transparent RMI access to remote repositories. +*

    + * The methods in this interface are documented only with a reference + * to a corresponding Repository method. The remote object will simply + * forward the method call to the underlying Repository instance. + * {@link javax.jcr.Session Session} objects are returned as remote references + * to the {@link RemoteSession RemoteSession} interface. Simple return + * values and possible exceptions are copied over the network to the client. + * RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.Repository + * @see org.apache.jackrabbit.rmi.client.ClientRepository + * @see org.apache.jackrabbit.rmi.server.ServerRepository + */ +public interface RemoteRepository extends Remote { + + /** + * Remote version of the + * {@link javax.jcr.Repository#getDescriptor(String) Repository.getDescriptor(String)} + * method. + * + * @param key descriptor key + * @return descriptor value + * @throws RemoteException on RMI errors + */ + String getDescriptor(String key) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#getDescriptorKeys() Repository.getDescriptorKeys()} + * method. + * + * @return descriptor keys + * @throws RemoteException on RMI errors + */ + String[] getDescriptorKeys() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#login() Repository.login(}} method. + * + * @return remote session + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteSession login() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#login(String) Repository.login(String}} + * method. + * + * @param workspace workspace name + * @return remote session + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteSession login(String workspace) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#login(Credentials) Repository.login(Credentials}} + * method. + * + * @param credentials client credentials + * @return remote session + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteSession login(Credentials credentials) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#login(Credentials,String) Repository.login(Credentials,String}} + * method. + * + * @param credentials client credentials + * @param workspace workspace name + * @return remote session + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteSession login(Credentials credentials, String workspace) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#getDescriptorValue(String) Repository.getDescriptorValue(String)} + * method. + * + * @return descriptor value + * @throws RemoteException on RMI errors + */ + Value getDescriptorValue(String key) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#getDescriptorValues(String) Repository.getDescriptorValues(String)} + * method. + * + * @return descriptor value array + * @throws RemoteException on RMI errors + */ + Value[] getDescriptorValues(String key) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#isSingleValueDescriptor(String) Repository.isSingleValueDescriptor(String)} + * method. + * + * @return boolean + * @throws RemoteException on RMI errors + */ + boolean isSingleValueDescriptor(String key) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Repository#isStandardDescriptor(String) Repository.isStandardDescriptor(String)} + * method. + * + * @return boolean + * @throws RemoteException on RMI errors + */ + boolean isStandardDescriptor(String key) throws RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteRow.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteRow.java new file mode 100644 index 00000000000..0aa8365d126 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteRow.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * Remote version of the JCR {@link javax.jcr.query.Row Row} interface. + * Used by the {@link org.apache.jackrabbit.rmi.server.ServerRow ServerRow} + * and {@link org.apache.jackrabbit.rmi.client.ClientRow ClientRow} + * adapter base classes to provide transparent RMI access to remote items. + *

    + * RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.query.Row + * @see org.apache.jackrabbit.rmi.client.ClientRow + * @see org.apache.jackrabbit.rmi.server.ServerRow + */ +public interface RemoteRow extends Remote { + + /** + * @see javax.jcr.query.Row#getValues() + * + * @return row values + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + Value[] getValues() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Row#getValue(String) + * + * @param propertyName property name + * @return identified value + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + Value getValue(String propertyName) + throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Row#getNode() + * + * @return a node + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteNode getNode() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Row#getNode(String) + * + * @param selectorName + * @return a node + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteNode getNode(String selectorName) throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Row#getPath() + * + * @return the path + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getPath() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Row#getPath(String) + * + * @param selectorName + * @return the path + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getPath(String selectorName) throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Row#getScore() + * + * @return the score + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + double getScore() throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.query.Row#getScore(String) + * + * @param selectorName + * @return the score + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + double getScore(String selectorName) throws RepositoryException, RemoteException; +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteSession.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteSession.java new file mode 100644 index 00000000000..7edce7929ab --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteSession.java @@ -0,0 +1,462 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.remote; + +import java.io.IOException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager; + +/** + * Remote version of the JCR {@link javax.jcr.Session Session} interface. + * Used by the + * {@link org.apache.jackrabbit.rmi.server.ServerSession ServerSession} + * and + * {@link org.apache.jackrabbit.rmi.client.ClientSession ClientSession} + * adapters to provide transparent RMI access to remote sessions. + *

    + * Most of the methods in this interface are documented only with a reference + * to a corresponding Session method. In these cases the remote object + * will simply forward the method call to the underlying Session instance. + * Complex return values like workspaces and other objects are returned + * as remote references to the corresponding remote interface. Simple + * return values and possible exceptions are simply copied over the network + * to the client. RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.Session + * @see org.apache.jackrabbit.rmi.client.ClientSession + * @see org.apache.jackrabbit.rmi.server.ServerSession + */ +public interface RemoteSession extends Remote { + + /** + * Remote version of the + * {@link javax.jcr.Session#getUserID() Session.getUserID()} method. + * + * @return user id + * @throws RemoteException on RMI errors + * @see javax.jcr.Session#getUserID() + */ + String getUserID() throws RemoteException; + + /** + * Returns the named attribute. Note that only serializable + * attribute values can be transmitted over the network and that + * the client should have (or be able to fetch) the object class + * to access the returned value. Failures to meet these conditions + * are signalled with RemoteExceptions. + * + * @param name attribute name + * @return attribute value + * @throws RemoteException on RMI errors + * @see javax.jcr.Session#getAttribute(java.lang.String) + */ + Object getAttribute(String name) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getAttributeNames() Session.getAttributeNames()} + * method. + * + * @return attribute names + * @throws RemoteException on RMI errors + */ + String[] getAttributeNames() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getWorkspace() Session.getWorkspace()} method. + * + * @return workspace + * @see javax.jcr.Session#getWorkspace() + * @throws RemoteException on RMI errors + */ + RemoteWorkspace getWorkspace() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#impersonate(Credentials) Session.impersonate(Credentials)} + * method. + * + * @param credentials credentials for the new session + * @return new session + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteSession impersonate(Credentials credentials) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getNodeByIdentifier(String) Session.getNodeByIdentifier(String)} + * method. + * + * @param id node identifier + * @return node + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteNode getNodeByIdentifier(String id) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getNodeByUUID(String) Session.getNodeByUUID(String)} + * method. + * + * @param uuid node uuid + * @return node + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteNode getNodeByUUID(String uuid) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getItem(String) Session.getItem(String)} + * method. + * + * @param path item path + * @return item + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteItem getItem(String path) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getNode(String) Session.getNode(String)} + * method. + * + * @param path node path + * @return node + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteNode getNode(String path) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getProperty(String) Session.getProperty(String)} + * method. + * + * @param path property path + * @return property + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteProperty getProperty(String path) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#itemExists(String) Session.itemExists(String)} + * method. + * + * @param path item path + * @return true if the item exists, + * false otherwise + * @throws RepositoryException on repository exception + * @throws RemoteException on RMI errors + */ + boolean itemExists(String path) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#nodeExists(String) Session.nodeExists(String)} + * method. + * + * @param path node path + * @return true if the node exists, + * false otherwise + * @throws RepositoryException on repository exception + * @throws RemoteException on RMI errors + */ + boolean nodeExists(String path) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#propertyExists(String) Session.propertyExists(String)} + * method. + * + * @param path property path + * @return true if the property exists, + * false otherwise + * @throws RepositoryException on repository exception + * @throws RemoteException on RMI errors + */ + boolean propertyExists(String path) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#removeItem(String) Session.removeItem(String)} + * method. + * + * @param path item path + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void removeItem(String path) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#move(String,String) Session.move(String,String)} + * method. + * + * @param from source path + * @param to destination path + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void move(String from, String to) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#save() Session.save()} method. + * + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void save() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#refresh(boolean) Session.refresh(boolean)} + * method. + * + * @param keepChanges flag to keep transient changes + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void refresh(boolean keepChanges) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#logout() Session.logout()} + * method. + * + * @throws RemoteException on RMI errors + */ + void logout() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#isLive() Session.isLive()} + * method. + * + * @return true if the session is live, + * false otherwise + * @throws RemoteException on RMI errors + */ + boolean isLive() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getRootNode() Session.getRootNode()} method. + * + * @return root node + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteNode getRootNode() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#hasPendingChanges() Session.hasPendingChanges()} + * method. + * + * @return true if the session has pending changes, + * false otherwise + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + boolean hasPendingChanges() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#hasPermission(String,String) Session.hasPermission(String,String)} + * method. + * + * @param path item path + * @param actions actions + * @return true if permission is granted, + * false otherwise + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + boolean hasPermission(String path, String actions) + throws RepositoryException, RemoteException; + + /** + * Imports the system or document view XML data into a subtree of + * the identified node. Note that the entire XML stream is transferred + * as a single byte array over the network. This may cause problems with + * large XML streams. The remote server will wrap the XML data into + * a {@link java.io.ByteArrayInputStream ByteArrayInputStream} and feed + * it to the normal importXML method. + * + * @param path node path + * @param xml imported XML document + * @param uuidBehaviour UUID handling mode + * @throws IOException on IO errors + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + * @see javax.jcr.Session#importXML(java.lang.String, java.io.InputStream, int) + */ + void importXML(String path, byte[] xml, int uuidBehaviour) + throws IOException, RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#setNamespacePrefix(String,String) Session.setNamespacePrefix(String,String)} + * method. + * + * @param prefix namespace prefix + * @param uri namespace uri + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void setNamespacePrefix(String prefix, String uri) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getNamespacePrefixes() Session.getNamespacePrefixes()} + * method. + * + * @return namespace prefixes + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String[] getNamespacePrefixes() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getNamespaceURI(String) Session.getNamespaceURI(String)} + * method. + * + * @param prefix namespace prefix + * @return namespace uri + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getNamespaceURI(String prefix) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getNamespacePrefix(String) Session.getNamespacePrefix(String)} + * method. + * + * @param uri namespace uri + * @return namespace prefix + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String getNamespacePrefix(String uri) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#addLockToken(String) Session.addLockToken(String)} + * method. + * + * @param name lock token + * @throws RemoteException on RMI errors + */ + void addLockToken(String name) throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#getLockTokens() Session.getLockTokens()} + * method. + * + * @return lock tokens + * @throws RemoteException on RMI errors + */ + String[] getLockTokens() throws RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.Session#removeLockToken(String) Session.removeLockToken(String)} + * method. + * + * @param name lock token + * @throws RemoteException on RMI errors + */ + void removeLockToken(String name) throws RemoteException; + + /** + * Exports the identified repository subtree as a system view XML + * stream. Note that the entire XML stream is transferred as a + * single byte array over the network. This may cause problems with + * large exports. The remote server uses a + * {@link java.io.ByteArrayOutputStream ByteArrayOutputStream} to capture + * the XML data written by the normal exportSysView method. + * + * @param path node path + * @param skipBinary binary skip flag + * @param noRecurse no recursion flag + * @return exported XML document + * @throws IOException on IO errors + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + * @see javax.jcr.Session#exportSystemView + */ + byte[] exportSystemView(String path, boolean skipBinary, boolean noRecurse) + throws IOException, RepositoryException, RemoteException; + + /** + * Exports the identified repository subtree as a document view XML + * stream. Note that the entire XML stream is transferred as a + * single byte array over the network. This may cause problems with + * large exports. The remote server uses a + * {@link java.io.ByteArrayOutputStream ByteArrayOutputStream} to capture + * the XML data written by the normal exportDocView method. + * + * @param path node path + * @param skipBinary skip binary flag + * @param noRecurse no recursion flag + * @return exported XML document + * @throws IOException on IO errors + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + * @see javax.jcr.Session#exportDocumentView + */ + byte[] exportDocumentView(String path, boolean skipBinary, boolean noRecurse) + throws IOException, RepositoryException, RemoteException; + + /** + * Remote version of the {@link javax.jcr.Session#getAccessControlManager() + * Session.getAccessControlManager()} method. + * + * @throws UnsupportedRepositoryOperationException if the remote session + * does not support this method + * @throws RepositoryException if an error occurred getting the access + * control manager + * @throws RemoteException on RMI errors + */ + RemoteAccessControlManager getAccessControlManager() + throws UnsupportedRepositoryOperationException, + RepositoryException, RemoteException; +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersion.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersion.java new file mode 100644 index 00000000000..5598fa2c225 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersion.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.rmi.remote; + +import java.rmi.RemoteException; +import java.util.Calendar; + +import javax.jcr.RepositoryException; + +/** + * Remote version of the JCR {@link javax.jcr.version.Version Version} interface. + * Used by the {@link org.apache.jackrabbit.rmi.server.ServerVersion ServerVersion} + * and {@link org.apache.jackrabbit.rmi.client.ClientVersion ClientVersion} + * adapters to provide transparent RMI access to remote versions. + *

    + * The methods in this interface are documented only with a reference + * to a corresponding Version method. The remote object will simply forward + * the method call to the underlying Version instance. Argument and return + * values, as well as possible exceptions, are copied over the network. + * Complex return values (like Versions) are returned as remote + * references to the corresponding remote interfaces. Iterator values + * are transmitted as object arrays. RMI errors are signaled with + * RemoteExceptions. + * + * @see javax.jcr.version.Version + * @see org.apache.jackrabbit.rmi.client.ClientVersion + * @see org.apache.jackrabbit.rmi.server.ServerVersion + */ +public interface RemoteVersion extends RemoteNode { + + /** + * Remote version of the + * {@link javax.jcr.version.Version#getContainingHistory() Version.getContainingHistory()} method. + * + * @return a RemoteVersionHistory object. + * + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteVersionHistory getContainingHistory() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.Version#getCreated() Version.getCreated()} method. + * + * @return a Calendar object. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + Calendar getCreated() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.Version#getLinearSuccessor() Version.getLinearSuccessor()} method. + * + * @return a RemoteVersion or null if no linear + * successor exists. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + * @see RemoteVersionHistory#getAllLinearVersions + */ + RemoteVersion getLinearSuccessor() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.Version#getSuccessors() Version.getSuccessors()} method. + * + * @return a RemoteVersion array. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteVersion[] getSuccessors() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.Version#getLinearPredecessor() Version.getLinearPredecessor()} method. + * + * @return a RemoteVersion or null if no linear + * predecessor exists. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + * @see RemoteVersionHistory#getAllLinearVersions + */ + RemoteVersion getLinearPredecessor() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.Version#getPredecessors() Version.getPredecessors()} method. + * + * @return a RemoteVersion array. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteVersion[] getPredecessors() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.Version#getFrozenNode() Version.getFrozenNode()} method. + * + * @return a RemoteNode object. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteNode getFrozenNode() throws RepositoryException, RemoteException; +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersionHistory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersionHistory.java new file mode 100644 index 00000000000..6bcc0ef58c9 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersionHistory.java @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.remote; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +/** + * Remote version of the JC + * {@link javax.jcr.version.VersionHistory VersionHistory} interface. Used by + * the + * {@link org.apache.jackrabbit.rmi.server.ServerVersionHistory ServerVersionHistory} + * and + * {@link org.apache.jackrabbit.rmi.client.ClientVersionHistory ClientVersionHistory} + * adapters to provide transparent RMI access to remote version histories. + *

    + * The methods in this interface are documented only with a reference + * to a corresponding VersionHistory method. The remote object will simply + * forward the method call to the underlying VersionHistory instance. Argument + * and return values, as well as possible exceptions, are copied over the + * network. Complex return values (like Versions) are returned as remote + * references to the corresponding remote interfaces. Iterator values + * are transmitted as object arrays. RMI errors are signaled with + * RemoteExceptions. + * + * @see javax.jcr.version.Version + * @see org.apache.jackrabbit.rmi.client.ClientVersionHistory + * @see org.apache.jackrabbit.rmi.server.ServerVersionHistory + */ +public interface RemoteVersionHistory extends RemoteNode { + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getVersionableUUID()} VersionHistory.getVersionableUUID()} + * method. + * + * @return the uuid of the versionable node + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + * @deprecated As of JCR 2.0, {@link #getVersionableIdentifier} should be + * used instead. + */ + String getVersionableUUID() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getVersionableIdentifier()} VersionHistory.getVersionableIdentifier()} + * method. + * + * @return the identifier of the versionable node + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + */ + String getVersionableIdentifier() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getRootVersion() VersionHistory.getRootVersion()} + * method. + * + * @return a Version object. + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + */ + RemoteVersion getRootVersion() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getAllLinearVersions() VersionHistory.getAllLinearVersions()} + * method. + * + * @return linear remote versions + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + */ + RemoteIterator getAllLinearVersions() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getAllVersions() VersionHistory.getAllVersions()} + * method. + * + * @return remote versions + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + */ + RemoteIterator getAllVersions() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getAllLinearFrozenNodes() VersionHistory.getAllLinearFrozenNodes()} + * method. + * + * @return linear remote frozen nodes + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + */ + RemoteIterator getAllLinearFrozenNodes() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getAllFrozenNodes() VersionHistory.getAllFrozenNodes()} + * method. + * + * @return remote frozen nodes + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + */ + RemoteIterator getAllFrozenNodes() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getVersion(String) VersionHistory.getVersion(String)} + * method. + * + * @param versionName a version name + * @return a Version object. + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + */ + RemoteVersion getVersion(String versionName) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getVersionByLabel(String) VersionHistory.getVersionByLabel(String)} + * method. + * + * @param label a version label + * @return a Version object. + * @throws RepositoryException if an error occurs. + * @throws RemoteException on RMI errors + */ + RemoteVersion getVersionByLabel(String label) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean) + * VersionHistory.addVersionLabel(String, String, boolean)} + * method. + * + * @param versionName the name of the version to which the label is to be added. + * @param label the label to be added. + * @param moveLabel if true, then if label is already assigned to a version in + * this version history, it is moved to the new version specified; if false, then attempting + * to assign an already used label will throw a VersionException. + * + * @throws RepositoryException if another error occurs. + * @throws RemoteException on RMI errors + */ + void addVersionLabel(String versionName, String label, boolean moveLabel) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#removeVersionLabel(String) VersionHistory.removeVersionLabel(String)} + * method. + * + * @param label a version label + * @throws RepositoryException if another error occurs. + * @throws RemoteException on RMI errors + */ + void removeVersionLabel(String label) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#hasVersionLabel(String) VersionHistory.hasVersionLabel(String)} + * method. + * + * @param label a version label + * @return a boolean + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + boolean hasVersionLabel(String label) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#hasVersionLabel(javax.jcr.version.Version, String) hasVersionLabel(Version, String)} + * method. + * + * @param versionUUID The UUID of the version whose labels are to be returned. + * @param label a version label + * @return a boolean. + * @throws RepositoryException if another error occurs. + * @throws RemoteException on RMI errors + */ + boolean hasVersionLabel(String versionUUID, String label) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getVersionLabels() VersionHistory.getVersionLabels()} + * method. + * + * @return a String array containing all the labels of the version history + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + String[] getVersionLabels() throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#getVersionLabels(javax.jcr.version.Version) VersionHistory.getVersionLabels(Version)} + * method. + * + * @param versionUUID The UUID of the version whose labels are to be returned. + * @return a String array containing all the labels of the given version + * @throws RepositoryException if another error occurs. + * @throws RemoteException on RMI errors + */ + String[] getVersionLabels(String versionUUID) + throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionHistory#removeVersion(String) VersionHistory.removeVersion(String)} + * method. + * + * @param versionName the name of a version in this version history. + * @throws RepositoryException if another error occurs. + * @throws RemoteException on RMI errors + */ + void removeVersion(String versionName) + throws RepositoryException, RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersionManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersionManager.java new file mode 100644 index 00000000000..626be070ec7 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteVersionManager.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +public interface RemoteVersionManager extends Remote { + + /** + * Remote version of the + * {@link javax.jcr.version.VersionManager#checkin(String) VersionManager.checkin(String)} + * method. + * + * @param absPath an absolute path. + * @return the created version. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteVersion checkin(String absPath) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionManager#checkout(String) VersionManager.checkout(String)} + * method. + * + * @param absPath an absolute path. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + void checkout(String absPath) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionManager#checkpoint(String) VersionManager.checkpoint(String)} + * method. + * + * @param absPath an absolute path. + * @return the created version. + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteVersion checkpoint(String absPath) throws RepositoryException, RemoteException; + + /** + * Remote version of the + * {@link javax.jcr.version.VersionManager#isCheckedOut(String) VersionManager.isCheckedOut(String)} + * method. + * + * @param absPath an absolute path. + * @return a boolean + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + boolean isCheckedOut(String absPath) throws RepositoryException, RemoteException; + + RemoteVersionHistory getVersionHistory(String absPath) throws RepositoryException, RemoteException; + + RemoteVersion getBaseVersion(String absPath) throws RepositoryException, RemoteException; + + void restore(String[] versionIdentifiers, boolean removeExisting) throws RepositoryException, RemoteException; + + void restore(String absPath, String versionName, boolean removeExisting) throws RepositoryException, RemoteException; + + void restore(String versionIdentifier, boolean removeExisting) throws RepositoryException, RemoteException; + + void restoreVI(String absPath, String versionIdentifier, boolean removeExisting) throws RepositoryException, RemoteException; + + void restoreByLabel(String absPath, String versionLabel, boolean removeExisting) throws RepositoryException, RemoteException; + + RemoteIterator merge(String absPath, String srcWorkspace, boolean bestEffort) + throws RepositoryException, RemoteException; + + RemoteIterator merge(String absPath, String srcWorkspace, boolean bestEffort, boolean isShallow) + throws RepositoryException, RemoteException; + + void doneMerge(String absPath, String versionIdentifier) throws RepositoryException, RemoteException; + + void cancelMerge(String absPath, String versionIdentifier) throws RepositoryException, RemoteException; + + RemoteNode createConfiguration(String absPath) throws RepositoryException, RemoteException; + + RemoteNode setActivity(String activityNodeIdentifier) throws RepositoryException, RemoteException; + + RemoteNode getActivity() throws RepositoryException, RemoteException; + + RemoteNode createActivity(String title) throws RepositoryException, RemoteException; + + void removeActivity(String activityNodeIdentifier) throws RepositoryException, RemoteException; + + RemoteIterator merge(String activityNodeIdentifier) throws RepositoryException, RemoteException; + +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteWorkspace.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteWorkspace.java similarity index 81% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteWorkspace.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteWorkspace.java index 4476edf88da..078b20680cf 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/remote/RemoteWorkspace.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteWorkspace.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -36,9 +36,8 @@ * Complex return values like namespace registries and other objects are * returned as remote references to the corresponding remote interface. Simple * return values and possible exceptions are copied over the network - * to the client. RMI errors are signalled with RemoteExceptions. + * to the client. RMI errors are signaled with RemoteExceptions. * - * @author Jukka Zitting * @see javax.jcr.Workspace * @see org.apache.jackrabbit.rmi.client.ClientWorkspace * @see org.apache.jackrabbit.rmi.server.ServerWorkspace @@ -145,6 +144,18 @@ RemoteNamespaceRegistry getNamespaceRegistry() RemoteQueryManager getQueryManager() throws RepositoryException, RemoteException; + /** + * Remote version of the + * {@link javax.jcr.Workspace#getObservationManager() Workspace.getObservationManager()} + * method. + * + * @return observation manager + * @throws RepositoryException on repository errors + * @throws RemoteException on RMI errors + */ + RemoteObservationManager getObservationManager() + throws RepositoryException, RemoteException; + /** * Remote version of the * {@link javax.jcr.Workspace#getAccessibleWorkspaceNames() Workspace.getAccessibleWorkspaceNames()} @@ -172,4 +183,16 @@ String[] getAccessibleWorkspaceNames() void importXML(String path, byte[] xml, int uuidBehaviour) throws IOException, RepositoryException, RemoteException; + void createWorkspace(String name, String source) + throws RepositoryException, RemoteException; + + void deleteWorkspace(String name) + throws RepositoryException, RemoteException; + + RemoteLockManager getLockManager() + throws RepositoryException, RemoteException; + + RemoteVersionManager getVersionManager() + throws RepositoryException, RemoteException; + } diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteXASession.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteXASession.java new file mode 100644 index 00000000000..b6f425faa01 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/RemoteXASession.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.rmi.remote; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.Xid; + +/** + * Remote version of the {@link org.apache.jackrabbit.api.XASession} + * interface. + */ +public interface RemoteXASession extends RemoteSession, Remote { + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#commit(Xid, boolean)} method. + */ + void commit(Xid xid, boolean onePhase) throws XAException, RemoteException; + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#end(Xid, int)} method. + */ + void end(Xid xid, int flags) throws XAException, RemoteException; + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#forget(Xid)} method. + */ + void forget(Xid xid) throws XAException, RemoteException; + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#getTransactionTimeout()} method. + */ + int getTransactionTimeout() throws XAException, RemoteException; + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#prepare(Xid)} method. + */ + int prepare(Xid xid) throws XAException, RemoteException; + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#recover(int)} method. + */ + Xid[] recover(int flag) throws XAException, RemoteException; + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#rollback(Xid)} method. + */ + void rollback(Xid xid) throws XAException, RemoteException; + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#setTransactionTimeout(int)} method. + */ + boolean setTransactionTimeout(int seconds) + throws XAException, RemoteException; + + /** + * Remote version of the + * {@link javax.transaction.xa.XAResource#start(Xid, int)} method. + */ + void start(Xid xid, int flags) throws XAException, RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/SerializableXid.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/SerializableXid.java new file mode 100644 index 00000000000..83bd3524208 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/SerializableXid.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.rmi.remote; + +import java.io.Serializable; +import java.util.Arrays; + +import javax.transaction.xa.Xid; + +/** + * Serializable {@link Xid}. + * + * @since Jackrabbit JCR-RMI 1.5 + */ +public class SerializableXid implements Serializable, Xid { + + private final int formatId; + + private final byte[] globalTransactionId; + + private final byte[] branchQualifier; + + private final int hashCode; + + public SerializableXid(Xid xid) { + formatId = xid.getFormatId(); + globalTransactionId = xid.getGlobalTransactionId(); + branchQualifier = xid.getBranchQualifier(); + hashCode = xid.hashCode(); + } + + public int getFormatId() { + return formatId; + } + + public byte[] getGlobalTransactionId() { + return globalTransactionId; + } + + public byte[] getBranchQualifier() { + return branchQualifier; + } + + public int hashCode() { + return hashCode; + } + + public boolean equals(Object xid) { + return (xid instanceof Xid) + && formatId == ((Xid) xid).getFormatId() + && Arrays.equals( + globalTransactionId, ((Xid) xid).getGlobalTransactionId()) + && Arrays.equals( + branchQualifier, ((Xid) xid).getBranchQualifier()); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/principal/RemoteGroup.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/principal/RemoteGroup.java new file mode 100644 index 00000000000..ae3695396c0 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/principal/RemoteGroup.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.rmi.remote.principal; + +import java.rmi.RemoteException; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; + +/** + * Remote version of the JCR {@link org.apache.jackrabbit.api.security.principal.GroupPrincipal GroupPrincipal} interface. + * Used by the {@link org.apache.jackrabbit.rmi.server.principal.ServerGroup + * ServerGroup} and + * {@link org.apache.jackrabbit.rmi.client.principal.ClientGroup ClientGroup} + * adapter base classes to provide transparent RMI access to remote item + * definitions. + *

    + * The methods in this interface are documented only with a reference to a + * corresponding Group method. The remote object will simply forward the method + * call to the underlying Group instance. Argument and return values, as well as + * possible exceptions, are copied over the network. Complex return values are + * returned as remote references to the corresponding remote interface. RMI + * errors are signaled with RemoteExceptions. + * + * @see org.apache.jackrabbit.api.security.principal.GroupPrincipal + * @see org.apache.jackrabbit.rmi.client.principal.ClientGroup + * @see org.apache.jackrabbit.rmi.server.principal.ServerGroup + */ +public interface RemoteGroup extends RemotePrincipal { + + /** + * @see org.apache.jackrabbit.api.security.principal.GroupPrincipal#isMember(java.security.Principal) + */ + boolean isMember(String member) throws RemoteException; + + /** + * @see org.apache.jackrabbit.api.security.principal.GroupPrincipal#members() + */ + RemoteIterator members() throws RemoteException; + +} \ No newline at end of file diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/principal/RemotePrincipal.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/principal/RemotePrincipal.java new file mode 100644 index 00000000000..fb49911a9cd --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/principal/RemotePrincipal.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.rmi.remote.principal; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +/** + * Remote version of the JCR {@link java.security.Principal Principal} + * interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.principal.ServerPrincipal + * ServerPrincipal} and + * {@link org.apache.jackrabbit.rmi.client.principal.ClientPrincipal + * ClientPrincipal} adapter base classes to provide transparent RMI access to + * remote item definitions. + *

    + * The methods in this interface are documented only with a reference to a + * corresponding Principal method. The remote object will simply forward the + * method call to the underlying Principal instance. Argument and return values, + * as well as possible exceptions, are copied over the network. Complex return + * values are returned as remote references to the corresponding remote + * interface. RMI errors are signaled with RemoteExceptions. + * + * @see java.security.Principal + * @see org.apache.jackrabbit.rmi.client.principal.ClientPrincipal + * @see org.apache.jackrabbit.rmi.server.principal.ServerPrincipal + */ +public interface RemotePrincipal extends Remote { + + /** + * @see java.security.Principal#getName() + */ + public String getName() throws RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlEntry.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlEntry.java new file mode 100644 index 00000000000..b02fb584060 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlEntry.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.rmi.remote.security; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; + +/** + * Remote version of the JCR {@link javax.jcr.security.AccessControlEntry + * AccessControlEntry} interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.security.ServerAccessControlEntry + * ServerAccessControlEntry} and + * {@link org.apache.jackrabbit.rmi.client.security.ClientAccessControlEntry + * ClientAccessControlEntry} adapter base classes to provide transparent RMI + * access to remote item definitions. + *

    + * The methods in this interface are documented only with a reference to a + * corresponding AccessControlEntry method. The remote object will simply + * forward the method call to the underlying AccessControlEntry instance. + * Argument and return values, as well as possible exceptions, are copied over + * the network. Complex return values are returned as remote references to the + * corresponding remote interface. RMI errors are signaled with + * RemoteExceptions. + * + * @see javax.jcr.security.AccessControlEntry + * @see org.apache.jackrabbit.rmi.client.security.ClientAccessControlEntry + * @see org.apache.jackrabbit.rmi.server.security.ServerAccessControlEntry + */ +public interface RemoteAccessControlEntry extends Remote { + + /** + * @see javax.jcr.security.AccessControlEntry#getPrincipal() + */ + public RemotePrincipal getPrincipal() throws RemoteException; + + /** + * @see javax.jcr.security.AccessControlEntry#getPrivileges() + */ + public RemotePrivilege[] getPrivileges() throws RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlList.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlList.java new file mode 100644 index 00000000000..12b15d4e553 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlList.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.rmi.remote.security; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +/** + * Remote version of the JCR {@link javax.jcr.security.AccessControlList + * AccessControlList} interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.security.ServerAccessControlList + * ServerAccessControlList} and + * {@link org.apache.jackrabbit.rmi.client.security.ClientAccessControlList + * ClientAccessControlList} adapter base classes to provide transparent RMI + * access to remote item definitions. + *

    + * The methods in this interface are documented only with a reference to a + * corresponding AccessControlList method. The remote object will simply forward + * the method call to the underlying AccessControlList instance. Argument and + * return values, as well as possible exceptions, are copied over the network. + * Complex return values are returned as remote references to the corresponding + * remote interface. RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.security.AccessControlList + * @see org.apache.jackrabbit.rmi.client.security.ClientAccessControlList + * @see org.apache.jackrabbit.rmi.server.security.ServerAccessControlList + */ +public interface RemoteAccessControlList extends RemoteAccessControlPolicy { + + /** + * @see javax.jcr.security.AccessControlList#getAccessControlEntries() + */ + public RemoteAccessControlEntry[] getAccessControlEntries() + throws RepositoryException, RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlManager.java new file mode 100644 index 00000000000..bca2539f496 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlManager.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.rmi.remote.security; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; + +/** + * Remote version of the JCR {@link javax.jcr.security.AccessControlManager + * AccessControlManager} interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.security.ServerAccessControlManager + * ServerAccessControlManager} and + * {@link org.apache.jackrabbit.rmi.client.security.ClientAccessControlManager + * ClientAccessControlManager} adapter base classes to provide transparent RMI + * access to remote item definitions. + *

    + * The methods in this interface are documented only with a reference to a + * corresponding AccessControlManager method. The remote object will simply + * forward the method call to the underlying AccessControlManager instance. + * Argument and return values, as well as possible exceptions, are copied over + * the network. Complex return values are returned as remote references to the + * corresponding remote interface. RMI errors are signaled with + * RemoteExceptions. + * + * @see javax.jcr.security.AccessControlManager + * @see org.apache.jackrabbit.rmi.client.security.ClientAccessControlManager + * @see org.apache.jackrabbit.rmi.server.security.ServerAccessControlManager + */ +public interface RemoteAccessControlManager extends Remote { + + /** + * @see javax.jcr.security.AccessControlManager#getApplicablePolicies(String) + */ + public RemoteIterator getApplicablePolicies(String absPath) + throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.security.AccessControlManager#getEffectivePolicies(String) + */ + public RemoteAccessControlPolicy[] getEffectivePolicies(String absPath) + throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.security.AccessControlManager#getPolicies(String) + */ + public RemoteAccessControlPolicy[] getPolicies(String absPath) + throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.security.AccessControlManager#getPrivileges(String) + */ + public RemotePrivilege[] getPrivileges(String absPath) + throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.security.AccessControlManager#getSupportedPrivileges(String) + */ + public RemotePrivilege[] getSupportedPrivileges(String absPath) + throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.security.AccessControlManager#privilegeFromName(String) + */ + public RemotePrivilege privilegeFromName(String privilegeName) + throws RepositoryException, RemoteException; + + /** + * @see javax.jcr.security.AccessControlManager#hasPrivileges(String, + * javax.jcr.security.Privilege[]) + */ + public boolean hasPrivileges(String absPath, String[] privileges) + throws RepositoryException, RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlPolicy.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlPolicy.java new file mode 100644 index 00000000000..a775b5ac0f6 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemoteAccessControlPolicy.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.rmi.remote.security; + +import java.rmi.Remote; + +/** + * Remote version of the JCR {@link javax.jcr.security.AccessControlPolicy + * AccessControlPolicy} interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.security.ServerAccessControlPolicy + * ServerAccessControlPolicy} and + * {@link org.apache.jackrabbit.rmi.client.security.ClientAccessControlPolicy + * ClientAccessControlPolicy} adapter base classes to provide transparent RMI + * access to remote item definitions. + *

    + * The methods in this interface are documented only with a reference to a + * corresponding AccessControlPolicy method. The remote object will simply + * forward the method call to the underlying AccessControlPolicy instance. + * Argument and return values, as well as possible exceptions, are copied over + * the network. Complex return values are returned as remote references to the + * corresponding remote interface. RMI errors are signaled with + * RemoteExceptions. + * + * @see javax.jcr.security.AccessControlPolicy + * @see org.apache.jackrabbit.rmi.client.security.ClientAccessControlPolicy + * @see org.apache.jackrabbit.rmi.server.security.ServerAccessControlPolicy + */ +public interface RemoteAccessControlPolicy extends Remote { + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemotePrivilege.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemotePrivilege.java new file mode 100644 index 00000000000..148aff1de02 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/remote/security/RemotePrivilege.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.rmi.remote.security; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +/** + * Remote version of the JCR {@link javax.jcr.security.Privilege Privilege} + * interface. Used by the + * {@link org.apache.jackrabbit.rmi.server.security.ServerPrivilege + * ServerPrivilege} and + * {@link org.apache.jackrabbit.rmi.client.security.ClientPrivilege + * ClientPrivilege} adapter base classes to provide transparent RMI access to + * remote item definitions. + *

    + * The methods in this interface are documented only with a reference to a + * corresponding Privilege method. The remote object will simply forward the + * method call to the underlying Privilege instance. Argument and return values, + * as well as possible exceptions, are copied over the network. Complex return + * values are returned as remote references to the corresponding remote + * interface. RMI errors are signaled with RemoteExceptions. + * + * @see javax.jcr.security.Privilege + * @see org.apache.jackrabbit.rmi.client.security.ClientPrivilege + * @see org.apache.jackrabbit.rmi.server.security.ServerPrivilege + */ +public interface RemotePrivilege extends Remote { + + /** + * @see javax.jcr.security.Privilege#getAggregatePrivileges() + */ + public RemotePrivilege[] getAggregatePrivileges() throws RemoteException; + + /** + * @see javax.jcr.security.Privilege#getDeclaredAggregatePrivileges() + */ + public RemotePrivilege[] getDeclaredAggregatePrivileges() + throws RemoteException; + + /** + * @see javax.jcr.security.Privilege#getName() + */ + public String getName() throws RemoteException; + + /** + * @see javax.jcr.security.Privilege#isAbstract() + */ + public boolean isAbstract() throws RemoteException; + + /** + * @see javax.jcr.security.Privilege#isAggregate() + */ + public boolean isAggregate() throws RemoteException; +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/AbstractRemoteRepositoryFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/AbstractRemoteRepositoryFactory.java new file mode 100644 index 00000000000..1a8be8d8dd0 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/AbstractRemoteRepositoryFactory.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.rmi.repository; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; + +/** + * Abstract base class for repository factories that make a remote repository + * available locally. Subclasses need to implement the + * {@link #getRemoteRepository()} method to actually retrieve the remote + * repository reference. + * + * @since 1.4 + */ +public abstract class AbstractRemoteRepositoryFactory + implements RepositoryFactory { + + /** + * Local adapter factory. + */ + private final LocalAdapterFactory factory; + + /** + * Creates a factory for looking up a repository from the given RMI URL. + * + * @param factory local adapter factory + */ + protected AbstractRemoteRepositoryFactory(LocalAdapterFactory factory) { + this.factory = factory; + } + + /** + * Returns a local adapter for the remote repository. + * + * @return local adapter for the remote repository + * @throws RepositoryException if the remote repository is not available + */ + public Repository getRepository() throws RepositoryException { + return factory.getRepository(getRemoteRepository()); + } + + /** + * Returns the remote repository reference. + * + * @return remote repository reference + * @throws RepositoryException if the remote repository is not available + */ + protected abstract RemoteRepository getRemoteRepository() + throws RepositoryException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/JNDIRemoteRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/JNDIRemoteRepository.java new file mode 100644 index 00000000000..add03b1f880 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/JNDIRemoteRepository.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.rmi.repository; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; + +/** + * Proxy for a remote repository bound in JNDI. The configured repository is + * looked up from JNDI lazily during each method call. Thus the JNDI entry + * does not need to exist when this class is instantiated. The JNDI entry + * can also be replaced with another repository during the lifetime of an + * instance of this class. + * + * @since 1.4 + */ +public class JNDIRemoteRepository extends ProxyRepository { + + /** + * Creates a proxy for a remote repository in JNDI. + * + * @param factory local adapter factory + * @param context JNDI context + * @param location JNDI location + */ + public JNDIRemoteRepository( + LocalAdapterFactory factory, Context context, String location) { + super(new JNDIRemoteRepositoryFactory(factory, context, location)); + } + + /** + * Creates a proxy for the remote repository in JNDI. + * Uses {@link ClientAdapterFactory} as the default + * local adapter factory. + * + * @param context JNDI context + * @param location JNDI location + */ + public JNDIRemoteRepository(Context context, String location) { + this(new ClientAdapterFactory(), context, location); + } + + /** + * Creates a proxy for the remote repository in JNDI. + * Uses {@link ClientAdapterFactory} as the default + * local adapter factory. + * + * @param location JNDI location in default context + * @throws NamingException if the default JNDI context is not available + */ + public JNDIRemoteRepository(String location) throws NamingException { + this(new InitialContext(), location); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/JNDIRemoteRepositoryFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/JNDIRemoteRepositoryFactory.java new file mode 100644 index 00000000000..5d00a582c03 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/JNDIRemoteRepositoryFactory.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.repository; + +import javax.jcr.RepositoryException; +import javax.naming.Context; +import javax.naming.NamingException; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; + +/** + * Factory that looks up a remote repository from JNDI. + * + * @since 1.4 + */ +public class JNDIRemoteRepositoryFactory + extends AbstractRemoteRepositoryFactory { + + /** + * JNDI context of the remote repository. + */ + private final Context context; + + /** + * JNDI location of the remote repository. + */ + private final String location; + + /** + * Creates a factory for looking up a remote repository from JNDI. + * + * @param factory local adapter factory + * @param context JNDI context + * @param location JNDI location + */ + public JNDIRemoteRepositoryFactory( + LocalAdapterFactory factory, Context context, String location) { + super(factory); + this.context = context; + this.location = location; + } + + /** + * Looks up a remote repository from JNDI. + * + * @return remote repository reference + * @throws RepositoryException if the remote repository is not available + */ + protected RemoteRepository getRemoteRepository() + throws RepositoryException { + try { + Object remote = context.lookup(location); + if (remote instanceof RemoteRepository) { + return (RemoteRepository) remote; + } else if (remote == null) { + throw new RepositoryException( + "Remote repository not found: The JNDI entry " + + location + " is null"); + } else { + throw new RepositoryException( + "Invalid remote repository: The JNDI entry " + + location + " is an instance of " + + remote.getClass().getName()); + } + } catch (NamingException e) { + throw new RepositoryException( + "Remote repository not found: The JNDI entry " + location + + " could not be looked up", e); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/ProxyRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/ProxyRepository.java new file mode 100644 index 00000000000..36496d0ce3b --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/ProxyRepository.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.rmi.repository; + +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +/** + * Repository that proxies all method calls to another repository. + * The other repository is accessed lazily using a + * {@link RepositoryFactory repository factory}. + * + * @since 1.4 + */ +public class ProxyRepository implements Repository { + + /** + * The set of standard descriptor keys defined in the + * {@link Repository} interface. + */ + private static final Set STANDARD_KEYS = new HashSet() {{ + add(Repository.IDENTIFIER_STABILITY); + add(Repository.LEVEL_1_SUPPORTED); + add(Repository.LEVEL_2_SUPPORTED); + add(Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_INHERITANCE); + add(Repository.NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_PROPERTY_TYPES); + add(Repository.NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED); + add(Repository.NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPORTED); + add(Repository.OPTION_ACCESS_CONTROL_SUPPORTED); + add(Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED); + add(Repository.OPTION_LIFECYCLE_SUPPORTED); + add(Repository.OPTION_LOCKING_SUPPORTED); + add(Repository.OPTION_OBSERVATION_SUPPORTED); + add(Repository.OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED); + add(Repository.OPTION_QUERY_SQL_SUPPORTED); + add(Repository.OPTION_RETENTION_SUPPORTED); + add(Repository.OPTION_SHAREABLE_NODES_SUPPORTED); + add(Repository.OPTION_SIMPLE_VERSIONING_SUPPORTED); + add(Repository.OPTION_TRANSACTIONS_SUPPORTED); + add(Repository.OPTION_UNFILED_CONTENT_SUPPORTED); + add(Repository.OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED); + add(Repository.OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED); + add(Repository.OPTION_VERSIONING_SUPPORTED); + add(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED); + add(Repository.OPTION_XML_EXPORT_SUPPORTED); + add(Repository.OPTION_XML_IMPORT_SUPPORTED); + add(Repository.OPTION_ACTIVITIES_SUPPORTED); + add(Repository.OPTION_BASELINES_SUPPORTED); + + add(Repository.QUERY_FULL_TEXT_SEARCH_SUPPORTED); + add(Repository.QUERY_JOINS); + add(Repository.QUERY_LANGUAGES); + add(Repository.QUERY_STORED_QUERIES_SUPPORTED); + add(Repository.QUERY_XPATH_DOC_ORDER); + add(Repository.QUERY_XPATH_POS_INDEX); + add(Repository.REP_NAME_DESC); + add(Repository.REP_VENDOR_DESC); + add(Repository.REP_VENDOR_URL_DESC); + add(Repository.SPEC_NAME_DESC); + add(Repository.SPEC_VERSION_DESC); + add(Repository.WRITE_SUPPORTED); + }}; + + /** + * Factory for accessing the proxied repository. + */ + private final RepositoryFactory factory; + + /** + * Creates a proxy for the repository (or repositories) accessible + * through the given factory. + * + * @param factory repository factory + */ + public ProxyRepository(RepositoryFactory factory) { + this.factory = factory; + } + + /** + * Returns the descriptor keys of the proxied repository, or an empty + * array if the proxied repository can not be accessed. + * + * @return descriptor keys (possibly empty) + */ + public String[] getDescriptorKeys() { + try { + return factory.getRepository().getDescriptorKeys(); + } catch (RepositoryException e) { + return new String[0]; + } + } + + /** + * Checks whether the given key identifies a valid single-valued + * descriptor key in the proxied repository. Returns false + * if the proxied repository can not be accessed. + * + * @return true if the key identifies a valid single-valued + * descriptor in the proxied repository, + * false otherwise + */ + public boolean isSingleValueDescriptor(String key) { + try { + return factory.getRepository().isSingleValueDescriptor(key); + } catch (RepositoryException e) { + return false; + } + } + + /** + * Returns the descriptor with the given key from the proxied repository. + * Returns null if the descriptor does not exist or if the + * proxied repository can not be accessed. + * + * @param key descriptor key + * @return descriptor value, or null + */ + public String getDescriptor(String key) { + try { + return factory.getRepository().getDescriptor(key); + } catch (RepositoryException e) { + return null; + } + } + + /** + * Returns the value of the descriptor with the given key from the proxied + * repository. Returns null if the descriptor does not exist + * or if the proxied repository can not be accessed. + * + * @param key descriptor key + * @return descriptor value, or null + */ + public Value getDescriptorValue(String key) { + try { + return factory.getRepository().getDescriptorValue(key); + } catch (RepositoryException e) { + return null; + } + } + + /** + * Returns the values of the descriptor with the given key from the proxied + * repository. Returns null if the descriptor does not exist + * or if the proxied repository can not be accessed. + * + * @param key descriptor key + * @return descriptor values, or null + */ + public Value[] getDescriptorValues(String key) { + try { + return factory.getRepository().getDescriptorValues(key); + } catch (RepositoryException e) { + return null; + } + } + + /** + * Logs in to the proxied repository and returns the resulting session. + *

    + * Note that the {@link Session#getRepository()} method of the resulting + * session will return the proxied repository, not this repository proxy! + * + * @throws RepositoryException if the proxied repository can not be + * accessed, or if the login in the proxied + * repository fails + */ + public Session login(Credentials credentials, String workspace) + throws RepositoryException { + return factory.getRepository().login(credentials, workspace); + } + + /** + * Returns true if the given key identifies a standard descriptor. + * + * @param key descriptor key + * @return true if the key identifies a standard descriptor, + * false otherwise + */ + public boolean isStandardDescriptor(String key) { + return STANDARD_KEYS.contains(key); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * null arguments. + * + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login() throws RepositoryException { + return login(null, null); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * the given credentials and a null workspace name. + * + * @param credentials login credentials + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login(Credentials credentials) throws RepositoryException { + return login(credentials, null); + } + + /** + * Calls {@link Repository#login(Credentials, String)} with + * null credentials and the given workspace name. + * + * @param workspace workspace name + * @return logged in session + * @throws RepositoryException if an error occurs + */ + public Session login(String workspace) throws RepositoryException { + return login(null, workspace); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RMIRemoteRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RMIRemoteRepository.java new file mode 100644 index 00000000000..d1c2d0b866e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RMIRemoteRepository.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.rmi.repository; + +import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; + +/** + * Proxy for a remote repository bound in RMI. The configured repository is + * looked up from RMI lazily during each method call. Thus the RMI entry + * does not need to exist when this class is instantiated. The RMI entry + * can also be replaced with another repository during the lifetime of an + * instance of this class. + * + * @since 1.4 + */ +public class RMIRemoteRepository extends ProxyRepository { + + /** + * Creates a proxy for the remote repository in the given RMI URL. + * + * @param factory local adapter factory + * @param url RMI URL of the remote repository + */ + public RMIRemoteRepository(LocalAdapterFactory factory, String url) { + super(new RMIRemoteRepositoryFactory(factory, url)); + } + + /** + * Creates a proxy for the remote repository in the given RMI URL. + * Uses {@link ClientAdapterFactory} as the default + * local adapter factory. + * + * @param url URL of the remote repository + */ + public RMIRemoteRepository(String url) { + this(new ClientAdapterFactory(), url); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RMIRemoteRepositoryFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RMIRemoteRepositoryFactory.java new file mode 100644 index 00000000000..d462b8c8cda --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RMIRemoteRepositoryFactory.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.rmi.repository; + +import java.net.MalformedURLException; +import java.rmi.Naming; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; + +/** + * Factory that looks up a remote repository from an RMI registry. + * + * @since 1.4 + */ +public class RMIRemoteRepositoryFactory + extends AbstractRemoteRepositoryFactory { + + /** + * RMI URL of the remote repository. + */ + private final String url; + + /** + * Creates a factory for looking up a remote repository from + * an RMI registry. + * + * @param factory local adapter factory + * @param url RMI URL of the repository + */ + public RMIRemoteRepositoryFactory(LocalAdapterFactory factory, String url) { + super(factory); + this.url = url; + } + + /** + * Looks up a remote repository from the RMI registry. + * + * @return remote repository reference + * @throws RepositoryException if the remote repository is not available + */ + protected RemoteRepository getRemoteRepository() + throws RepositoryException { + try { + return (RemoteRepository) Naming.lookup(url); + } catch (MalformedURLException e) { + throw new RepositoryException("Invalid repository URL: " + url, e); + } catch (NotBoundException e) { + throw new RepositoryException("Repository not found: " + url, e); + } catch (ClassCastException e) { + throw new RepositoryException("Invalid repository: " + url, e); + } catch (RemoteException e) { + throw new RepositoryException("Repository access error: " + url, e); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RepositoryFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RepositoryFactory.java new file mode 100644 index 00000000000..92decd5c676 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RepositoryFactory.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.rmi.repository; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +/** + * Factory interface for JCR content repositories. + */ +interface RepositoryFactory { + + /** + * Returns a content repository. + * + * @return content repository + * @throws RepositoryException if a repository is not available + */ + Repository getRepository() throws RepositoryException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RmiRepositoryFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RmiRepositoryFactory.java new file mode 100644 index 00000000000..a4c1c85ac9f --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/RmiRepositoryFactory.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.repository; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.rmi.Naming; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.util.Hashtable; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.client.SafeClientRepository; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; + +public class RmiRepositoryFactory implements RepositoryFactory { + + private static final String REPOSITORY_URI = + "org.apache.jackrabbit.repository.uri"; + + @SuppressWarnings({"unchecked", "rawtypes"}) + public Repository getRepository(Map parameters) throws RepositoryException { + if (parameters != null && parameters.containsKey(REPOSITORY_URI)) { + URI uri; + try { + uri = new URI(parameters.get(REPOSITORY_URI).toString().trim()); + } catch (URISyntaxException e) { + return null; + } + + String scheme = uri.getScheme(); + if ("rmi".equalsIgnoreCase(scheme)) { + return getRmiRepository(uri.getSchemeSpecificPart()); + } else if ("jndi".equalsIgnoreCase(scheme)) { + Hashtable environment = new Hashtable(parameters); + environment.remove(REPOSITORY_URI); + return getJndiRepository( + uri.getSchemeSpecificPart(), environment); + } else { + try { + return getUrlRepository(uri.toURL()); + } catch (MalformedURLException e) { + return null; + } + } + } else { + return null; + } + } + + private Repository getUrlRepository(final URL url) + throws RepositoryException { + return new RmiSafeClientRepository(new ClientAdapterFactory()) { + + @Override + protected RemoteRepository getRemoteRepository() throws RemoteException { + try { + InputStream stream = url.openStream(); + try { + Object remote = new ObjectInputStream(stream).readObject(); + if (remote instanceof RemoteRepository) { + return (RemoteRepository) remote; + } else { + throw new RemoteException("The resource at URL " + url + + " is not a remote repository stub: " + remote); + } + } finally { + if (stream != null) { + stream.close(); + } + } + } catch (ClassNotFoundException e) { + throw new RemoteException("The resource at URL " + url + + " requires a class that is not available", e); + } catch (IOException e) { + throw new RemoteException("Failed to read the resource at URL " + + url, e); + } + } + }; + } + + @SuppressWarnings("rawtypes") + private Repository getJndiRepository(final String name, + final Hashtable environment) throws RepositoryException { + return new RmiSafeClientRepository(new ClientAdapterFactory()) { + + @Override + protected RemoteRepository getRemoteRepository() throws RemoteException { + try { + Object value = new InitialContext(environment).lookup(name); + if (value instanceof RemoteRepository) { + return (RemoteRepository) value; + } else { + throw new RemoteException("The JNDI resource " + name + + " is not a remote repository stub: " + value); + } + } catch (NamingException e) { + throw new RemoteException( + "Failed to look up the JNDI resource " + name, e); + } + } + }; + } + + private Repository getRmiRepository(final String name) + throws RepositoryException { + return new RmiSafeClientRepository(new ClientAdapterFactory()) { + + @Override + protected RemoteRepository getRemoteRepository() throws RemoteException { + try { + Object value = Naming.lookup(name); + if (value instanceof RemoteRepository) { + return (RemoteRepository) value; + } else { + throw new RemoteException("The RMI resource " + name + + " is not a remote repository stub: " + value); + } + } catch (NotBoundException e) { + throw new RemoteException( + "RMI resource " + name + " not found", e); + } catch (MalformedURLException e) { + throw new RemoteException("Invalid RMI name: " + name, e); + } catch (RemoteException e) { + throw new RemoteException("Failed to look up the RMI resource " + + name, e); + } + } + }; + } + + /** + * Basic SafeClientRepository for the different lookup types of a rmi repository + */ + private static class RmiSafeClientRepository extends SafeClientRepository { + + public RmiSafeClientRepository(LocalAdapterFactory factory) { + super(factory); + } + + @Override + protected RemoteRepository getRemoteRepository() throws RemoteException { + return null; + } + + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/URLRemoteRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/URLRemoteRepository.java new file mode 100644 index 00000000000..0e8fce5668b --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/URLRemoteRepository.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.rmi.repository; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; + +/** + * Proxy for a remote repository accessed via a URL. The configured URL is + * dereferenced lazily during each method call. Thus the resource pointed to + * by the URL does not need to exist when this class is instantiated. The + * resource can also be replaced with another remote repository instance + * during the lifetime of an instance of this class. + * + * @since 1.4 + */ +public class URLRemoteRepository extends ProxyRepository { + + /** + * Creates a proxy for the remote repository at the given URL. + * + * @param factory local adapter factory + * @param url URL of the remote repository + */ + public URLRemoteRepository(LocalAdapterFactory factory, URL url) { + super(new URLRemoteRepositoryFactory(factory, url)); + } + + /** + * Creates a proxy for the remote repository at the given URL. + * Uses {@link ClientAdapterFactory} as the default + * local adapter factory. + * + * @param url URL of the remote repository + */ + public URLRemoteRepository(URL url) { + this(new ClientAdapterFactory(), url); + } + + /** + * Creates a proxy for the remote repository at the given URL. + * Uses {@link ClientAdapterFactory} as the default + * local adapter factory. + * + * @param url URL of the remote repository + * @throws MalformedURLException if the given URL is malformed + */ + public URLRemoteRepository(String url) throws MalformedURLException { + this(new URL(url)); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/URLRemoteRepositoryFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/URLRemoteRepositoryFactory.java new file mode 100644 index 00000000000..633a98d1223 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/repository/URLRemoteRepositoryFactory.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.rmi.repository; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.URL; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; + +/** + * Factory that looks up a remote repository from a given URL. + * + * @since 1.4 + */ +public class URLRemoteRepositoryFactory + extends AbstractRemoteRepositoryFactory { + + /** + * URL of the remote repository. + */ + private final URL url; + + /** + * Creates a factory for looking up a remote repository from a URL. + * + * @param factory local adapter factory + * @param url URL or the remote repository + */ + public URLRemoteRepositoryFactory(LocalAdapterFactory factory, URL url) { + super(factory); + this.url = url; + } + + /** + * Looks up and returns a remote repository from the configured URL. + * + * @return remote repository reference + * @throws RepositoryException if the remote repository is not available + */ + protected RemoteRepository getRemoteRepository() + throws RepositoryException { + try { + ObjectInputStream input = new ObjectInputStream(url.openStream()); + try { + Object remote = input.readObject(); + if (remote instanceof RemoteRepository) { + return (RemoteRepository) remote; + } else if (remote == null) { + throw new RepositoryException( + "Remote repository not found: The resource at " + + url + " is null"); + } else { + throw new RepositoryException( + "Invalid remote repository: The resource at " + + url + " is an instance of " + + remote.getClass().getName()); + } + } finally { + input.close(); + } + } catch (ClassNotFoundException e) { + throw new RepositoryException( + "Invalid remote repository: The resource at " + url + + " is an instance of an unknown class", e); + } catch (IOException e) { + throw new RepositoryException( + "Remote repository not found: The resource at " + url + + " could not be retrieved", e); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/RemoteAdapterFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/RemoteAdapterFactory.java new file mode 100644 index 00000000000..58e6c940ff8 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/RemoteAdapterFactory.java @@ -0,0 +1,477 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.server; + +import java.rmi.RemoteException; +import java.security.Principal; +import java.util.Iterator; + +import javax.jcr.Item; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.ObservationManager; +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 javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.rmi.remote.RemoteEventCollection; +import org.apache.jackrabbit.rmi.remote.RemoteItem; +import org.apache.jackrabbit.rmi.remote.RemoteItemDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteLock; +import org.apache.jackrabbit.rmi.remote.RemoteLockManager; +import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; +import org.apache.jackrabbit.rmi.remote.RemoteObservationManager; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteQuery; +import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; +import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.remote.RemoteRow; +import org.apache.jackrabbit.rmi.remote.RemoteSession; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; +import org.apache.jackrabbit.rmi.remote.RemoteVersionManager; +import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlEntry; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlPolicy; +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; + +/** + * Factory interface for creating remote adapters for local resources. + * This interface defines how the local JCR interfaces are adapted to + * remote JCR-RMI references. The adaption mechanism can be + * modified (for example to add extra features) by changing the + * remote adapter factory used by the repository server. + *

    + * Note that the {@link ServerObject ServerObject} base class provides + * a number of utility methods designed to work with a remote adapter + * factory. Adapter implementations may want to inherit that functionality + * by subclassing from ServerObject. + * + * @see org.apache.jackrabbit.rmi.client.LocalAdapterFactory + * @see org.apache.jackrabbit.rmi.server.ServerAdapterFactory + * @see org.apache.jackrabbit.rmi.server.ServerObject + */ +public interface RemoteAdapterFactory { + + /** + * Returns the port number to which the server objects created by + * this factory are bound. This method is mostly used internally by + * the {@link ServerObject} constructor to determine which port number + * to use. + * + * @return port number, or 0 for a random port + */ + int getPortNumber(); + + /** + * Returns a remote adapter for the given local repository. + * + * @param repository local repository + * @return remote repository adapter + * @throws RemoteException on RMI errors + */ + RemoteRepository getRemoteRepository(Repository repository) + throws RemoteException; + + /** + * Returns a remote adapter for the given local session. + * + * @param session local session + * @return remote session adapter + * @throws RemoteException on RMI errors + */ + RemoteSession getRemoteSession(Session session) throws RemoteException; + + /** + * Returns a remote adapter for the given local workspace. + * + * @param workspace local workspace + * @return remote workspace adapter + * @throws RemoteException on RMI errors + */ + RemoteWorkspace getRemoteWorkspace(Workspace workspace) + throws RemoteException; + + /** + * Returns a remote adapter for the given local observation manager. + * + * @param observationManager local observation manager + * @return remote observation manager adapter + * @throws RemoteException on RMI errors + */ + RemoteObservationManager getRemoteObservationManager( + ObservationManager observationManager) + throws RemoteException; + + /** + * Returns a remote adapter for the given local namespace registry. + * + * @param registry local namespace registry + * @return remote namespace registry adapter + * @throws RemoteException on RMI errors + */ + RemoteNamespaceRegistry getRemoteNamespaceRegistry( + NamespaceRegistry registry) throws RemoteException; + + /** + * Returns a remote adapter for the given local node type manager. + * + * @param manager local node type manager + * @return remote node type manager adapter + * @throws RemoteException on RMI errors + */ + RemoteNodeTypeManager getRemoteNodeTypeManager(NodeTypeManager manager) + throws RemoteException; + + /** + * Returns a remote adapter for the given local item. This method + * will return an adapter that implements only the + * {@link Item Item} interface. The caller may want to introspect + * the local item to determine whether to use either the + * {@link #getRemoteNode(Node) getRemoteNode} or the + * {@link #getRemoteProperty(Property) getRemoteProperty} method instead. + * + * @param item local item + * @return remote item adapter + * @throws RemoteException on RMI errors + */ + RemoteItem getRemoteItem(Item item) throws RemoteException; + + /** + * Returns a remote adapter for the given local property. + * + * @param property local property + * @return remote property adapter + * @throws RemoteException on RMI errors + */ + RemoteProperty getRemoteProperty(Property property) throws RemoteException; + + /** + * Returns a remote adapter for the given local node. + * + * @param node local node + * @return remote node adapter + * @throws RemoteException on RMI errors + */ + RemoteNode getRemoteNode(Node node) throws RemoteException; + + /** + * Returns a remote adapter for the given local version. + * + * @param version local version + * @return remote version adapter + * @throws RemoteException on RMI errors + */ + RemoteVersion getRemoteVersion(Version version) throws RemoteException; + + /** + * Returns a remote adapter for the given local version history. + * + * @param versionHistory local version history + * @return remote version history adapter + * @throws RemoteException on RMI errors + */ + RemoteVersionHistory getRemoteVersionHistory(VersionHistory versionHistory) + throws RemoteException; + + /** + * Returns a remote adapter for the given local node type. + * + * @param type local node type + * @return remote node type adapter + * @throws RemoteException on RMI errors + */ + RemoteNodeType getRemoteNodeType(NodeType type) throws RemoteException; + + /** + * Returns a remote adapter for the given local item definition. + * This method will return an adapter that implements only the + * {@link ItemDefinition ItemDefinition} interface. The caller may want to introspect + * the local item definition to determine whether to use either the + * {@link #getRemoteNodeDefinition(NodeDefinition) getRemoteNodeDef} or the + * {@link #getRemotePropertyDefinition(PropertyDefinition) getRemotePropertyDef} + * method instead. + * + * @param def local item definition + * @return remote item definition adapter + * @throws RemoteException on RMI errors + */ + RemoteItemDefinition getRemoteItemDefinition(ItemDefinition def) throws RemoteException; + + /** + * Returns a remote adapter for the given local node definition. + * + * @param def local node definition + * @return remote node definition adapter + * @throws RemoteException on RMI errors + */ + RemoteNodeDefinition getRemoteNodeDefinition(NodeDefinition def) throws RemoteException; + + /** + * Returns a remote adapter for the given local property definition. + * + * @param def local property definition + * @return remote property definition adapter + * @throws RemoteException on RMI errors + */ + RemotePropertyDefinition getRemotePropertyDefinition(PropertyDefinition def) + throws RemoteException; + + /** + * Returns a remote adapter for the given local lock. + * + * @param lock local lock + * @return remote lock adapter + * @throws RemoteException on RMI errors + */ + RemoteLock getRemoteLock(Lock lock) throws RemoteException; + + /** + * Returns a remote adapter for the given local query manager. + * + * @param session current session + * @param manager local query manager + * @return remote query manager adapter + * @throws RemoteException on RMI errors + */ + RemoteQueryManager getRemoteQueryManager( + Session session, QueryManager manager) throws RemoteException; + + /** + * Returns a remote adapter for the given local query. + * + * @param query local query + * @return remote query adapter + * @throws RemoteException on RMI errors + */ + RemoteQuery getRemoteQuery(Query query) throws RemoteException; + + /** + * Returns a remote adapter for the given local query result. + * + * @param result local query result + * @return remote query result adapter + * @throws RemoteException on RMI errors + */ + RemoteQueryResult getRemoteQueryResult(QueryResult result) + throws RemoteException; + + /** + * Returns a remote adapter for the given local query row. + * + * @param row local query row + * @return remote query row adapter + * @throws RemoteException on RMI errors + */ + RemoteRow getRemoteRow(Row row) throws RemoteException; + + /** + * Returns a remote adapter for the given local events. + * + * @param listenerId The listener identifier to which the events are to be + * dispatched. + * @param events the local events + * @return remote event iterator adapter + * @throws RemoteException on RMI errors + */ + RemoteEventCollection getRemoteEvent(long listenerId, EventIterator events) + throws RemoteException; + + + /** + * Returns a remote adapter for the given local node iterator. + * + * @param iterator local node iterator + * @return remote iterator adapter + * @throws RemoteException on RMI errors + */ + RemoteIterator getRemoteNodeIterator(NodeIterator iterator) + throws RemoteException; + + /** + * Returns a remote adapter for the given local property iterator. + * + * @param iterator local property iterator + * @return remote iterator adapter + * @throws RemoteException on RMI errors + */ + RemoteIterator getRemotePropertyIterator(PropertyIterator iterator) + throws RemoteException; + + /** + * Returns a remote adapter for the given local version iterator. + * + * @param iterator local version iterator + * @return remote iterator adapter + * @throws RemoteException on RMI errors + */ + RemoteIterator getRemoteVersionIterator(VersionIterator iterator) + throws RemoteException; + + /** + * Returns a remote adapter for the given local node type iterator. + * + * @param iterator local node type iterator + * @return remote iterator adapter + * @throws RemoteException on RMI errors + */ + RemoteIterator getRemoteNodeTypeIterator(NodeTypeIterator iterator) + throws RemoteException; + + /** + * Returns a remote adapter for the given local row iterator. + * + * @param iterator local row iterator + * @return remote iterator adapter + * @throws RemoteException on RMI errors + */ + RemoteIterator getRemoteRowIterator(RowIterator iterator) + throws RemoteException; + + RemoteLockManager getRemoteLockManager(LockManager lockManager) + throws RemoteException; + + RemoteVersionManager getRemoteVersionManager(Session session, VersionManager versionManager) + throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @param acm local access control manager + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + RemoteAccessControlManager getRemoteAccessControlManager( + AccessControlManager acm) throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemotePrivilege getRemotePrivilege(final Privilege local) + throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemotePrivilege[] getRemotePrivilege(final Privilege[] local) + throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemoteAccessControlPolicy getRemoteAccessControlPolicy( + final AccessControlPolicy local) throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemoteAccessControlPolicy[] getRemoteAccessControlPolicy( + final AccessControlPolicy[] local) throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemoteIterator getRemoteAccessControlPolicyIterator( + AccessControlPolicyIterator iterator) throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemoteAccessControlEntry getRemoteAccessControlEntry( + final AccessControlEntry local) throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemoteAccessControlEntry[] getRemoteAccessControlEntry( + final AccessControlEntry[] local) throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemotePrincipal getRemotePrincipal(final Principal principal) + throws RemoteException; + + /** + * Returns a remote adapter for the given local access control manager. + * + * @return remote access control manager + * @throws RemoteException on RMI errors + */ + public RemoteIterator getRemotePrincipalIterator( + final Iterator principals) throws RemoteException; + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerAdapterFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerAdapterFactory.java new file mode 100644 index 00000000000..2551d6971d5 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerAdapterFactory.java @@ -0,0 +1,529 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.server; + +import java.rmi.RemoteException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Item; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.ObservationManager; +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 javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; +import javax.transaction.xa.XAResource; + +import org.apache.jackrabbit.rmi.remote.ArrayIterator; +import org.apache.jackrabbit.rmi.remote.BufferIterator; +import org.apache.jackrabbit.rmi.remote.RemoteEventCollection; +import org.apache.jackrabbit.rmi.remote.RemoteItem; +import org.apache.jackrabbit.rmi.remote.RemoteItemDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteLock; +import org.apache.jackrabbit.rmi.remote.RemoteLockManager; +import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; +import org.apache.jackrabbit.rmi.remote.RemoteObservationManager; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteQuery; +import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; +import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.remote.RemoteRow; +import org.apache.jackrabbit.rmi.remote.RemoteSession; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; +import org.apache.jackrabbit.rmi.remote.RemoteVersionManager; +import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlEntry; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlPolicy; +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; +import org.apache.jackrabbit.rmi.server.iterator.ServerNodeIterator; +import org.apache.jackrabbit.rmi.server.iterator.ServerNodeTypeIterator; +import org.apache.jackrabbit.rmi.server.iterator.ServerPropertyIterator; +import org.apache.jackrabbit.rmi.server.iterator.ServerRowIterator; +import org.apache.jackrabbit.rmi.server.iterator.ServerVersionIterator; +import org.apache.jackrabbit.rmi.server.principal.ServerGroup; +import org.apache.jackrabbit.rmi.server.principal.ServerPrincipal; +import org.apache.jackrabbit.rmi.server.principal.ServerPrincipalIterator; +import org.apache.jackrabbit.rmi.server.security.ServerAccessControlEntry; +import org.apache.jackrabbit.rmi.server.security.ServerAccessControlList; +import org.apache.jackrabbit.rmi.server.security.ServerAccessControlManager; +import org.apache.jackrabbit.rmi.server.security.ServerAccessControlPolicy; +import org.apache.jackrabbit.rmi.server.security.ServerAccessControlPolicyIterator; +import org.apache.jackrabbit.rmi.server.security.ServerPrivilege; + +/** + * Default implementation of the {@link RemoteAdapterFactory + * RemoteAdapterFactory} interface. This factory uses the server adapters + * defined in this package as the default adapter implementations. Subclasses + * can override or extend the default adapters by implementing the corresponding + * factory methods. + *

    + * The bufferSize property can be used to configure the size of the + * buffer used by iterators to speed up iterator traversal over the network. + */ +public class ServerAdapterFactory implements RemoteAdapterFactory { + + /** The default iterator buffer size. */ + private static final int DEFAULT_BUFFER_SIZE = 100; + + /** The buffer size of iterators created by this factory. */ + private int bufferSize = DEFAULT_BUFFER_SIZE; + + /** + * The port number for server objects. Initializes to the value of the + * org.apache.jackrabbit.rmi.port system property, or to 0 if + * the property is not set. Value 0 means that the server objects should use + * a random anonymous port. + */ + private int portNumber = Integer.getInteger( + "org.apache.jackrabbit.rmi.port", 0).intValue(); + + /** + * Returns the iterator buffer size. + * + * @return iterator buffer size + */ + public int getBufferSize() { + return bufferSize; + } + + /** + * Sets the iterator buffer size. + * + * @param bufferSize iterator buffer size + */ + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + /** + * Returns the port number for server objects. + * + * @return port number, or 0 for the default + */ + public int getPortNumber() { + return portNumber; + } + + /** + * Sets the port number for server objects. + * + * @param portNumber port number, or 0 for the default + */ + public void setPortNumber(int portNumber) { + this.portNumber = portNumber; + } + + /** + * Creates a {@link ServerRepository ServerRepository} instance. + * {@inheritDoc} + */ + public RemoteRepository getRemoteRepository(Repository repository) + throws RemoteException { + return new ServerRepository(repository, this); + } + + /** + * Creates a {@link ServerSession ServerSession} instance. In case the + * underlying session is transaction enabled, the remote interface is will + * be transaction enabled too through the {@link ServerXASession}. + * {@inheritDoc} + */ + public RemoteSession getRemoteSession(Session session) + throws RemoteException { + if (session instanceof XAResource) { + return new ServerXASession(session, (XAResource) session, this); + } else { + return new ServerSession(session, this); + } + } + + /** + * Creates a {@link ServerWorkspace ServerWorkspace} instance. {@inheritDoc} + */ + public RemoteWorkspace getRemoteWorkspace(Workspace workspace) + throws RemoteException { + return new ServerWorkspace(workspace, this); + } + + /** + * Creates a {@link ServerObservationManager ServerObservationManager} + * instance. {@inheritDoc} + */ + public RemoteObservationManager getRemoteObservationManager( + ObservationManager observationManager) throws RemoteException { + return new ServerObservationManager(observationManager, this); + } + + /** + * Creates a {@link ServerNamespaceRegistry ServerNamespaceRegistry} + * instance. {@inheritDoc} + */ + public RemoteNamespaceRegistry getRemoteNamespaceRegistry( + NamespaceRegistry registry) throws RemoteException { + return new ServerNamespaceRegistry(registry, this); + } + + /** + * Creates a {@link ServerNodeTypeManager ServerNodeTypeManager} instance. + * {@inheritDoc} + */ + public RemoteNodeTypeManager getRemoteNodeTypeManager( + NodeTypeManager manager) throws RemoteException { + return new ServerNodeTypeManager(manager, this); + } + + /** + * Creates a {@link ServerItem ServerItem} instance. {@inheritDoc} + */ + public RemoteItem getRemoteItem(Item item) throws RemoteException { + return new ServerItem(item, this); + } + + /** + * Creates a {@link ServerProperty ServerProperty} instance. {@inheritDoc} + */ + public RemoteProperty getRemoteProperty(Property property) + throws RemoteException { + return new ServerProperty(property, this); + } + + /** + * Creates a {@link ServerNode ServerNode} instance. {@inheritDoc} + */ + public RemoteNode getRemoteNode(Node node) throws RemoteException { + return new ServerNode(node, this); + } + + /** + * Creates a {@link ServerVersion ServerVersion} instance. {@inheritDoc} + */ + public RemoteVersion getRemoteVersion(Version version) + throws RemoteException { + return new ServerVersion(version, this); + } + + /** + * Creates a {@link ServerVersionHistory ServerVersionHistory} instance. + * {@inheritDoc} + */ + public RemoteVersionHistory getRemoteVersionHistory( + VersionHistory versionHistory) throws RemoteException { + return new ServerVersionHistory(versionHistory, this); + } + + /** + * Creates a {@link ServerNodeType ServerNodeType} instance. {@inheritDoc} + */ + public RemoteNodeType getRemoteNodeType(NodeType type) + throws RemoteException { + return new ServerNodeType(type, this); + } + + /** + * Creates a {@link ServerItemDefinition ServerItemDefinition} instance. + * {@inheritDoc} + */ + public RemoteItemDefinition getRemoteItemDefinition(ItemDefinition def) + throws RemoteException { + return new ServerItemDefinition(def, this); + } + + /** + * Creates a {@link ServerNodeDefinition ServerNodeDefinition} instance. + * {@inheritDoc} + */ + public RemoteNodeDefinition getRemoteNodeDefinition(NodeDefinition def) + throws RemoteException { + return new ServerNodeDefinition(def, this); + } + + /** + * Creates a {@link ServerPropertyDefinition ServerPropertyDefinition} + * instance. {@inheritDoc} + */ + public RemotePropertyDefinition getRemotePropertyDefinition( + PropertyDefinition def) throws RemoteException { + return new ServerPropertyDefinition(def, this); + } + + /** + * Creates a {@link ServerLock ServerLock} instance. {@inheritDoc} + */ + public RemoteLock getRemoteLock(Lock lock) throws RemoteException { + return new ServerLock(lock, this); + } + + /** + * Creates a {@link ServerQueryManager ServerQueryManager} instance. + * {@inheritDoc} + */ + public RemoteQueryManager getRemoteQueryManager(Session session, + QueryManager manager) throws RemoteException { + return new ServerQueryManager(session, manager, this); + } + + /** + * Creates a {@link ServerQuery ServerQuery} instance. {@inheritDoc} + */ + public RemoteQuery getRemoteQuery(Query query) throws RemoteException { + return new ServerQuery(query, this); + } + + /** + * Creates a {@link ServerQueryResult ServerQueryResult} instance. + * {@inheritDoc} + */ + public RemoteQueryResult getRemoteQueryResult(QueryResult result) + throws RemoteException { + return new ServerQueryResult(result, this); + } + + /** + * Creates a {@link ServerQueryResult ServerQueryResult} instance. + * {@inheritDoc} + */ + public RemoteRow getRemoteRow(Row row) throws RemoteException { + return new ServerRow(row, this); + } + + /** + * Creates a {@link ServerEventCollection ServerEventCollection} instances. + * {@inheritDoc} + */ + public RemoteEventCollection getRemoteEvent(long listenerId, + EventIterator events) throws RemoteException { + RemoteEventCollection.RemoteEvent[] remoteEvents; + if (events != null) { + List eventList = new ArrayList(); + while (events.hasNext()) { + Event event = events.nextEvent(); + eventList + .add(new ServerEventCollection.ServerEvent(event, this)); + } + remoteEvents = eventList.toArray(new RemoteEventCollection.RemoteEvent[eventList.size()]); + } else { + remoteEvents = new RemoteEventCollection.RemoteEvent[0]; // for + // safety + } + + return new ServerEventCollection(listenerId, remoteEvents, this); + } + + /** + * Optimizes the given remote iterator for transmission across the network. + * This method retrieves the first set of elements from the iterator by + * calling {@link RemoteIterator#nextObjects()} and then asks for the total + * size of the iterator. If the size is unkown or greater than the length of + * the retrieved array, then the elements, the size, and the remote iterator + * reference are wrapped into a {@link BufferIterator} instance that gets + * passed over the network. If the retrieved array of elements contains all + * the elements in the iterator, then the iterator instance is discarded and + * just the elements are wrapped into a {@link ArrayIterator} instance to be + * passed to the client. + *

    + * Subclasses can override this method to provide alternative optimizations. + * + * @param remote remote iterator + * @return optimized remote iterator + * @throws RemoteException on RMI errors + */ + protected RemoteIterator optimizeIterator(RemoteIterator remote) + throws RemoteException { + Object[] elements = remote.nextObjects(); + long size = remote.getSize(); + if (size == -1 || (elements != null && size > elements.length)) { + return new BufferIterator(elements, size, remote); + } else { + return new ArrayIterator(elements); + } + } + + /** + * Creates a {@link ServerNodeIterator} instance. {@inheritDoc} + */ + public RemoteIterator getRemoteNodeIterator(NodeIterator iterator) + throws RemoteException { + return optimizeIterator(new ServerNodeIterator(iterator, this, + bufferSize)); + } + + /** + * Creates a {@link ServerPropertyIterator} instance. {@inheritDoc} + */ + public RemoteIterator getRemotePropertyIterator(PropertyIterator iterator) + throws RemoteException { + return optimizeIterator(new ServerPropertyIterator(iterator, this, + bufferSize)); + } + + /** + * Creates a {@link ServerVersionIterator} instance. {@inheritDoc} + */ + public RemoteIterator getRemoteVersionIterator(VersionIterator iterator) + throws RemoteException { + return optimizeIterator(new ServerVersionIterator(iterator, this, + bufferSize)); + } + + /** + * Creates a {@link ServerNodeTypeIterator} instance. {@inheritDoc} + */ + public RemoteIterator getRemoteNodeTypeIterator(NodeTypeIterator iterator) + throws RemoteException { + return optimizeIterator(new ServerNodeTypeIterator(iterator, this, + bufferSize)); + } + + /** + * Creates a {@link ServerRowIterator} instance. {@inheritDoc} + */ + public RemoteIterator getRemoteRowIterator(RowIterator iterator) + throws RemoteException { + return optimizeIterator(new ServerRowIterator(iterator, this, + bufferSize)); + } + + public RemoteLockManager getRemoteLockManager(LockManager lockManager) + throws RemoteException { + return new ServerLockManager(lockManager, this); + } + + public RemoteVersionManager getRemoteVersionManager(Session session, + VersionManager versionManager) throws RemoteException { + return new ServerVersionManager(session, versionManager, this); + } + + /** + * Creates a + * {@link org.apache.jackrabbit.rmi.server.security.ServerAccessControlManager} + * instance. {@inheritDoc} + */ + public RemoteAccessControlManager getRemoteAccessControlManager( + AccessControlManager acm) throws RemoteException { + return new ServerAccessControlManager(acm, this); + } + + public RemotePrivilege getRemotePrivilege(final Privilege local) + throws RemoteException { + return new ServerPrivilege(local, this); + } + + public RemotePrivilege[] getRemotePrivilege(final Privilege[] local) + throws RemoteException { + RemotePrivilege[] remote = new RemotePrivilege[local.length]; + for (int i = 0; i < remote.length; i++) { + remote[i] = getRemotePrivilege(local[i]); + } + return remote; + } + + public RemoteAccessControlPolicy getRemoteAccessControlPolicy( + final AccessControlPolicy local) throws RemoteException { + if (local instanceof AccessControlList) { + return new ServerAccessControlList((AccessControlList) local, this); + } + return new ServerAccessControlPolicy(local, this); + } + + public RemoteAccessControlPolicy[] getRemoteAccessControlPolicy( + final AccessControlPolicy[] local) throws RemoteException { + RemoteAccessControlPolicy[] remote = new RemoteAccessControlPolicy[local.length]; + for (int i = 0; i < remote.length; i++) { + remote[i] = getRemoteAccessControlPolicy(local[i]); + } + return remote; + } + + /** + * Creates a {@link ServerNodeIterator} instance. {@inheritDoc} + */ + public RemoteIterator getRemoteAccessControlPolicyIterator( + AccessControlPolicyIterator iterator) throws RemoteException { + return optimizeIterator(new ServerAccessControlPolicyIterator(iterator, + this, bufferSize)); + } + + public RemoteAccessControlEntry getRemoteAccessControlEntry( + final AccessControlEntry local) throws RemoteException { + return new ServerAccessControlEntry(local, this); + } + + public RemoteAccessControlEntry[] getRemoteAccessControlEntry( + final AccessControlEntry[] local) throws RemoteException { + RemoteAccessControlEntry[] remote = new RemoteAccessControlEntry[local.length]; + for (int i = 0; i < remote.length; i++) { + remote[i] = getRemoteAccessControlEntry(local[i]); + } + return remote; + } + + public RemotePrincipal getRemotePrincipal(final Principal principal) throws RemoteException { + if (ServerGroup.isGroup(principal)) { + return new ServerGroup(principal, this); + } + + return new ServerPrincipal(principal, this); + } + + public RemoteIterator getRemotePrincipalIterator( + Iterator principals) throws RemoteException { + return optimizeIterator(new ServerPrincipalIterator(principals, this, + bufferSize)); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerEventCollection.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerEventCollection.java new file mode 100644 index 00000000000..c242f62765d --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerEventCollection.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.rmi.server; + +import java.rmi.RemoteException; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.rmi.remote.RemoteEventCollection; + +/** + * The ServerEventCollection class implements the + * {@link org.apache.jackrabbit.rmi.remote.RemoteEventCollection}event to + * actually sent the server-side event to the client. + *

    + * This class does not directly relate to any JCR class because beside the list + * of events the unique identifier of the client-side listener has to be + * provided such that the receiving listener may be identified on the + * client-side. + */ +public class ServerEventCollection extends ServerObject implements + RemoteEventCollection { + + /** The unique identifier of the receiving listener */ + private final long listenerId; + + /** + * The list of + * {@link org.apache.jackrabbit.rmi.remote.RemoteEventCollection.RemoteEvent}. + */ + private final RemoteEvent[] events; + + /** + * Creates an instance of this class. + * + * @param listenerId The unique identifier of the client-side listener to + * which the events should be sent. + * @param events The list of {@link RemoteEvent remote events}. + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + ServerEventCollection( + long listenerId, RemoteEvent[] events, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + + this.listenerId = listenerId; + this.events = events; + } + + /** {@inheritDoc} */ + public long getListenerId() { + return listenerId; + } + + /** {@inheritDoc} */ + public RemoteEvent[] getEvents() { + return events; + } + + /** + * Server side implementation of the {@link RemoteEvent} interface. + * + * {@inheritDoc} + */ + public static class ServerEvent extends ServerObject implements RemoteEvent { + + /** The adapted local event. */ + private Event event; + + /** + * Creates an instance of this class. + * @param type The event type. + * @param path The absolute path to the underlying item. + * @param userId The userID of the originating session. + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + ServerEvent(Event event, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.event = event; + } + + /** {@inheritDoc} */ + public String getPath() throws RepositoryException { + return event.getPath(); + } + + /** {@inheritDoc} */ + public int getType() { + return event.getType(); + } + + /** {@inheritDoc} */ + public String getUserID() { + return event.getUserID(); + } + + /** {@inheritDoc} */ + public String getIdentifier() throws RepositoryException, + RemoteException { + return event.getIdentifier(); + } + + /** {@inheritDoc} */ + public Map getInfo() throws RepositoryException, RemoteException { + return event.getInfo(); + } + + /** {@inheritDoc} */ + public String getUserData() throws RepositoryException, RemoteException { + return event.getUserData(); + } + + /** {@inheritDoc} */ + public long getDate() throws RepositoryException, RemoteException { + return event.getDate(); + } + } +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerItem.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerItem.java similarity index 90% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerItem.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerItem.java index 3a8ac36f350..2ced6414a1a 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerItem.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerItem.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -33,7 +33,6 @@ * and {@link org.apache.jackrabbit.rmi.server.ServerNode ServerNode} * adapters. * - * @author Jukka Zitting * @see javax.jcr.Item * @see org.apache.jackrabbit.rmi.remote.RemoteItem */ diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerItemDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerItemDefinition.java new file mode 100644 index 00000000000..486fc925d83 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerItemDefinition.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.rmi.remote.RemoteItemDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; + +/** + * Remote adapter for the JCR {@link javax.jcr.nodetype.ItemDefinition ItemDefinition} + * interface. This class makes a local item definition available as an + * RMI service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteItemDefinition RemoteItemDefinition} + * interface. Used mainly as the base class for the + * {@link org.apache.jackrabbit.rmi.server.ServerPropertyDefinition ServerPropertyDefinition} + * and + * {@link org.apache.jackrabbit.rmi.server.ServerNodeDefinition ServerNodeDefinition} + * adapters. + * + * @see javax.jcr.nodetype.ItemDefinition + * @see org.apache.jackrabbit.rmi.remote.RemoteItemDefinition + */ +public class ServerItemDefinition extends ServerObject implements RemoteItemDefinition { + + /** The adapted local item definition. */ + private ItemDefinition def; + + /** + * Creates a remote adapter for the given local item definition. + * + * @param def local item definition + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerItemDefinition(ItemDefinition def, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.def = def; + } + + /** {@inheritDoc} */ + public RemoteNodeType getDeclaringNodeType() throws RemoteException { + NodeType nt = def.getDeclaringNodeType(); + if (nt == null) { + return null; + } else { + return getFactory().getRemoteNodeType(nt); + } + } + + /** {@inheritDoc} */ + public String getName() throws RemoteException { + return def.getName(); + } + + /** {@inheritDoc} */ + public boolean isAutoCreated() throws RemoteException { + return def.isAutoCreated(); + } + + /** {@inheritDoc} */ + public boolean isMandatory() throws RemoteException { + return def.isMandatory(); + } + + /** {@inheritDoc} */ + public int getOnParentVersion() throws RemoteException { + return def.getOnParentVersion(); + } + + /** {@inheritDoc} */ + public boolean isProtected() throws RemoteException { + return def.isProtected(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerLock.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerLock.java new file mode 100644 index 00000000000..af268afec43 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerLock.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.lock.Lock; + +import org.apache.jackrabbit.rmi.remote.RemoteLock; +import org.apache.jackrabbit.rmi.remote.RemoteNode; + +/** + * Remote adapter for the JCR {@link javax.jcr.lock.Lock Lock} interface. + * This class makes a local lock available as an RMI service using + * the {@link org.apache.jackrabbit.rmi.remote.RemoteLock RemoteLock} + * interface. + * + * @see javax.jcr.lock.Lock + * @see org.apache.jackrabbit.rmi.remote.RemoteLock + */ +public class ServerLock extends ServerObject implements RemoteLock { + + /** The adapted local lock. */ + private Lock lock; + + /** + * Creates a remote adapter for the given local lock. + * + * @param lock local lock + * @throws RemoteException on RMI errors + */ + public ServerLock(Lock lock, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.lock = lock; + } + + /** {@inheritDoc} */ + public RemoteNode getNode() throws RemoteException { + return getRemoteNode(lock.getNode()); + } + + /** {@inheritDoc} */ + public String getLockOwner() throws RemoteException { + return lock.getLockOwner(); + } + + /** {@inheritDoc} */ + public boolean isDeep() throws RemoteException { + return lock.isDeep(); + } + + /** {@inheritDoc} */ + public String getLockToken() throws RemoteException { + return lock.getLockToken(); + } + + /** {@inheritDoc} */ + public boolean isLive() throws RepositoryException, RemoteException { + return lock.isLive(); + } + + /** {@inheritDoc} */ + public void refresh() throws RepositoryException, RemoteException { + lock.refresh(); + } + + /** {@inheritDoc} */ + public boolean isSessionScoped() throws RemoteException { + return lock.isSessionScoped(); + } + + /** {@inheritDoc} */ + public long getSecondsRemaining() throws RepositoryException, RemoteException { + return lock.getSecondsRemaining(); + } + + /** {@inheritDoc} */ + public boolean isLockOwningSession() throws RemoteException { + return lock.isLockOwningSession(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerLockManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerLockManager.java new file mode 100644 index 00000000000..1708b10527f --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerLockManager.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockManager; + +import org.apache.jackrabbit.rmi.remote.RemoteLock; +import org.apache.jackrabbit.rmi.remote.RemoteLockManager; + +public class ServerLockManager extends ServerObject + implements RemoteLockManager { + + /** The adapted local lock manager. */ + private LockManager manager; + + public ServerLockManager(LockManager manager, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.manager = manager; + } + + public String[] getLockTokens() throws RepositoryException { + try { + return manager.getLockTokens(); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public void addLockToken(String lockToken) throws RepositoryException { + try { + manager.addLockToken(lockToken); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public void removeLockToken(String lockToken) throws RepositoryException { + try { + manager.removeLockToken(lockToken); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public boolean isLocked(String absPath) throws RepositoryException { + try { + return manager.isLocked(absPath); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public boolean holdsLock(String absPath) throws RepositoryException { + try { + return manager.holdsLock(absPath); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteLock getLock(String absPath) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteLock(manager.getLock(absPath)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteLock lock( + String absPath, boolean isDeep, boolean isSessionScoped, + long timeoutHint, String ownerInfo) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteLock(manager.lock( + absPath, isDeep, isSessionScoped, timeoutHint, ownerInfo)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public void unlock(String absPath) throws RepositoryException { + try { + manager.unlock(absPath); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNamespaceRegistry.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNamespaceRegistry.java similarity index 88% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNamespaceRegistry.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNamespaceRegistry.java index 95608dbbf3d..8ddea6ca2b9 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerNamespaceRegistry.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNamespaceRegistry.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -31,7 +31,6 @@ * {@link org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry RemoteNamespaceRegistry} * interface. * - * @author Jukka Zitting * @see javax.jcr.NamespaceRegistry * @see org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry */ diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNode.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNode.java new file mode 100644 index 00000000000..b611d4278b7 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNode.java @@ -0,0 +1,686 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.lock.Lock; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.rmi.remote.RemoteItem; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteLock; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; + +/** + * Remote adapter for the JCR {@link javax.jcr.Node Node} interface. + * This class makes a local node available as an RMI service using + * the {@link org.apache.jackrabbit.rmi.remote.RemoteNode RemoteNode} + * interface. + * + * @see javax.jcr.Node + * @see org.apache.jackrabbit.rmi.remote.RemoteNode + */ +public class ServerNode extends ServerItem implements RemoteNode { + + /** The adapted local node. */ + private Node node; + + /** + * Creates a remote adapter for the given local node. + * + * @param node local node + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerNode(Node node, RemoteAdapterFactory factory) + throws RemoteException { + super(node, factory); + this.node = node; + } + + /** {@inheritDoc} */ + public RemoteNode addNode(String path) + throws RepositoryException, RemoteException { + try { + return getRemoteNode(node.addNode(path)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNode addNode(String path, String type) + throws RepositoryException, RemoteException { + try { + return getRemoteNode(node.addNode(path, type)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteProperty getProperty(String path) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteProperty(node.getProperty(path)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getProperties() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemotePropertyIterator(node.getProperties()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteItem getPrimaryItem() + throws RepositoryException, RemoteException { + try { + return getRemoteItem(node.getPrimaryItem()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getProperties(String pattern) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemotePropertyIterator(node.getProperties(pattern)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getProperties(String[] globs) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemotePropertyIterator(node.getProperties(globs)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getReferences() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemotePropertyIterator(node.getReferences()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getReferences(String name) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemotePropertyIterator(node.getReferences(name)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getIdentifier() throws RepositoryException, RemoteException { + try { + return node.getIdentifier(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + @SuppressWarnings("deprecation") + public String getUUID() throws RepositoryException, RemoteException { + try { + return node.getUUID(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasNodes() throws RepositoryException, RemoteException { + try { + return node.hasNodes(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasProperties() throws RepositoryException, RemoteException { + try { + return node.hasProperties(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasProperty(String path) + throws RepositoryException, RemoteException { + try { + return node.hasProperty(path); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNodeType[] getMixinNodeTypes() + throws RepositoryException, RemoteException { + try { + return getRemoteNodeTypeArray(node.getMixinNodeTypes()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNodeType getPrimaryNodeType() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeType(node.getPrimaryNodeType()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isNodeType(String type) + throws RepositoryException, RemoteException { + try { + return node.isNodeType(type); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getNodes() throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeIterator(node.getNodes()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getNodes(String pattern) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeIterator(node.getNodes(pattern)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getNodes(String[] globs) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeIterator(node.getNodes(globs)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNode getNode(String path) + throws RepositoryException, RemoteException { + try { + return getRemoteNode(node.getNode(path)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasNode(String path) + throws RepositoryException, RemoteException { + try { + return node.hasNode(path); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteProperty setProperty(String name, Value value) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteProperty(node.setProperty(name, value)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteProperty setProperty(String name, Value value, int type) + throws RepositoryException, RemoteException { + try { + Property property = node.setProperty(name, value, type); + if (property == null) { + return null; + } else { + return getFactory().getRemoteProperty(property); + } + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void addMixin(String name) + throws RepositoryException, RemoteException { + try { + node.addMixin(name); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean canAddMixin(String name) + throws RepositoryException, RemoteException { + try { + return node.canAddMixin(name); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeMixin(String name) + throws RepositoryException, RemoteException { + try { + node.removeMixin(name); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void orderBefore(String src, String dst) + throws RepositoryException, RemoteException { + try { + node.orderBefore(src, dst); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteProperty setProperty(String name, Value[] values) + throws RepositoryException, RemoteException { + try { + Property property = node.setProperty(name, values); + if (property == null) { + return null; + } else { + return getFactory().getRemoteProperty(property); + } + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNodeDefinition getDefinition() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeDefinition(node.getDefinition()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion checkin() throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersion(node.checkin()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void checkout() throws RepositoryException, RemoteException { + try { + node.checkout(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getCorrespondingNodePath(String workspace) + throws RepositoryException, RemoteException { + try { + return node.getCorrespondingNodePath(workspace); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public int getIndex() throws RepositoryException, RemoteException { + try { + return node.getIndex(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator merge(String workspace, boolean bestEffort) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeIterator(node.merge(workspace, bestEffort)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void cancelMerge(String versionUUID) + throws RepositoryException, RemoteException { + try { + node.cancelMerge(getVersionByUUID(versionUUID)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void doneMerge(String versionUUID) + throws RepositoryException, RemoteException { + try { + node.doneMerge(getVersionByUUID(versionUUID)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void restore(String version, boolean removeExisting) + throws RepositoryException, RemoteException { + try { + node.restore(version, removeExisting); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void restoreByUUID(String versionUUID, boolean removeExisting) + throws RepositoryException, RemoteException { + try { + node.restore(getVersionByUUID(versionUUID), removeExisting); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void restore(String versionUUID, String path, boolean removeExisting) + throws RepositoryException, RemoteException { + try { + node.restore(getVersionByUUID(versionUUID), path, removeExisting); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void restoreByLabel(String label, boolean removeExisting) + throws RepositoryException, RemoteException { + try { + node.restoreByLabel(label, removeExisting); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void update(String workspace) + throws RepositoryException, RemoteException { + try { + node.update(workspace); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean holdsLock() throws RepositoryException, RemoteException { + try { + return node.holdsLock(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isCheckedOut() throws RepositoryException, RemoteException { + try { + return node.isCheckedOut(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersionHistory getVersionHistory() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersionHistory(node.getVersionHistory()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion getBaseVersion() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersion(node.getBaseVersion()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean isLocked() throws RepositoryException, RemoteException { + try { + return node.isLocked(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteProperty setProperty(String name, Value[] values, int type) + throws RepositoryException, RemoteException { + try { + Property property = node.setProperty(name, values, type); + return getFactory().getRemoteProperty(property); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void unlock() throws RepositoryException, RemoteException { + try { + node.unlock(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteLock getLock() throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteLock(node.getLock()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteLock lock(boolean isDeep, boolean isSessionScoped) + throws RepositoryException, RemoteException { + try { + Lock lock = node.lock(isDeep, isSessionScoped); + return getFactory().getRemoteLock(lock); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getSharedSet() + throws RepositoryException, RemoteException { + try { + NodeIterator sharedSet = node.getSharedSet(); + return getFactory().getRemoteNodeIterator(sharedSet); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void followLifecycleTransition(String transition) + throws RepositoryException, RemoteException { + try { + node.followLifecycleTransition(transition); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getAllowedLifecycleTransistions() + throws RepositoryException, RemoteException { + try { + return node.getAllowedLifecycleTransistions(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getWeakReferences() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemotePropertyIterator(node.getWeakReferences()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getWeakReferences(String name) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemotePropertyIterator(node.getWeakReferences(name)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeShare() throws RepositoryException, RemoteException { + try { + node.removeShare(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeSharedSet() throws RepositoryException, RemoteException { + try { + node.removeSharedSet(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void setPrimaryType(String nodeTypeName) + throws RepositoryException, RemoteException { + try { + node.setPrimaryType(nodeTypeName); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + //---------- Implementation helper ----------------------------------------- + + /** + * Returns the {@link Version} instance for the given UUID. + * + * @param versionUUID The UUID of the version. + * + * @return The version node. + * + * @throws RepositoryException if an error occurrs accessing the version + * node or if the UUID does not denote a version. + */ + protected Version getVersionByUUID(String versionUUID) + throws RepositoryException { + + // get the version node by its UUID from the version history's session + Session session = node.getSession(); + Node versionNode = session.getNodeByUUID(versionUUID); + + // check whether the node is a session, which it should be according + // to the spec (methods returning nodes should automatically return + // the correct type). + if (versionNode instanceof Version) { + return (Version) versionNode; + } + + // otherwise fail + throw new RepositoryException("Cannot find version " + versionUUID); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeDefinition.java new file mode 100644 index 00000000000..eab72362eac --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeDefinition.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; + +/** + * Remote adapter for the JCR {@link javax.jcr.nodetype.NodeDefinition NodeDefinition} + * interface. This class makes a local node definition available as an + * RMI service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition RemoteNodeDefinition} + * interface. + * + * @see javax.jcr.nodetype.NodeDefinition + * @see org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition + */ +public class ServerNodeDefinition extends ServerItemDefinition implements RemoteNodeDefinition { + + /** The adapted node definition. */ + private NodeDefinition def; + + /** + * Creates a remote adapter for the given local node definition. + * + * @param def local node definition + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerNodeDefinition(NodeDefinition def, RemoteAdapterFactory factory) + throws RemoteException { + super(def, factory); + this.def = def; + } + + /** {@inheritDoc} */ + public RemoteNodeType[] getRequiredPrimaryTypes() throws RemoteException { + return getRemoteNodeTypeArray(def.getRequiredPrimaryTypes()); + } + + /** {@inheritDoc} */ + public RemoteNodeType getDefaultPrimaryType() throws RemoteException { + NodeType nt = def.getDefaultPrimaryType(); + if (nt == null) { + return null; + } else { + return getFactory().getRemoteNodeType(def.getDefaultPrimaryType()); + } + } + + /** {@inheritDoc} */ + public boolean allowsSameNameSiblings() throws RemoteException { + return def.allowsSameNameSiblings(); + } + + /** {@inheritDoc} */ + public String getDefaultPrimaryTypeName() throws RemoteException { + return def.getDefaultPrimaryTypeName(); + } + + /** {@inheritDoc} */ + public String[] getRequiredPrimaryTypeNames() throws RemoteException { + return def.getRequiredPrimaryTypeNames(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeType.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeType.java new file mode 100644 index 00000000000..918d06f32a2 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeType.java @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.Value; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteNodeDefinition; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; + +/** + * Remote adapter for the JCR {@link javax.jcr.nodetype.NodeType NodeType} + * interface. This class makes a local node type available as an RMI service + * using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeType RemoteNodeType} + * interface. + * + * @see javax.jcr.nodetype.NodeType + * @see org.apache.jackrabbit.rmi.remote.RemoteNodeType + */ +public class ServerNodeType extends ServerObject implements RemoteNodeType { + + /** The adapted local node type. */ + private NodeType type; + + /** + * Creates a remote adapter for the given local node type. + * + * @param type local node type + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerNodeType(NodeType type, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.type = type; + } + + /** + * Utility method for creating an array of remote references for + * local node definitions. The remote references are created using the + * remote adapter factory. + *

    + * A null input is treated as an empty array. + * + * @param defs local node definition array + * @return remote node definition array + * @throws RemoteException on RMI errors + */ + private RemoteNodeDefinition[] getRemoteNodeDefArray(NodeDefinition[] defs) + throws RemoteException { + if (defs != null) { + RemoteNodeDefinition[] remotes = + new RemoteNodeDefinition[defs.length]; + for (int i = 0; i < defs.length; i++) { + remotes[i] = getFactory().getRemoteNodeDefinition(defs[i]); + } + return remotes; + } else { + return new RemoteNodeDefinition[0]; // for safety + } + } + + /** + * Utility method for creating an array of remote references for + * local property definitions. The remote references are created using the + * remote adapter factory. + *

    + * A null input is treated as an empty array. + * + * @param defs local property definition array + * @return remote property definition array + * @throws RemoteException on RMI errors + */ + private RemotePropertyDefinition[] getRemotePropertyDefArray( + PropertyDefinition[] defs) throws RemoteException { + if (defs != null) { + RemotePropertyDefinition[] remotes = + new RemotePropertyDefinition[defs.length]; + for (int i = 0; i < defs.length; i++) { + remotes[i] = getFactory().getRemotePropertyDefinition(defs[i]); + } + return remotes; + } else { + return new RemotePropertyDefinition[0]; // for safety + } + } + + + /** {@inheritDoc} */ + public String getName() throws RemoteException { + return type.getName(); + } + + /** {@inheritDoc} */ + public boolean isMixin() throws RemoteException { + return type.isMixin(); + } + + /** {@inheritDoc} */ + public boolean isAbstract() throws RemoteException { + return type.isAbstract(); + } + + /** {@inheritDoc} */ + public boolean hasOrderableChildNodes() throws RemoteException { + return type.hasOrderableChildNodes(); + } + + /** {@inheritDoc} */ + public RemoteNodeType[] getSupertypes() throws RemoteException { + return getRemoteNodeTypeArray(type.getSupertypes()); + } + + /** {@inheritDoc} */ + public RemoteNodeType[] getDeclaredSupertypes() throws RemoteException { + return getRemoteNodeTypeArray(type.getDeclaredSupertypes()); + } + + /** {@inheritDoc} */ + public boolean isNodeType(String type) throws RemoteException { + return this.type.isNodeType(type); + } + + /** {@inheritDoc} */ + public RemotePropertyDefinition[] getPropertyDefs() throws RemoteException { + PropertyDefinition[] defs = type.getPropertyDefinitions(); + return getRemotePropertyDefArray(defs); + } + + /** {@inheritDoc} */ + public RemotePropertyDefinition[] getDeclaredPropertyDefs() + throws RemoteException { + PropertyDefinition[] defs = type.getDeclaredPropertyDefinitions(); + return getRemotePropertyDefArray(defs); + } + + /** {@inheritDoc} */ + public RemoteNodeDefinition[] getChildNodeDefs() throws RemoteException { + return getRemoteNodeDefArray(type.getChildNodeDefinitions()); + } + + /** {@inheritDoc} */ + public RemoteNodeDefinition[] getDeclaredChildNodeDefs() throws RemoteException { + return getRemoteNodeDefArray(type.getDeclaredChildNodeDefinitions()); + } + + /** {@inheritDoc} */ + public boolean canSetProperty(String name, Value value) + throws RemoteException { + return type.canSetProperty(name, value); + } + + /** {@inheritDoc} */ + public boolean canSetProperty(String name, Value[] values) + throws RemoteException { + return type.canSetProperty(name, values); + } + + /** {@inheritDoc} */ + public boolean canAddChildNode(String name) throws RemoteException { + return type.canAddChildNode(name); + } + + /** {@inheritDoc} */ + public boolean canAddChildNode(String name, String type) + throws RemoteException { + return this.type.canAddChildNode(name, type); + } + + /** {@inheritDoc} */ + public boolean canRemoveItem(String name) throws RemoteException { + return type.canRemoveItem(name); + } + + /** {@inheritDoc} */ + public String getPrimaryItemName() throws RemoteException { + return type.getPrimaryItemName(); + } + + /** {@inheritDoc} */ + public boolean canRemoveNode(String nodeName) { + return type.canRemoveNode(nodeName); + } + + /** {@inheritDoc} */ + public boolean canRemoveProperty(String propertyName) { + return type.canRemoveProperty(propertyName); + } + + /** {@inheritDoc} */ + public String[] getDeclaredSupertypeNames() { + return type.getDeclaredSupertypeNames(); + } + + /** {@inheritDoc} */ + public boolean isQueryable() { + return type.isQueryable(); + } + + /** {@inheritDoc} */ + public RemoteIterator getDeclaredSubtypes() throws RemoteException { + return getFactory().getRemoteNodeTypeIterator(type.getDeclaredSubtypes()); + } + + /** {@inheritDoc} */ + public RemoteIterator getSubtypes() throws RemoteException { + return getFactory().getRemoteNodeTypeIterator(type.getSubtypes()); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeTypeManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeTypeManager.java new file mode 100644 index 00000000000..bb3fba80908 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerNodeTypeManager.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; + +/** + * Remote adapter for the JCR + * {@link javax.jcr.nodetype.NodeTypeManager NodeTypeManager} + * interface. This class makes a local node type manager available as an + * RMI service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager RemoteNodeTypeManager} + * interface. + * + * @see javax.jcr.nodetype.NodeTypeManager + * @see org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager + */ +public class ServerNodeTypeManager extends ServerObject + implements RemoteNodeTypeManager { + + /** The adapted local node type manager. */ + private NodeTypeManager manager; + + /** + * Creates a remote adapter for the given local node type manager. + * + * @param manager local node type manager + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerNodeTypeManager( + NodeTypeManager manager, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.manager = manager; + } + + /** {@inheritDoc} */ + public RemoteNodeType getNodeType(String name) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeType(manager.getNodeType(name)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getAllNodeTypes() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeTypeIterator( + manager.getAllNodeTypes()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getPrimaryNodeTypes() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeTypeIterator( + manager.getPrimaryNodeTypes()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getMixinNodeTypes() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeTypeIterator( + manager.getMixinNodeTypes()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + public boolean hasNodeType(String name) + throws RepositoryException, RemoteException { + try { + return manager.hasNodeType(name); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + public void unregisterNodeTypes(String[] names) + throws RepositoryException, RemoteException { + try { + manager.unregisterNodeTypes(names); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerObject.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerObject.java new file mode 100644 index 00000000000..e5be43c2498 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerObject.java @@ -0,0 +1,261 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.server; + +import java.io.Serializable; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.MergeException; +import javax.jcr.NamespaceException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.rmi.remote.RemoteItem; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteNodeType; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; + +/** + * Base class for remote adapters. The purpose of this class is to + * centralize the handling of the RemoteAdapterFactory instance used + * to instantiate new server adapters. + */ +public class ServerObject extends UnicastRemoteObject { + + /** Factory for creating server adapters. */ + private RemoteAdapterFactory factory; + + /** + * Creates a basic server adapter that uses the given factory + * to create new adapters. + * + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + protected ServerObject(RemoteAdapterFactory factory) + throws RemoteException { + super(factory.getPortNumber()); + this.factory = factory; + } + + /** + * Returns the remote adapter factory used to create new adapters. + * + * @return remote adapter factory + */ + protected RemoteAdapterFactory getFactory() { + return factory; + } + + /** + * Returns a cleaned version of the given exception. In some cases + * the underlying repository implementation may throw exceptions + * that are either unserializable, use exception subclasses that are + * only locally available, contain references to unserializable or + * only locally available classes. This method returns a cleaned + * version of such an exception. The returned exception contains only + * the message string from the original exception, and uses the public + * JCR exception class that most specifically matches the original + * exception. + * + * @param ex the original exception + * @return clean exception + */ + protected RepositoryException getRepositoryException( + RepositoryException ex) { + if (ex instanceof AccessDeniedException) { + return new AccessDeniedException(ex.getMessage()); + } else if (ex instanceof ConstraintViolationException) { + return new ConstraintViolationException(ex.getMessage()); + } else if (ex instanceof InvalidItemStateException) { + return new InvalidItemStateException(ex.getMessage()); + } else if (ex instanceof InvalidQueryException) { + return new InvalidQueryException(ex.getMessage()); + } else if (ex instanceof InvalidSerializedDataException) { + return new InvalidSerializedDataException(ex.getMessage()); + } else if (ex instanceof ItemExistsException) { + return new ItemExistsException(ex.getMessage()); + } else if (ex instanceof ItemNotFoundException) { + return new ItemNotFoundException(ex.getMessage()); + } else if (ex instanceof LockException) { + return new LockException(ex.getMessage()); + } else if (ex instanceof LoginException) { + return new LoginException(ex.getMessage()); + } else if (ex instanceof MergeException) { + return new MergeException(ex.getMessage()); + } else if (ex instanceof NamespaceException) { + return new NamespaceException(ex.getMessage()); + } else if (ex instanceof NoSuchNodeTypeException) { + return new NoSuchNodeTypeException(ex.getMessage()); + } else if (ex instanceof NoSuchWorkspaceException) { + return new NoSuchWorkspaceException(ex.getMessage()); + } else if (ex instanceof PathNotFoundException) { + return new PathNotFoundException(ex.getMessage()); + } else if (ex instanceof ReferentialIntegrityException) { + return new ReferentialIntegrityException(ex.getMessage()); + } else if (ex instanceof UnsupportedRepositoryOperationException) { + return new UnsupportedRepositoryOperationException(ex.getMessage()); + } else if (ex instanceof ValueFormatException) { + return new ValueFormatException(ex.getMessage()); + } else if (ex instanceof VersionException) { + return new VersionException(ex.getMessage()); + } else if (ex instanceof AccessControlException) { + return new AccessControlException(ex.getMessage()); + } else { + return new RepositoryException(ex.getMessage()); + } + } + + /** + * Utility method for creating a remote reference for a local item. + * Unlike the factory method for creating remote item references, this + * method introspects the type of the local item and returns the + * corresponding node, property, or item remote reference using the + * remote adapter factory. + *

    + * If the item, this method calls the + * {@link #getRemoteNode(Node)} to return the correct remote type. + * + * @param item local node, property, or item + * @return remote node, property, or item reference + * @throws RemoteException on RMI errors + */ + protected RemoteItem getRemoteItem(Item item) throws RemoteException { + if (item instanceof Property) { + return factory.getRemoteProperty((Property) item); + } else if (item instanceof Node) { + return getRemoteNode((Node) item); + } else { + return factory.getRemoteItem(item); + } + } + + /** + * Utility method for creating a remote reference for a local node. + * Unlike the factory method for creating remote node references, this + * method introspects the type of the local node and returns the + * corresponding node, version, or version history remote reference using + * the remote adapter factory. + * + * @param node local version, versionhistory, or normal node + * @return remote node, property, or item reference + * @throws RemoteException on RMI errors + */ + protected RemoteNode getRemoteNode(Node node) throws RemoteException { + if (node instanceof Version) { + return factory.getRemoteVersion((Version) node); + } else if (node instanceof VersionHistory) { + return factory.getRemoteVersionHistory((VersionHistory) node); + } else { + return factory.getRemoteNode(node); + } + } + + /** + * Utility method for creating an array of remote references for + * local node types. The remote references are created using the + * remote adapter factory. + *

    + * A null input is treated as an empty array. + * + * @param types local node type array + * @return remote node type array + * @throws RemoteException on RMI errors + */ + protected RemoteNodeType[] getRemoteNodeTypeArray(NodeType[] types) + throws RemoteException { + if (types != null) { + RemoteNodeType[] remotes = new RemoteNodeType[types.length]; + for (int i = 0; i < types.length; i++) { + remotes[i] = factory.getRemoteNodeType(types[i]); + } + return remotes; + } else { + return new RemoteNodeType[0]; // for safety + } + } + + /** + * Utility method for preparing an array of values for serialization. + * The returned array will contain serializable versions of all the + * given values. + *

    + * If the given array is null, then an empty array is + * returned. + * + * @param values the values to be decorated + * @return array of decorated values + * @throws RepositoryException if the values can not be serialized + */ + protected Value[] getSerialValues(Value[] values) + throws RepositoryException { + List serials = new ArrayList(); + if (values != null) { + for (Value value : values) { + if (value != null) { + serials.add(getSerialValue(value)); + } + } + } + return serials.toArray(new Value[serials.size()]); + } + + /** + * Utility method for decorating a value. Note that the contents of the + * original values will only be copied when the decorators are serialized. + * Null referenced and already serializable values are passed as-is. + * + * @param value the value to be decorated, or null + * @return the decorated value, or null + * @throws RepositoryException if the value can not be serialized + */ + protected Value getSerialValue(Value value) throws RepositoryException { + // if the value is null or already serializable, just return it + if (value == null || value instanceof Serializable) { + return value; + } else { + return SerialValueFactory.makeSerialValue(value); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerObservationManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerObservationManager.java new file mode 100644 index 00000000000..a382e03f400 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerObservationManager.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.server; + +import java.rmi.RemoteException; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.ObservationManager; + +import org.apache.jackrabbit.rmi.observation.Queue; +import org.apache.jackrabbit.rmi.observation.ServerEventListenerProxy; +import org.apache.jackrabbit.rmi.remote.RemoteEventCollection; +import org.apache.jackrabbit.rmi.remote.RemoteObservationManager; + +/** + * Remote adapter for the JCR + * {@link javax.jcr.observation.ObservationManager ObservationManager} interface. + * This class makes a local item available as an RMI service using + * the {@link org.apache.jackrabbit.rmi.remote.RemoteObservationManager RemoteObservationManager} + * interface. + *

    + * This class works in conjunction with the + * {@link org.apache.jackrabbit.rmi.client.ClientObservationManager} class to + * implement the distributed the event listener registration described in + * observation package + * comment. + * + * @see javax.jcr.observation.ObservationManager + * @see org.apache.jackrabbit.rmi.remote.RemoteObservationManager + */ +public class ServerObservationManager extends ServerObject implements + RemoteObservationManager { + + /** The adapted local observation manager. */ + private ObservationManager observationManager; + + /** + * The map of event listener proxies indexed by the unique identifier. + */ + private Map proxyMap; + + /** + * The queue to which event listener proxies post events to be reported + * by the {@link #getNextEvent(long)} method. + */ + private Queue queue; + + /** + * Creates a remote adapter for the given local workspace. + * + * @param observationManager local observation manager + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerObservationManager(ObservationManager observationManager, + RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.observationManager = observationManager; + } + + /** {@inheritDoc} */ + public void addEventListener(long listenerId, int eventTypes, + String absPath, boolean isDeep, String[] uuid, String[] nodeTypeName, + boolean noLocal) throws RepositoryException, RemoteException { + + // find the proxy or create one + ServerEventListenerProxy proxy; + synchronized (this) { + if (proxyMap == null) { + proxyMap = new HashMap(); + } + + Long id = Long.valueOf(listenerId); + proxy = proxyMap.get(id); + if (proxy == null) { + proxy = new ServerEventListenerProxy(getFactory(), listenerId, + getQueue()); + proxyMap.put(id, proxy); + } + } + + // register the proxy with the observation manager + observationManager.addEventListener(proxy, eventTypes, absPath, + isDeep, uuid, nodeTypeName, noLocal); + } + + /** {@inheritDoc} */ + public void removeEventListener(long listenerId) + throws RepositoryException, RemoteException { + + // try to find the proxy in the map + ServerEventListenerProxy proxy; + synchronized (this) { + if (proxyMap == null) { + return; + } + + Long id = new Long(listenerId); + proxy = (ServerEventListenerProxy) proxyMap.remove(id); + if (proxy == null) { + return; + } + } + + // register the proxy with the observation manager + observationManager.removeEventListener(proxy); + } + + /** {@inheritDoc} */ + public RemoteEventCollection getNextEvent(long timeout) throws RemoteException { + // need the queue + checkQueue(); + + try { + if (timeout < 0) { + timeout = 0; + } + return (RemoteEventCollection) queue.get(timeout); + } catch (InterruptedException ie) { + // don't retry, but log + } + + // did not get anything, fall back to nothing + return null; + } + + //---------- internal ------------------------------------------------------ + + /** + * Makes sure, the {@link #queue} field is assigned a value. + */ + private synchronized void checkQueue() { + if (queue == null) { + queue = new Queue(); + } + } + + /** + * Returns the Channel allocating it if required. + * + * @return queue + */ + private Queue getQueue() { + checkQueue(); + return queue; + } +} diff --git a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerProperty.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerProperty.java similarity index 81% rename from contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerProperty.java rename to jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerProperty.java index 797cccfa4a9..f4806ed6a31 100644 --- a/contrib/jcr-rmi/src/java/org/apache/jackrabbit/rmi/server/ServerProperty.java +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerProperty.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -23,8 +23,8 @@ import javax.jcr.Value; import org.apache.jackrabbit.rmi.remote.RemoteProperty; -import org.apache.jackrabbit.rmi.remote.RemotePropertyDef; -import org.apache.jackrabbit.rmi.remote.SerialValue; +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; /** * Remote adapter for the JCR {@link javax.jcr.Property Property} @@ -33,7 +33,6 @@ * {@link org.apache.jackrabbit.rmi.remote.RemoteProperty RemoteProperty} * interface. * - * @author Jukka Zitting * @see javax.jcr.Property * @see org.apache.jackrabbit.rmi.remote.RemoteProperty */ @@ -58,7 +57,7 @@ public ServerProperty(Property property, RemoteAdapterFactory factory) /** {@inheritDoc} */ public Value getValue() throws RepositoryException, RemoteException { try { - return new SerialValue(property.getValue()); + return SerialValueFactory.makeSerialValue(property.getValue()); } catch (RepositoryException ex) { throw getRepositoryException(ex); } @@ -67,7 +66,7 @@ public Value getValue() throws RepositoryException, RemoteException { /** {@inheritDoc} */ public Value[] getValues() throws RepositoryException, RemoteException { try { - return SerialValue.makeSerialValueArray(property.getValues()); + return getSerialValues(property.getValues()); } catch (RepositoryException ex) { throw getRepositoryException(ex); } @@ -112,10 +111,10 @@ public long[] getLengths() throws RepositoryException, RemoteException { } /** {@inheritDoc} */ - public RemotePropertyDef getDefinition() + public RemotePropertyDefinition getDefinition() throws RepositoryException, RemoteException { try { - return getFactory().getRemotePropertyDef(property.getDefinition()); + return getFactory().getRemotePropertyDefinition(property.getDefinition()); } catch (RepositoryException ex) { throw getRepositoryException(ex); } diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerPropertyDefinition.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerPropertyDefinition.java new file mode 100644 index 00000000000..42655a2d366 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerPropertyDefinition.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition; + +/** + * Remote adapter for the JCR + * {@link javax.jcr.nodetype.PropertyDefinition PropertyDefinition} interface. This + * class makes a local property definition available as an RMI service + * using the + * {@link org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition RemotePropertyDefinition} + * interface. + * + * @see javax.jcr.nodetype.PropertyDefinition + * @see org.apache.jackrabbit.rmi.remote.RemotePropertyDefinition + */ +public class ServerPropertyDefinition extends ServerItemDefinition + implements RemotePropertyDefinition { + + /** The adapted local property definition. */ + private PropertyDefinition def; + + /** + * Creates a remote adapter for the given local property definition. + * + * @param def local property definition + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerPropertyDefinition(PropertyDefinition def, RemoteAdapterFactory factory) + throws RemoteException { + super(def, factory); + this.def = def; + } + + /** {@inheritDoc} */ + public int getRequiredType() throws RemoteException { + return def.getRequiredType(); + } + + /** {@inheritDoc} */ + public String[] getValueConstraints() throws RemoteException { + return def.getValueConstraints(); + } + + /** {@inheritDoc} */ + public Value[] getDefaultValues() throws RemoteException { + try { + return getSerialValues(def.getDefaultValues()); + } catch (RepositoryException e) { + throw new RemoteException("Unable to serialize default values"); + } + } + + /** {@inheritDoc} */ + public boolean isMultiple() throws RemoteException { + return def.isMultiple(); + } + + /** {@inheritDoc} */ + public String[] getAvailableQueryOperators() throws RemoteException { + return def.getAvailableQueryOperators(); + } + + /** {@inheritDoc} */ + public boolean isFullTextSearchable() throws RemoteException { + return def.isFullTextSearchable(); + } + + /** {@inheritDoc} */ + public boolean isQueryOrderable() throws RemoteException { + return def.isQueryOrderable(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQuery.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQuery.java new file mode 100644 index 00000000000..8aeec951d89 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQuery.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Query; + +import org.apache.jackrabbit.rmi.remote.RemoteQuery; +import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; +import org.apache.jackrabbit.rmi.remote.RemoteNode; + +/** + * Remote adapter for the JCR {@link javax.jcr.query.Query Query} interface. + * This class makes a local session available as an RMI service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteQuery RemoteQuery} + * interface. + * + * @see javax.jcr.query.Query + * @see org.apache.jackrabbit.rmi.remote.RemoteQuery + */ +public class ServerQuery extends ServerObject implements RemoteQuery { + + /** The adapted local query manager. */ + private Query query; + + /** + * Creates a remote adapter for the given local Query. + * + * @param query local Query + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerQuery(Query query, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.query = query; + } + + /** {@inheritDoc} */ + public RemoteQueryResult execute() + throws RepositoryException, RemoteException { + return getFactory().getRemoteQueryResult(query.execute()); + } + + /** {@inheritDoc} */ + public String getStatement() throws RemoteException { + return query.getStatement(); + } + + /** {@inheritDoc} */ + public String getLanguage() throws RemoteException { + return query.getLanguage(); + } + + /** {@inheritDoc} */ + public String getStoredQueryPath() + throws RepositoryException, RemoteException { + return query.getStoredQueryPath(); + } + + /** {@inheritDoc} */ + public RemoteNode storeAsNode(String absPath) + throws RepositoryException, RemoteException { + return getRemoteNode(query.storeAsNode(absPath)); + } + + /** {@inheritDoc} */ + public void bindValue(String varName, Value value) + throws RepositoryException, RemoteException { + query.bindValue(varName, value); + } + + /** {@inheritDoc} */ + public String[] getBindVariableNames() + throws RepositoryException, RemoteException { + try { + return query.getBindVariableNames(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void setLimit(long limit) throws RemoteException { + query.setLimit(limit); + } + + /** {@inheritDoc} */ + public void setOffset(long offset) throws RemoteException { + query.setOffset(offset); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQueryManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQueryManager.java new file mode 100644 index 00000000000..c70904ee21e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQueryManager.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; + +import org.apache.jackrabbit.rmi.remote.RemoteQuery; +import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; + +/** + * Remote adapter for the JCR {@link javax.jcr.query.QueryManager QueryManager} + * interface. This class makes a local query manager available as an RMI + * service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteQueryManager RemoteQueryManager} + * interface. + * + * @see javax.jcr.query.QueryManager + * @see org.apache.jackrabbit.rmi.remote.RemoteQueryManager + */ +public class ServerQueryManager extends ServerObject + implements RemoteQueryManager { + + /** The current session. */ + private Session session; + + /** The adapted local query manager. */ + private QueryManager manager; + + /** + * Creates a remote adapter for the given local query manager. + * + * @param session current session + * @param manager local query manager + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerQueryManager( + Session session, QueryManager manager, ServerAdapterFactory factory) + throws RemoteException { + super(factory); + this.session = session; + this.manager = manager; + } + + /** {@inheritDoc} */ + public RemoteQuery createQuery(String statement, String language) + throws RepositoryException, RemoteException { + try { + Query query = manager.createQuery(statement, language); + return getFactory().getRemoteQuery(query); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteQuery getQuery(String absPath) + throws RepositoryException, RemoteException { + try { + Item item = session.getItem(absPath); + if (!item.isNode()) { + throw new InvalidQueryException("Not a query node: " + absPath); + } + return getFactory().getRemoteQuery(manager.getQuery((Node) item)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getSupportedQueryLanguages() + throws RepositoryException, RemoteException { + try { + return manager.getSupportedQueryLanguages(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQueryResult.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQueryResult.java new file mode 100644 index 00000000000..36535e9867c --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerQueryResult.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryResult; + +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteQueryResult; + +/** + * Remote adapter for the JCR {@link javax.jcr.query.QueryResult QueryResult} interface. + * This class makes a local session available as an RMI service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteQueryResult RemoteQueryResult} + * interface. + * + * @see javax.jcr.query.QueryResult + * @see org.apache.jackrabbit.rmi.remote.RemoteQueryResult + */ +public class ServerQueryResult extends ServerObject + implements RemoteQueryResult { + + /** The adapted local query result. */ + private QueryResult result; + + /** + * Creates a remote adapter for the given local QueryResult. + * + * @param result local QueryResult + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerQueryResult(QueryResult result, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.result = result; + } + + /** {@inheritDoc} */ + public String[] getColumnNames() + throws RepositoryException, RemoteException { + return result.getColumnNames(); + } + + /** {@inheritDoc} */ + public RemoteIterator getRows() throws RepositoryException, RemoteException { + return getFactory().getRemoteRowIterator(result.getRows()); + } + + /** {@inheritDoc} */ + public RemoteIterator getNodes() throws RepositoryException, RemoteException { + return getFactory().getRemoteNodeIterator(result.getNodes()); + } + + /** {@inheritDoc} */ + public String[] getSelectorNames() throws RepositoryException, RemoteException { + return result.getSelectorNames(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerRepository.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerRepository.java new file mode 100644 index 00000000000..75d764376db --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerRepository.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.remote.RemoteSession; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; + +/** + * Remote adapter for the JCR {@link javax.jcr.Repository Repository} + * interface. This class makes a local repository available as an RMI service + * using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteRepository RemoteRepository} + * interface. + * + * @see javax.jcr.Repository + * @see org.apache.jackrabbit.rmi.remote.RemoteRepository + */ +public class ServerRepository extends ServerObject implements RemoteRepository { + + /** The adapted local repository. */ + private Repository repository; + + /** + * Creates a remote adapter for the given local repository. + * + * @param repository local repository + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerRepository( + Repository repository, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.repository = repository; + } + + /** {@inheritDoc} */ + public String getDescriptor(String name) throws RemoteException { + return repository.getDescriptor(name); + } + + /** {@inheritDoc} */ + public String[] getDescriptorKeys() throws RemoteException { + return repository.getDescriptorKeys(); + } + + /** {@inheritDoc} */ + public RemoteSession login() throws RepositoryException, RemoteException { + try { + Session session = repository.login(); + return getFactory().getRemoteSession(session); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteSession login(String workspace) + throws RepositoryException, RemoteException { + try { + Session session = repository.login(workspace); + return getFactory().getRemoteSession(session); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteSession login(Credentials credentials) + throws RepositoryException, RemoteException { + try { + Session session = repository.login(credentials); + return getFactory().getRemoteSession(session); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteSession login(Credentials credentials, String workspace) + throws RepositoryException, RemoteException { + try { + Session session = repository.login(credentials, workspace); + return getFactory().getRemoteSession(session); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Value getDescriptorValue(String key) throws RemoteException { + try { + return SerialValueFactory.makeSerialValue(repository.getDescriptorValue(key)); + } catch (RepositoryException ex) { + throw new RemoteException(ex.getMessage(), ex); + } + } + + /** {@inheritDoc} */ + public Value[] getDescriptorValues(String key) throws RemoteException { + try { + return SerialValueFactory.makeSerialValueArray(repository.getDescriptorValues(key)); + } catch (RepositoryException ex) { + throw new RemoteException(ex.getMessage(), ex); + } + } + + /** {@inheritDoc} */ + public boolean isSingleValueDescriptor(String key) throws RemoteException { + return repository.isSingleValueDescriptor(key); + } + + /** {@inheritDoc} */ + public boolean isStandardDescriptor(String key) throws RemoteException { + return repository.isStandardDescriptor(key); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerRow.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerRow.java new file mode 100644 index 00000000000..f14283f6891 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerRow.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Row; + +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteRow; +import org.apache.jackrabbit.rmi.value.SerialValueFactory; + +/** + * Remote adapter for the JCR {@link javax.jcr.query.Row Row} interface. + * This class makes a local session available as an RMI service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteRow RemoteRow} + * interface. + * + * @see javax.jcr.query.Row + * @see org.apache.jackrabbit.rmi.remote.RemoteRow + */ +public class ServerRow extends ServerObject implements RemoteRow { + + /** The adapted local row. */ + private Row row; + + /** + * Creates a remote adapter for the given local query row. + * + * @param row local query row + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerRow(Row row, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.row = row; + } + + /** {@inheritDoc} */ + public Value[] getValues() throws RepositoryException, RemoteException { + try { + return getSerialValues(row.getValues()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Value getValue(String propertyName) + throws RepositoryException, RemoteException { + try { + return SerialValueFactory.makeSerialValue(row.getValue(propertyName)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNode getNode() + throws RepositoryException, RemoteException { + try { + return getRemoteNode(row.getNode()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNode getNode(String selectorName) + throws RepositoryException, RemoteException { + try { + return getRemoteNode(row.getNode(selectorName)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getPath() + throws RepositoryException, RemoteException { + try { + return row.getPath(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getPath(String selectorName) + throws RepositoryException, RemoteException { + try { + return row.getPath(selectorName); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public double getScore() + throws RepositoryException, RemoteException { + try { + return row.getScore(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public double getScore(String selectorName) + throws RepositoryException, RemoteException { + try { + return row.getScore(selectorName); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerSession.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerSession.java new file mode 100644 index 00000000000..0ec4e164f89 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerSession.java @@ -0,0 +1,351 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.server; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.rmi.RemoteException; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.rmi.remote.RemoteItem; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteProperty; +import org.apache.jackrabbit.rmi.remote.RemoteSession; +import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager; + +/** + * Remote adapter for the JCR {@link javax.jcr.Session Session} interface. + * This class makes a local session available as an RMI service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteSession RemoteSession} + * interface. + * + * @see javax.jcr.Session + * @see org.apache.jackrabbit.rmi.remote.RemoteSession + */ +public class ServerSession extends ServerObject implements RemoteSession { + + /** The adapted local session. */ + private Session session; + + /** + * The server workspace for this session. This field is assigned on demand + * by the first call to {@link #getWorkspace()}. The assumption is that + * there is only one workspace instance per session and that each call to + * the Session.getWorkspace() method of a single session will + * allways return the same object. + */ + private RemoteWorkspace remoteWorkspace; + + /** + * Creates a remote adapter for the given local session. + * + * @param session local session + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerSession(Session session, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.session = session; + } + + /** {@inheritDoc} */ + public String getUserID() throws RemoteException { + return session.getUserID(); + } + + /** {@inheritDoc} */ + public Object getAttribute(String name) throws RemoteException { + return session.getAttribute(name); + } + + /** {@inheritDoc} */ + public String[] getAttributeNames() throws RemoteException { + return session.getAttributeNames(); + } + + /** {@inheritDoc} */ + public RemoteSession impersonate(Credentials credentials) + throws RepositoryException, RemoteException { + try { + Session newSession = session.impersonate(credentials); + return getFactory().getRemoteSession(newSession); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteWorkspace getWorkspace() throws RemoteException { + if (remoteWorkspace == null) { + remoteWorkspace = + getFactory().getRemoteWorkspace(session.getWorkspace()); + } + + return remoteWorkspace; + } + + /** {@inheritDoc} */ + public boolean hasPermission(String path, String actions) + throws RepositoryException, RemoteException { + return session.hasPermission(path, actions); + } + + /** {@inheritDoc} */ + public String getNamespacePrefix(String uri) + throws RepositoryException, RemoteException { + try { + return session.getNamespacePrefix(uri); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getNamespacePrefixes() + throws RepositoryException, RemoteException { + try { + return session.getNamespacePrefixes(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getNamespaceURI(String prefix) + throws RepositoryException, RemoteException { + try { + return session.getNamespaceURI(prefix); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void setNamespacePrefix(String prefix, String uri) + throws RepositoryException, RemoteException { + try { + session.setNamespacePrefix(prefix, uri); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean itemExists(String path) throws RepositoryException, RemoteException { + return session.itemExists(path); + } + + /** {@inheritDoc} */ + public boolean nodeExists(String path) throws RepositoryException, RemoteException { + return session.nodeExists(path); + } + + /** {@inheritDoc} */ + public boolean propertyExists(String path) throws RepositoryException, RemoteException { + return session.propertyExists(path); + } + + /** {@inheritDoc} */ + public RemoteNode getNodeByIdentifier(String id) + throws RepositoryException, RemoteException { + try { + return getRemoteNode(session.getNodeByIdentifier(id)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + @SuppressWarnings("deprecation") + public RemoteNode getNodeByUUID(String uuid) + throws RepositoryException, RemoteException { + try { + return getRemoteNode(session.getNodeByUUID(uuid)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNode getRootNode() + throws RepositoryException, RemoteException { + try { + return getRemoteNode(session.getRootNode()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteItem getItem(String path) + throws RepositoryException, RemoteException { + try { + return getRemoteItem(session.getItem(path)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNode getNode(String path) + throws RepositoryException, RemoteException { + try { + return getRemoteNode(session.getNode(path)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteProperty getProperty(String path) + throws RepositoryException, RemoteException { + try { + return (RemoteProperty) getRemoteItem(session.getProperty(path)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasPendingChanges() + throws RepositoryException, RemoteException { + try { + return session.hasPendingChanges(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeItem(String path) + throws RepositoryException, RemoteException { + try { + session.removeItem(path); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void move(String from, String to) + throws RepositoryException, RemoteException { + try { + session.move(from, to); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void save() throws RepositoryException, RemoteException { + try { + session.save(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void refresh(boolean keepChanges) + throws RepositoryException, RemoteException { + try { + session.refresh(keepChanges); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void logout() throws RemoteException { + session.logout(); + } + + /** {@inheritDoc} */ + public boolean isLive() throws RemoteException { + return session.isLive(); + } + + /** {@inheritDoc} */ + public void importXML(String path, byte[] xml, int mode) + throws IOException, RepositoryException, RemoteException { + try { + session.importXML(path, new ByteArrayInputStream(xml), mode); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void addLockToken(String token) throws RemoteException { + session.addLockToken(token); + } + + /** {@inheritDoc} */ + public String[] getLockTokens() throws RemoteException { + return session.getLockTokens(); + } + + /** {@inheritDoc} */ + public void removeLockToken(String token) throws RemoteException { + session.removeLockToken(token); + } + + /** {@inheritDoc} */ + public byte[] exportDocumentView( + String path, boolean binaryAsLink, boolean noRecurse) + throws IOException, RepositoryException, RemoteException { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + session.exportDocumentView(path, buffer, binaryAsLink, noRecurse); + return buffer.toByteArray(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public byte[] exportSystemView( + String path, boolean binaryAsLink, boolean noRecurse) + throws IOException, RepositoryException, RemoteException { + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + session.exportSystemView(path, buffer, binaryAsLink, noRecurse); + return buffer.toByteArray(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteAccessControlManager getAccessControlManager() + throws UnsupportedRepositoryOperationException, + RepositoryException, RemoteException { + try { + return getFactory().getRemoteAccessControlManager( + session.getAccessControlManager()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersion.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersion.java new file mode 100644 index 00000000000..581a9d0359c --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersion.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.rmi.server; + +import java.rmi.RemoteException; +import java.util.Calendar; + +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; + +/** + * Remote adapter for the JCR {@link javax.jcr.version.Version Version} interface. + * This class makes a local version available as an RMI service using + * the {@link org.apache.jackrabbit.rmi.remote.RemoteVersion RemoteVersion} + * interface. + * + * @see javax.jcr.version.Version + * @see org.apache.jackrabbit.rmi.remote.RemoteVersion + */ +public class ServerVersion extends ServerNode implements RemoteVersion { + + /** The adapted local version. */ + private Version version; + + /** + * Creates a remote adapter for the given local version. + * + * @param version local version + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerVersion(Version version, RemoteAdapterFactory factory) + throws RemoteException { + super(version, factory); + this.version = version; + } + + /** + * Utility method for creating an array of remote references for + * local versions. The remote references are created using the + * remote adapter factory. + *

    + * A null input is treated as an empty array. + * + * @param versions local version array + * @return remote version array + * @throws RemoteException on RMI errors + */ + private RemoteVersion[] getRemoteVersionArray(Version[] versions) + throws RemoteException { + if (versions != null) { + RemoteVersion[] remotes = new RemoteVersion[versions.length]; + for (int i = 0; i < remotes.length; i++) { + remotes[i] = getFactory().getRemoteVersion(versions[i]); + } + return remotes; + } else { + return new RemoteVersion[0]; // for safety + } + } + + /** {@inheritDoc} */ + public RemoteVersionHistory getContainingHistory() throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersionHistory(version.getContainingHistory()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public Calendar getCreated() throws RepositoryException { + try { + return version.getCreated(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion getLinearSuccessor() throws RepositoryException, + RemoteException { + try { + Version linearSuccessor = version.getLinearSuccessor(); + if (linearSuccessor == null) { + return null; + } else { + return getFactory().getRemoteVersion(linearSuccessor); + } + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion[] getSuccessors() throws RepositoryException, RemoteException { + try { + return getRemoteVersionArray(version.getSuccessors()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion getLinearPredecessor() throws RepositoryException, + RemoteException { + try { + Version linearPredecessor = version.getLinearPredecessor(); + if (linearPredecessor == null) { + return null; + } else { + return getFactory().getRemoteVersion(linearPredecessor); + } + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion[] getPredecessors() throws RepositoryException, RemoteException { + try { + return getRemoteVersionArray(version.getPredecessors()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNode getFrozenNode() throws RepositoryException, + RemoteException { + try { + return getFactory().getRemoteNode(version.getFrozenNode()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersionHistory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersionHistory.java new file mode 100644 index 00000000000..466a496fb1d --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersionHistory.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; + +/** + * Remote adapter for the JCR {@link javax.jcr.version.VersionHistory VersionHistory} + * interface. This class makes a local version history available as an RMI + * service using the + * {@link org.apache.jackrabbit.rmi.remote.RemoteVersionHistory RemoteVersionHistory} + * interface. + * + * @see javax.jcr.version.VersionHistory + * @see org.apache.jackrabbit.rmi.remote.RemoteVersionHistory + */ +public class ServerVersionHistory extends ServerNode + implements RemoteVersionHistory { + + /** The adapted local version history. */ + private VersionHistory versionHistory; + + /** + * Creates a remote adapter for the given local version history. + * + * @param versionHistory local version history + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerVersionHistory(VersionHistory versionHistory, + RemoteAdapterFactory factory) throws RemoteException { + super(versionHistory, factory); + this.versionHistory = versionHistory; + } + + /** {@inheritDoc} */ + public String getVersionableIdentifier() throws RepositoryException, + RemoteException { + try { + return versionHistory.getVersionableIdentifier(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion getRootVersion() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersion(versionHistory.getRootVersion()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getAllLinearVersions() throws RepositoryException, + RemoteException { + try { + return getFactory().getRemoteVersionIterator( + versionHistory.getAllLinearVersions()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getAllVersions() + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersionIterator( + versionHistory.getAllVersions()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getAllLinearFrozenNodes() throws RepositoryException, + RemoteException { + try { + return getFactory().getRemoteNodeIterator( + versionHistory.getAllLinearFrozenNodes()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteIterator getAllFrozenNodes() throws RepositoryException, + RemoteException { + try { + return getFactory().getRemoteNodeIterator( + versionHistory.getAllFrozenNodes()); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion getVersion(String versionName) + throws RepositoryException, RemoteException { + try { + Version version = versionHistory.getVersion(versionName); + return getFactory().getRemoteVersion(version); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteVersion getVersionByLabel(String label) + throws RepositoryException, RemoteException { + try { + Version version = versionHistory.getVersionByLabel(label); + return getFactory().getRemoteVersion(version); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void addVersionLabel(String versionName, String label, + boolean moveLabel) throws RepositoryException, RemoteException { + try { + versionHistory.addVersionLabel(versionName, label, moveLabel); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeVersionLabel(String label) throws RepositoryException, + RemoteException { + try { + versionHistory.removeVersionLabel(label); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public boolean hasVersionLabel(String label) throws RepositoryException, RemoteException { + return versionHistory.hasVersionLabel(label); + } + + /** {@inheritDoc} */ + public boolean hasVersionLabel(String versionUUID, String label) + throws RepositoryException, RemoteException { + try { + Version version = getVersionByUUID(versionUUID); + return versionHistory.hasVersionLabel(version, label); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getVersionLabels() throws RepositoryException, RemoteException { + return versionHistory.getVersionLabels(); + } + + /** {@inheritDoc} */ + public String[] getVersionLabels(String versionUUID) + throws RepositoryException, RemoteException { + try { + Version version = getVersionByUUID(versionUUID); + return versionHistory.getVersionLabels(version); + } catch (ClassCastException cce) { + // we do not expect this here as nodes should be returned correctly + throw getRepositoryException(new RepositoryException(cce)); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void removeVersion(String versionName) + throws RepositoryException, RemoteException { + try { + versionHistory.removeVersion(versionName); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String getVersionableUUID() throws RepositoryException, RemoteException { + return versionHistory.getVersionableUUID(); + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersionManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersionManager.java new file mode 100644 index 00000000000..d8766af3a35 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerVersionManager.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.RemoteNode; +import org.apache.jackrabbit.rmi.remote.RemoteVersion; +import org.apache.jackrabbit.rmi.remote.RemoteVersionHistory; +import org.apache.jackrabbit.rmi.remote.RemoteVersionManager; + +public class ServerVersionManager extends ServerObject + implements RemoteVersionManager { + + private final Session session; + + private final VersionManager manager; + + public ServerVersionManager(Session session, + VersionManager manager, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.session = session; + this.manager = manager; + } + + public RemoteVersion checkin(String absPath) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersion(manager.checkin(absPath)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public void checkout(String absPath) throws RepositoryException { + try { + manager.checkout(absPath); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteVersion checkpoint(String absPath) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersion(manager.checkpoint(absPath)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteNode createActivity(String title) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNode(manager.createActivity(title)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteNode createConfiguration(String absPath) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNode( + manager.createConfiguration(absPath)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteNode getActivity() + throws RepositoryException, RemoteException { + try { + Node activity = manager.getActivity(); + if (activity == null) { + return null; + } else { + return getFactory().getRemoteNode(activity); + } + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteVersion getBaseVersion(String absPath) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersion( + manager.getBaseVersion(absPath)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteVersionHistory getVersionHistory(String absPath) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteVersionHistory( + manager.getVersionHistory(absPath)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public boolean isCheckedOut(String absPath) + throws RepositoryException { + try { + return manager.isCheckedOut(absPath); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteIterator merge( + String absPath, String srcWorkspace, boolean bestEffort) + throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeIterator( + manager.merge(absPath, srcWorkspace, bestEffort)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public RemoteIterator merge( + String absPath, String srcWorkspace, boolean bestEffort, + boolean isShallow) throws RepositoryException, RemoteException { + try { + return getFactory().getRemoteNodeIterator( + manager.merge(absPath, srcWorkspace, bestEffort, isShallow)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public void restore( + String absPath, String versionName, boolean removeExisting) + throws RepositoryException { + try { + manager.restore(absPath, versionName, removeExisting); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public void restoreByLabel( + String absPath, String versionLabel, boolean removeExisting) + throws RepositoryException { + try { + manager.restoreByLabel(absPath, versionLabel, removeExisting); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public void cancelMerge(String absPath, String versionIdentifier) + throws RepositoryException, RemoteException { + try { + Version version = (Version) session.getNodeByIdentifier(versionIdentifier); + manager.cancelMerge(absPath, version); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + public void doneMerge(String absPath, String versionIdentifier) + throws RepositoryException, RemoteException { + try { + Version version = (Version) session.getNodeByIdentifier(versionIdentifier); + manager.doneMerge(absPath, version); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + @Override + public void restore(String[] versionIdentifiers, boolean removeExisting) + throws RepositoryException, RemoteException { + try { + Version[] versions = new Version[versionIdentifiers.length]; + for (int i = 0; i < versions.length; i++) { + Version version = (Version) session.getNodeByIdentifier(versionIdentifiers[i]); + versions[i] = version; + } + manager.restore(versions, removeExisting); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + @Override + public void restore(String versionIdentifier, boolean removeExisting) + throws RepositoryException, RemoteException { + try { + Version version = (Version) session.getNodeByIdentifier(versionIdentifier); + manager.restore(version, removeExisting); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + @Override + public RemoteNode setActivity(String activityNodeIdentifier) + throws RepositoryException, RemoteException { + try { + Node newActivityNode; + if (activityNodeIdentifier == null) { + newActivityNode = null; + } else { + newActivityNode = session.getNodeByIdentifier(activityNodeIdentifier); + } + Node oldActivityNode = manager.setActivity(newActivityNode); + if (oldActivityNode == null) { + return null; + } else { + return getFactory().getRemoteNode(oldActivityNode); + } + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + @Override + public void removeActivity(String activityNodeIdentifier) + throws RepositoryException, RemoteException { + try { + Node activityNode = session.getNodeByIdentifier(activityNodeIdentifier); + manager.removeActivity(activityNode); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + @Override + public RemoteIterator merge(String activityNodeIdentifier) + throws RepositoryException, RemoteException { + try { + Node activityNode = session.getNodeByIdentifier(activityNodeIdentifier); + return getFactory().getRemoteNodeIterator(manager.merge(activityNode)); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + + @Override + public void restoreVI(String absPath, String versionIdentifier, + boolean removeExisting) throws RepositoryException, RemoteException { + try { + Version version = (Version) session.getNodeByIdentifier(versionIdentifier); + manager.restore(absPath, version, removeExisting); + } catch (RepositoryException e) { + throw getRepositoryException(e); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerWorkspace.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerWorkspace.java new file mode 100644 index 00000000000..664adbaf5db --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerWorkspace.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.server; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.rmi.RemoteException; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +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.rmi.remote.RemoteLockManager; +import org.apache.jackrabbit.rmi.remote.RemoteNamespaceRegistry; +import org.apache.jackrabbit.rmi.remote.RemoteNodeTypeManager; +import org.apache.jackrabbit.rmi.remote.RemoteObservationManager; +import org.apache.jackrabbit.rmi.remote.RemoteQueryManager; +import org.apache.jackrabbit.rmi.remote.RemoteVersionManager; +import org.apache.jackrabbit.rmi.remote.RemoteWorkspace; + +/** + * Remote adapter for the JCR {@link Workspace Workspace} interface. + * This class makes a local workspace available as an RMI service using the + * {@link RemoteWorkspace RemoteWorkspace} interface. + * + * @see Workspace + * @see RemoteWorkspace + */ +public class ServerWorkspace extends ServerObject implements RemoteWorkspace { + + /** The adapted local workspace. */ + private Workspace workspace; + + /** + * The remote observation manager for this workspace. This field is assigned + * on demand by the first call to {@link #getObservationManager()}. The + * assumption is that there is only one observation manager instance per + * workspace and that each call to the + * Workspace.getObservationManager() method of a single + * workspace will allways return the same object. + */ + private RemoteObservationManager remoteObservationManager; + + private RemoteLockManager remoteLockManager; + + private RemoteVersionManager remoteVersionManager; + + /** + * Creates a remote adapter for the given local workspace. + * + * @param workspace local workspace + * @param factory remote adapter factory + * @throws RemoteException on RMI errors + */ + public ServerWorkspace(Workspace workspace, RemoteAdapterFactory factory) + throws RemoteException { + super(factory); + this.workspace = workspace; + } + + /** {@inheritDoc} */ + public String getName() throws RemoteException { + return workspace.getName(); + } + + /** {@inheritDoc} */ + public void copy(String from, String to) + throws RepositoryException, RemoteException { + try { + workspace.copy(from, to); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void copy(String workspace, String from, String to) + throws RepositoryException, RemoteException { + try { + this.workspace.copy(workspace, from, to); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void clone( + String workspace, String from, String to, boolean removeExisting) + throws RepositoryException, RemoteException { + try { + this.workspace.clone(workspace, from, to, removeExisting); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void move(String from, String to) + throws RepositoryException, RemoteException { + try { + workspace.move(from, to); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNodeTypeManager getNodeTypeManager() + throws RepositoryException, RemoteException { + try { + NodeTypeManager manager = workspace.getNodeTypeManager(); + return getFactory().getRemoteNodeTypeManager(manager); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteNamespaceRegistry getNamespaceRegistry() + throws RepositoryException, RemoteException { + try { + NamespaceRegistry registry = workspace.getNamespaceRegistry(); + return getFactory().getRemoteNamespaceRegistry(registry); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteQueryManager getQueryManager() + throws RepositoryException, RemoteException { + try { + QueryManager queryManager = workspace.getQueryManager(); + return getFactory().getRemoteQueryManager( + workspace.getSession(), queryManager); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public RemoteObservationManager getObservationManager() + throws RepositoryException, RemoteException { + try { + if (remoteObservationManager == null) { + ObservationManager observationManager = + workspace.getObservationManager(); + remoteObservationManager = + getFactory().getRemoteObservationManager(observationManager); + } + return remoteObservationManager; + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public String[] getAccessibleWorkspaceNames() + throws RepositoryException, RemoteException { + try { + return workspace.getAccessibleWorkspaceNames(); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + /** {@inheritDoc} */ + public void importXML(String path, byte[] xml, int uuidBehaviour) + throws IOException, RepositoryException, RemoteException { + try { + workspace.importXML( + path, new ByteArrayInputStream(xml), uuidBehaviour); + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + public void createWorkspace(String name, String source) + throws RepositoryException, RemoteException { + if (source != null) { + workspace.createWorkspace(name, source); + } else { + workspace.createWorkspace(name); + } + } + + public void deleteWorkspace(String name) + throws RepositoryException, RemoteException { + workspace.deleteWorkspace(name); + } + + /** {@inheritDoc} */ + public RemoteLockManager getLockManager() + throws RepositoryException, RemoteException { + try { + if (remoteLockManager == null) { + LockManager lockManager = workspace.getLockManager(); + remoteLockManager = + getFactory().getRemoteLockManager(lockManager); + } + return remoteLockManager; + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + + public RemoteVersionManager getVersionManager() + throws RepositoryException, RemoteException { + try { + if (remoteVersionManager == null) { + VersionManager versionManager = workspace.getVersionManager(); + remoteVersionManager = + getFactory().getRemoteVersionManager(workspace.getSession(), versionManager); + } + return remoteVersionManager; + } catch (RepositoryException ex) { + throw getRepositoryException(ex); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerXASession.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerXASession.java new file mode 100644 index 00000000000..8d792973ea9 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/ServerXASession.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.rmi.server; + +import java.rmi.RemoteException; + +import javax.jcr.Session; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import org.apache.jackrabbit.rmi.remote.RemoteXASession; +import org.apache.jackrabbit.rmi.remote.SerializableXid; + +/** + * Remote adapter for XA-enabled sessions. + * + * @since 1.4 + */ +public class ServerXASession extends ServerSession implements RemoteXASession { + + /** + * The adapted local XA resource + */ + private final XAResource resource; + + /** + * Creates a remote adapter for the given local, transaction enabled, + * session. + */ + public ServerXASession( + Session session, XAResource resource, RemoteAdapterFactory factory) + throws RemoteException { + super(session, factory); + this.resource = resource; + } + + private static XAException getXAException(XAException e) { + return new XAException(e.getMessage()); + } + + public void commit(Xid xid, boolean onePhase) throws XAException { + try { + resource.commit(xid, onePhase); + } catch (XAException e) { + throw getXAException(e); + } + } + + public void end(Xid xid, int flags) throws XAException { + try { + resource.end(xid, flags); + } catch (XAException e) { + throw getXAException(e); + } + } + + public void forget(Xid xid) throws XAException { + try { + resource.forget(xid); + } catch (XAException e) { + throw getXAException(e); + } + } + + public int getTransactionTimeout() throws XAException { + try { + return resource.getTransactionTimeout(); + } catch (XAException e) { + throw getXAException(e); + } + } + + public int prepare(Xid xid) throws XAException { + try { + return resource.prepare(xid); + } catch (XAException e) { + throw getXAException(e); + } + } + + public Xid[] recover(int flag) throws XAException { + try { + Xid[] xids = resource.recover(flag); + for (int i = 0; i < xids.length; i++) { + xids[i] = new SerializableXid(xids[i]); + } + return xids; + } catch (XAException e) { + throw getXAException(e); + } + } + + public void rollback(Xid xid) throws XAException { + try { + resource.rollback(xid); + } catch (XAException e) { + throw getXAException(e); + } + } + + public boolean setTransactionTimeout(int seconds) throws XAException { + try { + return resource.setTransactionTimeout(seconds); + } catch (XAException e) { + throw getXAException(e); + } + } + + public void start(Xid xid, int flags) throws XAException { + try { + resource.start(xid, flags); + } catch (XAException e) { + throw getXAException(e); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerIterator.java new file mode 100644 index 00000000000..775d1059377 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerIterator.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.rmi.server.iterator; + +import java.rmi.RemoteException; +import java.util.ArrayList; +import java.util.NoSuchElementException; + +import javax.jcr.RangeIterator; + +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerObject; + + +/** + * Remote adapter for the JCR {@link RangeIterator} interface. This + * class makes a local iterator available as an RMI service using the + * {@link RemoteIterator} interface. + */ +public abstract class ServerIterator extends ServerObject + implements RemoteIterator { + + /** The adapted local iterator. */ + private final RangeIterator iterator; + + /** The maximum number of elements to send per request. */ + private final int maxBufferSize; + + /** + * The cached number of elements in the iterator, -1 if the iterator + * size is unknown, or -2 if the size has not been retrieved from the + * adapted local iterator. This variable is useful in cases when the + * underlying iterator does not know its sizes (getSize() returns -1) + * but we reach the end of the iterator in a nextObjects() call and + * can thus determine the size of the iterator. + */ + private long size; + + /** + * Creates a remote adapter for the given local item. + * + * @param iterator local iterator to be adapted + * @param factory remote adapter factory + * @param maxBufferSize maximum buffer size + * @throws RemoteException on RMI errors + */ + public ServerIterator( + RangeIterator iterator, RemoteAdapterFactory factory, + int maxBufferSize) throws RemoteException { + super(factory); + this.iterator = iterator; + this.maxBufferSize = maxBufferSize; + this.size = -2; + } + + /** + * Returns the size of the iterator. The size is cached by invoking the + * adapted local iterator when this method is first called or by + * determining the size from an end-of-iterator condition in nextObjects(). + * + * @return size of the iterator + * @throws RemoteException on RMI errors + */ + @Override + public long getSize() throws RemoteException { + if (size == -2) { + size = iterator.getSize(); + } + return size; + } + + /** + * Skips the given number of elements. + * + * @param items number of elements to skip + * @throws NoSuchElementException if skipped past the last element + * @throws RemoteException on RMI errors + */ + @Override + public void skip(long items) + throws NoSuchElementException, RemoteException { + try { + iterator.skip(items); + } catch (NoSuchElementException e) { + throw new NoSuchElementException(e.getMessage()); + } + } + + /** + * Returns a remote adapter for the given local object. This abstract + * method is used by {@link #nextObjects()} to convert the local + * objects to remote references to be sent to the client. + *

    + * Subclasses should implement this method to use the remote adapter + * factory to create remote adapters of the specific element type. + * + * @param object local object + * @return remote adapter + * @throws RemoteException on RMI errors + */ + protected abstract Object getRemoteObject(Object object) + throws RemoteException; + + /** + * Returns an array of remote references to the next elements in this + * iteration. + * + * @return array of remote references, or null + * @throws RemoteException on RMI errors + */ + @Override + public Object[] nextObjects() throws RemoteException { + ArrayList items = new ArrayList(); + while (items.size() < maxBufferSize && iterator.hasNext()) { + items.add(getRemoteObject(iterator.next())); + } + if (items.size() > 0) { + return items.toArray(new Object[items.size()]); + } else { + size = iterator.getPosition(); + return null; + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerNodeIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerNodeIterator.java new file mode 100644 index 00000000000..83dd0e39b99 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerNodeIterator.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.rmi.server.iterator; + +import java.rmi.RemoteException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; + +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; + +/** + * A ServerIterator for iterating nodes. + */ +public class ServerNodeIterator extends ServerIterator { + + /** + * Creates a ServerNodeIterator instance. + * + * @param iterator local node iterator + * @param factory remote adapter factory + * @param maxBufferSize maximum size of the element buffer + * @throws RemoteException on RMI errors + */ + public ServerNodeIterator( + NodeIterator iterator, RemoteAdapterFactory factory, + int maxBufferSize) throws RemoteException { + super(iterator, factory, maxBufferSize); + } + + /** + * Creates and returns a remote adapter for the given node. + * + * @param object local object + * @return remote adapter + * @throws RemoteException on RMI errors + * @see ServerIterator#getRemoteObject(Object) + */ + protected Object getRemoteObject(Object object) throws RemoteException { + return getRemoteNode((Node) object); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerNodeTypeIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerNodeTypeIterator.java new file mode 100644 index 00000000000..dd153d7a732 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerNodeTypeIterator.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.rmi.server.iterator; + +import java.rmi.RemoteException; + +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; + +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; + +/** + * A ServerIterator for iterating node types. + */ +public class ServerNodeTypeIterator extends ServerIterator { + + /** + * Creates a ServerNodeTypeIterator instance. + * + * @param iterator local node type iterator + * @param factory remote adapter factory + * @param maxBufferSize maximum size of the element buffer + * @throws RemoteException on RMI errors + */ + public ServerNodeTypeIterator( + NodeTypeIterator iterator, RemoteAdapterFactory factory, + int maxBufferSize) throws RemoteException { + super(iterator, factory, maxBufferSize); + } + + /** + * Creates and returns a remote adapter for the given node type. + * + * @param object local object + * @return remote adapter + * @throws RemoteException on RMI errors + * @see ServerIterator#getRemoteObject(Object) + */ + protected Object getRemoteObject(Object object) throws RemoteException { + return getFactory().getRemoteNodeType((NodeType) object); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerPropertyIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerPropertyIterator.java new file mode 100644 index 00000000000..8709bd90a86 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerPropertyIterator.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.rmi.server.iterator; + +import java.rmi.RemoteException; + +import javax.jcr.Property; +import javax.jcr.PropertyIterator; + +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; + +/** + * A ServerIterator for iterating properties. + */ +public class ServerPropertyIterator extends ServerIterator { + + /** + * Creates a ServerPropertyIterator instance. + * + * @param iterator local property iterator + * @param factory remote adapter factory + * @param maxBufferSize maximum size of the element buffer + * @throws RemoteException on RMI errors + */ + public ServerPropertyIterator( + PropertyIterator iterator, RemoteAdapterFactory factory, + int maxBufferSize) throws RemoteException { + super(iterator, factory, maxBufferSize); + } + + /** + * Creates and returns a remote adapter for the given property. + * + * @param object local object + * @return remote adapter + * @throws RemoteException on RMI errors + * @see ServerIterator#getRemoteObject(Object) + */ + protected Object getRemoteObject(Object object) throws RemoteException { + return getFactory().getRemoteProperty((Property) object); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerRowIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerRowIterator.java new file mode 100644 index 00000000000..5b77ef43bbf --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerRowIterator.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.rmi.server.iterator; + +import java.rmi.RemoteException; + +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; + +/** + * A ServerIterator for iterating rows. + */ +public class ServerRowIterator extends ServerIterator { + + /** + * Creates a ServerRowIterator instance. + * + * @param iterator local row iterator + * @param factory remote adapter factory + * @param maxBufferSize maximum size of the element buffer + * @throws RemoteException on RMI errors + */ + public ServerRowIterator( + RowIterator iterator, RemoteAdapterFactory factory, + int maxBufferSize) throws RemoteException { + super(iterator, factory, maxBufferSize); + } + + /** + * Creates and returns a remote adapter for the given row. + * + * @param object local object + * @return remote adapter + * @throws RemoteException on RMI errors + * @see ServerIterator#getRemoteObject(Object) + */ + protected Object getRemoteObject(Object object) throws RemoteException { + return getFactory().getRemoteRow((Row) object); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerVersionIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerVersionIterator.java new file mode 100644 index 00000000000..39675d595c0 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/iterator/ServerVersionIterator.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.rmi.server.iterator; + +import java.rmi.RemoteException; + +import javax.jcr.version.Version; +import javax.jcr.version.VersionIterator; + +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; + +/** + * A ServerIterator for iterating versions. + */ +public class ServerVersionIterator extends ServerIterator { + + /** + * Creates a ServerVersionIterator instance. + * + * @param iterator local version iterator + * @param factory remote adapter factory + * @param maxBufferSize maximum size of the element buffer + * @throws RemoteException on RMI errors + */ + public ServerVersionIterator( + VersionIterator iterator, RemoteAdapterFactory factory, + int maxBufferSize) throws RemoteException { + super(iterator, factory, maxBufferSize); + } + + /** + * Creates and returns a remote adapter for the given version.. + * + * @param object local object + * @return remote adapter + * @throws RemoteException on RMI errors + * @see ServerIterator#getRemoteObject(Object) + */ + protected Object getRemoteObject(Object object) throws RemoteException { + return getFactory().getRemoteVersion((Version) object); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/jmx/JCRServer.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/jmx/JCRServer.java new file mode 100644 index 00000000000..ec9a31c3c90 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/jmx/JCRServer.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.rmi.server.jmx; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Properties; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.naming.InitialContext; + +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerAdapterFactory; + +/** + * MBean that registers a JCR RMI server through JNDI. + */ +public class JCRServer implements JCRServerMBean { + + /** + * local repository address + */ + private String localAddress; + + /** + * remote repository address + */ + private String remoteAddress; + + /** + * Optional local JNDI environment properties + */ + private String localEnvironment; + + /** + * Optional remote JNDI environment properties + */ + private String remoteEnvironment; + + /** + * Remote repository instance + */ + RemoteRepository remote; + + /** + * Local repository instance + */ + private Repository localRepository; + + public void start() throws Exception { + + if (this.localAddress == null) { + throw new IllegalStateException("local repository address is null"); + } + + if (this.remoteAddress == null) { + throw new IllegalStateException("remote repository address is null"); + } + + // local repository + InitialContext localContext = createInitialContext(localEnvironment); + localRepository = (Repository) localContext.lookup(this.localAddress); + if (localRepository == null) { + throw new IllegalArgumentException("local repository not found at " + + this.localAddress); + } + + // remote repository + InitialContext remoteContext = createInitialContext(remoteEnvironment); + RemoteAdapterFactory factory = new ServerAdapterFactory(); + remote = factory.getRemoteRepository(localRepository); + + // bind remote server + remoteContext.bind(this.remoteAddress, remote); + } + + /** + * + * @param jndiProps + * jndi environment properties + * @return an InitialContext for the given environment properties + * @throws Exception + * if any error occurs + */ + private InitialContext createInitialContext(String jndiProps) + throws Exception { + InitialContext initialContext = null; + if (jndiProps != null) { + InputStream is = new ByteArrayInputStream(jndiProps.getBytes()); + Properties props = new Properties(); + props.load(is); + initialContext = new InitialContext(props); + } else { + initialContext = new InitialContext(); + } + return initialContext; + } + + public void stop() throws Exception { + // unbind remote server + InitialContext ctx = new InitialContext(); + ctx.unbind(this.remoteAddress); + remote = null; + } + + public void createWorkspace( + String username, String password, String name) + throws RepositoryException { + Session session = localRepository.login( + new SimpleCredentials(username, password.toCharArray())); + try { + session.getWorkspace().createWorkspace(name); + } finally { + session.logout(); + } + } + + public String getLocalAddress() { + return localAddress; + } + + public void setLocalAddress(String localAddress) { + this.localAddress = localAddress; + } + + public String getRemoteAddress() { + return remoteAddress; + } + + public void setRemoteAddress(String remoteAddress) { + this.remoteAddress = remoteAddress; + } + + public String getLocalEnvironment() { + return localEnvironment; + } + + public void setLocalEnvironment(String localEnvironment) { + this.localEnvironment = localEnvironment; + } + + public String getRemoteEnvironment() { + return remoteEnvironment; + } + + public void setRemoteEnvironment(String remoteEnvironment) { + this.remoteEnvironment = remoteEnvironment; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/jmx/JCRServerMBean.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/jmx/JCRServerMBean.java new file mode 100644 index 00000000000..8884577e921 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/jmx/JCRServerMBean.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.rmi.server.jmx; + +import javax.jcr.RepositoryException; + +public interface JCRServerMBean { + + void start() throws Exception; + + void stop() throws Exception; + + /** + * Creates a workspace in the managed repository. + * + * @param username administrator username + * @param password administrator password + * @param workspace name of the workspace to create + * @throws RepositoryException if the workspace could not be created + */ + void createWorkspace(String username, String password, String workspace) + throws RepositoryException; + + String getLocalAddress(); + + void setLocalAddress(String address); + + String getRemoteAddress(); + + void setRemoteAddress(String address); + + String getRemoteEnvironment(); + + void setRemoteEnvironment(String remoteEnvironment); + + String getLocalEnvironment(); + + void setLocalEnvironment(String localEnvironment); + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerGroup.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerGroup.java new file mode 100644 index 00000000000..750b966f14b --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerGroup.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.rmi.server.principal; + +import java.rmi.RemoteException; +import java.security.Principal; +import java.security.acl.Group; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; + +import org.apache.jackrabbit.api.security.principal.GroupPrincipal; +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.principal.RemoteGroup; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; + +public class ServerGroup extends ServerPrincipal implements RemoteGroup { + + public ServerGroup(final Group principal, final RemoteAdapterFactory factory) + throws RemoteException { + super(principal, factory); + } + + public ServerGroup(final Principal principal, final RemoteAdapterFactory factory) + throws RemoteException { + super(principal, factory); + } + + public boolean isMember(String member) { + return isMember(member, getPrincipal()); + } + + public RemoteIterator members() throws RemoteException { + Iterator members = new Iterator() { + final Enumeration base = members(getPrincipal()); + + public boolean hasNext() { + return base.hasMoreElements(); + } + + public Principal next() { + return base.nextElement(); + } + + public void remove() { + throw new UnsupportedOperationException("remove"); + } + }; + return getFactory().getRemotePrincipalIterator(members); + } + + private static boolean isMember(final String memberName, final Principal group) { + Enumeration pe = members(group); + while (pe.hasMoreElements()) { + Principal p = pe.nextElement(); + if (memberName.equals(p.getName())) { + return true; + } + + if (isGroup(p) && isMember(memberName, p)) { + return true; + } + } + return false; + } + + public static boolean isGroup(Principal principal) { + return principal instanceof Group || principal instanceof GroupPrincipal; + } + + private static Enumeration members(Principal principal) { + if (principal instanceof Group) { + return ((Group) principal).members(); + } + if (principal instanceof GroupPrincipal) { + return ((GroupPrincipal) principal).members(); + } + return Collections.emptyEnumeration(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerPrincipal.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerPrincipal.java new file mode 100644 index 00000000000..378742f0020 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerPrincipal.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.rmi.server.principal; + +import java.rmi.RemoteException; +import java.security.Principal; + +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerObject; + +public class ServerPrincipal extends ServerObject implements RemotePrincipal { + + private final Principal principal; + + public ServerPrincipal(final Principal principal, + final RemoteAdapterFactory factory) throws RemoteException { + super(factory); + this.principal = principal; + } + + public String getName() { + return principal.getName(); + } + + /** + * Returns the {@link Principal} encapsulated in this instance. + *

    + * NOTE: This method is intended to only be used in the JCR RMI + * implementation to be able to "send back" remote principals to the server + * for implementation of the remote JCR API. + * + * @return the {@link Principal} encapsulated in this instance. + */ + public Principal getPrincipal() { + return principal; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerPrincipalIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerPrincipalIterator.java new file mode 100644 index 00000000000..b3114a9acc8 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/principal/ServerPrincipalIterator.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.rmi.server.principal; + +import java.rmi.RemoteException; +import java.security.Principal; +import java.util.Iterator; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.iterator.ServerIterator; + +public class ServerPrincipalIterator extends ServerIterator { + + public ServerPrincipalIterator(Iterator iterator, + RemoteAdapterFactory factory, int maxBufferSize) + throws RemoteException { + super(new RangeIteratorAdapter(iterator), factory, maxBufferSize); + } + + /** + * {@inheritDoc} + */ + protected Object getRemoteObject(Object object) throws RemoteException { + return getFactory().getRemotePrincipal((Principal) object); + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlEntry.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlEntry.java new file mode 100644 index 00000000000..f19470aa818 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlEntry.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.rmi.server.security; + +import java.rmi.RemoteException; + +import javax.jcr.security.AccessControlEntry; + +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlEntry; +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerObject; + +public class ServerAccessControlEntry extends ServerObject implements + RemoteAccessControlEntry { + + private final AccessControlEntry ace; + + public ServerAccessControlEntry(final AccessControlEntry ace, + final RemoteAdapterFactory factory) throws RemoteException { + super(factory); + this.ace = ace; + } + + public RemotePrincipal getPrincipal() throws RemoteException { + return getFactory().getRemotePrincipal(ace.getPrincipal()); + } + + public RemotePrivilege[] getPrivileges() throws RemoteException { + return getFactory().getRemotePrivilege(ace.getPrivileges()); + } + + AccessControlEntry getAccessControlEntry() { + return ace; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlList.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlList.java new file mode 100644 index 00000000000..5b6b237315c --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlList.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.rmi.server.security; + +import java.rmi.RemoteException; +import java.security.Principal; + +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.rmi.remote.principal.RemotePrincipal; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlEntry; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlList; +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.principal.ServerPrincipal; + +public class ServerAccessControlList extends ServerAccessControlPolicy + implements RemoteAccessControlList { + + public ServerAccessControlList(final AccessControlList acl, + final RemoteAdapterFactory factory) throws RemoteException { + super(acl, factory); + } + + public RemoteAccessControlEntry[] getAccessControlEntries() + throws RepositoryException, RemoteException { + return getFactory().getRemoteAccessControlEntry( + ((AccessControlList) getAccessControlPolicy()).getAccessControlEntries()); + } + + public boolean addAccessControlEntry(RemotePrincipal principal, + RemotePrivilege[] privileges) throws RepositoryException { + + Principal p = null; + if (principal instanceof ServerPrincipal) { + p = ((ServerPrincipal) principal).getPrincipal(); + } + Privilege[] privs = new Privilege[privileges.length]; + for (int i = 0; privs != null && i < privs.length; i++) { + if (privileges[i] instanceof ServerPrivilege) { + privs[i] = ((ServerPrivilege) privileges[i]).getPrivilege(); + } else { + // not a compatible remote privilege, abort + privs = null; + } + } + + if (p != null && privs != null) { + return ((AccessControlList) getAccessControlPolicy()).addAccessControlEntry( + p, privs); + } + + throw new RepositoryException("Unsupported Remote types"); + } + + public void removeAccessControlEntry(RemoteAccessControlEntry ace) + throws RepositoryException { + if (ace instanceof ServerAccessControlEntry) { + AccessControlEntry lace = ((ServerAccessControlEntry) ace).getAccessControlEntry(); + ((AccessControlList) getAccessControlPolicy()).removeAccessControlEntry(lace); + } else { + throw new RepositoryException( + "Unsupported RemoteAccessControlEntry type " + ace.getClass()); + } + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlManager.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlManager.java new file mode 100644 index 00000000000..45ded92f5e1 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlManager.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.rmi.server.security; + +import java.rmi.RemoteException; + +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.rmi.remote.RemoteIterator; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlManager; +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlPolicy; +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerObject; + +public class ServerAccessControlManager extends ServerObject implements + RemoteAccessControlManager { + + private final AccessControlManager acm; + + public ServerAccessControlManager(AccessControlManager acm, + RemoteAdapterFactory factory) throws RemoteException { + super(factory); + this.acm = acm; + } + + public RemoteIterator getApplicablePolicies(String absPath) + throws RepositoryException, RemoteException { + return getFactory().getRemoteAccessControlPolicyIterator( + acm.getApplicablePolicies(absPath)); + } + + public RemoteAccessControlPolicy[] getEffectivePolicies(String absPath) + throws RepositoryException, RemoteException { + return getFactory().getRemoteAccessControlPolicy( + acm.getEffectivePolicies(absPath)); + } + + public RemoteAccessControlPolicy[] getPolicies(String absPath) + throws RepositoryException, RemoteException { + return getFactory().getRemoteAccessControlPolicy( + acm.getPolicies(absPath)); + } + + public RemotePrivilege[] getPrivileges(String absPath) + throws RepositoryException, RemoteException { + return getFactory().getRemotePrivilege(acm.getPrivileges(absPath)); + } + + public RemotePrivilege[] getSupportedPrivileges(String absPath) + throws RepositoryException, RemoteException { + return getFactory().getRemotePrivilege( + acm.getSupportedPrivileges(absPath)); + } + + public boolean hasPrivileges(String absPath, String[] privileges) + throws RepositoryException { + Privilege[] privs = new Privilege[privileges.length]; + for (int i = 0; i < privs.length; i++) { + privs[i] = acm.privilegeFromName(privileges[i]); + } + + return acm.hasPrivileges(absPath, privs); + } + + public RemotePrivilege privilegeFromName(String privilegeName) + throws RepositoryException, RemoteException { + return getFactory().getRemotePrivilege( + acm.privilegeFromName(privilegeName)); + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlPolicy.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlPolicy.java new file mode 100644 index 00000000000..33703d24354 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlPolicy.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.rmi.server.security; + +import java.rmi.RemoteException; + +import javax.jcr.security.AccessControlPolicy; + +import org.apache.jackrabbit.rmi.remote.security.RemoteAccessControlPolicy; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerObject; + +public class ServerAccessControlPolicy extends ServerObject implements + RemoteAccessControlPolicy { + + private final AccessControlPolicy acp; + + public ServerAccessControlPolicy(final AccessControlPolicy acp, + final RemoteAdapterFactory factory) + + throws RemoteException { + super(factory); + this.acp = acp; + } + + AccessControlPolicy getAccessControlPolicy() { + return acp; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlPolicyIterator.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlPolicyIterator.java new file mode 100644 index 00000000000..507866d9d08 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerAccessControlPolicyIterator.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.rmi.server.security; + +import java.rmi.RemoteException; + +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; + +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.iterator.ServerIterator; + +/** + * A ServerIterator for iterating rows. + */ +public class ServerAccessControlPolicyIterator extends ServerIterator { + + /** + * Creates a ServerRowIterator instance. + * + * @param iterator local row iterator + * @param factory remote adapter factory + * @param maxBufferSize maximum size of the element buffer + * @throws RemoteException on RMI errors + */ + public ServerAccessControlPolicyIterator( + AccessControlPolicyIterator iterator, RemoteAdapterFactory factory, + int maxBufferSize) throws RemoteException { + super(iterator, factory, maxBufferSize); + } + + /** + * Creates and returns a remote adapter for the given row. + * + * @param object local object + * @return remote adapter + * @throws RemoteException on RMI errors + * @see ServerIterator#getRemoteObject(Object) + */ + protected Object getRemoteObject(Object object) throws RemoteException { + return getFactory().getRemoteAccessControlPolicy( + (AccessControlPolicy) object); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerPrivilege.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerPrivilege.java new file mode 100644 index 00000000000..1c01436ff3b --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/server/security/ServerPrivilege.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.rmi.server.security; + +import java.rmi.RemoteException; +import java.rmi.server.RemoteStub; + +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.rmi.remote.security.RemotePrivilege; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerObject; + +public class ServerPrivilege extends ServerObject implements RemotePrivilege { + + private final Privilege privilege; + + public ServerPrivilege(final Privilege privilege, + final RemoteAdapterFactory factory) throws RemoteException { + super(factory); + this.privilege = privilege; + } + + public RemotePrivilege[] getAggregatePrivileges() throws RemoteException { + return getFactory().getRemotePrivilege( + privilege.getAggregatePrivileges()); + } + + public RemotePrivilege[] getDeclaredAggregatePrivileges() + throws RemoteException { + return getFactory().getRemotePrivilege( + privilege.getDeclaredAggregatePrivileges()); + } + + public String getName() { + return privilege.getName(); + } + + public boolean isAbstract() { + return privilege.isAbstract(); + } + + public boolean isAggregate() { + return privilege.isAggregate(); + } + + Privilege getPrivilege() { + return privilege; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/AbstractValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/AbstractValue.java new file mode 100644 index 00000000000..786528e0ed3 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/AbstractValue.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.value; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.FilterInputStream; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; + +import javax.jcr.Binary; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +/** + * Abstract base class for {@link Value} implementations. This class + * implements all {@link Value} methods except getString and + * getType. + *

    + * Most of the default value getters always throw {@link ValueFormatException}s + * and expect type-specific subclasses to override that behaviour with the + * appropriate value conversions. + *

    + * The {@link #getBinary()} method is implemented based on the abstract + * {@link #getString()} method, but subclasses can override that default + * implementation. + *

    + * The {@link #getStream()} method uses {@link #getBinary()} to implement + * the deprecated JCR 1.0 behaviour. This method must not be overridden. + */ +abstract class AbstractValue implements Value, Serializable { + + /** + * Serial version UID + */ + private static final long serialVersionUID = -1989277354799918598L; + + /** + * The stream instance returned by {@link #getStream()}. Note that + * the stream is not included when serializing the value. + */ + private transient InputStream stream = null; + + /** + * Returns the stream representation of this value. This method implements + * the deprecated JCR 1.0 behaviour of always returning the same stream + * instance. The stream is retrieved from a {@link Binary} instance + * returned by {@link #getBinary()}. + * + * @return stream representation of this value + * @throws RepositoryException if the stream can not be created + */ + public synchronized final InputStream getStream() + throws RepositoryException { + if (stream == null) { + final Binary binary = getBinary(); + try { + stream = new FilterInputStream(binary.getStream()) { + @Override + public void close() throws IOException { + super.close(); + binary.dispose(); + } + }; + } finally { + // Proper cleanup also when binary.getStream() fails + if (stream == null) { + binary.dispose(); + } + } + } + return stream; + } + + /** + * Returns the binary representation of this value. The default + * implementation uses the UTF-8 serialization of the string returned + * by {@link #getString()}. Subclasses + */ + public Binary getBinary() throws RepositoryException { + final byte[] value = getString().getBytes(StandardCharsets.UTF_8); + return new Binary() { + public int read(byte[] b, long position) { + if (position >= value.length) { + return -1; + } else { + int p = (int) position; + int n = Math.min(b.length, value.length - p); + System.arraycopy(value, p, b, 0, n); + return n; + } + } + public InputStream getStream() { + return new ByteArrayInputStream(value); + } + public long getSize() { + return value.length; + } + public void dispose() { + } + }; + } + + /** + * Always throws a ValueFormatException. Implementations should + * overwrite if conversion to boolean is supported. + * + * @return nothing + * @throws ValueFormatException If the value cannot be converted to a + * boolean. + */ + public boolean getBoolean() throws ValueFormatException { + throw getValueFormatException(PropertyType.TYPENAME_BOOLEAN); + } + + /** + * Always throws a ValueFormatException. Implementations should + * overwrite if conversion to Calender is supported. + * + * @return nothing + * @throws ValueFormatException If the value cannot be converted to a + * Calendar instance. + */ + public Calendar getDate() throws ValueFormatException { + throw getValueFormatException(PropertyType.TYPENAME_DATE); + } + + /** + * Always throws a ValueFormatException. Implementations should + * overwrite if conversion to a {@link BigDecimal} is supported. + * + * @return nothing + * @throws ValueFormatException If the value cannot be converted to a + * {@link BigDecimal}. + */ + public BigDecimal getDecimal() throws RepositoryException { + throw getValueFormatException(PropertyType.TYPENAME_DECIMAL); + } + + /** + * Always throws a ValueFormatException. Implementations should + * overwrite if conversion to double is supported. + * + * @return nothing + * @throws ValueFormatException If the value cannot be converted to a + * double. + */ + public double getDouble() throws ValueFormatException { + throw getValueFormatException(PropertyType.TYPENAME_DOUBLE); + } + + /** + * Always throws a ValueFormatException. Implementations should + * overwrite if conversion to long is supported. + * + * @return nothing + * @throws ValueFormatException If the value cannot be converted to a + * long. + */ + public long getLong() throws ValueFormatException { + throw getValueFormatException(PropertyType.TYPENAME_LONG); + } + + /** + * Returns a ValueFormatException with a message indicating + * what kind of type conversion is not supported. + * + * @return nothing + * @param destType The name of the value type to which this value cannot + * be converted. + */ + protected ValueFormatException getValueFormatException(String destType) { + return new ValueFormatException( + "Cannot convert value \"" + this + "\" of type " + + PropertyType.nameFromValue(getType()) + " to " + destType); + } + + //--------------------------------------------------------------< Object > + + /** + * Compares values as defined in the JCR specification. + * + * @param object value for comparison + * @return true if the values are equal, + * false otherwise + * @see JCRRMI-16 + */ + public boolean equals(Object object) { + try { + return (object instanceof Value) + && getType() == ((Value) object).getType() + && getString().equals(((Value) object).getString()); + } catch (RepositoryException e) { + return false; + } + } + + /** + * Returns a hash code that's in line with how the {@link #equals(Object)} + * method is implemented. + * + * @return hash code of this value + */ + public int hashCode() { + try { + return getType() + getString().hashCode(); + } catch (RepositoryException e) { + return getType(); + } + } + + /** + * Returns a string representation of this value. + * + * @return value string + */ + public String toString() { + try { + return getString(); + } catch (RepositoryException e) { + return PropertyType.nameFromValue(getType()); + } + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/BinaryValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/BinaryValue.java new file mode 100644 index 00000000000..d4b224a25c3 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/BinaryValue.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.rmi.value; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Calendar; + +import javax.jcr.Binary; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * Binary value. + */ +class BinaryValue implements Value, Serializable { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 1719020811685971215L; + + /** + * The binary value + */ + private final Binary value; + + /** + * The stream instance returned by {@link #getStream()}. Note that + * the stream is not included when serializing the value. + */ + private transient InputStream stream = null; + + /** + * Creates a binary value. + */ + public BinaryValue(Binary value) { + this.value = value; + } + + /** + * Returns {@link PropertyType#BINARY}. + */ + public int getType() { + return PropertyType.BINARY; + } + + public Binary getBinary() { + return value; + } + + public String getString() throws RepositoryException { + try { + InputStream stream = value.getStream(); + try { + Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); + StringBuilder builder = new StringBuilder(); + char[] buffer = new char[1024]; + int n = reader.read(buffer); + while (n != -1) { + builder.append(buffer, 0, n); + n = reader.read(buffer); + } + return builder.toString(); + } finally { + stream.close(); + } + } catch (IOException e) { + throw new RepositoryException("Unable to read the binary value", e); + } + } + + public synchronized InputStream getStream() throws RepositoryException { + if (stream == null) { + stream = value.getStream(); + } + return stream; + } + + public boolean getBoolean() throws RepositoryException { + return new StringValue(getString()).getBoolean(); + } + + public Calendar getDate() throws RepositoryException { + return new StringValue(getString()).getDate(); + } + + public BigDecimal getDecimal() throws RepositoryException { + return new StringValue(getString()).getDecimal(); + } + + public double getDouble() throws RepositoryException { + return new StringValue(getString()).getDouble(); + } + + public long getLong() throws RepositoryException { + return new StringValue(getString()).getLong(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/BooleanValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/BooleanValue.java new file mode 100644 index 00000000000..1fa3926869f --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/BooleanValue.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.rmi.value; + +import javax.jcr.PropertyType; + +/** + * Boolean value. + */ +class BooleanValue extends AbstractValue { + + /** + * Serial version UID + */ + private static final long serialVersionUID = 5266937874230536517L; + + /** + * The boolean value + */ + private final boolean value; + + /** + * Creates an instance for the given boolean value. + */ + public BooleanValue(boolean value) { + this.value = value; + } + + /** + * Returns {@link PropertyType#BOOLEAN}. + */ + public int getType() { + return PropertyType.BOOLEAN; + } + + /** + * Returns the boolean value. + */ + @Override + public boolean getBoolean() { + return value; + } + + /** + * The boolean is converted using {@link Boolean#toString()}. + */ + public String getString() { + return Boolean.toString(value); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DateValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DateValue.java new file mode 100644 index 00000000000..2e748375599 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DateValue.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.value; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import javax.jcr.PropertyType; +import javax.jcr.ValueFormatException; + +/** + * Date value. + */ +class DateValue extends AbstractValue { + + /** + * Serial version UID + */ + private static final long serialVersionUID = -2382837055824423966L; + + // misc. numeric formats used in formatting + private static final DecimalFormat XX_FORMAT = new DecimalFormat("00"); + private static final DecimalFormat XXX_FORMAT = new DecimalFormat("000"); + private static final DecimalFormat XXXX_FORMAT = new DecimalFormat("0000"); + + /** + * The date value + */ + private final Calendar value; + + /** + * Creates an instance for the given date value. + * + * @param value the date value + */ + public DateValue(Calendar value) { + this.value = value; + } + + /** + * Returns {@link PropertyType#DATE}. + */ + public int getType() { + return PropertyType.DATE; + } + + /** + * Returns a copy of this Calendar value. Modifying the + * returned Calendar does not change the value of this + * instance. + */ + @Override + public Calendar getDate() { + return (Calendar) value.clone(); + } + + /** + * The date is converted to the number of milliseconds since + * 00:00 (UTC) 1 January 1970 (1970-01-01T00:00:00.000Z). + */ + @Override + public BigDecimal getDecimal() { + return new BigDecimal(value.getTimeInMillis()); + } + + /** + * The date is converted to the number of milliseconds since + * 00:00 (UTC) 1 January 1970 (1970-01-01T00:00:00.000Z). If this number + * is out-of-range for a double, a ValueFormatException is thrown. + */ + @Override + public double getDouble() { + return value.getTimeInMillis(); + } + + /** + * The date is converted to the number of milliseconds since + * 00:00 (UTC) 1 January 1970 (1970-01-01T00:00:00.000Z). If this number + * is out-of-range for a long, a ValueFormatException is thrown. + */ + @Override + public long getLong() { + return value.getTimeInMillis(); + } + + /** + * The date is converted to the following format: + * sYYYY-MM-DDThh:mm:ss.sssTZD + * where: + *

    + *
    sYYYY
    + *
    + * Four-digit year with optional leading positive (‘+’) or + * negative (‘-’) sign. 0000 , -0000 and +0000 all indicate + * the year 1 BCE. –YYYY where YYYY is the number y indicates + * the year (y+1) BCE. The absence of a sign or the presence + * of a positive sign indicates a year CE. For example, -0054 + * would indicate the year 55 BCE, while +1969 and 1969 + * indicate the year 1969 CE. + *
    + *
    MM
    + *
    + * Two-digit month (01 = January, etc.) + *
    + *
    DD
    + *
    + * Two-digit day of month (01 through 31) + *
    + *
    hh
    + *
    + * Two digits of hour (00 through 23, or 24 if mm is 00 and + * ss.sss is 00.000) + *
    + *
    mm
    + *
    + * Two digits of minute (00 through 59) + *
    + *
    ss.sss
    + *
    + * Seconds, to three decimal places (00.000 through 59.999 or + * 60.999 in the case of leap seconds) + *
    + *
    TZD
    + *
    + * Time zone designator (either Z for Zulu, i.e. UTC, or +hh:mm or + * -hh:mm, i.e. an offset from UTC) + *
    + *
    + *

    + * Note that the “T” separating the date from the time and the + * separators “-”and “:” appear literally in the string. + *

    + * This format is a subset of the format defined by ISO 8601:2004. + * If the DATE value cannot be represented in this format a + * {@link ValueFormatException} is thrown. + */ + public String getString() { + // determine era and adjust year if necessary + int year = value.get(Calendar.YEAR); + if (value.isSet(Calendar.ERA) + && value.get(Calendar.ERA) == GregorianCalendar.BC) { + // calculate year using astronomical system: + // year n BCE => astronomical year - n + 1 + year = 0 - year + 1; + } + + // note that we cannot use java.text.SimpleDateFormat for + // formatting because it can't handle years <= 0 and TZD's + StringBuilder buf = new StringBuilder(32); + + // year ([-]YYYY) + buf.append(XXXX_FORMAT.format(year)); + buf.append('-'); + // month (MM) + buf.append(XX_FORMAT.format(value.get(Calendar.MONTH) + 1)); + buf.append('-'); + // day (DD) + buf.append(XX_FORMAT.format(value.get(Calendar.DAY_OF_MONTH))); + buf.append('T'); + // hour (hh) + buf.append(XX_FORMAT.format(value.get(Calendar.HOUR_OF_DAY))); + buf.append(':'); + // minute (mm) + buf.append(XX_FORMAT.format(value.get(Calendar.MINUTE))); + buf.append(':'); + // second (ss) + buf.append(XX_FORMAT.format(value.get(Calendar.SECOND))); + buf.append('.'); + // millisecond (SSS) + buf.append(XXX_FORMAT.format(value.get(Calendar.MILLISECOND))); + + // time zone designator (Z or +00:00 or -00:00) + TimeZone tz = value.getTimeZone(); + // time zone offset (in minutes) from UTC (including daylight saving) + int offset = tz.getOffset(value.getTimeInMillis()) / 1000 / 60; + if (offset != 0) { + int hours = Math.abs(offset / 60); + int minutes = Math.abs(offset % 60); + buf.append(offset < 0 ? '-' : '+'); + buf.append(XX_FORMAT.format(hours)); + buf.append(':'); + buf.append(XX_FORMAT.format(minutes)); + } else { + buf.append('Z'); + } + + return buf.toString(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DecimalValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DecimalValue.java new file mode 100644 index 00000000000..1f2f8e3ed26 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DecimalValue.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.rmi.value; + +import java.math.BigDecimal; +import java.util.Calendar; + +import javax.jcr.PropertyType; +import javax.jcr.ValueFormatException; + +/** + * Decimal value. + */ +class DecimalValue extends AbstractValue { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 2077767642124007133L; + + /** + * The minimum value for date conversion. + */ + private static final BigDecimal MIN_DATE = + BigDecimal.valueOf(Long.MIN_VALUE); + + /** + * The maximum value for date conversion. + */ + private static final BigDecimal MAX_DATE = + BigDecimal.valueOf(Long.MAX_VALUE); + + /** + * The decimal value. + */ + private final BigDecimal value; + + /** + * Creates an instance for the given decimal value. + */ + public DecimalValue(BigDecimal value) { + this.value = value; + } + + /** + * Returns {@link PropertyType#DECIMAL}. + */ + public int getType() { + return PropertyType.DECIMAL; + } + + /** + * The decimal is converted to a long and interpreted as the number of + * milliseconds since 00:00 (UTC) 1 January 1970 (1970-01-01T00:00:00.000Z). + * If the resulting value is out of range for a date, + * a {@link ValueFormatException} is thrown. + */ + @Override + public Calendar getDate() throws ValueFormatException { + if (value.compareTo(MIN_DATE) >= 0 && value.compareTo(MAX_DATE) <= 0) { + Calendar date = Calendar.getInstance(); + date.setTimeInMillis(getLong()); + return date; + } else { + throw new ValueFormatException( + "Decimal value is outside the date range: " + value); + } + } + + /** + * Returns the decimal value. + */ + @Override + public BigDecimal getDecimal() { + return value; + } + + /** + * The decimal is converted using {@link BigDecimal#doubleValue()}. + */ + @Override + public double getDouble() { + return value.doubleValue(); + } + + /** + * The decimal is converted using {@link BigDecimal#longValue()}. + */ + @Override + public long getLong() { + return value.longValue(); + } + + /** + * The decimal is converted using {@link BigDecimal#toString()}. + */ + public String getString() { + return value.toString(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DoubleValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DoubleValue.java new file mode 100644 index 00000000000..ae151d13f1e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/DoubleValue.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.value; + +import java.math.BigDecimal; +import java.util.Calendar; + +import javax.jcr.PropertyType; +import javax.jcr.ValueFormatException; + +/** + * Double value. + */ +class DoubleValue extends AbstractValue { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = -2767063038068929611L; + + /** + * The double value. + */ + private final double value; + + /** + * Creates an instance for the given double value. + */ + public DoubleValue(double value) { + this.value = value; + } + + /** + * Returns {@link PropertyType#DOUBLE}. + */ + public int getType() { + return PropertyType.DOUBLE; + } + + /** + * Returns a Calendar instance interpreting the double as the + * time in milliseconds since the epoch (1.1.1970, 0:00, UTC). If the + * resulting value is out of range for a date, + * a {@link ValueFormatException} is thrown. + */ + @Override + public Calendar getDate() throws ValueFormatException { + if (Long.MIN_VALUE <= value && value <= Long.MAX_VALUE) { + Calendar date = Calendar.getInstance(); + date.setTimeInMillis((long) value); + return date; + } else { + throw new ValueFormatException( + "Double value is outside the date range: " + value); + } + } + + /** + * The double is converted using the constructor + * {@link BigDecimal#BigDecimal(double)}. + */ + @Override + public BigDecimal getDecimal() { + return new BigDecimal(value); + } + + /** + * Returns the double value. + */ + @Override + public double getDouble() { + return value; + } + + /** + * Standard Java type coercion is used. + */ + @Override + public long getLong() { + return (long) value; + } + + /** + * The double is converted using {@link Double#toString(double)}. + */ + public String getString() { + return Double.toString(value); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/LongValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/LongValue.java new file mode 100644 index 00000000000..75ac27e27fc --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/LongValue.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.rmi.value; + +import java.math.BigDecimal; +import java.util.Calendar; + +import javax.jcr.PropertyType; + +/** + * Long value. + */ +class LongValue extends AbstractValue { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = -5983072186237752887L; + + /** The long value */ + private final long value; + + /** + * Creates an instance for the given long value. + */ + public LongValue(long value) { + this.value = value; + } + + /** + * Returns PropertyType.LONG. + */ + public int getType() { + return PropertyType.LONG; + } + + /** + * The long is interpreted as the number of milliseconds since + * 00:00 (UTC) 1 January 1970 (1970-01-01T00:00:00.000Z). + */ + @Override + public Calendar getDate() { + Calendar date = Calendar.getInstance(); + date.setTimeInMillis(value); + return date; + } + + /** + * The long is converted using the method {@link BigDecimal#valueOf(long)}. + */ + @Override + public BigDecimal getDecimal() { + return BigDecimal.valueOf(value); + } + + /** + * Standard Java type coercion is used. + */ + @Override + public double getDouble() { + return value; + } + + /** + * Returns the long value. + */ + @Override + public long getLong() { + return value; + } + + /** + * The long is converted using {@link Long#toString(long)}. + */ + public String getString() { + return Long.toString(value); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/NameValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/NameValue.java new file mode 100644 index 00000000000..90c5e831f8a --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/NameValue.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.rmi.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +/** + * The NameValue class implements the committed value state for + * Name values as a part of the State design pattern (Gof) used by this package. + * + * @since 0.16.4.1 + */ +public class NameValue extends AbstractValue { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 4598175360244278453L; + + /** The name value. */ + private final String value; + + /** + * Creates an instance for the given name value. + */ + protected NameValue(String value) throws ValueFormatException { + // TODO: Check name format + this.value = value; + } + + /** + * Returns PropertyType.NAME. + */ + public int getType() { + return PropertyType.NAME; + } + + /** + * Returns the string representation of the Name value. + */ + public String getString() throws RepositoryException { + return value; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/PathValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/PathValue.java new file mode 100644 index 00000000000..0657637de34 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/PathValue.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.rmi.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +/** + * The PathValue class implements the committed value state for + * Path values as a part of the State design pattern (Gof) used by this package. + * + * @since 0.16.4.1 + */ +public class PathValue extends AbstractValue { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 6233090249008329224L; + + /** The path value. */ + private final String value; + + /** + * Creates an instance for the given path value. + */ + protected PathValue(String value) throws ValueFormatException { + // TODO: Check path format + this.value = value; + } + + /** + * Returns PropertyType.PATH. + */ + public int getType() { + return PropertyType.PATH; + } + + /** + * Returns the string representation of the path value. + */ + public String getString() throws ValueFormatException, RepositoryException { + return value; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/ReferenceValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/ReferenceValue.java new file mode 100644 index 00000000000..65cf752b5b8 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/ReferenceValue.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.rmi.value; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +/** + * The ReferenceValue class implements the committed value state + * for Reference values as a part of the State design pattern (Gof) used by + * this package. + * + * @since 0.16.4.1 + */ +public class ReferenceValue extends AbstractValue { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 5245358709465803351L; + + /** The reference value */ + private final String value; + + /** + * Creates an instance for the given reference value. + */ + protected ReferenceValue(String value) throws ValueFormatException { + // TODO: check syntax + this.value = value; + } + + /** + * Returns PropertyType.REFERENCE. + */ + public int getType() { + return PropertyType.REFERENCE; + } + + /** + * Returns the string representation of the reference value. + */ + public String getString() throws ValueFormatException, RepositoryException { + return value; + } +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/SerialValueFactory.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/SerialValueFactory.java new file mode 100644 index 00000000000..0a548447a77 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/SerialValueFactory.java @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.value; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +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; + +/** + * The SerialValueFactory class is used in the RMI infrastructure + * to create serializable Value instances on the client side. + *

    + * This class works in conjunction with the implementations of the + * javax.jcr.Value interface found in this package. + *

    + * This class may be extended to overwrite any of the + * createXXXValue methods to create instances of the respective + * type of {@link Value} implementation. The + * methods of the ValueFactory interface are declared final to + * guard against breaking the rules. + */ +public class SerialValueFactory implements ValueFactory { + + /** The singleton value factory instance */ + private static final SerialValueFactory INSTANCE = new SerialValueFactory(); + + /** + * Returns the ValueFactory instance, which currently is a + * singleton instance of this class. + *

    + * Future revisions will support some kind of configuration to specify + * which concrete class should be used. + */ + public static final SerialValueFactory getInstance() { + return INSTANCE; + } + + /** + * Utility method for decorating an array of values. The returned array will + * contain serializable value decorators for all the given values. Note that + * the contents of the original values will only be copied when the + * decorators are serialized. + *

    + * If the given array is null, then an empty array is + * returned. + * + * @param values the values to be decorated + * @return array of decorated values + * @throws RepositoryException if the values can not be serialized + */ + public static Value[] makeSerialValueArray(Value[] values) + throws RepositoryException { + List serials = new ArrayList(); + if (values != null) { + for (Value value : values) { + if (value != null) { + serials.add(makeSerialValue(value)); + } + } + } + return serials.toArray(new Value[serials.size()]); + } + + /** + * Utility method for decorating a value. Note that the contents of the + * original values will only be copied when the decorators are serialized. + * Null referenced and already serializable values are passed as-is. + * + * @param value the value to be decorated, or null + * @return the decorated value, or null + * @throws RepositoryException if the value can not be serialized + */ + public static Value makeSerialValue(Value value) throws RepositoryException { + // if the value is null or already serializable, just return it + if (value == null || value instanceof Serializable) { + return value; + } else { + return INSTANCE.createValue(value); + } + } + + /** + * Utility method for converting an array of strings to serializable + * string values. + *

    + * If the given array is null, then an empty array is + * returned. + * + * @param values the string array + * @return array of string values + */ + public static Value[] makeSerialValueArray(String[] values) { + List serials = new ArrayList(); + if (values != null) { + for (String value : values) { + if (value != null) { + serials.add(INSTANCE.createValue(value)); + } + } + } + return serials.toArray(new Value[serials.size()]); + } + + /** + * Default constructor only visible to extensions of this class. See + * class comments for details. + */ + protected SerialValueFactory() { + } + + /** {@inheritDoc} */ + public Value createValue(String value) { + return new StringValue(value); + } + + /** {@inheritDoc} */ + public final Value createValue(String value, int type) + throws ValueFormatException { + try { + return createValue(new StringValue(value), type); + } catch (ValueFormatException e) { + throw e; + } catch (RepositoryException e) { + throw new ValueFormatException( + "Unexpected error when creating value: " + value, e); + } + } + + private Value createValue(Value value) throws RepositoryException { + return createValue(value, value.getType()); + } + + private Value createValue(Value value, int type) + throws RepositoryException { + switch (type) { + case PropertyType.BINARY: + Binary binary = value.getBinary(); + try { + return createValue(binary.getStream()); + } finally { + binary.dispose(); + } + case PropertyType.BOOLEAN: + return createValue(value.getBoolean()); + case PropertyType.DATE: + return createValue(value.getDate()); + case PropertyType.DECIMAL: + return createValue(value.getDecimal()); + case PropertyType.DOUBLE: + return createValue(value.getDouble()); + case PropertyType.LONG: + return createValue(value.getLong()); + case PropertyType.NAME: + return new NameValue(value.getString()); + case PropertyType.PATH: + return new PathValue(value.getString()); + case PropertyType.REFERENCE: + return new ReferenceValue(value.getString()); + case PropertyType.STRING: + return createValue(value.getString()); + default: + throw new ValueFormatException("Unknown value type " + type); + } + } + + /** {@inheritDoc} */ + public final Value createValue(long value) { + return new LongValue(value); + } + + /** {@inheritDoc} */ + public final Value createValue(double value) { + return new DoubleValue(value); + } + + /** {@inheritDoc} */ + public final Value createValue(boolean value) { + return new BooleanValue(value); + } + + /** {@inheritDoc} */ + public Value createValue(BigDecimal value) { + return new DecimalValue(value); + } + + /** {@inheritDoc} */ + public final Value createValue(Calendar value) { + return new DateValue(value); + } + + /** {@inheritDoc} */ + public final Value createValue(InputStream value) { + try { + return createValue(createBinary(value)); + } catch (RepositoryException e) { + throw new RuntimeException("Unable to create a binary value", e); + } + } + + /** {@inheritDoc} */ + public final Value createValue(Node value) throws RepositoryException { + return createValue(value.getUUID(), PropertyType.REFERENCE); + } + + public Binary createBinary(InputStream stream) throws RepositoryException { + try { + try { + return new SerializableBinary(stream); + } finally { + stream.close(); + } + } catch (IOException e) { + throw new RepositoryException("Unable to read binary stream", e); + } + } + + public Value createValue(Binary value) { + return new BinaryValue(value); + } + + public Value createValue(Node value, boolean weak) + throws RepositoryException { + return new ReferenceValue(value.getUUID()); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/SerializableBinary.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/SerializableBinary.java new file mode 100644 index 00000000000..03ffb6e19f7 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/SerializableBinary.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.rmi.value; + +import java.io.ByteArrayInputStream; +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.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.io.Serializable; + +import javax.jcr.Binary; +import javax.jcr.RepositoryException; + +/** + * Serializable binary. + */ +class SerializableBinary implements Binary, Serializable { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = -7742179594834275853L; + + private static final int BUFFER_SIZE = 64 * 1024; + + private transient long length; + + private transient byte[] data; + + private transient File file; + + /** + * Creates a binary from the given stream. The stream is not closed. + * + * @param stream binary stream + */ + public SerializableBinary(InputStream stream) throws IOException { + length = 0; + data = null; + file = null; + + OutputStream output = null; + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int n = stream.read(buffer); + while (n != -1) { + length += n; + if (length < buffer.length) { + n = stream.read( + buffer, (int) length, buffer.length - (int) length); + } else { + if (file == null) { + file = File.createTempFile("jackrabbit-jcr-rmi-", null); + output = new FileOutputStream(file); + output.write(buffer); + } else { + output.write(buffer, 0, n); + } + n = stream.read(buffer); + } + } + if (file == null) { + data = new byte[(int) length]; + System.arraycopy(buffer, 0, data, 0, (int) length); + } + } finally { + if (output != null) { + output.close(); + } + } + } + + public synchronized int read(byte[] b, long position) + throws RepositoryException { + if (position < 0 || position >= length) { + return -1; + } else if (data != null) { + int n = Math.min(b.length, data.length - (int) position); + System.arraycopy(data, (int) position, b, 0, n); + return n; + } else if (file != null) { + try { + RandomAccessFile random = new RandomAccessFile(file, "r"); + try { + random.seek(position); + return random.read(b); + } finally { + random.close(); + } + } catch (FileNotFoundException e) { + throw new RepositoryException("Binary file is missing", e); + } catch (IOException e) { + throw new RepositoryException("Unable to read the binary", e); + } + } else { + throw new IllegalStateException("This binary has been disposed"); + } + } + + public synchronized InputStream getStream() throws RepositoryException { + if (data != null) { + return new ByteArrayInputStream(data); + } else if (file != null) { + try { + return new FileInputStream(file); + } catch (FileNotFoundException e) { + throw new RepositoryException("Binary file is missing", e); + } + } else { + throw new IllegalStateException("This binary has been disposed"); + } + } + + public long getSize() { + return length; + } + + public synchronized void dispose() { + data = null; + if (file != null) { + file.delete(); + file = null; + } + } + + private synchronized void writeObject(ObjectOutputStream stream) + throws IOException { + stream.writeLong(length); + if (data != null) { + stream.write(data); + } else if (file != null) { + InputStream input = new FileInputStream(file); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int n = input.read(buffer); + while (n != -1) { + stream.write(buffer, 0, n); + n = input.read(buffer); + } + } finally { + input.close(); + } + } else { + throw new IllegalStateException("This binary has been disposed"); + } + } + + private void readObject(ObjectInputStream stream) + throws IOException { + length = stream.readLong(); + if (length <= BUFFER_SIZE) { + data = new byte[(int) length]; + stream.readFully(data); + file = null; + } else { + data = null; + file = File.createTempFile("jackrabbit-jcr-rmi-", null); + OutputStream output = new FileOutputStream(file); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + long count = 0; + int n = stream.read(buffer); + while (n != -1 && count < length) { + output.write(buffer, 0, n); + count += n; + n = stream.read(buffer, 0, Math.min( + buffer.length, (int) (length - count))); + } + } finally { + output.close(); + } + } + } + + public void finalize() { + dispose(); + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/StringValue.java b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/StringValue.java new file mode 100644 index 00000000000..d4c45e9e6f9 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/java/org/apache/jackrabbit/rmi/value/StringValue.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.rmi.value; + +import java.math.BigDecimal; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import javax.jcr.PropertyType; +import javax.jcr.ValueFormatException; + +/** + * String value. + */ +class StringValue extends AbstractValue { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 220963478492833703L; + + /** The string value */ + private final String value; + + /** + * Creates an instance for the given string value. + */ + public StringValue(String value) { + this.value = value; + } + + /** + * Returns {@link PropertyType#STRING}. + */ + public int getType() { + return PropertyType.STRING; + } + + /** + * The string is converted using {@link Boolean#valueOf(String)}. + */ + @Override + public boolean getBoolean() { + return Boolean.valueOf(value); + } + + /** + * If the string is in the format described in + * {@link DateValue#getString()}, it is converted directly, otherwise + * a {@link ValueFormatException} is thrown. + */ + @Override + public Calendar getDate() throws ValueFormatException { + // check optional leading sign + char sign = '+'; + int start = 0; + if (value.startsWith("-")) { + sign = '-'; + start = 1; + } else if (value.startsWith("+")) { + sign = '+'; + start = 1; + } + + // note that we cannot use java.text.SimpleDateFormat for + // parsing because it can't handle years <= 0 and TZD's + int year, month, day, hour, min, sec, ms; + String tzID; + try { + // year (YYYY) + year = Integer.parseInt(value.substring(start, start + 4)); + start += 4; + // delimiter '-' + if (value.charAt(start) != '-') { + throw new ValueFormatException("Not a date: " + value); + } + start++; + // month (MM) + month = Integer.parseInt(value.substring(start, start + 2)); + start += 2; + // delimiter '-' + if (value.charAt(start) != '-') { + throw new ValueFormatException("Not a date: " + value); + } + start++; + // day (DD) + day = Integer.parseInt(value.substring(start, start + 2)); + start += 2; + // delimiter 'T' + if (value.charAt(start) != 'T') { + throw new ValueFormatException("Not a date: " + value); + } + start++; + // hour (hh) + hour = Integer.parseInt(value.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (value.charAt(start) != ':') { + throw new ValueFormatException("Not a date: " + value); + } + start++; + // minute (mm) + min = Integer.parseInt(value.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (value.charAt(start) != ':') { + throw new ValueFormatException("Not a date: " + value); + } + start++; + // second (ss) + sec = Integer.parseInt(value.substring(start, start + 2)); + start += 2; + // delimiter '.' + if (value.charAt(start) != '.') { + throw new ValueFormatException("Not a date: " + value); + } + start++; + // millisecond (SSS) + ms = Integer.parseInt(value.substring(start, start + 3)); + start += 3; + // time zone designator (Z or +00:00 or -00:00) + if (value.charAt(start) == '+' || value.charAt(start) == '-') { + // offset to UTC specified in the format +00:00/-00:00 + tzID = "GMT" + value.substring(start); + } else if (value.substring(start).equals("Z")) { + tzID = "GMT"; + } else { + throw new ValueFormatException( + "Invalid time zone in a date: " + value); + } + } catch (IndexOutOfBoundsException e) { + throw new ValueFormatException("Not a date: " + value, e); + } catch (NumberFormatException e) { + throw new ValueFormatException("Not a date: " + value, e); + } + + TimeZone tz = TimeZone.getTimeZone(tzID); + // verify id of returned time zone (getTimeZone defaults to "GMT") + if (!tz.getID().equals(tzID)) { + throw new ValueFormatException( + "Invalid time zone in a date: " + value); + } + + // initialize Calendar object + Calendar cal = Calendar.getInstance(tz); + cal.setLenient(false); + // year and era + if (sign == '-' || year == 0) { + // not CE, need to set era (BCE) and adjust year + cal.set(Calendar.YEAR, year + 1); + cal.set(Calendar.ERA, GregorianCalendar.BC); + } else { + cal.set(Calendar.YEAR, year); + cal.set(Calendar.ERA, GregorianCalendar.AD); + } + // month (0-based!) + cal.set(Calendar.MONTH, month - 1); + // day of month + cal.set(Calendar.DAY_OF_MONTH, day); + // hour + cal.set(Calendar.HOUR_OF_DAY, hour); + // minute + cal.set(Calendar.MINUTE, min); + // second + cal.set(Calendar.SECOND, sec); + // millisecond + cal.set(Calendar.MILLISECOND, ms); + + try { + // the following call will trigger an IllegalArgumentException + // if any of the set values are illegal or out of range + cal.getTime(); + } catch (IllegalArgumentException e) { + throw new ValueFormatException("Not a date: " + value, e); + } + + return cal; + } + + /** + * The string is converted using the constructor + * {@link BigDecimal#BigDecimal(String)}. + */ + @Override + public BigDecimal getDecimal() throws ValueFormatException { + try { + return new BigDecimal(value); + } catch (NumberFormatException e) { + throw new ValueFormatException("Not a decimal value: " + value, e); + } + } + + + /** + * The string is converted using {@link Double#valueOf(String)}. + */ + @Override + public double getDouble() throws ValueFormatException { + try { + return Double.valueOf(value); + } catch (NumberFormatException e) { + throw new ValueFormatException("Not a double value: " + value, e); + } + } + + /** + * The string is converted using {@link Long#valueOf(String)}. + */ + @Override + public long getLong() throws ValueFormatException { + try { + return Long.valueOf(value); + } catch (NumberFormatException e) { + throw new ValueFormatException("Not a long value: " + value, e); + } + } + + /** + * Returns the string value. + */ + public String getString() { + return value; + } + +} diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/client/iterator/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/client/iterator/package.html new file mode 100644 index 00000000000..2bcd960b8e1 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/client/iterator/package.html @@ -0,0 +1,19 @@ + + +Local adapters for remote iterators. + diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/client/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/client/package.html new file mode 100644 index 00000000000..bc61b5b85f7 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/client/package.html @@ -0,0 +1,76 @@ + + +Client implementation of the transparent JCR-RMI layer. +

    +This package contains the default client implementation of the +transparent JCR-RMI layer. The classes in this package can be used +to make a remote JCR-RMI service seem like a local JCR repository. +

    +The contents of this package is designed using two design patterns, +Factory and Adapter. All the ClientObject subclasses implement the +Adapter pattern to adapt a remote JCR-RMI reference to the corresponding +local JCR interface. The Factory pattern is used to centralize the +creation and configuration of all adapter instances. + +

    Looking up a JCR-RMI client

    +

    +The ClientRepositoryFactory class provides a convenient mechanism for +looking up a remote JCR-RMI repository. The factory can be used either +directly or as a JNDI object factory. +

    +The following example shows how to use the ClientRepositoryFactory +directly: + +

    +    String name = ...; // The RMI URL of the repository
    +    
    +    ClientRepositoryFactory factory = new ClientRepositoryFactory();
    +    Repository repository = factory.getRepository(name);
    +
    + +

    +The ClientRepositoryFactory can also be used via JNDI. The following +example settings and code demonstrate how to configure and use the +transparent JCR-RMI layer in a Tomcat 5.5 web application: + +

    +context.xml:
    +    <Resource name="jcr/Repository" auth="Container"
    +              type="javax.jcr.Repository"
    +              factory="org.apache.jackrabbit.rmi.client.ClientRepositoryFactory"
    +              url="..."/>
    +              
    +web.xml:
    +    <resource-env-ref>
    +      <description>The external content repository</description>
    +      <resource-env-ref-name>jcr/Repository</resource-env-ref-name>
    +      <resource-env-ref-type>javac.jcr.Repository</resource-env-ref-type>
    +    </resource-env-ref>
    +
    +...SomeServlet.java:
    +    Context initial = new InitialContext();
    +    Context context = (Context) initial.lookup("java:comp/env");
    +    Repository repository = (Repository) context.lookup("jcr/Repository");
    +
    + +

    +Note that in the example above only the context.xml configuration file +contains a direct references to the JCR-RMI layer. All other parts of the +web application can be implemented using the standard JCR interfaces. + + diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/iterator/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/iterator/package.html new file mode 100644 index 00000000000..00327bdc80e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/iterator/package.html @@ -0,0 +1,26 @@ + + +Utility classes for implementing JCR iterators based on static arrays. +

    +This package contains array-based implementations of the JCR +{@link javax.jcr.RangeIterator RangeIterator} interfaces. +

    +These utility classes were designed for the transparent JCR-RMI layer, +but can easily be used as a part of any JCR repository implementation. +This package depends only on the standard JCR and J2SE APIs. + diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/observation/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/observation/package.html new file mode 100644 index 00000000000..c52620028b3 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/observation/package.html @@ -0,0 +1,83 @@ + + + +

    +Helper class used by the observation manager classes. +

    + +

    +The JCR observation framework defines a notification mechanism where an +EventListener is registered with the observation manager +to receive certain events during the lifetime of the registration. For +the remote case, where the repository and the application run in different +Java VMs on possibly different hosts, there are issues related to the +observation framework. + +

    +The listener mechanism is a call-back mechanism where the server calls +code in the client application. The client application code most probably +hooks into other parts of that application. Therefore it is not practically +feasible to just require the client listener to be serializable to be sent +to the server for several reasons: + +

      +
    • The RMI server cannot call any method on the RMI client. To support such +call-back situations, the client side application would have to register +another server, which the server side would have to call. +
    • When trying to "transfer" the listener to the server side, the listener +class would have to be available to the server side - either locally in the +class path or through RMI class loading mechanisms. +
        + +

        +To circumvent these issues and still be able to register event listeners, +support for observation events is implemented in a manner similar to the Java +Management Extensions Remote API 1.0 (JSR 160) as laid out in Chapter 2.4, +Adding Remote Listeners: + +

        +The ObservationManager interface is not implemented in the RMI layer like +other interfaces, which just forward calls to the API to the corresponding +remote object by means of the RMI framework. Instead the client-side +ObservationManager manages its own list of registered event listeners. Each +listener registered with an ObservationManager is assigned a unique +identifier. + +

        +The unique identifier along with the filter configuration (event type, +path, depth flag, uuid list, node type list, local flag) is sent to the +server-side remote observation manager. This latter instantiates a proxy +event listener representing the client side event listener contains the +unique identifier as a ilnk to the client side event listener. The proxy +event listener is the registered to the repository's ObservationManager +with the configuration received from the client side. + +

        +When an event arrives at the event listener proxy, the proxy creates a +new RemoteEvent instance, which contains the client-side event listener +identifier along with the Event objects from the EventIterator. This +RemoteEvent instance is added to a server-side queue, which may be +retrieved from the client-side. + +

        +The client-side ObservationManager has a helper class ClientEventPoll, +which works in the background asking the server for the RemoteEvents from +the event queue. Each such event is then dispatched to the client-side +event listener by calling the EventListener.onEvent() method. + + diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/remote/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/remote/package.html new file mode 100644 index 00000000000..b01fd602254 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/remote/package.html @@ -0,0 +1,35 @@ + + +Remote interfaces of the transparent JCR-RMI layer. +

        +This package contains all the interfaces and classes used by both +the client and server sides of the transparent JCR-RMI layer. +The compiled contents of this package should thus be included +in both client and server installations. Note also that RMI stubs and +skeletons need to be generated for all the remote interfaces when using +old JDK versions (stubs for JDK < 1.5, skeletons for JDK < 1.2). +

        +The interfaces in this package are remote versions of the +corresponding interfaces in the javax.jcr packages. They are used by +the adapter classes in the .rmi.client and .rmi.server packages. The server +classes adapt local JCR objects to the remote interfaces, and the client +classes adapt the resulting remote references back to the JCR interfaces. +

        +The SerialValue class is a decorator utility used by both the client and +server classes to safely pass Value objects over the network. + diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/server/iterator/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/server/iterator/package.html new file mode 100644 index 00000000000..1fd4bb78e1d --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/server/iterator/package.html @@ -0,0 +1,19 @@ + + +Remote adapters for local iterators. + diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/server/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/server/package.html new file mode 100644 index 00000000000..7ee6b820ccb --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/server/package.html @@ -0,0 +1,80 @@ + + +Server implementation of the transparent JCR-RMI layer. +

        +This package contains the default server implementation of the +transparent JCR-RMI layer. The classes in this package can be used +to make a local JCR repository available as an RMI service. In addition, +this package offers a straightforward mechanism for extending or modifying +the behaviour of the server layer. +

        +The contents of this package is designed using two design patterns, +Factory and Adapter. All the remotely accessible ServerObject subclasses +implement the Adapter pattern to adapt a local JCR interface to the +corresponding remote JCR-RMI interface. The Factory pattern is used +to centralize the creation and configuration of all adapter instances. + +

        Setting up a JCR-RMI server

        +

        +Setting up the server part of the JCR-RMI layer is quite straightforward. +After instantiating a local JCR repository you need to wrap it into a +remote adapter and create an RMI binding for the repository. A variation +of the following code is usually all that is needed in addition to the +standard RMI setup (starting rmiregistry, etc.): + +

        +    Repository repository = ...; // The local repository
        +    String name = ...; // The RMI URL for the repository
        +    
        +    RemoteAdapterFactory factory = new ServerAdapterFactory();
        +    RemoteRepository remote = factory.getRemoteRepository(repository);
        +    Naming.bind(name, remote);  // Make the RMI binding using java.rmi.Naming
        +
        + +

        Extending the JCR-RMI server

        +

        +The Factory pattern used by this package makes it easy to extend +the behaviour of the JCR-RMI server. Such changes in behaviour or policy +can be implemented by modifying or replacing the default +ServerAdapterFactory used in the example above. +

        +The following example code adds transparent logging of all session logins +and logouts: + +

        +    Repository repository = ...; // The local repository
        +    String name = ...; // The RMI URL for the repository
        +    
        +    RemoteAdapterFactory factory = new ServerAdapterFactory() {
        +        public RemoteSession getRemoteSession(Session session)
        +                throws RemoteException {
        +            System.out.println("LOGIN: " + session.getUserId());
        +            return new ServerSession(session, this) {
        +                public void logout() {
        +                    System.out.println("LOGOUT: " + session.getUserId());
        +                    super.logout();
        +                }
        +            };
        +        }
        +    };
        +
        +    RemoteRepository remote = factory.getRemoteRepository(repository);
        +    Naming.bind(name, remote);  // Make the RMI binding using java.rmi.Naming
        +
        + + diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/value/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/value/package.html new file mode 100644 index 00000000000..8dc61302031 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/value/package.html @@ -0,0 +1,30 @@ + + + +Serializable implementation of the JCR Value interfaces. +

        +This package contains a simple in-memory implementation of the JCR +{@link javax.jcr.Value Value} and {@link javax.jcr.ValueFactory ValueFactory} +interfaces. The implementation has no external dependencies and supports +serialization of Value instances. +

        +

        +Note that the Value instances created by this package are thread safe. +

        + + diff --git a/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/xml/package.html b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/xml/package.html new file mode 100644 index 00000000000..9040e137b0e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/javadoc/org/apache/jackrabbit/rmi/xml/package.html @@ -0,0 +1,31 @@ + + +Utility classes for importing SAX event streams. +

        +The classes in this package can be used for to implement the JCR +{@link javax.jcr.Session#getImportContentHandler(java.lang.String) Session.getImportContentHandler(String)} +and +{@link javax.jcr.Workspace#getImportContentHandler(java.lang.String, int) Workspace.getImportContentHandler(String, int)} +methods in terms of the corresponding importXML() methods. +

        +These utility classes were designed for the transparent JCR-RMI layer, +but can easily be used as a part of any JCR repository implementation. +The public interface of this package depends only on the standard JCR and +J2SE APIs. The implementation uses Xerces as an extra dependency to +serialize the SAX event streams. + diff --git a/jackrabbit-jcr-rmi/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory b/jackrabbit-jcr-rmi/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory new file mode 100644 index 00000000000..26b601efd87 --- /dev/null +++ b/jackrabbit-jcr-rmi/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.rmi.repository.RmiRepositoryFactory diff --git a/jackrabbit-jcr-rmi/src/main/resources/jackrabbit-rmi-service.xml b/jackrabbit-jcr-rmi/src/main/resources/jackrabbit-rmi-service.xml new file mode 100644 index 00000000000..3ef2674f748 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/main/resources/jackrabbit-rmi-service.xml @@ -0,0 +1,41 @@ + + + + + + + java:jcr/local + jnp://localhost:1099/jcrServer + + + jboss.jca:service=ManagedConnectionFactory,name=jcr/local + + diff --git a/jackrabbit-jcr-rmi/src/test/java/org/apache/jackrabbit/rmi/ConformanceTest.java b/jackrabbit-jcr-rmi/src/test/java/org/apache/jackrabbit/rmi/ConformanceTest.java new file mode 100644 index 00000000000..f2f59d3e91a --- /dev/null +++ b/jackrabbit-jcr-rmi/src/test/java/org/apache/jackrabbit/rmi/ConformanceTest.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.rmi; + +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.apache.jackrabbit.test.JCRTestSuite; + +/** + * JCR API conformance test suite. + */ +public class ConformanceTest extends TestCase { + + public static TestSuite suite() { + TestSuite suite = new TestSuite(); + if (Boolean.getBoolean("jackrabbit.test.integration")) { + suite.addTest(new JCRTestSuite()); + } + return suite; + } + +} diff --git a/jackrabbit-jcr-rmi/src/test/java/org/apache/jackrabbit/rmi/RepositoryStubImpl.java b/jackrabbit-jcr-rmi/src/test/java/org/apache/jackrabbit/rmi/RepositoryStubImpl.java new file mode 100644 index 00000000000..055d4b881c3 --- /dev/null +++ b/jackrabbit-jcr-rmi/src/test/java/org/apache/jackrabbit/rmi/RepositoryStubImpl.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.rmi; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.rmi.server.RemoteObject; +import java.security.Principal; +import java.util.Properties; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.core.JackrabbitRepositoryStub; +import org.apache.jackrabbit.core.SessionImpl; +import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerAdapterFactory; +import org.apache.jackrabbit.rmi.server.principal.ServerGroup; +import org.apache.jackrabbit.test.RepositoryStubException; + +public class RepositoryStubImpl extends JackrabbitRepositoryStub { + + /** + * A known principal used for access control tests. + */ + private Principal principal; + + private RemoteRepository remote; + + private Repository repository; + + public RepositoryStubImpl(Properties env) { + super(env); + } + + @Override + public synchronized Repository getRepository() + throws RepositoryStubException { + if (repository == null) { + try { + Repository repo = super.getRepository(); + principal = findKnownPrincipal(repo); + + RemoteAdapterFactory raf = new ServerAdapterFactory(); + remote = raf.getRemoteRepository(repo); + + // Make sure that the remote reference survives serialization + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(buffer); + oos.writeObject(RemoteObject.toStub(remote)); + oos.close(); + + ObjectInputStream ois = new ObjectInputStream( + new ByteArrayInputStream(buffer.toByteArray())); + LocalAdapterFactory laf = new ClientAdapterFactory(); + repository = + laf.getRepository((RemoteRepository) ois.readObject()); + } catch (Exception e) { + throw new RepositoryStubException(e); + } + } + return repository; + } + + private static Principal findKnownPrincipal(Repository repo) + throws RepositoryException { + SessionImpl session = (SessionImpl) repo.login( + new SimpleCredentials("admin", "admin".toCharArray())); + try { + for (Principal principal : session.getSubject().getPrincipals()) { + if (!ServerGroup.isGroup(principal)) { + return principal; + } + } + throw new RepositoryException("Known principal not found"); + } finally { + session.logout(); + } + } + + @Override + public Principal getKnownPrincipal(Session ignored) + throws RepositoryException { + return principal; + } + +} diff --git a/jackrabbit-jcr-rmi/src/test/resources/logback-test.xml b/jackrabbit-jcr-rmi/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..15d98f9b73f --- /dev/null +++ b/jackrabbit-jcr-rmi/src/test/resources/logback-test.xml @@ -0,0 +1,31 @@ + + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-jcr-rmi/src/test/resources/repositoryStubImpl.properties b/jackrabbit-jcr-rmi/src/test/resources/repositoryStubImpl.properties new file mode 100644 index 00000000000..bc101aaba6e --- /dev/null +++ b/jackrabbit-jcr-rmi/src/test/resources/repositoryStubImpl.properties @@ -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. + +# Stub implementation class +javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.rmi.RepositoryStubImpl diff --git a/jackrabbit-jcr-server/README.txt b/jackrabbit-jcr-server/README.txt new file mode 100644 index 00000000000..19547f07470 --- /dev/null +++ b/jackrabbit-jcr-server/README.txt @@ -0,0 +1,93 @@ +================================ +Welcome to Jackrabbit JCR Server +================================ + +This is the JCR Server component of the Apache Jackrabbit project. +This component contains two WebDAV based JCR server implementations: + + 1) WebDAV server ('simple') + + DAV1,2 compliant WebDAV server implementation to access a + JSR170 repository. + + Futher information such as configuration as well as the + SimpleWebdavServlet itself may be found in the 'webapp' project. + + Packages: + - org.apache.jackrabbit.server = server + - org.apache.jackrabbit.server.io = import/export + - org.apache.jackrabbit.webdav.simple = dav-resource implementation + config. + + Servlet (webapp project): + - org.apache.jackrabbit.j2ee.SimpleWebdavServlet.java + + + 2) 'jcr' server: + + Server used to remove JSR170 calls via WebDAV. + No particular effort to be compliant to WebDAV related RFCs. + + The 'client' counterpart of this server is under development and + can be found within the /contrib/spi contribution. + + Packages: + - org.apache.jackrabbit.server = server + - org.apache.jackrabbit.server.jcr = jcr-server specific server part + - org.apache.jackrabbit.webdav.jcr = dav-resources, reports, properties + + Servlet (webapp project): + - org.apache.jackrabbit.j2ee.JCRServerServlet.java + + Further reading: + - http://www.day.com/jsr170/server/JCR_Webdav_Protocol.zip + +Things to do +============ + +------------------------------------------------------------------- +TODO 'jcr' server implementation +------------------------------------------------------------------- + +general + +- undo incomplete changes in case of exception +- multistatus fuer lock, copy, move, delete wherever required. +- DAV:supported-live-property-set +- timeout: remove expired locks/subscriptions +- improve definition methods/compliance-class +- OPTIONS to *-request-uri (according to RFC 2616) + + +lock + +- implement session-scoped locks. this includes: + > uncommenting supported-locks entry + > build caching mechanism for session in case of session-scoped locks. + > retrieval of cached sessions (currently not possible from IfHeader). + > open issue in JCR: scope of lock cannot be retrieved. + +- JCR lock-token currently not checked for compliance with RFC2518. If the + token is modified accordingly, setting the lock-token to the subsequent + session (currently in the WebdavRequestImpl) must be aware of that change.... + +- transaction locks + - lock returned upon lock-discovery + - remove after timeout (>> releasing cached sessions) + - define reasonable timeout or make timeout configurable + - createLock must respect existing locks in the subtree, for lock is always deep. + - repository transactions ('global') are only possible with jackrabbit, where + the session represents the XAResource itself. + since j2ee explicitely requires any usertransaction to be completed + upon the end of the servletes service method. + general review necessary.... + + +observation + +- make sure all expired subscriptions are removed. +- subscription: reasonable default/max timeout make it configurable... + +versioning + +- Additional VERSION-CONTROL Semantics with workspace not implemented. +- BaseLine/Activity not respected yet (see jsr283) diff --git a/jackrabbit-jcr-server/pom.xml b/jackrabbit-jcr-server/pom.xml new file mode 100644 index 00000000000..61baa32735b --- /dev/null +++ b/jackrabbit-jcr-server/pom.xml @@ -0,0 +1,276 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-jcr-server + Jackrabbit JCR Server + WebDAV server implementations for JCR + bundle + + + + + maven-surefire-plugin + + + **/*Test.java + + once + ${test.opts} + + + derby.stream.error.file + target/derby.log + + + + + + org.apache.felix + maven-scr-plugin + 1.24.0 + + + generate-scr-scrdescriptor + + scr + + + + The Apache Software Foundation + + + + + + + + org.slf4j + slf4j-simple + ${slf4j.api.version} + + + + org.codehaus.plexus + plexus-utils + 3.0.10 + + + + + org.apache.felix + maven-bundle-plugin + true + + + + org.apache.jackrabbit.server + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.apache.felix + + maven-scr-plugin + + + [1.7.2,) + + + scr + + + + + + + + + + + + + + + + + integrationTesting + + litmus + + + + + maven-surefire-plugin + + + + jackrabbit.test.integration + true + + + litmus + ${litmus} + + + derby.system.durability + test + + + derby.storage.fileSyncTransactionLog + true + + + derby.stream.error.file + target/derby.log + + + + + + + + + + + + javax.jcr + jcr + + + org.apache.jackrabbit + jackrabbit-spi-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-webdav + ${project.version} + + + org.apache.tika + tika-core + + + org.slf4j + slf4j-api + + + javax.servlet + servlet-api + provided + + + commons-fileupload + commons-fileupload + + + + + org.osgi + org.osgi.compendium + 4.2.0 + provided + + + org.apache.felix + org.apache.felix.scr.annotations + 1.9.6 + provided + + + org.osgi + org.osgi.annotation + provided + + + + junit + junit + test + + + org.easymock + easymock + test + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-jcr-tests + ${project.version} + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-servlet + test + + + ch.qos.logback + logback-classic + test + + + org.mockito + mockito-core + test + + + + diff --git a/jackrabbit-jcr-server/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-jcr-server/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/BasicCredentialsProvider.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/BasicCredentialsProvider.java new file mode 100644 index 00000000000..fb810b37289 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/BasicCredentialsProvider.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.server; + +import org.apache.jackrabbit.util.Base64; +import org.apache.jackrabbit.webdav.DavConstants; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.SimpleCredentials; +import javax.jcr.GuestCredentials; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * This Class implements a credentials provider that extracts the credentials + * from the 'WWW-Authenticate' header and only supports 'Basic' authentication. + */ +public class BasicCredentialsProvider implements CredentialsProvider { + + public static final String EMPTY_DEFAULT_HEADER_VALUE = ""; + public static final String GUEST_DEFAULT_HEADER_VALUE = "guestcredentials"; + + private final String defaultHeaderValue; + + /** + * Constructs a new BasicCredentialsProvider with the given default + * value. See {@link #getCredentials} for details. + * + * @param defaultHeaderValue + */ + public BasicCredentialsProvider(String defaultHeaderValue) { + this.defaultHeaderValue = defaultHeaderValue; + } + + /** + * {@inheritDoc} + * + * Build a {@link Credentials} object for the given authorization header. + * The creds may be used to login to the repository. If the specified header + * string is null the behaviour depends on the + * {@link #defaultHeaderValue} field:
        + *

          + *
        • if this field is null, a LoginException is thrown. + * This is suitable for clients (eg. webdav clients) for with + * sending a proper authorization header is not possible, if the + * server never send a 401. + *
        • if this an empty string, null-credentials are returned, thus + * forcing an null login on the repository + *
        • if this field has a 'user:password' value, the respective + * simple credentials are generated. + *
        + *

        + * If the request header is present but cannot be parsed a + * ServletException is thrown. + * + * @param request the servlet request + * @return credentials or null. + * @throws ServletException If the Authorization header cannot be decoded. + * @throws LoginException if no suitable auth header and missing-auth-mapping + * is not present + */ + public Credentials getCredentials(HttpServletRequest request) + throws LoginException, ServletException { + try { + String authHeader = request.getHeader(DavConstants.HEADER_AUTHORIZATION); + if (authHeader != null) { + String[] authStr = authHeader.split(" "); + if (authStr.length >= 2 && authStr[0].equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Base64.decode(authStr[1].toCharArray(), out); + String decAuthStr = out.toString("ISO-8859-1"); + int pos = decAuthStr.indexOf(':'); + String userid = decAuthStr.substring(0, pos); + String passwd = decAuthStr.substring(pos + 1); + return new SimpleCredentials(userid, passwd.toCharArray()); + } + throw new ServletException("Unable to decode authorization."); + } else { + // check special handling + if (defaultHeaderValue == null) { + throw new LoginException(); + } else if (EMPTY_DEFAULT_HEADER_VALUE.equals(defaultHeaderValue)) { + return null; + } else if (GUEST_DEFAULT_HEADER_VALUE.equals(defaultHeaderValue)) { + return new GuestCredentials(); + } else { + int pos = defaultHeaderValue.indexOf(':'); + if (pos < 0) { + return new SimpleCredentials(defaultHeaderValue, new char[0]); + } else { + return new SimpleCredentials( + defaultHeaderValue.substring(0, pos), + defaultHeaderValue.substring(pos+1).toCharArray() + ); + } + } + } + } catch (IOException e) { + throw new ServletException("Unable to decode authorization: " + e.toString()); + } + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/CredentialsProvider.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/CredentialsProvider.java new file mode 100644 index 00000000000..63a1920c86a --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/CredentialsProvider.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.server; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +/** + * This Interface defines a provider for the credentials. + */ +public interface CredentialsProvider { + + /** + * Extracts the credentials from the given servlet request. + * + * @param request + * @return the credentials or null + * @throws LoginException if the credentials are invalid + * @throws ServletException if an error occurs + */ + public Credentials getCredentials(HttpServletRequest request) + throws LoginException, ServletException; +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/SessionProvider.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/SessionProvider.java new file mode 100644 index 00000000000..0fec8622e65 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/SessionProvider.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.server; + +import javax.jcr.LoginException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +/** + * This Interface defines a provider for repository sessions + */ +public interface SessionProvider { + + /** + * Provides the repository session suitable for the given request. + * + * @param request + * @param rep the repository to login + * @param workspace the workspace name + * @return the session or null + * @throws LoginException if the credentials are invalid + * @throws ServletException if an error occurs + */ + public Session getSession(HttpServletRequest request, Repository rep, String workspace) + throws LoginException, ServletException, RepositoryException; + + /** + * Informs this provider that the session acquired by a previous + * {@link SessionProvider#getSession} call is no longer needed. + * + * @param session + */ + public void releaseSession(Session session); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/SessionProviderImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/SessionProviderImpl.java new file mode 100644 index 00000000000..52054af9f6b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/SessionProviderImpl.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.server; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +/** + * This class implements a default session provider based on a given + * {@link CredentialsProvider credentials provider}. Additionally, + * since Jackrabbit 2.4, if another session provider is available as + * the "org.apache.jackrabbit.server.SessionProvider" request attribute, + * then that provider is asked first for a session before the default + * credential-based login mechanism is used. + */ +public class SessionProviderImpl implements SessionProvider { + + /** + * the credentials provider + */ + private CredentialsProvider cp; + + /** + * Map of sessions acquired from custom session providers looked up + * from request attributes. We need to keep track of such providers + * so we can route the {@link #releaseSession(Session)} call to the + * correct provider. + */ + private final Map externalSessions = + Collections.synchronizedMap(new HashMap()); + + /** + * Creates a new SessionProvider + * + * @param cp + */ + public SessionProviderImpl(CredentialsProvider cp) { + this.cp = cp; + } + + /** + * {@inheritDoc } + */ + public Session getSession(HttpServletRequest request, + Repository repository, String workspace) throws LoginException, + RepositoryException, ServletException { + Session s = null; + + // JCR-3222: Check if a custom session provider is available as a + // request attribute. If one is available, ask it first for a session. + Object object = request.getAttribute(SessionProvider.class.getName()); + if (object instanceof SessionProvider) { + SessionProvider provider = (SessionProvider) object; + s = provider.getSession(request, repository, workspace); + if (s != null) { + externalSessions.put(s, provider); + } + } + + if (s == null) { + Credentials creds = cp.getCredentials(request); + if (creds == null) { + s = repository.login(workspace); + } else { + s = repository.login(creds, workspace); + } + } + + return s; + } + + /** + * {@inheritDoc } + */ + public void releaseSession(Session session) { + // JCR-3222: If the session was acquired from a custom session + // provider, we need to ask that provider to release the session. + SessionProvider provider = externalSessions.remove(session); + if (provider != null) { + provider.releaseSession(session); + } else { + session.logout(); + } + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/AbstractExportContext.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/AbstractExportContext.java new file mode 100644 index 00000000000..461dbae25d8 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/AbstractExportContext.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.server.io; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; + +/** + * AbstractExportContext covers methods common to most ExportContext + * implementations. + */ +public abstract class AbstractExportContext implements ExportContext { + + private static Logger log = LoggerFactory.getLogger(AbstractExportContext.class); + + private final IOListener ioListener; + private final Item exportRoot; + private final boolean hasStream; + + protected boolean completed; + + public AbstractExportContext( + Item exportRoot, boolean hasStream, IOListener ioListener) { + this.exportRoot = exportRoot; + this.hasStream = hasStream; + this.ioListener = (ioListener != null) ? ioListener : new DefaultIOListener(log); + } + + public IOListener getIOListener() { + return ioListener; + } + + public Item getExportRoot() { + return exportRoot; + } + + public boolean hasStream() { + return hasStream; + } + + public void informCompleted(boolean success) { + completed = true; + } + + public boolean isCompleted() { + return completed; + } + + protected void checkCompleted() { + if (completed) { + throw new IllegalStateException("ExportContext has already been finalized."); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/BoundedInputStream.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/BoundedInputStream.java new file mode 100644 index 00000000000..bcb81beb796 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/BoundedInputStream.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * This is a stream that will only supply bytes up to a certain length - if its + * position goes above that, it will stop. + *

        + * This is useful to wrap ServletInputStreams. The ServletInputStream will block + * if you try to read content from it that isn't there, because it doesn't know + * whether the content hasn't arrived yet or whether the content has finished. + * So, one of these, initialized with the Content-length sent in the + * ServletInputStream's header, will stop it blocking, providing it's been sent + * with a correct content length. + * + * @author InigoSurguy + */ +public class BoundedInputStream extends InputStream { + + /** the wrapped input stream */ + private final InputStream in; + + /** the max length to provide */ + private final int max; + + /** the number of bytes already returned */ + private int pos = 0; + + /** the marked position */ + private int mark = -1; + + /** flag if close should be propagated */ + private boolean propagateClose = true; + + /** + * Creates a new BoundedInputStream that wraps the given input + * stream and limits it to a certain size. + * + * @param in The wrapped input stream + * @param size The maximum number of bytes to return + */ + public BoundedInputStream(InputStream in, long size) { + // Some badly designed methods - eg the servlet API - overload length + // such that "-1" means stream finished + this.max = (int) size; + this.in = in; + } + + public BoundedInputStream(InputStream in) { + this(in, -1); + } + + /** + * {@inheritDoc} + */ + @Override + public int read() throws IOException { + if (max>=0 && pos==max) { + return -1; + } + int result = in.read(); + pos++; + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public int read(byte[] b) throws IOException { + return this.read(b, 0, b.length); + } + + /** + * {@inheritDoc} + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (max>=0 && pos>=max) { + return -1; + } + int maxRead = max>=0 ? Math.min(len, max-pos) : len; + int bytesRead = in.read(b, off, maxRead); + + if (bytesRead==-1) { + return -1; + } + + pos+=bytesRead; + return bytesRead; + } + + /** + * {@inheritDoc} + */ + @Override + public long skip(long n) throws IOException { + long toSkip = max>=0 ? Math.min(n, max-pos) : n; + long skippedBytes = in.skip(toSkip); + pos+=skippedBytes; + return skippedBytes; + } + + /** + * {@inheritDoc} + */ + @Override + public int available() throws IOException { + if (max>=0 && pos>=max) { + return 0; + } + return in.available(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return in.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + if (propagateClose) { + in.close(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void reset() throws IOException { + in.reset(); + pos = mark; + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void mark(int readlimit) { + in.mark(readlimit); + mark = pos; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean markSupported() { + return in.markSupported(); + } + + public boolean isPropagateClose() { + return propagateClose; + } + + public void setPropagateClose(boolean propagateClose) { + this.propagateClose = propagateClose; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveContext.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveContext.java new file mode 100644 index 00000000000..877218e322b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveContext.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.server.io; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; + +/** + * CopyMoveContext... + */ +public interface CopyMoveContext { + + /** + * @return true if this context defines a shallow copy. + */ + boolean isShallowCopy(); + + /** + * @return the jcr session associated with this context. + */ + Session getSession(); + + /** + * @return The JCR workspace associated with this context. + * @throws RepositoryException If an error occurs. + */ + Workspace getWorkspace() throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveContextImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveContextImpl.java new file mode 100644 index 00000000000..8887206a220 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveContextImpl.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.server.io; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; + +/** + * CopyMoveContextImpl... + */ +public class CopyMoveContextImpl implements CopyMoveContext { + + private final boolean isShallow; + private final Session session; + + public CopyMoveContextImpl(Session session) { + this(session, false); + } + + public CopyMoveContextImpl(Session session, boolean isShallowCopy) { + this.isShallow = isShallowCopy; + this.session = session; + } + + //----------------------------------------------------< CopyMoveContext >--- + /** + * @see org.apache.jackrabbit.server.io.CopyMoveContext#isShallowCopy() + */ + public boolean isShallowCopy() { + return isShallow; + } + + /** + * @see org.apache.jackrabbit.server.io.CopyMoveContext#getSession() + */ + public Session getSession() { + return session; + } + + /** + * @see org.apache.jackrabbit.server.io.CopyMoveContext#getWorkspace() + */ + public Workspace getWorkspace() throws RepositoryException { + return session.getWorkspace(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveHandler.java new file mode 100644 index 00000000000..fb237938889 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveHandler.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.server.io; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; + +/** + * CopyMoveHandler... + */ +public interface CopyMoveHandler { + + /** + * Validates if this handler is able to execute a copy with the given + * parameters. + * + * @param context The context of the copy. + * @param source The source of the copy. + * @param destination The destination of the copy. + * @return true if this instance can handle a copy with the given parameters; + * false otherwise. + */ + public boolean canCopy(CopyMoveContext context, DavResource source, DavResource destination); + + /** + * Executes the copy with the given parameters. + * + * @param context The context of the copy. + * @param source The source of the copy. + * @param destination The destination of the copy. + * @return true if this instance successfully executed the copy operation + * with the given parameters; false otherwise. + * @throws DavException If an error occurs. + */ + public boolean copy(CopyMoveContext context, DavResource source, DavResource destination) throws DavException; + + /** + * Validates if this handler is able to execute a move with the given + * parameters. + * + * @param context The context of the move. + * @param source The source of the move. + * @param destination The destination of the move. + * @return true if this instance successfully executed the move operation + * with the given parameters; false otherwise. + */ + public boolean canMove(CopyMoveContext context, DavResource source, DavResource destination); + + /** + * Executes the move with the given parameters. + * + * @param context The context of the move. + * @param source The source of the move. + * @param destination The destination of the move. + * @return true if this instance successfully executed the move operation + * with the given parameters; + * false otherwise. + * @throws DavException If an error occurs. + */ + public boolean move(CopyMoveContext context, DavResource source, DavResource destination) throws DavException; +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveManager.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveManager.java new file mode 100644 index 00000000000..2eafa52dac8 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveManager.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.server.io; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; + +/** + * CopyMoveManager... + */ +public interface CopyMoveManager { + + /** + * Handles the copy command + * + * @param context The context used for this copy operation. + * @param source The source of the copy. + * @param destination The destination of the copy. + * @return true if the copy succeeded. + * @throws DavException If an error occurs. + */ + public boolean copy(CopyMoveContext context, DavResource source, DavResource destination) throws DavException; + + /** + * Handles the move command + * + * @param context The context used for this move operation. + * @param source The source of the move. + * @param destination The destination of the move. + * @return true if the move succeeded. + * @throws DavException If an error occurs. + */ + public boolean move(CopyMoveContext context, DavResource source, DavResource destination) throws DavException; + + /** + * Adds the specified handler to the list of handlers. + * + * @param copyMoveHandler handler to be added + */ + public void addCopyMoveHandler(CopyMoveHandler copyMoveHandler); + + /** + * Returns all handlers that have been added to this manager. + * + * @return Array of all handlers + */ + public CopyMoveHandler[] getCopyMoveHandlers(); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveManagerImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveManagerImpl.java new file mode 100644 index 00000000000..702d82b8741 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/CopyMoveManagerImpl.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.io; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; + +/** + * CopyMoveManagerImpl... + */ +public class CopyMoveManagerImpl implements CopyMoveManager { + + private static CopyMoveManager DEFAULT_MANAGER; + + private final List copyMoveHandlers = new ArrayList(); + + /** + * Create a new CopyMoveManagerImpl. + */ + public CopyMoveManagerImpl() { + } + + //----------------------------------------------------< CopyMoveManager >--- + /** + * @see CopyMoveManager#copy(CopyMoveContext,org.apache.jackrabbit.webdav.DavResource,org.apache.jackrabbit.webdav.DavResource) + */ + public boolean copy(CopyMoveContext context, DavResource source, DavResource destination) throws DavException { + boolean success = false; + CopyMoveHandler[] copyMoveHandlers = getCopyMoveHandlers(); + for (int i = 0; i < copyMoveHandlers.length && !success; i++) { + CopyMoveHandler cmh = copyMoveHandlers[i]; + if (cmh.canCopy(context, source, destination)) { + success = cmh.copy(context, source, destination); + } + } + return success; + } + + /** + * @see CopyMoveManager#move(CopyMoveContext,org.apache.jackrabbit.webdav.DavResource,org.apache.jackrabbit.webdav.DavResource) + */ + public boolean move(CopyMoveContext context, DavResource source, DavResource destination) throws DavException { + boolean success = false; + CopyMoveHandler[] copyMoveHandlers = getCopyMoveHandlers(); + for (int i = 0; i < copyMoveHandlers.length && !success; i++) { + CopyMoveHandler cmh = copyMoveHandlers[i]; + if (cmh.canMove(context, source, destination)) { + success = cmh.move(context, source, destination); + } + } + return success; + } + + /** + * @see org.apache.jackrabbit.server.io.CopyMoveManager#addCopyMoveHandler(CopyMoveHandler) + */ + public void addCopyMoveHandler(CopyMoveHandler copyMoveHandler) { + if (copyMoveHandler == null) { + throw new IllegalArgumentException("'null' is not a valid copyMoveHandler."); + } + copyMoveHandlers.add(copyMoveHandler); + } + + /** + * @see CopyMoveManager#getCopyMoveHandlers() + */ + public CopyMoveHandler[] getCopyMoveHandlers() { + return copyMoveHandlers.toArray(new CopyMoveHandler[copyMoveHandlers.size()]); + } + + //-------------------------------------------------------------------------- + /** + * @return an instance of CopyMoveManager populated with default handlers. + */ + public static CopyMoveManager getDefaultManager() { + if (DEFAULT_MANAGER == null) { + CopyMoveManager manager = new CopyMoveManagerImpl(); + manager.addCopyMoveHandler(new DefaultHandler()); + DEFAULT_MANAGER = manager; + } + return DEFAULT_MANAGER; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultHandler.java new file mode 100644 index 00000000000..6122a552a29 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultHandler.java @@ -0,0 +1,866 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.io; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.NamespaceHelper; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.tika.metadata.Metadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.PropertyIterator; +import javax.jcr.Session; +import javax.jcr.nodetype.PropertyDefinition; +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import java.util.List; +import java.util.HashMap; + +/** + * DefaultHandler implements a simple IOHandler that creates 'file' + * and 'folder' nodes. This handler will create the following nodes: + *

          + *
        • New Collection: creates a new node with the {@link #getCollectionNodeType() + * collection nodetype}. The name of the node corresponds to the systemId + * present on the import context.
        • + * + *
        • New Non-Collection: first creates a new node with the {@link #getNodeType() + * non-collection nodetype}. The name of the node corresponds to the systemId + * present on the import context. Below it creates a node with name + * {@link JcrConstants#JCR_CONTENT jcr:content} and the nodetype specified + * by {@link #getContentNodeType()}.
        • + *
        + *

        + * Import of the content:
        + * The content is imported to the {@link JcrConstants#JCR_DATA} property of the + * content node. By default this handler will fail on a attempt to create/replace + * a collection if {@link ImportContext#hasStream()} is true. + * Subclasses therefore should provide their own {@link #importData(ImportContext, boolean, Node) + * importData} method, that handles the data according their needs. + */ +public class DefaultHandler implements IOHandler, PropertyHandler, CopyMoveHandler, DeleteHandler { + + private static Logger log = LoggerFactory.getLogger(DefaultHandler.class); + + private String collectionNodetype; + + private String defaultNodetype; + + private String contentNodetype; + + private IOManager ioManager; + + /** + * Creates a new DefaultHandler with default nodetype definitions:
        + *

          + *
        • Nodetype for Collection: {@link JcrConstants#NT_FOLDER nt:folder}
        • + *
        • Nodetype for Non-Collection: {@link JcrConstants#NT_FILE nt:file}
        • + *
        • Nodetype for Non-Collection content: {@link JcrConstants#NT_UNSTRUCTURED nt:unstructured}
        • + *
        + */ + public DefaultHandler() { + this(null); + } + + /** + * Creates a new DefaultHandler with default nodetype definitions:
        + *
          + *
        • Nodetype for Collection: {@link JcrConstants#NT_FOLDER nt:folder}
        • + *
        • Nodetype for Non-Collection: {@link JcrConstants#NT_FILE nt:file}
        • + *
        • Nodetype for Non-Collection content: {@link JcrConstants#NT_UNSTRUCTURED nt:unstructured}
        • + *
        + * + * @param ioManager the I/O manager + */ + public DefaultHandler(IOManager ioManager) { + this(ioManager, + JcrConstants.NT_FOLDER, + JcrConstants.NT_FILE, + // IMPORTANT NOTE: for webDAV compliance the default type + // of the content node has been changed from nt:resource to + // nt:unstructured + JcrConstants.NT_UNSTRUCTURED); + } + + /** + * Creates a new DefaultHandler. Please note that the specified + * nodetypes must match the definitions of the defaults. + */ + public DefaultHandler(IOManager ioManager, String collectionNodetype, String defaultNodetype, String contentNodetype) { + this.ioManager = ioManager; + + this.collectionNodetype = collectionNodetype; + this.defaultNodetype = defaultNodetype; + this.contentNodetype = contentNodetype; + } + + /** + * @see IOHandler#getIOManager() + */ + public IOManager getIOManager() { + return ioManager; + } + + /** + * @see IOHandler#setIOManager(IOManager) + */ + public void setIOManager(IOManager ioManager) { + this.ioManager = ioManager; + } + + /** + * @see IOHandler#getName() + */ + public String getName() { + return getClass().getName(); + } + + /** + * @see IOHandler#canImport(ImportContext, boolean) + */ + public boolean canImport(ImportContext context, boolean isCollection) { + if (context == null || context.isCompleted()) { + return false; + } + Item contextItem = context.getImportRoot(); + return contextItem != null && contextItem.isNode() && context.getSystemId() != null; + } + + /** + * @see IOHandler#canImport(ImportContext, DavResource) + */ + public boolean canImport(ImportContext context, DavResource resource) { + if (resource == null) { + return false; + } + return canImport(context, resource.isCollection()); + } + + /** + * @see IOHandler#importContent(ImportContext, boolean) + */ + public boolean importContent(ImportContext context, boolean isCollection) throws IOException { + if (!canImport(context, isCollection)) { + throw new IOException(getName() + ": Cannot import " + context.getSystemId()); + } + + boolean success = false; + try { + Node contentNode = getContentNode(context, isCollection); + success = importData(context, isCollection, contentNode); + if (success) { + success = importProperties(context, isCollection, contentNode); + } + } catch (RepositoryException e) { + success = false; + throw new IOException(e.getMessage()); + } finally { + // revert any changes made in case the import failed. + if (!success) { + try { + context.getImportRoot().refresh(false); + } catch (RepositoryException e) { + throw new IOException(e.getMessage()); + } + } + } + return success; + } + + /** + * @see IOHandler#importContent(ImportContext, DavResource) + */ + public boolean importContent(ImportContext context, DavResource resource) throws IOException { + if (!canImport(context, resource)) { + throw new IOException(getName() + ": Cannot import " + context.getSystemId()); + } + return importContent(context, resource.isCollection()); + } + + /** + * Imports the data present on the import context to the specified content + * node. + */ + protected boolean importData(ImportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException { + InputStream in = context.getInputStream(); + if (in != null) { + // NOTE: with the default folder-nodetype (nt:folder) no inputstream + // is allowed. setting the property would therefore fail. + if (isCollection) { + return false; + } + try { + contentNode.setProperty(JcrConstants.JCR_DATA, in); + } finally { + in.close(); + } + } + // success if no data to import. + return true; + } + + /** + * Imports the properties present on the specified context to the content + * node. + */ + protected boolean importProperties(ImportContext context, boolean isCollection, Node contentNode) { + try { + // set mimeType property upon resource creation but don't modify + // it on a subsequent PUT. In contrast to a PROPPATCH request, which + // is handled by #importProperties(PropertyContext, boolean)} + if (!contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { + contentNode.setProperty(JcrConstants.JCR_MIMETYPE, context.getMimeType()); + } + } catch (RepositoryException e) { + // ignore: property may not be present on the node + } + try { + // set encoding property upon resource creation but don't modify + // it on a subsequent PUT. In contrast to a PROPPATCH request, which + // is handled by #importProperties(PropertyContext, boolean)} + if (!contentNode.hasProperty(JcrConstants.JCR_ENCODING)) { + contentNode.setProperty(JcrConstants.JCR_ENCODING, context.getEncoding()); + } + } catch (RepositoryException e) { + // ignore: property may not be present on the node + } + setLastModified(contentNode, context.getModificationTime()); + return true; + } + + /** + * Retrieves/creates the node that will be used to import properties and + * data. In case of a non-collection this includes and additional content node + * to be created beside the 'file' node. + *

        + * Please note: If the jcr:content node already exists and contains child + * nodes, those will be removed in order to make sure, that the import + * really replaces the existing content of the file-node. + */ + protected Node getContentNode(ImportContext context, boolean isCollection) throws RepositoryException { + Node parentNode = (Node)context.getImportRoot(); + String name = context.getSystemId(); + if (parentNode.hasNode(name)) { + parentNode = parentNode.getNode(name); + } else { + String ntName = (isCollection) ? getCollectionNodeType() : getNodeType(); + parentNode = parentNode.addNode(name, ntName); + } + Node contentNode = null; + if (isCollection) { + contentNode = parentNode; + } else { + if (parentNode.hasNode(JcrConstants.JCR_CONTENT)) { + contentNode = parentNode.getNode(JcrConstants.JCR_CONTENT); + // check if nodetype is compatible (might be update of an existing file) + if (contentNode.isNodeType(getContentNodeType()) || + !forceCompatibleContentNodes()) { + // remove all entries in the jcr:content since replacing content + // includes properties (DefaultHandler) and nodes (e.g. ZipHandler) + if (contentNode.hasNodes()) { + NodeIterator it = contentNode.getNodes(); + while (it.hasNext()) { + it.nextNode().remove(); + } + } + } else { + contentNode.remove(); + contentNode = null; + } + } + if (contentNode == null) { + // JCR-2070: Use the predefined content node type only + // when the underlying repository allows it to be used + if (parentNode.getPrimaryNodeType().canAddChildNode( + JcrConstants.JCR_CONTENT, getContentNodeType())) { + contentNode = parentNode.addNode( + JcrConstants.JCR_CONTENT, getContentNodeType()); + } else { + contentNode = parentNode.addNode(JcrConstants.JCR_CONTENT); + } + } + } + return contentNode; + } + + /** + * Defines if content nodes should be replace if they don't have the + * node type given by {@link #getCollectionNodeType()}. + * + * @return true if content nodes should be replaced. + */ + protected boolean forceCompatibleContentNodes() { + return false; + } + + /** + * Returns true if the export root is a node and if it contains a child node + * with name {@link JcrConstants#JCR_CONTENT jcr:content} in case this + * export is not intended for a collection. + * + * @return true if the export root is a node. If the specified boolean parameter + * is false (not a collection export) the given export root must contain a + * child node with name {@link JcrConstants#JCR_CONTENT jcr:content}. + * + * @see IOHandler#canExport(ExportContext, boolean) + */ + public boolean canExport(ExportContext context, boolean isCollection) { + if (context == null || context.isCompleted()) { + return false; + } + Item exportRoot = context.getExportRoot(); + boolean success = exportRoot != null && exportRoot.isNode(); + if (success && !isCollection) { + try { + Node n = ((Node)exportRoot); + success = n.hasNode(JcrConstants.JCR_CONTENT); + } catch (RepositoryException e) { + // should never occur. + success = false; + } + } + return success; + } + + /** + * @see IOHandler#canExport(ExportContext, DavResource) + */ + public boolean canExport(ExportContext context, DavResource resource) { + if (resource == null) { + return false; + } + return canExport(context, resource.isCollection()); + } + + /** + * Retrieves the content node that will be used for exporting properties and + * data and calls the corresponding methods. + * + * @param context the export context + * @param isCollection true if collection + * @see #exportProperties(ExportContext, boolean, Node) + * @see #exportData(ExportContext, boolean, Node) + */ + public boolean exportContent(ExportContext context, boolean isCollection) throws IOException { + if (!canExport(context, isCollection)) { + throw new IOException(getName() + ": Cannot export " + context.getExportRoot()); + } + try { + Node contentNode = getContentNode(context, isCollection); + exportProperties(context, isCollection, contentNode); + if (context.hasStream()) { + exportData(context, isCollection, contentNode); + } // else: missing stream. ignore. + return true; + } catch (RepositoryException e) { + // should never occur, since the proper structure of the content + // node must be asserted in the 'canExport' call. + throw new IOException(e.getMessage()); + } + } + + /** + * Same as (@link IOHandler#exportContent(ExportContext, boolean)} where + * the boolean values is defined by {@link DavResource#isCollection()}. + * + * @see IOHandler#exportContent(ExportContext, DavResource) + */ + public boolean exportContent(ExportContext context, DavResource resource) throws IOException { + if (!canExport(context, resource)) { + throw new IOException(getName() + ": Cannot export " + context.getExportRoot()); + } + return exportContent(context, resource.isCollection()); + } + + /** + * Checks if the given content node contains a jcr:data property + * and spools its value to the output stream of the export context.
        + * Please note, that subclasses that define a different structure of the + * content node should create their own + * {@link #exportData(ExportContext, boolean, Node) exportData} method. + * + * @param context export context + * @param isCollection true if collection + * @param contentNode the content node + * @throws IOException if an I/O error occurs + */ + protected void exportData(ExportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException { + if (contentNode.hasProperty(JcrConstants.JCR_DATA)) { + Property p = contentNode.getProperty(JcrConstants.JCR_DATA); + IOUtil.spool(p.getStream(), context.getOutputStream()); + } // else: stream undefined -> content length was not set + } + + /** + * Retrieves mimetype, encoding and modification time from the content node. + * The content length is determined by the length of the jcr:data property + * if it is present. The creation time however is retrieved from the parent + * node (in case of isCollection == false only). + * + * @param context the export context + * @param isCollection true if collection + * @param contentNode the content node + * @throws java.io.IOException If an error occurs. + */ + protected void exportProperties(ExportContext context, boolean isCollection, Node contentNode) throws IOException { + try { + // only non-collections: 'jcr:created' is present on the parent 'fileNode' only + if (!isCollection && contentNode.getDepth() > 0 && contentNode.getParent().hasProperty(JcrConstants.JCR_CREATED)) { + long cTime = contentNode.getParent().getProperty(JcrConstants.JCR_CREATED).getValue().getLong(); + context.setCreationTime(cTime); + } + + long length = IOUtil.UNDEFINED_LENGTH; + if (contentNode.hasProperty(JcrConstants.JCR_DATA)) { + Property p = contentNode.getProperty(JcrConstants.JCR_DATA); + length = p.getLength(); + context.setContentLength(length); + } + + String mimeType = null; + String encoding = null; + if (contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { + mimeType = contentNode.getProperty(JcrConstants.JCR_MIMETYPE).getString(); + } + if (contentNode.hasProperty(JcrConstants.JCR_ENCODING)) { + encoding = contentNode.getProperty(JcrConstants.JCR_ENCODING).getString(); + // ignore "" encoding (although this is avoided during import) + if ("".equals(encoding)) { + encoding = null; + } + } + context.setContentType(mimeType, encoding); + + long modTime = IOUtil.UNDEFINED_TIME; + if (contentNode.hasProperty(JcrConstants.JCR_LASTMODIFIED)) { + modTime = contentNode.getProperty(JcrConstants.JCR_LASTMODIFIED).getLong(); + context.setModificationTime(modTime); + } else { + context.setModificationTime(System.currentTimeMillis()); + } + + if (length > IOUtil.UNDEFINED_LENGTH && modTime > IOUtil.UNDEFINED_TIME) { + String etag = "\"" + length + "-" + modTime + "\""; + context.setETag(etag); + } + } catch (RepositoryException e) { + // should never occur + log.error("Unexpected error {} while exporting properties: {}", e.getClass().getName(), e.getMessage()); + throw new IOException(e.getMessage()); + } + } + + /** + * Retrieves the content node that contains the data to be exported. In case + * isCollection is true, this corresponds to the export root. Otherwise there + * must be a child node with name {@link JcrConstants#JCR_CONTENT jcr:content}. + * + * @param context the export context + * @param isCollection true if collection + * @return content node used for the export + * @throws RepositoryException if an error during repository access occurs. + */ + protected Node getContentNode(ExportContext context, boolean isCollection) throws RepositoryException { + Node contentNode = (Node)context.getExportRoot(); + // 'file' nodes must have an jcr:content child node (see canExport) + if (!isCollection) { + contentNode = contentNode.getNode(JcrConstants.JCR_CONTENT); + } + return contentNode; + } + + /** + * Name of the nodetype to be used to create a new collection node (folder) + * + * @return nodetype name + */ + public String getCollectionNodeType() { + return collectionNodetype; + } + + /** + * Name of the nodetype to be used to create a new non-collection node (file) + * + * @return nodetype name + */ + public String getNodeType() { + return defaultNodetype; + } + + /** + * Name of the nodetype to be used to create the content node below + * a new non-collection node, whose name is always {@link JcrConstants#JCR_CONTENT + * jcr:content}. + * + * @return nodetype name + */ + public String getContentNodeType() { + return contentNodetype; + } + + //----------------------------------------------------< PropertyHandler >--- + + public boolean canExport(PropertyExportContext context, boolean isCollection) { + return canExport((ExportContext) context, isCollection); + } + + public boolean exportProperties(PropertyExportContext exportContext, boolean isCollection) throws RepositoryException { + if (!canExport(exportContext, isCollection)) { + throw new RepositoryException("PropertyHandler " + getName() + " failed to export properties."); + } + + Node cn = getContentNode(exportContext, isCollection); + try { + // export the properties common with normal I/O handling + exportProperties(exportContext, isCollection, cn); + + // export all other properties as well + PropertyIterator it = cn.getProperties(); + while (it.hasNext()) { + Property p = it.nextProperty(); + String name = p.getName(); + PropertyDefinition def = p.getDefinition(); + if (def.isMultiple() || isDefinedByFilteredNodeType(def)) { + log.debug("Skip property '" + name + "': not added to webdav property set."); + continue; + } + if (JcrConstants.JCR_DATA.equals(name) + || JcrConstants.JCR_MIMETYPE.equals(name) + || JcrConstants.JCR_ENCODING.equals(name) + || JcrConstants.JCR_LASTMODIFIED.equals(name)) { + continue; + } + + DavPropertyName davName = getDavName(name, p.getSession()); + exportContext.setProperty(davName, p.getValue().getString()); + } + return true; + } catch (IOException e) { + // should not occur (log output see 'exportProperties') + return false; + } + } + + public boolean canImport(PropertyImportContext context, boolean isCollection) { + if (context == null || context.isCompleted()) { + return false; + } + Item contextItem = context.getImportRoot(); + try { + return contextItem != null && contextItem.isNode() && (isCollection || ((Node)contextItem).hasNode(JcrConstants.JCR_CONTENT)); + } catch (RepositoryException e) { + log.error("Unexpected error: " + e.getMessage()); + return false; + } + } + + public Map importProperties(PropertyImportContext importContext, boolean isCollection) throws RepositoryException { + if (!canImport(importContext, isCollection)) { + throw new RepositoryException("PropertyHandler " + getName() + " failed import properties"); + } + + // loop over List and remember all properties and propertyNames + // that failed to be imported (set or remove). + Map failures = new HashMap(); + List changeList = importContext.getChangeList(); + + // for collections the import-root is the target node where properties + // are altered. in contrast 'non-collections' are with the handler + // represented by 'file' nodes, that must have a jcr:content child + // node, which holds all properties except jcr:created. + // -> see canImport for the corresponding assertions + Node cn = (Node) importContext.getImportRoot(); + if (!isCollection && cn.hasNode(JcrConstants.JCR_CONTENT)) { + cn = cn.getNode(JcrConstants.JCR_CONTENT); + } + + if (changeList != null) { + for (PropEntry propEntry : changeList) { + try { + if (propEntry instanceof DavPropertyName) { + // remove + DavPropertyName propName = (DavPropertyName) propEntry; + removeJcrProperty(propName, cn); + } else if (propEntry instanceof DavProperty) { + // add or modify property + DavProperty prop = (DavProperty) propEntry; + setJcrProperty(prop, cn); + } else { + // ignore any other entry in the change list + log.error("unknown object in change list: " + propEntry.getClass().getName()); + } + } catch (RepositoryException e) { + failures.put(propEntry, e); + } + } + } + if (failures.isEmpty()) { + setLastModified(cn, IOUtil.UNDEFINED_LENGTH); + } + return failures; + } + + /** + * Detects the media type of a document based on the given name. + * + * @param name document name + * @return detected content type (or application/octet-stream) + */ + protected String detect(String name) { + try { + Metadata metadata = new Metadata(); + metadata.set(Metadata.RESOURCE_NAME_KEY, name); + if (ioManager != null && ioManager.getDetector() != null) { + return ioManager.getDetector().detect(null, metadata).toString(); + } else { + return "application/octet-stream"; + } + } catch (IOException e) { + // Can not happen since the InputStream above is null + throw new IllegalStateException( + "Unexpected IOException", e); + } + } + + //----------------------------------------------------< CopyMoveHandler >--- + /** + * @see CopyMoveHandler#canCopy(CopyMoveContext, org.apache.jackrabbit.webdav.DavResource, org.apache.jackrabbit.webdav.DavResource) + */ + public boolean canCopy(CopyMoveContext context, DavResource source, DavResource destination) { + return true; + } + + /** + * @see CopyMoveHandler#copy(CopyMoveContext, org.apache.jackrabbit.webdav.DavResource, org.apache.jackrabbit.webdav.DavResource) + */ + public boolean copy(CopyMoveContext context, DavResource source, DavResource destination) throws DavException { + if (context.isShallowCopy() && source.isCollection()) { + // TODO: currently no support for shallow copy; however this is + // only relevant if the source resource is a collection, because + // otherwise it doesn't make a difference + throw new DavException(DavServletResponse.SC_FORBIDDEN, "Unable to perform shallow copy."); + } + try { + context.getSession().getWorkspace().copy(source.getLocator().getRepositoryPath(), destination.getLocator().getRepositoryPath()); + return true; + } catch (PathNotFoundException e) { + // according to rfc 2518: missing parent + throw new DavException(DavServletResponse.SC_CONFLICT, e.getMessage()); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * @see CopyMoveHandler#canMove(CopyMoveContext, org.apache.jackrabbit.webdav.DavResource, org.apache.jackrabbit.webdav.DavResource) + */ + public boolean canMove(CopyMoveContext context, DavResource source, DavResource destination) { + return true; + } + + /** + * @see CopyMoveHandler#move(CopyMoveContext, org.apache.jackrabbit.webdav.DavResource, org.apache.jackrabbit.webdav.DavResource) + */ + public boolean move(CopyMoveContext context, DavResource source, DavResource destination) throws DavException { + try { + context.getWorkspace().move(source.getLocator().getRepositoryPath(), destination.getLocator().getRepositoryPath()); + return true; + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //----------------------------------------------------< DeleteHandler >--- + + /** + * @see DeleteHandler#canDelete(DeleteContext, DavResource) + */ + public boolean canDelete(DeleteContext deleteContext, DavResource member) { + return true; + } + + /** + * @see DeleteHandler#delete(DeleteContext, DavResource) + */ + public boolean delete(DeleteContext deleteContext, DavResource member) throws DavException { + try { + String itemPath = member.getLocator().getRepositoryPath(); + Item item = deleteContext.getSession().getItem(itemPath); + if (item instanceof Node) { + ((Node) item).removeShare(); + } else { + item.remove(); + } + deleteContext.getSession().save(); + log.debug("default handler deleted {}", member.getResourcePath()); + return true; + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //------------------------------------------------------------< private >--- + /** + * Builds a webdav property name from the given jcrName. In case the jcrName + * contains a namespace prefix that would conflict with any of the predefined + * webdav namespaces a new prefix is assigned.
        + * Please note, that the local part of the jcrName is checked for XML + * compatibility by calling {@link ISO9075#encode(String)} + * + * @param jcrName name of the jcr property + * @param session session + * @return a DavPropertyName for the given jcr name. + * @throws RepositoryException if an error during repository access occurs. + */ + private DavPropertyName getDavName(String jcrName, Session session) throws RepositoryException { + // make sure the local name is xml compliant + String localName = ISO9075.encode(Text.getLocalName(jcrName)); + String prefix = Text.getNamespacePrefix(jcrName); + String uri = session.getNamespaceURI(prefix); + Namespace namespace = Namespace.getNamespace(prefix, uri); + DavPropertyName name = DavPropertyName.create(localName, namespace); + return name; + } + + /** + * Build jcr property name from dav property name. If the property name + * defines a namespace uri, that has not been registered yet, an attempt + * is made to register the uri with the prefix defined. + * + * @param propName name of the dav property + * @param session repository session + * @return jcr name + * @throws RepositoryException if an error during repository access occurs. + */ + private String getJcrName(DavPropertyName propName, Session session) throws RepositoryException { + // remove any encoding necessary for xml compliance + String pName = ISO9075.decode(propName.getName()); + Namespace propNamespace = propName.getNamespace(); + if (!Namespace.EMPTY_NAMESPACE.equals(propNamespace)) { + NamespaceHelper helper = new NamespaceHelper(session); + String prefix = helper.registerNamespace( + propNamespace.getPrefix(), propNamespace.getURI()); + pName = prefix + ":" + pName; + } + return pName; + } + + + /** + * @param property dav property + * @param contentNode the content node + * @throws RepositoryException if an error during repository access occurs. + */ + private void setJcrProperty(DavProperty property, Node contentNode) throws RepositoryException { + // Retrieve the property value. Note, that a 'null' value is replaced + // by empty string, since setting a jcr property value to 'null' + // would be equivalent to its removal. + String value = ""; + if (property.getValue() != null) { + value = property.getValue().toString(); + } + + DavPropertyName davName = property.getName(); + if (DavPropertyName.GETCONTENTTYPE.equals(davName)) { + String mimeType = IOUtil.getMimeType(value); + String encoding = IOUtil.getEncoding(value); + contentNode.setProperty(JcrConstants.JCR_MIMETYPE, mimeType); + contentNode.setProperty(JcrConstants.JCR_ENCODING, encoding); + } else { + contentNode.setProperty(getJcrName(davName, contentNode.getSession()), value); + } + } + + /** + * @param propertyName dav property name + * @param contentNode the content node + * @throws RepositoryException if an error during repository access occurs. + */ + private void removeJcrProperty(DavPropertyName propertyName, Node contentNode) + throws RepositoryException { + if (DavPropertyName.GETCONTENTTYPE.equals(propertyName)) { + if (contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { + contentNode.getProperty(JcrConstants.JCR_MIMETYPE).remove(); + } + if (contentNode.hasProperty(JcrConstants.JCR_ENCODING)) { + contentNode.getProperty(JcrConstants.JCR_ENCODING).remove(); + } + } else { + String jcrName = getJcrName(propertyName, contentNode.getSession()); + if (contentNode.hasProperty(jcrName)) { + contentNode.getProperty(jcrName).remove(); + } + // removal of non existing property succeeds + } + } + + private void setLastModified(Node contentNode, long hint) { + try { + Calendar lastMod = Calendar.getInstance(); + if (hint > IOUtil.UNDEFINED_TIME) { + lastMod.setTimeInMillis(hint); + } else { + lastMod.setTime(new Date()); + } + contentNode.setProperty(JcrConstants.JCR_LASTMODIFIED, lastMod); + } catch (RepositoryException e) { + // ignore: property may not be available on the node. + // deliberately not re-throwing as IOException. + } + } + + private static boolean isDefinedByFilteredNodeType(PropertyDefinition def) { + String ntName = def.getDeclaringNodeType().getName(); + return ntName.equals(JcrConstants.NT_BASE) + || ntName.equals(JcrConstants.MIX_REFERENCEABLE) + || ntName.equals(JcrConstants.MIX_VERSIONABLE) + || ntName.equals(JcrConstants.MIX_LOCKABLE); + } + + //-------------------------------------------< setter for configuration >--- + + public void setCollectionNodetype(String collectionNodetype) { + this.collectionNodetype = collectionNodetype; + } + + public void setDefaultNodetype(String defaultNodetype) { + this.defaultNodetype = defaultNodetype; + } + + public void setContentNodetype(String contentNodetype) { + this.contentNodetype = contentNodetype; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultIOListener.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultIOListener.java new file mode 100644 index 00000000000..7e8839104de --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultIOListener.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.server.io; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * DefaultIOListener implements an IOListener that + * writes debug/error output to the {@link Logger logger} specified in the constructor. + */ +public class DefaultIOListener implements IOListener { + + private static Logger log = LoggerFactory.getLogger(DefaultIOListener.class); + + private Logger ioLog; + + /** + * Creates a new DefaultIOListener + */ + public DefaultIOListener(Logger ioLog) { + this.ioLog = (ioLog != null) ? ioLog : log; + } + + /** + * @see IOListener#onBegin(IOHandler, IOContext) + */ + public void onBegin(IOHandler handler, IOContext ioContext) { + ioLog.debug("Starting IOHandler (" + handler.getName() + ")"); + } + + /** + * @see IOListener#onEnd(IOHandler, IOContext, boolean) + */ + public void onEnd(IOHandler handler, IOContext ioContext, boolean success) { + ioLog.debug("Result for IOHandler (" + handler.getName() + "): " + (success ? "OK" : "Failed")); + } + + /** + * @see IOListener#onError(IOHandler, IOContext, Exception) + */ + public void onError(IOHandler ioHandler, IOContext ioContext, Exception e) { + ioLog.debug("Error: " + e.getMessage()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultIOManager.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultIOManager.java new file mode 100644 index 00000000000..4e60663e954 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DefaultIOManager.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.server.io; + +/** + * DefaultIOManager... + */ +public class DefaultIOManager extends IOManagerImpl { + + /** + * Creates a new DefaultIOManager and populates the internal + * list of IOHandlers by the defaults. + * + * @see #init() + */ + public DefaultIOManager() { + init(); + } + + /** + * Add the predefined IOHandlers to this manager. This includes + *

          + *
        • {@link VersionHistoryHandler}
        • + *
        • {@link VersionHandler}
        • + *
        • {@link ZipHandler}
        • + *
        • {@link XmlHandler}
        • + *
        • {@link DirListingExportHandler}
        • + *
        • {@link DefaultHandler}.
        • + *
        + */ + protected void init() { + addIOHandler(new VersionHistoryHandler(this)); + addIOHandler(new VersionHandler(this)); + addIOHandler(new ZipHandler(this)); + addIOHandler(new XmlHandler(this)); + addIOHandler(new DirListingExportHandler(this)); + addIOHandler(new DefaultHandler(this)); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteContext.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteContext.java new file mode 100644 index 00000000000..a468c8071d5 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteContext.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.server.io; + +import javax.jcr.Session; + +/** + * The context associated with a DELETE operation + */ +public interface DeleteContext { + + /** + * @return the jcr session associated with this context. + */ + public Session getSession(); + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteContextImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteContextImpl.java new file mode 100644 index 00000000000..b25d7981cfb --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteContextImpl.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.server.io; + +import javax.jcr.Session; + +/** + * Implements a simple delete context + */ +public class DeleteContextImpl implements DeleteContext { + + private final Session session; + + public DeleteContextImpl(Session session) { + this.session = session; + } + + /** + * @see DeleteContext#getSession() + */ + public Session getSession() { + return this.session; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteHandler.java new file mode 100644 index 00000000000..1dfa76377ba --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteHandler.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.server.io; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; + +/** + * The DeleteHandler is invoked when a webdav DELETE request is received. Implementers of this interface should plugin + * their handling of DELETE request here + */ +public interface DeleteHandler { + + /** + * Executes the delete operation with the given parameters. + * + * @param deleteContext The context of the delete. + * @param resource The resource to be deleted + * @return {@code true} if this instance successfully executed the delete operation with the given parameters; + * {@code false} otherwise. + * @throws DavException If an error occurs. + */ + public boolean delete(DeleteContext deleteContext, DavResource resource) throws DavException; + + + /** + * Validates if this handler is able to execute a delete operation with the given + * parameters. + * + * @param deleteContext The context of the delete + * @param resource The resource to be deleted + * @return {@code true} if this instance can successfully execute the delete operation with the given parameters; + * {@code false} otherwise. + */ + public boolean canDelete(DeleteContext deleteContext, DavResource resource); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteManager.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteManager.java new file mode 100644 index 00000000000..b3418b97f5f --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteManager.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.server.io; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; + +/** + * The DeleteManager handles DELETE operation by delegating it to its handlers. It also provides a way + * to register {@link org.apache.jackrabbit.server.io.DeleteHandler} within it. Implementers of this interface + * must invoke the registered delete handlers appropriately when a DELETE operation is to be performed + */ +public interface DeleteManager { + + /** + * Delegates the delete operation to the fist handler that accepts it. + * + * @param deleteContext The context associated with the DELETE operation + * @param resource The resource to be deleted + * @return {@code true} if this instance successfully executed the delete operation with the given parameters; + * {@code false} otherwise. + * @throws DavException If an error occurs. + */ + public boolean delete(DeleteContext deleteContext, DavResource resource) throws DavException; + + /** + * Registers a delete handler + * + * @param deleteHandler Registers a delete handler with this delete manager + */ + public void addDeleteHandler(DeleteHandler deleteHandler); + + /** + * Returns the registered delete handlers + * + * @return An array of all the registered delete handlers. + */ + public DeleteHandler[] getDeleteHandlers(); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteManagerImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteManagerImpl.java new file mode 100644 index 00000000000..99b9294e7a7 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DeleteManagerImpl.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.server.io; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; + +import java.util.ArrayList; +import java.util.List; + +public class DeleteManagerImpl implements DeleteManager { + + private static DeleteManager DEFAULT_MANAGER; + + private final List deleteHandlers = new ArrayList(); + + /** + * @see DeleteManager#delete(DeleteContext, DavResource) + */ + public boolean delete(DeleteContext deleteContext, DavResource member) throws DavException { + boolean success = false; + DeleteHandler[] deleteHandlers = getDeleteHandlers(); + for (int i = 0; i < deleteHandlers.length && !success; i++) { + DeleteHandler dh = deleteHandlers[i]; + if (dh.canDelete(deleteContext, member)) { + success = dh.delete(deleteContext, member); + } + } + return success; + } + + /** + * @see DeleteManager#addDeleteHandler(DeleteHandler) + */ + public void addDeleteHandler(DeleteHandler deleteHandler) { + if (deleteHandler == null) { + throw new IllegalArgumentException("'null' is not a valid DeleteHandler."); + } + deleteHandlers.add(deleteHandler); + + } + + /** + * @see DeleteManager#getDeleteHandlers() + */ + public DeleteHandler[] getDeleteHandlers() { + return deleteHandlers.toArray(new DeleteHandler[deleteHandlers.size()]); + } + + /** + * Returns this delete manager singleton + */ + public static DeleteManager getDefaultManager() { + if (DEFAULT_MANAGER == null) { + DeleteManager manager = new DeleteManagerImpl(); + manager.addDeleteHandler(new DefaultHandler()); + DEFAULT_MANAGER = manager; + } + return DEFAULT_MANAGER; + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DirListingExportHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DirListingExportHandler.java new file mode 100644 index 00000000000..2ff033bf604 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/DirListingExportHandler.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.server.io; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.Date; +import java.util.Map; + +/** + * DirListingExportHandler represents a simple export for collections: + * a human-readable view listing the members. + *

        + * Note: If {@link #exportContent(ExportContext, boolean)} is called the view list + * child nodes only, without respecting their representation as DavResources. + */ +public class DirListingExportHandler implements IOHandler, PropertyHandler { + + private static Logger log = LoggerFactory.getLogger(DirListingExportHandler.class); + + private IOManager ioManager; + + /** + * Creates a new DirListingExportHandler + */ + public DirListingExportHandler() { + } + + /** + * Creates a new DirListingExportHandler + * + * @param ioManager + */ + public DirListingExportHandler(IOManager ioManager) { + this.ioManager = ioManager; + } + + /** + * Always returns false + * + * @see IOHandler#canImport(ImportContext, boolean) + */ + public boolean canImport(ImportContext context, boolean isFolder) { + return false; + } + + /** + * Always returns false + * + * @see IOHandler#canImport(ImportContext, DavResource) + */ + public boolean canImport(ImportContext context, DavResource resource) { + return false; + } + + /** + * Does nothing and returns false + * + * @see IOHandler#importContent(ImportContext, boolean) + */ + public boolean importContent(ImportContext context, boolean isCollection) throws IOException { + // can only handle export + return false; + } + + /** + * Does nothing and returns false + * + * @see IOHandler#importContent(ImportContext, DavResource) + */ + public boolean importContent(ImportContext context, DavResource resource) throws IOException { + return false; + } + + /** + * @return true if the specified context is still valid and provides a + * export root and if 'isCollection' is true. False otherwise + * @see IOHandler#canExport(ExportContext, boolean) + */ + public boolean canExport(ExportContext context, boolean isCollection) { + if (context == null || context.isCompleted()) { + return false; + } + return isCollection && context.getExportRoot() != null; + } + + /** + * @return true if the specified context is still valid and provides a + * export root and if the specified resource is a collection. False otherwise. + * @see IOHandler#canExport(ExportContext, DavResource) + * @see DavResource#isCollection() + */ + public boolean canExport(ExportContext context, DavResource resource) { + if (resource == null) { + return false; + } + return canExport(context, resource.isCollection()); + } + + /** + * @see IOHandler#exportContent(ExportContext, boolean) + */ + public boolean exportContent(ExportContext context, boolean isCollection) throws IOException { + if (!canExport(context, isCollection)) { + throw new IOException(getName() + ": Cannot export " + context.getExportRoot()); + } + + // properties (content length undefined) + context.setModificationTime(new Date().getTime()); + context.setContentType("text/html", "UTF-8"); + context.setETag(""); + + // data + if (context.hasStream()) { + PrintWriter writer = new PrintWriter(new OutputStreamWriter(context.getOutputStream(), "utf8")); + try { + Item item = context.getExportRoot(); + Repository rep = item.getSession().getRepository(); + String repName = rep.getDescriptor(Repository.REP_NAME_DESC); + String repURL = rep.getDescriptor(Repository.REP_VENDOR_URL_DESC); + String repVersion = rep.getDescriptor(Repository.REP_VERSION_DESC); + writer.print(""); + writer.print(Text.encodeIllegalHTMLCharacters(repName)); + writer.print(" "); + writer.print(Text.encodeIllegalHTMLCharacters(repVersion)); + writer.print(" "); + writer.print(Text.encodeIllegalHTMLCharacters(item.getPath())); + writer.print(""); + writer.print("

        "); + writer.print(Text.encodeIllegalHTMLCharacters(item.getPath())); + writer.print("


        Powered by "); + writer.print(Text.encodeIllegalHTMLCharacters(repName)); + writer.print(" version "); + writer.print(Text.encodeIllegalHTMLCharacters(repVersion)); + writer.print(""); + } catch (RepositoryException e) { + // should not occur + log.debug(e.getMessage()); + } + writer.close(); + } + return true; + } + + /** + * @see IOHandler#exportContent(ExportContext, DavResource) + */ + public boolean exportContent(ExportContext context, DavResource resource) throws IOException { + if (!canExport(context, resource)) { + throw new IOException(getName() + ": Cannot export " + context.getExportRoot()); + } + + // properties (content length undefined) + context.setModificationTime(new Date().getTime()); + context.setContentType("text/html", "UTF-8"); + context.setETag(""); + + // data + if (context.hasStream()) { + PrintWriter writer = new PrintWriter(new OutputStreamWriter(context.getOutputStream(), "utf8")); + try { + Item item = context.getExportRoot(); + Repository rep = item.getSession().getRepository(); + String repName = rep.getDescriptor(Repository.REP_NAME_DESC); + String repURL = rep.getDescriptor(Repository.REP_VENDOR_URL_DESC); + String repVersion = rep.getDescriptor(Repository.REP_VERSION_DESC); + writer.print(""); + writer.print(Text.encodeIllegalHTMLCharacters(repName)); + writer.print(" "); + writer.print(Text.encodeIllegalHTMLCharacters(repVersion)); + writer.print(" "); + writer.print(Text.encodeIllegalHTMLCharacters(resource.getResourcePath())); + writer.print(""); + writer.print("

        "); + writer.print(Text.encodeIllegalHTMLCharacters(resource.getResourcePath())); + writer.print("


        Powered by "); + writer.print(Text.encodeIllegalHTMLCharacters(repName)); + writer.print(" version "); + writer.print(Text.encodeIllegalHTMLCharacters(repVersion)); + writer.print(""); + } catch (RepositoryException e) { + // should not occur + log.debug(e.getMessage()); + } + writer.close(); + } + return true; + } + + /** + * @see IOHandler#getIOManager() + */ + public IOManager getIOManager() { + return ioManager; + } + + /** + * @see IOHandler#setIOManager(IOManager) + */ + public void setIOManager(IOManager ioManager) { + this.ioManager = ioManager; + } + + /** + * @see IOHandler#getName() + */ + public String getName() { + return "DirListing Export"; + } + + //----------------------------------------------------< PropertyHandler >--- + /** + * Always returns false. + * @param context + * @param isCollection + * @return always returns false. + */ + public boolean canExport(PropertyExportContext context, boolean isCollection) { + return false; + } + + /** + * @see PropertyHandler#exportProperties(PropertyExportContext, boolean) + */ + public boolean exportProperties(PropertyExportContext exportContext, boolean isCollection) throws RepositoryException { + // export-content facility only... no responsible for PROPFIND. + throw new RepositoryException(getName() + ": Cannot export properties for context " + exportContext); + } + + public boolean canImport(PropertyImportContext context, boolean isCollection) { + return false; + } + + /** + * @see PropertyHandler#importProperties(PropertyImportContext, boolean) + */ + public Map importProperties(PropertyImportContext importContext, boolean isCollection) throws RepositoryException { + // export facilities only -> throw + throw new RepositoryException(getName() + ": Cannot import properties."); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ExportContext.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ExportContext.java new file mode 100644 index 00000000000..3376051c2e3 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ExportContext.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.server.io; + +import javax.jcr.Item; + +import java.io.OutputStream; + +/** + * ExportContext... + */ +public interface ExportContext extends IOContext { + + /** + * Returns the item to be exported + */ + public Item getExportRoot(); + + /** + * Return the output stream to be used for the export or null + * + * @return output stream or null + */ + public OutputStream getOutputStream(); + + /** + * Set the content type for the resource content + */ + public void setContentType(String mimeType, String encoding); + + /** + * Sets the content language. + */ + public void setContentLanguage(String contentLanguage); + + /** + * Sets the length of the data. + * + * @param contentLength the content length + */ + public void setContentLength(long contentLength); + + /** + * Sets the creation time of the resource. A successful properties export may + * set this member. + * + * @param creationTime the creation time + */ + public void setCreationTime(long creationTime); + + /** + * Sets the modification time of the resource + * + * @param modificationTime the modification time + */ + public void setModificationTime(long modificationTime); + + /** + * Sets the ETag of the resource. A successful export command + * may set this member. + * + * @param etag the ETag + */ + public void setETag(String etag); + + /** + * Sets an arbitrary property to this export context. + */ + public void setProperty(Object propertyName, Object propertyValue); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ExportContextImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ExportContextImpl.java new file mode 100644 index 00000000000..12e61493e43 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ExportContextImpl.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.server.io; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * ExportContextImpl implements an ExportContext that + * wraps around the specified OutputContext as it was passed to + * {@link DavResource#spool(OutputContext)}. If a stream is provided a temporary + * file is created, which is deleted as soon as {@link #informCompleted(boolean)} + * is called on this context. Note however, that the properties and the stream + * are written to the OutputContext but upon successful completion. + * + * @see #informCompleted(boolean) + */ +public class ExportContextImpl extends AbstractExportContext { + + private static Logger log = LoggerFactory.getLogger(ExportContextImpl.class); + + private final Map properties = new HashMap(); + private final OutputContext outputCtx; + + private File outFile; + private OutputStream outStream; + + public ExportContextImpl(Item exportRoot, OutputContext outputCtx) + throws IOException { + super(exportRoot, outputCtx != null && outputCtx.hasStream(), null); + this.outputCtx = outputCtx; + if (hasStream()) { + // we need a tmp file, since the export could fail + outFile = File.createTempFile("__exportcontext", "tmp"); + } + } + + /** + * Returns a new OutputStream to the temporary file or + * null if this context provides no stream. + * + * @see ExportContext#getOutputStream() + * @see #informCompleted(boolean) + */ + public OutputStream getOutputStream() { + checkCompleted(); + if (hasStream()) { + try { + // clean up the stream retrieved by the preceding handler, that + // did not behave properly and failed to export although initially + // willing to handle the export. + if (outStream != null) { + outStream.close(); + } + outStream = new FileOutputStream(outFile); + return outStream; + } catch (IOException e) { + // unexpected error... ignore and return null + } + } + return null; + } + + /** + * @see ExportContext#setContentLanguage(String) + */ + public void setContentLanguage(String contentLanguage) { + properties.put(DavConstants.HEADER_CONTENT_LANGUAGE, contentLanguage); + } + + /** + * @see ExportContext#setContentLength(long) + */ + public void setContentLength(long contentLength) { + properties.put(DavConstants.HEADER_CONTENT_LENGTH, contentLength + ""); + } + + /** + * @see ExportContext#setContentType(String,String) + */ + public void setContentType(String mimeType, String encoding) { + properties.put(DavConstants.HEADER_CONTENT_TYPE, IOUtil.buildContentType(mimeType, encoding)); + } + + /** + * Does nothing since the wrapped output context does not understand + * creation time + * + * @see ExportContext#setCreationTime(long) + */ + public void setCreationTime(long creationTime) { + // ignore since output-ctx does not understand creation time + } + + /** + * @see ExportContext#setModificationTime(long) + */ + public void setModificationTime(long modificationTime) { + if (modificationTime <= IOUtil.UNDEFINED_TIME) { + modificationTime = new Date().getTime(); + } + String lastMod = IOUtil.getLastModified(modificationTime); + properties.put(DavConstants.HEADER_LAST_MODIFIED, lastMod); + } + + /** + * @see ExportContext#setETag(String) + */ + public void setETag(String etag) { + properties.put(DavConstants.HEADER_ETAG, etag); + } + + /** + * @see ExportContext#setProperty(Object, Object) + */ + public void setProperty(Object propertyName, Object propertyValue) { + if (propertyName != null && propertyValue != null) { + properties.put(propertyName.toString(), propertyValue.toString()); + } + } + + /** + * If success is true, the properties set before an the output stream are + * written to the wrapped OutputContext. + * + * @see ExportContext#informCompleted(boolean) + */ + @Override + public void informCompleted(boolean success) { + checkCompleted(); + completed = true; + // make sure the outputStream gets closed (and don't assume the handlers + // took care of this. + if (outStream != null) { + try { + outStream.close(); + } catch (IOException e) { + // ignore + } + } + if (success) { + // write properties and data to the output-context + if (outputCtx != null) { + boolean hasContentLength = false; + for (String name : properties.keySet()) { + String value = properties.get(name); + if (name != null && value != null) { + outputCtx.setProperty(name, value); + // check for content-length + hasContentLength = DavConstants.HEADER_CONTENT_LENGTH.equals(name); + } + } + + if (outputCtx.hasStream() && outFile != null) { + OutputStream out = outputCtx.getOutputStream(); + try { + // make sure the content-length is set + if (!hasContentLength) { + outputCtx.setContentLength(outFile.length()); + } + FileInputStream in = new FileInputStream(outFile); + IOUtil.spool(in, out); + } catch (IOException e) { + log.error(e.toString()); + } + } + } + } + if (outFile != null) { + outFile.delete(); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOContext.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOContext.java new file mode 100644 index 00000000000..9fbdce4ee34 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOContext.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.server.io; + +/** + * IOContext defines the common methods for {@link ImportContext} + * and {@link ExportContext} + */ +public interface IOContext { + + /** + * Returns the IOListener. + */ + public IOListener getIOListener(); + + /** + * Return true if the given export context can provide an output stream + */ + public boolean hasStream(); + + /** + * Informs this context that it will not be used for further exports any + * more. A boolean flag indicates about the success of the export. + */ + public void informCompleted(boolean success); + + /** + * Returns true if this context already has been completed. + * + * @return true if this context already has been completed. + */ + public boolean isCompleted(); +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOHandler.java new file mode 100644 index 00000000000..b7bbc7338d6 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOHandler.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.server.io; + +import org.apache.jackrabbit.webdav.DavResource; + +import java.io.IOException; + +/** + * IOHandler interface defines methods for importing and + * exporting resource content as well as some fundamental resource properties + * which use to be set/retrieved together with content import and export (e.g. + * content length, modification date etc.). + */ +public interface IOHandler { + + /** + * Returns the IOManager that called this handler or null. + */ + public IOManager getIOManager(); + + /** + * Sets the IOManager that called this handler. + */ + public void setIOManager(IOManager ioManager); + + /** + * Returns a human readable name for this IOHandler. + */ + public String getName(); + + /** + * Returns true, if this handler can run a successful import based on the + * specified context. + */ + public boolean canImport(ImportContext context, boolean isCollection); + + /** + * Returns true, if this handler can run a successful import based on + * the specified context and resource. A simple implementation may choose + * to return the same as {@link IOHandler#canImport(ImportContext, boolean)} + * where the isCollection flag is determined by + * {@link DavResource#isCollection()}. + */ + public boolean canImport(ImportContext context, DavResource resource); + + /** + * Runs the import for the given context and indicates by a boolean return + * value, if the import could be completed successfully. If the specified + * ImportContext does not provide a {@link ImportContext#hasStream() stream} + * the implementation is free, to only import properties of to refuse the + * import. + *

        + * Please note, that it is the responsibility of the specified + * ImportContext to assert, that its stream is not consumed + * multiple times when being passed to a chain of IOHandlers. + * + * @param context + * @param isCollection + * @return true if the import was successful. + * @throws IOException if an unexpected error occurs or if this method has + * been called although {@link IOHandler#canImport(ImportContext, boolean)} + * returns false. + */ + public boolean importContent(ImportContext context, boolean isCollection) throws IOException; + + /** + * Runs the import for the given context and resource. It indicates by a boolean return + * value, if the import could be completed successfully. If the specified + * ImportContext does not provide a {@link ImportContext#hasStream() stream} + * the implementation is free, to only import properties of to refuse the + * import. A simple implementation may return the same as + * {@link IOHandler#importContent(ImportContext, boolean)} where the + * isCollection flag is determined by {@link DavResource#isCollection()} + *

        + * Please note, that it is the responsibility of the specified + * ImportContext to assert, that its stream is not consumed + * multiple times when being passed to a chain of IOHandlers. + * + * @param context + * @param resource + * @return + * @throws IOException if an unexpected error occurs or if this method has + * been called although {@link IOHandler#canImport(ImportContext, DavResource)} + * returns false. + * @see IOHandler#importContent(ImportContext, boolean) + */ + public boolean importContent(ImportContext context, DavResource resource) throws IOException; + + /** + * Returns true, if this handler can run a successful export based on the + * specified context. + */ + public boolean canExport(ExportContext context, boolean isCollection); + + /** + * Returns true, if this handler can run a successful export based on + * the specified context and resource. A simple implementation may choose + * to return the same as {@link IOHandler#canExport(ExportContext, boolean)} + * where the isCollection flag is determined by + * {@link DavResource#isCollection()}. + */ + public boolean canExport(ExportContext context, DavResource resource); + + /** + * Runs the export for the given context. It indicates by a boolean return + * value, if the export could be completed successfully. If the specified + * ExportContext does not provide a {@link ExportContext#hasStream() stream} + * the implementation should set the properties only and ignore the content to + * be exported. A simple implementation may return the same as + * {@link IOHandler#exportContent(ExportContext, boolean)} where the + * isCollection flag is determined by {@link DavResource#isCollection()} + *

        + * Please note, that it is the responsibility of the specified + * ExportContext to assert, that its stream is not written + * multiple times when being passed to a chain of IOHandlers. + * + * @param context + * @param isCollection + * @return + * @throws IOException if an unexpected error occurs or if this method has + * been called although {@link IOHandler#canExport(ExportContext, boolean)} + * returns false. + */ + public boolean exportContent(ExportContext context, boolean isCollection) throws IOException; + + /** + * Runs the export for the given context and resource. It indicates by a boolean return + * value, if the export could be completed successfully. If the specified + * ExportContext does not provide a {@link ExportContext#hasStream() stream} + * the implementation should set the properties only and ignore the content to + * be exported. A simple implementation may return the same as + * {@link IOHandler#exportContent(ExportContext, boolean)} where the + * isCollection flag is determined by {@link DavResource#isCollection()} + *

        + * Please note, that it is the responsibility of the specified + * ExportContext to assert, that its stream is not written + * multiple times when being passed to a chain of IOHandlers. + * + * @param context + * @param resource + * @return + * @throws IOException if an unexpected error occurs or if this method has + * been called although {@link IOHandler#canExport(ExportContext, DavResource)} + * returns false. + */ + public boolean exportContent(ExportContext context, DavResource resource) throws IOException; +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOListener.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOListener.java new file mode 100644 index 00000000000..4993b1231e2 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOListener.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.server.io; + +/** + * IOListener defines an import/export listener. + */ +public interface IOListener { + + /** + * The import/export context has been passed to the given IOHandler. + */ + public void onBegin(IOHandler handler, IOContext context); + + /** + * The specified IOHandler finished. A boolean flag indicates + * whether the handler was able to run the import/export. + */ + public void onEnd(IOHandler handler, IOContext context, boolean success); + + /** + * An exception occurred during import/export within the specified + * IOHandler. + */ + public void onError(IOHandler ioHandler, IOContext context, Exception e); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOManager.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOManager.java new file mode 100644 index 00000000000..bf6b5350716 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOManager.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.server.io; + +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.tika.detect.Detector; + +import java.io.IOException; + +/** + * IOManager interface provides the means to define a list of + * IOHandlers that should be asked to perform an import or export. + */ +public interface IOManager { + + /** + * Adds the specified handler to the list of handlers. + * + * @param ioHandler to be added + */ + public void addIOHandler(IOHandler ioHandler); + + /** + * Returns all handlers that have been added to this manager. + * + * @return Array of all handlers + */ + public IOHandler[] getIOHandlers(); + + /** + * Return the configured type detector. + * + * @return content type detector + */ + Detector getDetector(); + + /** + * Sets the configured type detector. + * + * @param detector content type detector. + */ + void setDetector(Detector detector); + + /** + * Passes the specified context and boolean value to the IOHandlers present + * on this manager. + * As soon as the first handler indicates success the import should be + * considered completed. If none of the handlers can deal with the given + * information this method must return false. + * + * @param context + * @param isCollection + * @return true if any of the handlers import the given context. + * False otherwise. + * @throws IOException + * @see IOHandler#importContent(ImportContext, boolean) + */ + public boolean importContent(ImportContext context, boolean isCollection) throws IOException; + + /** + * Passes the specified information to the IOHandlers present on this manager. + * As soon as the first handler indicates success the import should be + * considered completed. If none of the handlers can deal with the given + * information this method must return false. + * + * @param context + * @param resource + * @return true if any of the handlers import the information present on the + * specified context. + * @throws IOException + * @see IOHandler#importContent(ImportContext, DavResource) + */ + public boolean importContent(ImportContext context, DavResource resource) throws IOException; + + /** + * Passes the specified information to the IOHandlers present on this manager. + * As soon as the first handler indicates success the export should be + * considered completed. If none of the handlers can deal with the given + * information this method must return false. + * + * @param context + * @param isCollection + * @return true if any of the handlers could run the export successfully, + * false otherwise. + * @throws IOException + * @see IOHandler#exportContent(ExportContext, boolean) + */ + public boolean exportContent(ExportContext context, boolean isCollection) throws IOException; + + /** + * Passes the specified information to the IOHandlers present on this manager. + * As soon as the first handler indicates success the export should be + * considered completed. If none of the handlers can deal with the given + * information this method must return false. + * + * @param context + * @param resource + * @return true if any of the handlers could run the export successfully, + * false otherwise. + * @throws IOException + * @see IOHandler#exportContent(ExportContext, DavResource) + */ + public boolean exportContent(ExportContext context, DavResource resource) throws IOException; +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOManagerImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOManagerImpl.java new file mode 100644 index 00000000000..3392f38483e --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOManagerImpl.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.io; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.tika.detect.Detector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * IOManagerImpl represents the most simple IOManager + * implementation that provides a default constructor and does define any + * IOHandlers. + */ +public class IOManagerImpl implements IOManager { + + private static Logger log = LoggerFactory.getLogger(IOManagerImpl.class); + + /** + * Content type detector. + */ + private Detector detector; + + private final List ioHandlers = new ArrayList(); + + /** + * Create a new IOManager. + * Note, that this manager does not define any IOHandlers by + * default. Use {@link #addIOHandler(IOHandler)} in order to populate the + * internal list of handlers that are called for importContent and + * exportContent. + */ + public IOManagerImpl() { + } + + /** + * @see IOManager#addIOHandler(IOHandler) + */ + public void addIOHandler(IOHandler ioHandler) { + if (ioHandler == null) { + throw new IllegalArgumentException("'null' is not a valid IOHandler."); + } + ioHandler.setIOManager(this); + ioHandlers.add(ioHandler); + } + + /** + * @see IOManager#getIOHandlers() + */ + public IOHandler[] getIOHandlers() { + return ioHandlers.toArray(new IOHandler[ioHandlers.size()]); + } + + /** + * Return the configured type detector. + * + * @return content type detector + */ + public Detector getDetector() { + return detector; + } + + /** + * Sets the configured type detector. + * + * @param detector content type detector + */ + public void setDetector(Detector detector) { + this.detector = detector; + } + + /** + * @see IOManager#importContent(ImportContext, boolean) + */ + public boolean importContent(ImportContext context, boolean isCollection) throws IOException { + boolean success = false; + if (context != null) { + IOListener ioListener = context.getIOListener(); + if (ioListener == null) { + ioListener = new DefaultIOListener(log); + } + IOHandler[] ioHandlers = getIOHandlers(); + for (int i = 0; i < ioHandlers.length && !success; i++) { + IOHandler ioh = ioHandlers[i]; + if (ioh.canImport(context, isCollection)) { + ioListener.onBegin(ioh, context); + success = ioh.importContent(context, isCollection); + ioListener.onEnd(ioh, context, success); + } + } + context.informCompleted(success); + } + return success; + } + + /** + * @see IOManager#importContent(ImportContext, DavResource) + */ + public boolean importContent(ImportContext context, DavResource resource) throws IOException { + boolean success = false; + if (context != null && resource != null) { + IOListener ioListener = context.getIOListener(); + if (ioListener == null) { + ioListener = new DefaultIOListener(log); + } + IOHandler[] ioHandlers = getIOHandlers(); + for (int i = 0; i < ioHandlers.length && !success; i++) { + IOHandler ioh = ioHandlers[i]; + if (ioh.canImport(context, resource)) { + ioListener.onBegin(ioh, context); + success = ioh.importContent(context, resource); + ioListener.onEnd(ioh, context, success); + } + } + context.informCompleted(success); + } + return success; + } + + /** + * @see IOManager#exportContent(ExportContext, boolean) + */ + public boolean exportContent(ExportContext context, boolean isCollection) throws IOException { + boolean success = false; + if (context != null) { + IOListener ioListener = context.getIOListener(); + if (ioListener == null) { + ioListener = new DefaultIOListener(log); + } + IOHandler[] ioHandlers = getIOHandlers(); + for (int i = 0; i < ioHandlers.length && !success; i++) { + IOHandler ioh = ioHandlers[i]; + if (ioh.canExport(context, isCollection)) { + ioListener.onBegin(ioh, context); + success = ioh.exportContent(context, isCollection); + ioListener.onEnd(ioh, context, success); + } + } + context.informCompleted(success); + } + return success; + } + + /** + * @see IOManager#exportContent(ExportContext, DavResource) + */ + public boolean exportContent(ExportContext context, DavResource resource) throws IOException { + boolean success = false; + if (context != null && resource != null) { + IOListener ioListener = context.getIOListener(); + if (ioListener == null) { + ioListener = new DefaultIOListener(log); + } + IOHandler[] ioHandlers = getIOHandlers(); + for (int i = 0; i < ioHandlers.length && !success; i++) { + IOHandler ioh = ioHandlers[i]; + if (ioh.canExport(context, resource)) { + ioListener.onBegin(ioh, context); + success = ioh.exportContent(context, resource); + ioListener.onEnd(ioh, context, success); + } + } + context.informCompleted(success); + } + return success; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOUtil.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOUtil.java new file mode 100644 index 00000000000..c6299aca18e --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/IOUtil.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.server.io; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.util.HttpDateFormat; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +/** + * IOUtil provides utility methods used for import and export + * operations. + */ +public final class IOUtil { + + /** + * Avoid instantiation + */ + private IOUtil() {} + + /** + * Constant for undefined modification/creation time + */ + public static final long UNDEFINED_TIME = DavConstants.UNDEFINED_TIME; + + /** + * Constant for undefined content length + */ + public static final long UNDEFINED_LENGTH = -1; + + /** + * Return the last modification time as formatted string. + * + * @return last modification time as string. + * @see org.apache.jackrabbit.webdav.util.HttpDateFormat#modificationDateFormat() + */ + public static String getLastModified(long modificationTime) { + if (modificationTime <= IOUtil.UNDEFINED_TIME) { + modificationTime = new Date().getTime(); + } + return HttpDateFormat.modificationDateFormat().format(new Date(modificationTime)); + } + + /** + * Return the creation time as formatted string. + * + * @return creation time as string. + * @see org.apache.jackrabbit.webdav.util.HttpDateFormat#creationDateFormat() + */ + public static String getCreated(long createdTime) { + if (createdTime <= IOUtil.UNDEFINED_TIME) { + createdTime = 0; + } + return HttpDateFormat.creationDateFormat().format(new Date(createdTime)); + } + + /** + */ + public static void spool(InputStream in, OutputStream out) throws IOException { + try { + byte[] buffer = new byte[8192]; + int read; + while ((read = in.read(buffer)) >= 0) { + out.write(buffer, 0, read); + } + } finally { + in.close(); + } + } + + /** + * Build a valid content type string from the given mimeType and encoding: + *

        +     * <mimeType>; charset="<encoding>"
        +     * 
        + * If the specified mimeType is null, null is returned. + * + * @param mimeType + * @param encoding + * @return contentType or null if the specified mimeType is + * null + */ + public static String buildContentType(String mimeType, String encoding) { + String contentType = mimeType; + if (contentType != null && encoding != null) { + contentType += "; charset=" + encoding; + } + return contentType; + } + + /** + * Retrieve the mimeType from the specified contentType. + * + * @param contentType + * @return mimeType or null + */ + public static String getMimeType(String contentType) { + String mimeType = contentType; + if (mimeType == null) { + // property will be removed. + // Note however, that jcr:mimetype is a mandatory property with the + // built-in nt:file nodetype. + return mimeType; + } + // strip any parameters + int semi = mimeType.indexOf(';'); + return (semi > 0) ? mimeType.substring(0, semi) : mimeType; + } + + /** + * Retrieve the encoding from the specified contentType. + * + * @param contentType + * @return encoding or null if the specified contentType is + * null or does not define a charset. + */ + public static String getEncoding(String contentType) { + // find the charset parameter + int equal; + if (contentType == null || (equal = contentType.indexOf("charset=")) == -1) { + // jcr:encoding property will be removed + return null; + } + String encoding = contentType.substring(equal + 8); + // get rid of any other parameters that might be specified after the charset + int semi = encoding.indexOf(';'); + if (semi != -1) { + encoding = encoding.substring(0, semi); + } + return encoding; + } + + /** + * Builds a new temp. file from the given input stream. + *

        + * It is left to the user to remove the file as soon as it is not used + * any more. + * + * @param inputStream the input stream + * @return temp. file or null if the specified input is + * null. + */ + public static File getTempFile(InputStream inputStream) throws IOException { + if (inputStream == null) { + return null; + } + // we need a tmp file, since the import could fail + File tmpFile = File.createTempFile("__importcontext", ".tmp"); + FileOutputStream out = new FileOutputStream(tmpFile); + byte[] buffer = new byte[8192]; + int read; + while ((read=inputStream.read(buffer))>0) { + out.write(buffer, 0, read); + } + out.close(); + inputStream.close(); + return tmpFile; + } + + /** + * Recursively creates nodes below the specified root node. + * + * @param root + * @param relPath + * @return the node corresponding to the last segment of the specified + * relative path. + * @throws RepositoryException + */ + public static Node mkDirs(Node root, String relPath, String dirNodeType) throws RepositoryException { + for (String seg : Text.explode(relPath, '/')) { + if (!root.hasNode(seg)) { + root.addNode(seg, dirNodeType); + } + root = root.getNode(seg); + } + return root; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ImportContext.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ImportContext.java new file mode 100644 index 00000000000..99d3b270563 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ImportContext.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.server.io; + +import javax.jcr.Item; +import java.io.InputStream; + +/** + * ImportContext... + */ +public interface ImportContext extends IOContext { + + /** + * Returns the import root of the resource to import, i.e. the parent node + * of the new content to be created. + * + * @return the import root of the resource to import. + */ + public Item getImportRoot(); + + /** + * Returns the system id of the resource to be imported. This id depends on + * the system the resource is coming from. it can be a filename, a + * display name of a webdav resource, an URI, etc. + * + * @return the system id of the resource to import + */ + public String getSystemId(); + + /** + * Returns the input stream of the data to import or null if + * there are none. + * + * @return the input stream. + * @see #hasStream() + */ + public InputStream getInputStream(); + + /** + * Returns the modification time of the resource or the current time if + * the modification time has not been set. + * + * @return the modification time. + */ + public long getModificationTime(); + + /** + * Returns the content language or null + * + * @return contentLanguage + */ + public String getContentLanguage(); + + /** + * Returns the length of the data or {@link IOUtil#UNDEFINED_LENGTH -1} if + * the content length could not be determined. + * + * @return the content length + */ + public long getContentLength(); + + /** + * Returns the main media type. It should be retrieved from a content type + * (as present in a http request) or from the systemId. If either value + * is undefined null should be returned. + * + * @return the mimetype of the resource to be imported + */ + public String getMimeType(); + + /** + * Returns the encoding extracted from a content type as present in a + * request header or null + * + * @return the encoding to be used for importing + */ + public String getEncoding(); + + /** + */ + public Object getProperty(Object propertyName); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ImportContextImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ImportContextImpl.java new file mode 100644 index 00000000000..cabfffd4104 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ImportContextImpl.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.server.io; + +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.tika.detect.Detector; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MediaType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; + +/** + * ImportContextImpl... + */ +public class ImportContextImpl implements ImportContext { + + private static Logger log = LoggerFactory.getLogger(ImportContextImpl.class); + + private final IOListener ioListener; + private final Item importRoot; + private final String systemId; + private final File inputFile; + + private InputContext inputCtx; + private boolean completed; + + private final MediaType type; + + /** + * Creates a new item import context. The specified InputStream is written + * to a temporary file in order to avoid problems with multiple IOHandlers + * that try to run the import but fail. The temporary file is deleted as soon + * as this context is informed that the import has been completed and it + * will not be used any more. + * + * @param importRoot + * @param systemId + * @param inputCtx input context, or null + * @param stream document input stream, or null + * @param ioListener + * @param detector content type detector + * @throws IOException + * @see ImportContext#informCompleted(boolean) + */ + public ImportContextImpl( + Item importRoot, String systemId, InputContext inputCtx, + InputStream stream, IOListener ioListener, Detector detector) + throws IOException { + this.importRoot = importRoot; + this.systemId = systemId; + this.inputCtx = inputCtx; + this.ioListener = (ioListener != null) ? ioListener : new DefaultIOListener(log); + + Metadata metadata = new Metadata(); + if (inputCtx != null && inputCtx.getContentType() != null) { + metadata.set(Metadata.CONTENT_TYPE, inputCtx.getContentType()); + } + if (systemId != null) { + metadata.set(Metadata.RESOURCE_NAME_KEY, systemId); + } + if (stream != null && !stream.markSupported()) { + stream = new BufferedInputStream(stream); + } + this.type = detector.detect(stream, metadata); + this.inputFile = IOUtil.getTempFile(stream); + } + + /** + * @see ImportContext#getIOListener() + */ + public IOListener getIOListener() { + return ioListener; + } + + /** + * @see ImportContext#getImportRoot() + */ + public Item getImportRoot() { + return importRoot; + } + + /** + * @see ImportContext#hasStream() + */ + public boolean hasStream() { + return inputFile != null; + } + + /** + * Returns a new InputStream to the temporary file created + * during instantiation or null, if this context does not + * provide a stream. + * + * @see ImportContext#getInputStream() + * @see #hasStream() + */ + public InputStream getInputStream() { + checkCompleted(); + InputStream in = null; + if (inputFile != null) { + try { + in = new FileInputStream(inputFile); + } catch (IOException e) { + // unexpected error... ignore and return null + } + } + return in; + } + + /** + * @see ImportContext#getSystemId() + */ + public String getSystemId() { + return systemId; + } + + /** + * @see ImportContext#getModificationTime() + */ + public long getModificationTime() { + return (inputCtx != null) ? inputCtx.getModificationTime() : new Date().getTime(); + } + + /** + * @see ImportContext#getContentLanguage() + */ + public String getContentLanguage() { + return (inputCtx != null) ? inputCtx.getContentLanguage() : null; + } + + /** + * @see ImportContext#getContentLength() + */ + public long getContentLength() { + long length = IOUtil.UNDEFINED_LENGTH; + if (inputCtx != null) { + length = inputCtx.getContentLength(); + } + if (length < 0 && inputFile != null) { + length = inputFile.length(); + } + if (length < 0) { + log.debug("Unable to determine content length -> default value = " + IOUtil.UNDEFINED_LENGTH); + } + return length; + } + + /** + * @see ImportContext#getMimeType() + */ + public String getMimeType() { + return IOUtil.getMimeType(type.toString()); + } + + /** + * @see ImportContext#getEncoding() + */ + public String getEncoding() { + return IOUtil.getEncoding(type.toString()); + } + + /** + * @see ImportContext#getProperty(Object) + */ + public Object getProperty(Object propertyName) { + return (inputCtx != null) ? inputCtx.getProperty(propertyName.toString()) : null; + } + + /** + * @see ImportContext#informCompleted(boolean) + */ + public void informCompleted(boolean success) { + checkCompleted(); + completed = true; + if (inputFile != null) { + inputFile.delete(); + } + } + + /** + * @see ImportContext#isCompleted() + */ + public boolean isCompleted() { + return completed; + } + + /** + * @throws IllegalStateException if the context is already completed. + * @see #isCompleted() + * @see #informCompleted(boolean) + */ + private void checkCompleted() { + if (completed) { + throw new IllegalStateException("ImportContext has already been consumed."); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyExportContext.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyExportContext.java new file mode 100644 index 00000000000..edad5ca1848 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyExportContext.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.server.io; + +/** + * PropertyExportContext represents a marker interface to distinguish + * the ExportContext (which is mainly used to export data and some fundamental + * properties) from a context that is used to export properties only. + */ +public interface PropertyExportContext extends ExportContext { + +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyHandler.java new file mode 100644 index 00000000000..68dfba26e2d --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyHandler.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.io; + +import org.apache.jackrabbit.webdav.property.PropEntry; + +import javax.jcr.RepositoryException; +import java.util.Map; + +/** + * PropertyHandler interface defines methods for importing and + * exporting resource properties. + */ +public interface PropertyHandler { + + /** + * Returns true, if this handler can run a successful export based on the + * specified context. + * + * @param context + * @param isCollection + * @return true if this PropertyHandler is export properties + * given the specified parameters. + */ + public boolean canExport(PropertyExportContext context, boolean isCollection); + + /** + * Exports properties to the given context. Note that the export must + * be consistent with properties that might be exposed by content export + * such as defined by {@link IOHandler#exportContent(ExportContext, boolean)}. + * + * @param exportContext + * @param isCollection + * @return true if the export succeeded. + * @throws RepositoryException If an attempt is made to export properties + * even if {@link PropertyHandler#canExport(PropertyExportContext, boolean)} + * returns false or if some other unrecoverable error occurs. + */ + public boolean exportProperties(PropertyExportContext exportContext, boolean isCollection) throws RepositoryException; + + /** + * Returns true, if this handler can run a property import based on the + * specified context. + * + * @param context + * @param isCollection + * @return true if this PropertyHandler can import properties + * given the specified parameters. + */ + public boolean canImport(PropertyImportContext context, boolean isCollection); + + /** + * Imports, modifies or removes properties according the the + * {@link PropertyImportContext#getChangeList() change list} available from + * the import context. Note, that according to JSR 170 setting a property + * value to null is equivalent to its removal. + *

        + * The return value of this method must be used to provided detailed + * information about any kind of failures. + * + * @param importContext + * @param isCollection + * @return Map listing those properties that failed to be updated. An empty + * map indicates a successful import for all properties listed in the context. + * @throws RepositoryException If + * {@link PropertyHandler#canImport(PropertyImportContext, boolean)} + * returns false for the given parameters or if some other unrecoverable + * error occurred. Note, that normal failure of a property update must be + * reported with the return value and should not result in an exception. + */ + public Map importProperties(PropertyImportContext importContext, boolean isCollection) throws RepositoryException; + +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyImportContext.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyImportContext.java new file mode 100644 index 00000000000..a03e761cd86 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyImportContext.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.server.io; + +import org.apache.jackrabbit.webdav.property.PropEntry; + +import javax.jcr.Item; +import java.util.List; + +/** + * PropertyImportContext... + */ +public interface PropertyImportContext extends IOContext { + + /** + * Returns the import root for the properties to be altered. Note, that + * a particular implementation may still apply the modifications to + * child items at any depth as long as property import is consistent with + * the corresponding export. + * + * @return the import root of the resource to import. + */ + public Item getImportRoot(); + + /** + * Returns a list of properties to be modified by a call to + * {@link PropertyHandler#importProperties(PropertyImportContext, boolean)}. + * + * @return list of properties to be modified + */ + public List getChangeList(); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyManager.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyManager.java new file mode 100644 index 00000000000..811353c92a6 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyManager.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.server.io; + +import org.apache.jackrabbit.webdav.property.PropEntry; + +import javax.jcr.RepositoryException; +import java.util.Map; + +/** + * PropertyManager... + */ +public interface PropertyManager { + + /** + */ + public boolean exportProperties(PropertyExportContext exportContext, boolean isCollection) throws RepositoryException; + + /** + */ + public Map alterProperties(PropertyImportContext importContext, boolean isCollection) throws RepositoryException; + + /** + */ + public void addPropertyHandler(PropertyHandler propertyHandler); + + /** + */ + public PropertyHandler[] getPropertyHandlers(); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyManagerImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyManagerImpl.java new file mode 100644 index 00000000000..c51e762f53d --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/PropertyManagerImpl.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.server.io; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.webdav.property.PropEntry; + +/** + * PropertyManagerImpl... + */ +public class PropertyManagerImpl implements PropertyManager { + + private static PropertyManager DEFAULT_MANAGER; + + private final List propertyHandlers = new ArrayList(); + + /** + * Create a new PropertyManagerImpl. + * Note, that this manager does not define any PropertyHandlers by + * default. Use {@link #addPropertyHandler(PropertyHandler)} in order to populate the + * internal list of handlers that are called for importProperties and + * exportProperties respectively. See {@link #getDefaultManager()} + * for an instance of this class populated with default handlers. + */ + public PropertyManagerImpl() { + } + + /** + * @see PropertyManager#exportProperties(PropertyExportContext, boolean) + */ + public boolean exportProperties(PropertyExportContext context, boolean isCollection) throws RepositoryException { + boolean success = false; + PropertyHandler[] propertyHandlers = getPropertyHandlers(); + for (int i = 0; i < propertyHandlers.length && !success; i++) { + PropertyHandler ph = propertyHandlers[i]; + if (ph.canExport(context, isCollection)) { + success = ph.exportProperties(context, isCollection); + } + } + context.informCompleted(success); + return success; + } + + /** + * @see PropertyManager#alterProperties(PropertyImportContext, boolean) + */ + public Map alterProperties(PropertyImportContext context, boolean isCollection) throws RepositoryException { + Map failures = null; + for (PropertyHandler ph : getPropertyHandlers()) { + if (ph.canImport(context, isCollection)) { + failures = ph.importProperties(context, isCollection); + break; + } + } + if (failures == null) { + throw new RepositoryException("Unable to alter properties: No matching handler found."); + } + context.informCompleted(failures.isEmpty()); + return failures; + } + + /** + * @see PropertyManager#addPropertyHandler(PropertyHandler) + */ + public void addPropertyHandler(PropertyHandler propertyHandler) { + if (propertyHandler == null) { + throw new IllegalArgumentException("'null' is not a valid IOHandler."); + } + propertyHandlers.add(propertyHandler); + } + + /** + * @see PropertyManager#getPropertyHandlers() + */ + public PropertyHandler[] getPropertyHandlers() { + return propertyHandlers.toArray(new PropertyHandler[propertyHandlers.size()]); + } + + /** + * @return an instance of PropertyManager populated with default handlers. + */ + public static PropertyManager getDefaultManager() { + if (DEFAULT_MANAGER == null) { + PropertyManager manager = new PropertyManagerImpl(); + manager.addPropertyHandler(new ZipHandler()); + manager.addPropertyHandler(new XmlHandler()); + manager.addPropertyHandler(new DefaultHandler()); + DEFAULT_MANAGER = manager; + } + return DEFAULT_MANAGER; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/VersionHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/VersionHandler.java new file mode 100644 index 00000000000..9074610f6e4 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/VersionHandler.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.io; + +import java.io.IOException; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.property.PropEntry; + +public class VersionHandler extends DefaultHandler implements IOHandler{ + + public VersionHandler() { + } + + public VersionHandler(IOManager ioManager) { + super(ioManager); + } + + //----------------------------------------------------------< IOHandler >--- + @Override + public boolean canImport(ImportContext context, boolean isCollection) { + // version node is read only. + return false; + } + + @Override + public boolean canImport(ImportContext context, DavResource resource) { + // version node is read only. + return false; + } + + @Override + public boolean importContent(ImportContext context, boolean isCollection) throws IOException { + // version node is read only. + return false; + } + + @Override + public boolean importContent(ImportContext context, DavResource resource) throws IOException { + // version node is read only. + return false; + } + + /** + * @param context + * @param isCollection + * @return true if the export root is a Version node. False otherwise. + */ + @Override + public boolean canExport(ExportContext context, boolean isCollection) { + if (context == null) { + return false; + } + return context.getExportRoot() instanceof Version; + } + + /** + * @return true if the export root is a Version node. False otherwise. + * @see IOHandler#canExport(ExportContext, DavResource) + */ + @Override + public boolean canExport(ExportContext context, DavResource resource) { + if (context == null) { + return false; + } + return context.getExportRoot() instanceof Version; + } + + //----------------------------------------------------< PropertyHandler >--- + @Override + public boolean canImport(PropertyImportContext context, boolean isCollection) { + // version is read only + return false; + } + + @Override + public Map importProperties(PropertyImportContext importContext, boolean isCollection) throws RepositoryException { + // version is read only + throw new RepositoryException("Properties cannot be imported"); + } + + /** + * @see PropertyHandler#exportProperties(PropertyExportContext, boolean) + */ + @Override + public boolean exportProperties(PropertyExportContext exportContext, boolean isCollection) throws RepositoryException { + if (!canExport(exportContext, isCollection)) { + throw new RepositoryException("PropertyHandler " + getName() + " failed to export properties."); + } + Node cn = getContentNode(exportContext, isCollection); + try { + // export the properties common with normal IO handling + exportProperties(exportContext, isCollection, cn); + // BUT don't export the special properties defined by nt:version + return true; + } catch (IOException e) { + // should not occur (log output see 'exportProperties') + return false; + } + } + + /** + * Retrieves the content node that contains the data to be exported. + * + * @param context + * @param isCollection + * @return content node used for the export + * @throws javax.jcr.RepositoryException + */ + @Override + protected Node getContentNode(ExportContext context, boolean isCollection) throws RepositoryException { + Node node = (Node)context.getExportRoot(); + Node frozenNode = node.getNode(JcrConstants.JCR_FROZENNODE); + if (frozenNode.hasNode(JcrConstants.JCR_CONTENT)) { + return frozenNode.getNode(JcrConstants.JCR_CONTENT); + } else { + return frozenNode; + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/VersionHistoryHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/VersionHistoryHandler.java new file mode 100644 index 00000000000..14eb6323d18 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/VersionHistoryHandler.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.server.io; + +import java.io.IOException; +import java.util.Map; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.property.PropEntry; + +/** + * VersionHistoryHandler... + */ +public class VersionHistoryHandler implements IOHandler, PropertyHandler { + + private IOManager ioManager; + + public VersionHistoryHandler() { + } + + public VersionHistoryHandler(IOManager ioManager) { + this.ioManager = ioManager; + } + + //----------------------------------------------------------< IOHandler >--- + public IOManager getIOManager() { + return ioManager; + } + + public void setIOManager(IOManager ioManager) { + this.ioManager = ioManager; + } + + public String getName() { + return getClass().getName(); + } + + public boolean canImport(ImportContext context, boolean isCollection) { + return false; + } + + public boolean canImport(ImportContext context, DavResource resource) { + return false; + } + + public boolean importContent(ImportContext context, boolean isCollection) throws IOException { + throw new UnsupportedOperationException(); + } + + public boolean importContent(ImportContext context, DavResource resource) throws IOException { + throw new UnsupportedOperationException(); + } + + public boolean canExport(ExportContext context, boolean isCollection) { + if (context == null) { + return false; + } + return context.getExportRoot() instanceof VersionHistory; + } + + public boolean canExport(ExportContext context, DavResource resource) { + if (context == null) { + return false; + } + return context.getExportRoot() instanceof VersionHistory; + } + + public boolean exportContent(ExportContext context, boolean isCollection) throws IOException { + Item exportRoot = context.getExportRoot(); + if (exportRoot instanceof VersionHistory) { + return export(context); + } else { + return false; + } + } + + public boolean exportContent(ExportContext context, DavResource resource) throws IOException { + Item exportRoot = context.getExportRoot(); + if (exportRoot instanceof VersionHistory) { + return export(context); + } else { + return false; + } + } + + //----------------------------------------------------< PropertyHandler >--- + public boolean canImport(PropertyImportContext context, boolean isCollection) { + return false; + } + + public Map importProperties(PropertyImportContext importContext, boolean isCollection) throws RepositoryException { + throw new UnsupportedOperationException(); + } + + public boolean canExport(PropertyExportContext context, boolean isCollection) { + return canExport((ExportContext) context, isCollection); + } + + public boolean exportProperties(PropertyExportContext exportContext, boolean isCollection) throws RepositoryException { + if (!canExport(exportContext, isCollection)) { + throw new RepositoryException("PropertyHandler " + getName() + " failed to export properties."); + } + return export(exportContext); + } + + //-------------------------------------------------------------------------- + private boolean export(ExportContext exportContext) { + // don't export any properties of the version history node. deltaV + // defines a fix set of properties to be exported and the dav-resource + // needs to take care of those. + exportContext.setContentLength(0); + exportContext.setModificationTime(IOUtil.UNDEFINED_TIME); + return true; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/XmlHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/XmlHandler.java new file mode 100644 index 00000000000..59c443ea185 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/XmlHandler.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.JcrConstants; + +/** + * XmlHandler imports xml files and exports nodes that have + * the proper {@link #XML_MIMETYPE} defined with their content. The export is + * performed by running a {@link Session#exportDocumentView(String, OutputStream, boolean, boolean) + * document view export} for the content of the export root defined with the + * specified {@link ExportContext}. + *

        + * Please note that this handler is not suited for a generic system or document + * view import/export of {@link Node}s because an extra root node is always + * created during import and expected during export, respectively. + */ +public class XmlHandler extends DefaultHandler { + + /** + * the xml mimetype + */ + public static final String XML_MIMETYPE = "text/xml"; + + /** + * the alternative xml mimetype. tika detects xml as this. + */ + public static final String XML_MIMETYPE_ALT = "application/xml"; + + private static final Set supportedTypes; + static { + supportedTypes = new HashSet(); + supportedTypes.add(XML_MIMETYPE); + supportedTypes.add(XML_MIMETYPE_ALT); + } + + + /** + * Creates a new XmlHandler with default nodetype definitions + * and without setting the IOManager. + * + * @see IOHandler#setIOManager(IOManager) + */ + public XmlHandler() { + } + + /** + * Creates a new XmlHandler with default nodetype definitions:
        + *

          + *
        • Nodetype for Collection: {@link JcrConstants#NT_UNSTRUCTURED nt:unstructured}
        • + *
        • Nodetype for Non-Collection: {@link JcrConstants#NT_FILE nt:file}
        • + *
        • Nodetype for Non-Collection content: {@link JcrConstants#NT_UNSTRUCTURED nt:unstructured}
        • + *
        + * + * @param ioManager + */ + public XmlHandler(IOManager ioManager) { + super(ioManager, JcrConstants.NT_UNSTRUCTURED, JcrConstants.NT_FILE, JcrConstants.NT_UNSTRUCTURED); + } + + /** + * Creates a new XmlHandler + * + * @param ioManager + * @param collectionNodetype + * @param defaultNodetype + * @param contentNodetype + */ + public XmlHandler(IOManager ioManager, String collectionNodetype, String defaultNodetype, String contentNodetype) { + super(ioManager, collectionNodetype, defaultNodetype, contentNodetype); + } + + /** + * @see IOHandler#canImport(ImportContext, boolean) + */ + @Override + public boolean canImport(ImportContext context, boolean isCollection) { + return !(context == null || context.isCompleted()) + && supportedTypes.contains(context.getMimeType()) + && context.hasStream() + && context.getContentLength() > 0 + && super.canImport(context, isCollection); + } + + /** + * @see DefaultHandler#importData(ImportContext, boolean, Node) + */ + @Override + protected boolean importData(ImportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException { + InputStream in = context.getInputStream(); + int uuidBehavior = (isCollection) + ? ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING + : ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW; + try { + contentNode.getSession().importXML(contentNode.getPath(), in, uuidBehavior); + } finally { + in.close(); + } + return true; + } + + /** + * @see DefaultHandler#importProperties(ImportContext, boolean, Node) + */ + @Override + protected boolean importProperties(ImportContext context, boolean isCollection, Node contentNode) { + boolean success = super.importProperties(context, isCollection, contentNode); + if (success) { + // encoding: always UTF-8 for the xml import + try { + contentNode.setProperty(JcrConstants.JCR_ENCODING, "UTF-8"); + } catch (RepositoryException e) { + // ignore, since given nodetype could not allow encoding + // deliberately not re-throwing an IOException. + } + } + return success; + } + + /** + * {@inheritDoc} + * + * @return true, always. + */ + @Override + protected boolean forceCompatibleContentNodes() { + return true; + } + + /** + * @see IOHandler#canExport(ExportContext, boolean) + */ + @Override + public boolean canExport(ExportContext context, boolean isCollection) { + if (super.canExport(context, isCollection)) { + String mimeType = null; + try { + Node contentNode = getContentNode(context, isCollection); + if (contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { + mimeType = contentNode.getProperty(JcrConstants.JCR_MIMETYPE).getString(); + } else { + mimeType = detect(context.getExportRoot().getName()); + } + } catch (RepositoryException e) { + // ignore and return false + } + return XML_MIMETYPE.equals(mimeType); + } + return false; + } + + /** + * @see DefaultHandler#exportData(ExportContext, boolean, Node) + */ + @Override + protected void exportData(ExportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException { + // first child of content is XML document root + if (contentNode.getNodes().hasNext()) { + contentNode = contentNode.getNodes().nextNode(); + } + OutputStream out = context.getOutputStream(); + contentNode.getSession().exportDocumentView(contentNode.getPath(), out, true, false); + } + + /** + * @see DefaultHandler#exportProperties(ExportContext, boolean, Node) + */ + @Override + protected void exportProperties(ExportContext context, boolean isCollection, Node contentNode) throws IOException { + super.exportProperties(context, isCollection, contentNode); + // set mimetype if the content node did not provide the + // jcr property (thus not handled by super class) + try { + if (!contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { + context.setContentType("text/xml", "UTF-8"); + } + } catch (RepositoryException e) { + // should never occur + throw new IOException(e.getMessage()); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ZipHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ZipHandler.java new file mode 100644 index 00000000000..a99585b96bb --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/io/ZipHandler.java @@ -0,0 +1,377 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.io; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * ZipHandler imports and extracts Zip files and exported nodes + * (an their subnodes) to a Zip file. Please not that for the export the selected + * export root must have the property {@link #ZIP_MIMETYPE} defined with its + * content. Furthermore the content must not represent a zip-file that has + * been imported to a binary {@link Property property}, which is properly + * handled by the {@link DefaultHandler}. + */ +public class ZipHandler extends DefaultHandler { + + private static Logger log = LoggerFactory.getLogger(ZipHandler.class); + + /** + * the zip mimetype + */ + public static final String ZIP_MIMETYPE = "application/zip"; + + private boolean intermediateSave; + + /** + * Creates a new ZipHandler with default nodetype definitions + * and without setting the IOManager. + * + * @see IOHandler#setIOManager(IOManager) + */ + public ZipHandler() { + } + + /** + * Creates a new ZipHandler with default nodetype definitions:
        + *
          + *
        • Nodetype for Collection: {@link JcrConstants#NT_UNSTRUCTURED nt:unstructured}
        • + *
        • Nodetype for Non-Collection: {@link JcrConstants#NT_FILE nt:file}
        • + *
        • Nodetype for Non-Collection content: {@link JcrConstants#NT_UNSTRUCTURED nt:unstructured}
        • + *
        + * + * @param ioManager + * @throws IllegalArgumentException if the specified IOManager + * is null + */ + public ZipHandler(IOManager ioManager) { + this(ioManager, JcrConstants.NT_FOLDER, JcrConstants.NT_FILE, JcrConstants.NT_UNSTRUCTURED); + } + + /** + * Creates a new ZipHandler + * + * @param ioManager + * @param collectionNodetype + * @param defaultNodetype + * @param contentNodetype + * @throws IllegalArgumentException if the specified IOManager + * is null + */ + public ZipHandler(IOManager ioManager, String collectionNodetype, String defaultNodetype, String contentNodetype) { + super(ioManager, collectionNodetype, defaultNodetype, contentNodetype); + if (ioManager == null) { + throw new IllegalArgumentException("The IOManager must not be null."); + } + } + + /** + * If set to true the import root will be {@link Item#save() saved} + * after every imported zip entry. Note however, that this removes the possibility + * to revert all modifications if the import cannot be completed successfully. + * By default the intermediate save is disabled. + * + * @param intermediateSave + */ + public void setIntermediateSave(boolean intermediateSave) { + this.intermediateSave = intermediateSave; + } + + /** + * @see IOHandler#canImport(ImportContext, boolean) + */ + @Override + public boolean canImport(ImportContext context, boolean isCollection) { + if (context == null || context.isCompleted()) { + return false; + } + boolean isZip = ZIP_MIMETYPE.equals(context.getMimeType()); + return isZip && context.hasStream() && super.canImport(context, isCollection); + } + + /** + * @see DefaultHandler#importData(ImportContext, boolean, Node) + */ + @Override + protected boolean importData(ImportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException { + boolean success = true; + InputStream in = context.getInputStream(); + ZipInputStream zin = new ZipInputStream(in); + try { + ZipEntry entry; + while ((entry=zin.getNextEntry())!=null && success) { + success = importZipEntry(zin, entry, context, contentNode); + zin.closeEntry(); + } + } finally { + zin.close(); + in.close(); + } + return success; + } + + /** + * @see IOHandler#canExport(ExportContext, boolean) + */ + @Override + public boolean canExport(ExportContext context, boolean isCollection) { + if (super.canExport(context, isCollection)) { + // mimetype must be application/zip + String mimeType = null; + // if zip-content has not been extracted -> delegate to some other handler + boolean hasDataProperty = false; + try { + Node contentNode = getContentNode(context, isCollection); + // jcr:data property indicates that the zip-file has been imported as binary (not extracted) + hasDataProperty = contentNode.hasProperty(JcrConstants.JCR_DATA); + if (contentNode.hasProperty(JcrConstants.JCR_MIMETYPE)) { + mimeType = contentNode.getProperty(JcrConstants.JCR_MIMETYPE).getString(); + } else { + mimeType = detect(context.getExportRoot().getName()); + } + } catch (RepositoryException e) { + // ignore and return false + } + return ZIP_MIMETYPE.equals(mimeType) && !hasDataProperty; + } + return false; + } + + /** + * @see DefaultHandler#exportData(ExportContext,boolean,Node) + */ + @Override + protected void exportData(ExportContext context, boolean isCollection, Node contentNode) throws IOException, RepositoryException { + ZipOutputStream zout = new ZipOutputStream(context.getOutputStream()); + zout.setMethod(ZipOutputStream.DEFLATED); + try { + int pos = contentNode.getPath().length(); + exportZipEntry(context, zout, contentNode, pos > 1 ? pos+1 : pos); + } finally { + zout.finish(); + } + } + + /** + * If the specified node is the defined non-collection nodetype a new + * Zip entry is created and the exportContent is called on the IOManager + * defined with this handler. If in contrast the specified node does not + * represent a non-collection this method is called recursively for all + * child nodes. + * + * @param context + * @param zout + * @param node + * @param pos + * @throws IOException + */ + private void exportZipEntry(ExportContext context, ZipOutputStream zout, Node node, int pos) throws IOException{ + try { + if (node.isNodeType(getNodeType())) { + ZipEntryExportContext subctx = new ZipEntryExportContext(node, zout, context, pos); + // try if iomanager can treat node as zip entry otherwise recurs. + zout.putNextEntry(subctx.entry); + getIOManager().exportContent(subctx, false); + } else { + // recurs + NodeIterator niter = node.getNodes(); + while (niter.hasNext()) { + exportZipEntry(context, zout, niter.nextNode(), pos); + } + } + } catch (RepositoryException e) { + log.error(e.getMessage()); + // should never occur + } + } + + /** + * Creates a new sub context for the specified Zip entry and passes it to + * the IOManager defined with this handler. + * + * @param zin + * @param entry + * @param context + * @param node + * @return + * @throws RepositoryException + * @throws IOException + */ + private boolean importZipEntry(ZipInputStream zin, ZipEntry entry, ImportContext context, Node node) throws RepositoryException, IOException { + boolean success = false; + log.debug("entry: " + entry.getName() + " size: " + entry.getSize()); + if (entry.isDirectory()) { + IOUtil.mkDirs(node, makeValidJCRPath(entry.getName(), false), getCollectionNodeType()); + success = true; + } else { + // import zip entry as file + BoundedInputStream bin = new BoundedInputStream(zin); + bin.setPropagateClose(false); + ImportContext entryContext = new ZipEntryImportContext(context, entry, bin, node); + + // let the iomanager deal with the individual entries. + IOManager ioManager = getIOManager(); + success = (ioManager != null) ? ioManager.importContent(entryContext, false) : false; + + // intermediate save in order to avoid problems with large zip files + if (intermediateSave) { + context.getImportRoot().save(); + } + } + return success; + } + + /** + * Creates a valid jcr label from the given one + * + * @param label + * @return + */ + private static String makeValidJCRPath(String label, boolean appendLeadingSlash) { + if (appendLeadingSlash && !label.startsWith("/")) { + label = "/" + label; + } + StringBuffer ret = new StringBuffer(label.length()); + for (int i=0; i--- + /** + * Inner class used to create subcontexts for the import of the individual + * zip file entries. + */ + private class ZipEntryImportContext extends ImportContextImpl { + + private final Item importRoot; + private final ZipEntry entry; + + private ZipEntryImportContext(ImportContext context, ZipEntry entry, BoundedInputStream bin, Node contentNode) throws IOException, RepositoryException { + super(contentNode, Text.getName(makeValidJCRPath(entry.getName(), true)), + null, bin, context.getIOListener(), getIOManager().getDetector()); + this.entry = entry; + String path = makeValidJCRPath(entry.getName(), true); + importRoot = IOUtil.mkDirs(contentNode, Text.getRelativeParent(path, 1), getCollectionNodeType()); + } + + @Override + public Item getImportRoot() { + return importRoot; + } + + @Override + public long getModificationTime() { + return entry.getTime(); + } + + @Override + public long getContentLength() { + return entry.getSize(); + } + } + + /** + * Inner class used to create subcontexts for the export of the individual + * zip file entries. + */ + private static class ZipEntryExportContext extends AbstractExportContext { + + private ZipEntry entry; + private OutputStream out; + + private ZipEntryExportContext(Item exportRoot, OutputStream out, ExportContext context, int pos) { + super(exportRoot, out != null, context.getIOListener()); + this.out = out; + try { + String entryPath = (exportRoot.getPath().length() > pos) ? exportRoot.getPath().substring(pos) : ""; + entry = new ZipEntry(entryPath); + } catch (RepositoryException e) { + // should never occur + } + } + + /** + * Returns the Zip output stream. Note, that this context does not + * deal properly with multiple IOHandlers writing to the stream. + * + * @return + */ + public OutputStream getOutputStream() { + return out; + } + + public void setContentType(String mimeType, String encoding) { + if (entry != null) { + entry.setComment(mimeType); + } + } + + public void setContentLanguage(String contentLanguage) { + // ignore + } + + public void setContentLength(long contentLength) { + if (entry != null) { + entry.setSize(contentLength); + } + } + + public void setCreationTime(long creationTime) { + // ignore + } + + public void setModificationTime(long modificationTime) { + if (entry != null) { + entry.setTime(modificationTime); + } + } + + public void setETag(String etag) { + // ignore + } + + public void setProperty(Object propertyName, Object propertyValue) { + // ignore + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/jcr/JCRWebdavServer.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/jcr/JCRWebdavServer.java new file mode 100644 index 00000000000..461b5818701 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/jcr/JCRWebdavServer.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.server.jcr; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.server.SessionProvider; +import org.apache.jackrabbit.spi.commons.SessionExtensions; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.DavSessionProvider; +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.header.IfHeader; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.util.LinkHeaderFieldParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.LoginException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * JCRWebdavServer... + */ +public class JCRWebdavServer implements DavSessionProvider { + + /** the default logger */ + private static Logger log = LoggerFactory.getLogger(JCRWebdavServer.class); + + /** the session cache */ + private final SessionCache cache; + + /** the jcr repository */ + private final Repository repository; + + /** the provider for the credentials */ + private final SessionProvider sessionProvider; + + /** + * Creates a new JCRWebdavServer that operates on the given repository. + * + * @param repository + */ + public JCRWebdavServer(Repository repository, SessionProvider sessionProvider) { + this.repository = repository; + this.sessionProvider = sessionProvider; + cache = new SessionCache(); + } + + /** + * Creates a new JCRWebdavServer that operates on the given repository. + * + * @param repository + * @param concurrencyLevel + */ + public JCRWebdavServer(Repository repository, SessionProvider sessionProvider, int concurrencyLevel) { + this.repository = repository; + this.sessionProvider = sessionProvider; + cache = new SessionCache(concurrencyLevel); + } + + //---------------------------------------< DavSessionProvider interface >--- + /** + * Acquires a DavSession either from the session cache or creates a new + * one by login to the repository. + * Upon success, the WebdavRequest will reference that session. + * + * @param request + * @throws DavException if no session could be obtained. + * @see DavSessionProvider#attachSession(org.apache.jackrabbit.webdav.WebdavRequest) + */ + public boolean attachSession(WebdavRequest request) + throws DavException { + DavSession session = cache.get(request); + request.setDavSession(session); + return true; + } + + /** + * Releases the reference from the request to the session. If no further + * references to the session exist, the session will be removed from the + * cache. + * + * @param request + * @see DavSessionProvider#releaseSession(org.apache.jackrabbit.webdav.WebdavRequest) + */ + public void releaseSession(WebdavRequest request) { + DavSession session = request.getDavSession(); + if (session != null) { + session.removeReference(request); + } + // remove the session from the request + request.setDavSession(null); + } + + //-------------------------------------------------------------------------- + /** + * Private inner class implementing the DavSession interface. + */ + private class DavSessionImpl extends JcrDavSession { + + /** + * Private constructor. + * + * @param session + */ + private DavSessionImpl(Session session) { + super(session); + } + + /** + * Add a reference to this DavSession. + * + * @see DavSession#addReference(Object) + */ + public void addReference(Object reference) { + cache.addReference(this, reference); + } + + /** + * Removes the reference from this DavSession. If no + * more references are present, this DavSession is removed + * from the internal cache and the underlying session is released by + * calling {@link SessionProvider#releaseSession(javax.jcr.Session)} + * + * @see DavSession#removeReference(Object) + */ + public void removeReference(Object reference) { + cache.removeReference(this, reference); + } + } + + /** + * Private inner class providing a cache for referenced session objects. + */ + private class SessionCache { + + private static final int CONCURRENCY_LEVEL_DEFAULT = 50; + private static final int INITIAL_CAPACITY = 50; + private static final int INITIAL_CAPACITY_REF_TO_SESSION = 3 * INITIAL_CAPACITY; + + private ConcurrentMap> sessionMap; + private ConcurrentMap referenceToSessionMap; + + /** + * Create a new session cache with the {@link #CONCURRENCY_LEVEL_DEFAULT default concurrency level}. + */ + private SessionCache() { + this(CONCURRENCY_LEVEL_DEFAULT); + } + + /** + * Create a new session cache with the specified the level of concurrency + * for this server. + * + * @param cacheConcurrencyLevel A positive int value specifying the + * concurrency level of the server. + */ + private SessionCache(int cacheConcurrencyLevel) { + sessionMap = new ConcurrentHashMap>(INITIAL_CAPACITY, .75f, cacheConcurrencyLevel); + referenceToSessionMap = new ConcurrentHashMap(INITIAL_CAPACITY_REF_TO_SESSION, .75f, cacheConcurrencyLevel); + } + + /** + * Try to retrieve DavSession if a TransactionId or + * SubscriptionId is present in the request header. If no cached session + * was found null is returned. + * + * @param request + * @return a cached DavSession or null. + * @throws DavException + */ + private DavSession get(WebdavRequest request) + throws DavException { + String txId = request.getTransactionId(); + String subscriptionId = request.getSubscriptionId(); + String lockToken = request.getLockToken(); + + DavSession session = null; + // try to retrieve a cached session + if (lockToken != null && containsReference(lockToken)) { + session = getSessionByReference(lockToken); + } else if (txId != null && containsReference(txId)) { + session = getSessionByReference(txId); + } else if (subscriptionId != null && containsReference(subscriptionId)) { + session = getSessionByReference(subscriptionId); + } + + if (session == null) { + // try tokens present in the if-header + IfHeader ifHeader = new IfHeader(request); + for (Iterator it = ifHeader.getAllTokens(); it.hasNext();) { + String token = it.next(); + if (containsReference(token)) { + session = getSessionByReference(token); + break; + } + } + } + + // no cached session present -> create new one. + if (session == null) { + Session repSession = getRepositorySession(request); + session = new DavSessionImpl(repSession); + + // TODO: review again if using ConcurrentMap#putIfAbsent() was more appropriate. + sessionMap.put(session, new HashSet()); + log.debug("login: User '" + repSession.getUserID() + "' logged in."); + } else { + log.debug("login: Retrieved cached session for user '" + getUserID(session) + "'"); + } + addReference(session, request); + return session; + } + + /** + * Add a references to the specified DavSession. + * + * @param session + * @param reference + */ + private void addReference(DavSession session, Object reference) { + Set referenceSet = sessionMap.get(session); + if (referenceSet != null) { + referenceSet.add(reference); + referenceToSessionMap.put(reference, session); + } else { + log.error("Failed to add reference to session. No entry in cache found."); + } + } + + /** + * Remove the given reference from the specified DavSession. + * + * @param session + * @param reference + */ + private void removeReference(DavSession session, Object reference) { + Set referenceSet = sessionMap.get(session); + if (referenceSet != null) { + if (referenceSet.remove(reference)) { + log.debug("Removed reference " + reference + " to session " + session); + referenceToSessionMap.remove(reference); + } else { + log.warn("Failed to remove reference " + reference + " to session " + session); + } + if (referenceSet.isEmpty()) { + log.debug("No more references present on webdav session -> clean up."); + sessionMap.remove(session); + try { + Session repSession = DavSessionImpl.getRepositorySession(session); + String usr = getUserID(session) ; + sessionProvider.releaseSession(repSession); + log.debug("Login: User '" + usr + "' logged out"); + } catch (DavException e) { + // should not occur, since we originally built a + // DavSessionImpl that wraps a repository session. + log.error("Unexpected error: " + e.getMessage(), e.getCause()); + } + } else { + log.debug(referenceSet.size() + " references remaining on webdav session " + session); + } + } else { + log.error("Failed to remove reference from session. No entry in cache found."); + } + } + + /** + * Returns true, if there exists a DavSession in the cache + * that is referenced by the specified object. + * + * @param reference + * @return true if a DavSession is referenced by the given + * object. + */ + private boolean containsReference(Object reference) { + return referenceToSessionMap.containsKey(reference); + } + + /** + * Returns the DavSession that is referenced by the + * specified reference object. + * + * @param reference + * @return DavSession that is referenced by this reference + * object. + * @see #containsReference(Object) + */ + private DavSession getSessionByReference(Object reference) { + return referenceToSessionMap.get(reference); + } + + /** + * Retrieve the {@link Session} object for the given request. + * + * @param request + * @return JCR session object used to build the DavSession + * @throws DavException + * @throws DavException in case a {@link javax.jcr.LoginException} or {@link javax.jcr.RepositoryException} occurs. + */ + private Session getRepositorySession(WebdavRequest request) throws DavException { + try { + String workspaceName = null; + if (DavMethods.DAV_MKWORKSPACE != DavMethods.getMethodCode(request.getMethod())) { + workspaceName = request.getRequestLocator().getWorkspaceName(); + } + + Session session = sessionProvider.getSession( + request, repository, workspaceName); + + // extract information from Link header fields + LinkHeaderFieldParser lhfp = + new LinkHeaderFieldParser(request.getHeaders("Link")); + setJcrUserData(session, lhfp); + setSessionIdentifier(session, lhfp); + + return session; + } catch (LoginException e) { + // LoginException results in UNAUTHORIZED, + throw new JcrDavException(e); + } catch (RepositoryException e) { + // RepositoryException results in FORBIDDEN + throw new JcrDavException(e); + } catch (ServletException e) { + throw new DavException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * Find first link relation for JCR user data and set it as + * the user data of the observation manager of the given session. + */ + private void setJcrUserData( + Session session, LinkHeaderFieldParser lhfp) + throws RepositoryException { + String data = null; + + // extract User Data string from RFC 2397 "data" URI + // only supports the simple case of "data:,..." for now + String target = lhfp.getFirstTargetForRelation( + JcrRemotingConstants.RELATION_USER_DATA); + if (target != null) { + try { + URI uri = new URI(target); + // Poor Man's data: URI parsing + if ("data".equalsIgnoreCase(uri.getScheme())) { + String sspart = uri.getRawSchemeSpecificPart(); + if (sspart.startsWith(",")) { + data = Text.unescape(sspart.substring(1)); + } + } + } catch (URISyntaxException ex) { + // not a URI, skip + } + } + + try { + session.getWorkspace().getObservationManager().setUserData(data); + } catch (UnsupportedRepositoryOperationException ignore) { + } + } + + /** + * Find first link relation for remote session identifier and set + * it as an attribute of the given session. + */ + private void setSessionIdentifier( + Session session, LinkHeaderFieldParser lhfp) { + if (session instanceof SessionExtensions) { + String name = JcrRemotingConstants.RELATION_REMOTE_SESSION_ID; + String id = lhfp.getFirstTargetForRelation(name); + ((SessionExtensions) session).setAttribute(name, id); + } + } + + private String getUserID(DavSession session) { + try { + Session s = DavSessionImpl.getRepositorySession(session); + if (s != null) { + return s.getUserID(); + } + } catch (DavException e) { + log.error(e.toString()); + } + // fallback + return session.toString(); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/package-info.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/package-info.java new file mode 100644 index 00000000000..03b39e9c9e7 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0") +package org.apache.jackrabbit.server; diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/AclRemoveHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/AclRemoveHandler.java new file mode 100644 index 00000000000..9f508b5710b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/AclRemoveHandler.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.server.remoting.davex; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; + +import org.apache.jackrabbit.util.Text; + +public class AclRemoveHandler implements ProtectedItemRemoveHandler { + + private static final String NT_REP_ACL = "rep:ACL"; + + @Override + public boolean remove(Session session, String itemPath) throws RepositoryException { + if (canHandle(session, itemPath)) { + String controlledPath = Text.getRelativeParent(itemPath, 1); + AccessControlManager acMgr = session.getAccessControlManager(); + AccessControlPolicy[] policies = acMgr.getPolicies(controlledPath); + for (AccessControlPolicy policy : policies) { + acMgr.removePolicy(controlledPath, policy); + } + return true; + } + return false; + } + + // -----------------------------------------------------------< private >--- + private boolean canHandle(Session session, String itemPath) throws RepositoryException { + Item aclItem = session.getItem(itemPath); + if (aclItem.isNode() && itemPath.startsWith("/")) { + if (isJackrabbitAclNodeType((Node) aclItem)) { + return true; + } + } + return false; + } + + private boolean isJackrabbitAclNodeType(Node aclNode) throws RepositoryException { + String ntName = aclNode.getPrimaryNodeType().getName(); + return ntName.equals(NT_REP_ACL); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/BatchReadConfig.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/BatchReadConfig.java new file mode 100644 index 00000000000..b7c5fba0367 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/BatchReadConfig.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.server.remoting.davex; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import java.util.Map; +import java.util.HashMap; +import java.util.Properties; +import java.util.Enumeration; +import java.io.InputStream; +import java.io.IOException; + +/** + * BatchReadConfig defines if and how deep child item + * information should be retrieved, when accessing a Node. + * The configuration is based on node type names. + */ +class BatchReadConfig { + + private static Logger log = LoggerFactory.getLogger(BatchReadConfig.class); + + private static final String NAME_DEFAULT = "default"; + public static final int DEPTH_DEFAULT = 0; + public static final int DEPTH_INFINITE = -1; + + private int defaultDepth = DEPTH_DEFAULT; + private final Map depthMap = new HashMap(); + + /** + * Create an empty batch-read config. + */ + BatchReadConfig() {} + + /** + * Load the batch read configuration. + * + * @param in An input stream. + * @throws IOException If an error occurs. + */ + public void load(InputStream in) throws IOException { + Properties props = new Properties(); + props.load(in); + add(props); + } + + /** + * Add the configuration entries present in the given properties. + * + * @param props + */ + public void add(Properties props) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + String name = en.nextElement().toString(); + String depthStr = props.getProperty(name); + try { + int depth = Integer.parseInt(depthStr); + if (depth < DEPTH_INFINITE) { + log.warn("invalid depth " + depthStr + " -> ignoring."); + continue; + } + if (NAME_DEFAULT.equals(name)) { + setDefaultDepth(depth); + } else { + setDepth(name, depth); + } + } catch (NumberFormatException e) { + // invalid entry in the properties file -> ignore + log.warn("Invalid depth value for name " + name + ". " + depthStr + " cannot be parsed into an integer."); + } + } + } + + /** + * Return the depth for the given node type name. If the name is + * not defined in this configuration, the {@link #DEPTH_DEFAULT default value} + * is returned. + * + * @param ntName The jcr name of the node type. + * @return {@link #DEPTH_INFINITE -1} If all child infos should be return or + * any value greater than {@link #DEPTH_DEFAULT 0} if only parts of the + * subtree should be returned. If the given nodetype name is not defined + * in this configuration, the default depth {@link #DEPTH_DEFAULT 0} will + * be returned. + */ + public int getDepth(String ntName) { + if (depthMap.containsKey(ntName)) { + return depthMap.get(ntName); + } else { + return defaultDepth; + } + } + + /** + * Return the depth for the given node or the default depth if the config + * does not provide an specific entry for the given node. + * + * @param node The node for with depth information should be retrieved. + * @return {@link #DEPTH_INFINITE -1} If all child infos should be return or + * any value greater than {@link #DEPTH_DEFAULT 0} if only parts of the + * subtree should be returned. + */ + public int getDepth(Node node) { + int depth = defaultDepth; + try { + String ntName = node.getPrimaryNodeType().getName(); + if (depthMap.containsKey(ntName)) { + depth = depthMap.get(ntName); + } + } catch (RepositoryException e) { + // ignore and return default. + } + return depth; + } + + /** + * Define the batch-read depth for the given node type name. + * + * @param ntName jcr name of the node type for which depth is defined. + * @param depth Depth for the specified node type name. + * @throws IllegalArgumentException if ntName is null + * or depth is lower than {@link #DEPTH_INFINITE}. + */ + public void setDepth(String ntName, int depth) { + if (ntName == null || depth < DEPTH_INFINITE) { + throw new IllegalArgumentException(); + } + depthMap.put(ntName, depth); + } + + /** + * Returns the default depth. + * + * @return the default depth. + */ + public int getDefaultDepth() { + return defaultDepth; + } + + /** + * Set the default depth. + * + * @param depth The default depth. + * @throws IllegalArgumentException if depth is lower than + * {@link #DEPTH_INFINITE}. + */ + public void setDefaultDepth(int depth) { + if (depth < -1) { + throw new IllegalArgumentException(); + } + defaultDepth = depth; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DavexServletService.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DavexServletService.java new file mode 100644 index 00000000000..7c6b35b5797 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DavexServletService.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.server.remoting.davex; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.jcr.LoginException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Properties; +import org.apache.felix.scr.annotations.Property; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.ReferencePolicy; +import org.apache.felix.scr.annotations.Service; +import org.apache.jackrabbit.server.SessionProvider; +import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet; +import org.apache.jackrabbit.webdav.util.CSRFUtil; + +@Component(metatype = true, label = "%dav.name", description = "%dav.description") +@Service(Servlet.class) +@Properties({ + @Property(name = "service.description", value = "Apache Jackrabbit JcrRemoting Servlet"), + @Property(name = JcrRemotingServlet.INIT_PARAM_AUTHENTICATE_HEADER, value = AbstractWebdavServlet.DEFAULT_AUTHENTICATE_HEADER), + @Property(name = JcrRemotingServlet.INIT_PARAM_CSRF_PROTECTION, value = CSRFUtil.DISABLED), + @Property(name = JcrRemotingServlet.INIT_PARAM_MISSING_AUTH_MAPPING, value = ""), + @Property(name = "contextId", value = "") }) +@Reference( + name = "providers", referenceInterface = SessionProvider.class, + policy = ReferencePolicy.DYNAMIC, + cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, + bind = "addSessionProvider", unbind = "removeSessionProvider") +public class DavexServletService extends JcrRemotingServlet + implements SessionProvider { + + /** Serial version UID */ + private static final long serialVersionUID = -901601294536148635L; + + private static final String DEFAULT_ALIAS = "/server"; + + @Property(value = DEFAULT_ALIAS) + private static final String PARAM_ALIAS = "alias"; + + @Reference + private Repository repository; + + private String alias; + + /** + * Currently available custom session providers. They're used + * first before the default provider gets consulted. The associated + * set of sessions is used to forcibly release all sessions acquired + * from a provider when that provider is being removed. + */ + private final Map> providers = + new LinkedHashMap>(); + + /** + * Currently active sessions. Used to link a session to the correct + * provider in the {@link #releaseSession(Session)} method. + */ + private final Map sessions = + new HashMap(); + + @Override + protected Repository getRepository() { + return repository; + } + + @Override + protected String getResourcePathPrefix() { + return alias; + } + + @Activate + public void activate(Map config) { + Object object = config.get(PARAM_ALIAS); + String string = ""; + if (object != null) { + string = object.toString(); + } + if (string.length() > 0) { + this.alias = string; + } else { + this.alias = DEFAULT_ALIAS; + } + } + + @Override + protected SessionProvider getSessionProvider() { + return this; + } + + /** + * Adds a custom session provider service. + * + * @param provider session provider + */ + public synchronized void addSessionProvider(SessionProvider provider) { + providers.put(provider, new HashSet()); + } + + /** + * Removes a custom session provider service. All active sessions + * acquired from that provider are forcibly released. + * + * @param provider session provider + */ + public synchronized void removeSessionProvider(SessionProvider provider) { + Set sessions = providers.remove(provider); + if (sessions != null) { + for (Session session : sessions) { + releaseSession(session); + } + } + } + + //-----------------------------------------------------< SessionProvider > + + /** + * Asks each available session provider in order for a session and + * returns the first session given. The default provider is used + * if no custom provider service is available or can provide a requested + * session. + */ + public synchronized Session getSession( + HttpServletRequest request, Repository repository, String workspace) + throws LoginException, ServletException, RepositoryException { + SessionProvider provider = null; + Session session = null; + + for (Map.Entry> entry : providers.entrySet()) { + provider = entry.getKey(); + session = provider.getSession(request, repository, workspace); + if (session != null) { + entry.getValue().add(session); + break; + } + } + + if (session == null) { + provider = super.getSessionProvider(); + session = provider.getSession(request, repository, workspace); + } + + if (session != null) { + sessions.put(session, provider); + } + + return session; + } + + /** + * Releases the given session using the provider from which it was acquired. + */ + public synchronized void releaseSession(Session session) { + SessionProvider provider = sessions.remove(session); + if (provider != null) { + provider.releaseSession(session); + } + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffException.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffException.java new file mode 100644 index 00000000000..d80d716241f --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffException.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.server.remoting.davex; + +import java.io.IOException; + +/** + * DiffException... + */ +class DiffException extends IOException { + + private Throwable cause; + + public DiffException(String message) { + super(message); + } + + public DiffException(String message, Throwable cause) { + super(message); + this.cause = cause; + } + + @Override + public Throwable getCause() { + return cause; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffHandler.java new file mode 100644 index 00000000000..432bfe9cd39 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffHandler.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.server.remoting.davex; + +/** + * DiffHandler... + */ +interface DiffHandler { + + void addNode(String targetPath, String diffValue) throws DiffException; + + void setProperty(String targetPath, String diffValue) throws DiffException; + + void remove(String targetPath, String diffValue) throws DiffException; + + void move(String targetPath, String diffValue) throws DiffException; +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffParser.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffParser.java new file mode 100644 index 00000000000..bfb64c150ce --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/DiffParser.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.remoting.davex; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** DiffParser... */ +class DiffParser { + + // TODO: review again: currently all line-sep. chars before an diff-char are + // TODO: ignored unless they are escaped in way the handler understands (e.g. + // TODO: JSON does: \\r for \r). + // TODO: in contrast line sep. at the end of the string are treated as value. + // TODO: ... similar: line sep. following by non-diff symbol. + + private final DiffHandler handler; + + private static final int EOF = -1; + + private static final char SYMBOL_ADD_NODE = '+'; + private static final char SYMBOL_MOVE = '>'; + private static final char SYMBOL_REMOVE = '-'; + private static final char SYMBOL_SET_PROPERTY = '^'; + + private static final int STATE_START_LINE = 0; + private static final int STATE_START_TARGET = 1; + private static final int STATE_TARGET = 2; + private static final int STATE_START_VALUE = 3; + private static final int STATE_VALUE = 4; + + /** + * + * @param handler + */ + public DiffParser(DiffHandler handler) { + this.handler = handler; + } + + public void parse(String str) throws IOException, DiffException { + parse(new BufferedReader(new StringReader(str))); + } + + public void parse(InputStream input, String charSetName) throws IOException, DiffException { + parse(new BufferedReader(new InputStreamReader(input, charSetName))); + } + + public void parse(Reader reader) throws IOException, DiffException { + int action = -1; + String path = null; + + StringBuffer lineSeparator = null; + StringBuffer bf = null; + + int state = STATE_START_LINE; + int next = reader.read(); + + while (next != EOF) { + switch (state) { + case STATE_START_LINE: + if (isSymbol(next)) { + // notify the last action read + if (action > -1) { + informAction(action, path, bf); + } + // ... and start recording the next action + action = next; + bf = null; + lineSeparator = null; + state = STATE_START_TARGET; + } else if (isLineSeparator(next)) { + // still line-separator -> append c to the lineSeparator + // buffer and keep state set to STATE_START_LINE + if (lineSeparator == null) { + throw new DiffException("Invalid start of new line."); + } else { + lineSeparator.append((char) next); + } + } else if (lineSeparator != null && bf != null) { + // append the collected return/linefeed chars as part + // of the value read and continued reading value. + bf.append(lineSeparator); + bf.append((char) next); + lineSeparator = null; + state = STATE_VALUE; + } else { + throw new DiffException("Invalid start of new line."); + } + break; + + case STATE_START_TARGET: + if (Character.isWhitespace((char) next) || next == ':') { + throw new DiffException("Invalid start of target path '" + next + "'"); + } + bf = new StringBuffer(); + bf.append((char) next); + state = STATE_TARGET; + break; + + case STATE_TARGET: + if (Character.isWhitespace((char) next) && endsWithDelim(bf)) { + // a sequence of 'wsp:wsp' indicates the delimiter between + // the target path and the diff value. + path = bf.substring(0, bf.lastIndexOf(":")).trim(); + state = STATE_START_VALUE; + // reset buffer + bf = null; + } else { + // continue reading the path into the buffer. + bf.append((char) next); + } + break; + + case STATE_START_VALUE: + if (isLineSeparator(next)) { + lineSeparator = new StringBuffer(); + lineSeparator.append((char) next); + bf = new StringBuffer(); + state = STATE_START_LINE; + } else { + bf = new StringBuffer(); + bf.append((char) next); + state = STATE_VALUE; + } + break; + + case STATE_VALUE: + if (isLineSeparator(next)) { + lineSeparator = new StringBuffer(); + lineSeparator.append((char) next); + state = STATE_START_LINE; + } else { + bf.append((char) next); + // keep state set to STATE_VALUE + } + break; + + } + // read the next character. + next = reader.read(); + } + + // a diff ending after a command or within the target is invalid. + if (state == STATE_START_TARGET || state == STATE_TARGET) { + throw new DiffException("Invalid end of DIFF string: missing separator and value."); + } + if (state == STATE_START_VALUE ) { + // line separator AND buffer must be null + if (!(lineSeparator == null && bf == null)) { + throw new DiffException("Invalid end of DIFF string."); + } + } + + // append eventual remaining line-separators to the value + if (lineSeparator != null) { + bf.append(lineSeparator); + } + // notify the last action read + informAction(action, path, bf); + } + + private void informAction(int action, String path, StringBuffer diffVal) throws DiffException { + if (path == null) { + throw new DiffException("Missing path for action " + action + "(diffValue = '"+ diffVal +"')"); + } + String value = (diffVal == null) ? null : diffVal.toString(); + switch (action) { + case SYMBOL_ADD_NODE: + handler.addNode(path, value); + break; + case SYMBOL_SET_PROPERTY: + handler.setProperty(path, value); + break; + case SYMBOL_MOVE: + handler.move(path, value); + break; + case SYMBOL_REMOVE: + handler.remove(path, value); + break; + default: + throw new DiffException("Invalid action " + action); + } + } + + private static boolean isSymbol(int c) { + return c == SYMBOL_ADD_NODE || c == SYMBOL_SET_PROPERTY || c == SYMBOL_MOVE || c == SYMBOL_REMOVE; + } + + private static boolean isLineSeparator(int c) { + return c == '\n' || c == '\r'; + + } + private static boolean endsWithDelim(StringBuffer bf) { + if (bf.length() < 2) { + return false; + } else { + return ':' == bf.charAt(bf.length()-1) && Character.isWhitespace(bf.charAt(bf.length()-2)); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JcrRemotingServlet.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JcrRemotingServlet.java new file mode 100644 index 00000000000..752724459ed --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JcrRemotingServlet.java @@ -0,0 +1,807 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.remoting.davex; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Set; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NodeType; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.server.util.RequestData; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.WebdavResponse; +import org.apache.jackrabbit.webdav.jcr.JCRWebdavServerServlet; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl; +import org.apache.jackrabbit.webdav.observation.SubscriptionManager; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JcrRemotingServlet is an extended version of the + * {@link org.apache.jackrabbit.webdav.jcr.JCRWebdavServerServlet JCR Remoting Servlet} + * that provides improved + * + * functionality and supports cross workspace copy and cloning. + * + *

        Batch Read

        + * + * Upon RepositoryService.getItemInfos a JSON object is composed containing + * the information for the requested node and its child items up to a + * specified or configuration determined depth. + *

        + * Batch read is triggered by adding a '.json' extension to the resource href. + * Optionally the client may explicitly specify the desired batch read depth + * by appending '.depth.json' extension. If no json extension is present the + * GET request is processed by the base servlet. + *

        + * The JSON writer applies the following rules: + * + *

        + * - Nodes are represented as JSON objects.
        + *
        + * - Each Node has its properties included as JSON key/value pairs.
        + *
        + * - Single valued Properties are simple key/value pairs.
        + *
        + * - Multi valued Properties are represented as JSON array.
        + *
        + * - Each Node has its child nodes included as long a maximal depths is not reached.
        + * 
        + * - Nodes without any child nodes get a special JSON member named
        + *   ::NodeIteratorSize, whose value is zero.
        + *
        + * - If the maximal depth is reached only name, index and unique id of the
        + *   direct child are included (incomplete node info). In order to obtain
        + *   the complete information the client sends another GET with .json extension.
        + * 
        + * + * Same name sibling nodes and properties whose type cannot be unambiguously be + * extracted from the JSON on the client side need some special handling: + * + *
        + * - Node with index > 1, get a JSON key consisting of
        + *   Node.getName() + "[" + Node.getIndex() + "]" 
        + *
        + * - Binary Property
        + *   JSON value = length of the JCR value.
        + *   The JCR value must be retrieved separately.
        + *
        + * - Name, Path, Reference and Date Property
        + *   The JSON member representing the Property (name, value) is preceded by a
        + *   special member consisting of
        + *   JSON key = ":" + Property.getName()
        + *   JSON value = PropertyType.nameFromValue(Property.getType())
        + *
        + * - Multi valued properties with Property.getValues().length == 0 will be
        + *   treated as special property types above (extra property indicating the
        + *   type of the property).
        + *
        + * - Double Property
        + *   JSON value must not have any trailing ".0" removed.
        + * 
        + * + *

        Multi Read

        + *

        + * Since Jackrabbit 2.3.6 it is also possible to request multiple subtrees + * in a single request. This is done by adding one or more ":include" + * parameters to a batch read request describe above. These extra parameters + * specify the (relative) paths of all the nodes to be included in the + * response. The response is a JSON object whose "nodes" property contains + * all the selected nodes keyed by path. Missing nodes are not included in + * the response. Each included node is serialized as defined above for + * batch read. + *

        + * Example: + *

        + * $ curl 'http://.../parent.json?:path=child1&:path=child2'
        + * {"nodes":{"/parent/child1":{...},"/parent/child2":{...}}}
        + * 
        + * + *

        Batch Write

        + * + * The complete SPI Batch is sent to the server in a single request, currently a + * POST request containing a custom ":diff" parameter. + *
        + * NOTE that this is targeted to be replaced by a PATCH request. + * + *

        Diff format

        + * + * The diff parameter currently consists of JSON-like key-value pairs with the + * following special requirements: + * + *
        + *   diff       ::= members
        + *   members    ::= pair | pairs
        + *   pair       ::= key " : " value
        + *   pairs      ::= pair line-end pair | pair line-end pairs
        + *   line-end   ::= "\r\n" | "\n" | "\r"
        + *   key        ::= diffchar path
        + *   diffchar   ::= "+" | "^" | "-" | ">"
        + *   path       ::= abspath | relpath
        + *   abspath    ::= * absolute path to an item *
        + *   relpath    ::= * relpath from item at request URI to an item *
        + *   value      ::= value+ | value- | value^ | value>
        + *   value+     ::= * a JSON object *
        + *   value-     ::= ""
        + *   value^     ::= * any JSON value except JSON object *
        + *   value>     ::= path | path "#before" | path "#after" | "#first" | "#last"
        + * 
        + * + * In other words: + *
          + *
        • diff consists of one or more key-value pair(s)
        • + *
        • key must start with a diffchar followed by a rel. or abs. item path
        • + *
        • diffchar being any of "+", "^", "-" or ">" representing the transient + * item modifications as follows + *
          + *   "+" addNode
          + *   "^" setProperty / setValue / removeProperty
          + *   "-" remove Item
          + *   ">" move / reorder Nodes
          + * 
          + *
        • + *
        • key must be separated from the value by a ":" surrounded by whitespace.
        • + *
        • two pairs must be separated by a line end
        • + *
        • the format of the value depends on the diffchar
        • + *
        • for moving around node the value must consist of a abs. or rel. path. + * in contrast reordering of existing nodes is achieved by appending a trailing + * order position hint (#first, #last, #before or #after)
        • + *
        + * + * NOTE the following special handling of JCR properties of type + * Binary, Name, Path, Date and Reference: + *
          + *
        • the JSON value must be missing
        • + *
        • the POST request is expected to contain extra multipart(s) or request + * parameter(s) for the property value(s)
        • + *
        • the content type of the extra parts/params must reflect the property + * type:"jcr-value/" + PropertyType.nameFromValue(Property.getType).toLowerCase()
        • + *
        + * + * @see www.json.org for the definition of + * JSON object and JSON value. + */ +public abstract class JcrRemotingServlet extends JCRWebdavServerServlet { + + private static Logger log = LoggerFactory.getLogger(JcrRemotingServlet.class); + + /** + * the home init parameter. other relative filesystem paths are + * relative to this location. + */ + public static final String INIT_PARAM_HOME = "home"; + + /** + * the 'temp-directory' init parameter + */ + public static final String INIT_PARAM_TMP_DIRECTORY = "temp-directory"; + /** + * temp-dir attribute to be set to the servlet-context + */ + public static final String ATTR_TMP_DIRECTORY = "remoting-servlet.tmpdir"; + + /** + * the 'temp-directory' init parameter + */ + public static final String INIT_PARAM_BATCHREAD_CONFIG = "batchread-config"; + + /** + * the 'protectedhandlers-config' init paramter. this parameter contains the XML + * configuration file for protected item remove handlers. + */ + public static final String INIT_PARAM_PROTECTED_HANDLERS_CONFIG = "protectedhandlers-config"; + + private static final String PARAM_DIFF = ":diff"; + private static final String PARAM_COPY = ":copy"; + private static final String PARAM_CLONE = ":clone"; + private static final String PARAM_INCLUDE = ":include"; + + private BatchReadConfig brConfig; + private ProtectedRemoveManager protectedRemoveManager; + + @Override + public void init() throws ServletException { + super.init(); + + brConfig = new BatchReadConfig(); + String brConfigParam = getServletConfig().getInitParameter(INIT_PARAM_BATCHREAD_CONFIG); + if (brConfigParam == null) { + // TODO: define default values. + log.debug("batchread-config missing -> initialize defaults."); + brConfig.setDepth("nt:file", BatchReadConfig.DEPTH_INFINITE); + brConfig.setDefaultDepth(5); + } else { + try { + InputStream in = getServletContext().getResourceAsStream(brConfigParam); + if (in != null) { + brConfig.load(in); + } + } catch (IOException e) { + log.debug("Unable to build BatchReadConfig from " + brConfigParam + "."); + } + } + + String protectedHandlerConfig = getServletConfig().getInitParameter(INIT_PARAM_PROTECTED_HANDLERS_CONFIG); + InputStream in = null; + try { + in = getServletContext().getResourceAsStream(protectedHandlerConfig); + if (in != null){ + protectedRemoveManager = new ProtectedRemoveManager(); + protectedRemoveManager.load(in); + } else { + //Config might be direct class implementation + protectedRemoveManager = new ProtectedRemoveManager(protectedHandlerConfig); + } + } catch (IOException e) { + log.debug("Unable to create ProtectedRemoveManager from " + protectedHandlerConfig , e); + } finally{ + if (in != null){ + try { + in.close(); + } catch (IOException ignore) { + } + } + } + + // Determine the configured location for temporary files used when + // processing file uploads. Since JCR-3029 the default is the + // standard java.io.tmpdir location, but the presence of explicit + // configuration parameters restores the original behavior. + File tmp = null; + ServletConfig config = getServletConfig(); + String paramHome = config.getInitParameter(INIT_PARAM_HOME); + String paramTemp = config.getInitParameter(INIT_PARAM_TMP_DIRECTORY); + if (paramHome != null || paramTemp != null) { + if (paramHome == null) { + log.debug("Missing init-param " + INIT_PARAM_HOME + + ". Using default: 'jackrabbit'"); + paramHome = "jackrabbit"; + } else if (paramTemp == null) { + log.debug("Missing init-param " + INIT_PARAM_TMP_DIRECTORY + + ". Using default: 'tmp'"); + paramTemp = "tmp"; + } + + tmp = new File(paramHome, paramTemp); + try { + tmp = tmp.getCanonicalFile(); + tmp.mkdirs(); + log.debug(" temp-directory = " + tmp.getPath()); + } catch (IOException e) { + log.warn("Invalid temporary directory " + tmp.getPath() + + ", using system default instead", e); + tmp = null; + } + } + getServletContext().setAttribute(ATTR_TMP_DIRECTORY, tmp); + + // force usage of custom locator factory. + super.setLocatorFactory(new DavLocatorFactoryImpl(getResourcePathPrefix())); + } + + protected String getResourcePathPrefix() { + return getInitParameter(INIT_PARAM_RESOURCE_PATH_PREFIX); + } + + @Override + public DavResourceFactory getResourceFactory() { + return new ResourceFactoryImpl(txMgr, subscriptionMgr); + } + + @Override + protected void doGet(WebdavRequest webdavRequest, + WebdavResponse webdavResponse, + DavResource davResource) throws IOException, DavException { + if (canHandle(DavMethods.DAV_GET, webdavRequest, davResource)) { + // return json representation of the requested resource + DavResourceLocator locator = davResource.getLocator(); + String path = locator.getRepositoryPath(); + + Session session = getRepositorySession(webdavRequest); + try { + Node node = session.getNode(path); + int depth = ((WrappingLocator) locator).getDepth(); + + webdavResponse.setContentType("text/plain;charset=utf-8"); + webdavResponse.setStatus(DavServletResponse.SC_OK); + JsonWriter writer = new JsonWriter(webdavResponse.getWriter()); + + String[] includes = webdavRequest.getParameterValues(PARAM_INCLUDE); + if (includes == null) { + if (depth < BatchReadConfig.DEPTH_INFINITE) { + NodeType type = node.getPrimaryNodeType(); + depth = brConfig.getDepth(type.getName()); + } + writer.write(node, depth); + } else { + writeMultiple(writer, node, includes, depth); + } + } catch (PathNotFoundException e) { + // properties cannot be requested as json object. + throw new JcrDavException( + new ItemNotFoundException("No node at " + path), + DavServletResponse.SC_NOT_FOUND); + } catch (RepositoryException e) { + // should only get here if the item does not exist. + log.debug(e.getMessage()); + throw new JcrDavException(e); + } + } else { + super.doGet(webdavRequest, webdavResponse, davResource); + } + } + + private void writeMultiple( + JsonWriter writer, Node node, String[] includes, int depth) + throws RepositoryException, IOException { + Collection nodes = new ArrayList(); + Set alreadyAdded = new HashSet(); + for (String include : includes) { + try { + Node n; + if (include.startsWith("/")) { + n = node.getSession().getNode(include); + } else { + n = node.getNode(include); + } + String np = n.getPath(); + if (!alreadyAdded.contains(np)) { + nodes.add(n); + alreadyAdded.add(np); + } + } catch (PathNotFoundException e) { + // skip missing node + } + } + writer.write(nodes, depth); + } + + @Override + protected void doPost(WebdavRequest webdavRequest, WebdavResponse webdavResponse, DavResource davResource) + throws IOException, DavException { + if (canHandle(DavMethods.DAV_POST, webdavRequest, davResource)) { + // special remoting request: the defined parameters are exclusive + // and cannot be combined. + Session session = getRepositorySession(webdavRequest); + RequestData data = new RequestData(webdavRequest, getTempDirectory(getServletContext())); + String loc = null; + try { + String[] pValues; + String[] includes = null; // multi-read over POST + if ((pValues = data.getParameterValues(PARAM_CLONE)) != null) { + loc = clone(session, pValues, davResource.getLocator()); + } else if ((pValues = data.getParameterValues(PARAM_COPY)) != null) { + loc = copy(session, pValues, davResource.getLocator()); + } else if (data.getParameterValues(PARAM_DIFF) != null) { + String targetPath = davResource.getLocator().getRepositoryPath(); + processDiff(session, targetPath, data, protectedRemoveManager); + } else if ((pValues = data.getParameterValues(PARAM_INCLUDE)) != null + && canHandle(DavMethods.DAV_GET, webdavRequest, davResource)) { + includes = pValues; + } else { + String targetPath = davResource.getLocator().getRepositoryPath(); + loc = modifyContent(session, targetPath, data, protectedRemoveManager); + } + + // TODO: append entity + if (loc == null) { + webdavResponse.setStatus(HttpServletResponse.SC_OK); + if (includes != null) { + webdavResponse.setContentType("text/plain;charset=utf-8"); + JsonWriter writer = new JsonWriter(webdavResponse.getWriter()); + + DavResourceLocator locator = davResource.getLocator(); + String path = locator.getRepositoryPath(); + + Node node = session.getNode(path); + int depth = ((WrappingLocator) locator).getDepth(); + + writeMultiple(writer, node, includes, depth); + } + } else { + webdavResponse.setHeader(DeltaVConstants.HEADER_LOCATION, loc); + webdavResponse.setStatus(HttpServletResponse.SC_CREATED); + } + } catch (RepositoryException e) { + log.warn(e.getMessage(), e); + throw new JcrDavException(e); + } catch (DiffException e) { + log.warn(e.getMessage()); + Throwable cause = e.getCause(); + if (cause instanceof RepositoryException) { + throw new JcrDavException((RepositoryException) cause); + } else { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid diff format."); + } + } finally { + data.dispose(); + } + } else { + super.doPost(webdavRequest, webdavResponse, davResource); + } + } + + private boolean canHandle(int methodCode, WebdavRequest request, DavResource davResource) { + DavResourceLocator locator = davResource.getLocator(); + switch (methodCode) { + case DavMethods.DAV_GET: + return davResource.exists() && (locator instanceof WrappingLocator) + && ((WrappingLocator) locator).isJsonRequest; + case DavMethods.DAV_POST: + String ct = request.getContentType(); + if (ct == null) { + return false; + } else { + int semicolon = ct.indexOf(';'); + if (semicolon >= 0) { + ct = ct.substring(0, semicolon); + } + ct = ct.trim().toLowerCase(Locale.ENGLISH); + return "multipart/form-data".equals(ct) || "application/x-www-form-urlencoded".equals(ct); + } + default: + return false; + } + } + + private static String clone(Session session, String[] cloneArgs, DavResourceLocator reqLocator) throws RepositoryException { + Workspace wsp = session.getWorkspace(); + String destPath = null; + for (String cloneArg : cloneArgs) { + String[] args = cloneArg.split(","); + if (args.length == 4) { + wsp.clone(args[0], args[1], args[2], Boolean.valueOf(args[3])); + destPath = args[2]; + } else { + throw new RepositoryException(":clone parameter must have a value consisting of the 4 args needed for a Workspace.clone() call."); + } + } + return buildLocationHref(session, destPath, reqLocator); + } + + private static String copy(Session session, String[] copyArgs, DavResourceLocator reqLocator) throws RepositoryException { + Workspace wsp = session.getWorkspace(); + String destPath = null; + for (String copyArg : copyArgs) { + String[] args = copyArg.split(","); + switch (args.length) { + case 2: + wsp.copy(args[0], args[1]); + destPath = args[1]; + break; + case 3: + wsp.copy(args[0], args[1], args[2]); + destPath = args[2]; + break; + default: + throw new RepositoryException(":copy parameter must have a value consisting of 2 jcr paths or workspaceName plus 2 jcr paths separated by ','."); + } + } + return buildLocationHref(session, destPath, reqLocator); + } + + private static String buildLocationHref(Session s, String destPath, DavResourceLocator reqLocator) throws RepositoryException { + if (destPath != null) { + NodeIterator it = s.getRootNode().getNodes(destPath.substring(1)); + Node n = null; + while (it.hasNext()) { + n = it.nextNode(); + } + if (n != null) { + DavResourceLocator loc = reqLocator.getFactory().createResourceLocator(reqLocator.getPrefix(), reqLocator.getWorkspacePath(), n.getPath(), false); + return loc.getHref(true); + } + } + + // unable to determine -> no location header sent back. + return null; + } + + private static void processDiff(Session session, String targetPath, RequestData data, ProtectedRemoveManager protectedRemoveManager) + throws RepositoryException, DiffException, IOException { + + String[] diffs = data.getParameterValues(PARAM_DIFF); + DiffHandler handler = new JsonDiffHandler(session, targetPath, data, protectedRemoveManager); + DiffParser parser = new DiffParser(handler); + + for (String diff : diffs) { + boolean success = false; + try { + parser.parse(diff); + + session.save(); + success = true; + } finally { + if (!success) { + session.refresh(false); + } + } + } + } + + /** + * TODO: doesn't work properly with intermediate SNS-nodes + * TODO: doesn't respect jcr:uuid properties. + */ + private static String modifyContent(Session session, String targetPath, RequestData data, ProtectedRemoveManager protectedRemoveManager) + throws RepositoryException, DiffException { + + JsonDiffHandler dh = new JsonDiffHandler(session, targetPath, data, protectedRemoveManager); + boolean success = false; + try { + for (Iterator pNames = data.getParameterNames(); pNames.hasNext();) { + String paramName = pNames.next(); + String propPath = dh.getItemPath(paramName); + String parentPath = Text.getRelativeParent(propPath, 1); + + if (!session.itemExists(parentPath) || !session.getItem(parentPath).isNode()) { + createNode(session, parentPath, data); + } + + if (JcrConstants.JCR_PRIMARYTYPE.equals(Text.getName(propPath))) { + // already handled by createNode above -> ignore + continue; + } + // none of the special properties -> let the diffhandler take care + // of the property creation/modification. + dh.setProperty(paramName, null); + } + + // save the complete set of modifications + session.save(); + success = true; + } finally { + if (!success) { + session.refresh(false); + } + } + return null; // TODO build loc-href if items were created. + } + + private static void createNode(Session session, String nodePath, RequestData data) throws RepositoryException { + Node parent = session.getRootNode(); + String[] smgts = Text.explode(nodePath, '/'); + + for (String nodeName : smgts) { + if (parent.hasNode(nodeName)) { + parent = parent.getNode(nodeName); + } else { + // need to create the node + // TODO: won't work for SNS + String nPath = parent.getPath() + "/" + nodeName; + String ntName = data.getParameter(nPath + "/" + JcrConstants.JCR_PRIMARYTYPE); + if (ntName == null) { + parent = parent.addNode(nodeName); + } else { + parent = parent.addNode(nodeName, ntName); + } + } + } + } + + private static Session getRepositorySession(WebdavRequest request) throws DavException { + DavSession ds = request.getDavSession(); + return JcrDavSession.getRepositorySession(ds); + } + + /** + * Returns the temp directory + * + * @return the temp directory + */ + private static File getTempDirectory(ServletContext servletCtx) { + return (File) servletCtx.getAttribute(ATTR_TMP_DIRECTORY); + } + + //-------------------------------------------------------------------------- + /** + * Locator factory that specially deals with hrefs having a .json extension. + */ + private static class DavLocatorFactoryImpl extends org.apache.jackrabbit.webdav.jcr.DavLocatorFactoryImpl { + + public DavLocatorFactoryImpl(String s) { + super(s); + } + + @Override + public DavResourceLocator createResourceLocator(String prefix, String href) { + return createResourceLocator(prefix, href, false); + } + + @Override + public DavResourceLocator createResourceLocator(String prefix, String href, boolean forDestination) { + DavResourceLocator loc = super.createResourceLocator(prefix, href); + if (!forDestination && endsWithJson(href)) { + loc = new WrappingLocator(super.createResourceLocator(prefix, href)); + } + return loc; + } + + @Override + public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) { + DavResourceLocator loc = super.createResourceLocator(prefix, workspacePath, path, isResourcePath); + if (isResourcePath && endsWithJson(path)) { + loc = new WrappingLocator(loc); + } + return loc; + } + + private static boolean endsWithJson(String s) { + return s.endsWith(".json"); + } + } + + /** + * Resource locator that removes trailing .json extensions and depth + * selector that do not form part of the repository path. + * As the locator and it's factory do not have access to a JCR session + * the extraJson flag may be reset later on. + * + * @see ResourceFactoryImpl#getItem(org.apache.jackrabbit.webdav.jcr.JcrDavSession, org.apache.jackrabbit.webdav.DavResourceLocator) + */ + private static class WrappingLocator implements DavResourceLocator { + + private final DavResourceLocator loc; + private boolean isJsonRequest = true; + private int depth = Integer.MIN_VALUE; + private String repositoryPath; + + private WrappingLocator(DavResourceLocator loc) { + this.loc = loc; + } + + private void extract() { + String rp = loc.getRepositoryPath(); + rp = rp.substring(0, rp.lastIndexOf('.')); + int pos = rp.lastIndexOf('.'); + if (pos > -1) { + String depthStr = rp.substring(pos + 1); + try { + depth = Integer.parseInt(depthStr); + rp = rp.substring(0, pos); + } catch (NumberFormatException e) { + // apparently no depth-info -> ignore + } + } + repositoryPath = rp; + } + + private int getDepth() { + if (isJsonRequest) { + if (repositoryPath == null) { + extract(); + } + return depth; + } else { + return Integer.MIN_VALUE; + } + } + + public String getPrefix() { + return loc.getPrefix(); + } + public String getResourcePath() { + return loc.getResourcePath(); + } + public String getWorkspacePath() { + return loc.getWorkspacePath(); + } + public String getWorkspaceName() { + return loc.getWorkspaceName(); + } + public boolean isSameWorkspace(DavResourceLocator davResourceLocator) { + return loc.isSameWorkspace(davResourceLocator); + } + public boolean isSameWorkspace(String string) { + return loc.isSameWorkspace(string); + } + public String getHref(boolean b) { + return loc.getHref(b); + } + public boolean isRootLocation() { + return loc.isRootLocation(); + } + public DavLocatorFactory getFactory() { + return loc.getFactory(); + } + public String getRepositoryPath() { + if (isJsonRequest) { + if (repositoryPath == null) { + extract(); + } + return repositoryPath; + } else { + return loc.getRepositoryPath(); + } + } + } + + /** + * Resource factory used to make sure that the .json extension was properly + * interpreted. + */ + private static class ResourceFactoryImpl extends org.apache.jackrabbit.webdav.jcr.DavResourceFactoryImpl { + + /** + * Create a new DavResourceFactoryImpl. + * + * @param txMgr + * @param subsMgr + */ + public ResourceFactoryImpl(TxLockManagerImpl txMgr, SubscriptionManager subsMgr) { + super(txMgr, subsMgr); + } + + @Override + protected Item getItem(JcrDavSession sessionImpl, DavResourceLocator locator) throws PathNotFoundException, RepositoryException { + if (locator instanceof WrappingLocator && ((WrappingLocator)locator).isJsonRequest) { + // check if the .json extension has been correctly interpreted. + Session s = sessionImpl.getRepositorySession(); + try { + if (s.itemExists(((WrappingLocator)locator).loc.getRepositoryPath())) { + // an item exists with the original calculated repo-path + // -> assume that the repository item path ends with .json + // or .depth.json. i.e. .json wasn't an extra extension + // appended to request the json-serialization of the node. + // -> change the flag in the WrappingLocator correspondingly. + ((WrappingLocator) locator).isJsonRequest = false; + } + } catch (RepositoryException e) { + // if the unmodified repository path isn't valid (e.g. /a/b[2].5.json) + // -> ignore. + } + } + return super.getItem(sessionImpl, locator); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandler.java new file mode 100644 index 00000000000..fed6cda07b3 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandler.java @@ -0,0 +1,1022 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.remoting.davex; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.webdav.JcrValueType; +import org.apache.jackrabbit.server.util.RequestData; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.commons.json.JsonHandler; +import org.apache.jackrabbit.commons.json.JsonParser; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Stack; +import java.util.LinkedList; + +/** JsonDiffHandler... */ +class JsonDiffHandler implements DiffHandler { + + private static final Logger log = LoggerFactory.getLogger(JsonDiffHandler.class); + + private static final String ORDER_POSITION_AFTER = "#after"; + private static final String ORDER_POSITION_BEFORE = "#before"; + private static final String ORDER_POSITION_FIRST = "#first"; + private static final String ORDER_POSITION_LAST = "#last"; + + private final Session session; + private final ValueFactory vf; + private final String requestItemPath; + private final RequestData data; + private final ProtectedRemoveManager protectedRemoveManager; + + private NodeTypeManager ntManager; + + JsonDiffHandler(Session session, String requestItemPath, RequestData data) throws RepositoryException { + this(session, requestItemPath, data, null); + } + + JsonDiffHandler(Session session, String requestItemPath, RequestData data, ProtectedRemoveManager protectedRemoveManager) throws RepositoryException { + this.session = session; + this.requestItemPath = requestItemPath; + this.data = data; + vf = session.getValueFactory(); + this.protectedRemoveManager = protectedRemoveManager; + } + + //--------------------------------------------------------< DiffHandler >--- + /** + * @see DiffHandler#addNode(String, String) + */ + @Override + public void addNode(String targetPath, String diffValue) throws DiffException { + if (diffValue == null || !(diffValue.startsWith("{") && diffValue.endsWith("}"))) { + throw new DiffException("Invalid 'addNode' value '" + diffValue + "'"); + } + + try { + String itemPath = getItemPath(targetPath); + String parentPath = Text.getRelativeParent(itemPath, 1); + String nodeName = Text.getName(itemPath); + + addNode(parentPath, nodeName, diffValue); + + } catch (RepositoryException e) { + throw new DiffException(e.getMessage(), e); + } + } + + /** + * @see DiffHandler#setProperty(String, String) + */ + @Override + public void setProperty(String targetPath, String diffValue) throws DiffException { + try { + String itemPath = getItemPath(targetPath); + Item item = session.getItem(Text.getRelativeParent(itemPath, 1)); + if (!item.isNode()) { + throw new DiffException("No such node " + itemPath, new ItemNotFoundException(itemPath)); + } + + Node parent = (Node) item; + String propName = Text.getName(itemPath); + + if (JcrConstants.JCR_MIXINTYPES.equals(propName)) { + setMixins(parent, extractValuesFromRequest(targetPath)); + } else if (JcrConstants.JCR_PRIMARYTYPE.equals(propName)) { + setPrimaryType(parent, extractValuesFromRequest(targetPath)); + } else { + if (diffValue == null || diffValue.length() == 0) { + // single valued property with value present in multipart. + Value[] vs = extractValuesFromRequest(targetPath); + if (vs.length == 0) { + if (parent.hasProperty(propName)) { + // avoid problems with single vs. multi valued props. + parent.getProperty(propName).remove(); + } else { + // property does not exist -> stick to rule that missing + // [] indicates single valued. + parent.setProperty(propName, (Value) null); + } + } else if (vs.length == 1) { + parent.setProperty(propName, vs[0]); + } else { + throw new DiffException("Unexpected number of values in multipart. Was " + vs.length + " but expected 1."); + } + } else if (diffValue.startsWith("[") && diffValue.endsWith("]")) { + // multivalued property + if (diffValue.length() == 2) { + // empty array OR values in multipart + Value[] vs = extractValuesFromRequest(targetPath); + parent.setProperty(propName, vs); + } else { + // json array + Value[] vs = extractValues(diffValue); + parent.setProperty(propName, vs); + } + } else { + // single prop value included in the diff + Value v = extractValue(diffValue); + parent.setProperty(propName, v); + } + } + } catch (RepositoryException e) { + throw new DiffException(e.getMessage(), e); + } catch (IOException e) { + if (e instanceof DiffException) { + throw (DiffException) e; + } else { + throw new DiffException(e.getMessage(), e); + } + } + } + + /** + * @see DiffHandler#remove(String, String) + */ + @Override + public void remove(String targetPath, String diffValue) throws DiffException { + if (!(diffValue == null || diffValue.trim().length() == 0)) { + throw new DiffException("'remove' may not have a diffValue."); + } + try { + String itemPath = getItemPath(targetPath); + Item item = session.getItem(itemPath); + + ItemDefinition def = (item.isNode()) ? ((Node) item).getDefinition() : ((Property) item).getDefinition(); + if (def.isProtected()) { + // delegate to the manager. + if (protectedRemoveManager == null || !protectedRemoveManager.remove(session, itemPath)) { + throw new ConstraintViolationException("Cannot remove protected node: no suitable handler configured."); + } + } else { + item.remove(); + } + } catch (RepositoryException e) { + throw new DiffException(e.getMessage(), e); + } + } + + /** + * @see DiffHandler#move(String, String) + */ + @Override + public void move(String targetPath, String diffValue) throws DiffException { + if (diffValue == null || diffValue.length() == 0) { + throw new DiffException("Invalid 'move' value '" + diffValue + "'"); + } + try { + String srcPath = getItemPath(targetPath); + String orderPosition = getOrderPosition(diffValue); + if (orderPosition == null) { + // simple move + String destPath = getItemPath(diffValue); + session.move(srcPath, destPath); + } else { + String srcName = Text.getName(srcPath); + int pos = diffValue.lastIndexOf('#'); + String destName = (pos == 0) ? null : Text.getName(diffValue.substring(0, pos)); + + Item item = session.getItem(Text.getRelativeParent(srcPath, 1)); + if (!item.isNode()) { + throw new ItemNotFoundException(srcPath); + } + Node parent = (Node) item; + + if (ORDER_POSITION_FIRST.equals(orderPosition)) { + if (destName != null) { + throw new DiffException(ORDER_POSITION_FIRST + " may not have a leading destination."); + } + destName = Text.getName(parent.getNodes().nextNode().getPath()); + parent.orderBefore(srcName, destName); + } else if (ORDER_POSITION_LAST.equals(orderPosition)) { + if (destName != null) { + throw new DiffException(ORDER_POSITION_LAST + " may not have a leading destination."); + } + parent.orderBefore(srcName, null); + } else if (ORDER_POSITION_AFTER.equals(orderPosition)) { + if (destName == null) { + throw new DiffException(ORDER_POSITION_AFTER + " must have a leading destination."); + } + for (NodeIterator it = parent.getNodes(); it.hasNext();) { + Node child = it.nextNode(); + if (destName.equals(child.getName())) { + if (it.hasNext()) { + destName = Text.getName(it.nextNode().getName()); + } else { + destName = null; + } + break; + } + } + // reorder... if no child node matches the original destName + // the reorder will fail. no extra check. + parent.orderBefore(srcName, destName); + } else { + // standard jcr reorder (before) + parent.orderBefore(srcName, destName); + } + } + + } catch (RepositoryException e) { + throw new DiffException(e.getMessage(), e); + } + } + + //-------------------------------------------------------------------------- + /** + * + * @param diffPath + * @return + * @throws RepositoryException + */ + String getItemPath(String diffPath) throws RepositoryException { + StringBuffer itemPath; + if (!diffPath.startsWith("/")) { + // diff path is relative to the item path retrieved from the + // request URI -> calculate item path. + itemPath = new StringBuffer(requestItemPath); + if (!requestItemPath.endsWith("/")) { + itemPath.append('/'); + } + itemPath.append(diffPath); + } else { + itemPath = new StringBuffer(diffPath); + } + return normalize(itemPath.toString()); + } + + private void addNode(String parentPath, String nodeName, String diffValue) + throws DiffException, RepositoryException { + Item item = session.getItem(parentPath); + if (!item.isNode()) { + throw new ItemNotFoundException(parentPath); + } + + Node parent = (Node) item; + try { + NodeHandler hndlr = new NodeHandler(parent, nodeName); + new JsonParser(hndlr).parse(diffValue); + } catch (IOException e) { + if (e instanceof DiffException) { + throw (DiffException) e; + } else { + throw new DiffException(e.getMessage(), e); + } + } + } + + private NodeTypeManager getNodeTypeManager() throws RepositoryException { + if (ntManager == null) { + ntManager = session.getWorkspace().getNodeTypeManager(); + } + return ntManager; + } + + private static String normalize(String path) { + if (path.indexOf('.') == -1) { + return path; + } + String[] elems = Text.explode(path, '/', false); + LinkedList queue = new LinkedList(); + String last = ".."; + for (String segm : elems) { + if ("..".equals(segm) && !"..".equals(last)) { + queue.removeLast(); + if (queue.isEmpty()) { + last = ".."; + } else { + last = queue.getLast(); + } + } else if (!".".equals(segm)) { + last = segm; + queue.add(last); + } + } + return "/" + Text.implode(queue.toArray(new String[queue.size()]), "/"); + } + + private static ContentHandler createContentHandler(Node parent) throws RepositoryException { + return parent.getSession().getImportContentHandler(parent.getPath(), ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + } + + private static Node importNode(Node parent, String nodeName, String ntName, + String uuid) throws RepositoryException { + + String uri = "http://www.jcp.org/jcr/sv/1.0"; + String prefix = "sv:"; + + ContentHandler ch = createContentHandler(parent); + try { + ch.startDocument(); + + String nN = "node"; + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute(uri, "name", prefix + "name", "CDATA", nodeName); + ch.startElement(uri, nN, prefix + nN, attrs); + + // primary node type + String pN = "property"; + attrs = new AttributesImpl(); + attrs.addAttribute(uri, "name", prefix + "name", "CDATA", JcrConstants.JCR_PRIMARYTYPE); + attrs.addAttribute(uri, "type", prefix + "type", "CDATA", PropertyType.nameFromValue(PropertyType.NAME)); + ch.startElement(uri, pN, prefix + pN, attrs); + ch.startElement(uri, "value", prefix + "value", new AttributesImpl()); + char[] val = ntName.toCharArray(); + ch.characters(val, 0, val.length); + ch.endElement(uri, "value", prefix + "value"); + ch.endElement(uri, pN, prefix + pN); + + // uuid + attrs = new AttributesImpl(); + attrs.addAttribute(uri, "name", prefix + "name", "CDATA", JcrConstants.JCR_UUID); + attrs.addAttribute(uri, "type", prefix + "type", "CDATA", PropertyType.nameFromValue(PropertyType.STRING)); + ch.startElement(uri, pN, prefix + pN, attrs); + ch.startElement(uri, "value", prefix + "value", new AttributesImpl()); + val = uuid.toCharArray(); + ch.characters(val, 0, val.length); + ch.endElement(uri, "value", prefix + "value"); + ch.endElement(uri, pN, prefix + pN); + + ch.endElement(uri, nN, prefix + nN); + ch.endDocument(); + + } catch (SAXException e) { + throw new RepositoryException(e); + } + + Node n = null; + NodeIterator it = parent.getNodes(nodeName); + while (it.hasNext()) { + n = it.nextNode(); + } + if (n == null) { + throw new RepositoryException("Internal error: No child node added."); + } + return n; + } + + private static void setPrimaryType(Node n, Value[] values) throws RepositoryException, DiffException { + if (values.length == 1) { + String ntName = values[0].getString(); + if (!ntName.equals(n.getPrimaryNodeType().getName())) { + n.setPrimaryType(ntName); + } // else: same primaryType as before -> nothing to do. + } else { + throw new DiffException("Invalid diff: jcr:primarytype cannot have multiple values, nor can it's value be removed."); + } + } + + private static void setMixins(Node n, Value[] values) throws RepositoryException { + if (values.length == 0) { + // remove all mixins + NodeType[] mixins = n.getMixinNodeTypes(); + for (NodeType mixin : mixins) { + String mixinName = mixin.getName(); + n.removeMixin(mixinName); + } + } else { + List newMixins = new ArrayList(values.length); + for (Value value : values) { + newMixins.add(value.getString()); + } + NodeType[] mixins = n.getMixinNodeTypes(); + for (NodeType mixin : mixins) { + String mixinName = mixin.getName(); + if (!newMixins.remove(mixinName)) { + n.removeMixin(mixinName); + } + } + for (String newMixinName : newMixins) { + n.addMixin(newMixinName); + } + } + } + + private static String getOrderPosition(String diffValue) { + String position = null; + if (diffValue.indexOf('#') > -1) { + if (diffValue.endsWith(ORDER_POSITION_FIRST) || + diffValue.endsWith(ORDER_POSITION_LAST) || + diffValue.endsWith(ORDER_POSITION_BEFORE) || + diffValue.endsWith(ORDER_POSITION_AFTER)) { + position = diffValue.substring(diffValue.lastIndexOf('#')); + } // else: apparently # is part of the move path. + } + return position; + } + + private Value[] extractValuesFromRequest(String paramName) throws RepositoryException, IOException { + ValueFactory vf = session.getValueFactory(); + Value[] vs; + InputStream[] ins = data.getFileParameters(paramName); + if (ins != null) { + vs = new Value[ins.length]; + for (int i = 0; i < ins.length; i++) { + vs[i] = vf.createValue(ins[i]); + } + } else { + String[] strs = data.getParameterValues(paramName); + if (strs == null) { + vs = new Value[0]; + } else { + List valList = new ArrayList(strs.length); + for (int i = 0; i < strs.length; i++) { + if (strs[i] != null) { + String[] types = data.getParameterTypes(paramName); + int type = (types == null || types.length <= i) ? PropertyType.UNDEFINED : JcrValueType.typeFromContentType(types[i]); + if (type == PropertyType.UNDEFINED) { + valList.add(vf.createValue(strs[i])); + } else { + valList.add(vf.createValue(strs[i], type)); + } + } + } + vs = valList.toArray(new Value[valList.size()]); + } + } + return vs; + } + + private Value extractValue(String diffValue) throws RepositoryException, DiffException, IOException { + ValueHandler hndlr = new ValueHandler(); + // surround diff value { key : } to make it parsable + new JsonParser(hndlr).parse("{\"a\":"+diffValue+"}"); + + return hndlr.getValue(); + } + + private Value[] extractValues(String diffValue) throws RepositoryException, DiffException, IOException { + ValuesHandler hndlr = new ValuesHandler(); + // surround diff value { key : } to make it parsable + new JsonParser(hndlr).parse("{\"a\":" + diffValue + "}"); + + return hndlr.getValues(); + } + + //-------------------------------------------------------------------------- + /** + * Inner class used to parse a single value + */ + private final class ValueHandler implements JsonHandler { + private Value v; + + @Override + public void object() throws IOException { + // ignore + } + @Override + public void endObject() throws IOException { + // ignore + } + @Override + public void array() throws IOException { + // ignore + } + @Override + public void endArray() throws IOException { + // ignore + } + @Override + public void key(String key) throws IOException { + // ignore + } + + @Override + public void value(String value) throws IOException { + v = (value == null) ? null : vf.createValue(value); + } + @Override + public void value(boolean value) throws IOException { + v = vf.createValue(value); + } + @Override + public void value(long value) throws IOException { + v = vf.createValue(value); + } + @Override + public void value(double value) throws IOException { + v = vf.createValue(value); + } + + private Value getValue() { + return v; + } + } + + /** + * Inner class used to parse the values from a simple json array + */ + private final class ValuesHandler implements JsonHandler { + private List values = new ArrayList(); + + @Override + public void object() throws IOException { + // ignore + } + @Override + public void endObject() throws IOException { + // ignore + } + @Override + public void array() throws IOException { + // ignore + } + @Override + public void endArray() throws IOException { + // ignore + } + @Override + public void key(String key) throws IOException { + // ignore + } + + @Override + public void value(String value) throws IOException { + if (value != null) { + values.add(vf.createValue(value)); + } else { + log.warn("Null element for a multivalued property -> Ignore."); + } + } + @Override + public void value(boolean value) throws IOException { + values.add(vf.createValue(value)); + } + @Override + public void value(long value) throws IOException { + values.add(vf.createValue(value)); + } + @Override + public void value(double value) throws IOException { + values.add(vf.createValue(value)); + } + + private Value[] getValues() { + return values.toArray(new Value[values.size()]); + } + } + + /** + * Inner class for parsing a simple json object defining a node and its + * child nodes and/or child properties + */ + private final class NodeHandler implements JsonHandler { + private Node parent; + private String key; + + private Stack st = new Stack(); + + private NodeHandler(Node parent, String nodeName) throws IOException { + this.parent = parent; + key = nodeName; + } + + @Override + public void object() throws IOException { + ImportNode n; + if (st.isEmpty()) { + try { + n = new ImportNode(parent.getPath(), key); + } catch (RepositoryException e) { + throw new DiffException(e.getMessage(), e); + } + + } else { + ImportItem obj = st.peek(); + n = new ImportNode(obj.getPath(), key); + if (obj instanceof ImportNode) { + ((ImportNode) obj).addNode(n); + } else { + throw new DiffException("Invalid DIFF format: The JSONArray may only contain simple values."); + } + } + st.push(n); + } + + @Override + public void endObject() throws IOException { + // element on stack must be ImportMvProp since array may only + // contain simple values, no arrays/objects are allowed. + ImportItem obj = st.pop(); + if (!((obj instanceof ImportNode))) { + throw new DiffException("Invalid DIFF format."); + } + if (st.isEmpty()) { + // everything parsed -> start adding all nodes and properties + try { + if (obj.mandatesImport(parent)) { + obj.importItem(createContentHandler(parent)); + } else { + obj.createItem(parent); + } + } catch (IOException e) { + log.error(e.getMessage()); + throw new DiffException(e.getMessage(), e); + } catch (RepositoryException e) { + log.error(e.getMessage()); + throw new DiffException(e.getMessage(), e); + } + } + } + + @Override + public void array() throws IOException { + ImportItem obj = st.peek(); + ImportMvProp prop = new ImportMvProp(obj.getPath(), key); + if (obj instanceof ImportNode) { + ((ImportNode)obj).addProp(prop); + } else { + throw new DiffException("Invalid DIFF format: The JSONArray may only contain simple values."); + } + st.push(prop); + } + + @Override + public void endArray() throws IOException { + // element on stack must be ImportMvProp since array may only + // contain simple values, no arrays/objects are allowed. + ImportItem obj = st.pop(); + if (!((obj instanceof ImportMvProp))) { + throw new DiffException("Invalid DIFF format: The JSONArray may only contain simple values."); + } + } + + @Override + public void key(String key) throws IOException { + this.key = key; + } + + @Override + public void value(String value) throws IOException { + Value v = (value == null) ? null : vf.createValue(value); + value(v); + } + + @Override + public void value(boolean value) throws IOException { + value(vf.createValue(value)); + } + + @Override + public void value(long value) throws IOException { + Value v = vf.createValue(value); + value(v); + } + + @Override + public void value(double value) throws IOException { + value(vf.createValue(value)); + } + + private void value(Value v) throws IOException { + ImportItem obj = st.peek(); + if (obj instanceof ImportMvProp) { + ((ImportMvProp) obj).values.add(v); + } else { + ((ImportNode) obj).addProp(new ImportProp(obj.getPath(), key, v)); + } + } + } + + private abstract class ImportItem { + + static final String TYPE_CDATA = "CDATA"; + + final String parentPath; + final String name; + final String path; + + private ImportItem(String parentPath, String name) throws IOException { + if (name == null) { + throw new DiffException("Invalid DIFF format: NULL key."); + } + this.name = name; + this.parentPath = parentPath; + this.path = parentPath+"/"+name; + } + + void setNameAttribute(AttributesImpl attr) { + attr.addAttribute(Name.NS_SV_URI, "name", Name.NS_SV_PREFIX +":name", TYPE_CDATA, name); + } + + String getPath() { + return path; + } + + abstract boolean mandatesImport(Node parent); + + abstract void createItem(Node parent) throws RepositoryException, IOException; + + abstract void importItem(ContentHandler contentHandler) throws IOException; + } + + private final class ImportNode extends ImportItem { + + private static final String LOCAL_NAME = "node"; + + private ImportProp ntName; + private ImportProp uuid; + + private List childN = new ArrayList(); + private List childP = new ArrayList(); + + private ImportNode(String parentPath, String name) throws IOException { + super(parentPath, name); + } + + private String getUUID() { + if (uuid != null && uuid.value != null) { + try { + return uuid.value.getString(); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + return null; + } + + private String getPrimaryType() { + if (ntName != null && ntName.value != null) { + try { + return ntName.value.getString(); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + return null; + } + + @Override + boolean mandatesImport(Node parent) { + String primaryType = getPrimaryType(); + // Very simplistic and simplified test for protection that doesn't + // take mixin types into account and ignores all JCR primary types + if (!primaryType.startsWith(Name.NS_NT_PREFIX)) { + try { + NodeType nt = getNodeTypeManager().getNodeType(primaryType); + for (NodeDefinition nd : nt.getChildNodeDefinitions()) { + if (nd.isProtected()) { + return true; + } + } + for (PropertyDefinition pd : nt.getPropertyDefinitions()) { + if (!pd.getName().startsWith(Name.NS_JCR_PREFIX) && pd.isProtected()) { + return true; + } + } + } catch (RepositoryException e) { + log.warn(e.getMessage(), e); + } + } + return false; + } + + void addProp(ImportProp prop) { + if (prop.name.equals(JcrConstants.JCR_PRIMARYTYPE)) { + ntName = prop; + } else if (prop.name.equals(JcrConstants.JCR_UUID)) { + uuid = prop; + } else { + // regular property + childP.add(prop); + } + } + + void addProp(ImportMvProp prop) { + childP.add(prop); + } + + void addNode(ImportNode node) { + childN.add(node); + } + + @Override + void importItem(ContentHandler contentHandler) throws IOException { + try { + AttributesImpl attr = new AttributesImpl(); + setNameAttribute(attr); + contentHandler.startElement(Name.NS_SV_URI, LOCAL_NAME, Name.NS_SV_PREFIX+":"+LOCAL_NAME, attr); + + if (ntName != null && ntName.value != null) { + ntName.importItem(contentHandler); + } + if (uuid != null && uuid.value != null) { + uuid.importItem(contentHandler); + } + + for(ImportProperty prop : childP) { + prop.importItem(contentHandler); + } + + for(ImportNode node : childN) { + node.importItem(contentHandler); + } + contentHandler.endElement(Name.NS_SV_URI, LOCAL_NAME, Name.NS_SV_PREFIX+":"+LOCAL_NAME); + } catch(SAXException e) { + throw new DiffException(e.getMessage(), e); + } + } + + @Override + void createItem(Node parent) throws RepositoryException, IOException { + if (mandatesImport(parent)) { + ContentHandler ch = createContentHandler(parent); + try { + ch.startDocument(); + importItem(ch); + ch.endDocument(); + } catch (SAXException e) { + throw new DiffException(e.getMessage(), e); + } + } else { + Node n; + String uuidValue = getUUID(); + String primaryType = getPrimaryType(); + if (uuidValue == null) { + n = (primaryType == null) ? parent.addNode(name) : parent.addNode(name, primaryType); + } else { + n = importNode(parent, name, primaryType, uuidValue); + } + // create all properties + for (ImportItem obj : childP) { + obj.createItem(n); + } + // recursively create all child nodes + for (ImportItem obj : childN) { + obj.createItem(n); + } + } + } + } + + private abstract class ImportProperty extends ImportItem { + + static final String VALUE = "value"; + static final String TYPE = "type"; + static final String LOCAL_NAME = "property"; + + private ImportProperty(String parentPath, String name) throws IOException { + super(parentPath, name); + } + + @Override + boolean mandatesImport(Node parent) { + // TODO: verify again if a protected property (except for jcr:primaryType and jcr:mixinTypes) will ever change outside the scope of importing the whole tree. + return false; + } + + @Override + void importItem(ContentHandler contentHandler) throws IOException { + try { + AttributesImpl propAtts = new AttributesImpl(); + setNameAttribute(propAtts); + setTypeAttribute(propAtts); + contentHandler.startElement(Name.NS_SV_URI, LOCAL_NAME, Name.NS_SV_PREFIX+":"+LOCAL_NAME, propAtts); + startValueElement(contentHandler); + contentHandler.endElement(Name.NS_SV_URI, LOCAL_NAME, Name.NS_SV_PREFIX+":"+LOCAL_NAME); + } catch(SAXException e) { + throw new DiffException(e.getMessage(), e); + } + } + + void setTypeAttribute(AttributesImpl attr) { + String type = null; + if (name.equals(JcrConstants.JCR_PRIMARYTYPE)) { + type = PropertyType.nameFromValue(PropertyType.NAME); + } else if (name.equals(JcrConstants.JCR_MIXINTYPES)) { + type = PropertyType.nameFromValue(PropertyType.NAME); + } else if (name.equals(JcrConstants.JCR_UUID)) { + type = PropertyType.nameFromValue(PropertyType.STRING); + } else { + type = PropertyType.nameFromValue(PropertyType.UNDEFINED); + } + attr.addAttribute(Name.NS_SV_URI, TYPE, Name.NS_SV_PREFIX+":"+TYPE, TYPE_CDATA, type); + } + + abstract void startValueElement(ContentHandler contentHandler) throws IOException; + } + + private final class ImportProp extends ImportProperty { + + private final Value value; + + private ImportProp(String parentPath, String name, Value value) throws IOException { + super(parentPath, name); + try { + if (value == null) { + this.value = extractValuesFromRequest(getPath())[0]; + } else { + this.value = value; + } + } catch (RepositoryException e) { + throw new DiffException(e.getMessage(), e); + } + } + + @Override + void createItem(Node parent) throws RepositoryException { + parent.setProperty(name, value); + } + + @Override + void startValueElement(ContentHandler contentHandler) throws IOException { + try { + String str = value.getString(); + contentHandler.startElement(Name.NS_SV_URI, VALUE, Name.NS_SV_PREFIX + ":" + VALUE, new AttributesImpl()); + contentHandler.characters(str.toCharArray(), 0, str.length()); + contentHandler.endElement(Name.NS_SV_URI, VALUE, Name.NS_SV_PREFIX + ":" + VALUE); + } catch (SAXException e) { + throw new DiffException(e.getMessage()); + } catch (ValueFormatException e) { + throw new DiffException(e.getMessage()); + } catch (RepositoryException e) { + throw new DiffException(e.getMessage()); + } + } + } + + private final class ImportMvProp extends ImportProperty { + + private List values = new ArrayList(); + + private ImportMvProp(String parentPath, String name) throws IOException { + super(parentPath, name); + } + + @Override + void createItem(Node parent) throws RepositoryException { + Value[] vls = values.toArray(new Value[values.size()]); + if (JcrConstants.JCR_MIXINTYPES.equals(name)) { + setMixins(parent, vls); + } else { + parent.setProperty(name, vls); + } + } + + @Override + void startValueElement(ContentHandler contentHandler) throws IOException { + try { + // Multi-valued property with values present in the request + // multi-part + if (values.size() == 0) { + values = Arrays.asList(extractValuesFromRequest(getPath())); + } + + for (Value v : values) { + String str = v.getString(); + contentHandler.startElement(Name.NS_SV_URI, VALUE, Name.NS_SV_PREFIX + ":" + VALUE, new AttributesImpl()); + contentHandler.characters(str.toCharArray(), 0, str.length()); + contentHandler.endElement(Name.NS_SV_URI, VALUE, Name.NS_SV_PREFIX + ":" + VALUE); + } + } catch (SAXException e) { + throw new DiffException(e.getMessage()); + } catch (ValueFormatException e) { + throw new DiffException(e.getMessage()); + } catch (RepositoryException e) { + throw new DiffException(e.getMessage()); + } + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JsonWriter.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JsonWriter.java new file mode 100644 index 00000000000..127f6565200 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/JsonWriter.java @@ -0,0 +1,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.remoting.davex; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.json.JsonUtil; + +/** + * JsonWriter traverses a tree of JCR items and writes a JSON object + * exposing nodes as JSON object members and properties as JSON pairs. + *

        + * Note: Using JSON.org library is deliberately avoided for the + * following reasons. + *

          + *
        • JSONObject does not preserve the order of members added, which is required + * for JCR remoting.
        • + *
        • JSONObject#numberToString: + * Double numbers get their trailing '.0' stripped away, which removes + * the ability to distinguish between JCR values of type {@link PropertyType#DOUBLE} + * and {@link PropertyType#LONG}.
        • + *
        + */ +class JsonWriter { + + private final Writer writer; + + /** + * Create a new JsonItemWriter + * + * @param writer Writer to which the generated JSON string is written. + */ + JsonWriter(Writer writer) { + this.writer = writer; + } + + void write(Node node, int maxLevels) throws RepositoryException, IOException { + write(node, 0, maxLevels); + } + + void write(Collection nodes, int maxLevels) + throws RepositoryException, IOException { + writer.write('{'); + writeKey("nodes"); + writer.write('{'); + boolean first = true; + for (Node node : nodes) { + if (first) { + first = false; + } else { + writer.write(','); + } + writeKey(node.getPath()); + write(node, maxLevels); + } + writer.write('}'); + writer.write('}'); + } + + private void write(Node node, int currentLevel, int maxLevels) + throws RepositoryException, IOException { + // start of node info + writer.write('{'); + + // append the jcr properties as JSON pairs. + PropertyIterator props = node.getProperties(); + while (props.hasNext()) { + Property prop = props.nextProperty(); + writeProperty(prop); + // add separator: next json pair/member is either a property or + // a childnode or the special no-children-present pair. + writer.write(','); + } + + // for jcr child nodes include member unless the max-depths is reached. + // in case there are no children at all, append a special pair. + final NodeIterator children = node.getNodes(); + if (!children.hasNext()) { + // no child present at all -> add special property. + writeKeyValue("::NodeIteratorSize", 0); + } else { + // the child nodes + while (children.hasNext()) { + final Node n = children.nextNode(); + String name = n.getName(); + int index = n.getIndex(); + if (index > 1) { + writeKey(name + "[" + index + "]"); + } else { + writeKey(name); + } + if (maxLevels < 0 || currentLevel < maxLevels) { + write(n, currentLevel + 1, maxLevels); + } else { + /** + * In order to be able to compute the set of child-node entries + * upon Node creation -> add incomplete "node info" JSON + * object for the child node omitting properties and child + * information except for the jcr:uuid property (if present + * at all). + * the latter is required in order to build the correct SPI + * ChildInfo for Node n. + */ + writeChildInfo(n); + } + if (children.hasNext()) { + writer.write(','); + } + } + } + + // end of node info + writer.write('}'); + } + + /** + * Write child info without including the complete node info. + * + * @param n + * @throws RepositoryException + * @throws IOException + */ + private void writeChildInfo(Node n) throws RepositoryException, IOException { + // start child info + writer.write('{'); + + // make sure the SPI childInfo can be built correctly on the + // client side -> pass uuid if present. + if (n.isNodeType(JcrConstants.MIX_REFERENCEABLE) && + n.hasProperty(JcrConstants.JCR_UUID)) { + writeProperty(n.getProperty(JcrConstants.JCR_UUID)); + } + + // end child info + writer.write('}'); + } + + /** + * Write a single property + * + * @param p + * @throws javax.jcr.RepositoryException + * @throws java.io.IOException + */ + private void writeProperty(Property p) throws RepositoryException, IOException { + // special handling for binaries: we dump the length and not the length + int type = p.getType(); + if (type == PropertyType.BINARY) { + // mark binary properties with a leading ':' + // the value(s) reflect the jcr-values length instead of the binary data. + String key = ":" + p.getName(); + if (p.isMultiple()) { + long[] binLengths = p.getLengths(); + writeKeyArray(key, binLengths); + } else { + writeKeyValue(key, p.getLength()); + } + } else { + boolean isMultiple = p.isMultiple(); + if (requiresTypeInfo(p) || (isMultiple && p.getValues().length == 0)) { + /* special property types that have no correspondence in JSON + are transported as String. the type is transported with an + extra key-value pair, the key having a leading ':' the value + reflects the type. + the same applies for multivalued properties consisting of an + empty array -> property type guessing would not be possible. + */ + writeKeyValue(":" + p.getName(), PropertyType.nameFromValue(type), true); + } + /* append key-value pair containing the jcr value(s). + for String, Boolean, Double, Long -> types in json available */ + if (isMultiple) { + writeKeyArray(p.getName(), p.getValues()); + } else { + writeKeyValue(p.getName(), p.getValue()); + } + } + } + + private static boolean requiresTypeInfo(Property p) throws RepositoryException { + switch (p.getType()) { + case PropertyType.NAME: + case PropertyType.PATH: + case PropertyType.REFERENCE: + case PropertyType.DATE: + case PropertyType.WEAKREFERENCE: + case PropertyType.URI: + case PropertyType.DECIMAL: + case PropertyType.DOUBLE: + return true; + default: + // any other property type + return false; + } + } + + private void writeKeyValue(String key, String value, boolean hasNext) throws IOException { + writeKey(key); + writer.write(JsonUtil.getJsonString(value)); + if (hasNext) { + writer.write(','); + } + } + + private void writeKeyValue(String key, Value value) throws RepositoryException, IOException { + writeKey(key); + writeJsonValue(value); + } + + private void writeKeyArray(String key, Value[] values) throws RepositoryException, IOException { + writeKey(key); + writer.write('['); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + writer.write(','); + } + writeJsonValue(values[i]); + } + writer.write(']'); + } + + private void writeKeyValue(String key, long binLength) throws IOException { + writeKey(key); + writer.write(String.valueOf(binLength)); + } + + private void writeKeyArray(String key, long[] binLengths) throws RepositoryException, IOException { + writeKey(key); + writer.write('['); + for (int i = 0; i < binLengths.length; i++) { + if (i > 0) { + writer.write(','); + } + writer.write(String.valueOf(binLengths[i])); + } + writer.write(']'); + } + + private void writeKey(String key) throws IOException { + writer.write(JsonUtil.getJsonString(key)); + writer.write(':'); + } + + private void writeJsonValue(Value v) throws RepositoryException, IOException { + + switch (v.getType()) { + case PropertyType.BINARY: + // should never get here + throw new IllegalArgumentException(); + + case PropertyType.BOOLEAN: + case PropertyType.LONG: + writer.write(v.getString()); + break; + case PropertyType.DOUBLE: + double d = v.getDouble(); + String str = v.getString(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + str = JsonUtil.getJsonString(str); + } + writer.write(str); + break; + + default: + writer.write(JsonUtil.getJsonString(v.getString())); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedItemRemoveHandler.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedItemRemoveHandler.java new file mode 100644 index 00000000000..4b8ccf2994e --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedItemRemoveHandler.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.server.remoting.davex; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * ProtectedItemRemoveHandler... TODO + */ +public interface ProtectedItemRemoveHandler { + + public boolean remove(Session session, String itemPath) throws RepositoryException; +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedRemoveConfig.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedRemoveConfig.java new file mode 100644 index 00000000000..526b2389989 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedRemoveConfig.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.server.remoting.davex; + +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * A configuration of ProtectedItemRemoveHandler(s). + * + * ProtectedRemoveConfig + */ +class ProtectedRemoveConfig { + + private static final Logger log = LoggerFactory.getLogger(ProtectedRemoveConfig.class); + + private static final String ELEMENT_HANDLER = "protecteditemremovehandler"; + private static final String ELEMENT_CLASS = "class"; + private static final String ATTRIBUTE_NAME = "name"; + + private final ProtectedRemoveManager manager; + + ProtectedRemoveConfig(ProtectedRemoveManager manager) { + this.manager = manager; + } + + void parse(InputStream inputStream) throws IOException { + Element config; + ProtectedItemRemoveHandler instance = null; + try { + config = DomUtil.parseDocument(inputStream).getDocumentElement(); + if (config == null) { + log.warn("Missing mandatory config element"); + return; + } + ElementIterator handlers = DomUtil.getChildren(config, ELEMENT_HANDLER, null); + while (handlers.hasNext()) { + Element handler = handlers.nextElement(); + instance = createHandler(handler); + manager.addHandler(instance); + } + } catch (ParserConfigurationException e) { + log.error(e.getMessage(), e); + } catch (SAXException e) { + log.error(e.getMessage(), e); + } + } + + private ProtectedItemRemoveHandler createHandler(Element parent) { + ProtectedItemRemoveHandler instance = null; + Element classElem = DomUtil.getChildElement(parent, ELEMENT_CLASS, null); + if (classElem != null) { + String className = DomUtil.getAttribute(classElem, ATTRIBUTE_NAME, null); + if (className != null) { + instance = manager.createHandler(className); + } + } + return instance; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedRemoveManager.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedRemoveManager.java new file mode 100644 index 00000000000..f6e3f785a54 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/remoting/davex/ProtectedRemoveManager.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.server.remoting.davex; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ProtectedRemoveManager { + + private static final Logger log = LoggerFactory.getLogger(ProtectedRemoveManager.class); + + private List handlers = new ArrayList(); + + ProtectedRemoveManager(){ + + } + + ProtectedRemoveManager(String config) throws IOException { + + if (config == null) { + log.warn("protectedhandlers-config is missing -> DIFF processing can fail for the Remove operation if the content to" + + "remove is protected!"); + } else { + File file = new File(config); + if (file.exists()) { + try { + InputStream fis = new FileInputStream(file); + load(fis); + } catch (FileNotFoundException e) { + throw new IOException(e.getMessage(), e); + } + } else { // config is an Impl class + if (!config.isEmpty()) { + ProtectedItemRemoveHandler handler = createHandler(config); + addHandler(handler); + } else { + log.debug("Fail to locate the protected-item-remove-handler properties file."); + } + } + } + } + + void load(InputStream fis) throws IOException { + ProtectedRemoveConfig prConfig = new ProtectedRemoveConfig(this); + prConfig.parse(fis); + } + + boolean remove(Session session, String itemPath) throws RepositoryException { + for (ProtectedItemRemoveHandler handler : handlers) { + if (handler.remove(session, itemPath)) { + return true; + } + } + return false; + } + + /** + * Instantiates and returns a concrete ProtectedItemRemoveHandler implementation. + * @param className + * @return + * @throws RepositoryException + */ + ProtectedItemRemoveHandler createHandler(String className) { + ProtectedItemRemoveHandler irHandler = null; + try { + if (!className.isEmpty()) { + Class irHandlerClass = Class.forName(className); + if (ProtectedItemRemoveHandler.class.isAssignableFrom(irHandlerClass)) { + irHandler = (ProtectedItemRemoveHandler) irHandlerClass.newInstance(); + } + } + } catch (ClassNotFoundException e) { + log.error(e.getMessage(), e); + } catch (InstantiationException e) { + log.error(e.getMessage(), e); + } catch (IllegalAccessException e) { + log.error(e.getMessage(), e); + } + return irHandler; + } + + void addHandler(ProtectedItemRemoveHandler instance) { + if (instance != null) { + handlers.add(instance); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/HttpMultipartPost.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/HttpMultipartPost.java new file mode 100644 index 00000000000..26b1cb7c54e --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/HttpMultipartPost.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.server.util; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * HttpMultipartPost... + */ +class HttpMultipartPost { + + private static final Logger log = LoggerFactory.getLogger(HttpMultipartPost.class); + + private final Map> nameToItems = new LinkedHashMap>(); + private final Set fileParamNames = new HashSet(); + + private boolean initialized; + + HttpMultipartPost(HttpServletRequest request, File tmpDir) throws IOException { + extractMultipart(request, tmpDir); + initialized = true; + } + + private static FileItemFactory getFileItemFactory(File tmpDir) { + DiskFileItemFactory fiFactory = new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD, tmpDir); + return fiFactory; + } + + private void extractMultipart(HttpServletRequest request, File tmpDir) + throws IOException { + if (!ServletFileUpload.isMultipartContent(request)) { + log.debug("Request does not contain multipart content -> ignoring."); + return; + } + + ServletFileUpload upload = new ServletFileUpload(getFileItemFactory(tmpDir)); + // make sure the content disposition headers are read with the charset + // specified in the request content type (or UTF-8 if no charset is specified). + // see JCR + if (request.getCharacterEncoding() == null) { + upload.setHeaderEncoding("UTF-8"); + } + try { + @SuppressWarnings("unchecked") + List fileItems = upload.parseRequest(request); + for (FileItem fileItem : fileItems) { + addItem(fileItem); + } + } catch (FileUploadException e) { + log.error("Error while processing multipart.", e); + throw new IOException(e.toString()); + } + } + + /** + * Add the given file item to the list defined for its name and make the + * list is present in the map. If the item does not represent a simple + * form field its name is also added to the fileParamNames set. + * + * @param item The {@link FileItem} to add. + */ + private void addItem(FileItem item) { + String name = item.getFieldName(); + List l = nameToItems.get(item.getFieldName()); + if (l == null) { + l = new ArrayList(); + nameToItems.put(name, l); + } + l.add(item); + + // if file parameter, add name to the set of file parameters in order to + // be able to extract the file parameter values later on without iterating + // over all keys. + if (!item.isFormField()) { + fileParamNames.add(name); + } + } + + private void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("HttpMultipartPost not initialized (or already disposed)."); + } + } + + /** + * Release all file items hold with the name-to-items map. Especially those + * having a temporary file associated with. + * + * @see FileItem#delete() + */ + synchronized void dispose() { + checkInitialized(); + + for (List fileItems : nameToItems.values()) { + for (FileItem fileItem : fileItems) { + fileItem.delete(); + } + } + + nameToItems.clear(); + fileParamNames.clear(); + initialized = false; + } + + /** + * Returns an iterator over all file item names. + * + * @return a set of strings. + */ + Set getParameterNames() { + checkInitialized(); + return nameToItems.keySet(); + } + + + /** + * Returns the content types of the parameters with the given name. If + * the parameter does not exist null is returned. If the content + * type of any of the parameter values is not known, the corresponding entry + * in the array returned is null. + *

        + * The content type of a parameter is only known here if the information + * has been sent by the client browser. This is generally only the case + * for file upload fields of HTML forms which have been posted using the + * HTTP POST with multipart/form-data encoding. + *

        + * Example : For the form + *

        +         
        +
        +
        + +
        + *
        + * this method will return an array of two entries when called for the + * Upload parameter. The first entry will contain the content + * type (if transmitted by the client) of the file uploaded. The second + * entry will be null because the content type of the text + * input field will generally not be sent by the client. + * + * @param name The name of the parameter whose content type is to be + * returned. + * @return The content types of the file items with the specified name. + */ + String[] getParameterTypes(String name) { + checkInitialized(); + String[] cts = null; + List l = nameToItems.get(name); + if (l != null && !l.isEmpty()) { + cts = new String[l.size()]; + for (int i = 0; i < cts.length; i++) { + cts[i] = l.get(i).getContentType(); + } + } + return cts; + } + + /** + * Returns the first value of the file items with the given name. + * The byte to string conversion is done using either the content type of + * the file items or the formEncoding. + *

        + * Please note that if the addressed parameter is an uploaded file rather + * than a simple form entry, the name of the original file is returned + * instead of the content. + * + * @param name the name of the parameter + * @return the string of the first value or null if the + * parameter does not exist + */ + String getParameter(String name) { + checkInitialized(); + List l = nameToItems.get(name); + if (l == null || l.isEmpty()) { + return null; + } else { + FileItem item = l.get(0); + if (item.isFormField()) { + return item.getString(); + } else { + return item.getName(); + } + } + } + + /** + * Returns an array of Strings with all values of the parameter addressed + * by name. the byte to string conversion is done using either + * the content type of the multipart body or the formEncoding. + *

        + * Please note that if the addressed parameter is an uploaded file rather + * than a simple form entry, the name of the original file is returned + * instead of the content. + * + * @param name the name of the parameter + * @return a string array of values or null if no entry with the + * given name exists. + */ + String[] getParameterValues(String name) { + checkInitialized(); + List l = nameToItems.get(name); + if (l == null || l.isEmpty()) { + return null; + } else { + String[] values = new String[l.size()]; + for (int i = 0; i < values.length; i++) { + FileItem item = l.get(i); + if (item.isFormField()) { + values[i] = item.getString(); + } else { + values[i] = item.getName(); + } + } + return values; + } + } + + /** + * Returns a set of the file parameter names. An empty set if + * no file parameters were present in the request. + * + * @return an set of file item names representing the file + * parameters available with the request. + */ + Set getFileParameterNames() { + checkInitialized(); + return fileParamNames; + } + + /** + * Returns an array of input streams for uploaded file parameters. + * + * @param name the name of the file parameter(s) + * @return an array of input streams or null if no file params + * with the given name exist. + * @throws IOException if an I/O error occurs + */ + InputStream[] getFileParameterValues(String name) throws IOException { + checkInitialized(); + InputStream[] values = null; + if (fileParamNames.contains(name)) { + List l = nameToItems.get(name); + if (l != null && !l.isEmpty()) { + List ins = new ArrayList(l.size()); + for (FileItem item : l) { + if (!item.isFormField()) { + ins.add(item.getInputStream()); + } + } + values = ins.toArray(new InputStream[ins.size()]); + } + } + return values; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/RequestData.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/RequestData.java new file mode 100644 index 00000000000..6f523052864 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/server/util/RequestData.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.server.util; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Iterator; + +/** + * RequestData... + */ +public class RequestData { + + private final HttpServletRequest request; + private final HttpMultipartPost mpReq; + + public RequestData(HttpServletRequest request, File tmpDir) throws IOException { + this.request = request; + this.mpReq = new HttpMultipartPost(request, tmpDir); + } + + /** + * Dispose resources used. + */ + public void dispose() { + mpReq.dispose(); + } + + /** + * Returns an iterator over all parameter names. + * + * @return an iterator over strings. + */ + public Iterator getParameterNames() { + @SuppressWarnings("unchecked") + HashSet names = new HashSet(request.getParameterMap().keySet()); + names.addAll(mpReq.getParameterNames()); + + return names.iterator(); + } + + /** + * Returns the first value of the parameter with the given name. + * The byte to string conversion is done using either the content type of + * the parameter or the formEncoding. + *

        + * Please note that if the addressed parameter is a file parameter, the + * name of the original file is returned, and not its content. + * + * @param name the name of the parameter + * @return the string of the first value or null if the + * parameter does not exist + */ + public String getParameter(String name) { + String ret = mpReq.getParameter(name); + return (ret == null) ? request.getParameter(name) : ret; + } + + /** + * Returns the content types retrieved for parameters with the specified + * name from the multipart or null if the multipart does not + * contain parameter(s) with the given name. + * + * @param name parameter name + * @return the parameter types retrieved for the specified parameter + * name from the multipart or null. + */ + public String[] getParameterTypes(String name) { + String[] types = mpReq.getParameterTypes(name); + return types == null ? null : types; + } + + /** + * Returns an array of Strings with all values of the parameter addressed + * by name. the byte to string conversion is done using either + * the content type of the multipart body or the formEncoding. + *

        + * Please note that if the addressed parameter is a file parameter, the + * name of the original file is returned, and not its content. + * + * @param name the name of the parameter + * @return a string array of values or null if the parameter + * does not exist. + */ + public String[] getParameterValues(String name) { + String[] ret = mpReq.getParameterValues(name); + return ret == null ? request.getParameterValues(name) : ret; + } + + /** + * Returns an array of input streams for uploaded file parameters. + * + * @param name the name of the file parameter(s) + * @return an array of input streams or an empty array if no file params + * with the given name exist. + * @throws IOException if an I/O error occurs + */ + public InputStream[] getFileParameters(String name) throws IOException { + return mpReq.getFileParameterValues(name); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/AbstractItemResource.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/AbstractItemResource.java new file mode 100644 index 00000000000..cf60e83f187 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/AbstractItemResource.java @@ -0,0 +1,482 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavCompliance; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.jcr.nodetype.ItemDefinitionImpl; +import org.apache.jackrabbit.webdav.jcr.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.webdav.jcr.nodetype.PropertyDefinitionImpl; +import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet; +import org.apache.jackrabbit.webdav.observation.EventDiscovery; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.observation.ObservationResource; +import org.apache.jackrabbit.webdav.observation.Subscription; +import org.apache.jackrabbit.webdav.observation.SubscriptionInfo; +import org.apache.jackrabbit.webdav.observation.SubscriptionManager; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.transaction.TxLockEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import java.io.IOException; + +/** + * AbstractItemResource covers common functionality for the various + * resources, that represent a repository item. + */ +abstract class AbstractItemResource extends AbstractResource implements + ObservationResource, ItemResourceConstants { + + private static Logger log = LoggerFactory.getLogger(AbstractItemResource.class); + + private SubscriptionManager subsMgr; + protected final Item item; + + /** + * Create a new AbstractItemResource. + * + * @param locator + * @param session + * @param factory + * @param item + */ + AbstractItemResource(DavResourceLocator locator, JcrDavSession session, + DavResourceFactory factory, Item item) { + super(locator, session, factory); + this.item = item; + + // initialize the supported locks and reports + initLockSupport(); + initSupportedReports(); + } + + //----------------------------------------------< DavResource interface >--- + /** + * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() + */ + @Override + public String getComplianceClass() { + return DavCompliance.concatComplianceClasses( + new String[] { + super.getComplianceClass(), + DavCompliance.OBSERVATION, + } + ); + } + + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = super.getProperty(name); + if (prop == null) { + if (JCR_DEFINITION.equals(name)) { + if (exists()) { + try { + + // protected 'definition' property revealing the item definition + ItemDefinitionImpl val; + if (item.isNode()) { + val = NodeDefinitionImpl.create(((Node)item).getDefinition()); + } else { + val = PropertyDefinitionImpl.create(((Property)item).getDefinition()); + } + prop = new DefaultDavProperty(JCR_DEFINITION, val, true); + } catch (RepositoryException e) { + // should not get here + log.error("Error while accessing item definition: " + e.getMessage()); + } + } + } else if (JCR_ISNEW.equals(name)) { + // transaction resource additional protected properties + if (exists() && item.isNew()) { + prop = new DefaultDavProperty(JCR_ISNEW, null, true); + } + } else if (JCR_ISMODIFIED.equals(name)) { + // transaction resource additional protected properties + if (exists() && item.isModified()) { + prop = new DefaultDavProperty(JCR_ISMODIFIED, null, true); + } + } else if (ObservationConstants.SUBSCRIPTIONDISCOVERY.equals(name)) { + // observation resource + prop = subsMgr.getSubscriptionDiscovery(this); + } + } + + return prop; + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() + */ + @Override + public String getSupportedMethods() { + return ItemResourceConstants.METHODS; + } + + /** + * Returns true if there exists a {@link Item repository item} with the given + * resource path, false otherwise. + * + * @see org.apache.jackrabbit.webdav.DavResource#exists() + */ + @Override + public boolean exists() { + return item != null; + } + + /** + * Retrieves the last segment of the item path (or the resource path if + * this resource does not exist). An item path is in addition first translated + * to the corresponding resource path.
        + * NOTE: the display name is not equivalent to {@link Item#getName() item name} + * which is exposed with the {@link ItemResourceConstants#JCR_NAME + * {http://www.day.com/jcr/webdav/1.0}name} property. + * + * @see org.apache.jackrabbit.webdav.DavResource#getDisplayName() + */ + @Override + public String getDisplayName() { + String resPath = getResourcePath(); + return (resPath != null) ? Text.getName(resPath) : resPath; + } + + /** + * Spools the properties of this resource to the context. Note that subclasses + * are in charge of spooling the data to the output stream provided by the + * context. + * + * @see DavResource#spool(OutputContext) + */ + @Override + public void spool(OutputContext outputContext) throws IOException { + if (!initedProps) { + initProperties(); + } + // export properties + outputContext.setModificationTime(getModificationTime()); + DavProperty etag = getProperty(DavPropertyName.GETETAG); + if (etag != null) { + outputContext.setETag(String.valueOf(etag.getValue())); + } + DavProperty contentType = getProperty(DavPropertyName.GETCONTENTTYPE); + if (contentType != null) { + outputContext.setContentType(String.valueOf(contentType.getValue())); + } + DavProperty contentLength = getProperty(DavPropertyName.GETCONTENTLENGTH); + if (contentLength != null) { + try { + long length = Long.parseLong(contentLength.getValue() + ""); + if (length > 0) { + outputContext.setContentLength(length); + } + } catch (NumberFormatException e) { + log.error("Could not build content length from property value '" + contentLength.getValue() + "'"); + } + } + DavProperty contentLanguage = getProperty(DavPropertyName.GETCONTENTLANGUAGE); + if (contentLanguage != null) { + outputContext.setContentLanguage(contentLanguage.getValue().toString()); + } + } + + /** + * Returns the resource representing the parent item of the repository item + * represented by this resource. If this resoure represents the root item + * a {@link RootCollection} is returned. + * + * @return the collection this resource is internal member of. Except for the + * repository root, the returned collection always represent the parent + * repository node. + * @see org.apache.jackrabbit.webdav.DavResource#getCollection() + */ + @Override + public DavResource getCollection() { + DavResource collection = null; + + String parentPath = Text.getRelativeParent(getResourcePath(), 1); + DavResourceLocator parentLoc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), getLocator().getWorkspacePath(), parentPath); + try { + collection = createResourceFromLocator(parentLoc); + } catch (DavException e) { + log.error("Unexpected error while retrieving collection: " + e.getMessage()); + } + + return collection; + } + + /** + * Moves the underlying repository item to the indicated destination. + * + * @param destination + * @throws DavException + * @see DavResource#move(DavResource) + * @see javax.jcr.Session#move(String, String) + */ + @Override + public void move(DavResource destination) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + DavResourceLocator destLocator = destination.getLocator(); + if (!getLocator().isSameWorkspace(destLocator)) { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + try { + String itemPath = getLocator().getRepositoryPath(); + String destItemPath = destination.getLocator().getRepositoryPath(); + if (getTransactionId() == null) { + // if not part of a transaction directly import on workspace + getRepositorySession().getWorkspace().move(itemPath, destItemPath); + } else { + // changes will not be persisted unless the tx is completed. + getRepositorySession().move(itemPath, destItemPath); + } + // no use in calling 'complete' that would fail for a moved item anyway. + } catch (PathNotFoundException e) { + // according to rfc 2518 + throw new DavException(DavServletResponse.SC_CONFLICT, e.getMessage()); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Copies the underlying repository item to the indicated destination. If + * the locator of the specified destination resource indicates a different + * workspace, {@link Workspace#copy(String, String, String)} is used to perform + * the copy operation, {@link Workspace#copy(String, String)} otherwise. + *

        + * Note, that this implementation does not support shallow copy. + * + * @param destination + * @param shallow + * @throws DavException + * @see DavResource#copy(DavResource, boolean) + * @see Workspace#copy(String, String) + * @see Workspace#copy(String, String, String) + */ + @Override + public void copy(DavResource destination, boolean shallow) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + // TODO: support shallow and deep copy is required by RFC 2518 + if (shallow) { + throw new DavException(DavServletResponse.SC_FORBIDDEN, "Unable to perform shallow copy."); + } + + try { + String itemPath = getLocator().getRepositoryPath(); + String destItemPath = destination.getLocator().getRepositoryPath(); + Workspace workspace = getRepositorySession().getWorkspace(); + if (getLocator().isSameWorkspace(destination.getLocator())) { + workspace.copy(itemPath, destItemPath); + } else { + log.error("Copy between workspaces is not yet implemented (src: '" + getHref() + "', dest: '" + destination.getHref() + "')"); + throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED); + } + } catch (PathNotFoundException e) { + // according to RFC 2518, should not occur + throw new DavException(DavServletResponse.SC_NOT_FOUND, e.getMessage()); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //--------------------------------------< ObservationResource interface >--- + /** + * @see ObservationResource#init(SubscriptionManager) + */ + @Override + public void init(SubscriptionManager subsMgr) { + this.subsMgr = subsMgr; + } + + /** + * @see ObservationResource#subscribe(org.apache.jackrabbit.webdav.observation.SubscriptionInfo, String) + * @see SubscriptionManager#subscribe(org.apache.jackrabbit.webdav.observation.SubscriptionInfo, String, org.apache.jackrabbit.webdav.observation.ObservationResource) + */ + @Override + public Subscription subscribe(SubscriptionInfo info, String subscriptionId) + throws DavException { + return subsMgr.subscribe(info, subscriptionId, this); + } + + /** + * @see ObservationResource#unsubscribe(String) + * @see SubscriptionManager#unsubscribe(String, org.apache.jackrabbit.webdav.observation.ObservationResource) + */ + @Override + public void unsubscribe(String subscriptionId) throws DavException { + subsMgr.unsubscribe(subscriptionId, this); + } + + /** + * @see ObservationResource#poll(String, long) + * @see SubscriptionManager#poll(String, long, org.apache.jackrabbit.webdav.observation.ObservationResource) + */ + @Override + public EventDiscovery poll(String subscriptionId, long timeout) throws DavException { + return subsMgr.poll(subscriptionId, timeout, this); + } + + //-------------------------------------------------------------------------- + /** + * Initialize the {@link org.apache.jackrabbit.webdav.lock.SupportedLock} property + * with entries that are valid for any type item resources. + * + * @see org.apache.jackrabbit.webdav.lock.SupportedLock + * @see org.apache.jackrabbit.webdav.transaction.TxLockEntry + * @see AbstractResource#initLockSupport() + */ + @Override + protected void initLockSupport() { + if (exists()) { + // add supported lock entries for local and eventually for global + // transaction locks + supportedLock.addEntry(new TxLockEntry(true)); + supportedLock.addEntry(new TxLockEntry(false)); + } + } + + @Override + protected void initPropertyNames() { + super.initPropertyNames(); + if (exists()) { + names.addAll(JcrDavPropertyNameSet.EXISTING_ITEM_BASE_SET); + try { + if (item.getDepth() > 0) { + names.add(JCR_PARENT); + } + } catch (RepositoryException e) { + log.warn("Error while accessing node depth: " + e.getMessage()); + } + if (item.isNew()) { + names.add(JCR_ISNEW); + } else if (item.isModified()) { + names.add(JCR_ISMODIFIED); + } + } else { + names.addAll(JcrDavPropertyNameSet.ITEM_BASE_SET); + } + } + + /** + * Fill the property set for this resource. + */ + @Override + protected void initProperties() { + super.initProperties(); + if (exists()) { + try { + properties.add(new DefaultDavProperty(JCR_NAME, item.getName())); + properties.add(new DefaultDavProperty(JCR_PATH, item.getPath())); + int depth = item.getDepth(); + properties.add(new DefaultDavProperty(JCR_DEPTH, String.valueOf(depth))); + // add href-property for the items parent unless its the root item + if (depth > 0) { + String parentHref = getLocatorFromItem(item.getParent()).getHref(true); + properties.add(new HrefProperty(JCR_PARENT, parentHref, false)); + } + } catch (RepositoryException e) { + // should not get here + log.error("Error while accessing jcr properties: " + e.getMessage()); + } + } + } + + /** + * @return href of the workspace or null if this resource + * does not represent a repository item. + * + * @see AbstractResource#getWorkspaceHref() + */ + @Override + protected String getWorkspaceHref() { + String workspaceHref = null; + DavResourceLocator locator = getLocator(); + if (locator != null && locator.getWorkspacePath() != null) { + String wspPath = locator.getWorkspacePath(); + DavResourceLocator wspLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), wspPath, wspPath); + workspaceHref = wspLocator.getHref(true); + } + log.debug(workspaceHref); + return workspaceHref; + } + + /** + * If this resource exists but does not contain a transaction id, complete + * will try to persist any modifications present on the underlying + * repository item. + * + * @throws DavException if calling {@link Item#save()} fails + */ + void complete() throws DavException { + if (exists() && getTransactionId() == null) { + try { + if (item.isModified()) { + item.save(); + } + } catch (RepositoryException e) { + // this includes LockException, ConstraintViolationException etc. not detected before + log.error("Error while completing request: " + e.getMessage() +" -> reverting changes."); + try { + item.refresh(false); + } catch (RepositoryException re) { + log.error("Error while reverting changes: " + re.getMessage()); + } + throw new JcrDavException(e); + } + } + } + + /** + * Retrieves the last segment of the given path and removes the index if + * present. + * + * @param itemPath + * @return valid jcr item name + */ + protected static String getItemName(String itemPath) { + if (itemPath == null) { + throw new IllegalArgumentException("Cannot retrieve name from a 'null' item path."); + } + // retrieve the last part of the path + String name = Text.getName(itemPath); + // remove index + if (name.endsWith("]")) { + name = name.substring(0, name.lastIndexOf('[')); + } + return name; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/AbstractResource.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/AbstractResource.java new file mode 100644 index 00000000000..b6f1a4808ec --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/AbstractResource.java @@ -0,0 +1,831 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import org.apache.jackrabbit.server.io.IOUtil; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.DavCompliance; +import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet; +import org.apache.jackrabbit.webdav.util.HttpDateFormat; +import org.apache.jackrabbit.webdav.jcr.search.SearchResourceImpl; +import org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl; +import org.apache.jackrabbit.webdav.jcr.version.report.NodeTypesReport; +import org.apache.jackrabbit.webdav.jcr.version.report.LocateByUuidReport; +import org.apache.jackrabbit.webdav.jcr.version.report.RegisteredNamespacesReport; +import org.apache.jackrabbit.webdav.jcr.version.report.RepositoryDescriptorsReport; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockDiscovery; +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.SupportedLock; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.ResourceType; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.search.QueryGrammerSet; +import org.apache.jackrabbit.webdav.search.SearchInfo; +import org.apache.jackrabbit.webdav.search.SearchResource; +import org.apache.jackrabbit.webdav.transaction.TransactionConstants; +import org.apache.jackrabbit.webdav.transaction.TransactionInfo; +import org.apache.jackrabbit.webdav.transaction.TransactionResource; +import org.apache.jackrabbit.webdav.transaction.TxLockManager; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.apache.jackrabbit.webdav.version.DeltaVResource; +import org.apache.jackrabbit.webdav.version.OptionsInfo; +import org.apache.jackrabbit.webdav.version.OptionsResponse; +import org.apache.jackrabbit.webdav.version.SupportedMethodSetProperty; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.xml.parsers.ParserConfigurationException; + +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + +/** + * AbstractResource provides functionality common to all + * resources. + */ +abstract class AbstractResource implements DavResource, TransactionResource, + DeltaVResource, SearchResource { + + private static Logger log = LoggerFactory.getLogger(AbstractResource.class); + + private static final String COMPLIANCE_CLASSES = + DavCompliance.concatComplianceClasses(new String[] { + DavCompliance._1_, + DavCompliance._2_, + DavCompliance._3_, + DavCompliance.VERSION_CONTROL, + DavCompliance.VERSION_HISTORY, + DavCompliance.CHECKOUT_IN_PLACE, + DavCompliance.LABEL, + DavCompliance.MERGE, + DavCompliance.UPDATE, + DavCompliance.WORKSPACE + }); + + private final DavResourceLocator locator; + private final JcrDavSession session; + private final DavResourceFactory factory; + + private TxLockManagerImpl txMgr; + private String transactionId; + + protected boolean initedProps; + protected DavPropertySet properties = new DavPropertySet(); + protected DavPropertyNameSet names; + protected SupportedLock supportedLock = new SupportedLock(); + protected SupportedReportSetProperty supportedReports = new SupportedReportSetProperty(); + + /** + * Create a new AbstractResource + * + * @param locator + * @param session + * @param factory + */ + AbstractResource(DavResourceLocator locator, JcrDavSession session, + DavResourceFactory factory) { + if (session == null) { + throw new IllegalArgumentException("Creating AbstractItemResource: DavSession must not be null and must provide a JCR session."); + } + this.locator = locator; + this.session = session; + this.factory = factory; + } + + /** + * Returns a string listing the compliance classes for this resource as it + * is required for the DAV response header. This includes DAV 1, 2 which + * is supported by all derived classes as well as a subset of the + * classes defined by DeltaV: version-control, version-history, checkout-in-place, + * label, merge, update and workspace.
        + * Those compliance classes are added as required by RFC3253 since all + * all resources in the jcr-server support at least the reporting and some + * basic versioning functionality. + * + * @return string listing the compliance classes. + * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() + */ + @Override + public String getComplianceClass() { + return COMPLIANCE_CLASSES; + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getLocator() + */ + @Override + public DavResourceLocator getLocator() { + return locator; + } + + /** + * Returns the path of the underlying repository item or the item to + * be created (PUT/MKCOL). If the resource exists but does not represent + * a repository item null is returned. + * + * @return path of the underlying repository item. + * @see DavResource#getResourcePath() + * @see org.apache.jackrabbit.webdav.DavResourceLocator#getResourcePath() + */ + @Override + public String getResourcePath() { + return locator.getResourcePath(); + } + + /** + * @see DavResource#getHref() + * @see DavResourceLocator#getHref(boolean) + */ + @Override + public String getHref() { + return locator.getHref(isCollection()); + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getPropertyNames() + */ + @Override + public DavPropertyName[] getPropertyNames() { + initPropertyNames(); + return names.getContent().toArray(new DavPropertyName[names.getContentSize()]); + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) + */ + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = getProperties().get(name); + if (prop == null) { + if (DeltaVConstants.SUPPORTED_METHOD_SET.equals(name)) { + prop = new SupportedMethodSetProperty(getSupportedMethods().split(",\\s")); + } else if (DeltaVConstants.SUPPORTED_REPORT_SET.equals(name)) { + prop = supportedReports; + } else if (DeltaVConstants.CREATOR_DISPLAYNAME.equals(name)) { + // DAV:creator-displayname default value : not available + prop = new DefaultDavProperty(DeltaVConstants.CREATOR_DISPLAYNAME, getCreatorDisplayName(), true); + } else if (DeltaVConstants.COMMENT.equals(name)) { + // DAV:comment not value available from jcr + prop = new DefaultDavProperty(DeltaVConstants.COMMENT, null, true); + } else if (DeltaVConstants.WORKSPACE.equals(name)) { + // 'workspace' property as defined by RFC 3253 + String workspaceHref = getWorkspaceHref(); + if (workspaceHref != null) { + prop = new HrefProperty(DeltaVConstants.WORKSPACE, workspaceHref, true); + } + } + } + + // TODO: required supported-live-property-set + return prop; + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getProperties() + */ + @Override + public DavPropertySet getProperties() { + if (!initedProps) { + initProperties(); + } + return properties; + } + + /** + * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * + * @param property + * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * @see org.apache.jackrabbit.webdav.DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) + */ + @Override + public void setProperty(DavProperty property) throws DavException { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + + /** + * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * + * @param propertyName + * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * @see org.apache.jackrabbit.webdav.DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) + */ + @Override + public void removeProperty(DavPropertyName propertyName) throws DavException { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + + /** + * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * + * @see DavResource#alterProperties(List) + */ + @Override + public MultiStatusResponse alterProperties(List changeList) throws DavException { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + + /** + * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * + * @param destination + * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * @see DavResource#move(org.apache.jackrabbit.webdav.DavResource) + */ + @Override + public void move(DavResource destination) throws DavException { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + + /** + * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * + * @param destination + * @param shallow + * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + * @see DavResource#copy(org.apache.jackrabbit.webdav.DavResource, boolean) + */ + @Override + public void copy(DavResource destination, boolean shallow) throws DavException { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + + + /** + * Returns true, if the {@link SupportedLock} property contains an entry + * with the given type and scope. By default resources allow for {@link org.apache.jackrabbit.webdav.transaction.TransactionConstants#XML_TRANSACTION + * transaction} lock only. + * + * @param type + * @param scope + * @return true if this resource may be locked by the given type and scope. + * @see DavResource#isLockable(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) + */ + @Override + public boolean isLockable(Type type, Scope scope) { + return supportedLock.isSupportedLock(type, scope); + } + + /** + * Returns true if this resource has a lock applied with the given type and scope. + * + * @param type + * @param scope + * @return true if this resource has a lock applied with the given type and scope. + * @see DavResource#hasLock(Type, Scope) + */ + @Override + public boolean hasLock(Type type, Scope scope) { + return getLock(type, scope) != null; + } + + /** + * @see DavResource#getLock(Type, Scope) + */ + @Override + public ActiveLock getLock(Type type, Scope scope) { + ActiveLock lock = null; + if (TransactionConstants.TRANSACTION.equals(type)) { + lock = txMgr.getLock(type, scope, this); + } + return lock; + } + + /** + * @see DavResource#getLocks() + * todo improve.... + */ + @Override + public ActiveLock[] getLocks() { + List locks = new ArrayList(); + // tx locks + ActiveLock l = getLock(TransactionConstants.TRANSACTION, TransactionConstants.LOCAL); + if (l != null) { + locks.add(l); + } + l = getLock(TransactionConstants.TRANSACTION, TransactionConstants.GLOBAL); + if (l != null) { + locks.add(l); + } + // write lock (either exclusive or session-scoped). + l = getLock(Type.WRITE, Scope.EXCLUSIVE); + if (l != null) { + locks.add(l); + } else { + l = getLock(Type.WRITE, ItemResourceConstants.EXCLUSIVE_SESSION); + if (l != null) { + locks.add(l); + } + } + return locks.toArray(new ActiveLock[locks.size()]); + } + + /** + * @see DavResource#lock(org.apache.jackrabbit.webdav.lock.LockInfo) + */ + @Override + public ActiveLock lock(LockInfo reqLockInfo) throws DavException { + if (isLockable(reqLockInfo.getType(), reqLockInfo.getScope())) { + return txMgr.createLock(reqLockInfo, this); + } else { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + } + + /** + * Only transaction lock may be available on this resource. + * + * @param info + * @param lockToken + * @throws DavException + * @see DavResource#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String) + */ + @Override + public ActiveLock refreshLock(LockInfo info, String lockToken) throws DavException { + return txMgr.refreshLock(info, lockToken, this); + } + + /** + * Throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} since only transaction + * locks may be present on this resource, that need to be released by calling + * {@link TransactionResource#unlock(String, org.apache.jackrabbit.webdav.transaction.TransactionInfo)}. + * + * @param lockToken + * @throws DavException Always throws {@link DavServletResponse#SC_METHOD_NOT_ALLOWED} + */ + @Override + public void unlock(String lockToken) throws DavException { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + + /** + * @see DavResource#addLockManager(org.apache.jackrabbit.webdav.lock.LockManager) + */ + @Override + public void addLockManager(LockManager lockMgr) { + if (lockMgr instanceof TxLockManagerImpl) { + txMgr = (TxLockManagerImpl) lockMgr; + } + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getFactory() + */ + @Override + public DavResourceFactory getFactory() { + return factory; + } + + //-------------------------------------------------------------------------- + /** + * @see org.apache.jackrabbit.webdav.transaction.TransactionResource#getSession() + * @see org.apache.jackrabbit.webdav.observation.ObservationResource#getSession() + */ + @Override + public DavSession getSession() { + return session; + } + + //--------------------------------------< TransactionResource interface >--- + /** + * @see TransactionResource#init(TxLockManager, String) + */ + @Override + public void init(TxLockManager txMgr, String transactionId) { + this.txMgr = (TxLockManagerImpl) txMgr; + this.transactionId = transactionId; + } + + /** + * @see TransactionResource#unlock(String, org.apache.jackrabbit.webdav.transaction.TransactionInfo) + */ + @Override + public void unlock(String lockToken, TransactionInfo tInfo) throws DavException { + txMgr.releaseLock(tInfo, lockToken, this); + } + + /** + * @see TransactionResource#getTransactionId() + */ + @Override + public String getTransactionId() { + return transactionId; + } + + //-------------------------------------------< DeltaVResource interface >--- + /** + * @param optionsInfo + * @return object to be used in the OPTIONS response body or null + * @see DeltaVResource#getOptionResponse(org.apache.jackrabbit.webdav.version.OptionsInfo) + */ + @Override + public OptionsResponse getOptionResponse(OptionsInfo optionsInfo) { + OptionsResponse oR = null; + if (optionsInfo != null) { + oR = new OptionsResponse(); + // currently only DAV:version-history-collection-set and + // DAV:workspace-collection-set is supported. + if (optionsInfo.containsElement(DeltaVConstants.XML_VH_COLLECTION_SET, DeltaVConstants.NAMESPACE)) { + String[] hrefs = new String[] { + getLocatorFromItemPath(ItemResourceConstants.VERSIONSTORAGE_PATH).getHref(true) + }; + oR.addEntry(DeltaVConstants.XML_VH_COLLECTION_SET, DeltaVConstants.NAMESPACE, hrefs); + } + if (optionsInfo.containsElement(DeltaVConstants.XML_WSP_COLLECTION_SET, DeltaVConstants.NAMESPACE)) { + // workspaces cannot be created anywhere. + oR.addEntry(DeltaVConstants.XML_WSP_COLLECTION_SET, DeltaVConstants.NAMESPACE, new String[0]); + } + } + return oR; + } + + /** + * @param reportInfo + * @return the requested report + * @throws DavException + * @see DeltaVResource#getReport(org.apache.jackrabbit.webdav.version.report.ReportInfo) + */ + @Override + public Report getReport(ReportInfo reportInfo) throws DavException { + if (reportInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "A REPORT request must provide a valid XML request body."); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + if (!supportedReports.isSupportedReport(reportInfo)) { + Element condition = null; + try { + condition = DomUtil.createDocument().createElementNS("DAV:", "supported-report"); + } catch (ParserConfigurationException ex) { + // we don't care THAT much + } + throw new DavException(DavServletResponse.SC_CONFLICT, + "Unknown report '" + reportInfo.getReportName() + "' requested.", null, condition); + } + + return ReportType.getType(reportInfo).createReport(this, reportInfo); + } + + /** + * The JCR api does not provide methods to create new workspaces. Calling + * addWorkspace on this resource will always fail. + * + * @param workspace + * @throws DavException Always throws. + * @see DeltaVResource#addWorkspace(org.apache.jackrabbit.webdav.DavResource) + */ + @Override + public void addWorkspace(DavResource workspace) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Return an array of DavResource objects that are referenced + * by the property with the specified name. + * + * @param hrefPropertyName + * @return array of DavResources + * @throws DavException + * @see DeltaVResource#getReferenceResources(org.apache.jackrabbit.webdav.property.DavPropertyName) + */ + @Override + public DavResource[] getReferenceResources(DavPropertyName hrefPropertyName) throws DavException { + DavProperty prop = getProperty(hrefPropertyName); + if (prop == null || !(prop instanceof HrefProperty)) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Unknown Href-Property '" + hrefPropertyName + "' on resource " + getResourcePath()); + } + + List hrefs = ((HrefProperty)prop).getHrefs(); + DavResource[] refResources = new DavResource[hrefs.size()]; + Iterator hrefIter = hrefs.iterator(); + int i = 0; + while (hrefIter.hasNext()) { + refResources[i] = getResourceFromHref(hrefIter.next()); + i++; + } + return refResources; + } + + /** + * Retrieve the DavResource object that is represented by + * the given href String. + * + * @param href + * @return DavResource object + */ + private DavResource getResourceFromHref(String href) throws DavException { + // build a new locator: remove trailing prefix + DavResourceLocator locator = getLocator(); + String prefix = locator.getPrefix(); + DavResourceLocator loc = locator.getFactory().createResourceLocator(prefix, href); + + // create a new resource object + try { + DavResource res; + if (getRepositorySession().itemExists(loc.getRepositoryPath())) { + res = createResourceFromLocator(loc); + } else { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + return res; + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //-------------------------------------------< SearchResource interface >--- + /** + * @return + * @see org.apache.jackrabbit.webdav.search.SearchResource#getQueryGrammerSet() + */ + @Override + public QueryGrammerSet getQueryGrammerSet() { + return new SearchResourceImpl(getLocator(), session).getQueryGrammerSet(); + } + + /** + * @param sInfo + * @return + * @throws DavException + * @see SearchResource#search(org.apache.jackrabbit.webdav.search.SearchInfo) + */ + @Override + public MultiStatus search(SearchInfo sInfo) throws DavException { + return new SearchResourceImpl(getLocator(), session).search(sInfo); + } + + //-------------------------------------------------------------------------- + /** + * Property names common to all resources. + */ + protected void initPropertyNames() { + names = new DavPropertyNameSet(JcrDavPropertyNameSet.BASE_SET); + } + + /** + * Fill the set of default properties + */ + protected void initProperties() { + if (getDisplayName() != null) { + properties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, getDisplayName())); + } + if (isCollection()) { + properties.add(new ResourceType(ResourceType.COLLECTION)); + // Windows XP support + properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, "1")); + } else { + properties.add(new ResourceType(ResourceType.DEFAULT_RESOURCE)); + // Windows XP support + properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, "0")); + } + // todo: add etag + + // default last modified + String lastModified = IOUtil.getLastModified(getModificationTime()); + properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, lastModified)); + + // default creation time + properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, getCreationDate())); + + // supported lock property + properties.add(supportedLock); + + // set current lock information. If no lock is applied to this resource, + // an empty xlockdiscovery will be returned in the response. + properties.add(new LockDiscovery(getLocks())); + + // name of the jcr workspace + properties.add(new DefaultDavProperty(ItemResourceConstants.JCR_WORKSPACE_NAME, + getRepositorySession().getWorkspace().getName())); + } + + /** + * Create a new DavResource from the given locator. + * @param loc + * @return new DavResource + * @throws org.apache.jackrabbit.webdav.DavException + */ + protected DavResource createResourceFromLocator(DavResourceLocator loc) + throws DavException { + DavResource res = factory.createResource(loc, session); + if (res instanceof AbstractResource) { + ((AbstractResource)res).transactionId = this.transactionId; + } + return res; + } + + /** + * Build a DavResourceLocator from the given itemPath path. + * + * @param itemPath + * @return a new DavResourceLocator + * @see DavLocatorFactory#createResourceLocator(String, String, String) + */ + protected DavResourceLocator getLocatorFromItemPath(String itemPath) { + DavResourceLocator loc = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), itemPath, false); + return loc; + } + + /** + * Build a new {@link DavResourceLocator} from the given repository item. + * + * @param repositoryItem + * @return a new locator for the specified item. + * @see #getLocatorFromItemPath(String) + */ + protected DavResourceLocator getLocatorFromItem(Item repositoryItem) { + String itemPath = null; + try { + if (repositoryItem != null) { + itemPath = repositoryItem.getPath(); + } + } catch (RepositoryException e) { + // ignore: should not occur + log.warn(e.getMessage()); + } + return getLocatorFromItemPath(itemPath); + } + + /** + * Shortcut for getSession().getRepositorySession() + * + * @return repository session present in the {@link AbstractResource#session}. + */ + protected Session getRepositorySession() { + return session.getRepositorySession(); + } + + /** + * Define the set of locks supported by this resource. + * + * @see org.apache.jackrabbit.webdav.lock.SupportedLock + */ + abstract protected void initLockSupport(); + + /** + * Define the set of reports supported by this resource. + * + * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty + */ + /** + * Define the set of reports supported by this resource. + * + * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty + * @see AbstractResource#initSupportedReports() + */ + protected void initSupportedReports() { + if (exists()) { + supportedReports = new SupportedReportSetProperty(new ReportType[] { + ReportType.EXPAND_PROPERTY, + NodeTypesReport.NODETYPES_REPORT, + LocateByUuidReport.LOCATE_BY_UUID_REPORT, + RegisteredNamespacesReport.REGISTERED_NAMESPACES_REPORT, + RepositoryDescriptorsReport.REPOSITORY_DESCRIPTORS_REPORT + }); + } + } + + /** + * Retrieve the href of the workspace the current session belongs to. + * + * @return href of the workspace + */ + abstract protected String getWorkspaceHref(); + + /** + * Returns the display name of the creator which is used for the protected + * {@link DeltaVConstants#CREATOR_DISPLAYNAME} property. + * + * @return always null; subclasses may provide a regular value. + */ + protected String getCreatorDisplayName() { + return null; + } + + /** + * Returns the creation date which is used for the + * {@link DavPropertyName#CREATIONDATE} property. + * + * @return a dummy date; subclasses may provide a reasonable value. + */ + protected String getCreationDate() { + return HttpDateFormat.creationDateFormat().format(new Date(0)); + } + + //-------------------------------------------------------------------------- + /** + * Register the specified event listener with the observation manager present + * the repository session. + * + * @param listener + * @param nodePath + * @throws javax.jcr.RepositoryException + */ + void registerEventListener(EventListener listener, String nodePath) throws RepositoryException { + getRepositorySession().getWorkspace().getObservationManager().addEventListener(listener, EListener.ALL_EVENTS, nodePath, true, null, null, false); + } + + /** + * Unregister the specified event listener with the observation manager present + * the repository session. + * + * @param listener + * @throws javax.jcr.RepositoryException + */ + void unregisterEventListener(EventListener listener) throws RepositoryException { + getRepositorySession().getWorkspace().getObservationManager().removeEventListener(listener); + } + + //------------------------------------------------------< inner classes >--- + /** + * Simple EventListener that creates a new {@link org.apache.jackrabbit.webdav.MultiStatusResponse} object + * for each event and adds it to the specified {@link org.apache.jackrabbit.webdav.MultiStatus}. + */ + class EListener implements EventListener { + + private static final int ALL_EVENTS = Event.NODE_ADDED + | Event.NODE_REMOVED + | Event.PROPERTY_ADDED + | Event.PROPERTY_CHANGED + | Event.PROPERTY_REMOVED + | Event.NODE_MOVED + | Event.PERSIST; + + private final DavPropertyNameSet propNameSet; + private MultiStatus ms; + + EListener(DavPropertyNameSet propNameSet, MultiStatus ms) { + this.propNameSet = propNameSet; + this.ms = ms; + } + + /** + * @see EventListener#onEvent(javax.jcr.observation.EventIterator) + */ + @Override + public void onEvent(EventIterator events) { + while (events.hasNext()) { + try { + Event e = events.nextEvent(); + DavResourceLocator loc = getLocatorFromItemPath(e.getPath()); + DavResource res = createResourceFromLocator(loc); + ms.addResponse(new MultiStatusResponse(res, propNameSet)); + + } catch (DavException e) { + // should not occur + log.error("Error while building MultiStatusResponse from Event: " + e.getMessage()); + } catch (RepositoryException e) { + // should not occur + log.error("Error while building MultiStatusResponse from Event: " + e.getMessage()); + } + } + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DavLocatorFactoryImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DavLocatorFactoryImpl.java new file mode 100644 index 00000000000..391452ceaf7 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DavLocatorFactoryImpl.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.webdav.jcr; + +import org.apache.jackrabbit.webdav.AbstractLocatorFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * DavLocatorFactoryImpl... + */ +public class DavLocatorFactoryImpl extends AbstractLocatorFactory { + + private static Logger log = LoggerFactory.getLogger(DavLocatorFactoryImpl.class); + + /** + * Create a new factory + * + * @param pathPrefix Prefix, that needs to be removed in order to retrieve + * the repository path from a given href. + */ + public DavLocatorFactoryImpl(String pathPrefix) { + super(pathPrefix); + } + + //---------------------------------------------------------------------- + /** + * + * @param resourcePath + * @param wspPath + * @return + * @see AbstractLocatorFactory#getRepositoryPath(String, String) + */ + @Override + protected String getRepositoryPath(String resourcePath, String wspPath) { + if (resourcePath == null) { + return null; + } + if (resourcePath.equals(wspPath)) { + // workspace + log.debug("Resource path represents workspace path -> repository path is null."); + return null; + } else { + // a repository item -> remove wspPath + /jcr:root + String pfx = wspPath + ItemResourceConstants.ROOT_ITEM_RESOURCEPATH; + if (resourcePath.startsWith(pfx)) { + String repositoryPath = resourcePath.substring(pfx.length()); + return (repositoryPath.length() == 0) ? ItemResourceConstants.ROOT_ITEM_PATH : repositoryPath; + } else { + log.error("Unexpected format of resource path."); + throw new IllegalArgumentException("Unexpected format of resource path: " + resourcePath + " (workspace: " + wspPath + ")"); + } + } + } + + /** + * + * @param repositoryPath + * @param wspPath + * @return + * @see AbstractLocatorFactory#getResourcePath(String, String) + */ + @Override + protected String getResourcePath(String repositoryPath, String wspPath) { + if (wspPath != null) { + StringBuffer b = new StringBuffer(wspPath); + if (repositoryPath != null) { + b.append(ItemResourceConstants.ROOT_ITEM_RESOURCEPATH); + if (!ItemResourceConstants.ROOT_ITEM_PATH.equals(repositoryPath)) { + b.append(repositoryPath); + } + } + return b.toString(); + } else { + log.debug("Workspace path is 'null' -> 'null' resource path"); + return null; + } + } +} + diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DavResourceFactoryImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DavResourceFactoryImpl.java new file mode 100644 index 00000000000..79b867e182b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DavResourceFactoryImpl.java @@ -0,0 +1,251 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.observation.EventJournal; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.servlet.http.HttpServletResponse; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletRequest; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl; +import org.apache.jackrabbit.webdav.jcr.version.VersionHistoryItemCollection; +import org.apache.jackrabbit.webdav.jcr.version.VersionItemCollection; +import org.apache.jackrabbit.webdav.observation.ObservationResource; +import org.apache.jackrabbit.webdav.observation.SubscriptionManager; +import org.apache.jackrabbit.webdav.transaction.TransactionDavServletRequest; +import org.apache.jackrabbit.webdav.transaction.TransactionResource; +import org.apache.jackrabbit.webdav.version.DeltaVServletRequest; +import org.apache.jackrabbit.webdav.version.VersionControlledResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * DavResourceFactoryImpl... + */ +public class DavResourceFactoryImpl implements DavResourceFactory { + + private static Logger log = LoggerFactory.getLogger(DavResourceFactoryImpl.class); + + private final TxLockManagerImpl txMgr; + private final SubscriptionManager subsMgr; + + /** + * Create a new DavResourceFactoryImpl. + * + * @param txMgr + * @param subsMgr + */ + public DavResourceFactoryImpl(TxLockManagerImpl txMgr, SubscriptionManager subsMgr) { + this.txMgr = txMgr; + this.subsMgr = subsMgr; + } + + /** + * Create a new DavResource from the specified locator and request + * objects. Note, that in contrast to + * {@link #createResource(DavResourceLocator, DavSession)} the locator may + * point to a non-existing resource. + *

        + * If the request contains a {@link org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getLabel() + * Label header}, the resource is build from the indicated + * {@link org.apache.jackrabbit.webdav.version.VersionResource version} instead. + * + * @param locator + * @param request + * @param response + * @return + * @see DavResourceFactory#createResource(org.apache.jackrabbit.webdav.DavResourceLocator, org.apache.jackrabbit.webdav.DavServletRequest, org.apache.jackrabbit.webdav.DavServletResponse) + */ + public DavResource createResource(DavResourceLocator locator, + DavServletRequest request, + DavServletResponse response) throws DavException { + JcrDavSession.checkImplementation(request.getDavSession()); + JcrDavSession session = (JcrDavSession)request.getDavSession(); + + DavResource resource; + String type = request.getParameter("type"); + + if (locator.isRootLocation()) { + // root + resource = new RootCollection(locator, session, this); + } else if ("journal".equals(type) && locator.getResourcePath().equals(locator.getWorkspacePath())) { + // feed/event journal resource + try { + EventJournal ej = session.getRepositorySession().getWorkspace().getObservationManager() + .getEventJournal(); + if (ej == null) { + throw new DavException(HttpServletResponse.SC_NOT_IMPLEMENTED, "event journal not supported"); + } + resource = new EventJournalResourceImpl(ej, locator, session, request, this); + } catch (AccessDeniedException ex) { + // EventJournal only allowed for admin? + throw new DavException(HttpServletResponse.SC_UNAUTHORIZED, ex); + } catch (RepositoryException ex) { + throw new DavException(HttpServletResponse.SC_BAD_REQUEST, ex); + } + } else if (locator.getResourcePath().equals(locator.getWorkspacePath())) { + // workspace resource + resource = new WorkspaceResourceImpl(locator, session, this); + } else { + // resource corresponds to a repository item + try { + resource = createResourceForItem(locator, session); + + Item item = getItem(session, locator); + boolean versionable = item.isNode() && ((Node) item).isNodeType(JcrConstants.MIX_VERSIONABLE); + + /* if the created resource is version-controlled and the request + contains a Label header, the corresponding Version must be used + instead.*/ + if (request instanceof DeltaVServletRequest && versionable) { + String labelHeader = ((DeltaVServletRequest)request).getLabel(); + if (labelHeader != null && DavMethods.isMethodAffectedByLabel(request) && isVersionControlled(resource)) { + Version v = ((Node)item).getVersionHistory().getVersionByLabel(labelHeader); + DavResourceLocator vloc = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), v.getPath(), false); + resource = new VersionItemCollection(vloc, session, this, v); + } + } + } catch (PathNotFoundException e) { + /* item does not exist yet: create the default resources + Note: MKCOL request forces a collection-resource even if there already + exists a repository-property with the given path. the MKCOL will + in that particular case fail with a 405 (method not allowed).*/ + if (DavMethods.getMethodCode(request.getMethod()) == DavMethods.DAV_MKCOL) { + resource = new VersionControlledItemCollection(locator, session, this, null); + } else { + resource = new DefaultItemResource(locator, session, this, null); + } + } catch (RepositoryException e) { + log.error("Failed to build resource from item '"+ locator.getRepositoryPath() + "'"); + throw new JcrDavException(e); + } + } + + if (request instanceof TransactionDavServletRequest && resource instanceof TransactionResource) { + ((TransactionResource)resource).init(txMgr, ((TransactionDavServletRequest)request).getTransactionId()); + } + if (resource instanceof ObservationResource) { + ((ObservationResource)resource).init(subsMgr); + } + return resource; + } + + /** + * Create a new DavResource from the given locator and session. + * + * @param locator + * @param session + * @return DavResource representing either a repository item or the {@link RootCollection}. + * @throws DavException if the given locator does neither refer to a repository item + * nor does represent the {@link org.apache.jackrabbit.webdav.DavResourceLocator#isRootLocation() + * root location}. + */ + public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { + JcrDavSession.checkImplementation(session); + JcrDavSession sessionImpl = (JcrDavSession)session; + + DavResource resource; + if (locator.isRootLocation()) { + resource = new RootCollection(locator, sessionImpl, this); + } else if (locator.getResourcePath().equals(locator.getWorkspacePath())) { + resource = new WorkspaceResourceImpl(locator, sessionImpl, this); + } else { + try { + resource = createResourceForItem(locator, sessionImpl); + } catch (RepositoryException e) { + log.debug("Creating resource for non-existing repository item: " + locator.getRepositoryPath()); + // todo: is this correct? + resource = new VersionControlledItemCollection(locator, sessionImpl, this, null); + } + } + + // todo: currently transactionId is set manually after creation > to be improved. + resource.addLockManager(txMgr); + if (resource instanceof ObservationResource) { + ((ObservationResource)resource).init(subsMgr); + } + return resource; + } + + /** + * Tries to retrieve the repository item defined by the locator's resource + * path and build the corresponding WebDAV resource. The following distinction + * is made between items: Version nodes, VersionHistory nodes, root node, + * unspecified nodes and finally property items. + * + * @param locator + * @param sessionImpl + * @return DavResource representing a repository item. + * @throws RepositoryException if {@link javax.jcr.Session#getItem(String)} fails. + */ + private DavResource createResourceForItem(DavResourceLocator locator, JcrDavSession sessionImpl) throws RepositoryException, DavException { + DavResource resource; + Item item = getItem(sessionImpl, locator); + if (item.isNode()) { + // create special resources for Version and VersionHistory + if (item instanceof Version) { + resource = new VersionItemCollection(locator, sessionImpl, this, item); + } else if (item instanceof VersionHistory) { + resource = new VersionHistoryItemCollection(locator, sessionImpl, this, item); + } else{ + resource = new VersionControlledItemCollection(locator, sessionImpl, this, item); + } + } else { + resource = new DefaultItemResource(locator, sessionImpl, this, item); + } + return resource; + } + + protected Item getItem(JcrDavSession sessionImpl, DavResourceLocator locator) + throws PathNotFoundException, RepositoryException { + return sessionImpl.getRepositorySession().getItem(locator.getRepositoryPath()); + } + + /** + * Returns true, if the specified resource is a {@link VersionControlledResource} + * and has a version history. + * + * @param resource + * @return true if the specified resource is version-controlled. + */ + private boolean isVersionControlled(DavResource resource) { + boolean vc = false; + if (resource instanceof VersionControlledResource) { + try { + vc = ((VersionControlledResource)resource).getVersionHistory() != null; + } catch (DavException e) { + log.debug("Resource '" + resource.getHref() + "' is not version-controlled."); + } + } + return vc; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DefaultItemCollection.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DefaultItemCollection.java new file mode 100644 index 00000000000..b6e3efff1bb --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DefaultItemCollection.java @@ -0,0 +1,1168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +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.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.Version; +import javax.jcr.lock.Lock; +import javax.jcr.nodetype.NodeType; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.webdav.JcrValueType; +import org.apache.jackrabbit.server.io.IOUtil; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavCompliance; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet; +import org.apache.jackrabbit.webdav.jcr.security.JcrUserPrivilegesProperty; +import org.apache.jackrabbit.webdav.jcr.security.JcrSupportedPrivilegesProperty; +import org.apache.jackrabbit.webdav.jcr.security.SecurityUtils; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.util.HttpDateFormat; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.jcr.lock.JcrActiveLock; +import org.apache.jackrabbit.webdav.jcr.lock.SessionScopedLockEntry; +import org.apache.jackrabbit.webdav.jcr.nodetype.NodeTypeProperty; +import org.apache.jackrabbit.webdav.jcr.property.ValuesProperty; +import org.apache.jackrabbit.webdav.jcr.version.report.ExportViewReport; +import org.apache.jackrabbit.webdav.jcr.version.report.LocateCorrespondingNodeReport; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.ordering.OrderPatch; +import org.apache.jackrabbit.webdav.ordering.OrderingConstants; +import org.apache.jackrabbit.webdav.ordering.OrderingResource; +import org.apache.jackrabbit.webdav.ordering.OrderingType; +import org.apache.jackrabbit.webdav.ordering.Position; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * DefaultItemCollection represents a JCR node item. + */ +public class DefaultItemCollection extends AbstractItemResource + implements OrderingResource { + + private static Logger log = LoggerFactory.getLogger(DefaultItemCollection.class); + private static final String TMP_PREFIX = "_tmp_"; + + /** + * Create a new DefaultItemCollection. + * + * @param locator + * @param session + * @param factory + * @param item + */ + protected DefaultItemCollection(DavResourceLocator locator, + JcrDavSession session, + DavResourceFactory factory, Item item) { + super(locator, session, factory, item); + if (exists() && !(item instanceof Node)) { + throw new IllegalArgumentException("A collection resource can not be constructed from a Property item."); + } + } + + //----------------------------------------------< DavResource interface >--- + /** + * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() + */ + @Override + public String getComplianceClass() { + String cc = super.getComplianceClass(); + if (isOrderable()) { + return DavCompliance.concatComplianceClasses( + new String[] { + cc, + DavCompliance.ORDERED_COLLECTIONS, + } + ); + } else { + return cc; + } + } + + @Override + public long getModificationTime() { + // retrieve mod-time from jcr:lastmodified property if existing + if (exists()) { + try { + if (((Node)item).hasProperty(JcrConstants.JCR_LASTMODIFIED)) { + return ((Node)item).getProperty(JcrConstants.JCR_LASTMODIFIED).getLong(); + } + } catch (RepositoryException e) { + log.warn("Error while accessing jcr:lastModified property"); + } + } + // fallback: return 'now' + return new Date().getTime(); + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() + */ + @Override + public String getSupportedMethods() { + String ms = super.getSupportedMethods(); + if (isOrderable()) { + StringBuffer sb = new StringBuffer(ms); + sb.append(", ").append(OrderingResource.METHODS); + return sb.toString(); + } else { + return ms; + } + } + + /** + * Always returns true + * + * @return true + * @see org.apache.jackrabbit.webdav.DavResource#isCollection() + */ + @Override + public boolean isCollection() { + return true; + } + + /** + * If this resource represents an existing Node the system + * view is spooled as resource content. + * + * @param outputContext + * @throws IOException + * @see Session#exportSystemView(String, OutputStream, boolean, boolean) + */ + @Override + public void spool(OutputContext outputContext) throws IOException { + // spool properties + super.spool(outputContext); + // spool data + try { + OutputStream out = outputContext.getOutputStream(); + if (out != null && exists()) { + getRepositorySession().exportSystemView(item.getPath(), out, false, true); + } + } catch (PathNotFoundException e) { + log.error("Error while spooling resource content: " + e.getMessage()); + } catch (RepositoryException e) { + log.error("Error while spooling resource content: " + e.getMessage()); + } + } + + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = super.getProperty(name); + + if (prop == null && exists()) { + Node n = (Node) item; + + // add node-specific resource properties + try { + if (JCR_INDEX.equals(name)) { + prop = new DefaultDavProperty(JCR_INDEX, n.getIndex(), true); + } else if (JCR_REFERENCES.equals(name)) { + prop = getHrefProperty(JCR_REFERENCES, n.getReferences(), true); + } else if (JCR_WEAK_REFERENCES.equals(name)) { + prop = getHrefProperty(JCR_WEAK_REFERENCES, n.getWeakReferences(), true); + } else if (JCR_UUID.equals(name)) { + if (isReferenceable()) { + prop = new DefaultDavProperty(JCR_UUID, n.getUUID(), true); + } + } else if (JCR_PRIMARYITEM.equals(name)) { + if (hasPrimaryItem()) { + Item primaryItem = n.getPrimaryItem(); + prop = getHrefProperty(JCR_PRIMARYITEM, new Item[] {primaryItem}, true); + } + } else if (OrderingConstants.ORDERING_TYPE.equals(name) && isOrderable()) { + // property defined by RFC 3648: this resource always has custom ordering! + prop = new OrderingType(OrderingConstants.ORDERING_TYPE_CUSTOM); + } else if (SecurityConstants.SUPPORTED_PRIVILEGE_SET.equals(name)) { + prop = new JcrSupportedPrivilegesProperty(getRepositorySession(), n.getPath()).asDavProperty(); + } else if (SecurityConstants.CURRENT_USER_PRIVILEGE_SET.equals(name)) { + prop = new JcrUserPrivilegesProperty(getRepositorySession(), n.getPath()).asDavProperty(); + } + } catch (RepositoryException e) { + log.error("Failed to retrieve node-specific property: " + e); + } + } + + return prop; + } + + /** + * This implementation of the DavResource does only allow + * to set the mixinnodetypes property. Please note that the existing list of + * mixin nodetypes will be completely replaced.
        + * In order to add / set any other repository property on the underlying + * {@link javax.jcr.Node} use addMember(DavResource) or + * addMember(DavResource, InputStream) or modify the value + * of the corresponding resource. + * + * @param property + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) + * @see #JCR_MIXINNODETYPES + */ + @Override + public void setProperty(DavProperty property) throws DavException { + internalSetProperty(property); + complete(); + } + + /** + * Internal method used to set or add the given property + * + * @param property + * @throws DavException + * @see #setProperty(DavProperty) + */ + private void internalSetProperty(DavProperty property) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + DavPropertyName propName = property.getName(); + if (JCR_MIXINNODETYPES.equals(propName)) { + Node n = (Node) item; + try { + NodeTypeProperty mix = new NodeTypeProperty(property); + Set mixins = mix.getNodeTypeNames(); + + for (NodeType existingMixin : n.getMixinNodeTypes()) { + String name = existingMixin.getName(); + if (mixins.contains(name)){ + // do not add existing mixins + mixins.remove(name); + } else { + // remove mixin that are not contained in the new list + n.removeMixin(name); + } + } + + // add the remaining mixing types that are not yet set + for (String mixin : mixins) { + n.addMixin(mixin); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else if (JCR_PRIMARYNODETYPE.equals(propName)) { + Node n = (Node) item; + try { + NodeTypeProperty ntProp = new NodeTypeProperty(property); + Set names = ntProp.getNodeTypeNames(); + if (names.size() == 1) { + String ntName = names.iterator().next(); + n.setPrimaryType(ntName); + } else { + // only a single node type can be primary node type. + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else { + // all props except for mixin node types and primaryType are read-only + throw new DavException(DavServletResponse.SC_CONFLICT); + } + } + + /** + * This implementation of the DavResource does only allow + * to remove the mixinnodetypes property. + * + * @param propertyName + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) + * @see #JCR_MIXINNODETYPES + */ + @Override + public void removeProperty(DavPropertyName propertyName) throws DavException { + internalRemoveProperty(propertyName); + complete(); + } + + /** + * Internal method used to remove the property with the given name. + * + * @param propertyName + * @throws DavException + * @see #removeProperty(DavPropertyName) + */ + private void internalRemoveProperty(DavPropertyName propertyName) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (JCR_MIXINNODETYPES.equals(propertyName)) { + // remove all mixin nodetypes + try { + Node n = (Node)item; + for (NodeType mixin : n.getMixinNodeTypes()) { + n.removeMixin(mixin.getName()); + } + } catch (RepositoryException e) { + // NoSuchNodeTypeException, ConstraintViolationException should never occur... + throw new JcrDavException(e); + } + } else { + // all props except for mixin node types are read-only + throw new DavException(DavServletResponse.SC_CONFLICT); + } + } + + /** + * Loops over the given Lists and alters the properties accordingly. + * Changes are persisted at the end according to the rules defined with + * the {@link AbstractItemResource#complete()} method.

        + * Please note: since there is only a single property + * ({@link ItemResourceConstants#JCR_MIXINNODETYPES} + * that can be set or removed with PROPPATCH, this method either succeeds + * or throws an exception, even if this violates RFC 2518. Thus no property + * specific multistatus will be created in case of an error. + * + * @param changeList + * @return + * @throws DavException + */ + @Override + public MultiStatusResponse alterProperties(List changeList) throws DavException { + for (PropEntry propEntry : changeList) { + if (propEntry instanceof DavPropertyName) { + // use the internal remove method in order to prevent premature 'save' + DavPropertyName propName = (DavPropertyName) propEntry; + internalRemoveProperty(propName); + } else if (propEntry instanceof DavProperty) { + // use the internal set method in order to prevent premature 'save' + DavProperty prop = (DavProperty) propEntry; + internalSetProperty(prop); + } else { + throw new IllegalArgumentException("unknown object in change list: " + propEntry.getClass().getName()); + } + } + // TODO: missing undo of successful set/remove if subsequent operation fails + // NOTE, that this is relevant with transactions only. + + // success: save all changes together if no error occurred + complete(); + return new MultiStatusResponse(getHref(), DavServletResponse.SC_OK); + } + + /** + * If the specified resource represents a collection, a new node is {@link Node#addNode(String) + * added} to the item represented by this resource. If an input stream is specified + * together with a collection resource {@link Session#importXML(String, java.io.InputStream, int)} + * is called instead and this resource path is used as parentAbsPath argument. + *

        + * However, if the specified resource is not of resource type collection a + * new {@link Property} is set or an existing one is changed by modifying its + * value.
        + * NOTE: with the current implementation it is not possible to create or + * modify multivalue JCR properties.
        + * NOTE: if the JCR property represented by the specified resource has an + * {@link PropertyType#UNDEFINED undefined} resource type, its value will be + * changed/set to type {@link PropertyType#BINARY binary}. + * + * @param resource + * @param inputContext + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.DavResource#addMember(org.apache.jackrabbit.webdav.DavResource, InputContext) + * @see Node#addNode(String) + * @see Node#setProperty(String, java.io.InputStream) + */ + @Override + public void addMember(DavResource resource, InputContext inputContext) + throws DavException { + + /* RFC 2815 states that all 'parents' must exist in order all addition of members */ + if (!exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT); + } + + File tmpFile = null; + try { + Node n = (Node) item; + InputStream in = (inputContext != null) ? inputContext.getInputStream() : null; + String itemPath = getLocator().getRepositoryPath(); + String memberName = getItemName(resource.getLocator().getRepositoryPath()); + if (resource.isCollection()) { + if (in == null) { + // MKCOL without a request body, try if a default-primary-type is defined. + n.addNode(memberName); + } else { + // MKCOL, which is not allowed for existing resources + int uuidBehavior = ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW; + String str = inputContext.getProperty(IMPORT_UUID_BEHAVIOR); + if (str != null) { + try { + uuidBehavior = Integer.parseInt(str); + } catch (NumberFormatException e) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } + if (getTransactionId() == null) { + // if not part of a transaction directly import on workspace + // since changes would be explicitly saved in the + // complete-call. + getRepositorySession().getWorkspace().importXML(itemPath, in, uuidBehavior); + } else { + // changes will not be persisted unless the tx is completed. + getRepositorySession().importXML(itemPath, in, uuidBehavior); + } + } + } else { + if (in == null) { + // PUT: not possible without request body + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Cannot create a new non-collection resource without request body."); + } + // PUT : create new or overwrite existing property. + String ct = inputContext.getContentType(); + int type = JcrValueType.typeFromContentType(ct); + if (type != PropertyType.UNDEFINED) { + // no need to create value/values property. instead + // prop-value can be retrieved directly: + int pos = ct.indexOf(';'); + String charSet = (pos > -1) ? ct.substring(pos) : "UTF-8"; + if (type == PropertyType.BINARY) { + n.setProperty(memberName, inputContext.getInputStream()); + } else { + BufferedReader r = new BufferedReader(new InputStreamReader(inputContext.getInputStream(), charSet)); + String line; + StringBuffer value = new StringBuffer(); + while ((line = r.readLine()) != null) { + value.append(line); + } + n.setProperty(memberName, value.toString(), type); + } + } else { + // try to parse the request body into a 'values' property. + tmpFile = File.createTempFile(TMP_PREFIX + Text.escape(memberName), null, null); + FileOutputStream out = new FileOutputStream(tmpFile); + IOUtil.spool(in, out); + out.close(); + // try to parse the request body into a 'values' property. + ValuesProperty vp = buildValuesProperty(new FileInputStream(tmpFile)); + if (vp != null) { + if (JCR_VALUE.equals(vp.getName())) { + n.setProperty(memberName, vp.getJcrValue()); + } else { + n.setProperty(memberName, vp.getJcrValues()); + } + } else { + // request body cannot be parsed into a 'values' property. + // fallback: try to import as single value from stream. + n.setProperty(memberName, new FileInputStream(tmpFile)); + } + } + } + if (resource.exists() && resource instanceof AbstractItemResource) { + // PUT may modify value of existing jcr property. thus, this + // node is not modified by the 'addMember' call. + ((AbstractItemResource)resource).complete(); + } else { + complete(); + } + } catch (ItemExistsException e) { + // according to RFC 2518: MKCOL only possible on non-existing/deleted resource + throw new JcrDavException(e, DavServletResponse.SC_METHOD_NOT_ALLOWED); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } catch (IOException e) { + throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY, e.getMessage()); + } finally { + if (tmpFile != null) { + tmpFile.delete(); + } + } + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getMembers() + */ + @Override + public DavResourceIterator getMembers() { + ArrayList memberList = new ArrayList(); + if (exists()) { + try { + Node n = (Node)item; + // add all node members + NodeIterator it = n.getNodes(); + while (it.hasNext()) { + Node node = it.nextNode(); + DavResourceLocator loc = getLocatorFromItem(node); + memberList.add(createResourceFromLocator(loc)); + } + // add all property members + PropertyIterator propIt = n.getProperties(); + while (propIt.hasNext()) { + Property prop = propIt.nextProperty(); + DavResourceLocator loc = getLocatorFromItem(prop); + memberList.add(createResourceFromLocator(loc)); + } + } catch (RepositoryException e) { + // ignore + log.error(e.getMessage()); + } catch (DavException e) { + // should never occur. + log.error(e.getMessage()); + } + } + return new DavResourceIteratorImpl(memberList); + } + + /** + * Removes the repository item represented by the specified member + * resource. + * + * @throws DavException if this resource does not exist or if an error occurs + * while deleting the underlying item. + * @see DavResource#removeMember(DavResource) + * @see javax.jcr.Item#remove() + */ + @Override + public void removeMember(DavResource member) throws DavException { + Session session = getRepositorySession(); + try { + String itemPath = member.getLocator().getRepositoryPath(); + if (!exists() || !session.itemExists(itemPath)) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (!getResourcePath().equals(Text.getRelativeParent(member.getResourcePath(), 1))) { + throw new DavException(DavServletResponse.SC_CONFLICT, member.getResourcePath() + "is not member of this resource (" + getResourcePath() + ")"); + } + getRepositorySession().getItem(itemPath).remove(); + complete(); + } catch (RepositoryException e) { + log.error("Unexpected error: " + e.getMessage()); + throw new JcrDavException(e); + } + } + + /** + * @param type + * @param scope + * @return true if a lock with the specified type and scope is present on + * this resource, false otherwise. If retrieving the corresponding information + * fails, false is returned. + * @see org.apache.jackrabbit.webdav.DavResource#hasLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) + */ + @Override + public boolean hasLock(Type type, Scope scope) { + if (isLockable(type, scope)) { + if (Type.WRITE.equals(type)) { + try { + return ((Node) item).isLocked(); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } else { + return super.hasLock(type, scope); + } + } + return false; + } + + /** + * Retrieve the lock with the specified type and scope. + * + * @param type + * @param scope + * @return lock with the specified type and scope is present on this + * resource or null. NOTE: If retrieving the write lock present + * on the underlying repository item fails, null is return. + * @see org.apache.jackrabbit.webdav.DavResource#getLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) + * @see javax.jcr.Node#getLock() for the write locks. + */ + @Override + public ActiveLock getLock(Type type, Scope scope) { + ActiveLock lock = null; + if (Type.WRITE.equals(type)) { + try { + if (!exists()) { + log.warn("Unable to retrieve lock: no item found at '" + getResourcePath() + "'"); + } else if (((Node) item).isLocked()) { + Lock jcrLock = ((Node) item).getLock(); + lock = new JcrActiveLock(jcrLock); + DavResourceLocator locator = super.getLocator(); + String lockroot = locator + .getFactory() + .createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), jcrLock.getNode().getPath(), + false).getHref(false); + lock.setLockroot(lockroot); + } + } catch (AccessDeniedException e) { + log.error("Error while accessing resource lock: "+e.getMessage()); + } catch (UnsupportedRepositoryOperationException e) { + log.error("Error while accessing resource lock: "+e.getMessage()); + } catch (RepositoryException e) { + log.error("Error while accessing resource lock: "+e.getMessage()); + } + } else { + lock = super.getLock(type, scope); + } + return lock; + } + + /** + * Creates a lock on this resource by locking the underlying + * {@link javax.jcr.Node node}. Except for the {@link org.apache.jackrabbit.webdav.lock.LockInfo#isDeep()} } + * all information included in the LockInfo object is ignored. + * Lock timeout is defined by JCR implementation. + * + * @param reqLockInfo + * @return lock object representing the lock created on this resource. + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.DavResource#lock(org.apache.jackrabbit.webdav.lock.LockInfo) + * @see Node#lock(boolean, boolean) + */ + @Override + public ActiveLock lock(LockInfo reqLockInfo) throws DavException { + + if (!isLockable(reqLockInfo.getType(), reqLockInfo.getScope())) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + + if (Type.WRITE.equals(reqLockInfo.getType())) { + if (!exists()) { + log.warn("Cannot create a write lock for non-existing JCR node (" + getResourcePath() + ")"); + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + try { + boolean sessionScoped = EXCLUSIVE_SESSION.equals(reqLockInfo.getScope()); + long timeout = reqLockInfo.getTimeout(); + if (timeout == LockInfo.INFINITE_TIMEOUT) { + timeout = Long.MAX_VALUE; + } else { + timeout = timeout/1000; + } + javax.jcr.lock.LockManager lockMgr = getRepositorySession().getWorkspace().getLockManager(); + Lock jcrLock = lockMgr.lock((item).getPath(), reqLockInfo.isDeep(), + sessionScoped, timeout, reqLockInfo.getOwner()); + ActiveLock lock = new JcrActiveLock(jcrLock); + // add reference to DAVSession for this lock + getSession().addReference(lock.getToken()); + return lock; + } catch (RepositoryException e) { + // UnsupportedRepositoryOperationException should not occur... + throw new JcrDavException(e); + } + } else { + return super.lock(reqLockInfo); + } + } + + /** + * Refreshes the lock on this resource. With this implementation the + * {@link javax.jcr.lock lock} present on the underlying {@link javax.jcr.Node node} + * is refreshed. The timeout indicated by the LockInfo + * object is ignored. + * + * @param reqLockInfo LockInfo as build from the request. + * @param lockToken + * @return the updated lock info object. + * @throws org.apache.jackrabbit.webdav.DavException in case the lock could not be refreshed. + * @see org.apache.jackrabbit.webdav.DavResource#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String) + * @see javax.jcr.lock.Lock#refresh() + */ + @Override + public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) + throws DavException { + + if (lockToken == null) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + + ActiveLock lock = getLock(reqLockInfo.getType(), reqLockInfo.getScope()); + if (lock == null) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No lock with the given scope/type present on this resource."); + } + + if (Type.WRITE.equals(lock.getType())) { + try { + Lock jcrLock = ((Node) item).getLock(); + jcrLock.refresh(); + return new JcrActiveLock(jcrLock); + } catch (RepositoryException e) { + /* + NOTE: LockException is only thrown by Lock.refresh() + the lock exception thrown by Node.getLock() was circumvented + by the init test if there is a lock applied... + NOTE: UnsupportedRepositoryOperationException should not occur + */ + throw new JcrDavException(e); + } + } else { + return super.refreshLock(reqLockInfo, lockToken); + } + } + + /** + * Remove the write lock from this resource by unlocking the underlying + * {@link javax.jcr.Node node}. + * + * @param lockToken + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.DavResource#unlock(String) + * @see javax.jcr.Node#unlock() + */ + @Override + public void unlock(String lockToken) throws DavException { + ActiveLock lock = getWriteLock(); + if (lock != null && lockToken.equals(lock.getToken())) { + try { + ((Node) item).unlock(); + getSession().removeReference(lock.getToken()); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else { + super.unlock(lockToken); + } + } + + /** + * Returns the write lock present on this resource or null if + * no write lock exists. NOTE: that the scope of a write lock may either + * be {@link org.apache.jackrabbit.webdav.lock.Scope#EXCLUSIVE} or + * {@link ItemResourceConstants#EXCLUSIVE_SESSION}. + * + * @return write lock or null + * @throws DavException if this resource does not represent a repository item. + */ + private ActiveLock getWriteLock() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND, "Unable to retrieve write lock for non existing repository item (" + getResourcePath() + ")"); + } + ActiveLock writeLock = getLock(Type.WRITE, Scope.EXCLUSIVE); + if (writeLock == null) { + writeLock = getLock(Type.WRITE, EXCLUSIVE_SESSION); + } + return writeLock; + } + + //-----------------------------------------< OrderingResource interface >--- + /** + * Returns true if this resource exists and the nodetype defining the + * underlying repository node allow to reorder this nodes children. + * + * @return true if {@link DefaultItemCollection#orderMembers(OrderPatch)} + * can be called on this resource. + * @see org.apache.jackrabbit.webdav.ordering.OrderingResource#isOrderable() + * @see javax.jcr.nodetype.NodeType#hasOrderableChildNodes() + */ + @Override + public boolean isOrderable() { + boolean orderable = false; + if (exists()) { + try { + orderable = ((Node) item).getPrimaryNodeType().hasOrderableChildNodes(); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + } + return orderable; + } + + /** + * Reorder the child nodes of the repository item represented by this + * resource as indicated by the specified {@link OrderPatch} object. + * + * @param orderPatch + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.ordering.OrderingResource#orderMembers(org.apache.jackrabbit.webdav.ordering.OrderPatch) + * @see Node#orderBefore(String, String) + */ + @Override + public void orderMembers(OrderPatch orderPatch) throws DavException { + if (!isOrderable()) { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + // only custom ordering is allowed + if (!OrderingConstants.ORDERING_TYPE_CUSTOM.equalsIgnoreCase(orderPatch.getOrderingType())) { + throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY, "Only DAV:custom ordering type supported."); + } + + Node n = (Node)item; + try { + for (OrderPatch.Member instruction : orderPatch.getOrderInstructions()) { + String srcRelPath = Text.unescape(instruction.getMemberHandle()); + Position pos = instruction.getPosition(); + String destRelPath = getRelDestinationPath(pos, n.getNodes()); + // preform the reordering + n.orderBefore(srcRelPath, destRelPath); + } + complete(); + } catch (RepositoryException e) { + // UnsupportedRepositoryException should not occur + throw new JcrDavException(e); + } + } + + /** + * Retrieve the relative path of the child node that acts as destination. + * A null destination path is used to place the child node indicated + * by the source path at the end of the list. + * + * @param position + * @param childNodes + * @return the relative path of the child node used as destination or null + * if the source node should be placed at the last position. + * @throws javax.jcr.RepositoryException + */ + private String getRelDestinationPath(Position position, NodeIterator childNodes) + throws RepositoryException { + + String destRelPath = null; + if (OrderingConstants.XML_FIRST.equals(position.getType())) { + if (childNodes.hasNext()) { + Node firstChild = childNodes.nextNode(); + // use last segment of node-path instead of name. + destRelPath = Text.getName(firstChild.getPath()); + } + // no child nodes available > reordering to 'first' position fails. + if (destRelPath == null) { + throw new ItemNotFoundException("No 'first' item found for reordering."); + } + } else if (OrderingConstants.XML_AFTER.equals(position.getType())) { + String afterRelPath = position.getSegment(); + boolean found = false; + // jcr only knows order-before > retrieve the node that follows the + // one indicated by the 'afterRelPath'. + while (childNodes.hasNext() && destRelPath == null) { + // compare to last segment of node-path instead of name. + String childRelPath = Text.getName(childNodes.nextNode().getPath()); + if (found) { + destRelPath = childRelPath; + } else { + found = afterRelPath.equals(childRelPath); + } + } + } else { + // before or last. in the latter case the segment is 'null' + destRelPath = position.getSegment(); + } + if (destRelPath != null) { + destRelPath = Text.unescape(destRelPath); + } + return destRelPath; + } + + //-------------------------------------------------------------------------- + /** + * Extend the general {@link AbstractResource#supportedLock} field by + * lock entries specific for this resource: write locks (exclusive or + * exclusive session-scoped) in case the underlying node has the node + * type mix:lockable. + * + * @see org.apache.jackrabbit.JcrConstants#MIX_LOCKABLE + */ + @Override + protected void initLockSupport() { + super.initLockSupport(); + // add exclusive write lock if allowed for the given node + try { + if (exists() && ((Node)item).isNodeType(JcrConstants.MIX_LOCKABLE)) { + supportedLock.addEntry(Type.WRITE, Scope.EXCLUSIVE); + supportedLock.addEntry(new SessionScopedLockEntry()); + } + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + } + + /** + * Defines the additional reports supported by this resource (reports + * specific for resources representing a repository {@link Node node}): + *

          + *
        • {@link ExportViewReport export view report}
        • + *
        • {@link LocateCorrespondingNodeReport locate corresponding node report}
        • + *
        + * + * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty + */ + @Override + protected void initSupportedReports() { + super.initSupportedReports(); + if (exists()) { + supportedReports.addReportType(ExportViewReport.EXPORTVIEW_REPORT); + supportedReports.addReportType(LocateCorrespondingNodeReport.LOCATE_CORRESPONDING_NODE_REPORT); + } + } + + @Override + protected void initPropertyNames() { + super.initPropertyNames(); + + if (exists()) { + names.addAll(JcrDavPropertyNameSet.NODE_SET); + + if (isReferenceable()) { + names.add(JCR_UUID); + } + if (hasPrimaryItem()) { + names.add(JCR_PRIMARYITEM); + } + if (isOrderable()) { + names.add(OrderingConstants.ORDERING_TYPE); + } + if (SecurityUtils.supportsAccessControl(getRepositorySession())) { + names.add(SecurityConstants.SUPPORTED_PRIVILEGE_SET); + names.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + } + } + } + + /** + * Fill the property set for this resource. + */ + @Override + protected void initProperties() { + super.initProperties(); + if (exists()) { + // resource is serialized as system-view (xml) + properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTTYPE, "text/xml")); + Node n = (Node)item; + + // add node-specific resource properties + try { + properties.add(new NodeTypeProperty(JCR_PRIMARYNODETYPE, n.getPrimaryNodeType(), false)); + properties.add(new NodeTypeProperty(JCR_MIXINNODETYPES, n.getMixinNodeTypes(), false)); + } catch (RepositoryException e) { + log.error("Failed to retrieve node-specific property: " + e); + } + } + } + + @Override + protected String getCreatorDisplayName() { + // overwrite the default creation date and creator-displayname if possible + try { + // DAV:creator-displayname -> use jcr:createBy if present. + if (exists() && ((Node) item).hasProperty(Property.JCR_CREATED_BY)) { + return ((Node) item).getProperty(Property.JCR_CREATED_BY).getString(); + } + } catch (RepositoryException e) { + log.warn("Error while accessing jcr:createdBy property"); + } + + // fallback + return super.getCreatorDisplayName(); + } + + @Override + protected String getCreationDate() { + // overwrite the default creation date and creator-displayname if possible + try { + if (exists() && ((Node) item).hasProperty(JcrConstants.JCR_CREATED)) { + long creationTime = ((Node) item).getProperty(JcrConstants.JCR_CREATED).getValue().getLong(); + return HttpDateFormat.creationDateFormat().format(new Date(creationTime)); + } + } catch (RepositoryException e) { + log.warn("Error while accessing jcr:created property"); + } + + // fallback + return super.getCreationDate(); + } + + /** + * Creates a new HrefProperty with the specified name using the given + * array of items as value. + * + * @param name + * @param values + * @param isProtected + * @return + */ + protected HrefProperty getHrefProperty(DavPropertyName name, Item[] values, boolean isProtected) { + String[] pHref = new String[values.length]; + for (int i = 0; i < values.length; i++) { + pHref[i] = getLocatorFromItem(values[i]).getHref(true); + } + return new HrefProperty(name, pHref, isProtected); + } + + /** + * Add a {@link org.apache.jackrabbit.webdav.property.HrefProperty} with the + * specified property name and values. Each item present in the specified + * values array is referenced in the resulting property. + * + * @param name + * @param values + * @param isProtected + */ + protected void addHrefProperty(DavPropertyName name, Item[] values, boolean isProtected) { + properties.add(getHrefProperty(name, values, isProtected)); + } + + /** + * Creates a new {@link HrefProperty href property} to the property set, where + * all properties present in the specified iterator are referenced in the + * resulting property. + * + * @param name + * @param itemIterator + * @param isProtected + * @return + */ + protected HrefProperty getHrefProperty(DavPropertyName name, PropertyIterator itemIterator, + boolean isProtected) { + ArrayList l = new ArrayList(); + while (itemIterator.hasNext()) { + l.add(itemIterator.nextProperty()); + } + return getHrefProperty(name, l.toArray(new Property[l.size()]), isProtected); + } + + /** + * Add a new {@link HrefProperty href property} to the property set, where + * all properties present in the specified iterator are referenced in the + * resulting property. + * + * @param name + * @param itemIterator + * @param isProtected + * @see #addHrefProperty(DavPropertyName, Item[], boolean) + */ + protected void addHrefProperty(DavPropertyName name, PropertyIterator itemIterator, + boolean isProtected) { + properties.add(getHrefProperty(name, itemIterator, isProtected)); + } + + /** + * Add a new {@link HrefProperty href property} to the property set, where + * all versions present in the specified iterator are referenced in the + * resulting property. + * + * @param name + * @param itemIterator + * @param isProtected + */ + protected HrefProperty getHrefProperty(DavPropertyName name, VersionIterator itemIterator, + boolean isProtected) { + ArrayList l = new ArrayList(); + while (itemIterator.hasNext()) { + l.add(itemIterator.nextVersion()); + } + return getHrefProperty(name, l.toArray(new Version[l.size()]), isProtected); + } + + /** + * Add a new {@link HrefProperty href property} to the property set, where + * all versions present in the specified iterator are referenced in the + * resulting property. + * + * @param name + * @param itemIterator + * @param isProtected + */ + protected void addHrefProperty(DavPropertyName name, VersionIterator itemIterator, + boolean isProtected) { + properties.add(getHrefProperty(name, itemIterator, isProtected)); + } + + /** + * Tries to parse the given input stream as xml document and build a + * {@link ValuesProperty} out of it. + * + * @param in + * @return values property or 'null' if the given stream cannot be parsed + * into an XML document or if build the property fails. + */ + private ValuesProperty buildValuesProperty(InputStream in) { + String errorMsg = "Cannot parse stream into a 'ValuesProperty'."; + try { + Document reqBody = DomUtil.parseDocument(in); + DavProperty defaultProp = DefaultDavProperty.createFromXml(reqBody.getDocumentElement()); + ValuesProperty vp = new ValuesProperty(defaultProp, PropertyType.STRING, getRepositorySession().getValueFactory()); + return vp; + } catch (IOException e) { + log.debug(errorMsg, e); + } catch (ParserConfigurationException e) { + log.debug(errorMsg, e); + } catch (SAXException e) { + log.debug(errorMsg, e); + } catch (DavException e) { + log.debug(errorMsg, e); + } catch (RepositoryException e) { + log.debug(errorMsg, e); + } + // cannot parse request body into a 'values' property + return null; + } + + private boolean hasPrimaryItem() { + try { + return exists() && ((Node) item).getPrimaryNodeType().getPrimaryItemName() != null; + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + return false; + } + + private boolean isReferenceable() { + try { + return exists() && ((Node) item).isNodeType(JcrConstants.MIX_REFERENCEABLE); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + return false; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DefaultItemResource.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DefaultItemResource.java new file mode 100644 index 00000000000..763b9331e67 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/DefaultItemResource.java @@ -0,0 +1,421 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import org.apache.jackrabbit.commons.webdav.JcrValueType; +import org.apache.jackrabbit.commons.xml.SerializingContentHandler; +import org.apache.jackrabbit.server.io.IOUtil; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet; +import org.apache.jackrabbit.webdav.jcr.property.LengthsProperty; +import org.apache.jackrabbit.webdav.jcr.property.ValuesProperty; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import javax.jcr.Binary; +import javax.jcr.Item; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXResult; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * DefaultItemResource represents JCR property item. + * + * @see Property + */ +public class DefaultItemResource extends AbstractItemResource { + + private static Logger log = LoggerFactory.getLogger(DefaultItemResource.class); + + /** + * Create a new DefaultItemResource. + * + * @param locator + * @param session + */ + public DefaultItemResource(DavResourceLocator locator, JcrDavSession session, + DavResourceFactory factory, Item item) { + super(locator, session, factory, item); + } + + //----------------------------------------------< DavResource interface >--- + /** + * Returns false. + * + * @return false + * @see DavResource#isCollection() + */ + @Override + public boolean isCollection() { + return false; + } + + /** + * Always returns 'now' + * + * @return + * @see DavResource#getModificationTime() + */ + @Override + public long getModificationTime() { + return new Date().getTime(); + } + + /** + * In case an underlying repository {@link Property property} exists the following + * logic is applied to spool the property content: + *
          + *
        • Property is not multi valued: Return the {@link javax.jcr.Value#getStream() + * stream representation} of the property value.
        • + *
        • Property is multivalue: Return the xml representation of the values.
        • + *
        + * + * @param outputContext + * @see DavResource#spool(OutputContext) + */ + @Override + public void spool(OutputContext outputContext) throws IOException { + // write properties + super.spool(outputContext); + // spool content + OutputStream out = outputContext.getOutputStream(); + if (out != null && exists()) { + if (isMultiple()) { + spoolMultiValued(out); + } else { + spoolSingleValued(out); + } + } + } + + private void spoolMultiValued(OutputStream out) { + try { + Document doc = DomUtil.createDocument(); + doc.appendChild(getProperty(JCR_VALUES).toXml(doc)); + + ContentHandler handler = + SerializingContentHandler.getSerializer(out); + + Transformer transformer = + TransformerFactory.newInstance().newTransformer(); + transformer.transform( + new DOMSource(doc), new SAXResult(handler)); + } catch (SAXException e) { + log.error("Failed to set up XML serializer for " + item, e); + } catch (TransformerConfigurationException e) { + log.error("Failed to set up XML transformer for " + item, e); + } catch (ParserConfigurationException e) { + log.error("Failed to set up XML document for " + item, e); + } catch (TransformerException e) { + log.error("Failed to serialize the values of " + item, e); + } + } + + private void spoolSingleValued(OutputStream out) throws IOException { + try { + Binary binary = ((Property) item).getBinary(); + try { + InputStream in = binary.getStream(); + try { + IOUtil.spool(in, out); + } finally { + in.close(); + } + } finally { + binary.dispose(); + } + } catch (RepositoryException e) { + log.error("Cannot obtain stream from " + item, e); + } + } + + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = super.getProperty(name); + + if (prop == null && exists()) { + try { + Property p = (Property) item; + if (isMultiple()) { + if (JCR_LENGTHS.equals(name)) { + prop = new LengthsProperty(p.getLengths()); + } + } else { + if (JCR_LENGTH.equals(name)) { + long length = p.getLength(); + prop = new DefaultDavProperty(JCR_LENGTH, String.valueOf(length), true); + } else if (JCR_GET_STRING.equals(name) && p.getType() != PropertyType.BINARY) { + // getstring property is only created for single value + // non-binary jcr properties + prop = new DefaultDavProperty(JCR_GET_STRING, p.getString(), true); + } + } + } catch (RepositoryException e) { + log.error("Failed to retrieve resource properties: "+e.getMessage()); + } + } + + return prop; + } + + /** + * Sets the given property. Note, that {@link #JCR_VALUE} and {@link #JCR_VALUES} + * are the only resource properties that are allowed to be modified. Any other + * property is read-only and will throw an exception ('Conflict'). + * + * @param property + * @throws DavException + * @see DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) + */ + @Override + public void setProperty(DavProperty property) throws DavException { + internalSetProperty(property); + complete(); + } + + /** + * Internal method that performs the setting or adding of properties + * + * @param property + * @throws DavException + * @see #setProperty(DavProperty) + * @see #alterProperties(List) + */ + private void internalSetProperty(DavProperty property) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + try { + Property prop = (Property) item; + int defaultType = prop.getType(); + ValueFactory vfact = getRepositorySession().getValueFactory(); + ValuesProperty vp = new ValuesProperty(property, defaultType, vfact); + if (property.getName().equals(JCR_VALUE)) { + prop.setValue(vp.getJcrValue(vp.getValueType(), vfact)); + } else if (property.getName().equals(JCR_VALUES)) { + prop.setValue(vp.getJcrValues()); + } else { + throw new DavException(DavServletResponse.SC_CONFLICT); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Removing properties is not allowed, for a single-value JCR-property without + * a value does not exist. For multivalue properties an empty {@link Value values array} + * may be specified with by setting the {@link #JCR_VALUES 'values' webdav property}. + * + * @param propertyName + * @throws DavException + * @see org.apache.jackrabbit.webdav.DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) + */ + @Override + public void removeProperty(DavPropertyName propertyName) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Loops over the given List and alters the properties accordingly. + * Changes are persisted at the end only according to the rules defined with + * the {@link #complete()} method.

        + * Please note: since there is only a single property than can be set + * from a client (i.e. jcr:value OR jcr:values) this method either succeeds + * or throws an exception, even if this violates RFC 2518. + * + * @param changeList + * @throws DavException + * @see DavResource#alterProperties(List) + */ + @Override + public MultiStatusResponse alterProperties(List changeList) throws DavException { + for (PropEntry propEntry : changeList) { + if (propEntry instanceof DavPropertyName) { + // altering any properties fails if an attempt is made to remove + // a property + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } else if (propEntry instanceof DavProperty) { + DavProperty prop = (DavProperty) propEntry; + internalSetProperty(prop); + } else { + throw new IllegalArgumentException("unknown object in change list: " + propEntry.getClass().getName()); + } + } + complete(); + return new MultiStatusResponse(getHref(), DavServletResponse.SC_OK); + } + + /** + * Method is not allowed. + * + * @see org.apache.jackrabbit.webdav.DavResource#addMember(org.apache.jackrabbit.webdav.DavResource, InputContext) + */ + @Override + public void addMember(DavResource resource, InputContext inputContext) throws DavException { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED, "Cannot add members to a non-collection resource"); + } + + /** + * Always returns an empty iterator for a non-collection resource might + * not have internal members. + * + * @return an empty iterator + * @see DavResource#getMembers() + */ + @Override + public DavResourceIterator getMembers() { + log.warn("A non-collection resource never has internal members."); + List drl = Collections.emptyList(); + return new DavResourceIteratorImpl(drl); + } + + /** + * Method is not allowed. + * + * @see DavResource#removeMember(DavResource) + */ + @Override + public void removeMember(DavResource member) throws DavException { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED, "Cannot remove members from a non-collection resource"); + } + + /** + * {@link javax.jcr.Property JCR properties} are locked if their + * parent node is locked; thus this method will always return the + * {@link ActiveLock lock} object from the collection this resource is + * internal member of. + * + * @param type + * @param scope + * @return lock present on this resource or null if this resource + * has no lock. + * @see DavResource#getLock(Type, Scope) + */ + @Override + public ActiveLock getLock(Type type, Scope scope) { + if (Type.WRITE.equals(type)) { + return getCollection().getLock(type, scope); + } else { + return super.getLock(type, scope); + } + } + + //-------------------------------------------------------------------------- + @Override + protected void initPropertyNames() { + super.initPropertyNames(); + if (exists()) { + DavPropertyNameSet propNames = (isMultiple() ? + JcrDavPropertyNameSet.PROPERTY_MV_SET : + JcrDavPropertyNameSet.PROPERTY_SET); + names.addAll(propNames); + } + } + + /** + * Add resource specific properties. + */ + @Override + protected void initProperties() { + super.initProperties(); + if (exists()) { + try { + Property prop = (Property)item; + int type = prop.getType(); + + // set the content type + String contentType; + if (isMultiple()) { + contentType = IOUtil.buildContentType("text/xml","utf-8"); + } else { + contentType = IOUtil.buildContentType(JcrValueType.contentTypeFromType(type), "utf-8"); + + } + properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTTYPE, contentType)); + + // add jcr-specific resource properties + properties.add(new DefaultDavProperty(JCR_TYPE, PropertyType.nameFromValue(type))); + if (isMultiple()) { + properties.add(new ValuesProperty(prop.getValues())); + } else { + properties.add(new ValuesProperty(prop.getValue())); + } + } catch (RepositoryException e) { + log.error("Failed to retrieve resource properties: "+e.getMessage()); + } + } + } + + /** + * Returns true if the JCR Property represented by this resource is a multi + * value property. Note: if this resource does not exist or if the definition + * could not be retrieved false is returned. + * + * @return true if the underlying resource is a multi value property. + */ + private boolean isMultiple() { + try { + if (exists() && ((Property)item).isMultiple()) { + return true; + } + } catch (RepositoryException e) { + log.error("Error while retrieving property definition: " + e.getMessage()); + } + return false; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/EventJournalResourceImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/EventJournalResourceImpl.java new file mode 100644 index 00000000000..868ee32f319 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/EventJournalResourceImpl.java @@ -0,0 +1,483 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventJournal; +import javax.servlet.http.HttpServletRequest; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; + +import org.apache.jackrabbit.commons.webdav.AtomFeedConstants; +import org.apache.jackrabbit.commons.webdav.EventUtil; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.AdditionalEventInfo; +import org.apache.jackrabbit.util.ISO8601; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * Implements a JCR {@link EventJournal} in terms of an RFC 4287 Atom feed. + *

        + * Each feed entry represents either a single event, or, if the repository + * supports the {@link Event#PERSIST} event, an event bundle. The actual event + * data is sent in the Atom <content> element and uses the same XML + * serialization as the one used for subscriptions. + *

        + * Skipping is implemented by specifying the desired time offset (represented + * as hexadecimal long in ms since the epoch) disguised as ETag in the HTTP "If-None-Match" + * header field. + *

        + * The generated feed may not be complete; the total number of events is limited in + * order not to overload the client. + *

        + * Furthermore, the number of events is limited by going up to 2000 ms into the future + * (based on the request time). This is supposed to limit the wait time for the client). + */ +public class EventJournalResourceImpl extends AbstractResource { + + public static final String RELURIFROMWORKSPACE = "?type=journal"; + + public static final String EVENTMEDIATYPE = "application/vnd.apache.jackrabbit.event+xml"; + + private static Logger log = LoggerFactory.getLogger(EventJournalResourceImpl.class); + + private final HttpServletRequest request; + private final EventJournal journal; + private final DavResourceLocator locator; + + EventJournalResourceImpl(EventJournal journal, DavResourceLocator locator, JcrDavSession session, + HttpServletRequest request, DavResourceFactory factory) { + super(locator, session, factory); + this.journal = journal; + this.locator = locator; + this.request = request; + } + + @Override + public String getSupportedMethods() { + return "GET, HEAD"; + } + + @Override + public boolean exists() { + try { + List available = Arrays.asList(getRepositorySession().getWorkspace().getAccessibleWorkspaceNames()); + return available.contains(getLocator().getWorkspaceName()); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + return false; + } + } + + @Override + public boolean isCollection() { + return false; + } + + @Override + public String getDisplayName() { + return "event journal for " + getLocator().getWorkspaceName(); + } + + @Override + public long getModificationTime() { + return System.currentTimeMillis(); + } + + private static final String ATOMNS = AtomFeedConstants.NS_URI; + private static final String EVNS = ObservationConstants.NAMESPACE.getURI(); + + private static final String AUTHOR = AtomFeedConstants.XML_AUTHOR; + private static final String CONTENT = AtomFeedConstants.XML_CONTENT; + private static final String ENTRY = AtomFeedConstants.XML_ENTRY; + private static final String FEED = AtomFeedConstants.XML_FEED; + private static final String ID = AtomFeedConstants.XML_ID; + private static final String LINK = AtomFeedConstants.XML_LINK; + private static final String NAME = AtomFeedConstants.XML_NAME; + private static final String TITLE = AtomFeedConstants.XML_TITLE; + private static final String UPDATED = AtomFeedConstants.XML_UPDATED; + + private static final String E_EVENT = ObservationConstants.XML_EVENT; + private static final String E_EVENTDATE = ObservationConstants.XML_EVENTDATE; + private static final String E_EVENTIDENTIFIER = ObservationConstants.XML_EVENTIDENTIFIER; + private static final String E_EVENTINFO = ObservationConstants.XML_EVENTINFO; + private static final String E_EVENTTYPE = ObservationConstants.XML_EVENTTYPE; + private static final String E_EVENTMIXINNODETYPE = ObservationConstants.XML_EVENTMIXINNODETYPE; + private static final String E_EVENTPRIMARNODETYPE = ObservationConstants.XML_EVENTPRIMARNODETYPE; + private static final String E_EVENTUSERDATA = ObservationConstants.XML_EVENTUSERDATA; + + private static final int MAXWAIT = 2000; // maximal wait time + private static final int MAXEV = 10000; // maximal event number + + private static final Attributes NOATTRS = new AttributesImpl(); + + @Override + public void spool(OutputContext outputContext) throws IOException { + + Calendar cal = Calendar.getInstance(Locale.ENGLISH); + + try { + outputContext.setContentType("application/atom+xml; charset=UTF-8"); + outputContext.setProperty("Vary", "If-None-Match"); + // TODO: Content-Encoding: gzip + + // find out where to start + long prevts = -1; + String inm = request.getHeader("If-None-Match"); + if (inm != null) { + // TODO: proper parsing when comma-delimited + inm = inm.trim(); + if (inm.startsWith("\"") && inm.endsWith("\"")) { + String tmp = inm.substring(1, inm.length() - 1); + try { + prevts = Long.parseLong(tmp, 16); + journal.skipTo(prevts); + } catch (NumberFormatException ex) { + // broken etag + } + } + } + + boolean hasPersistEvents = false; + + if (outputContext.hasStream()) { + + long lastts = -1; + long now = System.currentTimeMillis(); + boolean done = false; + + // collect events + List events = new ArrayList(MAXEV); + + while (!done && journal.hasNext()) { + Event e = journal.nextEvent(); + + hasPersistEvents |= e.getType() == Event.PERSIST; + + if (e.getDate() != lastts) { + // consider stopping + if (events.size() > MAXEV) { + done = true; + } + if (e.getDate() > now + MAXWAIT) { + done = true; + } + } + + if (!done && (prevts == -1 || e.getDate() >= prevts)) { + events.add(e); + } + + lastts = e.getDate(); + } + + if (lastts >= 0) { + // construct ETag from newest event + outputContext.setETag("\"" + Long.toHexString(lastts) + "\""); + } + + OutputStream os = outputContext.getOutputStream(); + StreamResult streamResult = new StreamResult(os); + SAXTransformerFactory tf = (SAXTransformerFactory) TransformerFactory.newInstance(); + TransformerHandler th = tf.newTransformerHandler(); + Transformer s = th.getTransformer(); + s.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + s.setOutputProperty(OutputKeys.INDENT, "yes"); + s.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + + th.setResult(streamResult); + + th.startDocument(); + + th.startElement(ATOMNS, FEED, FEED, NOATTRS); + + writeAtomElement(th, TITLE, "EventJournal for " + getLocator().getWorkspaceName()); + + th.startElement(ATOMNS, AUTHOR, AUTHOR, NOATTRS); + writeAtomElement(th, NAME, "Jackrabbit Event Journal Feed Generator"); + th.endElement(ATOMNS, AUTHOR, AUTHOR); + + String id = getFullUri(request); + writeAtomElement(th, ID, id); + + AttributesImpl linkattrs = new AttributesImpl(); + linkattrs.addAttribute(null, "self", "self", "CDATA", id); + writeAtomElement(th, LINK, linkattrs, null); + + cal.setTimeInMillis(lastts >= 0 ? lastts : now); + String upd = ISO8601.format(cal); + writeAtomElement(th, UPDATED, upd); + + String lastDateString = ""; + long lastTimeStamp = 0; + long index = 0; + + AttributesImpl contentatt = new AttributesImpl(); + contentatt.addAttribute(null, "type", "type", "CDATA", EVENTMEDIATYPE); + + while (!events.isEmpty()) { + + List bundle = null; + String path = null; + String op; + + if (hasPersistEvents) { + bundle = new ArrayList(); + Event e = null; + op = "operations"; + + do { + e = events.remove(0); + bundle.add(e); + + // compute common path + if (path == null) { + path = e.getPath(); + } else { + if (e.getPath() != null && e.getPath().length() < path.length()) { + path = e.getPath(); + } + } + } while (e.getType() != Event.PERSIST && !events.isEmpty()); + } else { + // no persist events + Event e = events.remove(0); + bundle = Collections.singletonList(e); + path = e.getPath(); + op = EventUtil.getEventName(e.getType()); + } + + Event firstEvent = bundle.get(0); + + String entryupd = lastDateString; + if (lastTimeStamp != firstEvent.getDate()) { + cal.setTimeInMillis(firstEvent.getDate()); + entryupd = ISO8601.format(cal); + index = 0; + } else { + index += 1; + } + + th.startElement(ATOMNS, ENTRY, ENTRY, NOATTRS); + + String entrytitle = op + (path != null ? (": " + path) : ""); + writeAtomElement(th, TITLE, entrytitle); + + String entryid = id + "?type=journal&ts=" + Long.toHexString(firstEvent.getDate()) + "-" + index; + writeAtomElement(th, ID, entryid); + + String author = firstEvent.getUserID() == null || firstEvent.getUserID().length() == 0 ? null + : firstEvent.getUserID(); + if (author != null) { + th.startElement(ATOMNS, AUTHOR, AUTHOR, NOATTRS); + writeAtomElement(th, NAME, author); + th.endElement(ATOMNS, AUTHOR, AUTHOR); + } + + writeAtomElement(th, UPDATED, entryupd); + + th.startElement(ATOMNS, CONTENT, CONTENT, contentatt); + + for (Event e : bundle) { + + // serialize the event + th.startElement(EVNS, E_EVENT, E_EVENT, NOATTRS); + + // DAV:href + if (e.getPath() != null) { + boolean isCollection = (e.getType() == Event.NODE_ADDED || e.getType() == Event.NODE_REMOVED); + String href = locator + .getFactory() + .createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), + e.getPath(), false).getHref(isCollection); + th.startElement(DavConstants.NAMESPACE.getURI(), DavConstants.XML_HREF, + DavConstants.XML_HREF, NOATTRS); + th.characters(href.toCharArray(), 0, href.length()); + th.endElement(DavConstants.NAMESPACE.getURI(), DavConstants.XML_HREF, DavConstants.XML_HREF); + } + + // event type + String evname = EventUtil.getEventName(e.getType()); + th.startElement(EVNS, E_EVENTTYPE, E_EVENTTYPE, NOATTRS); + th.startElement(EVNS, evname, evname, NOATTRS); + th.endElement(EVNS, evname, evname); + th.endElement(EVNS, E_EVENTTYPE, E_EVENTTYPE); + + // date + writeObsElement(th, E_EVENTDATE, Long.toString(e.getDate())); + + // user data + if (e.getUserData() != null && e.getUserData().length() > 0) { + writeObsElement(th, E_EVENTUSERDATA, firstEvent.getUserData()); + } + + // user id: already sent as Atom author/name + + // try to compute nodetype information + if (e instanceof AdditionalEventInfo) { + try { + Name pnt = ((AdditionalEventInfo) e).getPrimaryNodeTypeName(); + if (pnt != null) { + writeObsElement(th, E_EVENTPRIMARNODETYPE, pnt.toString()); + } + + Set mixins = ((AdditionalEventInfo) e).getMixinTypeNames(); + if (mixins != null) { + for (Name mixin : mixins) { + writeObsElement(th, E_EVENTMIXINNODETYPE, mixin.toString()); + } + } + + } catch (UnsupportedRepositoryOperationException ex) { + // optional + } + } + + // identifier + if (e.getIdentifier() != null) { + writeObsElement(th, E_EVENTIDENTIFIER, e.getIdentifier()); + } + + // info + if (!e.getInfo().isEmpty()) { + th.startElement(EVNS, E_EVENTINFO, E_EVENTINFO, NOATTRS); + Map m = e.getInfo(); + for (Map.Entry entry : m.entrySet()) { + String key = entry.getKey().toString(); + Object value = entry.getValue(); + String t = value != null ? value.toString() : null; + writeElement(th, null, key, NOATTRS, t); + } + th.endElement(EVNS, E_EVENTINFO, E_EVENTINFO); + } + + th.endElement(EVNS, E_EVENT, E_EVENT); + + lastTimeStamp = e.getDate(); + lastDateString = entryupd; + } + + th.endElement(ATOMNS, CONTENT, CONTENT); + th.endElement(ATOMNS, ENTRY, ENTRY); + } + + th.endElement(ATOMNS, FEED, FEED); + + th.endDocument(); + + os.flush(); + } + } catch (Exception ex) { + throw new IOException("error generating feed: " + ex.getMessage()); + } + } + + @Override + public DavResource getCollection() { + return null; + } + + @Override + public void addMember(DavResource resource, InputContext inputContext) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + @Override + public DavResourceIterator getMembers() { + return DavResourceIteratorImpl.EMPTY; + } + + @Override + public void removeMember(DavResource member) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + @Override + protected void initLockSupport() { + // lock not allowed + } + + @Override + protected String getWorkspaceHref() { + return getHref(); + } + + private void writeElement(TransformerHandler th, String ns, String name, Attributes attrs, String textContent) + throws SAXException { + th.startElement(ns, name, name, attrs); + if (textContent != null) { + th.characters(textContent.toCharArray(), 0, textContent.length()); + } + th.endElement(ns, name, name); + } + + private void writeAtomElement(TransformerHandler th, String name, Attributes attrs, String textContent) + throws SAXException { + writeElement(th, ATOMNS, name, attrs, textContent); + } + + private void writeAtomElement(TransformerHandler th, String name, String textContent) throws SAXException { + writeAtomElement(th, name, NOATTRS, textContent); + } + + private void writeObsElement(TransformerHandler th, String name, String textContent) throws SAXException { + writeElement(th, EVNS, name, NOATTRS, textContent); + } + + private String getFullUri(HttpServletRequest req) { + + String scheme = req.getScheme(); + int port = req.getServerPort(); + boolean isDefaultPort = (scheme.equals("http") && port == 80) || (scheme.equals("http") && port == 443); + String query = request.getQueryString() != null ? "?" + request.getQueryString() : ""; + + return String.format("%s://%s%s%s%s%s", scheme, req.getServerName(), isDefaultPort ? ":" : "", + isDefaultPort ? Integer.toString(port) : "", req.getRequestURI(), query); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/ItemResourceConstants.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/ItemResourceConstants.java new file mode 100644 index 00000000000..eda1c972072 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/ItemResourceConstants.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.webdav.jcr; + +import javax.jcr.Session; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.observation.ObservationResource; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.search.SearchResource; +import org.apache.jackrabbit.webdav.security.Privilege; +import org.apache.jackrabbit.webdav.version.DeltaVResource; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * ItemResourceConstants provides constants for any resources + * representing repository items. + */ +public interface ItemResourceConstants extends JcrRemotingConstants { + + /** + * Methods common to all item resources. + */ + public static final String METHODS = DavResource.METHODS + ", " + ObservationResource.METHODS + ", " + SearchResource.METHODS + ", " +DeltaVResource.METHODS; + + /** + * The namespace for all jcr specific extensions. + */ + public static final Namespace NAMESPACE = Namespace.getNamespace(NS_PREFIX, NS_URI); + + /** + * Extension to the WebDAV 'exclusive' lock, that allows to distinguish + * the session-scoped and open-scoped locks on a JCR node. + * + * @see javax.jcr.Node#lock(boolean, boolean) + */ + public static final Scope EXCLUSIVE_SESSION = Scope.create(XML_EXCLUSIVE_SESSION_SCOPED, NAMESPACE); + + // name of the workspace for which the repository session has been created. + public static final DavPropertyName JCR_WORKSPACE_NAME = DavPropertyName.create(JCR_WORKSPACE_NAME_LN, NAMESPACE); + + // general property names + public static final DavPropertyName JCR_NAME = DavPropertyName.create(JCR_NAME_LN, NAMESPACE); + public static final DavPropertyName JCR_PATH = DavPropertyName.create(JCR_PATH_LN, NAMESPACE); + public static final DavPropertyName JCR_DEPTH = DavPropertyName.create(JCR_DEPTH_LN, NAMESPACE); + public static final DavPropertyName JCR_PARENT = DavPropertyName.create(JCR_PARENT_LN, NAMESPACE); + public static final DavPropertyName JCR_ISNEW = DavPropertyName.create(JCR_ISNEW_LN, NAMESPACE); + public static final DavPropertyName JCR_ISMODIFIED = DavPropertyName.create(JCR_ISMODIFIED_LN, NAMESPACE); + public static final DavPropertyName JCR_DEFINITION = DavPropertyName.create(JCR_DEFINITION_LN, NAMESPACE); + public static final DavPropertyName JCR_SELECTOR_NAME = DavPropertyName.create(JCR_SELECTOR_NAME_LN, NAMESPACE); + + // property names used for resources representing jcr-nodes + public static final DavPropertyName JCR_PRIMARYNODETYPE = DavPropertyName.create(JCR_PRIMARYNODETYPE_LN, NAMESPACE); + public static final DavPropertyName JCR_MIXINNODETYPES = DavPropertyName.create(JCR_MIXINNODETYPES_LN, NAMESPACE); + public static final DavPropertyName JCR_INDEX = DavPropertyName.create(JCR_INDEX_LN, NAMESPACE); + public static final DavPropertyName JCR_REFERENCES = DavPropertyName.create(JCR_REFERENCES_LN, NAMESPACE); + /** + * @since JCR 2.0 + */ + public static final DavPropertyName JCR_WEAK_REFERENCES = DavPropertyName.create(JCR_WEAK_REFERENCES_LN, NAMESPACE); + public static final DavPropertyName JCR_UUID = DavPropertyName.create(JCR_UUID_LN, NAMESPACE); + public static final DavPropertyName JCR_PRIMARYITEM = DavPropertyName.create(JCR_PRIMARYITEM_LN, NAMESPACE); + + // property names used for resources representing jcr-properties + public static final DavPropertyName JCR_TYPE = DavPropertyName.create(JCR_TYPE_LN, NAMESPACE); + public static final DavPropertyName JCR_VALUE = DavPropertyName.create(JCR_VALUE_LN, NAMESPACE); + public static final DavPropertyName JCR_VALUES = DavPropertyName.create(JCR_VALUES_LN, NAMESPACE); + public static final DavPropertyName JCR_LENGTH = DavPropertyName.create(JCR_LENGTH_LN, NAMESPACE); + public static final DavPropertyName JCR_LENGTHS = DavPropertyName.create(JCR_LENGTHS_LN, NAMESPACE); + public static final DavPropertyName JCR_GET_STRING = DavPropertyName.create(JCR_GET_STRING_LN, NAMESPACE); + + // property names used for resource representing a workspace + public static final DavPropertyName JCR_NAMESPACES = DavPropertyName.create(JCR_NAMESPACES_LN, NAMESPACE); + public static final DavPropertyName JCR_NODETYPES_CND = DavPropertyName.create(JCR_NODETYPES_CND_LN, NAMESPACE); + + // property names used for resource representing a version history + public static final DavPropertyName JCR_VERSIONABLEUUID = DavPropertyName.create(JCR_VERSIONABLEUUID_LN, NAMESPACE); + + //-----------------------------------------< JSR170 specific privileges >--- + /** + * Privilege representing the JSR170 'read' action. + *

        Note: the name of this constant is somewhat misleading + * as it corresponds to {@link javax.jcr.Session#ACTION_READ} and not + * to {@link javax.jcr.security.Privilege#JCR_READ}.

        + */ + public static final Privilege PRIVILEGE_JCR_READ = Privilege.getPrivilege(Session.ACTION_READ, NAMESPACE); + /** + * Privilege representing the JSR170 'add_node' action. + *

        Note: the name of this constant is somewhat misleading + * as it corresponds to {@link javax.jcr.Session#ACTION_ADD_NODE} and not + * to {@link javax.jcr.security.Privilege#JCR_ADD_CHILD_NODES}.

        + */ + public static final Privilege PRIVILEGE_JCR_ADD_NODE = Privilege.getPrivilege(Session.ACTION_ADD_NODE, NAMESPACE); + /** + * Privilege representing the JSR170 'set_property' action. + *

        Note: the name of this constant is somewhat misleading + * as it corresponds to {@link javax.jcr.Session#ACTION_SET_PROPERTY} and not + * to {@link javax.jcr.security.Privilege#JCR_MODIFY_PROPERTIES}.

        + */ + public static final Privilege PRIVILEGE_JCR_SET_PROPERTY = Privilege.getPrivilege(Session.ACTION_SET_PROPERTY, NAMESPACE); + /** + * Privilege representing the JSR170 'remove' action. + *

        Note: the name of this constant is somewhat misleading + * as it corresponds to {@link javax.jcr.Session#ACTION_REMOVE} and not + * to {@link javax.jcr.security.Privilege#JCR_REMOVE_NODE} or + * {@link javax.jcr.security.Privilege#JCR_REMOVE_CHILD_NODES}.

        + */ + public static final Privilege PRIVILEGE_JCR_REMOVE = Privilege.getPrivilege(Session.ACTION_REMOVE, NAMESPACE); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JCRWebdavServerServlet.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JCRWebdavServerServlet.java new file mode 100644 index 00000000000..2a78844d9da --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JCRWebdavServerServlet.java @@ -0,0 +1,321 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import org.apache.jackrabbit.server.BasicCredentialsProvider; +import org.apache.jackrabbit.server.CredentialsProvider; +import org.apache.jackrabbit.server.SessionProvider; +import org.apache.jackrabbit.server.SessionProviderImpl; +import org.apache.jackrabbit.server.jcr.JCRWebdavServer; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSessionProvider; +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.jcr.observation.SubscriptionManagerImpl; +import org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl; +import org.apache.jackrabbit.webdav.observation.SubscriptionManager; +import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +/** + * JCRWebdavServerServlet provides request/response handling for the + * JCRWebdavServer. + *

        + * Implementations of this abstract class must implement the + * {@link #getRepository()} method to access the repository. + */ +public abstract class JCRWebdavServerServlet extends AbstractWebdavServlet { + + /** + * the default logger + */ + private static Logger log = LoggerFactory.getLogger(JCRWebdavServerServlet.class); + + /** + * Init parameter specifying the prefix used with the resource path. + */ + public static final String INIT_PARAM_RESOURCE_PATH_PREFIX = "resource-path-prefix"; + + /** + * Optional 'concurrency-level' parameter defining the concurrency level + * within the jcr-server. If the parameter is omitted the internal default + * value (50) is used. + */ + public final static String INIT_PARAM_CONCURRENCY_LEVEL = "concurrency-level"; + + /** + * Servlet context attribute used to store the path prefix instead of + * having a static field with this servlet. The latter causes problems + * when running multiple + */ + public static final String CTX_ATTR_RESOURCE_PATH_PREFIX = "jackrabbit.webdav.jcr.resourcepath"; + + private String pathPrefix; + + private JCRWebdavServer server; + private DavResourceFactory resourceFactory; + private DavLocatorFactory locatorFactory; + protected TxLockManagerImpl txMgr; + protected SubscriptionManager subscriptionMgr; + + /** + * Initializes the servlet set reads the following parameter from the + * servlet configuration: + *

          + *
        • resource-path-prefix: optional prefix for all resources.
        • + *
        + * + * @throws ServletException + */ + @Override + public void init() throws ServletException { + super.init(); + + // set resource path prefix + pathPrefix = getInitParameter(INIT_PARAM_RESOURCE_PATH_PREFIX); + getServletContext().setAttribute(CTX_ATTR_RESOURCE_PATH_PREFIX, pathPrefix); + log.debug(INIT_PARAM_RESOURCE_PATH_PREFIX + " = " + pathPrefix); + + txMgr = new TxLockManagerImpl(); + subscriptionMgr = new SubscriptionManagerImpl(); + txMgr.addTransactionListener((SubscriptionManagerImpl) subscriptionMgr); + + // todo: eventually make configurable + resourceFactory = new DavResourceFactoryImpl(txMgr, subscriptionMgr); + locatorFactory = new DavLocatorFactoryImpl(pathPrefix); + } + + /** + * Returns true if the preconditions are met. This includes validation of + * {@link WebdavRequest#matchesIfHeader(DavResource) If header} and validation + * of {@link org.apache.jackrabbit.webdav.transaction.TransactionConstants#HEADER_TRANSACTIONID + * TransactionId header}. This method will also return false if the requested + * resource resides within a different workspace as is assigned to the repository + * session attached to the given request. + * + * @see AbstractWebdavServlet#isPreconditionValid(WebdavRequest, DavResource) + */ + @Override + protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) { + // first check matching If header + if (!request.matchesIfHeader(resource)) { + return false; + } + + // test if the requested path matches to the existing session + // this may occur if the session was retrieved from the cache. + try { + Session repositorySesssion = JcrDavSession.getRepositorySession(request.getDavSession()); + String reqWspName = resource.getLocator().getWorkspaceName(); + String wsName = repositorySesssion.getWorkspace().getName(); + // compare workspace names if the requested resource isn't the + // root-collection and the request not MKWORKSPACE. + if (DavMethods.DAV_MKWORKSPACE != DavMethods.getMethodCode(request.getMethod()) && + reqWspName != null && !reqWspName.equals(wsName)) { + return false; + } + } catch (DavException e) { + log.error("Internal error: " + e.toString()); + return false; + } + + + // make sure, the TransactionId header is valid + String txId = request.getTransactionId(); + return txId == null || txMgr.hasLock(txId, resource); + } + + /** + * Returns the DavSessionProvider + * + * @return server + * @see AbstractWebdavServlet#getDavSessionProvider() + */ + @Override + public DavSessionProvider getDavSessionProvider() { + if (server == null) { + Repository repository = getRepository(); + String cl = getInitParameter(INIT_PARAM_CONCURRENCY_LEVEL); + if (cl != null) { + try { + server = new JCRWebdavServer(repository, getSessionProvider(), Integer.parseInt(cl)); + } catch (NumberFormatException e) { + log.debug("Invalid value '" + cl+ "' for init-param 'concurrency-level'. Using default instead."); + server = new JCRWebdavServer(repository, getSessionProvider()); + } + } else { + server = new JCRWebdavServer(repository, getSessionProvider()); + } + } + return server; + } + + /** + * Throws UnsupportedOperationException. + * + * @see AbstractWebdavServlet#setDavSessionProvider(DavSessionProvider) + */ + @Override + public void setDavSessionProvider(DavSessionProvider davSessionProvider) { + throw new UnsupportedOperationException("Not implemented. DavSession(s) are provided by the 'JCRWebdavServer'"); + } + + /** + * Returns the DavLocatorFactory + * + * @see AbstractWebdavServlet#getLocatorFactory() + */ + @Override + public DavLocatorFactory getLocatorFactory() { + if (locatorFactory == null) { + locatorFactory = new DavLocatorFactoryImpl(pathPrefix); + } + return locatorFactory; + } + + /** + * Sets the DavLocatorFactory + * + * @see AbstractWebdavServlet#setLocatorFactory(DavLocatorFactory) + */ + @Override + public void setLocatorFactory(DavLocatorFactory locatorFactory) { + this.locatorFactory = locatorFactory; + } + + /** + * Returns the DavResourceFactory. + * + * @see AbstractWebdavServlet#getResourceFactory() + */ + @Override + public DavResourceFactory getResourceFactory() { + if (resourceFactory == null) { + resourceFactory = new DavResourceFactoryImpl(txMgr, subscriptionMgr); + } + return resourceFactory; + } + + /** + * Sets the DavResourceFactory. + * + * @see AbstractWebdavServlet#setResourceFactory(org.apache.jackrabbit.webdav.DavResourceFactory) + */ + @Override + public void setResourceFactory(DavResourceFactory resourceFactory) { + this.resourceFactory = resourceFactory; + } + + /** + * Modified variant needed for JCR move and copy that isn't compliant to + * WebDAV. The latter requires both methods to fail if the destination already + * exists and Overwrite is set to F (false); in JCR however this depends on + * the node type characteristics of the parent (SNSiblings allowed or not). + * + * @param destResource destination resource to be validated. + * @param request The webdav request + * @param checkHeader flag indicating if the destination header must be present. + * @return status code indicating whether the destination is valid. + */ + @Override + protected int validateDestination(DavResource destResource, WebdavRequest request, boolean checkHeader) + throws DavException { + + if (checkHeader) { + String destHeader = request.getHeader(HEADER_DESTINATION); + if (destHeader == null || "".equals(destHeader)) { + return DavServletResponse.SC_BAD_REQUEST; + } + } + if (destResource.getLocator().equals(request.getRequestLocator())) { + return DavServletResponse.SC_FORBIDDEN; + } + + int status; + if (destResource.exists()) { + if (request.isOverwrite()) { + // matching if-header required for existing resources + if (!request.matchesIfHeader(destResource)) { + return DavServletResponse.SC_PRECONDITION_FAILED; + } else { + // overwrite existing resource + destResource.getCollection().removeMember(destResource); + status = DavServletResponse.SC_NO_CONTENT; + } + } else { + /* NO overwrite header: + + but, instead of return the 412 Precondition-Failed code required + by the WebDAV specification(s) leave the validation to the + JCR repository. + */ + status = DavServletResponse.SC_CREATED; + } + + } else { + // destination does not exist >> copy/move can be performed + status = DavServletResponse.SC_CREATED; + } + return status; + } + + /** + * Returns the configured path prefix + * + * @param ctx The servlet context. + * @return resourcePathPrefix + * @see #INIT_PARAM_RESOURCE_PATH_PREFIX + */ + public static String getPathPrefix(ServletContext ctx) { + return (String) ctx.getAttribute(CTX_ATTR_RESOURCE_PATH_PREFIX); + } + + /** + * Returns the repository to be used by this servlet. + * + * @return the JCR repository to be used by this servlet + */ + protected abstract Repository getRepository(); + + /** + * Returns a new instanceof BasicCredentialsProvider. + * + * @return a new credentials provider + */ + protected CredentialsProvider getCredentialsProvider() { + return new BasicCredentialsProvider(getInitParameter(INIT_PARAM_MISSING_AUTH_MAPPING)); + } + + /** + * Returns a new instanceof SessionProviderImpl. + * + * @return a new session provider + */ + protected SessionProvider getSessionProvider() { + return new SessionProviderImpl(getCredentialsProvider()); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrDavException.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrDavException.java new file mode 100644 index 00000000000..c866d8d1a87 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrDavException.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.webdav.jcr; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.MergeException; +import javax.jcr.NamespaceException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PathNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.version.VersionException; +import java.util.Map; +import java.util.LinkedHashMap; + + +/** + * JcrDavException extends the {@link DavException} in order to + * wrap various repository exceptions. + */ +public class JcrDavException extends DavException { + + private static Logger log = LoggerFactory.getLogger(JcrDavException.class); + + // ordered mapping of Jcr exceptions to error codes. + private static Map, Integer> codeMap = new LinkedHashMap, Integer>(20); + static { + codeMap.put(AccessDeniedException.class, DavServletResponse.SC_FORBIDDEN); + codeMap.put(ConstraintViolationException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(InvalidItemStateException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(InvalidSerializedDataException.class, DavServletResponse.SC_BAD_REQUEST); + codeMap.put(InvalidQueryException.class, DavServletResponse.SC_BAD_REQUEST); + codeMap.put(ItemExistsException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(ItemNotFoundException.class, DavServletResponse.SC_FORBIDDEN); + codeMap.put(LockException.class, DavServletResponse.SC_LOCKED); + codeMap.put(MergeException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(NamespaceException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(NoSuchNodeTypeException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(NoSuchWorkspaceException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(PathNotFoundException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(ReferentialIntegrityException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(LoginException.class, DavServletResponse.SC_UNAUTHORIZED); + codeMap.put(UnsupportedRepositoryOperationException.class, DavServletResponse.SC_NOT_IMPLEMENTED); + codeMap.put(ValueFormatException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(VersionException.class, DavServletResponse.SC_CONFLICT); + codeMap.put(RepositoryException.class, DavServletResponse.SC_FORBIDDEN); + } + + private static int lookupErrorCode(Class exceptionClass) { + Integer code = codeMap.get(exceptionClass); + if (code == null) { + for (Class jcrExceptionClass : codeMap.keySet()) { + if (jcrExceptionClass.isAssignableFrom(exceptionClass)) { + code = codeMap.get(jcrExceptionClass); + break; + } + } + if (code == null) { + code = DavServletResponse.SC_FORBIDDEN; // fallback + } + } + return code; + } + + /** + * The exception wrapped by this DavException instance. + */ + private Class exceptionClass; + + /** + * Create a new JcrDavException. + * + * @param cause The original cause of this DavException. Note, that + * in contrast to {@link Throwable#Throwable(Throwable)}, {@link Throwable#Throwable(String, Throwable)} and + * {@link Throwable#initCause(Throwable)} the cause must not be null. + * @param errorCode Status code for the response. + * @throws NullPointerException if the given exception is null. + * @see DavException#DavException(int, String) + * @see DavException#DavException(int) + */ + public JcrDavException(Throwable cause, int errorCode) { + super(errorCode, cause.getMessage(), cause, null); + exceptionClass = cause.getClass(); + if (log.isDebugEnabled()) { + log.debug("Handling exception with error code " + errorCode, cause); + } + } + + /** + * Same as {@link JcrDavException#JcrDavException(Throwable, int)} where the + * error code is retrieved from an internal mapping. + * + * @param cause Cause of this DavException + * @throws NullPointerException if the given exception is null. + * @see JcrDavException#JcrDavException(Throwable, int) + */ + public JcrDavException(RepositoryException cause) { + this(cause, lookupErrorCode(cause.getClass())); + } + + /** + * Always returns true. + * + * @return true + */ + @Override + public boolean hasErrorCondition() { + return true; + } + + /** + * Returns a DAV:error Xml element containing the exceptions class and the + * message as child elements. + * + * @return Xml representation of this exception. + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + @Override + public Element toXml(Document document) { + Element error = DomUtil.createElement(document, XML_ERROR, DavConstants.NAMESPACE); + Element excep = DomUtil.createElement(document, "exception", ItemResourceConstants.NAMESPACE); + DomUtil.addChildElement(excep, "class", ItemResourceConstants.NAMESPACE, exceptionClass.getName()); + DomUtil.addChildElement(excep, "message", ItemResourceConstants.NAMESPACE, getMessage()); + error.appendChild(excep); + return error; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrDavSession.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrDavSession.java new file mode 100644 index 00000000000..d4fe9c2332e --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrDavSession.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.webdav.jcr; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.jcr.lock.LockTokenMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.HashSet; + +/** + * JcrDavSession specific base implementation of the + * DavSession interface, which simply wraps a {@link Session} + * object. This implementation adds a utility method that allows to + * {@link #getRepositorySession() unwrap} the underlying repository session. + *
        + * Note, that in this basic implementation the following methods are simply + * forwarded to the corresponding call on Session: + *
          + *
        • {@link #getLockTokens()} => {@link Session#getLockTokens()}
        • + *
        • {@link #addLockToken(String)} => {@link Session#addLockToken(String)}
        • + *
        • {@link #removeLockToken(String)} => {@link Session#removeLockToken(String)}
        • + *
        + * Subclasses may overwrite or extend this behaviour. + */ +public abstract class JcrDavSession implements DavSession { + + private static Logger log = LoggerFactory.getLogger(JcrDavSession.class); + + /** the underlying jcr session */ + private final Session session; + + /** the lock tokens of this session */ + private final HashSet lockTokens = new HashSet(); + + /** + * + * @param session + */ + protected JcrDavSession(Session session) { + this.session = session; + } + + /** + * + * @param davSession + * @throws DavException + */ + public static void checkImplementation(DavSession davSession) throws DavException { + if (!(davSession instanceof JcrDavSession)) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "JCR specific DavSession expected. Found: " + davSession); + } + } + + /** + * + * @param davSession + * @return + * @throws DavException + */ + public static Session getRepositorySession(DavSession davSession) throws DavException { + checkImplementation(davSession); + return ((JcrDavSession)davSession).getRepositorySession(); + } + + /** + * Unwrap the {@link Session repository session} object. + * + * @return the session object wrapped by this DavSession + */ + public Session getRepositorySession() { + return session; + } + + //---------------------------------------------------------< DavSession >--- + /** + * + * @param token + * @see DavSession#addLockToken(String) + */ + @Override + public void addLockToken(String token) { + if (!LockTokenMapper.isForSessionScopedLock(token)) { + try { + session.getWorkspace().getLockManager().addLockToken(LockTokenMapper.getJcrLockToken(token)); + } + catch (RepositoryException ex) { + log.debug("trying to add lock token " + token + " to session", ex); + } + } + lockTokens.add(token); + } + + /** + * + * @return + * @see DavSession#getLockTokens() + */ + @Override + public String[] getLockTokens() { + return lockTokens.toArray(new String[lockTokens.size()]); + } + + /** + * + * @param token + * @see DavSession#removeLockToken(String) + */ + @Override + public void removeLockToken(String token) { + if (!LockTokenMapper.isForSessionScopedLock(token)) { + try { + session.getWorkspace().getLockManager().removeLockToken(LockTokenMapper.getJcrLockToken(token)); + } + catch (RepositoryException ex) { + log.debug("trying to remove lock token " + token + " to session", ex); + } + } + lockTokens.remove(token); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrValueType.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrValueType.java new file mode 100644 index 00000000000..9d941c551d5 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/JcrValueType.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.webdav.jcr; + +/** JcrValueType... + * + * @deprecated As of Jackrabbit 2.2. Please Use {@link org.apache.jackrabbit.commons.webdav.JcrValueType} instead. + */ +public final class JcrValueType extends org.apache.jackrabbit.commons.webdav.JcrValueType { + +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/RootCollection.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/RootCollection.java new file mode 100644 index 00000000000..a86a3694b6b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/RootCollection.java @@ -0,0 +1,310 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.jcr.security.JcrSupportedPrivilegesProperty; +import org.apache.jackrabbit.webdav.jcr.security.SecurityUtils; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.search.SearchResource; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.version.DeltaVResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RootCollection represent the WebDAV root resource that does not + * represent any repository item. A call to getMembers() returns a + * DavResourceIterator containing only workspace resources + * resources, thus revealing the names of the accessible JCR workspaces. + */ +public class RootCollection extends AbstractResource { + + private static Logger log = LoggerFactory.getLogger(RootCollection.class); + + /** + * Create a new RootCollection. + * + * @param locator + * @param session + * @param factory + */ + protected RootCollection(DavResourceLocator locator, JcrDavSession session, + DavResourceFactory factory) { + super(locator, session, factory); + + // initialize the supported locks and reports + initLockSupport(); + initSupportedReports(); + } + + //--------------------------------------------------------< DavResource >--- + /** + * Returns a string listing the METHODS for this resource as it + * is required for the "Allow" response header. + * + * @return string listing the METHODS allowed + * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() + */ + @Override + public String getSupportedMethods() { + StringBuilder sb = new StringBuilder(DavResource.METHODS); + sb.append(", "); + sb.append(DeltaVResource.METHODS_INCL_MKWORKSPACE); + sb.append(", "); + sb.append(SearchResource.METHODS); + return sb.toString(); + } + + /** + * Returns true + * + * @return true + * @see org.apache.jackrabbit.webdav.DavResource#exists() + */ + @Override + public boolean exists() { + return true; + } + + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = super.getProperty(name); + if (prop == null) { + try { + if (SecurityConstants.SUPPORTED_PRIVILEGE_SET.equals(name)) { + prop = new JcrSupportedPrivilegesProperty(getRepositorySession()).asDavProperty(); + } + } catch (RepositoryException e) { + log.error("Failed to build SupportedPrivilegeSet property: " + e.getMessage()); + } + } + return prop; + } + + /** + * Returns true + * + * @return true + * @see org.apache.jackrabbit.webdav.DavResource#isCollection() + */ + @Override + public boolean isCollection() { + return true; + } + + /** + * Returns an empty string. + * + * @return empty string + * @see org.apache.jackrabbit.webdav.DavResource#getDisplayName() + */ + @Override + public String getDisplayName() { + return ""; + } + + /** + * Always returns 'now' + * + * @return + * @see org.apache.jackrabbit.webdav.DavResource#getModificationTime() + */ + @Override + public long getModificationTime() { + return new Date().getTime(); + } + + /** + * Sets content lengths to '0' and retrieves the modification time. + * + * @param outputContext + * @throws IOException + * @see DavResource#spool(org.apache.jackrabbit.webdav.io.OutputContext) + */ + @Override + public void spool(OutputContext outputContext) throws IOException { + if (outputContext.hasStream()) { + Session session = getRepositorySession(); + Repository rep = session.getRepository(); + String repName = rep.getDescriptor(Repository.REP_NAME_DESC); + String repURL = rep.getDescriptor(Repository.REP_VENDOR_URL_DESC); + String repVersion = rep.getDescriptor(Repository.REP_VERSION_DESC); + String repostr = repName + " " + repVersion; + + StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append(repostr); + sb.append(""); + sb.append("

        ").append(repostr).append("

        "); + sb.append("

        Available Workspace Resources:


        Powered by ").append(repName); + sb.append(" ").append(repVersion); + sb.append(""); + + outputContext.setContentLength(sb.length()); + outputContext.setModificationTime(getModificationTime()); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputContext.getOutputStream(), "utf8")); + writer.print(sb.toString()); + writer.close(); + } else { + outputContext.setContentLength(0); + outputContext.setModificationTime(getModificationTime()); + } + } + + /** + * Always returns null + * + * @return null for the root resource is not internal member + * of any resource. + * @see org.apache.jackrabbit.webdav.DavResource#getCollection() + */ + @Override + public DavResource getCollection() { + return null; + } + + /** + * Throws exception: 403 Forbidden. + * @see DavResource#addMember(DavResource, InputContext) + */ + @Override + public void addMember(DavResource resource, InputContext inputContext) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Returns an iterator over the member resources, which are all + * workspace resources available. + * + * @return members of this collection + * @see org.apache.jackrabbit.webdav.DavResource#getMembers() + */ + @Override + public DavResourceIterator getMembers() { + List memberList = new ArrayList(); + try { + String[] wsNames = getRepositorySession().getWorkspace().getAccessibleWorkspaceNames(); + for (String wsName : wsNames) { + String wspPath = "/" + wsName; + DavResourceLocator childLoc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), wspPath, wspPath); + memberList.add(createResourceFromLocator(childLoc)); + } + } catch (RepositoryException e) { + log.error(e.getMessage()); + } catch (DavException e) { + // should never occur + log.error(e.getMessage()); + } + return new DavResourceIteratorImpl(memberList); + } + + /** + * Calls {@link Workspace#deleteWorkspace(String)} for the workspace with + * the name as indicated by the specified member. + * + * @see DavResource#removeMember(org.apache.jackrabbit.webdav.DavResource) + */ + @Override + public void removeMember(DavResource member) throws DavException { + Workspace wsp = getRepositorySession().getWorkspace(); + String name = Text.getName(member.getResourcePath()); + try { + wsp.deleteWorkspace(name); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //-----------------------------------------------------< DeltaVResource >--- + /** + * @see DeltaVResource#addWorkspace(org.apache.jackrabbit.webdav.DavResource) + */ + @Override + public void addWorkspace(DavResource workspace) throws DavException { + Workspace wsp = getRepositorySession().getWorkspace(); + String name = workspace.getDisplayName(); + try { + wsp.createWorkspace(name); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //-------------------------------------------------------------------------- + /** + * @see AbstractResource#initLockSupport() + */ + @Override + protected void initLockSupport() { + // no locking supported + } + + /** + * Since the root resource does not represent a repository item and therefore + * is not member of a workspace resource, this method always returns + * null. + * + * @return null + * @see AbstractResource#getWorkspaceHref() + */ + @Override + protected String getWorkspaceHref() { + return null; + } + + @Override + protected void initPropertyNames() { + super.initPropertyNames(); + if (SecurityUtils.supportsAccessControl(getRepositorySession())) { + names.add(SecurityConstants.SUPPORTED_PRIVILEGE_SET); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/VersionControlledItemCollection.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/VersionControlledItemCollection.java new file mode 100644 index 00000000000..7542b8e8c8b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/VersionControlledItemCollection.java @@ -0,0 +1,702 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.version.LabelInfo; +import org.apache.jackrabbit.webdav.version.MergeInfo; +import org.apache.jackrabbit.webdav.version.UpdateInfo; +import org.apache.jackrabbit.webdav.version.VersionControlledResource; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.VersionResource; +import org.apache.jackrabbit.webdav.version.VersionableResource; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.observation.EventListener; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; +import java.util.List; +import java.util.Collections; + +/** + * VersionControlledItemCollection represents a JCR node item and + * covers all functionality related to versioning of {@link Node}s. + * + * @see Node + */ +public class VersionControlledItemCollection extends DefaultItemCollection + implements VersionControlledResource { + + private static Logger log = LoggerFactory.getLogger(VersionControlledItemCollection.class); + + /** + * Create a new VersionControlledItemCollection. + * + * @param locator + * @param session + * @param factory + * @param item + */ + public VersionControlledItemCollection(DavResourceLocator locator, + JcrDavSession session, + DavResourceFactory factory, + Item item) { + super(locator, session, factory, item); + if (exists() && !(item instanceof Node)) { + throw new IllegalArgumentException("A collection resource can not be constructed from a Property item."); + } + } + + //----------------------------------------------< DavResource interface >--- + /** + * Return a comma separated string listing the supported method names. + * + * @return the supported method names. + * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() + */ + @Override + public String getSupportedMethods() { + StringBuffer sb = new StringBuffer(super.getSupportedMethods()); + // Versioning support + sb.append(", ").append(VersionableResource.METHODS); + if (isVersionControlled()) { + try { + if (((Node)item).isCheckedOut()) { + sb.append(", ").append(VersionControlledResource.methods_checkedOut); + } else { + sb.append(", ").append(VersionControlledResource.methods_checkedIn); + } + } catch (RepositoryException e) { + // should not occur. + log.error(e.getMessage()); + } + } + return sb.toString(); + } + + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = super.getProperty(name); + if (prop == null && isVersionControlled()) { + Node n = (Node) item; + // properties defined by RFC 3253 for version-controlled resources + // workspace property already set in AbstractResource.initProperties() + try { + if (VERSION_HISTORY.equals(name)) { + // DAV:version-history (computed) + String vhHref = getLocatorFromItem(n.getVersionHistory()).getHref(true); + prop = new HrefProperty(VERSION_HISTORY, vhHref, true); + } else if (CHECKED_OUT.equals(name) && n.isCheckedOut()) { + // DAV:checked-out property (protected) + String baseVHref = getLocatorFromItem(n.getBaseVersion()).getHref(true); + prop = new HrefProperty(CHECKED_OUT, baseVHref, true); + } else if (CHECKED_IN.equals(name) && !n.isCheckedOut()) { + // DAV:checked-in property (protected) + String baseVHref = getLocatorFromItem(n.getBaseVersion()).getHref(true); + prop = new HrefProperty(CHECKED_IN, baseVHref, true); + } + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + + return prop; + } + + /** + * @param changeList + * @throws DavException + * @see DefaultItemCollection#alterProperties(List) + * for additional description of non-compliant behaviour. + */ + @Override + public MultiStatusResponse alterProperties(List changeList) throws DavException { + /* first resolve merge conflict since they cannot be handled by + setting property values in jcr (and are persisted immediately). + NOTE: this violates RFC 2518 that requires that proppatch + is processed in the order entries are present in the xml and that + required that no changes must be persisted if any set/remove fails. + */ + // TODO: solve violation of RFC 2518 + resolveMergeConflict(changeList); + // alter other properties only if merge-conflicts could be handled + return super.alterProperties(changeList); + } + + /** + * Resolve one or multiple merge conflicts present on this resource. Please + * note that the 'setProperties' or 'removeProperties' set my contain additional + * resource properties, that need to be changed. Those properties are left + * untouched, whereas the {@link #AUTO_MERGE_SET DAV:auto-merge-set}, is + * removed from the list upon successful resolution of a merge conflict.
        + * If the removeProperties or setProperties set do not contain the mentioned + * merge conflict resource properties or if the value of those properties do + * not allow for a resolution of an existing merge conflict, this method + * returns silently. + * + * @param changeList + * @throws org.apache.jackrabbit.webdav.DavException + * @see Node#doneMerge(Version) + * @see Node#cancelMerge(Version) + */ + private void resolveMergeConflict(List changeList) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + try { + Node n = (Node) item; + VersionManager vMgr = getVersionManager(); + String path = item.getPath(); + + DavProperty autoMergeSet = null; + DavProperty predecessorSet = null; + /* find DAV:auto-merge-set entries. If none exists no attempt is made + to resolve merge conflict > return silently */ + for (int i = 0; i < changeList.size(); i++) { + PropEntry propEntry = changeList.get(i); + // If DAV:auto-merge-set is DavPropertyName all remaining merge + // conflicts are resolved with 'cancel' + if (propEntry instanceof DavPropertyName && AUTO_MERGE_SET.equals(propEntry)) { + // retrieve the current jcr:mergeFailed property values + if (!n.hasProperty(JcrConstants.JCR_MERGEFAILED)) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Attempt to resolve non-existing merge conflicts."); + } + Value[] mergeFailed = n.getProperty(JcrConstants.JCR_MERGEFAILED).getValues(); + for (Value value : mergeFailed) { + vMgr.cancelMerge(path, (Version) getRepositorySession().getNodeByIdentifier(value.getString())); + } + // remove this entry from the changeList + changeList.remove(propEntry); + } else if (propEntry instanceof DavProperty) { + if (AUTO_MERGE_SET.equals(((DavProperty)propEntry).getName())) { + autoMergeSet = (DavProperty) propEntry; + } else if (PREDECESSOR_SET.equals(((DavProperty)propEntry).getName())) { + predecessorSet = (DavProperty) propEntry; + } + } + } + + // If DAV:auto-merge-set is a DavProperty merge conflicts need to be + // resolved individually according to the DAV:predecessor-set property. + if (autoMergeSet != null) { + // retrieve the current jcr:mergeFailed property values + if (!n.hasProperty(JcrConstants.JCR_MERGEFAILED)) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Attempt to resolve non-existing merge conflicts."); + } + + List mergeset = new HrefProperty(autoMergeSet).getHrefs(); + List predecL; + if (predecessorSet == null) { + predecL = Collections.emptyList(); + } else { + predecL = new HrefProperty(predecessorSet).getHrefs(); + } + + Session session = getRepositorySession(); + // loop over the mergeFailed values (versions) and test whether they are + // removed from the DAV:auto-merge-set thus indicating resolution. + Value[] mergeFailed = n.getProperty(JcrConstants.JCR_MERGEFAILED).getValues(); + for (Value value : mergeFailed) { + // build version-href from each entry in the jcr:mergeFailed property + // in order to be able to compare to the entries in the HrefProperty. + Version version = (Version) session.getNodeByIdentifier(value.getString()); + String href = getLocatorFromItem(version).getHref(true); + + // Test if that version has been removed from the merge-set. + // thus indicating that this merge conflict needs to be resolved. + if (!mergeset.contains(href)) { + // If the conflict value has been moved over from DAV:auto-merge-set + // to the predecessor-set, resolution with 'doneMerge' is + // appropriate. If the value has been removed from the + // merge-set but not added to the predecessors 'cancelMerge' + // must be called. + if (predecL.contains(href)) { + vMgr.doneMerge(path, version); + } else { + vMgr.cancelMerge(path, version); + } + } + } + // after successful resolution of merge-conflicts according to + // DAV:auto-merge-set and DAV:predecessor-set remove these entries + // from the changeList. + changeList.remove(autoMergeSet); + if (predecessorSet != null) { + changeList.remove(predecessorSet); + } + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //--------------------------------------< VersionableResource interface >--- + /** + * Adds version control to this resource. If the resource is already under + * version control, this method has no effect. + * + * @throws org.apache.jackrabbit.webdav.DavException if this resource does not + * exist yet or if an error occurs while making the underlying node versionable. + * @see org.apache.jackrabbit.webdav.version.VersionableResource#addVersionControl() + */ + @Override + public void addVersionControl() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (!isVersionControlled()) { + try { + ((Node)item).addMixin(JcrConstants.MIX_VERSIONABLE); + item.save(); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } // else: is already version controlled -> ignore + } + + //--------------------------------< VersionControlledResource interface >--- + /** + * Calls {@link javax.jcr.Node#checkin()} on the underlying repository node. + * + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkin() + */ + @Override + public String checkin() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (!isVersionControlled()) { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + try { + Version v = getVersionManager().checkin(item.getPath()); + String versionHref = getLocatorFromItem(v).getHref(true); + return versionHref; + } catch (RepositoryException e) { + // UnsupportedRepositoryException should not occur + throw new JcrDavException(e); + } + } + + /** + * Calls {@link javax.jcr.Node#checkout()} on the underlying repository node. + * + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkout() + */ + @Override + public void checkout() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (!isVersionControlled()) { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + try { + getVersionManager().checkout(item.getPath()); + } catch (RepositoryException e) { + // UnsupportedRepositoryException should not occur + throw new JcrDavException(e); + } + } + + /** + * Not implemented. Always throws a DavException with error code + * {@link org.apache.jackrabbit.webdav.DavServletResponse#SC_NOT_IMPLEMENTED}. + * + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#uncheckout() + */ + @Override + public void uncheckout() throws DavException { + throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED); + } + + /** + * Perform an update on this resource. Depending on the format of the updateInfo + * this is translated to one of the following methods defined by the JCR API: + *
          + *
        • {@link Node#restore(javax.jcr.version.Version, boolean)}
        • + *
        • {@link Node#restore(javax.jcr.version.Version, String, boolean)}
        • + *
        • {@link Node#restoreByLabel(String, boolean)}
        • + *
        • {@link Node#update(String)}
        • + *
        + *

        + * Limitation: note that the MultiStatus returned by this method + * will not list any nodes that have been removed due to an Uuid conflict. + * + * @param updateInfo + * @return + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#update(org.apache.jackrabbit.webdav.version.UpdateInfo) + */ + //TODO: with jcr the node must not be versionable in order to perform Node.update. + @Override + public MultiStatus update(UpdateInfo updateInfo) throws DavException { + if (updateInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid update request body required."); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + MultiStatus ms = new MultiStatus(); + try { + Node node = (Node)item; + Element udElem = updateInfo.getUpdateElement(); + boolean removeExisting = DomUtil.hasChildElement(udElem, XML_REMOVEEXISTING, NAMESPACE); + + // register eventListener in order to be able to report the modified resources. + EventListener el = new EListener(updateInfo.getPropertyNameSet(), ms); + registerEventListener(el, node.getPath()); + + // perform the update/restore according to the update info + if (updateInfo.getVersionHref() != null) { + String[] hrefs = updateInfo.getVersionHref(); + if (hrefs.length != 1) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid update request body missing version href or containing multiple version hrefs."); + } + + String versionPath = getLocatorFromHref(hrefs[0]).getRepositoryPath(); + String versionName = getItemName(versionPath); + + String relPath = DomUtil.getChildText(udElem, XML_RELPATH, NAMESPACE); + if (relPath == null) { + // restore version by name + node.restore(versionName, removeExisting); + } else if (node.hasNode(relPath)) { + Version v = node.getNode(relPath).getVersionHistory().getVersion(versionName); + node.restore(v, relPath, removeExisting); + } else { + Version v = (Version) getRepositorySession().getNode(versionPath); + node.restore(v, relPath, removeExisting); + } + + } else if (updateInfo.getLabelName() != null) { + String[] labels = updateInfo.getLabelName(); + if (labels.length != 1) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid update request body: Multiple labels specified."); + } + node.restoreByLabel(labels[0], removeExisting); + + } else if (updateInfo.getWorkspaceHref() != null) { + String href = obtainAbsolutePathFromUri(updateInfo.getWorkspaceHref()); + String workspaceName = getLocatorFromHref(href).getWorkspaceName(); + node.update(workspaceName); + } else { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid update request body."); + } + + // unregister the event listener again + unregisterEventListener(el); + + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + return ms; + } + + /** + * Merge the repository node represented by this resource according to the + * information present in the given {@link MergeInfo} object. + * + * @param mergeInfo + * @return MultiStatus recording all repository items modified + * by this merge call as well as the resources that a client must modify to + * complete the merge (see RFC 3253) + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#merge(org.apache.jackrabbit.webdav.version.MergeInfo) + * @see Node#merge(String, boolean) + */ + //TODO: with jcr the node must not be versionable in order to perform Node.merge + @Override + public MultiStatus merge(MergeInfo mergeInfo) throws DavException { + if (mergeInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + MultiStatus ms = new MultiStatus(); + try { + // NOTE: RFC requires that all modified resources are reported in the + // multistatus response. this doesn't work however with the remoting + // there is no way to distinguish the 'failedId's from any other + // resources that got modified by this merge operation -> omitted. + + // todo: RFC allows multiple href elements inside the DAV:source element + String workspaceName = getLocatorFromHref(mergeInfo.getSourceHrefs()[0]).getWorkspaceName(); + + String depth = DomUtil.getChildTextTrim(mergeInfo.getMergeElement(), DavConstants.XML_DEPTH, DavConstants.NAMESPACE); + boolean isShallow = "0".equals(depth); + + NodeIterator failed = getVersionManager().merge(item.getPath(), workspaceName, !mergeInfo.isNoAutoMerge(), isShallow); + + // add resources to the multistatus, that failed to be merged + while (failed.hasNext()) { + Node failedNode = failed.nextNode(); + DavResourceLocator loc = getLocatorFromItem(failedNode); + DavResource res = createResourceFromLocator(loc); + ms.addResponse(new MultiStatusResponse(res, mergeInfo.getPropertyNameSet())); + } + + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + + return ms; + } + + /** + * Modify the labels present with the versions of this resource. + * + * @param labelInfo + * @throws DavException + * @see VersionHistory#addVersionLabel(String, String, boolean) + * @see VersionHistory#removeVersionLabel(String) + */ + @Override + public void label(LabelInfo labelInfo) throws DavException { + if (labelInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid label request body required."); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + try { + if (!isVersionControlled() || ((Node)item).isCheckedOut()) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "A LABEL request may only be applied to a version-controlled, checked-in resource."); + } + DavResource[] resArr = this.getReferenceResources(CHECKED_IN); + if (resArr.length == 1 && resArr[0] instanceof VersionResource) { + ((VersionResource)resArr[0]).label(labelInfo); + } else { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "DAV:checked-in property on '" + getHref() + "' did not point to a single VersionResource."); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Returns the {@link VersionHistory} associated with the repository node. + * If the node is not versionable an exception is thrown. + * + * @return the {@link VersionHistoryResource} associated with this resource. + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#getVersionHistory() + * @see javax.jcr.Node#getVersionHistory() + */ + @Override + public VersionHistoryResource getVersionHistory() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + try { + VersionHistory vh = ((Node)item).getVersionHistory(); + DavResourceLocator loc = getLocatorFromItem(vh); + return (VersionHistoryResource) createResourceFromLocator(loc); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //-------------------------------------------------------------------------- + /** + * Define the set of reports supported by this resource. + * + * @see SupportedReportSetProperty + */ + @Override + protected void initSupportedReports() { + super.initSupportedReports(); + if (exists()) { + supportedReports.addReportType(ReportType.LOCATE_BY_HISTORY); + if (this.isVersionControlled()) { + supportedReports.addReportType(ReportType.VERSION_TREE); + } + } + } + + @Override + protected void initPropertyNames() { + super.initPropertyNames(); + + if (isVersionControlled()) { + names.addAll(JcrDavPropertyNameSet.VERSIONABLE_SET); + + Node n = (Node) item; + try { + if (n.isCheckedOut()) { + names.add(CHECKED_OUT); + if (n.hasProperty(JcrConstants.JCR_PREDECESSORS)) { + names.add(PREDECESSOR_SET); + } + if (n.hasProperty(JcrConstants.JCR_MERGEFAILED)) { + names.add(AUTO_MERGE_SET); + } + // todo: checkout-fork, checkin-fork + } else { + names.add(CHECKED_IN); + } + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + } + } + + /** + * Fill the property set for this resource. + */ + @Override + protected void initProperties() { + super.initProperties(); + if (isVersionControlled()) { + Node n = (Node)item; + // properties defined by RFC 3253 for version-controlled resources + // workspace property already set in AbstractResource.initProperties() + try { + // DAV:version-history (computed) + String vhHref = getLocatorFromItem(n.getVersionHistory()).getHref(true); + properties.add(new HrefProperty(VERSION_HISTORY, vhHref, true)); + + // DAV:auto-version property: there is no auto version, explicit CHECKOUT is required. + properties.add(new DefaultDavProperty(AUTO_VERSION, null, false)); + + String baseVHref = getLocatorFromItem(n.getBaseVersion()).getHref(true); + if (n.isCheckedOut()) { + // DAV:predecessors property + if (n.hasProperty(JcrConstants.JCR_PREDECESSORS)) { + Value[] predec = n.getProperty(JcrConstants.JCR_PREDECESSORS).getValues(); + addHrefProperty(PREDECESSOR_SET, predec, false); + } + // DAV:auto-merge-set property. NOTE: the DAV:merge-set + // never occurs, because merging without bestEffort flag + // being set results in an exception on failure. + if (n.hasProperty(JcrConstants.JCR_MERGEFAILED)) { + Value[] mergeFailed = n.getProperty(JcrConstants.JCR_MERGEFAILED).getValues(); + addHrefProperty(AUTO_MERGE_SET, mergeFailed, false); + } + // todo: checkout-fork, checkin-fork + } + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + } + + /** + * Add a {@link org.apache.jackrabbit.webdav.property.HrefProperty} with the + * specified property name and values. + * + * @param name + * @param values Array of {@link Value}s. + * @param isProtected + * @throws javax.jcr.ValueFormatException + * @throws IllegalStateException + * @throws javax.jcr.RepositoryException + */ + private void addHrefProperty(DavPropertyName name, Value[] values, + boolean isProtected) + throws ValueFormatException, IllegalStateException, RepositoryException { + Node[] nodes = new Node[values.length]; + for (int i = 0; i < values.length; i++) { + nodes[i] = getRepositorySession().getNodeByIdentifier(values[i].getString()); + } + addHrefProperty(name, nodes, isProtected); + } + + /** + * @return true, if this resource represents an existing repository node + * that has the mixin nodetype 'mix:versionable' set. + */ + private boolean isVersionControlled() { + boolean vc = false; + if (exists()) { + try { + vc = ((Node) item).isNodeType(JcrConstants.MIX_VERSIONABLE); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + } + return vc; + } + + /** + * Build a new locator for the given href. + * + * @param href + * @return + */ + private DavResourceLocator getLocatorFromHref(String href) { + DavLocatorFactory f = getLocator().getFactory(); + String prefix = getLocator().getPrefix(); + return f.createResourceLocator(prefix, href); + } + + private VersionManager getVersionManager() throws RepositoryException { + return getRepositorySession().getWorkspace().getVersionManager(); + } + + private static String obtainAbsolutePathFromUri(String uri) { + try { + java.net.URI u = new java.net.URI(uri); + StringBuilder sb = new StringBuilder(); + sb.append(u.getRawPath()); + if (u.getRawQuery() != null) { + sb.append("?").append(u.getRawQuery()); + } + return sb.toString(); + } + catch (java.net.URISyntaxException ex) { + log.warn("parsing " + uri, ex); + return uri; + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/WorkspaceResourceImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/WorkspaceResourceImpl.java new file mode 100644 index 00000000000..8df49b8a137 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/WorkspaceResourceImpl.java @@ -0,0 +1,626 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefReader; +import org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefWriter; +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.commons.cnd.TemplateBuilderFactory; +import org.apache.jackrabbit.commons.webdav.AtomFeedConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet; +import org.apache.jackrabbit.webdav.jcr.property.NamespacesProperty; +import org.apache.jackrabbit.webdav.jcr.security.JcrUserPrivilegesProperty; +import org.apache.jackrabbit.webdav.jcr.security.JcrSupportedPrivilegesProperty; +import org.apache.jackrabbit.webdav.jcr.security.SecurityUtils; +import org.apache.jackrabbit.webdav.jcr.version.report.JcrPrivilegeReport; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.search.SearchResource; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.version.DeltaVResource; +import org.apache.jackrabbit.webdav.version.LabelInfo; +import org.apache.jackrabbit.webdav.version.MergeInfo; +import org.apache.jackrabbit.webdav.version.UpdateInfo; +import org.apache.jackrabbit.webdav.version.VersionControlledResource; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.WorkspaceResource; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +import javax.jcr.Item; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.observation.EventListener; +import javax.jcr.version.Version; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * WorkspaceResourceImpl... + */ +public class WorkspaceResourceImpl extends AbstractResource + implements WorkspaceResource, VersionControlledResource { + + private static Logger log = LoggerFactory.getLogger(WorkspaceResourceImpl.class); + + /** + * Create a new WorkspaceResourceImpl + * + * @param locator + * @param session + * @param factory + */ + WorkspaceResourceImpl(DavResourceLocator locator, JcrDavSession session, DavResourceFactory factory) { + super(locator, session, factory); + + // initialize the supported locks and reports + initLockSupport(); + initSupportedReports(); + } + + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = super.getProperty(name); + if (prop == null) { + StringWriter writer = null; + try { + if (ItemResourceConstants.JCR_NODETYPES_CND.equals(name)) { + writer = new StringWriter(); + Session s = getRepositorySession(); + + CompactNodeTypeDefWriter cndWriter = new CompactNodeTypeDefWriter(writer, s, true); + NodeTypeIterator ntIterator = s.getWorkspace().getNodeTypeManager().getAllNodeTypes(); + while (ntIterator.hasNext()) { + cndWriter.write(ntIterator.nextNodeType()); + } + cndWriter.close(); + /* + NOTE: avoid having JCR_NODETYPES_CND exposed upon allprop + PROPFIND request since it needs to be calculated. + nevertheless, this property can be altered using + PROPPATCH, which is not consistent with the specification + */ + prop = new DefaultDavProperty(ItemResourceConstants.JCR_NODETYPES_CND, writer.toString(), true); + + } else if (SecurityConstants.SUPPORTED_PRIVILEGE_SET.equals(name)) { + prop = new JcrSupportedPrivilegesProperty(getRepositorySession(), null).asDavProperty(); + } else if (SecurityConstants.CURRENT_USER_PRIVILEGE_SET.equals(name)) { + prop = new JcrUserPrivilegesProperty(getRepositorySession(), null).asDavProperty(); + } + } catch (RepositoryException e) { + log.error("Failed to access NodeTypeManager: " + e.getMessage()); + } catch (IOException e) { + log.error("Failed to write compact node definition: " + e.getMessage()); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + log.error(e.getMessage()); + } + } + } + } + + // TODO: required property DAV:workspace-checkout-set (computed) + + return prop; + } + + //--------------------------------------------------------< DavResource >--- + + @Override + public String getSupportedMethods() { + StringBuilder sb = new StringBuilder(DavResource.METHODS); + sb.append(", "); + sb.append(DeltaVResource.METHODS_INCL_MKWORKSPACE); + sb.append(", "); + sb.append(SearchResource.METHODS); + // from vc-resource methods only UPDATE is supported + sb.append(", "); + sb.append(DavMethods.METHOD_UPDATE); + return sb.toString(); + } + + /** + * @return true if the workspace name (see {@link #getDisplayName()} is + * present in the list of available workspace names such as exposed by + * the editing JCR session. + */ + @Override + public boolean exists() { + try { + List available = Arrays.asList(getRepositorySession().getWorkspace().getAccessibleWorkspaceNames()); + return available.contains(getDisplayName()); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + return false; + } + } + + /** + * @return true + */ + @Override + public boolean isCollection() { + return true; + } + + /** + * Returns the name of the workspace. + * + * @return The workspace name + * @see org.apache.jackrabbit.webdav.DavResource#getDisplayName() + * @see javax.jcr.Workspace#getName() + */ + @Override + public String getDisplayName() { + return getLocator().getWorkspaceName(); + } + + /** + * Always returns 'now' + * + * @return + */ + @Override + public long getModificationTime() { + return new Date().getTime(); + } + + /** + * @param outputContext + * @throws IOException + */ + @Override + public void spool(OutputContext outputContext) throws IOException { + + outputContext.setProperty("Link", "; title=\"Event Journal\"; rel=alternate; type=\"" + AtomFeedConstants.MEDIATYPE + "\""); + + if (outputContext.hasStream()) { + Session session = getRepositorySession(); + Repository rep = session.getRepository(); + String repName = rep.getDescriptor(Repository.REP_NAME_DESC); + String repURL = rep.getDescriptor(Repository.REP_VENDOR_URL_DESC); + String repVersion = rep.getDescriptor(Repository.REP_VERSION_DESC); + String repostr = repName + " " + repVersion; + + StringBuilder sb = new StringBuilder(); + sb.append(""); + sb.append(repostr); + sb.append(""); + sb.append(""); + sb.append(""); + sb.append("

        ").append(repostr).append("


        Powered by ").append(repName); + sb.append(" ").append(repVersion); + sb.append(""); + + outputContext.setContentLength(sb.length()); + outputContext.setModificationTime(getModificationTime()); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputContext.getOutputStream(), "utf8")); + writer.print(sb.toString()); + writer.close(); + } else { + outputContext.setContentLength(0); + outputContext.setModificationTime(getModificationTime()); + } + } + + /** + * Retrieve the collection that has all workspace collections + * as internal members. + * + * @see org.apache.jackrabbit.webdav.DavResource#getCollection() + */ + @Override + public DavResource getCollection() { + DavResource collection = null; + // create location with 'null' values for workspace-path and resource-path + DavResourceLocator parentLoc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), null, null, false); + try { + collection = createResourceFromLocator(parentLoc); + } catch (DavException e) { + log.error("Unexpected error while retrieving collection: " + e.getMessage()); + } + return collection; + } + + /** + * Throws 403 exception (Forbidden) + * + * @param resource + * @param inputContext + * @throws DavException + */ + @Override + public void addMember(DavResource resource, InputContext inputContext) throws DavException { + log.error("Cannot add a new member to the workspace resource."); + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Returns the resource representing the JCR root node. + * + * @return + */ + @Override + public DavResourceIterator getMembers() { + try { + DavResourceLocator loc = getLocatorFromItem(getRepositorySession().getRootNode()); + List list = Collections.singletonList(createResourceFromLocator(loc)); + return new DavResourceIteratorImpl(list); + } catch (DavException e) { + log.error("Internal error while building resource for the root node.", e); + return DavResourceIteratorImpl.EMPTY; + } catch (RepositoryException e) { + log.error("Internal error while building resource for the root node.", e); + return DavResourceIteratorImpl.EMPTY; + } + } + + /** + * Throws 403 exception (Forbidden) + * + * @param member + * @throws DavException + */ + @Override + public void removeMember(DavResource member) throws DavException { + log.error("Cannot add a remove the root node."); + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Allows to alter the registered namespaces ({@link ItemResourceConstants#JCR_NAMESPACES}) + * or register node types {@link ItemResourceConstants#JCR_NODETYPES_CND} + * where the passed value is a cnd string containing the definition + * and forwards any other property to the super class. + *

        + * Note that again no property status is set. Any failure while setting + * a property results in an exception (violating RFC 2518). + * + * @param property + * @throws DavException + * @see DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) + */ + @Override + public void setProperty(DavProperty property) throws DavException { + if (ItemResourceConstants.JCR_NAMESPACES.equals(property.getName())) { + NamespacesProperty nsp = new NamespacesProperty(property); + try { + Map changes = new HashMap(nsp.getNamespaces()); + NamespaceRegistry nsReg = getRepositorySession().getWorkspace().getNamespaceRegistry(); + for (String prefix : nsReg.getPrefixes()) { + if (!changes.containsKey(prefix)) { + // prefix not present amongst the new values any more > unregister + nsReg.unregisterNamespace(prefix); + } else if (changes.get(prefix).equals(nsReg.getURI(prefix))) { + // present with same uri-value >> no action required + changes.remove(prefix); + } + } + + // try to register any prefix/uri pair that has a changed uri or + // it has not been present before. + for (String prefix : changes.keySet()) { + String uri = changes.get(prefix); + nsReg.registerNamespace(prefix, uri); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else if (ItemResourceConstants.JCR_NODETYPES_CND.equals(property.getName())) { + try { + Object value = property.getValue(); + List cmds; + if (value instanceof List) { + cmds = (List) value; + } else if (value instanceof Element) { + cmds = Collections.singletonList(value); + } else { + log.warn("Unexpected structure of dcr:nodetypes-cnd property."); + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + String registerCnd = null; + boolean allowUpdate = false; + List unregisterNames = new ArrayList(); + + for (Object listEntry : cmds) { + if (listEntry instanceof Element) { + Element e = (Element) listEntry; + String localName = e.getLocalName(); + if (ItemResourceConstants.XML_CND.equals(localName)) { + registerCnd = DomUtil.getText(e); + } else if (ItemResourceConstants.XML_ALLOWUPDATE.equals(localName)) { + String allow = DomUtil.getTextTrim(e); + allowUpdate = Boolean.parseBoolean(allow); + } else if (ItemResourceConstants.XML_NODETYPENAME.equals(localName)) { + unregisterNames.add(DomUtil.getTextTrim(e)); + } + } + } + + // TODO: for simplicity it's currently either registration or unregistration as nt-modifications are immediately persisted. + Session s = getRepositorySession(); + NodeTypeManager ntMgr = s.getWorkspace().getNodeTypeManager(); + if (registerCnd != null) { + StringReader reader = new StringReader(registerCnd); + DefinitionBuilderFactory factory = + new TemplateBuilderFactory(ntMgr, s.getValueFactory(), s.getWorkspace().getNamespaceRegistry()); + + CompactNodeTypeDefReader cndReader = + new CompactNodeTypeDefReader(reader, "davex", factory); + + List ntts = cndReader.getNodeTypeDefinitions(); + ntMgr.registerNodeTypes(ntts.toArray(new NodeTypeTemplate[ntts.size()]), allowUpdate); + } else if (!unregisterNames.isEmpty()) { + ntMgr.unregisterNodeTypes(unregisterNames.toArray(new String[unregisterNames.size()])); + } + + } catch (ParseException e) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, e); + } + catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else { + // only jcr:namespace or node types can be modified + throw new DavException(DavServletResponse.SC_CONFLICT); + } + } + + /** + * Handles an attempt to set {@link ItemResourceConstants#JCR_NAMESPACES} + * and forwards any other set or remove requests to the super class. + * + * @see #setProperty(DavProperty) + * @see DefaultItemCollection#alterProperties(List) + */ + @Override + public MultiStatusResponse alterProperties(List changeList) throws DavException { + if (changeList.size() == 1) { + PropEntry propEntry = changeList.get(0); + // only modification of prop is allowed. removal is not possible + if (propEntry instanceof DavProperty + && (ItemResourceConstants.JCR_NAMESPACES.equals(((DavProperty)propEntry).getName()) + || ItemResourceConstants.JCR_NODETYPES_CND.equals(((DavProperty)propEntry).getName()))) { + setProperty((DavProperty) propEntry); + } else { + // attempt to remove the namespace property + throw new DavException(DavServletResponse.SC_CONFLICT); + } + } else { + // change list contains more than the jcr:namespaces property + // TODO: build multistatus instead + throw new DavException(DavServletResponse.SC_CONFLICT); + } + return new MultiStatusResponse(getHref(), DavServletResponse.SC_OK); + } + + //------------------------------------------------< VersionableResource >--- + /** + * @throws DavException (403) since workspace is not versionable. implementing + * VersionControlledResource only for 'update'. + */ + @Override + public void addVersionControl() throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + //------------------------------------------< VersionControlledResource >--- + /** + * @throws DavException (403) since workspace is not versionable. implementing + * VersionControlledResource only for 'update'. + */ + @Override + public String checkin() throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * @throws DavException (403) since workspace is not versionable. implementing + * VersionControlledResource only for 'update'. + */ + @Override + public void checkout() throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * @throws DavException (403) since workspace is not versionable. implementing + * VersionControlledResource only for 'update'. + */ + @Override + public void uncheckout() throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * While RFC 3253 does not define any version-related operations for the + * workspace resource, this implementation uses {@link VersionControlledResource#update(UpdateInfo)} + * to map {@link Workspace#restore(javax.jcr.version.Version[], boolean)} to + * a WebDAV call. + *

        + * Limitation: note that the MultiStatus returned by this method + * will not list any nodes that have been removed due to an Uuid conflict. + * + * @param updateInfo + * @return + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#update(org.apache.jackrabbit.webdav.version.UpdateInfo) + */ + @Override + public MultiStatus update(UpdateInfo updateInfo) throws DavException { + if (updateInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid update request body required."); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + Session session = getRepositorySession(); + MultiStatus ms = new MultiStatus(); + try { + Element udElem = updateInfo.getUpdateElement(); + boolean removeExisting = DomUtil.hasChildElement(udElem, ItemResourceConstants.XML_REMOVEEXISTING, ItemResourceConstants.NAMESPACE); + + // register eventListener in order to be able to report the modified resources. + EventListener el = new EListener(updateInfo.getPropertyNameSet(), ms); + registerEventListener(el, session.getRootNode().getPath()); + + String[] hrefs = updateInfo.getVersionHref(); + if (hrefs == null || hrefs.length < 1) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid update request body: at least a single version href must be specified."); + } + // perform the update/restore according to the update info + Version[] versions = new Version[hrefs.length]; + for (int i = 0; i < hrefs.length; i++) { + DavResourceLocator vLoc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), hrefs[i]); + String versionPath = vLoc.getRepositoryPath(); + Item item = getRepositorySession().getItem(versionPath); + if (item instanceof Version) { + versions[i] = (Version) item; + } else { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid update request body: href does not identify a version " + hrefs[i]); + } + } + session.getWorkspace().restore(versions, removeExisting); + + // unregister the event listener again + unregisterEventListener(el); + + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + return ms; + } + + /** + * @throws DavException (403) since workspace is not versionable. implementing + * VersionControlledResource only for 'update'. + */ + @Override + public MultiStatus merge(MergeInfo mergeInfo) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * @throws DavException (403) since workspace is not versionable. implementing + * VersionControlledResource only for 'update'. + */ + @Override + public void label(LabelInfo labelInfo) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * @throws DavException (403) since workspace is not versionable. implementing + * VersionControlledResource only for 'update'. + */ + @Override + public VersionHistoryResource getVersionHistory() throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + //---------------------------------------------------< AbstractResource >--- + @Override + protected void initLockSupport() { + // lock not allowed + } + + @Override + protected void initSupportedReports() { + super.initSupportedReports(); + supportedReports.addReportType(JcrPrivilegeReport.PRIVILEGES_REPORT); + } + + @Override + protected String getWorkspaceHref() { + return getHref(); + } + + @Override + protected void initPropertyNames() { + super.initPropertyNames(); + names.addAll(JcrDavPropertyNameSet.WORKSPACE_SET); + if (SecurityUtils.supportsAccessControl(getRepositorySession())) { + names.add(SecurityConstants.SUPPORTED_PRIVILEGE_SET); + names.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + } + } + + @Override + protected void initProperties() { + super.initProperties(); + try { + // init workspace specific properties + NamespaceRegistry nsReg = getRepositorySession().getWorkspace().getNamespaceRegistry(); + DavProperty namespacesProp = new NamespacesProperty(nsReg); + properties.add(namespacesProp); + } catch (RepositoryException e) { + log.error("Failed to access NamespaceRegistry: " + e.getMessage()); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/JcrActiveLock.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/JcrActiveLock.java new file mode 100644 index 00000000000..f7ebd9a2140 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/JcrActiveLock.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.webdav.jcr.lock; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.lock.AbstractActiveLock; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.lock.Lock; + +/** + * JcrActiveLock wraps a {@link Lock JCR lock} object. + */ +public class JcrActiveLock extends AbstractActiveLock implements ActiveLock, DavConstants { + + private static Logger log = LoggerFactory.getLogger(JcrActiveLock.class); + + private final Lock lock; + + /** + * Create a new ActiveLock object with type '{@link Type#WRITE write}' + * and scope '{@link Scope#EXCLUSIVE exclusive}'. + * + * @param lock + */ + public JcrActiveLock(Lock lock) { + if (lock == null) { + throw new IllegalArgumentException("Can not create a ActiveLock with a 'null' argument."); + } + this.lock = lock; + } + + /** + * Return true if the given lock token equals the token holding that lock. + * + * @param lockToken + * @return true if the given lock token equals this locks token. + * @see org.apache.jackrabbit.webdav.lock.ActiveLock#isLockedByToken(String) + */ + public boolean isLockedByToken(String lockToken) { + if (lockToken != null && lockToken.equals(getToken())) { + return true; + } + return false; + } + + /** + * @see ActiveLock#isExpired() + */ + public boolean isExpired() { + try { + return !lock.isLive(); + } catch (RepositoryException e) { + log.error("Unexpected error: " + e.getMessage()); + return false; + } + } + + /** + * Return the lock token if the {@link javax.jcr.Session} that obtained the lock + * is the lock token holder, null otherwise.
        + * NOTE: currently the token generated by the underlying JCR repository + * is not checked for compliance with RFC 2518 ("OpaqueLockToken-URI = "opaquelocktoken:" + * UUID [Extension] ; The UUID production is the string representation of a + * UUID, as defined in [ISO-11578]. Note that white space (LWS) is not allowed + * between elements of this production."). + *

        + * In case of session-scoped JCR 2.0 locks, the token is never exposed even + * if the current session is lock holder. In order to cope with DAV specific + * requirements and the fulfill the requirement stated above, the node's + * identifier is subsequently exposed as DAV-token. + * + * @see ActiveLock#getToken() + */ + public String getToken() { + try { + return LockTokenMapper.getDavLocktoken(lock); + } catch (RepositoryException e) { + // should never get here + log.warn("Unexpected error while retrieving node identifier for building a DAV specific lock token. {}", + e.getMessage()); + return null; + } + } + + /** + * @see ActiveLock#getOwner() + */ + public String getOwner() { + return lock.getLockOwner(); + } + + /** + * @see ActiveLock#setOwner(String) + */ + public void setOwner(String owner) { + throw new UnsupportedOperationException("setOwner is not implemented"); + } + + /** + * Calculates the milliseconds of the timeout from + * {@link javax.jcr.lock.Lock#getSecondsRemaining()}. If the timeout of + * jcr lock is undefined or infinite {@link #INFINITE_TIMEOUT} is + * returned. + * + * @see ActiveLock#getTimeout() + */ + public long getTimeout() { + try { + long to = lock.getSecondsRemaining(); + long reportAs; + + if (to == Long.MAX_VALUE) { + reportAs = INFINITE_TIMEOUT; + } + else if (to / 1000 <= Long.MAX_VALUE / 1000) { + // expressible as long? + reportAs = to * 1000; + } + else { + reportAs = INFINITE_TIMEOUT; + } + + return reportAs; + } catch (RepositoryException e) { + return INFINITE_TIMEOUT; + } + } + + /** + * Throws UnsupportedOperationException + * + * @see ActiveLock#setTimeout(long) + */ + public void setTimeout(long timeout) { + throw new UnsupportedOperationException("setTimeout is not implemented"); + } + + /** + * @see ActiveLock#isDeep() + */ + public boolean isDeep() { + return lock.isDeep(); + } + + /** + * @see ActiveLock#setIsDeep(boolean) + */ + public void setIsDeep(boolean isDeep) { + throw new UnsupportedOperationException("setIsDeep is not implemented"); + } + + /** + * Always returns {@link Type#WRITE}. + * + * @return {@link Type#WRITE} + * @see ActiveLock#getType() + */ + public Type getType() { + return Type.WRITE; + } + + /** + * @return The scope of this lock, which may either by an {@link Scope#EXCLUSIVE exclusive} + * or {@link ItemResourceConstants#EXCLUSIVE_SESSION exclusive session scoped} + * lock. + * @see ActiveLock#getScope() + */ + public Scope getScope() { + return (lock.isSessionScoped()) ? ItemResourceConstants.EXCLUSIVE_SESSION : Scope.EXCLUSIVE; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/LockTokenMapper.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/LockTokenMapper.java new file mode 100644 index 00000000000..2daba0a0828 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/LockTokenMapper.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.webdav.jcr.lock; + +import javax.jcr.RepositoryException; +import javax.jcr.lock.Lock; + +import org.apache.jackrabbit.util.Text; + +/** + * Maps between WebDAV lock tokens and JCR lock tokens. + *

        + * The following notations are used: + * + *

        + * opaquelocktoken:SESSIONSCOPED:NODEIDENTIFIER
        + * opaquelocktoken:OPENSCOPED:JCRLOCKTOKEN
        + * 
        + * + * The first format is used if the JCR lock does not reveal a lock token, such + * as when it is a session-scoped lock (where SESSIONSCOPED is a constant UUID + * defined below, and NODEIDENTIFIER is the suitably escaped JCR Node + * identifier). + *

        + * The second format is used for open-scoped locks (where OPENSCOPED is another + * constant UUID defined below, and JCRLOCKTOKEN is the suitably escaped JCR + * lock token). + */ +public class LockTokenMapper { + + private static final String OL = "opaquelocktoken:"; + + private static final String SESSIONSCOPED = "4403ef44-4124-11e1-b965-00059a3c7a00"; + private static final String OPENSCOPED = "dccce564-412e-11e1-b969-00059a3c7a00"; + + private static final String SESSPREFIX = OL + SESSIONSCOPED + ":"; + private static final String OPENPREFIX = OL + OPENSCOPED + ":"; + + public static String getDavLocktoken(Lock lock) throws RepositoryException { + String jcrLockToken = lock.getLockToken(); + + if (jcrLockToken == null) { + return SESSPREFIX + Text.escape(lock.getNode().getIdentifier()); + } else { + return OPENPREFIX + Text.escape(jcrLockToken); + } + } + + public static String getJcrLockToken(String token) throws RepositoryException { + if (token.startsWith(OPENPREFIX)) { + return Text.unescape(token.substring(OPENPREFIX.length())); + } else { + throw new RepositoryException("not a token for an open-scoped JCR lock: " + token); + } + } + + public static boolean isForSessionScopedLock(String token) { + return token.startsWith(SESSPREFIX); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/SessionScopedLockEntry.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/SessionScopedLockEntry.java new file mode 100644 index 00000000000..9570a23056c --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/lock/SessionScopedLockEntry.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.webdav.jcr.lock; + +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.lock.AbstractLockEntry; +import org.apache.jackrabbit.webdav.lock.LockEntry; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SessionScopedLockEntry represents the 'session-scoped' write + * lock as defined by JCR. + */ +public class SessionScopedLockEntry extends AbstractLockEntry { + + private static Logger log = LoggerFactory.getLogger(SessionScopedLockEntry.class); + + /** + * @return always returns {@link Type#WRITE write}. + * @see LockEntry#getType() + */ + public Type getType() { + return Type.WRITE; + } + + /** + * @return returns {@link ItemResourceConstants#EXCLUSIVE_SESSION}. + * @see LockEntry#getScope() + */ + public Scope getScope() { + return ItemResourceConstants.EXCLUSIVE_SESSION; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/ItemDefinitionImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/ItemDefinitionImpl.java new file mode 100644 index 00000000000..2e2417adfc9 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/ItemDefinitionImpl.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.webdav.jcr.nodetype; + +import org.apache.jackrabbit.commons.webdav.NodeTypeConstants; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.OnParentVersionAction; + +/** + * ItemDefinitionImpl... + */ +abstract public class ItemDefinitionImpl implements ItemDefinition, NodeTypeConstants, XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(ItemDefinitionImpl.class); + + private final String name; + private NodeType declaringNodeType; + private final boolean isAutoCreated; + private final boolean isMandatory; + private final boolean isProtected; + private final int onParentVersion; + + ItemDefinitionImpl(ItemDefinition definition) { + if (definition == null) { + throw new IllegalArgumentException("PropDef argument can not be null"); + } + name = definition.getName(); + declaringNodeType = definition.getDeclaringNodeType(); + isAutoCreated = definition.isAutoCreated(); + isMandatory = definition.isMandatory(); + isProtected = definition.isProtected(); + onParentVersion = definition.getOnParentVersion(); + } + + /** + * @see ItemDefinition#getDeclaringNodeType() + */ + public NodeType getDeclaringNodeType() { + return declaringNodeType; + } + + /** + * @see ItemDefinition#getName() + */ + public String getName() { + return name; + } + + /** + * @see ItemDefinition#isAutoCreated() + */ + public boolean isAutoCreated() { + return isAutoCreated; + } + + /** + * @see ItemDefinition#isMandatory() + */ + public boolean isMandatory() { + return isMandatory; + } + + /** + * @see ItemDefinition#getOnParentVersion() + */ + public int getOnParentVersion() { + return onParentVersion; + } + + /** + * @see ItemDefinition#isProtected() + */ + public boolean isProtected() { + return isProtected; + } + + //------------------------------------------< XmlSerializable interface >--- + /** + * Returns the Xml representation of a {@link ItemDefinition} object. + * + * @return Xml representation of the specified {@link ItemDefinition def}. + * @param document + */ + public Element toXml(Document document) { + Element elem = document.createElement(getElementName()); + NodeType dnt = getDeclaringNodeType(); + if (dnt != null) { + elem.setAttribute(DECLARINGNODETYPE_ATTRIBUTE, dnt.getName()); + } + elem.setAttribute(NAME_ATTRIBUTE, getName()); + elem.setAttribute(AUTOCREATED_ATTRIBUTE, Boolean.toString(isAutoCreated())); + elem.setAttribute(MANDATORY_ATTRIBUTE, Boolean.toString(isMandatory())); + elem.setAttribute(ONPARENTVERSION_ATTRIBUTE, OnParentVersionAction.nameFromValue(getOnParentVersion())); + elem.setAttribute(PROTECTED_ATTRIBUTE, Boolean.toString(isProtected())); + return elem; + } + + //-------------------------------------< implementation specific method >--- + /** + * Returns the name of the root element + * + * @return the name of the root element + */ + abstract String getElementName(); +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/NodeDefinitionImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/NodeDefinitionImpl.java new file mode 100644 index 00000000000..4f85a267980 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/NodeDefinitionImpl.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.webdav.jcr.nodetype; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; + +/** + * NodeDefinitionImpl... + */ +public final class NodeDefinitionImpl extends ItemDefinitionImpl implements NodeDefinition { + + private static Logger log = LoggerFactory.getLogger(NodeDefinitionImpl.class); + + private final NodeType[] requiredPrimaryTypes; + private final NodeType defaultPrimaryType; + private final boolean allowsSameNameSiblings; + + private NodeDefinitionImpl(NodeDefinition definition) { + super(definition); + + requiredPrimaryTypes = definition.getRequiredPrimaryTypes(); + defaultPrimaryType = definition.getDefaultPrimaryType(); + allowsSameNameSiblings = definition.allowsSameNameSiblings(); + } + + public static NodeDefinitionImpl create(NodeDefinition definition) { + if (definition instanceof NodeDefinitionImpl) { + return (NodeDefinitionImpl) definition; + } else { + return new NodeDefinitionImpl(definition); + } + } + + //-----------------------------------------------------< NodeDefinition >--- + /** + * @see javax.jcr.nodetype.NodeDefinition#getRequiredPrimaryTypes() + */ + public NodeType[] getRequiredPrimaryTypes() { + return requiredPrimaryTypes; + } + + /** + * @see javax.jcr.nodetype.NodeDefinition#getDefaultPrimaryType() + */ + public NodeType getDefaultPrimaryType() { + return defaultPrimaryType; + } + + /** + * @see javax.jcr.nodetype.NodeDefinition#allowsSameNameSiblings() + */ + public boolean allowsSameNameSiblings() { + return allowsSameNameSiblings; + } + + /** + * @see javax.jcr.nodetype.NodeDefinition#getDefaultPrimaryTypeName() + */ + public String getDefaultPrimaryTypeName() { + return defaultPrimaryType.getName(); + } + + /** + * @see javax.jcr.nodetype.NodeDefinition#getRequiredPrimaryTypeNames() + */ + public String[] getRequiredPrimaryTypeNames() { + String[] names = new String[requiredPrimaryTypes.length]; + for (int i = 0; i < requiredPrimaryTypes.length; i++) { + names[i] = requiredPrimaryTypes[i].getName(); + } + return names; + } + + //-------------------------------------< implementation specific method >--- + /** + * Returns xml representation + * + * @return xml representation + * @param document + */ + @Override + public Element toXml(Document document) { + Element elem = super.toXml(document); + elem.setAttribute(SAMENAMESIBLINGS_ATTRIBUTE, Boolean.toString(allowsSameNameSiblings())); + // defaultPrimaryType can be 'null' + NodeType defaultPrimaryType = getDefaultPrimaryType(); + if (defaultPrimaryType != null) { + elem.setAttribute(DEFAULTPRIMARYTYPE_ATTRIBUTE, defaultPrimaryType.getName()); + } + // reqPrimaryTypes: minimal set is nt:base. + Element reqPrimaryTypes = document.createElement(REQUIREDPRIMARYTYPES_ELEMENT); + for (NodeType nt : getRequiredPrimaryTypes()) { + Element rptElem = document.createElement(REQUIREDPRIMARYTYPE_ELEMENT); + DomUtil.setText(rptElem, nt.getName()); + reqPrimaryTypes.appendChild(rptElem); + + } + elem.appendChild(reqPrimaryTypes); + return elem; + } + + /** + * Returns {@link #CHILDNODEDEFINITION_ELEMENT} + * + * @return always returns {@link #CHILDNODEDEFINITION_ELEMENT}. + */ + @Override + String getElementName() { + return CHILDNODEDEFINITION_ELEMENT; + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/NodeTypeProperty.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/NodeTypeProperty.java new file mode 100644 index 00000000000..7d917d41e8b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/NodeTypeProperty.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.webdav.jcr.nodetype; + +import org.apache.jackrabbit.commons.webdav.NodeTypeConstants; +import org.apache.jackrabbit.commons.webdav.NodeTypeUtil; +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.nodetype.NodeType; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * NodeTypeProperty... + */ +public class NodeTypeProperty extends AbstractDavProperty> implements NodeTypeConstants { + + private final Set nodetypeNames = new HashSet(); + + public NodeTypeProperty(DavPropertyName name, NodeType nodeType, boolean isProtected) { + this(name, new NodeType[]{nodeType}, isProtected); + } + + public NodeTypeProperty(DavPropertyName name, NodeType[] nodeTypes, boolean isProtected) { + super(name, isProtected); + for (NodeType nt : nodeTypes) { + if (nt != null) { + nodetypeNames.add(nt.getName()); + } + } + } + + public NodeTypeProperty(DavPropertyName name, String[] nodeTypeNames, boolean isProtected) { + super(name, isProtected); + for (String nodeTypeName : nodeTypeNames) { + if (nodeTypeName != null) { + nodetypeNames.add(nodeTypeName); + } + } + } + + /** + * Create a new NodeTypeProperty from the specified general + * DavProperty object. + * + * @param property + */ + public NodeTypeProperty(DavProperty property) { + super(property.getName(), property.isInvisibleInAllprop()); + if (property instanceof NodeTypeProperty) { + nodetypeNames.addAll(((NodeTypeProperty) property).nodetypeNames); + } else { + nodetypeNames.addAll(NodeTypeUtil.ntNamesFromXml(property.getValue())); + } + } + + /** + * Return a set of node type names present in this property. + * + * @return set of node type names + */ + public Set getNodeTypeNames() { + return Collections.unmodifiableSet(nodetypeNames); + } + + /** + * Returns the value of this property which is a Set of nodetype names. + * + * @return a Set of nodetype names (String). + */ + public Set getValue() { + return Collections.unmodifiableSet(nodetypeNames); + } + + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + for (String name : getNodeTypeNames()) { + elem.appendChild(NodeTypeUtil.ntNameToXml(name, document)); + } + return elem; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/PropertyDefinitionImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/PropertyDefinitionImpl.java new file mode 100644 index 00000000000..129a63e5634 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/nodetype/PropertyDefinitionImpl.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.webdav.jcr.nodetype; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.PropertyDefinition; + +/** + * PropertyDefinitionImpl... + */ +public final class PropertyDefinitionImpl extends ItemDefinitionImpl implements PropertyDefinition { + + private static Logger log = LoggerFactory.getLogger(PropertyDefinitionImpl.class); + + private final int type; + private final String[] valueConstraints; + private final Value[] defaultValues; + private final boolean isMultiple; + private final String[] availableQueryOperators; + private final boolean isFullTextSearchable; + private final boolean isQueryOrderable; + + private PropertyDefinitionImpl(PropertyDefinition definition) { + super(definition); + + type = definition.getRequiredType(); + valueConstraints = definition.getValueConstraints(); + defaultValues = definition.getDefaultValues(); + isMultiple = definition.isMultiple(); + availableQueryOperators = definition.getAvailableQueryOperators(); + isFullTextSearchable = definition.isFullTextSearchable(); + isQueryOrderable = definition.isQueryOrderable(); + } + + public static PropertyDefinitionImpl create(PropertyDefinition definition) { + if (definition instanceof PropertyDefinitionImpl) { + return (PropertyDefinitionImpl)definition; + } else { + return new PropertyDefinitionImpl(definition); + } + } + + //----------------------------------------< PropertyDefintion interface >--- + /** + * @see PropertyDefinition#getRequiredType() + */ + public int getRequiredType() { + return type; + } + + /** + * @see PropertyDefinition#getValueConstraints() + */ + public String[] getValueConstraints() { + return valueConstraints; + } + + /** + * @see PropertyDefinition#getDefaultValues() + */ + public Value[] getDefaultValues() { + return defaultValues; + } + + /** + * @see PropertyDefinition#isMultiple() + */ + public boolean isMultiple() { + return isMultiple; + } + + /** + * @see PropertyDefinition#getAvailableQueryOperators() + */ + public String[] getAvailableQueryOperators() { + return availableQueryOperators; + } + + /** + * @see PropertyDefinition#isFullTextSearchable() + */ + public boolean isFullTextSearchable() { + return isFullTextSearchable; + } + + /** + * @see PropertyDefinition#isQueryOrderable() + */ + public boolean isQueryOrderable() { + return isQueryOrderable; + } + + //-------------------------------------< implementation specific method >--- + /** + * Return xml representation + * + * @return xml representation + * @param document + */ + @Override + public Element toXml(Document document) { + Element elem = super.toXml(document); + + elem.setAttribute(MULTIPLE_ATTRIBUTE, Boolean.toString(isMultiple())); + elem.setAttribute(REQUIREDTYPE_ATTRIBUTE, PropertyType.nameFromValue(getRequiredType())); + + // JCR 2.0 extensions + elem.setAttribute(FULL_TEXT_SEARCHABLE_ATTRIBUTE, Boolean.toString(isFullTextSearchable())); + elem.setAttribute(QUERY_ORDERABLE_ATTRIBUTE, Boolean.toString(isQueryOrderable())); + + // default values may be 'null' + Value[] values = getDefaultValues(); + if (values != null) { + Element dvElement = document.createElement(DEFAULTVALUES_ELEMENT); + for (Value value : values) { + try { + Element valElem = document.createElement(DEFAULTVALUE_ELEMENT); + DomUtil.setText(valElem, value.getString()); + dvElement.appendChild(valElem); + } catch (RepositoryException e) { + // should not occur + log.error(e.getMessage()); + } + } + elem.appendChild(dvElement); + } + // value constraints array is never null. + Element constrElem = document.createElement(VALUECONSTRAINTS_ELEMENT); + for (String constraint : getValueConstraints()) { + Element vcElem = document.createElement(VALUECONSTRAINT_ELEMENT); + DomUtil.setText(vcElem, constraint); + constrElem.appendChild(vcElem); + } + elem.appendChild(constrElem); + + // JCR 2.0 extension + Element qopElem = document.createElement(AVAILABLE_QUERY_OPERATORS_ELEMENT); + for (String qop : getAvailableQueryOperators()) { + Element opElem = document.createElement(AVAILABLE_QUERY_OPERATOR_ELEMENT); + DomUtil.setText(opElem, qop); + qopElem.appendChild(opElem); + } + elem.appendChild(qopElem); + + return elem; + } + + /** + * Returns {@link #PROPERTYDEFINITION_ELEMENT}. + * + * @return always returns {@link #PROPERTYDEFINITION_ELEMENT} + */ + @Override + String getElementName() { + return PROPERTYDEFINITION_ELEMENT; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/observation/SubscriptionImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/observation/SubscriptionImpl.java new file mode 100644 index 00000000000..5d76160bf23 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/observation/SubscriptionImpl.java @@ -0,0 +1,637 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.observation; + +import org.apache.jackrabbit.commons.webdav.EventUtil; +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.AdditionalEventInfo; +import org.apache.jackrabbit.spi.commons.SessionExtensions; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.transaction.TransactionResource; +import org.apache.jackrabbit.webdav.jcr.transaction.TransactionListener; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.observation.EventBundle; +import org.apache.jackrabbit.webdav.observation.EventDiscovery; +import org.apache.jackrabbit.webdav.observation.EventType; +import org.apache.jackrabbit.webdav.observation.Filter; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.observation.ObservationResource; +import org.apache.jackrabbit.webdav.observation.Subscription; +import org.apache.jackrabbit.webdav.observation.SubscriptionInfo; +import org.apache.jackrabbit.webdav.observation.DefaultEventType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The Subscription class encapsulates a single subscription with + * the following responsibilities:

          + *
        • Providing access to the subscription info,
        • + *
        • Recording events this subscription is interested in,
        • + *
        • Providing access to the events.
        • + *
        + */ +public class SubscriptionImpl implements Subscription, ObservationConstants, EventListener { + + private static Logger log = LoggerFactory.getLogger(SubscriptionImpl.class); + private static final long DEFAULT_TIMEOUT = 300000; // 5 minutes + + private SubscriptionInfo info; + private long expirationTime; + + private final DavResourceLocator locator; + private final String subscriptionId = UUID.randomUUID().toString(); + private final List eventBundles = new ArrayList(); + private final ObservationManager obsMgr; + private final Session session; + + /** + * Create a new Subscription with the given {@link SubscriptionInfo} + * and {@link org.apache.jackrabbit.webdav.observation.ObservationResource resource}. + * + * @param info + * @param resource + * @throws DavException if resource is not based on a JCR repository or + * the repository does not support observation. + */ + public SubscriptionImpl(SubscriptionInfo info, ObservationResource resource) + throws DavException { + setInfo(info); + locator = resource.getLocator(); + session = JcrDavSession.getRepositorySession(resource.getSession()); + try { + obsMgr = session.getWorkspace().getObservationManager(); + } catch (RepositoryException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } + } + + //-------------------------------------------------------< Subscription >--- + /** + * Returns the id of this subscription. + * + * @return subscriptionId + */ + public String getSubscriptionId() { + return subscriptionId; + } + + public boolean eventsProvideNodeTypeInformation() { + String t = session.getRepository().getDescriptor("org.apache.jackrabbit.spi.commons.AdditionalEventInfo"); + return t == null ? false : Boolean.parseBoolean(t); + } + + public boolean eventsProvideNoLocalFlag() { + return session instanceof SessionExtensions; + } + + //----------------------------------------------------< XmlSerializable >--- + /** + * Return the Xml representation of this Subscription as required + * for the {@link org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery} + * webdav property that in included in the response body of a successful SUBSCRIBE + * request or as part of a PROPFIND response. + * + * @return Xml representation + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Element subscr = DomUtil.createElement(document, XML_SUBSCRIPTION, NAMESPACE); + + subscr.appendChild(info.toXml(document)); + subscr.appendChild(DomUtil.depthToXml(info.isDeep(), document)); + subscr.appendChild(DomUtil.timeoutToXml(info.getTimeOut(), document)); + + if (getSubscriptionId() != null) { + Element id = DomUtil.addChildElement(subscr, XML_SUBSCRIPTIONID, NAMESPACE); + id.appendChild(DomUtil.hrefToXml(getSubscriptionId(), document)); + } + + DomUtil.addChildElement(subscr, XML_EVENTSWITHTYPES, NAMESPACE, + Boolean.toString(eventsProvideNodeTypeInformation())); + DomUtil.addChildElement(subscr, XML_EVENTSWITHLOCALFLAG, NAMESPACE, + Boolean.toString(eventsProvideNoLocalFlag())); + + return subscr; + } + + //--------------------------------------------< implementation specific >--- + /** + * Modify the {@link SubscriptionInfo} for this subscription. + * + * @param info + */ + void setInfo(SubscriptionInfo info) { + this.info = info; + // validate the timeout and adjust value, if it is invalid or missing + long timeout = info.getTimeOut(); + if (timeout <= 0) { + timeout = DEFAULT_TIMEOUT; + } + expirationTime = System.currentTimeMillis() + timeout; + } + + /** + * @return JCR compliant integer representation of the event types defined + * for this {@link SubscriptionInfo}. + */ + int getJcrEventTypes() throws DavException { + EventType[] eventTypes = info.getEventTypes(); + int events = 0; + for (EventType eventType : eventTypes) { + events |= getJcrEventType(eventType); + } + return events; + } + + /** + * @return a String array with size > 0 or null + */ + String[] getUuidFilters() { + return getFilterValues(XML_UUID); + } + + /** + * @return a String array with size > 0 or null + */ + String[] getNodetypeNameFilters() { + return getFilterValues(XML_NODETYPE_NAME); + } + + private String[] getFilterValues(String filterLocalName) { + List values = new ArrayList(); + for (Filter filter : info.getFilters(filterLocalName, NAMESPACE)) { + String val = filter.getValue(); + if (val != null) { + values.add(val); + } + } + return (values.size() > 0) ? values.toArray(new String[values.size()]) : null; + } + + /** + * + * @return true if a {@link ObservationConstants#XML_NOLOCAL} element + * is present in the {@link SubscriptionInfo}. + */ + boolean isNoLocal() { + return info.isNoLocal(); + } + + /** + * @return true if this subscription is intended to be deep. + */ + boolean isDeep() { + return info.isDeep(); + } + + /** + * @return the locator of the {@link ObservationResource resource} this + * Subscription was requested for. + */ + DavResourceLocator getLocator() { + return locator; + } + + /** + * Returns true if this Subscription matches the given + * resource. + * + * @param resource + * @return true if this Subscription matches the given + * resource. + */ + boolean isSubscribedToResource(ObservationResource resource) { + return locator.getResourcePath().equals(resource.getResourcePath()); + } + + /** + * Returns true if this Subscription is expired and therefore + * stopped recording events. + * + * @return true if this Subscription is expired + */ + boolean isExpired() { + return System.currentTimeMillis() > expirationTime; + } + + /** + * Returns a {@link org.apache.jackrabbit.webdav.observation.EventDiscovery} object listing all events that were + * recorded since the last call to this method and clears the list of event + * bundles. + * + * @param timeout time in milliseconds to wait at most for events if none + * are present currently. + * @return object listing all events that were recorded. + * @see #onEvent(EventIterator) + */ + synchronized EventDiscovery discoverEvents(long timeout) { + EventDiscovery ed = new EventDiscovery(); + if (eventBundles.isEmpty() && timeout > 0) { + try { + wait(timeout); + } catch (InterruptedException e) { + // continue and possibly return empty event discovery + } + } + for (EventBundle eb : eventBundles) { + ed.addEventBundle(eb); + } + // clear list + eventBundles.clear(); + return ed; + } + + /** + * Creates a new transaction listener for the scope of a transaction + * commit (save call). + * @return a transaction listener for this subscription. + */ + TransactionListener createTransactionListener() { + if (info.isNoLocal()) { + // a subscription which is not interested in local changes does + // not need the transaction id + return new TransactionEvent() { + @Override + public void onEvent(EventIterator events) { + // ignore + } + + @Override + public void beforeCommit(TransactionResource resource, String lockToken) { + // ignore + } + + @Override + public void afterCommit(TransactionResource resource, + String lockToken, + boolean success) { + // ignore + } + }; + } else { + return new TransactionEvent(); + } + } + + /** + * Suspend this subscription. This call will remove this subscription as + * event listener from the observation manager. + */ + void suspend() throws DavException { + try { + obsMgr.removeEventListener(this); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Resumes this subscription. This call will register this subscription + * again as event listener to the observation manager. + */ + void resume() throws DavException { + try { + obsMgr.addEventListener(this, getJcrEventTypes(), + getLocator().getRepositoryPath(), isDeep(), getUuidFilters(), + getNodetypeNameFilters(), isNoLocal()); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //--------------------------------------------< EventListener interface >--- + /** + * Records the events passed as a new event bundle in order to make them + * available with the next {@link #discoverEvents(long)} request. If this + * subscription is expired it will remove itself as listener from the + * observation manager. + * + * @param events to be recorded. + * @see EventListener#onEvent(EventIterator) + * @see #discoverEvents(long) + */ + public synchronized void onEvent(EventIterator events) { + if (!isExpired()) { + eventBundles.add(new EventBundleImpl(events)); + } else { + // expired -> unsubscribe + try { + obsMgr.removeEventListener(this); + } catch (RepositoryException e) { + log.warn("Exception while unsubscribing: " + e); + } + } + notifyAll(); + } + + //-------------------------------------------------------------------------- + /** + * Static utility method to convert the type defined by a + * {@link javax.jcr.observation.Event JCR event} into an EventType + * object. + * + * @param jcrEventType + * @return EventType representation of the given JCR event type. + * @throws IllegalArgumentException if the given int does not represent a + * valid type constants as defined by {@link Event}.
        + * Valid values are + *
          + *
        • {@link Event#NODE_ADDED}
        • + *
        • {@link Event#NODE_REMOVED}
        • + *
        • {@link Event#PROPERTY_ADDED}
        • + *
        • {@link Event#PROPERTY_REMOVED}
        • + *
        • {@link Event#PROPERTY_CHANGED}
        • + *
        • {@link Event#NODE_MOVED}
        • + *
        • {@link Event#PERSIST}
        • + *
        + */ + public static EventType getEventType(int jcrEventType) { + String localName = EventUtil.getEventName(jcrEventType); + return DefaultEventType.create(localName, NAMESPACE); + } + + /** + * @return The DAV event type representation for all known JCR event types. + */ + public static EventType[] getAllEventTypes() { + EventType[] types = DefaultEventType.create(EventUtil.EVENT_ALL, NAMESPACE); + return types; + } + + /** + * Static utility method to convert an EventType as present in + * the Xml body into the corresponding JCR event constant defined by + * {@link javax.jcr.observation.Event}. + * + * @param eventType + * @return Any of the event types defined by {@link Event}.
        + * Possible values are + *
          + *
        • {@link Event#NODE_ADDED}
        • + *
        • {@link Event#NODE_REMOVED}
        • + *
        • {@link Event#PROPERTY_ADDED}
        • + *
        • {@link Event#PROPERTY_REMOVED}
        • + *
        • {@link Event#PROPERTY_CHANGED}
        • + *
        • {@link Event#NODE_MOVED}
        • + *
        • {@link Event#PERSIST}
        • + *
        + * @throws DavException if the given event type does not define a valid + * JCR event type, such as returned by {@link #getEventType(int)}. + */ + public static int getJcrEventType(EventType eventType) throws DavException { + if (eventType == null || !NAMESPACE.equals(eventType.getNamespace())) { + throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY, "Invalid JCR event type: "+ eventType + ": Namespace mismatch."); + } + String eventName = eventType.getName(); + if (!EventUtil.isValidEventName(eventName)) { + throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY, "Invalid event type: "+eventName); + } + return EventUtil.getJcrEventType(eventName); + } + + /** + * Inner class EventBundle encapsulates an event bundle as + * recorded {@link SubscriptionImpl#onEvent(EventIterator) on event} and + * provides the possibility to retrieve the Xml representation of the + * bundle and the events included in order to respond to a POLL request. + * + * @see SubscriptionImpl#discoverEvents(long) + */ + private class EventBundleImpl implements EventBundle { + + private final EventIterator events; + + private final String transactionId; + + private EventBundleImpl(EventIterator events) { + this(events, null); + } + + private EventBundleImpl(EventIterator events, String transactionId) { + this.events = events; + this.transactionId = transactionId; + } + + public Element toXml(Document document) { + Element bundle = DomUtil.createElement(document, XML_EVENTBUNDLE, NAMESPACE); + // TODO: this appears to be unused now + if (transactionId != null) { + DomUtil.setAttribute(bundle, XML_EVENT_TRANSACTION_ID, NAMESPACE, transactionId); + } + + boolean localFlagSet = false; + + while (events.hasNext()) { + Event event = events.nextEvent(); + + if (!localFlagSet) { + // obtain remote session identifier + localFlagSet = true; + String name = JcrRemotingConstants.RELATION_REMOTE_SESSION_ID; + Object forSessionId = session.getAttribute(name); + // calculate "local" flags + if (forSessionId != null + && event instanceof AdditionalEventInfo) { + AdditionalEventInfo aei = (AdditionalEventInfo) event; + try { + boolean isLocal = forSessionId.equals( + aei.getSessionAttribute(name)); + DomUtil.setAttribute( + bundle, XML_EVENT_LOCAL, null, + Boolean.toString(isLocal)); + } catch (UnsupportedRepositoryOperationException ex) { + // optional feature + } + } + } + + Element eventElem = DomUtil.addChildElement(bundle, XML_EVENT, NAMESPACE); + // href + String eHref = ""; + try { + boolean isCollection = (event.getType() == Event.NODE_ADDED || event.getType() == Event.NODE_REMOVED); + eHref = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), event.getPath(), false).getHref(isCollection); + } catch (RepositoryException e) { + // should not occur.... + log.error(e.getMessage()); + } + eventElem.appendChild(DomUtil.hrefToXml(eHref, document)); + // event type + Element eType = DomUtil.addChildElement(eventElem, XML_EVENTTYPE, NAMESPACE); + eType.appendChild(getEventType(event.getType()).toXml(document)); + // user id + DomUtil.addChildElement(eventElem, XML_EVENTUSERID, NAMESPACE, event.getUserID()); + + // try to compute nodetype information + if (event instanceof AdditionalEventInfo) { + try { + DomUtil.addChildElement(eventElem, + XML_EVENTPRIMARNODETYPE, NAMESPACE, + ((AdditionalEventInfo) event) + .getPrimaryNodeTypeName().toString()); + for (Name mixin : ((AdditionalEventInfo) event) + .getMixinTypeNames()) { + DomUtil.addChildElement(eventElem, + XML_EVENTMIXINNODETYPE, NAMESPACE, + mixin.toString()); + } + } catch (UnsupportedRepositoryOperationException ex) { + // optional + } + } + + // Additional JCR 2.0 event information + // user data + try { + DomUtil.addChildElement(eventElem, XML_EVENTUSERDATA, NAMESPACE, event.getUserData()); + } catch (RepositoryException e) { + log.error("Internal error while retrieving event user data. {}", e.getMessage()); + } + // time stamp + try { + DomUtil.addChildElement(eventElem, XML_EVENTDATE, NAMESPACE, String.valueOf(event.getDate())); + } catch (RepositoryException e) { + log.error("Internal error while retrieving event date. {}", e.getMessage()); + } + // identifier + try { + DomUtil.addChildElement(eventElem, XML_EVENTIDENTIFIER, NAMESPACE, event.getIdentifier()); + } catch (RepositoryException e) { + log.error("Internal error while retrieving event identifier. {}", e.getMessage()); + } + // info + try { + serializeInfoMap(eventElem, session, event.getInfo()); + } catch (RepositoryException e) { + log.error("Internal error while retrieving event info. {}", e.getMessage()); + } + } + return bundle; + } + } + + protected static void serializeInfoMap(Element eventElem, Session session, Map map) { + // info + Element info = DomUtil.addChildElement(eventElem, XML_EVENTINFO, NAMESPACE); + Map m = map; + for (Map.Entry entry : m.entrySet()) { + try { + String key = entry.getKey().toString(); + Namespace ns = Namespace.EMPTY_NAMESPACE; + int colon = key.indexOf(':'); + if (colon >= 0) { + String prefix = key.substring(0, colon); + String localname = key.substring(colon + 1); + ns = Namespace.getNamespace(prefix, session.getNamespaceURI(prefix)); + key = localname; + } + Object value = entry.getValue(); + if (value != null) { + DomUtil.addChildElement(info, key, ns, value.toString()); + } else { + DomUtil.addChildElement(info, key, ns); + } + } catch (RepositoryException nse) { + log.error("Internal error while getting namespaceUri, info map field skipped for {}", entry.getKey()); + } + } + } + + //----------------------------< TransactionEvent >------------------------ + + /** + * Implements a transaction event which listeners for events during a save + * call on the repository. + */ + private class TransactionEvent implements EventListener, TransactionListener { + + private String transactionId; + + /** + * {@inheritDoc} + */ + public void onEvent(EventIterator events) { + String tId = transactionId; + if (tId == null) { + tId = UUID.randomUUID().toString(); + } + synchronized (SubscriptionImpl.this) { + eventBundles.add(new EventBundleImpl(events, tId)); + SubscriptionImpl.this.notifyAll(); + } + } + + //-----------------------------< TransactionListener >------------------ + + /** + * {@inheritDoc} + */ + public void beforeCommit(TransactionResource resource, String lockToken) { + try { + transactionId = lockToken; + obsMgr.addEventListener(this, getJcrEventTypes(), + getLocator().getRepositoryPath(), isDeep(), getUuidFilters(), + getNodetypeNameFilters(), isNoLocal()); + // suspend the subscription + suspend(); + } catch (RepositoryException e) { + log.warn("Unable to register TransactionListener: " + e); + } catch (DavException e) { + log.warn("Unable to register TransactionListener: " + e); + } + } + + /** + * {@inheritDoc} + */ + public void afterCommit(TransactionResource resource, + String lockToken, + boolean success) { + try { + // resume the subscription + resume(); + // remove this transaction event + obsMgr.removeEventListener(this); + } catch (RepositoryException e) { + log.warn("Unable to remove listener: " + e); + } catch (DavException e) { + log.warn("Unable to resume Subscription: " + e); + } + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/observation/SubscriptionManagerImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/observation/SubscriptionManagerImpl.java new file mode 100644 index 00000000000..7cf13b5438c --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/observation/SubscriptionManagerImpl.java @@ -0,0 +1,358 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.observation; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.transaction.TransactionResource; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.jcr.transaction.TransactionListener; +import org.apache.jackrabbit.webdav.observation.EventDiscovery; +import org.apache.jackrabbit.webdav.observation.ObservationResource; +import org.apache.jackrabbit.webdav.observation.Subscription; +import org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery; +import org.apache.jackrabbit.webdav.observation.SubscriptionInfo; +import org.apache.jackrabbit.webdav.observation.SubscriptionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.w3c.dom.Document; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.ObservationManager; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; + +/** + * SubscriptionManager collects all subscriptions requested, handles + * the subscription timeout and provides METHODS to discover subscriptions + * present on a given resource as well as events for an specific subscription. + */ +// todo: make sure all expired subscriptions are removed! +public class SubscriptionManagerImpl implements SubscriptionManager, TransactionListener { + + private static Logger log = LoggerFactory.getLogger(SubscriptionManagerImpl.class); + + /** + * Map containing all {@link org.apache.jackrabbit.webdav.observation.Subscription subscriptions}. + */ + private final SubscriptionMap subscriptions = new SubscriptionMap(); + + private final Map> transactionListenerById = new HashMap>(); + + /** + * Retrieve the {@link org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery} + * object for the given resource. Note, that the discovery object will be empty + * if there are no subscriptions present.
        + * Note that all subscriptions present on the given resource are returned. + * However, the subscription id will not be visible in order to avoid abuse + * by clients not having registered the subscription originally. + * + * @param resource + */ + public SubscriptionDiscovery getSubscriptionDiscovery(ObservationResource resource) { + Subscription[] subsForResource = subscriptions.getByPath(resource.getLocator()); + return new SubscriptionDiscovery(subsForResource); + } + + /** + * Create a new Subscription or update an existing Subscription + * and add it as eventlistener to the {@link javax.jcr.observation.ObservationManager}. + * + * @param info + * @param subscriptionId + * @param resource + * @return Subscription that has been added to the {@link javax.jcr.observation.ObservationManager} + * @throws DavException if the subscription fails + */ + public Subscription subscribe(SubscriptionInfo info, String subscriptionId, + ObservationResource resource) + throws DavException { + + Subscription subscription; + if (subscriptionId == null) { + // new subscription + SubscriptionImpl newSubs = new SubscriptionImpl(info, resource); + registerSubscription(newSubs, resource); + + // adjust references to this subscription + subscriptions.put(newSubs.getSubscriptionId(), newSubs); + resource.getSession().addReference(newSubs.getSubscriptionId()); + subscription = newSubs; + } else { + // refresh/modify existing one + SubscriptionImpl existing = validate(subscriptionId, resource); + existing.setInfo(info); + registerSubscription(existing, resource); + + subscription = new WrappedSubscription(existing); + } + return subscription; + } + + /** + * Register the event listener defined by the given subscription to the + * repository's observation manager. + * + * @param subscription + * @param resource + * @throws DavException + */ + private void registerSubscription(SubscriptionImpl subscription, + ObservationResource resource) throws DavException { + try { + Session session = getRepositorySession(resource); + ObservationManager oMgr = session.getWorkspace().getObservationManager(); + String itemPath = subscription.getLocator().getRepositoryPath(); + oMgr.addEventListener(subscription, subscription.getJcrEventTypes(), + itemPath, subscription.isDeep(), + subscription.getUuidFilters(), + subscription.getNodetypeNameFilters(), + subscription.isNoLocal()); + } catch (RepositoryException e) { + log.error("Unable to register eventlistener: "+e.getMessage()); + throw new JcrDavException(e); + } + } + + /** + * Unsubscribe the Subscription with the given id and remove it + * from the {@link javax.jcr.observation.ObservationManager} as well as + * from the internal map. + * + * @param subscriptionId + * @param resource + * @throws DavException + */ + public void unsubscribe(String subscriptionId, ObservationResource resource) + throws DavException { + + SubscriptionImpl subs = validate(subscriptionId, resource); + unregisterSubscription(subs, resource); + } + + /** + * Remove the event listener defined by the specified subscription from + * the repository's observation manager and clean up the references present + * on the DavSession. + * + * @param subscription + * @param resource + * @throws DavException + */ + private void unregisterSubscription(SubscriptionImpl subscription, + ObservationResource resource) throws DavException { + try { + Session session = getRepositorySession(resource); + session.getWorkspace().getObservationManager().removeEventListener(subscription); + String sId = subscription.getSubscriptionId(); + + // clean up any references + subscriptions.remove(sId); + resource.getSession().removeReference(sId); + + } catch (RepositoryException e) { + log.error("Unable to remove eventlistener: "+e.getMessage()); + throw new JcrDavException(e); + } + } + + /** + * Retrieve all event bundles accumulated since for the subscription specified + * by the given id. + * + * @param subscriptionId + * @param timeout timeout in milliseconds + * @param resource + * @return object encapsulating the events. + */ + public EventDiscovery poll(String subscriptionId, long timeout, ObservationResource resource) + throws DavException { + + SubscriptionImpl subs = validate(subscriptionId, resource); + return subs.discoverEvents(timeout); + } + + /** + * Validate the given subscription id. The validation will fail under the following + * conditions:
          + *
        • The subscription with the given id does not exist,
        • + *
        • DavResource path does not match the subscription id,
        • + *
        • The subscription with the given id is already expired.
        • + *
        + * + * @param subscriptionId + * @param resource + * @return Subscription with the given id. + * @throws DavException if an error occurred while retrieving the Subscription + */ + private SubscriptionImpl validate(String subscriptionId, ObservationResource resource) + throws DavException { + + SubscriptionImpl subs; + if (subscriptions.contains(subscriptionId)) { + subs = subscriptions.get(subscriptionId); + if (!subs.isSubscribedToResource(resource)) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to operate on subscription with invalid resource path."); + } + if (subs.isExpired()) { + unregisterSubscription(subs, resource); + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to operate on expired subscription."); + } + return subs; + } else { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to modify or to poll for non-existing subscription."); + } + } + + /** + * @param resource + * @return JCR session + */ + private static Session getRepositorySession(ObservationResource resource) throws DavException { + return JcrDavSession.getRepositorySession(resource.getSession()); + } + + //---------------------------< TransactionListener >------------------------ + + /** + * {@inheritDoc} + */ + public synchronized void beforeCommit(TransactionResource resource, + String lockToken) { + // suspend regular subscriptions during a commit + List transactionListeners = new ArrayList(); + for (Iterator it = subscriptions.iterator(); it.hasNext(); ) { + SubscriptionImpl sub = it.next(); + TransactionListener tl = sub.createTransactionListener(); + tl.beforeCommit(resource, lockToken); + transactionListeners.add(tl); + } + transactionListenerById.put(lockToken, transactionListeners); + } + + /** + * {@inheritDoc} + */ + public void afterCommit(TransactionResource resource, String lockToken, boolean success) { + List transactionListeners = transactionListenerById.remove(lockToken); + if (transactionListeners != null) { + for (TransactionListener txListener : transactionListeners) { + txListener.afterCommit(resource, lockToken, success); + } + } + } + + //----------------------------------------------< private inner classes >--- + /** + * Private inner class wrapping around an Subscription as + * present in the internal map. This allows to hide the subscription Id + * from other sessions, that did create the subscription. + */ + private static class WrappedSubscription implements Subscription { + + private final Subscription delegatee; + + private WrappedSubscription(Subscription subsc) { + this.delegatee = subsc; + } + + public String getSubscriptionId() { + // always return null, since the subscription id must not be exposed + // but to the client, that created the subscription. + return null; + } + + public Element toXml(Document document) { + return delegatee.toXml(document); + } + + public boolean eventsProvideNodeTypeInformation() { + return delegatee.eventsProvideNodeTypeInformation(); + } + + public boolean eventsProvideNoLocalFlag() { + return delegatee.eventsProvideNoLocalFlag(); + } + } + + /** + * Private inner class SubscriptionMap that allows for quick + * access by resource path as well as by subscription id. + */ + private class SubscriptionMap { + + private HashMap subscriptions = new HashMap(); + private HashMap> ids = new HashMap>(); + + private boolean contains(String subscriptionId) { + return subscriptions.containsKey(subscriptionId); + } + + private SubscriptionImpl get(String subscriptionId) { + return subscriptions.get(subscriptionId); + } + + private Iterator iterator() { + return subscriptions.values().iterator(); + } + + private void put(String subscriptionId, SubscriptionImpl subscription) { + subscriptions.put(subscriptionId, subscription); + DavResourceLocator key = subscription.getLocator(); + Set idSet; + if (ids.containsKey(key)) { + idSet = ids.get(key); + } else { + idSet = new HashSet(); + ids.put(key, idSet); + } + if (!idSet.contains(subscriptionId)) { + idSet.add(subscriptionId); + } + } + + private void remove(String subscriptionId) { + SubscriptionImpl sub = subscriptions.remove(subscriptionId); + ids.get(sub.getLocator()).remove(subscriptionId); + } + + private Subscription[] getByPath(DavResourceLocator locator) { + Set idSet = ids.get(locator); + if (idSet != null && !idSet.isEmpty()) { + Subscription[] subsForResource = new Subscription[idSet.size()]; + int i = 0; + for (String id : idSet) { + SubscriptionImpl s = subscriptions.get(id); + subsForResource[i] = new WrappedSubscription(s); + i++; + } + return subsForResource; + } else { + return new Subscription[0]; + } + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/JcrDavPropertyNameSet.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/JcrDavPropertyNameSet.java new file mode 100644 index 00000000000..480212bcf4d --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/JcrDavPropertyNameSet.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.webdav.jcr.property; + +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.apache.jackrabbit.webdav.version.VersionControlledResource; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.VersionResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JcrDavPropertyNameSet... + */ +public final class JcrDavPropertyNameSet implements ItemResourceConstants { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(JcrDavPropertyNameSet.class); + + + //------------------------------------------------< property name sets >---- + /** + * Default property names present with all resources. + */ + public static final DavPropertyNameSet BASE_SET = new DavPropertyNameSet(); + static { + BASE_SET.add(DavPropertyName.DISPLAYNAME); + BASE_SET.add(DavPropertyName.RESOURCETYPE); + BASE_SET.add(DavPropertyName.ISCOLLECTION); + BASE_SET.add(DavPropertyName.GETLASTMODIFIED); + BASE_SET.add(DavPropertyName.CREATIONDATE); + BASE_SET.add(DavPropertyName.SUPPORTEDLOCK); + BASE_SET.add(DavPropertyName.LOCKDISCOVERY); + BASE_SET.add(DeltaVConstants.SUPPORTED_METHOD_SET); + BASE_SET.add(DeltaVConstants.SUPPORTED_REPORT_SET); + BASE_SET.add(DeltaVConstants.CREATOR_DISPLAYNAME); + BASE_SET.add(DeltaVConstants.COMMENT); + BASE_SET.add(JCR_WORKSPACE_NAME); + } + + /** + * Property names defined for JCR workspace resources. + */ + public static final DavPropertyNameSet WORKSPACE_SET = new DavPropertyNameSet(); + static { + WORKSPACE_SET.add(DeltaVConstants.WORKSPACE); + WORKSPACE_SET.add(JCR_NAMESPACES); + WORKSPACE_SET.add(JCR_NODETYPES_CND); + } + + /** + * Additional property names defined for existing and non-existing item resources. + */ + public static final DavPropertyNameSet ITEM_BASE_SET = new DavPropertyNameSet(); + static { + ITEM_BASE_SET.add(DavPropertyName.GETCONTENTTYPE); + ITEM_BASE_SET.add(DeltaVConstants.WORKSPACE); + ITEM_BASE_SET.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + } + + /** + * Additional property names defined for existing item resources. + */ + public static final DavPropertyNameSet EXISTING_ITEM_BASE_SET = new DavPropertyNameSet(ITEM_BASE_SET); + static { + EXISTING_ITEM_BASE_SET.add(JCR_NAME); + EXISTING_ITEM_BASE_SET.add(JCR_PATH); + EXISTING_ITEM_BASE_SET.add(JCR_DEPTH); + EXISTING_ITEM_BASE_SET.add(JCR_DEFINITION); + } + + /** + * Additional property names defined by single value JCR properties. + */ + public static final DavPropertyNameSet PROPERTY_SET = new DavPropertyNameSet(); + static { + PROPERTY_SET.add(JCR_TYPE); + PROPERTY_SET.add(JCR_VALUE); + PROPERTY_SET.add(JCR_LENGTH); + } + + /** + * Additional property names defined by single value JCR properties. + */ + public static final DavPropertyNameSet PROPERTY_MV_SET = new DavPropertyNameSet(); + static { + PROPERTY_MV_SET.add(JCR_TYPE); + PROPERTY_MV_SET.add(JCR_VALUES); + PROPERTY_MV_SET.add(JCR_LENGTHS); + } + + /** + * Additional property names defined by regular JCR nodes. + */ + public static final DavPropertyNameSet NODE_SET = new DavPropertyNameSet(); + static { + NODE_SET.add(JCR_PRIMARYNODETYPE); + NODE_SET.add(JCR_MIXINNODETYPES); + NODE_SET.add(JCR_INDEX); + NODE_SET.add(JCR_REFERENCES); + NODE_SET.add(JCR_WEAK_REFERENCES); + } + + /** + * Additional property names defined by versionable JCR nodes. + */ + public static final DavPropertyNameSet VERSIONABLE_SET = new DavPropertyNameSet(); + static { + VERSIONABLE_SET.add(VersionControlledResource.VERSION_HISTORY); + VERSIONABLE_SET.add(VersionControlledResource.AUTO_VERSION); + } + + /** + * Additional property names defined by JCR version nodes. + */ + public static final DavPropertyNameSet VERSION_SET = new DavPropertyNameSet(); + static { + VERSION_SET.add(VersionResource.VERSION_NAME); + VERSION_SET.add(VersionResource.LABEL_NAME_SET); + VERSION_SET.add(VersionResource.PREDECESSOR_SET); + VERSION_SET.add(VersionResource.SUCCESSOR_SET); + VERSION_SET.add(VersionResource.VERSION_HISTORY); + VERSION_SET.add(VersionResource.CHECKOUT_SET); + } + + /** + * Additional property names defined by JCR version history nodes. + */ + public static final DavPropertyNameSet VERSIONHISTORY_SET = new DavPropertyNameSet(); + static { + VERSIONHISTORY_SET.add(VersionHistoryResource.ROOT_VERSION); + VERSIONHISTORY_SET.add(VersionHistoryResource.VERSION_SET); + VERSIONHISTORY_SET.add(JCR_VERSIONABLEUUID); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/LengthsProperty.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/LengthsProperty.java new file mode 100644 index 00000000000..fb1ef82a90b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/LengthsProperty.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.webdav.jcr.property; + +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * LengthsProperty extends {@link org.apache.jackrabbit.webdav.property.DavProperty} providing + * utilities to handle the multiple lengths of the property item represented + * by this resource. + */ +public class LengthsProperty extends AbstractDavProperty implements ItemResourceConstants { + + private final long[] value; + + /** + * Create a new LengthsProperty from the given long array. + * + * @param lengths as retrieved from the JCR property + */ + public LengthsProperty(long[] lengths) { + super(JCR_LENGTHS, true); + this.value = lengths; + } + + /** + * Returns an array of {@link long}s representing the value of this + * property. + * + * @return an array of {@link long}s + */ + public long[] getValue() { + return value; + } + + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + for (long length : value) { + String txtContent = String.valueOf(length); + DomUtil.addChildElement(elem, XML_LENGTH, ItemResourceConstants.NAMESPACE, txtContent); + } + return elem; + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/NamespacesProperty.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/NamespacesProperty.java new file mode 100644 index 00000000000..bdb31ca2406 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/NamespacesProperty.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.property; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; + +/** + * NamespacesProperty... + */ +public class NamespacesProperty extends AbstractDavProperty> implements ItemResourceConstants { + + private static Logger log = LoggerFactory.getLogger(NamespacesProperty.class); + + private final Map value = new HashMap(); + + public NamespacesProperty(NamespaceRegistry nsReg) throws RepositoryException { + super(JCR_NAMESPACES, false); + if (nsReg != null) { + for (String prefix : nsReg.getPrefixes()) { + value.put(prefix, nsReg.getURI(prefix)); + } + } + } + + public NamespacesProperty(Map namespaces) { + super(JCR_NAMESPACES, false); + value.putAll(namespaces); + } + + public NamespacesProperty(DavProperty property) throws DavException { + super(JCR_NAMESPACES, false); + Object v = property.getValue(); + if (!(v instanceof List)) { + log.warn("Unexpected structure of dcr:namespace property."); + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + // retrieve list of prefix/uri pairs that build the new values of + // the ns-registry + for (Object listEntry : (List) v) { + if (listEntry instanceof Element) { + Element e = (Element)listEntry; + if (XML_NAMESPACE.equals(e.getLocalName())) { + Element pElem = DomUtil.getChildElement(e, XML_PREFIX, ItemResourceConstants.NAMESPACE); + String prefix = DomUtil.getText(pElem, Namespace.EMPTY_NAMESPACE.getPrefix()); + Element uElem = DomUtil.getChildElement(e, XML_URI, ItemResourceConstants.NAMESPACE); + String uri = DomUtil.getText(uElem, Namespace.EMPTY_NAMESPACE.getURI()); + value.put(prefix, uri); + } + } + } + } + + public Map getNamespaces() { + return Collections.unmodifiableMap(value); + } + + public Map getValue() { + return Collections.unmodifiableMap(value); + } + + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + for (String prefix : value.keySet()) { + String uri = value.get(prefix); + Element nsElem = DomUtil.addChildElement(elem, XML_NAMESPACE, ItemResourceConstants.NAMESPACE); + DomUtil.addChildElement(nsElem, XML_PREFIX, ItemResourceConstants.NAMESPACE, prefix); + DomUtil.addChildElement(nsElem, XML_URI, ItemResourceConstants.NAMESPACE, uri); + } + return elem; + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/ValuesProperty.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/ValuesProperty.java new file mode 100644 index 00000000000..716048b6286 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/property/ValuesProperty.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.webdav.jcr.property; + +import org.apache.jackrabbit.commons.webdav.ValueUtil; +import org.apache.jackrabbit.value.ValueHelper; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.ValueFactory; + +/** + * ValuesProperty implements {@link org.apache.jackrabbit.webdav.property.DavProperty} + * providing utilities to handle the value(s) of a JCR property item resource. + * In case the property is multivalued the DavProperty value consists of + * an element named {@link #JCR_VALUES} otherwise its name is {@link #JCR_VALUE}. + */ +public class ValuesProperty extends AbstractDavProperty implements ItemResourceConstants { + + private static Logger log = LoggerFactory.getLogger(ValuesProperty.class); + + private final Value[] jcrValues; + + /** + * Create a new ValuesProperty from the given single {@link Value}. + * + * @param value Array of Value objects as obtained from the JCR property. + */ + public ValuesProperty(Value value) { + super(JCR_VALUE, false); + // finally set the value to the DavProperty + jcrValues = (value == null) ? new Value[0] : new Value[] {value}; + } + + /** + * Create a new ValuesProperty from the given {@link javax.jcr.Value Value + * array}. + * + * @param values Array of Value objects as obtained from the JCR property. + */ + public ValuesProperty(Value[] values) { + super(JCR_VALUES, false); + // finally set the value to the DavProperty + jcrValues = (values == null) ? new Value[0] : values; + } + + /** + * Wrap the specified DavProperty in a new ValuesProperty. + * + * @param property + * @param defaultType default type of the values to be deserialized. If however + * the {@link #XML_VALUE 'value'} elements provide a {@link #ATTR_VALUE_TYPE 'type'} + * attribute, the default value is ignored. + * @param valueFactory Factory used to retrieve JCR values from the value + * of the given DavProperty. + */ + public ValuesProperty(DavProperty property, int defaultType, + ValueFactory valueFactory) + throws RepositoryException, DavException { + super(property.getName(), false); + + if (!(JCR_VALUES.equals(property.getName()) || JCR_VALUE.equals(getName()))) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "ValuesProperty may only be created with a property that has name="+JCR_VALUES.getName()); + } + + jcrValues = ValueUtil.valuesFromXml(property.getValue(), defaultType, valueFactory); + } + + private void checkPropertyName(DavPropertyName reqName) throws ValueFormatException { + if (!reqName.equals(getName())) { + throw new ValueFormatException("Attempt to retrieve multiple values from single property '" + getName() + "'."); + } + } + + /** + * Converts the value of this property to a {@link javax.jcr.Value value array}. + * + * @return Array of Value objects + * @throws ValueFormatException if converting the internal jcr values to + * the specified value type fails. + */ + public Value[] getJcrValues(int propertyType, ValueFactory valueFactory) throws ValueFormatException { + checkPropertyName(JCR_VALUES); + Value[] vs = new Value[jcrValues.length]; + for (int i = 0; i < jcrValues.length; i++) { + vs[i] = ValueHelper.convert(jcrValues[i], propertyType, valueFactory); + } + return vs; + } + + /** + * Returns the internal property value as jcr Value array + * + * @return the internal property value as jcr Value array + */ + public Value[] getJcrValues() throws ValueFormatException { + checkPropertyName(JCR_VALUES); + return jcrValues; + } + + /** + * + * @param propertyType + * @return + * @throws ValueFormatException + */ + public Value getJcrValue(int propertyType, ValueFactory valueFactory) throws ValueFormatException { + checkPropertyName(JCR_VALUE); + return (jcrValues.length == 0) + ? null + : ValueHelper.convert(jcrValues[0], propertyType, valueFactory); + } + + /** + * + * @return + * @throws ValueFormatException + */ + public Value getJcrValue() throws ValueFormatException { + checkPropertyName(JCR_VALUE); + return (jcrValues.length == 0) ? null : jcrValues[0]; + } + + /** + * Returns the type of the {@link Value value}s present in this property + * or {@link PropertyType#UNDEFINED} if no values are available. + * + * @return type of values or {@link PropertyType#UNDEFINED} + */ + public int getValueType() { + // TODO: check if correct behaviour if values array is empty + return (jcrValues.length > 0) ? jcrValues[0].getType() : PropertyType.UNDEFINED; + } + + /** + * Returns an array of {@link Value}s representing the value of this + * property. + * + * @return an array of {@link Value}s + * @see #getJcrValues() + * @see #getJcrValue() + */ + public Value[] getValue() { + return jcrValues; + } + + /** + * + * @param document + * @return the xml element + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + try { + for (Value v : jcrValues) { + Element xmlValue = ValueUtil.valueToXml(v, document); + elem.appendChild(xmlValue); + } + } catch (RepositoryException e) { + log.error("Unexpected Error while converting jcr value to String: " + e.getMessage()); + } + return elem; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/search/SearchResourceImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/search/SearchResourceImpl.java new file mode 100644 index 00000000000..304a0ecf340 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/search/SearchResourceImpl.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.search; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.search.QueryGrammerSet; +import org.apache.jackrabbit.webdav.search.SearchInfo; +import org.apache.jackrabbit.webdav.search.SearchResource; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.util.ISO9075; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.NamespaceRegistry; +import javax.jcr.ValueFactory; +import javax.jcr.PropertyType; +import javax.jcr.query.InvalidQueryException; +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 java.util.Map; +import java.util.List; +import java.util.ArrayList; + +/** + * SearchResourceImpl... + */ +public class SearchResourceImpl implements SearchResource { + + private static Logger log = LoggerFactory.getLogger(SearchResourceImpl.class); + + private final JcrDavSession session; + private final DavResourceLocator locator; + + public SearchResourceImpl(DavResourceLocator locator, JcrDavSession session) { + this.session = session; + this.locator = locator; + } + + //-------------------------------------------< SearchResource interface >--- + /** + * @see SearchResource#getQueryGrammerSet() + */ + public QueryGrammerSet getQueryGrammerSet() { + QueryGrammerSet qgs = new QueryGrammerSet(); + try { + QueryManager qMgr = getRepositorySession().getWorkspace().getQueryManager(); + String[] langs = qMgr.getSupportedQueryLanguages(); + for (String lang : langs) { + // Note: Existing clients already assume that the + // query languages returned in the DASL header are + // not prefixed with any namespace, so we probably + // shouldn't use an explicit namespace here. + qgs.addQueryLanguage(lang, Namespace.EMPTY_NAMESPACE); + } + } catch (RepositoryException e) { + log.debug(e.getMessage()); + } + return qgs; + } + + /** + * Execute the query defined by the given sInfo. + * + * @see SearchResource#search(org.apache.jackrabbit.webdav.search.SearchInfo) + */ + public MultiStatus search(SearchInfo sInfo) throws DavException { + try { + QueryResult result = getQuery(sInfo).execute(); + + MultiStatus ms = new MultiStatus(); + + if (ItemResourceConstants.NAMESPACE.equals( + sInfo.getLanguageNameSpace())) { + ms.setResponseDescription( + "Columns: " + encode(result.getColumnNames()) + + "\nSelectors: " + encode(result.getSelectorNames())); + } else { + ms.setResponseDescription(encode(result.getColumnNames())); + } + + queryResultToMultiStatus(result, ms); + + return ms; + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Create a query from the information present in the sInfo + * object.
        The following JCR specific logic is applied: + *
          + *
        • If the requested resource represents a node with nodetype nt:query, the + * request body is ignored and the query defined with the node is executed + * instead.
        • + *
        • If the requested resource does not represent an existing item, the + * specified query is persisted by calling {@link Query#storeAsNode(String)}.
        • + *
        + * @param sInfo defining the query to be executed + * @return Query object. + * @throws javax.jcr.query.InvalidQueryException if the query defined by sInfo is invalid + * @throws RepositoryException the query manager cannot be accessed or if + * another error occurs. + * @throws DavException if sInfo is null and + * the underlying repository item is not an nt:query node or if an error + * occurs when calling {@link Query#storeAsNode(String)}/ + */ + private Query getQuery(SearchInfo sInfo) + throws InvalidQueryException, RepositoryException, DavException { + + Session session = getRepositorySession(); + NamespaceRegistry nsReg = session.getWorkspace().getNamespaceRegistry(); + Node rootNode = session.getRootNode(); + QueryManager qMgr = getRepositorySession().getWorkspace().getQueryManager(); + + // test if query is defined by requested repository node + String itemPath = locator.getRepositoryPath(); + if (itemPath != null && !rootNode.getPath().equals(itemPath)) { + String qNodeRelPath = itemPath.substring(1); + if (rootNode.hasNode(qNodeRelPath)) { + Node qNode = rootNode.getNode(qNodeRelPath); + if (qNode.isNodeType(JcrConstants.NT_QUERY)) { + return qMgr.getQuery(qNode); + } + } + } + + Query q; + if (sInfo != null) { + // apply namespace mappings to session + Map namespaces = sInfo.getNamespaces(); + try { + for (Map.Entry entry : namespaces.entrySet()) { + String prefix = entry.getKey(); + String uri = entry.getValue(); + session.setNamespacePrefix(prefix, uri); + } + q = qMgr.createQuery(sInfo.getQuery(), sInfo.getLanguageName()); + + if (SearchInfo.NRESULTS_UNDEFINED != sInfo.getNumberResults()) { + q.setLimit(sInfo.getNumberResults()); + } + if (SearchInfo.OFFSET_UNDEFINED != sInfo.getOffset()) { + q.setOffset(sInfo.getOffset()); + } + } finally { + // reset namespace mappings + for (String uri : namespaces.values()) { + try { + session.setNamespacePrefix(nsReg.getPrefix(uri), uri); + } catch (RepositoryException e) { + log.warn("Unable to reset mapping of namespace: " + uri); + } + } + } + } else { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, locator.getResourcePath() + " is not a nt:query node -> searchRequest body required."); + } + + /* test if resource path does not exist -> thus indicating that + the query must be made persistent by calling Query.save(String) */ + if (itemPath != null && !getRepositorySession().itemExists(itemPath)) { + try { + q.storeAsNode(itemPath); + } catch (RepositoryException e) { + // ItemExistsException should never occur. + throw new JcrDavException(e); + } + } + return q; + } + + /** + * Build a MultiStatus object from the specified query result. + * + * @param query the query to execute. + * @return MultiStatus object listing the query result in + * Webdav compatible form. + * @throws RepositoryException if an error occurs. + */ + private void queryResultToMultiStatus(QueryResult result, MultiStatus ms) + throws RepositoryException { + List columnNames = new ArrayList(); + + ValueFactory vf = getRepositorySession().getValueFactory(); + List descr = new ArrayList(); + for (String columnName : result.getColumnNames()) { + if (!isPathOrScore(columnName)) { + columnNames.add(columnName); + descr.add(new PlainValue(columnName, null, vf)); + } + } + + // add path and score for each selector + String[] sns = result.getSelectorNames(); + boolean join = sns.length > 1; + for (String selectorName : sns) { + descr.add(new PathValue(JcrConstants.JCR_PATH, selectorName, vf)); + columnNames.add(JcrConstants.JCR_PATH); + descr.add(new ScoreValue(JcrConstants.JCR_SCORE, selectorName, vf)); + columnNames.add(JcrConstants.JCR_SCORE); + } + + int n = 0; + String root = getHref("/"); + String[] selectorNames = createSelectorNames(descr); + String[] colNames = columnNames.toArray(new String[columnNames.size()]); + RowIterator rowIter = result.getRows(); + while (rowIter.hasNext()) { + Row row = rowIter.nextRow(); + List values = new ArrayList(); + for (RowValue rv : descr) { + values.add(rv.getValue(row)); + } + + // create a new ms-response for this row of the result set + String href; + if (join) { + // We need a distinct href for each join result row to + // allow the MultiStatus response to keep them separate + href = root + "?" + n++; + } else { + href = getHref(row.getPath()); + } + MultiStatusResponse resp = new MultiStatusResponse(href, null); + + // build the s-r-property + SearchResultProperty srp = new SearchResultProperty(colNames, + selectorNames, values.toArray(new Value[values.size()])); + resp.add(srp); + ms.addResponse(resp); + } + } + + /** + * Returns the resource location of the given query result row. + * The result rows of join queries have no meaningful single resource + * location, so we'll just default to the root node for all such rows. + * + * @param row query result row + * @param join flag to indicate a join query + * @return resource location of the row + */ + private String getHref(String path) throws RepositoryException { + DavResourceLocator l = locator.getFactory().createResourceLocator( + locator.getPrefix(), locator.getWorkspacePath(), path, false); + return l.getHref(true); + } + + private String encode(String[] names) { + StringBuilder builder = new StringBuilder(); + String delim = ""; + for (String name : names) { + builder.append(delim); + builder.append(ISO9075.encode(name)); + delim = " "; + } + return builder.toString(); + } + + private static String[] createSelectorNames(Iterable rows) + throws RepositoryException { + List sn = new ArrayList(); + for (RowValue rv : rows) { + sn.add(rv.getSelectorName()); + } + return sn.toArray(new String[sn.size()]); + } + + /** + * @param columnName a column name. + * @return true if columnName is either + * jcr:path or jcr:score; + * false otherwise. + */ + private static boolean isPathOrScore(String columnName) { + return JcrConstants.JCR_PATH.equals(columnName) + || JcrConstants.JCR_SCORE.equals(columnName); + } + + /** + * @return the session associated with this resource. + */ + private Session getRepositorySession() { + return session.getRepositorySession(); + } + + private interface RowValue { + + public Value getValue(Row row) throws RepositoryException; + + public String getColumnName() throws RepositoryException; + + public String getSelectorName() throws RepositoryException; + } + + private static final class PlainValue extends SelectorValue { + + public PlainValue(String columnName, + String selectorName, + ValueFactory vf) { + super(columnName, selectorName, vf); + } + + public Value getValue(Row row) throws RepositoryException { + return row.getValue(columnName); + } + } + + private static abstract class SelectorValue implements RowValue { + + protected final String columnName; + + protected final String selectorName; + + protected final ValueFactory vf; + + public SelectorValue(String columnName, + String selectorName, + ValueFactory vf) { + this.columnName = columnName; + this.selectorName = selectorName; + this.vf = vf; + } + + public String getColumnName() throws RepositoryException { + return columnName; + } + + public String getSelectorName() throws RepositoryException { + return selectorName; + } + } + + private static final class ScoreValue extends SelectorValue { + + public ScoreValue(String columnName, + String selectorName, + ValueFactory vf) { + super(columnName, selectorName, vf); + } + + public Value getValue(Row row) throws RepositoryException { + double score; + if (selectorName != null) { + score = row.getScore(selectorName); + } else { + score = row.getScore(); + } + return vf.createValue(score); + } + } + + private static final class PathValue extends SelectorValue { + + public PathValue(String columnName, + String selectorName, + ValueFactory vf) { + super(columnName, selectorName, vf); + } + + public Value getValue(Row row) throws RepositoryException { + String path; + if (selectorName != null) { + path = row.getPath(selectorName); + } else { + path = row.getPath(); + } + return (path == null) ? null : vf.createValue(path, PropertyType.PATH); + } + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/search/SearchResultProperty.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/search/SearchResultProperty.java new file mode 100644 index 00000000000..cc974ae4cc4 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/search/SearchResultProperty.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.search; + +import org.apache.jackrabbit.commons.webdav.QueryUtil; +import org.apache.jackrabbit.value.ValueHelper; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import java.util.ArrayList; +import java.util.List; + +/** + * SearchResultProperty... + */ +// todo: find proper solution for transporting search results... +public class SearchResultProperty extends AbstractDavProperty implements ItemResourceConstants { + + private static Logger log = LoggerFactory.getLogger(SearchResultProperty.class); + + private static final DavPropertyName SEARCH_RESULT_PROPERTY = DavPropertyName.create(JCR_QUERY_RESULT_LN, ItemResourceConstants.NAMESPACE); + + private final String[] columnNames; + private final String[] selectorNames; + private final Value[] values; + + /** + * Creates a new SearchResultProperty. + * + * @param columnNames the column names of the search row represented by + * this dav property. + * @param selectorNames the selector names of the row represented by this + * dav property. + * @param values the values present in the columns + */ + public SearchResultProperty(String[] columnNames, + String[] selectorNames, + Value[] values) { + super(SEARCH_RESULT_PROPERTY, true); + this.columnNames = columnNames; + this.selectorNames = selectorNames; + this.values = values; + } + + /** + * Wrap the specified DavProperty in a new SearchResultProperty. + * + * @param property + * @param valueFactory factory used to deserialize the xml value to a JCR value. + * @throws RepositoryException if an error occurs while build the property value + * @throws IllegalArgumentException if the specified property does have the + * required form. + * @see #getValues() + */ + public SearchResultProperty(DavProperty property, ValueFactory valueFactory) throws RepositoryException { + super(property.getName(), true); + if (!SEARCH_RESULT_PROPERTY.equals(getName())) { + throw new IllegalArgumentException("SearchResultProperty may only be created from a property named " + SEARCH_RESULT_PROPERTY.toString()); + } + + List colList = new ArrayList(); + List selList = new ArrayList(); + List valList = new ArrayList(); + + QueryUtil.parseResultPropertyValue(property.getValue(), colList, selList, valList, valueFactory); + + columnNames = colList.toArray(new String[colList.size()]); + selectorNames = selList.toArray(new String[selList.size()]); + values = valList.toArray(new Value[valList.size()]); + } + + /** + * Return the column names representing the names of the properties present + * in the {@link #getValues() values}. + * + * @return columnNames + */ + public String[] getColumnNames() { + return columnNames; + } + + /** + * @return the selector name for each of the columns in the result property. + */ + public String[] getSelectorNames() { + return selectorNames; + } + + /** + * Return the values representing the values of that row in the search + * result table. + * + * @return values + * @see javax.jcr.query.Row#getValues() + */ + public Value[] getValues() { + return values; + } + + + /** + * Same as {@link #getValues()} + * + * @return Array of JCR Value object + */ + public Value[] getValue() { + return values; + } + + /** + * Return the xml representation of this webdav property. For every value in + * the query result row a dcr:name, dcr:value, dcr:type and an optional + * dcr:selectorName element is created. + * Example: + *
        +     * -----------------------------------------------------------
        +     *   col-name  |   bla   |   bli   |  jcr:path  |  jcr:score
        +     * -----------------------------------------------------------
        +     *   value     |   xxx   |   111   |  /aNode    |    1
        +     *   type      |    1    |    3    |     8      |    3
        +     *   sel-name  |         |         |     S      |    S
        +     * -----------------------------------------------------------
        +     * 
        + * results in: + *
        +     * <dcr:search-result-property xmlns:dcr="http://www.day.com/jcr/webdav/1.0">
        +     *    <dcr:column>
        +     *       <dcr:name>bla<dcr:name/>
        +     *       <dcr:value dcr:type="String">xxx<dcr:value/>
        +     *    </dcr:column>
        +     *    <dcr:column>
        +     *       <dcr:name>bli<dcr:name/>
        +     *       <dcr:value dcr:type="Long">111<dcr:value/>
        +     *    </dcr:column>
        +     *    <dcr:column>
        +     *       <dcr:name>jcr:path<dcr:name/>
        +     *       <dcr:value dcr:type="Path">/aNode<dcr:value/>
        +     *       <dcr:selectorName>S<dcr:selectorName/>
        +     *    </dcr:column>
        +     *    <dcr:column>
        +     *       <dcr:name>jcr:score<dcr:name/>
        +     *       <dcr:value dcr:type="Long">1<dcr:value/>
        +     *       <dcr:selectorName>S<dcr:selectorName/>
        +     *    </dcr:column>
        +     * </dcr:search-result-property>
        +     * 
        + * + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(org.w3c.dom.Document) + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + for (int i = 0; i < columnNames.length; i++) { + String propertyName = columnNames[i]; + String selectorName = selectorNames[i]; + Value propertyValue = values[i]; + + Element columnEl = DomUtil.addChildElement(elem, XML_QUERY_RESULT_COLUMN, ItemResourceConstants.NAMESPACE); + DomUtil.addChildElement(columnEl, JCR_NAME.getName(), JCR_NAME.getNamespace(), propertyName); + if (propertyValue != null) { + try { + String serializedValue = ValueHelper.serialize(propertyValue, true); + Element xmlValue = DomUtil.addChildElement(columnEl, XML_VALUE, ItemResourceConstants.NAMESPACE, serializedValue); + String type = PropertyType.nameFromValue(propertyValue.getType()); + DomUtil.setAttribute(xmlValue, ATTR_VALUE_TYPE, ItemResourceConstants.NAMESPACE, type); + } catch (RepositoryException e) { + log.error(e.toString()); + } + } + if (selectorName != null) { + DomUtil.addChildElement(columnEl, JCR_SELECTOR_NAME.getName(), JCR_SELECTOR_NAME.getNamespace(), selectorName); + } + } + return elem; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/JcrSupportedPrivilegesProperty.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/JcrSupportedPrivilegesProperty.java new file mode 100644 index 00000000000..af8e870c86c --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/JcrSupportedPrivilegesProperty.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.webdav.jcr.security; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.security.SupportedPrivilege; +import org.apache.jackrabbit.webdav.security.SupportedPrivilegeSetProperty; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * JcrSupportedPrivilegesProperty... + */ +public class JcrSupportedPrivilegesProperty { + + private final Session session; + private final String absPath; + private final Set privileges = new HashSet(); + + private final Map supportedPrivileges = new HashMap(); + private final HashSet aggregated = new HashSet(); + + /** + * Build supported privileges for the jcr:all privilege. + * + * @param session The reading session + */ + public JcrSupportedPrivilegesProperty(Session session) throws RepositoryException { + this.session = session; + this.absPath = null; + AccessControlManager acMgr = session.getAccessControlManager(); + Privilege jcrAll = acMgr.privilegeFromName(Privilege.JCR_ALL); + privileges.add(jcrAll); + } + + /** + * @param session The reading session + * @param absPath An absolute path to an existing JCR node or {@code null}. + */ + public JcrSupportedPrivilegesProperty(Session session, String absPath) { + this.session = session; + this.absPath = absPath; + } + + /** + * Calculated the supported privileges at {@code absPath} and build a + * {@link org.apache.jackrabbit.webdav.security.SupportedPrivilegeSetProperty} + * from the result. + * + * @return a new {@code SupportedPrivilegeSetProperty} property. + * @throws RepositoryException + */ + public SupportedPrivilegeSetProperty asDavProperty() throws RepositoryException { + if (privileges.isEmpty()) { + AccessControlManager acMgr = session.getAccessControlManager(); + privileges.addAll(Arrays.asList(acMgr.getSupportedPrivileges(absPath))); + } + for (Privilege p : privileges) { + if (!aggregated.contains(p.getName())) { + createSupportedPrivilege(p); + } + } + return new SupportedPrivilegeSetProperty(supportedPrivileges.values().toArray(new SupportedPrivilege[supportedPrivileges.size()])); + } + + private SupportedPrivilege createSupportedPrivilege(Privilege privilege) throws RepositoryException { + String privilegeName = privilege.getName(); + + String localName = Text.getLocalName(privilegeName); + String prefix = Text.getNamespacePrefix(privilegeName); + Namespace ns = (prefix.isEmpty()) ? Namespace.EMPTY_NAMESPACE : Namespace.getNamespace(prefix, session.getNamespaceURI(prefix)); + org.apache.jackrabbit.webdav.security.Privilege davPrivilege = org.apache.jackrabbit.webdav.security.Privilege.getPrivilege(localName, ns); + + SupportedPrivilege[] aggregates = (privilege.isAggregate()) ? getDeclaredAggregates(privilege) : null; + + SupportedPrivilege sp = new SupportedPrivilege(davPrivilege, null, null, privilege.isAbstract(), aggregates); + if (!aggregated.contains(privilegeName)) { + supportedPrivileges.put(privilegeName, sp); + } + return sp; + } + + private SupportedPrivilege[] getDeclaredAggregates(Privilege privilege) throws RepositoryException { + List declAggr = new ArrayList(); + for (Privilege decl : privilege.getDeclaredAggregatePrivileges()) { + String name = decl.getName(); + if (aggregated.add(name)) { + if (supportedPrivileges.containsKey(name)) { + declAggr.add(supportedPrivileges.remove(name)); + } else { + declAggr.add(createSupportedPrivilege(decl)); + } + } + } + return declAggr.toArray(new SupportedPrivilege[declAggr.size()]); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/JcrUserPrivilegesProperty.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/JcrUserPrivilegesProperty.java new file mode 100644 index 00000000000..f763781bb87 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/JcrUserPrivilegesProperty.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.webdav.jcr.security; + +import java.util.ArrayList; +import java.util.List; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.security.CurrentUserPrivilegeSetProperty; +import org.apache.jackrabbit.webdav.security.Privilege; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * JcrPrivilegesProperty... + */ +public class JcrUserPrivilegesProperty { + + private final Session session; + private final String absPath; + + /** + * @param session The reading session + * @param absPath An absolute path to an existing JCR node or {@code null}. + */ + public JcrUserPrivilegesProperty(Session session, String absPath) throws RepositoryException { + this.session = session; + this.absPath = absPath; + } + + public CurrentUserPrivilegeSetProperty asDavProperty() throws RepositoryException { + List davPrivs = new ArrayList(); + for (javax.jcr.security.Privilege privilege : session.getAccessControlManager().getPrivileges(absPath)) { + String privilegeName = privilege.getName(); + + String prefix = Text.getNamespacePrefix(privilegeName); + Namespace ns = (prefix.isEmpty()) ? Namespace.EMPTY_NAMESPACE : Namespace.getNamespace(prefix, session.getNamespaceURI(prefix)); + davPrivs.add(Privilege.getPrivilege(Text.getLocalName(privilegeName), ns)); + } + + return new CurrentUserPrivilegeSetProperty(davPrivs.toArray(new Privilege[davPrivs.size()])); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/SecurityUtils.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/SecurityUtils.java new file mode 100644 index 00000000000..8997e6a60b7 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/security/SecurityUtils.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.webdav.jcr.security; + +import javax.jcr.Repository; +import javax.jcr.Session; + +public final class SecurityUtils { + + private SecurityUtils() {} + + public static boolean supportsAccessControl(Session session) { + String desc = session.getRepository().getDescriptor(Repository.OPTION_ACCESS_CONTROL_SUPPORTED); + return Boolean.valueOf(desc); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/transaction/TransactionListener.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/transaction/TransactionListener.java new file mode 100644 index 00000000000..2a71ac54a2e --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/transaction/TransactionListener.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.webdav.jcr.transaction; + +import org.apache.jackrabbit.webdav.transaction.TransactionResource; + +/** + * TransactionListener provides callbacks when a transaction + * is committed. + */ +public interface TransactionListener { + + /** + * This method is called right before a transaction is committed. + * + * @param resource the transaction resource which will be committed. + * @param lockToken the lock token + */ + public void beforeCommit(TransactionResource resource, String lockToken); + + /** + * This method is called after the commit has been executed. + * + * @param resource the transaction resource which had been committed. + * @param lockToken the lock token. + * @param success if the commit was successful. + */ + public void afterCommit(TransactionResource resource, String lockToken, boolean success); +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/transaction/TxLockManagerImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/transaction/TxLockManagerImpl.java new file mode 100644 index 00000000000..c8a88e8c24a --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/transaction/TxLockManagerImpl.java @@ -0,0 +1,796 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.transaction; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.WebdavResponse; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.transaction.TransactionConstants; +import org.apache.jackrabbit.webdav.transaction.TransactionInfo; +import org.apache.jackrabbit.webdav.transaction.TransactionResource; +import org.apache.jackrabbit.webdav.transaction.TxActiveLock; +import org.apache.jackrabbit.webdav.transaction.TxLockManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; +import java.util.HashMap; +import java.util.Iterator; +import java.util.IdentityHashMap; +import java.util.Map; + +/** + * TxLockManagerImpl manages locks with locktype + * '{@link TransactionConstants#TRANSACTION dcr:transaction}'. + *

        + */ + //todo: removing all expired locks + //todo: 'local' and 'global' are not accurate terms in the given context > replace + /*todo: the usage of the 'global' transaction is not according to the JTA specification, + which explicitly requires any transaction present on a servlet to be completed before + the service method returns. Starting/completing transactions on the session object, + which is possible with the jackrabbit implementation is a hack.*/ + /*todo: review of this transaction part is therefore required. Is there a use-case + for those 'global' transactions at all...*/ +public class TxLockManagerImpl implements TxLockManager { + + private static Logger log = LoggerFactory.getLogger(TxLockManagerImpl.class); + + private final TransactionMap map = new TransactionMap(); + + private final Map listeners = new IdentityHashMap(); + + /** + * Create a new lock. + * + * @param lockInfo as present in the request body. + * @param resource + * @return the lock + * @throws DavException if the lock could not be obtained. + * @throws IllegalArgumentException if the resource is null or + * does not implement {@link TransactionResource} interface. + * @see LockManager#createLock(org.apache.jackrabbit.webdav.lock.LockInfo, org.apache.jackrabbit.webdav.DavResource) + */ + public ActiveLock createLock(LockInfo lockInfo, DavResource resource) + throws DavException { + if (resource == null || !(resource instanceof TransactionResource)) { + throw new IllegalArgumentException("Invalid resource"); + } + return createLock(lockInfo, (TransactionResource) resource); + } + + /** + * Create a new lock. + * + * @param lockInfo + * @param resource + * @return the lock + * @throws DavException if the request lock has the wrong lock type or if + * the lock could not be obtained for any reason. + */ + private synchronized ActiveLock createLock(LockInfo lockInfo, TransactionResource resource) + throws DavException { + if (!lockInfo.isDeep() || !TransactionConstants.TRANSACTION.equals(lockInfo.getType())) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + + ActiveLock existing = getLock(lockInfo.getType(), lockInfo.getScope(), resource); + if (existing != null) { + throw new DavException(DavServletResponse.SC_LOCKED); + } + // TODO: check for locks on member resources is required as well for lock is always deep! + + Transaction tx = createTransaction(resource.getLocator(), lockInfo); + tx.start(resource); + + // keep references to this lock + addReferences(tx, getMap(resource), resource); + + return tx.getLock(); + } + + /** + * Build the transaction object associated by the lock. + * + * @param locator + * @param lockInfo + * @return + */ + private Transaction createTransaction(DavResourceLocator locator, LockInfo lockInfo) { + if (TransactionConstants.GLOBAL.equals(lockInfo.getScope())) { + return new GlobalTransaction(locator, new TxActiveLock(lockInfo)); + } else { + return new LocalTransaction(locator, new TxActiveLock(lockInfo)); + } + } + + /** + * Refresh the lock identified by the given lock token. + * + * @param lockInfo + * @param lockToken + * @param resource + * @return the lock + * @throws DavException + * @throws IllegalArgumentException if the resource is null or + * does not implement {@link TransactionResource} interface. + * @see LockManager#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String, org.apache.jackrabbit.webdav.DavResource) + */ + public ActiveLock refreshLock(LockInfo lockInfo, String lockToken, + DavResource resource) throws DavException { + if (resource == null || !(resource instanceof TransactionResource)) { + throw new IllegalArgumentException("Invalid resource"); + } + return refreshLock(lockInfo, lockToken, (TransactionResource) resource); + } + + /** + * Reset the timeout of the lock identified by the given lock token. + * + * @param lockInfo + * @param lockToken + * @param resource + * @return + * @throws DavException if the lock did not exist or is expired. + */ + private synchronized ActiveLock refreshLock(LockInfo lockInfo, String lockToken, + TransactionResource resource) throws DavException { + + TransactionMap responsibleMap = getMap(resource); + Transaction tx = responsibleMap.get(lockToken); + if (tx == null) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No valid transaction lock found for resource '" + resource.getResourcePath() + "'"); + } else if (tx.getLock().isExpired()) { + removeExpired(tx, responsibleMap, resource); + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction lock for resource '" + resource.getResourcePath() + "' was already expired."); + } else { + tx.getLock().setTimeout(lockInfo.getTimeout()); + } + return tx.getLock(); + } + + /** + * Throws UnsupportedOperationException. + * + * @param lockToken + * @param resource + * @throws DavException + * @see LockManager#releaseLock(String, org.apache.jackrabbit.webdav.DavResource) + */ + public void releaseLock(String lockToken, DavResource resource) + throws DavException { + throw new UnsupportedOperationException("A transaction lock can only be release with a TransactionInfo object and a lock token."); + } + + /** + * Release the lock identified by the given lock token. + * + * @param lockInfo + * @param lockToken + * @param resource + * @throws DavException + */ + public synchronized void releaseLock(TransactionInfo lockInfo, String lockToken, + TransactionResource resource) throws DavException { + if (resource == null) { + throw new IllegalArgumentException("Resource must not be null."); + } + TransactionMap responsibleMap = getMap(resource); + Transaction tx = responsibleMap.get(lockToken); + + if (tx == null) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No transaction lock found for resource '" + resource.getResourcePath() + "'"); + } else if (tx.getLock().isExpired()) { + removeExpired(tx, responsibleMap, resource); + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction lock for resource '" + resource.getResourcePath() + "' was already expired."); + } else { + if (lockInfo.isCommit()) { + TransactionListener[] txListeners; + synchronized (listeners) { + txListeners = listeners.values().toArray(new TransactionListener[listeners.values().size()]); + } + for (TransactionListener txListener : txListeners) { + txListener.beforeCommit(resource, lockToken); + } + DavException ex = null; + try { + tx.commit(resource); + } catch (DavException e) { + ex = e; + } + for (TransactionListener txListener : txListeners) { + txListener.afterCommit(resource, lockToken, ex == null); + } + if (ex != null) { + throw ex; + } + } else { + tx.rollback(resource); + } + removeReferences(tx, responsibleMap, resource); + } + } + + /** + * Always returns null + * + * @param type + * @param scope + * @param resource + * @return null + * @see #getLock(Type, Scope, TransactionResource) + * @see LockManager#getLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope, org.apache.jackrabbit.webdav.DavResource) + */ + public ActiveLock getLock(Type type, Scope scope, DavResource resource) { + return null; + } + + /** + * Returns true if the given lock token belongs to a lock that applies to + * the given resource, false otherwise. The token may either be retrieved + * from the {@link DavConstants#HEADER_LOCK_TOKEN Lock-Token header} or + * from the {@link TransactionConstants#HEADER_TRANSACTIONID TransactionId header}. + * + * @param token + * @param resource + * @return + * @see LockManager#hasLock(String token, DavResource resource) + */ + public boolean hasLock(String token, DavResource resource) { + return getLock(token, null, resource) != null; + } + + /** + * Return the lock applied to the given resource or null + * + * @param type + * @param scope + * @param resource + * @return lock applied to the given resource or null + * @see LockManager#getLock(Type, Scope, DavResource) + * todo: is it correct to return one that specific lock, the current session is token-holder of? + */ + public ActiveLock getLock(Type type, Scope scope, TransactionResource resource) { + ActiveLock lock = null; + if (TransactionConstants.TRANSACTION.equals(type)) { + String[] sessionTokens = resource.getSession().getLockTokens(); + int i = 0; + while (lock == null && i < sessionTokens.length) { + String lockToken = sessionTokens[i]; + lock = getLock(lockToken, scope, resource); + i++; + } + } + return lock; + } + + //-----------------------------< listener support >------------------------- + + /** + * Adds a transaction listener to this TxLockManager. + * @param listener the listener to add. + */ + public void addTransactionListener(TransactionListener listener) { + synchronized (listeners) { + listeners.put(listener, listener); + } + } + + /** + * Removes a transaction listener from this TxLockManager. + * @param listener the listener to remove. + */ + public void removeTransactionListener(TransactionListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + /** + * @param lockToken + * @param resource + * @return + */ + private ActiveLock getLock(String lockToken, Scope scope, DavResource resource) { + if (!(resource instanceof TransactionResource)) { + log.warn("TransactionResource expected"); + return null; + } + + ActiveLock lock = null; + Transaction tx = null; + TransactionMap m = map; + // check if main-map contains that txId + if (m.containsKey(lockToken)) { + tx = m.get(lockToken); + } else { + // look through all the nested tx-maps (i.e. global txs) for the given txId + Iterator it = m.values().iterator(); + while (it.hasNext() && tx == null) { + Transaction txMap = it.next(); + if (!txMap.isLocal()) { + m = (TransactionMap) txMap; + if (m.containsKey(lockToken)) { + tx = ((TransactionMap) txMap).get(lockToken); + } + } + } + } + + if (tx != null) { + if (tx.getLock().isExpired()) { + removeExpired(tx, m, (TransactionResource) resource); + } else if (tx.appliesToResource(resource) && (scope == null || tx.getLock().getScope().equals(scope))) { + lock = tx.getLock(); + } + } + return lock; + } + + /** + * Return the map that may contain a transaction lock for the given resource. + * In case the resource provides a transactionId, the map must be a + * repository transaction that is identified by the given id and which in + * turn can act as map. + * + * @param resource + * @return responsible map. + * @throws DavException if no map could be retrieved. + */ + private TransactionMap getMap(TransactionResource resource) + throws DavException { + + String txKey = resource.getTransactionId(); + if (txKey == null) { + return map; + } else { + if (!map.containsKey(txKey)) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Transaction map '" + map + " does not contain a transaction with TransactionId '" + txKey + "'."); + } + Transaction tx = map.get(txKey); + if (tx.isLocal()) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "TransactionId '" + txKey + "' points to a local transaction, that cannot act as transaction map"); + } else if (tx.getLock() != null && tx.getLock().isExpired()) { + removeExpired(tx, map, resource); + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Attempt to retrieve an expired global transaction."); + } + // tx is a global transaction that acts as map as well. + return (TransactionMap) tx; + } + } + + /** + * Rollbacks the specified transaction and releases the lock. This includes + * the removal of all references. + * + * @param tx + * @param responsibleMap + * @param resource + */ + private static void removeExpired(Transaction tx, TransactionMap responsibleMap, + TransactionResource resource) { + log.debug("Removing expired transaction lock " + tx); + try { + tx.rollback(resource); + removeReferences(tx, responsibleMap, resource); + } catch (DavException e) { + log.error("Error while removing expired transaction lock: " + e.getMessage()); + } + } + + /** + * Create the required references to the new transaction specified by tx. + * + * @param tx + * @param responsibleMap + * @param resource + * @throws DavException + */ + private static void addReferences(Transaction tx, TransactionMap responsibleMap, + TransactionResource resource) { + log.debug("Adding transactionId '" + tx.getId() + "' as session lock token."); + resource.getSession().addLockToken(tx.getId()); + + responsibleMap.put(tx.getId(), tx); + resource.getSession().addReference(tx.getId()); + } + + /** + * Remove all references to the specified transaction. + * + * @param tx + * @param responsibleMap + * @param resource + */ + private static void removeReferences(Transaction tx, TransactionMap responsibleMap, + TransactionResource resource) { + log.debug("Removing transactionId '" + tx.getId() + "' from session lock tokens."); + resource.getSession().removeLockToken(tx.getId()); + + responsibleMap.remove(tx.getId()); + resource.getSession().removeReference(tx.getId()); + } + + /** + * @param resource + * @return JCR session + */ + private static Session getRepositorySession(TransactionResource resource) throws DavException { + return JcrDavSession.getRepositorySession(resource.getSession()); + } + + //------------------------------------------< inner classes, interfaces >--- + /** + * Internal Transaction interface + */ + private interface Transaction { + + TxActiveLock getLock(); + + /** + * @return the id of this transaction. + */ + String getId(); + + /** + * @return path of the lock holding resource + */ + String getResourcePath(); + + /** + * @param resource + * @return true if the lock defined by this transaction applies to the + * given resource, either due to the resource holding that lock or due + * to a deep lock hold by any ancestor resource. + */ + boolean appliesToResource(DavResource resource); + + /** + * @return true if this transaction is used to allow for transient changes + * on the underlying repository, that may be persisted with the final + * UNLOCK request only. + */ + boolean isLocal(); + + /** + * Start this transaction. + * + * @param resource + * @throws DavException if an error occurs. + */ + void start(TransactionResource resource) throws DavException; + + /** + * Commit this transaction + * + * @param resource + * @throws DavException if an error occurs. + */ + void commit(TransactionResource resource) throws DavException; + + /** + * Rollback this transaction. + * + * @param resource + * @throws DavException if an error occurs. + */ + void rollback(TransactionResource resource) throws DavException; + } + + /** + * Abstract transaction covering functionally to both implementations. + */ + private abstract static class AbstractTransaction extends TransactionMap implements Transaction { + + private final DavResourceLocator locator; + private final TxActiveLock lock; + + private AbstractTransaction(DavResourceLocator locator, TxActiveLock lock) { + this.locator = locator; + this.lock = lock; + } + + //----------------------------------------------------< Transaction >--- + /** + * @see #getLock() + */ + public TxActiveLock getLock() { + return lock; + } + + /** + * @see #getId() + */ + public String getId() { + return lock.getToken(); + } + + /** + * @see #getResourcePath() + */ + public String getResourcePath() { + return locator.getResourcePath(); + } + + /** + * @see #appliesToResource(DavResource) + */ + public boolean appliesToResource(DavResource resource) { + if (locator.isSameWorkspace(resource.getLocator())) { + String lockResourcePath = getResourcePath(); + String resPath = resource.getResourcePath(); + + while (!"".equals(resPath)) { + if (lockResourcePath.equals(resPath)) { + return true; + } + resPath = Text.getRelativeParent(resPath, 1); + } + } + return false; + } + } + + /** + * Local transaction + */ + private final static class LocalTransaction extends AbstractTransaction { + + private LocalTransaction(DavResourceLocator locator, TxActiveLock lock) { + super(locator, lock); + } + + //----------------------------------------------------< Transaction >--- + /** + * @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#isLocal() + */ + public boolean isLocal() { + return true; + } + + /** + * @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#start(TransactionResource) + */ + public void start(TransactionResource resource) throws DavException { + try { + // make sure, the given resource represents an existing repository item + if (!getRepositorySession(resource).itemExists(resource.getLocator().getRepositoryPath())) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Unable to start local transaction: no repository item present at " + getResourcePath()); + } + } catch (RepositoryException e) { + log.error("Unexpected error: " + e.getMessage()); + throw new JcrDavException(e); + } + } + + /** + * @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#commit(TransactionResource) + */ + public void commit(TransactionResource resource) throws DavException { + try { + getItem(resource).save(); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#rollback(TransactionResource) + */ + public void rollback(TransactionResource resource) throws DavException { + try { + getItem(resource).refresh(false); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //-------------------------------------------------< TransactionMap >--- + /** + * Always throws DavException. + * + * @see TransactionMap#putTransaction(String, org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction) + */ + @Override + public Transaction putTransaction(String key, Transaction value) throws DavException { + throw new DavException(WebdavResponse.SC_PRECONDITION_FAILED, "Attempt to nest a new transaction into a local one."); + } + + //--------------------------------------------------------< private >--- + /** + * Retrieve the repository item from the given transaction resource. + * + * @param resource + * @return + * @throws PathNotFoundException + * @throws RepositoryException + * @throws DavException + */ + private Item getItem(TransactionResource resource) throws PathNotFoundException, RepositoryException, DavException { + String itemPath = resource.getLocator().getRepositoryPath(); + return getRepositorySession(resource).getItem(itemPath); + } + } + + /** + * Global transaction + */ + private static class GlobalTransaction extends AbstractTransaction { + + private Xid xid; + + private GlobalTransaction(DavResourceLocator locator, TxActiveLock lock) { + super(locator, lock); + xid = new XidImpl(lock.getToken()); + } + + //----------------------------------------------------< Transaction >--- + /** + * @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#isLocal() + */ + public boolean isLocal() { + return false; + } + + /** + * @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#start(TransactionResource) + */ + public void start(TransactionResource resource) throws DavException { + XAResource xaRes = getXAResource(resource); + try { + xaRes.setTransactionTimeout((int) getLock().getTimeout() / 1000); + xaRes.start(xid, XAResource.TMNOFLAGS); + } catch (XAException e) { + throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage()); + } + } + + /** + * @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#commit(TransactionResource) + */ + public void commit(TransactionResource resource) throws DavException { + XAResource xaRes = getXAResource(resource); + try { + xaRes.commit(xid, false); + removeLocalTxReferences(resource); + } catch (XAException e) { + throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage()); + } + } + + /** + * @see org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl.Transaction#rollback(TransactionResource) + */ + public void rollback(TransactionResource resource) throws DavException { + XAResource xaRes = getXAResource(resource); + try { + xaRes.rollback(xid); + removeLocalTxReferences(resource); + } catch (XAException e) { + throw new DavException(DavServletResponse.SC_FORBIDDEN, e.getMessage()); + } + } + + //-------------------------------------------------< TransactionMap >--- + @Override + public Transaction putTransaction(String key, Transaction value) throws DavException { + if (!(value instanceof LocalTransaction)) { + throw new DavException(WebdavResponse.SC_PRECONDITION_FAILED, "Attempt to nest global transaction into a global one."); + } + return super.put(key, value); + } + + //--------------------------------------------------------< private >--- + private XAResource getXAResource(TransactionResource resource) throws DavException { + /* + // commented, since server should be jackrabbit independent + Session session = resource.getSession().getRepositorySession(); + if (session instanceof XASession) { + return ((XASession)session).getXAResource(); + } else { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + */ + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + private void removeLocalTxReferences(TransactionResource resource) { + for (Object o : values()) { + Transaction tx = (Transaction) o; + removeReferences(tx, this, resource); + } + } + } + + /** + * + */ + private static class TransactionMap extends HashMap { + + public Transaction get(String key) { + Transaction tx = null; + if (containsKey(key)) { + tx = super.get(key); + } + return tx; + } + + public Transaction putTransaction(String key, Transaction value) throws DavException { + // any global and local transactions allowed. + return super.put(key, value); + } + } + + /** + * Private class implementing Xid interface. + */ + private static class XidImpl implements Xid { + + private final String id; + + /** + * Create a new Xid + * + * @param id + */ + private XidImpl(String id) { + this.id = id; + } + + /** + * @return 1 + * @see javax.transaction.xa.Xid#getFormatId() + */ + public int getFormatId() { + // todo: define reasonable format id + return 1; + } + + /** + * @return an empty byte array. + * @see javax.transaction.xa.Xid#getBranchQualifier() + */ + public byte[] getBranchQualifier() { + return new byte[0]; + } + + /** + * @return id as byte array + * @see javax.transaction.xa.Xid#getGlobalTransactionId() + */ + public byte[] getGlobalTransactionId() { + return id.getBytes(); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/VersionHistoryItemCollection.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/VersionHistoryItemCollection.java new file mode 100644 index 00000000000..08ca099b1ff --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/VersionHistoryItemCollection.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.version; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.jcr.DefaultItemCollection; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.ResourceType; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.VersionResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import java.util.ArrayList; + +/** + * VersionHistoryItemCollection represents a JCR version history. + * + * @see VersionHistory + */ +public class VersionHistoryItemCollection extends DefaultItemCollection + implements VersionHistoryResource { + + private static Logger log = LoggerFactory.getLogger(VersionHistoryItemCollection.class); + + /** + * Create a new VersionHistoryItemCollection resource. + * + * @param resourcePath + * @param session + * @param factory + */ + public VersionHistoryItemCollection(DavResourceLocator resourcePath, + JcrDavSession session, + DavResourceFactory factory, + Item item) { + super(resourcePath, session, factory, item); + if (item == null || !(item instanceof VersionHistory)) { + throw new IllegalArgumentException("VersionHistory item expected."); + } + } + + //----------------------------------------------< DavResource interface >--- + /** + * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() + */ + @Override + public String getSupportedMethods() { + StringBuffer sb = new StringBuffer(ItemResourceConstants.METHODS); + sb.append(", ").append(VersionHistoryResource.METHODS); + return sb.toString(); + } + + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = super.getProperty(name); + if (prop == null) { + // required, protected version-set property for version-history resource + try { + if (ROOT_VERSION.equals(name)) { + // required root-version property for version-history resource + String rootVersionHref = getLocatorFromItem(((VersionHistory)item).getRootVersion()).getHref(true); + prop = new HrefProperty(ROOT_VERSION, rootVersionHref, true); + } else if (VERSION_SET.equals(name)) { + VersionIterator vIter = ((VersionHistory) item).getAllVersions(); + prop = getHrefProperty(VERSION_SET, vIter, true); + } + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + + return prop; + } + + /** + * Removing a version resource is achieved by calling removeVersion + * on the versionhistory item this version belongs to. + * + * @throws DavException if the version does not exist or if an error occurs + * while deleting. + * @see DavResource#removeMember(org.apache.jackrabbit.webdav.DavResource) + */ + @Override + public void removeMember(DavResource member) throws DavException { + if (exists()) { + VersionHistory versionHistory = (VersionHistory) item; + try { + versionHistory.removeVersion(getItemName(member.getLocator().getRepositoryPath())); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + } + //-----------------------------------< VersionHistoryResource interface >--- + /** + * Return an array of {@link VersionResource}s representing all versions + * present in the underlying JCR version history. + * + * @return array of {@link VersionResource}s representing all versions + * present in the underlying JCR version history. + * @throws DavException + * @see org.apache.jackrabbit.webdav.version.VersionHistoryResource#getVersions() + */ + public VersionResource[] getVersions() throws DavException { + try { + VersionIterator vIter = ((VersionHistory)item).getAllVersions(); + ArrayList l = new ArrayList(); + while (vIter.hasNext()) { + DavResourceLocator versionLoc = getLocatorFromItem(vIter.nextVersion()); + VersionResource vr = (VersionResource) createResourceFromLocator(versionLoc); + l.add(vr); + } + return l.toArray(new VersionResource[l.size()]); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //-------------------------------------------------------------------------- + @Override + protected void initPropertyNames() { + super.initPropertyNames(); + + if (exists()) { + names.addAll(JcrDavPropertyNameSet.VERSIONHISTORY_SET); + } + } + + /** + * Fill the property set for this resource. + */ + @Override + protected void initProperties() { + super.initProperties(); + + // change resource type defined by default item collection + properties.add(new ResourceType(ResourceType.VERSION_HISTORY)); + + // jcr specific property pointing to the node this history belongs to + try { + properties.add(new DefaultDavProperty(JCR_VERSIONABLEUUID, ((VersionHistory)item).getVersionableIdentifier())); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/VersionItemCollection.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/VersionItemCollection.java new file mode 100644 index 00000000000..3fa576158c8 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/VersionItemCollection.java @@ -0,0 +1,240 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.version; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.util.HttpDateFormat; +import org.apache.jackrabbit.webdav.jcr.DefaultItemCollection; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.version.LabelInfo; +import org.apache.jackrabbit.webdav.version.LabelSetProperty; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.VersionResource; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import java.util.ArrayList; +import java.util.List; + +/** + * VersionItemCollection represents a JCR version. + * + * @see Version + */ +public class VersionItemCollection extends DefaultItemCollection + implements VersionResource { + + private static Logger log = LoggerFactory.getLogger(VersionItemCollection.class); + + /** + * Create a new VersionItemCollection. + * + * @param locator + * @param session + * @param factory + */ + public VersionItemCollection(DavResourceLocator locator, + JcrDavSession session, + DavResourceFactory factory, Item item) { + super(locator, session, factory, item); + if (item == null || !(item instanceof Version)) { + throw new IllegalArgumentException("Version item expected."); + } + } + + @Override + public DavProperty getProperty(DavPropertyName name) { + DavProperty prop = super.getProperty(name); + if (prop == null && exists()) { + Version v = (Version) item; + try { + if (VERSION_NAME.equals(name)) { + // required, protected DAV:version-name property + prop = new DefaultDavProperty(VERSION_NAME, v.getName(), true); + } else if (VERSION_HISTORY.equals(name)) { + // required DAV:version-history (computed) property + String vhHref = getLocatorFromItem(getVersionHistoryItem()).getHref(true); + prop = new HrefProperty(VERSION_HISTORY, vhHref, true); + } else if (PREDECESSOR_SET.equals(name)) { + // required DAV:predecessor-set (protected) property + prop = getHrefProperty(VersionResource.PREDECESSOR_SET, v.getPredecessors(), true); + } else if (SUCCESSOR_SET.equals(name)) { + // required DAV:successor-set (computed) property + prop = getHrefProperty(SUCCESSOR_SET, v.getSuccessors(), true); + } else if (LABEL_NAME_SET.equals(name)) { + // required, protected DAV:label-name-set property + String[] labels = getVersionHistoryItem().getVersionLabels(v); + prop = new LabelSetProperty(labels); + } else if (CHECKOUT_SET.equals(name)) { + // required DAV:checkout-set (computed) property + PropertyIterator it = v.getReferences(); + List nodeList = new ArrayList(); + while (it.hasNext()) { + Property p = it.nextProperty(); + if (JcrConstants.JCR_BASEVERSION.equals(p.getName())) { + Node n = p.getParent(); + if (n.isCheckedOut()) { + nodeList.add(n); + } + } + } + prop = getHrefProperty(CHECKOUT_SET, nodeList.toArray(new Node[nodeList.size()]), true); + } + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + + return prop; + } + + //----------------------------------------------< DavResource interface >--- + /** + * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() + */ + @Override + public String getSupportedMethods() { + StringBuffer sb = new StringBuffer(ItemResourceConstants.METHODS); + sb.append(", ").append(VersionResource.METHODS); + return sb.toString(); + } + + //------------------------------------------< VersionResource interface >--- + /** + * Modify the labels defined for the underlying repository version. + * + * @param labelInfo + * @throws org.apache.jackrabbit.webdav.DavException + * @see VersionResource#label(org.apache.jackrabbit.webdav.version.LabelInfo) + * @see VersionHistory#addVersionLabel(String, String, boolean) + * @see VersionHistory#removeVersionLabel(String) + */ + public void label(LabelInfo labelInfo) throws DavException { + if (labelInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid label request body required."); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + try { + VersionHistory vh = getVersionHistoryItem(); + if (labelInfo.getType() == LabelInfo.TYPE_REMOVE) { + vh.removeVersionLabel(labelInfo.getLabelName()); + } else if (labelInfo.getType() == LabelInfo.TYPE_ADD) { + // ADD: only add if not yet existing + vh.addVersionLabel(item.getName(), labelInfo.getLabelName(), false); + } else { + // SET: move label if already existing + vh.addVersionLabel(item.getName(), labelInfo.getLabelName(), true); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Returns the {@link VersionHistory} associated with the repository version. + * Note: in contrast to a versionable node, the version history of a version + * item is always represented by its nearest ancestor. + * + * @return the {@link VersionHistoryResource} associated with this resource. + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionResource#getVersionHistory() + * @see javax.jcr.Item#getParent() + */ + public VersionHistoryResource getVersionHistory() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + try { + VersionHistory vh = getVersionHistoryItem(); + DavResourceLocator loc = getLocatorFromItem(vh); + return (VersionHistoryResource) createResourceFromLocator(loc); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Return versionhistory that contains this version item + * + * @return versionhistory that contains this version item + * @throws RepositoryException + * @see javax.jcr.version.Version#getContainingHistory() + */ + private VersionHistory getVersionHistoryItem() throws RepositoryException { + return ((Version)item).getContainingHistory(); + } + + //-------------------------------------------------------------------------- + /** + * Define the set of reports supported by this resource. + * + * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty + */ + @Override + protected void initSupportedReports() { + super.initSupportedReports(); + if (exists()) { + supportedReports.addReportType(ReportType.VERSION_TREE); + } + } + + @Override + protected void initPropertyNames() { + super.initPropertyNames(); + + if (exists()) { + names.addAll(JcrDavPropertyNameSet.VERSION_SET); + } + } + + @Override + protected String getCreationDate() { + if (exists()) { + Version v = (Version) item; + try { + return HttpDateFormat.creationDateFormat().format(v.getCreated().getTime()); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + + // fallback + return super.getCreationDate(); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/AbstractJcrReport.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/AbstractJcrReport.java new file mode 100644 index 00000000000..c6d31d47324 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/AbstractJcrReport.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.webdav.jcr.version.report; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; + +import javax.jcr.Session; + +/** + * AbstractJcrReport... + */ +public abstract class AbstractJcrReport implements Report { + + private Session session; + private ReportInfo reportInfo; + + /** + * Performs basic validation checks common to all JCR specific reports. + * + * @param resource + * @param info + * @throws DavException + * @see Report#init(DavResource, ReportInfo) + */ + public void init(DavResource resource, ReportInfo info) throws DavException { + if (resource == null || info == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unable to run report: WebDAV Resource and ReportInfo must not be null."); + } + if (!getType().isRequestedReportType(info)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Expected report type: '" + getType().getReportName() + "', found: '" + info.getReportName() + ";" + "'."); + } + if (info.getDepth() > DavConstants.DEPTH_0) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid Depth header: " + info.getDepth()); + } + + DavSession davSession = resource.getSession(); + if (davSession == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "The resource must provide a non-null session object in order to create '" + getType().getReportName()+ "' report."); + } + session = JcrDavSession.getRepositorySession(resource.getSession()); + if (session == null) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal error: Unable to access repository session."); + } + reportInfo = info; + } + + //-----------------------------------------------------< implementation >--- + /** + * @return session Session object as obtained from the {@link DavSession}. + */ + Session getRepositorySession() { + return session; + } + + /** + * @return reportInfo the ReportInfo specifying the requested + * report details. + */ + ReportInfo getReportInfo() { + return reportInfo; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/ExportViewReport.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/ExportViewReport.java new file mode 100644 index 00000000000..1fc16f04986 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/ExportViewReport.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.webdav.jcr.version.report; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * ExportViewReport handles REPORT requests for the 'exportview' + * report. The 'exportview' report is used to export + * {@link Session#exportDocumentView(String, java.io.OutputStream, boolean, boolean) DocView} + * and {@link Session#exportSystemView(String, java.io.OutputStream, boolean, boolean) SysView} + * of the {@link javax.jcr.Item item} represented by the requested resource. + *

        + * The request body must contain a {@link ItemResourceConstants#NAMESPACE dcr}:exportview + * element: + *

        + * <!ELEMENT exportview  ( (sysview | docview)?, skipbinary?, norecurse ) >
        + * <!ELEMENT sysview EMPTY >
        + * <!ELEMENT docview EMPTY >
        + * <!ELEMENT skipbinary EMPTY >
        + * <!ELEMENT norecurse EMPTY >
        + * 
        + * If no view type is specified the DocView is generated. + */ +public class ExportViewReport extends AbstractJcrReport { + + private static Logger log = LoggerFactory.getLogger(ExportViewReport.class); + + /** + * The exportview report type + */ + public static final ReportType EXPORTVIEW_REPORT = ReportType.register(JcrRemotingConstants.REPORT_EXPORT_VIEW, ItemResourceConstants.NAMESPACE, ExportViewReport.class); + + private String absNodePath; + + /** + * Returns {@link #EXPORTVIEW_REPORT} report type. + * + * @return {@link #EXPORTVIEW_REPORT} + * @see org.apache.jackrabbit.webdav.version.report.Report#getType() + */ + public ReportType getType() { + return EXPORTVIEW_REPORT; + } + + /** + * Always returns false. + * + * @return false + */ + public boolean isMultiStatusReport() { + return false; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + // delegate validation to super class + super.init(resource, info); + // report specific validation: resource must represent an existing + // repository node + absNodePath = resource.getLocator().getRepositoryPath(); + try { + if (!(getRepositorySession().itemExists(absNodePath) && getRepositorySession().getItem(absNodePath).isNode())) { + throw new JcrDavException(new PathNotFoundException(absNodePath + " does not exist.")); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + + } + + /** + * Creates a Xml document from the generated view. + * + * @param document + * @return Xml element representing the output of the specified view. + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + boolean skipBinary = getReportInfo().containsContentElement("skipbinary", ItemResourceConstants.NAMESPACE); + boolean noRecurse = getReportInfo().containsContentElement("norecurse", ItemResourceConstants.NAMESPACE); + // todo improve... + try { + // create tmpFile in default system-tmp directory + String prefix = "_tmp_" + Text.getName(absNodePath); + File tmpfile = File.createTempFile(prefix, null, null); + tmpfile.deleteOnExit(); + + FileOutputStream out = new FileOutputStream(tmpfile); + if (getReportInfo().containsContentElement("sysview", ItemResourceConstants.NAMESPACE)) { + getRepositorySession().exportSystemView(absNodePath, out, skipBinary, noRecurse); + } else { + // default is docview + getRepositorySession().exportDocumentView(absNodePath, out, skipBinary, noRecurse); + } + out.close(); + + Document tmpDoc = + DomUtil.parseDocument(new FileInputStream(tmpfile)); + + // import the root node of the generated xml to the given document. + Element rootElem = (Element)document.importNode(tmpDoc.getDocumentElement(), true); + return rootElem; + + } catch (RepositoryException e) { + log.error(e.getMessage()); + } catch (FileNotFoundException e) { + log.error(e.getMessage()); + } catch (IOException e) { + log.error(e.getMessage()); + } catch (ParserConfigurationException e) { + log.error(e.getMessage()); + } catch (SAXException e) { + log.error(e.getMessage()); + } + return null; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/JcrPrivilegeReport.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/JcrPrivilegeReport.java new file mode 100644 index 00000000000..069bc3817ad --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/JcrPrivilegeReport.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.webdav.jcr.version.report; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.security.Privilege; +import org.apache.jackrabbit.webdav.security.CurrentUserPrivilegeSetProperty; +import org.w3c.dom.Element; +import org.w3c.dom.Document; + +import javax.jcr.RepositoryException; +import java.util.List; +import java.util.ArrayList; + +/** + *

        Report to retrieve the permissions granted to the reading session as defined + * by {@link javax.jcr.Session#hasPermission(String, String)}.

        + * + *

        NOTE: the name of this report and the names of the privileges are + * misleading as they rather correspond to the actions defined by + * {@link javax.jcr.Session}; while the JCR privileges s.str. have only been + * specified as of JSR 283 in the {@link javax.jcr.security.Privilege} interface. + * A better name would have been JcrActionReport

        + * + * @see org.apache.jackrabbit.webdav.jcr.security.JcrUserPrivilegesProperty for + * the webdav correspondence to {@link javax.jcr.security.AccessControlManager#getPrivileges(String)} + * mapped to the {@link org.apache.jackrabbit.webdav.security.CurrentUserPrivilegeSetProperty}. + */ +public class JcrPrivilegeReport extends AbstractJcrReport { + + private static Logger log = LoggerFactory.getLogger(JcrPrivilegeReport.class); + + /** + * The exportview report type + */ + public static final ReportType PRIVILEGES_REPORT = ReportType.register(JcrRemotingConstants.REPORT_PRIVILEGES, ItemResourceConstants.NAMESPACE, JcrPrivilegeReport.class); + + private static final Privilege[] PRIVS = new Privilege[] { + ItemResourceConstants.PRIVILEGE_JCR_READ, + ItemResourceConstants.PRIVILEGE_JCR_ADD_NODE, + ItemResourceConstants.PRIVILEGE_JCR_SET_PROPERTY, + ItemResourceConstants.PRIVILEGE_JCR_REMOVE}; + + private final MultiStatus ms = new MultiStatus(); + + /** + * Returns {@link #PRIVILEGES_REPORT} report type. + * + * @return {@link #PRIVILEGES_REPORT} + * @see org.apache.jackrabbit.webdav.version.report.Report#getType() + */ + @Override + public ReportType getType() { + return PRIVILEGES_REPORT; + } + + /** + * Always returns true. + * + * @return true + * @see org.apache.jackrabbit.webdav.version.report.Report#isMultiStatusReport() + */ + @Override + public boolean isMultiStatusReport() { + return true; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + // delegate basic validation to super class + super.init(resource, info); + // make also sure, the info contains a DAV:href child element + if (!info.containsContentElement(DavConstants.XML_HREF, DavConstants.NAMESPACE)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "dcr:privileges element must at least contain a single DAV:href child."); + } + // immediately build the final multistatus element + Element hrefElem = info.getContentElement(DavConstants.XML_HREF, DavConstants.NAMESPACE); + String href = DomUtil.getTextTrim(hrefElem); + href = obtainAbsolutePathFromUri(href); // TODO: we should check whether the authority component matches + DavResourceLocator resourceLoc = resource.getLocator(); + DavResourceLocator loc = resourceLoc.getFactory().createResourceLocator(resourceLoc.getPrefix(), href); + // immediately build the final multistatus element + addResponses(loc); + } + + /** + * Creates a Xml document from the generated view. + * + * @param document + * @return Xml element representing the output of the specified view. + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + @Override + public Element toXml(Document document) { + return ms.toXml(document); + } + + private void addResponses(DavResourceLocator locator) { + String repositoryPath = locator.getRepositoryPath(); + MultiStatusResponse resp = new MultiStatusResponse(locator.getHref(false), null); + List currentPrivs = new ArrayList(); + for (Privilege priv : PRIVS) { + try { + if (getRepositorySession().hasPermission(repositoryPath, priv.getName())) { + currentPrivs.add(priv); + } + } catch (RepositoryException e) { + // ignore + log.debug(e.toString()); + } + } + resp.add(new CurrentUserPrivilegeSetProperty(currentPrivs.toArray(new Privilege[currentPrivs.size()]))); + ms.addResponse(resp); + } + + private static String obtainAbsolutePathFromUri(String uri) { + try { + java.net.URI u = new java.net.URI(uri); + StringBuilder sb = new StringBuilder(); + sb.append(u.getRawPath()); + if (u.getRawQuery() != null) { + sb.append("?").append(u.getRawQuery()); + } + return sb.toString(); + } + catch (java.net.URISyntaxException ex) { + log.warn("parsing " + uri, ex); + return uri; + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/LocateByUuidReport.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/LocateByUuidReport.java new file mode 100644 index 00000000000..3d0208a632e --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/LocateByUuidReport.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.webdav.jcr.version.report; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** + * LocateByUuidReport handles REPORT requests for the 'locate-by-uuid' + * report. + *

        + * The request body must be a 'dcr:locate-by-uuid' XML element: + *

        + * <!ELEMENT locate-by-uuid ( href , prop? ) >
        + * 
        + * The response to a successful report request will be a Multi-Status response. + */ +public class LocateByUuidReport extends AbstractJcrReport { + + private static Logger log = LoggerFactory.getLogger(LocateByUuidReport.class); + + /** + * The exportview report type + */ + public static final ReportType LOCATE_BY_UUID_REPORT = ReportType.register(JcrRemotingConstants.REPORT_LOCATE_BY_UUID, ItemResourceConstants.NAMESPACE, LocateByUuidReport.class); + + private MultiStatus ms; + + /** + * Returns {@link #LOCATE_BY_UUID_REPORT} report type. + * + * @return {@link #LOCATE_BY_UUID_REPORT} + * @see org.apache.jackrabbit.webdav.version.report.Report#getType() + */ + public ReportType getType() { + return LOCATE_BY_UUID_REPORT; + } + + /** + * Always returns true. + * + * @return true + * @see org.apache.jackrabbit.webdav.version.report.Report#isMultiStatusReport() + */ + public boolean isMultiStatusReport() { + return true; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + // delegate basic validation to super class + super.init(resource, info); + // make also sure, the info contains a DAV:href child element + if (!info.containsContentElement(DavConstants.XML_HREF, DavConstants.NAMESPACE)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "dcr:locate-by-uuid element must at least contain a single DAV:href child."); + } + // immediately build the final multistatus element + try { + Element hrefElem = info.getContentElement(DavConstants.XML_HREF, DavConstants.NAMESPACE); + String uuid = DomUtil.getTextTrim(hrefElem); + DavResourceLocator resourceLoc = resource.getLocator(); + Node n = getRepositorySession().getNodeByUUID(uuid); + DavResourceLocator loc = resourceLoc.getFactory().createResourceLocator(resourceLoc.getPrefix(), resourceLoc.getWorkspacePath(), n.getPath(), false); + DavResource locatedResource = resource.getFactory().createResource(loc, resource.getSession()); + ms = new MultiStatus(); + ms.addResourceProperties(locatedResource, info.getPropertyNameSet(), info.getDepth()); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Creates a Xml document from the generated view. + * + * @param document + * @return Xml element representing the output of the specified view. + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + return ms.toXml(document); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/LocateCorrespondingNodeReport.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/LocateCorrespondingNodeReport.java new file mode 100644 index 00000000000..4bd9099a08b --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/LocateCorrespondingNodeReport.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr.version.report; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * LocateCorrespondingNodeReport is used to identify the resource that + * represents the corresponding node in another workspace. + * + *

        + * The request body must be a 'dcr:locate-corresponding-node' XML element, that + * contains the href of the source workspace, where the corresponding node should + * be searched: + *

        + * <!ELEMENT locate-corresponding-node ( workspace ) >
        + * <!ELEMENT workspace ( href ) >  (as defined by RFC 3253)
        + * 
        + * The response to a successful report request must be a 'dcr:locate-corresponding-node-report' + * element that contains the href of the corresponding node in the given source + * workspace: + * + *
        + * <!ELEMENT locate-corresponding-node-report ( href ) >
        + * 
        + * + * @see javax.jcr.Node#getCorrespondingNodePath(String) + */ +public class LocateCorrespondingNodeReport extends AbstractJcrReport { + + private static Logger log = LoggerFactory.getLogger(LocateCorrespondingNodeReport.class); + + private String correspHref; + + /** + * The corresponding-node report type + */ + public static final ReportType LOCATE_CORRESPONDING_NODE_REPORT = ReportType.register(JcrRemotingConstants.REPORT_LOCATE_CORRESPONDING_NODE, ItemResourceConstants.NAMESPACE, LocateByUuidReport.class); + + /** + * Returns {@link #LOCATE_CORRESPONDING_NODE_REPORT} + * + * @return always returns {@link #LOCATE_CORRESPONDING_NODE_REPORT} + * @see org.apache.jackrabbit.webdav.version.report.Report#getType() + */ + public ReportType getType() { + return LOCATE_CORRESPONDING_NODE_REPORT; + } + + /** + * Always returns false. + * + * @return false + * @see org.apache.jackrabbit.webdav.version.report.Report#isMultiStatusReport() + */ + public boolean isMultiStatusReport() { + return false; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + // general validation checks + super.init(resource, info); + // specific for this report: a workspace href must be provided + Element workspace = info.getContentElement(DeltaVConstants.WORKSPACE.getName(), DeltaVConstants.WORKSPACE.getNamespace()); + String workspaceHref = DomUtil.getChildTextTrim(workspace, DavConstants.XML_HREF, DavConstants.NAMESPACE); + if (workspaceHref == null || "".equals(workspaceHref)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Request body must define the href of a source workspace"); + } + // retrieve href of the corresponding resource in the other workspace + try { + this.correspHref = getCorrespondingResourceHref(resource, getRepositorySession(), workspaceHref); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element elem = DomUtil.createElement(document, "locate-corresponding-node-report", ItemResourceConstants.NAMESPACE); + if (correspHref != null) { + elem.appendChild(DomUtil.hrefToXml(correspHref, document)); + } + return elem; + } + + /** + * Retrieve the href of the corresponding resource in the indicated workspace. + * + * @param resource + * @param session Session object used to access the {@link Node} object + * represented by the given resource. + * @param workspaceHref + * @return + * @throws RepositoryException + */ + private static String getCorrespondingResourceHref(DavResource resource, Session session, String workspaceHref) throws RepositoryException { + DavResourceLocator rLoc = resource.getLocator(); + String itemPath = rLoc.getRepositoryPath(); + Item item = session.getItem(itemPath); + if (item.isNode()) { + String workspaceName = rLoc.getFactory().createResourceLocator(rLoc.getPrefix(), workspaceHref).getWorkspaceName(); + String corrPath = ((Node)item).getCorrespondingNodePath(workspaceName); + DavResourceLocator corrLoc = rLoc.getFactory().createResourceLocator(rLoc.getPrefix(), "/" + workspaceName, corrPath, false); + return corrLoc.getHref(true); + } else { + throw new PathNotFoundException("Node with path " + itemPath + " does not exist."); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/NodeTypesReport.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/NodeTypesReport.java new file mode 100644 index 00000000000..8bb38e1a8cc --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/NodeTypesReport.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.webdav.jcr.version.report; + +import org.apache.jackrabbit.commons.iterator.NodeTypeIteratorAdapter; +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.commons.webdav.NodeTypeConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.webdav.jcr.nodetype.PropertyDefinitionImpl; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import java.util.ArrayList; +import java.util.List; + +/** + * NodeTypesReport allows to retrieve the definition of a single + * or multiple node types. The request body must be a 'dcr:nodetypes' element: + *
        + * <!ELEMENT nodetypes ( nodetype+ | all-nodetypes | mixin-nodetypes | primary-nodetypes ) >
        + *
        + * <!ELEMENT nodetype ( nodetypename ) >
        + * <!ELEMENT nodetypename (#PCDATA) >
        + *
        + * <!ELEMENT all-nodetypes EMPTY >
        + * <!ELEMENT mixin-nodetypes EMPTY >
        + * <!ELEMENT primary-nodetypes EMPTY >
        + * 
        + */ +//todo: currently the nodetype report is not consistent with the general way of representing nodetype names (with NodetypeElement) in order to be compatible with the jackrabbit nodetype registry... +//todo: for the same reason, not the complete nodetype-definition, but only the nodetype def as stored is represented. +//todo: no namespace definition with response (> jackrabbit)... and nodetype element has same name as the one used with dav-properties +public class NodeTypesReport extends AbstractJcrReport implements NodeTypeConstants { + + private static Logger log = LoggerFactory.getLogger(NodeTypesReport.class); + + /** + * The registered type of this report. + */ + public static final ReportType NODETYPES_REPORT = ReportType.register(JcrRemotingConstants.REPORT_NODETYPES, ItemResourceConstants.NAMESPACE, NodeTypesReport.class); + + private NodeTypeIterator ntIter; + + /** + * Returns {@link #NODETYPES_REPORT} type. + * @return {@link #NODETYPES_REPORT} + * @see org.apache.jackrabbit.webdav.version.report.Report#getType() + */ + public ReportType getType() { + return NODETYPES_REPORT; + } + + /** + * Always returns false. + * + * @return false + * @see org.apache.jackrabbit.webdav.version.report.Report#isMultiStatusReport() + */ + public boolean isMultiStatusReport() { + return false; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + // delegate basic validation to super class + super.init(resource, info); + // report specific validation and preparation for xml serialization + try { + ntIter = getNodeTypes(getRepositorySession(), info); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + if (ntIter == null) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * Returns a Xml representation of the node type definition(s) according + * to the info object. + * + * @param document + * @return Xml representation of the node type definition(s) + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element report = document.createElement(NODETYPES_ELEMENT); + // loop over the nodetypes to be returned in the report + while (ntIter.hasNext()) { + NodeType nt = ntIter.nextNodeType(); + Element ntDef = document.createElement(NODETYPE_ELEMENT); + ntDef.setAttribute(NAME_ATTRIBUTE, nt.getName()); + ntDef.setAttribute(ISMIXIN_ATTRIBUTE, Boolean.toString(nt.isMixin())); + ntDef.setAttribute(HASORDERABLECHILDNODES_ATTRIBUTE, Boolean.toString(nt.hasOrderableChildNodes())); + // JCR 2.0 extension + ntDef.setAttribute(ISABSTRACT_ATTRIBUTE, Boolean.toString(nt.isAbstract())); + // JCR 2.0 extension + ntDef.setAttribute(ISQUERYABLE_ATTRIBUTE, Boolean.toString(nt.isQueryable())); + + // declared supertypes + Element supertypes = DomUtil.addChildElement(ntDef, SUPERTYPES_ELEMENT, null); + for (NodeType snt : nt.getDeclaredSupertypes()) { + DomUtil.addChildElement(supertypes, SUPERTYPE_ELEMENT, null, snt.getName()); + } + + // declared child node definitions + for (NodeDefinition aCnd : nt.getChildNodeDefinitions()) { + if (aCnd.getDeclaringNodeType().getName().equals(nt.getName())) { + ntDef.appendChild(NodeDefinitionImpl.create(aCnd).toXml(document)); + } + } + + // declared property definitions + for (PropertyDefinition aPd : nt.getPropertyDefinitions()) { + if (aPd.getDeclaringNodeType().getName().equals(nt.getName())) { + ntDef.appendChild(PropertyDefinitionImpl.create(aPd).toXml(document)); + } + } + + String primaryItemName = nt.getPrimaryItemName(); + if (primaryItemName != null) { + ntDef.setAttribute(PRIMARYITEMNAME_ATTRIBUTE, primaryItemName); + } + report.appendChild(ntDef); + } + return report; + } + + /** + * Parse the Xml element in the info object an return an interator over + * the specified node types. + * + * @return + * @throws RepositoryException + * @throws DavException + */ + private static NodeTypeIterator getNodeTypes(Session session, ReportInfo info) throws RepositoryException, DavException { + NodeTypeManager ntMgr = session.getWorkspace().getNodeTypeManager(); + + // check the simple types first... + if (info.containsContentElement(XML_REPORT_ALLNODETYPES, ItemResourceConstants.NAMESPACE)) { + return ntMgr.getAllNodeTypes(); + } else if (info.containsContentElement(XML_REPORT_MIXINNODETYPES, ItemResourceConstants.NAMESPACE)) { + return ntMgr.getMixinNodeTypes(); + } else if (info.containsContentElement(XML_REPORT_PRIMARYNODETYPES, ItemResourceConstants.NAMESPACE)) { + return ntMgr.getPrimaryNodeTypes(); + } else { + // None of the simple types. test if a report for individual + // nodetype was request. If not, the request body is not valid. + List elemList = info.getContentElements(XML_NODETYPE, ItemResourceConstants.NAMESPACE); + if (elemList.isEmpty()) { + // throw exception if the request body does not contain a single nodetype element + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "NodeTypes report: request body has invalid format."); + } + + // todo: find better solution... + List ntList = new ArrayList(); + for (Element el : elemList) { + String nodetypeName = DomUtil.getChildTextTrim(el, XML_NODETYPENAME, ItemResourceConstants.NAMESPACE); + if (nodetypeName != null) { + ntList.add(ntMgr.getNodeType(nodetypeName)); + } + } + return new NodeTypeIteratorAdapter(ntList); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/RegisteredNamespacesReport.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/RegisteredNamespacesReport.java new file mode 100644 index 00000000000..37ec85e0d1f --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/RegisteredNamespacesReport.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.webdav.jcr.version.report; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; + +/** + * RegisteredNamespacesReport let the client retrieve the namespaces + * registered on the repository. + *

        + * Request body: + *

        + * <!ELEMENT registerednamespaces EMPTY >
        + * 
        + * + * Response body: + *
        + * <!ELEMENT registerednamespaces-report (namespace)* >
        + * <!ELEMENT namespace (prefix, uri) >
        + * <!ELEMENT prefix (#PCDATA) >
        + * <!ELEMENT uri (#PCDATA) >
        + * 
        + * + * @see javax.jcr.Workspace#getNamespaceRegistry() + */ +public class RegisteredNamespacesReport extends AbstractJcrReport implements ItemResourceConstants { + + private static Logger log = LoggerFactory.getLogger(RegisteredNamespacesReport.class); + + /** + * The registered type of this report. + */ + public static final ReportType REGISTERED_NAMESPACES_REPORT = ReportType.register(REPORT_REGISTERED_NAMESPACES, ItemResourceConstants.NAMESPACE, RegisteredNamespacesReport.class); + + private NamespaceRegistry nsReg; + + /** + * Returns {@link #REGISTERED_NAMESPACES_REPORT} type. + * @return {@link #REGISTERED_NAMESPACES_REPORT} + * @see org.apache.jackrabbit.webdav.version.report.Report#getType() + */ + public ReportType getType() { + return REGISTERED_NAMESPACES_REPORT; + } + + /** + * Always returns false. + * + * @return false + * @see org.apache.jackrabbit.webdav.version.report.Report#isMultiStatusReport() + */ + public boolean isMultiStatusReport() { + return false; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + // delegate validation to abstract super class + super.init(resource, info); + try { + nsReg = getRepositorySession().getWorkspace().getNamespaceRegistry(); + } catch (RepositoryException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * Returns a Xml representation of the registered namespace(s). + * + * @return Xml representation of the registered namespace(s) + * error occurs while retrieving the namespaces. + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Element report = DomUtil.createElement(document, "registerednamespaces-report", NAMESPACE); + try { + for (String prefix : nsReg.getPrefixes()) { + Element elem = DomUtil.addChildElement(report, XML_NAMESPACE, NAMESPACE); + DomUtil.addChildElement(elem, XML_PREFIX, NAMESPACE, prefix); + DomUtil.addChildElement(elem, XML_URI, NAMESPACE, nsReg.getURI(prefix)); + } + } catch (RepositoryException e) { + // should not occur. + log.error(e.getMessage()); + } + return report; + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/RepositoryDescriptorsReport.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/RepositoryDescriptorsReport.java new file mode 100644 index 00000000000..9cc40421a5d --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/jcr/version/report/RepositoryDescriptorsReport.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.webdav.jcr.version.report; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.jcr.Repository; +import javax.jcr.Value; +import javax.jcr.RepositoryException; +import javax.jcr.PropertyType; + +/** + * RepositoryDescriptorsReport allows to retrieve the repository + * descriptors. The request body must be an empty 'dcr:repositorydescriptors' element: + *
        + * <!ELEMENT repositorydescriptors EMPTY >
        + * 
        + *
        + * The response body must match the following format + * + *
        + * <!ELEMENT repositorydescriptors-report ( descriptor )* >
        + * <!ELEMENT descriptor ( descriptorkey, descriptorvalue+ ) >
        + * <!ELEMENT descriptorkey (#PCDATA) >
        + * <!ELEMENT descriptorvalue (#PCDATA) >
        + * <!ATTLIST descriptorvalue
        + *      type ( Reference | Path | Name | Boolean | String | Date | Double |
        +               Long | Binary | WeakReference | URI | Decimal )
        +   >
        + * 
        + * + * @see javax.jcr.Repository#getDescriptorKeys() + * @see javax.jcr.Repository#getDescriptor(String) + * @see javax.jcr.Repository#getDescriptorValue(String) + * @see javax.jcr.Repository#getDescriptorValues(String) + */ +public class RepositoryDescriptorsReport extends AbstractJcrReport implements ItemResourceConstants { + + private static Logger log = LoggerFactory.getLogger(RepositoryDescriptorsReport.class); + + /** + * The registered type of this report. + */ + public static final ReportType REPOSITORY_DESCRIPTORS_REPORT = ReportType.register(REPORT_REPOSITORY_DESCRIPTORS, ItemResourceConstants.NAMESPACE, RepositoryDescriptorsReport.class); + + /** + * Returns {@link #REPOSITORY_DESCRIPTORS_REPORT} type. + * @return {@link #REPOSITORY_DESCRIPTORS_REPORT} + * @see org.apache.jackrabbit.webdav.version.report.Report#getType() + */ + public ReportType getType() { + return REPOSITORY_DESCRIPTORS_REPORT; + } + + /** + * Always returns false. + * + * @return false + * @see org.apache.jackrabbit.webdav.version.report.Report#isMultiStatusReport() + */ + public boolean isMultiStatusReport() { + return false; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + // delegate validation to abstract super class + super.init(resource, info); + } + + /** + * Returns a Xml representation of the repository descriptors according + * to the info object. + * + * @return Xml representation of the repository descriptors + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Repository repository = getRepositorySession().getRepository(); + Element report = DomUtil.createElement(document, "repositorydescriptors-report", NAMESPACE); + for (String key : repository.getDescriptorKeys()) { + Element elem = DomUtil.addChildElement(report, XML_DESCRIPTOR, NAMESPACE); + DomUtil.addChildElement(elem, XML_DESCRIPTORKEY, NAMESPACE, key); + for (Value v : repository.getDescriptorValues(key)) { + String value; + try { + value = v.getString(); + } catch (RepositoryException e) { + log.error("Internal error while reading descriptor value: ", e); + value = repository.getDescriptor(key); + } + Element child = DomUtil.addChildElement(elem, XML_DESCRIPTORVALUE, NAMESPACE, value); + if (PropertyType.STRING != v.getType()) { + child.setAttribute(ATTR_VALUE_TYPE, PropertyType.nameFromValue(v.getType())); + } + } + } + return report; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavResourceImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavResourceImpl.java new file mode 100644 index 00000000000..0bf7b7152b5 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavResourceImpl.java @@ -0,0 +1,1128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.simple; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.server.io.AbstractExportContext; +import org.apache.jackrabbit.server.io.CopyMoveContextImpl; +import org.apache.jackrabbit.server.io.DefaultIOListener; +import org.apache.jackrabbit.server.io.DeleteContextImpl; +import org.apache.jackrabbit.server.io.DeleteManager; +import org.apache.jackrabbit.server.io.ExportContext; +import org.apache.jackrabbit.server.io.ExportContextImpl; +import org.apache.jackrabbit.server.io.IOListener; +import org.apache.jackrabbit.server.io.IOManager; +import org.apache.jackrabbit.server.io.IOUtil; +import org.apache.jackrabbit.server.io.ImportContext; +import org.apache.jackrabbit.server.io.ImportContextImpl; +import org.apache.jackrabbit.server.io.PropertyExportContext; +import org.apache.jackrabbit.server.io.PropertyImportContext; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavCompliance; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.bind.BindConstants; +import org.apache.jackrabbit.webdav.bind.BindableResource; +import org.apache.jackrabbit.webdav.bind.ParentElement; +import org.apache.jackrabbit.webdav.bind.ParentSet; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.jcr.lock.JcrActiveLock; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockDiscovery; +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.SupportedLock; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.ResourceType; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Workspace; +import javax.jcr.lock.Lock; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * DavResourceImpl implements a DavResource. + */ +public class DavResourceImpl implements DavResource, BindableResource, JcrConstants { + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(DavResourceImpl.class); + + public static final String METHODS = DavResource.METHODS + ", " + BindConstants.METHODS; + + public static final String COMPLIANCE_CLASSES = DavCompliance.concatComplianceClasses( + new String[] { + DavCompliance._1_, + DavCompliance._2_, + DavCompliance._3_, + DavCompliance.BIND + } + ); + + private DavResourceFactory factory; + private LockManager lockManager; + private JcrDavSession session; + private Node node; + private DavResourceLocator locator; + + protected DavPropertySet properties = new DavPropertySet(); + protected boolean propsInitialized = false; + private boolean isCollection = true; + private String rfc4122Uri; + + private ResourceConfig config; + private long modificationTime = IOUtil.UNDEFINED_TIME; + + /** + * Create a new {@link DavResource}. + * + * @param locator + * @param factory + * @param session + * @param config + * @param isCollection + * @throws DavException + */ + public DavResourceImpl(DavResourceLocator locator, DavResourceFactory factory, + DavSession session, ResourceConfig config, + boolean isCollection) throws DavException { + this(locator, factory, session, config, null); + this.isCollection = isCollection; + } + + /** + * Create a new {@link DavResource}. + * + * @param locator + * @param factory + * @param session + * @param config + * @param node + * @throws DavException + */ + public DavResourceImpl(DavResourceLocator locator, DavResourceFactory factory, + DavSession session, ResourceConfig config, Node node) throws DavException { + if (locator == null || session == null || config == null) { + throw new IllegalArgumentException(); + } + JcrDavSession.checkImplementation(session); + this.session = (JcrDavSession)session; + this.factory = factory; + this.locator = locator; + this.config = config; + + if (locator.getResourcePath() != null) { + if (node != null) { + this.node = node; + // define what is a collection in webdav + isCollection = config.isCollectionResource(node); + initRfc4122Uri(); + } + } else { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + } + + /** + * If the Node associated with this DavResource has a UUID that allows for the creation of a rfc4122 compliant + * URI, we use it as the value of the protected DAV property DAV:resource-id, which is defined by the BIND + * specification. + */ + private void initRfc4122Uri() { + try { + if (node.isNodeType(MIX_REFERENCEABLE)) { + String uuid = node.getUUID(); + try { + UUID.fromString(uuid); + rfc4122Uri = "urn:uuid:" + uuid; + } catch (IllegalArgumentException e) { + //no, this is not a UUID + } + } + } catch (RepositoryException e) { + log.warn("Error while detecting UUID", e); + } + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() + */ + public String getComplianceClass() { + return COMPLIANCE_CLASSES; + } + + /** + * @return DavResource#METHODS + * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() + */ + public String getSupportedMethods() { + return METHODS; + } + + /** + * @see DavResource#exists() ) + */ + public boolean exists() { + return node != null; + } + + /** + * @see DavResource#isCollection() + */ + public boolean isCollection() { + return isCollection; + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getLocator() + */ + public DavResourceLocator getLocator() { + return locator; + } + + /** + * @see DavResource#getResourcePath() + */ + public String getResourcePath() { + return locator.getResourcePath(); + } + + /** + * @see DavResource#getHref() + */ + public String getHref() { + return locator.getHref(isCollection()); + } + + /** + * Returns the the last segment of the resource path.

        + * Note that this must not correspond to the name of the underlying + * repository item for two reasons:

          + *
        • SameNameSiblings have an index appended to their item name.
        • + *
        • the resource path may differ from the item path.
        • + *
        + * Using the item name as DAV:displayname caused problems with XP built-in + * client in case of resources representing SameNameSibling nodes. + * + * @see DavResource#getDisplayName() + */ + public String getDisplayName() { + String resPath = getResourcePath(); + return (resPath != null) ? Text.getName(resPath) : resPath; + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getModificationTime() + */ + public long getModificationTime() { + initProperties(); + return modificationTime; + } + + /** + * If this resource exists and the specified context is not null + * this implementation build a new {@link ExportContext} based on the specified + * context and forwards the export to its IOManager. If the + * {@link IOManager#exportContent(ExportContext, DavResource)} fails, + * an IOException is thrown. + * + * @see DavResource#spool(OutputContext) + * @see ResourceConfig#getIOManager() + * @throws IOException if the export fails. + */ + public void spool(OutputContext outputContext) throws IOException { + if (exists() && outputContext != null) { + ExportContext exportCtx = getExportContext(outputContext); + if (!config.getIOManager().exportContent(exportCtx, this)) { + throw new IOException("Unexpected Error while spooling resource."); + } + } + } + + /** + * @see DavResource#getProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) + */ + public DavProperty getProperty(DavPropertyName name) { + initProperties(); + return properties.get(name); + } + + /** + * @see DavResource#getProperties() + */ + public DavPropertySet getProperties() { + initProperties(); + return properties; + } + + /** + * @see DavResource#getPropertyNames() + */ + public DavPropertyName[] getPropertyNames() { + return getProperties().getPropertyNames(); + } + + /** + * Fill the set of properties + */ + protected void initProperties() { + if (!exists() || propsInitialized) { + return; + } + + try { + config.getPropertyManager().exportProperties(getPropertyExportContext(), isCollection()); + } catch (RepositoryException e) { + log.warn("Error while accessing resource properties", e); + } + + // set (or reset) fundamental properties + if (getDisplayName() != null) { + properties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, getDisplayName())); + } + if (isCollection()) { + properties.add(new ResourceType(ResourceType.COLLECTION)); + // Windows XP support + properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, "1")); + } else { + properties.add(new ResourceType(ResourceType.DEFAULT_RESOURCE)); + // Windows XP support + properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, "0")); + } + + if (rfc4122Uri != null) { + properties.add(new HrefProperty(BindConstants.RESOURCEID, rfc4122Uri, true)); + } + + Set parentElements = getParentElements(); + if (!parentElements.isEmpty()) { + properties.add(new ParentSet(parentElements)); + } + + /* set current lock information. If no lock is set to this resource, + an empty lock discovery will be returned in the response. */ + properties.add(new LockDiscovery(getLock(Type.WRITE, Scope.EXCLUSIVE))); + + /* lock support information: all locks are lockable. */ + SupportedLock supportedLock = new SupportedLock(); + supportedLock.addEntry(Type.WRITE, Scope.EXCLUSIVE); + properties.add(supportedLock); + + propsInitialized = true; + } + + /** + * @param property + * @throws DavException + * @see DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty) + */ + public void setProperty(DavProperty property) throws DavException { + alterProperty(property); + } + + /** + * @param propertyName + * @throws DavException + * @see DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName) + */ + public void removeProperty(DavPropertyName propertyName) throws DavException { + alterProperty(propertyName); + } + + private void alterProperty(PropEntry prop) throws DavException { + if (isLocked(this)) { + throw new DavException(DavServletResponse.SC_LOCKED); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + try { + List list = Collections.singletonList(prop); + alterProperties(list); + Map failure = config.getPropertyManager().alterProperties(getPropertyImportContext(list), isCollection()); + if (failure.isEmpty()) { + node.save(); + } else { + node.refresh(false); + // TODO: retrieve specific error from failure-map + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } catch (RepositoryException e) { + // revert any changes made so far + JcrDavException je = new JcrDavException(e); + try { + node.refresh(false); + } catch (RepositoryException re) { + // should not happen... + } + throw je; + } + } + + public MultiStatusResponse alterProperties(List changeList) throws DavException { + if (isLocked(this)) { + throw new DavException(DavServletResponse.SC_LOCKED); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + MultiStatusResponse msr = new MultiStatusResponse(getHref(), null); + try { + Map failures = config.getPropertyManager().alterProperties(getPropertyImportContext(changeList), isCollection()); + if (failures.isEmpty()) { + // save all changes together (reverted in case this fails) + node.save(); + } else { + // set/remove of at least a single prop failed: undo modifications. + node.refresh(false); + } + /* loop over list of properties/names that were successfully altered + and them to the multistatus response respecting the result of the + complete action. in case of failure set the status to 'failed-dependency' + in order to indicate, that altering those names/properties would + have succeeded, if no other error occured.*/ + for (PropEntry propEntry : changeList) { + int statusCode; + if (failures.containsKey(propEntry)) { + Object error = failures.get(propEntry); + statusCode = (error instanceof RepositoryException) + ? new JcrDavException((RepositoryException) error).getErrorCode() + : DavServletResponse.SC_INTERNAL_SERVER_ERROR; + } else { + statusCode = (failures.isEmpty()) ? DavServletResponse.SC_OK : DavServletResponse.SC_FAILED_DEPENDENCY; + } + if (propEntry instanceof DavProperty) { + msr.add(((DavProperty) propEntry).getName(), statusCode); + } else { + msr.add((DavPropertyName) propEntry, statusCode); + } + } + return msr; + } catch (RepositoryException e) { + // revert any changes made so far an throw exception + try { + node.refresh(false); + } catch (RepositoryException re) { + // should not happen + } + throw new JcrDavException(e); + } + } + + /** + * @see DavResource#getCollection() + */ + public DavResource getCollection() { + DavResource parent = null; + if (getResourcePath() != null && !getResourcePath().equals("/")) { + String parentPath = Text.getRelativeParent(getResourcePath(), 1); + if (parentPath.equals("")) { + parentPath = "/"; + } + DavResourceLocator parentloc = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentPath); + try { + parent = factory.createResource(parentloc, session); + } catch (DavException e) { + // should not occur + } + } + return parent; + } + + /** + * @see DavResource#getMembers() + */ + public DavResourceIterator getMembers() { + ArrayList list = new ArrayList(); + if (exists() && isCollection()) { + try { + NodeIterator it = node.getNodes(); + while (it.hasNext()) { + Node n = it.nextNode(); + if (!isFilteredItem(n)) { + DavResourceLocator resourceLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), n.getPath(), false); + DavResource childRes = factory.createResource(resourceLocator, session); + list.add(childRes); + } else { + log.debug("Filtered resource '" + n.getName() + "'."); + } + } + } catch (RepositoryException e) { + // should not occur + } catch (DavException e) { + // should not occur + } + } + return new DavResourceIteratorImpl(list); + } + + /** + * Adds a new member to this resource. + * + * @see DavResource#addMember(DavResource, org.apache.jackrabbit.webdav.io.InputContext) + */ + public void addMember(DavResource member, InputContext inputContext) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_CONFLICT); + } + if (isLocked(this) || isLocked(member)) { + throw new DavException(DavServletResponse.SC_LOCKED); + } + try { + // don't allow creation of nodes if this resource represents a protected + // item or if the new resource would be filtered out + if (isFilteredResource(member) || node.getDefinition().isProtected()) { + log.debug("Forbidden to add member: " + member.getDisplayName()); + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + String memberName = Text.getName(member.getLocator().getRepositoryPath()); + ImportContext ctx = getImportContext(inputContext, memberName); + if (!config.getIOManager().importContent(ctx, member)) { + // any changes should have been reverted in the importer + throw new DavException(DavServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + } + // persist changes after successful import + node.save(); + } catch (RepositoryException e) { + log.error("Error while importing resource: " + e.toString()); + throw new JcrDavException(e); + } catch (IOException e) { + log.error("Error while importing resource: " + e.toString()); + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + /** + * @see DavResource#removeMember(DavResource) + */ + public void removeMember(DavResource member) throws DavException { + if (!exists() || !member.exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (isLocked(this) || isLocked(member)) { + throw new DavException(DavServletResponse.SC_LOCKED); + } + + // don't allow removal of nodes, that would be filtered out + if (isFilteredResource(member)) { + log.debug("Avoid removal of filtered resource: " + member.getDisplayName()); + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + DeleteManager dm = config.getDeleteManager(); + dm.delete(new DeleteContextImpl(getJcrSession()), member); + + // make sure, non-jcr locks are removed, once the removal is completed + try { + if (!isJcrLockable()) { + ActiveLock lock = getLock(Type.WRITE, Scope.EXCLUSIVE); + if (lock != null) { + lockManager.releaseLock(lock.getToken(), member); + } + } + } catch (DavException e) { + // since check for 'locked' exception has been performed before + // ignore any error here + } + } + + /** + * @see DavResource#move(DavResource) + */ + public void move(DavResource destination) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (isLocked(this)) { + throw new DavException(DavServletResponse.SC_LOCKED); + } + if (isFilteredResource(destination)) { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + // make sure, that src and destination belong to the same workspace + checkSameWorkspace(destination.getLocator()); + if (!config.getCopyMoveManager().move(new CopyMoveContextImpl(getJcrSession()), this, destination)) { + throw new DavException(DavServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + } + } + + /** + * @see DavResource#copy(DavResource, boolean) + */ + public void copy(DavResource destination, boolean shallow) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (isLocked(destination)) { + throw new DavException(DavServletResponse.SC_LOCKED); + } + if (isFilteredResource(destination)) { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + // make sure, that src and destination belong to the same workspace + checkSameWorkspace(destination.getLocator()); + if (!config.getCopyMoveManager().copy(new CopyMoveContextImpl(getJcrSession(), shallow), this, destination)) { + throw new DavException(DavServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + } + } + + /** + * @param type + * @param scope + * @return true if type is {@link Type#WRITE} and scope is {@link Scope#EXCLUSIVE} + * @see DavResource#isLockable(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) + */ + public boolean isLockable(Type type, Scope scope) { + return Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope); + } + + /** + * @see DavResource#hasLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) + */ + public boolean hasLock(Type type, Scope scope) { + return getLock(type, scope) != null; + } + + /** + * @see DavResource#getLock(Type, Scope) + */ + public ActiveLock getLock(Type type, Scope scope) { + ActiveLock lock = null; + if (exists() && Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope)) { + // try to retrieve the repository lock information first + try { + if (node.isLocked()) { + Lock jcrLock = node.getLock(); + if (jcrLock != null && jcrLock.isLive()) { + lock = new JcrActiveLock(jcrLock); + String lockroot = locator + .getFactory() + .createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), + jcrLock.getNode().getPath(), false).getHref(false); + lock.setLockroot(lockroot); + } + } + } catch (RepositoryException e) { + // LockException (no lock applies) >> should never occur + // RepositoryException, AccessDeniedException or another error >> ignore + } + + // could not retrieve a jcr-lock. test if a simple webdav lock is present. + if (lock == null) { + lock = lockManager.getLock(type, scope, this); + } + } + return lock; + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getLocks() + */ + public ActiveLock[] getLocks() { + ActiveLock writeLock = getLock(Type.WRITE, Scope.EXCLUSIVE); + return (writeLock != null) ? new ActiveLock[]{writeLock} : new ActiveLock[0]; + } + + /** + * @see DavResource#lock(LockInfo) + */ + public ActiveLock lock(LockInfo lockInfo) throws DavException { + ActiveLock lock = null; + if (isLockable(lockInfo.getType(), lockInfo.getScope())) { + // TODO: deal with existing locks, that may have been created, before the node was jcr-lockable... + if (isJcrLockable()) { + try { + javax.jcr.lock.LockManager lockMgr = node.getSession().getWorkspace().getLockManager(); + long timeout = lockInfo.getTimeout(); + if (timeout == LockInfo.INFINITE_TIMEOUT) { + timeout = Long.MAX_VALUE; + } else { + timeout = timeout / 1000; + } + // try to execute the lock operation + Lock jcrLock = lockMgr.lock(node.getPath(), lockInfo.isDeep(), false, timeout, lockInfo.getOwner()); + if (jcrLock != null) { + lock = new JcrActiveLock(jcrLock); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else { + // create a new webdav lock + lock = lockManager.createLock(lockInfo, this); + } + } else { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "Unsupported lock type or scope."); + } + return lock; + } + + /** + * @see DavResource#refreshLock(LockInfo, String) + */ + public ActiveLock refreshLock(LockInfo lockInfo, String lockToken) throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + ActiveLock lock = getLock(lockInfo.getType(), lockInfo.getScope()); + if (lock == null) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No lock with the given type/scope present on resource " + getResourcePath()); + } + + if (lock instanceof JcrActiveLock) { + try { + // refresh JCR lock and return the original lock object. + node.getLock().refresh(); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else { + lock = lockManager.refreshLock(lockInfo, lockToken, this); + } + /* since lock has infinite lock (simple) or undefined timeout (jcr) + return the lock as retrieved from getLock. */ + return lock; + } + + /** + * @see DavResource#unlock(String) + */ + public void unlock(String lockToken) throws DavException { + ActiveLock lock = getLock(Type.WRITE, Scope.EXCLUSIVE); + if (lock == null) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } else if (lock.isLockedByToken(lockToken)) { + if (lock instanceof JcrActiveLock) { + try { + node.unlock(); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else { + lockManager.releaseLock(lockToken, this); + } + } else { + throw new DavException(DavServletResponse.SC_LOCKED); + } + } + + /** + * @see DavResource#addLockManager(org.apache.jackrabbit.webdav.lock.LockManager) + */ + public void addLockManager(LockManager lockMgr) { + this.lockManager = lockMgr; + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getFactory() + */ + public DavResourceFactory getFactory() { + return factory; + } + + /** + * @see org.apache.jackrabbit.webdav.DavResource#getSession() + */ + public DavSession getSession() { + return session; + } + + + /** + * @see BindableResource#rebind(DavResource, DavResource) + */ + public void bind(DavResource collection, DavResource newBinding) throws DavException { + if (!exists()) { + //DAV:bind-source-exists + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + if (isLocked(collection)) { + //DAV:locked-update-allowed? + throw new DavException(DavServletResponse.SC_LOCKED); + } + if (isFilteredResource(newBinding)) { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + checkSameWorkspace(collection.getLocator()); + try { + if (!node.isNodeType(MIX_SHAREABLE)) { + if (!node.canAddMixin(MIX_SHAREABLE)) { + //DAV:binding-allowed + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + node.addMixin(MIX_SHAREABLE); + node.save(); + } + Workspace workspace = session.getRepositorySession().getWorkspace(); + workspace.clone(workspace.getName(), node.getPath(), newBinding.getLocator().getRepositoryPath(), false); + + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + + } + + /** + * @see BindableResource#rebind(DavResource, DavResource) + */ + public void rebind(DavResource collection, DavResource newBinding) throws DavException { + if (!exists()) { + //DAV:rebind-source-exists + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + if (isLocked(this)) { + //DAV:protected-source-url-deletion.allowed + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + if (isLocked(collection)) { + //DAV:locked-update-allowed? + throw new DavException(DavServletResponse.SC_LOCKED); + } + if (isFilteredResource(newBinding)) { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + checkSameWorkspace(collection.getLocator()); + try { + if (!node.isNodeType(MIX_REFERENCEABLE)) { + throw new DavException(node.canAddMixin(MIX_REFERENCEABLE)? + DavServletResponse.SC_CONFLICT : DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + getJcrSession().getWorkspace().move(locator.getRepositoryPath(), newBinding.getLocator().getRepositoryPath()); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * @see org.apache.jackrabbit.webdav.bind.BindableResource#getParentElements() + */ + public Set getParentElements() { + try { + if (node.getDepth() > 0) { + Set ps = new HashSet(); + NodeIterator sharedSetIterator = node.getSharedSet(); + while (sharedSetIterator.hasNext()) { + Node sharednode = sharedSetIterator.nextNode(); + DavResourceLocator loc = locator.getFactory().createResourceLocator( + locator.getPrefix(), locator.getWorkspacePath(), sharednode.getParent().getPath(), false); + ps.add(new ParentElement(loc.getHref(true), sharednode.getName())); + } + return ps; + } + } catch (UnsupportedRepositoryOperationException e) { + log.debug("unable to calculate parent set", e); + } catch (RepositoryException e) { + log.warn("unable to calculate parent set", e); + } + return Collections.emptySet(); + } + + /** + * Returns the node that is wrapped by this resource. + * + * @return The underlying JCR node instance. + */ + protected Node getNode() { + return node; + } + + /** + * Returns a new ImportContext + * + * @param inputCtx + * @param systemId + * @return a new ImportContext + * @throws IOException + */ + protected ImportContext getImportContext(InputContext inputCtx, String systemId) throws IOException { + return new ImportContextImpl( + node, systemId, inputCtx, + (inputCtx != null) ? inputCtx.getInputStream() : null, + new DefaultIOListener(log), config.getDetector()); + } + + /** + * Returns a new ExportContext + * + * @param outputCtx + * @return a new ExportContext + * @throws IOException + */ + protected ExportContext getExportContext(OutputContext outputCtx) throws IOException { + return new ExportContextImpl(node, outputCtx); + } + + /** + * Returns a new PropertyImportContext. + * + * @param changeList + * @return a new PropertyImportContext. + */ + protected PropertyImportContext getPropertyImportContext(List changeList) { + return new PropertyImportCtx(changeList); + } + + /** + * Returns a new PropertyExportContext. + * + * @return a new PropertyExportContext + */ + protected PropertyExportContext getPropertyExportContext() { + return new PropertyExportCtx(); + } + + /** + * Returns true, if the underlying node is nodetype jcr:lockable, + * without checking its current lock status. If the node is not jcr-lockable + * an attempt is made to add the mix:lockable mixin type. + * + * @return true if this resource is lockable. + */ + private boolean isJcrLockable() { + boolean lockable = false; + if (exists()) { + try { + lockable = node.isNodeType(MIX_LOCKABLE); + // not jcr-lockable: try to make the node jcr-lockable + if (!lockable && node.canAddMixin(MIX_LOCKABLE)) { + node.addMixin(MIX_LOCKABLE); + node.save(); + lockable = true; + } + } catch (RepositoryException e) { + // -> node is definitely not jcr-lockable. + } + } + return lockable; + } + + /** + * Return true if this resource cannot be modified due to a write lock + * that is not owned by the given session. + * + * @return true if this resource cannot be modified due to a write lock + */ + private boolean isLocked(DavResource res) { + ActiveLock lock = res.getLock(Type.WRITE, Scope.EXCLUSIVE); + if (lock == null) { + return false; + } else { + for (String sLockToken : session.getLockTokens()) { + if (sLockToken.equals(lock.getToken())) { + return false; + } + } + return true; + } + } + + private Session getJcrSession() { + return session.getRepositorySession(); + } + + private boolean isFilteredResource(DavResource resource) { + // TODO: filtered nodetypes should be checked as well in order to prevent problems. + ItemFilter filter = config.getItemFilter(); + return filter != null && filter.isFilteredItem(resource.getDisplayName(), getJcrSession()); + } + + private boolean isFilteredItem(Item item) { + ItemFilter filter = config.getItemFilter(); + return filter != null && filter.isFilteredItem(item); + } + + private void checkSameWorkspace(DavResourceLocator otherLoc) throws DavException { + String wspname = getJcrSession().getWorkspace().getName(); + if (!wspname.equals(otherLoc.getWorkspaceName())) { + throw new DavException(DavServletResponse.SC_FORBIDDEN, "Workspace mismatch: expected '" + wspname + "'; found: '" + otherLoc.getWorkspaceName() + "'"); + } + } + + //--------------------------------------------------------< inner class >--- + /** + * ExportContext that writes the properties of this DavResource + * and provides not stream. + */ + private class PropertyExportCtx extends AbstractExportContext implements PropertyExportContext { + + private PropertyExportCtx() { + super(node, false, null); + // set defaults: + setCreationTime(IOUtil.UNDEFINED_TIME); + setModificationTime(IOUtil.UNDEFINED_TIME); + } + + public OutputStream getOutputStream() { + return null; + } + + public void setContentLanguage(String contentLanguage) { + if (contentLanguage != null) { + properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTLANGUAGE, contentLanguage)); + } + } + + public void setContentLength(long contentLength) { + if (contentLength > IOUtil.UNDEFINED_LENGTH) { + properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, contentLength + "")); + } + } + + public void setContentType(String mimeType, String encoding) { + String contentType = IOUtil.buildContentType(mimeType, encoding); + if (contentType != null) { + properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTTYPE, contentType)); + } + } + + public void setCreationTime(long creationTime) { + String created = IOUtil.getCreated(creationTime); + properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, created)); + } + + public void setModificationTime(long modTime) { + if (modTime <= IOUtil.UNDEFINED_TIME) { + modificationTime = new Date().getTime(); + } else { + modificationTime = modTime; + } + String lastModified = IOUtil.getLastModified(modificationTime); + properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, lastModified)); + } + + public void setETag(String etag) { + if (etag != null) { + properties.add(new DefaultDavProperty(DavPropertyName.GETETAG, etag)); + } + } + + public void setProperty(Object propertyName, Object propertyValue) { + if (propertyValue == null) { + log.warn("Ignore 'setProperty' for " + propertyName + "with null value."); + return; + } + + if (propertyValue instanceof DavProperty) { + properties.add((DavProperty)propertyValue); + } else { + DavPropertyName pName; + if (propertyName instanceof DavPropertyName) { + pName = (DavPropertyName)propertyName; + } else { + // create property name with default DAV: namespace + pName = DavPropertyName.create(propertyName.toString()); + } + properties.add(new DefaultDavProperty(pName, propertyValue)); + } + } + } + + private class PropertyImportCtx implements PropertyImportContext { + + private final IOListener ioListener = new DefaultIOListener(log); + private final List changeList; + private boolean completed; + + private PropertyImportCtx(List changeList) { + this.changeList = changeList; + } + + /** + * @see PropertyImportContext#getImportRoot() + */ + public Item getImportRoot() { + return node; + } + + /** + * @see PropertyImportContext#getChangeList() + */ + public List getChangeList() { + return Collections.unmodifiableList(changeList); + } + + public IOListener getIOListener() { + return ioListener; + } + + public boolean hasStream() { + return false; + } + + /** + * @see PropertyImportContext#informCompleted(boolean) + */ + public void informCompleted(boolean success) { + checkCompleted(); + completed = true; + } + + /** + * @see PropertyImportContext#isCompleted() + */ + public boolean isCompleted() { + return completed; + } + + /** + * @throws IllegalStateException if the context is already completed. + * @see #isCompleted() + * @see #informCompleted(boolean) + */ + private void checkCompleted() { + if (completed) { + throw new IllegalStateException("PropertyImportContext has already been consumed."); + } + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavSessionImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavSessionImpl.java new file mode 100644 index 00000000000..d465b000923 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavSessionImpl.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.webdav.simple; + +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; + +import javax.jcr.Session; +import java.util.HashSet; + +/** + * Simple implementation of the {@link DavSession} interface. Stores + * lock tokens but does not yet store references. + */ +public class DavSessionImpl extends JcrDavSession { + + /** the lock tokens of this session */ + private final HashSet lockTokens = new HashSet(); + + /** + * Creates a new DavSession based on a jcr session + * @param session + */ + public DavSessionImpl(Session session) { + super(session); + } + + /** + * @see DavSession#addReference(Object) + */ + public void addReference(Object reference) { + throw new UnsupportedOperationException("No yet implemented."); + } + + /** + * @see DavSession#removeReference(Object) + */ + public void removeReference(Object reference) { + throw new UnsupportedOperationException("No yet implemented."); + } + + /** + * @see DavSession#addLockToken(String) + */ + @Override + public void addLockToken(String token) { + super.addLockToken(token); + lockTokens.add(token); + } + + /** + * @see DavSession#getLockTokens() + */ + @Override + public String[] getLockTokens() { + return lockTokens.toArray(new String[lockTokens.size()]); + } + + /** + * @see DavSession#removeLockToken(String) + */ + @Override + public void removeLockToken(String token) { + super.removeLockToken(token); + lockTokens.remove(token); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavSessionProviderImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavSessionProviderImpl.java new file mode 100644 index 00000000000..6fc82bba2dd --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DavSessionProviderImpl.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.webdav.simple; + +import org.apache.jackrabbit.server.CredentialsProvider; +import org.apache.jackrabbit.server.SessionProvider; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.DavSessionProvider; +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.ServletException; + +/** + * Simple implementation of the {@link org.apache.jackrabbit.webdav.DavSessionProvider} + * interface that uses a {@link CredentialsProvider} to locate + * credentials in the request, log into the respository, and provide + * a {@link org.apache.jackrabbit.webdav.DavSession} to the request. + */ +public class DavSessionProviderImpl implements DavSessionProvider { + + private static Logger log = LoggerFactory.getLogger(DavSessionProviderImpl.class); + + /** + * the repository + */ + private final Repository repository; + + /** + * the credentials provider + */ + private final SessionProvider sesProvider; + + /** + * Creates a new DavSessionProviderImpl + * @param rep + * @param sesProvider + */ + public DavSessionProviderImpl(Repository rep, SessionProvider sesProvider) { + this.repository = rep; + this.sesProvider = sesProvider; + } + + /** + * Acquires a DavSession. Upon success, the WebdavRequest will + * reference that session. + * + * A session will not be available if an exception is thrown. + * + * @param request + * @throws org.apache.jackrabbit.webdav.DavException if a problem occurred while obtaining the session + * @see DavSessionProvider#attachSession(org.apache.jackrabbit.webdav.WebdavRequest) + */ + public boolean attachSession(WebdavRequest request) throws DavException { + try { + // retrieve the workspace name + String workspaceName = request.getRequestLocator().getWorkspaceName(); + // empty workspaceName rather means default -> must be 'null' + if (workspaceName != null && "".equals(workspaceName)) { + workspaceName = null; + } + // login to repository + Session repSession = sesProvider.getSession(request, repository, workspaceName); + if (repSession == null) { + log.debug("Could not to retrieve a repository session."); + return false; + } + DavSession ds = new DavSessionImpl(repSession); + log.debug("Attaching session '"+ ds + "' to request '" + request + "'"); + request.setDavSession(ds); + return true; + } catch (NoSuchWorkspaceException e) { + // the default error-code for NoSuchWorkspaceException is 409 conflict + // which seems not appropriate here + throw new JcrDavException(e, DavServletResponse.SC_NOT_FOUND); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } catch (ServletException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + /** + * Only removes the DavSession object from the given request object. + * and remove all the lock tokens from the underlying repository session + * in order make sure they can be reset when attaching a session to the + * next request. Finally the session provider is informed, that the + * session is no longer used. + * + * @param request + * @see DavSessionProvider#releaseSession(org.apache.jackrabbit.webdav.WebdavRequest) + */ + public void releaseSession(WebdavRequest request) { + DavSession ds = request.getDavSession(); + if (ds != null && ds instanceof DavSessionImpl) { + Session repSession = ((DavSessionImpl)ds).getRepositorySession(); + for (String lockToken : repSession.getLockTokens()) { + repSession.removeLockToken(lockToken); + } + sesProvider.releaseSession(repSession); + log.debug("Releasing session '"+ ds + "' from request '" + request + "'"); + } // else : session is null. nothing to be done. + request.setDavSession(null); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DefaultItemFilter.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DefaultItemFilter.java new file mode 100644 index 00000000000..bc1bb537827 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DefaultItemFilter.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.webdav.simple; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.ArrayList; +import java.util.List; + +/** + * DefaultItemFilter... + */ +public class DefaultItemFilter implements ItemFilter { + + private static Logger log = LoggerFactory.getLogger(DefaultItemFilter.class); + + private List prefixFilter = new ArrayList(); + private List uriFilter = new ArrayList(); + private List nodetypeFilter = new ArrayList(); + + public DefaultItemFilter() { + } + + /** + * @see ItemFilter#setFilteredURIs(String[]) + */ + public void setFilteredURIs(String[] uris) { + if (uris != null) { + for (String uri : uris) { + uriFilter.add(uri); + } + } + } + + /** + * @see ItemFilter#setFilteredPrefixes(String[]) + */ + public void setFilteredPrefixes(String[] prefixes) { + if (prefixes != null) { + for (String prefix : prefixes) { + prefixFilter.add(prefix); + } + } + } + + /** + * @see ItemFilter#setFilteredNodetypes(String[]) + */ + public void setFilteredNodetypes(String[] nodetypeNames) { + if (nodetypeNames != null) { + for (String nodetypeName : nodetypeNames) { + nodetypeFilter.add(nodetypeName); + } + } + } + + /** + * Returns true if the given item matches either one of the namespace or + * of the the nodetype filters specified. + * + * @see ItemFilter#isFilteredItem(Item) + */ + public boolean isFilteredItem(Item item) { + return isFilteredNamespace(item) || isFilteredNodeType(item); + } + + /** + * @see ItemFilter#isFilteredItem(String, Session) + */ + public boolean isFilteredItem(String displayName, Session session) { + return isFilteredNamespace(displayName, session); + } + + /** + * + * @param name + * @param session + * @return + */ + private boolean isFilteredNamespace(String name, Session session) { + // shortcut + if (prefixFilter.isEmpty() && uriFilter.isEmpty()) { + return false; + } + int pos = name.indexOf(':'); + if (pos < 0) { + // no namespace info present + return false; + } + try { + String prefix = name.substring(0, pos); + String uri = session.getNamespaceURI(prefix); + return prefixFilter.contains(prefix) || uriFilter.contains(uri); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + return false; + } + + /** + * + * @param item + * @return + */ + private boolean isFilteredNamespace(Item item) { + try { + return isFilteredNamespace(item.getName(), item.getSession()); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + return false; + } + + /** + * + * @param item + * @return + */ + private boolean isFilteredNodeType(Item item) { + // shortcut + if (nodetypeFilter.isEmpty()) { + return false; + } + try { + String ntName; + if (item.isNode()) { + ntName = ((Node) item).getDefinition().getDeclaringNodeType().getName(); + } else { + ntName = ((Property) item).getDefinition().getDeclaringNodeType().getName(); + } + return nodetypeFilter.contains(ntName); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + // nodetype info could not be retrieved + return false; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DeltaVResourceImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DeltaVResourceImpl.java new file mode 100644 index 00000000000..bedd5a45342 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/DeltaVResourceImpl.java @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.simple; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.webdav.DavCompliance; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.apache.jackrabbit.webdav.version.DeltaVResource; +import org.apache.jackrabbit.webdav.version.OptionsInfo; +import org.apache.jackrabbit.webdav.version.OptionsResponse; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +/** + * The DeltaVResourceImpl encapsulates the functionality common to all + * DeltaV compliant resources. + */ +public class DeltaVResourceImpl extends DavResourceImpl implements DeltaVResource { + + protected SupportedReportSetProperty supportedReports = new SupportedReportSetProperty(); + private static final Logger log = LoggerFactory.getLogger(DeltaVResourceImpl.class); + + private static final String DELTAV_COMPLIANCE_CLASSES = DavCompliance.concatComplianceClasses( + new String[] { + DavResourceImpl.COMPLIANCE_CLASSES, + DavCompliance.BIND, + } + ); + + public DeltaVResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session, ResourceConfig config, Item item) throws DavException { + super(locator, factory, session, config, (Node)item); + initSupportedReports(); + } + + public DeltaVResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session, ResourceConfig config, boolean isCollection) throws DavException { + super(locator, factory, session, config, isCollection); + initSupportedReports(); + } + + //---------------------------------------------------------< DavResource>--- + /** + * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass() + */ + @Override + public String getComplianceClass() { + return DELTAV_COMPLIANCE_CLASSES; + } + + //------------------------------------------------------< DeltaVResource>--- + /** + * @param optionsInfo + * @return object to be used in the OPTIONS response body or null + * @see DeltaVResource#getOptionResponse(org.apache.jackrabbit.webdav.version.OptionsInfo) + */ + public OptionsResponse getOptionResponse(OptionsInfo optionsInfo) { + OptionsResponse oR = null; + if (optionsInfo != null) { + oR = new OptionsResponse(); + // currently only DAV:version-history-collection-set is supported + if (optionsInfo.containsElement(DeltaVConstants.XML_VH_COLLECTION_SET, DeltaVConstants.NAMESPACE)) { + String[] hrefs = new String[] { + getLocatorFromNodePath("/"+JcrConstants.JCR_SYSTEM+"/"+JcrConstants.JCR_VERSIONSTORAGE).getHref(true) + }; + oR.addEntry(DeltaVConstants.XML_VH_COLLECTION_SET, DeltaVConstants.NAMESPACE, hrefs); + } + } + return oR; + } + + /** + * @param reportInfo + * @return the requested report + * @throws DavException + * @see DeltaVResource#getReport(org.apache.jackrabbit.webdav.version.report.ReportInfo) + */ + public Report getReport(ReportInfo reportInfo) throws DavException { + if (reportInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "A REPORT request must provide a valid XML request body."); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + if (!supportedReports.isSupportedReport(reportInfo)) { + Element condition = null; + try { + condition = DomUtil.createDocument().createElementNS("DAV:", "supported-report"); + } catch (ParserConfigurationException ex) { + // we don't care THAT much + } + throw new DavException(DavServletResponse.SC_CONFLICT, + "Unknown report '" + reportInfo.getReportName() + "' requested.", null, condition); + } + return ReportType.getType(reportInfo).createReport(this, reportInfo); + } + + /** + * The JCR api does not provide methods to create new workspaces. Calling + * addWorkspace on this resource will always fail. + * + * @param workspace + * @throws DavException Always throws. + * @see DeltaVResource#addWorkspace(org.apache.jackrabbit.webdav.DavResource) + */ + public void addWorkspace(DavResource workspace) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Return an array of DavResource objects that are referenced + * by the property with the specified name. + * + * @param hrefPropertyName + * @return array of DavResources + * @throws DavException + * @see DeltaVResource#getReferenceResources(org.apache.jackrabbit.webdav.property.DavPropertyName) + */ + public DavResource[] getReferenceResources(DavPropertyName hrefPropertyName) throws DavException { + DavProperty prop = getProperty(hrefPropertyName); + List resources = new ArrayList(); + if (prop != null && prop instanceof HrefProperty) { + HrefProperty hp = (HrefProperty)prop; + // process list of hrefs + for (String href : hp.getHrefs()) { + DavResourceLocator locator = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), href); + resources.add(createResourceFromLocator(locator)); + } + } else { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + return resources.toArray(new DavResource[resources.size()]); + } + + /** + * Build a DavResourceLocator from the given nodePath path. + * + * @param nodePath + * @return a new DavResourceLocator + * @see DavLocatorFactory#createResourceLocator(String, String, String) + */ + protected DavResourceLocator getLocatorFromNodePath(String nodePath) { + DavResourceLocator loc = getLocator().getFactory().createResourceLocator(getLocator().getPrefix(), getLocator().getWorkspacePath(), nodePath, false); + return loc; + } + + + /** + * Build a new {@link DavResourceLocator} from the given repository node. + * + * @param repositoryNode + * @return a new locator for the specified node. + * @see #getLocatorFromNodePath(String) + */ + protected DavResourceLocator getLocatorFromNode(Node repositoryNode) { + String nodePath = null; + try { + if (repositoryNode != null) { + nodePath = repositoryNode.getPath(); + } + } catch (RepositoryException e) { + // ignore: should not occur + log.warn(e.getMessage()); + } + return getLocatorFromNodePath(nodePath); + } + + /** + * Create a new DavResource from the given locator. + * @param loc + * @return new DavResource + */ + protected DavResource createResourceFromLocator(DavResourceLocator loc) + throws DavException { + DavResource res = getFactory().createResource(loc, getSession()); + return res; + } + + /** + * Returns a {@link org.apache.jackrabbit.webdav.property.HrefProperty} with the + * specified property name and values. Each node present in the specified + * array is referenced in the resulting property. + * + * @param name + * @param values + * @param isProtected + * @return HrefProperty + */ + protected HrefProperty getHrefProperty(DavPropertyName name, Node[] values, + boolean isProtected, boolean isCollection) { + if (values == null) { + return null; + } + String[] pHref = new String[values.length]; + for (int i = 0; i < values.length; i++) { + pHref[i] = getLocatorFromNode(values[i]).getHref(isCollection); + } + return new HrefProperty(name, pHref, isProtected); + } + + /** + * Initialize the supported reports field + */ + protected void initSupportedReports() { + if (exists()) { + supportedReports.addReportType(ReportType.EXPAND_PROPERTY); + if (isCollection()) { + supportedReports.addReportType(ReportType.LOCATE_BY_HISTORY); + } + } + } + + /** + * Fill the property set for this resource. + */ + @Override + protected void initProperties() { + if (!propsInitialized) { + super.initProperties(); + if (exists()) { + properties.add(supportedReports); + + // DAV:creator-displayname -> use jcr:createBy if present. + Node n = getNode(); + try { + if (n.hasProperty(Property.JCR_CREATED_BY)) { + String createdBy = n.getProperty(Property.JCR_CREATED_BY).getString(); + properties.add(new DefaultDavProperty(DeltaVConstants.CREATOR_DISPLAYNAME, createdBy, true)); + } + } catch (RepositoryException e) { + log.debug("Error while accessing jcr:createdBy property"); + } + } + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ItemFilter.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ItemFilter.java new file mode 100644 index 00000000000..21374510ff6 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ItemFilter.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.webdav.simple; + +import javax.jcr.Item; +import javax.jcr.Session; + +/** + * ItemFilter + */ +public interface ItemFilter { + + /** + * Define the URIs that should be filtered out if present in the prefix + * of an items name. + * + * @param uris + */ + public void setFilteredURIs(String[] uris); + + /** + * Define the namespace prefixes that should be filtered if present in + * the prefix of an items name. + * + * @param prefixes + */ + public void setFilteredPrefixes(String[] prefixes); + + /** + * Set the nodetype names that should be used if a given item should be + * filtered. Note that not the nodetype(s) defined for a given item + * is relevant but rather the nodetype that defined the definition of the item. + * + * @param nodetypeNames + */ + public void setFilteredNodetypes(String[] nodetypeNames); + + /** + * Returns true if the given item should be filtered. + * + * @param item to be tested + * @return true if the given item should be filtered. + */ + public boolean isFilteredItem(Item item); + + /** + * Returns true if the resouce with the given name should be filtered. + * + * @param name to be tested for a filtered namespace prefix + * @param session used for looking up namespace mappings + * @return true if the given resource should be filtered. + */ + public boolean isFilteredItem(String name, Session session); +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImpl.java new file mode 100644 index 00000000000..70421e634cb --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImpl.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.webdav.simple; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ResourceFactoryImpl implements a simple DavLocatorFactory + */ +// todo improve special handling of root item.... +public class LocatorFactoryImpl implements DavLocatorFactory { + + /** the default logger */ + private static final Logger log = LoggerFactory.getLogger(LocatorFactoryImpl.class); + + private final String repositoryPrefix; + + public LocatorFactoryImpl(String repositoryPrefix) { + this.repositoryPrefix = repositoryPrefix; + } + + public DavResourceLocator createResourceLocator(String prefix, String href) { + // build prefix string and remove all prefixes from the given href. + StringBuffer b = new StringBuffer(""); + if (prefix != null && prefix.length() > 0) { + b.append(prefix); + if (href.startsWith(prefix)) { + href = href.substring(prefix.length()); + } + } + if (repositoryPrefix != null && repositoryPrefix.length() > 0 && !prefix.endsWith(repositoryPrefix)) { + b.append(repositoryPrefix); + if (href.startsWith(repositoryPrefix)) { + href = href.substring(repositoryPrefix.length()); + } + } + + // special treatment for root item, that has no name but '/' path. + if (href == null || "".equals(href)) { + href = "/"; + } + return new Locator(b.toString(), Text.unescape(href), this); + } + + public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) { + return createResourceLocator(prefix, workspacePath, resourcePath, true); + } + + public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) { + return new Locator(prefix, path, this); + } + + //-------------------------------------------------------------------------- + private static class Locator implements DavResourceLocator { + + private final String prefix; + private final String resourcePath; + private final DavLocatorFactory factory; + private final String href; + + private Locator(String prefix, String resourcePath, DavLocatorFactory factory) { + this.prefix = prefix; + this.factory = factory; + // remove trailing '/' that is not part of the resourcePath except for the root item. + if (resourcePath.endsWith("/") && !"/".equals(resourcePath)) { + resourcePath = resourcePath.substring(0, resourcePath.length()-1); + } + this.resourcePath = resourcePath; + href = prefix + Text.escapePath(resourcePath); + } + + public String getPrefix() { + return prefix; + } + + public String getResourcePath() { + return resourcePath; + } + + public String getWorkspacePath() { + return ""; + } + + public String getWorkspaceName() { + return ""; + } + + public boolean isSameWorkspace(DavResourceLocator locator) { + return isSameWorkspace(locator.getWorkspaceName()); + } + + public boolean isSameWorkspace(String workspaceName) { + return getWorkspaceName().equals(workspaceName); + } + + public String getHref(boolean isCollection) { + // avoid doubled trailing '/' for the root item + String suffix = (isCollection && !isRootLocation()) ? "/" : ""; + return href + suffix; + } + + public boolean isRootLocation() { + return "/".equals(resourcePath); + } + + public DavLocatorFactory getFactory() { + return factory; + } + + /** + * Returns the same as {@link #getResourcePath()}. No encoding is performed + * at all. + * @see DavResourceLocator#getRepositoryPath() + */ + public String getRepositoryPath() { + return getResourcePath(); + } + + /** + * Computes the hash code from the href, which is built using the final + * fields prefix and resourcePath. + * + * @return the hash code + */ + @Override + public int hashCode() { + return href.hashCode(); + } + + /** + * Equality of path is achieved if the specified object is a DavResourceLocator + * object with the same hash code. + * + * @param obj the object to compare to + * @return true if the 2 objects are equal; + * false otherwise + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof DavResourceLocator) { + DavResourceLocator other = (DavResourceLocator) obj; + return hashCode() == other.hashCode(); + } + return false; + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImplEx.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImplEx.java new file mode 100644 index 00000000000..3e5b2b61e3f --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImplEx.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.webdav.simple; + +import org.apache.jackrabbit.webdav.AbstractLocatorFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * LocatorFactoryImplEx... + */ +public class LocatorFactoryImplEx extends AbstractLocatorFactory { + + private static Logger log = LoggerFactory.getLogger(LocatorFactoryImplEx.class); + + /** + * Create a new factory + * + * @param pathPrefix Prefix, that needs to be removed in order to retrieve + * the path of the repository item from a given DavResourceLocator. + */ + public LocatorFactoryImplEx(String pathPrefix) { + super(pathPrefix); + } + + /** + * + * @see AbstractLocatorFactory#getRepositoryPath(String, String) + */ + @Override + protected String getRepositoryPath(String resourcePath, String wspPath) { + if (resourcePath == null) { + return resourcePath; + } + + if (resourcePath.equals(wspPath) || startsWithWorkspace(resourcePath, wspPath)) { + String repositoryPath = resourcePath.substring(wspPath.length()); + return (repositoryPath.length() == 0) ? "/" : repositoryPath; + } else { + throw new IllegalArgumentException("Unexpected format of resource path: " + resourcePath + " (workspace: " + wspPath + ")"); + } + } + + /** + * + * @see AbstractLocatorFactory#getResourcePath(String, String) + */ + @Override + protected String getResourcePath(String repositoryPath, String wspPath) { + if (repositoryPath == null) { + throw new IllegalArgumentException("Cannot build resource path from 'null' repository path"); + } + return wspPath + repositoryPath; + } + + private boolean startsWithWorkspace(String repositoryPath, String wspPath) { + if (wspPath == null) { + return true; + } else { + return repositoryPath.startsWith(wspPath + "/"); + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ResourceConfig.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ResourceConfig.java new file mode 100644 index 00000000000..1292b129ce3 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ResourceConfig.java @@ -0,0 +1,543 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.simple; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.server.io.CopyMoveHandler; +import org.apache.jackrabbit.server.io.CopyMoveManager; +import org.apache.jackrabbit.server.io.CopyMoveManagerImpl; +import org.apache.jackrabbit.server.io.DefaultIOManager; +import org.apache.jackrabbit.server.io.DeleteManager; +import org.apache.jackrabbit.server.io.DeleteManagerImpl; +import org.apache.jackrabbit.server.io.IOHandler; +import org.apache.jackrabbit.server.io.IOManager; +import org.apache.jackrabbit.server.io.PropertyHandler; +import org.apache.jackrabbit.server.io.PropertyManager; +import org.apache.jackrabbit.server.io.PropertyManagerImpl; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.tika.detect.Detector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * ResourceConfig... + */ +public class ResourceConfig { + + private static Logger log = LoggerFactory.getLogger(ResourceConfig.class); + + private static final String ELEMENT_IOMANAGER = "iomanager"; + private static final String ELEMENT_IOHANDLER = "iohandler"; + + private static final String ELEMENT_PROPERTYMANAGER = "propertymanager"; + private static final String ELEMENT_PROPERTYHANDLER = "propertyhandler"; + + private static final String ELEMENT_COPYMOVEMANAGER = "copymovemanager"; + private static final String ELEMENT_COPYMOVEHANDLER = "copymovehandler"; + + private static final String ELEMENT_CLASS = "class"; + + private static final String ELEMENT_PARAM = "param"; + private static final String ATTR_NAME = "name"; + private static final String ATTR_VALUE = "value"; + + /** + * Content type detector. + */ + private final Detector detector; + + private ItemFilter itemFilter; + private IOManager ioManager; + private CopyMoveManager cmManager; + private PropertyManager propManager; + private DeleteManager deleteManager; + private String[] nodetypeNames = new String[0]; + private boolean collectionNames = false; + + public ResourceConfig(Detector detector) { + this.detector = detector; + } + + /** + * Tries to parse the given xml configuration file. + * The xml must match the following structure:
        + *
        +     * <!ELEMENT config (iomanager, propertymanager, (collection | noncollection)?, filter?, mimetypeproperties?) >
        +     * <!ELEMENT iomanager (class, iohandler*) >
        +     * <!ELEMENT iohandler (class) >
        +     * <!ELEMENT propertymanager (class, propertyhandler*) >
        +     * <!ELEMENT propertyhandler (class) >
        +     * <!ELEMENT collection (nodetypes) >
        +     * <!ELEMENT noncollection (nodetypes) >
        +     * <!ELEMENT filter (class, namespaces?, nodetypes?) >
        +     * <!ELEMENT class >
        +     *    <!ATTLIST class
        +     *      name  CDATA #REQUIRED
        +     *    >
        +     * <!ELEMENT namespaces (prefix|uri)* >
        +     * <!ELEMENT prefix (CDATA) >
        +     * <!ELEMENT uri (CDATA) >
        +     * <!ELEMENT nodetypes (nodetype)* >
        +     * <!ELEMENT nodetype (CDATA) >
        +     * <!ELEMENT mimetypeproperties (mimemapping*, defaultmimetype) >
        +     * <!ELEMENT mimemapping >
        +     *    <!ATTLIST mimemapping
        +     *      extension  CDATA #REQUIRED
        +     *      mimetype  CDATA #REQUIRED
        +     *    >
        +     * <!ELEMENT defaultmimetype (CDATA) >
        +     * 
        + *

        + * The <mimetypeproperties/> settings have been deprecated and will + * be ignored with a warning. Instead you can use the + * {@link SimpleWebdavServlet#INIT_PARAM_MIME_INFO mime-info} + * servlet initialization parameter to customize the media type settings. + * + * @param configURL + */ + public void parse(URL configURL) { + try { + parse(configURL.openStream()); + } catch (IOException e) { + log.debug("Invalid resource configuration: " + e.getMessage()); + } + } + + /** + * Parses the given input stream into the xml configuration file. + * The xml must match the following structure:
        + *

        +     * <!ELEMENT config (iomanager, propertymanager, (collection | noncollection)?, filter?, mimetypeproperties?) >
        +     * <!ELEMENT iomanager (class, iohandler*) >
        +     * <!ELEMENT iohandler (class) >
        +     * <!ELEMENT propertymanager (class, propertyhandler*) >
        +     * <!ELEMENT propertyhandler (class) >
        +     * <!ELEMENT collection (nodetypes) >
        +     * <!ELEMENT noncollection (nodetypes) >
        +     * <!ELEMENT filter (class, namespaces?, nodetypes?) >
        +     * <!ELEMENT class >
        +     *    <!ATTLIST class
        +     *      name  CDATA #REQUIRED
        +     *    >
        +     * <!ELEMENT namespaces (prefix|uri)* >
        +     * <!ELEMENT prefix (CDATA) >
        +     * <!ELEMENT uri (CDATA) >
        +     * <!ELEMENT nodetypes (nodetype)* >
        +     * <!ELEMENT nodetype (CDATA) >
        +     * <!ELEMENT mimetypeproperties (mimemapping*, defaultmimetype) >
        +     * <!ELEMENT mimemapping >
        +     *    <!ATTLIST mimemapping
        +     *      extension  CDATA #REQUIRED
        +     *      mimetype  CDATA #REQUIRED
        +     *    >
        +     * <!ELEMENT defaultmimetype (CDATA) >
        +     * 
        + *

        + * The <mimetypeproperties/> settings have been deprecated and will + * be ignored with a warning. Instead you can use the + * {@link SimpleWebdavServlet#INIT_PARAM_MIME_INFO mime-info} + * servlet initialization parameter to customize the media type settings. + * + * @param stream + */ + public void parse(InputStream stream) { + try { + Element config = DomUtil.parseDocument(stream).getDocumentElement(); + if (config == null) { + log.warn("Mandatory 'config' element is missing."); + return; + } + + // iomanager config entry + Element el = DomUtil.getChildElement(config, ELEMENT_IOMANAGER, null); + if (el != null) { + Object inst = buildClassFromConfig(el); + if (inst != null && inst instanceof IOManager) { + ioManager = (IOManager)inst; + ioManager.setDetector(detector); + // get optional 'iohandler' child elements and populate the + // ioManager with the instances + ElementIterator iohElements = DomUtil.getChildren(el, ELEMENT_IOHANDLER, null); + while (iohElements.hasNext()) { + Element iohEl = iohElements.nextElement(); + inst = buildClassFromConfig(iohEl); + if (inst != null && inst instanceof IOHandler) { + IOHandler handler = (IOHandler) inst; + setParameters(handler, iohEl); + ioManager.addIOHandler(handler); + } else { + log.warn("Not a valid IOHandler : " + getClassName(iohEl)); + } + } + } else { + log.warn("'iomanager' element does not define a valid IOManager."); + } + } else { + log.warn("'iomanager' element is missing."); + } + + // propertymanager config entry + el = DomUtil.getChildElement(config, ELEMENT_PROPERTYMANAGER, null); + if (el != null) { + Object inst = buildClassFromConfig(el); + if (inst != null && inst instanceof PropertyManager) { + propManager = (PropertyManager)inst; + // get optional 'iohandler' child elements and populate the + // ioManager with the instances + ElementIterator iohElements = DomUtil.getChildren(el, ELEMENT_PROPERTYHANDLER, null); + while (iohElements.hasNext()) { + Element iohEl = iohElements.nextElement(); + inst = buildClassFromConfig(iohEl); + if (inst != null && inst instanceof PropertyHandler) { + PropertyHandler handler = (PropertyHandler) inst; + setParameters(handler, iohEl); + propManager.addPropertyHandler(handler); + } else { + log.warn("Not a valid PropertyHandler : " + getClassName(iohEl)); + } + } + } else { + log.warn("'propertymanager' element does not define a valid PropertyManager."); + } + } else { + log.debug("'propertymanager' element is missing."); + } + + // copymovemanager config entry + el = DomUtil.getChildElement(config, ELEMENT_COPYMOVEMANAGER, null); + if (el != null) { + Object inst = buildClassFromConfig(el); + if (inst != null && inst instanceof CopyMoveManager) { + cmManager = (CopyMoveManager) inst; + // get optional 'copymovehandler' child elements and populate + // the copy move manager with the instances + ElementIterator iohElements = DomUtil.getChildren(el, ELEMENT_COPYMOVEHANDLER, null); + while (iohElements.hasNext()) { + Element iohEl = iohElements.nextElement(); + inst = buildClassFromConfig(iohEl); + if (inst != null && inst instanceof CopyMoveHandler) { + CopyMoveHandler handler = (CopyMoveHandler) inst; + setParameters(handler, iohEl); + cmManager.addCopyMoveHandler(handler); + } else { + log.warn("Not a valid CopyMoveHandler : " + getClassName(iohEl)); + } + } + } else { + log.warn("'copymovemanager' element does not define a valid CopyMoveManager."); + } + } else { + log.debug("'copymovemanager' element is missing."); + } + + // collection/non-collection config entry + el = DomUtil.getChildElement(config, "collection", null); + if (el != null) { + nodetypeNames = parseNodeTypesEntry(el); + collectionNames = true; + } else if ((el = DomUtil.getChildElement(config, "noncollection", null)) != null) { + nodetypeNames = parseNodeTypesEntry(el); + collectionNames = false; + } + // todo: should check if both 'noncollection' and 'collection' are present and write a warning + + // filter config entry + el = DomUtil.getChildElement(config, "filter", null); + if (el != null) { + Object inst = buildClassFromConfig(el); + if (inst != null && inst instanceof ItemFilter) { + itemFilter = (ItemFilter)inst; + } + if (itemFilter != null) { + itemFilter.setFilteredNodetypes(parseNodeTypesEntry(el)); + parseNamespacesEntry(el); + } + } else { + log.debug("No 'filter' element specified."); + } + + el = DomUtil.getChildElement(config, "mimetypeproperties", null); + if (el != null) { + log.warn("Ignoring deprecated mimetypeproperties settings"); + } + } catch (IOException e) { + log.debug("Invalid resource configuration: " + e.getMessage()); + } catch (ParserConfigurationException e) { + log.warn("Failed to parse resource configuration: " + e.getMessage()); + } catch (SAXException e) { + log.warn("Failed to parse resource configuration: " + e.getMessage()); + } + } + + private void parseNamespacesEntry(Element parent) { + Element namespaces = DomUtil.getChildElement(parent, "namespaces", null); + if (namespaces != null) { + List l = new ArrayList(); + // retrieve prefix child elements + ElementIterator it = DomUtil.getChildren(namespaces, "prefix", null); + while (it.hasNext()) { + Element e = it.nextElement(); + l.add(DomUtil.getText(e)); + } + String[] prefixes = l.toArray(new String[l.size()]); + l.clear(); + // retrieve uri child elements + it = DomUtil.getChildren(namespaces, "uri", null); + while (it.hasNext()) { + Element e = it.nextElement(); + l.add(DomUtil.getText(e)); + } + String[] uris = l.toArray(new String[l.size()]); + itemFilter.setFilteredPrefixes(prefixes); + itemFilter.setFilteredURIs(uris); + } + } + + private static String[] parseNodeTypesEntry(Element parent) { + String[] ntNames; + Element nodetypes = DomUtil.getChildElement(parent, "nodetypes", null); + if (nodetypes != null) { + List l = new ArrayList(); + ElementIterator it = DomUtil.getChildren(nodetypes, "nodetype", null); + while (it.hasNext()) { + Element e = it.nextElement(); + l.add(DomUtil.getText(e)); + } + ntNames = l.toArray(new String[l.size()]); + } else { + ntNames = new String[0]; + } + return ntNames; + } + + private static Object buildClassFromConfig(Element parent) { + Object instance = null; + Element classElem = DomUtil.getChildElement(parent, "class", null); + if (classElem != null) { + // contains a 'class' child node + try { + String className = DomUtil.getAttribute(classElem, "name", null); + if (className != null) { + Class c = Class.forName(className); + instance = c.newInstance(); + } else { + log.error("Invalid configuration: missing 'class' element"); + } + } catch (Exception e) { + log.error("Error while create class instance: " + e.getMessage()); + } + } + return instance; + } + + private static String getClassName(Element parent) { + String className = null; + Element classElem = DomUtil.getChildElement(parent, "class", null); + if (classElem != null) { + className = DomUtil.getAttribute(classElem, "name", null); + } + return (className == null) ? "" : className; + } + + /** + * Retrieve 'param' elements for the specified xmlElement and + * use the public setter methods of the given instance to set + * the corresponding instance fields. + * + * @param instance + * @param xmlElement + */ + private static void setParameters(Object instance, Element xmlElement) { + ElementIterator paramElems = DomUtil.getChildren(xmlElement, ELEMENT_PARAM, Namespace.EMPTY_NAMESPACE); + if (paramElems.hasNext()) { + Map setters = getSetters(instance.getClass()); + if (!setters.isEmpty()) { + while (paramElems.hasNext()) { + Element parameter = paramElems.next(); + String name = DomUtil.getAttribute(parameter, ATTR_NAME, null); + String value = DomUtil.getAttribute(parameter, ATTR_VALUE, null); + if (name == null || value == null) { + log.error("Parameter name or value missing -> ignore."); + continue; + } + Method setter = setters.get(name); + if (setter != null) { + Class type = setter.getParameterTypes()[0]; + try { + if (type.isAssignableFrom(String.class) + || type.isAssignableFrom(Object.class)) { + setter.invoke(instance, value); + } else if (type.isAssignableFrom(Boolean.TYPE) + || type.isAssignableFrom(Boolean.class)) { + setter.invoke(instance, Boolean.valueOf(value)); + } else if (type.isAssignableFrom(Integer.TYPE) + || type.isAssignableFrom(Integer.class)) { + setter.invoke(instance, Integer.valueOf(value)); + } else if (type.isAssignableFrom(Long.TYPE) + || type.isAssignableFrom(Long.class)) { + setter.invoke(instance, Long.valueOf(value)); + } else if (type.isAssignableFrom(Double.TYPE) + || type.isAssignableFrom(Double.class)) { + setter.invoke(instance, Double.valueOf(value)); + } else { + log.error("Cannot set configuration property " + name); + } + } catch (Exception e) { + log.error("Invalid format (" + value + ") for property " + name + " of class " + instance.getClass().getName(), e); + } + } + } + } + } + } + + private static Map getSetters(Class cl) { + Map methods = new HashMap(); + for (Method method : cl.getMethods()) { + String name = method.getName(); + if (name.startsWith("set") && name.length() > 3 + && Modifier.isPublic(method.getModifiers()) + && !Modifier.isStatic(method.getModifiers()) + && Void.TYPE.equals(method.getReturnType()) + && method.getParameterTypes().length == 1) { + methods.put(name.substring(3, 4).toLowerCase() + name.substring(4), method); + } + } + return methods; + } + + /** + * + * @return + */ + public IOManager getIOManager() { + if (ioManager == null) { + log.debug("Missing io-manager > building DefaultIOManager "); + ioManager = new DefaultIOManager(); + ioManager.setDetector(detector); + } + return ioManager; + } + + /** + * + * @return + */ + public PropertyManager getPropertyManager() { + if (propManager == null) { + log.debug("Missing property-manager > building default."); + propManager = PropertyManagerImpl.getDefaultManager(); + } + return propManager; + } + + /** + * + * @return + */ + public CopyMoveManager getCopyMoveManager() { + if (cmManager == null) { + log.debug("Missing copymove-manager > building default."); + cmManager = CopyMoveManagerImpl.getDefaultManager(); + } + return cmManager; + } + + /** + * Returns the delete manager. + * @return the delete manager + */ + public DeleteManager getDeleteManager() { + if (deleteManager == null) { + log.debug("Missing delete-manager > building default."); + deleteManager = DeleteManagerImpl.getDefaultManager(); + } + return deleteManager; + } + + /** + * Returns true, if the given item represents a {@link Node node} that is + * either any of the nodetypes specified to represent a collection or + * none of the nodetypes specified to represent a non-collection, respectively. + * If no valid configuration entry is present, this method returns true + * for node items. For items which are not a node, this method always + * returns false. + * + * @param item + * @return true if the given item is a node that represents a webdav + * collection, false otherwise. + */ + public boolean isCollectionResource(Item item) { + if (item.isNode()) { + boolean isCollection = true; + Node n = (Node)item; + try { + for (int i = 0; i < nodetypeNames.length && isCollection; i++) { + isCollection = collectionNames ? n.isNodeType(nodetypeNames[i]) : !n.isNodeType(nodetypeNames[i]); + } + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + return isCollection; + } else { + return false; + } + } + + /** + * Returns the item filter specified with the configuration or {@link DefaultItemFilter} + * if the configuration was missing the corresponding entry or the parser failed + * to build a ItemFilter instance from the configuration. + * + * @return item filter as defined by the config or {@link DefaultItemFilter} + */ + public ItemFilter getItemFilter() { + if (itemFilter == null) { + log.debug("Missing resource filter > building DefaultItemFilter "); + itemFilter = new DefaultItemFilter(); + } + return itemFilter; + } + + /** + * Returns the configured content type detector. + * + * @return content type detector + */ + public Detector getDetector() { + return detector; + } + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ResourceFactoryImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ResourceFactoryImpl.java new file mode 100644 index 00000000000..32f5ecd77c8 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/ResourceFactoryImpl.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.simple; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletRequest; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.jcr.JcrDavSession; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +/** + * ResourceFactoryImpl implements a simple DavResourceFactory + */ +public class ResourceFactoryImpl implements DavResourceFactory { + + private static Logger log =LoggerFactory.getLogger(ResourceFactoryImpl.class); + + private final LockManager lockMgr; + private final ResourceConfig resourceConfig; + + /** + * Create a new ResourceFactory that uses the given lock + * manager and resource filter. + * + * @param lockMgr + * @param resourceConfig + */ + public ResourceFactoryImpl(LockManager lockMgr, ResourceConfig resourceConfig) { + this.lockMgr = lockMgr; + this.resourceConfig = resourceConfig; + } + + /** + * Create a new DavResource from the given locator and + * request. + * + * @param locator + * @param request + * @param response + * @return DavResource + * @throws DavException + * @see DavResourceFactory#createResource(DavResourceLocator, + * DavServletRequest, DavServletResponse) + */ + public DavResource createResource(DavResourceLocator locator, DavServletRequest request, + DavServletResponse response) throws DavException { + try { + Node node = getNode(request.getDavSession(), locator); + DavResource resource; + if (node == null) { + log.debug("Creating resource for non-existing repository node."); + boolean isCollection = DavMethods.isCreateCollectionRequest(request); + resource = createNullResource(locator, request.getDavSession(), isCollection); + } else { + resource = createResource(node, locator, request.getDavSession()); + } + resource.addLockManager(lockMgr); + return resource; + } catch (RepositoryException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } + } + + /** + * Create a new DavResource from the given locator and webdav + * session. + * + * @param locator + * @param session + * @return + * @throws DavException + * @see DavResourceFactory#createResource(DavResourceLocator, DavSession) + */ + public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { + try { + Node node = getNode(session, locator); + DavResource resource = createResource(node, locator, session); + resource.addLockManager(lockMgr); + return resource; + } catch (RepositoryException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } + } + + /** + * Returns the Node corresponding to the given locator or + * null if it does not exist or if the existing item represents + * a Property. + * + * @param sessionImpl + * @param locator + * @return + * @throws RepositoryException + */ + private Node getNode(DavSession sessionImpl, DavResourceLocator locator) + throws RepositoryException { + Node node = null; + try { + String repoPath = locator.getRepositoryPath(); + if (repoPath != null) { + Session session = ((JcrDavSession)sessionImpl).getRepositorySession(); + Item item = session.getItem(repoPath); + if (item instanceof Node) { + node = (Node)item; + } // else: item is a property -> return null + } + } catch (PathNotFoundException e) { + // item does not exist (yet). return null -> create null-resource + } + return node; + } + + /** + * Create a 'null resource' + * + * @param locator + * @param session + * @param isCollection + * @return + * @throws DavException + */ + private DavResource createNullResource(DavResourceLocator locator, + DavSession session, + boolean isCollection) throws DavException { + JcrDavSession.checkImplementation(session); + JcrDavSession sessionImpl = (JcrDavSession)session; + + DavResource resource; + if (versioningSupported(sessionImpl.getRepositorySession())) { + resource = new VersionControlledResourceImpl(locator, this, sessionImpl, resourceConfig, isCollection); + } else { + resource = new DavResourceImpl(locator, this, sessionImpl, resourceConfig, isCollection); + } + return resource; + } + + /** + * Tries to retrieve the repository item defined by the locator's resource + * path and build the corresponding WebDAV resource. If the repository + * supports the versioning option different resources are created for + * version, versionhistory and common nodes. + * + * @param node + * @param locator + * @param session + * @return + * @throws DavException + */ + private DavResource createResource(Node node, DavResourceLocator locator, + DavSession session) throws DavException { + JcrDavSession.checkImplementation(session); + JcrDavSession sessionImpl = (JcrDavSession)session; + + DavResource resource; + if (versioningSupported(sessionImpl.getRepositorySession())) { + // create special resources for Version and VersionHistory + if (node instanceof Version) { + resource = new VersionResourceImpl(locator, this, sessionImpl, resourceConfig, node); + } else if (node instanceof VersionHistory) { + resource = new VersionHistoryResourceImpl(locator, this, sessionImpl, resourceConfig, node); + } else { + resource = new VersionControlledResourceImpl(locator, this, sessionImpl, resourceConfig, node); + } + } else { + resource = new DavResourceImpl(locator, this, session, resourceConfig, node); + } + return resource; + } + + /** + * @param repoSession + * @return true if the JCR repository supports versioning. + */ + private static boolean versioningSupported(Session repoSession) { + String desc = repoSession.getRepository().getDescriptor(Repository.OPTION_VERSIONING_SUPPORTED); + return Boolean.valueOf(desc); + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/SimpleWebdavServlet.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/SimpleWebdavServlet.java new file mode 100644 index 00000000000..2cff084b37a --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/SimpleWebdavServlet.java @@ -0,0 +1,391 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.simple; + +import org.apache.jackrabbit.server.BasicCredentialsProvider; +import org.apache.jackrabbit.server.CredentialsProvider; +import org.apache.jackrabbit.server.SessionProvider; +import org.apache.jackrabbit.server.SessionProviderImpl; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavSessionProvider; +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.apache.jackrabbit.webdav.lock.SimpleLockManager; +import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet; +import org.apache.tika.detect.Detector; +import org.apache.tika.mime.MimeTypeException; +import org.apache.tika.mime.MimeTypesFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Repository; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * WebdavServlet provides WebDAV support (level + * {@link org.apache.jackrabbit.webdav.DavCompliance#_1_ 1}, + * {@link org.apache.jackrabbit.webdav.DavCompliance#_2_ 2}, + * {@link org.apache.jackrabbit.webdav.DavCompliance#_3_ 3} and + * {@link org.apache.jackrabbit.webdav.DavCompliance#BIND bind} compliant) for + * repository resources. + *

        + * Implementations of this abstract class must implement the + * {@link #getRepository()} method to access the repository. + */ +public abstract class SimpleWebdavServlet extends AbstractWebdavServlet { + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(SimpleWebdavServlet.class); + + /** + * init param name of the repository prefix + */ + public static final String INIT_PARAM_RESOURCE_PATH_PREFIX = "resource-path-prefix"; + + /** + * Name of the init parameter that specify a separate configuration used + * for filtering the resources displayed. + */ + public static final String INIT_PARAM_RESOURCE_CONFIG = "resource-config"; + + /** + * Name of the parameter that specifies the servlet resource path of + * a custom <mime-info/> configuration file. The default setting + * is to use the MIME media type database included in Apache Tika. + */ + public static final String INIT_PARAM_MIME_INFO = "mime-info"; + + /** + * Servlet context attribute used to store the path prefix instead of + * having a static field with this servlet. The latter causes problems + * when running multiple + */ + public static final String CTX_ATTR_RESOURCE_PATH_PREFIX = "jackrabbit.webdav.simple.resourcepath"; + + /** + * the resource path prefix + */ + private String resourcePathPrefix; + + /** + * Map used to remember any webdav lock created without being reflected + * in the underlying repository. + * This is needed because some clients rely on a successful locking + * mechanism in order to perform properly (e.g. mac OSX built-in dav client) + */ + private LockManager lockManager; + + /** + * the resource factory + */ + private DavResourceFactory resourceFactory; + + /** + * the locator factory + */ + private DavLocatorFactory locatorFactory; + + /** + * the webdav session provider + */ + private DavSessionProvider davSessionProvider; + + /** + * the repository session provider + */ + private SessionProvider sessionProvider; + + /** + * The config + */ + private ResourceConfig config; + + /** + * Init this servlet + * + * @throws ServletException + */ + @Override + public void init() throws ServletException { + super.init(); + + resourcePathPrefix = getInitParameter(INIT_PARAM_RESOURCE_PATH_PREFIX); + if (resourcePathPrefix == null) { + log.debug("Missing path prefix > setting to empty string."); + resourcePathPrefix = ""; + } else if (resourcePathPrefix.endsWith("/")) { + log.debug("Path prefix ends with '/' > removing trailing slash."); + resourcePathPrefix = resourcePathPrefix.substring(0, resourcePathPrefix.length() - 1); + } + getServletContext().setAttribute(CTX_ATTR_RESOURCE_PATH_PREFIX, resourcePathPrefix); + log.info(INIT_PARAM_RESOURCE_PATH_PREFIX + " = '" + resourcePathPrefix + "'"); + + config = new ResourceConfig(getDetector()); + String configParam = getInitParameter(INIT_PARAM_RESOURCE_CONFIG); + if (configParam != null) { + try { + config.parse(getServletContext().getResource(configParam)); + } catch (MalformedURLException e) { + log.debug("Unable to build resource filter provider", e); + } + } + } + + /** + * Reads and returns the configured <mime-info/> database. + * + * @see #INIT_PARAM_MIME_INFO + * @return MIME media type database + * @throws ServletException if the database is invalid or can not be read + */ + private Detector getDetector() throws ServletException { + URL url; + + String mimeInfo = getInitParameter(INIT_PARAM_MIME_INFO); + if (mimeInfo != null) { + try { + url = getServletContext().getResource(mimeInfo); + } catch (MalformedURLException e) { + throw new ServletException( + "Invalid " + INIT_PARAM_MIME_INFO + + " configuration setting: " + mimeInfo, e); + } + } else { + url = MimeTypesFactory.class.getResource("tika-mimetypes.xml"); + } + + try { + return MimeTypesFactory.create(url); + } catch (MimeTypeException e) { + throw new ServletException( + "Invalid MIME media type database: " + url, e); + } catch (IOException e) { + throw new ServletException( + "Unable to read MIME media type database: " + url, e); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean isPreconditionValid(WebdavRequest request, + DavResource resource) { + return !resource.exists() || request.matchesIfHeader(resource); + } + + /** + * Returns the configured path prefix + * + * @return resourcePathPrefix + * @see #INIT_PARAM_RESOURCE_PATH_PREFIX + */ + public String getPathPrefix() { + return resourcePathPrefix; + } + + /** + * Returns the configured path prefix + * + * @param ctx The servlet context. + * @return resourcePathPrefix + * @see #INIT_PARAM_RESOURCE_PATH_PREFIX + */ + public static String getPathPrefix(ServletContext ctx) { + return (String) ctx.getAttribute(CTX_ATTR_RESOURCE_PATH_PREFIX); + } + + /** + * Returns the DavLocatorFactory. If no locator factory has + * been set or created a new instance of {@link org.apache.jackrabbit.webdav.simple.LocatorFactoryImpl} is + * returned. + * + * @return the locator factory + * @see AbstractWebdavServlet#getLocatorFactory() + */ + @Override + public DavLocatorFactory getLocatorFactory() { + if (locatorFactory == null) { + locatorFactory = new LocatorFactoryImplEx(resourcePathPrefix); + } + return locatorFactory; + } + + /** + * Sets the DavLocatorFactory. + * + * @param locatorFactory The DavLocatorFactory to use. + * @see AbstractWebdavServlet#setLocatorFactory(DavLocatorFactory) + */ + @Override + public void setLocatorFactory(DavLocatorFactory locatorFactory) { + this.locatorFactory = locatorFactory; + } + + /** + * Returns the LockManager. If no lock manager has + * been set or created a new instance of {@link SimpleLockManager} is + * returned. + * + * @return the lock manager + */ + public LockManager getLockManager() { + if (lockManager == null) { + lockManager = new SimpleLockManager(); + } + return lockManager; + } + + /** + * Sets the LockManager. + * + * @param lockManager The LockManager to be used. + */ + public void setLockManager(LockManager lockManager) { + this.lockManager = lockManager; + } + + /** + * Returns the DavResourceFactory. If no request factory has + * been set or created a new instance of {@link ResourceFactoryImpl} is + * returned. + * + * @return the resource factory + * @see AbstractWebdavServlet#getResourceFactory() + */ + @Override + public DavResourceFactory getResourceFactory() { + if (resourceFactory == null) { + resourceFactory = new ResourceFactoryImpl(getLockManager(), getResourceConfig()); + } + return resourceFactory; + } + + /** + * Sets the DavResourceFactory. + * + * @param resourceFactory The DavResourceFactory to use. + * @see AbstractWebdavServlet#setResourceFactory(org.apache.jackrabbit.webdav.DavResourceFactory) + */ + @Override + public void setResourceFactory(DavResourceFactory resourceFactory) { + this.resourceFactory = resourceFactory; + } + + /** + * Returns the SessionProvider. If no session provider has been + * set or created a new instance of {@link SessionProviderImpl} that extracts + * credentials from the Authorization request header is + * returned. + * + * @return the session provider + */ + public synchronized SessionProvider getSessionProvider() { + if (sessionProvider == null) { + sessionProvider = new SessionProviderImpl(getCredentialsProvider()); + } + return sessionProvider; + } + + /** + * Factory method for creating the credentials provider to be used for + * accessing the credentials associated with a request. The default + * implementation returns a {@link BasicCredentialsProvider} instance, + * but subclasses can override this method to add support for other + * types of credentials. + * + * @return the credentials provider + * @since 1.3 + */ + protected CredentialsProvider getCredentialsProvider() { + return new BasicCredentialsProvider(getInitParameter(INIT_PARAM_MISSING_AUTH_MAPPING)); + } + + /** + * Sets the SessionProvider. + * + * @param sessionProvider The SessionProvider to use. + */ + public synchronized void setSessionProvider(SessionProvider sessionProvider) { + this.sessionProvider = sessionProvider; + } + + /** + * Returns the DavSessionProvider. If no session provider has + * been set or created a new instance of {@link DavSessionProviderImpl} + * is returned. + * + * @return the session provider + * @see AbstractWebdavServlet#getDavSessionProvider() + */ + @Override + public synchronized DavSessionProvider getDavSessionProvider() { + if (davSessionProvider == null) { + davSessionProvider = + new DavSessionProviderImpl(getRepository(), getSessionProvider()); + } + return davSessionProvider; + } + + /** + * Sets the DavSessionProvider. + * + * @param sessionProvider The DavSessionProvider to use. + * @see AbstractWebdavServlet#setDavSessionProvider(org.apache.jackrabbit.webdav.DavSessionProvider) + */ + @Override + public synchronized void setDavSessionProvider(DavSessionProvider sessionProvider) { + this.davSessionProvider = sessionProvider; + } + + /** + * Returns the resource configuration to be applied + * + * @return the resource configuration. + */ + public ResourceConfig getResourceConfig() { + return config; + } + + /** + * Set the resource configuration + * + * @param config The resource configuration. + */ + public void setResourceConfig(ResourceConfig config) { + this.config = config; + } + + /** + * Returns the Repository. If no repository has been set or + * created the repository initialized by RepositoryAccessServlet + * is returned. + * + * @return repository + */ + public abstract Repository getRepository(); + +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionControlledResourceImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionControlledResourceImpl.java new file mode 100644 index 00000000000..d98a279dc4f --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionControlledResourceImpl.java @@ -0,0 +1,379 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.simple; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.version.LabelInfo; +import org.apache.jackrabbit.webdav.version.MergeInfo; +import org.apache.jackrabbit.webdav.version.UpdateInfo; +import org.apache.jackrabbit.webdav.version.VersionControlledResource; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.VersionResource; +import org.apache.jackrabbit.webdav.version.VersionableResource; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + + +/** + * VersionControlledResourceImpl represents a JCR node item and + * covers all functionality related to versioning of {@link Node}s. + * + * @see Node + */ +public class VersionControlledResourceImpl extends DeltaVResourceImpl + implements VersionControlledResource { + + private static final Logger log = LoggerFactory.getLogger(VersionControlledResourceImpl.class); + + /** + * Create a new {@link org.apache.jackrabbit.webdav.DavResource}. + * + * @param locator + * @param factory + * @param session + * @param config + * @param item + * @throws DavException + */ + public VersionControlledResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session, ResourceConfig config, Item item) throws DavException { + super(locator, factory, session, config, item); + initSupportedReports(); + } + + /** + * Create a new {@link org.apache.jackrabbit.webdav.DavResource}. + * + * @param locator + * @param factory + * @param session + * @param config + * @param isCollection + * @throws DavException + */ + public VersionControlledResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session, ResourceConfig config, boolean isCollection) throws DavException { + super(locator, factory, session, config, isCollection); + initSupportedReports(); + } + + //--------------------------------------------------------< DavResource >--- + /** + * Return a comma separated string listing the supported method names. + * + * @return the supported method names. + * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods() + */ + @Override + public String getSupportedMethods() { + StringBuffer sb = new StringBuffer(super.getSupportedMethods()); + // Versioning support + sb.append(", ").append(VersionableResource.METHODS); + if (isVersionControlled()) { + try { + if (getNode().isCheckedOut()) { + sb.append(", ").append(DavMethods.METHOD_CHECKIN); + } else { + sb.append(", ").append(DavMethods.METHOD_CHECKOUT); + sb.append(", ").append(DavMethods.METHOD_LABEL); + } + } catch (RepositoryException e) { + // should not occur. + log.error(e.getMessage()); + } + } + return sb.toString(); + } + + //------------------------------------------< VersionControlledResource >--- + /** + * Adds version control to this resource. If the resource is already under + * version control, this method has no effect. If this resource is a Collection + * resource this method fails with {@link DavServletResponse#SC_METHOD_NOT_ALLOWED}. + * + * @throws org.apache.jackrabbit.webdav.DavException if this resource does not + * exist yet, is a collection or if an error occurs while making the + * underlying node versionable. + * @see org.apache.jackrabbit.webdav.version.VersionableResource#addVersionControl() + */ + public void addVersionControl() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (isCollection()) { + // since the version-controlled-collection feature is not supported + // collections may not be put under dav version control even if + // the underlying node was / could be made jcr versionable. + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + if (!isVersionControlled()) { + Node item = getNode(); + try { + item.addMixin(JcrConstants.MIX_VERSIONABLE); + item.save(); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } // else: is already version controlled -> ignore + } + + /** + * Calls {@link javax.jcr.Node#checkin()} on the underlying repository node. + * + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkin() + */ + public String checkin() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (!isVersionControlled()) { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + try { + Version v = getNode().checkin(); + String versionHref = getLocatorFromNode(v).getHref(false); + return versionHref; + } catch (RepositoryException e) { + // UnsupportedRepositoryException should not occur + throw new JcrDavException(e); + } + } + + /** + * Calls {@link javax.jcr.Node#checkout()} on the underlying repository node. + * + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkout() + */ + public void checkout() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (!isVersionControlled()) { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + try { + getNode().checkout(); + } catch (RepositoryException e) { + // UnsupportedRepositoryException should not occur + throw new JcrDavException(e); + } + } + + + /** + * UNCHECKOUT cannot be implemented on top of JSR 170 repository. + * Therefore this methods always throws a DavException with error code + * {@link org.apache.jackrabbit.webdav.DavServletResponse#SC_NOT_IMPLEMENTED}. + * + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#uncheckout() + */ + public void uncheckout() throws DavException { + throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED); + } + + /** + * UPDATE feature is not (yet) supported. This method allows fails with + * {@link DavServletResponse#SC_NOT_IMPLEMENTED}. + * + * @param updateInfo + * @return + * @throws DavException + * @see VersionControlledResource#update(UpdateInfo) + */ + public MultiStatus update(UpdateInfo updateInfo) throws DavException { + throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED); + } + + /** + * MERGE feature is not (yet) supported. This method allows fails with + * {@link DavServletResponse#SC_NOT_IMPLEMENTED}. + * + * @param mergeInfo + * @return + * @throws DavException + * @see VersionControlledResource#merge(MergeInfo) + */ + public MultiStatus merge(MergeInfo mergeInfo) throws DavException { + throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED); + } + + /** + * Modify the labels present with the versions of this resource. + * + * @param labelInfo + * @throws DavException + * @see VersionControlledResource#label(LabelInfo) + * @see javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean) + * @see javax.jcr.version.VersionHistory#removeVersionLabel(String) + */ + public void label(LabelInfo labelInfo) throws DavException { + if (labelInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid label request body required."); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + try { + if (!isVersionControlled() || getNode().isCheckedOut()) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "A LABEL request may only be applied to a version-controlled, checked-in resource."); + } + DavResource[] resArr = this.getReferenceResources(CHECKED_IN); + if (resArr.length == 1 && resArr[0] instanceof VersionResource) { + ((VersionResource)resArr[0]).label(labelInfo); + } else { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "DAV:checked-in property on '" + getHref() + "' did not point to a single VersionResource."); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Returns the {@link javax.jcr.version.VersionHistory} associated with the repository node. + * If the node is not versionable an exception is thrown. + * + * @return the {@link VersionHistoryResource} associated with this resource. + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#getVersionHistory() + * @see javax.jcr.Node#getVersionHistory() + */ + public VersionHistoryResource getVersionHistory() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + if (!isVersionControlled()) { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + try { + VersionHistory vh = getNode().getVersionHistory(); + DavResourceLocator loc = getLocatorFromNode(vh); + DavResource vhr = createResourceFromLocator(loc); + if (vhr instanceof VersionHistoryResource) { + return (VersionHistoryResource)vhr; + } else { + // severe error since resource factory doesn't behave correctly. + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //-------------------------------------------------------------------------- + /** + * Define the set of reports supported by this resource. + * + * @see SupportedReportSetProperty + * @see DeltaVResourceImpl#initSupportedReports() + */ + @Override + protected void initSupportedReports() { + super.initSupportedReports(); + if (exists()) { + supportedReports.addReportType(ReportType.LOCATE_BY_HISTORY); + if (isVersionControlled()) { + supportedReports.addReportType(ReportType.VERSION_TREE); + } + } + } + + /** + * Fill the property set for this resource. + * @see DavResourceImpl#initProperties() + */ + @Override + protected void initProperties() { + if (!propsInitialized) { + super.initProperties(); + if (isVersionControlled()) { + Node n = getNode(); + // properties defined by RFC 3253 for version-controlled resources + try { + // DAV:version-history (computed) + String vhHref = getLocatorFromNode(n.getVersionHistory()).getHref(true); + properties.add(new HrefProperty(VERSION_HISTORY, vhHref, true)); + + // DAV:auto-version property: there is no auto version, explicit CHECKOUT is required. + properties.add(new DefaultDavProperty(AUTO_VERSION, null, true)); + + // baseVersion -> used for DAV:checked-out or DAV:checked-in + String baseVHref = getLocatorFromNode(n.getBaseVersion()).getHref(false); + if (n.isCheckedOut()) { + // DAV:predecessors property + if (n.hasProperty(JcrConstants.JCR_PREDECESSORS)) { + Value[] pv = n.getProperty(JcrConstants.JCR_PREDECESSORS).getValues(); + Node[] predecessors = new Node[pv.length]; + for (int i = 0; i < pv.length; i++) { + predecessors[i] = n.getSession().getNodeByIdentifier(pv[i].getString()); + } + properties.add(getHrefProperty(VersionResource.PREDECESSOR_SET, predecessors, true, false)); + } + // DAV:checked-out property (protected) + properties.add(new HrefProperty(CHECKED_OUT, baseVHref, true)); + } else { + // DAV:checked-in property (protected) + properties.add(new HrefProperty(CHECKED_IN, baseVHref, true)); + } + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + } + } + + /** + * @return true, if this resource is a non-collection resource and represents + * an existing repository node that has the mixin nodetype 'mix:versionable' set. + */ + private boolean isVersionControlled() { + boolean vc = false; + // since the version-controlled-collection feature is not supported + // all collection are excluded from version-control even if the + // underlying node was JCR versionable. + if (exists() && !isCollection()) { + Node item = getNode(); + try { + vc = item.isNodeType(JcrConstants.MIX_VERSIONABLE); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + } + return vc; + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionHistoryResourceImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionHistoryResourceImpl.java new file mode 100644 index 00000000000..ff1b0005921 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionHistoryResourceImpl.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.simple; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.ResourceType; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.VersionResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.Version; + +import java.util.ArrayList; +import java.util.List; + +/** + * VersionHistoryResourceImpl represents a JCR version history. + * + * @see VersionHistory + */ +public class VersionHistoryResourceImpl extends DeltaVResourceImpl implements VersionHistoryResource { + + private static final Logger log = LoggerFactory.getLogger(VersionHistoryResourceImpl.class); + + public VersionHistoryResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session, ResourceConfig config, Item item) throws DavException { + super(locator, factory, session, config, item); + if (getNode() == null || !(getNode() instanceof VersionHistory)) { + throw new IllegalArgumentException("VersionHistory item expected."); + } + } + + //--------------------------------------------------------< DavResource >--- + /** + * Show all versions of this history as members. + * + * @return + * @see DavResource#getMembers() + */ + @Override + public DavResourceIterator getMembers() { + ArrayList list = new ArrayList(); + if (exists() && isCollection()) { + try { + // only display versions as members of the vh. the jcr:versionLabels + // node is an internal structure. + VersionIterator it = ((VersionHistory) getNode()).getAllVersions(); + while (it.hasNext()) { + // omit item filter here. if the version history is visible + // its versions should be visible as well. + Version v = it.nextVersion(); + DavResourceLocator vhLocator = getLocator(); + DavResourceLocator resourceLocator = vhLocator.getFactory().createResourceLocator(vhLocator.getPrefix(), vhLocator.getWorkspacePath(), v.getPath(), false); + DavResource childRes = getFactory().createResource(resourceLocator, getSession()); + list.add(childRes); + } + } catch (RepositoryException e) { + // should not occur + log.error("Unexpected error",e); + } catch (DavException e) { + // should not occur + log.error("Unexpected error",e); + } + } + return new DavResourceIteratorImpl(list); + } + + /** + * The version storage is read-only -> fails with 403. + * + * @see DavResource#addMember(DavResource, InputContext) + */ + @Override + public void addMember(DavResource member, InputContext inputContext) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Removing a version resource is achieved by calling removeVersion + * on the versionhistory item this version belongs to. + * + * @throws DavException if the version does not exist or if an error occurs + * while deleting. + * @see DavResource#removeMember(org.apache.jackrabbit.webdav.DavResource) + */ + @Override + public void removeMember(DavResource member) throws DavException { + if (exists()) { + VersionHistory versionHistory = (VersionHistory) getNode(); + try { + String itemPath = member.getLocator().getRepositoryPath(); + // Retrieve the last segment of the given path and removes the index if present. + if (itemPath == null) { + throw new IllegalArgumentException("Cannot retrieve name from a 'null' item path."); + } + String name = Text.getName(itemPath); + // remove index + if (name.endsWith("]")) { + name = name.substring(0, name.lastIndexOf('[')); + } + versionHistory.removeVersion(name); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } else { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + } + + /** + * Version storage is read-only -> fails with 403. + * + * @see DavResource#setProperty(DavProperty) + */ + @Override + public void setProperty(DavProperty property) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Version storage is read-only -> fails with 403. + * + * @see DavResource#removeProperty(DavPropertyName) + */ + @Override + public void removeProperty(DavPropertyName propertyName) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Version storage is read-only -> fails with 403. + * + * @see DavResource#alterProperties(List) + */ + @Override + public MultiStatusResponse alterProperties(List changeList) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + //---------------------------------------------< VersionHistoryResource >--- + /** + * Return an array of {@link org.apache.jackrabbit.webdav.version.VersionResource}s representing all versions + * present in the underlying JCR version history. + * + * @return array of {@link org.apache.jackrabbit.webdav.version.VersionResource}s representing all versions + * present in the underlying JCR version history. + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionHistoryResource#getVersions() + */ + public VersionResource[] getVersions() throws DavException { + try { + VersionIterator vIter = ((VersionHistory)getNode()).getAllVersions(); + ArrayList l = new ArrayList(); + while (vIter.hasNext()) { + DavResourceLocator versionLoc = getLocatorFromNode(vIter.nextVersion()); + DavResource vr = createResourceFromLocator(versionLoc); + if (vr instanceof VersionResource) { + l.add((VersionResource) vr); + } else { + // severe error since resource factory doesn't behave correctly. + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + return l.toArray(new VersionResource[l.size()]); + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + //-------------------------------------------------------------------------- + /** + * Fill the property set for this resource. + */ + @Override + protected void initProperties() { + if (!propsInitialized) { + super.initProperties(); + + // change resource type defined by default item collection + properties.add(new ResourceType(new int[] {ResourceType.COLLECTION, ResourceType.VERSION_HISTORY})); + + // required root-version property for version-history resource + try { + String rootVersionHref = getLocatorFromNode(((VersionHistory)getNode()).getRootVersion()).getHref(false); + properties.add(new HrefProperty(VersionHistoryResource.ROOT_VERSION, rootVersionHref, false)); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + + // required, protected version-set property for version-history resource + try { + VersionIterator vIter = ((VersionHistory)getNode()).getAllVersions(); + ArrayList l = new ArrayList(); + while (vIter.hasNext()) { + l.add(vIter.nextVersion()); + } + properties.add(getHrefProperty(VersionHistoryResource.VERSION_SET, l.toArray(new Version[l.size()]), true, false)); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + } +} diff --git a/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionResourceImpl.java b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionResourceImpl.java new file mode 100644 index 00000000000..4a6d40a27a7 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/java/org/apache/jackrabbit/webdav/simple/VersionResourceImpl.java @@ -0,0 +1,287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.simple; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceIteratorImpl; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.util.HttpDateFormat; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.jcr.JcrDavException; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.version.LabelInfo; +import org.apache.jackrabbit.webdav.version.LabelSetProperty; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.VersionResource; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import java.util.ArrayList; +import java.util.List; + +/** + * VersionResourceImpl represents a JCR version. + * + * @see Version + */ +public class VersionResourceImpl extends DeltaVResourceImpl implements VersionResource { + + private static final Logger log = LoggerFactory.getLogger(VersionResourceImpl.class); + + /** + * Create a new {@link org.apache.jackrabbit.webdav.DavResource}. + * @param locator + * @param factory + * @param session + * @param config + * @param item + * @throws DavException + * + */ + public VersionResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session, ResourceConfig config, Item item) throws DavException { + super(locator, factory, session, config, item); + if (getNode() == null || !(getNode() instanceof Version)) { + throw new IllegalArgumentException("Version item expected."); + } + } + + //--------------------------------------------------------< DavResource >--- + /** + * Since this implementation of VersionResource never is a + * version belonging to a version controlled collection, this method always + * returns false not respecting the configuration. + * + * @return always false + */ + @Override + public boolean isCollection() { + return false; + } + + /** + * @return An empty DavResourceIterator + */ + @Override + public DavResourceIterator getMembers() { + return DavResourceIteratorImpl.EMPTY; + } + + /** + * The version storage is read-only -> fails with 403. + * + * @see DavResource#addMember(DavResource, InputContext) + */ + @Override + public void addMember(DavResource member, InputContext inputContext) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * The version storage is read-only -> fails with 403. + * + * @see DavResource#removeMember(DavResource) + */ + @Override + public void removeMember(DavResource member) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Version storage is read-only -> fails with 403. + * + * @see DavResource#setProperty(DavProperty) + */ + @Override + public void setProperty(DavProperty property) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Version storage is read-only -> fails with 403. + * + * @see DavResource#removeProperty(DavPropertyName) + */ + @Override + public void removeProperty(DavPropertyName propertyName) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + /** + * Version storage is read-only -> fails with 403. + * + * @see DavResource#alterProperties(List) + */ + @Override + public MultiStatusResponse alterProperties(List changeList) throws DavException { + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + + //----------------------------------------------------< VersionResource >--- + /** + * Modify the labels defined for the underlying repository version. + * + * @param labelInfo + * @throws org.apache.jackrabbit.webdav.DavException + * @see VersionResource#label(org.apache.jackrabbit.webdav.version.LabelInfo) + * @see javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean) + * @see javax.jcr.version.VersionHistory#removeVersionLabel(String) + */ + public void label(LabelInfo labelInfo) throws DavException { + if (labelInfo == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid label request body required."); + } + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + try { + VersionHistory vh = getVersionHistoryItem(); + if (labelInfo.getType() == LabelInfo.TYPE_REMOVE) { + vh.removeVersionLabel(labelInfo.getLabelName()); + } else if (labelInfo.getType() == LabelInfo.TYPE_ADD) { + // ADD: only add if not yet existing + vh.addVersionLabel(getNode().getName(), labelInfo.getLabelName(), false); + } else { + // SET: move label if already existing + vh.addVersionLabel(getNode().getName(), labelInfo.getLabelName(), true); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Returns the {@link VersionHistory} associated with the repository version. + * Note: in contrast to a versionable node, the version history of a version + * item is always represented by its nearest ancestor. + * + * @return the {@link org.apache.jackrabbit.webdav.version.VersionHistoryResource} associated with this resource. + * @throws org.apache.jackrabbit.webdav.DavException + * @see org.apache.jackrabbit.webdav.version.VersionResource#getVersionHistory() + * @see javax.jcr.Item#getParent() + */ + public VersionHistoryResource getVersionHistory() throws DavException { + if (!exists()) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + + try { + VersionHistory vh = getVersionHistoryItem(); + DavResourceLocator loc = getLocatorFromNode(vh); + DavResource vhr = createResourceFromLocator(loc); + if (vhr instanceof VersionHistoryResource) { + return (VersionHistoryResource)vhr; + } else { + // severe error since resource factory doesn't behave correctly. + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } catch (RepositoryException e) { + throw new JcrDavException(e); + } + } + + /** + * Return versionhistory that contains this version item + * + * @return versionhistory that contains this version item + * @throws RepositoryException + * @see javax.jcr.version.Version#getContainingHistory() + */ + private VersionHistory getVersionHistoryItem() throws RepositoryException { + return ((Version)getNode()).getContainingHistory(); + } + + //-------------------------------------------------------------------------- + /** + * Define the set of reports supported by this resource. + * + * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty + */ + @Override + protected void initSupportedReports() { + super.initSupportedReports(); + if (exists()) { + supportedReports.addReportType(ReportType.VERSION_TREE); + } + } + + /** + * Fill the property set for this resource. + */ + @Override + protected void initProperties() { + if (!propsInitialized) { + super.initProperties(); + Version v = (Version) getNode(); + try { + String creationDate = HttpDateFormat.creationDateFormat().format(v.getCreated().getTime()); + // replace dummy creation date from default collection + properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, creationDate)); + + // required, protected DAV:version-name property + properties.add(new DefaultDavProperty(VERSION_NAME, v.getName(), true)); + + // required, protected DAV:label-name-set property + String[] labels = getVersionHistoryItem().getVersionLabels(v); + properties.add(new LabelSetProperty(labels)); + + // required DAV:predecessor-set (protected) and DAV:successor-set (computed) properties + properties.add(getHrefProperty(VersionResource.PREDECESSOR_SET, v.getPredecessors(), true, false)); + properties.add(getHrefProperty(SUCCESSOR_SET, v.getSuccessors(), true, false)); + + // required DAV:version-history (computed) property + String vhHref = getLocatorFromNode(getVersionHistoryItem()).getHref(true); + properties.add(new HrefProperty(VersionResource.VERSION_HISTORY, vhHref, true)); + + // required DAV:checkout-set (computed) property + PropertyIterator it = v.getReferences(); + List nodeList = new ArrayList(); + while (it.hasNext()) { + Property p = it.nextProperty(); + if (JcrConstants.JCR_BASEVERSION.equals(p.getName())) { + Node n = p.getParent(); + if (n.isCheckedOut()) { + nodeList.add(n); + } + } + } + properties.add(getHrefProperty(CHECKOUT_SET, nodeList.toArray(new Node[nodeList.size()]), true, false)); + + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + } + } +} diff --git a/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/server/io/package.html b/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/server/io/package.html new file mode 100644 index 00000000000..16acab2ed83 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/server/io/package.html @@ -0,0 +1,21 @@ + + +The jackrabbit webdav server uses the classes defined in this package in order +to perform import and export operations in order to respond to PUT, MKCOL, PROPPATCH +and PROPFIND, GET, HEAD requests, respectively. + diff --git a/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/package.html b/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/package.html new file mode 100644 index 00000000000..b4ab6d62095 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/package.html @@ -0,0 +1,19 @@ + + +Contains JCR specific implementations. + diff --git a/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/version/package.html b/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/version/package.html new file mode 100644 index 00000000000..da7a5c7e50e --- /dev/null +++ b/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/version/package.html @@ -0,0 +1,25 @@ + + +Contains JCR specific implementations for the following interfaces: +

          +
        • VersionableResource
        • +
        • VersionControlledResource
        • +
        • VersionResource
        • +
        • VersionHistoryResource
        • +
        + diff --git a/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/version/report/package.html b/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/version/report/package.html new file mode 100644 index 00000000000..45f5023d7f5 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/javadoc/org/apache/jackrabbit/webdav/jcr/version/report/package.html @@ -0,0 +1,19 @@ + + +Contains JCR specific reports. + diff --git a/jackrabbit-jcr-server/src/main/resources/OSGI-INF/metatype/metatype.properties b/jackrabbit-jcr-server/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 00000000000..831cb22bd99 --- /dev/null +++ b/jackrabbit-jcr-server/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -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. +# + + +# +# This file contains localization strings for configuration labels and +# descriptions as used in the metatype.xml descriptor generated by the +# the SCR plugin + +dav.name = Apache Jackrabbit DavEx Servlet +dav.description = The DavEx Servlet allows direct access to the \ + complete Repository. + +alias.name = Root Path +alias.description = The root path at which the DavEx Servlet is \ + accessible. The default value is "/server". + +missing-auth-mapping.name = Missing Credentials Handling +missing-auth-mapping.description = Defines how a missing authorization \ + header should be handled. If this property is missing, a 401 response \ + is generated. This is suiteable for clients (eg. webdav clients) for \ + which sending a proper authorization header is not possible if the \ + server never sent a 401. If this property is present with an empty \ + value, null-credentials are returned, thus forcing an null login \ + on the repository. If this propert is present with the value \ + 'guestcredentials' java.jcr.GuestCredentials are used to login to the \ + repository. If this property has a 'user:password' value, the \ + respective simple credentials are generated. + +authenticate-header.name = Realm> +authenticate-header.description = Defines the value of the \ + 'WWW-Authenticate' header. Default is 'Basic realm="Jackrabbit Webdav Server"'. + +csrf-protection.name = CSRF Protection +csrf-protection.description = Configuration of referrer based CSRF \ + protection. If config is not configured or empty string the default \ + behaviour is to allow only requests with an empty referrer header or a \ + referrer host equal to the server host. A comma separated list of \ + additional allowed referrer hosts which are valid in addition to default \ + behaviour (see above). The value "disabled" may be used to disable the \ + referrer checking altogether. The default is "disabled" assuming that \ + a site-wide CSRF protection filter is installed. diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/BasicCredentialsProviderTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/BasicCredentialsProviderTest.java new file mode 100644 index 00000000000..41d86c9e728 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/BasicCredentialsProviderTest.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server; + +import junit.framework.TestCase; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpSession; +import javax.servlet.ServletInputStream; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.jcr.LoginException; +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import javax.jcr.GuestCredentials; +import java.util.Enumeration; +import java.util.Map; +import java.util.Locale; +import java.util.HashMap; +import java.security.Principal; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import java.io.BufferedReader; + +/** + * BasicCredentialsProviderTest... + */ +public class BasicCredentialsProviderTest extends TestCase { + + public void testMissingDefaultHeader() throws ServletException { + CredentialsProvider cb = new BasicCredentialsProvider(null); + try { + Credentials creds = cb.getCredentials(new RequestImpl(null)); + fail("LoginException expected"); + } catch (LoginException e) { + // ok + } + } + + public void testGuestCredentialsDefaultHeader() throws ServletException, LoginException { + CredentialsProvider cb = new BasicCredentialsProvider(BasicCredentialsProvider.GUEST_DEFAULT_HEADER_VALUE); + Credentials creds = cb.getCredentials(new RequestImpl(null)); + + assertTrue(creds instanceof GuestCredentials); + } + + public void testEmptyDefaultHeader() throws ServletException, LoginException { + CredentialsProvider cb = new BasicCredentialsProvider(BasicCredentialsProvider.EMPTY_DEFAULT_HEADER_VALUE); + Credentials creds = cb.getCredentials(new RequestImpl(null)); + + assertNull(creds); + } + + public void testDefaultPassword() throws ServletException, LoginException { + Map m = new HashMap(); + m.put("userId", new char[0]); + m.put("userId:", new char[0]); + m.put("userId:pw", "pw".toCharArray()); + + for (String uid : m.keySet()) { + char[] pw = m.get(uid); + + CredentialsProvider cb = new BasicCredentialsProvider(uid); + Credentials creds = cb.getCredentials(new RequestImpl(null)); + + assertNotNull(creds); + assertTrue(creds instanceof SimpleCredentials); + assertEquals("userId", ((SimpleCredentials) creds).getUserID()); + if (pw.length == 0) { + assertEquals(0, ((SimpleCredentials) creds).getPassword().length); + } else { + assertEquals(new String(pw), new String(((SimpleCredentials) creds).getPassword())); + } + } + } + + + + + private class RequestImpl implements HttpServletRequest { + + private final String authHeader; + + private RequestImpl(String authHeader) { + this.authHeader = authHeader; + } + + public String getAuthType() { + return null; + } + + public Cookie[] getCookies() { + return new Cookie[0]; + } + + public long getDateHeader(String name) { + return 0; + } + + public String getHeader(String name) { + return authHeader; + } + + public Enumeration getHeaders(String name) { + return null; + } + + public Enumeration getHeaderNames() { + return null; + } + + public int getIntHeader(String name) { + return 0; + } + + public String getMethod() { + return null; + } + + public String getPathInfo() { + return null; + } + + public String getPathTranslated() { + return null; + } + + public String getContextPath() { + return null; + } + + public String getQueryString() { + return null; + } + + public String getRemoteUser() { + return null; + } + + public boolean isUserInRole(String role) { + return false; + } + + public Principal getUserPrincipal() { + return null; + } + + public String getRequestedSessionId() { + return null; + } + + public String getRequestURI() { + return null; + } + + public StringBuffer getRequestURL() { + return null; + } + + public String getServletPath() { + return null; + } + + public HttpSession getSession(boolean create) { + return null; + } + + public HttpSession getSession() { + return null; + } + + public boolean isRequestedSessionIdValid() { + return false; + } + + public boolean isRequestedSessionIdFromCookie() { + return false; + } + + public boolean isRequestedSessionIdFromURL() { + return false; + } + + public boolean isRequestedSessionIdFromUrl() { + return false; + } + + public Object getAttribute(String name) { + return null; + } + + public Enumeration getAttributeNames() { + return null; + } + + public String getCharacterEncoding() { + return null; + } + + public void setCharacterEncoding(String s) throws UnsupportedEncodingException { + } + + public int getContentLength() { + return 0; + } + + public String getContentType() { + return null; + } + + public ServletInputStream getInputStream() throws IOException { + return null; + } + + public String getParameter(String name) { + return null; + } + + public Enumeration getParameterNames() { + return null; + } + + public String[] getParameterValues(String name) { + return new String[0]; + } + + public Map getParameterMap() { + return null; + } + + public String getProtocol() { + return null; + } + + public String getScheme() { + return null; + } + + public String getServerName() { + return null; + } + + public int getServerPort() { + return 0; + } + + public BufferedReader getReader() throws IOException { + return null; + } + + public String getRemoteAddr() { + return null; + } + + public String getRemoteHost() { + return null; + } + + public void setAttribute(String name, Object o) { + } + + public void removeAttribute(String name) { + } + + public Locale getLocale() { + return null; + } + + public Enumeration getLocales() { + return null; + } + + public boolean isSecure() { + return false; + } + + public RequestDispatcher getRequestDispatcher(String path) { + return null; + } + + public String getRealPath(String path) { + return null; + } + + public int getRemotePort() { + return 0; + } + + public String getLocalName() { + return null; + } + + public String getLocalAddr() { + return null; + } + + public int getLocalPort() { + return 0; + } + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/TestAll.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/TestAll.java new file mode 100644 index 00000000000..26efafb9d1b --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/TestAll.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.server; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package org.apache.jackrabbit.server. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.server tests"); + + suite.addTestSuite(BasicCredentialsProviderTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/BatchReadConfigTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/BatchReadConfigTest.java new file mode 100644 index 00000000000..b046a0e7362 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/BatchReadConfigTest.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.server.remoting.davex; + +import junit.framework.TestCase; + +import java.util.Properties; + +/** + * BatchReadConfigTest... + */ +public class BatchReadConfigTest extends TestCase { + + public void testDefaultDepth() { + BatchReadConfig cnf = new BatchReadConfig(); + + assertEquals(BatchReadConfig.DEPTH_DEFAULT, cnf.getDefaultDepth()); + assertEquals(cnf.getDefaultDepth(), cnf.getDepth("nt:base")); + + cnf.setDefaultDepth(5); + assertEquals(5, cnf.getDefaultDepth()); + assertEquals(cnf.getDefaultDepth(), cnf.getDepth("nt:base")); + + cnf.setDefaultDepth(BatchReadConfig.DEPTH_INFINITE); + assertEquals(BatchReadConfig.DEPTH_INFINITE, cnf.getDefaultDepth()); + assertEquals(cnf.getDefaultDepth(), cnf.getDepth("nt:base")); + + try { + cnf.setDefaultDepth(-12); + fail("Invalid depth"); + } catch (IllegalArgumentException e) { + //ok + } + } + + public void testDepth() { + BatchReadConfig cnf = new BatchReadConfig(); + + cnf.setDepth("nt:file", 15); + assertEquals(15, cnf.getDepth("nt:file")); + assertEquals(cnf.getDefaultDepth(), cnf.getDepth("nt:base")); + + cnf.setDepth("nt:file", BatchReadConfig.DEPTH_INFINITE); + assertEquals(BatchReadConfig.DEPTH_INFINITE, cnf.getDepth("nt:file")); + assertEquals(cnf.getDefaultDepth(), cnf.getDepth("nt:base")); + + try { + cnf.setDepth("nt:file",-12); + fail("Invalid depth"); + } catch (IllegalArgumentException e) { + //ok + } + } + + public void testAdd() { + Properties props = new Properties(); + props.setProperty("nt:file", "15"); + props.setProperty("default", "-1"); + + BatchReadConfig cnf = new BatchReadConfig(); + cnf.add(props); + + assertEquals(15, cnf.getDepth("nt:file")); + assertEquals(BatchReadConfig.DEPTH_INFINITE, cnf.getDefaultDepth()); + assertEquals(cnf.getDefaultDepth(), cnf.getDepth("nt:base")); + + cnf.setDefaultDepth(BatchReadConfig.DEPTH_DEFAULT); + assertEquals(15, cnf.getDepth("nt:file")); + assertEquals(BatchReadConfig.DEPTH_DEFAULT, cnf.getDefaultDepth()); + assertEquals(cnf.getDefaultDepth(), cnf.getDepth("nt:base")); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/DiffParserTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/DiffParserTest.java new file mode 100644 index 00000000000..43a717e77ad --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/DiffParserTest.java @@ -0,0 +1,385 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.remoting.davex; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** DiffParserTest... */ +public class DiffParserTest extends TestCase { + + public void testSetProperty() throws IOException, DiffException { + ArrayList l = new ArrayList(); + l.add("\"simple string\""); + l.add("2345"); + l.add("true"); + l.add("false"); + l.add("234.3455"); + l.add("null"); + + for (final String value : l) { + String diff = "^/a/prop : " + value; + + DummyDiffHandler handler = new DummyDiffHandler() { + @Override + public void setProperty(String targetPath, String diffValue) { + assertEquals(targetPath, "/a/prop"); + assertEquals(value, diffValue); + } + }; + DiffParser parser = new DiffParser(handler); + parser.parse(diff); + } + } + + public void testSetPropertyMissing() throws IOException, + DiffException { + ArrayList l = new ArrayList(); + l.add(""); + l.add(null); + + for (String value : l) { + String diff = "^/a/prop : " + ((value == null) ? "" : value); + + DummyDiffHandler handler = new DummyDiffHandler() { + @Override + public void setProperty(String targetPath, String diffValue) { + assertEquals(targetPath, "/a/prop"); + assertTrue(diffValue == null || "".equals(diffValue)); + } + }; + DiffParser parser = new DiffParser(handler); + parser.parse(diff); + } + } + + public void testSetPropertyWithUnicodeChars() throws IOException, + DiffException { + final String value = "\"String value containing \u2355\u8723 unicode chars.\""; + String diff = "^/a/prop : " + value; + + DiffHandler handler = new DummyDiffHandler() { + @Override + public void setProperty(String targetPath, String diffValue) { + assertEquals(targetPath, "/a/prop"); + assertEquals(value, diffValue); + } + }; + + DiffParser parser = new DiffParser(handler); + parser.parse(diff); + } + + public void testSetPropertyWithTrailingLineSep() throws IOException, + DiffException { + final String value = "\"String value ending with \r\r\n\n\r\n.\""; + String diff = "^/a/prop : " + value; + + DiffHandler handler = new DummyDiffHandler() { + @Override + public void setProperty(String targetPath, String diffValue) { + assertEquals(targetPath, "/a/prop"); + assertEquals(value, diffValue); + } + }; + + DiffParser parser = new DiffParser(handler); + parser.parse(diff); + } + + public void testSetPropertyWithSpecialChar() throws IOException, DiffException { + final String value = "+abc \\r+ \\n-ab >c \r\\r\\n+"; + String diff = "^/a/prop : " + value; + + DiffHandler handler = new DummyDiffHandler() { + @Override + public void setProperty(String targetPath, String diffValue) { + assertEquals(targetPath, "/a/prop"); + assertEquals(value, diffValue); + } + }; + + DiffParser parser = new DiffParser(handler); + parser.parse(diff); + } + + public void testSetPropertyUnterminatedString() throws IOException, + DiffException { + final String value = "\"String value ending with \r\r\n\n\r\n."; + String diff = "^/a/prop : " + value; + + DiffHandler handler = new DummyDiffHandler() { + @Override + public void setProperty(String targetPath, String diffValue) { + assertEquals(targetPath, "/a/prop"); + assertEquals(value, diffValue); + } + }; + DiffParser parser = new DiffParser(handler); + parser.parse(diff); + } + + public void testSetPropertyWithUnescapedAction() throws IOException, + DiffException { + String diff = "^abc : \r+def : \n-ghi : \r\n^jkl : \n\r>mno : \n"; + + DiffHandler handler = new DummyDiffHandler() { + @Override + public void addNode(String targetPath, String diffValue) { + assertEquals("def", targetPath); + assertEquals("", diffValue); + } + @Override + public void setProperty(String targetPath, String diffValue) { + assertTrue("abc".equals(targetPath) || "jkl".equals(targetPath)); + assertEquals("", diffValue); + } + @Override + public void remove(String targetPath, String diffValue) { + assertEquals("ghi", targetPath); + assertEquals("", diffValue); + } + + @Override + public void move(String targetPath, String diffValue) { + assertEquals("mno", targetPath); + assertEquals("\n", diffValue); + } + }; + + DiffParser parser = new DiffParser(handler); + parser.parse(diff); + } + + public void testValidDiffs() throws IOException, DiffException { + List l = new ArrayList(); + // unquoted string value + l.add(new String[] {"+/a/b : 134", "/a/b","134"}); + l.add(new String[] {"+/a/b : 2.3", "/a/b","2.3"}); + l.add(new String[] {"+/a/b : true", "/a/b","true"}); + // quoted string value + l.add(new String[] {"+/a/b : \"true\"", "/a/b","\"true\""}); + l.add(new String[] {"+/a/b : \"string value containing \u3456 unicode char.\"", "/a/b","\"string value containing \u3456unicode char.\""}); + // value consisting of quotes + l.add(new String[] {"+/a/b : \"", "/a/b","\""}); + l.add(new String[] {"+/a/b : \"\"", "/a/b","\"\""}); + // value consisting of single + l.add(new String[] {"+/a/b : '", "/a/b","'"}); + l.add(new String[] {"+/a/b : ''''", "/a/b","''''"}); + // value consisting of space(s) only + l.add(new String[] {"+/a/b : ", "/a/b"," "}); + l.add(new String[] {"+/a/b : ", "/a/b"," "}); + // value consisting of line separators only + l.add(new String[] {"+/a/b : \n", "/a/b","\n"}); + l.add(new String[] {"+/a/b : \r", "/a/b","\r"}); + l.add(new String[] {"+/a/b : \r\n", "/a/b","\r\n"}); + l.add(new String[] {"+/a/b : \r\n\n\r", "/a/b","\r\n\n\r"}); + // path containing white space + l.add(new String[] {"+/a /b : 123", "/a /b","123"}); + l.add(new String[] {"+/a\r\t/b : 123", "/a\r\t/b","123"}); + // path having trailing white space + l.add(new String[] {"+/a/b : 123", "/a/b","123"}); + l.add(new String[] {"+/a/b\r : 123", "/a/b\r","123"}); + l.add(new String[] {"+/a/b\r\n\n\r\n: 123", "/a/b\r\n\n\r\n","123"}); + // path containing reserved characters + l.add(new String[] {"++abc+ : val", "+abc+","val"}); + l.add(new String[] {"++++++ : val", "+++++","val"}); + // value containing reserved characters + l.add(new String[] {"+/a/b : +", "/a/b","+"}); + l.add(new String[] {"+/a/b : +->+-", "/a/b","+->+-"}); + l.add(new String[] {"+/a/b : \"+->+-\"", "/a/b","\"+->+-\""}); + // other white space than ' ' used as key-value separator + l.add(new String[] {"+/a/b :\r123", "/a/b","123"}); + l.add(new String[] {"+/a/b\r: 123", "/a/b","123"}); + l.add(new String[] {"+/a/b\r:\r123", "/a/b","123"}); + l.add(new String[] {"+/a/b\r:\n123", "/a/b","123"}); + l.add(new String[] {"+/a/b\t:\r123", "/a/b","123"}); + l.add(new String[] {"+/a/b\t:\t123", "/a/b","123"}); + // path containing colon + l.add(new String[] {"+/a:b/c:d : 123", "/a:b/c:d","123"}); + // value starting with colon -> ok + l.add(new String[] {"+/a/b : : val", "/a/b",": val"}); + // missing value + l.add(new String[] {"+/a/b : ", "/a/b", ""}); + l.add(new String[] {"+/a/b :\n", "/a/b", ""}); + + for (final String[] strs : l) { + DiffHandler hndl = new DummyDiffHandler() { + @Override + public void setProperty(String targetPath, String diffValue) { + assertEquals(strs[1], targetPath); + assertEquals(strs[2], diffValue); + } + }; + DiffParser parser = new DiffParser(hndl); + parser.parse(strs[0]); + } + + List l2 = new ArrayList(); + // multiple commands + l2.add("+abc :\n\n+def : val"); + l2.add("+abc :\n\n+def : val\n"); + l2.add("+abc : \r+def : val"); + l2.add("+/a/b : val\r+abc : \r "); + l2.add("+/a/b : val\r+abc :\n\n "); + // missing value in the last action. + l2.add("+/a/b : \r+abc :\n"); + l2.add("+/a/b : \\r+abc : abc\r\r+abc :\r"); + l2.add("+abc :\n\n+def : val\r\r>abc : "); + + for (String diff : l2) { + final List li = new ArrayList(); + DiffHandler dh = new DummyDiffHandler() { + @Override + public void addNode(String targetPath, String diffValue) { + li.add(diffValue); + } + }; + + DiffParser parser = new DiffParser(dh); + parser.parse(diff); + assertEquals(2, li.size()); + } + } + + public void testSeparatorLines() throws IOException, DiffException { + String diff = "+abc :\n\n+val : val"; + DiffHandler dh = new DummyDiffHandler() { + @Override + public void addNode(String targetPath, String diffValue) { + if ("abc".equals(targetPath)) { + assertEquals("", diffValue); + } else { + assertEquals("val", diffValue); + } + } + }; + new DiffParser(dh).parse(diff); + + diff = "+abc :\n+val : val"; + dh = new DummyDiffHandler() { + @Override + public void addNode(String targetPath, String diffValue) { + assertEquals("+val : val", diffValue); + } + }; + new DiffParser(dh).parse(diff); + + // TODO: check again: currently all line separation chars before an diff-char are ignored unless they are escaped in way the handler understands (e.g. JSON does: \\r for \r). + diff = "+abc :\r\r\r+def : val"; + dh = new DummyDiffHandler() { + @Override + public void addNode(String targetPath, String diffValue) { + if ("abc".equals(targetPath)) { + assertEquals("", diffValue); + } else { + assertEquals("val", diffValue); + } + } + }; + new DiffParser(dh).parse(diff); + + diff = "+abc : val\r+def :\n\n "; + dh = new DummyDiffHandler() { + @Override + public void addNode(String targetPath, String diffValue) { + if ("abc".equals(targetPath)) { + assertEquals("val", diffValue); + } else { + assertEquals("\n ", diffValue); + } + } + }; + new DiffParser(dh).parse(diff); + } + + public void testUnicodeLineSep() throws IOException, DiffException { + String diff = "+abc : val" + new String(new byte[] {Character.LINE_SEPARATOR}, "utf-8") + "+abc : val"; + DiffHandler dh = new DummyDiffHandler() { + @Override + public void addNode(String targetPath, String diffValue) { + assertEquals("abc", targetPath); + assertEquals("val", diffValue); + } + }; + new DiffParser(dh).parse(diff); + } + + public void testInvalidDiff() throws IOException, DiffException { + List l = new ArrayList(); + l.add(""); + // path, separator and value missing + l.add("+"); + l.add("+/a/b : val\r+"); + // path starting with white space, separator and value missing + l.add("+\n"); + // separator and value missing + l.add("+/a/b"); + l.add("+/a/b : val\r+abc\n"); + l.add("+/a/b :"); + // invalid for separator and value are missing (all : and white space + // is interpreted as part of the path. + l.add("+/a/b:"); + l.add("+/a/b:val"); + l.add("+/a/b: val"); + l.add("+/a/b:\rval"); + l.add("+/a/b :: val"); + // diff starting with white space + l.add(" +/a/b: val"); + l.add("\r\r\r\r\r\r+/a/b: val"); + // key starting with white space + l.add("+\r/a/b : 123"); + l.add("+ /a/b : 123"); + // key starting with colon + l.add("+:/a/b : 123"); + + for (String diff : l) { + try { + DiffParser parser = new DiffParser(new DummyDiffHandler()); + parser.parse(diff); + fail(diff + " is not a valid diff string -> should throw DiffException."); + } catch (DiffException e) { + // ok + } + } + } + + private class DummyDiffHandler implements DiffHandler { + + public void addNode(String targetPath, String diffValue) + throws DiffException { + // does nothing + } + + public void setProperty(String targetPath, String diffValue) + throws DiffException { + // does nothing + } + + public void remove(String targetPath, String diffValue) + throws DiffException { + // does nothing + } + + public void move(String targetPath, String diffValue) throws DiffException { + // does nothing + } + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandlerImportTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandlerImportTest.java new file mode 100644 index 00000000000..d92643eadde --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandlerImportTest.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.server.remoting.davex; + +import java.util.ArrayList; +import java.util.List; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Tests for {@code JsonDiffHandler} that trigger the import mode. + */ +public class JsonDiffHandlerImportTest extends AbstractJCRTest { + + private static final String JSOP_POLICY_TREE = "+rep:policy : {" + + "\"jcr:primaryType\" : \"rep:ACL\"," + + "\"allow\" : {" + "\"jcr:primaryType\" : \"rep:GrantACE\"," + + "\"rep:principalName\" : \"everyone\"," + + "\"rep:privileges\" : [\"jcr:write\"]" + "}" + "}"; + + private static final List ADD_NODES = new ArrayList(); + static { + ADD_NODES.add( + "+node1 : {" + +"\"jcr:primaryType\" : \"nt:file\"," + + "\"jcr:mixinTypes\" : [\"rep:AccessControllable\"]," + +"\"jcr:uuid\" : \"0a0ca2e9-ab98-4433-a12b-d57283765207\"," + +"\"rep:policy\" : {" + +"\"jcr:primaryType\" : \"rep:ACL\"," + +"\"deny0\" : {" + +"\"jcr:primaryType\" : \"rep:DenyACE\"," + +"\"rep:principalName\" : \"everyone\"," + +"\"rep:privileges\" : [\"jcr:read\"]" + +"}"+"}"+"}"); + ADD_NODES.add( + "+node2 : {" + +"\"jcr:primaryType\" : \"nt:unstructured\"," + + "\"jcr:mixinTypes\" : [\"rep:AccessControllable\"]," + +"\"rep:policy\" : {" + +"\"jcr:primaryType\" : \"rep:ACL\"," + +"\"allow\" : {" + +"\"jcr:primaryType\" : \"rep:GrantACE\"," + +"\"rep:principalName\" : \"everyone\"," + +"\"rep:privileges\" : [\"jcr:read\"]" + +"}," + +"\"deny\" : {" + +"\"jcr:primaryType\" : \"rep:DenyACE\"," + +"\"rep:principalName\" : \"everyone\"," + +"\"rep:privileges\" : [\"jcr:write\"]" + +"}" + +"}"+"}"); + + } + + private AccessControlManager acMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + acMgr = superuser.getAccessControlManager(); + } + + private static void assertPolicy(AccessControlManager acMgr, Node targetNode, int noACEs) throws RepositoryException { + AccessControlPolicy[] policies = acMgr.getPolicies(targetNode.getPath()); + assertEquals(policies.length, 1); + + AccessControlPolicy acl = policies[0]; + assertTrue(acl instanceof JackrabbitAccessControlList); + AccessControlEntry[] entries = ((JackrabbitAccessControlList) acl).getAccessControlEntries(); + assertEquals(noACEs, entries.length); + } + + /** + * Test two subsequent DIFF strings with policies, thus multiple addNode operations. + */ + public void testMultipleAddNodeOperations() throws Exception { + for(String jsonString : ADD_NODES) { + JsonDiffHandler h = new JsonDiffHandler(superuser, testRoot, null); + new DiffParser(h).parse(jsonString); + } + + assertPolicy(acMgr, testRootNode.getNode("node1"), 1); + assertPolicy(acMgr, testRootNode.getNode("node2"), 2); + } + + /** + * Test adding 'rep:policy' policy node as a child node of /testroot without + * intermediate node. + */ + public void testAllPolicyNode() throws Exception { + try { + testRootNode.addMixin("rep:AccessControllable"); + + JsonDiffHandler handler = new JsonDiffHandler(superuser, testRoot, null); + new DiffParser(handler).parse(JSOP_POLICY_TREE); + + assertTrue(testRootNode.hasNode("rep:policy")); + assertTrue(testRootNode.getNode("rep:policy").getDefinition().isProtected()); + + assertTrue(testRootNode.getNode("rep:policy").getPrimaryNodeType() + .getName().equals("rep:ACL")); + + assertPolicy(acMgr, testRootNode, 1); + + AccessControlEntry entry = ((AccessControlList) acMgr.getPolicies(testRoot)[0]).getAccessControlEntries()[0]; + assertEquals(EveryonePrincipal.NAME, entry.getPrincipal().getName()); + assertEquals(1, entry.getPrivileges().length); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + if (entry instanceof JackrabbitAccessControlEntry) { + assertTrue(((JackrabbitAccessControlEntry) entry).isAllow()); + } + + } finally { + superuser.refresh(false); + } + } + + /** + * Test adding 'rep:policy' policy node as a child node of /testroot without + * intermediate node. + */ + public void testUpdatePolicyNode() throws Exception { + try { + AccessControlUtils.addAccessControlEntry(superuser, testRoot, EveryonePrincipal.getInstance(), new String[] {Privilege.JCR_READ}, false); + + JsonDiffHandler handler = new JsonDiffHandler(superuser, testRoot, null); + new DiffParser(handler).parse(JSOP_POLICY_TREE); + + assertTrue(testRootNode.hasNode("rep:policy")); + assertTrue(testRootNode.getNode("rep:policy").getDefinition().isProtected()); + + assertTrue(testRootNode.getNode("rep:policy").getPrimaryNodeType() + .getName().equals("rep:ACL")); + + assertPolicy(acMgr, testRootNode, 1); + + AccessControlEntry entry = ((AccessControlList) acMgr.getPolicies(testRoot)[0]).getAccessControlEntries()[0]; + assertEquals(EveryonePrincipal.NAME, entry.getPrincipal().getName()); + assertEquals(1, entry.getPrivileges().length); + assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]); + + if (entry instanceof JackrabbitAccessControlEntry) { + assertTrue(((JackrabbitAccessControlEntry) entry).isAllow()); + } + + } finally { + superuser.refresh(false); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandlerTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandlerTest.java new file mode 100644 index 00000000000..9fc7a887d84 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonDiffHandlerTest.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.server.remoting.davex; + +import junit.framework.TestCase; +import org.xml.sax.ContentHandler; + +import javax.jcr.Credentials; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.ValueFactory; +import javax.jcr.Workspace; +import javax.jcr.retention.RetentionManager; +import javax.jcr.security.AccessControlManager; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.AccessControlException; +import java.util.HashMap; +import java.util.Map; + +/** + * JsonDiffHandlerTest... + */ +public class JsonDiffHandlerTest extends TestCase { + + public void testGetItemPath() throws Exception { + Map m = new HashMap(); + m.put("abc", "/reqPath/abc"); + m.put("abc/def/ghi", "/reqPath/abc/def/ghi"); + m.put("/abc", "/abc"); + m.put("/abc/def/ghi", "/abc/def/ghi"); + m.put(".", "/reqPath"); + m.put("./abc", "/reqPath/abc"); + m.put("abc/./def", "/reqPath/abc/def"); + m.put("/abc/./def", "/abc/def"); + m.put("..", "/"); + m.put("../abc/def", "/abc/def"); + m.put("abc/../def/.", "/reqPath/def"); + m.put("abc/../def/..", "/reqPath"); + m.put("abc/../def/..", "/reqPath"); + m.put("abc/../def/..", "/reqPath"); + m.put("abc/../def/..", "/reqPath"); + m.put("abc/../def/..", "/reqPath"); + m.put("abc/../def/..", "/reqPath"); + m.put("./././././", "/reqPath"); + m.put("/./././././", "/"); + m.put("/./abc/def/../ghi", "/abc/ghi"); + + JsonDiffHandler handler = new JsonDiffHandler(new DummySession(), "/reqPath", null); + for (String targetPath : m.keySet()) { + String expItemPath = m.get(targetPath); + assertEquals(expItemPath, handler.getItemPath(targetPath)); + } + } + + private final class DummySession implements Session { + + @Override + public Repository getRepository() { + return null; + } + + @Override + public String getUserID() { + return null; + } + + @Override + public Object getAttribute(String name) { + return null; + } + + @Override + public String[] getAttributeNames() { + return new String[0]; + } + + @Override + public Workspace getWorkspace() { + return null; + } + + @Override + public Session impersonate(Credentials credentials) { + return null; + } + + @Override + public Node getRootNode() { + return null; + } + + @Override + public Node getNodeByUUID(String uuid) { + return null; + } + + @Override + public Item getItem(String absPath) { + return null; + } + + @Override + public boolean itemExists(String absPath) { + return false; + } + + @Override + public void move(String srcAbsPath, String destAbsPath) { + } + + @Override + public void save() { + } + + @Override + public void refresh(boolean keepChanges) { + } + + @Override + public boolean hasPendingChanges() { + return false; + } + + @Override + public ValueFactory getValueFactory() { + return null; + } + + @Override + public void checkPermission(String absPath, String actions) throws AccessControlException { + } + + @Override + public ContentHandler getImportContentHandler(String parentAbsPath, int uuidBehavior) { + return null; + } + + @Override + public void importXML(String parentAbsPath, InputStream in, int uuidBehavior) { + } + + @Override + public void exportSystemView(String absPath, ContentHandler contentHandler, boolean skipBinary, boolean noRecurse) { + } + + @Override + public void exportSystemView(String absPath, OutputStream out, boolean skipBinary, boolean noRecurse) { + } + + @Override + public void exportDocumentView(String absPath, ContentHandler contentHandler, boolean skipBinary, boolean noRecurse) { + } + + @Override + public void exportDocumentView(String absPath, OutputStream out, boolean skipBinary, boolean noRecurse) { + } + + @Override + public void setNamespacePrefix(String prefix, String uri) { + } + + @Override + public String[] getNamespacePrefixes() { + return new String[0]; + } + + @Override + public String getNamespaceURI(String prefix) { + return null; + } + + @Override + public String getNamespacePrefix(String uri) { + return null; + } + + @Override + public void logout() { + } + + @Override + public boolean isLive() { + return false; + } + + @Override + public void addLockToken(String lt) { + } + + @Override + public String[] getLockTokens() { + return new String[0]; + } + + @Override + public void removeLockToken(String lt) { + } + + @Override + public AccessControlManager getAccessControlManager() { + return null; + } + + @Override + public Node getNode(String arg0) { + return null; + } + + @Override + public Node getNodeByIdentifier(String arg0) { + return null; + } + + @Override + public Property getProperty(String arg0) { + return null; + } + + @Override + public RetentionManager getRetentionManager() { + return null; + } + + @Override + public boolean hasCapability(String arg0, Object arg1, Object[] arg2) { + return false; + } + + @Override + public boolean hasPermission(String arg0, String arg1) { + return false; + } + + @Override + public boolean nodeExists(String arg0) { + return false; + } + + @Override + public boolean propertyExists(String arg0) { + return false; + } + + @Override + public void removeItem(String arg0) { + + } + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonWriterTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonWriterTest.java new file mode 100755 index 00000000000..b5909f04145 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/JsonWriterTest.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.server.remoting.davex; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter; +import org.easymock.EasyMockSupport; +import org.easymock.IAnswer; +import org.junit.Test; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.Value; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; +import static org.easymock.EasyMock.*; + +public class JsonWriterTest extends EasyMockSupport { + + @Test + public void testDoubleOutput() throws Exception { + StringWriter writer = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(writer); + + Node parent = createMock(Node.class); + Property doubleProperty = createMock(Property.class); + Value doublePropertyValue = createMock(Value.class); + expect(doubleProperty.getType()).andReturn(PropertyType.DOUBLE).anyTimes(); + expect(doubleProperty.getName()).andReturn("singleValued").anyTimes(); + expect(doubleProperty.isMultiple()).andReturn(false).anyTimes(); + expect(doubleProperty.getValue()).andReturn(doublePropertyValue).anyTimes(); + expect(doublePropertyValue.getType()).andReturn(PropertyType.DOUBLE).anyTimes(); + expect(doublePropertyValue.getDouble()).andReturn(5d).anyTimes(); + expect(doublePropertyValue.getString()).andReturn("5").anyTimes(); + + Property mvDoubleProperty = createMock(Property.class); + Value mvDoublePropertyValue1 = createMock(Value.class); + Value mvDoublePropertyValue2 = createMock(Value.class); + expect(mvDoubleProperty.getType()).andReturn(PropertyType.DOUBLE).anyTimes(); + expect(mvDoubleProperty.getName()).andReturn("multiValued").anyTimes(); + expect(mvDoubleProperty.isMultiple()).andReturn(true).anyTimes(); + expect(mvDoubleProperty.getValues()).andReturn(new Value[] { mvDoublePropertyValue1, mvDoublePropertyValue2}).anyTimes(); + expect(mvDoublePropertyValue1.getType()).andReturn(PropertyType.DOUBLE).anyTimes(); + expect(mvDoublePropertyValue1.getDouble()).andReturn(42d).anyTimes(); + expect(mvDoublePropertyValue1.getString()).andReturn("42").anyTimes(); + expect(mvDoublePropertyValue2.getType()).andReturn(PropertyType.DOUBLE).anyTimes(); + expect(mvDoublePropertyValue2.getDouble()).andReturn(98.6).anyTimes(); + expect(mvDoublePropertyValue2.getString()).andReturn("98.6").anyTimes(); + + final List properties = new ArrayList(); + properties.add(doubleProperty); + properties.add(mvDoubleProperty); + expect(parent.getProperties()).andAnswer(new IAnswer() { + @Override + public PropertyIterator answer() throws Throwable { + return new PropertyIteratorAdapter(properties.iterator()); + } + }); + expect(parent.getNodes()).andAnswer(new IAnswer() { + @Override + public NodeIterator answer() throws Throwable { + return new NodeIteratorAdapter(Collections.emptyIterator()); + } + }); + replayAll(); + + jsonWriter.write(parent, 1); + + assertEquals("{\":singleValued\":\"Double\",\"singleValued\":5,\":multiValued\":\"Double\",\"multiValued\":[42,98.6],\"::NodeIteratorSize\":0}", + writer.toString()); + + verifyAll(); + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/TestAll.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/TestAll.java new file mode 100644 index 00000000000..05db010796e --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/server/remoting/davex/TestAll.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.server.remoting.davex; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package org.apache.jackrabbit.server.remoting.davex. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.server.remoting.davex tests"); + + suite.addTestSuite(DiffParserTest.class); + suite.addTestSuite(JsonDiffHandlerImportTest.class); + suite.addTestSuite(JsonDiffHandlerTest.class); + suite.addTestSuite(BatchReadConfigTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/JcrDavExceptionTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/JcrDavExceptionTest.java new file mode 100644 index 00000000000..fcf13989892 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/JcrDavExceptionTest.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.webdav.jcr; + +import junit.framework.TestCase; + +import javax.jcr.lock.LockException; +import javax.jcr.RepositoryException; + +/** JcrDavExceptionTest... */ +public class JcrDavExceptionTest extends TestCase { + + public void testDerivedException() { + RepositoryException re = new DerievedRepositoryException(); + + // creating JcrDavException from the derived exception must not throw + // NPE (see issue https://issues.apache.org/jira/browse/JCR-1678) + JcrDavException jde = new JcrDavException(re); + + // error code must be the same as for LockException + assertEquals(new JcrDavException(new LockException()).getErrorCode(), + jde.getErrorCode()); + } + + public void testNullException() { + try { + new JcrDavException(null); + fail("Should throw NPE"); + } catch (NullPointerException e) { + // as documented in the javadoc + } + } + + /** + * Derived exception that does not extend from RepositoryException, which + * returns the 'default' error code. + */ + private static final class DerievedRepositoryException extends LockException { + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/JcrValueTypeTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/JcrValueTypeTest.java new file mode 100644 index 00000000000..90bc8a7ded7 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/JcrValueTypeTest.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.webdav.jcr; + +import junit.framework.TestCase; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.commons.webdav.JcrValueType; + +/** + * JcrValueTypeTest... + */ +public class JcrValueTypeTest extends TestCase { + + public void testTypeFromContentType() { + for (int i = PropertyType.UNDEFINED; i <= PropertyType.DECIMAL; i++) { + String ct = JcrValueType.contentTypeFromType(i); + assertEquals(i, JcrValueType.typeFromContentType(ct)); + } + } + + public void testTypeFromContentTypeIncludingCharSet() { + for (int i = PropertyType.UNDEFINED; i <= PropertyType.DECIMAL; i++) { + String ct = JcrValueType.contentTypeFromType(i) + "; charset=UTF-8"; + assertEquals(i, JcrValueType.typeFromContentType(ct)); + } + } + public void testTypeFromInvalidContentType() { + String[] invalids = new String[] {null, "", "jcr-value/invalid", "invalid/as-well"}; + + for (String invalid : invalids) { + assertEquals(PropertyType.UNDEFINED, JcrValueType.typeFromContentType(invalid)); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/LockTimeOutFormatTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/LockTimeOutFormatTest.java new file mode 100644 index 00000000000..8c634da8852 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/LockTimeOutFormatTest.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.webdav.jcr; + +import java.net.URISyntaxException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.xml.parsers.ParserConfigurationException; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.webdav.jcr.lock.JcrActiveLock; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** + * LockTimeOutFormatTest... + */ +public class LockTimeOutFormatTest extends TestCase { + + public void testOneSec() throws RepositoryException, URISyntaxException, ParserConfigurationException { + testfmt(1, "Second-1"); + } + + public void testInf() throws RepositoryException, URISyntaxException, ParserConfigurationException { + testfmt(Long.MAX_VALUE, "Infinite"); + } + + public void testTooLong() throws RepositoryException, URISyntaxException, ParserConfigurationException { + testfmt(Integer.MAX_VALUE + 100000L, "Infinite"); + } + + public void testNeg() throws RepositoryException, URISyntaxException, ParserConfigurationException { + // expired + testfmt(-1, null); + } + + private void testfmt(long jcrtimeout, String expectedString) throws RepositoryException, URISyntaxException, ParserConfigurationException { + + Lock l = new TestLock(jcrtimeout); + JcrActiveLock al = new JcrActiveLock(l); + + Document d = DomUtil.createDocument(); + Element activeLock = al.toXml(d); + assertEquals("activelock", activeLock.getLocalName()); + NodeList nl = activeLock.getElementsByTagNameNS("DAV:", "timeout"); + + if (expectedString == null) { + assertEquals(0, nl.getLength()); + } + else { + assertEquals(1, nl.getLength()); + Element timeout = (Element)nl.item(0); + String t = DomUtil.getText(timeout); + assertEquals(expectedString, t); + } + } + + /** + * Minimal Lock impl for tests above + */ + private static class TestLock implements Lock { + + private final long timeout; + + public TestLock(long timeout) { + this.timeout = timeout; + } + + public String getLockOwner() { + return null; + } + + public boolean isDeep() { + return false; + } + + public Node getNode() { + return null; + } + + public String getLockToken() { + return "foo"; + } + + public long getSecondsRemaining() throws RepositoryException { + return timeout; + } + + public boolean isLive() throws RepositoryException { + return timeout >= 0; + } + + public boolean isSessionScoped() { + return false; + } + + public boolean isLockOwningSession() { + return false; + } + + public void refresh() throws LockException, RepositoryException { + } + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/LockTokenMappingTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/LockTokenMappingTest.java new file mode 100644 index 00000000000..4b40748b7f0 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/LockTokenMappingTest.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.jcr; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.UUID; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.webdav.jcr.lock.LockTokenMapper; + +import junit.framework.TestCase; + +/** + * LockTokenMappingTest... + */ +public class LockTokenMappingTest extends TestCase { + + // test lock with a lock token similar to the ones assigned by Jackrabbit + public void testOpenScopedJcr() throws RepositoryException, URISyntaxException { + testRoundtrip(UUID.randomUUID().toString() + "-X"); + } + + // test a fancy lock string + public void testOpenScopedFancy() throws RepositoryException, URISyntaxException { + testRoundtrip("\n\u00c4 \u20ac"); + } + + private void testRoundtrip(String token) throws RepositoryException, URISyntaxException { + + Lock l = new TestLock(token); + String davtoken = LockTokenMapper.getDavLocktoken(l); + + // valid URI? + URI u = new URI(davtoken); + assertTrue("lock token must be absolute URI", u.isAbsolute()); + assertEquals("lock token URI must be all-ASCII", u.toASCIIString(), u.toString()); + + String jcrtoken = LockTokenMapper.getJcrLockToken(davtoken); + assertEquals(jcrtoken, l.getLockToken()); + } + + /** + * Minimal Lock impl for tests above + */ + private static class TestLock implements Lock { + + private final String token; + + public TestLock(String token) { + this.token = token; + } + + public String getLockOwner() { + return null; + } + + public boolean isDeep() { + return false; + } + + public Node getNode() { + return null; + } + + public String getLockToken() { + return token; + } + + public long getSecondsRemaining() throws RepositoryException { + return 0; + } + + public boolean isLive() throws RepositoryException { + return false; + } + + public boolean isSessionScoped() { + return false; + } + + public boolean isLockOwningSession() { + return false; + } + + public void refresh() throws LockException, RepositoryException { + } + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/observation/InfoMapTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/observation/InfoMapTest.java new file mode 100755 index 00000000000..89e0bd45b32 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/observation/InfoMapTest.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.webdav.jcr.observation; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.mockito.Mockito; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import junit.framework.TestCase; + +public class InfoMapTest extends TestCase { + + public void testInfoMap() + throws ParserConfigurationException, TransformerException, SAXException, IOException, RepositoryException { + + Session s = Mockito.mock(Session.class); + Mockito.when(s.getNamespaceURI("jcr")).thenReturn("http://www.jcp.org/jcr/1.0"); + + Map map = new HashMap(); + // mandated by JCR 2.0 + map.put("srcChildRelPath", "/x"); + // OAK extension, see https://issues.apache.org/jira/browse/OAK-1669 + map.put("jcr:primaryType", "nt:unstructured"); + Document doc = DomUtil.createDocument(); + Element container = DomUtil.createElement(doc, "x", null); + doc.appendChild(container); + SubscriptionImpl.serializeInfoMap(container, s, map); + ByteArrayOutputStream xml = new ByteArrayOutputStream(); + DomUtil.transformDocument(doc, xml); + + // reparse + Document tripped = DomUtil.parseDocument(new ByteArrayInputStream(xml.toByteArray())); + Element top = tripped.getDocumentElement(); + assertEquals("x", top.getLocalName()); + Element emap = DomUtil.getChildElement(top, ObservationConstants.N_EVENTINFO); + assertNotNull(emap); + Element path = DomUtil.getChildElement(emap, "srcChildRelPath", null); + assertNotNull(path); + Element type = DomUtil.getChildElement(emap, "primaryType", Namespace.getNamespace("http://www.jcp.org/jcr/1.0")); + assertNotNull(type); + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/AbstractSecurityTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/AbstractSecurityTest.java new file mode 100644 index 00000000000..7d24079e5bf --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/AbstractSecurityTest.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.webdav.jcr.security; + +import javax.jcr.Repository; +import javax.jcr.security.AccessControlManager; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractSecurityTest extends AbstractJCRTest { + + protected AccessControlManager acMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (isSupported(Repository.OPTION_ACCESS_CONTROL_SUPPORTED)) { + acMgr = superuser.getAccessControlManager(); + } else { + throw new NotExecutableException(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/JcrSupportedPrivilegePropertyTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/JcrSupportedPrivilegePropertyTest.java new file mode 100644 index 00000000000..65877707b26 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/JcrSupportedPrivilegePropertyTest.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.webdav.jcr.security; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.webdav.security.SupportedPrivilege; + +public class JcrSupportedPrivilegePropertyTest extends AbstractSecurityTest { + + public void testSupportedPrivileges() throws RepositoryException { + Set privs = new HashSet(Arrays.asList(acMgr.getSupportedPrivileges(testRoot))); + JcrSupportedPrivilegesProperty prop = new JcrSupportedPrivilegesProperty(superuser, testRoot); + List value = prop.asDavProperty().getValue(); + + if (privs.contains(acMgr.privilegeFromName(Privilege.JCR_ALL))) { + assertEquals(1, value.size()); + } + } + + public void testJcrAllPrivilege() throws RepositoryException { + JcrSupportedPrivilegesProperty prop = new JcrSupportedPrivilegesProperty(superuser); + List value = prop.asDavProperty().getValue(); + + assertEquals(1, value.size()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/JcrUserPrivilegesPropertyTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/JcrUserPrivilegesPropertyTest.java new file mode 100644 index 00000000000..c861d28237b --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/jcr/security/JcrUserPrivilegesPropertyTest.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.webdav.jcr.security; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlManager; + +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.security.Privilege; +import org.apache.jackrabbit.webdav.xml.Namespace; + +public class JcrUserPrivilegesPropertyTest extends AbstractSecurityTest { + + private Set getExpected(AccessControlManager acMgr, Session s) throws RepositoryException { + Set expected = new HashSet(); + for (javax.jcr.security.Privilege p : acMgr.getPrivileges(testRoot)) { + String localName = Text.getLocalName(p.getName()); + String prefix = Text.getNamespacePrefix(p.getName()); + Namespace ns = (prefix.isEmpty()) ? Namespace.EMPTY_NAMESPACE : Namespace.getNamespace(prefix, s.getNamespaceURI(prefix)); + expected.add(Privilege.getPrivilege(localName, ns)); + } + return expected; + } + + public void testAdminPrivileges() throws RepositoryException { + Set expected = getExpected(acMgr, superuser); + + JcrUserPrivilegesProperty upp = new JcrUserPrivilegesProperty(superuser, testRoot); + Collection davPrivs = upp.asDavProperty().getValue(); + + assertEquals(expected.size(), davPrivs.size()); + assertTrue(davPrivs.containsAll(expected)); + } + + public void testReadOnlyPrivileges() throws RepositoryException { + Session readOnly = getHelper().getReadOnlySession(); + try { + Set expected = getExpected(readOnly.getAccessControlManager(), readOnly); + + JcrUserPrivilegesProperty upp = new JcrUserPrivilegesProperty(readOnly, testRoot); + Collection davPrivs = upp.asDavProperty().getValue(); + + assertEquals(expected.size(), davPrivs.size()); + assertTrue(davPrivs.containsAll(expected)); + } finally { + if (readOnly != null) { + readOnly.logout(); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/LitmusTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/LitmusTest.java new file mode 100644 index 00000000000..e3198c00441 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/LitmusTest.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.webdav.simple; + +import java.io.ByteArrayOutputStream; +import java.io.File; + +import javax.jcr.Repository; +import javax.jcr.Session; + +import junit.framework.TestCase; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.util.Text; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LitmusTest extends TestCase { + + /** + * Logger instance. + */ + private static final Logger log = LoggerFactory.getLogger(LitmusTest.class); + + public void testLitmus() throws Exception { + File dir = new File("target", "litmus"); + String litmus = System.getProperty("litmus", "litmus"); + + if (Boolean.getBoolean("jackrabbit.test.integration") + && isLitmusAvailable(litmus)) { + final Repository repository = JcrUtils.getRepository( + "jcr-jackrabbit://" + Text.escapePath(dir.getCanonicalPath())); + Session session = repository.login(); // for the TransientRepository + try { + Server server = new Server(); + + ServerConnector connector = new ServerConnector(server); + connector.setHost("localhost"); + connector.setPort(Integer.getInteger("litmus.port", 0)); + + server.addConnector(connector); + + ServletHolder holder = new ServletHolder( + new SimpleWebdavServlet() { + @Override + public Repository getRepository() { + return repository; + } + }); + holder.setInitParameter("resource-config", "/config.xml"); + + ServletContextHandler schandler = new ServletContextHandler(server, "/"); + schandler.addServlet(holder, "/*"); + + server.start(); + try { + int port = connector.getLocalPort(); + String url = "http://localhost:" + port + "/default"; + + ProcessBuilder builder = + new ProcessBuilder(litmus, url, "admin", "admin"); + builder.directory(dir); + builder.redirectErrorStream(); + + assertLitmus(builder, "basic", 0); + + assertLitmus(builder, "http", 0); + + assertLitmus(builder, "props", 0); + + // FIXME: JCR-2637: WebDAV shallow copy test failure + assertLitmus(builder, "copymove", 1); + + // FIXME: JCR-2638: Litmus locks test failures + assertLitmus(builder, "locks", 1); + } finally { + server.stop(); + } + } finally { + session.logout(); + } + } + } + + private void assertLitmus( + ProcessBuilder builder, String tests, int exit) throws Exception { + builder.environment().put("TESTS", tests); + Process process = builder.start(); + IOUtils.copy(process.getInputStream(), System.out); + assertEquals(exit, process.waitFor()); + } + + private static boolean isLitmusAvailable(String litmus) { + try { + ProcessBuilder builder = new ProcessBuilder(litmus, "--version"); + builder.redirectErrorStream(); + Process process = builder.start(); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + IOUtils.copy(process.getInputStream(), buffer); + int rv = process.waitFor(); + log.info("litmus version: {}", buffer.toString("US-ASCII").trim()); + + return rv == 0; + } catch (Exception e) { + log.warn("litmus is not available: " + litmus, e); + return false; + } + } + +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImplExTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImplExTest.java new file mode 100644 index 00000000000..d0f17d0c577 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/LocatorFactoryImplExTest.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.webdav.simple; + +import junit.framework.TestCase; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavResourceLocator; + +/** LocatorFactoryImplExTest... */ +public class LocatorFactoryImplExTest extends TestCase { + + private DavLocatorFactory factory; + + @Override + protected void setUp() throws Exception { + super.setUp(); + // for simplicity (not yet used) ignore the path prefix. + factory = new LocatorFactoryImplEx(null); + } + + /** + * Test for issue https://issues.apache.org/jira/browse/JCR-1679: An top + * level resource (node directly below the root) whose name equals the + * workspace name results in wrong collection behaviour (garbeled locator + * of child resources). + */ + public void testCollectionNameEqualsWorkspaceName() { + String prefix = "http://localhost:8080/jackrabbit/repository"; + String workspacePath = "/default"; + String nodePath = "/default/another"; + + DavResourceLocator locator = factory.createResourceLocator(prefix, workspacePath, nodePath, false); + assertTrue(locator.getHref(true).indexOf("/default/default") > 0); + + DavResourceLocator locator2 = factory.createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), locator.getResourcePath()); + assertEquals(locator, locator2); + assertEquals(nodePath, locator2.getRepositoryPath()); + } +} diff --git a/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/ResourceConfigTest.java b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/ResourceConfigTest.java new file mode 100644 index 00000000000..0ac24f2f214 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/java/org/apache/jackrabbit/webdav/simple/ResourceConfigTest.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.webdav.simple; + +import junit.framework.TestCase; +import org.apache.jackrabbit.server.io.DefaultHandler; +import org.apache.jackrabbit.server.io.IOHandler; +import org.apache.jackrabbit.server.io.IOManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * ResourceConfigTest... + */ +public class ResourceConfigTest extends TestCase { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(ResourceConfigTest.class); + + public void testIOManagerConfig() throws Exception { + InputStream in = new ByteArrayInputStream(CONFIG_1.getBytes("UTF-8")); + + ResourceConfig config = new ResourceConfig(null); + config.parse(in); + + IOManager ioMgr = config.getIOManager(); + assertNotNull(ioMgr); + assertEquals("org.apache.jackrabbit.server.io.IOManagerImpl", ioMgr.getClass().getName()); + + IOHandler[] handlers = ioMgr.getIOHandlers(); + assertNotNull(handlers); + assertEquals(1, handlers.length); + assertEquals("org.apache.jackrabbit.server.io.DefaultHandler", handlers[0].getName()); + } + + public void testIOManagerConfigWithParam() throws Exception { + InputStream in = new ByteArrayInputStream(CONFIG_2.getBytes("UTF-8")); + + ResourceConfig config = new ResourceConfig(null); + config.parse(in); + + IOManager ioMgr = config.getIOManager(); + assertNotNull(ioMgr); + assertEquals("org.apache.jackrabbit.server.io.IOManagerImpl", ioMgr.getClass().getName()); + + IOHandler[] handlers = ioMgr.getIOHandlers(); + assertNotNull(handlers); + assertEquals(1, handlers.length); + assertEquals("org.apache.jackrabbit.server.io.DefaultHandler", handlers[0].getName()); + DefaultHandler dh = (DefaultHandler) handlers[0]; + assertEquals("nt:unstructured", dh.getCollectionNodeType()); + assertEquals("nt:unstructured", dh.getNodeType()); + assertEquals("nt:resource", dh.getContentNodeType()); + } + + + private static final String CONFIG_1 = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " " + + ""; + + private static final String CONFIG_2 = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " " + + ""; +} \ No newline at end of file diff --git a/jackrabbit-jcr-server/src/test/resources/config.xml b/jackrabbit-jcr-server/src/test/resources/config.xml new file mode 100644 index 00000000000..a01206d651e --- /dev/null +++ b/jackrabbit-jcr-server/src/test/resources/config.xml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nt:file + nt:resource + + + + + + + + + + + + + rep + jcr + + + + + + diff --git a/jackrabbit-jcr-server/src/test/resources/logback-test.xml b/jackrabbit-jcr-server/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..fcc74e923bc --- /dev/null +++ b/jackrabbit-jcr-server/src/test/resources/logback-test.xml @@ -0,0 +1,31 @@ + + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-jcr-server/src/test/resources/protectedHandlers.properties b/jackrabbit-jcr-server/src/test/resources/protectedHandlers.properties new file mode 100644 index 00000000000..f51e6563dd8 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/resources/protectedHandlers.properties @@ -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. + +# ProtectedItemRemoveHandler implementation class +javax.jcr.tck.access.control.list.handler=org.apache.jackrabbit.server.remoting.davex.AclRemoveHandler diff --git a/jackrabbit-jcr-server/src/test/resources/protectedHandlersConfig.xml b/jackrabbit-jcr-server/src/test/resources/protectedHandlersConfig.xml new file mode 100644 index 00000000000..949aaf100df --- /dev/null +++ b/jackrabbit-jcr-server/src/test/resources/protectedHandlersConfig.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/jackrabbit-jcr-server/src/test/resources/repository.xml b/jackrabbit-jcr-server/src/test/resources/repository.xml new file mode 100644 index 00000000000..95f050e8ce8 --- /dev/null +++ b/jackrabbit-jcr-server/src/test/resources/repository.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-jcr-server/src/test/resources/repositoryStubImpl.properties b/jackrabbit-jcr-server/src/test/resources/repositoryStubImpl.properties new file mode 100644 index 00000000000..e932c14a43e --- /dev/null +++ b/jackrabbit-jcr-server/src/test/resources/repositoryStubImpl.properties @@ -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. + +# Stub implementation class +javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.core.JackrabbitRepositoryStub + +# the repository home +org.apache.jackrabbit.repository.home=target/repository + +# the repository configuration +org.apache.jackrabbit.repository.config=target/test-classes/repository.xml diff --git a/jackrabbit-jcr-servlet/README.txt b/jackrabbit-jcr-servlet/README.txt new file mode 100644 index 00000000000..d007976b0bc --- /dev/null +++ b/jackrabbit-jcr-servlet/README.txt @@ -0,0 +1,8 @@ +======================================================= +Apache Jackrabbit JCR Servlets +======================================================= + +JCR Servlets is a collection of servlets and other classes designed to +make it easier to use Jackrabbit and other JCR content repositories in +web applications. + diff --git a/jackrabbit-jcr-servlet/pom.xml b/jackrabbit-jcr-servlet/pom.xml new file mode 100644 index 00000000000..4df90047138 --- /dev/null +++ b/jackrabbit-jcr-servlet/pom.xml @@ -0,0 +1,67 @@ + + + + + + 4.0.0 + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + + jackrabbit-jcr-servlet + + Jackrabbit JCR Servlets + + Servlets and related classes for easy use of JCR content repositories + in web applications. + + + + + javax.jcr + jcr + + + javax.servlet + servlet-api + 2.3 + provided + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-rmi + ${project.version} + true + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + true + + + + diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/AbstractRepositoryServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/AbstractRepositoryServlet.java new file mode 100644 index 00000000000..8de91fbf35c --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/AbstractRepositoryServlet.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.servlet; + +import java.io.IOException; +import java.util.Properties; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.jackrabbit.commons.repository.ProxyRepository; + +/** + * Abstract base class for servlets that make a repository available in + * the servlet context. This class handles the initialization and cleanup + * tasks of setting up and clearing the configured repository attribute, + * while a subclass only needs to implement the abstract + * {@link #getRepository()} method that returns the actual content repository. + *

        + * The {@link Repository} instance bound to the servlet context is actually + * a {@link ProxyRepository} for late binding of the underlying content repository. + *

        + * The default name of the repository attribute is + * "javax.jcr.Repository", but it can be changed by specifying + * an init parameter with the same name: + *

        + * <servlet>
        + *   <init-param>
        + *     <param-name>javax.jcr.Repository</param-name>
        + *     <param-value>my.repository.attribute</param-value>
        + *     <description>
        + *       This init parameter causes the repository to be looked up from
        + *       the "my.repository.attribute" attribute instead of the default
        + *       "javax.jcr.Repository".
        + *     </description>
        + *   </init-param>
        + * </servlet>
        + * 
        + *

        + * A repository servlet can also be mapped to the URL space. See the + * {@link #doGet(HttpServletRequest, HttpServletResponse)} method for + * the details of the default behavior. + * + * @since 1.4 + */ +public abstract class AbstractRepositoryServlet extends HttpServlet { + + /** + * Binds a {@link ProxyRepository} with the repository returned by + * {@link #getRepository()} in the configured servlet context attribute. + * + * @throws ServletException + */ + public void init() throws ServletException { + getServletContext().setAttribute( + getAttributeName(), + new ProxyRepository() { + @Override + protected Repository getRepository() + throws RepositoryException { + return AbstractRepositoryServlet.this.getRepository(); + } + }); + } + + /** + * Removes the repository attribute from the servlet context. + */ + public void destroy() { + getServletContext().removeAttribute(getAttributeName()); + } + + /** + * Returns the repository that will be used by the + * {@link ProxyRepository} bound to the servlet context. + * + * @return repository + * @throws RepositoryException if the repository could not be created + */ + protected abstract Repository getRepository() + throws RepositoryException; + + /** + * Returns the name of the repository attribute. The default + * implementation returns "javax.jcr.Repository" or + * the value of the "javax.jcr.Repository" init parameter. + *

        + * A subclass can override this method to customize the attribute name, + * but for consistency it is generally better not to do that. + * + * @return name of the repository attribute + */ + protected String getAttributeName() { + String name = Repository.class.getName(); + return getInitParameter(name, name); + } + + /** + * Utility method that returns the named init parameter or the given + * default value if the parameter does not exist. + * + * @param name name of the init parameter + * @param def default value + * @return value of the init parameter, or the default value + */ + protected String getInitParameter(String name, String def) { + String value = getInitParameter(name); + if (value == null) { + value = def; + } + return value; + } + + /** + * Outputs the repository descriptors either as a collection of properties + * (see {@link Properties#store(java.io.OutputStream, String)} or + * individually addressable text/plain resources based on the request URI. + *

        + * A typical mapping for a repository servlet would be: + *

        +     * <servlet-mapping>
        +     *   <servlet-name>Repository</servlet-name>
        +     *   <url-pattern>/repository/*</url-pattern>
        +     * </servlet-mapping>
        +     * 
        + *

        + * This mapping would allow clients to retrieve all repository descriptors + * from http://server/context/repository/ and to address + * individual descriptors by key with URIs like + * http://server/context/repository/key. + * For example, the name of the repository vendor could be retrieved from + * http://server/context/repository/jcr.repository.vendor. + * Likewise, a 404 (not found) response from + * http://server/context/repository/level.2.supported would + * indicate that the repository does not support Level 2 features. + *

        + * Note that mapping a repository servlet to the URL space is optional, + * as the main purpose of the servlet is to make a repository available + * in the servlet context, not to expose repository information to web + * clients. + * + * @param request HTTP request + * @param response HTTP response + * @throws IOException on IO errors + * @throws ServletException on servlet errors + */ + protected void doGet( + HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + Repository repository = new ServletRepository(this); + + String info = request.getPathInfo(); + if (info == null || info.equals("/")) { + Properties descriptors = new Properties(); + String[] keys = repository.getDescriptorKeys(); + for (int i = 0; i < keys.length; i++) { + descriptors.setProperty( + keys[i], repository.getDescriptor(keys[i])); + } + // TODO: Using UTF-8 instead of ISO-8859-1 would be better, but + // would require re-implementing the Properties.store() method + response.setContentType("text/plain; charset=ISO-8859-1"); + descriptors.store(response.getOutputStream(), getAttributeName()); + } else { + String key = info.substring(1); // skip the leading "/" + String descriptor = repository.getDescriptor(key); + if (descriptor != null) { + response.setContentType("text/plain; charset=UTF-8"); + response.getWriter().write(descriptor); + } else { + response.sendError( + HttpServletResponse.SC_NOT_FOUND, + "Repository descriptor " + key + " not found"); + } + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ContextRepositoryServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ContextRepositoryServlet.java new file mode 100644 index 00000000000..4b9a7d838c1 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ContextRepositoryServlet.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.servlet; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.servlet.ServletContext; + +/** + * Servlet that makes a repository from one servlet context attribute + * available as another servlet context attribute. The source context + * can be different from the context of this servlet. + *

        + * The supported initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the target servlet context attribute. + * The default value is "javax.jcr.Repository". + *
        + *
        path
        + *
        + * Context path of the source servlet context. The source context + * defaults to the context of this servlet if this parameter is not set. + *
        + *
        name
        + *
        + * Name of the source servlet context attribute. The default value + * is "javax.jcr.Repository". The name of the source attribute + * can be the same as the name of target attribute only if the source + * context is different from the context of this servlet. + *
        + *
        + *

        + * This servlet can also be mapped to the URL space. See + * {@link AbstractRepositoryServlet} for the details. + * + * @since 1.4 + */ +public class ContextRepositoryServlet extends AbstractRepositoryServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 6222606878557491477L; + + /** + * Creates and returns the repository in the configured servlet + * context attribute. + * + * @return repository + */ + protected Repository getRepository() throws RepositoryException { + String path = getInitParameter("path"); + String name = getInitParameter("name", Repository.class.getName()); + + ServletContext context = getServletContext(); + if (path != null && context.equals(context.getContext(path))) { + path = null; + } + + if (path == null && name.equals(getAttributeName())) { + throw new RepositoryException( + "Invalid configuration: Can not duplicate attribute " + + name + " of servlet " + getServletName()); + } + + ServletContext otherContext = context.getContext(path); + if (otherContext == null) { + throw new RepositoryException( + "Repository not found: Servlet context " + path + + " does not exist or is not accessible from context " + + context.getServletContextName()); + } + + Object repository = otherContext.getAttribute(name); + if (repository instanceof Repository) { + return (Repository) repository; + } else if (repository != null) { + throw new RepositoryException( + "Invalid repository: Attribute " + name + + " in servlet context " + otherContext.getServletContextName() + + " is an instance of " + repository.getClass().getName()); + } else { + throw new RepositoryException( + "Repository not found: Attribute " + name + + " does not exist in servlet context " + + otherContext.getServletContextName()); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/FilterRepositoryFactory.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/FilterRepositoryFactory.java new file mode 100644 index 00000000000..3baad0a1958 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/FilterRepositoryFactory.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.servlet; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; + +import org.apache.jackrabbit.commons.repository.RepositoryFactory; + +/** + * Factory that looks up a repository from the context of a given filter. + *

        + * The default name of the repository attribute is + * "javax.jcr.Repository", but it can be changed by specifying + * an init parameter with the same name: + *

        + * <filter>
        + *   <init-param>
        + *     <param-name>javax.jcr.Repository</param-name>
        + *     <param-value>my.repository.attribute</param-value>
        + *     <description>
        + *       This init parameter causes the repository to be looked up from
        + *       the "my.repository.attribute" attribute instead of the default
        + *       "javax.jcr.Repository".
        + *     </description>
        + *   </init-param>
        + * </filter>
        + * 
        + * + * @since Apache Jackrabbit 1.6 + */ +public class FilterRepositoryFactory implements RepositoryFactory { + + /** + * Configuration of the filter whose context contains the repository. + */ + private final FilterConfig config; + + /** + * Creates a factory for looking up a repository from the context + * associated with the given filter configuration. + * + * @param config filter configuration + */ + public FilterRepositoryFactory(FilterConfig config) { + this.config = config; + } + + /** + * Looks up and returns a repository bound in the servlet context of + * the given filter. + * + * @return repository from servlet context + * @throws RepositoryException if the repository is not available + */ + public Repository getRepository() throws RepositoryException { + String name = config.getInitParameter(Repository.class.getName()); + if (name == null) { + name = Repository.class.getName(); + } + + ServletContext context = config.getServletContext(); + Object repository = context.getAttribute(name); + if (repository instanceof Repository) { + return (Repository) repository; + } else if (repository != null) { + throw new RepositoryException( + "Invalid repository: Attribute " + name + + " in servlet context " + context.getServletContextName() + + " is an instance of " + repository.getClass().getName()); + } else { + throw new RepositoryException( + "Repository not found: Attribute " + name + + " does not exist in servlet context " + + context.getServletContextName()); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/JNDIBindingServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/JNDIBindingServlet.java new file mode 100644 index 00000000000..de6b5a0ffbc --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/JNDIBindingServlet.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.servlet; + +import java.util.Enumeration; +import java.util.Hashtable; + +import javax.jcr.Repository; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; + +/** + * Servlet that binds a repository from a servlet context attribute in JNDI. + *

        + * The initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute that contains the repository. + * The default value is "javax.jcr.Repository". + *
        + *
        location
        + *
        + * Location where to bind the repository in the JNDI directory. + * The default value is "javax/jcr/Repository". + *
        + *
        *
        + *
        + * All other init parameters are used as the JNDI environment when + * instantiating {@link InitialContext} for binding up the repository. + *
        + *
        + * + * @since 1.4 + */ +public class JNDIBindingServlet extends HttpServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = -9033906248473370936L; + + /** + * JNDI context to which to bind the repository. + */ + private Context context; + + /** + * Location of the repository within the JNDI context. + */ + private String location = Repository.class.getName().replace('.', '/'); + + /** + * Binds a repository from the servlet context in the configured + * JNDI location. + * + * @throws ServletException if the repository could not be bound in JNDI + */ + public void init() throws ServletException { + try { + Hashtable environment = new Hashtable(); + Enumeration names = getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + if (name.equals("location")) { + location = getInitParameter(name); + } else if (!name.equals(Repository.class.getName())) { + environment.put(name, getInitParameter(name)); + } + } + context = new InitialContext(environment); + context.bind(location, new ServletRepository(this)); + } catch (NamingException e) { + throw new ServletException( + "Failed to bind repository to JNDI: " + location, e); + } + } + + /** + * Unbinds the repository from JNDI. + */ + public void destroy() { + try { + context.unbind(location); + } catch (NamingException e) { + log("Failed to unbind repository from JNDI: " + location, e); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/JNDIRepositoryServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/JNDIRepositoryServlet.java new file mode 100644 index 00000000000..f014e348553 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/JNDIRepositoryServlet.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.servlet; + +import java.util.Enumeration; +import java.util.Hashtable; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.apache.jackrabbit.commons.repository.JNDIRepositoryFactory; + +/** + * Servlet that makes a repository from JNDI available as an attribute + * in the servlet context. + *

        + * The supported initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute to put the repository in. + * The default value is "javax.jcr.Repository". + *
        + *
        location
        + *
        + * Location of the repository in the JNDI directory. + * The default value is "javax/jcr/Repository". + *
        + *
        *
        + *
        + * All other init parameters are used as the JNDI environment when + * instantiating {@link InitialContext} for looking up the repository. + *
        + *
        + *

        + * This servlet can also be mapped to the URL space. See + * {@link AbstractRepositoryServlet} for the details. + * + * @since 1.4 + */ +public class JNDIRepositoryServlet extends AbstractRepositoryServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 8952525573562952560L; + + /** + * Returns a JNDI repository based on the configured init parameters. + * + * @return JNDI repository + */ + protected Repository getRepository() throws RepositoryException { + try { + String location = Repository.class.getName().replace('.', '/'); + Hashtable environment = new Hashtable(); + Enumeration names = getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + if (name.equals("location")) { + location = getInitParameter(name); + } else if (!name.equals(Repository.class.getName())) { + environment.put(name, getInitParameter(name)); + } + } + return new JNDIRepositoryFactory( + new InitialContext(environment), location).getRepository(); + } catch (NamingException e) { + throw new RepositoryException( + "Repository not found: Invalid JNDI context", e); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ServletRepository.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ServletRepository.java new file mode 100644 index 00000000000..42605130916 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ServletRepository.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.servlet; + +import javax.servlet.FilterConfig; +import javax.servlet.GenericServlet; + +import org.apache.jackrabbit.commons.repository.ProxyRepository; + +/** + * Proxy for a repository bound in servlet context. The configured repository + * attribute is looked up from the servlet context during each method call. + * Thus the repository does not need to exist when this class is instantiated. + * The repository can also be replaced with another repository during the + * lifetime of an instance of this class. + *

        + * A typical way to use this class would be: + *

        + * public class MyServlet extends HttpServlet {
        + *
        + *     private final Repository repository = new ServletRepository(this);
        + *
        + *     protected void doGet(
        + *             HttpServletRequest request, HttpServletResponse response)
        + *             throws ServletException, IOException {
        + *          try {
        + *              Session session = repository.login();
        + *              try {
        + *                  ...;
        + *              } finally {
        + *                  session.logout();
        + *              }
        + *          } catch (RepositoryException e) {
        + *              throw new ServletException(e);
        + *          }
        + *      }
        + *
        + * }
        + * 
        + *

        + * Starting with version 1.6 this class can also be used by a servlet filter: + *

        + * public class MyFilter implements Filter {
        + *
        + *     private Repository repository;
        + *
        + *     public void init(FilterConfig config) {
        + *         repository = new ServletRepository(config);
        + *     }
        + *
        + *     // ...
        + *
        + * }
        + * 
        + + * + * @since 1.4 + * @see ServletRepositoryFactory + * @see FilterRepositoryFactory + */ +public class ServletRepository extends ProxyRepository { + + /** + * Creates a proxy for a repository found in the context of the given + * servlet. + * + * @param servlet servlet + */ + public ServletRepository(GenericServlet servlet) { + super(new ServletRepositoryFactory(servlet)); + } + + /** + * Creates a proxy for a repository found in the servlet context + * associated with the given filter configuration. + * + * @since Apache Jackrabbit 1.6 + * @param config filter configuration + */ + public ServletRepository(FilterConfig config) { + super(new FilterRepositoryFactory(config)); + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ServletRepositoryFactory.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ServletRepositoryFactory.java new file mode 100644 index 00000000000..5a70c3a86c3 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/ServletRepositoryFactory.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.servlet; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.servlet.GenericServlet; +import javax.servlet.ServletContext; + +import org.apache.jackrabbit.commons.repository.RepositoryFactory; + +/** + * Factory that looks up a repository from the context of a given servlet. + *

        + * The default name of the repository attribute is + * "javax.jcr.Repository", but it can be changed by specifying + * an init parameter with the same name: + *

        + * <servlet>
        + *   <init-param>
        + *     <param-name>javax.jcr.Repository</param-name>
        + *     <param-value>my.repository.attribute</param-value>
        + *     <description>
        + *       This init parameter causes the repository to be looked up from
        + *       the "my.repository.attribute" attribute instead of the default
        + *       "javax.jcr.Repository".
        + *     </description>
        + *   </init-param>
        + * </servlet>
        + * 
        + * + * @since 1.4 + */ +public class ServletRepositoryFactory implements RepositoryFactory { + + /** + * The servlet whose context contains the repository. + */ + private final GenericServlet servlet; + + /** + * Creates a factory for looking up a repository from the context of + * the given servlet. + * + * @param servlet servlet whose context contains the repository + */ + public ServletRepositoryFactory(GenericServlet servlet) { + this.servlet = servlet; + } + + /** + * Looks up and returns a repository bound in the configured servlet + * context attribute. + * + * @return repository from servlet context + * @throws RepositoryException if the repository is not available + */ + public Repository getRepository() throws RepositoryException { + String name = servlet.getInitParameter(Repository.class.getName()); + if (name == null) { + name = Repository.class.getName(); + } + + ServletContext context = servlet.getServletContext(); + Object repository = context.getAttribute(name); + if (repository instanceof Repository) { + return (Repository) repository; + } else if (repository != null) { + throw new RepositoryException( + "Invalid repository: Attribute " + name + + " in servlet context " + context.getServletContextName() + + " is an instance of " + repository.getClass().getName()); + } else { + throw new RepositoryException( + "Repository not found: Attribute " + name + + " does not exist in servlet context " + + context.getServletContextName()); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/jackrabbit/JackrabbitRepositoryServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/jackrabbit/JackrabbitRepositoryServlet.java new file mode 100644 index 00000000000..06c5997535d --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/jackrabbit/JackrabbitRepositoryServlet.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.servlet.jackrabbit; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.servlet.ServletException; + +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.servlet.AbstractRepositoryServlet; + +/** + * Servlet that makes a Jackrabbit repository available as a servlet context + * attribute. The repository is started during servlet initialization and + * shut down when the servlet is destroyed. + *

        + * The supported initialization parameters of this servlet are: + *

        + *
        org.apache.jackrabbit.core.RepositoryContext
        + *
        + * Name of the servlet context attribute to put the internal + * component context of the repository in. The default value is + * "org.apache.jackrabbit.core.RepositoryContext". + *
        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute to put the repository in. + * The default value is "javax.jcr.Repository". + *
        + *
        repository.home
        + *
        + * Path of the repository home directory. The default value is + * "jackrabbit-repository". The home directory is + * automatically created during servlet initialization if it does + * not already exist. + *
        + *
        repository.config
        + *
        + * Path of the repository configuration file. The default value is + * "repository.xml" within the configured repository home + * directory. A standard configuration file is automatically copied to + * the configured location during servlet initialization if the file + * does not already exist. + *
        + *
        + *

        + * The repository servlet can also be mapped to the URL space. See + * {@link AbstractRepositoryServlet} for the details. + * + * @since 1.4 + */ +public class JackrabbitRepositoryServlet extends AbstractRepositoryServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 7102770011290708450L; + + /** + * Repository instance. + */ + private RepositoryContext context; + + /** + * Starts the repository instance and makes it available in the + * servlet context. + * + * @throws ServletException if the repository can not be started + */ + public void init() throws ServletException { + try { + File home = new File(getInitParameter( + "repository.home", "jackrabbit-repository")); + if (!home.exists()) { + log("Creating repository home directory: " + home); + home.mkdirs(); + } + + File config = new File(getInitParameter( + "repository.config", + new File(home, "repository.xml").getPath())); + if (!config.exists()) { + log("Creating default repository configuration: " + config); + createDefaultConfiguration(config); + } + + context = RepositoryContext.create(RepositoryConfig.create( + config.toURI(), home.getPath())); + + String name = RepositoryContext.class.getName(); + getServletContext().setAttribute( + getInitParameter(name, name), context); + } catch (RepositoryException e) { + throw new ServletException("Failed to start Jackrabbit", e); + } + + super.init(); + } + + /** + * Removes the repository from the servlet context and shuts it down. + */ + public void destroy() { + super.destroy(); + context.getRepository().shutdown(); + } + + /** + * Returns the configured repository instance. + * + * @return repository + */ + @Override + protected Repository getRepository() { + return context.getRepository(); + } + + /** + * Copies the default repository configuration file to the given location. + * + * @param config path of the configuration file + * @throws ServletException if the configuration file could not be copied + */ + private void createDefaultConfiguration(File config) + throws ServletException { + try { + OutputStream output = new FileOutputStream(config); + try { + InputStream input = + RepositoryImpl.class.getResourceAsStream("repository.xml"); + try { + byte[] buffer = new byte[8192]; + int n = input.read(buffer); + while (n != -1) { + output.write(buffer, 0, n); + n = input.read(buffer); + } + } finally { + input.close(); + } + } finally { + output.close(); + } + } catch (IOException e) { + throw new ServletException( + "Failed to copy default configuration: " + config, e); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/jackrabbit/StatisticsServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/jackrabbit/StatisticsServlet.java new file mode 100644 index 00000000000..2077ed77fef --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/jackrabbit/StatisticsServlet.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.servlet.jackrabbit; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.jackrabbit.api.stats.RepositoryStatistics; +import org.apache.jackrabbit.api.stats.TimeSeries; +import org.apache.jackrabbit.core.RepositoryContext; + +/** + * Servlet that makes Jackrabbit repository statistics available as + * a JSON object. + * + * @since Apache Jackrabbit 2.3.1 + */ +public class StatisticsServlet extends HttpServlet { + + /** Serial version UID */ + private static final long serialVersionUID = -7494195499389135951L; + + @Override + protected void doGet( + HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String klass = RepositoryContext.class.getName(); + String name = getServletConfig().getInitParameter(klass); + if (name == null) { + name = klass; + } + + RepositoryContext context = (RepositoryContext) + getServletContext().getAttribute(name); + if (context != null) { + RepositoryStatistics statistics = context.getRepositoryStatistics(); + response.setContentType("application/json"); + Writer writer = response.getWriter(); + writer.write('{'); + write(writer, "read", statistics.getTimeSeries( + RepositoryStatistics.Type.SESSION_READ_COUNTER)); + writer.write(','); + write(writer, "write", statistics.getTimeSeries( + RepositoryStatistics.Type.SESSION_WRITE_COUNTER)); + writer.write(','); + write(writer, "login", statistics.getTimeSeries( + RepositoryStatistics.Type.SESSION_LOGIN_COUNTER)); + writer.write('}'); + } else { + response.sendError( + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Jackrabbit repository internals are not available"); + } + } + + private void write(Writer writer, String name, TimeSeries series) + throws IOException { + writer.write('"'); + writer.write(name); + writer.write('"'); + writer.write(':'); + writer.write('{'); + write(writer, "second", series.getValuePerSecond()); + writer.write(','); + write(writer, "minute", series.getValuePerMinute()); + writer.write(','); + write(writer, "hour", series.getValuePerHour()); + writer.write(','); + write(writer, "week", series.getValuePerWeek()); + writer.write('}'); + } + + private void write(Writer writer, String name, long[] values) + throws IOException { + writer.write('"'); + writer.write(name); + writer.write('"'); + writer.write(':'); + writer.write('['); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + writer.write(','); + } + writer.write(String.valueOf(values[i])); + } + writer.write(']'); + } +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/AbstractLoginFilter.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/AbstractLoginFilter.java new file mode 100644 index 00000000000..2da62cb9909 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/AbstractLoginFilter.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.servlet.login; + +import java.io.IOException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.jackrabbit.servlet.ServletRepository; + +/** + * + * @since Apache Jackrabbit 1.6 + */ +public abstract class AbstractLoginFilter implements Filter { + + private Repository repository; + + private String workspace; + + private String sessionAttribute; + + private String nodeAttribute; + + public void init(FilterConfig config) { + repository = new ServletRepository(config); + workspace = config.getInitParameter("workspace"); + + sessionAttribute = config.getInitParameter(Session.class.getName()); + if (sessionAttribute == null) { + sessionAttribute = Session.class.getName(); + } + + nodeAttribute = config.getInitParameter(Node.class.getName()); + if (nodeAttribute == null) { + nodeAttribute = Node.class.getName(); + } + } + + public void destroy() { + } + + public void doFilter( + ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + try { + Credentials credentials = getCredentials(httpRequest); + Session session = repository.login(credentials, workspace); + try { + request.setAttribute(sessionAttribute, session); + request.setAttribute(nodeAttribute, session.getRootNode()); + chain.doFilter(request, response); + if (session.hasPendingChanges()) { + session.save(); + } + } finally { + session.logout(); + } + } catch (ServletException e) { + Throwable cause = e.getRootCause(); + if (cause instanceof AccessDeniedException) { + httpResponse.sendError( + HttpServletResponse.SC_FORBIDDEN, cause.getMessage()); + } else { + throw e; + } + } catch (LoginException e) { + httpResponse.sendError( + HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); + } catch (NoSuchWorkspaceException e) { + throw new ServletException( + "Workspace " + workspace + + " not found in the content repository", e); + } catch (RepositoryException e) { + throw new ServletException( + "Unable to access the content repository", e); + } + } + + protected abstract Credentials getCredentials(HttpServletRequest request); + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/BasicLoginFilter.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/BasicLoginFilter.java new file mode 100644 index 00000000000..d4490d4caf1 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/BasicLoginFilter.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.servlet.login; + +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import javax.servlet.http.HttpServletRequest; + +/** + * + * @since Apache Jackrabbit 1.6 + */ +public class BasicLoginFilter extends AbstractLoginFilter { + + protected Credentials getCredentials(HttpServletRequest request) { + String authorization = request.getHeader("Authorization"); + if (authorization != null) { + return new SimpleCredentials("TODO", "TODO".toCharArray()); + } else { + return null; + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/ContainerLoginFilter.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/ContainerLoginFilter.java new file mode 100644 index 00000000000..b61bbe0c57b --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/ContainerLoginFilter.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.servlet.login; + +import javax.jcr.Credentials; +import javax.jcr.SimpleCredentials; +import javax.servlet.FilterConfig; +import javax.servlet.http.HttpServletRequest; + +/** + * Login filter that relies on container authentication to provide the + * authenticated username of a request. This username is associated with + * a dummy password (empty by default, configurable through the init + * parameter "password") in a {@link SimpleCredentials} object that is + * used to log in to the underlying content repository. If no authenticated + * user is found, then null credentials are used. + *

        + * It is expected that the underlying repository is configured to simply + * trust the given username. If the same repository is also made available + * for direct logins, then a special secret password that allows logins with + * any username could be configured just for this filter. + * + * @since Apache Jackrabbit 1.6 + */ +public class ContainerLoginFilter extends AbstractLoginFilter { + + /** + * The dummy password used for the repository login. Empty by default. + */ + private char[] password = new char[0]; + + public void init(FilterConfig config) { + super.init(config); + + String password = config.getInitParameter("password"); + if (password != null) { + this.password = password.toCharArray(); + } + } + + protected Credentials getCredentials(HttpServletRequest request) { + String user = request.getRemoteUser(); + if (user != null) { + return new SimpleCredentials(user, password); + } else { + return null; + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/NullLoginFilter.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/NullLoginFilter.java new file mode 100644 index 00000000000..14f29fa3f11 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/login/NullLoginFilter.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.servlet.login; + +import javax.jcr.Credentials; +import javax.servlet.http.HttpServletRequest; + +/** + * Login filter that always uses null credentials for logging in + * to the content repository. This is useful for example for public web sites + * where all repository access is performed using anonymous sessions. Another + * use case for this login filter is when login information is made available + * to the content repository through JAAS or some other out-of-band mechanism. + * + * @since Apache Jackrabbit 1.6 + */ +public class NullLoginFilter extends AbstractLoginFilter { + + /** + * Always returns null. + * + * @param request ignored + * @return null credentials + */ + protected Credentials getCredentials(HttpServletRequest request) { + return null; + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/JNDIRemoteBindingServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/JNDIRemoteBindingServlet.java new file mode 100644 index 00000000000..5d21f1d6f89 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/JNDIRemoteBindingServlet.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.servlet.remote; + +import java.util.Enumeration; +import java.util.Hashtable; + +import javax.jcr.Repository; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletException; + +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; + +/** + * Servlet that binds a repository from a servlet context attribute to JNDI + * as a remote repository reference. + *

        + * The initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute that contains the repository. + * The default value is "javax.jcr.Repository". + *
        + *
        org.apache.jackrabbit.rmi.server.RemoteAdapterFactory
        + *
        + * Name of the remote adapter factory class used to create the remote + * repository reference. The configured class should have public + * constructor that takes no arguments. + *
        + *
        location
        + *
        + * Location where to bind the repository in the JNDI directory. + * The default value is + * "org/apache/jackrabbit/rmi/remote/RemoteRepository". + *
        + *
        *
        + *
        + * All other init parameters are used as the JNDI environment when + * instantiating {@link InitialContext} for binding up the repository. + *
        + *
        + * + * @since 1.4 + */ +public class JNDIRemoteBindingServlet extends RemoteBindingServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = -7984144838866544543L; + + /** + * JNDI context to which to bind the repository. + */ + private Context context; + + /** + * Location of the repository within the JNDI context. + */ + private String location = + RemoteRepository.class.getName().replace('.', '/'); + + /** + * Binds a repository from the servlet context in the configured RMI URL. + * + * @throws ServletException if the repository could not be bound in RMI + */ + public void init() throws ServletException { + try { + Hashtable environment = new Hashtable(); + Enumeration names = getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + if (name.equals("location")) { + location = getInitParameter(name); + } else if (!name.equals(Repository.class.getName()) + && !name.equals(RemoteAdapterFactory.class.getName())) { + environment.put(name, getInitParameter(name)); + } + } + context = new InitialContext(environment); + context.bind(location, getRemoteRepository()); + } catch (NamingException e) { + throw new ServletException( + "Failed to bind remote repository to JNDI: " + location, e); + } + } + + /** + * Unbinds the remote repository from JNDI. + */ + public void destroy() { + try { + context.unbind(location); + } catch (NamingException e) { + log("Failed to unbind remote repository from JNDI: " + location, e); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/JNDIRemoteRepositoryServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/JNDIRemoteRepositoryServlet.java new file mode 100644 index 00000000000..0b62965bcfd --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/JNDIRemoteRepositoryServlet.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.servlet.remote; + +import java.util.Enumeration; +import java.util.Hashtable; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletException; + +import org.apache.jackrabbit.commons.repository.RepositoryFactory; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.repository.JNDIRemoteRepositoryFactory; +import org.apache.jackrabbit.servlet.AbstractRepositoryServlet; + +/** + * Servlet that makes a remote repository from JNDI available as an attribute + * in the servlet context. + *

        + * The supported initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute to put the repository in. + * The default value is "javax.jcr.Repository". + *
        + *
        org.apache.jackrabbit.rmi.client.LocalAdapterFactory
        + *
        + * Name of the local adapter factory class used to create the local + * adapter for the remote repository. The configured class should have + * public constructor that takes no arguments. + *
        + *
        location
        + *
        + * Location of the remote repository in the JNDI directory. + * The default value is + * "org/apache/jackrabbit/rmi/remote/RemoteRepository". + *
        + *
        *
        + *
        + * All other init parameters are used as the JNDI environment when + * instantiating {@link InitialContext} for looking up the repository. + *
        + *
        + *

        + * This servlet can also be mapped to the URL space. See + * {@link AbstractRepositoryServlet} for the details. + * + * @since 1.4 + */ +public class JNDIRemoteRepositoryServlet extends RemoteRepositoryServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 9029928193416404478L; + + /** + * Returns the remote repository in the configured JNDI location. + * + * @return repository + * @throws RepositoryException if the repository could not be accessed + */ + @Override + protected Repository getRepository() throws RepositoryException { + String location = + "//localhost/" + RemoteRepository.class.getName().replace('.', '/'); + try { + Hashtable environment = new Hashtable(); + Enumeration names = getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + if (name.equals("location")) { + location = getInitParameter(name); + } else if (!name.equals(Repository.class.getName()) + && !name.equals(LocalAdapterFactory.class.getName())) { + environment.put(name, getInitParameter(name)); + } + } + return new JNDIRemoteRepositoryFactory( + getLocalAdapterFactory(), + new InitialContext(environment), location).getRepository(); + } catch (NamingException e) { + throw new RepositoryException( + "Repository not found: Invalid JNDI context", e); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RMIRemoteBindingServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RMIRemoteBindingServlet.java new file mode 100644 index 00000000000..fe591a36a23 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RMIRemoteBindingServlet.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.servlet.remote; + +import java.net.MalformedURLException; +import java.rmi.Naming; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; + +import javax.servlet.ServletException; + +/** + * Servlet that binds a repository from a servlet context attribute in RMI. + *

        + * The initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute that contains the repository. + * The default value is "javax.jcr.Repository". + *
        + *
        org.apache.jackrabbit.rmi.server.RemoteAdapterFactory
        + *
        + * Name of the remote adapter factory class used to create the remote + * repository reference. The configured class should have public + * constructor that takes no arguments. + *
        + *
        url
        + *
        + * RMI URL where to bind the repository in. The default value is + * "//localhost/javax/jcr/Repository". + *
        + *
        + * + * @since 1.4 + */ +public class RMIRemoteBindingServlet extends RemoteBindingServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 1627580747678104906L; + + /** + * Location of the repository within the JNDI context. + */ + private String url; + + /** + * Binds a repository from the servlet context in the configured RMI URL. + * + * @throws ServletException if the repository could not be bound in RMI + */ + public void init() throws ServletException { + url = getInitParameter("url"); + if (url == null) { + url = "//localhost/javax/jcr/Repository"; + } + try { + Naming.rebind(url, getRemoteRepository()); + } catch (MalformedURLException e) { + log("Invalid RMI URL: " + url, e); + } catch (RemoteException e) { + log("Failed to bind repository to RMI: " + url, e); + } + } + + /** + * Unbinds the repository from RMI. + */ + public void destroy() { + try { + Naming.unbind(url); + } catch (NotBoundException e) { + // Ignore, perhaps the reference was already manually removed + } catch (MalformedURLException e) { + // Ignore, we already logged a warning about this during init() + } catch (RemoteException e) { + log("Failed to unbind repository from RMI: " + url, e); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RMIRemoteRepositoryServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RMIRemoteRepositoryServlet.java new file mode 100644 index 00000000000..292a0c1d58e --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RMIRemoteRepositoryServlet.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.servlet.remote; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.servlet.ServletException; + +import org.apache.jackrabbit.rmi.repository.RMIRemoteRepositoryFactory; +import org.apache.jackrabbit.servlet.AbstractRepositoryServlet; + +/** + * Servlet that makes a repository from RMI available as an attribute + * in the servlet context. + *

        + * The supported initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute to put the repository in. + * The default value is "javax.jcr.Repository". + *
        + *
        org.apache.jackrabbit.rmi.client.LocalAdapterFactory
        + *
        + * Name of the local adapter factory class used to create the local + * adapter for the remote repository. The configured class should have + * public constructor that takes no arguments. + *
        + *
        url
        + *
        + * RMI URL of the remote repository. The default value is + * "//localhost/javax/jcr/Repository". + *
        + *
        + *

        + * This servlet can also be mapped to the URL space. See + * {@link AbstractRepositoryServlet} for the details. + * + * @since 1.4 + */ +public class RMIRemoteRepositoryServlet extends RemoteRepositoryServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 2410543206806054854L; + + /** + * Creates and returns an RMI repository factory for the configured RMI URL. + * + * @return RMI repository factory + * @throws RepositoryException if the factory could not be created + */ + @Override + protected Repository getRepository() throws RepositoryException { + return new RMIRemoteRepositoryFactory( + getLocalAdapterFactory(), + getInitParameter("url", "//localhost/javax/jcr/Repository") + ).getRepository(); + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RemoteBindingServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RemoteBindingServlet.java new file mode 100644 index 00000000000..77bca0b5d52 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RemoteBindingServlet.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.servlet.remote; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.rmi.RemoteException; +import java.rmi.server.RemoteObject; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerAdapterFactory; +import org.apache.jackrabbit.servlet.ServletRepository; + +/** + * Servlet that makes a repository in servlet context available as a remote + * repository reference. By default this servlet makes the serialized + * reference available through HTTP GET, but subclasses can extend this + * behavior to bind the remote reference to various locations like JNDI + * or the RMI registry. + *

        + * The initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute that contains the repository. + * The default value is "javax.jcr.Repository". + *
        + *
        org.apache.jackrabbit.rmi.server.RemoteAdapterFactory
        + *
        + * Name of the remote adapter factory class used to create the remote + * repository reference. The configured class should have public + * constructor that takes no arguments. + *
        + *
        + * + * @since 1.4 + */ +public class RemoteBindingServlet extends HttpServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = -162482284843619248L; + + /** + * Remote repository. + */ + private RemoteRepository remote; + + /** + * Returns the configured remote repository reference. The remote + * repository is instantiated and memorized during the first call to + * this method. + * + * @return remote repository + * @throws ServletException if the repository could not be instantiated + */ + protected RemoteRepository getRemoteRepository() throws ServletException { + if (remote == null) { + try { + RemoteAdapterFactory factory = getRemoteAdapterFactory(); + remote = factory.getRemoteRepository(new ServletRepository(this)); + } catch (RemoteException e) { + throw new ServletException( + "Failed to create the remote repository reference", e); + } + } + return remote; + } + + /** + * Instantiates and returns the configured remote adapter factory. + * + * @return remote adapter factory + * @throws ServletException if the factory could not be instantiated + */ + private RemoteAdapterFactory getRemoteAdapterFactory() + throws ServletException { + String name = getInitParameter(RemoteAdapterFactory.class.getName()); + if (name == null) { + return new ServerAdapterFactory(); + } + try { + Class factoryClass = Class.forName(name); + return (RemoteAdapterFactory) factoryClass.newInstance(); + } catch (ClassNotFoundException e) { + throw new ServletException( + "Remote adapter factory class not found: " + name, e); + } catch (InstantiationException e) { + throw new ServletException( + "Failed to instantiate the adapter factory: " + name, e); + } catch (IllegalAccessException e) { + throw new ServletException( + "Adapter factory constructor is not public: " + name, e); + } catch (ClassCastException e) { + throw new ServletException( + "Invalid remote adapter factory class: " + name, e); + } + } + + /** + * Outputs the remote repository reference as a serialized stream. + * + * @param request HTTP request + * @param response HTTP response + * @throws ServletException if the remote reference is not available + * @throws IOException on IO errors + */ + protected void doGet( + HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("application/octet-stream"); + ObjectOutputStream output = + new ObjectOutputStream(response.getOutputStream()); + output.writeObject(RemoteObject.toStub(getRemoteRepository())); + output.flush(); + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RemoteRepositoryServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RemoteRepositoryServlet.java new file mode 100644 index 00000000000..d219cae55cb --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/RemoteRepositoryServlet.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.servlet.remote; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; +import org.apache.jackrabbit.rmi.client.LocalAdapterFactory; +import org.apache.jackrabbit.servlet.AbstractRepositoryServlet; + +/** + * Abstract base class for servlets that make a remote repository available + * locally in the servlet context. + *

        + * The supported initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute to put the repository in. + * The default value is "javax.jcr.Repository". + *
        + *
        org.apache.jackrabbit.rmi.client.LocalAdapterFactory
        + *
        + * Name of the local adapter factory class used to create the local + * adapter for the remote repository. The configured class should have + * public constructor that takes no arguments. + *
        + *
        + *

        + * This servlet can also be mapped to the URL space. See + * {@link AbstractRepositoryServlet} for the details. + * + * @since 1.4 + */ +public abstract class RemoteRepositoryServlet + extends AbstractRepositoryServlet { + + /** + * Instantiates and returns the configured local adapter factory. + * + * @return local adapter factory + * @throws RepositoryException if the factory could not be instantiated + */ + protected LocalAdapterFactory getLocalAdapterFactory() + throws RepositoryException { + String name = getInitParameter( + LocalAdapterFactory.class.getName(), + ClientAdapterFactory.class.getName()); + try { + Class factoryClass = Class.forName(name); + return (LocalAdapterFactory) factoryClass.newInstance(); + } catch (ClassNotFoundException e) { + throw new RepositoryException( + "Local adapter factory class not found: " + name, e); + } catch (InstantiationException e) { + throw new RepositoryException( + "Failed to instantiate the adapter factory: " + name, e); + } catch (IllegalAccessException e) { + throw new RepositoryException( + "Adapter factory constructor is not public: " + name, e); + } catch (ClassCastException e) { + throw new RepositoryException( + "Invalid local adapter factory class: " + name, e); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/URLRemoteBindingServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/URLRemoteBindingServlet.java new file mode 100644 index 00000000000..8a3f395d184 --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/URLRemoteBindingServlet.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.servlet.remote; + +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.servlet.ServletException; + +/** + * Servlet that writes the remote reference of a repository in the servlet + * context to the configured URL. + *

        + * The initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute that contains the repository. + * The default value is "javax.jcr.Repository". + *
        + *
        org.apache.jackrabbit.rmi.server.RemoteAdapterFactory
        + *
        + * Name of the remote adapter factory class used to create the remote + * repository reference. The configured class should have public + * constructor that takes no arguments. + *
        + *
        url
        + *
        + * URL where to store the remote repository reference. + *
        + *
        + * + * @since 1.4 + */ +public class URLRemoteBindingServlet extends RemoteBindingServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 3187755583290121129L; + + /** + * Writes the remote reference of a repository in the servlet context + * to the configured URL. + * + * @throws ServletException if the URL could not be written to + */ + public void init() throws ServletException { + String url = getInitParameter("url"); + if (url == null) { + throw new ServletException("Missing init parameter: url"); + } + try { + ObjectOutputStream output = new ObjectOutputStream( + new URL(url).openConnection().getOutputStream()); + try { + output.writeObject(getRemoteRepository()); + } finally { + output.close(); + } + } catch (MalformedURLException e) { + throw new ServletException("Malformed URL: " + url, e); + } catch (IOException e) { + throw new ServletException("Failed to write to URL: " + url, e); + } + } + +} diff --git a/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/URLRemoteRepositoryServlet.java b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/URLRemoteRepositoryServlet.java new file mode 100644 index 00000000000..d4a6dba535d --- /dev/null +++ b/jackrabbit-jcr-servlet/src/main/java/org/apache/jackrabbit/servlet/remote/URLRemoteRepositoryServlet.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.servlet.remote; + +import java.net.MalformedURLException; +import java.net.URL; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.rmi.repository.URLRemoteRepositoryFactory; +import org.apache.jackrabbit.servlet.AbstractRepositoryServlet; + +/** + * Servlet that makes a remote repository from a ULR available as an attribute + * in the servlet context. + *

        + * The supported initialization parameters of this servlet are: + *

        + *
        javax.jcr.Repository
        + *
        + * Name of the servlet context attribute to put the repository in. + * The default value is "javax.jcr.Repository". + *
        + *
        org.apache.jackrabbit.rmi.client.LocalAdapterFactory
        + *
        + * Name of the local adapter factory class used to create the local + * adapter for the remote repository. The configured class should have + * public constructor that takes no arguments. + *
        + *
        url
        + *
        + * URL of the remote repository. + *
        + *
        + *

        + * This servlet can also be mapped to the URL space. See + * {@link AbstractRepositoryServlet} for the details. + * + * @since 1.4 + */ +public class URLRemoteRepositoryServlet extends RemoteRepositoryServlet { + + /** + * Serial version UID. + */ + private static final long serialVersionUID = 6144781813459102448L; + + /** + * Returns the remote repository at the given URL. + * + * @return repository + * @throws RepositoryException if the repository could not be accessed + */ + @Override + protected Repository getRepository() throws RepositoryException { + String url = getInitParameter("url"); + if (url == null) { + throw new RepositoryException("Missing init parameter: url"); + } + + try { + return new URLRemoteRepositoryFactory( + getLocalAdapterFactory(), new URL(url)).getRepository(); + } catch (MalformedURLException e) { + throw new RepositoryException("Invalid repository URL: " + url, e); + } + } + +} diff --git a/jackrabbit-jcr-tests/README.txt b/jackrabbit-jcr-tests/README.txt new file mode 100644 index 00000000000..8732ac7bd7e --- /dev/null +++ b/jackrabbit-jcr-tests/README.txt @@ -0,0 +1,10 @@ +=============================== +Welcome to Jackrabbit JCR Tests +=============================== + +This is the JCR Tests component of the Apache Jackrabbit project. +This component contains a set of JCR API test cases designed for +testing the compliance of an implementation. Note that although +this code base is used also for the official JCR TCK, this is not +the official TCK and passing the test of this component can not +be used as an indication of API compliance. diff --git a/jackrabbit-jcr-tests/pom.xml b/jackrabbit-jcr-tests/pom.xml new file mode 100644 index 00000000000..8d74dfc7c98 --- /dev/null +++ b/jackrabbit-jcr-tests/pom.xml @@ -0,0 +1,71 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-jcr-tests + Jackrabbit JCR Tests + JCR API test cases + + + + + org.apache.rat + apache-rat-plugin + + + **/nodetype/spec/*.txt + *.log + .checkstyle + + + + + + + + + javax.jcr + jcr + + + junit + junit + + + org.slf4j + slf4j-api + + + concurrent + concurrent + + + + diff --git a/jackrabbit-jcr-tests/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-jcr-tests/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/AbstractJCRTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/AbstractJCRTest.java new file mode 100644 index 00000000000..79317034f27 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/AbstractJCRTest.java @@ -0,0 +1,916 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test; + +import junit.framework.TestResult; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.Session; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Repository; +import javax.jcr.NamespaceException; +import javax.jcr.RangeIterator; +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.ConstraintViolationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.retention.RetentionManager; + +import java.util.StringTokenizer; +import java.util.Random; +import java.util.List; +import java.util.Arrays; + +/** + * Abstract base class for all JCR test classes. + */ +public abstract class AbstractJCRTest extends JUnitTest { + + /** + * Pool of helper objects to access repository transparently + */ + private static final RepositoryHelperPool HELPER_POOL = RepositoryHelperPoolImpl.getInstance(); + + /** + * Namespace URI for jcr prefix. + */ + public static final String NS_JCR_URI = "http://www.jcp.org/jcr/1.0"; + + /** + * Namespace URI for nt prefix. + */ + public static final String NS_NT_URI = "http://www.jcp.org/jcr/nt/1.0"; + + /** + * Namespace URI for mix prefix. + */ + public static final String NS_MIX_URI = "http://www.jcp.org/jcr/mix/1.0"; + + /** + * Namespace URI for sv prefix + */ + public static final String NS_SV_URI = "http://www.jcp.org/jcr/sv/1.0"; + + /** + * The repository helper for this test. + */ + private RepositoryHelper helper; + + /** + * JCR Name jcr:primaryType using the namespace resolver of the current session. + */ + protected String jcrPrimaryType; + + /** + * JCR Name jcr:mixinTypes using the namespace resolver of the current session. + */ + protected String jcrMixinTypes; + + /** + * JCR Name jcr:predecessors using the namespace resolver of the current session. + */ + protected String jcrPredecessors; + + /** + * JCR Name jcr:successors using the namespace resolver of the current session. + */ + protected String jcrSuccessors; + + /** + * JCR Name jcr:created using the namespace resolver of the current session. + */ + protected String jcrCreated; + + /** + * JCR Name jcr:created using the namespace resolver of the current session. + */ + protected String jcrVersionHistory; + + /** + * JCR Name jcr:copiedFrom using the namespace resolver of the current session. + */ + protected String jcrCopiedFrom; + + /** + * JCR Name jcr:frozenNode using the namespace resolver of the current session. + */ + protected String jcrFrozenNode; + + /** + * JCR Name jcr:frozenUuid using the namespace resolver of the current session. + */ + protected String jcrFrozenUuid; + + /** + * JCR Name jcr:rootVersion using the namespace resolver of the current session. + */ + protected String jcrRootVersion; + + /** + * JCR Name jcr:isCheckedOut using the namespace resolver of the current session. + */ + protected String jcrIsCheckedOut; + + /** + * JCR Name jcr:baseVersion using the namespace resolver of the current session. + */ + protected String jcrBaseVersion; + + /** + * JCR Name jcr:uuid using the namespace resolver of the current session. + */ + protected String jcrUUID; + + /** + * JCR Name jcr:lockOwner using the namespace resolver of the current session. + */ + protected String jcrLockOwner; + + /** + * JCR Name jcr:lockIsDeep using the namespace resolver of the current session. + */ + protected String jcrlockIsDeep; + + /** + * JCR Name jcr:mergeFailed using the namespace resolver of the current session. + */ + protected String jcrMergeFailed; + + /** + * JCR Name jcr:system using the namespace resolver of the current session. + */ + protected String jcrSystem; + + /** + * JCR Name nt:base using the namespace resolver of the current session. + */ + protected String ntBase; + + /** + * JCR Name nt:unstructured using the namespace resolver of the current session. + */ + protected String ntUnstructured; + + /** + * JCR Name nt:version using the namespace resolver of the current session. + */ + protected String ntVersion; + + /** + * JCR Name nt:versionHistory using the namespace resolver of the current session. + */ + protected String ntVersionHistory; + + /** + * JCR Name nt:versionHistory using the namespace resolver of the current session. + */ + protected String ntVersionLabels; + + /** + * JCR Name nt:frozenNode using the namespace resolver of the current session. + */ + protected String ntFrozenNode; + + /** + * JCR Name mix:referenceable using the namespace resolver of the current session. + */ + protected String mixReferenceable; + + /** + * JCR Name mix:versionable using the namespace resolver of the current session. + */ + protected String mixVersionable; + + /** + * JCR Name mix:simpleVersionable using the namespace resolver of the current session. + */ + protected String mixSimpleVersionable; + + /** + * JCR Name mix:lockable using the namespace resolver of the current session. + */ + protected String mixLockable; + + /** + * JCR Name mix:title using the namespace resolver of the current session. + */ + protected String mixTitle; + + /** + * JCR Name mix:shareable using the namespace resolver of the current session. + */ + protected String mixShareable; + + /** + * JCR Name nt:query using the namespace resolver of the current session. + */ + protected String ntQuery; + + /** + * Relative path to the test root node. + */ + protected String testPath; + + /** + * Absolute path to the test root node. + */ + protected String testRoot; + + /** + * The node type name for newly created nodes. + */ + protected String testNodeType; + + /** + * The node type name for the test root node. + */ + protected String testNodeTypeTestRoot; + + /** + * A node type that does not allow any child nodes, such as nt:base. + */ + protected String testNodeTypeNoChildren; + + /** + * Name of a node that will be created during a test case. + */ + protected String nodeName1; + + /** + * Name of a node that will be created during a test case. + */ + protected String nodeName2; + + /** + * Name of a node that will be created during a test case. + */ + protected String nodeName3; + + /** + * Name of a node that will be created during a test case. + */ + protected String nodeName4; + + /** + * Name of a property that will be used during a test case. + */ + protected String propertyName1; + + /** + * Name of a property that will be used during a test case. + */ + protected String propertyName2; + + /** + * Name of a workspace to use instead of the default workspace. + */ + protected String workspaceName; + + /** + * The superuser session for the default workspace + */ + protected Session superuser; + + /** + * Flag that indicates if the current test is a read-only test, that is + * no content is written to the workspace by the test. + */ + protected boolean isReadOnly = false; + + /** + * The root Node for testing + */ + protected Node testRootNode; + + /** + * The value factory for {@link #superuser}. + */ + protected ValueFactory vf; + + protected void setUp() throws Exception { + super.setUp(); + testRoot = getProperty(RepositoryStub.PROP_TESTROOT); + if (testRoot == null) { + fail("Property '" + RepositoryStub.PROP_TESTROOT + "' is not defined."); + } + + // cut off '/' to build testPath + testPath = testRoot.substring(1); + testNodeType = getProperty(RepositoryStub.PROP_NODETYPE); + testNodeTypeTestRoot = getProperty(RepositoryStub.PROP_NODETYPETESTROOT); + if (testNodeTypeTestRoot == null) { + testNodeTypeTestRoot = testNodeType; // backwards compatibility + } + testNodeTypeNoChildren = getProperty(RepositoryStub.PROP_NODETYPENOCHILDREN); + // setup node names + nodeName1 = getProperty(RepositoryStub.PROP_NODE_NAME1); + if (nodeName1 == null) { + fail("Property '" + RepositoryStub.PROP_NODE_NAME1 + "' is not defined."); + } + nodeName2 = getProperty(RepositoryStub.PROP_NODE_NAME2); + if (nodeName2 == null) { + fail("Property '" + RepositoryStub.PROP_NODE_NAME2 + "' is not defined."); + } + nodeName3 = getProperty(RepositoryStub.PROP_NODE_NAME3); + if (nodeName3 == null) { + fail("Property '" + RepositoryStub.PROP_NODE_NAME3 + "' is not defined."); + } + nodeName4 = getProperty(RepositoryStub.PROP_NODE_NAME4); + if (nodeName4 == null) { + fail("Property '" + RepositoryStub.PROP_NODE_NAME4 + "' is not defined."); + } + propertyName1 = getProperty(RepositoryStub.PROP_PROP_NAME1); + if (propertyName1 == null) { + fail("Property '" + RepositoryStub.PROP_PROP_NAME1 + "' is not defined."); + } + propertyName2 = getProperty(RepositoryStub.PROP_PROP_NAME2); + if (propertyName2 == null) { + fail("Property '" + RepositoryStub.PROP_PROP_NAME2 + "' is not defined."); + } + workspaceName = getProperty(RepositoryStub.PROP_WORKSPACE_NAME); + if (workspaceName == null) { + fail("Property '" + RepositoryStub.PROP_WORKSPACE_NAME + "' is not defined."); + } + + superuser = getHelper().getSuperuserSession(); + + // setup some common names + jcrPrimaryType = superuser.getNamespacePrefix(NS_JCR_URI) + ":primaryType"; + jcrMixinTypes = superuser.getNamespacePrefix(NS_JCR_URI) + ":mixinTypes"; + jcrPredecessors = superuser.getNamespacePrefix(NS_JCR_URI) + ":predecessors"; + jcrSuccessors = superuser.getNamespacePrefix(NS_JCR_URI) + ":successors"; + jcrCreated = superuser.getNamespacePrefix(NS_JCR_URI) + ":created"; + jcrVersionHistory = superuser.getNamespacePrefix(NS_JCR_URI) + ":versionHistory"; + jcrCopiedFrom = superuser.getNamespacePrefix(NS_JCR_URI) + ":copiedFrom"; + jcrFrozenNode = superuser.getNamespacePrefix(NS_JCR_URI) + ":frozenNode"; + jcrFrozenUuid = superuser.getNamespacePrefix(NS_JCR_URI) + ":frozenUuid"; + jcrRootVersion = superuser.getNamespacePrefix(NS_JCR_URI) + ":rootVersion"; + jcrBaseVersion = superuser.getNamespacePrefix(NS_JCR_URI) + ":baseVersion"; + jcrIsCheckedOut = superuser.getNamespacePrefix(NS_JCR_URI) + ":isCheckedOut"; + jcrUUID = superuser.getNamespacePrefix(NS_JCR_URI) + ":uuid"; + jcrLockOwner = superuser.getNamespacePrefix(NS_JCR_URI) + ":lockOwner"; + jcrlockIsDeep = superuser.getNamespacePrefix(NS_JCR_URI) + ":lockIsDeep"; + jcrMergeFailed = superuser.getNamespacePrefix(NS_JCR_URI) + ":mergeFailed"; + jcrSystem = superuser.getNamespacePrefix(NS_JCR_URI) + ":system"; + ntBase = superuser.getNamespacePrefix(NS_NT_URI) + ":base"; + ntUnstructured = superuser.getNamespacePrefix(NS_NT_URI) + ":unstructured"; + ntVersion = superuser.getNamespacePrefix(NS_NT_URI) + ":version"; + ntVersionHistory = superuser.getNamespacePrefix(NS_NT_URI) + ":versionHistory"; + ntVersionLabels = superuser.getNamespacePrefix(NS_NT_URI) + ":versionLabels"; + ntFrozenNode = superuser.getNamespacePrefix(NS_NT_URI) + ":frozenNode"; + mixReferenceable = superuser.getNamespacePrefix(NS_MIX_URI) + ":referenceable"; + mixVersionable = superuser.getNamespacePrefix(NS_MIX_URI) + ":versionable"; + mixSimpleVersionable = superuser.getNamespacePrefix(NS_MIX_URI) + ":simpleVersionable"; + mixLockable = superuser.getNamespacePrefix(NS_MIX_URI) + ":lockable"; + mixShareable = superuser.getNamespacePrefix(NS_MIX_URI) + ":shareable"; + mixTitle = superuser.getNamespacePrefix(NS_MIX_URI) + ":title"; + ntQuery = superuser.getNamespacePrefix(NS_NT_URI) + ":query"; + + // setup custom namespaces + if (isSupported(Repository.LEVEL_2_SUPPORTED)) { + NamespaceRegistry nsReg = superuser.getWorkspace().getNamespaceRegistry(); + String namespaces = getProperty(RepositoryStub.PROP_NAMESPACES); + if (namespaces != null) { + String[] prefixes = namespaces.split(" "); + for (int i = 0; i < prefixes.length; i++) { + String uri = getProperty(RepositoryStub.PROP_NAMESPACES + "." + prefixes[i]); + if (uri != null) { + try { + nsReg.getPrefix(uri); + } catch (NamespaceException e) { + // not yet registered + nsReg.registerNamespace(prefixes[i], uri); + } + } + } + } + vf = superuser.getValueFactory(); + } + + if (isReadOnly) { + if (testPath.length() == 0) { + // test root is the root node + testRootNode = superuser.getRootNode(); + } else if (!superuser.getRootNode().hasNode(testPath)) { + cleanUp(); + fail("Workspace does not contain test data at: " + testRoot); + } else { + testRootNode = superuser.getRootNode().getNode(testPath); + } + } else if (isSupported(Repository.LEVEL_2_SUPPORTED)) { + testRootNode = cleanUpTestRoot(superuser); + // also clean second workspace + Session s = getHelper().getSuperuserSession(workspaceName); + try { + cleanUpTestRoot(s); + } finally { + s.logout(); + } + } else { + cleanUp(); + fail("Test case requires level 2 support."); + } + } + + protected void cleanUp() throws Exception { + if (superuser != null) { + try { + if (!isReadOnly && isSupported(Repository.LEVEL_2_SUPPORTED)) { + cleanUpTestRoot(superuser); + } + } catch (Exception e) { + log.println("Exception in tearDown: " + e.toString()); + } finally { + superuser.logout(); + superuser = null; + vf = null; + } + } + testRootNode = null; + } + + protected void tearDown() throws Exception { + cleanUp(); + super.tearDown(); + } + + /** + * Runs the test cases of this test class and reports the results to + * testResult. In contrast to the default implementation of + * TestCase.run() this method will suppress tests errors with + * a {@link NotExecutableException}. That is, test cases that throw this + * exception will still result as successful. + * @param testResult the test result. + */ + public void run(TestResult testResult) { + try { + helper = HELPER_POOL.borrowHelper(); + try { + super.run(new JCRTestResult(testResult, log)); + } finally { + HELPER_POOL.returnHelper(helper); + helper = null; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * @return the repository helper instance that is associated with this test. + */ + protected RepositoryHelper getHelper() { + return helper; + } + + /** + * Returns the value of the configuration property with propName. + * The sequence how configuration properties are read is the follwoing: + *

          + *
        1. javax.jcr.tck.<testClassName>.<testCaseName>.<propName>
        2. + *
        3. javax.jcr.tck.<testClassName>.<propName>
        4. + *
        5. javax.jcr.tck.<packageName>.<propName>
        6. + *
        7. javax.jcr.tck.<propName>
        8. + *
        + * Where: + *
          + *
        • <testClassName> is the name of the test class without package prefix.
        • + *
        • <testMethodName> is the name of the test method
        • + *
        • <packageName> is the name of the package of the test class. + * Example: packageName for org.apache.jackrabbit.test.api.BooleanPropertyTest: api
        • + *
        + * @param propName the propName of the configration property. + * @return the value of the property or null if the property + * does not exist. + * @throws RepositoryException if an error occurs while reading from + * the configuration. + */ + public String getProperty(String propName) throws RepositoryException { + String testCaseName = getName(); + String testClassName = getClass().getName(); + String testPackName = ""; + int idx; + if ((idx = testClassName.lastIndexOf('.')) > -1) { + testPackName = testClassName.substring(testClassName.lastIndexOf('.', idx - 1) + 1, idx); + testClassName = testClassName.substring(idx + 1); + } + + // 1) test case specific property first + String value = getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + + testClassName + "." + testCaseName + "." + propName); + if (value != null) { + return value; + } + + // 2) check test class property + value = getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + + testClassName + "." + propName); + if (value != null) { + return value; + } + + // 3) check package property + value = getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + + testPackName + "." + propName); + if (value != null) { + return value; + } + + // finally try global property + return getHelper().getProperty(RepositoryStub.PROP_PREFIX + "." + propName); + } + + /** + * Returns the value of the configuration property with specified + * name. If the property does not exist defaultValue is + * returned. + *

        + * Configuration properties are defined in the file: + * repositoryStubImpl.properties. + * + * @param name the name of the property to retrieve. + * @param defaultValue the default value if the property does not exist. + * @return the value of the property or defaultValue if non existent. + * @throws RepositoryException if the configuration file cannot be found. + */ + public String getProperty(String name, String defaultValue) throws RepositoryException { + String val = getProperty(name); + if (val == null) { + val = defaultValue; + } + return val; + } + + /** + * Create a JCR value based on the configuration. + * + * @param s + * @param valueProp Name of the config property that contains the property value. + * @param typeProp Name of the config property that contains the property type. + * If the config parameter is missing, {@link PropertyType#STRING} is used + * to create the JCR value. + * @param defaultValue Default value to be used if the config does not define + * the value property. + * @return JCR value to be used for a test. + * @throws RepositoryException + */ + public Value getJcrValue(Session s, String valueProp, String typeProp, String defaultValue) throws RepositoryException { + ValueFactory vf = s.getValueFactory(); + String val = getProperty(valueProp, defaultValue); + int type = PropertyType.valueFromName(getProperty(typeProp, PropertyType.TYPENAME_STRING)); + return vf.createValue(val, type); + } + + /** + * Returns the size of the RangeIterator it. + * Note, that the RangeIterator might get consumed, because + * {@link RangeIterator#getSize()} might return -1 (information unavailable). + * @param it a RangeIterator. + * @return the size of the iterator (number of elements). + */ + protected long getSize(RangeIterator it) { + long size = it.getSize(); + if (size != -1) { + return size; + } + size = 0; + while (it.hasNext()) { + it.next(); + size++; + } + return size; + } + + /** + * Returns the local name for the given jcrName. + * + * @param jcrName + * the name. + * @return the local name part. + */ + protected static String getLocalName(String jcrName) { + int idx = jcrName.indexOf(':'); + if (idx != -1) { + return jcrName.substring(idx + 1); + } else { + return jcrName; + } + } + + /** + * Returns the prefix for the given jcrName. + * + * @param jcrName + * the name. + * @return the prefix part (empty string when not prefixed) + */ + protected static String getPrefix(String jcrName) { + int idx = jcrName.indexOf(':'); + if (idx != -1) { + return jcrName.substring(0, idx); + } else { + return ""; + } + } + + /** + * Returns the expanded name for the given jcrName. + * + * @param jcrName + * the name. + * @return the expanded name representation + * @throws RepositoryException + * @throws NamespaceException + */ + protected static String getQualifiedName(Session session, String jcrName) throws RepositoryException { + String prefix = getPrefix(jcrName); + String namespace = session.getNamespaceURI(prefix); + String localname = getLocalName(jcrName); + return (namespace.length() > 0 ? "{" + namespace + "}" : "{}") + localname; + } + + /** + * Returns the name of a workspace that is not accessible from + * session. + * @param session the session. + * @return name of a non existing workspace. + * @throws RepositoryException if an error occurs. + */ + protected String getNonExistingWorkspaceName(Session session) throws RepositoryException { + List names = Arrays.asList(session.getWorkspace().getAccessibleWorkspaceNames()); + String nonExisting = null; + while (nonExisting == null) { + String name = createRandomString(10); + if (!names.contains(name)) { + nonExisting = name; + } + } + return nonExisting; + } + + /** + * Creates a String with a random sequence of characters + * using 'a' - 'z'. + * @param numChars number of characters. + * @return the generated String. + */ + protected String createRandomString(int numChars) { + Random rand = new Random(System.currentTimeMillis()); + StringBuffer tmp = new StringBuffer(numChars); + for (int i = 0; i < numChars; i++) { + char c = (char) (rand.nextInt(('z' + 1) - 'a') + 'a'); + tmp.append(c); + } + return tmp.toString(); + } + + /** + * Returns true if this repository support a certain optional + * feature; otherwise false is returned. If there is no + * such descriptorKey present in the repository, this method + * also returns false. + * + * @param descriptorKey the descriptor key. + * @return true if the option is supported. + * @throws RepositoryException if an error occurs. + */ + protected boolean isSupported(String descriptorKey) throws RepositoryException { + return "true".equals(getHelper().getRepository().getDescriptor(descriptorKey)); + } + + /** + * Throws a NotExecutableException if the repository does + * not support the feature identified by the given discriptorKey. + * + * @param descriptorKey the descriptor key. + * @throws RepositoryException if an error occurs. + * @throws NotExecutableException If the feature is not supported. + */ + protected void checkSupportedOption(String descriptorKey) throws RepositoryException, NotExecutableException { + String value = getHelper().getRepository().getDescriptor(descriptorKey); + if (value == null || ! Boolean.valueOf(value).booleanValue()) { + throw new NotExecutableException ( + "Repository feature not supported: " + descriptorKey); + } + } + + /** + * Checks that the repository supports multiple workspace, otherwise aborts with + * {@link NotExecutableException}. + * @throws NotExecutableException when the repository only supports a single + * workspace + */ + protected void ensureMultipleWorkspacesSupported() throws RepositoryException, NotExecutableException { + String workspacenames[] = superuser.getWorkspace().getAccessibleWorkspaceNames(); + if (workspacenames == null || workspacenames.length < 2) { + throw new NotExecutableException("This repository does not seem to support multiple workspaces."); + } + } + + /** + * Checks that the repository supports locking, otherwise aborts with + * {@link NotExecutableException}. + * @throws NotExecutableException when the repository does not support locking + */ + protected void ensureLockingSupported() throws RepositoryException, NotExecutableException { + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("This repository does not support locking."); + } + } + + private boolean canSetProperty(NodeType nodeType, String propertyName, int propertyType, boolean isMultiple) { + PropertyDefinition propDefs[] = nodeType.getPropertyDefinitions(); + + for (int i = 0; i < propDefs.length; i++) { + if (propDefs[i].getName().equals(propertyName) || propDefs[i].getName().equals("*")) { + if ((propDefs[i].getRequiredType() == propertyType || propDefs[i].getRequiredType() == PropertyType.UNDEFINED) + && propDefs[i].isMultiple() == isMultiple) { + return true; + } + } + } + + return false; + } + + private boolean canSetProperty(Node node, String propertyName, int propertyType, boolean isMultiple) throws RepositoryException { + + if (canSetProperty(node.getPrimaryNodeType(), propertyName, propertyType, isMultiple)) { + return true; + } + else { + NodeType mixins[] = node.getMixinNodeTypes(); + boolean canSetIt = false; + for (int i = 0; i < mixins.length && !canSetIt; i++) { + canSetIt |= canSetProperty(mixins[i], propertyName, propertyType, isMultiple); + } + return canSetIt; + } + } + + /** + * Checks that the repository can set the property to the required type, otherwise aborts with + * {@link NotExecutableException}. + * @throws NotExecutableException when setting the property to the required + * type is not supported + */ + protected void ensureCanSetProperty(Node node, String propertyName, int propertyType, boolean isMultiple) throws NotExecutableException, RepositoryException { + + if (! canSetProperty(node, propertyName, propertyType, isMultiple)) { + throw new NotExecutableException("configured property name " + propertyName + " can not be set on node " + node.getPath()); + } + } + + /** + * Checks that the repository can set the property to the required type, otherwise aborts with + * {@link NotExecutableException}. + * @throws NotExecutableException when setting the property to the required + * type is not supported + */ + protected void ensureCanSetProperty(Node node, String propertyName, Value value) throws NotExecutableException, RepositoryException { + ensureCanSetProperty(node, propertyName, value.getType(), false); + } + + /** + * Checks that the repository can set the property to the required type, otherwise aborts with + * {@link NotExecutableException}. + * @throws NotExecutableException when setting the property to the required + * type is not supported + */ + protected void ensureCanSetProperty(Node node, String propertyName, Value[] values) throws NotExecutableException, RepositoryException { + + int propertyType = values.length == 0 ? PropertyType.UNDEFINED : values[0].getType(); + + if (! canSetProperty(node, propertyName, propertyType, true)) { + throw new NotExecutableException("configured property name " + propertyName + " can not be set on node " + node.getPath()); + } + } + + /** + * Checks that the repository supports the specified node type, otherwise aborts with + * {@link NotExecutableException} + * @throws NotExecutableException when the specified node type is unknown + */ + protected void ensureKnowsNodeType(Session session, String nodetype) throws NotExecutableException, RepositoryException { + + try { + session.getWorkspace().getNodeTypeManager().getNodeType(nodetype); + } + catch (NoSuchNodeTypeException ex) { + throw new NotExecutableException("Repository does not support node type " + nodetype); + } + } + + /** + * Ensures that the given node is of the given mixin type. + * + * @param node a node. + * @param mixin the name of a mixin type. + * @throws NotExecutableException if the node is not of type mixin and the + * mixin cannot be added. + * @throws RepositoryException if an error occurs. + */ + protected void ensureMixinType(Node node, String mixin) + throws NotExecutableException, RepositoryException { + if (!node.isNodeType(mixin)) { + if (node.canAddMixin(mixin)) { + node.addMixin(mixin); + } else { + throw new NotExecutableException(node.getPath() + + " does not support adding " + mixin); + } + } + } + + /** + * Checks whether the node already has the specified mixin node type + */ + protected boolean needsMixin(Node node, String mixin) throws RepositoryException { + return ! node.getSession().getWorkspace().getNodeTypeManager().getNodeType(node.getPrimaryNodeType().getName()).isNodeType(mixin); + } + + /** + * Reverts any pending changes made by s and deletes any nodes + * under {@link #testRoot}. If there is no node at {@link #testRoot} then + * the necessary nodes are created. + * + * @param s the session to clean up. + * @return the {@link javax.jcr.Node} that represents the test root. + * @throws RepositoryException if an error occurs. + */ + protected Node cleanUpTestRoot(Session s) throws RepositoryException { + // do a 'rollback' + s.refresh(false); + Node root = s.getRootNode(); + Node testRootNode; + + if (root.hasNode(testPath)) { + RetentionManager rm; + try { + rm = s.getRetentionManager(); + } catch (UnsupportedRepositoryOperationException e) { + rm = null; + } + + // clean test root + testRootNode = root.getNode(testPath); + NodeIterator children = testRootNode.getNodes(); + while (children.hasNext()) { + Node child = children.nextNode(); + + // Remove retention policy if needed + String childPath = child.getPath(); + if (rm != null && rm.getRetentionPolicy(childPath) != null) { + rm.removeRetentionPolicy(childPath); + s.save(); + } + + NodeDefinition nodeDef = child.getDefinition(); + if (!nodeDef.isMandatory() && !nodeDef.isProtected()) { + // try to remove child + try { + child.remove(); + } catch (ConstraintViolationException e) { + log.println("unable to remove node: " + child.getPath()); + } + } + } + } else { + // create nodes to testPath + StringTokenizer names = new StringTokenizer(testPath, "/"); + Node currentNode = root; + while (names.hasMoreTokens()) { + String name = names.nextToken(); + if (currentNode.hasNode(name)) { + currentNode = currentNode.getNode(name); + } else { + currentNode = currentNode.addNode(name, testNodeTypeTestRoot); + } + } + testRootNode = currentNode; + } + s.save(); + return testRootNode; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/ConcurrentTestSuite.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/ConcurrentTestSuite.java new file mode 100644 index 00000000000..7738c8fad05 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/ConcurrentTestSuite.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.test; + +import junit.framework.TestSuite; +import junit.framework.TestResult; +import junit.framework.Test; +import EDU.oswego.cs.dl.util.concurrent.Executor; +import EDU.oswego.cs.dl.util.concurrent.PooledExecutor; + +/** + * ConcurrentTestSuite implements a test suite that runs tests + * with a given concurrency level using multiple threads. + */ +public class ConcurrentTestSuite extends TestSuite { + + private final Executor executor; + + private volatile int finishedTestCount; + + public ConcurrentTestSuite(String name) { + this(name, Runtime.getRuntime().availableProcessors() * 2); + } + + public ConcurrentTestSuite() { + this(null); + } + + public ConcurrentTestSuite(int numThreads) { + this(null, numThreads); + } + + public ConcurrentTestSuite(String name, int numThreads) { + super(name); + executor = new PooledExecutor(numThreads) { + { + waitWhenBlocked(); + } + }; + } + + public void run(TestResult result) { + finishedTestCount = 0; + super.run(result); + waitUntilFinished(); + } + + public void runTest(final Test test, final TestResult result) { + try { + executor.execute(new Runnable() { + public void run() { + try { + ConcurrentTestSuite.super.runTest(test, result); + } finally { + runFinished(); + } + } + }); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private synchronized void waitUntilFinished() { + while (finishedTestCount < testCount()) { + try { + wait(); + } catch (InterruptedException e) { + return; // ignore + } + } + } + + private synchronized void runFinished() { + finishedTestCount++; + notifyAll(); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/ISO8601.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/ISO8601.java new file mode 100644 index 00000000000..e418ec27d43 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/ISO8601.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * The ISO8601 utility class provides helper methods + * to deal with date/time formatting using a specific ISO8601-compliant + * format (see ISO 8601). + *

        + * The currently supported format is: + *

        + *   ±YYYY-MM-DDThh:mm:ss.SSSTZD
        + * 
        + * where: + *
        + *   ±YYYY = four-digit year with optional sign where values <= 0 are
        + *           denoting years BCE and values > 0 are denoting years CE,
        + *           e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE,
        + *           0001 denotes the year 1 CE, and so on...
        + *   MM    = two-digit month (01=January, etc.)
        + *   DD    = two-digit day of month (01 through 31)
        + *   hh    = two digits of hour (00 through 23) (am/pm NOT allowed)
        + *   mm    = two digits of minute (00 through 59)
        + *   ss    = two digits of second (00 through 59)
        + *   SSS   = three digits of milliseconds (000 through 999)
        + *   TZD   = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC
        + *           in the form of +hh:mm or -hh:mm
        + * 
        + */ +public final class ISO8601 { + /** + * Parses an ISO8601-compliant date/time string. + * + * @param text the date/time string to be parsed + * @return a Calendar, or null if the input could + * not be parsed + * @throws IllegalArgumentException if a null argument is passed + */ + public static Calendar parse(String text) { + if (text == null) { + throw new IllegalArgumentException("argument can not be null"); + } + + // check optional leading sign + char sign; + int start; + if (text.startsWith("-")) { + sign = '-'; + start = 1; + } else if (text.startsWith("+")) { + sign = '+'; + start = 1; + } else { + sign = '+'; // no sign specified, implied '+' + start = 0; + } + + /** + * the expected format of the remainder of the string is: + * YYYY-MM-DDThh:mm:ss.SSSTZD + * + * note that we cannot use java.text.SimpleDateFormat for + * parsing because it can't handle years <= 0 and TZD's + */ + + int year, month, day, hour, min, sec, ms; + String tzID; + try { + // year (YYYY) + year = Integer.parseInt(text.substring(start, start + 4)); + start += 4; + // delimiter '-' + if (text.charAt(start) != '-') { + return null; + } + start++; + // month (MM) + month = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter '-' + if (text.charAt(start) != '-') { + return null; + } + start++; + // day (DD) + day = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter 'T' + if (text.charAt(start) != 'T') { + return null; + } + start++; + // hour (hh) + hour = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (text.charAt(start) != ':') { + return null; + } + start++; + // minute (mm) + min = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter ':' + if (text.charAt(start) != ':') { + return null; + } + start++; + // second (ss) + sec = Integer.parseInt(text.substring(start, start + 2)); + start += 2; + // delimiter '.' + if (text.charAt(start) != '.') { + return null; + } + start++; + // millisecond (SSS) + ms = Integer.parseInt(text.substring(start, start + 3)); + start += 3; + // time zone designator (Z or +00:00 or -00:00) + if (text.charAt(start) == '+' || text.charAt(start) == '-') { + // offset to UTC specified in the format +00:00/-00:00 + tzID = "GMT" + text.substring(start); + } else if (text.substring(start).equals("Z")) { + tzID = "GMT"; + } else { + // invalid time zone designator + return null; + } + } catch (IndexOutOfBoundsException e) { + return null; + } catch (NumberFormatException e) { + return null; + } + + TimeZone tz = TimeZone.getTimeZone(tzID); + // verify id of returned time zone (getTimeZone defaults to "GMT") + if (!tz.getID().equals(tzID)) { + // invalid time zone + return null; + } + + // initialize Calendar object + Calendar cal = Calendar.getInstance(tz); + cal.setLenient(false); + // year and era + if (sign == '-' || year == 0) { + // not CE, need to set era (BCE) and adjust year + cal.set(Calendar.YEAR, year + 1); + cal.set(Calendar.ERA, GregorianCalendar.BC); + } else { + cal.set(Calendar.YEAR, year); + cal.set(Calendar.ERA, GregorianCalendar.AD); + } + // month (0-based!) + cal.set(Calendar.MONTH, month - 1); + // day of month + cal.set(Calendar.DAY_OF_MONTH, day); + // hour + cal.set(Calendar.HOUR_OF_DAY, hour); + // minute + cal.set(Calendar.MINUTE, min); + // second + cal.set(Calendar.SECOND, sec); + // millisecond + cal.set(Calendar.MILLISECOND, ms); + + try { + /** + * the following call will trigger an IllegalArgumentException + * if any of the set values are illegal or out of range + */ + cal.getTime(); + /** + * in addition check the validity of the year + */ + getYear(cal); + } catch (IllegalArgumentException e) { + return null; + } + + return cal; + } + + /** + * Formats a Calendar value into an ISO8601-compliant + * date/time string. + * + * @param cal the time value to be formatted into a date/time string. + * @return the formatted date/time string. + * @throws IllegalArgumentException if a null argument is passed + * or the calendar cannot be represented as defined by ISO 8601 (i.e. year + * with more than four digits). + */ + public static String format(Calendar cal) throws IllegalArgumentException { + if (cal == null) { + throw new IllegalArgumentException("argument can not be null"); + } + + /** + * the format of the date/time string is: + * YYYY-MM-DDThh:mm:ss.SSSTZD + * + * note that we cannot use java.text.SimpleDateFormat for + * formatting because it can't handle years <= 0 and TZD's + */ + StringBuffer buf = new StringBuffer(); + // year ([-]YYYY) + appendZeroPaddedInt(buf, getYear(cal), 4); + buf.append('-'); + // month (MM) + appendZeroPaddedInt(buf, cal.get(Calendar.MONTH) + 1, 2); + buf.append('-'); + // day (DD) + appendZeroPaddedInt(buf, cal.get(Calendar.DAY_OF_MONTH), 2); + buf.append('T'); + // hour (hh) + appendZeroPaddedInt(buf, cal.get(Calendar.HOUR_OF_DAY), 2); + buf.append(':'); + // minute (mm) + appendZeroPaddedInt(buf, cal.get(Calendar.MINUTE), 2); + buf.append(':'); + // second (ss) + appendZeroPaddedInt(buf, cal.get(Calendar.SECOND), 2); + buf.append('.'); + // millisecond (SSS) + appendZeroPaddedInt(buf, cal.get(Calendar.MILLISECOND), 3); + // time zone designator (Z or +00:00 or -00:00) + TimeZone tz = cal.getTimeZone(); + // determine offset of timezone from UTC (incl. daylight saving) + int offset = tz.getOffset(cal.getTimeInMillis()); + if (offset != 0) { + int hours = Math.abs((offset / (60 * 1000)) / 60); + int minutes = Math.abs((offset / (60 * 1000)) % 60); + buf.append(offset < 0 ? '-' : '+'); + appendZeroPaddedInt(buf, hours, 2); + buf.append(':'); + appendZeroPaddedInt(buf, minutes, 2); + } else { + buf.append('Z'); + } + return buf.toString(); + } + + /** + * Returns the astronomical year of the given calendar. + * + * @param cal a calendar instance. + * @return the astronomical year. + * @throws IllegalArgumentException if calendar cannot be represented as + * defined by ISO 8601 (i.e. year with more + * than four digits). + */ + public static int getYear(Calendar cal) throws IllegalArgumentException { + // determine era and adjust year if necessary + int year = cal.get(Calendar.YEAR); + if (cal.isSet(Calendar.ERA) + && cal.get(Calendar.ERA) == GregorianCalendar.BC) { + /** + * calculate year using astronomical system: + * year n BCE => astronomical year -n + 1 + */ + year = 0 - year + 1; + } + + if (year > 9999 || year < -9999) { + throw new IllegalArgumentException("Calendar has more than four " + + "year digits, cannot be formatted as ISO8601: " + year); + } + return year; + } + + /** + * Appends a zero-padded number to the given string buffer. + *

        + * This is an internal helper method which doesn't perform any + * validation on the given arguments. + * + * @param buf String buffer to append to + * @param n number to append + * @param precision number of digits to append + */ + private static void appendZeroPaddedInt(StringBuffer buf, int n, int precision) { + if (n < 0) { + buf.append('-'); + n = -n; + } + + for (int exp = precision - 1; exp > 0; exp--) { + if (n < Math.pow(10, exp)) { + buf.append('0'); + } else { + break; + } + } + buf.append(n); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JCRTestResult.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JCRTestResult.java new file mode 100644 index 00000000000..40954aecbd6 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JCRTestResult.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.test; + +import junit.framework.TestFailure; +import junit.framework.TestResult; +import junit.framework.Test; +import junit.framework.AssertionFailedError; +import junit.framework.TestListener; +import junit.framework.TestCase; + +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.StringTokenizer; + +/** + * Extends the standard JUnit TestResult class. This class ignores test errors + * that originated in throwing a {@link NotExecutableException}. + */ +public class JCRTestResult extends TestResult { + + /** The original TestResult we delegate to */ + private final TestResult orig; + + /** The log writer of the test classes */ + private final LogPrintWriter log; + + /** + * Set of Strings that identify the test methods that currently fails but + * are recognized as known issues. Those will not be reported as errors. + */ + private final Set knownIssues; + + /** + * Set of Strings that identify the test methods that are listed as known + * issues but whose test failures should still be reported. + */ + private final Set knownIssuesOverride; + + /** + * Creates a new JCRTestResult that delegates to orig. + * @param orig the original TestResult this result wraps. + * @param log the logger + */ + public JCRTestResult(TestResult orig, LogPrintWriter log) { + this.orig = orig; + this.log = log; + this.knownIssues = JCRTestResult.tokenize("known.issues"); + this.knownIssuesOverride = JCRTestResult.tokenize("known.issues.override"); + } + + @Override + protected void run(TestCase test) { + if (isKnownIssue(test)) { + // notify listeners but not actually run the test + startTest(test); + log.println("Known issue: " + test + " (skipped)"); + endTest(test); + } else { + super.run(test); + } + } + + /** + * Only add an error if throwable is not of type + * {@link NotExecutableException} and the test case is not a known issue. + * @param test the test. + * @param throwable the exception thrown by the test. + */ + public synchronized void addError(Test test, Throwable throwable) { + if (throwable instanceof NotExecutableException) { + log.println("Test case: " + test.toString() + " not executable: " + throwable.getMessage()); + } else if (isKnownIssue(test)) { + log.println("Known issue: " + test + ": " + throwable.getMessage()); + } else { + orig.addError(test, throwable); + } + } + + /** + * Only adds a failure if test is not a known issue. + * @param test the test case that failed. + * @param assertionFailedError the assertion error. + */ + public synchronized void addFailure(Test test, + AssertionFailedError assertionFailedError) { + if (isKnownIssue(test)) { + log.println("Known issue: " + test + ": " + assertionFailedError.getMessage()); + } else { + orig.addFailure(test, assertionFailedError); + } + } + + //-----------------------< default overwrites >----------------------------- + + public synchronized void addListener(TestListener testListener) { + orig.addListener(testListener); + } + + public synchronized void removeListener(TestListener testListener) { + orig.removeListener(testListener); + } + + public void endTest(Test test) { + orig.endTest(test); + } + + public synchronized int errorCount() { + return orig.errorCount(); + } + + public synchronized Enumeration errors() { + return orig.errors(); + } + + public synchronized int failureCount() { + return orig.failureCount(); + } + + public synchronized Enumeration failures() { + return orig.failures(); + } + + public synchronized int runCount() { + return orig.runCount(); + } + + public synchronized boolean shouldStop() { + return orig.shouldStop(); + } + + public void startTest(Test test) { + orig.startTest(test); + } + + public synchronized void stop() { + orig.stop(); + } + + public synchronized boolean wasSuccessful() { + return orig.wasSuccessful(); + } + + //------------------------------< internal >-------------------------------- + + /** + * Takes the named system property and returns the set of string tokens + * in the property value. Returns an empty set if the named property does + * not exist. + * + * @param name name of the system property + * @return set of string tokens + */ + private static Set tokenize(String name) { + Set tokens = new HashSet(); + StringTokenizer tokenizer = + new StringTokenizer(System.getProperty(name, "")); + while (tokenizer.hasMoreTokens()) { + tokens.add(tokenizer.nextToken()); + } + return tokens; + } + + /** + * Checks if a variation of the name of the given test case is included + * in the given set of token. The tested variations are: + *

          + *
        • package name
        • + *
        • non-qualified class name
        • + *
        • fully qualified class name
        • + *
        • non-qualified method name
        • + *
        • class-qualified method name
        • + *
        • fully-qualified method name
        • + *
        + * + * @param tokens set of string tokens + * @param test test case + * @return true if the test case name is included, + * false otherwise + */ + private static boolean contains(Set tokens, TestCase test) { + String className = test.getClass().getName(); + String methodName = test.getName(); + + int i = className.lastIndexOf('.'); + if (i >= 0) { + String packageName = className.substring(0, i); + String shortName = className.substring(i + 1); + return tokens.contains(packageName) + || tokens.contains(shortName) + || tokens.contains(className) + || tokens.contains(methodName) + || tokens.contains(shortName + "#" + methodName) + || tokens.contains(className + "#" + methodName); + } else { + return tokens.contains(className) + || tokens.contains(methodName) + || tokens.contains(className + "#" + methodName); + } + } + + /** + * Returns true if test is a known issue + * whose test result should be ignored; false otherwise. + * + * @param test the test case to check. + * @return true if test result should be ignored + */ + private boolean isKnownIssue(Test test) { + if (test instanceof TestCase) { + return contains(knownIssues, (TestCase) test) + && !contains(knownIssuesOverride, (TestCase) test); + } else { + return false; + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JCRTestSuite.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JCRTestSuite.java new file mode 100644 index 00000000000..8e970f8eb50 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JCRTestSuite.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.test; + +/** + * Test suite that includes all the JCR API tests + */ +public class JCRTestSuite extends ConcurrentTestSuite { + + public JCRTestSuite() { + super("JCR API tests"); + addTest(org.apache.jackrabbit.test.api.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.query.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.query.qom.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.nodetype.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.util.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.lock.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.version.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.version.simple.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.observation.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.retention.TestAll.suite()); + addTest(org.apache.jackrabbit.test.api.security.TestAll.suite()); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JNDIRepositoryStub.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JNDIRepositoryStub.java new file mode 100644 index 00000000000..d2041f72c00 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JNDIRepositoryStub.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.test; + +import java.security.Principal; +import java.util.Properties; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.rmi.PortableRemoteObject; + +/** + * Implements the abstract class RepositoryStub and uses JNDI + * to obtain a javax.jcr.Repository instance. + */ +public class JNDIRepositoryStub extends RepositoryStub { + + public static final String REPOSITORY_LOOKUP_PROP = "javax.jcr.tck.jndi.repository_lookup_name"; + + private Repository repository = null; + + public JNDIRepositoryStub(Properties env) { + super(env); + } + + /** + * Returns a reference to the Repository provided by this + * RepositoryStub. + * @return + */ + public synchronized Repository getRepository() throws RepositoryStubException { + if (repository == null) { + try { + String lookupName = environment.getProperty(REPOSITORY_LOOKUP_PROP); + if (lookupName == null) { + throw new RepositoryStubException("Property " + REPOSITORY_LOOKUP_PROP + " not defined!"); + } + InitialContext initial = new InitialContext(environment); + Object obj = initial.lookup(lookupName); + + repository = (Repository)PortableRemoteObject.narrow(obj, Repository.class); + + } catch (ClassCastException e) { + // ClassCastException may be thrown by ProtableRemoteObject.narrow() + throw new RepositoryStubException( + "Object cannot be narrowed to javax.jcr.Repository", e); + } catch (NamingException e) { + throw new RepositoryStubException(e); + } + } + return repository; + } + + public Principal getKnownPrincipal(Session session) throws RepositoryException { + // TODO Auto-generated method stub + throw new RepositoryException("TDB"); + } + + public Principal getUnknownPrincipal(Session session) throws RepositoryException, NotExecutableException { + // TODO Auto-generated method stub + throw new RepositoryException("TDB"); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JUnitTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JUnitTest.java new file mode 100644 index 00000000000..27a71f233b8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/JUnitTest.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.test; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +import junit.framework.TestCase; + +/** + * Abstract base class for any JUnit test case. + */ +public abstract class JUnitTest extends TestCase { + + /** + * Logger instance for test cases + */ + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Output stream for general messages from tests. + */ + public final LogPrintWriter log = new LogPrintWriter(logger); + + protected void setUp() throws Exception { + super.setUp(); + MDC.put("testclass", getClass().getName()); + MDC.put("testcase", getName()); + logger.info("Starting test case {}", getName()); + } + + protected void tearDown() throws Exception { + logger.info("Completed test case {}", getName()); + MDC.remove("testcase"); + MDC.remove("testclass"); + super.tearDown(); + } + +} diff --git a/src/test/org/apache/jackrabbit/test/LogPrintWriter.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/LogPrintWriter.java similarity index 83% rename from src/test/org/apache/jackrabbit/test/LogPrintWriter.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/LogPrintWriter.java index 7467dca56bf..435fcca1dc0 100644 --- a/src/test/org/apache/jackrabbit/test/LogPrintWriter.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/LogPrintWriter.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -16,8 +16,7 @@ */ package org.apache.jackrabbit.test; -import org.apache.log4j.Logger; -import org.apache.log4j.Level; +import org.slf4j.Logger; import java.io.Writer; import java.io.IOException; @@ -39,11 +38,6 @@ public class LogPrintWriter extends PrintWriter { */ private Logger log; - /** - * Default Level is debug. - */ - private Level level = Level.DEBUG; - /** * Creates a new LogPrintWriter which is based on a * Writer. @@ -90,16 +84,6 @@ public void setLogger(Logger log) { this.log = log; } - /** - * Sets the log level for messages written to the Logger - * instance currently set in this LogPrintWriter. - * - * @param level the log level to set. - */ - public void setMsgLevel(Level level) { - this.level = level; - } - //------------------< overrides from PrintWriter >------------------------- public void close() { @@ -139,7 +123,7 @@ private void flushBuffer() { return; } if (log != null) { - log.log(level, buffer); + log.debug(buffer.toString()); } else { try { out.write(buffer.toString()); diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/NotExecutableException.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/NotExecutableException.java new file mode 100644 index 00000000000..af6fa480336 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/NotExecutableException.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.test; + +/** + * This exception indicates that a test case cannot be executed due to missing + * data or if the repository does not have the ability to perform the test. E.g. + * a feature is optional. + *

        + * A test method may simply declare this exception in its signature and throw + * this exception at any point in the method. + *

        + * The TCK framework will take care that a test method throwing this exception + * is not considered to be in error, but that the repository is unable to + * execute this test. + */ +public class NotExecutableException extends Exception { + + /** + * Creates a NotExecutableException without a message. + */ + public NotExecutableException() { + } + + /** + * Creates a NotExecutableException with a detailed message. + * @param message describes why the test case cannot be executed. + */ + public NotExecutableException(String message) { + super(message); + } +} diff --git a/src/test/org/apache/jackrabbit/test/RepositoryHelper.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelper.java similarity index 86% rename from src/test/org/apache/jackrabbit/test/RepositoryHelper.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelper.java index 269fe49b249..61e83f82c35 100644 --- a/src/test/org/apache/jackrabbit/test/RepositoryHelper.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelper.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -16,12 +16,14 @@ */ package org.apache.jackrabbit.test; +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Credentials; import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; -import javax.jcr.Credentials; -import java.util.Map; -import java.util.HashMap; /** * Utility class to get access to {@link javax.jcr.Session} instances. @@ -137,7 +139,7 @@ public Session getReadOnlySession(String workspaceName) throws RepositoryExcepti * Returns the value of the configuration property with specified * name. If the property does not exist null is * returned. - *

        + *

        * Configuration properties are defined in the file: * repositoryStubImpl.properties. * @@ -186,4 +188,20 @@ public Credentials getReadWriteCredentials() { public Credentials getSuperuserCredentials() { return repoStub.getSuperuserCredentials(); } + + /** + * Returns a {@link Principal} identifying a known user. + * @param session + */ + public Principal getKnownPrincipal(Session session) throws RepositoryException { + return repoStub.getKnownPrincipal(session); + } + + /** + * Returns a {@link Principal} identifiying an unknown user. + * @param session + */ + public Principal getUnknownPrincipal(Session session) throws NotExecutableException, RepositoryException { + return repoStub.getUnknownPrincipal(session); + } } diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelperPool.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelperPool.java new file mode 100644 index 00000000000..868a745d093 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelperPool.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.test; + +/** + * RepositoryHelperPool defines a pool of repository helper instances. + */ +public interface RepositoryHelperPool { + + /** + * Borrows a repository helper instance. + * + * @return a repository helper. + * @throws InterruptedException if this thread is interrupted while waiting + * for a repository helper. + */ + public RepositoryHelper borrowHelper() throws InterruptedException; + + /** + * Borrows all available repository helper instances. Waits until one + * becomes available. + * + * @return a repository helper. + * @throws InterruptedException if this thread is interrupted while waiting + * for a repository helper. + */ + public RepositoryHelper[] borrowHelpers() throws InterruptedException; + + /** + * Returns the given repository helper to the pool. + * + * @param helper the repository helper to return. + */ + public void returnHelper(RepositoryHelper helper); +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelperPoolImpl.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelperPoolImpl.java new file mode 100644 index 00000000000..bbdc443cbbf --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryHelperPoolImpl.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.test; + +import java.util.List; +import java.util.LinkedList; +import java.util.Properties; +import java.util.Map; +import java.util.HashMap; +import java.io.InputStream; +import java.io.IOException; + +/** + * RepositoryHelperPoolImpl implements a pool of repository helper + * instances. + */ +public class RepositoryHelperPoolImpl implements RepositoryHelperPool { + + private static final String PROP_FILE = "repositoryHelperPool.properties"; + + private List helpers = new LinkedList(); + + private static RepositoryHelperPool POOL = null; + + public synchronized static RepositoryHelperPool getInstance() { + if (POOL == null) { + POOL = new RepositoryHelperPoolImpl(); + } + return POOL; + } + + private RepositoryHelperPoolImpl() { + InputStream in = RepositoryHelperPoolImpl.class.getClassLoader().getResourceAsStream(PROP_FILE); + if (in != null) { + try { + Properties props = new Properties(); + props.load(in); + for (int i = 0;; i++) { + String prefix = "helper." + i + "."; + Map helperProp = new HashMap(); + for (Map.Entry entry : props.entrySet()) { + String key = (String) entry.getKey(); + if (key.startsWith(prefix)) { + helperProp.put(key.substring(prefix.length()), entry.getValue()); + } + } + if (helperProp.isEmpty()) { + break; + } + addHelper(new RepositoryHelper(helperProp)); + } + } catch (IOException e) { + // ignore and use default + } finally { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } + if (helpers.isEmpty()) { + // use single default repo helper + addHelper(new RepositoryHelper()); + } + } + + public synchronized void addHelper(RepositoryHelper helper) { + helpers.add(helper); + } + + public synchronized RepositoryHelper borrowHelper() + throws InterruptedException { + while (helpers.isEmpty()) { + wait(); + } + return helpers.remove(0); + } + + public synchronized RepositoryHelper[] borrowHelpers() throws InterruptedException { + while (helpers.isEmpty()) { + wait(); + } + try { + return helpers.toArray(new RepositoryHelper[helpers.size()]); + } finally { + helpers.clear(); + } + } + + public synchronized void returnHelper(RepositoryHelper helper) { + helpers.add(helper); + notifyAll(); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryStub.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryStub.java new file mode 100644 index 00000000000..ff876feb3d0 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryStub.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.test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.security.Principal; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +/** + * The RepositoryStub is the entry point to the JCR Repository + * for the TCK Test harness. + *

        + * Implementors of the JCR specification need to provide an implementation + * for the abstract methods defined in this class. + */ +public abstract class RepositoryStub { + + public static final String STUB_IMPL_PROPS = "repositoryStubImpl.properties"; + + public static final String PROP_PREFIX = "javax.jcr.tck"; + + public static final String STUB_IMPL_SYS_PROPS = PROP_PREFIX + ".properties"; + + public static final String PROP_STUB_IMPL_CLASS = PROP_PREFIX + ".repository_stub_impl"; + + public static final String PROP_SUPERUSER_PWD = "superuser.pwd"; + + public static final String PROP_SUPERUSER_NAME = "superuser.name"; + + public static final String PROP_READONLY_PWD = "readonly.pwd"; + + public static final String PROP_READONLY_NAME = "readonly.name"; + + public static final String PROP_READWRITE_PWD = "readwrite.pwd"; + + public static final String PROP_READWRITE_NAME = "readwrite.name"; + + public static final String PROP_NODETYPE = "nodetype"; + + public static final String PROP_NODETYPETESTROOT = "nodetypetestroot"; + + public static final String PROP_NODETYPENOCHILDREN = "nodetypenochildren"; + + public static final String PROP_TESTROOT = "testroot"; + + public static final String PROP_NODE_NAME1 = "nodename1"; + + public static final String PROP_NODE_NAME2 = "nodename2"; + + public static final String PROP_NODE_NAME3 = "nodename3"; + + public static final String PROP_NODE_NAME4 = "nodename4"; + + public static final String PROP_PROP_NAME1 = "propertyname1"; + + public static final String PROP_PROP_NAME2 = "propertyname2"; + + /** + * @since JCR 2.0 + */ + public static final String PROP_PROP_VALUE1 = "propertyvalue1"; + + /** + * @since JCR 2.0 + */ + public static final String PROP_PROP_VALUE2 = "propertyvalue2"; + + /** + * @since JCR 2.0 + */ + public static final String PROP_PROP_TYPE1 = "propertytype1"; + + /** + * @since JCR 2.0 + */ + public static final String PROP_PROP_TYPE2 = "propertytype2"; + + public static final String PROP_WORKSPACE_NAME = "workspacename"; + + public static final String PROP_NAMESPACES = "namespaces"; + + /** + * @since JCR 2.0 + */ + public static final String PROP_LOCK_TIMEOUT = "lock.timeout"; + + /** + * @since JCR 2.0 + */ + public static final String PROP_LOCK_OWNER = "lock.owner"; + + /** + * Determines whether the repository implementation allows open scoped locks + * to be owned by multiple sessions (see JCR 2.0 Section 17.3). + * + * @since JCR 2.0 + */ + public static final String PROP_OPEN_SCOPED_LOCK_MULTIPLE = "open.scoped.lock.multiple"; + + /** + * @since JCR 2.0 + */ + public static final String PROP_HOLD_NAME = "holdname"; + + public static final String RETENTION_POLICY_HOLDER = "retentionpolicyholder"; + + /** + * @since JCR 2.0 + */ + public static final String REPOSITORY_FACTORY = "repository.factory"; + + protected final Properties environment; + + protected SimpleCredentials superuser; + + protected SimpleCredentials readonly; + + protected SimpleCredentials readwrite; + + /** + * Implementations of this class must overwrite this constructor. + * + * @param env the environment variables. This parameter must not be null. + */ + protected RepositoryStub(Properties env) { + if (env == null) { + throw new IllegalArgumentException("Parameter 'env' must not be null!"); + } + environment = env; + superuser = new SimpleCredentials(env.getProperty(PROP_PREFIX + "." + PROP_SUPERUSER_NAME, ""), + env.getProperty(PROP_PREFIX + "." + PROP_SUPERUSER_PWD, "").toCharArray()); + readonly = new SimpleCredentials(env.getProperty(PROP_PREFIX + "." + PROP_READONLY_NAME, ""), + env.getProperty(PROP_PREFIX + "." + PROP_READONLY_PWD, "").toCharArray()); + readwrite = new SimpleCredentials(env.getProperty(PROP_PREFIX + "." + PROP_READWRITE_NAME, ""), + env.getProperty(PROP_PREFIX + "." + PROP_READWRITE_PWD, "").toCharArray()); + } + + /** + * Creates and/or returns the configured RepositryStub + * implementation. + *

        + * The property file is located in the following sequence: + *

          + *
        1. If the system property -Djavax.jcr.tck.properties is + * set, then the accroding file is used as configuration.
        2. + *
        3. If the system property -Djavax.jcr.tck.properties is + * not set, then the TCK tries to load the file repositoryStubImpl.properties + * as a resource from the ClassLoader of this RepositryStub class.
        4. + *
        + * The properties are then overlayed with the the key / values from + * configuration map. + * + * @param configuration a Map of additional configuration entries. + * @return a RepositoryStub implementation. + * @throws RepositoryStubException + */ + public static synchronized RepositoryStub getInstance(Map configuration) + throws RepositoryStubException { + Properties props = null; + RepositoryStub stub = null; + String implProp = System.getProperty(STUB_IMPL_SYS_PROPS); + if (implProp != null) { + File implPropFile = new File(implProp); + if (implPropFile.exists()) { + props = new Properties(); + try { + props.load(new FileInputStream(implPropFile)); + } catch (IOException e) { + throw new RepositoryStubException( + "Unable to load config file: " + implProp, e); + } + } else { + throw new RepositoryStubException("File does not exist: " + implProp); + } + } + + if (props == null) { + props = new Properties(); + InputStream is = RepositoryStub.class.getClassLoader().getResourceAsStream(STUB_IMPL_PROPS); + if (is != null) { + try { + props.load(is); + } catch (IOException e) { + throw new RepositoryStubException( + "Exception reading " + STUB_IMPL_PROPS, e); + } + } + } + + // overlay with configuration parameter + props.putAll(configuration); + + try { + String className = props.getProperty(PROP_STUB_IMPL_CLASS); + if (className == null || className.length() == 0) { + throw new RepositoryStubException("Property " + PROP_STUB_IMPL_CLASS + " not defined!"); + } + Class stubClass = Class.forName(className); + Constructor constr = stubClass.getConstructor(new Class[]{Properties.class}); + stub = (RepositoryStub) constr.newInstance(new Object[]{props}); + } catch (Exception e) { + throw new RepositoryStubException(e); + } + + return stub; + } + + /** + * Returns a reference to the Repository provided by this + * RepositoryStub. + * + * @return + */ + public abstract Repository getRepository() throws RepositoryStubException; + + /** + * Returns a Credentials object, that can be used to login + * to the Repository returned by {@link #getRepository}. + *

        + * The Credentials returned has 'superuser' rights. That + * is, the Session object returned by {@link Repository#login(Credentials)} + * has read write access to the whole Content Repository. + * + * @return a Credentials object, that allows to login to the + * Repository as 'superuser'. + */ + public Credentials getSuperuserCredentials() { + return superuser; + } + + /** + * Returns a Credentials object, that can be used to login + * to the Repository returned by {@link #getRepository}. + *

        + * The Credentials returned has read/write rights. That + * is, the Session object returned by {@link Repository#login(Credentials)} + * has read write access to the Node configured in the + * JCR TCK Interview. + *

        + * For details, see: JCR TCK User Guide. + * + * @return a Credentials object, that allows to login to the + * Repository with read/write right. + */ + public Credentials getReadWriteCredentials() { + return readwrite; + } + + /** + * Returns a Credentials object, that can be used to login + * to the Repository returned by {@link #getRepository}. + *

        + * The Credentials returned must have read-only rights. That + * is, the Session object returned by {@link Repository#login()} + * has read-only access to the Node configured in the + * JCR TCK Interview. + *

        + * For details, see: JCR TCK User Guide. + * + * @return a Credentials object, that allows to login to the + * Repository with read-only right. + */ + public Credentials getReadOnlyCredentials() { + return readonly; + } + + /** + * Returns the property with the specified name. If a + * property with the given name does not exist, null is + * returned. + * @param name the name of the property. + * @return the property, or null if the property does not + * exist. + */ + public String getProperty(String name) { + return environment.getProperty(name); + } + + /** + * Returns a {@link Principal} identifiying a known user (not group) + * @param session + */ + public abstract Principal getKnownPrincipal(Session session) throws RepositoryException; + + /** + * Returns a {@link Principal} identifiying an unknown user. + * @param session + * @throws NotExecutableException if the implementation can not provide an + * instance of Principal for unkwnown users + */ + public abstract Principal getUnknownPrincipal(Session session) throws RepositoryException, NotExecutableException; +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryStubException.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryStubException.java new file mode 100644 index 00000000000..35fab06ae4f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/RepositoryStubException.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.test; + +/** + * + */ +public class RepositoryStubException extends Exception { + + public RepositoryStubException(String msg) { + super(msg); + } + + public RepositoryStubException(Throwable cause) { + super(cause); + } + + public RepositoryStubException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/TestAll.java new file mode 100644 index 00000000000..27847098485 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/TestAll.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.test; + +import junit.framework.TestCase; +import junit.framework.Test; +import org.apache.jackrabbit.test.JCRTestSuite; + +public class TestAll extends TestCase { + + public static Test suite() { + return new JCRTestSuite(); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/XMLChar.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/XMLChar.java new file mode 100644 index 00000000000..7cab3184059 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/XMLChar.java @@ -0,0 +1,1025 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test; + +// Note: This file is a copy from org.apache.xerces.util. +// See http://issues.apache.org/jira/browse/JCR-367 + +import java.util.Arrays; + +/** + * This class defines the basic XML character properties. The data + * in this class can be used to verify that a character is a valid + * XML character or if the character is a space, name start, or name + * character. + *

        + * A series of convenience methods are supplied to ease the burden + * of the developer. Because inlining the checks can improve per + * character performance, the tables of character properties are + * public. Using the character as an index into the CHARS + * array and applying the appropriate mask flag (e.g. + * MASK_VALID), yields the same results as calling the + * convenience methods. There is one exception: check the comments + * for the isValid method for details. + * + * @author Glenn Marcy, IBM + * @author Andy Clark, IBM + * @author Eric Ye, IBM + * @author Arnaud Le Hors, IBM + * @author Michael Glavassevich, IBM + * @author Rahul Srivastava, Sun Microsystems Inc. + * + * @version $Id$ + */ +public class XMLChar { + + // + // Constants + // + + /** Character flags. */ + private static final byte[] CHARS = new byte[1 << 16]; + + /** Valid character mask. */ + public static final int MASK_VALID = 0x01; + + /** Space character mask. */ + public static final int MASK_SPACE = 0x02; + + /** Name start character mask. */ + public static final int MASK_NAME_START = 0x04; + + /** Name character mask. */ + public static final int MASK_NAME = 0x08; + + /** Pubid character mask. */ + public static final int MASK_PUBID = 0x10; + + /** + * Content character mask. Special characters are those that can + * be considered the start of markup, such as '<' and '&'. + * The various newline characters are considered special as well. + * All other valid XML characters can be considered content. + *

        + * This is an optimization for the inner loop of character scanning. + */ + public static final int MASK_CONTENT = 0x20; + + /** NCName start character mask. */ + public static final int MASK_NCNAME_START = 0x40; + + /** NCName character mask. */ + public static final int MASK_NCNAME = 0x80; + + // + // Static initialization + // + + static { + + // Initializing the Character Flag Array + // Code generated by: XMLCharGenerator. + + CHARS[9] = 35; + CHARS[10] = 19; + CHARS[13] = 19; + CHARS[32] = 51; + CHARS[33] = 49; + CHARS[34] = 33; + Arrays.fill(CHARS, 35, 38, (byte) 49 ); // Fill 3 of value (byte) 49 + CHARS[38] = 1; + Arrays.fill(CHARS, 39, 45, (byte) 49 ); // Fill 6 of value (byte) 49 + Arrays.fill(CHARS, 45, 47, (byte) -71 ); // Fill 2 of value (byte) -71 + CHARS[47] = 49; + Arrays.fill(CHARS, 48, 58, (byte) -71 ); // Fill 10 of value (byte) -71 + CHARS[58] = 61; + CHARS[59] = 49; + CHARS[60] = 1; + CHARS[61] = 49; + CHARS[62] = 33; + Arrays.fill(CHARS, 63, 65, (byte) 49 ); // Fill 2 of value (byte) 49 + Arrays.fill(CHARS, 65, 91, (byte) -3 ); // Fill 26 of value (byte) -3 + Arrays.fill(CHARS, 91, 93, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[93] = 1; + CHARS[94] = 33; + CHARS[95] = -3; + CHARS[96] = 33; + Arrays.fill(CHARS, 97, 123, (byte) -3 ); // Fill 26 of value (byte) -3 + Arrays.fill(CHARS, 123, 183, (byte) 33 ); // Fill 60 of value (byte) 33 + CHARS[183] = -87; + Arrays.fill(CHARS, 184, 192, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 192, 215, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[215] = 33; + Arrays.fill(CHARS, 216, 247, (byte) -19 ); // Fill 31 of value (byte) -19 + CHARS[247] = 33; + Arrays.fill(CHARS, 248, 306, (byte) -19 ); // Fill 58 of value (byte) -19 + Arrays.fill(CHARS, 306, 308, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 308, 319, (byte) -19 ); // Fill 11 of value (byte) -19 + Arrays.fill(CHARS, 319, 321, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 321, 329, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[329] = 33; + Arrays.fill(CHARS, 330, 383, (byte) -19 ); // Fill 53 of value (byte) -19 + CHARS[383] = 33; + Arrays.fill(CHARS, 384, 452, (byte) -19 ); // Fill 68 of value (byte) -19 + Arrays.fill(CHARS, 452, 461, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 461, 497, (byte) -19 ); // Fill 36 of value (byte) -19 + Arrays.fill(CHARS, 497, 500, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 500, 502, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 502, 506, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 506, 536, (byte) -19 ); // Fill 30 of value (byte) -19 + Arrays.fill(CHARS, 536, 592, (byte) 33 ); // Fill 56 of value (byte) 33 + Arrays.fill(CHARS, 592, 681, (byte) -19 ); // Fill 89 of value (byte) -19 + Arrays.fill(CHARS, 681, 699, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 699, 706, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 706, 720, (byte) 33 ); // Fill 14 of value (byte) 33 + Arrays.fill(CHARS, 720, 722, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 722, 768, (byte) 33 ); // Fill 46 of value (byte) 33 + Arrays.fill(CHARS, 768, 838, (byte) -87 ); // Fill 70 of value (byte) -87 + Arrays.fill(CHARS, 838, 864, (byte) 33 ); // Fill 26 of value (byte) 33 + Arrays.fill(CHARS, 864, 866, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 866, 902, (byte) 33 ); // Fill 36 of value (byte) 33 + CHARS[902] = -19; + CHARS[903] = -87; + Arrays.fill(CHARS, 904, 907, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[907] = 33; + CHARS[908] = -19; + CHARS[909] = 33; + Arrays.fill(CHARS, 910, 930, (byte) -19 ); // Fill 20 of value (byte) -19 + CHARS[930] = 33; + Arrays.fill(CHARS, 931, 975, (byte) -19 ); // Fill 44 of value (byte) -19 + CHARS[975] = 33; + Arrays.fill(CHARS, 976, 983, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 983, 986, (byte) 33 ); // Fill 3 of value (byte) 33 + CHARS[986] = -19; + CHARS[987] = 33; + CHARS[988] = -19; + CHARS[989] = 33; + CHARS[990] = -19; + CHARS[991] = 33; + CHARS[992] = -19; + CHARS[993] = 33; + Arrays.fill(CHARS, 994, 1012, (byte) -19 ); // Fill 18 of value (byte) -19 + Arrays.fill(CHARS, 1012, 1025, (byte) 33 ); // Fill 13 of value (byte) 33 + Arrays.fill(CHARS, 1025, 1037, (byte) -19 ); // Fill 12 of value (byte) -19 + CHARS[1037] = 33; + Arrays.fill(CHARS, 1038, 1104, (byte) -19 ); // Fill 66 of value (byte) -19 + CHARS[1104] = 33; + Arrays.fill(CHARS, 1105, 1117, (byte) -19 ); // Fill 12 of value (byte) -19 + CHARS[1117] = 33; + Arrays.fill(CHARS, 1118, 1154, (byte) -19 ); // Fill 36 of value (byte) -19 + CHARS[1154] = 33; + Arrays.fill(CHARS, 1155, 1159, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 1159, 1168, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 1168, 1221, (byte) -19 ); // Fill 53 of value (byte) -19 + Arrays.fill(CHARS, 1221, 1223, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1223, 1225, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1225, 1227, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1227, 1229, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1229, 1232, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 1232, 1260, (byte) -19 ); // Fill 28 of value (byte) -19 + Arrays.fill(CHARS, 1260, 1262, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1262, 1270, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 1270, 1272, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1272, 1274, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1274, 1329, (byte) 33 ); // Fill 55 of value (byte) 33 + Arrays.fill(CHARS, 1329, 1367, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 1367, 1369, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[1369] = -19; + Arrays.fill(CHARS, 1370, 1377, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 1377, 1415, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 1415, 1425, (byte) 33 ); // Fill 10 of value (byte) 33 + Arrays.fill(CHARS, 1425, 1442, (byte) -87 ); // Fill 17 of value (byte) -87 + CHARS[1442] = 33; + Arrays.fill(CHARS, 1443, 1466, (byte) -87 ); // Fill 23 of value (byte) -87 + CHARS[1466] = 33; + Arrays.fill(CHARS, 1467, 1470, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[1470] = 33; + CHARS[1471] = -87; + CHARS[1472] = 33; + Arrays.fill(CHARS, 1473, 1475, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[1475] = 33; + CHARS[1476] = -87; + Arrays.fill(CHARS, 1477, 1488, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 1488, 1515, (byte) -19 ); // Fill 27 of value (byte) -19 + Arrays.fill(CHARS, 1515, 1520, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 1520, 1523, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 1523, 1569, (byte) 33 ); // Fill 46 of value (byte) 33 + Arrays.fill(CHARS, 1569, 1595, (byte) -19 ); // Fill 26 of value (byte) -19 + Arrays.fill(CHARS, 1595, 1600, (byte) 33 ); // Fill 5 of value (byte) 33 + CHARS[1600] = -87; + Arrays.fill(CHARS, 1601, 1611, (byte) -19 ); // Fill 10 of value (byte) -19 + Arrays.fill(CHARS, 1611, 1619, (byte) -87 ); // Fill 8 of value (byte) -87 + Arrays.fill(CHARS, 1619, 1632, (byte) 33 ); // Fill 13 of value (byte) 33 + Arrays.fill(CHARS, 1632, 1642, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 1642, 1648, (byte) 33 ); // Fill 6 of value (byte) 33 + CHARS[1648] = -87; + Arrays.fill(CHARS, 1649, 1720, (byte) -19 ); // Fill 71 of value (byte) -19 + Arrays.fill(CHARS, 1720, 1722, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1722, 1727, (byte) -19 ); // Fill 5 of value (byte) -19 + CHARS[1727] = 33; + Arrays.fill(CHARS, 1728, 1743, (byte) -19 ); // Fill 15 of value (byte) -19 + CHARS[1743] = 33; + Arrays.fill(CHARS, 1744, 1748, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[1748] = 33; + CHARS[1749] = -19; + Arrays.fill(CHARS, 1750, 1765, (byte) -87 ); // Fill 15 of value (byte) -87 + Arrays.fill(CHARS, 1765, 1767, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1767, 1769, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[1769] = 33; + Arrays.fill(CHARS, 1770, 1774, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 1774, 1776, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1776, 1786, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 1786, 2305, (byte) 33 ); // Fill 519 of value (byte) 33 + Arrays.fill(CHARS, 2305, 2308, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2308] = 33; + Arrays.fill(CHARS, 2309, 2362, (byte) -19 ); // Fill 53 of value (byte) -19 + Arrays.fill(CHARS, 2362, 2364, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2364] = -87; + CHARS[2365] = -19; + Arrays.fill(CHARS, 2366, 2382, (byte) -87 ); // Fill 16 of value (byte) -87 + Arrays.fill(CHARS, 2382, 2385, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2385, 2389, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 2389, 2392, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2392, 2402, (byte) -19 ); // Fill 10 of value (byte) -19 + Arrays.fill(CHARS, 2402, 2404, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2404, 2406, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2406, 2416, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2416, 2433, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 2433, 2436, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2436] = 33; + Arrays.fill(CHARS, 2437, 2445, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 2445, 2447, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2447, 2449, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2449, 2451, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2451, 2473, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2473] = 33; + Arrays.fill(CHARS, 2474, 2481, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2481] = 33; + CHARS[2482] = -19; + Arrays.fill(CHARS, 2483, 2486, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2486, 2490, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2490, 2492, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2492] = -87; + CHARS[2493] = 33; + Arrays.fill(CHARS, 2494, 2501, (byte) -87 ); // Fill 7 of value (byte) -87 + Arrays.fill(CHARS, 2501, 2503, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2503, 2505, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2505, 2507, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2507, 2510, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2510, 2519, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[2519] = -87; + Arrays.fill(CHARS, 2520, 2524, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2524, 2526, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2526] = 33; + Arrays.fill(CHARS, 2527, 2530, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2530, 2532, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2532, 2534, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2534, 2544, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2544, 2546, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2546, 2562, (byte) 33 ); // Fill 16 of value (byte) 33 + CHARS[2562] = -87; + Arrays.fill(CHARS, 2563, 2565, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2565, 2571, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 2571, 2575, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2575, 2577, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2577, 2579, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2579, 2601, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2601] = 33; + Arrays.fill(CHARS, 2602, 2609, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2609] = 33; + Arrays.fill(CHARS, 2610, 2612, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2612] = 33; + Arrays.fill(CHARS, 2613, 2615, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2615] = 33; + Arrays.fill(CHARS, 2616, 2618, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2618, 2620, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2620] = -87; + CHARS[2621] = 33; + Arrays.fill(CHARS, 2622, 2627, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 2627, 2631, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2631, 2633, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2633, 2635, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2635, 2638, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2638, 2649, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 2649, 2653, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[2653] = 33; + CHARS[2654] = -19; + Arrays.fill(CHARS, 2655, 2662, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 2662, 2674, (byte) -87 ); // Fill 12 of value (byte) -87 + Arrays.fill(CHARS, 2674, 2677, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2677, 2689, (byte) 33 ); // Fill 12 of value (byte) 33 + Arrays.fill(CHARS, 2689, 2692, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2692] = 33; + Arrays.fill(CHARS, 2693, 2700, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2700] = 33; + CHARS[2701] = -19; + CHARS[2702] = 33; + Arrays.fill(CHARS, 2703, 2706, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[2706] = 33; + Arrays.fill(CHARS, 2707, 2729, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2729] = 33; + Arrays.fill(CHARS, 2730, 2737, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2737] = 33; + Arrays.fill(CHARS, 2738, 2740, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2740] = 33; + Arrays.fill(CHARS, 2741, 2746, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 2746, 2748, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2748] = -87; + CHARS[2749] = -19; + Arrays.fill(CHARS, 2750, 2758, (byte) -87 ); // Fill 8 of value (byte) -87 + CHARS[2758] = 33; + Arrays.fill(CHARS, 2759, 2762, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2762] = 33; + Arrays.fill(CHARS, 2763, 2766, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2766, 2784, (byte) 33 ); // Fill 18 of value (byte) 33 + CHARS[2784] = -19; + Arrays.fill(CHARS, 2785, 2790, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 2790, 2800, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2800, 2817, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 2817, 2820, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2820] = 33; + Arrays.fill(CHARS, 2821, 2829, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 2829, 2831, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2831, 2833, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2833, 2835, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2835, 2857, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2857] = 33; + Arrays.fill(CHARS, 2858, 2865, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2865] = 33; + Arrays.fill(CHARS, 2866, 2868, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2868, 2870, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2870, 2874, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2874, 2876, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2876] = -87; + CHARS[2877] = -19; + Arrays.fill(CHARS, 2878, 2884, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 2884, 2887, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2887, 2889, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2889, 2891, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2891, 2894, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2894, 2902, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 2902, 2904, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2904, 2908, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2908, 2910, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2910] = 33; + Arrays.fill(CHARS, 2911, 2914, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2914, 2918, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2918, 2928, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2928, 2946, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 2946, 2948, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[2948] = 33; + Arrays.fill(CHARS, 2949, 2955, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 2955, 2958, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2958, 2961, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[2961] = 33; + Arrays.fill(CHARS, 2962, 2966, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2966, 2969, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2969, 2971, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2971] = 33; + CHARS[2972] = -19; + CHARS[2973] = 33; + Arrays.fill(CHARS, 2974, 2976, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2976, 2979, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2979, 2981, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2981, 2984, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2984, 2987, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2987, 2990, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2990, 2998, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[2998] = 33; + Arrays.fill(CHARS, 2999, 3002, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 3002, 3006, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3006, 3011, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 3011, 3014, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 3014, 3017, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3017] = 33; + Arrays.fill(CHARS, 3018, 3022, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3022, 3031, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[3031] = -87; + Arrays.fill(CHARS, 3032, 3047, (byte) 33 ); // Fill 15 of value (byte) 33 + Arrays.fill(CHARS, 3047, 3056, (byte) -87 ); // Fill 9 of value (byte) -87 + Arrays.fill(CHARS, 3056, 3073, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 3073, 3076, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3076] = 33; + Arrays.fill(CHARS, 3077, 3085, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3085] = 33; + Arrays.fill(CHARS, 3086, 3089, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3089] = 33; + Arrays.fill(CHARS, 3090, 3113, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3113] = 33; + Arrays.fill(CHARS, 3114, 3124, (byte) -19 ); // Fill 10 of value (byte) -19 + CHARS[3124] = 33; + Arrays.fill(CHARS, 3125, 3130, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 3130, 3134, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3134, 3141, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[3141] = 33; + Arrays.fill(CHARS, 3142, 3145, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3145] = 33; + Arrays.fill(CHARS, 3146, 3150, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3150, 3157, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3157, 3159, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3159, 3168, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 3168, 3170, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3170, 3174, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3174, 3184, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3184, 3202, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 3202, 3204, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3204] = 33; + Arrays.fill(CHARS, 3205, 3213, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3213] = 33; + Arrays.fill(CHARS, 3214, 3217, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3217] = 33; + Arrays.fill(CHARS, 3218, 3241, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3241] = 33; + Arrays.fill(CHARS, 3242, 3252, (byte) -19 ); // Fill 10 of value (byte) -19 + CHARS[3252] = 33; + Arrays.fill(CHARS, 3253, 3258, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 3258, 3262, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3262, 3269, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[3269] = 33; + Arrays.fill(CHARS, 3270, 3273, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3273] = 33; + Arrays.fill(CHARS, 3274, 3278, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3278, 3285, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3285, 3287, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3287, 3294, (byte) 33 ); // Fill 7 of value (byte) 33 + CHARS[3294] = -19; + CHARS[3295] = 33; + Arrays.fill(CHARS, 3296, 3298, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3298, 3302, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3302, 3312, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3312, 3330, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 3330, 3332, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3332] = 33; + Arrays.fill(CHARS, 3333, 3341, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3341] = 33; + Arrays.fill(CHARS, 3342, 3345, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3345] = 33; + Arrays.fill(CHARS, 3346, 3369, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3369] = 33; + Arrays.fill(CHARS, 3370, 3386, (byte) -19 ); // Fill 16 of value (byte) -19 + Arrays.fill(CHARS, 3386, 3390, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3390, 3396, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3396, 3398, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3398, 3401, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3401] = 33; + Arrays.fill(CHARS, 3402, 3406, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3406, 3415, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[3415] = -87; + Arrays.fill(CHARS, 3416, 3424, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 3424, 3426, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3426, 3430, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3430, 3440, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3440, 3585, (byte) 33 ); // Fill 145 of value (byte) 33 + Arrays.fill(CHARS, 3585, 3631, (byte) -19 ); // Fill 46 of value (byte) -19 + CHARS[3631] = 33; + CHARS[3632] = -19; + CHARS[3633] = -87; + Arrays.fill(CHARS, 3634, 3636, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3636, 3643, (byte) -87 ); // Fill 7 of value (byte) -87 + Arrays.fill(CHARS, 3643, 3648, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 3648, 3654, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 3654, 3663, (byte) -87 ); // Fill 9 of value (byte) -87 + CHARS[3663] = 33; + Arrays.fill(CHARS, 3664, 3674, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3674, 3713, (byte) 33 ); // Fill 39 of value (byte) 33 + Arrays.fill(CHARS, 3713, 3715, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3715] = 33; + CHARS[3716] = -19; + Arrays.fill(CHARS, 3717, 3719, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3719, 3721, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3721] = 33; + CHARS[3722] = -19; + Arrays.fill(CHARS, 3723, 3725, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[3725] = -19; + Arrays.fill(CHARS, 3726, 3732, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 3732, 3736, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[3736] = 33; + Arrays.fill(CHARS, 3737, 3744, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[3744] = 33; + Arrays.fill(CHARS, 3745, 3748, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3748] = 33; + CHARS[3749] = -19; + CHARS[3750] = 33; + CHARS[3751] = -19; + Arrays.fill(CHARS, 3752, 3754, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3754, 3756, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3756] = 33; + Arrays.fill(CHARS, 3757, 3759, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3759] = 33; + CHARS[3760] = -19; + CHARS[3761] = -87; + Arrays.fill(CHARS, 3762, 3764, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3764, 3770, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[3770] = 33; + Arrays.fill(CHARS, 3771, 3773, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3773] = -19; + Arrays.fill(CHARS, 3774, 3776, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3776, 3781, (byte) -19 ); // Fill 5 of value (byte) -19 + CHARS[3781] = 33; + CHARS[3782] = -87; + CHARS[3783] = 33; + Arrays.fill(CHARS, 3784, 3790, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3790, 3792, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3792, 3802, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3802, 3864, (byte) 33 ); // Fill 62 of value (byte) 33 + Arrays.fill(CHARS, 3864, 3866, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3866, 3872, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 3872, 3882, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3882, 3893, (byte) 33 ); // Fill 11 of value (byte) 33 + CHARS[3893] = -87; + CHARS[3894] = 33; + CHARS[3895] = -87; + CHARS[3896] = 33; + CHARS[3897] = -87; + Arrays.fill(CHARS, 3898, 3902, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3902, 3904, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3904, 3912, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3912] = 33; + Arrays.fill(CHARS, 3913, 3946, (byte) -19 ); // Fill 33 of value (byte) -19 + Arrays.fill(CHARS, 3946, 3953, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3953, 3973, (byte) -87 ); // Fill 20 of value (byte) -87 + CHARS[3973] = 33; + Arrays.fill(CHARS, 3974, 3980, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3980, 3984, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3984, 3990, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[3990] = 33; + CHARS[3991] = -87; + CHARS[3992] = 33; + Arrays.fill(CHARS, 3993, 4014, (byte) -87 ); // Fill 21 of value (byte) -87 + Arrays.fill(CHARS, 4014, 4017, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4017, 4024, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[4024] = 33; + CHARS[4025] = -87; + Arrays.fill(CHARS, 4026, 4256, (byte) 33 ); // Fill 230 of value (byte) 33 + Arrays.fill(CHARS, 4256, 4294, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 4294, 4304, (byte) 33 ); // Fill 10 of value (byte) 33 + Arrays.fill(CHARS, 4304, 4343, (byte) -19 ); // Fill 39 of value (byte) -19 + Arrays.fill(CHARS, 4343, 4352, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[4352] = -19; + CHARS[4353] = 33; + Arrays.fill(CHARS, 4354, 4356, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4356] = 33; + Arrays.fill(CHARS, 4357, 4360, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[4360] = 33; + CHARS[4361] = -19; + CHARS[4362] = 33; + Arrays.fill(CHARS, 4363, 4365, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4365] = 33; + Arrays.fill(CHARS, 4366, 4371, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 4371, 4412, (byte) 33 ); // Fill 41 of value (byte) 33 + CHARS[4412] = -19; + CHARS[4413] = 33; + CHARS[4414] = -19; + CHARS[4415] = 33; + CHARS[4416] = -19; + Arrays.fill(CHARS, 4417, 4428, (byte) 33 ); // Fill 11 of value (byte) 33 + CHARS[4428] = -19; + CHARS[4429] = 33; + CHARS[4430] = -19; + CHARS[4431] = 33; + CHARS[4432] = -19; + Arrays.fill(CHARS, 4433, 4436, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4436, 4438, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4438, 4441, (byte) 33 ); // Fill 3 of value (byte) 33 + CHARS[4441] = -19; + Arrays.fill(CHARS, 4442, 4447, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 4447, 4450, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[4450] = 33; + CHARS[4451] = -19; + CHARS[4452] = 33; + CHARS[4453] = -19; + CHARS[4454] = 33; + CHARS[4455] = -19; + CHARS[4456] = 33; + CHARS[4457] = -19; + Arrays.fill(CHARS, 4458, 4461, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4461, 4463, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4463, 4466, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4466, 4468, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4468] = 33; + CHARS[4469] = -19; + Arrays.fill(CHARS, 4470, 4510, (byte) 33 ); // Fill 40 of value (byte) 33 + CHARS[4510] = -19; + Arrays.fill(CHARS, 4511, 4520, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[4520] = -19; + Arrays.fill(CHARS, 4521, 4523, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[4523] = -19; + Arrays.fill(CHARS, 4524, 4526, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 4526, 4528, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4528, 4535, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 4535, 4537, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4537] = 33; + CHARS[4538] = -19; + CHARS[4539] = 33; + Arrays.fill(CHARS, 4540, 4547, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 4547, 4587, (byte) 33 ); // Fill 40 of value (byte) 33 + CHARS[4587] = -19; + Arrays.fill(CHARS, 4588, 4592, (byte) 33 ); // Fill 4 of value (byte) 33 + CHARS[4592] = -19; + Arrays.fill(CHARS, 4593, 4601, (byte) 33 ); // Fill 8 of value (byte) 33 + CHARS[4601] = -19; + Arrays.fill(CHARS, 4602, 7680, (byte) 33 ); // Fill 3078 of value (byte) 33 + Arrays.fill(CHARS, 7680, 7836, (byte) -19 ); // Fill 156 of value (byte) -19 + Arrays.fill(CHARS, 7836, 7840, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 7840, 7930, (byte) -19 ); // Fill 90 of value (byte) -19 + Arrays.fill(CHARS, 7930, 7936, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 7936, 7958, (byte) -19 ); // Fill 22 of value (byte) -19 + Arrays.fill(CHARS, 7958, 7960, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 7960, 7966, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 7966, 7968, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 7968, 8006, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 8006, 8008, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8008, 8014, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 8014, 8016, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8016, 8024, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[8024] = 33; + CHARS[8025] = -19; + CHARS[8026] = 33; + CHARS[8027] = -19; + CHARS[8028] = 33; + CHARS[8029] = -19; + CHARS[8030] = 33; + Arrays.fill(CHARS, 8031, 8062, (byte) -19 ); // Fill 31 of value (byte) -19 + Arrays.fill(CHARS, 8062, 8064, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8064, 8117, (byte) -19 ); // Fill 53 of value (byte) -19 + CHARS[8117] = 33; + Arrays.fill(CHARS, 8118, 8125, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[8125] = 33; + CHARS[8126] = -19; + Arrays.fill(CHARS, 8127, 8130, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8130, 8133, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[8133] = 33; + Arrays.fill(CHARS, 8134, 8141, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 8141, 8144, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8144, 8148, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 8148, 8150, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8150, 8156, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 8156, 8160, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 8160, 8173, (byte) -19 ); // Fill 13 of value (byte) -19 + Arrays.fill(CHARS, 8173, 8178, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 8178, 8181, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[8181] = 33; + Arrays.fill(CHARS, 8182, 8189, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 8189, 8400, (byte) 33 ); // Fill 211 of value (byte) 33 + Arrays.fill(CHARS, 8400, 8413, (byte) -87 ); // Fill 13 of value (byte) -87 + Arrays.fill(CHARS, 8413, 8417, (byte) 33 ); // Fill 4 of value (byte) 33 + CHARS[8417] = -87; + Arrays.fill(CHARS, 8418, 8486, (byte) 33 ); // Fill 68 of value (byte) 33 + CHARS[8486] = -19; + Arrays.fill(CHARS, 8487, 8490, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8490, 8492, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 8492, 8494, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[8494] = -19; + Arrays.fill(CHARS, 8495, 8576, (byte) 33 ); // Fill 81 of value (byte) 33 + Arrays.fill(CHARS, 8576, 8579, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 8579, 12293, (byte) 33 ); // Fill 3714 of value (byte) 33 + CHARS[12293] = -87; + CHARS[12294] = 33; + CHARS[12295] = -19; + Arrays.fill(CHARS, 12296, 12321, (byte) 33 ); // Fill 25 of value (byte) 33 + Arrays.fill(CHARS, 12321, 12330, (byte) -19 ); // Fill 9 of value (byte) -19 + Arrays.fill(CHARS, 12330, 12336, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[12336] = 33; + Arrays.fill(CHARS, 12337, 12342, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 12342, 12353, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 12353, 12437, (byte) -19 ); // Fill 84 of value (byte) -19 + Arrays.fill(CHARS, 12437, 12441, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 12441, 12443, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 12443, 12445, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 12445, 12447, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 12447, 12449, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 12449, 12539, (byte) -19 ); // Fill 90 of value (byte) -19 + CHARS[12539] = 33; + Arrays.fill(CHARS, 12540, 12543, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 12543, 12549, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 12549, 12589, (byte) -19 ); // Fill 40 of value (byte) -19 + Arrays.fill(CHARS, 12589, 19968, (byte) 33 ); // Fill 7379 of value (byte) 33 + Arrays.fill(CHARS, 19968, 40870, (byte) -19 ); // Fill 20902 of value (byte) -19 + Arrays.fill(CHARS, 40870, 44032, (byte) 33 ); // Fill 3162 of value (byte) 33 + Arrays.fill(CHARS, 44032, 55204, (byte) -19 ); // Fill 11172 of value (byte) -19 + Arrays.fill(CHARS, 55204, 55296, (byte) 33 ); // Fill 92 of value (byte) 33 + Arrays.fill(CHARS, 57344, 65534, (byte) 33 ); // Fill 8190 of value (byte) 33 + + } // () + + // + // Public static methods + // + + /** + * Returns true if the specified character is a supplemental character. + * + * @param c The character to check. + */ + public static boolean isSupplemental(int c) { + return (c >= 0x10000 && c <= 0x10FFFF); + } + + /** + * Returns true the supplemental character corresponding to the given + * surrogates. + * + * @param h The high surrogate. + * @param l The low surrogate. + */ + public static int supplemental(char h, char l) { + return (h - 0xD800) * 0x400 + (l - 0xDC00) + 0x10000; + } + + /** + * Returns the high surrogate of a supplemental character + * + * @param c The supplemental character to "split". + */ + public static char highSurrogate(int c) { + return (char) (((c - 0x00010000) >> 10) + 0xD800); + } + + /** + * Returns the low surrogate of a supplemental character + * + * @param c The supplemental character to "split". + */ + public static char lowSurrogate(int c) { + return (char) (((c - 0x00010000) & 0x3FF) + 0xDC00); + } + + /** + * Returns whether the given character is a high surrogate + * + * @param c The character to check. + */ + public static boolean isHighSurrogate(int c) { + return (0xD800 <= c && c <= 0xDBFF); + } + + /** + * Returns whether the given character is a low surrogate + * + * @param c The character to check. + */ + public static boolean isLowSurrogate(int c) { + return (0xDC00 <= c && c <= 0xDFFF); + } + + + /** + * Returns true if the specified character is valid. This method + * also checks the surrogate character range from 0x10000 to 0x10FFFF. + *

        + * If the program chooses to apply the mask directly to the + * CHARS array, then they are responsible for checking + * the surrogate character range. + * + * @param c The character to check. + */ + public static boolean isValid(int c) { + return (c < 0x10000 && (CHARS[c] & MASK_VALID) != 0) || + (0x10000 <= c && c <= 0x10FFFF); + } // isValid(int):boolean + + /** + * Returns true if the specified character is invalid. + * + * @param c The character to check. + */ + public static boolean isInvalid(int c) { + return !isValid(c); + } // isInvalid(int):boolean + + /** + * Returns true if the specified character can be considered content. + * + * @param c The character to check. + */ + public static boolean isContent(int c) { + return (c < 0x10000 && (CHARS[c] & MASK_CONTENT) != 0) || + (0x10000 <= c && c <= 0x10FFFF); + } // isContent(int):boolean + + /** + * Returns true if the specified character can be considered markup. + * Markup characters include '<', '&', and '%'. + * + * @param c The character to check. + */ + public static boolean isMarkup(int c) { + return c == '<' || c == '&' || c == '%'; + } // isMarkup(int):boolean + + /** + * Returns true if the specified character is a space character + * as defined by production [3] in the XML 1.0 specification. + * + * @param c The character to check. + */ + public static boolean isSpace(int c) { + return c <= 0x20 && (CHARS[c] & MASK_SPACE) != 0; + } // isSpace(int):boolean + + /** + * Returns true if the specified character is a valid name start + * character as defined by production [5] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isNameStart(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NAME_START) != 0; + } // isNameStart(int):boolean + + /** + * Returns true if the specified character is a valid name + * character as defined by production [4] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isName(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NAME) != 0; + } // isName(int):boolean + + /** + * Returns true if the specified character is a valid NCName start + * character as defined by production [4] in Namespaces in XML + * recommendation. + * + * @param c The character to check. + */ + public static boolean isNCNameStart(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NCNAME_START) != 0; + } // isNCNameStart(int):boolean + + /** + * Returns true if the specified character is a valid NCName + * character as defined by production [5] in Namespaces in XML + * recommendation. + * + * @param c The character to check. + */ + public static boolean isNCName(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NCNAME) != 0; + } // isNCName(int):boolean + + /** + * Returns true if the specified character is a valid Pubid + * character as defined by production [13] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isPubid(int c) { + return c < 0x10000 && (CHARS[c] & MASK_PUBID) != 0; + } // isPubid(int):boolean + + /* + * [5] Name ::= (Letter | '_' | ':') (NameChar)* + */ + /** + * Check to see if a string is a valid Name according to [5] + * in the XML 1.0 Recommendation + * + * @param name string to check + * @return true if name is a valid Name + */ + public static boolean isValidName(String name) { + if (name.length() == 0) + return false; + char ch = name.charAt(0); + if( isNameStart(ch) == false) + return false; + for (int i = 1; i < name.length(); i++ ) { + ch = name.charAt(i); + if( isName( ch ) == false ){ + return false; + } + } + return true; + } // isValidName(String):boolean + + + /* + * from the namespace rec + * [4] NCName ::= (Letter | '_') (NCNameChar)* + */ + /** + * Check to see if a string is a valid NCName according to [4] + * from the XML Namespaces 1.0 Recommendation + * + * @param ncName string to check + * @return true if name is a valid NCName + */ + public static boolean isValidNCName(String ncName) { + if (ncName.length() == 0) + return false; + char ch = ncName.charAt(0); + if( isNCNameStart(ch) == false) + return false; + for (int i = 1; i < ncName.length(); i++ ) { + ch = ncName.charAt(i); + if( isNCName( ch ) == false ){ + return false; + } + } + return true; + } // isValidNCName(String):boolean + + /* + * [7] Nmtoken ::= (NameChar)+ + */ + /** + * Check to see if a string is a valid Nmtoken according to [7] + * in the XML 1.0 Recommendation + * + * @param nmtoken string to check + * @return true if nmtoken is a valid Nmtoken + */ + public static boolean isValidNmtoken(String nmtoken) { + if (nmtoken.length() == 0) + return false; + for (int i = 0; i < nmtoken.length(); i++ ) { + char ch = nmtoken.charAt(i); + if( ! isName( ch ) ){ + return false; + } + } + return true; + } // isValidName(String):boolean + + + + + + // encodings + + /** + * Returns true if the encoding name is a valid IANA encoding. + * This method does not verify that there is a decoder available + * for this encoding, only that the characters are valid for an + * IANA encoding name. + * + * @param ianaEncoding The IANA encoding name. + */ + public static boolean isValidIANAEncoding(String ianaEncoding) { + if (ianaEncoding != null) { + int length = ianaEncoding.length(); + if (length > 0) { + char c = ianaEncoding.charAt(0); + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + for (int i = 1; i < length; i++) { + c = ianaEncoding.charAt(i); + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && + (c < '0' || c > '9') && c != '.' && c != '_' && + c != '-') { + return false; + } + } + return true; + } + } + } + return false; + } // isValidIANAEncoding(String):boolean + + /** + * Returns true if the encoding name is a valid Java encoding. + * This method does not verify that there is a decoder available + * for this encoding, only that the characters are valid for an + * Java encoding name. + * + * @param javaEncoding The Java encoding name. + */ + public static boolean isValidJavaEncoding(String javaEncoding) { + if (javaEncoding != null) { + int length = javaEncoding.length(); + if (length > 0) { + for (int i = 1; i < length; i++) { + char c = javaEncoding.charAt(i); + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && + (c < '0' || c > '9') && c != '.' && c != '_' && + c != '-') { + return false; + } + } + return true; + } + } + return false; + } // isValidIANAEncoding(String):boolean + + +} // class XMLChar diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractImportXmlTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractImportXmlTest.java new file mode 100644 index 00000000000..50a15b2b93f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractImportXmlTest.java @@ -0,0 +1,498 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Attr; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Node; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.RepositoryException; +import javax.jcr.PathNotFoundException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; +import java.util.StringTokenizer; +import java.io.File; +import java.io.FileInputStream; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; + +/** + * AbstractImportXmlTest Provides names, data and methods to create + * xml documents and referenceable nodes for the tests of document view import + * methods of Workspace and Session. + */ +abstract class AbstractImportXmlTest extends AbstractJCRTest { + + protected final boolean WORKSPACE = true; + protected final boolean SESSION = false; + protected boolean CONTENTHANDLER = true; + protected boolean STREAM = false; + + // the absolute path to the target nodes for the imports + protected String target; + protected String refTarget; + + protected Session session; + protected Workspace workspace; + protected NodeTypeManager ntManager; + protected NamespaceRegistry nsp; + + protected String ntUnstructured; + + protected File file; + + // some node names + protected String referenced; + protected String referencing; + + // the target nodes + protected Node targetNode; + protected Node refTargetNode; + + protected String unusedPrefix; + protected String unusedURI; + + // names for namespace import + protected final String TEST_PREFIX = "docview"; + protected final String TEST_URI = "http://www.apache.org/jackrabbit/test/namespaceImportTest"; + protected final String XML_NS = "xmlns"; + + // xml document related names + protected static final String rootElem = "docRoot"; + protected static final String refNodeElem = "refNodeElem"; + protected static final String xmltextElem = "xmltextElem"; + protected static final String childElem = "childElem"; + protected static final String grandChildElem = "grandChildElem"; + + protected static final String encodedElemName = "Element_x003C__x003E_Name"; + protected static final String decodedElemName = "Element<>Name"; + + protected static final String attributeName = "attribute"; + protected static final String attributeValue = "attrVal"; + + protected static final String encodedAttributeName = "Prop_x0020_Name"; + protected static final String decodedAttributeName = "Prop Name"; + protected static final String encodedAttributeValue = "Hello_x0009_&_x0009_GoodBye"; + protected static final String decodedAttributeValue = "Hello\t&\tGoodBye"; + + //String value for the test with leading and trailing spaces and entity reference charachters + protected String xmltext = "\t Text for docView Export test_x0009_with escaped _x003C_ characters. "; + + // is semantic of mix:referenceable respected? + protected boolean respectMixRef = false; + + // default uuidBehaviour for the tests + protected int uuidBehaviour = ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW; + + protected DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + protected DocumentBuilder dom; + + /** + * Sets up the fixture for the test cases. + */ + public void setUp() throws Exception { + super.setUp(); + + try { + dom = factory.newDocumentBuilder(); + file = File.createTempFile("docViewImportTest", ".xml"); + log.print("Tempfile: " + file.getAbsolutePath()); + session = superuser; + workspace = session.getWorkspace(); + // create the target nodes for the imports + target = testRoot + "/target"; + targetNode = createAncestors(target); + refTarget = testRoot + "/refTarget"; + refTargetNode = createAncestors(refTarget); + + nsp = workspace.getNamespaceRegistry(); + ntManager = workspace.getNodeTypeManager(); + + // construct a namespace not existing in the repository + unusedPrefix = getUnusedPrefix(); + unusedURI = getUnusedURI(); + referenced = nodeName1; + referencing = nodeName2; + // test if jcr:uuid of mix:referenceable node type is respected + respectMixRef = isMixRefRespected(); + } + catch (Exception ex) { + if (file != null) { + file.delete(); + file = null; + } + throw (ex); + } + } + + public void tearDown() throws Exception { + if (file != null) { + file.delete(); + file = null; + } + session = null; + workspace = null; + ntManager = null; + nsp = null; + targetNode = null; + refTargetNode = null; + super.tearDown(); + } + + /** + * Creates a document with some nodes and props for Namespace adding test + * and for correct tree structure tests after having imported. + * + * @return + */ + public Document createSimpleDocument() { + Document doc = dom.newDocument(); + Element root = doc.createElementNS(unusedURI, unusedPrefix + ":" + rootElem); + root.setAttribute(XML_NS + ":" + unusedPrefix, unusedURI); + Element child = doc.createElement(childElem); + Element xmlElem = doc.createElement(xmltextElem); + Element encoded = doc.createElement(encodedElemName); + Element grandchild = doc.createElement(grandChildElem); + + Attr attr = doc.createAttribute(attributeName); + attr.setValue(attributeValue); + Attr encodedAttr = doc.createAttribute(encodedAttributeName); + encodedAttr.setValue(encodedAttributeValue); + + child.appendChild(encoded); + child.setAttributeNode(encodedAttr); + + grandchild.setAttributeNode(attr); + + xmlElem.appendChild(doc.createTextNode(xmltext)); + xmlElem.appendChild(grandchild); + xmlElem.setAttribute(attributeName, attributeValue); + + root.appendChild(child); + root.appendChild(xmlElem); + + doc.appendChild(root); + return doc; + } + + /** + * Imports a given document using either Workspace.importXML or + * Session.importXML method. + * + * @param absPath the absPath to the parent node where to import the + * document + * @param document the document to import + * @param uuidBehaviour how the uuid collisions should be handled + * @param withWorkspace if workspace or session interface should be used + * @throws RepositoryException + * @throws IOException + */ + protected void importXML(String absPath, Document document, + int uuidBehaviour, boolean withWorkspace) + throws RepositoryException, IOException { + serialize(document); + BufferedInputStream bin = new BufferedInputStream(new FileInputStream(file)); + try { + if (withWorkspace) { + workspace.importXML(absPath, bin, uuidBehaviour); + } else { + session.importXML(absPath, bin, uuidBehaviour); + session.save(); + } + } finally { + try { bin.close(); } catch (IOException ignore) {} + } + } + + /** + * Imports a given document using the ContentHandler received either with + * Workspace.getImportContentHandler or Session.getImportContentHandler. + * This handler is then passed to a XML parser which parses the given + * document. + * + * @param absPath the absPath to the parent node where to import the + * document + * @param document the document to import + * @param uuidBehaviour how the uuid collisions should be handled + * @param withWorkspace if workspace or session interface should be used + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + public void importWithHandler(String absPath, Document document, + int uuidBehaviour, boolean withWorkspace) + throws Exception { + + serialize(document); + BufferedInputStream bin = new BufferedInputStream(new FileInputStream(file)); + + try { + ContentHandler handler; + if (withWorkspace) { + handler = workspace.getImportContentHandler(absPath, uuidBehaviour); + } else { + handler = session.getImportContentHandler(absPath, uuidBehaviour); + } + + XMLReader reader = XMLReaderFactory.createXMLReader(); + reader.setFeature("http://xml.org/sax/features/namespaces", true); + reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); + + reader.setContentHandler(handler); + reader.parse(new InputSource(bin)); + + if (!withWorkspace) { + session.save(); + } + } + finally { + bin.close(); + } + } + +//--------------------------------< helpers >----------------------------------- + /** + * Tests if jcr:uuid property of mix:referenceable nodetype is respected. + * This is believed as true when during import with uuidBehaviour + * IMPORT_UUID_COLLISION_REMOVE_EXISTING a node with the same uuid as a node + * to be imported will be deleted. + * + * @return + * @throws RepositoryException + * @throws IOException + */ + public boolean isMixRefRespected() throws RepositoryException, IOException { + boolean respected = false; + if (supportsNodeType(mixReferenceable)) { + String uuid; + try { + uuid = createReferenceableNode(referenced); + } catch (NotExecutableException e) { + return false; + } + Document document = dom.newDocument(); + Element root = document.createElement(rootElem); + root.setAttribute(XML_NS + ":jcr", NS_JCR_URI); + root.setAttributeNS(NS_JCR_URI, jcrUUID, uuid); + root.setAttributeNS(NS_JCR_URI, jcrMixinTypes, mixReferenceable); + document.appendChild(root); + + importXML(refTargetNode.getPath(), document, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING, SESSION); + session.save(); + + // existing node with same uuid should now be deleted + respected = !testRootNode.hasNode(referenced); + + // check if imported document node is referenceable + Node rootNode = refTargetNode.getNode(rootElem); + respected &= rootNode.isNodeType(mixReferenceable); + } + return respected; + } + + /** + * Creates a node with given name below the testRootNode which will be + * referenced by the node nodeName2 and returns the UUID assigned to the + * created node. + * + * @param name + * @return + * @throws RepositoryException + * @throws NotExecutableException if the created node is not referenceable + * and cannot be made referenceable by adding mix:referenceable. + */ + public String createReferenceableNode(String name) throws RepositoryException, NotExecutableException { + // remove a yet existing node at the target + try { + Node node = testRootNode.getNode(name); + node.remove(); + session.save(); + } catch (PathNotFoundException pnfe) { + // ok + } + // a referenceable node + Node n1 = testRootNode.addNode(name, testNodeType); + if (!n1.isNodeType(mixReferenceable) && !n1.canAddMixin(mixReferenceable)) { + n1.remove(); + session.save(); + throw new NotExecutableException("node type " + testNodeType + + " does not support mix:referenceable"); + } + n1.addMixin(mixReferenceable); + // make sure jcr:uuid is available + testRootNode.getSession().save(); + return n1.getUUID(); + } + + /** + * Creates a document with a element rootElem containing a jcr:uuid + * attribute with the given uuid as value. This document is imported below + * the node with path absPath. If nod node at absPth it is created. + * If there is yet a node rootElem below the then this node is + * romoved in advance. + * + * @param uuid + * @param uuidBehaviour + * @param withWorkspace + * @param withHandler + * @throws RepositoryException + * @throws IOException + */ + public void importRefNodeDocument( + String absPath, String uuid, int uuidBehaviour, + boolean withWorkspace, boolean withHandler) + throws Exception { + + Document document = dom.newDocument(); + Element root = document.createElement(rootElem); + root.setAttribute(XML_NS + ":jcr", NS_JCR_URI); + root.setAttributeNS(NS_JCR_URI, jcrUUID, uuid); + root.setAttributeNS(NS_JCR_URI, jcrMixinTypes, mixReferenceable); + root.setAttribute(propertyName1, "some text"); + document.appendChild(root); + + Node targetNode = null; + try { + targetNode = (Node) session.getItem(absPath); + // remove a yet existing node at the target + try { + Node node = targetNode.getNode(rootElem); + node.remove(); + session.save(); + } catch (PathNotFoundException pnfe) { + // ok + } + } catch (PathNotFoundException pnfe) { + // create the node + targetNode = createAncestors(absPath); + } + + if (withHandler) { + importWithHandler(targetNode.getPath(), document, uuidBehaviour, withWorkspace); + } else { + importXML(targetNode.getPath(), document, uuidBehaviour, withWorkspace); + } + session.save(); + } + + protected Node createAncestors(String absPath) throws RepositoryException { + // create nodes to name of absPath + Node root = session.getRootNode(); + StringTokenizer names = new StringTokenizer(absPath, "/"); + Node currentNode = root; + while (names.hasMoreTokens()) { + String name = names.nextToken(); + if (currentNode.hasNode(name)) { + currentNode = currentNode.getNode(name); + } else { + currentNode = currentNode.addNode(name); + } + } + root.save(); + return currentNode; + } + + public void serialize(Document document) throws IOException { + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); + try { + // disable pretty printing/default line wrapping! + Transformer t = TransformerFactory.newInstance().newTransformer(); + t.setOutputProperty(OutputKeys.METHOD, "xml"); + t.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + t.setOutputProperty(OutputKeys.INDENT, "no"); + Source s = new DOMSource(document); + Result r = new StreamResult(bos); + t.transform(s, r); + } catch (TransformerException te) { + throw (IOException) new IOException(te.getMessage()).initCause(te); + } finally { + bos.close(); + } + } + + public boolean supportsNodeType(String ntName) throws RepositoryException { + boolean support = false; + try { + ntManager.getNodeType(ntName); + support = true; + } catch (NoSuchNodeTypeException nste) { + // + } + return support; + } + + /** + * Returns a namespace prefix that currently not used in the namespace + * registry. + * + * @return an unused namespace prefix. + */ + protected String getUnusedPrefix() throws RepositoryException { + Set prefixes = new HashSet(Arrays.asList(nsp.getPrefixes())); + String prefix = TEST_PREFIX; + int i = 0; + while (prefixes.contains(prefix)) { + prefix = TEST_PREFIX + i++; + } + return prefix; + } + + /** + * Returns a namespace URI that currently not used in the namespace + * registry. + * + * @return an unused namespace URI. + */ + protected String getUnusedURI() throws RepositoryException { + Set uris = new HashSet(Arrays.asList(nsp.getURIs())); + String uri = TEST_URI; + int i = 0; + while (uris.contains(uri)) { + uri = TEST_URI + i++; + } + return uri; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractPropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractPropertyTest.java new file mode 100644 index 00000000000..95ea59d387a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractPropertyTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Session; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.Value; + +/** + * Provides the common setup method for all level 1 property tests. + */ +abstract class AbstractPropertyTest extends AbstractJCRTest { + + /** String encoding in a stream */ + protected static String UTF8 = "UTF-8"; + + /** A read only session */ + protected Session session; + + /* The property under test */ + protected Property prop; + + /** true if the property is multi valued */ + protected boolean multiple; + + /** + * Concrete subclasses return the type of property they test. One of the + * values defined in {@link javax.jcr.PropertyType}. + */ + protected abstract int getPropertyType(); + + /** + * Concrete subclasses return the multivalued-ness of property they test. + * (null: does not matter) + */ + protected abstract Boolean getPropertyIsMultivalued(); + + /** + * Sets up the fixture for the tests. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + + prop = PropertyUtil.searchProp(session, session.getRootNode().getNode(testPath), getPropertyType(), getPropertyIsMultivalued()); + if (prop == null) { + cleanUp(); + String msg = "Workspace does not contain a node with a " + + PropertyType.nameFromValue(getPropertyType()) + " property."; + throw new NotExecutableException(msg); + } + multiple = prop.getDefinition().isMultiple(); + Value val = PropertyUtil.getValue(prop); + if (val == null) { + cleanUp(); + String msg = PropertyType.nameFromValue(getPropertyType()) + + " property does not contain a value"; + throw new NotExecutableException(msg); + } + } + + protected void cleanUp() throws Exception { + if (session != null) { + session.logout(); + } + super.cleanUp(); + } + + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + cleanUp(); + session = null; + prop = null; + super.tearDown(); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyBetweenTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyBetweenTest.java new file mode 100644 index 00000000000..d4167c72c81 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyBetweenTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; + +/** + * AbstractWorkspaceCopyCloneBetweenWorkspacesTest is the abstract + * base class for all copying and cloning related test classes between + * workspaces. + */ +abstract class AbstractWorkspaceCopyBetweenTest extends AbstractWorkspaceCopyTest { + + /** + * The superuser session for the non default workspace + */ + protected Session superuserW2; + + /** + * A read-write session for the non default workspace + */ + protected Session rwSessionW2; + + /** + * The workspace in the non default session. + */ + Workspace workspaceW2; + + /** + * The testroot node in the non default session + */ + Node testRootNodeW2; + + /** + * A referenceable node in default workspace + */ + protected Node node1W2; + + /** + * A non-referenceable node in default workspace + */ + protected Node node2W2; + + protected void setUp() throws Exception { + super.setUp(); + + // init second workspace + String otherWspName = getOtherWorkspaceName(); + superuserW2 = getHelper().getSuperuserSession(otherWspName); + rwSessionW2 = getHelper().getReadWriteSession(otherWspName); + workspaceW2 = superuserW2.getWorkspace(); + + initNodesW2(); + } + + protected void tearDown() throws Exception { + // remove all test nodes in second workspace + if (superuserW2 != null) { + try { + if (!isReadOnly) { + // do a 'rollback' + cleanUpTestRoot(superuserW2); + } + } finally { + superuserW2.logout(); + superuserW2 = null; + } + } + if (rwSessionW2 != null) { + rwSessionW2.logout(); + rwSessionW2 = null; + } + workspaceW2 = null; + testRootNodeW2 = null; + node1W2 = null; + node2W2 = null; + super.tearDown(); + } + + protected String getOtherWorkspaceName() throws NotExecutableException { + if (workspace.getName().equals(workspaceName)) { + throw new NotExecutableException("Cannot test copy between workspaces. 'workspaceName' points to default workspace as well."); + } + return workspaceName; + } + + protected void initNodesW2() throws RepositoryException { + + // testroot + if (superuserW2.getRootNode().hasNode(testPath)) { + testRootNodeW2 = superuserW2.getRootNode().getNode(testPath); + // clean test root + for (NodeIterator it = testRootNodeW2.getNodes(); it.hasNext(); ) { + it.nextNode().remove(); + } + testRootNodeW2.save(); + } else { + testRootNodeW2 = superuserW2.getRootNode().addNode(testPath, testNodeType); + superuserW2.save(); + } + + // some test nodes + superuserW2.getWorkspace().copy(workspace.getName(), node1.getPath(), node1.getPath()); + node1W2 = testRootNodeW2.getNode(node1.getName()); + + superuserW2.getWorkspace().copy(workspace.getName(), node2.getPath(), node2.getPath()); + node2W2 = testRootNodeW2.getNode(node2.getName()); + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyTest.java similarity index 77% rename from src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyTest.java index 62ad00b807f..22622e3a0d0 100644 --- a/src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceCopyTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -53,6 +53,9 @@ protected void setUp() throws Exception { } protected void tearDown() throws Exception { + node1 = null; + node2 = null; + workspace = null; super.tearDown(); } @@ -70,7 +73,7 @@ private void initNodes() { // create a non-referenceable node try { node2 = testRootNode.addNode(nodeName2, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); } catch (RepositoryException e) { fail("Failed to createtest node." + e.getMessage()); } diff --git a/src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceReferenceableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceReferenceableTest.java similarity index 75% rename from src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceReferenceableTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceReferenceableTest.java index 1a537dacbaa..f8d23d0fc04 100644 --- a/src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceReferenceableTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceReferenceableTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -50,17 +50,13 @@ protected void setUp() throws Exception { /** * add the mix:referenceable mixin type to a node. * - * @param parent * @param node * @return referenceable node. */ - protected Node addMixinReferenceableToNode(Node parent, Node node) throws RepositoryException { - NodeType nodetype = node.getPrimaryNodeType(); - if (!nodetype.isNodeType(mixReferenceable)) { - node.addMixin(mixReferenceable); - } - parent.save(); - + protected Node addMixinReferenceableToNode(Node node) throws RepositoryException, + NotExecutableException { + ensureMixinType(node, mixReferenceable); + node.save(); return node; } } \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceSameNameSibsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceSameNameSibsTest.java new file mode 100644 index 00000000000..733cb3cf454 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceSameNameSibsTest.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AbstractWorkspaceSameNameSibsTest is the abstract base class for + * all copying/moving/cloning related test classes with samename siblings + * allowed in workspace. + */ +abstract class AbstractWorkspaceSameNameSibsTest extends AbstractWorkspaceCopyBetweenTest { + + /** + * Node type with sameNameSibs=true NodeDef + */ + protected final String PROP_SAME_NAME_SIBS_TRUE_NODE_TYPE = "sameNameSibsTrueNodeType"; + + /** + * Node type with sameNameSibs=false NodeDef + */ + protected final String PROP_SAME_NAME_SIBS_FALSE_NODE_TYPE = "sameNameSibsFalseNodeType"; + + /** + * A node type where same-name siblings are allowed + */ + protected NodeType sameNameSibsTrueNodeType; + + /** + * A node type where NO same-name siblings allowed + */ + protected NodeType sameNameSibsFalseNodeType; + + protected void setUp() throws Exception { + super.setUp(); + + // we assume sameNameSibs is supported by repository + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + + // sameNameSibs ALLOWED + // make sure 'sameNameSibsTrue' nodetype is properly defined + try { + sameNameSibsTrueNodeType = ntMgr.getNodeType(getProperty(PROP_SAME_NAME_SIBS_TRUE_NODE_TYPE)); + NodeDefinition[] childNodeDefs = sameNameSibsTrueNodeType.getDeclaredChildNodeDefinitions(); + boolean isSameNameSibs = false; + for (int i = 0; i < childNodeDefs.length; i++) { + if (childNodeDefs[i].allowsSameNameSiblings()) { + isSameNameSibs = true; + break; + } + } + if (!isSameNameSibs) { + throw new NotExecutableException("Property 'sameNameSibsTrueNodeType' does not define a nodetype where sameNameSibs are allowed: '" + sameNameSibsTrueNodeType.getName() + "'"); + } + } catch (NoSuchNodeTypeException e) { + fail("Property 'sameNameSibsTrueNodeType' does not define an existing nodetype: '" + sameNameSibsTrueNodeType + "'"); + } + + // sameNameSibs NOT ALLOWED + // make sure 'sameNameSibsFalse' nodetype is properly defined + try { + sameNameSibsFalseNodeType = ntMgr.getNodeType(getProperty(PROP_SAME_NAME_SIBS_FALSE_NODE_TYPE)); + NodeDefinition[] childNodeDefs = sameNameSibsFalseNodeType.getDeclaredChildNodeDefinitions(); + boolean isSameNameSibs = true; + for (int i = 0; i < childNodeDefs.length; i++) { + if (!childNodeDefs[i].allowsSameNameSiblings()) { + isSameNameSibs = false; + break; + } + } + if (isSameNameSibs) { + fail("Property 'sameNameSibsFalseNodeType' does define a nodetype where sameNameSibs are not allowed: '" + sameNameSibsFalseNodeType.getName() + "'"); + } + } catch (NoSuchNodeTypeException e) { + fail("Property 'sameNameSibsFalseNodeType' does not define an existing nodetype: '" + sameNameSibsFalseNodeType + "'"); + } + + } + + protected void tearDown() throws Exception { + sameNameSibsTrueNodeType = null; + sameNameSibsFalseNodeType = null; + super.tearDown(); + } +} \ No newline at end of file diff --git a/src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceVersionableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceVersionableTest.java similarity index 77% rename from src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceVersionableTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceVersionableTest.java index a1139bd4c00..5befb7b8958 100644 --- a/src/test/org/apache/jackrabbit/test/api/AbstractWorkspaceVersionableTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AbstractWorkspaceVersionableTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -52,11 +52,9 @@ protected void setUp() throws Exception { /** * add the mix:versionable mixin type to a node. */ - protected Node addMixinVersionableToNode(Node parent, Node node) throws RepositoryException { - NodeType nodetype = node.getPrimaryNodeType(); - if (!nodetype.isNodeType(mixVersionable)) { - node.addMixin(mixVersionable); - } + protected Node addMixinVersionableToNode(Node parent, Node node) throws RepositoryException, + NotExecutableException { + ensureMixinType(node, mixVersionable); parent.save(); return node; diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AddNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AddNodeTest.java new file mode 100644 index 00000000000..f65318f86dc --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/AddNodeTest.java @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import java.text.Normalizer; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.ItemExistsException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.Session; + +/** + * AddNodeTest contains the test cases for the method + * Node.addNode(String, String). + * + */ +public class AddNodeTest extends AbstractJCRTest { + + /** + * Tests if the name of the created node is correct. + */ + public void testName() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + assertEquals("Wrong node name.", n1.getName(), nodeName1); + } + + /** + * Tests if the node type of the created node is correct. + */ + public void testNodeType() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + String ntName = n1.getPrimaryNodeType().getName(); + assertEquals("Wrong node NodeType name.", testNodeType, ntName); + } + + /** + * Tests if same name siblings have equal names or if same name + * siblings are not supported a ItemExistsException is thrown. + */ + public void testSameNameSiblings() throws RepositoryException { + if (testRootNode.getDefinition().allowsSameNameSiblings()) { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + assertEquals("Names of same name siblings are not equal.", + n1.getName(), n2.getName()); + } else { + testRootNode.addNode(nodeName1, testNodeType); + try { + testRootNode.addNode(nodeName1, testNodeType); + fail("Expected ItemExistsException."); + } catch (ItemExistsException e) { + // correct + } + } + } + + /** + * Tests if addNode() throws a NoSuchNodeTypeException in case + * of an unknown node type. + */ + public void testUnknownNodeType() throws RepositoryException { + try { + testRootNode.addNode(nodeName1, testNodeType + "unknownSuffix"); + fail("Expected NoSuchNodeTypeException."); + } catch (NoSuchNodeTypeException e) { + // correct. + } + } + + /** + * Tests if addNode() throws a ConstraintViolationException in case + * of an abstract node type. + */ + public void testAbstractNodeType() throws RepositoryException { + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeIterator nts = ntMgr.getPrimaryNodeTypes(); + while (nts.hasNext()) { + NodeType nt = nts.nextNodeType(); + if (nt.isAbstract()) { + try { + testRootNode.addNode(nodeName1, nt.getName()); + superuser.save(); + fail("Expected ConstraintViolationException."); + } catch (ConstraintViolationException e) { + // correct. + } finally { + superuser.refresh(false); + } + } + } + } + + /** + * Tests if addNode() throws a ConstraintViolationException in case + * of an mixin node type. + */ + public void testMixinNodeType() throws RepositoryException, NotExecutableException { + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeIterator nts = ntMgr.getMixinNodeTypes(); + if (nts.hasNext()) { + try { + testRootNode.addNode(nodeName1, nts.nextNodeType().getName()); + superuser.save(); + fail("Expected ConstraintViolationException."); + } catch (ConstraintViolationException e) { + // correct. + } + } else { + throw new NotExecutableException("no mixins."); + } + } + + /** + * Tests if the path of the created node is correct. + */ + public void testPath() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + String expected = testRootNode.getPath() + "/" + nodeName1; + assertEquals("Wrong path for created node.", expected, n1.getPath()); + } + + /** + * Tests if addNode() throws a PathNotFoundException in case + * intermediary nodes do not exist. + */ + public void testPathNotFound() throws RepositoryException { + try { + testRootNode.addNode(nodeName1 + "/" + nodeName1, testNodeType); + fail("Expected PathNotFoundException."); + } catch (PathNotFoundException e) { + // correct. + } + } + + /** + * Tests if a ConstraintViolationException is thrown when one attempts + * to add a node at a path that references a property. + */ + public void testConstraintViolation() throws RepositoryException { + try { + Node rootNode = superuser.getRootNode(); + String propPath = testPath + "/" + jcrPrimaryType; + rootNode.addNode(propPath + "/" + nodeName1, testNodeType); + fail("Expected ConstraintViolationException."); + } catch (ConstraintViolationException e) { + // correct. + } + } + + /** + * Tests if a RepositoryException is thrown in case the path + * for the new node contains an index. + */ + public void testRepositoryException() { + try { + testRootNode.addNode(nodeName1 + "[1]"); + fail("Expected RepositoryException."); + } catch (RepositoryException e) { + // correct. + } + try { + testRootNode.addNode(nodeName1 + "[1]", testNodeType); + fail("Expected RepositoryException."); + } catch (RepositoryException e) { + // correct. + } + } + + /** + * Creates a new node using {@link Node#addNode(String,String)}, saves using + * {@link javax.jcr.Node#save()} on parent node. Uses a second session to + * verify if the node have been saved. + */ + public void testAddNodeParentSave() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // add node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save new node + defaultRootNode.save(); + + // use a different session to verify if the node is there + Session session = getHelper().getReadOnlySession(); + try { + session.getItem(testNode.getPath()); + } finally { + session.logout(); + } + } + + /** + * Creates a new node using {@link Node#addNode(String, String)}, saves using + * {@link javax.jcr.Session#save()}. Uses a second session to verify if the + * node has been safed. + */ + public void testAddNodeSessionSave() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // add node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save new node + superuser.save(); + + // use a different session to verify if the node is there + Session session = getHelper().getReadOnlySession(); + try { + session.getItem(testNode.getPath()); + } finally { + session.logout(); + } + } + + /** + * Creates a new node using {@link Node#addNode(String, String)}, then tries + * to call {@link javax.jcr.Node#save()} on the new node. + *

        + * This should throw an {@link RepositoryException}. + */ + public void testAddNodeRepositoryExceptionSaveOnNewNode() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // add a node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + try { + // try to call save on newly created node + testNode.save(); + fail("Calling Node.save() on a newly created node should throw RepositoryException"); + } catch (RepositoryException e) { + // ok, works as expected. + } + } + + /** + * Tests the behavior with respect to case-sensitivity + */ + public void testSimilarNodeNamesUpperLower() throws RepositoryException { + + internalTestSimilarNodeNames("Test-a", "Test-A"); + } + + /** + * Tests the behavior with respect to Unicode normalization + */ + public void testSimilarNodeNamesNfcNfd() throws RepositoryException { + + String precomposed = "Test-\u00e4"; // a umlaut + String decomposed = Normalizer.normalize(precomposed, Normalizer.Form.NFD); + assertFalse(precomposed.equals(decomposed)); // sanity check + internalTestSimilarNodeNames(precomposed, decomposed); + } + + /** + * Tests behavior for creation of "similarly" named nodes + * @throws RepositoryException + */ + private void internalTestSimilarNodeNames(String name1, String name2) throws RepositoryException { + + Node n1 = null, n2 = null; + Session s = testRootNode.getSession(); + + try { + n1 = testRootNode.addNode(name1); + assertEquals(name1, n1.getName()); + s.save(); + + assertFalse(testRootNode.hasNode(name2)); + } catch (ConstraintViolationException e) { + // accepted + } + try { + n2 = testRootNode.addNode(name2); + assertEquals(name2, n2.getName()); + s.save(); + } catch (ConstraintViolationException e) { + // accepted + } + + // If both nodes have been created, do further checks + if (n1 != null && n2 != null) { + assertFalse(n1.isSame(n2)); + assertFalse(n1.getIdentifier().equals(n2.getIdentifier())); + String n2path = n2.getPath(); + n1.remove(); + s.save(); + Node n3 = s.getNode(n2path); + assertTrue(n3.isSame(n2)); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/Base64.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/Base64.java new file mode 100644 index 00000000000..9fdf9fa8011 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/Base64.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.test.api; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; + +/** + * Base64 provides Base64 encoding/decoding of strings and streams. + * NOTE: This is a copy of the original org.apache.jackrabbit.core.util.Base64 + * class to not introduce a dependency to jackrabbit from the test classes. + */ +public class Base64 { + // charset used for base64 encoded data (7-bit ASCII) + private static final String CHARSET = "US-ASCII"; + + // encoding table (the 64 valid base64 characters) + private static final char[] BASE64CHARS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + + // decoding table (used to lookup original 6-bit with base64 character as table index) + private static final byte[] DECODETABLE = new byte[128]; + + static { + // initialize decoding table + for (int i = 0; i < DECODETABLE.length; i++) { + DECODETABLE[i] = 0x7f; + } + // build decoding table + for (int i = 0; i < BASE64CHARS.length; i++) { + DECODETABLE[BASE64CHARS[i]] = (byte) i; + } + } + + // pad character + private static final char BASE64PAD = '='; + + /** + * empty private constructor + */ + private Base64() { + } + + /** + * Calculates the size (i.e. number of bytes) of the base64 encoded output + * given the length (i.e. number of bytes) of the data to be encoded. + * + * @param dataLength length (i.e. number of bytes) of the data to be encoded + * @return size (i.e. number of bytes) of the base64 encoded output + */ + public static long calcEncodedLength(long dataLength) { + long encLen = dataLength * 4 / 3; + encLen += (encLen + 4) % 4; + return encLen; + } + + /** + * Pessimistically guesses the size (i.e. number of bytes) of the decoded output + * given the length (i.e. number of bytes) of the base64 encoded data. + * + * @param encLength length (i.e. number of bytes) of the base64 encoded data + * @return size (i.e. number of bytes) of the decoded output + */ + public static long guessDecodedLength(long encLength) { + long decLen = encLength * 3 / 4; + return decLen + 3; + } + + /** + * Outputs base64 representation of the specified stream data to an OutputStream. + * + * @param in stream data to be encoded + * @param writer writer to output the encoded data + */ + public static void encode(InputStream in, Writer writer) + throws IOException { + // encode stream data in chunks; + // chunksize must be a multiple of 3 in order + // to avoid padding within output + byte[] buffer = new byte[9 * 1024]; + int read = 0; + while ((read = in.read(buffer)) > 0) { + encode(buffer, 0, read, writer); + } + } + + /** + * Outputs base64 representation of the specified stream data to an OutputStream. + * + * @param in stream data to be encoded + * @param out stream where the encoded data should be written to + */ + public static void encode(InputStream in, OutputStream out) + throws IOException { + Writer writer = new OutputStreamWriter(out, CHARSET); + encode(in, writer); + } + + /** + * Outputs base64 representation of the specified data to an OutputStream. + * + * @param data data to be encoded + * @param off offset within data at which to start encoding + * @param len length of data to encode + * @param writer writer to output the encoded data + */ + public static void encode(byte[] data, int off, int len, Writer writer) + throws IOException { + if (len == 0) { + return; + } + if (len < 0 || off >= data.length + || len + off > data.length) { + throw new IllegalArgumentException(); + } + char[] enc = new char[4]; + while (len >= 3) { + int i = ((data[off] & 0xff) << 16) + + ((data[off + 1] & 0xff) << 8) + + (data[off + 2] & 0xff); + enc[0] = BASE64CHARS[i >> 18]; + enc[1] = BASE64CHARS[(i >> 12) & 0x3f]; + enc[2] = BASE64CHARS[(i >> 6) & 0x3f]; + enc[3] = BASE64CHARS[i & 0x3f]; + writer.write(enc, 0, 4); + off += 3; + len -= 3; + } + // add padding if necessary + if (len == 1) { + int i = data[off] & 0xff; + enc[0] = BASE64CHARS[i >> 2]; + enc[1] = BASE64CHARS[(i << 4) & 0x3f]; + enc[2] = BASE64PAD; + enc[3] = BASE64PAD; + writer.write(enc, 0, 4); + } else if (len == 2) { + int i = ((data[off] & 0xff) << 8) + (data[off + 1] & 0xff); + enc[0] = BASE64CHARS[i >> 10]; + enc[1] = BASE64CHARS[(i >> 4) & 0x3f]; + enc[2] = BASE64CHARS[(i << 2) & 0x3f]; + enc[3] = BASE64PAD; + writer.write(enc, 0, 4); + } + } + + /** + * Decode base64 encoded data. + * + * @param reader reader for the base64 encoded data to be decoded + * @param out stream where the decoded data should be written to + */ + public static void decode(Reader reader, OutputStream out) throws IOException { + char[] chunk = new char[8192]; + int read; + while ((read = reader.read(chunk)) > -1) { + decode(chunk, 0, read, out); + } + } + + /** + * Decode base64 encoded data. The data read from the inputstream is + * assumed to be of charset "US-ASCII". + * + * @param in inputstream of the base64 encoded data to be decoded + * @param out stream where the decoded data should be written to + */ + public static void decode(InputStream in, OutputStream out) throws IOException { + decode(new InputStreamReader(in, CHARSET), out); + } + + /** + * Decode base64 encoded data. + * + * @param data the base64 encoded data to be decoded + * @param out stream where the decoded data should be written to + */ + public static void decode(String data, OutputStream out) throws IOException { + char[] chars = data.toCharArray(); + decode(chars, 0, chars.length, out); + } + + /** + * Decode base64 encoded data. + * + * @param chars the base64 encoded data to be decoded + * @param out stream where the decoded data should be written to + */ + public static void decode(char[] chars, OutputStream out) throws IOException { + decode(chars, 0, chars.length, out); + } + + /** + * Decode base64 encoded data. + * + * @param chars the base64 encoded data to be decoded + * @param off offset within data at which to start decoding + * @param len length of data to decode + * @param out stream where the decoded data should be written to + */ + public static void decode(char[] chars, int off, int len, OutputStream out) throws IOException { + if (len == 0) { + return; + } + if (len < 0 || off >= chars.length + || len + off > chars.length) { + throw new IllegalArgumentException(); + } + char[] chunk = new char[4]; + byte[] dec = new byte[3]; + int posChunk = 0; + // decode in chunks of 4 characters + for (int i = off; i < (off + len); i++) { + char c = chars[i]; + if (c < DECODETABLE.length && DECODETABLE[c] != 0x7f + || c == BASE64PAD) { + chunk[posChunk++] = c; + if (posChunk == chunk.length) { + int b0 = DECODETABLE[chunk[0]]; + int b1 = DECODETABLE[chunk[1]]; + int b2 = DECODETABLE[chunk[2]]; + int b3 = DECODETABLE[chunk[3]]; + if (chunk[3] == BASE64PAD && chunk[2] == BASE64PAD) { + dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); + out.write(dec, 0, 1); + } else if (chunk[3] == BASE64PAD) { + dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); + dec[1] = (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf); + out.write(dec, 0, 2); + } else { + dec[0] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); + dec[1] = (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf); + dec[2] = (byte) (b2 << 6 & 0xc0 | b3 & 0x3f); + out.write(dec, 0, 3); + } + posChunk = 0; + } + } else { + throw new IllegalArgumentException("specified data is not base64 encoded"); + } + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/BinaryPropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/BinaryPropertyTest.java new file mode 100644 index 00000000000..ef03507f0c0 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/BinaryPropertyTest.java @@ -0,0 +1,477 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import java.io.InputStream; +import java.io.IOException; +import java.io.ByteArrayOutputStream; + +import javax.jcr.Binary; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +/** + * Tests a binary property. If the workspace does not contain a node with a + * binary property a {@link org.apache.jackrabbit.test.NotExecutableException} + * is thrown. + * + */ +public class BinaryPropertyTest extends AbstractPropertyTest { + + /** + * Returns {@link javax.jcr.PropertyType#BINARY}. + * @return {@link javax.jcr.PropertyType#BINARY}. + */ + protected int getPropertyType() { + return PropertyType.BINARY; + } + + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + + /** + * Tests that when Value.getStream() is called a second time the same Stream + * object is returned. Also tests that when a new Value object is requested + * also a new Stream object is returned by calling getStream() on the new + * Value object. + */ + public void testSameStream() throws RepositoryException, IOException { + Value val = PropertyUtil.getValue(prop); + InputStream in = val.getStream(); + InputStream in2 = val.getStream(); + Value otherVal = PropertyUtil.getValue(prop); + InputStream in3 = otherVal.getStream(); + try { + assertSame("Same InputStream object expected when " + + "Value.getStream is called twice.", in, in2); + assertNotSame("Value.getStream() called on a new value " + + "object should return a different Stream object.", in, in3); + } finally { + // cleaning up + try { + in.close(); + } catch (IOException ignore) {} + if (in2 != in) { + try { + in2.close(); + } catch (IOException ignore) {} + } + if (in3 != in) { + try { + in3.close(); + } catch (IOException ignore) {} + } + } + } + + /** + * Tests that when Binary.getStream() is called a second time a new stream + * object is returned. + */ + public void testSameStreamJcr2() throws RepositoryException, IOException { + Value val = PropertyUtil.getValue(prop); + Binary bin = val.getBinary(); + try { + InputStream in = bin.getStream(); + InputStream in2 = bin.getStream(); + try { + assertNotSame("Value.getStream() called on a new value " + + "object should return a different Stream object.", in, in2); + //check if both streams can be read independently but contain the same bytes + int n,n2; + while ((n = in.read()) != -1) { + n2 = in2.read(); + assertEquals("streams from the same binary object should have identical content", n, n2); + } + assertEquals("streams from the same binary object should have identical content", -1, in2.read()); + } finally { + // cleaning up + try { + in.close(); + } catch (IOException ignore) {} + if (in2 != in) { + try { + in2.close(); + } catch (IOException ignore) {} + } + } + } finally { + bin.dispose(); + } + } + + /** + * Tests the failure of calling Property.getStream() on a multivalue + * property. + */ + public void testMultiValue() throws RepositoryException, IOException { + if (multiple) { + InputStream in = null; + try { + in = prop.getStream(); + fail("Calling getStream() on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } finally { + if (in != null) { + in.close(); + } + } + } + } + + /** + * Tests the failure of calling Property.getBinary() on a multivalue + * property. + */ + public void testMultiValueJcr2() throws RepositoryException, IOException { + if (multiple) { + try { + prop.getBinary(); + fail("Calling getStream() on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests that Property.getStream() delivers the same as Value.getStream(). + * We check this by reading each byte of the two streams and assuring that + * they are equal. + */ + public void testValue() throws IOException, RepositoryException { + Value val = PropertyUtil.getValue(prop); + InputStream in = val.getStream(); + InputStream in2; + if (prop.getDefinition().isMultiple()) { + // prop has at least one value (checked in #setUp()) + in2 = prop.getValues()[0].getStream(); + } else { + in2 = prop.getStream(); + } + try { + int b = in.read(); + while (b != -1) { + int b2 = in2.read(); + assertEquals("Value.getStream() and Property.getStream() " + + "return different values.", b, b2); + b = in.read(); + } + assertEquals("Value.getStream() and Property.getStream() " + + "return different values.", -1, in2.read()); + } finally { + try { + in.close(); + } catch (IOException ignore) {} + try { + in2.close(); + } catch (IOException ignore) {} + } + } + + /** + * Tests that Value.getStream() delivers the same as Value.getBinary.getStream(). + * We check this by reading each byte of the two streams and assuring that + * they are equal. + */ + public void testValueJcr2() throws IOException, RepositoryException { + Value val = PropertyUtil.getValue(prop); + InputStream in = val.getStream(); + Binary bin = val.getBinary(); + try { + InputStream in2 = bin.getStream(); + try { + int b = in.read(); + while (b != -1) { + int b2 = in2.read(); + assertEquals("Value.getStream() and Value.getBinary().getStream() " + + "return different values.", b, b2); + b = in.read(); + } + assertEquals("Value.getStream() and Value.getBinary().getStream() " + + "return different values.", -1, in2.read()); + } finally { + try { + in.close(); + } catch (IOException ignore) {} + try { + in2.close(); + } catch (IOException ignore) {} + } + } finally { + bin.dispose(); + } + } + + /** + * Tests conversion from Binary type to Boolean type. This is done via + * String conversion. + */ + public void testGetBoolean() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + String str = val.getString(); + boolean bool = val.getBoolean(); + assertEquals("Wrong conversion from Binary to Boolean.", + new Boolean(bool), Boolean.valueOf(str)); + } + + /** + * Tests conversion from Binary type to Date type. This is done via String + * conversion. + */ + public void testGetDate() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + if (PropertyUtil.isDateFormat(val.getString())) { + val.getDate(); + } else { + try { + val.getDate(); + fail("Conversion from a malformed String to a Date " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests conversion from Binary type to Double type. This is done via String + * conversion. + */ + public void testGetDouble() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + String str = val.getString(); + // double + try { + Double.parseDouble(str); + double d = val.getDouble(); + assertEquals("Wrong conversion from Binary to Double", + new Double(d), Double.valueOf(str)); + } catch (NumberFormatException nfe) { + try { + val.getDouble(); + fail("Conversion from malformed Binary to Double " + + "should throw ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests conversion from Binary type to Long type. This is done via String + * conversion. + */ + public void testGetLong() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + String str = val.getString(); + try { + Long.parseLong(str); + long l = val.getLong(); + assertEquals("Wrong conversion from Binary to Long", + new Long(l), Long.valueOf(str)); + } catch (NumberFormatException nfe) { + try { + val.getLong(); + fail("Conversion from malformed Binary to Long " + + "should throw ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests if Value.getType() returns the same as Property.getType() and + * also tests that prop.getDefinition().getRequiredType() returns the same + * type in case it is not of Undefined type. + */ + public void testGetType() throws RepositoryException { + assertTrue("Value.getType() returns wrong type.", + PropertyUtil.checkGetType(prop, PropertyType.BINARY)); + } + + /** + * Tests the conversion from Binary type to Reference or Path type. This conversion + * passes through previous String conversion. + */ + public void testGetNode() throws RepositoryException, NotExecutableException { + if (!multiple) { + // not testable since format of ID is implementation specific + } else { + try { + prop.getNode(); + fail("Property.getNode() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests the conversion from Binary type to Path type. This conversion + * passes through previous String conversion. + */ + public void testGetProperty() throws RepositoryException, NotExecutableException { + if (!multiple) { + // not testable since format of ID is implementation specific + } else { + try { + prop.getProperty(); + fail("Property.getProperty() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests the Property.getLength() method. + */ + public void testGetLength() throws RepositoryException { + if (multiple) { + try { + prop.getLength(); + fail("Property.getLength() called on a multivalue property " + + "should throw a ValueFormatException."); + + } catch (ValueFormatException vfe) { + // ok + } + } else { + long length = prop.getLength(); + if (length > -1) { + long bytes = PropertyUtil.countBytes(prop.getValue()); + if (bytes != -1) { + assertEquals("Property.getLength() returns wrong number of bytes.", + bytes, length); + } + + } + } + } + + /** + * Tests the Binary.getSize() method. + */ + public void testGetLengthJcr2() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + Binary binary = val.getBinary(); + long length; + try { + length = binary.getSize(); + } finally { + binary.dispose(); + } + long bytes = PropertyUtil.countBytes(val); + if (bytes != -1) { + assertEquals("Binary.getSize() returns wrong number of bytes.", + bytes, length); + } + } + + /** + * Tests the Property.getLengths() method. The test is successful, if either + * -1 is returned + */ + public void testGetLengths() throws RepositoryException { + if (multiple) { + Value[] values = prop.getValues(); + long[] lengths = prop.getLengths(); + for (int i = 0; i < lengths.length; i++) { + long length = PropertyUtil.countBytes(values[i]); + assertEquals("Property.getLengths() returns " + + "wrong array of the lengths of a multivalue property.", + length, lengths[i]); + } + } else { + try { + prop.getLengths(); + fail("Property.getLengths() called on a sinlge value property " + + "should throw a ValueFormatException."); + + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests the Binary.read() method. + */ + public void testRandomAccess() throws RepositoryException, IOException { + Value val = PropertyUtil.getValue(prop); + Binary bin = val.getBinary(); + try { + byte[] buf = new byte[0x1000]; + + //verify that reading behind EOF returns -1 + assertEquals("reading behind EOF must return -1", -1, bin.read(buf, bin.getSize())); + + //read content using Binary.read() + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (int cnt, pos = 0; (cnt = bin.read(buf, pos)) > 0; pos += cnt) { + out.write(buf, 0, cnt); + } + byte[] content = out.toByteArray(); + assertEquals("unexpected content length", bin.getSize(), content.length); + + //verify against stream + InputStream in = val.getStream(); + try { + int k = 0; + for (int b; (b = in.read()) != -1; k++) { + assertEquals("Value.getStream().read() and Value.getBinary().read() " + + "return different values.", (byte) b, content[k]); + } + assertEquals("unexpected content length", k, content.length); + } finally { + try { + in.close(); + } catch (IOException ignore) {} + } + + //verify random access + buf = new byte[1]; + assertTrue("unexpected result of Value.getBinary.read()", -1 != bin.read(buf, 0)); + assertEquals("unexpected result of Value.getBinary.read()", content[0], buf[0]); + if (content.length > 0) { + assertTrue("unexpected result of Value.getBinary.read()", -1 != bin.read(buf, content.length - 1)); + assertEquals("unexpected result of Value.getBinary.read()", content[content.length - 1], buf[0]); + assertTrue("unexpected result of Value.getBinary.read()", -1 != bin.read(buf, 0)); + assertEquals("unexpected result of Value.getBinary.read()", content[0], buf[0]); + } + } finally { + bin.dispose(); + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/BooleanPropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/BooleanPropertyTest.java new file mode 100644 index 00000000000..601c4aca6e3 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/BooleanPropertyTest.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import javax.jcr.PropertyType; +import javax.jcr.Value; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.io.IOException; +import java.io.BufferedInputStream; +import java.io.InputStream; + +/** + * Tests a boolean property. If the workspace does not contain a node with a + * boolean property a {@link org.apache.jackrabbit.test.NotExecutableException} + * is thrown. + * + */ +public class BooleanPropertyTest extends AbstractPropertyTest { + + /** + * Returns {@link javax.jcr.PropertyType#BOOLEAN}. + * + * @return {@link javax.jcr.PropertyType#BOOLEAN}. + */ + protected int getPropertyType() { + return PropertyType.BOOLEAN; + } + + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + + /** + * Tests that Property.getBoolean() delivers the same as Value.getBoolean() + * and that in case of a multivalue property Property.getBoolean() throws a + * ValueFormatException. + */ + public void testValue() throws RepositoryException { + if (multiple) { + try { + prop.getBoolean(); + fail("Property.getBoolean() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } else { + boolean bool = prop.getValue().getBoolean(); + boolean otherBool = prop.getBoolean(); + assertTrue("Value.getBoolean() and Property.getBoolean() " + + "return different values.", bool == otherBool); + } + } + + /** + * Tests failure of conversion from Boolean type to Date type. + */ + public void testGetDate() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getDate(); + fail("Conversion from a Boolean value to a Date value " + + "should throw a ValueFormatException"); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests failure from Boolean type to Double type. + */ + public void testGetDouble() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getDouble(); + fail("Conversion from a Boolean value to a Double value " + + "should throw a ValueFormatException"); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests failure of conversion from Boolean type to Long type. + */ + public void testGetLong() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getLong(); + fail("Conversion from a Boolean value to a Long value " + + "should throw a ValueFormatException"); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests conversion from Boolean type to Binary type. + */ + public void testGetStream() throws RepositoryException, IOException { + Value val = PropertyUtil.getValue(prop); + BufferedInputStream in = new BufferedInputStream(val.getStream()); + Value otherVal = PropertyUtil.getValue(prop); + InputStream ins = null; + byte[] utf8bytes = otherVal.getString().getBytes(UTF8); + // if yet utf-8 encoded these bytes should be equal + // to the ones received from the stream + int i = 0; + byte[] b = new byte[1]; + while (in.read(b) != -1) { + assertTrue("Boolean as a Stream is not utf-8 encoded", + b[0] == utf8bytes[i]); + i++; + } + try { + val.getBoolean(); + } catch (IllegalStateException ise) { + fail("Non stream method call after stream method call " + + "should not throw an IllegalStateException"); + } + try { + ins = otherVal.getStream(); + } catch (IllegalStateException ise) { + fail("Stream method call after a non stream method call " + + "should not throw an IllegalStateException"); + } finally { + if (in != null) { + in.close(); + } + if (ins != null) { + ins.close(); + } + } + } + + /** + * Tests the conversion from a Boolean to a String Value. + */ + public void testGetString() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + String str = val.getString(); + String otherStr = new Boolean(val.getBoolean()).toString(); + assertEquals("Conversion from a Boolean value to a String value failed.", + str, otherStr); + } + + /** + * Tests if Value.getType() returns the same as Property.getType() and also + * tests that prop.getDefinition().getRequiredType() returns the same type + * in case it is not of Undefined type. + */ + public void testGetType() throws RepositoryException { + assertTrue("Value.getType() returns wrong type.", + PropertyUtil.checkGetType(prop, PropertyType.BOOLEAN)); + } + + /** + * Tests failure of conversion from Boolean type to Reference or Path type. + */ + public void testGetNode() throws RepositoryException { + if (!multiple) { + try { + prop.getNode(); + fail("Conversion from a Boolean value to a Reference or Path value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } else { + try { + prop.getNode(); + fail("Property.getNode() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + } + + /** + * Tests failure of conversion from Boolean type to Path type. + */ + public void testGetProperty() throws RepositoryException { + if (!multiple) { + try { + prop.getProperty(); + fail("Conversion from a Boolean value to a Path value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } else { + try { + prop.getProperty(); + fail("Property.getProperty() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests the Property.getLength() method. The length returned is either -1 + * or it is the length of the string received by conversion. + */ + public void testGetLength() throws RepositoryException { + if (multiple) { + try { + prop.getLength(); + fail("Property.getLength() called on a multivalue property " + + "should throw a ValueFormatException."); + + } catch (ValueFormatException vfe) { + // ok + } + } else { + long length = prop.getLength(); + if (length > -1) { + assertEquals("Property.getLength() returns wrong number of bytes.", + length, prop.getString().length()); + } + } + } + + /** + * Tests the Property.getLengths() method. The returned values are either -1 + * or the lengths of the according conversions to strings. + */ + public void testGetLengths() throws RepositoryException { + if (multiple) { + Value[] values = prop.getValues(); + long[] lengths = prop.getLengths(); + for (int i = 0; i < lengths.length; i++) { + if (lengths[i] > -1) { + assertEquals("Property.getLengths() returns " + + "wrong array of the lengths of a multivalue property.", + values[i].getString().length(), lengths[i]); + } + } + } else { + try { + prop.getLengths(); + fail("Property.getLengths() called on a sinlge value property " + + "should throw a ValueFormatException."); + + } catch (ValueFormatException vfe) { + // ok + } + } + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/CheckPermissionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/CheckPermissionTest.java similarity index 84% rename from src/test/org/apache/jackrabbit/test/api/CheckPermissionTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/CheckPermissionTest.java index 9d575b14a6c..ee58f327834 100644 --- a/src/test/org/apache/jackrabbit/test/api/CheckPermissionTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/CheckPermissionTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -26,24 +26,20 @@ * Tests if {@link Session#checkPermission(String, String)} yields the correct * permissions for a read-only session and a 'superuser' session. * - * @test - * @sources CheckPermissionTest.java - * @executeClass org.apache.jackrabbit.test.api.CheckPermissionTest - * @keywords level2 */ public class CheckPermissionTest extends AbstractJCRTest { /** * Tests if Session.checkPermission(String, String) works * properly:

        • Returns quietly if access is permitted.
        • - *
        • Throws an {@link java.security.AccessControlException) if access is + *
        • Throws an {@link java.security.AccessControlException} if access is * denied.
        */ public void testCheckPermission() throws Exception { - testRootNode.addNode(nodeName2); + testRootNode.addNode(nodeName2, testNodeType); superuser.save(); - Session readOnly = helper.getReadOnlySession(); + Session readOnly = getHelper().getReadOnlySession(); try { permissionCheckReadOnly(readOnly); permissionCheckReadWrite(superuser); diff --git a/src/test/org/apache/jackrabbit/test/api/DatePropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DatePropertyTest.java similarity index 78% rename from src/test/org/apache/jackrabbit/test/api/DatePropertyTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DatePropertyTest.java index 22f6e6db53f..c9ed1bbda73 100644 --- a/src/test/org/apache/jackrabbit/test/api/DatePropertyTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DatePropertyTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -30,10 +30,6 @@ * property a {@link org.apache.jackrabbit.test.NotExecutableException} is * thrown. * - * @test - * @sources DatePropertyTest.java - * @executeClass org.apache.jackrabbit.test.api.DatePropertyTest - * @keywords level1 */ public class DatePropertyTest extends AbstractPropertyTest { @@ -45,6 +41,14 @@ protected int getPropertyType() { return PropertyType.DATE; } + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + /** * Tests that Property.getDate() delivers the same as Value.getDate() and * that in case of a multivalue property a ValueFormatException is thrown. @@ -58,12 +62,12 @@ public void testValue() throws RepositoryException { } catch (ValueFormatException vfe) { // ok } + } else { + Calendar calendar = prop.getValue().getDate(); + Calendar calendar2 = prop.getDate(); + assertEquals("Value.getDate() and Property.getDate() return different values.", + calendar, calendar2); } - // Calendar returned? - Calendar calendar = prop.getValue().getDate(); - Calendar calendar2 = prop.getDate(); - assertTrue("Value.getDate() and Property.getDate() return different values.", - calendar.getTimeInMillis() == calendar2.getTimeInMillis()); } /** @@ -75,9 +79,9 @@ public void testGetString() throws RepositoryException { // correct format: YYYY-MM-DDThh:mm:ss.sssTZD // month(01-12), day(01-31), hours(00-23), minutes(00-59), seconds(00-59), // TZD(Z or +hh:mm or -hh:mm) - //String aDay="2005-01-19T15:34:15.917+01:00"; + // String aDay="2005-01-19T15:34:15.917+01:00"; String date = val.getString(); - System.out.println("date str = " + date); + log.println("date str = " + date); boolean match = PropertyUtil.isDateFormat(prop.getString()); assertTrue("Date not in correct String format.", match); } @@ -124,7 +128,7 @@ public void testGetLong() throws RepositoryException { public void testGetStream() throws RepositoryException, IOException { Value val = PropertyUtil.getValue(prop); BufferedInputStream in = new BufferedInputStream(val.getStream()); - Value otherVal = prop.getValue(); + Value otherVal = PropertyUtil.getValue(prop); InputStream ins = null; byte[] utf8bytes = otherVal.getString().getBytes(UTF8); // if yet utf-8 encoded these bytes should be equal @@ -138,17 +142,15 @@ public void testGetStream() throws RepositoryException, IOException { } try { val.getDate(); - fail("Non stream method call after stream method call " + - "should throw an IllegalStateException."); } catch (IllegalStateException ise) { - //ok + fail("Non stream method call after stream method call " + + "should not throw an IllegalStateException."); } try { ins = otherVal.getStream(); - fail("Stream method call after a non stream method call " + - "should throw an IllegalStateException."); } catch (IllegalStateException ise) { - // ok + fail("Stream method call after a non stream method call " + + "should not throw an IllegalStateException."); } finally { if (in != null) { in.close(); @@ -171,13 +173,13 @@ public void testGetType() throws RepositoryException { /** - * Tests failure of conversion from Date type to Reference type. + * Tests failure of conversion from Date type to Reference or Path type. */ - public void testAsReference() throws RepositoryException { + public void testGetNode() throws RepositoryException { if (!multiple) { try { prop.getNode(); - fail("Conversion from a Date value to a Reference value " + + fail("Conversion from a Date value to a Reference or Path value " + "should throw a ValueFormatException."); } catch (ValueFormatException vfe) { //ok @@ -193,6 +195,29 @@ public void testAsReference() throws RepositoryException { } } + /** + * Tests failure of conversion from Date type to Path type. + */ + public void testGetProperty() throws RepositoryException { + if (!multiple) { + try { + prop.getProperty(); + fail("Conversion from a Date value to a Path value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } else { + try { + prop.getProperty(); + fail("Property.getProperty() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + /** * Tests the Property.getLength() method. The length returned is either -1 * or it is the length of the string received by conversion. @@ -242,4 +267,4 @@ public void testGetLengths() throws RepositoryException { } } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DocumentViewImportTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DocumentViewImportTest.java new file mode 100644 index 00000000000..f601ef6141e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DocumentViewImportTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.xml.sax.SAXException; + +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.PropertyIterator; +import javax.jcr.NamespaceException; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.ItemExistsException; +import javax.jcr.nodetype.ConstraintViolationException; +import java.io.IOException; + +/** + * DocumentViewImportTest Tests importXML and + * getImportContentHandler methods of the Workspace and Session class. Also + * tests the UuidBehaviour flag. + * + */ +public class DocumentViewImportTest extends AbstractImportXmlTest { + + private String JCR_XMLTEXT; + private String JCR_XMLCHAR; + + private boolean withHandler; + private boolean withWorkspace; + + public void setUp() throws Exception { + super.setUp(); + JCR_XMLTEXT = superuser.getNamespacePrefix(NS_JCR_URI) + ":xmltext"; + JCR_XMLCHAR = superuser.getNamespacePrefix(NS_JCR_URI) + ":xmlcharacters"; + } + + public void tearDown() throws Exception { + file.delete(); + super.tearDown(); + } + + public void testWorkspaceImportXml() throws Exception { + withHandler = false; + withWorkspace = WORKSPACE; + doTestImportXML(); + } + + public void testSessionImportXml() throws Exception { + withHandler = false; + withWorkspace = SESSION; + doTestImportXML(); + } + + public void testWorkspaceGetImportContentHandler() throws Exception { + withHandler = true; + withWorkspace = SESSION; + doTestGetImportContentHandler(); + } + + public void testSessionGetImportContentHandler() throws Exception { + withHandler = true; + withWorkspace = WORKSPACE; + doTestGetImportContentHandler(); + } + + /** + * Tests importXML method with uuidBehaviour IMPORT_UUID_CREATE_NEW. It + * imports the document created with createSimpleDocument method and checks + * the imported tree according the rules outlined in chapter 7.3.2 of the + * specification. + *

        + * Additionally it checks the uuidBehaviour flag if the jcr:uuid property is + * respected during import. + * + * @throws RepositoryException + * @throws IOException + * @throws SAXException + * @throws NotExecutableException + */ + public void doTestImportXML() throws Exception { + + importXML(target, createSimpleDocument(), uuidBehaviour, withWorkspace); + + // some implementations may require a refresh to get content + // added diretly to the workspace + if (withWorkspace) { + session.refresh(false); + } + + performTests(); + } + + /** + * Tests getImportContentHandler method with uuidBehaviour + * IMPORT_UUID_CREATE_NEW. It imports the document created with + * createSimpleDocument method and checks the imported tree according the + * rules outlined in chapter 7.3.2 of the specification. + *

        + * Additionally it checks the uuidBehaviour flag if the jcr:uuid property is + * respected during import. + * + * @throws RepositoryException + * @throws SAXException + * @throws IOException + * @throws NotExecutableException + */ + public void doTestGetImportContentHandler() throws Exception { + + importWithHandler(target, createSimpleDocument(), uuidBehaviour, withWorkspace); + + // some implementations may require a refresh to get content + // added diretly to the workspace + if (withWorkspace) { + session.refresh(false); + } + + performTests(); + } + + + private void performTests() throws Exception { + + checkImportSimpleXMLTree(); + checkNamespaceAdded(); + if (!respectMixRef) { + throw new NotExecutableException("ImportXML tests with " + + "uuidBehaviour flag not executable."); + } else { + checkImportDocumentView_IMPORT_UUID_CREATE_NEW(); + checkImportDocumentView_IMPORT_UUID_COLLISION_REMOVE_EXISTING(); + checkImportDocumentView_IMPORT_UUID_COLLISION_REPLACE_EXISTING(); + checkImportDocumentView_IMPORT_UUID_COLLISION_THROW(); + } + } + + /** + * Tests if the simple xml document defined in createSimpleDocument() is + * imported correctly according the specification rules given in 7.3.2 + */ + public void checkImportSimpleXMLTree() throws RepositoryException, IOException { + Node parent = (Node) session.getItem(target); + + try { + // check the node names + String prefix = session.getNamespacePrefix(unusedURI); + String rootName = prefix + ":" + rootElem; + //String rootName = rootElem; + Node rootNode = parent.getNode(rootName); + Node child = rootNode.getNode(childElem); + Node xmlTextNode = rootNode.getNode(xmltextElem); + Node grandChild = xmlTextNode.getNode(grandChildElem); + + // check xmltext + checkXmlTextNode(xmlTextNode); + + // check the property names and values + Property prop = grandChild.getProperty(attributeName); + Property prop2 = xmlTextNode.getProperty(attributeName); + String value = prop.getString(); + String value2 = prop2.getString(); + assertEquals("Value " + attributeValue + " of attribute " + + attributeName + " is imported to different property values.", value, value2); + assertEquals("Value " + attributeValue + " of attribute " + + attributeName + " is not correctly imported.", value, attributeValue); + + // check the encoded names and values + Property decodedProp; + // is decoded + try { + child.getNode(decodedElemName); + decodedProp = child.getProperty(decodedAttributeName); + String propVal = decodedProp.getString(); + // both possibilities + if (!propVal.equals(encodedAttributeValue) + && !propVal.equals(decodedAttributeValue)) { + fail("Value " + encodedAttributeValue + " of attribute " + + decodedAttributeName + " is not correctly imported."); + } + + } catch (PathNotFoundException pnfe) { + try { + // is not decoded + child.getNode(encodedElemName); + decodedProp = child.getProperty(encodedAttributeName); + String propVal = decodedProp.getString(); + // both possibilities + if (!propVal.equals(encodedAttributeValue) + && !propVal.equals(decodedAttributeValue)) { + fail("Value " + encodedAttributeValue + " of attribute " + + encodedAttributeName + " is not correctly imported."); + } + } catch (PathNotFoundException pnfe2) { + fail("XML Element " + encodedElemName + " or attribute " + + encodedAttributeName + " not imported: " + pnfe2); + } + } + } catch (PathNotFoundException pne) { + fail("Element or attribute is not imported: " + pne); + } + } + + /** + * Tests if xmltext in a body of a xml element is correctly imported to a + * node with name jcr:xmltext and that the value of the text is stored in + * the singlevalued jcr:xmlcharacters property of String type. + * + * @throws RepositoryException + */ + public void checkXmlTextNode(Node node) throws RepositoryException, IOException { + + if (node.hasNode(JCR_XMLTEXT)) { + Node xmlNode = node.getNode(JCR_XMLTEXT); + if (xmlNode.hasProperty(JCR_XMLCHAR)) { + Property prop = xmlNode.getProperty(JCR_XMLCHAR); + // correct type? + assertTrue("Property " + prop.getPath() + " is not of type String.", + prop.getType() == PropertyType.STRING); + // correct text? + // todo remove the trim as only the white spaces of the current text should be collected + assertEquals("Xml text is not correctly stored.", + xmltext.trim(), prop.getString().trim()); + // only jcr:xmlcharacters property beneath the jcr:primaryType + PropertyIterator iter = xmlNode.getProperties(); + assertTrue(JCR_XMLCHAR + " is not the only property beneath " + + jcrPrimaryType + " in a " + JCR_XMLTEXT + " node.", getSize(iter) == 2); + } else { + fail("Xmltext not stored in property named " + JCR_XMLCHAR); + } + } else { + fail("Xmltext not imported to Node named " + JCR_XMLTEXT); + } + } + + /** + * Checks if a namespace not yet existing in the repository is registered + * after an according document import. + * + * @throws RepositoryException + * @throws IOException + */ + public void checkNamespaceAdded() throws RepositoryException, IOException { + try { + assertEquals("URI not correctly imported.", nsp.getURI(unusedPrefix), unusedURI); + assertEquals("Prefix not correctly imported", nsp.getPrefix(unusedURI), unusedPrefix); + } catch (NamespaceException nse) { + fail("Namespace " + unusedPrefix + ":" + unusedURI + + " not imported during document view import."); + } + } + + //------------------< uuid collision behaviour tests >-------------------------- + // we create a node named referenced below the testRootNode, also we create a xml + // document with an element named rootElem having the uuid of the referenced node + // as attribute and we import this document below the refTargetNode. + + /** + * Checks {@link ImportUUIDBehavior#IMPORT_UUID_CREATE_NEW} i.e. that a node + * receives a new uuid when imported in any case. + */ + public void checkImportDocumentView_IMPORT_UUID_CREATE_NEW() throws Exception { + + String uuid = createReferenceableNode(referenced); + // import a document with a element having the same uuid as the node referenced + importRefNodeDocument(refTarget, uuid, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW, withWorkspace, withHandler); + + // different uuid? + Node node = refTargetNode.getNode(rootElem); + String rootElemUUID = node.getUUID(); + assertFalse("Imported node " + rootElem + " has a UUID which is " + + "yet assigned to another node", uuid.equals(rootElemUUID)); + } + + /** + * Checks ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING i.e that + * the existing node is removed in case of uuid collision. + */ + public void checkImportDocumentView_IMPORT_UUID_COLLISION_REMOVE_EXISTING() + throws Exception { + + String uuid = createReferenceableNode(referenced); + // import a document with a element having the same uuid as the node referenced + importRefNodeDocument(refTarget, uuid, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING, + withWorkspace, withHandler); + + try { + // should be removed now + testRootNode.getNode(referenced); + fail("UUID behavior IMPORT_UUID_COLLISION_REMOVE_EXISTING test is failed: " + + "existing node not removed"); + } catch (PathNotFoundException pnfe) { + // ok + } + try { + // should be there + refTargetNode.getNode(rootElem); + } catch (PathNotFoundException pnfe) { + fail("UUID behavior IMPORT_UUID_COLLISION_REMOVE_EXISTING test is failed: " + + "imported node not in its correct place."); + } + } + + /** + * Checks ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING i.e that + * the existing node is replaced by the imported one node when uuid + * collision occurs. + */ + public void checkImportDocumentView_IMPORT_UUID_COLLISION_REPLACE_EXISTING() + throws Exception { + + String uuid = createReferenceableNode(referenced); + // import a document with a element having the same uuid as the node referenced + importRefNodeDocument(refTarget, uuid, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING, + withWorkspace, withHandler); + + // should be replaced i.e should be modified, in case Workspace method is used + // we cannot decide unless we have some additional property of the imported node. + if (!withWorkspace) { + Node node = testRootNode.getNode(rootElem); + assertTrue("Node " + node.getPath() + " not replaced during " + + "import with IMPORT_UUID_COLLISION_REPLACE_EXISTING", node.hasProperty(propertyName1)); + } + } + + /** + * Checks ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW i.e that a + * ItemExistsException is thrown in case of importing with an input stream + * or a SAXException is thrown in case of importing with a ContentHandler. + * + * @throws RepositoryException + * @throws IOException + */ + public void checkImportDocumentView_IMPORT_UUID_COLLISION_THROW() + throws Exception { + + String uuid = createReferenceableNode(referenced); + try { + importRefNodeDocument(refTarget, uuid, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW, + withWorkspace, withHandler); + fail("UUID behavior IMPORT_UUID_COLLISION_THROW test is failed: " + + "should throw an Exception."); + } catch (ItemExistsException e) { + if (!withHandler) { + // ok + } else { + throw e; + } + } catch (SAXException e) { + if (withHandler) { + // ok + } else { + throw e; + } + } + } + //-------------------------------< exception tests >-------------------------------------- + /** + * Tests correct failure of importing a element wit the same UUID as the target node or + * an ancestor of it in case of uuidBehavior IMPORT_UUID_COLLISION_REMOVE_EXISTING. + * + * The imported document contains a element with jcr:uuid attribute the same as the + * parent of the import target. + */ + public void doTestSameUUIDAtAncestor(boolean withWorkspace, boolean withHandler) + throws Exception { + + String uuid = createReferenceableNode(referenced); + Node node = testRootNode.getNode(referenced); + Node node2 = node.addNode("newParent"); + session.save(); + // import a document with a element having the same uuid as the node referenced + try { + importRefNodeDocument(node2.getPath(), uuid, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING, + withWorkspace, withHandler); + fail("UUID collision with an ancestor of the target node hould throw a " + + "SAXException or a ConstraintViolationException in case of " + + "uuidBehavior IMPORT_UUID_COLLISION_REMOVE_EXISTING."); + } catch(SAXException se) { + if (!withHandler) { + throw se; + } + // ok + } catch (ConstraintViolationException cve) { + if (withHandler) { + throw cve; + } + // ok + } + } + + public void testSameUUIDAtAncestorWorkspaceHandler() throws Exception { + doTestSameUUIDAtAncestor(WORKSPACE, CONTENTHANDLER); + } + + public void testSameUUIDAtAncestorWorkspace() throws Exception { + doTestSameUUIDAtAncestor(WORKSPACE, STREAM); + } + + public void testSameUUIDAtAncestorSessionHandler() throws Exception { + doTestSameUUIDAtAncestor(SESSION, CONTENTHANDLER); + } + + public void testSameUUIDAtAncestorSession() throws Exception { + doTestSameUUIDAtAncestor(SESSION, STREAM); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DoublePropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DoublePropertyTest.java new file mode 100644 index 00000000000..8a35143dfa4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/DoublePropertyTest.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.Value; +import java.util.Calendar; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Tests a double property. If the workspace does not contain a node with a + * double property a {@link org.apache.jackrabbit.test.NotExecutableException} + * is thrown. + * + */ +public class DoublePropertyTest extends AbstractPropertyTest { + + /** + * Returns {@link javax.jcr.PropertyType#DOUBLE}. + * + * @return {@link javax.jcr.PropertyType#DOUBLE}. + */ + protected int getPropertyType() { + return PropertyType.DOUBLE; + } + + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + + /** + * tests that Property.getDouble() delivers the same as Value.getDouble() + * and if in case of a multivalue property a ValueFormatException is + * thrown. + */ + public void testValue() throws RepositoryException { + if (multiple) { + try { + prop.getDouble(); + fail("Property.getDouble() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } else { + double d = prop.getValue().getDouble(); + double dd = prop.getDouble(); + assertTrue("Value.getDouble() and Property.getDouble() return different values.", d == dd); + } + } + + /** + * tests failure of conversion from Double type to Boolean type + */ + public void testGetBoolean() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getBoolean(); + fail("Conversion from a Double value to a Boolean value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * tests conversion from Double type to Date type + */ + public void testGetDate() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + Calendar calendar = val.getDate(); + assertEquals("Conversion from Double value to Date value is not correct.", + calendar.getTimeInMillis(), new Double(val.getDouble()).longValue()); + } + + /** + * tests the conversion from a Double to a Long Value + */ + public void testGetLong() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + long l = val.getLong(); + long ll = new Double(val.getDouble()).longValue(); + assertTrue("Conversion from Double value to Long value is not correct.", l == ll); + } + + /** + * tests conversion from Double type to Binary type + */ + public void testGetStream() throws RepositoryException, IOException { + Value val = PropertyUtil.getValue(prop); + BufferedInputStream in = new BufferedInputStream(val.getStream()); + Value otherVal = PropertyUtil.getValue(prop); + InputStream ins = null; + byte[] utf8bytes = otherVal.getString().getBytes(); + // if yet utf-8 encoded these bytes should be equal + // to the ones received from the stream + int i = 0; + byte b = (byte) in.read(); + while (b != -1) { + assertTrue("Double as a Stream is not utf-8 encoded.", + b == utf8bytes[i]); + b = (byte) in.read(); + i++; + } + try { + val.getDouble(); + } catch (IllegalStateException ise) { + fail("Non stream method call after stream method call " + + "should not throw an IllegalStateException."); + } + try { + ins = otherVal.getStream(); + } catch (IllegalStateException ise) { + fail("Stream method call after a non stream method call " + + "should not throw an IllegalStateException."); + } finally { + if (in != null) { + in.close(); + } + if (ins != null) { + ins.close(); + } + } + } + + /** + * tests the conversion from a Double to a String Value + */ + public void testGetString() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + String str = val.getString(); + String otherStr = Double.toString(val.getDouble()); + assertEquals("Conversion from Double value to String value is not correct.", str, otherStr); + } + + /** + * Tests if Value.getType() returns the same as Property.getType() and also + * tests that prop.getDefinition().getRequiredType() returns the same type + * in case it is not of Undefined type. + */ + public void testGetType() throws RepositoryException { + assertTrue("Value.getType() returns wrong type.", + PropertyUtil.checkGetType(prop, PropertyType.DOUBLE)); + } + + /** + * Tests failure of conversion from Double type to Reference, WeakReference or Path type. + */ + public void testGetNode() throws RepositoryException { + if (!multiple) { + try { + prop.getNode(); + fail("Conversion from a Double value to a Reference or Path value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException e) { + // success. + } + } else { + try { + prop.getNode(); + fail("Property.getNode() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + } + + /** + * Tests failure of conversion from Double type to Path type. + */ + public void testGetProperty() throws RepositoryException { + if (!multiple) { + try { + prop.getProperty(); + fail("Conversion from a Double value to a Path value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException e) { + // success. + } + } else { + try { + prop.getProperty(); + fail("Property.getProperty() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests the Property.getLength() method. The length returned is either -1 + * or it is the length of the string received by conversion. + */ + public void testGetLength() throws RepositoryException { + if (multiple) { + try { + prop.getLength(); + fail("Property.getLength() called on a multivalue property " + + "should throw a ValueFormatException."); + + } catch (ValueFormatException vfe) { + // ok + } + } else { + long length = prop.getLength(); + if (length > -1) { + assertEquals("Property.getLength() returns wrong number of bytes.", + length, prop.getString().length()); + } + } + } + + /** + * Tests the Property.getLengths() method. The returned values are either -1 + * or the lengths of the according conversions to strings. + */ + public void testGetLengths() throws RepositoryException { + if (multiple) { + Value[] values = prop.getValues(); + long[] lengths = prop.getLengths(); + for (int i = 0; i < lengths.length; i++) { + if (lengths[i] > -1) { + assertEquals("Property.getLengths() returns " + + "wrong array of the lengths of a multivalue property.", + values[i].getString().length(), lengths[i]); + } + } + } else { + try { + prop.getLengths(); + fail("Property.getLengths() called on a sinlge value property " + + "should throw a ValueFormatException."); + + } catch (ValueFormatException vfe) { + // ok + } + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/EscapeJCRUtil.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/EscapeJCRUtil.java new file mode 100644 index 00000000000..c4215740e30 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/EscapeJCRUtil.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.test.api; + +import org.apache.jackrabbit.test.XMLChar; + +/** + * Class providing some character escape methods. The escape schemata are + * defined in chapter 6.4.3 (for JCR names) and (6.4.4 for JCR values) in the + * JCR specification. + */ +public class EscapeJCRUtil { + + private static final String utf16esc = "_x[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]_"; + + private static char[] Utf16Padding = {'0', '0', '0'}; + + private static int utf16length = 4; + + public static String escapeJCRValues(String str) { + return escapeValues(str); + } + + public static String escapeJCRNames(String str) { + return escapeNames(str); + } + + //----------------------- private methods ---------------------------------- + + private static String escapeNames(String str) { + String[] split = str.split(":"); + if (split.length == 2) { + // prefix should yet be a valid xml name + String localname = escape(split[1]); + String name = split[0] + ":" + localname; + return name; + } else { + String localname = escape(split[0]); + return localname; + } + } + + private static String escapeValues(String str) { + char[] chars = str.toCharArray(); + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (c == '\u0020' + || c == '\u0009' + || c == '\n' + || c == '\r') { + buf.append(escapeChar(c)); + } else { + buf.append(c); + } + } + return buf.toString(); + } + + /** + * Check if a substring can be misinterpreted as an escape sequence. + * + * @param str + * @return + */ + private static boolean canMisinterpret(String str) { + boolean misinterprete = false; + // check if is like "_xXXXX_" + if (str.length() >= 7) { + String sub16 = str.substring(0, 7); + if (sub16.matches(utf16esc)) { + misinterprete = true; + } + } + return misinterprete; + } + + /** + * Escapes a single (invalid xml) character. + * + * @param c + * @return + */ + private static String escapeChar(char c) { + String unicodeRepr = Integer.toHexString(c); + StringBuffer escaped = new StringBuffer(); + escaped.append("_x"); + escaped.append(Utf16Padding, 0, utf16length - unicodeRepr.length()); + escaped.append(unicodeRepr); + escaped.append("_"); + return escaped.toString(); + } + + /** + * Escapes a string containing invalid xml character(s). + * + * @param str + * @return + */ + private static String escape(String str) { + char[] chars = str.toCharArray(); + StringBuffer buf = new StringBuffer(); + + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + // handle start character + if (i == 0) { + if (!XMLChar.isNameStart(c)) { + String escaped = escapeChar(c); + buf.append(escaped); + } else { + String substr = str.substring(i, str.length()); + if (canMisinterpret(substr)) { + buf.append(escapeChar(c)); + } else { + buf.append(c); + } + } + } else { + if (!XMLChar.isName(c)) { + buf.append(escapeChar(c)); + } else { + String substr = str.substring(i, str.length()); + if (canMisinterpret(substr)) { + buf.append(escapeChar(c)); + } else { + buf.append(c); + } + } + } + } + return buf.toString(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ExportDocViewTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ExportDocViewTest.java new file mode 100644 index 00000000000..90ed34dfab8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ExportDocViewTest.java @@ -0,0 +1,1154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.XMLChar; + +import org.xml.sax.SAXException; +import org.xml.sax.ContentHandler; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Attr; +import org.w3c.dom.NamedNodeMap; + +import javax.xml.transform.sax.SAXTransformerFactory; +import javax.xml.transform.sax.TransformerHandler; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.Transformer; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.PropertyIterator; +import javax.jcr.Value; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.ArrayList; +import java.io.File; +import java.io.IOException; +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.ByteArrayOutputStream; +import java.io.StringWriter; + +/** + * ExportDocViewTest tests the two Session methods : + * {@link Session#exportDocumentView(String, ContentHandler, boolean, boolean)} + * and {@link Session#exportDocumentView(String, java.io.OutputStream, boolean, boolean)} + * against the required behaviours according the document view xml mapping + * defined in the JSR 170 specification in chapter 6.4.2, 6.4.3 and 6.4.4 . + * + */ +public class ExportDocViewTest extends AbstractJCRTest { + + private final boolean CONTENTHANDLER = true, STREAM = false; + private final boolean SKIPBINARY = true, SAVEBINARY = false; + private final boolean NORECURSE = true, RECURSE = false; + + /** + * Resolved Name for jcr:xmltext + */ + private String JCR_XMLTEXT; + /** + * Resolved Name for jcr:xmlcharacters + */ + private String JCR_XMLDATA; + /** + * the stack of the text node values to check + */ + private Stack textValuesStack; + + private class StackEntry { + // the list of text node values of the text nodes of an xml element + List textValues; + // the current position in the ArrayList + int position = 0; + } + + /** + * indicates if the tested repository exports multivalued properties. + */ + private boolean exportMultivalProps = false; + + /** + * indicates if the tested repository escapes (xml)invalid jcr names. + */ + private boolean exportInvalidXmlNames = false; + + private boolean skipBinary; + private boolean noRecurse; + private boolean withHandler; + + private File file; + private Session session; + private Workspace workspace; + private NamespaceRegistry nsr; + private String testPath; + + private Document doc; + + protected void setUp() throws Exception { + isReadOnly = true; + session = getHelper().getReadOnlySession(); + workspace = session.getWorkspace(); + nsr = workspace.getNamespaceRegistry(); + file = File.createTempFile("docViewExportTest", ".xml"); + super.setUp(); + + JCR_XMLTEXT = session.getNamespacePrefix(NS_JCR_URI) + ":xmltext"; + JCR_XMLDATA = session.getNamespacePrefix(NS_JCR_URI) + ":xmlcharacters"; + + testPath = testRoot; + } + + protected void tearDown() throws Exception { + file.delete(); + if (session != null) { + session.logout(); + session = null; + } + workspace = null; + nsr = null; + super.tearDown(); + } + + public void testExportDocView_handler_session_skipBinary_noRecurse() + throws IOException, RepositoryException, SAXException, TransformerException { + doTestExportDocView(CONTENTHANDLER, SKIPBINARY, NORECURSE); + } + + public void testExportDocView_handler_session_skipBinary_recurse() + throws IOException, RepositoryException, SAXException, TransformerException { + doTestExportDocView(CONTENTHANDLER, SKIPBINARY, RECURSE); + } + + public void testExportDocView_handler_session_saveBinary_noRecurse() + throws IOException, RepositoryException, SAXException, TransformerException { + doTestExportDocView(CONTENTHANDLER, SAVEBINARY, NORECURSE); + } + + public void testExportDocView_handler_session_saveBinary_recurse() + throws IOException, RepositoryException, SAXException, TransformerException { + doTestExportDocView(CONTENTHANDLER, SAVEBINARY, RECURSE); + } + + public void testExportDocView_stream_session_skipBinary_recurse() + throws IOException, RepositoryException, SAXException, TransformerException { + doTestExportDocView(STREAM, SKIPBINARY, RECURSE); + } + + public void testExportDocView_stream_session_skipBinary_noRecurse() + throws IOException, RepositoryException, SAXException, TransformerException { + doTestExportDocView(STREAM, SKIPBINARY, NORECURSE); + } + + public void testExportDocView_stream_session_saveBinary_noRecurse() + throws IOException, RepositoryException, SAXException, TransformerException { + doTestExportDocView(STREAM, SAVEBINARY, NORECURSE); + } + + public void testExportDocView_stream_session_saveBinary_recurse() + throws IOException, RepositoryException, SAXException, TransformerException { + doTestExportDocView(STREAM, SAVEBINARY, RECURSE); + } + + /** + * Tests session.exportDocView with the different argument possibilities. + * The flag withHandler decides if the method requiring a ContentHandler as + * argument is called. The class org.apache.xml.serialize.XMLSerializer is + * taken as ContentHandler in this case. In both cases ( export with a + * ContentHandler and export with Stream) the test node is exported to the + * file defined in the setUp. This exported file is parsed using + * javax.xml.transform package and the receiving document is compared with + * the test node and its properties and child nodes in the repository. + * + * @param withHandler boolean, decides to call method requiring a + * ContentHandler as argument + * @param skipBinary + * @param noRecurse + */ + public void doTestExportDocView(boolean withHandler, boolean skipBinary, boolean noRecurse) + throws RepositoryException, IOException, SAXException, TransformerException { + + this.skipBinary = skipBinary; + this.noRecurse = noRecurse; + this.withHandler = withHandler; + BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file)); + try { + if (withHandler) { + SAXTransformerFactory stf = + (SAXTransformerFactory) SAXTransformerFactory.newInstance(); + TransformerHandler th = stf.newTransformerHandler(); + th.setResult(new StreamResult(os)); + session.exportDocumentView(testPath, th, skipBinary, noRecurse); + } else { + session.exportDocumentView(testPath, os, skipBinary, noRecurse); + } + } finally { + os.close(); + } + + // build the DOM tree + InputStream in = new BufferedInputStream(new FileInputStream(file)); + doc = readDocument(in); + + compareTree(); + } + + /** + * Compares the test node with the document's root element. In case also the + * child nodes are exported (noRecurse = false) the child nodes of the test + * node are compared with the child elements of the root element too. + * + * @throws RepositoryException + */ + private void compareTree() throws RepositoryException, IOException { + Element root = doc.getDocumentElement(); + textValuesStack = new Stack(); + // we assume the path is valid + Item item = session.getItem(testPath); + + // only an absolute path to a node is allowed + if (!item.isNode()) { + fail("Item at the given root path " + testPath + " is not a node."); + } + Node node = (Node) item; + // multival props exported? + setExportMultivalProps(node, root, false); + // items with invalid xml names exported? + setExportInvalidXmlNames(node, root, false); + // check the test root node + checkRootElement(node, root); + // check the namespaces + compareNamespaces(root); + // check the exported data against the node which is exported. + compareNode(node, root); + + // check the whole tree + if (!noRecurse) { + checkChildNodes(node, root); + } + } + + /** + * Assures that root element exists and has correct jcr:root name if it is + * the root node of the repository. (chapter 6.4.2.2 of the JCR + * specification.) Also checks if multivalued properties are exported + * (chapter 6.4.2.5 of the JCR specification.) Also tries to find out if + * items with an invalid xml name are exported or not. (chapter 6.4.2.4 of + * the JCR specification.) + * + * @param node + * @param root + * @throws RepositoryException + */ + private void checkRootElement(Node node, Element root) throws RepositoryException { + + boolean isValidName = XMLChar.isValidName(node.getName()); + if (root != null) { + // check correct element name if the root node of the repository is exported. + if (node.getDepth() == 0) { + assertEquals("Exported root node has not correct name jcr:root.", + "jcr:root", root.getTagName()); + } + } else { + if (exportInvalidXmlNames || isValidName) { + fail("Node " + node.getPath() + " is not exported."); + } + } + } + + /** + * Checks the child nodes of the given node against the child nodes of the + * given xml element. The found text nodes of the xml element are hold in an + * ArrayList and put on a stack for further checking if another child + * element is between them. + * + * @param node + * @param elem + * @throws RepositoryException + */ + private void checkChildNodes(Node node, Element elem) + throws RepositoryException, IOException { + + NodeIterator nodeIter = node.getNodes(); + if (getSize(node.getNodes()) == 0) { + assertTrue("Exported node " + node.getPath() + " has child elements " + + "although it has no child nodes ", 0 == countChildElems(elem)); + } else { + // create a stack entry for the text child nodes + // of the current xml element + StackEntry entry = new StackEntry(); + entry.textValues = getChildTextNodeValues(elem); + textValuesStack.push(entry); + // xmltext nodes directly following each other + // are serialized together as xml text + List jcrTextNodes = new ArrayList(); + + while (nodeIter.hasNext()) { + Node childNode = nodeIter.nextNode(); + if (isXMLTextNode(childNode)) { + jcrTextNodes.add(childNode); + } else { + if (jcrTextNodes.size() > 0) { + compareXmltextNodes(jcrTextNodes, elem); + // reset the Array + jcrTextNodes.clear(); + } + compareChildTree(childNode, elem); + } + } + // finally we are through the child nodes + // so we delete the stackEntry + textValuesStack.pop(); + } + } + + /** + * Compares the child tree of a given node against the child elements of a + * given element. (chapter 6.4.2.1 points 2,3,4 of the JCR specification). + *

        + * Considered are the export constraints regarding nodes named jcr:xmldata + * (chapter 6.4.2.3 of the JCR specification). + *

        + * Also the numbers of exported child elements is compared with the number + * of child nodes. + * + * @param node + * @param parentElem + * @throws RepositoryException + */ + private void compareChildTree(Node node, Element parentElem) + throws RepositoryException, IOException { + + Element nodeElem; + + // find a childElem belonging to the node and check it. + nodeElem = findElem(node, parentElem); + if (nodeElem != null) { + compareNode(node, nodeElem); + // go deep + checkChildNodes(node, nodeElem); + } + } + + /** + * Checks the given Element if it has a child element with the same (or + * eventually escaped) name as the given node. (chapter 6.4.2.1 point 3 of + * the JCR specification). + * + * @param node + * @param parentElem + * @return Child Element of parentElem. Null if no corresponidng element is + * found. + * @throws RepositoryException + */ + private Element findElem(Node node, Element parentElem) throws RepositoryException { + String name = node.getName(); + Element nodeElem = null; + // valid xml name? + boolean isValidName = XMLChar.isValidName(name); + + name = !isValidName ? escapeNames(name) : name; + // same name sibs + List children = getChildElems(parentElem, name); + + if (children.size() > 0) { + // xmltext nodes are not exported as elements + if (isXMLTextNode(node)) { + fail("Xml text node " + node.getPath() + + " is wronlgy exported as element."); + } else { + // order of same name siblings is preserved during export + int index = node.getIndex(); + try { + nodeElem = children.get(index - 1); + } catch (IndexOutOfBoundsException iobe) { + fail("Node " + node.getPath() + " is not exported." + + iobe.toString()); + } + } + } else { + // need to be exported + if (!isXMLTextNode(node) && (isValidName || exportInvalidXmlNames)) { + fail("Node " + node.getPath() + " is not exported."); + } + } + return nodeElem; + } + + /** + * Check if a property of a node is exported. This is true if a + * corresponding attribute is found in the element the node is exported to. + * An attribute is corresponding when it has the same name as the given + * property (or it is equal to its escaped name). (chapter 6.4.2.1 point 5 + * of the JCR specification). + * + * @param prop + * @param elem + * @return + * @throws RepositoryException + */ + private Attr findAttribute(Property prop, Element elem) + throws RepositoryException { + + String name = prop.getName(); + + boolean isValidName = XMLChar.isValidName(name); + + name = !isValidName ? escapeNames(name) : name; + Attr attribute = elem.getAttributeNode(name); + return attribute; + } + + /** + * Check if a property should be exported according the three choices + * skipBinary, exportMultivalProps and exportInvalidXmlNames. + * + * @param prop + * @param attribute + * @throws RepositoryException + */ + private void checkAttribute(Property prop, Attr attribute) throws RepositoryException { + + boolean isBinary = (prop.getType() == PropertyType.BINARY); + boolean isMultiple = prop.getDefinition().isMultiple(); + if (skipBinary) { + if (isBinary && !(isMultiple && !exportMultivalProps)) { + assertEquals("Value of binary property " + prop.getPath() + + " exported although skipBinary is true", + attribute.getValue().length(), 0); + } + // check the flags + else { + checkExportFlags(prop, attribute); + } + } + // saveBinary + else { + if (isBinary && !(isMultiple && !exportMultivalProps)) { + assertTrue("Binary property " + prop.getPath() + + " not exported although skipBinary is false", attribute != null); + } + // check anyway the flags + checkExportFlags(prop, attribute); + } + } + + /** + * Checks attribute export regarding the two flags and without considering + * skipBinary. + * + * @param prop + * @param attribute + * @throws RepositoryException + */ + private void checkExportFlags(Property prop, Attr attribute) + throws RepositoryException { + + String name = prop.getName(); + boolean isMultiple = prop.getDefinition().isMultiple(); + boolean isValidName = XMLChar.isValidName(name); + + if (isMultiple) { + if (exportMultivalProps) { + assertTrue("Not all multivalued properties are exported: " + + prop.getPath() + " is not exported.", attribute != null); + } else { + // skipping multi-valued properties entirely is legal + // according to "6.4.2.5 Multi-value Properties" of the + // jsr-170 specification + return; + } + } + // check anyway the other flag + if (exportInvalidXmlNames && !isValidName) { + assertTrue("Not all properties with invalid xml name are exported: " + + prop.getPath() + " is not exported.", attribute != null); + } else { + assertTrue("Property " + prop.getPath() + " is not exported.", + attribute != null); + } + } + + /** + * Compares the given node with the given element. Comparison is succesful + * if the number of exported child nodes and exported properties match the + * found child elements and attributes considering the possible exceptions + * and if the comparison of the properties of the node with the attributes + * of the element is successful too. + * + * @param node + * @param elem + * @throws RepositoryException + */ + private void compareNode(Node node, Element elem) + throws RepositoryException, IOException { + // count the child nodes and compare with the exported child elements + compareChildNumber(node, elem); + // count the properties and compare with attributes exported + comparePropNumber(node, elem); + + PropertyIterator propIter = node.getProperties(); + while (propIter.hasNext()) { + Property prop = propIter.nextProperty(); + Attr attr = findAttribute(prop, elem); + checkAttribute(prop, attr); + if (attr != null) { + compareProperty(prop, attr); + } + } + } + + /** + * Compare the given property with the given attribute. Comparison is + * successful if their values can be matched considering binary type, + * multivalue export. (chapter 6.4.2.1 point 6 of the JCR specification). + * + * @param prop + * @param attr + * @throws RepositoryException + */ + private void compareProperty(Property prop, Attr attr) + throws RepositoryException, IOException { + + boolean isMultiple = prop.getDefinition().isMultiple(); + boolean isBinary = (prop.getType() == PropertyType.BINARY); + String attrVal = attr.getValue(); + String val = null; + if (isMultiple) { + val = exportValues(prop, isBinary); + } else { + if (isBinary) { + try { + attrVal = decodeBase64(attrVal); + val = prop.getString(); + } catch (IOException ioe) { + fail("Could not decode value of binary attribute " + + attr.getName() + " of element " + + attr.getOwnerElement().getTagName()); + } + } else { + val = prop.getString(); + } + } + if (isBinary && skipBinary) { + assertEquals("Value of binary property " + prop.getPath() + + " is not exported correctly: ", "", attrVal); + assertEquals("Value of binary property " + prop.getPath() + + " exported although skipBinary is true", + "", attrVal); + } else { + assertTrue("Value of property " + prop.getPath() + + " is not exported correctly: " + attrVal, + val.equals(attrVal) || escapeValues(val).equals(attrVal)); + } + } + + /** + * Checks if all registered namespaces are exported into the root element. + * (chapter 6.4.2.1 point 1 of the JCR specification). + * + * @param root + * @throws RepositoryException + */ + private void compareNamespaces(Element root) throws RepositoryException { + + Map nameSpaces = new AttributeSeparator(root).getNsAttrs(); + // check if all namespaces exist that were exported + for (Iterator e = nameSpaces.keySet().iterator(); e.hasNext(); ) { + String prefix = e.next(); + String URI = nameSpaces.get(prefix); + + assertEquals("Prefix of uri" + URI + "is not exported correctly.", + nsr.getPrefix(URI), prefix); + assertEquals("Uri of prefix " + prefix + "is not exported correctly.", + nsr.getURI(prefix), URI); + } + + String[] registeredNamespaces = nsr.getURIs(); + // check if all required namespaces are exported + for (int i = 0; i < registeredNamespaces.length; i++) { + String prefix = nsr.getPrefix(registeredNamespaces[i]); + // skip default namespace and xml namespaces + if (prefix.length() == 0 || prefix.startsWith("xml")) { + continue; + } else { + assertTrue("namespace: " + registeredNamespaces[i] + " not exported", nameSpaces.keySet().contains(prefix)); + } + } + } + + /** + * Count the number of child nodes of a node which are exported and compare + * with the number expected. + * + * @param node + * @param elem + * @throws RepositoryException + */ + private void compareChildNumber(Node node, Element elem) throws RepositoryException { + NodeIterator iter = node.getNodes(); + long size = 0; + + long exported = countChildElems(elem); + // child tree is exported too + if (!noRecurse) { + size = getSize(node.getNodes()); + while (iter.hasNext()) { + Node n = iter.nextNode(); + String name = n.getName(); + + // xmltext node ? + if (isXMLTextNode(n)) { + size--; + } + if (!exportInvalidXmlNames && !XMLChar.isValidName(name)) { + size--; + } + + } + } + assertEquals("The number of child nodes of node " + node.getPath() + + " which are exported is not correct: ", size, exported); + } + + /** + * Count the number of exported properties of a given node and compare with + * the number of the properties expected to be exported. + * + * @param node + * @param elem + * @throws RepositoryException + */ + private void comparePropNumber(Node node, Element elem) + throws RepositoryException { + + PropertyIterator iter = node.getProperties(); + long size = getSize(node.getProperties()); + long exported = new AttributeSeparator(elem).getNonNsAttrs().size(); + while (iter.hasNext()) { + Property prop = iter.nextProperty(); + String name = prop.getName(); + boolean isMultiple = prop.getDefinition().isMultiple(); + + // props not exported so we decrease the expected size. + if (!exportInvalidXmlNames && !XMLChar.isValidName(name)) { + size--; + } else if (!exportMultivalProps && isMultiple) { + size--; + } + } + assertEquals("The number of properties exported of node " + node.getPath() + + " is not correct.", size, exported); + } + + /** + * Compares the text of a given XML element with the values of the + * jcr:xmlcharacters properties of the given jcr:xmltext nodes sequel. If + * the sequel has more than one node the serialized values are concatenated + * with a space. We only check the case withHandler is true. + * + * @param nodes + * @param parentElem + * @throws RepositoryException + */ + private void compareXmltextNodes(List nodes, Element parentElem) + throws RepositoryException { + // only this case + if (withHandler) { + String value = ""; + String exportedVal = ""; + + StackEntry currentEntry = (StackEntry) textValuesStack.pop(); + try { + exportedVal = (String) currentEntry.textValues.get(currentEntry.position); + currentEntry.position++; + textValuesStack.push(currentEntry); + } catch (IndexOutOfBoundsException iobe) { + fail("Xmltext nodes not correctly exported: " + iobe.getMessage()); + } + + int size = nodes.size(); + if (size == 1) { + Node node = nodes.get(0); + Property prop = node.getProperty(JCR_XMLDATA); + value = prop.getString(); + assertEquals("The " + JCR_XMLTEXT + " node " + node.getPath() + + " is not exported correctly.", + value, exportedVal); + } else { + // check the concatenated values sequenceally + for (int i = 0; i < nodes.size(); i++) { + Node node = nodes.get(i); + Property prop = node.getProperty(JCR_XMLDATA); + value = prop.getString(); + // the first one + if (i == 0) { + if (exportedVal.regionMatches(0, value, 0, value.length())) { + // test ok, remove the checked part of the text + exportedVal = exportedVal.substring(0, value.length()); + } else { + fail("The " + JCR_XMLTEXT + " node " + node.getPath() + + " is not exported correctly: expected: " + + value + " found: " + exportedVal); + } + } + // we assume at the moment that any white space char is possible + // between two adjacent xmltext nodesso we try to match as long + // as space characters are at the beginning of the + // remaining exported string + // todo once this will be specified in the spec more exactly + else { + // the last one + if (exportedVal.regionMatches(0, value, 0, value.length())) { + // test ok + exportedVal = exportedVal.substring(0, value.length()); + } else { + boolean match = false; + int j = 0; + char c = exportedVal.charAt(j); + while (c == ' ' || c == '\n' || c == '\r' + || c == '\t' || c == '\u000B') { + if (exportedVal.regionMatches(j, value, 0, value.length())) { + exportedVal = exportedVal.substring(j, value.length() + j); + match = true; + break; + } else { + j++; + c = exportedVal.charAt(j); + } + } + assertTrue("The " + JCR_XMLTEXT + " node " + node.getPath() + + " is not exported correctly: expected: " + + value + " found: " + exportedVal, match); + } + } + } + } + } + } + + /** + * Loops through all child items of a given node to test if items with + * invalid xml name are exported. (chapter 6.4.2.4 of the JCR + * specification). + * + * @param node the root node of the tree to search + * @param elem the parent element of the element to which the parent node of + * the given node is exported. + * @throws RepositoryException + */ + + private boolean setExportInvalidXmlNames(Node node, Element elem, boolean isSet) + throws RepositoryException { + + if (!XMLChar.isValidName(node.getName())) { + if (elem != null) { + exportInvalidXmlNames = true; + isSet = true; + } else { + exportInvalidXmlNames = false; + isSet = true; + } + } + + // try properties + if (!isSet) { + PropertyIterator iter = node.getProperties(); + while (iter.hasNext()) { + Property prop = iter.nextProperty(); + if (!exportMultivalProps && prop.getDefinition().isMultiple()) { + continue; + } + if (!XMLChar.isValidName(prop.getName())) { + // property exported? + exportInvalidXmlNames = isExportedProp(prop, elem); + isSet = true; + } + } + } + + // try child nodes + if (!isSet && !noRecurse) { + // search again + NodeIterator iter = node.getNodes(); + while (iter.hasNext()) { + Node n = iter.nextNode(); + Element el = findElem(n, elem); + isSet = setExportInvalidXmlNames(n, el, isSet); + } + } + return isSet; + } + + /** + * Set the exportMultivalProps flag. Traverses the tree given by the node + * and searches a multivalue property which is exported to an attribute of a + * element of an element tree. (chapter 6.4.2.5 of the JCR specification). + * + * @param node + * @param elem + * @throws RepositoryException + */ + private boolean setExportMultivalProps(Node node, Element elem, boolean isSet) + throws RepositoryException { + + Property[] props = searchMultivalProps(node); + // multivalued property with valid xml name + if (props[0] != null) { + exportMultivalProps = isExportedProp(props[0], elem); + isSet = true; + } else { + // invalid xml named multivalue property exported + if (props[1] != null) { + exportMultivalProps = isExportedProp(props[1], elem); + if (!exportMultivalProps && exportInvalidXmlNames) { + isSet = true; + } + } + } + + if (!isSet && !noRecurse) { + // search again + NodeIterator iter = node.getNodes(); + while (iter.hasNext()) { + Node n = iter.nextNode(); + Element el = findElem(n, elem); + if (el != null) { + isSet = setExportMultivalProps(n, el, isSet); + } else { + isSet = false; + } + } + } + return isSet; + } + + //-----------------------------------< helper methods >----------------------------- + /** + * Search a given node if it contains a multivalue property. As invalid xml + * names may be exported or not we want to find a multivalue property with + * valid xml name and also one with an invalid xml name. Returned is a pair + * of multivalued properties, the first has a valid xml name, the second an + * invalid one. In case one of these is not found it is replaced by null in + * the pair. + * + * @param node the node to start the search. + * @return A pair of multivalued properties. + * @throws RepositoryException + */ + private Property[] searchMultivalProps(Node node) throws RepositoryException { + Property[] properties = {null, null}; + for (PropertyIterator props = node.getProperties(); props.hasNext();) { + Property property = props.nextProperty(); + if (property.getDefinition().isMultiple()) { + if (XMLChar.isValidName(property.getName())) { + properties[0] = property; + break; + } else { + properties[1] = property; + } + } + } + return properties; + } + + /** + * Tests if a property is exported or not. + * + * @param prop + * @param elem + * @return + * @throws RepositoryException + */ + private boolean isExportedProp(Property prop, Element elem) throws RepositoryException { + String name = prop.getName(); + name = XMLChar.isValidName(prop.getName()) ? name : escapeNames(name); + Attr attr = elem.getAttributeNode(name); + return (attr != null); + } + + /** + * Checks if a given node is a jcr:xmltext named node and fulfills the + * condition that the property's value is exported as text. + * + * @param node The node to check. + * @return boolean indicating if the given node fulfills the required + * conditions. + * @throws RepositoryException + */ + private boolean isXMLTextNode(Node node) throws RepositoryException { + boolean isTrue = node.getName().equals(JCR_XMLTEXT); + if (node.hasProperty(JCR_XMLDATA)) { + Property prop = node.getProperty(JCR_XMLDATA); + + isTrue = !prop.getDefinition().isMultiple() + && prop.getType() == PropertyType.STRING + // only one property beneath the required jcr:primaryType + && getSize(node.getProperties()) == 2 + && getSize(node.getNodes()) == 0; + } else { + isTrue = false; + } + return isTrue; + } + +//-----------------------------------< static helper methods >----------------------------- + + /** + * Decodes a given base 64 encoded string. + * + * @param str + * @return + * @throws IOException + */ + private static String decodeBase64(String str) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Base64.decode(str, bos); + String decoded = bos.toString("UTF-8"); + return decoded; + } + + /** + * Encodes a given stream to base64. + * + * @param in the stream to encode. + * @return the encoded string in base64. + * @throws IOException if an error occurs. + */ + private static String encodeBase64(InputStream in) throws IOException { + StringWriter writer = new StringWriter(); + Base64.encode(in, writer); + return writer.getBuffer().toString(); + } + + /** + * Exports values of a multivalue property and concatenate the values + * separated by a space. (chapter 6.4.4 of the JCR specification). + * + * @param prop + * @param isBinary + * @return + * @throws RepositoryException + */ + private static String exportValues(Property prop, boolean isBinary) + throws RepositoryException, IOException { + Value[] vals = prop.getValues(); + // order of multi values is preserved. + // multival with empty array is exported as empty string + StringBuffer exportedVal = new StringBuffer(); + + String space = ""; + if (isBinary) { + for (int i = 0; i < vals.length; i++) { + exportedVal.append(space); + InputStream in = vals[i].getStream(); + try { + exportedVal.append(encodeBase64(in)); + } finally { + in.close(); + } + space = " "; + } + } else { + for (int i = 0; i < vals.length; i++) { + exportedVal.append(space); + exportedVal.append(escapeValues(vals[i].getString())); + space = " "; + } + } + return exportedVal.toString(); + } + + /** + * Escapes the characters of a given String representing a Name of an item. + * The escaping scheme is according the requirements of the JSR 170 + * Specification chapter 6.4.3 . No check performed if the given string is + * indeed a Name or not. + * + * @param name + * @return + */ + private static String escapeNames(String name) { + return EscapeJCRUtil.escapeJCRNames(name); + } + + /** + * Escapes the characters of a given string value according the requirements + * of chapter 6.4.4 of JSR 170 Specification. + * + * @param value The string to escape its characters. + * @return + */ + private static String escapeValues(String value) { + return EscapeJCRUtil.escapeJCRValues(value); + } + +//----------------< helpers to retrieve data from an xml document >------------------- + + /** + * Returns all child elements of the given xml element which have the given + * name. + * + * @param elem + * @param name + * @return + */ + private List getChildElems(Element elem, String name) { + List children = new ArrayList(); + org.w3c.dom.Node child = elem.getFirstChild(); + while (child != null) { + if (child.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) { + if (name.equals("*") || name.equals(child.getNodeName())) { + children.add((Element)child); + } + } + child = child.getNextSibling(); + } + return children; + } + + /** + * Counts the number of child elements of the given xml element. + * + * @param elem + * @return + */ + private long countChildElems(Element elem) { + long length = 0; + org.w3c.dom.Node child = elem.getFirstChild(); + while (child != null) { + if (child.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) { + length++; + } + child = child.getNextSibling(); + } + return length; + } + + /** + * Collects the characters of successive text nodes of the given xml element + * into an ArrayList. + * + * @param elem + * @return + */ + private List getChildTextNodeValues(Element elem) { + List textValues = new ArrayList(); + StringBuffer buf = new StringBuffer(); + org.w3c.dom.Node child = elem.getFirstChild(); + // collect the characters of successive text nodes + while (child != null) { + if (child.getNodeType() == org.w3c.dom.Node.TEXT_NODE) { + while (child != null + && child.getNodeType() == org.w3c.dom.Node.TEXT_NODE) { + buf.append(child.getNodeValue()); + child = child.getNextSibling(); + } + textValues.add(buf.toString()); + buf = new StringBuffer(); + } else { + child = child.getNextSibling(); + } + } + return textValues; + } + + /** + * Reads a DOM document from the given XML stream. + * + * @param xml XML stream + * @return DOM document + * @throws RepositoryException if the document could not be read + */ + private Document readDocument(InputStream xml) throws RepositoryException { + try { + StreamSource source = new StreamSource(xml); + DOMResult result = new DOMResult(); + TransformerFactory factory = TransformerFactory.newInstance(); + Transformer transformer = factory.newTransformer(); + transformer.transform(source, result); + return (Document) result.getNode(); + } catch (TransformerException e) { + throw new RepositoryException("Unable to read xml file", e); + } + } + + /** + * Helper class to separate the attributes with xmlns namespace from the + * attributes without xmlns namspace. Solely used for the root element of an + * xml document. + */ + private class AttributeSeparator { + private static final String xmlnsURI = "http://www.w3.org/2000/xmlns/"; + private static final String xmlnsPrefix = "xmlns"; + + NamedNodeMap attrs; + Map nsAttrs; + Map nonNsAttrs; + + AttributeSeparator(Element elem) { + nsAttrs = new HashMap(); + nonNsAttrs = new HashMap(); + attrs = elem.getAttributes(); + separateAttrs(); + } + + public Map getNsAttrs() { + return nsAttrs; + } + + public Map getNonNsAttrs() { + return nonNsAttrs; + } + + private void separateAttrs() { + for (int i = 0; i < attrs.getLength(); i++) { + Attr attribute = (Attr) attrs.item(i); + + if (xmlnsURI.equals(attribute.getNamespaceURI())) { + String localName = attribute.getLocalName(); + // ignore setting default namespace + if (xmlnsPrefix.equals(localName)) { + continue; + } + nsAttrs.put(localName, attribute.getValue()); + } else { + nonNsAttrs.put(attribute.getName(), attribute.getValue()); + } + } + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ExportSysViewTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ExportSysViewTest.java new file mode 100644 index 00000000000..ecf6f335b1c --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ExportSysViewTest.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.InputSource; + +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import java.io.File; +import java.io.IOException; +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +/** + * ExportSysViewTest tests the SysView Export of a tree given by a + * node path. This is done with checking the SAX events of the sysview export + * against the items found by a traverse of the given tree. + * + */ +public class ExportSysViewTest extends AbstractJCRTest { + + private File file; + + private final boolean SKIPBINARY = true, SAVEBINARY = false; + private final boolean NORECURSE = true, RECURSE = false; + + private Session session; + private String testPath; + + + protected void setUp() throws Exception { + isReadOnly = true; + session = getHelper().getReadOnlySession(); + file = File.createTempFile("SysViewExportTest", ".xml"); + + super.setUp(); + this.testPath = testRoot; + } + + protected void tearDown() throws Exception { + file.delete(); + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + public void testExportSysView_handler_session_skipBinary_noRecurse() + throws IOException, RepositoryException, SAXException, IOException { + doTestWithHandler(SKIPBINARY, NORECURSE); + } + + public void testExportSysView_handler_session_skipBinary_recurse() + throws IOException, RepositoryException, SAXException, IOException { + doTestWithHandler(SKIPBINARY, RECURSE); + } + + public void testExportSysView_handler_session_saveBinary_noRecurse() + throws IOException, RepositoryException, SAXException, IOException { + doTestWithHandler(SAVEBINARY, NORECURSE); + } + + public void testExportSysView_handler_session_saveBinary_recurse() + throws IOException, RepositoryException, SAXException, IOException { + doTestWithHandler(SAVEBINARY, RECURSE); + } + + public void testExportSysView_stream_session_skipBinary_recurse() + throws IOException, RepositoryException, SAXException { + doTestWithStream(SKIPBINARY, RECURSE); + } + + public void testExportSysView_stream_session_skipBinary_noRecurse() + throws IOException, RepositoryException, SAXException { + doTestWithStream(SKIPBINARY, NORECURSE); + } + + public void testExportSysView_stream_session_saveBinary_noRecurse() + throws IOException, RepositoryException, SAXException { + doTestWithStream(SAVEBINARY, NORECURSE); + } + + public void testExportSysView_stream_session_saveBinary_recurse() + throws IOException, RepositoryException, SAXException { + doTestWithStream(SAVEBINARY, RECURSE); + } + + /** + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + public void doTestWithHandler(boolean skipBinary, boolean noRecurse) + throws RepositoryException, SAXException, IOException { + + ContentHandler contentHandler = new SysViewContentHandler(testPath, session, skipBinary, noRecurse); + session.exportSystemView(testPath, contentHandler, skipBinary, noRecurse); + } + + /** + * @throws RepositoryException + * @throws SAXException + * @throws IOException + */ + public void doTestWithStream(boolean skipBinary, boolean noRecurse) + throws RepositoryException, SAXException, IOException { + + BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file)); + + try { + session.exportSystemView(testPath, os, false, false); + SysViewParser parser = new SysViewParser(testPath, session, SAVEBINARY, RECURSE); + parser.parse(file); + } catch (RepositoryException re) { + fail("Could not initialize the contenthandler due to: " + re.toString()); + } finally { + os.close(); + } + + } + + /** + * class to parse the XML file generated by the sysview export using an + * OutputStream + */ + protected class SysViewParser { + //todo : test encoding of exported file + // the path to the exported file + String filePath; + // the absolut path to the node which was exported + String nodePath; + Node node; + SAXParser parser; + SysViewContentHandler handler; + + public SysViewParser(String nodePath, Session session, boolean skipBinary, boolean noRecurse) + throws SAXException, RepositoryException { + this.nodePath = nodePath; + this.handler = new SysViewContentHandler(nodePath, session, skipBinary, noRecurse); + try { + parser = SAXParserFactory.newInstance().newSAXParser(); + } catch (ParserConfigurationException e) { + throw new SAXException(e); + } + } + + public void parse(File file) throws IOException, SAXException { + FileInputStream in = null; + try { + in = new FileInputStream(file); + } catch (FileNotFoundException e) { + fail("Input file not opened: " + e); + } + InputSource source = new InputSource(in); + parser.parse(source, handler); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/GetWeakReferencesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/GetWeakReferencesTest.java new file mode 100644 index 00000000000..04a05483601 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/GetWeakReferencesTest.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.test.api; + +import javax.jcr.Node; +import javax.jcr.Value; +import javax.jcr.RepositoryException; +import javax.jcr.PropertyIterator; +import javax.jcr.Property; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * GetWeakReferencesTest checks implementation of + * {@link Node#getWeakReferences()} and {@link Node#getWeakReferences(String)}. + */ +public class GetWeakReferencesTest extends AbstractJCRTest { + + private Node target; + + private Node referring; + + protected void setUp() throws Exception { + super.setUp(); + target = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(target, mixReferenceable); + referring = testRootNode.addNode(nodeName2, testNodeType); + superuser.save(); + } + + public void testSingleValue() throws RepositoryException { + Value weakRef = vf.createValue(target, true); + referring.setProperty(propertyName1, weakRef); + superuser.save(); + + PropertyIterator it = target.getWeakReferences(); + assertTrue("no weak references returned", it.hasNext()); + Property p = it.nextProperty(); + assertEquals("wrong weak reference property", referring.getProperty(propertyName1).getPath(), p.getPath()); + assertFalse("no more weak references expected", it.hasNext()); + } + + public void testSingleValueWithName() throws RepositoryException { + Value weakRef = vf.createValue(target, true); + referring.setProperty(propertyName1, weakRef); + superuser.save(); + + PropertyIterator it = target.getWeakReferences(propertyName1); + assertTrue("no weak references returned", it.hasNext()); + Property p = it.nextProperty(); + assertEquals("wrong weak reference property", referring.getProperty(propertyName1).getPath(), p.getPath()); + assertFalse("no more weak references expected", it.hasNext()); + } + + public void testMultiValues() throws RepositoryException { + Value weakRef = vf.createValue(target, true); + Value[] refs = new Value[]{weakRef, weakRef}; + referring.setProperty(propertyName1, refs); + superuser.save(); + + PropertyIterator it = target.getWeakReferences(); + assertTrue("no weak references returned", it.hasNext()); + Property p = it.nextProperty(); + assertEquals("wrong weak reference property", referring.getProperty(propertyName1).getPath(), p.getPath()); + assertFalse("no more weak references expected", it.hasNext()); + } + + public void testMultiValuesWithName() throws RepositoryException { + Value weakRef = vf.createValue(target, true); + Value[] refs = new Value[]{weakRef, weakRef}; + referring.setProperty(propertyName1, refs); + superuser.save(); + + PropertyIterator it = target.getWeakReferences(propertyName1); + assertTrue("no weak references returned", it.hasNext()); + Property p = it.nextProperty(); + assertEquals("wrong weak reference property", referring.getProperty(propertyName1).getPath(), p.getPath()); + assertFalse("no more weak references expected", it.hasNext()); + } + + public void testNonReferenceable() throws RepositoryException, NotExecutableException { + Node nonReferenceable = null; + if (testRootNode.isNodeType(mixReferenceable)) { + Node child = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + if (!child.isNodeType(mixReferenceable)) { + nonReferenceable = child; + } + } else { + nonReferenceable = testRootNode; + } + + if (nonReferenceable == null) { + throw new NotExecutableException("Test node is referenceable."); + } + + // getWeakReferences must return an empty iterator and must not throw. + assertFalse(nonReferenceable.getWeakReferences().hasNext()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/HasPermissionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/HasPermissionTest.java new file mode 100644 index 00000000000..cb2211b7145 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/HasPermissionTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Session; +import javax.jcr.Node; + +/** + * Tests if {@link javax.jcr.Session#hasPermission(String, String)} yields the + * correct permissions for a read-only, a read-write and an admin session. + */ +public class HasPermissionTest extends AbstractJCRTest { + + private static final String ACTION_ALL = Session.ACTION_READ + "," + Session.ACTION_ADD_NODE + "," + Session.ACTION_REMOVE + "," + Session.ACTION_SET_PROPERTY; + + /** + * Tests if Session.hasPermission(String, String) returns + *

          + *
        • true for READ.
        • + *
        • false for all other actions.
        • + *
        + */ + public void testReadOnlyPermission() throws Exception { + Node n = testRootNode.addNode(nodeName2, testNodeType); + superuser.save(); + + Session readOnly = getHelper().getReadOnlySession(); + try { + String path = n.getPath(); + + assertTrue(readOnly.hasPermission(path, Session.ACTION_READ)); + assertFalse(readOnly.hasPermission(path + "newNode", Session.ACTION_ADD_NODE)); + assertFalse(readOnly.hasPermission(path, Session.ACTION_REMOVE)); + assertFalse(readOnly.hasPermission(path, Session.ACTION_SET_PROPERTY)); + assertFalse(readOnly.hasPermission(path, ACTION_ALL)); + assertFalse(readOnly.hasPermission(path, Session.ACTION_REMOVE + "," + Session.ACTION_SET_PROPERTY)); + } finally { + readOnly.logout(); + } + } + + /** + * Tests if Session.hasPermission(String, String) returns + *
          + *
        • true for all actions.
        • + *
        + */ + public void testReadWritePermission() throws Exception { + Node n = testRootNode.addNode(nodeName2, testNodeType); + superuser.save(); + + String path = n.getPath(); + Session rwSession = getHelper().getReadWriteSession(); + try { + assertTrue(rwSession.hasPermission(path, Session.ACTION_READ)); + assertTrue(rwSession.hasPermission(path + "newNode", Session.ACTION_ADD_NODE)); + assertTrue(rwSession.hasPermission(path, Session.ACTION_REMOVE)); + assertTrue(rwSession.hasPermission(path, Session.ACTION_SET_PROPERTY)); + assertTrue(rwSession.hasPermission(path, ACTION_ALL)); + } finally { + rwSession.logout(); + } + } + + + /** + * Tests if Session.hasPermission(String, String) returns + *
          + *
        • true for all actions.
        • + *
        + */ + public void testAdminPermission() throws Exception { + Node n = testRootNode.addNode(nodeName2, testNodeType); + superuser.save(); + + String path = n.getPath(); + + assertTrue(superuser.hasPermission(path, Session.ACTION_READ)); + assertTrue(superuser.hasPermission(path + "newNode", Session.ACTION_ADD_NODE)); + assertTrue(superuser.hasPermission(path, Session.ACTION_REMOVE)); + assertTrue(superuser.hasPermission(path, Session.ACTION_SET_PROPERTY)); + assertTrue(superuser.hasPermission(path, ACTION_ALL)); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ImpersonateTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ImpersonateTest.java new file mode 100644 index 00000000000..fd6fd64f305 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ImpersonateTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Session; +import javax.jcr.Credentials; +import javax.jcr.NodeIterator; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.LoginException; +import java.security.AccessControlException; + +/** + * Tests if {@link Session#impersonate(Credentials)} to a read-only session + * respects access controls. + * + */ +public class ImpersonateTest extends AbstractJCRTest { + + /** + * Tests if Session.impersonate(Credentials) works properly + */ + public void testImpersonate() throws RepositoryException, NotExecutableException { + // impersonate to read-only user + Session session; + try { + session = superuser.impersonate(getHelper().getReadOnlyCredentials()); + } catch (LoginException e) { + throw new NotExecutableException("impersonate threw LoginException"); + } + + try { + // get a path to test the permissions on + String thePath = ""; + NodeIterator ni = session.getRootNode().getNodes(); + while (ni.hasNext()) { + Node n = ni.nextNode(); + if (!n.getPath().equals("/" + jcrSystem)) { + thePath = n.getPath(); + break; + } + } + + // check that all 4 permissions are granted/denied correctly + session.checkPermission(thePath, "read"); + + try { + session.checkPermission(thePath + "/" + nodeName1, "add_node"); + fail("add_node permission on \"" + thePath + "/" + nodeName1 + "\" granted to read-only Session"); + } catch (AccessControlException success) { + // ok + } + + try { + session.checkPermission(thePath + "/" + propertyName1, "set_property"); + fail("set_property permission on \"" + thePath + "/" + propertyName1 + "\" granted to read-only Session"); + } catch (AccessControlException success) { + // ok + } + + try { + session.checkPermission(thePath, "remove"); + fail("remove permission on \"" + thePath + "\" granted to read-only Session"); + } catch (AccessControlException success) { + // ok + } + + } finally { + session.logout(); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/LifecycleTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/LifecycleTest.java new file mode 100644 index 00000000000..996899402a9 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/LifecycleTest.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.test.api; + +import javax.jcr.InvalidLifecycleTransitionException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * Compliance tests for section 6.12 Lifecycle Management. + * + */ +public class LifecycleTest extends AbstractJCRTest { + + private String path; + + private String transition; + + protected void setUp() throws Exception { + super.setUp(); + checkSupportedOption(Repository.OPTION_LIFECYCLE_SUPPORTED); + ensureKnowsNodeType(superuser, NodeType.MIX_LIFECYCLE); + + path = getProperty("lifecycleNode"); + if (path == null) { + path = testRoot + "/lifecycle"; + } + + transition = getProperty("lifecycleTransition"); + if (transition == null) { + transition = "identity"; + } + } + + public void testGetAllowedLifecycleTransitions() + throws RepositoryException, NotExecutableException { + Node node = superuser.getNode(path); + try { + String[] transitions = node.getAllowedLifecycleTransistions(); + assertNotNull( + "Return value of getAllowedLifecycleTransitions is null", + transitions); + + for (int i = 0; i < transitions.length; i++) { + if (transition.equals(transitions[i])) { + return; + } + } + fail("Configured lifecycle transition \"" + transition + + "\" is not among the allowed transitions from node " + + path); + } catch (UnsupportedRepositoryOperationException e) { + fail("Unable to get allowed lifecycle transitions for node " + + path + ": " + e.getMessage()); + } + } + + public void testFollowLifecycleTransition() + throws RepositoryException, NotExecutableException { + Node node = superuser.getNode(path); + try { + node.followLifecycleTransition(transition); + // Note that there is nothing much here for us to check, + // as the spec doesn't specify any fixed behaviour for + // this method! + } catch (UnsupportedRepositoryOperationException e) { + fail("Unable to follow lifecycle transition \"" + transition + + "\" for node " + path + ": " + e.getMessage()); + } catch (InvalidLifecycleTransitionException e) { + fail("Unable to follow lifecycle transition \"" + transition + + "\" for node " + path + ": " + e.getMessage()); + } + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/LongPropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/LongPropertyTest.java similarity index 78% rename from src/test/org/apache/jackrabbit/test/api/LongPropertyTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/LongPropertyTest.java index ea154110464..73cfb54f7e4 100644 --- a/src/test/org/apache/jackrabbit/test/api/LongPropertyTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/LongPropertyTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -30,10 +30,6 @@ * property a {@link org.apache.jackrabbit.test.NotExecutableException} is * thrown. * - * @test - * @sources LongPropertyTest.java - * @executeClass org.apache.jackrabbit.test.api.LongPropertyTest - * @keywords level1 */ public class LongPropertyTest extends AbstractPropertyTest { @@ -46,6 +42,14 @@ protected int getPropertyType() { return PropertyType.LONG; } + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + /** * Tests that Property.getLong() delivers the same as Value.getLong() and if * in case of a multivalue property a ValueFormatException is thrown. @@ -59,10 +63,11 @@ public void testValue() throws RepositoryException { } catch (ValueFormatException vfe) { // ok } + } else { + long l = prop.getValue().getLong(); + long ll = prop.getLong(); + assertEquals("Value.getLong() and Property.getLong() return different values.", l, ll); } - long l = prop.getValue().getLong(); - long ll = prop.getLong(); - assertEquals("Value.getLong() and Property.getLong() return different values.", l, ll); } /** @@ -105,7 +110,7 @@ public void testGetDouble() throws RepositoryException { public void testGetStream() throws RepositoryException, IOException { Value val = PropertyUtil.getValue(prop); BufferedInputStream in = new BufferedInputStream(val.getStream()); - Value otherVal = prop.getValue(); + Value otherVal = PropertyUtil.getValue(prop); InputStream ins = null; byte[] utf8bytes = otherVal.getString().getBytes(UTF8); // if yet utf-8 encoded these bytes should be equal @@ -119,17 +124,15 @@ public void testGetStream() throws RepositoryException, IOException { } try { val.getLong(); - fail("Non stream method call after stream method call " + - "should throw an IllegalStateException."); } catch (IllegalStateException ise) { - //ok + fail("Non stream method call after stream method call " + + "should not throw an IllegalStateException."); } try { ins = otherVal.getStream(); - fail("Stream method call after a non stream method call " + - "should throw an IllegalStateException."); } catch (IllegalStateException ise) { - // ok + fail("Stream method call after a non stream method call " + + "should not throw an IllegalStateException."); } finally { if (in != null) { in.close(); @@ -162,22 +165,45 @@ public void testGetType() throws RepositoryException { } /** - * Tests failure of conversion from Long type to Reference type. + * Tests failure of conversion from Long type to Reference, WeakReference or Path type. */ - public void testAsReference() throws RepositoryException { + public void testGetNode() throws RepositoryException { if (!multiple) { try { prop.getNode(); - fail("Conversion from a Double value to a Reference value " + + fail("Conversion from a Long value to a Reference or Path value " + "should throw a ValueFormatException."); - } catch (ValueFormatException vfe) { - //ok + } catch (ValueFormatException e) { + // success. } } else { try { prop.getNode(); fail("Property.getNode() called on a multivalue property " + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + } + + /** + * Tests failure of conversion from Long type to Path type. + */ + public void testGetProperty() throws RepositoryException { + if (!multiple) { + try { + prop.getProperty(); + fail("Conversion from a Long value to a Path value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException e) { + // success. + } + } else { + try { + prop.getProperty(); + fail("Property.getProperty() called on a multivalue property " + + "should throw a ValueFormatException."); } catch (ValueFormatException vfe) { // ok } @@ -233,4 +259,4 @@ public void testGetLengths() throws RepositoryException { } } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamePropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamePropertyTest.java new file mode 100644 index 00000000000..b58dc4267cd --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamePropertyTest.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.test.api; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.PropertyType; +import javax.jcr.Property; +import javax.jcr.Node; + +/** + * Tests a date property. If the workspace does not contain a node with a date + * property a {@link org.apache.jackrabbit.test.NotExecutableException} is + * thrown. + * + */ +public class NamePropertyTest extends AbstractPropertyTest { + + /** + * Returns {@link javax.jcr.PropertyType#NAME}. + * + * @return {@link javax.jcr.PropertyType#NAME}. + */ + protected int getPropertyType() { + return PropertyType.NAME; + } + + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + + /** + * Tests conversion from Name type to String type. + * + * @throws RepositoryException + */ + public void testGetString() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + assertTrue("Not a valid Name property: " + prop.getName(), + PropertyUtil.checkNameFormat(val.getString(), session)); + + } + + /** + * Tests failure of conversion from Name type to Boolean type. + * + * @throws RepositoryException + */ + public void testGetBoolean() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getBoolean(); + fail("Conversion from a Name value to a Boolean value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests failure of conversion from Name type to Date type. + * + * @throws RepositoryException + */ + public void testGetDate() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getDate(); + fail("Conversion from a Name value to a Date value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests failure from Name type to Double type. + * + * @throws RepositoryException + */ + public void testGetDouble() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getDouble(); + fail("Conversion from a Name value to a Double value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests failure of conversion from Name type to Long type. + * + * @throws RepositoryException + */ + public void testGetLong() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getLong(); + fail("Conversion from a Name value to a Long value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests if Value.getType() returns the same as Property.getType() and also + * tests that prop.getDefinition().getRequiredType() returns the same type + * in case it is not of Undefined type. + * + * @throws RepositoryException + */ + public void testGetType() throws RepositoryException { + assertTrue("Value.getType() returns wrong type.", + PropertyUtil.checkGetType(prop, PropertyType.NAME)); + } + + /** + * Since JCR 2.0 a path property can be dereferenced if it points to a + * Node. + * TODO: create several tests out of this one + */ + public void testGetNode() throws RepositoryException { + if (!multiple) { + String path = prop.getString(); + if (prop.getParent().hasNode(path)) { + Node n = prop.getNode(); + assertEquals("The name of the dereferenced property must be equal to the value", path, n.getName()); + } else { + try { + prop.getNode(); + fail("Calling Property.getNode() for a NAME value that doesn't have a corresponding Node, ItemNotFoundException is expected"); + } catch (ItemNotFoundException e) { + // success. + } + } + } else { + try { + prop.getNode(); + fail("Property.getNode() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + } + + /** + * Since JCR 2.0 a path property can be dereferenced if it points to a + * Property. + * TODO: create several tests out of this one + */ + public void testGetProperty() throws RepositoryException { + if (!multiple) { + String path = prop.getString(); + if (prop.getParent().hasProperty(path)) { + Property p = prop.getProperty(); + assertEquals("The name of the dereferenced property must be equal to the value", path, p.getName()); + } else { + try { + prop.getProperty(); + fail("Calling Property.getProperty() for a NAME value that doesn't have a corresponding Node, ItemNotFoundException is expected"); + } catch (ItemNotFoundException e) { + // success. + } + } + } else { + try { + prop.getProperty(); + fail("Property.getNode() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NameTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NameTest.java new file mode 100644 index 00000000000..2c08dcd040b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NameTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.RepositoryStub; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.Property; +import javax.jcr.Workspace; + +/** + * NameTest... + */ +public class NameTest extends AbstractJCRTest { + + private String getExpandedName(String jcrName) throws RepositoryException { + if (jcrName.startsWith("{")) { + return jcrName; + } else { + int pos = jcrName.indexOf(":"); + String prefix = (pos > -1) ? jcrName.substring(0, pos) : ""; + String uri = superuser.getNamespaceURI(prefix); + return "{" + uri + "}" + jcrName.substring(pos+1); + } + } + + /** + * Expanded names must always be resolved. + * Test NAME-value creation. + * + * @throws RepositoryException + */ + public void testExpandedNameValue() throws RepositoryException { + ValueFactory vf = superuser.getValueFactory(); + Value nameValue = vf.createValue(Workspace.NAME_VERSION_STORAGE_NODE, PropertyType.NAME); + + assertEquals(PropertyType.NAME, nameValue.getType()); + assertEquals(nameValue.getString(), vf.createValue("jcr:versionStorage", PropertyType.NAME).getString()); + assertEquals(nameValue, vf.createValue("jcr:versionStorage", PropertyType.NAME)); + assertEquals("jcr:versionStorage", nameValue.getString()); + } + + /** + * Expanded names must always be resolved. + * Test setting a NAME-value property. + * + * @throws RepositoryException + */ + public void testExpandedNameValueProperty() throws RepositoryException { + ValueFactory vf = superuser.getValueFactory(); + Value nameValue = vf.createValue(Workspace.NAME_VERSION_STORAGE_NODE, PropertyType.NAME); + + Property p = testRootNode.setProperty(propertyName1, nameValue); + assertEquals(PropertyType.NAME, p.getType()); + assertEquals(nameValue.getString(), p.getValue().getString()); + assertEquals(nameValue, p.getValue()); + assertEquals("jcr:versionStorage", p.getString()); + } + + /** + * Test if the name of property created with an expanded name is properly + * return as standard JCR name. + * + * @throws RepositoryException + */ + public void testExpandedNameItem() throws RepositoryException { + String propName = getExpandedName(propertyName1); + Property p = testRootNode.setProperty(propName, getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE1, RepositoryStub.PROP_PROP_TYPE1, "test")); + + assertEquals(propertyName1, p.getName()); + } + + /** + * Test if creating a node with an expanded node type name returns the proper + * standard JCR node type name. + * + * @throws RepositoryException + */ + public void testExpandedNodeTypeName() throws RepositoryException { + String nodeName = getExpandedName(nodeName1); + String ntName = getExpandedName(testNodeType); + Node n = testRootNode.addNode(nodeName, ntName); + + assertEquals(nodeName1, n.getName()); + assertEquals(testNodeType, n.getPrimaryNodeType().getName()); + } +} \ No newline at end of file diff --git a/src/test/org/apache/jackrabbit/test/api/NamespaceRegistryReadMethodsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRegistryReadMethodsTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/NamespaceRegistryReadMethodsTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRegistryReadMethodsTest.java index a344f3837f7..a276a35d3b2 100644 --- a/src/test/org/apache/jackrabbit/test/api/NamespaceRegistryReadMethodsTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRegistryReadMethodsTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -33,10 +33,6 @@ * NamespaceRegistry class and also the correct Exception throwing for methods * not supported in level 1. * - * @test - * @sources NamespaceRegistryReadMethodsTest.java - * @executeClass org.apache.jackrabbit.test.api.NamespaceRegistryReadMethodsTest - * @keywords level1 */ public class NamespaceRegistryReadMethodsTest extends AbstractJCRTest { @@ -49,20 +45,35 @@ public class NamespaceRegistryReadMethodsTest extends AbstractJCRTest { /** The NamespaceRegistry of the repository */ private NamespaceRegistry nsr; + /** A read-only session */ + private Session session; + public void setUp() throws Exception { isReadOnly = true; super.setUp(); - Session session = helper.getReadOnlySession(); + session = getHelper().getReadOnlySession(); Workspace ws = session.getWorkspace(); nsr = ws.getNamespaceRegistry(); } + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + nsr = null; + super.tearDown(); + } + /** * Tests if {@link javax.jcr.NamespaceRegistry#getPrefixes()} returns the * required namespace prefixes and if they are mapped to the correct URIs. */ public void testGetNamespacePrefixes() throws RepositoryException { - Set prefixes = new HashSet(); + Set prefixes = new HashSet(); prefixes.addAll(Arrays.asList(nsr.getPrefixes())); for (int i = 0; i < BUILTIN_PREFIXES.length; i++) { String prefix = BUILTIN_PREFIXES[i]; @@ -77,7 +88,7 @@ public void testGetNamespacePrefixes() throws RepositoryException { * required namespace URIs and if they are mapped to the correct prefixes. */ public void testGetNamespaceURIs() throws RepositoryException { - Set uris = new HashSet(); + Set uris = new HashSet(); uris.addAll(Arrays.asList(nsr.getURIs())); for (int i = 0; i < BUILTIN_URIS.length; i++) { String uri = BUILTIN_URIS[i]; @@ -93,7 +104,7 @@ public void testGetNamespaceURIs() throws RepositoryException { * unknown prefix. */ public void testGetURINamespaceException() throws RepositoryException, NotExecutableException { - Set prefixes = new HashSet(); + Set prefixes = new HashSet(); prefixes.addAll(Arrays.asList(nsr.getPrefixes())); String prefix = "myapp"; int count = 0; @@ -116,7 +127,7 @@ public void testGetURINamespaceException() throws RepositoryException, NotExecut * unknown URI. */ public void testGetPrefixNamespaceException() throws RepositoryException, NotExecutableException { - Set uris = new HashSet(); + Set uris = new HashSet(); uris.addAll(Arrays.asList(nsr.getURIs())); String uri = "http://www.unknown-company.com/namespace"; int count = 0; @@ -133,4 +144,4 @@ public void testGetPrefixNamespaceException() throws RepositoryException, NotExe } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRegistryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRegistryTest.java new file mode 100644 index 00000000000..1e54e1f43b8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRegistryTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Item; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; +import javax.jcr.NamespaceException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * NamespaceRegistryTest tests whether the repository registers and + * unregisters namespaces correctly. This is a level 2 feature. + *

        + * NOTE: Implementations are free to not support unregistering. In other words: + * Even a repository that supports namespaces may always legally throw an + * exception when you try to unregister. + * + */ +public class NamespaceRegistryTest extends AbstractJCRTest { + + /** The system namespace prefixes */ + private static final String[] SYSTEM_PREFIXES = {"jcr", "nt", "mix", "sv"}; + + /** Default value of test prefix */ + private static final String TEST_PREFIX = "tst"; + + /** Default value of test namespace uri */ + private static final String TEST_URI = "http://www.apache.org/jackrabbit/test/namespaceRegistryTest"; + + /** The namespace registry of the superuser session */ + private NamespaceRegistry nsp; + + /** The namespace prefix we use for the tests */ + private String namespacePrefix; + + /** The namespace uri we use for the tests */ + private String namespaceUri; + + protected void setUp() throws Exception { + super.setUp(); + nsp = superuser.getWorkspace().getNamespaceRegistry(); + + namespacePrefix = getUnusedPrefix(); + namespaceUri = getUnusedURI(); + } + + protected void tearDown() throws Exception { + try { + if (Arrays.asList(nsp.getPrefixes()).contains(namespacePrefix)) { + nsp.unregisterNamespace(namespacePrefix); + } + } catch (NamespaceException e) { + log.println("Unable to unregister name space with prefix " + namespacePrefix + ": " + e.toString()); + } finally { + nsp = null; + super.tearDown(); + } + } + + /** + * Trying to register a system namespace must throw a NamespaceException + */ + public void testRegisterNamespaceExceptions() throws RepositoryException { + try { + nsp.registerNamespace("jcr", namespaceUri); + fail("Trying to register the namespace 'jcr' must throw a NamespaceException."); + } catch (NamespaceException e) { + // expected behaviour + } + try { + nsp.registerNamespace("nt", namespaceUri); + fail("Trying to register the namespace 'nt' must throw a NamespaceException."); + } catch (NamespaceException e) { + // expected behaviour + } + try { + nsp.registerNamespace("mix", namespaceUri); + fail("Trying to register the namespace 'mix' must throw a NamespaceException."); + } catch (NamespaceException e) { + // expected behaviour + } + try { + nsp.registerNamespace("sv", namespaceUri); + fail("Trying to register the namespace 'sv' must throw a NamespaceException."); + } catch (NamespaceException e) { + // expected behaviour + } + } + + /** + * Trying to register "xml" or anything that starts with "xml" as a + * namespace must throw a repository exception + */ + public void testRegisterNamespaceXmlExceptions() throws RepositoryException { + try { + nsp.registerNamespace("xml", namespaceUri); + fail("Trying to register the namespace 'xml' must throw a NamespaceException."); + } catch (NamespaceException e) { + // expected behaviour + } + try { + nsp.registerNamespace("xml" + Math.floor(Math.random() * 999999), namespaceUri); + fail("Trying to register a namespace that starts with 'xml' must throw a NamespaceException."); + } catch (NamespaceException e) { + // expected behaviour + } + } + + /** + * Tries to register a namespace. + */ + public void testRegisterNamespace() throws RepositoryException { + nsp.registerNamespace(namespacePrefix, namespaceUri); + + assertEquals("Namespace prefix was not registered.", namespacePrefix, nsp.getPrefix(namespaceUri)); + assertEquals("Namespace URI was not registered.", namespaceUri, nsp.getURI(namespacePrefix)); + + Item created; + + try { + created = testRootNode.addNode(namespacePrefix + ":root"); + testRootNode.getSession().save(); + } + catch (RepositoryException ex) { + // that didn't work; maybe the repository allows a property here? + testRootNode.getSession().refresh(false); + created = testRootNode.setProperty(namespacePrefix + ":root", "test"); + testRootNode.getSession().save(); + } + + // Need to remove it here, otherwise teardown can't unregister the NS. + testRootNode.getSession().getItem(created.getPath()).remove(); + testRootNode.getSession().save(); + } + + /** + * Tests whether unregistering a system namespace or an undefined namespace + * throws the expected exception. + */ + public void testUnregisterNamespaceExceptions() throws RepositoryException { + // Attempting to unregister a built-in namespace + // must throw a NamespaceException. + for (int t = 0; t < SYSTEM_PREFIXES.length; t++) { + try { + nsp.unregisterNamespace(SYSTEM_PREFIXES[t]); + fail("Trying to unregister " + SYSTEM_PREFIXES[t] + " must fail"); + } catch (NamespaceException e) { + // expected behaviour + } + } + + // An attempt to unregister a namespace that is not currently registered + // must throw a NamespaceException. + try { + nsp.unregisterNamespace("ThisNamespaceIsNotCurrentlyRegistered"); + fail("Trying to unregister an unused prefix must fail"); + } catch (NamespaceException e) { + // expected behaviour + } + } + + //------------------------< internal >-------------------------------------- + + /** + * Returns a namespace prefix that currently not used in the namespace + * registry. + * @return an unused namespace prefix. + */ + private String getUnusedPrefix() throws RepositoryException { + Set prefixes = new HashSet(Arrays.asList(nsp.getPrefixes())); + String prefix = TEST_PREFIX; + int i = 0; + while (prefixes.contains(prefix)) { + prefix += i++; + } + return prefix; + } + + /** + * Returns a namespace URI that currently not used in the namespace + * registry. + * @return an unused namespace URI. + */ + private String getUnusedURI() throws RepositoryException { + Set uris = new HashSet(Arrays.asList(nsp.getURIs())); + String uri = TEST_URI; + int i = 0; + while (uris.contains(uri)) { + uri += i++; + } + return uri; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRemappingTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRemappingTest.java new file mode 100644 index 00000000000..37c7561bd93 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NamespaceRemappingTest.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.test.api; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Test cases for JSR 283 sections 3.5.2 Session-Local Mappings and + * 5.11 Namespace Mapping and the related namespace mapping methods + * in {@link Session}. + * + */ +public class NamespaceRemappingTest extends AbstractJCRTest { + + /** + * The read only session for the test cases + */ + private Session session; + + /** + * The namespace registry of the current session + */ + private NamespaceRegistry nsr; + + /** + * Sets up the fixture for the tests. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + nsr = session.getWorkspace().getNamespaceRegistry(); + } + + protected void tearDown() throws Exception { + try { + if (session != null) { + session.logout(); + session = null; + } + } finally { + nsr = null; + super.tearDown(); + } + } + + /** + * Tests if the remapping of jcr:primaryType to a different prefix works and + * returns the property with the correct primaryType value. + */ + public void testNamespaceRemapping() throws RepositoryException { + Property primaryTypeProp = session.getRootNode().getProperty(jcrPrimaryType); + NodeType ntBaseType = session.getWorkspace().getNodeTypeManager().getNodeType(ntBase); + + // find an unused prefix + String jcrPrefix = getUnusedPrefix(); + // remap jcr prefix + session.setNamespacePrefix(jcrPrefix, NS_JCR_URI); + // find an unused prefix + String ntPrefix = getUnusedPrefix(); + // remap nt prefix + session.setNamespacePrefix(ntPrefix, NS_NT_URI); + + assertTrue("Unable to retrieve property with new namespace prefix.", + session.getRootNode().getProperty(jcrPrefix + ":primaryType").isSame(primaryTypeProp)); + + assertEquals("NodeType name does not use new namespace prefix.", + ntBaseType.getName(), ntPrefix + ":base"); + + String propval = session.getRootNode().getProperty(jcrPrefix + ":primaryType").getString(); + String primaryType = session.getRootNode().getPrimaryNodeType().getName(); + assertEquals("Remapping of jcr prefix failed", primaryType, propval); + } + + /** + * Test case for the automatic generation of a new local prefix for a + * registered namespace URI that doesn't have a local mapping, as specified + * in section 3.5.2 Session-Local Mappings: + * + *

        + * If a JCR method returns a name from the repository with a namespace URI + * for which no local mapping exists, a prefix is created automatically + * and a mapping between that prefix and the namespace URI in question is + * added to the set of local mappings. The new prefix must differ from + * those already present among the set of local mappings. + *
        + */ + public void testAutomaticNewLocalPrefix() throws RepositoryException { + Set prefixes = + new HashSet(Arrays.asList(session.getNamespacePrefixes())); + prefixes.remove(session.getNamespacePrefix(NS_JCR_URI)); + prefixes.remove(session.getNamespacePrefix(NS_NT_URI)); + + // Remove the local mapping of NS_JCR_URI + // NOTE: JCR 1.0 repositories will throw an exception on this + String before = session.getNamespacePrefix(NS_JCR_URI); + session.setNamespacePrefix(before, NS_NT_URI); + + // Have the repository come up with a new prefix for this namespace + String name = + session.getProperty("/{" + NS_JCR_URI + "}primaryType").getName(); + int colon = name.indexOf(':'); + String after = name.substring(0, colon); + + assertFalse( + "Automatically created new local prefix of a namespace" + + " must be different from the prefix that removed the mapping", + after.equals(before)); + + assertFalse( + "Automatically created new local prefix of a namespace" + + " must be different from those already present", + prefixes.contains(after)); + + try { + assertEquals( + "The local namespace mappings must match the" + + " automatically created new prefix of a namespace", + after, session.getNamespacePrefix(NS_JCR_URI)); + } catch (NamespaceException e) { + fail("Automatically created new prefix must be included in" + + " the set of local namespace mappings"); + } + } + + /** + * Test case for the unknown prefix behaviour specified in + * section 3.5.2 Session-Local Mappings: + * + *
        + * If a JCR method is passed a name or path containing a prefix which + * does not exist in the local mapping an exception is thrown. + *
        + */ + public void testExceptionOnUnknownPrefix() throws RepositoryException { + // Change the local prefix of of NS_JCR_URI + // NOTE: JCR 1.0 repositories will throw an exception on this + String before = session.getNamespacePrefix(NS_JCR_URI); + String after = before + "-changed"; + session.setNamespacePrefix(after, NS_JCR_URI); + + // Try to use the changed prefix + try { + session.propertyExists("/" + before + ":primaryType"); + fail("A path with an unknown prefix must cause" + + " an exception to be thrown"); + } catch (RepositoryException expected) { + } + } + + /** + * Test case for the initial set of local namespace mappings as specified + * in section 3.5.2 Session-Local Mappings: + * + *
        + * When a new session is acquired, the mappings present in the persistent + * namespace registry are copied to the local namespace mappings of that + * session. + *
        + */ + public void testInitialLocalNamespaceMappings() throws RepositoryException { + String[] uris = nsr.getURIs(); + for (int i = 0; i < uris.length; i++) { + assertEquals( + "The initial local namespace prefix of \"" + + uris[i] + "\" must match the persistent registry mapping", + nsr.getPrefix(uris[i]), + session.getNamespacePrefix(uris[i])); + + } + } + + /** + * Test case for the scope of the local namespace mappings as specified + * in section 3.5.2 Session-Local Mappings: + * + *
        + * The resulting mapping table applies only within the scope of that session + *
        + * + *

        + * Also specified in the javadoc of + * {@link Session#setNamespacePrefix(String, String)}: + * + *

        + * The remapping only affects operations done through this + * Session. To clear all remappings, the client must + * acquire a new Session. + *
        + */ + public void testScopeOfLocalNamepaceMappings() throws RepositoryException { + String before = session.getNamespacePrefix(NS_JCR_URI); + String after = before + "-changed"; + + // Change the prefix of a well-known namespace + session.setNamespacePrefix(after, NS_JCR_URI); + assertEquals(after, session.getNamespacePrefix(NS_JCR_URI)); + + // Check whether the mapping affects another session + Session another = getHelper().getReadOnlySession(); + try { + assertEquals( + "Local namespace changes must not affect other sessions", + before, another.getNamespacePrefix(NS_JCR_URI)); + } finally { + another.logout(); + } + } + + /** + * Test case for the exception clauses of section 5.11 Namespace Mapping: + * + *
        + * However, the method will throw an exception if + *
          + *
        • the specified prefix begins with the characters "xml" + * (in any combination of case) or,
        • + *
        • the specified prefix is the empty string or,
        • + *
        • the specified namespace URI is the empty string.
        • + *
        + *
        + * + *

        + * Also specified in the javadoc for throwing a {@link NamespaceException} + * from {@link Session#setNamespacePrefix(String, String)}: + * + *

        + * if an attempt is made to map a namespace URI to a prefix beginning + * with the characters "xml" (in any combination of case) + * or if an attempt is made to map either the empty prefix or the empty + * namespace (i.e., if either prefix or uri + * are the empty string). + *
        + * + *

        + * Section 3.2 Names also contains extra constraints on the prefix and + * namespace URI syntax: + * + *

        +     * Namespace   ::= EmptyString | Uri
        +     * EmptyString ::= The empty string
        +     * Uri         ::= A URI, as defined in Section 3 in
        +     *                 http://tools.ietf.org/html/rfc3986#section-3
        +     * Prefix      ::= Any string that matches the NCName production in
        +     *                 http://www.w3.org/TR/REC-xml-names
        +     * 
        + * + *

        + * It is unspecified whether an implementation should actually enforce + * these constraints, so for now this test case does not + * check this behaviour. + */ + public void testExceptionsFromRemapping() throws RepositoryException { + // Remapping to "xml..." in any combination of case must fail + assertSetNamespacePrefixFails("xml", NS_JCR_URI); + assertSetNamespacePrefixFails("xmlfoo", NS_JCR_URI); + assertSetNamespacePrefixFails("XML", NS_JCR_URI); + assertSetNamespacePrefixFails("XMLFOO", NS_JCR_URI); + assertSetNamespacePrefixFails("Xml", NS_JCR_URI); + assertSetNamespacePrefixFails("XmlFoo", NS_JCR_URI); + + // Remapping the empty prefix or empty URI must fail + assertSetNamespacePrefixFails("", NS_JCR_URI); + assertSetNamespacePrefixFails("prefix", ""); + } + + /** + * Checks that a {@link Session#setNamespacePrefix(String, String)} + * call with the given arguments throws a {@link NamespaceException}. + * + * @param prefix namespace prefix + * @param uri namespace URI + * @throws RepositoryException if another error occurs + */ + private void assertSetNamespacePrefixFails(String prefix, String uri) + throws RepositoryException { + try { + session.setNamespacePrefix(prefix, uri); + fail("Setting a local namespace mapping from \"" + + prefix + "\" to \"" + uri + "\" must fail"); + } catch (NamespaceException expected) { + } + } + + /** + * Tests that Session.getNamespaceURI() returns according the session scoped + * mapping + */ + public void testGetNamespaceURI() throws RepositoryException { + String testPrefix = getUnusedPrefix(); + // remap the jcr uri + session.setNamespacePrefix(testPrefix, NS_JCR_URI); + String uri = session.getNamespaceURI(testPrefix); + assertEquals("Session.getNamespaceURI does not return the correct value.", NS_JCR_URI, uri); + } + + /** + * Tests that Session.getNamespacePrefix returns the session scoped + * mapping. + */ + public void testGetNamespacePrefix() throws RepositoryException { + String testPrefix = getUnusedPrefix(); + // remap the jcr uri + session.setNamespacePrefix(testPrefix, NS_JCR_URI); + String prefix = session.getNamespacePrefix(NS_JCR_URI); + assertEquals("Session.getNamespacePrefix does not return the correct value.", testPrefix, prefix); + } + + /** + * Tests if {@link javax.jcr.Session#getNamespacePrefixes()} returns + * all prefixes currently set for this session, including all those + * registered in the NamespaceRegistry but not over-ridden by a + * Session.setNamespacePrefix, plus those currently set locally by + * Session.setNamespacePrefix. + */ + public void testGetNamespacePrefixes() throws RepositoryException { + String testPrefix = getUnusedPrefix(); + + // remap the jcr uri + session.setNamespacePrefix(testPrefix, NS_JCR_URI); + + String prefixes[] = session.getNamespacePrefixes(); + + assertEquals("Session.getNamespacePrefixes() must return all prefixes " + + "currently set for this session.", + nsr.getPrefixes().length, + session.getNamespacePrefixes().length); + + // the prefix of the jcr uri as set in the namespace registry + String prefixNSR = nsr.getPrefix(NS_JCR_URI); + + // test if the "NSR prefix" (and over-ridden by the session) is not + // returned by Session.getNamespacePrefixes() + for (int i = 0; i < prefixes.length; i++) { + if (prefixes[i].equals(prefixNSR)) { + fail("Session.getNamespacePrefixes() must not return the " + + "prefixes over-ridden by Session.setNamespacePrefix"); + } + } + } + + + /** + * Returns a namespace prefix that is not in use. + * + * @return a namespace prefix that is not in use. + */ + private String getUnusedPrefix() throws RepositoryException { + Set prefixes = new HashSet(); + prefixes.addAll(Arrays.asList(session.getNamespacePrefixes())); + String prefix = "myapp"; + int count = 0; + while (prefixes.contains(prefix + count)) { + count++; + } + return prefix + count; + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeAddMixinTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeAddMixinTest.java new file mode 100644 index 00000000000..678cf288a3a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeAddMixinTest.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * NodeAddMixinTest contains the test cases for the method + * Node.AddMixin(String). + * + */ +public class NodeAddMixinTest extends AbstractJCRTest { + + /** + * Tests if Node.addMixin(String mixinName) adds the requested + * mixin and stores it in property jcr:mixinTypes + */ + public void testAddSuccessfully() + throws NotExecutableException, RepositoryException { + + Session session = testRootNode.getSession(); + Node node = testRootNode.addNode(nodeName1, testNodeType); + String mixinName = NodeMixinUtil.getAddableMixinName(session, node); + + if (mixinName == null) { + throw new NotExecutableException("No testable mixin node type found"); + } + + node.addMixin(mixinName); + + // test if mixin is written to property jcr:mixinTypes immediately + Value mixinValues[] = node.getProperty(jcrMixinTypes).getValues(); + boolean found = false; + for (int i = 0; i < mixinValues.length; i++) { + found |= mixinName.equals(mixinValues[i].getString()); + } + if (! found) { + fail("Mixin type must be added to property " + jcrMixinTypes + " immediately."); + } + + // it is implementation-specific if a added mixin is available + // before or after save therefore save before further tests + testRootNode.getSession().save(); + + // test if added mixin is available by node.getMixinNodeTypes() + NodeType mixins[] = node.getMixinNodeTypes(); + found = false; + for (int i = 0; i < mixins.length; i++) { + found |= mixinName.equals(mixins[i].getName()); + } + if (! found) { + fail("Mixin '" + mixinName+ "' type not added."); + } + } + + /** + * Tests if Node.addMixin(String mixinName) throws a + * NoSuchNodeTypeException if mixinName is not the + * name of an existing mixin node type + */ + public void testAddNonExisting() throws RepositoryException { + Session session = testRootNode.getSession(); + String nonExistingMixinName = NodeMixinUtil.getNonExistingMixinName(session); + + Node node = testRootNode.addNode(nodeName1, testNodeType); + + try { + node.addMixin(nonExistingMixinName); + fail("Node.addMixin(String mixinName) must throw a " + + "NoSuchNodeTypeException if mixinName is an unknown mixin type"); + } catch (NoSuchNodeTypeException e) { + // success + } + } + + /** + * Test if adding the same mixin twice works as expected. + * + * @throws RepositoryException + * @throws NotExecutableException + * @since JCR 2.0 + */ + public void testAddMixinTwice() throws RepositoryException, NotExecutableException { + Session session = testRootNode.getSession(); + Node node = testRootNode.addNode(nodeName1, testNodeType); + String mixinName = NodeMixinUtil.getAddableMixinName(session, node); + + if (mixinName == null) { + throw new NotExecutableException("No testable mixin node type found"); + } + + node.addMixin(mixinName); + // adding again must succeed + node.addMixin(mixinName); + + session.save(); + + node.addMixin(mixinName); + assertFalse(node.isModified()); + } + + /** + * Test if adding an inherited mixin type has no effect. + * + * @throws RepositoryException + * @since JCR 2.0 + */ + public void testAddInheritedMixin() throws RepositoryException, NotExecutableException { + Session session = testRootNode.getSession(); + Node node = testRootNode.addNode(nodeName1, testNodeType); + session.save(); + + String inheritedMixin = null; + + NodeType nt = node.getPrimaryNodeType(); + NodeType[] superTypes = nt.getSupertypes(); + for (int i = 0; i < superTypes.length && inheritedMixin == null; i++) { + if (superTypes[i].isMixin()) { + inheritedMixin = superTypes[i].getName(); + } + } + + if (inheritedMixin != null) { + node.addMixin(inheritedMixin); + assertFalse(node.isModified()); + } else { + throw new NotExecutableException("Primary node type does not have a mixin supertype"); + } + } + + /** + * Tests if Node.addMixin(String mixinName) throws a + * LockException if Node is locked + *

        + * The test creates a node nodeName1 of type + * testNodeType under testRoot and locks the node + * with the superuser session. Then the test tries to add a mixin to + * nodeName1 with the readWrite Session. + */ + public void testLocked() + throws NotExecutableException, RepositoryException { + + Session session = testRootNode.getSession(); + + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Locking is not supported."); + } + + // create a node that is lockable + Node node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it lockable if it is not + ensureMixinType(node, mixLockable); + testRootNode.getSession().save(); + + String mixinName = NodeMixinUtil.getAddableMixinName(session, node); + if (mixinName == null) { + throw new NotExecutableException("No testable mixin node type found"); + } + + // remove first slash of path to get rel path to root + String pathRelToRoot = node.getPath().substring(1); + + // access node through another session to lock it + Session session2 = getHelper().getSuperuserSession(); + try { + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.lock(true, true); + + try { + // implementation specific: either throw LockException upon + // addMixin or upon save. + node.addMixin(mixinName); + node.save(); + fail("Node.addMixin(String mixinName) must throw a LockException " + + "if the node is locked."); + } catch (LockException e) { + // success + } + + // unlock to remove node at tearDown() + node2.unlock(); + } finally { + session2.logout(); + } + } + + /** + * Tests if Node.addMixin(String mixinName) throws a + * VersionException if Node is checked-in. + *

        + * The test creates a node nodeName1 of type + * testNodeType under testRoot and checks it in. + * Then the test tries to add a mixin to nodeName1. + */ + public void testCheckedIn() + throws NotExecutableException, RepositoryException { + + Session session = testRootNode.getSession(); + + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { + throw new NotExecutableException("Versioning is not supported."); + } + + // create a node that is versionable + Node node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it versionable if it is not + ensureMixinType(node, mixVersionable); + testRootNode.getSession().save(); + + String mixinName = NodeMixinUtil.getAddableMixinName(session, node); + if (mixinName == null || node.isNodeType(mixinName)) { + throw new NotExecutableException("No testable mixin node type found"); + } + + node.checkin(); + + try { + node.addMixin(mixinName); + fail("Node.addMixin(String mixinName) must throw a VersionException " + + "if the node is checked-in."); + } catch (VersionException e) { + // success + } + } + + /** + * Tests if adding mix:referenceable automatically populates the jcr:uuid + * value. + */ + public void testAddMixinReferencable() + throws NotExecutableException, RepositoryException { + + // check if repository supports references + checkMixReferenceable(); + + // get session an create default node + Node node = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(node, mixReferenceable); + // implementation specific: mixin may take effect only upon save + testRootNode.getSession().save(); + + // check that it did + assertTrue(node.isNodeType(mixReferenceable)); + + // test if jcr:uuid is not null, empty or throws a exception + // (format of value is not defined so we can only test if not empty) + try { + String uuid = node.getProperty(jcrUUID).getValue().getString(); + // default value is null so check for null + assertNotNull("Acessing jcr:uuid after assginment of mix:referencable returned null", uuid); + // check if it was not set to an empty string + assertTrue("Acessing jcr:uuid after assginment of mix:referencable returned an empty String!", uuid.length() > 0); + } catch (ValueFormatException e) { + // trying to access the uuid caused an exception + fail("Acessing jcr:uuid after assginment of mix:referencable caused an ValueFormatException!"); + } + } + + + /** + * Checks if the repository supports the mixin mix:Referenceable otherwise a + * {@link NotExecutableException} is thrown. + * + * @throws NotExecutableException if the repository does not support the + * mixin mix:referenceable. + */ + private void checkMixReferenceable() throws RepositoryException, NotExecutableException { + try { + superuser.getWorkspace().getNodeTypeManager().getNodeType(mixReferenceable); + } catch (NoSuchNodeTypeException e) { + throw new NotExecutableException("Repository does not support mix:referenceable"); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeCanAddMixinTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeCanAddMixinTest.java new file mode 100644 index 00000000000..23555ff70c9 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeCanAddMixinTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; + +/** + * NodeCanAddMixinTest contains the test cases for the method + * Node.canAddMixin(String). + * + */ +public class NodeCanAddMixinTest extends AbstractJCRTest { + + /** + * Tests if Node.canAddMixin(String mixinName) throws a + * LockException if Node is locked + */ + public void testLocked() + throws ConstraintViolationException, NotExecutableException, RepositoryException { + + Session session = testRootNode.getSession(); + + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Locking is not supported."); + } + + // create a node that is lockable + Node node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it lockable if it is not + ensureMixinType(node, mixLockable); + testRootNode.getSession().save(); + + String mixinName = NodeMixinUtil.getAddableMixinName(session, node); + if (mixinName == null) { + throw new NotExecutableException("No testable mixin node type found"); + } + + // remove first slash of path to get rel path to root + String pathRelToRoot = node.getPath().substring(1); + + // access node through another session to lock it + Session session2 = getHelper().getSuperuserSession(); + try { + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.lock(true, true); + + node.refresh(false); + assertFalse("Node.canAddMixin(String mixinName) must return false " + + "if the node is locked.", + node.canAddMixin(mixinName)); + + node2.unlock(); + } finally { + session2.logout(); + } + } + + /** + * Tests if Node.canAddMixin(String mixinName) throws a + * VersionException if Node is checked-in + */ + public void testCheckedIn() + throws ConstraintViolationException, NotExecutableException, RepositoryException { + + Session session = testRootNode.getSession(); + + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { + throw new NotExecutableException("Versioning is not supported."); + } + + // create a node that is versionable + Node node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it versionable if it is not + ensureMixinType(node, mixVersionable); + testRootNode.getSession().save(); + + String mixinName = NodeMixinUtil.getAddableMixinName(session, node); + if (mixinName == null) { + throw new NotExecutableException("No testable mixin node type found"); + } + + node.checkin(); + + assertFalse("Node.canAddMixin(String mixinName) must return false " + + "if the node is checked-in.", + node.canAddMixin(mixinName)); + } + + /** + * Tests if Node.canAddMixin(String mixinName) throws a + * NoSuchNodeTypeException if mixinName is not the + * name of an existing mixin node type + */ + public void testNonExisting() throws RepositoryException { + Session session = testRootNode.getSession(); + String nonExistingMixinName = NodeMixinUtil.getNonExistingMixinName(session); + + Node node = testRootNode.addNode(nodeName1, testNodeType); + + try { + node.canAddMixin(nonExistingMixinName); + fail("Node.canAddMixin(String mixinName) must throw a " + + "NoSuchNodeTypeException if mixinName is an unknown mixin type"); + } catch (NoSuchNodeTypeException e) { + // success + } + } + + /** + * Test if adding the same mixin twice would be allowed. + * + * @throws RepositoryException + * @throws NotExecutableException + * @since JCR 2.0 + */ + public void testAddMixinTwice() throws RepositoryException, NotExecutableException { + Session session = testRootNode.getSession(); + Node node = testRootNode.addNode(nodeName1, testNodeType); + String mixinName = NodeMixinUtil.getAddableMixinName(session, node); + + if (mixinName == null) { + throw new NotExecutableException("No testable mixin node type found"); + } + + assertTrue(node.canAddMixin(mixinName)); + node.addMixin(mixinName); + // adding again must be possible (though it has no effect) + assertTrue(node.canAddMixin(mixinName)); + + session.save(); + + // adding again must be possible (though it has no effect) + assertTrue(node.canAddMixin(mixinName)); + } + + /** + * Test if an inherited mixin could be added. + * + * @throws RepositoryException + * @since JCR 2.0 + */ + public void testAddInheritedMixin() throws RepositoryException { + Session session = testRootNode.getSession(); + Node node = testRootNode.addNode(nodeName1, testNodeType); + session.save(); + + NodeType nt = node.getPrimaryNodeType(); + NodeType[] superTypes = nt.getSupertypes(); + for (int i = 0; i < superTypes.length; i++) { + if (superTypes[i].isMixin()) { + String mixinName = superTypes[i].getName(); + // adding again must be possible (though it has no effect) + assertTrue(node.canAddMixin(mixinName)); + } + } + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/NodeDiscoveringNodeTypesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeDiscoveringNodeTypesTest.java similarity index 80% rename from src/test/org/apache/jackrabbit/test/api/NodeDiscoveringNodeTypesTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeDiscoveringNodeTypesTest.java index ff0c7afa25b..84d6124a90d 100644 --- a/src/test/org/apache/jackrabbit/test/api/NodeDiscoveringNodeTypesTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeDiscoveringNodeTypesTest.java @@ -1,211 +1,216 @@ -/* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.jackrabbit.test.api; - -import org.apache.jackrabbit.test.AbstractJCRTest; -import org.apache.jackrabbit.test.NotExecutableException; - -import javax.jcr.Session; -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Value; -import javax.jcr.RepositoryException; -import javax.jcr.nodetype.NodeType; -import java.util.NoSuchElementException; - -/** - * All test cases in this class rely on content in the repository. That is the - * default workspace must at least contain one child node under {@link #testRoot} - * otherwise a {@link NotExecutableException} is thrown. - * - * @test - * @sources NodeDiscoveringNodeTypesTest.java - * @executeClass org.apache.jackrabbit.test.api.NodeDiscoveringNodeTypesTest - * @keywords level1 - */ -public class NodeDiscoveringNodeTypesTest extends AbstractJCRTest { - - /** - * The session we use for the tests - */ - private Session session; - - /** - * A child node of the root node in the default workspace. - */ - private Node childNode; - - /** - * Sets up the fixture for the test cases. - */ - protected void setUp() throws Exception { - isReadOnly = true; - super.setUp(); - - session = helper.getReadOnlySession(); - testRootNode = session.getRootNode().getNode(testPath); - NodeIterator nodes = testRootNode.getNodes(); - try { - childNode = nodes.nextNode(); - } catch (NoSuchElementException e) { - } - } - - /** - * Releases the session aquired in {@link #setUp()}. - */ - protected void tearDown() throws Exception { - if (session != null) { - session.logout(); - } - super.tearDown(); - } - - /** - * Test if getPrimaryNodeType() returns the node type according to the - * property "jcr:primaryType" - */ - public void testGetPrimaryNodeType() - throws NotExecutableException, RepositoryException { - - if (childNode == null) { - throw new NotExecutableException("Workspace does not have sufficient content for this test. " + - "Root node must have at least one child node."); - } - - NodeType type = childNode.getPrimaryNodeType(); - String name = childNode.getProperty(jcrPrimaryType).getString(); - - assertEquals("getPrimaryNodeType() must return the node type stored " + - "as property \"jcr:primaryType\"", - name, type.getName()); - - assertFalse("getPrimaryNodeType() must return a primary node type", - type.isMixin()); - } - - - /** - * Test if getMixinNodeType returns the node types according to the property - * "jcr:mixinTypes". Therefor a node with mixin types is located recursively - * in the entire repository. A NotExecutableException is thrown when no such - * node is found. - */ - public void testGetMixinNodeTypes() - throws NotExecutableException, RepositoryException { - - if (childNode == null) { - throw new NotExecutableException("Workspace does not have sufficient content for this test. " + - "Root node must have at least one child node."); - } - - Node node = locateNodeWithMixinNodeTypes(testRootNode); - - if (node == null) { - throw new NotExecutableException("Workspace does not contain a node with mixin node types defined"); - } - - Value names[] = node.getProperty(jcrMixinTypes).getValues(); - NodeType types[] = node.getMixinNodeTypes(); - - assertEquals("getMixinNodeTypes() does not return the same number of " + - "node types as " + - "getProperty(\"jcr:mixinTypes\").getValues()", - types.length, - names.length); - - StringBuffer namesString = new StringBuffer(); - for (int i = 0; i < names.length; i++) { - namesString.append("|" + names[i].getString() + "|"); - } - - for (int i = 0; i < types.length; i++) { - String pattern = "|" + types[i].getName() + "|"; - - assertTrue("getMixinNodeTypes() does not return the same node" + - "types as getProperty(\"jcr:mixinTypes\").getValues()", - namesString.indexOf(pattern) != -1); - - assertTrue("All nodes returned by getMixinNodeTypes() must be" + - "mixin", - types[i].isMixin()); - } - } - - - /** - * Test if isNodeTye(String nodeTypeName) returns true if nodeTypeName is the - * name of the primary node type, the name of a mixin node type and the name - * of a supertype. - */ - public void testIsNodeType() - throws NotExecutableException, RepositoryException { - - String nodeTypeName; - - // test with primary node's name - nodeTypeName = testRootNode.getPrimaryNodeType().getName(); - assertTrue("isNodeType(String nodeTypeName) must return true if " + - "nodeTypeName is the name of the primary node type", - testRootNode.isNodeType(nodeTypeName)); - - // test with mixin node's name - // (if such a node is available) - Node nodeWithMixin = locateNodeWithMixinNodeTypes(testRootNode); - if (nodeWithMixin != null) { - NodeType types[] = nodeWithMixin.getMixinNodeTypes(); - nodeTypeName = types[0].getName(); - assertTrue("isNodeType(String nodeTypeName) must return true if " + - "nodeTypeName is the name of one of the " + - "mixin node types", - nodeWithMixin.isNodeType(nodeTypeName)); - } - - // test with the name of predefined supertype "nt:base" - assertTrue("isNodeType(String nodeTypeName) must return true if " + - "nodeTypeName is the name of a node type of a supertype", - testRootNode.isNodeType(ntBase)); - } - - //-----------------------< internal >--------------------------------------- - - /** - * Returns the first descendant of node which defines mixin - * node type(s). - * - * @param node Node to start traversal. - * @return first node with mixin node type(s) - */ - private Node locateNodeWithMixinNodeTypes(Node node) - throws RepositoryException { - - if (node.getMixinNodeTypes().length != 0) { - return node; - } - - NodeIterator nodes = node.getNodes(); - while (nodes.hasNext()) { - Node returnedNode = this.locateNodeWithMixinNodeTypes(nodes.nextNode()); - if (returnedNode != null) { - return returnedNode; - } - } - return null; - } - - -} \ No newline at end of file +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Value; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import java.util.NoSuchElementException; + +/** + * All test cases in this class rely on content in the repository. That is the + * default workspace must at least contain one child node under {@link #testRoot} + * otherwise a {@link NotExecutableException} is thrown. + * + */ +public class NodeDiscoveringNodeTypesTest extends AbstractJCRTest { + + /** + * The session we use for the tests + */ + private Session session; + + /** + * A child node of the root node in the default workspace. + */ + private Node childNode; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + NodeIterator nodes = testRootNode.getNodes(); + try { + childNode = nodes.nextNode(); + } catch (NoSuchElementException e) { + } + } + + /** + * Releases the session acquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + childNode = null; + super.tearDown(); + } + + /** + * Test if getPrimaryNodeType() returns the node type according to the + * property "jcr:primaryType" + */ + public void testGetPrimaryNodeType() + throws NotExecutableException, RepositoryException { + + if (childNode == null) { + throw new NotExecutableException("Workspace does not have sufficient content for this test. " + + "Root node must have at least one child node."); + } + + NodeType type = childNode.getPrimaryNodeType(); + String name = childNode.getProperty(jcrPrimaryType).getString(); + + assertEquals("getPrimaryNodeType() must return the node type stored " + + "as property \"jcr:primaryType\"", + name, type.getName()); + + assertFalse("getPrimaryNodeType() must return a primary node type", + type.isMixin()); + } + + + /** + * Test if getMixinNodeType returns the node types according to the property + * "jcr:mixinTypes". Therefore a node with mixin types is located recursively + * in the entire repository. A NotExecutableException is thrown when no such + * node is found. + */ + public void testGetMixinNodeTypes() + throws NotExecutableException, RepositoryException { + + if (childNode == null) { + throw new NotExecutableException("Workspace does not have sufficient content for this test. " + + "Root node must have at least one child node."); + } + + Node node = locateNodeWithMixinNodeTypes(testRootNode); + + if (node == null) { + throw new NotExecutableException("Workspace does not contain a node with mixin node types defined"); + } + + Value names[] = node.getProperty(jcrMixinTypes).getValues(); + NodeType types[] = node.getMixinNodeTypes(); + + assertEquals("getMixinNodeTypes() does not return the same number of " + + "node types as " + + "getProperty(\"jcr:mixinTypes\").getValues()", + types.length, + names.length); + + StringBuffer namesString = new StringBuffer(); + for (int i = 0; i < names.length; i++) { + namesString.append("|" + names[i].getString() + "|"); + } + + for (int i = 0; i < types.length; i++) { + String pattern = "|" + types[i].getName() + "|"; + + assertTrue("getMixinNodeTypes() does not return the same node" + + "types as getProperty(\"jcr:mixinTypes\").getValues()", + namesString.indexOf(pattern) != -1); + + assertTrue("All nodes returned by getMixinNodeTypes() must be" + + "mixin", + types[i].isMixin()); + } + } + + + /** + * Test if isNodeTye(String nodeTypeName) returns true if nodeTypeName is the + * name of the primary node type, the name of a mixin node type and the name + * of a supertype. + */ + public void testIsNodeType() + throws NotExecutableException, RepositoryException { + + String nodeTypeName; + + // test with primary node's name + nodeTypeName = testRootNode.getPrimaryNodeType().getName(); + assertTrue("isNodeType(String nodeTypeName) must return true if " + + "nodeTypeName is the name of the primary node type", + testRootNode.isNodeType(nodeTypeName)); + + String expNodeTypeName = getQualifiedName(testRootNode.getSession(), nodeTypeName); + assertTrue("isNodeType(String expNodeTypeName) must return true if " + + "expNodeTypeName is the name of the primary node type", testRootNode.isNodeType(expNodeTypeName)); + + // test with mixin node's name + // (if such a node is available) + Node nodeWithMixin = locateNodeWithMixinNodeTypes(testRootNode); + if (nodeWithMixin != null) { + NodeType types[] = nodeWithMixin.getMixinNodeTypes(); + nodeTypeName = types[0].getName(); + expNodeTypeName = getQualifiedName(testRootNode.getSession(), nodeTypeName); + assertTrue("isNodeType(String nodeTypeName) must return true if " + "nodeTypeName is the name of one of the " + + "mixin node types", nodeWithMixin.isNodeType(nodeTypeName)); + assertTrue("isNodeType(String expNodeTypeName) must return true if " + "expNodeTypeName is the name of one of the " + + "mixin node types", nodeWithMixin.isNodeType(expNodeTypeName)); + } + + // test with the name of predefined supertype "nt:base" + assertTrue("isNodeType(String nodeTypeName) must return true if " + + "nodeTypeName is the name of a node type of a supertype", + testRootNode.isNodeType(ntBase)); + assertTrue("isNodeType(String nodeTypeName) must return true if " + + "nodeTypeName is the name of a node type of a supertype", testRootNode.isNodeType(NodeType.NT_BASE)); + } + + //-----------------------< internal >--------------------------------------- + + /** + * Returns the first descendant of node which defines mixin + * node type(s). + * + * @param node Node to start traversal. + * @return first node with mixin node type(s) + */ + private Node locateNodeWithMixinNodeTypes(Node node) + throws RepositoryException { + + if (node.getMixinNodeTypes().length != 0) { + return node; + } + + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node returnedNode = this.locateNodeWithMixinNodeTypes(nodes.nextNode()); + if (returnedNode != null) { + return returnedNode; + } + } + return null; + } + + +} diff --git a/src/test/org/apache/jackrabbit/test/api/NodeItemIsModifiedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeItemIsModifiedTest.java similarity index 87% rename from src/test/org/apache/jackrabbit/test/api/NodeItemIsModifiedTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeItemIsModifiedTest.java index ec98cbd3e61..d9fa2760b27 100644 --- a/src/test/org/apache/jackrabbit/test/api/NodeItemIsModifiedTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeItemIsModifiedTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -24,16 +24,13 @@ /** * Test cases for {@link Item#isModified()} on a node. - *

        - * Configuration requirements:
        + *

        + * Configuration requirements: + *

        * The node at {@link #testRoot} must allow a child node of type * {@link #testNodeType} with name {@link #nodeName1}. The node type must * support a non-mandatory string property with name {@link #propertyName1}. * - * @test - * @sources NodeItemIsModifiedTest.java - * @executeClass org.apache.jackrabbit.test.api.NodeItemIsModifiedTest - * @keywords level2 */ public class NodeItemIsModifiedTest extends AbstractJCRTest { @@ -62,7 +59,7 @@ public void testTransientNewNodeItemIsModified () throws RepositoryException { */ public void testPersistentNewNodeItemIsModified () throws RepositoryException { Node testNode = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); Item testNodeItem = superuser.getItem(testNode.getPath()); // check testNodeItem.isModified() for a new NodeItem after save assertFalse("Item.isModified() must return false after a new NodeItem is added and the parent Node is saved", testNodeItem.isModified()); @@ -77,7 +74,7 @@ public void testPersistentNewNodeItemIsModified () throws RepositoryException { */ public void testTransientNodeItemIsModified () throws RepositoryException { Node testNode = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); // modify the persistent testNode testNode.setProperty(propertyName1, "test"); Item testNodeItem = superuser.getItem(testNode.getPath()); @@ -94,7 +91,7 @@ public void testTransientNodeItemIsModified () throws RepositoryException { */ public void testPersistentNodeItemIsModified () throws RepositoryException { Node testNode = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); // modify the persistent testNode testNode.setProperty(propertyName1, "test"); testNode.save(); @@ -103,4 +100,4 @@ public void testPersistentNodeItemIsModified () throws RepositoryException { assertFalse("Item.isModified() must return false after an existing Property is modified and the current Node is saved", testNodeItem.isModified()); } -} \ No newline at end of file +} diff --git a/src/test/org/apache/jackrabbit/test/api/NodeItemIsNewTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeItemIsNewTest.java similarity index 79% rename from src/test/org/apache/jackrabbit/test/api/NodeItemIsNewTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeItemIsNewTest.java index bafbcc9d68f..354195599dd 100644 --- a/src/test/org/apache/jackrabbit/test/api/NodeItemIsNewTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeItemIsNewTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -24,15 +24,12 @@ /** * Test cases for {@link Item#isNew()} on a node. - *

        - * Configuration requirements:
        + *

        + * Configuration requirements: + *

        * The node at {@link #testRoot} must allow a child node of type * {@link #testNodeType} with name {@link #nodeName1}. * - * @test - * @sources NodeItemIsNewTest.java - * @executeClass org.apache.jackrabbit.test.api.NodeItemIsNewTest - * @keywords level2 */ public class NodeItemIsNewTest extends AbstractJCRTest { @@ -57,10 +54,10 @@ public void testTransientNodeItemIsNew () throws RepositoryException { */ public void testPersistentNodeItemIsNew () throws RepositoryException { Node testNode = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); Item testNodeItem = superuser.getItem(testNode.getPath()); // check testNodeItem is new after save assertFalse("Item.isNew() must return false after a new NodeItem is added and the parent Node is saved", testNodeItem.isNew()); } -} \ No newline at end of file +} diff --git a/src/test/org/apache/jackrabbit/test/api/NodeIteratorTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeIteratorTest.java similarity index 86% rename from src/test/org/apache/jackrabbit/test/api/NodeIteratorTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeIteratorTest.java index 3643f426a6a..a7cef59cfcd 100644 --- a/src/test/org/apache/jackrabbit/test/api/NodeIteratorTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeIteratorTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -26,10 +26,6 @@ /** * Tests the {@link javax.jcr.NodeIterator} implementation. * - * @test - * @sources NodeIteratorTest.java - * @executeClass org.apache.jackrabbit.test.api.NodeIteratorTest - * @keywords level1 */ public class NodeIteratorTest extends AbstractJCRTest { @@ -59,15 +55,15 @@ public void testGetSize() throws RepositoryException, NotExecutableException { } /** - * Tests if {@link javax.jcr.NodeIterator#getPos()} return correct values. + * Tests if {@link javax.jcr.NodeIterator#getPosition()} return correct values. */ public void testGetPos() throws RepositoryException { NodeIterator iter = testRootNode.getNodes(); - assertEquals("Initial call to getPos() must return zero", 0, iter.getPos()); + assertEquals("Initial call to getPos() must return zero", 0, iter.getPosition()); int index = 0; while (iter.hasNext()) { iter.nextNode(); - assertEquals("Wrong position returned by getPos()", ++index, iter.getPos()); + assertEquals("Wrong position returned by getPos()", ++index, iter.getPosition()); } } @@ -121,4 +117,4 @@ public void testSkip() throws RepositoryException { } } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeMixinUtil.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeMixinUtil.java new file mode 100644 index 00000000000..34e3f3a33e8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeMixinUtil.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.test.api; + +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Utility class to locate mixins in the NodeTyeManager. + */ +public class NodeMixinUtil { + + /** + * @return the name of a mixin node type that can be added by the requested + * node + */ + public static String getAddableMixinName(Session session, Node node) + throws RepositoryException { + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator mixins = manager.getMixinNodeTypes(); + + // Skip mix:shareable since not supported by removeMixin + String mixShareable = session.getNamespacePrefix(AbstractJCRTest.NS_MIX_URI) + ":shareable"; + + while (mixins.hasNext()) { + String name = mixins.nextNodeType().getName(); + if (node.canAddMixin(name) + && !node.isNodeType(name) + && !mixShareable.equals(name)) { + return name; + } + } + return null; + } + + public static String getNotAssignedMixinName(Session session, Node node) throws RepositoryException { + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator mixins = manager.getMixinNodeTypes(); + + Set existingMixins = new HashSet(); + for (NodeType nt : node.getMixinNodeTypes()) { + existingMixins.add(nt.getName()); + } + + while (mixins.hasNext()) { + String ntName = mixins.nextNodeType().getName(); + if (!existingMixins.contains(ntName)) { + return ntName; + } + } + return null; + } + + /** + * @return a string that is not the name of a mixin type + */ + public static String getNonExistingMixinName(Session session) + throws RepositoryException { + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator mixins = manager.getMixinNodeTypes(); + StringBuffer s = new StringBuffer("X"); + while (mixins.hasNext()) { + s.append(mixins.nextNodeType().getName()); + } + return s.toString().replaceAll(":", ""); + } + + +} diff --git a/src/test/org/apache/jackrabbit/test/api/NodeOrderableChildNodesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeOrderableChildNodesTest.java similarity index 82% rename from src/test/org/apache/jackrabbit/test/api/NodeOrderableChildNodesTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeOrderableChildNodesTest.java index c6ec80884fe..0f999efe4ee 100644 --- a/src/test/org/apache/jackrabbit/test/api/NodeOrderableChildNodesTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeOrderableChildNodesTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -19,7 +19,6 @@ import org.apache.jackrabbit.test.AbstractJCRTest; import org.apache.jackrabbit.test.NotExecutableException; -import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.nodetype.NodeType; import javax.jcr.Node; import javax.jcr.ItemNotFoundException; @@ -31,10 +30,10 @@ /** * NodeOrderableChildNodesTest contains all node writing tests (LEVEL 2) that require a node * that allows child node ordering (tests therefore are optional). - *

        + *

        * If the repository does not support a node type with orderable child nodes * a {@link NotExecutableException} exception is thrown. - *

        + *

        * Prerequisites: *

          *
        • javax.jcr.tck.NodeOrderableChildNodesTest.nodetype2Name of a @@ -43,10 +42,6 @@ * valid node type that can be added as child node
        • *
        * - * @test - * @sources NodeOrderableChildNodesTest.java - * @executeClass org.apache.jackrabbit.test.api.NodeOrderableChildNodesTest - * @keywords level2 */ public class NodeOrderableChildNodesTest extends AbstractJCRTest { @@ -65,10 +60,18 @@ public class NodeOrderableChildNodesTest extends AbstractJCRTest { */ private Node parentNode; + protected void tearDown() throws Exception { + initialFirstNode = null; + initialSecondNode = null; + parentNode = null; + super.tearDown(); + } + /** * Tries to reorder child nodes using {@link Node#orderBefore(String, String)} - * with an invalid destination reference.

        This should - * throw an {@link ItemNotFoundException}. + * with an invalid destination reference. + *

        + * This should throw an {@link ItemNotFoundException}. */ public void testOrderBeforeInvalidDest() throws RepositoryException, NotExecutableException { @@ -87,8 +90,9 @@ public void testOrderBeforeInvalidDest() /** * Tries to reorder child nodes using {@link Node#orderBefore(String, - * String)} with an invalid source reference.

        This should throw - * an {@link ItemNotFoundException}. + * String)} with an invalid source reference. + *

        + * This should throw an {@link ItemNotFoundException}. */ public void testOrderBeforeInvalidSrc() throws RepositoryException, NotExecutableException { @@ -105,41 +109,32 @@ public void testOrderBeforeInvalidSrc() } } - /** - * Tries to reorder child nodes using {@link Node#orderBefore(String, - * String)} using same source and destination reference.

        This - * should throw an {@link ConstraintViolationException}. - */ - public void testOrderBeforeConstraintViolationException() - throws RepositoryException, NotExecutableException { - checkOrderableNodeType(getProperty("nodetype2")); - prepareTest(); - // ok lets try to reorder - try { - parentNode.orderBefore(initialFirstNode.getName(), initialFirstNode.getName()); - fail("Trying to reorder child nodes using Node.orderBefore() where source and target are same " + - "hould throw ConstraintViolationException!"); - } catch (ConstraintViolationException e) { - // ok - } - } - /** * Tries to reorder on a node using {@link Node#orderBefore(String, String)} - * that does not support child reordering.

        This should throw and + * that does not support child reordering. + *

        + * This should throw and * {@link UnsupportedRepositoryOperationException}. Prequisites:

          - *
        • javax.jcr.tck.NodeOrderableChildNodesTest.testOrderBeforeUnsupportedRepositoryOperationException.nodetype2
        • - * A valid node type that does not suport child node ordering. - *
        • javax.jcr.tck.NodeOrderableChildNodesTest.testOrderBeforeUnsupportedRepositoryOperationException.nodetype3
        • + *
        • javax.jcr.tck.NodeOrderableChildNodesTest.testOrderBeforeUnsupportedRepositoryOperationException.nodetype2: + * A valid node type that does not support child node ordering.
        • + *
        • javax.jcr.tck.NodeOrderableChildNodesTest.testOrderBeforeUnsupportedRepositoryOperationException.nodetype3: * A valid node type that can be added as a child.
        */ public void testOrderBeforeUnsupportedRepositoryOperationException() throws RepositoryException, NotExecutableException { - prepareTest(); + + // create testNode + parentNode = testRootNode.addNode(nodeName1, getProperty("nodetype2")); + // add child node + Node firstNode = parentNode.addNode(nodeName2, getProperty("nodetype3")); + // add a second child node + Node secondNode = parentNode.addNode(nodeName3, getProperty("nodetype3")); + // save the new nodes + superuser.save(); // ok lets try to reorder try { - parentNode.orderBefore(initialSecondNode.getName(), initialFirstNode.getName()); + parentNode.orderBefore(secondNode.getName(), firstNode.getName()); fail("Trying to reorder child nodes using Node.orderBefore() on node that " + "does not support ordering should throw UnsupportedRepositoryException!"); } catch (UnsupportedRepositoryOperationException e) { diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeReadMethodsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeReadMethodsTest.java new file mode 100644 index 00000000000..78e7ed2c6f8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeReadMethodsTest.java @@ -0,0 +1,1248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.PathNotFoundException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Item; +import javax.jcr.ItemVisitor; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.UnsupportedRepositoryOperationException; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Tests the 'read' methods specified in the {@link javax.jcr.Node} interface on + * a level 1 repository. + *

        + * Most tests require at least one child node under the root node, otherwise a + * {@link org.apache.jackrabbit.test.NotExecutableException} is thrown. + * + */ +public class NodeReadMethodsTest extends AbstractJCRTest { + + /** + * Session to access the workspace + */ + private Session session; + + /** + * The first child node of the testRootNode + */ + private Node childNode; + + /** + * Sets up the fixture for this test. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + + testRootNode = session.getRootNode().getNode(testPath); + NodeIterator nodes = testRootNode.getNodes(); + try { + childNode = nodes.nextNode(); + } catch (NoSuchElementException e) { + } + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + childNode = null; + super.tearDown(); + } + + // -----------< tests of methods inherited of Item >------------------------ + + /** + * Tests if getPath() returns the correct path. + */ + public void testGetPath() + throws NotExecutableException, RepositoryException { + + if (childNode == null) { + throw new NotExecutableException("Workspace does not have sufficient content to run this test."); + } + + String path = testRoot + "/" + childNode.getName(); + if (getSize(testRootNode.getNodes(childNode.getName())) > 1) { + // is a same-name-sibling, append index + path += "[" + childNode.getIndex() + "]"; + } + + log.println("path: " + childNode.getPath()); + assertEquals("getPath returns wrong result", + path, + childNode.getPath()); + } + + /** + * Tests if getName() returns same as last name returned by getPath() + */ + public void testGetName() throws RepositoryException, NotExecutableException { + if (childNode == null) { + throw new NotExecutableException("Workspace does not have sufficient content to run this test."); + } + + // build name from path + String path = childNode.getPath(); + String name = path.substring(path.lastIndexOf("/") + 1); + if (name.indexOf("[") != -1) { + name = name.substring(0, name.indexOf("[")); + } + assertEquals("getName() must be the same as the last item in the path", + name, + childNode.getName()); + } + + /** + * Test if the ancestor at depth = n, where n is the depth of this + * Item, returns this Node itself. + */ + public void testGetAncestorOfNodeDepth() throws RepositoryException { + Node nodeAtDepth = (Node) testRootNode.getAncestor(testRootNode.getDepth()); + assertTrue("The ancestor of depth = n, where n is the depth of this " + + "Node must be the item itself.", testRootNode.isSame(nodeAtDepth)); + } + + /** + * Test if getting the ancestor of depth = n, where n is greater than depth + * of this Node, throws an ItemNotFoundException. + */ + public void testGetAncestorOfGreaterDepth() throws RepositoryException { + try { + int greaterDepth = testRootNode.getDepth() + 1; + testRootNode.getAncestor(greaterDepth); + fail("Getting ancestor of depth n, where n is greater than depth of" + + "this Node must throw an ItemNotFoundException"); + } catch (ItemNotFoundException e) { + // success + } + } + + /** + * Test if getting the ancestor of negative depth throws an + * ItemNotFoundException. + */ + public void testGetAncestorOfNegativeDepth() throws RepositoryException { + try { + testRootNode.getAncestor(-1); + fail("Getting ancestor of depth < 0 must throw an ItemNotFoundException."); + } catch (ItemNotFoundException e) { + // success + } + } + + /** + * Tests if getParent() returns parent node + */ + public void testGetParent() + throws NotExecutableException, RepositoryException { + + if (childNode == null) { + throw new NotExecutableException("Workspace does not have sufficient content to run this test."); + } + + assertTrue("getParent() of a child node return the parent node.", + testRootNode.isSame(childNode.getParent())); + } + + /** + * Tests if getParent() of root throws an ItemNotFoundException + */ + public void testGetParentOfRoot() throws RepositoryException { + try { + session.getRootNode().getParent(); + fail("getParent() of root must throw an ItemNotFoundException."); + } catch (ItemNotFoundException e) { + // success + } + } + + /** + * Tests if depth of root is 0 and depth of a sub node of root is 1 + */ + public void testGetDepth() throws RepositoryException { + assertEquals("getDepth() of root must be 0", 0, session.getRootNode().getDepth()); + for (NodeIterator it = session.getRootNode().getNodes(); it.hasNext();) { + assertEquals("getDepth() of child node of root must be 1", 1, + it.nextNode().getDepth()); + } + } + + /** + * Tests if getSession() is same as through which the Item was acquired + */ + public void testGetSession() throws RepositoryException { + assertSame("getSession must return the Session through which " + + "the Node was acquired.", + testRootNode.getSession(), + session); + } + + /** + * Tests if isNode() returns true + */ + public void testIsNode() { + assertTrue("isNode() must return true.", + testRootNode.isNode()); + } + + /** + * Tests if isSame() returns true when retrieving an item through different + * sessions + */ + public void testIsSame() throws RepositoryException { + // access same node through different session + Session s = getHelper().getReadOnlySession(); + try { + Item otherTestNode = s.getRootNode().getNode(testPath); + assertTrue("isSame(Item item) must return true for the same " + + "item retrieved through different sessions.", + testRootNode.isSame(otherTestNode)); + } finally { + s.logout(); + } + } + + /** + * + */ + public void testAccept() throws RepositoryException { + final Node n = testRootNode; + + ItemVisitor itemVisitor = new ItemVisitor() { + public void visit(Property property) { + fail("Wrong accept method executed."); + } + + public void visit(Node node) throws RepositoryException { + assertTrue("Visited node is not the same as the one passed in method visit(Node)", + n.isSame(node)); + } + }; + + n.accept(itemVisitor); + } + + // -----------< tests of Node specific methods >---------------------------- + + /** + * Test if getNode(String relPath) returns the correct node and if a + * PathNotFoundException is thrown when Node at relPath does not exist + */ + public void testGetNode() + throws NotExecutableException, RepositoryException { + + StringBuffer notExistingPath = new StringBuffer("X"); + NodeIterator nodes = testRootNode.getNodes(); + while (nodes.hasNext()) { + // build a path that for sure is not existing + // (":" of namespace prefix will be replaced later on) + notExistingPath.append(nodes.nextNode().getName()); + } + + try { + testRootNode.getNode(notExistingPath.toString().replaceAll(":", "")); + fail("getNode(String relPath) must throw a PathNotFoundException" + + "if no node exists at relPath"); + } catch (PathNotFoundException e) { + // success + } + + try { + NodeIterator nodes2 = testRootNode.getNodes(); + Node node = nodes2.nextNode(); + assertTrue("Node from Iterator is not the same as the Node from getNode()", + testRootNode.getNode(node.getName()).isSame(node)); + } catch (NoSuchElementException e) { + throw new NotExecutableException("Workspace does not have sufficient content for this test. " + + "Root node must have at least one child node."); + } + } + + /** + * Test if all returned items are of type node. + */ + public void testGetNodes() throws RepositoryException { + NodeIterator nodes = testRootNode.getNodes(); + while (nodes.hasNext()) { + Item item = (Item) nodes.next(); + assertTrue("Item is not a node", item.isNode()); + } + } + + /** + * Test getNodes(String namePattern) with all possible patterns. Tested + * node: root - NotExecutableException is thrown when root node has no sub + * nodes. + */ + public void testGetNodesNamePattern() + throws NotExecutableException, RepositoryException { + + // get root node and build an ArrayList of its sub nodes + Node node = testRootNode; + if (!node.hasNodes()) { + throw new NotExecutableException("Workspace does not have sufficient content for this test. " + + "Root node must have at least one child node."); + } + NodeIterator allNodesIt = node.getNodes(); + List allNodes = new ArrayList(); + while (allNodesIt.hasNext()) { + Node n = allNodesIt.nextNode(); + allNodes.add(n); + } + + // test if an empty NodeIterator is returned + // when the pattern is not matching any child node + String pattern0 = ""; + NodeIterator nodes0 = node.getNodes(pattern0); + try { + nodes0.nextNode(); + fail("An empty NodeIterator must be returned if pattern does" + + "not match any child node."); + } catch (NoSuchElementException e) { + // success + } + + // all further tests are using root's first sub node + Node firstNode = allNodes.get(0); + + // test pattern "*" + String pattern1 = "*"; + String assertString1 = "node.getNodes(\"" + pattern1 + "\"): "; + NodeIterator nodes1 = node.getNodes(pattern1); + // test if the number of found nodes is correct + assertEquals(assertString1 + "number of nodes found: ", + allNodes.size(), + getSize(nodes1)); + + // test pattern "nodeName" + String pattern2 = firstNode.getName(); + String assertString2 = "node.getNodes(\"" + pattern2 + "\"): "; + // test if the names of the found nodes are matching the pattern + NodeIterator nodes2 = node.getNodes(pattern2); + while (nodes2.hasNext()) { + Node n = nodes2.nextNode(); + assertEquals(assertString2 + "name comparison failed: ", + firstNode.getName(), + n.getName()); + } + // test if the number of found nodes is correct + int numExpected2 = 0; + for (int i = 0; i < allNodes.size(); i++) { + Node n = allNodes.get(i); + if (n.getName().equals(firstNode.getName())) { + numExpected2++; + } + } + nodes2 = node.getNodes(pattern2); + assertEquals(assertString2 + "number of nodes found: ", + numExpected2, + getSize(nodes2)); + + + // test pattern "nodeName|nodeName" + String pattern3 = firstNode.getName() + "|" + firstNode.getName(); + String assertString3 = "node.getNodes(\"" + pattern3 + "\"): "; + // test if the names of the found nodes are matching the pattern + NodeIterator nodes3 = node.getNodes(pattern3); + while (nodes3.hasNext()) { + Node n = nodes3.nextNode(); + assertEquals(assertString2 + "name comparison failed: ", + firstNode.getName(), + n.getName()); + } + // test if the number of found nodes is correct + int numExpected3 = 0; + for (int i = 0; i < allNodes.size(); i++) { + Node n = allNodes.get(i); + if (n.getName().equals(firstNode.getName())) { + numExpected3++; + } + } + nodes3 = node.getNodes(pattern3); + assertEquals(assertString3 + "number of nodes found: ", + numExpected3, + getSize(nodes3)); + + + // test pattern "*odeNam*" + if (firstNode.getName().length() > 2) { + String name = firstNode.getName(); + String shortenName = name.substring(1, name.length() - 1); + String pattern4 = "*" + shortenName + "*"; + String assertString4 = "node.getNodes(\"" + pattern4 + "\"): "; + // test if the names of the found nodes are matching the pattern + NodeIterator nodes4 = node.getNodes(pattern4); + while (nodes4.hasNext()) { + Node n = nodes4.nextNode(); + assertTrue(assertString4 + "name comparison failed: *" + + shortenName + "* not found in " + n.getName(), + n.getName().indexOf(shortenName) != -1); + } + // test if the number of found nodes is correct + int numExpected4 = 0; + for (int i = 0; i < allNodes.size(); i++) { + Node n = allNodes.get(i); + if (n.getName().indexOf(shortenName) != -1) { + numExpected4++; + } + } + nodes4 = node.getNodes(pattern4); + assertEquals(assertString4 + "number of nodes found: ", + numExpected4, + getSize(nodes4)); + } + } + + /** + * Test getNodes(String[] namePattern) with all possible patterns. Tested + * node: root + * @throws NotExecutableException is thrown when root node has no sub + * nodes. + */ + public void testGetNodesNamePatternArray() + throws NotExecutableException, RepositoryException { + + // get root node and build an ArrayList of its sub nodes + Node node = testRootNode; + if (!node.hasNodes()) { + throw new NotExecutableException("Workspace does not have sufficient content for this test. " + + "Root node must have at least one child node."); + } + NodeIterator allNodesIt = node.getNodes(); + List allNodes = new ArrayList(); + while (allNodesIt.hasNext()) { + Node n = allNodesIt.nextNode(); + allNodes.add(n); + } + + // test if an empty NodeIterator is returned + // when the pattern is not matching any child node + String pattern0 = ""; + NodeIterator nodes0 = node.getNodes(new String[] { pattern0 }); + try { + nodes0.nextNode(); + fail("An empty NodeIterator must be returned if pattern does" + + "not match any child node."); + } catch (NoSuchElementException e) { + // success + } + + // all further tests are using root's first sub node + Node firstNode = allNodes.get(0); + + // test pattern "*" + String pattern1 = "*"; + String assertString1 = "node.getNodes(\"" + pattern1 + "\"): "; + NodeIterator nodes1 = node.getNodes(new String[] { pattern1 }); + // test if the number of found nodes is correct + assertEquals(assertString1 + "number of nodes found: ", + allNodes.size(), + getSize(nodes1)); + + // test pattern "nodeName" + String pattern2 = firstNode.getName(); + String assertString2 = "node.getNodes(\"" + pattern2 + "\"): "; + // test if the names of the found nodes are matching the pattern + NodeIterator nodes2 = node.getNodes(new String[] { pattern2 }); + while (nodes2.hasNext()) { + Node n = nodes2.nextNode(); + assertEquals(assertString2 + "name comparison failed: ", + firstNode.getName(), + n.getName()); + } + // test if the number of found nodes is correct + int numExpected2 = 0; + for (int i = 0; i < allNodes.size(); i++) { + Node n = allNodes.get(i); + if (n.getName().equals(firstNode.getName())) { + numExpected2++; + } + } + assertEquals(assertString2 + "number of nodes found: ", + numExpected2, + getSize(nodes2)); + + // test pattern "nodeName", "nodeName" + String pattern4 = firstNode.getName() + "," + firstNode.getName(); + String assertString4 = "node.getNodes(\"" + pattern4 + "\"): "; + // test if the names of the found nodes are matching the pattern + NodeIterator nodes4 = node.getNodes(new String[] { firstNode.getName(), firstNode.getName() }); + while (nodes4.hasNext()) { + Node n = nodes4.nextNode(); + assertEquals(assertString2 + "name comparison failed: ", + firstNode.getName(), + n.getName()); + } + // test if the number of found nodes is correct + int numExpected4 = 0; + for (int i = 0; i < allNodes.size(); i++) { + Node n = allNodes.get(i); + if (n.getName().equals(firstNode.getName())) { + numExpected4++; + } + } + assertEquals(assertString4 + "number of nodes found: ", + numExpected4, + getSize(nodes4)); + + + // test pattern "*odeNam*" + if (firstNode.getName().length() > 2) { + String name = firstNode.getName(); + String shortenName = name.substring(1, name.length() - 1); + String pattern5 = "*" + shortenName + "*"; + String assertString5 = "node.getNodes(\"" + pattern5 + "\"): "; + // test if the names of the found nodes are matching the pattern + NodeIterator nodes5 = node.getNodes(new String[] { pattern5 }); + while (nodes5.hasNext()) { + Node n = nodes5.nextNode(); + assertTrue(assertString5 + "name comparison failed: *" + + shortenName + "* not found in " + n.getName(), + n.getName().indexOf(shortenName) != -1); + } + // test if the number of found nodes is correct + int numExpected5 = 0; + for (int i = 0; i < allNodes.size(); i++) { + Node n = allNodes.get(i); + if (n.getName().indexOf(shortenName) != -1) { + numExpected5++; + } + } + assertEquals(assertString5 + "number of nodes found: ", + numExpected5, + getSize(nodes5)); + } + } + + /** + * Test if getProperty(String relPath) returns the correct node and if a + * PathNotFoundException is thrown when property at relPath does not exist + */ + public void testGetProperty() + throws NotExecutableException, RepositoryException { + StringBuffer notExistingPath = new StringBuffer("X"); + PropertyIterator properties = testRootNode.getProperties(); + while (properties.hasNext()) { + // build a path that for sure is not existing + // (":" of namespace prefix will be replaced later on) + notExistingPath.append(properties.nextProperty().getName()); + } + + try { + testRootNode.getProperty(notExistingPath.toString().replaceAll(":", "")); + fail("getProperty(String relPath) must throw a " + + "PathNotFoundException if no node exists at relPath"); + } catch (PathNotFoundException e) { + // success + } + + try { + PropertyIterator properties2 = testRootNode.getProperties(); + Property property = properties2.nextProperty(); + assertTrue("Property returned by getProperties() is not the same as returned by getProperty(String).", + testRootNode.getProperty(property.getName()).isSame(property)); + } catch (NoSuchElementException e) { + fail("Root node must always have at least one property: jcr:primaryType"); + } + } + + /** + * Test if all returned items are of type node. + */ + public void testGetProperties() throws RepositoryException { + PropertyIterator properties = testRootNode.getProperties(); + while (properties.hasNext()) { + Item item = (Item) properties.next(); + assertFalse("Item is not a property", item.isNode()); + } + } + + /** + * Test getProperties(String namePattern) with all possible patterns. Tested + * node: root - a NotExecutableException is thrown when root node has no + * properties. + */ + public void testGetPropertiesNamePattern() + throws NotExecutableException, RepositoryException { + + // get root node and build an ArrayList of its sub nodes + Node node = testRootNode; + if (!node.hasProperties()) { + fail("Root node must always have at least one property: jcr:primaryType"); + } + PropertyIterator allPropertiesIt = node.getProperties(); + List allProperties = new ArrayList(); + StringBuffer notExistingPropertyName = new StringBuffer(); + while (allPropertiesIt.hasNext()) { + Property p = allPropertiesIt.nextProperty(); + allProperties.add(p); + notExistingPropertyName.append(p.getName() + "X"); + } + + + // test that an empty NodeIterator is returned + // when the pattern is not matching any child node + String pattern0 = notExistingPropertyName.toString().replaceAll(":", ""); + NodeIterator properties0 = node.getNodes(pattern0); + try { + properties0.nextNode(); + fail("An empty NodeIterator must be returned if pattern does" + + "not match any child node."); + } catch (NoSuchElementException e) { + // success + } + + // all tests are running using root's first property + Property firstProperty = allProperties.get(0); + + // test: getProperties("*") + String pattern1 = "*"; + String assertString1 = "node.getProperties(\"" + pattern1 + "\"): "; + PropertyIterator properties1 = node.getProperties(pattern1); + assertEquals(assertString1 + "number of properties found: ", + allProperties.size(), + getSize(properties1)); + + // test: getProperties("propertyName") + String pattern2 = firstProperty.getName(); + String assertString2 = "node.getProperties(\"" + pattern2 + "\"): "; + // test if the names of the found properties are matching the pattern + PropertyIterator properties2 = node.getProperties(pattern2); + while (properties2.hasNext()) { + Property p = properties2.nextProperty(); + assertEquals(assertString2 + "name comparison failed: ", + firstProperty.getName(), + p.getName()); + } + // test if the number of found properties is correct + int numExpected2 = 0; + for (int i = 0; i < allProperties.size(); i++) { + Property p = allProperties.get(i); + if (p.getName().equals(firstProperty.getName())) { + numExpected2++; + } + } + properties2 = node.getProperties(pattern2); + assertEquals(assertString2 + "number of properties found: ", + numExpected2, + getSize(properties2)); + + + // test: getProperties("propertyName|propertyName") + String pattern3 = firstProperty.getName() + "|" + firstProperty.getName(); + String assertString3 = "node.getProperties(\"" + pattern3 + "\"): "; + // test if the names of the found properties are matching the pattern + PropertyIterator properties3 = node.getProperties(pattern3); + while (properties3.hasNext()) { + Property p = properties3.nextProperty(); + assertEquals(assertString2 + "name comparison failed: ", + firstProperty.getName(), + p.getName()); + } + // test if the number of found properties is correct + int numExpected3 = 0; + for (int i = 0; i < allProperties.size(); i++) { + Property p = allProperties.get(i); + if (p.getName().equals(firstProperty.getName())) { + numExpected3++; + } + } + properties3 = node.getProperties(pattern3); + assertEquals(assertString3 + "number of properties found: ", + numExpected3, + getSize(properties3)); + + + // test: getProperties("*opertyNam*") + if (firstProperty.getName().length() > 2) { + String name = firstProperty.getName(); + String shortenName = name.substring(1, name.length() - 1); + String pattern4 = "*" + shortenName + "*"; + String assertString4 = "node.getProperties(\"" + pattern4 + "\"): "; + // test if the names of the found properties are matching the pattern + PropertyIterator properties4 = node.getProperties(pattern4); + while (properties4.hasNext()) { + Property p = properties4.nextProperty(); + assertTrue(assertString4 + "name comparison failed: *" + + shortenName + "* not found in " + p.getName(), + p.getName().indexOf(shortenName) != -1); + } + // test if the number of found properties is correct + int numExpected4 = 0; + for (int i = 0; i < allProperties.size(); i++) { + Property p = allProperties.get(i); + if (p.getName().indexOf(shortenName) != -1) { + numExpected4++; + } + } + properties4 = node.getProperties(pattern4); + assertEquals(assertString4 + "number of properties found: ", + numExpected4, + getSize(properties4)); + } + } + + /** + * Test getProperties(String[] namePattern) with all possible patterns. + * @throws NotExecutableException is thrown when root node has no properties. + */ + public void testGetPropertiesNamePatternArray() + throws NotExecutableException, RepositoryException { + + // get root node and build an ArrayList of its sub nodes + Node node = testRootNode; + if (!node.hasProperties()) { + fail("Root node must always have at least one property: jcr:primaryType"); + } + PropertyIterator allPropertiesIt = node.getProperties(); + List allProperties = new ArrayList(); + StringBuffer notExistingPropertyName = new StringBuffer(); + while (allPropertiesIt.hasNext()) { + Property p = allPropertiesIt.nextProperty(); + allProperties.add(p); + notExistingPropertyName.append(p.getName() + "X"); + } + + // all tests are running using root's first property + Property firstProperty = allProperties.get(0); + + // test: getProperties("*") + String pattern1 = "*"; + String assertString1 = "node.getProperties(\"" + pattern1 + "\"): "; + PropertyIterator properties1 = node.getProperties(new String[] { pattern1 }); + assertEquals(assertString1 + "number of properties found: ", + allProperties.size(), + getSize(properties1)); + + // test: getProperties("propertyName") + String pattern2 = firstProperty.getName(); + String assertString2 = "node.getProperties(\"" + pattern2 + "\"): "; + // test if the names of the found properties are matching the pattern + PropertyIterator properties2 = node.getProperties(new String[] { pattern2 }); + while (properties2.hasNext()) { + Property p = properties2.nextProperty(); + assertEquals(assertString2 + "name comparison failed: ", + firstProperty.getName(), + p.getName()); + } + // test if the number of found properties is correct + int numExpected2 = 0; + for (int i = 0; i < allProperties.size(); i++) { + Property p = allProperties.get(i); + if (p.getName().equals(firstProperty.getName())) { + numExpected2++; + } + } + assertEquals(assertString2 + "number of properties found: ", + numExpected2, + getSize(properties2)); + + // test: getProperties("propertyName|propertyName") + // test: getProperties("propertyName", "propertyName") + String pattern4 = firstProperty.getName() + "," + firstProperty.getName(); + String assertString4 = "node.getProperties(\"" + pattern4 + "\"): "; + // test if the names of the found properties are matching the pattern + PropertyIterator properties4 = node.getProperties(new String[] { firstProperty.getName(), firstProperty.getName() }); + while (properties4.hasNext()) { + Property p = properties4.nextProperty(); + assertEquals(assertString2 + "name comparison failed: ", + firstProperty.getName(), + p.getName()); + } + // test if the number of found properties is correct + int numExpected4 = 0; + for (int i = 0; i < allProperties.size(); i++) { + Property p = allProperties.get(i); + if (p.getName().equals(firstProperty.getName())) { + numExpected4++; + } + } + assertEquals(assertString4 + "number of properties found: ", + numExpected4, + getSize(properties4)); + + // test: getProperties("*opertyNam*") + if (firstProperty.getName().length() > 2) { + String name = firstProperty.getName(); + String shortenName = name.substring(1, name.length() - 1); + String pattern5 = "*" + shortenName + "*"; + String assertString5 = "node.getProperties(\"" + pattern5 + "\"): "; + // test if the names of the found properties are matching the pattern + PropertyIterator properties5 = node.getProperties(new String[] { pattern5 }); + while (properties5.hasNext()) { + Property p = properties5.nextProperty(); + assertTrue(assertString5 + "name comparison failed: *" + + shortenName + "* not found in " + p.getName(), + p.getName().indexOf(shortenName) != -1); + } + // test if the number of found properties is correct + int numExpected5 = 0; + for (int i = 0; i < allProperties.size(); i++) { + Property p = allProperties.get(i); + if (p.getName().indexOf(shortenName) != -1) { + numExpected5++; + } + } + properties5 = node.getProperties(pattern5); + assertEquals(assertString5 + "number of properties found: ", + numExpected5, + getSize(properties5)); + } + } + + /** + * Test if getPrimaryItem returns the primary item as defined in the primary + * node type. Therefor a node with a primary item is located recursively in + * the entire repository. A NotExecutableException is thrown when no such + * node is found. + */ + public void testGetPrimaryItem() + throws NotExecutableException, RepositoryException { + + Node node = locateNodeWithPrimaryItem(testRootNode); + + if (node == null) { + throw new NotExecutableException("Workspace does not contain a node with primary item defined"); + } + + String primaryItemName = node.getPrimaryNodeType().getPrimaryItemName(); + + Item primaryItem = node.getPrimaryItem(); + if (primaryItem.isNode()) { + assertTrue("Node returned by getPrimaryItem() is not the same as " + + "the one aquired by getNode(String)", + node.getNode(primaryItemName).isSame(primaryItem)); + } else { + assertTrue("Property returned by getPrimaryItem() is not the same as " + + "the one aquired by getProperty(String)", + node.getProperty(primaryItemName).isSame(primaryItem)); + } + } + + /** + * Test if getPrimaryItem does throw an ItemNotFoundException if the primary + * node type does not define a primary item. Therefor a node without a + * primary item is located recursively in the entire repository. A + * NotExecutableException is thrown when no such node is found. + */ + public void testGetPrimaryItemItemNotFoundException() + throws NotExecutableException, RepositoryException { + + Node node = locateNodeWithoutPrimaryItem(testRootNode); + + if (node == null) { + throw new NotExecutableException("Workspace does not contain a node without primary item defined"); + } + + try { + node.getPrimaryItem(); + fail("getPrimaryItem() must throw a ItemNotFoundException " + + "if the primary node type does not define one"); + } catch (ItemNotFoundException e) { + // success + } + } + + /** + * Test if getIndex() returns the correct index. Therefor a node with same + * name sibling is located recursively in the entire repository. If no such + * node is found, the test checks if the testRootNode returns 1 + */ + public void testGetIndex() + throws RepositoryException { + + Node node = locateNodeWithSameNameSiblings(testRootNode); + + if (node == null) { + assertEquals("getIndex() of a node without same name siblings " + + "must return 1", testRootNode.getIndex(), 1); + } else { + NodeIterator nodes = node.getParent().getNodes(node.getName()); + int i = 1; + while (nodes.hasNext()) { + assertEquals("getIndex() must return the correct index", + nodes.nextNode().getIndex(), + i); + i++; + } + } + } + + public void testGetReferences() + throws NotExecutableException, RepositoryException { + + Node node = locateNodeWithReference(testRootNode); + + if (node == null) { + throw new NotExecutableException("Workspace does not contain a node with a reference property set"); + } + + PropertyIterator properties = node.getProperties(); + while (properties.hasNext()) { + Property p = properties.nextProperty(); + if (p.getType() == PropertyType.REFERENCE && !p.getDefinition().isMultiple()) { + Node referencedNode = p.getNode(); + PropertyIterator refs = referencedNode.getReferences(); + boolean referenceFound = false; + while (refs.hasNext()) { + Property ref = refs.nextProperty(); + if (ref.isSame(p)) { + referenceFound = true; + } + } + assertTrue("Correct reference not found", referenceFound); + } + } + } + + /** + * Test if getUUID() returns the string value of the property "jcr:uuid". + * Therefor a node of type "mix:referenceable" is located recursively in the + * entire repository. A NotExecutableException is thrown when no node of + * this type is found. + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testGetUUID() + throws NotExecutableException, RepositoryException { + + // find a node of type mix:referenceable + Node node = locateReferenceableNode(testRootNode); + + if (node == null) { + throw new NotExecutableException("Workspace does not contain a referencable node"); + } + + try { + assertEquals("node.getUUID() does not match " + + "node.getProperty(\"jcr:uuid\").getString()", + node.getProperty(jcrUUID).getString(), node.getUUID()); + } catch (PathNotFoundException e) { + fail("Property UUID expected for " + + "node of type \"" + mixReferenceable + "\""); + } + } + + /** + * Test if getUUID() throws a UnsupportedRepositoryOperationException if + * Node is not referenceable + */ + public void testGetUUIDOfNonReferenceableNode() + throws NotExecutableException, RepositoryException { + + // find a node NOT of type mix:referenceable + Node node = locateNonReferenceableNode(testRootNode); + + if (node == null) { + throw new NotExecutableException("Workspace does not contain a non referenceable node"); + } + + try { + node.getUUID(); + fail("UnsupportedRepositoryOperationException expected"); + } catch (UnsupportedRepositoryOperationException e) { + // success + } + } + + /** + * Test if hasNode(String relPath) returns true if the required node exists + * and false if it doesn't. Tested node: root + */ + public void testHasNode() + throws NotExecutableException, RepositoryException { + + Node node = testRootNode; + + NodeIterator nodes = node.getNodes(); + StringBuffer notExistingNodeName = new StringBuffer(); + while (nodes.hasNext()) { + Node n = nodes.nextNode(); + assertTrue("hasNode(String relPath) returns false although " + + "node at relPath is existing", + node.hasNode(n.getName())); + notExistingNodeName.append(n.getName() + "X"); + } + if (notExistingNodeName.toString().equals("")) { + throw new NotExecutableException("Workspace does not have sufficient content for this test. " + + "Root node must have at least one child node."); + } + + assertFalse("hasNode(String relPath) returns true although " + + "node at relPath is not existing", + node.hasNode(notExistingNodeName.toString().replaceAll(":", ""))); + } + + /** + * Test if hasNodes() returns true if any sub node exists or false if not. + * Tested node: root + */ + public void testHasNodes() throws RepositoryException { + Node node = testRootNode; + NodeIterator nodes = node.getNodes(); + + int i = 0; + while (nodes.hasNext()) { + nodes.nextNode(); + i++; + } + + if (i == 0) { + assertFalse("node.hasNodes() returns true although " + + "no sub nodes existing", + node.hasNodes()); + } else { + assertTrue("node.hasNodes() returns false althuogh " + + "sub nodes are existing", + node.hasNodes()); + } + } + + /** + * Test if hasProperty(String relPath) returns true if a required property + * exists and false if it doesn't. Tested node: root + */ + public void testHasProperty() + throws NotExecutableException, RepositoryException { + + Node node = testRootNode; + + PropertyIterator properties = node.getProperties(); + StringBuffer notExistingPropertyName = new StringBuffer(); + while (properties.hasNext()) { + Property p = properties.nextProperty(); + assertTrue("node.hasProperty(\"relPath\") returns false " + + "although property at relPath is existing", + node.hasProperty(p.getName())); + notExistingPropertyName.append(p.getName() + "X"); + } + if (notExistingPropertyName.toString().equals("")) { + fail("Root node must at least have one property: jcr:primaryType"); + } + + assertFalse("node.hasProperty(\"relPath\") returns true " + + "although property at relPath is not existing", + node.hasProperty(notExistingPropertyName.toString().replaceAll(":", ""))); + } + + /** + * Test if hasProperty() returns true if any property exists or false if + * not. Tested node: root + * + * @throws RepositoryException + */ + public void testHasProperties() throws RepositoryException { + Node node = testRootNode; + PropertyIterator properties = node.getProperties(); + + int i = 0; + while (properties.hasNext()) { + Property p = properties.nextProperty(); + log.println(p.getName()); + i++; + } + + if (i == 0) { + assertFalse("Must return false when no properties exist", + node.hasProperties()); + } else { + assertTrue("Must return true when one or more properties exist", + node.hasProperties()); + } + } + + //-----------------------< internal >--------------------------------------- + + /** + * Returns the first descendant of node which is of type + * mix:referencable. + * + * @param node Node to start traversal. + * @return first node of type mix:referenceable + */ + private Node locateReferenceableNode(Node node) + throws RepositoryException { + + if (node.isNodeType(mixReferenceable)) { + return node; + } + + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node returnedNode = locateReferenceableNode(nodes.nextNode()); + if (returnedNode != null) { + return returnedNode; + } + } + return null; + } + + /** + * Returns the first descendant of node which is not of type + * mix:referenceable. + * + * @param node Node to start traversal. + * @return first node which is not of type mix:referenceable + */ + private Node locateNonReferenceableNode(Node node) + throws RepositoryException { + + if (!node.isNodeType(mixReferenceable)) { + return node; + } + + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node returnedNode = locateNonReferenceableNode(nodes.nextNode()); + if (returnedNode != null) { + return returnedNode; + } + } + return null; + } + + /** + * Returns the first descendant of node which has a property of + * type {@link javax.jcr.PropertyType#REFERENCE} set and is not + * multivalued. + * + * @param node Node to start traversal. + * @return first node with a property of PropertType.REFERENCE + */ + private Node locateNodeWithReference(Node node) + throws RepositoryException { + + PropertyIterator properties = node.getProperties(); + while (properties.hasNext()) { + Property p = properties.nextProperty(); + if (p.getType() == PropertyType.REFERENCE && !p.getDefinition().isMultiple()) { + return node; + } + } + + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node returnedNode = locateNodeWithReference(nodes.nextNode()); + if (returnedNode != null) { + return returnedNode; + } + } + return null; + } + + /** + * Returns the first descendant of node which defines a primary + * item. + * + * @param node Node to start traversal. + * @return first node with a primary item + */ + private Node locateNodeWithPrimaryItem(Node node) + throws RepositoryException { + + if (node.getPrimaryNodeType().getPrimaryItemName() != null) { + return node; + } + + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node returnedNode = locateNodeWithPrimaryItem(nodes.nextNode()); + if (returnedNode != null) { + return returnedNode; + } + } + return null; + } + + /** + * Returns the first descendant of node which does not define a + * primary item. + * + * @param node Node to start traversal. + * @return first node without a primary item + */ + private Node locateNodeWithoutPrimaryItem(Node node) + throws RepositoryException { + + if (node.getPrimaryNodeType().getPrimaryItemName() == null) { + return node; + } + + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node n = locateNodeWithoutPrimaryItem(nodes.nextNode()); + if (n != null) { + return n; + } + } + return null; + } + + /** + * Returns the first descendant of node which has same name + * siblings. + * + * @param node Node to start traversal. + * @return first node with same name siblings + */ + private Node locateNodeWithSameNameSiblings(Node node) + throws RepositoryException { + + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node n = nodes.nextNode(); + NodeIterator nodes2 = node.getNodes(n.getName()); + int i = 0; + while (nodes2.hasNext()) { + nodes2.next(); + i++; + } + if (i > 1) { + // node has same name siblings + return n; + } else { + Node returnedNode = locateNodeWithSameNameSiblings(n); + if (returnedNode != null) { + return returnedNode; + } + } + } + return null; + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/NodeRemoveMixinTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeRemoveMixinTest.java similarity index 76% rename from src/test/org/apache/jackrabbit/test/api/NodeRemoveMixinTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeRemoveMixinTest.java index bfdd69b9747..b75b8a8be4e 100644 --- a/src/test/org/apache/jackrabbit/test/api/NodeRemoveMixinTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeRemoveMixinTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -16,28 +16,24 @@ */ package org.apache.jackrabbit.test.api; -import org.apache.jackrabbit.test.AbstractJCRTest; -import org.apache.jackrabbit.test.NotExecutableException; - -import javax.jcr.version.VersionException; -import javax.jcr.lock.LockException; -import javax.jcr.nodetype.ConstraintViolationException; -import javax.jcr.nodetype.NoSuchNodeTypeException; -import javax.jcr.RepositoryException; -import javax.jcr.Session; import javax.jcr.Node; -import javax.jcr.Property; import javax.jcr.PathNotFoundException; +import javax.jcr.Property; import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; /** * NodeRemoveMixinTest contains the test cases for the method * Node.removeMixin(String). * - * @test - * @sources NodeRemoveMixinTest.java - * @executeClass org.apache.jackrabbit.test.api.NodeRemoveMixinTest - * @keywords level2 */ public class NodeRemoveMixinTest extends AbstractJCRTest { @@ -57,7 +53,7 @@ public void testRemoveSuccessfully() } node.addMixin(mixinName); - testRootNode.save(); + testRootNode.getSession().save(); try { node.removeMixin(mixinName); @@ -86,7 +82,7 @@ public void testRemoveSuccessfully() // it is implementation-specific if a removed mixin isn't available // before or after save therefore save before further tests - testRootNode.save(); + testRootNode.getSession().save(); // test if removed mixin isn't available anymore by node.getMixinNodeTypes() assertTrue("removeMixin(String mixinName) did not remove mixin.", @@ -110,9 +106,10 @@ public void testNotAssigned() } node.addMixin(mixinName); - testRootNode.save(); + testRootNode.getSession().save(); + - String notAssignedMixin = NodeMixinUtil.getAddableMixinName(session, node); + String notAssignedMixin = NodeMixinUtil.getNotAssignedMixinName(session, node); if (notAssignedMixin == null) { throw new NotExecutableException("No testable mixin node type found"); } @@ -130,7 +127,7 @@ public void testNotAssigned() /** * Tests if Node.removeMixin(String mixinName) throws a * LockException if Node is locked. - *

        + *

        * The test creates a node nodeName1 of type * testNodeType under testRoot, adds a mixin and * then locks the node with the superuser session. Then the test tries to @@ -141,22 +138,15 @@ public void testLocked() Session session = testRootNode.getSession(); - if (session.getRepository().getDescriptor(Repository.OPTION_LOCKING_SUPPORTED) == null) { + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { throw new NotExecutableException("Locking is not supported."); } // create a node that is lockable Node node = testRootNode.addNode(nodeName1, testNodeType); // or try to make it lockable if it is not - if (!node.isNodeType(mixLockable)) { - if (node.canAddMixin(mixLockable)) { - node.addMixin(mixLockable); - } else { - throw new NotExecutableException("Node " + nodeName1 + " is not lockable and does not " + - "allow to add mix:lockable"); - } - } - testRootNode.save(); + ensureMixinType(node, mixLockable); + testRootNode.getSession().save(); String mixinName = NodeMixinUtil.getAddableMixinName(session, node); if (mixinName == null) { @@ -164,33 +154,39 @@ public void testLocked() } node.addMixin(mixinName); - testRootNode.save(); + testRootNode.getSession().save(); // remove first slash of path to get rel path to root String pathRelToRoot = node.getPath().substring(1); // access node through another session to lock it - Session session2 = helper.getSuperuserSession(); - Node node2 = session2.getRootNode().getNode(pathRelToRoot); - node2.lock(true, true); - + Session session2 = getHelper().getSuperuserSession(); try { - node.removeMixin(mixinName); - fail("Node.removeMixin(String mixinName) must throw a " + - "LockException if the node is locked."); - } catch (LockException e) { - // success - } + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.lock(true, true); + + try { + // remove mixin on locked node must throw either directly upon + // removeMixin or upon save. + node.removeMixin(mixinName); + node.save(); + fail("Node.removeMixin(String mixinName) must throw a " + + "LockException if the node is locked."); + } catch (LockException e) { + // success + } - // unlock to remove node at tearDown() - node2.unlock(); - session2.logout(); + // unlock to remove node at tearDown() + node2.unlock(); + } finally { + session2.logout(); + } } /** * Tests if Node.removeMixin(String mixinName) throws a * VersionException if Node is checked-in - *

        + *

        * The test creates a node nodeName1 of type * testNodeType under testRoot, adds a mixin and * then checks it in. Then the test tries to remove the added. @@ -200,30 +196,23 @@ public void testCheckedIn() Session session = testRootNode.getSession(); - if (session.getRepository().getDescriptor(Repository.OPTION_LOCKING_SUPPORTED) == null) { + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { throw new NotExecutableException("Versioning is not supported."); } // create a node that is versionable Node node = testRootNode.addNode(nodeName1, testNodeType); // or try to make it versionable if it is not - if (!node.isNodeType(mixVersionable)) { - if (node.canAddMixin(mixVersionable)) { - node.addMixin(mixVersionable); - } else { - throw new NotExecutableException("Node " + nodeName1 + " is not versionable and does not " + - "allow to add mix:versionable"); - } - } - testRootNode.save(); + ensureMixinType(node, mixVersionable); + testRootNode.getSession().save(); String mixinName = NodeMixinUtil.getAddableMixinName(session, node); - if (mixinName == null) { + if (mixinName == null || node.isNodeType(mixinName)) { throw new NotExecutableException("No testable mixin node type found"); } node.addMixin(mixinName); - testRootNode.save(); + testRootNode.getSession().save(); node.checkin(); try { diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeSetPrimaryTypeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeSetPrimaryTypeTest.java new file mode 100644 index 00000000000..f4f706000be --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeSetPrimaryTypeTest.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.test.api; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * SetPrimaryType... + */ +public class NodeSetPrimaryTypeTest extends AbstractJCRTest { + + // TODO: test if node definition is properly reset + // TODO: test if child items are properly reset upon changing definition + // TODO: test if conflicts are properly detected + + /** + * Tests a successful call to Node.setPrimaryType(String) + */ + public void testSetPrimaryType() throws RepositoryException { + Session session = testRootNode.getSession(); + Session otherSession = null; + + String nonExistingMixinName = NodeMixinUtil.getNonExistingMixinName(session); + + Node node = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + // TODO improve. retrieve settable node type name from config. + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator nts = manager.getPrimaryNodeTypes(); + while (nts.hasNext()) { + NodeType nt = nts.nextNodeType(); + String ntName = nt.getName(); + if (!nt.isAbstract() && !ntFrozenNode.equals(ntName)) { + try { + node.setPrimaryType(ntName); + // property value must be adjusted immediately + assertEquals("The value of the jcr:primaryType property must change upon setPrimaryType.", ntName, node.getProperty(jcrPrimaryType).getString()); + + // save changes -> reflected upon Node.getPrimaryNodeType and Property.getValue + superuser.save(); + + assertEquals("Node.getPrimaryNodeType must reflect the changes made.", ntName, node.getPrimaryNodeType().getName()); + assertEquals("The value of the jcr:primaryType property must change upon setPrimaryType.", ntName, node.getProperty(jcrPrimaryType).getString()); + + otherSession = getHelper().getReadOnlySession(); + assertEquals("Node.getPrimaryNodeType must reflect the changes made.", ntName, otherSession.getNode(node.getPath()).getPrimaryNodeType().getName()); + assertEquals("The value of the jcr:primaryType property must change upon setPrimaryType.", ntName, otherSession.getNode(node.getPath()).getProperty(jcrPrimaryType).getString()); + + // was successful + return; + + } catch (ConstraintViolationException e) { + // may happen as long as arbitrary primary types are used for testing -> ignore + } finally { + if (otherSession != null) { + otherSession.logout(); + } + // revert any unsaved changes. + session.refresh(false); + } + } + } + } + + /** + * Passing the current primary type to {@link Node#setPrimaryType(String)} + * must always succeed. + * + * @throws RepositoryException + */ + public void testSetCurrentType() throws RepositoryException { + Session session = testRootNode.getSession(); + + Node node = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + node.setPrimaryType(testNodeType); + superuser.save(); + } + + /** + * Passing the current primary type to {@link Node#setPrimaryType(String)} + * to a new node must always succeed. + * + * @throws RepositoryException + */ + public void testSetCurrentTypeOnNew() throws RepositoryException { + Session session = testRootNode.getSession(); + + Node node = testRootNode.addNode(nodeName1, testNodeType); + node.setPrimaryType(testNodeType); + superuser.save(); + } + + /** + * Tests if Node.setPrimaryType(String) throws a + * NoSuchNodeTypeException if the + * name of an existing node type is passed. + */ + public void testAddNonExisting() throws RepositoryException { + Session session = testRootNode.getSession(); + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + String nonExistingMixinName = "abc"; + while (manager.hasNodeType(nonExistingMixinName)) { + nonExistingMixinName += "_"; + } + Node node = testRootNode.addNode(nodeName1, testNodeType); + + try { + node.setPrimaryType(nonExistingMixinName); + // ev. only detected upon save + superuser.save(); + fail("Node.setPrimaryType(String) must throw a NoSuchNodeTypeException if no nodetype exists with the given name."); + } catch (NoSuchNodeTypeException e) { + // success + } + } + + /** + * Tests if Node.setPrimaryType(String) throws a + * ConstraintViolationException if the + * name of a mixin type is passed + */ + public void testSetMixinAsPrimaryType() throws RepositoryException { + Session session = testRootNode.getSession(); + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator nts = manager.getMixinNodeTypes(); + while (nts.hasNext()) { + try { + Node node = testRootNode.addNode(nodeName1, testNodeType); + node.setPrimaryType(nts.nextNodeType().getName()); + fail("Node.setPrimaryType(String) must throw ConstraintViolationException if the specified node type name refers to a mixin."); + } catch (ConstraintViolationException e) { + // success + } finally { + // reset the changes. + session.refresh(false); + } + } + } + + /** + * Tests if Node.setPrimaryType(String) throws a + * ConstraintViolationException if the + * name of a mixin type is passed + */ + public void testSetAbstractAsPrimaryType() throws RepositoryException { + Session session = testRootNode.getSession(); + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator nts = manager.getPrimaryNodeTypes(); + while (nts.hasNext()) { + NodeType nt = nts.nextNodeType(); + if (nt.isAbstract()) { + try { + Node node = testRootNode.addNode(nodeName1, testNodeType); + node.setPrimaryType(nt.getName()); + fail("Node.setPrimaryType(String) must throw ConstraintViolationException if the specified node type name refers to an abstract node type."); + } catch (ConstraintViolationException e) { + // success + } finally { + // reset the changes. + session.refresh(false); + } + } + } + } + + /** + * Tests if Node.setPrimaryType(String) throws a + * LockException if Node is locked. + */ + public void testLocked() throws NotExecutableException, RepositoryException { + + Session session = testRootNode.getSession(); + + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Locking is not supported."); + } + + // create a node that is lockable + Node node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it lockable if it is not + ensureMixinType(node, mixLockable); + testRootNode.getSession().save(); + + String primaryTypeName = getPrimaryTypeName(session, node); + if (primaryTypeName == null) { + throw new NotExecutableException("No testable node type found"); + } + + // remove first slash of path to get rel path to root + String pathRelToRoot = node.getPath().substring(1); + + // access node through another session to lock it + Session session2 = getHelper().getSuperuserSession(); + try { + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.lock(true, true); + + try { + // implementation specific: either throw LockException upon + // addMixin or upon save. + node.setPrimaryType(primaryTypeName); + node.save(); + fail("Node.setPrimaryType(String) must throw a LockException if the node is locked."); + } catch (LockException e) { + // success + } + + // unlock to remove node at tearDown() + node2.unlock(); + } finally { + session2.logout(); + } + } + + /** + * Tests if Node.setPrimaryType(String) throws a + * VersionException if Node is checked-in. + */ + public void testCheckedIn() throws NotExecutableException, RepositoryException { + + Session session = testRootNode.getSession(); + + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { + throw new NotExecutableException("Versioning is not supported."); + } + + // create a node that is versionable + Node node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it versionable if it is not + ensureMixinType(node, mixVersionable); + superuser.save(); + + String primaryTypeName = getPrimaryTypeName(session, node); + if (primaryTypeName == null) { + throw new NotExecutableException("No testable node type found"); + } + + node.checkin(); + + try { + node.setPrimaryType(primaryTypeName); + fail("Node.setPrimaryType(String) must throw a VersionException if the node is checked-in."); + } catch (VersionException e) { + // success + } + } + + private static String getPrimaryTypeName(Session session, Node node) + throws RepositoryException { + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator nts = manager.getPrimaryNodeTypes(); + + while (nts.hasNext()) { + String name = nts.nextNodeType().getName(); + if (!name.equals(node.getPrimaryNodeType().getName())) { + return name; + } + } + return null; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeTest.java new file mode 100644 index 00000000000..0798001a148 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeTest.java @@ -0,0 +1,1147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.PathNotFoundException; +import javax.jcr.Repository; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.lock.LockException; + +/** + * NodeTest contains all test cases for the + * javax.jcr.Node that are related to writing, modifying or deleting + * nodes (level 2 of the specification). + * + */ +public class NodeTest extends AbstractJCRTest { + + private Session superuserW2; + + /** + * to be able to test the update(String) and getCorrespondingNodePath(String) + * methods we need an additional workspace + */ + public void setUp() throws Exception { + super.setUp(); + + // login to second workspace + superuserW2 = getHelper().getSuperuserSession(workspaceName); + } + + /** + * remove all nodes in second workspace and log out + */ + public void tearDown() throws Exception { + try { + cleanUpTestRoot(superuserW2); + } catch (RepositoryException e) { + log.println("Exception in tearDown: " + e.toString()); + } finally { + // log out + superuserW2.logout(); + superuserW2 = null; + } + + super.tearDown(); + } + + + /** + * Calls {@link javax.jcr.Node#getCorrespondingNodePath(String )} with a non + * existing workspace. + *

        + * This should throw an {@link javax.jcr.NoSuchWorkspaceException }. + */ + public void testGetCorrespondingNodePathNoSuchWorkspaceException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create testNode in default workspace + Node defaultTestNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save changes + superuser.save(); + + try { + defaultTestNode.getCorrespondingNodePath(getNonExistingWorkspaceName(superuser)); + fail("Calling Node.getCorrespondingNodePath(workspace) with invalid workspace should throw NoSuchWorkspaceException"); + } catch (NoSuchWorkspaceException e) { + // ok, works as expected + } + } + + + /** + * Calls {@link javax.jcr.Node#getCorrespondingNodePath(String)} on a node + * that has no corresponding node in second workspace + */ + public void testGetCorrespondingNodePathItemNotFoundException() throws RepositoryException, NotExecutableException { + + // make sure the repository supports multiple workspaces + super.ensureMultipleWorkspacesSupported(); + + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create testNode in default workspace + Node defaultTestNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save changes + superuser.save(); + + try { + // call the update method on test node in default workspace + defaultTestNode.getCorrespondingNodePath(workspaceName); + fail("Calling Node.getCorrespondingNodePath() on node that has no correspondend node should throw ItemNotFoundException"); + } catch (ItemNotFoundException e) { + // ok, works as expected + } + } + + /** + * Creates a node with same path in both workspaces to check if {@link + * javax.jcr.Node#getCorrespondingNodePath(String)} works properly. + */ + public void testGetCorrespondingNodePath() throws RepositoryException, NotExecutableException { + + // make sure the repository supports multiple workspaces + super.ensureMultipleWorkspacesSupported(); + + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create test node in default workspace + Node defaultTestNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save changes + superuser.save(); + + // get the root node in the second workspace + Node rootNodeW2 = (Node) superuserW2.getItem(testRootNode.getPath()); + + // create test node in second workspace + rootNodeW2.addNode(nodeName1, testNodeType); + + // save changes + superuserW2.save(); + + // call the update method on test node in default workspace + defaultTestNode.getCorrespondingNodePath(workspaceName); + + // ok, works as expected + } + + /** + * Tries calling {@link javax.jcr.Node#update(String)} after node has + * changed in first workspace but not been saved yet. + *

        + * This should throw an {@link javax.jcr.InvalidItemStateException}. + *

        + * Prerequisites:

        • javax.jcr.tck.propertyname1 name of + * a String property that can be modified in javax.jcr.tck.nodetype + * for testing
        + */ + public void testUpdateInvalidItemStateException() throws RepositoryException, NotExecutableException { + + // make sure the repository supports multiple workspaces + super.ensureMultipleWorkspacesSupported(); + + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a test node in default workspace + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save changes + superuser.save(); + + // get the root node in the second workspace + Node rootNodeW2 = (Node) superuserW2.getItem(testRootNode.getPath()); + + // create test node in second workspace + rootNodeW2.addNode(nodeName1); + + // save changes + superuserW2.save(); + + // modify the node + testNode.setProperty(propertyName1, "test"); + + try { + // try calling update + testNode.update(workspaceName); + fail("Calling Node.update() on modified node should throw InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // ok, works as expected + } + } + + /** + * Tries to use {@link javax.jcr.Node#update(String)} with an invalid + * workspace. + *

        + * This should throw an {@link javax.jcr.NoSuchWorkspaceException}. + */ + public void testUpdateNoSuchWorkspaceException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a test node in default workspace + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save changes + superuser.save(); + + try { + testNode.update(getNonExistingWorkspaceName(superuser)); + fail("Calling Node.update() on a non existing workspace should throw NoSuchWorkspaceException"); + } catch (NoSuchWorkspaceException e) { + // ok, works as expected + } + } + + /** + * Calls {@link javax.jcr.Node#update(String)} for a node that only exists + * in current workspace.

        In that case nothing should happen. + *

        + * Prerequisites:

        • javax.jcr.tck.propertyname1 + * name of a String property that can be modified in + * javax.jcr.tck.nodetype for testing
        + */ + public void testUpdateNoClone() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a test node in default workspace + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // modify the node + testNode.setProperty(propertyName1, "test"); + + superuser.save(); + + // call the update method on test node in default workspace + testNode.update(workspaceName); + + // check if property is still there + assertTrue("Node got property removed after Node.update() eventhough node has no clone", testNode.hasProperty(propertyName1)); + // check if node did not get children suddenly + assertFalse("Node has children assigned after Node.update() eventhough node has no clone", testNode.hasNodes()); + } + + + /** + * Checks if {@link javax.jcr.Node#update(String)} works properly by + * creating the same node in two workspaces one with a child node the other + * with a property set. + *

        + * Calling update() on the node + * with properties, should remove the properties and add the child node. + *

        + * Prerequisites:

        • javax.jcr.tck.nodetype + * must allow children of same nodetype.
        • javax.jcr.tck.propertyname1 + * name of a String property that can be modified in + * javax.jcr.tck.nodetype for testing
        + */ + public void testUpdate() throws RepositoryException, NotExecutableException { + + // make sure the repository supports multiple workspaces + super.ensureMultipleWorkspacesSupported(); + + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create test node in default workspace + Node defaultTestNode = defaultRootNode.addNode(nodeName1, testNodeType); + + defaultTestNode.setProperty(propertyName1, "test"); + + // save changes + superuser.save(); + + // get the root node in the second workspace + Node rootNodeW2 = (Node) superuserW2.getItem(testRootNode.getPath()); + + // create test node in second workspace + Node testNodeW2 = rootNodeW2.addNode(nodeName1, testNodeType); + + // add a child node + testNodeW2.addNode(nodeName2, testNodeType); + + // save changes + superuserW2.save(); + + // call the update method on test node in default workspace + defaultTestNode.update(workspaceName); + + // ok first check if node has no longer properties + assertFalse("Node updated with Node.update() should have property removed", defaultTestNode.hasProperty(propertyName1)); + // ok check if the child has been added + assertTrue("Node updated with Node.update() should have received childrens", defaultTestNode.hasNode(nodeName2)); + } + + /** + * Tries to add a node using {@link javax.jcr.Node#addNode(String)} where + * node type can not be determined by parent (nt:base is used + * as parent nodetype). + *

        This should throw a {@link javax.jcr.nodetype.ConstraintViolationException}. + */ + public void testAddNodeConstraintViolationExceptionUndefinedNodeType() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + String nodetype = testNodeTypeNoChildren == null ? ntBase : testNodeTypeNoChildren; + Node defaultTestNode = defaultRootNode.addNode(nodeName1, nodetype); + + try { + defaultTestNode.addNode(nodeName2); + fail("Adding a node with node.addNode(node) where nodetype can not be determined from parent should" + + " throw ConstraintViolationException"); + } catch (ConstraintViolationException e) { + // ok, works as expected + } + } + + /** + * Tries to add a node using {@link javax.jcr.Node#addNode(String)} as a + * child of a property. + *

        + * This should throw an {@link javax.jcr.nodetype.ConstraintViolationException}. + *

        + * Prerequisites:

        • javax.jcr.tck.propertyname1 + * name of a String property that can be set in javax.jcr.tck.nodetype + * for testing
        + */ + public void testAddNodeConstraintViolationExceptionProperty() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // add a node + Node defaultTestNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // set a property + defaultTestNode.setProperty(propertyName1, "test"); + + try { + // try to add a node as a child of a property + defaultTestNode.addNode(propertyName1 + "/" + nodeName2); + fail("Adding a node as a child of a property should throw ConstraintViolationException"); + } catch (ConstraintViolationException e) { + // ok, works as expected + } + } + + /** + * Tries to create a node using {@link javax.jcr.Node#addNode(String, + * String)} at a location where there is already a node with same name and + * the parent does not allow same name siblings. + *

        + * This should throw an {@link javax.jcr.ItemExistsException}. + *

        + * Prerequisites: + *

        • javax.jcr.tck.NodeTest.testAddNodeItemExistsException.nodetype + * node type that does not allow same name siblings and allows to add child + * nodes of the same type.
        + */ + public void testAddNodeItemExistsException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // add a node + Node defaultTestNode = defaultRootNode.addNode(nodeName2, testNodeType); + // add a child + defaultTestNode.addNode(nodeName3, testNodeType); + + // save the new node + defaultRootNode.save(); + + try { + // try to add a node with same name again + defaultTestNode.addNode(nodeName3, testNodeType); + defaultRootNode.save(); + fail("Adding a node to a location where same name siblings are not allowed, but a node with same name" + + " already exists should throw ItemExistsException "); + } catch (ItemExistsException e) { + //ok, works as expected + } + } + + /** + * Tries to add a node using {@link javax.jcr.Node#addNode(String)} to a non + * existing destination node. + *

        + * This should throw an {@link javax.jcr.PathNotFoundException}. + */ + public void testAddNodePathNotFoundException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + try { + // use invalid parent path + defaultRootNode.addNode(nodeName1 + "/" + nodeName2); + fail("Creating a node at a non existent destination should throw PathNotFoundException"); + } catch (PathNotFoundException e) { + // ok, works as expected + } + } + + /** + * Adds a new node using {@link javax.jcr.Node#addNode(String)} with an + * index for the new name. + *

        This should throw an {@link RepositoryException}. + */ + public void testAddNodeRepositoryExceptionRelPathIndex() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + try { + // use invalid relPath + defaultRootNode.addNode(nodeName1 + "[1]", testNodeType); + fail("Creating a node with index as postfix for new name should throw RepositoryException"); + } catch (RepositoryException e) { + // ok, works as expected + } + } + + /** + * Creates a new node using {@link Node#addNode(String)}, then tries to call + * {@link javax.jcr.Node#save()} on the newly node. + *

        + * This should throw an {@link RepositoryException}. + */ + public void testAddNodeRepositoryExceptionSaveOnNewNode() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // add a node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + try { + // try to call save on newly created node + testNode.save(); + fail("Calling Node.save() on a newly created node should throw RepositoryException"); + } catch (RepositoryException e) { + // ok, works as expected. + } + } + + /** + * Creates a new node using {@link Node#addNode(String)} , saves using + * {@link javax.jcr.Node#save()} on parent node. Uses a second session to + * verify if the node has been saved. + */ + public void testAddNodeParentSave() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // add a node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save new nodes + defaultRootNode.save(); + + // use a different session to verify if the node is there + Session session = getHelper().getReadOnlySession(); + try { + testNode = (Node) session.getItem(testNode.getPath()); + } finally { + session.logout(); + } + } + + /** + * Creates a new node using {@link Node#addNode(String)} , saves using + * {@link javax.jcr.Session#save()}. Uses a second session to verify if the + * node has been saved. + */ + public void testAddNodeSessionSave() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // add a node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save new nodes + superuser.save(); + + // use a different session to verify if the node is there + Session session = getHelper().getReadOnlySession(); + try { + testNode = (Node) session.getItem(testNode.getPath()); + } finally { + session.logout(); + } + } + + /** + * Creates a node with a mandatory child node using {@link + * Node#addNode(String, String)}, saves on parent node then tries to delete + * the mandatory child node. + *

        + * This should throw a {@link ConstraintViolationException}. + *

        + * Prerequisites:

          + *
        • javax.jcr.tck.NodeTest.testRemoveMandatoryNode.nodetype2 + * a node type that has a mandatory child node
        • javax.jcr.tck.NodeTest.testRemoveMandatoryNode.nodetype3 + * nodetype of the mandatory child node
        • javax.jcr.tck.NodeTest.testRemoveMandatoryNode.nodename3 + * name of the mandatory child node
        + */ + public void testRemoveMandatoryNode() throws RepositoryException { + + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create the node with the mandatory child node definition + Node defaultTestNode = defaultRootNode.addNode(nodeName2, getProperty("nodetype2")); + + // add the mandatory child node + Node defaultTestNodeChild = defaultTestNode.addNode(nodeName3, getProperty("nodetype3")); + + // save changes + defaultRootNode.save(); + + try { + // try to remove the mandatory node + defaultTestNodeChild.remove(); + + defaultTestNode.save(); + fail("Removing a mandatory node should throw a ConstraintViolationException"); + } catch (ConstraintViolationException e) { + // ok, works as expected + } + } + + /** + * Removes a node using {@link javax.jcr.Node#remove()} with session 1, + * afterwards it tries the same with session 2. + *

        + * This should throw an {@link InvalidItemStateException}. + */ + public void testRemoveInvalidItemStateException() throws RepositoryException { + + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create the node + Node defaultTestNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save the nodes + superuser.save(); + + // get the node with session 2 + Session testSession = getHelper().getReadWriteSession(); + try { + Node defaultTestNodeSession2 = (Node) testSession.getItem(defaultTestNode.getPath()); + + // remove node with session 1 + defaultTestNode.remove(); + superuser.save(); + + // try to remove already deleted node with session 2 + try { + defaultTestNodeSession2.remove(); + testSession.save(); + fail("Removing a node already deleted by other session should throw an InvalidItemStateException!"); + } catch (InvalidItemStateException e) { + //ok, works as expected + } + } finally { + testSession.logout(); + } + } + + /** + * Removes a node using {@link javax.jcr.Node#remove()}, then saves with + * parent's nodes {@link javax.jcr.Node#save()} method. + */ + public void testRemoveNodeParentSave() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create the node + Node defaultTestNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save the nodes + defaultRootNode.save(); + + // remove them + defaultTestNode.remove(); + + defaultRootNode.save(); + + // check if the node has been properly removed + try { + defaultRootNode.getNode(nodeName1); + fail("Permanently removed node should no longer be adressable using Parent Node's getNode() method"); + } catch (PathNotFoundException e) { + // ok, works as expected + } + } + + + /** + * Removes a node using {@link javax.jcr.Node#remove()}, then saves using + * {@link javax.jcr.Session#save()} method. + */ + public void testRemoveNodeSessionSave() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create the node + Node defaultTestNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save the nodes + superuser.save(); + + // remove them + defaultTestNode.remove(); + + superuser.save(); + + // check if the node has been properly removed + try { + superuser.getItem(defaultRootNode.getPath() + "/" + nodeName1); + fail("Permanently removed node should no longer be adressable using Session.getItem()"); + } catch (PathNotFoundException e) { + // ok, works as expected + } + } + + /** + * Tests if Node.remove() does not throw a + * LockException if Node is locked. + *

        + * The test creates a node nodeName1 of type + * testNodeType under testRoot and locks the node + * with the superuser session. Then the test removes + * nodeName1. + */ + public void testRemoveNodeLockedItself() + throws LockException, NotExecutableException, RepositoryException { + + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Locking is not supported."); + } + + // create a node that is lockable + Node node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it lockable if it is not + ensureMixinType(node, mixLockable); + testRootNode.getSession().save(); + + // remove first slash of path to get rel path to root + String pathRelToRoot = node.getPath().substring(1); + + // access node through another session to lock it + Session session2 = getHelper().getSuperuserSession(); + try { + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.lock(true, true); + + // test fails if a LockException is thrown when removing the node + // (remove must be possible since the parent is not locked) + node.remove(); + } finally { + session2.logout(); + } + } + + /** + * Tests if Node.remove() throws a LockException + * if the parent node of Node is locked. + *

        + * The test creates a node nodeName1 of type + * testNodeType under testRoot, adds a child node + * nodeName2 and locks it with the superuser session. Then the + * test tries to remove the nodeName2. + */ + public void testRemoveNodeParentLocked() + throws LockException, NotExecutableException, RepositoryException { + + Session session = testRootNode.getSession(); + + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Locking is not supported."); + } + + // create a node that is lockable + Node node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it lockable if it is not + ensureMixinType(node, mixLockable); + // create a child node + Node subNode = node.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + + // lock the node + // remove first slash of path to get rel path to root + String pathRelToRoot = node.getPath().substring(1); + // access node through another session to lock it + Session session2 = getHelper().getSuperuserSession(); + try { + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.lock(true, true); + + try { + subNode.remove(); + session.save(); + fail("Removal of a Node must throw a LockException upon remove() " + + "or upon save() if the parent of the node is locked"); + } catch (LockException e) { + // success + } + + // unlock to remove node at tearDown() + node2.unlock(); + } finally { + session2.logout(); + } + } + + /** + * Tests object identity, meaning two nodes objects acquired through the + * same session must have the same properties and states. + *

        + * Prerequisites:

        • javax.jcr.tck.nodetype must allow + * children of same node type
        • javax.jcr.tck.propertyname1 + * name of a String property that can be set in javax.jcr.tck.nodetype + * for testing
        + */ + public void testNodeIdentity() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode1 = defaultRootNode.addNode(nodeName1, testNodeType); + // add a child node + testNode1.addNode(nodeName1, testNodeType); + // add a property + testNode1.setProperty(propertyName1, "test"); + + // save the new node + defaultRootNode.save(); + + // acquire the same node with session 2 + Node testNode2 = (Node) superuser.getItem(testNode1.getPath()); + + // check if they have the same property + assertTrue("Two references of same node have different properties", testNode1.getProperty(propertyName1).isSame(testNode2.getProperty(propertyName1))); + // check if they have the same child + assertTrue("Two references of same node have different children", testNode1.getNode(nodeName1).isSame(testNode2.getNode(nodeName1))); + // check state methods + assertEquals("Two references of same node have different State for Node.isCheckedOut()", testNode1.isCheckedOut(), testNode2.isCheckedOut()); + assertEquals("Two references of same node have different State for Node.isLocked()", testNode1.isLocked(), testNode2.isLocked()); + assertEquals("Two references of same node have different State for Node.isModified()", testNode1.isModified(), testNode2.isModified()); + assertEquals("Two references of same node have different State for Node.isNew()", testNode1.isNew(), testNode2.isNew()); + assertEquals("Two references of same node have different State for Node.isNode()", testNode1.isNode(), testNode2.isNode()); + assertEquals("Two references of same node have different State for Node.isNodeType()", testNode1.isNodeType(testNodeType), testNode2.isNodeType(testNodeType)); + assertTrue("Two references of same node should return true for Node1.isSame(Node2)", testNode1.isSame(testNode2)); + assertEquals("Two references of same node have different Definitions", testNode1.getDefinition().getName(), testNode2.getDefinition().getName()); + } + + /** + * Tests if Item.isSame(Item otherItem) will return true when + * two Node objects representing the same actual repository + * item have been retrieved through two different sessions and one has been + * modified. + */ + public void testIsSameMustNotCompareStates() + throws RepositoryException { + + // create a node and save it + Node testNode1 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + + // acquire the same node with a different session + Session session = getHelper().getReadOnlySession(); + try { + Node testNode2 = (Node) session.getItem(testNode1.getPath()); + + // add a property and do not save it so property is different in testNode2 + testNode1.setProperty(propertyName1, "value1"); + + assertTrue("Two references of same node should return true for Node1.isSame(Node2)", + testNode1.isSame(testNode2)); + } finally { + session.logout(); + } + } + + /** + * Checks if {@link Node#isModified()} works correctly for unmodified and + * modified nodes. + */ + public void testIsModified() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + defaultRootNode.save(); + + assertFalse("Unmodified node should return false on Node.isModified()", testNode.isModified()); + + // check if modified properties are recognized + testNode.setProperty(propertyName1, "test"); + + assertTrue("Modified node should return true on Node.isModified()", testNode.isModified()); + + defaultRootNode.save(); + + // check if modified child nodes are recognized + testNode.addNode(nodeName2, testNodeType); + + assertTrue("Modified node should return true on Node.isModified()", testNode.isModified()); + + } + + /** + * Checks if {@link Node#isNew()} works correctly for new and existing, + * unmodified nodes. + */ + public void testIsNew() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + assertTrue("Newly created node should return true on newNode.isNew()", testNode.isNew()); + + defaultRootNode.save(); + + assertFalse("Unmodified, exisiting node should return false on newNode.isNew()", testNode.isNew()); + + } + + /** + * Tries to call {@link Node#refresh(boolean)} on a deleted node. + *

        + * This should throw an {@link InvalidItemStateException}. + */ + public void testRefreshInvalidItemStateException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save the new node + defaultRootNode.save(); + + // remove the node + defaultRootNode.remove(); + + try { + testNode.refresh(true); + fail("Calling Node.refresh() on deleted node should throw InvalidItemStateException!"); + } catch (InvalidItemStateException e) { + // ok, works as expected + } + } + + /** + * Checks if {@link javax.jcr.Node#refresh(boolean refresh)} works properly + * with refresh set to false. + *

        + * Procedure:

        • Creates two nodes with session 1
        • Modifies + * node 1 with session 1 by adding a child node
        • Get node 2 with + * session 2
        • Modifies node 2 with session 2 by adding a child + * node
        • saves session 2 changes using {@link + * javax.jcr.Node#save()}
        • calls Node.refresh(false) + * on root node in session 1
        Session 1 changes should be cleared + * and session 2 changes should now be visible to session 1. + *

        + * Prerequisites:

        • javax.jcr.tck.nodetype + * must accept children of same nodetype
        + */ + public void testRefreshBooleanFalse() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode1Session1 = defaultRootNode.addNode(nodeName1, testNodeType); + // create a second node + Node testNode2Session1 = defaultRootNode.addNode(nodeName2, testNodeType); + + // save the new nodes + defaultRootNode.save(); + + // add child node to test node 1 using session 1 + testNode1Session1.addNode(nodeName2, testNodeType); + + // get session 2 + Session session2 = getHelper().getReadWriteSession(); + + try { + // get the second node + Node testNode2Session2 = (Node) session2.getItem(testNode2Session1.getPath()); + + // adds a child node + testNode2Session2.addNode(nodeName3, testNodeType); + + // save the changes + session2.save(); + // call refresh on session 1 + defaultRootNode.refresh(false); + + // check if session 1 flag has been cleared + assertFalse("Session should have no pending changes recorded after Node.refresh(false)!", superuser.hasPendingChanges()); + + // check if added child node for node 1 by session 1 has been removed + assertFalse("Node Modifications have not been flushed after Node.refresh(false)", testNode1Session1.hasNodes()); + + // check if added child node for node 2 by session 2 has become visible in session 1 + assertTrue("Node modified by a different session has not been updated after Node.refresh(false)", testNode2Session1.hasNodes()); + } finally { + session2.logout(); + } + } + + /** + * Checks if {@link javax.jcr.Node#refresh(boolean refresh)} works properly + * with refresh set to true. + *

        + * Procedure:

        • Creates two nodes with session 1
        • Modifies + * node 1 with session 1 by adding a child node
        • Get node 2 with + * session 2
        • Modifies node 2 with session 2 by adding a child + * node
        • saves session 2 changes using {@link + * javax.jcr.Node#save()}
        • calls Node.refresh(true) on + * root node in session 1
        Session 1 changes and session 2 + * changes now be visible to session 1. + *

        + * Prerequisites:

          + *
        • javax.jcr.tck.nodetype must accept children of same + * nodetype
        + */ + public void testRefreshBooleanTrue() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode1Session1 = defaultRootNode.addNode(nodeName1, testNodeType); + // create a second node + Node testNode2Session1 = defaultRootNode.addNode(nodeName2, testNodeType); + + // save the new nodes + defaultRootNode.save(); + + // add child node to test node 1 using session 1 + testNode1Session1.addNode(nodeName2, testNodeType); + + // get session 2 + Session session2 = getHelper().getReadWriteSession(); + + try { + // get the second node + Node testNode2Session2 = (Node) session2.getItem(testNode2Session1.getPath()); + + // adds a child node + testNode2Session2.addNode(nodeName3, testNodeType); + + // save the changes + session2.save(); + + // call refresh on session 1 + defaultRootNode.refresh(true); + + // check if session 1 flag has been cleared + assertTrue("Session should still have pending changes recorded after Node.refresh(true)!", superuser.hasPendingChanges()); + + // check if added child node for node 1 by session 1 is still there + assertTrue("Node Modifications are lost after Node.refresh(true)", testNode1Session1.hasNodes()); + + // check if added child node for node 2 by session 2 has become visible in session 1 + assertTrue("Node modified by a different session has not been updated after Node.refresh(true)", testNode2Session1.hasNodes()); + } finally { + session2.logout(); + } + } + + /** + * Tries to save a node using {@link javax.jcr.Node#save()} that was already + * deleted by an other session. + *

        + * Procedure:

        • Creates a new + * node with session 1, saves it, adds a child node.
        • Access new + * node with session 2,deletes the node, saves it.
        • Session 1 tries + * to save modifications using Node.save() on root node .
        • + *
        This should throw an {@link javax.jcr.InvalidItemStateException}. + *

        + * Prerequisites:

        • javax.jcr.tck.nodetype + * must accept children of same nodetype
        + */ + public void testSaveInvalidStateException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node nodeSession1 = defaultRootNode.addNode(nodeName1, testNodeType); + + // save new node + superuser.save(); + + // make a modification + nodeSession1.addNode(nodeName2, testNodeType); + + // get the new node with a different session + Session testSession = getHelper().getReadWriteSession(); + try { + Node nodeSession2 = (Node) testSession.getItem(nodeSession1.getPath()); + + // delete the node with the new session + nodeSession2.remove(); + + // make node removal persistent + testSession.save(); + + // save changes made wit superuser session + try { + defaultRootNode.save(); + fail("Saving a modified Node using Node.save() already deleted by an other session should throw InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // ok, works as expected + } + } finally { + testSession.logout(); + } + } + + /** + * Tries to create and save a node using {@link javax.jcr.Node#save()} with + * an mandatory property that is not set on saving time. + *

        + * Prerequisites:

        • javax.jcr.tck.Node.testSaveConstraintViolationException.nodetype2 + * must reference a nodetype that has at least one property that is + * mandatory but not autocreated
        + */ + public void testSaveConstraintViolationException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node with at least one mandatory, not autocreated property + defaultRootNode.addNode(nodeName1, this.getProperty("nodetype2")); + + // save changes + try { + superuser.save(); + fail("Trying to use parent Node.save() with a node that has a mandatory property not set, should throw ConstraintViolationException"); + } catch (ConstraintViolationException e) { + // ok + } + } + + /** + * Creates a new node, saves it uses second session to verify if node has + * been added. + */ + public void testNodeSave() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save new node + defaultRootNode.save(); + + // get the new node with a different session + Session testSession = getHelper().getReadOnlySession(); + try { + testSession.getItem(testNode.getPath()); + } finally { + testSession.logout(); + } + } + + /** + * Tests if a {@link javax.jcr.RepositoryException} is thrown when calling + * Node.save() on a newly added node + */ + public void testSaveOnNewNodeRepositoryException() throws Exception { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node newNode = defaultRootNode.addNode(nodeName1, testNodeType); + + try { + newNode.save(); + fail("Calling Node.save() on a newly added node should throw a RepositoryException"); + } catch (RepositoryException success) { + // ok + } + } + + /** + * Tests if the primary node type is properly stored in jcr:primaryType + */ + public void testPrimaryType() throws Exception { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + assertEquals("The primary node type is not properly stored in jcr:primaryType",testNodeType,testNode.getProperty(jcrPrimaryType).getString()); + } + + /** + * Tests if jcr:primaryType is protected + */ + public void testPrimaryTypeProtected() throws Exception { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + try { + testNode.setProperty(jcrPrimaryType,ntBase); + fail("Manually setting jcr:primaryType should throw a ConstraintViolationException"); + } + catch (ConstraintViolationException success) { + // ok + } + } + + /** + * Tests if jcr:mixinTypes is protected + */ + public void testMixinTypesProtected() throws Exception { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + Node testNode = defaultRootNode.addNode(nodeName1, testNodeType); + + Value mixinName = superuser.getValueFactory().createValue(mixLockable, PropertyType.NAME); + try { + testNode.setProperty(jcrMixinTypes, new Value[]{mixinName}); + fail("Manually setting jcr:mixinTypes should throw a ConstraintViolationException"); + } + catch (ConstraintViolationException success) { + // ok + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeUUIDTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeUUIDTest.java new file mode 100644 index 00000000000..1b99266ed56 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/NodeUUIDTest.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +/** + * NodeUUIDTest contains all tests for the + * javax.jcr.Node class that require a UUID (and therefore are + * optional). If the repository does not support the node type mix:referenceable + * a {@link NotExecutableException} is thrown. + * + */ +public class NodeUUIDTest extends AbstractJCRTest { + + /** + * Tries to remove a node that is a reference target using {@link + * Node#save()}.

        Procedure:
        • Creates two nodes with same + * session
        • One has a referencing property pointing to the other + * node
        • Target node gets removed.
        This should + * generate a {@link javax.jcr.ReferentialIntegrityException} upon save. + *

        Prerequisites:
        • javax.jcr.tck.NodeUUIDTest.nodetype + * must allow a property of type {@link javax.jcr.PropertyType#REFERENCE}
        • + *
        • javax.jcr.tck.NodeUUIDTest.propertyname1 name of the + * property of type {@link javax.jcr.PropertyType#REFERENCE}
        • + *
        • javax.jcr.tck.NodeUUIDTest.nodetype2 must have the mixin + * type mix:referenceable assigned.
        + */ + public void testSaveReferentialIntegrityException() throws RepositoryException, NotExecutableException { + checkMixReferenceable(); + + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node with a property of type PropertyType.REFERENCE + Node referencingNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // create a node with a jcr:uuid property to serve as target + Node refTargetNode = defaultRootNode.addNode(nodeName2, getProperty("nodetype2")); + // make sure, mix:referenceable is effective. some impls may require a save() call. + defaultRootNode.save(); + + + // abort test if the repository does not allow setting + // reference properties on this node + ensureCanSetProperty(referencingNode, propertyName1, referencingNode.getSession().getValueFactory().createValue(refTargetNode)); + + // set the reference + referencingNode.setProperty(propertyName1, refTargetNode); + + // save the new nodes + defaultRootNode.save(); + + // remove the referenced node + refTargetNode.remove(); + + // try to save + try { + defaultRootNode.save(); + fail("Saving a deleted node using Node.save() that is a reference target should throw ReferentialIntegrityException"); + } catch (ReferentialIntegrityException e) { + // ok, works as expected + } + } + + /** + * Moves a referencable node using {@link javax.jcr.Session#move(String, + * String)} with one session and saves afterward changes made with a second + * session to the moved node using {@link Node#save()}. + *

        + * Procedure:

        • Creates node 1 and node 2 with session 1
        • + *
        • Gets reference to node 1 using session 2
        • Session 1 moves + * node 1 under node 2, saves changes
        • Session 2 modifes node 1, + * saves
        This should work (since the modified node is identified + * by its UUID, not by position in repository) or throw an + * InvalidItemStateException if 'move' is reported to the second + * session as a sequence of remove and add events.

        Prerequisites:
          + *
        • javax.jcr.tck.NodeUUIDTest.nodetype2 must have the mixin + * type mix:referenceable assigned.
        • + *
        • javax.jcr.tck.NodeUUIDTest.testSaveMovedRefNode.propertyname1 + * name of a property that can be modified in nodetype2 for + * testing
        + */ + public void testSaveMovedRefNode() throws RepositoryException, NotExecutableException { + checkMixReferenceable(); + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node newParentNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // create a referenceable node + Node refTargetNode = defaultRootNode.addNode(nodeName2, getProperty("nodetype2")); + + // save the new nodes + superuser.save(); + + // get the moving node with session 2 + Session testSession = getHelper().getReadWriteSession(); + try { + Node refTargetNodeSession2 = (Node) testSession.getItem(refTargetNode.getPath()); + + //move the node with session 1 + superuser.move(refTargetNode.getPath(), newParentNode.getPath() + "/" + nodeName3); + + // make the move persistent with session 1 + superuser.save(); + + // modify some prop of the moved node with session 2 + try { + refTargetNodeSession2.setProperty(propertyName1, "test"); + + // save it + refTargetNodeSession2.save(); + // ok, works as expected + } catch (InvalidItemStateException e) { + // ok as well. + } + } finally { + testSession.logout(); + } + } + + /** + * Checks if the repository supports the mixin mix:Referenceable otherwise a + * {@link NotExecutableException} is thrown. + * + * @throws NotExecutableException if the repository does not support the + * mixin mix:referenceable. + */ + private void checkMixReferenceable() throws RepositoryException, NotExecutableException { + try { + superuser.getWorkspace().getNodeTypeManager().getNodeType(mixReferenceable); + } catch (NoSuchNodeTypeException e) { + throw new NotExecutableException("Repository does not support mix:referenceable"); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PathPropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PathPropertyTest.java new file mode 100644 index 00000000000..18802c841ea --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PathPropertyTest.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.test.api; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.PropertyType; +import javax.jcr.Node; +import javax.jcr.Property; + +/** + * Tests a path property. If the workspace does not contain a node with a path + * property a {@link org.apache.jackrabbit.test.NotExecutableException} is + * thrown. + * + */ +public class PathPropertyTest extends AbstractPropertyTest { + + /** + * Returns {@link javax.jcr.PropertyType#PATH}. + * + * @return {@link javax.jcr.PropertyType#PATH}. + */ + protected int getPropertyType() { + return PropertyType.PATH; + } + + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + + /** + * Tests conversion from Path type to String type and if the resulting + * string has correct format. + */ + public void testGetString() throws RepositoryException { + Value val = PropertyUtil.getValue(prop); + assertTrue("Not a valid Path property: " + prop.getName(), + PropertyUtil.checkPathFormat(val.getString(), session)); + } + + /** + * Tests failure of conversion from Path type to Boolean type. + */ + public void testGetBoolean() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getBoolean(); + fail("Conversion from a Path value to a Boolean value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests failure of conversion from Path type to Date type. + */ + public void testGetDate() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getDate(); + fail("Conversion from a Path value to a Date value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests failure from Path type to Double type. + */ + public void testGetDouble() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getDouble(); + fail("Conversion from a Path value to a Double value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests failure of conversion from Path type to Long type. + */ + public void testGetLong() throws RepositoryException { + try { + Value val = PropertyUtil.getValue(prop); + val.getLong(); + fail("Conversion from a Path value to a Long value " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + + /** + * Tests if Value.getType() returns the same as Property.getType() and also + * tests that prop.getDefinition().getRequiredType() returns the same type + * in case it is not of Undefined type. + */ + public void testGetType() throws RepositoryException { + assertTrue("Value.getType() returns wrong type.", + PropertyUtil.checkGetType(prop, PropertyType.PATH)); + } + + /** + * Since JCR 2.0 a path property can be dereferenced if it points to a + * Node. + * TODO: create several tests out of this one + */ + public void testGetNode() throws RepositoryException { + if (!multiple) { + String nodePath = prop.getParent().getPath(); + String propName = prop.getName(); + + // absolute nodes path + prop.getParent().setProperty(propName, nodePath, PropertyType.PATH); + String value = prop.getString(); + Node n = prop.getNode(); + assertEquals("The path of the dereferenced property must be equal to the value", n.getPath(), value); + assertTrue("The property value must be resolved to the correct node.", prop.getParent().isSame(n)); + + // relative node path + prop.getParent().setProperty(propName, ".", PropertyType.PATH); + n = prop.getNode(); + assertTrue("The property value must be resolved to the correct node.", prop.getParent().getNode(".").isSame(n)); + + // non-existing property path + while (session.nodeExists(nodePath)) { + nodePath += "x"; + } + prop.getParent().setProperty(propName, nodePath, PropertyType.PATH); + try { + prop.getNode(); + fail("Calling Property.getNode() for a PATH value that doesn't have a corresponding Node, ItemNotFoundException is expected"); + } catch (ItemNotFoundException e) { + //ok + } + } else { + try { + prop.getNode(); + fail("Property.getNode() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + } + } + + /** + * Since JCR 2.0 a path property can be dereferenced if it points to a + * Property. + * TODO: create several tests out of this one + */ + public void testGetProperty() throws RepositoryException { + if (!multiple) { + String propPath = prop.getPath(); + String propName = prop.getName(); + + // absolute property path + prop.getParent().setProperty(propName, propPath, PropertyType.PATH); + String path = prop.getString(); + Property p = prop.getProperty(); + assertEquals("The path of the dereferenced property must be equal to the value", path, p.getPath()); + assertTrue("The property value must be resolved to the correct property.", prop.isSame(p)); + + // relative property path + prop.getParent().setProperty(propName, propName, PropertyType.PATH); + path = prop.getString(); + p = prop.getProperty(); + assertEquals("The path of the dereferenced property must be equal to the value", path, p.getName()); + assertTrue("The property value must be resolved to the correct property.", prop.getParent().getProperty(path).isSame(p)); + + // non-existing property path + while (session.propertyExists(propPath)) { + propPath += "x"; + } + prop.getParent().setProperty(propName, propPath, PropertyType.PATH); + try { + prop.getProperty(); + fail("Calling Property.getProperty() for a PATH value that doesn't have a corresponding Property, ItemNotFoundException is expected"); + } catch (ItemNotFoundException e) { + //ok + } + } else { + try { + prop.getProperty(); + fail("Property.getNode() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PathTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PathTest.java new file mode 100644 index 00000000000..32062e18475 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PathTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.Property; +import javax.jcr.Workspace; + +/** + * PathTest... + */ +public class PathTest extends AbstractJCRTest { + + private String identifier; + + protected void setUp() throws Exception { + super.setUp(); + + identifier = testRootNode.getIdentifier(); + } + + public void testGetItem() throws RepositoryException { + Item item = superuser.getItem("[" + identifier + "]"); + assertTrue(item.isSame(testRootNode)); + } + + public void testCreatePathValue() throws RepositoryException { + ValueFactory vf = superuser.getValueFactory(); + Value pathValue = vf.createValue("[" +identifier+ "]", PropertyType.PATH); + + assertEquals(PropertyType.PATH, pathValue.getType()); + assertEquals("[" +identifier+ "]", pathValue.getString()); + assertFalse(pathValue.equals(vf.createValue(testRootNode.getPath(), PropertyType.PATH))); + } + + public void testCreateMultiplePathValue() throws RepositoryException { + ValueFactory vf = superuser.getValueFactory(); + Value vID1 = vf.createValue("[" +identifier+ "]", PropertyType.PATH); + Value v = vf.createValue(testRootNode.getPath(), PropertyType.PATH); + Value vID2 = vf.createValue("[" +identifier+ "]", PropertyType.PATH); + Value v2 = vf.createValue(testRootNode.getPath(), PropertyType.PATH); + + assertEquals(vID1, vID2); + assertEquals(v, v2); + + assertFalse(v.equals(vID1)); + assertFalse(v.equals(vID2)); + } + + public void testIdentifierBasedPropertyValue() throws RepositoryException { + ValueFactory vf = superuser.getValueFactory(); + Value pathValue = vf.createValue("[" +identifier+ "]", PropertyType.PATH); + + Property p = testRootNode.setProperty(propertyName1, pathValue); + + assertEquals(PropertyType.PATH, p.getType()); + assertEquals(pathValue.getString(), p.getValue().getString()); + assertEquals(pathValue, p.getValue()); + } + + public void testResolvedIdentifierBasedPropertyValue() throws RepositoryException { + ValueFactory vf = superuser.getValueFactory(); + Value pathValue = vf.createValue("[" +identifier+ "]", PropertyType.PATH); + + Property p = testRootNode.setProperty(propertyName1, pathValue); + assertTrue("Identifier-based PATH value must resolve to the Node the identifier has been obtained from.", + testRootNode.isSame(p.getNode())); + } + + public void testExtendedNameBasedPathValue() throws RepositoryException { + ValueFactory vf = superuser.getValueFactory(); + Value pathValue = vf.createValue(Workspace.PATH_VERSION_STORAGE_NODE, PropertyType.PATH); + + Property p = testRootNode.setProperty(propertyName1, pathValue); + assertEquals("/jcr:system/jcr:versionStorage", p.getString()); + + String path = Workspace.PATH_VERSION_STORAGE_NODE + "/a/b/c/jcr:frozenNode"; + pathValue = vf.createValue(path, PropertyType.PATH); + + p = testRootNode.setProperty(propertyName1, pathValue); + assertEquals("/jcr:system/jcr:versionStorage/a/b/c/jcr:frozenNode", p.getString()); + } + + public void testNotNormalizedPathValue() throws RepositoryException { + ValueFactory vf = superuser.getValueFactory(); + Value pathValue = vf.createValue("/a/../b/./c/dd/..", PropertyType.PATH); + + Property p = testRootNode.setProperty(propertyName1, pathValue); + + assertEquals(PropertyType.PATH, p.getType()); + assertEquals(pathValue.getString(), p.getValue().getString()); + assertEquals(pathValue, p.getValue()); + } +} \ No newline at end of file diff --git a/src/test/org/apache/jackrabbit/test/api/PropertyItemIsModifiedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyItemIsModifiedTest.java similarity index 87% rename from src/test/org/apache/jackrabbit/test/api/PropertyItemIsModifiedTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyItemIsModifiedTest.java index bfef8f505d1..b9ae0c9427d 100644 --- a/src/test/org/apache/jackrabbit/test/api/PropertyItemIsModifiedTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyItemIsModifiedTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -25,16 +25,13 @@ /** * Test cases for {@link Item#isModified()} on a property. - *

        - * Configuration requirements:
        + *

        + * Configuration requirements: + *

        * The node at {@link #testRoot} must allow a child node of type * {@link #testNodeType} with name {@link #nodeName1}. The node type must * support a non-mandatory string property with name {@link #propertyName1}. * - * @test - * @sources PropertyItemIsModifiedTest.java - * @executeClass org.apache.jackrabbit.test.api.PropertyItemIsModifiedTest - * @keywords level2 */ public class PropertyItemIsModifiedTest extends AbstractJCRTest { @@ -45,12 +42,17 @@ protected void setUp() throws Exception { // build persistent node try { testNode = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); } catch (RepositoryException e) { fail("Failed to create test node." + e.getMessage()); } } + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + /** * Test if Item.isModified() returns false after a new PropertyItem is set * (before node is saved (transient). That means the PropertyItem don't exists @@ -113,4 +115,4 @@ public void testPersistentPropertyItemIsModified () throws RepositoryException { assertFalse("Item.isModified() must return false after an existing Property is modified and the current Node is saved", testPropertyItem.isModified()); } -} \ No newline at end of file +} diff --git a/src/test/org/apache/jackrabbit/test/api/PropertyItemIsNewTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyItemIsNewTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/PropertyItemIsNewTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyItemIsNewTest.java index 7917895637c..f172192bc18 100644 --- a/src/test/org/apache/jackrabbit/test/api/PropertyItemIsNewTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyItemIsNewTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -25,16 +25,13 @@ /** * Test cases for {@link Item#isNew()} on a property. - *

        - * Configuration requirements:
        + *

        + * Configuration requirements: + *

        * The node at {@link #testRoot} must allow a child node of type * {@link #testNodeType} with name {@link #nodeName1}. The node type must * support a non-mandatory string property with name {@link #propertyName1}. * - * @test - * @sources PropertyItemIsNewTest.java - * @executeClass org.apache.jackrabbit.test.api.PropertyItemIsNewTest - * @keywords level2 */ public class PropertyItemIsNewTest extends AbstractJCRTest { @@ -45,12 +42,17 @@ protected void setUp() throws Exception { // build persistent node try { testNode = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); } catch (RepositoryException e) { fail("Failed to create test node." + e.getMessage()); } } + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + /** * Test if Item.isNew() returns true direct after a new PropertyItem is set * (before node is saved (transient)). @@ -80,4 +82,4 @@ public void testPersistentPropertyItemIsNew () throws RepositoryException { assertFalse("Item.isNew() must return false after a new PropertyItem is set and the current Node is saved", testPropertyItem.isNew()); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyReadMethodsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyReadMethodsTest.java new file mode 100644 index 00000000000..3f2dd04f8f5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyReadMethodsTest.java @@ -0,0 +1,345 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import java.util.NoSuchElementException; + +/** + * PropertyReadMethodsTest... + * + */ +public class PropertyReadMethodsTest extends AbstractJCRTest { + + /** + * Session to access the workspace + */ + private Session session; + + /** + * A property of the root node + */ + private Property property; + + /** + * Sets up the fixture for this test. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + + PropertyIterator properties = testRootNode.getProperties(); + try { + property = properties.nextProperty(); + } catch (NoSuchElementException e) { + fail("Any node must have at least one property set: jcr:primaryType"); + } + + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + property = null; + super.tearDown(); + } + + // -----------< tests of methods inherited of Item >------------------------ + + /** + * Tests if getPath() returns the correct path. + */ + public void testGetPath() + throws NotExecutableException, RepositoryException { + + assertEquals("getPath returns wrong result", + testRoot + "/" + property.getName(), + property.getPath()); + } + + /** + * Tests if getName() returns same as last name returned by getPath() + */ + public void testGetName() throws RepositoryException { + String path = property.getPath(); + String name = path.substring(path.lastIndexOf("/") + 1); + assertEquals("getName() must be the same as the last item in the path", + name, + property.getName()); + } + + /** + * Test if the ancestor at depth = n, where n is the depth of this + * Item, returns this Property itself. + * + * @throws RepositoryException + */ + public void testGetAncestorOfItemDepth() throws RepositoryException { + Property propertyAtDepth = (Property) property.getAncestor(property.getDepth()); + assertTrue("The ancestor of depth = n, where n is the depth of this " + + "Property must be the item itself.", property.isSame(propertyAtDepth)); + } + + /** + * Test if getting the ancestor of depth = n, where n is greater than depth + * of this Property, throws an ItemNotFoundException. + * + * @throws RepositoryException + */ + public void testGetAncestorOfGreaterDepth() throws RepositoryException { + try { + int greaterDepth = property.getDepth() + 1; + property.getAncestor(greaterDepth); + fail("Getting ancestor of depth n, where n is greater than depth of" + + "this Property must throw an ItemNotFoundException"); + } catch (ItemNotFoundException e) { + // success + } + } + + /** + * Test if getting the ancestor of negative depth throws an + * ItemNotFoundException. + * + * @throws RepositoryException + */ + public void testGetAncestorOfNegativeDepth() throws RepositoryException { + try { + property.getAncestor(-1); + fail("Getting ancestor of depth < 0 must throw an ItemNotFoundException."); + } catch (ItemNotFoundException e) { + // success + } + } + + /** + * Tests if getParent() returns parent node + */ + public void testGetParent() throws RepositoryException { + assertTrue("getParent() of a property must return the parent node.", + testRootNode.isSame(property.getParent())); + } + + /** + * Tests if depth of a property of depth of node + 1 + */ + public void testGetDepth() throws RepositoryException { + assertEquals("getDepth() of a property of root must be 1", testRootNode.getDepth() + 1, + property.getDepth()); + } + + /** + * Tests if getSession() is same as through which the Property was acquired + */ + public void testGetSession() throws RepositoryException { + assertSame("getSession must return the Session through which " + + "the Property was acquired.", + property.getSession(), + session); + } + + /** + * Tests if isMultiple() is consistent with PropertyDefinition.isMultiple(). + */ + public void testIsMultiple() throws RepositoryException { + assertEquals("Property.isMultiple() must be consistent with PropertyDefinition.isMultiple()", + property.isMultiple(), property.getDefinition().isMultiple()); + } + + /** + * Tests if isNode() returns false + */ + public void testIsNode() { + assertFalse("isNode() must return false.", + property.isNode()); + } + + /** + * Tests if isSame() returns true when retrieving a property through + * different sessions + */ + public void testIsSame() throws RepositoryException { + // access same property through different session + Session otherSession = getHelper().getReadOnlySession(); + try { + Property otherProperty = otherSession.getRootNode().getNode(testPath).getProperty(property.getName()); + assertTrue("isSame must return true for the same " + + "property retrieved through different sessions.", + property.isSame(otherProperty)); + } + finally { + otherSession.logout(); + } + } + + /** + * Tests if a Property calls the correct visit method on an {@link + * ItemVisitor}. + */ + public void testAccept() throws RepositoryException { + final Property p = property; + + ItemVisitor itemVisitor = new ItemVisitor() { + public void visit(Property property) + throws RepositoryException { + assertTrue("Visited Property is not the same as the one returned by visit(Property).", + p.isSame(property)); + } + + public void visit(Node node) { + fail("Wrong accept method executed."); + } + }; + + p.accept(itemVisitor); + } + + /** + * Tests that no null value property exists in a given node tree. + */ + public void testNoNullValue() throws RepositoryException { + assertFalse("Single property with null value found.", + PropertyUtil.nullValues(testRootNode)); + } + + /** + * Tests that all values of a multivalue property have the same property + * type. + */ + public void testMultiValueType() throws RepositoryException, NotExecutableException { + Property multiValProp = PropertyUtil.searchMultivalProp(testRootNode); + if (multiValProp != null) { + Value[] vals = multiValProp.getValues(); + if (vals.length > 0) { + int type = vals[0].getType(); + for (int i = 1; i < vals.length; i++) { + assertEquals("Multivalue property has values with different types.", + type, vals[i].getType()); + } + } + } else { + throw new NotExecutableException(); + } + } + + /** + * Tests failure of Property.getValue() method for a multivalue property. + */ + public void testGetValue() throws RepositoryException, NotExecutableException { + Property multiValProp = PropertyUtil.searchMultivalProp(testRootNode); + if (multiValProp != null) { + try { + multiValProp.getValue(); + fail("Property.getValue() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } else { + throw new NotExecutableException(); + } + } + + /** + * Tests failure of Property.getValues() method for a single value + * property. + */ + public void testGetValues() throws RepositoryException, NotExecutableException { + Property singleProp = PropertyUtil.searchSingleValuedProperty(testRootNode); + if (singleProp == null) { + throw new NotExecutableException("No single valued property found."); + } + + try { + singleProp.getValues(); + fail("Property.getValues() called on a single property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + + /** + * Tests if Property.getValues() returns an array that is a copy + * of the stored values, so changes to it are not reflected in internal storage. + */ + public void testGetValueCopyStoredValues() + throws NotExecutableException, RepositoryException { + + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("No multivalued property found."); + } + + // acquire the values of the property and change the zeroth value + Value[] values = prop.getValues(); + if (values.length == 0) { + throw new NotExecutableException("No testable property found."); + } + values[0] = null; + + // re-acquire the values and check if nulled value still exists + Value[] values2 = prop.getValues(); + assertNotNull("Changes on the array returned by Property.getValues() must " + + "not be reflected in the internal storage.", + values2[0]); + } + + /** + * Tests if Property.getNode() fails with ValueFormatException for + * multivalued properties. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testGetNode() throws RepositoryException, NotExecutableException { + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("Test Property.getNode is throwing a " + + "ValueFormaException not executable in case of a multivalued property."); + } + else { + try { + prop.getNode(); + fail("Property.getNode should throw a ValueFormatException in case of " + + "a multivalued property."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyTest.java new file mode 100644 index 00000000000..d48ba827423 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Session; + +/** + * PropertyTest contains all test cases for the + * javax.jcr.Property that are related to writing, modifying or + * deleting properties (level 2 of the specification). + *

          + *
        • {@code nodetype} name of a node type. The node at testroot + * must allow child nodes with this node. + *
        • {@code nodename1} name of a child node at testroot. + *
        • {@code propertyname1} name of a string property in + * nodetype. + *
        + */ +public class PropertyTest extends AbstractJCRTest { + + /** + * Tests if Item.isSame(Item otherItem) will return true when + * two Property objects representing the same actual repository + * item have been retrieved through two different sessions and one has been + * modified. + * + * @since JCR 2.0 + */ + public void testIsSameMustNotCompareStates() + throws RepositoryException { + + // create a node, add a property and save it + Node testNode1 = testRootNode.addNode(nodeName1, testNodeType); + Property prop1 = testNode1.setProperty(propertyName1, "value1"); + testRootNode.getSession().save(); + + // accuire the same property through a different session + Session session = getHelper().getSuperuserSession(); + try { + Property prop2 = session.getProperty(prop1.getPath()); + + // change the value of prop2 + prop2.setValue("value2"); + + assertTrue("Two references of same property must return true for " + + "property1.isSame(property2)", prop1.isSame(prop2)); + } finally { + session.logout(); + } + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/PropertyTypeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyTypeTest.java similarity index 86% rename from src/test/org/apache/jackrabbit/test/api/PropertyTypeTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyTypeTest.java index 3bca9c5ef65..7b5e7329132 100644 --- a/src/test/org/apache/jackrabbit/test/api/PropertyTypeTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyTypeTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -31,10 +31,6 @@ * as no property is of type UNDEFINED. This test runs recursively through * the workspace starting at {@link #testRoot}. * - * @test - * @sources PropertyTypeTest.java - * @executeClass org.apache.jackrabbit.test.api.PropertyTypeTest - * @keywords level1 */ public class PropertyTypeTest extends AbstractJCRTest { @@ -52,7 +48,7 @@ protected void setUp() throws Exception { * the workspace starting at {@link #testRoot}. */ public void testType() throws RepositoryException { - Session session = helper.getReadOnlySession(); + Session session = getHelper().getReadOnlySession(); try { Node root = session.getRootNode().getNode(testPath); typeCheckChildren(root); @@ -103,4 +99,4 @@ private void typeCheckChildren(Node parentNode) } -} \ No newline at end of file +} diff --git a/src/test/org/apache/jackrabbit/test/api/PropertyUtil.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyUtil.java similarity index 90% rename from src/test/org/apache/jackrabbit/test/api/PropertyUtil.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyUtil.java index 3e1fbdb56c0..61dac71bcbe 100644 --- a/src/test/org/apache/jackrabbit/test/api/PropertyUtil.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/PropertyUtil.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -30,6 +30,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.IOException; /** * This class provides various utility methods that are used by the property @@ -81,8 +83,8 @@ public class PropertyUtil { "\\u0D57|\\u0E31|\\u0E34-\\u0E3A|\\u0E47-\\u0E4E|\\u0EB1|\\u0EB4-\\u0EB9|\\u0EBB-\\u0EBC|\\u0EC8-\\u0ECD|" + "\\u0F18-\\u0F19|\\u0F35|\\u0F37|\\u0F39|\\u0F3E|\\u0F3F|\\u0F71-\\u0F84|\\u0F86-\\u0F8B|\\u0F90-\\u0F95|\\u0F97|" + "\\u0F99-\\u0FAD|\\u0FB1-\\u0FB7|\\u0FB9|\\u20D0-\\u20DC|\\u20E1|\\u302A-\\u302F|\\u3099|\\u309A"; - - public static final String DIGIT = + + public static final String DIGIT = "\\u0030-\\u0039|\\u0660-\\u0669|\\u06F0-\\u06F9|\\u0966-\\u096F|\\u09E6-\\u09EF|\\u0A66-\\u0A6F|\\u0AE6-\\u0AEF|" + "\\u0B66-\\u0B6F|\\u0BE7-\\u0BEF|\\u0C66-\\u0C6F|\\u0CE6-\\u0CEF|\\u0D66-\\u0D6F|\\u0E50-\\u0E59|\\u0ED0-\\u0ED9|" + "\\u0F20-\\u0F29"; @@ -106,7 +108,7 @@ public class PropertyUtil { public static final String SIMPLENAME_CHAR = "[^/:\\[\\]\\*'\"\\s]"; - + public static final String PATTERNSTRING_NAME = "((" + NC_NAME + "):)?" + // prefix SIMPLENAME_CHAR + "([" + SIMPLENAME_CHAR + "| ]*" + SIMPLENAME_CHAR +")?"; @@ -144,9 +146,10 @@ private PropertyUtil() { * * @param node the node to start traverse * @param type the property type to search for + * @param multiple whether the property should be multivalued (null: does not matter) * @return the property found or null if no property is found */ - public static Property searchProp(Session session, Node node, int type) + public static Property searchProp(Session session, Node node, int type, Boolean multiple) throws RepositoryException, ValueFormatException { Property prop = null; @@ -155,7 +158,7 @@ public static Property searchProp(Session session, Node node, int type) for (PropertyIterator props = node.getProperties(); props.hasNext();) { Property property = props.nextProperty(); propType = property.getType(); - if (propType == type) { + if (propType == type && (multiple == null || multiple.booleanValue() == property.getDefinition().isMultiple())) { prop = property; break; } @@ -164,7 +167,7 @@ public static Property searchProp(Session session, Node node, int type) if (prop == null) { for (NodeIterator nodes = node.getNodes(); nodes.hasNext();) { Node n = nodes.nextNode(); - prop = searchProp(session, n, type); + prop = searchProp(session, n, type, multiple); if (prop != null) { break; } @@ -281,14 +284,22 @@ public static boolean isDateFormat(String str) { */ public static long countBytes(Value val) { int length = 0; + InputStream in = null; try { - BufferedInputStream bin = new BufferedInputStream(val.getStream()); + in = val.getStream(); + BufferedInputStream bin = new BufferedInputStream(in); while (bin.read() != -1) { length++; } bin.close(); } catch (Exception e) { length = -1; + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignore) {} + } } return length; } @@ -416,6 +427,24 @@ public static Property searchMultivalProp(Node node, int type) throws Repository return multiVal; } + /** + * Retrieve a single valued property from the given node. + * + * @param node + * @return the property found or null if no property is found. + */ + public static Property searchSingleValuedProperty(Node node) + throws RepositoryException, ValueFormatException { + PropertyIterator props = node.getProperties(); + while (props.hasNext()) { + Property p = props.nextProperty(); + if (!p.getDefinition().isMultiple()) { + return p; + } + } + // should never get here, since every Node must provide the jcr:primaryType + // property, which is single valued. + return null; + } +} - -} \ No newline at end of file diff --git a/src/test/org/apache/jackrabbit/test/api/ReferencePropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferencePropertyTest.java similarity index 82% rename from src/test/org/apache/jackrabbit/test/api/ReferencePropertyTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferencePropertyTest.java index 48ce865681b..f85998439b0 100644 --- a/src/test/org/apache/jackrabbit/test/api/ReferencePropertyTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferencePropertyTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -29,10 +29,6 @@ * a reference property a {@link org.apache.jackrabbit.test.NotExecutableException} * is thrown. * - * @test - * @sources ReferencePropertyTest.java - * @executeClass org.apache.jackrabbit.test.api.ReferencePropertyTest - * @keywords level1 */ public class ReferencePropertyTest extends AbstractPropertyTest { @@ -47,6 +43,11 @@ protected void setUp() throws Exception { referencedNode = prop.getNode(); } + protected void tearDown() throws Exception { + referencedNode = null; + super.tearDown(); + } + /** * Returns {@link javax.jcr.PropertyType#REFERENCE}. * @return {@link javax.jcr.PropertyType#REFERENCE}. @@ -55,6 +56,14 @@ protected int getPropertyType() { return PropertyType.REFERENCE; } + /** + * Returns {@link Boolean#FALSE}. + * @return {@link Boolean#FALSE}. + */ + protected Boolean getPropertyIsMultivalued() { + return Boolean.FALSE; + } + /** * Tests if the referenced node is of nodeType mix:referenceable. */ @@ -82,7 +91,7 @@ public void testPropValue() throws RepositoryException { } } assertTrue("Referencing property of node " + referenced.getName() + - " nof found.", found); + " not found.", found); assertTrue("Referenced node retrieved with getNode is different " + "from the node retrieved with getNodeByUUID", referenced.isSame(referencedNode)); @@ -157,6 +166,19 @@ public void testGetString() throws RepositoryException { val.getString(); } + /** + * Tests dereferencing a REFERENCE property to a Property + * @since JCR 2.0 + */ + public void testGetProperty() throws RepositoryException { + try { + prop.getProperty(); + fail("A REFERENCE property cannot be resolved to a Property."); + } catch (ValueFormatException e) { + // ok + } + } + /** * Tests if Value.getType() returns the same as Property.getType() and also * tests that prop.getDefinition().getRequiredType() returns the same type @@ -170,8 +192,8 @@ public void testGetType() throws RepositoryException { * Tests equals method of Reference value. */ public void testEquals() throws RepositoryException { - Property prop2 = referencedNode.getProperty("jcr:uuid"); + Property prop2 = referencedNode.getProperty(jcrUUID); assertTrue("Incorrect equals method of Reference value.", PropertyUtil.equalValues(prop2.getValue(), prop.getValue())); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferenceableRootNodesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferenceableRootNodesTest.java new file mode 100644 index 00000000000..34e62bcae32 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferenceableRootNodesTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** + * ReferenceableRootNodesTest contains tests with referenceable + * nodes between different workspaces. + * + */ +public class ReferenceableRootNodesTest extends AbstractJCRTest { + + /** + * The read-only session for the second workspace + */ + protected Session sessionW2; + + /** + * The read-only session for the default workspace + */ + protected Session session; + + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + sessionW2 = getHelper().getReadOnlySession(workspaceName); + + String wspName = session.getWorkspace().getName(); + boolean sameWsp = (wspName == null) ? workspaceName == null : wspName.equals(workspaceName); + if (sameWsp) { + throw new NotExecutableException("Cannot compare uuid behaviour of different workspaces. Only a single workspace configured."); + } + } + + /** + * Releases the sessions aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (sessionW2 != null) { + sessionW2.logout(); + sessionW2 = null; + } + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * A repository implementation may make its workspace root nodes + * mix:referenceable. If so, then the root node of all workspaces must be + * referenceable, and all must have the same UUID. + */ + public void testReferenceableRootNode() + throws RepositoryException, NotExecutableException { + // compare UUID of default workspace and a second workspace + Node rootNode = session.getRootNode(); + if (rootNode.isNodeType(mixReferenceable)) { + + // check if root node in second workspace is referenceable too + Node rootNodeW2 = sessionW2.getRootNode(); + if (!rootNodeW2.isNodeType(mixReferenceable)) { + fail("Root node in second workspace is not referenceable."); + } + + // check if all root nodes have the same UUID + assertEquals("Referenceable root nodes of different workspaces must have same UUID.", + rootNode.getUUID(), + rootNodeW2.getUUID()); + } else { + throw new NotExecutableException("Root node is not referenceable"); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferencesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferencesTest.java new file mode 100644 index 00000000000..24156dbde8c --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ReferencesTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import java.util.Set; +import java.util.HashSet; + +/** + * ReferencesTest contains the test cases for the references. + * + */ +public class ReferencesTest extends AbstractJCRTest { + + /** + * Tests Node.getReferences() + */ + public void testReferences() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixReferenceable); + + // with some impls. the mixin type has only affect upon save + testRootNode.getSession().save(); + + // make sure the node is now referenceable + assertTrue("test node should be mix:referenceable", n1.isNodeType(mixReferenceable)); + + // create references: n2.p1 -> n1 + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + + Value[] values = new Value[]{superuser.getValueFactory().createValue(n1)}; + + // abort test if the repository does not allow setting + // reference properties on this node + ensureCanSetProperty(n2, propertyName1, values); + + Property p1 = n2.setProperty(propertyName1, values); + testRootNode.getSession().save(); + PropertyIterator iter = n1.getReferences(); + if (iter.hasNext()) { + assertEquals("Wrong referer", iter.nextProperty().getPath(), p1.getPath()); + } else { + fail("no referer"); + } + + // create references: n3.p1 -> n1 + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + n3.setProperty(propertyName1, n1); + testRootNode.getSession().save(); + iter = n1.getReferences(); + while (iter.hasNext()) { + Property p = iter.nextProperty(); + if (n2 != null && p.getParent().getPath().equals(n2.getPath())) { + n2 = null; + } else if (n3 != null && p.getParent().getPath().equals(n3.getPath())) { + n3 = null; + } else { + fail("too many referers: " + p.getPath()); + } + } + if (n2 != null) { + fail("referer not in references set: " + n2.getPath()); + } + if (n3 != null) { + fail("referer not in references set: " + n3.getPath()); + } + + // remove reference n3.p1 -> n1 + testRootNode.getNode(nodeName3).getProperty(propertyName1).remove(); + testRootNode.getSession().save(); + iter = n1.getReferences(); + if (iter.hasNext()) { + assertEquals("Wrong referer", iter.nextProperty().getParent().getPath(), testRootNode.getNode(nodeName2).getPath()); + } else { + fail("no referer"); + } + if (iter.hasNext()) { + fail("too many referers: " + iter.nextProperty().getPath()); + } + + // remove reference n2.p1 -> n1 + testRootNode.getNode(nodeName2).getProperty(propertyName1).setValue(new Value[0]); + testRootNode.getSession().save(); + iter = n1.getReferences(); + if (iter.hasNext()) { + fail("too many referers: " + iter.nextProperty().getPath()); + } + } + + /** + * Tests Node.getReferences(String) + */ + public void testGetReferencesWithName() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixReferenceable); + + // with some impls. the mixin type has only affect upon save + testRootNode.getSession().save(); + + // make sure the node is now referenceable + assertTrue("test node should be mix:referenceable", n1.isNodeType(mixReferenceable)); + + // create references: + // n2.p1 -> n1 + // n2.p2 -> n1 + // n3.p1 -> n1 + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + + Value[] values = new Value[]{superuser.getValueFactory().createValue(n1)}; + + // abort test if the repository does not allow setting + // reference properties on this node + ensureCanSetProperty(n2, propertyName1, values); + ensureCanSetProperty(n2, propertyName2, values); + ensureCanSetProperty(n3, propertyName1, values); + + Property p1 = n2.setProperty(propertyName1, values); + Property p2 = n2.setProperty(propertyName2, values); + Property p3 = n3.setProperty(propertyName1, n1); + testRootNode.getSession().save(); + + // get references with name propertyName1 + // (should return p1 and p3)) + PropertyIterator iter = n1.getReferences(propertyName1); + Set results = new HashSet(); + while (iter.hasNext()) { + results.add(iter.nextProperty().getPath()); + } + assertEquals("wrong number of references reported", 2, results.size()); + assertTrue("missing reference property: " + p1.getPath(), results.contains(p1.getPath())); + assertTrue("missing reference property: " + p3.getPath(), results.contains(p3.getPath())); + + // get references with name propertyName2 + // (should return p2)) + iter = n1.getReferences(propertyName2); + results.clear(); + while (iter.hasNext()) { + results.add(iter.nextProperty().getPath()); + } + assertEquals("wrong number of references reported", 1, results.size()); + assertTrue("missing reference property: " + p2.getPath(), results.contains(p2.getPath())); + + // remove reference n3.p1 -> n1 + testRootNode.getNode(nodeName3).getProperty(propertyName1).remove(); + testRootNode.getSession().save(); + + // get references with name propertyName1 + // (should return p1)) + iter = n1.getReferences(propertyName1); + results.clear(); + while (iter.hasNext()) { + results.add(iter.nextProperty().getPath()); + } + assertEquals("wrong number of references reported", 1, results.size()); + assertTrue("missing reference property: " + p1.getPath(), results.contains(p1.getPath())); + + // remove reference n2.p1 -> n1 + p1.remove(); + testRootNode.getSession().save(); + + // get references with name propertyName1 + // (should nothing)) + iter = n1.getReferences(propertyName1); + results.clear(); + while (iter.hasNext()) { + results.add(iter.nextProperty().getPath()); + } + assertEquals("wrong number of references reported", 0, results.size()); + } + + /** + * Tests Property.getNode(); + */ + public void testReferenceTarget() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixReferenceable); + + // with some impls. the mixin type has only affect upon save + testRootNode.getSession().save(); + + // make sure the node is now referenceable + assertTrue("test node should be mix:referenceable", n1.isNodeType(mixReferenceable)); + + // create references: n2.p1 -> n1 + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + + // abort test if the repository does not allow setting + // reference properties on this node + ensureCanSetProperty(n2, propertyName1, n2.getSession().getValueFactory().createValue(n1)); + + n2.setProperty(propertyName1, n1); + testRootNode.getSession().save(); + assertEquals("Wrong reference target.", n2.getProperty(propertyName1).getNode().getUUID(), n1.getUUID()); + n2.remove(); + testRootNode.getSession().save(); + } + + /** + * Tests changing a reference property + */ + public void testAlterReference() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixReferenceable); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixReferenceable); + + // with some impls. the mixin type has only affect upon save + testRootNode.getSession().save(); + + // make sure the nodes are now referenceable + assertTrue("test node should be mix:referenceable", n1.isNodeType(mixReferenceable)); + assertTrue("test node should be mix:referenceable", n2.isNodeType(mixReferenceable)); + + // create references: n3.p1 -> n1 + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + + // abort test if the repository does not allow setting + // reference properties on this node + ensureCanSetProperty(n3, propertyName1, n3.getSession().getValueFactory().createValue(n1)); + + n3.setProperty(propertyName1, n1); + testRootNode.getSession().save(); + assertEquals("Wrong reference target.", n3.getProperty(propertyName1).getNode().getUUID(), n1.getUUID()); + PropertyIterator iter = n1.getReferences(); + if (iter.hasNext()) { + assertEquals("Wrong referer", iter.nextProperty().getParent().getPath(), n3.getPath()); + } else { + fail("no referer"); + } + if (iter.hasNext()) { + fail("too many referers: " + iter.nextProperty().getPath()); + } + // change reference: n3.p1 -> n2 + n3.setProperty(propertyName1, n2); + n3.save(); + assertEquals("Wrong reference target.", n3.getProperty(propertyName1).getNode().getUUID(), n2.getUUID()); + iter = n1.getReferences(); + if (iter.hasNext()) { + fail("too many referers: " + iter.nextProperty().getPath()); + } + iter = n2.getReferences(); + if (iter.hasNext()) { + assertEquals("Wrong referer", iter.nextProperty().getParent().getPath(), n3.getPath()); + } else { + fail("no referers"); + } + if (iter.hasNext()) { + fail("too many referers: " + iter.nextProperty().getPath()); + } + + // clear reference by overwriting by other type + n3.setProperty(propertyName1, "Hello, world."); + n3.save(); + iter = n2.getReferences(); + if (iter.hasNext()) { + fail("too many referers: " + iter.nextProperty().getPath()); + } + } + + public void testNonReferenceable() throws RepositoryException, NotExecutableException { + Node nonReferenceable = null; + if (testRootNode.isNodeType(mixReferenceable)) { + Node child = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + if (!child.isNodeType(mixReferenceable)) { + nonReferenceable = child; + } + } else { + nonReferenceable = testRootNode; + } + + if (nonReferenceable == null) { + throw new NotExecutableException("Test node is referenceable."); + } + + // getReferences must return an empty iterator and must not throw. + assertFalse(nonReferenceable.getReferences().hasNext()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryDescriptorTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryDescriptorTest.java new file mode 100644 index 00000000000..f0d2e0b447a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryDescriptorTest.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Arrays; + +/** + * Tests if the required repository descriptors are available. + * + */ +public class RepositoryDescriptorTest extends AbstractJCRTest { + + private static final Set requiredDescriptorKeys = new HashSet(); + + static { + requiredDescriptorKeys.add(Repository.IDENTIFIER_STABILITY); + requiredDescriptorKeys.add(Repository.LEVEL_1_SUPPORTED); + requiredDescriptorKeys.add(Repository.LEVEL_2_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_INHERITANCE); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_PROPERTY_TYPES); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED); + requiredDescriptorKeys.add(Repository.NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPORTED); + requiredDescriptorKeys.add(Repository.OPTION_ACCESS_CONTROL_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_LIFECYCLE_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_LOCKING_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_OBSERVATION_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_QUERY_SQL_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_RETENTION_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_SHAREABLE_NODES_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_SIMPLE_VERSIONING_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_TRANSACTIONS_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_UNFILED_CONTENT_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_VERSIONING_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_XML_EXPORT_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_XML_IMPORT_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_ACTIVITIES_SUPPORTED); + requiredDescriptorKeys.add(Repository.OPTION_BASELINES_SUPPORTED); + + requiredDescriptorKeys.add(Repository.QUERY_FULL_TEXT_SEARCH_SUPPORTED); + requiredDescriptorKeys.add(Repository.QUERY_JOINS); + requiredDescriptorKeys.add(Repository.QUERY_LANGUAGES); + requiredDescriptorKeys.add(Repository.QUERY_STORED_QUERIES_SUPPORTED); + requiredDescriptorKeys.add(Repository.QUERY_XPATH_DOC_ORDER); + requiredDescriptorKeys.add(Repository.QUERY_XPATH_POS_INDEX); + requiredDescriptorKeys.add(Repository.REP_NAME_DESC); + requiredDescriptorKeys.add(Repository.REP_VENDOR_DESC); + requiredDescriptorKeys.add(Repository.REP_VENDOR_URL_DESC); + requiredDescriptorKeys.add(Repository.SPEC_NAME_DESC); + requiredDescriptorKeys.add(Repository.SPEC_VERSION_DESC); + requiredDescriptorKeys.add(Repository.WRITE_SUPPORTED); + } + + /** The session for the tests */ + private Session session; + + /** + * Sets up the fixture for this test. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Tests that the required repository descriptors are available. + */ + public void testRequiredDescriptors() { + Repository rep = session.getRepository(); + for (Iterator it = requiredDescriptorKeys.iterator(); it.hasNext();) { + String descName = it.next(); + assertTrue(descName + " is a standard descriptor", rep.isStandardDescriptor(descName)); + if (rep.isSingleValueDescriptor(descName)) { + Value val = rep.getDescriptorValue(descName); + assertNotNull("Required descriptor is missing: " + descName, + val); + } else { + Value[] vals = rep.getDescriptorValues(descName); + assertNotNull("Required descriptor is missing: " + descName, + vals); + } + } + } + + /** + * Tests if {@link Repository#getDescriptorKeys()} returns all required + * descriptors keys. + */ + public void testGetDescriptorKeys() { + List keys = Arrays.asList(session.getRepository().getDescriptorKeys()); + for (Iterator it = requiredDescriptorKeys.iterator(); it.hasNext();) { + String key = it.next(); + assertTrue("Required descriptor is missing: " + key, + keys.contains(key)); + } + } + + /** + * Tests whether {@link Repository#getDescriptorValues(String)} returns an + * Value[] of size 1 for single valued descriptors. + */ + public void testGetDescriptorValues() { + Repository rep = session.getRepository(); + // "option.node.type.management.supported" denotes a single-valued BOOLEAN descriptor + String descName = Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED; + assertTrue(rep.isSingleValueDescriptor(descName)); + Value[] vals = rep.getDescriptorValues(descName); + assertNotNull("Required descriptor is missing: " + descName, vals); + assertEquals(1, vals.length); + assertEquals(PropertyType.BOOLEAN, vals[0].getType()); + try { + // getDescriptorValue(key).getString() is equivalent to getDescriptor(key) + assertEquals(vals[0].getString(), rep.getDescriptor(descName)); + } catch (RepositoryException e) { + fail(e.getMessage()); + } + + // "option.node.type.management.supported" denotes a single-valued BOOLEAN descriptor + descName = Repository.QUERY_LANGUAGES; + assertFalse(rep.isSingleValueDescriptor(descName)); + Value val = rep.getDescriptorValue(descName); + assertNull(descName + " is a multi-value descriptor, getDescriptorValue() should return null", val); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryFactoryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryFactoryTest.java new file mode 100644 index 00000000000..2f39caedca2 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryFactoryTest.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.test.api; + +import java.util.Collections; + +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.RepositoryStub; + +/** + * RepositoryFactoryTest checks if there is a repository factory + * implementation and that is works according to the spec. + */ +public class RepositoryFactoryTest extends AbstractJCRTest { + + public void testDefaultRepository() throws Exception { + // must not throw + getRepositoryFactory().getRepository(null); + } + + public void testEmptyParameters() throws Exception { + // must not throw + getRepositoryFactory().getRepository(Collections.EMPTY_MAP); + } + + protected RepositoryFactory getRepositoryFactory() + throws RepositoryException { + String className = getProperty(RepositoryStub.REPOSITORY_FACTORY); + if (className == null) { + fail("Property '" + RepositoryStub.REPOSITORY_FACTORY + "' is not defined."); + } else { + try { + return (RepositoryFactory) Class.forName(className).newInstance(); + } catch (Exception e) { + fail(e.toString()); + } + } + return null; + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/RepositoryLoginTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryLoginTest.java similarity index 78% rename from src/test/org/apache/jackrabbit/test/api/RepositoryLoginTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryLoginTest.java index 7d584d89572..46053d823b6 100644 --- a/src/test/org/apache/jackrabbit/test/api/RepositoryLoginTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RepositoryLoginTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -27,10 +27,6 @@ /** * RepositoryLoginTest tests the login methods of a repository. * - * @test - * @sources RepositoryLoginTest.java - * @executeClass org.apache.jackrabbit.test.api.RepositoryLoginTest - * @keywords level1 */ public class RepositoryLoginTest extends AbstractJCRTest { @@ -45,9 +41,9 @@ protected void setUp() throws Exception { isReadOnly = true; super.setUp(); - credentials = helper.getReadOnlyCredentials(); + credentials = getHelper().getReadOnlyCredentials(); workspaceName = superuser.getWorkspace().getName(); - repository = helper.getRepository(); + repository = getHelper().getRepository(); } /** @@ -58,20 +54,25 @@ protected void setUp() throws Exception { public void testNoSuchWorkspaceException() throws RepositoryException { - Session session = helper.getReadOnlySession(); + Session session = getHelper().getReadOnlySession(); String name; try { name = getNonExistingWorkspaceName(session); } finally { session.logout(); + session = null; } try { - helper.getRepository().login(credentials, name); + session = getHelper().getRepository().login(credentials, name); fail("login with a not available workspace name must throw a " + "NoSuchWorkspaceException"); } catch (NoSuchWorkspaceException e) { // success + } finally { + if (session != null) { + session.logout(); + } } } @@ -108,4 +109,4 @@ public void testSignatureCredentials() s.logout(); } } -} \ No newline at end of file +} diff --git a/src/test/org/apache/jackrabbit/test/api/RootNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RootNodeTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/RootNodeTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RootNodeTest.java index e3991f83109..8eb4adbec0f 100644 --- a/src/test/org/apache/jackrabbit/test/api/RootNodeTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/RootNodeTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -26,10 +26,6 @@ /** * Test cases for the root node. * - * @test - * @sources RootNodeTest.java - * @executeClass org.apache.jackrabbit.test.api.RootNodeTest - * @keywords level1 */ public class RootNodeTest extends AbstractJCRTest { @@ -45,7 +41,7 @@ public class RootNodeTest extends AbstractJCRTest { protected void setUp() throws Exception { isReadOnly = true; super.setUp(); - session = helper.getReadOnlySession(); + session = getHelper().getReadOnlySession(); rootNode = session.getRootNode(); } @@ -55,7 +51,9 @@ protected void setUp() throws Exception { protected void tearDown() throws Exception { if (session != null) { session.logout(); + session = null; } + rootNode = null; super.tearDown(); } @@ -91,4 +89,4 @@ public void testGetParent() throws RepositoryException { // success: ItemNotFoundException as required by the specification. } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SerializationContext.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SerializationContext.java new file mode 100644 index 00000000000..805f66c8d10 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SerializationContext.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.RepositoryStub; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Session; + +/** + * Encapsulates the various properties that are needed for a serialization test + * case. + */ +class SerializationContext { + + private AbstractJCRTest baseTest; + public String testroot; + public String nodetype; + public String sourceFolderName; + public String targetFolderName; + public String rootNodeName; + public String nodeName1; + public String nodeName2; + public String nodeName3; + public String testNodeType; + public String propertyName1; + public String jcrPrimaryType; + public String mixReferenceable; + + public String propertyValueMayChange; + public String propertySkipped; + + public String nodeTypesTestNode; + public String mixinTypeTestNode; + public String propertyTypesTestNode; + public String sameNameChildrenTestNode; + public String multiValuePropertiesTestNode; + public String referenceableNodeTestNode; + public String orderChildrenTestNode; + public String namespaceTestNode; + public String sameNameSibsFalseChildNodeDefinition; + + + public String stringTestProperty; + public String binaryTestProperty; + public String dateTestProperty; + public String longTestProperty; + public String doubleTestProperty; + public String booleanTestProperty; + public String nameTestProperty; + public String pathTestProperty; + public String referenceTestProperty; + public String multiValueTestProperty; + + public SerializationContext(AbstractJCRTest test, Session session) + throws RepositoryException { + // creates a serialization context based on a test class + baseTest = test; + + testroot = get("testroot"); + nodetype = get("nodetype"); + sourceFolderName = get("sourceFolderName"); + targetFolderName = get("targetFolderName"); + rootNodeName = get("rootNodeName"); + nodeName1 = get(RepositoryStub.PROP_NODE_NAME1); + nodeName2 = get(RepositoryStub.PROP_NODE_NAME2); + nodeName3 = get(RepositoryStub.PROP_NODE_NAME3); + testNodeType = get(RepositoryStub.PROP_NODETYPE); + propertyName1 = get(RepositoryStub.PROP_PROP_NAME1); + jcrPrimaryType = session.getNamespacePrefix(AbstractJCRTest.NS_JCR_URI) + ":primaryType"; + mixReferenceable = session.getNamespacePrefix(AbstractJCRTest.NS_MIX_URI) + ":referenceable"; + + propertyValueMayChange = " " + get("propertyValueMayChange") + " "; + propertySkipped = " " + get("propertySkipped") + " "; + + nodeTypesTestNode = get("nodeTypesTestNode"); + mixinTypeTestNode = get("mixinTypeTestNode"); + propertyTypesTestNode = get("propertyTypesTestNode"); + sameNameChildrenTestNode = get("sameNameChildrenTestNode"); + multiValuePropertiesTestNode = get("multiValuePropertiesTestNode"); + referenceableNodeTestNode = get("referenceableNodeTestNode"); + orderChildrenTestNode = get("orderChildrenTestNode"); + namespaceTestNode = get("namespaceTestNode"); + sameNameSibsFalseChildNodeDefinition = get("sameNameSibsFalseChildNodeDefinition"); + + stringTestProperty = get("stringTestProperty"); + binaryTestProperty = get("binaryTestProperty"); + dateTestProperty = get("dateTestProperty"); + longTestProperty = get("longTestProperty"); + doubleTestProperty = get("doubleTestProperty"); + booleanTestProperty = get("booleanTestProperty"); + nameTestProperty = get("nameTestProperty"); + pathTestProperty = get("pathTestProperty"); + referenceTestProperty = get("referenceTestProperty"); + multiValueTestProperty = get("multiValueTestProperty"); + } + + private String get(String name) throws RepositoryException { + String value = baseTest.getProperty(name); + if (value == null) { + throw new NullPointerException("Property '" + name + "' is not defined."); + } + return value; + } + + public void log(String message) { + baseTest.log.println(message); + } + + /** + * Ensures that the given node is of the given mixin type. + * + * @param node a node. + * @param mixin the name of a mixin type. + * @throws NotExecutableException if the node is not of type mixin and the + * mixin cannot be added. + * @throws RepositoryException if an error occurs. + */ + protected void ensureMixinType(Node node, String mixin) + throws NotExecutableException, RepositoryException { + if (!node.isNodeType(mixin)) { + if (node.canAddMixin(mixin)) { + node.addMixin(mixin); + } else { + throw new NotExecutableException(node.getPath() + + " does not support adding " + mixin); + } + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SerializationTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SerializationTest.java new file mode 100644 index 00000000000..7e530a6bf61 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SerializationTest.java @@ -0,0 +1,771 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.util.InputStreamWrapper; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; +import org.xml.sax.helpers.DefaultHandler; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.version.VersionException; +import javax.xml.parsers.ParserConfigurationException; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +/** + * SerializationTest contains the test cases for the method + * Workspace.exportSysView() and Session.importSysView(). + *

        + * This class exports and re-imports the repository. The tests check for + * differences between the original and the re-imported repository. + * + */ +public class SerializationTest extends AbstractJCRTest { + protected Workspace workspace; + protected File file; + protected TreeComparator treeComparator; + + protected final boolean CONTENTHANDLER = true, STREAM = false; + protected final boolean WORKSPACE = true, SESSION = false; + protected final boolean SKIPBINARY = true, SAVEBINARY = false; + protected final boolean NORECURSE = true, RECURSE = false; + + protected Session session; + + public void setUp() throws RepositoryException, Exception { + super.setUp(); + + try { + session = superuser; + workspace = session.getWorkspace(); + + SerializationContext sc = new SerializationContext(this, session); + treeComparator = new TreeComparator(sc, session); + treeComparator.createComplexTree(treeComparator.WORKSPACE); + + file = File.createTempFile("serializationTest", ".xml"); + log.print("Tempfile: " + file.getAbsolutePath()); + } + catch (Exception ex) { + if (file != null) { + file.delete(); + file = null; + } + throw (ex); + } + } + + public void tearDown() throws Exception { + if (file != null) { + file.delete(); + file = null; + } + if (session != null && session.isLive()) { + session.logout(); + session = null; + } + workspace = null; + super.tearDown(); + } + +// ---------------< versioning exception tests >----------------------------------------- + /** + * Creates a simple target tree consisting of a checked-in node and an + * ordinary child node below. Throws a {@link NotExecutableException} if + * {@link #testNodeType} is not versionable. + * + * @param returnParent Whether the method returns a checked-in parent or the + * child of a checked-in parent. + * @return The requested node (a node that is checked in or that has a + * parent that is checked in). + */ + protected Node initVersioningException(boolean returnParent) throws RepositoryException, NotExecutableException, IOException { + Node vNode = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(vNode, mixVersionable); + Node vChild = vNode.addNode(nodeName2, testNodeType); + session.save(); + vNode.checkin(); + + exportRepository(SKIPBINARY, RECURSE); + + if (returnParent) { + return vNode; + } else { + return vChild; + } + } + + public void doTestVersioningExceptionFileParent(boolean useWorkspace, boolean useHandler) + throws Exception { + Node n = initVersioningException(true); + + FileInputStream in = new FileInputStream(file); + try { + doImport(n.getPath(), in, useWorkspace, useHandler); + fail("Importing to a checked-in node must throw a ConstraintViolationException."); + } catch (VersionException e) { + // success + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + public void doTestVersioningExceptionFileChild(boolean useWorkspace, boolean useHandler) + throws Exception { + Node n = initVersioningException(false); + + FileInputStream in = new FileInputStream(file); + try { + doImport(n.getPath(), in, useWorkspace, useHandler); + fail("Importing to a child of a checked-in node must throw a ConstraintViolationException."); + } catch (VersionException e) { + // success + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + public void testVersioningExceptionFileParentWorkspaceContentHandler() throws Exception { + doTestVersioningExceptionFileParent(WORKSPACE, CONTENTHANDLER); + } + + public void testVersioningExceptionFileParentSessionContentHandler() throws Exception { + doTestVersioningExceptionFileParent(SESSION, CONTENTHANDLER); + } + + public void testVersioningExceptionFileParentWorkspace() throws Exception { + doTestVersioningExceptionFileParent(WORKSPACE, STREAM); + } + + public void testVersioningExceptionFileParentSession() throws Exception { + doTestVersioningExceptionFileParent(SESSION, STREAM); + } + + public void testVersioningExceptionFileChildWorkspaceContentHandler() throws Exception { + doTestVersioningExceptionFileChild(WORKSPACE, CONTENTHANDLER); + } + + public void testVersioningExceptionFileChildSessionContentHandler() throws Exception { + doTestVersioningExceptionFileChild(SESSION, CONTENTHANDLER); + } + + public void testVersioningExceptionFileChildWorkspace() throws Exception { + doTestVersioningExceptionFileChild(WORKSPACE, STREAM); + } + + public void testVersioningExceptionFileChildSession() throws Exception { + doTestVersioningExceptionFileChild(SESSION, STREAM); + } + +// ----------------< locking exceptions tests >---------------------------- + /** + * Tests whether importing a tree respects locking. + */ + public void doTestLockException(boolean useWorkspace, boolean useHandler) + throws Exception { + exportRepository(SKIPBINARY, RECURSE); + if (isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + //A LockException is thrown if a lock prevents the addition of the subtree. + Node lNode = testRootNode.addNode(nodeName1); + ensureMixinType(lNode, mixLockable); + testRootNode.getSession().save(); + Lock lock = lNode.lock(true, true); + session.removeLockToken(lock.getLockToken()); //remove the token, so the lock is for me, too + FileInputStream in = new FileInputStream(file); + try { + doImport(lNode.getPath(), in, useWorkspace, useHandler); + } catch (LockException e) { + // success + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } else { + log.println("Locking not supported."); + } + } + + public void testLockExceptionWorkspaceWithHandler() throws Exception { + doTestVersioningExceptionFileChild(WORKSPACE, CONTENTHANDLER); + } + + public void testLockExceptionSessionWithHandler() throws Exception { + doTestVersioningExceptionFileChild(SESSION, CONTENTHANDLER); + } + + public void testLockExceptionWorkspace() throws Exception { + doTestVersioningExceptionFileChild(WORKSPACE, STREAM); + } + + public void testLockExceptionSession() throws Exception { + doTestVersioningExceptionFileChild(SESSION, STREAM); + } + +//--------------< Import of invalid xml file tests >------------------------------------------- + /** + * Tests whether importing an invalid XML file throws a SAX exception. The + * file used here is more or less garbage. + */ + public void testInvalidXmlThrowsSaxException() + throws IOException, ParserConfigurationException { + StringReader in = new StringReader(" file/>"); + ContentHandler ih = null; + try { + ih = session.getImportContentHandler(treeComparator.targetFolder, + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + } catch (RepositoryException e) { + fail("ImportHandler not created: " + e); + } + helpTestSaxException(ih, in, "session"); + + in = new StringReader(" file/>"); + try { + ih = workspace.getImportContentHandler(treeComparator.targetFolder, 0); + } catch (RepositoryException e) { + fail("ImportHandler not created: " + e); + } + helpTestSaxException(ih, in, "workspace"); + } + + /** + * Helper method for testSaxException. + * + * @param ih + * @param in + */ + private void helpTestSaxException(ContentHandler ih, Reader in, String mode) + throws IOException { + try { + createXMLReader(ih).parse(new InputSource(in)); + fail("Parsing an invalid XML file with via " + mode + " ContentHandler did not throw a SAXException."); + } catch (SAXException e) { + // success + } + } + + /** + * Tests whether importing an invalid XML file throws a InvalidSerializedDataException. + * The file used here is more or less garbage. + */ + public void testInvalidXmlThrowsInvalidSerializedDataException() + throws RepositoryException, IOException { + + String data = " file/>"; + ByteArrayInputStream in = new ByteArrayInputStream(data.getBytes()); + + try { + session.importXML(treeComparator.targetFolder, in, + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + fail("Importing a invalid XML file should throw a InvalidSerializedDataException."); + } catch (InvalidSerializedDataException e) { + // ok + } + in = new ByteArrayInputStream(data.getBytes()); + try { + workspace.importXML(treeComparator.targetFolder, in, 0); + fail("Importing a invalid XML file should throw a InvalidSerializedDataException."); + } catch (InvalidSerializedDataException e) { + // ok + } + } + +// -------------------< PathNotFoundException tests >------------------------------------ + /** + * Supplying an invalid repository path for import must throw a + * PathNotFoundException + */ + public void testWorkspaceGetImportContentHandlerExceptions() throws RepositoryException { + //Specifying a path that does not exist throws a PathNotFound exception + try { + workspace.getImportContentHandler(treeComparator.targetFolder + "/thisIsNotAnExistingNode", + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + fail("Specifying a non-existing path must throw a PathNotFoudException."); + } catch (PathNotFoundException e) { + // success + } + } + + /** + * Supplying an invalid repository path for import must throw a + * PathNotFoundException + */ + public void testSessionGetImportContentHandlerExceptions() throws RepositoryException { + //Specifying a path that does not exist throws a PathNotFound exception + try { + session.getImportContentHandler(treeComparator.targetFolder + "/thisIsNotAnExistingNode", + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + fail("Specifying a non-existing path must throw a PathNotFoudException."); + } catch (PathNotFoundException e) { + // success + } + } + + /** + * Tests the exception when importing: If the parent node does not exist. + */ + public void testSessionImportXmlExceptions() throws RepositoryException, IOException { + exportRepository(SKIPBINARY, RECURSE); + FileInputStream in = new FileInputStream(file); + + // If no node exists at parentAbsPath, a PathNotFoundException is thrown. + try { + session.importXML(treeComparator.targetFolder + "/thisNodeDoesNotExist", + in, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + fail("Importing to a non-existing node does not throw a PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + /** + * Tests the exceptions when importing: If the parent node does not exist, + * and if an IO error occurs. + */ + public void testWorkspaceImportXmlExceptions() throws RepositoryException, IOException { + exportRepository(SKIPBINARY, RECURSE); + FileInputStream in = new FileInputStream(file); + + //If no node exists at parentAbsPath, a PathNotFoundException is thrown. + try { + workspace.importXML(treeComparator.targetFolder + "/thisNodeDoesNotExist", in, 0); + fail("Importing to a non-existing node does not throw a PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + +// ---------------< Overwrite existing target node tests >------------------------------ +// ---------------< in case same name siblings not supported >------------------------------ + /** + * Tries to overwrite an existing node. This only works for nodes that do + * not allow same-name siblings. + */ + public void doTestOverwriteException(boolean useWorkspace, boolean useHandler) + throws Exception { + //If deserialization would overwrite an existing item, + // an ItemExistsException respective a SAXException is thrown. + + Node folder = testRootNode.addNode("myFolder", treeComparator.sc.sameNameSibsFalseChildNodeDefinition); + Node subfolder = folder.addNode("subfolder"); + + session.save(); + FileOutputStream out = new FileOutputStream(file); + try { + session.exportSystemView(subfolder.getPath(), out, true, true); + } finally { + out.close(); + } + + FileInputStream in = new FileInputStream(file); + try { + if (useHandler) { + try { + doImport(folder.getPath(), in, useWorkspace, useHandler); + fail("Overwriting an existing node during import must throw a SAXException"); + } catch (SAXException e) { + // success + } + } else { + try { + doImport(folder.getPath(), in, useWorkspace, useHandler); + fail("Overwriting an existing node during import must throw an ItemExistsException"); + } catch (ItemExistsException e) { + // success + } + } + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + public void testOverwriteExceptionWorkspaceWithHandler() throws Exception { + doTestOverwriteException(WORKSPACE, CONTENTHANDLER); + } + + public void testOverwriteExceptionSessionWithHandler() throws Exception { + doTestOverwriteException(SESSION, CONTENTHANDLER); + } + + public void testOverwriteExceptionWorkspace() throws Exception { + doTestOverwriteException(WORKSPACE, STREAM); + } + + public void testOverwriteExceptionSession() throws Exception { + doTestOverwriteException(SESSION, STREAM); + } + + // ------------------< Node type constraint violation tests >-------------------------------- + /** + * Create a node named ntBase with node type nt:base + * and creates a tree in the repository which will be exported + * and reimported below the node ntBase. + * + * @param useWorkspace + * @param useHandler + * @throws RepositoryException + * @throws FileNotFoundException + * @throws IOException + */ + public void doTestNodeTypeConstraintViolation(boolean useWorkspace, boolean useHandler) + throws Exception { + + treeComparator.createExampleTree(); + String nodetype = testNodeTypeNoChildren == null ? ntBase : testNodeTypeNoChildren; + Node node = testRootNode.addNode("ntBase", nodetype); + session.save(); + + FileInputStream in = new FileInputStream(file); + try { + if (useHandler) { + try { + doImport(node.getPath(), in, useWorkspace, useHandler); + fail("Node type constraint violation should throw a SAXException " + + "during xml import using a Contenthandler."); + } catch (SAXException se) { + // ok + } + } else { + try { + doImport(node.getPath(), in, useWorkspace, useHandler); + fail("Node type constraint violation should throw a " + + " InvalidSerializedDataException during xml import " + + "using a Contenthandler."); + } catch (InvalidSerializedDataException isde) { + // ok + } + } + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + + public void testNodeTypeConstraintViolationWorkspaceWithHandler() throws Exception { + doTestNodeTypeConstraintViolation(WORKSPACE, CONTENTHANDLER); + } + + public void testNodeTypeConstraintViolationSessionWithHandler() throws Exception { + doTestNodeTypeConstraintViolation(SESSION, CONTENTHANDLER); + } + + public void testNodeTypeConstraintViolationWorkspace() throws Exception { + doTestNodeTypeConstraintViolation(WORKSPACE, STREAM); + } + + public void testNodeTypeConstraintViolationSession() throws Exception { + doTestNodeTypeConstraintViolation(SESSION, STREAM); + } + +// ------------< tests that nothing is imported if session is closed before saving the import >----------------- + /** + * Makes sure that importing into the session does not save anything if the + * session is closed. + */ + public void testSessionImportXml() throws RepositoryException, IOException { + FileInputStream in = new FileInputStream(file); + try { + exportRepository(SAVEBINARY, RECURSE); + session.importXML(treeComparator.targetFolder, in, + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // after logout/login, no nodes are in the session + session.logout(); + superuser = null; //so tearDown won't fail + + session = getHelper().getReadWriteSession(); + treeComparator.setSession(session); + treeComparator.compare(treeComparator.CHECK_EMPTY); + } + + + /** + * Makes sure that importing into the session does not save anything if the + * session is closed. + */ + public void testSessionGetContentHandler() throws Exception { + FileInputStream in = new FileInputStream(file); + try { + exportRepository(SAVEBINARY, RECURSE); + doImportNoSave(treeComparator.targetFolder, in, CONTENTHANDLER); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // after logout/login, no nodes are in the session + session.logout(); + superuser = null; //so tearDown won't fail + + session = getHelper().getReadWriteSession(); + treeComparator.setSession(session); + treeComparator.compare(treeComparator.CHECK_EMPTY); + } + +//----------------< tests input stream handling contract >---------------------- + + /** + * Tests whether Session.importXML and Workspace.importXML + * obey the stream handling contract. + */ + public void testStreamHandling() throws RepositoryException, IOException { + exportRepository(SAVEBINARY, RECURSE); + + InputStreamWrapper in = new InputStreamWrapper(new FileInputStream(file)); + session.importXML(treeComparator.targetFolder, in, + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + assertTrue("Session.importXML(..., InputStream, ...) is expected to close the passed input stream", in.isClosed()); + session.refresh(false); + + in = new InputStreamWrapper(new FileInputStream(file)); + workspace.importXML(treeComparator.targetFolder, in, + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + assertTrue("Workspace.importXML(..., InputStream, ...) is expected to close the passed input stream", in.isClosed()); + } + + +//----------------< import test helper >-------------------------------------------------------- + /** + * Helper method which imports the given FileInputStream using Workspace or Session + * and via the methods importXML respective getImportContentHandler. The target node of the + * import is specified with its absolute path. + * + * @param absPath + * @param in + * @param useWorkspace + * @param useHandler + * @throws RepositoryException + * @throws IOException + */ + public void doImport(String absPath, FileInputStream in, boolean useWorkspace, boolean useHandler) + throws Exception { + if (useHandler) { + if (useWorkspace) { + ContentHandler ih = workspace.getImportContentHandler(absPath, 0); + createXMLReader(ih).parse(new InputSource(in)); + } else { + ContentHandler ih = session.getImportContentHandler(absPath, 0); + createXMLReader(ih).parse(new InputSource(in)); + session.save(); + } + } else { + if (useWorkspace) { + workspace.importXML(absPath, in, 0); + } else { + session.importXML(absPath, in, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + session.save(); + } + } + } + + public void doImportNoSave(String absPath, FileInputStream in, boolean useHandler) + throws Exception { + if (useHandler) { + ContentHandler ih = session.getImportContentHandler(absPath, 0); + createXMLReader(ih).parse(new InputSource(in)); + } else { + session.importXML(absPath, in, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + } + } +//------------< System view export import tests >----------------------------------- + + public void testExportSysView_stream_workspace_skipBinary_noRecurse() throws Exception { + doTest(STREAM, WORKSPACE, SKIPBINARY, NORECURSE); + } + + public void testExportSysView_stream_workspace_skipBinary_recurse() throws Exception { + doTest(STREAM, WORKSPACE, SKIPBINARY, RECURSE); + } + + public void testExportSysView_stream_workspace_saveBinary_noRecurse() throws Exception { + doTest(STREAM, WORKSPACE, SAVEBINARY, NORECURSE); + } + + public void testExportSysView_stream_workspace_saveBinary_recurse() throws Exception { + doTest(STREAM, WORKSPACE, SAVEBINARY, RECURSE); + } + + public void testExportSysView_stream_session_skipBinary_noRecurse() throws Exception { + doTest(STREAM, SESSION, SKIPBINARY, NORECURSE); + } + + public void testExportSysView_stream_session_skipBinary_recurse() throws Exception { + doTest(STREAM, SESSION, SKIPBINARY, RECURSE); + } + + public void testExportSysView_stream_session_saveBinary_noRecurse() throws Exception { + doTest(STREAM, SESSION, SAVEBINARY, NORECURSE); + } + + public void testExportSysView_stream_session_saveBinary_recurse() throws Exception { + doTest(STREAM, SESSION, SAVEBINARY, RECURSE); + } + + public void testExportSysView_handler_workspace_skipBinary_noRecurse() throws Exception { + doTest(CONTENTHANDLER, WORKSPACE, SKIPBINARY, NORECURSE); + } + + public void testExportSysView_handler_workspace_skipBinary_recurse() throws Exception { + doTest(CONTENTHANDLER, WORKSPACE, SKIPBINARY, RECURSE); + } + + public void testExportSysView_handler_workspace_saveBinary_noRecurse() throws Exception { + doTest(CONTENTHANDLER, WORKSPACE, SAVEBINARY, NORECURSE); + } + + public void testExportSysView_handler_workspace_saveBinary_recurse() throws Exception { + doTest(CONTENTHANDLER, WORKSPACE, SAVEBINARY, RECURSE); + } + + public void testExportSysView_handler_session_skipBinary_noRecurse() throws Exception { + doTest(CONTENTHANDLER, SESSION, SKIPBINARY, NORECURSE); + } + + public void testExportSysView_handler_session_skipBinary_recurse() throws Exception { + doTest(CONTENTHANDLER, SESSION, SKIPBINARY, RECURSE); + } + + public void testExportSysView_handler_session_saveBinary_noRecurse() throws Exception { + doTest(CONTENTHANDLER, SESSION, SAVEBINARY, NORECURSE); + } + + public void testExportSysView_handler_session_saveBinary_recurse() throws Exception { + doTest(CONTENTHANDLER, SESSION, SAVEBINARY, RECURSE); + } + + /** + * Exports the tree at source node, imports it at the traget node, and + * compares the source and target tree. + * + * @param handler true = use content handler for import, false = use the + * importXML method + * @param workspace true = import to the workspace, false = import to the + * session (export is from session anyway) + * @param skipBinary true = skip binary properties. The binary properties + * are omitted (without any replacement) + * @param noRecurse true = export only top node, false = export entire + * subtree + */ + private void doTest(boolean handler, boolean workspace, boolean skipBinary, boolean noRecurse) + throws Exception { + exportRepository(skipBinary, noRecurse); + importRepository(handler, workspace); + treeComparator.showTree(); + treeComparator.compare(skipBinary, noRecurse); + } + + /** + * Exports the repository to a temporary file using the system view + * serialization. + * + * @param skipBinary true = omit any binary properties (without any + * replacement) + * @param noRecurse true = save only top node, false = save entire subtree + * @throws IOException + */ + private void exportRepository(boolean skipBinary, boolean noRecurse) throws IOException { + FileOutputStream out = new FileOutputStream(file); + try { + session.refresh(false); //move the workspace into the session, then save it. The workspace is always valid, the session not necessarily. + session.exportSystemView(treeComparator.getSourceRootPath(), out, skipBinary, noRecurse); + } catch (RepositoryException e) { + fail("Could not export the repository: " + e); + } finally { + out.close(); + } + } + + /** + * Imports the repository + * + * @param useHandler True = use the import handler, false = use file input + * stream + * @param workspace True = import into workspace, false = import into + * session + */ + public void importRepository(boolean useHandler, boolean workspace) throws Exception { + FileInputStream in = new FileInputStream(file); + try { + if (useHandler) { + if (workspace) { + ContentHandler ih = this.workspace.getImportContentHandler(treeComparator.targetFolder, 0); + createXMLReader(ih).parse(new InputSource(in)); + } else { + ContentHandler ih = session.getImportContentHandler(treeComparator.targetFolder, + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + createXMLReader(ih).parse(new InputSource(in)); + session.save(); + } + } else { + if (workspace) { + this.workspace.importXML(treeComparator.targetFolder, in, 0); + } else { + session.importXML(treeComparator.targetFolder, in, + ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + session.save(); + } + } + } catch (SAXException e) { + fail("Error while parsing the imported repository: " + e); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + /** + * Creates an XMLReader for the given content handler. + * + * @param handler the content handler. + * @return an XMLReader for the given content handler. + * @throws SAXException if the reader cannot be created. + */ + private XMLReader createXMLReader(ContentHandler handler) throws SAXException { + XMLReader reader = XMLReaderFactory.createXMLReader(); + reader.setFeature("http://xml.org/sax/features/namespaces", true); + reader.setFeature("http://xml.org/sax/features/namespace-prefixes", false); + reader.setContentHandler(handler); + reader.setErrorHandler(new DefaultHandler()); + return reader; + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/SessionReadMethodsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionReadMethodsTest.java similarity index 78% rename from src/test/org/apache/jackrabbit/test/api/SessionReadMethodsTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionReadMethodsTest.java index aceb916a1c3..3220d862ad9 100644 --- a/src/test/org/apache/jackrabbit/test/api/SessionReadMethodsTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionReadMethodsTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -27,15 +27,9 @@ import javax.jcr.ItemNotFoundException; import javax.jcr.NodeIterator; -import java.security.AccessControlException; - /** * SessionReadMethodsTest... * - * @test - * @sources SessionReadMethodsTest.java - * @executeClass org.apache.jackrabbit.test.api.SessionReadMethodsTest - * @keywords level1 */ public class SessionReadMethodsTest extends AbstractJCRTest { @@ -52,7 +46,18 @@ public class SessionReadMethodsTest extends AbstractJCRTest { protected void setUp() throws Exception { isReadOnly = true; super.setUp(); - session = helper.getReadOnlySession(); + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null && session.isLive()) { + session.logout(); + session = null; + } + super.tearDown(); } /** @@ -119,6 +124,19 @@ public void testGetNodeByUUID() throws RepositoryException, NotExecutableExcepti referenced.isSame(node)); } + /** + * Tests session.getNodeByIdentifier() + * + * @since JCR 2.0 + */ + public void testGetNodeByIdentifier() throws RepositoryException, NotExecutableException { + String identifier = testRootNode.getIdentifier(); + Node node = session.getNodeByIdentifier(identifier); + assertTrue("Node retrieved with session.getNodeByIdentifier is not the same " + + "as the node having the given identifier.", + testRootNode.isSame(node)); + } + /** * Tests if getAttribute(String name) returns not null if the requested * attribute is existing @@ -163,6 +181,22 @@ public void testGetAttributeNames() { } } + /** + * Tests if isLive() returns true if the Session is usable by + * the client and false if it is not usable + */ + public void testIsLive() { + assertTrue("Method isLive() must return true if the session " + + "is usable by the client.", + session.isLive()); + + session.logout(); + assertFalse("Method isLive() must return false if the session " + + "is not usable by the client, e.g. if the session is " + + "logged-out.", + session.isLive()); + } + //----------------------< internal >---------------------------------------- /** @@ -207,4 +241,5 @@ private Node findReferenceable(Node node) throws RepositoryException { } return referenced; } + } diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionRemoveItemTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionRemoveItemTest.java new file mode 100644 index 00000000000..f716296c824 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionRemoveItemTest.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.test.api; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; + +/** SessionRemoveItemTest... */ +public class SessionRemoveItemTest extends AbstractJCRTest { + + private Session adminSession; + private Session readOnlySession; + + private Node removeNode; + private String nPath; + + protected void setUp() throws Exception { + super.setUp(); + + adminSession = superuser; + + readOnlySession = getHelper().getReadOnlySession(); + + removeNode = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + nPath = removeNode.getPath(); + } + + protected void tearDown() throws Exception { + if (readOnlySession != null) { + readOnlySession.logout(); + } + super.tearDown(); + } + + public void testRemoveItem() throws RepositoryException { + adminSession.removeItem(nPath); + assertFalse(adminSession.nodeExists(nPath)); + } + + public void testRemoveItem2() throws RepositoryException { + adminSession.removeItem(nPath); + try { + removeNode.getParent(); + fail("Cannot retrieve the parent from a transiently removed item."); + } catch (InvalidItemStateException e) { + // success + } + } + + public void testRemoveItem3() throws RepositoryException { + adminSession.removeItem(nPath); + readOnlySession.refresh(false); // see JCR-3302 + + // node must still exist for another session. + assertTrue(readOnlySession.nodeExists(nPath)); + } + + public void testRemoveItem4() throws RepositoryException { + try { + readOnlySession.refresh(false); // see JCR-3302 + + readOnlySession.removeItem(nPath); + readOnlySession.save(); + fail("A read-only session must not be allowed to remove an item"); + } catch (AccessDeniedException e) { + // success + } + } + + public void testRemoveLockedNode() throws RepositoryException, NotExecutableException { + ensureLockingSupported(); + ensureMixinType(removeNode, mixLockable); + removeNode.save(); + + // make sure the test node is locked. + removeNode.lock(true, true); + Session testSession = null; + try { + testSession = getHelper().getReadWriteSession(); + // removal of the locked node is a alteration of the parent, which + // isn't locked -> must succeed. + testSession.removeItem(nPath); + testSession.save(); + } finally { + if (testSession != null) { + testSession.logout(); + } + } + } + + public void testRemoveLockedChildItem() throws RepositoryException, NotExecutableException { + // add a child property and a child node to test deep lock effect. + Node childN = removeNode.addNode(nodeName2); + Value v = getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE2, RepositoryStub.PROP_PROP_TYPE2, "propvalue2"); + Property childP = removeNode.setProperty(propertyName2, v); + removeNode.save(); + + ensureMixinType(removeNode, mixLockable); + removeNode.save(); + + // make sure the test node is locked. + removeNode.lock(true, true); + Session testSession = null; + + try { + testSession = getHelper().getReadWriteSession(); + try { + testSession.removeItem(childN.getPath()); + testSession.save(); + fail("Locked child node cannot be removed by another session."); + } catch (LockException e) { + // success + } + try { + testSession.removeItem(childP.getPath()); + testSession.save(); + fail("Locked child node cannot be removed by another session."); + } catch (LockException e) { + // success + } + } finally { + if (testSession != null) { + testSession.logout(); + } + removeNode.unlock(); + } + } + + public void testRemoveCheckedInItem() throws RepositoryException, NotExecutableException { + // add a child property and a child node to test deep lock effect. + Node childN = removeNode.addNode(nodeName2); + Value v = getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE2, RepositoryStub.PROP_PROP_TYPE2, "propvalue2"); + Property childP = removeNode.setProperty(propertyName2, v); + removeNode.save(); + + ensureMixinType(removeNode, mixVersionable); + removeNode.save(); + + removeNode.checkin(); + try { + adminSession.removeItem(childP.getPath()); + adminSession.save(); + fail("child property of a checked-in node cannot be removed."); + } catch (VersionException e) { + // success + } + try { + adminSession.removeItem(childN.getPath()); + adminSession.save(); + fail("child node of a checked-in node cannot be removed."); + } catch (VersionException e) { + // success + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionTest.java new file mode 100644 index 00000000000..0779682c55e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionTest.java @@ -0,0 +1,701 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * SessionTest contains all test cases for the + * javax.jcr.Session class that are level 2 (modifing repository + * content). + * + */ +public class SessionTest extends AbstractJCRTest { + + /** + * Tries to move a node using {@link javax.jcr.Session#move(String src, String dest)} + * to a location where a node already exists with + * same name. + *

        + * Prerequisites: + *

          + *
        • javax.jcr.tck.SessionTest.testMoveItemExistsException.nodetype2 + * must contain name of a nodetype that does not allow same name sibling + * child nodes.
        • + *
        • javax.jcr.tck.SessionTest.testMoveItemExistsException.nodetype3 + * must contain name of a valid nodetype that can be added as a child of + * nodetype2
        • + *
        + *

        + * This should throw an {@link javax.jcr.ItemExistsException}. + */ + public void testMoveItemExistsException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create parent node + Node srcParentNode = defaultRootNode.addNode(nodeName1, testNodeType); + // create node to move + Node moveNode = srcParentNode.addNode(nodeName2, getProperty("nodetype3")); + + // create a second node that will serve as new parent, must use a nodetype that does not allow + // same name siblings + Node destParentNode = defaultRootNode.addNode(nodeName3, getProperty("nodetype2")); + // add a valid child + Node destNode = destParentNode.addNode(nodeName2, getProperty("nodetype3")); + + // save the new nodes + superuser.save(); + + try { + // move the node + superuser.move(moveNode.getPath(), destNode.getPath()); + fail("Moving a node using Session.move() to a location where a node with same name already exists must throw ItemExistsException"); + } catch (ItemExistsException e) { + // ok, works as expected + } + } + + /** + * Calls {@link javax.jcr.Session#move(String src, String dest)} + * with invalid destination path. + *

        + * Should throw a {@link javax.jcr.PathNotFoundException}. + */ + public void testMovePathNotFoundExceptionDestInvalid() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create parent node + Node srcParentNode = defaultRootNode.addNode(nodeName1, testNodeType); + // create node to move + Node moveNode = srcParentNode.addNode(nodeName2, testNodeType); + + // save the new nodes + superuser.save(); + + // move the node + try { + superuser.move(moveNode.getPath(), defaultRootNode.getPath() + "/" + nodeName2 + "/" + nodeName1); + fail("Invalid destination path during Session.move() must throw PathNotFoundException"); + } catch (PathNotFoundException e) { + // ok, works as expected + } + } + + /** + * Calls {@link javax.jcr.Session#move(String src, String dest)} with + * invalid source path. + *

        + * Should throw a {@link javax.jcr.PathNotFoundException}. + */ + public void testMovePathNotFoundExceptionSrcInvalid() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node that will serve as new parent + Node destParentNode = defaultRootNode.addNode(nodeName3, testNodeType); + + // save the new nodes + superuser.save(); + + // move the node + try { + superuser.move(defaultRootNode.getPath() + "/" + nodeName1, destParentNode.getPath() + "/" + nodeName2); + fail("Invalid source path during Session.move() must throw PathNotFoundException"); + } catch (PathNotFoundException e) { + // ok. works as expected + } + } + + /** + * Calls {@link javax.jcr.Session#move(String src, String dest)} + * with a destination path that has an index postfixed. + *

        + * This should throw a {@link javax.jcr.RepositoryException}. + */ + public void testMoveRepositoryException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create parent node + Node srcParentNode = defaultRootNode.addNode(nodeName1, testNodeType); + // create node to be moved + Node moveNode = srcParentNode.addNode(nodeName2, testNodeType); + + // create a node that will serve as new parent + Node destParentNode = defaultRootNode.addNode(nodeName3, testNodeType); + + // save the new nodes + superuser.save(); + + // move the node + try { + superuser.move(moveNode.getPath(), destParentNode.getPath() + "/" + nodeName2 + "[1]"); + fail("If destination path of Session.move() contains an index as postfix it must throw RepositoryException"); + } catch (RepositoryException e) { + // ok works as expected + } + } + + /** + * Moves a node using {@link javax.jcr.Session#move(String src, String dest)}, + * afterwards it tries to only save the old parent node. + *

        + * This should throw {@link javax.jcr.nodetype.ConstraintViolationException}. + *

        + * Prerequisites:

        • javax.jcr.tck.nodetype + * must accept children of same nodetype
        + */ + public void testMoveConstraintViolationExceptionSrc() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create parent node + Node srcParentNode = defaultRootNode.addNode(nodeName1, testNodeType); + // create node to be moved + Node moveNode = srcParentNode.addNode(nodeName2, testNodeType); + + // create a node that will serve as new parent + Node destParentNode = defaultRootNode.addNode(nodeName3, testNodeType); + + // save the new nodes + superuser.save(); + + // move the node + superuser.move(moveNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + + // save only old parent node + try { + srcParentNode.save(); + fail("Saving only the source parent node after a Session.move() operation must throw ConstraintViolationException"); + } catch (ConstraintViolationException e) { + // ok both work as expected + } + } + + /** + * Moves a node using {@link javax.jcr.Session#move(String src, String dest)}, + * afterwards it tries to only save the destination parent + * node. + *

        + * This should throw a {@link javax.jcr.nodetype.ConstraintViolationException}. + *

        + * Prerequisites:

        • javax.jcr.tck.nodetype + * must accept children of same nodetype
        + */ + public void testMoveConstraintViolationExceptionDest() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create parent node + Node srcParentNode = defaultRootNode.addNode(nodeName1, testNodeType); + // create node to be moved + Node moveNode = srcParentNode.addNode(nodeName2, testNodeType); + + // create a node that will serve as new parent + Node destParentNode = defaultRootNode.addNode(nodeName3, testNodeType); + + // save the new nodes + superuser.save(); + + // move the node + superuser.move(moveNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + + // save only moved node + try { + destParentNode.save(); + fail("Saving only moved node after a Session.move() operation should throw ConstraintViolationException"); + } catch (ConstraintViolationException e) { + // ok try to save the source + } + } + + /** + * Calls {@link javax.jcr.Session#move(String src, String dest)} where + * the parent node of src is locked. + *

        + * Should throw a {@link LockException} immediately or on save. + */ + public void testMoveLockException() + throws NotExecutableException, RepositoryException { + + Session session = superuser; + + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Locking is not supported."); + } + + // create a node that is lockable + Node lockableNode = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it lockable if it is not + ensureMixinType(lockableNode, mixLockable); + + // add a sub node (the one that is tried to move later on) + Node srcNode = lockableNode.addNode(nodeName1, testNodeType); + + testRootNode.getSession().save(); + + // remove first slash of path to get rel path to root + String pathRelToRoot = lockableNode.getPath().substring(1); + + // access node through another session to lock it + Session session2 = getHelper().getSuperuserSession(); + try { + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.lock(true, true); + + try { + String destPath = testRoot + "/" + nodeName2; + session.move(srcNode.getPath(), destPath); + testRootNode.getSession().save(); + fail("A LockException is thrown either immediately or on save if a lock prevents the move."); + } catch (LockException e){ + // success + } + + } finally { + session2.logout(); + } + } + + /** + * Checks if {@link javax.jcr.Session#move(String src, String dest)} + * works properly. To verify if node has been moved properly + * it uses a second session to retrieve the moved node. + *

        + * Prerequisites:

        • javax.jcr.tck.nodetype + * must accept children of same nodetype
        + */ + public void testMoveNode() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create parent node + Node srcParentNode = defaultRootNode.addNode(nodeName1, testNodeType); + // create node to be moved + Node moveNode = srcParentNode.addNode(nodeName2, testNodeType); + + // create a node that will serve as new parent + Node destParentNode = defaultRootNode.addNode(nodeName3, testNodeType); + + // save the new nodes + superuser.save(); + + //move the nodes + superuser.move(moveNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + + superuser.save(); + + // get moved tree root node with session 2 + Session testSession = getHelper().getReadWriteSession(); + try { + testSession.getItem(destParentNode.getPath() + "/" + nodeName2); + // node found + } finally { + testSession.logout(); + } + } + + /** + * Checks if a newly created node gets properly saved using {@link + * javax.jcr.Session#save()}. + *

        + * It creates a new node, saves + * it using session.save() then uses a different session to + * verify if the node has been properly saved. + */ + public void testSaveNewNode() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node newNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save changes + superuser.save(); + + // use a different session to verify if the node is there + Session s = getHelper().getReadOnlySession(); + try { + s.getItem(newNode.getPath()); + // throws PathNotFoundException if item was not saved + } finally { + s.logout(); + } + } + + /** + * Checks if a modified node gets properly saved using {@link + * javax.jcr.Session#save()}. + *

        + * It creates a new node, saves + * it using session.save(), modifies the node by adding a child + * node, saves again and finally verifies with a different session if + * changes have been stored properly. + *

        + * Prerequisites:

          + *
        • javax.jcr.tck.nodetype must accept children of same + * nodetype
        + */ + public void testSaveModifiedNode() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node newNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // save new node + superuser.save(); + + // and add a child node + newNode.addNode(nodeName2, testNodeType); + + // save the changes + superuser.save(); + + // check if the child node was created properly + + // get a reference with a second session to the modified node + Session s = getHelper().getReadOnlySession(); + try { + Node newNodeSession2 = (Node) s.getItem(newNode.getPath()); + // check if child is there + assertTrue("Modifications on a node are not save after Session.save()", newNodeSession2.hasNode(nodeName2)); + } finally { + s.logout(); + } + } + + /** + * Tries to create and save a node using {@link javax.jcr.Session#save()} + * with an mandatory property that is not set on saving time. + *

        + * Prerequisites:

        • javax.jcr.tck.SessionTest.testSaveConstraintViolationException.nodetype2 + * must reference a nodetype that has one at least one property that is + * mandatory but not autocreated
        + */ + public void testSaveConstraintViolationException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node with at least one mandatory, not autocreated property + defaultRootNode.addNode(nodeName1, getProperty("nodetype2")); + + // save changes + try { + superuser.save(); + fail("Trying to use Session.save() with a node that has a mandatory property not set, should throw ConstraintViolationException"); + } catch (ConstraintViolationException e) { + // ok + } + } + + /** + * Tries to save a node using {@link javax.jcr.Session#save()} that was + * already deleted by an other session. + *

        + * Procedure:

          + *
        • Creates a new node with session 1, saves it, adds a child node.
        • + *
        • Access new node with session 2,deletes the node, saves it.
        • + *
        • session 1 tries to save modifications .
        This should throw + * an {@link javax.jcr.InvalidItemStateException}. + *

        + * Prerequisites: + *

        • javax.jcr.tck.nodetype must accept children of same + * nodetype
        + */ + public void testSaveInvalidStateException() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node nodeSession1 = defaultRootNode.addNode(nodeName1, testNodeType); + + // save new node + superuser.save(); + + // make a modification + nodeSession1.addNode(nodeName2, testNodeType); + + // get the new node with a different session + Session testSession = getHelper().getReadWriteSession(); + try { + Node nodeSession2 = (Node) testSession.getItem(nodeSession1.getPath()); + + // delete the node with the new session + nodeSession2.remove(); + + // make node removal persistent + testSession.save(); + + // save changes made with superuser session + try { + superuser.save(); + fail("Saving a modified Node using Session.save() already deleted by an other session should throw InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // ok, works as expected + } + } finally { + testSession.logout(); + } + } + + /** + * Checks if {@link javax.jcr.Session#refresh(boolean refresh)} works + * properly with refresh set to false. + *

        + * Procedure:

        • Creates two nodes with session 1
        • Modifies + * node 1 with session 1 by adding a child node
        • Get node 2 with + * session 2
        • Modifies node 2 with session 2 by adding a child + * node
        • saves session 2 changes using {@link + * javax.jcr.Session#save()}
        • calls Session.refresh(false) + * on session 1
        Session 1 changes should be cleared and session 2 + * changes should now be visible to session 1. + *

        + * Prerequisites:

          + *
        • javax.jcr.tck.nodetype must accept children of same + * nodetype
        + */ + public void testRefreshBooleanFalse() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode1Session1 = defaultRootNode.addNode(nodeName1, testNodeType); + // create a second node + Node testNode2Session1 = defaultRootNode.addNode(nodeName2, testNodeType); + + // save the new nodes + superuser.save(); + + // add child node to test node 1 using session 1 + testNode1Session1.addNode(nodeName2, testNodeType); + + // get session 2 + Session session2 = getHelper().getReadWriteSession(); + + try { + // get the second node + Node testNode2Session2 = (Node) session2.getItem(testNode2Session1.getPath()); + + // adds a child node + testNode2Session2.addNode(nodeName3, testNodeType); + + // save the changes + session2.save(); + + // call refresh on session 1 + superuser.refresh(false); + + // check if session 1 flag has been cleared + assertFalse("Session should have no pending changes recorded after Session.refresh(false)!", superuser.hasPendingChanges()); + + // check if added child node for node 1 by session 1 has been removed + assertFalse("Node Modifications have not been flushed after session.refresh(false)", testNode1Session1.hasNodes()); + + // check if added child node for node 2 by session 2 has become visible in session 1 + assertTrue("Node modified by a different session has not been updated after Session.refresh(false)", testNode2Session1.hasNodes()); + } finally { + session2.logout(); + } + } + + /** + * Checks if {@link javax.jcr.Session#refresh(boolean refresh)} works + * properly with refresh set to true. + *

        + * Procedure:

        • Creates two nodes with session 1
        • Modifies + * node 1 with session 1 by adding a child node
        • Get node 2 with + * session 2
        • Modifies node 2 with session 2 by adding a child + * node
        • saves session 2 changes using {@link + * javax.jcr.Session#save()}
        • calls Session.refresh(true) + * on session 1
        Session 1 changes and session 2 changes now be + * visible to session 1. + *

        + * Prerequisites:

          + *
        • javax.jcr.tck.nodetype must accept children of same + * nodetype
        + */ + public void testRefreshBooleanTrue() throws RepositoryException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node testNode1Session1 = defaultRootNode.addNode(nodeName1, testNodeType); + // create a second node + Node testNode2Session1 = defaultRootNode.addNode(nodeName2, testNodeType); + + // save the new nodes + superuser.save(); + + // add child node to test node 1 using session 1 + testNode1Session1.addNode(nodeName2, testNodeType); + + // get session 2 + Session session2 = getHelper().getReadWriteSession(); + + try { + // get the second node + Node testNode2Session2 = (Node) session2.getItem(testNode2Session1.getPath()); + + // adds a child node + testNode2Session2.addNode(nodeName3, testNodeType); + + // save the changes + session2.save(); + + // call refresh on session 1 + superuser.refresh(true); + + // check if session 1 flag has been cleared + assertTrue("Session should still have pending changes recorded after Session.refresh(true)!", superuser.hasPendingChanges()); + + // check if added child node for node 1 by session 1 is still there + assertTrue("Node Modifications are lost after session.refresh(true)", testNode1Session1.hasNodes()); + + // check if added child node for node 2 by session 2 has become visible in session 1 + assertTrue("Node modified by a different session has not been updated after Session.refresh(true)", testNode2Session1.hasNodes()); + } finally { + session2.logout(); + } + } + + /** + * Checks if {@link javax.jcr.Session#hasPendingChanges()} works + * properly. + *

        + * Procedure: + *

        • Gets a session, checks + * inital flag setting
        • Adds a node, checks flag
        • Saves on + * session, checks flag
        • Adds a property, checks flag
        • Saves + * on session, checks flag
        • Adds a child node, checks flag
        • + *
        • Saves on session, checks flag
        • Removes child node, checks + * flag
        • Saves on session, checks flag
        • Removes property, + * checks flag
        • Saves on session, checks flag
        + * Prerequisites:
        • javax.jcr.tck.nodetype must accept + * children of same nodetype
        • javax.jcr.tck.propertyname1 + * must be the name of a String property that can be added to a node of type + * set in javax.jcr.tck.nodetype
        + */ + public void testHasPendingChanges() throws RepositoryException { + + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // initial check if flag is set correctly + assertFalse("Session should have no pending changes recorded!", superuser.hasPendingChanges()); + + // test with adding a node + Node testNode1 = defaultRootNode.addNode(nodeName1, testNodeType); + assertTrue("Session should have pending changes recorded after new node was added!", superuser.hasPendingChanges()); + + // save the node + superuser.save(); + // pending changes should have been cleared + assertFalse("Session should have no pending changes recorded after new node was added and saved!", superuser.hasPendingChanges()); + + // adds a property + testNode1.setProperty(propertyName1, "test"); + assertTrue("Session should have pending changes recorded after a property was added!", superuser.hasPendingChanges()); + + // save the new prop + superuser.save(); + // pending changes should have been cleared + assertFalse("Session should have no pending changes recorded after added property hase been saved!", superuser.hasPendingChanges()); + + // add child node + Node testChildNode = testNode1.addNode(nodeName1, testNodeType); + + assertTrue("Session should have pending changes recorded after child node has been added!", superuser.hasPendingChanges()); + + // save the new child nodes + superuser.save(); + + // pending changes should have been cleared + assertFalse("Session should have no pending changes recorded after new child node has been added and saved!", superuser.hasPendingChanges()); + + + // remove the child node + testChildNode.remove(); + assertTrue("Session should have pending changes recorded after child node has been removed", superuser.hasPendingChanges()); + + // save the change + superuser.save(); + + // pending changes should have been cleared + assertFalse("Session should have no pending changes recorded after child node has been removed and saved!", superuser.hasPendingChanges()); + + // remove the property + testNode1.setProperty(propertyName1, (Value) null); + assertTrue("Session should have pending changes recorded after property has been removed", superuser.hasPendingChanges()); + + // save the change + superuser.save(); + + // pending changes should have been cleared + assertFalse("Session should have no pending changes recorded after property has been removed and saved!", superuser.hasPendingChanges()); + + } + + /** + * Checks if {@link javax.jcr.Session#hasCapability(String, Object, Object[])} + * works as specified. + *

        + * + * @throws RepositoryException + */ + public void testHasCapability() throws RepositoryException { + Session roSession = getHelper().getReadOnlySession(); + try { + Node testRoot = roSession.getNode(testRootNode.getPath()); + Object[] args = new Object[] { "foo" }; + if (!roSession.hasCapability("addNode", testRoot, args)) { + // if hasCapability() returns false, the actual method call + // is expected to fail + try { + testRoot.addNode("foo"); + roSession.save(); + fail("Node.addNode() should fail according to Session.hasCapability()"); + } catch (RepositoryException e) { + // expected + } + } else { + // hasCapability() returning true doesn't guarantee that the + // actual method call succeeds, it's just a best-effort. + // therefore nothing to test here... + } + + } finally { + roSession.logout(); + } + + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionUUIDTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionUUIDTest.java new file mode 100644 index 00000000000..cbd4c0914ba --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SessionUUIDTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.Session; +import javax.jcr.InvalidItemStateException; + +/** + * SessionUUIDTest contains all tests for the {@link javax.jcr.Session} + * class that require a UUID (and therefore are optional). + * + */ +public class SessionUUIDTest extends AbstractJCRTest { + + /** + * Tries to remove a node that is a reference target using {@link Session#save()}. + *

        + * Procedure: + *

          + *
        • Creates two nodes with same session
        • + *
        • One has a referencing property pointing to the other node
        • + *
        • Target node gets removed
        • + *
        + *

        + * This should generate a {@link javax.jcr.ReferentialIntegrityException} upon save. + *

        + * Prerequisites: + *

          + *
        • javax.jcr.tck.SessionUUIDTest.nodetype must allow a property of type {@link javax.jcr.PropertyType#REFERENCE}
        • + *
        • javax.jcr.tck.SessionUUIDTest.propertyname1 name of the property of type {@link javax.jcr.PropertyType#REFERENCE}
        • + *
        • javax.jcr.tck.SessionUUIDTest.nodetype2 must have the mixin type mix:referenceable assigned.
        • + *
        + */ + public void testSaveReferentialIntegrityException() throws RepositoryException, NotExecutableException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node with a property of type PropertyType.REFERENCE + Node referencingNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // create a node with a jcr:uuid property to serve as target + Node refTargetNode = defaultRootNode.addNode(nodeName2, getProperty("nodetype2")); + // implementations may only have the mix:referenceable active upon save + defaultRootNode.save(); + + if (!refTargetNode.isNodeType(mixReferenceable)) { + throw new NotExecutableException("Cannot test referential integrity. Node is not referenceable."); + } + + // abort test if the repository does not allow setting + // reference properties on this node + ensureCanSetProperty(referencingNode, propertyName1, referencingNode.getSession().getValueFactory().createValue(refTargetNode)); + + // set the reference + referencingNode.setProperty(propertyName1, refTargetNode); + + // save the new nodes + superuser.save(); + + // remove the referenced node + refTargetNode.remove(); + + // try to save + try { + superuser.save(); + fail("Saving a deleted node using Session.save() that is a reference target should throw ReferentialIntegrityException"); + } catch (ReferentialIntegrityException e) { + // ok, works as expected + } + } + + /** + * Moves a referencable node using {@link javax.jcr.Session#move(String, String)} with one session and + * saves afterward changes made with a second session to the moved node using {@link Session#save()}. + *

        + * Procedure: + *

          + *
        • Creates node 1 and node 2 with session 1
        • + *
        • Gets reference to node 1 using session 2
        • + *
        • Session 1 moves node 1 under node 2, saves changes
        • + *
        • Session 2 modifes node 1, saves
        • + *
        + * This should work (since the modified node is identified by its UUID, not by position in repository) + * or throw an InvalidItemStateException if 'move' is reported + * to the second session as a sequence of remove and add events. + *

        Prerequisites: + *
          + *
        • javax.jcr.tck.SessionUUIDTest.nodetype2 must have the mixin type mix:referenceable assigned.
        • + *
        • javax.jcr.tck.SessionUUIDTest.testSaveMovedRefNode.propertyname1 + * name of a property that can be modified in nodetype2 for testing
        • + *
        + */ + public void testSaveMovedRefNode() throws RepositoryException, NotExecutableException { + // get default workspace test root node using superuser session + Node defaultRootNode = (Node) superuser.getItem(testRootNode.getPath()); + + // create a node + Node newParentNode = defaultRootNode.addNode(nodeName1, testNodeType); + + // create a referenceable node + Node refTargetNode = defaultRootNode.addNode(nodeName2, getProperty("nodetype2")); + + // save the new nodes + superuser.save(); + + if (!refTargetNode.isNodeType(mixReferenceable)) { + throw new NotExecutableException("Cannot test referential integrity. Node is not referenceable."); + } + + // get the moving node with session 2 + Session testSession = getHelper().getReadWriteSession(); + + try { + Node refTargetNodeSession2 = (Node) testSession.getItem(refTargetNode.getPath()); + + // move the node with session 1 + superuser.move(refTargetNode.getPath(), newParentNode.getPath() + "/" + nodeName2); + + // make the move persistent with session 1 + superuser.save(); + + try { + // modify some prop of the moved node with session 2 + refTargetNodeSession2.setProperty(propertyName1, "test"); + // save it + testSession.save(); + // ok, works as expected + } catch (InvalidItemStateException e) { + // ok as well. + } + } finally { + testSession.logout(); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyAssumeTypeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyAssumeTypeTest.java new file mode 100644 index 00000000000..831f2fbedf4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyAssumeTypeTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.ISO8601; +import org.apache.jackrabbit.test.api.nodetype.NodeTypeUtil; + +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.Node; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.util.Date; + +/** + * SetPropertyAssumeTypeTest tests if when setting a property + * of type PropertyType.UNDEFINED the type is assumed correctly. + * The signatures Node.setProperty(String, Value, int), + * Node.setProperty(String, String, int), + * Node.setProperty(String, Value[], int) and + * Node.setProperty(String, Node) are tested. + * + */ +public class SetPropertyAssumeTypeTest extends AbstractJCRTest { + + private Node testNode; + private String testPropName; + private Value binaryValue; + private Value booleanValue; + private Value dateValue; + private Value doubleValue; + private Value longValue; + private Value nameValue; + private Value pathValue; + private Value stringValue; + private Value binaryValues[]; + private Value booleanValues[]; + private Value dateValues[]; + private Value doubleValues[]; + private Value longValues[]; + private Value nameValues[]; + private Value pathValues[]; + private Value stringValues[]; + + public void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1, testNodeType); + + binaryValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BINARY); + booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN); + dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE); + longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG); + nameValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME); + pathValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.PATH); + stringValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.STRING); + + binaryValues = new Value[] {binaryValue}; + booleanValues = new Value[] {booleanValue}; + dateValues = new Value[] {dateValue}; + doubleValues = new Value[] {doubleValue}; + longValues = new Value[] {longValue}; + nameValues = new Value[] {nameValue}; + pathValues = new Value[] {pathValue}; + stringValues = new Value[] {stringValue}; + } + + protected void tearDown() throws Exception { + testNode = null; + testPropName = null; + binaryValue = null; + booleanValue = null; + dateValue = null; + doubleValue = null; + longValue = null; + nameValue = null; + pathValue = null; + stringValue = null; + binaryValues = null; + booleanValues = null; + dateValues = null; + doubleValues = null; + longValues = null; + nameValues = null; + pathValues = null; + stringValues = null; + super.tearDown(); + } + + /** + * Tests if Node.setProperty(String, Value, int) if the node + * type of this node does not indicate a specific property type, then the + * type parameter is used. + */ + public void testValue() throws NotExecutableException, RepositoryException { + + setUpNodeWithUndefinedProperty(false); + + Property prop; + + prop = testNode.setProperty(testPropName, binaryValue, PropertyType.BINARY); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.BINARY, + prop.getType()); + + prop = testNode.setProperty(testPropName, stringValue, PropertyType.BOOLEAN); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.BOOLEAN, + prop.getType()); + + prop = testNode.setProperty(testPropName, doubleValue, PropertyType.DATE); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.DATE, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValue, PropertyType.DOUBLE); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.DOUBLE, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValue, PropertyType.LONG); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.LONG, + prop.getType()); + + // create a PathValue that is convertible to the value of name property + Value valueConvertibleToName = superuser.getValueFactory().createValue(nameValue.getString(), PropertyType.PATH); + prop = testNode.setProperty(testPropName, valueConvertibleToName, PropertyType.NAME); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.NAME, + prop.getType()); + + prop = testNode.setProperty(testPropName, nameValue, PropertyType.PATH); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.PATH, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValue, PropertyType.STRING); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.STRING, + prop.getType()); + } + + /** + * Tests if Node.setProperty(String, Value[], int) if the node + * type of this node does not indicate a specific property type, then the + * type parameter is used. + */ + public void testValues() throws NotExecutableException, RepositoryException { + + setUpNodeWithUndefinedProperty(true); + + Property prop; + + prop = testNode.setProperty(testPropName, binaryValues, PropertyType.BINARY); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.BINARY, + prop.getType()); + + prop = testNode.setProperty(testPropName, stringValues, PropertyType.BOOLEAN); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.BOOLEAN, + prop.getType()); + + prop = testNode.setProperty(testPropName, doubleValues, PropertyType.DATE); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.DATE, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValues, PropertyType.DOUBLE); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.DOUBLE, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValues, PropertyType.LONG); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.LONG, + prop.getType()); + + // create a PathValue that is convertible to the value of name property + Value valuesConvertibleToName[] = + new Value[] {superuser.getValueFactory().createValue(nameValue.getString(), PropertyType.PATH)}; + prop = testNode.setProperty(testPropName, valuesConvertibleToName, PropertyType.NAME); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.NAME, + prop.getType()); + + prop = testNode.setProperty(testPropName, nameValues, PropertyType.PATH); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.PATH, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValues, PropertyType.STRING); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.STRING, + prop.getType()); + } + + /** + * Tests if Node.setProperty(String, String, int) if the node + * type of this node does not indicate a specific property type, then the + * type parameter is used. + */ + public void testString() throws NotExecutableException, RepositoryException { + + setUpNodeWithUndefinedProperty(false); + + Property prop; + + prop = testNode.setProperty(testPropName, binaryValue.getString(), PropertyType.BINARY); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.BINARY, + prop.getType()); + + prop = testNode.setProperty(testPropName, booleanValue.getString(), PropertyType.BOOLEAN); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.BOOLEAN, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValue.getString(), PropertyType.DATE); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.DATE, + prop.getType()); + + prop = testNode.setProperty(testPropName, doubleValue.getString(), PropertyType.DOUBLE); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.DOUBLE, + prop.getType()); + + prop = testNode.setProperty(testPropName, longValue.getString(), PropertyType.LONG); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.LONG, + prop.getType()); + + prop = testNode.setProperty(testPropName, nameValue.getString(), PropertyType.NAME); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.NAME, + prop.getType()); + + prop = testNode.setProperty(testPropName, pathValue.getString(), PropertyType.PATH); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.PATH, + prop.getType()); + + prop = testNode.setProperty(testPropName, stringValue.getString(), PropertyType.STRING); + assertEquals("setProperty(String, Value, int) of a property of type undefined " + + "must assume the property type of the type parameter.", + PropertyType.STRING, + prop.getType()); + } + + /** + * Tests if Node.setProperty(String, Value) if the node type of + * this node does not indicate a specific property type, then the property + * type of the supplied Value object is used and if the property already + * exists (has previously been set) it assumes the new property type. + */ + public void testValueAssumeTypeOfValue() throws NotExecutableException, RepositoryException { + + setUpNodeWithUndefinedProperty(false); + + Property prop; + + prop = testNode.setProperty(testPropName, binaryValue); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.BINARY, + prop.getType()); + + prop = testNode.setProperty(testPropName, booleanValue); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.BOOLEAN, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValue); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.DATE, + prop.getType()); + + prop = testNode.setProperty(testPropName, doubleValue); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.DOUBLE, + prop.getType()); + + prop = testNode.setProperty(testPropName, longValue); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.LONG, + prop.getType()); + + prop = testNode.setProperty(testPropName, nameValue); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.NAME, + prop.getType()); + + prop = testNode.setProperty(testPropName, pathValue); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.PATH, + prop.getType()); + + prop = testNode.setProperty(testPropName, stringValue); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.STRING, + prop.getType()); + } + + /** + * Tests if Node.setProperty(String, Node) if the node type of + * this node does not indicate a specific property type, then the property + * type of the supplied Value object is used and if the property already + * exists (has previously been set) it assumes the new property type. + */ + public void testNodeAssumeTypeOfValue() + throws NotExecutableException, RepositoryException { + + setUpNodeWithUndefinedProperty(false); + + Node referenceableNode = testRootNode.addNode(nodeName2); + ensureMixinType(referenceableNode, mixReferenceable); + + // some implementations may require a save after addMixin() + testRootNode.getSession().save(); + + Property prop = testNode.setProperty(testPropName, referenceableNode); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.REFERENCE, + prop.getType()); + } + + /** + * Tests if Node.setProperty(String, Value[]) if the node type of + * this node does not indicate a specific property type, then the property + * type of the supplied Value object is used and if the property already + * exists (has previously been set) it assumes the new property type. + */ + public void testValuesAssumeTypeOfValue() throws NotExecutableException, RepositoryException { + + setUpNodeWithUndefinedProperty(true); + + Property prop; + + prop = testNode.setProperty(testPropName, binaryValues); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.BINARY, + prop.getType()); + + prop = testNode.setProperty(testPropName, booleanValues); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.BOOLEAN, + prop.getType()); + + prop = testNode.setProperty(testPropName, dateValues); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.DATE, + prop.getType()); + + prop = testNode.setProperty(testPropName, doubleValues); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.DOUBLE, + prop.getType()); + + prop = testNode.setProperty(testPropName, longValues); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.LONG, + prop.getType()); + + prop = testNode.setProperty(testPropName, nameValues); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.NAME, + prop.getType()); + + prop = testNode.setProperty(testPropName, pathValues); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.PATH, + prop.getType()); + + prop = testNode.setProperty(testPropName, stringValues); + assertEquals("setProperty(String, Value) of a property of type undefined " + + "must assume the property type of the supplied value object.", + PropertyType.STRING, + prop.getType()); + } + + /** + * Tests if Node.setProperty(String, Value, int) throws a + * ConstraintViolationException if the type parameter and the type of the + * property do not match. The exception has to be thrown either immediately + * (by this method) or on save. + */ + public void testValueConstraintViolationExceptionBecauseOfInvalidTypeParameter() + throws NotExecutableException, RepositoryException { + + try { + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date(0)); + Value v = superuser.getValueFactory().createValue(ISO8601.format(cal)); + testNode.setProperty(propertyName1, v, PropertyType.DATE); + testRootNode.getSession().save(); + fail("Node.setProperty(String, Value, int) must throw a " + + "ConstraintViolationExcpetion if the type parameter and the " + + "type of the property do not match." ); + } + catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if Node.setProperty(String, String, int) throws a + * ConstraintViolationException if the type parameter and the type of the + * property do not match. The exception has to be thrown either immediately + * (by this method) or on save. + */ + public void testStringConstraintViolationExceptionBecauseOfInvalidTypeParameter() + throws NotExecutableException, RepositoryException { + + try { + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date(0)); + testNode.setProperty(propertyName1, ISO8601.format(cal), PropertyType.DATE); + testRootNode.getSession().save(); + fail("Node.setProperty(String, Value, int) must throw a " + + "ConstraintViolationExcpetion if the type parameter and the " + + "type of the property do not match." ); + } + catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if Node.setProperty(String, Value[], int) throws a + * ConstraintViolationException or ValueFormatException if the type + * parameter and the type of the property do not match. The exception has to + * be thrown either immediately (by this method) or on save. + */ + public void testValuesConstraintViolationExceptionBecauseOfInvalidTypeParameter() + throws NotExecutableException, RepositoryException { + + try { + testNode.setProperty(propertyName1, stringValues, PropertyType.DATE); + testRootNode.getSession().save(); + fail("Node.setProperty(String, Value, int) must throw a " + + "ConstraintViolationExcpetion or a ValueFormatException if " + + "the type parameter and the type of the property do not match."); + } catch (ConstraintViolationException e) { + // success + } catch (ValueFormatException e) { + // success + } + } + + //--------------------------< internal >------------------------------------ + + private void setUpNodeWithUndefinedProperty(boolean multiple) + throws NotExecutableException { + + try { + // locate a property definition of type undefined + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.UNDEFINED, multiple, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No testable property of type " + + "UNDEFINED has been found."); + } + + // create a node of type propDef.getDeclaringNodeType() + String nodeType = propDef.getDeclaringNodeType().getName(); + testNode = testRootNode.addNode(nodeName1, nodeType); + testPropName = propDef.getName(); + } + catch (RepositoryException e) { + throw new NotExecutableException("Not able to set up test items."); + } + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/SetPropertyBooleanTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyBooleanTest.java similarity index 84% rename from src/test/org/apache/jackrabbit/test/api/SetPropertyBooleanTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyBooleanTest.java index 0663f9a203f..34bf7457bea 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetPropertyBooleanTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyBooleanTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -25,10 +25,6 @@ * SetPropertyBooleanTest tests the Node.setProperty(String, * boolean) method * - * @test - * @sources SetPropertyBooleanTest.java - * @executeClass org.apache.jackrabbit.test.api.SetPropertyBooleanTest - * @keywords level2 */ public class SetPropertyBooleanTest extends AbstractJCRTest { @@ -39,6 +35,11 @@ protected void setUp() throws Exception { testNode = testRootNode.addNode(nodeName1, testNodeType); } + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + /** * Tests if adding a property with Node.setProperty(String, * boolean) works with Session.save() @@ -71,7 +72,7 @@ public void testModifyBooleanPropertySession() throws Exception { */ public void testNewBooleanPropertyParent() throws Exception { testNode.setProperty(propertyName1, true); - testRootNode.save(); + testRootNode.getSession().save(); assertEquals("Setting property with Node.setProperty(String, boolean) and parentNode.save() not working", true, testNode.getProperty(propertyName1).getBoolean()); @@ -83,9 +84,9 @@ public void testNewBooleanPropertyParent() throws Exception { */ public void testModifyBooleanPropertyParent() throws Exception { testNode.setProperty(propertyName1, true); - testRootNode.save(); + testRootNode.getSession().save(); testNode.setProperty(propertyName1, false); - testRootNode.save(); + testRootNode.getSession().save(); assertEquals("Modifying property with Node.setProperty(String, boolean) and parentNode.save() not working", false, testNode.getProperty(propertyName1).getBoolean()); @@ -112,11 +113,11 @@ public void testRemoveBooleanPropertySession() throws Exception { */ public void testRemoveBooleanPropertyParent() throws Exception { testNode.setProperty(propertyName1, true); - testRootNode.save(); + testRootNode.getSession().save(); testNode.setProperty(propertyName1, (Value) null); - testRootNode.save(); + testRootNode.getSession().save(); assertFalse("Removing boolean property with Node.setProperty(String, null) and parentNode.save() not working", testNode.hasProperty(propertyName1)); } -} \ No newline at end of file +} diff --git a/src/test/org/apache/jackrabbit/test/api/SetPropertyCalendarTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyCalendarTest.java similarity index 77% rename from src/test/org/apache/jackrabbit/test/api/SetPropertyCalendarTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyCalendarTest.java index f0dea28fc2c..a97f885367d 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetPropertyCalendarTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyCalendarTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -27,10 +27,6 @@ * SetPropertyCalendarTest tests the Node.setProperty(String, * Calendar) method * - * @test - * @sources SetPropertyCalendarTest.java - * @executeClass org.apache.jackrabbit.test.api.SetPropertyCalendarTest - * @keywords level2 */ public class SetPropertyCalendarTest extends AbstractJCRTest { @@ -44,6 +40,11 @@ protected void setUp() throws Exception { testNode = testRootNode.addNode(nodeName1, testNodeType); } + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + /** * Tests if adding a property with Node.setProperty(String, * Calendar) works with Session.save() @@ -52,8 +53,8 @@ public void testNewCalendarPropertySession() throws Exception { testNode.setProperty(propertyName1, c1); superuser.save(); assertEquals("Setting property with Node.setProperty(String, Calendar) and Session.save() not working", - c1, - testNode.getProperty(propertyName1).getDate()); + vf.createValue(c1), + testNode.getProperty(propertyName1).getValue()); } /** @@ -66,8 +67,8 @@ public void testModifyCalendarPropertySession() throws Exception { testNode.setProperty(propertyName1, c2); superuser.save(); assertEquals("Modifying property with Node.setProperty(String, Calendar) and Session.save() not working", - c2, - testNode.getProperty(propertyName1).getDate()); + vf.createValue(c2), + testNode.getProperty(propertyName1).getValue()); } /** @@ -76,10 +77,10 @@ public void testModifyCalendarPropertySession() throws Exception { */ public void testNewCalendarPropertyParent() throws Exception { testNode.setProperty(propertyName1, c1); - testRootNode.save(); + testRootNode.getSession().save(); assertEquals("Setting property with Node.setProperty(String, Calendar) and parentNode.save() not working", - c1, - testNode.getProperty(propertyName1).getDate()); + vf.createValue(c1), + testNode.getProperty(propertyName1).getValue()); } /** @@ -88,12 +89,12 @@ public void testNewCalendarPropertyParent() throws Exception { */ public void testModifyCalendarPropertyParent() throws Exception { testNode.setProperty(propertyName1, c1); - testRootNode.save(); + testRootNode.getSession().save(); testNode.setProperty(propertyName1, c2); - testRootNode.save(); + testRootNode.getSession().save(); assertEquals("Modifying property with Node.setProperty(String, Calendar) and parentNode.save() not working", - c2, - testNode.getProperty(propertyName1).getDate()); + vf.createValue(c2), + testNode.getProperty(propertyName1).getValue()); } /** @@ -117,11 +118,11 @@ public void testRemoveCalendarPropertySession() throws Exception { */ public void testRemoveCalendarPropertyParent() throws Exception { testNode.setProperty(propertyName1, c1); - testRootNode.save(); + testRootNode.getSession().save(); testNode.setProperty(propertyName1, (Calendar) null); - testRootNode.save(); + testRootNode.getSession().save(); assertFalse("Removing property with Node.setProperty(String, (Calendar)null) and parentNode.save() not working", testNode.hasProperty(propertyName1)); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyConstraintViolationExceptionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyConstraintViolationExceptionTest.java new file mode 100644 index 00000000000..dd6101e23bc --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyConstraintViolationExceptionTest.java @@ -0,0 +1,433 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.nodetype.NodeTypeUtil; + +/** + * SetPropertyConstraintViolationExceptionTest tests if + * setProperty() throws a ConstraintViolationException either immediately (by + * setValue()) or on save, if the change would violate a value constraint. + * + */ +public class SetPropertyConstraintViolationExceptionTest extends AbstractJCRTest { + + /** + * Tests if setProperty(String name, boolean value) and setProperty(String + * name, Value value) where value is a BooleanValue throw a + * ConstraintViolationException either immediately (by setProperty()), or on + * save, if the change would violate a node type constraint + */ + public void testBooleanProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.BOOLEAN, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No boolean property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No boolean property def with " + + "testable value constraints has been found"); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + Node node; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setProperty(String name, boolean value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied.getBoolean()); + node.save(); + fail("setProperty(String name, boolean value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setProperty(String name, Value value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied); + node.save(); + fail("setProperty(String name, boolean value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setProperty(String name, Calendar value) and setProperty(String + * name, Value value) where value is a DateValue throw a + * ConstraintViolationException either immediately (by setProperty()), or on + * save, if the change would violate a node type constraint + */ + public void testDateProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.DATE, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No date property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No date property def with " + + "testable value constraints has been found"); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + Node node; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setProperty(String name, Calendar value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied.getDate()); + node.save(); + fail("setProperty(String name, Calendar value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setProperty(String name, Value value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied); + node.save(); + fail("setProperty(String name, Value value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setProperty(String name, double value) and setProperty(String + * name, Value value) where value is a DoubleValue throw a + * ConstraintViolationException either immediately (by setProperty()), or on + * save, if the change would violate a node type constraint + */ + public void testDoubleProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.DOUBLE, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No double property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No double property def with " + + "testable value constraints has been found"); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + Node node; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setProperty(String name, double value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied.getDouble()); + node.save(); + fail("setProperty(String name, double value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setProperty(String name, Value value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied); + node.save(); + fail("setProperty(String name, Value value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setProperty(String name, InputStream value) and + * setProperty(String name, Value value) where value is a BinaryValue throw + * a ConstraintViolationException either immediately (by setProperty()), or + * on save, if the change would violate a node type constraint + */ + public void testBinaryProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.BINARY, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No binary property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied1 = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + Value valueNotSatisfied2 = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied1 == null || valueNotSatisfied2 == null) { + throw new NotExecutableException("No binary property def with " + + "testable value constraints has been found"); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + Node node; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setProperty(String name, InputStream value) + InputStream in = valueNotSatisfied1.getStream(); + try { + node.setProperty(propDef.getName(), in); + node.save(); + fail("setProperty(String name, InputStream value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // test of signature setProperty(String name, Value value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied2); + node.save(); + fail("setProperty(String name, Value value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setProperty(String name, long value) and setProperty(String + * name, Value value) where value is a LongValue throw a + * ConstraintViolationException either immediately (by setProperty()), or on + * save, if the change would violate a node type constraint + */ + public void testLongProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.LONG, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No long property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No long property def with " + + "testable value constraints has been found"); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + Node node; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setProperty(String name, long value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied.getLong()); + node.save(); + fail("setProperty(String name, long value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setProperty(String name, Value value) + try { + node.setProperty(propDef.getName(), valueNotSatisfied); + node.save(); + fail("setProperty(String name, Value value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setProperty(String name, Node value) and setProperty(String + * name, Value value) where value is a ReferenceValue throw a + * ConstraintViolationException either immediately (by setProperty()), or on + * save, if the change would violate a node type constraint + */ + public void testReferenceProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.REFERENCE, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No reference property def with " + + "testable value constraints has been found"); + } + + String valueConstraints[] = propDef.getValueConstraints(); + if (valueConstraints == null || valueConstraints.length == 0) { + throw new NotExecutableException("No reference property def with " + + "testable value constraints has been found"); + } + List constraints = Arrays.asList(valueConstraints); + String nodeTypeNotSatisfied = null; + + NodeTypeManager manager = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeIterator types = manager.getAllNodeTypes(); + + // find a NodeType which is not satisfying the constraints + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + String name = type.getName(); + if (constraints.contains(name) || ntFrozenNode.equals(name)) { + continue; + } + if (type.getChildNodeDefinitions() != null + && type.getChildNodeDefinitions().length > 0) { + continue; + } + nodeTypeNotSatisfied = name; + break; + } + + if (nodeTypeNotSatisfied == null) { + throw new NotExecutableException("No reference property def with " + + "testable value constraints has been found"); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + Node node; + Node nodeNotSatisfied; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + + // create a referenceable node not satisfying the constraint + nodeNotSatisfied = testRootNode.addNode(nodeName4, nodeTypeNotSatisfied); + ensureMixinType(nodeNotSatisfied, mixReferenceable); + + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setProperty(String name, Node value) + try { + node.setProperty(propDef.getName(), nodeNotSatisfied); + node.save(); + fail("setProperty(String name, Node value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setProperty(String name, Value value) + try { + node.setProperty(propDef.getName(), superuser.getValueFactory().createValue(nodeNotSatisfied)); + node.save(); + fail("setProperty(String name, Value value) must throw a " + + "ConstraintViolationException if the change would violate a " + + "node type constraint either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyDecimalTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyDecimalTest.java new file mode 100644 index 00000000000..14ff33e7745 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyDecimalTest.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.test.api; + +import java.math.BigDecimal; + +import javax.jcr.Node; +import javax.jcr.Value; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * SetPropertyDecmimalTest tests the Node.setProperty(String, + * BigDecimal) method + * + */ +public class SetPropertyDecimalTest extends AbstractJCRTest { + + private Node testNode; + + private BigDecimal b1 = new BigDecimal("-123545678901234567890123545678901234567890.123545678901234567890"); + private BigDecimal b2 = new BigDecimal("123545678901234567890123545678901234567890.123545678901234567890"); + + protected void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1, testNodeType); + // abort test if the repository does not allow setting + // decimal properties on this node + ensureCanSetProperty(testNode, propertyName1, testNode.getSession().getValueFactory().createValue(new BigDecimal(0))); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * BigDecimal) works with Session.save() + */ + public void testNewDecimalPropertySession() throws Exception { + testNode.setProperty(propertyName1, b1); + superuser.save(); + assertEquals("Setting property with Node.setProperty(String, double) and Session.save() not working", + b1, + testNode.getProperty(propertyName1).getDecimal()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * BigDecimal) works with Session.save() + */ + public void testModifyDecimalPropertySession() throws Exception { + testNode.setProperty(propertyName1, b1); + superuser.save(); + testNode.setProperty(propertyName1, b2); + superuser.save(); + assertEquals("Modifying property with Node.setProperty(String, double) and Session.save() not working", + b2, + testNode.getProperty(propertyName1).getDecimal()); + } + + /** + * Tests if removing a BigDecimal property with + * Node.setProperty(String, null) works with + * Session.save() + */ + public void testRemoveDecimalPropertySession() throws Exception { + testNode.setProperty(propertyName1, b1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, (Value) null); + testRootNode.getSession().save(); + assertFalse("Removing decimal property with Node.setProperty(String, null) and Session.save() not working", + testNode.hasProperty(propertyName1)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyDoubleTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyDoubleTest.java new file mode 100644 index 00000000000..b6af9df7d7e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyDoubleTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Value; +import javax.jcr.Node; + +/** + * SetPropertyDoubleTest tests the Node.setProperty(String, + * double) method + * + */ +public class SetPropertyDoubleTest extends AbstractJCRTest { + + private Node testNode; + + private double d1 = 1.23e20; + private double d2 = 1.24e21; + + protected void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1, testNodeType); + + // abort test if the repository does not allow setting + // double properties on this node + ensureCanSetProperty(testNode, propertyName1, testNode.getSession().getValueFactory().createValue(0.0d)); + } + + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * double) works with Session.save() + */ + public void testNewDoublePropertySession() throws Exception { + testNode.setProperty(propertyName1, d1); + superuser.save(); + assertEquals("Setting property with Node.setProperty(String, double) and Session.save() not working", + new Double(d1), + new Double(testNode.getProperty(propertyName1).getDouble())); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * double) works with Session.save() + */ + public void testModifyDoublePropertySession() throws Exception { + testNode.setProperty(propertyName1, d1); + superuser.save(); + testNode.setProperty(propertyName1, d2); + superuser.save(); + assertEquals("Modifying property with Node.setProperty(String, double) and Session.save() not working", + new Double(d2), + new Double(testNode.getProperty(propertyName1).getDouble())); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * double) works with parentNode.save() + */ + public void testNewDoublePropertyParent() throws Exception { + testNode.setProperty(propertyName1, d1); + testRootNode.getSession().save(); + assertEquals("Setting property with Node.setProperty(String, double) and parentNode.save() not working", + new Double(d1), + new Double(testNode.getProperty(propertyName1).getDouble())); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * double) works with parentNode.save() + */ + public void testModifyDoublePropertyParent() throws Exception { + testNode.setProperty(propertyName1, d1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, d2); + testRootNode.getSession().save(); + assertEquals("Modifying property with Node.setProperty(String, double) and parentNode.save() not working", + new Double(d2), + new Double(testNode.getProperty(propertyName1).getDouble())); + } + + /** + * Tests if removing a double property with + * Node.setProperty(String, null) works with + * Session.save() + */ + public void testRemoveDoublePropertySession() throws Exception { + testNode.setProperty(propertyName1, d1); + superuser.save(); + testNode.setProperty(propertyName1, (Value) null); + superuser.save(); + assertFalse("Removing double property with Node.setProperty(String, null) and Session.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests if removing a double property with + * Node.setProperty(String, null) works with + * parentNode.save() + */ + public void testRemoveDoublePropertyParent() throws Exception { + testNode.setProperty(propertyName1, d1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, (Value) null); + testRootNode.getSession().save(); + assertFalse("Removing double property with Node.setProperty(String, null) and parentNode.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests that in infinity and NaN values can be persisted and round-tripped. + */ + public void testEdgeCases() throws Exception { + double tests[] = { Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY }; + String path = testNode.getPath(); + + for (double v : tests) { + testNode.setProperty(propertyName1, v); + testRootNode.getSession().save(); + assertEquals("Round-trip of " + v, v, superuser.getNode(path).getProperty(propertyName1).getDouble()); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyInputStreamTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyInputStreamTest.java new file mode 100644 index 00000000000..0a93a36ef4f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyInputStreamTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.util.InputStreamWrapper; + +import javax.jcr.Node; +import javax.jcr.Property; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ByteArrayInputStream; + +/** + * SetPropertyInputStreamTest tests the Node.setProperty(String, + * InputStream) method + * + */ +public class SetPropertyInputStreamTest extends AbstractJCRTest { + + private Node testNode; + + byte[] bytes1 = {73, 26, 32, -36, 40, -43, -124}; + private InputStream is1 = new ByteArrayInputStream(bytes1); + byte[] bytes2 = {-124, -43, 40, -36, 32, 26, 73}; + private InputStream is2 = new ByteArrayInputStream(bytes2); + + protected void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + + // special case for repositories that do allow binary property + // values, but only on jcr:content/jcr:data + if (propertyName1.equals("jcr:data") && testNode.hasNode("jcr:content") + && testNode.getNode("jcr:content").isNodeType("nt:resource") && ! testNode.hasProperty("jcr:data")) { + testNode = testNode.getNode("jcr:content"); + } + } + + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * InputStream) works with Session.save() + */ + public void testNewInputStreamPropertySession() throws Exception { + testNode.setProperty(propertyName1, is1); + superuser.save(); + is1 = new ByteArrayInputStream(bytes1); + InputStream in = testNode.getProperty(propertyName1).getStream(); + try { + assertTrue("Setting property with Node.setProperty(String, InputStream) and Session.save() not working", + compareInputStreams(is1, in)); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * InputStream) works with Session.save() + */ + public void testModifyInputStreamPropertySession() throws Exception { + testNode.setProperty(propertyName1, is1); + superuser.save(); + testNode.setProperty(propertyName1, is2); + superuser.save(); + is2 = new ByteArrayInputStream(bytes2); + InputStream in = testNode.getProperty(propertyName1).getStream(); + try { + assertTrue("Modifying property with Node.setProperty(String, InputStream) and Session.save() not working", + compareInputStreams(is2, in)); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + /** + * Tests if adding a property with Node.setProperty(String, + * InputStream) works with parentNode.save() + */ + public void testNewInputStreamPropertyParent() throws Exception { + testNode.setProperty(propertyName1, is1); + testRootNode.getSession().save(); + is1 = new ByteArrayInputStream(bytes1); + InputStream in = testNode.getProperty(propertyName1).getStream(); + try { + assertTrue("Setting property with Node.setProperty(String, InputStream) and parentNode.save() not working", + compareInputStreams(is1, in)); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * InputStream) works with parentNode.save() + */ + public void testModifyInputStreamPropertyParent() throws Exception { + testNode.setProperty(propertyName1, is1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, is2); + testRootNode.getSession().save(); + is2 = new ByteArrayInputStream(bytes2); + InputStream in = testNode.getProperty(propertyName1).getStream(); + try { + assertTrue("Modifying property with Node.setProperty(String, InputStream) and parentNode.save() not working", + compareInputStreams(is2, in)); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + /** + * Tests if removing a InputStream property with + * Node.setProperty(String, null) works with + * Session.save() + */ + public void testRemoveInputStreamPropertySession() throws Exception { + testNode.setProperty(propertyName1, is1); + superuser.save(); + + Property property = testNode.getProperty(propertyName1); + if (property.getDefinition().isMandatory() || property.getDefinition().isProtected()) { + throw new NotExecutableException("property " + property.getName() + " can not be removed"); + } + + testNode.setProperty(propertyName1, (InputStream) null); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (InputStream)null) and Session.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests if removing a InputStream property with + * Node.setProperty(String, null) works with + * parentNode.save() + */ + public void testRemoveInputStreamPropertyParent() throws Exception { + testNode.setProperty(propertyName1, is1); + testRootNode.getSession().save(); + + Property property = testNode.getProperty(propertyName1); + if (property.getDefinition().isMandatory() || property.getDefinition().isProtected()) { + throw new NotExecutableException("property " + property.getName() + " can not be removed"); + } + + testNode.setProperty(propertyName1, (InputStream) null); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (InputStream)null) and parentNode.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests whether the passed input stream is closed. + * @throws Exception + */ + public void testInputStreamClosed() throws Exception { + InputStreamWrapper in = new InputStreamWrapper(new ByteArrayInputStream(bytes1)); + testNode.setProperty(propertyName1, in); + assertTrue("Node.setProperty(..., InputStream) is expected to close the passed input stream", in.isClosed()); + } + + /** + * helper function: InputStream comparison + */ + private boolean compareInputStreams(InputStream f1, InputStream f2) { + try { + boolean equal = false; + int f1byte, f2byte; + + while ((f1byte = f1.read()) != -1) { + // byte match -> check next + if ((f2byte = f2.read()) != -1 && f2byte == f1byte) { + equal = true; + continue; + } + // byte mismatch + else { + equal = false; + break; + } + } + + // length mismatch + if ((f2byte = f2.read()) != -1) { + equal = false; + } + + return equal; + } catch (Exception e) { + return false; + } + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/SetPropertyLongTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyLongTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/SetPropertyLongTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyLongTest.java index c614606aa22..69ecfed59ff 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetPropertyLongTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyLongTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -25,10 +25,6 @@ * SetPropertyLongTest tests the Node.setProperty(String, * long) method * - * @test - * @sources SetPropertyLongTest.java - * @executeClass org.apache.jackrabbit.test.api.SetPropertyLongTest - * @keywords level2 */ public class SetPropertyLongTest extends AbstractJCRTest { @@ -42,6 +38,11 @@ protected void setUp() throws Exception { testNode = testRootNode.addNode(nodeName1, testNodeType); } + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + /** * Tests if adding a property with Node.setProperty(String, * long) works with Session.save() @@ -49,7 +50,7 @@ protected void setUp() throws Exception { public void testNewLongPropertySession() throws Exception { testNode.setProperty(propertyName1, l1); superuser.save(); - assertEquals("Setting property with Node.setProperty(String, double) and Session.save() not working", + assertEquals("Setting property with Node.setProperty(String, long) and Session.save() not working", new Long(l1), new Long(testNode.getProperty(propertyName1).getLong())); } @@ -63,7 +64,7 @@ public void testModifyLongPropertySession() throws Exception { superuser.save(); testNode.setProperty(propertyName1, l2); superuser.save(); - assertEquals("Modifying property with Node.setProperty(String, double) and Session.save() not working", + assertEquals("Modifying property with Node.setProperty(String, long) and Session.save() not working", new Long(l2), new Long(testNode.getProperty(propertyName1).getLong())); } @@ -74,8 +75,8 @@ public void testModifyLongPropertySession() throws Exception { */ public void testNewLongPropertyParent() throws Exception { testNode.setProperty(propertyName1, l1); - testRootNode.save(); - assertEquals("Setting property with Node.setProperty(String, double) and parentNode.save() not working", + testRootNode.getSession().save(); + assertEquals("Setting property with Node.setProperty(String, long) and parentNode.save() not working", new Long(l1), new Long(testNode.getProperty(propertyName1).getLong())); } @@ -86,10 +87,10 @@ public void testNewLongPropertyParent() throws Exception { */ public void testModifyLongPropertyParent() throws Exception { testNode.setProperty(propertyName1, l1); - testRootNode.save(); + testRootNode.getSession().save(); testNode.setProperty(propertyName1, l2); - testRootNode.save(); - assertEquals("Modifying property with Node.setProperty(String, double) and parentNode.save() not working", + testRootNode.getSession().save(); + assertEquals("Modifying property with Node.setProperty(String, long) and parentNode.save() not working", new Long(l2), new Long(testNode.getProperty(propertyName1).getLong())); } @@ -115,11 +116,11 @@ public void testRemoveLongPropertySession() throws Exception { */ public void testRemoveLongPropertyParent() throws Exception { testNode.setProperty(propertyName1, l1); - testRootNode.save(); + testRootNode.getSession().save(); testNode.setProperty(propertyName1, (Value) null); - testRootNode.save(); + testRootNode.getSession().save(); assertFalse("Removing long property with Node.setProperty(String, null) and parentNode.save() not working", testNode.hasProperty(propertyName1)); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyNodeTest.java new file mode 100644 index 00000000000..74fd6e9baa7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyNodeTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; + +/** + * SetPropertyNodeTest tests the Node.setProperty(String, + * Node) method + * + */ +public class SetPropertyNodeTest extends AbstractJCRTest { + + private Node testNode; + + private Node n1; + private Node n2; + + protected void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1, testNodeType); + + n1 = testRootNode.addNode(nodeName2, testNodeType); + n2 = testRootNode.addNode(nodeName3, testNodeType); + /* + JSR170 section 4.9 Referenceable Nodes: + The UUID of a referenceable node is assigned on node creation (or at + least on node persistence) by the system itself. + => call save in order to make sure, that the uuid is created and contains + a value generated by the system. + */ + testRootNode.getSession().save(); + + // abort test if the repository does not allow setting + // reference properties on this node + ensureCanSetProperty(testNode, propertyName1, testNode.getSession().getValueFactory().createValue(n1)); + + if (!n1.isNodeType(mixReferenceable)) { + throw new NotExecutableException("Node " + nodeName2 + " with nodetype " + testNodeType + " is not mix:referenceable."); + } + if (!n2.isNodeType(mixReferenceable)) { + throw new NotExecutableException("Node " + nodeName3 + " with nodetype " + testNodeType + " is not mix:referenceable."); + } + } + + protected void tearDown() throws Exception { + testNode = null; + n1 = null; + n2 = null; + super.tearDown(); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * Node) works with Session.save() + */ + public void testNewNodePropertySession() throws Exception { + testNode.setProperty(propertyName1, n1); + superuser.save(); + assertEquals("Setting property with Node.setProperty(String, Node) and Session.save() not working", + n1.getUUID(), + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * Node) works with Session.save() + */ + public void testModifyNodePropertySession() throws Exception { + testNode.setProperty(propertyName1, n1); + superuser.save(); + testNode.setProperty(propertyName1, n2); + superuser.save(); + assertEquals("Modifying property with Node.setProperty(String, Node) and Session.save() not working", + n2.getUUID(), + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * Node) works with parentNode.save() + */ + public void testNewNodePropertyParent() throws Exception { + testNode.setProperty(propertyName1, n1); + testRootNode.getSession().save(); + assertEquals("Setting property with Node.setProperty(String, Node) and parentNode.save() not working", + n1.getUUID(), + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * Node) works with parentNode.save() + */ + public void testModifyNodePropertyParent() throws Exception { + testNode.setProperty(propertyName1, n1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, n2); + testRootNode.getSession().save(); + assertEquals("Modifying property with Node.setProperty(String, Node) and parentNode.save() not working", + n2.getUUID(), + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if removing a Node property with + * Node.setProperty(String, null) works with + * Session.save() + */ + public void testRemoveNodePropertySession() throws Exception { + testNode.setProperty(propertyName1, n1); + superuser.save(); + testNode.setProperty(propertyName1, (Node) null); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (Node)null) and Session.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests if removing a Node property with + * Node.setProperty(String, null) works with + * parentNode.save() + */ + public void testRemoveNodePropertyParent() throws Exception { + testNode.setProperty(propertyName1, n1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, (Node) null); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (Node)null) and parentNode.save() not working", + testNode.hasProperty(propertyName1)); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyStringTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyStringTest.java new file mode 100644 index 00000000000..ee2688ff016 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyStringTest.java @@ -0,0 +1,440 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Value; +import javax.jcr.Node; + +import javax.jcr.ValueFormatException; +import javax.jcr.PropertyType; + +import java.util.Arrays; + + +/** + * SetPropertyStringTest tests the methods + * Node.setProperty(String, String), Node.setProperty(String, + * String[]) and Node.setProperty(String, String[], int) + * + */ +public class SetPropertyStringTest extends AbstractJCRTest { + + private Node testNode; + + private String s1 = "abc"; + private String s2 = "xyz"; + + private String[] sArray1 = new String[3]; + private String[] sArray2 = new String[3]; + private String[] sArrayNull = new String[3]; + + private Value[] vArray1 = new Value[3]; + private Value[] vArray2 = new Value[3]; + + + protected void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1, testNodeType); + + sArray1[0] = "a"; + sArray1[1] = "b"; + sArray1[2] = "c"; + sArray2[0] = "x"; + sArray2[1] = "y"; + sArray2[2] = "z"; + sArrayNull[0] = null; + sArrayNull[1] = null; + sArrayNull[2] = null; + + vArray1[0] = superuser.getValueFactory().createValue("a"); + vArray1[1] = superuser.getValueFactory().createValue("b"); + vArray1[2] = superuser.getValueFactory().createValue("c"); + + vArray2[0] = superuser.getValueFactory().createValue("x"); + vArray2[1] = superuser.getValueFactory().createValue("y"); + vArray2[2] = superuser.getValueFactory().createValue("z"); + } + + protected void tearDown() throws Exception { + testNode = null; + for (int i = 0; i < vArray1.length; i++) { + vArray1[i] = null; + } + for (int i = 0; i < vArray2.length; i++) { + vArray2[i] = null; + } + super.tearDown(); + } + + // String + + /** + * Tests if adding a property with Node.setProperty(String, + * String) works with Session.save() + */ + public void testNewStringPropertySession() throws Exception { + testNode.setProperty(propertyName1, s1); + superuser.save(); + assertEquals("Setting property with Node.setProperty(String, String) and Session.save() not working", + s1, + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * String) works with Session.save() + */ + public void testModifyStringPropertySession() throws Exception { + testNode.setProperty(propertyName1, s1); + superuser.save(); + testNode.setProperty(propertyName1, s2); + superuser.save(); + assertEquals("Modifying property with Node.setProperty(String, String) and Session.save() not working", + s2, + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * String) works with parentNode.save() + */ + public void testNewStringPropertyParent() throws Exception { + testNode.setProperty(propertyName1, s1); + testRootNode.getSession().save(); + assertEquals("Setting property with Node.setProperty(String, String) and parentNode.save() not working", + s1, + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * String) works with parentNode.save() + */ + public void testModifyStringPropertyParent() throws Exception { + testNode.setProperty(propertyName1, s1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, s2); + testRootNode.getSession().save(); + assertEquals("Modifying property with Node.setProperty(String, String) and parentNode.save() not working", + s2, + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if removing a String property with + * Node.setProperty(String, null) works with + * Session.save() + */ + public void testRemoveStringPropertySession() throws Exception { + testNode.setProperty(propertyName1, s1); + superuser.save(); + testNode.setProperty(propertyName1, (String) null); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (String)null) and Session.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests if removing a String property with + * Node.setProperty(String, null) works with + * parentNode.save() + */ + public void testRemoveStringPropertyParent() throws Exception { + testNode.setProperty(propertyName1, s1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, (String) null); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (String)null) and parentNode.save() not working", + testNode.hasProperty(propertyName1)); + } + + + // String with PropertyType + + /** + * Tests if adding a property with Node.setProperty(String, + * String, int) works with Session.save() + */ + public void testNewStringPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, s1, PropertyType.STRING); + superuser.save(); + assertEquals("Setting property with Node.setProperty(String, String, int) and Session.save() not working", + s1, + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * String, int) works with Session.save() + */ + public void testModifyStringPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, s1, PropertyType.STRING); + superuser.save(); + testNode.setProperty(propertyName1, s2, PropertyType.STRING); + superuser.save(); + assertEquals("Modifying property with Node.setProperty(String, String, int) and Session.save() not working", + s2, + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * String, int) works with parentNode.save() + */ + public void testNewStringPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, s1, PropertyType.STRING); + testRootNode.getSession().save(); + assertEquals("Setting property with Node.setProperty(String, String, int) and parentNode.save() not working", + s1, + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * String, int) works with parentNode.save() + */ + public void testModifyStringPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, s1, PropertyType.STRING); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, s2, PropertyType.STRING); + testRootNode.getSession().save(); + assertEquals("Modifying property with Node.setProperty(String, String, int) and parentNode.save() not working", + s2, + testNode.getProperty(propertyName1).getString()); + } + + /** + * Tests if removing a String property with + * Node.setProperty(String, null, int) works with + * Session.save() + */ + public void testRemoveStringPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, s1, PropertyType.STRING); + superuser.save(); + testNode.setProperty(propertyName1, (String) null, PropertyType.STRING); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (String)null, int) and Session.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests if removing a String property with + * Node.setProperty(String, null, int) works with + * parentNode.save() + */ + public void testRemoveStringPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, s1, PropertyType.STRING); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, (String) null, PropertyType.STRING); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (String)null, int) and parentNode.save() not working", + testNode.hasProperty(propertyName1)); + } + + + // String[] + + /** + * Tests if adding properties with Node.setProperty(String, + * String[]) works with Session.save() + */ + public void testNewStringArrayPropertySession() throws Exception { + testNode.setProperty(propertyName2, sArray1); + superuser.save(); + assertEquals("Setting properties with Node.setProperty(String, String[]) and Session.save() not working", + Arrays.asList(vArray1), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if modifying properties with Node.setProperty(String, + * String[]) works with Session.save() + */ + public void testModifyStringArrayPropertySession() throws Exception { + testNode.setProperty(propertyName2, sArray1); + superuser.save(); + testNode.setProperty(propertyName2, sArray2); + superuser.save(); + assertEquals("Modifying properties with Node.setProperty(String, String[]) and Session.save() not working", + Arrays.asList(vArray2), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if adding properties with Node.setProperty(String, + * String[]) works with parentNode.save() + */ + public void testNewStringArrayPropertyParent() throws Exception { + testNode.setProperty(propertyName2, sArray1); + testRootNode.getSession().save(); + assertEquals("Setting properties with Node.setProperty(String, String[]) and parentNode.save() not working", + Arrays.asList(vArray1), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if modifying properties with Node.setProperty(String, + * String[]) works with parentNode.save() + */ + public void testModifyStringArrayPropertyParent() throws Exception { + testNode.setProperty(propertyName2, sArray1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName2, sArray2); + testRootNode.getSession().save(); + assertEquals("Modifying properties with Node.setProperty(String, String[]) and parentNode.save() not working", + Arrays.asList(vArray2), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if removing a String[] property with + * Node.setProperty(String, null) works with + * Session.save() + */ + public void testRemoveStringArrayPropertySession() throws Exception { + testNode.setProperty(propertyName2, sArray1); + superuser.save(); + testNode.setProperty(propertyName2, (String[]) null); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (String[])null) and Session.save() not working", + testNode.hasProperty(propertyName2)); + } + + /** + * Tests if removing a String[] property with + * Node.setProperty(String, null) works with + * parentNode.save() + */ + public void testRemoveStringArrayPropertyParent() throws Exception { + testNode.setProperty(propertyName2, sArray1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName2, (String[]) null); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (String[])null) and parentNode.save() not working", + testNode.hasProperty(propertyName2)); + } + + /** + * Tests if Node.setProperty(String, String[]) saves an array + * of null values as an empty String[] + */ + public void testSetNullStringArray() throws Exception { + testNode.setProperty(propertyName2, sArrayNull); + superuser.save(); + assertEquals("Node.setProperty(String, nullStringArray[]) did not set the property to an empty String[]", + Arrays.asList(new Value[0]), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + + // String[] with PropertyType + + /** + * Tests if adding properties with Node.setProperty(String, String[], + * int) works with Session.save() + */ + public void testNewStringArrayPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, sArray1, PropertyType.STRING); + superuser.save(); + assertEquals("Setting properties with Node.setProperty(String, String[], int) and Session.save() not working", + Arrays.asList(vArray1), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if modifying properties with Node.setProperty(String, + * String[], int) works with Session.save() + */ + public void testModifyStringArrayPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, sArray1, PropertyType.STRING); + superuser.save(); + testNode.setProperty(propertyName2, sArray2, PropertyType.STRING); + superuser.save(); + assertEquals("Modifying properties with Node.setProperty(String, String[], int) and Session.save() not working", + Arrays.asList(vArray2), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if adding properties with Node.setProperty(String, String[], + * int) works with parentNode.save() + */ + public void testNewStringArrayPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, sArray1, PropertyType.STRING); + testRootNode.getSession().save(); + assertEquals("Setting properties with Node.setProperty(String, String[], int) and parentNode.save() not working", + Arrays.asList(vArray1), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if modifying properties with Node.setProperty(String, + * String[], int) works with parentNode.save() + */ + public void testModifyStringArrayPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, sArray1, PropertyType.STRING); + testRootNode.getSession().save(); + testNode.setProperty(propertyName2, sArray2, PropertyType.STRING); + testRootNode.getSession().save(); + assertEquals("Modifying properties with Node.setProperty(String, String[], int) and parentNode.save() not working", + Arrays.asList(vArray2), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if removing a String[] property with + * Node.setProperty(String, null, int) works with + * Session.save() + */ + public void testRemoveStringArrayPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, sArray1, PropertyType.STRING); + superuser.save(); + testNode.setProperty(propertyName2, (String[]) null, PropertyType.STRING); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (String[])null, int) and Session.save() not working", + testNode.hasProperty(propertyName2)); + } + + /** + * Tests if removing a String[] property with + * Node.setProperty(String, null, int) works with + * parentNode.save() + */ + public void testRemoveStringArrayPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, sArray1, PropertyType.STRING); + testRootNode.getSession().save(); + testNode.setProperty(propertyName2, (String[]) null, PropertyType.STRING); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (String[])null, int) and parentNode.save() not working", + testNode.hasProperty(propertyName2)); + } + + /** + * Tests if Node.setProperty(String, String[], int) saves an + * array of null values as an empty String[] + */ + public void testSetNullStringArrayWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, sArrayNull, PropertyType.STRING); + superuser.save(); + assertEquals("Node.setProperty(String, nullStringArray[], int) did not set the property to an empty Value[]", + Arrays.asList(new Value[0]), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyValueTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyValueTest.java new file mode 100644 index 00000000000..d5fc2c6e34a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetPropertyValueTest.java @@ -0,0 +1,491 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Value; +import javax.jcr.Node; + +import javax.jcr.ValueFormatException; +import javax.jcr.PropertyType; + +import java.util.Arrays; + +/** + * SetPropertyValueTest tests the methods Node.setProperty(String, + * Value), Node.setProperty(String, Value[]) and + * Node.setProperty(String, Value[], int) + * + */ +public class SetPropertyValueTest extends AbstractJCRTest { + + private Node testNode; + + private Value v1; + private Value v2; + + private Value[] vArray1 = new Value[3]; + private Value[] vArray2 = new Value[3]; + private Value[] vArrayMixed = new Value[3]; + private Value[] vArrayNull = new Value[3]; + private Value[] vArrayWithNulls = new Value[5]; + + protected void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1, testNodeType); + + v1 = superuser.getValueFactory().createValue("abc"); + v2 = superuser.getValueFactory().createValue("xyz"); + + vArray1[0] = superuser.getValueFactory().createValue("a"); + vArray1[1] = superuser.getValueFactory().createValue("b"); + vArray1[2] = superuser.getValueFactory().createValue("c"); + + vArray2[0] = superuser.getValueFactory().createValue("x"); + vArray2[1] = superuser.getValueFactory().createValue("y"); + vArray2[2] = superuser.getValueFactory().createValue("z"); + + vArrayMixed[0] = superuser.getValueFactory().createValue("abc"); + vArrayMixed[1] = superuser.getValueFactory().createValue(true); + vArrayMixed[2] = superuser.getValueFactory().createValue(2147483650L); + + vArrayWithNulls[1] = superuser.getValueFactory().createValue("a"); + vArrayWithNulls[3] = superuser.getValueFactory().createValue("z"); + } + + protected void tearDown() throws Exception { + testNode = null; + v1 = null; + v2 = null; + for (int i = 0; i < vArray1.length; i++) { + vArray1[i] = null; + } + for (int i = 0; i < vArray2.length; i++) { + vArray2[i] = null; + } + for (int i = 0; i < vArrayMixed.length; i++) { + vArrayMixed[i] = null; + } + for (int i = 0; i < vArrayNull.length; i++) { + vArrayNull[i] = null; + } + for (int i = 0; i < vArrayWithNulls.length; i++) { + vArrayWithNulls[i] = null; + } + super.tearDown(); + } + + /** + * Value + */ + + /** + * Tests if adding a property with Node.setProperty(String, + * Value) works with Session.save() + */ + public void testNewValuePropertySession() throws Exception { + testNode.setProperty(propertyName1, v1); + superuser.save(); + assertEquals("Setting property with Node.setProperty(String, Value) and Session.save() not working", + v1, + testNode.getProperty(propertyName1).getValue()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * Value) works with Session.save() + */ + public void testModifyValuePropertySession() throws Exception { + testNode.setProperty(propertyName1, v1); + superuser.save(); + testNode.setProperty(propertyName1, v2); + superuser.save(); + assertEquals("Modifying property with Node.setProperty(String, Value) and Session.save() not working", + v2, + testNode.getProperty(propertyName1).getValue()); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * Value) works with parentNode.save() + */ + public void testNewValuePropertyParent() throws Exception { + testNode.setProperty(propertyName1, v1); + testRootNode.getSession().save(); + assertEquals("Setting property with Node.setProperty(String, Value) and parentNode.save() not working", + v1, + testNode.getProperty(propertyName1).getValue()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * Value) works with parentNode.save() + */ + public void testModifyValuePropertyParent() throws Exception { + testNode.setProperty(propertyName1, v1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, v2); + testRootNode.getSession().save(); + assertEquals("Modifying property with Node.setProperty(String, Value) and parentNode.save() not working", + v2, + testNode.getProperty(propertyName1).getValue()); + } + + /** + * Tests if removing a Value property with + * Node.setProperty(String, null) works with + * Session.save() + */ + public void testRemoveValuePropertySession() throws Exception { + testNode.setProperty(propertyName1, v1); + superuser.save(); + testNode.setProperty(propertyName1, (Value) null); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (Value)null) and Session.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests if removing a Value property with + * Node.setProperty(String, null) works with + * parentNode.save() + */ + public void testRemoveValuePropertyParent() throws Exception { + testNode.setProperty(propertyName1, v1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, (Value) null); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (Value)null) and parentNode.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Value with PropertyType + */ + + /** + * Tests if adding a property with Node.setProperty(String, + * Value, int) works with Session.save() + */ + public void testNewValuePropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, v1, PropertyType.STRING); + superuser.save(); + assertEquals("Setting property with Node.setProperty(String, Value, int) and Session.save() not working", + v1, + testNode.getProperty(propertyName1).getValue()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * Value, int) works with Session.save() + */ + public void testModifyValuePropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, v1, PropertyType.STRING); + superuser.save(); + testNode.setProperty(propertyName1, v2, PropertyType.STRING); + superuser.save(); + assertEquals("Modifying property with Node.setProperty(String, Value, int) and Session.save() not working", + v2, + testNode.getProperty(propertyName1).getValue()); + } + + /** + * Tests if adding a property with Node.setProperty(String, + * Value, int) works with parentNode.save() + */ + public void testNewValuePropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, v1, PropertyType.STRING); + testRootNode.getSession().save(); + assertEquals("Setting property with Node.setProperty(String, Value, int) and parentNode.save() not working", + v1, + testNode.getProperty(propertyName1).getValue()); + } + + /** + * Tests if modifying a property with Node.setProperty(String, + * Value, int) works with parentNode.save() + */ + public void testModifyValuePropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, v1, PropertyType.STRING); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, v2, PropertyType.STRING); + testRootNode.getSession().save(); + assertEquals("Modifying property with Node.setProperty(String, Value, int) and parentNode.save() not working", + v2, + testNode.getProperty(propertyName1).getValue()); + } + + /** + * Tests if removing a Value property with + * Node.setProperty(String, null, int) works with + * Session.save() + */ + public void testRemoveValuePropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, v1, PropertyType.STRING); + superuser.save(); + testNode.setProperty(propertyName1, (Value) null, PropertyType.STRING); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (Value)null, int) and Session.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Tests if removing a Value property with + * Node.setProperty(String, null, int) works with + * parentNode.save() + */ + public void testRemoveValuePropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName1, v1, PropertyType.STRING); + testRootNode.getSession().save(); + testNode.setProperty(propertyName1, (Value) null, PropertyType.STRING); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (Value)null, int) and parentNode.save() not working", + testNode.hasProperty(propertyName1)); + } + + /** + * Value[] + */ + + /** + * Tests if adding properties with Node.setProperty(String, + * Value[]) works with Session.save() + */ + public void testNewValueArrayPropertySession() throws Exception { + testNode.setProperty(propertyName2, vArray1); + superuser.save(); + assertEquals("Setting properties with Node.setProperty(String, Value[]) and Session.save() not working", + Arrays.asList(vArray1), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if modifying properties with Node.setProperty(String, + * Value[]) works with Session.save() + */ + public void testModifyValueArrayPropertySession() throws Exception { + testNode.setProperty(propertyName2, vArray1); + superuser.save(); + testNode.setProperty(propertyName2, vArray2); + superuser.save(); + assertEquals("Modifying properties with Node.setProperty(String, Value[]) and Session.save() not working", + Arrays.asList(vArray2), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if adding properties with Node.setProperty(String, + * Value[]) works with parentNode.save() + */ + public void testNewValueArrayPropertyParent() throws Exception { + testNode.setProperty(propertyName2, vArray1); + testRootNode.getSession().save(); + assertEquals("Setting properties with Node.setProperty(String, Value[]) and parentNode.save() not working", + Arrays.asList(vArray1), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if modifying properties with Node.setProperty(String, + * Value[]) works with parentNode.save() + */ + public void testModifyValueArrayPropertyParent() throws Exception { + testNode.setProperty(propertyName2, vArray1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName2, vArray2); + testRootNode.getSession().save(); + assertEquals("Modifying properties with Node.setProperty(String, Value[]) and parentNode.save() not working", + Arrays.asList(vArray2), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if Node.setProperty(String, Value[]) throws a {@link + * javax.jcr.ValueFormatException} when trying to set a multi-value property + * to an array of values with different types + */ + public void testSetMixedValueArrayValueFormatException() throws Exception { + try { + testNode.setProperty(propertyName2, vArrayMixed); + fail("setProperty(String, mixedValueArray[]) not throwing a ValueFormatException"); + } catch (ValueFormatException success) { + } + } + + /** + * Tests if removing a Value[] property with + * Node.setProperty(String, null) works with + * Session.save() + */ + public void testRemoveValueArrayPropertySession() throws Exception { + testNode.setProperty(propertyName2, vArray1); + superuser.save(); + testNode.setProperty(propertyName2, (Value[]) null); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (Value[])null) and Session.save() not working", + testNode.hasProperty(propertyName2)); + } + + /** + * Tests if removing a Value[] property with + * Node.setProperty(String, null) works with + * parentNode.save() + */ + public void testRemoveValueArrayPropertyParent() throws Exception { + testNode.setProperty(propertyName2, vArray1); + testRootNode.getSession().save(); + testNode.setProperty(propertyName2, (Value[]) null); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (Value[])null) and parentNode.save() not working", + testNode.hasProperty(propertyName2)); + } + + /** + * Tests if Node.setProperty(String, Value[]) saves an array of + * null values as an empty Value[] + */ + public void testSetNullValueArray() throws Exception { + testNode.setProperty(propertyName2, vArrayNull); + superuser.save(); + assertEquals("Node.setProperty(String, nullValueArray[]) did not set the property to an empty Value[]", + Arrays.asList(new Value[0]), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if Node.setProperty(String, Value[]) correctly compacts + * the value array by removing all null values + */ + public void testCompactValueArrayWithNulls() throws Exception { + testNode.setProperty(propertyName2, vArrayWithNulls); + superuser.save(); + assertEquals("Node.setProperty(String, valueArrayWithNulls[]) did not compact the value array by removing the null values", + 2, + testNode.getProperty(propertyName2).getValues().length); + } + + /** + * Value[] with PropertyType + */ + + /** + * Tests if adding properties with Node.setProperty(String, Value[], + * int) works with Session.save() + */ + public void testNewValueArrayPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, vArray1, PropertyType.STRING); + superuser.save(); + assertEquals("Setting properties with Node.setProperty(String, Value[], int) and Session.save() not working", + Arrays.asList(vArray1), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if modifying properties with Node.setProperty(String, + * Value[], int) works with Session.save() + */ + public void testModifyValueArrayPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, vArray1, PropertyType.STRING); + superuser.save(); + testNode.setProperty(propertyName2, vArray2, PropertyType.STRING); + superuser.save(); + assertEquals("Modifying properties with Node.setProperty(String, Value[], int) and Session.save() not working", + Arrays.asList(vArray2), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if adding properties with Node.setProperty(String, Value[], + * int) works with parentNode.save() + */ + public void testNewValueArrayPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, vArray1, PropertyType.STRING); + testRootNode.getSession().save(); + assertEquals("Setting properties with Node.setProperty(String, Value[], int) and parentNode.save() not working", + Arrays.asList(vArray1), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if modifying properties with Node.setProperty(String, + * Value[], int) works with parentNode.save() + */ + public void testModifyValueArrayPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, vArray1, PropertyType.STRING); + testRootNode.getSession().save(); + testNode.setProperty(propertyName2, vArray2, PropertyType.STRING); + testRootNode.getSession().save(); + assertEquals("Modifying properties with Node.setProperty(String, Value[], int) and parentNode.save() not working", + Arrays.asList(vArray2), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + + /** + * Tests if Node.setProperty(String, Value[], int) throws a + * {@link javax.jcr.ValueFormatException} when trying to set a multi-value + * property to an array of values with different types + */ + public void testSetMixedValueArrayValueFormatExceptionWithPropertyType() throws Exception { + try { + testNode.setProperty(propertyName2, vArrayMixed, PropertyType.STRING); + fail("setProperty(String, mixedValueArray[], int) not throwing a ValueFormatException"); + } catch (ValueFormatException success) { + } + } + + /** + * Tests if removing a Value[] property with + * Node.setProperty(String, null, int) works with + * Session.save() + */ + public void testRemoveValueArrayPropertySessionWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, vArray1, PropertyType.STRING); + superuser.save(); + testNode.setProperty(propertyName2, (Value[]) null, PropertyType.STRING); + superuser.save(); + assertFalse("Removing property with Node.setProperty(String, (Value[])null, int) and Session.save() not working", + testNode.hasProperty(propertyName2)); + } + + /** + * Tests if removing a Value[] property with + * Node.setProperty(String, null, int) works with + * parentNode.save() + */ + public void testRemoveValueArrayPropertyParentWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, vArray1, PropertyType.STRING); + testRootNode.getSession().save(); + testNode.setProperty(propertyName2, (Value[]) null, PropertyType.STRING); + testRootNode.getSession().save(); + assertFalse("Removing property with Node.setProperty(String, (Value[])null, int) and parentNode.save() not working", + testNode.hasProperty(propertyName2)); + } + + /** + * Tests if Node.setProperty(String, Value[], int) saves an + * array of null values as an empty Value[] + */ + public void testSetNullValueArrayWithPropertyType() throws Exception { + testNode.setProperty(propertyName2, vArrayNull, PropertyType.STRING); + superuser.save(); + assertEquals("Node.setProperty(String, nullValueArray[], int) did not set the property to an empty Value[]", + Arrays.asList(new Value[0]), + Arrays.asList(testNode.getProperty(propertyName2).getValues())); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueBinaryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueBinaryTest.java new file mode 100644 index 00000000000..1475b595f93 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueBinaryTest.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.IOException; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * Tests the various {@link Property#setValue(Value)} methods. + *

        + * Configuration requirements: + *

        + * The node at {@link #testRoot} must allow a + * child node of type {@link #testNodeType} with name {@link #nodeName1}. The + * node type {@link #testNodeType} must define a single value binary property + * with name {@link #propertyName1}. As a special case, if the specified node + * type automatically adds a jcr:content child node of type nt:resource, and + * propertyName1 is specified as "jcr:data", that binary property + * is used instead. + * + */ +public class SetValueBinaryTest extends AbstractJCRTest { + + /** + * The binary value + */ + private Value value; + + /** + * The binary data + */ + private byte[] data; + + /** + * The node with the binary property + */ + private Node node; + + /** + * The binary property + */ + private Property property1; + + protected void setUp() throws Exception { + super.setUp(); + + // initialize some binary value + data = createRandomString(10).getBytes(); + value = superuser.getValueFactory().createValue(new ByteArrayInputStream(data)); + + // create a new node under the testRootNode + node = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + + // special case for repositories that do allow binary property + // values, but only on jcr:content/jcr:data + if (propertyName1.equals("jcr:data") && node.hasNode("jcr:content") + && node.getNode("jcr:content").isNodeType("nt:resource") && ! node.hasProperty("jcr:data")) { + node = node.getNode("jcr:content"); + } + + // create a new single-value property and save it + property1 = node.setProperty(propertyName1, superuser.getValueFactory().createValue(new ByteArrayInputStream(new byte[0]))); + superuser.save(); + } + + protected void tearDown() throws Exception { + value = null; + node = null; + property1 = null; + super.tearDown(); + } + + /** + * Test the persistence of a property modified with an BinaryValue parameter + * and saved from the Session + */ + public void testBinarySession() throws RepositoryException, IOException { + property1.setValue(value); + superuser.save(); + InputStream in = property1.getValue().getStream(); + try { + compareStream(data, in); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + /** + * Test the persistence of a property modified with an BinaryValue parameter + * and saved from the Session + */ + public void testBinarySessionJcr2() throws RepositoryException, IOException { + property1.setValue(value); + superuser.save(); + Binary bin = property1.getValue().getBinary(); + try { + InputStream in = bin.getStream(); + try { + compareStream(data, in); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } finally { + bin.dispose(); + } + } + + /** + * Test the persistence of a property modified with an input stream + * parameter and saved from the parent Node + */ + public void testBinaryParent() throws RepositoryException, IOException { + InputStream in = value.getStream(); + try { + property1.setValue(in); + node.save(); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + in = property1.getValue().getStream(); + try { + compareStream(data, in); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } + + /** + * Test the persistence of a property modified with an input stream + * parameter and saved from the parent Node + */ + public void testBinaryParentJcr2() throws RepositoryException, IOException { + Binary bin = value.getBinary(); + try { + property1.setValue(bin); + node.save(); + bin = property1.getValue().getBinary(); + InputStream in = bin.getStream(); + try { + compareStream(data, in); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + } finally { + bin.dispose(); + } + } + + /** + * Test the deletion of a property by assigning it a null value, saved from + * the Session + */ + public void testRemoveBinarySession() throws RepositoryException, NotExecutableException { + if (property1.getDefinition().isMandatory() || property1.getDefinition().isProtected()) { + throw new NotExecutableException("property " + property1.getName() + " can not be removed"); + } + + property1.setValue((InputStream) null); + superuser.save(); + + try { + node.getProperty(propertyName1); + fail("The property should not exist anymore, as a null Value has been assigned"); + } catch (Exception e) { + //success : the property has been deleted by assigning it a null value + } + } + + /** + * Test the deletion of a property by assigning it a null value, saved from + * the parent Node + */ + public void testRemoveBinaryParent() throws RepositoryException, NotExecutableException { + if (property1.getDefinition().isMandatory() || property1.getDefinition().isProtected()) { + throw new NotExecutableException("property " + property1.getName() + " can not be removed"); + } + + property1.setValue((Value) null); + node.save(); + + try { + node.getProperty(propertyName1); + fail("The property should not exist anymore, as a null Value has been assigned"); + } catch (Exception e) { + //success : the property has been deleted by assigning it a null value + } + } + + //--------------------------< internal >------------------------------------ + + private void compareStream(byte[] data, InputStream s) throws IOException { + byte[] read = new byte[1]; + for (int i = 0; i < data.length; i++) { + assertEquals("Stream data does not match value set.", 1, s.read(read)); + assertEquals("Stream data does not match value set.", data[i], read[0]); + } + if (s.available() > 0) { + fail("InputStream has more data than value set."); + } + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/SetValueBooleanTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueBooleanTest.java similarity index 79% rename from src/test/org/apache/jackrabbit/test/api/SetValueBooleanTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueBooleanTest.java index 93499435e0f..c549cdfc2ed 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetValueBooleanTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueBooleanTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,29 +21,26 @@ import javax.jcr.Property; import javax.jcr.Value; import javax.jcr.RepositoryException; -import javax.jcr.BooleanValue; import javax.jcr.Node; import javax.jcr.PathNotFoundException; /** * Tests the various {@link Property#setValue(Value)} methods. - *

        - * Configuration requirements:
        The node at {@link #testRoot} must allow a + *

        + * Configuration requirements: + *

        + * The node at {@link #testRoot} must allow a * child node of type {@link #testNodeType} with name {@link #nodeName1}. The * node type {@link #testNodeType} must define a single value boolean property * with name {@link #propertyName1}. * - * @test - * @sources SetValueBooleanTest.java - * @executeClass org.apache.jackrabbit.test.api.SetValueBooleanTest - * @keywords level2 */ public class SetValueBooleanTest extends AbstractJCRTest { /** * The boolean value */ - private BooleanValue value; + private Value value; /** * The node with the boolean property @@ -59,7 +56,7 @@ protected void setUp() throws Exception { super.setUp(); // initialize some boolean value - value = new BooleanValue(true); + value = superuser.getValueFactory().createValue(true); // create a new node under the testRootNode node = testRootNode.addNode(nodeName1, testNodeType); @@ -69,6 +66,13 @@ protected void setUp() throws Exception { superuser.save(); } + protected void tearDown() throws Exception { + value = null; + node = null; + property1 = null; + super.tearDown(); + } + /** * Test the persistence of a property modified with an BooleanValue * parameter and saved from the Session @@ -94,7 +98,7 @@ public void testBooleanParent() throws RepositoryException { * the Session */ public void testRemoveBooleanSession() throws RepositoryException { - property1.setValue((BooleanValue) null); + property1.setValue((Value) null); superuser.save(); try { @@ -110,7 +114,7 @@ public void testRemoveBooleanSession() throws RepositoryException { * the parent Node */ public void testRemoveBooleanParent() throws RepositoryException { - property1.setValue((BooleanValue) null); + property1.setValue((Value) null); node.save(); try { diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueConstraintViolationExceptionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueConstraintViolationExceptionTest.java new file mode 100644 index 00000000000..36ff082041f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueConstraintViolationExceptionTest.java @@ -0,0 +1,856 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.nodetype.NodeTypeUtil; + +/** + * SetValueConstraintViolationExceptionTest tests if setValue() + * throws a ConstraintViolationException either immediately (by setValue()) or + * on save, if the change would violate a value constraint. + * + */ +public class SetValueConstraintViolationExceptionTest extends AbstractJCRTest { + + /** + * Tests if setValue(InputStream value) and setValue(Value value) where + * value is a BinaryValue throw a ConstraintViolationException if the change + * would violate a value constraint + */ + public void testBinaryProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.BINARY, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No binary property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied1 = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + Value valueNotSatisfied2 = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied1 == null || valueNotSatisfied2 == null) { + throw new NotExecutableException("No binary property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), valueSatisfied); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(InputStream value) + InputStream in = valueNotSatisfied1.getStream(); + try { + prop.setValue(in); + node.save(); + fail("setValue(InputStream value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // test of signature setValue(Value value) + try { + prop.setValue(valueNotSatisfied2); + node.save(); + fail("setValue(Value value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(boolean value) and setValue(Value value) where value is + * a BooleanValue throw a ConstraintViolationException if the change would + * violate a value constraint + */ + public void testBooleanProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.BOOLEAN, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No boolean property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No boolean property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), valueSatisfied); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(boolean value) + try { + prop.setValue(valueNotSatisfied.getBoolean()); + node.save(); + fail("setValue(boolean value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setValue(Value value) + try { + prop.setValue(valueNotSatisfied); + node.save(); + fail("setValue(Value value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(Calendar value) and setValue(Value value) where value + * is a DateValue throw a ConstraintViolationException if the change would + * violate a value constraint + */ + public void testDateProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.DATE, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No date property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No date property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), valueSatisfied); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(Calendar value) + try { + prop.setValue(valueNotSatisfied.getDate()); + node.save(); + fail("setValue(Date value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setValue(Value value) + try { + prop.setValue(valueNotSatisfied); + node.save(); + fail("setValue(Value value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(Double value) and setValue(Value value) where value is + * a DoubleValue throw a ConstraintViolationException if the change would + * violate a value constraint + */ + public void testDoubleProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.DOUBLE, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No double property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No double property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), valueSatisfied); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(double value) + try { + prop.setValue(valueNotSatisfied.getDouble()); + node.save(); + fail("setValue(double value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setValue(Value value) + try { + prop.setValue(valueNotSatisfied); + node.save(); + fail("setValue(Value value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(Long value) and setValue(Value value) where value is a + * LongValue throw a ConstraintViolationException if the change would + * violate a value constraint + */ + public void testLongProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.LONG, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No long property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No long property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), valueSatisfied); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(long value) + try { + prop.setValue(valueNotSatisfied.getLong()); + node.save(); + fail("setValue(long value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setValue(Value value) + try { + prop.setValue(valueNotSatisfied); + node.save(); + fail("setValue(Value value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + + /** + * Tests if setValue(Node value) and setValue(Value value) where value is a + * ReferenceValue throw a ConstraintViolationException if the change would + * violate a value constraint + */ + public void testReferenceProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.REFERENCE, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No reference property def with " + + "testable value constraints has been found"); + } + + String valueConstraints[] = propDef.getValueConstraints(); + if (valueConstraints == null || valueConstraints.length == 0) { + throw new NotExecutableException("No reference property def with " + + "testable value constraints has been found"); + } + List constraints = Arrays.asList(valueConstraints); + String nodeTypeSatisfied = constraints.get(0); + String nodeTypeNotSatisfied = null; + + NodeTypeManager manager = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeIterator types = manager.getAllNodeTypes(); + + // find a NodeType which is not satisfying the constraints + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + String name = type.getName(); + if (constraints.contains(name) || ntFrozenNode.equals(name)) { + continue; + } + if (type.getChildNodeDefinitions() != null + && type.getChildNodeDefinitions().length > 0) { + continue; + } + nodeTypeNotSatisfied = name; + break; + } + + if (nodeTypeNotSatisfied == null) { + throw new NotExecutableException("No reference property def with " + + "testable value constraints has been found"); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + Node nodeSatisfied; + Node nodeNotSatisfied; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + + // create a referenceable node satisfying the constraint + nodeSatisfied = testRootNode.addNode(nodeName3, nodeTypeSatisfied); + ensureMixinType(nodeSatisfied, mixReferenceable); + + // create a referenceable node not satisfying the constraint + nodeNotSatisfied = testRootNode.addNode(nodeName4, nodeTypeNotSatisfied); + ensureMixinType(nodeNotSatisfied, mixReferenceable); + + // some implementations may require a save after addMixin() + testRootNode.getSession().save(); + + prop = node.setProperty(propDef.getName(), nodeSatisfied); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(Node value) + try { + prop.setValue(nodeNotSatisfied); + node.save(); + fail("setValue(Node value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + + // test of signature setValue(Value value) + try { + prop.setValue(superuser.getValueFactory().createValue(nodeNotSatisfied)); + node.save(); + fail("setValue(Value value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(Value[] values) where values are of type BinaryValue + * throw a ConstraintViolationException if the change would violate a value + * constraint + */ + public void testMultipleBinaryProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.BINARY, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple binary property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No multiple binary property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), new Value[]{valueSatisfied}); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + try { + prop.setValue(new Value[]{valueNotSatisfied}); + node.save(); + fail("setValue(Value[] values) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(Value[] values) where values are of type BooleanValue + * throw a ConstraintViolationException if the change would violate a value + * constraint + */ + public void testMultipleBooleanProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.BOOLEAN, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple boolean property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No multiple boolean property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), new Value[]{valueSatisfied}); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + try { + prop.setValue(new Value[]{valueNotSatisfied}); + node.save(); + fail("setValue(Value[] values) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + + /** + * Tests if setValue(Value[] values) where values are of type DateValue + * throw a ConstraintViolationException if the change would violate a value + * constraint + */ + public void testMultipleDateProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.DATE, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple date property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No multiple date property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), new Value[]{valueSatisfied}); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + try { + prop.setValue(new Value[]{valueNotSatisfied}); + node.save(); + fail("setValue(Value[] values) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(Value[] values) where values are of type DoubleValue + * throw a ConstraintViolationException if the change would violate a value + * constraint + */ + public void testMultipleDoubleProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.DOUBLE, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple double property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No multiple double property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), new Value[]{valueSatisfied}); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(Value value) + try { + prop.setValue(new Value[]{valueNotSatisfied}); + node.save(); + fail("setValue(Value[] values) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(Value[] values) where values are of type LongValue + * throw a ConstraintViolationException if the change would violate a value + * constraint + */ + public void testMultipleLongProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.LONG, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple long property def with " + + "testable value constraints has been found"); + } + + // find a Value that does not satisfy the ValueConstraints of propDef + Value valueNotSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (valueNotSatisfied == null) { + throw new NotExecutableException("No multiple long property def with " + + "testable value constraints has been found"); + } + + // find a Value that does satisfy the ValueConstraints of propDef + Value valueSatisfied = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, true); + if (valueSatisfied == null) { + throw new NotExecutableException("The value constraints do not allow any value."); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + prop = node.setProperty(propDef.getName(), new Value[]{valueSatisfied}); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(Value value) + try { + prop.setValue(new Value[]{valueNotSatisfied}); + node.save(); + fail("setValue(Value value) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * Tests if setValue(Value[] values) where values are of type ReferenceValue + * throw a ConstraintViolationException if the change would violate a value + * constraint + */ + public void testMultipleReferenceProperty() + throws NotExecutableException, RepositoryException { + + // locate a PropertyDefinition with ValueConstraints + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(superuser, PropertyType.REFERENCE, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple reference property def with " + + "testable value constraints has been found"); + } + + String valueConstraints[] = propDef.getValueConstraints(); + if (valueConstraints == null || valueConstraints.length == 0) { + throw new NotExecutableException("No reference property def with " + + "testable value constraints has been found"); + } + List constraints = Arrays.asList(valueConstraints); + String nodeTypeSatisfied = constraints.get(0); + String nodeTypeNotSatisfied = null; + + NodeTypeManager manager = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeIterator types = manager.getAllNodeTypes(); + + // find a NodeType which is not satisfying the constraints + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + String name = type.getName(); + if (constraints.contains(name) || ntFrozenNode.equals(name)) { + continue; + } + if (type.getChildNodeDefinitions() != null + && type.getChildNodeDefinitions().length > 0) { + continue; + } + nodeTypeNotSatisfied = name; + break; + } + + if (nodeTypeNotSatisfied == null) { + throw new NotExecutableException("No reference property def with " + + "testable value constraints has been found"); + } + + // create a sub node of testRootNode of type propDef.getDeclaringNodeType() + // and add a property with constraints to this node + Node node; + Property prop; + Node nodeSatisfied; + Node nodeNotSatisfied; + try { + String nodeType = propDef.getDeclaringNodeType().getName(); + node = testRootNode.addNode(nodeName2, nodeType); + + // create a referenceable node satisfying the constraint + nodeSatisfied = testRootNode.addNode(nodeName3, nodeTypeSatisfied); + ensureMixinType(nodeSatisfied, mixReferenceable); + + // create a referenceable node not satisfying the constraint + nodeNotSatisfied = testRootNode.addNode(nodeName4, nodeTypeNotSatisfied); + ensureMixinType(nodeNotSatisfied, mixReferenceable); + + // some implementations may require a save after addMixin() + testRootNode.getSession().save(); + + Value valueSatisfied = superuser.getValueFactory().createValue(nodeSatisfied); + prop = node.setProperty(propDef.getName(), new Value[]{valueSatisfied}); + testRootNode.getSession().save(); + } catch (ConstraintViolationException e) { + // implementation specific constraints do not allow to set up test environment + throw new NotExecutableException("Not able to create required test items."); + } + + // test of signature setValue(Value value) + try { + Value valueNotSatisfied = superuser.getValueFactory().createValue(nodeNotSatisfied); + prop.setValue(new Value[]{valueNotSatisfied}); + node.save(); + fail("setValue(Value[] values) must throw a ConstraintViolationException " + + "if the change would violate a node type constraint " + + "either immediately or on save"); + } catch (ConstraintViolationException e) { + // success + } + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/SetValueDateTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDateTest.java similarity index 75% rename from src/test/org/apache/jackrabbit/test/api/SetValueDateTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDateTest.java index 7631384f143..17f06f1466f 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetValueDateTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDateTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,7 +21,6 @@ import javax.jcr.Property; import javax.jcr.Value; import javax.jcr.RepositoryException; -import javax.jcr.DateValue; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import java.util.GregorianCalendar; @@ -29,23 +28,21 @@ /** * Tests the various {@link Property#setValue(Value)} methods. - *

        - * Configuration requirements:
        The node at {@link #testRoot} must allow a + *

        + * Configuration requirements: + *

        + * The node at {@link #testRoot} must allow a * child node of type {@link #testNodeType} with name {@link #nodeName1}. The * node type {@link #testNodeType} must define a single value date property * with name {@link #propertyName1}. * - * @test - * @sources SetValueDateTest.java - * @executeClass org.apache.jackrabbit.test.api.SetValueDateTest - * @keywords level2 */ public class SetValueDateTest extends AbstractJCRTest { /** * The date value */ - private DateValue value; + private Value value; /** * The node with the date property @@ -61,7 +58,7 @@ protected void setUp() throws Exception { super.setUp(); // initialize some date value - value = new DateValue(GregorianCalendar.getInstance()); + value = superuser.getValueFactory().createValue(GregorianCalendar.getInstance()); // create a new node under the testRootNode node = testRootNode.addNode(nodeName1, testNodeType); @@ -73,6 +70,13 @@ protected void setUp() throws Exception { superuser.save(); } + protected void tearDown() throws Exception { + value = null; + node = null; + property1 = null; + super.tearDown(); + } + /** * Test the persistence of a property modified with an DateValues parameter * and saved from the Session @@ -80,7 +84,7 @@ protected void setUp() throws Exception { public void testDateSession() throws RepositoryException { property1.setValue(value); superuser.save(); - assertEquals("Date node property not saved", value.getDate(), property1.getValue().getDate()); + assertEquals("Date node property not saved", value, property1.getValue()); } /** @@ -90,7 +94,8 @@ public void testDateSession() throws RepositoryException { public void testDateParent() throws RepositoryException { property1.setValue(value.getDate()); node.save(); - assertEquals("Date node property not saved", value.getDate(), property1.getValue().getDate()); + Value orig = superuser.getValueFactory().createValue(value.getDate()); + assertEquals("Date node property not saved", orig, property1.getValue()); } /** @@ -98,7 +103,7 @@ public void testDateParent() throws RepositoryException { * the parent Node */ public void testRemoveDateParent() throws RepositoryException { - property1.setValue((DateValue) null); + property1.setValue((Value) null); node.save(); try { diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDecimalTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDecimalTest.java new file mode 100644 index 00000000000..dbbb49becaf --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDecimalTest.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.test.api; + +import java.math.BigDecimal; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Property; +import javax.jcr.Value; +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; + +/** + * Tests the various {@link Property#setValue(Value)} methods. + *

        + * Configuration requirements: + *

        + * The node at {@link #testRoot} must allow a + * child node of type {@link #testNodeType} with name {@link #nodeName1}. The + * node type {@link #testNodeType} must define a single value decimal property + * with name {@link #propertyName1}. + * + */ +public class SetValueDecimalTest extends AbstractJCRTest { + + /** + * The decimal value + */ + private Value value; + + /** + * The node with the decimal property + */ + private Node node; + + /** + * The decimal property + */ + private Property property1; + + protected void setUp() throws Exception { + super.setUp(); + + // initialize some decimal value + value = superuser.getValueFactory().createValue(new BigDecimal("457841848484454646544884.484984949849498489771174")); + + // create a new node under the testRootNode + node = testRootNode.addNode(nodeName1, testNodeType); + + // abort test if the repository does not allow setting + // decimal properties on this node + ensureCanSetProperty(node, propertyName1, node.getSession().getValueFactory().createValue(new BigDecimal(0))); + + // create a new single-value property and save it + property1 = node.setProperty(propertyName1, new BigDecimal(0)); + superuser.save(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + /** + * Test the persistence of a property modified with an decimal parameter and + * saved from the Session + */ + public void testDoubleValueSession() throws RepositoryException { + property1.setValue(value); + superuser.save(); + assertEquals("Decimal node property not saved", value.getDecimal(), property1.getValue().getDecimal()); + } + + /** + * Test the persistence of a property modified with an decimal parameter and + * saved from the Session + */ + public void testDoubleSession() throws RepositoryException { + property1.setValue(value.getDecimal()); + superuser.save(); + assertEquals("Decimal node property not saved", value.getDecimal(), property1.getValue().getDecimal()); + } + + /** + * Test the deletion of a property by assigning it a null value, saved from + * the Session + */ + public void testRemoveDoubleSession() throws RepositoryException { + property1.setValue((Value) null); + superuser.save(); + + try { + node.getProperty(propertyName1); + fail("The property should not exist anymore, as a null Decimal has been assigned"); + } catch (PathNotFoundException e) { + //success : the property has been deleted by assigning it a null value + } + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/SetValueDoubleTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDoubleTest.java similarity index 75% rename from src/test/org/apache/jackrabbit/test/api/SetValueDoubleTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDoubleTest.java index 60c54a8b2d1..f49f0a96022 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetValueDoubleTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueDoubleTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,29 +21,26 @@ import javax.jcr.Property; import javax.jcr.Value; import javax.jcr.RepositoryException; -import javax.jcr.DoubleValue; import javax.jcr.Node; import javax.jcr.PathNotFoundException; /** * Tests the various {@link Property#setValue(Value)} methods. - *

        - * Configuration requirements:
        The node at {@link #testRoot} must allow a + *

        + * Configuration requirements: + *

        + * The node at {@link #testRoot} must allow a * child node of type {@link #testNodeType} with name {@link #nodeName1}. The * node type {@link #testNodeType} must define a single value double property * with name {@link #propertyName1}. * - * @test - * @sources SetValueDoubleTest.java - * @executeClass org.apache.jackrabbit.test.api.SetValueDoubleTest - * @keywords level2 */ public class SetValueDoubleTest extends AbstractJCRTest { /** * The double value */ - private DoubleValue value; + private Value value; /** * The node with the double property @@ -59,16 +56,27 @@ protected void setUp() throws Exception { super.setUp(); // initialize some double value - value = new DoubleValue(93845.94d); + value = superuser.getValueFactory().createValue(93845.94d); // create a new node under the testRootNode node = testRootNode.addNode(nodeName1, testNodeType); + // abort test if the repository does not allow setting + // double properties on this node + ensureCanSetProperty(node, propertyName1, node.getSession().getValueFactory().createValue(0.0d)); + // create a new single-value property and save it property1 = node.setProperty(propertyName1, 0.0d); superuser.save(); } + protected void tearDown() throws Exception { + value = null; + node = null; + property1 = null; + super.tearDown(); + } + /** * Test the persistence of a property modified with an double parameter and * saved from the Session @@ -94,7 +102,7 @@ public void testDoubleParent() throws RepositoryException { * the Session */ public void testRemoveDoubleSession() throws RepositoryException { - property1.setValue((DoubleValue) null); + property1.setValue((Value) null); superuser.save(); try { @@ -110,7 +118,7 @@ public void testRemoveDoubleSession() throws RepositoryException { * the parent Node */ public void testRemoveDoubleParent() throws RepositoryException { - property1.setValue((DoubleValue) null); + property1.setValue((Value) null); node.save(); try { diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueInputStreamTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueInputStreamTest.java new file mode 100644 index 00000000000..ffb42a7d58b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueInputStreamTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.api.util.InputStreamWrapper; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * Tests the {@link javax.jcr.Property#setValue(java.io.InputStream)} method. + *

        + * Configuration requirements: + *

        + * The node at {@link #testRoot} must allow a + * child node of type {@link #testNodeType} with name {@link #nodeName1}. The + * node type {@link #testNodeType} must define a single value binary property + * with name {@link #propertyName1}. As a special case, if the specified node + * type automatically adds a jcr:content child node of type nt:resource, and + * propertyName1 is specified as "jcr:data", that binary property + * is used instead. + * + */ +public class SetValueInputStreamTest extends AbstractJCRTest { + + /** + * The binary data + */ + private byte[] data; + + /** + * The node with the binary property + */ + private Node node; + + /** + * The binary property + */ + private Property property1; + + protected void setUp() throws Exception { + super.setUp(); + + // initialize some binary value + data = createRandomString(10).getBytes(); + + // create a new node under the testRootNode + node = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + + // special case for repositories that do allow binary property + // values, but only on jcr:content/jcr:data + if (propertyName1.equals("jcr:data") && node.hasNode("jcr:content") + && node.getNode("jcr:content").isNodeType("nt:resource") && ! node.hasProperty("jcr:data")) { + node = node.getNode("jcr:content"); + } + + // create a new single-value property and save it + property1 = node.setProperty(propertyName1, superuser.getValueFactory().createValue(new ByteArrayInputStream(new byte[0]))); + superuser.save(); + } + + protected void tearDown() throws Exception { + node = null; + property1 = null; + super.tearDown(); + } + + /** + * Tests whether Property.setValue(InputStream) obeys the + * stream handling contract. + */ + public void testInputStreamClosed() throws RepositoryException, IOException { + InputStreamWrapper in = new InputStreamWrapper(new ByteArrayInputStream(data)); + property1.setValue(in); + assertTrue("Property.setValue(InputStream) is expected to close the passed input stream", in.isClosed()); + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/SetValueLongTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueLongTest.java similarity index 79% rename from src/test/org/apache/jackrabbit/test/api/SetValueLongTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueLongTest.java index 4814b90733f..52b7b59593b 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetValueLongTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueLongTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,29 +21,26 @@ import javax.jcr.Property; import javax.jcr.Value; import javax.jcr.RepositoryException; -import javax.jcr.LongValue; import javax.jcr.Node; import javax.jcr.PathNotFoundException; /** * Tests the various {@link Property#setValue(Value)} methods. - *

        - * Configuration requirements:
        The node at {@link #testRoot} must allow a + *

        + * Configuration requirements: + *

        + * The node at {@link #testRoot} must allow a * child node of type {@link #testNodeType} with name {@link #nodeName1}. The * node type {@link #testNodeType} must define a single value long property * with name {@link #propertyName1}. * - * @test - * @sources SetValueLongTest.java - * @executeClass org.apache.jackrabbit.test.api.SetValueLongTest - * @keywords level2 */ public class SetValueLongTest extends AbstractJCRTest { /** * The long value */ - private LongValue value; + private Value value; /** * The node with the long property @@ -59,7 +56,7 @@ protected void setUp() throws Exception { super.setUp(); // initialize some long value - value = new LongValue(73057230); + value = superuser.getValueFactory().createValue(73057230); // create a new node under the testRootNode node = testRootNode.addNode(nodeName1, testNodeType); @@ -69,6 +66,13 @@ protected void setUp() throws Exception { superuser.save(); } + protected void tearDown() throws Exception { + value = null; + node = null; + property1 = null; + super.tearDown(); + } + /** * Test the persistence of a property modified with an LongValue parameter * and saved from the Session @@ -94,7 +98,7 @@ public void testLongParent() throws RepositoryException { * the parent Node */ public void testRemoveLongParent() throws RepositoryException { - property1.setValue((LongValue) null); + property1.setValue((Value) null); node.save(); try { @@ -110,7 +114,7 @@ public void testRemoveLongParent() throws RepositoryException { * the Session */ public void testRemoveLongSession() throws RepositoryException { - property1.setValue((LongValue) null); + property1.setValue((Value) null); superuser.save(); try { diff --git a/src/test/org/apache/jackrabbit/test/api/SetValueReferenceTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueReferenceTest.java similarity index 82% rename from src/test/org/apache/jackrabbit/test/api/SetValueReferenceTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueReferenceTest.java index 5dea946022a..a315c91c694 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetValueReferenceTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueReferenceTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -23,30 +23,27 @@ import javax.jcr.Value; import javax.jcr.RepositoryException; import javax.jcr.Node; -import javax.jcr.ReferenceValue; import javax.jcr.PathNotFoundException; /** * Tests the various {@link Property#setValue(Value)} methods. - *

        - * Configuration requirements:
        The node at {@link #testRoot} must allow a + *

        + * Configuration requirements: + *

        + * The node at {@link #testRoot} must allow a * child node of type {@link #testNodeType} with name {@link #nodeName1}. The * node type {@link #testNodeType} must define a single value reference property * with name {@link #propertyName1}. The node type {@link #testNodeType} must * be referenceable or allow to add a mix:referenceable, otherwise a * {@link NotExecutableException} is thrown. * - * @test - * @sources SetValueReferenceTest.java - * @executeClass org.apache.jackrabbit.test.api.SetValueReferenceTest - * @keywords level2 */ public class SetValueReferenceTest extends AbstractJCRTest { /** * The reference value */ - private ReferenceValue value; + private Value value; /** * The node with the reference property @@ -70,13 +67,20 @@ protected void setUp() throws Exception { ensureReferenceable(node); // initialize some reference value - value = new ReferenceValue(node); + value = superuser.getValueFactory().createValue(node); // create a new single-value property and save it property1 = node.setProperty(propertyName1, value); superuser.save(); } + protected void tearDown() throws Exception { + value = null; + node = null; + property1 = null; + super.tearDown(); + } + /** * Test the persistence of a property modified with an Node parameter and * saved from the Session Requires a Node value (node) @@ -102,7 +106,7 @@ public void testNodeParent() throws RepositoryException { * the Session */ public void testRemoveNodeSession() throws RepositoryException { - property1.setValue((ReferenceValue) null); + property1.setValue((Value) null); superuser.save(); try { @@ -144,13 +148,7 @@ public void testRemoveNodeParent() throws RepositoryException { * referenceable nodes. */ private void ensureReferenceable(Node n) throws RepositoryException, NotExecutableException { - if (n.isNodeType(mixReferenceable)) { - return; - } - if (n.canAddMixin(mixReferenceable)) { - n.addMixin(mixReferenceable); - } else { - throw new NotExecutableException("Node is not referenceable: " + n.getPath()); - } + ensureMixinType(n, mixReferenceable); + n.getSession().save(); } } diff --git a/src/test/org/apache/jackrabbit/test/api/SetValueStringTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueStringTest.java similarity index 93% rename from src/test/org/apache/jackrabbit/test/api/SetValueStringTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueStringTest.java index 4ed34f2849a..d308edb5f66 100644 --- a/src/test/org/apache/jackrabbit/test/api/SetValueStringTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueStringTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,7 +21,6 @@ import javax.jcr.Property; import javax.jcr.Value; import javax.jcr.Node; -import javax.jcr.StringValue; import javax.jcr.RepositoryException; import javax.jcr.ValueFormatException; import javax.jcr.PathNotFoundException; @@ -31,18 +30,15 @@ /** * Tests the various {@link Property#setValue(Value)} methods. - *

        - * Configuration requirements:
        + *

        + * Configuration requirements: + *

        * The node at {@link #testRoot} must allow a child node of type * {@link #testNodeType} with name {@link #nodeName1}. The node type * {@link #testNodeType} must define a single value string property with * name {@link #propertyName1} and a multi value string property with name * {@link #propertyName2}. * - * @test - * @sources SetValueStringTest.java - * @executeClass org.apache.jackrabbit.test.api.SetValueStringTest - * @keywords level2 */ public class SetValueStringTest extends AbstractJCRTest { @@ -59,8 +55,8 @@ protected void setUp() throws Exception { super.setUp(); // initialize some multi-value properties - sv1 = new StringValue(PROP_VALUE_1); - sv2 = new StringValue(PROP_VALUE_2); + sv1 = superuser.getValueFactory().createValue(PROP_VALUE_1); + sv2 = superuser.getValueFactory().createValue(PROP_VALUE_2); mv1 = new Value[]{sv1}; mv2 = new Value[]{sv1, sv2}; @@ -77,6 +73,17 @@ protected void setUp() throws Exception { superuser.save(); } + protected void tearDown() throws Exception { + property1 = null; + property2 = null; + node = null; + sv1 = null; + sv2 = null; + mv1 = null; + mv2 = null; + super.tearDown(); + } + // Value tests /** @@ -95,7 +102,7 @@ public void testValueSession() throws RepositoryException { */ public void testValueParent() throws RepositoryException { property1.setValue(sv2); - testRootNode.save(); + testRootNode.getSession().save(); assertEquals("Value node property not saved", sv2, property1.getValue()); } @@ -310,7 +317,7 @@ public void testMultiStringSession() throws RepositoryException { property2.setValue(mv); superuser.save(); Value[] values = property2.getValues(); - List strValues = new ArrayList(); + List strValues = new ArrayList(); for (int i = 0; i < values.length; i++) { strValues.add(values[i].getString()); } @@ -327,7 +334,7 @@ public void testMultiStringParent() throws RepositoryException { property2.setValue(mv); node.save(); Value[] values = property2.getValues(); - List strValues = new ArrayList(); + List strValues = new ArrayList(); for (int i = 0; i < values.length; i++) { strValues.add(values[i].getString()); } @@ -402,4 +409,4 @@ public void testEmptyMultiStringSession() throws RepositoryException { assertEquals("Property.setValue(emptyStringArray) did not set the property to an empty array", 0, property2.getValues().length); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueValueFormatExceptionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueValueFormatExceptionTest.java new file mode 100644 index 00000000000..4163fac5e9f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueValueFormatExceptionTest.java @@ -0,0 +1,302 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.nodetype.NodeTypeUtil; + +import javax.jcr.PropertyType; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.RepositoryException; + +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.util.Calendar; + +/** + * SetValueValueFormatExceptionTest tests if Property.setValue() throws + * a ValueFormatException if a best-effort conversion fails. + * The ValueFormatException has to be thrown immediately (not on save). + * + */ +public class SetValueValueFormatExceptionTest extends AbstractJCRTest { + + /** + * Tests if setValue(Value) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testValue() + throws NotExecutableException, RepositoryException { + + Property booleanProperty = createProperty(PropertyType.BOOLEAN, false); + try { + Value dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + booleanProperty.setValue(dateValue); + fail("Property.setValue(Value) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(Value[]) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testValueArray() + throws NotExecutableException, RepositoryException { + + Property booleanProperty = createProperty(PropertyType.BOOLEAN, true); + try { + Value dateValues[] = + new Value[]{NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE)}; + booleanProperty.setValue(dateValues); + fail("Property.setValue(Value[]) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(String) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testString() + throws NotExecutableException, RepositoryException { + + Property dateProperty = createProperty(PropertyType.DATE, false); + try { + dateProperty.setValue("abc"); + fail("Property.setValue(String) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(String[]) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testStringArray() + throws NotExecutableException, RepositoryException { + + Property dateProperty = createProperty(PropertyType.DATE, true); + try { + String values[] = new String[]{"abc"}; + dateProperty.setValue(values); + fail("Property.setValue(String[]) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(InputStream) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testInputStream() + throws NotExecutableException, RepositoryException { + + Property dateProperty = createProperty(PropertyType.DATE, false); + try { + byte[] bytes = {123}; + InputStream value = new ByteArrayInputStream(bytes); + dateProperty.setValue(value); + fail("Property.setValue(InputStream) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(long) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testLong() + throws NotExecutableException, RepositoryException { + + Property booleanProperty = createProperty(PropertyType.BOOLEAN, false); + try { + booleanProperty.setValue(123); + fail("Property.setValue(long) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(double) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testDouble() + throws NotExecutableException, RepositoryException { + + Property booleanProperty = createProperty(PropertyType.BOOLEAN, false); + try { + booleanProperty.setValue(1.23); + fail("Property.setValue(double) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(Calendar) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testCalendar() + throws NotExecutableException, RepositoryException { + + Property booleanProperty = createProperty(PropertyType.BOOLEAN, false); + try { + booleanProperty.setValue(Calendar.getInstance()); + fail("Property.setValue(Calendar) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(boolean) throws a ValueFormatException immediately (not + * on save) if a conversion fails. + */ + public void testBoolean() + throws NotExecutableException, RepositoryException { + + Property dateProperty = createProperty(PropertyType.DATE, false); + try { + dateProperty.setValue(true); + fail("Property.setValue(boolean) must throw a ValueFormatException " + + "immediately if a conversion fails."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(Node) throws a ValueFormatException immediately (not + * on save) if the property is not of type REFERENCE. + */ + public void testNode() + throws NotExecutableException, RepositoryException { + + Property booleanProperty = createProperty(PropertyType.BOOLEAN, false); + try { + Node referenceableNode = testRootNode.addNode(nodeName3, testNodeType); + if (needsMixin(referenceableNode, mixReferenceable)) { + ensureMixinType(referenceableNode, mixReferenceable); + } + + // some implementations may require a save after addMixin() + testRootNode.getSession().save(); + + // make sure the node is now referenceable + assertTrue("test node should be mix:referenceable", referenceableNode.isNodeType(mixReferenceable)); + + booleanProperty.setValue(referenceableNode); + fail("Property.setValue(Node) must throw a ValueFormatException " + + "immediately if the property is not of type REFERENCE."); + } + catch (ValueFormatException e) { + // success + } + } + + /** + * Tests if setValue(Node) throws a ValueFormatException immediately (not + * on save) if the specified node is not referencable. + */ + public void testNodeNotReferenceable() + throws NotExecutableException, RepositoryException { + + if (testRootNode.isNodeType(mixReferenceable)) { + throw new NotExecutableException("test requires testRootNode to be non-referenceable"); + } + + Property referenceProperty = createProperty(PropertyType.REFERENCE, false); + try { + referenceProperty.setValue(testRootNode); + fail("Property.setValue(Node) must throw a ValueFormatException " + + "immediately if the specified node is not referenceable."); + } catch (ValueFormatException e) { + // success + } + } + + /** + * Creates a node under {@link #testRootNode} and sets a property on with + * propertyType on the newly created node. + * + * @param propertyType the type of the property to create. + * @param multiple if the property must support multiple values. + * @return the property + * @throws RepositoryException if an error occurs + * @throws NotExecutableException if there is no such property defined on + * the node type for the new child node. + */ + private Property createProperty(int propertyType, boolean multiple) throws RepositoryException, NotExecutableException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + if (propertyType == PropertyType.REFERENCE && !n.isNodeType(mixReferenceable)) { + if (!n.canAddMixin(mixReferenceable)) { + throw new NotExecutableException(testNodeType + " does not support adding of mix:referenceable"); + } else { + n.addMixin(mixReferenceable); + // some implementations may require a save after addMixin() + testRootNode.getSession().save(); + } + } + + Value initValue; + if (propertyType == PropertyType.REFERENCE) { + initValue = superuser.getValueFactory().createValue(n); + } else { + initValue = NodeTypeUtil.getValueOfType(superuser, propertyType); + } + + if (multiple) { + Value[] initValues = new Value[]{initValue}; + if (!n.getPrimaryNodeType().canSetProperty(propertyName1, initValues)) { + + throw new NotExecutableException("Node type: " + testNodeType + + " does not support a multi valued " + + PropertyType.nameFromValue(propertyType) + " property " + + "called: " + propertyName1); + } + return n.setProperty(propertyName1, initValues); + } else { + if (!n.getPrimaryNodeType().canSetProperty(propertyName1, initValue)) { + + throw new NotExecutableException("Node type: " + testNodeType + + " does not support a single valued " + + PropertyType.nameFromValue(propertyType) + " property " + + "called: " + propertyName1); + } + return n.setProperty(propertyName1, initValue); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueVersionExceptionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueVersionExceptionTest.java new file mode 100644 index 00000000000..1ecfb16844b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SetValueVersionExceptionTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Session; +import javax.jcr.Property; +import javax.jcr.Node; +import javax.jcr.Value; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionException; +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.util.Calendar; + +/** + * SetValueVersionExceptionTest... + * + */ +public class SetValueVersionExceptionTest extends AbstractJCRTest { + + /** + * The session we use for the tests + */ + private Session session; + + private Node node; + + private Property property; + private Property multiProperty; + + private Value initialValue; + private Value[] initialValues; + private Value modifiedValue; + private Value[] modifiedValues; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + super.setUp(); + session = getHelper().getReadOnlySession(); + + initialValue = session.getValueFactory().createValue("abc"); + modifiedValue = session.getValueFactory().createValue("def"); + initialValues = new Value[] {initialValue}; + modifiedValues = new Value[] {initialValue, modifiedValue}; + + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { + throw new NotExecutableException("Versioning is not supported."); + } + + // create a node that is versionable + node = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it versionable if it is not + ensureMixinType(node, mixVersionable); + + property = node.setProperty(propertyName1, initialValue); + multiProperty = node.setProperty(propertyName2, initialValues); + + testRootNode.getSession().save(); + + node.checkin(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + try { + superuser.refresh(false); + node.checkout(); + } finally { + if (session != null) { + session.logout(); + session = null; + } + node = null; + property = null; + multiProperty = null; + initialValue = null; + initialValues = null; + modifiedValue = null; + modifiedValues = null; + super.tearDown(); + } + } + + /** + * Tests if setValue(Value) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testValue() throws RepositoryException { + try { + property.setValue(modifiedValue); + node.save(); + fail("Property.setValue(Value) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(Value[]) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testValueArray() throws RepositoryException { + try { + multiProperty.setValue(modifiedValues); + node.save(); + fail("Property.setValue(Value[]) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(String) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testString() throws RepositoryException { + try { + property.setValue(modifiedValue.getString()); + node.save(); + fail("Property.setValue(String) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(String[]) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testStringArray() throws RepositoryException { + try { + String values[] = new String[0]; + multiProperty.setValue(values); + node.save(); + fail("Property.setValue(String[]) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(InputStream) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testInputStream() throws RepositoryException { + try { + byte[] bytes = {123}; + InputStream value = new ByteArrayInputStream(bytes); + property.setValue(value); + node.save(); + fail("Property.setValue(InputStream) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(long) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testLong() throws RepositoryException { + try { + property.setValue(123); + node.save(); + fail("Property.setValue(long) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(double) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testDouble() throws RepositoryException { + try { + property.setValue(1.23); + node.save(); + fail("Property.setValue(double) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(Calendar) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testCalendar() throws RepositoryException { + try { + property.setValue(Calendar.getInstance()); + node.save(); + fail("Property.setValue(Calendar) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(boolean) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + */ + public void testBoolean() throws RepositoryException { + try { + property.setValue(true); + node.save(); + fail("Property.setValue(boolean) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + } + + /** + * Tests if setValue(Node) throws a VersionException immediately + * or on save if the parent node of this property is checked-in. + *

          + *
        • {@code nodetype2} name of a node type with a reference property + *
        • {@code propertyname3} name of a single value reference property + * declared in nodetype2 + *
        + */ + public void testNode() + throws NotExecutableException, RepositoryException { + + String nodeType3 = getProperty("nodetype3"); + + // create a referenceable node + Node referenceableNode = (nodeType3 == null) + ? testRootNode.addNode(nodeName3) + : testRootNode.addNode(nodeName3, nodeType3); + + // try to make it referenceable if it is not + ensureMixinType(referenceableNode, mixReferenceable); + + // implementation specific if mixin takes effect immediately or upon save + testRootNode.getSession().save(); + + String refPropName = getProperty("propertyname3"); + String nodeType = getProperty("nodetype2"); + + Node node = testRootNode.addNode(nodeName4, nodeType); + + // try to make it versionable if it is not + ensureMixinType(node, mixVersionable); + + // fail early when reference properties are not suppoerted + ensureCanSetProperty(node, refPropName, node.getSession().getValueFactory().createValue(referenceableNode)); + + Property property = node.setProperty(refPropName, referenceableNode); + testRootNode.getSession().save(); + + node.checkin(); + + try { + property.setValue(node); + node.save(); + fail("Property.setValue(Node) must throw a VersionException " + + "immediately or on save if the parent node of this property " + + "is checked-in."); + } + catch (VersionException e) { + // success + } + + superuser.refresh(false); + node.checkout(); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ShareableNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ShareableNodeTest.java new file mode 100644 index 00000000000..08ce8fe944e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ShareableNodeTest.java @@ -0,0 +1,1504 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +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.util.ArrayList; +import java.util.List; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Workspace; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * Tests features available with shareable nodes. + */ +public class ShareableNodeTest extends AbstractJCRTest { + + protected void setUp() throws Exception { + super.setUp(); + try { + checkSupportedOption(Repository.OPTION_SHAREABLE_NODES_SUPPORTED); + ensureKnowsNodeType(superuser, mixShareable); + } catch (NotExecutableException e) { + cleanUp(); + throw e; + } + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + //------------------------------------------------------ specification tests + + /** + * Verify that Node.getIndex returns the correct index in a shareable + * node (6.13). + */ + public void testGetIndex() throws Exception { + + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + a2.addNode("b"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + + // verify indices of nodes b1/b2 in shared set + assertEquals(1, b1.getIndex()); + assertEquals(2, b2.getIndex()); + } + + /** + * Verify that Node.getName returns the correct name in a shareable node + * (6.13). + */ + public void testGetName() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + + // verify names of nodes b1/b2 in shared set + assertEquals("b1", b1.getName()); + assertEquals("b2", b2.getName()); + } + + /** + * Verify that Node.getPath returns the correct path in a shareable + * node (6.13). + */ + public void testGetPath() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + + // verify paths of nodes b1/b2 in shared set + String testRootNodePath = testRootNode.getPath(); + assertEquals(testRootNodePath + "/a1/b1", b1.getPath()); + assertEquals(testRootNodePath + "/a2/b2", b2.getPath()); + } + + /** + * Verify that the shareable node returned by Node.getNode() has the right + * name. + */ + public void testGetNode() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // a1.getNode("b1") should return b1 + b1 = a1.getNode("b1"); + assertEquals("b1", b1.getName()); + + // a2.getNode("b2") should return b2 + Node b2 = a2.getNode("b2"); + assertEquals("b2", b2.getName()); + } + + /** + * Verify that the shareable nodes returned by Node.getNodes() have + * the right name. + */ + public void testGetNodes() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // a1.getNodes() should return b1 + Node[] children = toArray(a1.getNodes()); + assertEquals(1, children.length); + assertEquals("b1", children[0].getName()); + + // a2.getNodes() should return b2 + children = toArray(a2.getNodes()); + assertEquals(1, children.length); + assertEquals("b2", children[0].getName()); + } + + /** + * Verify that the shareable nodes returned by Node.getNodes(String) have + * the right name. + */ + public void testGetNodesByPattern() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // a1.getNodes(*) should return b1 + Node[] children = toArray(a1.getNodes("*")); + assertEquals(1, children.length); + assertEquals("b1", children[0].getName()); + + // a2.getNodes(*) should return b2 + children = toArray(a2.getNodes("*")); + assertEquals(1, children.length); + assertEquals("b2", children[0].getName()); + } + + /** + * Check new API Node.getSharedSet() (6.13.1) + */ + public void testGetSharedSet() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // verify shared set contains 2 items + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + } + + /** + * Add the mix:shareable mixin to a node (6.13.2). + */ + public void testAddMixin() throws Exception { + // setup parent node and first child + Node a = testRootNode.addNode("a"); + Node b = a.addNode("b"); + testRootNode.getSession().save(); + + ensureMixinType(b, mixShareable); + b.save(); + } + + /** + * Create a shareable node by restoring it (6.13.3). + */ + public void testRestore() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // make b1 shareable + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // make a2 versionable + ensureMixinType(a2, mixVersionable); + a2.save(); + + // check in version and check out again + Version v = a2.checkin(); + a2.checkout(); + + // delete b2 and save + a2.getNode("b2").remove(); + a2.save(); + + // verify shared set contains one element only + Node[] shared = getSharedSet(b1); + assertEquals(1, shared.length); + + // restore version + a2.restore(v, false); + + // verify shared set contains again two elements + shared = getSharedSet(b1); + assertEquals(2, shared.length); + } + + + /** + * Check new API Node.removeShare() (6.13.4). + */ + public void testRemoveShare() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + + // remove b1 from shared set + b1.removeShare(); + a1.save(); + + // verify shared set of b2 contains only 1 item, namely b2 itself + shared = getSharedSet(b2); + assertEquals(1, shared.length); + assertTrue(shared[0].isSame(b2)); + } + + /** + * Check new API Node.removeSharedSet() (6.13.4). + */ + public void testRemoveSharedSet() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // remove shared set + b1.removeSharedSet(); + testRootNode.getSession().save(); + + // verify neither a1 nor a2 contain any more children + assertFalse(a1.hasNodes()); + assertFalse(a2.hasNodes()); + } + + /** + * Invoke Node.removeSharedSet(), but save only one of the parent nodes + * of the shared set. This doesn't need to be supported according to the + * specification (6.13.4). + */ + public void testRemoveSharedSetSaveOneParentOnly() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // remove shared set + b1.removeSharedSet(); + + try { + // save only one of the parents, should fail + a1.save(); + fail("Removing a shared set requires saving all parents."); + } catch (ConstraintViolationException e) { + // expected + } + } + + /** + * Verify that shareable nodes in the same shared set have the same + * jcr:uuid (6.13.10). + */ + public void testSameUUID() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + + // verify nodes in a shared set have the same jcr:uuid + assertTrue(b1.getUUID().equals(b2.getUUID())); + } + + /** + * Add a child to a shareable node and verify that another node in the + * same shared set has the same child and is modified when the first + * one is (6.13.11). + */ + public void testAddChild() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + + // add node to b1, verify b2 is modified as well and contains that child + b1.addNode("c"); + assertTrue(b2.isModified()); + assertTrue(b2.hasNode("c")); + b1.save(); + } + + /** + * Copy a subtree that contains shareable nodes. Verify that the nodes + * newly created are not in the shared set that existed before the copy, + * but if two nodes in the source of a copy are in the same shared set, then + * the two corresponding nodes in the destination of the copy must also be + * in the same shared set (6.13.12). + */ + public void testCopy() throws Exception { + // setup parent node and first child + Node s = testRootNode.addNode("s"); + Node a1 = s.addNode("a1"); + Node a2 = s.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // copy source tree to destination + workspace.copy(s.getPath(), testRootNode.getPath() + "/d"); + + // verify source contains shared set with 2 entries + Node[] shared1 = getSharedSet(b1); + assertEquals(2, shared1.length); + + // verify destination contains shared set with 2 entries + Node[] shared2 = getSharedSet(testRootNode.getNode("d/a1/b1")); + assertEquals(2, shared2.length); + + // verify elements in source shared set and destination shared set + // don't have the same UUID + String srcUUID = shared1[0].getUUID(); + String destUUID = shared2[0].getUUID(); + assertFalse( + "Source and destination of a copy must not have the same UUID", + srcUUID.equals(destUUID)); + } + + /** + * Verify that a share cycle is detected (6.13.13) when a shareable node + * is cloned. + */ + public void testDetectShareCycleOnClone() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + Workspace workspace = b1.getSession().getWorkspace(); + + try { + // clone underneath b1: this must fail + workspace.clone(workspace.getName(), b1.getPath(), + b1.getPath() + "/c", false); + fail("Share cycle not detected on clone."); + } catch (RepositoryException e) { + // expected + } + } + + /** + * Verify that a share cycle is detected (6.13.13) when a node is moved. + */ + public void testDetectShareCycleOnMove() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child node + Node c = b1.addNode("c"); + b1.save(); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + + // move node + try { + workspace.move(testRootNode.getPath() + "/a2", c.getPath() + "/d"); + fail("Share cycle not detected on move."); + } catch (RepositoryException e) { + // expected + } + } + + /** + * Verify that a share cycle is detected (6.13.13) when a node is + * transiently moved. + */ + public void testDetectShareCycleOnTransientMove() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Session session = b1.getSession(); + Workspace workspace = session.getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child node + Node c = b1.addNode("c"); + b1.save(); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + + // move node + try { + session.move(testRootNode.getPath() + "/a2", c.getPath()); + fail("Share cycle not detected on transient move."); + } catch (RepositoryException e) { + // expected + } + } + + /** + * Verify export and import of a tree containing multiple nodes in the + * same shared set (6.13.14). The first serialized node in that shared + * set is serialized in the normal fashion (with all of its properties + * and children), but any subsequent shared node in that shared set is + * serialized as a special node of type nt:share, which + * contains only the jcr:uuid property of the shared node + * and the jcr:primaryType property indicating the type + * nt:share. + */ + public void testImportExportNtShare() throws Exception { + // setup parent nodes and first child + Node p = testRootNode.addNode("p"); + Node a1 = p.addNode("a1"); + Node a2 = p.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Session session = b1.getSession(); + Workspace workspace = session.getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // create temp file + File tmpFile = File.createTempFile("test", null); + tmpFile.deleteOnExit(); + + // export system view of /p + OutputStream out = new FileOutputStream(tmpFile); + try { + session.exportSystemView(p.getPath(), out, false, false); + } finally { + out.close(); + } + + // delete p and save + p.remove(); + testRootNode.getSession().save(); + + // and import again underneath test root + InputStream in = new FileInputStream(tmpFile); + try { + workspace.importXML(testRootNode.getPath(), in, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // verify shared set consists of two nodes + Node[] shared = getSharedSet(testRootNode.getNode("p/a1/b1")); + assertEquals(2, shared.length); + } + + /** + * Verify system view import via workspace (6.13.14). Export a system view + * containing a shareable node and verify, that reimporting underneath + * a different parent adds another member to the shared set and does not + * duplicate children nodes. + */ + public void testImportSystemViewCollision() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node a3 = testRootNode.addNode("a3"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Session session = b1.getSession(); + Workspace workspace = session.getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child c to shareable nodes b1 & b2 + b1.addNode("c"); + b1.save(); + + // create temp file + File tmpFile = File.createTempFile("test", null); + tmpFile.deleteOnExit(); + + // export system view of /a1/b1 + OutputStream out = new FileOutputStream(tmpFile); + try { + session.exportSystemView(b1.getPath(), out, false, false); + } finally { + out.close(); + } + + // and import again underneath /a3 + InputStream in = new FileInputStream(tmpFile); + try { + workspace.importXML(a3.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // verify there's another element in the shared set + Node[] shared = getSharedSet(b1); + assertEquals(3, shared.length); + + // verify child c has not been duplicated + Node[] children = toArray(b1.getNodes()); + assertEquals(1, children.length); + } + + /** + * Verify document view import via workspace (6.13.14). Export a document + * view containing a shareable node and verify, that reimporting + * underneath a different parent adds another member to the shared set and + * does not duplicate children nodes. + */ + public void testImportDocumentViewCollision() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node a3 = testRootNode.addNode("a3"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Session session = b1.getSession(); + Workspace workspace = session.getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child c to shareable nodes b1 & b2 + b1.addNode("c"); + b1.save(); + + // create temp file + File tmpFile = File.createTempFile("test", null); + tmpFile.deleteOnExit(); + + // export system view of /a1/b1 + OutputStream out = new FileOutputStream(tmpFile); + try { + session.exportDocumentView(b1.getPath(), out, false, false); + } finally { + out.close(); + } + + // and import again underneath /a3 + InputStream in = new FileInputStream(tmpFile); + try { + workspace.importXML(a3.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // verify there's another element in the shared set + Node[] shared = getSharedSet(b1); + assertEquals(3, shared.length); + + // verify child c has not been duplicated + Node[] children = toArray(b1.getNodes()); + assertEquals(1, children.length); + } + + /** + * Verify system view import via session (6.13.14). Export a system view + * containing a shareable node and verify, that reimporting underneath + * a different parent adds another member to the shared set and does not + * duplicate children nodes. + */ + public void testSessionImportSystemViewCollision() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node a3 = testRootNode.addNode("a3"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Session session = b1.getSession(); + Workspace workspace = session.getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child c to shareable nodes b1 & b2 + b1.addNode("c"); + b1.save(); + + // create temp file + File tmpFile = File.createTempFile("test", null); + tmpFile.deleteOnExit(); + + // export system view of /a1/b1 + OutputStream out = new FileOutputStream(tmpFile); + try { + session.exportSystemView(b1.getPath(), out, false, false); + } finally { + out.close(); + } + + // and import again underneath /a3 + InputStream in = new FileInputStream(tmpFile); + try { + session.importXML(a3.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + session.save(); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // verify there's another element in the shared set + Node[] shared = getSharedSet(b1); + assertEquals(3, shared.length); + + // verify child c has not been duplicated + Node[] children = toArray(b1.getNodes()); + assertEquals(1, children.length); + } + + /** + * Verify document view import via session (6.13.14). Export a document + * view containing a shareable node and verify, that reimporting + * underneath a different parent adds another member to the shared set and + * does not duplicate children nodes. + */ + public void testSessionImportDocumentViewCollision() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node a3 = testRootNode.addNode("a3"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Session session = b1.getSession(); + Workspace workspace = session.getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child c to shareable nodes b1 & b2 + b1.addNode("c"); + b1.save(); + + // create temp file + File tmpFile = File.createTempFile("test", null); + tmpFile.deleteOnExit(); + + // export system view of /a1/b1 + OutputStream out = new FileOutputStream(tmpFile); + try { + session.exportSystemView(b1.getPath(), out, false, false); + } finally { + out.close(); + } + + // and import again underneath /a3 + InputStream in = new FileInputStream(tmpFile); + try { + session.importXML(a3.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + session.save(); + } finally { + try { in.close(); } catch (IOException ignore) {} + } + + // verify there's another element in the shared set + Node[] shared = getSharedSet(b1); + assertEquals(3, shared.length); + + // verify child c has not been duplicated + Node[] children = toArray(b1.getNodes()); + assertEquals(1, children.length); + } + + /** + * Verify that a lock applies to all nodes in a shared set (6.13.16). + */ + public void testLock() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + ensureMixinType(a1, mixLockable); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + ensureMixinType(b1, mixLockable); + b1.save(); + + // add child c + Node c = b1.addNode("c"); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + + // lock shareable node -> all nodes in shared set are locked + b1.lock(false, true); + try { + assertTrue(b2.isLocked()); + } finally { + b1.unlock(); + } + + // deep-lock parent -> locks (common) child node + a1.lock(true, true); + try { + assertTrue(c.isLocked()); + } finally { + a1.unlock(); + } + } + + /** + * Restore a shareable node that automatically removes an existing shareable + * node (6.13.19). In this case the particular shared node is removed but + * its descendants continue to exist below the remaining members of the + * shared set. + */ + public void testRestoreRemoveExisting() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // make b1 shareable + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child c + b1.addNode("c"); + b1.save(); + + // make a2 versionable + ensureMixinType(a2, mixVersionable); + a2.save(); + + // check in version and check out again + Version v = a2.checkin(); + a2.checkout(); + + // delete b2 and save + a2.getNode("b2").remove(); + a2.save(); + + // verify shareable set contains one elements only + Node[] shared = getSharedSet(b1); + assertEquals(1, shared.length); + + // restore version and remove existing (i.e. b1) + a2.restore(v, true); + + // verify shareable set contains still one element + shared = getSharedSet(a2.getNode("b2")); + assertEquals(1, shared.length); + + // verify child c still exists + Node[] children = toArray(a2.getNode("b2").getNodes()); + assertEquals(1, children.length); + } + + /** + * Clone a mix:shareable node to the same workspace (6.13.20). Verify + * that cloning without mix:shareable fails. + */ + public void testClone() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + Workspace workspace = b1.getSession().getWorkspace(); + + try { + // clone (1st attempt, without mix:shareable, should fail) + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + fail("Cloning a node into the same workspace should fail."); + } catch (RepositoryException e) { + // expected + } + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone (2nd attempt, with mix:shareable) + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + } + + /** + * Verify that Node.isSame returns true for shareable nodes + * in the same shared set (6.13.21) + */ + public void testIsSame() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + + // verify b1 is same as b2 (and vice-versa) + assertTrue(b1.isSame(b2)); + assertTrue(b2.isSame(b1)); + } + + /** + * Remove mix:shareable from a shareable node. + */ + public void testRemoveMixin() throws Exception { + // setup parent node and first child + Node a = testRootNode.addNode("a"); + Node b = a.addNode("b"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b, mixShareable); + b.getSession().save(); + + // Removing the mixin will either succeed or will fail with a + // ConstraintViolationException + // (per Section 14.15 of JSR-283 specification) + try { + // remove mixin + b.removeMixin(mixShareable); + b.getSession().save(); + // If this happens, then b shouldn't be shareable anymore ... + assertFalse(b.isNodeType(mixShareable)); + } catch (ConstraintViolationException e) { + // one possible outcome if removing 'mix:shareable' isn't supported + } catch (UnsupportedRepositoryOperationException e) { + // also possible if the implementation doesn't support this + // capability + } + } + + /** + * Remove mix:shareable from a shareable node that has 2 nodes in the shared set. + */ + public void testRemoveMixinFromSharedNode() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.getSession().save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), a2.getPath() + "/b2", false); + + Node[] shared = getSharedSet(b1); + assertEquals(2, shared.length); + b1 = shared[0]; + Node b2 = shared[1]; + assertTrue(b2.isSame(b1)); + + // Removing the mixin will either succeed or will fail with a + // ConstraintViolationException + // (per Section 14.15 of JSR-283 specification) + try { + // remove mixin + b1.removeMixin(mixShareable); + b1.getSession().save(); + // If this happens, then b1 shouldn't be shareable anymore + // ... + assertFalse(b1.isNodeType(mixShareable)); + assertFalse(b2.isSame(b1)); + } catch (ConstraintViolationException e) { + // one possible outcome if removing 'mix:shareable' isn't supported + } catch (UnsupportedRepositoryOperationException e) { + // also possible if the implementation doesn't support this + // capability + } + } + + /** + * Verify that a descendant of a shareable node appears once in the + * result set (6.13.23) + */ + public void testSearch() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add new referenceable child + Node c = b1.addNode("c"); + ensureMixinType(c, mixReferenceable); + b1.save(); + + String sql = "SELECT * FROM nt:unstructured WHERE jcr:uuid = '"+c.getUUID()+"'"; + QueryResult res = workspace.getQueryManager().createQuery(sql, Query.SQL).execute(); + + List list = new ArrayList(); + + NodeIterator iter = res.getNodes(); + while (iter.hasNext()) { + list.add(iter.nextNode()); + } + assertEquals(1, list.size()); + assertTrue(list.get(0).isSame(c)); + } + + //--------------------------------------------------------- limitation tests + + /** + * Clone a mix:shareable node to the same workspace, with the same + * parent. This is unsupported in Jackrabbit. + */ + public void testCloneToSameParent() throws Exception { + // setup parent nodes and first child + Node a = testRootNode.addNode("a"); + Node b1 = a.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + Workspace workspace = b1.getSession().getWorkspace(); + + try { + // clone to same parent + workspace.clone(workspace.getName(), b1.getPath(), + a.getPath() + "/b2", false); + fail("Cloning inside same parent should fail."); + } catch (UnsupportedRepositoryOperationException e) { + // expected + } + } + + /** + * Move a node in a shared set. + */ + public void testMoveShareableNode() throws Exception { + // setup parent nodes and first children + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b = a1.addNode("b"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b, mixShareable); + b.getSession().save(); + + // move + Workspace workspace = b.getSession().getWorkspace(); + + // move shareable node + String newPath = a2.getPath() + "/b"; + workspace.move(b.getPath(), newPath); + // move was performed using the workspace, so refresh the session + b.getSession().refresh(false); + assertEquals(newPath, b.getPath()); + } + + /** + * Transiently move a node in a shared set. + */ + public void testTransientMoveShareableNode() throws Exception { + // setup parent nodes and first children + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b = a1.addNode("b"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b, mixShareable); + b.getSession().save(); + + // move + Session session = superuser; + + // move shareable node + String newPath = a2.getPath() + "/b"; + session.move(b.getPath(), newPath); + session.save(); + assertEquals(newPath, b.getPath()); + } + + //----------------------------------------------------- implementation tests + + /** + * Verify that invoking save() on a share-ancestor will save changes in + * all share-descendants. + */ + public void testRemoveDescendantAndSave() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Session session = b1.getSession(); + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child node c to b1 + Node c = b1.addNode("c"); + b1.save(); + + // remove child node c + c.remove(); + + // save a2 (having path /testroot/a2): this should save c as well + // since one of the paths to c is /testroot/a2/b2/c + a2.save(); + assertFalse("Saving share-ancestor should save share-descendants", + session.hasPendingChanges()); + } + + /** + * Verify that invoking save() on a share-ancestor will save changes in + * all share-descendants. + */ + public void testRemoveDescendantAndRemoveShareAndSave() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Session session = b1.getSession(); + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child node c to b1 + Node c = b1.addNode("c"); + b1.save(); + + // remove child node c + c.remove(); + + // remove share b2 from a2 + a2.getNode("b2").removeShare(); + + // save a2 (having path /testroot/a2): this should save c as well + // since one of the paths to c was /testroot/a2/b2/c + a2.save(); + assertFalse("Saving share-ancestor should save share-descendants", + session.hasPendingChanges()); + } + + /** + * Verify that invoking save() on a share-ancestor will save changes in + * all share-descendants. + */ + public void testModifyDescendantAndSave() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child node c to b1 + Node c = b1.addNode("c"); + b1.save(); + + // add child d to c, this modifies c + c.addNode("d"); + + // save a2 (having path /testroot/a2): this should save c as well + // since one of the paths to c is /testroot/a2/b2/c + a2.save(); + assertFalse("Saving share-ancestor should save share-descendants", + c.isModified()); + } + + /** + * Verify that invoking save() on a share-ancestor will save changes in + * all share-descendants. + */ + public void testModifyDescendantAndRemoveShareAndSave() throws Exception { + // setup parent nodes and first child + Node a1 = testRootNode.addNode("a1"); + Node a2 = testRootNode.addNode("a2"); + Node b1 = a1.addNode("b1"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b1, mixShareable); + b1.save(); + + // clone + Workspace workspace = b1.getSession().getWorkspace(); + workspace.clone(workspace.getName(), b1.getPath(), + a2.getPath() + "/b2", false); + + // add child node c to b1 + Node c = b1.addNode("c"); + b1.save(); + + // add child d to c, this modifies c + c.addNode("d"); + + // remove share b2 from a2 + a2.getNode("b2").removeShare(); + + // save a2 (having path /testroot/a2): this should save c as well + // since one of the paths to c was /testroot/a2/b2/c + a2.save(); + assertFalse("Saving share-ancestor should save share-descendants", + c.isModified()); + } + + /** + * Clone a mix:shareable node to the same workspace multiple times, remove + * all parents and save. Exposes an error that occurred when having more + * than two members in a shared set and parents were removed in the same + * order they were created. + */ + public void testCloneMultipleTimes() throws Exception { + final int count = 10; + Node[] parents = new Node[count]; + + // setup parent nodes and first child + for (int i = 0; i < parents.length; i++) { + parents[i] = testRootNode.addNode("a" + (i + 1)); + } + Node b = parents[0].addNode("b"); + testRootNode.getSession().save(); + + // add mixin + ensureMixinType(b, mixShareable); + b.save(); + + Workspace workspace = b.getSession().getWorkspace(); + + // clone to all other nodes + for (int i = 1; i < parents.length; i++) { + workspace.clone(workspace.getName(), b.getPath(), + parents[i].getPath() + "/b", false); + } + + // remove all parents and save + for (int i = 0; i < parents.length; i++) { + parents[i].remove(); + } + testRootNode.getSession().save(); + } + + /** + * Verify that shared nodes return correct paths. + */ + public void testSharedNodePath() throws Exception { + Node a1 = testRootNode.addNode("a1"); + Node a2 = a1.addNode("a2"); + Node b1 = a1.addNode("b1"); + ensureMixinType(b1, mixShareable); + testRootNode.getSession().save(); + + //now we have a shareable node N with path a1/b1 + + Session session = testRootNode.getSession(); + Workspace workspace = session.getWorkspace(); + String path = a2.getPath() + "/b2"; + workspace.clone(workspace.getName(), b1.getPath(), path, false); + + //now we have another shareable node N' in the same shared set as N with path a1/a2/b2 + + //using the path a1/a2/b2, we should get the node N' here + Item item = session.getItem(path); + assertEquals("unexpectedly got the path from another node from the same shared set", path, item.getPath()); + } + + //---------------------------------------------------------- utility methods + + /** + * Return a shared set as an array of nodes. + * + * @param n node + * @return array of nodes in shared set + */ + private static Node[] getSharedSet(Node n) throws RepositoryException { + return toArray(n.getSharedSet()); + } + + /** + * Return an array of nodes given a NodeIterator. + * + * @param iter node iterator + * @return node array + */ + private static Node[] toArray(NodeIterator iter) { + List list = new ArrayList(); + + while (iter.hasNext()) { + list.add(iter.nextNode()); + } + + Node[] result = new Node[list.size()]; + list.toArray(result); + return result; + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/StringPropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/StringPropertyTest.java similarity index 83% rename from src/test/org/apache/jackrabbit/test/api/StringPropertyTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/StringPropertyTest.java index b870c56825b..f4bb0513ea5 100644 --- a/src/test/org/apache/jackrabbit/test/api/StringPropertyTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/StringPropertyTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -30,40 +30,36 @@ * conversions to other Properties (except Name and Path property). If no String * property is found or only a multivalue String property with an empty array, a * NotExecutableException is thrown on setUp. More precisely, the tests are: - *

        + *

        * - Value.getString() should return a string equals to Property.getString(), * and in case of a multivalue property the failure of Property.getString() is * checked. - *

        + *

        * - Value.getBoolean() Conversion to Boolean property. - *

        + *

        * - Value.getDate() Conversion to Date property is only valid when the String * follows the required Date pattern (6.2.5.1 of jsr170 specification). - *

        + *

        * - Value.getDouble() Conversion to Double are only valid when the String * follows the correct patterns as required by the according Java classes. - *

        + *

        * - Value.getLong() Conversion to Double are only valid when the String follows * the correct patterns as required by the according Java classes. - *

        + *

        * - Value.getStream() Conversion to a Binary property follows the rules of * Value.getStream() as explained in chapter 6.2.7 of the jsr170 specification. * The required encoding is utf-8. - *

        + *

        * - Property.getNode() Conversion to a Reference property is tested with * Property.getNode. The String should match the UUID pattern but this doesn't * guarantee to be a reference (which especially requires integrity). - *

        + *

        * - Property.getLength() . - *

        + *

        * - Property.getLengths() . - *

        + *

        * - Property.getType() is compared to Value.getType() . * - * @test - * @sources StringPropertyTest.java - * @executeClass org.apache.jackrabbit.test.api.StringPropertyTest - * @keywords level1 */ public class StringPropertyTest extends AbstractPropertyTest { @@ -76,6 +72,14 @@ protected int getPropertyType() { return PropertyType.STRING; } + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + /** * Tests that Property.getString() delivers a string equal to the string * received with Value.getString(). @@ -89,9 +93,10 @@ public void testValue() throws RepositoryException { } catch (ValueFormatException vfe) { // ok } + } else { + assertEquals("Value.getString() and Property.getString() return different values.", + prop.getValue().getString(), prop.getString()); } - assertEquals("Value.getString() and Property.getString() return different values.", - prop.getValue().getString(), prop.getString()); } /** @@ -173,7 +178,7 @@ public void testGetLong() throws RepositoryException { public void testGetStream() throws RepositoryException, IOException { Value val = PropertyUtil.getValue(prop); BufferedInputStream in = new BufferedInputStream(val.getStream()); - Value otherVal = prop.getValue(); + Value otherVal = PropertyUtil.getValue(prop); byte[] utf8bytes = otherVal.getString().getBytes(UTF8); // compare the bytearray with the bytes received from a Stream created with this String int i = 0; @@ -184,25 +189,23 @@ public void testGetStream() throws RepositoryException, IOException { } try { val.getString(); - fail("Non stream method call after stream method call " + - "should throw an IllegalStateException."); } catch (IllegalStateException ise) { - //ok + fail("Non stream method call after stream method call " + + "should not throw an IllegalStateException."); } try { otherVal.getStream(); - fail("Stream method call after a non stream method call " + - "should throw an IllegalStateException."); } catch (IllegalStateException ise) { - // ok + fail("Stream method call after a non stream method call " + + "should not throw an IllegalStateException."); } in.close(); } /** - * Tests conversion from String type to Reference type. + * Tests conversion from String type to Reference or Path type. */ - public void testAsReference() throws RepositoryException, NotExecutableException { + public void testGetNode() throws RepositoryException, NotExecutableException { if (!multiple) { // not testable since format of ID is implementation specific } else { @@ -216,6 +219,23 @@ public void testAsReference() throws RepositoryException, NotExecutableException } } + /** + * Tests conversion from String type to Reference or Path type. + */ + public void testGetProperty() throws RepositoryException, NotExecutableException { + if (!multiple) { + // not testable as a STRING may or may not be convertable to Path or Reference + } else { + try { + prop.getProperty(); + fail("Property.getNode() called on a multivalue property " + + "should throw a ValueFormatException."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + /** * Tests the Property.getLength() method. The length returned is either -1 * or it is the length of the string. diff --git a/src/test/org/apache/jackrabbit/test/api/SysViewContentHandler.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SysViewContentHandler.java similarity index 86% rename from src/test/org/apache/jackrabbit/test/api/SysViewContentHandler.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SysViewContentHandler.java index 3ba100628ed..ca3e88a1c95 100644 --- a/src/test/org/apache/jackrabbit/test/api/SysViewContentHandler.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/SysViewContentHandler.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -15,7 +15,7 @@ * limitations under the License. */ package org.apache.jackrabbit.test.api; - + import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.Attributes; import org.xml.sax.SAXException; @@ -29,7 +29,6 @@ import javax.jcr.RepositoryException; import javax.jcr.NodeIterator; import javax.jcr.PropertyType; -import javax.jcr.NamespaceRegistry; import javax.jcr.PropertyIterator; import javax.jcr.Property; import javax.jcr.Value; @@ -39,9 +38,14 @@ import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Stack; import java.util.Hashtable; import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; + +import junit.framework.Assert; /** * ContentHandler implementation which checks if the system view export of @@ -64,14 +68,12 @@ class SysViewContentHandler extends DefaultHandler { PropElemData currentPropElem; // the valueElem in process protected StringBuffer currentValue; - // the value(s) of the current propElem e.g if multiple values - protected ArrayList currentValues; // prefix mapping data - protected HashMap prefixes; + protected Map prefixes; // if the first node is yet treated protected boolean testRootDone; // The stack holding the opened nodeElems - Stack nodeElemStack; + Stack nodeElemStack; // resolved QNames for well known node and property names private String jcrRoot; @@ -121,12 +123,12 @@ public void startDocument() throws SAXException { // Check the given path, init the treeState stack Item item = session.getItem(path); checkCondition("TestPath "+path+" is not a path to a node.", item.isNode()); - nodeElemStack = new Stack(); + nodeElemStack = new Stack(); currentNodeElem = new NodeElemData(); currentNodeElem.name = item.getName(); currentNodeElem.node = (Node) item; currentNodeElem.path = path; - prefixes = new HashMap(); + prefixes = new HashMap(); testRootDone = false; } catch (PathNotFoundException pe) { checkCondition("TestPath " + path + " is not a valid path." @@ -245,7 +247,7 @@ else if (qName.equals(svProperty)) { currentPropElem.name = attributes.getValue(svName); currentPropElem.typeName = attributes.getValue(svType); currentPropElem.type = PropertyType.valueFromName(currentPropElem.typeName); - currentPropElem.values = new ArrayList(); + currentPropElem.values = new ArrayList(); } else if (qName.equals(svValue)) { @@ -333,28 +335,36 @@ else if (qName.equals(svNode)) { } public void endDocument() throws SAXException { - // check the prefixes exported + // check exported namespaces try { - NamespaceRegistry nsr = session.getWorkspace().getNamespaceRegistry(); - String[] registeredPrefixes = nsr.getPrefixes(); - // check against the found prefixes - checkCondition("Size of included prefixes is not the size of " + - "registered prefixes", registeredPrefixes.length == prefixes.size()) ; - for (int i=0; i sessionNamespaces = new HashMap(); + String[] sessionPrefixes = session.getNamespacePrefixes(); + for (int i = 0; i < sessionPrefixes.length; i++) { + sessionNamespaces.put(sessionPrefixes[i], session.getNamespaceURI(sessionPrefixes[i])); + } + + // check prefixes against namespace mapping in session + for (Iterator it = prefixes.keySet().iterator(); it.hasNext(); ) { + String prefix = it.next(); + if ("xml".equals(prefix)) { + Assert.fail("Prefix mapping for 'xml' must not be exported."); + } + + String uri = prefixes.get(prefix); + checkCondition("Exported uri " + uri + " is not a registered namespace.", + sessionNamespaces.containsValue(uri)); + checkCondition("Exported prefix " + prefix + " does not match " + + "current namespacce mapping in Session", + sessionNamespaces.containsKey(prefix)); } } catch (RepositoryException re) { - // + throw new SAXException(re); } } // helpers for test result forward - private void checkCondition(String str, boolean bool) throws SAXException { - if (!bool) - throw new SAXException(new ConditionException(str)); + private void checkCondition(String str, boolean bool) { + Assert.assertTrue(str, bool); } public class ConditionException extends SAXException { @@ -376,13 +386,13 @@ private void checkPropOrder(NodeElemData nodeElem) boolean jcrPrimaryTypeFound = false; boolean jcrMixinTypesFound = true; boolean uuidFound = true; - PropElemData propElem = (PropElemData) nodeElem.propElems.get(0); + PropElemData propElem = nodeElem.propElems.get(0); jcrPrimaryTypeFound = (jcrPrimaryType.equals(propElem.name)); checkCondition("Exported property jcr:primaryType of node " + nodeElem.path + " is not at the first position.", jcrPrimaryTypeFound); if (nodeElem.node.hasProperty(jcrMixinTypes)) { - PropElemData propElem2 = (PropElemData) nodeElem.propElems.get(1); + PropElemData propElem2 = nodeElem.propElems.get(1); jcrMixinTypesFound = (jcrMixinTypes.equals(propElem2.name)); checkCondition("Exported property jcr:jcrMixinTypes of node " + nodeElem.path + " is not at the second position.", jcrMixinTypesFound); @@ -390,7 +400,7 @@ private void checkPropOrder(NodeElemData nodeElem) NodeType[] mixins = nodeElem.node.getMixinNodeTypes(); for (int j=0; j propElems = nodeElem.propElems; // no props exported if (propElems.size() == 0) { @@ -440,9 +449,9 @@ private void checkAllProps(NodeElemData nodeElem, boolean skipBinary) } else { // compare the propElems with the properties of the given node - for (int i = 0; i < propElems.size() -1; i++) { + for (int i = 0; i < propElems.size(); i++) { correctVal = false; - PropElemData propElem = (PropElemData) propElems.get(i); + PropElemData propElem = propElems.get(i); int propType = propElem.type; if (node.hasProperty(propElem.name)) { Property prop = node.getProperty(propElem.name); @@ -480,8 +489,7 @@ private void checkAllProps(NodeElemData nodeElem, boolean skipBinary) else { str = prop.getString(); } - str = escapeString(str); - String val = (String) propElem.values.get(0); + String val = propElem.values.get(0); if (prop.getType() == PropertyType.BINARY) { // decode value @@ -499,17 +507,16 @@ private void checkAllProps(NodeElemData nodeElem, boolean skipBinary) checkCondition("Number of exported values of property " + prop.getPath() + " does not match the number " + "its values", vals.length == size); - for (int j = 0; j < size -1; j++) { + for (int j = 0; j < size; j++) { // we know that the order of the values // of a mulitval prop is preserved during export - String val = (String)propElem.values.get(i); + String val = propElem.values.get(j); if (prop.getType() == PropertyType.BINARY) { // decode value val = decodeBase64(val); } String str = vals[j].getString(); - str = escapeString(str); correctVal = (val.equals(str)); checkCondition("Property value of property " + propElem.name + " of node " + nodeElem.path + @@ -524,8 +531,9 @@ private void checkAllProps(NodeElemData nodeElem, boolean skipBinary) } // skipBinary true and propType is Binary, should be skipped else { - checkCondition("Binary property "+ prop.getPath() - + " exported although skipBinary flag is true.", false); + checkCondition("Value of binary property "+ prop.getPath() + + " exported although skipBinary flag is true.", + propElem.values.isEmpty()); } } // given node has no property with the name given by the prop element @@ -536,17 +544,8 @@ private void checkAllProps(NodeElemData nodeElem, boolean skipBinary) } } // compare the sizes here - if (skipBinary) { - PropertyIterator propIter = node.getProperties(); - while (propIter.hasNext()) { - if (propIter.nextProperty().getType() == PropertyType.BINARY) { - binaryCounter++; - } - } - } - long otherSize = getSize(node.getProperties()); - allFound = (propElems.size() + binaryCounter == otherSize); + allFound = propElems.size() == otherSize; checkCondition("Not all properties of node " + nodeElem.path + " are exported.", allFound); } @@ -558,12 +557,11 @@ private void checkAllProps(NodeElemData nodeElem, boolean skipBinary) * @param nodeElem The node to check. * @param noRecurse Boolean if child nodes should be exported at all. * @throws RepositoryException - * @throws SAXException */ private void checkChildren(NodeElemData nodeElem, boolean noRecurse) - throws RepositoryException, SAXException { + throws RepositoryException { - Hashtable childElemsFound = nodeElem.childNodeElemNames; + Hashtable childElemsFound = nodeElem.childNodeElemNames; boolean totalSumOk = false; boolean partialSumOk = true; if (noRecurse) { @@ -577,8 +575,8 @@ private void checkChildren(NodeElemData nodeElem, boolean noRecurse) NodeIterator nodeIter = nodeElem.node.getNodes(); long children = getSize(nodeIter); - for (Enumeration e = childElemsFound.elements(); e.hasMoreElements();) { - ChildNodeElem child = (ChildNodeElem) e.nextElement(); + for (Enumeration e = childElemsFound.elements(); e.hasMoreElements();) { + ChildNodeElem child = e.nextElement(); String name = child.name; long number = child.number; @@ -615,33 +613,6 @@ private String decodeBase64(String str) throws IOException { return decoded; } - /** - * Escapes the predefined entity references in XML. - * @param str - * @return the escaped string - */ - private String escapeString (String str) { - String orig = str; - String amp = "&"; - String less = "<"; - String great = ">"; - String apo = "'"; - String quot = "\""; - - String r_amp = "&"; - String r_less = "<"; - String r_great = ">"; - String r_apo = "'"; - String r_quot = """; - - String repl = orig.replaceAll(amp, r_amp); - repl = repl.replaceAll(less, r_less); - repl = repl.replaceAll(great, r_great); - repl = repl.replaceAll(apo, r_apo); - repl = repl.replaceAll(quot, r_quot); - return repl; - } - /** * * @param it @@ -674,7 +645,7 @@ private class NodeElemData { // the path of the node String path; // List of PropElemData - ArrayList propElems = new ArrayList(); + List propElems = new ArrayList(); // the node itself Node node; // the current position of the child node in process among its same name siblings. @@ -682,7 +653,7 @@ private class NodeElemData { int position = 0; // the childNodeElems (stored are key: name and // value: number of the same name siblings) - Hashtable childNodeElemNames = new Hashtable(); + Hashtable childNodeElemNames = new Hashtable(); } /** @@ -692,7 +663,7 @@ private class PropElemData { String name; String typeName; int type; - ArrayList values; + List values; } /** diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/TestAll.java new file mode 100644 index 00000000000..27f700aa961 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/TestAll.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.test.api; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the package + * javax.jcr. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("javax.jcr tests"); + + // ADD TEST CLASSES HERE: + + // level 1 tests + suite.addTestSuite(RootNodeTest.class); + suite.addTestSuite(NodeReadMethodsTest.class); + suite.addTestSuite(PropertyTypeTest.class); + suite.addTestSuite(NodeDiscoveringNodeTypesTest.class); + + suite.addTestSuite(BinaryPropertyTest.class); + suite.addTestSuite(BooleanPropertyTest.class); + suite.addTestSuite(DatePropertyTest.class); + suite.addTestSuite(DoublePropertyTest.class); + suite.addTestSuite(LongPropertyTest.class); + suite.addTestSuite(NamePropertyTest.class); + suite.addTestSuite(PathPropertyTest.class); + suite.addTestSuite(ReferencePropertyTest.class); + suite.addTestSuite(StringPropertyTest.class); + suite.addTestSuite(UndefinedPropertyTest.class); + + suite.addTestSuite(NamespaceRegistryReadMethodsTest.class); + suite.addTestSuite(NamespaceRemappingTest.class); + suite.addTestSuite(NodeIteratorTest.class); + suite.addTestSuite(PropertyReadMethodsTest.class); + suite.addTestSuite(RepositoryDescriptorTest.class); + suite.addTestSuite(SessionReadMethodsTest.class); + suite.addTestSuite(WorkspaceReadMethodsTest.class); + suite.addTestSuite(ReferenceableRootNodesTest.class); + + suite.addTestSuite(ExportSysViewTest.class); + suite.addTestSuite(ExportDocViewTest.class); + + suite.addTestSuite(NameTest.class); + suite.addTestSuite(PathTest.class); + + // level 2 tests + suite.addTestSuite(AddNodeTest.class); + suite.addTestSuite(NamespaceRegistryTest.class); + suite.addTestSuite(ReferencesTest.class); + suite.addTestSuite(SessionTest.class); + suite.addTestSuite(SessionUUIDTest.class); + suite.addTestSuite(NodeTest.class); + suite.addTestSuite(NodeUUIDTest.class); + suite.addTestSuite(NodeOrderableChildNodesTest.class); + suite.addTestSuite(PropertyTest.class); + + suite.addTestSuite(SetValueBinaryTest.class); + suite.addTestSuite(SetValueBooleanTest.class); + suite.addTestSuite(SetValueDateTest.class); + suite.addTestSuite(SetValueDecimalTest.class); + suite.addTestSuite(SetValueDoubleTest.class); + suite.addTestSuite(SetValueLongTest.class); + suite.addTestSuite(SetValueReferenceTest.class); + suite.addTestSuite(SetValueStringTest.class); + suite.addTestSuite(SetValueConstraintViolationExceptionTest.class); + suite.addTestSuite(SetValueValueFormatExceptionTest.class); + suite.addTestSuite(SetValueVersionExceptionTest.class); + + suite.addTestSuite(SetPropertyBooleanTest.class); + suite.addTestSuite(SetPropertyCalendarTest.class); + suite.addTestSuite(SetPropertyDecimalTest.class); + suite.addTestSuite(SetPropertyDoubleTest.class); + suite.addTestSuite(SetPropertyInputStreamTest.class); + suite.addTestSuite(SetPropertyLongTest.class); + suite.addTestSuite(SetPropertyNodeTest.class); + suite.addTestSuite(SetPropertyStringTest.class); + suite.addTestSuite(SetPropertyValueTest.class); + suite.addTestSuite(SetPropertyConstraintViolationExceptionTest.class); + suite.addTestSuite(SetPropertyAssumeTypeTest.class); + + suite.addTestSuite(NodeItemIsModifiedTest.class); + suite.addTestSuite(NodeItemIsNewTest.class); + suite.addTestSuite(PropertyItemIsModifiedTest.class); + suite.addTestSuite(PropertyItemIsNewTest.class); + + suite.addTestSuite(NodeAddMixinTest.class); + suite.addTestSuite(NodeCanAddMixinTest.class); + suite.addTestSuite(NodeRemoveMixinTest.class); + + suite.addTestSuite(NodeSetPrimaryTypeTest.class); + + suite.addTestSuite(WorkspaceCloneReferenceableTest.class); + suite.addTestSuite(WorkspaceCloneSameNameSibsTest.class); + suite.addTestSuite(WorkspaceCloneTest.class); + suite.addTestSuite(WorkspaceCloneVersionableTest.class); + suite.addTestSuite(WorkspaceCopyBetweenWorkspacesReferenceableTest.class); + suite.addTestSuite(WorkspaceCopyBetweenWorkspacesSameNameSibsTest.class); + suite.addTestSuite(WorkspaceCopyBetweenWorkspacesTest.class); + suite.addTestSuite(WorkspaceCopyBetweenWorkspacesVersionableTest.class); + suite.addTestSuite(WorkspaceCopyReferenceableTest.class); + suite.addTestSuite(WorkspaceCopySameNameSibsTest.class); + suite.addTestSuite(WorkspaceCopyTest.class); + suite.addTestSuite(WorkspaceCopyVersionableTest.class); + suite.addTestSuite(WorkspaceMoveReferenceableTest.class); + suite.addTestSuite(WorkspaceMoveSameNameSibsTest.class); + suite.addTestSuite(WorkspaceMoveTest.class); + suite.addTestSuite(WorkspaceMoveVersionableTest.class); + + suite.addTestSuite(RepositoryLoginTest.class); + suite.addTestSuite(ImpersonateTest.class); + suite.addTestSuite(CheckPermissionTest.class); + + suite.addTestSuite(DocumentViewImportTest.class); + suite.addTestSuite(SerializationTest.class); + + suite.addTestSuite(ValueFactoryTest.class); + + // JCR 2.0 + + //// new node types + suite.addTestSuite(GetWeakReferencesTest.class); + + //// new Session features + suite.addTestSuite(SessionRemoveItemTest.class); + suite.addTestSuite(HasPermissionTest.class); + + //// new Workspace features + suite.addTestSuite(WorkspaceTest.class); + + //// shareable nodes + suite.addTestSuite(ShareableNodeTest.class); + + //// repository factory + suite.addTestSuite(RepositoryFactoryTest.class); + + //// lifecycle management + suite.addTestSuite(LifecycleTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/src/test/org/apache/jackrabbit/test/api/TreeComparator.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/TreeComparator.java similarity index 83% rename from src/test/org/apache/jackrabbit/test/api/TreeComparator.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/TreeComparator.java index 3c6c943d9ca..590819a6861 100644 --- a/src/test/org/apache/jackrabbit/test/api/TreeComparator.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/TreeComparator.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -16,15 +16,11 @@ */ package org.apache.jackrabbit.test.api; -import org.apache.jackrabbit.test.AbstractJCRTest; - import javax.jcr.Session; import javax.jcr.Workspace; import javax.jcr.Item; import javax.jcr.Node; import javax.jcr.RepositoryException; -import javax.jcr.NameValue; -import javax.jcr.PathValue; import javax.jcr.NodeIterator; import javax.jcr.PropertyIterator; import javax.jcr.Property; @@ -37,15 +33,18 @@ import java.util.Calendar; import java.io.ByteArrayInputStream; +import junit.framework.Assert; + /** * TreeComparator compares two trees. This allows re-use for * different tests, and it allows to test a function on any tree, not just a * simple example node. - *

        + *

        * TreeComparator also creates an example tree that contains as many features as * possible. */ -class TreeComparator extends AbstractJCRTest { +class TreeComparator extends Assert { + public SerializationContext sc; public final boolean WORKSPACE = true; @@ -66,12 +65,15 @@ class TreeComparator extends AbstractJCRTest { public TreeComparator(SerializationContext sc, Session s) throws Exception { this.sc = sc; - setUp(); session = s; workspace = session.getWorkspace(); init(); } + public void setSession(Session session) { + this.session = session; + } + /** * Makes sure that the source and target folder exist, and are empty */ @@ -112,7 +114,7 @@ public void createExampleTree() { /** * Creates a simple example tree. Use this tree for general repository * functions, such as serialization, namespaces and versioning. - *

        + *

        * The beauty of this is that the tree contains exactly the features that * are supported by the repository. Any repository exceptions that occur are * displayed on "out", but are otherwise ignored. @@ -124,13 +126,13 @@ public void createExampleTree(boolean save) { try { Node src = (Node) session.getItem(sourceFolder); Node root = src.addNode(sc.rootNodeName); - root.addNode(nodeName1); - root.addNode(nodeName2, testNodeType); + root.addNode(sc.nodeName1); + root.addNode(sc.nodeName2, sc.testNodeType); byte[] byteArray = {(byte) 0, (byte) 255, (byte) 167, (byte) 100, (byte) 21, (byte) 6, (byte) 19, (byte) 71, (byte) 221}; - root.setProperty(propertyName1, new ByteArrayInputStream(byteArray)); - root.setProperty(nodeName3, "hello"); + root.setProperty(sc.propertyName1, new ByteArrayInputStream(byteArray)); + root.setProperty(sc.nodeName3, "hello"); } catch (Exception e) { - log.println("Error while creating example tree: " + e.getMessage()); + sc.log("Error while creating example tree: " + e.getMessage()); } if (save) { try { @@ -173,7 +175,7 @@ public void createComplexTree(boolean save) { rootNode.addNode(sc.orderChildrenTestNode); rootNode.addNode(sc.namespaceTestNode); } catch (RepositoryException e) { - log.println("Error while creating example tree: " + e.getMessage()); + sc.log("Error while creating example tree: " + e.getMessage()); } // Add nodes with mixin types @@ -194,15 +196,17 @@ public void createComplexTree(boolean save) { try { n = nt.addNode(name); n.addMixin(t.getName()); - // try saving, because some exceptions are trown only at save time + // try saving, because some exceptions are thrown only at save time session.save(); } catch (RepositoryException e) { - log.println("Cannot create node with mixin node type: " + e); + sc.log("Cannot create node with mixin node type: " + e); // if saving failed for a node, then remove it again (or else the next save will fail on it) try { - n.remove(); + if (n != null) { + n.remove(); + } } catch (RepositoryException e1) { - log.println("Could not remove node: " + e); + sc.log("Could not remove node: " + e); } } } @@ -225,14 +229,15 @@ public void createComplexTree(boolean save) { // Boolean pt.setProperty(sc.booleanTestProperty, true); // Name - pt.setProperty(sc.nameTestProperty, NameValue.valueOf(jcrPrimaryType)); + pt.setProperty(sc.nameTestProperty, session.getValueFactory().createValue(sc.jcrPrimaryType, PropertyType.NAME)); // Path - pt.setProperty(sc.pathTestProperty, PathValue.valueOf("paths/dont/have/to/point/anywhere")); + pt.setProperty(sc.pathTestProperty, session.getValueFactory().createValue("paths/dont/have/to/point/anywhere", PropertyType.PATH)); // Reference: Note that I only check if the node exists. We do not specify what happens with // the UUID during serialization. - if (!referenceable.isNodeType(mixReferenceable)) { - referenceable.addMixin(mixReferenceable); - } + sc.ensureMixinType(referenceable, sc.mixReferenceable); + // some implementations may require a save after addMixin() + session.save(); + pt.setProperty(sc.referenceTestProperty, referenceable); // Create a boolean property on the root node, so I can test noRecurse and skipBinary at the same time @@ -248,7 +253,7 @@ public void createComplexTree(boolean save) { mvp.setProperty(sc.multiValueTestProperty, s); session.save(); } catch (RepositoryException e) { - log.println("Could not create multi-value property: " + e); + sc.log("Could not create multi-value property: " + e); } // Save to the workspace. Note that export is from session anyway. @@ -361,7 +366,7 @@ public void compare(String sourcePath, int level) { */ public void compareNodes(Node a, Node b) { try { - log.println("Comparing " + a.getPath() + " to " + b.getPath()); + sc.log("Comparing " + a.getPath() + " to " + b.getPath()); } catch (RepositoryException e) { fail("Nodes not available: " + e.getMessage()); } @@ -369,8 +374,8 @@ public void compareNodes(Node a, Node b) { // check primary node type String primaryTypeA = null, primaryTypeB = null; try { - primaryTypeA = a.getProperty(jcrPrimaryType).getName(); - primaryTypeB = b.getProperty(jcrPrimaryType).getName(); + primaryTypeA = a.getProperty(sc.jcrPrimaryType).getName(); + primaryTypeB = b.getProperty(sc.jcrPrimaryType).getName(); } catch (RepositoryException e) { fail("Primary node type not available: " + e); } @@ -395,6 +400,14 @@ public void compareProperties(Node a, Node b) { while (ai.hasNext()) { Property pa = (Property) ai.next(); String pName = null; + // todo + String pPath = null; + try { + pPath = pa.getPath(); + } catch (RepositoryException e) { + + } + int pType = 0; try { @@ -408,28 +421,27 @@ public void compareProperties(Node a, Node b) { fail("Cannot access property information: " + e); } + if (propertyValueMayChange(pName)) { + continue; + } + Property pb = null; - try { - pb = b.getProperty(pName); - //fail if the property is there but should not be - if (skipBinary && pType == PropertyType.BINARY) { - fail("Property '" + pName + "' must not be available if skipBinary=true."); + // avoid skipped properties + if (!propertySkipped(pName)) { + try { + pb = b.getProperty(pName); + } catch (RepositoryException e) { + //fail if the property is not there but should + fail("Property '" + pPath + "' not available: " + e); } - } catch (RepositoryException e) { - //fail if the property is not there but should if (!(skipBinary && pType == PropertyType.BINARY)) { - fail("Property '" + pName + "' not available: " + e); + // todo + // compare source and target value + compareProperties(pa, pb); } } - - //if the property should be available and is available, then compare source and target value - if (!(skipBinary && pType == PropertyType.BINARY)) { - compareProperties(pa, pb); - } - } - } /** @@ -545,7 +557,7 @@ public void showTree() { n = (Node) session.getItem(sc.testroot); showTree(n, 0); } catch (RepositoryException e) { - log.println("Cannot display tree diagnostics: " + e); + sc.log("Cannot display tree diagnostics: " + e); } } @@ -553,21 +565,37 @@ public void showTree() { * Recursive display of source and target tree */ public void showTree(Node n, int level) throws RepositoryException { + StringBuffer sb = new StringBuffer(); for (int t = 0; t < level; t++) { - log.print("-"); + sb.append("-"); } - log.print(n.getName() + " "); - log.print(n.getPrimaryNodeType().getName() + " [ "); + sb.append(n.getName() + " "); + sb.append(n.getPrimaryNodeType().getName() + " [ "); PropertyIterator pi = n.getProperties(); while (pi.hasNext()) { Property p = (Property) pi.next(); - log.print(p.getName() + " "); + sb.append(p.getName() + " "); } - log.println("]"); + sb.append("]"); + sc.log(sb.toString()); NodeIterator ni = n.getNodes(); while (ni.hasNext()) { showTree((Node) ni.next(), level + 1); } } -} \ No newline at end of file + + /** + * Checks if a given property should be skipped during xml import. + * + * @param propertyName + * @return + */ + public boolean propertySkipped(String propertyName) { + if (sc.propertySkipped.indexOf(" " + propertyName + " ") < 0) { + return false; + } else { + return true; + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/UndefinedPropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/UndefinedPropertyTest.java new file mode 100644 index 00000000000..bfbc25535d7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/UndefinedPropertyTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Property; +import javax.jcr.PropertyType; + +/** + * Tests if no property in the workspace is of type + * {@link javax.jcr.PropertyType#UNDEFINED}. + * + */ +public class UndefinedPropertyTest extends AbstractJCRTest { + + /** + * Sets up the fixture for this test. + */ + protected void setUp() throws NotExecutableException, Exception { + isReadOnly = true; + super.setUp(); + } + + /** + * Returns "does not matter" (null). + * @return null. + */ + protected Boolean getPropertyIsMultivalued() { + return null; + } + + /** + * Tests that no actual property with type Undefined exists. + */ + public void testUndefinedProperty() throws RepositoryException { + Session session = getHelper().getReadOnlySession(); + try { + Property prop = PropertyUtil.searchProp(session, session.getRootNode().getNode(testPath), PropertyType.UNDEFINED, null); + assertNull("Property with type Undefined found.", prop); + } finally { + session.logout(); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ValueFactoryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ValueFactoryTest.java new file mode 100644 index 00000000000..4d779bcd70e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/ValueFactoryTest.java @@ -0,0 +1,333 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.util.InputStreamWrapper; + +import javax.jcr.Binary; +import javax.jcr.Session; +import javax.jcr.ValueFactory; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; +import javax.jcr.PathNotFoundException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import java.util.Calendar; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * ValueFactoryTest tests the different ValueFactory.createValue methods. + * + */ +public class ValueFactoryTest extends AbstractJCRTest { + + private Session session; + private ValueFactory valueFactory; + + private static final boolean booleanValue = false; + private Calendar dateValue = null; + private static final double doubleValue = 3.1414926; + private static final long longValue = Long.MAX_VALUE; + private Node referenceNode = null; + private static final String stringValue = "a string"; + private static String nameValue = "aName"; + private static String pathValue = "/a/Path[1]"; + private byte[] binaryValue = null; + + private String dateValueFail = nameValue; + private static final String doubleValueFail = nameValue; + private static final String longValueFail = nameValue; + private static String nameValueFail = ";pre fix::name;"; + private static String pathValueFail =nameValueFail; + + private static int[] types = {PropertyType.DATE, PropertyType.DOUBLE, PropertyType.LONG, + PropertyType.NAME, PropertyType.PATH, PropertyType.REFERENCE, + PropertyType.STRING, PropertyType.BINARY, PropertyType.BOOLEAN}; + + + public void setUp() throws Exception { + super.setUp(); + session = getHelper().getReadWriteSession(); + try { + valueFactory = session.getValueFactory(); + } catch (UnsupportedRepositoryOperationException e) { + String message = "ValueFactory Test not executable: " + e.getMessage(); + throw new NotExecutableException(message); + } + //notReferenceableNode = getProperty(not_ReferenceableNode); + nameValue = testRootNode.getName(); + pathValue = testRootNode.getPath(); + dateValue = Calendar.getInstance(); + binaryValue = createRandomString(10).getBytes(); + referenceNode = createReferenceableNode(nodeName1); + } + + + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + valueFactory = null; + referenceNode = null; + super.tearDown(); + } + + /** + * Create a referenceable node under the testRootNode + * or null if it is not possible to create one. + * @param name + * @return + * @throws RepositoryException + */ + public Node createReferenceableNode(String name) throws RepositoryException { + // remove a yet existing node at the target + try { + Node node = testRootNode.getNode(name); + node.remove(); + session.save(); + } catch (PathNotFoundException pnfe) { + // ok + } + // a referenceable node + Node n1 = testRootNode.addNode(name, testNodeType); + if (n1.canAddMixin(mixReferenceable)) { + n1.addMixin(mixReferenceable); + // make sure jcr:uuid is available + testRootNode.getSession().save(); + return n1; + } + else { + return null; + } + } + + /** + * Tests if the type of a created value is set correctly. + * + * @throws RepositoryException + */ + public void testValueType() throws RepositoryException { + Value value = null; + int type = -1; + for (int i = 0; i < types.length; i++) { + + switch (types[i]) { + + case PropertyType.BINARY: + try { + ByteArrayInputStream in = new ByteArrayInputStream(binaryValue); + value = valueFactory.createValue(in); + session.save(); + type = value.getType(); + in.close(); + } catch (IOException ioe) { + + } + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.BINARY) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.BINARY == type); + break; + + case PropertyType.BOOLEAN: + value = valueFactory.createValue(booleanValue); + session.save(); + type = value.getType(); + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.BOOLEAN) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.BOOLEAN == type); + break; + + case PropertyType.DATE: + value = valueFactory.createValue(dateValue); + session.save(); + type = value.getType(); + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.DATE) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.DATE == type); + break; + + case PropertyType.DOUBLE: + value = valueFactory.createValue(doubleValue); + session.save(); + type = value.getType(); + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.DOUBLE) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.DOUBLE == type); + break; + + case PropertyType.LONG: + value = valueFactory.createValue(longValue); + session.save(); + type = value.getType(); + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.LONG) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.LONG == type); + break; + + case PropertyType.NAME: + value = valueFactory.createValue(nameValue, PropertyType.NAME); + session.save(); + type = value.getType(); + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.NAME) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.NAME == type); + break; + + case PropertyType.PATH: + value = valueFactory.createValue(pathValue, PropertyType.PATH); + session.save(); + type = value.getType(); + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.PATH) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.PATH == type); + + break; + + case PropertyType.REFERENCE: + if (referenceNode != null) { + value = valueFactory.createValue(referenceNode); + session.save(); + type = value.getType(); + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.REFERENCE) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.REFERENCE == type); + // correct value? + assertEquals("Reference value does not contain the UUID of the " + + "referenced node.", referenceNode.getUUID(), value.getString()); + } + break; + + case PropertyType.STRING: + value = valueFactory.createValue(stringValue); + session.save(); + type = value.getType(); + assertTrue("Type of created value not correct: Expected: " + + PropertyType.nameFromValue(PropertyType.STRING) + + " but was: " + PropertyType.nameFromValue(type), + PropertyType.STRING == type); + break; + + } + } + + } + + /** + * Tests if a ValueFormatexception is thrown in case the passed string + * cannot be converted to the required value type. + * value creation. + * @throws RepositoryException + */ + public void testValueFormatException() throws RepositoryException { + Value value = null; + for (int i = 0; i < types.length; i++) { + + switch (types[i]) { + + case PropertyType.DATE: + try { + value = valueFactory.createValue(dateValueFail,PropertyType.DATE); + fail("Conversion from String " + dateValueFail + + " to a " + PropertyType.nameFromValue(types[i]) + + " value should throw ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + break; + + case PropertyType.DOUBLE: + try { + value = valueFactory.createValue(doubleValueFail,PropertyType.DOUBLE); + fail("Conversion from String " + doubleValueFail + + " to a " + PropertyType.nameFromValue(types[i]) + + " value should throw ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + break; + + case PropertyType.LONG: + try { + value = valueFactory.createValue(longValueFail,PropertyType.LONG); + fail("Conversion from String " + longValueFail + + " to a " + PropertyType.nameFromValue(types[i]) + + " value should throw ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + break; + + case PropertyType.NAME: + try { + value = valueFactory.createValue(nameValueFail,PropertyType.NAME); + fail("Conversion from String " + nameValueFail + + " to a " + PropertyType.nameFromValue(types[i]) + + " value should throw ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + break; + + case PropertyType.PATH: + try { + value = valueFactory.createValue(pathValueFail,PropertyType.PATH); + fail("Conversion from String " + pathValueFail + + " to a " + PropertyType.nameFromValue(types[i]) + + " value should throw ValueFormatException."); + } catch (ValueFormatException vfe) { + //ok + } + break; + + default: + break; + } + } + + } + + /** + * Tests whether a passed InputStream is closed + * by the implementation. + * + * @throws RepositoryException + */ + public void testInputStream() throws RepositoryException { + InputStreamWrapper in = new InputStreamWrapper(new ByteArrayInputStream(binaryValue)); + valueFactory.createValue(in); + assertTrue("ValueFactory.createValue(InputStream) is expected to close the passed input stream", in.isClosed()); + + in = new InputStreamWrapper(new ByteArrayInputStream(binaryValue)); + Binary bin = valueFactory.createBinary(in); + assertTrue("ValueFactory.createBinary(InputStream) is expected to close the passed input stream", in.isClosed()); + bin.dispose(); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneReferenceableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneReferenceableTest.java new file mode 100644 index 00000000000..0bdb8648371 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneReferenceableTest.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.test.api; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.ItemExistsException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * WorkspaceCloneReferenceableTest contains tests for cloning + * referenceable nodes between workspaces. + * + */ +public class WorkspaceCloneReferenceableTest extends AbstractWorkspaceReferenceableTest { + + /** + * In the case of referenceable nodes clone preserves the node's UUID so + * that the new node in the destination workspcace has the same UUID as the + * node in the source workspace. + */ + public void testCloneNodesReferenceableNodesOriginalUUID() throws RepositoryException, + NotExecutableException { + // add mixin referenceable to node1 + addMixinReferenceableToNode(node1); + + // copy referenceable node below non-referenceable node + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + workspaceW2.clone(workspace.getName(), node1.getPath(), dstAbsPath, true); + + // uuid of copied node should be different than original node uuid + String originalUUID = node1.getUUID(); + Node copiedNode = node2W2.getNode(node1.getName()); + String copiedUUID = copiedNode.getUUID(); + + assertTrue(originalUUID.equals(copiedUUID)); + } + + /** + * If removeExisting is true then the existing node is removed from its + * current location and the cloned node with the same UUID from srcWorkspace + * is copied to this workspace as part of the copied subtree (that is, not + * into the former location of the old node). The subtree of the cloned node + * will reflect the clones state in srcWorkspace, in other words the + * existing node will be moved and changed. + */ + public void testCloneNodesRemoveExistingTrue() throws RepositoryException, + NotExecutableException { + // add mixin referenceable to node1 + addMixinReferenceableToNode(node1); + + // clone a node from default workspace to have the same uuid on second workspace + String dstAbsPath = node2W2.getPath() + "/" + nodeName2; + workspaceW2.clone(workspace.getName(), node1.getPath(), dstAbsPath, true); + Node clonedNode = node2W2.getNode(nodeName2); + + // clone node1 from default workspace to second workspace + dstAbsPath = node2W2.getPath() + "/" + nodeName3; + workspaceW2.clone(workspace.getName(), node1.getPath(), dstAbsPath, true); + Node clonedNode2 = node2W2.getNode(nodeName3); + + // because a node with same uuid exists (cloned node in earlier step - nodeName2), the existing node (and its subtree) + // should be removed ... + assertFalse(node2W2.hasNode(nodeName2)); + } + + /** + * If removeExisting is false then a UUID collision causes this method to + * throw a ItemExistsException and no changes are made. + */ + public void testCloneNodesRemoveExistingFalse() throws RepositoryException, + NotExecutableException { + // add mixin referenceable to node1 + addMixinReferenceableToNode(node1); + + // clone a node from default workspace to have the same uuid on second workspace + workspaceW2.clone(workspace.getName(), node1.getPath(), testRootNodeW2.getPath() + "/" + nodeName3, false); + + // clone node1 from default workspace to second workspace + try { + workspaceW2.clone(workspace.getName(), node1.getPath(), testRootNodeW2.getPath() + "/" + nodeName4, false); + fail("If removeExisting is false then a UUID collision should throw a ItemExistsException"); + } catch (ItemExistsException e) { + // successful + } + } + + /** + * The clone method clones both referenceable and nonreferenceable nodes. + */ + public void testCloneNodesReferenceableAndNonreferenceable() throws RepositoryException, + NotExecutableException { + // clone referenceable node + + // add mixin referenceable to node1 + addMixinReferenceableToNode(node1); + if (node1.isNodeType(mixReferenceable)) { + workspaceW2.clone(workspace.getName(), node1.getPath(), testRootNodeW2.getPath() + "/" + nodeName3, false); + } else { + fail("Node should be referenceable."); + } + + // clone nonreferenceable node + if (node2.isNodeType(mixReferenceable)) { + fail("Node should not be referenceable."); + } else { + workspaceW2.clone(workspace.getName(), node2.getPath(), testRootNodeW2.getPath() + "/" + nodeName4, false); + assertTrue(testRootNodeW2.hasNode(nodeName4)); + } + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/WorkspaceCloneSameNameSibsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneSameNameSibsTest.java similarity index 75% rename from src/test/org/apache/jackrabbit/test/api/WorkspaceCloneSameNameSibsTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneSameNameSibsTest.java index 560ef84b04d..761f7cb0fdb 100644 --- a/src/test/org/apache/jackrabbit/test/api/WorkspaceCloneSameNameSibsTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneSameNameSibsTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -19,15 +19,12 @@ import javax.jcr.ItemExistsException; import javax.jcr.Node; import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; /** * WorkspaceCloneSameNameSibsTest contains tests for cloning nodes * as same name siblings between workspace. * - * @test - * @sources WorkspaceCloneSameNameSibsTest.java - * @executeClass org.apache.jackrabbit.test.api.WorkspaceCopySameNameSibsTest - * @keywords level2 */ public class WorkspaceCloneSameNameSibsTest extends AbstractWorkspaceSameNameSibsTest { @@ -38,7 +35,7 @@ public class WorkspaceCloneSameNameSibsTest extends AbstractWorkspaceSameNameSib */ public void testCloneNodesOrderingSupportedByParent() throws RepositoryException { // test assumes that repositry supports Orderable Child Node Support (optional) - /*String[] orderList = {nodeName1, nodeName2, nodeName3}; + String[] orderList = {nodeName1, nodeName2, nodeName3}; // copy node three times below a node and check the order for (int i = 0; i < orderList.length; i++) { @@ -50,22 +47,22 @@ public void testCloneNodesOrderingSupportedByParent() throws RepositoryException int cnt = 0; NodeIterator iter = node2.getNodes(); while (iter.hasNext()) { - Node n = (Node) iter.nextNode(); + Node n = iter.nextNode(); assertTrue(n.getName().equals(orderList[cnt])); cnt++; - } */ - //@TODO: Testcase corrupts workspace, so it's commented. - fail("Testcase corrupts workspace, so it's commented."); + } } /** * An ItemExistsException is thrown if a node or property already exists at * destAbsPath. - * @tck.config sameNameSibsFalseNodeType name of a node type that does not + *

          + *
        • {@code sameNameSibsFalseNodeType} name of a node type that does not * allows same name siblings. - * @tck.config nodeName3 name of a child node that does not allow same name + *
        • {@code nodeName3} name of a child node that does not allow same name * siblings.. + *
        */ public void testCloneNodesNodeExistsAtDestPath() throws RepositoryException { // create a parent node where allowSameNameSiblings are set to false @@ -84,4 +81,4 @@ public void testCloneNodesNodeExistsAtDestPath() throws RepositoryException { // successful } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneTest.java new file mode 100644 index 00000000000..55b25052310 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneTest.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api; + +import javax.jcr.AccessDeniedException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * WorkspaceCloneTest contains tests for cloning nodes between + * workspace. + * + */ +public class WorkspaceCloneTest extends AbstractWorkspaceCopyBetweenTest { + + /** + * If successful, the changes are persisted immediately, there is no need to + * call save. + */ + public void testCloneNodes() throws RepositoryException { + // clone referenceable node below non-referenceable node + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + workspaceW2.clone(workspace.getName(), node1.getPath(), dstAbsPath, true); + + // there should not be any pending changes after clone + assertFalse(superuserW2.hasPendingChanges()); + } + + /** + * If successful, the changes are persisted immediately, there is no need to + * call save. + */ + public void testCloneNodesTwice() throws RepositoryException, + NotExecutableException { + // clone referenceable node below non-referenceable node + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + + Node folder = node1.addNode(nodeName3); + ensureMixinType(folder, mixReferenceable); + node1.save(); + workspaceW2.clone(workspace.getName(), node1.getPath(), dstAbsPath, true); + workspaceW2.clone(workspace.getName(), node1.getPath(), dstAbsPath, true); + + // there should not be any pending changes after clone + assertFalse(superuserW2.hasPendingChanges()); + } + + /** + * A NoSuchWorkspaceException is thrown if srcWorkspace does not exist. + */ + public void testCloneNodesInvalidWorkspace() throws RepositoryException { + // clone a node to a non-existing workspace + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + try { + workspaceW2.clone(getNonExistingWorkspaceName(superuser), node1.getPath(), dstAbsPath, true); + fail("Invalid Source Workspace should throw NoSuchWorkspaceException."); + } catch (NoSuchWorkspaceException e) { + // successful + } + } + + /** + * The destAbsPath provided must not have an index on its final element. If + * it does, then a RepositoryException is thrown. Strictly speaking, the + * destAbsPath parameter is actually an absolute path to the parent node of + * the new location, appended with the new name desired for the copied node. + * It does not specify a position within the child node ordering. + */ + public void testCloneNodesAbsolutePath() { + try { + // copy referenceable node to an absolute path containing index + String dstAbsPath = node2W2.getPath() + "/" + node1.getName() + "[2]"; + workspaceW2.clone(workspace.getName(), node1.getPath(), dstAbsPath, true); + fail("Cloning a node to an absolute path containing index should not be possible."); + } catch (RepositoryException e) { + // successful + } + } + + /** + * A ConstraintViolationException is thrown if the operation would violate a + * node-type or other implementation-specific constraint. + */ + public void testCloneNodesConstraintViolationException() throws RepositoryException { + // if parent node is nt:base then no sub nodes can be created + String nodetype = testNodeTypeNoChildren == null ? ntBase : testNodeTypeNoChildren; + Node subNodesNotAllowedNode = testRootNodeW2.addNode(nodeName3, nodetype); + testRootNodeW2.save(); + try { + String dstAbsPath = subNodesNotAllowedNode.getPath() + "/" + node2.getName(); + workspaceW2.clone(workspace.getName(), node2.getPath(), dstAbsPath, true); + fail("Cloning a node below a node which can not have any sub nodes should throw a ConstraintViolationException."); + } catch (ConstraintViolationException e) { + // successful + } + } + + + /** + * An AccessDeniedException is thrown if the current session (i.e., the + * session that was used to acquire this Workspace object) does not have + * sufficient access permissions to complete the operation. + */ + public void testCloneNodesAccessDenied() throws RepositoryException { + // get read only session + Session readOnlySuperuser = getHelper().getReadOnlySession(); + try { + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + try { + readOnlySuperuser.getWorkspace().clone(workspaceW2.getName(), node1.getPath(), dstAbsPath, true); + fail("Cloning in a read-only session should throw an AccessDeniedException."); + } catch (AccessDeniedException e) { + // successful + } + } finally { + readOnlySuperuser.logout(); + } + } + + /** + * A PathNotFoundException is thrown if the node at srcAbsPath or the parent + * of the new node at destAbsPath does not exist. + */ + public void testCloneNodesPathNotExisting() throws RepositoryException { + + String srcAbsPath = node1.getPath(); + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + + // srcAbsPath is not existing + String invalidSrcPath = srcAbsPath + "invalid"; + try { + workspaceW2.clone(workspace.getName(), invalidSrcPath, dstAbsPath, true); + fail("Not existing source path '" + invalidSrcPath + "' should throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // successful + } + + // dstAbsPath parent is not existing + String invalidDstParentPath = node2W2.getPath() + "invalid/" + node1.getName(); + try { + workspaceW2.clone(workspace.getName(), srcAbsPath, invalidDstParentPath, true); + fail("Not existing destination parent path '" + invalidDstParentPath + "' should throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // successful + } + } + + /** + * A LockException is thrown if a lock prevents the copy. + */ + public void testCloneNodesLocked() + throws RepositoryException, NotExecutableException { + // we assume repository supports locking + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + + // get lock target node in destination wsp through other session + Node lockTarget = (Node) rwSessionW2.getItem(node2W2.getPath()); + + // add mixin "lockable" to be able to lock the node + ensureMixinType(lockTarget, mixLockable); + lockTarget.getParent().save(); + + // lock dst parent node using other session + lockTarget.lock(true, true); + + try { + workspaceW2.clone(workspace.getName(), node1.getPath(), dstAbsPath, true); + fail("LockException was expected."); + } catch (LockException e) { + // successful + } finally { + lockTarget.unlock(); + } + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/WorkspaceCloneVersionableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneVersionableTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/WorkspaceCloneVersionableTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneVersionableTest.java index bfd2e5833b2..e0079cf246c 100644 --- a/src/test/org/apache/jackrabbit/test/api/WorkspaceCloneVersionableTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCloneVersionableTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -19,15 +19,13 @@ import javax.jcr.RepositoryException; import javax.jcr.version.VersionException; +import org.apache.jackrabbit.test.NotExecutableException; + /** * WorkspaceCloneVersionableTest contains tests for cloning * versionable nodes between workspace. * - * @test - * @sources WorkspaceCloneVersionableTest.java - * @executeClass org.apache.jackrabbit.test.api.WorkspaceCloneVersionableTest - * @keywords level2 versioning */ public class WorkspaceCloneVersionableTest extends AbstractWorkspaceVersionableTest { @@ -36,7 +34,8 @@ public class WorkspaceCloneVersionableTest extends AbstractWorkspaceVersionableT * versionable and checked-in, or is non-versionable but its nearest * versionable ancestor is checked-in. */ - public void testCloneNodesVersionableAndCheckedIn() throws RepositoryException { + public void testCloneNodesVersionableAndCheckedIn() throws RepositoryException, + NotExecutableException { // prepare the test data // create a non-versionable node below a versionable node // required for having a nearest versionable ancestor to a nonversionable sub node @@ -63,4 +62,4 @@ public void testCloneNodesVersionableAndCheckedIn() throws RepositoryException { // successful } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesReferenceableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesReferenceableTest.java new file mode 100644 index 00000000000..52748c0d19e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesReferenceableTest.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.test.api; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * WorkspaceCopyBetweenWorkspacesReferenceableTest contains tests + * for copying referenceable nodes between workspace. + * + */ +public class WorkspaceCopyBetweenWorkspacesReferenceableTest extends AbstractWorkspaceReferenceableTest { + + /** + * Copies of referenceable nodes (nodes with UUIDs) are automatically given + * new UUIDs. + */ + public void testCopyNodesReferenceableNodesNewUUID() throws RepositoryException, + NotExecutableException { + // add mixin referenceable to node1 + addMixinReferenceableToNode(node1); + + // copy referenceable node below non-referenceable node + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + workspaceW2.copy(workspace.getName(), node1.getPath(), dstAbsPath); + + // uuid of copied node should be different than original node uuid + String originalUUID = node1.getUUID(); + Node copiedNode = node2W2.getNode(node1.getName()); + String copiedUUID = copiedNode.getUUID(); + + assertFalse(originalUUID.equals(copiedUUID)); + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesSameNameSibsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesSameNameSibsTest.java similarity index 80% rename from src/test/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesSameNameSibsTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesSameNameSibsTest.java index 17bbf35a3e9..8f6c980d85e 100644 --- a/src/test/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesSameNameSibsTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesSameNameSibsTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -25,10 +25,6 @@ * WorkspaceCopyBetweenWorkspacesSameNameSibsTest contains tests * for copying nodes as same name siblings between workspace. * - * @test - * @sources WorkspaceCopyBetweenWorkspacesSameNameSibsTest.java - * @executeClass org.apache.jackrabbit.test.api.WorkspaceCopyBetweenWorkspacesSameNameSibsTest - * @keywords level2 */ public class WorkspaceCopyBetweenWorkspacesSameNameSibsTest extends AbstractWorkspaceSameNameSibsTest { @@ -50,7 +46,7 @@ public void testCopyNodesOrderingSupportedByParent() throws RepositoryException int cnt = 0; NodeIterator iter = node2.getNodes(); while (iter.hasNext()) { - Node n = (Node) iter.nextNode(); + Node n = iter.nextNode(); assertTrue(n.getName().equals(orderList[cnt])); cnt++; @@ -60,10 +56,12 @@ public void testCopyNodesOrderingSupportedByParent() throws RepositoryException /** * An ItemExistsException is thrown if a node or property already exists at * destAbsPath. - * @tck.config sameNameSibsFalseNodeType name of a node type that does not + *
          + *
        • {@code sameNameSibsFalseNodeType} name of a node type that does not * allows same name siblings. - * @tck.config nodeName3 name of a child node that does not allow same name + *
        • {@code nodeName3} name of a child node that does not allow same name * siblings.. + *
        */ public void testCopyNodesNodeExistsAtDestPath() throws RepositoryException { // create a parent node where allowSameNameSiblings are set to false @@ -82,4 +80,4 @@ public void testCopyNodesNodeExistsAtDestPath() throws RepositoryException { // successful } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesTest.java new file mode 100644 index 00000000000..a2122efa190 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Repository; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; + +/** + * WorkspaceCopyBetweenWorkspacesTest contains tests for copying + * nodes between workspace.
        + * + */ +public class WorkspaceCopyBetweenWorkspacesTest extends AbstractWorkspaceCopyBetweenTest { + + /** + * Operation is performed entirely within the persistent workspace, it does + * not involve transient storage and therefore does not require a save + */ + public void testCopyNodes() throws RepositoryException { + // copy referenceable node below non-referenceable node + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + workspaceW2.copy(workspace.getName(), node1.getPath(), dstAbsPath); + + // there should not be any pending changes after copy + assertFalse(superuserW2.hasPendingChanges()); + } + + /** + * A NoSuchWorkspaceException is thrown if srcWorkspace does not exist. + */ + public void testCopyNodesInvalidWorkspace() throws RepositoryException { + // copy referenceable node below non-referenceable node to a non-existing workspace + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + try { + superuserW2.getWorkspace().copy(getNonExistingWorkspaceName(superuser), node1.getPath(), dstAbsPath); + fail("Invalid Source Workspace should throw NoSuchWorkspaceException."); + } catch (NoSuchWorkspaceException e) { + // successful + } + } + + + /** + * The destAbsPath provided must not have an index on its final element. If + * it does, then a RepositoryException is thrown. Strictly speaking, the + * destAbsPath parameter is actually an absolute path to the parent node of + * the new location, appended with the new name desired for the copied node. + * It does not specify a position within the child node ordering. + */ + public void testCopyNodesAbsolutePath() { + try { + // copy referenceable node to an absolute path containing index + String dstAbsPath = node2W2.getPath() + "/" + node1.getName() + "[2]"; + workspaceW2.copy(workspace.getName(), node1.getPath(), dstAbsPath); + fail("Copying a node to an absolute path containing index should not be possible."); + } catch (RepositoryException e) { + // successful + } + } + + /** + * A ConstraintViolationException is thrown if the operation would violate a + * node-type or other implementation-specific constraint. + */ + public void testCopyNodesConstraintViolationException() throws RepositoryException { + // if parent node is nt:base then no sub nodes can be created + String nodetype = testNodeTypeNoChildren == null ? ntBase : testNodeTypeNoChildren; + Node subNodesNotAllowedNode = testRootNodeW2.addNode(nodeName3, nodetype); + testRootNodeW2.save(); + try { + String dstAbsPath = subNodesNotAllowedNode.getPath() + "/" + node2.getName(); + workspaceW2.copy(workspace.getName(), node2.getPath(), dstAbsPath); + fail("Copying a node below a node which can not have any sub nodes should throw a ConstraintViolationException."); + } catch (ConstraintViolationException e) { + // successful + } + } + + + /** + * An AccessDeniedException is thrown if the current session (i.e., the + * session that was used to acquire this Workspace object) does not have + * sufficient access permissions to complete the operation. + */ + public void testCopyNodesAccessDenied() throws RepositoryException { + Session readOnlySuperuser = getHelper().getReadOnlySession(); + try { + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + try { + readOnlySuperuser.getWorkspace().copy(workspaceW2.getName(), node1W2.getPath(), dstAbsPath); + fail("Copy in a read-only session should throw an AccessDeniedException."); + } catch (AccessDeniedException e) { + // successful + } + } finally { + readOnlySuperuser.logout(); + } + } + + /** + * A PathNotFoundException is thrown if the node at srcAbsPath or the parent + * of the new node at destAbsPath does not exist. + */ + public void testCopyNodesPathNotExisting() throws RepositoryException { + + String srcAbsPath = node1.getPath(); + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + + // srcAbsPath is not existing + String invalidSrcPath = srcAbsPath + "invalid"; + try { + workspaceW2.copy(workspace.getName(), invalidSrcPath, dstAbsPath); + fail("Not existing source path '" + invalidSrcPath + "' should throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // successful + } + + // dstAbsPath parent is not existing + String invalidDstParentPath = node2W2.getPath() + "invalid/" + node1.getName(); + try { + workspaceW2.copy(workspace.getName(), srcAbsPath, invalidDstParentPath); + fail("Not existing destination parent path '" + invalidDstParentPath + "' should throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // successful + } + } + + /** + * A LockException is thrown if a lock prevents the copy. + */ + public void testCopyNodesLocked() throws RepositoryException, NotExecutableException { + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Repository does not support locking."); + } + // we assume repository supports locking + String dstAbsPath = node2W2.getPath() + "/" + node1.getName(); + + // get lock target node in destination wsp through other session + Node lockTarget = (Node) rwSessionW2.getItem(node2W2.getPath()); + + // add mixin "lockable" to be able to lock the node + ensureMixinType(lockTarget, mixLockable); + lockTarget.getParent().save(); + + // lock dst parent node using other session + lockTarget.lock(true, true); + + try { + workspaceW2.copy(workspace.getName(), node1.getPath(), dstAbsPath); + fail("LockException was expected."); + } catch (LockException e) { + // successful + } finally { + lockTarget.unlock(); + } + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesVersionableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesVersionableTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesVersionableTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesVersionableTest.java index 7002c127468..e57010f76c6 100644 --- a/src/test/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesVersionableTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyBetweenWorkspacesVersionableTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -19,14 +19,12 @@ import javax.jcr.RepositoryException; import javax.jcr.version.VersionException; +import org.apache.jackrabbit.test.NotExecutableException; + /** * WorkspaceCopyBetweenWorkspacesVersionableTest contains tests for * copying versionable nodes between workspace. * - * @test - * @sources WorkspaceCopyBetweenWorkspacesVersionableTest.java - * @executeClass org.apache.jackrabbit.test.api.WorkspaceCopyBetweenWorkspacesVersionableTest - * @keywords level2 versioning */ public class WorkspaceCopyBetweenWorkspacesVersionableTest extends AbstractWorkspaceVersionableTest { @@ -35,7 +33,8 @@ public class WorkspaceCopyBetweenWorkspacesVersionableTest extends AbstractWorks * versionable and checked-in, or is non-versionable but its nearest * versionable ancestor is checked-in. */ - public void testCopyNodesVersionableAndCheckedIn() throws RepositoryException { + public void testCopyNodesVersionableAndCheckedIn() throws RepositoryException, + NotExecutableException { // prepare the test data // create a non-versionable node below a versionable node // required for having a nearest versionable ancestor to a nonversionable sub node @@ -62,4 +61,4 @@ public void testCopyNodesVersionableAndCheckedIn() throws RepositoryException { // successful } } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyReferenceableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyReferenceableTest.java new file mode 100644 index 00000000000..4ea33a00286 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyReferenceableTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** + * WorkspaceCopyReferenceableTest contains tests for copying + * referenceable nodes in one workspace. + * + */ +public class WorkspaceCopyReferenceableTest extends AbstractWorkspaceReferenceableTest { + + protected String getOtherWorkspaceName() throws NotExecutableException { + return workspace.getName(); + } + + protected void initNodesW2() throws RepositoryException { + // nothing to do. + } + + /** + * Copies of referenceable nodes (nodes with UUIDs) are automatically given + * new UUIDs. + */ + public void testCopyNodesNewUUID() throws RepositoryException, + NotExecutableException { + // add mixin referenceable to node1 + addMixinReferenceableToNode(node1); + + // copy referenceable node below non-referenceable node + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + workspace.copy(node1.getPath(), dstAbsPath); + + // uuid of copied node should be different than original node uuid + String originalUUID = node1.getUUID(); + Node copiedNode = node2.getNode(node1.getName()); + String copiedUUID = copiedNode.getUUID(); + + assertFalse(originalUUID.equals(copiedUUID)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopySameNameSibsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopySameNameSibsTest.java new file mode 100644 index 00000000000..ccd40ff20a8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopySameNameSibsTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; + +/** + * WorkspaceCopySameNameSibsTest contains tests for copying nodes + * as same name siblings in one workspace. + * + */ +public class WorkspaceCopySameNameSibsTest extends AbstractWorkspaceSameNameSibsTest { + + protected String getOtherWorkspaceName() throws NotExecutableException { + return workspace.getName(); + } + + protected void initNodesW2() throws RepositoryException { + // nothing to do. + } + + /** + * If ordering is supported by the node type of the parent node of the new + * location, then the newly moved node is appended to the end of the child + * node list. + */ + public void testCopyNodesOrderingSupportedByParent() throws RepositoryException { + // test assumes that repositry supports Orderable Child Node Support (optional) + String[] orderList = {nodeName1, nodeName2, nodeName3}; + + // copy node three times below a node and check the order + for (int i = 0; i < orderList.length; i++) { + workspace.copy(node1.getPath(), node2.getPath() + "/" + orderList[i]); + } + + // check regarding orderList if nodes are added at the end + int cnt = 0; + NodeIterator iter = node2.getNodes(); + while (iter.hasNext()) { + Node n = iter.nextNode(); + + assertTrue(n.getName().equals(orderList[cnt])); + cnt++; + } + } + + /** + * An ItemExistsException is thrown if a node or property already exists at + * destAbsPath. + *
          + *
        • {@code sameNameSibsFalseNodeType} name of a node type that does not + * allows same name siblings. + *
        • {@code nodeName3} name of a child node that does not allow same name + * siblings.. + *
        + */ + public void testCopyNodesNodeExistsAtDestPath() throws RepositoryException { + // create a parent node where allowSameNameSiblings are set to false + Node snsfNode = testRootNode.addNode(nodeName3, sameNameSibsFalseNodeType.getName()); + testRootNode.getSession().save(); + + String dstAbsPath = snsfNode.getPath() + "/" + node1.getName(); + workspace.copy(node1.getPath(), dstAbsPath); + + // try to copy again the node to same destAbsPath where node already exists + try { + workspace.copy(node1.getPath(), dstAbsPath); + fail("Node exists below '" + dstAbsPath + "'. Test should fail."); + } catch (ItemExistsException e) { + // successful + } + } + + /** + * NO ItemExistsException is thrown if a node already exists at destAbsPath + * and the node allows same-name-siblings. + *
          + *
        • {@code sameNameSibsTrueNodeType} name of a node type that + * allows same name siblings. + *
        • {@code nodeName3} name of a child node that allows children with + * same name. + *
        + */ + public void testCopyNodesNodeExistsAtDestPath2() throws RepositoryException { + // create a parent node where allowSameNameSiblings are set to true + Node snsfNode = testRootNode.addNode(nodeName3, sameNameSibsTrueNodeType.getName()); + testRootNode.getSession().save(); + + String dstAbsPath = snsfNode.getPath() + "/" + node1.getName(); + workspace.copy(node1.getPath(), dstAbsPath); + + // try to copy again the node to same destAbsPath where node already exists + // must succeed + workspace.copy(node1.getPath(), dstAbsPath); + + // make sure the parent now has 2 children with the same name + NodeIterator it = snsfNode.getNodes(node1.getName()); + long size = it.getSize(); + if (it.getSize() == -1) { + size = 0; + while (it.hasNext()) { + it.nextNode(); + size++; + } + } + assertEquals("After second copy 2 same-name-siblings must exist",2, size); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyTest.java new file mode 100644 index 00000000000..adc04592441 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyTest.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.test.api; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * WorkspaceCopyTest contains tests for copying nodes in one + * workspace. + * + */ +public class WorkspaceCopyTest extends AbstractWorkspaceCopyTest { + + /** + * Operation is performed entirely within the persistent workspace, it does + * not involve transient storage and therefore does not require a save + */ + public void testCopyNodes() throws RepositoryException { + // copy referenceable node below non-referenceable node + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + workspace.copy(node1.getPath(), dstAbsPath); + + // there should not be any pending changes after copy + assertFalse(superuser.hasPendingChanges()); + } + + /** + * The destAbsPath provided must not have an index on its final element. If + * it does, then a RepositoryException is thrown. Strictly speaking, the + * destAbsPath parameter is actually an absolute path to the parent node of + * the new location, appended with the new name desired for the copied node. + * It does not specify a position within the child node ordering. + */ + public void testCopyNodesAbsolutePath() { + try { + // copy referenceable node to an absolute path containing index + String dstAbsPath = node2.getPath() + "/" + node1.getName() + "[2]"; + workspace.copy(node1.getPath(), dstAbsPath); + fail("Copying a node to an absolute path containing index should not be possible."); + } catch (RepositoryException e) { + // successful + + } + } + + /** + * A ConstraintViolationException is thrown if the operation would violate a + * node-type or other implementation-specific constraint. + */ + public void testCopyNodesConstraintViolationException() throws RepositoryException { + // if parent node is nt:base then no sub nodes can be created + String nodetype = testNodeTypeNoChildren == null ? ntBase : testNodeTypeNoChildren; + Node subNodesNotAllowedNode = testRootNode.addNode(nodeName3, nodetype); + testRootNode.getSession().save(); + try { + String dstAbsPath = subNodesNotAllowedNode.getPath() + "/" + node2.getName(); + workspace.copy(node2.getPath(), dstAbsPath); + fail("Copying a node below a node which can not have any sub nodes should throw a ConstraintViolationException."); + } catch (ConstraintViolationException e) { + // successful + } + } + + /** + * An AccessDeniedException is thrown if the current session (i.e., the + * session that was used to acquire this Workspace object) does not have + * sufficient access permissions to complete the operation. + */ + public void testCopyNodesAccessDenied() throws RepositoryException { + Session readOnlySuperuser = getHelper().getReadOnlySession(); + try { + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + try { + readOnlySuperuser.getWorkspace().copy(node1.getPath(), dstAbsPath); + fail("Copy in a read-only session should throw an AccessDeniedException."); + } catch (AccessDeniedException e) { + // successful + } + } finally { + readOnlySuperuser.logout(); + } + } + + /** + * A PathNotFoundException is thrown if the node at srcAbsPath or the parent + * of the new node at destAbsPath does not exist. + */ + public void testCopyNodesPathNotExisting() throws RepositoryException { + + String srcAbsPath = node1.getPath(); + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + + // srcAbsPath is not existing + String invalidSrcPath = srcAbsPath + "invalid"; + try { + workspace.copy(invalidSrcPath, dstAbsPath); + fail("Not existing source path '" + invalidSrcPath + "' should throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // successful + } + + // dstAbsPath parent is not existing + String invalidDstParentPath = node2.getPath() + "invalid/" + node1.getName(); + try { + workspace.copy(srcAbsPath, invalidDstParentPath); + fail("Not existing destination parent path '" + invalidDstParentPath + "' should throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // successful + } + } + + /** + * A LockException is thrown if a lock prevents the copy. + */ + public void testCopyNodesLocked() throws RepositoryException, + NotExecutableException { + // we assume repository supports locking + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + + // get other session + Session otherSession = getHelper().getReadWriteSession(); + + try { + // get lock target node in destination wsp through other session + Node lockTarget = (Node) otherSession.getItem(node2.getPath()); + + // add mixin "lockable" to be able to lock the node + ensureMixinType(lockTarget, mixLockable); + lockTarget.getParent().save(); + + // lock dst parent node using other session + lockTarget.lock(true, true); + + try { + workspace.copy(node1.getPath(), dstAbsPath); + fail("LockException was expected."); + } catch (LockException e) { + // successful + } finally { + lockTarget.unlock(); + } + } finally { + otherSession.logout(); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyVersionableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyVersionableTest.java new file mode 100644 index 00000000000..c79c6b5c14b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceCopyVersionableTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + + +/** + * WorkspaceCopyVersionableTest contains tests for copying + * versionable nodes in one workspace. + * + */ +public class WorkspaceCopyVersionableTest extends AbstractWorkspaceVersionableTest { + + protected String getOtherWorkspaceName() throws NotExecutableException { + return workspace.getName(); + } + + protected void initNodesW2() throws RepositoryException { + // nothing to do. + } + + /** + * A VersionException is thrown if the parent node of destAbsPath is + * versionable and checked-in, or is non-versionable but its nearest + * versionable ancestor is checked-in. + */ + public void testCopyNodesVersionableAndCheckedIn() throws RepositoryException, NotExecutableException { + // prepare the test data + // create a non-versionable node below a versionable node + // required for having a nearest versionable ancestor to a nonversionable sub node + String dstAbsPath = node1.getPath() + "/" + node2.getName(); + workspace.copy(node2.getPath(), dstAbsPath); + + try { + // make parent node versionable and check-in + addMixinVersionableToNode(testRootNode, node1); + node1.checkin(); + } + catch (ConstraintViolationException ex) { + throw new NotExecutableException("server does not support making the parent versionable: " + ex.getMessage()); + } + + // 1. parent node of destAbsPath is non-versionable but its nearest versionable ancestor is checked-in + try { + workspace.copy(node2.getPath(), dstAbsPath + "/" + node2.getName()); + fail("Copying a node to a node's versionable and checked-in nearest ancestor node of destAbsPath should throw VersionException."); + } catch (VersionException e) { + // successful + } + + // 2. parent node of destAbsPath is versionable and checked-in + try { + workspace.copy(node2.getPath(), node1.getPath() + "/" + node2.getName()); + fail("Copying a node to a versionable and checked-in parent node of destAbsPath should throw VersionException."); + } catch (VersionException e) { + // successful + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceManagementTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceManagementTest.java new file mode 100644 index 00000000000..52fec35e78f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceManagementTest.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.test.api; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import javax.jcr.NoSuchWorkspaceException; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * WorkspaceManagementTest... + */ +public class WorkspaceManagementTest extends AbstractJCRTest { + + private Workspace workspace; + + protected void setUp() throws Exception { + super.setUp(); + + super.checkSupportedOption(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED); + + workspace = superuser.getWorkspace(); + } + + /** + * Tests {@link javax.jcr.Workspace#createWorkspace(String)} and + * {@link javax.jcr.Workspace#createWorkspace(String, String)}. + * + * @throws javax.jcr.RepositoryException + */ + public void testCreateWorkspace() throws RepositoryException { + + try { + workspace.createWorkspace(workspaceName); + fail("creating a new workspace with the name of an already existing one must fail"); + } catch (RepositoryException e) { + // excepted + } + + // create empty workspace + workspace.createWorkspace("tmp" + System.currentTimeMillis()); + + // create pre-initialized workspace, specifying unknwon src workspace + try { + workspace.createWorkspace("tmp" + System.currentTimeMillis(), "unknownworkspace"); + fail("NoSuchWorkspaceException expected"); + } catch (NoSuchWorkspaceException e) { + // excepted + } + + // create pre-initialized workspace, specifying existing src workspace + workspace.createWorkspace("tmp" + System.currentTimeMillis(), superuser.getWorkspace().getName()); + } + + + /** + * Tests {@link javax.jcr.Workspace#deleteWorkspace(String)}. + * + * @throws javax.jcr.RepositoryException + */ + public void testDeleteWorkspace() throws RepositoryException { + + try { + workspace.deleteWorkspace("unknownworkspace"); + fail("NoSuchWorkspaceException expected"); + } catch (NoSuchWorkspaceException e) { + // excepted + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveReferenceableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveReferenceableTest.java new file mode 100644 index 00000000000..2bf2c05e12b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveReferenceableTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** + * WorkspaceMoveReferenceableTest contains tests for moving + * referenceable nodes in one workspace. + * + */ +public class WorkspaceMoveReferenceableTest extends AbstractWorkspaceReferenceableTest { + + protected String getOtherWorkspaceName() throws NotExecutableException { + return workspace.getName(); + } + + protected void initNodesW2() throws RepositoryException { + // nothing to do. + } + + /** + * Copies of referenceable nodes (nodes with UUIDs) remains their original + * UUIDs. + */ + public void testMoveNodesReferenceableNodesNewUUID() throws RepositoryException, + NotExecutableException { + // add mixin referenceable to node1 + addMixinReferenceableToNode(node1); + + // copy referenceable node below non-referenceable node + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + String originalUUID = node1.getUUID(); // remember for check + workspace.move(node1.getPath(), dstAbsPath); + + // uuid of copied node should be different than original node uuid + Node movedNode = node2.getNode(node1.getName()); + String movedUUID = movedNode.getUUID(); + + assertTrue(originalUUID.equals(movedUUID)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveSameNameSibsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveSameNameSibsTest.java new file mode 100644 index 00000000000..555add13873 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveSameNameSibsTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; + +/** + * WorkspaceMoveSameNameSibsTest contains tests for moving nodes + * with same name siblings supported in one workspace. + * + */ +public class WorkspaceMoveSameNameSibsTest extends AbstractWorkspaceSameNameSibsTest { + + protected String getOtherWorkspaceName() throws NotExecutableException { + return workspace.getName(); + } + + protected void initNodesW2() throws RepositoryException { + // nothing to do. + } + + /** + * If ordering is supported by the node type of the parent node of the new + * location, then the newly moved node is appended to the end of the child + * node list. + */ + public void testMoveNodesOrderingSupportedByParent() throws RepositoryException { + // test assumes that repositry supports Orderable Child Node Support (optional) + String[] orderList = {nodeName1, nodeName2, nodeName3}; + + // create a new node to move nodes + Node newNode = testRootNode.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + + // copy node three times below a node and check the order + for (int i = 0; i < orderList.length; i++) { + workspace.copy(node1.getPath(), newNode.getPath() + "/" + orderList[i]); + } + + // check regarding orderList with the counter if nodes are added at the end + int cnt = 0; + NodeIterator iter = node2.getNodes(); + while (iter.hasNext()) { + Node n = iter.nextNode(); + + assertTrue(n.getName().equals(orderList[cnt])); + cnt++; + } + } + + /** + * An ItemExistsException is thrown if a node or property already exists at + * destAbsPath. + *
          + *
        • {@code sameNameSibsFalseNodeType} name of a node type that does not + * allows same name siblings. + *
        • {@code nodeName3} name of a child node that does not allow same name + * siblings.. + *
        + */ + public void testMoveNodesNodeExistsAtDestPath() throws RepositoryException { + // create a parent node where allowSameNameSiblings are set to false + Node snsfNode = testRootNode.addNode(nodeName3, sameNameSibsFalseNodeType.getName()); + testRootNode.getSession().save(); + + String dstAbsPath = snsfNode.getPath() + "/" + node1.getName(); + workspace.copy(node1.getPath(), dstAbsPath); + + // try to copy again the node to same destAbsPath + // property already exist + try { + workspace.move(node1.getPath(), dstAbsPath); + fail("Node exists below '" + dstAbsPath + "'. Test should fail."); + } catch (ItemExistsException e) { + // successful + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveTest.java new file mode 100644 index 00000000000..9ac3116d50f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveTest.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.test.api; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * WorkspaceMoveTest contains tests for copying nodes in one + * workspace. + * + */ +public class WorkspaceMoveTest extends AbstractWorkspaceCopyTest { + + /** + * Operation is performed entirely within the persistent workspace, it does + * not involve transient storage and therefore does not require a save + */ + public void testMoveNodes() throws RepositoryException { + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + workspace.move(node1.getPath(), dstAbsPath); + + // there should not be any pending changes after copy + assertFalse(superuser.hasPendingChanges()); + } + + /** + * The destAbsPath provided must not have an index on its final element. If + * it does, then a RepositoryException is thrown. Strictly speaking, the + * destAbsPath parameter is actually an absolute path to the parent node of + * the new location, appended with the new name desired for the copied node. + * It does not specify a position within the child node ordering. + */ + public void testMoveNodesAbsolutePath() { + try { + // copy referenceable node to an absolute path containing index + String dstAbsPath = node2.getPath() + "/" + node1.getName() + "[2]"; + workspace.move(node1.getPath(), dstAbsPath); + fail("Moving a node to an absolute path containing index should not be possible."); + } catch (RepositoryException e) { + // successful + + } + } + + /** + * A ConstraintViolationException is thrown if the operation would violate a + * node-type or other implementation-specific constraint. + */ + public void testMoveNodesConstraintViolationException() throws RepositoryException { + // if parent node is nt:base then no sub nodes can be created + String nodetype = testNodeTypeNoChildren == null ? ntBase : testNodeTypeNoChildren; + Node subNodesNotAllowedNode = testRootNode.addNode(nodeName3, nodetype); + testRootNode.getSession().save(); + try { + String dstAbsPath = subNodesNotAllowedNode.getPath() + "/" + node2.getName(); + workspace.move(node2.getPath(), dstAbsPath); + fail("Moving a node below a node which can not have any sub nodes should throw a ConstraintViolationException."); + } catch (ConstraintViolationException e) { + // successful + } + } + + + /** + * An AccessDeniedException is thrown if the current session (i.e., the + * session that was used to acquire this Workspace object) does not have + * sufficient access permissions to complete the operation. + */ + public void testMoveNodesAccessDenied() throws RepositoryException { + Session readOnlySuperuser = getHelper().getReadOnlySession(); + try { + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + try { + readOnlySuperuser.getWorkspace().move(node1.getPath(), dstAbsPath); + fail("Copy in a read-only session should throw an AccessDeniedException."); + } catch (AccessDeniedException e) { + // successful + } + } finally { + readOnlySuperuser.logout(); + } + } + + /** + * A PathNotFoundException is thrown if the node at srcAbsPath or the parent + * of the new node at destAbsPath does not exist. + */ + public void testMoveNodesPathNotExisting() throws RepositoryException { + + String srcAbsPath = node1.getPath(); + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + + // srcAbsPath is not existing + String invalidSrcPath = srcAbsPath + "invalid"; + try { + workspace.move(invalidSrcPath, dstAbsPath); + fail("Not existing source path '" + invalidSrcPath + "' should throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // successful + } + + // dstAbsPath parent is not existing + String invalidDstParentPath = node2.getPath() + "invalid/" + node1.getName(); + try { + workspace.move(srcAbsPath, invalidDstParentPath); + fail("Not existing destination parent path '" + invalidDstParentPath + "' should throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // successful + } + } + + + /** + * A LockException is thrown if a lock prevents the copy. + */ + public void testMoveNodesLocked() throws RepositoryException, + NotExecutableException { + // we assume repository supports locking + String dstAbsPath = node2.getPath() + "/" + node1.getName(); + + // get other session + Session otherSession = getHelper().getReadWriteSession(); + + try { + // get lock target node in destination wsp through other session + Node lockTarget = (Node) otherSession.getItem(node2.getPath()); + + // add mixin "lockable" to be able to lock the node + ensureMixinType(lockTarget, mixLockable); + lockTarget.getParent().save(); + + // lock dst parent node using other session + lockTarget.lock(true, true); + + try { + workspace.move(node1.getPath(), dstAbsPath); + fail("LockException was expected."); + } catch (LockException e) { + // successful + } finally { + lockTarget.unlock(); + } + } finally { + otherSession.logout(); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveVersionableTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveVersionableTest.java new file mode 100644 index 00000000000..b3fd43adc8f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceMoveVersionableTest.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.test.api; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + + +/** + * WorkspaceMoveVersionableTest contains tests for moving + * versionable nodes in one workspace. + * + */ +public class WorkspaceMoveVersionableTest extends AbstractWorkspaceVersionableTest { + + protected String getOtherWorkspaceName() throws NotExecutableException { + return workspace.getName(); + } + + protected void initNodesW2() throws RepositoryException { + // nothing to do. + } + + /** + * A VersionException is thrown if the parent node of destAbsPath is + * versionable and checked-in, or is non-versionable but its nearest + * versionable ancestor is checked-in. + */ + public void testMoveNodesVersionableAndCheckedIn() throws RepositoryException, NotExecutableException { + // prepare the test data + // create a non-versionable node below a versionable node + // required for having a nearest versionable ancestor to a nonversionable sub node + String dstAbsPath = node1.getPath() + "/" + node2.getName(); + workspace.copy(node2.getPath(), dstAbsPath); + + try { + // make parent node versionable and check-in + addMixinVersionableToNode(testRootNode, node1); + node1.checkin(); + } + catch (ConstraintViolationException ex) { + throw new NotExecutableException("server does not support making the parent versionable: " + ex.getMessage()); + } + + // 1. parent node of destAbsPath is non-versionable but its nearest versionable ancestor is checked-in + try { + workspace.move(node2.getPath(), dstAbsPath + "/" + node2.getName()); + fail("Copying a node to a node's versionable and checked-in nearest ancestor node of destAbsPath should throw VersionException."); + } catch (VersionException e) { + // successful + } + + // 2. parent node of destAbsPath is versionable and checked-in + try { + workspace.move(node2.getPath(), node1.getPath() + "/" + node2.getName()); + fail("Copying a node to a versionable and checked-in parent node of destAbsPath should throw VersionException."); + } catch (VersionException e) { + // successful + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceReadMethodsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceReadMethodsTest.java new file mode 100644 index 00000000000..07a78a28036 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceReadMethodsTest.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.test.api; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; + +/** + * WorkspaceReadMethodsTest... + * + */ +public class WorkspaceReadMethodsTest extends AbstractJCRTest { + + /** + * Sets up the fixture for the test. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + } + + /** + * Tests the getSession() method which returns the same session object as + * this workspace was requested from. + */ + public void testGetSession() throws RepositoryException { + Session session = getHelper().getReadOnlySession(); + try { + Session otherSession = session.getWorkspace().getSession(); + assertSame("Workspace.getSession() returns not the same session object.", + session, otherSession); + } finally { + session.logout(); + } + } + + /** + * Tests that the name returned by Workspace.getName() is equal to the one + * used for login. + */ + public void testGetName() throws RepositoryException { + Session session = getHelper().getReadOnlySession(workspaceName); + try { + String name = session.getWorkspace().getName(); + if (workspaceName != null) { + assertEquals("Workspace.getName() returns wrong name.", + workspaceName, name); + } + } finally { + session.logout(); + } + } + + /** + * Tests Workspace.getQueryManager. This should just return correctly a + * QueryManager object. + */ + public void testGetQueryManager() throws RepositoryException { + Workspace ws = getHelper().getReadOnlySession().getWorkspace(); + try { + assertNotNull("Workspace does not return a QueryManager object.", ws.getQueryManager()); + } finally { + ws.getSession().logout(); + } + } + + /** + * Tests Workspace.getAccessibleWorkspaceNames() by logging into the + * Workspaces given by the returned names. The credentials are the same as + * used for accessing the current workspace. + */ + public void testGetAccessibleWorkspaceNames() throws RepositoryException { + Session session = getHelper().getReadOnlySession(); + try { + String[] wsNames = session.getWorkspace().getAccessibleWorkspaceNames(); + for (int i = 0; i < wsNames.length; i++) { + // login + Session s = getHelper().getReadOnlySession(wsNames[i]); + s.logout(); + } + } finally { + session.logout(); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceTest.java new file mode 100644 index 00000000000..0b6b0845874 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/WorkspaceTest.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.test.api; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Workspace; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * WorkspaceTest... + */ +public class WorkspaceTest extends AbstractJCRTest { + + private Workspace workspace; + + protected void setUp() throws Exception { + super.setUp(); + + workspace = superuser.getWorkspace(); + } + + /** + * Tests {@link javax.jcr.Workspace#getLockManager()}. + * + * @throws RepositoryException + */ + public void testGetLockManager() throws RepositoryException { + if (isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + assertNotNull(workspace.getLockManager()); + } else { + try { + workspace.getLockManager(); + fail("UnsupportedRepositoryOperationException expected. Locking is not supported."); + } catch (UnsupportedRepositoryOperationException e) { + // success. + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/AbstractLockTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/AbstractLockTest.java new file mode 100644 index 00000000000..0aac5725081 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/AbstractLockTest.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.test.api.lock; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** AbstractLockTest... */ +public abstract class AbstractLockTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(AbstractLockTest.class); + + protected LockManager lockMgr; + protected Node lockedNode; + protected Node childNode; + protected Lock lock; + + protected void setUp() throws Exception { + // check for lock support before creating the session in the super.setup + checkSupportedOption(Repository.OPTION_LOCKING_SUPPORTED); + + super.setUp(); + + lockedNode = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(lockedNode, mixLockable); + childNode = lockedNode.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + + lockMgr = getLockManager(testRootNode.getSession()); + lock = lockMgr.lock(lockedNode.getPath(), isDeep(), isSessionScoped(), getTimeoutHint(), getLockOwner()); + } + + protected void tearDown() throws Exception { + // release the lock created during setup + if (lockMgr != null && lockedNode != null && lockMgr.isLocked(lockedNode.getPath())) { + try { + lockMgr.unlock(lockedNode.getPath()); + } catch (RepositoryException e) { + // ignore + } + } + super.tearDown(); + } + + protected abstract boolean isSessionScoped(); + protected abstract boolean isDeep(); + + protected void assertLockable(Node n) + throws RepositoryException, NotExecutableException { + ensureMixinType(n, mixLockable); + n.getSession().save(); + } + + protected long getTimeoutHint() throws RepositoryException { + String timoutStr = getProperty(RepositoryStub.PROP_LOCK_TIMEOUT); + long hint = Long.MAX_VALUE; + if (timoutStr != null) { + try { + hint = Long.parseLong(timoutStr); + } catch (NumberFormatException e) { + log.warn(e.getMessage()); + } + } + return hint; + } + + protected String getLockOwner() throws RepositoryException { + String ownerStr = getProperty(RepositoryStub.PROP_LOCK_OWNER); + if (ownerStr == null) { + ownerStr = superuser.getUserID(); + } + return ownerStr; + } + + protected static LockManager getLockManager(Session session) throws RepositoryException { + return session.getWorkspace().getLockManager(); + } + + /** + * Test {@link javax.jcr.lock.Lock#isDeep()}. + */ + public void testIsDeep() { + assertEquals("Lock.isDeep must be consistent with lock call.", isDeep(), lock.isDeep()); + } + + /** + * Test {@link javax.jcr.lock.Lock#isLive()}. + */ + public void testIsLive() throws RepositoryException { + assertTrue("Lock.isLive must be true.", lock.isLive()); + } + + /** + * Test {@link javax.jcr.lock.Lock#refresh()} on a released lock. + */ + public void testRefresh() throws RepositoryException { + // refresh must succeed + lock.refresh(); + } + + // TODO: test if timeout gets reset upon Lock.refresh() + + /** + * Test {@link javax.jcr.lock.Lock#refresh()} on a released lock. + * + * @throws Exception + */ + public void testRefreshNotLive() throws Exception { + // release the lock + lockMgr.unlock(lockedNode.getPath()); + // refresh + try { + lock.refresh(); + fail("Refresh on a lock that is not alive must fail"); + } catch (LockException e) { + // success + } + } + + /** + * Test {@link javax.jcr.lock.Lock#getNode()}. + * + * @throws RepositoryException If an exception occurs. + */ + public void testLockHoldingNode() throws RepositoryException { + assertTrue("Lock.getNode() must be lockholding node.", lock.getNode().isSame(lockedNode)); + } + + /** + * Test {@link LockManager#isLocked(String)} and {@link javax.jcr.Node#isLocked()}. + * + * @throws RepositoryException If an exception occurs. + */ + public void testNodeIsLocked() throws RepositoryException { + assertTrue("Node must be locked after lock creation.", lockedNode.isLocked()); + assertTrue("Node must be locked after lock creation.", lockMgr.isLocked(lockedNode.getPath())); + } + + /** + * Test {@link LockManager#holdsLock(String)} and {@link javax.jcr.Node#holdsLock()}. + * + * @throws RepositoryException If an exception occurs. + */ + public void testNodeHoldsLocked() throws RepositoryException { + assertTrue("Node must hold lock after lock creation.", lockedNode.holdsLock()); + assertTrue("Node must hold lock after lock creation.", lockMgr.holdsLock(lockedNode.getPath())); + } + + + /** + * A locked node must also be locked if accessed by some other session. + */ + public void testLockVisibility() throws RepositoryException { + Session otherSession = getHelper().getReadWriteSession(); + try { + Node ln = (Node) otherSession.getItem(lockedNode.getPath()); + assertTrue("Locked node must also be locked for another session", ln.isLocked()); + assertTrue("Locked node must also be locked for another session", ln.holdsLock()); + assertTrue("Locked node must also be locked for another session", getLockManager(otherSession).holdsLock(ln.getPath())); + } finally { + otherSession.logout(); + } + } + + /** + * Test {@link javax.jcr.lock.Lock#isSessionScoped()} + */ + public void testIsSessionScoped() { + assertEquals("Lock.isSessionScoped must be consistent with lock call.", isSessionScoped(), lock.isSessionScoped()); + } + + /** + * Test {@link javax.jcr.lock.Lock#isLockOwningSession()} + * + * @throws RepositoryException If an exception occurs. + */ + public void testIsLockOwningSession() throws RepositoryException { + assertTrue("Session must be lock owner", lock.isLockOwningSession()); + assertTrue("Session must be lock owner", lockedNode.getLock().isLockOwningSession()); + assertTrue("Session must be lock owner", lockMgr.getLock(lockedNode.getPath()).isLockOwningSession()); + + Session otherSession = getHelper().getReadOnlySession(); + try { + Lock lck = otherSession.getNode(lockedNode.getPath()).getLock(); + assertFalse("Session must not be lock owner", lck.isLockOwningSession()); + + Lock lck2 = getLockManager(otherSession).getLock(lockedNode.getPath()); + assertFalse("Session must not be lock owner", lck2.isLockOwningSession()); + } finally { + otherSession.logout(); + } + + Session otherAdmin = getHelper().getSuperuserSession(); + try { + Lock lck = otherAdmin.getNode(lockedNode.getPath()).getLock(); + assertFalse("Other Session for the same userID must not be lock owner", lck.isLockOwningSession()); + + Lock lck2 = getLockManager(otherAdmin).getLock(lockedNode.getPath()); + assertFalse("Other Session for the same userID must not be lock owner", lck2.isLockOwningSession()); + + } finally { + otherAdmin.logout(); + } + } + + /** + * Test {@link javax.jcr.lock.Lock#getSecondsRemaining()} + */ + public void testGetSecondsRemaining() throws RepositoryException { + if (lock.isLive()) { + assertTrue("Seconds remaining must be a positive long.", lock.getSecondsRemaining() > 0); + } else { + assertTrue("Seconds remaining must be a negative long.", lock.getSecondsRemaining() < 0); + } + } + + /** + * Test {@link javax.jcr.lock.Lock#getSecondsRemaining()} + */ + public void testGetSecondsRemainingAfterUnlock() throws RepositoryException { + lockMgr.unlock(lockedNode.getPath()); + assertTrue("Lock has been released: seconds remaining must be a negative long.", lock.getSecondsRemaining() < 0); + } + + /** + * Test expiration of the lock + */ + public synchronized void testLockExpiration() + throws RepositoryException, NotExecutableException { + lockedNode.unlock(); + + long hint = 1; + lock = lockMgr.lock( + lockedNode.getPath(), isDeep(), isSessionScoped(), hint, null); + + // only test if timeout hint was respected. + long remaining = lock.getSecondsRemaining(); + if (remaining <= hint) { + if (remaining > 0) { + try { + wait(remaining * 4000); // wait four time as long to be safe + } catch (InterruptedException ignore) { + } + } + long secs = lock.getSecondsRemaining(); + assertTrue( + "A released lock must return a negative number of seconds, was: " + secs, + secs < 0); + String message = "If the timeout hint is respected the lock" + + " must be automatically released."; + assertFalse(message, lock.isLive()); + assertFalse(message, lockedNode.isLocked()); + assertFalse(message, lockMgr.isLocked(lockedNode.getPath())); + assertFalse(message, lockedNode.hasProperty(Property.JCR_LOCK_IS_DEEP)); + assertFalse(message, lockedNode.hasProperty(Property.JCR_LOCK_OWNER)); + } else { + throw new NotExecutableException("timeout hint was ignored."); + } + } + + /** + * Test expiration of the lock + */ + public synchronized void testOwnerHint() + throws RepositoryException, NotExecutableException { + lockedNode.unlock(); + + lock = lockMgr.lock(lockedNode.getPath(), isDeep(), isSessionScoped(), Long.MAX_VALUE, "test"); + + String owner = lock.getLockOwner(); + if (!"test".equals(lock.getLockOwner())) { + throw new NotExecutableException(); + } else { + assertTrue(lockedNode.hasProperty(Property.JCR_LOCK_OWNER)); + assertEquals("test", lockedNode.getProperty(Property.JCR_LOCK_OWNER).getString()); + } + } + + /** + * Test if Lock is properly released. + * + * @throws RepositoryException + */ + public void testUnlock() throws RepositoryException { + // release the lock + lockMgr.unlock(lockedNode.getPath()); + + // assert: lock must not be alive + assertFalse("lock must not be alive", lock.isLive()); + } + + /** + * Test {@link LockManager#unlock(String)} for a session that is not + * lock owner. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testUnlockByOtherSession() throws RepositoryException, NotExecutableException { + Session otherSession = getHelper().getReadWriteSession(); + try { + getLockManager(otherSession).unlock(lockedNode.getPath()); + fail("Another session must not be allowed to unlock."); + } catch (LockException e) { + // success + // make sure the node is still locked and the lock properties are + // still present. + assertTrue(lockMgr.isLocked(lockedNode.getPath())); + assertTrue(lockedNode.hasProperty(jcrlockIsDeep)); + assertTrue(lockedNode.hasProperty(jcrLockOwner)); + } finally { + otherSession.logout(); + } + } + + public void testIsLockedChild() throws RepositoryException { + assertEquals("Child node must be locked according to isDeep flag.", isDeep(), childNode.isLocked()); + assertEquals("Child node must be locked according to isDeep flag.", isDeep(), lockMgr.isLocked(childNode.getPath())); + } + + public void testIsLockedNewChild() throws RepositoryException { + Node newChild = lockedNode.addNode(nodeName3, testNodeType); + assertEquals("New child node must be locked according to isDeep flag.", isDeep(), + newChild.isLocked()); + assertEquals("New child node must be locked according to isDeep flag.", isDeep(), + lockMgr.isLocked(newChild.getPath())); + } + + public void testHoldsLockChild() throws RepositoryException { + assertFalse("Child node below a locked node must never be lock holder", + childNode.holdsLock()); + assertFalse("Child node below a locked node must never be lock holder", + lockMgr.holdsLock(childNode.getPath())); + } + + public void testHoldsLockNewChild() throws RepositoryException { + Node newChild = lockedNode.addNode(nodeName3, testNodeType); + assertFalse("Child node below a locked node must never be lock holder", + newChild.holdsLock()); + assertFalse("Child node below a locked node must never be lock holder", + lockMgr.holdsLock(newChild.getPath())); + } + + public void testGetLockOnChild() throws RepositoryException { + if (isDeep()) { + // get lock must succeed even if child is not lockable. + Lock lock = childNode.getLock(); + assertNotNull(lock); + assertTrue("Lock.getNode() must return the lock holding node", lockedNode.isSame(lock.getNode())); + + Lock lock2 = lockMgr.getLock(childNode.getPath()); + assertNotNull(lock2); + assertTrue("Lock.getNode() must return the lock holding node", lockedNode.isSame(lock2.getNode())); + } else { + try { + childNode.getLock(); + fail("Node.getLock() must throw if node is not locked."); + } catch (LockException e) { + // success + } + try { + lockMgr.getLock(childNode.getPath()); + fail("LockManager.getLock(String) must throw if node is not locked."); + } catch (LockException e) { + // success + } + } + } + + public void testGetLockOnNewChild() throws RepositoryException { + Node newChild = lockedNode.addNode(nodeName3, testNodeType); + if (isDeep()) { + // get lock must succeed even if child is not lockable. + Lock lock = newChild.getLock(); + assertNotNull(lock); + assertTrue("Lock.getNode() must return the lock holding node", lockedNode.isSame(lock.getNode())); + + Lock lock2 = lockMgr.getLock(newChild.getPath()); + assertNotNull(lock2); + assertTrue("Lock.getNode() must return the lock holding node", lockedNode.isSame(lock2.getNode())); + } else { + try { + newChild.getLock(); + fail("Node.getLock() must throw if node is not locked."); + } catch (LockException e) { + // success + } + try { + lockMgr.getLock(newChild.getPath()); + fail("LockManager.getLock(String) must throw if node is not locked."); + } catch (LockException e) { + // success + } + } + } + + public void testRemoveMixLockableFromLockedNode() throws RepositoryException, + NotExecutableException { + try { + lockedNode.removeMixin(mixLockable); + lockedNode.save(); + + // the mixin got removed -> the lock should implicitly be released + // as well in order not to have inconsistencies + String msg = "Lock should have been released."; + assertFalse(msg, lock.isLive()); + assertFalse(msg, lockedNode.isLocked()); + assertFalse(msg, lockMgr.isLocked(lockedNode.getPath())); + + assertFalse(msg, lockedNode.hasProperty(jcrLockOwner)); + assertFalse(msg, lockedNode.hasProperty(jcrlockIsDeep)); + + } catch (ConstraintViolationException e) { + // cannot remove the mixin -> ok + // consequently the node must still be locked, the lock still live... + String msg = "Lock must still be live."; + assertTrue(msg, lock.isLive()); + assertTrue(msg, lockedNode.isLocked()); + assertTrue(msg, lockMgr.isLocked(lockedNode.getPath())); + + assertTrue(msg, lockedNode.hasProperty(jcrLockOwner)); + assertTrue(msg, lockedNode.hasProperty(jcrlockIsDeep)); + } finally { + // ev. re-add the mixin in order to be able to unlock the node + if (lockedNode.isLocked()) { + ensureMixinType(lockedNode, mixLockable); + lockedNode.save(); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/DeepLockTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/DeepLockTest.java new file mode 100644 index 00000000000..13b6982fdee --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/DeepLockTest.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.test.api.lock; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; + +/** DeepLockTest... */ +public class DeepLockTest extends AbstractLockTest { + + protected boolean isSessionScoped() { + return true; + } + + protected boolean isDeep() { + return true; + } + + public void testGetNodeOnLockObtainedFromChild() throws RepositoryException { + javax.jcr.lock.Lock lock = childNode.getLock(); + assertTrue("Lock.getNode() must return the lock holding node even if lock is obtained from child node.", lock.getNode().isSame(lockedNode)); + } + + public void testGetNodeOnLockObtainedFromNewChild() throws RepositoryException { + Node newChild = lockedNode.addNode(nodeName3, testNodeType); + javax.jcr.lock.Lock lock = newChild.getLock(); + assertTrue("Lock.getNode() must return the lock holding node even if lock is obtained from child node.", lock.getNode().isSame(lockedNode)); + } + + public void testParentChildDeepLock() + throws RepositoryException, NotExecutableException { + ensureMixinType(childNode, mixLockable); + testRootNode.getSession().save(); + + // try to lock child node + try { + childNode.lock(false, false); + fail("child node is already locked by deep lock on parent."); + } catch (LockException e) { + // ok + } + + try { + lockMgr.lock(childNode.getPath(), false, false, getTimeoutHint(), getLockOwner()); + fail("child node is already locked by deep lock on parent."); + } catch (LockException e) { + // ok + } + } + + public void testDeepLockAboveLockedChild() throws RepositoryException, NotExecutableException { + Node parent = lockedNode.getParent(); + if (!parent.isNodeType(mixLockable)) { + try { + ensureMixinType(parent, mixLockable); + parent.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + } + + try { + parent.lock(true, true); + fail("Creating a deep lock on a parent of a locked node must fail."); + } catch (LockException e) { + // expected + } + try { + lockMgr.lock(parent.getPath(), true, true, getTimeoutHint(), getLockOwner()); + fail("Creating a deep lock on a parent of a locked node must fail."); + } catch (LockException e) { + // expected + } + } + + public void testShallowLockAboveLockedChild() throws RepositoryException, NotExecutableException { + Node parent = lockedNode.getParent(); + ensureMixinType(parent, mixLockable); + parent.save(); + + try { + // creating a shallow lock on the parent must succeed. + parent.lock(false, true); + } finally { + parent.unlock(); + } + try { + // creating a shallow lock on the parent must succeed. + lockMgr.lock(parent.getPath(), false, true, getTimeoutHint(), getLockOwner()); + } finally { + parent.unlock(); + } + } + + public void testRemoveLockedChild() throws RepositoryException { + Session otherSession = getHelper().getReadWriteSession(); + try { + Node child = (Node) otherSession.getItem(childNode.getPath()); + child.remove(); + otherSession.save(); + fail("A node below a deeply locked node cannot be removed by another Session."); + } catch (LockException e) { + // success + } finally { + otherSession.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/LockManagerTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/LockManagerTest.java new file mode 100644 index 00000000000..6f1207dd93a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/LockManagerTest.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.test.api.lock; + +import java.util.Arrays; +import java.util.List; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.lock.LockManager; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; + +/** LockManagerTest... */ +public class LockManagerTest extends AbstractJCRTest { + + protected LockManager lockMgr; + protected Node testNode; + protected String testPath; + + protected boolean openScopedLockMultiple; + + protected void setUp() throws Exception { + super.setUp(); + + // check for lock support + if (Boolean.FALSE.toString().equals(superuser.getRepository().getDescriptor(Repository.OPTION_LOCKING_SUPPORTED))) { + throw new NotExecutableException(); + } + + testNode = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + testPath = testNode.getPath(); + openScopedLockMultiple = Boolean.TRUE.toString() + .equals(getProperty(RepositoryStub.PROP_OPEN_SCOPED_LOCK_MULTIPLE, Boolean.FALSE.toString())); + + lockMgr = getLockManager(superuser); + } + + protected void tearDown() throws Exception { + if (lockMgr != null && lockMgr.holdsLock(testPath)) { + lockMgr.unlock(testPath); + } + super.tearDown(); + } + + private void assertLockable(Node n) throws RepositoryException, + NotExecutableException { + ensureMixinType(n, mixLockable); + n.getSession().save(); + } + + private static LockManager getLockManager(Session session) throws RepositoryException { + return session.getWorkspace().getLockManager(); + } + + private static boolean containsLockToken(LockManager lMgr, String token) throws RepositoryException { + return containsLockToken(lMgr.getLockTokens(), token); + } + + private static boolean containsLockToken(String[] tokens, String token) throws RepositoryException { + for (int i = 0; i < tokens.length; i++) { + if (tokens[i].equals(token)) { + return true; + } + } + return false; + } + + public void testLockNonLockable() throws NotExecutableException, RepositoryException { + if (testNode.isNodeType(mixLockable)) { + throw new NotExecutableException(); + } + try { + lockMgr.lock(testPath, true, true, Long.MAX_VALUE, superuser.getUserID()); + fail("Attempt to lock a non-lockable node must throw LockException."); + } catch (LockException e) { + // success + } + } + + public void testLockWithPendingChanges() throws RepositoryException, + NotExecutableException { + assertLockable(testNode); + + // transient modification + testNode.addNode(nodeName2); + try { + lockMgr.lock(testPath, true, true, Long.MAX_VALUE, superuser.getUserID()); + fail("Attempt to lock a node with transient modifications must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + // success + } + } + + public void testNullOwnerHint() throws RepositoryException, + NotExecutableException { + assertLockable(testNode); + + Lock l = lockMgr.lock(testPath, true, true, Long.MAX_VALUE, null); + assertNotNull(l.getLockOwner()); + } + + public void testGetLockTokens() throws RepositoryException, + NotExecutableException { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + assertTrue("Creating open scoped lock must add token to the lock manager.", + containsLockToken(lockMgr, ltoken)); + assertTrue("Creating open scoped lock must add token to the lock manager.", + containsLockToken(superuser.getLockTokens(), ltoken)); + } + + public void testGetLockTokensAfterUnlock() throws RepositoryException, + NotExecutableException { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + lockMgr.unlock(testPath); + assertFalse("Removing an open scoped lock must remove the token from the lock manager.", + containsLockToken(lockMgr, ltoken)); + assertFalse("Removing an open scoped lock must remove the token from the lock manager.", + containsLockToken(superuser.getLockTokens(), ltoken)); + } + + public void testGetLockTokensSessionScoped() throws RepositoryException, + NotExecutableException { + assertLockable(testNode); + + List tokensBefore = Arrays.asList(lockMgr.getLockTokens()); + + boolean sessionScoped = true; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + + assertEquals("Creating a session scoped lock must not change the lock tokens.", + tokensBefore, Arrays.asList(lockMgr.getLockTokens())); + assertEquals("Creating a session scoped lock must not change the lock tokens.", + tokensBefore, Arrays.asList(superuser.getLockTokens())); + } + + public void testAddLockToken() throws RepositoryException, + NotExecutableException { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + // adding lock token should have no effect. + lockMgr.addLockToken(ltoken); + } + + public void testAddInvalidLockToken() throws RepositoryException { + try { + lockMgr.addLockToken("any-token"); + fail("Adding an invalid token must fail."); + } catch (LockException e) { + // success + } + } + + public void testAddLockTokenToAnotherSession() throws RepositoryException, + NotExecutableException { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + Session other = getHelper().getReadWriteSession(); + try { + LockManager otherLockMgr = getLockManager(other); + assertFalse(containsLockToken(otherLockMgr, ltoken)); + + try { + otherLockMgr.addLockToken(ltoken); + if (!openScopedLockMultiple) { + fail("Adding token to another session must fail (see config property " + + RepositoryStub.PROP_OPEN_SCOPED_LOCK_MULTIPLE + "."); + } + } catch (LockException e) { + if (openScopedLockMultiple) { + fail("Adding token to another session must not fail (see config property " + + RepositoryStub.PROP_OPEN_SCOPED_LOCK_MULTIPLE + "."); + } + } + } finally { + other.logout(); + } + } + + public void testRemoveLockToken() throws Exception { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + try { + // remove lock token + lockMgr.removeLockToken(ltoken); + + assertFalse(containsLockToken(lockMgr, ltoken)); + assertFalse(containsLockToken(superuser.getLockTokens(), ltoken)); + } finally { + // make sure lock token is added even if test fail + lockMgr.addLockToken(ltoken); + assertTrue(containsLockToken(lockMgr, ltoken)); + assertNotNull("Token must be exposed again", l.getLockToken()); + assertEquals("The lock must get the same token again.", ltoken, l.getLockToken()); + } + } + + public void testRemoveLockToken2() throws Exception { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + try { + lockMgr.removeLockToken(ltoken); + + String nlt = l.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(ltoken)); + } finally { + // make sure lock token is added even if test fail + lockMgr.addLockToken(ltoken); + } + } + + public void testRemoveLockToken3() throws Exception { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + try { + lockMgr.removeLockToken(ltoken); + + // without holding the token session must not be allowed to modify + // the locked node. + try { + testNode.addNode(nodeName2, testNodeType); + fail("Session must not be allowed to modify node"); + } catch (LockException e) { + // expected + } + } finally { + // make sure lock token is added even if test fail + lockMgr.addLockToken(ltoken); + } + } + + public void testRemoveLockTokenTwice() throws Exception { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + lockMgr.removeLockToken(ltoken); + try { + // remove token a second time + lockMgr.removeLockToken(ltoken); + fail("Removing a lock token twice must fail."); + } catch (LockException e) { + // success + } finally { + // make sure lock token is added even if test fail + lockMgr.addLockToken(ltoken); + } + } + + public void testAddLockTokenAgain() throws Exception { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + try { + // remove lock token + lockMgr.removeLockToken(ltoken); + } finally { + // make sure lock token is added even if test fail + lockMgr.addLockToken(ltoken); + assertTrue(containsLockToken(lockMgr, ltoken)); + assertNotNull("Token must be exposed again", l.getLockToken()); + assertEquals("The lock must get the same token again.", ltoken, l.getLockToken()); + } + } + + public void testLockTransfer() throws Exception { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + Session other = getHelper().getReadWriteSession(); + LockManager otherLockMgr = getLockManager(other); + try { + lockMgr.removeLockToken(ltoken); + otherLockMgr.addLockToken(ltoken); + + assertTrue("The new holding manager must contain the token.", containsLockToken(otherLockMgr, ltoken)); + + Lock otherL = otherLockMgr.getLock(testPath); + assertNotNull("Token must be exposed to new lock holder.", otherL.getLockToken()); + assertEquals("Token must be the same again.", ltoken, otherL.getLockToken()); + + } finally { + otherLockMgr.removeLockToken(ltoken); + lockMgr.addLockToken(ltoken); + other.logout(); + } + } + + public void testLockTransfer2() throws Exception { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + Session other = getHelper().getReadWriteSession(); + LockManager otherLockMgr = getLockManager(other); + try { + lockMgr.removeLockToken(ltoken); + otherLockMgr.addLockToken(ltoken); + + lockMgr.addLockToken(ltoken); + if (!openScopedLockMultiple) { + fail("Adding token to another session must fail (see config property " + + RepositoryStub.PROP_OPEN_SCOPED_LOCK_MULTIPLE + "."); + } + } catch (LockException e) { + if (openScopedLockMultiple) { + fail("Adding token to another session must not fail (see config property " + + RepositoryStub.PROP_OPEN_SCOPED_LOCK_MULTIPLE + "."); + } + } finally { + otherLockMgr.removeLockToken(ltoken); + lockMgr.addLockToken(ltoken); + other.logout(); + } + } + + public void testLockTransfer3() throws Exception { + assertLockable(testNode); + + boolean sessionScoped = false; + Lock l = lockMgr.lock(testPath, true, sessionScoped, Long.MAX_VALUE, null); + String ltoken = l.getLockToken(); + + Session other = getHelper().getReadWriteSession(); + LockManager otherLockMgr = getLockManager(other); + try { + lockMgr.removeLockToken(ltoken); + otherLockMgr.addLockToken(ltoken); + + lockMgr.removeLockToken(ltoken); + fail("Removing a token that has been transfered to another manager must fail."); + } catch (LockException e) { + // success + } finally { + otherLockMgr.removeLockToken(ltoken); + lockMgr.addLockToken(ltoken); + other.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/LockTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/LockTest.java new file mode 100644 index 00000000000..fc91f5324af --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/LockTest.java @@ -0,0 +1,737 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.lock; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * LockTest contains the test cases for the lock support in + * the JCR specification. + * + *
          + *
        • {@code testroot} must allow child nodes of type nodetype + *
        • {@code nodetype} nodetype which is lockable or allows to add mix:lockable. + * The node must also allow child nodes with the same node type as itself. + *
        • {@code nodename1} name of a lockable child node of type nodetype. + *
        + */ +public class LockTest extends AbstractJCRTest { + + /** + * Test lock token functionality + */ + public void testAddRemoveLockToken() throws Exception { + // create new node + Node n = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n, mixLockable); + testRootNode.getSession().save(); + + // lock node and get lock token + Lock lock = n.lock(false, false); + String lockToken = lock.getLockToken(); + try { + // assert: session must get a non-null lock token + assertNotNull("session must get a non-null lock token", lockToken); + + // assert: session must hold lock token + assertTrue("session must hold lock token", containsLockToken(superuser, lockToken)); + + // remove lock token + superuser.removeLockToken(lockToken); + + String nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + + // assert: session must still hold lock token + assertFalse("session must not hold lock token", + containsLockToken(superuser, lockToken)); + + // assert: session unable to modify node + try { + n.addNode(nodeName2, testNodeType); + fail("session unable to modify node"); + } catch (LockException e) { + // expected + } + + // add lock token + superuser.addLockToken(lockToken); + + // assert: session must get a non-null lock token + assertNotNull("session must get a non-null lock token", + lock.getLockToken()); + + // assert: session must hold lock token + assertTrue("session must hold lock token", + containsLockToken(superuser, lock.getLockToken())); + + // assert: session able to modify node + n.addNode(nodeName2, testNodeType); + } finally { + // make sure lock token is added even if test fail + superuser.addLockToken(lockToken); + } + } + + /** + * Test session scope: other session may not access nodes that are + * locked. + */ + public void testNodeLocked() throws Exception { + // create new node and lock it + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + testRootNode.getSession().save(); + + // lock node + Lock lock = n1.lock(false, true); + + // assert: isLive must return true + assertTrue("Lock must be live", lock.isLive()); + + // create new session + Session otherSuperuser = getHelper().getSuperuserSession(); + + try { + // get same node + Node n2 = (Node) otherSuperuser.getItem(n1.getPath()); + + // assert: lock token must be null for other session + assertNull("Lock token must be null for other session", + n2.getLock().getLockToken()); + + // assert: modifying same node in other session must fail + try { + n2.addNode(nodeName2, testNodeType); + fail("modifying same node in other session must fail"); + } catch (LockException e) { + // expected + } + } finally { + otherSuperuser.logout(); + } + } + + /** + * Test to get the lock holding node of a node + */ + public void testGetNode() throws Exception { + // create new node with a sub node and lock it + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + Node n1Sub = n1.addNode(nodeName1, testNodeType); + ensureMixinType(n1Sub, mixLockable); + testRootNode.getSession().save(); + + // lock node + n1.lock(true, true); + + assertEquals("getNode() must return the lock holder", + n1.getPath(), + n1.getLock().getNode().getPath()); + + assertEquals("getNode() must return the lock holder", + n1.getPath(), + n1Sub.getLock().getNode().getPath()); + + n1.unlock(); + } + + /** + * Test if getLockOwner() returns the same value as returned by + * Session.getUserId at the time that the lock was placed + */ + public void testGetLockOwnerProperty() throws Exception { + // create new node and lock it + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + testRootNode.getSession().save(); + + // lock node + Lock lock = n1.lock(false, true); + + if (n1.getSession().getUserID() == null) { + assertFalse("jcr:lockOwner must not exist if Session.getUserId() returns null", + n1.hasProperty(jcrLockOwner)); + } else { + assertEquals("getLockOwner() must return the same value as stored " + + "in property " + jcrLockOwner + " of the lock holding " + + "node", + n1.getProperty(jcrLockOwner).getString(), + lock.getLockOwner()); + } + n1.unlock(); + } + + /** + * Test if getLockOwner() returns the same value as returned by + * Session.getUserId at the time that the lock was placed + */ + public void testGetLockOwner() throws Exception { + // create new node and lock it + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + testRootNode.getSession().save(); + + // lock node + Lock lock = n1.lock(false, true); + + assertEquals("getLockOwner() must return the same value as returned " + + "by Session.getUserId at the time that the lock was placed", + testRootNode.getSession().getUserID(), + lock.getLockOwner()); + + n1.unlock(); + } + + /** + * Test if a shallow lock does not lock the child nodes of the locked node. + */ + public void testShallowLock() throws Exception { + // create new nodes + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + Node n2 = n1.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + + // lock parent node + n1.lock(false, true); + + assertFalse("Shallow lock must not lock the child nodes of a node.", + n2.isLocked()); + } + + /** + * Test if it is possible to lock and unlock a checked-in node. + */ + public void testCheckedIn() + throws NotExecutableException, RepositoryException { + + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { + throw new NotExecutableException("Versioning is not supported."); + } + + // create a node that is lockable and versionable + Node node = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(node, mixLockable); + // try to make it versionable if it is not + ensureMixinType(node, mixVersionable); + testRootNode.getSession().save(); + + node.checkin(); + + try { + node.lock(false, false); + } + catch (RepositoryException ex) { + // repository may not allow shallow locks on this resource + // retry with a deep lock + node.lock(true, false); + } + + assertTrue("Locking of a checked-in node failed.", + node.isLocked()); + + node.unlock(); + assertFalse("Unlocking of a checked-in node failed.", + node.isLocked()); + } + + /** + * Test parent/child lock + */ + public void testParentChildLock() throws Exception { + // create new nodes + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + Node n2 = n1.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixLockable); + testRootNode.getSession().save(); + + // lock parent node + n1.lock(false, true); + + // lock child node + n2.lock(false, true); + + // unlock parent node + n1.unlock(); + + // child node must still hold lock + assertTrue("child node must still hold lock", n2.holdsLock()); + } + + /** + * Test parent/child lock + */ + public void testParentChildDeepLock() throws Exception { + // create new nodes + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + Node n2 = n1.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixLockable); + testRootNode.getSession().save(); + + // lock child node + n2.lock(false, true); + + // assert: unable to deep lock parent node + try { + n1.lock(true, true); + fail("unable to deep lock parent node"); + } catch (LockException e) { + // expected + } + } + + /** + * Test Lock.isDeep() + */ + public void testIsDeep() throws RepositoryException, NotExecutableException { + // create two lockable nodes + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixLockable); + testRootNode.getSession().save(); + + // lock node 1 "undeeply" + Lock lock1 = n1.lock(false, true); + assertFalse("Lock.isDeep() must be false if the lock has not been set " + + "as not deep", + lock1.isDeep()); + + // lock node 2 "deeply" + Lock lock2 = n2.lock(true, true); + assertTrue("Lock.isDeep() must be true if the lock has been set " + + "as deep", + lock2.isDeep()); + } + + /** + * Test Lock.isSessionScoped() + */ + public void testIsSessionScoped() throws RepositoryException, + NotExecutableException { + // create two lockable nodes + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixLockable); + testRootNode.getSession().save(); + + // lock node 1 session-scoped + Lock lock1 = n1.lock(false, true); + assertTrue("Lock.isSessionScoped() must be true if the lock " + + "is session-scoped", + lock1.isSessionScoped()); + + // lock node 2 open-scoped + Lock lock2 = n2.lock(false, false); + assertFalse("Lock.isSessionScoped() must be false if the lock " + + "is open-scoped", + lock2.isSessionScoped()); + + n2.unlock(); + } + + /** + * Test locks are released when session logs out + */ + public void testLogout() throws Exception { + // add node + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + testRootNode.getSession().save(); + + // create new session + Session otherSuperuser = getHelper().getSuperuserSession(); + + Lock lock; + try { + // get node created above + Node n2 = (Node) otherSuperuser.getItem(n1.getPath()); + + // lock node + lock = n2.lock(false, true); + + // assert: lock must be alive + assertTrue("lock must be alive", lock.isLive()); + + // assert: node must be locked + n1.refresh(false); + assertTrue("node must be locked", n1.isLocked()); + } finally { + // log out + otherSuperuser.logout(); + } + + + // assert: lock must not be alive + assertFalse("lock must not be alive", lock.isLive()); + + // assert: node must not be locked + n1.getSession().refresh(true); + assertFalse("node must not be locked", n1.isLocked()); + } + + /** + * Test locks may be transferred to other session + */ + public void testLockTransfer() throws Exception { + // add node + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + testRootNode.getSession().save(); + + // create new session + Session otherSuperuser = getHelper().getSuperuserSession(); + + try { + // get node created above + Node n2 = (Node) otherSuperuser.getItem(n1.getPath()); + + // lock node + Lock lock = n2.lock(false, false); + + // assert: user must get non-null token + assertNotNull("user must get non-null token", lock.getLockToken()); + + // transfer to standard session + String lockToken = lock.getLockToken(); + otherSuperuser.removeLockToken(lockToken); + superuser.addLockToken(lockToken); + + String nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + + // assert: user must get non-null token + assertNotNull("user must get non-null token", + n1.getLock().getLockToken()); + } finally { + // log out + otherSuperuser.logout(); + // unlock again + n1.unlock(); + } + } + + /** + * Test open-scoped locks + */ + public void testOpenScopedLocks() throws Exception { + // add node + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + testRootNode.getSession().save(); + + // create new session + Session otherSuperuser = getHelper().getSuperuserSession(); + + try { + // get node created above + Node n2 = (Node) otherSuperuser.getItem(n1.getPath()); + + // lock node + Lock lock = n2.lock(false, false); + + // transfer to standard session + String lockToken = lock.getLockToken(); + otherSuperuser.removeLockToken(lockToken); + superuser.addLockToken(lockToken); + } finally { + // log out + otherSuperuser.logout(); + } + + // assert: node still locked + assertTrue(n1.isLocked()); + } + + /** + * Test refresh + */ + public void testRefresh() throws Exception { + // create new node + Node n = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n, mixLockable); + testRootNode.getSession().save(); + + // lock node and get lock token + Lock lock = n.lock(false, true); + + // assert: lock must be alive + assertTrue("lock must be alive", lock.isLive()); + + // assert: refresh must succeed + lock.refresh(); + + // unlock node + n.unlock(); + + // assert: lock must not be alive + assertFalse("lock must not be alive", lock.isLive()); + } + + /** + * Test refresh + */ + public void testRefreshNotLive() throws Exception { + // create new node + Node n = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n, mixLockable); + testRootNode.getSession().save(); + + // lock node and get lock token + Lock lock = n.lock(false, true); + + // assert: lock must be alive + assertTrue("lock must be alive", lock.isLive()); + + // unlock node + n.unlock(); + + // assert: lock must not be alive + assertFalse("lock must not be alive", lock.isLive()); + + // refresh + try { + lock.refresh(); + fail("Refresh on a lock that is not alive must fail"); + } catch (LockException e) { + // success + } + } + + /** + * Test getLock + */ + public void testGetLock() throws Exception { + // create new nodes + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixLockable); + Node n2 = n1.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixLockable); + testRootNode.getSession().save(); + + // deep lock parent node + n1.lock(true, true); + + // get lock on child node + Lock lock = n2.getLock(); + + // lock holding node must be parent + assertTrue("lock holding node must be parent", lock.getNode().isSame(n1)); + } + + /** + * Tests if a locked, checked-in node can be unlocked + */ + public void testCheckedInUnlock() throws Exception { + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { + throw new NotExecutableException("Repository does not support versioning."); + } + + // set up versionable and lockable node + Node testNode = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(testNode, mixLockable); + ensureMixinType(testNode, mixVersionable); + testRootNode.getSession().save(); + + // lock and check-in + try { + testNode.lock(false, false); + } + catch (RepositoryException ex) { + // repository may not allow shallow locks on this resource + // retry with a deep lock + testNode.lock(true, false); + } + testNode.save(); + testNode.checkin(); + + // do the unlock + testNode.unlock(); + assertFalse("Could not unlock a locked, checked-in node", testNode.holdsLock()); + } + + /** + * Tests if locks are maintained when child nodes are reordered + */ + public void testReorder() throws Exception { + + Node testNode = setUpSameNameSiblings(); + + // lock last node (3) + testNode.lock(false, true); + + // assert: last node locked + assertTrue("Third child node locked", + testRootNode.getNode(nodeName1 + "[3]").isLocked()); + + // move last node in front of first + testRootNode.orderBefore(nodeName1 + "[3]", nodeName1 + "[1]"); + testRootNode.getSession().save(); + + // assert: first node locked + assertTrue("First child node locked", + testRootNode.getNode(nodeName1 + "[1]").isLocked()); + } + + /** + * Tests if locks are maintained when child nodes are reordered + */ + public void testReorder2() throws Exception { + + setUpSameNameSiblings(); + + // lock first node (1) + testRootNode.getNode(nodeName1 + "[1]").lock(false, true); + + // assert: first node locked + assertTrue("First child node locked", + testRootNode.getNode(nodeName1 + "[1]").isLocked()); + + // move first node to last + testRootNode.orderBefore(nodeName1 + "[1]", null); + testRootNode.getSession().save(); + + // assert: third node locked + assertTrue("Third child node locked", + testRootNode.getNode(nodeName1 + "[3]").isLocked()); + } + + /** + * Tests if move preserves lock state (JIRA issue JCR-207). A node that has + * been locked must still appear locked when it has been moved or renamed, + * regardless whether the changes have already been made persistent. + */ + public void testMoveLocked() throws Exception { + Session session = testRootNode.getSession(); + + // create two nodes, parent and child + Node testNode1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(testNode1, mixLockable); + Node testNode2 = testNode1.addNode(nodeName2, testNodeType); + ensureMixinType(testNode2, mixLockable); + testRootNode.getSession().save(); + + // lock child node + testNode2.lock(false, true); + + // assert: child node locked + assertTrue("Child node locked", testNode2.isLocked()); + + // move child node up + String newPath = testRootNode.getPath() + "/" + testNode2.getName(); + session.move(testNode2.getPath(), newPath); + + // assert: child node locked, before save + assertTrue("Child node locked before save", testNode2.isLocked()); + session.save(); + + // assert: child node locked, after save + assertTrue("Child node locked after save", testNode2.isLocked()); + } + + /** + * Tests if unlocking the first of two locked same-name sibling nodes does + * not unlock the second (JIRA issue JCR-284). + */ + public void testUnlockSameNameSibling() throws RepositoryException, NotExecutableException { + Session session = testRootNode.getSession(); + + Node n1, n2; + + try { + // create two same-name sibling nodes + n1 = testRootNode.addNode(nodeName1, testNodeType); + n2 = testRootNode.addNode(nodeName1, testNodeType); + session.save(); + } + catch (ItemExistsException ex) { + throw new NotExecutableException("Node does not seem to allow same name siblings"); + } + + ensureMixinType(n1, mixLockable); + ensureMixinType(n2, mixLockable); + session.save(); + + // lock both nodes + n1.lock(true, true); + n2.lock(true, true); + + // assert: both nodes are locked + assertTrue("First node locked: ", n1.isLocked()); + assertTrue("Second node locked: ", n2.isLocked()); + + // unlock first sibling + n1.unlock(); + + // assert: first node unlocked, second node still locked + assertFalse("First node unlocked: ", n1.isLocked()); + assertTrue("Second node locked: ", n2.isLocked()); + } + + /** + * Return a flag indicating whether the indicated session contains + * a specific lock token + */ + private boolean containsLockToken(Session session, String lockToken) { + String[] lt = session.getLockTokens(); + for (int i = 0; i < lt.length; i++) { + if (lt[i].equals(lockToken)) { + return true; + } + } + return false; + } + + /** + * Create three child nodes with identical names + */ + private Node setUpSameNameSiblings() throws RepositoryException, NotExecutableException { + // create three lockable nodes with same name + try { + Node testNode = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(testNode, mixLockable); + testNode = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(testNode, mixLockable); + testNode = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(testNode, mixLockable); + testRootNode.getSession().save(); + return testNode; + } + catch (ItemExistsException ex) { + // repository does not seem to support same name siblings on this node type + throw new NotExecutableException("Node type " + testNodeType + " does not support same-name-siblings"); + } + } +} + + diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/OpenScopedLockTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/OpenScopedLockTest.java new file mode 100644 index 00000000000..16bab5904e3 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/OpenScopedLockTest.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.test.api.lock; + +/** OpenScopedLockTest... */ +public class OpenScopedLockTest extends AbstractLockTest { + + protected boolean isSessionScoped() { + return false; + } + + protected boolean isDeep() { + return false; + } + + /** + * + */ + public void testGetLockToken() { + assertNotNull("A open scoped lock must expose the token to the lock holder.", lock.getLockToken()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/SessionScopedLockTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/SessionScopedLockTest.java new file mode 100644 index 00000000000..647155e1eee --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/SessionScopedLockTest.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.test.api.lock; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.lock.LockManager; + +/** SessionScopedLockTest... */ +public class SessionScopedLockTest extends AbstractLockTest { + + protected boolean isSessionScoped() { + return true; + } + + protected boolean isDeep() { + return false; + } + + /** + * {@link javax.jcr.lock.Lock#getLockToken()} must + * always return null for session scoped locks. + */ + public void testGetLockToken() { + assertNull("A session scoped lock may never expose the token.", lock.getLockToken()); + } + + /** + * Test locks are released when session logs out + */ + public void testImplicitUnlock() throws RepositoryException, + NotExecutableException { + Session other = getHelper().getReadWriteSession(); + try { + Node testNode = (Node) other.getItem(testRootNode.getPath()); + Node lockedNode = testNode.addNode(nodeName1, testNodeType); + other.save(); + + assertLockable(lockedNode); + + Lock lock = getLockManager(other).lock(lockedNode.getPath(), isDeep(), isSessionScoped(), getTimeoutHint(), getLockOwner()); + other.logout(); + + assertFalse(lock.isLive()); + } finally { + if (other.isLive()) { + other.logout(); + } + } + } + + /** + * Test locks are released when session logs out + */ + public void testImplicitUnlock2() throws RepositoryException, + NotExecutableException { + Session other = getHelper().getReadWriteSession(); + try { + Node testNode = (Node) other.getItem(testRootNode.getPath()); + Node lockedNode = testNode.addNode(nodeName1, testNodeType); + other.save(); + + assertLockable(lockedNode); + + LockManager lMgr = getLockManager(other); + Lock lock = lMgr.lock(lockedNode.getPath(), isDeep(), isSessionScoped(), getTimeoutHint(), getLockOwner()); + + // access the locked noded added by another session + testRootNode.refresh(false); + Node n = (Node) superuser.getItem(lockedNode.getPath()); + + // remove lock implicit by logout lock-holding session + other.logout(); + + // check if superuser session is properly informed about the unlock + assertFalse(n.isLocked()); + assertFalse(n.holdsLock()); + try { + n.getLock(); + fail("Upon logout of the session a session-scoped lock must be gone."); + } catch (LockException e) { + // ok + } + } finally { + if (other.isLive()) { + other.logout(); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/SetValueLockExceptionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/SetValueLockExceptionTest.java new file mode 100644 index 00000000000..a202d72a952 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/SetValueLockExceptionTest.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.test.api.lock; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.lock.LockException; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.PathNotFoundException; +import java.util.Calendar; +import java.io.ByteArrayInputStream; + + +/** + * SetValueLockExceptionTest Tests throwing of a LockException for the + * Property.setValue() methods in case the parentNode of the given property is locked. + * + */ +public class SetValueLockExceptionTest extends AbstractJCRTest { + + private Node testNode; + + private static final String binaryProp = "binaryProp"; + private static final String booleanProp = "booleanProp"; + private static final String dateProp = "dateProp"; + private static final String doubleProp = "doubleProp"; + private static final String longProp = "longProp"; + private static final String referenceProp = "referenceProp"; + private static final String stringProp = "stringProp"; + private static final String multiStringProp ="multiStringProp"; + + private static final boolean booleanValue = false; + private Calendar dateValue = null; + private static final double doubleValue = 3.1414926; + private static final long longValue = Long.MAX_VALUE; + private Node referenceNode = null; + private static final String stringValue = "a string"; + private byte[] binaryValue = null; + private String[] multiString = {"one", "two", "three"}; + + // types for the different method signatures of Property.setValue + private static final int TYPE_VALUE = 20; + private static final int TYPE_MULTIVAL = 21; + private static final int TYPE_MULTSTRING = 22; + private static int[] types = {PropertyType.DATE, PropertyType.DOUBLE, PropertyType.LONG, + PropertyType.REFERENCE, PropertyType.STRING, + PropertyType.BINARY, PropertyType.BOOLEAN, TYPE_VALUE, + TYPE_MULTIVAL, TYPE_MULTSTRING}; + + + /** + * Check if Locking is supported and if yes setup a lockable node with properties + * each one for the possible values passed to Property.setValue . + * and + * @throws Exception + */ + public void setUp() throws Exception { + super.setUp(); + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("SetValueLockExceptionTest " + + "not executable: Locking not supported"); + } + else { + // add a lockable node + testNode = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(testNode, mixLockable); + + // add properties + dateValue = Calendar.getInstance(); + referenceNode = createReferenceableNode(nodeName2); + binaryValue = createRandomString(10).getBytes(); + + ByteArrayInputStream in = new ByteArrayInputStream(binaryValue); + ensureCanSetProperty(testNode, binaryProp, PropertyType.BINARY, false); + testNode.setProperty(binaryProp, in); + ensureCanSetProperty(testNode, booleanProp, PropertyType.BOOLEAN, false); + testNode.setProperty(booleanProp, booleanValue); + ensureCanSetProperty(testNode, dateProp, PropertyType.DATE, false); + testNode.setProperty(dateProp, dateValue); + ensureCanSetProperty(testNode, doubleProp, PropertyType.DOUBLE, false); + testNode.setProperty(doubleProp, doubleValue); + ensureCanSetProperty(testNode, longProp, PropertyType.LONG, false); + testNode.setProperty(longProp, longValue); + if (referenceNode != null) { + ensureCanSetProperty(testNode, referenceProp, PropertyType.REFERENCE, false); + testNode.setProperty(referenceProp, referenceNode); + } + ensureCanSetProperty(testNode, stringProp, PropertyType.STRING, false); + testNode.setProperty(stringProp, stringValue); + ensureCanSetProperty(testNode, multiStringProp, PropertyType.STRING, true); + testNode.setProperty(multiStringProp, multiString); + testRootNode.getSession().save(); + } + } + + public void tearDown() throws Exception { + if (testNode.holdsLock()) { + testNode.unlock(); + } + testNode = null; + referenceNode = null; + superuser.save(); + super.tearDown(); + } + + /** + * Tests if a LockException is thrown if a value is added to a property of a locked node. + * + * @param type The possible argument types. + * @throws RepositoryException + */ + public void doTestSetValueLockException(int type) + throws RepositoryException { + + // lock if not yet locked + if (!testNode.holdsLock()) { + testNode.lock(false, false); + superuser.save(); + } + + // another session + Session session = getHelper().getReadWriteSession(); + try { + Node node = (Node) session.getItem(testNode.getPath()); + Property prop = null; + switch (type) { + case PropertyType.BINARY: + ByteArrayInputStream in = new ByteArrayInputStream(binaryValue); + prop = node.getProperty(binaryProp); + prop.setValue(in); + break; + + case PropertyType.BOOLEAN: + prop = node.getProperty(booleanProp); + prop.setValue(booleanValue); + break; + + case PropertyType.DATE: + prop = node.getProperty(dateProp); + prop.setValue(dateValue); + break; + + case PropertyType.DOUBLE: + prop = node.getProperty(doubleProp); + prop.setValue(doubleValue); + break; + + case PropertyType.LONG: + prop = node.getProperty(longProp); + prop.setValue(longValue); + break; + + case PropertyType.REFERENCE: + prop = node.getProperty(referenceProp); + if (referenceNode != null) { + prop.setValue(referenceNode); + } + break; + + case PropertyType.STRING: + prop = node.getProperty(stringProp); + prop.setValue(stringValue); + break; + + case TYPE_VALUE: + prop = node.getProperty(stringProp); + Value value = session.getValueFactory().createValue(stringValue); + prop.setValue(value); + break; + + case TYPE_MULTIVAL: + prop = node.getProperty(multiStringProp); + Value[] values = { session.getValueFactory().createValue(stringValue), + session.getValueFactory().createValue(stringValue), + session.getValueFactory().createValue(stringValue) }; + prop.setValue(values); + break; + + case TYPE_MULTSTRING: + prop = node.getProperty(multiStringProp); + String[] strVals = {stringValue, stringValue, stringValue}; + prop.setValue(strVals); + break; + } + session.save(); + fail("Property.setValue should throw a LockException " + + "if the parent node holds a Lock."); + } catch (LockException le) { + // ok + } finally { + session.logout(); + } + } + + /** + * Performs the test for all argument types. + * @throws RepositoryException + */ + public void testSetValueLockException() throws RepositoryException { + for (int i = 0; i < types.length; i++) { + doTestSetValueLockException(types[i]); + } + } + + /** + * Create a referenceable node under the testRootNode + * or null if it is not possible to create one. + * @param name + * @throws RepositoryException + */ + public Node createReferenceableNode(String name) throws RepositoryException { + // remove a yet existing node at the target + try { + Node node = testRootNode.getNode(name); + node.remove(); + superuser.save(); + } catch (PathNotFoundException pnfe) { + // ok + } + // a referenceable node + Node n1 = testRootNode.addNode(name, testNodeType); + if (n1.canAddMixin(mixReferenceable)) { + n1.addMixin(mixReferenceable); + // make sure jcr:uuid is available + superuser.save(); + return n1; + } + else { + return null; + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/TestAll.java new file mode 100644 index 00000000000..f72843b488d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/lock/TestAll.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.test.api.lock; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the package + * javax.jcr.lock. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("javax.jcr.lock tests"); + + suite.addTestSuite(LockTest.class); + suite.addTestSuite(SetValueLockExceptionTest.class); + + // JCR 2.0 tests + + suite.addTestSuite(DeepLockTest.class); + suite.addTestSuite(LockManagerTest.class); + suite.addTestSuite(OpenScopedLockTest.class); + suite.addTestSuite(SessionScopedLockTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanAddChildNodeCallWithNodeTypeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanAddChildNodeCallWithNodeTypeTest.java new file mode 100644 index 00000000000..dc129e17dac --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanAddChildNodeCallWithNodeTypeTest.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * Tests NodeType.canAddChildNode(String childNodeName, String nodeTypeName) + * returns true if a node of name childNodeName and of node type + * childNodeName could be added to a node of type NodeType. + * + */ +public class CanAddChildNodeCallWithNodeTypeTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * The node type manager we use for the tests + */ + private NodeTypeManager manager; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + manager = session.getWorkspace().getNodeTypeManager(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + manager = null; + super.tearDown(); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName, String nodeTypeName) + * returns true if childNodeName and nodeTypeName + * match the NodeDef. + */ + public void testDefinedAndLegalType() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, false, false, false); + + if (nodeDef == null) { + throw new NotExecutableException("No child node def with " + + "defaultPrimaryType found"); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String childNodeName = nodeDef.getName(); + String nodeTypeName = nodeDef.getRequiredPrimaryTypes()[0].getName(); + if (nodeTypeName.equals(ntBase)) { + // nt:base is abstract and can never be added, upgrade for check below + nodeTypeName = ntUnstructured; + } + + assertTrue("NodeType.canAddChildNode(String childNodeName, String nodeTypeName) " + + "must return true if childNodeName and nodeTypeName match the " + + "child node def of NodeType.", + nodeType.canAddChildNode(childNodeName, nodeTypeName)); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName, String nodeTypeName) + * returns false if childNodeName does and nodeTypeName + * does not match the NodeDef. + */ + public void testDefinedAndIllegalType() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, false, false, false); + + if (nodeDef == null) { + throw new NotExecutableException("No testable node type found."); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String childNodeName = nodeDef.getName(); + + String legalType = nodeDef.getRequiredPrimaryTypes()[0].getName(); + String illegalType = NodeTypeUtil.getIllegalChildNodeType(manager, legalType); + if (illegalType == null) { + throw new NotExecutableException("No illegal node type name found"); + } + + assertFalse("NodeType.canAddChildNode(String childNodeName, String nodeTypeName) " + + "must return false if childNodeName does and nodeTypeName does not " + + "match the child node def of NodeType.", + nodeType.canAddChildNode(childNodeName, illegalType)); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName, String nodeTypeName) + * returns false if nodeTypeName represents a mixin. + */ + public void testCanAddMixinType() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, false, false, false); + + if (nodeDef == null) { + throw new NotExecutableException("No testable node type found."); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String childNodeName = nodeDef.getName(); + String mixinName; + NodeTypeIterator it = manager.getMixinNodeTypes(); + if (it.hasNext()) { + mixinName = it.nextNodeType().getName(); + } else { + throw new NotExecutableException("No mixin type found."); + } + + assertFalse("NodeType.canAddChildNode(String childNodeName, String nodeTypeName) " + + "must return false if nodeTypeName represents a mixin type.", + nodeType.canAddChildNode(childNodeName, mixinName)); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName, String nodeTypeName) + * returns false if nodeTypeName represents an abstract node type. + */ + public void testCanAddAbstractType() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, false, false, false); + + if (nodeDef == null) { + throw new NotExecutableException("No testable node type found."); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String childNodeName = nodeDef.getName(); + String abstractName = null; + NodeTypeIterator it = manager.getPrimaryNodeTypes(); + while (it.hasNext() && abstractName == null) { + NodeType nt = it.nextNodeType(); + if (nt.isAbstract()) { + abstractName = nt.getName(); + } + } + if (abstractName == null) { + throw new NotExecutableException("No abstract type found."); + } + + assertFalse("NodeType.canAddChildNode(String childNodeName, String nodeTypeName) " + + "must return false if nodeTypeName represents an abstract node type.", + nodeType.canAddChildNode(childNodeName, abstractName)); + } + /** + * Tests if NodeType.canAddChildNode(String childNodeName, String nodeTypeName) + * returns false if childNodeName does not match the NodeDef. + */ + public void testUndefined() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, false, true, false); + + if (nodeDef == null) { + throw new NotExecutableException("No testable node type found."); + } + + String type = nodeDef.getRequiredPrimaryTypes()[0].getName(); + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String undefinedName = NodeTypeUtil.getUndefinedChildNodeName(nodeType); + + assertFalse("NodeType.canAddChildNode(String childNodeName, String nodeTypeName) " + + "must return false if childNodeName does not match the " + + "child node def of NodeType.", + nodeType.canAddChildNode(undefinedName, type)); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName, String nodeTypeName) + * returns true if childNodeName does not match the NodeDef + * but nodeTypeName matches the node type of a residual NodeDef. + */ + public void testResidualAndLegalType() + throws NotExecutableException, RepositoryException { + + String type = null; + NodeType nodeType = null; + + for (NodeDefinition nodeDef : NodeTypeUtil.locateAllChildNodeDef( + session, false, false, true)) { + for (NodeType nt : nodeDef.getRequiredPrimaryTypes()) { + if (!nt.isAbstract()) { + nodeType = nodeDef.getDeclaringNodeType(); + type = nt.getName(); + } + } + } + if (nodeType == null || type == null) { + throw new NotExecutableException( + "No testable residual child node def."); + } + + String undefinedName = NodeTypeUtil.getUndefinedChildNodeName(nodeType); + + assertTrue("NodeType.canAddChildNode(String childNodeName, String nodeTypeName) " + + "must return true for a not defined childNodeName if nodeTypeName " + + "matches the type of a residual child node def", + nodeType.canAddChildNode(undefinedName, type)); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName, String nodeTypeName) + * returns false if childNodeName does not match the NodeDef + * and nodeTypeName does not matches the node type of a residual + * NodeDef. + */ + public void testResidualAndIllegalType() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, false, false, true); + + if (nodeDef == null) { + throw new NotExecutableException("No testable residual child node def."); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String undefinedName = NodeTypeUtil.getUndefinedChildNodeName(nodeType); + + String legalType = nodeDef.getRequiredPrimaryTypes()[0].getName(); + String illegalType = NodeTypeUtil.getIllegalChildNodeType(manager, legalType); + if (illegalType == null) { + throw new NotExecutableException("No illegal node type name found"); + } + + assertFalse("NodeType.canAddChildNode(String childNodeName, String nodeTypeName) " + + "must return false for a not defined childNodeName if nodeTypeName " + + "does not matches the type of a residual child node def", + nodeType.canAddChildNode(undefinedName, illegalType)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanAddChildNodeCallWithoutNodeTypeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanAddChildNodeCallWithoutNodeTypeTest.java new file mode 100644 index 00000000000..a85b42b3f58 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanAddChildNodeCallWithoutNodeTypeTest.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; + +/** + * Tests NodeType.canAddChildNode(String childNodeName) returns true if + * a node of name childNodeName could be added to a node of + * type NodeType. + * + */ +public class CanAddChildNodeCallWithoutNodeTypeTest extends AbstractJCRTest { + + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName) returns + * true if NodeType contains a NodeDef named + * childNodeName with a default primary type. + */ + public void testDefinedWithDefault() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, true, true, false); + + if (nodeDef == null) { + throw new NotExecutableException("No child node def with " + + "defaultPrimaryType found"); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + + assertTrue("NodeType.canAddChildNode(String childNodeName) must return " + + "true if child node def 'childNodeName' defines a defaultPrimaryType.", + nodeType.canAddChildNode(nodeDef.getName())); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName) returns + * true if NodeType contains a NodeDef named + * childNodeName without a default primary type. + */ + public void testDefinedWithoutDefault() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, true, false, false); + + if (nodeDef == null) { + throw new NotExecutableException("No child node def without " + + "defaultPrimaryType found"); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + + assertFalse("NodeType.canAddChildNode(String childNodeName) must return false " + + "if child node def 'childNodeName' does not define a defaultPrimaryType.", + nodeType.canAddChildNode(nodeDef.getName())); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName) returns + * true if NodeType nor does contain a NodeDef named + * childNodeName nor a residual definition. + */ + public void testUndefined() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, false, true, false); + + if (nodeDef == null) { + throw new NotExecutableException("No testable node type found."); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String undefinedName = NodeTypeUtil.getUndefinedChildNodeName(nodeType); + + assertFalse("NodeType.canAddChildNode(String childNodeName) must return " + + "false if 'childNodeName' is a undefined child node def", + nodeType.canAddChildNode(undefinedName)); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName) returns + * true if NodeType contains a residual NodeDef + * with a default primary type. + */ + public void testResidualWithDefault() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, true, true, true); + + if (nodeDef == null) { + throw new NotExecutableException("No residual child node def " + + "without a defaultPrimaryType found."); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String undefinedName = NodeTypeUtil.getUndefinedChildNodeName(nodeType); + + assertTrue("NodeType.canAddChildNode(String childNodeName) must return " + + "true for a not defined childNodeName if NodeType has a residual child node " + + "definition with a defaultPrimaryType", + nodeType.canAddChildNode(undefinedName)); + } + + /** + * Tests if NodeType.canAddChildNode(String childNodeName) returns + * true if NodeType contains a residual NodeDef + * without a default primary type. + */ + public void testResidualWithoutDefault() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = NodeTypeUtil.locateChildNodeDef(session, true, false, true); + + if (nodeDef == null) { + throw new NotExecutableException("No residual child node def " + + "with a defaultPrimaryType found."); + } + + NodeType nodeType = nodeDef.getDeclaringNodeType(); + String undefinedName = NodeTypeUtil.getUndefinedChildNodeName(nodeType); + + assertFalse("NodeType.canAddChildNode(String childNodeName) must return " + + "false for a not defiend childNodeName if NodeType has a " + + "residual child node definition without a defaultPrimaryType", + nodeType.canAddChildNode(undefinedName)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanRemoveItemTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanRemoveItemTest.java new file mode 100644 index 00000000000..96215f1cf66 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanRemoveItemTest.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.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +/** + * Tests that {@link NodeType#canRemoveItem(String)} returns true + * node or property is removable (same for {@link NodeType#canRemoveNode(String)} + * and {@link NodeType#canRemoveProperty(String)}). + * + */ +public class CanRemoveItemTest extends AbstractJCRTest { + + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests that {@link NodeType#canRemoveItem(String)} and + * {@link NodeType#canRemoveProperty(String)} return true + * if the specified property is not a protected nor a mandatory + * property. + */ + public void testRemovableProperty() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, false, false); + + if (propDef == null) { + throw new NotExecutableException("No mandatory property def found."); + } + + NodeType type = propDef.getDeclaringNodeType(); + + assertTrue("NodeType.canRemoveItem(String itemName) must return true " + + "if itemName is not a protected nor a mandatory property def.", + type.canRemoveItem(propDef.getName())); + + assertTrue("NodeType.canRemoveProperty(String propertyName) must return true " + + "if propertyName is not a protected nor a mandatory property def.", + type.canRemoveProperty(propDef.getName())); + } + + /** + * Tests if {@link NodeType#canRemoveItem(String)} and + * {@link NodeType#canRemoveProperty(String)} return false + * if the specified property is a protected property. + */ + public void testProtectedProperty() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, true, false); + + if (propDef == null) { + throw new NotExecutableException("No protected property def found."); + } + + NodeType type = propDef.getDeclaringNodeType(); + + assertFalse("NodeType.canRemoveItem(String itemName) must return false " + + "if itemName is a protected property def.", + type.canRemoveItem(propDef.getName())); + + assertFalse("NodeType.canRemoveProperty(String propertyName) must return false " + + "if propertyName is a protected property def.", + type.canRemoveProperty(propDef.getName())); + } + + /** + * Tests if {@link NodeType#canRemoveItem(String)} and + * {@link NodeType#canRemoveProperty(String)} return false + * if the specified property is a mandatory property. + */ + public void testMandatoryProperty() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, false, true); + + if (propDef == null) { + throw new NotExecutableException("No mandatory property def found."); + } + + NodeType type = propDef.getDeclaringNodeType(); + + assertFalse("NodeType.canRemoveItem(String itemName) must return false " + + "if itemName is a mandatory property def.", + type.canRemoveItem(propDef.getName())); + + assertFalse("NodeType.canRemoveProperty(String propertyName) must return false " + + "if propertyName is a mandatory property def.", + type.canRemoveProperty(propDef.getName())); + } + + /** + * Tests if {@link NodeType#canRemoveItem(String)} and + * {@link NodeType#canRemoveNode(String)} return true + * if the specified node is not a protected nor a mandatory + * child node. + */ + public void testRemovableChildNode() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = + NodeTypeUtil.locateChildNodeDef(session, false, false); + + if (nodeDef == null) { + throw new NotExecutableException("No mandatory property def found."); + } + + NodeType type = nodeDef.getDeclaringNodeType(); + + assertTrue("NodeType.canRemoveItem(String itemName) must return true " + + "if itemName is not a protected nor a mandatory child node def.", + type.canRemoveItem(nodeDef.getName())); + + assertTrue("NodeType.canRemoveNode(String nodeName) must return true " + + "if nodeName is not a protected nor a mandatory child node def.", + type.canRemoveNode(nodeDef.getName())); +} + + /** + * Tests if {@link NodeType#canRemoveItem(String)} and + * {@link NodeType#canRemoveNode(String)} return + * false if the specified node is a protected child node. + */ + public void testProtectedChildNode() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = + NodeTypeUtil.locateChildNodeDef(session, true, false); + + if (nodeDef == null) { + throw new NotExecutableException("No mandatory property def found."); + } + + NodeType type = nodeDef.getDeclaringNodeType(); + + assertFalse("NodeType.canRemoveItem(String itemName) must return false " + + "if itemName is a protected child node def.", + type.canRemoveItem(nodeDef.getName())); + + assertFalse("NodeType.canRemoveNode(String nodeName) must return false " + + "if nodeName is a protected child node def.", + type.canRemoveNode(nodeDef.getName())); +} + + /** + * Tests if {@link NodeType#canRemoveItem(String)} and + * {@link NodeType#canRemoveNode(String)} return + * false if the specified node is a mandatory child node. + */ + public void testMandatoryChildNode() + throws NotExecutableException, RepositoryException { + + NodeDefinition nodeDef = + NodeTypeUtil.locateChildNodeDef(session, true, false); + + if (nodeDef == null) { + throw new NotExecutableException("No mandatory property def found."); + } + + NodeType type = nodeDef.getDeclaringNodeType(); + + assertFalse("NodeType.canRemoveItem(String itemName) must return false " + + "if itemName is a mandatory child node def.", + type.canRemoveItem(nodeDef.getName())); + + assertFalse("NodeType.canRemoveNode(String nodeName) must return false " + + "if nodeName is a mandatory child node def.", + type.canRemoveNode(nodeDef.getName())); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyBinaryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyBinaryTest.java new file mode 100644 index 00000000000..bed3e9a345e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyBinaryTest.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.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import java.text.ParseException; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) and NodeType.canSetProperty(String propertyName, Value[] + * values) where property is of type Binary. + * + */ +public class CanSetPropertyBinaryTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns true if value and its type are convertible to BinaryValue. + */ + public void testConversions() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.BINARY, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No string property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value stringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Binary and value is a StringValue", + nodeType.canSetProperty(propDef.getName(), stringValue)); + + Value binaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Binary and value is a BinaryValue", + nodeType.canSetProperty(propDef.getName(), binaryValue)); + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Binary and value is a DateValue", + nodeType.canSetProperty(propDef.getName(), dateValue)); + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Binary and value is a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValue)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Binary and value is a LongValue", + nodeType.canSetProperty(propDef.getName(), longValue)); + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Binary and value is a BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValue)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Binary and value is a NameValue", + nodeType.canSetProperty(propDef.getName(), nameValue)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Binary and value is a PathValue", + nodeType.canSetProperty(propDef.getName(), pathValue)); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns true if all values and its types are convertible to BinaryValue. + */ + public void testConversionsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.BINARY, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple string property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value binaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + + Value stringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + Value stringValues[] = new Value[] {stringValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Binary and values are of type StringValue", + nodeType.canSetProperty(propDef.getName(), stringValues)); + + Value binaryValues[] = new Value[] {binaryValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Binary and values are of type BinaryValue", + nodeType.canSetProperty(propDef.getName(), binaryValues)); + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + Value dateValues[] = new Value[] {dateValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Binary and values are of type DateValue", + nodeType.canSetProperty(propDef.getName(), dateValues)); + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + Value doubleValues[] = new Value[] {doubleValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Binary and values are of type DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValues)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + Value longValues[] = new Value[] {longValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Binary and values are of type LongValue", + nodeType.canSetProperty(propDef.getName(), longValues)); + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + Value booleanValues[] = new Value[] {booleanValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Binary and values are of type BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValues)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + Value nameValues[] = new Value[] {nameValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Binary and values are of type NameValue", + nodeType.canSetProperty(propDef.getName(), nameValues)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + Value pathValues[] = new Value[] {pathValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Binary and values are of type PathValue", + nodeType.canSetProperty(propDef.getName(), pathValues)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) returns false + * if value does not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfied() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.BINARY, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No binary property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(session, propDef, false); + if (value == null) { + throw new NotExecutableException("No binary property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if value does not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) returns + * false if values do not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfiedMultiple() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.BINARY, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple binary property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(session, propDef, false); + if (value == null) { + throw new NotExecutableException("No multiple binary property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if values do not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), values)); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyBooleanTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyBooleanTest.java new file mode 100644 index 00000000000..36d2576d84e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyBooleanTest.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.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import java.text.ParseException; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) and NodeType.canSetProperty(String propertyName, Value[] + * values) where property is of type Boolean. + * + */ +public class CanSetPropertyBooleanTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns true if value and its type are convertible to BooleanValue. + */ + public void testConversions() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.BOOLEAN, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No boolean property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value stringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Boolean and value is a StringValue", + nodeType.canSetProperty(propDef.getName(), stringValue)); + + Value binaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Boolean and value is a BinaryValue" + + "in UTF-8", + nodeType.canSetProperty(propDef.getName(), binaryValue)); + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Boolean and value is a DateValue", + nodeType.canSetProperty(propDef.getName(), dateValue)); + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Boolean and value is a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValue)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Boolean and value is a LongValue", + nodeType.canSetProperty(propDef.getName(), longValue)); + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Boolean and value is a BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValue)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Boolean and value is a NameValue", + nodeType.canSetProperty(propDef.getName(), nameValue)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Boolean and value is a PathValue", + nodeType.canSetProperty(propDef.getName(), pathValue)); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns true if all values and its types are convertible to + * BooleanValue. + */ + public void testConversionsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.BOOLEAN, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple boolean property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + + Value stringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + Value stringValues[] = new Value[] {stringValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Boolean and values are of type StringValue", + nodeType.canSetProperty(propDef.getName(), stringValues)); + + Value binaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + Value binaryValues[] = new Value[] {binaryValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Boolean and values are of type BinaryValue", + nodeType.canSetProperty(propDef.getName(), binaryValues)); + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + Value dateValues[] = new Value[] {booleanValue, dateValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Boolean and values are of type DateValue", + nodeType.canSetProperty(propDef.getName(), dateValues)); + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + Value doubleValues[] = new Value[] {booleanValue, doubleValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Boolean and values are of type DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValues)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + Value longValues[] = new Value[] {booleanValue, longValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Boolean and values are of type LongValue", + nodeType.canSetProperty(propDef.getName(), longValues)); + + Value booleanValues[] = new Value[] {booleanValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Boolean and values are of type BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValues)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + Value nameValues[] = new Value[] {booleanValue, nameValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Boolean and values are of type NameValue", + nodeType.canSetProperty(propDef.getName(), nameValues)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + Value pathValues[] = new Value[] {booleanValue, pathValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Boolean and values are of type PathValue", + nodeType.canSetProperty(propDef.getName(), pathValues)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) returns false + * if value does not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfied() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.BOOLEAN, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No boolean property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(session, propDef, false); + if (value == null) { + throw new NotExecutableException("No boolean property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if value does not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) returns + * false if values do not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfiedMultiple() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.BOOLEAN, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple boolean property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(session, propDef, false); + if (value == null) { + throw new NotExecutableException("No multiple boolean property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if values do not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), values)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyDateTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyDateTest.java new file mode 100644 index 00000000000..eeefe850c08 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyDateTest.java @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import java.text.ParseException; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) and NodeType.canSetProperty(String propertyName, Value[] + * values) where property is of type Date. + * + */ +public class CanSetPropertyDateTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns true if value and its type are convertible to DateValue. + */ + public void testConversions() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.DATE, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No date property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value anyStringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Date and value is a StringValue " + + "not in date format", + nodeType.canSetProperty(propDef.getName(), anyStringValue)); + + Value dateStringValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.DATE).getString()); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Date and value is a StringValue " + + "in date format", + nodeType.canSetProperty(propDef.getName(), dateStringValue)); + + Value anyBinaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Date and value is a UTF-8 " + + "BinaryValue not in date format", + nodeType.canSetProperty(propDef.getName(), anyBinaryValue)); + + Value dateBinaryValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.DATE).getString(), PropertyType.BINARY); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Date and value is a UTF-8 " + + "BinaryValue in date format", + nodeType.canSetProperty(propDef.getName(), dateBinaryValue)); + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Date and value is a DateValue", + nodeType.canSetProperty(propDef.getName(), dateValue)); + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Date and value is a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValue)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Date and value is a LongValue", + nodeType.canSetProperty(propDef.getName(), longValue)); + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Date and value is a BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValue)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Date and value is a NameValue", + nodeType.canSetProperty(propDef.getName(), nameValue)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Date and value is a PathValue", + nodeType.canSetProperty(propDef.getName(), pathValue)); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns true if all values and its types are convertible to DateValue. + */ + public void testConversionsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.DATE, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple string property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + + Value anyStringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + // note: for assertFalse, use first value of requested type to check + // if not only first value is checked + Value anyStringValues[] = new Value[] {dateValue, anyStringValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Date and values are of type StringValue " + + "not in date format", + nodeType.canSetProperty(propDef.getName(), anyStringValues)); + + Value dateStringValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.DATE).getString()); + Value dateStringValues[] = new Value[] {dateStringValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Date and values are of type StringValue " + + "in date format", + nodeType.canSetProperty(propDef.getName(), dateStringValues)); + + Value dateValues[] = new Value[] {dateValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Date and values are of type DateValue", + nodeType.canSetProperty(propDef.getName(), dateValues)); + + Value anyBinaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + Value anyBinaryValues[] = new Value[] {dateValue, anyBinaryValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Date and values are of type BinaryValue" + + "in UTF-8 but not in date format", + nodeType.canSetProperty(propDef.getName(), anyBinaryValues)); + + Value dateBinaryValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.DATE).getString(), PropertyType.BINARY); + Value dateBinaryValues[] = new Value[] {dateBinaryValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Date and values are of type BinaryValue" + + "in UTF-8 and in date format", + nodeType.canSetProperty(propDef.getName(), dateBinaryValues)); + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + Value doubleValues[] = new Value[] {doubleValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Date and values are of type DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValues)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + Value longValues[] = new Value[] {longValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Date and values are of type LongValue", + nodeType.canSetProperty(propDef.getName(), longValues)); + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + Value booleanValues[] = new Value[] {dateValue, booleanValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Date and values are of type BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValues)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + Value nameValues[] = new Value[] {dateValue, nameValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Date and values are of type NameValue", + nodeType.canSetProperty(propDef.getName(), nameValues)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + Value pathValues[] = new Value[] {dateValue, pathValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Date and values are of type PathValue", + nodeType.canSetProperty(propDef.getName(), pathValues)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) returns false + * if value does not match the value constraints of the property def + */ + public void testValueConstraintNotSatisfied() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.DATE, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No date property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(session, propDef, false); + if (value == null) { + throw new NotExecutableException("No date property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if value does not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) returns + * false if values do not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfiedMultiple() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.DATE, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple date property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(session, propDef, false); + if (value == null) { + throw new NotExecutableException("No multiple date property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if values do not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), values)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyDoubleTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyDoubleTest.java new file mode 100644 index 00000000000..1a020b684c2 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyDoubleTest.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.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import java.text.ParseException; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) and NodeType.canSetProperty(String propertyName, Value[] + * values) where property is of type Double. + * + */ +public class CanSetPropertyDoubleTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns true if value and its type are convertible to DoubleValue. + */ + public void testConversions() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.DOUBLE, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No double property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value anyStringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Double and value is a StringValue " + + "that is not convertible to a DoubleValue", + nodeType.canSetProperty(propDef.getName(), anyStringValue)); + + Value doubleStringValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE).getString()); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Double and value is a StringValue " + + "that is convertible to a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleStringValue)); + + Value anyBinaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Double and value is a UTF-8 " + + "BinaryValue that is not convertible to a DoubleValue", + nodeType.canSetProperty(propDef.getName(), anyBinaryValue)); + + Value doubleBinaryValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE).getString(), PropertyType.BINARY); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Double and value is a UTF-8 " + + "BinaryValue that is convertible to a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleBinaryValue)); + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Double and value is a DateValue", + nodeType.canSetProperty(propDef.getName(), dateValue)); + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Double and value is a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValue)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Double and value is a LongValue", + nodeType.canSetProperty(propDef.getName(), longValue)); + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Double and value is a BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValue)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Double and value is a NameValue", + nodeType.canSetProperty(propDef.getName(), nameValue)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Double and value is a PathValue", + nodeType.canSetProperty(propDef.getName(), pathValue)); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns true if all values and its types are convertible to DoubleValue. + */ + public void testConversionsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.DOUBLE, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple double property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + + Value anyStringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + Value anyStringValues[] = new Value[] {doubleValue, anyStringValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Double and values are of type StringValue " + + "that are not convertible to DoubleValues", + nodeType.canSetProperty(propDef.getName(), anyStringValues)); + + Value doubleStringValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE).getString()); + Value doubleStringValues[] = new Value[] {doubleStringValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Double and values are of type StringValue " + + "that are convertible to DoubleValues", + nodeType.canSetProperty(propDef.getName(), doubleStringValues)); + + Value anyBinaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + Value anyBinaryValues[] = new Value[] {doubleValue, anyBinaryValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Double and values are of type BinaryValue " + + "that are not convertible to DoubleValues", + nodeType.canSetProperty(propDef.getName(), anyBinaryValues)); + + Value doubleBinaryValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE).getString(), PropertyType.BINARY); + Value doubleBinaryValues[] = new Value[] {doubleBinaryValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Double and values are of type BinaryValue " + + "that are convertible to DoubleValues", + nodeType.canSetProperty(propDef.getName(), doubleBinaryValues)); + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + Value dateValues[] = new Value[] {dateValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Double and values are of type DateValue", + nodeType.canSetProperty(propDef.getName(), dateValues)); + + Value doubleValues[] = new Value[] {doubleValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Double and values are of type DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValues)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + Value longValues[] = new Value[] {longValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Double and values are of type LongValue", + nodeType.canSetProperty(propDef.getName(), longValues)); + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + Value booleanValues[] = new Value[] {doubleValue, booleanValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Double and values are of type BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValues)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + Value nameValues[] = new Value[] {doubleValue, nameValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Double and values are of type NameValue", + nodeType.canSetProperty(propDef.getName(), nameValues)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + Value pathValues[] = new Value[] {doubleValue, pathValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Double and values are of type PathValue", + nodeType.canSetProperty(propDef.getName(), pathValues)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) returns false + * if value does not satsfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfied() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.DOUBLE, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No double property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(session, propDef, false); + if (value == null) { + throw new NotExecutableException("No double property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if value does not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) returns + * false if values do not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfiedMultiple() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.DOUBLE, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple double property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(session, propDef, false); + if (value == null) { + throw new NotExecutableException("No multiple double property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if values do not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), values)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyLongTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyLongTest.java new file mode 100644 index 00000000000..af7ed0c0293 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyLongTest.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.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import java.text.ParseException; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) and NodeType.canSetProperty(String propertyName, Value[] + * values) where property is of type Long. + * + */ +public class CanSetPropertyLongTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns true if value and its type are convertible to LongValue. + */ + public void testConversions() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.LONG, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No long property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value anyStringValue = NodeTypeUtil.getValueOfType(session, PropertyType.STRING); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Long and value is a StringValue " + + "that is not convertible to a LongValue", + nodeType.canSetProperty(propDef.getName(), anyStringValue)); + + Value longStringValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.LONG).getString()); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Long and value is a StringValue " + + "that is convertible to a LongValue", + nodeType.canSetProperty(propDef.getName(), longStringValue)); + + Value anyBinaryValue = NodeTypeUtil.getValueOfType(session, PropertyType.BINARY); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Long and value is a UTF-8 " + + "BinaryValue that is not convertible to a LongValue", + nodeType.canSetProperty(propDef.getName(), anyBinaryValue)); + + Value longBinaryValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(session, PropertyType.LONG).getString(), PropertyType.BINARY); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Long and value is a UTF-8 " + + "BinaryValue that is convertible to a LongValue", + nodeType.canSetProperty(propDef.getName(), longBinaryValue)); + + Value dateValue = NodeTypeUtil.getValueOfType(session, PropertyType.DATE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Long and value is a DateValue", + nodeType.canSetProperty(propDef.getName(), dateValue)); + + Value doubleValue = NodeTypeUtil.getValueOfType(session, PropertyType.DOUBLE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Long and value is a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValue)); + + Value longValue = NodeTypeUtil.getValueOfType(session, PropertyType.LONG); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Long and value is a LongValue", + nodeType.canSetProperty(propDef.getName(), longValue)); + + Value booleanValue = NodeTypeUtil.getValueOfType(session, PropertyType.BOOLEAN); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Long and value is a BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValue)); + + Value nameValue = NodeTypeUtil.getValueOfType(session, PropertyType.NAME); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Long and value is a NameValue", + nodeType.canSetProperty(propDef.getName(), nameValue)); + + Value pathValue = NodeTypeUtil.getValueOfType(session, PropertyType.PATH); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Long and value is a PathValue", + nodeType.canSetProperty(propDef.getName(), pathValue)); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns true if all values and its types are convertible to LongValue. + */ + public void testConversionsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.LONG, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple long property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG); + + Value anyStringValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.STRING); + Value anyStringValues[] = new Value[] {longValue, anyStringValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Long and values are of type StringValue " + + "that are not convertible to LongValues", + nodeType.canSetProperty(propDef.getName(), anyStringValues)); + + Value longStringValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG).getString()); + Value longStringValues[] = new Value[] {longStringValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Long and values are of type StringValue " + + "that are convertible to LongValues", + nodeType.canSetProperty(propDef.getName(), longStringValues)); + + Value anyBinaryValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BINARY); + Value anyBinaryValues[] = new Value[] {longValue, anyBinaryValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Long and values are of type BinaryValue " + + "that are not convertible to LongValues", + nodeType.canSetProperty(propDef.getName(), anyBinaryValues)); + + Value longBinaryValue = + superuser.getValueFactory().createValue(NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG).getString(), PropertyType.BINARY); + Value longBinaryValues[] = new Value[] {longBinaryValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Long and values are of type BinaryValue " + + "that are convertible to LongValues", + nodeType.canSetProperty(propDef.getName(), longBinaryValues)); + + Value dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + Value dateValues[] = new Value[] {dateValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Long and values are of type DateValue", + nodeType.canSetProperty(propDef.getName(), dateValues)); + + Value doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE); + Value doubleValues[] = new Value[] {doubleValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Long and values are of type DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValues)); + + Value longValues[] = new Value[] {longValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Long and values are of type LongValue", + nodeType.canSetProperty(propDef.getName(), longValues)); + + Value booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN); + Value booleanValues[] = new Value[] {longValue, booleanValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Long and values are of type BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValues)); + + Value nameValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME); + Value nameValues[] = new Value[] {longValue, nameValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Long and values are of type NameValue", + nodeType.canSetProperty(propDef.getName(), nameValues)); + + Value pathValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.PATH); + Value pathValues[] = new Value[] {longValue, pathValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Long and values are of type PathValue", + nodeType.canSetProperty(propDef.getName(), pathValues)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) returns false + * if value does not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfied() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.LONG, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No long property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (value == null) { + throw new NotExecutableException("No long property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if value does not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) returns + * false if values do not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfiedMultiple() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.LONG, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple long property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (value == null) { + throw new NotExecutableException("No multiple long property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if values do not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), values)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyMultipleTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyMultipleTest.java new file mode 100644 index 00000000000..10e4151c50b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyMultipleTest.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.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value[] + * values) + * + */ +public class CanSetPropertyMultipleTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns false if the property is protected. + */ + public void testReturnFalseBecauseIsProtected() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, NodeTypeUtil.ANY_PROPERTY_TYPE, true, true, false, false); + + // will never happen since at least jcr:mixinTypes of nt:base accomplish the request + if (propDef == null) { + throw new NotExecutableException("No protected, multiple property def found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value value = NodeTypeUtil.getValueOfType(superuser, propDef.getRequiredType()); + Value values[] = new Value[] {value, value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return true if the property is protected.", + nodeType.canSetProperty(propDef.getName(), values)); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns false if the property is not multiple + */ + public void testReturnFalseBecauseIsNotMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, NodeTypeUtil.ANY_PROPERTY_TYPE, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No not multiple, not protected property def found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value value = NodeTypeUtil.getValueOfType(superuser, propDef.getRequiredType()); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if the property is not multiple", + nodeType.canSetProperty(propDef.getName(), values)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) where values + * is null returns the same as canRemoveItem + */ + public void testMultipleValuesNull() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, NodeTypeUtil.ANY_PROPERTY_TYPE, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No not protected, multiple property def found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertEquals("nodeType.canSetProperty(String propertyName, Value[] values) " + + "where values is null must return the same result as " + + "nodeType.canRemoveItem(String propertyName)", + nodeType.canRemoveItem(propDef.getName()), + nodeType.canSetProperty(propDef.getName(), (Value[]) null)); + } + + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyNameTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyNameTest.java new file mode 100644 index 00000000000..0946c132a1a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyNameTest.java @@ -0,0 +1,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import java.text.ParseException; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) and NodeType.canSetProperty(String propertyName, Value[] + * values) where property is of type Name. + * + */ +public class CanSetPropertyNameTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns true if value and its type are convertible to NameValue. + */ + public void testConversions() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.NAME, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No name property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value nameStringValue = superuser.getValueFactory().createValue("abc"); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Name and value is a StringValue " + + "that is convertible to a NameValue", + nodeType.canSetProperty(propDef.getName(), nameStringValue)); + + Value noNameStringValue = superuser.getValueFactory().createValue("a:b:c"); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Name and value is a StringValue " + + "that is not convertible to a NameValue", + nodeType.canSetProperty(propDef.getName(), noNameStringValue)); + + Value nameBinaryValue = superuser.getValueFactory().createValue("abc", PropertyType.BINARY); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Name and value is a UTF-8 " + + "BinaryValue that is convertible to a NameValue", + nodeType.canSetProperty(propDef.getName(), nameBinaryValue)); + + Value noNameBinaryValue = superuser.getValueFactory().createValue("a:b:c", PropertyType.BINARY); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Name and value is a UTF-8 " + + "BinaryValue that is not convertible to a NameValue", + nodeType.canSetProperty(propDef.getName(), noNameBinaryValue)); + + Value dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Name and value is a DateValue", + nodeType.canSetProperty(propDef.getName(), dateValue)); + + Value doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Name and value is a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValue)); + + Value longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Name and value is a LongValue", + nodeType.canSetProperty(propDef.getName(), longValue)); + + Value booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Name and value is a BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValue)); + + Value nameValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Name and value is a NameValue", + nodeType.canSetProperty(propDef.getName(), nameValue)); + + Value namePathValue = superuser.getValueFactory().createValue("abc", PropertyType.PATH); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Name and value is a PathValue " + + "if Path is relative, is one element long and has no index", + nodeType.canSetProperty(propDef.getName(), namePathValue)); + + Value noNamePathValue = superuser.getValueFactory().createValue("/abc", PropertyType.PATH); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Name and value is a PathValue " + + "if Path is not relative, is more than one element long or has an index", + nodeType.canSetProperty(propDef.getName(), noNamePathValue)); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns true if all values and its types are convertible to NameValue. + */ + public void testConversionsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.NAME, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple name property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value nameValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME); + + Value nameStringValue = superuser.getValueFactory().createValue("abc"); + Value nameStringValues[] = new Value[] {nameStringValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Name and values are of type StringValue " + + "that are convertible to NameValues", + nodeType.canSetProperty(propDef.getName(), nameStringValues)); + + Value notNameStringValue = superuser.getValueFactory().createValue("a:b:c"); + Value notNameStringValues[] = new Value[] {nameValue, notNameStringValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Name and values are of type StringValue " + + "that are not convertible to NameValues ", + nodeType.canSetProperty(propDef.getName(), notNameStringValues)); + + Value nameBinaryValue = superuser.getValueFactory().createValue("abc", PropertyType.BINARY); + Value nameBinaryValues[] = new Value[] {nameBinaryValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Name and values are of type BinaryValue " + + "that are convertible to NameValues", + nodeType.canSetProperty(propDef.getName(), nameBinaryValues)); + + Value notNameBinaryValue = superuser.getValueFactory().createValue("a:b:c", PropertyType.BINARY); + Value notNameBinaryValues[] = new Value[] {nameValue, notNameBinaryValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Name and values are of type BinaryValue " + + "that are not convertible to NameValues", + nodeType.canSetProperty(propDef.getName(), notNameBinaryValues)); + + Value dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + Value dateValues[] = new Value[] {nameValue, dateValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Name and values are of type DateValue", + nodeType.canSetProperty(propDef.getName(), dateValues)); + + Value doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE); + Value doubleValues[] = new Value[] {nameValue, doubleValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Name and values are of type DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValues)); + + Value longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG); + Value longValues[] = new Value[] {nameValue, longValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Name and values are of type LongValue", + nodeType.canSetProperty(propDef.getName(), longValues)); + + Value booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN); + Value booleanValues[] = new Value[] {booleanValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Name and values are of type BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValues)); + + Value nameValues[] = new Value[] {nameValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Name and values are of type NameValue", + nodeType.canSetProperty(propDef.getName(), nameValues)); + + Value namePathValue = superuser.getValueFactory().createValue("abc", PropertyType.PATH); + Value namePathValues[] = new Value[] {namePathValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Name and values are of type PathValue " + + "if Path is relative, is one element long and has no index", + nodeType.canSetProperty(propDef.getName(), namePathValues)); + + Value notNamePathValue =superuser.getValueFactory().createValue("/abc", PropertyType.PATH); + Value notNamePathValues[] = new Value[] {nameValue, notNamePathValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Name and values are of type PathValue " + + "if Path is not relative, is more than one element long or has an index", + nodeType.canSetProperty(propDef.getName(), notNamePathValues)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) returns false + * if value does not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfied() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.NAME, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No name property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (value == null) { + throw new NotExecutableException("No name property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if value does not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) returns + * false if values do not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfiedMultiple() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.NAME, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple name property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (value == null) { + throw new NotExecutableException("No multiple name property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if values do not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), values)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyPathTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyPathTest.java new file mode 100644 index 00000000000..7e357ae78ef --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyPathTest.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.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.Session; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import java.text.ParseException; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) and NodeType.canSetProperty(String propertyNa me, + * Value[] values) where property is of type Path. + * + */ +public class CanSetPropertyPathTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns true if value and its type are convertible to PathValue. + */ + public void testConversions() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.PATH, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No path property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value pathStringValue = superuser.getValueFactory().createValue("abc"); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Path and value is a StringValue " + + "that is convertible to a PathValue", + nodeType.canSetProperty(propDef.getName(), pathStringValue)); + + Value noPathStringValue = superuser.getValueFactory().createValue("a:b:c"); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Path and value is a StringValue " + + "that is not convertible to a PathValue", + nodeType.canSetProperty(propDef.getName(), noPathStringValue)); + + Value pathBinaryValue = superuser.getValueFactory().createValue("abc", PropertyType.BINARY); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Path and value is a UTF-8 " + + "BinaryValue that is convertible to a PathValue", + nodeType.canSetProperty(propDef.getName(), pathBinaryValue)); + + Value noPathBinaryValue = superuser.getValueFactory().createValue("a:b:c", PropertyType.BINARY); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Path and value is a BinaryValue" + + "that is not convertible to a PathValue", + nodeType.canSetProperty(propDef.getName(), noPathBinaryValue)); + + Value dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Path and value is a DateValue", + nodeType.canSetProperty(propDef.getName(), dateValue)); + + Value doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Path and value is a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValue)); + + Value longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Path and value is a LongValue", + nodeType.canSetProperty(propDef.getName(), longValue)); + + Value booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN); + assertFalse("canSetProperty(String propertyName, Value value) must return " + + "false if the property is of type Path and value is a BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValue)); + + Value pathValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Path and value is a NameValue", + nodeType.canSetProperty(propDef.getName(), pathValue)); + + Value relPathValue = superuser.getValueFactory().createValue("abc", PropertyType.PATH); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Path and value is a PathValue", + nodeType.canSetProperty(propDef.getName(), relPathValue)); + + Value absPathValue = superuser.getValueFactory().createValue("/abc", PropertyType.PATH); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type Path and value is a PathValue", + nodeType.canSetProperty(propDef.getName(), absPathValue)); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns true if all values and its types are convertible to PathValue. + */ + public void testConversionsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.PATH, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple path property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value pathValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.PATH); + + Value pathStringValue = superuser.getValueFactory().createValue("abc"); + Value pathStringValues[] = new Value[] {pathStringValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Path and values are of type StringValue " + + "that are convertible to PathValues", + nodeType.canSetProperty(propDef.getName(), pathStringValues)); + + Value notPathStringValue = superuser.getValueFactory().createValue("a:b:c"); + Value notPathStringValues[] = new Value[] {pathValue, notPathStringValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Path and values are of type StringValue " + + "that are not convertible to PathValues ", + nodeType.canSetProperty(propDef.getName(), notPathStringValues)); + + Value pathBinaryValue = superuser.getValueFactory().createValue("abc", PropertyType.BINARY); + Value pathBinaryValues[] = new Value[] {pathBinaryValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Path and values are of type BinaryValue " + + "that are convertible to PathValues", + nodeType.canSetProperty(propDef.getName(), pathBinaryValues)); + + Value notPathBinaryValue = superuser.getValueFactory().createValue("a:b:c", PropertyType.BINARY); + Value notPathBinaryValues[] = new Value[] {pathValue, notPathBinaryValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Path and values are of type BinaryValue " + + "that are not convertible to PathValues", + nodeType.canSetProperty(propDef.getName(), notPathBinaryValues)); + + Value dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + Value dateValues[] = new Value[] {pathValue, dateValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Path and values are of type DateValue", + nodeType.canSetProperty(propDef.getName(), dateValues)); + + Value doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE); + Value doubleValues[] = new Value[] {pathValue, doubleValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Path and values are of type DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValues)); + + Value longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG); + Value longValues[] = new Value[] {pathValue, longValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Path and values are of type LongValue", + nodeType.canSetProperty(propDef.getName(), longValues)); + + Value booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN); + Value booleanValues[] = new Value[] {booleanValue}; + assertFalse("canSetProperty(String propertyName, Value[] values) must return " + + "false if the property is of type Path and values are of type BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValues)); + + Value nameValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME); + Value nameValues[] = new Value[] {nameValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Path and values are of type NameValue", + nodeType.canSetProperty(propDef.getName(), nameValues)); + + Value pathValues[] = new Value[] {pathValue, pathValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type Path and values are of type PathValue", + nodeType.canSetProperty(propDef.getName(), pathValues)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) returns false + * if value does not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfied() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.PATH, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No path property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (value == null) { + throw new NotExecutableException("No path property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if value does not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) returns + * false if values do not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfiedMultiple() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.PATH, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple path property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (value == null) { + throw new NotExecutableException("No multiple path property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if values do not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), values)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyStringTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyStringTest.java new file mode 100644 index 00000000000..b14c42bd7c1 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyStringTest.java @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.Session; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import java.text.ParseException; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) and NodeType.canSetProperty(String propertyName, Value[] + * values) where property is of type String. + * + */ +public class CanSetPropertyStringTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns true if value and its type are convertable to StringValue. + */ + public void testConversions() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.STRING, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No string property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value stringValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.STRING); + + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type String and value is a StringValue", + nodeType.canSetProperty(propDef.getName(), stringValue)); + + Value binaryValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BINARY); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type String and value is a BinaryValue " + + "and is UTF-8", + nodeType.canSetProperty(propDef.getName(), binaryValue)); + + Value dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type String and value is a DateValue", + nodeType.canSetProperty(propDef.getName(), dateValue)); + + Value doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type String and value is a DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValue)); + + Value longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type String and value is a LongValue", + nodeType.canSetProperty(propDef.getName(), longValue)); + + Value booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type String and value is a BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValue)); + + Value nameValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type String and value is a NameValue", + nodeType.canSetProperty(propDef.getName(), nameValue)); + + Value pathValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.PATH); + assertTrue("canSetProperty(String propertyName, Value value) must return " + + "true if the property is of type String and value is a PathValue", + nodeType.canSetProperty(propDef.getName(), pathValue)); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value[] values) + * returns true if all values and its types are convertible to StringValue. + */ + public void testConversionsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.STRING, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple string property def that meets the " + + "requirements of the test has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + + Value stringValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.STRING); + Value stringValues[] = new Value[] {stringValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type String and values are of type StringValue", + nodeType.canSetProperty(propDef.getName(), stringValues)); + + Value binaryValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BINARY); + Value binaryValues[] = new Value[] {binaryValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type String and values are of type BinaryValue " + + "and is UTF-8", + nodeType.canSetProperty(propDef.getName(), binaryValues)); + + Value dateValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DATE); + Value dateValues[] = new Value[] {dateValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type String and values are of type DateValue", + nodeType.canSetProperty(propDef.getName(), dateValues)); + + Value doubleValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.DOUBLE); + Value doubleValues[] = new Value[] {doubleValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type String and values are of type DoubleValue", + nodeType.canSetProperty(propDef.getName(), doubleValues)); + + Value longValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.LONG); + Value longValues[] = new Value[] {longValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type String and values are of type LongValue", + nodeType.canSetProperty(propDef.getName(), longValues)); + + Value booleanValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.BOOLEAN); + Value booleanValues[] = new Value[] {booleanValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type String and values are of type BooleanValue", + nodeType.canSetProperty(propDef.getName(), booleanValues)); + + Value nameValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.NAME); + Value nameValues[] = new Value[] {nameValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type String and values are of type NameValue", + nodeType.canSetProperty(propDef.getName(), nameValues)); + + Value pathValue = NodeTypeUtil.getValueOfType(superuser, PropertyType.PATH); + Value pathValues[] = new Value[] {pathValue}; + assertTrue("canSetProperty(String propertyName, Value[] values) must return " + + "true if the property is of type String and values are of type PathValue", + nodeType.canSetProperty(propDef.getName(), pathValues)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) returns false + * if value does not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfied() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.STRING, false, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No string property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (value == null) { + throw new NotExecutableException("No string property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if value does not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value[] values) returns + * false if values do not satisfy the value constraints of the property def + */ + public void testValueConstraintNotSatisfiedMultiple() + throws NotExecutableException, ParseException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, PropertyType.STRING, true, false, true, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple string property def with " + + "testable value constraints has been found"); + } + + Value value = NodeTypeUtil.getValueAccordingToValueConstraints(superuser, propDef, false); + if (value == null) { + throw new NotExecutableException("No multiple string property def with " + + "testable value constraints has been found"); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value values[] = new Value[] {value}; + + assertFalse("canSetProperty(String propertyName, Value[] values) must " + + "return false if values do not match the value constraints.", + nodeType.canSetProperty(propDef.getName(), values)); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyTest.java new file mode 100644 index 00000000000..412bfd527f4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/CanSetPropertyTest.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.test.api.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** + * Test of NodeType.canSetProperty(String propertyName, Value + * value) + * + */ +public class CanSetPropertyTest extends AbstractJCRTest { + /** + * The session we use for the tests + */ + private Session session; + + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns false if the property is protected. + */ + public void testReturnFalseBecauseIsProtected() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, NodeTypeUtil.ANY_PROPERTY_TYPE, false, true, false, false); + + // will never happen since at least jcr:primaryType of nt:base accomplish the request + if (propDef == null) { + throw new NotExecutableException("No protected property def found."); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value value = NodeTypeUtil.getValueOfType(superuser, propDef.getRequiredType()); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if the property is protected.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + + /** + * Tests if NodeType.canSetProperty(String propertyName, Value value) + * returns false if the property is multiple + */ + public void testReturnFalseBecauseIsMultiple() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, NodeTypeUtil.ANY_PROPERTY_TYPE, true, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No multiple, not protected property def found."); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + Value value = NodeTypeUtil.getValueOfType(superuser, propDef.getRequiredType()); + + assertFalse("canSetProperty(String propertyName, Value value) must " + + "return false if the property is multiple.", + nodeType.canSetProperty(propDef.getName(), value)); + } + + /** + * Tests if canSetProperty(String propertyName, Value value) where value is + * null returns the same as canRemoveItem + */ + public void testValueNull() + throws NotExecutableException, RepositoryException { + + PropertyDefinition propDef = + NodeTypeUtil.locatePropertyDef(session, NodeTypeUtil.ANY_PROPERTY_TYPE, false, false, false, false); + + if (propDef == null) { + throw new NotExecutableException("No not protected property def found."); + } + + NodeType nodeType = propDef.getDeclaringNodeType(); + + assertEquals("nodeType.canSetProperty(String propertyName, Value value) " + + "where value is null must return the same result as " + + "nodeType.canRemoveItem(String propertyName).", + nodeType.canRemoveItem(propDef.getName()), + nodeType.canSetProperty(propDef.getName(), (Value) null)); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeDefTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeDefTest.java new file mode 100644 index 00000000000..863f84899e8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeDefTest.java @@ -0,0 +1,333 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; + +/** + * Tests if node definitions are respected in node instances in the workspace. + * + */ +public class NodeDefTest extends AbstractJCRTest { + + /** + * The session we use for the tests + */ + private Session session; + + /** + * The node type manager of the session + */ + private NodeTypeManager manager; + + /** + * If true indicates that the test found a mandatory node + */ + private boolean foundMandatoryNode = false; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + manager = session.getWorkspace().getNodeTypeManager(); + // re-fetch testRootNode with read-only session + testRootNode = (Node) session.getItem(testRoot); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + manager = null; + super.tearDown(); + } + + /** + * Test getDeclaringNodeType() returns the node type which is defining the + * requested child node def. Test runs for all existing node types. + */ + public void testGetDeclaringNodeType() + throws RepositoryException { + + NodeTypeIterator types = manager.getAllNodeTypes(); + // loop all node types + while (types.hasNext()) { + NodeType currentType = types.nextNodeType(); + NodeDefinition defsOfCurrentType[] = + currentType.getChildNodeDefinitions(); + + // loop all child node defs of each node type + for (int i = 0; i < defsOfCurrentType.length; i++) { + NodeDefinition def = defsOfCurrentType[i]; + NodeType type = def.getDeclaringNodeType(); + + // check if def is part of the child node defs of the + // declaring node type + NodeDefinition defs[] = type.getChildNodeDefinitions(); + boolean hasType = false; + for (int j = 0; j < defs.length; j++) { + if (defs[j].getName().equals(def.getName())) { + hasType = true; + break; + } + } + assertTrue("getDeclaringNodeType() must return the node " + + "which defines the corresponding child node def.", + hasType); + } + } + } + + + /** + * Tests if auto create nodes are not a residual set definition (getName() + * does not return "*") + */ + public void testIsAutoCreate() + throws RepositoryException { + + NodeTypeIterator types = manager.getAllNodeTypes(); + // loop all node types + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + NodeDefinition defs[] = type.getChildNodeDefinitions(); + for (int i = 0; i < defs.length; i++) { + if (defs[i].isAutoCreated()) { + assertFalse("An auto create node must not be a " + + "residual set definition.", + defs[i].getName().equals("*")); + } + } + } + } + + + /** + * This test checks if item definitions with mandatory constraints are + * respected. + *

        + * If the default workspace does not contain a node with a node type + * definition that specifies a mandatory child node a {@link + * org.apache.jackrabbit.test.NotExecutableException} is thrown. + */ + public void testIsMandatory() throws RepositoryException, NotExecutableException { + traverse(testRootNode); + if (!foundMandatoryNode) { + throw new NotExecutableException("Workspace does not contain any node with a mandatory child node definition"); + } + } + + + /** + * Tests if getRequiredPrimaryTypes() does not return an empty array. Test + * runs for all existing node types. + */ + public void testGetRequiredPrimaryTypes() + throws RepositoryException { + + // loop all node types + for (NodeTypeIterator types = manager.getAllNodeTypes(); types.hasNext(); ) { + NodeType type = types.nextNodeType(); + NodeDefinition defs[] = type.getChildNodeDefinitions(); + + for (int i = 0; i < defs.length; i++) { + assertTrue("getRequiredPrimaryTypes() must never return an " + + "empty array.", + defs[i].getRequiredPrimaryTypes().length > 0); + } + } + } + + /** + * Tests that the information from getRequiredPrimaryTypeNames() + * matches getRequiredPrimaryTypes(). + * + * @since JCR 2.0 + */ + public void testGetRequiredPrimaryTypeNames() + throws RepositoryException { + + // loop all node types + for (NodeTypeIterator types = manager.getAllNodeTypes(); types.hasNext(); ) { + NodeType type = types.nextNodeType(); + NodeDefinition defs[] = type.getChildNodeDefinitions(); + + for (int i = 0; i < defs.length; i++) { + NodeType requiredPrimaryTypes[] = defs[i].getRequiredPrimaryTypes(); + Set rptnames = new HashSet(); + for (int j = 0; j < requiredPrimaryTypes.length; j++) { + rptnames.add(requiredPrimaryTypes[j].getName()); + } + + Set rptnames2 = new HashSet(Arrays.asList(defs[i].getRequiredPrimaryTypeNames())); + assertEquals("names returned from getRequiredPrimaryTypeNames should match types returned from getRequiredPrimaryTypes", rptnames, rptnames2); + } + } + } + + /** + * Tests if the default primary type is of the same or a sub node type as the + * the required primary types. Test runs for all existing node types. Also + * tests the string based access ({@link NodeDefinition#getDefaultPrimaryTypeName()}. + * + * @since JCR 2.0 + */ + public void testGetDefaultPrimaryTypes() + throws RepositoryException { + + // loop all node types + for (NodeTypeIterator types = manager.getAllNodeTypes(); types.hasNext(); ) { + NodeType type = types.nextNodeType(); + NodeDefinition defs[] = type.getChildNodeDefinitions(); + + for (int i = 0; i < defs.length; i++) { + + NodeDefinition def = defs[i]; + NodeType defaultType = def.getDefaultPrimaryType(); + String defaultTypeName = def.getDefaultPrimaryTypeName(); + if (defaultType != null) { + + NodeType requiredTypes[] = + def.getRequiredPrimaryTypes(); + + for (int j = 0; j < requiredTypes.length; j++) { + NodeType requiredType = requiredTypes[j]; + + boolean isSubType = compareWithRequiredType(requiredType, + defaultType); + + assertTrue("The NodeType returned by " + + "getDefaultPrimaryType or one of its " + + "supertypes must match all NodeTypes " + + "returned by getRequiredPrimaryTypes()", + isSubType); + } + + assertEquals("type names obtained from getDefaultPrimaryType and getDefaultPrimaryTypeName should match", defaultType.getName(), defaultTypeName); + NodeType tmpType = manager.getNodeType(defaultTypeName); + assertEquals(tmpType.getName(), defaultTypeName); + } + else { + assertNull("getDefaultPrimaryTypeName should return null when getDefaultPrimaryType does", defaultTypeName); + } + } + } + } + + + //-----------------------< internal >--------------------------------------- + + /** + * Traverses the node hierarchy and applies + * {@link #checkMandatoryConstraint(javax.jcr.Node, javax.jcr.nodetype.NodeType)} + * to all descendant nodes of parentNode. + */ + private void traverse(Node parentNode) + throws RepositoryException { + + NodeIterator nodes = parentNode.getNodes(); + while (nodes.hasNext()) { + Node node = nodes.nextNode(); + + NodeType primaryType = node.getPrimaryNodeType(); + checkMandatoryConstraint(node, primaryType); + + NodeType mixins[] = node.getMixinNodeTypes(); + for (int i = 0; i < mixins.length; i++) { + checkMandatoryConstraint(node, mixins[i]); + } + + traverse(node); + } + } + + + /** + * Checks if mandatory node definitions are respected. + */ + private void checkMandatoryConstraint(Node node, NodeType type) + throws RepositoryException { + + // test if node contains all mandatory nodes of current type + NodeDefinition nodeDefs[] = type.getChildNodeDefinitions(); + for (int i = 0; i < nodeDefs.length; i++) { + NodeDefinition nodeDef = nodeDefs[i]; + if (nodeDef.isMandatory()) { + foundMandatoryNode = true; + try { + node.getNode(nodeDef.getName()); + } catch (PathNotFoundException e) { + fail("Mandatory child " + nodeDef.getName() + " for " + + node.getPath() + " does not exist."); + } + } + } + } + + + /** + * Returns true if defaultType or one of its supertypes is of the same + * NodeType as requiredType. + * + * @param requiredType one of the required primary types of a NodeDef + * @param defaultType the default primary type of a NodeDef + */ + private boolean compareWithRequiredType(NodeType requiredType, + NodeType defaultType) { + + // if (defaultType == requiredType) return true; + // rather use: + if (defaultType.getName().equals(requiredType.getName())) { + return true; + } + + NodeType superTypes[] = defaultType.getSupertypes(); + for (int i = 0; i < superTypes.length; i++) { + // if (superTypes[i] == requiredType) return true; + // rather use: + if (superTypes[i].getName().equals(requiredType.getName())) { + return true; + } + } + return false; + } + + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeCreationTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeCreationTest.java new file mode 100644 index 00000000000..4165c013d6b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeCreationTest.java @@ -0,0 +1,518 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import java.util.List; +import java.util.Arrays; + +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinitionTemplate; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * Tests the node type creation functionality of the {@link NodeTypeManager}. + * + */ +public class NodeTypeCreationTest extends AbstractJCRTest { + + private String expandedPropName; + private String jcrPropName; + + private NodeTypeManager ntm; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + super.setUp(); + ntm = superuser.getWorkspace().getNodeTypeManager(); + super.checkSupportedOption(Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED); + + expandedPropName = "{" + NS_JCR_URI + "}" + "boolean"; + jcrPropName = superuser.getNamespacePrefix(NS_JCR_URI) + ":boolean"; + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testEmptyNodeTypeTemplate() throws Exception { + + NodeTypeTemplate ntt = ntm.createNodeTypeTemplate(); + assertNull(ntt.getName()); + + assertFalse(ntt.isMixin()); + assertFalse(ntt.isAbstract()); + assertFalse(ntt.hasOrderableChildNodes()); + + // note: isQueryable cannot be tested as defautl value is defined + // by the implementation + + assertNotNull(ntt.getDeclaredSupertypeNames()); + assertEquals(0, ntt.getDeclaredSupertypeNames().length); + + assertNull(ntt.getPrimaryItemName()); + + assertNull(ntt.getDeclaredChildNodeDefinitions()); + assertNull(ntt.getDeclaredPropertyDefinitions()); + + assertNotNull(ntt.getNodeDefinitionTemplates()); + assertTrue(ntt.getNodeDefinitionTemplates().isEmpty()); + + assertNotNull(ntt.getPropertyDefinitionTemplates()); + assertTrue(ntt.getPropertyDefinitionTemplates().isEmpty()); + } + + public void testNonEmptyNodeTypeTemplate() throws Exception { + + NodeTypeDefinition ntd = ntm.getNodeType("nt:address"); + NodeTypeTemplate ntt = ntm.createNodeTypeTemplate(ntm.getNodeType("nt:address")); + + assertEquals(ntt.getName(), ntd.getName()); + assertEquals(ntt.isMixin(), ntd.isMixin()); + assertEquals(ntt.isAbstract(), ntd.isAbstract()); + assertEquals(ntt.hasOrderableChildNodes(), ntd.hasOrderableChildNodes()); + assertEquals(ntt.isQueryable(), ntd.isQueryable()); + assertEquals(ntt.getPrimaryItemName(), ntd.getPrimaryItemName()); + assertTrue(Arrays.equals(ntt.getDeclaredSupertypeNames(), ntd.getDeclaredSupertypeNames())); + NodeDefinition[] nda = ntt.getDeclaredChildNodeDefinitions(); + NodeDefinition[] nda1 = ntd.getDeclaredChildNodeDefinitions(); + assertEquals(nda.length, nda1.length); + for (int i = 0; i < nda.length; i++) { + assertEquals(nda[i].getName(), nda1[i].getName()); + assertEquals(nda[i].allowsSameNameSiblings(), nda1[i].allowsSameNameSiblings()); + assertTrue(Arrays.equals(nda[i].getRequiredPrimaryTypeNames(), nda1[i].getRequiredPrimaryTypeNames())); + assertEquals(nda[i].getDefaultPrimaryTypeName(), nda1[i].getDefaultPrimaryTypeName()); + assertEquals(nda[i].getRequiredPrimaryTypeNames(), nda1[i].getRequiredPrimaryTypeNames()); + } + + PropertyDefinition[] pda = ntt.getDeclaredPropertyDefinitions(); + PropertyDefinition[] pda1 = ntd.getDeclaredPropertyDefinitions(); + assertEquals(pda.length, pda1.length); + for (int i = 0; i < pda.length; i++) { + assertEquals(pda[i].getName(), pda1[i].getName()); + assertEquals(pda[i].getRequiredType(), pda1[i].getRequiredType()); + assertTrue(Arrays.equals(pda[i].getAvailableQueryOperators(), pda1[i].getAvailableQueryOperators())); + assertTrue(Arrays.equals(pda[i].getValueConstraints(), pda1[i].getValueConstraints())); + assertEquals(pda[i].isFullTextSearchable(), pda1[i].isFullTextSearchable()); + assertEquals(pda[i].isMultiple(), pda1[i].isMultiple()); + assertEquals(pda[i].isQueryOrderable(), pda1[i].isQueryOrderable()); + } + } + + public void testNewNodeTypeTemplate() throws Exception { + + String expandedName = "{" + NS_MIX_URI + "}" + "littlemixin"; + String jcrName = superuser.getNamespacePrefix(NS_MIX_URI) + ":littlemixin"; + + NodeTypeTemplate ntt = ntm.createNodeTypeTemplate(); + + ntt.setName(expandedName); + assertEquals(jcrName, ntt.getName()); + ntt.setName(jcrName); + assertEquals(jcrName, ntt.getName()); + + ntt.setAbstract(false); + assertFalse(ntt.isAbstract()); + + try { + ntt.setDeclaredSuperTypeNames(null); + fail("null isn't a valid array of jcr name"); + } catch (ConstraintViolationException e) { + // success + } + assertNotNull(ntt.getDeclaredSupertypeNames()); + assertEquals(0, ntt.getDeclaredSupertypeNames().length); + + ntt.setDeclaredSuperTypeNames(new String[] {mixReferenceable}); + assertNotNull(ntt.getDeclaredSupertypeNames()); + assertEquals(1, ntt.getDeclaredSupertypeNames().length); + assertEquals(mixReferenceable, ntt.getDeclaredSupertypeNames()[0]); + + ntt.setMixin(true); + assertTrue(ntt.isMixin()); + + ntt.setOrderableChildNodes(true); + assertTrue(ntt.hasOrderableChildNodes()); + + ntt.setQueryable(false); + assertFalse(ntt.isQueryable()); + + ntt.setPrimaryItemName(null); + assertNull(ntt.getPrimaryItemName()); + + ntt.setPrimaryItemName(jcrPrimaryType); + assertEquals(jcrPrimaryType, ntt.getPrimaryItemName()); + + PropertyDefinitionTemplate pdTemplate = createBooleanPropTemplate(); + + List pdefs = ntt.getPropertyDefinitionTemplates(); + pdefs.add(pdTemplate); + + assertNotNull(ntt.getDeclaredPropertyDefinitions()); + assertEquals(1, ntt.getDeclaredPropertyDefinitions().length); + assertEquals(pdTemplate, ntt.getDeclaredPropertyDefinitions()[0]); + + pdefs = ntt.getPropertyDefinitionTemplates(); + assertEquals(1, pdefs.size()); + assertEquals(pdTemplate, pdefs.get(0)); + + NodeDefinitionTemplate ndTemplate = ntm.createNodeDefinitionTemplate(); + + List ndefs = ntt.getNodeDefinitionTemplates(); + ndefs.add(ndTemplate); + + assertNotNull(ntt.getDeclaredChildNodeDefinitions()); + assertEquals(1, ntt.getDeclaredChildNodeDefinitions().length); + assertEquals(ndTemplate, ntt.getDeclaredChildNodeDefinitions()[0]); + + ndefs = ntt.getNodeDefinitionTemplates(); + assertEquals(1, ndefs.size()); + assertEquals(ndTemplate, ndefs.get(0)); + } + + public void testEmptyPropertyDefinitionTemplate() throws Exception { + PropertyDefinitionTemplate pdt = ntm.createPropertyDefinitionTemplate(); + + assertNull(pdt.getName()); + assertFalse(pdt.isAutoCreated()); + assertFalse(pdt.isMandatory()); + assertFalse(pdt.isProtected()); + assertEquals(OnParentVersionAction.COPY, pdt.getOnParentVersion()); + assertNull(pdt.getDeclaringNodeType()); + + assertEquals(PropertyType.STRING, pdt.getRequiredType()); + assertFalse(pdt.isMultiple()); + assertNull(pdt.getValueConstraints()); + assertNull(pdt.getDefaultValues()); + + // the following methods cannot be tested as default value is + // implementation specific: + // - getAvailableQueryOperators + // - isFullTextSearchable + // - isQueryOrderable + + } + + public void testPropertyDefinitionTemplate() throws Exception { + PropertyDefinitionTemplate pdt = createBooleanPropTemplate(); + + assertEquals(jcrPropName, pdt.getName()); + try { + pdt.setName(null); + fail("null isn't a valid JCR name"); + } catch (ConstraintViolationException e) { + // success + } + + + assertEquals(false, pdt.isAutoCreated()); + assertEquals(false, pdt.isMandatory()); + assertEquals(OnParentVersionAction.IGNORE, pdt.getOnParentVersion()); + assertEquals(false, pdt.isProtected()); + assertEquals(PropertyType.BOOLEAN, pdt.getRequiredType()); + assertEquals(null, pdt.getValueConstraints()); + assertEquals(null, pdt.getDefaultValues()); + assertEquals(false, pdt.isMultiple()); + String[] qo = pdt.getAvailableQueryOperators(); + assertEquals(1, qo.length); + assertEquals(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, qo[0]); + assertEquals(false, pdt.isFullTextSearchable()); + assertEquals(false, pdt.isQueryOrderable()); + } + + public void testSetDefaultValues() throws Exception { + + PropertyDefinitionTemplate pdt = ntm.createPropertyDefinitionTemplate(); + pdt.setRequiredType(PropertyType.LONG); + + pdt.setDefaultValues(null); + assertNull(pdt.getDefaultValues()); + + pdt.setDefaultValues(new Value[0]); + assertNotNull(pdt.getDefaultValues()); + assertEquals(0, pdt.getDefaultValues().length); + + pdt.setDefaultValues(new Value[] { superuser.getValueFactory().createValue(24)}); + assertNotNull(pdt.getDefaultValues()); + assertEquals(1, pdt.getDefaultValues().length); + assertEquals(24, pdt.getDefaultValues()[0].getLong()); + assertEquals(PropertyType.LONG, pdt.getDefaultValues()[0].getType()); + } + + public void testEmptyNodeDefinitionTemplate() throws Exception { + NodeDefinitionTemplate ndt = ntm.createNodeDefinitionTemplate(); + + assertNull(ndt.getName()); + assertFalse(ndt.isAutoCreated()); + assertFalse(ndt.isMandatory()); + assertFalse(ndt.isProtected()); + assertEquals(OnParentVersionAction.COPY, ndt.getOnParentVersion()); + assertNull(ndt.getDeclaringNodeType()); + + assertNull(ndt.getRequiredPrimaryTypes()); + assertNull(ndt.getRequiredPrimaryTypeNames()); + assertNull(ndt.getDefaultPrimaryType()); + assertNull(ndt.getDefaultPrimaryTypeName()); + assertFalse(ndt.allowsSameNameSiblings()); + } + + public void testNodeDefinitionTemplate() throws Exception { + NodeDefinitionTemplate ndt = ntm.createNodeDefinitionTemplate(); + + try { + ndt.setName(null); + fail("null isn't a valid JCR name"); + } catch (ConstraintViolationException e) { + // success + } + + String expandedName = "{" + NS_JCR_URI + "}" + "content"; + String jcrName = superuser.getNamespacePrefix(NS_JCR_URI) + ":content"; + ndt.setName(expandedName); + assertEquals(jcrName, ndt.getName()); + ndt.setName(jcrName); + assertEquals(jcrName, ndt.getName()); + + ndt.setSameNameSiblings(true); + assertTrue(ndt.allowsSameNameSiblings()); + + ndt.setAutoCreated(true); + assertTrue(ndt.isAutoCreated()); + + ndt.setMandatory(true); + assertTrue(ndt.isMandatory()); + + ndt.setProtected(true); + assertTrue(ndt.isProtected()); + + ndt.setOnParentVersion(OnParentVersionAction.VERSION); + assertEquals(OnParentVersionAction.VERSION, ndt.getOnParentVersion()); + + expandedName = "{" + NS_NT_URI + "}" + "folder"; + jcrName = superuser.getNamespacePrefix(NS_NT_URI) + ":folder"; + ndt.setDefaultPrimaryTypeName(expandedName); + assertEquals(jcrName, ndt.getDefaultPrimaryTypeName()); + + ndt.setDefaultPrimaryTypeName(null); + assertEquals("setting null must clear the name.", null, ndt.getDefaultPrimaryTypeName()); + + ndt.setRequiredPrimaryTypeNames(new String[] {expandedName}); + assertNotNull(ndt.getRequiredPrimaryTypeNames()); + assertEquals(1, ndt.getRequiredPrimaryTypeNames().length); + assertEquals(jcrName, ndt.getRequiredPrimaryTypeNames()[0]); + + try { + ndt.setRequiredPrimaryTypeNames(null); + fail("null isn't a valid array of jcr name"); + } catch (ConstraintViolationException e) { + // success + } + } + + public void testResidualNames() throws Exception { + String residualName = "*"; + + NodeDefinitionTemplate ndt = ntm.createNodeDefinitionTemplate(); + ndt.setName(residualName); + assertEquals(residualName, ndt.getName()); + + PropertyDefinitionTemplate pdt = ntm.createPropertyDefinitionTemplate(); + pdt.setName(residualName); + assertEquals(residualName, pdt.getName()); + } + + public void testInvalidJCRNames() throws Exception { + String invalidName = ":ab[2]"; + + // invalid name(s) passed to NT-template methods + NodeTypeTemplate ntt = ntm.createNodeTypeTemplate(); + try { + ntt.setName(invalidName); + fail("ConstraintViolationException expected. Nt-name is invalid"); + } catch (ConstraintViolationException e) { + // success + } + try { + ntt.setDeclaredSuperTypeNames(new String[] {"{" + NS_MIX_URI + "}" + "littlemixin", invalidName}); + fail("ConstraintViolationException expected. One of the super type names is invalid"); + } catch (ConstraintViolationException e) { + // success + } + try { + ntt.setPrimaryItemName(invalidName); + fail("ConstraintViolationException expected. Primary item name is invalid"); + } catch (ConstraintViolationException e) { + // success + } + + // invalid name(s) passed to NodeDefinitionTemplate + NodeDefinitionTemplate ndt = ntm.createNodeDefinitionTemplate(); + try { + ndt.setName(invalidName); + fail("ConstraintViolationException expected. Name is invalid"); + } catch (ConstraintViolationException e) { + // success + } + try { + ndt.setRequiredPrimaryTypeNames(new String[] {"{" + NS_MIX_URI + "}" + "littlemixin", invalidName}); + fail("ConstraintViolationException expected. One of the required primary type names is invalid"); + } catch (ConstraintViolationException e) { + // success + } + try { + ndt.setDefaultPrimaryTypeName(invalidName); + fail("ConstraintViolationException expected. Default primary type name is invalid"); + } catch (ConstraintViolationException e) { + // success + } + + // invalid name(s) passed to PropertyDefinitionTemplate + PropertyDefinitionTemplate pdt = ntm.createPropertyDefinitionTemplate(); + try { + pdt.setName(invalidName); + fail("ConstraintViolationException expected. Name is invalid"); + } catch (ConstraintViolationException e) { + // success + } + } + + public void testRegisterNodeType() throws Exception { + NodeTypeTemplate ntt = ntm.createNodeTypeTemplate(); + + ntt.setName("mix:foo"); + ntt.setAbstract(false); + ntt.setMixin(true); + ntt.setOrderableChildNodes(false); + ntt.setQueryable(false); + + PropertyDefinitionTemplate pdt = ntm.createPropertyDefinitionTemplate(); + pdt.setAutoCreated(false); + pdt.setName("foo"); + pdt.setMultiple(false); + pdt.setRequiredType(PropertyType.STRING); + List pdefs = ntt.getPropertyDefinitionTemplates(); + pdefs.add(pdt); + + ntm.registerNodeType(ntt, true); + + try { + ntm.registerNodeType(ntt, false); + fail("NodeTypeExistsException expected."); + } catch (NodeTypeExistsException e) { + // success + } + } + + public void testUnregisterNodeType() throws Exception { + try { + ntm.unregisterNodeType("unknownnodetype"); + fail("NoSuchNodeTypeException expected."); + } catch (NoSuchNodeTypeException e) { + // success + } + + try { + ntm.unregisterNodeType("nt:base"); + fail("RepositoryException expected."); + } catch (RepositoryException e) { + // success + } + } + + public void testUnregisterNodeTypes() throws Exception { + try { + ntm.unregisterNodeTypes(new String[] {"unknownnodetype1","unknownnodetype2"}); + fail("NoSuchNodeTypeException expected."); + } catch (NoSuchNodeTypeException e) { + // success + } + + try { + ntm.unregisterNodeTypes(new String[] {"nt:base", "nt:address"}); + fail("RepositoryException expected."); + } catch (RepositoryException e) { + // success + } + } + + public void testRegisterNodeTypes() throws Exception { + NodeTypeDefinition[] defs = new NodeTypeDefinition[5]; + for (int i = 0; i < defs.length; i++) { + NodeTypeTemplate ntt = ntm.createNodeTypeTemplate(); + ntt.setName("mix:foo" + i); + ntt.setAbstract(false); + ntt.setMixin(true); + ntt.setOrderableChildNodes(false); + ntt.setQueryable(false); + + PropertyDefinitionTemplate pdt = ntm.createPropertyDefinitionTemplate(); + pdt.setAutoCreated(false); + pdt.setName("foo" + i); + pdt.setMultiple(false); + pdt.setRequiredType(PropertyType.STRING); + List pdefs = ntt.getPropertyDefinitionTemplates(); + pdefs.add(pdt); + + defs[i] = ntt; + } + ntm.registerNodeTypes(defs, true); + + try { + ntm.registerNodeTypes(defs, false); + fail("NodeTypeExistsException expected."); + } catch (NodeTypeExistsException e) { + // success + } + } + + private PropertyDefinitionTemplate createBooleanPropTemplate() throws RepositoryException { + PropertyDefinitionTemplate pdt = ntm.createPropertyDefinitionTemplate(); + pdt.setName(expandedPropName); + pdt.setAutoCreated(false); + pdt.setMandatory(false); + pdt.setOnParentVersion(OnParentVersionAction.IGNORE); + pdt.setProtected(false); + pdt.setRequiredType(PropertyType.BOOLEAN); + pdt.setValueConstraints(null); + pdt.setDefaultValues(null); + pdt.setMultiple(false); + pdt.setAvailableQueryOperators(new String[] { QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO }); + pdt.setFullTextSearchable(false); + pdt.setQueryOrderable(false); + + return pdt; + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/nodetype/NodeTypeManagerTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeManagerTest.java similarity index 88% rename from src/test/org/apache/jackrabbit/test/api/nodetype/NodeTypeManagerTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeManagerTest.java index f4806404ea8..52aa0ceb57e 100644 --- a/src/test/org/apache/jackrabbit/test/api/nodetype/NodeTypeManagerTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeManagerTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -29,10 +29,6 @@ * Tests if the {@link NodeTypeManager} properly returns primary types an mixin * types. * - * @test - * @sources NodeTypeManagerTest.java - * @executeClass org.apache.jackrabbit.test.api.nodetype.NodeTypeManagerTest - * @keywords level1 */ public class NodeTypeManagerTest extends AbstractJCRTest { @@ -53,7 +49,7 @@ protected void setUp() throws Exception { isReadOnly = true; super.setUp(); - session = helper.getReadOnlySession(); + session = getHelper().getReadOnlySession(); manager = session.getWorkspace().getNodeTypeManager(); } @@ -63,7 +59,9 @@ protected void setUp() throws Exception { protected void tearDown() throws Exception { if (session != null) { session.logout(); + session = null; } + manager = null; super.tearDown(); } @@ -137,4 +135,4 @@ public void testGetMixinNodeTypes() throws RepositoryException { } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeTest.java new file mode 100644 index 00000000000..955bb67429d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeTest.java @@ -0,0 +1,507 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests if the node type hierarchy is correctly mapped to the methods + * defined in {@link NodeType}. + * + */ +public class NodeTypeTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(NodeTypeTest.class); + + /** + * The session we use for the tests + */ + private Session session; + + /** + * The node type manager of the session + */ + private NodeTypeManager manager; + + /** + * The root node of the default workspace + */ + private Node rootNode; + + /** + * Sets up the fixture for the test cases. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + manager = session.getWorkspace().getNodeTypeManager(); + rootNode = session.getRootNode(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + manager = null; + rootNode = null; + super.tearDown(); + } + + /** + * Test if getNode() returns the name of a node type. + */ + public void testGetName() throws RepositoryException { + NodeType type = manager.getNodeType(ntBase); + assertEquals("getName() must return the name of the node", + ntBase, type.getName()); + } + + /** + * Test if isMixin() returns false if applied on a primary node type and true + * on a mixin node type. + */ + public void testIsMixin() throws RepositoryException { + + NodeTypeIterator primaryTypes = manager.getPrimaryNodeTypes(); + assertFalse("testIsMixin() must return false if applied on a " + + "primary node type", + primaryTypes.nextNodeType().isMixin()); + + // if a mixin node type exist, test if isMixin() returns true + NodeTypeIterator mixinTypes = manager.getMixinNodeTypes(); + if (getSize(mixinTypes) > 0) { + // need to re-aquire iterator {@link #getSize} may consume iterator + mixinTypes = manager.getMixinNodeTypes(); + assertTrue("testIsMixin() must return true if applied on a " + + "mixin node type", + mixinTypes.nextNodeType().isMixin()); + } + // else skip the test for mixin node types + } + + /** + * Test if node.getPrimaryItemName() returns the same name as + * node.getPrimaryItem().getName() + */ + public void testGetPrimaryItemName() + throws NotExecutableException, RepositoryException { + + Node node = locateNodeWithPrimaryItem(rootNode); + + if (node == null) { + throw new NotExecutableException("Workspace does not contain a node with primary item defined"); + } + + String name = node.getPrimaryItem().getName(); + NodeType type = node.getPrimaryNodeType(); + + assertEquals("node.getPrimaryNodeType().getPrimaryItemName() " + + "must return the same name as " + + "node.getPrimaryItem().getName()", + name, type.getPrimaryItemName()); + } + + /** + * Test if node.getPrimaryItemName() returns null if no primary item is + * defined + */ + public void testGetPrimaryItemNameNotExisting() + throws NotExecutableException, RepositoryException { + + Node node = locateNodeWithoutPrimaryItem(rootNode); + + if (node == null) { + throw new NotExecutableException("Workspace does not contain a node without primary item defined"); + } + + NodeType type = node.getPrimaryNodeType(); + + assertNull("getPrimaryItemName() must return null if NodeType " + + "does not define a primary item", + type.getPrimaryItemName()); + } + + + /** + * Test if getSupertypes() of a primary node that is not "nt:base" returns at + * least "nt:base". NotExecutableException is thrown if no primary node type + * apart from "nt:base". + */ + public void testGetSupertypes() + throws NotExecutableException, RepositoryException { + + // find a primary node type but not "nt:base" + NodeTypeIterator types = manager.getPrimaryNodeTypes(); + NodeType type = null; + while (types.hasNext()) { + type = types.nextNodeType(); + if (!type.getName().equals(ntBase)) { + break; + } + } + + // note: type is never null, since at least "nt:base" must exist + if (type.getName().equals("nt:base")) { + throw new NotExecutableException("Workspace does not have sufficient primary node types to run " + + "this test. At least nt:base plus anther type are required."); + } + + NodeType supertypes[] = type.getSupertypes(); + boolean hasNTBase = false; + for (int i = 0; i < supertypes.length; i++) { + if (supertypes[i].getName().equals(ntBase)) { + hasNTBase = true; + break; + } + } + assertTrue("getSupertypes() of a primary node type that is not " + + "\"nt:base\" must at least return \"nt:base\"", + hasNTBase); + } + + /** + * Test if all node types returned by getDeclaredSupertypes() are also + * returned by getSupertypes(). All existing node types are tested. + */ + public void testGetDeclaredSupertypes() + throws RepositoryException { + + for (NodeTypeIterator types = manager.getAllNodeTypes(); types.hasNext(); ) { + NodeType type = types.nextNodeType(); + + Set declaredSupertypeNames = asSetOfNames(type.getDeclaredSupertypes()); + Set supertypeNames = asSetOfNames(type.getSupertypes()); + + assertTrue("all declared supertypes must be supertypes: " + + (new HashSet(declaredSupertypeNames).removeAll(supertypeNames)), + supertypeNames.containsAll(declaredSupertypeNames)); + + assertEquals("getDeclaredSuperTypes and getDeclaredSuperTypeNames must be consistent", + declaredSupertypeNames, new HashSet(Arrays.asList(type.getDeclaredSupertypeNames()))); + } + } + + /** + * Test if all node types returned by getDeclaredSubtypes() are also + * returned by getSubtypes(), and that the information is consistent + * with getSuperTypes/getDeclaredSuperTypes. All existing node types are tested. + * + * @since JCR 2.0 + */ + public void testGetDeclaredSubtypes() + throws RepositoryException { + + for (NodeTypeIterator types = manager.getAllNodeTypes(); types.hasNext(); ) { + NodeType type = types.nextNodeType(); + String name = type.getName(); + + Set declaredSubtypeNames = asSetOfNames(type.getDeclaredSubtypes()); + Set subtypeNames = asSetOfNames(type.getSubtypes()); + + assertTrue("all declared subtypes must be subtypes: " + + (new HashSet(declaredSubtypeNames).removeAll(subtypeNames)), + subtypeNames.containsAll(declaredSubtypeNames)); + + // check the reverse relation + for (Iterator it = subtypeNames.iterator(); it.hasNext(); ) { + String subtypename = it.next(); + boolean isDeclared = declaredSubtypeNames.contains(subtypename); + + NodeType subtype = manager.getNodeType(subtypename); + Set supertypeNames = asSetOfNames(subtype.getSupertypes()); + + assertTrue(name + " should occur in set of super types: " + supertypeNames, + supertypeNames.contains(name)); + + if (isDeclared) { + Set declaredSupertypeNames = asSetOfNames(subtype.getDeclaredSupertypes()); + assertTrue(name + " should occur in set of declared super types: " + declaredSupertypeNames, + declaredSupertypeNames.contains(name)); + } + } + } + } + + /** + * Test if isNodeType(String nodeTypeName) returns true if nodeTypeName is + * the name of the node itself. Also, primary node types must return true if + * nodeTypeName is "nt:base", and mixin node types must return false in that + * case. + */ + public void testIsNodeType() + throws RepositoryException { + + // find a primary node type but not "nt:base" + NodeTypeIterator types = manager.getPrimaryNodeTypes(); + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + assertTrue("isNodeType(String nodeTypeName) must return true if " + + "NodeType is nodeTypeName", + type.isNodeType(type.getName())); + if (type.isMixin()) { + assertFalse("isNodeType(String nodeTypeName) must return " + + "false if NodeType is not a subtype of " + + "nodeTypeName", + type.isNodeType(ntBase)); + } else { + assertTrue("isNodeType(String nodeTypeName) must return true if " + + "NodeType is a subtype of nodeTypeName", + type.isNodeType(ntBase)); + } + } + } + + /** + * Like {@link #testIsNodeType()}, but using qualified names + */ + public void testIsNodeTypeQName() throws RepositoryException { + + // find a primary node type but not "nt:base" + NodeTypeIterator types = manager.getPrimaryNodeTypes(); + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + String typename = type.getName(); + String ns = session.getNamespaceURI(AbstractJCRTest.getPrefix(typename)); + if (ns.length() != 0 && !ns.contains(":")) { + log.warn("Node type '" + typename + "' has invalid namespace '" + ns + + "', thus skipping testIsNodeTypeQName() for this type"); + } else { + String qn = AbstractJCRTest.getQualifiedName(session, typename); + assertTrue("isNodeType(String nodeTypeName) must return true if " + "NodeType is nodeTypeName", + type.isNodeType(qn)); + } + if (type.isMixin()) { + assertFalse("isNodeType(String nodeTypeName) must return " + "false if NodeType is not a subtype of " + + "nodeTypeName", type.isNodeType(NodeType.NT_BASE)); + } else { + assertTrue("isNodeType(String nodeTypeName) must return true if " + "NodeType is a subtype of nodeTypeName", + type.isNodeType(NodeType.NT_BASE)); + } + } + } + + /** + * Test if all property defs returned by getDeclatedPropertyDefs() are also + * returned by getPropertyDefs(). All existing node types are tested. + */ + public void testGetDeclaredPropertyDefs() + throws RepositoryException { + + NodeTypeIterator types = manager.getAllNodeTypes(); + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + + PropertyDefinition declaredDefs[] = type.getDeclaredPropertyDefinitions(); + PropertyDefinition defs[] = type.getPropertyDefinitions(); + + try { + for (int i = 0; i < declaredDefs.length; i++) { + boolean exists = false; + for (int j = 0; j < defs.length; j++) { + if (defs[j].getName().equals(declaredDefs[i].getName())) { + exists = true; + break; + } + } + assertTrue("All property defs returned by " + + "getDeclaredPropertyDefs() must also be " + + "returned by getPropertyDefs()", + exists); + } + } catch (ArrayIndexOutOfBoundsException e) { + fail("The array returned by " + + "getDeclaredPropertyDefs() must not exceed " + + "the one returned by getPropertyDefs()"); + } + } + } + + /** + * Test if getPropertyDefs() of a primary node returns also "jcr:primaryType" + * which is inherited from "nt:base". + */ + public void testGetPropertyDefs() + throws NotExecutableException, RepositoryException { + + // find a primary node type but not "nt:base" + NodeTypeIterator types = manager.getPrimaryNodeTypes(); + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + PropertyDefinition defs[] = type.getPropertyDefinitions(); + boolean hasJCRPrimaryType = false; + for (int i = 0; i < defs.length; i++) { + if (defs[i].getName().equals(jcrPrimaryType)) { + hasJCRPrimaryType = true; + break; + } + } + assertTrue("getPropertyDefs() of a primary node type " + + "must return also \"jcr:primaryType\".", + hasJCRPrimaryType); + } + } + + /** + * Test if all node defs returned by getDeclaredChildNodeDefs() are also + * returned by getChildNodeDefs(). All existing node types are tested. + */ + public void testGetDeclaredChildNodeDefs() + throws RepositoryException { + + NodeTypeIterator types = manager.getAllNodeTypes(); + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + + NodeDefinition declaredDefs[] = type.getDeclaredChildNodeDefinitions(); + NodeDefinition defs[] = type.getChildNodeDefinitions(); + + try { + for (int i = 0; i < declaredDefs.length; i++) { + boolean exists = false; + for (int j = 0; j < defs.length; j++) { + if (defs[j].getName().equals(declaredDefs[i].getName())) { + exists = true; + break; + } + } + assertTrue("All node defs returned by " + + "getDeclaredChildNodeDefs() must also be " + + "returned by getChildNodeDefs().", + exists); + } + } catch (ArrayIndexOutOfBoundsException e) { + fail("The array returned by " + + "getDeclaredChildNodeDefs() must not exceed " + + "the one returned by getChildNodeDefs()"); + } + } + } + + //-----------------------< internal >--------------------------------------- + + /** + * Returns the first descendant of node which defines primary + * item + * + * @param node Node to start traversal. + * @return first node with primary item + */ + private Node locateNodeWithPrimaryItem(Node node) + throws RepositoryException { + + try { + node.getPrimaryItem(); + return node; + } catch (ItemNotFoundException e) { + + } + + Node skippedFolder = null; + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node testNode = nodes.nextNode(); + if (testNode.getPath().equals("/jcr:system")) { + skippedFolder = testNode; + } else { + Node returnedNode = locateNodeWithPrimaryItem(testNode); + if (returnedNode != null) { + return returnedNode; + } + } + } + // check jcr:system if we skipped it before + if (skippedFolder != null) { + Node returnedNode = locateNodeWithPrimaryItem(skippedFolder); + if (returnedNode != null) { + return returnedNode; + } + } + return null; + } + + /** + * Returns the first descendant of node without a primary item + * + * @param node + * @return first node without primary item + */ + private Node locateNodeWithoutPrimaryItem(Node node) + throws RepositoryException { + + try { + node.getPrimaryItem(); + } catch (ItemNotFoundException e) { + return node; + } + + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + Node returnedNode = this.locateNodeWithoutPrimaryItem(nodes.nextNode()); + if (returnedNode != null) { + return returnedNode; + } + } + return null; + } + + /** + * Return the set of node type names for the specified node types. + */ + private Set asSetOfNames(NodeType[] types) { + Set result = new HashSet(); + for (int i = 0; i < types.length; i++) { + result.add(types[i].getName()); + } + return result; + } + + /** + * Return the set of node type names for the specified node types. + */ + private Set asSetOfNames(NodeTypeIterator it) { + Set result = new HashSet(); + while (it.hasNext()) { + result.add(it.nextNodeType().getName()); + } + return result; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeUtil.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeUtil.java new file mode 100644 index 00000000000..f1d561f669e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/NodeTypeUtil.java @@ -0,0 +1,960 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.nodetype; + +import org.apache.jackrabbit.test.ISO8601; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class to locate item definitions in the NodeTyeManager. + */ +public class NodeTypeUtil { + + public static final int ANY_PROPERTY_TYPE = -1; + + /** + * Locate a non-protected child node def declared by a non-abstract node type + * parsing all node types + * + * @param session the session to access the node types + * @param regardDefaultPrimaryType if true, the default primary type of the + * returned NodeDef is + * according to param defaultPrimaryType. + * If false, the returned NodeDef + * might have a default primary type or + * not. + * @param defaultPrimaryType if regardDefaultPrimaryType + * is true: if true, the returned + * NodeDef has a default + * primary type, else not + * @param residual if true, the returned NodeDef + * is of the residual name "*", else not + * @return + * @throws RepositoryException + */ + public static NodeDefinition locateChildNodeDef(Session session, + boolean regardDefaultPrimaryType, + boolean defaultPrimaryType, + boolean residual) + throws RepositoryException { + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator types = manager.getAllNodeTypes(); + + boolean skip = false; + + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + + // node types with more than one residual child node definition + // will cause trouble in test cases. the implementation + // might pick another definition than the definition returned by + // this method, when a child node is set. + NodeDefinition[] childDefs = type.getChildNodeDefinitions(); + int residuals = 0; + for (int i = 0; i < childDefs.length; i++) { + if (childDefs[i].getName().equals("*")) { + residuals++; + } + } + if (residuals > 1) { + // more than one residual, not suitable for tests + continue; + } + + NodeDefinition nodeDefs[] = type.getDeclaredChildNodeDefinitions(); + + for (int i = 0; i < nodeDefs.length; i++) { + NodeDefinition nodeDef = nodeDefs[i]; + + if (nodeDef.getDeclaringNodeType().isAbstract()) { + continue; + } + + if (nodeDef.isProtected()) { + continue; + } + + if (nodeDef.getRequiredPrimaryTypes().length > 1) { + // behaviour of implementations that support multiple multiple inheritance + // of primary node types is not specified + continue; + } + + if (regardDefaultPrimaryType) { + + if (defaultPrimaryType && nodeDef.getDefaultPrimaryType() == null) { + continue; + } + + if (!defaultPrimaryType && nodeDef.getDefaultPrimaryType() != null) { + continue; + } + } + + if (residual && !nodeDef.getName().equals("*")) { + continue; + } + + if (!residual) { + // if another child node def is a residual definition + // skip the current node type + NodeDefinition nodeDefsAll[] = type.getChildNodeDefinitions(); + for (int j = 0; j < nodeDefsAll.length; j++) { + if (nodeDefsAll[j].getName().equals("*")) { + skip = true; + break; + } + } + if (skip) { + // break the loop of the current child not defs + skip = false; + break; + } + } + + return nodeDef; + } + } + return null; + } + + /** + * Locate all non-protected child node def declared by a non-abstract node type + * parsing all node types + * + * @param session the session to access the node types + * @param regardDefaultPrimaryType if true, the default primary type of the + * returned NodeDef is + * according to param defaultPrimaryType. + * If false, the returned NodeDef + * might have a default primary type or + * not. + * @param defaultPrimaryType if regardDefaultPrimaryType + * is true: if true, the returned + * NodeDef has a default + * primary type, else not + * @param residual if true, the returned NodeDef + * is of the residual name "*", else not + * @return + * @throws RepositoryException + */ + public static List locateAllChildNodeDef(Session session, + boolean regardDefaultPrimaryType, + boolean defaultPrimaryType, + boolean residual) + throws RepositoryException { + List nodeTypes = new ArrayList(); + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator types = manager.getAllNodeTypes(); + + boolean skip = false; + + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + + // node types with more than one residual child node definition + // will cause trouble in test cases. the implementation + // might pick another definition than the definition returned by + // this method, when a child node is set. + NodeDefinition[] childDefs = type.getChildNodeDefinitions(); + int residuals = 0; + for (int i = 0; i < childDefs.length; i++) { + if (childDefs[i].getName().equals("*")) { + residuals++; + } + } + if (residuals > 1) { + // more than one residual, not suitable for tests + continue; + } + + NodeDefinition nodeDefs[] = type.getDeclaredChildNodeDefinitions(); + + for (int i = 0; i < nodeDefs.length; i++) { + NodeDefinition nodeDef = nodeDefs[i]; + + if (nodeDef.getDeclaringNodeType().isAbstract()) { + continue; + } + + if (nodeDef.isProtected()) { + continue; + } + + if (nodeDef.getRequiredPrimaryTypes().length > 1) { + // behaviour of implementations that support multiple multiple inheritance + // of primary node types is not specified + continue; + } + + if (regardDefaultPrimaryType) { + + if (defaultPrimaryType && nodeDef.getDefaultPrimaryType() == null) { + continue; + } + + if (!defaultPrimaryType && nodeDef.getDefaultPrimaryType() != null) { + continue; + } + } + + if (residual && !nodeDef.getName().equals("*")) { + continue; + } + + if (!residual) { + // if another child node def is a residual definition + // skip the current node type + NodeDefinition nodeDefsAll[] = type.getChildNodeDefinitions(); + for (int j = 0; j < nodeDefsAll.length; j++) { + if (nodeDefsAll[j].getName().equals("*")) { + skip = true; + break; + } + } + if (skip) { + // break the loop of the current child not defs + skip = false; + break; + } + } + + nodeTypes.add(nodeDef); + } + } + return nodeTypes; + } + + /** + * Locate a child node def parsing all node types + * + * @param session the session to access the node types + * @param isProtected if true, the returned NodeDef is + * protected, else not + * @param mandatory if true, the returned NodeDef is + * mandatory, else not + * @return the first NodeDef found fitting the requirements + */ + public static NodeDefinition locateChildNodeDef(Session session, + boolean isProtected, + boolean mandatory) + throws RepositoryException { + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator types = manager.getAllNodeTypes(); + + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + NodeDefinition nodeDefs[] = type.getDeclaredChildNodeDefinitions(); + + for (int i = 0; i < nodeDefs.length; i++) { + NodeDefinition nodeDef = nodeDefs[i]; + + if (nodeDef.getName().equals("*")) { + continue; + } + + if (isProtected && !nodeDef.isProtected()) { + continue; + } + if (!isProtected && nodeDef.isProtected()) { + continue; + } + + if (mandatory && !nodeDef.isMandatory()) { + continue; + } + if (!mandatory && nodeDef.isMandatory()) { + continue; + } + + return nodeDef; + } + } + return null; + } + + /** + * Locate a property def parsing all node types + * + * @param session the session to access the node types + * @param propertyType the type of the returned property. -1 indicates to + * return a property of any type but not UNDEFIEND + * @param multiple if true, the returned PropertyDef is + * multiple, else not + * @param isProtected if true, the returned PropertyDef is + * protected, else not + * @param residual if true, the returned PropertyDef is of + * the residual name "*", else not + * @return the first PropertyDef found fitting the + * requirements + */ + public static PropertyDefinition locatePropertyDef(Session session, + int propertyType, + boolean multiple, + boolean isProtected, + boolean constraints, + boolean residual) + throws RepositoryException { + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator types = manager.getAllNodeTypes(); + + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + PropertyDefinition propDefs[] = type.getDeclaredPropertyDefinitions(); + for (int i = 0; i < propDefs.length; i++) { + PropertyDefinition propDef = propDefs[i]; + + if (propertyType != ANY_PROPERTY_TYPE && + propDef.getRequiredType() != propertyType) { + continue; + } + + if (propertyType == ANY_PROPERTY_TYPE && + propDef.getRequiredType() == PropertyType.UNDEFINED) { + continue; + } + + if (multiple && !propDef.isMultiple()) { + continue; + } + if (!multiple && propDef.isMultiple()) { + continue; + } + + if (isProtected && !propDef.isProtected()) { + continue; + } + if (!isProtected && propDef.isProtected()) { + continue; + } + + String vc[] = propDef.getValueConstraints(); + if (!constraints && vc != null && vc.length > 0) { + continue; + } + if (constraints) { + // property def with constraints requested + if (vc == null || vc.length == 0) { + // property def has no constraints + continue; + } + } + + if (!residual && propDef.getName().equals("*")) { + continue; + } + + if (residual && !propDef.getName().equals("*")) { + continue; + } + + // also skip property residual property definition if there + // is another residual definition + if (residual) { + // check if there is another residual property def + if (getNumResidualPropDefs(type) > 1) { + continue; + } + } + + if (!residual) { + // if not looking for a residual property def then there + // must not be any residual definition at all on the node + // type + if (getNumResidualPropDefs(type) > 0) { + continue; + } + } + + return propDef; + } + } + return null; + } + + /** + * Returns the number of residual property definitions of type + * including its base types. + * @param type the node type + * @return the number of residual property definitions. + */ + private static int getNumResidualPropDefs(NodeType type) { + PropertyDefinition[] pDefs = type.getPropertyDefinitions(); + int residuals = 0; + for (int j = 0; j < pDefs.length; j++) { + PropertyDefinition pDef = pDefs[j]; + if (pDef.getName().equals("*")) { + residuals++; + } + } + return residuals; + } + + /** + * Locate a property def parsing all node types + * + * @param session the session to access the node types + * @param isProtected if true, the returned PropertyDef is + * protected, else not + * @param mandatory if true, the returned PropertyDef is + * mandatory, else not + * @return the first PropertyDef found fitting the + * requirements + */ + public static PropertyDefinition locatePropertyDef(Session session, + boolean isProtected, + boolean mandatory) + throws RepositoryException { + + NodeTypeManager manager = session.getWorkspace().getNodeTypeManager(); + NodeTypeIterator types = manager.getAllNodeTypes(); + + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + PropertyDefinition propDefs[] = type.getDeclaredPropertyDefinitions(); + for (int i = 0; i < propDefs.length; i++) { + PropertyDefinition propDef = propDefs[i]; + + if (propDef.getName().equals("*")) { + continue; + } + + if (isProtected && !propDef.isProtected()) { + continue; + } + if (!isProtected && propDef.isProtected()) { + continue; + } + + if (mandatory && !propDef.isMandatory()) { + continue; + } + if (!mandatory && propDef.isMandatory()) { + continue; + } + + return propDef; + } + } + return null; + } + + /** + * Returns a name that is not defined by the nodeType's child node def + */ + public static String getUndefinedChildNodeName(NodeType nodeType) { + + NodeDefinition nodeDefs[] = nodeType.getChildNodeDefinitions(); + StringBuffer s = new StringBuffer("X"); + + for (int i = 0; i < nodeDefs.length; i++) { + s.append(nodeDefs[i].getName()); + } + String undefinedName = s.toString(); + undefinedName = undefinedName.replaceAll("\\*", ""); + undefinedName = undefinedName.replaceAll(":", ""); + return undefinedName; + } + + /** + * Returns a node type that is nor legalType nor a sub type of of + */ + public static String getIllegalChildNodeType(NodeTypeManager manager, + String legalType) + throws RepositoryException { + + NodeTypeIterator types = manager.getAllNodeTypes(); + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + if (!type.getName().equals(legalType)) { + NodeType superTypes[] = type.getSupertypes(); + boolean isSubType = false; + for (int i = 0; i < superTypes.length; i++) { + String name = superTypes[i].getName(); + if (name.equals(legalType)) { + isSubType = true; + break; + } + } + if (!isSubType) { + return type.getName(); + } + } + } + return null; + } + + /** + * Returns any value of the requested type + */ + public static Value getValueOfType(Session session, int type) + throws ValueFormatException, UnsupportedOperationException, RepositoryException { + switch (type) { + case (PropertyType.BINARY): + // note: If binary is not UTF-8 behavior is implementation-specific + return session.getValueFactory().createValue("abc", PropertyType.BINARY); + case (PropertyType.BOOLEAN): + return session.getValueFactory().createValue(true); + case (PropertyType.DATE): + return session.getValueFactory().createValue(Calendar.getInstance()); + case (PropertyType.DOUBLE): + return session.getValueFactory().createValue(1.0); + case (PropertyType.LONG): + return session.getValueFactory().createValue(1); + case (PropertyType.NAME): + return session.getValueFactory().createValue("abc", PropertyType.NAME); + case (PropertyType.PATH): + return session.getValueFactory().createValue("/abc", PropertyType.PATH); + default: + // STRING and UNDEFINED + // note: REFERENCE is not testable since its format is implementation-specific + return session.getValueFactory().createValue("abc"); + } + } + + /** + * Returns a value according to the value contraints of a + * PropertyDefinition + * + * @param propDef The PropertyDefinition whose constraints + * will be regarded + * @param satisfied If true, the returned Value will satisfying + * the constraints - If false, the returned + * Value will not satisfying the constraints. + * @return Depending on param satisfied a Value + * satisfying or not satistying the constraints of + * propDef will be returned. Null will be returned if + * no accordant Value could be build. + */ + public static Value getValueAccordingToValueConstraints(Session session, + PropertyDefinition propDef, + boolean satisfied) + throws ValueFormatException, RepositoryException { + + int type = propDef.getRequiredType(); + String constraints[] = propDef.getValueConstraints(); + + if (constraints == null || constraints.length == 0) { + return null; + } + + switch (type) { + case (PropertyType.BINARY): + { + long absMin = 0; + long absMax = 0; + + // indicate if absMin and absMax are already set + boolean absMinSet = false; + boolean absMaxSet = false; + + // boundless vars indicate min/max without bounds, + // if constraint is e.g.(min,) or [,max] + boolean maxBoundless = false; + boolean minBoundless = false; + + // find smallest min and largest max value + for (int i = 0; i < constraints.length; i++) { + if (!minBoundless) { + String minStr = getConstraintMin(constraints[i]); + if (minStr == null) { + minBoundless = true; + } else { + long min = Long.valueOf(minStr).longValue(); + if (!absMinSet) { + absMin = min; + absMinSet = true; + } else if (min < absMin) { + absMin = min; + } + } + } + if (!maxBoundless) { + String maxStr = getConstraintMax(constraints[i]); + if (maxStr == null) { + maxBoundless = true; + } else { + long max = Long.valueOf(maxStr).longValue(); + if (!absMaxSet) { + absMax = max; + absMaxSet = true; + } else if (max > absMax) { + absMin = max; + } + } + } + } + if (satisfied) { + // build a binary value absMin < size > absMax + StringBuffer content = new StringBuffer(); + for (int i = 0; i <= absMin + 1; i++) { + content.append("X"); + } + if (!maxBoundless && content.length() >= absMax) { + return null; + } else { + return session.getValueFactory().createValue(content.toString(), PropertyType.BINARY); + } + } else { + if (!minBoundless && absMin > 1) { + // return a value of size < absMin + return session.getValueFactory().createValue("0", PropertyType.BINARY); + } else if (!maxBoundless) { + // build a binary value of size > absMax + StringBuffer content = new StringBuffer(); + for (int i = 0; i <= absMax; i = i + 10) { + content.append("0123456789"); + } + return session.getValueFactory().createValue(content.toString(), PropertyType.BINARY); + } else { + return null; + } + } + } + + case (PropertyType.BOOLEAN): + { + if (constraints.length > 1) { + return null; // silly constraint + } + boolean value = Boolean.valueOf(constraints[0]).booleanValue(); + if (satisfied) { + return session.getValueFactory().createValue(value); + } else { + return session.getValueFactory().createValue(!value); + } + } + + case (PropertyType.DATE): + { + Calendar absMin = null; + Calendar absMax = null; + + // boundless vars indicate min/max without bounds, + // if constraint is e.g.(min,) or [,max] + boolean maxBoundless = false; + boolean minBoundless = false; + + // find smallest min and largest max value + for (int i = 0; i < constraints.length; i++) { + if (!minBoundless) { + String minStr = getConstraintMin(constraints[i]); + if (minStr == null) { + minBoundless = true; + } else { + Calendar min = ISO8601.parse(minStr); + if (absMin == null || min.before(absMin)) { + absMin = min; + } + } + } + if (!maxBoundless) { + String maxStr = getConstraintMax(constraints[i]); + if (maxStr == null) { + maxBoundless = true; + } else { + Calendar max = ISO8601.parse(maxStr); + if (absMax == null || max.after(absMax)) { + absMax = max; + } + } + } + } + if (satisfied) { + if (absMin != null) { + absMin.setTimeInMillis(absMin.getTimeInMillis() + 1); + if (absMin.after(absMax)) { + return null; + } + return session.getValueFactory().createValue(absMin); + } else if (absMax != null) { + absMax.setTimeInMillis(absMax.getTimeInMillis() - 1); + if (absMax.before(absMin)) { + return null; + } + return session.getValueFactory().createValue(absMax); + } else { + // neither min nor max set: return "now" + return session.getValueFactory().createValue(Calendar.getInstance()); + } + } else { + if (!minBoundless) { + absMin.setTimeInMillis(absMin.getTimeInMillis() - 1); + return session.getValueFactory().createValue(absMin); + } else if (!maxBoundless) { + absMax.setTimeInMillis(absMax.getTimeInMillis() + 1); + return session.getValueFactory().createValue(absMax); + } else { + return null; + } + } + } + + case (PropertyType.DOUBLE): + { + double absMin = 0; + double absMax = 0; + + // indicate if absMin and absMax are already set + boolean absMinSet = false; + boolean absMaxSet = false; + + // boundless vars indicate min/max without bounds, + // if constraint is e.g.(min,) or [,max] + boolean maxBoundless = false; + boolean minBoundless = false; + + // find smallest min and largest max value + for (int i = 0; i < constraints.length; i++) { + if (!minBoundless) { + String minStr = getConstraintMin(constraints[i]); + if (minStr == null) { + minBoundless = true; + } else { + double min = Double.valueOf(minStr).doubleValue(); + if (!absMinSet) { + absMin = min; + absMinSet = true; + } else if (min < absMin) { + absMin = min; + } + } + } + if (!maxBoundless) { + String maxStr = getConstraintMax(constraints[i]); + if (maxStr == null) { + maxBoundless = true; + } else { + double max = Double.valueOf(maxStr).doubleValue(); + if (!absMaxSet) { + absMax = max; + absMaxSet = true; + } else if (max > absMax) { + absMax = max; + } + } + } + } + if (satisfied) { + if (minBoundless) { + return session.getValueFactory().createValue(absMax - 1.0); + } else if (maxBoundless) { + return session.getValueFactory().createValue(absMin + 1.0); + } else if (absMin < absMax) { + double d = (absMin + absMax) / 2; + return session.getValueFactory().createValue(d); + } else { + return null; + } + } else { + if (!minBoundless) { + return session.getValueFactory().createValue(absMin - 1.0); + } else if (!maxBoundless) { + return session.getValueFactory().createValue(absMax + 1.0); + } else { + return null; + } + } + } + + case (PropertyType.LONG): + { + long absMin = 0; + long absMax = 0; + + // indicate if absMin and absMax are already set + boolean absMinSet = false; + boolean absMaxSet = false; + + // boundless vars indicate min/max without bounds, + // if constraint is e.g.(min,) or [,max] + boolean maxBoundless = false; + boolean minBoundless = false; + + // find smallest min and largest max value + for (int i = 0; i < constraints.length; i++) { + if (!minBoundless) { + String minStr = getConstraintMin(constraints[i]); + if (minStr == null) { + minBoundless = true; + } else { + long min = Long.valueOf(minStr).longValue(); + if (!absMinSet) { + absMin = min; + absMinSet = true; + } else if (min < absMin) { + absMin = min; + } + } + } + if (!maxBoundless) { + String maxStr = getConstraintMax(constraints[i]); + if (maxStr == null) { + maxBoundless = true; + } else { + long max = Long.valueOf(maxStr).longValue(); + if (!absMaxSet) { + absMax = max; + absMaxSet = true; + } else if (max > absMax) { + absMax = max; + } + } + } + } + if (satisfied) { + if (minBoundless) { + return session.getValueFactory().createValue(absMax - 1); + } else if (maxBoundless) { + return session.getValueFactory().createValue(absMin + 1); + } else if (absMin < absMax - 1) { + long x = (absMin + absMax) / 2; + return session.getValueFactory().createValue(x); + } else { + return null; + } + } else { + if (!minBoundless) { + return session.getValueFactory().createValue(absMin - 1); + } else if (!maxBoundless) { + return session.getValueFactory().createValue(absMax + 1); + } else { + return null; + } + } + } + + case (PropertyType.NAME): + { + if (satisfied) { + // not in use so far + return null; + } else { + // build a name that is for sure not part of the constraints + StringBuffer name = new StringBuffer("X"); + for (int i = 0; i < constraints.length; i++) { + name.append(constraints[i].replaceAll(":", "")); + } + return session.getValueFactory().createValue(name.toString(), PropertyType.NAME); + } + } + + case (PropertyType.PATH): + { + if (satisfied) { + // not in use so far + return null; + } else { + // build a path that is for sure not part of the constraints + StringBuffer path = new StringBuffer("X"); + for (int i = 0; i < constraints.length; i++) { + path.append(constraints[i]); + } + String pathStr = path.toString(); + + // replace colon to avoid /a/x:b + y:c => /a/x:b:y:c + // where x:b:y:c is not a legal path element + pathStr = pathStr.replaceAll(":", ""); + pathStr = pathStr.replaceAll("\\*", ""); + pathStr = pathStr.replaceAll("//", "/"); + + return session.getValueFactory().createValue(pathStr, PropertyType.PATH); + } + } + + case (PropertyType.UNDEFINED): + { + return null; + } + + default: + { + if (satisfied) { + // not in use so far + return null; + } else { + // build a string that will probably not satisfy the constraints + StringBuffer value = new StringBuffer("X"); + for (int i = 0; i < constraints.length; i++) { + value.append(constraints[i]); + } + + // test if value does not match any of the constraints + for (int i = 0; i < constraints.length; i++) { + Pattern pattern = Pattern.compile(constraints[i]); + Matcher matcher = pattern.matcher(value); + if (matcher.matches()) { + return null; + } + } + return session.getValueFactory().createValue(value.toString()); + } + } + } + } + + // ------------------------< internal >------------------------------------- + + /** + * Get the min value (as string) of a numeric/date constraint string + */ + private static String getConstraintMin(String constraint) { + String min = constraint.substring(0, constraint.indexOf(",")); + min = min.replaceAll("\\(", ""); + min = min.replaceAll("\\[", ""); + min = min.replaceAll(" ", ""); + if (min.equals("")) { + min = null; + } + return min; + } + + /** + * Get the max value (as string) of a numeric/date constraint string + */ + private static String getConstraintMax(String constraint) { + String max = constraint.substring(constraint.indexOf(",") + 1); + max = max.replaceAll("\\)", ""); + max = max.replaceAll("\\]", ""); + max = max.replaceAll(" ", ""); + if (max.equals("")) { + max = null; + } + return max; + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/PredefinedNodeTypeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/PredefinedNodeTypeTest.java new file mode 100644 index 00000000000..e08cf16d00f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/PredefinedNodeTypeTest.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.test.api.nodetype; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * PredefinedNodeTypeTest tests if the predefined node types are + * implemented correctly. + * + */ +public class PredefinedNodeTypeTest extends AbstractJCRTest { + + private static final Map SUPERTYPES = new HashMap(); + + static { + SUPERTYPES.put("mix:created", new String[]{}); + SUPERTYPES.put("mix:etag", new String[]{}); + SUPERTYPES.put("mix:language", new String[]{}); + SUPERTYPES.put("mix:lastModified", new String[]{}); + SUPERTYPES.put("mix:lifecycle", new String[]{}); + SUPERTYPES.put("mix:lockable", new String[]{}); + SUPERTYPES.put("mix:mimeType", new String[]{}); + SUPERTYPES.put("mix:referenceable", new String[]{}); + SUPERTYPES.put("mix:shareable", new String[]{"mix:referenceable"}); + SUPERTYPES.put("mix:simpleVersionable", new String[]{}); + SUPERTYPES.put("mix:title", new String[]{}); + SUPERTYPES.put("mix:versionable", new String[]{"mix:referenceable", "mix:simpleVersionable"}); + SUPERTYPES.put("nt:activity", new String[]{"nt:base"}); + SUPERTYPES.put("nt:address", new String[]{"nt:base"}); + SUPERTYPES.put("nt:base", new String[]{}); + SUPERTYPES.put("nt:childNodeDefinition", new String[]{"nt:base"}); + SUPERTYPES.put("nt:configuration", new String[]{"nt:base"}); + SUPERTYPES.put("nt:file", new String[]{"nt:hierarchyNode"}); + SUPERTYPES.put("nt:folder", new String[]{"nt:hierarchyNode"}); + SUPERTYPES.put("nt:frozenNode", new String[]{"nt:base", "mix:referenceable"}); + SUPERTYPES.put("nt:hierarchyNode", new String[]{"nt:base", "mix:created"}); + SUPERTYPES.put("nt:linkedFile", new String[]{"nt:hierarchyNode"}); + SUPERTYPES.put("nt:nodeType", new String[]{"nt:base"}); + SUPERTYPES.put("nt:propertyDefinition", new String[]{"nt:base"}); + SUPERTYPES.put("nt:query", new String[]{"nt:base"}); + SUPERTYPES.put("nt:resource", new String[]{"nt:base", "mix:lastModified", "mix:mimeType"}); + SUPERTYPES.put("nt:unstructured", new String[]{"nt:base"}); + SUPERTYPES.put("nt:version", new String[]{"nt:base", "mix:referenceable"}); + SUPERTYPES.put("nt:versionedChild", new String[]{"nt:base"}); + SUPERTYPES.put("nt:versionHistory", new String[]{"nt:base", "mix:referenceable"}); + SUPERTYPES.put("nt:versionLabels", new String[]{"nt:base"}); + } + + /** + * The NodeTypeManager of the session + */ + private NodeTypeManager manager; + + /** + * The read-only session for the test + */ + private Session session; + + /** + * Sets up the fixture for this test. + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + + session = getHelper().getReadOnlySession(); + manager = session.getWorkspace().getNodeTypeManager(); + } + + /** + * Releases the session aquired in {@link #setUp()}. + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + manager = null; + super.tearDown(); + } + + /** + * Tests if all primary node types are subtypes of node type nt:base + */ + public void testIfPrimaryNodeTypesAreSubtypesOfNTBase() + throws RepositoryException { + NodeTypeIterator types = manager.getPrimaryNodeTypes(); + while (types.hasNext()) { + NodeType type = types.nextNodeType(); + assertTrue("Primary node type " + type.getName() + + " must inherit nt:base", + type.isNodeType("nt:base")); + } + } + + /** Test for the predefined mix:lifecycle node type. */ + public void testLifecycle() throws NotExecutableException { + testPredefinedNodeType("mix:lifecycle", false); + } + + /** Test for the predefined mix:lockable node type. */ + public void testLockable() throws NotExecutableException { + testPredefinedNodeType("mix:lockable", false); + } + + /** Test for the predefined mix:referenceable node type. */ + public void testReferenceable() throws NotExecutableException { + testPredefinedNodeType("mix:referenceable", false); + } + + /** Test for the predefined mix:referenceable node type. */ + public void testShareable() throws NotExecutableException { + testPredefinedNodeType("mix:shareable", false); + } + + /** Test for the predefined mix:versionable node type. */ + public void testVersionable() throws NotExecutableException { + testPredefinedNodeType("mix:versionable", false); + } + + /** Test for the predefined mix:simpleVersionable node type. */ + public void testSimpleVersionable() throws NotExecutableException { + testPredefinedNodeType("mix:simpleVersionable", false); + } + + /** Test for the predefined mix:created node type. */ + public void testMixCreated() throws NotExecutableException { + testPredefinedNodeType("mix:created", true); + } + + /** Test for the predefined mix:lastModified node type. */ + public void testMixLastModified() throws NotExecutableException { + testPredefinedNodeType("mix:lastModified", true); + } + + /** Test for the predefined mix:etag node type. */ + public void testMixETag() throws NotExecutableException { + testPredefinedNodeType("mix:etag", false); + } + + /** Test for the predefined mix:title node type. */ + public void testMixTitle() throws NotExecutableException { + testPredefinedNodeType("mix:title", true); + } + + /** Test for the predefined mix:language node type. */ + public void testMixLanguage() throws NotExecutableException { + testPredefinedNodeType("mix:language", true); + } + + /** Test for the predefined mix:language node type. */ + public void testMixMimeType() throws NotExecutableException { + testPredefinedNodeType("mix:mimeType", true); + } + + /** Test for the predefined nt:address node type. */ + public void testNtAddress() throws NotExecutableException { + testPredefinedNodeType("nt:address", false); + } + + /** Test for the predefined nt:base node type. */ + public void testBase() throws NotExecutableException { + testPredefinedNodeType("nt:base", false); + } + + /** Test for the predefined nt:unstructured node type. */ + public void testUnstructured() throws NotExecutableException { + testPredefinedNodeType("nt:unstructured", false); + } + + /** Test for the predefined nt:hierarchyNode node type. */ + public void testHierarchyNode() throws NotExecutableException { + testPredefinedNodeType("nt:hierarchyNode", false); + } + + /** Test for the predefined nt:file node type. */ + public void testFile() throws NotExecutableException { + testPredefinedNodeType("nt:file", false); + } + + /** Test for the predefined nt:linkedFile node type. */ + public void testLinkedFile() throws NotExecutableException { + testPredefinedNodeType("nt:linkedFile", false); + } + + /** Test for the predefined nt:folder node type. */ + public void testFolder() throws NotExecutableException { + testPredefinedNodeType("nt:folder", false); + } + + /** Test for the predefined nt:nodeType node type. */ + public void testNodeType() throws NotExecutableException { + testPredefinedNodeType("nt:nodeType", false); + } + + /** Test for the predefined nt:propertyDef node type. */ + public void testPropertyDef() throws NotExecutableException { + testPredefinedNodeType("nt:propertyDefinition", false); + } + + /** Test for the predefined nt:childNodeDef node type. */ + public void testChildNodeDef() throws NotExecutableException { + testPredefinedNodeType("nt:childNodeDefinition", false); + } + + /** Test for the predefined nt:versionHistory node type. */ + public void testVersionHistory() throws NotExecutableException { + testPredefinedNodeType("nt:versionHistory", false); + } + + /** Test for the predefined nt:versionLabels node type. */ + public void testVersionLabels() throws NotExecutableException { + testPredefinedNodeType("nt:versionLabels", false); + } + + /** Test for the predefined nt:version node type. */ + public void testVersion() throws NotExecutableException { + testPredefinedNodeType("nt:version", false); + } + + /** Test for the predefined nt:activity node type. */ + public void testActivity() throws NotExecutableException { + testPredefinedNodeType("nt:activity", false); + } + + /** Test for the predefined nt:configuration node type. */ + public void testConfiguration() throws NotExecutableException { + testPredefinedNodeType("nt:configuration", false); + } + + /** Test for the predefined nt:frozenNode node type. */ + public void testFrozenNode() throws NotExecutableException { + testPredefinedNodeType("nt:frozenNode", false); + } + + /** Test for the predefined nt:versionedChild node type. */ + public void testVersionedChild() throws NotExecutableException { + testPredefinedNodeType("nt:versionedChild", false); + } + + /** Test for the predefined nt:query node type. */ + public void testQuery() throws NotExecutableException { + testPredefinedNodeType("nt:query", false); + } + + /** Test for the predefined nt:resource node type. */ + public void testResource() throws NotExecutableException { + testPredefinedNodeType("nt:resource", false); + } + + /** + * Tests that the named node type matches the JSR 170 specification. + * The test is performed by genererating a node type definition spec + * string in the format used by the JSR 170 specification, and comparing + * the result with a static spec file extracted from the specification + * itself. + *

        + * Note that the extracted spec files are not exact copies of the node + * type specification in the JSR 170 document. Some formatting and + * ordering changes have been made to simplify the test code, but the + * semantics remain the same. + * + * @param name node type name + * @param propsVariant whether the properties of this node type may + * have implementation variant autocreated and OPV flags. + * @throws NotExecutableException if the node type is not supported by + * this repository implementation. + */ + private void testPredefinedNodeType(String name, boolean propsVariant) + throws NotExecutableException { + try { + StringBuffer spec = new StringBuffer(); + String resource = + "org/apache/jackrabbit/test/api/nodetype/spec/" + + name.replace(':', '-') + ".txt"; + Reader reader = new InputStreamReader( + getClass().getClassLoader().getResourceAsStream(resource)); + for (int ch = reader.read(); ch != -1; ch = reader.read()) { + spec.append((char) ch); + } + + NodeType type = manager.getNodeType(name); + String current = getNodeTypeSpec(type, propsVariant).trim(); + if (!System.getProperty("line.separator").equals("\n")) { + current = normalizeLineSeparators(current); + } + String expected = normalizeLineSeparators(spec.toString()).trim(); + + assertEquals("Predefined node type " + name, expected, current); + + // check minimum declared supertypes + Set declaredSupertypes = new HashSet(); + for (Iterator it = Arrays.asList( + type.getDeclaredSupertypes()).iterator(); it.hasNext(); ) { + NodeType nt = it.next(); + declaredSupertypes.add(nt.getName()); + } + for (Iterator it = Arrays.asList( + SUPERTYPES.get(name)).iterator(); it.hasNext(); ) { + String supertype = it.next(); + assertTrue("Predefined node type " + name + " does not " + + "declare supertype " + supertype, + declaredSupertypes.contains(supertype)); + } + } catch (IOException e) { + fail(e.getMessage()); + } catch (NoSuchNodeTypeException e) { + // only nt:base is strictly required + if ("nt:base".equals(name)) { + fail(e.getMessage()); + } else { + throw new NotExecutableException("NodeType " + name + + " not supported by this repository implementation."); + } + } catch (RepositoryException e) { + fail(e.getMessage()); + } + } + + /** + * Creates and returns a spec string for the given node type definition. + * The returned spec string follows the node type definition format + * used in the JSR 170 specification. + * + * @param type node type definition + * @param propsVariant whether the properties of this node type may + * have implementation variant autocreated and OPV flags. + * @return spec string + * @throws RepositoryException on repository errors + */ + private static String getNodeTypeSpec(NodeType type, boolean propsVariant) + throws RepositoryException { + String typeName = type.getName(); + StringWriter buffer = new StringWriter(); + + PrintWriter writer = new PrintWriter(buffer); + writer.println("NodeTypeName"); + writer.println(" " + typeName); + writer.println("IsMixin"); + writer.println(" " + type.isMixin()); + writer.println("HasOrderableChildNodes"); + writer.println(" " + type.hasOrderableChildNodes()); + writer.println("PrimaryItemName"); + writer.println(" " + type.getPrimaryItemName()); + NodeDefinition[] nodes = type.getDeclaredChildNodeDefinitions(); + Arrays.sort(nodes, NODE_DEF_COMPARATOR); + for (int i = 0; i < nodes.length; i++) { + writer.print(getChildNodeDefSpec(nodes[i])); + } + PropertyDefinition[] properties = type.getDeclaredPropertyDefinitions(); + Arrays.sort(properties, PROPERTY_DEF_COMPARATOR); + for (int i = 0; i < properties.length; i++) { + writer.print(getPropertyDefSpec(properties[i], propsVariant)); + } + + return buffer.toString(); + } + + /** + * Creates and returns a spec string for the given node definition. + * The returned spec string follows the child node definition format + * used in the JSR 170 specification. + * + * @param node child node definition + * @return spec string + */ + private static String getChildNodeDefSpec(NodeDefinition node) { + StringWriter buffer = new StringWriter(); + + PrintWriter writer = new PrintWriter(buffer); + writer.println("ChildNodeDefinition"); + if (node.getName().equals("*")) { + writer.println(" Name \"*\""); + } else { + writer.println(" Name " + node.getName()); + } + writer.print(" RequiredPrimaryTypes ["); + NodeType[] types = node.getRequiredPrimaryTypes(); + Arrays.sort(types, NODE_TYPE_COMPARATOR); + for (int j = 0; j < types.length; j++) { + if (j > 0) { + writer.print(','); + } + writer.print(types[j].getName()); + } + writer.println("]"); + if (node.getDefaultPrimaryType() != null) { + writer.println(" DefaultPrimaryType " + + node.getDefaultPrimaryType().getName()); + } else { + writer.println(" DefaultPrimaryType null"); + } + writer.println(" AutoCreated " + node.isAutoCreated()); + writer.println(" Mandatory " + node.isMandatory()); + writer.println(" OnParentVersion " + + OnParentVersionAction.nameFromValue(node.getOnParentVersion())); + writer.println(" Protected " + node.isProtected()); + writer.println(" SameNameSiblings " + node.allowsSameNameSiblings()); + + return buffer.toString(); + } + + /** + * Creates and returns a spec string for the given property definition. + * The returned spec string follows the property definition format + * used in the JSR 170 specification. + * + * @param property property definition + * @param propsVariant whether the properties of this node type may + * have implementation variant autocreated and OPV flags. + * @return spec string + * @throws RepositoryException on repository errors + */ + private static String getPropertyDefSpec(PropertyDefinition property, + boolean propsVariant) + throws RepositoryException { + StringWriter buffer = new StringWriter(); + + PrintWriter writer = new PrintWriter(buffer); + writer.println("PropertyDefinition"); + if (property.getName().equals("*")) { + writer.println(" Name \"*\""); + } else { + writer.println(" Name " + property.getName()); + } + String type = PropertyType.nameFromValue(property.getRequiredType()); + writer.println(" RequiredType " + type.toUpperCase()); + Value[] values = property.getDefaultValues(); + if (values != null && values.length > 0) { + writer.print(" DefaultValues ["); + for (int j = 0; j < values.length; j++) { + if (j > 0) { + writer.print(','); + } + writer.print(values[j].getString()); + } + writer.println("]"); + } else { + writer.println(" DefaultValues null"); + } + if (!propsVariant) { + writer.println(" AutoCreated " + property.isAutoCreated()); + } + writer.println(" Mandatory " + property.isMandatory()); + String action = OnParentVersionAction.nameFromValue( + property.getOnParentVersion()); + if (!propsVariant) { + writer.println(" OnParentVersion " + action); + } + writer.println(" Protected " + property.isProtected()); + writer.println(" Multiple " + property.isMultiple()); + + return buffer.toString(); + } + + /** + * Replaces platform-dependant line-separators in stringValue + * with "\n". + * + * @param stringValue string to normalize + * @return the normalized string + */ + private String normalizeLineSeparators(String stringValue) { + // Replace "\r\n" (Windows format) with "\n" (Unix format) + stringValue = stringValue.replaceAll("\r\n", "\n"); + // Replace "\r" (Mac format) with "\n" (Unix format) + stringValue = stringValue.replaceAll("\r", "\n"); + + return stringValue; + } + + /** + * Comparator for ordering node definition arrays. Node definitions are + * ordered by name, with the wildcard item definition ("*") ordered last. + */ + private static final Comparator NODE_DEF_COMPARATOR = new Comparator() { + public int compare(NodeDefinition nda, NodeDefinition ndb) { + if (nda.getName().equals("*") && !ndb.getName().equals("*")) { + return 1; + } else if (!nda.getName().equals("*") && ndb.getName().equals("*")) { + return -1; + } else { + return nda.getName().compareTo(ndb.getName()); + } + } + }; + + /** + * Comparator for ordering property definition arrays. Property definitions + * are ordered by name, with the wildcard item definition ("*") ordered + * last, and isMultiple flag, with isMultiple==true ordered last. + */ + private static final Comparator PROPERTY_DEF_COMPARATOR = new Comparator() { + public int compare(PropertyDefinition pda, PropertyDefinition pdb) { + if (pda.getName().equals("*") && !pdb.getName().equals("*")) { + return 1; + } else if (!pda.getName().equals("*") && pdb.getName().equals("*")) { + return -1; + } + int result = pda.getName().compareTo(pdb.getName()); + if (result != 0) { + return result; + } + if (pda.isMultiple() && !pdb.isMultiple()) { + return 1; + } else if (!pda.isMultiple() && pdb.isMultiple()) { + return -1; + } else { + return 0; + } + } + }; + + /** + * Comparator for ordering node type arrays. Node types are ordered by + * name, with all primary node types ordered before mixin node types. + */ + private static final Comparator NODE_TYPE_COMPARATOR = new Comparator() { + public int compare(NodeType nta, NodeType ntb) { + if (nta.isMixin() && !ntb.isMixin()) { + return 1; + } else if (!nta.isMixin() && ntb.isMixin()) { + return -1; + } else { + return nta.getName().compareTo(ntb.getName()); + } + } + }; + +} diff --git a/src/test/org/apache/jackrabbit/test/api/nodetype/PropertyDefTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/PropertyDefTest.java similarity index 85% rename from src/test/org/apache/jackrabbit/test/api/nodetype/PropertyDefTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/PropertyDefTest.java index cf7cee9bd7a..273fb5ce826 100644 --- a/src/test/org/apache/jackrabbit/test/api/nodetype/PropertyDefTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/PropertyDefTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -30,17 +30,13 @@ import javax.jcr.nodetype.NodeTypeManager; import javax.jcr.nodetype.NodeTypeIterator; import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.PropertyDef; +import javax.jcr.nodetype.PropertyDefinition; import java.util.regex.Pattern; import java.util.regex.Matcher; /** * Tests if property definitions are properly defined. * - * @test - * @sources PropertyDefTest.java - * @executeClass org.apache.jackrabbit.test.api.nodetype.PropertyDefTest - * @keywords level1 */ public class PropertyDefTest extends AbstractJCRTest { @@ -75,11 +71,6 @@ public class PropertyDefTest extends AbstractJCRTest { */ private NodeTypeManager manager; - /** - * The root node of the default workspace - */ - private Node rootNode; - /** * If true indicates that the test found a mandatory property */ @@ -92,9 +83,10 @@ protected void setUp() throws Exception { isReadOnly = true; super.setUp(); - session = helper.getReadOnlySession(); + session = getHelper().getReadOnlySession(); manager = session.getWorkspace().getNodeTypeManager(); - rootNode = session.getRootNode(); + // re-fetch testRootNode with read-only session + testRootNode = (Node) session.getItem(testRoot); } /** @@ -103,7 +95,9 @@ protected void setUp() throws Exception { protected void tearDown() throws Exception { if (session != null) { session.logout(); + session = null; } + manager = null; super.tearDown(); } @@ -117,17 +111,17 @@ public void testGetDeclaringNodeType() throws RepositoryException { // loop all node types while (types.hasNext()) { NodeType currentType = types.nextNodeType(); - PropertyDef defsOfCurrentType[] = - currentType.getPropertyDefs(); + PropertyDefinition defsOfCurrentType[] = + currentType.getPropertyDefinitions(); // loop all property defs of each node type for (int i = 0; i < defsOfCurrentType.length; i++) { - PropertyDef def = defsOfCurrentType[i]; + PropertyDefinition def = defsOfCurrentType[i]; NodeType type = def.getDeclaringNodeType(); // check if def is part of the property defs of the // declaring node type - PropertyDef defs[] = type.getPropertyDefs(); + PropertyDefinition defs[] = type.getPropertyDefinitions(); boolean hasType = false; for (int j = 0; j < defs.length; j++) { if (defs[j].getName().equals(def.getName())) { @@ -152,9 +146,9 @@ public void testIsAutoCreate() throws RepositoryException { // loop all node types while (types.hasNext()) { NodeType type = types.nextNodeType(); - PropertyDef defs[] = type.getPropertyDefs(); + PropertyDefinition defs[] = type.getPropertyDefinitions(); for (int i = 0; i < defs.length; i++) { - if (defs[i].isAutoCreate()) { + if (defs[i].isAutoCreated()) { assertFalse("An auto create property must not be a " + "residual set definition.", defs[i].getName().equals("*")); @@ -166,21 +160,22 @@ public void testIsAutoCreate() throws RepositoryException { /** * This test checks if item definitions with mandatory constraints are * respected. - *

        + *

        * If the default workspace does not contain a node with a node type * definition that specifies a mandatory property a {@link * org.apache.jackrabbit.test.NotExecutableException} is thrown. */ public void testIsMandatory() throws RepositoryException, NotExecutableException { - traverse(rootNode); + traverse(testRootNode); if (!foundMandatoryProperty) { throw new NotExecutableException("Workspace does not contain any node with a mandatory property definition"); } } /** - * Tests if isRequiredType() returns a valid PropertyType.

        The test - * runs for all available node types. + * Tests if isRequiredType() returns a valid PropertyType. + *

        + * The test runs for all available node types. */ public void testIsRequiredType() throws RepositoryException { @@ -189,7 +184,7 @@ public void testIsRequiredType() // loop all node types while (types.hasNext()) { NodeType type = types.nextNodeType(); - PropertyDef defs[] = type.getPropertyDefs(); + PropertyDefinition defs[] = type.getPropertyDefinitions(); for (int i = 0; i < defs.length; i++) { switch (defs[i].getRequiredType()) { case PropertyType.STRING: @@ -202,6 +197,9 @@ public void testIsRequiredType() case PropertyType.REFERENCE: case PropertyType.BOOLEAN: case PropertyType.UNDEFINED: + case PropertyType.WEAKREFERENCE: + case PropertyType.DECIMAL: + case PropertyType.URI: // success break; default: @@ -214,8 +212,10 @@ public void testIsRequiredType() /** * Tests if value constraints match the pattern specified by the required - * property type.

        The test runs for all value constraints of all - * properties of all available node types. + * property type. + *

        + * The test runs for all value constraints of all properties of all + * available node types. */ public void testGetValueConstraints() throws RepositoryException { @@ -223,9 +223,9 @@ public void testGetValueConstraints() throws RepositoryException { // loop all node types while (types.hasNext()) { NodeType type = types.nextNodeType(); - PropertyDef defs[] = type.getPropertyDefs(); + PropertyDefinition defs[] = type.getPropertyDefinitions(); for (int i = 0; i < defs.length; i++) { - PropertyDef def = defs[i]; + PropertyDefinition def = defs[i]; String constraints[] = def.getValueConstraints(); if (constraints != null) { @@ -317,9 +317,11 @@ public void testGetValueConstraints() throws RepositoryException { } /** - * Tests if single-valued properties do have not more than one default value - *

        The test runs for all default values of all properties of all - * available node types. + * Tests if single-valued properties do have not more than one default + * value. + *

        + * The test runs for all default values of all properties of all available + * node types. */ public void testGetDefaultValues() throws RepositoryException { @@ -328,9 +330,9 @@ public void testGetDefaultValues() // loop all node types while (types.hasNext()) { NodeType type = types.nextNodeType(); - PropertyDef defs[] = type.getPropertyDefs(); + PropertyDefinition defs[] = type.getPropertyDefinitions(); for (int i = 0; i < defs.length; i++) { - PropertyDef def = defs[i]; + PropertyDefinition def = defs[i]; Value values[] = def.getDefaultValues(); if (values != null) { @@ -338,8 +340,10 @@ public void testGetDefaultValues() for (int j = 0; j < values.length; j++) { if (!def.isMultiple()) { - assertEquals("Single-valued properties must not " + - "have more than one default value.", + assertEquals( + "Single-valued property " + + type.getName() +"/" + def.getName() + + " must not have more than one default value.", 1, values.length); } } @@ -381,15 +385,16 @@ private void checkMandatoryConstraint(Node node, NodeType type) throws RepositoryException { // test if node contains all mandatory properties of current type - PropertyDef propDefs[] = type.getPropertyDefs(); + PropertyDefinition propDefs[] = type.getPropertyDefinitions(); for (int i = 0; i < propDefs.length; i++) { - PropertyDef propDef = propDefs[i]; + PropertyDefinition propDef = propDefs[i]; if (propDef.isMandatory()) { foundMandatoryProperty = true; String name = propDef.getName(); - assertTrue("Node instance does not contain value for mandatory property.", node.hasProperty(name)); + assertTrue("Node " + node.getPath() + " does not contain " + + "value for mandatory property: " + name, node.hasProperty(name)); // todo check back with latest spec! /* try { diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/TestAll.java new file mode 100644 index 00000000000..d27c780e98f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/nodetype/TestAll.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.test.api.nodetype; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the package + * javax.jcr.nodetype. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("javax.jcr.nodetype tests"); + + // ADD TEST CLASSES HERE: + suite.addTestSuite(NodeDefTest.class); + suite.addTestSuite(NodeTypeManagerTest.class); + suite.addTestSuite(NodeTypeTest.class); + suite.addTestSuite(PropertyDefTest.class); + + suite.addTestSuite(PredefinedNodeTypeTest.class); + + suite.addTestSuite(CanSetPropertyBinaryTest.class); + suite.addTestSuite(CanSetPropertyBooleanTest.class); + suite.addTestSuite(CanSetPropertyDateTest.class); + suite.addTestSuite(CanSetPropertyDoubleTest.class); + suite.addTestSuite(CanSetPropertyLongTest.class); + suite.addTestSuite(CanSetPropertyMultipleTest.class); + suite.addTestSuite(CanSetPropertyNameTest.class); + suite.addTestSuite(CanSetPropertyPathTest.class); + suite.addTestSuite(CanSetPropertyStringTest.class); + suite.addTestSuite(CanSetPropertyTest.class); + + suite.addTestSuite(CanAddChildNodeCallWithNodeTypeTest.class); + suite.addTestSuite(CanAddChildNodeCallWithoutNodeTypeTest.class); + + suite.addTestSuite(CanRemoveItemTest.class); + + // JCR 2.0 + + suite.addTestSuite(NodeTypeCreationTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/AbstractObservationTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/AbstractObservationTest.java new file mode 100644 index 00000000000..2c88e700c92 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/AbstractObservationTest.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.observation; + +import java.util.ArrayList; +import java.util.Arrays; +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 javax.jcr.observation.Event; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.EventListenerIterator; +import javax.jcr.observation.ObservationManager; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * This class implements the basic {@link #setUp} and {@link #tearDown()} + * methods for the observation test cases. + */ +public abstract class AbstractObservationTest extends AbstractJCRTest { + + /** + * Default wait timeout for events: 5000 ms + */ + protected static final long DEFAULT_WAIT_TIMEOUT = 5000; + + + protected static final int ALL_TYPES = Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED | javax.jcr.observation.Event.NODE_MOVED; + + /** + * The ObservationManager + */ + protected ObservationManager obsMgr; + + protected void setUp() throws Exception { + super.setUp(); + try { + obsMgr = superuser.getWorkspace().getObservationManager(); + } + catch (UnsupportedRepositoryOperationException ex) { + throw new NotExecutableException("observation not supported"); + } + } + + protected void tearDown() throws Exception { + obsMgr = null; + super.tearDown(); + } + + /** + * Registers an EventListener for all events. + * + * @param listener the EventListener. + * @throws RepositoryException if registration fails. + */ + protected void addEventListener(EventListener listener) throws RepositoryException { + addEventListener(listener, + Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED); + } + + /** + * Registers an EventListener for events of the specified + * type(s). + * + * @param listener the EventListener. + * @param eventType the event types + * @throws RepositoryException if registration fails. + */ + protected void addEventListener(EventListener listener, int eventType) + throws RepositoryException { + if (obsMgr != null) { + obsMgr.addEventListener(listener, + eventType, + superuser.getRootNode().getPath(), + true, + null, + null, + false); + } else { + throw new IllegalStateException("ObservationManager not available."); + } + } + + /** + * Removes the EventListener from the ObservationManager. + * + * @param listener the EventListener to unregister. + * @throws RepositoryException if unregister fails. + */ + protected void removeEventListener(EventListener listener) throws RepositoryException { + if (obsMgr != null) { + obsMgr.removeEventListener(listener); + } else { + throw new IllegalStateException("ObservationManager not available."); + } + } + + /** + * Consumes the EventListenerIterator and returns the + * EventListener as an array. + * @param it the iterator. + * @return array of EventListeners. + */ + protected EventListener[] toArray(EventListenerIterator it) { + List listeners = new ArrayList(); + while (it.hasNext()) { + listeners.add(it.nextEventListener()); + } + return listeners.toArray(new EventListener[listeners.size()]); + } + + //--------------------< check methods >------------------------------------- + + /** + * Checks Events for paths. All relPaths are + * relative to {@link #testRoot}. + * + * @param events the Events. + * @param requiredRelPaths paths to child nodes added relative to {@link + * #testRoot} (required events). + * @param optionalRelPaths paths to child nodes added relative to {@link + * #testRoot} (optional events). + * @throws RepositoryException if an error occurs while retrieving the nodes + * from event instances. + */ + protected void checkNodeAdded(Event[] events, String[] requiredRelPaths, String[] optionalRelPaths) + throws RepositoryException { + checkNodes(events, requiredRelPaths, optionalRelPaths, Event.NODE_ADDED); + } + + /** + * Checks Events for paths. All relPaths are + * relative to {@link #testRoot}. + * + * @param events the Events. + * @param requiredRelPaths paths to child nodes added relative to {@link + * #testRoot} (required events). + * @param optionalRelPaths paths to child nodes added relative to {@link + * #testRoot} (optional events). + * @throws RepositoryException if an error occurs while retrieving the nodes + * from event instances. + */ + protected void checkNodeRemoved(Event[] events, String[] requiredRelPaths, String[] optionalRelPaths) + throws RepositoryException { + checkNodes(events, requiredRelPaths, optionalRelPaths, Event.NODE_REMOVED); + } + + /** + * Checks Events for paths. All relPaths are + * relative to {@link #testRoot}. + * + * @param events the Events. + * @param relPaths paths to added properties relative to {@link + * #testRoot}. + * @throws RepositoryException if an error occurs while retrieving the nodes + * from event instances. + */ + protected void checkPropertyAdded(Event[] events, String[] relPaths) + throws RepositoryException { + checkNodes(events, relPaths, null, Event.PROPERTY_ADDED); + } + + /** + * Checks Events for paths. All relPaths are + * relative to {@link #testRoot}. + * + * @param events the Events. + * @param relPaths paths to changed properties relative to {@link + * #testRoot}. + * @throws RepositoryException if an error occurs while retrieving the nodes + * from event instances. + */ + protected void checkPropertyChanged(Event[] events, String[] relPaths) + throws RepositoryException { + checkNodes(events, relPaths, null, Event.PROPERTY_CHANGED); + } + + /** + * Checks Events for paths. All relPaths are + * relative to {@link #testRoot}. + * + * @param events the Events. + * @param relPaths paths to removed properties relative to {@link + * #testRoot}. + * @throws RepositoryException if an error occurs while retrieving the nodes + * from event instances. + */ + protected void checkPropertyRemoved(Event[] events, String[] relPaths) + throws RepositoryException { + checkNodes(events, relPaths, null, Event.PROPERTY_REMOVED); + } + + /** + * Checks Events for paths. All relPaths are + * relative to {@link #testRoot}. + * + * @param events the Events. + * @param requiredRelPaths paths to required item events relative to {@link #testRoot}. + * @param optionalRelPaths paths to optional item events relative to {@link #testRoot}. + * @param eventType the type of event to check. + * @throws RepositoryException if an error occurs while retrieving the nodes + * from event instances. + */ + protected void checkNodes(Event[] events, String[] requiredRelPaths, String[] optionalRelPaths, long eventType) + throws RepositoryException { + Set paths = new HashSet(); + for (int i = 0; i < events.length; i++) { + assertEquals("Wrong event type", eventType, events[i].getType()); + String path = events[i].getPath(); + paths.add(path); + } + // check all required paths are there + for (int i = 0; i < requiredRelPaths.length; i++) { + String expected = testRoot + "/" + requiredRelPaths[i]; + assertTrue("Path " + expected + " not found in events.", + paths.contains(expected)); + paths.remove(expected); + } + // check what remains in the set is indeed optional + Set optional = new HashSet(); + if (optionalRelPaths != null) { + for (int i = 0; i < optionalRelPaths.length; i++) { + optional.add(testRoot + "/" + optionalRelPaths[i]); + } + } + for (Iterator it = paths.iterator(); it.hasNext(); ) { + String path = it.next(); + assertTrue("Path " + path + " not expected in events.", + optional.contains(path)); + } + } + + + /** + * Registers an event listener for the passed eventTypes and + * calls the callable. + * + * @param call the callable. + * @param eventTypes the types of the events to listen for. + * @return the events that were generated during execution of the callable. + * @throws RepositoryException if an error occurs. + */ + protected Event[] getEvents(Callable call, int eventTypes) + throws RepositoryException { + EventResult result = new EventResult(log); + addEventListener(result, eventTypes); + call.call(); + Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); + return events; + } + + /** + * Returns the first event with the given path. + * + * @param events the events. + * @param path the path. + * @return the event with the given path or {@link #fail()}s if + * no such event exists. + * @throws RepositoryException if an error occurs while reading from the + * repository. + */ + protected Event getEventByPath(Event[] events, String path) + throws RepositoryException { + for (int i = 0; i < events.length; i++) { + if (events[i].getPath().equals(path)) { + return events[i]; + } + } + fail("no event with path: " + path + " in " + Arrays.asList(events)); + return null; + } + + /** + * Helper interface. + */ + protected interface Callable { + public void call() throws RepositoryException; + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/observation/AddEventListenerTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/AddEventListenerTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/observation/AddEventListenerTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/AddEventListenerTest.java index 80e7b0c60a3..d03d631228b 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/AddEventListenerTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/AddEventListenerTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -25,22 +25,19 @@ /** * Tests the options for addEventListener(). - *

        - * Configuration requirements are:
        + *

        + * Configuration requirements: + *

        * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. * The child nodes that are created will be named {@link #nodeName1} and * {@link #nodeName2}. Furthermore {@link #testNodeType} must allow to add * child nodes of the same type ({@link #testNodeType}). - *

        + *

        * Certain test require that {@link #testNodeType} is mix:referenceable or * allows to add that mixin. If the repository does not support mix:referenceable * a {@link org.apache.jackrabbit.test.NotExecutableException} is thrown * in those test cases. * - * @test - * @sources AddEventListenerTest.java - * @executeClass org.apache.jackrabbit.test.api.observation.AddEventListenerTest - * @keywords observation */ public class AddEventListenerTest extends AbstractObservationTest { @@ -51,11 +48,11 @@ public void testPath() throws RepositoryException { EventResult listener = new EventResult(log); obsMgr.addEventListener(listener, Event.NODE_ADDED, testRoot + "/" + nodeName1, true, null, null, false); Node n = testRootNode.addNode(nodeName1, testNodeType); - n.addNode(nodeName2); - testRootNode.save(); - obsMgr.removeEventListener(listener); + n.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); Event[] events = listener.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(events, new String[]{nodeName1, nodeName1 + "/" + nodeName2}); + obsMgr.removeEventListener(listener); + checkNodeAdded(events, new String[]{nodeName1 + "/" + nodeName2}, null); } /** @@ -66,11 +63,11 @@ public void testIsDeepFalseNodeAdded() throws RepositoryException { EventResult listener = new EventResult(log); obsMgr.addEventListener(listener, Event.NODE_ADDED, testRoot + "/" + nodeName1, false, null, null, false); Node n = testRootNode.addNode(nodeName1, testNodeType); - n.addNode(nodeName2); - testRootNode.save(); - obsMgr.removeEventListener(listener); + n.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); Event[] events = listener.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(events, new String[]{nodeName1}); + obsMgr.removeEventListener(listener); + checkNodeAdded(events, new String[]{nodeName1 + "/" + nodeName2}, null); } /** @@ -80,14 +77,14 @@ public void testIsDeepFalseNodeAdded() throws RepositoryException { public void testIsDeepFalsePropertyAdded() throws RepositoryException { Node n1 = testRootNode.addNode(nodeName1, testNodeType); Node n2 = testRootNode.addNode(nodeName2, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); EventResult listener = new EventResult(log); obsMgr.addEventListener(listener, Event.PROPERTY_ADDED, testRoot + "/" + nodeName1, false, null, null, false); n1.setProperty(propertyName1, "foo"); n2.setProperty(propertyName1, "foo"); - testRootNode.save(); - obsMgr.removeEventListener(listener); + testRootNode.getSession().save(); Event[] events = listener.getEvents(DEFAULT_WAIT_TIMEOUT); + obsMgr.removeEventListener(listener); checkPropertyAdded(events, new String[]{nodeName1 + "/" + propertyName1}); } @@ -106,9 +103,10 @@ public void testNoLocalTrue() throws RepositoryException { true); // noLocal testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); + // only wait one second because we don't expect any event + Event[] events = listener.getEvents(1000); obsMgr.removeEventListener(listener); - Event[] events = listener.getEvents(DEFAULT_WAIT_TIMEOUT); assertEquals("EventListener must not receive own modification when noLocal=true", 0, events.length); } @@ -124,20 +122,20 @@ public void testUUID() throws RepositoryException, NotExecutableException { } catch (RepositoryException e) { throw new NotExecutableException("Repository does not support mix:referenceable"); } - testRootNode.save(); + testRootNode.getSession().save(); EventResult listener = new EventResult(log); obsMgr.addEventListener(listener, Event.PROPERTY_ADDED, testRoot, true, - new String[]{n1.getUUID()}, + new String[]{n1.getIdentifier()}, null, false); n1.setProperty(propertyName1, "foo"); n2.setProperty(propertyName1, "foo"); - testRootNode.save(); - obsMgr.removeEventListener(listener); + testRootNode.getSession().save(); Event[] events = listener.getEvents(DEFAULT_WAIT_TIMEOUT); + obsMgr.removeEventListener(listener); checkPropertyAdded(events, new String[]{nodeName1 + "/" + propertyName1}); } @@ -149,7 +147,7 @@ public void testNodeType() throws RepositoryException { EventResult listener = new EventResult(log); Node n1 = testRootNode.addNode(nodeName1, testNodeType); Node n2 = testRootNode.addNode(nodeName2, nodetype2); - testRootNode.save(); + testRootNode.getSession().save(); obsMgr.addEventListener(listener, Event.NODE_ADDED, testRoot, @@ -157,10 +155,10 @@ public void testNodeType() throws RepositoryException { null, new String[]{testNodeType}, false); - Session s = helper.getSuperuserSession(); + Session s = getHelper().getSuperuserSession(); try { Node n = (Node) s.getItem(n1.getPath()); - n.addNode(nodeName3, ntBase); + n.addNode(nodeName3, testNodeType); n = (Node) s.getItem(n2.getPath()); n.addNode(nodeName3, nodetype2); n = (Node) s.getItem(testRoot); @@ -168,9 +166,9 @@ public void testNodeType() throws RepositoryException { } finally { s.logout(); } - obsMgr.removeEventListener(listener); Event[] events = listener.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(events, new String[]{nodeName1 + "/" + nodeName3}); + obsMgr.removeEventListener(listener); + checkNodeAdded(events, new String[]{nodeName1 + "/" + nodeName3}, null); } //-------------------------< internal >------------------------------------- @@ -187,11 +185,10 @@ public void testNodeType() throws RepositoryException { * @throws RepositoryException if node creation fails. */ private Node createReferenceable(String nodeName, String nodeType) - throws RepositoryException { + throws RepositoryException, NotExecutableException { Node n = testRootNode.addNode(nodeName, nodeType); - if (!n.isNodeType(mixReferenceable)) { - n.addMixin(mixReferenceable); - } + ensureMixinType(n, mixReferenceable); + testRootNode.getSession().save(); return n; } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventIteratorTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventIteratorTest.java new file mode 100644 index 00000000000..edf141dcf86 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventIteratorTest.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.test.api.observation; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.RepositoryException; +import java.util.NoSuchElementException; + +/** + * Tests the methods the following methods: + *

          + *
        • {@link javax.jcr.observation.EventIterator#getSize()}
        • + *
        • {@link javax.jcr.observation.EventIterator#getPosition()}
        • + *
        • {@link javax.jcr.observation.EventIterator#skip(long)}
        • + *
        + *

        + * Configuration requirements: + *

        + * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. + * The child nodes that are created will be named {@link #nodeName1}, + * {@link #nodeName2} and {@link #nodeName3}. + * + */ +public class EventIteratorTest extends AbstractObservationTest{ + + /** + * Tests if getSize() returns the correct number of events. If getSize() + * returns -1 a {@link org.apache.jackrabbit.test.NotExecutableException} + * is thrown. + */ + public void testGetSize() throws RepositoryException, NotExecutableException { + EventResult listener = new EventResult(log); + addEventListener(listener, Event.NODE_ADDED); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + EventIterator events = listener.getEventIterator(DEFAULT_WAIT_TIMEOUT); + removeEventListener(listener); + assertNotNull("No events delivered within " + DEFAULT_WAIT_TIMEOUT + "ms.", events); + long size = events.getSize(); + if (size == -1) { + throw new NotExecutableException("EventIterator.getSize() returns unavailable size."); + } + assertEquals("Wrong number of events", 1, size); + } + + /** + * Tests if getPosition() returns the correct values. + */ + public void testGetPosition() throws RepositoryException { + EventResult listener = new EventResult(log); + addEventListener(listener, Event.NODE_ADDED); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + EventIterator events = listener.getEventIterator(DEFAULT_WAIT_TIMEOUT); + removeEventListener(listener); + assertNotNull("No events delivered within " + DEFAULT_WAIT_TIMEOUT + "ms.", events); + assertEquals("Initial call to getPosition() must return 0.", 0, events.getPosition()); + events.nextEvent(); + assertEquals("Wrong value for getPosition()", 1, events.getPosition()); + events.nextEvent(); + assertEquals("Wrong value for getPosition()", 2, events.getPosition()); + events.nextEvent(); + assertEquals("Wrong value for getPosition()", 3, events.getPosition()); + } + + /** + * Tests the method skip() + */ + public void testSkip() throws RepositoryException { + EventResult listener = new EventResult(log); + addEventListener(listener, Event.NODE_ADDED); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + EventIterator events = listener.getEventIterator(DEFAULT_WAIT_TIMEOUT); + removeEventListener(listener); + assertNotNull("No events delivered within " + DEFAULT_WAIT_TIMEOUT + "ms.", events); + // skip zero elements + events.skip(0); + assertEquals("getPosition() for first element must return 0.", 0, events.getPosition()); + // skip one element + events.skip(2); + assertEquals("Wrong value for getPosition()", 2, events.getPosition()); + // skip past end + try { + events.skip(2); + fail("EventIterator must throw NoSuchElementException when skipping past the end"); + } catch (NoSuchElementException e) { + // success + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventJournalTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventJournalTest.java new file mode 100644 index 00000000000..71fca1347ea --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventJournalTest.java @@ -0,0 +1,274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.observation; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventJournal; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * EventJournalTest performs EventJournal tests. + */ +public class EventJournalTest extends AbstractObservationTest { + + private EventJournal journal; + + protected void setUp() throws Exception { + checkSupportedOption(Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED); + super.setUp(); + journal = obsMgr.getEventJournal(); + } + + public void testSkipToNow() throws RepositoryException { + // skip everything + skipToNow(); + assertFalse(journal.hasNext()); + } + + public void testSkipTo() throws Exception { + long time = System.currentTimeMillis(); + + // add some nodes + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = testRootNode.addNode(nodeName2); + + // make sure some time passed otherwise we might + // skip this change as well. + while (time == System.currentTimeMillis()) { + Thread.sleep(1); + } + + // now save + superuser.save(); + + journal.skipTo(time); + // at least the two added nodes must be returned by the journal + checkJournal(new String[]{n1.getPath(), n2.getPath()}, new String[0]); + } + + public void testLiveJournal() throws RepositoryException { + skipToNow(); + assertFalse(journal.hasNext()); + + testRootNode.addNode(nodeName1); + superuser.save(); + + assertTrue(journal.hasNext()); + } + + public void testWorkspaceSeparation() throws RepositoryException { + skipToNow(); + assertFalse(journal.hasNext()); + + Session session = getHelper().getSuperuserSession(workspaceName); + try { + Node rootNode = session.getRootNode(); + if (rootNode.hasNode(nodeName1)) { + rootNode.getNode(nodeName1).remove(); + } else { + rootNode.addNode(nodeName1); + } + session.save(); + } finally { + session.logout(); + } + + assertFalse(journal.hasNext()); + } + + public void testIsDeepTrue() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = n1.addNode(nodeName2); + + journal = obsMgr.getEventJournal(); + skipToNow(); + + superuser.save(); + + checkJournal(new String[]{n1.getPath(), n2.getPath()}, new String[0]); + } + + public void testUUID() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1); + ensureMixinType(n1, mixReferenceable); + superuser.save(); + + Node n2 = n1.addNode(nodeName2); + + journal = obsMgr.getEventJournal(); + skipToNow(); + + superuser.save(); + + checkJournal(new String[]{n2.getPath()}, new String[0]); + } + + public void testUserData() throws RepositoryException { + testRootNode.addNode(nodeName1); + String data = createRandomString(5); + obsMgr.setUserData(data); + + journal = obsMgr.getEventJournal(); + skipToNow(); + + superuser.save(); + + assertTrue("no more events", journal.hasNext()); + assertEquals("Wrong user data", data, journal.nextEvent().getUserData()); + } + + public void testEventType() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + + journal = getEventJournal(Event.PROPERTY_ADDED, testRoot, true, null, null); + skipToNow(); + + superuser.save(); + + checkJournal(new String[]{n1.getPath() + "/" + jcrPrimaryType}, + new String[]{n1.getPath()}); + } + + public void testPath() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = n1.addNode(nodeName2); + + journal = getEventJournal(ALL_TYPES, n1.getPath(), true, null, null); + skipToNow(); + superuser.save(); + + checkJournal(new String[]{n2.getPath()}, new String[]{n1.getPath()}); + } + + public void testIsDeepFalse() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1); + Node n2 = n1.addNode(nodeName2); + + journal = getEventJournal(ALL_TYPES, testRoot, false, null, null); + skipToNow(); + + superuser.save(); + + checkJournal(new String[]{n1.getPath()}, new String[]{n2.getPath()}); + } + + public void testNodeType() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, "nt:folder"); + Node n2 = n1.addNode(nodeName2, "nt:folder"); + + journal = getEventJournal(ALL_TYPES, testRoot, true, null, + new String[]{"nt:folder"}); + skipToNow(); + + superuser.save(); + + checkJournal(new String[]{n2.getPath()}, new String[]{n1.getPath()}); + } + + public void testPersist() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1); + + journal = getEventJournal(Event.PERSIST, testRoot, true, null, null); + skipToNow(); + + superuser.save(); + + boolean hasPersistEvents = journal.hasNext(); + if (! hasPersistEvents) { + throw new NotExecutableException("repository does not appear to provide PERSIST events"); + } + + journal = getEventJournal(ALL_TYPES | Event.PERSIST, testRoot, true, null, null); + skipToNow(); + + // add another child node + Node n3 = testRootNode.addNode(nodeName2); + String target = n1.getPath(); + n1.remove(); + superuser.save(); + + // move it + superuser.getWorkspace().move(n3.getPath(), target); + + // remove it again + n3 = superuser.getNode(target); + n3.remove(); + superuser.save(); + + int persistCount = 0; + Event e = null; + while (journal.hasNext()) { + e = journal.nextEvent(); + if (e.getType() == Event.PERSIST) { + persistCount += 1; + } + } + + assertEquals(3, persistCount); + + // last event should be persist + assertEquals(Event.PERSIST, e.getType()); + } + + //-------------------------------< internal >------------------------------- + + private void skipToNow() { + long now = System.currentTimeMillis(); + journal.skipTo(now); + while (now == System.currentTimeMillis()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + } + } + + private EventJournal getEventJournal(int eventTypes, String absPath, boolean isDeep, String[] uuid, String[] nodeTypeName) throws RepositoryException { + return superuser.getWorkspace().getObservationManager().getEventJournal(eventTypes, absPath, isDeep, uuid, nodeTypeName); + } + + /** + * Checks the journal for events. + * + * @param allowed allowed paths for the returned events. + * @param denied denied paths for the returned events. + * @throws RepositoryException if an error occurs while reading the event + * journal. + */ + private void checkJournal(String[] allowed, String[] denied) throws RepositoryException { + Set allowedSet = new HashSet(Arrays.asList(allowed)); + Set deniedSet = new HashSet(Arrays.asList(denied)); + while (journal.hasNext()) { + String path = journal.nextEvent().getPath(); + allowedSet.remove(path); + if (deniedSet.contains(path)) { + fail(path + " must not be present in journal"); + } + } + assertTrue("Missing paths in journal: " + allowedSet, allowedSet.isEmpty()); + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/observation/EventResult.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventResult.java similarity index 87% rename from src/test/org/apache/jackrabbit/test/api/observation/EventResult.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventResult.java index 150c70cbc36..d360eb5eebb 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/EventResult.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventResult.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -29,7 +29,7 @@ /** * Utility class for Event retrieval with an * EventListener. - *

        + *

        * The {@link #getEventIterator(long)} and {@link #getEvents(long)} methods * will block until an event is delivered and then return the events. Note, that * only one of the methods can be called for an expected event delivery. Calling @@ -70,7 +70,7 @@ public EventResult(PrintWriter log) { /** * Gets the events from the EventListener. Waits at most wait * milliseconds for the events. - *

        + *

        * If the events are not delivered within wait time an empty * array is returned and a log message is written. * @@ -89,7 +89,7 @@ public Event[] getEvents(long wait) { /** * Gets the events from the EventListener. Waits at most wait * milliseconds for the events. - *

        + *

        * If the events are not delivered within wait time * null is returned and a log message is written. * @param wait time in milliseconds to wait at most for @@ -127,10 +127,10 @@ public void onEvent(EventIterator events) { * @return the events from the iterator. */ private Event[] getEvents(EventIterator events) { - List eventList = new ArrayList(); + List eventList = new ArrayList(); while (events.hasNext()) { eventList.add(events.nextEvent()); } - return (Event[]) eventList.toArray(new Event[eventList.size()]); + return eventList.toArray(new Event[eventList.size()]); } } diff --git a/src/test/org/apache/jackrabbit/test/api/observation/EventTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventTest.java similarity index 76% rename from src/test/org/apache/jackrabbit/test/api/observation/EventTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventTest.java index c101c3a32cc..62d43732c1c 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/EventTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/EventTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -22,15 +22,12 @@ /** * Tests methods on the {@link javax.jcr.observation.Event} interface. - *

        - * Configuration requirements are:
        + *

        + * Configuration requirements: + *

        * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. * The child node that is created will be named {@link #nodeName1}. * - * @test - * @sources EventTest.java - * @executeClass org.apache.jackrabbit.test.api.observation.EventTest - * @keywords observation */ public class EventTest extends AbstractObservationTest { @@ -41,9 +38,9 @@ public void testGetNodePath() throws RepositoryException{ EventResult result = new EventResult(log); addEventListener(result, Event.NODE_ADDED); Node addedNode = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); assertEquals("Wrong number of events returned", 1, events.length); String path = events[0].getPath(); String absPath = addedNode.getPath(); @@ -51,19 +48,19 @@ public void testGetNodePath() throws RepositoryException{ } /** - * Tests if {@link javax.jcr.observation.Event#getUserId()} returns the same - * value as {@link javax.jcr.Session#getUserId()}. + * Tests if {@link javax.jcr.observation.Event#getUserID()} returns the same + * value as {@link javax.jcr.Session#getUserID()}. */ public void testGetUserId() throws RepositoryException{ EventResult result = new EventResult(log); addEventListener(result, Event.NODE_ADDED); testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); removeEventListener(result); assertEquals("Wrong number of events returned", 1, events.length); - String userId = events[0].getUserId(); - String sessionUId = superuser.getUserId(); + String userId = events[0].getUserID(); + String sessionUId = superuser.getUserID(); assertEquals("UserId of event is not equal to userId of session", userId, sessionUId); } @@ -74,11 +71,11 @@ public void testGetType() throws RepositoryException{ EventResult result = new EventResult(log); addEventListener(result, Event.NODE_ADDED); testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); assertEquals("Wrong number of events returned", 1, events.length); int type = events[0].getType(); assertEquals("Event did not return correct event type", Event.NODE_ADDED, type); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetDateTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetDateTest.java new file mode 100644 index 00000000000..e0e6ee782fe --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetDateTest.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.test.api.observation; + +import java.util.List; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Collections; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; + +/** + * GetDateTest checks if the dates returned by events are + * monotonically increasing. + */ +public class GetDateTest extends AbstractObservationTest { + + public void testLinearTime() throws RepositoryException { + List names = Arrays.asList(new String[]{nodeName1, nodeName2, nodeName3}); + List dates = new ArrayList(); + for (Iterator it = names.iterator(); it.hasNext(); ) { + final String name = it.next(); + Event[] events = getEvents(new Callable() { + public void call() throws RepositoryException { + testRootNode.addNode(name, testNodeType); + testRootNode.getSession().save(); + } + }, Event.NODE_ADDED); + for (int i = 0; i < events.length; i++) { + dates.add(new Long(events[i].getDate())); + } + try { + // wait for a moment + Thread.sleep(100); + } catch (InterruptedException e) { + // ignore + } + } + List sortedDates = new ArrayList(dates); + Collections.sort(sortedDates); + assertEquals(sortedDates, dates); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetIdentifierTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetIdentifierTest.java new file mode 100644 index 00000000000..ec162217c6f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetIdentifierTest.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.observation; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.observation.Event; + +/** + * IdentifierTest checks if the identifier of an event is correct. + */ +public class GetIdentifierTest extends AbstractObservationTest { + + public void testNodeAdded() throws RepositoryException { + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + } + }, Event.NODE_ADDED); + Node n = testRootNode.getNode(nodeName1); + assertEquals(n.getIdentifier(), getEventByPath(events, n.getPath()).getIdentifier()); + } + + public void testNodeMoved() throws RepositoryException { + final Node n = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + superuser.getWorkspace().move(n.getPath(), testRoot + "/" + nodeName2); + } + }, Event.NODE_MOVED); + String path = testRootNode.getNode(nodeName2).getPath(); + assertEquals(n.getIdentifier(), getEventByPath(events, path).getIdentifier()); + } + + public void testNodeRemoved() throws RepositoryException { + final Node n = testRootNode.addNode(nodeName1, testNodeType); + String path = n.getPath(); + testRootNode.getSession().save(); + String identifier = n.getIdentifier(); + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + n.remove(); + testRootNode.getSession().save(); + } + }, Event.NODE_REMOVED); + assertEquals(identifier, getEventByPath(events, path).getIdentifier()); + } + + public void testPropertyAdded() throws RepositoryException { + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType).setProperty(propertyName1, "test"); + testRootNode.getSession().save(); + } + }, Event.PROPERTY_ADDED); + Node n = testRootNode.getNode(nodeName1); + Property prop = n.getProperty(propertyName1); + assertEquals(n.getIdentifier(), getEventByPath(events, prop.getPath()).getIdentifier()); + } + + public void testPropertyChanged() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + final Property prop = n.setProperty(propertyName1, "test"); + testRootNode.getSession().save(); + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + prop.setValue("modified"); + testRootNode.getSession().save(); + } + }, Event.PROPERTY_CHANGED); + assertEquals(n.getIdentifier(), getEventByPath(events, prop.getPath()).getIdentifier()); + } + + public void testPropertyRemoved() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + final Property prop = n.setProperty(propertyName1, "test"); + String propPath = prop.getPath(); + testRootNode.getSession().save(); + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + prop.remove(); + testRootNode.getSession().save(); + } + }, Event.PROPERTY_REMOVED); + assertEquals(n.getIdentifier(), getEventByPath(events, propPath).getIdentifier()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetInfoTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetInfoTest.java new file mode 100644 index 00000000000..93f999cca4d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetInfoTest.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.test.api.observation; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; + +/** + * GetInfoTest checks that the info map is empty for event types: + * {@link Event#NODE_ADDED}, {@link Event#NODE_REMOVED}, + * {@link Event#PROPERTY_ADDED}, {@link Event#PROPERTY_CHANGED} and + * {@link Event#PROPERTY_REMOVED}. + */ +public class GetInfoTest extends AbstractObservationTest { + + public void testNodeAdded() throws RepositoryException { + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + } + }, Event.NODE_ADDED); + for (int i = 0; i < events.length; i++) { + Set unexpectedKeys = getUnexpectedKeys(events[i].getInfo()); + assertEquals("info map contains invalid keys: " + unexpectedKeys, 0, unexpectedKeys.size()); + } + } + + public void testNodeRemoved() throws RepositoryException { + final Node n = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + n.remove(); + testRootNode.getSession().save(); + } + }, Event.NODE_REMOVED); + for (int i = 0; i < events.length; i++) { + Set unexpectedKeys = getUnexpectedKeys(events[i].getInfo()); + assertEquals("info map must be empty", 0, unexpectedKeys.size()); + } + } + + public void testPropertyAdded() throws RepositoryException { + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType).setProperty(propertyName1, "test"); + testRootNode.getSession().save(); + } + }, Event.PROPERTY_ADDED); + for (int i = 0; i < events.length; i++) { + Set unexpectedKeys = getUnexpectedKeys(events[i].getInfo()); + assertEquals("info map must be empty", 0, unexpectedKeys.size()); + } + } + + public void testPropertyChanged() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + final Property prop = n.setProperty(propertyName1, "test"); + testRootNode.getSession().save(); + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + prop.setValue("modified"); + prop.getSession().save(); + } + }, Event.PROPERTY_CHANGED); + for (int i = 0; i < events.length; i++) { + Set unexpectedKeys = getUnexpectedKeys(events[i].getInfo()); + assertEquals("info map must be empty: " + unexpectedKeys, 0, unexpectedKeys.size()); + } + } + + public void testPropertyRemoved() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + final Property prop = n.setProperty(propertyName1, "test"); + testRootNode.getSession().save(); + Event[] events = getEvents(new Callable(){ + public void call() throws RepositoryException { + Session s = prop.getSession(); + prop.remove(); + s.save(); + } + }, Event.PROPERTY_REMOVED); + for (int i = 0; i < events.length; i++) { + Set unexpectedKeys = getUnexpectedKeys(events[i].getInfo()); + assertEquals("info map must be empty: " + unexpectedKeys, 0, unexpectedKeys.size()); + } + } + + private static Set getUnexpectedKeys(Map info) { + Set result = new HashSet(); + result.addAll(info.keySet()); + result.remove("jcr:primaryType"); + result.remove("jcr:mixinTypes"); + return result; + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/observation/GetRegisteredEventListenersTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetRegisteredEventListenersTest.java similarity index 82% rename from src/test/org/apache/jackrabbit/test/api/observation/GetRegisteredEventListenersTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetRegisteredEventListenersTest.java index c7ab0e5bc5a..c00c37c9248 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/GetRegisteredEventListenersTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetRegisteredEventListenersTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -23,10 +23,6 @@ /** * Tests the method {@link javax.jcr.observation.ObservationManager#getRegisteredEventListeners()}. * - * @test - * @sources GetRegisteredEventListenersTest.java - * @executeClass org.apache.jackrabbit.test.api.observation.GetRegisteredEventListenersTest - * @keywords observation */ public class GetRegisteredEventListenersTest extends AbstractObservationTest { @@ -62,4 +58,4 @@ public void testRemoveEventListener() throws RepositoryException { assertEquals("Returned listener is not equal to regsitered one.", listener2, listeners[0]); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetUserDataTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetUserDataTest.java new file mode 100644 index 00000000000..1600f488331 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/GetUserDataTest.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.test.api.observation; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * GetUserDataTest performs observation tests with user data set + * on the observation manager. + */ +public class GetUserDataTest extends AbstractObservationTest { + + public void testSave() throws RepositoryException { + runWithUserData(new Callable() { + public void call() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + } + }, ALL_TYPES); + } + + public void testWorkspaceOperation() throws RepositoryException { + testRootNode.addNode(nodeName1); + testRootNode.getSession().save(); + + runWithUserData(new Callable() { + public void call() throws RepositoryException { + String src = testRoot + "/" + nodeName1; + String dest = testRoot + "/" + nodeName2; + superuser.getWorkspace().move(src, dest); + } + }, ALL_TYPES); + } + + public void testVersioning() + throws RepositoryException, NotExecutableException { + checkSupportedOption(Repository.OPTION_VERSIONING_SUPPORTED); + + final Node n1 = testRootNode.addNode(nodeName1); + ensureMixinType(n1, mixVersionable); + testRootNode.getSession().save(); + + runWithUserData(new Callable() { + public void call() throws RepositoryException { + n1.checkin(); + } + }, Event.NODE_ADDED); // get events for added version node + } + + protected void runWithUserData(final Callable c, int eventTypes) + throws RepositoryException { + final String data = createRandomString(5); + Event[] events = getEvents(new Callable() { + public void call() throws RepositoryException { + obsMgr.setUserData(data); + c.call(); + } + }, eventTypes); + + assertTrue("no events returned", events.length > 0); + for (int i = 0; i < events.length; i++) { + assertEquals("Wrong user data", data, events[i].getUserData()); + } + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/observation/LockingTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/LockingTest.java similarity index 80% rename from src/test/org/apache/jackrabbit/test/api/observation/LockingTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/LockingTest.java index f0d5a3e9333..9a300cca794 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/LockingTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/LockingTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -22,20 +22,19 @@ import java.util.List; import java.util.ArrayList; +import org.apache.jackrabbit.test.NotExecutableException; + /** * Tests if locking a node triggers property added events for jcr:lockOwner * and jcr:lockIsDeep. - *

        - * Configuration requirements are:
        + *

        + * Configuration requirements: + *

        * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. * The child node that is created will be named {@link #nodeName1}. The node * type {@link #testNodeType} must either have mix:lockable as one of its * supertypes or must allow to add mix:lockable as mixin. * - * @test - * @sources LockingTest.java - * @executeClass org.apache.jackrabbit.test.api.observation.LockingTest - * @keywords observation locking */ public class LockingTest extends AbstractObservationTest { @@ -43,23 +42,24 @@ public class LockingTest extends AbstractObservationTest { * Tests if locking a node triggers property added events for the properties * jcr:lockOwner and jcr:lockIsDeep. */ - public void testAddLockToNode() throws RepositoryException { + public void testAddLockToNode() throws RepositoryException, + NotExecutableException { Node lockable = createLockable(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_ADDED); // now lock node. no save needed lockable.lock(false,true); - removeEventListener(result); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); assertEquals("Wrong number of events.", 2, events.length); for (int i = 0; i < events.length; i++) { assertEquals("Wrong type of event.", Event.PROPERTY_ADDED, events[i].getType()); } - List paths = new ArrayList(); + List paths = new ArrayList(); for (int i = 0; i < events.length; i++) { paths.add(events[i].getPath()); } @@ -75,23 +75,24 @@ public void testAddLockToNode() throws RepositoryException { * Tests if unlocking a node triggers property removed events for the * properties jcr:lockOwner and jcr:lockIsDeep. */ - public void testRemoveLockFromNode() throws RepositoryException { + public void testRemoveLockFromNode() throws RepositoryException, + NotExecutableException { Node lockable = createLockable(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); // lock the node lockable.lock(false, true); EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_REMOVED); lockable.unlock(); - removeEventListener(result); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); assertEquals("Wrong number of events.", 2, events.length); for (int i = 0; i < events.length; i++) { assertEquals("Wrong type of event.", Event.PROPERTY_REMOVED, events[i].getType()); } - List paths = new ArrayList(); + List paths = new ArrayList(); for (int i = 0; i < events.length; i++) { paths.add(events[i].getPath()); } @@ -110,11 +111,9 @@ public void testRemoveLockFromNode() throws RepositoryException { * @return the lockable node */ private Node createLockable(String nodeName, String nodeType) - throws RepositoryException { + throws RepositoryException, NotExecutableException { Node n = testRootNode.addNode(nodeName, nodeType); - if (!n.isNodeType(mixLockable)) { - n.addMixin(mixLockable); - } + ensureMixinType(n, mixLockable); return n; } -} \ No newline at end of file +} diff --git a/src/test/org/apache/jackrabbit/test/api/observation/NodeAddedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeAddedTest.java similarity index 78% rename from src/test/org/apache/jackrabbit/test/api/observation/NodeAddedTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeAddedTest.java index a03e7f273a6..921e31f44ee 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/NodeAddedTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeAddedTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,18 +21,15 @@ import javax.jcr.observation.Event; /** - * Test cases for {@link javax.jcr.observation.Event.NODE_ADDED} events. - *

        - * Configuration requirements are:
        + * Test cases for {@link javax.jcr.observation.Event#NODE_ADDED} events. + *

        + * Configuration requirements: + *

        * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. * The child nodes that are created will be named {@link #nodeName1} and * {@link #nodeName2}. Furthermore {@link #testNodeType} must allow to add * child nodes of the same type ({@link #testNodeType}). * - * @test - * @sources NodeAddedTest.java - * @executeClass org.apache.jackrabbit.test.api.observation.NodeAddedTest - * @keywords observation */ public class NodeAddedTest extends AbstractObservationTest { @@ -44,10 +41,10 @@ public void testSingleNodeAdded() throws RepositoryException { EventResult result = new EventResult(log); addEventListener(result, Event.NODE_ADDED); testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(events, new String[]{nodeName1}); + removeEventListener(result); + checkNodeAdded(events, new String[]{nodeName1}, null); } /** @@ -59,10 +56,10 @@ public void testMultipleNodeAdded1() throws RepositoryException { addEventListener(result, Event.NODE_ADDED); testRootNode.addNode(nodeName1, testNodeType); testRootNode.addNode(nodeName2, testNodeType); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(events, new String[]{nodeName1, nodeName2}); + removeEventListener(result); + checkNodeAdded(events, new String[]{nodeName1, nodeName2}, null); } /** @@ -74,10 +71,10 @@ public void testMultipleNodeAdded2() throws RepositoryException { addEventListener(result, Event.NODE_ADDED); Node n1 = testRootNode.addNode(nodeName1, testNodeType); n1.addNode(nodeName2, testNodeType); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(events, new String[]{nodeName1, nodeName1 + "/" + nodeName2}); + removeEventListener(result); + checkNodeAdded(events, new String[]{nodeName1, nodeName1 + "/" + nodeName2}, null); } /** @@ -89,9 +86,9 @@ public void testTransientNodeAddedRemoved() throws RepositoryException { testRootNode.addNode(nodeName1, testNodeType); Node n2 = testRootNode.addNode(nodeName2, testNodeType); n2.remove(); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(events, new String[]{nodeName1}); + removeEventListener(result); + checkNodeAdded(events, new String[]{nodeName1}, null); } } diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeMovedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeMovedTest.java new file mode 100644 index 00000000000..7ad367403ac --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeMovedTest.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.test.api.observation; + +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.observation.Event; + +/** + * Tests if {@link javax.jcr.Session#move} operations trigger the appropriate + * observation events. + *

        + * Configuration requirements: + *

        + * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. + * The child nodes that are created will be named {@link #nodeName1}, + * {@link #nodeName2}, {@link #nodeName3} and {@link #nodeName4}. Furthermore + * {@link #testNodeType} must allow to add child nodes of the same type + * ({@link #testNodeType}). + * + */ +public class NodeMovedTest extends AbstractObservationTest { + + /** + * The key srcAbsPath in the info map. + */ + private static final String SRC_ABS_PATH = "srcAbsPath"; + + /** + * The key destAbsPath in the info map. + */ + private static final String DEST_ABS_PATH = "destAbsPath"; + + /** + * Tests if node removed and node added event is triggered when a tree + * is moved. + */ + public void testMoveTree() throws RepositoryException { + /** + * Initial tree: + * + testroot + * + nodename1 + * + nodename2 + * + * After move: + * + testroot + * + nodename3 + * + nodename2 + */ + + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + EventResult addNodeListener = new EventResult(log); + EventResult removeNodeListener = new EventResult(log); + EventResult moveNodeListener = new EventResult(log); + addEventListener(addNodeListener, Event.NODE_ADDED); + addEventListener(removeNodeListener, Event.NODE_REMOVED); + addEventListener(moveNodeListener, Event.NODE_MOVED); + superuser.move(n1.getPath(), testRoot + "/" + nodeName3); + testRootNode.getSession().save(); + Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] moved = moveNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(addNodeListener); + removeEventListener(removeNodeListener); + removeEventListener(moveNodeListener); + checkNodeAdded(added, new String[]{nodeName3}, new String[]{nodeName3 + "/" + nodeName2}); + checkNodeRemoved(removed, new String[]{nodeName1}, new String[]{nodeName1 + "/" + nodeName2}); + checkNodeMoved(moved, nodeName1, nodeName3); + } + + /** + * Tests if node removed and node added event is triggered when a node + * is moved. + */ + public void testMoveNode() throws RepositoryException { + /** + * Initial tree: + * + testroot + * + nodename1 + * + nodename2 + * + * After move: + * + testroot + * + nodename1 + * + nodename2 + */ + + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = n1.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + EventResult addNodeListener = new EventResult(log); + EventResult removeNodeListener = new EventResult(log); + EventResult moveNodeListener = new EventResult(log); + addEventListener(addNodeListener, Event.NODE_ADDED); + addEventListener(removeNodeListener, Event.NODE_REMOVED); + addEventListener(moveNodeListener, Event.NODE_MOVED); + superuser.move(n2.getPath(), testRoot + "/" + nodeName2); + testRootNode.getSession().save(); + Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] moved = moveNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(addNodeListener); + removeEventListener(removeNodeListener); + removeEventListener(moveNodeListener); + checkNodeAdded(added, new String[]{nodeName2}, null); + checkNodeRemoved(removed, new String[]{nodeName1 + "/" + nodeName2}, null); + checkNodeMoved(moved, nodeName1 + "/" + nodeName2, nodeName2); + } + + /** + * Tests if a node moved triggers the correct events when the former parent + * node is removed at the same time. + */ + public void testMoveWithRemove() throws RepositoryException { + /** + * Initial tree: + * + testroot + * + nodename1 + * + nodename2 + * + nodename3 + * + * After move and remove: + * + testroot + * + nodename3 + * + nodename2 + */ + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = n1.addNode(nodeName2, testNodeType); + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + EventResult addNodeListener = new EventResult(log); + EventResult removeNodeListener = new EventResult(log); + EventResult moveNodeListener = new EventResult(log); + addEventListener(addNodeListener, Event.NODE_ADDED); + addEventListener(removeNodeListener, Event.NODE_REMOVED); + addEventListener(moveNodeListener, Event.NODE_MOVED); + // move n2 + superuser.move(n2.getPath(), n3.getPath() + "/" + nodeName2); + // remove n1 + n1.remove(); + testRootNode.getSession().save(); + Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] moved = moveNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(addNodeListener); + removeEventListener(removeNodeListener); + removeEventListener(moveNodeListener); + checkNodeAdded(added, new String[]{nodeName3 + "/" + nodeName2}, null); + checkNodeRemoved(removed, new String[]{nodeName1 + "/" + nodeName2, nodeName1}, null); + checkNodeMoved(moved, nodeName1 + "/" + nodeName2, nodeName3 + "/" + nodeName2); + } + + /** + * TODO: move to base class once JSR 283 is final + * Checks Events for paths. All relPaths are + * relative to {@link #testRoot}. + * + * @param events the Events. + * @param from the source path where the node was moved from. + * @param to the destination path where the node was moved to. + * @throws RepositoryException if an error occurs while retrieving the nodes + * from event instances. + */ + protected void checkNodeMoved(Event[] events, String from, String to) + throws RepositoryException { + checkNodes(events, new String[]{to}, null, Event.NODE_MOVED); + assertEquals("Wrong number of events", 1, events.length); + Map info = events[0].getInfo(); + checkInfoEntry(info, SRC_ABS_PATH, testRoot + "/" + from); + checkInfoEntry(info, DEST_ABS_PATH, testRoot + "/" + to); + } + + /** + * TODO: move to base class once JSR 283 is final + * Checks if the info map contains the given key with the + * expected value. + * + * @param info the event info map. + * @param key the name of the key. + * @param expected the expected value. + */ + protected void checkInfoEntry(Map info, String key, String expected) { + String value = (String) info.get(key); + assertNotNull("Missing event info key: " + key, value); + assertEquals("Wrong event info value for: " + key, expected, value); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeRemovedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeRemovedTest.java new file mode 100644 index 00000000000..397c3b82949 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeRemovedTest.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.test.api.observation; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; + +/** + * Test cases for {@link javax.jcr.observation.Event#NODE_REMOVED} events. + *

        + * Configuration requirements: + *

        + * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. + * The child nodes that are created will be named {@link #nodeName1} and + * {@link #nodeName2}. Furthermore {@link #testNodeType} must allow to add + * child nodes of the same type ({@link #testNodeType}). + * + */ +public class NodeRemovedTest extends AbstractObservationTest { + + /** + * Tests if a {@link javax.jcr.observation.Event#NODE_REMOVED} is triggered + * when a single node is removed. + */ + public void testSingleNodeRemoved() throws RepositoryException { + Node foo = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + EventResult result = new EventResult(log); + addEventListener(result, Event.NODE_REMOVED); + foo.remove(); + testRootNode.getSession().save(); + Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); + checkNodeRemoved(events, new String[]{nodeName1}, null); + } + + /** + * Tests if {@link javax.jcr.observation.Event#NODE_REMOVED} events are + * triggered when multiple nodes are removed. + */ + public void testMultiNodesRemoved() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + EventResult result = new EventResult(log); + addEventListener(result, Event.NODE_REMOVED); + n1.remove(); + testRootNode.getSession().save(); + Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); + checkNodeRemoved(events, new String[]{nodeName1, nodeName1 + "/" + nodeName2}, null); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeReorderTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeReorderTest.java new file mode 100644 index 00000000000..0a1ad548b3e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/NodeReorderTest.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.observation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.observation.Event; + +/** + * Tests if {@link javax.jcr.Node#orderBefore(String, String)} operations trigger + * the appropriate observation events. + *

          + *
        • {@code testroot} must allow orderable child nodes of type + * nodetype, otherwise the test cases throw a + * {@link NotExecutableException}. Some tests are only executed if the node + * at testroot support same name sibling child nodes. + *
        • {@code nodetype} node type that allows child nodes of the same type. + *
        • {@code nodename1} child node name of type nodetype + *
        • {@code nodename2} child node name of type nodetype + *
        • {@code nodename3} child node name of type nodetype + *
        + */ +public class NodeReorderTest extends AbstractObservationTest { + + /** + * The key srcChildRelPath in the info map. + */ + private static final String SRC_CHILD_REL_PATH = "srcChildRelPath"; + + /** + * The key destChildRelPath in the info map. + */ + private static final String DEST_CHILD_REL_PATH = "destChildRelPath"; + + private void doTestNodeReorder(List added, List removed, List moved) + throws RepositoryException, NotExecutableException { + if (!testRootNode.getDefinition().getDeclaringNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Node at '" + testRoot + "' does not support orderable child nodes."); + } + + /** + * Initial tree: + * + testroot + * + nodename1 + * + nodename2 + * + nodename3 + * + * After reorder: + * + testroot + * + nodename1 + * + nodename3 + * + nodename2 + */ + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + EventResult listener = new EventResult(log); + addEventListener(listener, Event.NODE_ADDED | Event.NODE_REMOVED | Event.NODE_MOVED); + testRootNode.orderBefore(nodeName3, nodeName2); + testRootNode.getSession().save(); + List events = Arrays.asList(listener.getEvents(DEFAULT_WAIT_TIMEOUT)); + for (Event e : events) { + if (e.getType() == Event.NODE_ADDED) { + added.add(e); + } else if (e.getType() == Event.NODE_REMOVED) { + removed.add(e); + } else if (e.getType() == Event.NODE_MOVED) { + moved.add(e); + } else { + fail("unexpected event type: " + e.getType()); + } + } + removeEventListener(listener); + } + + /** + * Tests if reordering a child node triggers a {@link Event#NODE_REMOVED} + * and a {@link Event#NODE_ADDED} event. + */ + public void testNodeReorderAddRemove() throws RepositoryException, NotExecutableException { + + List added = new ArrayList(); + List removed = new ArrayList(); + List moved = new ArrayList(); + + doTestNodeReorder(added, removed, moved); + + // either + // 1) nodename2 has been reordered to the end + // or: + // 2) nodename3 has been reordered before nodename2 + // that is, the following event sets are correct: + // 1) nodename2:remove, nodename2:add + // or: + // 2) nodename3:remove, nodename3:add + + // if true, check for option 1) + boolean reorderEnd = false; + for (Event e : added) { + if (e.getPath().endsWith(nodeName2)) { + reorderEnd = true; + break; + } + } + + if (reorderEnd) { + checkNodeAdded(added, new String[] { nodeName2 }, null); + checkNodeRemoved(removed, new String[] { nodeName2 }, null); + } else { + checkNodeAdded(added, new String[] { nodeName3 }, null); + checkNodeRemoved(removed, new String[] { nodeName3 }, null); + } + } + + /** + * Tests if reordering a child node triggers a {@link Event#NODE_MOVED} + * event. + */ + public void testNodeReorderMove() throws RepositoryException, NotExecutableException { + + List added = new ArrayList(); + List removed = new ArrayList(); + List moved = new ArrayList(); + + doTestNodeReorder(added, removed, moved); + + checkNodeReordered(moved, nodeName3, nodeName3, nodeName2); + } + + /** + * Tests if reordering a child node triggers a {@link Event#NODE_REMOVED} + * and a {@link Event#NODE_ADDED} event with same name siblings. + */ + public void testNodeReorderSameName() + throws RepositoryException, NotExecutableException { + if (!testRootNode.getDefinition().getDeclaringNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Node at '" + testRoot + "' does not support orderable child nodes."); + } + + /** + * Initial tree: + * + testroot + * + nodename1[1] + * + nodename1[2] + * + nodename1[3] + * + * After reorder: + * + testroot + * + nodename1[1] + * + nodename1[2] (was 3) + * + nodename1[3] (was 2) + */ + Node n = testRootNode.addNode(nodeName1, testNodeType); + if (!n.getDefinition().allowsSameNameSiblings()) { + throw new NotExecutableException("Node at " + testRoot + " does not allow same name siblings with name " + nodeName1); + } + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.getSession().save(); + EventResult addNodeListener = new EventResult(log); + EventResult removeNodeListener = new EventResult(log); + EventResult moveNodeListener = new EventResult(log); + addEventListener(addNodeListener, Event.NODE_ADDED); + addEventListener(removeNodeListener, Event.NODE_REMOVED); + addEventListener(moveNodeListener, Event.NODE_MOVED); + testRootNode.orderBefore(nodeName1 + "[3]", nodeName1 + "[2]"); + //testRootNode.orderBefore(nodeName1 + "[2]", null); + testRootNode.getSession().save(); + Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] moved = moveNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(addNodeListener); + removeEventListener(removeNodeListener); + removeEventListener(moveNodeListener); + // either + // 1) nodename1[2] has been reordered to the end + // or: + // 2) nodename1[3] has been reordered before nodename1[2] + // that is, the following event sets are correct: + // 1) nodename1[2]:remove, nodename1[3]:add + // or: + // 2) nodename1[3]:remove, nodename1[2]:add + + // if true, check for option 1) + boolean reorderEnd = false; + for (int i = 0; i < added.length; i++) { + if (added[i].getPath().endsWith(nodeName1 + "[3]")) { + reorderEnd = true; + break; + } + } + if (reorderEnd) { + checkNodeAdded(added, new String[]{nodeName1 + "[3]"}, null); + checkNodeRemoved(removed, new String[]{nodeName1 + "[2]"}, null); + checkNodeReordered(moved, nodeName1 + "[2]", nodeName1 + "[3]", null); + } else { + checkNodeAdded(added, new String[]{nodeName1 + "[2]"}, null); + checkNodeRemoved(removed, new String[]{nodeName1 + "[3]"}, null); + checkNodeReordered(moved, nodeName1 + "[3]", nodeName1 + "[2]", nodeName1 + "[2]"); + } + } + + /** + * Tests if reordering a child node triggers a {@link Event#NODE_REMOVED} + * and a {@link Event#NODE_ADDED} event with same name siblings. Furthermore + * a node is removed in the same save scope. + */ + public void testNodeReorderSameNameWithRemove() + throws RepositoryException, NotExecutableException { + if (!testRootNode.getDefinition().getDeclaringNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Node at '" + testRoot + "' does not support orderable child nodes."); + } + + /** + * Initial tree: + * + testroot + * + nodename1[1] + * + nodename2 + * + nodename1[2] + * + nodename1[3] + * + nodename3 + * + * After reorder: + * + testroot + * + nodename1[1] + * + nodename2 + * + nodename1[2] (was 3) + * + nodename1[3] (was 2) + */ + Node n = testRootNode.addNode(nodeName1, testNodeType); + if (!n.getDefinition().allowsSameNameSiblings()) { + throw new NotExecutableException("Node at " + testRoot + " does not allow same name siblings with name " + nodeName1); + } + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + EventResult addNodeListener = new EventResult(log); + EventResult removeNodeListener = new EventResult(log); + EventResult moveNodeListener = new EventResult(log); + addEventListener(addNodeListener, Event.NODE_ADDED); + addEventListener(removeNodeListener, Event.NODE_REMOVED); + addEventListener(moveNodeListener, Event.NODE_MOVED); + testRootNode.orderBefore(nodeName1 + "[2]", null); + testRootNode.getNode(nodeName3).remove(); + testRootNode.getSession().save(); + Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + Event[] moved = moveNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(addNodeListener); + removeEventListener(removeNodeListener); + removeEventListener(moveNodeListener); + // either + // 1) nodename1[2] has been reordered to the end + // or: + // 2) nodename1[3] has been reordered before nodename1[2] + // + // that is, the following event sets are correct: + // 1) nodename1[2]:remove, nodename1[3]:add, nodename3:remove + // or: + // 2) nodename1[3]:remove, nodename1[2]:add, nodename3:remove + + // if true, check for option 1) + boolean reorderEnd = false; + for (int i = 0; i < added.length; i++) { + if (added[i].getPath().endsWith(nodeName1 + "[3]")) { + reorderEnd = true; + break; + } + } + if (reorderEnd) { + checkNodeAdded(added, new String[]{nodeName1 + "[3]"}, null); + checkNodeRemoved(removed, new String[]{nodeName1 + "[2]", nodeName3}, null); + checkNodeReordered(moved, nodeName1 + "[2]", nodeName1 + "[3]", null); + } else { + checkNodeAdded(added, new String[]{nodeName1 + "[2]"}, null); + checkNodeRemoved(removed, new String[]{nodeName1 + "[3]", nodeName3}, null); + checkNodeReordered(moved, nodeName1 + "[3]", nodeName1 + "[2]", nodeName1 + "[2]"); + } + } + + /** + * Checks Events for paths. All relPaths are + * relative to {@link #testRoot}. + * + * @param events the Events. + * @param src the source child path where the node was reordered from. + * @param dest the destination child path where the node was reordered to. + * @param before the destination child path where the node was reordered before. + * @throws RepositoryException if an error occurs while retrieving the nodes + * from event instances. + */ + protected void checkNodeReordered(Event[] events, String src, + String dest, String before) + throws RepositoryException { + checkNodes(events, new String[]{dest}, null, Event.NODE_MOVED); + assertEquals("Wrong number of events", 1, events.length); + Map info = events[0].getInfo(); + checkInfoEntry(info, SRC_CHILD_REL_PATH, src); + checkInfoEntry(info, DEST_CHILD_REL_PATH, before); + } + + protected void checkNodeReordered(List events, String src, String dest, String before) + throws RepositoryException { + checkNodes(events.toArray(new Event[0]), new String[] { dest }, null, Event.NODE_MOVED); + assertEquals("Wrong number of events", 1, events.size()); + Map info = events.get(0).getInfo(); + checkInfoEntry(info, SRC_CHILD_REL_PATH, src); + checkInfoEntry(info, DEST_CHILD_REL_PATH, before); + } + + /** + * Checks if the info map contains the given key with the + * expected value. + * + * @param info the event info map. + * @param key the name of the key. + * @param expected the expected value. + */ + protected void checkInfoEntry(Map info, String key, String expected) { + assertTrue("Missing event info key: " + key, info.containsKey(key)); + assertEquals("Wrong event info value for: " + key, + expected, (String) info.get(key)); + } + + protected void checkNodeAdded(List events, String[] requiredRelPaths, String[] optionalRelPaths) + throws RepositoryException { + checkNodes(events.toArray(new Event[0]), requiredRelPaths, optionalRelPaths, Event.NODE_ADDED); + } + + protected void checkNodeRemoved(List events, String[] requiredRelPaths, String[] optionalRelPaths) + throws RepositoryException { + checkNodes(events.toArray(new Event[0]), requiredRelPaths, optionalRelPaths, Event.NODE_REMOVED); + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/observation/PropertyAddedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyAddedTest.java similarity index 80% rename from src/test/org/apache/jackrabbit/test/api/observation/PropertyAddedTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyAddedTest.java index fe5801d9b4d..ca37964f37c 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/PropertyAddedTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyAddedTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -23,19 +23,16 @@ import java.util.List; /** - * Test cases for {@link javax.jcr.observation.Event.PROPERTY_ADDED} events. - *

        - * Configuration requirements are:
        + * Test cases for {@link javax.jcr.observation.Event#PROPERTY_ADDED} events. + *

        + * Configuration requirements: + *

        * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. * The child nodes that are created will be named {@link #nodeName1} and * {@link #nodeName2}. * {@link #testNodeType} must also support String properties with names * {@link #propertyName1} and {@link #propertyName2}. * - * @test - * @sources PropertyAddedTest.java - * @executeClass org.apache.jackrabbit.test.api.observation.PropertyAddedTest - * @keywords observation */ public class PropertyAddedTest extends AbstractObservationTest { @@ -47,10 +44,10 @@ public void testSystemGenerated() throws RepositoryException { EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_ADDED); testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); - List paths = new ArrayList(); + removeEventListener(result); + List paths = new ArrayList(); for (int i = 0; i < events.length; i++) { paths.add(events[i].getPath()); } @@ -65,13 +62,13 @@ public void testSystemGenerated() throws RepositoryException { */ public void testSinglePropertyAdded() throws RepositoryException { Node foo = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_ADDED); foo.setProperty(propertyName1, "test content"); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); checkPropertyAdded(events, new String[]{nodeName1 + "/" + propertyName1}); } @@ -81,14 +78,14 @@ public void testSinglePropertyAdded() throws RepositoryException { */ public void testMultiPropertyAdded() throws RepositoryException { Node foo = testRootNode.addNode(nodeName1, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_ADDED); foo.setProperty(propertyName1, "foo"); foo.setProperty(propertyName2, "bar"); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); checkPropertyAdded(events, new String[]{nodeName1 + "/" + propertyName1, nodeName1 + "/" + propertyName2}); } diff --git a/src/test/org/apache/jackrabbit/test/api/observation/PropertyChangedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyChangedTest.java similarity index 84% rename from src/test/org/apache/jackrabbit/test/api/observation/PropertyChangedTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyChangedTest.java index 1808807b4ee..2ee7c7ab0d5 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/PropertyChangedTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyChangedTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,25 +21,21 @@ import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.PropertyType; -import javax.jcr.StringValue; -import javax.jcr.LongValue; +import javax.jcr.Value; import javax.jcr.nodetype.NodeType; import javax.jcr.observation.Event; /** - * Test cases for {@link javax.jcr.observation.Event.PROPERTY_CHANGED} events. - *

        - * Configuration requirements are:
        + * Test cases for {@link javax.jcr.observation.Event#PROPERTY_CHANGED} events. + *

        + * Configuration requirements: + *

        * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. * The child nodes that are created will be named {@link #nodeName1} and * {@link #nodeName2}. * {@link #testNodeType} must also support String properties with names * {@link #propertyName1} and {@link #propertyName2}. * - * @test - * @sources PropertyChangedTest.java - * @executeClass org.apache.jackrabbit.test.api.observation.PropertyChangedTest - * @keywords observation */ public class PropertyChangedTest extends AbstractObservationTest { @@ -50,13 +46,13 @@ public class PropertyChangedTest extends AbstractObservationTest { public void testSinglePropertyChanged() throws RepositoryException { Node node = testRootNode.addNode(nodeName1, testNodeType); node.setProperty(propertyName1, "foo"); - testRootNode.save(); + testRootNode.getSession().save(); EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_CHANGED); node.getProperty(propertyName1).setValue("foobar"); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); checkPropertyChanged(events, new String[]{nodeName1 + "/" + propertyName1}); } @@ -69,14 +65,14 @@ public void testMultiPropertyChanged() throws RepositoryException { Node node = testRootNode.addNode(nodeName1, testNodeType); node.setProperty(propertyName1, "foo"); node.setProperty(propertyName2, "bar"); - testRootNode.save(); + testRootNode.getSession().save(); EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_CHANGED); node.getProperty(propertyName1).setValue("foobar"); node.getProperty(propertyName2).setValue("foobar"); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); checkPropertyChanged(events, new String[]{nodeName1 + "/" + propertyName1, nodeName1 + "/" + propertyName2}); } @@ -88,14 +84,14 @@ public void testMultiPropertyChanged() throws RepositoryException { public void testSinglePropertyChangedWithAdded() throws RepositoryException { Node node = testRootNode.addNode(nodeName1, testNodeType); node.setProperty(propertyName1, "foo"); - testRootNode.save(); + testRootNode.getSession().save(); EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_CHANGED); node.getProperty(propertyName1).setValue("foobar"); node.setProperty(propertyName2, "bar"); // will not fire prop changed event - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); checkPropertyChanged(events, new String[]{nodeName1 + "/" + propertyName1}); } @@ -107,7 +103,7 @@ public void testSinglePropertyChangedWithAdded() throws RepositoryException { * * is triggered if a property is transiently removed and set again with * the same name but different type and then saved. - *

        + *

        * If the node type {@link #testNodeType} does not suppport a property with * name {@link #propertyName1} of type {@link PropertyType#UNDEFINED} a * {@link NotExecutableException} is thrown. @@ -116,20 +112,20 @@ public void testPropertyRemoveCreate() throws RepositoryException, NotExecutableException { Node n = testRootNode.addNode(nodeName1, testNodeType); NodeType nt = superuser.getWorkspace().getNodeTypeManager().getNodeType(testNodeType); - StringValue v1 = new StringValue("foo"); - LongValue v2 = new LongValue(System.currentTimeMillis()); + Value v1 = superuser.getValueFactory().createValue("foo"); + Value v2 = superuser.getValueFactory().createValue(System.currentTimeMillis()); if (!nt.canSetProperty(propertyName1, v1) || !nt.canSetProperty(propertyName1, v2)) { throw new NotExecutableException("Property " + propertyName1 + " is not of type UNDEFINED"); } n.setProperty(propertyName1, v1); - testRootNode.save(); + testRootNode.getSession().save(); EventResult result = new EventResult(log); addEventListener(result, Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED); n.getProperty(propertyName1).remove(); n.setProperty(propertyName1, v2); - testRootNode.save(); - removeEventListener(result); + testRootNode.getSession().save(); Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); if (events.length == 1) { checkPropertyChanged(events, new String[]{nodeName1 + "/" + propertyName1}); diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyRemovedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyRemovedTest.java new file mode 100644 index 00000000000..d4fb45e6c09 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/PropertyRemovedTest.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.test.api.observation; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.observation.Event; + +/** + * Test cases for {@link javax.jcr.observation.Event#PROPERTY_REMOVED} events. + *

        + * Configuration requirements: + *

        + * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. + * The child nodes that are created will be named {@link #nodeName1} and + * {@link #nodeName2}. + * {@link #testNodeType} must also support String properties with names + * {@link #propertyName1} and {@link #propertyName2}. + * + */ +public class PropertyRemovedTest extends AbstractObservationTest { + + /** + * Tests if a {@link javax.jcr.observation.Event#PROPERTY_REMOVED} is + * triggered when a property is removed. + */ + public void testSinglePropertyRemoved() throws RepositoryException { + Node node = testRootNode.addNode(nodeName1, testNodeType); + Property prop1 = node.setProperty(propertyName1, "foo"); + node.setProperty(propertyName2, "bar"); + testRootNode.getSession().save(); + EventResult result = new EventResult(log); + addEventListener(result, Event.PROPERTY_REMOVED); + prop1.remove(); + testRootNode.getSession().save(); + Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); + checkPropertyRemoved(events, new String[]{nodeName1 + "/" + propertyName1}); + } + + /** + * Tests if {@link javax.jcr.observation.Event#PROPERTY_REMOVED} are + * triggered when multiple properties are removed. + */ + public void testMultiPropertyRemoved() throws RepositoryException { + Node node = testRootNode.addNode(nodeName1, testNodeType); + Property prop1 = node.setProperty(propertyName1, "foo"); + Property prop2 = node.setProperty(propertyName2, "bar"); + testRootNode.getSession().save(); + EventResult result = new EventResult(log); + addEventListener(result, Event.PROPERTY_REMOVED); + prop1.remove(); + prop2.remove(); + testRootNode.getSession().save(); + Event[] events = result.getEvents(DEFAULT_WAIT_TIMEOUT); + removeEventListener(result); + checkPropertyRemoved(events, new String[]{nodeName1 + "/" + propertyName1, + nodeName1 + "/" + propertyName2}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/TestAll.java new file mode 100644 index 00000000000..e3b0e06ca16 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/TestAll.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.test.api.observation; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the Observation module. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("Observation tests"); + + suite.addTestSuite(EventIteratorTest.class); + suite.addTestSuite(EventTest.class); + suite.addTestSuite(GetRegisteredEventListenersTest.class); + suite.addTestSuite(LockingTest.class); + suite.addTestSuite(NodeAddedTest.class); + suite.addTestSuite(NodeRemovedTest.class); + suite.addTestSuite(NodeMovedTest.class); + suite.addTestSuite(NodeReorderTest.class); + suite.addTestSuite(PropertyAddedTest.class); + suite.addTestSuite(PropertyChangedTest.class); + suite.addTestSuite(PropertyRemovedTest.class); + suite.addTestSuite(AddEventListenerTest.class); + suite.addTestSuite(WorkspaceOperationTest.class); + + // JCR 2.0 + + suite.addTestSuite(EventJournalTest.class); + suite.addTestSuite(GetDateTest.class); + suite.addTestSuite(GetIdentifierTest.class); + suite.addTestSuite(GetInfoTest.class); + suite.addTestSuite(GetUserDataTest.class); + + return suite; + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/observation/WorkspaceOperationTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/WorkspaceOperationTest.java similarity index 76% rename from src/test/org/apache/jackrabbit/test/api/observation/WorkspaceOperationTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/WorkspaceOperationTest.java index 7917145c91f..247b8ce9968 100644 --- a/src/test/org/apache/jackrabbit/test/api/observation/WorkspaceOperationTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/observation/WorkspaceOperationTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -22,19 +22,15 @@ /** * Tests if workspace operations trigger the appropriate observation events. - * - *

        - * Configuration requirements are:
        + *

        + * Configuration requirements: + *

        * The {@link #testRoot} must allow child nodes of type {@link #testNodeType}. * The child nodes that are created will be named {@link #nodeName1}, * {@link #nodeName2}, {@link #nodeName3} and {@link #nodeName4}. Furthermore * {@link #testNodeType} must allow to add child nodes of the same type * ({@link #testNodeType}). * - * @test - * @sources WorkspaceOperationTest.java - * @executeClass org.apache.jackrabbit.test.api.observation.WorkspaceOperationTest - * @keywords observation */ public class WorkspaceOperationTest extends AbstractObservationTest { @@ -45,13 +41,13 @@ public class WorkspaceOperationTest extends AbstractObservationTest { public void testCopy() throws RepositoryException { Node n1 = testRootNode.addNode(nodeName1, testNodeType); n1.addNode(nodeName2, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); EventResult listener = new EventResult(log); addEventListener(listener, Event.NODE_ADDED); superuser.getWorkspace().copy(testRoot + "/" + nodeName1, testRoot + "/" + nodeName3); - removeEventListener(listener); Event[] events = listener.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(events, new String[]{nodeName3, nodeName3 + "/" + nodeName2}); + removeEventListener(listener); + checkNodeAdded(events, new String[]{nodeName3, nodeName3 + "/" + nodeName2}, null); } /** @@ -61,19 +57,19 @@ public void testCopy() throws RepositoryException { public void testRename() throws RepositoryException { Node n1 = testRootNode.addNode(nodeName1, testNodeType); n1.addNode(nodeName2, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); EventResult addNodeListener = new EventResult(log); EventResult removeNodeListener = new EventResult(log); addEventListener(addNodeListener, Event.NODE_ADDED); addEventListener(removeNodeListener, Event.NODE_REMOVED); superuser.getWorkspace().move(n1.getPath(), testRoot + "/" + nodeName3); - testRootNode.save(); - removeEventListener(addNodeListener); - removeEventListener(removeNodeListener); + testRootNode.getSession().save(); Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(added, new String[]{nodeName3}); - checkNodeRemoved(removed, new String[]{nodeName1}); + removeEventListener(addNodeListener); + removeEventListener(removeNodeListener); + checkNodeAdded(added, new String[]{nodeName3}, new String[]{nodeName3 + "/" + nodeName2}); + checkNodeRemoved(removed, new String[]{nodeName1}, new String[]{nodeName1 + "/" + nodeName2}); } /** @@ -84,19 +80,19 @@ public void testMove() throws RepositoryException { Node n1 = testRootNode.addNode(nodeName1, testNodeType); Node n3 = testRootNode.addNode(nodeName3, testNodeType); n1.addNode(nodeName2, testNodeType); - testRootNode.save(); + testRootNode.getSession().save(); EventResult addNodeListener = new EventResult(log); EventResult removeNodeListener = new EventResult(log); addEventListener(addNodeListener, Event.NODE_ADDED); addEventListener(removeNodeListener, Event.NODE_REMOVED); superuser.getWorkspace().move(n1.getPath(), n3.getPath() + "/" + nodeName4); - testRootNode.save(); - removeEventListener(addNodeListener); - removeEventListener(removeNodeListener); + testRootNode.getSession().save(); Event[] added = addNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); Event[] removed = removeNodeListener.getEvents(DEFAULT_WAIT_TIMEOUT); - checkNodeAdded(added, new String[]{nodeName3 + "/" + nodeName4}); - checkNodeRemoved(removed, new String[]{nodeName1}); + removeEventListener(addNodeListener); + removeEventListener(removeNodeListener); + checkNodeAdded(added, new String[]{nodeName3 + "/" + nodeName4}, new String[]{nodeName3 + "/" + nodeName4 + "/" + nodeName2}); + checkNodeRemoved(removed, new String[]{nodeName1}, new String[]{nodeName1 + "/" + nodeName2}); } } diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractOrderByTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractOrderByTest.java new file mode 100644 index 00000000000..81007115af9 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractOrderByTest.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.test.api.query; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.Ordering; +import javax.jcr.query.qom.DynamicOperand; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; +import java.math.BigDecimal; + +/** + * Abstract base class for all order by tests. Provides utility methods. + */ +class AbstractOrderByTest extends AbstractQueryTest { + + /** If true this repository supports sql queries */ + protected boolean checkSQL; + + private String[] nodeNames; + + protected void setUp() throws Exception { + super.setUp(); + checkSQL = isSupported(Repository.OPTION_QUERY_SQL_SUPPORTED); + nodeNames = new String[]{nodeName1, nodeName2, nodeName3, nodeName4}; + } + + /** + * Populates the workspace with child nodes under testroot with + * each node has a String value set in property with name + * propertyname1. + * @param values the String values. + */ + protected void populate(String[] values) throws RepositoryException { + for (int i = 0; i < values.length; i++) { + Node node = testRootNode.addNode(nodeNames[i], testNodeType); + node.setProperty(propertyName1, values[i]); + } + superuser.save(); + } + + /** + * Populates the workspace with child nodes under testroot with + * each node has a value set in property with name + * propertyname1. The actual value is created by using the + * sessions value factory and the given type. + * + * @param values the String values. + * @param type a JCR property type. + */ + protected void populate(String[] values, int type) throws RepositoryException { + for (int i = 0; i < values.length; i++) { + Node node = testRootNode.addNode(nodeNames[i], testNodeType); + node.setProperty(propertyName1, vf.createValue(values[i], type)); + } + superuser.save(); + } + + /** + * Populates the workspace with child nodes under testroot with + * each node has a calendar value set in property with name + * propertyname1. + * @param values the calendar values. + */ + protected void populate(Calendar[] values) throws RepositoryException { + for (int i = 0; i < values.length; i++) { + Node node = testRootNode.addNode(nodeNames[i], testNodeType); + node.setProperty(propertyName1, values[i]); + } + superuser.save(); + } + + /** + * Populates the workspace with child nodes under testroot with + * each node has a long value set in property with name + * propertyname1. + * @param values the long values. + */ + protected void populate(long[] values) throws RepositoryException { + for (int i = 0; i < values.length; i++) { + Node node = testRootNode.addNode(nodeNames[i], testNodeType); + node.setProperty(propertyName1, values[i]); + } + superuser.save(); + } + + /** + * Populates the workspace with child nodes under testroot with + * each node has a double value set in property with name + * propertyname1. + * @param values the double values. + */ + protected void populate(double[] values) throws RepositoryException { + for (int i = 0; i < values.length; i++) { + Node node = testRootNode.addNode(nodeNames[i], testNodeType); + node.setProperty(propertyName1, values[i]); + } + superuser.save(); + } + + /** + * Populates the workspace with child nodes under testroot with + * each node has a decimal value set in property with name + * propertyname1. + * @param values the decimal values. + */ + protected void populate(BigDecimal[] values) throws RepositoryException { + for (int i = 0; i < values.length; i++) { + Node node = testRootNode.addNode(nodeNames[i], testNodeType); + node.setProperty(propertyName1, values[i]); + } + superuser.save(); + } + + /** + * Runs queries on the workspace and checks if the ordering is according + * to the nodeNames. + * @param nodeNames the sequence of node names required in the result set. + */ + protected void checkOrder(String[] nodeNames) throws RepositoryException { + // first check ascending + + String sql = createSQL(); + String xpath = createXPath(); + Query q; + QueryResult result; + if (sql != null) { + q = superuser.getWorkspace().getQueryManager().createQuery(sql, qsSQL); + result = q.execute(); + checkResultOrder(result, nodeNames); + } + + if (xpath != null) { + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, qsXPATH); + result = q.execute(); + checkResultOrder(result, nodeNames); + } + + q = createQOM(true); + result = q.execute(); + checkResultOrder(result, nodeNames); + + // then check descending + Collections.reverse(Arrays.asList(nodeNames)); + + if (sql != null) { + q = superuser.getWorkspace().getQueryManager().createQuery(sql + " DESC", qsSQL); + result = q.execute(); + checkResultOrder(result, nodeNames); + } + + if (xpath != null) { + q = superuser.getWorkspace().getQueryManager().createQuery(xpath + " descending", qsXPATH); + result = q.execute(); + checkResultOrder(result, nodeNames); + } + + q = createQOM(false); + result = q.execute(); + checkResultOrder(result, nodeNames); + } + + /** + * Checks if the node ordering in result is according to + * nodeNames. + * @param result the query result. + * @param nodeNames the node names. + */ + protected void checkResultOrder(QueryResult result, String[] nodeNames) + throws RepositoryException { + List nodes = new ArrayList(); + for (NodeIterator it = result.getNodes(); it.hasNext();) { + nodes.add(it.nextNode()); + } + assertEquals("Wrong hit count:", nodeNames.length, nodes.size()); + + for (int i = 0; i < nodeNames.length; i++) { + String name = nodes.get(i).getName(); + assertEquals("Wrong order of nodes:", nodeNames[i], name); + } + } + + /** + * @return a basic QOM to test order by queries. + * @throws RepositoryException if an error occurs. + */ + protected QueryObjectModel createQOM(boolean ascending) + throws RepositoryException { + DynamicOperand op = createOrderingOperand(); + Ordering ordering; + if (ascending) { + ordering = qf.ascending(op); + } else { + ordering = qf.descending(op); + } + return qf.createQuery( + qf.selector(testNodeType, "s"), + qf.descendantNode("s", testRoot), + new Ordering[]{ordering}, + null + ); + } + + /** + * @return a dynamic operand that is used in the QOM created by + * {@link #createQOM(boolean)}. + * @throws RepositoryException if an error occurs. + */ + protected DynamicOperand createOrderingOperand() + throws RepositoryException { + return qf.propertyValue("s", propertyName1); + } + + /** + * @return a basic SQL statement to test order by queries. Returns + * null if SQL is not supported. + */ + protected String createSQL() { + if (checkSQL) { + return "SELECT " + escapeIdentifierForSQL(propertyName1) + + " FROM "+ escapeIdentifierForSQL(testNodeType) + " WHERE " + + jcrPath + " LIKE '" + testRoot + "/%' ORDER BY " + + escapeIdentifierForSQL(propertyName1); + } else { + return null; + } + } + + /** + * @return a basic XPath statement to test order by queries. Returns + * null is XPath is not supported. + * @throws RepositoryException if an error occurs. + */ + protected String createXPath() throws RepositoryException { + List languages = Arrays.asList(superuser.getWorkspace().getQueryManager().getSupportedQueryLanguages()); + if (languages.contains(qsXPATH)) { + return xpathRoot + "/*[@jcr:primaryType='" + testNodeType + "'] order by @" + propertyName1; + } else { + return null; + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractQueryLevel2Test.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractQueryLevel2Test.java new file mode 100644 index 00000000000..1b05c1f3282 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractQueryLevel2Test.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.RowIterator; +import javax.jcr.query.Row; +import javax.jcr.Value; +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; + +/** + * Implements common setup methods for level 2 queries. + */ +public abstract class AbstractQueryLevel2Test extends AbstractQueryTest { + + /** + * Creates two nodes with name {@link #nodeName1} and {@link #nodeName2} + * with nodetype {@link #testNodeType}. The node type must allow a String + * property with name {@link #propertyName1} which is fulltext indexed. + */ + protected void setUpFullTextTest() throws RepositoryException { + Node node = testRootNode.addNode(nodeName1, testNodeType); + node.setProperty(propertyName1, "The quick brown fox jumps over the lazy dog."); + + node = testRootNode.addNode(nodeName2, testNodeType); + node.setProperty(propertyName1, "The quick brown cat jumps over the lazy dog."); + testRootNode.getSession().save(); + } + + /** + * Creates three nodes with names: {@link #nodeName1}, {@link #nodeName2} + * and {@link #nodeName3}. All nodes are of node type {@link #testNodeType}. + * the node type must allow a String property with name {@link + * #propertyName1}. + */ + protected void setUpRangeTest() throws RepositoryException { + Node node = testRootNode.addNode(nodeName1, testNodeType); + node.setProperty(propertyName1, "a"); + + node = testRootNode.addNode(nodeName2, testNodeType); + node.setProperty(propertyName1, "b"); + + Node cNode = node.addNode(nodeName3, testNodeType); + cNode.setProperty(propertyName1, "c"); + testRootNode.getSession().save(); + } + + /** + * Creates three nodes with names: {@link #nodeName1}, {@link #nodeName2} + * and {@link #nodeName3}. All nodes are of node type {@link #testNodeType}. + * the node type must allow a String property with name {@link + * #propertyName1} and a multi valued String property with name {@link + * #propertyName2}. + *

        + * If the node type does not support multi values for {@link #propertyName2} + * a {@link org.apache.jackrabbit.test.NotExecutableException} is thrown. + */ + protected void setUpMultiValueTest() throws RepositoryException, NotExecutableException { + // check if NodeType supports mvp + NodeType nt = superuser.getWorkspace().getNodeTypeManager().getNodeType(testNodeType); + Value[] testValue = new Value[]{superuser.getValueFactory().createValue("one"), superuser.getValueFactory().createValue("two"), superuser.getValueFactory().createValue("three")}; + if (!nt.canSetProperty(propertyName2, testValue)) { + throw new NotExecutableException("Property " + propertyName2 + " of NodeType " + testNodeType + " does not allow multi values"); + } + + Node node = testRootNode.addNode(nodeName1, testNodeType); + node.setProperty(propertyName1, "existence"); + node.setProperty(propertyName2, testValue); + + node = testRootNode.addNode(nodeName2, testNodeType); + node.setProperty(propertyName1, "nonexistence"); + node.setProperty(propertyName2, new String[]{"one", "three"}); + + Node cNode = node.addNode(nodeName3, testNodeType); + cNode.setProperty(propertyName1, "existence"); + testRootNode.getSession().save(); + } + + /** + * Tests if all results contain only the searched value is contained in the + * selected property + * + * @param itr rows of the query result. + * @param propertyName selected property, that should contain the value. + * @param expectedValue the value that is expected to be found + */ + protected void checkValue(RowIterator itr, + String propertyName, + String expectedValue) throws RepositoryException { + while (itr.hasNext()) { + Row row = itr.nextRow(); + // check fullText + Value value = row.getValue(propertyName); + if (value == null) { + fail("Search Test: fails result does not contain value for selected property"); + } + assertEquals("Value in query result row does not match expected value", + expectedValue, value.getString()); + } + } + + /** + * Checks if all nodes in itr have a property with name + * propertyName and have the expectedValue. + * + * @param itr the nodes to check. + * @param propertyName the name of the property. + * @param expectedValue the exected value of the property. + * @throws RepositoryException if an error occurs. + */ + protected void checkValue(NodeIterator itr, + String propertyName, + String expectedValue) throws RepositoryException { + while (itr.hasNext()) { + Node node = itr.nextNode(); + // check fullText + Value value = node.getProperty(propertyName).getValue(); + assertEquals("Value in query result row does not match expected value", + expectedValue, value.getString()); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractQueryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractQueryTest.java new file mode 100644 index 00000000000..97c788a87f2 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/AbstractQueryTest.java @@ -0,0 +1,437 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.query; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.util.ISO9075; + +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import javax.jcr.ValueFactory; + +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Abstract base class for query test cases. + */ +public abstract class AbstractQueryTest extends AbstractJCRTest { + + /** + * Resolved Name for jcr:score + */ + protected String jcrScore; + + /** + * Resolved Name for jcr:path + */ + protected String jcrPath; + + /** + * Resolved Name for jcr:root + */ + protected String jcrRoot; + + /** + * Resolved Name for jcr:contains + */ + protected String jcrContains; + + /** + * Resolved Name for jcr:deref + */ + protected String jcrDeref; + + /** + * The string /${jcrRoot}${testRoot} with all components of the test path + * properly escaped for XPath. + * + * @see JCR-714 + */ + protected String xpathRoot; + + /** + * The query object model factory for {@link #superuser}. + */ + protected QueryObjectModelFactory qf; + + /** + * The value factory for creating literals for the query object model. + */ + protected ValueFactory vf; + + /** + * The query manager for {@link #superuser} + */ + protected QueryManager qm; + + /** + * The identifier for the "XPATH" query syntax + */ + @SuppressWarnings("deprecation") + protected String qsXPATH = Query.XPATH; + + /** + * The identifier for the "SQL" query syntax + */ + @SuppressWarnings("deprecation") + protected String qsSQL = Query.SQL; + + /** + * Set-up the configuration values used for the test. Per default retrieves + * a session, configures testRoot, and nodetype and checks if the query + * language for the current language is available.
        + */ + protected void setUp() throws Exception { + super.setUp(); + jcrScore = superuser.getNamespacePrefix(NS_JCR_URI) + ":score"; + jcrPath = superuser.getNamespacePrefix(NS_JCR_URI) + ":path"; + jcrRoot = superuser.getNamespacePrefix(NS_JCR_URI) + ":root"; + jcrContains = superuser.getNamespacePrefix(NS_JCR_URI) + ":contains"; + jcrDeref = superuser.getNamespacePrefix(NS_JCR_URI) + ":deref"; + xpathRoot = "/" + jcrRoot + ISO9075.encodePath(testRoot); + qm = superuser.getWorkspace().getQueryManager(); + qf = qm.getQOMFactory(); + vf = superuser.getValueFactory(); + } + + protected void tearDown() throws Exception { + qm = null; + qf = null; + vf = null; + super.tearDown(); + } + + /** + * Create a {@link Query} for a given {@link Statement}. + * + * @param statement the query should be created for + * @return + * + * @throws RepositoryException + * @see #createQuery(String, String) + */ + protected Query createQuery(Statement statement) + throws RepositoryException, NotExecutableException { + return createQuery(statement.getStatement(), statement.getLanguage()); + } + + /** + * Creates a {@link Query} for the given statement in the requested + * language, treating optional languages gracefully + * @throws RepositoryException + */ + protected Query createQuery(String statement, String language) throws RepositoryException, NotExecutableException { + return createQuery(superuser, statement, language); + } + + /** + * Creates a {@link Query} for the given statement in the requested + * language, treating optional languages gracefully + * @throws RepositoryException + */ + protected Query createQuery(Session session, String statement, String language) throws RepositoryException, NotExecutableException { + log.println("Creating query: " + statement); + + // check for unsupported query languages early + if (! isSupportedLanguage(language) && !Query.JCR_SQL2.equals(language)) { + throw new NotExecutableException("Repository does not support " + language + " query syntax"); + } + return session.getWorkspace().getQueryManager().createQuery(statement, language); + } + + /** + * Creates and executes a {@link Query} for the given {@link Statement} + * + * @param statement to execute + * @return + * + * @throws RepositoryException + * @see #execute(String, String) + */ + protected QueryResult execute(Statement statement) + throws RepositoryException, NotExecutableException { + return execute(statement.getStatement(), statement.getLanguage()); + } + + /** + * Creates and executes a {@link Query} for a given Statement in a given + * query language + * + * @param statement the query should be build for + * @param language query language the stement is written in + * @return + * + * @throws RepositoryException + */ + protected QueryResult execute(String statement, String language) + throws RepositoryException, NotExecutableException { + Query query = createQuery(statement, language); + return query.execute(); + } + + /** + * Checks if the result contains a number of + * hits. + * + * @param result the QueryResult. + * @param hits the number of expected hits. + * @throws RepositoryException if an error occurs while iterating over the + * result nodes. + */ + protected void checkResult(QueryResult result, int hits) + throws RepositoryException { + RowIterator itr = result.getRows(); + long count = itr.getSize(); + if (count == 0) { + log.println(" NONE"); + } else if (count == -1) { + // have to count in a loop + count = 0; + while (itr.hasNext()) { + itr.nextRow(); + count++; + } + } + assertEquals("Wrong hit count.", hits, count); + } + + /** + * Checks if the result contains a number of hits + * and properties. + * + * @param result the QueryResult. + * @param hits the number of expected hits. + * @param properties the number of expected properties. + * @throws RepositoryException if an error occurs while iterating over the + * result nodes. + */ + protected void checkResult(QueryResult result, int hits, int properties) + throws RepositoryException { + checkResult(result, hits); + // now check property count + int count = 0; + log.println("Properties:"); + String[] propNames = result.getColumnNames(); + for (RowIterator it = result.getRows(); it.hasNext();) { + StringBuffer msg = new StringBuffer(); + Value[] values = it.nextRow().getValues(); + for (int i = 0; i < propNames.length; i++, count++) { + msg.append(" ").append(propNames[i]).append(": "); + if (values[i] == null) { + msg.append("null"); + } else { + msg.append(values[i].getString()); + } + } + log.println(msg); + } + if (count == 0) { + log.println(" NONE"); + } + assertEquals("Wrong property count.", properties, count); + } + + /** + * Checks if the {@link QueryResult} is ordered according order property in + * direction of related argument. + * + * @param queryResult to be tested + * @param propName Name of the porperty to order by + * @param descending if true order has to be descending + * @throws RepositoryException + * @throws NotExecutableException in case of less than two results or all + * results have same size of value in its + * order-property + */ + protected void evaluateResultOrder(QueryResult queryResult, String propName, + boolean descending) + throws RepositoryException, NotExecutableException { + NodeIterator nodes = queryResult.getNodes(); + if (getSize(nodes) < 2) { + fail("Workspace does not contain sufficient content to test ordering on result nodes."); + } + // need to re-aquire nodes, {@link #getSize} may consume elements. + nodes = queryResult.getNodes(); + int changeCnt = 0; + String last = descending ? "\uFFFF" : ""; + while (nodes.hasNext()) { + String value = nodes.nextNode().getProperty(propName).getString(); + int cp = value.compareTo(last); + // if value changed evaluate if the ordering is correct + if (cp != 0) { + changeCnt++; + if (cp > 0 && descending) { + fail("Repository doesn't order properly descending"); + } else if (cp < 0 && !descending) { + fail("Repository doesn't order properly ascending"); + } + } + last = value; + } + if (changeCnt < 1) { + fail("Workspace does not contain distinct values for " + propName); + } + } + + /** + * Executes the xpath query and checks the results against + * the specified nodes. + * @param session the session to use for the query. + * @param xpath the xpath query. + * @param expectedNodes the expected nodes. + * @throws NotExecutableException + */ + protected void executeXPathQuery(Session session, String xpath, Node[] expectedNodes) + throws RepositoryException, NotExecutableException { + QueryResult res = createQuery(session, xpath, qsXPATH).execute(); + checkResult(res, expectedNodes, null); + } + + /** + * Executes the sql query and checks the results against + * the specified nodes. + * @param session the session to use for the query. + * @param sql the sql query. + * @param expectedNodes the expected nodes. + * @throws NotExecutableException + */ + protected void executeSqlQuery(Session session, String sql, Node[] expectedNodes) + throws RepositoryException, NotExecutableException { + executeSqlQuery(session, sql, expectedNodes, null); + } + + /** + * Executes the sql query and checks the results against + * the specified nodes. + * @param session the session to use for the query. + * @param sql the sql query. + * @param requiredNodes the nodes that need to be in the result set + * (null if no node is required). + * @param optionalNodes the nodes that may be in the result set + * (null if no node is optional). + * @throws NotExecutableException + */ + protected void executeSqlQuery(Session session, String sql, Node[] requiredNodes, Node[] optionalNodes) + throws RepositoryException, NotExecutableException { + QueryResult res = createQuery(session, sql, qsSQL).execute(); + checkResult(res, requiredNodes, optionalNodes); + } + + /** + * Checks if the result set contains exactly the nodes. + * @param result the query result. + * @param expectedNodes the expected nodes. + */ + protected void checkResult(QueryResult result, Node[] expectedNodes) + throws RepositoryException { + checkResult(result, expectedNodes, null); + } + + /** + * Checks if the result set contains exactly the nodes. + * @param result the query result. + * @param requiredNodes the nodes that need to be in the result set + * (null if no node is required). + * @param optionalNodes the nodes that may be in the result set + * (null if no node is optional). + */ + protected void checkResult(QueryResult result, Node[] requiredNodes, Node[] optionalNodes) + throws RepositoryException { + // collect paths + Set requiredPaths = getPathSet(requiredNodes); + Set optionalPaths = getPathSet(optionalNodes); + Set resultPaths = new HashSet(); + for (NodeIterator it = result.getNodes(); it.hasNext();) { + resultPaths.add(it.nextNode().getPath()); + } + // check if all required nodes are in result + for (Iterator it = requiredPaths.iterator(); it.hasNext();) { + String path = it.next(); + assertTrue(path + " is not part of the result set", resultPaths.contains(path)); + } + // check result does not contain more than expected + for (Iterator it = resultPaths.iterator(); it.hasNext();) { + String path = it.next(); + if (!optionalPaths.contains(path)) { + assertTrue(path + " is not expected to be part of the result set", requiredPaths.contains(path)); + } + } + } + + private static HashSet getPathSet(Node[] nodes) throws RepositoryException { + HashSet paths = new HashSet(); + if (nodes != null) { + for (int i = 0; i < nodes.length; i++) { + paths.add(nodes[i].getPath()); + } + } + return paths; + } + + /** + * Returns the nodes in it as an array of Nodes. + * @param it the NodeIterator. + * @return the elements of the iterator as an array of Nodes. + */ + protected Node[] toArray(NodeIterator it) { + List nodes = new ArrayList(); + while (it.hasNext()) { + nodes.add(it.nextNode()); + } + return nodes.toArray(new Node[nodes.size()]); + } + + /** + * Escape an identifier suitable for the SQL parser + *

        + * TODO: currently only handles dash character + */ + protected String escapeIdentifierForSQL(String identifier) { + + boolean needsEscaping = identifier.indexOf('-') >= 0; + + if (!needsEscaping) { + return identifier; + } + return '"' + identifier + '"'; + } + + /** + * @param language a query language. + * @return true if language is supported; + * false otherwise. + * @throws RepositoryException if an error occurs. + */ + protected boolean isSupportedLanguage(String language) + throws RepositoryException { + return Arrays.asList(qm.getSupportedQueryLanguages()).contains(language); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/CreateQueryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/CreateQueryTest.java new file mode 100644 index 00000000000..51dedf958db --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/CreateQueryTest.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.test.api.query; + +import java.util.List; +import java.util.Arrays; + +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryManager; +import javax.jcr.query.InvalidQueryException; + +/** + * CreateQueryTest checks if {@link QueryManager#createQuery(String, String)} + * throws an {@link InvalidQueryException} for an unknown query language. + */ +public class CreateQueryTest extends AbstractQueryTest { + + public void testUnknownQueryLanguage() throws RepositoryException { + List supported = Arrays.asList(qm.getSupportedQueryLanguages()); + String language; + do { + language = createRandomString(5); + } while (supported.contains(language)); + try { + qm.createQuery("foo", language); + fail("createQuery() must throw for unknown query language: " + language); + } catch (InvalidQueryException e) { + // expected + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/DerefQueryLevel1Test.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/DerefQueryLevel1Test.java new file mode 100644 index 00000000000..df97e628bf7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/DerefQueryLevel1Test.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.test.api.query; + +import org.apache.jackrabbit.test.api.PropertyUtil; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.PropertyType; +import javax.jcr.Property; +import javax.jcr.Node; +import javax.jcr.Value; +import java.util.List; +import java.util.ArrayList; + +/** + * Tests the XPath function jcr:deref() in a level 1 repository. + * + */ +public class DerefQueryLevel1Test extends AbstractQueryTest { + + /** A read-only session */ + private Session session; + + /** + * Sets up the test cases + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + } + + /** + * Releases the session acquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Test a deref query on a single valued reference property with a node test. + * @throws NotExecutableException if the workspace does not have sufficient + * content. + */ + public void testDerefSinglePropWithNodeTest() + throws RepositoryException, NotExecutableException { + Property refProp = PropertyUtil.searchProp(session, testRootNode, PropertyType.REFERENCE, Boolean.FALSE); + if (refProp == null) { + throw new NotExecutableException("Workspace does not contain a node with a reference property."); + } + Node target = refProp.getNode(); + String xpath = createStatement(refProp, target.getName()); + executeDerefQuery(session, xpath, new Node[]{target}); + } + + /** + * Test a deref query on a single valued reference property with a '*' node + * test. + * @throws NotExecutableException if the workspace does not have sufficient + * content. + */ + public void testDerefSinglePropWithNodeStar() + throws RepositoryException, NotExecutableException { + Property refProp = PropertyUtil.searchProp(session, testRootNode, PropertyType.REFERENCE, Boolean.FALSE); + if (refProp == null) { + throw new NotExecutableException("Workspace does not contain a node with a reference property."); + } + Node target = refProp.getNode(); + String xpath = createStatement(refProp, "*"); + executeDerefQuery(session, xpath, new Node[]{target}); + } + + /** + * Test a deref query on a multi valued reference property with a node test. + * @throws NotExecutableException if the workspace does not have sufficient + * content. + */ + public void testDerefMultiPropWithNodeTest() + throws RepositoryException, NotExecutableException { + Property refProp = PropertyUtil.searchMultivalProp(testRootNode, PropertyType.REFERENCE); + if (refProp == null) { + throw new NotExecutableException("Workspace does not contain a node with a multivalue reference property."); + } + Value[] targets = refProp.getValues(); + Node[] targetNodes = new Node[targets.length]; + for (int i = 0; i < targets.length; i++) { + targetNodes[i] = session.getNodeByUUID(targets[i].getString()); + } + if (targetNodes.length == 0) { + throw new NotExecutableException("Reference property does not contain a value"); + } + String nodeName = targetNodes[0].getName(); + List resultNodes = new ArrayList(); + for (int i = 0; i < targetNodes.length; i++) { + if (targetNodes[i].getName().equals(nodeName)) { + resultNodes.add(targetNodes[i]); + } + } + targetNodes = resultNodes.toArray(new Node[resultNodes.size()]); + String xpath = createStatement(refProp, nodeName); + executeDerefQuery(session, xpath, targetNodes); + } + + /** + * Test a deref query on a multi valued reference property with a '*' node. + * @throws NotExecutableException if the workspace does not have sufficient + * content. + */ + public void testDerefMultiPropWithNodeStar() + throws RepositoryException, NotExecutableException { + Property refProp = PropertyUtil.searchMultivalProp(testRootNode, PropertyType.REFERENCE); + if (refProp == null) { + throw new NotExecutableException("Workspace does not contain a node with a multivalue reference property."); + } + Value[] targets = refProp.getValues(); + Node[] targetNodes = new Node[targets.length]; + for (int i = 0; i < targets.length; i++) { + targetNodes[i] = session.getNodeByUUID(targets[i].getString()); + } + if (targetNodes.length == 0) { + throw new NotExecutableException("Reference property does not contain a value"); + } + String xpath = createStatement(refProp, "*"); + executeDerefQuery(session, xpath, targetNodes); + } + + //----------------------------< internal >---------------------------------- + + /** + * Creates a xpath deref statement with a reference property and a nametest. + * @param refProperty the reference property. + * @param nameTest the nametest. + * @return the xpath statement. + */ + private String createStatement(Property refProperty, String nameTest) + throws RepositoryException { + StringBuffer stmt = new StringBuffer(); + stmt.append("/").append(jcrRoot).append(refProperty.getParent().getPath()); + stmt.append("/").append(jcrDeref).append("(@"); + stmt.append(refProperty.getName()).append(", '"); + stmt.append(nameTest).append("')"); + return stmt.toString(); + } + + /** + * Executes the xpath query and checks the results against the + * specified nodes. + * + * @param session the session to use for the query. + * @param xpath the xpath query. + * @param nodes the expected result nodes. + * @throws NotExecutableException if this repository does not support the + * jcr:deref() function. + */ + private void executeDerefQuery(Session session, + String xpath, + Node[] nodes) throws NotExecutableException { + try { + executeXPathQuery(session, xpath, nodes); + } catch (RepositoryException e) { + // assume jcr:deref() is not supported + throw new NotExecutableException(e.getMessage()); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/ElementTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/ElementTest.java new file mode 100644 index 00000000000..a4fc8993758 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/ElementTest.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; + +/** + * Tests the element test function in XPath. + *

          + *
        • {@code testroot} path to node that allows child nodes of type + * nodetype and nt:base. + *
        • {@code nodetype} node type name for nodes to create + *
        • {@code nodename1} node name for a child node of type + * nodetype or nt:base + *
        • {@code nodename2} node name for a child node of type + * nodetype or nt:base + *
        • {@code nodename3} node name for a child node of type + * nodetype or nt:base + *
        + */ +public class ElementTest extends AbstractQueryTest { + + private String simpleNodeType; + + protected void setUp() throws Exception { + super.setUp(); + simpleNodeType = testNodeTypeNoChildren == null ? ntBase : testNodeTypeNoChildren; + } + + /** + * Tests the element test without arguments. + * @throws NotExecutableException + */ + public void testElementTest() throws RepositoryException, NotExecutableException { + + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, simpleNodeType); + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + + String query = "/" + jcrRoot + testRoot + "/element()"; + executeXPathQuery(superuser, query, new Node[]{n1, n2, n3}); + } + + /** + * Tests the element test with one any node argument. + * @throws NotExecutableException + */ + public void testElementTestAnyNode() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, simpleNodeType); + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + + String query = "/" + jcrRoot + testRoot + "/element(*)"; + executeXPathQuery(superuser, query, new Node[]{n1, n2, n3}); + } + + /** + * Tests the element test with an any node argument and a type argument + * that matches all nodes (nt:base). + * @throws NotExecutableException + */ + public void testElementTestAnyNodeNtBase() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, simpleNodeType); + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + + String query = "/" + jcrRoot + testRoot + "/element(*, " + ntBase + ")"; + executeXPathQuery(superuser, query, new Node[]{n1, n2, n3}); + } + + /** + * Tests the element test with an any node argument and a type argument + * that matches only certain child nodes. + * @throws NotExecutableException + */ + public void testElementTestAnyNodeSomeNT() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, simpleNodeType); + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + + String query = "/" + jcrRoot + testRoot + "/element(*, " + testNodeType + ")"; + executeXPathQuery(superuser, query, new Node[]{n1, n3}); + } + + /** + * Tests the element test with one single name test argument. + * @throws NotExecutableException + */ + public void testElementTestNameTest() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, simpleNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + + String query = "/" + jcrRoot + testRoot + "/element(" + nodeName1 + ")"; + executeXPathQuery(superuser, query, new Node[]{n1}); + } + + /** + * Tests the element test with a name test argument and a type argument that + * matches all nodes (nt:base). + * @throws NotExecutableException + */ + public void testElementTestNameTestNtBase() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, simpleNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + + String query = "/" + jcrRoot + testRoot + "/element(" + nodeName1 + ", " + ntBase + ")"; + executeXPathQuery(superuser, query, new Node[]{n1}); + } + + /** + * Tests the element test with a name test argument and a type argument that + * matches only certain child nodes. + * @throws NotExecutableException + */ + public void testElementTestNameTestSomeNT() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, simpleNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + + String query = "/" + jcrRoot + testRoot + "/element(" + nodeName1 + ", " + testNodeType + ")"; + executeXPathQuery(superuser, query, new Node[]{n1}); + } + + /** + * Tests the element test with a name test argument and a type argument that + * matches only certain child nodes. Additonally this test requires that + * testroot allows same name sibling child nodes. + */ + public void testElementTestNameTestSomeNTWithSNS() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + if (!n1.getDefinition().allowsSameNameSiblings()) { + throw new NotExecutableException("Node at " + testRoot + " does not allow same name siblings with name " + nodeName1); + } + testRootNode.addNode(nodeName1, simpleNodeType); + Node n2 = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, simpleNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + + String query = "/" + jcrRoot + testRoot + "/element(" + nodeName1 + ", " + testNodeType + ")"; + executeXPathQuery(superuser, query, new Node[]{n1, n2}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetLanguageTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetLanguageTest.java new file mode 100644 index 00000000000..05e505cd839 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetLanguageTest.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.test.api.query; + +import javax.jcr.query.Query; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * Test the method {@link Query#getLanguage()}. + * + */ +public class GetLanguageTest extends AbstractQueryTest { + + /** A read-only session */ + private Session session; + + /** + * Sets up the test cases + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + } + + /** + * Releases the session acquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Tests if a XPath query returns {@link Query#XPATH} when calling + * {@link Query#getLanguage()}. + */ + public void testGetLanguage() throws RepositoryException { + String statement = "/" + jcrRoot; + Query q = session.getWorkspace().getQueryManager().createQuery(statement, qsXPATH); + assertEquals("Query returns wrong language.", qsXPATH, q.getLanguage()); + } + + /** + * Tests if a SQL query returns {@link Query#SQL} when calling + * {@link Query#getLanguage()}. + */ + public void testSQL() throws RepositoryException, NotExecutableException { + if (isSupportedLanguage(qsSQL)) { + String stmt = "select * from " + testNodeType; + Query q = session.getWorkspace().getQueryManager().createQuery(stmt, qsSQL); + assertEquals("Query returns wrong language.", qsSQL, q.getLanguage()); + } else { + throw new NotExecutableException("SQL not supported"); + } + } + + /** + * Tests if a JCR_SQL2 query returns {@link Query#JCR_SQL2} when calling + * {@link Query#getLanguage()}. + */ + public void testJCRSQL2() throws RepositoryException { + String stmt = "SELECT * FROM [" + testNodeType + "]"; + Query q = session.getWorkspace().getQueryManager().createQuery(stmt, Query.JCR_SQL2); + assertEquals("Query returns wrong language.", Query.JCR_SQL2, q.getLanguage()); + } + + /** + * Tests if a query object model returns {@link Query#JCR_JQOM} when calling + * {@link Query#getLanguage()}. + */ + public void testJCRQOM() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + null, null, null + ); + assertEquals("Query returns wrong language.", Query.JCR_JQOM, qom.getLanguage()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPersistentQueryPathLevel1Test.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPersistentQueryPathLevel1Test.java new file mode 100644 index 00000000000..c72714bec30 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPersistentQueryPathLevel1Test.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.test.api.query; + +import javax.jcr.query.Query; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ItemNotFoundException; + +/** + * Test the method {@link Query#getStoredQueryPath()}. + * + */ +public class GetPersistentQueryPathLevel1Test extends AbstractQueryTest { + + /** A read-only session */ + private Session session; + + /** + * Sets up the test cases + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + } + + /** + * Releases the session acquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Tests if a non-persistent query throws an {@link ItemNotFoundException} + * when {@link Query#getStoredQueryPath()} is called. + */ + public void testGetStoredQueryPath() throws RepositoryException { + String statement = "/" + jcrRoot; + Query q = session.getWorkspace().getQueryManager().createQuery(statement, qsXPATH); + try { + q.getStoredQueryPath(); + fail("Query.getStoredQueryPath() on a transient query must throw an ItemNotFoundException."); + } catch (ItemNotFoundException e) { + // success + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPersistentQueryPathTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPersistentQueryPathTest.java new file mode 100644 index 00000000000..f64c56d16bc --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPersistentQueryPathTest.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.query.Query; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +/** + * Test the method {@link javax.jcr.query.Query#getStoredQueryPath()}. + *
          + *
        • {@code testroot} node that allows to create a child node of type nt:query. + *
        • {@code nodename1} name of an nt:query node that can be created below the + * testroot. + *
        + */ +public class GetPersistentQueryPathTest extends AbstractQueryTest { + + /** + * Tests if {@link Query#getStoredQueryPath()} returns the correct path + * where the query had been saved. + * + * @throws NotExecutableException if the repository does not support the + * node type nt:query. + */ + public void testGetPersistentQueryPath() throws RepositoryException, NotExecutableException { + try { + superuser.getWorkspace().getNodeTypeManager().getNodeType(ntQuery); + } catch (NoSuchNodeTypeException e) { + // not supported + throw new NotExecutableException("repository does not support nt:query"); + } + String statement = "/" + jcrRoot; + Query q = superuser.getWorkspace().getQueryManager().createQuery(statement, qsXPATH); + String path = testRoot + "/" + nodeName1; + q.storeAsNode(path); + assertEquals("Query.getPersistentQueryPath() does not return the correct path.", + path, + q.getStoredQueryPath()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPropertyNamesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPropertyNamesTest.java new file mode 100644 index 00000000000..d423442df20 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetPropertyNamesTest.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.test.api.query; + +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.PropertyDefinition; +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; + +/** + * Tests if the property names of an XPath query without a jcr:primaryType + * predicate matches the ones declared in nt:base. + * + */ +public class GetPropertyNamesTest extends AbstractQueryTest { + + /** A read-only session */ + private Session session; + + /** + * Sets up the test cases + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + } + + /** + * Releases the session acquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Check if the property names from the search results match the + * non-residual ones from the base node type + */ + public void testGetPropertyNames() throws RepositoryException { + String queryStatement = "/" + jcrRoot; + + // build and execute search query + Query query = superuser.getWorkspace().getQueryManager().createQuery(queryStatement, qsXPATH); + QueryResult result = query.execute(); + + // Get the node's non-residual properties + PropertyDefinition[] pd = superuser.getWorkspace().getNodeTypeManager().getNodeType(ntBase).getDeclaredPropertyDefinitions(); + + List singleValPropNames = new ArrayList(); + for (int i = 0; i < pd.length; i++) { + // only keep the single-value properties + if (!pd[i].isMultiple()) { + singleValPropNames.add(pd[i].getName()); + } + } + // add jcr:path + singleValPropNames.add(jcrPath); + singleValPropNames.add(jcrScore); + + String[] foundPropertyNames = result.getColumnNames(); + Object[] realPropertyNames = singleValPropNames.toArray(); + + // sort the 2 arrays before comparing them + Arrays.sort(foundPropertyNames); + Arrays.sort(realPropertyNames); + + assertTrue("Property names don't match", Arrays.equals(foundPropertyNames, realPropertyNames)); + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/query/GetStatementTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetStatementTest.java similarity index 75% rename from src/test/org/apache/jackrabbit/test/api/query/GetStatementTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetStatementTest.java index 7fc5a4b9351..c76f8f5eb06 100644 --- a/src/test/org/apache/jackrabbit/test/api/query/GetStatementTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetStatementTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -23,10 +23,6 @@ /** * Tests the method {@link Query#getStatement()}. * - * @test - * @sources GetStatementTest.java - * @executeClass org.apache.jackrabbit.test.api.query.GetStatementTest - * @keywords level1 */ public class GetStatementTest extends AbstractQueryTest { @@ -39,7 +35,7 @@ public class GetStatementTest extends AbstractQueryTest { protected void setUp() throws Exception { isReadOnly = true; super.setUp(); - session = helper.getReadOnlySession(); + session = getHelper().getReadOnlySession(); testRootNode = session.getRootNode().getNode(testPath); } @@ -49,6 +45,7 @@ protected void setUp() throws Exception { protected void tearDown() throws Exception { if (session != null) { session.logout(); + session = null; } super.tearDown(); } @@ -59,7 +56,7 @@ protected void tearDown() throws Exception { */ public void testGetStatement() throws RepositoryException { String statement = "/" + jcrRoot + "/foo"; - Query q = session.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + Query q = session.getWorkspace().getQueryManager().createQuery(statement, qsXPATH); assertEquals("Statement returned by Query.getStatement() is not equal to the initial statement.", statement, q.getStatement()); diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetSupportedQueryLanguagesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetSupportedQueryLanguagesTest.java new file mode 100644 index 00000000000..45456459b06 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/GetSupportedQueryLanguagesTest.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.test.api.query; + +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Repository; +import java.util.List; +import java.util.Arrays; + +/** + * Test the method {@link QueryManager#getSupportedQueryLanguages()}. + * + */ +public class GetSupportedQueryLanguagesTest extends AbstractQueryTest { + + /** A read-only session */ + private Session session; + + /** + * Sets up the test cases + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + } + + /** + * Releases the session acquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Tests if all implementations return {@link Query#JCR_SQL2} with + * {@link QueryManager#getSupportedQueryLanguages()}. Tests if repositores + * that have the SQL descriptor set in the repository return {@link Query#SQL}. + */ + public void testGetSupportedQueryLanguages() throws RepositoryException { + List langs = Arrays.asList(session.getWorkspace().getQueryManager().getSupportedQueryLanguages()); + // all repositories must support XPath + assertTrue("JCR_SQL2 not returnd with QueryManager.getSupportedQueryLanguages(), got: " + langs, + langs.contains(Query.JCR_SQL2)); + + // if repository descriptor for sql is present also sql must be returned + if (isSupported(Repository.OPTION_QUERY_SQL_SUPPORTED)) { + assertTrue("SQL not returned with QueryManager.getSupportedQueryLanguages(), got: " + langs, + langs.contains(qsSQL)); + } + + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/query/OrderByDateTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDateTest.java similarity index 80% rename from src/test/org/apache/jackrabbit/test/api/query/OrderByDateTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDateTest.java index 827d6a60eb0..6791112bad1 100644 --- a/src/test/org/apache/jackrabbit/test/api/query/OrderByDateTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDateTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,20 +21,16 @@ /** * Test cases for order by queries on date properties. - * - * @tck.config testroot path to node that accepts child nodes of type + *
          + *
        • {@code testroot} path to node that accepts child nodes of type * nodetype - * @tck.config nodetype name of a node type - * @tck.config nodename1 name of a child node of type nodetype - * @tck.config nodename2 name of a child node of type nodetype - * @tck.config nodename3 name of a child node of type nodetype - * @tck.config nodename4 name of a child node of type nodetype - * @tck.config propertyname1 name of a single value calendar property. - * - * @test - * @sources OrderByDateTest.java - * @executeClass org.apache.jackrabbit.test.api.query.OrderByDateTest - * @keywords level2 + *
        • {@code nodetype} name of a node type + *
        • {@code nodename1} name of a child node of type nodetype + *
        • {@code nodename2} name of a child node of type nodetype + *
        • {@code nodename3} name of a child node of type nodetype + *
        • {@code nodename4} name of a child node of type nodetype + *
        • {@code propertyname1} name of a single value calendar property. + *
        */ public class OrderByDateTest extends AbstractOrderByTest { diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDecimalTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDecimalTest.java new file mode 100644 index 00000000000..ff2e2403437 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDecimalTest.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.test.api.query; + +import java.math.BigDecimal; + +import javax.jcr.RepositoryException; + +/** + * OrderByDecimalTest tests order by queries with decimal properties. + */ +public class OrderByDecimalTest extends AbstractOrderByTest { + + public void testDecimal() throws RepositoryException { + populate(new BigDecimal[]{new BigDecimal(0), new BigDecimal(-1), new BigDecimal(1), new BigDecimal(5)}); + checkOrder(new String[]{nodeName2, nodeName1, nodeName3, nodeName4}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDoubleTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDoubleTest.java new file mode 100644 index 00000000000..7fc8d2cc8a4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByDoubleTest.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.test.api.query; + +/** + * Test cases for order by queries on double properties. + *
          + *
        • {@code testroot} path to node that accepts child nodes of type + * nodetype + *
        • {@code nodetype} name of a node type + *
        • {@code nodename1} name of a child node of type nodetype + *
        • {@code nodename2} name of a child node of type nodetype + *
        • {@code nodename3} name of a child node of type nodetype + *
        • {@code nodename4} name of a child node of type nodetype + *
        • {@code propertyname1} name of a single value double property. + *
        + */ +public class OrderByDoubleTest extends AbstractOrderByTest { + + /** + * Tests order by queries with double properties. + */ + public void testDoubleOrder1() throws Exception { + populate(new double[]{-2.4, 4.3, 0.0}); + checkOrder(new String[]{nodeName1, nodeName3, nodeName2}); + } + + /** + * Tests order by queries with double properties. + */ + public void testDoubleOrder2() throws Exception { + populate(new double[]{-1.5, -1.4, -1.39}); + checkOrder(new String[]{nodeName1, nodeName2, nodeName3}); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLengthTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLengthTest.java new file mode 100644 index 00000000000..5a07b1d3080 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLengthTest.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.test.api.query; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.DynamicOperand; + +/** + * OrderByLengthTest contains test cases for order by queries on + * property lengths. + */ +public class OrderByLengthTest extends AbstractOrderByTest { + + public void testLength() throws RepositoryException { + populate(new String[]{"abc", "d", "efgh", "ij"}); + checkOrder(new String[]{nodeName2, nodeName4, nodeName1, nodeName3}); + } + + protected DynamicOperand createOrderingOperand() + throws RepositoryException { + return qf.length(qf.propertyValue("s", propertyName1)); + } + + protected String createSQL() { + // no SQL equivalent + return null; + } + + protected String createXPath() throws RepositoryException { + // no SQL equivalent + return null; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLocalNameTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLocalNameTest.java new file mode 100644 index 00000000000..e4eb4a5af51 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLocalNameTest.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.test.api.query; + +import java.util.Arrays; +import java.util.Comparator; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.DynamicOperand; + +/** + * OrderByLocalNameTest contains test cases for order by queries + * on local node names. + */ +public class OrderByLocalNameTest extends AbstractOrderByTest { + + public void testLocalName() throws RepositoryException { + populate(new String[]{"a", "a", "a", "a"}); // dummy values + String[] names = new String[]{nodeName1, nodeName2, nodeName3, nodeName4}; + Arrays.sort(names, new Comparator() { + public int compare(String o1, String o2) { + String s1 = getLocalName(o1); + String s2 = getLocalName(o2); + return s1.compareTo(s2); + } + }); + checkOrder(names); + } + + protected DynamicOperand createOrderingOperand() + throws RepositoryException { + return qf.nodeLocalName("s"); + } + + protected String createSQL() { + // no SQL equivalent + return null; + } + + protected String createXPath() throws RepositoryException { + // no SQL equivalent + return null; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLongTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLongTest.java new file mode 100644 index 00000000000..f24718823bc --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLongTest.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.test.api.query; + +/** + * Test cases for order by queries on long properties. + *
          + *
        • {@code testroot} path to node that accepts child nodes of type + * nodetype + *
        • {@code nodetype} name of a node type + *
        • {@code nodename1} name of a child node of type nodetype + *
        • {@code nodename2} name of a child node of type nodetype + *
        • {@code nodename3} name of a child node of type nodetype + *
        • {@code nodename4} name of a child node of type nodetype + *
        • {@code propertyname1} name of a single value long property. + *
        + */ +public class OrderByLongTest extends AbstractOrderByTest { + + /** + * Tests order by queries with long properties. + */ + public void testIntegerOrder() throws Exception { + populate(new long[]{0, -1, 1, 5}); + checkOrder(new String[]{nodeName2, nodeName1, nodeName3, nodeName4}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLowerCaseTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLowerCaseTest.java new file mode 100644 index 00000000000..1a292f3ed5f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByLowerCaseTest.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.test.api.query; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.DynamicOperand; + +/** + * OrderByLowerCaseTest contains test cases for order by queries + * on lower cased property values. + */ +public class OrderByLowerCaseTest extends AbstractOrderByTest { + + public void testLowerCase() throws RepositoryException { + populate(new String[]{"a", "AB", "abc", "aBCd"}); + checkOrder(new String[]{nodeName1, nodeName2, nodeName3, nodeName4}); + } + + protected DynamicOperand createOrderingOperand() + throws RepositoryException { + return qf.lowerCase(super.createOrderingOperand()); + } + + protected String createSQL() { + // no SQL equivalent + return null; + } + + protected String createXPath() throws RepositoryException { + // no SQL equivalent + return null; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByMultiTypeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByMultiTypeTest.java new file mode 100644 index 00000000000..3b7d3609776 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByMultiTypeTest.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.test.api.query; + +import javax.jcr.Node; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +/** + * Test cases for order by queries on long properties. + *
          + *
        • {@code testroot} path to node that accepts child nodes of type + * nodetype + *
        • {@code nodetype} name of a node type + *
        • {@code nodename1} name of a child node of type nodetype + *
        • {@code nodename2} name of a child node of type nodetype + *
        • {@code nodename3} name of a child node of type nodetype + *
        • {@code nodename4} name of a child node of type nodetype + *
        • {@code propertyname1} name of a single value String property. + *
        • {@code propertyname2} name of a single value long property. + *
        + */ +public class OrderByMultiTypeTest extends AbstractOrderByTest { + + /** + * Tests order by queries with a String property and a long property. + */ + public void testMultipleOrder() throws Exception { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + + n1.setProperty(propertyName1, "aaa"); + n1.setProperty(propertyName2, 3); + n2.setProperty(propertyName1, "bbb"); + n2.setProperty(propertyName2, 2); + n3.setProperty(propertyName1, "ccc"); + n3.setProperty(propertyName2, 2); + + testRootNode.getSession().save(); + + // both ascending + String sql = "SELECT " + propertyName2 + " FROM " + testNodeType + " WHERE " + + jcrPath + " LIKE '" + testRoot + "/%' ORDER BY " + propertyName2 + ", " + propertyName1; + Query q; + QueryResult result; + if (checkSQL) { + q = superuser.getWorkspace().getQueryManager().createQuery(sql, qsSQL); + result = q.execute(); + checkResultOrder(result, new String[]{nodeName2, nodeName3, nodeName1}); + } + + String xpath = "/" + jcrRoot + testRoot + "/*[@" + jcrPrimaryType + "='" + testNodeType + + "'] order by @" + propertyName2 + ", @" + propertyName1; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, qsXPATH); + result = q.execute(); + checkResultOrder(result, new String[]{nodeName2, nodeName3, nodeName1}); + + // both descending + sql = "SELECT " + propertyName2 + " FROM " + testNodeType + " WHERE " + + jcrPath + " LIKE '" + testRoot + "/%' ORDER BY " + + propertyName2 + " DESC, " + + propertyName1 + " DESC"; + if (checkSQL) { + q = superuser.getWorkspace().getQueryManager().createQuery(sql, qsSQL); + result = q.execute(); + checkResultOrder(result, new String[]{nodeName1, nodeName3, nodeName2}); + } + + xpath = "/" + jcrRoot + testRoot + "/*[@" + jcrPrimaryType + "='" + + testNodeType + "'] order by @" + + propertyName2 + " descending, @" + + propertyName1 + " descending"; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, qsXPATH); + result = q.execute(); + checkResultOrder(result, new String[]{nodeName1, nodeName3, nodeName2}); + + // mixed ascending and descending + sql = "SELECT " + propertyName2 + " FROM " + testNodeType + " WHERE " + + jcrPath + " LIKE '" + testRoot + "/%' ORDER BY " + + propertyName2 + " DESC, " + propertyName1; + if (checkSQL) { + q = superuser.getWorkspace().getQueryManager().createQuery(sql, qsSQL); + result = q.execute(); + checkResultOrder(result, new String[]{nodeName1, nodeName2, nodeName3}); + } + + xpath = "/" + jcrRoot + testRoot + "/*[@" + jcrPrimaryType + "='" + + testNodeType + "'] order by @" + propertyName2 + + " descending, @" + propertyName1; + q = superuser.getWorkspace().getQueryManager().createQuery(xpath, qsXPATH); + result = q.execute(); + checkResultOrder(result, new String[]{nodeName1, nodeName2, nodeName3}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByNameTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByNameTest.java new file mode 100644 index 00000000000..b3cb080ceef --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByNameTest.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.test.api.query; + +import java.util.Arrays; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.DynamicOperand; + +/** + * OrderByNameTest contains test cases for order by queries on node + * names. + */ +public class OrderByNameTest extends AbstractOrderByTest { + + public void testName() throws RepositoryException { + populate(new String[]{"a", "a", "a", "a"}); // dummy values + String[] names = new String[]{nodeName1, nodeName2, nodeName3, nodeName4}; + Arrays.sort(names); + checkOrder(names); + } + + protected DynamicOperand createOrderingOperand() + throws RepositoryException { + return qf.nodeName("s"); + } + + protected String createSQL() { + // no SQL equivalent + return null; + } + + protected String createXPath() throws RepositoryException { + // no SQL equivalent + return null; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByStringTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByStringTest.java new file mode 100644 index 00000000000..31e666a2d8a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByStringTest.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.test.api.query; + +/** + * Test cases for order by queries on String properties. + *
          + *
        • {@code testroot} path to node that accepts child nodes of type + * nodetype + *
        • {@code nodetype} name of a node type + *
        • {@code nodename1} name of a child node of type nodetype + *
        • {@code nodename2} name of a child node of type nodetype + *
        • {@code nodename3} name of a child node of type nodetype + *
        • {@code nodename4} name of a child node of type nodetype + *
        • {@code propertyname1} name of a single value String property. + *
        + */ +public class OrderByStringTest extends AbstractOrderByTest { + + /** + * Tests order by queries with String properties. + */ + public void testStringOrder() throws Exception { + populate(new String[]{"aaaa", "cccc", "bbbb", "dddd"}); + checkOrder(new String[]{nodeName1, nodeName3, nodeName2, nodeName4}); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByURITest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByURITest.java new file mode 100644 index 00000000000..8a72b6fa807 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByURITest.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.test.api.query; + +import javax.jcr.RepositoryException; +import javax.jcr.PropertyType; + +/** + * OrderByURITest tests order by queries with URI properties. + */ +public class OrderByURITest extends AbstractOrderByTest { + + private static final String BASE_URI = "http://example.com/"; + + public void testURI() throws RepositoryException { + populate(new String[]{BASE_URI + "a", BASE_URI + "b", BASE_URI + "c", BASE_URI + "d"}, PropertyType.URI); + checkOrder(new String[]{nodeName1, nodeName2, nodeName3, nodeName4}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByUpperCaseTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByUpperCaseTest.java new file mode 100644 index 00000000000..842b2ef15ff --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/OrderByUpperCaseTest.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.test.api.query; + +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.DynamicOperand; + +/** + * OrderByUpperCaseTest contains test cases for order by queries + * on upper cased property values. + */ +public class OrderByUpperCaseTest extends AbstractOrderByTest { + + public void testLowerCase() throws RepositoryException { + populate(new String[]{"a", "AB", "abc", "aBCd"}); + checkOrder(new String[]{nodeName1, nodeName2, nodeName3, nodeName4}); + } + + protected DynamicOperand createOrderingOperand() + throws RepositoryException { + return qf.upperCase(super.createOrderingOperand()); + } + + protected String createSQL() { + // no SQL equivalent + return null; + } + + protected String createXPath() throws RepositoryException { + // no SQL equivalent + return null; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/PredicatesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/PredicatesTest.java new file mode 100644 index 00000000000..237d8fd5f67 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/PredicatesTest.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.test.api.query; + +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.QueryManager; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * Tests if queries with predicates are accepted. Test cases in this class only + * perform tests that check if the QueryManager accepts the query, but the tests + * will not execute the query and check its results. + * + */ +public class PredicatesTest extends AbstractQueryTest { + + /** + * the node type of the root node + */ + private String nodeTypeName; + + /** + * A read-only session + */ + private Session session; + + /** + * the query manager of the session + */ + private QueryManager qm; + + /** + * Sets up the test cases + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + + nodeTypeName = session.getRootNode().getPrimaryNodeType().getName(); + qm = session.getWorkspace().getQueryManager(); + } + + /** + * Releases the session acquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + qm = null; + super.tearDown(); + } + + /** + * Verifies that the value of a property can be searched + * + * @throws RepositoryException + */ + public void testEquality() throws RepositoryException { + String stmt = + xpathRoot + "/*[@" + jcrPrimaryType + "='" + nodeTypeName + "']"; + + try { + qm.createQuery(stmt, qsXPATH); + } catch (InvalidQueryException e) { + fail("invalid statement syntax for '" + stmt + "'"); + } + } + + /** + * Verifies that the or operator is accepted for properties's values + * + * @throws RepositoryException + */ + public void testCombinedOr() throws RepositoryException { + String stmt = + xpathRoot + "/*[@" + jcrPrimaryType + "='" + nodeTypeName + + "' or @" + jcrPrimaryType + "='" + ntBase + "']"; + + try { + qm.createQuery(stmt, qsXPATH); + } catch (InvalidQueryException e) { + fail("invalid statement syntax for '" + stmt + "'"); + } + } + + /** + * Verifies that the or operator is accepted for a property name + * + * @throws RepositoryException + */ + public void testOr() throws RepositoryException { + String stmt = + xpathRoot + "/*[@" + jcrPrimaryType + " or @" + jcrMixinTypes + "]"; + + try { + qm.createQuery(stmt, qsXPATH); + } catch (InvalidQueryException e) { + fail("invalid statement syntax for '" + stmt + "'"); + } + } + + /** + * Verifies that the and operator is accepted for a property name + * + * @throws RepositoryException + */ + public void testAnd() throws RepositoryException { + String stmt = + xpathRoot + "/*[@" + jcrPrimaryType + " and @" + jcrMixinTypes + "]"; + + try { + qm.createQuery(stmt, qsXPATH); + } catch (InvalidQueryException e) { + fail("invalid statement syntax for '" + stmt + "'"); + } + } + + /** + * Verifies that the and operator is accepted for properties's values + * + * @throws RepositoryException + */ + public void testCombinedAnd() throws RepositoryException { + String stmt = + xpathRoot + "/*[@" + jcrPrimaryType + "='" + nodeTypeName + + "' and @" + jcrPrimaryType + "='" + ntBase + "']"; + + try { + qm.createQuery(stmt, qsXPATH); + } catch (InvalidQueryException e) { + fail("invalid statement syntax for '" + stmt + "'"); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/QueryResultNodeIteratorTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/QueryResultNodeIteratorTest.java new file mode 100644 index 00000000000..2c517a3ec67 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/QueryResultNodeIteratorTest.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; +import javax.jcr.query.QueryResult; +import java.util.NoSuchElementException; + +/** + * Tests methods on {@link javax.jcr.NodeIterator} returned by + * {@link javax.jcr.query.QueryResult#getNodes()}. + * + */ +public class QueryResultNodeIteratorTest extends AbstractQueryTest { + + /** + * Sets up the fixture for test cases. + */ + protected void setUp() throws Exception { + super.setUp(); + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName3, testNodeType); + testRootNode.getSession().save(); + } + + /** + * Tests if {@link javax.jcr.NodeIterator#getSize()} returns the correct + * size. + * + * @throws org.apache.jackrabbit.test.NotExecutableException + * if getSize() returns -1 (unavailable). + */ + public void testGetSize() throws RepositoryException, NotExecutableException { + NodeIterator it = execute(xpathRoot + "//*", qsXPATH).getNodes(); + long size = testRootNode.getNodes().getSize(); + if (size != -1) { + long count = 0; + while (it.hasNext()) { + it.nextNode(); + count++; + } + assertEquals("NodeIterator.getSize does not return correct number.", size, count); + } else { + throw new NotExecutableException("NodeIterator.getSize() does not return size information."); + } + } + + /** + * Tests the method NodeIterator.getPosition(). + */ + public void testGetPosition() throws RepositoryException, NotExecutableException { + QueryResult rs = execute(xpathRoot + "//*", qsXPATH); + + // getPosition initially returns 0 + NodeIterator it = rs.getNodes(); + assertEquals("Initial call to getPosition() must return 0.", 0, it.getPosition()); + + // check getPosition while iterating + int index = 0; + while (it.hasNext()) { + it.nextNode(); + assertEquals("Wrong position returned by getPosition()", ++index, it.getPosition()); + } + } + + /** + * Tests the method NodeIterator.getPosition() on an empty + * NodeIterator. + * @throws NotExecutableException + */ + public void testGetPositionEmptyIterator() throws RepositoryException, NotExecutableException { + QueryResult rs = execute(xpathRoot + "/" + nodeName4, qsXPATH); + + NodeIterator it = rs.getNodes(); + assertFalse("NodeIterator must be empty.", it.hasNext()); + + assertEquals("Empty NodeIterator must return 0 on getPosition()", 0, it.getPosition()); + } + + /** + * Tests if a {@link java.util.NoSuchElementException} is thrown when {@link + * javax.jcr.NodeIterator#nextNode()} is called and there are no more nodes + * available. + * @throws NotExecutableException + */ + public void testNoSuchElementException() throws RepositoryException, NotExecutableException { + NodeIterator it = execute(xpathRoot + "//*", qsXPATH).getNodes(); + while (it.hasNext()) { + it.nextNode(); + } + try { + it.nextNode(); + fail("nextNode() must throw a NoSuchElementException when no nodes are available"); + } catch (NoSuchElementException e) { + // success + } + } + + /** + * Tests if {@link javax.jcr.NodeIterator#skip(long)} works correctly. + * @throws NotExecutableException + */ + public void testSkip() throws RepositoryException, NotExecutableException { + String query = xpathRoot + "//*"; + QueryResult rs = execute(query, qsXPATH); + NodeIterator it = rs.getNodes(); + + // find out if there is anything we can skip + int count = 0; + while (it.hasNext()) { + it.nextNode(); + count++; + } + if (count > 1) { + // re-execute the query + rs = execute(query, qsXPATH); + it = rs.getNodes(); + // skip all but one + it.skip(count - 1); + // get last one + it.nextNode(); + try { + it.nextNode(); + fail("nextNode() must throw a NoSuchElementException when no nodes are available"); + } catch (NoSuchElementException e) { + // success + } + + // re-execute the query + rs = execute(query, qsXPATH); + it = rs.getNodes(); + try { + it.skip(count + 1); + fail("skip() must throw a NoSuchElementException if one tries to skip past the end of the iterator"); + } catch (NoSuchElementException e) { + // success + } + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLJcrPathTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLJcrPathTest.java new file mode 100644 index 00000000000..8f60ee69898 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLJcrPathTest.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import java.util.Arrays; + +/** + * Tests if the jcr:path property is returned in the query result. + * + */ +public class SQLJcrPathTest extends AbstractQueryTest { + + /** A read-only session */ + private Session session; + + /** + * Sets up the test cases + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + } + + /** + * Releases the session acquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Verify that the jcr:path is present in the query result. + */ + public void testJcrPath() throws RepositoryException, NotExecutableException { + String nodeTypeName = session.getRootNode().getPrimaryNodeType().getName(); + String queryStatement = "select * from " + nodeTypeName; + + // execute the search query + Query query = super.createQuery(queryStatement, qsSQL); + QueryResult result = query.execute(); + + assertTrue("jcr:path must be present in query result row", + Arrays.asList(result.getColumnNames()).contains(jcrPath)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLJoinTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLJoinTest.java new file mode 100644 index 00000000000..97059ed5f6d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLJoinTest.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; + +/** + * Tests SQL statements with a join of a node type with a mixin type. + *
          + *
        • {@code testroot} path to node that allows child nodes of type: + * nodetype + *
        • {@code nodetype} name of a node type that allows assignment of mixin + * referenceable. + *
        • {@code nodename1} name of a child node of type: nodetype. + *
        • {@code nodename2} name of a child node of type: nodetype. + *
        + */ +public class SQLJoinTest extends AbstractQueryTest { + + /** + * Test a SQL query with a primary and mixin nodetype join. + */ + public void testJoin() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + String testMixin = mixReferenceable; + if (needsMixin(n1, testMixin)) { + ensureMixinType(n1, testMixin); + } else { + testMixin = mixVersionable; + if (needsMixin(n1, testMixin)) { + ensureMixinType(n1, testMixin); + } + } + + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + + assertFalse("Node at " + n2.getPath() + " should not have mixin " + testMixin, n2.isNodeType(testMixin)); + + StringBuffer query = new StringBuffer("SELECT * FROM "); + query.append(testNodeType).append(", ").append(testMixin); + query.append(" WHERE "); + query.append(testNodeType).append(".").append(jcrPath); + query.append(" = "); + query.append(testMixin).append(".").append(jcrPath); + query.append(" AND ").append(jcrPath).append(" LIKE "); + query.append("'").append(testRoot).append("/%'"); + + executeSqlQuery(superuser, query.toString(), new Node[]{n1}); + } + + /** + * Test a SQL query with a nt:base primary type and mixin nodetype join. + */ + public void testJoinNtBase() throws RepositoryException, + NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + String testMixin = mixReferenceable; + if (needsMixin(n1, testMixin)) { + ensureMixinType(n1, testMixin); + } else { + testMixin = mixVersionable; + if (needsMixin(n1, testMixin)) { + ensureMixinType(n1, testMixin); + } + } + + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + testRootNode.getSession().save(); + + assertFalse("Node at " + n2.getPath() + " should not have mixin " + testMixin, n2.isNodeType(testMixin)); + + StringBuffer query = new StringBuffer("SELECT * FROM "); + query.append(testNodeType).append(", ").append(testMixin); + query.append(" WHERE "); + query.append(testNodeType).append(".").append(jcrPath); + query.append(" = "); + query.append(testMixin).append(".").append(jcrPath); + query.append(" AND ").append(jcrPath).append(" LIKE "); + query.append("'").append(testRoot).append("/%'"); + + executeSqlQuery(superuser, query.toString(), new Node[]{n1}); + } + + /** + * Test a SQL query with a primary type and mixin nodetype join. + */ + public void testJoinFilterPrimaryType() + throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixReferenceable); + String nodetype = testNodeTypeNoChildren == null ? ntBase : testNodeTypeNoChildren; + Node n2 = testRootNode.addNode(nodeName2, nodetype); + ensureMixinType(n2, mixReferenceable); + testRootNode.getSession().save(); + + StringBuffer query = new StringBuffer("SELECT * FROM "); + query.append(testNodeType).append(", ").append(ntBase); + query.append(" WHERE "); + query.append(testNodeType).append(".").append(jcrPath); + query.append(" = "); + query.append(ntBase).append(".").append(jcrPath); + query.append(" AND ").append(jcrPath).append(" LIKE "); + query.append("'").append(testRoot).append("/%'"); + + executeSqlQuery(superuser, query.toString(), new Node[]{n1}); + } + + /** + * Test a SQL query with a primary and mixin nodetype join on child nodes + * with same name siblings. + *
          + *
        • {@code testroot} path to node that allows child nodes with same name. + *
        • {@code nodename1} node name of the same name siblings. + *
        + * @throws NotExecutableException if testroot does not allow + * same name siblings. + */ + public void testJoinSNS() throws RepositoryException, NotExecutableException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + ensureMixinType(n1, mixReferenceable); + if (!n1.getDefinition().allowsSameNameSiblings()) { + throw new NotExecutableException("Node at " + testRoot + " does not allow same name siblings with name " + nodeName1); + } + testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixReferenceable); + testRootNode.getSession().save(); + + StringBuffer query = new StringBuffer("SELECT * FROM "); + query.append(testNodeType).append(", ").append(mixReferenceable); + query.append(" WHERE "); + query.append(testNodeType).append(".").append(jcrPath); + query.append(" = "); + query.append(mixReferenceable).append(".").append(jcrPath); + query.append(" AND ").append(jcrPath).append(" LIKE "); + query.append("'").append(testRoot).append("/%'"); + + executeSqlQuery(superuser, query.toString(), new Node[]{n1, n2}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLOrderByTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLOrderByTest.java new file mode 100644 index 00000000000..2a1e151aea6 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLOrderByTest.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.test.api.query; + +/** + * This test searches for all nodes of a specific node type and orders them by + * the property with name configured as {@link #propertyName1}. + *

        + * The default workspace must at least contain two nodes of type {@link #testNodeType} + * with String properties named {@link #propertyName1} containing distinct + * values. + * + */ +public class SQLOrderByTest extends AbstractQueryTest { + + /** + * Statement without order by modifier. + */ + private String baseStatement; + + /** + * Prepare a statement without the order by modifier to be used for the + * tests + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + // setup common base statement + StringBuffer tmp = new StringBuffer("SELECT ").append(escapeIdentifierForSQL(propertyName1)); + tmp.append(" FROM ").append(escapeIdentifierForSQL(testNodeType)); + tmp.append(" WHERE ").append(escapeIdentifierForSQL(propertyName1)).append(" IS NOT NULL"); + tmp.append(" ORDER BY "); + tmp.append(escapeIdentifierForSQL(propertyName1)); + baseStatement = tmp.toString(); + } + + /** + * Test if sort order ascending is respected. + *

        + * For configuration description see {@link SQLOrderByTest}. + */ + public void testOrderByAscending() throws Exception { + Statement stmt = new Statement(baseStatement + " ASC", qsSQL); + evaluateResultOrder(execute(stmt), propertyName1, false); + } + + /** + * Test if sort order descending is respected. + *

        + * For configuration description see {@link SQLOrderByTest}. + */ + public void testOrderByDescending() throws Exception { + Statement stmt = new Statement(baseStatement + " DESC", qsSQL); + evaluateResultOrder(execute(stmt), propertyName1, true); + } + + /** + * Test if default sort order is respected and is ascending if the + * order by modifier is missing. + *

        + * For configuration description see {@link SQLOrderByTest}. + */ + public void testOrderByDefault() throws Exception { + Statement stmt = new Statement(baseStatement, qsSQL); + evaluateResultOrder(execute(stmt), propertyName1, false); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLPathTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLPathTest.java new file mode 100644 index 00000000000..6a85bc8652f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLPathTest.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; +import javax.jcr.Session; +import javax.jcr.util.TraversingItemVisitor; + +import org.apache.jackrabbit.test.NotExecutableException; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests path predicates in SQL queries. The default workspace must contain a + * node tree at testroot with at least two levels. + * + */ +public class SQLPathTest extends AbstractQueryTest { + + /** A read-only session */ + private Session session; + + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + // check precondition for this test + if (testRootNode.hasNodes()) { + for (NodeIterator it = testRootNode.getNodes(); it.hasNext();) { + if (it.nextNode().hasNodes()) { + return; + } + } + } + fail("Default workspace at " + testRoot + " does not contain sufficient content."); + } + + /** + * Releases the session aquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Tests if <somepath>/% returns the descendants of <somepath>. + * @throws NotExecutableException + */ + public void testDescendantTestRoot() throws RepositoryException, NotExecutableException { + String sql = getStatement(testRoot + "/%"); + executeSqlQuery(session, sql, getDescendants(testRootNode)); + } + + /** + * Tests if <somepath>/% returns no nodes if node at <somepath> + * is a leaf. + * @throws NotExecutableException + */ + public void testDescendantLeaf() throws RepositoryException, NotExecutableException { + // find leaf + Node leaf = testRootNode; + while (leaf.hasNodes()) { + leaf = leaf.getNodes().nextNode(); + } + String sql = getStatement(leaf.getPath() + "/%"); + executeSqlQuery(session, sql, new Node[0]); + } + + /** + * Tests if <somepath>/%/<nodename> OR <somepath>/<nodename> + * returns nodes with name <nodename> which are descendants of + * node at testroot. + * @throws NotExecutableException + */ + public void testDescendantSelfTestRoot() throws RepositoryException, NotExecutableException { + // get first node which is two levels deeper than node at testroot + Node n = null; + for (NodeIterator it = testRootNode.getNodes(); it.hasNext();) { + Node child = it.nextNode(); + if (child.hasNodes()) { + n = child.getNodes().nextNode(); + break; + } + } + final String name = n.getName(); + String sql = getStatement(testRoot + "/%/" + name); + sql += " OR " + jcrPath + " = '" + testRoot + "/" + name + "'"; + // gather the nodes with visitor + final List nodes = new ArrayList(); + testRootNode.accept(new TraversingItemVisitor.Default() { + protected void entering(Node node, int level) throws RepositoryException { + if (node.getName().equals(name) && !testRootNode.isSame(node)) { + nodes.add(node); + } + } + }); + executeSqlQuery(session, sql, nodes.toArray(new Node[nodes.size()])); + } + + /** + * Tests if /% AND NOT /%/% returns the child nodes of the root node. + * @throws NotExecutableException + */ + public void testChildAxisRoot() throws RepositoryException, NotExecutableException { + String sql = getStatement("/%"); + sql += " AND NOT " + jcrPath + " LIKE '/%/%'"; + Node[] nodes = toArray(session.getRootNode().getNodes()); + // optionally, the result may include the root node - + // the specification allows to not return it even if using jcr:path LIKE '/%' + // see also the JCR 1.0 specification, section 8.5.2.2 ("Pseudo-property jcr:path") + Node[] optional = { session.getRootNode() }; + executeSqlQuery(session, sql, nodes, optional); + } + + /** + * Tests if <somepath>/% AND NOT <somepath>/%/% returns the child + * nodes of node at <somepath>. + * @throws NotExecutableException + */ + public void testChildAxisTestRoot() throws RepositoryException, NotExecutableException { + String sql = getStatement(testRoot + "/%"); + sql += " AND NOT " + jcrPath + " LIKE '" + testRoot + "/%/%'"; + Node[] nodes = toArray(testRootNode.getNodes()); + executeSqlQuery(session, sql, nodes); + } + + /** + * Tests if <somepath>/% AND NOT <somepath>/%/% returns no nodes + * if the node at <somepath> is a leaf. + * @throws NotExecutableException + */ + public void testChildAxisLeaf() throws RepositoryException, NotExecutableException { + // find leaf + Node leaf = testRootNode; + while (leaf.hasNodes()) { + leaf = leaf.getNodes().nextNode(); + } + String sql = getStatement(leaf.getPath() + "/%"); + sql += " AND NOT " + jcrPath + " LIKE '" + leaf.getPath() + "/%/%'"; + executeSqlQuery(session, sql, new Node[0]); + } + + //-----------------------------< internal >--------------------------------- + + /** + * Creates a SQL statement with a path predicate. + * @param path the path + * @return the SQL statement. + */ + private String getStatement(String path) { + return "SELECT * FROM " + ntBase + " WHERE " + jcrPath + " LIKE '" + path + "'"; + } + + /** + * Returns the descendants of node as an array in document + * order. + * @param node the starting node. + * @return descendants of node. + * @throws RepositoryException if an error occurs. + */ + private Node[] getDescendants(final Node node) throws RepositoryException { + final List descendants = new ArrayList(); + + node.accept(new TraversingItemVisitor.Default() { + protected void entering(Node n, int level) + throws RepositoryException { + if (!node.isSame(n)) { + descendants.add(n); + } + } + }); + + return descendants.toArray(new Node[descendants.size()]); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLQueryLevel2Test.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLQueryLevel2Test.java new file mode 100644 index 00000000000..2d68785b337 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SQLQueryLevel2Test.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.test.api.query; + +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; +import javax.jcr.query.Row; +import javax.jcr.Value; + +/** + * Tests SQL queries on content written to the workspace by the test itself. + * + */ +public class SQLQueryLevel2Test extends AbstractQueryLevel2Test { + + /** + * Test if the optional jcr:score property for full-text search is + * supported. + *

        + * For configuration description see {@link #setUpFullTextTest()}. + */ + public void testScoreColumn() throws Exception { + setUpFullTextTest(); + QueryResult result = execute(getFullTextStatement()); + RowIterator rows = result.getRows(); + // test mere existence + rows.nextRow().getValue(jcrScore); + } + + /** + * Test full-text search of the repository. + *

        + * For configuration description see {@link #setUpFullTextTest()}. + */ + public void testFullTextSearch() throws Exception { + setUpFullTextTest(); + QueryResult result = execute(getFullTextStatement()); + + // must be 1 + checkResult(result, 1); + + // evaluate result + RowIterator itr = result.getRows(); + while (itr.hasNext()) { + Row row = itr.nextRow(); + Value value = row.getValue(propertyName1); + if (value != null) { + String fullText = value.getString(); + if (fullText.indexOf("cat") > 0) { + fail("Search Text: full text search not correct, returned prohibited text"); + } + } + } + } + + /** + * Test range evaluation of a Query. + *

        + * For configuration description see {@link #setUpRangeTest()}. + */ + public void testRange() throws Exception { + setUpRangeTest(); + QueryResult result = execute(getRangeStatement()); + + // should be 1 + checkResult(result, 1); + + // evaluate result + checkValue(result.getRows(), propertyName1, "b"); + } + + /** + * Test multi-value support of search. + *

        + * For configuration description see {@link #setUpMultiValueTest()}. + */ + public void testMultiValueSearch() throws Exception { + setUpMultiValueTest(); + QueryResult result = execute(getMultiValueStatement()); + + //should be 1 + checkResult(result, 1); + + //evaluate result + checkValue(result.getRows(), propertyName1, "existence"); + } + + /** + * Test if the optional jcr:path pseudo property is contained in the query + * result. + *

        + * For configuration description see {@link #setUpFullTextTest()}. + */ + public void testPathColumn() throws Exception { + setUpFullTextTest(); + QueryResult result = execute(getFullTextStatement()); + RowIterator rows = result.getRows(); + if (getSize(rows) < 1) { + fail("Query result did not return any nodes"); + } + // re-aquire rows + rows = result.getRows(); + + // test mere existence + rows.nextRow().getValue(jcrPath); + } + + //------------------------< internal >-------------------------------------- + + /** + * @return Statement selecting a node by a phrase, and proper escaped value + * and excluding with a word + */ + private Statement getFullTextStatement() { + StringBuffer tmp = new StringBuffer("SELECT "); + tmp.append(escapeIdentifierForSQL(propertyName1)); + tmp.append(" FROM ").append(escapeIdentifierForSQL(testNodeType)); + tmp.append(" WHERE CONTAINS(., '\"quick brown\" -cat')"); + tmp.append(" AND ").append(jcrPath).append(" LIKE '"); + tmp.append(testRoot).append("/%'"); + return new Statement(tmp.toString(), qsSQL); + } + + /** + * @return Statement selecting nodes by its value contained in a multi-value + * property + */ + private Statement getMultiValueStatement() { + StringBuffer tmp = new StringBuffer("SELECT "); + tmp.append(escapeIdentifierForSQL(propertyName1)); + tmp.append(" FROM ").append(escapeIdentifierForSQL(testNodeType)); + tmp.append(" WHERE 'two' IN "); + tmp.append(escapeIdentifierForSQL(propertyName2)); + tmp.append(" AND 'existence' IN "); + tmp.append(escapeIdentifierForSQL(propertyName1)); + tmp.append(" AND ").append(jcrPath).append(" LIKE '"); + tmp.append(testRoot).append("/%'"); + return new Statement(tmp.toString(), qsSQL); + } + + /** + * @return Statement selecting nodes by its range in {@link #propertyName1} + */ + private Statement getRangeStatement() { + StringBuffer tmp = new StringBuffer("SELECT "); + tmp.append(escapeIdentifierForSQL(propertyName1)); + tmp.append(" FROM ").append(escapeIdentifierForSQL(testNodeType)); + tmp.append(" WHERE "); + tmp.append(escapeIdentifierForSQL(propertyName1)); + tmp.append(" <= 'b' AND "); + tmp.append(escapeIdentifierForSQL(propertyName1)); + tmp.append(" > 'a'"); + tmp.append(" AND ").append(jcrPath).append(" LIKE '"); + tmp.append(testRoot).append("/%'"); + return new Statement(tmp.toString(), qsSQL); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SaveTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SaveTest.java new file mode 100644 index 00000000000..0a0e2d029be --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SaveTest.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.test.api.query; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.ItemExistsException; +import javax.jcr.PathNotFoundException; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.lock.LockException; +import javax.jcr.version.VersionException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.query.Query; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * Tests the method {@link javax.jcr.query.Query#storeAsNode(String)}. + * + */ +public class SaveTest extends AbstractJCRTest { + + /** Simple XPath statement for test cases */ + private String statement; + + protected void setUp() throws Exception { + super.setUp(); + statement = "//*[@jcr:primaryType='" + ntBase + "']"; + } + + /** + * Stores a {@link javax.jcr.query.Query#XPATH} query at: + * testRoot + "/" + nodeName1. + * @throws NotExecutableException if nt:query is not supported. + */ + public void testSave() throws RepositoryException, NotExecutableException { + checkNtQuery(); + Query query = superuser.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + query.storeAsNode(testRoot + "/" + nodeName1); + + assertTrue("Node has not been stored", testRootNode.hasNode(nodeName1)); + + Node queryNode = testRootNode.getNode(nodeName1); + assertTrue("Query node is not of type nt:query", queryNode.isNodeType(ntQuery)); + + Query query2 = superuser.getWorkspace().getQueryManager().getQuery(queryNode); + assertEquals("Persisted query does not match initial query.", query.getStatement(), query2.getStatement()); + } + + /** + * Tests if an {@link javax.jcr.ItemExistsException} is thrown when a query + * is stored on an existing node and same name siblings are not allowed. + * @throws NotExecutableException if nt:query is not supported. + */ + public void testItemExistsException() throws RepositoryException, NotExecutableException { + checkNtQuery(); + Query query = superuser.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + Node qNode = query.storeAsNode(testRoot + "/" + nodeName1); + + // create another one + query = superuser.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + try { + query.storeAsNode(testRoot + "/" + nodeName1); + if (!qNode.getDefinition().allowsSameNameSiblings()) { + // must throw if same name siblings are not allowed + fail("Query.storeAsNode() did not throw ItemExistsException"); + } + } catch (ItemExistsException e) { + if (qNode.getDefinition().allowsSameNameSiblings()) { + fail("Query.storeAsNode() must not throw ItemExistsException " + + "when same name siblings are allowed"); + } else { + // expected behaviour + } + } + } + + /** + * Tests if a {@link javax.jcr.PathNotFoundException} is thrown when a query + * is stored to a non existent path. + * @throws NotExecutableException if nt:query is not supported. + */ + public void testPathNotFoundException() throws RepositoryException, NotExecutableException { + checkNtQuery(); + Query query = superuser.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + try { + query.storeAsNode(testRoot + "/" + nodeName1 + "/" + nodeName1); + fail("Query.storeAsNode() must throw PathNotFoundException on invalid path"); + } catch (PathNotFoundException e) { + // expected behaviour + } + } + + /** + * Tests if a {@link javax.jcr.version.VersionException} is thrown when a + * query is stored under a checked in node. + *

        + * The tests creates a node under testRoot with name + * nodeName1 and adds a mix:versionable mixin if the node is + * not already versionable. + * Then the test tries to store a query as nodeName2 under node + * nodeName1. + * @throws NotExecutableException if nt:query is not supported. + */ + public void testVersionException() throws RepositoryException, NotExecutableException { + checkNtQuery(); + // check if repository supports versioning + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { + throw new NotExecutableException(); + } + + Query query = superuser.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + // create a node that is versionable + Node versionable = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it versionable if it is not + ensureMixinType(versionable, mixVersionable); + testRootNode.getSession().save(); + versionable.checkin(); + + try { + query.storeAsNode(testRoot + "/" + nodeName1 + "/" + nodeName2); + fail("Query.storeAsNode() must throw VersionException, parent node is checked in."); + } catch (VersionException e) { + // expected behaviour + } + } + + /** + * Tests if a {@link javax.jcr.nodetype.ConstraintViolationException} is + * thrown if a query is stored under a node which does not allow child nodes. + *

        + * The test creates a node nodeName1 of type testNodeType + * under testRoot. Then the test tries to store a query as + * nodeName2 under nodeName1. + * @throws NotExecutableException if nt:query is not supported. + */ + public void testConstraintViolationException() throws RepositoryException, NotExecutableException { + checkNtQuery(); + Query query = superuser.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + testRootNode.addNode(nodeName1, testNodeTypeNoChildren); + try { + query.storeAsNode(testRoot + "/" + nodeName1 + "/" + nodeName2); + superuser.save(); + fail("Query.storeAsNode() must throw ConstraintViolationException, parent node does not allow child nodes."); + } catch (ConstraintViolationException e) { + // expected behaviour + } + } + + /** + * Tests if a {@link javax.jcr.lock.LockException} is thrown if a query is + * stored under a node locked by another Session. + *

        + * The test creates a node nodeName1 of type testNodeType + * under testRoot and locks the node with the superuser session. + * Then the test tries to store a query as nodeName2 under + * nodeName1 with the readWrite Session. + * @throws NotExecutableException if nt:query is not supported. + */ + public void testLockException() throws RepositoryException, NotExecutableException { + checkNtQuery(); + // check if repository supports locking + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException(); + } + // create a node that is lockable + Node lockable = testRootNode.addNode(nodeName1, testNodeType); + // or try to make it lockable if it is not + ensureMixinType(lockable, mixLockable); + testRootNode.getSession().save(); + lockable.lock(false, true); + + Session readWrite = getHelper().getReadWriteSession(); + try { + Query query = readWrite.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + query.storeAsNode(testRoot + "/" + nodeName1 + "/" + nodeName2); + fail("Query.storeAsNode() must throw LockException, parent node is locked."); + } catch (LockException e) { + // expected behaviour + } finally { + readWrite.logout(); + lockable.unlock(); + } + } + + /** + * Tests if the a {@link javax.jcr.RepositoryException} is thrown when + * an malformed path is passed in {@link javax.jcr.query.Query#storeAsNode(String)}. + * @throws NotExecutableException if nt:query is not supported. + */ + public void testRepositoryException() throws RepositoryException, NotExecutableException { + checkNtQuery(); + Query query = superuser.getWorkspace().getQueryManager().createQuery(statement, Query.XPATH); + try { + query.storeAsNode(testRoot + "/invalid[42]"); + fail("Query.storeAsNode() must throw RepositoryException on malformed path."); + } catch (RepositoryException e) { + // expected behaviour + } + } + + //-------------------------------< internal >------------------------------- + + /** + * Checks if the repository supports the nt:query node type otherwise throws + * a NotExecutableException. + * + * @throws NotExecutableException if nt:query is not supported. + */ + private void checkNtQuery() throws RepositoryException, NotExecutableException { + try { + superuser.getWorkspace().getNodeTypeManager().getNodeType(ntQuery); + } catch (NoSuchNodeTypeException e) { + // not supported + throw new NotExecutableException("repository does not support nt:query"); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SetLimitTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SetLimitTest.java new file mode 100644 index 00000000000..e3812981148 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SetLimitTest.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.test.api.query; + +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; + +/** + * SetLimitTest contains test cases for the method Query.setLimit(). + */ +public class SetLimitTest extends AbstractQueryTest { + + public void testSetLimit() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName3, testNodeType); + superuser.save(); + for (int i = 0; i < 5; i++) { + Query query = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.descendantNode("s", testRoot), + null, + null + ); + query.setLimit(i); + long expected = Math.min(i, 3); + assertEquals("Wrong numer of results", expected, + getSize(query.execute().getNodes())); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SetOffsetTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SetOffsetTest.java new file mode 100644 index 00000000000..592c52c737e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SetOffsetTest.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.test.api.query; + +import javax.jcr.query.Query; +import javax.jcr.RepositoryException; + +/** + * SetOffsetTest contains test cases for the method Query.setOffset(). + */ +public class SetOffsetTest extends AbstractQueryTest { + + public void testSetOffset() throws RepositoryException { + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, testNodeType); + testRootNode.addNode(nodeName3, testNodeType); + superuser.save(); + for (int i = 0; i < 5; i++) { + Query query = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.descendantNode("s", testRoot), + null, + null + ); + query.setOffset(i); + long expected = Math.max(3 - i, 0); + assertEquals("Wrong numer of results", expected, + getSize(query.execute().getNodes())); + } + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/query/SimpleSelectionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SimpleSelectionTest.java similarity index 76% rename from src/test/org/apache/jackrabbit/test/api/query/SimpleSelectionTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SimpleSelectionTest.java index efa259cf3ad..a46a43a1724 100644 --- a/src/test/org/apache/jackrabbit/test/api/query/SimpleSelectionTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/SimpleSelectionTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -27,10 +27,6 @@ /** * SimpleSelectionTest... * - * @test - * @sources SimpleSelectionTest.java - * @executeClass org.apache.jackrabbit.test.api.query.SimpleSelectionTest - * @keywords level1 */ public class SimpleSelectionTest extends AbstractQueryTest { @@ -45,7 +41,7 @@ public class SimpleSelectionTest extends AbstractQueryTest { protected void setUp() throws Exception { isReadOnly = true; super.setUp(); - session = helper.getReadOnlySession(); + session = getHelper().getReadOnlySession(); testRootNode = session.getRootNode().getNode(testPath); } @@ -55,6 +51,7 @@ protected void setUp() throws Exception { protected void tearDown() throws Exception { if (session != null) { session.logout(); + session = null; } super.tearDown(); } @@ -77,11 +74,11 @@ public void testSingleProperty() String propQuery = "/" + jcrRoot + firstChildpath + "[@" + jcrPrimaryType + "]"; // execute search query - Query query = session.getWorkspace().getQueryManager().createQuery(propQuery, Query.XPATH); + Query query = session.getWorkspace().getQueryManager().createQuery(propQuery, qsXPATH); QueryResult result = query.execute(); - assertEquals("Should have only 1 result", 1, result.getRows().getSize()); + assertEquals("Should have only 1 result", 1, getSize(result.getRows())); assertTrue("Should contain the searched property", - Arrays.asList(result.getPropertyNames()).contains(jcrPrimaryType)); + Arrays.asList(result.getColumnNames()).contains(jcrPrimaryType)); } -} \ No newline at end of file +} diff --git a/src/test/org/apache/jackrabbit/test/api/query/Statement.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/Statement.java similarity index 76% rename from src/test/org/apache/jackrabbit/test/api/query/Statement.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/Statement.java index 99fc2d6e9ba..6962e5d144f 100644 --- a/src/test/org/apache/jackrabbit/test/api/query/Statement.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/Statement.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/TestAll.java new file mode 100644 index 00000000000..0f4d136adb8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/TestAll.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.test.api.query; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the package + * javax.jcr.query. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("javax.jcr.query tests"); + + // ADD TEST CLASSES HERE: + suite.addTestSuite(SaveTest.class); + suite.addTestSuite(SQLOrderByTest.class); + suite.addTestSuite(SQLQueryLevel2Test.class); + suite.addTestSuite(SQLJoinTest.class); + suite.addTestSuite(SQLJcrPathTest.class); + suite.addTestSuite(SQLPathTest.class); + suite.addTestSuite(XPathPosIndexTest.class); + suite.addTestSuite(XPathDocOrderTest.class); + suite.addTestSuite(XPathOrderByTest.class); + suite.addTestSuite(XPathQueryLevel2Test.class); + suite.addTestSuite(XPathJcrPathTest.class); + + suite.addTestSuite(DerefQueryLevel1Test.class); + suite.addTestSuite(ElementTest.class); + suite.addTestSuite(TextNodeTest.class); + suite.addTestSuite(GetLanguageTest.class); + suite.addTestSuite(GetPersistentQueryPathLevel1Test.class); + suite.addTestSuite(GetPersistentQueryPathTest.class); + suite.addTestSuite(GetStatementTest.class); + suite.addTestSuite(GetSupportedQueryLanguagesTest.class); + suite.addTestSuite(CreateQueryTest.class); + + suite.addTestSuite(QueryResultNodeIteratorTest.class); + suite.addTestSuite(GetPropertyNamesTest.class); + suite.addTestSuite(PredicatesTest.class); + suite.addTestSuite(SimpleSelectionTest.class); + + suite.addTestSuite(OrderByDateTest.class); + suite.addTestSuite(OrderByDoubleTest.class); + suite.addTestSuite(OrderByLongTest.class); + suite.addTestSuite(OrderByMultiTypeTest.class); + suite.addTestSuite(OrderByStringTest.class); + suite.addTestSuite(OrderByLengthTest.class); + suite.addTestSuite(OrderByLocalNameTest.class); + suite.addTestSuite(OrderByNameTest.class); + suite.addTestSuite(OrderByLowerCaseTest.class); + suite.addTestSuite(OrderByUpperCaseTest.class); + suite.addTestSuite(OrderByDecimalTest.class); + suite.addTestSuite(OrderByURITest.class); + suite.addTestSuite(SetLimitTest.class); + suite.addTestSuite(SetOffsetTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/TextNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/TextNodeTest.java new file mode 100644 index 00000000000..fcd8428de0f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/TextNodeTest.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Repository; + +/** + * Tests the text() node test in XPath. + *

          + *
        • {@code testroot} path to node that allows child nodes of type + * nodetype. The node at testroot must allow child + * nodes with name jcr:xmltext. Assignment of node type for that child node must + * be determined by the child node definition. That is, the test will create the + * node with {@link javax.jcr.Node#addNode(String)}, without giving an explicit + * node type. + *
        • {@code nodetype} name of a node type for nodes under + * testroot. This node type must allow child nodes with name + * jcr:xmltext. Assignment of node type for that child node must be determined + * by the child node definition. That is, the test will create the node with + * {@link javax.jcr.Node#addNode(String)}, without giving an explicit node + * type. + *
        • {@code nodename1} name of a child node under testroot. + *
        + */ +public class TextNodeTest extends AbstractQueryTest { + + /** Resolved Name for jcr:xmltext */ + private String jcrXMLText; + + private String jcrXMLCharacters; + + protected void setUp() throws Exception { + super.setUp(); + jcrXMLText = superuser.getNamespacePrefix(NS_JCR_URI) + ":xmltext"; + jcrXMLCharacters = superuser.getNamespacePrefix(NS_JCR_URI) + ":xmlcharacters"; + } + + /** + * Tests if text() node test is equivalent with jcr:xmltext. + * @throws NotExecutableException + */ + public void testTextNodeTest() throws RepositoryException, NotExecutableException { + Node text1 = testRootNode.addNode(jcrXMLText); + text1.setProperty(jcrXMLCharacters, "foo"); + testRootNode.getSession().save(); + String xpath = "/" + jcrRoot + testRoot + "/text()"; + executeXPathQuery(superuser, xpath, new Node[]{text1}); + } + + /** + * Tests if text() node test is equivalent with jcr:xmltext and will select + * multiple nodes with name jcr:xmltext. + * @throws NotExecutableException + */ + public void testTextNodeTestMultiNodes() throws RepositoryException, NotExecutableException { + Node text1 = testRootNode.addNode(jcrXMLText); + text1.setProperty(jcrXMLCharacters, "foo"); + Node text2 = testRootNode.addNode(nodeName1, testNodeType).addNode(jcrXMLText); + text2.setProperty(jcrXMLCharacters, "foo"); + testRootNode.getSession().save(); + String xpath = "/" + jcrRoot + testRoot + "//text()"; + executeXPathQuery(superuser, xpath, new Node[]{text1, text2}); + } + + /** + * Tests if text() node test is equivalent with jcr:xmltext and jcr:contains + * matches content in jcr:xmlcharacters property. + * @throws NotExecutableException + */ + public void testTextNodeTestContains() throws RepositoryException, NotExecutableException { + Node text1 = testRootNode.addNode(jcrXMLText); + text1.setProperty(jcrXMLCharacters, "the quick brown fox jumps over the lazy dog."); + Node text2 = testRootNode.addNode(nodeName1, testNodeType).addNode(jcrXMLText); + text2.setProperty(jcrXMLCharacters, "java content repository"); + testRootNode.getSession().save(); + String xpath = "/" + jcrRoot + testRoot + "//text()[" + jcrContains + "(., 'fox')]"; + executeXPathQuery(superuser, xpath, new Node[]{text1}); + } + + /** + * Tests text() node test with various position predicates: position(), + * first(), last(). + * @throws NotExecutableException if the repository does not support queries + * with position inidex. + */ + public void testTextNodeTestWithPosition() + throws RepositoryException, NotExecutableException { + if (!isSupported(Repository.QUERY_XPATH_POS_INDEX)) { + throw new NotExecutableException("Repository does not support position index"); + } + Node text1 = testRootNode.addNode(jcrXMLText); + text1.setProperty(jcrXMLCharacters, "foo"); + if (!text1.getDefinition().allowsSameNameSiblings()) { + throw new NotExecutableException("Node at path: " + testRoot + " does not allow same name siblings with name: " + jcrXMLText); + } + testRootNode.addNode(nodeName1, testNodeType); + Node text2 = testRootNode.addNode(jcrXMLText); + text2.setProperty(jcrXMLCharacters, "foo"); + testRootNode.getSession().save(); + String xpath = "/" + jcrRoot + testRoot + "/text()[2]"; + executeXPathQuery(superuser, xpath, new Node[]{text2}); + xpath = "/" + jcrRoot + testRoot + "/text()[last()]"; + executeXPathQuery(superuser, xpath, new Node[]{text2}); + xpath = "/" + jcrRoot + testRoot + "/text()[position() = 2]"; + executeXPathQuery(superuser, xpath, new Node[]{text2}); + xpath = "/" + jcrRoot + testRoot + "/text()[first()]"; + executeXPathQuery(superuser, xpath, new Node[]{text1}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathDocOrderTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathDocOrderTest.java new file mode 100644 index 00000000000..bcf4bcf106f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathDocOrderTest.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.query.QueryResult; + +/** + * Tests if the repository supports document order in XPath. The tests will + * check the repository descriptor {@link javax.jcr.Repository#QUERY_XPATH_DOC_ORDER} + * first and throw a {@link org.apache.jackrabbit.test.NotExecutableException} + * if the descriptor is not present. + *

        + * This is a level 1 test, therefore does not write content to the workspace. + * The tests require the following content in the default workspace: + *

        + * At least three nodes under the {@link #testRoot}. + * + */ +public class XPathDocOrderTest extends AbstractQueryTest { + + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + } + + /** + * Tests the position() function. + *

        + * For configuration description see {@link XPathDocOrderTest}. + */ + public void testDocOrderPositionFunction() throws Exception { + String xpath = xpathRoot + "/*[position()=2]"; + String resultPath = ""; + for (NodeIterator nodes = testRootNode.getNodes(); nodes.hasNext() && nodes.getPosition() < 2;) { + resultPath = nodes.nextNode().getPath(); + } + docOrderTest(new Statement(xpath, qsXPATH), resultPath); + } + + /** + * Tests if position index and document order on child axis returns the + * correct node. + *

        + * For configuration description see {@link XPathDocOrderTest}. + */ + public void testDocOrderPositionIndex() throws Exception { + String xpath = xpathRoot + "/*[2]"; + String resultPath = ""; + for (NodeIterator nodes = testRootNode.getNodes(); nodes.hasNext() && nodes.getPosition() < 2;) { + resultPath = nodes.nextNode().getPath(); + } + docOrderTest(new Statement(xpath, qsXPATH), resultPath); + } + + /** + * Tests the last() function. + *

        + * For configuration description see {@link XPathDocOrderTest}. + */ + public void testDocOrderLastFunction() throws Exception { + String xpath = xpathRoot + "/*[position()=last()]"; + String resultPath = ""; + for (NodeIterator nodes = testRootNode.getNodes(); nodes.hasNext();) { + resultPath = nodes.nextNode().getPath(); + } + docOrderTest(new Statement(xpath, qsXPATH), resultPath); + } + + /** + * Tests the first() function. + *

        + * For configuration description see {@link XPathDocOrderTest}. + */ + public void testDocOrderFirstFunction() throws Exception { + String xpath = xpathRoot + "/*[first()]"; + String resultPath = testRootNode.getNodes().nextNode().getPath(); + docOrderTest(new Statement(xpath, qsXPATH), resultPath); + } + + //-----------------------------< internal >--------------------------------- + + /** + * Executes a statement, checks if the Result contains exactly one node with + * path. + * + * @param stmt to be executed + * @param path the path of the node in the query result. + */ + private void docOrderTest(Statement stmt, String path) + throws RepositoryException, NotExecutableException { + if (!isSupported(Repository.QUERY_XPATH_DOC_ORDER)) { + throw new NotExecutableException("Repository does not support document order on result set."); + } + + int count = 0; + // check precondition: at least 3 nodes + for (NodeIterator it = testRootNode.getNodes(); it.hasNext(); it.nextNode()) { + count++; + } + if (count < 3) { + throw new NotExecutableException("Workspace does not contain enough content under: " + testRoot + + ". At least 3 nodes are required for this test."); + } + + QueryResult result = execute(stmt); + checkResult(result, 1); + assertEquals("Wrong result node.", path, result.getNodes().nextNode().getPath()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathJcrPathTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathJcrPathTest.java new file mode 100644 index 00000000000..26a22aba643 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathJcrPathTest.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.test.api.query; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import java.util.Arrays; + +/** + * Tests if the jcr:path property is returned in the query result. + * + */ +public class XPathJcrPathTest extends AbstractQueryTest { + + /** A read-only session */ + private Session session; + + /** + * Sets up the test cases + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + session = getHelper().getReadOnlySession(); + testRootNode = session.getRootNode().getNode(testPath); + } + + /** + * Releases the session acquired in setUp(). + */ + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Verify that the jcr:path is present in the query result. + */ + public void testJcrPath() throws RepositoryException, NotExecutableException { + String nodeTypeName = session.getRootNode().getPrimaryNodeType().getName(); + String queryStatement = "//element(*, " + nodeTypeName + ")"; + + // execute the search query + Query query = session.getWorkspace().getQueryManager().createQuery(queryStatement, qsXPATH); + QueryResult result = query.execute(); + + assertTrue("jcr:path must be present in query result row", + Arrays.asList(result.getColumnNames()).contains(jcrPath)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathOrderByTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathOrderByTest.java new file mode 100644 index 00000000000..76df6dc878e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathOrderByTest.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.test.api.query; + +/** + * This test searches for all nodes of a specific node type and orders them by + * the property with name configured as {@link #propertyName1}. + *

        + * The default workspace must at least contain two nodes of type {@link #testNodeType} + * with String properties named {@link #propertyName1} containing + * distinct values. + * + */ +public class XPathOrderByTest extends AbstractQueryTest { + + /** + * Statement without order by modifier. + */ + private String baseStatement; + + /** + * Prepare a statement without order by modifier to be used for the tests + */ + protected void setUp() throws Exception { + isReadOnly = true; + super.setUp(); + baseStatement = + xpathRoot + "/*[@" + propertyName1 + "] order by @" + propertyName1; + } + + /** + * Test if sort order ascending is respected. + *

        + * For configuration description see {@link XPathOrderByTest}. + */ + public void testOrderByAscending() throws Exception { + Statement stmt = new Statement(baseStatement + " ascending", qsXPATH); + evaluateResultOrder(execute(stmt), propertyName1, false); + } + + /** + * Test if sort order descending is respected. + *

        + * For configuration description see {@link XPathOrderByTest}. + */ + public void testOrderByDescending() throws Exception { + Statement stmt = new Statement(baseStatement + " descending", qsXPATH); + evaluateResultOrder(execute(stmt), propertyName1, true); + } + + /** + * Test if default sort order is ascending + *

        + * For configuration description see {@link XPathOrderByTest}. + */ + public void testOrderBy() throws Exception { + Statement stmt = new Statement(baseStatement, qsXPATH); + evaluateResultOrder(execute(stmt), propertyName1, false); + } + +} diff --git a/src/test/org/apache/jackrabbit/test/api/query/XPathPosIndexTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathPosIndexTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/query/XPathPosIndexTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathPosIndexTest.java index 28587037e17..97ab8367d09 100644 --- a/src/test/org/apache/jackrabbit/test/api/query/XPathPosIndexTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathPosIndexTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -21,7 +21,6 @@ import javax.jcr.RepositoryException; import javax.jcr.NodeIterator; import javax.jcr.Repository; -import javax.jcr.query.Query; import javax.jcr.query.QueryResult; /** @@ -29,17 +28,13 @@ * check the repository descriptor {@link javax.jcr.Repository#QUERY_XPATH_POS_INDEX} * first and throw a {@link org.apache.jackrabbit.test.NotExecutableException} * if the descriptor is not present. - *

        + *

        * This is a level 1 test, therefore does not write content to the workspace. * The tests require the following content in the default workspace: - *

        + *

        * At least three nodes with the name {@link #nodeName1} under the - * {@link #testRoot}. + * {@link #testRoot}. * - * @test - * @sources XPathPosIndexTest.java - * @executeClass org.apache.jackrabbit.test.api.query.XPathPosIndexTest - * @keywords level1 */ public class XPathPosIndexTest extends AbstractQueryTest { @@ -50,14 +45,14 @@ protected void setUp() throws Exception { /** * Test if the indexed notation is supported. - *

        + *

        * For configuration description see {@link XPathPosIndexTest}. */ public void testDocOrderIndexedNotation() throws Exception { String path = testRoot + "/" + nodeName1 + "[2]"; StringBuffer tmp = new StringBuffer("/"); tmp.append(jcrRoot).append(path); - docOrderTest(new Statement(tmp.toString(), Query.XPATH), path); + docOrderTest(new Statement(tmp.toString(), qsXPATH), path); } //-----------------------------< internal >--------------------------------- @@ -71,7 +66,7 @@ public void testDocOrderIndexedNotation() throws Exception { */ private void docOrderTest(Statement stmt, String path) throws RepositoryException, NotExecutableException { - if (!hasDescriptor(Repository.QUERY_XPATH_POS_INDEX)) { + if (!isSupported(Repository.QUERY_XPATH_POS_INDEX)) { throw new NotExecutableException("Repository does not support document order on result set."); } @@ -89,4 +84,4 @@ private void docOrderTest(Statement stmt, String path) checkResult(result, 1); assertEquals("Wrong result node.", path, result.getNodes().nextNode().getPath()); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathQueryLevel2Test.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathQueryLevel2Test.java new file mode 100644 index 00000000000..19f7bd2f392 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/XPathQueryLevel2Test.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.test.api.query; + +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; +import javax.jcr.Value; +import javax.jcr.NodeIterator; + +/** + * Tests XPath queries on content written to the workspace by the test itself. + * + */ +public class XPathQueryLevel2Test extends AbstractQueryLevel2Test { + + /** + * Test if the optional jcr:score property for full-text search is + * supported. + *

        + * For configuration description see {@link #setUpFullTextTest()}. + */ + public void testScoreColumn() throws Exception { + setUpFullTextTest(); + QueryResult result = execute(getFullTextStatement()); + RowIterator rows = result.getRows(); + // test mere existence + rows.nextRow().getValue(jcrScore); + } + + /** + * Test full-text search of the repository.
        + *

        + * For configuration description see {@link #setUpFullTextTest()}. + */ + public void testFullTextSearch() throws Exception { + setUpFullTextTest(); + QueryResult result = execute(getFullTextStatement()); + + // must be 1 + checkResult(result, 1); + + // evaluate result + NodeIterator itr = result.getNodes(); + while (itr.hasNext()) { + Value value = itr.nextNode().getProperty(propertyName1).getValue(); + if (value != null) { + String fullText = value.getString(); + if (fullText.indexOf("cat") > 0) { + fail("Search Text: full text search not correct, returned prohibited text"); + } + } + } + } + + /** + * Test range evauluation of Query.
        + *

        + * For configuration description see {@link #setUpRangeTest()}. + */ + public void testRange() throws Exception { + setUpRangeTest(); + QueryResult result = execute(getRangeStatement()); + + // should be 1 + checkResult(result, 1); + + // evaluate result + checkValue(result.getNodes(), propertyName1, "b"); + } + + /** + * Test multi-value support of search.
        + *

        + * For configuration description see {@link #setUpMultiValueTest()}. + */ + public void testMultiValueSearch() throws Exception { + setUpMultiValueTest(); + QueryResult result = execute(getMultiValueStatement()); + + // should be 1 + checkResult(result, 1); + + // evaluate result + checkValue(result.getNodes(), propertyName1, "existence"); + } + + /** + * Test if the jcr:path pseudo property is contained in result. + *

        + * For configuration description see {@link #setUpFullTextTest()}. + */ + public void testPathColumn() throws Exception { + setUpFullTextTest(); + QueryResult result = execute(getFullTextStatement()); + RowIterator rows = result.getRows(); + if (getSize(rows) < 1) { + fail("Query result did not return any nodes"); + } + // re-aquire rows + rows = result.getRows(); + + // test mere existence + rows.nextRow().getValue(jcrPath); + } + + //---------------------------< internal >----------------------------------- + + /** + * @return Statement selecting a node by a phrase, and proper escaped value + * and excluding with a word + */ + private Statement getFullTextStatement() { + String xpath = + xpathRoot + "/*[" + jcrContains + "(., '\"quick brown\" -cat')]"; + return new Statement(xpath, qsXPATH); + } + + /** + * @return Statement selecting nodes by its value contained in a multi-value + * property + */ + private Statement getMultiValueStatement() { + String xpath = + xpathRoot + "/*[@" + propertyName2 + " = 'two' and @" + + propertyName1 + " = 'existence']"; + return new Statement(xpath, qsXPATH); + } + + /** + * @return Statement selecting nodes by its range in {@link #propertyName1} + */ + private Statement getRangeStatement() { + String xpath = + xpathRoot + "/*[@" + propertyName1 + " <= 'b' and @" + + propertyName1 + " > 'a']"; + return new Statement(xpath, qsXPATH); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AbstractJoinTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AbstractJoinTest.java new file mode 100644 index 00000000000..5de7cde2eb0 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AbstractJoinTest.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.test.api.query.qom; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryResult; +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelConstants; + +/** + * AbstractJoinTest provides utility methods for join related + * tests. + */ +public abstract class AbstractJoinTest extends AbstractQOMTest { + + /** + * Name of the left selector. + */ + protected static final String LEFT = "left"; + + /** + * Name of the right selector. + */ + protected static final String RIGHT = "right"; + + /** + * The selector names for the join. + */ + protected static final String[] SELECTOR_NAMES = new String[]{LEFT, RIGHT}; + + //--------------------------< utilities >----------------------------------- + + protected void checkQOM(QueryObjectModel qom, Node[][] nodes) + throws RepositoryException { + checkQOM(qom, SELECTOR_NAMES, nodes); + } + + protected void checkResult(QueryResult result, Node[][] nodes) + throws RepositoryException { + checkResult(result, SELECTOR_NAMES, nodes); + } + + protected QueryObjectModel createQuery(String joinType, + JoinCondition condition) + throws RepositoryException { + return createQuery(joinType, condition, null, null); + } + + protected QueryObjectModel createQuery(String joinType, + JoinCondition condition, + Constraint left, + Constraint right) + throws RepositoryException { + // only consider nodes under test root + Constraint constraint; + if (QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER.equals(joinType)) { + constraint = qf.descendantNode(LEFT, testRoot); + } else { + constraint = qf.descendantNode(RIGHT, testRoot); + } + + if (left != null) { + constraint = qf.and(constraint, left); + } + if (right != null) { + constraint = qf.and(constraint, right); + } + Join join = qf.join( + qf.selector(testNodeType, LEFT), + qf.selector(testNodeType, RIGHT), + joinType, + condition); + return qf.createQuery(join, constraint, null, null); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AbstractQOMTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AbstractQOMTest.java new file mode 100644 index 00000000000..7836a6c11a5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AbstractQOMTest.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.query.qom; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.List; +import java.util.ArrayList; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; +import javax.jcr.query.qom.QueryObjectModel; + +import org.apache.jackrabbit.test.api.query.AbstractQueryTest; + +/** + * AbstractQOMTest is a base class for test cases on the JQOM. + */ +public abstract class AbstractQOMTest extends AbstractQueryTest { + + /** + * Binds the given value to the variable named + * var. + * + * @param q the query + * @param var name of variable in query + * @param value value to bind + * @throws IllegalArgumentException if var is not a valid + * variable in this query. + * @throws RepositoryException if an error occurs. + */ + protected void bindVariableValue(Query q, String var, Value value) + throws RepositoryException { + q.bindValue(var, value); + } + + protected void checkResultOrder(QueryObjectModel qom, + String[] selectorNames, + Node[][] nodes) + throws RepositoryException { + checkResultOrder(qom.execute(), selectorNames, nodes); + checkResultOrder(qm.createQuery(qom.getStatement(), Query.JCR_SQL2).execute(), + selectorNames, nodes); + } + + protected void checkResultOrder(QueryResult result, + String[] selectorNames, + Node[][] nodes) + throws RepositoryException { + // collect rows + List expectedPaths = new ArrayList(); + log.println("expected:"); + for (int i = 0; i < nodes.length; i++) { + StringBuffer aggregatedPaths = new StringBuffer(); + for (int j = 0; j < nodes[i].length; j++) { + aggregatedPaths.append(getPath(nodes[i][j])); + aggregatedPaths.append("|"); + } + expectedPaths.add(aggregatedPaths.toString()); + log.println(aggregatedPaths.toString()); + } + + List resultPaths = new ArrayList(); + log.println("result:"); + for (RowIterator it = result.getRows(); it.hasNext();) { + Row r = it.nextRow(); + StringBuffer aggregatedPaths = new StringBuffer(); + for (int i = 0; i < selectorNames.length; i++) { + aggregatedPaths.append(getPath(r.getNode(selectorNames[i]))); + aggregatedPaths.append("|"); + } + resultPaths.add(aggregatedPaths.toString()); + log.println(aggregatedPaths.toString()); + } + + assertEquals("wrong result order", expectedPaths, resultPaths); + } + + /** + * Checks the query object model by executing it directly and matching the + * result against the given nodes. Then the QOM is executed + * again using {@link QueryObjectModel#getStatement()} with {@link + * Query#JCR_SQL2}. + * + * @param qom the query object model to check. + * @param nodes the result nodes. + * @throws RepositoryException if an error occurs while executing the + * query. + */ + protected void checkQOM(QueryObjectModel qom, Node[] nodes) + throws RepositoryException { + checkResult(qom.execute(), nodes); + checkResult(qm.createQuery(qom.getStatement(), Query.JCR_SQL2).execute(), nodes); + } + + /** + * Checks the query object model by executing it directly and matching the + * result against the given nodes. Then the QOM is executed + * again using {@link QueryObjectModel#getStatement()} with + * {@link Query#JCR_SQL2}. + * + * @param qom the query object model to check. + * @param selectorNames the selector names of the qom. + * @param nodes the result nodes. + * @throws RepositoryException if an error occurs while executing the + * query. + */ + protected void checkQOM(QueryObjectModel qom, + String[] selectorNames, + Node[][] nodes) throws RepositoryException { + checkResult(qom.execute(), selectorNames, nodes); + checkResult(qm.createQuery(qom.getStatement(), Query.JCR_SQL2).execute(), + selectorNames, nodes); + } + + protected void checkResult(QueryResult result, + String[] selectorNames, + Node[][] nodes) + throws RepositoryException { + // collect rows + Set expectedPaths = new HashSet(); + log.println("expected:"); + for (int i = 0; i < nodes.length; i++) { + StringBuffer aggregatedPaths = new StringBuffer(); + for (int j = 0; j < nodes[i].length; j++) { + aggregatedPaths.append(getPath(nodes[i][j])); + aggregatedPaths.append("|"); + } + expectedPaths.add(aggregatedPaths.toString()); + log.println(aggregatedPaths.toString()); + } + + Set resultPaths = new HashSet(); + log.println("result:"); + for (RowIterator it = result.getRows(); it.hasNext();) { + Row r = it.nextRow(); + StringBuffer aggregatedPaths = new StringBuffer(); + for (int i = 0; i < selectorNames.length; i++) { + aggregatedPaths.append(getPath(r.getNode(selectorNames[i]))); + aggregatedPaths.append("|"); + } + resultPaths.add(aggregatedPaths.toString()); + log.println(aggregatedPaths.toString()); + } + + // check if all expected are in result + for (Iterator it = expectedPaths.iterator(); it.hasNext();) { + String path = it.next(); + assertTrue(path + " is not part of the result set", resultPaths.contains(path)); + } + // check result does not contain more than expected + for (Iterator it = resultPaths.iterator(); it.hasNext();) { + String path = it.next(); + assertTrue(path + " is not expected to be part of the result set", expectedPaths.contains(path)); + } + } + + /** + * Returns the path of the node or an empty string if + * node is null. + * + * @param node a node or null. + * @return the path of the node or an empty string if node is + * null. + * @throws RepositoryException if an error occurs while reading from the + * repository. + */ + protected static String getPath(Node node) throws RepositoryException { + if (node != null) { + return node.getPath(); + } else { + return ""; + } + } + + /** + * Calls back the callable first with the qom and + * then a JCR_SQL2 query created from {@link QueryObjectModel#getStatement()}. + * + * @param qom a query object model. + * @param callable the callback. + * @throws RepositoryException if an error occurs. + */ + protected void forQOMandSQL2(QueryObjectModel qom, Callable callable) + throws RepositoryException { + List queries = new ArrayList(); + queries.add(qom); + queries.add(qm.createQuery(qom.getStatement(), Query.JCR_SQL2)); + for (Iterator it = queries.iterator(); it.hasNext();) { + callable.call(it.next()); + } + } + + protected interface Callable { + + public Object call(Query query) throws RepositoryException; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AndConstraintTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AndConstraintTest.java new file mode 100644 index 00000000000..6d083746762 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/AndConstraintTest.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.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.QueryResult; +import javax.jcr.query.Query; + +/** + * AndConstraintTest contains tests that check AND constraints. + */ +public class AndConstraintTest extends AbstractQOMTest { + + public void testAnd() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.setProperty(propertyName1, "foo"); + n1.setProperty(propertyName2, "bar"); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + n2.setProperty(propertyName2, "bar"); + superuser.save(); + + QueryResult result = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.descendantNode("s", testRootNode.getPath()), + qf.and( + qf.propertyExistence("s", propertyName1), + qf.propertyExistence("s", propertyName2) + ) + ), + null, + null + ).execute(); + checkResult(result, new Node[]{n1}); + + String stmt = "SELECT * FROM [" + testNodeType + "] AS s WHERE " + + "ISDESCENDANTNODE(s, [" + testRootNode.getPath() + "]) " + + "AND s.[" + propertyName1 + "] IS NOT NULL " + + "AND s.[" + propertyName2 + "] IS NOT NULL"; + result = qm.createQuery(stmt, Query.JCR_SQL2).execute(); + checkResult(result, new Node[]{n1}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/BindVariableValueTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/BindVariableValueTest.java new file mode 100644 index 00000000000..9e681847a94 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/BindVariableValueTest.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.test.api.query.qom; + +import java.util.Calendar; +import java.math.BigDecimal; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.query.Query; +import javax.jcr.query.qom.QueryObjectModelConstants; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * BindVariableValueTest... + */ +public class BindVariableValueTest extends AbstractQOMTest { + + private static final String STRING_VALUE = "JSR"; + + private static final long LONG_VALUE = 283; + + private static final double DOUBLE_VALUE = Math.PI; + + private static final boolean BOOLEAN_VALUE = true; + + private static final Calendar DATE_VALUE = Calendar.getInstance(); + + private static final BigDecimal DECIMAL_VALUE = new BigDecimal(LONG_VALUE); + + private static final String URI_VALUE = "http://example.com/"; + + private Query qomQuery; + + private Query sqlQuery; + + protected void setUp() throws Exception { + super.setUp(); + qomQuery = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.comparison( + qf.propertyValue("s", propertyName1), + QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + qf.bindVariable("v") + ) + ), null, null); + sqlQuery = qm.createQuery(qomQuery.getStatement(), Query.JCR_SQL2); + } + + protected void tearDown() throws Exception { + qomQuery = null; + super.tearDown(); + } + + public void testBindVariableNames() throws RepositoryException { + String[] names = qomQuery.getBindVariableNames(); + assertNotNull(names); + assertEquals(1, names.length); + assertEquals("v", names[0]); + } + + public void testIllegalArgumentException() throws RepositoryException { + try { + bindVariableValue(qomQuery, "x", vf.createValue(STRING_VALUE)); + fail("Query.bindValue() must throw IllegalArgumentException for unknown variable name"); + } catch (IllegalArgumentException e) { + // expected + } + try { + bindVariableValue(sqlQuery, "x", vf.createValue(STRING_VALUE)); + fail("Query.bindValue() must throw IllegalArgumentException for unknown variable name"); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testString() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, STRING_VALUE); + superuser.save(); + + bindVariableValue(qomQuery, "v", vf.createValue(STRING_VALUE)); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", vf.createValue(STRING_VALUE)); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testDate() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, DATE_VALUE); + superuser.save(); + + bindVariableValue(sqlQuery, "v", vf.createValue(DATE_VALUE)); + checkResult(sqlQuery.execute(), new Node[]{n}); + + bindVariableValue(qomQuery, "v", vf.createValue(DATE_VALUE)); + checkResult(qomQuery.execute(), new Node[]{n}); + } + + public void testLong() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, LONG_VALUE); + superuser.save(); + + bindVariableValue(qomQuery, "v", vf.createValue(LONG_VALUE)); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", vf.createValue(LONG_VALUE)); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testDouble() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, DOUBLE_VALUE); + superuser.save(); + + bindVariableValue(qomQuery, "v", vf.createValue(DOUBLE_VALUE)); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", vf.createValue(DOUBLE_VALUE)); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testBoolean() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, BOOLEAN_VALUE); + superuser.save(); + + bindVariableValue(qomQuery, "v", vf.createValue(BOOLEAN_VALUE)); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", vf.createValue(BOOLEAN_VALUE)); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testName() throws RepositoryException { + Value name = vf.createValue(STRING_VALUE, PropertyType.NAME); + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, name); + superuser.save(); + + bindVariableValue(qomQuery, "v", name); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", name); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testPath() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + Value path = vf.createValue(n.getPath(), PropertyType.PATH); + n.setProperty(propertyName1, path); + superuser.save(); + + bindVariableValue(qomQuery, "v", path); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", path); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testReference() throws RepositoryException, + NotExecutableException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + ensureMixinType(n, mixReferenceable); + superuser.save(); + n.setProperty(propertyName1, n); + superuser.save(); + + + bindVariableValue(qomQuery, "v", vf.createValue(n)); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", vf.createValue(n)); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testWeakReference() throws RepositoryException, + NotExecutableException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + ensureMixinType(n, mixReferenceable); + superuser.save(); + n.setProperty(propertyName1, vf.createValue(n, true)); + superuser.save(); + + bindVariableValue(qomQuery, "v", vf.createValue(n, true)); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", vf.createValue(n, true)); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testURI() throws RepositoryException { + Value value = vf.createValue(URI_VALUE, PropertyType.URI); + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, value); + superuser.save(); + + bindVariableValue(qomQuery, "v", value); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", value); + checkResult(sqlQuery.execute(), new Node[]{n}); + } + + public void testDecimal() throws RepositoryException { + Value value = vf.createValue(DECIMAL_VALUE); + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, value); + superuser.save(); + + bindVariableValue(qomQuery, "v", value); + checkResult(qomQuery.execute(), new Node[]{n}); + + bindVariableValue(sqlQuery, "v", value); + checkResult(sqlQuery.execute(), new Node[]{n}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ChildNodeJoinConditionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ChildNodeJoinConditionTest.java new file mode 100644 index 00000000000..7a4e5756cc9 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ChildNodeJoinConditionTest.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.test.api.query.qom; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelConstants; + +/** + * ChildNodeJoinConditionTest contains test cases that cover + * ChildNodeJoinCondition. + */ +public class ChildNodeJoinConditionTest extends AbstractJoinTest { + + private Node n1; + + private Node n2; + + protected void setUp() throws Exception { + super.setUp(); + n1 = testRootNode.addNode(nodeName1, testNodeType); + n2 = n1.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixReferenceable); + superuser.save(); + } + + public void testInnerJoin() throws RepositoryException { + JoinCondition c = qf.childNodeJoinCondition(LEFT, RIGHT); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, c); + checkQOM(qom, new Node[][]{{n2, n1}}); + } + + public void testRightOuterJoin() throws RepositoryException { + JoinCondition c = qf.childNodeJoinCondition(LEFT, RIGHT); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER, c); + checkQOM(qom, new Node[][]{{n2, n1}, {null, n2}}); + } + + public void testLeftOuterJoin() throws RepositoryException { + JoinCondition c = qf.childNodeJoinCondition(LEFT, RIGHT); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER, c); + List result = new ArrayList(); + result.add(new Node[]{n2, n1}); + if (testRootNode.isNodeType(testNodeType)) { + result.add(new Node[]{n1, testRootNode}); + } else { + result.add(new Node[]{n1, null}); + } + checkQOM(qom, result.toArray(new Node[result.size()][])); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ChildNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ChildNodeTest.java new file mode 100644 index 00000000000..9c1b0561ba4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ChildNodeTest.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.test.api.query.qom; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.Query; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.QueryObjectModel; + +/** + * ChildNodeTest contains test cases that cover the QOM ChildNode + * condition. + */ +public class ChildNodeTest extends AbstractQOMTest { + + public void testChildNode() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + QueryObjectModel qom = qf.createQuery(qf.selector(testNodeType, "s"), + qf.childNode("s", testRoot), null, null); + checkQOM(qom, new Node[]{n}); + } + + public void testChildNodes() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + Node n3 = testRootNode.addNode(nodeName3, testNodeType); + superuser.save(); + + QueryObjectModel qom = qf.createQuery(qf.selector(testNodeType, "s"), + qf.childNode("s", testRoot), null, null); + checkQOM(qom, new Node[]{n1, n2, n3}); + } + + public void testPathDoesNotExist() throws RepositoryException { + QueryObjectModel qom = qf.createQuery(qf.selector(testNodeType, "s"), + qf.childNode("s", testRoot + "/" + nodeName1), + null, null); + checkQOM(qom, new Node[]{}); + } + + public void testChildNodesDoNotMatchSelector() + throws RepositoryException, NotExecutableException { + testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeIterator it = ntMgr.getPrimaryNodeTypes(); + NodeType testNt = ntMgr.getNodeType(testNodeType); + while (it.hasNext()) { + NodeType nt = it.nextNodeType(); + if (!testNt.isNodeType(nt.getName())) { + // perform test + QueryObjectModel qom = qf.createQuery( + qf.selector(nt.getName(), "s"), + qf.childNode("s", testRoot), null, null); + checkQOM(qom, new Node[]{}); + return; + } + } + throw new NotExecutableException("No suitable node type found to " + + "perform test against '" + testNodeType + "' nodes"); + } + + public void testRelativePath() throws RepositoryException { + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.childNode("s", testPath), null, null); + q.execute(); + fail("ChildNode with relative path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s WHERE " + + "ISCHILDNODE(s, [" + testPath + "])"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISCHILDNODE() with relative path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testSyntacticallyInvalidPath() throws RepositoryException { + String invalidPath = testRoot + "/" + nodeName1 + "["; + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.childNode("s", invalidPath), + null, null); + q.execute(); + fail("ChildNode with syntactically invalid path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s WHERE " + + "ISCHILDNODE(s, [" + invalidPath + "])"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISCHILDNODE() with syntactically invalid path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testNotASelectorName() throws RepositoryException { + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.childNode("x", testRoot), null, null); + q.execute(); + fail("ChildNode with an unknown selector name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s WHERE " + + "ISCHILDNODE(x, [" + testRoot + "])"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISCHILDNODE() with an unknown selector name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ColumnTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ColumnTest.java new file mode 100644 index 00000000000..4b3a47f40c9 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/ColumnTest.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.test.api.query.qom; + +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.query.qom.Column; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; +import javax.jcr.query.Row; +import javax.jcr.query.Query; + +/** + * ColumnTest contains test cases related to QOM column. + */ +public class ColumnTest extends AbstractQOMTest { + + private static final String SELECTOR_1 = "s"; + + private static final String SELECTOR_2 = "p"; + + private static final String TEST_VALUE = "value"; + + /** + * From the spec: + *

        + * If propertyName is not specified, a column is included for each + * single-valued non-residual property of the node type specified by the + * nodeType attribute of the selector selectorName. + *

        + * [..] If propertyName is not specified, + * columnName must not be specified, and the included columns will be + * named "selectorName.propertyName". + */ + public void testExpandColumnsForNodeType() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, SELECTOR_1), + null, + null, + new Column[]{qf.column(SELECTOR_1, null, null)}); + forQOMandSQL2(qom, new Callable() { + public Object call(Query query) throws RepositoryException { + QueryResult result = query.execute(); + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + NodeType nt = ntMgr.getNodeType(testNodeType); + PropertyDefinition[] propDefs = nt.getPropertyDefinitions(); + Set names = new HashSet(); + for (int i = 0; i < propDefs.length; i++) { + PropertyDefinition propDef = propDefs[i]; + if (!propDef.isMultiple() && !propDef.getName().equals("*")) { + String columnName = SELECTOR_1 + "." + propDef.getName(); + names.add(columnName); + } + } + for (String columnName : result.getColumnNames()) { + names.remove(columnName); + } + assertTrue("Missing required column(s): " + names, names.isEmpty()); + return null; + } + }); + } + + /** + * From the spec: + *

        + * If propertyName is specified, columnName is required and used to name + * the column in the tabular results. + */ + public void testColumnNames() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, SELECTOR_1), + null, + null, + new Column[]{qf.column(SELECTOR_1, propertyName1, propertyName1)}); + forQOMandSQL2(qom, new Callable() { + public Object call(Query query) throws RepositoryException { + QueryResult result = query.execute(); + List names = new ArrayList(Arrays.asList(result.getColumnNames())); + assertTrue("Missing column: " + propertyName1, names.remove(propertyName1)); + for (Iterator it = names.iterator(); it.hasNext(); ) { + fail(it.next() + " was not declared as a column"); + } + return null; + } + }); + } + + public void testMultiColumn() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, TEST_VALUE); + superuser.save(); + + final String columnName1 = SELECTOR_1 + "." + propertyName1; + final String columnName2 = SELECTOR_2 + "." + propertyName1; + QueryObjectModel qom = qf.createQuery( + qf.join( + qf.selector(testNodeType, SELECTOR_1), + qf.selector(testNodeType, SELECTOR_2), + QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, + qf.equiJoinCondition(SELECTOR_1, propertyName1, SELECTOR_2, propertyName1) + ), + qf.descendantNode(SELECTOR_1, testRoot), + null, + new Column[]{ + qf.column(SELECTOR_1, propertyName1, columnName1), + qf.column(SELECTOR_2, propertyName1, columnName2) + } + ); + forQOMandSQL2(qom, new Callable() { + public Object call(Query query) throws RepositoryException { + RowIterator rows = query.execute().getRows(); + assertTrue("empty result", rows.hasNext()); + Row r = rows.nextRow(); + assertEquals("unexpected value", TEST_VALUE, r.getValue(columnName1).getString()); + assertEquals("unexpected value", TEST_VALUE, r.getValue(columnName2).getString()); + return null; + } + }); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/DescendantNodeJoinConditionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/DescendantNodeJoinConditionTest.java new file mode 100644 index 00000000000..5dc69877fb4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/DescendantNodeJoinConditionTest.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.test.api.query.qom; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelConstants; + +/** + * DescendantNodeJoinConditionTest contains test cases that cover + * DescendantNodeJoinCondition. + */ +public class DescendantNodeJoinConditionTest extends AbstractJoinTest { + + private Node n1; + + private Node n2; + + protected void setUp() throws Exception { + super.setUp(); + n1 = testRootNode.addNode(nodeName1, testNodeType); + n2 = n1.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixReferenceable); + superuser.save(); + } + + public void testInnerJoin() throws RepositoryException { + JoinCondition c = qf.descendantNodeJoinCondition(LEFT, RIGHT); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, c); + checkQOM(qom, new Node[][]{{n2, n1}}); + } + + public void testRightOuterJoin() throws RepositoryException { + JoinCondition c = qf.descendantNodeJoinCondition(LEFT, RIGHT); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER, c); + checkQOM(qom, new Node[][]{{n2, n1}, {null, n2}}); + } + + public void testLeftOuterJoin() throws RepositoryException { + JoinCondition c = qf.descendantNodeJoinCondition(LEFT, RIGHT); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER, c); + List result = new ArrayList(); + result.add(new Node[]{n2, n1}); + // for each ancestor-or-self of testRootNode check + // whether it is of type testNodeType and add + // two matches in that case + Node n = testRootNode; + for (;;) { + if (n.isNodeType(testNodeType)) { + result.add(new Node[]{n1, n}); + result.add(new Node[]{n2, n}); + } + if (n.getDepth() == 0) { + break; + } else { + n = n.getParent(); + } + } + if (result.size() == 1) { + // n1 not yet covered + result.add(new Node[]{n1, null}); + } + checkQOM(qom, result.toArray(new Node[result.size()][])); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/DescendantNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/DescendantNodeTest.java new file mode 100644 index 00000000000..32680b0be10 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/DescendantNodeTest.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.test.api.query.qom; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.Query; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.QueryObjectModel; + +/** + * DescendantNodeTest contains test cases related to QOM + * DescendantNode constraints. + */ +public class DescendantNodeTest extends AbstractQOMTest { + + public void testDescendantNode() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + QueryObjectModel qom = qf.createQuery(qf.selector(testNodeType, "s"), + qf.descendantNode("s", testRoot), null, null); + checkQOM(qom, new Node[]{n}); + } + + public void testDescendantNodes() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + Node n21 = n2.addNode(nodeName1, testNodeType); + superuser.save(); + + QueryObjectModel qom = qf.createQuery(qf.selector(testNodeType, "s"), + qf.descendantNode("s", testRoot), null, null); + checkQOM(qom, new Node[]{n1, n2, n21}); + } + + public void testPathDoesNotExist() throws RepositoryException { + QueryObjectModel qom = qf.createQuery(qf.selector(testNodeType, "s"), + qf.descendantNode("s", testRoot + "/" + nodeName1), + null, null); + checkQOM(qom, new Node[]{}); + } + + public void testDescendantNodesDoNotMatchSelector() + throws RepositoryException, NotExecutableException { + testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeIterator it = ntMgr.getPrimaryNodeTypes(); + NodeType testNt = ntMgr.getNodeType(testNodeType); + while (it.hasNext()) { + NodeType nt = it.nextNodeType(); + if (!testNt.isNodeType(nt.getName())) { + // perform test + QueryObjectModel qom = qf.createQuery(qf.selector(nt.getName(), "s"), + qf.descendantNode("s", testRoot), null, null); + checkQOM(qom, new Node[]{}); + return; + } + } + throw new NotExecutableException("No suitable node type found to " + + "perform test against '" + testNodeType + "' nodes"); + } + + public void testRelativePath() throws RepositoryException { + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.descendantNode("s", testPath), null, null); + q.execute(); + fail("DescendantNode with relative path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s WHERE " + + "ISDESCENDANTNODE(s, [" + testPath + "])"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISDESCENDANTNODE() with relative path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testSyntacticallyInvalidPath() throws RepositoryException { + String invalidPath = testRoot + "/" + nodeName1 + "["; + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.descendantNode("s", invalidPath), null, null); + q.execute(); + fail("DescendantNode with syntactically invalid path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s WHERE " + + "ISDESCENDANTNODE(s, [" + invalidPath + "])"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISDESCENDANTNODE() with syntactically invalid path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testNotASelectorName() throws RepositoryException { + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.descendantNode("x", testRoot), null, null); + q.execute(); + fail("DescendantNode with an unknown selector name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s WHERE " + + "ISDESCENDANTNODE(x, [" + testRoot + "])"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISDESCENDANTNODE() with an unknown selector name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/EquiJoinConditionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/EquiJoinConditionTest.java new file mode 100644 index 00000000000..819eb9c504a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/EquiJoinConditionTest.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.test.api.query.qom; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelConstants; + +/** + * EquiJoinConditionTest contains test cases that cover + * EquiJoinCondition. + */ +public class EquiJoinConditionTest extends AbstractJoinTest { + + private Node n1; + + private Node n2; + + protected void setUp() throws Exception { + super.setUp(); + String value = createRandomString(10); + n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.setProperty(propertyName1, value); + + n2 = n1.addNode(nodeName2, testNodeType); + n2.setProperty(propertyName1, value); + n2.setProperty(propertyName2, value); + ensureMixinType(n2, mixReferenceable); + superuser.save(); + } + + public void testInnerJoin1() throws RepositoryException { + JoinCondition c = qf.equiJoinCondition( + LEFT, propertyName1, RIGHT, propertyName2); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, c); + checkQOM(qom, new Node[][]{{n1, n2}, {n2, n2}}); + } + + public void testInnerJoin2() throws RepositoryException { + JoinCondition c = qf.equiJoinCondition( + LEFT, propertyName2, RIGHT, propertyName1); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, c); + checkQOM(qom, new Node[][]{{n2, n1}, {n2, n2}}); + } + + public void testRightOuterJoin1() throws RepositoryException { + JoinCondition c = qf.equiJoinCondition( + LEFT, propertyName1, RIGHT, propertyName2); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER, c); + checkQOM(qom, new Node[][]{{null, n1}, {n1, n2}, {n2, n2}}); + } + + public void testRightOuterJoin2() throws RepositoryException { + JoinCondition c = qf.equiJoinCondition( + LEFT, propertyName2, RIGHT, propertyName1); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER, c); + checkQOM(qom, new Node[][]{{n2, n1}, {n2, n2}}); + } + + public void testLeftOuterJoin1() throws RepositoryException { + JoinCondition c = qf.equiJoinCondition( + LEFT, propertyName1, RIGHT, propertyName2); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER, c); + checkQOM(qom, new Node[][]{{n1, n2}, {n2, n2}}); + } + + + public void testLeftOuterJoin2() throws RepositoryException { + JoinCondition c = qf.equiJoinCondition( + LEFT, propertyName2, RIGHT, propertyName1); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER, c); + checkQOM(qom, new Node[][]{{n1, null}, {n2, n1}, {n2, n2}}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/FullTextSearchScoreTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/FullTextSearchScoreTest.java new file mode 100644 index 00000000000..53efc2284f5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/FullTextSearchScoreTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.qom.Ordering; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.RowIterator; +import javax.jcr.query.Query; + +/** + * FullTextSearchScoreTest contains fulltext search score tests. + */ +public class FullTextSearchScoreTest extends AbstractQOMTest { + + private static final String TEXT = "the quick brown fox jumps over the lazy dog."; + + protected void setUp() throws Exception { + super.setUp(); + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.setProperty(propertyName1, TEXT); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + n2.setProperty(propertyName1, TEXT); + n2.setProperty(propertyName2, TEXT); + superuser.save(); + } + + public void testOrdering() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.fullTextSearch("s", null, qf.literal(vf.createValue("fox"))), + qf.descendantNode("s", testRootNode.getPath()) + ), + new Ordering[]{qf.ascending(qf.fullTextSearchScore("s"))}, + null + ); + forQOMandSQL2(qom, new Callable() { + public Object call(Query query) throws RepositoryException { + RowIterator rows = query.execute().getRows(); + double previousScore = Double.NaN; + while (rows.hasNext()) { + double score = rows.nextRow().getScore("s"); + if (!Double.isNaN(previousScore)) { + assertTrue("wrong order", previousScore <= score); + } + previousScore = score; + } + return null; + } + }); + } + + public void testConstraint() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.and( + qf.fullTextSearch("s", null, qf.literal(vf.createValue("fox"))), + qf.comparison( + qf.fullTextSearchScore("s"), + QueryObjectModelFactory.JCR_OPERATOR_GREATER_THAN, + qf.literal(vf.createValue(Double.MIN_VALUE)) + ) + ), + qf.descendantNode("s", testRootNode.getPath()) + ), + new Ordering[]{qf.descending(qf.fullTextSearchScore("s"))}, + null + ); + forQOMandSQL2(qom, new Callable() { + public Object call(Query query) throws RepositoryException { + RowIterator rows = query.execute().getRows(); + while (rows.hasNext()) { + double score = rows.nextRow().getScore("s"); + if (!Double.isNaN(score)) { + assertTrue("wrong full text search score", Double.MIN_VALUE < score); + } + } + return null; + } + }); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/GetQueryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/GetQueryTest.java new file mode 100644 index 00000000000..a7e1c813501 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/GetQueryTest.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.test.api.query.qom; + +import java.util.ArrayList; +import java.util.List; +import java.util.Iterator; + +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * GetQueryTest contains test cases that check + * {@link QueryManager#getQuery(Node)}. + */ +public class GetQueryTest extends AbstractQOMTest { + + public void testGetQuery() throws RepositoryException, NotExecutableException { + checkNtQuery(); + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + List queries = new ArrayList(); + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.childNode("s", testRoot), + null, + null + ); + queries.add(qom); + queries.add(qm.createQuery(qom.getStatement(), Query.JCR_SQL2)); + if (isSupportedLanguage(qsXPATH)) { + String xpath = testPath + "/element(*, " + testNodeType + ")"; + queries.add(qm.createQuery(xpath, qsXPATH)); + } + if (isSupportedLanguage(qsSQL)) { + String sql = "select * from " + testNodeType + " where jcr:path like '" + testRoot + "/%'"; + queries.add(qm.createQuery(sql, qsSQL)); + } + for (Iterator it = queries.iterator(); it.hasNext(); ) { + Query q = it.next(); + String lang = q.getLanguage(); + checkResult(q.execute(), new Node[]{n}); + + Node stored = q.storeAsNode(testRoot + "/storedQuery"); + q = qm.getQuery(stored); + assertEquals("language of stored query does not match", lang, q.getLanguage()); + checkResult(q.execute(), new Node[]{n}); + stored.remove(); + } + } + + public void testInvalidQueryException() throws RepositoryException { + try { + qm.getQuery(testRootNode); + fail("getQuery() must throw InvalidQueryException when node is not of type nt:query"); + } catch (InvalidQueryException e) { + // expected + } + } + + /** + * Checks if the repository supports the nt:query node type otherwise throws + * a NotExecutableException. + * + * @throws NotExecutableException if nt:query is not supported. + * @throws RepositoryException if another error occurs. + */ + private void checkNtQuery() throws RepositoryException, NotExecutableException { + try { + superuser.getWorkspace().getNodeTypeManager().getNodeType(ntQuery); + } catch (NoSuchNodeTypeException e) { + // not supported + throw new NotExecutableException("repository does not support nt:query"); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/LengthTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/LengthTest.java new file mode 100644 index 00000000000..88fcce3d506 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/LengthTest.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.test.api.query.qom; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.Binary; +import javax.jcr.query.QueryResult; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModel; + +import java.io.ByteArrayInputStream; +import java.util.Calendar; +import java.math.BigDecimal; + +/** + * LengthTest performs tests with the Query Object Model length + * operand. + */ +public class LengthTest extends AbstractQOMTest { + + private Node node; + + + protected void setUp() throws Exception { + super.setUp(); + node = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + } + + protected void tearDown() throws Exception { + node = null; + super.tearDown(); + } + + public void testStringLength() throws RepositoryException { + node.setProperty(propertyName1, "abc"); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testBinaryLength() throws RepositoryException { + byte[] data = "abc".getBytes(); + Binary b = vf.createBinary(new ByteArrayInputStream(data)); + try { + node.setProperty(propertyName1, b); + } finally { + b.dispose(); + } + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testLongLength() throws RepositoryException { + node.setProperty(propertyName1, 123); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testDoubleLength() throws RepositoryException { + node.setProperty(propertyName1, Math.PI); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testDateLength() throws RepositoryException { + node.setProperty(propertyName1, Calendar.getInstance()); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testBooleanLength() throws RepositoryException { + node.setProperty(propertyName1, false); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testNameLength() throws RepositoryException { + node.setProperty(propertyName1, vf.createValue(node.getName(), PropertyType.NAME)); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testPathLength() throws RepositoryException { + node.setProperty(propertyName1, vf.createValue(node.getPath(), PropertyType.PATH)); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testReferenceLength() throws RepositoryException, NotExecutableException { + ensureMixinType(node, mixReferenceable); + superuser.save(); + node.setProperty(propertyName1, node); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testWeakReferenceLength() + throws RepositoryException, NotExecutableException { + ensureMixinType(node, mixReferenceable); + superuser.save(); + node.setProperty(propertyName1, vf.createValue(node, true)); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testURILength() throws RepositoryException { + node.setProperty(propertyName1, vf.createValue("http://example.com", PropertyType.URI)); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + public void testDecimalLength() throws RepositoryException { + node.setProperty(propertyName1, new BigDecimal(123)); + superuser.save(); + checkOperators(propertyName1, node.getProperty(propertyName1).getLength()); + } + + //------------------------< conversion tests >------------------------------ + + public void testLengthStringLiteral() throws RepositoryException { + node.setProperty(propertyName1, "abc"); + superuser.save(); + + String length = String.valueOf(node.getProperty(propertyName1).getLength()); + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue(length)); + } + + public void testLengthBinaryLiteral() throws RepositoryException { + node.setProperty(propertyName1, "abc"); + superuser.save(); + + String length = String.valueOf(node.getProperty(propertyName1).getLength()); + Binary b = vf.createBinary(new ByteArrayInputStream(length.getBytes())); + try { + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + vf.createValue(b)); + } finally { + b.dispose(); + } + } + + public void testLengthDoubleLiteral() throws RepositoryException { + node.setProperty(propertyName1, "abc"); + superuser.save(); + + double length = node.getProperty(propertyName1).getLength(); + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue(length)); + } + + public void testLengthDateLiteral() throws RepositoryException { + node.setProperty(propertyName1, "abc"); + superuser.save(); + + Calendar length = Calendar.getInstance(); + length.setTimeInMillis(node.getProperty(propertyName1).getLength()); + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue(length)); + } + + public void testLengthBooleanLiteral() throws RepositoryException { + try { + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue(false)); + fail("Boolean literal cannot be converted to long"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testLengthNameLiteral() throws RepositoryException { + try { + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue( + propertyName1, PropertyType.NAME)); + fail("Name literal cannot be converted to long"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testLengthPathLiteral() throws RepositoryException { + try { + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue( + node.getPath(), PropertyType.PATH)); + fail("Path literal cannot be converted to long"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testLengthReferenceLiteral() throws RepositoryException, NotExecutableException { + ensureMixinType(node, mixReferenceable); + superuser.save(); + try { + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue(node)); + fail("Reference literal cannot be converted to long"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testLengthWeakReferenceLiteral() throws RepositoryException, NotExecutableException { + ensureMixinType(node, mixReferenceable); + superuser.save(); + try { + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue(node, true)); + fail("Reference literal cannot be converted to long"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testLengthURILiteral() throws RepositoryException { + try { + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + vf.createValue(node.getPath(), PropertyType.URI)); + fail("URI literal cannot be converted to long"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testLengthDecimalLiteral() throws RepositoryException { + node.setProperty(propertyName1, "abc"); + superuser.save(); + + BigDecimal length = new BigDecimal(node.getProperty(propertyName1).getLength()); + executeQueries(propertyName1, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, vf.createValue(length)); + } + + //------------------------< internal helpers >------------------------------ + + private void checkOperators(String propertyName, + long length) throws RepositoryException { + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, length, true); + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, length - 1, false); + + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN, length - 1, true); + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN, length, false); + + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, length, true); + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, length + 1, false); + + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN, length + 1, true); + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN, length, false); + + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, length, true); + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, length - 1, false); + + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO, length - 1, true); + checkLength(propertyName, QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO, length, false); + } + + private void checkLength(String propertyName, + String operator, + long length, + boolean matches) throws RepositoryException { + Node[] expected; + if (matches) { + expected = new Node[]{node}; + } else { + expected = new Node[0]; + } + QueryResult[] results = executeQueries(propertyName, operator, length); + for (int i = 0; i < results.length; i++) { + checkResult(results[i], expected); + } + } + + private QueryResult[] executeQueries(String propertyName, + String operator, + long length) + throws RepositoryException { + Value v = vf.createValue(length); + return executeQueries(propertyName, operator, v); + } + + private QueryResult[] executeQueries(String propertyName, + String operator, + Value length) + throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.comparison( + qf.length( + qf.propertyValue( + "s", propertyName)), + operator, + qf.literal(length)) + + ), null, null); + QueryResult[] results = new QueryResult[2]; + results[0] = qom.execute(); + results[1] = qm.createQuery(qom.getStatement(), Query.JCR_SQL2).execute(); + return results; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NodeLocalNameTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NodeLocalNameTest.java new file mode 100644 index 00000000000..09f2adba28c --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NodeLocalNameTest.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModel; + +import java.util.Calendar; +import java.math.BigDecimal; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * NodeLocalNameTest checks if conversion of literals is correctly + * performed and operators work as specified. + */ +public class NodeLocalNameTest extends AbstractQOMTest { + + private Node node1; + + private String nodeLocalName; + + protected void setUp() throws Exception { + super.setUp(); + node1 = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + int colon = nodeName1.indexOf(':'); + if (colon != -1) { + nodeLocalName = nodeName1.substring(colon + 1); + } else { + nodeLocalName = nodeName1; + } + } + + protected void tearDown() throws Exception { + node1 = null; + super.tearDown(); + } + + public void testStringLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue(nodeLocalName); + Query q = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkResult(q.execute(), new Node[]{node1}); + } + + public void testStringLiteralInvalidName() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue("[" + nodeLocalName); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with STRING that cannot be converted to NAME must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testBinaryLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue( + nodeLocalName, PropertyType.BINARY); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{node1}); + } + + public void testDateLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue(Calendar.getInstance()); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{}); + } + + public void testDoubleLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue(Math.PI); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{}); + } + + public void testDecimalLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue(new BigDecimal(283)); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{}); + } + + public void testLongLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue(283); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{}); + } + + public void testBooleanLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue(true); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{}); + } + + public void testNameLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue( + nodeLocalName, PropertyType.NAME); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{node1}); + } + + public void testPathLiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue( + nodeLocalName, PropertyType.PATH); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{node1}); + + literal = superuser.getValueFactory().createValue( + node1.getPath(), PropertyType.PATH); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with absolute PATH must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + + literal = superuser.getValueFactory().createValue( + nodeName1 + "/" + nodeName1, PropertyType.PATH); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with PATH length >1 must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testReferenceLiteral() throws RepositoryException, + NotExecutableException { + ensureMixinType(node1, mixReferenceable); + superuser.save(); + Value literal = superuser.getValueFactory().createValue(node1); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{}); + } + + public void testWeakReferenceLiteral() throws RepositoryException, + NotExecutableException { + ensureMixinType(node1, mixReferenceable); + superuser.save(); + Value literal = superuser.getValueFactory().createValue(node1, true); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{}); + } + + public void testURILiteral() throws RepositoryException { + Value literal = superuser.getValueFactory().createValue("http://example.com", PropertyType.URI); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with URI that cannot be converted to NAME must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testEqualTo() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, false, true, false); + } + + public void testGreaterThan() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN, true, false, false); + } + + public void testGreaterThanOrEqualTo() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO, true, true, false); + } + + public void testLessThan() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN, false, false, true); + } + + public void testLessThanOrEqualTo() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO, false, true, true); + } + + public void testLike() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_LIKE, false, true, false); + } + + public void testNotEqualTo() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO, true, false, true); + } + + //------------------------------< helper >---------------------------------- + + private void checkOperator(String operator, + boolean matchesLesser, + boolean matchesEqual, + boolean matchesGreater) + throws RepositoryException { + checkOperatorSingleLiteral(createLexicographicallyLesser(nodeLocalName), operator, matchesLesser); + checkOperatorSingleLiteral(nodeLocalName, operator, matchesEqual); + checkOperatorSingleLiteral(createLexicographicallyGreater(nodeLocalName), operator, matchesGreater); + } + + private void checkOperatorSingleLiteral(String literal, + String operator, + boolean matches) + throws RepositoryException { + Value value = superuser.getValueFactory().createValue(literal); + QueryObjectModel qom = createQuery(operator, value); + checkQOM(qom, matches ? new Node[]{node1} : new Node[0]); + } + + private String createLexicographicallyGreater(String name) { + StringBuffer tmp = new StringBuffer(name); + tmp.setCharAt(tmp.length() - 1, (char) (tmp.charAt(tmp.length() - 1) + 1)); + return tmp.toString(); + } + + private String createLexicographicallyLesser(String name) { + StringBuffer tmp = new StringBuffer(name); + tmp.setCharAt(tmp.length() - 1, (char) (tmp.charAt(tmp.length() - 1) - 1)); + return tmp.toString(); + } + + private QueryObjectModel createQuery(String operator, Value literal) + throws RepositoryException { + return qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.comparison( + qf.nodeLocalName("s"), + operator, + qf.literal(literal) + ) + ), null, null); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NodeNameTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NodeNameTest.java new file mode 100644 index 00000000000..a26fa1ca4dc --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NodeNameTest.java @@ -0,0 +1,344 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.query.Query; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModel; + +import java.util.Calendar; +import java.math.BigDecimal; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * NodeNameTest checks if conversion of literals is correctly + * performed and operators work as specified. + */ +public class NodeNameTest extends AbstractQOMTest { + + private Node node1; + + protected void setUp() throws Exception { + super.setUp(); + node1 = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + } + + protected void tearDown() throws Exception { + node1 = null; + super.tearDown(); + } + + public void testStringLiteral() throws RepositoryException { + Value literal = vf.createValue(nodeName1); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{node1}); + } + + public void testStringLiteralInvalidName() throws RepositoryException { + Value literal = vf.createValue("[" + nodeName1); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with STRING that cannot be converted to NAME must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = '" + literal.getString() + "'"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with STRING that cannot be converted to NAME must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testBinaryLiteral() throws RepositoryException { + Value literal = vf.createValue( + nodeName1, PropertyType.BINARY); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{node1}); + } + + public void testDateLiteral() throws RepositoryException { + Value literal = vf.createValue(Calendar.getInstance()); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with DATE must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST('" + literal.getString() + "' AS DATE)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with DATE must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testDoubleLiteral() throws RepositoryException { + Value literal = vf.createValue(Math.PI); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with DOUBLE must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST('" + literal.getString() + "' AS DOUBLE)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with DOUBLE must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testDecimalLiteral() throws RepositoryException { + Value literal = vf.createValue(new BigDecimal(283)); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with DECIMAL must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST('" + literal.getString() + "' AS DECIMAL)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with DECIMAL must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testLongLiteral() throws RepositoryException { + Value literal = vf.createValue(283); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with LONG must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST(" + literal.getString() + " AS LONG)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with LONG must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testBooleanLiteral() throws RepositoryException { + Value literal = vf.createValue(true); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with BOOLEAN must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST(" + literal.getString() + " AS BOOLEAN)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with BOOLEAN must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testNameLiteral() throws RepositoryException { + Value literal = vf.createValue(nodeName1, PropertyType.NAME); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{node1}); + } + + public void testPathLiteral() throws RepositoryException { + Value literal = vf.createValue(nodeName1, PropertyType.PATH); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{node1}); + + literal = vf.createValue(node1.getPath(), PropertyType.PATH); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with absolute PATH must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST('" + literal.getString() + "' AS PATH)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with absolute PATH must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + + literal = vf.createValue(nodeName1 + "/" + nodeName1, PropertyType.PATH); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with PATH length >1 must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST('" + literal.getString() + "' AS PATH)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with PATH length >1 must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testReferenceLiteral() throws RepositoryException, + NotExecutableException { + ensureMixinType(node1, mixReferenceable); + superuser.save(); + Value literal = vf.createValue(node1); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with REFERENCE must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST('" + literal.getString() + "' AS REFERENCE)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with REFERENCE must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testWeakReferenceLiteral() throws RepositoryException, + NotExecutableException { + ensureMixinType(node1, mixReferenceable); + superuser.save(); + Value literal = vf.createValue(node1, true); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with WEAKREFERENCE must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST('" + literal.getString() + "' AS WEAKREFERENCE)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with absolute WEAKREFERENCE must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + /** + * If the URI consists of a single path segment without a colon (for + * example, simply bar) it is converted to a NAME by percent-unescaping + * followed by UTF-8-decoding of the byte sequence. If it has a redundant + * leading ./ followed by a single segment (with or without a colon, like + * ./bar or ./foo:bar ) the redundant ./ is removed and the remainder is + * converted to a NAME in the same way. Otherwise a ValueFormatException is + * thrown. + */ + public void testURILiteral() throws RepositoryException { + Value literal = vf.createValue("./" + nodeName1, PropertyType.URI); + QueryObjectModel qom = createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal); + checkQOM(qom, new Node[]{node1}); + + literal = vf.createValue("http://example.com", PropertyType.URI); + try { + createQuery(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, literal).execute(); + fail("NodeName comparison with URI that cannot be converted to NAME must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE NAME(s) = CAST('" + literal.getString() + "' AS URI)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("NAME() comparison with URI that cannot be converted to NAME must fail with InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testEqualTo() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, false, true, false); + } + + public void testNotEqualTo() throws RepositoryException { + checkOperator(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO, true, false, true); + } + + //------------------------------< helper >---------------------------------- + + private void checkOperator(String operator, + boolean matchesLesser, + boolean matchesEqual, + boolean matchesGreater) + throws RepositoryException { + checkOperatorSingleLiteral(createLexicographicallyLesser(nodeName1), operator, matchesLesser); + checkOperatorSingleLiteral(nodeName1, operator, matchesEqual); + checkOperatorSingleLiteral(createLexicographicallyGreater(nodeName1), operator, matchesGreater); + } + + private void checkOperatorSingleLiteral(String literal, + String operator, + boolean matches) + throws RepositoryException { + Value value = vf.createValue(literal); + QueryObjectModel qom = createQuery(operator, value); + checkQOM(qom, matches ? new Node[]{node1} : new Node[0]); + } + + private String createLexicographicallyGreater(String name) { + StringBuffer tmp = new StringBuffer(name); + tmp.setCharAt(tmp.length() - 1, (char) (tmp.charAt(tmp.length() - 1) + 1)); + return tmp.toString(); + } + + private String createLexicographicallyLesser(String name) { + StringBuffer tmp = new StringBuffer(name); + tmp.setCharAt(tmp.length() - 1, (char) (tmp.charAt(tmp.length() - 1) - 1)); + return tmp.toString(); + } + + private QueryObjectModel createQuery(String operator, Value literal) + throws RepositoryException { + return qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.comparison( + qf.nodeName("s"), + operator, + qf.literal(literal) + ) + ), null, null); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NotConstraintTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NotConstraintTest.java new file mode 100644 index 00000000000..049095f0449 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/NotConstraintTest.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.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.qom.QueryObjectModel; + +/** + * NotConstraintTest contains tests that check NOT constraints. + */ +public class NotConstraintTest extends AbstractQOMTest { + + public void testNot() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.setProperty(propertyName1, "foo"); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + n2.setProperty(propertyName2, "bar"); + superuser.save(); + + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.descendantNode("s", testRootNode.getPath()), + qf.not( + qf.propertyExistence("s", propertyName1) + ) + ), + null, + null + ); + checkQOM(qom, new Node[]{n2}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/OrConstraintTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/OrConstraintTest.java new file mode 100644 index 00000000000..4b25d929c49 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/OrConstraintTest.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.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.qom.QueryObjectModel; + +/** + * OrConstraintTest contains tests that check OR constraints. + */ +public class OrConstraintTest extends AbstractQOMTest { + + public void testOr() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.setProperty(propertyName1, "foo"); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + n2.setProperty(propertyName2, "bar"); + superuser.save(); + + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.descendantNode("s", testRootNode.getPath()), + qf.or( + qf.propertyExistence("s", propertyName1), + qf.propertyExistence("s", propertyName2) + ) + ), + null, + null + ); + checkQOM(qom, new Node[]{n1, n2}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/OrderingTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/OrderingTest.java new file mode 100644 index 00000000000..edc67cb779d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/OrderingTest.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.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.Ordering; + +/** + * OrderingTest contains test cases that check + */ +public class OrderingTest extends AbstractJoinTest { + + private Node n1; + + private Node n2; + + protected void setUp() throws Exception { + super.setUp(); + String value = "a"; + n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.setProperty(propertyName1, value); + n1.setProperty(propertyName2, "b"); + + n2 = n1.addNode(nodeName2, testNodeType); + n2.setProperty(propertyName1, value); + n2.setProperty(propertyName2, value); + superuser.save(); + } + + public void testMultipleSelectors() throws RepositoryException { + // ascending + Ordering[] orderings = new Ordering[]{ + qf.ascending(qf.propertyValue(LEFT, propertyName2)) + }; + QueryObjectModel qom = createQuery(orderings); + checkResultOrder(qom, SELECTOR_NAMES, new Node[][]{{n2, n2}, {n1, n2}}); + + // descending + orderings[0] = qf.descending(qf.propertyValue(LEFT, propertyName2)); + qom = createQuery(orderings); + checkResultOrder(qom, SELECTOR_NAMES, new Node[][]{{n1, n2}, {n2, n2}}); + } + + protected QueryObjectModel createQuery(Ordering[] orderings) + throws RepositoryException { + JoinCondition c = qf.equiJoinCondition( + LEFT, propertyName1, RIGHT, propertyName2); + QueryObjectModel qom = createQuery( + QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, c); + return qf.createQuery(qom.getSource(), qom.getConstraint(), + orderings, qom.getColumns()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/PropertyExistenceTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/PropertyExistenceTest.java new file mode 100644 index 00000000000..32e08764ecf --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/PropertyExistenceTest.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.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.qom.QueryObjectModel; + +/** + * PropertyExistenceTest performs a test with + * PropertyExistence. + */ +public class PropertyExistenceTest extends AbstractQOMTest { + + public void testPropertyExistence() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.setProperty(propertyName1, "abc"); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + n2.setProperty(propertyName2, "abc"); + superuser.save(); + + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.propertyExistence("s", propertyName1) + ), null, null); + checkQOM(qom, new Node[]{n1}); + + qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.propertyExistence("s", propertyName2) + ), null, null); + checkQOM(qom, new Node[]{n2}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/PropertyValueTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/PropertyValueTest.java new file mode 100644 index 00000000000..75ab8a7288d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/PropertyValueTest.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.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.query.qom.QueryObjectModelFactory; +import javax.jcr.query.qom.QueryObjectModel; + +/** + * PropertyValueTest performs a test with property value + * comparision. + */ +public class PropertyValueTest extends AbstractQOMTest { + + private static final String TEXT = "abc"; + + public void testPropertyExistence() throws RepositoryException { + Node n1 = testRootNode.addNode(nodeName1, testNodeType); + n1.setProperty(propertyName1, TEXT); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + n2.setProperty(propertyName2, TEXT); + superuser.save(); + + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.comparison( + qf.propertyValue("s", propertyName1), + QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, + qf.literal(vf.createValue(TEXT)) + ) + ), null, null); + checkQOM(qom, new Node[]{n1}); + + qom = qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.comparison( + qf.propertyValue("s", propertyName2), + QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, + qf.literal(vf.createValue(TEXT)) + ) + ), null, null); + checkQOM(qom, new Node[]{n2}); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/QueryObjectModelFactoryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/QueryObjectModelFactoryTest.java new file mode 100644 index 00000000000..193569c9f1b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/QueryObjectModelFactoryTest.java @@ -0,0 +1,632 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.query.qom; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +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.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.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.StaticOperand; +import javax.jcr.query.qom.UpperCase; +import javax.jcr.query.qom.Literal; + +/** + * QueryObjectModelFactoryTest tests all methods on the + * {@link QueryObjectModelFactory}. + */ +public class QueryObjectModelFactoryTest extends AbstractQOMTest { + + /** + * A test selector name. + */ + private static final String SELECTOR_NAME1 = "selector1"; + + /** + * Another test selector name. + */ + private static final String SELECTOR_NAME2 = "selector2"; + + /** + * A test column name. + */ + private static final String COLUMN_NAME = "column"; + + /** + * A test variable name. + */ + private static final String VARIABLE_NAME = "varName"; + + /** + * A test full text search expression + */ + private static final String FULLTEXT_SEARCH_EXPR = "foo -bar"; + + /** + * Set of all possible operators. + */ + private static final Set OPERATORS = new HashSet(); + + /** + * Set of all possible join types. + */ + private static final Set JOIN_TYPES = new HashSet(); + + static { + OPERATORS.add(QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO); + OPERATORS.add(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN); + OPERATORS.add(QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO); + OPERATORS.add(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN); + OPERATORS.add(QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO); + OPERATORS.add(QueryObjectModelConstants.JCR_OPERATOR_LIKE); + OPERATORS.add(QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO); + + JOIN_TYPES.add(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER); + JOIN_TYPES.add(QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER); + JOIN_TYPES.add(QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER); + } + + /** + * Test case for {@link QueryObjectModelFactory#and(Constraint, Constraint)} + */ + public void testAnd() throws RepositoryException { + PropertyExistence c1 = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + PropertyExistence c2 = qf.propertyExistence(SELECTOR_NAME1, propertyName2); + And and = qf.and(c1, c2); + assertTrue("Not a PropertyExistence constraint", + and.getConstraint1() instanceof PropertyExistence); + assertTrue("Not a PropertyExistence constraint", + and.getConstraint2() instanceof PropertyExistence); + } + + /** + * Test case for {@link QueryObjectModelFactory#ascending(DynamicOperand)} + */ + public void testOrderingAscending() throws RepositoryException { + PropertyValue op = qf.propertyValue(SELECTOR_NAME1, propertyName1); + Ordering asc = qf.ascending(op); + assertEquals("Ordering.getOrder() must return QueryObjectModelConstants.ORDER_ASCENDING", + QueryObjectModelConstants.JCR_ORDER_ASCENDING, asc.getOrder()); + assertTrue("Not a PropertyValue operand", asc.getOperand() instanceof PropertyValue); + } + + /** + * Test case for {@link QueryObjectModelFactory#bindVariable(String)} + */ + public void testBindVariableValue() throws RepositoryException { + BindVariableValue bindVar = qf.bindVariable(propertyName1); + assertEquals("Wrong variable name", propertyName1, bindVar.getBindVariableName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#childNode(String, String)} + */ + public void testChildNode() throws RepositoryException { + ChildNode childNode = qf.childNode(SELECTOR_NAME1, testRootNode.getPath()); + assertEquals("Wrong path", testRootNode.getPath(), childNode.getParentPath()); + assertEquals("Wrong selector name", SELECTOR_NAME1, childNode.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#childNode(String, String)} + */ + public void testChildNodeWithSelector() throws RepositoryException { + ChildNode childNode = qf.childNode(SELECTOR_NAME1, testRootNode.getPath()); + assertEquals("Wrong path", testRootNode.getPath(), childNode.getParentPath()); + assertEquals("Wrong selector name", SELECTOR_NAME1, childNode.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#childNodeJoinCondition(String, String)} + */ + public void testChildNodeJoinCondition() throws RepositoryException { + ChildNodeJoinCondition cond = qf.childNodeJoinCondition(SELECTOR_NAME1, SELECTOR_NAME2); + assertEquals("Wrong selector name", cond.getChildSelectorName(), SELECTOR_NAME1); + assertEquals("Wrong selector name", cond.getParentSelectorName(), SELECTOR_NAME2); + } + + /** + * Test case for {@link QueryObjectModelFactory#column(String, String, String)} + */ + public void testColumn() throws RepositoryException { + Column col = qf.column(SELECTOR_NAME1, propertyName1, propertyName1); + assertEquals("Wrong selector name", SELECTOR_NAME1, col.getSelectorName()); + assertEquals("Wrong property name", propertyName1, col.getPropertyName()); + assertEquals("Wrong column name", propertyName1, col.getColumnName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#column(String, String, String)} + */ + public void testColumnAllProperties() throws RepositoryException { + Column col = qf.column(SELECTOR_NAME1, null, null); + assertEquals("Wrong selector name", SELECTOR_NAME1, col.getSelectorName()); + assertNull("Property name must be null", col.getPropertyName()); + assertNull("Column name must be null", col.getColumnName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#column(String, String, String)} + */ + public void testColumnWithColumnName() throws RepositoryException { + Column col = qf.column(SELECTOR_NAME1, propertyName1, COLUMN_NAME); + assertEquals("Wrong selector name", SELECTOR_NAME1, col.getSelectorName()); + assertEquals("Wrong property name", propertyName1, col.getPropertyName()); + assertEquals("Wrong column name", COLUMN_NAME, col.getColumnName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#column(String, String, String)} + */ + public void testColumnWithSelector() throws RepositoryException { + Column col = qf.column(SELECTOR_NAME1, propertyName1, COLUMN_NAME); + assertEquals("Wrong selector name", SELECTOR_NAME1, col.getSelectorName()); + assertEquals("Wrong property name", propertyName1, col.getPropertyName()); + assertEquals("Wrong column name", COLUMN_NAME, col.getColumnName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#comparison(DynamicOperand, String, StaticOperand)} + */ + public void testComparison() throws RepositoryException { + PropertyValue op1 = qf.propertyValue(SELECTOR_NAME1, propertyName1); + BindVariableValue op2 = qf.bindVariable(VARIABLE_NAME); + for (Iterator it = OPERATORS.iterator(); it.hasNext(); ) { + String operator = it.next(); + Comparison comp = qf.comparison(op1, operator, op2); + assertTrue("Not a PropertyValue operand", comp.getOperand1() instanceof PropertyValue); + assertTrue("Not a BindVariableValue operand", comp.getOperand2() instanceof BindVariableValue); + assertEquals("Wrong operator", operator, comp.getOperator()); + } + } + + public void testCreateQuery() throws RepositoryException { + Selector selector = qf.selector(testNodeType, SELECTOR_NAME1); + QueryObjectModel qom = qf.createQuery(selector, null, null, null); + assertTrue("Not a selector source", qom.getSource() instanceof Selector); + assertNull("Constraint must be null", qom.getConstraint()); + assertEquals("Wrong size of orderings", 0, qom.getOrderings().length); + assertEquals("Wrong size of columns", 0, qom.getColumns().length); + } + + public void testCreateQueryWithConstraint() throws RepositoryException { + Selector selector = qf.selector(testNodeType, SELECTOR_NAME1); + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + QueryObjectModel qom = qf.createQuery( + selector, propExist, null, null); + assertTrue("Not a selector source", qom.getSource() instanceof Selector); + assertTrue("Not a property existence constraint", qom.getConstraint() instanceof PropertyExistence); + assertEquals("Wrong size of orderings", 0, qom.getOrderings().length); + assertEquals("Wrong size of columns", 0, qom.getColumns().length); + } + + public void testCreateQueryWithConstraintAndOrdering() throws RepositoryException { + Selector selector = qf.selector(testNodeType, SELECTOR_NAME1); + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + PropertyValue propValue = qf.propertyValue(SELECTOR_NAME1, propertyName1); + Ordering ordering = qf.ascending(propValue); + QueryObjectModel qom = qf.createQuery(selector, propExist, + new Ordering[]{ordering}, null); + assertTrue("Not a selector source", qom.getSource() instanceof Selector); + assertTrue("Not a property existence constraint", qom.getConstraint() instanceof PropertyExistence); + assertEquals("Wrong size of orderings", 1, qom.getOrderings().length); + assertEquals("Wrong size of columns", 0, qom.getColumns().length); + } + + public void testCreateQueryWithConstraintOrderingAndColumn() throws RepositoryException { + Selector selector = qf.selector(testNodeType, SELECTOR_NAME1); + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + PropertyValue propValue = qf.propertyValue(SELECTOR_NAME1, propertyName1); + Ordering ordering = qf.ascending(propValue); + Column column = qf.column(SELECTOR_NAME1, propertyName1, propertyName1); + QueryObjectModel qom = qf.createQuery(selector, propExist, + new Ordering[]{ordering}, new Column[]{column}); + assertTrue("Not a selector source", qom.getSource() instanceof Selector); + assertTrue("Not a property existence constraint", qom.getConstraint() instanceof PropertyExistence); + assertEquals("Wrong size of orderings", 1, qom.getOrderings().length); + assertEquals("Wrong size of columns", 1, qom.getColumns().length); + } + + public void testCreateQueryFromSource() throws RepositoryException { + Source selector = qf.selector(testNodeType, SELECTOR_NAME1); + QueryObjectModel qom = qf.createQuery(selector, null, null, null); + assertTrue("Not a selector source", qom.getSource() instanceof Selector); + assertNull("Constraint must be null", qom.getConstraint()); + assertEquals("Wrong size of orderings", 0, qom.getOrderings().length); + assertEquals("Wrong size of columns", 0, qom.getColumns().length); + } + + public void testCreateQueryFromSourceWithConstraint() throws RepositoryException { + Source selector = qf.selector(testNodeType, SELECTOR_NAME1); + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + QueryObjectModel qom = qf.createQuery( + selector, propExist, null, null); + assertTrue("Not a selector source", qom.getSource() instanceof Selector); + assertTrue("Not a property existence constraint", qom.getConstraint() instanceof PropertyExistence); + assertEquals("Wrong size of orderings", 0, qom.getOrderings().length); + assertEquals("Wrong size of columns", 0, qom.getColumns().length); + } + + public void testCreateQueryFromSourceWithConstraintAndOrdering() throws RepositoryException { + Source selector = qf.selector(testNodeType, SELECTOR_NAME1); + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + PropertyValue propValue = qf.propertyValue(SELECTOR_NAME1, propertyName1); + Ordering ordering = qf.ascending(propValue); + QueryObjectModel qom = qf.createQuery(selector, propExist, + new Ordering[]{ordering}, null); + assertTrue("Not a selector source", qom.getSource() instanceof Selector); + assertTrue("Not a property existence constraint", qom.getConstraint() instanceof PropertyExistence); + assertEquals("Wrong size of orderings", 1, qom.getOrderings().length); + assertEquals("Wrong size of columns", 0, qom.getColumns().length); + } + + public void testCreateQueryFromSourceWithConstraintOrderingAndColumn() throws RepositoryException { + Source selector = qf.selector(testNodeType, SELECTOR_NAME1); + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + PropertyValue propValue = qf.propertyValue(SELECTOR_NAME1, propertyName1); + Ordering ordering = qf.ascending(propValue); + Column column = qf.column(SELECTOR_NAME1, propertyName1, propertyName1); + QueryObjectModel qom = qf.createQuery(selector, propExist, + new Ordering[]{ordering}, new Column[]{column}); + assertTrue("Not a selector source", qom.getSource() instanceof Selector); + assertTrue("Not a property existence constraint", qom.getConstraint() instanceof PropertyExistence); + assertEquals("Wrong size of orderings", 1, qom.getOrderings().length); + assertEquals("Wrong size of columns", 1, qom.getColumns().length); + } + + /** + * Test case for {@link QueryObjectModelFactory#descendantNode(String, String)} + */ + public void testDescendantNode() throws RepositoryException { + DescendantNode descNode = qf.descendantNode(SELECTOR_NAME1, testRootNode.getPath()); + assertEquals("Wrong selector", SELECTOR_NAME1, descNode.getSelectorName()); + assertEquals("Wrong path", testRootNode.getPath(), descNode.getAncestorPath()); + } + + /** + * Test case for {@link QueryObjectModelFactory#descendantNode(String, String)} + */ + public void testDescendantNodeWithSelector() throws RepositoryException { + DescendantNode descNode = qf.descendantNode(SELECTOR_NAME1, testRootNode.getPath()); + assertEquals("Wrong selector name", SELECTOR_NAME1, descNode.getSelectorName()); + assertEquals("Wrong path", testRootNode.getPath(), descNode.getAncestorPath()); + } + + /** + * Test case for {@link QueryObjectModelFactory#descendantNodeJoinCondition(String, String)} + */ + public void testDescendantNodeJoinCondition() throws RepositoryException { + DescendantNodeJoinCondition cond = qf.descendantNodeJoinCondition(SELECTOR_NAME1, SELECTOR_NAME2); + assertEquals("Wrong selector name", SELECTOR_NAME1, cond.getDescendantSelectorName()); + assertEquals("Wrong selector name", SELECTOR_NAME2, cond.getAncestorSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#descending(DynamicOperand)} + */ + public void testOrderingDescending() throws RepositoryException { + PropertyValue op = qf.propertyValue(SELECTOR_NAME1, propertyName1); + Ordering desc = qf.descending(op); + assertEquals("Ordering.getOrder() must return QueryObjectModelConstants.ORDER_DESCENDING", + QueryObjectModelConstants.JCR_ORDER_DESCENDING, desc.getOrder()); + assertTrue("Not a PropertyValue operand", desc.getOperand() instanceof PropertyValue); + } + + /** + * Test case for {@link QueryObjectModelFactory#equiJoinCondition(String, String, String, String)} + */ + public void testEquiJoinCondition() throws RepositoryException { + EquiJoinCondition cond = qf.equiJoinCondition(SELECTOR_NAME1, propertyName1, SELECTOR_NAME2, propertyName2); + assertEquals("Wrong selector name", SELECTOR_NAME1, cond.getSelector1Name()); + assertEquals("Wrong property name", propertyName1, cond.getProperty1Name()); + assertEquals("Wrong selector name", SELECTOR_NAME2, cond.getSelector2Name()); + assertEquals("Wrong property name", propertyName2, cond.getProperty2Name()); + } + + /** + * Test case for {@link QueryObjectModelFactory#fullTextSearch(String, String, StaticOperand)} + */ + public void testFullTextSearch() throws RepositoryException { + FullTextSearch ftSearch = qf.fullTextSearch( + SELECTOR_NAME1, propertyName1, + qf.literal(vf.createValue(FULLTEXT_SEARCH_EXPR))); + assertEquals("Wrong selector name", SELECTOR_NAME1, ftSearch.getSelectorName()); + assertEquals("Wrong propertyName", propertyName1, ftSearch.getPropertyName()); + + StaticOperand op = ftSearch.getFullTextSearchExpression(); + assertNotNull(op); + assertTrue("not a Literal", op instanceof Literal); + Literal literal = (Literal) op; + assertEquals(FULLTEXT_SEARCH_EXPR, literal.getLiteralValue().getString()); + } + + /** + * Test case for {@link QueryObjectModelFactory#fullTextSearch(String, String, StaticOperand)} + */ + public void testFullTextSearchAllProperties() throws RepositoryException { + FullTextSearch ftSearch = qf.fullTextSearch( + SELECTOR_NAME1, null, + qf.literal(vf.createValue(FULLTEXT_SEARCH_EXPR))); + assertEquals("Wrong selector name", SELECTOR_NAME1, ftSearch.getSelectorName()); + assertNull("Property name must be null", ftSearch.getPropertyName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#fullTextSearch(String, String, StaticOperand)} + */ + public void testFullTextSearchWithBindVariableValue() throws RepositoryException { + FullTextSearch ftSearch = qf.fullTextSearch( + SELECTOR_NAME1, propertyName1, + qf.bindVariable(VARIABLE_NAME)); + assertEquals("Wrong selector name", SELECTOR_NAME1, ftSearch.getSelectorName()); + assertEquals("Wrong propertyName", propertyName1, ftSearch.getPropertyName()); + + StaticOperand op = ftSearch.getFullTextSearchExpression(); + assertNotNull(op); + assertTrue("not a BindVariableValue", op instanceof BindVariableValue); + BindVariableValue value = (BindVariableValue) op; + assertEquals(VARIABLE_NAME, value.getBindVariableName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#fullTextSearchScore(String)} + */ + public void testFullTextSearchScore() throws RepositoryException { + FullTextSearchScore score = qf.fullTextSearchScore(SELECTOR_NAME1); + assertEquals("Wrong selector name", SELECTOR_NAME1, score.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#fullTextSearchScore(String)} + */ + public void testFullTextSearchScoreWithSelector() throws RepositoryException { + FullTextSearchScore score = qf.fullTextSearchScore(SELECTOR_NAME1); + assertEquals("Wrong selector name", SELECTOR_NAME1, score.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#join(Source, Source, String, JoinCondition)} + */ + public void testJoin() throws RepositoryException { + Selector s1 = qf.selector(ntBase, SELECTOR_NAME1); + Selector s2 = qf.selector(testNodeType, SELECTOR_NAME1); + JoinCondition cond = qf.equiJoinCondition(ntBase, jcrPrimaryType, testNodeType, jcrPrimaryType); + for (Iterator it = JOIN_TYPES.iterator(); it.hasNext(); ) { + String joinType = it.next(); + Join join = qf.join(s1, s2, joinType, cond); + assertTrue("Not a selector source", join.getLeft() instanceof Selector); + assertTrue("Not a selector source", join.getRight() instanceof Selector); + assertEquals("Wrong join type", joinType, join.getJoinType()); + assertTrue("Not an EquiJoinCondition", join.getJoinCondition() instanceof EquiJoinCondition); + } + } + + /** + * Test case for {@link QueryObjectModelFactory#length(PropertyValue)} + */ + public void testLength() throws RepositoryException { + PropertyValue propValue = qf.propertyValue(SELECTOR_NAME1, propertyName1); + Length len = qf.length(propValue); + assertNotNull("Property value must not be null", len.getPropertyValue()); + } + + /** + * Test case for {@link QueryObjectModelFactory#literal(Value)} + */ + public void testLiteral() throws RepositoryException { + Value v = superuser.getValueFactory().createValue("test"); + Literal literal = qf.literal(v); + assertEquals("Wrong literal value", v.getString(), + literal.getLiteralValue().getString()); + } + + /** + * Test case for {@link QueryObjectModelFactory#lowerCase(DynamicOperand)} + */ + public void testLowerCase() throws RepositoryException { + PropertyValue propValue = qf.propertyValue(SELECTOR_NAME1, propertyName1); + LowerCase lower = qf.lowerCase(propValue); + assertTrue("Not a property value operand", lower.getOperand() instanceof PropertyValue); + } + + /** + * Test case for {@link QueryObjectModelFactory#nodeLocalName(String)} + */ + public void testNodeLocalName() throws RepositoryException { + NodeLocalName localName = qf.nodeLocalName(SELECTOR_NAME1); + assertEquals("Wrong selector name", SELECTOR_NAME1, localName.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#nodeLocalName(String)} + */ + public void testNodeLocalNameWithSelector() throws RepositoryException { + NodeLocalName localName = qf.nodeLocalName(SELECTOR_NAME1); + assertEquals("Wrong selector name", SELECTOR_NAME1, localName.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#nodeName(String)} + */ + public void testNodeName() throws RepositoryException { + NodeName nodeName = qf.nodeName(SELECTOR_NAME1); + assertEquals("Wrong selector name", SELECTOR_NAME1, nodeName.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#nodeName(String)} + */ + public void testNodeNameWithSelector() throws RepositoryException { + NodeName nodeName = qf.nodeName(SELECTOR_NAME1); + assertEquals("Wrong selector name", SELECTOR_NAME1, nodeName.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#not(Constraint)} + */ + public void testNot() throws RepositoryException { + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + Not not = qf.not(propExist); + assertTrue("Not a property existence constraint", not.getConstraint() instanceof PropertyExistence); + } + + /** + * Test case for {@link QueryObjectModelFactory#or(Constraint, Constraint)} + */ + public void testOr() throws RepositoryException { + PropertyExistence c1 = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + PropertyExistence c2 = qf.propertyExistence(SELECTOR_NAME1, propertyName2); + Or or = qf.or(c1, c2); + assertTrue("Not a PropertyExistence constraint", + or.getConstraint1() instanceof PropertyExistence); + assertTrue("Not a PropertyExistence constraint", + or.getConstraint2() instanceof PropertyExistence); + } + + /** + * Test case for {@link QueryObjectModelFactory#propertyExistence(String, String)} + */ + public void testPropertyExistence() throws RepositoryException { + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + assertEquals("Wrong selector", SELECTOR_NAME1, propExist.getSelectorName()); + assertEquals("Wrong property name", propertyName1, propExist.getPropertyName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#propertyExistence(String, String)} + */ + public void testPropertyExistenceWithSelector() throws RepositoryException { + PropertyExistence propExist = qf.propertyExistence(SELECTOR_NAME1, propertyName1); + assertEquals("Wrong selector name", SELECTOR_NAME1, propExist.getSelectorName()); + assertEquals("Wrong property name", propertyName1, propExist.getPropertyName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#propertyValue(String, String)} + */ + public void testPropertyValue() throws RepositoryException { + PropertyValue propVal = qf.propertyValue(SELECTOR_NAME1, propertyName1); + assertEquals("Wrong selector name", SELECTOR_NAME1, propVal.getSelectorName()); + assertEquals("Wrong property name", propertyName1, propVal.getPropertyName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#propertyValue(String, String)} + */ + public void testPropertyValueWithSelector() throws RepositoryException { + PropertyValue propVal = qf.propertyValue(SELECTOR_NAME1, propertyName1); + assertEquals("Wrong selector name", SELECTOR_NAME1, propVal.getSelectorName()); + assertEquals("Wrong property name", propertyName1, propVal.getPropertyName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#sameNode(String, String)} + */ + public void testSameNode() throws RepositoryException { + SameNode sameNode = qf.sameNode(SELECTOR_NAME1, testRootNode.getPath()); + assertEquals("Wrong selector name", SELECTOR_NAME1, sameNode.getSelectorName()); + assertEquals("Wrong path", testRootNode.getPath(), sameNode.getPath()); + } + + /** + * Test case for {@link QueryObjectModelFactory#sameNode(String, String)} + */ + public void testSameNodeWithSelector() throws RepositoryException { + SameNode sameNode = qf.sameNode(SELECTOR_NAME1, testRootNode.getPath()); + assertEquals("Wrong selector name", SELECTOR_NAME1, sameNode.getSelectorName()); + assertEquals("Wrong path", testRootNode.getPath(), sameNode.getPath()); + } + + /** + * Test case for {@link QueryObjectModelFactory#sameNodeJoinCondition(String, String, String)} + */ + public void testSameNodeJoinCondition() throws RepositoryException { + SameNodeJoinCondition cond = qf.sameNodeJoinCondition(SELECTOR_NAME1, SELECTOR_NAME2, "."); + assertEquals("Wrong selector name", SELECTOR_NAME1, cond.getSelector1Name()); + assertEquals("Wrong selector name", SELECTOR_NAME2, cond.getSelector2Name()); + assertEquals("Wrong selector path", ".", cond.getSelector2Path()); + } + + /** + * Test case for {@link QueryObjectModelFactory#sameNodeJoinCondition(String, String, String)} + */ + public void testSameNodeJoinConditionWithPath() throws RepositoryException { + SameNodeJoinCondition cond = qf.sameNodeJoinCondition(SELECTOR_NAME1, SELECTOR_NAME2, nodeName1); + assertEquals("Wrong selector name", SELECTOR_NAME1, cond.getSelector1Name()); + assertEquals("Wrong selector name", SELECTOR_NAME2, cond.getSelector2Name()); + assertEquals("Wrong path", nodeName1, cond.getSelector2Path()); + } + + /** + * Test case for {@link QueryObjectModelFactory#selector(String, String)} + */ + public void testSelector() throws RepositoryException { + Selector selector = qf.selector(ntBase, SELECTOR_NAME1); + assertEquals("Wrong node type name", ntBase, selector.getNodeTypeName()); + assertEquals("Wrong selector name", SELECTOR_NAME1, selector.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#selector(String, String)} + */ + public void testSelectorWithName() throws RepositoryException { + Selector selector = qf.selector(ntBase, SELECTOR_NAME1); + assertEquals("Wrong node type name", ntBase, selector.getNodeTypeName()); + assertEquals("Wrong selector name", SELECTOR_NAME1, selector.getSelectorName()); + } + + /** + * Test case for {@link QueryObjectModelFactory#upperCase(DynamicOperand)} + */ + public void testUpperCase() throws RepositoryException { + PropertyValue propValue = qf.propertyValue(SELECTOR_NAME1, propertyName1); + UpperCase upper = qf.upperCase(propValue); + assertTrue("Not a property value operand", upper.getOperand() instanceof PropertyValue); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/RowTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/RowTest.java new file mode 100644 index 00000000000..9e9aae14b0d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/RowTest.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.test.api.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Value; +import javax.jcr.query.RowIterator; +import javax.jcr.query.Row; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.Column; + +/** + * RowIteratorTest contains test cases for {@link Row}. + */ +public class RowTest extends AbstractQOMTest { + + private static final String TEST_VALUE = "value"; + + private static final String SELECTOR_NAME = "s"; + + protected void setUp() throws Exception { + super.setUp(); + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.setProperty(propertyName1, TEST_VALUE); + superuser.save(); + } + + public void testGetValues() throws RepositoryException { + Row r = getRow(); + Value[] values = r.getValues(); + assertEquals("wrong number of columns", 1, values.length); + assertEquals("property value does not match", TEST_VALUE, values[0].getString()); + } + + public void testGetValue() throws RepositoryException { + Row r = getRow(); + assertEquals("property value does not match", TEST_VALUE, r.getValue(propertyName1).getString()); + } + + public void testGetNode() throws RepositoryException { + Row r = getRow(); + String expectedPath = testRootNode.getNode(nodeName1).getPath(); + assertEquals("unexpected result node", expectedPath, r.getNode().getPath()); + } + + public void testGetNodeWithSelector() throws RepositoryException { + Row r = getRow(); + String expectedPath = testRootNode.getNode(nodeName1).getPath(); + assertEquals("unexpected result node", expectedPath, r.getNode(SELECTOR_NAME).getPath()); + } + + public void testGetPath() throws RepositoryException { + Row r = getRow(); + String expectedPath = testRootNode.getNode(nodeName1).getPath(); + assertEquals("unexpected result node", expectedPath, r.getPath()); + } + + public void testGetPathWithSelector() throws RepositoryException { + Row r = getRow(); + String expectedPath = testRootNode.getNode(nodeName1).getPath(); + assertEquals("unexpected result node", expectedPath, r.getPath(SELECTOR_NAME)); + } + + public void testGetScore() throws RepositoryException { + Row r = getRow(); + // value is implementation dependent, simply call method... + r.getScore(); + } + + public void testGetScoreWithSelector() throws RepositoryException { + Row r = getRow(); + // value is implementation dependent, simply call method... + r.getScore(SELECTOR_NAME); + } + + private Row getRow() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, SELECTOR_NAME), + qf.descendantNode(SELECTOR_NAME, testRoot), + null, + new Column[]{qf.column(SELECTOR_NAME, propertyName1, propertyName1)}); + RowIterator rows = qom.execute().getRows(); + assertTrue("empty result", rows.hasNext()); + Row r = rows.nextRow(); + assertFalse("result must not contain more than one row", rows.hasNext()); + return r; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SameNodeJoinConditionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SameNodeJoinConditionTest.java new file mode 100644 index 00000000000..6cb163ba992 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SameNodeJoinConditionTest.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.test.api.query.qom; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelConstants; + +/** + * SameNodeJoinConditionTest contains test cases that cover + * SameNodeJoinCondition. + */ +public class SameNodeJoinConditionTest extends AbstractJoinTest { + + private Node n1; + + private Node n2; + + protected void setUp() throws Exception { + super.setUp(); + n1 = testRootNode.addNode(nodeName1, testNodeType); + n2 = n1.addNode(nodeName2, testNodeType); + ensureMixinType(n2, mixReferenceable); + superuser.save(); + } + + public void testInnerJoin() throws RepositoryException { + QueryObjectModel qom = createQomQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, null); + checkQOM(qom, new Node[][]{{n1, n1}, {n2, n2}}); + } + + public void testInnerJoinWithPath() throws RepositoryException { + QueryObjectModel qom = createQomQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, nodeName2); + checkQOM(qom, new Node[][]{{n2, n1}}); + } + + public void testLeftOuterJoin() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.join( + qf.selector(testNodeType, LEFT), + qf.selector(mixReferenceable, RIGHT), + QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER, + qf.sameNodeJoinCondition(LEFT, RIGHT, ".")), + qf.descendantNode(LEFT, testRoot), + null, null); + + checkQOM(qom, new Node[][]{{n1, null}, {n2, n2}}); + } + + public void testLeftOuterJoinWithPath() throws RepositoryException { + QueryObjectModel qom = createQomQuery(QueryObjectModelConstants.JCR_JOIN_TYPE_LEFT_OUTER, nodeName2); + checkQOM(qom, new Node[][]{{n1, null}, {n2, n1}}); + } + + public void testRightOuterJoin() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.join( + qf.selector(mixReferenceable, LEFT), + qf.selector(testNodeType, RIGHT), + QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER, + qf.sameNodeJoinCondition(LEFT, RIGHT, ".")), + qf.descendantNode(RIGHT, testRoot), + null, null); + + checkQOM(qom, new Node[][]{{null, n1}, {n2, n2}}); + } + + public void testRightOuterJoinWithPath() throws RepositoryException { + QueryObjectModel qom = qf.createQuery( + qf.join( + qf.selector(mixReferenceable, LEFT), + qf.selector(testNodeType, RIGHT), + QueryObjectModelConstants.JCR_JOIN_TYPE_RIGHT_OUTER, + qf.sameNodeJoinCondition(LEFT, RIGHT, nodeName2)), + qf.descendantNode(RIGHT, testRoot), + null, null); + + checkQOM(qom, new Node[][]{{n2, n1}, {null, n2}}); + } + + //-----------------------------< utilities >-------------------------------- + + private QueryObjectModel createQomQuery(String joinType, String relPath) + throws RepositoryException { + JoinCondition c; + if (relPath != null) { + c = qf.sameNodeJoinCondition(LEFT, RIGHT, relPath); + } else { + c = qf.sameNodeJoinCondition(LEFT, RIGHT, "."); + } + return createQuery(joinType, c); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SameNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SameNodeTest.java new file mode 100644 index 00000000000..383667c4ad7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SameNodeTest.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.test.api.query.qom; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.qom.QueryObjectModel; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * SameNodeTest... + */ +public class SameNodeTest extends AbstractQOMTest { + + public void testSameNode() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + QueryObjectModel qom = qf.createQuery(qf.selector(testNodeType, "s"), + qf.sameNode("s", testRoot + "/" + nodeName1), null, null); + checkQOM(qom, new Node[]{n}); + } + + public void testPathDoesNotExist() throws RepositoryException { + QueryObjectModel qom = qf.createQuery(qf.selector(testNodeType, "s"), + qf.sameNode("s", testRoot + "/" + nodeName1), + null, null); + checkQOM(qom, new Node[]{}); + } + + public void testChildNodesDoNotMatchSelector() + throws RepositoryException, NotExecutableException { + testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeIterator it = ntMgr.getPrimaryNodeTypes(); + NodeType testNt = ntMgr.getNodeType(testNodeType); + while (it.hasNext()) { + NodeType nt = it.nextNodeType(); + if (!testNt.isNodeType(nt.getName())) { + // perform test + QueryObjectModel qom = qf.createQuery(qf.selector(nt.getName(), "s"), + qf.sameNode("s", testRoot + "/" + nodeName1), null, null); + checkQOM(qom, new Node[]{}); + return; + } + } + throw new NotExecutableException("No suitable node type found to " + + "perform test against '" + testNodeType + "' nodes"); + } + + public void testRelativePath() throws RepositoryException { + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.sameNode("s", testPath), null, null); + q.execute(); + fail("SameNode with relative path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE ISSAMENODE(s, [" + testPath + "]"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISSAMENODE() with relative path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testSyntacticallyInvalidPath() throws RepositoryException { + String invalidPath = testRoot + "/" + nodeName1 + "["; + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.sameNode("s", invalidPath), + null, null); + q.execute(); + fail("SameNode with syntactically invalid path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE ISSAMENODE(s, [" + invalidPath + "]"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISSAMENODE() with syntactically invalid path argument must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testNotASelectorName() throws RepositoryException { + try { + Query q = qf.createQuery(qf.selector(testNodeType, "s"), + qf.sameNode("x", testRoot), null, null); + q.execute(); + fail("SameNode with an invalid selector name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS s " + + "WHERE ISSAMENODE(x, [" + testRoot + "]"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("ISSAMENODE with an invalid selector name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SelectorTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SelectorTest.java new file mode 100644 index 00000000000..4d4a0b7c92f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/SelectorTest.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.test.api.query.qom; + +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModel; + +/** + * SelectorTest... + */ +public class SelectorTest extends AbstractQOMTest { + + public void testSelector() throws RepositoryException { + // make sure there's at least one node with this node type + testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + QueryObjectModel qom = qf.createQuery( + qf.selector(testNodeType, "s"), null, null, null); + forQOMandSQL2(qom, new Callable() { + public Object call(Query query) throws RepositoryException { + QueryResult result = query.execute(); + String[] names = result.getSelectorNames(); + assertNotNull(names); + assertEquals(1, names.length); + assertEquals("s", names[0]); + NodeIterator it = result.getNodes(); + while (it.hasNext()) { + assertTrue("Wrong node type", it.nextNode().isNodeType(testNodeType)); + } + return null; + } + }); + } + + public void testSyntacticallyInvalidName() throws RepositoryException { + String invalidNodeType = testNodeType + "["; + try { + Query q = qf.createQuery(qf.selector(invalidNodeType, "s"), + null, null, null); + q.execute(); + fail("Selector with syntactically invalid name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + invalidNodeType + "]"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("selectorName with syntactically invalid name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testUnknownNodeType() throws RepositoryException { + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + String ntName = testNodeType; + for (;;) { + try { + ntMgr.getNodeType(ntName); + ntName += "x"; + } catch (NoSuchNodeTypeException e) { + break; + } + } + try { + qf.createQuery(qf.selector(ntName, "s"), null, null, null).execute(); + fail("Selector with unknown node type must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + ntName + "] AS nt"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("Selector with unknown node type must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } + + public void testDuplicateNodeType() throws RepositoryException { + try { + Query q = qf.createQuery( + qf.join( + qf.selector(testNodeType, "nt"), + qf.selector(testNodeType, "nt"), + QueryObjectModelConstants.JCR_JOIN_TYPE_INNER, + qf.descendantNodeJoinCondition("nt", "nt")), + null, null, null); + q.execute(); + fail("Selector with two identical selector names must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + try { + String stmt = "SELECT * FROM [" + testNodeType + "] AS nt, [" + + testNodeType + "] AS nt nt INNER JOIN nt ON ISDESCENDANTNODE(nt, nt)"; + qm.createQuery(stmt, Query.JCR_SQL2).execute(); + fail("selectorName with syntactically invalid name must throw InvalidQueryException"); + } catch (InvalidQueryException e) { + // expected + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/TestAll.java new file mode 100644 index 00000000000..be447047bbb --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/TestAll.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.test.api.query.qom; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * TestAll includes tests that are related to the + * QueryObjectModel. + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("QOM tests"); + + suite.addTestSuite(AndConstraintTest.class); + suite.addTestSuite(BindVariableValueTest.class); + suite.addTestSuite(ChildNodeJoinConditionTest.class); + suite.addTestSuite(ChildNodeTest.class); + suite.addTestSuite(ColumnTest.class); + suite.addTestSuite(DescendantNodeJoinConditionTest.class); + suite.addTestSuite(DescendantNodeTest.class); + suite.addTestSuite(EquiJoinConditionTest.class); + suite.addTestSuite(FullTextSearchScoreTest.class); + suite.addTestSuite(GetQueryTest.class); + suite.addTestSuite(LengthTest.class); + suite.addTestSuite(NodeLocalNameTest.class); + suite.addTestSuite(NodeNameTest.class); + suite.addTestSuite(NotConstraintTest.class); + suite.addTestSuite(OrConstraintTest.class); + suite.addTestSuite(OrderingTest.class); + suite.addTestSuite(PropertyExistenceTest.class); + suite.addTestSuite(PropertyValueTest.class); + suite.addTestSuite(QueryObjectModelFactoryTest.class); + suite.addTestSuite(RowTest.class); + suite.addTestSuite(SameNodeJoinConditionTest.class); + suite.addTestSuite(SameNodeTest.class); + suite.addTestSuite(SelectorTest.class); + suite.addTestSuite(UpperLowerCaseTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/UpperLowerCaseTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/UpperLowerCaseTest.java new file mode 100644 index 00000000000..cdb785d95c5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/query/qom/UpperLowerCaseTest.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.test.api.query.qom; + +import javax.jcr.Node; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.QueryObjectModel; + +/** + * UpperLowerCaseTest performs tests with upper- and lower-case + * operands. + */ +public class UpperLowerCaseTest extends AbstractQOMTest { + + private Node node; + + protected void setUp() throws Exception { + super.setUp(); + node = testRootNode.addNode(nodeName1, testNodeType); + node.setProperty(propertyName1, "abc"); + node.setProperty(propertyName2, "ABC"); + superuser.save(); + } + + protected void tearDown() throws Exception { + node = null; + super.tearDown(); + } + + public void testLength() throws RepositoryException { + String lenStr = String.valueOf(node.getProperty(propertyName1).getLength()); + // upper case + checkQueries(qf.length(qf.propertyValue("s", propertyName1)), + true, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{lenStr.toUpperCase()}, + PropertyType.STRING, + new boolean[]{true}); + + // lower case + checkQueries(qf.length(qf.propertyValue("s", propertyName1)), + false, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{lenStr.toLowerCase()}, + PropertyType.STRING, + new boolean[]{true}); + } + + public void testNodeLocalName() throws RepositoryException { + String localName = getLocalName(node.getName()); + // upper case + checkQueries(qf.nodeLocalName("s"), + true, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{localName.toLowerCase(), localName.toUpperCase()}, + PropertyType.STRING, + new boolean[]{false, true}); + + // lower case + checkQueries(qf.nodeLocalName("s"), + false, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{localName.toLowerCase(), localName.toUpperCase()}, + PropertyType.STRING, + new boolean[]{true, false}); + } + + public void testNodeName() throws RepositoryException { + // upper case + checkQueries(qf.nodeName("s"), + true, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{node.getName().toLowerCase(), node.getName().toUpperCase()}, + PropertyType.NAME, + new boolean[]{false, true}); + + // lower case + checkQueries(qf.nodeName("s"), + false, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{node.getName().toLowerCase(), node.getName().toUpperCase()}, + PropertyType.NAME, + new boolean[]{true, false}); + } + + public void testPropertyValue() throws RepositoryException { + // upper case + checkQueries(qf.propertyValue("s", propertyName1), + true, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{"abc", "Abc", "aBc", "abC", "ABC"}, + PropertyType.STRING, + new boolean[]{false, false, false, false, true}); + + checkQueries(qf.propertyValue("s", propertyName2), + true, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{"abc", "Abc", "aBc", "abC", "ABC"}, + PropertyType.STRING, + new boolean[]{false, false, false, false, true}); + + // lower case + checkQueries(qf.propertyValue("s", propertyName1), + false, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{"abc", "Abc", "aBc", "abC", "ABC"}, + PropertyType.STRING, + new boolean[]{true, false, false, false, false}); + + checkQueries(qf.propertyValue("s", propertyName2), + false, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{"abc", "Abc", "aBc", "abC", "ABC"}, + PropertyType.STRING, + new boolean[]{true, false, false, false, false}); + } + + public void testUpperLowerCase() throws RepositoryException { + // first upper case, then lower case again + checkQueries(qf.upperCase(qf.propertyValue("s", propertyName1)), + false, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{"abc", "Abc", "aBc", "abC", "ABC"}, + PropertyType.STRING, + new boolean[]{true, false, false, false, false}); + } + + public void testUpperCaseTwice() throws RepositoryException { + // upper case twice + checkQueries(qf.upperCase(qf.propertyValue("s", propertyName1)), + true, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{"abc", "Abc", "aBc", "abC", "ABC"}, + PropertyType.STRING, + new boolean[]{false, false, false, false, true}); + } + + public void testLowerUpperCase() throws RepositoryException { + // first lower case, then upper case again + checkQueries(qf.lowerCase(qf.propertyValue("s", propertyName1)), + true, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{"abc", "Abc", "aBc", "abC", "ABC"}, + PropertyType.STRING, + new boolean[]{false, false, false, false, true}); + } + + public void testLowerCaseTwice() throws RepositoryException { + // lower case twice + checkQueries(qf.lowerCase(qf.propertyValue("s", propertyName1)), + false, QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO, + new String[]{"abc", "Abc", "aBc", "abC", "ABC"}, + PropertyType.STRING, + new boolean[]{true, false, false, false, false}); + } + + //-------------------------------< internal >------------------------------- + + private void checkQueries(DynamicOperand operand, + boolean toUpper, + String operator, + String[] literals, + int type, + boolean[] matches) throws RepositoryException { + for (int i = 0; i < literals.length; i++) { + QueryObjectModel qom = createQuery(operand, toUpper, operator, vf.createValue(literals[i], type)); + checkQOM(qom, matches[i] ? new Node[]{node} : new Node[0]); + } + } + + private QueryObjectModel createQuery(DynamicOperand operand, + boolean toUpper, + String operator, + Value literal) + throws RepositoryException { + if (toUpper) { + operand = qf.upperCase(operand); + } else { + operand = qf.lowerCase(operand); + } + return qf.createQuery( + qf.selector(testNodeType, "s"), + qf.and( + qf.childNode("s", testRoot), + qf.comparison( + operand, + operator, + qf.literal(literal) + ) + ), null, null); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/AbstractRetentionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/AbstractRetentionTest.java new file mode 100644 index 00000000000..b3a60b638db --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/AbstractRetentionTest.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.test.api.retention; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.retention.RetentionManager; +import javax.jcr.retention.RetentionPolicy; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; + +/** + * AbstractAccessControlTest... + */ +public abstract class AbstractRetentionTest extends AbstractJCRTest { + + protected RetentionManager retentionMgr; + protected String testNodePath; + + protected void setUp() throws Exception { + checkSupportedOption(Repository.OPTION_RETENTION_SUPPORTED); + + super.setUp(); + + try { + retentionMgr = getRetentionManager(superuser); + } catch (NotExecutableException e) { + cleanUp(); + throw e; + } + testNodePath = testRootNode.getPath(); + } + + protected String getHoldName() throws RepositoryException, NotExecutableException { + String holdName = getProperty(RepositoryStub.PROP_HOLD_NAME); + if (holdName == null) { + throw new NotExecutableException(); + } + return holdName; + } + + protected RetentionPolicy getApplicableRetentionPolicy() throws NotExecutableException, RepositoryException { + return superuser.getRetentionManager().getRetentionPolicy(getProperty(RepositoryStub.RETENTION_POLICY_HOLDER)); + } + + protected static RetentionManager getRetentionManager(Session s) throws RepositoryException, NotExecutableException { + try { + return s.getRetentionManager(); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/HoldEffectTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/HoldEffectTest.java new file mode 100644 index 00000000000..f4ee6f21901 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/HoldEffectTest.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.test.api.retention; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.retention.Hold; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; + +/** + * HoldEffectTest... + */ +public class HoldEffectTest extends AbstractRetentionTest { + + private Node childN; + private Property childP; + private Session otherS; + + protected void setUp() throws Exception { + super.setUp(); + + childN = testRootNode.addNode(nodeName2); + Value v = getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE1, RepositoryStub.PROP_PROP_TYPE1, "test"); + childP = testRootNode.setProperty(propertyName1, v); + superuser.save(); + + otherS = getHelper().getSuperuserSession(); + } + + protected void tearDown() throws Exception { + if (otherS != null) { + otherS.logout(); + } + Hold[] holds = retentionMgr.getHolds(testNodePath); + for (int i = 0; i < holds.length; i++) { + retentionMgr.removeHold(testNodePath, holds[i]); + } + superuser.save(); + + super.tearDown(); + } + + // TODO: test importXML (session/wsp) / move (session/wsp) / copy ... + // TODO: test effect on child items + + public void testTransientShallowHold() throws RepositoryException, NotExecutableException { + retentionMgr.addHold(testNodePath, getHoldName(), false); + + assertNoEffect(testRootNode, nodeName3, propertyName2); + assertNoEffect(childN, nodeName3, propertyName2); + assertNoEffect(childP); + } + + public void testTransientShallowHoldForOtherSession() throws RepositoryException, NotExecutableException { + retentionMgr.addHold(testNodePath, getHoldName(), false); + + assertNoEffect((Node) otherS.getItem(testNodePath), nodeName3, propertyName2); + assertNoEffect((Node) otherS.getItem(childN.getPath()), nodeName3, propertyName2); + assertNoEffect((Property) otherS.getItem(childP.getPath())); + } + + public void testTransientDeepHold() throws RepositoryException, NotExecutableException { + retentionMgr.addHold(testNodePath, getHoldName(), true); + + assertNoEffect(testRootNode, nodeName3, propertyName2); + assertNoEffect(childN, nodeName3, propertyName2); + assertNoEffect(childP); + } + + public void testTransientDeepHoldForOtherSession() throws RepositoryException, NotExecutableException { + retentionMgr.addHold(testNodePath, getHoldName(), true); + + assertNoEffect((Node) otherS.getItem(testNodePath), nodeName3, propertyName2); + assertNoEffect((Node) otherS.getItem(childN.getPath()), nodeName3, propertyName2); + assertNoEffect((Property) otherS.getItem(childP.getPath())); + } + + public void testShallowHold() throws RepositoryException, NotExecutableException { + retentionMgr.addHold(testNodePath, getHoldName(), false); + superuser.save(); + + // check for superuser + assertNoEffect(childN, nodeName3, propertyName2); + assertEffect(testRootNode, childN.getName(), childP.getName(), nodeName3, propertyName2); + } + + public void testShallowHoldForOtherSession() throws RepositoryException, NotExecutableException { + retentionMgr.addHold(testNodePath, getHoldName(), false); + superuser.save(); + + // check for other session + assertNoEffect((Node) otherS.getItem(childN.getPath()), nodeName3, propertyName2); + assertEffect((Node) otherS.getItem(testNodePath), childN.getName(), childP.getName(), nodeName3, propertyName2); + } + + public void testDeepHold() throws RepositoryException, NotExecutableException { + Node n = childN.addNode(nodeName2); + Value v = getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE1, RepositoryStub.PROP_PROP_TYPE1, "test"); + Property p = childN.setProperty(propertyName1, v); + retentionMgr.addHold(testNodePath, getHoldName(), true); + superuser.save(); + + // check for superuser + assertEffect(testRootNode, childN.getName(), childP.getName(), nodeName3, propertyName2); + assertEffect(childN, n.getName(), p.getName(), nodeName3, propertyName2); + } + + public void testDeepHoldForOtherSession() throws RepositoryException, NotExecutableException { + Node n = childN.addNode(nodeName2); + Value v = getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE1, RepositoryStub.PROP_PROP_TYPE1, "test"); + Property p = childN.setProperty(propertyName1, v); + retentionMgr.addHold(testNodePath, getHoldName(), true); + superuser.save(); + + // check for other session + assertEffect((Node) otherS.getItem(testNodePath), childN.getName(), childP.getName(), nodeName3, propertyName2); + assertEffect((Node) otherS.getItem(childN.getPath()), n.getName(), p.getName(), nodeName3, propertyName2); + } + + private void assertEffect(Node targetNode, String childName, + String propName, String childName2, + String propName2) throws RepositoryException { + Session s = targetNode.getSession(); + try { + Node child = targetNode.getNode(childName); + child.remove(); + s.save(); + fail("Hold present must prevent a child node from being removed."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + Property p = targetNode.getProperty(propName); + p.remove(); + s.save(); + fail("Hold present must prevent a child property from being removed."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + Property p = targetNode.getProperty(propName); + p.setValue("test2"); + s.save(); + fail("Hold present must prevent the child property from being modified."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + targetNode.addNode(childName2); + s.save(); + fail("Hold present must prevent the target node from having new nodes added."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + Value v = getJcrValue(s, RepositoryStub.PROP_PROP_VALUE2, RepositoryStub.PROP_PROP_TYPE2, "test"); + targetNode.setProperty(propName2, v); + s.save(); + fail("Hold present must prevent the target node from having new properties set."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + + NodeType[] mixins = targetNode.getMixinNodeTypes(); + if (mixins.length > 0) { + try { + targetNode.removeMixin(mixins[0].getName()); + s.save(); + fail("Hold present must prevent the target node from having it's mixin types changed."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + } + try { + targetNode.remove(); + s.save(); + fail("Hold present must prevent the target node from being removed."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + } + + private void assertNoEffect(Node target, String childName, String propName) throws RepositoryException { + Session s = target.getSession(); + + Node n = target.addNode(childName); + Value v = getJcrValue(s, RepositoryStub.PROP_PROP_VALUE2, RepositoryStub.PROP_PROP_TYPE2, "test"); + Property p = target.setProperty(propName, v); + + n.remove(); + p.remove(); + } + + private void assertNoEffect(Property target) throws RepositoryException { + target.setValue("test3"); + target.remove(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/HoldTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/HoldTest.java new file mode 100644 index 00000000000..e466e529dd4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/HoldTest.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.test.api.retention; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.retention.Hold; +import javax.jcr.retention.RetentionManager; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * RetentionManagerTest... + */ +public class HoldTest extends AbstractRetentionTest { + + private static boolean containsHold(Hold[] holds, Hold toTest) throws RepositoryException { + for (int i = 0; i < holds.length; i++) { + if (holds[i].getName().equals(toTest.getName()) && holds[i].isDeep() == toTest.isDeep()) { + return true; + } + } + return false; + } + + public void testAddHold() throws RepositoryException, NotExecutableException { + Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false); + Hold[] holds = retentionMgr.getHolds(testNodePath); + assertTrue("getHolds must return the hold added before.", holds.length >= 1); + assertTrue("getHolds doesn't return the hold added before", containsHold(holds, hold)); + } + + public void testAddHold2() throws RepositoryException, NotExecutableException { + Hold[] holdsBefore = retentionMgr.getHolds(testNodePath); + Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false); + assertFalse("The hold added must not have been present before.", containsHold(holdsBefore, hold)); + } + + public void testAddHoldIsTransient() throws RepositoryException, NotExecutableException { + Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false); + Hold[] holds = retentionMgr.getHolds(testNodePath); + + // revert the changes made + superuser.refresh(false); + Hold[] holds2 = retentionMgr.getHolds(testNodePath); + + assertEquals("Reverting transient changes must revert the hold added.", + holds.length -1, holds2.length); + assertFalse("Reverting transient changes must revert the hold added.", + containsHold(holds2, hold)); + } + + public void testRemoveHold() throws RepositoryException, NotExecutableException { + Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false); + + Hold[] holds = retentionMgr.getHolds(testNodePath); + + retentionMgr.removeHold(testNodePath, hold); + Hold[] holds2 = retentionMgr.getHolds(testNodePath); + + assertEquals("RetentionManager.removeHold should removed the hold added before.", + holds.length -1, holds2.length); + assertFalse("RetentionManager.removeHold should removed the hold added before.", + containsHold(holds2, hold)); + } + + public void testRemoveHoldIsTransient() throws RepositoryException, NotExecutableException { + Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false); + superuser.save(); + try { + Hold[] holds = retentionMgr.getHolds(testNodePath); + + retentionMgr.removeHold(testNodePath, hold); + superuser.refresh(false); + + Hold[] holds2 = retentionMgr.getHolds(testNodePath); + assertEquals("Reverting transient hold removal must restore the original state.", + Arrays.asList(holds), Arrays.asList(holds2)); + } finally { + // clear the hold that was permanently added before. + retentionMgr.removeHold(testNodePath, hold); + superuser.save(); + } + } + + public void testRemoveHoldFromChild() throws RepositoryException, NotExecutableException { + String childPath = testRootNode.addNode(nodeName2, testNodeType).getPath(); + Hold hold = retentionMgr.addHold(testNodePath, getHoldName(), false); + + try { + retentionMgr.removeHold(childPath, hold); + fail("Removing hold from another node must fail"); + } catch (RepositoryException e) { + // success + assertTrue(containsHold(retentionMgr.getHolds(testNodePath), hold)); + } + + // check again with persisted hold + superuser.save(); + try { + retentionMgr.removeHold(childPath, hold); + fail("Removing hold from another node must fail"); + } catch (RepositoryException e) { + // success + assertTrue(containsHold(retentionMgr.getHolds(testNodePath), hold)); + } finally { + // clear the hold that was permanently added before. + retentionMgr.removeHold(testNodePath, hold); + superuser.save(); + } + } + + public void testInvalidPath() throws RepositoryException, NotExecutableException { + String invalidPath = testPath; // not an absolute path. + try { + retentionMgr.getHolds(invalidPath); + fail("Accessing holds an invalid path must throw RepositoryException."); + } catch (RepositoryException e) { + // success + } + try { + retentionMgr.addHold(invalidPath, getHoldName(), true); + fail("Adding a hold at an invalid path must throw RepositoryException."); + } catch (RepositoryException e) { + // success + } + try { + Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + retentionMgr.removeHold(invalidPath, h); + fail("Removing a hold at an invalid path must throw RepositoryException."); + } catch (RepositoryException e) { + // success + } + } + + public void testNonExistingNodePath() throws RepositoryException, NotExecutableException { + String invalidPath = testNodePath + "/nonexisting"; + int cnt = 0; + while (superuser.nodeExists(invalidPath)) { + invalidPath += cnt++; + } + + try { + retentionMgr.getHolds(invalidPath); + fail("Accessing holds from non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + try { + retentionMgr.addHold(invalidPath, getHoldName(), true); + fail("Adding a hold for a non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + try { + Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + retentionMgr.removeHold(invalidPath, h); + fail("Removing a hold at a non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + } + + public void testPropertyPath() throws RepositoryException, NotExecutableException { + String propPath = null; + for (PropertyIterator it = testRootNode.getProperties(); it.hasNext();) { + String path = it.nextProperty().getPath(); + if (! superuser.nodeExists(path)) { + propPath = path; + break; + } + } + if (propPath == null) { + throw new NotExecutableException(); + } + try { + retentionMgr.getHolds(propPath); + fail("Accessing holds from non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + try { + retentionMgr.addHold(propPath, getHoldName(), true); + fail("Adding a hold for a non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + try { + Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + retentionMgr.removeHold(propPath, h); + fail("Removing a hold at a non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + } + + public void testInvalidName() { + try { + String invalidName = "*.[y]"; + retentionMgr.addHold(testNodePath, invalidName, false); + fail("Adding a hold with an invalid JCR name must fail."); + } catch (RepositoryException e) { + // success + } + } + + public void testReadOnlySession() throws NotExecutableException, RepositoryException { + javax.jcr.Session s = getHelper().getReadOnlySession(); + try { + RetentionManager rmgr = getRetentionManager(s); + try { + rmgr.getHolds(testNodePath); + fail("Read-only session doesn't have sufficient privileges to retrieve holds."); + } catch (AccessDeniedException e) { + // success + } + try { + rmgr.addHold(testNodePath, getHoldName(), false); + fail("Read-only session doesn't have sufficient privileges to retrieve holds."); + } catch (AccessDeniedException e) { + // success + } + } finally { + s.logout(); + } + } + + public void testAddHoldOnLockedNode() throws NotExecutableException, RepositoryException { + Node child = getLockedChildNode(); + // remember current holds for clean up. + List holdsBefore = Arrays.asList(retentionMgr.getHolds(child.getPath())); + + // get another session. + javax.jcr.Session otherS = getHelper().getSuperuserSession(); + try { + RetentionManager rmgr = getRetentionManager(otherS); + rmgr.addHold(child.getPath(), getHoldName(), false); + otherS.save(); + + fail("Adding hold on a locked node must throw LockException."); + } catch (LockException e) { + // success + } finally { + otherS.logout(); + + // clear holds (in case of test failure) + List holds = new ArrayList(Arrays.asList(retentionMgr.getHolds(child.getPath()))); + if (holds.removeAll(holdsBefore)) { + for (Iterator it = holds.iterator(); it.hasNext();) { + retentionMgr.removeHold(child.getPath(), (Hold) it.next()); + } + } + superuser.save(); + } + } + + public void testRemoveHoldOnLockedNode() throws NotExecutableException, RepositoryException { + Node child = getLockedChildNode(); + Hold h = retentionMgr.addHold(child.getPath(), getHoldName(), false); + testRootNode.getSession().save(); + + javax.jcr.Session otherS = getHelper().getSuperuserSession(); + try { + RetentionManager rmgr = getRetentionManager(otherS); + Hold[] holds = rmgr.getHolds(child.getPath()); + + if (holds.length > 0) { + rmgr.removeHold(child.getPath(), holds[0]); + otherS.save(); + fail("Removing a hold on a locked node must throw LockException."); + } + } catch (LockException e) { + // success + } finally { + otherS.logout(); + + // clear hold added before + try { + retentionMgr.removeHold(child.getPath(), h); + superuser.save(); + } catch (RepositoryException e) { + // should not get here if test is correctly executed. + } + } + } + + private Node getLockedChildNode() throws NotExecutableException, RepositoryException { + checkSupportedOption(Repository.OPTION_LOCKING_SUPPORTED); + Node child = testRootNode.addNode(nodeName2, testNodeType); + ensureMixinType(child, mixLockable); + testRootNode.getSession().save(); + child.lock(false, true); // session-scoped lock clean upon superuser-logout. + return child; + } + + public void testAddHoldOnCheckedInNode() throws NotExecutableException, RepositoryException { + Node child = getVersionableChildNode(); + child.checkout(); + child.checkin(); + + // get another session. + javax.jcr.Session otherS = getHelper().getSuperuserSession(); + try { + RetentionManager rmgr = getRetentionManager(otherS); + rmgr.addHold(child.getPath(), getHoldName(), false); + otherS.save(); + + fail("Adding hold on a checked-in node must throw VersionException."); + } catch (VersionException e) { + // success + } finally { + otherS.logout(); + + // clear holds (in case of test failure) + child.checkout(); + Hold[] holds = retentionMgr.getHolds(child.getPath()); + for (int i = 0; i < holds.length; i++) { + retentionMgr.removeHold(child.getPath(), holds[i]); + } + superuser.save(); + } + } + + public void testRemoveHoldOnCheckedInNode() throws NotExecutableException, RepositoryException { + Node vn = getVersionableChildNode(); + vn.checkout(); + Node n = vn.addNode(nodeName2); + Hold h = retentionMgr.addHold(n.getPath(), getHoldName(), false); + superuser.save(); + + // checkin on the parent node make the hold-containing node checked-in. + vn.checkin(); + + javax.jcr.Session otherS = getHelper().getSuperuserSession(); + try { + RetentionManager rmgr = getRetentionManager(otherS); + Hold[] holds = rmgr.getHolds(n.getPath()); + + if (holds.length > 0) { + rmgr.removeHold(n.getPath(), holds[0]); + otherS.save(); + fail("Removing a hold on a checked-in node must throw VersionException."); + } + } catch (VersionException e) { + // success + } finally { + otherS.logout(); + + // clear hold added before + vn.checkout(); + try { + retentionMgr.removeHold(n.getPath(), h); + superuser.save(); + } catch (RepositoryException e) { + // should not get here if test is correctly executed. + } + } + } + + private Node getVersionableChildNode() throws NotExecutableException, RepositoryException { + checkSupportedOption(Repository.OPTION_VERSIONING_SUPPORTED); + Node child = testRootNode.addNode(nodeName2, testNodeType); + ensureMixinType(child, mixVersionable); + testRootNode.getSession().save(); + return child; + } + + public void testHoldGetName() throws RepositoryException, NotExecutableException { + String holdName = getHoldName(); + Hold h = retentionMgr.addHold(testNodePath, getHoldName(), false); + assertEquals("Hold.getName() must return the specified name.",holdName, h.getName()); + } + + public void testHoldGetName2() throws RepositoryException, NotExecutableException { + String holdName = getHoldName(); + Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + assertEquals("Hold.getName() must return the specified name.",holdName, h.getName()); + } + + public void testHoldIsDeep() throws RepositoryException, NotExecutableException { + Hold h = retentionMgr.addHold(testNodePath, getHoldName(), false); + assertEquals("Hold.isDeep() must reflect the specified flag.", false, h.isDeep()); + } + + public void testHoldIsDeep2() throws RepositoryException, NotExecutableException { + Hold h = retentionMgr.addHold(testNodePath, getHoldName(), true); + assertEquals("Hold.isDeep() must reflect the specified flag.", true, h.isDeep()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/RetentionPolicyEffectTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/RetentionPolicyEffectTest.java new file mode 100644 index 00000000000..0e2ce341dd2 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/RetentionPolicyEffectTest.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.test.api.retention; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.retention.RetentionPolicy; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; + +/** + * RetentionPolicyEffectTest... + */ +public class RetentionPolicyEffectTest extends AbstractRetentionTest { + + private Node childN; + private Property childP; + private Session otherS; + + protected void setUp() throws Exception { + super.setUp(); + + childN = testRootNode.addNode(nodeName2); + Value v = getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE1, RepositoryStub.PROP_PROP_TYPE1, "test"); + childP = testRootNode.setProperty(propertyName1, v); + superuser.save(); + + otherS = getHelper().getSuperuserSession(); + } + + protected void tearDown() throws Exception { + if (otherS != null) { + otherS.logout(); + } + superuser.refresh(false); + RetentionPolicy rp = retentionMgr.getRetentionPolicy(testNodePath); + if (rp != null) { + retentionMgr.removeRetentionPolicy(testNodePath); + superuser.save(); + } + super.tearDown(); + } + + // TODO: test importXML (session/wsp) / move (session/wsp) / copy ... + // TODO: test effect on child items + + public void testTransientRententionPolicy() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + + assertNoEffect(testRootNode, nodeName3, propertyName2); + assertNoEffect(childN, nodeName3, propertyName2); + assertNoEffect(childP); + } + + public void testTransientRententionPolicy2() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + + assertNoEffect((Node) otherS.getItem(testNodePath), nodeName3, propertyName2); + assertNoEffect((Node) otherS.getItem(childN.getPath()), nodeName3, propertyName2); + assertNoEffect((Property) otherS.getItem(childP.getPath())); + } + + public void testRententionPolicy() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + superuser.save(); + + // check for superuser + assertEffect(testRootNode, childN.getName(), childP.getName(), nodeName3, propertyName2); + } + + public void testRententionPolicy2() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + superuser.save(); + + // check for other session + assertEffect((Node) otherS.getItem(testNodePath), childN.getName(), childP.getName(), nodeName3, propertyName2); + } + + private void assertEffect(Node targetNode, String childName, + String propName, String childName2, + String propName2) throws RepositoryException { + Session s = targetNode.getSession(); + try { + Node child = targetNode.getNode(childName); + child.remove(); + s.save(); + fail("Retention policy present must prevent a child node from being removed."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + Property p = targetNode.getProperty(propName); + p.remove(); + s.save(); + fail("Retention policy present must prevent a child property from being removed."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + Property p = targetNode.getProperty(propName); + p.setValue("test2"); + s.save(); + fail("Retention policy present must prevent the child property from being modified."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + targetNode.addNode(childName2); + s.save(); + fail("Retention policy present must prevent the target node from having new nodes added."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + Value v = getJcrValue(s, RepositoryStub.PROP_PROP_VALUE2, RepositoryStub.PROP_PROP_TYPE2, "test"); + targetNode.setProperty(propName2, v); + s.save(); + fail("Retention policy present must prevent the target node from having new properties set."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + try { + targetNode.remove(); + s.save(); + fail("Hold present must prevent the target node from being removed."); + } catch (RepositoryException e) { + // success + s.refresh(false); + } + } + + private void assertNoEffect(Node target, String childName, String propName) throws RepositoryException { + Session s = target.getSession(); + + Node n = target.addNode(childName); + Value v = getJcrValue(s, RepositoryStub.PROP_PROP_VALUE2, RepositoryStub.PROP_PROP_TYPE2, "test"); + Property p = target.setProperty(propName, v); + + n.remove(); + p.remove(); + } + + private void assertNoEffect(Property target) throws RepositoryException { + target.setValue("test3"); + target.remove(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/RetentionPolicyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/RetentionPolicyTest.java new file mode 100644 index 00000000000..7214c12bb85 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/RetentionPolicyTest.java @@ -0,0 +1,428 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.retention; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.LockException; +import javax.jcr.retention.RetentionManager; +import javax.jcr.retention.RetentionPolicy; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * RetentionPolicyTest... + */ +public class RetentionPolicyTest extends AbstractRetentionTest { + + protected void setUp() throws Exception { + super.setUp(); + + // make sure there is no retention policy defined at testNodePath. + RetentionPolicy p = retentionMgr.getRetentionPolicy(testNodePath); + if (p != null) { + retentionMgr.removeRetentionPolicy(testNodePath); + superuser.save(); + } + } + + public void testGetRetentionPolicy() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + + RetentionPolicy policy = retentionMgr.getRetentionPolicy(testNodePath); + assertNotNull("RetentionManager.getRetentionPolicy must return the policy set before.", policy); + } + + public void testGetRetentionPolicyOnChild() throws RepositoryException, NotExecutableException { + String childPath = testRootNode.addNode(nodeName2, testNodeType).getPath(); + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + + assertNull("RetentionManager.getRetentionPolicy called on child must not return the policy set before.", + retentionMgr.getRetentionPolicy(childPath)); + } + + public void testRetentionPolicyGetName() throws RepositoryException, NotExecutableException { + RetentionPolicy p = getApplicableRetentionPolicy(); + retentionMgr.setRetentionPolicy(testNodePath, p); + + RetentionPolicy policy = retentionMgr.getRetentionPolicy(testNodePath); + assertEquals("RetentionPolicy.getName() must match the name of the policy set before.", p.getName(), policy.getName()); + } + + public void testSetRetentionPolicyIsTransient() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + superuser.refresh(false); + + assertNull("Reverting transient changes must remove the pending retention policy.", + retentionMgr.getRetentionPolicy(testNodePath)); + } + + public void testRemovePendingRetentionPolicy() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + retentionMgr.removeRetentionPolicy(testNodePath); + + assertNull("Removing pending retention policy must succeed.", retentionMgr.getRetentionPolicy(testNodePath)); + } + + public void testRemoveRetentionPolicy() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + superuser.save(); + + retentionMgr.removeRetentionPolicy(testNodePath); + assertNull("Removing persisted retention policy must succeed.", retentionMgr.getRetentionPolicy(testNodePath)); + superuser.save(); + assertNull("Removing persisted retention policy must succeed.", retentionMgr.getRetentionPolicy(testNodePath)); + } + + public void testRemoveRetentionPolicyIsTransient() throws RepositoryException, NotExecutableException { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + superuser.save(); + + try { + retentionMgr.removeRetentionPolicy(testNodePath); + superuser.refresh(false); + assertNotNull("Reverting transient removal must re-add the retention policy.", + retentionMgr.getRetentionPolicy(testNodePath)); + } finally { + retentionMgr.removeRetentionPolicy(testNodePath); + superuser.save(); + } + } + + public void testRemoveRetentionPolicyFromChild() throws RepositoryException, NotExecutableException { + String childPath = testRootNode.addNode(nodeName2, testNodeType).getPath(); + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + + try { + retentionMgr.removeRetentionPolicy(childPath); + fail("Removing retention policy from another node must fail"); + } catch (RepositoryException e) { + // success + assertNull(retentionMgr.getRetentionPolicy(childPath)); + } + + // check again with persisted policy + superuser.save(); + try { + retentionMgr.removeRetentionPolicy(childPath); + fail("Removing retention policy from another node must fail"); + } catch (RepositoryException e) { + // success + assertNull(retentionMgr.getRetentionPolicy(childPath)); + } finally { + // rm the policy that was permanently added before. + retentionMgr.removeRetentionPolicy(testNodePath); + superuser.save(); + } + } + + public void testInvalidPath() throws RepositoryException, NotExecutableException { + String invalidPath = testPath; // not an absolute path + try { + retentionMgr.getRetentionPolicy(invalidPath); + fail("Accessing retention policy for an invalid path must throw RepositoryException."); + } catch (RepositoryException e) { + // success + } + try { + retentionMgr.setRetentionPolicy(invalidPath, getApplicableRetentionPolicy()); + fail("Setting retention policy with an invalid path must throw RepositoryException."); + } catch (RepositoryException e) { + // success + } + try { + retentionMgr.removeRetentionPolicy(invalidPath); + fail("Removing retention policy with an invalid path must throw RepositoryException."); + } catch (RepositoryException e) { + // success + } + } + + public void testNonExistingNodePath() throws RepositoryException, NotExecutableException { + String invalidPath = testNodePath + "/nonexisting"; + int cnt = 0; + while (superuser.nodeExists(invalidPath)) { + invalidPath += cnt++; + } + + try { + retentionMgr.getRetentionPolicy(invalidPath); + fail("Accessing retention policy from non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + try { + retentionMgr.setRetentionPolicy(invalidPath, getApplicableRetentionPolicy()); + fail("Setting retention policy for a non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + try { + retentionMgr.removeRetentionPolicy(invalidPath); + fail("Removing retention policy at a non-existing node must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + } + + public void testPropertyPath() throws RepositoryException, NotExecutableException { + String propPath = null; + for (PropertyIterator it = testRootNode.getProperties(); it.hasNext();) { + String path = it.nextProperty().getPath(); + if (!superuser.nodeExists(path)) { + propPath = path; + break; + } + } + if (propPath == null) { + throw new NotExecutableException(); + } + try { + retentionMgr.getRetentionPolicy(propPath); + fail("Accessing retention policy from property must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + try { + retentionMgr.setRetentionPolicy(propPath, getApplicableRetentionPolicy()); + fail("Setting retention policy for property must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + try { + retentionMgr.removeRetentionPolicy(propPath); + fail("Removing retention policy at property path must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + } + + public void testInvalidName() { + try { + RetentionPolicy rp = new RetentionPolicy() { + public String getName() throws RepositoryException { + return "*.[y]"; + } + }; + retentionMgr.setRetentionPolicy(testNodePath, rp); + fail("Setting a policy with an invalid JCR name must fail."); + } catch (RepositoryException e) { + // success + } catch (IllegalArgumentException e) { + // fine as well. + } + } + + public void testReadOnlySession() throws NotExecutableException, RepositoryException { + Session s = getHelper().getReadOnlySession(); + try { + RetentionManager rmgr = getRetentionManager(s); + try { + rmgr.getRetentionPolicy(testNodePath); + fail("Read-only session doesn't have sufficient privileges to retrieve retention policy."); + } catch (AccessDeniedException e) { + // success + } + try { + rmgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + fail("Read-only session doesn't have sufficient privileges to retrieve retention policy."); + } catch (AccessDeniedException e) { + // success + } + } finally { + s.logout(); + } + } + + public void testSetRetentionPolicyOnLockedNode() throws NotExecutableException, RepositoryException { + String childPath = getLockedChildNode().getPath(); + + // get another session. + Session otherS = getHelper().getSuperuserSession(); + try { + RetentionManager rmgr = getRetentionManager(otherS); + rmgr.setRetentionPolicy(childPath, getApplicableRetentionPolicy()); + otherS.save(); + + fail("Setting a retention policy on a locked node must throw LockException."); + } catch (LockException e) { + // success + } finally { + otherS.logout(); + + if (retentionMgr.getRetentionPolicy(childPath) != null) { + retentionMgr.removeRetentionPolicy(childPath); + } + superuser.save(); + } + } + + public void testRemoveRetentionPolicyOnLockedNode() throws NotExecutableException, RepositoryException { + String childPath = getLockedChildNode().getPath(); + retentionMgr.setRetentionPolicy(childPath, getApplicableRetentionPolicy()); + testRootNode.getSession().save(); + + Session otherS = getHelper().getSuperuserSession(); + try { + RetentionManager rmgr = getRetentionManager(otherS); + rmgr.removeRetentionPolicy(childPath); + fail("Removing a retention policy on a locked node must throw LockException."); + } catch (LockException e) { + // success + } finally { + otherS.logout(); + + // clear retention policy added before + try { + retentionMgr.removeRetentionPolicy(childPath); + superuser.save(); + } catch (RepositoryException e) { + // should not get here if test is correctly executed. + } + } + } + + private Node getLockedChildNode() throws NotExecutableException, RepositoryException { + checkSupportedOption(Repository.OPTION_LOCKING_SUPPORTED); + Node child = testRootNode.addNode(nodeName2, testNodeType); + ensureMixinType(child, mixLockable); + testRootNode.getSession().save(); + child.lock(false, true); // session-scoped lock clean upon superuser-logout. + return child; + } + + public void testSetRetentionPolicyOnCheckedInNode() throws NotExecutableException, RepositoryException { + Node child = getVersionableChildNode(); + child.checkout(); + child.checkin(); + String childPath = child.getPath(); + + // get another session. + Session otherS = getHelper().getSuperuserSession(); + try { + RetentionManager rmgr = getRetentionManager(otherS); + rmgr.setRetentionPolicy(childPath, getApplicableRetentionPolicy()); + otherS.save(); + + fail("Setting a retention policy on a checked-in node must throw VersionException."); + } catch (VersionException e) { + // success + } finally { + otherS.logout(); + + // clear policies (in case of test failure) + try { + retentionMgr.removeRetentionPolicy(childPath); + superuser.save(); + } catch (RepositoryException e) { + // ignore. + } + } + } + + public void testRemoveRetentionPolicyOnCheckedInNode() throws NotExecutableException, RepositoryException { + Node child = getVersionableChildNode(); + child.checkout(); + retentionMgr.setRetentionPolicy(child.getPath(), getApplicableRetentionPolicy()); + superuser.save(); + child.checkin(); + + Session otherS = getHelper().getSuperuserSession(); + try { + RetentionManager rmgr = getRetentionManager(otherS); + rmgr.removeRetentionPolicy(child.getPath()); + otherS.save(); + fail("Removing a retention policy on a checked-in node must throw VersionException."); + } catch (VersionException e) { + // success + } finally { + otherS.logout(); + + // clear policy added before + child.checkout(); + try { + retentionMgr.removeRetentionPolicy(child.getPath()); + superuser.save(); + } catch (RepositoryException e) { + // should not get here if test is correctly executed. + } + } + } + + private Node getVersionableChildNode() throws NotExecutableException, RepositoryException { + checkSupportedOption(Repository.OPTION_VERSIONING_SUPPORTED); + Node child = testRootNode.addNode(nodeName2, testNodeType); + ensureMixinType(child, mixVersionable); + testRootNode.getSession().save(); + return child; + } + + + public void testSetRetentionPolicyBelow() throws RepositoryException, NotExecutableException { + Node childN = testRootNode.addNode(nodeName2); + superuser.save(); + + try { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + retentionMgr.setRetentionPolicy(childN.getPath(), getApplicableRetentionPolicy()); + superuser.save(); + } finally { + superuser.refresh(false); + if (retentionMgr.getRetentionPolicy(testNodePath) != null) { + retentionMgr.removeRetentionPolicy(testNodePath); + } + if (retentionMgr.getRetentionPolicy(childN.getPath()) != null) { + retentionMgr.removeRetentionPolicy(childN.getPath()); + } + superuser.save(); + } + } + + public void testOtherSessionSetsRetentionPolicyBelow() throws RepositoryException, NotExecutableException { + Node childN = testRootNode.addNode(nodeName2); + superuser.save(); + + Session otherS = getHelper().getSuperuserSession(); + try { + retentionMgr.setRetentionPolicy(testNodePath, getApplicableRetentionPolicy()); + superuser.save(); + + getRetentionManager(otherS).setRetentionPolicy(childN.getPath(), getApplicableRetentionPolicy()); + otherS.save(); + } finally { + // logout the other session + otherS.logout(); + + // remove the retention policies again. + superuser.refresh(false); + if (retentionMgr.getRetentionPolicy(testNodePath) != null) { + retentionMgr.removeRetentionPolicy(testNodePath); + } + if (retentionMgr.getRetentionPolicy(childN.getPath()) != null) { + retentionMgr.removeRetentionPolicy(childN.getPath()); + } + superuser.save(); + } + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/TestAll.java new file mode 100644 index 00000000000..c86f7c553d0 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/retention/TestAll.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.test.api.retention; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package javax.jcr.retention. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("javax.jcr.retention tests"); + + suite.addTestSuite(HoldTest.class); + suite.addTestSuite(HoldEffectTest.class); + suite.addTestSuite(RetentionPolicyTest.class); + suite.addTestSuite(RetentionPolicyEffectTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AbstractAccessControlTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AbstractAccessControlTest.java new file mode 100644 index 00000000000..e5f62c6415b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AbstractAccessControlTest.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.test.api.security; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AbstractAccessControlTest... + */ +public abstract class AbstractAccessControlTest extends AbstractJCRTest { + + protected AccessControlManager acMgr; + + protected void setUp() throws Exception { + checkSupportedOption(Repository.OPTION_ACCESS_CONTROL_SUPPORTED); + + super.setUp(); + try { + acMgr = getAccessControlManager(superuser); + } catch (NotExecutableException e) { + cleanUp(); + throw e; + } + } + + protected static AccessControlManager getAccessControlManager(Session s) throws RepositoryException, NotExecutableException { + try { + return s.getAccessControlManager(); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(); + } + } + + protected Privilege[] privilegesFromName(String privilegeName) throws RepositoryException, NotExecutableException { + AccessControlManager acMgr = getAccessControlManager(superuser); + return new Privilege[] {acMgr.privilegeFromName(privilegeName)}; + } + + protected Privilege[] privilegesFromNames(String[] privilegeNames) throws RepositoryException, NotExecutableException { + AccessControlManager acMgr = getAccessControlManager(superuser); + Privilege[] privs = new Privilege[privilegeNames.length]; + for (int i = 0; i < privilegeNames.length; i++) { + privs[i] = acMgr.privilegeFromName(privilegeNames[i]); + } + return privs; + } + + protected void checkCanReadAc(String path) throws RepositoryException, NotExecutableException { + if (!acMgr.hasPrivileges(path, privilegesFromName(Privilege.JCR_READ_ACCESS_CONTROL))) { + throw new NotExecutableException(); + } + } + + protected void checkCanModifyAc(String path) throws RepositoryException, NotExecutableException { + if (!acMgr.hasPrivileges(path, privilegesFromName(Privilege.JCR_MODIFY_ACCESS_CONTROL))) { + throw new NotExecutableException(); + } + } + + protected String getPathToNonExistingNode() throws RepositoryException { + String name = "nonexisting"; + String path = name; + int i = 0; + while (testRootNode.hasNode(path)) { + path = name + i; + i++; + } + + path = testRootNode.getPath() + "/" + path; + return path; + } + + protected String getPathToProperty() throws RepositoryException { + String path = testRootNode.getPath() + "/" + jcrPrimaryType; + if (superuser.nodeExists(path)) { + throw new RepositoryException("Path " + path + " should point to property."); + } + return path; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlDiscoveryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlDiscoveryTest.java new file mode 100644 index 00000000000..10cacfe3623 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlDiscoveryTest.java @@ -0,0 +1,353 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.security; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AccessControlDiscoveryTest... + */ +public class AccessControlDiscoveryTest extends AbstractAccessControlTest { + + public void testGetSupportedPrivileges() throws RepositoryException { + // retrieving supported privileges: + // Quote from spec: + // "[...] it returns the privileges that the repository supports." + Privilege[] privileges = acMgr.getSupportedPrivileges(testRootNode.getPath()); + + // Quote from spec: + // "A repository must support the following standard privileges." + List names = new ArrayList(privileges.length); + for (int i = 0; i < privileges.length; i++) { + names.add(privileges[i].getName()); + } + + // test if those privileges are present: + String msg = "A repository must support the privilege "; + assertTrue(msg + Privilege.JCR_READ, names.contains(getJCRName(Privilege.JCR_READ, superuser))); + assertTrue(msg + Privilege.JCR_ADD_CHILD_NODES, names.contains(getJCRName(Privilege.JCR_ADD_CHILD_NODES, superuser))); + assertTrue(msg + Privilege.JCR_REMOVE_CHILD_NODES, names.contains(getJCRName(Privilege.JCR_REMOVE_CHILD_NODES, superuser))); + assertTrue(msg + Privilege.JCR_MODIFY_PROPERTIES, names.contains(getJCRName(Privilege.JCR_MODIFY_PROPERTIES, superuser))); + assertTrue(msg + Privilege.JCR_REMOVE_NODE, names.contains(getJCRName(Privilege.JCR_REMOVE_NODE, superuser))); + assertTrue(msg + Privilege.JCR_READ_ACCESS_CONTROL, names.contains(getJCRName(Privilege.JCR_READ_ACCESS_CONTROL, superuser))); + assertTrue(msg + Privilege.JCR_MODIFY_ACCESS_CONTROL, names.contains(getJCRName(Privilege.JCR_MODIFY_ACCESS_CONTROL, superuser))); + assertTrue(msg + Privilege.JCR_WRITE, names.contains(getJCRName(Privilege.JCR_WRITE, superuser))); + assertTrue(msg + Privilege.JCR_ALL, names.contains(getJCRName(Privilege.JCR_ALL, superuser))); + } + + public void testPrivilegeFromName() throws RepositoryException { + Privilege[] privileges = acMgr.getSupportedPrivileges(testRootNode.getPath()); + for (int i = 0; i < privileges.length; i++) { + Privilege p = acMgr.privilegeFromName(privileges[i].getName()); + assertEquals("Expected equal privilege name.", privileges[i].getName(), p.getName()); + assertEquals("Expected equal privilege.", privileges[i], p); + } + } + + public void testMandatoryPrivilegeFromName() throws RepositoryException { + List l = new ArrayList(); + l.add(getJCRName(Privilege.JCR_READ, superuser)); + l.add(getJCRName(Privilege.JCR_ADD_CHILD_NODES, superuser)); + l.add(getJCRName(Privilege.JCR_REMOVE_CHILD_NODES, superuser)); + l.add(getJCRName(Privilege.JCR_MODIFY_PROPERTIES, superuser)); + l.add(getJCRName(Privilege.JCR_REMOVE_NODE, superuser)); + l.add(getJCRName(Privilege.JCR_READ_ACCESS_CONTROL, superuser)); + l.add(getJCRName(Privilege.JCR_MODIFY_ACCESS_CONTROL, superuser)); + l.add(getJCRName(Privilege.JCR_WRITE, superuser)); + l.add(getJCRName(Privilege.JCR_ALL, superuser)); + + for (Iterator it = l.iterator(); it.hasNext();) { + String privName = it.next(); + Privilege p = acMgr.privilegeFromName(privName); + assertEquals("Expected equal privilege name.", privName, p.getName()); + } + } + + public void testUnknownPrivilegeFromName() throws RepositoryException { + String unknownPrivilegeName = Math.random() + ""; + try { + acMgr.privilegeFromName(unknownPrivilegeName); + fail(unknownPrivilegeName + " isn't the name of a known privilege."); + } catch (AccessControlException e) { + // success + } + } + + public void testAllPrivilegeContainsAll() throws RepositoryException, NotExecutableException { + Privilege[] supported = acMgr.getSupportedPrivileges(testRootNode.getPath()); + + Privilege all = acMgr.privilegeFromName(Privilege.JCR_ALL); + Set allSet = new HashSet(); + allSet.addAll(Arrays.asList(all.getAggregatePrivileges())); + + String msg = "The all privilege must also contain "; + for (int i=0; i < supported.length; i++) { + Privilege sp = supported[i]; + if (sp.isAggregate()) { + Collection col = Arrays.asList(sp.getAggregatePrivileges()); + assertTrue(msg + sp.getName(), allSet.containsAll(col)); + } else { + assertTrue(msg + sp.getName(), allSet.contains(sp)); + } + } + } + + /** + * Test the jcr:all privilege. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testAllPrivilege() throws RepositoryException, NotExecutableException { + Privilege all = acMgr.privilegeFromName(Privilege.JCR_ALL); + assertFalse("All privilege must be not be abstract.", all.isAbstract()); + assertTrue("All privilege must be an aggregate privilege.", all.isAggregate()); + String expected = getJCRName(Privilege.JCR_ALL, superuser); + assertEquals("The name of the all privilege must be " + expected, expected, all.getName()); + } + + /** + * Test the jcr:write privilege. + * + * @throws RepositoryException If an error occurs. + * @throws NotExecutableException If the test cannot be executed. + */ + public void testWritePrivilege() throws RepositoryException, NotExecutableException { + Privilege w = acMgr.privilegeFromName(Privilege.JCR_WRITE); + assertTrue("Write privilege must be an aggregate privilege.", w.isAggregate()); + String expected = getJCRName(Privilege.JCR_WRITE, superuser); + assertEquals("The name of the write privilege must be " + expected, expected, w.getName()); + } + + /** + * Test some simple characteristics of the known aggregate privileges (jcr:write and jcr:all). + * + * @throws RepositoryException + */ + public void testAggregregatePrivileges() throws RepositoryException { + List l = new ArrayList(); + l.add(getJCRName(Privilege.JCR_WRITE, superuser)); + l.add(getJCRName(Privilege.JCR_ALL, superuser)); + + for (Iterator it = l.iterator(); it.hasNext();) { + String privName = it.next(); + Privilege p = acMgr.privilegeFromName(privName); + + assertTrue("write and all must always be aggregate privileges.", p.isAggregate()); + Privilege[] aggregatedPrvs = p.getAggregatePrivileges(); + Privilege[] declaredPrvs = p.getDeclaredAggregatePrivileges(); + + assertNotNull("An aggregate privilege must return the aggregated privileges", aggregatedPrvs); + assertTrue("An aggregate privilege must return the aggregated privileges", aggregatedPrvs.length > 0); + + assertNotNull("An aggregate privilege must return the declared aggregated privileges", declaredPrvs); + assertTrue("An aggregate privilege must return the declared aggregated privileges", declaredPrvs.length > 0); + + assertTrue("The may be at least the same amount of declared aggregated privileges.", aggregatedPrvs.length >= declaredPrvs.length); + } + + } + + /** + * Tests if the privilege name is treated as JCR Name and consequently + * reflects changes made to the namespace prefix. + * + * @throws RepositoryException If an error occurs. + */ + public void testPrivilegeName() throws RepositoryException { + Privilege allPriv = acMgr.privilegeFromName(Privilege.JCR_ALL); + try { + String remappedPrefix = "_jcr"; + superuser.setNamespacePrefix(remappedPrefix, "http://www.jcp.org/jcr/1.0"); + + List l = new ArrayList(); + l.add(acMgr.privilegeFromName(Privilege.JCR_ALL)); + l.add(acMgr.privilegeFromName(remappedPrefix + ":all")); + + for (Iterator it = l.iterator(); it.hasNext();) { + Privilege p = it.next(); + + assertEquals("The privilege name must reflect the modified namespace prefix.",remappedPrefix + ":all", p.getName()); + assertEquals("jcr:all privileges must be equal.",allPriv, p); + } + + try { + acMgr.privilegeFromName("jcr:all"); + fail("Modified namespace prefix: 'jcr:all' privilege must not exist."); + } catch (RepositoryException e) { + // success + } + } finally { + superuser.setNamespacePrefix("jcr", "http://www.jcp.org/jcr/1.0"); + } + } + + /** + * + * @throws RepositoryException If an error occurs. + */ + public void testGetPrivileges() throws RepositoryException { + acMgr.getPrivileges(testRootNode.getPath()); + } + + /** + * + * @throws RepositoryException If an error occurs. + */ + public void testGetPrivilegesOnNonExistingNode() throws RepositoryException { + String path = getPathToNonExistingNode(); + try { + acMgr.getPrivileges(path); + fail("AccessControlManager.getPrivileges for an invalid absPath must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // ok + } + } + + /** + * + * @throws RepositoryException If an error occurs. + * @throws NotExecutableException If the test cannot be executed. + */ + public void testGetPrivilegesOnProperty() throws RepositoryException, NotExecutableException { + String path = getPathToProperty(); + try { + acMgr.getPrivileges(path); + fail("AccessControlManager.getPrivileges for a property path must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // ok + } + } + + /** + * + * @throws RepositoryException If an error occurs. + */ + public void testHasPrivileges() throws RepositoryException { + Privilege[] privs = acMgr.getPrivileges(testRootNode.getPath()); + assertTrue(acMgr.hasPrivileges(testRootNode.getPath(), privs)); + } + + /** + * + * @throws RepositoryException If an error occurs. + */ + public void testHasIndividualPrivileges() throws RepositoryException { + Privilege[] privs = acMgr.getPrivileges(testRootNode.getPath()); + + for (int i = 0; i < privs.length; i++) { + Privilege[] single = new Privilege[] {privs[i]}; + assertTrue(acMgr.hasPrivileges(testRootNode.getPath(), single)); + } + } + + /** + * + * @throws RepositoryException If an error occurs. + * @throws NotExecutableException If the test cannot be executed. + */ + public void testNotHasPrivileges() throws RepositoryException, NotExecutableException { + Privilege[] privs = acMgr.getPrivileges(testRootNode.getPath()); + Privilege all = acMgr.privilegeFromName(Privilege.JCR_ALL); + + // remove all privileges that are granted. + Set notGranted = new HashSet(Arrays.asList(all.getAggregatePrivileges())); + for (int i = 0; i < privs.length; i++) { + if (privs[i].isAggregate()) { + notGranted.removeAll(Arrays.asList(privs[i].getAggregatePrivileges())); + } else { + notGranted.remove(privs[i]); + } + } + + // make sure that either 'all' are granted or the 'diff' is denied. + if (notGranted.isEmpty()) { + assertTrue(acMgr.hasPrivileges(testRootNode.getPath(), new Privilege[] {all})); + } else { + Privilege[] toTest = notGranted.toArray(new Privilege[notGranted.size()]); + assertTrue(!acMgr.hasPrivileges(testRootNode.getPath(), toTest)); + } + } + + /** + * + * @throws RepositoryException If an error occurs. + */ + public void testHasPrivilegesOnNotExistingNode() throws RepositoryException { + String path = getPathToNonExistingNode(); + try { + acMgr.hasPrivileges(path, new Privilege[0]); + fail("AccessControlManager.hasPrivileges for an invalid absPath must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + } + + /** + * + * @throws RepositoryException If an error occurs. + * @throws NotExecutableException If the test cannot be executed. + */ + public void testHasPrivilegesOnProperty() throws RepositoryException, NotExecutableException { + String path = getPathToProperty(); + try { + acMgr.hasPrivileges(path, new Privilege[0]); + fail("AccessControlManager.hasPrivileges for a property path must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // success + } + } + + /** + * + * @throws RepositoryException If an error occurs. + * @throws NotExecutableException If the test cannot be executed. + */ + public void testHasPrivilegesEmptyArray() throws RepositoryException, NotExecutableException { + assertTrue(acMgr.hasPrivileges(testRootNode.getPath(), new Privilege[0])); + } + + //-------------------------------------------------------------------------- + /** + * Retrieve the prefixed jcr name from a given privilege name constant. + * + * @param privilegeNameConstant + * @param session + * @return + * @throws RepositoryException If an error occurs. + */ + private static String getJCRName(String privilegeNameConstant, Session session) throws RepositoryException { + int pos = privilegeNameConstant.indexOf('}'); + String uri = privilegeNameConstant.substring(1, pos); + String localName = privilegeNameConstant.substring(pos + 1); + return session.getNamespacePrefix(uri) + ":" + localName; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlListTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlListTest.java new file mode 100644 index 00000000000..096b952cfc7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlListTest.java @@ -0,0 +1,524 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.security; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AccessControlEntryTest... + */ +public class AccessControlListTest extends AbstractAccessControlTest { + + private static Logger log = LoggerFactory.getLogger(AccessControlListTest.class); + + private String path; + private Privilege[] privs; + private Principal testPrincipal; + + private List privilegesToRestore = new ArrayList(); + + protected void setUp() throws Exception { + checkSupportedOption(Repository.OPTION_ACCESS_CONTROL_SUPPORTED); + + super.setUp(); + + // TODO: make sure, entries to ADD are not present yet. + try { + // TODO: retrieve targetPath from configuration + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + path = n.getPath(); + + privs = acMgr.getSupportedPrivileges(path); + + if (privs.length == 0) { + throw new NotExecutableException("No supported privileges at absPath " + path); + } + + testPrincipal = getHelper().getKnownPrincipal(superuser); + + // remember existing entries for test-principal -> later restore. + privilegesToRestore = currentPrivileges(getList(acMgr, path), testPrincipal); + } catch (Exception e) { + superuser.logout(); + throw e; + } + } + + protected void tearDown() throws Exception { + try { + // restore original entries (remove others). + AccessControlList list = getList(acMgr, path); + AccessControlEntry[] entries = list.getAccessControlEntries(); + for (int i = 0; i < entries.length; i++) { + AccessControlEntry ace = entries[i]; + if (testPrincipal.equals(ace.getPrincipal())) { + list.removeAccessControlEntry(ace); + } + } + if (!privilegesToRestore.isEmpty()) { + list.addAccessControlEntry(testPrincipal, (Privilege[]) privilegesToRestore.toArray(new Privilege[privilegesToRestore.size()])); + } + if (list.getAccessControlEntries().length > 0 && acMgr.getPolicies(path).length > 0) { + acMgr.setPolicy(path, list); + superuser.save(); + } + } catch (Exception e) { + log.warn("Unexpected error while removing test entries.", e); + } + super.tearDown(); + } + + private static AccessControlList getList(AccessControlManager acMgr, String path) + throws NotExecutableException, AccessDeniedException, RepositoryException { + for (AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); it.hasNext();) { + AccessControlPolicy acp = it.nextAccessControlPolicy(); + if (acp instanceof AccessControlList) { + return (AccessControlList) acp; + } + } + AccessControlPolicy[] acps = acMgr.getPolicies(path); + for (int i = 0; i < acps.length; i++) { + if (acps[i] instanceof AccessControlList) { + return (AccessControlList) acps[i] ; + } + } + throw new NotExecutableException("No AccessControlList at " + path); + } + + private static List currentPrivileges(AccessControlList acl, Principal principal) throws RepositoryException { + List privileges = new ArrayList(); + AccessControlEntry[] entries = acl.getAccessControlEntries(); + for (int i = 0; i < entries.length; i++) { + AccessControlEntry ace = entries[i]; + if (principal.equals(ace.getPrincipal())) { + privileges.addAll(Arrays.asList(ace.getPrivileges())); + } + } + return privileges; + } + + public void testGetAccessControlEntries() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + AccessControlList acl = getList(acMgr, path); + + // call must succeed. + AccessControlEntry[] entries = acl.getAccessControlEntries(); + assertNotNull("AccessControlList#getAccessControlEntries must not return null.", entries); + for (int i = 0; i < entries.length; i++) { + assertNotNull("An ACE must contain a principal", entries[i].getPrincipal()); + Privilege[] privs = entries[i].getPrivileges(); + assertTrue("An ACE must contain at least a single privilege", privs != null && privs.length > 0); + } + } + + public void testAddAccessControlEntry() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + Privilege[] privileges = new Privilege[] {privs[0]}; + AccessControlList acl = getList(acMgr, path); + + AccessControlEntry entry = null; + if (acl.addAccessControlEntry(testPrincipal, privileges)) { + AccessControlEntry[] aces = acl.getAccessControlEntries(); + for (int i = 0; i < aces.length; i++) { + if (aces[i].getPrincipal().equals(testPrincipal) && + Arrays.asList(privileges).equals(Arrays.asList(aces[i].getPrivileges()))) { + entry = aces[i]; + } + } + if (entry == null) throw new NotExecutableException(); + } else { + throw new NotExecutableException(); + + } + assertEquals("Principal name of the ACE must be equal to the name of the passed Principal", testPrincipal.getName(), entry.getPrincipal().getName()); + assertEquals("Privileges of the ACE must be equal to the passed ones", Arrays.asList(privileges), Arrays.asList(entry.getPrivileges())); + } + + public void testAddAggregatePrivilege() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + Privilege aggregate = null; + for (int i = 0; i < privs.length; i++) { + if (privs[i].isAggregate()) { + aggregate = privs[i]; + break; + } + } + if (aggregate == null) { + throw new NotExecutableException("No aggregate privilege supported at " + path); + } + + AccessControlList acl = getList(acMgr, path); + acl.addAccessControlEntry(testPrincipal, new Privilege[] {aggregate}); + + // make sure all privileges are present now + List privs = currentPrivileges(acl, testPrincipal); + assertTrue("Privileges added through 'addAccessControlEntry' must be " + + "reflected upon getAccessControlEntries", + privs.contains(aggregate) || privs.containsAll(Arrays.asList(aggregate.getAggregatePrivileges()))); + } + + public void testAddAggregatedPrivilegesSeparately() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + Privilege aggregate = null; + for (int i = 0; i < privs.length; i++) { + if (privs[i].isAggregate()) { + aggregate = privs[i]; + break; + } + } + if (aggregate == null) { + throw new NotExecutableException("No aggregate privilege supported at " + path); + } + + AccessControlList acl = getList(acMgr, path); + acl.addAccessControlEntry(testPrincipal, new Privilege[] {aggregate}); + + Privilege[] privs = aggregate.getAggregatePrivileges(); + for (int i = 0; i < privs.length; i++) { + boolean modified = acl.addAccessControlEntry(testPrincipal, new Privilege[] {privs[i]}); + assertFalse("Adding the aggregated privs individually later on must not modify the policy", modified); + } + } + + public void testAddAbstractPrivilege() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + Privilege abstractPriv = null; + Privilege[] allPrivs = acMgr.privilegeFromName(Privilege.JCR_ALL).getAggregatePrivileges(); + for (int i = 0; i < allPrivs.length; i++) { + if (allPrivs[i].isAbstract()) { + abstractPriv = allPrivs[i]; + break; + } + } + if (abstractPriv == null) { + throw new NotExecutableException("No abstract privilege found."); + } + + AccessControlList acl = getList(acMgr, path); + try { + acl.addAccessControlEntry(testPrincipal, new Privilege[] {abstractPriv}); + fail("Adding an ACE with an abstract privilege must fail."); + } catch (AccessControlException e) { + // success + } + } + + public void testAddPrivilegesPresentInEntries() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + AccessControlList acl = getList(acMgr, path); + acl.addAccessControlEntry(testPrincipal, privs); + + Set assignedPrivs = new HashSet(); + AccessControlEntry[] entries = acl.getAccessControlEntries(); + for (int i = 0; i < entries.length; i++) { + if (entries[i].getPrincipal().equals(testPrincipal)) { + Privilege[] prvs = entries[i].getPrivileges(); + for (int j = 0; j < prvs.length; j++) { + if (prvs[j].isAggregate()) { + assignedPrivs.addAll(Arrays.asList(prvs[j].getAggregatePrivileges())); + } else { + assignedPrivs.add(prvs[j]); + } + } + } + } + + Set expected = new HashSet(); + for (int i = 0; i < privs.length; i++) { + if (privs[i].isAggregate()) { + expected.addAll(Arrays.asList(privs[i].getAggregatePrivileges())); + } else { + expected.add(privs[i]); + } + } + assertTrue("getAccessControlEntries must contain an entry or entries that grant at least the added privileges.", assignedPrivs.containsAll(expected)); + } + + public void testAddAccessControlEntryAndSetPolicy() throws RepositoryException, NotExecutableException { + checkCanModifyAc(path); + + AccessControlList acl = getList(acMgr, path); + List originalAces = Arrays.asList(acl.getAccessControlEntries()); + + if (!acl.addAccessControlEntry(testPrincipal, privs)) { + throw new NotExecutableException(); + } + + // re-access ACL from AC-Manager -> must not yet have changed + assertEquals("Before calling setPolicy any modifications to an ACL must not be reflected in the policies", originalAces, Arrays.asList(getList(acMgr, path).getAccessControlEntries())); + + // setting the modified policy -> policy must change. + acMgr.setPolicy(path, acl); + assertEquals("Before calling setPolicy any modifications to an ACL must not be reflected in the policies", Arrays.asList(acl.getAccessControlEntries()), Arrays.asList(getList(acMgr, path).getAccessControlEntries())); + } + + public void testAddAccessControlEntryIsTransient() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + AccessControlList acl = getList(acMgr, path); + List originalAces = Arrays.asList(acl.getAccessControlEntries()); + + if (!acl.addAccessControlEntry(testPrincipal, privs)) { + throw new NotExecutableException(); + } + // set the policy (see #testAddAccessControlEntryAndSetPolicy) + acMgr.setPolicy(path, acl); + + // revert the changes made + superuser.refresh(false); + assertEquals("After calling Session.refresh() any changes to a nodes policies must be reverted.", originalAces, Arrays.asList(getList(acMgr, path).getAccessControlEntries())); + } + + public void testAddAccessControlEntryInvalidPrincipal() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + try { + Principal invalidPrincipal = getHelper().getUnknownPrincipal(superuser); + AccessControlList acl = getList(acMgr, path); + acl.addAccessControlEntry(invalidPrincipal, privs); + fail("Adding an entry with an unknown principal must throw AccessControlException."); + } catch (AccessControlException e) { + // success. + } finally { + superuser.refresh(false); + } + } + + public void testAddAccessControlEntryEmptyPrivilegeArray() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + try { + Privilege[] invalidPrivs = new Privilege[0]; + AccessControlList acl = getList(acMgr, path); + acl.addAccessControlEntry(testPrincipal, invalidPrivs); + fail("Adding an entry with an invalid privilege array must throw AccessControlException."); + } catch (AccessControlException e) { + // success. + } finally { + superuser.refresh(false); + } + } + + public void testAddAccessControlEntryInvalidPrivilege() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + try { + Privilege[] invalidPrivs = new Privilege[] {new Privilege() { + public String getName() { + return null; + } + public boolean isAbstract() { + return false; + } + public boolean isAggregate() { + return false; + } + public Privilege[] getDeclaredAggregatePrivileges() { + return new Privilege[0]; + } + public Privilege[] getAggregatePrivileges() { + return new Privilege[0]; + } + }}; + AccessControlList acl = getList(acMgr, path); + acl.addAccessControlEntry(testPrincipal, invalidPrivs); + fail("Adding an entry with an invalid privilege must throw AccessControlException."); + } catch (AccessControlException e) { + // success. + } finally { + superuser.refresh(false); + } + } + + public void testRemoveAccessControlEntry() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + AccessControlList acl = getList(acMgr, path); + AccessControlEntry[] entries = acl.getAccessControlEntries(); + if (entries.length > 0) { + AccessControlEntry ace = entries[0]; + acl.removeAccessControlEntry(ace); + + // retrieve entries again: + List remainingEntries = Arrays.asList(acl.getAccessControlEntries()); + assertFalse("AccessControlList.getAccessControlEntries still returns a removed ACE.", remainingEntries.contains(ace)); + } + } + + public void testRemoveAddedAccessControlEntry() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + AccessControlList acl = getList(acMgr, path); + acl.addAccessControlEntry(testPrincipal, privs); + + AccessControlEntry[] aces = acl.getAccessControlEntries(); + for (int i = 0; i < aces.length; i++) { + acl.removeAccessControlEntry(aces[i]); + } + assertEquals("After removing all ACEs the ACL must be empty", 0, acl.getAccessControlEntries().length); + } + + public void testRemoveAccessControlEntryAndSetPolicy() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + // add a new ACE that can be removed later on. + AccessControlList acl = getList(acMgr, path); + if (!acl.addAccessControlEntry(testPrincipal, privs)) { + throw new NotExecutableException(); + } else { + acMgr.setPolicy(path, acl); + } + + // try to re-access the modifiable ACL in order to remove the ACE + // added before. + acl = getList(acMgr, path); + AccessControlEntry ace = null; + AccessControlEntry[] aces = acl.getAccessControlEntries(); + if (aces.length == 0) { + throw new NotExecutableException(); + } else { + ace = aces[0]; + acl.removeAccessControlEntry(ace); + } + + // before setting the policy again -> no changes visible. + assertEquals("Removal of an ACE must only be visible upon 'setPolicy'", Arrays.asList(aces), Arrays.asList(getList(acMgr, path).getAccessControlEntries())); + + // set policy again. + acMgr.setPolicy(path, acl); + assertEquals("After 'setPolicy' the ACE-removal must be visible to the editing session.", Arrays.asList(acl.getAccessControlEntries()), Arrays.asList(getList(acMgr, path).getAccessControlEntries())); + } + + public void testRemoveAccessControlEntryIsTransient() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + AccessControlList acl = getList(acMgr, path); + // make sure an ACE is present and modifications are persisted. + if (acl.addAccessControlEntry(testPrincipal, privs)) { + acMgr.setPolicy(path, acl); + superuser.save(); + } else { + throw new NotExecutableException(); + } + + // retrieve ACL again -> transient removal of the ace + acl = getList(acMgr, path); + AccessControlEntry ace = acl.getAccessControlEntries()[0]; + acl.removeAccessControlEntry(ace); + acMgr.setPolicy(path, acl); + + // revert changes -> removed entry must be present again. + superuser.refresh(false); + List entries = Arrays.asList(getList(acMgr, path).getAccessControlEntries()); + assertTrue("After reverting any changes the removed ACE should be present again.", entries.contains(ace)); + } + + public void testRemoveIllegalAccessControlEntry() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + try { + AccessControlEntry entry = new AccessControlEntry() { + public Principal getPrincipal() { + return testPrincipal; + } + public Privilege[] getPrivileges() { + return privs; + } + }; + AccessControlList acl = getList(acMgr, path); + acl.removeAccessControlEntry(entry); + fail("AccessControlManager.removeAccessControlEntry with an unknown entry must throw AccessControlException."); + } catch (AccessControlException e) { + // ok + } + } + + public void testAddAccessControlEntryTwice() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + AccessControlList acl = getList(acMgr, path); + if (acl.addAccessControlEntry(testPrincipal, privs)) { + assertFalse("Adding the same ACE twice should not modify the AC-List.", + acl.addAccessControlEntry(testPrincipal, privs)); + } + } + + public void testAddAccessControlEntryAgain() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + + AccessControlList list = getList(acMgr, path); + list.addAccessControlEntry(testPrincipal, privs); + AccessControlEntry[] entries = list.getAccessControlEntries(); + if (entries.length > 0) { + assertFalse("Adding an existing entry again must not modify the AC-List", + list.addAccessControlEntry(entries[0].getPrincipal(), entries[0].getPrivileges())); + } else { + throw new NotExecutableException(); + } + } + + public void testExtendPrivileges() throws NotExecutableException, RepositoryException { + checkCanModifyAc(path); + // search 2 non-aggregated privileges + List twoPrivs = new ArrayList(2); + for (int i = 0; i < privs.length && twoPrivs.size() < 2; i++) { + if (!privs[i].isAggregate()) { + twoPrivs.add(privs[i]); + } + } + if (twoPrivs.size() < 2) { + throw new NotExecutableException("At least 2 supported, non-aggregate privileges required at " + path); + } + + AccessControlList acl = getList(acMgr, path); + Privilege privilege = twoPrivs.get(0); + // add first privilege: + acl.addAccessControlEntry(testPrincipal, new Privilege[] {privilege}); + + // add a second privilege (but not specifying the privilege added before) + // -> the first privilege must not be removed. + Privilege privilege2 = twoPrivs.get(1); + acl.addAccessControlEntry(testPrincipal, new Privilege[] {privilege2}); + + List currentPrivileges = currentPrivileges(acl, testPrincipal); + assertTrue("'AccessControlList.addAccessControlEntry' must not remove privileges added before", currentPrivileges.containsAll(twoPrivs)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlPolicyIteratorTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlPolicyIteratorTest.java new file mode 100644 index 00000000000..c52d3c874d0 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlPolicyIteratorTest.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.test.api.security; + +import java.util.NoSuchElementException; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlPolicyIterator; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AccessControlPolicyIteratorTest... + */ +public class AccessControlPolicyIteratorTest extends AbstractAccessControlTest { + + private String path; + + protected void setUp() throws Exception { + super.setUp(); + + // policy-option is cover the by the 'OPTION_ACCESS_CONTROL_SUPPORTED' -> see super-class + + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + path = n.getPath(); + } + + public void testGetSize() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + // get size must succeed. its value however is indefined. + long size = it.getSize(); + assertTrue("Size must be -1 or any value >= 0", size == -1 || size >= 0); + } + + public void testGetInitialPosition() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + // the initial position of the policy iterator must be 0. + assertTrue("Initial position of AccessControlPolicyIterator must be 0.", it.getPosition() == 0); + } + + public void testGetPosition() throws NotExecutableException, RepositoryException { + checkCanReadAc(path); + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + + long position = 0; + while (it.hasNext()) { + assertEquals("Position must be adjusted during iteration.", position, it.getPosition()); + it.nextAccessControlPolicy(); + assertEquals("Position must be adjusted after calling next.", ++position, it.getPosition()); + } + } + + public void testSkip() throws NotExecutableException, RepositoryException { + checkCanReadAc(path); + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + + long size = it.getSize(); + if (size > -1) { + it.skip(size); + assertFalse("After skipping all elements 'hasNext()' must return false", it.hasNext()); + + try { + it.nextAccessControlPolicy(); + fail("After skipping all 'nextAccessControlPolicy()' must fail."); + } catch (NoSuchElementException e) { + // success + } + } else { + throw new NotExecutableException(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlPolicyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlPolicyTest.java new file mode 100644 index 00000000000..7661351e8c9 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/AccessControlPolicyTest.java @@ -0,0 +1,470 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.security; + +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.AccessDeniedException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.NamedAccessControlPolicy; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AccessControlPolicyTest... + */ +public class AccessControlPolicyTest extends AbstractAccessControlTest { + + private static Logger log = LoggerFactory.getLogger(AccessControlPolicyTest.class); + + private String path; + private Map addedPolicies = new HashMap(); + + protected void setUp() throws Exception { + super.setUp(); + + // policy-option is covered the by the 'OPTION_ACCESS_CONTROL_SUPPORTED' -> see super-class + + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + path = n.getPath(); + } + + protected void tearDown() throws Exception { + try { + for (Iterator it = addedPolicies.keySet().iterator(); it.hasNext();) { + String path = it.next(); + AccessControlPolicy policy = addedPolicies.get(path); + acMgr.removePolicy(path, policy); + } + superuser.save(); + } catch (Exception e) { + log.error("Unexpected error while removing test policies.", e); + } + addedPolicies.clear(); + super.tearDown(); + } + + public void testGetEffectivePolicies() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + // call must succeed without exception + AccessControlPolicy[] policies = acMgr.getEffectivePolicies(path); + if (policies == null || policies.length == 0) { + fail("To every existing node at least a single effective policy applies."); + } + } + + public void testGetEffectivePoliciesForNonExistingNode() throws RepositoryException, AccessDeniedException, NotExecutableException { + String path = getPathToNonExistingNode(); + try { + acMgr.getEffectivePolicies(path); + fail("AccessControlManager.getEffectivePolicy for an invalid absPath must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // ok + } + } + + public void testGetEffectivePoliciesForProperty() throws RepositoryException, AccessDeniedException, NotExecutableException { + String path = getPathToProperty(); + try { + acMgr.getEffectivePolicies(path); + fail("AccessControlManager.getEffectivePolicy for property must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // ok + } + } + + public void testGetPolicies() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + // call must succeed without exception + AccessControlPolicy[] policies = acMgr.getPolicies(path); + + assertNotNull("AccessControlManager.getPolicies must never return null.", policies); + for (int i = 0; i < policies.length; i++) { + if (policies[i] instanceof NamedAccessControlPolicy) { + assertNotNull("The name of an NamedAccessControlPolicy must not be null.", ((NamedAccessControlPolicy) policies[i]).getName()); + } else if (policies[i] instanceof AccessControlList) { + assertNotNull("The entries of an AccessControlList must not be null.", ((AccessControlList) policies[i]).getAccessControlEntries()); + } + } + } + + public void testGetApplicablePolicies() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + // call must succeed without exception + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + assertNotNull("The iterator of applicable policies must not be null", it); + } + + public void testApplicablePoliciesAreDistinct() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + // call must succeed without exception + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + Set acps = new HashSet(); + + while (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + if (!acps.add(policy)) { + fail("The applicable policies present should be unique among the choices. Policy " + policy + " occured multiple times."); + } + } + } + + public void testApplicablePoliciesAreDistintFromSetPolicies() throws RepositoryException, NotExecutableException { + checkCanReadAc(path); + // call must succeed without exception + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + Set acps = new HashSet(); + while (it.hasNext()) { + acps.add(it.nextAccessControlPolicy()); + } + + AccessControlPolicy[] policies = acMgr.getPolicies(path); + for (int i = 0; i < policies.length; i++) { + assertFalse("The applicable policies obtained should not be present among the policies obtained through AccessControlManager.getPolicies.", acps.contains(policies[i])); + } + } + + public void testSetPolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanModifyAc(path); + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + } else { + throw new NotExecutableException(); + } + } + + public void testSetIllegalPolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanModifyAc(path); + try { + acMgr.setPolicy(path, new AccessControlPolicy() {}); + fail("SetPolicy with an unknown policy should throw AccessControlException."); + } catch (AccessControlException e) { + // success. + } + } + + public void testSetAllPolicies() throws RepositoryException, NotExecutableException { + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (!it.hasNext()) { + throw new NotExecutableException(); + } + while (it.hasNext()) { + acMgr.setPolicy(path, it.nextAccessControlPolicy()); + } + // all policies have been set -> no additional applicable policies. + it = acMgr.getApplicablePolicies(path); + assertFalse("After having set all applicable policies AccessControlManager.getApplicablePolicies should return an empty iterator.", + it.hasNext()); + assertEquals("After having set all applicable policies AccessControlManager.getApplicablePolicies should return an empty iterator.", + 0, it.getSize()); + } + + public void testGetPolicyAfterSet() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + checkCanModifyAc(path); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + + AccessControlPolicy[] policies = acMgr.getPolicies(path); + for (int i = 0; i < policies.length; i++) { + if (policy.equals(policies[i])) { + // ok + return; + } + } + fail("GetPolicies must at least return the policy that has been set before."); + } else { + throw new NotExecutableException(); + } + } + + public void testResetPolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + checkCanModifyAc(path); + + // make sure that at least a single policy has been set. + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + } + + // access the policies already present at path and test if updating + // (resetting) the policies works as well. + AccessControlPolicy[] policies = acMgr.getPolicies(path); + for (int i = 0; i < policies.length; i++) { + acMgr.setPolicy(path, policies[i]); + } + } + + public void testSetPolicyIsTransient() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanModifyAc(path); + + List currentPolicies = Arrays.asList(acMgr.getPolicies(path)); + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + superuser.refresh(false); + + String mgs = "Reverting 'setPolicy' must change back the return value of getPolicies."; + if (currentPolicies.isEmpty()) { + assertTrue(mgs, acMgr.getPolicies(path).length == 0); + } else { + assertEquals(mgs, currentPolicies, Arrays.asList(acMgr.getPolicies(path))); + } + } else { + throw new NotExecutableException(); + } + } + + public void testGetPolicyAfterSave() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + checkCanModifyAc(path); + + AccessControlPolicy policy; + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + superuser.save(); + + // remember for tearDown + addedPolicies.put(path, policy); + } else { + throw new NotExecutableException(); + } + + Session s2 = null; + try { + s2 = getHelper().getSuperuserSession(); + List plcs = Arrays.asList(getAccessControlManager(s2).getPolicies(path)); + // TODO: check again if policies can be compared with equals! + assertTrue("Policy must be visible to another superuser session.", plcs.contains(policy)); + } finally { + if (s2 != null) { + s2.logout(); + } + } + } + + + public void testNodeIsModifiedAfterSecondSetPolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanModifyAc(path); + // make sure a policy has been explicitely set. + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + superuser.save(); + // remember for tearDown + addedPolicies.put(path, policy); + } else { + throw new NotExecutableException(); + } + + // call 'setPolicy' a second time -> Node must be modified. + it = acMgr.getApplicablePolicies(path); + try { + if (it.hasNext()) { + Item item = superuser.getItem(path); + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + + assertTrue("After setting a policy the node must be marked modified.", item.isModified()); + } else { + throw new NotExecutableException(); + } + } finally { + // revert changes + superuser.refresh(false); + } + } + + public void testNodeIsModifiedAfterSetPolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanModifyAc(path); + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + Item item = superuser.getItem(path); + + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + + assertTrue("After setting a policy the node must be marked modified.", item.isModified()); + } else { + throw new NotExecutableException(); + } + } + + public void testRemovePolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanModifyAc(path); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + acMgr.removePolicy(path, policy); + + AccessControlPolicy[] plcs = acMgr.getPolicies(path); + for (int i = 0; i < plcs.length; i++) { + if (plcs[i].equals(policy)) { + fail("RemovePolicy must remove the policy that has been set before."); + } + } + } else { + throw new NotExecutableException(); + } + } + + public void testRemovePolicyIsTransient() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + checkCanModifyAc(path); + + AccessControlPolicy[] currentPolicies = acMgr.getPolicies(path); + int size = currentPolicies.length; + AccessControlPolicy toRemove; + if (size == 0) { + // no policy to remove ->> apply one + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + superuser.save(); + + // remember for teardown + addedPolicies.put(path, policy); + + toRemove = policy; + currentPolicies = acMgr.getPolicies(path); + size = currentPolicies.length; + } else { + throw new NotExecutableException(); + } + } else { + toRemove = currentPolicies[0]; + } + + // test transient behaviour of the removal + acMgr.removePolicy(path, toRemove); + + assertEquals("After transient remove AccessControlManager.getPolicies must return less policies.", size - 1, acMgr.getPolicies(path).length); + + // revert changes + superuser.refresh(false); + assertEquals("Reverting a Policy removal must restore the original state.", Arrays.asList(currentPolicies), Arrays.asList(acMgr.getPolicies(path))); + } + + public void testNodeIsModifiedAfterRemovePolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + checkCanReadAc(path); + checkCanModifyAc(path); + + Item item = superuser.getItem(path); + if (acMgr.getPolicies(path).length == 0) { + // no policy to remove ->> apply one + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + superuser.save(); + + // remember for teardown + addedPolicies.put(path, policy); + } else { + throw new NotExecutableException(); + } + } + + // test transient behaviour of the removal + try { + AccessControlPolicy[] plcs = acMgr.getPolicies(path); + if (plcs.length > 0) { + acMgr.removePolicy(path, plcs[0]); + assertTrue("After removing a policy the node must be marked modified.", item.isModified()); + } + } finally { + item.refresh(false); + } + } + + public void testNullPolicyOnNewNode() throws NotExecutableException, RepositoryException, AccessDeniedException { + Node n; + try { + n = ((Node) superuser.getItem(path)).addNode(nodeName2, testNodeType); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + assertTrue("A new Node must not have an access control policy set.", acMgr.getPolicies(n.getPath()).length == 0); + } + + public void testSetPolicyOnNewNode() throws NotExecutableException, RepositoryException, AccessDeniedException { + Node n; + try { + n = ((Node) superuser.getItem(path)).addNode(nodeName2, testNodeType); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(n.getPath()); + while (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(n.getPath(), policy); + + AccessControlPolicy[] plcs = acMgr.getPolicies(n.getPath()); + assertNotNull("After calling setPolicy the manager must return a non-null policy array for the new Node.", plcs); + assertTrue("After calling setPolicy the manager must return a policy array with a length greater than zero for the new Node.", plcs.length > 0); + } + } + + public void testRemoveTransientlyAddedPolicy() throws RepositoryException, AccessDeniedException { + AccessControlPolicy[] ex = acMgr.getPolicies(path); + + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + while (it.hasNext()) { + AccessControlPolicy policy = it.nextAccessControlPolicy(); + acMgr.setPolicy(path, policy); + acMgr.removePolicy(path, policy); + + String msg = "transiently added AND removing a policy must revert " + + "the changes made. " + + "ACMgr.getPolicies must then return the original value."; + assertEquals(msg, Arrays.asList(ex), Arrays.asList(acMgr.getPolicies(path))); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlDiscoveryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlDiscoveryTest.java new file mode 100644 index 00000000000..a04622aa576 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlDiscoveryTest.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.test.api.security; + +import java.util.Arrays; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * RSessionAccessControlDiscoveryTest: A read-only session must + * be able to call 'hasPrivilege' and 'getPrivileges' and 'getSupportedPrivileges' + * without access denied exception + */ +public class RSessionAccessControlDiscoveryTest extends AbstractAccessControlTest { + + private Session readOnlySession; + private AccessControlManager testAcMgr; + private String testPath; + + protected void setUp() throws Exception { + super.setUp(); + + readOnlySession = getHelper().getReadOnlySession(); + testAcMgr = getAccessControlManager(readOnlySession); + testPath = testRootNode.getPath(); + } + + protected void tearDown() throws Exception { + if (readOnlySession != null) { + readOnlySession.logout(); + } + super.tearDown(); + } + + public void testGetSupportedPrivileges() throws RepositoryException { + Privilege[] privileges = testAcMgr.getSupportedPrivileges(testPath); + assertNotNull("getSupportedPrivileges must return a non-null value even for read-only session.", privileges); + assertTrue("getSupportedPrivileges must return a non-empty array even for read-only session.", privileges.length > 0); + } + + public void testGetPrivileges() throws RepositoryException { + List privs = Arrays.asList(testAcMgr.getPrivileges(testPath)); + Privilege readPrivilege = testAcMgr.privilegeFromName(Privilege.JCR_READ); + assertTrue("A read-only session must have READ access to the test node.", + privs.contains(readPrivilege)); + } + + public void testHasPrivileges() throws RepositoryException, NotExecutableException { + Privilege priv = testAcMgr.privilegeFromName(Privilege.JCR_READ); + assertTrue("Read-only session must have READ privilege on test node.", + testAcMgr.hasPrivileges(testPath, new Privilege[] {priv})); + } + + public void testNotHasPrivileges() throws RepositoryException, NotExecutableException { + Privilege all = testAcMgr.privilegeFromName(Privilege.JCR_ALL); + assertFalse("Read-only session must not have ALL privilege", + testAcMgr.hasPrivileges(testPath, new Privilege[] {all})); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlPolicyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlPolicyTest.java new file mode 100644 index 00000000000..875e66ff1a3 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlPolicyTest.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.test.api.security; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AccessControlPolicyTest... + */ +public class RSessionAccessControlPolicyTest extends AbstractAccessControlTest { + + private String path; + private Session readOnlySession; + private AccessControlManager testAcMgr; + + protected void setUp() throws Exception { + super.setUp(); + + // policy-option is cover the by the 'OPTION_ACCESS_CONTROL_SUPPORTED' -> see super-class + + Node n = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + path = n.getPath(); + + readOnlySession = getHelper().getReadOnlySession(); + testAcMgr = getAccessControlManager(readOnlySession); + } + + protected void tearDown() throws Exception { + if (readOnlySession != null) { + readOnlySession.logout(); + } + super.tearDown(); + } + + public void testGetPolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + try { + testAcMgr.getPolicies(path); + fail("read only session may not read AC content."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testGetEffectivePolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + try { + testAcMgr.getEffectivePolicies(path); + fail("read only session may not read AC content."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testGetApplicablePolicies() throws RepositoryException, AccessDeniedException, NotExecutableException { + try { + testAcMgr.getApplicablePolicies(path); + fail("read only session may not read AC content."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testSetPolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + // retrieve valid policy using superuser session: + AccessControlPolicyIterator it = acMgr.getApplicablePolicies(path); + if (!it.hasNext()) { + throw new NotExecutableException(); + } + + try { + testAcMgr.setPolicy(path, it.nextAccessControlPolicy()); + fail("read only session may not modify AC content."); + } catch (AccessControlException e) { + // success. + } + } + + public void testSetInvalidPolicy() throws RepositoryException, AccessDeniedException, NotExecutableException { + try { + testAcMgr.setPolicy(path, new AccessControlPolicy() { + public String getName() throws RepositoryException { + return getClass().getName(); + } + public String getDescription() throws RepositoryException { + return ""; + } + }); + fail("Invalid policy may not be set by a READ-only session."); + } catch (AccessControlException e) { + // success. + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlTest.java new file mode 100644 index 00000000000..85492f9cdb3 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/RSessionAccessControlTest.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.test.api.security; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; + +import org.apache.jackrabbit.test.RepositoryStub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** RSessionAccessControlTest... */ +public class RSessionAccessControlTest extends AbstractAccessControlTest { + + private static Logger log = LoggerFactory.getLogger(RSessionAccessControlTest.class); + + private Session readOnlySession; + private String testNodePath; + private String testPropertyPath; + + protected void setUp() throws Exception { + super.setUp(); + Node n = testRootNode.addNode(nodeName1, testNodeType); + testNodePath = n.getPath(); + Value v = getJcrValue(superuser, RepositoryStub.PROP_PROP_VALUE1, RepositoryStub.PROP_PROP_TYPE1, "test"); + Property p = n.setProperty(propertyName1, v); + testPropertyPath = p.getPath(); + testRootNode.getSession().save(); + + readOnlySession = getHelper().getReadOnlySession(); + } + + protected void tearDown() throws Exception { + if (readOnlySession != null) { + readOnlySession.logout(); + } + super.tearDown(); + } + + public void testSetProperty() throws RepositoryException { + Node n = (Node) readOnlySession.getItem(testNodePath); + try { + n.setProperty(propertyName1, "otherValue"); + n.save(); + fail("A read only session must not be allowed to modify a property value"); + } catch (AccessDeniedException e) { + // success + } + } + + public void testSetValue() throws RepositoryException { + Property p = (Property) readOnlySession.getItem(testPropertyPath); + try { + p.setValue("otherValue"); + p.save(); + fail("A read only session must not be allowed to modify a property value"); + } catch (AccessDeniedException e) { + // success + } + } + + public void testDeleteNode() throws Exception { + Node n = (Node) readOnlySession.getItem(testNodePath); + try { + n.remove(); + readOnlySession.save(); + fail("A read only session must not be allowed to remove a node"); + } catch (AccessDeniedException e) { + // success + } + } + + public void testDeleteProperty() throws Exception { + Property p = (Property) readOnlySession.getItem(testPropertyPath); + try { + p.remove(); + readOnlySession.save(); + fail("A read only session must not be allowed to remove a property."); + } catch (AccessDeniedException e) { + // success + } + } + + public void testMoveNode() throws Exception { + Node n = (Node) readOnlySession.getItem(testNodePath); + String destPath = testRootNode.getPath() + "/" + nodeName2; + + try { + readOnlySession.move(n.getPath(), destPath); + readOnlySession.save(); + fail("A read only session must not be allowed to move a node"); + } catch (AccessDeniedException e) { + // expected + log.debug(e.getMessage()); + } + } + + public void testWorkspaceMoveNode() throws Exception { + Node n = (Node) readOnlySession.getItem(testNodePath); + String destPath = testRootNode.getPath() + "/" + nodeName2; + try { + readOnlySession.getWorkspace().move(n.getPath(), destPath); + fail("A read only session must not be allowed to move a node"); + } catch (AccessDeniedException e) { + // expected + log.debug(e.getMessage()); + } + } + + public void testCopyNode() throws Exception { + Node n = (Node) readOnlySession.getItem(testNodePath); + String destPath = testRootNode.getPath() + "/" + nodeName2; + try { + readOnlySession.getWorkspace().copy(n.getPath(), destPath); + fail("A read only session must not be allowed to copy a node"); + } catch (AccessDeniedException e) { + // expected + log.debug(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/TestAll.java new file mode 100644 index 00000000000..99da1678163 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/security/TestAll.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.test.api.security; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * TestAll... + */ +/** + * Test suite + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("jsr 283 security tests"); + + suite.addTestSuite(AccessControlDiscoveryTest.class); + suite.addTestSuite(AccessControlPolicyTest.class); + suite.addTestSuite(AccessControlPolicyIteratorTest.class); + suite.addTestSuite(AccessControlListTest.class); + + // tests with read only session: + suite.addTestSuite(RSessionAccessControlDiscoveryTest.class); + suite.addTestSuite(RSessionAccessControlPolicyTest.class); + suite.addTestSuite(RSessionAccessControlTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/ISO9075.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/ISO9075.java new file mode 100644 index 00000000000..b929648feab --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/ISO9075.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.util; + +// JCR-714: Class copied from jcr-commons to avoid the extra dependency + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Implements the encode and decode routines as specified for XML name to SQL + * identifier conversion in ISO 9075-14:2003.
        + * If a character c is not valid at a certain position in an XML 1.0 + * NCName it is encoded in the form: '_x' + hexValueOf(c) + '_'. + */ +public class ISO9075 { + + /** Hidden constructor. */ + private ISO9075() { } + + /** Pattern on an encoded character */ + private static final Pattern ENCODE_PATTERN = Pattern.compile("_x\\p{XDigit}{4}_"); + + /** Padding characters */ + private static final char[] PADDING = new char[] {'0', '0', '0'}; + + /** All the possible hex digits */ + private static final String HEX_DIGITS = "0123456789abcdefABCDEF"; + + /** + * Encodes name as specified in ISO 9075. + * @param name the String to encode. + * @return the encoded String or name if it does + * not need encoding. + */ + public static String encode(String name) { + // quick check for root node name + if (name.length() == 0) { + return name; + } + if (XMLChar.isValidName(name) && name.indexOf("_x") < 0) { + // already valid + return name; + } else { + // encode + StringBuffer encoded = new StringBuffer(); + for (int i = 0; i < name.length(); i++) { + if (i == 0) { + // first character of name + if (XMLChar.isNameStart(name.charAt(i))) { + if (needsEscaping(name, i)) { + // '_x' must be encoded + encode('_', encoded); + } else { + encoded.append(name.charAt(i)); + } + } else { + // not valid as first character -> encode + encode(name.charAt(i), encoded); + } + } else if (!XMLChar.isName(name.charAt(i))) { + encode(name.charAt(i), encoded); + } else { + if (needsEscaping(name, i)) { + // '_x' must be encoded + encode('_', encoded); + } else { + encoded.append(name.charAt(i)); + } + } + } + return encoded.toString(); + } + } + + /** + * Encodes path as specified in ISO 9075. Please note that + * the character '[' is not encoded but rather interpreted as + * the start of an index in a path segment. + * + * @param path the String to encode. + * @return the encoded String. + */ + public static String encodePath(String path) { + String[] names = Text.explode(path, '/', true); + StringBuffer encoded = new StringBuffer(path.length()); + for (int i = 0; i < names.length; i++) { + // detect index + String index = null; + int idx = names[i].indexOf('['); + if (idx != -1) { + index = names[i].substring(idx); + names[i] = names[i].substring(0, idx); + } + encoded.append(encode(names[i])); + if (index != null) { + encoded.append(index); + } + if (i < names.length - 1) { + encoded.append('/'); + } + } + return encoded.toString(); + } + + /** + * Decodes the name. + * @param name the String to decode. + * @return the decoded String. + */ + public static String decode(String name) { + // quick check + if (name.indexOf("_x") < 0) { + // not encoded + return name; + } + StringBuffer decoded = new StringBuffer(); + Matcher m = ENCODE_PATTERN.matcher(name); + while (m.find()) { + char ch = (char) Integer.parseInt(m.group().substring(2, 6), 16); + if (ch == '$' || ch == '\\') { + m.appendReplacement(decoded, "\\" + ch); + } else { + m.appendReplacement(decoded, Character.toString(ch)); + } + } + m.appendTail(decoded); + return decoded.toString(); + } + + //-------------------------< internal >------------------------------------- + + /** + * Encodes the character c as a String in the following form: + * "_x" + hex value of c + "_". Where the hex value has + * four digits if the character with possibly leading zeros. + *

        + * Example: ' ' (the space character) is encoded to: _x0020_ + * @param c the character to encode + * @param b the encoded character is appended to StringBuffer + * b. + */ + private static void encode(char c, StringBuffer b) { + b.append("_x"); + String hex = Integer.toHexString(c); + b.append(PADDING, 0, 4 - hex.length()); + b.append(hex); + b.append("_"); + } + + /** + * Returns true if name.charAt(location) is the underscore + * character and the following character sequence is 'xHHHH_' where H + * is a hex digit. + * @param name the name to check. + * @param location the location to look at. + * @throws ArrayIndexOutOfBoundsException if location > name.length() + */ + private static boolean needsEscaping(String name, int location) + throws ArrayIndexOutOfBoundsException { + if (name.charAt(location) == '_' && name.length() >= location + 6) { + return name.charAt(location + 1) == 'x' + && HEX_DIGITS.indexOf(name.charAt(location + 2)) != -1 + && HEX_DIGITS.indexOf(name.charAt(location + 3)) != -1 + && HEX_DIGITS.indexOf(name.charAt(location + 4)) != -1 + && HEX_DIGITS.indexOf(name.charAt(location + 5)) != -1; + } else { + return false; + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/InputStreamWrapper.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/InputStreamWrapper.java new file mode 100644 index 00000000000..15478a7946d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/InputStreamWrapper.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.test.api.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Helper class which allows to check whether the #close() method has been + * called on this stream. + */ +public class InputStreamWrapper extends FilterInputStream { + + private boolean closed; + + public InputStreamWrapper(InputStream in) { + super(in); + closed = false; + } + + public void close() throws IOException { + closed = true; + super.close(); + } + + public boolean isClosed() { + return closed; + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/TestAll.java new file mode 100644 index 00000000000..6faed21d875 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/TestAll.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.test.api.util; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the package + * javax.jcr.util. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("javax.jcr.util tests"); + + // ADD TEST CLASSES HERE: + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/Text.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/Text.java new file mode 100644 index 00000000000..7e85448488a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/Text.java @@ -0,0 +1,786 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.util; + +// JCR-714: Class copied from jcr-commons to avoid the extra dependency + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Properties; + +/** + * This Class provides some text related utilities + */ +public class Text { + + /** + * Hidden constructor. + */ + private Text() { + } + + /** + * used for the md5 + */ + public static final char[] hexTable = "0123456789abcdef".toCharArray(); + + /** + * Calculate an MD5 hash of the string given. + * + * @param data the data to encode + * @param enc the character encoding to use + * @return a hex encoded string of the md5 digested input + */ + public static String md5(String data, String enc) + throws UnsupportedEncodingException { + try { + return digest("MD5", data.getBytes(enc)); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("MD5 digest not available???"); + } + } + + /** + * Calculate an MD5 hash of the string given using 'utf-8' encoding. + * + * @param data the data to encode + * @return a hex encoded string of the md5 digested input + */ + public static String md5(String data) { + try { + return md5(data, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new InternalError("UTF8 digest not available???"); + } + } + + /** + * Digest the plain string using the given algorithm. + * + * @param algorithm The alogrithm for the digest. This algorithm must be + * supported by the MessageDigest class. + * @param data The plain text String to be digested. + * @param enc The character encoding to use + * @return The digested plain text String represented as Hex digits. + * @throws java.security.NoSuchAlgorithmException if the desired algorithm is not supported by + * the MessageDigest class. + * @throws java.io.UnsupportedEncodingException if the encoding is not supported + */ + public static String digest(String algorithm, String data, String enc) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + + return digest(algorithm, data.getBytes(enc)); + } + + /** + * Digest the plain string using the given algorithm. + * + * @param algorithm The alogrithm for the digest. This algorithm must be + * supported by the MessageDigest class. + * @param data the data to digest with the given algorithm + * @return The digested plain text String represented as Hex digits. + * @throws java.security.NoSuchAlgorithmException if the desired algorithm is not supported by + * the MessageDigest class. + */ + public static String digest(String algorithm, byte[] data) + throws NoSuchAlgorithmException { + + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] digest = md.digest(data); + StringBuffer res = new StringBuffer(digest.length * 2); + for (int i = 0; i < digest.length; i++) { + byte b = digest[i]; + res.append(hexTable[(b >> 4) & 15]); + res.append(hexTable[b & 15]); + } + return res.toString(); + } + + /** + * returns an array of strings decomposed of the original string, split at + * every occurance of 'ch'. if 2 'ch' follow each other with no intermediate + * characters, empty "" entries are avoided. + * + * @param str the string to decompose + * @param ch the character to use a split pattern + * @return an array of strings + */ + public static String[] explode(String str, int ch) { + return explode(str, ch, false); + } + + /** + * returns an array of strings decomposed of the original string, split at + * every occurance of 'ch'. + * + * @param str the string to decompose + * @param ch the character to use a split pattern + * @param respectEmpty if true, empty elements are generated + * @return an array of strings + */ + public static String[] explode(String str, int ch, boolean respectEmpty) { + if (str == null || str.length() == 0) { + return new String[0]; + } + + List strings = new ArrayList(); + int pos; + int lastpos = 0; + + // add snipples + while ((pos = str.indexOf(ch, lastpos)) >= 0) { + if (pos - lastpos > 0 || respectEmpty) { + strings.add(str.substring(lastpos, pos)); + } + lastpos = pos + 1; + } + // add rest + if (lastpos < str.length()) { + strings.add(str.substring(lastpos)); + } else if (respectEmpty && lastpos == str.length()) { + strings.add(""); + } + + // return stringarray + return strings.toArray(new String[strings.size()]); + } + + /** + * Concatenates all strings in the string array using the specified delimiter. + * @param arr + * @param delim + * @return the concatenated string + */ + public static String implode(String[] arr, String delim) { + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + buf.append(delim); + } + buf.append(arr[i]); + } + return buf.toString(); + } + + /** + * Replaces all occurences of oldString in text + * with newString. + * + * @param text + * @param oldString old substring to be replaced with newString + * @param newString new substring to replace occurences of oldString + * @return a string + */ + public static String replace(String text, String oldString, String newString) { + if (text == null || oldString == null || newString == null) { + throw new IllegalArgumentException("null argument"); + } + int pos = text.indexOf(oldString); + if (pos == -1) { + return text; + } + int lastPos = 0; + StringBuffer sb = new StringBuffer(text.length()); + while (pos != -1) { + sb.append(text.substring(lastPos, pos)); + sb.append(newString); + lastPos = pos + oldString.length(); + pos = text.indexOf(oldString, lastPos); + } + if (lastPos < text.length()) { + sb.append(text.substring(lastPos)); + } + return sb.toString(); + } + + /** + * Replaces illegal XML characters in the given string by their corresponding + * predefined entity references. + * + * @param text text to be escaped + * @return a string + */ + public static String encodeIllegalXMLCharacters(String text) { + if (text == null) { + throw new IllegalArgumentException("null argument"); + } + StringBuffer buf = null; + int length = text.length(); + int pos = 0; + for (int i = 0; i < length; i++) { + int ch = text.charAt(i); + switch (ch) { + case '<': + case '>': + case '&': + case '"': + case '\'': + if (buf == null) { + buf = new StringBuffer(); + } + if (i > 0) { + buf.append(text.substring(pos, i)); + } + pos = i + 1; + break; + default: + continue; + } + if (ch == '<') { + buf.append("<"); + } else if (ch == '>') { + buf.append(">"); + } else if (ch == '&') { + buf.append("&"); + } else if (ch == '"') { + buf.append("""); + } else if (ch == '\'') { + buf.append("'"); + } + } + if (buf == null) { + return text; + } else { + if (pos < length) { + buf.append(text.substring(pos)); + } + return buf.toString(); + } + } + + /** + * The list of characters that are not encoded by the escape() + * and unescape() METHODS. They contains the characters as + * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax': + *

        + *

        +     * unreserved  = alphanum | mark
        +     * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
        +     * 
        + */ + public static BitSet URISave; + + /** + * Same as {@link #URISave} but also contains the '/' + */ + public static BitSet URISaveEx; + + static { + URISave = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + URISave.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + URISave.set(i); + } + for (i = '0'; i <= '9'; i++) { + URISave.set(i); + } + URISave.set('-'); + URISave.set('_'); + URISave.set('.'); + URISave.set('!'); + URISave.set('~'); + URISave.set('*'); + URISave.set('\''); + URISave.set('('); + URISave.set(')'); + + URISaveEx = (BitSet) URISave.clone(); + URISaveEx.set('/'); + } + + /** + * Does an URL encoding of the string using the + * escape character. The characters that don't need encoding + * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' + * RFC 2396, but without the escape character. + * + * @param string the string to encode. + * @param escape the escape character. + * @return the escaped string + * @throws NullPointerException if string is null. + */ + public static String escape(String string, char escape) { + return escape(string, escape, false); + } + + /** + * Does an URL encoding of the string using the + * escape character. The characters that don't need encoding + * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' + * RFC 2396, but without the escape character. If isPath is + * true, additionally the slash '/' is ignored, too. + * + * @param string the string to encode. + * @param escape the escape character. + * @param isPath if true, the string is treated as path + * @return the escaped string + * @throws NullPointerException if string is null. + */ + public static String escape(String string, char escape, boolean isPath) { + try { + BitSet validChars = isPath ? URISaveEx : URISave; + byte[] bytes = string.getBytes("utf-8"); + StringBuffer out = new StringBuffer(bytes.length); + for (int i = 0; i < bytes.length; i++) { + int c = bytes[i] & 0xff; + if (validChars.get(c) && c != escape) { + out.append((char) c); + } else { + out.append(escape); + out.append(hexTable[(c >> 4) & 0x0f]); + out.append(hexTable[(c) & 0x0f]); + } + } + return out.toString(); + } catch (UnsupportedEncodingException e) { + throw new InternalError(e.toString()); + } + } + + /** + * Does a URL encoding of the string. The characters that + * don't need encoding are those defined 'unreserved' in section 2.3 of + * the 'URI generic syntax' RFC 2396. + * + * @param string the string to encode + * @return the escaped string + * @throws NullPointerException if string is null. + */ + public static String escape(String string) { + return escape(string, '%'); + } + + /** + * Does a URL encoding of the path. The characters that + * don't need encoding are those defined 'unreserved' in section 2.3 of + * the 'URI generic syntax' RFC 2396. In contrast to the + * {@link #escape(String)} method, not the entire path string is escaped, + * but every individual part (i.e. the slashes are not escaped). + * + * @param path the path to encode + * @return the escaped path + * @throws NullPointerException if path is null. + */ + public static String escapePath(String path) { + return escape(path, '%', true); + } + + /** + * Does a URL decoding of the string using the + * escape character. Please note that in opposite to the + * {@link java.net.URLDecoder} it does not transform the + into spaces. + * + * @param string the string to decode + * @param escape the escape character + * @return the decoded string + * @throws NullPointerException if string is null. + * @throws IllegalArgumentException if the 2 characters following the escape + * character do not represent a hex-number + * or if not enough characters follow an + * escape character + */ + public static String unescape(String string, char escape) { + try { + byte[] utf8 = string.getBytes("utf-8"); + + // Check whether escape occurs at invalid position + if ((utf8.length >= 1 && utf8[utf8.length - 1] == escape) || + (utf8.length >= 2 && utf8[utf8.length - 2] == escape)) { + throw new IllegalArgumentException("Premature end of escape sequence at end of input"); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(utf8.length); + for (int k = 0; k < utf8.length; k++) { + byte b = utf8[k]; + if (b == escape) { + out.write((decodeDigit(utf8[++k]) << 4) + decodeDigit(utf8[++k])); + } + else { + out.write(b); + } + } + + return new String(out.toByteArray(), "utf-8"); + } + catch (UnsupportedEncodingException e) { + throw new InternalError(e.toString()); + } + } + + /** + * Does a URL decoding of the string. Please note that in + * opposite to the {@link java.net.URLDecoder} it does not transform the + + * into spaces. + * + * @param string the string to decode + * @return the decoded string + * @throws NullPointerException if string is null. + * @throws ArrayIndexOutOfBoundsException if not enough character follow an + * escape character + * @throws IllegalArgumentException if the 2 characters following the escape + * character do not represent a hex-number. + */ + public static String unescape(String string) { + return unescape(string, '%'); + } + + /** + * Escapes all illegal JCR name characters of a string. + * The encoding is loosely modeled after URI encoding, but only encodes + * the characters it absolutely needs to in order to make the resulting + * string a valid JCR name. + * Use {@link #unescapeIllegalJcrChars(String)} for decoding. + *

        + * QName EBNF: + *

        +     * simplename ::= onecharsimplename | twocharsimplename | threeormorecharname
        +     * onecharsimplename ::= (* Any Unicode character except: '.', '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace character *)
        +     * twocharsimplename ::= '.' onecharsimplename | onecharsimplename '.' | onecharsimplename onecharsimplename
        +     * threeormorecharname ::= nonspace string nonspace
        +     * string ::= char | string char
        +     * char ::= nonspace | ' '
        +     * nonspace ::= (* Any Unicode character except: '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace character *)
        +     * 
        + * + * @param name the name to escape + * @return the escaped name + */ + public static String escapeIllegalJcrChars(String name) { + StringBuffer buffer = new StringBuffer(name.length() * 2); + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + if (ch == '%' || ch == '/' || ch == ':' || ch == '[' || ch == ']' + || ch == '*' || ch == '\'' || ch == '"' || ch == '|' + || (ch == '.' && name.length() < 3) + || (ch == ' ' && (i == 0 || i == name.length() - 1)) + || ch == '\t' || ch == '\r' || ch == '\n') { + buffer.append('%'); + buffer.append(Character.toUpperCase(Character.forDigit(ch / 16, 16))); + buffer.append(Character.toUpperCase(Character.forDigit(ch % 16, 16))); + } else { + buffer.append(ch); + } + } + return buffer.toString(); + } + + /** Escapes all illegal XPath search characters of a string. + *

        Example:
        + * A search string like 'test?' will run into a ParseException + * documented in http://issues.apache.org/jira/browse/JCR-1248 + * + * @param s the string to encode + * @return the escaped string + */ + public static String escapeIllegalXpathSearchChars(String s) { + StringBuffer sb = new StringBuffer(); + sb.append(s.substring(0, (s.length() - 1))); + char c = s.charAt(s.length() - 1); + // NOTE: keep this in sync with _ESCAPED_CHAR below! + if (c == '!' || c == '(' || c == ':' || c == '^' + || c == '[' || c == ']' || c == '\"' || c == '{' + || c == '}' || c == '?') { + sb.append('\\'); + } + sb.append(c); + return sb.toString(); + } + + /** + * Unescapes previously escaped jcr chars. + *

        + * Please note, that this does not exactly the same as the url related + * {@link #unescape(String)}, since it handles the byte-encoding + * differently. + * + * @param name the name to unescape + * @return the unescaped name + */ + public static String unescapeIllegalJcrChars(String name) { + StringBuffer buffer = new StringBuffer(name.length()); + int i = name.indexOf('%'); + while (i > -1 && i + 2 < name.length()) { + buffer.append(name.toCharArray(), 0, i); + int a = Character.digit(name.charAt(i + 1), 16); + int b = Character.digit(name.charAt(i + 2), 16); + if (a > -1 && b > -1) { + buffer.append((char) (a * 16 + b)); + name = name.substring(i + 3); + } else { + buffer.append('%'); + name = name.substring(i + 1); + } + i = name.indexOf('%'); + } + buffer.append(name); + return buffer.toString(); + } + + /** + * Returns the name part of the path. If the given path is already a name + * (i.e. contains no slashes) it is returned. + * + * @param path the path + * @return the name part or null if path is null. + */ + public static String getName(String path) { + return getName(path, '/'); + } + + /** + * Returns the name part of the path, delimited by the given delim. + * If the given path is already a name (i.e. contains no delim + * characters) it is returned. + * + * @param path the path + * @param delim the delimiter + * @return the name part or null if path is null. + */ + public static String getName(String path, char delim) { + return path == null + ? null + : path.substring(path.lastIndexOf(delim) + 1); + } + + /** + * Same as {@link #getName(String)} but adding the possibility + * to pass paths that end with a trailing '/' + * + * @see #getName(String) + */ + public static String getName(String path, boolean ignoreTrailingSlash) { + if (ignoreTrailingSlash && path != null && path.endsWith("/") && path.length() > 1) { + path = path.substring(0, path.length()-1); + } + return getName(path); + } + + /** + * Returns the namespace prefix of the given qname. If the + * prefix is missing, an empty string is returned. Please note, that this + * method does not validate the name or prefix. + *

        + * The qname has the format: qname := [prefix ':'] local; + * + * @param qname a qualified name + * @return the prefix of the name or "". + * + * @see #getLocalName(String) + * + * @throws NullPointerException if qname is null + */ + public static String getNamespacePrefix(String qname) { + int pos = qname.indexOf(':'); + return pos >=0 ? qname.substring(0, pos) : ""; + } + + /** + * Returns the local name of the given qname. Please note, that + * this method does not validate the name. + *

        + * The qname has the format: qname := [prefix ':'] local; + * + * @param qname a qualified name + * @return the localname + * + * @see #getNamespacePrefix(String) + * + * @throws NullPointerException if qname is null + */ + public static String getLocalName(String qname) { + int pos = qname.indexOf(':'); + return pos >=0 ? qname.substring(pos+1) : qname; + } + + /** + * Determines, if two paths denote hierarchical siblins. + * + * @param p1 first path + * @param p2 second path + * @return true if on same level, false otherwise + */ + public static boolean isSibling(String p1, String p2) { + int pos1 = p1.lastIndexOf('/'); + int pos2 = p2.lastIndexOf('/'); + return (pos1 == pos2 && pos1 >= 0 && p1.regionMatches(0, p2, 0, pos1)); + } + + /** + * Determines if the descendant path is hierarchical a + * descendant of path. + * + * @param path the current path + * @param descendant the potential descendant + * @return true if the descendant is a descendant; + * false otherwise. + */ + public static boolean isDescendant(String path, String descendant) { + String pattern = path.endsWith("/") ? path : path + "/"; + return !pattern.equals(descendant) && + descendant.startsWith(pattern); + } + + /** + * Determines if the descendant path is hierarchical a + * descendant of path or equal to it. + * + * @param path the path to check + * @param descendant the potential descendant + * @return true if the descendant is a descendant + * or equal; false otherwise. + */ + public static boolean isDescendantOrEqual(String path, String descendant) { + if (path.equals(descendant)) { + return true; + } else { + String pattern = path.endsWith("/") ? path : path + "/"; + return descendant.startsWith(pattern); + } + } + + /** + * Returns the nth relative parent of the path, where n=level. + *

        Example:
        + * + * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar" + * + * + * @param path the path of the page + * @param level the level of the parent + */ + public static String getRelativeParent(String path, int level) { + int idx = path.length(); + while (level > 0) { + idx = path.lastIndexOf('/', idx - 1); + if (idx < 0) { + return ""; + } + level--; + } + return (idx == 0) ? "/" : path.substring(0, idx); + } + + /** + * Same as {@link #getRelativeParent(String, int)} but adding the possibility + * to pass paths that end with a trailing '/' + * + * @see #getRelativeParent(String, int) + */ + public static String getRelativeParent(String path, int level, boolean ignoreTrailingSlash) { + if (ignoreTrailingSlash && path.endsWith("/") && path.length() > 1) { + path = path.substring(0, path.length()-1); + } + return getRelativeParent(path, level); + } + + /** + * Returns the nth absolute parent of the path, where n=level. + *

        Example:
        + * + * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar" + * + * + * @param path the path of the page + * @param level the level of the parent + */ + public static String getAbsoluteParent(String path, int level) { + int idx = 0; + int len = path.length(); + while (level >= 0 && idx < len) { + idx = path.indexOf('/', idx + 1); + if (idx < 0) { + idx = len; + } + level--; + } + return level >= 0 ? "" : path.substring(0, idx); + } + + /** + * Performs variable replacement on the given string value. + * Each ${...} sequence within the given value is replaced + * with the value of the named parser variable. If a variable is not found + * in the properties an IllegalArgumentException is thrown unless + * ignoreMissing is true. In the later case, the + * missing variable is replaced by the empty string. + * + * @param value the original value + * @param ignoreMissing if true, missing variables are replaced + * by the empty string. + * @return value after variable replacements + * @throws IllegalArgumentException if the replacement of a referenced + * variable is not found + */ + public static String replaceVariables(Properties variables, String value, + boolean ignoreMissing) + throws IllegalArgumentException { + StringBuffer result = new StringBuffer(); + + // Value: + // +--+-+--------+-+-----------------+ + // | |p|--> |q|--> | + // +--+-+--------+-+-----------------+ + int p = 0, q = value.indexOf("${"); // Find first ${ + while (q != -1) { + result.append(value.substring(p, q)); // Text before ${ + p = q; + q = value.indexOf("}", q + 2); // Find } + if (q != -1) { + String variable = value.substring(p + 2, q); + String replacement = variables.getProperty(variable); + if (replacement == null) { + if (ignoreMissing) { + replacement = ""; + } else { + throw new IllegalArgumentException( + "Replacement not found for ${" + variable + "}."); + } + } + result.append(replacement); + p = q + 1; + q = value.indexOf("${", p); // Find next ${ + } + } + result.append(value.substring(p, value.length())); // Trailing text + + return result.toString(); + } + + private static byte decodeDigit(byte b) { + if (b >= 0x30 && b <= 0x39) { + return (byte) (b - 0x30); + } + else if (b >= 0x41 && b <= 0x46) { + return (byte) (b - 0x37); + } + else if (b >= 0x61 && b <= 0x66) { + return (byte) (b - 0x57); + } + else { + throw new IllegalArgumentException("Escape sequence is not hexadecimal: " + (char)b); + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/XMLChar.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/XMLChar.java new file mode 100644 index 00000000000..c3aaca2a67a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/util/XMLChar.java @@ -0,0 +1,1027 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.util; + +// JCR-714: Class copied from jcr-commons to avoid the extra dependency + +// Note: This file is a copy from org.apache.xerces.util. +// See http://issues.apache.org/jira/browse/JCR-367 + +import java.util.Arrays; + +/** + * This class defines the basic XML character properties. The data + * in this class can be used to verify that a character is a valid + * XML character or if the character is a space, name start, or name + * character. + *

        + * A series of convenience methods are supplied to ease the burden + * of the developer. Because inlining the checks can improve per + * character performance, the tables of character properties are + * public. Using the character as an index into the CHARS + * array and applying the appropriate mask flag (e.g. + * MASK_VALID), yields the same results as calling the + * convenience methods. There is one exception: check the comments + * for the isValid method for details. + * + * @author Glenn Marcy, IBM + * @author Andy Clark, IBM + * @author Eric Ye, IBM + * @author Arnaud Le Hors, IBM + * @author Michael Glavassevich, IBM + * @author Rahul Srivastava, Sun Microsystems Inc. + * + * @version $Id: XMLChar.java 776776 2009-05-20 17:33:05Z jukka $ + */ +public class XMLChar { + + // + // Constants + // + + /** Character flags. */ + private static final byte[] CHARS = new byte[1 << 16]; + + /** Valid character mask. */ + public static final int MASK_VALID = 0x01; + + /** Space character mask. */ + public static final int MASK_SPACE = 0x02; + + /** Name start character mask. */ + public static final int MASK_NAME_START = 0x04; + + /** Name character mask. */ + public static final int MASK_NAME = 0x08; + + /** Pubid character mask. */ + public static final int MASK_PUBID = 0x10; + + /** + * Content character mask. Special characters are those that can + * be considered the start of markup, such as '<' and '&'. + * The various newline characters are considered special as well. + * All other valid XML characters can be considered content. + *

        + * This is an optimization for the inner loop of character scanning. + */ + public static final int MASK_CONTENT = 0x20; + + /** NCName start character mask. */ + public static final int MASK_NCNAME_START = 0x40; + + /** NCName character mask. */ + public static final int MASK_NCNAME = 0x80; + + // + // Static initialization + // + + static { + + // Initializing the Character Flag Array + // Code generated by: XMLCharGenerator. + + CHARS[9] = 35; + CHARS[10] = 19; + CHARS[13] = 19; + CHARS[32] = 51; + CHARS[33] = 49; + CHARS[34] = 33; + Arrays.fill(CHARS, 35, 38, (byte) 49 ); // Fill 3 of value (byte) 49 + CHARS[38] = 1; + Arrays.fill(CHARS, 39, 45, (byte) 49 ); // Fill 6 of value (byte) 49 + Arrays.fill(CHARS, 45, 47, (byte) -71 ); // Fill 2 of value (byte) -71 + CHARS[47] = 49; + Arrays.fill(CHARS, 48, 58, (byte) -71 ); // Fill 10 of value (byte) -71 + CHARS[58] = 61; + CHARS[59] = 49; + CHARS[60] = 1; + CHARS[61] = 49; + CHARS[62] = 33; + Arrays.fill(CHARS, 63, 65, (byte) 49 ); // Fill 2 of value (byte) 49 + Arrays.fill(CHARS, 65, 91, (byte) -3 ); // Fill 26 of value (byte) -3 + Arrays.fill(CHARS, 91, 93, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[93] = 1; + CHARS[94] = 33; + CHARS[95] = -3; + CHARS[96] = 33; + Arrays.fill(CHARS, 97, 123, (byte) -3 ); // Fill 26 of value (byte) -3 + Arrays.fill(CHARS, 123, 183, (byte) 33 ); // Fill 60 of value (byte) 33 + CHARS[183] = -87; + Arrays.fill(CHARS, 184, 192, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 192, 215, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[215] = 33; + Arrays.fill(CHARS, 216, 247, (byte) -19 ); // Fill 31 of value (byte) -19 + CHARS[247] = 33; + Arrays.fill(CHARS, 248, 306, (byte) -19 ); // Fill 58 of value (byte) -19 + Arrays.fill(CHARS, 306, 308, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 308, 319, (byte) -19 ); // Fill 11 of value (byte) -19 + Arrays.fill(CHARS, 319, 321, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 321, 329, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[329] = 33; + Arrays.fill(CHARS, 330, 383, (byte) -19 ); // Fill 53 of value (byte) -19 + CHARS[383] = 33; + Arrays.fill(CHARS, 384, 452, (byte) -19 ); // Fill 68 of value (byte) -19 + Arrays.fill(CHARS, 452, 461, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 461, 497, (byte) -19 ); // Fill 36 of value (byte) -19 + Arrays.fill(CHARS, 497, 500, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 500, 502, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 502, 506, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 506, 536, (byte) -19 ); // Fill 30 of value (byte) -19 + Arrays.fill(CHARS, 536, 592, (byte) 33 ); // Fill 56 of value (byte) 33 + Arrays.fill(CHARS, 592, 681, (byte) -19 ); // Fill 89 of value (byte) -19 + Arrays.fill(CHARS, 681, 699, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 699, 706, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 706, 720, (byte) 33 ); // Fill 14 of value (byte) 33 + Arrays.fill(CHARS, 720, 722, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 722, 768, (byte) 33 ); // Fill 46 of value (byte) 33 + Arrays.fill(CHARS, 768, 838, (byte) -87 ); // Fill 70 of value (byte) -87 + Arrays.fill(CHARS, 838, 864, (byte) 33 ); // Fill 26 of value (byte) 33 + Arrays.fill(CHARS, 864, 866, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 866, 902, (byte) 33 ); // Fill 36 of value (byte) 33 + CHARS[902] = -19; + CHARS[903] = -87; + Arrays.fill(CHARS, 904, 907, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[907] = 33; + CHARS[908] = -19; + CHARS[909] = 33; + Arrays.fill(CHARS, 910, 930, (byte) -19 ); // Fill 20 of value (byte) -19 + CHARS[930] = 33; + Arrays.fill(CHARS, 931, 975, (byte) -19 ); // Fill 44 of value (byte) -19 + CHARS[975] = 33; + Arrays.fill(CHARS, 976, 983, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 983, 986, (byte) 33 ); // Fill 3 of value (byte) 33 + CHARS[986] = -19; + CHARS[987] = 33; + CHARS[988] = -19; + CHARS[989] = 33; + CHARS[990] = -19; + CHARS[991] = 33; + CHARS[992] = -19; + CHARS[993] = 33; + Arrays.fill(CHARS, 994, 1012, (byte) -19 ); // Fill 18 of value (byte) -19 + Arrays.fill(CHARS, 1012, 1025, (byte) 33 ); // Fill 13 of value (byte) 33 + Arrays.fill(CHARS, 1025, 1037, (byte) -19 ); // Fill 12 of value (byte) -19 + CHARS[1037] = 33; + Arrays.fill(CHARS, 1038, 1104, (byte) -19 ); // Fill 66 of value (byte) -19 + CHARS[1104] = 33; + Arrays.fill(CHARS, 1105, 1117, (byte) -19 ); // Fill 12 of value (byte) -19 + CHARS[1117] = 33; + Arrays.fill(CHARS, 1118, 1154, (byte) -19 ); // Fill 36 of value (byte) -19 + CHARS[1154] = 33; + Arrays.fill(CHARS, 1155, 1159, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 1159, 1168, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 1168, 1221, (byte) -19 ); // Fill 53 of value (byte) -19 + Arrays.fill(CHARS, 1221, 1223, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1223, 1225, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1225, 1227, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1227, 1229, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1229, 1232, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 1232, 1260, (byte) -19 ); // Fill 28 of value (byte) -19 + Arrays.fill(CHARS, 1260, 1262, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1262, 1270, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 1270, 1272, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1272, 1274, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1274, 1329, (byte) 33 ); // Fill 55 of value (byte) 33 + Arrays.fill(CHARS, 1329, 1367, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 1367, 1369, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[1369] = -19; + Arrays.fill(CHARS, 1370, 1377, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 1377, 1415, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 1415, 1425, (byte) 33 ); // Fill 10 of value (byte) 33 + Arrays.fill(CHARS, 1425, 1442, (byte) -87 ); // Fill 17 of value (byte) -87 + CHARS[1442] = 33; + Arrays.fill(CHARS, 1443, 1466, (byte) -87 ); // Fill 23 of value (byte) -87 + CHARS[1466] = 33; + Arrays.fill(CHARS, 1467, 1470, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[1470] = 33; + CHARS[1471] = -87; + CHARS[1472] = 33; + Arrays.fill(CHARS, 1473, 1475, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[1475] = 33; + CHARS[1476] = -87; + Arrays.fill(CHARS, 1477, 1488, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 1488, 1515, (byte) -19 ); // Fill 27 of value (byte) -19 + Arrays.fill(CHARS, 1515, 1520, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 1520, 1523, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 1523, 1569, (byte) 33 ); // Fill 46 of value (byte) 33 + Arrays.fill(CHARS, 1569, 1595, (byte) -19 ); // Fill 26 of value (byte) -19 + Arrays.fill(CHARS, 1595, 1600, (byte) 33 ); // Fill 5 of value (byte) 33 + CHARS[1600] = -87; + Arrays.fill(CHARS, 1601, 1611, (byte) -19 ); // Fill 10 of value (byte) -19 + Arrays.fill(CHARS, 1611, 1619, (byte) -87 ); // Fill 8 of value (byte) -87 + Arrays.fill(CHARS, 1619, 1632, (byte) 33 ); // Fill 13 of value (byte) 33 + Arrays.fill(CHARS, 1632, 1642, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 1642, 1648, (byte) 33 ); // Fill 6 of value (byte) 33 + CHARS[1648] = -87; + Arrays.fill(CHARS, 1649, 1720, (byte) -19 ); // Fill 71 of value (byte) -19 + Arrays.fill(CHARS, 1720, 1722, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1722, 1727, (byte) -19 ); // Fill 5 of value (byte) -19 + CHARS[1727] = 33; + Arrays.fill(CHARS, 1728, 1743, (byte) -19 ); // Fill 15 of value (byte) -19 + CHARS[1743] = 33; + Arrays.fill(CHARS, 1744, 1748, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[1748] = 33; + CHARS[1749] = -19; + Arrays.fill(CHARS, 1750, 1765, (byte) -87 ); // Fill 15 of value (byte) -87 + Arrays.fill(CHARS, 1765, 1767, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 1767, 1769, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[1769] = 33; + Arrays.fill(CHARS, 1770, 1774, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 1774, 1776, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 1776, 1786, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 1786, 2305, (byte) 33 ); // Fill 519 of value (byte) 33 + Arrays.fill(CHARS, 2305, 2308, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2308] = 33; + Arrays.fill(CHARS, 2309, 2362, (byte) -19 ); // Fill 53 of value (byte) -19 + Arrays.fill(CHARS, 2362, 2364, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2364] = -87; + CHARS[2365] = -19; + Arrays.fill(CHARS, 2366, 2382, (byte) -87 ); // Fill 16 of value (byte) -87 + Arrays.fill(CHARS, 2382, 2385, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2385, 2389, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 2389, 2392, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2392, 2402, (byte) -19 ); // Fill 10 of value (byte) -19 + Arrays.fill(CHARS, 2402, 2404, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2404, 2406, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2406, 2416, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2416, 2433, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 2433, 2436, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2436] = 33; + Arrays.fill(CHARS, 2437, 2445, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 2445, 2447, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2447, 2449, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2449, 2451, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2451, 2473, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2473] = 33; + Arrays.fill(CHARS, 2474, 2481, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2481] = 33; + CHARS[2482] = -19; + Arrays.fill(CHARS, 2483, 2486, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2486, 2490, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2490, 2492, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2492] = -87; + CHARS[2493] = 33; + Arrays.fill(CHARS, 2494, 2501, (byte) -87 ); // Fill 7 of value (byte) -87 + Arrays.fill(CHARS, 2501, 2503, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2503, 2505, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2505, 2507, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2507, 2510, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2510, 2519, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[2519] = -87; + Arrays.fill(CHARS, 2520, 2524, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2524, 2526, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2526] = 33; + Arrays.fill(CHARS, 2527, 2530, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2530, 2532, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2532, 2534, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2534, 2544, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2544, 2546, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2546, 2562, (byte) 33 ); // Fill 16 of value (byte) 33 + CHARS[2562] = -87; + Arrays.fill(CHARS, 2563, 2565, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2565, 2571, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 2571, 2575, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2575, 2577, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2577, 2579, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2579, 2601, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2601] = 33; + Arrays.fill(CHARS, 2602, 2609, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2609] = 33; + Arrays.fill(CHARS, 2610, 2612, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2612] = 33; + Arrays.fill(CHARS, 2613, 2615, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2615] = 33; + Arrays.fill(CHARS, 2616, 2618, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2618, 2620, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2620] = -87; + CHARS[2621] = 33; + Arrays.fill(CHARS, 2622, 2627, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 2627, 2631, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2631, 2633, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2633, 2635, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2635, 2638, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2638, 2649, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 2649, 2653, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[2653] = 33; + CHARS[2654] = -19; + Arrays.fill(CHARS, 2655, 2662, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 2662, 2674, (byte) -87 ); // Fill 12 of value (byte) -87 + Arrays.fill(CHARS, 2674, 2677, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2677, 2689, (byte) 33 ); // Fill 12 of value (byte) 33 + Arrays.fill(CHARS, 2689, 2692, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2692] = 33; + Arrays.fill(CHARS, 2693, 2700, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2700] = 33; + CHARS[2701] = -19; + CHARS[2702] = 33; + Arrays.fill(CHARS, 2703, 2706, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[2706] = 33; + Arrays.fill(CHARS, 2707, 2729, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2729] = 33; + Arrays.fill(CHARS, 2730, 2737, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2737] = 33; + Arrays.fill(CHARS, 2738, 2740, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2740] = 33; + Arrays.fill(CHARS, 2741, 2746, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 2746, 2748, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2748] = -87; + CHARS[2749] = -19; + Arrays.fill(CHARS, 2750, 2758, (byte) -87 ); // Fill 8 of value (byte) -87 + CHARS[2758] = 33; + Arrays.fill(CHARS, 2759, 2762, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2762] = 33; + Arrays.fill(CHARS, 2763, 2766, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2766, 2784, (byte) 33 ); // Fill 18 of value (byte) 33 + CHARS[2784] = -19; + Arrays.fill(CHARS, 2785, 2790, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 2790, 2800, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2800, 2817, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 2817, 2820, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[2820] = 33; + Arrays.fill(CHARS, 2821, 2829, (byte) -19 ); // Fill 8 of value (byte) -19 + Arrays.fill(CHARS, 2829, 2831, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2831, 2833, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2833, 2835, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2835, 2857, (byte) -19 ); // Fill 22 of value (byte) -19 + CHARS[2857] = 33; + Arrays.fill(CHARS, 2858, 2865, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[2865] = 33; + Arrays.fill(CHARS, 2866, 2868, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2868, 2870, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2870, 2874, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2874, 2876, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[2876] = -87; + CHARS[2877] = -19; + Arrays.fill(CHARS, 2878, 2884, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 2884, 2887, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2887, 2889, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2889, 2891, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 2891, 2894, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 2894, 2902, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 2902, 2904, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 2904, 2908, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2908, 2910, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2910] = 33; + Arrays.fill(CHARS, 2911, 2914, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2914, 2918, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 2918, 2928, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 2928, 2946, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 2946, 2948, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[2948] = 33; + Arrays.fill(CHARS, 2949, 2955, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 2955, 2958, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2958, 2961, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[2961] = 33; + Arrays.fill(CHARS, 2962, 2966, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 2966, 2969, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2969, 2971, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[2971] = 33; + CHARS[2972] = -19; + CHARS[2973] = 33; + Arrays.fill(CHARS, 2974, 2976, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2976, 2979, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2979, 2981, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 2981, 2984, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2984, 2987, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 2987, 2990, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 2990, 2998, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[2998] = 33; + Arrays.fill(CHARS, 2999, 3002, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 3002, 3006, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3006, 3011, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 3011, 3014, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 3014, 3017, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3017] = 33; + Arrays.fill(CHARS, 3018, 3022, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3022, 3031, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[3031] = -87; + Arrays.fill(CHARS, 3032, 3047, (byte) 33 ); // Fill 15 of value (byte) 33 + Arrays.fill(CHARS, 3047, 3056, (byte) -87 ); // Fill 9 of value (byte) -87 + Arrays.fill(CHARS, 3056, 3073, (byte) 33 ); // Fill 17 of value (byte) 33 + Arrays.fill(CHARS, 3073, 3076, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3076] = 33; + Arrays.fill(CHARS, 3077, 3085, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3085] = 33; + Arrays.fill(CHARS, 3086, 3089, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3089] = 33; + Arrays.fill(CHARS, 3090, 3113, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3113] = 33; + Arrays.fill(CHARS, 3114, 3124, (byte) -19 ); // Fill 10 of value (byte) -19 + CHARS[3124] = 33; + Arrays.fill(CHARS, 3125, 3130, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 3130, 3134, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3134, 3141, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[3141] = 33; + Arrays.fill(CHARS, 3142, 3145, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3145] = 33; + Arrays.fill(CHARS, 3146, 3150, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3150, 3157, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3157, 3159, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3159, 3168, (byte) 33 ); // Fill 9 of value (byte) 33 + Arrays.fill(CHARS, 3168, 3170, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3170, 3174, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3174, 3184, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3184, 3202, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 3202, 3204, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3204] = 33; + Arrays.fill(CHARS, 3205, 3213, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3213] = 33; + Arrays.fill(CHARS, 3214, 3217, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3217] = 33; + Arrays.fill(CHARS, 3218, 3241, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3241] = 33; + Arrays.fill(CHARS, 3242, 3252, (byte) -19 ); // Fill 10 of value (byte) -19 + CHARS[3252] = 33; + Arrays.fill(CHARS, 3253, 3258, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 3258, 3262, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3262, 3269, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[3269] = 33; + Arrays.fill(CHARS, 3270, 3273, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3273] = 33; + Arrays.fill(CHARS, 3274, 3278, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3278, 3285, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3285, 3287, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3287, 3294, (byte) 33 ); // Fill 7 of value (byte) 33 + CHARS[3294] = -19; + CHARS[3295] = 33; + Arrays.fill(CHARS, 3296, 3298, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3298, 3302, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3302, 3312, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3312, 3330, (byte) 33 ); // Fill 18 of value (byte) 33 + Arrays.fill(CHARS, 3330, 3332, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3332] = 33; + Arrays.fill(CHARS, 3333, 3341, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3341] = 33; + Arrays.fill(CHARS, 3342, 3345, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3345] = 33; + Arrays.fill(CHARS, 3346, 3369, (byte) -19 ); // Fill 23 of value (byte) -19 + CHARS[3369] = 33; + Arrays.fill(CHARS, 3370, 3386, (byte) -19 ); // Fill 16 of value (byte) -19 + Arrays.fill(CHARS, 3386, 3390, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3390, 3396, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3396, 3398, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3398, 3401, (byte) -87 ); // Fill 3 of value (byte) -87 + CHARS[3401] = 33; + Arrays.fill(CHARS, 3402, 3406, (byte) -87 ); // Fill 4 of value (byte) -87 + Arrays.fill(CHARS, 3406, 3415, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[3415] = -87; + Arrays.fill(CHARS, 3416, 3424, (byte) 33 ); // Fill 8 of value (byte) 33 + Arrays.fill(CHARS, 3424, 3426, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3426, 3430, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3430, 3440, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3440, 3585, (byte) 33 ); // Fill 145 of value (byte) 33 + Arrays.fill(CHARS, 3585, 3631, (byte) -19 ); // Fill 46 of value (byte) -19 + CHARS[3631] = 33; + CHARS[3632] = -19; + CHARS[3633] = -87; + Arrays.fill(CHARS, 3634, 3636, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3636, 3643, (byte) -87 ); // Fill 7 of value (byte) -87 + Arrays.fill(CHARS, 3643, 3648, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 3648, 3654, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 3654, 3663, (byte) -87 ); // Fill 9 of value (byte) -87 + CHARS[3663] = 33; + Arrays.fill(CHARS, 3664, 3674, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3674, 3713, (byte) 33 ); // Fill 39 of value (byte) 33 + Arrays.fill(CHARS, 3713, 3715, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3715] = 33; + CHARS[3716] = -19; + Arrays.fill(CHARS, 3717, 3719, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3719, 3721, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3721] = 33; + CHARS[3722] = -19; + Arrays.fill(CHARS, 3723, 3725, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[3725] = -19; + Arrays.fill(CHARS, 3726, 3732, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 3732, 3736, (byte) -19 ); // Fill 4 of value (byte) -19 + CHARS[3736] = 33; + Arrays.fill(CHARS, 3737, 3744, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[3744] = 33; + Arrays.fill(CHARS, 3745, 3748, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[3748] = 33; + CHARS[3749] = -19; + CHARS[3750] = 33; + CHARS[3751] = -19; + Arrays.fill(CHARS, 3752, 3754, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3754, 3756, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3756] = 33; + Arrays.fill(CHARS, 3757, 3759, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[3759] = 33; + CHARS[3760] = -19; + CHARS[3761] = -87; + Arrays.fill(CHARS, 3762, 3764, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 3764, 3770, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[3770] = 33; + Arrays.fill(CHARS, 3771, 3773, (byte) -87 ); // Fill 2 of value (byte) -87 + CHARS[3773] = -19; + Arrays.fill(CHARS, 3774, 3776, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3776, 3781, (byte) -19 ); // Fill 5 of value (byte) -19 + CHARS[3781] = 33; + CHARS[3782] = -87; + CHARS[3783] = 33; + Arrays.fill(CHARS, 3784, 3790, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3790, 3792, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 3792, 3802, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3802, 3864, (byte) 33 ); // Fill 62 of value (byte) 33 + Arrays.fill(CHARS, 3864, 3866, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3866, 3872, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 3872, 3882, (byte) -87 ); // Fill 10 of value (byte) -87 + Arrays.fill(CHARS, 3882, 3893, (byte) 33 ); // Fill 11 of value (byte) 33 + CHARS[3893] = -87; + CHARS[3894] = 33; + CHARS[3895] = -87; + CHARS[3896] = 33; + CHARS[3897] = -87; + Arrays.fill(CHARS, 3898, 3902, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3902, 3904, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 3904, 3912, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[3912] = 33; + Arrays.fill(CHARS, 3913, 3946, (byte) -19 ); // Fill 33 of value (byte) -19 + Arrays.fill(CHARS, 3946, 3953, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 3953, 3973, (byte) -87 ); // Fill 20 of value (byte) -87 + CHARS[3973] = 33; + Arrays.fill(CHARS, 3974, 3980, (byte) -87 ); // Fill 6 of value (byte) -87 + Arrays.fill(CHARS, 3980, 3984, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 3984, 3990, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[3990] = 33; + CHARS[3991] = -87; + CHARS[3992] = 33; + Arrays.fill(CHARS, 3993, 4014, (byte) -87 ); // Fill 21 of value (byte) -87 + Arrays.fill(CHARS, 4014, 4017, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4017, 4024, (byte) -87 ); // Fill 7 of value (byte) -87 + CHARS[4024] = 33; + CHARS[4025] = -87; + Arrays.fill(CHARS, 4026, 4256, (byte) 33 ); // Fill 230 of value (byte) 33 + Arrays.fill(CHARS, 4256, 4294, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 4294, 4304, (byte) 33 ); // Fill 10 of value (byte) 33 + Arrays.fill(CHARS, 4304, 4343, (byte) -19 ); // Fill 39 of value (byte) -19 + Arrays.fill(CHARS, 4343, 4352, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[4352] = -19; + CHARS[4353] = 33; + Arrays.fill(CHARS, 4354, 4356, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4356] = 33; + Arrays.fill(CHARS, 4357, 4360, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[4360] = 33; + CHARS[4361] = -19; + CHARS[4362] = 33; + Arrays.fill(CHARS, 4363, 4365, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4365] = 33; + Arrays.fill(CHARS, 4366, 4371, (byte) -19 ); // Fill 5 of value (byte) -19 + Arrays.fill(CHARS, 4371, 4412, (byte) 33 ); // Fill 41 of value (byte) 33 + CHARS[4412] = -19; + CHARS[4413] = 33; + CHARS[4414] = -19; + CHARS[4415] = 33; + CHARS[4416] = -19; + Arrays.fill(CHARS, 4417, 4428, (byte) 33 ); // Fill 11 of value (byte) 33 + CHARS[4428] = -19; + CHARS[4429] = 33; + CHARS[4430] = -19; + CHARS[4431] = 33; + CHARS[4432] = -19; + Arrays.fill(CHARS, 4433, 4436, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4436, 4438, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4438, 4441, (byte) 33 ); // Fill 3 of value (byte) 33 + CHARS[4441] = -19; + Arrays.fill(CHARS, 4442, 4447, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 4447, 4450, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[4450] = 33; + CHARS[4451] = -19; + CHARS[4452] = 33; + CHARS[4453] = -19; + CHARS[4454] = 33; + CHARS[4455] = -19; + CHARS[4456] = 33; + CHARS[4457] = -19; + Arrays.fill(CHARS, 4458, 4461, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4461, 4463, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4463, 4466, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 4466, 4468, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4468] = 33; + CHARS[4469] = -19; + Arrays.fill(CHARS, 4470, 4510, (byte) 33 ); // Fill 40 of value (byte) 33 + CHARS[4510] = -19; + Arrays.fill(CHARS, 4511, 4520, (byte) 33 ); // Fill 9 of value (byte) 33 + CHARS[4520] = -19; + Arrays.fill(CHARS, 4521, 4523, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[4523] = -19; + Arrays.fill(CHARS, 4524, 4526, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 4526, 4528, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 4528, 4535, (byte) 33 ); // Fill 7 of value (byte) 33 + Arrays.fill(CHARS, 4535, 4537, (byte) -19 ); // Fill 2 of value (byte) -19 + CHARS[4537] = 33; + CHARS[4538] = -19; + CHARS[4539] = 33; + Arrays.fill(CHARS, 4540, 4547, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 4547, 4587, (byte) 33 ); // Fill 40 of value (byte) 33 + CHARS[4587] = -19; + Arrays.fill(CHARS, 4588, 4592, (byte) 33 ); // Fill 4 of value (byte) 33 + CHARS[4592] = -19; + Arrays.fill(CHARS, 4593, 4601, (byte) 33 ); // Fill 8 of value (byte) 33 + CHARS[4601] = -19; + Arrays.fill(CHARS, 4602, 7680, (byte) 33 ); // Fill 3078 of value (byte) 33 + Arrays.fill(CHARS, 7680, 7836, (byte) -19 ); // Fill 156 of value (byte) -19 + Arrays.fill(CHARS, 7836, 7840, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 7840, 7930, (byte) -19 ); // Fill 90 of value (byte) -19 + Arrays.fill(CHARS, 7930, 7936, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 7936, 7958, (byte) -19 ); // Fill 22 of value (byte) -19 + Arrays.fill(CHARS, 7958, 7960, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 7960, 7966, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 7966, 7968, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 7968, 8006, (byte) -19 ); // Fill 38 of value (byte) -19 + Arrays.fill(CHARS, 8006, 8008, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8008, 8014, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 8014, 8016, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8016, 8024, (byte) -19 ); // Fill 8 of value (byte) -19 + CHARS[8024] = 33; + CHARS[8025] = -19; + CHARS[8026] = 33; + CHARS[8027] = -19; + CHARS[8028] = 33; + CHARS[8029] = -19; + CHARS[8030] = 33; + Arrays.fill(CHARS, 8031, 8062, (byte) -19 ); // Fill 31 of value (byte) -19 + Arrays.fill(CHARS, 8062, 8064, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8064, 8117, (byte) -19 ); // Fill 53 of value (byte) -19 + CHARS[8117] = 33; + Arrays.fill(CHARS, 8118, 8125, (byte) -19 ); // Fill 7 of value (byte) -19 + CHARS[8125] = 33; + CHARS[8126] = -19; + Arrays.fill(CHARS, 8127, 8130, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8130, 8133, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[8133] = 33; + Arrays.fill(CHARS, 8134, 8141, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 8141, 8144, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8144, 8148, (byte) -19 ); // Fill 4 of value (byte) -19 + Arrays.fill(CHARS, 8148, 8150, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 8150, 8156, (byte) -19 ); // Fill 6 of value (byte) -19 + Arrays.fill(CHARS, 8156, 8160, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 8160, 8173, (byte) -19 ); // Fill 13 of value (byte) -19 + Arrays.fill(CHARS, 8173, 8178, (byte) 33 ); // Fill 5 of value (byte) 33 + Arrays.fill(CHARS, 8178, 8181, (byte) -19 ); // Fill 3 of value (byte) -19 + CHARS[8181] = 33; + Arrays.fill(CHARS, 8182, 8189, (byte) -19 ); // Fill 7 of value (byte) -19 + Arrays.fill(CHARS, 8189, 8400, (byte) 33 ); // Fill 211 of value (byte) 33 + Arrays.fill(CHARS, 8400, 8413, (byte) -87 ); // Fill 13 of value (byte) -87 + Arrays.fill(CHARS, 8413, 8417, (byte) 33 ); // Fill 4 of value (byte) 33 + CHARS[8417] = -87; + Arrays.fill(CHARS, 8418, 8486, (byte) 33 ); // Fill 68 of value (byte) 33 + CHARS[8486] = -19; + Arrays.fill(CHARS, 8487, 8490, (byte) 33 ); // Fill 3 of value (byte) 33 + Arrays.fill(CHARS, 8490, 8492, (byte) -19 ); // Fill 2 of value (byte) -19 + Arrays.fill(CHARS, 8492, 8494, (byte) 33 ); // Fill 2 of value (byte) 33 + CHARS[8494] = -19; + Arrays.fill(CHARS, 8495, 8576, (byte) 33 ); // Fill 81 of value (byte) 33 + Arrays.fill(CHARS, 8576, 8579, (byte) -19 ); // Fill 3 of value (byte) -19 + Arrays.fill(CHARS, 8579, 12293, (byte) 33 ); // Fill 3714 of value (byte) 33 + CHARS[12293] = -87; + CHARS[12294] = 33; + CHARS[12295] = -19; + Arrays.fill(CHARS, 12296, 12321, (byte) 33 ); // Fill 25 of value (byte) 33 + Arrays.fill(CHARS, 12321, 12330, (byte) -19 ); // Fill 9 of value (byte) -19 + Arrays.fill(CHARS, 12330, 12336, (byte) -87 ); // Fill 6 of value (byte) -87 + CHARS[12336] = 33; + Arrays.fill(CHARS, 12337, 12342, (byte) -87 ); // Fill 5 of value (byte) -87 + Arrays.fill(CHARS, 12342, 12353, (byte) 33 ); // Fill 11 of value (byte) 33 + Arrays.fill(CHARS, 12353, 12437, (byte) -19 ); // Fill 84 of value (byte) -19 + Arrays.fill(CHARS, 12437, 12441, (byte) 33 ); // Fill 4 of value (byte) 33 + Arrays.fill(CHARS, 12441, 12443, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 12443, 12445, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 12445, 12447, (byte) -87 ); // Fill 2 of value (byte) -87 + Arrays.fill(CHARS, 12447, 12449, (byte) 33 ); // Fill 2 of value (byte) 33 + Arrays.fill(CHARS, 12449, 12539, (byte) -19 ); // Fill 90 of value (byte) -19 + CHARS[12539] = 33; + Arrays.fill(CHARS, 12540, 12543, (byte) -87 ); // Fill 3 of value (byte) -87 + Arrays.fill(CHARS, 12543, 12549, (byte) 33 ); // Fill 6 of value (byte) 33 + Arrays.fill(CHARS, 12549, 12589, (byte) -19 ); // Fill 40 of value (byte) -19 + Arrays.fill(CHARS, 12589, 19968, (byte) 33 ); // Fill 7379 of value (byte) 33 + Arrays.fill(CHARS, 19968, 40870, (byte) -19 ); // Fill 20902 of value (byte) -19 + Arrays.fill(CHARS, 40870, 44032, (byte) 33 ); // Fill 3162 of value (byte) 33 + Arrays.fill(CHARS, 44032, 55204, (byte) -19 ); // Fill 11172 of value (byte) -19 + Arrays.fill(CHARS, 55204, 55296, (byte) 33 ); // Fill 92 of value (byte) 33 + Arrays.fill(CHARS, 57344, 65534, (byte) 33 ); // Fill 8190 of value (byte) 33 + + } // () + + // + // Public static methods + // + + /** + * Returns true if the specified character is a supplemental character. + * + * @param c The character to check. + */ + public static boolean isSupplemental(int c) { + return (c >= 0x10000 && c <= 0x10FFFF); + } + + /** + * Returns true the supplemental character corresponding to the given + * surrogates. + * + * @param h The high surrogate. + * @param l The low surrogate. + */ + public static int supplemental(char h, char l) { + return (h - 0xD800) * 0x400 + (l - 0xDC00) + 0x10000; + } + + /** + * Returns the high surrogate of a supplemental character + * + * @param c The supplemental character to "split". + */ + public static char highSurrogate(int c) { + return (char) (((c - 0x00010000) >> 10) + 0xD800); + } + + /** + * Returns the low surrogate of a supplemental character + * + * @param c The supplemental character to "split". + */ + public static char lowSurrogate(int c) { + return (char) (((c - 0x00010000) & 0x3FF) + 0xDC00); + } + + /** + * Returns whether the given character is a high surrogate + * + * @param c The character to check. + */ + public static boolean isHighSurrogate(int c) { + return (0xD800 <= c && c <= 0xDBFF); + } + + /** + * Returns whether the given character is a low surrogate + * + * @param c The character to check. + */ + public static boolean isLowSurrogate(int c) { + return (0xDC00 <= c && c <= 0xDFFF); + } + + + /** + * Returns true if the specified character is valid. This method + * also checks the surrogate character range from 0x10000 to 0x10FFFF. + *

        + * If the program chooses to apply the mask directly to the + * CHARS array, then they are responsible for checking + * the surrogate character range. + * + * @param c The character to check. + */ + public static boolean isValid(int c) { + return (c < 0x10000 && (CHARS[c] & MASK_VALID) != 0) || + (0x10000 <= c && c <= 0x10FFFF); + } // isValid(int):boolean + + /** + * Returns true if the specified character is invalid. + * + * @param c The character to check. + */ + public static boolean isInvalid(int c) { + return !isValid(c); + } // isInvalid(int):boolean + + /** + * Returns true if the specified character can be considered content. + * + * @param c The character to check. + */ + public static boolean isContent(int c) { + return (c < 0x10000 && (CHARS[c] & MASK_CONTENT) != 0) || + (0x10000 <= c && c <= 0x10FFFF); + } // isContent(int):boolean + + /** + * Returns true if the specified character can be considered markup. + * Markup characters include '<', '&', and '%'. + * + * @param c The character to check. + */ + public static boolean isMarkup(int c) { + return c == '<' || c == '&' || c == '%'; + } // isMarkup(int):boolean + + /** + * Returns true if the specified character is a space character + * as defined by production [3] in the XML 1.0 specification. + * + * @param c The character to check. + */ + public static boolean isSpace(int c) { + return c <= 0x20 && (CHARS[c] & MASK_SPACE) != 0; + } // isSpace(int):boolean + + /** + * Returns true if the specified character is a valid name start + * character as defined by production [5] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isNameStart(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NAME_START) != 0; + } // isNameStart(int):boolean + + /** + * Returns true if the specified character is a valid name + * character as defined by production [4] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isName(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NAME) != 0; + } // isName(int):boolean + + /** + * Returns true if the specified character is a valid NCName start + * character as defined by production [4] in Namespaces in XML + * recommendation. + * + * @param c The character to check. + */ + public static boolean isNCNameStart(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NCNAME_START) != 0; + } // isNCNameStart(int):boolean + + /** + * Returns true if the specified character is a valid NCName + * character as defined by production [5] in Namespaces in XML + * recommendation. + * + * @param c The character to check. + */ + public static boolean isNCName(int c) { + return c < 0x10000 && (CHARS[c] & MASK_NCNAME) != 0; + } // isNCName(int):boolean + + /** + * Returns true if the specified character is a valid Pubid + * character as defined by production [13] in the XML 1.0 + * specification. + * + * @param c The character to check. + */ + public static boolean isPubid(int c) { + return c < 0x10000 && (CHARS[c] & MASK_PUBID) != 0; + } // isPubid(int):boolean + + /* + * [5] Name ::= (Letter | '_' | ':') (NameChar)* + */ + /** + * Check to see if a string is a valid Name according to [5] + * in the XML 1.0 Recommendation + * + * @param name string to check + * @return true if name is a valid Name + */ + public static boolean isValidName(String name) { + if (name.length() == 0) + return false; + char ch = name.charAt(0); + if( isNameStart(ch) == false) + return false; + for (int i = 1; i < name.length(); i++ ) { + ch = name.charAt(i); + if( isName( ch ) == false ){ + return false; + } + } + return true; + } // isValidName(String):boolean + + + /* + * from the namespace rec + * [4] NCName ::= (Letter | '_') (NCNameChar)* + */ + /** + * Check to see if a string is a valid NCName according to [4] + * from the XML Namespaces 1.0 Recommendation + * + * @param ncName string to check + * @return true if name is a valid NCName + */ + public static boolean isValidNCName(String ncName) { + if (ncName.length() == 0) + return false; + char ch = ncName.charAt(0); + if( isNCNameStart(ch) == false) + return false; + for (int i = 1; i < ncName.length(); i++ ) { + ch = ncName.charAt(i); + if( isNCName( ch ) == false ){ + return false; + } + } + return true; + } // isValidNCName(String):boolean + + /* + * [7] Nmtoken ::= (NameChar)+ + */ + /** + * Check to see if a string is a valid Nmtoken according to [7] + * in the XML 1.0 Recommendation + * + * @param nmtoken string to check + * @return true if nmtoken is a valid Nmtoken + */ + public static boolean isValidNmtoken(String nmtoken) { + if (nmtoken.length() == 0) + return false; + for (int i = 0; i < nmtoken.length(); i++ ) { + char ch = nmtoken.charAt(i); + if( ! isName( ch ) ){ + return false; + } + } + return true; + } // isValidName(String):boolean + + + + + + // encodings + + /** + * Returns true if the encoding name is a valid IANA encoding. + * This method does not verify that there is a decoder available + * for this encoding, only that the characters are valid for an + * IANA encoding name. + * + * @param ianaEncoding The IANA encoding name. + */ + public static boolean isValidIANAEncoding(String ianaEncoding) { + if (ianaEncoding != null) { + int length = ianaEncoding.length(); + if (length > 0) { + char c = ianaEncoding.charAt(0); + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + for (int i = 1; i < length; i++) { + c = ianaEncoding.charAt(i); + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && + (c < '0' || c > '9') && c != '.' && c != '_' && + c != '-') { + return false; + } + } + return true; + } + } + } + return false; + } // isValidIANAEncoding(String):boolean + + /** + * Returns true if the encoding name is a valid Java encoding. + * This method does not verify that there is a decoder available + * for this encoding, only that the characters are valid for an + * Java encoding name. + * + * @param javaEncoding The Java encoding name. + */ + public static boolean isValidJavaEncoding(String javaEncoding) { + if (javaEncoding != null) { + int length = javaEncoding.length(); + if (length > 0) { + for (int i = 1; i < length; i++) { + char c = javaEncoding.charAt(i); + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && + (c < '0' || c > '9') && c != '.' && c != '_' && + c != '-') { + return false; + } + } + return true; + } + } + return false; + } // isValidIANAEncoding(String):boolean + + +} // class XMLChar diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractMergeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractMergeTest.java new file mode 100644 index 00000000000..b51c44413f4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractMergeTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AbstractMergeTest is the abstract base class for all merge + * related test classes. + */ +public abstract class AbstractMergeTest extends AbstractJCRTest { + + private static final String PROP_VERSIONABLE_NODE_TYPE = "versionableNodeType"; + + // global variable used in different tests + + protected String versionableNodeType; + protected String nonVersionableNodeType; + + /** + * The superuser session for the second workspace + */ + protected Session superuserW2; + + /** + * The default workspace + */ + protected Workspace workspace; + + /** + * The second workspace + */ + protected Workspace workspaceW2; + + /** + * The test root node in second workspace to test + */ + protected Node testRootNodeW2; + + /** + * The modified string to check + */ + protected static final String CHANGED_STRING = "changed"; + + /** + * Initialising used variables coming from the properties file.
        Setup + * some nodes on the 2 workspaces.
        + */ + protected void setUp() throws Exception { + super.setUp(); + + super.checkSupportedOption(Repository.OPTION_VERSIONING_SUPPORTED); + + NodeTypeManager ntm = superuser.getWorkspace().getNodeTypeManager(); + + // versionable node type + versionableNodeType = getProperty(PROP_VERSIONABLE_NODE_TYPE); + if (versionableNodeType == null) { + fail("Property '" + PROP_VERSIONABLE_NODE_TYPE + "' is not defined."); + } + + NodeType vNt = ntm.getNodeType(versionableNodeType); + if (!vNt.isNodeType(mixVersionable)) { + fail("Property '" + PROP_VERSIONABLE_NODE_TYPE + "' does not define a versionable nodetype."); + } + + // non versionable node type + // test node type defines always a non versionable node type + nonVersionableNodeType = testNodeType; + if (nonVersionableNodeType == null) { + fail("Property '" + testNodeType + "' is not defined."); + } + + NodeType nvNt = ntm.getNodeType(nonVersionableNodeType); + if (nvNt.isNodeType(mixVersionable)) { + fail("Property '" + testNodeType + "' does define a versionable nodetype."); + } + + // initialise a new session on second workspace as superuser + superuserW2 = getHelper().getSuperuserSession(workspaceName); + + workspace = superuser.getWorkspace(); + workspaceW2 = superuserW2.getWorkspace(); + + // get/create test root node on second workspace + testRootNodeW2 = cleanUpTestRoot(superuserW2); + + // initialize test nodes + initNodes(); + } + + /** + * Tidy the testRootNodes of both workspaces, then logout sessions + * + * @throws Exception + */ + protected void tearDown() throws Exception { + // remove all test nodes in second workspace + if (superuserW2 != null) { + try { + if (!isReadOnly) { + cleanUpTestRoot(superuserW2); + } + } finally { + superuserW2.logout(); + superuserW2 = null; + } + } + workspace = null; + workspaceW2 = null; + testRootNodeW2 = null; + + super.tearDown(); + } + + // initialize nodes + abstract void initNodes() throws RepositoryException, NotExecutableException; +} \ No newline at end of file diff --git a/src/test/org/apache/jackrabbit/test/api/version/AbstractOnParentVersionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractOnParentVersionTest.java similarity index 79% rename from src/test/org/apache/jackrabbit/test/api/version/AbstractOnParentVersionTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractOnParentVersionTest.java index 7f4f3d10865..74f4be6036d 100644 --- a/src/test/org/apache/jackrabbit/test/api/version/AbstractOnParentVersionTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractOnParentVersionTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -19,7 +19,7 @@ import javax.jcr.RepositoryException; import javax.jcr.Property; import javax.jcr.Node; -import javax.jcr.nodetype.PropertyDef; +import javax.jcr.nodetype.PropertyDefinition; import javax.jcr.version.OnParentVersionAction; @@ -46,18 +46,24 @@ protected void setUp() throws Exception { p = versionableNode.setProperty(propertyName1, initialPropValue); // assert that property has the proper opv-behaviour - PropertyDef pd = p.getDefinition(); + PropertyDefinition pd = p.getDefinition(); if (pd.getOnParentVersion() != OPVAction) { fail("JCR Property at '"+p.getPath()+"' does not have the required OnParentVersion "+OnParentVersionAction.nameFromValue(OPVAction)+" definition."); } - testRootNode.save(); + testRootNode.getSession().save(); + } + + protected void tearDown() throws Exception { + p = null; + super.tearDown(); } /** * Add a child node to the versionable node created in the setup with the * name and nodetype name defined in the corresponding configuration. After * creation of the child node, an assertion is made for the proper onParentVersion - * behaviour.

        + * behaviour. + *

        * NOTE: the child node is removed together with the versionable node after * each test. * diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractVersionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractVersionTest.java new file mode 100644 index 00000000000..3b5864e7113 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/AbstractVersionTest.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.test.api.version; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionHistory; + +/** + * AbstractVersionTest is the abstract base class for all + * versioning related test classes. + */ +public class AbstractVersionTest extends AbstractJCRTest { + + protected NodeType versionableNodeType; + protected NodeType nonVersionableNodeType; + + protected Node versionableNode; + protected Node nonVersionableNode; + + protected String propertyValue; + + protected void setUp() throws Exception { + super.setUp(); + + super.checkSupportedOption(Repository.OPTION_VERSIONING_SUPPORTED); + + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + + // assert that this repository support versioning + try { + NodeType versionableNt = ntMgr.getNodeType(mixVersionable); + if (versionableNt == null) { + fail("Repository does not support Versioning: mixin nodetype 'mix:versionable' is missing."); + } + } catch (NoSuchNodeTypeException e) { + fail("Repository does not support Versioning: mixin nodetype 'mix:versionable' is missing."); + } + + // retrieve versionable nodetype + String versionableNodeTypeName = getProperty("versionableNodeType"); + try { + versionableNodeType = ntMgr.getNodeType(versionableNodeTypeName); + if (versionableNodeType == null) { + fail("Property 'versionableNodeType' does not define a valid nodetype: '"+versionableNodeTypeName+"'"); + } + } catch (NoSuchNodeTypeException e) { + fail("Property 'versionableNodeType' does not define an existing nodetype: '"+versionableNodeTypeName+"'"); + } + + // make sure 'non-versionable' nodetype is properly defined + try { + nonVersionableNodeType = ntMgr.getNodeType(testNodeType); + if (nonVersionableNodeType == null || nonVersionableNodeType.isNodeType(mixVersionable)) { + fail("Property 'testNodeType' does define a versionable nodetype: '"+testNodeType+"'"); + } + } catch (NoSuchNodeTypeException e) { + fail("Property 'testNodeType' does not define an existing nodetype: '"+testNodeType+"'"); + } + + // build persistent versionable and non-versionable nodes + try { + versionableNode = createVersionableNode(testRootNode, nodeName1, versionableNodeType); + } catch (RepositoryException e) { + fail("Failed to create versionable test node." + e.getMessage()); + } + try { + nonVersionableNode = testRootNode.addNode(nodeName3, nonVersionableNodeType.getName()); + testRootNode.getSession().save(); + } catch (RepositoryException e) { + fail("Failed to create non-versionable test node." + e.getMessage()); + } + + propertyValue = getProperty("propertyValue"); + if (propertyValue == null) { + fail("Property 'propertyValue' is not defined."); + } + } + + protected void tearDown() throws Exception { + // remove versionable nodes + try { + if (versionableNode != null) { + versionableNode.remove(); + } + testRootNode.getSession().save(); + } catch (Exception e) { + log.println("Exception in tearDown: " + e.toString()); + } finally { + versionableNodeType = null; + nonVersionableNodeType = null; + versionableNode = null; + nonVersionableNode = null; + super.tearDown(); + } + } + + /** + * Retrieve the number of versions present in the given version history. + * + * @param vHistory + * @return number of versions + * @throws RepositoryException + */ + protected long getNumberOfVersions(VersionHistory vHistory) throws RepositoryException { + return getSize(vHistory.getAllVersions()); + } + + /** + * Create a versionable node below the given parent node. If the specified + * nodetype name is not mix:versionable an attempt is made to add the + * mix:versionable mixin type to the created child node. + * + * @param parent + * @param name + * @param nodetype + * @return versionable node. + * @throws RepositoryException + */ + protected Node createVersionableNode(Node parent, String name, NodeType nodetype) + throws RepositoryException, NotExecutableException { + Node versionableNode = parent.addNode(name, nodetype.getName()); + ensureMixinType(versionableNode, mixVersionable); + parent.save(); + + return versionableNode; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/ActivitiesTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/ActivitiesTest.java new file mode 100644 index 00000000000..456bf8bd313 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/ActivitiesTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.VersionManager; + + +/** + * ActivitiesTest covers methods related to the Activities + * feature in Versioning. + * @since JCR 2.0 + */ +public class ActivitiesTest extends AbstractVersionTest { + + private VersionManager vm; + + private static String PREFIX = "/jcr:system/jcr:activities/"; + + protected void setUp() throws Exception { + super.setUp(); + checkSupportedOption(Repository.OPTION_ACTIVITIES_SUPPORTED); + vm = superuser.getWorkspace().getVersionManager(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testCreateRemoveActivity() throws Exception { + + Node an = null; + + try { + an = vm.createActivity("foobar"); + assertNotNull(an); + + NodeType annt = an.getPrimaryNodeType(); + assertTrue("create node must be subtype of nt:activity", annt.isNodeType("nt:activity")); + } + finally { + if (an != null) { + vm.removeActivity(an); + } + } + } + + public void testSetGetActivity() throws Exception { + + Node an = null; + + try { + an = vm.createActivity("foobar"); + assertNotNull(an); + + assertNull(vm.getActivity()); + + Node old = vm.setActivity(an); + assertNull(old); + assertEquals(an.getPath(), vm.getActivity().getPath()); + + old = vm.setActivity(null); + assertEquals(old.getPath(), an.getPath()); + assertNull(vm.getActivity()); + } + finally { + if (an != null) { + vm.removeActivity(an); + } + } + } + + public void testActivitiesPath() throws Exception { + + Node an = null; + + try { + an = vm.createActivity("foobar"); + assertNotNull(an); + + NodeType annt = an.getPrimaryNodeType(); + assertTrue("create node must be subtype of nt:activity", annt.isNodeType("nt:activity")); + + assertTrue("path for activity must be below " + PREFIX + ", but was " + an.getPath(), an.getPath().startsWith(PREFIX)); + + Node activities = superuser.getNode(PREFIX); + + try { + activities.addNode("foobar"); + fail("/jcr:system/jcr:activities must be protected."); + } catch (RepositoryException e) { + // ok + } + } + finally { + if (an != null) { + vm.removeActivity(an); + } + } + } + + public void testActivitiesRelation() throws Exception { + + Node an = null; + + try { + an = vm.createActivity("foobar"); + vm.setActivity(an); + + String path = versionableNode.getPath(); + + if (versionableNode.isCheckedOut()) { + vm.checkin(path); + } + + vm.checkout(path); + + versionableNode = superuser.getNode(path); + Property act = versionableNode.getProperty(Property.JCR_ACTIVITY); + assertNotNull(act); + assertEquals(PropertyType.REFERENCE, act.getType()); + assertTrue(act.getNode().isSame(an)); + + versionableNode.remove(); + versionableNode.getSession().save(); + } + finally { + if (an != null) { + vm.removeActivity(an); + } + } + } + + public void testActivitiesRelationWithCheckpoint() throws Exception { + + Node an = null; + + try { + an = vm.createActivity("foobar2"); + vm.setActivity(an); + + String path = versionableNode.getPath(); + + vm.checkpoint(path); + + versionableNode = superuser.getNode(path); + Property act = versionableNode.getProperty(Property.JCR_ACTIVITY); + assertNotNull(act); + assertEquals(PropertyType.REFERENCE, act.getType()); + assertTrue(act.getNode().isSame(an)); + + versionableNode.remove(); + superuser.save(); + } + finally { + if (an != null) { + vm.removeActivity(an); + } + } + } +} + diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CheckinTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CheckinTest.java new file mode 100644 index 00000000000..e6ee52f5fd2 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CheckinTest.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.InvalidItemStateException; +import javax.jcr.UnsupportedRepositoryOperationException; +import java.util.Arrays; + +/** + * CheckinTest covers tests related to {@link javax.jcr.Node#checkin()}. + * + */ +public class CheckinTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + } + + /** + * Test if Node.isCheckedOut() return false after calling Node.checkin() + * + * @throws javax.jcr.RepositoryException + */ + @SuppressWarnings("deprecation") + public void testIsCheckedOut() throws RepositoryException { + versionableNode.checkin(); + assertTrue("After calling Node.checkin() on a versionable node N, N.isCheckedOut() must return false", versionableNode.isCheckedOut() == false); + } + + /** + * Test if VersionManager.isCheckedOut(P) returns false if P is the + * absolute path of a checked-in versionable node. + * + * @throws javax.jcr.RepositoryException + */ + public void testIsCheckedOutJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkin(path); + assertTrue("VersionManager.isCheckedOut(P) must return false if the path P resolves to a checked-in node.", versionManager.isCheckedOut(path) == false); + } + + /** + * Test if the node's jcr:predecessors property contains an empty value array + * after checkin. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testCheckinRemovesPredecessorProperty() throws RepositoryException { + + versionableNode.checkin(); + Value[] predecessorsValue = versionableNode.getProperty(jcrPredecessors).getValues(); + + assertTrue("Checkin must set the node's jcr:predecessors property to the empty array", predecessorsValue.length == 0); + } + + /** + * Test if the node's jcr:predecessors property contains an empty value array + * after checkin. + * + * @throws RepositoryException + */ + public void testCheckinRemovesPredecessorPropertyJcr2() throws RepositoryException { + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkin(path); + Value[] predecessorsValue = versionableNode.getProperty(jcrPredecessors).getValues(); + + assertTrue("Checkin must set the node's jcr:predecessors property to the empty array", predecessorsValue.length == 0); + } + + /** + * Test if the nodes jcr:predecessors property is copied to the new version + * on Node.checkin(). + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testPredecessorIsCopiedToNewVersion() throws RepositoryException { + + Value[] nPredecessorsValue = versionableNode.getProperty(jcrPredecessors).getValues(); + + Version v = versionableNode.checkin(); + Value[] vPredecessorsValue = v.getProperty(jcrPredecessors).getValues(); + + assertEquals("The versionable checked-out node's jcr:predecessors property is copied to the new version on checkin.", Arrays.asList(nPredecessorsValue), Arrays.asList(vPredecessorsValue)); + } + + /** + * Test if the nodes jcr:predecessors property is copied to the new version + * on checkin. + * + * @throws RepositoryException + */ + public void testPredecessorIsCopiedToNewVersionJcr2() throws RepositoryException { + + Value[] nPredecessorsValue = versionableNode.getProperty(jcrPredecessors).getValues(); + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Value[] vPredecessorsValue = v.getProperty(jcrPredecessors).getValues(); + + assertEquals("The versionable checked-out node's jcr:predecessors property is copied to the new version on checkin.", Arrays.asList(nPredecessorsValue), Arrays.asList(vPredecessorsValue)); + } + + /** + * Test if Node.checkin() on a checked-in node has no effect. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testMultipleCheckinHasNoEffect() throws RepositoryException { + + Version v = versionableNode.checkin(); + try { + Version v2 = versionableNode.checkin(); + + assertTrue("Calling checkin() on a node that is already checked-in must not have an effect.", v.isSame(v2)); + } catch (RepositoryException e) { + fail("Calling checkin() on a node that is already checked-in must not throw an exception."); + } + } + + /** + * Test if VersionManager.checkin(P) has no effect if the path P resolves + * to a checked-in node. + * + * @throws RepositoryException + */ + public void testMultipleCheckinHasNoEffectJcr2() throws RepositoryException { + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + try { + Version v2 = versionManager.checkin(path); + + assertTrue("Calling VersionManager.checkin(P) must not have an if the path P resolves to a node that is already checked-in.", v.isSame(v2)); + } catch (RepositoryException e) { + fail("Calling VersionManager.checkin(P) must not throw an exception if the path P resolves to a node that is already checked-in."); + } + } + + /** + * Test if versionable node N's jcr:baseVersion property is set to refer to + * the new version after checkin. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testBaseVersionAfterCheckin() throws RepositoryException { + Version v = versionableNode.checkin(); + Value baseVersionRef = versionableNode.getProperty(jcrBaseVersion).getValue(); + + assertEquals("Checked-in node's jcr:baseVersion property is set to refer to the version created on checkin.", superuser.getValueFactory().createValue(v), baseVersionRef); + } + + /** + * Test if versionable node N's jcr:baseVersion property is set to refer to + * the new version after checkin. + * + * @throws RepositoryException + */ + public void testBaseVersionAfterCheckinJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Value baseVersionRef = versionableNode.getProperty(jcrBaseVersion).getValue(); + + assertEquals("Checked-in node's jcr:baseVersion property is set to refer to the version created on checkin.", superuser.getValueFactory().createValue(v), baseVersionRef); + } + + /** + * Test if Node.checkin() throws InvalidItemStateException if the node + * has unsaved changes pending. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testCheckinWithPendingChanges() throws RepositoryException { + try { + // modify node without calling save() + versionableNode.setProperty(propertyName1, propertyValue); + versionableNode.checkin(); + + fail("InvalidItemStateException must be thrown on attempt to checkin a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if VersionManager.checkin(P) throws InvalidItemStateException if + * the path P resolves to a node that has unsaved changes pending. + * + * @throws RepositoryException + */ + public void testCheckinWithPendingChangesJcr2() throws RepositoryException { + try { + // modify node without calling save() + versionableNode.setProperty(propertyName1, propertyValue); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkin(path); + + fail("InvalidItemStateException must be thrown on attempt to checkin a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if Node.isCheckedOut() returns false after Node.checkin(). + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testIsNotCheckedOut() throws RepositoryException { + versionableNode.checkin(); + boolean isCheckedOut = versionableNode.isCheckedOut(); + + assertFalse("Node.isCheckedOut() must return false after Node.checkin().", isCheckedOut); + } + + /** + * Test if VersionManager.isCheckedOut(P) returns false after calling VersionManager.checkin(P). + * + * @throws RepositoryException + */ + public void testIsNotCheckedOutJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkin(path); + boolean isCheckedOut = versionManager.isCheckedOut(path); + + assertFalse("VersionManager.isCheckedOut(P) must return false after VersionManager.checkin(P).", isCheckedOut); + } + + /** + * Test if Node.checkin() adds another version to the VersionHistory + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testCheckinCreatesNewVersion() throws RepositoryException { + + long initialNumberOfVersions = getNumberOfVersions(versionableNode.getVersionHistory()); + versionableNode.checkin(); + long numberOfVersions = getNumberOfVersions(versionableNode.getVersionHistory()); + + assertTrue("Checkin must create a new Version in the VersionHistory.", numberOfVersions == initialNumberOfVersions + 1); + } + + /** + * Test if VersionManager.checkin(String) adds another version to the VersionHistory + * + * @throws RepositoryException + */ + public void testCheckinCreatesNewVersionJcr2() throws RepositoryException { + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + long initialNumberOfVersions = getNumberOfVersions(versionManager.getVersionHistory(path)); + versionManager.checkin(path); + long numberOfVersions = getNumberOfVersions(versionManager.getVersionHistory(path)); + + assertTrue("Checkin must create a new Version in the VersionHistory.", numberOfVersions == initialNumberOfVersions + 1); + } + + /** + * Test calling Node.checkin() on a non-versionable node. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testCheckinNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.checkin(); + fail("Node.checkin() on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling VersionManager.checkin(P) with the path P resolving to + * a non-versionable node. + * + * @throws RepositoryException + */ + public void testCheckinNonVersionableNodeJcr2() throws RepositoryException { + try { + VersionManager versionManager = nonVersionableNode.getSession().getWorkspace().getVersionManager(); + String path = nonVersionableNode.getPath(); + versionManager.checkin(path); + fail("VersionManager.checkin(P) must throw UnsupportedRepositoryOperationException if the path P resolves to a non-versionable node."); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CheckoutTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CheckoutTest.java new file mode 100644 index 00000000000..99632f20af7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CheckoutTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.ItemNotFoundException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.version.VersionManager; + +/** + * CheckoutTest covers tests related to {@link + * javax.jcr.Node#checkout()} and {@link javax.jcr.Node#isCheckedOut()}. + * + */ +public class CheckoutTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + if (!versionManager.isCheckedOut(path)) { + fail("A versionable node must be checked-out after persistent creation."); + } + if (!versionableNode.isCheckedOut()) { + fail("A versionable node must be checked-out after persistent creation."); + } + versionManager.checkin(path); + } + + /** + * Test if Node.isCheckedOut() returns true, if the versionable node has + * been checked out before. + */ + @SuppressWarnings("deprecation") + public void testIsCheckedOut() throws RepositoryException { + versionableNode.checkout(); + assertTrue("After calling Node.checkout() a versionable node N, N.isCheckedOut() must return true.", versionableNode.isCheckedOut()); + } + + /** + * Test if VersionManager.isCheckedOut(P) returns true if P is the + * absolute path of a versionable node that has been checked out before. + */ + public void testIsCheckedOutJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + assertTrue("After successfully calling VersionManager.checkout(P) with P denoting the absolute path of a versionable node, VersionManager.isCheckedOut(P) must return true.", versionManager.isCheckedOut(path)); + } + + /** + * Test calling Node.isCheckedOut() on a non-versionable. + */ + public void testIsCheckedOutNonVersionableNode() throws RepositoryException { + boolean isCheckedOut = nonVersionableNode.isCheckedOut(); + Node vParent = null; + try { + vParent = nonVersionableNode.getParent(); + while (!vParent.isNodeType(mixVersionable)) { + vParent = vParent.getParent(); + } + } catch (ItemNotFoundException e) { + // root reached. + } + + if (vParent != null && vParent.isNodeType(mixVersionable)) { + if (vParent.isCheckedOut()) { + assertTrue("Node.isCheckedOut() must return true if the node is non-versionable and its nearest versionable ancestor is checked-out.", isCheckedOut); + } else { + assertFalse("Node.isCheckedOut() must return false if the node is non-versionable and its nearest versionable ancestor is checked-in.", isCheckedOut); + } + } else { + assertTrue("Node.isCheckedOut() must return true if the node is non-versionable and has no versionable ancestor", isCheckedOut); + } + } + + /** + * Test calling VersionManager.isCheckedOut(P) with P denoting the + * absolute path of a non-versionable node. + */ + public void testIsCheckedOutNonVersionableNodeJcr2() throws RepositoryException { + VersionManager versionManager = nonVersionableNode.getSession().getWorkspace().getVersionManager(); + String path = nonVersionableNode.getPath(); + boolean isCheckedOut = versionManager.isCheckedOut(path); + Node vParent = null; + try { + vParent = nonVersionableNode.getParent(); + while (!vParent.isNodeType(mixVersionable)) { + vParent = vParent.getParent(); + } + } catch (ItemNotFoundException e) { + // root reached. + } + + if (vParent != null && vParent.isNodeType(mixVersionable)) { + String parentPath = vParent.getPath(); + if (versionManager.isCheckedOut(parentPath)) { + assertTrue("VersionManager.isCheckedOut(P) must return true if P denotes the absolute path of a non-versionable node whose nearest versionable ancestor is checked-out.", isCheckedOut); + } else { + assertFalse("VersionManager.isCheckedOut(P) must return false if P denotes the absolute path of a non-versionable node whose nearest versionable ancestor is checked-in.", isCheckedOut); + } + } else { + assertTrue("VersionManager.isCheckedOut(P) must return true if P denotes the absolute path of a non-versionable node that has no versionable ancestor", isCheckedOut); + } + } + + /** + * Test calling Node.checkout() on a non-versionable node. + */ + @SuppressWarnings("deprecation") + public void testCheckoutNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.checkout(); + fail("Node.checkout() on a non-versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling VersionManager.checkout(P) with P denoting the absolute + * path of a non-versionable node. + */ + public void testCheckoutNonVersionableNodeJcr2() throws RepositoryException { + VersionManager versionManager = nonVersionableNode.getSession().getWorkspace().getVersionManager(); + String path = nonVersionableNode.getPath(); + try { + versionManager.checkout(path); + fail("VersionManager.checkout(P) with P denoting the absolute path of a non-versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test if Node.checkout() doesn't throw any exception if the versionable + * node has been checked out before. + */ + @SuppressWarnings("deprecation") + public void testCheckoutTwiceDoesNotThrow() throws RepositoryException { + versionableNode.checkout(); + versionableNode.checkout(); + } + + /** + * Test if VersionManager.checkout(P) doesn't throw any exception if P + * denotes the absolute path of a versionable node that has been checked + * out before. + */ + public void testCheckoutTwiceDoesNotThrowJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + versionManager.checkout(path); + } + + /** + * Test if Node.checkout() copies the node's jcr:baseVersion to node's + * jcr:predecessors property (no save required). + */ + @SuppressWarnings("deprecation") + public void testCheckoutCopiesBaseValueToPredecessorProperty() throws RepositoryException { + Value baseVersionValue = versionableNode.getProperty(jcrBaseVersion).getValue(); + versionableNode.checkout(); + Value[] predecessorsValues = versionableNode.getProperty(jcrPredecessors).getValues(); + + // loop over all values of jcr:predecessors property as it's not sure + // on which position jcr:baseVersion is copied. + boolean foundBaseVersionProp = false; + int i = 0; + while (i < predecessorsValues.length && !foundBaseVersionProp) { + if (predecessorsValues[i].equals(baseVersionValue)) { + foundBaseVersionProp = true; + } + i++; + } + if (!foundBaseVersionProp) { + fail("After calling Node.checkout() the current value of node's jcr:baseVersion must be copied to node's jcr:predecessors property"); + } + } + + /** + * Test if VersionManager.checkout(P), with P denoting the absolute path + * of a versionable node, copies the node's jcr:baseVersion to the node's + * jcr:predecessors property (no save required). + */ + public void testCheckoutCopiesBaseValueToPredecessorPropertyJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Value baseVersionValue = versionableNode.getProperty(jcrBaseVersion).getValue(); + versionManager.checkout(path); + Value[] predecessorsValues = versionableNode.getProperty(jcrPredecessors).getValues(); + + // loop over all values of jcr:predecessors property as it's not sure + // on which position jcr:baseVersion is copied. + boolean foundBaseVersionProp = false; + int i = 0; + while (i < predecessorsValues.length && !foundBaseVersionProp) { + if (predecessorsValues[i].equals(baseVersionValue)) { + foundBaseVersionProp = true; + } + i++; + } + if (!foundBaseVersionProp) { + fail("After calling Node.checkout() the current value of node's jcr:baseVersion must be copied to node's jcr:predecessors property"); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/ConfigurationsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/ConfigurationsTest.java new file mode 100644 index 00000000000..0928dbc4d7e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/ConfigurationsTest.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + + +/** + * ConfigurationsTest covers methods related to the Configurations + * feature in Versioning. + * @since JCR 2.0 + */ +public class ConfigurationsTest extends AbstractVersionTest { + + private VersionManager vm; + + private static String PREFIX = "/jcr:system/jcr:configurations/"; + + private String ntConfiguration; + + private Node versionableNode2; + + protected void setUp() throws Exception { + super.setUp(); + checkSupportedOption(Repository.OPTION_BASELINES_SUPPORTED); + vm = superuser.getWorkspace().getVersionManager(); + + versionableNode2 = createVersionableNode(testRootNode, nodeName4, versionableNodeType); + + ntConfiguration = superuser.getNamespacePrefix(NS_NT_URI) + ":configuration"; + + } + + protected void tearDown() throws Exception { + // remove configuration, otherwise + // subsequent tests will fail when cleaning the test root + if (versionableNode != null) { + removeConfiguration(versionableNode); + } + removeConfiguration(versionableNode2); + versionableNode2.remove(); + // save is called in super.tearDown() + super.tearDown(); + } + + private void removeConfiguration(Node node) throws RepositoryException { + if (node.hasProperty("jcr:configuration")) { + node.getProperty("jcr:configuration").getNode().remove(); + } + } + + public void testCreateConfiguration() throws Exception { + Node config = vm.createConfiguration(versionableNode.getPath()); + assertNotNull(config); + NodeType nt = config.getPrimaryNodeType(); + assertTrue("created node must be subtype of nt:configuration", nt.isNodeType(ntConfiguration)); + + // check if the configuration points to the versionable + assertTrue("jcr:root property of the configuration must reference the versionable node", + config.getProperty("jcr:root").getNode().isSame(versionableNode)); + + // check if the versionable points to the configuration + assertTrue("jcr:configuration property of the versionable node must reference the configuration", + versionableNode.getProperty("jcr:configuration").getNode().isSame(config)); + + } + + public void testCreateConfigurationNotVersionableFails() throws Exception { + try { + vm.createConfiguration(nonVersionableNode.getPath()); + fail("Create configuration must fail for non-versionable node"); + } catch (UnsupportedRepositoryOperationException e) { + // ignore + } + } + + public void testCreateConfigurationTwiceFails() throws Exception { + vm.createConfiguration(versionableNode.getPath()); + try { + vm.createConfiguration(versionableNode.getPath()); + fail("Create configuration must fail if versionable is already a configuration"); + } catch (UnsupportedRepositoryOperationException e) { + // ignore + } + } + + public void testConfigurationsPath() throws Exception { + Node config = vm.createConfiguration(versionableNode.getPath()); + assertNotNull(config); + NodeType nt = config.getPrimaryNodeType(); + assertTrue("created node must be subtype of nt:configuration", nt.isNodeType(ntConfiguration)); + + assertTrue("path for configuration must be below " + PREFIX + ", but was " + + config.getPath(), config.getPath().startsWith(PREFIX)); + } + + public void testCheckinConfigFailsWithUnversionedChild() throws Exception { + Node config = vm.createConfiguration(versionableNode.getPath()); + try { + vm.checkin(config.getPath()); + fail("Checkin configuration must fail one of the recorded versions is not versioned."); + } catch (UnsupportedRepositoryOperationException e) { + // ignore + } + } + + public void testCheckinConfig() throws Exception { + vm.checkin(versionableNode.getPath()); + Node config = vm.createConfiguration(versionableNode.getPath()); + vm.checkin(config.getPath()); + } + + public void testCreateConfigWithBaseline() throws Exception { + // create configuration + String path = versionableNode.getPath(); + Version baseVersion = vm.checkin(path); + Node config = vm.createConfiguration(path); + // record baseline + Version baseline = vm.checkin(config.getPath()); + + // remove workspace nodes + removeConfiguration(versionableNode); + versionableNode.remove(); + versionableNode = null; + testRootNode.getSession().save(); + + // and try to restore it + vm.restore(path, baseline, true); + + versionableNode = testRootNode.getSession().getNode(path); + Version baseVersion2 = vm.getBaseVersion(versionableNode.getPath()); + assertTrue("restored node must have former base version.", baseVersion.isSame(baseVersion2)); + + config = versionableNode.getProperty("jcr:configuration").getNode(); + + // base version of config must be baseline + assertTrue("Baseversion of restored config must be given baseline.", + vm.getBaseVersion(config.getPath()).isSame(baseline)); + + } + + public void testCreateConfigWithNonExistentParentFails() throws Exception { + // create configuration + String path = versionableNode.getPath(); + vm.checkin(path); + Node config = vm.createConfiguration(path); + // record baseline + Version baseline = vm.checkin(config.getPath()); + + // remove workspace nodes + removeConfiguration(versionableNode); + versionableNode.remove(); + versionableNode = null; + testRootNode.getSession().save(); + + try { + vm.restore("/non/existent/parent", baseline, true); + fail("Create configuration must fail if parent does not exist."); + } catch (RepositoryException e) { + // ignore + } + } + + public void testCreateConfigWithExistentConfigFromBaselineFails() throws Exception { + // create configuration + String path = versionableNode.getPath(); + vm.checkin(path); + Node config = vm.createConfiguration(path); + // record baseline + Version baseline = vm.checkin(config.getPath()); + + try { + vm.restore(testRoot + "/nonExisting", baseline, true); + fail("Create configuration must fail if config recorded in baseline already exists."); + } catch (RepositoryException e) { + // ignore + } + } + + public void testRestoreBaseline() throws Exception { + // create configuration + String path = versionableNode.getPath(); + Version bv1 = vm.checkpoint(path); + Node config = vm.createConfiguration(path); + // record baseline 1 (should contain bv1) + Version bl1 = vm.checkpoint(config.getPath()); + // create bv2 + Version bv2 = vm.checkpoint(path); + // record baseline 2 (should contain bv2) + Version bl2 = vm.checkpoint(config.getPath()); + + // restore bl1 + vm.restore(bl1, true); + Version bv = vm.getBaseVersion(path); + assertTrue("restored node must have former base version V1.0.", bv.isSame(bv1)); + + // restore bl2 + vm.restore(bl2, true); + bv = vm.getBaseVersion(path); + assertTrue("restored node must have former base version V1.1.", bv.isSame(bv2)); + } + + public void testRestoreConfig() throws Exception { + // create configuration + String path = versionableNode.getPath(); + Version bv1 = vm.checkpoint(path); + Node config = vm.createConfiguration(path); + String configPath = config.getPath(); + + // record baseline 1 (should contain bv1) + Version bl1 = vm.checkpoint(config.getPath()); + // create bv2 + Version bv2 = vm.checkpoint(path); + // record baseline 2 (should contain bv2) + Version bl2 = vm.checkpoint(config.getPath()); + + // restore bl1 + vm.restore(configPath, bl1.getName(), true); + Version bv = vm.getBaseVersion(path); + assertTrue("restored node must have former base version V1.0.", bv.isSame(bv1)); + + // restore bl2 + vm.restore(configPath, bl2.getName(), true); + bv = vm.getBaseVersion(path); + assertTrue("restored node must have former base version V1.1.", bv.isSame(bv2)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CopyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CopyTest.java new file mode 100644 index 00000000000..7c538de9e22 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/CopyTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; +import javax.jcr.version.Version; + +/** + * CopyTest checks if full versionable nodes are copied correctly: + * + * 15.1.4 Copying Versionable Nodes and Version Lineage + * Under both simple and full versioning, when an existing versionable node N is + * copied to a new location either in the same workspace or another, and the + * repository preserves the versionable mixin (see 10.7.4 Dropping Mixins on + * Copy): + * - A copy of N, call it M, is created, as usual. + * - A new, empty, version history for M, call it HM, is also created. + * + * Under full versioning: + * - The properties jcr:versionHistory, jcr:baseVersion and + * jcr:predecessors of M are not copied from N but are initialized as usual. + * - The jcr:copiedFrom property of HM is set to point to the base version of N. + * + */ +public class CopyTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + } + + protected void tearDown() throws Exception { + // remove copied node + try { + String dstPath = getProperty("destination"); + superuser.getNode(dstPath).remove(); + testRootNode.getSession().save(); + } catch (Exception e) { + log.println("Exception in tearDown: " + e.toString()); + } finally { + super.tearDown(); + } + } + + public void testCopy() throws RepositoryException { + Workspace wsp = superuser.getWorkspace(); + VersionManager vMgr = wsp.getVersionManager(); + String srcPath = versionableNode.getPath(); + String dstPath = getProperty("destination"); + wsp.copy(srcPath, dstPath); + + // check versionable + Node v = superuser.getNode(dstPath); + assertTrue("Copied Node.isNodeType(mix:cersionable) must return true.", + v.isNodeType(mixVersionable)); + + // check different version history + VersionHistory vh1 = vMgr.getVersionHistory(srcPath); + VersionHistory vh2 = vMgr.getVersionHistory(dstPath); + assertFalse("Copied node needs a new version history.", vh1.isSame(vh2)); + + // check if 1 version + assertEquals("Copied node must have 1 version.", 1, getNumberOfVersions(vh2)); + + // check if jcr:copiedFrom is set correctly + assertTrue("Version history of desination must have a jcr:copiedFrom property", vh2.hasProperty(jcrCopiedFrom)); + + Node ref = vh2.getProperty(jcrCopiedFrom).getNode(); + Version base = vMgr.getBaseVersion(srcPath); + assertTrue("jcr:copiedFrom must point to the base version of the original.", ref.isSame(base)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/FrozenNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/FrozenNodeTest.java new file mode 100644 index 00000000000..629e26f409c --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/FrozenNodeTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * CheckinTest covers tests related to {@link javax.jcr.Node#checkin()}. + * + */ +public class FrozenNodeTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + } + + /** + * @throws RepositoryException + */ + public void testFrozenNodeUUUID() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode(); + String puuid = n.getProperty(jcrUUID).getValue().getString(); + String nuuid = n.getIdentifier(); + assertEquals("jcr:uuid needs to be equal to the getIdentifier() return value.", nuuid, puuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenChildNodeUUUID() throws RepositoryException { + versionableNode.addNode("child"); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode().getNode("child"); + String puuid = n.getProperty(jcrUUID).getValue().getString(); + String nuuid = n.getIdentifier(); + assertEquals("jcr:uuid needs to be equal to the getIdentifier() return value.", nuuid, puuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenUUUID() throws RepositoryException, + NotExecutableException { + // make versionable node referenceable + ensureMixinType(versionableNode, mixReferenceable); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode(); + String fuuid = n.getProperty(jcrFrozenUuid).getValue().getString(); + String ruuid = versionableNode.getIdentifier(); + assertEquals("jcr:frozenUuid needs to be equal to the getIdentifier() return value.", ruuid, fuuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenChildUUUID() throws RepositoryException, + NotExecutableException { + Node n1 = versionableNode.addNode("child"); + ensureMixinType(n1, mixReferenceable); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode().getNode("child"); + String fuuid = n.getProperty(jcrFrozenUuid).getValue().getString(); + String ruuid = n1.getIdentifier(); + assertEquals("jcr:frozenUuid needs to be equal to the getIdentifier() return value.", ruuid, fuuid); + } + + + /** + * @throws RepositoryException + */ + public void testFrozenNodeNodeType() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode(); + String puuid = n.getProperty(jcrPrimaryType).getValue().getString(); + String nuuid = n.getPrimaryNodeType().getName(); + assertEquals("jcr:primaryType needs to be equal to the getPrimaryNodeType() return value.", nuuid, puuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenChildNodeNodeType() throws RepositoryException { + versionableNode.addNode("child"); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode().getNode("child"); + String puuid = n.getProperty(jcrPrimaryType).getValue().getString(); + String nuuid = n.getPrimaryNodeType().getName(); + assertEquals("jcr:primaryType needs to be equal to the getPrimaryNodeType() return value.", nuuid, puuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenNodeType() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode(); + String fuuid = n.getProperty("jcr:frozenPrimaryType").getValue().getString(); + String ruuid = versionableNode.getPrimaryNodeType().getName(); + assertEquals("jcr:frozenPrimaryType needs to be equal to the getPrimaryNodeType() return value.", ruuid, fuuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenChildNodeType() throws RepositoryException { + Node n1 = versionableNode.addNode("child"); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode().getNode("child"); + String fuuid = n.getProperty("jcr:frozenPrimaryType").getValue().getString(); + String ruuid = n1.getPrimaryNodeType().getName(); + assertEquals("jcr:frozenPrimaryType needs to be equal to the getPrimaryNodeType() return value.", ruuid, fuuid); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetContainingHistoryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetContainingHistoryTest.java new file mode 100644 index 00000000000..dd62a781dda --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetContainingHistoryTest.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.test.api.version; + +import javax.jcr.version.Version; +import javax.jcr.RepositoryException; + +/** + * GetContainingHistoryTest provides test methods covering {@link + * javax.jcr.version.Version#getContainingHistory()}. + * + */ +public class GetContainingHistoryTest extends AbstractVersionTest { + + /** + * Tests if {@link javax.jcr.version.Version#getContainingHistory()} returns + * the correct VersionHistory instance. + */ + public void testGetContainingHistory() throws RepositoryException { + // create version + versionableNode.checkout(); + Version version = versionableNode.checkin(); + + assertTrue("Method getContainingHistory() must return the same VersionHistory " + + "as getVersionHistory() of the corresponding Node.", + versionableNode.getVersionHistory().isSame(version.getContainingHistory())); + } +} diff --git a/src/test/org/apache/jackrabbit/test/api/version/GetCreatedTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetCreatedTest.java similarity index 81% rename from src/test/org/apache/jackrabbit/test/api/version/GetCreatedTest.java rename to jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetCreatedTest.java index 4923e008d2c..24a33e217cd 100644 --- a/src/test/org/apache/jackrabbit/test/api/version/GetCreatedTest.java +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetCreatedTest.java @@ -1,10 +1,10 @@ /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -25,10 +25,6 @@ * GetCreatedTest provides test methods covering {@link * javax.jcr.version.Version#getCreated()}. * - * @test - * @sources GetCreatedTest.java - * @executeClass org.apache.jackrabbit.test.api.version.GetCreatedTest - * @keywords versioning */ public class GetCreatedTest extends AbstractVersionTest { @@ -65,4 +61,4 @@ public void testGetCreatedCheckAgainstProperty() throws RepositoryException { assertEquals("Method getCreated() should return value of the jcr:created property.", calGetCreated, calCreatedProp); } -} \ No newline at end of file +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetPredecessorsTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetPredecessorsTest.java new file mode 100644 index 00000000000..00ab2a55752 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetPredecessorsTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +/** + * GetPredecessorsTest provides test methods covering {@link + * Version#getPredecessors()}, {@link Version#getLinearPredecessor()} and + * {@link Version#getLinearSuccessor()}. + * + */ +public class GetPredecessorsTest extends AbstractVersionTest { + + /** + * Returns the predecessor versions of this version. This corresponds to + * returning all the nt:version nodes whose jcr:successors property includes + * a reference to the nt:version node that represents this version. A + * RepositoryException is thrown if an error occurs. + */ + public void testGetPredecessors() throws RepositoryException { + // create a new version + versionableNode.checkout(); + Version version = versionableNode.checkin(); + + assertTrue("Version should have at minimum one predecessor version.", version.getPredecessors().length > 0); + } + + /** + * Checks obtaining the linear predecessor. + * @since JCR 2.0 + */ + public void testGetLinearPredecessorSuccessor() throws RepositoryException { + + String path = versionableNode.getPath(); + + VersionManager vm = versionableNode.getSession().getWorkspace().getVersionManager(); + + // get the previous version + Version pred = vm.getBaseVersion(path); + + // shouldn't have a predecessor + assertNull(pred.getLinearPredecessor()); + + // shouldn't have a successor yet + assertNull(pred.getLinearSuccessor()); + + // check root version + Version root = vm.getVersionHistory(path).getRootVersion(); + assertNull(root.getLinearSuccessor()); + + // create a new version + vm.checkout(path); + Version version = vm.checkin(path); + + // refresh the predecessor + pred = (Version)versionableNode.getSession().getNode(pred.getPath()); + + assertTrue("linear predecessor of new version should be previous version", + version.getLinearPredecessor().isSame(pred)); + assertTrue("linear successor of previous version should be new version", + pred.getLinearSuccessor().isSame(version)); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetReferencesNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetReferencesNodeTest.java new file mode 100644 index 00000000000..5a2c95cae7c --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetReferencesNodeTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * GetReferencesNodeTest contains test to check if references are + * returned from versions. + *

          + *
        • {@code versionableNodeType} name of a node type which is versionable + *
        • {@code testroot} path to test root. Must allow versionable child nodes. + *
        • {@code nodename1} name of a versionable child node. + *
        • {@code nodename2} name of a versionable child node. + *
        • {@code propertyname1} name of a reference property declared in the + * versionable node type. + *
        + */ +public class GetReferencesNodeTest extends AbstractJCRTest { + + private static final String PROP_VERSIONABLE_NODE_TYPE = "versionableNodeType"; + private String versionableNodeType; + + private Node testNode; + private Node nodeToBeReferenced; + + protected void setUp() throws Exception { + super.setUp(); + + super.checkSupportedOption(Repository.OPTION_VERSIONING_SUPPORTED); + + versionableNodeType = getProperty(PROP_VERSIONABLE_NODE_TYPE); + if (versionableNodeType == null) { + fail("Property '" + PROP_VERSIONABLE_NODE_TYPE + "' is not defined."); + } + } + + protected void tearDown() throws Exception { + testRoot = null; + nodeToBeReferenced = null; + super.tearDown(); + } + + /** + * Node.getReferences() never returns a reference that is stored in a + * version. 1. Create some test nodes 2. Create a version 1.0 with reference + * 3. Create a new version 1.1 after changing reference 4. Check if + * reference is found by getReferences() + */ + public void testGetReferencesNeverFromVersions() throws RepositoryException, NotExecutableException { + // create some test nodes + initTestNodes(); + + // create a version 1.0 and reference test node + testNode.checkout(); + ensureCanSetProperty(testNode, propertyName1, PropertyType.REFERENCE, false); + testNode.setProperty(propertyName1, nodeToBeReferenced); + + testRootNode.getSession().save(); + testNode.checkin(); + + // create a version 1.1 and remove reference + testNode.checkout(); + testNode.getProperty(propertyName1).remove(); + testRootNode.getSession().save(); + testNode.checkin(); + + // check if reference is returned + boolean nodeToBeReferencedIsReference = false; + PropertyIterator propIter = nodeToBeReferenced.getReferences(); + while (propIter.hasNext()) { + nodeToBeReferencedIsReference = true; + fail("Reference found in version."); + // not successful + } + // references in versions should not be found + assertFalse(nodeToBeReferencedIsReference); + } + + private void initTestNodes() throws RepositoryException, + NotExecutableException { + // create a versionable node with reference property + testNode = testRootNode.addNode(nodeName1, versionableNodeType); + ensureMixinType(testNode, mixVersionable); + + // node to be referenced, does not have to be versionable + nodeToBeReferenced = testRootNode.addNode(nodeName2, versionableNodeType); + testRootNode.getSession().save(); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetVersionableUUIDTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetVersionableUUIDTest.java new file mode 100644 index 00000000000..3aa6fe7a4f5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/GetVersionableUUIDTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; + +/** + * GetVersionableUUIDTest provides test methods covering {@link + * VersionHistory#getVersionableUUID()} and {@link VersionHistory#getVersionableIdentifier()}. + * + */ +public class GetVersionableUUIDTest extends AbstractVersionTest { + + /** + * Tests if VersionHistory.getVersionableUUID() returns the uuid of the + * corresponding versionable node. + */ + public void testGetVersionableUUID() throws RepositoryException { + // create version + versionableNode.checkout(); + Version version = versionableNode.checkin(); + + assertEquals("Method getVersionableUUID() must return the UUID of the corresponding Node.", + version.getContainingHistory().getVersionableUUID(), + versionableNode.getUUID()); + } + + /** + * Tests if VersionHistory.getVersionableIdentifier() returns the ID of the + * corresponding versionable node. + * @since JCR 2.9 + */ + public void testGetVersionableIdentifier() throws RepositoryException { + + VersionManager vm = versionableNode.getSession().getWorkspace().getVersionManager(); + vm.checkpoint(versionableNode.getPath()); + + assertEquals("Method getVersionableIdentifier() must return the identifier of the corresponding Node.", + vm.getVersionHistory(versionableNode.getPath()).getVersionableIdentifier(), + versionableNode.getIdentifier()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeActivityTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeActivityTest.java new file mode 100644 index 00000000000..243a9963896 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeActivityTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.test.NotExecutableException; +/** + * MergeActivityTest contains tests dealing with merging activities + * + */ + +public class MergeActivityTest extends AbstractMergeTest { + + String newValue; + + Node activityNode; + + protected void setUp() throws Exception { + super.setUp(); + } + + protected void tearDown() throws Exception { + if (activityNode != null) { + superuser.getWorkspace().getVersionManager().removeActivity(activityNode); + activityNode = null; + } + super.tearDown(); + } + + public void testMergeActivity() throws RepositoryException { + String p1 = testRootNodeW2.getProperty(nodeName1 + "/" + propertyName1).getString(); + String p2 = testRootNodeW2.getProperty(nodeName2 + "/" + propertyName1).getString(); + assertEquals("Cloned node has wrong property on node 1.", nodeName1, p1); + assertEquals("Cloned node has wrong property on node 2.", nodeName2, p2); + + VersionManager vm2 = testRootNodeW2.getSession().getWorkspace().getVersionManager(); + NodeIterator iter = vm2.merge(activityNode); + if (iter.hasNext()) { + StringBuffer failed = new StringBuffer(); + while (iter.hasNext()) { + failed.append(iter.nextNode().getPath()); + failed.append(", "); + } + fail("Merge must not fail. failed nodes: " + failed); + return; + } + + p1 = testRootNodeW2.getProperty(nodeName1 + "/" + propertyName1).getString(); + p2 = testRootNodeW2.getProperty(nodeName2 + "/" + propertyName1).getString(); + assertEquals("Activity merge did not restore property on node 1.", newValue, p1); + assertEquals("Activity merge did not restore property on node 2.", newValue, p2); + } + + /** + * initialize a versionable node on default and second workspace + */ + protected void initNodes() throws RepositoryException, NotExecutableException { + + checkSupportedOption(Repository.OPTION_ACTIVITIES_SUPPORTED); + + VersionManager versionManager = testRootNode.getSession().getWorkspace().getVersionManager(); + + // create 2 a versionable nodes + Node node1 = testRootNode.addNode(nodeName1, versionableNodeType); + node1.setProperty(propertyName1, nodeName1); + String path1 = node1.getPath(); + Node node2 = testRootNode.addNode(nodeName2, versionableNodeType); + node2.setProperty(propertyName1, nodeName2); + String path2 = node2.getPath(); + + // save default workspace + testRootNode.getSession().save(); + versionManager.checkin(path1); + versionManager.checkin(path2); + + log.println("test nodes created successfully on " + workspace.getName()); + + // clone the newly created node from src workspace into second workspace + // todo clone on testRootNode does not seem to work. + // workspaceW2.clone(workspace.getName(), testRootNode.getPath(), testRootNode.getPath(), true); + workspaceW2.clone(workspace.getName(), path1, path1, true); + workspaceW2.clone(workspace.getName(), path2, path2, true); + + testRootNodeW2 = (Node) superuserW2.getItem(testRoot); + + activityNode = versionManager.createActivity("foobar"); + versionManager.setActivity(activityNode); + + // update properties on source nodes + + versionManager.checkout(path1); + versionManager.checkout(path2); + + newValue = String.valueOf(System.currentTimeMillis()); + node1.setProperty(propertyName1, newValue); + node2.setProperty(propertyName1, newValue); + testRootNode.getSession().save(); + + versionManager.checkin(path1); + versionManager.checkin(path2); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeCancelMergeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeCancelMergeTest.java new file mode 100644 index 00000000000..63b15c76b27 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeCancelMergeTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +/** + * MergeCancelMergeTest contains tests dealing with nodes on which + * cancelMerge is called. + * + */ +public class MergeCancelMergeTest extends AbstractMergeTest { + + /** + * node to merge + */ + Node nodeToMerge; + + protected void setUp() throws Exception { + super.setUp(); + + nodeToMerge = testRootNodeW2.getNode(nodeName1); + // node has to be checked out while merging + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + versionManager.checkout(nodeToMerge.getPath()); + } + + protected void tearDown() throws Exception { + nodeToMerge = null; + super.tearDown(); + } + + /** + * Merge.cancelMerge(V): has the effect of removing the reference to V' from + * the jcr:mergeFailed property of N.
        without adding it to + * jcr:predecessors.
        Branches will not be joined.
        + */ + @SuppressWarnings("deprecation") + public void testMergeNodeCancelMerge() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + originalNode.checkout(); + originalNode.checkin(); + + // second workspace + nodeToMerge.checkin(); + + // "merge" the clonedNode with the newNode from the default workspace + nodeToMerge.checkout(); + nodeToMerge.merge(workspace.getName(), true); + + // get predecessors + Version[] predecessors = nodeToMerge.getBaseVersion().getPredecessors(); + // get mergeFailed property + Property mergeFailedProperty = nodeToMerge.getProperty(jcrMergeFailed); + Value[] mergeFailedReferences = mergeFailedProperty.getValues(); + + for (int i = 0; i < mergeFailedReferences.length; i++) { + String uuid = mergeFailedReferences[i].getString(); + nodeToMerge.cancelMerge((Version) superuser.getNodeByUUID(uuid)); + } + + // check predecessors - unchanged + Version[] predecessorsAfterCancel = nodeToMerge.getBaseVersion().getPredecessors(); + assertTrue(predecessors.length == predecessorsAfterCancel.length); + + // check mergeFailed property - reference removed + if (nodeToMerge.hasProperty(jcrMergeFailed)) { + Property mergeFailedPropertyAfterCancelMerge = nodeToMerge.getProperty(jcrMergeFailed); + Value[] mergeFailedReferencesAfterCancelMerge = mergeFailedPropertyAfterCancelMerge.getValues(); + assertTrue(mergeFailedReferences.length > mergeFailedReferencesAfterCancelMerge.length); + } + } + + /** + * initialize a versionable node on default and second workspace + */ + protected void initNodes() throws RepositoryException { + + VersionManager versionManager = testRootNode.getSession().getWorkspace().getVersionManager(); + + // create a versionable node + // nodeName1 + Node topVNode = testRootNode.addNode(nodeName1, versionableNodeType); + topVNode.setProperty(propertyName1, topVNode.getName()); + String path = topVNode.getPath(); + + // save default workspace + testRootNode.getSession().save(); + versionManager.checkin(path); + versionManager.checkout(path); + + log.println("test nodes created successfully on " + workspace.getName()); + + // clone the newly created node from src workspace into second workspace + workspaceW2.clone(workspace.getName(), topVNode.getPath(), topVNode.getPath(), true); + log.println(topVNode.getPath() + " cloned on " + superuserW2.getWorkspace().getName() + " at " + topVNode.getPath()); + + testRootNodeW2 = (Node) superuserW2.getItem(testRoot); + } + + /** + * Merge.cancelMerge(V): has the effect of removing the reference to V' from + * the jcr:mergeFailed property of N.
        without adding it to + * jcr:predecessors.
        Branches will not be joined.
        + */ + public void testMergeNodeCancelMergeJcr2() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + vmWsp1.checkout(originalPath); + vmWsp1.checkin(originalPath); + + // second workspace + VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + vmWsp2.checkin(path); + + // "merge" the clonedNode with the newNode from the default workspace + vmWsp2.checkout(path); + vmWsp2.merge(path, workspace.getName(), true); + + // get predecessors + Version[] predecessors = vmWsp2.getBaseVersion(path).getPredecessors(); + // get mergeFailed property + Property mergeFailedProperty = nodeToMerge.getProperty(jcrMergeFailed); + Value[] mergeFailedReferences = mergeFailedProperty.getValues(); + + for (int i = 0; i < mergeFailedReferences.length; i++) { + String id = mergeFailedReferences[i].getString(); + vmWsp2.cancelMerge(path, (Version) superuser.getNodeByIdentifier(id)); + } + + // check predecessors - unchanged + Version[] predecessorsAfterCancel = vmWsp2.getBaseVersion(path).getPredecessors(); + assertTrue(predecessors.length == predecessorsAfterCancel.length); + + // check mergeFailed property - reference removed + if (nodeToMerge.hasProperty(jcrMergeFailed)) { + Property mergeFailedPropertyAfterCancelMerge = nodeToMerge.getProperty(jcrMergeFailed); + Value[] mergeFailedReferencesAfterCancelMerge = mergeFailedPropertyAfterCancelMerge.getValues(); + assertTrue(mergeFailedReferences.length > mergeFailedReferencesAfterCancelMerge.length); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeCheckedoutSubNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeCheckedoutSubNodeTest.java new file mode 100644 index 00000000000..bcd1d161239 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeCheckedoutSubNodeTest.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.test.api.version; + +import javax.jcr.MergeException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionManager; + +/** + * MergeCheckedoutSubNodeTest contains tests dealing with + * checked-out nodes in the subtree of the node on which merge is called. + * + */ +public class MergeCheckedoutSubNodeTest extends AbstractMergeTest { + + /** + * node to merge + */ + Node nodeToMerge; + + protected void setUp() throws Exception { + super.setUp(); + + nodeToMerge = testRootNodeW2.getNode(nodeName1); + // node has to be checked out while merging + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + versionManager.checkout(nodeToMerge.getPath()); + } + + protected void tearDown() throws Exception { + nodeToMerge = null; + super.tearDown(); + } + + /** + * Node.merge(): If V' of a versionable subnode N' in the source workspace + * is a successor of V (the base version of a subnode N in this workspace), + * calling merge must fail. + */ + public void testFailIfCorrespondingNodeIsSuccessor() throws RepositoryException { + // make V' of a subnode N' in source workspace be a successor version of + // the base version of the corresponding subnode. + Node n = testRootNode.getNode(nodeName1 + "/" + nodeName2); + n.checkout(); + n.checkin(); + + n.checkout(); + + try { + // merge, besteffort set to false to stop at the first failure + nodeToMerge.merge(workspace.getName(), false); + fail("Merging a checkedout node if the version V' of the corresponding node is a successor of this node's base version must fail."); + + } catch (MergeException e) { + // success + } + } + + /** + * VersionManager.merge(): If V' of a versionable subnode N' in the source workspace + * is a successor of V (the base version of a subnode N in this workspace), + * calling merge must fail. + */ + public void testFailIfCorrespondingNodeIsSuccessorJcr2() throws RepositoryException { + // make V' of a subnode N' in source workspace be a successor version of + // the base version of the corresponding subnode. + Node n = testRootNode.getNode(nodeName1 + "/" + nodeName2); + VersionManager versionManager = n.getSession().getWorkspace().getVersionManager(); + String path = n.getPath(); + versionManager.checkout(path); + versionManager.checkin(path); + versionManager.checkout(path); + + try { + // merge, besteffort set to false to stop at the first failure + nodeToMerge.getSession().getWorkspace().getVersionManager().merge(nodeToMerge.getPath(), workspace.getName(), false); + fail("Merging a checkedout node if the version V' of the corresponding node is a successor of this node's base version must fail."); + + } catch (MergeException e) { + // success + } + } + + /** + * Node.merge(): If V' of a versionable subnode N' in the source workspace + * is a predeccessor of V or V' identical to V (the base version of a + * subnode N in this workspace), calling merge must be leave. + */ + public void testLeaveIfCorrespondingNodeIsPredeccessor() throws RepositoryException { + // make V' of a subnode N' in source workspace be a predeccessor version of + // the base version of the corresponding subnode. + Node n = testRootNodeW2.getNode(nodeName1 + "/" + nodeName2); + n.checkout(); + n.setProperty(propertyName1, CHANGED_STRING); + testRootNodeW2.save(); + n.checkin(); + + n.checkout(); + + // merge, besteffort set to false to stop at the first failure + nodeToMerge.merge(workspace.getName(), false); + + // check if subnode has status "leave" + assertTrue(n.getProperty(propertyName1).getString().equals(CHANGED_STRING)); + } + + /** + * VersionManager.merge(): If V' of a versionable subnode N' in the source workspace + * is a predeccessor of V or V' identical to V (the base version of a + * subnode N in this workspace), calling merge must be leave. + */ + public void testLeaveIfCorrespondingNodeIsPredeccessorJcr2() throws RepositoryException { + // make V' of a subnode N' in source workspace be a predeccessor version of + // the base version of the corresponding subnode. + Node n = testRootNodeW2.getNode(nodeName1 + "/" + nodeName2); + VersionManager versionManager = n.getSession().getWorkspace().getVersionManager(); + String path = n.getPath(); + versionManager.checkout(path); + n.setProperty(propertyName1, CHANGED_STRING); + testRootNodeW2.getSession().save(); + versionManager.checkin(path); + versionManager.checkout(path); + + // merge, besteffort set to false to stop at the first failure + nodeToMerge.getSession().getWorkspace().getVersionManager().merge(nodeToMerge.getPath(), workspace.getName(), false); + + // check if subnode has status "leave" + assertTrue(n.getProperty(propertyName1).getString().equals(CHANGED_STRING)); + } + + /** + * initialize a two-step-hierarchy on default and second workspace + */ + protected void initNodes() throws RepositoryException { + // create a versionable parent node + // nodeName1 + Node topVNode = testRootNode.addNode(nodeName1, versionableNodeType); + topVNode.setProperty(propertyName1, topVNode.getName()); + + // create a versionable sub node + // nodeName1/nodeName2 + Node subNvNode = topVNode.addNode(nodeName2, versionableNodeType); + subNvNode.setProperty(propertyName1, subNvNode.getName()); + + // save default workspace + testRootNode.getSession().save(); + + log.println("test nodes created successfully on " + workspace.getName()); + + // clone the newly created node from src workspace into second workspace + workspaceW2.clone(workspace.getName(), topVNode.getPath(), topVNode.getPath(), true); + log.println(topVNode.getPath() + " cloned on " + superuserW2.getWorkspace().getName() + " at " + topVNode.getPath()); + + testRootNodeW2 = (Node) superuserW2.getItem(testRoot); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeDoneMergeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeDoneMergeTest.java new file mode 100644 index 00000000000..2e9b294018e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeDoneMergeTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +/** + * MergeDoneMergeTest contains test dealing with nodes on which + * doneMerge is called. + * + */ +public class MergeDoneMergeTest extends AbstractMergeTest { + /** + * node to merge + */ + Node nodeToMerge; + + protected void setUp() throws Exception { + super.setUp(); + + nodeToMerge = testRootNodeW2.getNode(nodeName1); + // node has to be checked out while merging + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + versionManager.checkout(nodeToMerge.getPath()); + + } + + protected void tearDown() throws Exception { + nodeToMerge = null; + super.tearDown(); + } + + /** + * Node.doneMerge(V) throws VersionException if V is not among the Vs in the + * jcr:mergeFailed prop.
        with adding it to jcr:predecessors.
        + * Branches will be joined.
        + */ + public void testMergeNodeDoneMerge() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + originalNode.checkout(); + originalNode.checkin(); + + // second workspace + nodeToMerge.checkin(); + + // "merge" the clonedNode with the newNode from the default workspace + nodeToMerge.checkout(); + nodeToMerge.merge(workspace.getName(), true); + + // get predecessors + Version[] predecessors = nodeToMerge.getBaseVersion().getPredecessors(); + // get mergeFailed property + Property mergeFailedProperty = nodeToMerge.getProperty(jcrMergeFailed); + Value[] mergeFailedReferences = mergeFailedProperty.getValues(); + + for (int i = 0; i < mergeFailedReferences.length; i++) { + String uuid = mergeFailedReferences[i].getString(); + nodeToMerge.doneMerge((Version) superuser.getNodeByUUID(uuid)); + } + + // check mergeFailed property - reference moved to predecessor + if (nodeToMerge.hasProperty(jcrMergeFailed)) { + Property mergeFailedPropertyAfterCancelMerge = nodeToMerge.getProperty(jcrMergeFailed); + Value[] mergeFailedReferencesAfterCancelMerge = mergeFailedPropertyAfterCancelMerge.getValues(); + assertTrue(mergeFailedReferences.length > mergeFailedReferencesAfterCancelMerge.length); + } + } + + /** + * VersionManager.doneMerge(V) throws VersionException if V is not among the Vs in the + * jcr:mergeFailed prop.
        with adding it to jcr:predecessors.
        + * Branches will be joined.
        + */ + public void testMergeNodeDoneMergeJcr2() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + vmWsp1.checkout(originalPath); + vmWsp1.checkin(originalPath); + + // second workspace + VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + vmWsp2.checkin(path); + + // "merge" the clonedNode with the newNode from the default workspace + vmWsp2.checkout(path); + vmWsp2.merge(path, workspace.getName(), true); + + // get predecessors + Version[] predecessors = nodeToMerge.getBaseVersion().getPredecessors(); + // get mergeFailed property + Property mergeFailedProperty = nodeToMerge.getProperty(jcrMergeFailed); + Value[] mergeFailedReferences = mergeFailedProperty.getValues(); + + for (int i = 0; i < mergeFailedReferences.length; i++) { + String id = mergeFailedReferences[i].getString(); + vmWsp2.doneMerge(path, (Version) superuser.getNodeByIdentifier(id)); + } + + // check mergeFailed property - reference moved to predecessor + if (nodeToMerge.hasProperty(jcrMergeFailed)) { + Property mergeFailedPropertyAfterCancelMerge = nodeToMerge.getProperty(jcrMergeFailed); + Value[] mergeFailedReferencesAfterCancelMerge = mergeFailedPropertyAfterCancelMerge.getValues(); + assertTrue(mergeFailedReferences.length > mergeFailedReferencesAfterCancelMerge.length); + } + } + + /** + * initialize a versionable node on default and second workspace + */ + protected void initNodes() throws RepositoryException { + + VersionManager versionManager = testRootNode.getSession().getWorkspace().getVersionManager(); + + // create a versionable node + // nodeName1 + Node topVNode = testRootNode.addNode(nodeName1, versionableNodeType); + topVNode.setProperty(propertyName1, topVNode.getName()); + String path = topVNode.getPath(); + + // save default workspace + testRootNode.getSession().save(); + versionManager.checkin(path); + versionManager.checkout(path); + + log.println("test nodes created successfully on " + workspace.getName()); + + // clone the newly created node from src workspace into second workspace + workspaceW2.clone(workspace.getName(), topVNode.getPath(), topVNode.getPath(), true); + log.println(topVNode.getPath() + " cloned on " + superuserW2.getWorkspace().getName() + " at " + topVNode.getPath()); + + testRootNodeW2 = (Node) superuserW2.getItem(testRoot); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNodeIteratorTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNodeIteratorTest.java new file mode 100644 index 00000000000..0d099773078 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNodeIteratorTest.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.test.api.version; + +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.VersionManager; +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * MergeNodeIteratorTest tests if Node.merge(String, boolean) if + * bestEffort is true returns a NodeIterator over all versionalbe nodes in the + * subtree that received a merge result of fail. + * + */ +public class MergeNodeIteratorTest extends AbstractMergeTest { + + Node expectedFailedNodes[] = new Node[3]; + + protected void tearDown() throws Exception { + for (int i = 0; i < expectedFailedNodes.length; i++) { + expectedFailedNodes[i] = null; + + } + super.tearDown(); + } + + /** + * Tests if Node.merge() when bestEffort is true returns a NodeIterator + * containing all nodes that received a fail. + */ + public void testNodeIterator() throws RepositoryException { + + Node nodeToMerge = testRootNodeW2.getNode(nodeName1); + + NodeIterator failedNodes1 = nodeToMerge.merge(workspace.getName(), true); + + List nodeList = new ArrayList(); + while (failedNodes1.hasNext()) { + nodeList.add(failedNodes1.nextNode()); + } + + assertEquals("Node.merge() does not return a NodeIterator with " + + "expected number of elements.", + expectedFailedNodes.length, + nodeList.size()); + + // re-aquire iterator, has been consumed to get size + Iterator failedNodes2 = nodeList.iterator(); + compareReturnedWithExpected: + while (failedNodes2.hasNext()) { + String path = failedNodes2.next().getPath(); + for (int i = 0; i < expectedFailedNodes.length; i++) { + if (expectedFailedNodes[i] != null) { + String expectedPath = expectedFailedNodes[i].getPath(); + if (path.equals(expectedPath)) { + // to assure every failed node appears only once in the + // NodeIterator, set each found expected node to null + expectedFailedNodes[i] = null; + continue compareReturnedWithExpected; + } + } + } + fail("Node.merge() must return a NodeIterator over all " + + "nodes that did receive a result of fail."); + } + } + + /** + * Tests if VersionManager.merge() when bestEffort is true returns a NodeIterator + * containing all nodes that received a fail. + */ + public void testNodeIteratorJcr2() throws RepositoryException { + + Node nodeToMerge = testRootNodeW2.getNode(nodeName1); + + NodeIterator failedNodes1 = nodeToMerge.getSession().getWorkspace().getVersionManager().merge( + nodeToMerge.getPath(), workspace.getName(), true); + + List nodeList = new ArrayList(); + while (failedNodes1.hasNext()) { + nodeList.add(failedNodes1.nextNode()); + } + + assertEquals("Node.merge() does not return a NodeIterator with " + + "expected number of elements.", + expectedFailedNodes.length, + nodeList.size()); + + // re-aquire iterator, has been consumed to get size + Iterator failedNodes2 = nodeList.iterator(); + compareReturnedWithExpected: + while (failedNodes2.hasNext()) { + String path = failedNodes2.next().getPath(); + for (int i = 0; i < expectedFailedNodes.length; i++) { + if (expectedFailedNodes[i] != null) { + String expectedPath = expectedFailedNodes[i].getPath(); + if (path.equals(expectedPath)) { + // to assure every failed node appears only once in the + // NodeIterator, set each found expected node to null + expectedFailedNodes[i] = null; + continue compareReturnedWithExpected; + } + } + } + fail("Node.merge() must return a NodeIterator over all " + + "nodes that did receive a result of fail."); + } + } + + /** + * initialize some versionable nodes on default and second workspace + */ + protected void initNodes() throws RepositoryException { + + // create some versionable node in default workspace (WS1) + + VersionManager versionManager = testRootNode.getSession().getWorkspace().getVersionManager(); + + Node mergeRootNode = testRootNode.addNode(nodeName1, versionableNodeType); + + Node nodeWS1_1 = mergeRootNode.addNode(nodeName1, versionableNodeType); + Node nodeWS1_1Sub1 = nodeWS1_1.addNode(nodeName1, versionableNodeType); + Node nodeWS1_2 = mergeRootNode.addNode(nodeName2, versionableNodeType); + Node nodeWS1_2Sub1 = nodeWS1_2.addNode(nodeName1, versionableNodeType); + Node nodeWS1_3 = mergeRootNode.addNode(nodeName3, versionableNodeType); + + testRootNode.getSession().save(); + + versionManager.checkin(nodeWS1_1.getPath()); // create version 1.0 + versionManager.checkout(nodeWS1_1.getPath()); + + versionManager.checkin(nodeWS1_1Sub1.getPath()); // create version 1.0 + versionManager.checkout(nodeWS1_1Sub1.getPath()); + + versionManager.checkin(nodeWS1_2.getPath()); // create version 1.0 + versionManager.checkout(nodeWS1_2.getPath()); + + versionManager.checkin(nodeWS1_2Sub1.getPath()); // create version 1.0 + versionManager.checkout(nodeWS1_2Sub1.getPath()); + + versionManager.checkin(nodeWS1_3.getPath()); // create version 1.0 + versionManager.checkout(nodeWS1_3.getPath()); + + workspaceW2.clone(workspace.getName(), mergeRootNode.getPath(), mergeRootNode.getPath(), true); + + // get nodes in workspace 2 + Node nodeWS2_1 = (Node) superuserW2.getItem(nodeWS1_1.getPath()); + Node nodeWS2_2 = (Node) superuserW2.getItem(nodeWS1_2.getPath()); + Node nodeWS2_2Sub1 = (Node) superuserW2.getItem(nodeWS1_2Sub1.getPath()); + + // create version branches for some of the nodes + + versionManager.checkin(nodeWS2_1.getPath()); // create version 1.1 + versionManager.checkin(nodeWS1_1.getPath()); // create version 1.0.1 + + versionManager.checkin(nodeWS2_2.getPath()); // create version 1.1 + versionManager.checkin(nodeWS1_2.getPath()); // create version 1.0.1 + + versionManager.checkin(nodeWS2_2Sub1.getPath()); // create version 1.1 + versionManager.checkin(nodeWS1_2Sub1.getPath()); // create version 1.0.1 + + // set the nodes with version branches in expectedFailedNodes + expectedFailedNodes[0] = nodeWS1_1; + expectedFailedNodes[1] = nodeWS1_2; + expectedFailedNodes[2] = nodeWS1_2Sub1; + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNodeTest.java new file mode 100644 index 00000000000..ffe063bb4d5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNodeTest.java @@ -0,0 +1,581 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionManager; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Property; +import javax.jcr.Value; +import javax.jcr.MergeException; +import javax.jcr.Session; +import javax.jcr.Repository; +import javax.jcr.lock.LockException; + +/** + * MergeNodeTest contains tests dealing with general merge node + * calls. + * + */ + +public class MergeNodeTest extends AbstractMergeTest { + + /** + * node to merge + */ + Node nodeToMerge; + + protected void setUp() throws Exception { + super.setUp(); + + nodeToMerge = testRootNodeW2.getNode(nodeName1); + // node has to be checked out while merging + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + versionManager.checkout(nodeToMerge.getPath()); + + } + + protected void tearDown() throws Exception { + nodeToMerge = null; + super.tearDown(); + } + + /** + * Node.merge(): InvalidItemStateException if unsaved changes within the + * current Session
        + */ + @SuppressWarnings("deprecation") + public void testMergeNodeWithUnsavedStates() throws RepositoryException { + // set a property and do not save workspace + nodeToMerge.setProperty(propertyName1, CHANGED_STRING); + try { + nodeToMerge.merge(workspace.getName(), false); + fail("InvalidItemStateException if unsaved changes within the current Session was expected."); + } catch (InvalidItemStateException e) { + // success + } + } + + /** + * VersionManager.merge(): InvalidItemStateException if unsaved changes within the + * current Session
        + */ + public void testMergeNodeWithUnsavedStatesJcr2() throws RepositoryException { + // set a property and do not save workspace + nodeToMerge.setProperty(propertyName1, CHANGED_STRING); + try { + nodeToMerge.getSession().getWorkspace().getVersionManager().merge( + nodeToMerge.getPath(), workspace.getName(), false); + fail("InvalidItemStateException if unsaved changes within the current Session was expected."); + } catch (InvalidItemStateException e) { + // success + } + } + + /** + * Perform a merge on a node with a unkwnown workspacename + */ + @SuppressWarnings("deprecation") + public void testMergeUnknownWorkspaceName() throws RepositoryException { + try { + nodeToMerge.merge(getNonExistingWorkspaceName(superuser), false); + } catch (NoSuchWorkspaceException e) { + // success expected exception + } + } + + /** + * Perform a merge on a node with a unkwnown workspacename + */ + public void testMergeUnknownWorkspaceNameJcr2() throws RepositoryException { + try { + nodeToMerge.getSession().getWorkspace().getVersionManager().merge( + nodeToMerge.getPath(), getNonExistingWorkspaceName(superuser), false); + } catch (NoSuchWorkspaceException e) { + // success expected exception + } + } + + /** + * Node.merge(): If this node does not have a corresponding node in the + * indicated workspace
        then the merge method returns quietly and no + * changes are made.
        + */ + @SuppressWarnings("deprecation") + public void testMergeNodeNonCorrespondingNode() throws RepositoryException { + // create new node - this node has no corresponding node in default workspace + Node subNode = nodeToMerge.addNode(nodeName3, versionableNodeType); + subNode.setProperty(propertyName1, CHANGED_STRING); + superuserW2.save(); + subNode.checkin(); + + subNode.merge(workspace.getName(), true); + assertTrue(subNode.getProperty(propertyName1).getString().equals(CHANGED_STRING)); + } + + /** + * VersionManager.merge(): If this node does not have a corresponding node in the + * indicated workspace
        then the merge method returns quietly and no + * changes are made.
        + */ + public void testMergeNodeNonCorrespondingNodeJcr2() throws RepositoryException { + // create new node - this node has no corresponding node in default workspace + Node subNode = nodeToMerge.addNode(nodeName3, versionableNodeType); + subNode.setProperty(propertyName1, CHANGED_STRING); + superuserW2.save(); + VersionManager versionManager = subNode.getSession().getWorkspace().getVersionManager(); + String path = subNode.getPath(); + versionManager.checkin(path); + + versionManager.merge(path, workspace.getName(), true); + assertTrue(subNode.getProperty(propertyName1).getString().equals(CHANGED_STRING)); + } + + /** + * Node.merge(): versionable subNode N checked-in: If V is neither a + * successor of, predecessor of, nor identical with V', then the merge + * result for N is failed
        + */ + @SuppressWarnings("deprecation") + public void testMergeNodeVersionAmbiguous() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + originalNode.checkout(); + originalNode.checkin(); + + // second workspace + nodeToMerge.checkin(); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to false to stop at the first failure + try { + nodeToMerge.checkout(); + nodeToMerge.merge(workspace.getName(), false); + fail("Node has ambigous versions. Merge must throw a MergeException"); + } catch (MergeException e) { + // success if the merge exception thrown + } + } + + /** + * VersionManager.merge(): versionable subNode N checked-in: If V is neither a + * successor of, predecessor of, nor identical with V', then the merge + * result for N is failed
        + */ + public void testMergeNodeVersionAmbiguousJcr2() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + vmWsp1.checkout(originalPath); + vmWsp1.checkin(originalPath); + + // second workspace + VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + vmWsp2.checkin(path); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to false to stop at the first failure + try { + vmWsp2.checkout(path); + vmWsp2.merge(path, workspace.getName(), false); + fail("Node has ambigous versions. Merge must throw a MergeException"); + } catch (MergeException e) { + // success if the merge exception thrown + } + } + + /** + * Node.merge(): bestEffort is true > any merge-failure (represented by the + * version in the workspace) is reported in the jcrMergeFailed property
        + */ + @SuppressWarnings("deprecation") + public void testMergeNodeBestEffortTrueCheckMergeFailedProperty() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + originalNode.checkout(); + originalNode.checkin(); + + // second workspace + nodeToMerge.checkin(); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to true to report all failures + nodeToMerge.checkout(); + nodeToMerge.merge(workspace.getName(), true); + + // success merge exception was raised as expected + // jcrMergeFailed should contains reference to the V' as it is a different branche + String expectedReferenceUUID = originalNode.getBaseVersion().getUUID(); + Property mergeFailedProperty = nodeToMerge.getProperty(jcrMergeFailed); + Value[] references = mergeFailedProperty.getValues(); + boolean referenceFound = false; + if (references != null) { + for (int i = 0; i < references.length; i++) { + String referenceUUID = references[i].getString(); + if (referenceUUID.equals(expectedReferenceUUID)) { + referenceFound = true; + break; // it's not necessary to loop thru all the references + } + } + + assertTrue("reference to expected version that give the failure wasnt found in the mergeFailed", referenceFound); + } + } + + /** + * VersionManager.merge(): bestEffort is true > any merge-failure (represented by the + * version in the workspace) is reported in the jcrMergeFailed property
        + */ + public void testMergeNodeBestEffortTrueCheckMergeFailedPropertyJcr2() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + vmWsp1.checkout(originalPath); + vmWsp1.checkin(originalPath); + + // second workspace + VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + vmWsp2.checkin(path); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to true to report all failures + vmWsp2.checkout(path); + vmWsp2.merge(path, workspace.getName(), true); + + // success merge exception was raised as expected + // jcrMergeFailed should contains reference to the V' as it is a different branche + String expectedReferenceUUID = originalNode.getBaseVersion().getUUID(); + Property mergeFailedProperty = nodeToMerge.getProperty(jcrMergeFailed); + Value[] references = mergeFailedProperty.getValues(); + boolean referenceFound = false; + if (references != null) { + for (int i = 0; i < references.length; i++) { + String referenceUUID = references[i].getString(); + if (referenceUUID.equals(expectedReferenceUUID)) { + referenceFound = true; + break; // it's not necessary to loop thru all the references + } + } + + assertTrue("reference to expected version that give the failure wasnt found in the mergeFailed", referenceFound); + } + } + + /** + * if mergeFailedProperty is present > VersionException
        + */ + @SuppressWarnings("deprecation") + public void disable_testMergeNodeForceFailure() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + originalNode.checkout(); + originalNode.checkin(); + + // second workspace + nodeToMerge.checkin(); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to true to report all failures + nodeToMerge.checkout(); + nodeToMerge.merge(workspace.getName(), true); + + try { + nodeToMerge.merge(workspace.getName(), true); + fail("Merge failed for node in earlier merge operations. Because the mergeFailedProperty is present, merge must throw a VersionException"); + } catch (VersionException e) { + // success version exception expected + } + } + + /** + * if mergeFailedProperty is present > VersionException
        + */ + public void disable_testMergeNodeForceFailureJcr2() throws RepositoryException { + // create 2 independent versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + vmWsp1.checkout(originalPath); + vmWsp1.checkin(originalPath); + + // second workspace + VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + vmWsp2.checkin(path); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to true to report all failures + vmWsp2.checkout(path); + vmWsp2.merge(path, workspace.getName(), true); + + try { + vmWsp2.merge(path, workspace.getName(), true); + fail("Merge failed for node in earlier merge operations. Because the mergeFailedProperty is present, merge must throw a VersionException"); + } catch (VersionException e) { + // success version exception expected + } + } + + /** + * Node.merge(): bestEffort is false and any merge fails a MergeException is + * thrown.
        + */ + @SuppressWarnings("deprecation") + public void testMergeNodeBestEffortFalse() throws RepositoryException { + /// create successor versions for a node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + originalNode.checkout(); + originalNode.checkin(); + + // "merge" the clonedNode with the newNode from the default workspace + // merge, besteffort set to false + try { + nodeToMerge.merge(workspace.getName(), false); + fail("bestEffort is false and any merge should throw a MergeException."); + } catch (MergeException e) { + // successful + } + } + + /** + * VersionManager.merge(): bestEffort is false and any merge fails a MergeException is + * thrown.
        + */ + public void testMergeNodeBestEffortFalseJcr2() throws RepositoryException { + /// create successor versions for a node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + vmWsp1.checkout(originalPath); + vmWsp1.checkin(originalPath); + + // "merge" the clonedNode with the newNode from the default workspace + // merge, besteffort set to false + try { + nodeToMerge.getSession().getWorkspace().getVersionManager().merge( + nodeToMerge.getPath(), workspace.getName(), false); + fail("bestEffort is false and any merge should throw a MergeException."); + } catch (MergeException e) { + // successful + } + } + + /** + * A MergeVersionException is thrown if bestEffort is false and a + * versionable node is encountered whose corresponding node's base version + * is on a divergent branch from this node's base version. + */ + @SuppressWarnings("deprecation") + public void testMergeNodeBestEffortFalseAmbiguousVersions() throws RepositoryException { + /// create 2 independent base versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + originalNode.checkout(); + originalNode.checkin(); + + // second workspace + nodeToMerge.checkin(); + + // "merge" the clonedNode with the newNode from the default workspace + nodeToMerge.checkout(); + + // merge, besteffort set to false + try { + nodeToMerge.merge(workspace.getName(), false); + fail("BestEffort is false and corresponding node's version is ambiguous. Merge should throw a MergeException."); + } catch (MergeException e) { + // successful + } + } + + /** + * A MergeVersionException is thrown if bestEffort is false and a + * versionable node is encountered whose corresponding node's base version + * is on a divergent branch from this node's base version. + */ + public void testMergeNodeBestEffortFalseAmbiguousVersionsJcr2() throws RepositoryException { + /// create 2 independent base versions for a node and its corresponding node + // so merge fails for this node + + // default workspace + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager vmWsp1 = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + vmWsp1.checkout(originalPath); + vmWsp1.checkin(originalPath); + + // second workspace + VersionManager vmWsp2 = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + vmWsp2.checkin(path); + + // "merge" the clonedNode with the newNode from the default workspace + vmWsp2.checkout(path); + + // merge, besteffort set to false + try { + vmWsp2.merge(path, workspace.getName(), false); + fail("BestEffort is false and corresponding node's version is ambiguous. Merge should throw a MergeException."); + } catch (MergeException e) { + // successful + } + } + + /** + * Tests if a {@link LockException} is thrown when merge is called on a + * locked node. + * @throws NotExecutableException if repository does not support locking. + */ + @SuppressWarnings("deprecation") + public void disable_testMergeLocked() + throws NotExecutableException, RepositoryException { + + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Locking is not supported."); + } + + // try to make nodeToMerge lockable if it is not + ensureMixinType(nodeToMerge, mixLockable); + nodeToMerge.getParent().save(); + + // lock the node + // remove first slash of path to get rel path to root + String pathRelToRoot = nodeToMerge.getPath().substring(1); + // access node through another session to lock it + Session session2 = getHelper().getSuperuserSession(); + try { + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.lock(false, false); + + try { + nodeToMerge.merge(workspace.getName(), false); + fail("merge must throw a LockException if applied on a " + + "locked node"); + } catch (LockException e) { + // success + } + + node2.unlock(); + } finally { + session2.logout(); + } + } + + /** + * Tests if a {@link LockException} is thrown when merge is called on a + * locked node. + * @throws NotExecutableException if repository does not support locking. + */ + public void disable_testMergeLockedJcr2() + throws NotExecutableException, RepositoryException { + + if (!isSupported(Repository.OPTION_LOCKING_SUPPORTED)) { + throw new NotExecutableException("Locking is not supported."); + } + + // try to make nodeToMerge lockable if it is not + ensureMixinType(nodeToMerge, mixLockable); + nodeToMerge.getParent().getSession().save(); + + // lock the node + // remove first slash of path to get rel path to root + String pathRelToRoot = nodeToMerge.getPath().substring(1); + // access node through another session to lock it + Session session2 = getHelper().getSuperuserSession(); + try { + Node node2 = session2.getRootNode().getNode(pathRelToRoot); + node2.getSession().getWorkspace().getLockManager().lock(node2.getPath(), false, false, 60, ""); + + try { + nodeToMerge.getSession().getWorkspace().getVersionManager().merge( + nodeToMerge.getPath(), workspace.getName(), false); + fail("merge must throw a LockException if applied on a " + + "locked node"); + } catch (LockException e) { + // success + } + + node2.getSession().getWorkspace().getLockManager().unlock(node2.getPath()); + } finally { + session2.logout(); + } + } + + /** + * initialize a versionable node on default and second workspace + */ + protected void initNodes() throws RepositoryException { + + VersionManager versionManager = testRootNode.getSession().getWorkspace().getVersionManager(); + + // create a versionable node + // nodeName1 + Node topVNode = testRootNode.addNode(nodeName1, versionableNodeType); + topVNode.setProperty(propertyName1, topVNode.getName()); + String path = topVNode.getPath(); + + // save default workspace + testRootNode.getSession().save(); + versionManager.checkin(path); + versionManager.checkout(path); + + log.println("test nodes created successfully on " + workspace.getName()); + + // clone the newly created node from src workspace into second workspace + // todo clone on testRootNode does not seem to work. + // workspaceW2.clone(workspace.getName(), testRootNode.getPath(), testRootNode.getPath(), true); + workspaceW2.clone(workspace.getName(), topVNode.getPath(), topVNode.getPath(), true); + log.println(topVNode.getPath() + " cloned on " + superuserW2.getWorkspace().getName() + " at " + topVNode.getPath()); + + testRootNodeW2 = (Node) superuserW2.getItem(testRoot); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNonVersionableSubNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNonVersionableSubNodeTest.java new file mode 100644 index 00000000000..bd2182fe06e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeNonVersionableSubNodeTest.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; + +/** + * MergeNonVersionableSubNodeTest contains test dealing with + * nonversionable nodes in the subtree of the node on which merge is called. + * + */ +public class MergeNonVersionableSubNodeTest extends AbstractMergeTest { + + protected void setUp() throws Exception { + super.setUp(); + } + + /** + * Node.merge(): nonversionable subNode N: if it has no versionable + * ancestor, then it is updated to reflect the state of its corresponding + * node.
        + */ + public void testMergeNodeNonVersionableSubNodeNonVersionableAncestor() throws RepositoryException { + String nodeToMergePath = nodeName1 + "/" + nodeName2 + "/" + nodeName3; + + // node to merge in second workspace + Node nodeToMerge = testRootNodeW2.getNode(nodeToMergePath); + // corresponding node to nodeToMerge in default workspace + Node correspondingNode = testRootNode.getNode(nodeToMergePath); + + // modify value for non'v node in workspace2 so we can check if node in workspace2 after merge is updated + // to reflect the state of its corresponding node in default workspace.... + nodeToMerge.setProperty(propertyName1, CHANGED_STRING); + nodeToMerge.save(); + nodeToMerge.merge(workspace.getName(), true); + + // test if modification on non-v node is done according to corresponding node. + assertTrue(nodeToMerge.getProperty(propertyName1).getString().equals(correspondingNode.getName())); + } + + /** + * VersionManager.merge(): nonversionable subNode N: if it has no versionable + * ancestor, then it is updated to reflect the state of its corresponding + * node.
        + */ + public void testMergeNodeNonVersionableSubNodeNonVersionableAncestorJcr2() throws RepositoryException { + String nodeToMergePath = nodeName1 + "/" + nodeName2 + "/" + nodeName3; + + // node to merge in second workspace + Node nodeToMerge = testRootNodeW2.getNode(nodeToMergePath); + // corresponding node to nodeToMerge in default workspace + Node correspondingNode = testRootNode.getNode(nodeToMergePath); + + // modify value for non'v node in workspace2 so we can check if node in workspace2 after merge is updated + // to reflect the state of its corresponding node in default workspace.... + nodeToMerge.setProperty(propertyName1, CHANGED_STRING); + nodeToMerge.getSession().save(); + nodeToMerge.getSession().getWorkspace().getVersionManager().merge( + nodeToMerge.getPath(), workspace.getName(), true); + + // test if modification on non-v node is done according to corresponding node. + assertTrue(nodeToMerge.getProperty(propertyName1).getString().equals(correspondingNode.getName())); + } + + /** + * Node.merge(): nonversionable subNode N: if the merge result of its + * nearest versionable ancestor is update,
        then it is updated to reflect + * the state of its corresponding node.
        + */ + public void testMergeNodeNonVersionableSubNodeUpdate() throws RepositoryException { + // modify non versionable subnode so we can check if it's updated after merge + String changedString = CHANGED_STRING + System.currentTimeMillis(); + String nvSubNodePath = nodeName2 + "/" + nodeName3; + + // versionable ancestor to merge in first workspace (N) + Node n = testRootNodeW2.getNode(nodeName1); + + // versionable ancestor to merge in second workspace (N') + Node np = testRootNodeW2.getNode(nodeName1); + + // checkout N and make change + n.checkout(); + Node nvSubNode = n.getNode(nvSubNodePath); + nvSubNode.setProperty(propertyName1, changedString); + n.save(); + n.checkin(); + + // merge change into N' + np.merge(workspaceW2.getName(), true); + + // corresponding node to nvSubNode in 2nd workspace + Node nvSubNodeP = np.getNode(nvSubNodePath); + + // test if modification on N was merged into N' subnode + assertTrue(nvSubNodeP.getProperty(propertyName1).getString().equals(changedString)); + } + + /** + * VersionManager.merge(): nonversionable subNode N: if the merge result of its + * nearest versionable ancestor is update,
        then it is updated to reflect + * the state of its corresponding node.
        + */ + public void testMergeNodeNonVersionableSubNodeUpdateJcr2() throws RepositoryException { + // modify non versionable subnode so we can check if it's updated after merge + String changedString = CHANGED_STRING + System.currentTimeMillis(); + String nvSubNodePath = nodeName2 + "/" + nodeName3; + + // versionable ancestor to merge in first workspace (N) + Node n = testRootNodeW2.getNode(nodeName1); + + // versionable ancestor to merge in second workspace (N') + Node np = testRootNodeW2.getNode(nodeName1); + + // checkout N and make change + n.getSession().getWorkspace().getVersionManager().checkout(n.getPath()); + Node nvSubNode = n.getNode(nvSubNodePath); + nvSubNode.setProperty(propertyName1, changedString); + n.getSession().save(); + n.getSession().getWorkspace().getVersionManager().checkin(n.getPath()); + + // merge change into N' + np.getSession().getWorkspace().getVersionManager().merge(np.getPath(), workspaceW2.getName(), true); + + // corresponding node to nvSubNode in 2nd workspace + Node nvSubNodeP = np.getNode(nvSubNodePath); + + // test if modification on N was merged into N' subnode + assertTrue(nvSubNodeP.getProperty(propertyName1).getString().equals(changedString)); + } + + /** + * Node.merge(): nonversionable subNode N: is left unchanged if the nearest + * versionable ancestor has state leave.
        + */ + public void testMergeNodeNonVersionableSubNodeLeave() throws RepositoryException { + // modify non versionable subnode so we can check if it's updated after merge + String changedString = CHANGED_STRING + System.currentTimeMillis(); + String nvSubNodePath = nodeName2 + "/" + nodeName3; + + // versionable ancestor to merge in first workspace (N) + Node n = testRootNodeW2.getNode(nodeName1); + + // versionable ancestor to merge in second workspace (N') + Node np = testRootNodeW2.getNode(nodeName1); + + // checkout N' and make change + np.checkout(); + Node nvSubNodeP = np.getNode(nvSubNodePath); + nvSubNodeP.setProperty(propertyName1, changedString); + np.save(); + np.checkin(); + + // merge into N' + np.merge(workspaceW2.getName(), true); + + // corresponding node to nvSubNode in 2nd workspace + Node nvSubNode = np.getNode(nvSubNodePath); + + // test if modification on N' was not modified + assertTrue(nvSubNode.getProperty(propertyName1).getString().equals(changedString)); + } + + /** + * VersionManager.merge(): nonversionable subNode N: is left unchanged if the nearest + * versionable ancestor has state leave.
        + */ + public void testMergeNodeNonVersionableSubNodeLeaveJcr2() throws RepositoryException { + // modify non versionable subnode so we can check if it's updated after merge + String changedString = CHANGED_STRING + System.currentTimeMillis(); + String nvSubNodePath = nodeName2 + "/" + nodeName3; + + // versionable ancestor to merge in first workspace (N) + Node n = testRootNodeW2.getNode(nodeName1); + + // versionable ancestor to merge in second workspace (N') + Node np = testRootNodeW2.getNode(nodeName1); + + // checkout N' and make change + np.getSession().getWorkspace().getVersionManager().checkout(np.getPath()); + Node nvSubNodeP = np.getNode(nvSubNodePath); + nvSubNodeP.setProperty(propertyName1, changedString); + np.getSession().save(); + np.getSession().getWorkspace().getVersionManager().checkin(np.getPath()); + + // merge into N' + np.getSession().getWorkspace().getVersionManager().merge(np.getPath(), workspaceW2.getName(), true); + + // corresponding node to nvSubNode in 2nd workspace + Node nvSubNode = np.getNode(nvSubNodePath); + + // test if modification on N' was not modified + assertTrue(nvSubNode.getProperty(propertyName1).getString().equals(changedString)); + } + + /** + * initialize a three-step-hierarchy on default and second workspace + */ + protected void initNodes() throws RepositoryException { + // create a versionable parent node + // nodeName1 + Node topVNode = testRootNode.addNode(nodeName1, versionableNodeType); + topVNode.setProperty(propertyName1, topVNode.getName()); + + // create a non'versionable sub node + // nodeName1/nodeName2 + Node subNvNode = topVNode.addNode(nodeName2, testNodeType); + subNvNode.setProperty(propertyName1, subNvNode.getName()); + + // create a non'versionable sub node below nonversionable node + // nodeName1/nodeName2/nodeName3 + Node subSubNvNode = subNvNode.addNode(nodeName3, testNodeType); + subSubNvNode.setProperty(propertyName1, subSubNvNode.getName()); + + // save default workspace + testRootNode.getSession().save(); + + log.println("test nodes created successfully on " + workspace.getName()); + + // clone the newly created node from src workspace into second workspace + workspaceW2.clone(workspace.getName(), topVNode.getPath(), topVNode.getPath(), true); + log.println(topVNode.getPath() + " cloned on " + superuserW2.getWorkspace().getName() + " at " + topVNode.getPath()); + + testRootNodeW2 = (Node) superuserW2.getItem(testRoot); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeShallowTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeShallowTest.java new file mode 100644 index 00000000000..6b0ae0ef152 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeShallowTest.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.test.api.version; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionManager; + +/** + * MergeShallowTest contains tests dealing with general shallow + * merge calls. + * + */ + +public class MergeShallowTest extends AbstractMergeTest { + + /** + * node to merge + */ + Node nodeToMerge; + + String newValue; + + protected void setUp() throws Exception { + super.setUp(); + + nodeToMerge = testRootNodeW2.getNode(nodeName1); + + } + + protected void tearDown() throws Exception { + nodeToMerge = null; + super.tearDown(); + } + + public void testMergeRecursive() throws RepositoryException { + VersionManager vm2 = testRootNodeW2.getSession().getWorkspace().getVersionManager(); + NodeIterator iter = vm2.merge(nodeToMerge.getPath(), + superuser.getWorkspace().getName(), true, false); + if (iter.hasNext()) { + StringBuffer failed = new StringBuffer(); + while (iter.hasNext()) { + failed.append(iter.nextNode().getPath()); + failed.append(", "); + } + fail("Merge must not fail. failed nodes: " + failed); + return; + } + + String p1 = nodeToMerge.getProperty(propertyName1).getString(); + String p2 = nodeToMerge.getProperty(nodeName2 + "/" + propertyName1).getString(); + assertEquals("Recursive merge did not restore property on level 1.", newValue, p1); + assertEquals("Recursive merge did not restore property on level 2.", newValue, p2); + + } + + public void testMergeShallow() throws RepositoryException { + String oldP2 = nodeToMerge.getProperty(nodeName2 + "/" + propertyName1).getString(); + + VersionManager vm2 = testRootNodeW2.getSession().getWorkspace().getVersionManager(); + NodeIterator iter = vm2.merge(nodeToMerge.getPath(), + superuser.getWorkspace().getName(), true, true); + if (iter.hasNext()) { + StringBuffer failed = new StringBuffer(); + while (iter.hasNext()) { + failed.append(iter.nextNode().getPath()); + failed.append(", "); + } + fail("Merge must not fail. failed nodes: " + failed); + return; + } + + String p1 = nodeToMerge.getProperty(propertyName1).getString(); + String p2 = nodeToMerge.getProperty(nodeName2 + "/" + propertyName1).getString(); + assertEquals("Shallow merge did not restore property on level 1.", newValue, p1); + assertEquals("Shallow merge did restore property on level 2.", oldP2, p2); + + } + + /** + * initialize a versionable node on default and second workspace + */ + protected void initNodes() throws RepositoryException { + + VersionManager versionManager = testRootNode.getSession().getWorkspace().getVersionManager(); + + // create a versionable node + // nodeName1 + Node topVNode = testRootNode.addNode(nodeName1, versionableNodeType); + topVNode.setProperty(propertyName1, topVNode.getName()); + String path = topVNode.getPath(); + + // create a versionable sub node + // nodeName1/nodeName2 + Node subNvNode = topVNode.addNode(nodeName2, versionableNodeType); + subNvNode.setProperty(propertyName1, subNvNode.getName()); + String path2 = subNvNode.getPath(); + + // save default workspace + testRootNode.getSession().save(); + versionManager.checkin(path); + versionManager.checkin(path2); + + log.println("test nodes created successfully on " + workspace.getName()); + + // clone the newly created node from src workspace into second workspace + // todo clone on testRootNode does not seem to work. + // workspaceW2.clone(workspace.getName(), testRootNode.getPath(), testRootNode.getPath(), true); + workspaceW2.clone(workspace.getName(), topVNode.getPath(), topVNode.getPath(), true); + log.println(topVNode.getPath() + " cloned on " + superuserW2.getWorkspace().getName() + " at " + topVNode.getPath()); + + testRootNodeW2 = (Node) superuserW2.getItem(testRoot); + + versionManager.checkout(path); + versionManager.checkout(path2); + + // update properties on source nodes + newValue = String.valueOf(System.currentTimeMillis()); + + Node n1 = testRootNode.getNode(nodeName1); + n1.setProperty(propertyName1, newValue); + Node n2 = n1.getNode(nodeName2); + n2.setProperty(propertyName1, newValue); + testRootNode.getSession().save(); + + VersionManager vm1 = testRootNode.getSession().getWorkspace().getVersionManager(); + vm1.checkpoint(n2.getPath()); + vm1.checkpoint(n1.getPath()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeSubNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeSubNodeTest.java new file mode 100644 index 00000000000..764afeb4ce8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/MergeSubNodeTest.java @@ -0,0 +1,368 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.version.VersionManager; + +/** + * MergeSubNodeTest contains tests dealing with sub nodes in the + * subtree of the node on which merge is called. + * + */ + +public class MergeSubNodeTest extends AbstractMergeTest { + + /** + * node to merge + */ + Node nodeToMerge; + + protected void setUp() throws Exception { + super.setUp(); + + nodeToMerge = testRootNodeW2.getNode(nodeName1); + // node has to be checked out while merging + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + versionManager.checkout(nodeToMerge.getPath()); + + } + + protected void tearDown() throws Exception { + nodeToMerge = null; + super.tearDown(); + } + + /** + * Node.merge(): versionable subNode N: If N has status leave but parent is + * update, then the subnode N is removed
        retrieve the initialised node + * to perform operations we need before for this test
        + */ + @SuppressWarnings("deprecation") + public void disable_testRemoveNodeFromSourceWorkspaceAndMergeWithUpdate() throws RepositoryException { + // status 'update' for parent + nodeToMerge.checkin(); + nodeToMerge.checkout(); + + // status 'leave' for subnode + Node originalNode = testRootNode.getNode(nodeName1); + Node originalSubNode = originalNode.getNode(nodeName2); + originalSubNode.checkout(); + originalSubNode.checkin(); + + // "merge" the nodeToMerge with the newNode from the default workspace + // besteffort set to false to stop at the first failure + nodeToMerge.merge(workspace.getName(), false); + + // if merge passed newSubNode1 should be also removed from workspace2 + assertFalse("subNode1 not removed from " + workspaceW2.getName() + " as expected", nodeToMerge.hasNode(nodeName2)); + + // return version info about the clonedNode as it must also be updated + final String originalBaseVersionUUID = originalNode.getBaseVersion().getUUID(); + final String clonedBaseVersionUUID = nodeToMerge.getBaseVersion().getUUID(); + + assertTrue("clonedNode has different version UUID than expected, it should be updated with the newNode version UUID", originalBaseVersionUUID.equals(clonedBaseVersionUUID)); + } + + /** + * VersionManager.merge(): versionable subNode N: If N has status leave but parent is + * update, then the subnode N is removed
        retrieve the initialised node + * to perform operations we need before for this test
        + */ + public void disable_testRemoveNodeFromSourceWorkspaceAndMergeWithUpdateJcr2() throws RepositoryException { + // status 'update' for parent + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + versionManager.checkin(path); + versionManager.checkout(path); + + // status 'leave' for subnode + Node originalNode = testRootNode.getNode(nodeName1); + Node originalSubNode = originalNode.getNode(nodeName2); + VersionManager originalVersionManager = originalSubNode.getSession().getWorkspace().getVersionManager(); + String originalSubPath = originalSubNode.getPath(); + originalVersionManager.checkout(originalSubPath); + originalVersionManager.checkin(originalSubPath); + + // "merge" the nodeToMerge with the newNode from the default workspace + // besteffort set to false to stop at the first failure + versionManager.merge(path, workspace.getName(), false); + + // if merge passed newSubNode1 should be also removed from workspace2 + assertFalse("subNode1 not removed from " + workspaceW2.getName() + " as expected", nodeToMerge.hasNode(nodeName2)); + + // return version info about the clonedNode as it must also be updated + final String originalBaseVersionId = originalVersionManager.getBaseVersion(originalNode.getPath()).getIdentifier(); + final String clonedBaseVersionId = versionManager.getBaseVersion(path).getIdentifier(); + + assertTrue("clonedNode has different version UUID than expected, it should be updated with the newNode version UUID", originalBaseVersionId.equals(clonedBaseVersionId)); + } + + /** + * Node.merge(): versionable subNode N checked-in: If V' is a successor (to + * any degree) of V, then the merge result for N is update
        modify a node + * on the workspace1 and then merge the one in workspace2 with the one in + * workspace1 precondition is that the node in workspace2 is checked in + */ + @SuppressWarnings("deprecation") + public void disable_testMergeNodeFromUpdatedSourceWorkspace() throws RepositoryException { + Node originalNode = testRootNode.getNode(nodeName1); + + // update nodeName1 on workspace1 + originalNode.checkout(); + originalNode.checkin(); + + testRootNode.getSession().save(); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to false to stop at the first failure + nodeToMerge.merge(workspace.getName(), false); + + final String originalBaseVersionUUID = originalNode.getBaseVersion().getUUID(); + final String clonedBaseVersionUUID = nodeToMerge.getBaseVersion().getUUID(); + + assertTrue("clonedNode has different version UUID than expected, it should be updated with the newNode version UUID", originalBaseVersionUUID.equals(clonedBaseVersionUUID)); + } + + /** + * VersionManager.merge(): versionable subNode N checked-in: If V' is a successor (to + * any degree) of V, then the merge result for N is update
        modify a node + * on the workspace1 and then merge the one in workspace2 with the one in + * workspace1 precondition is that the node in workspace2 is checked in + */ + public void disable_testMergeNodeFromUpdatedSourceWorkspaceJcr2() throws RepositoryException { + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager originalVersionManager = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + + // update nodeName1 on workspace1 + originalVersionManager.checkout(originalPath); + originalVersionManager.checkin(originalPath); + + testRootNode.getSession().save(); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to false to stop at the first failure + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + versionManager.merge(path, workspace.getName(), false); + + final String originalBaseVersionId = originalVersionManager.getBaseVersion(originalPath).getIdentifier(); + final String clonedBaseVersionId = versionManager.getBaseVersion(path).getIdentifier(); + + assertTrue("clonedNode has different version UUID than expected, it should be updated with the newNode version UUID", originalBaseVersionId.equals(clonedBaseVersionId)); + } + + /** + * Node.merge(): versionable subNode N checked-in: If V' is a predecessor + * (to any degree) of V or if V and V' are identical (i.e., are actually the + * same version), then the merge result for N is leave
        modify a node on + * the workspace2 and then merge the one in workspace2 with the one in + * workspace1
        the node in workspace2 should be updated
        precondition + * is that the node in workspace2 is checked in + */ + @SuppressWarnings("deprecation") + public void testMergeNodeFromOlderSourceWorkspace() throws RepositoryException { + // touch the version on workspace2 + nodeToMerge.checkin(); + nodeToMerge.checkout(); + + String baseVersionUUIDbeforeMerge = nodeToMerge.getBaseVersion().getUUID(); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to false to stop at the first failure + nodeToMerge.merge(workspace.getName(), false); + + assertTrue("clonedNode has different UUID than expected, it should be left unchanged", baseVersionUUIDbeforeMerge.equals(nodeToMerge.getBaseVersion().getUUID())); + } + + /** + * VersionManager.merge(): versionable subNode N checked-in: If V' is a predecessor + * (to any degree) of V or if V and V' are identical (i.e., are actually the + * same version), then the merge result for N is leave
        modify a node on + * the workspace2 and then merge the one in workspace2 with the one in + * workspace1
        the node in workspace2 should be updated
        precondition + * is that the node in workspace2 is checked in + */ + public void testMergeNodeFromOlderSourceWorkspaceJcr2() throws RepositoryException { + + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + + // touch the version on workspace2 + versionManager.checkin(path); + versionManager.checkout(path); + + String baseVersionIdbeforeMerge = versionManager.getBaseVersion(path).getIdentifier(); + + // "merge" the clonedNode with the newNode from the default workspace + // besteffort set to false to stop at the first failure + versionManager.merge(path, workspace.getName(), false); + + assertTrue("clonedNode has different UUID than expected, it should be left unchanged", baseVersionIdbeforeMerge .equals(versionManager.getBaseVersion(path).getIdentifier())); + } + + /** + * Node.merge(): bestEffort is true > (sub)node which could not be merged + * are not affected.
        + */ + @SuppressWarnings("deprecation") + public void disable_testMergeNodeBestEffortTrue() throws RepositoryException { + // create 2 new nodes with two independent versions + // so merge fails for this node + Node originalNode = testRootNode.getNode(nodeName1); + originalNode.checkout(); + Node subNode = originalNode.getNode(nodeName2); + // will be unchanged after merge + subNode.checkout(); + subNode.setProperty(propertyName1, CHANGED_STRING); + // will be updated + originalNode.setProperty(propertyName1, CHANGED_STRING); + superuser.save(); + subNode.checkin(); + originalNode.checkin(); + + Node subNodeW2 = nodeToMerge.getNode(nodeName2); + subNodeW2.checkout(); + subNodeW2.setProperty(propertyName1, CHANGED_STRING); + superuserW2.save(); + subNodeW2.checkin(); + + nodeToMerge.checkout(); + + // merge, besteffort set to true + nodeToMerge.merge(workspace.getName(), true); + + // sub node should not be touched because merging failed + assertTrue(subNodeW2.getProperty(propertyName1).getString().equals("")); + + // test root node should be touched because update + assertFalse(nodeToMerge.getProperty(propertyName1).getString().equals(nodeToMerge.getName())); + } + + /** + * VersionManager.merge(): bestEffort is true > (sub)node which could not be merged + * are not affected.
        + */ + public void disable_testMergeNodeBestEffortTrueJcr2() throws RepositoryException { + // create 2 new nodes with two independent versions + // so merge fails for this node + Node originalNode = testRootNode.getNode(nodeName1); + VersionManager originalVersionManager = originalNode.getSession().getWorkspace().getVersionManager(); + String originalPath = originalNode.getPath(); + originalVersionManager.checkout(originalPath); + Node subNode = originalNode.getNode(nodeName2); + // will be unchanged after merge + originalVersionManager.checkout(subNode.getPath()); + subNode.setProperty(propertyName1, CHANGED_STRING); + // will be updated + originalNode.setProperty(propertyName1, CHANGED_STRING); + superuser.save(); + originalVersionManager.checkin(subNode.getPath()); + originalVersionManager.checkin(originalPath); + + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + Node subNodeW2 = nodeToMerge.getNode(nodeName2); + versionManager.checkout(subNodeW2.getPath()); + subNodeW2.setProperty(propertyName1, CHANGED_STRING); + superuserW2.save(); + versionManager.checkin(subNodeW2.getPath()); + + versionManager.checkout(nodeToMerge.getPath()); + + // merge, besteffort set to true + versionManager.merge(nodeToMerge.getPath(), workspace.getName(), true); + + // sub node should not be touched because merging failed + assertTrue(subNodeW2.getProperty(propertyName1).getString().equals("")); + + // test root node should be touched because update + assertFalse(nodeToMerge.getProperty(propertyName1).getString().equals(nodeToMerge.getName())); + } + + /** + * Node.merge(): For each versionable node N in the subtree rooted at this + * node,
        a merge test is performed comparing N with its corresponding + * node in workspace, N'.
        + */ + @SuppressWarnings("deprecation") + public void disable_testMergeNodeSubNodesMergeTest() throws RepositoryException { + //setCheckProperty(nodeToMerge); + nodeToMerge.checkout(); + + nodeToMerge.merge(workspace.getName(), true); + + // check subnodes if they were touched + for (NodeIterator ni = nodeToMerge.getNodes(); ni.hasNext();) { + Node n = ni.nextNode(); + if (n.getBaseVersion() != null) { + assertTrue(n.getProperty(propertyName1).getString().equals(CHANGED_STRING)); + } + } + } + + /** + * VersionManager.merge(): For each versionable node N in the subtree rooted at this + * node,
        a merge test is performed comparing N with its corresponding + * node in workspace, N'.
        + */ + public void disable_testMergeNodeSubNodesMergeTestJcr2() throws RepositoryException { + //setCheckProperty(nodeToMerge); + VersionManager versionManager = nodeToMerge.getSession().getWorkspace().getVersionManager(); + String path = nodeToMerge.getPath(); + versionManager.checkout(path); + + versionManager.merge(path, workspace.getName(), true); + + // check subnodes if they were touched + for (NodeIterator ni = nodeToMerge.getNodes(); ni.hasNext();) { + Node n = ni.nextNode(); + if (versionManager.getBaseVersion(n.getPath()) != null) { + assertTrue(n.getProperty(propertyName1).getString().equals(CHANGED_STRING)); + } + } + } + + /** + * initialize a two-step-hierarchy on default and second workspace + */ + protected void initNodes() throws RepositoryException { + // create a versionable parent node + // nodeName1 + Node topVNode = testRootNode.addNode(nodeName1, versionableNodeType); + topVNode.setProperty(propertyName1, topVNode.getName()); + + // create a versionable sub node + // nodeName1/nodeName2 + Node subNvNode = topVNode.addNode(nodeName2, versionableNodeType); + subNvNode.setProperty(propertyName1, subNvNode.getName()); + + // save default workspace + testRootNode.getSession().save(); + + log.println("test nodes created successfully on " + workspace.getName()); + + // clone the newly created node from src workspace into second workspace + workspaceW2.clone(workspace.getName(), topVNode.getPath(), topVNode.getPath(), true); + log.println(topVNode.getPath() + " cloned on " + superuserW2.getWorkspace().getName() + " at " + topVNode.getPath()); + + testRootNodeW2 = (Node) superuserW2.getItem(testRoot); + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionAbortTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionAbortTest.java new file mode 100644 index 00000000000..32b965bfacc --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionAbortTest.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.test.api.version; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionManager; + +/** + * OnParentVersionAbortTest tests the OnParentVersion {@link OnParentVersionAction#ABORT ABORT} + * behaviour. + * + */ +public class OnParentVersionAbortTest extends AbstractOnParentVersionTest { + + protected void setUp() throws Exception { + OPVAction = OnParentVersionAction.ABORT; + super.setUp(); + } + + /** + * Test the restore of a OnParentVersion-ABORT property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreProp() throws RepositoryException { + try { + p.getParent().checkout(); + p.getParent().checkin(); + fail("On checkin of N which has a property with OnParentVersion ABORT defined, an UnsupportedRepositoryOperationException must be thrown."); + } catch (VersionException e) { + // success + } + } + + /** + * Test the restore of a OnParentVersion-ABORT property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestorePropJcr2() throws RepositoryException { + try { + VersionManager versionManager = p.getSession().getWorkspace().getVersionManager(); + String path = p.getParent().getPath(); + versionManager.checkout(path); + versionManager.checkin(path); + fail("On checkin of N which has a property with OnParentVersion ABORT defined, an UnsupportedRepositoryOperationException must be thrown."); + } catch (VersionException e) { + // success + } + } + + /** + * Test the restore of a OnParentVersion-ABORT node + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testRestoreNode() throws RepositoryException, NotExecutableException { + // create child node with OPV-ABORT behaviour + addChildNode(OPVAction); + testRootNode.getSession().save(); + try { + versionableNode.checkin(); + fail("On checkin of N which has a child node with OnParentVersion ABORT defined, an UnsupportedRepositoryOperationException must be thrown."); + } catch (VersionException e) { + // success + } + } + + /** + * Test the restore of a OnParentVersion-ABORT node + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testRestoreNodeJcr2() throws RepositoryException, NotExecutableException { + // create child node with OPV-ABORT behaviour + addChildNode(OPVAction); + testRootNode.getSession().save(); + try { + versionableNode.getSession().getWorkspace().getVersionManager().checkin(versionableNode.getPath()); + fail("On checkin of N which has a child node with OnParentVersion ABORT defined, an UnsupportedRepositoryOperationException must be thrown."); + } catch (VersionException e) { + // success + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionComputeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionComputeTest.java new file mode 100644 index 00000000000..d4681e7fbf3 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionComputeTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +/** + * OnParentVersionComputeTest tests the OnParentVersion {@link OnParentVersionAction#COMPUTE COMPUTE} + * behaviour. + * + */ +public class OnParentVersionComputeTest extends AbstractOnParentVersionTest { + + protected void setUp() throws Exception { + OPVAction = OnParentVersionAction.COMPUTE; + super.setUp(); + } + + /** + * Test the restore of a OnParentVersion-COMPUTE property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreProp() throws RepositoryException { + + Node propParent = p.getParent(); + propParent.checkout(); + Version v = propParent.checkin(); + propParent.checkout(); + + p.setValue(newPropValue); + p.save(); + + propParent.restore(v, false); + + assertEquals("On restore of a OnParentVersion-COMPUTE property P, the current P in the workspace will be left unchanged.", p.getString(), newPropValue); + } + + /** + * Test the restore of a OnParentVersion-COMPUTE property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestorePropJcr2() throws RepositoryException { + + Node propParent = p.getParent(); + VersionManager versionManager = propParent.getSession().getWorkspace().getVersionManager(); + String path = propParent.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + versionManager.checkout(path); + + p.setValue(newPropValue); + p.getSession().save(); + + versionManager.restore(v, false); + + assertEquals("On restore of a OnParentVersion-COMPUTE property P, the current P in the workspace will be left unchanged.", p.getString(), newPropValue); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionCopyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionCopyTest.java new file mode 100644 index 00000000000..725bfb43a5d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionCopyTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +/** + * OnParentVersionCopyTest tests the OnParentVersion {@link OnParentVersionAction#COPY COPY} + * behaviour. + * + */ +public class OnParentVersionCopyTest extends AbstractOnParentVersionTest { + + String initialNodePath; + + protected void setUp() throws Exception { + OPVAction = OnParentVersionAction.COPY; + super.setUp(); + } + + /** + * Test the restore of a OnParentVersion-COPY property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreProp() throws RepositoryException { + assertEquals("On restore of a OnParentVersion-COPY property P the copy of P stored will be restored, replacing the current P in the workspace.", p.getString(), initialPropValue); + } + + /** + * Test the restore of a OnParentVersion-COPY node + * + * @throws javax.jcr.RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreNode() throws RepositoryException { + // prepare for node test + Node childNode = addChildNode(OPVAction); + Node nodeParent = childNode.getParent(); + // todo: added next line. correct? -> angela + nodeParent.save(); + + nodeParent.checkout(); + Version v = nodeParent.checkin(); + + initialNodePath = childNode.getPath(); + nodeParent.checkout(); + childNode.remove(); + nodeParent.save(); + + nodeParent.restore(v, false); + + if (!superuser.itemExists(initialNodePath)) { + fail("On restore of a OnParentVersion-COPY child node, the node needs to be restored, replacing the current node in the workspace."); + } + // todo: add proper comparison of restored node. equals does not work + // assertEquals("On restore of a OnParentVersion-COPY child node, the node needs to be restored, replacing the current node in the workspace.", childNode, superuser.getItem(initialNodePath)); + } + + /** + * Test the restore of a OnParentVersion-COPY node + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreNodeJcr2() throws RepositoryException { + // prepare for node test + Node childNode = addChildNode(OPVAction); + Node nodeParent = childNode.getParent(); + // todo: added next line. correct? -> angela + nodeParent.getSession().save(); + + VersionManager versionManager = nodeParent.getSession().getWorkspace().getVersionManager(); + String path = nodeParent.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + + initialNodePath = childNode.getPath(); + versionManager.checkout(path); + childNode.remove(); + nodeParent.getSession().save(); + + versionManager.restore(v, false); + + if (!superuser.itemExists(initialNodePath)) { + fail("On restore of a OnParentVersion-COPY child node, the node needs to be restored, replacing the current node in the workspace."); + } + // todo: add proper comparison of restored node. equals does not work + // assertEquals("On restore of a OnParentVersion-COPY child node, the node needs to be restored, replacing the current node in the workspace.", childNode, superuser.getItem(initialNodePath)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionIgnoreTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionIgnoreTest.java new file mode 100644 index 00000000000..6dc021ef8b9 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionIgnoreTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +/** + * OnParentVersionIgnoreTest tests the OnParentVersion {@link OnParentVersionAction#IGNORE IGNORE} + * behaviour. + * + */ +public class OnParentVersionIgnoreTest extends AbstractOnParentVersionTest { + + protected void setUp() throws Exception { + OPVAction = OnParentVersionAction.IGNORE; + super.setUp(); + } + + /** + * Test the restore of a OnParentVersion-IGNORE property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreProp() throws RepositoryException { + + Node propParent = p.getParent(); + propParent.checkout(); + Version v = propParent.checkin(); + propParent.checkout(); + + p.setValue(newPropValue); + p.save(); + + propParent.restore(v, false); + + assertEquals("On restore of a OnParentVersion-IGNORE property P, the current value of P must be left unchanged.", p.getString(), newPropValue); + } + + /** + * Test the restore of a OnParentVersion-IGNORE property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestorePropJcr2() throws RepositoryException { + + Node propParent = p.getParent(); + VersionManager versionManager = propParent.getSession().getWorkspace().getVersionManager(); + String path = propParent.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + versionManager.checkout(path); + + p.setValue(newPropValue); + p.getSession().save(); + + versionManager.restore(v, false); + + assertEquals("On restore of a OnParentVersion-IGNORE property P, the current value of P must be left unchanged.", p.getString(), newPropValue); + } + + /** + * Test the restore of a OnParentVersion-Ignore node + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreNode() throws RepositoryException { + + versionableNode.checkout(); + Version v = versionableNode.checkin(); + versionableNode.checkout(); + + // add 'ignore' child + String childName = addChildNode(OPVAction).getName(); + versionableNode.save(); + + versionableNode.restore(v, false); + + if (!versionableNode.hasNode(childName)) { + fail("On restore of a OnParentVersion-Ignore child node, the node needs to be untouched."); + } + } + + /** + * Test the restore of a OnParentVersion-Ignore node + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreNodeJcr2() throws RepositoryException { + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + versionManager.checkout(path); + + // add 'ignore' child + String childName = addChildNode(OPVAction).getName(); + versionableNode.getSession().save(); + + versionManager.restore(v, false); + + if (!versionableNode.hasNode(childName)) { + fail("On restore of a OnParentVersion-Ignore child node, the node needs to be untouched."); + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionInitializeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionInitializeTest.java new file mode 100644 index 00000000000..3b31d263911 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/OnParentVersionInitializeTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +/** + * OnParentVersionInitializeTest tests the {@link OnParentVersionAction#INITIALIZE INITIALIZE} + * behaviour. + * + */ +public class OnParentVersionInitializeTest extends AbstractOnParentVersionTest { + + protected void setUp() throws Exception { + OPVAction = OnParentVersionAction.INITIALIZE; + super.setUp(); + } + + /** + * Test the restore of a OnParentVersion-INITIALIZE property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreProp() throws RepositoryException { + + Node propParent = p.getParent(); + propParent.checkout(); + Version v = propParent.checkin(); + propParent.checkout(); + + p.setValue(newPropValue); + p.save(); + + propParent.restore(v, false); + + assertEquals("On restore of a OnParentVersion-INITIALIZE property P, the current value of P must be left unchanged.", p.getString(), newPropValue); + } + + /** + * Test the restore of a OnParentVersion-INITIALIZE property + * + * @throws javax.jcr.RepositoryException + */ + public void testRestorePropJcr2() throws RepositoryException { + + Node propParent = p.getParent(); + VersionManager versionManager = propParent.getSession().getWorkspace().getVersionManager(); + String path = propParent.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + versionManager.checkout(path); + + p.setValue(newPropValue); + p.getSession().save(); + + versionManager.restore(v, false); + + assertEquals("On restore of a OnParentVersion-INITIALIZE property P, the current value of P must be left unchanged.", p.getString(), newPropValue); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/RemoveVersionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/RemoveVersionTest.java new file mode 100644 index 00000000000..a8907fca59c --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/RemoveVersionTest.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.test.api.version; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Node; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.Value; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import java.util.ArrayList; +import java.util.List; + +/** + * RemoveVersionTest provides test methods covering {@link VersionHistory#removeVersion(String)}. + * Please note, that removing versions is defined to be an optional feature in + * the JSR 170 specification. The setup therefore includes a initial removal, + * in order to test, whether removing versions is supported. + * + */ +public class RemoveVersionTest extends AbstractVersionTest { + + protected Node versionableNode2; + protected Version version; + protected Version version2; + + protected VersionHistory vHistory; + + protected void setUp() throws Exception { + super.setUp(); + + Version testV = versionableNode.checkin(); // create 1.0 + versionableNode.checkout(); + versionableNode.checkin(); // create 1.1 + versionableNode.checkout(); + versionableNode.checkin(); // create 1.2 + try { + versionableNode.getVersionHistory().removeVersion(testV.getName()); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException("Removing version is not supported: " + e.getMessage()); + } + + versionableNode.checkout(); + version = versionableNode.checkin(); + // create a second version + versionableNode.checkout(); + version2 = versionableNode.checkin(); + + vHistory = versionableNode.getVersionHistory(); + + // build a second versionable node below the testroot + try { + versionableNode2 = createVersionableNode(testRootNode, nodeName2, versionableNodeType); + } catch (RepositoryException e) { + fail("Failed to create a second versionable node: " + e.getMessage()); + } + } + + protected void tearDown() throws Exception { + try { + versionableNode2.remove(); + } finally { + versionableNode2 = null; + version = null; + version2 = null; + vHistory = null; + super.tearDown(); + } + } + + /** + * Test removed version gets invalid + */ + public void testRemovedInvalid() throws Exception { + versionableNode.getVersionHistory().removeVersion(version.getName()); + // assert: version has become invalid + try { + version.getPredecessors(); + fail("Removed version still operational."); + } catch (RepositoryException e) { + // expected + } + } + + /** + * Test if the predecessors of the removed version are made predecessor of + * its original successor version. + * + * @throws RepositoryException + */ + public void testRemoveVersionAdjustPredecessorSet() throws RepositoryException { + + // retrieve predecessors to test and remove the version + List predecPaths = new ArrayList(); + Version[] predec = version.getPredecessors(); + for (int i = 0; i < predec.length; i++) { + predecPaths.add(predec[i].getPath()); + } + vHistory.removeVersion(version.getName()); + + // new predecessors of the additional version + Version[] predec2 = version2.getPredecessors(); + for (int i = 0; i < predec2.length; i++) { + if (!predecPaths.remove(predec2[i].getPath())) { + fail("All predecessors of the removed version must be made predecessors of it's original successor version."); + } + } + + if (!predecPaths.isEmpty()) { + fail("All predecessors of the removed version must be made predecessors of it's original successor version."); + } + } + + /** + * Test if the successors of the removed version are made successors of + * all predecessors of the the removed version. + * + * @throws RepositoryException + */ + public void testRemoveVersionAdjustSucessorSet() throws RepositoryException { + + // retrieve predecessors to test and remove the version + Version[] predec = version.getPredecessors(); + vHistory.removeVersion(version.getName()); + + for (int i = 0; i < predec.length; i++) { + boolean isContained = false; + Version[] succ = predec[i].getSuccessors(); + for (int j = 0; j < succ.length; j++) { + isContained |= succ[j].isSame(version2); + } + if (!isContained) { + fail("Removing a version must make all it's successor version to successors of the removed version's predecessors."); + } + } + } + + /** + * Test if removing a version from the version history throws a VersionException + * if the specified version does not exist. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testRemoveInvalidVersion() throws RepositoryException, NotExecutableException { + Version invalidV = versionableNode2.checkin(); + String invalidName = invalidV.getName(); + + // build a version name that is not present in the current history + boolean found = false; + for (int i = 0; i < 10 && !found; i++) { + try { + vHistory.getVersion(invalidName); + invalidName += i; + } catch (VersionException e) { + // ok > found a name that is invalid. + found = true; + } + } + + if (!found) { + throw new NotExecutableException("Failed to create an invalid name in order to test the removal of versions."); + } + + try { + vHistory.removeVersion(invalidName); + fail("Removing a version that does not exist must fail with a VersionException."); + } catch (VersionException e) { + // success + } + } + + /** + * Checks if {@link javax.jcr.version.VersionHistory#removeVersion(String)} + * throws a {@link javax.jcr.ReferentialIntegrityException} if the named + * version is still referenced by another node. + *
          + *
        • {@code nodetype} name of a node type that supports a reference + * property. + *
        • {@code nodename4} name of the node created with nodetype. + *
        • {@code propertyname1} a single value reference property available + * in nodetype. + *
        + */ + public void testReferentialIntegrityException() throws RepositoryException, NotExecutableException { + // create reference: n1.p1 -> version + Node n1 = testRootNode.addNode(nodeName4, testNodeType); + Value refValue = superuser.getValueFactory().createValue(version); + ensureCanSetProperty(n1, propertyName1, refValue); + n1.setProperty(propertyName1, refValue); + testRootNode.getSession().save(); + + try { + vHistory.removeVersion(version.getName()); + fail("Method removeVersion() must throw a ReferentialIntegrityException " + + "if the version is the target of a REFERENCE property and the current " + + "Session has read access to that REFERENCE property"); + } + catch (ReferentialIntegrityException e) { + // success + } + } + + /** + * Checks if all versions but the base and root one can be removed. + */ + public void testRemoveAllBut2() throws RepositoryException { + String baseVersion = versionableNode.getBaseVersion().getName(); + VersionHistory vh = versionableNode.getVersionHistory(); + VersionIterator vi = vh.getAllVersions(); + while (vi.hasNext()) { + Version currenVersion = vi.nextVersion(); + String versionName = currenVersion.getName(); + if (!versionName.equals("jcr:rootVersion") && !versionName.equals(baseVersion)) { + vh.removeVersion(versionName); + } + } + } + + /** + * Checks if all versions by the base and root one can be removed. + */ + public void testRemoveRootVersion() throws RepositoryException { + try { + versionableNode.getVersionHistory().getRootVersion().remove(); + fail("Removal of root version should throw an exception."); + } catch (RepositoryException e) { + // ignore + } + } + + /** + * Checks if all versions by the base and root one can be removed. + */ + public void testRemoveBaseVersion() throws RepositoryException { + try { + versionableNode.getBaseVersion().remove(); + fail("Removal of base version should throw an exception."); + } catch (RepositoryException e) { + // ignore + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/RestoreTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/RestoreTest.java new file mode 100644 index 00000000000..d15eb0e1388 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/RestoreTest.java @@ -0,0 +1,1462 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ItemExistsException; +import javax.jcr.NodeIterator; + +/** + * RestoreTest covers tests related to the restore methods available + * on {@link javax.jcr.Node}: + *
          + *
        • {@link javax.jcr.Node#restore(String, boolean)}
        • + *
        • {@link javax.jcr.Node#restore(javax.jcr.version.Version, boolean)}
        • + *
        • {@link javax.jcr.Node#restore(javax.jcr.version.Version, String, boolean)}
        • + *
        + * + */ +public class RestoreTest extends AbstractVersionTest { + + VersionManager versionManager; + + Version version; + Version version2; + Version rootVersion; + + Node versionableNode2; + + String propertyValue1; + String propertyValue2; + + protected void setUp() throws Exception { + super.setUp(); + versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + propertyValue1 = getProperty("propertyValue1"); + propertyValue2 = getProperty("propertyValue2"); + versionableNode.setProperty(propertyName1, propertyValue1); + versionableNode.getSession().save(); + version = versionManager.checkin(path); + versionManager.checkout(path); + versionableNode.setProperty(propertyName1, propertyValue2); + versionableNode.getSession().save(); + version2 = versionManager.checkin(path); + versionManager.checkout(path); + rootVersion = versionManager.getVersionHistory(path).getRootVersion(); + + // build a second versionable node below the testroot + try { + versionableNode2 = createVersionableNode(testRootNode, nodeName2, versionableNodeType); + } catch (RepositoryException e) { + fail("Failed to create a second versionable node: " + e.getMessage()); + } + } + + protected void tearDown() throws Exception { + try { + versionableNode2.remove(); + testRootNode.getSession().save(); + } finally { + version = null; + version2 = null; + rootVersion = null; + versionableNode2 = null; + super.tearDown(); + } + } + + /** + * Test if restoring the root version fails. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreRootVersionFail() throws RepositoryException { + try { + versionableNode.restore(rootVersion, true); + fail("Restore of jcr:rootVersion must throw VersionException."); + } catch (VersionException e) { + // success + } + } + + /** + * Test if restoring the root version fails. + * + * @throws RepositoryException + */ + public void testRestoreRootVersionFailJcr2() throws RepositoryException { + try { + versionManager.restore(rootVersion, true); + fail("Restore of jcr:rootVersion must throw VersionException."); + } catch (VersionException e) { + // success + } + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreOnCheckedInNode() throws RepositoryException { + versionableNode.checkin(); + versionableNode.restore(version, true); + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedInNodeJcr2_1() throws RepositoryException { + versionManager.checkin(versionableNode.getPath()); + versionManager.restore(version, true); + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedInNodeJcr2_2() throws RepositoryException { + versionManager.checkin(versionableNode.getPath()); + versionManager.restore(version, true); + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedInNodeJcr2_3() throws RepositoryException { + versionManager.checkin(versionableNode.getPath()); + versionManager.restore(versionableNode.getPath(), version.getName(), true); + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedInNodeJcr2_4() throws RepositoryException { + versionManager.checkin(versionableNode.getPath()); + versionManager.restore(new Version[] {version}, true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreOnCheckedOutNode() throws RepositoryException { + versionableNode.restore(version, true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedOutNodeJcr2() throws RepositoryException { + versionManager.restore(version, true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedOutNodeJcr2_2() throws RepositoryException { + versionManager.restore(version, true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedOutNodeJcr2_3() throws RepositoryException { + versionManager.restore(versionableNode.getPath(), version.getName(), true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedOutNodeJcr2_4() throws RepositoryException { + versionManager.restore(new Version[] {version}, true); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreSetsIsCheckedOutToFalse() throws RepositoryException { + versionableNode.restore(version, true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionableNode.isCheckedOut()); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + public void testRestoreSetsIsCheckedOutToFalseJcr2() throws RepositoryException { + versionManager.restore(version, true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionManager.isCheckedOut(versionableNode.getPath())); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + public void testRestoreSetsIsCheckedOutToFalseJcr2_2() throws RepositoryException { + versionManager.restore(version, true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionManager.isCheckedOut(versionableNode.getPath())); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + public void testRestoreSetsIsCheckedOutToFalseJcr3() throws RepositoryException { + versionManager.restore(versionableNode.getPath(), version.getName(), true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionManager.isCheckedOut(versionableNode.getPath())); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + public void testRestoreSetsIsCheckedOutToFalseJcr2_4() throws RepositoryException { + versionManager.restore(new Version[] {version}, true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionManager.isCheckedOut(versionableNode.getPath())); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreCorrectProperty() throws RepositoryException { + versionableNode.restore(version, true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + public void testRestoreCorrectPropertyJcr2() throws RepositoryException { + versionManager.restore(version, true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + public void testRestoreCorrectPropertyJcr2_2() throws RepositoryException { + versionManager.restore(version, true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + public void testRestoreCorrectPropertyJcr2_3() throws RepositoryException { + versionManager.restore(versionableNode.getPath(), version.getName(), true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + public void testRestoreCorrectPropertyJcr2_4() throws RepositoryException { + versionManager.restore(new Version[] {version}, true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node sets the jcr:baseVersion property correctly. + * + * @throws javax.jcr.RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreSetsBaseVersion() throws RepositoryException { + versionableNode.restore(version, true); + Version baseV = versionableNode.getBaseVersion(); + assertTrue("Restoring a node must set node's base version in order to point to the restored version.", version.isSame(baseV)); + } + + /** + * Test if restoring a node sets the jcr:baseVersion property correctly. + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreSetsBaseVersionJcr2() throws RepositoryException { + versionManager.restore(version, true); + Version baseV = versionManager.getBaseVersion(versionableNode.getPath()); + assertTrue("Restoring a node must set node's base version in order to point to the restored version.", version.isSame(baseV)); + } + + /** + * Test if restoring a node sets the jcr:baseVersion property correctly. + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreSetsBaseVersionJcr2_2() throws RepositoryException { + versionManager.restore(version, true); + Version baseV = versionManager.getBaseVersion(versionableNode.getPath()); + assertTrue("Restoring a node must set node's base version in order to point to the restored version.", version.isSame(baseV)); + } + + /** + * Test if restoring a node sets the jcr:baseVersion property correctly. + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreSetsBaseVersionJcr2_3() throws RepositoryException { + versionManager.restore(versionableNode.getPath(), version.getName(), true); + Version baseV = versionManager.getBaseVersion(versionableNode.getPath()); + assertTrue("Restoring a node must set node's base version in order to point to the restored version.", version.isSame(baseV)); + } + + /** + * Test if restoring a node sets the jcr:baseVersion property correctly. + * + * @throws javax.jcr.RepositoryException + */ + public void testRestoreSetsBaseVersionJcr2_4() throws RepositoryException { + versionManager.restore(new Version[] {version}, true); + Version baseV = versionManager.getBaseVersion(versionableNode.getPath()); + assertTrue("Restoring a node must set node's base version in order to point to the restored version.", version.isSame(baseV)); + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreWithPendingChanges() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionableNode.restore(version, true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + public void testRestoreWithPendingChangesJcr2() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionManager.restore(version, true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + public void testRestoreWithPendingChangesJcr2_2() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionManager.restore(version, true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + public void testRestoreWithPendingChangesJcr2_3() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionManager.restore(versionableNode.getPath(), version.getName(), true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + public void testRestoreWithPendingChangesJcr2_4() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionManager.restore(new Version[] {version}, true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * VersionException expected on Node.restore(Version, boolean) if the + * specified version is not part of this node's version history. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreInvalidVersion() throws RepositoryException { + Version vNode2 = versionableNode2.checkin(); + try { + versionableNode.restore(vNode2, true); + + fail("VersionException expected on Node.restore(Version, boolean) if the specified version is not part of this node's version history."); + } catch (VersionException e) { + // ok + } + } + + /** + * VersionException expected on Node.restore(Version, boolean) if the + * specified version is not part of this node's version history. + * + * @throws RepositoryException + */ + public void testRestoreInvalidVersionJcr2() throws RepositoryException { + Version vNode2 = versionManager.checkin(versionableNode2.getPath()); + try { + versionManager.restore(versionableNode.getPath(), vNode2, true); + + fail("VersionException expected on Node.restore(Version, boolean) if the specified version is not part of this node's version history."); + } catch (VersionException e) { + // ok + } + } + + /** + * VersionException expected on Node.restore(String, boolean) if the specified version is not part of this node's version history. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreInvalidVersion2() throws RepositoryException { + String invalidName; + do { + invalidName = createRandomString(3); + for (VersionIterator it = versionableNode.getVersionHistory().getAllVersions(); it.hasNext();) { + Version v = it.nextVersion(); + if (invalidName.equals(v.getName())) { + invalidName = null; + break; + } + } + } while (invalidName == null); + + try { + versionableNode.restore(invalidName, true); + fail("VersionException expected on Node.restore(String, boolean) if the specified version is not part of this node's version history."); + } catch (VersionException e) { + // ok + } + } + + /** + * VersionException expected on Node.restore(String, boolean) if the specified version is not part of this node's version history. + * + * @throws RepositoryException + */ + public void testRestoreInvalidVersion2Jcr2() throws RepositoryException { + String invalidName; + do { + invalidName = createRandomString(3); + for (VersionIterator it = versionManager.getVersionHistory(versionableNode.getPath()).getAllVersions(); it.hasNext();) { + Version v = it.nextVersion(); + if (invalidName.equals(v.getName())) { + invalidName = null; + break; + } + } + } while (invalidName == null); + + try { + versionManager.restore(versionableNode.getPath(), invalidName, true); + fail("VersionException expected on Node.restore(String, boolean) if the specified version is not part of this node's version history."); + } catch (VersionException e) { + // ok + } + } + + /** + * Test calling Node.restore(String, boolean) on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(String, boolean) + */ + @SuppressWarnings("deprecation") + public void testRestoreNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.restore("foo", true); + fail("Node.restore(String, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test restoring on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(String, boolean) + */ + public void testRestoreNonVersionableNodeJcr2_2() throws RepositoryException { + try { + versionManager.restore(nonVersionableNode.getPath(), "foo", true); + fail("trying to restore on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling Node.restore(Version, String, boolean) on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(Version, String, boolean) + */ + public void testRestoreNonVersionableNode2() throws RepositoryException { + // the 'version' will be restored at location 'foo'. + + try { + nonVersionableNode.getParent().restore(version, nonVersionableNode.getName(), true); + fail("Node.restore(Version, String, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling Node.restore(Version, boolean) on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(Version, boolean) + */ + @SuppressWarnings("deprecation") + public void testRestoreNonVersionableNode3() throws RepositoryException { + try { + nonVersionableNode.restore(version, true); + fail("Node.restore(Version, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test restoring on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(Version, boolean) + */ + public void testRestoreNonVersionableNode3Jcr2_2() throws RepositoryException { + try { + versionManager.restore(nonVersionableNode.getPath(), version.getName(), true); + fail("Node.restore(Version, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test if restoring a node with an invalid Version throws a VersionException + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreWithInvalidVersion() throws RepositoryException { + Version invalidVersion = versionableNode2.checkin(); + try { + versionableNode.restore(invalidVersion, true); + fail("Node.restore(Version, boolean): A VersionException must be thrown if the specified version does not exists in this node's version history."); + } catch (VersionException e) { + // success + } + } + + /** + * Test if restoring a node with an invalid Version throws a VersionException + * + * @throws RepositoryException + */ + public void testRestoreWithInvalidVersionJcr2() throws RepositoryException { + Version invalidVersion = versionManager.checkin(versionableNode2.getPath()); + try { + versionManager.restore(versionableNode.getPath(), invalidVersion, true); + fail("Node.restore(Version, boolean): A VersionException must be thrown if the specified version does not exists in this node's version history."); + } catch (VersionException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + @SuppressWarnings("deprecation") + public void testRestoreWithUUIDConflict() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionableNode.checkin(); + versionableNode.checkout(); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionableNode.restore(v, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testRestoreWithUUIDConflictJcr2() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionManager.restore(v, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testRestoreWithUUIDConflictJcr2_2() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionManager.restore(v, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testRestoreWithUUIDConflictJcr2_3() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionManager.restore(versionableNode.getPath(), v.getName(), false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testRestoreWithUUIDConflictJcr2_4() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionManager.restore(new Version[] {v}, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + public void testRestoreChild1() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionableNode.checkin(); + versionableNode.checkout(); + Version v2 = versionableNode.checkin(); + + versionableNode.restore(v1, true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionableNode.restore(version, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionableNode.restore(v2, true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + @SuppressWarnings("deprecation") + public void testRestoreRemoved() throws RepositoryException { + Node parent = versionableNode.getParent(); + String oldName = versionableNode.getName(); + Version v1 = versionableNode.checkin(); + versionableNode.remove(); + versionableNode = null; + parent.getSession().save(); + + parent.restore(v1, oldName, true); + + versionableNode = parent.getNode(oldName); + + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue2, value); + } + + public void testRestoreRemovedJcr2() throws RepositoryException { + String path = versionableNode.getPath(); + Version v1 = versionManager.checkin(path); + versionableNode.remove(); + versionableNode = null; + superuser.save(); + + versionManager.restore(path, v1, true); + + versionableNode = superuser.getNode(path); + + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue2, value); + } + + public void testRestoreChild1Jcr2() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + Version v2 = versionManager.checkin(versionableNode.getPath()); + + versionManager.restore(v1, true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionManager.restore(version, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionManager.restore(v2, true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + public void testRestoreChild1Jcr2_2() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + Version v2 = versionManager.checkin(versionableNode.getPath()); + + versionManager.restore(v1, true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionManager.restore(version, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionManager.restore(v2, true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + public void testRestoreChild1Jcr2_3() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + Version v2 = versionManager.checkin(versionableNode.getPath()); + + versionManager.restore(versionableNode.getPath(), v1.getName(), true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionManager.restore(versionableNode.getPath(), version.getName(), true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionManager.restore(versionableNode.getPath(), v2.getName(), true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + public void testRestoreChild1Jcr2_4() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + Version v2 = versionManager.checkin(versionableNode.getPath()); + + versionManager.restore(new Version[] {v1}, true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionManager.restore(new Version[] {version}, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionManager.restore(new Version[] {v2}, true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + /** + * Test the restore of a versionable node using a label. + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreLabel() throws RepositoryException { + // mark V1 with label test1 + versionableNode.getVersionHistory().addVersionLabel(version.getName(), "test", true); + + // restore V1 via label. + versionableNode.restoreByLabel("test", true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Node.restore('test') not correctly restored", propertyValue1, value); + } + + /** + * Test the restore of a versionable node using a label. + * @throws RepositoryException + */ + public void testRestoreLabelJcr2() throws RepositoryException { + // mark V1 with label test1 + versionManager.getVersionHistory(versionableNode.getPath()).addVersionLabel(version.getName(), "test", true); + + // restore V1 via label. + versionManager.restoreByLabel(versionableNode.getPath(), "test", true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Node.restore('test') not correctly restored", propertyValue1, value); + } + + /** + * Test the restore of the OPV=Version child nodes. + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreName() throws RepositoryException, + NotExecutableException { + // V1.0 of versionableNode has no child + Node child1 = versionableNode.addNode(nodeName4); + ensureMixinType(child1, mixVersionable); + versionableNode.getSession().save(); + // create v1.0 of child + Version v1Child = child1.checkin(); + + // V1 of versionable node has child1 + String v1 = versionableNode.checkin().getName(); + + // create V1.1 of child + child1.checkout(); + child1.checkin(); + + // V2 of versionable node has child1 + versionableNode.checkout(); + String v2 = versionableNode.checkin().getName(); + + // restore 1.0 of versionable node --> no child + versionableNode.restore(version, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode(nodeName4)); + + // restore V1 via name. since child was checkin first, 1.0 should be restored + versionableNode.restore(v1, true); + assertTrue("Node.restore('test') must restore child node.", versionableNode.hasNode(nodeName4)); + child1 = versionableNode.getNode(nodeName4); + assertEquals("Node.restore('test') must restore child node version 1.0.", v1Child.getName(), child1.getBaseVersion().getName()); + + // JSR283 is more clear about restoring versionable OPV=VERSION nodes + // and states that an existing one is not restored when the parent + // is restored (see 15.7.5 Chained Versions on Restore) + + // Old JSR170 version: + // restore V2 via name. child should be 1.1 + // versionableNode.restore(v2, true); + // child1 = versionableNode.getNode(nodeName4); + // assertEquals("Node.restore('foo') must restore child node version 1.1.", v11Child.getName(), child1.getBaseVersion().getName()); + + // New JSR283 version: + // restore V2 via name. child should still be be 1.0 + versionableNode.restore(v2, true); + child1 = versionableNode.getNode(nodeName4); + assertEquals("Node.restore('foo') must not restore child node and keep version 1.0.", v1Child.getName(), child1.getBaseVersion().getName()); + } + + /** + * Test the restore of the OPV=Version child nodes. + * @throws RepositoryException + */ + public void testRestoreNameJcr2() throws RepositoryException, + NotExecutableException { + // V1.0 of versionableNode has no child + Node child1 = versionableNode.addNode(nodeName4); + ensureMixinType(child1, mixVersionable); + versionableNode.getSession().save(); + // create v1.0 of child + Version v1Child = versionManager.checkin(child1.getPath()); + + // V1 of versionable node has child1 + String v1 = versionManager.checkin(versionableNode.getPath()).getName(); + + // create V1.1 of child + versionManager.checkout(child1.getPath()); + versionManager.checkin(child1.getPath()); + + // V2 of versionable node has child1 + versionManager.checkout(versionableNode.getPath()); + String v2 = versionManager.checkin(versionableNode.getPath()).getName(); + + // restore 1.0 of versionable node --> no child + versionManager.restore(version, true); + assertFalse("restore must remove child node.", versionableNode.hasNode(nodeName4)); + + // restore V1 via name. since child was checkin first, 1.0 should be restored + versionManager.restore(versionableNode.getPath(), v1, true); + assertTrue("restore must restore child node.", versionableNode.hasNode(nodeName4)); + child1 = versionableNode.getNode(nodeName4); + assertEquals("restore must restore child node version 1.0.", v1Child.getName(), versionManager.getBaseVersion(child1.getPath()).getName()); + + // JSR283 is more clear about restoring versionable OPV=VERSION nodes + // and states that an existing one is not restored when the parent + // is restored (see 15.7.5 Chained Versions on Restore) + + // New JSR283 version: + // restore V2 via name. child should still be be 1.0 + versionManager.restore(versionableNode.getPath(), v2, true); + child1 = versionableNode.getNode(nodeName4); + assertEquals("Node.restore('foo') must not restore child node and keep version 1.0.", v1Child.getName(), versionManager.getBaseVersion(child1.getPath()).getName()); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreOrder() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + child1.checkin(); + child2.checkin(); + Version v1 = testRoot.checkin(); + + // remove node 1 + testRoot.checkout(); + child1.remove(); + testRoot.getSession().save(); + testRoot.checkin(); + + // restore version 1.0 + testRoot.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrderJcr2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // remove node 1 + versionManager.checkout(testRoot.getPath()); + child1.remove(); + testRoot.getSession().save(); + versionManager.checkout(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrderJcr2_2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // remove node 1 + versionManager.checkout(testRoot.getPath()); + child1.remove(); + testRoot.getSession().save(); + versionManager.checkout(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrderJcr2_3() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // remove node 1 + versionManager.checkout(testRoot.getPath()); + child1.remove(); + testRoot.getSession().save(); + versionManager.checkout(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(testRoot.getPath(), v1.getName(), true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrderJcr2_4() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // remove node 1 + versionManager.checkout(testRoot.getPath()); + child1.remove(); + testRoot.getSession().save(); + versionManager.checkout(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(new Version[] {v1}, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreOrder2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + child1.checkin(); + child2.checkin(); + Version v1 = testRoot.checkin(); + + // reoder nodes + testRoot.checkout(); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + testRoot.checkin(); + + // restore version 1.0 + testRoot.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrder2Jcr2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // reoder nodes + versionManager.checkout(testRoot.getPath()); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + versionManager.checkin(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrder2Jcr2_2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // reoder nodes + versionManager.checkout(testRoot.getPath()); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + versionManager.checkin(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrder2Jcr2_3() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // reoder nodes + versionManager.checkout(testRoot.getPath()); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + versionManager.checkin(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrder2Jcr2_4() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // reoder nodes + versionManager.checkout(testRoot.getPath()); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + versionManager.checkin(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(new Version[] {v1}, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/SessionMoveVersionExceptionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/SessionMoveVersionExceptionTest.java new file mode 100644 index 00000000000..bb6a2cee834 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/SessionMoveVersionExceptionTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.VersionException; + +/** + * SessionMoveVersionExceptionTest contains tests dealing with + * moving nodes using {@link javax.jcr.Session#move(String, String)}. + * + */ +public class SessionMoveVersionExceptionTest extends AbstractVersionTest { + + /** + * Tries to move a node using {@link javax.jcr.Session#move(String, String)} + * where the source parent is checked in. This should throw an {@link + * javax.jcr.version.VersionException}. + */ + public void testSessionMoveSourceCheckedInVersionException() throws RepositoryException { + // add a node under a versionable node + Node movingNode = versionableNode.addNode(nodeName1, nonVersionableNodeType.getName()); + versionableNode.save(); + // check the parent node in + versionableNode.checkin(); + try { + // try to move the sub node this should throw an VersionException + // either instantly or upon save() + superuser.move(movingNode.getPath(), nonVersionableNode.getPath() + "/" + nodeName1); + superuser.save(); + fail("Moving a node using Session.move() where parent node is " + + "versionable and checked in should throw a VersionException!"); + } catch (VersionException e) { + // ok, works as expected + } + } + + /** + * Tries to move a node using {@link javax.jcr.Session#move(String, String)} + * where the destination parent is checked in. This should throw an {@link + * javax.jcr.version.VersionException}. + */ + public void testSessionMoveDestCheckedInVersionException() throws RepositoryException { + // make sure versionable node is checked in + versionableNode.checkin(); + + try { + // try to move the sub node this should throw an VersionException either instantly or upon save() + superuser.move(nonVersionableNode.getPath(), versionableNode.getPath() + "/" + nodeName1); + superuser.save(); + fail("Moving a node using Session.save() where destination parent " + + "node is versionable and checked in should throw a VersionException!"); + } catch (VersionException e) { + // ok, works as expected + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/TestAll.java new file mode 100644 index 00000000000..dd2ae526d42 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/TestAll.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.test.api.version; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for the package + * javax.jcr.version. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("javax.jcr.version tests"); + + suite.addTestSuite(VersionTest.class); + suite.addTestSuite(VersionHistoryTest.class); + suite.addTestSuite(VersionStorageTest.class); + suite.addTestSuite(VersionLabelTest.class); + suite.addTestSuite(CheckoutTest.class); + suite.addTestSuite(CheckinTest.class); + suite.addTestSuite(CopyTest.class); + suite.addTestSuite(VersionGraphTest.class); + suite.addTestSuite(RemoveVersionTest.class); + suite.addTestSuite(RestoreTest.class); + suite.addTestSuite(WorkspaceRestoreTest.class); + suite.addTestSuite(OnParentVersionAbortTest.class); + suite.addTestSuite(OnParentVersionComputeTest.class); + suite.addTestSuite(OnParentVersionCopyTest.class); + suite.addTestSuite(OnParentVersionIgnoreTest.class); + suite.addTestSuite(OnParentVersionInitializeTest.class); + suite.addTestSuite(GetReferencesNodeTest.class); + suite.addTestSuite(GetPredecessorsTest.class); + suite.addTestSuite(GetCreatedTest.class); + suite.addTestSuite(GetContainingHistoryTest.class); + suite.addTestSuite(GetVersionableUUIDTest.class); + suite.addTestSuite(SessionMoveVersionExceptionTest.class); + suite.addTestSuite(WorkspaceMoveVersionExceptionTest.class); + suite.addTestSuite(MergeCancelMergeTest.class); + suite.addTestSuite(MergeCheckedoutSubNodeTest.class); + suite.addTestSuite(MergeDoneMergeTest.class); + suite.addTestSuite(MergeNodeIteratorTest.class); + suite.addTestSuite(MergeNodeTest.class); + suite.addTestSuite(MergeShallowTest.class); + suite.addTestSuite(MergeActivityTest.class); + suite.addTestSuite(MergeNonVersionableSubNodeTest.class); + suite.addTestSuite(MergeSubNodeTest.class); + + // JCR 2.0 + + suite.addTestSuite(ActivitiesTest.class); + suite.addTestSuite(ConfigurationsTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionGraphTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionGraphTest.java new file mode 100644 index 00000000000..7701cb57efb --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionGraphTest.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.test.api.version; + +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.Value; +import javax.jcr.UnsupportedRepositoryOperationException; + +/** + * VersionGraphTest contains test methods related to version graph + * issues. + * + */ +public class VersionGraphTest extends AbstractVersionTest { + + /** + * Test that the initial base version after creation of a versionable node + * points to the root version. + * + * @throws javax.jcr.RepositoryException + */ + public void testInitialBaseVersionPointsToRootVersion() throws RepositoryException { + + Version rV = versionableNode.getVersionHistory().getRootVersion(); + Version bV = versionableNode.getBaseVersion(); + + assertTrue("After creation of a versionable node the node's baseVersion must point to the rootVersion in the version history.", rV.isSame(bV)); + } + + /** + * Test that the initial base version after creation of a versionable node + * points to the root version. + * + * @throws javax.jcr.RepositoryException + */ + public void testInitialBaseVersionPointsToRootVersionJcr2() throws RepositoryException { + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version rV = versionManager.getVersionHistory(path).getRootVersion(); + Version bV = versionManager.getBaseVersion(path); + + assertTrue("After creation of a versionable node the node's baseVersion must point to the rootVersion in the version history.", rV.isSame(bV)); + } + + /** + * Test if after creation of a versionable node N the multi-value + * REFERENCE property jcr:predecessors of N is initialized to contain a + * single UUID, that of the root version (the same as jcr:baseVersion). + * + * @throws RepositoryException + */ + public void testInitialNodePredecessors() throws RepositoryException { + + Property predecessors = versionableNode.getProperty(jcrPredecessors); + Value[] values = predecessors.getValues(); + Version rV = versionableNode.getVersionHistory().getRootVersion(); + if (values.length != 1) { + fail("The jcr:predecessors property of a versionable node must be initialized to contain a single value"); + } + + Value initialVal = values[0]; + + assertTrue("The jcr:predecessors property of a versionable node is initialized to contain a single UUID, that of the root version", initialVal.equals(superuser.getValueFactory().createValue(rV))); + } + + /** + * Test if after creation of a versionable node N the multi-value + * REFERENCE property jcr:predecessors of N is initialized to contain a + * single UUID, that of the root version (the same as jcr:baseVersion). + * + * @throws RepositoryException + */ + public void testInitialNodePredecessorsJcr2() throws RepositoryException { + + Property predecessors = versionableNode.getProperty(jcrPredecessors); + Value[] values = predecessors.getValues(); + Version rV = versionableNode.getSession().getWorkspace().getVersionManager().getVersionHistory(versionableNode.getPath()).getRootVersion(); + if (values.length != 1) { + fail("The jcr:predecessors property of a versionable node must be initialized to contain a single value"); + } + + Value initialVal = values[0]; + + assertTrue("The jcr:predecessors property of a versionable node is initialized to contain a single UUID, that of the root version", initialVal.equals(superuser.getValueFactory().createValue(rV))); + } + + /** + * Test if the root version does not have any predecessor versions. + * + * @throws RepositoryException + */ + public void testRootVersionHasNoPredecessor() throws RepositoryException { + Version[] predec = versionableNode.getVersionHistory().getRootVersion().getPredecessors(); + assertTrue("The root version may not have any predecessors.", predec.length == 0); + } + + /** + * Test if the root version does not have any predecessor versions. + * + * @throws RepositoryException + */ + public void testRootVersionHasNoPredecessorJcr2() throws RepositoryException { + Version[] predec = versionableNode.getSession().getWorkspace().getVersionManager().getVersionHistory(versionableNode.getPath()).getRootVersion().getPredecessors(); + assertTrue("The root version may not have any predecessors.", predec.length == 0); + } + + /** + * Test if UnsupportedRepositoryOperationException is thrown when calling + * Node.getVersionHistory() on a non-versionable node. + * + * @throws RepositoryException + */ + public void testGetBaseVersionOnNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.getBaseVersion(); + fail("Node.getBaseVersion() must throw UnsupportedRepositoryOperationException if the node is not versionable."); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test if UnsupportedRepositoryOperationException is thrown when calling + * Node.getVersionHistory() on a non-versionable node. + * + * @throws RepositoryException + */ + public void testGetBaseVersionOnNonVersionableNodeJcr2() throws RepositoryException { + try { + nonVersionableNode.getSession().getWorkspace().getVersionManager().getBaseVersion(nonVersionableNode.getPath()); + fail("Node.getBaseVersion() must throw UnsupportedRepositoryOperationException if the node is not versionable."); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionHistoryTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionHistoryTest.java new file mode 100644 index 00000000000..034ef1d4e4a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionHistoryTest.java @@ -0,0 +1,1191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + + +/** + * VersionHistoryTest provides test methods related to version + * history methods and general version history issues. + * + */ +public class VersionHistoryTest extends AbstractVersionTest { + + protected VersionHistory vHistory; + private Version version; + private VersionManager versionManager; + + /** + * helper class used in testAccept() + */ + private class ItemVisitorTest implements ItemVisitor { + + Node ivtNode; + + public ItemVisitorTest(VersionHistory v) { + ivtNode = v; + } + + public void visit(Node node) throws RepositoryException { + assertTrue("VersionHistory.accept(ItemVisitor) does not provide the right node to the ItemVisitor", ivtNode.isSame(node)); + } + + public void visit(Property property) throws RepositoryException { + } + } + + protected void setUp() throws Exception { + super.setUp(); + + versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + version = versionManager.checkin(versionableNode.getPath()); + + vHistory = versionManager.getVersionHistory(versionableNode.getPath()); + + + if (vHistory == null) { + fail("VersionHistory must be created on persistent creation of a versionable node."); + } + } + + protected void tearDown() throws Exception { + vHistory = null; + version = null; + super.tearDown(); + } + + /** + * Test if initially there is an auto-created root version present in the + * version history. + */ + public void testAutocreatedRootVersion() throws RepositoryException { + Version rootVersion = vHistory.getRootVersion(); + if (rootVersion == null) { + fail("The version history must contain an autocreated root version"); + } + } + + /** + * The version history must initially contain two versions (root version + + * first test version). + * + * @throws RepositoryException + */ + public void testInitialNumberOfVersions() throws RepositoryException { + long initialSize = getNumberOfVersions(vHistory); + assertEquals("VersionHistory.getAllVersions() initially returns an iterator with two versions.", 2, initialSize); + } + + /** + * The version history must initially contain two versions (root version + + * first test version) - linear variant + * + * @throws RepositoryException + * @since JCR 2.0 + */ + public void testInitialNumberOfLinearVersions() throws RepositoryException { + long initialSize = getNumberOfVersions(vHistory); + long initialLinearSize = getSize(vHistory.getAllLinearVersions()); + long initialLinearFrozenSize = getSize(vHistory.getAllLinearFrozenNodes()); + + assertEquals("VersionHistory.getAllVersions() and .getAllLinearVersions should return the same number of versions for a purely linear version history.", + initialSize, initialLinearSize); + assertEquals("VersionHistory.getAllVersions() and .getAllLinearFrozenNodes should return the same number of nodes for a purely linear version history.", + initialSize, initialLinearFrozenSize); + } + + /** + * Test if the iterator returned by {@link javax.jcr.version.VersionHistory#getAllVersions()} + * contains the root version upon creation of the version history. + * + * @see javax.jcr.version.VersionHistory#getRootVersion() + */ + public void testInitiallyGetAllVersionsContainsTheRootVersion() throws RepositoryException { + Version rootVersion = vHistory.getRootVersion(); + boolean isContained = false; + for (VersionIterator it = vHistory.getAllVersions(); it.hasNext(); ) { + isContained |= it.nextVersion().isSame(rootVersion); + } + assertTrue("root version must be part of the version history", isContained); + } + + /** + * Test if the iterator returned by {@link javax.jcr.version.VersionHistory#getAllLinearVersions()} + * contains both the root and the base version upon creation of the version history. + * @since JCR 2.0 + */ + public void testInitiallyGetAllLinearVersionsContainsTheRootAndTheBaseVersion() throws RepositoryException { + + VersionManager vm = versionableNode.getSession().getWorkspace().getVersionManager(); + + List lvh = new ArrayList(); + for (VersionIterator it = vHistory.getAllLinearVersions(); it.hasNext(); ) { + lvh.add(it.nextVersion().getName()); + } + + String rootVersion = vm.getVersionHistory(versionableNode.getPath()).getRootVersion().getName(); + String baseVersion = vm.getBaseVersion(versionableNode.getPath()).getName(); + + assertTrue("root version " + rootVersion + " must be part of the linear version history: " + + lvh, lvh.contains(rootVersion)); + assertTrue("base version " + baseVersion + " must be part of the linear version history: " + + lvh, lvh.contains(baseVersion)); + } + + /** + * Test that {@link VersionHistory#getAllVersions()} returns an iterator + * containing the root version and all versions that have been created by + * Node.checkin(). + * + * @see javax.jcr.version.VersionHistory#getAllVersions() + */ + @SuppressWarnings("deprecation") + public void testGetAllVersions() throws RepositoryException { + int cnt = 5; + Map versions = new HashMap(); + Version v = vHistory.getRootVersion(); + versions.put(v.getUUID(), v); + for (int i = 0; i < cnt; i++) { + v = versionableNode.checkin(); + versions.put(v.getUUID(), v); + versionableNode.checkout(); + } + + VersionIterator it = vHistory.getAllVersions(); + while (it.hasNext()) { + v = it.nextVersion(); + if (!versions.containsKey(v.getUUID())) { + fail("VersionHistory.getAllVersions() must only contain the root version and versions, that have been created by a Node.checkin() call."); + } + versions.remove(v.getUUID()); + } + assertTrue("VersionHistory.getAllVersions() must contain the root version and all versions that have been created with a Node.checkin() call.", versions.isEmpty()); + } + + /** + * Test that {@link VersionHistory#getAllVersions()} returns an iterator + * containing the root version and all versions that have been created by + * Node.checkin(). + * + * @see javax.jcr.version.VersionHistory#getAllVersions() + */ + public void testGetAllVersionsJcr2() throws RepositoryException { + int cnt = 5; + Map versions = new HashMap(); + List vnames = new ArrayList(); + Version v = vHistory.getRootVersion(); + versions.put(v.getIdentifier(), v); + vnames.add(v.getIdentifier()); + for (int i = 0; i < cnt; i++) { + v = versionManager.checkin(versionableNode.getPath()); + vnames.add(v.getIdentifier()); + versions.put(v.getIdentifier(), v); + versionManager.checkout(versionableNode.getPath()); + } + + VersionIterator it = vHistory.getAllVersions(); + while (it.hasNext()) { + v = it.nextVersion(); + if (!versions.containsKey(v.getIdentifier())) { + fail("VersionHistory.getAllVersions() must only contain the root version and versions, that have been created by a Node.checkin() call."); + } + versions.remove(v.getIdentifier()); + // check order of linear version history (see JCR 2.0, 15.1.1.2) + assertEquals("versions in a linear version history should be sorted by creation time", vnames.remove(0), v.getIdentifier()); + } + assertTrue("VersionHistory.getAllVersions() must only contain the root version and all versions that have been created with a Node.checkin() call.", versions.isEmpty()); + } + + /** + * Test that {@link VersionHistory#getAllFrozenNodes()} returns an iterator + * containing the frozen nodes of all versions that have been created by + * {@link VersionManager#checkpoint(String)}. + * + * @see javax.jcr.version.VersionHistory#getAllFrozenNodes() + * @since JCR 2.0 + */ + public void testGetAllFrozenNodes() throws RepositoryException { + + VersionManager vm = versionableNode.getSession().getWorkspace().getVersionManager(); + + String path = versionableNode.getPath(); + int cnt = 2; + + for (int i = 0; i < cnt; i++) { + vm.checkpoint(path); + } + + Set frozenIds = new HashSet(); + for (VersionIterator it = vm.getVersionHistory(path).getAllVersions(); it.hasNext(); ) { + Version v = it.nextVersion(); + frozenIds.add(v.getFrozenNode().getIdentifier()); + } + + Set test = new HashSet(); + for (NodeIterator it = vHistory.getAllFrozenNodes(); it.hasNext(); ) { + Node n = it.nextNode(); + assertTrue("Node " + n.getPath() + " must be of type frozen node", + n.isNodeType("nt:frozenNode")); + test.add(n.getIdentifier()); + } + + assertEquals("getAllFrozenNodes must return the IDs of all frozen nodes", frozenIds, test); + } + + /** + * Test if UnsupportedRepositoryOperationException is thrown when calling + * Node.getVersionHistory() on a non-versionable node. + */ + @SuppressWarnings("deprecation") + public void testGetVersionHistoryOnNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.getVersionHistory(); + fail("Node.getVersionHistory() must throw UnsupportedRepositoryOperationException if the node is not versionable."); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test if UnsupportedRepositoryOperationException is thrown when calling + * Node.getVersionHistory() on a non-versionable node. + */ + public void testGetVersionHistoryOnNonVersionableNodeJcr2() throws RepositoryException { + try { + versionManager.getVersionHistory(nonVersionableNode.getPath()); + fail("Node.getVersionHistory() must throw UnsupportedRepositoryOperationException if the node is not versionable."); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test VersionHistory.getVersion(String versionName) if 'versionName' is + * the name of an existing version (created by Node.checkin()). + * + * @see VersionHistory#getVersion(String) + */ + public void testGetVersion() throws RepositoryException { + + Version v = versionManager.checkin(versionableNode.getPath()); + Version v2 = vHistory.getVersion(v.getName()); + + assertTrue("VersionHistory.getVersion(String versionName) must return the version that is identified by the versionName specified, if versionName is the name of a version created by Node.checkin().", v.isSame(v2)); + } + + /** + * Tests if VersionHistory.accept(ItemVisitor) accepts a + * ItemVisitor and if the right Node is provided to that visitor. + */ + public void testAccept() throws Exception { + ItemVisitorTest ivt = new ItemVisitorTest(vHistory); + vHistory.accept(ivt); + } + + /** + * Tests if VersionHistory.addMixin(String) throws a {@link + * javax.jcr.nodetype.ConstraintViolationException} + */ + public void testAddMixin() throws Exception { + try { + vHistory.addMixin(mixVersionable); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.addMixin(String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if VersionHistory.addNode(String) and + * VersionHistory.addNode(String, String) throw a {@link + * javax.jcr.nodetype.ConstraintViolationException} + */ + public void testAddNode() throws Exception { + try { + vHistory.addNode(nodeName4); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.addNode(String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.addNode(nodeName4, ntBase); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.addNode(String,String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if VersionHistory.canAddMixin(String) returns + * false + */ + public void testCanAddMixin() throws Exception { + assertFalse("VersionHistory should be read-only: VersionHistory.canAddMixin(String) returned true", vHistory.canAddMixin(mixVersionable)); + } + + /** + * Tests if VersionHistory.cancelMerge(Version) throws an + * {@link javax.jcr.UnsupportedRepositoryOperationException} + */ + @SuppressWarnings("deprecation") + public void testCancelMerge() throws Exception { + try { + vHistory.cancelMerge(version); + fail("VersionHistory.cancelMerge(Version) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.cancelMerge(Version) throws an + * {@link javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCancelMergeJcr2() throws Exception { + try { + versionManager.cancelMerge(vHistory.getPath(), version); + fail("VersionHistory.cancelMerge(Version) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.checkin() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + @SuppressWarnings("deprecation") + public void testCheckin() throws Exception { + try { + vHistory.checkin(); + fail("VersionHistory.checkin() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.checkin() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCheckinJcr2() throws Exception { + try { + versionManager.checkin(vHistory.getPath()); + fail("VersionHistory.checkin() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.checkout() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + @SuppressWarnings("deprecation") + public void testCheckout() throws Exception { + try { + vHistory.checkout(); + fail("VersionHistory.checkout() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.checkout() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCheckoutJcr2() throws Exception { + try { + versionManager.checkout(vHistory.getPath()); + fail("VersionHistory.checkout() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.doneMerge(Version) throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + @SuppressWarnings("deprecation") + public void testDoneMerge() throws Exception { + try { + vHistory.doneMerge(version); + fail("VersionHistory should not be versionable: VersionHistory.doneMerge(Version) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.doneMerge(Version) throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testDoneMergeJcr2() throws Exception { + try { + versionManager.doneMerge(vHistory.getPath(), version); + fail("VersionHistory should not be versionable: VersionHistory.doneMerge(Version) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.getAncestor(int) returns the right + * ancestor + */ + public void testGetAncestor() throws Exception { + assertTrue("VersionHistory.getAncestor(int) does not work", superuser.getRootNode().isSame(vHistory.getAncestor(0))); + } + + /** + * Tests if VersionHistory.getBaseVersion() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + @SuppressWarnings("deprecation") + public void testGetBaseVersion() throws Exception { + try { + vHistory.getBaseVersion(); + fail("VersionHistory.getBaseVersion() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.getBaseVersion() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testGetBaseVersionJcr2() throws Exception { + try { + versionManager.getBaseVersion(vHistory.getPath()); + fail("VersionHistory.getBaseVersion() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.getCorrespondingNodePath(String) + * returns the right path + */ + public void testGetCorrespondingNodePath() throws Exception { + assertEquals("VersionHistory.getCorrespondingNodePath(String) did not return the right path", vHistory.getPath(), vHistory.getCorrespondingNodePath(workspaceName)); + } + + /** + * Tests if VersionHistory.getDepth() returns the right depth + */ + public void testGetDepth() throws Exception { + assertTrue("VersionHistory.getDepth() mismatch", vHistory.getDepth() >= 3); + } + + /** + * Tests if VersionHistory.getIndex() returns the right index + */ + public void testGetIndex() throws Exception { + assertEquals("VersionHistory.getIndex() mismatch", 1, vHistory.getIndex()); + } + + /** + * Tests if VersionHistory.getLock() throws an {@link + * javax.jcr.lock.LockException} + */ + @SuppressWarnings("deprecation") + public void testGetLock() throws Exception { + try { + vHistory.getLock(); + fail("VersionHistory should not be lockable: VersionHistory.getLock() did not throw a LockException"); + } catch (LockException success) { + } catch (UnsupportedRepositoryOperationException maybe) { + assertFalse(isSupported(Repository.OPTION_LOCKING_SUPPORTED)); + } + } + + /** + * Tests if VersionHistory.getLock() throws an {@link + * javax.jcr.lock.LockException} + */ + public void testGetLockJcr2() throws Exception { + ensureLockingSupported(); + try { + vHistory.getSession().getWorkspace().getLockManager().getLock(vHistory.getPath()); + fail("VersionHistory should not be lockable: VersionHistory.getLock() did not throw a LockException"); + } catch (LockException success) { + } + } + + /** + * Tests if VersionHistory.getMixinNodeTypes() does not return + * null. + */ + public void testGetMixinNodeTypes() throws Exception { + NodeType[] ntArray = vHistory.getMixinNodeTypes(); + assertNotNull("VersionHistory.getMixinNodeTypes() returns null array", ntArray); + } + + /** + * Tests if VersionHistory.getName() returns the right name + */ + public void testGetName() throws Exception { + assertEquals("VersionHistory.getName() does not return the right name", version.getParent().getName(), vHistory.getName()); + } + + /** + * Tests if VersionHistory.getNode(String) returns the right + * child Node + */ + public void testGetNode() throws Exception { + assertTrue("VersionHistory.getNode(String) does not return a sub-node of type nt:version", vHistory.getNode(jcrRootVersion).isNodeType(ntVersion)); + } + + /** + * Tests if VersionHistory.getNodes() and + * VersionHistory.getNodes(String) returns the right child + * Node + */ + public void testGetNodes() throws Exception { + Node n = vHistory.getNodes().nextNode(); + assertTrue("VersionHistory.getNodes() does not return a sub-node of type nt:version", n.isNodeType(ntVersion) || n.isNodeType(ntVersionLabels)); + assertTrue("VersionHistory.getNodes(String) does not return a sub-node of type nt:version", vHistory.getNodes(superuser.getNamespacePrefix(NS_JCR_URI) + ":r*").nextNode().isNodeType(ntVersion)); + } + + /** + * Tests if VersionHistory.getParent() returns the right parent + * Node + */ + public void testGetParent() throws Exception { + assertTrue("VersionHistory.getParent() does not return the right parent-node", version.getAncestor(version.getDepth() - 2).isSame(vHistory.getParent())); + } + + /** + * Tests if VersionHistory.getPath() returns the right path + */ + public void testGetPath() throws Exception { + assertTrue("VersionHistory.getPath() does not return the right path", vHistory.getPath().startsWith("/" + superuser.getNamespacePrefix(NS_JCR_URI) + ":system/" + superuser.getNamespacePrefix(NS_JCR_URI) + ":versionStorage/")); + } + + /** + * Tests if VersionHistory.getPrimaryItem() throws a {@link + * javax.jcr.ItemNotFoundException} + */ + public void testGetPrimaryItem() throws Exception { + try { + vHistory.getPrimaryItem(); + fail("VersionHistory.getPrimaryItem() did not throw a ItemNotFoundException"); + } catch (ItemNotFoundException success) { + } + } + + /** + * Tests if VersionHistory.getPrimaryNodeType() returns the + * right primary node type nt:versionHistory + */ + public void testGetPrimaryNodeType() throws Exception { + assertEquals("VersionHistory does not have the primary node type nt:versionHistory", ntVersionHistory, vHistory.getPrimaryNodeType().getName()); + } + + /** + * Tests if VersionHistory.getProperties() and + * VersionHistory.getProperties(String) return the right + * property + */ + public void testGetProperties() throws Exception { + PropertyIterator pi = vHistory.getProperties(); + boolean hasPropertyUUID = false; + while (pi.hasNext()) { + if (pi.nextProperty().getName().equals(jcrUUID)) { + hasPropertyUUID = true; + } + } + assertTrue("VersionHistory.getProperties() does not return property jcr:UUID", hasPropertyUUID); + + pi = vHistory.getProperties(superuser.getNamespacePrefix(NS_JCR_URI) + ":*"); + hasPropertyUUID = false; + while (pi.hasNext()) { + if (pi.nextProperty().getName().equals(jcrUUID)) { + hasPropertyUUID = true; + } + } + assertTrue("VersionHistory.getProperties(String) does not return property jcr:UUID", hasPropertyUUID); + } + + /** + * Tests if VersionHistory.getProperty(String) returns the + * right property + */ + public void testGetProperty() throws Exception { + assertTrue("VersionHistory.getProperty(String) does not return property jcr:UUID", vHistory.getProperty(jcrUUID).getName().equals(jcrUUID)); + } + + /** + * Tests if VersionHistory.getReferences() returns the right + * reference of the versionable node + */ + public void testGetReferences() throws Exception { + PropertyIterator pi = vHistory.getReferences(); + boolean hasNodeReference = false; + while (pi.hasNext()) { + Property p = pi.nextProperty(); + if (p.getName().equals(jcrVersionHistory) && superuser.getNodeByUUID(p.getString()).isSame(vHistory)) { + hasNodeReference = true; + break; + } + } + assertTrue("VersionHistory.getReferences() does not return the jcr:versionHistory property of the versioned Node", hasNodeReference); + } + + /** + * Tests if VersionHistory.getSession() returns the right + * session + */ + public void testGetSession() throws Exception { + assertSame("VersionHistory.getSession() did not return the right session", superuser, vHistory.getSession()); + } + + /** + * Tests if VersionHistory.getUUID() returns the right UUID + */ + public void testGetUUID() throws Exception { + assertEquals("VersionHistory.getUUID() did not return the right UUID", versionableNode.getProperty(jcrVersionHistory).getString(), vHistory.getUUID()); + } + + /** + * Tests if VersionHistory.getIdentifier() returns the right UUID + */ + public void testGetIdentifier() throws Exception { + assertEquals("VersionHistory.getIdentifier() did not return the right Id", versionableNode.getProperty(jcrVersionHistory).getString(), vHistory.getIdentifier()); + } + + /** + * Tests if VersionHistory.getVersionHistory() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testGetVersionHistory() throws Exception { + try { + vHistory.getVersionHistory(); + fail("VersionHistory.getVersionHistory() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.getVersionHistory() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testGetVersionHistoryJcr2() throws Exception { + try { + versionManager.getVersionHistory(vHistory.getPath()); + fail("VersionHistory.getVersionHistory() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.hasNode(String) returns the right + * boolean value + */ + public void testHasNode() throws Exception { + assertTrue("VersionHistory.hasNode(String) did not return true", vHistory.hasNode(jcrRootVersion)); + } + + /** + * Tests if VersionHistory.hasNodes() returns + * true + */ + public void testHasNodes() throws Exception { + assertTrue("VersionHistory.hasNodes() did not return true", vHistory.hasNodes()); + } + + /** + * Tests if VersionHistory.hasProperties() returns + * true + */ + public void testHasProperties() throws Exception { + assertTrue("VersionHistory.hasProperties() did not return true", vHistory.hasProperties()); + } + + /** + * Tests if VersionHistory.hasProperty(String) returns the + * right boolean value + */ + public void testHasProperty() throws Exception { + assertTrue("VersionHistory.hasProperty(String) did not return true", vHistory.hasProperty(jcrUUID)); + } + + /** + * Tests if VersionHistory.holdsLock() returns + * false + */ + public void testHoldsLock() throws Exception { + ensureLockingSupported(); + assertFalse("VersionHistory.holdsLock() did not return false", vHistory.holdsLock()); + } + + /** + * Tests if VersionHistory.holdsLock() returns + * false + */ + public void testHoldsLockJcr2() throws Exception { + ensureLockingSupported(); + assertFalse("VersionHistory.holdsLock() did not return false", vHistory.getSession().getWorkspace().getLockManager().holdsLock(vHistory.getPath())); + } + + /** + * Tests if VersionHistory.isCheckedOut() returns + * true + */ + public void testIsCheckedOut() throws Exception { + assertTrue("VersionHistory.isCheckedOut() did not return true", vHistory.isCheckedOut()); + } + + /** + * Tests if VersionHistory.isCheckedOut() returns + * true + */ + public void testIsCheckedOutJcr2() throws Exception { + assertTrue("VersionHistory.isCheckedOut() did not return true", versionManager.isCheckedOut(vHistory.getPath())); + } + + /** + * Tests if VersionHistory.isLocked() returns + * false + */ + public void testIsLocked() throws Exception { + assertFalse("VersionHistory.isLocked() did not return false", vHistory.isLocked()); + } + + /** + * Tests if VersionHistory.isLocked() returns + * false + */ + public void testIsLockedJcr2() throws Exception { + ensureLockingSupported(); + assertFalse("VersionHistory.isLocked() did not return false", vHistory.getSession().getWorkspace().getLockManager().isLocked(vHistory.getPath())); + } + + /** + * Tests if VersionHistory.isModified() returns + * false + */ + public void testIsModified() throws Exception { + assertFalse("VersionHistory.isModified() did not return false", vHistory.isModified()); + } + + /** + * Tests if VersionHistory.isNew() returns false + */ + public void testIsNew() throws Exception { + assertFalse("VersionHistory.isNew() did not return false", vHistory.isNew()); + } + + /** + * Tests if VersionHistory.isNode() returns true + */ + public void testIsNode() throws Exception { + assertTrue("VersionHistory.isNode() did not return true", vHistory.isNode()); + } + + /** + * Tests if VersionHistory.isNodeType(String) returns the right + * boolean value + */ + public void testIsNodeType() throws Exception { + assertTrue("VersionHistory.isNodeType(String) did not return true for nt:versionHistory", vHistory.isNodeType(ntVersionHistory)); + } + + /** + * Tests if VersionHistory.isSame() returns the right + * boolean value + */ + public void testIsSame() throws Exception { + assertTrue("VersionHistory.isSame(Item) did not return true", vHistory.isSame(version.getParent())); + } + + /** + * Tests if VersionHistory.lock(boolean, boolean) throws a + * {@link javax.jcr.lock.LockException} + */ + @SuppressWarnings("deprecation") + public void testLock() throws Exception { + ensureLockingSupported(); + try { + vHistory.lock(true, true); + fail("VersionHistory should not be lockable: VersionHistory.lock(true,true) did not throw a LockException"); + } catch (LockException success) { + } + try { + vHistory.lock(true, false); + fail("VersionHistory should not be lockable: VersionHistory.lock(true,false) did not throw a LockException"); + } catch (LockException success) { + } + try { + vHistory.lock(false, true); + fail("VersionHistory should not be lockable: VersionHistory.lock(false,true) did not throw a LockException"); + } catch (LockException success) { + } + try { + vHistory.lock(false, false); + fail("VersionHistory should not be lockable: VersionHistory.lock(false,false) did not throw a UnsupportedRepositoryOperationException"); + } catch (LockException success) { + } + } + + /** + * Tests if VersionHistory.lock(boolean, boolean) throws a + * {@link javax.jcr.lock.LockException} + */ + public void testLockJcr2() throws Exception { + ensureLockingSupported(); + LockManager lockManager = vHistory.getSession().getWorkspace().getLockManager(); + String path = vHistory.getPath(); + try { + lockManager.lock(path, true, true, 60, ""); + fail("VersionHistory should not be lockable: VersionHistory.lock(true,true) did not throw a LockException"); + } catch (LockException success) { + } + try { + lockManager.lock(path, true, false, 60, ""); + fail("VersionHistory should not be lockable: VersionHistory.lock(true,false) did not throw a LockException"); + } catch (LockException success) { + } + try { + lockManager.lock(path, false, true, 60, ""); + fail("VersionHistory should not be lockable: VersionHistory.lock(false,true) did not throw a LockException"); + } catch (LockException success) { + } + try { + lockManager.lock(path, false, false, 60, ""); + fail("VersionHistory should not be lockable: VersionHistory.lock(false,false) did not throw a UnsupportedRepositoryOperationException"); + } catch (LockException success) { + } + } + + /** + * Tests if VersionHistory.merge(String) throws an + * {@link javax.jcr.nodetype.ConstraintViolationException} + */ + public void testMerge() throws Exception { + try { + vHistory.merge(workspaceName, true); + fail("VersionHistory.merge(String, true) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.merge(workspaceName, false); + fail("VersionHistory.merge(String, false) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if VersionHistory.merge(String) throws an + * {@link javax.jcr.nodetype.ConstraintViolationException} + */ +/* + TODO: check why this fails + public void testMergeJcr2() throws Exception { + try { + versionManager.merge(vHistory.getPath(), workspaceName, true); + fail("VersionHistory.merge(String, true) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + versionManager.merge(vHistory.getPath(), workspaceName, false); + fail("VersionHistory.merge(String, false) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } +*/ + /** + * Tests if VersionHistory.orderBefore(String, String) throws + * an {@link javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testOrderBefore() throws Exception { + try { + vHistory.orderBefore(jcrFrozenNode, null); + fail("VersionHistory.orderBefore(String,String) did not throw an UnsupportedRepositoryOperationException or a ConstraintViolationException"); + } catch (UnsupportedRepositoryOperationException success) { + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if VersionHistory.refresh(boolean) works as expected + * (do nothing and return quietly) + */ + public void testRefresh() throws Exception { + // should do nothing and return quietly + vHistory.refresh(true); + vHistory.refresh(false); + } + + /** + * Tests if VersionHistory.remove() throws an {@link + * javax.jcr.nodetype.ConstraintViolationException} + */ + public void testRemove() throws Exception { + try { + Node vHistoryParent = vHistory.getParent(); + vHistory.remove(); + vHistoryParent.save(); + fail("VersionHistory should be read-only: VersionHistory.remove() did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if VersionHistory.removeMixin(String) throws an {@link + * javax.jcr.nodetype.NoSuchNodeTypeException} + */ + public void testRemoveMixin() throws Exception { + try { + vHistory.removeMixin(mixReferenceable); + fail("VersionHistory does not have mixins: VersionHistory.removeMixin(String) did not throw a NoSuchNodeTypeException."); + } catch (ConstraintViolationException success) { + } catch (NoSuchNodeTypeException success) { + } + } + + /** + * Tests if VersionHistory.restore(String, boolean) and + * VersionHistory.restore(Version, boolean) throw an {@link + * UnsupportedRepositoryOperationException} and VersionHistory.restore(Version, + * String, boolean) throws a {@link ConstraintViolationException}. + */ + public void testRestore() throws Exception { + try { + vHistory.restore("abc", true); + fail("VersionHistory.restore(String,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + try { + vHistory.restore(version, true); + fail("VersionHistory.restore(Version,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + try { + vHistory.restore(version, "abc", true); + fail("VersionHistory.restore(Version,String,boolean) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if VersionHistory.restore(String, boolean) and + * VersionHistory.restore(Version, boolean) throw an {@link + * UnsupportedRepositoryOperationException} and VersionHistory.restore(Version, + * String, boolean) throws a {@link ConstraintViolationException}. + */ + public void testRestoreJcr2() throws Exception { + try { + versionManager.restore(vHistory.getPath(), "abc", true); + fail("VersionHistory.restore(String,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.restoreByLabel(String, boolean) + * throws an {@link javax.jcr.UnsupportedRepositoryOperationException} + */ + @SuppressWarnings("deprecation") + public void testRestoreByLabel() throws Exception { + try { + vHistory.restoreByLabel("abc", true); + fail("VersionHistory.restoreByLabel(String,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if VersionHistory.restoreByLabel(String, boolean) + * throws an {@link javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testRestoreByLabelJcr2() throws Exception { + try { + versionManager.restoreByLabel(vHistory.getPath(), "abc", true); + fail("VersionHistory.restoreByLabel(String,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if
        • VersionHistory.setProperty(String, + * String[])
        • VersionHistory.setProperty(String, + * String[], int)
        • VersionHistory.setProperty(String, + * Value[])
        • VersionHistory.setProperty(String, + * Value[], int)
        • VersionHistory.setProperty(String, + * boolean)
        • VersionHistory.setProperty(String, + * double)
        • VersionHistory.setProperty(String, + * InputStream)
        • VersionHistory.setProperty(String, + * String)
        • VersionHistory.setProperty(String, + * Calendar)
        • VersionHistory.setProperty(String, + * Node)
        • VersionHistory.setProperty(String, + * Value)
        • VersionHistory.setProperty(String, + * long)
        all throw a {@link javax.jcr.nodetype.ConstraintViolationException} + */ + public void testSetProperty() throws Exception { + + // create Value[] object + Value[] vArray = new Value[3]; + vArray[0] = superuser.getValueFactory().createValue("abc"); + vArray[1] = superuser.getValueFactory().createValue("xyz"); + vArray[2] = superuser.getValueFactory().createValue("123"); + + // create String array + String[] s = {"abc", "xyz", "123"}; + + try { + vHistory.setProperty(propertyName1, s); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,String[]) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.setProperty(propertyName1, s, PropertyType.STRING); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,String[],int) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.setProperty(propertyName1, vArray); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,Value[]) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.setProperty(propertyName1, vArray, PropertyType.STRING); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,Value[],int]) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.setProperty(propertyName1, true); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,boolean) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.setProperty(propertyName1, 123); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,double) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + byte[] bytes = {73, 26, 32, -36, 40, -43, -124}; + InputStream inpStream = new ByteArrayInputStream(bytes); + vHistory.setProperty(propertyName1, inpStream); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,InputStream) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.setProperty(propertyName1, "abc"); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + Calendar c = new GregorianCalendar(1945, 1, 6, 16, 20, 0); + vHistory.setProperty(propertyName1, c); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,Calendar) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.setProperty(propertyName1, vHistory); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,Node) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + Value v = superuser.getValueFactory().createValue("abc"); + vHistory.setProperty(propertyName1, v); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,Value) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + vHistory.setProperty(propertyName1, -2147483650L); + vHistory.getSession().save(); + fail("VersionHistory should be read-only: VersionHistory.setProperty(String,long) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if VersionHistory.unlock() throws a {@link + * javax.jcr.lock.LockException} + */ + public void testUnlock() throws Exception { + ensureLockingSupported(); + try { + vHistory.unlock(); + fail("VersionHistory should not be lockable: VersionHistory.unlock() did not throw a LockException"); + } catch (LockException success) { + } + } + + /** + * Tests if VersionHistory.unlock() throws a {@link + * javax.jcr.lock.LockException} + */ + public void testUnlockJcr2() throws Exception { + ensureLockingSupported(); + try { + vHistory.getSession().getWorkspace().getLockManager().unlock(vHistory.getPath()); + fail("VersionHistory should not be lockable: VersionHistory.unlock() did not throw a LockException"); + } catch (LockException success) { + } + } + + /** + * Tests if VersionHistory.update(String) throws an + * {@link javax.jcr.nodetype.ConstraintViolationException} + */ + public void testUpdate() throws Exception { + try { + vHistory.update(workspaceName); + fail("VersionHistory.update(String) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionLabelTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionLabelTest.java new file mode 100644 index 00000000000..cd5842e9fb5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionLabelTest.java @@ -0,0 +1,509 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; + +/** + * VersionLabelTest covers methods related to version label such as + *
          + *
        • {@link VersionHistory#addVersionLabel(String, String, boolean)}
        • + *
        • {@link VersionHistory#removeVersionLabel(String)}
        • + *
        • {@link VersionHistory#restoreByLabel(String, boolean)}
        • + *
        • {@link VersionHistory#getVersionByLabel(String)}
        • + *
        • {@link javax.jcr.version.VersionHistory#getVersionLabels()}
        • + *
        • {@link javax.jcr.version.VersionHistory#hasVersionLabel(javax.jcr.version.Version, String)}
        • + *
        • {@link VersionHistory#hasVersionLabel(String)}
        • + *
        • {@link VersionHistory#hasVersionLabel(javax.jcr.version.Version, String)}
        • + *
        + * + */ +public class VersionLabelTest extends AbstractVersionTest { + + protected String versionLabel = "foo"; + protected String versionLabel2 = "bar"; + + protected VersionHistory vHistory; + protected Version version; + + /** + * JCR Name jcr:versionLabels using the namespace resolver of the current session. + */ + protected String jcrVersionLabels; + + protected void setUp() throws Exception { + super.setUp(); + + jcrVersionLabels = superuser.getNamespacePrefix(NS_JCR_URI) + ":versionLabels"; + + vHistory = versionableNode.getSession().getWorkspace().getVersionManager().getVersionHistory(versionableNode.getPath()); + VersionManager vMgr = superuser.getWorkspace().getVersionManager(); + vMgr.checkpoint(versionableNode.getPath()); + version = vMgr.getBaseVersion(versionableNode.getPath()); + + if (vHistory.hasVersionLabel(versionLabel)) { + fail("Version label '" + versionLabel + "' is already present in this version history. Label test cannot be performed."); + } + + if (vHistory.hasVersionLabel(versionLabel2)) { + fail("Version label '" + versionLabel2 + "' is already present in this version history. Label test cannot be performed."); + } + } + + protected void tearDown() throws Exception { + try { + // clean up: remove the version labels again. + vHistory.removeVersionLabel(versionLabel); + vHistory.removeVersionLabel(versionLabel2); + } catch (RepositoryException e) { + // ignore + } + vHistory = null; + version = null; + super.tearDown(); + } + + /** + * Test if the number of labels available in the version history is increased + * by added a new label. + * + * @throws RepositoryException + * @see VersionHistory#addVersionLabel(String, String, boolean) + */ + public void testAddVersionLabel() throws RepositoryException { + int initialLength = vHistory.getVersionLabels().length; + vHistory.addVersionLabel(version.getName(), versionLabel, false); + String[] labels = vHistory.getVersionLabels(); + + assertEquals("A version label that has been successfully added must increes the total number of version labels available in the history.", initialLength + 1, labels.length); + } + + /** + * Test if the a label added with VersionHistory.addVersionLabel(String, String, boolean) + * is present in the array returned by VersionHistory.getVersionLabels(), if + * the label has not been present before. + * + * @throws RepositoryException + * @see VersionHistory#addVersionLabel(String, String, boolean) + */ + public void testAddVersionLabel2() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + String[] labels = vHistory.getVersionLabels(); + boolean found = false; + for (int i = 0; i < labels.length; i++) { + if (labels[i].equals(versionLabel)) { + found = true; + break; + } + } + assertTrue("The version label that has been successfully added must be present in the array containing all labels.", found); + } + + /** + * Test if the a label added with VersionHistory.addVersionLabel(String, + * String, boolean) corresponds to adding a reference property to the + * jcr:versionLabels node of this history node, with the label as name of + * the property, and the reference targeting the version. + * + * @see VersionHistory#addVersionLabel(String, String, boolean) + */ + @SuppressWarnings("deprecation") + public void testAddVersionCheckVersionLabelsNode() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + + // get jcr:versionLabels node + vHistory = versionableNode.getVersionHistory(); + Node versionLabelNode = vHistory.getNode(jcrVersionLabels); + + assertTrue("The version label that has been successfully added must be present in the node '" + jcrVersionLabels + "'.", versionLabelNode.getProperty(versionLabel).getString().equals(version.getUUID())); + } + + /** + * Test if the a label added with VersionHistory.addVersionLabel(String, + * String, boolean) corresponds to adding a reference property to the + * jcr:versionLabels node of this history node, with the label as name of + * the property, and the reference targeting the version. + * + * @see VersionHistory#addVersionLabel(String, String, boolean) + */ + public void testAddVersionCheckVersionLabelsNodeJcr2() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + + // get jcr:versionLabels node + vHistory = versionableNode.getSession().getWorkspace().getVersionManager().getVersionHistory(versionableNode.getPath()); + Node versionLabelNode = vHistory.getNode(jcrVersionLabels); + + assertTrue("The version label that has been successfully added must be present in the node '" + jcrVersionLabels + "'.", versionLabelNode.getProperty(versionLabel).getString().equals(version.getUUID())); + } + + /** + * Test if VersionHistory.hasVersionLabel(String) returns true, if the label + * has beed successfully added before. + * + * @throws RepositoryException + * @see VersionHistory#hasVersionLabel(String) + */ + public void testHasVersionLabel() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + assertTrue("VersionHistory.hasVersionLabel(String) must return true if the label has been sucessfully added.", vHistory.hasVersionLabel(versionLabel)); + } + + /** + * Test if VersionHistory.hasVersionLabel(Version, String) returns true, if the label + * has beed successfully added before to the specified version. + * + * @throws RepositoryException + * @see VersionHistory#hasVersionLabel(javax.jcr.version.Version, String) + */ + public void testHasVersionLabelForVersion() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + assertTrue("VersionHistory.hasVersionLabel(Version, String) must return true if the label has been sucessfully added.", vHistory.hasVersionLabel(version, versionLabel)); + } + + /** + * Test if multiple distinct version labels can be added for a given version. + * + * @throws RepositoryException + */ + public void testAddMultipleVersionLabels() throws RepositoryException { + try { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + vHistory.addVersionLabel(version.getName(), versionLabel2, false); + } catch (VersionException e) { + fail("Adding multiple distict version labels to a version must be allowed."); + } + } + + /** + * Test if VersionHistory.addVersionLabel(versionName, label, moveLabel) + * throws VersionException the label already exists and if moveLabel is false) + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testAddDuplicateVersionLabel() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + try { + versionableNode.checkout(); + Version v = versionableNode.checkin(); + vHistory.addVersionLabel(v.getName(), versionLabel, false); + + fail("Adding a version label that already exist in the version history must throw a VersionException."); + } catch (VersionException e) { + //success + } + } + + /** + * Test if VersionHistory.addVersionLabel(versionName, label, moveLabel) + * throws VersionException the label already exists and if moveLabel is false) + * + * @throws RepositoryException + */ + public void testAddDuplicateVersionLabelJcr2() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + try { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + vHistory.addVersionLabel(v.getName(), versionLabel, false); + + fail("Adding a version label that already exist in the version history must throw a VersionException."); + } catch (VersionException e) { + //success + } + } + + /** + * Test if the 'moveLabel' flag moves an existing version label. + * + * @throws RepositoryException + * @see VersionHistory#addVersionLabel(String, String, boolean) with boolan flag equals true. + */ + @SuppressWarnings("deprecation") + public void testMoveLabel() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + try { + versionableNode.checkout(); + Version v = versionableNode.checkin(); + vHistory.addVersionLabel(v.getName(), versionLabel, true); + + if (!vHistory.hasVersionLabel(v, versionLabel)) { + fail("If 'moveLabel' is true, an existing version label must be moved to the indicated version."); + } + + } catch (VersionException e) { + fail("If 'moveLabel' is true, an existing version label must be moved to the indicated version."); + } + } + + /** + * Test if the 'moveLabel' flag moves an existing version label. + * + * @throws RepositoryException + * @see VersionHistory#addVersionLabel(String, String, boolean) with boolan flag equals true. + */ + public void testMoveLabelJcr2() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + try { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + vHistory.addVersionLabel(v.getName(), versionLabel, true); + + if (!vHistory.hasVersionLabel(v, versionLabel)) { + fail("If 'moveLabel' is true, an existing version label must be moved to the indicated version."); + } + + } catch (VersionException e) { + fail("If 'moveLabel' is true, an existing version label must be moved to the indicated version."); + } + } + + /** + * Test the removal of an version label that does not exist (must throw VersionException). + * + * @throws RepositoryException + */ + public void testRemoveNonExistingLabel() throws RepositoryException { + if (vHistory.hasVersionLabel(versionLabel)) { + fail("Testing the removal on a non-existing version label failed: '" + versionLabel + "' exists on version history."); + } + try { + vHistory.removeVersionLabel(versionLabel); + fail("VersionHistory.removeLabel(String) must throw a VersionException if the label does not exist."); + } catch (VersionException e) { + // success + } + } + + /** + * Test the removal of an version label that has successfully been added + * before. + * + * @throws RepositoryException + * @see VersionHistory#removeVersionLabel(String) + */ + public void testRemoveLabel() throws RepositoryException { + + vHistory.addVersionLabel(version.getName(), versionLabel, false); + vHistory.removeVersionLabel(versionLabel); + + assertFalse("VersionHistory.removeLabel(String) must remove the version label if it exists (has successfully been added before).", vHistory.hasVersionLabel(versionLabel)); + } + + /** + * Test if VersionHistory.getVersionByLabel(String) returns the version that + * has been specified with the addVersionLabel call. + * + * @throws RepositoryException + * @see VersionHistory#getVersionByLabel(String) + */ + public void testGetVersionByLabel() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, true); + Version v = vHistory.getVersionByLabel(versionLabel); + + assertTrue("VersionHistory.getVersionByLabel(String) must retrieve the particular version that was specified in addVersionLabel call.", v.isSame(version)); + } + + /** + * Test VersionHistory.getVersionLabels() returns all labels present on the version history. + * + * @throws RepositoryException + * @see javax.jcr.version.VersionHistory#getVersionLabels() + */ + @SuppressWarnings("deprecation") + public void testGetVersionLabels() throws RepositoryException { + + Set testLabels = new HashSet(Arrays.asList(vHistory.getVersionLabels())); + versionableNode.checkout(); + Version v = versionableNode.checkin(); + + vHistory.addVersionLabel(v.getName(), versionLabel, false); + testLabels.add(versionLabel); + vHistory.addVersionLabel(version.getName(), versionLabel2, false); + testLabels.add(versionLabel2); + + String[] labels = vHistory.getVersionLabels(); + for (int i = 0; i < labels.length; i++) { + String l = labels[i]; + if (!testLabels.contains(l)) { + fail("VersionHistory.getVersionLabels() must only return labels, that have been added to the history."); + } + testLabels.remove(l); + } + + assertTrue("VersionHistory.getVersionLabels() must return all labels, that have been added to the history.", testLabels.isEmpty()); + } + + /** + * Test VersionHistory.getVersionLabels() returns all labels present on the version history. + * + * @throws RepositoryException + * @see javax.jcr.version.VersionHistory#getVersionLabels() + */ + public void testGetVersionLabelsJcr2() throws RepositoryException { + + Set testLabels = new HashSet(Arrays.asList(vHistory.getVersionLabels())); + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + + vHistory.addVersionLabel(v.getName(), versionLabel, false); + testLabels.add(versionLabel); + vHistory.addVersionLabel(version.getName(), versionLabel2, false); + testLabels.add(versionLabel2); + + String[] labels = vHistory.getVersionLabels(); + for (int i = 0; i < labels.length; i++) { + String l = labels[i]; + if (!testLabels.contains(l)) { + fail("VersionHistory.getVersionLabels() must only return labels, that have been added to the history."); + } + testLabels.remove(l); + } + + assertTrue("VersionHistory.getVersionLabels() must return all labels, that have been added to the history.", testLabels.isEmpty()); + } + + /** + * Test VersionHistory.getVersionLabels(Version) only returns all labels present + * for the specified version. + * + * @throws RepositoryException + * @see VersionHistory#getVersionLabels(javax.jcr.version.Version) + */ + @SuppressWarnings("deprecation") + public void testGetVersionLabelsForVersion() throws RepositoryException { + + Set testLabels = new HashSet(Arrays.asList(vHistory.getVersionLabels(version))); + + vHistory.addVersionLabel(version.getName(), versionLabel, false); + testLabels.add(versionLabel); + + // add a version label to another version (not added to the testLabel set) + versionableNode.checkout(); + Version v = versionableNode.checkin(); + vHistory.addVersionLabel(v.getName(), versionLabel2, false); + + String[] labels = vHistory.getVersionLabels(version); + for (int i = 0; i < labels.length; i++) { + String l = labels[i]; + if (!testLabels.contains(l)) { + fail("VersionHistory.getVersionLabels(Version) must only return labels, that have been added for this version."); + } + testLabels.remove(l); + } + + assertTrue("VersionHistory.getVersionLabels(Version) must return all labels, that have been added for this version.", testLabels.isEmpty()); + } + + /** + * Test VersionHistory.getVersionLabels(Version) only returns all labels present + * for the specified version. + * + * @throws RepositoryException + * @see VersionHistory#getVersionLabels(javax.jcr.version.Version) + */ + public void testGetVersionLabelsForVersionJcr2() throws RepositoryException { + + Set testLabels = new HashSet(Arrays.asList(vHistory.getVersionLabels(version))); + + vHistory.addVersionLabel(version.getName(), versionLabel, false); + testLabels.add(versionLabel); + + // add a version label to another version (not added to the testLabel set) + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + Version v = versionManager.checkin(path); + vHistory.addVersionLabel(v.getName(), versionLabel2, false); + + String[] labels = vHistory.getVersionLabels(version); + for (int i = 0; i < labels.length; i++) { + String l = labels[i]; + if (!testLabels.contains(l)) { + fail("VersionHistory.getVersionLabels(Version) must only return labels, that have been added for this version."); + } + testLabels.remove(l); + } + + assertTrue("VersionHistory.getVersionLabels(Version) must return all labels, that have been added for this version.", testLabels.isEmpty()); + } + + /** + * Test calling Node.restoreByLabel(String, boolean) on a non-versionable node. + * + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#restoreByLabel(String, boolean) + */ + @SuppressWarnings("deprecation") + public void testRestoreByLabelNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.restoreByLabel(versionLabel, true); + fail("Node.restoreByLabel(String, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling Node.restoreByLabel(String, boolean) on a non-versionable node. + * + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#restoreByLabel(String, boolean) + */ + public void testRestoreByLabelNonVersionableNodeJcr2() throws RepositoryException { + try { + nonVersionableNode.getSession().getWorkspace().getVersionManager().restoreByLabel(nonVersionableNode.getPath(), versionLabel, true); + fail("Node.restoreByLabel(String, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test if versionHistory.getVersionLabels(Version) throws a VersionException if the + * specified version is not in this version history. + */ + public void testGetVersionLabelsForInvalidVersion() throws Exception { + // build a second versionable node below the testroot to get it's version. + Node versionableNode2 = createVersionableNode(testRootNode, nodeName2, versionableNodeType); + Version invalidV = versionableNode2.checkin(); + + try { + vHistory.getVersionLabels(invalidV); + fail("VersionHistory.getVersionLabels(Version) must throw a VersionException if the specified version is not in this version history"); + } catch (VersionException ve) { + // success + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionStorageTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionStorageTest.java new file mode 100644 index 00000000000..5376ccd308e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionStorageTest.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.test.api.version; + +import java.util.GregorianCalendar; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.ConstraintViolationException; + +/** + * VersionStorageTest provides tests regarding {@link + * javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean)} + * + */ +public class VersionStorageTest extends AbstractVersionTest { + + // path to version storage + protected String versionStoragePath; + + protected void setUp() throws Exception { + super.setUp(); + + // get versionStorage path + versionStoragePath = superuser.getNamespacePrefix(NS_JCR_URI) + ":system/" + superuser.getNamespacePrefix(NS_JCR_URI) + ":versionStorage"; + } + + /** + * Entire subtree is protected. + */ + public void testVersionStorageProtected() throws RepositoryException { + try { + versionableNode.getBaseVersion().setProperty(jcrCreated, GregorianCalendar.getInstance()); + fail("It should not be possible to modify a subnode/version in version storage."); + } catch (ConstraintViolationException e) { + // success + } + } + + /** + * The full set of version histories in the version storage, though stored + * in a single location in the repository, must be reflected in each + * workspace as a subtree below the node /jcr:system/jcr:versionStorage. + * Entire subtree must be identical across all workspaces and is protected. + */ + public void testVersionStorageIdenticalAcrossAllWorkspaces() throws RepositoryException { + // The superuser session for the second workspace + Session superuserW2 = getHelper().getSuperuserSession(workspaceName); + + try { + // check path to version storage + assertTrue("Version strorage must be reflected as a subtree below the node '" + versionStoragePath + "'", superuserW2.getRootNode().hasNode(versionStoragePath)); + + // check if subnodes in versionStorage are protected + try { + // try to create a version node + Node versionStorageNodeW2 = superuserW2.getRootNode().getNode(versionStoragePath); + versionStorageNodeW2.addNode(nodeName1, ntVersion); + fail("It should not be possible to add a subnode/version in version storage."); + } catch (ConstraintViolationException e) { + // success + } + } finally { + superuserW2.logout(); + } + } + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionTest.java new file mode 100644 index 00000000000..778eb902a58 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/VersionTest.java @@ -0,0 +1,934 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PropertyIterator; +import javax.jcr.Value; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.lock.LockException; +import javax.jcr.lock.LockManager; + +import java.util.GregorianCalendar; +import java.util.Calendar; +import java.util.List; +import java.util.Arrays; +import java.io.InputStream; +import java.io.ByteArrayInputStream; + +/** + * VersionTest covers tests related to the methods of the {@link + * javax.jcr.version.Version} class. + * + */ +public class VersionTest extends AbstractVersionTest { + + private VersionManager versionManager; + private Version version; + private Version version2; + + /** + * helper class used in testAccept() + */ + private class ItemVisitorTest implements ItemVisitor { + + Node ivtNode; + + public ItemVisitorTest(Version v) { + ivtNode = v; + } + + public void visit(Node node) throws RepositoryException { + assertTrue("Version.accept(ItemVisitor) does not provide the right node to the ItemVisitor", ivtNode.isSame(node)); + } + + public void visit(Property property) throws RepositoryException { + } + } + + protected void setUp() throws Exception { + super.setUp(); + + versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + + // create two versions + version = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + version2 = versionManager.checkin(versionableNode.getPath()); + } + + protected void tearDown() throws Exception { + // check the node out, so that it can be removed + versionManager.checkout(versionableNode.getPath()); + version = null; + version2 = null; + super.tearDown(); + } + + /** + * Tests if Version.accept(ItemVisitor) accepts a ItemVisitor + * and if the right Node is provided to that visitor. + */ + public void testAccept() throws Exception { + ItemVisitorTest ivt = new ItemVisitorTest(version); + version.accept(ivt); + } + + /** + * Tests if Version.addMixin(String) throws a {@link + * javax.jcr.nodetype.ConstraintViolationException} + */ + public void testAddMixin() throws Exception { + try { + version.addMixin(mixVersionable); + fail("Version should be read-only: Version.addMixin(String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if Version.addNode(String) and + * Version.addNode(String, String) throw a {@link + * javax.jcr.nodetype.ConstraintViolationException} + */ + public void testAddNode() throws Exception { + try { + version.addNode(nodeName4); + version.getSession().save(); + fail("Version should be read-only: Version.addNode(String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.addNode(nodeName4, ntBase); + version.getSession().save(); + fail("Version should be read-only: Version.addNode(String,String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if Version.canAddMixin(String) returns + * false + */ + public void testCanAddMixin() throws Exception { + assertFalse("Version should be read-only: Version.canAddMixin(String) returned true", version.canAddMixin(mixVersionable)); + } + + /** + * Tests if Version.cancelMerge(Version) throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCancelMerge() throws Exception { + try { + version.cancelMerge(version2); + fail("Version.cancelMerge(Version) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.cancelMerge(Version) throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCancelMergeJcr2() throws Exception { + try { + versionManager.cancelMerge(version.getPath(), version2); + fail("Version.cancelMerge(Version) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.checkin() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCheckin() throws Exception { + try { + version.checkin(); + fail("Version.checkin() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.checkin() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCheckinJcr2() throws Exception { + try { + versionManager.checkin(version.getPath()); + fail("Version.checkin() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.checkout() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCheckout() throws Exception { + try { + version.checkout(); + fail("Version.checkout() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.checkout() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testCheckoutJcr2() throws Exception { + try { + versionManager.checkout(version.getPath()); + fail("Version.checkout() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.doneMerge(Version) throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testDoneMerge() throws Exception { + try { + version.doneMerge(version2); + fail("Version should not be versionable: Version.doneMerge(Version) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.doneMerge(Version) throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testDoneMergeJcr2() throws Exception { + try { + versionManager.doneMerge(version.getPath(), version2); + fail("Version should not be versionable: Version.doneMerge(Version) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.getAncestor(int) returns the right + * ancestor + */ + public void testGetAncestor() throws Exception { + assertTrue("Version.getAncestor(int) does not work", superuser.getRootNode().isSame(version.getAncestor(0))); + } + + /** + * Tests if Version.getBaseVersion() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testGetBaseVersion() throws Exception { + try { + version.getBaseVersion(); + fail("Version.getBaseVersion() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.getBaseVersion() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testGetBaseVersionJcr2() throws Exception { + try { + versionManager.getBaseVersion(version.getPath()); + fail("Version.getBaseVersion() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.getCorrespondingNodePath(String) returns + * the right path + */ + public void testGetCorrespondingNodePath() throws Exception { + assertEquals("Version.getCorrespondingNodePath(String) did not return the right path", version.getPath(), version.getCorrespondingNodePath(workspaceName)); + } + + /** + * Tests if Version.getDepth() returns the right depth + */ + public void testGetDepth() throws Exception { + assertTrue("Version.getDepth() mismatch", version.getDepth() >= 4); + } + + /** + * Tests if Version.getIndex() returns the right index + */ + public void testGetIndex() throws Exception { + assertEquals("Version.getIndex() mismatch", 1, version.getIndex()); + } + + /** + * Tests if Version.getLock() throws a {@link + * javax.jcr.lock.LockException} + */ + public void testGetLock() throws Exception { + try { + version.getLock(); + fail("Version should not be lockable: Version.getLock() did not throw a LockException"); + } catch (LockException success) { + } catch (UnsupportedRepositoryOperationException maybe) { + assertFalse(isSupported(Repository.OPTION_LOCKING_SUPPORTED)); + } + } + + /** + * Tests if Version.getLock() throws a {@link + * javax.jcr.lock.LockException} + */ + public void testGetLockJcr2() throws Exception { + ensureLockingSupported(); + try { + version.getSession().getWorkspace().getLockManager().getLock(version.getPath()); + fail("Version should not be lockable: Version.getLock() did not throw a LockException"); + } catch (LockException success) { + } + } + + /** + * Tests if Version.getMixinNodeTypes() does not return null. + */ + public void testGetMixinNodeTypes() throws Exception { + NodeType[] ntArray = version.getMixinNodeTypes(); + assertNotNull("Version.getMixinNodeTypes returns null array", ntArray); + } + + /** + * Tests if Version.getName() returns the right name + */ + public void testGetName() throws Exception { + assertTrue("Version.getName() does not return the right name", versionableNode.getVersionHistory().getVersion(version.getName()).isSame(version)); + } + + /** + * Tests if Version.getNode(String) returns the right child + * Node + */ + public void testGetNode() throws Exception { + assertTrue("Version.getNode(String) does not return a sub-node of type nt:frozenNode", version.getNode(jcrFrozenNode).isNodeType(ntFrozenNode)); + } + + /** + * Tests if Version.getNodes() and Version.getNodes(String) + * returns the right child Node + */ + public void testGetNodes() throws Exception { + assertTrue("Version.getNodes() does not return a sub-node of type nt:frozenNode", version.getNodes().nextNode().isNodeType(ntFrozenNode)); + assertTrue("Version.getNodes(String) does not return a sub-node of type nt:frozenNode", version.getNodes(superuser.getNamespacePrefix(NS_JCR_URI) + ":*").nextNode().isNodeType(ntFrozenNode)); + } + + /** + * Tests if Version.getParent() returns the right parent Node + */ + public void testGetParent() throws Exception { + assertTrue("Version.getParent() does not return a parent-node of type nt:versionHistory", version.getParent().isNodeType(ntVersionHistory)); + } + + /** + * Tests if Version.getPath() returns the right path + */ + public void testGetPath() throws Exception { + assertTrue("Version.getPath() does not return the right path", version.getPath().startsWith("/" + superuser.getNamespacePrefix(NS_JCR_URI) + ":system/" + superuser.getNamespacePrefix(NS_JCR_URI) + ":versionStorage/")); + } + + /** + * Tests if Version.getPrimaryItem() throws a {@link + * javax.jcr.ItemNotFoundException} + */ + public void testGetPrimaryItem() throws Exception { + try { + version.getPrimaryItem(); + fail("Version.getPrimaryItem() did not throw a ItemNotFoundException"); + } catch (ItemNotFoundException success) { + } + } + + /** + * Tests if Version.getPrimaryNodeType() returns the right + * primary node type nt:version + */ + public void testGetPrimaryNodeType() throws Exception { + assertEquals("Version does not have the primary node type nt:version", ntVersion, version.getPrimaryNodeType().getName()); + } + + /** + * Tests if Version.getProperties() and + * Version.getProperties(String) return the right property + */ + public void testGetProperties() throws Exception { + PropertyIterator pi = version.getProperties(); + boolean hasPropertyCreated = false; + while (pi.hasNext()) { + if (pi.nextProperty().getName().equals(jcrCreated)) { + hasPropertyCreated = true; + } + } + assertTrue("Version.getProperties() does not return property jcr:created", hasPropertyCreated); + + pi = version.getProperties(superuser.getNamespacePrefix(NS_JCR_URI) + ":*"); + hasPropertyCreated = false; + while (pi.hasNext()) { + if (pi.nextProperty().getName().equals(jcrCreated)) { + hasPropertyCreated = true; + } + } + assertTrue("Version.getProperties(String) does not return property jcr:created", hasPropertyCreated); + } + + /** + * Tests if Version.getProperty(String) returns the right + * property + */ + public void testGetProperty() throws Exception { + assertTrue("Version.getProperty(String) does not return property jcr:created", version.getProperty(jcrCreated).getName().equals(jcrCreated)); + } + + /** + * Tests if Version.getSession() returns the right session + */ + public void testGetSession() throws Exception { + assertSame("Version.getSession() did not return the right session", superuser, version.getSession()); + } + + /** + * Tests if Version.getVersionHistory() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testGetVersionHistory() throws Exception { + try { + version.getVersionHistory(); + fail("Version.getVersionHistory() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.getVersionHistory() throws an {@link + * javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testGetVersionHistoryJcr2() throws Exception { + try { + versionManager.getVersionHistory(version.getPath()); + fail("Version.getVersionHistory() did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.getUUID() returns the right UUID + */ + public void testGetUUID() throws Exception { + List successorValues = Arrays.asList(versionableNode.getVersionHistory().getRootVersion().getProperty(jcrSuccessors).getValues()); + assertTrue("Version.getUUID() did not return the right UUID", successorValues.contains(superuser.getValueFactory().createValue(version))); + } + + /** + * Tests if Version.hasNode(String) returns the right + * boolean value + */ + public void testHasNode() throws Exception { + assertTrue("Version.hasNode(String) did not return true", version.hasNode(jcrFrozenNode)); + } + + /** + * Tests if Version.hasNodes() returns true + */ + public void testHasNodes() throws Exception { + assertTrue("Version.hasNodes() did not return true", version.hasNodes()); + } + + /** + * Tests if Version.hasProperties() returns true + */ + public void testHasProperties() throws Exception { + assertTrue("Version.hasProperties() did not return true", version.hasProperties()); + } + + /** + * Tests if Version.hasProperty(String) returns the right + * boolean value + */ + public void testHasProperty() throws Exception { + assertTrue("Version.hasProperty(String) did not return true", version.hasProperty(jcrCreated)); + } + + /** + * Tests if Version.holdsLock() returns false + */ + public void testHoldsLock() throws Exception { + ensureLockingSupported(); + assertFalse("Version.holdsLock() did not return false", version.holdsLock()); + } + + /** + * Tests if Version.holdsLock() returns false + */ + public void testHoldsLockJcr2() throws Exception { + ensureLockingSupported(); + assertFalse("Version.holdsLock() did not return false", version.getSession().getWorkspace().getLockManager().holdsLock(version.getPath())); + } + + /** + * Tests if Version.isCheckedOut() returns true + */ + public void testIsCheckedOut() throws Exception { + assertTrue("Version.isCheckedOut() did not return true", version.isCheckedOut()); + } + + /** + * Tests if Version.isCheckedOut() returns true + */ + public void testIsCheckedOutJcr2() throws Exception { + assertTrue("Version.isCheckedOut() did not return true", versionManager.isCheckedOut(version.getPath())); + } + + /** + * Tests if Version.isLocked() returns false + */ + public void testIsLocked() throws Exception { + assertFalse("Version.isLocked() did not return false", version.isLocked()); + } + + /** + * Tests if Version.isLocked() returns false + */ + public void testIsLockedJcr2() throws Exception { + ensureLockingSupported(); + assertFalse("Version.isLocked() did not return false", version.getSession().getWorkspace().getLockManager().isLocked(version.getPath())); + } + + /** + * Tests if Version.isModified() returns false + */ + public void testIsModified() throws Exception { + assertFalse("Version.isModified() did not return false", version.isModified()); + } + + /** + * Tests if Version.isNew() returns false + */ + public void testIsNew() throws Exception { + assertFalse("Version.isNew() did not return false", version.isNew()); + } + + /** + * Tests if Version.isNode() returns true + */ + public void testIsNode() throws Exception { + assertTrue("Version.isNode() did not return true", version.isNode()); + } + + /** + * Tests if Version.isNodeType(String) returns the right + * boolean value + */ + public void testIsNodeType() throws Exception { + assertTrue("Version.isNodeType(String) did not return true for nt:version", version.isNodeType(ntVersion)); + } + + /** + * Tests if Version.isSame() returns the right + * boolean value + */ + public void testIsSame() throws Exception { + assertTrue("Version.isSame(Item) did not return true", version2.isSame(versionableNode.getBaseVersion())); + } + + /** + * Tests if Version.isSame() returns the right + * boolean value + */ + public void testIsSameJcr2() throws Exception { + assertTrue("Version.isSame(Item) did not return true", version2.isSame(versionManager.getBaseVersion(versionableNode.getPath()))); + } + + /** + * Tests if Version.lock(boolean, boolean) throws a {@link + * LockException} + */ + public void testLock() throws Exception { + ensureLockingSupported(); + try { + version.lock(true, true); + fail("Version should not be lockable: Version.lock(true,true) did not throw a LockException"); + } catch (LockException success) { + } + try { + version.lock(true, false); + fail("Version should not be lockable: Version.lock(true,false) did not throw a LockException"); + } catch (LockException success) { + } + try { + version.lock(false, true); + fail("Version should not be lockable: Version.lock(false,true) did not throw a LockException"); + } catch (LockException success) { + } + try { + version.lock(false, false); + fail("Version should not be lockable: Version.lock(false,false) did not throw a LockException"); + } catch (LockException success) { + } + } + + /** + * Tests if Version.lock(boolean, boolean) throws a {@link + * LockException} + */ + public void testLockJcr2() throws Exception { + ensureLockingSupported(); + LockManager lockManager = version.getSession().getWorkspace().getLockManager(); + String path = version.getPath(); + try { + lockManager.lock(path, true, true, 60, ""); + fail("Version should not be lockable: Version.lock(true,true) did not throw a LockException"); + } catch (LockException success) { + } + try { + lockManager.lock(path, true, false, 60, ""); + fail("Version should not be lockable: Version.lock(true,false) did not throw a LockException"); + } catch (LockException success) { + } + try { + lockManager.lock(path, false, true, 60, ""); + fail("Version should not be lockable: Version.lock(false,true) did not throw a LockException"); + } catch (LockException success) { + } + try { + lockManager.lock(path, false, false, 60, ""); + fail("Version should not be lockable: Version.lock(false,false) did not throw a LockException"); + } catch (LockException success) { + } + } + + /** + * Tests if Version.merge(String) throws an + * {@link javax.jcr.nodetype.ConstraintViolationException} + */ + public void testMerge() throws Exception { + try { + version.merge(workspaceName, true); + fail("Version.merge(String, true) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.merge(workspaceName, false); + fail("Version.merge(String, false) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if Version.merge(String) throws an + * {@link javax.jcr.nodetype.ConstraintViolationException} + */ +/* + TODO: check why this fails + public void testMergeJcr2() throws Exception { + try { + versionManager.merge(version.getPath(), workspaceName, true); + fail("Version.merge(String, true) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + versionManager.merge(version.getPath(),workspaceName, false); + fail("Version.merge(String, false) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } +*/ + /** + * Tests if Version.orderBefore(String, String) throws an + * {@link javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testOrderBefore() throws Exception { + try { + version.orderBefore(jcrFrozenNode, null); + fail("Version.orderBefore(String,String) did not throw an UnsupportedRepositoryOperationException or a ConstraintViolationException"); + } catch (UnsupportedRepositoryOperationException success) { + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if Version.refresh(boolean) works as expected (do + * nothing and return quietly) + */ + public void testRefresh() throws Exception { + // should do nothing and return quietly + version.refresh(true); + version.refresh(false); + } + + /** + * Tests if Version.remove() throws an {@link + * javax.jcr.nodetype.ConstraintViolationException} + */ + public void testRemove() throws Exception { + try { + version.remove(); + versionableNode.getSession().save(); + fail("Version should be read-only: Version.remove() did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if Version.removeMixin(String) throws an {@link + * javax.jcr.nodetype.ConstraintViolationException} + */ + public void testRemoveMixin() throws Exception { + try { + version.removeMixin(mixVersionable); + fail("Version should be read-only: Version.removeMixin(String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if Version.restore(String, boolean) and + * Version.restore(Version, boolean) throw an + * {@link UnsupportedRepositoryOperationException} and + * Version.restore(Version, String, boolean) throws a + * {@link ConstraintViolationException}. + */ + public void testRestore() throws Exception { + try { + version.restore("abc", true); + fail("Version.restore(String,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + try { + version.restore(version2, true); + fail("Version.restore(Version,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + try { + version.restore(version2, "abc", true); + fail("Version.restore(Version,String,boolean) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if Version.restore(String, boolean) and + * Version.restore(Version, boolean) throw an + * {@link UnsupportedRepositoryOperationException} and + * Version.restore(Version, String, boolean) throws a + * {@link ConstraintViolationException}. + */ + public void testRestoreJcr2() throws Exception { + try { + versionManager.restore(version.getPath(), "abc", true); + fail("Version.restore(String,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.restoreByLabel(String, boolean) throws an + * {@link javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testRestoreByLabel() throws Exception { + try { + version.restoreByLabel("abc", true); + fail("Version.restoreByLabel(String,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if Version.restoreByLabel(String, boolean) throws an + * {@link javax.jcr.UnsupportedRepositoryOperationException} + */ + public void testRestoreByLabelJcr2() throws Exception { + try { + versionManager.restoreByLabel(version.getPath(), "abc", true); + fail("Version.restoreByLabel(String,boolean) did not throw an UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException success) { + } + } + + /** + * Tests if + *
        • Version.setProperty(String, String[])
        • + *
        • Version.setProperty(String, String[], int)
        • + *
        • Version.setProperty(String, Value[])
        • + *
        • Version.setProperty(String, Value[], int)
        • + *
        • Version.setProperty(String, boolean)
        • + *
        • Version.setProperty(String, double)
        • + *
        • Version.setProperty(String, InputStream)
        • + *
        • Version.setProperty(String, String)
        • + *
        • Version.setProperty(String, Calendar)
        • + *
        • Version.setProperty(String, Node)
        • + *
        • Version.setProperty(String, Value)
        • + *
        • Version.setProperty(String, long)
        • + *
        all throw a + * {@link javax.jcr.nodetype.ConstraintViolationException} + */ + public void testSetProperty() throws Exception { + + // create Value[] object + Value[] vArray = new Value[3]; + vArray[0] = superuser.getValueFactory().createValue("abc"); + vArray[1] = superuser.getValueFactory().createValue("xyz"); + vArray[2] = superuser.getValueFactory().createValue("123"); + + // create String array + String[] s = {"abc", "xyz", "123"}; + + try { + version.setProperty(propertyName1, s); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,String[]) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.setProperty(propertyName1, s, PropertyType.STRING); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,String[],int) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.setProperty(propertyName1, vArray); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,Value[]) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.setProperty(propertyName1, vArray, PropertyType.STRING); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,Value[],int]) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.setProperty(propertyName1, true); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,boolean) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.setProperty(propertyName1, 123); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,double) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + byte[] bytes = {73, 26, 32, -36, 40, -43, -124}; + InputStream inpStream = new ByteArrayInputStream(bytes); + version.setProperty(propertyName1, inpStream); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,InputStream) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.setProperty(propertyName1, "abc"); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,String) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + Calendar c = new GregorianCalendar(1945, 1, 6, 16, 20, 0); + version.setProperty(propertyName1, c); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,Calendar) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.setProperty(propertyName1, version); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,Node) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + Value v = superuser.getValueFactory().createValue("abc"); + version.setProperty(propertyName1, v); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,Value) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + try { + version.setProperty(propertyName1, -2147483650L); + version.getSession().save(); + fail("Version should be read-only: Version.setProperty(String,long) did not throw a ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if Version.unlock() throws a {@link + * javax.jcr.lock.LockException} + */ + public void testUnlock() throws Exception { + try { + version.unlock(); + fail("Version should not be lockable: Version.unlock() did not throw a LockException"); + } catch (LockException success) { + } catch (UnsupportedRepositoryOperationException maybe) { + assertFalse(isSupported(Repository.OPTION_LOCKING_SUPPORTED)); + } + } + + /** + * Tests if Version.unlock() throws a {@link + * javax.jcr.lock.LockException} + */ + public void testUnlockJcr2() throws Exception { + ensureLockingSupported(); + try { + version.getSession().getWorkspace().getLockManager().unlock(version.getPath()); + fail("Version should not be lockable: Version.unlock() did not throw a LockException"); + } catch (LockException success) { + } + } + + /** + * Tests if VersionHistory.update(String) throws an + * {@link javax.jcr.nodetype.ConstraintViolationException} + */ + public void testUpdate() throws Exception { + try { + version.update(workspaceName); + fail("VersionHistory.update(String) did not throw an ConstraintViolationException"); + } catch (ConstraintViolationException success) { + } + } + + /** + * Tests if the jcr:frozenUuid property has the correct type + * @throws Exception + */ + public void testFrozenUUID() throws Exception { + Property p = version.getNode(jcrFrozenNode).getProperty(jcrFrozenUuid); + assertEquals("jcr:fronzenUuid should be of type string", PropertyType.TYPENAME_STRING, PropertyType.nameFromValue(p.getType())); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/WorkspaceMoveVersionExceptionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/WorkspaceMoveVersionExceptionTest.java new file mode 100644 index 00000000000..c428db91c25 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/WorkspaceMoveVersionExceptionTest.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.test.api.version; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.version.VersionException; + +/** + * WorkspaceMoveVersionExceptionTest contains tests dealing with + * moving nodes using {@link javax.jcr.Workspace#move(String, String)}. + * + */ +public class WorkspaceMoveVersionExceptionTest extends AbstractVersionTest { + + /** + * Tries to move a node using {@link javax.jcr.Workspace#move(String, String)} + * where the source parent is checked in. This should throw an + * {@link javax.jcr.version.VersionException}. + */ + public void testWorkspaceMoveSourceCheckedInVersionException() throws RepositoryException { + // add a node under a versionable node + Node movingNode = versionableNode.addNode(nodeName1, nonVersionableNodeType.getName()); + versionableNode.save(); + // check the parent node in + versionableNode.checkin(); + try { + // try to move the sub node this should throw an VersionException + superuser.getWorkspace().move(movingNode.getPath(), nonVersionableNode.getPath() + "/" + nodeName1); + fail("Moving a node using Workspace.move() where parent node is " + + "versionable and checked in should throw a VersionException!"); + } catch (VersionException e) { + // ok, works as expected + } + } + + /** + * Tries to move a node using {@link javax.jcr.Workspace#move(String, String)} + * where the destination parent is checked in. This should throw an + * {@link javax.jcr.version.VersionException}. + */ + public void testWorkspaceMoveDestCheckedInVersionException() throws RepositoryException { + // make sure versionable node is checked in + versionableNode.checkin(); + + try { + // try to move the sub node this should throw an VersionException + superuser.getWorkspace().move(nonVersionableNode.getPath(), versionableNode.getPath() + "/" + nodeName1); + fail("Moving a node using Workspace.move() where destination parent " + + "node is versionable and checked in should throw a VersionException!"); + } catch (VersionException e) { + // ok, works as expected + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/WorkspaceRestoreTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/WorkspaceRestoreTest.java new file mode 100644 index 00000000000..766cf23ba2d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/WorkspaceRestoreTest.java @@ -0,0 +1,405 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version; + +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.VersionManager; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; + +/** + * WorkspaceRestoreTest provides test methods for the {@link javax.jcr.Workspace#restore(javax.jcr.version.Version[], boolean)} + * method. + * + */ +public class WorkspaceRestoreTest extends AbstractVersionTest { + + Session wSuperuser; + + Version version; + Version version2; + Version rootVersion; + + Node versionableNode2; + Node wTestRoot; + Node wVersionableNode; + Node wVersionableNode2; + Node wVersionableChildNode; + + Version wChildVersion; + + protected void setUp() throws Exception { + super.setUp(); + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + version = versionManager.checkin(path); + versionManager.checkout(path); + version2 = versionManager.checkin(path); + versionManager.checkout(path); + rootVersion = versionManager.getVersionHistory(path).getRootVersion(); + + // build a second versionable node below the testroot + try { + versionableNode2 = createVersionableNode(testRootNode, nodeName2, versionableNodeType); + } catch (RepositoryException e) { + fail("Failed to create a second versionable node: " + e.getMessage()); + } + try { + wSuperuser = getHelper().getSuperuserSession(workspaceName); + } catch (RepositoryException e) { + fail("Failed to retrieve superuser session for second workspace '" + workspaceName + "': " + e.getMessage()); + } + + // test if the required nodes exist in the second workspace if not try to clone them + try { + testRootNode.getCorrespondingNodePath(workspaceName); + } catch (ItemNotFoundException e) { + // clone testRoot + wSuperuser.getWorkspace().clone(superuser.getWorkspace().getName(), testRoot, testRoot, true); + } + + try { + versionableNode.getCorrespondingNodePath(workspaceName); + } catch (ItemNotFoundException e) { + // clone versionable node + wSuperuser.getWorkspace().clone(superuser.getWorkspace().getName(), versionableNode.getPath(), versionableNode.getPath(), true); + } + + try { + versionableNode2.getCorrespondingNodePath(workspaceName); + } catch (ItemNotFoundException e) { + // clone second versionable node + wSuperuser.getWorkspace().clone(superuser.getWorkspace().getName(), versionableNode2.getPath(), versionableNode2.getPath(), true); + } + + try { + // set node-fields (wTestRoot, wVersionableNode, wVersionableNode2) + // and check versionable nodes out. + wTestRoot = (Node) wSuperuser.getItem(testRootNode.getPath()); + + wVersionableNode = wSuperuser.getNodeByIdentifier(versionableNode.getIdentifier()); + wVersionableNode.getSession().getWorkspace().getVersionManager().checkout(wVersionableNode.getPath()); + + wVersionableNode2 = wSuperuser.getNodeByIdentifier(versionableNode2.getIdentifier()); + wVersionableNode2.getSession().getWorkspace().getVersionManager().checkout(wVersionableNode2.getPath()); + + } catch (RepositoryException e) { + fail("Failed to setup test environment in workspace: " + e.toString()); + } + + // create persistent versionable CHILD-node below wVersionableNode in workspace 2 + // that is not present in the default workspace. + try { + wVersionableChildNode = createVersionableNode(wVersionableNode, nodeName4, versionableNodeType); + } catch (RepositoryException e) { + fail("Failed to create versionable child node in second workspace: " + e.getMessage()); + } + + // create a version of the versionable child node + VersionManager wVersionManager = wVersionableChildNode.getSession().getWorkspace().getVersionManager(); + String wPath = wVersionableChildNode.getPath(); + wVersionManager.checkout(wPath); + wChildVersion = wVersionManager.checkin(wPath); + wVersionManager.checkout(wPath); + } + + + protected void tearDown() throws Exception { + try { + // remove all versionable nodes below the test + versionableNode2.remove(); + wVersionableNode.remove(); + wVersionableNode2.remove(); + wTestRoot.save(); + } finally { + if (wSuperuser != null) { + wSuperuser.logout(); + wSuperuser = null; + } + version = null; + version2 = null; + rootVersion = null; + versionableNode2 = null; + wTestRoot = null; + wVersionableNode = null; + wVersionableNode2 = null; + wVersionableChildNode = null; + wChildVersion = null; + super.tearDown(); + } + } + + /** + * Test if InvalidItemStateException is thrown if the session affected by + * Workspace.restore(Version[], boolean) has pending changes. + */ + @SuppressWarnings("deprecation") + public void testWorkspaceRestoreWithPendingChanges() throws RepositoryException { + versionableNode.checkout(); + try { + // modify node without calling save() + versionableNode.setProperty(propertyName1, propertyValue); + + // create version in second workspace + Version v = wVersionableNode.checkin(); + // try to restore that version + superuser.getWorkspace().restore(new Version[]{v}, false); + + fail("InvalidItemStateException must be thrown on attempt to call Workspace.restore(Version[], boolean) in a session having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // success + } + } + + /** + * Test if InvalidItemStateException is thrown if the session affected by + * VersionManager.restore(Version[], boolean) has pending changes. + */ + public void testWorkspaceRestoreWithPendingChangesJcr2() throws RepositoryException { + versionableNode.getSession().getWorkspace().getVersionManager().checkout(versionableNode.getPath()); + try { + // modify node without calling save() + versionableNode.setProperty(propertyName1, propertyValue); + + // create version in second workspace + Version v = wVersionableNode.getSession().getWorkspace().getVersionManager().checkin(wVersionableNode.getPath()); + // try to restore that version + superuser.getWorkspace().getVersionManager().restore(new Version[]{v}, false); + + fail("InvalidItemStateException must be thrown on attempt to call Workspace.restore(Version[], boolean) in a session having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // success + } + } + + /** + * Test if VersionException is thrown if the specified version array does + * not contain a version that has a corresponding node in this workspace. + */ + @SuppressWarnings("deprecation") + public void testWorkspaceRestoreHasCorrespondingNode() throws RepositoryException { + try { + superuser.getWorkspace().restore(new Version[]{wChildVersion}, false); + fail("Workspace.restore(Version[], boolean) must throw VersionException if non of the specified versions has a corresponding node in the workspace."); + } catch (VersionException e) { + // success + } + } + + /** + * Test if VersionException is thrown if the specified version array does + * not contain a version that has a corresponding node in this workspace. + */ + public void testWorkspaceRestoreHasCorrespondingNodeJcr2() throws RepositoryException { + try { + superuser.getWorkspace().getVersionManager().restore(new Version[]{wChildVersion}, false); + fail("Workspace.restore(Version[], boolean) must throw VersionException if non of the specified versions has a corresponding node in the workspace."); + } catch (VersionException e) { + // success + } + } + + /** + * Test if Workspace.restore(Version[], boolean) succeeds if the following two + * preconditions are fulfilled:
          + *
        • For every version V in S that corresponds to a missing node in the workspace, + * there must also be a parent of V in S.
        • + *
        • S must contain at least one version that corresponds to an existing + * node in the workspace.
        • + *
        + */ + @SuppressWarnings("deprecation") + public void testWorkspaceRestoreWithParent() throws RepositoryException { + + try { + Version parentV = wVersionableNode.checkin(); + superuser.getWorkspace().restore(new Version[]{parentV, wChildVersion}, false); + } catch (RepositoryException e) { + fail("Workspace.restore(Version[], boolean) with a version that has no corresponding node must succeed if a version of a parent with correspondance is present in the version array."); + } + } + + /** + * Test if VersionManager.restore(Version[], boolean) succeeds if the following two + * preconditions are fulfilled:
          + *
        • For every version V in S that corresponds to a missing node in the workspace, + * there must also be a parent of V in S.
        • + *
        • S must contain at least one version that corresponds to an existing + * node in the workspace.
        • + *
        + */ + public void testWorkspaceRestoreWithParentJcr2() throws RepositoryException { + + try { + Version parentV = wVersionableNode.getSession().getWorkspace().getVersionManager().checkin(wVersionableNode.getPath()); + superuser.getWorkspace().getVersionManager().restore(new Version[]{parentV, wChildVersion}, false); + } catch (RepositoryException e) { + fail("Workspace.restore(Version[], boolean) with a version that has no corresponding node must succeed if a version of a parent with correspondance is present in the version array."); + } + } + + /** + * Test if the removeExisting-flag removes an existing node in case of uuid conflict. + */ + @SuppressWarnings("deprecation") + public void testWorkspaceRestoreWithRemoveExisting() throws NotExecutableException, RepositoryException { + // create version for parentNode of childNode + superuser.getWorkspace().clone(workspaceName, wVersionableChildNode.getPath(), wVersionableChildNode.getPath(), false); + Version parentV = versionableNode.checkin(); + + // move child node in order to produce the uuid conflict + String newChildPath = wVersionableNode2.getPath() + "/" + wVersionableChildNode.getName(); + wSuperuser.move(wVersionableChildNode.getPath(), newChildPath); + wSuperuser.save(); + + // restore the parent with removeExisting == true >> moved child node + // must be removed. + wSuperuser.getWorkspace().restore(new Version[]{parentV}, true); + if (wSuperuser.itemExists(newChildPath)) { + fail("Workspace.restore(Version[], boolean) with the boolean flag set to true, must remove the existing node in case of Uuid conflict."); + } + } + + /** + * Test if the removeExisting-flag removes an existing node in case of uuid conflict. + */ + public void testWorkspaceRestoreWithRemoveExistingJcr2() throws NotExecutableException, RepositoryException { + // create version for parentNode of childNode + superuser.getWorkspace().clone(workspaceName, wVersionableChildNode.getPath(), wVersionableChildNode.getPath(), false); + Version parentV = versionableNode.getSession().getWorkspace().getVersionManager().checkin(versionableNode.getPath()); + + // move child node in order to produce the uuid conflict + String newChildPath = wVersionableNode2.getPath() + "/" + wVersionableChildNode.getName(); + wSuperuser.move(wVersionableChildNode.getPath(), newChildPath); + wSuperuser.save(); + + // restore the parent with removeExisting == true >> moved child node + // must be removed. + wSuperuser.getWorkspace().getVersionManager().restore(new Version[]{parentV}, true); + if (wSuperuser.itemExists(newChildPath)) { + fail("Workspace.restore(Version[], boolean) with the boolean flag set to true, must remove the existing node in case of Uuid conflict."); + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + @SuppressWarnings("deprecation") + public void testWorkspaceRestoreWithUUIDConflict() throws RepositoryException, NotExecutableException { + try { + // Verify that nodes used for the test are indeed versionable + NodeDefinition nd = wVersionableNode.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Nodes must be versionable in order to run this test."); + } + + Version v = wVersionableNode.checkin(); + wVersionableNode.checkout(); + wSuperuser.move(wVersionableChildNode.getPath(), wVersionableNode2.getPath() + "/" + wVersionableChildNode.getName()); + wSuperuser.save(); + wSuperuser.getWorkspace().restore(new Version[]{v}, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testWorkspaceRestoreWithUUIDConflictJcr2() throws RepositoryException, NotExecutableException { + try { + // Verify that nodes used for the test are indeed versionable + NodeDefinition nd = wVersionableNode.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Nodes must be versionable in order to run this test."); + } + + VersionManager versionManager = wVersionableNode.getSession().getWorkspace().getVersionManager(); + String path = wVersionableNode.getPath(); + Version v = versionManager.checkin(path); + versionManager.checkout(path); + wSuperuser.move(wVersionableChildNode.getPath(), wVersionableNode2.getPath() + "/" + wVersionableChildNode.getName()); + wSuperuser.save(); + wSuperuser.getWorkspace().getVersionManager().restore(new Version[]{v}, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + + /** + * Test if workspace-restoring a node works on checked-in node. + */ + @SuppressWarnings("deprecation") + public void testWorkspaceRestoreOnCheckedInNode() throws RepositoryException { + if (versionableNode.isCheckedOut()) { + versionableNode.checkin(); + } + superuser.getWorkspace().restore(new Version[]{version}, true); + } + + /** + * Test if workspace-restoring a node works on checked-in node. + */ + public void testWorkspaceRestoreOnCheckedInNodeJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + if (versionManager.isCheckedOut(path)) { + versionManager.checkin(path); + } + superuser.getWorkspace().getVersionManager().restore(new Version[]{version}, true); + } + + /** + * Test if workspace-restoring a node works on checked-out node. + */ + @SuppressWarnings("deprecation") + public void testWorkspaceRestoreOnCheckedOutNode() throws RepositoryException { + if (!versionableNode.isCheckedOut()) { + versionableNode.checkout(); + } + superuser.getWorkspace().restore(new Version[]{version}, true); + } + + /** + * Test if workspace-restoring a node works on checked-out node. + */ + public void testWorkspaceRestoreOnCheckedOutNodeJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + if (!versionManager.isCheckedOut(path)) { + versionManager.checkout(path); + } + superuser.getWorkspace().getVersionManager().restore(new Version[]{version}, true); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/AbstractVersionTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/AbstractVersionTest.java new file mode 100644 index 00000000000..d142aad765f --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/AbstractVersionTest.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.test.api.version.simple; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * AbstractVersionTest is the abstract base class for all + * simple versioning related test classes. + */ +public class AbstractVersionTest extends AbstractJCRTest { + + protected NodeType versionableNodeType; + protected NodeType nonVersionableNodeType; + + protected Node versionableNode; + protected Node nonVersionableNode; + + protected String propertyValue; + + protected void setUp() throws Exception { + super.setUp(); + + super.checkSupportedOption(Repository.OPTION_SIMPLE_VERSIONING_SUPPORTED); + + NodeTypeManager ntMgr = superuser.getWorkspace().getNodeTypeManager(); + + // assert that this repository support versioning + try { + NodeType versionableNt = ntMgr.getNodeType(mixSimpleVersionable); + if (versionableNt == null) { + fail("Repository does not support Versioning: mixin nodetype 'mix:simpleVersionable' is missing."); + } + } catch (NoSuchNodeTypeException e) { + fail("Repository does not support Versioning: mixin nodetype 'mix:simpleVersionable' is missing."); + } + + // retrieve versionable nodetype + String versionableNodeTypeName = getProperty("versionableNodeType"); + try { + versionableNodeType = ntMgr.getNodeType(versionableNodeTypeName); + if (versionableNodeType == null) { + fail("Property 'versionableNodeType' does not define a valid nodetype: '"+versionableNodeTypeName+"'"); + } + } catch (NoSuchNodeTypeException e) { + fail("Property 'simpleVersionableNodeType' does not define an existing nodetype: '"+versionableNodeTypeName+"'"); + } + + // make sure 'non-versionable' nodetype is properly defined + try { + nonVersionableNodeType = ntMgr.getNodeType(testNodeType); + if (nonVersionableNodeType == null || nonVersionableNodeType.isNodeType(mixVersionable)) { + fail("Property 'testNodeType' does define a versionable nodetype: '"+testNodeType+"'"); + } + } catch (NoSuchNodeTypeException e) { + fail("Property 'testNodeType' does not define an existing nodetype: '"+testNodeType+"'"); + } + + // build persistent versionable and non-versionable nodes + try { + versionableNode = createVersionableNode(testRootNode, nodeName1, versionableNodeType); + } catch (RepositoryException e) { + fail("Failed to create versionable test node." + e.getMessage()); + } + try { + nonVersionableNode = testRootNode.addNode(nodeName3, nonVersionableNodeType.getName()); + testRootNode.getSession().save(); + } catch (RepositoryException e) { + fail("Failed to create non-versionable test node." + e.getMessage()); + } + + propertyValue = getProperty("propertyValue"); + if (propertyValue == null) { + fail("Property 'propertyValue' is not defined."); + } + } + + protected void tearDown() throws Exception { + // remove versionable nodes + try { + versionableNode.remove(); + testRootNode.getSession().save(); + } catch (Exception e) { + log.println("Exception in tearDown: " + e.toString()); + } finally { + versionableNodeType = null; + nonVersionableNodeType = null; + versionableNode = null; + nonVersionableNode = null; + super.tearDown(); + } + } + + /** + * Retrieve the number of versions present in the given version history. + * + * @param vHistory + * @return number of versions + * @throws RepositoryException + */ + protected long getNumberOfVersions(VersionHistory vHistory) throws RepositoryException { + return getSize(vHistory.getAllVersions()); + } + + /** + * Create a versionable node below the given parent node. If the specified + * nodetype name is not mix:versionable an attempt is made to add the + * mix:versionable mixin type to the created child node. + * + * @param parent + * @param name + * @param nodetype + * @return versionable node. + * @throws RepositoryException + */ + protected Node createVersionableNode(Node parent, String name, NodeType nodetype) + throws RepositoryException, NotExecutableException { + Node versionableNode = parent.addNode(name, nodetype.getName()); + ensureMixinType(versionableNode, mixSimpleVersionable); + parent.getSession().save(); + + return versionableNode; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/BasicTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/BasicTest.java new file mode 100644 index 00000000000..01caaddba7b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/BasicTest.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.test.api.version.simple; + +import javax.jcr.RepositoryException; + +/** + * BasicTest checks if simple versioning is correctly set up + * + */ +public class BasicTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + } + + /** + * Test node is simple versionable + * + * @throws RepositoryException + */ + public void testNodeTypes() throws RepositoryException { + assertTrue("Node.isNodeType(mix:simpleVersionable) must return true.", + versionableNode.isNodeType(mixSimpleVersionable)); + assertFalse("Node.isNodeType(mix:versionable) must return false.", + versionableNode.isNodeType(mixVersionable)); + } + + /** + * Test if node has a jcr:isCheckedOut property + * + * @throws RepositoryException + */ + public void testICOProperty() throws RepositoryException { + assertTrue("Versionable node must have a jcr:isCheckedOut property.", + versionableNode.hasProperty(jcrIsCheckedOut)); + } + + +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CheckinTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CheckinTest.java new file mode 100644 index 00000000000..79304caee4d --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CheckinTest.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version.simple; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +/** + * CheckinTest covers tests related to {@link javax.jcr.Node#checkin()} + * on simple versionable nodes. + * + */ +public class CheckinTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + + try { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + } catch (RepositoryException e) { + cleanUp(); + throw e; + } + } + + /** + * Test if Node.isCheckedOut() return false after calling Node.checkin() + * + * @throws javax.jcr.RepositoryException + */ + @SuppressWarnings("deprecation") + public void testIsCheckedOut() throws RepositoryException { + versionableNode.checkin(); + assertTrue("After calling Node.checkin() on a versionable node N, N.isCheckedOut() must return false", versionableNode.isCheckedOut() == false); + } + + /** + * Test if VersionManager.isCheckedOut(P) returns false if P is the + * absolute path of a checked-in versionable node. + * + * @throws javax.jcr.RepositoryException + */ + public void testIsCheckedOutJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkin(path); + assertTrue("VersionManager.isCheckedOut(P) must return false if the path P resolves to a checked-in node.", versionManager.isCheckedOut(path) == false); + } + + /** + * Test if Node.checkin() on a checked-in node has no effect. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testMultipleCheckinHasNoEffect() throws RepositoryException { + + Version v = versionableNode.checkin(); + try { + Version v2 = versionableNode.checkin(); + + assertTrue("Calling checkin() on a node that is already checked-in must not have an effect.", v.isSame(v2)); + } catch (RepositoryException e) { + fail("Calling checkin() on a node that is already checked-in must not throw an exception."); + } + } + + /** + * Test if VersionManager.checkin(P) has no effect if the path P resolves + * to a checked-in node. + * + * @throws RepositoryException + */ + public void testMultipleCheckinHasNoEffectJcr2() throws RepositoryException { + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + try { + Version v2 = versionManager.checkin(path); + + assertTrue("Calling VersionManager.checkin(P) must not have an if the path P resolves to a node that is already checked-in.", v.isSame(v2)); + } catch (RepositoryException e) { + fail("Calling VersionManager.checkin(P) must not throw an exception if the path P resolves to a node that is already checked-in."); + } + } + + /** + * Test if Node.checkin() throws InvalidItemStateException if the node + * has unsaved changes pending. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testCheckinWithPendingChanges() throws RepositoryException { + try { + // modify node without calling save() + versionableNode.setProperty(propertyName1, propertyValue); + versionableNode.checkin(); + + fail("InvalidItemStateException must be thrown on attempt to checkin a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if VersionManager.checkin(P) throws InvalidItemStateException if + * the path P resolves to a node that has unsaved changes pending. + * + * @throws RepositoryException + */ + public void testCheckinWithPendingChangesJcr2() throws RepositoryException { + try { + // modify node without calling save() + versionableNode.setProperty(propertyName1, propertyValue); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkin(path); + + fail("InvalidItemStateException must be thrown on attempt to checkin a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if Node.isCheckedOut() returns false after Node.checkin(). + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testIsNotCheckedOut() throws RepositoryException { + versionableNode.checkin(); + boolean isCheckedOut = versionableNode.isCheckedOut(); + + assertFalse("Node.isCheckedOut() must return false after Node.checkin().", isCheckedOut); + } + + /** + * Test if VersionManager.isCheckedOut(P) returns false after calling VersionManager.checkin(P). + * + * @throws RepositoryException + */ + public void testIsNotCheckedOutJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkin(path); + boolean isCheckedOut = versionManager.isCheckedOut(path); + + assertFalse("VersionManager.isCheckedOut(P) must return false after VersionManager.checkin(P).", isCheckedOut); + } + + /** + * Test if Node.checkin() adds another version to the VersionHistory + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testCheckinCreatesNewVersion() throws RepositoryException { + + long initialNumberOfVersions = getNumberOfVersions(versionableNode.getVersionHistory()); + versionableNode.checkin(); + long numberOfVersions = getNumberOfVersions(versionableNode.getVersionHistory()); + + assertTrue("Checkin must create a new Version in the VersionHistory.", numberOfVersions == initialNumberOfVersions + 1); + } + + /** + * Test if VersionManager.checkin(String) adds another version to the VersionHistory + * + * @throws RepositoryException + */ + public void testCheckinCreatesNewVersionJcr2() throws RepositoryException { + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + long initialNumberOfVersions = getNumberOfVersions(versionManager.getVersionHistory(path)); + versionManager.checkin(path); + long numberOfVersions = getNumberOfVersions(versionManager.getVersionHistory(path)); + + assertTrue("Checkin must create a new Version in the VersionHistory.", numberOfVersions == initialNumberOfVersions + 1); + } + + /** + * Test calling Node.checkin() on a non-versionable node. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testCheckinNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.checkin(); + fail("Node.checkin() on a non-versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling VersionManager.checkin(P) with the path P resolving to + * a non-versionable node. + * + * @throws RepositoryException + */ + public void testCheckinNonVersionableNodeJcr2() throws RepositoryException { + try { + VersionManager versionManager = nonVersionableNode.getSession().getWorkspace().getVersionManager(); + String path = nonVersionableNode.getPath(); + versionManager.checkin(path); + fail("VersionManager.checkin(P) must throw UnsupportedRepositoryOperationException if the path P resolves to a non-versionable node."); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CheckoutTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CheckoutTest.java new file mode 100644 index 00000000000..d6520ac3d38 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CheckoutTest.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version.simple; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionManager; + +/** + * SVCheckoutTest covers tests related to {@link + * Node#checkout()} and {@link Node#isCheckedOut()} of simple versionable + * nodes. + * + */ +public class CheckoutTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + if (!versionManager.isCheckedOut(path)) { + fail("A versionable node must be checked-out after persistent creation."); + } + if (!versionableNode.isCheckedOut()) { + fail("A versionable node must be checked-out after persistent creation."); + } + try { + versionManager.checkin(path); + } catch (RepositoryException e) { + cleanUp(); + throw e; + } + } + + /** + * Test if Node.isCheckedOut() returns true, if the versionable node has + * been checked out before. + */ + public void testIsCheckedOut() throws RepositoryException { + versionableNode.checkout(); + assertTrue("After calling Node.checkout() a versionable node N, N.isCheckedOut() must return true.", versionableNode.isCheckedOut()); + } + + /** + * Test if VersionManager.isCheckedOut(P) returns true if P is the + * absolute path of a versionable node that has been checked out before. + */ + public void testIsCheckedOutJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + assertTrue("After successfully calling VersionManager.checkout(P) with P denoting the absolute path of a versionable node, VersionManager.isCheckedOut(P) must return true.", versionManager.isCheckedOut(path)); + } + + /** + * Test calling Node.isCheckedOut() on a non-versionable. + */ + public void testIsCheckedOutNonVersionableNode() throws RepositoryException { + boolean isCheckedOut = nonVersionableNode.isCheckedOut(); + Node vParent = null; + try { + vParent = nonVersionableNode.getParent(); + while (!vParent.isNodeType(mixVersionable)) { + vParent = vParent.getParent(); + } + } catch (ItemNotFoundException e) { + // root reached. + } + + if (vParent != null && vParent.isNodeType(mixVersionable)) { + if (vParent.isCheckedOut()) { + assertTrue("Node.isCheckedOut() must return true if the node is non-versionable and its nearest versionable ancestor is checked-out.", isCheckedOut); + } else { + assertFalse("Node.isCheckedOut() must return false if the node is non-versionable and its nearest versionable ancestor is checked-in.", isCheckedOut); + } + } else { + assertTrue("Node.isCheckedOut() must return true if the node is non-versionable and has no versionable ancestor", isCheckedOut); + } + } + + /** + * Test calling VersionManager.isCheckedOut(P) with P denoting the + * absolute path of a non-versionable node. + */ + public void testIsCheckedOutNonVersionableNodeJcr2() throws RepositoryException { + VersionManager versionManager = nonVersionableNode.getSession().getWorkspace().getVersionManager(); + String path = nonVersionableNode.getPath(); + boolean isCheckedOut = versionManager.isCheckedOut(path); + Node vParent = null; + try { + vParent = nonVersionableNode.getParent(); + while (!vParent.isNodeType(mixVersionable)) { + vParent = vParent.getParent(); + } + } catch (ItemNotFoundException e) { + // root reached. + } + + if (vParent != null && vParent.isNodeType(mixVersionable)) { + String parentPath = vParent.getPath(); + if (versionManager.isCheckedOut(parentPath)) { + assertTrue("VersionManager.isCheckedOut(P) must return true if P denotes the absolute path of a non-versionable node whose nearest versionable ancestor is checked-out.", isCheckedOut); + } else { + assertFalse("VersionManager.isCheckedOut(P) must return false if P denotes the absolute path of a non-versionable node whose nearest versionable ancestor is checked-in.", isCheckedOut); + } + } else { + assertTrue("VersionManager.isCheckedOut(P) must return true if P denotes the absolute path of a non-versionable node that has no versionable ancestor", isCheckedOut); + } + } + + /** + * Test calling Node.checkout() on a non-versionable node. + */ + public void testCheckoutNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.checkout(); + fail("Node.checkout() on a non-versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling VersionManager.checkout(P) with P denoting the absolute + * path of a non-versionable node. + */ + public void testCheckoutNonVersionableNodeJcr2() throws RepositoryException { + VersionManager versionManager = nonVersionableNode.getSession().getWorkspace().getVersionManager(); + String path = nonVersionableNode.getPath(); + try { + versionManager.checkout(path); + fail("VersionManager.checkout(P) with P denoting the absolute path of a non-versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test if Node.checkout() doesn't throw any exception if the versionable + * node has been checked out before. + */ + public void testCheckoutTwiceDoesNotThrow() throws RepositoryException { + versionableNode.checkout(); + versionableNode.checkout(); + } + + /** + * Test if VersionManager.checkout(P) doesn't throw any exception if P + * denotes the absolute path of a versionable node that has been checked + * out before. + */ + public void testCheckoutTwiceDoesNotThrowJcr2() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + versionManager.checkout(path); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CopyTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CopyTest.java new file mode 100644 index 00000000000..b3ffd69c4fa --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/CopyTest.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.test.api.version.simple; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionManager; + +/** + * CopyTest checks if simple versionable nodes are copied correctly: + * + * 15.1.4 Copying Versionable Nodes and Version Lineage + * Under both simple and full versioning, when an existing versionable node N is + * copied to a new location either in the same workspace or another, and the + * repository preserves the versionable mixin (see 10.7.4 Dropping Mixins on + * Copy): + * - A copy of N, call it M, is created, as usual. + * - A new, empty, version history for M, call it HM, is also created. + * + */ +public class CopyTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + } + + protected void tearDown() throws Exception { + // remove copied node + try { + String dstPath = getProperty("destination"); + superuser.getNode(dstPath).remove(); + testRootNode.getSession().save(); + } catch (Exception e) { + log.println("Exception in tearDown: " + e.toString()); + } finally { + super.tearDown(); + } + } + + public void testCopy() throws RepositoryException { + Workspace wsp = superuser.getWorkspace(); + VersionManager vMgr = wsp.getVersionManager(); + String srcPath = versionableNode.getPath(); + String dstPath = getProperty("destination"); + wsp.copy(srcPath, dstPath); + + // check versionable + Node v = superuser.getNode(dstPath); + assertTrue("Copied Node.isNodeType(mix:simpleVersionable) must return true.", + v.isNodeType(mixSimpleVersionable)); + assertFalse("Copied Node.isNodeType(mix:versionable) must return false.", + v.isNodeType(mixVersionable)); + + // check different version history + VersionHistory vh1 = vMgr.getVersionHistory(srcPath); + VersionHistory vh2 = vMgr.getVersionHistory(dstPath); + assertFalse("Copied node needs a new version history.", vh1.isSame(vh2)); + + // check if 1 version + assertEquals("Copied node must have 1 version.", 1, getNumberOfVersions(vh2)); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/FrozenNodeTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/FrozenNodeTest.java new file mode 100644 index 00000000000..5885dffdf5b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/FrozenNodeTest.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version.simple; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * SVFrozenNodeTest covers tests related to frozen nodes in + * simple versioning + * + */ +public class FrozenNodeTest extends AbstractVersionTest { + + protected void setUp() throws Exception { + super.setUp(); + + try { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + versionManager.checkout(path); + } catch (RepositoryException e) { + cleanUp(); + throw e; + } + } + + /** + * @throws RepositoryException + */ + public void testFrozenNodeUUUID() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode(); + String puuid = n.getProperty(jcrUUID).getValue().getString(); + String nuuid = n.getIdentifier(); + assertEquals("jcr:uuid needs to be equal to the getIdentifier() return value.", nuuid, puuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenChildNodeUUUID() throws RepositoryException { + versionableNode.addNode("child"); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode().getNode("child"); + String puuid = n.getProperty(jcrUUID).getValue().getString(); + String nuuid = n.getIdentifier(); + assertEquals("jcr:uuid needs to be equal to the getIdentifier() return value.", nuuid, puuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenUUUID() throws RepositoryException, + NotExecutableException { + // make versionable node referenceable + ensureMixinType(versionableNode, mixReferenceable); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode(); + String fuuid = n.getProperty(jcrFrozenUuid).getValue().getString(); + String ruuid = versionableNode.getIdentifier(); + assertEquals("jcr:frozenUuid needs to be equal to the getIdentifier() return value.", ruuid, fuuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenChildUUUID() throws RepositoryException, + NotExecutableException { + Node n1 = versionableNode.addNode("child"); + ensureMixinType(n1, mixReferenceable); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode().getNode("child"); + String fuuid = n.getProperty(jcrFrozenUuid).getValue().getString(); + String ruuid = n1.getIdentifier(); + assertEquals("jcr:frozenUuid needs to be equal to the getIdentifier() return value.", ruuid, fuuid); + } + + + /** + * @throws RepositoryException + */ + public void testFrozenNodeNodeType() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode(); + String puuid = n.getProperty(jcrPrimaryType).getValue().getString(); + String nuuid = n.getPrimaryNodeType().getName(); + assertEquals("jcr:primaryType needs to be equal to the getPrimaryNodeType() return value.", nuuid, puuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenChildNodeNodeType() throws RepositoryException { + versionableNode.addNode("child"); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode().getNode("child"); + String puuid = n.getProperty(jcrPrimaryType).getValue().getString(); + String nuuid = n.getPrimaryNodeType().getName(); + assertEquals("jcr:primaryType needs to be equal to the getPrimaryNodeType() return value.", nuuid, puuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenNodeType() throws RepositoryException { + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode(); + String fuuid = n.getProperty("jcr:frozenPrimaryType").getValue().getString(); + String ruuid = versionableNode.getPrimaryNodeType().getName(); + assertEquals("jcr:frozenPrimaryType needs to be equal to the getPrimaryNodeType() return value.", ruuid, fuuid); + } + + /** + * @throws RepositoryException + */ + public void testFrozenChildNodeType() throws RepositoryException { + Node n1 = versionableNode.addNode("child"); + versionableNode.getSession().save(); + VersionManager versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + Version v = versionManager.checkin(path); + Node n = v.getFrozenNode().getNode("child"); + String fuuid = n.getProperty("jcr:frozenPrimaryType").getValue().getString(); + String ruuid = n1.getPrimaryNodeType().getName(); + assertEquals("jcr:frozenPrimaryType needs to be equal to the getPrimaryNodeType() return value.", ruuid, fuuid); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/RestoreTest.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/RestoreTest.java new file mode 100644 index 00000000000..d70ddb81cf7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/RestoreTest.java @@ -0,0 +1,1519 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.test.api.version.simple; + +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.nodetype.NodeDefinition; +import javax.jcr.version.OnParentVersionAction; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * RestoreTest covers tests related to the restore methods available + * on {@link Node} in simple versioning: + *
          + *
        • {@link Node#restore(String, boolean)}
        • + *
        • {@link Node#restore(Version, boolean)}
        • + *
        • {@link Node#restore(Version, String, boolean)}
        • + *
        + * + */ +public class RestoreTest extends AbstractVersionTest { + + VersionManager versionManager; + + Version version; + Version version2; + Version rootVersion; + + Node versionableNode2; + + String propertyValue1; + String propertyValue2; + + protected void setUp() throws Exception { + super.setUp(); + try { + versionManager = versionableNode.getSession().getWorkspace().getVersionManager(); + String path = versionableNode.getPath(); + propertyValue1 = getProperty("propertyValue1"); + propertyValue2 = getProperty("propertyValue2"); + versionableNode.setProperty(propertyName1, propertyValue1); + versionableNode.getSession().save(); + version = versionManager.checkin(path); + versionManager.checkout(path); + versionableNode.setProperty(propertyName1, propertyValue2); + versionableNode.getSession().save(); + version2 = versionManager.checkin(path); + versionManager.checkout(path); + rootVersion = versionManager.getVersionHistory(path).getRootVersion(); + } catch (RepositoryException e) { + cleanUp(); + fail("Failed to setup test: " + e.getMessage()); + } + + // build a second versionable node below the testroot + try { + versionableNode2 = createVersionableNode(testRootNode, nodeName2, versionableNodeType); + } catch (RepositoryException e) { + fail("Failed to create a second versionable node: " + e.getMessage()); + } + } + + protected void tearDown() throws Exception { + try { + versionableNode2.remove(); + testRootNode.getSession().save(); + } finally { + version = null; + version2 = null; + rootVersion = null; + versionableNode2 = null; + super.tearDown(); + } + } + + /** + * Test if restoring the root version fails. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreRootVersionFail() throws RepositoryException { + try { + versionableNode.restore(rootVersion, true); + fail("Restore of jcr:rootVersion must throw VersionException."); + } catch (VersionException e) { + // success + } + } + + /** + * Test if restoring the root version fails. + * + * @throws RepositoryException + */ + public void testRestoreRootVersionFailJcr2() throws RepositoryException { + try { + versionManager.restore(rootVersion, true); + fail("Restore of jcr:rootVersion must throw VersionException."); + } catch (VersionException e) { + // success + } + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreOnCheckedInNode() throws RepositoryException { + versionableNode.checkin(); + versionableNode.restore(version, true); + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedInNodeJcr2_1() throws RepositoryException { + versionManager.checkin(versionableNode.getPath()); + versionManager.restore(version, true); + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedInNodeJcr2_2() throws RepositoryException { + versionManager.checkin(versionableNode.getPath()); + try { + versionManager.restore(versionableNode.getPath(), version, true); + fail("VersionManager.restore(String, Version, boolean) must fail on existing nodes."); + } catch (RepositoryException e) { + // ok + } + versionManager.restore(version, true); + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedInNodeJcr2_3() throws RepositoryException { + versionManager.checkin(versionableNode.getPath()); + versionManager.restore(versionableNode.getPath(), version.getName(), true); + } + + /** + * Test if restoring a node works on checked-in node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedInNodeJcr2_4() throws RepositoryException { + versionManager.checkin(versionableNode.getPath()); + versionManager.restore(new Version[] {version}, true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreOnCheckedOutNode() throws RepositoryException { + versionableNode.restore(version, true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedOutNodeJcr2() throws RepositoryException { + versionManager.restore(version, true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedOutNodeJcr2_2() throws RepositoryException { + try { + versionManager.restore(versionableNode.getPath(), version, true); + fail("VersionManager.restore(String, Version, boolean) must fail on existing nodes."); + } catch (RepositoryException e) { + // ok + } + versionManager.restore(version, true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedOutNodeJcr2_3() throws RepositoryException { + versionManager.restore(versionableNode.getPath(), version.getName(), true); + } + + /** + * Test if restoring a node works on checked-out node. + * + * @throws RepositoryException + */ + public void testRestoreOnCheckedOutNodeJcr2_4() throws RepositoryException { + versionManager.restore(new Version[] {version}, true); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreSetsIsCheckedOutToFalse() throws RepositoryException { + versionableNode.restore(version, true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionableNode.isCheckedOut()); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + public void testRestoreSetsIsCheckedOutToFalseJcr2() throws RepositoryException { + versionManager.restore(version, true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionManager.isCheckedOut(versionableNode.getPath())); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + public void testRestoreSetsIsCheckedOutToFalseJcr2_2() throws RepositoryException { + try { + versionManager.restore(versionableNode.getPath(), version, true); + fail("VersionManager.restore(String, Version, boolean) must fail on existing nodes."); + } catch (RepositoryException e) { + // ok + } + versionManager.restore(version, true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionManager.isCheckedOut(versionableNode.getPath())); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + public void testRestoreSetsIsCheckedOutToFalseJcr3() throws RepositoryException { + versionManager.restore(versionableNode.getPath(), version.getName(), true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionManager.isCheckedOut(versionableNode.getPath())); + } + + /** + * Restoring a node set the jcr:isCheckedOut property to false. + * + * @throws RepositoryException + */ + public void testRestoreSetsIsCheckedOutToFalseJcr2_4() throws RepositoryException { + versionManager.restore(new Version[] {version}, true); + assertFalse("Restoring a node sets the jcr:isCheckedOut property to false", versionManager.isCheckedOut(versionableNode.getPath())); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreCorrectProperty() throws RepositoryException { + versionableNode.restore(version, true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + public void testRestoreCorrectPropertyJcr2() throws RepositoryException { + versionManager.restore(version, true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + public void testRestoreCorrectPropertyJcr2_2() throws RepositoryException { + try { + versionManager.restore(versionableNode.getPath(), version, true); + fail("VersionManager.restore(String, Version, boolean) must fail on existing nodes."); + } catch (RepositoryException e) { + // ok + } + versionManager.restore(version, true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + public void testRestoreCorrectPropertyJcr2_3() throws RepositoryException { + versionManager.restore(versionableNode.getPath(), version.getName(), true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if restoring a node restores the correct property + * + * @throws RepositoryException + */ + public void testRestoreCorrectPropertyJcr2_4() throws RepositoryException { + versionManager.restore(new Version[] {version}, true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Restoring a node must set the correct property.", propertyValue1, value); + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreWithPendingChanges() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionableNode.restore(version, true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + public void testRestoreWithPendingChangesJcr2() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionManager.restore(version, true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + public void testRestoreWithPendingChangesJcr2_2() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionManager.restore(version, true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + public void testRestoreWithPendingChangesJcr2_3() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionManager.restore(versionableNode.getPath(), version.getName(), true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * Test if InvalidItemStateException is thrown if the node has pending changes. + * + * @throws RepositoryException + */ + public void testRestoreWithPendingChangesJcr2_4() throws RepositoryException { + // modify node without calling save() + try { + versionableNode.setProperty(propertyName1, propertyValue); + versionManager.restore(new Version[] {version}, true); + + fail("InvalidItemStateException must be thrown on attempt to restore a node having any unsaved changes pending."); + } catch (InvalidItemStateException e) { + // ok + } + } + + /** + * VersionException expected on Node.restore(Version, boolean) if the + * specified version is not part of this node's version history. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreInvalidVersion() throws RepositoryException { + Version vNode2 = versionableNode2.checkin(); + try { + versionableNode.restore(vNode2, true); + + fail("VersionException expected on Node.restore(Version, boolean) if the specified version is not part of this node's version history."); + } catch (VersionException e) { + // ok + } + } + + /** + * VersionException expected on restore if the + * specified version is not part of this node's version history. + * + * @throws RepositoryException + */ + public void testRestoreInvalidVersionJcr2() throws RepositoryException { + Version vNode2 = versionManager.checkin(versionableNode2.getPath()); + try { + versionManager.restore(versionableNode.getPath(), vNode2, true); + + fail("VersionException expected on Node.restore(Version, boolean) if the specified version is not part of this node's version history."); + } catch (VersionException e) { + // ok + } + } + + /** + * VersionException expected on Node.restore(String, boolean) if the specified version is not part of this node's version history. + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreInvalidVersion2() throws RepositoryException { + String invalidName; + do { + invalidName = createRandomString(3); + for (VersionIterator it = versionableNode.getVersionHistory().getAllVersions(); it.hasNext();) { + Version v = it.nextVersion(); + if (invalidName.equals(v.getName())) { + invalidName = null; + break; + } + } + } while (invalidName == null); + + try { + versionableNode.restore(invalidName, true); + fail("VersionException expected on Node.restore(String, boolean) if the specified version is not part of this node's version history."); + } catch (VersionException e) { + // ok + } + } + + /** + * VersionException expected on Node.restore(String, boolean) if the specified version is not part of this node's version history. + * + * @throws RepositoryException + */ + public void testRestoreInvalidVersion2Jcr2() throws RepositoryException { + String invalidName; + do { + invalidName = createRandomString(3); + for (VersionIterator it = versionManager.getVersionHistory(versionableNode.getPath()).getAllVersions(); it.hasNext();) { + Version v = it.nextVersion(); + if (invalidName.equals(v.getName())) { + invalidName = null; + break; + } + } + } while (invalidName == null); + + try { + versionManager.restore(versionableNode.getPath(), invalidName, true); + fail("VersionException expected on Node.restore(String, boolean) if the specified version is not part of this node's version history."); + } catch (VersionException e) { + // ok + } + } + + /** + * Test calling Node.restore(String, boolean) on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(String, boolean) + */ + @SuppressWarnings("deprecation") + public void testRestoreNonVersionableNode() throws RepositoryException { + try { + nonVersionableNode.restore("foo", true); + fail("Node.restore(String, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test restoring on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(String, boolean) + */ + public void testRestoreNonVersionableNodeJcr2_2() throws RepositoryException { + try { + versionManager.restore(nonVersionableNode.getPath(), "foo", true); + fail("trying to restore on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling Node.restore(Version, String, boolean) on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(Version, String, boolean) + */ + public void testRestoreNonVersionableNode2() throws RepositoryException { + // the 'version' will be restored at location 'foo'. + + try { + nonVersionableNode.getParent().restore(version, nonVersionableNode.getName(), true); + fail("Node.restore(Version, String, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test calling Node.restore(Version, boolean) on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(Version, boolean) + */ + @SuppressWarnings("deprecation") + public void testRestoreNonVersionableNode3() throws RepositoryException { + try { + nonVersionableNode.restore(version, true); + fail("Node.restore(Version, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test restoring on a non-versionable node. + * + * @throws RepositoryException + * @see Node#restore(Version, boolean) + */ + public void testRestoreNonVersionableNode3Jcr2_2() throws RepositoryException { + try { + versionManager.restore(nonVersionableNode.getPath(), version.getName(), true); + fail("Node.restore(Version, boolean) on a non versionable node must throw UnsupportedRepositoryOperationException"); + } catch (UnsupportedRepositoryOperationException e) { + //success + } + } + + /** + * Test if restoring a node with an invalid Version throws a VersionException + * + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreWithInvalidVersion() throws RepositoryException { + Version invalidVersion = versionableNode2.checkin(); + try { + versionableNode.restore(invalidVersion, true); + fail("Node.restore(Version, boolean): A VersionException must be thrown if the specified version does not exists in this node's version history."); + } catch (VersionException e) { + // success + } + } + + /** + * Test if restoring a node with an invalid Version throws a VersionException + * + * @throws RepositoryException + */ + public void testRestoreWithInvalidVersionJcr2() throws RepositoryException { + Version invalidVersion = versionManager.checkin(versionableNode2.getPath()); + try { + versionManager.restore(versionableNode.getPath(), invalidVersion, true); + fail("Node.restore(Version, boolean): A VersionException must be thrown if the specified version does not exists in this node's version history."); + } catch (VersionException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + @SuppressWarnings("deprecation") + public void testRestoreWithUUIDConflict() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionableNode.checkin(); + versionableNode.checkout(); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionableNode.restore(v, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testRestoreWithUUIDConflictJcr2() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionManager.restore(v, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testRestoreWithUUIDConflictJcr2_2() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionManager.restore(v, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testRestoreWithUUIDConflictJcr2_3() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionManager.restore(versionableNode.getPath(), v.getName(), false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + /** + * Tests if restoring the Version of an existing node throws an + * ItemExistsException if removeExisting is set to FALSE. + */ + public void testRestoreWithUUIDConflictJcr2_4() throws RepositoryException, NotExecutableException { + try { + Node naa = createVersionableNode(versionableNode, nodeName4, versionableNodeType); + // Verify that nodes used for the test have proper opv behaviour + NodeDefinition nd = naa.getDefinition(); + if (nd.getOnParentVersion() != OnParentVersionAction.COPY && nd.getOnParentVersion() != OnParentVersionAction.VERSION) { + throw new NotExecutableException("Child nodes must have OPV COPY or VERSION in order to be able to test Node.restore with uuid conflict."); + } + + Version v = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + superuser.move(naa.getPath(), versionableNode2.getPath() + "/" + naa.getName()); + superuser.save(); + versionManager.restore(new Version[] {v}, false); + + fail("Node.restore( Version, boolean ): An ItemExistsException must be thrown if the node to be restored already exsits and removeExisting was set to false."); + } catch (ItemExistsException e) { + // success + } + } + + @SuppressWarnings("deprecation") + public void testRestoreChild1() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionableNode.checkin(); + versionableNode.checkout(); + Version v2 = versionableNode.checkin(); + + versionableNode.restore(v1, true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionableNode.restore(version, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionableNode.restore(v2, true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + public void testRestoreChild1Jcr2() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + Version v2 = versionManager.checkin(versionableNode.getPath()); + + versionManager.restore(v1, true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionManager.restore(version, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionManager.restore(v2, true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + public void testRestoreChild1Jcr2_2() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + Version v2 = versionManager.checkin(versionableNode.getPath()); + + versionManager.restore(v1, true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionManager.restore(version, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionManager.restore(v2, true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + public void testRestoreChild1Jcr2_3() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + Version v2 = versionManager.checkin(versionableNode.getPath()); + + versionManager.restore(versionableNode.getPath(), v1.getName(), true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionManager.restore(versionableNode.getPath(), version.getName(), true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionManager.restore(versionableNode.getPath(), v2.getName(), true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + public void testRestoreChild1Jcr2_4() throws RepositoryException { + versionableNode.addNode("child1"); + versionableNode.getSession().save(); + Version v1 = versionManager.checkin(versionableNode.getPath()); + versionManager.checkout(versionableNode.getPath()); + Version v2 = versionManager.checkin(versionableNode.getPath()); + + versionManager.restore(new Version[] {v1}, true); + assertTrue("Node.restore('1.2') must not remove child node.", versionableNode.hasNode("child1")); + + versionManager.restore(new Version[] {version}, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode("child1")); + + try { + versionManager.restore(new Version[] {v2}, true); + } catch (RepositoryException e) { + fail("Node.restore('1.3') must fail."); + } + } + + /** + * Test the restore of a versionable node using a label. + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreLabel() throws RepositoryException { + // mark V1 with label test1 + versionableNode.getVersionHistory().addVersionLabel(version.getName(), "test", true); + + // restore V1 via label. + versionableNode.restoreByLabel("test", true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Node.restore('test') not correctly restored", propertyValue1, value); + } + + /** + * Test the restore of a versionable node using a label. + * @throws RepositoryException + */ + public void testRestoreLabelJcr2() throws RepositoryException { + // mark V1 with label test1 + versionManager.getVersionHistory(versionableNode.getPath()).addVersionLabel(version.getName(), "test", true); + + // restore V1 via label. + versionManager.restoreByLabel(versionableNode.getPath(), "test", true); + String value = versionableNode.getProperty(propertyName1).getString(); + assertEquals("Node.restore('test') not correctly restored", propertyValue1, value); + } + + /** + * Test the restore of the OPV=Version child nodes. + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreName() throws RepositoryException, + NotExecutableException { + // V1.0 of versionableNode has no child + Node child1 = versionableNode.addNode(nodeName4); + ensureMixinType(child1, mixVersionable); + versionableNode.getSession().save(); + // create v1.0 of child + Version v1Child = child1.checkin(); + + // V1 of versionable node has child1 + String v1 = versionableNode.checkin().getName(); + + // create V1.1 of child + child1.checkout(); + Version v11Child = child1.checkin(); + + // V2 of versionable node has child1 + versionableNode.checkout(); + String v2 = versionableNode.checkin().getName(); + + // restore 1.0 of versionable node --> no child + versionableNode.restore(version, true); + assertFalse("Node.restore('1.0') must remove child node.", versionableNode.hasNode(nodeName4)); + + // restore V1 via name. since child was checkin first, 1.0 should be restored + versionableNode.restore(v1, true); + assertTrue("Node.restore('test') must restore child node.", versionableNode.hasNode(nodeName4)); + child1 = versionableNode.getNode(nodeName4); + assertEquals("Node.restore('test') must restore child node version 1.0.", v1Child.getName(), child1.getBaseVersion().getName()); + + // restore V2 via name. child should be 1.1 + versionableNode.restore(v2, true); + child1 = versionableNode.getNode(nodeName4); + assertEquals("Node.restore('foo') must restore child node version 1.1.", v11Child.getName(), child1.getBaseVersion().getName()); + } + + /** + * Test the restore of the OPV=Version child nodes. + * @throws RepositoryException + */ + public void testRestoreNameJcr2() throws RepositoryException, + NotExecutableException { + // V1.0 of versionableNode has no child + Node child1 = versionableNode.addNode(nodeName4); + ensureMixinType(child1, mixVersionable); + versionableNode.getSession().save(); + // create v1.0 of child + Version v1Child = child1.checkin(); + + // V1 of versionable node has child1 + String v1 = versionManager.checkin(versionableNode.getPath()).getName(); + + // create V1.1 of child + versionManager.checkout(child1.getPath()); + Version v11Child = versionManager.checkin(child1.getPath()); + + // V2 of versionable node has child1 + versionManager.checkout(versionableNode.getPath()); + String v2 = versionManager.checkin(versionableNode.getPath()).getName(); + + // restore 1.0 of versionable node --> no child + versionManager.restore(version, true); + assertFalse("restore must remove child node.", versionableNode.hasNode(nodeName4)); + + // restore V1 via name. since child was checkin first, 1.0 should be restored + versionManager.restore(versionableNode.getPath(), v1, true); + assertTrue("restore must restore child node.", versionableNode.hasNode(nodeName4)); + child1 = versionableNode.getNode(nodeName4); + assertEquals("restore must restore child node version 1.0.", v1Child.getName(), versionManager.getBaseVersion(child1.getPath()).getName()); + + // restore V2 via name. child should be 1.1 + versionManager.restore(versionableNode.getPath(), v2, true); + child1 = versionableNode.getNode(nodeName4); + assertEquals("Node.restore('foo') must restore child node version 1.1.", v11Child.getName(), versionManager.getBaseVersion(child1.getPath()).getName()); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreOrder() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + child1.checkin(); + child2.checkin(); + Version v1 = testRoot.checkin(); + + // remove node 1 + testRoot.checkout(); + child1.remove(); + testRoot.getSession().save(); + testRoot.checkin(); + + // restore version 1.0 + testRoot.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrderJcr2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // remove node 1 + versionManager.checkout(testRoot.getPath()); + child1.remove(); + testRoot.getSession().save(); + versionManager.checkout(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrderJcr2_2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // remove node 1 + versionManager.checkout(testRoot.getPath()); + child1.remove(); + testRoot.getSession().save(); + versionManager.checkout(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrderJcr2_3() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // remove node 1 + versionManager.checkout(testRoot.getPath()); + child1.remove(); + testRoot.getSession().save(); + versionManager.checkout(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(testRoot.getPath(), v1.getName(), true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrderJcr2_4() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // remove node 1 + versionManager.checkout(testRoot.getPath()); + child1.remove(); + testRoot.getSession().save(); + versionManager.checkout(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(new Version[] {v1}, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + @SuppressWarnings("deprecation") + public void testRestoreOrder2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + child1.checkin(); + child2.checkin(); + Version v1 = testRoot.checkin(); + + // reoder nodes + testRoot.checkout(); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + testRoot.checkin(); + + // restore version 1.0 + testRoot.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrder2Jcr2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // reoder nodes + versionManager.checkout(testRoot.getPath()); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + versionManager.checkin(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrder2Jcr2_2() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // reoder nodes + versionManager.checkout(testRoot.getPath()); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + versionManager.checkin(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrder2Jcr2_3() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // reoder nodes + versionManager.checkout(testRoot.getPath()); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + versionManager.checkin(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(v1, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Test the child ordering of restored nodes. + * @throws RepositoryException + */ + public void testRestoreOrder2Jcr2_4() throws RepositoryException, + NotExecutableException { + // create a test-root that has orderable child nodes + Node testRoot = versionableNode.addNode(nodeName4, "nt:unstructured"); + ensureMixinType(testRoot, mixVersionable); + versionableNode.getSession().save(); + + // create children of vNode and checkin + Node child1 = testRoot.addNode(nodeName1); + ensureMixinType(child1, mixVersionable); + Node child2 = testRoot.addNode(nodeName2); + ensureMixinType(child2, mixVersionable); + testRoot.getSession().save(); + versionManager.checkin(child1.getPath()); + versionManager.checkin(child2.getPath()); + Version v1 = versionManager.checkin(testRoot.getPath()); + + // reoder nodes + versionManager.checkout(testRoot.getPath()); + testRoot.orderBefore(nodeName2, nodeName1); + testRoot.getSession().save(); + versionManager.checkin(testRoot.getPath()); + + // restore version 1.0 + versionManager.restore(new Version[] {v1}, true); + + // check order + NodeIterator iter = testRoot.getNodes(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n1 = iter.nextNode(); + assertTrue(testRoot.getName() + " should have 2 child nodes.", iter.hasNext()); + Node n2 = iter.nextNode(); + String orderOk = nodeName1 + ", " + nodeName2; + String order = n1.getName() + ", " + n2.getName(); + assertEquals("Invalid child node ordering", orderOk, order); + } + + /** + * Tests if restore on simple versioning creates a new version that is + * in the correct linear order. + */ + @SuppressWarnings("deprecation") + public void testLinearVersions() throws Exception { + // first get all linear versions + VersionIterator iter = versionableNode.getVersionHistory().getAllLinearVersions(); + StringBuffer expected = new StringBuffer(); + while (iter.hasNext()) { + expected.append(iter.nextVersion().getName()).append(","); + } + // restore version + versionableNode.restore(version, true); + // append new base version + expected.append(versionableNode.getBaseVersion().getName()).append(","); + + // get the version names again + iter = versionableNode.getVersionHistory().getAllLinearVersions(); + StringBuffer actual = new StringBuffer(); + while (iter.hasNext()) { + actual.append(iter.nextVersion().getName()).append(","); + } + assertEquals("Node.restore() on simple versioning must create a new version.", + expected.toString(), actual.toString()); + } + + /** + * Tests if restore on simple versioning creates a new version that is + * in the correct linear order. + */ + public void testLinearVersionsJcr2() throws Exception { + // first get all linear versions + VersionIterator iter = versionManager.getVersionHistory(versionableNode.getPath()).getAllLinearVersions(); + StringBuffer expected = new StringBuffer(); + while (iter.hasNext()) { + expected.append(iter.nextVersion().getName()).append(","); + } + // restore version + versionManager.restore(version, true); + // append new base version + expected.append(versionManager.getBaseVersion(versionableNode.getPath()).getName()).append(","); + + // get the version names again + iter = versionManager.getVersionHistory(versionableNode.getPath()).getAllLinearVersions(); + StringBuffer actual = new StringBuffer(); + while (iter.hasNext()) { + actual.append(iter.nextVersion().getName()).append(","); + } + assertEquals("Node.restore() on simple versioning must create a new version.", + expected.toString(), actual.toString()); + } + + /** + * Tests if restore on simple versioning creates a new version that is + * in the correct linear order. + */ + public void testLinearVersionsJcr2_2() throws Exception { + // first get all linear versions + VersionIterator iter = versionManager.getVersionHistory(versionableNode.getPath()).getAllLinearVersions(); + StringBuffer expected = new StringBuffer(); + while (iter.hasNext()) { + expected.append(iter.nextVersion().getName()).append(","); + } + // restore version + versionManager.restore(version, true); + // append new base version + expected.append(versionManager.getBaseVersion(versionableNode.getPath()).getName()).append(","); + + // get the version names again + iter = versionManager.getVersionHistory(versionableNode.getPath()).getAllLinearVersions(); + StringBuffer actual = new StringBuffer(); + while (iter.hasNext()) { + actual.append(iter.nextVersion().getName()).append(","); + } + assertEquals("Node.restore() on simple versioning must create a new version.", + expected.toString(), actual.toString()); + } + + /** + * Tests if restore on simple versioning creates a new version that is + * in the correct linear order. + */ + public void testLinearVersionsJcr2_3() throws Exception { + // first get all linear versions + VersionIterator iter = versionManager.getVersionHistory(versionableNode.getPath()).getAllLinearVersions(); + StringBuffer expected = new StringBuffer(); + while (iter.hasNext()) { + expected.append(iter.nextVersion().getName()).append(","); + } + // restore version + versionManager.restore(versionableNode.getPath(), version.getName(), true); + // append new base version + expected.append(versionManager.getBaseVersion(versionableNode.getPath()).getName()).append(","); + + // get the version names again + iter = versionManager.getVersionHistory(versionableNode.getPath()).getAllLinearVersions(); + StringBuffer actual = new StringBuffer(); + while (iter.hasNext()) { + actual.append(iter.nextVersion().getName()).append(","); + } + assertEquals("Node.restore() on simple versioning must create a new version.", + expected.toString(), actual.toString()); + } + + /** + * Tests if restore on simple versioning creates a new version that is + * in the correct linear order. + */ + public void testLinearVersionsJcr2_4() throws Exception { + // first get all linear versions + VersionIterator iter = versionManager.getVersionHistory(versionableNode.getPath()).getAllLinearVersions(); + StringBuffer expected = new StringBuffer(); + while (iter.hasNext()) { + expected.append(iter.nextVersion().getName()).append(","); + } + // restore version + versionManager.restore(new Version[] {version}, true); + // append new base version + expected.append(versionManager.getBaseVersion(versionableNode.getPath()).getName()).append(","); + + // get the version names again + iter = versionManager.getVersionHistory(versionableNode.getPath()).getAllLinearVersions(); + StringBuffer actual = new StringBuffer(); + while (iter.hasNext()) { + actual.append(iter.nextVersion().getName()).append(","); + } + assertEquals("Node.restore() on simple versioning must create a new version.", + expected.toString(), actual.toString()); + } +} diff --git a/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/TestAll.java b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/TestAll.java new file mode 100644 index 00000000000..c1c3959f313 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/java/org/apache/jackrabbit/test/api/version/simple/TestAll.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.test.api.version.simple; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** TestAll... */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("javax.jcr.version.simple tests"); + + suite.addTestSuite(BasicTest.class); + suite.addTestSuite(CheckinTest.class); + suite.addTestSuite(CheckoutTest.class); + suite.addTestSuite(CopyTest.class); + suite.addTestSuite(FrozenNodeTest.class); + suite.addTestSuite(RestoreTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-created.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-created.txt new file mode 100644 index 00000000000..ab8243ea6c8 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-created.txt @@ -0,0 +1,22 @@ +NodeTypeName + mix:created +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:created + RequiredType DATE + DefaultValues null + Mandatory false + Protected true + Multiple false +PropertyDefinition + Name jcr:createdBy + RequiredType STRING + DefaultValues null + Mandatory false + Protected true + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-etag.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-etag.txt new file mode 100644 index 00000000000..d49ede0b0c0 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-etag.txt @@ -0,0 +1,17 @@ +NodeTypeName + mix:etag +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:etag + RequiredType STRING + DefaultValues null + AutoCreated true + Mandatory false + OnParentVersion COPY + Protected true + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-language.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-language.txt new file mode 100644 index 00000000000..4b65d39559b --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-language.txt @@ -0,0 +1,15 @@ +NodeTypeName + mix:language +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:language + RequiredType STRING + DefaultValues null + Mandatory false + Protected false + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lastModified.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lastModified.txt new file mode 100644 index 00000000000..ec416b65fce --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lastModified.txt @@ -0,0 +1,22 @@ +NodeTypeName + mix:lastModified +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:lastModified + RequiredType DATE + DefaultValues null + Mandatory false + Protected false + Multiple false +PropertyDefinition + Name jcr:lastModifiedBy + RequiredType STRING + DefaultValues null + Mandatory false + Protected false + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lifecycle.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lifecycle.txt new file mode 100644 index 00000000000..a3aa73e3823 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lifecycle.txt @@ -0,0 +1,26 @@ +NodeTypeName + mix:lifecycle +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:currentLifecycleState + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion INITIALIZE + Protected true + Multiple false +PropertyDefinition + Name jcr:lifecyclePolicy + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion INITIALIZE + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lockable.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lockable.txt new file mode 100644 index 00000000000..b8e4b0a0667 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-lockable.txt @@ -0,0 +1,26 @@ +NodeTypeName + mix:lockable +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:lockIsDeep + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion IGNORE + Protected true + Multiple false +PropertyDefinition + Name jcr:lockOwner + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion IGNORE + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-mimeType.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-mimeType.txt new file mode 100644 index 00000000000..7ae486f7555 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-mimeType.txt @@ -0,0 +1,22 @@ +NodeTypeName + mix:mimeType +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:encoding + RequiredType STRING + DefaultValues null + Mandatory false + Protected false + Multiple false +PropertyDefinition + Name jcr:mimeType + RequiredType STRING + DefaultValues null + Mandatory false + Protected false + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-referenceable.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-referenceable.txt new file mode 100644 index 00000000000..cfaefacaa8a --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-referenceable.txt @@ -0,0 +1,17 @@ +NodeTypeName + mix:referenceable +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:uuid + RequiredType STRING + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion INITIALIZE + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-shareable.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-shareable.txt new file mode 100644 index 00000000000..880fc694544 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-shareable.txt @@ -0,0 +1,8 @@ +NodeTypeName + mix:shareable +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-simpleVersionable.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-simpleVersionable.txt new file mode 100644 index 00000000000..b6173fc3155 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-simpleVersionable.txt @@ -0,0 +1,17 @@ +NodeTypeName + mix:simpleVersionable +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:isCheckedOut + RequiredType BOOLEAN + DefaultValues [true] + AutoCreated true + Mandatory true + OnParentVersion IGNORE + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-title.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-title.txt new file mode 100644 index 00000000000..b43493a85c7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-title.txt @@ -0,0 +1,22 @@ +NodeTypeName + mix:title +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:description + RequiredType STRING + DefaultValues null + Mandatory false + Protected false + Multiple false +PropertyDefinition + Name jcr:title + RequiredType STRING + DefaultValues null + Mandatory false + Protected false + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-versionable.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-versionable.txt new file mode 100644 index 00000000000..96a1f7a34c7 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/mix-versionable.txt @@ -0,0 +1,62 @@ +NodeTypeName + mix:versionable +IsMixin + true +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:activity + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:baseVersion + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion IGNORE + Protected true + Multiple false +PropertyDefinition + Name jcr:configuration + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion IGNORE + Protected true + Multiple false +PropertyDefinition + Name jcr:mergeFailed + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple true +PropertyDefinition + Name jcr:predecessors + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion IGNORE + Protected true + Multiple true +PropertyDefinition + Name jcr:versionHistory + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion IGNORE + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-activity.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-activity.txt new file mode 100644 index 00000000000..a212c03d59e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-activity.txt @@ -0,0 +1,17 @@ +NodeTypeName + nt:activity +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:activityTitle + RequiredType STRING + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion COPY + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-address.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-address.txt new file mode 100644 index 00000000000..ce6dd6c8e80 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-address.txt @@ -0,0 +1,71 @@ +NodeTypeName + nt:address +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:host + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false +PropertyDefinition + Name jcr:id + RequiredType WEAKREFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false +PropertyDefinition + Name jcr:path + RequiredType PATH + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false +PropertyDefinition + Name jcr:port + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false +PropertyDefinition + Name jcr:protocol + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false +PropertyDefinition + Name jcr:repository + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false +PropertyDefinition + Name jcr:workspace + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-base.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-base.txt new file mode 100644 index 00000000000..dfcd7b24106 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-base.txt @@ -0,0 +1,26 @@ +NodeTypeName + nt:base +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:mixinTypes + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COMPUTE + Protected true + Multiple true +PropertyDefinition + Name jcr:primaryType + RequiredType NAME + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion COMPUTE + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-childNodeDefinition.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-childNodeDefinition.txt new file mode 100644 index 00000000000..2c0a67aa3b4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-childNodeDefinition.txt @@ -0,0 +1,80 @@ +NodeTypeName + nt:childNodeDefinition +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:autoCreated + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:defaultPrimaryType + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:mandatory + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:name + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:onParentVersion + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:protected + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:requiredPrimaryTypes + RequiredType NAME + DefaultValues [nt:base] + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple true +PropertyDefinition + Name jcr:sameNameSiblings + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-configuration.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-configuration.txt new file mode 100644 index 00000000000..f88cf7e9664 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-configuration.txt @@ -0,0 +1,17 @@ +NodeTypeName + nt:configuration +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:root + RequiredType REFERENCE + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion COPY + Protected true + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-file.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-file.txt new file mode 100644 index 00000000000..e2de2a52ae0 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-file.txt @@ -0,0 +1,17 @@ +NodeTypeName + nt:file +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + jcr:content +ChildNodeDefinition + Name jcr:content + RequiredPrimaryTypes [nt:base] + DefaultPrimaryType null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected false + SameNameSiblings false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-folder.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-folder.txt new file mode 100644 index 00000000000..0f5a98c9a77 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-folder.txt @@ -0,0 +1,17 @@ +NodeTypeName + nt:folder +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +ChildNodeDefinition + Name "*" + RequiredPrimaryTypes [nt:hierarchyNode] + DefaultPrimaryType null + AutoCreated false + Mandatory false + OnParentVersion VERSION + Protected false + SameNameSiblings false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-frozenNode.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-frozenNode.txt new file mode 100644 index 00000000000..292c42ff7f5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-frozenNode.txt @@ -0,0 +1,62 @@ +NodeTypeName + nt:frozenNode +IsMixin + false +HasOrderableChildNodes + true +PrimaryItemName + null +ChildNodeDefinition + Name "*" + RequiredPrimaryTypes [nt:base] + DefaultPrimaryType null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + SameNameSiblings true +PropertyDefinition + Name jcr:frozenMixinTypes + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple true +PropertyDefinition + Name jcr:frozenPrimaryType + RequiredType NAME + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion ABORT + Protected true + Multiple false +PropertyDefinition + Name jcr:frozenUuid + RequiredType STRING + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion ABORT + Protected true + Multiple false +PropertyDefinition + Name "*" + RequiredType UNDEFINED + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple false +PropertyDefinition + Name "*" + RequiredType UNDEFINED + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple true diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-hierarchyNode.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-hierarchyNode.txt new file mode 100644 index 00000000000..846616f0bd6 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-hierarchyNode.txt @@ -0,0 +1,8 @@ +NodeTypeName + nt:hierarchyNode +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-linkedFile.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-linkedFile.txt new file mode 100644 index 00000000000..6b480bd367c --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-linkedFile.txt @@ -0,0 +1,17 @@ +NodeTypeName + nt:linkedFile +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + jcr:content +PropertyDefinition + Name jcr:content + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected false + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-nodeType.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-nodeType.txt new file mode 100644 index 00000000000..e40b8c54868 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-nodeType.txt @@ -0,0 +1,89 @@ +NodeTypeName + nt:nodeType +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +ChildNodeDefinition + Name jcr:childNodeDefinition + RequiredPrimaryTypes [nt:childNodeDefinition] + DefaultPrimaryType nt:childNodeDefinition + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + SameNameSiblings true +ChildNodeDefinition + Name jcr:propertyDefinition + RequiredPrimaryTypes [nt:propertyDefinition] + DefaultPrimaryType nt:propertyDefinition + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + SameNameSiblings true +PropertyDefinition + Name jcr:hasOrderableChildNodes + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:isAbstract + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:isMixin + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:isQueryable + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:nodeTypeName + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:primaryItemName + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:supertypes + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + Multiple true \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-propertyDefinition.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-propertyDefinition.txt new file mode 100644 index 00000000000..29319ce91f5 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-propertyDefinition.txt @@ -0,0 +1,116 @@ +NodeTypeName + nt:propertyDefinition +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:autoCreated + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:availableQueryOperators + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple true +PropertyDefinition + Name jcr:defaultValues + RequiredType UNDEFINED + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + Multiple true +PropertyDefinition + Name jcr:isFullTextSearchable + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:isQueryOrderable + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:mandatory + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:multiple + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:name + RequiredType NAME + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:onParentVersion + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:protected + RequiredType BOOLEAN + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:requiredType + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected true + Multiple false +PropertyDefinition + Name jcr:valueConstraints + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected true + Multiple true \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-query.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-query.txt new file mode 100644 index 00000000000..957e4d9a4b4 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-query.txt @@ -0,0 +1,26 @@ +NodeTypeName + nt:query +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:language + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false +PropertyDefinition + Name jcr:statement + RequiredType STRING + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-resource.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-resource.txt new file mode 100644 index 00000000000..248562a0e05 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-resource.txt @@ -0,0 +1,17 @@ +NodeTypeName + nt:resource +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + jcr:data +PropertyDefinition + Name jcr:data + RequiredType BINARY + DefaultValues null + AutoCreated false + Mandatory true + OnParentVersion COPY + Protected false + Multiple false \ No newline at end of file diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-unstructured.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-unstructured.txt new file mode 100644 index 00000000000..3c54e9491aa --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-unstructured.txt @@ -0,0 +1,35 @@ +NodeTypeName + nt:unstructured +IsMixin + false +HasOrderableChildNodes + true +PrimaryItemName + null +ChildNodeDefinition + Name "*" + RequiredPrimaryTypes [nt:base] + DefaultPrimaryType nt:unstructured + AutoCreated false + Mandatory false + OnParentVersion VERSION + Protected false + SameNameSiblings true +PropertyDefinition + Name "*" + RequiredType UNDEFINED + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple false +PropertyDefinition + Name "*" + RequiredType UNDEFINED + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion COPY + Protected false + Multiple true diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-version.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-version.txt new file mode 100644 index 00000000000..730267774db --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-version.txt @@ -0,0 +1,53 @@ +NodeTypeName + nt:version +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +ChildNodeDefinition + Name jcr:frozenNode + RequiredPrimaryTypes [nt:frozenNode] + DefaultPrimaryType null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + SameNameSiblings false +PropertyDefinition + Name jcr:activity + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple false +PropertyDefinition + Name jcr:created + RequiredType DATE + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion ABORT + Protected true + Multiple false +PropertyDefinition + Name jcr:predecessors + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple true +PropertyDefinition + Name jcr:successors + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple true diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionHistory.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionHistory.txt new file mode 100644 index 00000000000..e577c5d721e --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionHistory.txt @@ -0,0 +1,53 @@ +NodeTypeName + nt:versionHistory +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +ChildNodeDefinition + Name jcr:rootVersion + RequiredPrimaryTypes [nt:version] + DefaultPrimaryType nt:version + AutoCreated true + Mandatory true + OnParentVersion ABORT + Protected true + SameNameSiblings false +ChildNodeDefinition + Name jcr:versionLabels + RequiredPrimaryTypes [nt:versionLabels] + DefaultPrimaryType nt:versionLabels + AutoCreated true + Mandatory true + OnParentVersion ABORT + Protected true + SameNameSiblings false +ChildNodeDefinition + Name "*" + RequiredPrimaryTypes [nt:version] + DefaultPrimaryType nt:version + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + SameNameSiblings false +PropertyDefinition + Name jcr:copiedFrom + RequiredType WEAKREFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple false +PropertyDefinition + Name jcr:versionableUuid + RequiredType STRING + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion ABORT + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionLabels.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionLabels.txt new file mode 100644 index 00000000000..e9ecf351f53 --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionLabels.txt @@ -0,0 +1,17 @@ +NodeTypeName + nt:versionLabels +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name "*" + RequiredType REFERENCE + DefaultValues null + AutoCreated false + Mandatory false + OnParentVersion ABORT + Protected true + Multiple false diff --git a/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionedChild.txt b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionedChild.txt new file mode 100644 index 00000000000..6a1f1f3daee --- /dev/null +++ b/jackrabbit-jcr-tests/src/main/resources/org/apache/jackrabbit/test/api/nodetype/spec/nt-versionedChild.txt @@ -0,0 +1,17 @@ +NodeTypeName + nt:versionedChild +IsMixin + false +HasOrderableChildNodes + false +PrimaryItemName + null +PropertyDefinition + Name jcr:childVersionHistory + RequiredType REFERENCE + DefaultValues null + AutoCreated true + Mandatory true + OnParentVersion ABORT + Protected true + Multiple false diff --git a/jackrabbit-jcr2dav/pom.xml b/jackrabbit-jcr2dav/pom.xml new file mode 100644 index 00000000000..694f29eceed --- /dev/null +++ b/jackrabbit-jcr2dav/pom.xml @@ -0,0 +1,208 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-jcr2dav + Jackrabbit JCR to WebDAV + + + 0 + + + + + integrationTesting + + + + maven-surefire-plugin + + ${test.opts} + true + + + jackrabbit.test.integration + true + + + org.apache.jackrabbit.jcr2dav.RepositoryStubImpl.port + ${org.apache.jackrabbit.jcr2dav.RepositoryStubImpl.port} + + + known.issues + + + + + org.apache.jackrabbit.test.api.SerializationTest#testNodeTypeConstraintViolationWorkspace + + org.apache.jackrabbit.test.api.ImpersonateTest + + org.apache.jackrabbit.test.api.ShareableNodeTest + + org.apache.jackrabbit.test.api.LifecycleTest + + org.apache.jackrabbit.test.api.lock.LockManagerTest#testAddInvalidLockToken + org.apache.jackrabbit.test.api.lock.LockManagerTest#testAddLockTokenToAnotherSession + org.apache.jackrabbit.test.api.lock.LockManagerTest#testLockTransfer2 + org.apache.jackrabbit.jcr2spi.lock.OpenScopedLockTest#testLogoutHasNoEffect + + org.apache.jackrabbit.test.api.observation.NodeReorderTest#testNodeReorderMove + + org.apache.jackrabbit.test.api.query.CreateQueryTest#testUnknownQueryLanguage + + org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest + + org.apache.jackrabbit.test.api.version.simple + + org.apache.jackrabbit.test.api.version.ActivitiesTest + org.apache.jackrabbit.test.api.version.MergeActivityTest#testMergeActivity + org.apache.jackrabbit.test.api.version.ConfigurationsTest + + org.apache.jackrabbit.jcr2spi.IsSameTest#testIsSameProperty3 + org.apache.jackrabbit.jcr2spi.IsSameTest#testIsSameProperty4 + org.apache.jackrabbit.jcr2spi.IsSameTest#testIsSameNode7 + + org.apache.jackrabbit.jcr2spi.name.NamespaceRegistryTest#testReRegisteredNamespace + org.apache.jackrabbit.jcr2spi.name.NamespaceRegistryTest#testReRegisteredNamespaceVisibility + org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testRightOuterJoin1 + org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testLeftOuterJoin2 + + + + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAggregatedPrivilegesSeparately + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryInvalidPrincipal + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryInvalidPrivilege + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryTwice + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryAgain + + org.apache.jackrabbit.test.api.security.RSessionAccessControlPolicyTest#testGetApplicablePolicies + org.apache.jackrabbit.test.api.security.RSessionAccessControlPolicyTest#testGetPolicy + + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testStringLiteralInvalidName + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testPathLiteral + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testURILiteral + + org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.acl.AccessControlManagerImplTest#testAddingFourAccessControlEntries + + + + derby.system.durability + test + + + derby.storage.fileSyncTransactionLog + true + + + derby.stream.error.file + target/derby.log + + + + + + + + + + + + javax.jcr + jcr + + + org.apache.jackrabbit + jackrabbit-jcr2spi + ${project.version} + + + org.apache.jackrabbit + jackrabbit-spi2dav + ${project.version} + + + + org.apache.jackrabbit + jackrabbit-jcr-tests + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-jcr2spi + ${project.version} + tests + test + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-jcr-server + ${project.version} + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-servlet + test + + + ch.qos.logback + logback-classic + test + + + + diff --git a/jackrabbit-jcr2dav/src/main/java/org/apache/jackrabbit/jcr2dav/Jcr2davRepositoryFactory.java b/jackrabbit-jcr2dav/src/main/java/org/apache/jackrabbit/jcr2dav/Jcr2davRepositoryFactory.java new file mode 100644 index 00000000000..81960bd60c5 --- /dev/null +++ b/jackrabbit-jcr2dav/src/main/java/org/apache/jackrabbit/jcr2dav/Jcr2davRepositoryFactory.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.jcr2dav; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory; +import org.apache.jackrabbit.jcr2spi.RepositoryImpl; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; +import org.apache.jackrabbit.spi2dav.Spi2davRepositoryServiceFactory; +import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory; + +/** + * Repository factory for JCR to WebDAV connections. This factory supports + * three main configuration parameters: + *
        + *
        {@link JcrUtils#REPOSITORY_URI org.apache.jackrabbit.repository.uri}
        + *
        + * If this parameter contains a valid http or https URI, then an spi2davex + * connection to that URI is returned. + *
        + *
        {@link Spi2davRepositoryServiceFactory#PARAM_REPOSITORY_URI org.apache.jackrabbit.spi2dav.uri}
        + *
        + * If this parameter is specified, then an spi2dav connection + * to that URI is returned. + *
        + *
        {@link Spi2davexRepositoryServiceFactory#PARAM_REPOSITORY_URI org.apache.jackrabbit.spi2davex.uri}
        + *
        + * If this parameter is specified, then an spi2davex connection + * to that URI is returned. + *
        + *
        + * + * @since Apache Jackrabbit 2.0 + */ +@SuppressWarnings("unchecked") +public class Jcr2davRepositoryFactory implements RepositoryFactory { + + private static final String DAV_URI = + Spi2davRepositoryServiceFactory.PARAM_REPOSITORY_URI; + + private static final String DAVEX_URI = + Spi2davexRepositoryServiceFactory.PARAM_REPOSITORY_URI; + + public Repository getRepository(Map parameters) throws RepositoryException { + if (parameters == null) { + return null; + } else if (parameters.containsKey(DAV_URI)) { + return getRepository( + new Spi2davRepositoryServiceFactory(), parameters); + } else if (parameters.containsKey(DAVEX_URI)) { + return getRepository( + new Spi2davexRepositoryServiceFactory(), parameters); + } else if (parameters.containsKey(JcrUtils.REPOSITORY_URI)) { + Map copy = new HashMap(parameters); + Object parameter = copy.remove(JcrUtils.REPOSITORY_URI); + try { + URI uri = new URI(parameter.toString().trim()); + String scheme = uri.getScheme(); + // TODO: Check whether this is a valid dav or davex URI + // TODO: Support tags like + if ("http".equalsIgnoreCase(scheme) + || "https".equalsIgnoreCase(scheme)) { + copy.put(DAVEX_URI, parameter); + return getRepository( + new Spi2davexRepositoryServiceFactory(), copy); + } else { + return null; + } + } catch (URISyntaxException e) { + return null; + } + } else { + return null; + } + } + + private Repository getRepository( + RepositoryServiceFactory factory, Map parameters) + throws RepositoryException { + try { + return RepositoryImpl.create( + new Jcr2spiRepositoryFactory.RepositoryConfigImpl( + factory, parameters)); + } catch (RepositoryException e) { + // Unable to connect to the specified repository. + // Most likely the server is either not running or + // the given URI does not point to a valid davex server. + return null; + } + } + +} diff --git a/jackrabbit-jcr2dav/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory b/jackrabbit-jcr2dav/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory new file mode 100644 index 00000000000..66524ad19d2 --- /dev/null +++ b/jackrabbit-jcr2dav/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.jcr2dav.Jcr2davRepositoryFactory diff --git a/jackrabbit-jcr2dav/src/main/resources/accessControlProvider.properties b/jackrabbit-jcr2dav/src/main/resources/accessControlProvider.properties new file mode 100644 index 00000000000..d89cc0e157c --- /dev/null +++ b/jackrabbit-jcr2dav/src/main/resources/accessControlProvider.properties @@ -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.jcr2spi.AccessControlProvider.class=org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.acl.AccessControlProviderImpl diff --git a/jackrabbit-jcr2dav/src/test/java/org/apache/jackrabbit/jcr2dav/ConformanceTest.java b/jackrabbit-jcr2dav/src/test/java/org/apache/jackrabbit/jcr2dav/ConformanceTest.java new file mode 100644 index 00000000000..313075ea880 --- /dev/null +++ b/jackrabbit-jcr2dav/src/test/java/org/apache/jackrabbit/jcr2dav/ConformanceTest.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.jcr2dav; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestResult; +import junit.framework.TestSuite; + +import org.apache.jackrabbit.jcr2spi.security.Jcr2SpiSecurityTestSuite; +import org.apache.jackrabbit.jcr2spi.Jcr2SpiTestSuite; +import org.apache.jackrabbit.test.JCRTestSuite; + +/** + * JCR API conformance test suite. + */ +public class ConformanceTest extends TestCase { + + public static TestSuite suite() { + TestSuite suite = new TestSuite(); + if (Boolean.getBoolean("jackrabbit.test.integration")) { + suite.addTest(new JCRTestSuite()); + suite.addTest(new Jcr2SpiTestSuite()); + suite.addTest(new Jcr2SpiSecurityTestSuite()); + suite.addTest(new StopRepository()); + } + return suite; + } + + private static class StopRepository implements Test { + + public int countTestCases() { + return 1; + } + + public void run(TestResult result) { + try { + RepositoryStubImpl.stopServer(); + } catch (Exception e) { + result.addError(this, e); + } + } + } +} diff --git a/jackrabbit-jcr2dav/src/test/java/org/apache/jackrabbit/jcr2dav/RepositoryStubImpl.java b/jackrabbit-jcr2dav/src/test/java/org/apache/jackrabbit/jcr2dav/RepositoryStubImpl.java new file mode 100644 index 00000000000..9bc44196dfc --- /dev/null +++ b/jackrabbit-jcr2dav/src/test/java/org/apache/jackrabbit/jcr2dav/RepositoryStubImpl.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.jcr2dav; + +import java.security.Principal; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.JackrabbitRepositoryStub; +import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStubException; +import org.apache.jackrabbit.webdav.jcr.JCRWebdavServerServlet; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +public class RepositoryStubImpl extends JackrabbitRepositoryStub { + + private static final String PROP_ACCESSCONTROL_PROVIDER_CLASS = "org.apache.jackrabbit.jcr2spi.AccessControlProvider.class"; + + private static final String PROP_PROTECTED_ITEM_REMOVE_CLASS = "org.apache.jackrabbit.server.ProtectedItemRemoveHandler.class"; + + private static Repository repository; + + private static ServerConnector connector; + + private static Server server; + + private static Repository client; + + private final String acProviderImplClass; + + private final String protectedRemoveImplClass; + + public RepositoryStubImpl(Properties env) { + super(env); + acProviderImplClass = env.getProperty(PROP_ACCESSCONTROL_PROVIDER_CLASS); + protectedRemoveImplClass = env.getProperty(PROP_PROTECTED_ITEM_REMOVE_CLASS); + } + + @Override + public Repository getRepository() throws RepositoryStubException { + if (repository == null) { + repository = super.getRepository(); + } + + if (server == null) { + server = new Server(); + + ServletHolder holder = new ServletHolder(new JcrRemotingServlet() { + protected Repository getRepository() { + return repository; + } + }); + holder.setInitParameter(JCRWebdavServerServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, ""); + holder.setInitParameter(JCRWebdavServerServlet.INIT_PARAM_MISSING_AUTH_MAPPING, ""); + holder.setInitParameter(JcrRemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG, protectedRemoveImplClass); + + ServletContextHandler schandler = new ServletContextHandler(server, "/"); + schandler.addServlet(holder, "/*"); + } + + if (connector == null) { + connector = new ServerConnector(server); + connector.setHost("localhost"); + String pvalue = System.getProperty("org.apache.jackrabbit.jcr2dav.RepositoryStubImpl.port", "0"); + int port = pvalue.equals("") ? 0 : Integer.parseInt(pvalue); + connector.setPort(port); + server.addConnector(connector); + + try { + server.start(); + } catch (Exception e) { + throw new RepositoryStubException(e); + } + } + + if (client == null) { + try { + Map parameters = new HashMap(); + + String uri = "http://localhost:" + connector.getLocalPort() + "/"; + + String parmName = System.getProperty(this.getClass().getName() + ".REPURIPARM", JcrUtils.REPOSITORY_URI); + parameters.put(parmName, uri); + parameters.put(PROP_ACCESSCONTROL_PROVIDER_CLASS, acProviderImplClass); + + client = JcrUtils.getRepository(parameters); + } catch (Exception e) { + throw new RepositoryStubException(e); + } + } + + return client; + } + + @Override + public Principal getKnownPrincipal(Session session) throws RepositoryException { + // TODO + return new Principal() { + @Override + public String getName() { + return "everyone"; + } + }; + } + + @Override + public Principal getUnknownPrincipal(Session session) throws RepositoryException, NotExecutableException { + // TODO + return new Principal() { + @Override + public String getName() { + return "unknownPrincipal"; + } + }; + } + + public static void stopServer() throws Exception { + if (server != null) { + server.stop(); + } + } +} diff --git a/jackrabbit-jcr2dav/src/test/resources/logback-test.xml b/jackrabbit-jcr2dav/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..15d98f9b73f --- /dev/null +++ b/jackrabbit-jcr2dav/src/test/resources/logback-test.xml @@ -0,0 +1,31 @@ + + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-jcr2dav/src/test/resources/protectedHandlersConfig.xml b/jackrabbit-jcr2dav/src/test/resources/protectedHandlersConfig.xml new file mode 100644 index 00000000000..949aaf100df --- /dev/null +++ b/jackrabbit-jcr2dav/src/test/resources/protectedHandlersConfig.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/jackrabbit-jcr2dav/src/test/resources/repository.xml b/jackrabbit-jcr2dav/src/test/resources/repository.xml new file mode 100644 index 00000000000..95f050e8ce8 --- /dev/null +++ b/jackrabbit-jcr2dav/src/test/resources/repository.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-jcr2dav/src/test/resources/repositoryStubImpl.properties b/jackrabbit-jcr2dav/src/test/resources/repositoryStubImpl.properties new file mode 100644 index 00000000000..590bfcca0fd --- /dev/null +++ b/jackrabbit-jcr2dav/src/test/resources/repositoryStubImpl.properties @@ -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. + +# Stub implementation class +javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.jcr2dav.RepositoryStubImpl + +# Access control provider implementation class +org.apache.jackrabbit.jcr2spi.AccessControlProvider.class=org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.acl.AccessControlProviderImpl + +# ProtectedItemRemoveHandler implementation class +org.apache.jackrabbit.server.ProtectedItemRemoveHandler.class=org.apache.jackrabbit.server.remoting.davex.AclRemoveHandler + +# the repository home +org.apache.jackrabbit.repository.home=target/repository + +# the repository configuration +org.apache.jackrabbit.repository.config=target/test-classes/repository.xml + +# credential configuration +javax.jcr.tck.superuser.name=admin +javax.jcr.tck.superuser.pwd=admin +javax.jcr.tck.readwrite.name=user +javax.jcr.tck.readwrite.pwd=user +javax.jcr.tck.readonly.name=anonymous +javax.jcr.tck.readonly.pwd= diff --git a/jackrabbit-jcr2spi/README.txt b/jackrabbit-jcr2spi/README.txt new file mode 100644 index 00000000000..3d4d3a41ba4 --- /dev/null +++ b/jackrabbit-jcr2spi/README.txt @@ -0,0 +1,7 @@ +================================= +Welcome to Jackrabbit JCR2SPI +================================= + +This is the JCR2SPI component of the Apache Jackrabbit project. +This component contains an implementation of the JSR-170 API and +covers the functionality that is not delegated to the SPI implementation. diff --git a/jackrabbit-jcr2spi/assembly.xml b/jackrabbit-jcr2spi/assembly.xml new file mode 100644 index 00000000000..023a7b4e0fb --- /dev/null +++ b/jackrabbit-jcr2spi/assembly.xml @@ -0,0 +1,33 @@ + + + + tests + false + + jar + + + + ${project.build.testOutputDirectory} + + + logback-test.xml + + + + diff --git a/jackrabbit-jcr2spi/pom.xml b/jackrabbit-jcr2spi/pom.xml new file mode 100644 index 00000000000..66f00fb12f0 --- /dev/null +++ b/jackrabbit-jcr2spi/pom.xml @@ -0,0 +1,149 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-jcr2spi + Jackrabbit JCR to SPI + bundle + + + + + org.apache.felix + maven-bundle-plugin + true + + + + org.apache.jackrabbit.jcr2spi;version=${project.version};include:=Jcr2spiRepositoryFactory, + org.apache.jackrabbit.jcr2spi.config;version=${project.version} + + + + + + maven-assembly-plugin + + + package + + single + + + + assembly.xml + + + + + + + maven-surefire-plugin + + + **/* + + + + known.issues + + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testStringLiteralInvalidName + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testPathLiteral + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testURILiteral + + + + + + + + + + + javax.jcr + jcr + + + org.apache.jackrabbit + jackrabbit-api + ${project.version} + + + org.apache.jackrabbit + jackrabbit-spi + ${project.version} + + + org.apache.jackrabbit + jackrabbit-spi-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + + org.slf4j + slf4j-api + + + commons-collections + commons-collections + + + commons-io + commons-io + + + concurrent + concurrent + test + + + junit + junit + test + + + org.apache.jackrabbit + jackrabbit-jcr-tests + ${project.version} + test + + + ch.qos.logback + logback-classic + test + + + + diff --git a/jackrabbit-jcr2spi/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-jcr2spi/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemCache.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemCache.java new file mode 100644 index 00000000000..d6454f749bd --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemCache.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.jcr2spi; + +import org.apache.jackrabbit.jcr2spi.state.ItemState; + +import javax.jcr.Item; + +/** + * ItemCache... + */ +public interface ItemCache extends ItemLifeCycleListener { + + /** + * Returns the cached Item that belongs to the given + * ItemState or null if the cache does not + * contain that Item. + * + * @param state State of the item that should be retrieved. + * @return The item reference stored in the corresponding cache entry + * or null if there's no corresponding cache entry. + */ + Item getItem(ItemState state); + + /** + * Clear all entries in the ItemCache and free resources. + */ + void clear(); +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemCacheImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemCacheImpl.java new file mode 100644 index 00000000000..9e0764a45a2 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemCacheImpl.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.jcr2spi; + +import java.util.Map; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +import org.apache.commons.collections.map.LRUMap; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ItemCacheImpl... + */ +public class ItemCacheImpl implements ItemCache { + + private static Logger log = LoggerFactory.getLogger(ItemCacheImpl.class); + + private final Map cache; + + @SuppressWarnings("unchecked") + ItemCacheImpl(int maxSize) { + cache = new LRUMap(maxSize); + } + + //----------------------------------------------------------< ItemCache >--- + /** + * @see ItemCache#getItem(ItemState) + */ + public Item getItem(ItemState state) { + return cache.get(state); + } + + /** + * @see ItemCache#clear() + */ + public void clear() { + cache.clear(); + } + + //----------------------------------------------< ItemLifeCycleListener >--- + /** + * @see ItemLifeCycleListener#itemCreated(Item) + */ + public void itemCreated(Item item) { + if (!(item instanceof ItemImpl)) { + String msg = "Incompatible Item object: " + ItemImpl.class.getName() + " expected."; + throw new IllegalArgumentException(msg); + } + if (log.isDebugEnabled()) { + log.debug("created item " + item); + } + // add instance to cache + cacheItem(((ItemImpl)item).getItemState(), item); + } + + public void itemUpdated(Item item, boolean modified) { + if (!(item instanceof ItemImpl)) { + String msg = "Incompatible Item object: " + ItemImpl.class.getName() + " expected."; + throw new IllegalArgumentException(msg); + } + if (log.isDebugEnabled()) { + log.debug("update item " + item); + } + + ItemState state = ((ItemImpl) item).getItemState(); + // touch the corresponding cache entry + Item cacheEntry = getItem(state); + if (cacheEntry == null) { + // .. or add the item to the cache, if not present yet. + cacheItem(state, item); + } + } + + /** + * @see ItemLifeCycleListener#itemDestroyed(Item) + */ + public void itemDestroyed(Item item) { + if (!(item instanceof ItemImpl)) { + String msg = "Incompatible Item object: " + ItemImpl.class.getName() + " expected."; + throw new IllegalArgumentException(msg); + } + if (log.isDebugEnabled()) { + log.debug("destroyed item " + item); + } + // we're no longer interested in this item + ((ItemImpl)item).removeLifeCycleListener(this); + // remove instance from cache + evictItem(((ItemImpl)item).getItemState()); + } + + //-------------------------------------------------< item cache methods >--- + /** + * Puts the reference of an item in the cache with + * the item's path as the key. + * + * @param item the item to cache + */ + private synchronized void cacheItem(ItemState state, Item item) { + if (cache.containsKey(state)) { + log.warn("overwriting cached item " + state); + } + if (log.isDebugEnabled()) { + log.debug("caching item " + state); + } + cache.put(state, item); + } + + /** + * Removes a cache entry for a specific item. + * + * @param itemState state of the item to remove from the cache + */ + private synchronized void evictItem(ItemState itemState) { + if (log.isDebugEnabled()) { + log.debug("removing item " + itemState + " from cache"); + } + cache.remove(itemState); + } + + //--------------------------------------------------------==---< Object >--- + + /** + * Returns the the state of this instance in a human readable format. + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + for (Map.Entry entry : cache.entrySet()) { + ItemState state = entry.getKey(); + Item item = entry.getValue(); + if (item.isNode()) { + builder.append("Node: "); + } else { + builder.append("Property: "); + } + if (item.isNew()) { + builder.append("new "); + } else if (item.isModified()) { + builder.append("modified "); + } else { + builder.append("- "); + } + String path; + try { + path = item.getPath(); + } catch (RepositoryException e) { + path = "-"; + } + builder.append(state + "\t" + path + " (" + item + ")\n"); + } + return builder.toString(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemImpl.java new file mode 100644 index 00000000000..f5fae8df719 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemImpl.java @@ -0,0 +1,602 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.util.Collections; +import java.util.Map; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.ItemVisitor; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.jcr2spi.config.CacheBehaviour; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.Remove; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateLifeCycleListener; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ItemImpl... + */ +public abstract class ItemImpl implements Item, ItemStateLifeCycleListener { + + private static Logger log = LoggerFactory.getLogger(ItemImpl.class); + + private final ItemState state; + + /** + * The session that created this item. + */ + protected SessionImpl session; + + /** + * Listeners (weak references) + */ + @SuppressWarnings("unchecked") + protected final Map listeners = + Collections.synchronizedMap(new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK)); + + public ItemImpl(SessionImpl session, ItemState state, + ItemLifeCycleListener[] listeners) { + this.session = session; + this.state = state; + + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + addLifeCycleListener(listeners[i]); + } + } + notifyCreated(); + + // add this item as listener to events of the underlying state object + state.addListener(this); + } + + //-----------------------------------------------------< Item interface >--- + /** + * @see javax.jcr.Item#getPath() + */ + public String getPath() throws RepositoryException { + checkStatus(); + return session.getPathResolver().getJCRPath(getQPath()); + } + + /** + * @see javax.jcr.Item#getName() + */ + public abstract String getName() throws RepositoryException; + + /** + * @see javax.jcr.Item#getAncestor(int) + */ + public Item getAncestor(int depth) throws ItemNotFoundException, AccessDeniedException, RepositoryException { + checkStatus(); + if (depth == 0) { + return session.getRootNode(); + } + String msg = "No ancestor at depth = " + depth; + try { + // Path.getAncestor requires relative degree, i.e. we need + // to convert absolute to relative ancestor degree + Path path = getQPath(); + int relDegree = path.getAncestorCount() - depth; + if (relDegree < 0) { + throw new ItemNotFoundException(msg); + } + Path ancestorPath = path.getAncestor(relDegree); + if (relDegree == 0) { + return this; + } else { + return getItemManager().getNode(ancestorPath); + } + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(msg); + } + } + + /** + * @see Item#getParent() + */ + public Node getParent() throws ItemNotFoundException, AccessDeniedException, RepositoryException { + checkStatus(); + + // special treatment for root node + if (state.isNode() && ((NodeState)state).isRoot()) { + String msg = "Root node doesn't have a parent."; + log.debug(msg); + throw new ItemNotFoundException(msg); + } + + NodeEntry parentEntry = getItemState().getHierarchyEntry().getParent(); + return (Node) getItemManager().getItem(parentEntry); + } + + /** + * @see javax.jcr.Item#getDepth() + */ + public int getDepth() throws RepositoryException { + checkStatus(); + if (state.isNode() && ((NodeState)state).isRoot()) { + // shortcut + return Path.ROOT_DEPTH; + } + return session.getHierarchyManager().getDepth(state.getHierarchyEntry()); + } + + /** + * Note: as of 2.x this method returns the session irrespective of the item's + * status. + * + * @see javax.jcr.Item#getSession() + * @see Issue JCR-2529 + */ + public Session getSession() throws RepositoryException { + return session; + } + + /** + * @see javax.jcr.Item#isNew() + */ + public boolean isNew() { + return state.getStatus() == Status.NEW; + } + + /** + * @see javax.jcr.Item#isModified() + */ + public boolean isModified() { + return state.getStatus() == Status.EXISTING_MODIFIED; + } + + /** + * @see javax.jcr.Item#isSame(Item) + */ + public boolean isSame(Item otherItem) throws RepositoryException { + checkStatus(); + if (this == otherItem) { + return true; + } + if (isNode() != otherItem.isNode()) { + return false; + } + if (otherItem instanceof ItemImpl) { + ItemImpl other = (ItemImpl) otherItem; + if (this.state == other.state) { + return true; + } + // check status of the other item. + other.checkStatus(); + + // 2 items may only be the same if the were accessed from Sessions + // bound to the same workspace + String otherWspName = other.session.getWorkspace().getName(); + if (session.getWorkspace().getName().equals(otherWspName)) { + // in addition they must provide the same id irrespective of + // any transient modifications. + if (state.getStatus() != Status.NEW && other.state.getStatus() != Status.NEW ) { + // if any ancestor is _invalidated_ force it's reload in + // order to detect id changes. + updateId(state); + updateId(other.state); + return state.getWorkspaceId().equals(other.state.getWorkspaceId()); + } + /* else: + - if both wsp-states are null, the items are both transiently + added and are only the same if they are obtained from the same + session. in this case, their states must be the same object, + which is covered above. + - either of the two items does not have a workspace state. + therefore the items cannot be the same, since one has been + transiently added in one but not the other session. + */ + } + } + return false; + } + + /** + * @see javax.jcr.Item#accept(ItemVisitor) + */ + public abstract void accept(ItemVisitor visitor) throws RepositoryException; + + /** + * @see javax.jcr.Item#isNode() + */ + public abstract boolean isNode(); + + /** + * @see javax.jcr.Item#save() + */ + public void save() throws AccessDeniedException, ConstraintViolationException, InvalidItemStateException, ReferentialIntegrityException, VersionException, LockException, RepositoryException { + // check state of this instance + checkStatus(); + session.getSessionItemStateManager().save(getItemState()); + } + + /** + * @see javax.jcr.Item#refresh(boolean) + */ + public void refresh(boolean keepChanges) throws InvalidItemStateException, RepositoryException { + // check session status + session.checkIsAlive(); + int status = state.getStatus(); + // check if item has been removed by this or another session + if (Status.isTerminal(status) || Status.EXISTING_REMOVED == status) { + throw new InvalidItemStateException("Item '" + this + "' doesn't exist anymore"); + } + + /* If 'keepChanges' is true, items that do not have changes pending have + their state refreshed to reflect the current saved state */ + if (keepChanges) { + if (status != Status.NEW && + session.getCacheBehaviour() != CacheBehaviour.OBSERVATION) { + // merge current transient modifications with latest changes + // from the 'server'. + // Note, that with Observation-CacheBehaviour no manual refresh + // is required. changes get pushed automatically. + state.getHierarchyEntry().invalidate(true); + } + } else { + // check status of item state + if (status == Status.NEW) { + String msg = "Cannot refresh a new item (" + safeGetJCRPath() + ")."; + log.debug(msg); + throw new RepositoryException(msg); + } + + /* + Reset all transient modifications from this item and its descendants. + */ + session.getSessionItemStateManager().undo(state); + + /* Unless the session is in 'observation' mode, mark all states + within this tree 'invalidated' in order to have them refreshed + from the server upon the next access.*/ + if (session.getCacheBehaviour() != CacheBehaviour.OBSERVATION) { + state.getHierarchyEntry().invalidate(true); + } + } + } + + /** + * @see javax.jcr.Item#remove() + */ + public void remove() throws VersionException, LockException, ConstraintViolationException, RepositoryException { + checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + checkStatus(); + + // validation checks are performed within remove operation + Operation rm = Remove.create(getItemState()); + session.getSessionItemStateManager().execute(rm); + } + + //-----------------------------------------< ItemStateLifeCycleListener >--- + /** + * + * @param state + * @param previousStatus + */ + public void statusChanged(ItemState state, int previousStatus) { + if (state != this.state) { + throw new IllegalArgumentException("Invalid argument: ItemState with changed status must be this.state."); + } + + switch (state.getStatus()) { + /** + * Nothing to do for + * - Status#EXISTING : modifications reverted or saved + * inform listeners about an update (status was MODIFIED before) + * or a simple refresh without modification (status was INVALIDATED). + */ + case Status.EXISTING: + if (previousStatus == Status.INVALIDATED || previousStatus == Status.MODIFIED) { + notifyUpdated(previousStatus == Status.MODIFIED); + } + break; + /** + * Nothing to do for + * - Status#EXISTING_MODIFIED : transient modification + * - Status#STALE_MODIFIED : external modifications while transient changes pending + * - Status#STALE_DESTROYED : external modifications while transient changes pending + * - Status#MODIFIED : externally modified -> marker for sessionISM states only + * - Status#EXISTING_REMOVED : transient removal + */ + case Status.EXISTING_MODIFIED: + case Status.STALE_MODIFIED: + case Status.STALE_DESTROYED: + case Status.MODIFIED: + case Status.EXISTING_REMOVED: + break; + /** + * Notify listeners that this item is transiently or permanently + * destroyed. + * - Status#REMOVED : permanent removal. item will never get back to life + */ + case Status.REMOVED: + state.removeListener(this); + notifyDestroyed(); + break; + /** + * Invalid status. A state can never change its state to 'New'. + */ + case Status.NEW: + // should never happen. + log.error("invalid state change to STATUS_NEW"); + break; + } + } + + //----------------------------------------------------------< LiveCycle >--- + + /** + * Notify the listeners that this instance has been created. + */ + private void notifyCreated() { + // copy listeners to array to avoid ConcurrentModificationException + ItemLifeCycleListener[] la = listeners.values().toArray(new ItemLifeCycleListener[listeners.size()]); + for (int i = 0; i < la.length; i++) { + la[i].itemCreated(this); + } + } + + /** + * Notify the listeners that this instance has been updated. + */ + private void notifyUpdated(boolean modified) { + // copy listeners to array to avoid ConcurrentModificationException + ItemLifeCycleListener[] la = listeners.values().toArray(new ItemLifeCycleListener[listeners.size()]); + for (int i = 0; i < la.length; i++) { + if (la[i] != null) { + la[i].itemUpdated(this, modified); + } + } + } + + /** + * Notify the listeners that this instance has been destroyed. + */ + private void notifyDestroyed() { + // copy listeners to array to avoid ConcurrentModificationException + ItemLifeCycleListener[] la = listeners.values().toArray(new ItemLifeCycleListener[listeners.size()]); + for (int i = 0; i < la.length; i++) { + if (la[i] != null) { + la[i].itemDestroyed(this); + } + } + } + + /** + * Add an ItemLifeCycleListener + * + * @param listener the new listener to be informed on life cycle changes + */ + void addLifeCycleListener(ItemLifeCycleListener listener) { + if (!listeners.containsKey(listener)) { + listeners.put(listener, listener); + } + } + + /** + * Remove an ItemLifeCycleListener + * + * @param listener an existing listener + */ + void removeLifeCycleListener(ItemLifeCycleListener listener) { + listeners.remove(listener); + } + + //------------------------------------------------------< check methods >--- + /** + * Performs a sanity check on this item and the associated session. If + * the underlying item state is in an invalidated state then it will be + * refreshed to get the current status of the item state. The status + * check is then performed on the newly retrieved status. + * + * @throws RepositoryException if this item has been rendered invalid for some reason + */ + protected void checkStatus() throws RepositoryException { + // check session status + session.checkIsAlive(); + // check status of this item for read operation + if (state.getStatus() == Status.INVALIDATED) { + // refresh to get current status from persistent storage + state.getHierarchyEntry().reload(false); + } + // now check if valid + if (!state.isValid()) { + throw new InvalidItemStateException("Item '" + this + "' doesn't exist anymore. (Status = " +Status.getName(state.getStatus())+ ")"); + } + } + + /** + * Returns true if the repository supports the given option. False otherwise. + * + * @param option Any of the option constants defined by {@link Repository} + * that either returns 'true' or 'false'. I.e. + *
          + *
        • {@link Repository#LEVEL_1_SUPPORTED}
        • + *
        • {@link Repository#LEVEL_2_SUPPORTED}
        • + *
        • {@link Repository#OPTION_TRANSACTIONS_SUPPORTED}
        • + *
        • {@link Repository#OPTION_VERSIONING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_OBSERVATION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_LOCKING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_QUERY_SQL_SUPPORTED}
        • + *
        + * @return true if the repository supports the given option. False otherwise. + */ + boolean isSupportedOption(String option) { + return session.isSupportedOption(option); + } + + /** + * Check if the given option is supported by the repository. + * + * @param option Any of the option constants defined by {@link Repository} + * that either returns 'true' or 'false'. I.e. + *
          + *
        • {@link Repository#LEVEL_1_SUPPORTED}
        • + *
        • {@link Repository#LEVEL_2_SUPPORTED}
        • + *
        • {@link Repository#OPTION_ACCESS_CONTROL_SUPPORTED}
        • + *
        • {@link Repository#OPTION_ACTIVITIES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_BASELINES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_JOURNALED_OBSERVATION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_LIFECYCLE_SUPPORTED}
        • + *
        • {@link Repository#OPTION_LOCKING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED}
        • + *
        • {@link Repository#OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_OBSERVATION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_QUERY_SQL_SUPPORTED}
        • + *
        • {@link Repository#OPTION_RETENTION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_SHAREABLE_NODES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_SIMPLE_VERSIONING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_TRANSACTIONS_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UNFILED_CONTENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED}
        • + *
        • {@link Repository#OPTION_VERSIONING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_WORKSPACE_MANAGEMENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_XML_EXPORT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_XML_IMPORT_SUPPORTED}
        • + *
        • {@link Repository#WRITE_SUPPORTED}
        • + *
        + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + */ + void checkSupportedOption(String option) throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkSupportedOption(option); + } + + /** + * Checks if the repository supports level 2 (writing) and the status of + * this item. Note, that this method does not perform any additional + * validation checks such as access restrictions, locking, checkin status + * or protection that affect the writing to nodes and properties. + * + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + * @see ItemStateValidator + */ + protected void checkIsWritable() throws UnsupportedRepositoryOperationException, ConstraintViolationException, RepositoryException { + checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + checkStatus(); + } + + /** + * Returns true if the repository supports level 2 (writing). Note, that + * this method does not perform any additional validation tests such as + * access restrictions, locking, checkin status or protection that affect + * the writing to nodes and properties. + * + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException if the sanity check on this item fails. + * See {@link ItemImpl#checkStatus()}. + * @see ItemStateValidator + */ + protected boolean isWritable() throws RepositoryException { + checkStatus(); + return session.isSupportedOption(Repository.LEVEL_2_SUPPORTED); + } + + //------------------------------------< Implementation specific methods >--- + /** + * Same as {@link Item#getName()} except that + * this method returns a Name instead of a + * String. + * + * @return the name of this item as Name + * @throws RepositoryException if an error occurs. + */ + abstract Name getQName() throws RepositoryException; + + /** + * Returns the primary path to this Item. + * + * @return the primary path to this Item + */ + Path getQPath() throws RepositoryException { + return state.getPath(); + } + + /** + * Returns the item-state associated with this Item. + * + * @return state associated with this Item + */ + protected ItemState getItemState() { + return state; + } + + /** + * Returns the ItemManager associated with this item's Session. + * + * @return ItemManager + */ + protected ItemManager getItemManager() { + return session.getItemManager(); + } + + /** + * Failsafe conversion of internal Path to JCR path for use in + * error messages etc. + * + * @return JCR path + */ + String safeGetJCRPath() { + return LogUtil.safeGetJCRPath(getItemState(), session.getPathResolver()); + } + + /** + * + * @param state + * @throws RepositoryException + */ + private static void updateId(ItemState state) throws RepositoryException { + HierarchyEntry he = state.getHierarchyEntry(); + while (he.getStatus() != Status.INVALIDATED) { + he = he.getParent(); + if (he == null) { + // root reached without intermediate invalidated entry + return; + } + } + // he is INVALIDATED -> force reloading in order to be aware of id changes + he.getItemState(); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemLifeCycleListener.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemLifeCycleListener.java new file mode 100644 index 00000000000..bd6d7b25c7b --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemLifeCycleListener.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.jcr2spi; + +import javax.jcr.Item; + +/** + * The ItemLifeCycleListener interface allows an implementing + * object to be informed about changes on an Item instance. + * + * @see ItemImpl#addLifeCycleListener + */ +public interface ItemLifeCycleListener { + + /** + * Called when an Item instance has been created. + * + * @param item the instance which has been created + */ + public void itemCreated(Item item); + + /** + * Called when an Item instance has been refreshed. If + * modified is true, the refresh included + * some modification. + * + * @param item the instance which has been refreshed + */ + void itemUpdated(Item item, boolean modified); + + /** + * Called when an ItemImpl instance has been destroyed + * (i.e. it has been permanently rendered 'invalid'). + *

        + * Note that most {@link javax.jcr.Item}, + * {@link javax.jcr.Node} and {@link javax.jcr.Property} + * methods will throw an InvalidItemStateException when called + * on a 'destroyed' item. + * + * @param item the instance which has been destroyed + */ + void itemDestroyed(Item item); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemManager.java new file mode 100644 index 00000000000..82a35a7d7e1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemManager.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.jcr2spi; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; + +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NodeIterator; +import javax.jcr.PropertyIterator; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; + +/** + * There's one ItemManager instance per Session + * instance. It is the factory for Node and Property + * instances. + *

        + * The ItemManager's responsibilities are: + *

          + *
        • providing access to Item instances by ItemState + * whereas Node and Item are only providing relative access. + *
        • returning the instance of an existing Node or Property, + * given its absolute path. + *
        • creating the per-session instance of a Node + * or Property that doesn't exist yet and needs to be created first. + *
        • guaranteeing that there aren't multiple instances representing the same + * Node or Property associated with the same + * Session instance. + *
        • maintaining a cache of the item instances it created. + *
        + *

        + * If the parent Session is an XASession, there is + * one ItemManager instance per started global transaction. + */ +public interface ItemManager { + + /** + * Disposes this ItemManager and frees resources. + */ + public void dispose(); + + /** + * Checks if the node with the given path exists. + * + * @param path path to the node to be checked + * @return true if the specified item exists + * @throws RepositoryException + */ + public boolean nodeExists(Path path) throws RepositoryException; + + /** + * Checks if the property with the given path exists. + * + * @param path path to the property to be checked + * @return true if the specified item exists + * @throws RepositoryException + */ + public boolean propertyExists(Path path) throws RepositoryException; + + /** + * Checks if the item for given HierarchyEntry exists. + * + * @param hierarchyEntry + * @return true if the specified item exists + @throws RepositoryException + */ + public boolean itemExists(HierarchyEntry hierarchyEntry) throws RepositoryException; + + /** + * + * @param path + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public Node getNode(Path path) throws PathNotFoundException, RepositoryException; + + /** + * + * @param path + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public Property getProperty(Path path) throws PathNotFoundException, RepositoryException; + + /** + * + * @param hierarchyEntry + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public Item getItem(HierarchyEntry hierarchyEntry) throws ItemNotFoundException, RepositoryException; + + /** + * + * @param parentEntry + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public boolean hasChildNodes(NodeEntry parentEntry) throws ItemNotFoundException, RepositoryException; + + /** + * + * @param parentEntry + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public NodeIterator getChildNodes(NodeEntry parentEntry) throws ItemNotFoundException, RepositoryException; + + /** + * + * @param parentEntry + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public boolean hasChildProperties(NodeEntry parentEntry) throws ItemNotFoundException, RepositoryException; + + /** + * + * @param parentEntry + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public PropertyIterator getChildProperties(NodeEntry parentEntry) throws ItemNotFoundException, RepositoryException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemManagerImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemManagerImpl.java new file mode 100644 index 00000000000..4a725827cba --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ItemManagerImpl.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateCreationListener; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.jcr2spi.version.VersionHistoryImpl; +import org.apache.jackrabbit.jcr2spi.version.VersionImpl; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; +import java.util.Iterator; + +/** + * ItemManagerImpl implements the ItemManager interface. + */ +public class ItemManagerImpl implements ItemManager, ItemStateCreationListener { + + private static Logger log = LoggerFactory.getLogger(ItemManagerImpl.class); + + private final SessionImpl session; + + private final HierarchyManager hierMgr; + + /** + * A cache for item instances created by this ItemManagerImpl. + * + * The ItemStates act as keys for the map. In contrast to + * o.a.j.core the item state are copied to transient space for reading and + * will therefor not change upon transient modifications. + */ + private final ItemCache itemCache; + + /** + * Creates a new per-session instance ItemManagerImpl instance. + * + * @param hierMgr HierarchyManager associated with the new instance + * @param session the session associated with the new instance + * @param cache the ItemCache to be used. + */ + ItemManagerImpl(HierarchyManager hierMgr, SessionImpl session, ItemCache cache) { + this.hierMgr = hierMgr; + this.session = session; + itemCache = cache; + + // start listening to creation of ItemStates upon batch-reading in the + // workspace item state factory. + Workspace wsp = session.getWorkspace(); + if (wsp instanceof WorkspaceImpl) { + ((WorkspaceImpl) wsp).getItemStateFactory().addCreationListener(this); + } + } + + //--------------------------------------------------------< ItemManager >--- + /** + * @see ItemManager#dispose() + */ + public void dispose() { + // stop listening + Workspace wsp = session.getWorkspace(); + if (wsp instanceof WorkspaceImpl) { + ((WorkspaceImpl) wsp).getItemStateFactory().removeCreationListener(this); + } + // ... and clear the cache. + itemCache.clear(); + } + + /** + * @see ItemManager#nodeExists(Path) + */ + public boolean nodeExists(Path path) throws RepositoryException { + try { + // session-sanity & permissions are checked upon itemExists(ItemState) + NodeState nodeState = hierMgr.getNodeState(path); + return itemExists(nodeState); + } catch (PathNotFoundException pnfe) { + return false; + } catch (ItemNotFoundException infe) { + return false; + } + } + + /** + * @see ItemManager#propertyExists(Path) + */ + public boolean propertyExists(Path path) throws RepositoryException { + try { + // session-sanity & permissions are checked upon itemExists(ItemState) + PropertyState propState = hierMgr.getPropertyState(path); + return itemExists(propState); + } catch (PathNotFoundException pnfe) { + return false; + } catch (ItemNotFoundException infe) { + return false; + } + } + + /** + * @see ItemManager#itemExists(HierarchyEntry) + */ + public boolean itemExists(HierarchyEntry hierarchyEntry) throws RepositoryException { + try { + // session-sanity & permissions are checked upon itemExists(ItemState) + ItemState state = hierarchyEntry.getItemState(); + return itemExists(state); + } catch (ItemNotFoundException e) { + return false; + } + } + + /** + * + * @param itemState + * @return + */ + private boolean itemExists(ItemState itemState) { + try { + // check sanity of session + session.checkIsAlive(); + // return true, if ItemState is valid. Access rights are granted, + // otherwise the state would not have been retrieved. + return itemState.isValid(); + } catch (ItemNotFoundException infe) { + return false; + } catch (RepositoryException re) { + return false; + } + } + + /** + * @see ItemManager#getNode(Path) + */ + public synchronized Node getNode(Path path) throws PathNotFoundException, RepositoryException { + NodeEntry nodeEntry = hierMgr.getNodeEntry(path); + try { + return (Node) getItem(nodeEntry); + } catch (ItemNotFoundException infe) { + throw new PathNotFoundException(LogUtil.safeGetJCRPath(path, session.getPathResolver())); + } + } + + /** + * @see ItemManager#getProperty(Path) + */ + public synchronized Property getProperty(Path path) throws PathNotFoundException, RepositoryException { + PropertyEntry propertyEntry = hierMgr.getPropertyEntry(path); + try { + return (Property) getItem(propertyEntry); + } catch (ItemNotFoundException infe) { + throw new PathNotFoundException(LogUtil.safeGetJCRPath(path, session.getPathResolver())); + } + } + + /** + * @see ItemManager#getItem(HierarchyEntry) + */ + public Item getItem(HierarchyEntry hierarchyEntry) throws ItemNotFoundException, RepositoryException { + session.checkIsAlive(); + ItemState state = hierarchyEntry.getItemState(); + if (!state.isValid()) { + throw new ItemNotFoundException(LogUtil.safeGetJCRPath(state, session.getPathResolver())); + } + + // first try to access item from cache + Item item = itemCache.getItem(state); + // not yet in cache, need to create instance + if (item == null) { + // create instance of item + if (hierarchyEntry.denotesNode()) { + item = createNodeInstance((NodeState) state); + } else { + item = createPropertyInstance((PropertyState) state); + } + } + return item; + } + + /** + * @see ItemManager#hasChildNodes(NodeEntry) + */ + public synchronized boolean hasChildNodes(NodeEntry parentEntry) + throws ItemNotFoundException, RepositoryException { + // check sanity of session + session.checkIsAlive(); + + Iterator iter = parentEntry.getNodeEntries(); + while (iter.hasNext()) { + try { + // check read access by accessing the nodeState (implicit validation check) + NodeEntry entry = iter.next(); + entry.getNodeState(); + return true; + } catch (ItemNotFoundException e) { + // should not occur. ignore + log.debug("Failed to access node state.", e); + } + } + return false; + } + + /** + * @see ItemManager#getChildNodes(NodeEntry) + */ + public synchronized NodeIterator getChildNodes(NodeEntry parentEntry) + throws ItemNotFoundException, RepositoryException { + // check sanity of session + session.checkIsAlive(); + + Iterator it = parentEntry.getNodeEntries(); + return new LazyItemIterator(this, it); + } + + /** + * @see ItemManager#hasChildProperties(NodeEntry) + */ + public synchronized boolean hasChildProperties(NodeEntry parentEntry) + throws ItemNotFoundException, RepositoryException { + // check sanity of session + session.checkIsAlive(); + + Iterator iter = parentEntry.getPropertyEntries(); + while (iter.hasNext()) { + try { + PropertyEntry entry = iter.next(); + // check read access by accessing the propState (also implicit validation). + entry.getPropertyState(); + return true; + } catch (ItemNotFoundException e) { + // should not occur. ignore + log.debug("Failed to access node state.", e); + } + } + return false; + } + + /** + * @see ItemManager#getChildProperties(NodeEntry) + */ + public synchronized PropertyIterator getChildProperties(NodeEntry parentEntry) + throws ItemNotFoundException, RepositoryException { + // check sanity of session + session.checkIsAlive(); + + Iterator propEntries = parentEntry.getPropertyEntries(); + return new LazyItemIterator(this, propEntries); + } + + //-------------------------------------------------------------< Object >--- + + /** + * Returns the the state of this instance in a human readable format. + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ItemManager (" + super.toString() + ")\n"); + builder.append("Items in cache:\n"); + builder.append(itemCache); + return builder.toString(); + } + + //----------------------------------------------------< private methods >--- + /** + * @param state + * @return a new Node instance. + * @throws RepositoryException + */ + private NodeImpl createNodeInstance(NodeState state) throws RepositoryException { + // we want to be informed on life cycle changes of the new node object + // in order to maintain item cache consistency + ItemLifeCycleListener[] listeners = new ItemLifeCycleListener[]{itemCache}; + + // check special nodes + Name ntName = state.getNodeTypeName(); + if (NameConstants.NT_VERSION.equals(ntName)) { + // version + return new VersionImpl(session, state, listeners); + } else if (NameConstants.NT_VERSIONHISTORY.equals(ntName)) { + // version-history + return new VersionHistoryImpl(session, state, listeners); + } else { + // create common node object + return new NodeImpl(session, state, listeners); + } + } + + /** + * @param state + * @return a new Property instance. + */ + private PropertyImpl createPropertyInstance(PropertyState state) { + // we want to be informed on life cycle changes of the new property object + // in order to maintain item cache consistency + ItemLifeCycleListener[] listeners = new ItemLifeCycleListener[]{itemCache}; + // create property object + PropertyImpl prop = new PropertyImpl(session, state, listeners); + return prop; + } + + //------------------------------------------< ItemStateCreationListener >--- + /** + * + * @param state + */ + public void created(ItemState state) { + if (state.isNode()) { + try { + createNodeInstance((NodeState) state); + } catch (RepositoryException e) { + // log warning and ignore + log.warn("Unable to create Node instance: " + e.getMessage()); + } + } else { + createPropertyInstance((PropertyState) state); + } + } + + public void statusChanged(ItemState state, int previousStatus) { + // stop listening if an state reached Status.REMOVED. + if (Status.REMOVED == state.getStatus()) { + state.removeListener(this); + } + // otherwise: nothing to do -> Item is listening to status changes and + // forces cleanup of cache entries through it's own status changes. + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/Jcr2spiRepositoryFactory.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/Jcr2spiRepositoryFactory.java new file mode 100644 index 00000000000..b4e00ee3894 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/Jcr2spiRepositoryFactory.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.jcr2spi; + +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +import org.apache.jackrabbit.jcr2spi.config.CacheBehaviour; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; +import org.apache.jackrabbit.spi.commons.logging.LogWriterProvider; +import org.apache.jackrabbit.spi.commons.logging.SpiLoggerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This implementation of {@link RepositoryFactory} is capable of returning the various + * SPI implementations of the Apache Jackrabbit project: + *

          + *
        • SPI2DAVex (see jackrabbit-spi2dav module)
        • + *
        • SPI2DAV (see jackrabbit-spi2dav module)
        • + *
        • SPI2JCR (see jackrabbit-spi2jcr module)
        • + *
        + */ +public class Jcr2spiRepositoryFactory implements RepositoryFactory { + static final Logger log = LoggerFactory.getLogger(Jcr2spiRepositoryFactory.class); + + /** + * This parameter determines the {@link RepositoryServiceFactory} to create the + * {@link RepositoryService}. This is either an instance of RepositoryServiceFactory + * or a fully qualified class name of a RepositoryServiceFactory + * having a no argument constructor. + */ + public static final String PARAM_REPOSITORY_SERVICE_FACTORY = "org.apache.jackrabbit.spi.RepositoryServiceFactory"; + + /** + * This parameter contains the {@link RepositoryConfig} instance. + */ + public static final String PARAM_REPOSITORY_CONFIG = "org.apache.jackrabbit.jcr2spi.RepositoryConfig"; + + /** + * Optional configuration parameter for {@link RepositoryConfig#getCacheBehaviour()}. This + * must be either {@link CacheBehaviour#INVALIDATE} or {@link CacheBehaviour#OBSERVATION} + * or one of the strings "invalidate" or "observation". + */ + public static final String PARAM_CACHE_BEHAVIOR = "org.apache.jackrabbit.jcr2spi.CacheBehaviour"; + + /** + * Default value for {@link #PARAM_CACHE_BEHAVIOR} + */ + public static final CacheBehaviour DEFAULT_CACHE_BEHAVIOR = CacheBehaviour.INVALIDATE; + + /** + * Optional configuration parameter for the {@link RepositoryConfig#getItemCacheSize()}. This + * must be either an Integer or a String which parses into an integer. + */ + public static final String PARAM_ITEM_CACHE_SIZE = "org.apache.jackrabbit.jcr2spi.ItemCacheSize"; + + /** + * Default value for {@link #PARAM_ITEM_CACHE_SIZE} + */ + public static final int DEFAULT_ITEM_CACHE_SIZE = 5000; + + /** + * Optional configuration parameter for the {@link RepositoryConfig#getPollTimeout()}. This + * must be either an Integer or a String which parses into an integer. + */ + public static final String PARAM_POLL_TIME_OUT = "org.apache.jackrabbit.jcr2spi.PollTimeOut"; + + /** + * Default value for {@link #PARAM_POLL_TIME_OUT} + */ + public static final int DEFAULT_POLL_TIME_OUT = 3000; // milli seconds + + /** + * LogWriterProvider configuration parameter: If the parameter is present the + * RepositoryService defined by the specified + * RepositoryConfig will be wrapped by calling + * {@link SpiLoggerFactory#create(org.apache.jackrabbit.spi.RepositoryService, org.apache.jackrabbit.spi.commons.logging.LogWriterProvider) } + * if the parameter value is an instance of LogWriterProvider or + * {@link SpiLoggerFactory#create(org.apache.jackrabbit.spi.RepositoryService)} + * otherwise. + * + * @see SpiLoggerFactory#create(org.apache.jackrabbit.spi.RepositoryService) + * @see SpiLoggerFactory#create(org.apache.jackrabbit.spi.RepositoryService, org.apache.jackrabbit.spi.commons.logging.LogWriterProvider) + */ + public static final String PARAM_LOG_WRITER_PROVIDER = "org.apache.jackrabbit.spi.commons.logging.LogWriterProvider"; + + /** + *

        Creates a SPI based Repository instance based on the + * parameters passed.

        + * + *

        If the {@link #PARAM_REPOSITORY_SERVICE_FACTORY} parameter is set, + * the specified {@link RepositoryServiceFactory} is used to create the + * {@link RepositoryService} instance. All parameters are passed to + * {@link RepositoryServiceFactory#createRepositoryService(Map)}.

        + * + *

        If the {@link #PARAM_REPOSITORY_CONFIG} parameter is set, the + * specified {@link RepositoryConfig} instance is used to create the + * repository.

        + * + *

        If both parameters are set, the latter takes precedence and the + * former is ignores.

        + * + *

        The known SPI implementations and its RepositoryServiceFactorys are: + *

          + *
        • SPI2DAVex (see jackrabbit-spi2dav module): Spi2davRepositoryServiceFactory
        • + *
        • SPI2DAV (see jackrabbit-spi2dav module): Spi2davexRepositoryServiceFactory
        • + *
        • SPI2JCR (see jackrabbit-spi2jcr module) Spi2jcrRepositoryServiceFactory
        • + *
        + *

        + * NOTE: If the parameters map contains an + * {@link #PARAM_LOG_WRITER_PROVIDER} entry the + * {@link org.apache.jackrabbit.spi.RepositoryService RepositoryService} obtained + * from the configuration is wrapped by a SPI logger. See the + * {@link org.apache.jackrabbit.spi.commons.logging.SpiLoggerFactory SpiLoggerFactory} + * for details. + * + * @see RepositoryFactory#getRepository(java.util.Map) + */ + public Repository getRepository(@SuppressWarnings("unchecked") Map parameters) throws RepositoryException { + RepositoryServiceFactory serviceFactory = getServiceFactory(parameters); + Object configParam = parameters.get(PARAM_REPOSITORY_CONFIG); + + if (serviceFactory == null && configParam == null) { + return null; + } + + RepositoryConfig config; + if (configParam instanceof RepositoryConfig) { + config = (RepositoryConfig) configParam; + if (serviceFactory != null) { + log.warn("Ignoring {} since {} was specified", PARAM_REPOSITORY_SERVICE_FACTORY, + PARAM_REPOSITORY_CONFIG); + } + } else { + if (serviceFactory == null) { + return null; + } else { + config = new RepositoryConfigImpl(serviceFactory, parameters); + } + } + + config = SpiLoggerConfig.wrap(config, parameters); + return RepositoryImpl.create(config); + } + + // -----------------------------------------------------< private >--- + + private static RepositoryServiceFactory getServiceFactory(Map parameters) + throws RepositoryException { + + Object serviceFactoryParam = parameters.get(PARAM_REPOSITORY_SERVICE_FACTORY); + if (serviceFactoryParam == null) { + return null; + } + + log.debug("Acquiring RepositoryServiceFactory from {}", PARAM_REPOSITORY_SERVICE_FACTORY); + + if (serviceFactoryParam instanceof RepositoryServiceFactory) { + log.debug("Found RepositoryServiceFactory {}", serviceFactoryParam); + return (RepositoryServiceFactory) serviceFactoryParam; + } else if (serviceFactoryParam instanceof String) { + String serviceFactoryName = (String) serviceFactoryParam; + log.debug("Found RepositoryServiceFactory class name {}", serviceFactoryName); + try { + Class serviceFactoryClass; + try { + serviceFactoryClass = Class.forName(serviceFactoryName, true, + Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + // Backup for OSGi + serviceFactoryClass = Class.forName(serviceFactoryName); + } + + Object serviceFactory = serviceFactoryClass.newInstance(); + + if (serviceFactory instanceof RepositoryServiceFactory) { + log.debug("Found RepositoryServiceFactory {}", serviceFactory); + return (RepositoryServiceFactory) serviceFactory; + } else { + String msg = "Error acquiring RepositoryServiceFactory " + serviceFactoryParam; + log.error(msg); + throw new RepositoryException(msg); + } + } catch (Exception e) { + String msg = "Error acquiring RepositoryServiceFactory"; + log.error(msg, e); + throw new RepositoryException(msg, e); + } + } else { + String msg = "Error acquiring RepositoryServiceFactory from " + serviceFactoryParam; + log.error(msg); + throw new RepositoryException(msg); + } + } + + public static class RepositoryConfigImpl implements RepositoryConfig { + private final RepositoryServiceFactory serviceFactory; + private final CacheBehaviour cacheBehaviour; + private final int itemCacheSize; + private final int pollTimeOut; + private final Map parameters; + private RepositoryService repositoryService; + + public RepositoryConfigImpl(RepositoryServiceFactory serviceFactory, Map parameters) + throws RepositoryException { + + super(); + this.serviceFactory = serviceFactory; + this.cacheBehaviour = getCacheBehaviour(parameters); + this.itemCacheSize = getItemCacheSize(parameters); + this.pollTimeOut = getPollTimeout(parameters); + this.parameters = parameters; + } + + public CacheBehaviour getCacheBehaviour() { + return cacheBehaviour; + } + + public int getItemCacheSize() { + return itemCacheSize; + } + + public int getPollTimeout() { + return pollTimeOut; + } + + @Override + public T getConfiguration(String name, T defaultValue) { + if (parameters.containsKey(name)) { + Object value = parameters.get(name); + Class clazz = (defaultValue == null) + ? value.getClass() + : defaultValue.getClass(); + if (clazz.isAssignableFrom(value.getClass())) { + return (T) value; + } + } + return defaultValue; + } + + public RepositoryService getRepositoryService() throws RepositoryException { + if (repositoryService == null) { + repositoryService = serviceFactory.createRepositoryService(parameters); + } + return repositoryService; + } + + // -----------------------------------------------------< private >--- + + private static CacheBehaviour getCacheBehaviour(Map parameters) throws RepositoryException { + Object paramCacheBehaviour = parameters.get(PARAM_CACHE_BEHAVIOR); + log.debug("Setting CacheBehaviour from {}", PARAM_CACHE_BEHAVIOR); + + if (paramCacheBehaviour == null) { + log.debug("{} not set, defaulting to {}", PARAM_CACHE_BEHAVIOR, DEFAULT_CACHE_BEHAVIOR); + return DEFAULT_CACHE_BEHAVIOR; + } else if (paramCacheBehaviour instanceof CacheBehaviour) { + log.debug("Setting CacheBehaviour to {}", paramCacheBehaviour); + return (CacheBehaviour) paramCacheBehaviour; + } else if (paramCacheBehaviour instanceof String) { + String cacheBehaviour = (String) paramCacheBehaviour; + if ("invalidate".equals(cacheBehaviour)) { + log.debug("Setting CacheBehaviour to {}", CacheBehaviour.INVALIDATE); + return CacheBehaviour.INVALIDATE; + } else if ("observation".equals(cacheBehaviour)) { + log.debug("Setting CacheBehaviour to {}", CacheBehaviour.OBSERVATION); + return CacheBehaviour.OBSERVATION; + } else { + log.error("Invalid valid for CacheBehaviour: {} {}", PARAM_CACHE_BEHAVIOR, cacheBehaviour); + throw new RepositoryException("Invalid value for CacheBehaviour: " + cacheBehaviour); + } + } else { + String msg = "Invalid value for CacheBehaviour: " + paramCacheBehaviour; + log.error(msg); + throw new RepositoryException(msg); + } + } + + private static int getItemCacheSize(Map parameters) throws RepositoryException { + Object paramItemCacheSize = parameters.get(PARAM_ITEM_CACHE_SIZE); + log.debug("Setting ItemCacheSize from {}", PARAM_ITEM_CACHE_SIZE); + + if (paramItemCacheSize == null) { + log.debug("{} not set, defaulting to {}", PARAM_ITEM_CACHE_SIZE, DEFAULT_ITEM_CACHE_SIZE); + return DEFAULT_ITEM_CACHE_SIZE; + } else if (paramItemCacheSize instanceof Integer) { + log.debug("Setting ItemCacheSize to {}", paramItemCacheSize); + return (Integer) paramItemCacheSize; + } else if (paramItemCacheSize instanceof String) { + try { + log.debug("Setting ItemCacheSize to {}", paramItemCacheSize); + return Integer.parseInt((String) paramItemCacheSize); + } catch (NumberFormatException e) { + String msg = "Invalid value for ItemCacheSize: " + paramItemCacheSize; + log.error(msg); + throw new RepositoryException(msg, e); + } + } else { + String msg = "Invalid value for ItemCacheSize: " + paramItemCacheSize; + log.error(msg); + throw new RepositoryException(msg); + } + } + + private static int getPollTimeout(Map parameters) throws RepositoryException { + Object paramPollTimeOut = parameters.get(PARAM_POLL_TIME_OUT); + log.debug("Setting PollTimeout from {}", PARAM_POLL_TIME_OUT); + + if (paramPollTimeOut == null) { + log.debug("{} not set, defaulting to {}", PARAM_POLL_TIME_OUT, DEFAULT_POLL_TIME_OUT); + return DEFAULT_POLL_TIME_OUT; + } else if (paramPollTimeOut instanceof Integer) { + log.debug("Setting PollTimeout to {}", paramPollTimeOut); + return (Integer) paramPollTimeOut; + } else if (paramPollTimeOut instanceof String) { + try { + log.debug("Setting PollTimeout to {}", paramPollTimeOut); + return Integer.parseInt((String) paramPollTimeOut); + } catch (NumberFormatException e) { + String msg = "Invalid value for PollTimeout: " + paramPollTimeOut; + log.error(msg); + throw new RepositoryException(msg, e); + } + } else { + String msg = "Invalid value for PollTimeout: " + paramPollTimeOut; + log.error(msg); + throw new RepositoryException(msg); + } + } + + } + + private static class SpiLoggerConfig implements RepositoryConfig { + private final RepositoryConfig config; + private final RepositoryService service; + + private SpiLoggerConfig(RepositoryConfig config, Map parameters) throws RepositoryException { + super(); + this.config = config; + + Object lwProvider = parameters.get(PARAM_LOG_WRITER_PROVIDER); + if (lwProvider instanceof LogWriterProvider) { + service = SpiLoggerFactory.create(config.getRepositoryService(), (LogWriterProvider) lwProvider); + } else { + service = SpiLoggerFactory.create(config.getRepositoryService()); + } + } + + public static RepositoryConfig wrap(RepositoryConfig config, Map parameters) + throws RepositoryException { + + if (config == null || parameters == null || !parameters.containsKey(PARAM_LOG_WRITER_PROVIDER)) { + return config; + } else { + return new SpiLoggerConfig(config, parameters); + } + } + + public CacheBehaviour getCacheBehaviour() { + return config.getCacheBehaviour(); + } + + public int getItemCacheSize() { + return config.getItemCacheSize(); + } + + public int getPollTimeout() { + return config.getPollTimeout(); + } + + @Override + public T getConfiguration(String name, T defaultValue) { + return config.getConfiguration(name, defaultValue); + } + + public RepositoryService getRepositoryService() throws RepositoryException { + return service; + } + + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/JcrLockManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/JcrLockManager.java new file mode 100644 index 00000000000..c387777ef6a --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/JcrLockManager.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.jcr2spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.jcr2spi.lock.LockStateManager; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; + +import javax.jcr.lock.LockManager; +import javax.jcr.lock.LockException; +import javax.jcr.RepositoryException; +import javax.jcr.Node; + +/** + * JcrLockManager... + */ +public class JcrLockManager implements LockManager { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(JcrLockManager.class); + + private final LockStateManager lockStateMgr; + private final ItemManager itemManager; + private final PathResolver resolver; + + protected JcrLockManager(SessionImpl session) { + lockStateMgr = session.getLockStateManager(); + itemManager = session.getItemManager(); + resolver = session.getPathResolver(); + } + + //--------------------------------------------------------< LockManager >--- + /** + * @see javax.jcr.lock.LockManager#getLock(String) + */ + public javax.jcr.lock.Lock getLock(String absPath) throws LockException, RepositoryException { + Node n = itemManager.getNode(resolver.getQPath(absPath)); + return n.getLock(); + } + + /** + * @see javax.jcr.lock.LockManager#isLocked(String) + */ + public boolean isLocked(String absPath) throws RepositoryException { + Node n = itemManager.getNode(resolver.getQPath(absPath)); + return n.isLocked(); + } + + /** + * @see javax.jcr.lock.LockManager#holdsLock(String) + */ + public boolean holdsLock(String absPath) throws RepositoryException { + Node n = itemManager.getNode(resolver.getQPath(absPath)); + return n.holdsLock(); + } + + /** + * @see javax.jcr.lock.LockManager#lock(String, boolean, boolean, long, String) + */ + public javax.jcr.lock.Lock lock(String absPath, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerInfo) throws RepositoryException { + Node n = itemManager.getNode(resolver.getQPath(absPath)); + return ((NodeImpl) n).lock(isDeep, isSessionScoped, timeoutHint, ownerInfo); + } + + /** + * @see javax.jcr.lock.LockManager#unlock(String) + */ + public void unlock(String absPath) throws LockException, RepositoryException { + Node n = itemManager.getNode(resolver.getQPath(absPath)); + n.unlock(); + } + + /** + * Returns the lock tokens present on the SessionInfo this + * manager has been created with. + * + * @see javax.jcr.lock.LockManager#getLockTokens() + */ + public String[] getLockTokens() throws RepositoryException { + return lockStateMgr.getLockTokens(); + } + + /** + * Delegates this call to {@link WorkspaceManager#addLockToken(String)}. + * If this succeeds this method will inform all locks stored in the local + * map in order to give them the chance to update their lock information. + * + * @see javax.jcr.lock.LockManager#addLockToken(String) + */ + public void addLockToken(String lt) throws LockException, RepositoryException { + lockStateMgr.addLockToken(lt); + } + + /** + * If the lock addressed by the token is session-scoped, this method will + * throw a LockException, such as defined by JSR170 v.1.0.1 for + * {@link javax.jcr.Session#removeLockToken(String)}.
        Otherwise the call is + * delegated to {@link WorkspaceManager#removeLockToken(String)}. + * All locks stored in the local lock map are notified by the removed + * token in order have them updated their lock information. + * + * @see javax.jcr.lock.LockManager#removeLockToken(String) + */ + public void removeLockToken(String lt) throws LockException, RepositoryException { + lockStateMgr.removeLockToken(lt); + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/JcrVersionManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/JcrVersionManager.java new file mode 100644 index 00000000000..6b8e3db260c --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/JcrVersionManager.java @@ -0,0 +1,358 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.util.Iterator; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.MergeException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; + +import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * VersionManagerImpl... + */ +public class JcrVersionManager implements javax.jcr.version.VersionManager { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(JcrVersionManager.class); + + private final VersionManager vMgr; + private final SessionImpl session; + private final ItemManager itemManager; + private final PathResolver resolver; + + /** + * The ID of the activity currently in effect for the session this + * manager has been created for. + */ + private NodeId activityId; + + protected JcrVersionManager(SessionImpl session) { + this.session = session; + vMgr = session.getVersionStateManager(); + itemManager = session.getItemManager(); + resolver = session.getPathResolver(); + } + + //-----------------------------------------------------< VersionManager >--- + /** + * @see javax.jcr.version.VersionManager#checkin(String) + */ + public Version checkin(String absPath) throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + return n.checkin(); + } + + /** + * @see javax.jcr.version.VersionManager#checkout(String) + */ + public void checkout(String absPath) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + n.checkout(); + } + + /** + * @see javax.jcr.version.VersionManager#checkpoint(String) + */ + public Version checkpoint(String absPath) throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException { + session.checkIsAlive(); + + NodeImpl n = (NodeImpl) itemManager.getNode(resolver.getQPath(absPath)); + return n.checkpoint(); + } + + /** + * @see javax.jcr.version.VersionManager#isCheckedOut(String) + */ + public boolean isCheckedOut(String absPath) throws RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + return n.isCheckedOut(); + } + + /** + * @see javax.jcr.version.VersionManager#getVersionHistory(String) + */ + public VersionHistory getVersionHistory(String absPath) throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + return n.getVersionHistory(); + } + + /** + * @see javax.jcr.version.VersionManager#getBaseVersion(String) + */ + public Version getBaseVersion(String absPath) throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + return n.getBaseVersion(); + } + + /** + * @see javax.jcr.version.VersionManager#restore(Version[], boolean) + */ + public void restore(Version[] versions, boolean removeExisting) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException { + session.checkIsAlive(); + session.checkHasPendingChanges(); + + NodeState[] versionStates = new NodeState[versions.length]; + for (int i = 0; i < versions.length; i++) { + versionStates[i] = session.getVersionState(versions[i]); + } + vMgr.restore(versionStates, removeExisting); + } + + /** + * @see javax.jcr.version.VersionManager#restore(String, String, boolean) + */ + public void restore(String absPath, String versionName, boolean removeExisting) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + n.restore(versionName, removeExisting); + } + + /** + * @see javax.jcr.version.VersionManager#restore(Version, boolean) + */ + public void restore(Version version, boolean removeExisting) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, RepositoryException { + restore(new Version[]{version}, removeExisting); + } + + /** + * @see javax.jcr.version.VersionManager#restore(String, Version, boolean) + */ + public void restore(String absPath, Version version, boolean removeExisting) throws PathNotFoundException, ItemExistsException, VersionException, ConstraintViolationException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + session.checkIsAlive(); + // get parent + int idx = absPath.lastIndexOf('/'); + String parent = idx == 0 ? "/" : absPath.substring(0, idx); + String name = absPath.substring(idx + 1); + Node n = itemManager.getNode(resolver.getQPath(parent)); + n.restore(version, name, removeExisting); + } + + /** + * @see javax.jcr.version.VersionManager#restoreByLabel(String, String, boolean) + */ + public void restoreByLabel(String absPath, String versionLabel, boolean removeExisting) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + n.restoreByLabel(versionLabel, removeExisting); + } + + /** + * @see javax.jcr.version.VersionManager#merge(String, String, boolean) + */ + public NodeIterator merge(String absPath, String srcWorkspace, boolean bestEffort) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + return merge(absPath, srcWorkspace, bestEffort, false); + } + + /** + * @see javax.jcr.version.VersionManager#merge(String, String, boolean, boolean) + */ + public NodeIterator merge(String absPath, String srcWorkspace, boolean bestEffort, boolean isShallow) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + session.checkIsAlive(); + + NodeImpl n = (NodeImpl) itemManager.getNode(resolver.getQPath(absPath)); + n.checkIsWritable(); + session.checkHasPendingChanges(); + + // if same workspace, ignore + if (session.getWorkspace().getName().equals(srcWorkspace)) { + return NodeIteratorAdapter.EMPTY; + } + // make sure the workspace exists and is accessible for this session. + session.checkAccessibleWorkspace(srcWorkspace); + + Iterator failedIds = session.getVersionStateManager().merge((NodeState) n.getItemState(), srcWorkspace, bestEffort, isShallow); + return new LazyItemIterator(itemManager, session.getHierarchyManager(), failedIds); + } + + /** + * @see javax.jcr.version.VersionManager#doneMerge(String, Version) + */ + public void doneMerge(String absPath, Version version) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + n.doneMerge(version); + } + + /** + * @see javax.jcr.version.VersionManager#cancelMerge(String, Version) + */ + public void cancelMerge(String absPath, Version version) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + + Node n = itemManager.getNode(resolver.getQPath(absPath)); + n.cancelMerge(version); + } + + /** + * @see javax.jcr.version.VersionManager#createConfiguration(String) + */ + public Node createConfiguration(String absPath) throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + + NodeImpl n = (NodeImpl) itemManager.getNode(resolver.getQPath(absPath)); + NodeEntry entry = vMgr.createConfiguration((NodeState) n.getItemState()); + return (Node) itemManager.getItem(entry); + } + + /** + * @see javax.jcr.version.VersionManager#setActivity(Node) + */ + public Node setActivity(Node activity) throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + session.checkSupportedOption(Repository.OPTION_ACTIVITIES_SUPPORTED); + + + Node oldActivity = getActivity(); + if (activity == null) { + activityId = null; + } else { + NodeImpl activityNode = getValidActivity(activity, "set"); + activityId = (NodeId) activityNode.getItemState().getId(); + } + return oldActivity; + } + + /** + * @see javax.jcr.version.VersionManager#getActivity() + */ + public Node getActivity() throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + session.checkSupportedOption(Repository.OPTION_ACTIVITIES_SUPPORTED); + + if (activityId == null) { + return null; + } else { + try { + return (Node) itemManager.getItem(session.getHierarchyManager().getNodeEntry(activityId)); + } catch (ItemNotFoundException e) { + // the activity doesn't exist any more. + log.warn("Activity node with id " + activityId + " doesn't exist any more."); + activityId = null; + return null; + } + } + } + + /** + * @see javax.jcr.version.VersionManager#createActivity(String) + */ + public Node createActivity(String title) throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + + NodeEntry entry = vMgr.createActivity(title); + return (Node) itemManager.getItem(entry); + } + + /** + * @see javax.jcr.version.VersionManager#removeActivity(Node) + */ + public void removeActivity(Node activityNode) throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkIsAlive(); + NodeImpl activity = getValidActivity(activityNode, "remove"); + + NodeState nState = (NodeState) activity.getItemState(); + ItemId removeId = nState.getId(); + vMgr.removeActivity(nState); + + // if the removal succeeded, make sure there is no current activity + // setting on this session, that points to the removed activity. + if (activityId != null && activityId.equals(removeId)) { + activityId = null; + } + } + + /** + * @see javax.jcr.version.VersionManager#merge(Node) + */ + public NodeIterator merge(Node activityNode) throws VersionException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + session.checkIsAlive(); + NodeImpl activity = getValidActivity(activityNode, "merge"); + Iterator failedIds = vMgr.mergeActivity((NodeState) activity.getItemState()); + return new LazyItemIterator(itemManager, session.getHierarchyManager(), failedIds); + } + + /** + * Assert that activity nodes passes to any of the activity methods have + * been obtained from the session this version manager has been created for. + * This is particularly important for workspace operations that are followed + * by internal updated of modified items: The hierarchy entries invalidated + * after successful completion of the operation must reside within scope + * defined by this session. + *
        + * In addition this method verifies that the passed node is of type nt:activity. + * + * @param activityNode + * @param methodName + * @return + * @throws RepositoryException + */ + private NodeImpl getValidActivity(Node activityNode, String methodName) throws UnsupportedRepositoryOperationException, RepositoryException { + NodeImpl activity; + if (session != activityNode.getSession()) { + String msg = "Attempt to " +methodName+ " an activity node that has been retrieved by another session."; + log.warn(msg); + activity = (NodeImpl) session.getNodeByIdentifier(activityNode.getIdentifier()); + } else { + activity = (NodeImpl) activityNode; + } + if (!activity.isNodeType(NameConstants.NT_ACTIVITY)) { + throw new UnsupportedRepositoryOperationException("Given node is not an activity."); + } + return activity; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/LazyItemIterator.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/LazyItemIterator.java new file mode 100644 index 00000000000..21878f1f5a1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/LazyItemIterator.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.jcr2spi; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RangeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionIterator; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * LazyItemIterator is an id-based iterator that instantiates + * the Items only when they are requested. + *

        + * Important: Items that appear to be nonexistent + * for some reason (e.g. because of insufficient access rights or because they + * have been removed since the iterator has been retrieved) are silently + * skipped. As a result the size of the iterator as reported by + * {@link #getSize()} always returns -1. + */ +public class LazyItemIterator implements NodeIterator, PropertyIterator, VersionIterator { + + /** Logger instance for this class */ + private static Logger log = LoggerFactory.getLogger(LazyItemIterator.class); + + private static final long UNDEFINED_SIZE = -1; + + /** the item manager that is used to lazily fetch the items */ + private final ItemManager itemMgr; + + /** Iterator over HierarchyEntry elements */ + private final Iterator iter; + + /** + * The number of items. + * Note, that the size may change over the time due to the lazy behaviour + * of this iterator that may only upon iteration found out, that a + * hierarchy entry has been invalidated or removed in the mean time. + */ + private long size; + /** the position of the next item */ + private int pos; + + /** prefetched item to be returned on {@link #next()} */ + private Item next; + + /** + * Creates a new LazyItemIterator instance. + * + * @param itemMgr item manager + * @param hierarchyEntryIterator Iterator over HierarchyEntries + */ + public LazyItemIterator(ItemManager itemMgr, Iterator hierarchyEntryIterator) { + this.itemMgr = itemMgr; + this.iter = hierarchyEntryIterator; + if (hierarchyEntryIterator instanceof RangeIterator) { + size = ((RangeIterator) hierarchyEntryIterator).getSize(); + } else { + size = UNDEFINED_SIZE; + } + pos = 0; + // fetch first item + next = prefetchNext(); + } + + /** + * Creates a new LazyItemIterator instance. + * + * @param itemMgr + * @param hierarchyMgr + * @param itemIds + */ + public LazyItemIterator(ItemManager itemMgr, HierarchyManager hierarchyMgr, + Iterator itemIds) + throws ItemNotFoundException, RepositoryException { + this.itemMgr = itemMgr; + List entries = new ArrayList(); + while (itemIds.hasNext()) { + ItemId id = itemIds.next(); + HierarchyEntry entry; + if (id.denotesNode()) { + entry = hierarchyMgr.getNodeEntry((NodeId) id); + } else { + entry = hierarchyMgr.getPropertyEntry((PropertyId) id); + } + entries.add(entry); + } + iter = entries.iterator(); + size = entries.size(); + pos = 0; + // fetch first item + next = prefetchNext(); + } + + /** + * Prefetches next item. + *

        + * {@link #next} is set to the next available item in this iterator or to + * null in case there are no more items. + */ + private Item prefetchNext() { + Item nextItem = null; + while (nextItem == null && iter.hasNext()) { + HierarchyEntry entry = iter.next(); + try { + nextItem = itemMgr.getItem(entry); + } catch (RepositoryException e) { + log.warn("Failed to fetch item " + entry.getName() + ", skipping.", e.getMessage()); + // reduce the size... and try the next one + size--; + } + } + return nextItem; + } + + //-------------------------------------------------------< NodeIterator >--- + /** + * {@inheritDoc} + * @see NodeIterator#nextNode() + */ + public Node nextNode() { + return (Node) next(); + } + + //---------------------------------------------------< PropertyIterator >--- + /** + * {@inheritDoc} + * @see PropertyIterator#nextProperty() + */ + public Property nextProperty() { + return (Property) next(); + } + + //----------------------------------------------------< VersionIterator >--- + /** + * {@inheritDoc} + * @see VersionIterator#nextVersion() + */ + public Version nextVersion() { + return (Version) next(); + } + + //------------------------------------------------------< RangeIterator >--- + /** + * {@inheritDoc} + * @see javax.jcr.RangeIterator#getPosition() + */ + public long getPosition() { + return pos; + } + + /** + * Returns the number of Items in this iterator or -1 if the + * size is unknown. + *

        + * Note: The number returned by this method may differ from the number + * of Items actually returned by calls to hasNext() / getNextNode(). + * This is caused by the lazy instantiation behaviour of this iterator, + * that may detect only upon iteration that an Item has been invalidated + * or removed in the mean time. As soon as an invalid Item is + * detected, the size of this iterator is adjusted. + * + * @return the number of Items in this iterator. + * @see RangeIterator#getSize() + */ + public long getSize() { + return size; + } + + /** + * {@inheritDoc} + * @see RangeIterator#skip(long) + */ + public void skip(long skipNum) { + if (skipNum < 0) { + throw new IllegalArgumentException("skipNum must not be negative"); + } + if (skipNum == 0) { + return; + } + if (next == null) { + throw new NoSuchElementException(); + } + + // skip the first (skipNum - 1) items without actually retrieving them + while (--skipNum > 0) { + pos++; + HierarchyEntry entry = iter.next(); + // check if item exists but don't build Item instance. + boolean itemExists = false; + while(!itemExists){ + try{ + itemExists = itemMgr.itemExists(entry); + }catch(RepositoryException e){ + log.warn("Failed to check that item {} exists",entry,e); + } + if(!itemExists){ + log.debug("Ignoring nonexistent item {}", entry); + entry = iter.next(); + } + } + } + // fetch final item (the one to be returned on next()) + pos++; + next = prefetchNext(); + } + + //-----------------------------------------------------------< Iterator >--- + /** + * {@inheritDoc} + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() { + return next != null; + } + + /** + * {@inheritDoc} + * @see Iterator#next() + */ + public Object next() { + if (next == null) { + throw new NoSuchElementException(); + } + Item item = next; + pos++; + next = prefetchNext(); + return item; + } + + /** + * {@inheritDoc} + * @see Iterator#remove() + * + * @throws UnsupportedOperationException always since removal is not implemented. + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ManagerProvider.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ManagerProvider.java new file mode 100644 index 00000000000..c81480eb3d5 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/ManagerProvider.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.jcr2spi; + +import org.apache.jackrabbit.jcr2spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.security.AccessManager; +import org.apache.jackrabbit.jcr2spi.lock.LockStateManager; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeDefinitionProvider; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; + +import javax.jcr.Session; +import javax.jcr.ValueFactory; +import javax.jcr.RepositoryException; + +/** + * ManagerProvider... + */ +public interface ManagerProvider { + + public org.apache.jackrabbit.spi.commons.conversion.NamePathResolver getNamePathResolver(); + + public NameResolver getNameResolver(); + + public org.apache.jackrabbit.spi.commons.conversion.PathResolver getPathResolver(); + + public NamespaceResolver getNamespaceResolver(); + + public HierarchyManager getHierarchyManager(); + + public AccessManager getAccessManager(); + + /** + * Returns the LockStateManager associated with this + * ManagerProvider. + * + * @return the LockStateManager associated with this + * ManagerProvider + */ + public LockStateManager getLockStateManager(); + + /** + * Returns the VersionManager associated with this + * ManagerProvider. + * + * @return the VersionManager associated with this + * ManagerProvider + */ + public VersionManager getVersionStateManager(); + + public ItemDefinitionProvider getItemDefinitionProvider(); + + public NodeTypeDefinitionProvider getNodeTypeDefinitionProvider(); + + public EffectiveNodeTypeProvider getEffectiveNodeTypeProvider(); + + /** + * Same as {@link Session#getValueFactory()} but omits the check, if this repository + * is really level 2 compliant. Therefore, this method may be used for + * internal functionality only, that require creation and conversion of + * JCR values. + * + * @return + * @throws RepositoryException + */ + public ValueFactory getJcrValueFactory() throws RepositoryException; + + public QValueFactory getQValueFactory() throws RepositoryException; + + public AccessControlProvider getAccessControlProvider() throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NamespaceRegistryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NamespaceRegistryImpl.java new file mode 100644 index 00000000000..dc4996ac64b --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NamespaceRegistryImpl.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.jcr2spi; + +import java.util.Collection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.NamespaceException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; + +/** + * NamespaceRegistryImpl implements the JCR client facing + * NamespaceRegistry. + */ +public class NamespaceRegistryImpl implements NamespaceRegistry { + + private static Logger log = LoggerFactory.getLogger(NamespaceRegistryImpl.class); + + private final NamespaceStorage storage; + + /** + * Create a new NamespaceRegistryImpl. + * + * @param storage + */ + public NamespaceRegistryImpl(NamespaceStorage storage) { + this.storage = storage; + } + + //--------------------------------------------------< NamespaceRegistry >--- + + /** + * @see NamespaceRegistry#registerNamespace(String, String) + */ + public void registerNamespace(String prefix, String uri) throws NamespaceException, UnsupportedRepositoryOperationException, RepositoryException { + storage.registerNamespace(prefix, uri); + } + + /** + * @see NamespaceRegistry#unregisterNamespace(String) + */ + public void unregisterNamespace(String prefix) throws NamespaceException, UnsupportedRepositoryOperationException, RepositoryException { + storage.unregisterNamespace(getURI(prefix)); + } + + /** + * @see javax.jcr.NamespaceRegistry#getPrefixes() + */ + public String[] getPrefixes() throws RepositoryException { + Collection prefixes = storage.getRegisteredNamespaces().keySet(); + return prefixes.toArray(new String[prefixes.size()]); + } + + /** + * @see javax.jcr.NamespaceRegistry#getURIs() + */ + public String[] getURIs() throws RepositoryException { + Collection uris = storage.getRegisteredNamespaces().values(); + return uris.toArray(new String[uris.size()]); + } + + /** + * @see javax.jcr.NamespaceRegistry#getURI(String) + * @see org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver#getURI(String) + */ + public String getURI(String prefix) throws NamespaceException { + // try to load the uri + try { + return storage.getURI(prefix); + } catch (RepositoryException ex) { + log.debug("Internal error while loading registered namespaces."); + throw new NamespaceException(prefix + ": is not a registered namespace prefix."); + } + } + + /** + * @see javax.jcr.NamespaceRegistry#getPrefix(String) + * @see org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver#getPrefix(String) + */ + public String getPrefix(String uri) throws NamespaceException { + // try to load the prefix + try { + return storage.getPrefix(uri); + } catch (RepositoryException ex) { + log.debug("Internal error while loading registered namespaces."); + throw new NamespaceException(uri + ": is not a registered namespace uri."); + } + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NamespaceStorage.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NamespaceStorage.java new file mode 100644 index 00000000000..293b706df4b --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NamespaceStorage.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.jcr2spi; + +import java.util.Map; + +import javax.jcr.NamespaceException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; + +/** + * NamespaceStorage... + */ +public interface NamespaceStorage { + + public Map getRegisteredNamespaces() throws RepositoryException; + + public String getPrefix(String uri) throws NamespaceException, RepositoryException; + + public String getURI(String prefix) throws NamespaceException, RepositoryException; + + public void registerNamespace(String prefix, String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException; + + public void unregisterNamespace(String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException; + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NodeImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NodeImpl.java new file mode 100644 index 00000000000..e7b599863d3 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/NodeImpl.java @@ -0,0 +1,1833 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.lock.LockStateManager; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeImpl; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.jcr2spi.operation.AddNode; +import org.apache.jackrabbit.jcr2spi.operation.AddProperty; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.ReorderNodes; +import org.apache.jackrabbit.jcr2spi.operation.SetMixin; +import org.apache.jackrabbit.jcr2spi.operation.SetPrimaryType; +import org.apache.jackrabbit.jcr2spi.operation.Update; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.jcr2spi.util.StateUtility; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.util.ChildrenCollectorFilter; +import org.apache.jackrabbit.value.ValueHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Iterator; +import java.util.List; + +/** + * NodeImpl... + */ +public class NodeImpl extends ItemImpl implements Node { + + private static Logger log = LoggerFactory.getLogger(NodeImpl.class); + + protected NodeImpl(SessionImpl session, NodeState state, ItemLifeCycleListener[] listeners) { + super(session, state, listeners); + Name nodeTypeName = state.getNodeTypeName(); + // make sure the nodetype name is valid + if (!session.getNodeTypeManager().hasNodeType(nodeTypeName)) { + // should not occur. Since nodetypes are defined by the 'server' + // its not possible to determine a fallback nodetype that is + // always available. + throw new IllegalArgumentException("Unknown nodetype " + LogUtil.saveGetJCRName(nodeTypeName, session.getNameResolver())); + } + } + + //---------------------------------------------------------------< Item >--- + /** + * @see Item#getName() + */ + @Override + public String getName() throws RepositoryException { + checkStatus(); + return session.getNameResolver().getJCRName(getQName()); + } + + /** + * Implementation of {@link Item#accept(javax.jcr.ItemVisitor)} for nodes. + * + * @param visitor + * @throws RepositoryException + * @see Item#accept(javax.jcr.ItemVisitor) + */ + @Override + public void accept(ItemVisitor visitor) throws RepositoryException { + checkStatus(); + visitor.visit(this); + } + + /** + * Returns true + * + * @return true + * @see Item#isNode() + */ + @Override + public boolean isNode() { + return true; + } + + //---------------------------------------------------------------< Node >--- + /** + * @see Node#addNode(String) + */ + public Node addNode(String relPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException { + // validation performed in subsequent method + return addNode(relPath, null); + } + + /** + * @see Node#addNode(String, String) + */ + public Node addNode(String relPath, String primaryNodeTypeName) throws ItemExistsException, PathNotFoundException, NoSuchNodeTypeException, LockException, VersionException, ConstraintViolationException, RepositoryException { + checkIsWritable(); + // build path object and retrieve parent node + Path nodePath = getPath(relPath).getNormalizedPath(); + if (nodePath.getIndex() != Path.INDEX_UNDEFINED) { + String msg = "Illegal subscript specified: " + relPath; + log.debug(msg); + throw new RepositoryException(msg); + } + + NodeImpl parentNode; + if (nodePath.getLength() == 1) { + parentNode = this; + } else { + Path parentPath = nodePath.getAncestor(1); + ItemManager itemMgr = getItemManager(); + if (itemMgr.nodeExists(parentPath)) { + parentNode = (NodeImpl) itemMgr.getNode(parentPath); + } else if (itemMgr.propertyExists(parentPath)) { + String msg = "Cannot add a node to property " + LogUtil.safeGetJCRPath(parentPath, session.getPathResolver()); + log.debug(msg); + throw new ConstraintViolationException(msg); + } else { + throw new PathNotFoundException("Cannot add a new node to a non-existing parent at " + LogUtil.safeGetJCRPath(parentPath, session.getPathResolver())); + } + } + + // get names objects for node and nt + Name nodeName = nodePath.getName(); + Name ntName = (primaryNodeTypeName == null) ? null : getQName(primaryNodeTypeName); + + // create new node (including validation checks) + return parentNode.createNode(nodeName, ntName); + } + + /** + * @see Node#orderBefore(String, String) + */ + public synchronized void orderBefore(String srcChildRelPath, + String destChildRelPath) + throws UnsupportedRepositoryOperationException, VersionException, ConstraintViolationException, ItemNotFoundException, LockException, RepositoryException { + checkIsWritable(); + + if (!getPrimaryNodeType().hasOrderableChildNodes()) { + throw new UnsupportedRepositoryOperationException("Child node ordering not supported on node " + safeGetJCRPath()); + } + // check arguments + if (srcChildRelPath.equals(destChildRelPath)) { + // there's nothing to do + return; + } + // check existence + if (!hasNode(srcChildRelPath)) { + throw new ItemNotFoundException("Node " + safeGetJCRPath() + " has no child node with name " + srcChildRelPath); + } + if (destChildRelPath != null && !hasNode(destChildRelPath)) { + throw new ItemNotFoundException("Node " + safeGetJCRPath() + " has no child node with name " + destChildRelPath); + } + + Path srcPath = getReorderPath(srcChildRelPath); + Path beforePath = null; + if (destChildRelPath != null) { + beforePath = getReorderPath(destChildRelPath); + } + + Operation op = ReorderNodes.create(getNodeState(), srcPath, beforePath); + session.getSessionItemStateManager().execute(op); + } + + /** + * @see Node#setProperty(String, Value) + */ + public Property setProperty(String name, Value value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + int type = PropertyType.UNDEFINED; + if (value != null) { + type = value.getType(); + } + return setProperty(name, value, type); + } + + /** + * @see Node#setProperty(String, javax.jcr.Value, int) + */ + public Property setProperty(String name, Value value, int type) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + checkIsWritable(); + Name propName = getQName(name); + Property prop; + if (hasProperty(propName)) { + // property already exists: pass call to property + prop = getProperty(propName); + Value v = (type == PropertyType.UNDEFINED) ? value : ValueHelper.convert(value, type, session.getValueFactory()); + prop.setValue(v); + } else { + if (value == null) { + return new StaleProperty(); + } else { + // new property to be added + prop = createProperty(propName, value, type); + } + } + return prop; + } + + /** + * @see Node#setProperty(String, Value[]) + */ + public Property setProperty(String name, Value[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + int type; + if (values == null || values.length == 0 || values[0] == null) { + type = PropertyType.UNDEFINED; + } else { + type = values[0].getType(); + } + return setProperty(name, values, type); + } + + /** + * @see Node#setProperty(String, Value[], int) + */ + public Property setProperty(String name, Value[] values, int type) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + checkIsWritable(); + Name propName = getQName(name); + Property prop; + if (hasProperty(propName)) { + // property already exists: pass call to property + prop = getProperty(propName); + Value[] vs = (type == PropertyType.UNDEFINED) ? values : ValueHelper.convert(values, type, session.getValueFactory()); + prop.setValue(vs); + } else { + if (values == null) { + // create and remove property is a nop. + throw new ItemNotFoundException("Cannot remove a non-existing property."); + } else { + // new property to be added + prop = createProperty(propName, values, type); + } + } + return prop; + } + + /** + * @see Node#setProperty(String, String[]) + */ + public Property setProperty(String name, String[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + return setProperty(name, values, PropertyType.UNDEFINED); + } + + /** + * @see Node#setProperty(String, String[], int) + */ + public Property setProperty(String name, String[] values, int type) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + Value[] vs; + if (type == PropertyType.UNDEFINED) { + vs = ValueHelper.convert(values, PropertyType.STRING, session.getValueFactory()); + } else { + vs = ValueHelper.convert(values, type, session.getValueFactory()); + } + return setProperty(name, vs, type); + } + + /** + * @see Node#setProperty(String, String) + */ + public Property setProperty(String name, String value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + Value v = (value == null) ? null : session.getValueFactory().createValue(value, PropertyType.STRING); + return setProperty(name, v, PropertyType.UNDEFINED); + } + + /** + * @see Node#setProperty(String, String, int) + */ + public Property setProperty(String name, String value, int type) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + Value v = (value == null) ? null : session.getValueFactory().createValue(value, type); + return setProperty(name, v, type); + } + + /** + * @see Node#setProperty(String, InputStream) + */ + public Property setProperty(String name, InputStream value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + Value v = (value == null ? null : session.getValueFactory().createValue(value)); + return setProperty(name, v, PropertyType.BINARY); + } + + /** + * @see javax.jcr.Node#setProperty(String, Binary) + */ + public Property setProperty(String name, Binary value) throws RepositoryException { + // validation performed in subsequent method + Value v = (value == null ? null : session.getValueFactory().createValue(value)); + return setProperty(name, v, PropertyType.BINARY); + } + + /** + * @see Node#setProperty(String, boolean) + */ + public Property setProperty(String name, boolean value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + return setProperty(name, session.getValueFactory().createValue(value), PropertyType.BOOLEAN); + } + + /** + * @see Node#setProperty(String, double) + */ + public Property setProperty(String name, double value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + return setProperty(name, session.getValueFactory().createValue(value), PropertyType.DOUBLE); + } + + /** + * @see javax.jcr.Node#setProperty(String, BigDecimal) + */ + public Property setProperty(String name, BigDecimal value) throws RepositoryException { + // validation performed in subsequent method + Value v = (value == null ? null : session.getValueFactory().createValue(value)); + return setProperty(name, v, PropertyType.DECIMAL); + } + + /** + * @see Node#setProperty(String, long) + */ + public Property setProperty(String name, long value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + return setProperty(name, session.getValueFactory().createValue(value), PropertyType.LONG); + } + + /** + * @see Node#setProperty(String, Calendar) + */ + public Property setProperty(String name, Calendar value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // validation performed in subsequent method + Value v = (value == null ? null : session.getValueFactory().createValue(value)); + return setProperty(name, v, PropertyType.DATE); + } + + /** + * @see Node#setProperty(String, Node) + */ + public Property setProperty(String name, Node value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + // duplicate check to make sure, property can be written before value + // validation below. + checkIsWritable(); + Value v; + if (value == null) { + v = null; + } else { + PropertyImpl.checkValidReference(value, PropertyType.REFERENCE, session.getNameResolver()); + v = session.getValueFactory().createValue(value); + } + return setProperty(name, v, PropertyType.REFERENCE); + } + + /** + * @see Node#getNode(String) + */ + public Node getNode(String relPath) throws PathNotFoundException, RepositoryException { + checkStatus(); + NodeEntry nodeEntry = resolveRelativeNodePath(relPath); + if (nodeEntry == null) { + throw new PathNotFoundException(relPath); + } + try { + return (Node) getItemManager().getItem(nodeEntry); + } catch (ItemNotFoundException e) { + throw new PathNotFoundException(relPath, e); + } + } + + /** + * @see Node#getNodes() + */ + public NodeIterator getNodes() throws RepositoryException { + checkStatus(); + // NOTE: Don't use a class derived from TraversingElementVisitor to traverse + // the child nodes because this would lead to an infinite recursion. + try { + return getItemManager().getChildNodes(getNodeEntry()); + } catch (ItemNotFoundException infe) { + String msg = "Failed to list the child nodes of " + safeGetJCRPath(); + log.debug(msg); + throw new RepositoryException(msg, infe); + } catch (AccessDeniedException ade) { + String msg = "Failed to list the child nodes of " + safeGetJCRPath(); + log.debug(msg); + throw new RepositoryException(msg, ade); + } + } + + /** + * @see Node#getNodes(String) + */ + public NodeIterator getNodes(String namePattern) throws RepositoryException { + checkStatus(); + + return ChildrenCollectorFilter.collectChildNodes(this, namePattern); + } + + /** + * @see javax.jcr.Node#getNodes(String[]) + */ + public NodeIterator getNodes(String[] nameGlobs) throws RepositoryException { + checkStatus(); + + return ChildrenCollectorFilter.collectChildNodes(this, nameGlobs); + } + + /** + * @see Node#getProperty(String) + */ + public Property getProperty(String relPath) throws PathNotFoundException, RepositoryException { + checkStatus(); + PropertyEntry entry = resolveRelativePropertyPath(relPath); + if (entry == null) { + throw new PathNotFoundException(relPath); + } + try { + return (Property) getItemManager().getItem(entry); + } catch (AccessDeniedException e) { + throw new PathNotFoundException(relPath); + } catch (ItemNotFoundException e) { + throw new PathNotFoundException(relPath); + } + } + + /** + * @see Node#getProperties() + */ + public PropertyIterator getProperties() throws RepositoryException { + checkStatus(); + try { + return getItemManager().getChildProperties(getNodeEntry()); + } catch (ItemNotFoundException infe) { + String msg = "Failed to list the child properties of " + getPath(); + log.debug(msg); + throw new RepositoryException(msg, infe); + } catch (AccessDeniedException ade) { + String msg = "Failed to list the child properties of " + getPath(); + log.debug(msg); + throw new RepositoryException(msg, ade); + } + } + + /** + * @see Node#getProperties(String) + */ + public PropertyIterator getProperties(String namePattern) throws RepositoryException { + checkStatus(); + + return ChildrenCollectorFilter.collectProperties(this, namePattern); + } + + /** + * @see javax.jcr.Node#getProperties(String) + */ + public PropertyIterator getProperties(String[] nameGlobs) + throws RepositoryException { + checkStatus(); + + return ChildrenCollectorFilter.collectProperties(this, nameGlobs); + } + + /** + * @see Node#getPrimaryItem() + */ + public Item getPrimaryItem() throws ItemNotFoundException, RepositoryException { + checkStatus(); + String name = getPrimaryNodeType().getPrimaryItemName(); + if (name == null) { + throw new ItemNotFoundException("No primary item present on Node " + safeGetJCRPath()); + } + 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 " + safeGetJCRPath()); + } + } + + /** + * @see Node#getUUID() + */ + public String getUUID() throws UnsupportedRepositoryOperationException, RepositoryException { + checkStatus(); + String uuid = getNodeState().getUniqueID(); + if (uuid == null || !isNodeType(NameConstants.MIX_REFERENCEABLE)) { + throw new UnsupportedRepositoryOperationException(); + } + // Node is referenceable -> NodeId must contain a UUID part + return uuid; + } + + /** + * @see Node#getIdentifier() + */ + public String getIdentifier() throws RepositoryException { + checkStatus(); + return session.getIdFactory().toJcrIdentifier(getNodeEntry().getId()); + } + + /** + * @see Node#getIndex() + */ + public int getIndex() throws RepositoryException { + checkStatus(); + int index = getNodeEntry().getIndex(); + if (index == Path.INDEX_UNDEFINED) { + throw new RepositoryException("Error while retrieving index."); + } + return index; + } + + /** + * @see Node#getReferences() + */ + public PropertyIterator getReferences() throws RepositoryException { + return getReferences(null); + } + + /** + * @see javax.jcr.Node#getReferences(String) + */ + public PropertyIterator getReferences(String name) throws RepositoryException { + return getReferences(name, false); + } + + /** + * @see javax.jcr.Node#getWeakReferences() + */ + public PropertyIterator getWeakReferences() throws RepositoryException { + return getWeakReferences(null); + } + + /** + * @see javax.jcr.Node#getWeakReferences() + */ + public PropertyIterator getWeakReferences(String name) throws RepositoryException { + return getReferences(name, true); + } + + /** + * @see Node#hasNode(String) + */ + public boolean hasNode(String relPath) throws RepositoryException { + checkStatus(); + NodeEntry nodeEntry = resolveRelativeNodePath(relPath); + return (nodeEntry != null) && getItemManager().itemExists(nodeEntry); + } + + /** + * @see Node#hasProperty(String) + */ + public boolean hasProperty(String relPath) throws RepositoryException { + checkStatus(); + PropertyEntry childEntry = resolveRelativePropertyPath(relPath); + return (childEntry != null) && getItemManager().itemExists(childEntry); + } + + /** + * Returns true, if this Node has a property with the given name. + * + * @param propertyName + * @return true, if this Node has a property with + * the given name. + */ + private boolean hasProperty(Name propertyName) { + return getNodeEntry().hasPropertyEntry(propertyName); + } + + /** + * @see Node#hasNodes() + */ + public boolean hasNodes() throws RepositoryException { + checkStatus(); + return getItemManager().hasChildNodes(getNodeEntry()); + } + + /** + * @see Node#hasProperties() + */ + public boolean hasProperties() throws RepositoryException { + checkStatus(); + return getItemManager().hasChildProperties(getNodeEntry()); + } + + /** + * @see Node#getPrimaryNodeType() + */ + public NodeType getPrimaryNodeType() throws RepositoryException { + checkStatus(); + return session.getNodeTypeManager().getNodeType(getPrimaryNodeTypeName()); + } + + /** + * @see javax.jcr.Node#setPrimaryType(String) + */ + public void setPrimaryType(String nodeTypeName) throws RepositoryException { + checkStatus(); + + if (getNodeState().isRoot()) { + String msg = "The primary type of the root node may not be changed."; + log.debug(msg); + throw new RepositoryException(msg); + } + + Name ntName = getQName(nodeTypeName); + if (ntName.equals(getPrimaryNodeTypeName())) { + log.debug("Changing the primary type has no effect: '" + nodeTypeName + "' already is the primary node type."); + return; + } + + NodeTypeManagerImpl ntMgr = session.getNodeTypeManager(); + NodeType nt = ntMgr.getNodeType(ntName); + if (nt.isMixin() || nt.isAbstract()) { + throw new ConstraintViolationException("Cannot change the primary type: '" + nodeTypeName + "' is a mixin type or abstract."); + } + + // perform the operation + Operation op = SetPrimaryType.create(getNodeState(), ntName); + session.getSessionItemStateManager().execute(op); + } + + /** + * @see Node#getMixinNodeTypes() + */ + public NodeType[] getMixinNodeTypes() throws RepositoryException { + checkStatus(); + Name[] mixinNames = getNodeState().getMixinTypeNames(); + NodeType[] nta = new NodeType[mixinNames.length]; + for (int i = 0; i < mixinNames.length; i++) { + nta[i] = session.getNodeTypeManager().getNodeType(mixinNames[i]); + } + return nta; + } + + /** + * @see Node#isNodeType(String) + */ + public boolean isNodeType(String nodeTypeName) throws RepositoryException { + checkStatus(); + // try shortcut first (avoids parsing of name) + if (session.getNameResolver().getJCRName(getPrimaryNodeTypeName()).equals(nodeTypeName)) { + return true; + } + // parse to Name and check against effective nodetype + return isNodeType(getQName(nodeTypeName)); + } + + /** + * @see Node#addMixin(String) + */ + public void addMixin(String mixinName) throws NoSuchNodeTypeException, + VersionException, ConstraintViolationException, LockException, RepositoryException { + checkIsWritable(); + Name mixinQName = getQName(mixinName); + + // get mixin types present in the jcr:mixinTypes property without + // modifying the NodeState. + List mixinValue = getMixinTypes(); + if (!mixinValue.contains(mixinQName) && !isNodeType(mixinQName)) { + if (!canAddMixin(mixinQName)) { + throw new ConstraintViolationException("Cannot add '" + mixinName + "' mixin type."); + } + + mixinValue.add(mixinQName); + // perform the operation + Operation op = SetMixin.create(getNodeState(), mixinValue.toArray(new Name[mixinValue.size()])); + session.getSessionItemStateManager().execute(op); + } + } + + /** + * @see Node#removeMixin(String) + */ + public void removeMixin(String mixinName) throws NoSuchNodeTypeException, + VersionException, ConstraintViolationException, LockException, RepositoryException { + checkIsWritable(); + Name ntName = getQName(mixinName); + List mixinValue = getMixinTypes(); + // remove name of target mixin + if (!mixinValue.remove(ntName)) { + throw new NoSuchNodeTypeException("Cannot remove mixin '" + mixinName + "': Nodetype is not present on this node."); + } + + // mix:referenceable needs additional assertion: the mixin cannot be + // removed, if any references are left to this node. + NodeTypeImpl mixin = session.getNodeTypeManager().getNodeType(ntName); + if (mixin.isNodeType(NameConstants.MIX_REFERENCEABLE)) { + EffectiveNodeType entRemaining = getRemainingENT(mixinValue); + if (!entRemaining.includesNodeType(NameConstants.MIX_REFERENCEABLE)) { + PropertyIterator iter = getReferences(); + if (iter.hasNext()) { + throw new ConstraintViolationException("Mixin type " + mixinName + " can not be removed: the node is being referenced through at least one property of type REFERENCE"); + } + } + } + + /* + * mix:lockable: the mixin cannot be removed if the node is currently + * locked even if the editing session is the lock holder. + */ + if (mixin.isNodeType((NameConstants.MIX_LOCKABLE))) { + EffectiveNodeType entRemaining = getRemainingENT(mixinValue); + if (!entRemaining.includesNodeType(NameConstants.MIX_LOCKABLE) && isLocked()) { + throw new ConstraintViolationException(mixinName + " can not be removed: the node is locked."); + } + } + + // delegate to operation + Name[] mixins = mixinValue.toArray(new Name[mixinValue.size()]); + Operation op = SetMixin.create(getNodeState(), mixins); + session.getSessionItemStateManager().execute(op); + } + + /** + * Retrieves the value of the jcr:mixinTypes property present with this + * Node including those that have been transiently added and excluding + * those, that have been transiently removed.
        + * NOTE, that the result of this method, does NOT represent the list of + * mixin-types that currently affect this node. + * + * @return mixin names present with the jcr:mixinTypes property. + */ + private List getMixinTypes() { + Name[] mixinValue; + if (getNodeState().getStatus() == Status.EXISTING) { + // jcr:mixinTypes must correspond to the mixins present on the nodestate. + mixinValue = getNodeState().getMixinTypeNames(); + } else { + try { + PropertyEntry pe = getNodeEntry().getPropertyEntry(NameConstants.JCR_MIXINTYPES); + if (pe != null) { + // prop entry exists (and ev. has been transiently mod.) + // -> retrieve mixin types from prop + mixinValue = StateUtility.getMixinNames(pe.getPropertyState()); + } else { + // prop entry has not been loaded yet -> not modified + mixinValue = getNodeState().getMixinTypeNames(); + } + } catch (RepositoryException e) { + // should never occur + log.warn("Internal error", e); + mixinValue = Name.EMPTY_ARRAY; + } + } + List l = new ArrayList(); + l.addAll(Arrays.asList(mixinValue)); + return l; + } + + /** + * Build the effective node type of remaining mixin's & primary type + * + * @param remainingMixins + * @return effective node type + * @throws ConstraintViolationException + * @throws NoSuchNodeTypeException + */ + private EffectiveNodeType getRemainingENT(List remainingMixins) + throws ConstraintViolationException, NoSuchNodeTypeException { + Name[] allRemaining = remainingMixins.toArray(new Name[remainingMixins.size() + 1]); + allRemaining[remainingMixins.size()] = getPrimaryNodeTypeName(); + return session.getEffectiveNodeTypeProvider().getEffectiveNodeType(allRemaining); + } + + /** + * @see Node#canAddMixin(String) + */ + public boolean canAddMixin(String mixinName) throws RepositoryException { + if (!isWritable()) { + // shortcut: repository does not support writing anyway. + return false; + } + try { + // first check if node is writable regarding protection status, + // locks, versioning, access restriction. + session.getValidator().checkIsWritable(getNodeState(), ItemStateValidator.CHECK_ALL); + // then make sure the new mixin would not conflict. + return canAddMixin(getQName(mixinName)); + } catch (LockException e) { + log.debug("Cannot add mixin '" + mixinName + "': " + e.getMessage()); + return false; + } catch (VersionException e) { + log.debug("Cannot add mixin '" + mixinName + "': " + e.getMessage()); + return false; + } catch (ConstraintViolationException e) { + log.debug("Cannot add mixin '" + mixinName + "': " + e.getMessage()); + return false; + } + } + + /** + * @see Node#getDefinition() + */ + public NodeDefinition getDefinition() throws RepositoryException { + checkStatus(); + QNodeDefinition qnd = getNodeState().getDefinition(); + return session.getNodeTypeManager().getNodeDefinition(qnd); + } + + /** + * @see Node#checkin() + */ + public Version checkin() throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException { + checkIsVersionable(); + checkHasPendingChanges(); + checkIsLocked(); + if (isCheckedOut()) { + NodeEntry newVersion = session.getVersionStateManager().checkin(getNodeState()); + return (Version) getItemManager().getItem(newVersion); + } else { + // nothing to do + log.debug("Node " + safeGetJCRPath() + " is already checked in."); + return getBaseVersion(); + } + } + + /** + * @see Node#checkout() + */ + public void checkout() throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + checkIsVersionable(); + checkIsLocked(); + if (!isCheckedOut()) { + if (session.isSupportedOption(Repository.OPTION_ACTIVITIES_SUPPORTED)) { + NodeImpl activity = (NodeImpl) session.getWorkspace().getVersionManager().getActivity(); + NodeId activityId = (activity == null) ? null : activity.getNodeState().getNodeId(); + session.getVersionStateManager().checkout(getNodeState(), activityId); + } else { + session.getVersionStateManager().checkout(getNodeState()); + } + } else { + // nothing to do + log.debug("Node " + safeGetJCRPath() + " is already checked out."); + } + } + + Version checkpoint() throws RepositoryException { + checkIsVersionable(); + checkHasPendingChanges(); + checkIsLocked(); + if (!isCheckedOut()) { + checkout(); + return getBaseVersion(); + } else { + NodeEntry newVersion; + if (session.isSupportedOption(Repository.OPTION_ACTIVITIES_SUPPORTED)) { + NodeImpl activity = (NodeImpl) session.getWorkspace().getVersionManager().getActivity(); + NodeId activityId = (activity == null) ? null : activity.getNodeState().getNodeId(); + newVersion = session.getVersionStateManager().checkpoint(getNodeState(), activityId); + } else { + newVersion = session.getVersionStateManager().checkpoint(getNodeState()); + } + return (Version) getItemManager().getItem(newVersion); + } + } + + /** + * @see Node#doneMerge(Version) + */ + public void doneMerge(Version version) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + resolveMergeConflict(version, true); + } + + /** + * @see Node#cancelMerge(Version) + */ + public void cancelMerge(Version version) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + resolveMergeConflict(version, false); + } + + /** + * Internal method covering both {@link #doneMerge(Version)} and {@link #cancelMerge(Version)}. + * + * @param version + * @param done + * @throws VersionException + * @throws InvalidItemStateException + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + */ + private void resolveMergeConflict(Version version, boolean done) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + checkIsVersionable(); + checkHasPendingChanges(); + checkIsLocked(); + + // check if checked out + if (!isCheckedOut()) { + String msg = "Unable to resolve merge conflict. Node is checked-in: " + safeGetJCRPath(); + log.error(msg); + throw new VersionException(msg); + } + + // check if version is in mergeFailed list + boolean isConflicting = false; + if (hasProperty(NameConstants.JCR_MERGEFAILED)) { + Value[] vals = getProperty(NameConstants.JCR_MERGEFAILED).getValues(); + for (int i = 0; i < vals.length && !isConflicting; i++) { + isConflicting = vals[i].getString().equals(version.getUUID()); + } + } + if (!isConflicting) { + String msg = "Unable to resolve merge conflict. Specified version is not in jcr:mergeFailed property: " + safeGetJCRPath(); + log.error(msg); + throw new VersionException(msg); + } + + NodeState versionState = session.getVersionState(version); + session.getVersionStateManager().resolveMergeConflict(getNodeState(), versionState, done); + } + + /** + * @see Node#update(String) + */ + public void update(String srcWorkspaceName) throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException { + checkIsWritable(); + checkSessionHasPendingChanges(); + + // if same workspace, ignore + if (session.getWorkspace().getName().equals(srcWorkspaceName)) { + return; + } + // test if the corresponding node exists in the src-workspace which includes + // a check if the specified source workspace is accessible + try { + getCorrespondingNodePath(srcWorkspaceName); + } catch (ItemNotFoundException e) { + // no corresponding node exists -> method has not effect + return; + } + + Operation op = Update.create(getNodeState(), srcWorkspaceName); + ((WorkspaceImpl)session.getWorkspace()).getUpdatableItemStateManager().execute(op); + } + + /** + * @see Node#merge(String, boolean) + */ + public NodeIterator merge(String srcWorkspace, boolean bestEffort) throws NoSuchWorkspaceException, AccessDeniedException, VersionException, LockException, InvalidItemStateException, RepositoryException { + return session.getWorkspace().getVersionManager().merge(getPath(), srcWorkspace, bestEffort); + } + + + /** + * TODO: Issue 728 of the pfd... this method is a leftover and will be removed in the final version. + * -> change to package protected then + */ + public NodeIterator merge(String srcWorkspace, boolean bestEffort, boolean isShallow) throws RepositoryException { + return session.getWorkspace().getVersionManager().merge(getPath(), srcWorkspace, bestEffort, isShallow); + } + + /** + * @see Node#getCorrespondingNodePath(String) + */ + public String getCorrespondingNodePath(String workspaceName) throws ItemNotFoundException, NoSuchWorkspaceException, AccessDeniedException, RepositoryException { + checkStatus(); + SessionImpl srcSession = null; + try { + // create session on other workspace for current subject + // (may throw NoSuchWorkspaceException and AccessDeniedException) + srcSession = session.switchWorkspace(workspaceName); + + // search nearest ancestor that is referenceable + NodeImpl referenceableNode = this; + while (referenceableNode.getDepth() != Path.ROOT_DEPTH + && !referenceableNode.isNodeType(NameConstants.MIX_REFERENCEABLE)) { + referenceableNode = (NodeImpl) referenceableNode.getParent(); + } + + // if root is common ancestor, corresponding path is same as ours + // otherwise access referenceable ancestor and calculate correspond. path. + String correspondingPath; + if (referenceableNode.getDepth() == Path.ROOT_DEPTH) { + if (!srcSession.getItemManager().nodeExists(getQPath())) { + throw new ItemNotFoundException("No corresponding path found in workspace " + workspaceName + "(" + safeGetJCRPath() + ")"); + } else { + correspondingPath = getPath(); + } + } else { + // get corresponding ancestor + Node correspNode = srcSession.getNodeByUUID(referenceableNode.getUUID()); + // path of m2 found, if m1 == n1 + if (referenceableNode == this) { + correspondingPath = correspNode.getPath(); + } else { + Path p = referenceableNode.getQPath().computeRelativePath(getQPath()); + // use prefix mappings of srcSession + String relPath = session.getPathResolver().getJCRPath(p); + if (!correspNode.hasNode(relPath)) { + throw new ItemNotFoundException("No corresponding path found in workspace " + workspaceName + "(" + safeGetJCRPath() + ")"); + } else { + correspondingPath = correspNode.getNode(relPath).getPath(); + } + } + } + return correspondingPath; + } finally { + if (srcSession != null) { + // we don't need the other session anymore, logout + srcSession.logout(); + } + } + } + + /** + * @see Node#isCheckedOut() + */ + public boolean isCheckedOut() throws RepositoryException { + checkStatus(); + // shortcut: if state is new, its ancestor must be checkout + if (isNew()) { + return true; + } + return session.getVersionStateManager().isCheckedOut(getNodeState()); + } + + /** + * @see Node#restore(String, boolean) + */ + public void restore(String versionName, boolean removeExisting) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + checkSessionHasPendingChanges(); + // check for version-enabled and lock are performed with subsequent calls. + Version v = getVersionHistory().getVersion(versionName); + restore(this, null, v, removeExisting); + } + + /** + * @see Node#restore(Version, boolean) + */ + public void restore(Version version, boolean removeExisting) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, RepositoryException { + checkSessionHasPendingChanges(); + restore(this, null, version, removeExisting); + } + + /** + * @see Node#restore(Version, String, boolean) + */ + public void restore(Version version, String relPath, boolean removeExisting) throws PathNotFoundException, ItemExistsException, VersionException, ConstraintViolationException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + checkSessionHasPendingChanges(); + + // 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 { + // node at 'relPath' does not yet exist -> build the NodeId + Path nPath = getPath(relPath); + Path parentPath = nPath.getAncestor(1); + ItemManager itemMgr = getItemManager(); + if (itemMgr.nodeExists(parentPath)) { + Node parent = itemMgr.getNode(parentPath); + Path relQPath = parentPath.computeRelativePath(nPath); + NodeImpl parentNode = ((NodeImpl)parent); + // call the restore + restore(parentNode, relQPath, version, removeExisting); + } else if (itemMgr.propertyExists(parentPath)) { + // the item at parentParentPath is Property + throw new ConstraintViolationException("Cannot restore to a parent presenting a property (relative path = '" + relPath + "'"); + } else { + // although the node itself must not exist, is direct ancestor must. + throw new PathNotFoundException("Cannot restore to relative path '" + relPath + ": Ancestor does not exist."); + } + } + } + + /** + * @see Node#restoreByLabel(String, boolean) + */ + public void restoreByLabel(String versionLabel, boolean removeExisting) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + checkSessionHasPendingChanges(); + + // check for version-enabled and lock are performed with subsequent calls. + Version v = getVersionHistory().getVersionByLabel(versionLabel); + if (v == null) { + throw new VersionException("No version for label " + versionLabel + " found."); + } + restore(this, null, v, removeExisting); + } + + /** + * Common internal restore method for the various Node#restore calls. + * + * @param targetNode + * @param relQPath + * @param version + * @param removeExisting + * @throws PathNotFoundException + * @throws ItemExistsException + * @throws VersionException + * @throws ConstraintViolationException + * @throws UnsupportedRepositoryOperationException + * @throws LockException + * @throws InvalidItemStateException + * @throws RepositoryException + */ + private void restore(NodeImpl targetNode, Path relQPath, Version version, boolean removeExisting) throws PathNotFoundException, ItemExistsException, VersionException, ConstraintViolationException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + if (relQPath == null) { + /* restore target already exists. */ + // target must be versionable + targetNode.checkIsVersionable(); + + VersionHistory vH = targetNode.getVersionHistory(); + // version must be a version of the target node + if (!vH.isSame(version.getContainingHistory())) { + throw new VersionException("Version " + version + " does not correspond to the restore target."); + } + // version must not be the root version + if (vH.getRootVersion().isSame(version)) { + throw new VersionException("Attempt to restore root version."); + } + targetNode.checkIsWritable(); + targetNode.checkIsLocked(); + } else { + /* If no node exists at relPath then a VersionException is thrown if + the parent node is not checked out. */ + if (!targetNode.isCheckedOut()) { + throw new VersionException("Parent " + targetNode.safeGetJCRPath() + + " for non-existing restore target '" + + LogUtil.safeGetJCRPath(relQPath, session.getPathResolver()) + + "' must be checked out."); + } + targetNode.checkIsLocked(); + // NOTE: check for nodetype constraint violation is left to the 'server' + } + + NodeState versionState = session.getVersionState(version); + session.getVersionStateManager().restore(targetNode.getNodeState(), relQPath, versionState, removeExisting); + } + + /** + * @see Node#getVersionHistory() + */ + public VersionHistory getVersionHistory() throws UnsupportedRepositoryOperationException, RepositoryException { + checkIsVersionable(); + return (VersionHistory) getProperty(NameConstants.JCR_VERSIONHISTORY).getNode(); + } + + /** + * @see Node#getBaseVersion() + */ + public Version getBaseVersion() throws UnsupportedRepositoryOperationException, RepositoryException { + checkIsVersionable(); + return (Version) getProperty(NameConstants.JCR_BASEVERSION).getNode(); + } + + /** + * @see Node#lock(boolean, boolean) + */ + public Lock lock(boolean isDeep, boolean isSessionScoped) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException { + return lock(isDeep, isSessionScoped, Long.MAX_VALUE, null); + } + + public Lock lock(boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerHint) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException { + checkIsLockable(); + checkHasPendingChanges(); + + return session.getLockStateManager().lock(getNodeState(), isDeep, isSessionScoped, timeoutHint, ownerHint); + } + + /** + * @see Node#getLock() + */ + public Lock getLock() throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + // lock can be inherited from a parent > do not check for node being lockable. + checkStatus(); + return session.getLockStateManager().getLock(getNodeState()); + } + + /** + * @see javax.jcr.Node#unlock() + */ + public void unlock() throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException { + checkIsLockable(); + checkHasPendingChanges(); + + session.getLockStateManager().unlock(getNodeState()); + } + + /** + * @see javax.jcr.Node#holdsLock() + */ + public boolean holdsLock() throws RepositoryException { + // lock can be inherited from a parent > do not check for node being lockable. + checkStatus(); + if (isNew() || !isNodeType(NameConstants.MIX_LOCKABLE)) { + // a node that is new or not lockable never holds a lock + return false; + } else { + LockStateManager lMgr = session.getLockStateManager(); + return (lMgr.isLocked(getNodeState()) && lMgr.getLock(getNodeState()).getNode().isSame(this)); + } + } + + /** + * @see javax.jcr.Node#isLocked() + */ + public boolean isLocked() throws RepositoryException { + // lock can be inherited from a parent > do not check for node being lockable. + checkStatus(); + return session.getLockStateManager().isLocked(getNodeState()); + } + + /** + * @see javax.jcr.Node#followLifecycleTransition(String) + */ + public void followLifecycleTransition(String transition) throws RepositoryException { + session.checkSupportedOption(Repository.OPTION_LIFECYCLE_SUPPORTED); + + // TODO: implementation missing + throw new UnsupportedRepositoryOperationException("JCR-1104"); + } + + /** + * @see javax.jcr.Node#getAllowedLifecycleTransistions() + */ + public String[] getAllowedLifecycleTransistions() throws RepositoryException { + session.checkSupportedOption(Repository.OPTION_LIFECYCLE_SUPPORTED); + + // TODO: implementation missing + throw new UnsupportedRepositoryOperationException("JCR-1104"); + } + + /** + * @see javax.jcr.Node#getSharedSet() + */ + public NodeIterator getSharedSet() throws RepositoryException { + // TODO: implementation missing + throw new UnsupportedRepositoryOperationException("JCR-1104"); + } + + /** + * @see javax.jcr.Node#removeShare() + */ + public void removeShare() throws RepositoryException { + // TODO: implementation missing + throw new UnsupportedRepositoryOperationException("JCR-1104"); + } + + /** + * @see javax.jcr.Node#removeSharedSet() + */ + public void removeSharedSet() throws RepositoryException { + throw new UnsupportedRepositoryOperationException("JCR-1104"); + } + + //--------------------------------------------------------< public impl >--- + /** + * + * @param qName + * @return + * @throws RepositoryException + */ + boolean isNodeType(Name qName) throws RepositoryException { + // first do trivial checks without using type hierarchy + if (qName.equals(getPrimaryNodeTypeName())) { + return true; + } + // check if contained in mixin types + for (Name mixin : getNodeState().getMixinTypeNames()) { + if (mixin.equals(qName)) { + return true; + } + } + // NEW nodes with inherited-mixins -> mixin not yet active + if (getNodeState().getStatus() == Status.NEW && + session.getNodeTypeManager().getNodeType(qName).isMixin()) { + return false; + } + + // check effective node type + EffectiveNodeType effnt = session.getEffectiveNodeTypeProvider().getEffectiveNodeType(getNodeState().getNodeTypeNames()); + return effnt.includesNodeType(qName); + } + + //-----------------------------------------------------------< ItemImpl >--- + /** + * @see ItemImpl#getName() + */ + @Override + Name getQName() throws RepositoryException { + if (getNodeState().isRoot()) { + // shortcut. the given state represents the root or an orphaned node + return NameConstants.ROOT; + } + + return getNodeState().getName(); + } + + + //------------------------------------------------------< check methods >--- + /** + * Checks if this nodes session has pending changes. + * + * @throws InvalidItemStateException if this nodes session has pending changes + * @throws RepositoryException + */ + private void checkSessionHasPendingChanges() throws RepositoryException { + session.checkHasPendingChanges(); + } + + /** + * + * @throws InvalidItemStateException + * @throws RepositoryException + */ + private void checkHasPendingChanges() throws InvalidItemStateException, RepositoryException { + if (hasPendingChanges()) { + String msg = "Node has pending changes: " + getPath(); + log.debug(msg); + throw new InvalidItemStateException(msg); + } + } + + /** + * @return true if this Node is modified or new. + */ + private boolean hasPendingChanges() { + return isModified() || isNew(); + } + + /** + * Checks if this node is lockable, i.e. has 'mix:lockable'. + * + * @throws UnsupportedRepositoryOperationException if this node is not lockable. + * @throws RepositoryException if another error occurs. + */ + private void checkIsLockable() throws UnsupportedRepositoryOperationException, RepositoryException { + checkStatus(); + if (!isNodeType(NameConstants.MIX_LOCKABLE)) { + String msg = "Unable to perform locking operation on non-lockable node: " + getPath(); + log.debug(msg); + throw new LockException(msg); + } + } + + /** + * Check whether this node is locked by somebody else. + * + * @throws LockException if this node is locked by somebody else. + * @throws RepositoryException if some other error occurs. + */ + void checkIsLocked() throws LockException, RepositoryException { + if (isNew()) { + // if this node is new, no checks must be performed. + return; + } + // perform check + session.getLockStateManager().checkLock(getNodeState()); + } + + /** + * Check if this node is versionable. + * + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + */ + private void checkIsVersionable() throws UnsupportedRepositoryOperationException, RepositoryException { + checkStatus(); + if (!isNodeType(NameConstants.MIX_VERSIONABLE)) { + String msg = "Unable to perform versioning operation on non versionable node: " + getPath(); + log.debug(msg); + throw new UnsupportedRepositoryOperationException(msg); + } + } + + //---------------------------------------------< private implementation >--- + /** + * Create a new NodeState and subsequently retrieves the + * corresponding Node object. + * + * @param nodeName name of the new node + * @param nodeTypeName name of the new node's node type or null + * if it should be determined automatically + * @return the newly added node + * @throws ItemExistsException + * @throws NoSuchNodeTypeException + * @throws VersionException + * @throws ConstraintViolationException + * @throws LockException + * @throws RepositoryException + */ + private synchronized Node createNode(Name nodeName, Name nodeTypeName) + throws ItemExistsException, NoSuchNodeTypeException, VersionException, + ConstraintViolationException, LockException, RepositoryException { + + QNodeDefinition definition = session.getItemDefinitionProvider().getQNodeDefinition(getNodeState().getAllNodeTypeNames(), nodeName, nodeTypeName); + if (nodeTypeName == null) { + // use default node type + nodeTypeName = definition.getDefaultPrimaryType(); + } + // validation check are performed by item state manager + // NOTE: uuid is generated while creating new state. + Operation an = AddNode.create(getNodeState(), nodeName, nodeTypeName, null); + session.getSessionItemStateManager().execute(an); + + // finally retrieve the new node + List addedStates = ((AddNode) an).getAddedStates(); + ItemState nState = addedStates.get(0); + return (Node) getItemManager().getItem(nState.getHierarchyEntry()); + } + + // TODO: protected due to usage within VersionImpl, VersionHistoryImpl (check for alternatives) + /** + * + * @param nodeName + * @param index + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + protected Node getNode(Name nodeName, int index) throws PathNotFoundException, RepositoryException { + checkStatus(); + try { + NodeEntry nEntry = getNodeEntry().getNodeEntry(nodeName, index); + if (nEntry == null) { + throw new PathNotFoundException(LogUtil.saveGetJCRName(nodeName, session.getNameResolver())); + } + return (Node) getItemManager().getItem(nEntry); + } catch (AccessDeniedException e) { + throw new PathNotFoundException(LogUtil.saveGetJCRName(nodeName, session.getNameResolver())); + } + } + + /** + * + * @param qName + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + // TODO: protected due to usage within VersionImpl, VersionHistoryImpl (check for alternatives) + protected Property getProperty(Name qName) throws PathNotFoundException, RepositoryException { + checkStatus(); + try { + PropertyEntry pEntry = getNodeEntry().getPropertyEntry(qName, true); + if (pEntry == null) { + throw new PathNotFoundException(LogUtil.saveGetJCRName(qName, session.getNameResolver())); + } + return (Property) getItemManager().getItem(pEntry); + } catch (AccessDeniedException e) { + throw new PathNotFoundException(LogUtil.saveGetJCRName(qName, session.getNameResolver())); + } + } + + /** + * Create a new single valued property + * + * @param qName + * @param type + * @param value + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found. + * @throws RepositoryException if another error occurs. + */ + private Property createProperty(Name qName, Value value, int type) + throws ConstraintViolationException, RepositoryException { + QPropertyDefinition def = getApplicablePropertyDefinition(qName, type, false); + int targetType = def.getRequiredType(); + if (targetType == PropertyType.UNDEFINED) { + targetType = type; + } + QValue qvs; + if (targetType == PropertyType.UNDEFINED) { + qvs = ValueFormat.getQValue(value, session.getNamePathResolver(), session.getQValueFactory()); + targetType = qvs.getType(); + } else { + Value targetValue = ValueHelper.convert(value, targetType, session.getValueFactory()); + qvs = ValueFormat.getQValue(targetValue, session.getNamePathResolver(), session.getQValueFactory()); + } + return createProperty(qName, targetType, def, new QValue[] {qvs}); + } + + /** + * Create a new multi valued property + * + * @param qName + * @param type + * @param values + * @return + * @throws ConstraintViolationException + * @throws RepositoryException + */ + private Property createProperty(Name qName, Value[] values, int type) + throws ConstraintViolationException, RepositoryException { + QPropertyDefinition def = getApplicablePropertyDefinition(qName, type, true); + int targetType = def.getRequiredType(); + // make sure, the final type is not set to undefined + if (targetType == PropertyType.UNDEFINED) { + if (type == PropertyType.UNDEFINED) { + // try to retrieve type from the values array + if (values.length > 0) { + for (Value value : values) { + if (value != null) { + targetType = value.getType(); + break; + } + } + } + if (targetType == PropertyType.UNDEFINED) { + // fallback + targetType = PropertyType.STRING; + } + } else { + targetType = type; + } + } + Value[] targetValues = ValueHelper.convert(values, targetType, session.getValueFactory()); + QValue[] qvs = ValueFormat.getQValues(targetValues, session.getNamePathResolver(), session.getQValueFactory()); + return createProperty(qName, targetType, def, qvs); + } + + /** + * + * @param qName + * @param type + * @param def + * @param qvs + * @return + * @throws PathNotFoundException + * @throws ConstraintViolationException + * @throws RepositoryException + */ + private Property createProperty(Name qName, int type, QPropertyDefinition def, + QValue[] qvs) + throws ConstraintViolationException, RepositoryException { + Operation op = AddProperty.create(getNodeState(), qName, type, def, qvs); + session.getSessionItemStateManager().execute(op); + return getProperty(qName); + } + + /** + * + * @param jcrName + * @return + * @throws RepositoryException + */ + private Name getQName(String jcrName) throws RepositoryException { + Name qName; + try { + qName = session.getNameResolver().getQName(jcrName); + } catch (NameException upe) { + throw new RepositoryException("invalid name: "+ jcrName, upe); + } + return qName; + } + + /** + * @return the primary node type name. + */ + private Name getPrimaryNodeTypeName() { + return getNodeState().getNodeTypeName(); + } + + /** + * + * @param name + * @param weak + * @return + * @throws RepositoryException + */ + private LazyItemIterator getReferences(String name, boolean weak) throws RepositoryException { + checkStatus(); + Name propName = (name == null) ? null : getQName(name); + Iterator itr = getNodeState().getNodeReferences(propName, weak); + return new LazyItemIterator(getItemManager(), session.getHierarchyManager(), itr); + } + + /** + * + * @param mixinName + * @return + * @throws NoSuchNodeTypeException + * @throws ConstraintViolationException + */ + private boolean canAddMixin(Name mixinName) throws NoSuchNodeTypeException, + ConstraintViolationException { + NodeTypeManagerImpl ntMgr = session.getNodeTypeManager(); + + // first check characteristics of each mixin + NodeType mixin = ntMgr.getNodeType(mixinName); + if (!mixin.isMixin()) { + log.error(mixin.getName() + ": not a mixin node type"); + return false; + } + + // get list of existing nodetypes + Name[] existingNts = getNodeState().getNodeTypeNames(); + // build effective node type representing primary type including existing mixins + EffectiveNodeType entExisting = session.getEffectiveNodeTypeProvider().getEffectiveNodeType(existingNts); + + // check if the base type supports adding this mixin + if (!entExisting.supportsMixin(mixinName)) { + log.debug(mixin.getName() + ": not supported on node type " + getPrimaryNodeTypeName()); + return false; + } + + // second, build new effective node type for nts including the new mixin + // types, detecting eventual incompatibilities + Name[] resultingNts = new Name[existingNts.length + 1]; + System.arraycopy(existingNts, 0, resultingNts, 0, existingNts.length); + resultingNts[existingNts.length] = mixinName; + session.getEffectiveNodeTypeProvider().getEffectiveNodeType(resultingNts); + + // all validations succeeded: return true + return true; + } + + /** + * @return NodeState of this Node + */ + private NodeState getNodeState() { + return (NodeState) getItemState(); + } + + /** + * @return NodeEntry of this Node + */ + private NodeEntry getNodeEntry() { + return (NodeEntry) getItemState().getHierarchyEntry(); + } + + /** + * + * @param relativePath + * @return + * @throws RepositoryException + */ + private Path getReorderPath(String relativePath) throws RepositoryException { + try { + Path p = session.getPathResolver().getQPath(relativePath); + if (p.isAbsolute() || p.getLength() != 1 || p.getDepth() != 1) { + throw new RepositoryException("Invalid relative path: " + relativePath); + } + return p; + } catch (NameException e) { + String msg = "Invalid relative path: " + relativePath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + /** + * @param relativeJcrPath + * @return Path object for the specified relative JCR path string. + * @throws RepositoryException + */ + private Path getPath(String relativeJcrPath) throws RepositoryException { + try { + Path p = session.getPathResolver().getQPath(relativeJcrPath); + return getPath(p); + } catch (NameException e) { + String msg = "Invalid relative path: " + relativeJcrPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + /** + * @param relativePath + * @return normalized absolute path calculated from the given relative + * path and the path of this node. + * @throws RepositoryException + */ + private Path getPath(Path relativePath) throws RepositoryException { + // shortcut + if (relativePath.getLength() == 1 && relativePath.denotesCurrent()) { + return getQPath(); + } + return session.getPathFactory().create(getQPath(), relativePath, true); + } + + /** + * Returns the NodeEntry at relPath or + * null if no node exists at relPath. + *

        + * Note that access rights are not checked. + * + * @param relPath relative path of a (possible) node. + * @return the HierarchyEntry of the node at relPath or + * null if no node exists at relPath. + * @throws RepositoryException if relPath is not a valid + * relative path. + */ + private NodeEntry resolveRelativeNodePath(String relPath) throws RepositoryException { + NodeEntry targetEntry = null; + try { + Path rp = session.getPathResolver().getQPath(relPath); + // shortcut + if (rp.getLength() == 1) { + if (rp.denotesCurrent()) { + targetEntry = getNodeEntry(); + } else if (rp.denotesParent()) { + targetEntry = getNodeEntry().getParent(); + } else { + // try to get child entry + force loading of not known yet + targetEntry = getNodeEntry().getNodeEntry( + rp.getName(), rp.getNormalizedIndex(), true); + } + } else { + // rp length > 1 + Path p = getPath(rp); + targetEntry = session.getHierarchyManager().getNodeEntry(p.getCanonicalPath()); + } + } catch (PathNotFoundException e) { + // item does not exist -> ignore and return null + } catch (NameException e) { + String msg = "Invalid relative path: " + relPath; + log.debug(msg); + throw new RepositoryException(msg, e); + } + return targetEntry; + } + + /** + * Returns the id of the property at relPath or null + * if no property exists at relPath. + *

        + * Note that access rights are not checked. + * + * @param relPath relative path of a (possible) property + * @return the PropertyEntry of the property at relPath or + * null if no property exists at relPath + * @throws RepositoryException if relPath is not a valid + * relative path + */ + private PropertyEntry resolveRelativePropertyPath(String relPath) throws RepositoryException { + PropertyEntry targetEntry = null; + try { + Path rp = session.getPathResolver().getQPath(relPath); + if (rp.getLength() == 1 && rp.denotesName()) { + // a single path element must always denote a name. '.' and '..' + // will never point to a property. If the NodeEntry does not + // contain such a property entry, the targetEntry is 'null; + Name propName = rp.getName(); + // check if property entry exists + targetEntry = getNodeEntry().getPropertyEntry(propName, true); + } else { + // build and resolve absolute path + Path p = getPath(rp).getCanonicalPath(); + try { + targetEntry = session.getHierarchyManager().getPropertyEntry(p); + } catch (PathNotFoundException e) { + // ignore -> return null; + } + } + } catch (NameException e) { + String msg = "failed to resolve property path " + relPath + " relative to " + safeGetJCRPath(); + log.debug(msg); + throw new RepositoryException(msg, e); + } + return targetEntry; + } + + /** + * Returns the applicable property definition for a property with the + * specified name and type. + * + * @param propertyName + * @param type + * @param multiValued + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found + * @throws RepositoryException if another error occurs + */ + private QPropertyDefinition getApplicablePropertyDefinition(Name propertyName, + int type, + boolean multiValued) + throws ConstraintViolationException, RepositoryException { + return session.getItemDefinitionProvider().getQPropertyDefinition(getNodeState().getAllNodeTypeNames(), propertyName, type, multiValued); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/PropertyImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/PropertyImpl.java new file mode 100644 index 00000000000..7927f131b3e --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/PropertyImpl.java @@ -0,0 +1,638 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Calendar; + +import javax.jcr.Binary; +import javax.jcr.Item; +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.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.SetPropertyValue; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.value.ValueHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * PropertyImpl... + */ +public class PropertyImpl extends ItemImpl implements Property { + + private static Logger log = LoggerFactory.getLogger(PropertyImpl.class); + + public static final int UNDEFINED_PROPERTY_LENGTH = -1; + + public PropertyImpl(SessionImpl session, PropertyState state, ItemLifeCycleListener[] listeners) { + super(session, state, listeners); + // NOTE: JCR value(s) will be read (and converted from the internal value + // representation) on demand. + } + + //-----------------------------------------------------< Item interface >--- + /** + * @see Item#getName() + */ + @Override + public String getName() throws RepositoryException { + checkStatus(); + Name name = getQName(); + return session.getNameResolver().getJCRName(name); + } + + /** + * Implementation of {@link Item#accept(javax.jcr.ItemVisitor)} for property. + * + * @param visitor + * @see Item#accept(javax.jcr.ItemVisitor) + */ + @Override + public void accept(ItemVisitor visitor) throws RepositoryException { + checkStatus(); + visitor.visit(this); + } + + /** + * Returns false + * + * @return false + * @see javax.jcr.Item#isNode() + */ + @Override + public boolean isNode() { + return false; + } + + //-------------------------------------------------< Property interface >--- + /** + * @see Property#setValue(javax.jcr.Value) + */ + public void setValue(Value value) throws ValueFormatException, VersionException, LockException, RepositoryException { + checkIsWritable(false); + int valueType = (value != null) ? value.getType() : PropertyType.UNDEFINED; + int reqType = getRequiredType(valueType); + setValue(value, reqType); + } + + /** + * @see Property#setValue(javax.jcr.Value[]) + */ + public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException, RepositoryException { + checkIsWritable(true); + // assert equal types for all values entries + int valueType = PropertyType.UNDEFINED; + if (values != null) { + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + // skip null values as those will be purged later + continue; + } + if (valueType == PropertyType.UNDEFINED) { + valueType = values[i].getType(); + } else if (valueType != values[i].getType()) { + String msg = "Inhomogeneous type of values (" + safeGetJCRPath() + ")"; + log.debug(msg); + throw new ValueFormatException(msg); + } + } + } + + int targetType = getDefinition().getRequiredType(); + if (targetType == PropertyType.UNDEFINED) { + targetType = (valueType == PropertyType.UNDEFINED) ? PropertyType.STRING : valueType; + } + // convert to internal values of correct type + QValue[] qValues = null; + if (values != null) { + Value[] vs = ValueHelper.convert(values, targetType, session.getValueFactory()); + qValues = ValueFormat.getQValues(vs, session.getNamePathResolver(), session.getQValueFactory()); + } + setInternalValues(qValues, targetType); + } + + /** + * @see Property#setValue(String) + */ + public void setValue(String value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.STRING); + if (value == null) { + setInternalValues(null, reqType); + } else { + setValue(session.getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(String[]) + */ + public void setValue(String[] values) throws ValueFormatException, VersionException, LockException, RepositoryException { + checkIsWritable(true); + int reqType = getRequiredType(PropertyType.STRING); + + QValue[] qValues = null; + // convert to internal values of correct type + if (values != null) { + qValues = new QValue[values.length]; + for (int i = 0; i < values.length; i++) { + String string = values[i]; + QValue qValue = null; + if (string != null) { + if (reqType != PropertyType.STRING) { + // type conversion required + Value v = ValueHelper.convert(string, reqType, session.getValueFactory()); + qValue = ValueFormat.getQValue(v, session.getNamePathResolver(), session.getQValueFactory()); + } else { + // no type conversion required + qValue = session.getQValueFactory().create(string, PropertyType.STRING); + } + } + qValues[i] = qValue; + } + } + setInternalValues(qValues, reqType); + } + + /** + * @see Property#setValue(InputStream) + */ + public void setValue(InputStream value) throws ValueFormatException, VersionException, LockException, RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.BINARY); + if (value == null) { + setInternalValues(null, reqType); + } else { + setValue(session.getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(Binary) + */ + public void setValue(Binary value) throws RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.BINARY); + if (value == null) { + setInternalValues(null, reqType); + } else { + setValue(session.getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(long) + */ + public void setValue(long value) throws ValueFormatException, VersionException, LockException, RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.LONG); + setValue(session.getValueFactory().createValue(value), reqType); + } + + /** + * @see Property#setValue(double) + */ + public void setValue(double value) throws ValueFormatException, VersionException, LockException, RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.DOUBLE); + setValue(session.getValueFactory().createValue(value), reqType); + } + + /** + * @see Property#setValue(BigDecimal) + */ + public void setValue(BigDecimal value) throws RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.DECIMAL); + setValue(session.getValueFactory().createValue(value), reqType); + } + + /** + * @see Property#setValue(Calendar) + */ + public void setValue(Calendar value) throws ValueFormatException, VersionException, LockException, RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.DATE); + if (value == null) { + setInternalValues(null, reqType); + } else { + setValue(session.getValueFactory().createValue(value), reqType); + } + } + + /** + * @see Property#setValue(boolean) + */ + public void setValue(boolean value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.BOOLEAN); + setValue(session.getValueFactory().createValue(value), reqType); + } + + /** + * @see Property#setValue(Node) + */ + public void setValue(Node value) throws ValueFormatException, VersionException, LockException, RepositoryException { + checkIsWritable(false); + int reqType = getRequiredType(PropertyType.REFERENCE); + if (value == null) { + setInternalValues(null, reqType); + } else { + checkValidReference(value, reqType, session.getNameResolver()); + QValue qValue = session.getQValueFactory().create(value.getUUID(), PropertyType.REFERENCE); + setInternalValues(new QValue[]{qValue}, reqType); + } + } + + /** + * @see Property#getValue() + */ + public Value getValue() throws ValueFormatException, RepositoryException { + QValue value = getQValue(); + return ValueFormat.getJCRValue(value, session.getNamePathResolver(), session.getJcrValueFactory()); + } + + /** + * @see Property#getValues() + */ + public Value[] getValues() throws ValueFormatException, RepositoryException { + QValue[] qValues = getQValues(); + Value[] values = new Value[qValues.length]; + for (int i = 0; i < qValues.length; i++) { + values[i] = ValueFormat.getJCRValue(qValues[i], session.getNamePathResolver(), session.getJcrValueFactory()); + } + return values; + } + + /** + * @see Property#getString() + */ + public String getString() throws ValueFormatException, RepositoryException { + return getValue().getString(); + } + + /** + * @see Property#getStream() + */ + public InputStream getStream() throws ValueFormatException, RepositoryException { + return getValue().getStream(); + } + + /** + * @see Property#getBinary() + */ + public Binary getBinary() throws RepositoryException { + return getValue().getBinary(); + } + + /** + * @see Property#getLong() + */ + public long getLong() throws ValueFormatException, RepositoryException { + return getValue().getLong(); + } + + /** + * @see Property#getDouble() + */ + public double getDouble() throws ValueFormatException, RepositoryException { + return getValue().getDouble(); + } + + /** + * @see Property#getDecimal() + */ + public BigDecimal getDecimal() throws RepositoryException { + return getValue().getDecimal(); + } + + /** + * @see Property#getDate() + */ + public Calendar getDate() throws ValueFormatException, RepositoryException { + return getValue().getDate(); + } + + /** + * @see Property#getBoolean() + */ + public boolean getBoolean() throws ValueFormatException, RepositoryException { + return getValue().getBoolean(); + } + + /** + * @see Property#getNode() + */ + public Node getNode() throws ValueFormatException, RepositoryException { + Value value = getValue(); + switch (value.getType()) { + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + return session.getNodeByIdentifier(value.getString()); + + case PropertyType.PATH: + case PropertyType.NAME: + String path = value.getString(); + Path p = session.getPathResolver().getQPath(path); + try { + return (p.isAbsolute()) ? session.getNode(path) : getParent().getNode(path); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(path); + } + + case PropertyType.STRING: + try { + Value refValue = ValueHelper.convert(value, PropertyType.REFERENCE, session.getValueFactory()); + return session.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, session.getValueFactory()); + p = session.getPathResolver().getQPath(pathValue.getString()); + try { + return (p.isAbsolute()) ? session.getNode(pathValue.getString()) : getParent().getNode(pathValue.getString()); + } catch (PathNotFoundException e1) { + throw new ItemNotFoundException(pathValue.getString()); + } + } + + default: + throw new ValueFormatException("Property value cannot be converted to a PATH, REFERENCE or WEAKREFERENCE"); + } + } + + /** + * @see Property#getProperty() + */ + public Property getProperty() throws RepositoryException { + Value value = getValue(); + Value pathValue = ValueHelper.convert(value, PropertyType.PATH, session.getValueFactory()); + String path = pathValue.getString(); + boolean absolute; + try { + Path p = session.getPathResolver().getQPath(path); + absolute = p.isAbsolute(); + } catch (RepositoryException e) { + throw new ValueFormatException("Property value cannot be converted to a PATH"); + } + try { + return (absolute) ? session.getProperty(path) : getParent().getProperty(path); + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(path); + } + } + + /** + * @see Property#getLength + */ + public long getLength() throws ValueFormatException, RepositoryException { + return getLength(getQValue()); + } + + /** + * @see Property#getLengths + */ + public long[] getLengths() throws ValueFormatException, RepositoryException { + QValue[] values = getQValues(); + long[] lengths = new long[values.length]; + for (int i = 0; i < values.length; i++) { + lengths[i] = getLength(values[i]); + } + return lengths; + } + + /** + * + * @param value + * @return + * @throws RepositoryException + */ + private long getLength(QValue value) throws RepositoryException { + long length; + switch (value.getType()) { + case PropertyType.NAME: + case PropertyType.PATH: + String jcrString = ValueFormat.getJCRString(value, session.getNamePathResolver()); + length = jcrString.length(); + break; + default: + length = value.getLength(); + break; + } + return length; + } + + /** + * @see javax.jcr.Property#getDefinition() + */ + public PropertyDefinition getDefinition() throws RepositoryException { + checkStatus(); + QPropertyDefinition qpd = getPropertyState().getDefinition(); + return session.getNodeTypeManager().getPropertyDefinition(qpd); + } + + /** + * @see javax.jcr.Property#getType() + */ + public int getType() throws RepositoryException { + checkStatus(); + return getPropertyState().getType(); + } + + /** + * + * @return true if the definition indicates that this Property is multivalued. + */ + public boolean isMultiple() { + return getPropertyState().isMultiValued(); + } + + //-----------------------------------------------------------< ItemImpl >--- + /** + * Returns the Name defined with this PropertyState + * + * @return + * @see PropertyState#getName() + * @see ItemImpl#getName() + */ + @Override + Name getQName() { + return getPropertyState().getName(); + } + + //------------------------------------------------------< check methods >--- + /** + * + * @param multiValues + * @throws RepositoryException + */ + private void checkIsWritable(boolean multiValues) throws RepositoryException { + // check common to properties and nodes + checkIsWritable(); + + // property specific check + if (isMultiple() != multiValues) { + throw new ValueFormatException(getPath() + "Multivalue definition of " + safeGetJCRPath() + " does not match to given value(s)."); + } + } + + //---------------------------------------------< private implementation >--- + + /** + * + * @param defaultType + * @return the required type for this property. + */ + 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; + } + + /** + * + * @return + * @throws ValueFormatException + * @throws RepositoryException + */ + private QValue getQValue() throws ValueFormatException, RepositoryException { + checkStatus(); + if (isMultiple()) { + throw new ValueFormatException(safeGetJCRPath() + " is multi-valued and can therefore only be retrieved as an array of values"); + } + // avoid unnecessary object creation if possible + return getPropertyState().getValue(); + } + + /** + * + * @return + * @throws ValueFormatException + * @throws RepositoryException + */ + private QValue[] getQValues() throws ValueFormatException, RepositoryException { + checkStatus(); + if (!isMultiple()) { + throw new ValueFormatException(safeGetJCRPath() + " is not multi-valued and can therefore only be retrieved as single value"); + } + // avoid unnecessary object creation if possible + return getPropertyState().getValues(); + } + + /** + * + * @param value + * @param requiredType + * @throws RepositoryException + */ + private void setValue(Value value, int requiredType) throws RepositoryException { + if (requiredType == PropertyType.UNDEFINED) { + // should never get here since calling methods assert valid type + throw new IllegalArgumentException("Property type of a value cannot be undefined (" + safeGetJCRPath() + ")."); + } + if (value == null) { + setInternalValues(null, requiredType); + return; + } + + QValue qValue; + if (requiredType != value.getType()) { + // type conversion required + Value v = ValueHelper.convert(value, requiredType, session.getValueFactory()); + qValue = ValueFormat.getQValue(v, session.getNamePathResolver(), session.getQValueFactory()); + } else { + // no type conversion required + qValue = ValueFormat.getQValue(value, session.getNamePathResolver(), session.getQValueFactory()); + } + setInternalValues(new QValue[]{qValue}, requiredType); + } + + /** + * + * @param qValues + * @param valueType + * @throws ConstraintViolationException + * @throws RepositoryException + */ + private void setInternalValues(QValue[] qValues, int valueType) throws ConstraintViolationException, RepositoryException { + // check for null value + if (qValues == null) { + // setting a property to null removes it automatically + remove(); + return; + } + // modify the state of this property + Operation op = SetPropertyValue.create(getPropertyState(), qValues, valueType); + session.getSessionItemStateManager().execute(op); + } + + /** + * Private helper to access the PropertyState directly + * + * @return state for this Property + */ + private PropertyState getPropertyState() { + return (PropertyState) getItemState(); + } + + /** + * + * @param value + * @param propertyType + * @throws ValueFormatException + * @throws RepositoryException + */ + static void checkValidReference(Node value, int propertyType, NameResolver resolver) throws ValueFormatException, RepositoryException { + if (propertyType == PropertyType.REFERENCE) { + String jcrName = resolver.getJCRName(NameConstants.MIX_REFERENCEABLE); + if (!value.isNodeType(jcrName)) { + throw new ValueFormatException("Target node must be of node type mix:referenceable"); + } + } else { + throw new ValueFormatException("Property must be of type REFERENCE."); + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/RepositoryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/RepositoryImpl.java new file mode 100644 index 00000000000..712cb5cbe2c --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/RepositoryImpl.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Map; +import java.util.HashMap; + +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.NamespaceException; +import javax.naming.Context; +import javax.naming.Name; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.Referenceable; +import javax.naming.StringRefAddr; +import javax.naming.spi.ObjectFactory; + +import org.apache.jackrabbit.commons.AbstractRepository; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.XASessionInfo; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.value.ValueFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RepositoryImpl... + */ +public class RepositoryImpl extends AbstractRepository implements Referenceable { + + private static Logger log = LoggerFactory.getLogger(RepositoryImpl.class); + + // configuration of the repository + private final RepositoryConfig config; + private final Map descriptors; + private Reference reference = null; + + private RepositoryImpl(RepositoryConfig config) throws RepositoryException { + this.config = config; + + // dummy value factory and dummy resolver as descriptors are not + // expected to contain Name or Path values. + ValueFactory vf = ValueFactoryImpl.getInstance(); + NamePathResolver resolver = new DefaultNamePathResolver(new NamespaceResolver() { + public String getURI(String prefix) throws NamespaceException { + return prefix; + } + public String getPrefix(String uri) throws NamespaceException { + return uri; + } + }); + + Map descr = config.getRepositoryService().getRepositoryDescriptors(); + descriptors = new HashMap(descr.size()); + for (String key : descr.keySet()) { + QValue[] qvs = descr.get(key); + Value[] vs = new Value[qvs.length]; + for (int i = 0; i < qvs.length; i++) { + vs[i] = ValueFormat.getJCRValue(qvs[i], resolver, vf); + } + descriptors.put(key, vs); + } + } + + public static Repository create(RepositoryConfig config) throws RepositoryException { + return new RepositoryImpl(config); + } + + //---------------------------------------------------------< Repository >--- + /** + * @see Repository#getDescriptorKeys() + */ + public String[] getDescriptorKeys() { + return descriptors.keySet().toArray(new String[descriptors.keySet().size()]); + } + + /** + * @see Repository#getDescriptor(String) + */ + public String getDescriptor(String key) { + Value v = getDescriptorValue(key); + try { + return (v == null) ? null : v.getString(); + } catch (RepositoryException e) { + log.error("corrupt descriptor value: " + key, e); + return null; + } + } + + /** + * @see Repository#getDescriptorValue(String) + */ + public Value getDescriptorValue(String key) { + Value[] vs = getDescriptorValues(key); + return (vs == null || vs.length != 1) ? null : vs[0]; + } + + /** + * @see Repository#getDescriptorValues(String) + */ + public Value[] getDescriptorValues(String key) { + if (!descriptors.containsKey(key)) { + return null; + } else { + return descriptors.get(key); + + } + } + + /** + * @see Repository#isSingleValueDescriptor(String) + */ + public boolean isSingleValueDescriptor(String key) { + Value[] vs = descriptors.get(key); + return (vs != null && vs.length == 1); + } + + /** + * @see Repository#login(javax.jcr.Credentials, String) + */ + public Session login(Credentials credentials, String workspaceName) throws LoginException, NoSuchWorkspaceException, RepositoryException { + SessionInfo info = config.getRepositoryService().obtain(credentials, workspaceName); + try { + if (info instanceof XASessionInfo) { + return new XASessionImpl((XASessionInfo) info, this, config); + } else { + return new SessionImpl(info, this, config); + } + } catch (RepositoryException ex) { + config.getRepositoryService().dispose(info); + throw ex; + } + } + + //------------------------------------------------------< Referenceable >--- + /** + * @see Referenceable#getReference() + */ + public Reference getReference() throws NamingException { + if (config instanceof Referenceable) { + Referenceable confref = (Referenceable)config; + if (reference == null) { + reference = new Reference(RepositoryImpl.class.getName(), RepositoryImpl.Factory.class.getName(), null); + // carry over all addresses from referenceable config + for (Enumeration en = confref.getReference().getAll(); en.hasMoreElements(); ) { + reference.add(en.nextElement()); + } + + // also add the information required by factory class + reference.add(new StringRefAddr(Factory.RCF, confref.getReference().getFactoryClassName())); + reference.add(new StringRefAddr(Factory.RCC, config.getClass().getName())); + } + + return reference; + } + else { + throw new javax.naming.OperationNotSupportedException("Contained RepositoryConfig needs to implement javax.naming.Referenceable"); + } + } + + /** + * Implementation of {@link ObjectFactory} for repository instances. + *

        + * Works by creating a {@link Reference} to a {@link RepositoryConfig} + * instance based on the information obtained from the {@link RepositoryImpl}'s + * {@link Reference}. + *

        + * Address Types: + *

        + *
        {@link #RCF} + *
        Class name for {@link ObjectFactory} creating instances of {@link RepositoryConfig}
        + *
        {@link #RCC} + *
        Class name for {@link RepositoryConfig} instances
        + *
        + *

        + * All other types are copied over verbatim to the new {@link Reference}. + *

        + * A sample JNDI configuration inside a servlet container's server.xml: + *

        +     *   <Resource
        +     *         name="jcr/repositoryname"
        +     *         auth="Container"
        +     *         type="org.apache.jackrabbit.jcr2spi.RepositoryImpl"
        +     *         factory="org.apache.jackrabbit.jcr2spi.RepositoryImpl$Factory"
        +     *         org.apache.jackrabbit.jcr2spi.RepositoryImpl.factory="class name of {@link ObjectFactory} for {@link RepositoryConfig} instances"
        +     *         org.apache.jackrabbit.jcr2spi.RepositoryImpl.class="class name of {@link RepositoryConfig} implementation class"
        +     *         ...additional properties passed to the {@link ObjectFactory}...
        +     *   />
        +     * 
        + */ + public static class Factory implements ObjectFactory { + + public static final String RCF = RepositoryImpl.class.getName() + ".factory"; + public static final String RCC = RepositoryImpl.class.getName() + ".class"; + + public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception { + + Object res = null; + if (obj instanceof Reference) { + Reference ref = (Reference)obj; + String classname = ref.getClassName(); + + if (RepositoryImpl.class.getName().equals(classname)) { + + RefAddr rfac = ref.get(RCF); + if (rfac == null || !(rfac instanceof StringRefAddr)) { + throw new Exception("Address type " + RCF + " missing or of wrong class: " + rfac); + } + String configFactoryClassName = (String)((StringRefAddr)rfac).getContent(); + + RefAddr rclas = ref.get(RCC); + if (rclas == null || !(rclas instanceof StringRefAddr)) { + throw new Exception("Address type " + RCC + " missing or of wrong class: " + rclas); + } + String repositoryConfigClassName = (String)((StringRefAddr)rclas).getContent(); + + Object rof = Class.forName(configFactoryClassName).newInstance(); + + if (! (rof instanceof ObjectFactory)) { + throw new Exception(rof + " must implement ObjectFactory"); + } + + ObjectFactory of = (ObjectFactory)rof; + Reference newref = new Reference(repositoryConfigClassName, + configFactoryClassName, null); + + // carry over all arguments except our own + for (Enumeration en = ref.getAll(); en.hasMoreElements(); ){ + RefAddr ra = en.nextElement(); + String type = ra.getType(); + if (! RCF.equals(type) && ! RCC.equals(type)) { + newref.add(ra); + } + } + + Object config = of.getObjectInstance(newref, name, nameCtx, environment); + if (! (config instanceof RepositoryConfig)) { + throw new Exception(config + " must implement RepositoryConfig"); + } + return RepositoryImpl.create((RepositoryConfig)config); + } + else { + throw new Exception("Unexpected class: " + classname); + } + } + return res; + } + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/SessionImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/SessionImpl.java new file mode 100644 index 00000000000..7f278d92d3f --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/SessionImpl.java @@ -0,0 +1,1076 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.io.IOException; +import java.io.InputStream; +import java.security.AccessControlException; +import java.util.Map; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.InvalidItemStateException; +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.NamespaceException; +import javax.jcr.NoSuchWorkspaceException; +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.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.retention.RetentionManager; +import javax.jcr.security.AccessControlManager; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.commons.AbstractSession; +import org.apache.jackrabbit.jcr2spi.config.CacheBehaviour; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManagerImpl; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.lock.LockStateManager; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeManagerImpl; +import org.apache.jackrabbit.jcr2spi.operation.Move; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.security.AccessManager; +import org.apache.jackrabbit.jcr2spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.jcr2spi.state.ItemStateFactory; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.SessionItemStateManager; +import org.apache.jackrabbit.jcr2spi.state.UpdatableItemStateManager; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.jcr2spi.xml.ImportHandler; +import org.apache.jackrabbit.jcr2spi.xml.Importer; +import org.apache.jackrabbit.jcr2spi.xml.SessionImporter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.XASessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.IdentifierResolver; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * SessionImpl... + */ +public class SessionImpl extends AbstractSession + implements NamespaceResolver, ManagerProvider { + + private static Logger log = LoggerFactory.getLogger(SessionImpl.class); + + private boolean alive; + + /** + * Listeners (weak references) + */ + @SuppressWarnings("unchecked") + private final Map listeners = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK); + + private final Repository repository; + private final RepositoryConfig config; + private final WorkspaceImpl workspace; + + private final SessionInfo sessionInfo; + + private NamePathResolver npResolver; + private final NodeTypeManagerImpl ntManager; + + private final ValueFactory valueFactory; + + private final SessionItemStateManager itemStateManager; + private final ItemManager itemManager; + private final ItemStateValidator validator; + + SessionImpl(SessionInfo sessionInfo, Repository repository, RepositoryConfig config) + throws RepositoryException { + + alive = true; + this.repository = repository; + this.config = config; + this.sessionInfo = sessionInfo; + + workspace = createWorkspaceInstance(config, sessionInfo); + + // build local name-mapping + IdentifierResolver idResolver = new IdResolver(); + npResolver = new DefaultNamePathResolver(this, idResolver, true); + + // build ValueFactory + valueFactory = new ValueFactoryQImpl(config.getRepositoryService().getQValueFactory(), npResolver); + + // build nodetype manager + ntManager = new NodeTypeManagerImpl(workspace.getNodeTypeRegistry(), this); + validator = new ItemStateValidator(this, getPathFactory()); + + itemStateManager = createSessionItemStateManager(workspace.getUpdatableItemStateManager(), workspace.getItemStateFactory()); + HierarchyManager hMgr = getHierarchyManager(); + itemManager = createItemManager(hMgr); + + if (hMgr instanceof HierarchyManagerImpl) { + ((HierarchyManagerImpl) hMgr).setResolver(npResolver); + } + } + + //--------------------------------------------------< Session interface >--- + /** + * @see javax.jcr.Session#getRepository() + */ + public Repository getRepository() { + return repository; + } + + /** + * @see javax.jcr.Session#getUserID() + */ + public String getUserID() { + return sessionInfo.getUserID(); + } + + /** + * Always returns null. + * + * @see javax.jcr.Session#getAttribute(String) + */ + public Object getAttribute(String name) { + return null; + } + + /** + * Always returns an empty String array. + * + * @see javax.jcr.Session#getAttributeNames() + */ + public String[] getAttributeNames() { + return new String[0]; + + } + + /** + * @see javax.jcr.Session#getWorkspace() + */ + public Workspace getWorkspace() { + return workspace; + } + + /** + * @see javax.jcr.Session#impersonate(Credentials) + */ + @Override + public Session impersonate(Credentials credentials) throws LoginException, RepositoryException { + checkIsAlive(); + SessionInfo info = config.getRepositoryService().impersonate(sessionInfo, credentials); + try { + if (info instanceof XASessionInfo) { + return new XASessionImpl((XASessionInfo) info, repository, config); + } else { + return new SessionImpl(info, repository, config); + } + } catch (RepositoryException ex) { + config.getRepositoryService().dispose(info); + throw ex; + } + } + + /** + * @see javax.jcr.Session#getRootNode() + */ + public Node getRootNode() throws RepositoryException { + checkIsAlive(); + + NodeEntry re = getHierarchyManager().getRootEntry(); + return (Node) itemManager.getItem(re); + } + + /** + * @see javax.jcr.Session#getNodeByUUID(String) + */ + public Node getNodeByUUID(String uuid) throws ItemNotFoundException, RepositoryException { + // sanity check performed by getNodeById + Node node = getNodeById(getIdFactory().createNodeId(uuid)); + if (node instanceof NodeImpl && ((NodeImpl)node).isNodeType(NameConstants.MIX_REFERENCEABLE)) { + return node; + } else { + // fall back + String mixReferenceable = getNameResolver().getJCRName(NameConstants.MIX_REFERENCEABLE); + if (node.isNodeType(mixReferenceable)) { + return node; + } + // there is a node with that uuid but the node does not expose it + throw new ItemNotFoundException(uuid); + } + } + + /** + * Retrieve the Node with the given id. + * + * @param id + * @return node with the given NodeId. + * @throws ItemNotFoundException if no such node exists or if this + * Session does not have permission to access the node. + * @throws RepositoryException + */ + private Node getNodeById(NodeId id) throws ItemNotFoundException, RepositoryException { + // check sanity of this session + checkIsAlive(); + try { + NodeEntry nodeEntry = getHierarchyManager().getNodeEntry(id); + Item item = getItemManager().getItem(nodeEntry); + if (item.isNode()) { + return (Node) item; + } else { + log.error("NodeId '" + id + " does not point to a Node"); + throw new ItemNotFoundException(LogUtil.saveGetIdString(id, getPathResolver())); + } + } catch (AccessDeniedException e) { + throw new ItemNotFoundException(LogUtil.saveGetIdString(id, getPathResolver())); + } + } + + /** + * @see javax.jcr.Session#getItem(String) + */ + @Override + public Item getItem(String absPath) throws PathNotFoundException, RepositoryException { + checkIsAlive(); + try { + Path qPath = getQPath(absPath).getNormalizedPath(); + ItemManager itemMgr = getItemManager(); + if (itemMgr.nodeExists(qPath)) { + return itemMgr.getNode(qPath); + } else { + return itemMgr.getProperty(qPath); + } + } catch (AccessDeniedException ade) { + throw new PathNotFoundException(absPath); + } + } + + /** + * @see javax.jcr.Session#itemExists(String) + */ + @Override + public boolean itemExists(String absPath) throws RepositoryException { + checkIsAlive(); + Path qPath = getQPath(absPath).getNormalizedPath(); + ItemManager itemMgr = getItemManager(); + return itemMgr.nodeExists(qPath) || itemMgr.propertyExists(qPath); + } + + /** + * @see javax.jcr.Session#move(String, String) + */ + public void move(String srcAbsPath, String destAbsPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException { + checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + checkIsAlive(); + + // build paths from the given JCR paths. + Path srcPath = getQPath(srcAbsPath); + Path destPath = getQPath(destAbsPath); + + // all validation is performed by Move Operation and state-manager + Operation op = Move.create(srcPath, destPath, getHierarchyManager(), getPathResolver(), true); + itemStateManager.execute(op); + } + + /** + * @see javax.jcr.Session#save() + */ + public void save() throws AccessDeniedException, ConstraintViolationException, InvalidItemStateException, VersionException, LockException, RepositoryException { + checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + // delegate to the root node (including check for isAlive) + getRootNode().save(); + } + + /** + * @see javax.jcr.Session#refresh(boolean) + */ + public void refresh(boolean keepChanges) throws RepositoryException { + // delegate to the root node (including check for isAlive) + getRootNode().refresh(keepChanges); + } + + /** + * @see javax.jcr.Session#hasPendingChanges() + */ + public boolean hasPendingChanges() throws RepositoryException { + checkIsAlive(); + return itemStateManager.hasPendingChanges(); + } + + /** + * @see javax.jcr.Session#getValueFactory() + */ + public ValueFactory getValueFactory() throws UnsupportedRepositoryOperationException, RepositoryException { + // must throw UnsupportedRepositoryOperationException if writing is + // not supported + checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + return getJcrValueFactory(); + } + + /** + * @see javax.jcr.Session#checkPermission(String, String) + */ + public void checkPermission(String absPath, String actions) throws AccessControlException, RepositoryException { + if (!hasPermission(absPath, actions)) { + throw new AccessControlException("Access control violation: path = " + absPath + ", actions = " + actions); + } + } + + /** + * @see Session#getImportContentHandler(String, int) + */ + public ContentHandler getImportContentHandler(String parentAbsPath, int uuidBehavior) throws PathNotFoundException, ConstraintViolationException, VersionException, LockException, RepositoryException { + checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + checkIsAlive(); + + Path parentPath = getQPath(parentAbsPath); + // NOTE: check if path corresponds to Node and is writable is performed + // within the SessionImporter. + Importer importer = new SessionImporter(parentPath, this, itemStateManager, uuidBehavior); + return new ImportHandler(importer, getNamespaceResolver(), workspace.getNamespaceRegistry(), getNameFactory(), getPathFactory()); + } + + /** + * @see javax.jcr.Session#importXML(String, java.io.InputStream, int) + */ + @Override + public void importXML(String parentAbsPath, InputStream in, int uuidBehavior) throws IOException, PathNotFoundException, ItemExistsException, ConstraintViolationException, VersionException, InvalidSerializedDataException, LockException, RepositoryException { + // NOTE: checks are performed by 'getImportContentHandler' + ImportHandler handler = (ImportHandler) getImportContentHandler(parentAbsPath, uuidBehavior); + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature( + "http://xml.org/sax/features/namespace-prefixes", false); + + SAXParser parser = factory.newSAXParser(); + parser.parse(new InputSource(in), handler); + } catch (SAXException se) { + // check for wrapped repository exception + Exception e = se.getException(); + if (e != null && e instanceof RepositoryException) { + throw (RepositoryException) e; + } else { + String msg = "failed to parse XML stream"; + log.debug(msg); + throw new InvalidSerializedDataException(msg, se); + } + } catch (ParserConfigurationException e) { + throw new RepositoryException("SAX parser configuration error", e); + } finally { + in.close(); // JCR-2903 + } + } + + /** + * @see javax.jcr.Session#setNamespacePrefix(String, String) + */ + @Override + public void setNamespacePrefix(String prefix, String uri) + throws RepositoryException { + super.setNamespacePrefix(prefix, uri); + // Reset name and path caches + npResolver = new DefaultNamePathResolver(this, true); + } + + /** + * @see javax.jcr.Session#logout() + */ + @Override + public void logout() { + if (!alive) { + // ignore + return; + } + + // notify listeners that session is about to be closed + notifyLoggingOut(); + + // dispose session item state manager + itemStateManager.dispose(); + // dispose item manager + itemManager.dispose(); + // dispose workspace + workspace.dispose(); + + // invalidate session + alive = false; + // finally notify listeners that session has been closed + notifyLoggedOut(); + } + + /** + * @see javax.jcr.Session#isLive() + */ + public boolean isLive() { + return alive; + } + + /** + * @see javax.jcr.Session#addLockToken(String) + */ + public void addLockToken(String lt) { + try { + getLockStateManager().addLockToken(lt); + } catch (RepositoryException e) { + log.warn("Unable to add lock token '" +lt+ "' to this session.", e); + } + } + + /** + * @see javax.jcr.Session#getLockTokens() + */ + public String[] getLockTokens() { + try { + return getLockStateManager().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) + */ + public void removeLockToken(String lt) { + try { + getLockStateManager().removeLockToken(lt); + } catch (RepositoryException e) { + log.warn("Unable to remove lock token '" +lt+ "' from this session. (" + e.getMessage() + ")"); + } + } + + /** + * @see Session#getAccessControlManager() + */ + public AccessControlManager getAccessControlManager() throws RepositoryException { + checkSupportedOption(Repository.OPTION_ACCESS_CONTROL_SUPPORTED); + + return getAccessControlProvider().createAccessControlManager(sessionInfo, itemStateManager, itemManager, getItemDefinitionProvider(), getHierarchyManager(), npResolver); + } + + /** + * @see Session#getNode(String) + */ + @Override + public Node getNode(String absPath) throws RepositoryException { + checkIsAlive(); + try { + Path qPath = getQPath(absPath).getNormalizedPath(); + ItemManager itemMgr = getItemManager(); + return itemMgr.getNode(qPath); + } catch (AccessDeniedException ade) { + throw new PathNotFoundException(absPath); + } + } + + /** + * @see Session#getNodeByIdentifier(String) + */ + public Node getNodeByIdentifier(String id) throws RepositoryException { + return getNodeById(getIdFactory().fromJcrIdentifier(id)); + } + + /** + * @see Session#getProperty(String) + */ + @Override + public Property getProperty(String absPath) throws RepositoryException { + checkIsAlive(); + try { + Path qPath = getQPath(absPath).getNormalizedPath(); + ItemManager itemMgr = getItemManager(); + return itemMgr.getProperty(qPath); + } catch (AccessDeniedException ade) { + throw new PathNotFoundException(absPath); + } + } + + /** + * @see Session#getRetentionManager() + */ + public RetentionManager getRetentionManager() + throws UnsupportedRepositoryOperationException, RepositoryException { + checkSupportedOption(Repository.OPTION_RETENTION_SUPPORTED); + + // TODO: implementation missing + throw new UnsupportedRepositoryOperationException("JCR-1104"); + } + + /** + * @see Session#hasCapability(String, Object, Object[]) + */ + public boolean hasCapability(String methodName, Object target, Object[] arguments) + throws RepositoryException { + // most trivial implementation allowed by the specification. + return true; + } + + /** + * @see Session#hasPermission(String, String) + */ + public boolean hasPermission(String absPath, String actions) throws RepositoryException { + checkIsAlive(); + // build the array of actions to be checked + String[] actionsArr = actions.split(","); + + Path targetPath = getQPath(absPath); + + boolean isGranted; + // The given abs-path may point to a non-existing item + if (itemManager.nodeExists(targetPath)) { + NodeState nState = getHierarchyManager().getNodeState(targetPath); + isGranted = getAccessManager().isGranted(nState, actionsArr); + } else if (itemManager.propertyExists(targetPath)) { + PropertyState pState = getHierarchyManager().getPropertyState(targetPath); + isGranted = getAccessManager().isGranted(pState, actionsArr); + } else { + NodeState parentState = null; + Path parentPath = targetPath; + while (parentState == null) { + parentPath = parentPath.getAncestor(1); + if (itemManager.nodeExists(parentPath)) { + parentState = getHierarchyManager().getNodeState(parentPath); + } + } + // parentState is the nearest existing nodeState or the root state. + Path relPath = parentPath.computeRelativePath(targetPath); + isGranted = getAccessManager().isGranted(parentState, relPath, actionsArr); + } + return isGranted; + } + + /** + * @see Session#nodeExists(String) + */ + @Override + public boolean nodeExists(String absPath) throws RepositoryException { + checkIsAlive(); + Path qPath = getQPath(absPath).getNormalizedPath(); + ItemManager itemMgr = getItemManager(); + return itemMgr.nodeExists(qPath); + } + + /** + * @see Session#propertyExists(String) + */ + @Override + public boolean propertyExists(String absPath) throws RepositoryException { + checkIsAlive(); + Path qPath = getQPath(absPath).getNormalizedPath(); + ItemManager itemMgr = getItemManager(); + return itemMgr.propertyExists(qPath); + } + + /** + * @see Session#removeItem(String) + */ + @Override + public void removeItem(String absPath) throws RepositoryException { + Item item = getItem(absPath); + item.remove(); + } + + //--------------------------------------------------< NamespaceResolver >--- + /** + * @see NamespaceResolver#getPrefix(String) + */ + public String getPrefix(String uri) throws NamespaceException { + try { + return getNamespacePrefix(uri); + } catch (NamespaceException e) { + throw e; + } catch (RepositoryException e) { + throw new NamespaceException("Namespace not found: " + uri, e); + } + } + + /** + * @see NamespaceResolver#getURI(String) + */ + public String getURI(String prefix) throws NamespaceException { + try { + return getNamespaceURI(prefix); + } catch (NamespaceException e) { + throw e; + } catch (RepositoryException e) { + throw new NamespaceException("Namespace not found: " + prefix, e); + } + } + + //--------------------------------------< register and inform listeners >--- + /** + * Add a SessionListener + * + * @param listener the new listener to be informed on modifications + */ + public void addListener(SessionListener listener) { + if (!listeners.containsKey(listener)) { + listeners.put(listener, listener); + } + } + + /** + * Remove a SessionListener + * + * @param listener an existing listener + */ + public void removeListener(SessionListener listener) { + listeners.remove(listener); + } + + /** + * Notify the listeners that this session is about to be closed. + */ + private void notifyLoggingOut() { + // copy listeners to array to avoid ConcurrentModificationException + SessionListener[] la = listeners.values().toArray(new SessionListener[listeners.size()]); + for (SessionListener sl : la) { + if (sl != null) { + sl.loggingOut(this); + } + } + } + + /** + * Notify the listeners that this session has been closed. + */ + private void notifyLoggedOut() { + // copy listeners to array to avoid ConcurrentModificationException + SessionListener[] la = listeners.values().toArray(new SessionListener[listeners.size()]); + for (SessionListener sl : la) { + if (sl != null) { + sl.loggedOut(this); + } + } + } + + //-------------------------------------------------------< init methods >--- + protected WorkspaceImpl createWorkspaceInstance(RepositoryConfig config, SessionInfo sessionInfo) throws RepositoryException { + return new WorkspaceImpl(sessionInfo.getWorkspaceName(), this, config, sessionInfo); + } + + protected SessionItemStateManager createSessionItemStateManager(UpdatableItemStateManager workspaceStateManager, ItemStateFactory isf) throws RepositoryException { + return new SessionItemStateManager(workspaceStateManager, getValidator(), getQValueFactory(), isf, this); + } + + protected ItemManager createItemManager(HierarchyManager hierarchyManager) { + ItemCache cache = new ItemCacheImpl(config.getItemCacheSize()); + ItemManagerImpl imgr = new ItemManagerImpl(hierarchyManager, this, cache); + return imgr; + } + + //----------------------------------------------------< ManagerProvider >--- + /** + * @see ManagerProvider#getNamePathResolver() + */ + public NamePathResolver getNamePathResolver() { + return npResolver; + } + + /** + * @see ManagerProvider#getNameResolver() + */ + public NameResolver getNameResolver() { + return npResolver; + } + + /** + * @see ManagerProvider#getPathResolver() + */ + public PathResolver getPathResolver() { + return npResolver; + } + + /** + * @see ManagerProvider#getNamespaceResolver() + */ + public NamespaceResolver getNamespaceResolver() { + return this; + } + + /** + * @see ManagerProvider#getHierarchyManager() + */ + public HierarchyManager getHierarchyManager() { + return workspace.getHierarchyManager(); + } + + /** + * @see ManagerProvider#getLockStateManager() + */ + public LockStateManager getLockStateManager() { + return workspace.getLockStateManager(); + } + + /** + * @see ManagerProvider#getAccessManager() + */ + public AccessManager getAccessManager() { + return workspace.getAccessManager(); + } + + /** + * @see ManagerProvider#getVersionStateManager() + */ + public VersionManager getVersionStateManager() { + return workspace.getVersionStateManager(); + } + + /** + * @see ManagerProvider#getItemDefinitionProvider() + */ + public ItemDefinitionProvider getItemDefinitionProvider() { + return workspace.getItemDefinitionProvider(); + } + + /** + * @see ManagerProvider#getNodeTypeDefinitionProvider() + */ + public NodeTypeDefinitionProvider getNodeTypeDefinitionProvider() { + return ntManager; + } + + /** + * @see ManagerProvider#getEffectiveNodeTypeProvider() + */ + public EffectiveNodeTypeProvider getEffectiveNodeTypeProvider() { + return workspace.getEffectiveNodeTypeProvider(); + } + + /** + * @see ManagerProvider#getQValueFactory() + */ + public QValueFactory getQValueFactory() throws RepositoryException { + return config.getRepositoryService().getQValueFactory(); + } + + /** + * @see ManagerProvider#getAccessControlProvider() + */ + public AccessControlProvider getAccessControlProvider() throws RepositoryException { + return workspace.getAccessControlProvider(); + } + + /** + * @see ManagerProvider#getJcrValueFactory() + */ + public ValueFactory getJcrValueFactory() throws RepositoryException { + return valueFactory; + } + + //-------------------------------------------------------------------------- + + ItemManager getItemManager() { + return itemManager; + } + + // TODO public for SessionImport only. review + public ItemStateValidator getValidator() { + return validator; + } + + // TODO public for SessionImport only. review + public IdFactory getIdFactory() throws RepositoryException { + return workspace.getIdFactory(); + } + + public NameFactory getNameFactory() throws RepositoryException { + return workspace.getNameFactory(); + } + + PathFactory getPathFactory() throws RepositoryException { + return workspace.getPathFactory(); + } + + /** + * Returns the ItemStateManager associated with this session. + * + * @return the ItemStateManager associated with this session + */ + SessionItemStateManager getSessionItemStateManager() { + return itemStateManager; + } + + NodeTypeManagerImpl getNodeTypeManager() { + return ntManager; + } + + CacheBehaviour getCacheBehaviour() { + return config.getCacheBehaviour(); + } + + //-------------------------------------------------------------------------- + SessionImpl switchWorkspace(String workspaceName) throws AccessDeniedException, + NoSuchWorkspaceException, RepositoryException { + checkAccessibleWorkspace(workspaceName); + + SessionInfo info = config.getRepositoryService().obtain(sessionInfo, workspaceName); + if (info instanceof XASessionInfo) { + return new XASessionImpl((XASessionInfo) info, repository, config); + } else { + return new SessionImpl(info, repository, config); + } + } + + /** + * Builds a Path object from the given absolute JCR path string. + * + * @param absPath + * @return A Path object. + * @throws RepositoryException if the resulting path isn't absolute + * or if the given JCR path cannot be resolved to a path object. + */ + Path getQPath(String absPath) throws RepositoryException { + try { + Path p = getPathResolver().getQPath(absPath); + if (!p.isAbsolute()) { + throw new RepositoryException("Not an absolute path: " + absPath); + } + return p; + } catch (NameException mpe) { + String msg = "Invalid path: " + absPath; + log.debug(msg); + throw new RepositoryException(msg, mpe); + } + } + + /** + * Returns the NodeState of the given Node and asserts that the state is + * listed in the hierarchy built by this Session. If the version + * was obtained from a different session, the 'corresponding' version + * state for this session is retrieved. + * + * @param version + * @return the NodeState associated with the specified version. + */ + NodeState getVersionState(Version version) throws RepositoryException { + NodeState nodeState; + if (version.getSession() == this) { + nodeState = (NodeState) ((NodeImpl) version).getItemState(); + } else { + Path p = getQPath(version.getPath()); + Path parentPath = p.getAncestor(1); + HierarchyEntry parentEntry = getHierarchyManager().lookup(parentPath); + if (parentEntry != null) { + // make sure the parent entry is up to date + parentEntry.invalidate(false); + } + nodeState = getHierarchyManager().getNodeState(p); + } + return nodeState; + } + //------------------------------------------------------< check methods >--- + /** + * Performs a sanity check on this session. + * + * @throws RepositoryException if this session has been rendered invalid + * for some reason (e.g. if this session has been closed explicitly by logout) + */ + void checkIsAlive() throws RepositoryException { + // check session status + if (!alive) { + throw new RepositoryException("This session has been closed."); + } + } + + /** + * Returns true if the repository supports the given option. False otherwise. + * + * @param option Any of the option constants defined by {@link Repository} + * that either returns 'true' or 'false'. I.e. + *
          + *
        • {@link Repository#LEVEL_1_SUPPORTED}
        • + *
        • {@link Repository#LEVEL_2_SUPPORTED}
        • + *
        • {@link Repository#OPTION_ACCESS_CONTROL_SUPPORTED}
        • + *
        • {@link Repository#OPTION_ACTIVITIES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_BASELINES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_JOURNALED_OBSERVATION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_LIFECYCLE_SUPPORTED}
        • + *
        • {@link Repository#OPTION_LOCKING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED}
        • + *
        • {@link Repository#OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_OBSERVATION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_QUERY_SQL_SUPPORTED}
        • + *
        • {@link Repository#OPTION_RETENTION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_SHAREABLE_NODES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_SIMPLE_VERSIONING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_TRANSACTIONS_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UNFILED_CONTENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED}
        • + *
        • {@link Repository#OPTION_VERSIONING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_WORKSPACE_MANAGEMENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_XML_EXPORT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_XML_IMPORT_SUPPORTED}
        • + *
        • {@link Repository#WRITE_SUPPORTED}
        • + *
        + * @return true if the repository supports the given option. False otherwise. + */ + boolean isSupportedOption(String option) { + String desc = repository.getDescriptor(option); + // if the descriptors are not available return true. the missing + // functionality of the given SPI impl will in this case be detected + // upon the corresponding SPI call (see JCR-3143). + return (desc == null) ? true : Boolean.valueOf(desc); + } + + /** + * Make sure the repository supports the option indicated by the given string. + * + * @param option Any of the option constants defined by {@link Repository} + * that either returns 'true' or 'false'. I.e. + *
          + *
        • {@link Repository#LEVEL_1_SUPPORTED}
        • + *
        • {@link Repository#LEVEL_2_SUPPORTED}
        • + *
        • {@link Repository#OPTION_ACCESS_CONTROL_SUPPORTED}
        • + *
        • {@link Repository#OPTION_ACTIVITIES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_BASELINES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_JOURNALED_OBSERVATION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_LIFECYCLE_SUPPORTED}
        • + *
        • {@link Repository#OPTION_LOCKING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED}
        • + *
        • {@link Repository#OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_OBSERVATION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_QUERY_SQL_SUPPORTED}
        • + *
        • {@link Repository#OPTION_RETENTION_SUPPORTED}
        • + *
        • {@link Repository#OPTION_SHAREABLE_NODES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_SIMPLE_VERSIONING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_TRANSACTIONS_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UNFILED_CONTENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED}
        • + *
        • {@link Repository#OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED}
        • + *
        • {@link Repository#OPTION_VERSIONING_SUPPORTED}
        • + *
        • {@link Repository#OPTION_WORKSPACE_MANAGEMENT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_XML_EXPORT_SUPPORTED}
        • + *
        • {@link Repository#OPTION_XML_IMPORT_SUPPORTED}
        • + *
        • {@link Repository#WRITE_SUPPORTED}
        • + *
        + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + * @see javax.jcr.Repository#getDescriptorKeys() + */ + void checkSupportedOption(String option) throws UnsupportedRepositoryOperationException, RepositoryException { + if (!isSupportedOption(option)) { + throw new UnsupportedRepositoryOperationException(option + " is not supported by this repository."); + } + } + + /** + * Checks if this nodes session has pending changes. + * + * @throws InvalidItemStateException if this nodes session has pending changes + * @throws RepositoryException + */ + void checkHasPendingChanges() throws RepositoryException { + // check for pending changes + if (hasPendingChanges()) { + String msg = "Unable to perform operation. Session has pending changes."; + log.debug(msg); + throw new InvalidItemStateException(msg); + } + } + + /** + * Check if the the workspace with the given name exists and is accessible + * for this Session. + * + * @param workspaceName + * @throws NoSuchWorkspaceException + * @throws RepositoryException + */ + void checkAccessibleWorkspace(String workspaceName) throws NoSuchWorkspaceException, RepositoryException { + String[] wsps = workspace.getAccessibleWorkspaceNames(); + boolean accessible = false; + for (int i = 0; i < wsps.length && !accessible; i++) { + accessible = wsps[i].equals(workspaceName); + } + + if (!accessible) { + throw new NoSuchWorkspaceException("Unknown workspace: '" + workspaceName + "'."); + } + } + + //-------------------------------------------------------------------------- + /** + * Inner class implementing the IdentifierResolver interface + */ + private final class IdResolver implements IdentifierResolver { + + //---------------------------------------------< IdentifierResolver >--- + /** + * @see IdentifierResolver#getPath(String) + */ + public Path getPath(String identifier) throws MalformedPathException { + try { + NodeId id = getIdFactory().fromJcrIdentifier(identifier); + return getHierarchyManager().getNodeEntry(id).getPath(); + } catch (RepositoryException e) { + throw new MalformedPathException("Invalid identifier '" + identifier + "'."); + } + } + + /** + * @see IdentifierResolver#checkFormat(String) + */ + public void checkFormat(String identifier) throws MalformedPathException { + try { + getIdFactory().fromJcrIdentifier(identifier); + } catch (Exception e) { + throw new MalformedPathException("Invalid identifier '" + identifier + "'."); + } + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/SessionListener.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/SessionListener.java new file mode 100644 index 00000000000..73f292c4c96 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/SessionListener.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.jcr2spi; + +import javax.jcr.Session; + +/** + * The SessionListener interface allows an implementing + * object to be informed about changes on a Session. + * + * @see SessionImpl#addListener + */ +public interface SessionListener { + + /** + * Called when a Session is about to be 'closed' by + * calling {@link javax.jcr.Session#logout()}. At this + * moment the session is still valid. + * + * @param session the Session that is about to be 'closed' + */ + void loggingOut(Session session); + + /** + * Called when a Session has been 'closed' by + * calling {@link javax.jcr.Session#logout()}. + * + * @param session the Session that has been 'closed' + */ + void loggedOut(Session session); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/StaleProperty.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/StaleProperty.java new file mode 100644 index 00000000000..d2a0a2a5d75 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/StaleProperty.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.jcr2spi; + +import org.apache.jackrabbit.commons.AbstractProperty; + +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.Node; +import javax.jcr.Property; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.version.VersionException; +import java.math.BigDecimal; + +/** + * This implementation of {@link Property} throws an {@link InvalidItemStateException} on + * all method calls. + */ +public class StaleProperty extends AbstractProperty { + public void setValue(Binary value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + throw createIISE(); + } + + public void setValue(BigDecimal value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { + throw createIISE(); + } + + public Value getValue() throws ValueFormatException, RepositoryException { + throw createIISE(); + } + + public Value[] getValues() throws ValueFormatException, RepositoryException { + throw createIISE(); + } + + public Binary getBinary() throws ValueFormatException, RepositoryException { + throw createIISE(); + } + + public BigDecimal getDecimal() throws ValueFormatException, RepositoryException { + throw createIISE(); + } + + public PropertyDefinition getDefinition() throws RepositoryException { + throw createIISE(); + } + + public boolean isMultiple() throws RepositoryException { + throw createIISE(); + } + + public String getName() throws RepositoryException { + throw createIISE(); + } + + public Node getParent() throws ItemNotFoundException, AccessDeniedException, RepositoryException { + throw createIISE(); + } + + public Session getSession() throws RepositoryException { + throw createIISE(); + } + + public boolean isNew() { + return false; + } + + public boolean isModified() { + return false; + } + + public boolean isSame(Item otherItem) throws RepositoryException { + throw createIISE(); + } + + public void save() throws AccessDeniedException, ItemExistsException, ConstraintViolationException, InvalidItemStateException, ReferentialIntegrityException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException { + throw createIISE(); + } + + public void refresh(boolean keepChanges) throws InvalidItemStateException, RepositoryException { + throw createIISE(); + } + + private InvalidItemStateException createIISE() { + return new InvalidItemStateException("property does not exist anymore"); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/WorkspaceImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/WorkspaceImpl.java new file mode 100644 index 00000000000..59ce90de765 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/WorkspaceImpl.java @@ -0,0 +1,597 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.lock.LockManagerImpl; +import org.apache.jackrabbit.jcr2spi.lock.LockStateManager; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.observation.ObservationManagerImpl; +import org.apache.jackrabbit.jcr2spi.operation.Clone; +import org.apache.jackrabbit.jcr2spi.operation.Copy; +import org.apache.jackrabbit.jcr2spi.operation.Move; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.WorkspaceImport; +import org.apache.jackrabbit.jcr2spi.query.QueryManagerImpl; +import org.apache.jackrabbit.jcr2spi.security.AccessManager; +import org.apache.jackrabbit.jcr2spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.jcr2spi.state.ItemStateFactory; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.UpdatableItemStateManager; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.jcr2spi.version.VersionManagerImpl; +import org.apache.jackrabbit.jcr2spi.xml.WorkspaceContentHandler; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.ItemExistsException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PathNotFoundException; +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.lock.LockException; +import javax.jcr.lock.LockManager; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.observation.ObservationManager; +import javax.jcr.query.QueryManager; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import java.io.IOException; +import java.io.InputStream; + +/** + * WorkspaceImpl... + */ +public class WorkspaceImpl implements Workspace, ManagerProvider { + + private static Logger log = LoggerFactory.getLogger(WorkspaceImpl.class); + + /** + * The name of this Workspace. + */ + private final String name; + /** + * The Session that created this Workspace object. + */ + protected final SessionImpl session; + + /** + * WorkspaceManager acting as ItemStateManager on the workspace level + * and as connection to the SPI implementation. + */ + private final WorkspaceManager wspManager; + + private LockStateManager lockManager; + private ObservationManager obsManager; + private QueryManager qManager; + private VersionManager versionManager; + + private LockManager jcrLockManager; + private javax.jcr.version.VersionManager jcrVersionManager; + + public WorkspaceImpl(String name, SessionImpl session, RepositoryConfig config, SessionInfo sessionInfo) throws RepositoryException { + this.name = name; + this.session = session; + wspManager = createManager(config, sessionInfo); + } + + //----------------------------------------------------------< Workspace >--- + /** + * @see javax.jcr.Workspace#getSession() + */ + public Session getSession() { + return session; + } + + /** + * @see javax.jcr.Workspace#getName() + */ + public String getName() { + return name; + } + + /** + * @see javax.jcr.Workspace#copy(String, String) + */ + public void copy(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException { + session.checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + session.checkIsAlive(); + + // do within workspace copy + Path srcPath = session.getQPath(srcAbsPath); + Path destPath = session.getQPath(destAbsPath); + + Operation op = Copy.create(srcPath, destPath, getName(), this, this); + getUpdatableItemStateManager().execute(op); + } + + /** + * @see javax.jcr.Workspace#copy(String, String, String) + */ + public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException { + session.checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + session.checkIsAlive(); + + // check workspace name + if (getName().equals(srcWorkspace)) { + // same as current workspace, delegate to within workspace copy method + copy(srcAbsPath, destAbsPath); + return; + } + + // make sure the specified workspace is visible for the current session. + session.checkAccessibleWorkspace(srcWorkspace); + + Path srcPath = session.getQPath(srcAbsPath); + Path destPath = session.getQPath(destAbsPath); + + // copy (i.e. pull) subtree at srcAbsPath from srcWorkspace + // to 'this' workspace at destAbsPath + SessionImpl srcSession = null; + try { + // create session on other workspace for current subject + // (may throw NoSuchWorkspaceException and AccessDeniedException) + srcSession = session.switchWorkspace(srcWorkspace); + WorkspaceImpl srcWsp = (WorkspaceImpl) srcSession.getWorkspace(); + + // do cross-workspace copy + Operation op = Copy.create(srcPath, destPath, srcWsp.getName(), srcWsp, this); + getUpdatableItemStateManager().execute(op); + } finally { + if (srcSession != null) { + // we don't need the other session anymore, logout + srcSession.logout(); + } + } + } + + /** + * @see javax.jcr.Workspace#clone(String, String, String, boolean) + */ + public void clone(String srcWorkspace, String srcAbsPath, String destAbsPath, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException { + session.checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + session.checkIsAlive(); + + // check workspace name + if (getName().equals(srcWorkspace)) { + // same as current workspace + String msg = srcWorkspace + ": illegal workspace (same as current)"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // make sure the specified workspace is visible for the current session. + session.checkAccessibleWorkspace(srcWorkspace); + + Path srcPath = session.getQPath(srcAbsPath); + Path destPath = session.getQPath(destAbsPath); + + // clone (i.e. pull) subtree at srcAbsPath from srcWorkspace + // to 'this' workspace at destAbsPath + + SessionImpl srcSession = null; + try { + // create session on other workspace for current subject + // (may throw NoSuchWorkspaceException and AccessDeniedException) + srcSession = session.switchWorkspace(srcWorkspace); + WorkspaceImpl srcWsp = (WorkspaceImpl) srcSession.getWorkspace(); + + // do clone + Operation op = Clone.create(srcPath, destPath, srcWsp.getName(), removeExisting, srcWsp, this); + getUpdatableItemStateManager().execute(op); + } finally { + if (srcSession != null) { + // we don't need the other session anymore, logout + srcSession.logout(); + } + } + } + + /** + * @see javax.jcr.Workspace#move(String, String) + */ + public void move(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException { + session.checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + session.checkIsAlive(); + + Path srcPath = session.getQPath(srcAbsPath); + Path destPath = session.getQPath(destAbsPath); + + Operation op = Move.create(srcPath, destPath, getHierarchyManager(), getPathResolver(), false); + getUpdatableItemStateManager().execute(op); + } + + /** + * @see javax.jcr.Workspace#restore(Version[], boolean) + */ + public void restore(Version[] versions, boolean removeExisting) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException { + getVersionManager().restore(versions, removeExisting); + } + + /** + * @see javax.jcr.Workspace#getQueryManager() + */ + public QueryManager getQueryManager() throws RepositoryException { + session.checkIsAlive(); + if (qManager == null) { + qManager = new QueryManagerImpl(session, session, + session.getItemManager(), wspManager); + } + return qManager; + } + + /** + * @see javax.jcr.Workspace#getNamespaceRegistry() + */ + public NamespaceRegistry getNamespaceRegistry() throws RepositoryException { + session.checkIsAlive(); + return wspManager.getNamespaceRegistryImpl(); + } + + /** + * @see javax.jcr.Workspace#getNodeTypeManager() + */ + public NodeTypeManager getNodeTypeManager() throws RepositoryException { + session.checkIsAlive(); + return session.getNodeTypeManager(); + } + + /** + * @see javax.jcr.Workspace#getObservationManager() + */ + public ObservationManager getObservationManager() throws UnsupportedRepositoryOperationException, RepositoryException { + session.checkSupportedOption(Repository.OPTION_OBSERVATION_SUPPORTED); + session.checkIsAlive(); + + if (obsManager == null) { + obsManager = createObservationManager(getNamePathResolver(), getNodeTypeRegistry()); + } + return obsManager; + } + + /** + * @see javax.jcr.Workspace#getAccessibleWorkspaceNames() + */ + public String[] getAccessibleWorkspaceNames() throws RepositoryException { + session.checkIsAlive(); + return wspManager.getWorkspaceNames(); + } + + /** + * @see javax.jcr.Workspace#getImportContentHandler(String, int) + */ + public ContentHandler getImportContentHandler(String parentAbsPath, int uuidBehavior) + throws PathNotFoundException, ConstraintViolationException, VersionException, + LockException, RepositoryException { + + session.checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + session.checkIsAlive(); + + Path parentPath = session.getQPath(parentAbsPath); + NodeState parentState = getHierarchyManager().getNodeState(parentPath); + + // make sure the given import target is accessible, not locked and checked out. + int options = ItemStateValidator.CHECK_ACCESS | ItemStateValidator.CHECK_LOCK | ItemStateValidator.CHECK_VERSIONING; + getValidator().checkIsWritable(parentState, options); + + // build the content handler + return new WorkspaceContentHandler(this, parentAbsPath, uuidBehavior); + } + + /** + * @see javax.jcr.Workspace#importXML(String, InputStream, int) + */ + public void importXML(String parentAbsPath, InputStream in, int uuidBehavior) + throws IOException, PathNotFoundException, ItemExistsException, + ConstraintViolationException, InvalidSerializedDataException, + LockException, RepositoryException { + + session.checkSupportedOption(Repository.LEVEL_2_SUPPORTED); + session.checkIsAlive(); + + Path parentPath = session.getQPath(parentAbsPath); + NodeState parentState = getHierarchyManager().getNodeState(parentPath); + // make sure the given import target is accessible, not locked and checked out. + int options = ItemStateValidator.CHECK_ACCESS | ItemStateValidator.CHECK_LOCK | ItemStateValidator.CHECK_VERSIONING; + getValidator().checkIsWritable(parentState, options); + + try { + // run the import + wspManager.execute(WorkspaceImport.create(parentState, in, uuidBehavior)); + } finally { + in. close(); // JCR-2903 + } + } + + /** + * @see javax.jcr.Workspace#createWorkspace(String) + */ + public void createWorkspace(String name) throws RepositoryException { + session.checkIsAlive(); + session.checkSupportedOption(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED); + wspManager.createWorkspace(name, null); + } + + + /** + * @see javax.jcr.Workspace#createWorkspace(String, String) + */ + public void createWorkspace(String name, String srcWorkspace) throws RepositoryException { + session.checkIsAlive(); + session.checkSupportedOption(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED); + wspManager.createWorkspace(name, srcWorkspace); + } + + + /** + * @see javax.jcr.Workspace#deleteWorkspace(String) + */ + public void deleteWorkspace(String name) throws RepositoryException { + session.checkIsAlive(); + session.checkSupportedOption(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED); + wspManager.deleteWorkspace(name); + } + + + /** + * @see javax.jcr.Workspace#getLockManager() + */ + public LockManager getLockManager() throws RepositoryException { + session.checkIsAlive(); + session.checkSupportedOption(Repository.OPTION_LOCKING_SUPPORTED); + if (jcrLockManager == null) { + jcrLockManager = new JcrLockManager(session); + } + return jcrLockManager; + } + + /** + * @see javax.jcr.Workspace#getVersionManager() + */ + public synchronized javax.jcr.version.VersionManager getVersionManager() + throws RepositoryException { + session.checkIsAlive(); + session.checkSupportedOption(Repository.OPTION_VERSIONING_SUPPORTED); + if (jcrVersionManager == null) { + jcrVersionManager = new JcrVersionManager(session); + } + return jcrVersionManager; + } + + //----------------------------------------------------< ManagerProvider >--- + /** + * @see ManagerProvider#getNamePathResolver() + */ + public org.apache.jackrabbit.spi.commons.conversion.NamePathResolver getNamePathResolver() { + return session.getNamePathResolver(); + } + + /** + * @see ManagerProvider#getNameResolver() + */ + public NameResolver getNameResolver() { + return session.getNameResolver(); + } + + /** + * @see ManagerProvider#getPathResolver() + */ + public PathResolver getPathResolver() { + return session.getPathResolver(); + } + + /** + * @see ManagerProvider#getNamespaceResolver() + */ + public NamespaceResolver getNamespaceResolver() { + return session.getNamespaceResolver(); + } + + /** + * @see ManagerProvider#getHierarchyManager() + */ + public HierarchyManager getHierarchyManager() { + return wspManager.getHierarchyManager(); + } + + /** + * @see ManagerProvider#getAccessManager() + */ + public AccessManager getAccessManager() { + return wspManager; + } + + /** + * @see ManagerProvider#getLockStateManager() + */ + public LockStateManager getLockStateManager() { + if (lockManager == null) { + lockManager = createLockManager(wspManager, session.getItemManager()); + } + return lockManager; + } + + /** + * @see ManagerProvider#getVersionStateManager() + */ + public VersionManager getVersionStateManager() { + if (versionManager == null) { + versionManager = createVersionManager(wspManager); + } + return versionManager; + } + + /** + * @see ManagerProvider#getItemDefinitionProvider() + */ + public ItemDefinitionProvider getItemDefinitionProvider() { + return wspManager.getItemDefinitionProvider(); + } + + /** + * @see ManagerProvider#getNodeTypeDefinitionProvider() + */ + public NodeTypeDefinitionProvider getNodeTypeDefinitionProvider() { + return session.getNodeTypeManager(); + } + + /** + * @see ManagerProvider#getEffectiveNodeTypeProvider() + */ + public EffectiveNodeTypeProvider getEffectiveNodeTypeProvider() { + return wspManager.getEffectiveNodeTypeProvider(); + } + + /** + * @see ManagerProvider#getJcrValueFactory() + */ + public ValueFactory getJcrValueFactory() throws RepositoryException { + return session.getJcrValueFactory(); + } + + /** + * @see ManagerProvider#getQValueFactory() + */ + public QValueFactory getQValueFactory() throws RepositoryException { + return session.getQValueFactory(); + } + + /** + * @see ManagerProvider#getAccessControlProvider() () + */ + public AccessControlProvider getAccessControlProvider() throws RepositoryException { + return wspManager.getAccessControlProvider(); + } + + //------------------------------------< implementation specific methods >--- + void dispose() { + // NOTE: wspManager has already been disposed upon SessionItemStateManager.dispose() + } + + NameFactory getNameFactory() throws RepositoryException { + return wspManager.getNameFactory(); + } + + PathFactory getPathFactory() throws RepositoryException { + return wspManager.getPathFactory(); + } + + IdFactory getIdFactory() throws RepositoryException { + return wspManager.getIdFactory(); + } + + NodeTypeRegistry getNodeTypeRegistry() { + return wspManager.getNodeTypeRegistry(); + } + + /** + * Returns the state manager associated with the workspace + * represented by this WorkspaceImpl instance. + * + * @return the state manager of this workspace + */ + UpdatableItemStateManager getUpdatableItemStateManager() { + return wspManager; + } + + ItemStateFactory getItemStateFactory() { + return wspManager.getItemStateFactory(); + } + + /** + * Returns the validator of the session + * + * @return validator + */ + private ItemStateValidator getValidator() { + return session.getValidator(); + } + + //-----------------------------------------------------< initialization >--- + /** + * Create the workspace state manager. May be overridden by subclasses. + * + * @param config the RepositoryConfiguration + * @param sessionInfo the SessionInfo used to create this instance. + * @return workspace manager + * @throws javax.jcr.RepositoryException If an error occurs + */ + protected WorkspaceManager createManager(RepositoryConfig config, + SessionInfo sessionInfo) throws RepositoryException { + return new WorkspaceManager(config, sessionInfo, session.isSupportedOption(Repository.OPTION_OBSERVATION_SUPPORTED)); + } + + /** + * Create the LockManager. May be overridden by subclasses. + * + * @param wspManager the workspace manager. + * @param itemManager the item manager. + * @return a new LockStateManager instance. + */ + protected LockStateManager createLockManager(WorkspaceManager wspManager, ItemManager itemManager) { + LockManagerImpl lMgr = new LockManagerImpl(wspManager, itemManager, session.getCacheBehaviour()); + session.addListener(lMgr); + return lMgr; + } + + /** + * Create the VersionManager. May be overridden by subclasses. + * + * @param wspManager the workspace manager. + * @return a new VersionManager instance. + */ + protected VersionManager createVersionManager(WorkspaceManager wspManager) { + return new VersionManagerImpl(wspManager); + } + + /** + * Create the ObservationManager. May be overridden by subclasses. + * + * @param resolver the namespace resolver. + * @param ntRegistry the node type registry. + * @return a new ObservationManager instance + * @throws RepositoryException If an error occurs. + */ + protected ObservationManager createObservationManager(NamePathResolver resolver, NodeTypeRegistry ntRegistry) throws RepositoryException { + return new ObservationManagerImpl(wspManager, resolver, ntRegistry); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/WorkspaceManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/WorkspaceManager.java new file mode 100644 index 00000000000..8a560726597 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/WorkspaceManager.java @@ -0,0 +1,1265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Semaphore; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.MergeException; +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PathNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.config.CacheBehaviour; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEventListener; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManagerImpl; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProviderImpl; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeCache; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeRegistryImpl; +import org.apache.jackrabbit.jcr2spi.observation.InternalEventListener; +import org.apache.jackrabbit.jcr2spi.operation.AddLabel; +import org.apache.jackrabbit.jcr2spi.operation.AddNode; +import org.apache.jackrabbit.jcr2spi.operation.AddProperty; +import org.apache.jackrabbit.jcr2spi.operation.Checkin; +import org.apache.jackrabbit.jcr2spi.operation.Checkout; +import org.apache.jackrabbit.jcr2spi.operation.Checkpoint; +import org.apache.jackrabbit.jcr2spi.operation.Clone; +import org.apache.jackrabbit.jcr2spi.operation.Copy; +import org.apache.jackrabbit.jcr2spi.operation.CreateActivity; +import org.apache.jackrabbit.jcr2spi.operation.CreateConfiguration; +import org.apache.jackrabbit.jcr2spi.operation.LockOperation; +import org.apache.jackrabbit.jcr2spi.operation.LockRefresh; +import org.apache.jackrabbit.jcr2spi.operation.LockRelease; +import org.apache.jackrabbit.jcr2spi.operation.Merge; +import org.apache.jackrabbit.jcr2spi.operation.Move; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.OperationVisitor; +import org.apache.jackrabbit.jcr2spi.operation.Remove; +import org.apache.jackrabbit.jcr2spi.operation.RemoveActivity; +import org.apache.jackrabbit.jcr2spi.operation.RemoveLabel; +import org.apache.jackrabbit.jcr2spi.operation.RemoveVersion; +import org.apache.jackrabbit.jcr2spi.operation.ReorderNodes; +import org.apache.jackrabbit.jcr2spi.operation.ResolveMergeConflict; +import org.apache.jackrabbit.jcr2spi.operation.Restore; +import org.apache.jackrabbit.jcr2spi.operation.SetMixin; +import org.apache.jackrabbit.jcr2spi.operation.SetTree; +import org.apache.jackrabbit.jcr2spi.operation.SetPrimaryType; +import org.apache.jackrabbit.jcr2spi.operation.SetPropertyValue; +import org.apache.jackrabbit.jcr2spi.operation.Update; +import org.apache.jackrabbit.jcr2spi.operation.WorkspaceImport; +import org.apache.jackrabbit.jcr2spi.security.AccessManager; +import org.apache.jackrabbit.jcr2spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.jcr2spi.security.authorization.AccessControlProviderStub; +import org.apache.jackrabbit.jcr2spi.state.ChangeLog; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateFactory; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.jcr2spi.state.TransientISFactory; +import org.apache.jackrabbit.jcr2spi.state.TransientItemStateFactory; +import org.apache.jackrabbit.jcr2spi.state.UpdatableItemStateManager; +import org.apache.jackrabbit.jcr2spi.state.WorkspaceItemStateFactory; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeStorage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * WorkspaceManager... + */ +public class WorkspaceManager + implements UpdatableItemStateManager, NamespaceStorage, AccessManager { + + private static Logger log = LoggerFactory.getLogger(WorkspaceManager.class); + + private final RepositoryConfig config; + private final RepositoryService service; + private final SessionInfo sessionInfo; + private final NameFactory nameFactory; + private final PathFactory pathFactory; + + private final ItemStateFactory isf; + private final HierarchyManager hierarchyManager; + + private final boolean observationSupported; + + private final IdFactory idFactory; + private final NamespaceRegistryImpl nsRegistry; + private final NodeTypeRegistryImpl ntRegistry; + private final ItemDefinitionProvider definitionProvider; + + private AccessControlProvider acProvider; + + /** + * Semaphore to synchronize the feed thread with client + * threads that call {@link #execute(Operation)} or {@link + * #execute(ChangeLog)}. + */ + private final Semaphore updateSync = new Semaphore(1); + + /** + * This is the event polling for changes. If null + * then the underlying repository service does not support observation. + * It is also null if {@link CacheBehaviour#INVALIDATE} is + * configured and no event listeners have been registered. + */ + private Thread changeFeed; + + /** + * Flag that indicates that the changeFeed thread should be disposed. + */ + private volatile boolean disposeChangeFeed = false; + + /** + * List of event listener that are set on this WorkspaceManager to get + * notifications about local and external changes. + */ + private final List listeners = new LinkedList(); + + /** + * The current subscription for change events if there are listeners. + */ + private Subscription subscription; + + /** + * A cache for item infos as supplied by {@link RepositoryService#getItemInfoCache(SessionInfo)} + */ + private ItemInfoCache cache; + + public WorkspaceManager(RepositoryConfig config, SessionInfo sessionInfo, boolean observationSupported) + throws RepositoryException { + + this.config = config; + this.service = config.getRepositoryService(); + this.sessionInfo = sessionInfo; + this.observationSupported = observationSupported; + + this.nameFactory = service.getNameFactory(); + this.pathFactory = service.getPathFactory(); + + idFactory = service.getIdFactory(); + nsRegistry = new NamespaceRegistryImpl(this); + ntRegistry = createNodeTypeRegistry(nsRegistry); + definitionProvider = createDefinitionProvider(getEffectiveNodeTypeProvider()); + + TransientItemStateFactory stateFactory = createItemStateFactory(); + this.isf = stateFactory; + this.hierarchyManager = createHierarchyManager(stateFactory, idFactory); + + // If cache behavior is observation register a hierarchy listener which is + // notified about all changes. Otherwise just add a hierarchy listener which + // is only notified on changes for which client event listeners have been + // installed. Note: this listener has to be the first one called in order + // for the hierarchy to be consistent with the event (See JCR-2293). + InternalEventListener listener = createHierarchyListener(hierarchyManager); + CacheBehaviour cacheBehaviour = config.getCacheBehaviour(); + if (cacheBehaviour == CacheBehaviour.OBSERVATION) { + addEventListener(listener); + } else { + listeners.add(listener); + } + } + + public NamespaceRegistryImpl getNamespaceRegistryImpl() { + return nsRegistry; + } + + public NodeTypeRegistry getNodeTypeRegistry() { + return ntRegistry; + } + + public ItemDefinitionProvider getItemDefinitionProvider() { + return definitionProvider; + } + + public EffectiveNodeTypeProvider getEffectiveNodeTypeProvider() { + return ntRegistry; + } + + public HierarchyManager getHierarchyManager() { + return hierarchyManager; + } + + public String[] getWorkspaceNames() throws RepositoryException { + return service.getWorkspaceNames(sessionInfo); + } + + public IdFactory getIdFactory() { + return idFactory; + } + + public NameFactory getNameFactory() { + return nameFactory; + } + + public PathFactory getPathFactory() { + return pathFactory; + } + + public ItemStateFactory getItemStateFactory() { + return isf; + } + + /** + * Locates and instantiates an AccessControlProvider implementation. + * @return an access control manager provider. + * @throws RepositoryException + */ + public AccessControlProvider getAccessControlProvider() throws RepositoryException { + if (acProvider == null) { + acProvider = AccessControlProviderStub.newInstance(config); + } + return acProvider; + } + + public LockInfo getLockInfo(NodeId nodeId) throws RepositoryException { + return service.getLockInfo(sessionInfo, nodeId); + } + + /** + * Returns the lock tokens present with the SessionInfo. + * + * @return lock tokens present with the SessionInfo. + * @throws UnsupportedRepositoryOperationException If not supported. + * @throws RepositoryException If another error occurs. + * @see org.apache.jackrabbit.spi.SessionInfo#getLockTokens() + */ + public String[] getLockTokens() throws UnsupportedRepositoryOperationException, RepositoryException { + return sessionInfo.getLockTokens(); + } + + /** + * This method succeeds if the lock tokens could be added to the + * SessionInfo. + * + * @param lt The lock token to be added. + * @throws UnsupportedRepositoryOperationException If not supported. + * @throws LockException If a lock related exception occurs. + * @throws RepositoryException If another exception occurs. + * @see SessionInfo#addLockToken(String) + */ + public void addLockToken(String lt) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + sessionInfo.addLockToken(lt); + } + + /** + * Tries to remove the given token from the SessionInfo. + * + * @param lt The lock token to be removed. + * @throws UnsupportedRepositoryOperationException If not supported. + * @throws LockException If a lock related exception occurs, e.g if the + * session info does not contain the specified lock token. + * @throws RepositoryException If another exception occurs. + * @see SessionInfo#removeLockToken(String) + */ + public void removeLockToken(String lt) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + for (String token : sessionInfo.getLockTokens()) { + if (token.equals(lt)) { + sessionInfo.removeLockToken(lt); + return; + } + } + // sessionInfo doesn't contain the given lock token and is therefore + // not the lock holder + throw new LockException("Unable to remove locktoken '" + lt + "' from Session."); + } + + /** + * + * @return The names of the supported query languages. + * @throws RepositoryException If an error occurs. + */ + public String[] getSupportedQueryLanguages() throws RepositoryException { + return service.getSupportedQueryLanguages(sessionInfo); + } + + /** + * Checks if the query statement is valid. + * + * @param statement the query statement. + * @param language the query language. + * @param namespaces the locally remapped namespaces which might be used in + * the query statement. + * @return the bind variable names. + * @throws InvalidQueryException if the query statement is invalid. + * @throws RepositoryException if an error occurs while checking the query + * statement. + */ + public String[] checkQueryStatement(String statement, + String language, + Map namespaces) + throws InvalidQueryException, RepositoryException { + return service.checkQueryStatement(sessionInfo, statement, language, namespaces); + } + + /** + * @param statement the query statement. + * @param language the query language. + * @param namespaces the locally remapped namespaces which might be used in + * the query statement. + * @param limit The result size limit as specified by {@link javax.jcr.query.Query#setLimit(long)}. + * @param offset The result offset as specified by {@link javax.jcr.query.Query#setOffset(long)} + * @param boundValues The bound values as specified by {@link javax.jcr.query.Query#bindValue(String, javax.jcr.Value)} + * @return the QueryInfo created by {@link RepositoryService#executeQuery(org.apache.jackrabbit.spi.SessionInfo, String, String, java.util.Map, long, long, java.util.Map)}. + * @throws RepositoryException If an error occurs. + */ + public QueryInfo executeQuery(String statement, String language, Map namespaces, + long limit, long offset, Map boundValues) throws RepositoryException { + return service.executeQuery(sessionInfo, statement, language, namespaces, limit, offset, boundValues); + } + + /** + * Sets the InternalEventListener that gets notifications about + * local and external changes. + * + * @param listener the new listener. + * @throws RepositoryException if the listener cannot be registered. + */ + public void addEventListener(InternalEventListener listener) throws RepositoryException { + if (changeFeed == null) { + changeFeed = createChangeFeed(config.getPollTimeout(), observationSupported); + } + + synchronized (listeners) { + listeners.add(listener); + EventFilter[] filters = getEventFilters(listeners); + if (subscription == null) { + subscription = service.createSubscription(sessionInfo, filters); + } else { + service.updateEventFilters(subscription, filters); + } + listeners.notifyAll(); + } + } + + /** + * Updates the event filters on the subscription. The filters are retrieved + * from the current list of internal event listeners. + * + * @throws RepositoryException If an error occurs. + */ + public void updateEventFilters() throws RepositoryException { + synchronized (listeners) { + service.updateEventFilters(subscription, getEventFilters(listeners)); + } + } + + /** + * + * @param listener The listener to be removed. + * @throws RepositoryException If an error occurs. + */ + public void removeEventListener(InternalEventListener listener) + throws RepositoryException { + synchronized (listeners) { + listeners.remove(listener); + if (listeners.isEmpty()) { + service.dispose(subscription); + subscription = null; + } else { + service.updateEventFilters(subscription, getEventFilters(listeners)); + } + } + } + + /** + * Creates an event filter based on the parameters available in {@link + * javax.jcr.observation.ObservationManager#addEventListener}. + * + * @param eventTypes A combination of one or more event type constants + * encoded as a bitmask. + * @param path an absolute path. + * @param isDeep a boolean. + * @param uuids array of UUIDs. + * @param nodeTypes array of node type names. + * @param noLocal a boolean. + * @return the event filter instance with the given parameters. + * @throws UnsupportedRepositoryOperationException + * if this implementation does not support observation. + */ + public EventFilter createEventFilter(int eventTypes, Path path, boolean isDeep, + String[] uuids, Name[] nodeTypes, + boolean noLocal) + throws UnsupportedRepositoryOperationException, RepositoryException { + return service.createEventFilter(sessionInfo, eventTypes, path, isDeep, uuids, nodeTypes, noLocal); + } + + /** + * Returns the events from the journal that occurred after a given date. + * + * @param filter the event filter to apply. + * @param after a date in milliseconds. + * @return the events as a bundle. + * @throws RepositoryException if an error occurs. + * @throws UnsupportedRepositoryOperationException + * if the implementation does not support + * journaled observation. + */ + public EventBundle getEvents(EventFilter filter, long after) + throws RepositoryException, UnsupportedRepositoryOperationException { + return service.getEvents(sessionInfo, filter, after); + } + + /** + * + * @param userData The user data used for the event processing. + * @throws RepositoryException If an error occurs. + */ + public void setUserData(String userData) throws RepositoryException { + sessionInfo.setUserData(userData); + } + //-------------------------------------------------------------------------- + + /** + * Gets the event filters from the passed listener list. + * + * @param listeners the internal event listeners. + * @return Array of EventFilter + */ + private static EventFilter[] getEventFilters(Collection listeners) { + List filters = new ArrayList(); + for (InternalEventListener listener : listeners) { + filters.addAll(listener.getEventFilters()); + } + return filters.toArray(new EventFilter[filters.size()]); + } + + /** + * @return a new instance of TransientItemStateFactory. + * @throws RepositoryException If an error occurs. + */ + private TransientItemStateFactory createItemStateFactory() throws RepositoryException { + cache = service.getItemInfoCache(sessionInfo); + WorkspaceItemStateFactory isf = new WorkspaceItemStateFactory(service, sessionInfo, + getItemDefinitionProvider(), cache); + + TransientItemStateFactory tisf = new TransientISFactory(isf, getItemDefinitionProvider()); + return tisf; + } + + /** + * @param tisf The transient item state factory. + * @param idFactory The id factory. + * @return a new instance of HierarchyManager. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + private HierarchyManager createHierarchyManager(TransientItemStateFactory tisf, IdFactory idFactory) throws RepositoryException { + return new HierarchyManagerImpl(tisf, idFactory, getPathFactory()); + } + + /** + * @param hierarchyMgr The hierarchy manager. + * @return a new InternalEventListener + */ + private InternalEventListener createHierarchyListener(HierarchyManager hierarchyMgr) { + InternalEventListener listener = new HierarchyEventListener(this, hierarchyMgr, config.getCacheBehaviour()); + return listener; + } + + /** + * @param nsRegistry The namespace registry. + * @return an instance of NodeTypeRegistryImpl. + */ + private NodeTypeRegistryImpl createNodeTypeRegistry(NamespaceRegistry nsRegistry) { + NodeTypeStorage ntst = new NodeTypeStorage() { + public Iterator getAllDefinitions() throws RepositoryException { + return service.getQNodeTypeDefinitions(sessionInfo); + } + public Iterator getDefinitions(Name[] nodeTypeNames) throws NoSuchNodeTypeException, RepositoryException { + return service.getQNodeTypeDefinitions(sessionInfo, nodeTypeNames); + } + public void registerNodeTypes(QNodeTypeDefinition[] nodeTypeDefs, boolean allowUpdate) throws RepositoryException { + service.registerNodeTypes(sessionInfo, nodeTypeDefs, allowUpdate); + } + public void unregisterNodeTypes(Name[] nodeTypeNames) throws NoSuchNodeTypeException, RepositoryException { + service.unregisterNodeTypes(sessionInfo, nodeTypeNames); + } + }; + NodeTypeCache ntCache = NodeTypeCache.getInstance(service, sessionInfo.getUserID()); + ntst = ntCache.wrap(ntst); + return NodeTypeRegistryImpl.create(ntst, nsRegistry); + } + + /** + * @param entProvider The effective node type provider. + * @return a new instance of ItemDefinitionProvider. + */ + private ItemDefinitionProvider createDefinitionProvider(EffectiveNodeTypeProvider entProvider) { + return new ItemDefinitionProviderImpl(entProvider, service, sessionInfo); + } + + /** + * Creates a background thread which polls for external changes on the + * RepositoryService. + * + * @param pollTimeout the polling timeout in milliseconds. + * @param enableObservation if observation should be enabled. + * @return the background polling thread or null if the underlying + * RepositoryService does not support observation. + */ + private Thread createChangeFeed(int pollTimeout, boolean enableObservation) { + Thread t = null; + if (enableObservation) { + t = new Thread(new ChangePolling(pollTimeout)); + t.setName("Change Polling"); + t.setDaemon(true); + t.start(); + } + return t; + } + + //-----------------------------------------------------< wsp management >--- + /** + * Create a new workspace with the specified name. If + * srcWorkspaceName isn't null the content of + * that workspace is used as initial content, otherwise an empty workspace + * will be created. + * + * @param name The name of the workspace to be created. + * @param srcWorkspaceName The name of the workspace from which the initial + * content of the new workspace will be 'cloned'. + * @throws RepositoryException If an exception occurs. + */ + void createWorkspace(String name, String srcWorkspaceName) throws RepositoryException { + service.createWorkspace(sessionInfo, name, srcWorkspaceName); + } + + /** + * Deletes the workspace with the specified name. + * + * @param name The name of the workspace to be deleted. + * @throws RepositoryException If the operation fails. + */ + void deleteWorkspace(String name) throws RepositoryException { + service.deleteWorkspace(sessionInfo, name); + } + + //------------------------------------------< UpdatableItemStateManager >--- + /** + * Creates a new batch from the single workspace operation and executes it. + * + * @see UpdatableItemStateManager#execute(Operation) + */ + public void execute(Operation operation) throws RepositoryException { + // block event delivery while changes are executed + try { + updateSync.acquire(); + } catch (InterruptedException e) { + throw new RepositoryException(e); + } + try { + /* + Execute operation and delegate invalidation of affected item + states to the operation. + NOTE, that the invalidation is independent of the cache behaviour + due to the fact, that local event bundles are not processed by + the HierarchyEventListener. + */ + new OperationVisitorImpl(sessionInfo).execute(operation); + operation.persisted(); + } finally { + updateSync.release(); + } + } + + /** + * Creates a new batch from the given ChangeLog and executes it. + * + * @param changes The set of transient changes to be executed. + * @throws RepositoryException + */ + public void execute(ChangeLog changes) throws RepositoryException { + // block event delivery while changes are executed + try { + updateSync.acquire(); + } catch (InterruptedException e) { + throw new RepositoryException(e); + } + try { + new OperationVisitorImpl(sessionInfo).execute(changes); + changes.persisted(); + } finally { + updateSync.release(); + } + } + + /** + * Dispose this WorkspaceManager + */ + public synchronized void dispose() { + try { + updateSync.acquire(); + } catch (InterruptedException e) { + log.warn("Exception while disposing WorkspaceManager: " + e); + return; + } + try { + if (changeFeed != null) { + disposeChangeFeed = true; + changeFeed.interrupt(); + changeFeed.join(); + } + hierarchyManager.dispose(); + if (subscription != null) { + service.dispose(subscription); + } + service.dispose(sessionInfo); + cache.dispose(); + } catch (Exception e) { + log.warn("Exception while disposing WorkspaceManager: " + e); + } finally { + updateSync.release(); + } + ntRegistry.dispose(); + } + + //------------------------------------------------------< AccessManager >--- + /** + * @see AccessManager#isGranted(NodeState, Path, String[]) + */ + public boolean isGranted(NodeState parentState, Path relPath, String[] actions) + throws ItemNotFoundException, RepositoryException { + if (parentState.getStatus() == Status.NEW) { + return true; + } + // TODO: check again. + // build itemId from the given state and the relative path without + // making an attempt to retrieve the proper id of the item possibly + // identified by the resulting id. + // the server must be able to deal with paths and with proper ids anyway. + // TODO: 'createNodeId' is basically wrong since isGranted is unspecific for any item. + ItemId id = idFactory.createNodeId((NodeId) parentState.getWorkspaceId(), relPath); + return service.isGranted(sessionInfo, id, actions); + } + + /** + * @see AccessManager#isGranted(ItemState, String[]) + */ + public boolean isGranted(ItemState itemState, String[] actions) throws ItemNotFoundException, RepositoryException { + // a 'new' state can always be read, written and removed + if (itemState.getStatus() == Status.NEW) { + return true; + } + return service.isGranted(sessionInfo, itemState.getWorkspaceId(), actions); + } + + /** + * @see AccessManager#canRead(ItemState) + */ + public boolean canRead(ItemState itemState) throws ItemNotFoundException, RepositoryException { + // a 'new' state can always be read + if (itemState.getStatus() == Status.NEW) { + return true; + } + return service.isGranted(sessionInfo, itemState.getWorkspaceId(), AccessManager.READ); + } + + /** + * @see AccessManager#canRemove(ItemState) + */ + public boolean canRemove(ItemState itemState) throws ItemNotFoundException, RepositoryException { + // a 'new' state can always be removed again + if (itemState.getStatus() == Status.NEW) { + return true; + } + return service.isGranted(sessionInfo, itemState.getWorkspaceId(), AccessManager.REMOVE); + } + + /** + * @see AccessManager#canAccess(String) + */ + public boolean canAccess(String workspaceName) throws NoSuchWorkspaceException, RepositoryException { + for (String wspName : getWorkspaceNames()) { + if (wspName.equals(workspaceName)) { + return true; + } + } + return false; + } + + //---------------------------------------------------< NamespaceStorage >--- + + public Map getRegisteredNamespaces() throws RepositoryException { + return service.getRegisteredNamespaces(sessionInfo); + } + + public String getPrefix(String uri) throws NamespaceException, RepositoryException { + return service.getNamespacePrefix(sessionInfo, uri); + } + + public String getURI(String prefix) throws NamespaceException, RepositoryException { + return service.getNamespaceURI(sessionInfo, prefix); + } + + public void registerNamespace(String prefix, String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + service.registerNamespace(sessionInfo, prefix, uri); + } + + public void unregisterNamespace(String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + service.unregisterNamespace(sessionInfo, uri); + } + + //-------------------------------------------------------------------------- + /** + * Called when local or external events occurred. This method is called after + * changes have been applied to the repository. + * + * @param eventBundles the event bundles generated by the repository service + * as the effect of an local or external change. + * @param lstnrs Array of internal event listeners + * @throws InterruptedException if this thread is interrupted while waiting + * for the {@link #updateSync}. + */ + private void onEventReceived(EventBundle[] eventBundles, + InternalEventListener[] lstnrs) + throws InterruptedException { + if (log.isDebugEnabled()) { + log.debug("received {} event bundles.", eventBundles.length); + for (EventBundle eventBundle : eventBundles) { + log.debug("IsLocal: {}", eventBundle.isLocal()); + for (Iterator it = eventBundle.getEvents(); it.hasNext();) { + Event e = it.next(); + String type; + switch (e.getType()) { + case Event.NODE_ADDED: + type = "NodeAdded"; + break; + case Event.NODE_REMOVED: + type = "NodeRemoved"; + break; + case Event.PROPERTY_ADDED: + type = "PropertyAdded"; + break; + case Event.PROPERTY_CHANGED: + type = "PropertyChanged"; + break; + case Event.PROPERTY_REMOVED: + type = "PropertyRemoved"; + break; + case Event.NODE_MOVED: + type = "NodeMoved"; + break; + case Event.PERSIST: + type = "Persist"; + break; + default: + type = "Unknown"; + } + log.debug(" {}; {}", e.getPath(), type); + } + } + } + + // do not deliver events while an operation executes + updateSync.acquire(); + try { + // notify listener + for (EventBundle eventBundle : eventBundles) { + for (InternalEventListener lstnr : lstnrs) { + try { + lstnr.onEvent(eventBundle); + } catch (Exception e) { + log.warn("Exception in event polling thread: " + e); + log.debug("Dump:", e); + } + } + } + } finally { + updateSync.release(); + } + } + + /** + * Executes a sequence of operations on the repository service within + * a given SessionInfo. + */ + private final class OperationVisitorImpl implements OperationVisitor { + + /** + * The session info for all operations in this batch. + */ + private final SessionInfo sessionInfo; + + private Batch batch; + + private OperationVisitorImpl(SessionInfo sessionInfo) { + this.sessionInfo = sessionInfo; + } + + /** + * Executes the operations on the repository service. + * + * @param changeLog The changelog to be executed + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.nodetype.NoSuchNodeTypeException + * @throws javax.jcr.version.VersionException + */ + private void execute(ChangeLog changeLog) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + RepositoryException ex = null; + try { + ItemState target = changeLog.getTarget(); + batch = service.createBatch(sessionInfo, target.getId()); + for (Operation op : changeLog.getOperations()) { + log.debug("executing " + op.getName()); + op.accept(this); + } + } catch (RepositoryException e) { + ex = e; + } finally { + if (batch != null) { + try { + // submit must be called even in case there is an + // exception to give the service a chance to clean + // up the batch + service.submit(batch); + } catch (RepositoryException e) { + if (ex == null) { + ex = e; + } else { + log.warn("Exception submitting batch", e); + } + } + // reset batch field + batch = null; + } + } + if (ex != null) { + throw ex; + } + } + + /** + * Executes the operations on the repository service. + * + * @param workspaceOperation The workspace operation to be executed. + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.nodetype.NoSuchNodeTypeException + * @throws javax.jcr.version.VersionException + */ + private void execute(Operation workspaceOperation) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + log.debug("executing " + workspaceOperation.getName()); + workspaceOperation.accept(this); + } + + //-----------------------------------------------< OperationVisitor >--- + /** + * @see OperationVisitor#visit(AddNode) + */ + public void visit(AddNode operation) throws RepositoryException { + NodeId parentId = operation.getParentId(); + batch.addNode(parentId, operation.getNodeName(), operation.getNodeTypeName(), operation.getUuid()); + } + + /** + * @see OperationVisitor#visit(AddProperty) + */ + public void visit(AddProperty operation) throws RepositoryException { + NodeId parentId = operation.getParentId(); + Name propertyName = operation.getPropertyName(); + if (operation.isMultiValued()) { + batch.addProperty(parentId, propertyName, operation.getValues()); + } else { + QValue value = operation.getValues()[0]; + batch.addProperty(parentId, propertyName, value); + } + } + + /** + * @see OperationVisitor#visit(org.apache.jackrabbit.jcr2spi.operation.SetTree) + */ + public void visit(SetTree operation) throws RepositoryException { + NodeState treeState = operation.getTreeState(); + Tree tree = service.createTree(sessionInfo, batch, treeState.getName(), treeState.getNodeTypeName(), treeState.getUniqueID()); + populateTree(tree, treeState.getNodeEntry()); + batch.setTree(operation.getParentId(), tree); + } + + private void populateTree(Tree tree, NodeEntry nodeEntry) throws RepositoryException { + Iterator pEntries = nodeEntry.getPropertyEntries(); + while (pEntries.hasNext()) { + PropertyState ps = pEntries.next().getPropertyState(); + if (!NameConstants.JCR_PRIMARYTYPE.equals(ps.getName()) && !NameConstants.JCR_UUID.equals(ps.getName())) { + if (ps.isMultiValued()) { + tree.addProperty(ps.getParent().getNodeId(), ps.getName(), ps.getType(), ps.getValues()); + } else { + tree.addProperty(ps.getParent().getNodeId(), ps.getName(), ps.getType(), ps.getValue()); + } + } + } + + Iterator nEntries = nodeEntry.getNodeEntries(); + while (nEntries.hasNext()) { + NodeEntry child = nEntries.next(); + NodeState childState = child.getNodeState(); + Tree childTree = tree.addChild(childState.getName(), childState.getNodeTypeName(), childState.getUniqueID()); + populateTree(childTree, child); + } + } + + /** + * @see OperationVisitor#visit(Clone) + */ + public void visit(Clone operation) throws NoSuchWorkspaceException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + NodeId nId = operation.getNodeId(); + NodeId destParentId = operation.getDestinationParentId(); + service.clone(sessionInfo, operation.getWorkspaceName(), nId, destParentId, operation.getDestinationName(), operation.isRemoveExisting()); + } + + /** + * @see OperationVisitor#visit(Copy) + */ + public void visit(Copy operation) throws NoSuchWorkspaceException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + NodeId nId = operation.getNodeId(); + NodeId destParentId = operation.getDestinationParentId(); + service.copy(sessionInfo, operation.getWorkspaceName(), nId, destParentId, operation.getDestinationName()); + } + + /** + * @see OperationVisitor#visit(Move) + */ + public void visit(Move operation) throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + NodeId moveId = operation.getSourceId(); + NodeId destParentId = operation.getDestinationParentId(); + + if (batch == null) { + service.move(sessionInfo, moveId, destParentId, operation.getDestinationName()); + } else { + batch.move(moveId, destParentId, operation.getDestinationName()); + } + } + + /** + * @see OperationVisitor#visit(Update) + */ + public void visit(Update operation) throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException { + NodeId nId = operation.getNodeId(); + service.update(sessionInfo, nId, operation.getSourceWorkspaceName()); + } + + /** + * @see OperationVisitor#visit(Remove) + */ + public void visit(Remove operation) throws RepositoryException { + batch.remove(operation.getRemoveId()); + } + + /** + * @see OperationVisitor#visit(SetMixin) + */ + public void visit(SetMixin operation) throws RepositoryException { + batch.setMixins(operation.getNodeId(), operation.getMixinNames()); + } + + /** + * @see OperationVisitor#visit(SetPrimaryType) + */ + public void visit(SetPrimaryType operation) throws RepositoryException { + batch.setPrimaryType(operation.getNodeId(), operation.getPrimaryTypeName()); + } + + /** + * @see OperationVisitor#visit(SetPropertyValue) + */ + public void visit(SetPropertyValue operation) throws RepositoryException { + PropertyId id = operation.getPropertyId(); + if (operation.isMultiValued()) { + batch.setValue(id, operation.getValues()); + } else { + batch.setValue(id, operation.getValues()[0]); + } + } + + /** + * @see OperationVisitor#visit(ReorderNodes) + */ + public void visit(ReorderNodes operation) throws RepositoryException { + NodeId parentId = operation.getParentId(); + NodeId insertId = operation.getInsertId(); + NodeId beforeId = operation.getBeforeId(); + batch.reorderNodes(parentId, insertId, beforeId); + } + + /** + * @see OperationVisitor#visit(Checkout) + */ + public void visit(Checkout operation) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + if (operation.supportsActivity()) { + service.checkout(sessionInfo, operation.getNodeId(), operation.getActivityId()); + } else { + service.checkout(sessionInfo, operation.getNodeId()); + } + } + + /** + * @see OperationVisitor#visit(Checkin) + */ + public void visit(Checkin operation) throws UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + NodeId newId = service.checkin(sessionInfo, operation.getNodeId()); + operation.setNewVersionId(newId); + } + + /** + * @see OperationVisitor#visit(Checkpoint) + */ + public void visit(Checkpoint operation) throws UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + NodeId newId; + if (operation.supportsActivity()) { + newId = service.checkpoint(sessionInfo, operation.getNodeId(), operation.getActivityId()); + } else { + newId = service.checkpoint(sessionInfo, operation.getNodeId()); + } + operation.setNewVersionId(newId); + } + + /** + * @see OperationVisitor#visit(Restore) + */ + public void visit(Restore operation) throws VersionException, PathNotFoundException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + NodeId nId = operation.getNodeId(); + if (nId == null) { + service.restore(sessionInfo, operation.getVersionIds(), operation.removeExisting()); + } else { + NodeId targetId; + Path relPath = operation.getRelativePath(); + if (relPath != null) { + targetId = idFactory.createNodeId(nId, relPath); + } else { + targetId = nId; + } + NodeId versionId = operation.getVersionIds()[0]; + service.restore(sessionInfo, targetId, versionId, operation.removeExisting()); + } + } + + /** + * @see OperationVisitor#visit(Merge) + */ + public void visit(Merge operation) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + NodeId nId = operation.getNodeId(); + Iterator failed; + if (operation.isActivityMerge()) { + failed = service.mergeActivity(sessionInfo, nId); + } else { + failed = service.merge(sessionInfo, nId, operation.getSourceWorkspaceName(), operation.bestEffort(), operation.isShallow()); + } + operation.setFailedIds(failed); + } + + /** + * @see OperationVisitor#visit(ResolveMergeConflict) + */ + public void visit(ResolveMergeConflict operation) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + NodeId nId = operation.getNodeId(); + NodeId[] mergedFailedIds = operation.getMergeFailedIds(); + NodeId[] predecessorIds = operation.getPredecessorIds(); + service.resolveMergeConflict(sessionInfo, nId, mergedFailedIds, predecessorIds); + } + + /** + * @see OperationVisitor#visit(LockOperation) + */ + public void visit(LockOperation operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException { + LockInfo lInfo = service.lock(sessionInfo, operation.getNodeId(), operation.isDeep(), operation.isSessionScoped(), operation.getTimeoutHint(), operation.getOwnerHint()); + operation.setLockInfo(lInfo); + } + + /** + * @see OperationVisitor#visit(LockRefresh) + */ + public void visit(LockRefresh operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException { + service.refreshLock(sessionInfo, operation.getNodeId()); + } + + /** + * @see OperationVisitor#visit(LockRelease) + */ + public void visit(LockRelease operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException { + service.unlock(sessionInfo, operation.getNodeId()); + } + + /** + * @see OperationVisitor#visit(AddLabel) + */ + public void visit(AddLabel operation) throws VersionException, RepositoryException { + NodeId vhId = operation.getVersionHistoryId(); + NodeId vId = operation.getVersionId(); + service.addVersionLabel(sessionInfo, vhId, vId, operation.getLabel(), operation.moveLabel()); + } + + /** + * @see OperationVisitor#visit(RemoveLabel) + */ + public void visit(RemoveLabel operation) throws VersionException, RepositoryException { + NodeId vhId = operation.getVersionHistoryId(); + NodeId vId = operation.getVersionId(); + service.removeVersionLabel(sessionInfo, vhId, vId, operation.getLabel()); + } + + /** + * @see OperationVisitor#visit(RemoveVersion) + */ + public void visit(RemoveVersion operation) throws VersionException, AccessDeniedException, ReferentialIntegrityException, RepositoryException { + NodeId versionId = (NodeId) operation.getRemoveId(); + NodeState vhState = operation.getParentState(); + service.removeVersion(sessionInfo, (NodeId) vhState.getWorkspaceId(), versionId); + } + + /** + * @see OperationVisitor#visit(WorkspaceImport) + */ + public void visit(WorkspaceImport operation) throws RepositoryException { + service.importXml(sessionInfo, operation.getNodeId(), operation.getXmlStream(), operation.getUuidBehaviour()); + } + + /** + * @see OperationVisitor#visit(CreateActivity) + */ + public void visit(CreateActivity operation) throws RepositoryException { + NodeId activityId = service.createActivity(sessionInfo, operation.getTitle()); + operation.setNewActivityId(activityId); + } + + /** + * @see OperationVisitor#visit(RemoveActivity) + */ + public void visit(RemoveActivity operation) throws RepositoryException { + service.removeActivity(sessionInfo, (NodeId) operation.getRemoveId()); + } + + /** + * @see OperationVisitor#visit(CreateConfiguration) + */ + public void visit(CreateConfiguration operation) throws RepositoryException { + NodeId configId = service.createConfiguration(sessionInfo, operation.getNodeId()); + operation.setNewConfigurationId(configId); + } + } + + //------------------------------------------------------< ChangePolling >--- + /** + * Implements the polling for changes on the repository service. + */ + private final class ChangePolling implements Runnable { + + /** + * The polling timeout in milliseconds. + */ + private final int pollTimeout; + + /** + * Creates a new change polling with a given polling timeout. + * + * @param pollTimeout the timeout in milliseconds. + */ + private ChangePolling(int pollTimeout) { + this.pollTimeout = pollTimeout; + } + + public void run() { + String wspName = sessionInfo.getWorkspaceName(); + while (!Thread.interrupted() && !disposeChangeFeed) { + try { + InternalEventListener[] iel; + Subscription subscr; + synchronized (listeners) { + while (subscription == null) { + listeners.wait(); + } + iel = listeners.toArray(new InternalEventListener[listeners.size()]); + subscr = subscription; + } + + log.debug("calling getEvents() (Workspace={})", wspName); + EventBundle[] bundles = service.getEvents(subscr, pollTimeout); + log.debug("returned from getEvents() (Workspace={})", wspName); + // check if thread had been interrupted while + // getting events + if (Thread.interrupted() || disposeChangeFeed) { + log.debug("Thread interrupted, terminating..."); + break; + } + if (bundles.length > 0) { + onEventReceived(bundles, iel); + } + } catch (UnsupportedRepositoryOperationException e) { + log.error("SPI implementation does not support observation: " + e); + // terminate + break; + } catch (RepositoryException e) { + log.info("Workspace=" + wspName + ": Exception while retrieving event bundles: " + e); + log.debug("Dump:", e); + } catch (InterruptedException e) { + // terminate + break; + } + } + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/XASession.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/XASession.java new file mode 100644 index 00000000000..36a25949597 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/XASession.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.jcr2spi; + +import javax.jcr.Session; +import javax.transaction.xa.XAResource; + +/** + * The XASession interface extends the capability of + * Session by adding access to a JCR repository's support for + * the Java Transaction API (JTA). + *

        + * This support takes the form of a javax.transaction.xa.XAResource + * object. The functionality of this object closely resembles that defined by + * the standard X/Open XA Resource interface. + *

        + * This interface is used by the transaction manager; an application does not + * use it directly. + */ +public interface XASession extends Session { + + /** + * Retrieves an XAResource object that the transaction manager + * will use to manage this XASession object's participation in + * a distributed transaction. + * + * @return the XAResource object. + */ + XAResource getXAResource(); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/XASessionImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/XASessionImpl.java new file mode 100644 index 00000000000..36914412675 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/XASessionImpl.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.jcr2spi; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.transaction.xa.XAResource; + +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.spi.XASessionInfo; + +/** + * XASessionImpl extends the regular session implementation with + * access to the XAResource. + */ +public class XASessionImpl extends SessionImpl implements XASession { + + /** + * The XASessionInfo of this SessionImpl. + */ + private final XASessionInfo sessionInfo; + + /** + * Creates a new XASessionImpl. + * + * @param repository the repository instance associated with this session. + * @param sessionInfo the session info. + * @param config the underlying repository configuration. + * @throws RepositoryException if an error occurs while creating a session. + */ + XASessionImpl(XASessionInfo sessionInfo, Repository repository, + RepositoryConfig config) throws RepositoryException { + super(sessionInfo, repository, config); + this.sessionInfo = sessionInfo; + } + + //--------------------------------< XASession >----------------------------- + + /** + * @see org.apache.jackrabbit.jcr2spi.XASession#getXAResource() + */ + public XAResource getXAResource() { + return sessionInfo.getXAResource(); + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/config/CacheBehaviour.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/config/CacheBehaviour.java new file mode 100644 index 00000000000..29ec7077db8 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/config/CacheBehaviour.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.config; + +/** + * CacheBehaviour defines constants for the various cache + * maintenance strategies. The effective strategy depends on two factors, + * whether the repository implementation supports observation and the behaviour + * provided in the {@link RepositoryConfig}. + */ +public final class CacheBehaviour { + + /** + * Cache maintenance is done by invalidating affected items of an operation + * and forcing the jcr2spi implementation to reload the item states when + * they are accessed next time. No event listener is used for cache + * maintenance even though the repository implementation might support + * observation. + */ + public static final CacheBehaviour INVALIDATE = new CacheBehaviour(); + + /** + * Cache maintenance is done using events from the repository. After an + * operation has been executed on the RepositoryService events are retrieved + * from the repository and the cache is updated based on the returned + * events. This strategy requires that the repository implementation + * supports observation. + */ + public static final CacheBehaviour OBSERVATION = new CacheBehaviour(); + + private CacheBehaviour() { + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/config/RepositoryConfig.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/config/RepositoryConfig.java new file mode 100644 index 00000000000..5d77203efbe --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/config/RepositoryConfig.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.jcr2spi.config; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.RepositoryService; + +/** + * This class bundles the information required by JCR2SPI to + * bootstrap an SPI implementation. + *

        + * Instances of this class should implement + * {@link javax.naming.Referenceable} in order to make JCR2SPI's + * {@link javax.jcr.Repository} itself referenceable. + */ +public interface RepositoryConfig { + + public RepositoryService getRepositoryService() throws RepositoryException; + + public CacheBehaviour getCacheBehaviour(); + + public int getItemCacheSize(); + + /** + * Specifies an interval used for polling the {@link RepositoryService} for changes. + * @return the poll timeout in milliseconds. + */ + public int getPollTimeout(); + + public T getConfiguration(String name, T defaultValue); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeAttic.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeAttic.java new file mode 100644 index 00000000000..20db0dccacd --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeAttic.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.jcr2spi.hierarchy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.spi.Name; + +import java.util.Iterator; +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; + +/** + * ChildNodeAttic... + */ +class ChildNodeAttic { + + private static Logger log = LoggerFactory.getLogger(ChildNodeAttic.class); + + private Set attic = new HashSet(); + + ChildNodeAttic() { + } + + boolean isEmpty() { + return attic.isEmpty(); + } + + boolean contains(Name name, int index) { + for (NodeEntryImpl ne : attic) { + if (ne.matches(name, index)) { + return true; + } + } + return false; + } + + boolean contains(Name name, int index, String uniqueId) { + for (NodeEntryImpl ne : attic) { + if (uniqueId != null && uniqueId.equals(ne.getUniqueID())) { + return true; + } else if (ne.matches(name, index)) { + return true; + } + } + // not found + return false; + } + + List get(Name name) { + List l = new ArrayList(); + for (NodeEntryImpl ne : attic) { + if (ne.matches(name)) { + l.add(ne); + } + } + return l; + } + + /** + * + * @param name The original name of the NodeEntry before it has been moved. + * @param index The original index of the NodeEntry before it has been moved. + * @return + */ + NodeEntry get(Name name, int index) { + for (NodeEntryImpl ne : attic) { + if (ne.matches(name, index)) { + return ne; + } + } + // not found + return null; + } + + /** + * + * @param uniqueId + * @return + */ + NodeEntry get(String uniqueId) { + if (uniqueId == null) { + throw new IllegalArgumentException(); + } + for (NodeEntry ne : attic) { + if (uniqueId.equals(ne.getUniqueID())) { + return ne; + } + } + // not found + return null; + } + + void add(NodeEntryImpl movedEntry) { + attic.add(movedEntry); + } + + boolean remove(NodeEntry movedEntry) { + if (attic.contains(movedEntry)) { + return attic.remove(movedEntry); + } + return false; + } + + Iterator iterator() { + return attic.iterator(); + } + + void clear() { + if (attic != null) { + attic.clear(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeEntries.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeEntries.java new file mode 100644 index 00000000000..af0c6062179 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeEntries.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * ChildNodeEntries represents a collection of NodeEntrys that + * also maintains the index values of same-name siblings on insertion and removal. + */ +public interface ChildNodeEntries { + + /** + * @return true if this ChildNodeEntries have + * been updated or completely loaded without being invalidated in the + * mean time. + */ + boolean isComplete(); + + /** + * Reloads this ChildNodeEntries object. + * + * @throws ItemNotFoundException + * @throws RepositoryException + */ + void reload() throws ItemNotFoundException, RepositoryException; + + /** + * Returns an unmodifiable iterator over all NodeEntry objects present in + * this ChildNodeEntries collection irrespective of their status. + * + * @return Iterator over all NodeEntry object + */ + Iterator iterator(); + + /** + * Returns a List of NodeEntrys for the + * given nodeName. This method does not filter out + * removed NodeEntrys. + * + * @param nodeName the child node name. + * @return same name sibling nodes with the given nodeName. + */ + List get(Name nodeName); + + /** + * Returns the NodeEntry with the given + * nodeName and index. Note, that this method + * does not filter out removed NodeEntrys. + * + * @param nodeName name of the child node entry. + * @param index the index of the child node entry. + * @return the NodeEntry or null if there + * is no such NodeEntry. + */ + NodeEntry get(Name nodeName, int index); + + /** + * Return the NodeEntry that matches the given nodeName and + * uniqueID or null if no matching entry can be found. + * + * @param nodeName + * @param uniqueID + * @return + * @throws IllegalArgumentException if the given uniqueID is null. + */ + NodeEntry get(Name nodeName, String uniqueID); + + /** + * Adds a NodeEntry to the end of the list. Same as + * {@link #add(NodeEntry, int)}, where the index is {@link Path#INDEX_UNDEFINED}. + * + * @param cne the NodeEntry to add. + */ + void add(NodeEntry cne); + + /** + * Adds a NodeEntry.
        + * Note the following special cases: + *

          + *
        1. If an entry with the given index already exists, the the new sibling + * is inserted before.
        2. + *
        3. If the given index is bigger that the last entry in the siblings list, + * intermediate entries will be created.
        4. + *
        + * + * @param cne the NodeEntry to add. + */ + void add(NodeEntry cne, int index); + + /** + * Adds a the new NodeEntry before beforeEntry. + * + * @param entry + * @param index + * @param beforeEntry + */ + void add(NodeEntry entry, int index, NodeEntry beforeEntry); + + /** + * Removes the child node entry referring to the node state. + * + * @param childEntry the entry to be removed. + * @return the removed entry or null if there is no such entry. + */ + NodeEntry remove(NodeEntry childEntry); + + /** + * Reorders an existing NodeEntry before another + * NodeEntry. If beforeEntry is + * null insertEntry is moved to the end of the + * child node entries. + * + * @param insertEntry the NodeEntry to move. + * @param beforeEntry the NodeEntry where insertEntry is + * reordered to. + * @return the NodeEntry that followed the 'insertEntry' before the reordering. + * @throws NoSuchElementException if insertEntry or + * beforeEntry does not have a NodeEntry + * in this ChildNodeEntries. + */ + NodeEntry reorder(NodeEntry insertEntry, NodeEntry beforeEntry); + + /** + * Reorders an existing NodeEntry after another + * NodeEntry. If afterEntry is + * null insertEntry is moved to the beginning of + * the child node entries. + * + * @param insertEntry the NodeEntry to move. + * @param afterEntry the NodeEntry where insertEntry is + * reordered behind. + * @throws NoSuchElementException if insertEntry or + * afterEntry does not have a NodeEntry + * in this ChildNodeEntries. + */ + void reorderAfter(NodeEntry insertEntry, NodeEntry afterEntry); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeEntriesImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeEntriesImpl.java new file mode 100644 index 00000000000..8fb4637147e --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildNodeEntriesImpl.java @@ -0,0 +1,919 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.commons.collections.list.AbstractLinkedList; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ChildNodeEntriesImpl implements a memory sensitive implementation + * of the ChildNodeEntries interface. + */ +final class ChildNodeEntriesImpl implements ChildNodeEntries { + + private static Logger log = LoggerFactory.getLogger(ChildNodeEntriesImpl.class); + + private boolean complete = false; + + /** + * Linked list of {@link NodeEntry} instances. + */ + private final LinkedEntries entries = new LinkedEntries(); + + /** + * Map used for lookup by name. + */ + private final NameMap entriesByName = new NameMap(); + + private final NodeEntry parent; + private final EntryFactory factory; + + /** + * Create a new ChildNodeEntries collection from the given + * childNodeInfos instead of retrieving them from the + * persistent layer. + * + * @param parent + * @param factory + * @param childNodeInfos The complete list of child infos or + * null if an 'empty' ChildNodeEntriesImpl should be created. + * In the latter case, individual child entries will be added on demand + * and the complete list will be retrieved only to answer {@link #iterator()} + * if the passed boolean is true. + */ + ChildNodeEntriesImpl(NodeEntry parent, EntryFactory factory, Iterator childNodeInfos) { + this.parent = parent; + this.factory = factory; + + if (childNodeInfos != null) { + while (childNodeInfos.hasNext()) { + ChildInfo ci = childNodeInfos.next(); + NodeEntry entry = factory.createNodeEntry(parent, ci.getName(), ci.getUniqueID()); + add(entry, ci.getIndex()); + } + complete = true; + } else { + complete = false; + } + } + + /** + * @param childEntry + * @return The node entry that directly follows the given childEntry + * or null if the given childEntry has no successor + * or was not found in this ChildNodeEntries. + */ + NodeEntry getNext(NodeEntry childEntry) { + LinkedEntries.LinkNode ln = entries.getLinkNode(childEntry); + LinkedEntries.LinkNode nextLn = (ln == null) ? null : ln.getNextLinkNode(); + return (nextLn == null) ? null : nextLn.getNodeEntry(); + } + + /** + * @param childEntry + * @return The node entry that directly precedes the given childEntry + * or null if the given childEntry is the first + * or was not found in this ChildNodeEntries. + */ + NodeEntry getPrevious(NodeEntry childEntry) { + LinkedEntries.LinkNode ln = entries.getLinkNode(childEntry); + LinkedEntries.LinkNode prevLn = (ln == null) ? null : ln.getPreviousLinkNode(); + return (prevLn == null) ? null : prevLn.getNodeEntry(); + } + + /** + * @see ChildNodeEntries#isComplete() + */ + public boolean isComplete() { + return (parent.getStatus() != Status.INVALIDATED && complete) || + parent.getStatus() == Status.NEW || + Status.isTerminal(parent.getStatus()); + } + + /** + * @see ChildNodeEntries#reload() + */ + public synchronized void reload() throws ItemNotFoundException, RepositoryException { + if (isComplete()) { + // nothing to do + return; + } + + NodeId id = parent.getWorkspaceId(); + Iterator childNodeInfos = factory.getItemStateFactory().getChildNodeInfos(id); + update(childNodeInfos); + } + + /** + * Update the child node entries according to the child-infos obtained + * from the persistence layer. + * NOTE: the status of the entries already present is not respected. Thus + * new or removed entries are not touched in order not to modify the + * transient status of the parent. Operations that affect the set or order + * of child entries (AddNode, Move, Reorder) currently assert the + * completeness of the ChildNodeEntries, therefore avoiding an update + * resulting in inconsistent entries. + * + * @param childNodeInfos + * @see HierarchyEntry#reload(boolean) that ignores items with + * pending changes. + * @see org.apache.jackrabbit.jcr2spi.operation.AddNode + * @see org.apache.jackrabbit.jcr2spi.operation.Move + * @see org.apache.jackrabbit.jcr2spi.operation.ReorderNodes + */ + synchronized void update(Iterator childNodeInfos) { + // insert missing entries and reorder all if necessary. + LinkedEntries.LinkNode prevLN = null; + while (childNodeInfos.hasNext()) { + ChildInfo ci = childNodeInfos.next(); + LinkedEntries.LinkNode ln = entriesByName.getLinkNode(ci.getName(), ci.getIndex(), ci.getUniqueID()); + if (ln == null) { + // add missing at the correct position. + NodeEntry entry = factory.createNodeEntry(parent, ci.getName(), ci.getUniqueID()); + ln = internalAddAfter(entry, ci.getIndex(), prevLN); + } else if (prevLN != null) { + // assert correct order of existing + if (prevLN != ln) { + reorderAfter(ln, prevLN); + } else { + // there was an existing entry but it's the same as the one + // created/retrieved before. getting here indicates that + // the SPI implementation provided invalid childNodeInfos. + log.error("ChildInfo iterator contains multiple entries with the same name|index or uniqueID -> ignore ChildNodeInfo."); + } + } + prevLN = ln; + } + // finally reset the status + complete = true; + } + + /** + * @see ChildNodeEntries#iterator() + */ + public Iterator iterator() { + List l = new ArrayList(entries.size()); + for (Iterator it = entries.linkNodeIterator(); it.hasNext();) { + l.add(it.next().getNodeEntry()); + } + return Collections.unmodifiableList(l).iterator(); + } + + /** + * @see ChildNodeEntries#get(Name) + */ + public List get(Name nodeName) { + return entriesByName.getList(nodeName); + } + + /** + * @see ChildNodeEntries#get(Name, int) + */ + public NodeEntry get(Name nodeName, int index) { + if (index < Path.INDEX_DEFAULT) { + throw new IllegalArgumentException("index is 1-based"); + } + return entriesByName.getNodeEntry(nodeName, index); + } + + /** + * @see ChildNodeEntries#get(Name, String) + */ + public NodeEntry get(Name nodeName, String uniqueID) { + if (uniqueID == null || nodeName == null) { + throw new IllegalArgumentException(); + } + for (NodeEntry cne : get(nodeName)) { + if (uniqueID.equals(cne.getUniqueID())) { + return cne; + } + } + return null; + } + + /** + * Adds a NodeEntry to the end of the list. Same as + * {@link #add(NodeEntry, int)}, where the index is {@link Path#INDEX_UNDEFINED}. + * + * @param cne the NodeEntry to add. + * @see ChildNodeEntries#add(NodeEntry) + */ + public synchronized void add(NodeEntry cne) { + internalAdd(cne, Path.INDEX_UNDEFINED); + } + + /** + * @see ChildNodeEntries#add(NodeEntry, int) + */ + public synchronized void add(NodeEntry cne, int index) { + if (index < Path.INDEX_UNDEFINED) { + throw new IllegalArgumentException("Invalid index" + index); + } + internalAdd(cne, index); + } + + /** + * @see ChildNodeEntries#add(NodeEntry, int, NodeEntry) + */ + public synchronized void add(NodeEntry entry, int index, NodeEntry beforeEntry) { + if (beforeEntry != null) { + // the link node where the new entry is ordered before + LinkedEntries.LinkNode beforeLN = entries.getLinkNode(beforeEntry); + if (beforeLN == null) { + throw new NoSuchElementException(); + } + LinkedEntries.LinkNode insertLN = internalAdd(entry, index); + reorder(entry.getName(), insertLN, beforeLN); + } else { + // 'before' is null -> simply append new entry at the end + add(entry); + } + } + + /** + * + * @param entry + * @param index + * @return the LinkNode belonging to the added entry. + */ + private LinkedEntries.LinkNode internalAdd(NodeEntry entry, int index) { + Name nodeName = entry.getName(); + + // retrieve ev. sibling node with same index. if index is 'undefined' + // the existing entry is always null and no reordering occurs. + LinkedEntries.LinkNode existing = null; + if (index >= Path.INDEX_DEFAULT) { + existing = entriesByName.getLinkNode(nodeName, index); + } + + // in case index greater than default -> create intermediate entries. + // TODO: TOBEFIXED in case of orderable node the order in the 'linked-entries' must be respected. + for (int i = Path.INDEX_DEFAULT; i < index; i++) { + LinkedEntries.LinkNode previous = entriesByName.getLinkNode(nodeName, i); + if (previous == null) { + NodeEntry sibling = factory.createNodeEntry(parent, nodeName, null); + internalAdd(sibling, i); + } + } + + // add new entry + LinkedEntries.LinkNode ln = entries.add(entry, index); + entriesByName.put(nodeName, index, ln); + + // reorder the child entries if, the new entry must be inserted rather + // than appended at the end of the list. + if (existing != null) { + reorder(nodeName, ln, existing); + } + return ln; + } + + /** + * Add the specified new entry after the specified insertAfter. + * + * @param newEntry + * @param index + * @param insertAfter + * @return the LinkNode associated with the newEntry. + */ + private LinkedEntries.LinkNode internalAddAfter(NodeEntry newEntry, int index, + LinkedEntries.LinkNode insertAfter) { + LinkedEntries.LinkNode ln = entries.addAfter(newEntry, index, insertAfter); + entriesByName.put(newEntry.getName(), index, ln); + return ln; + } + + /** + * Removes the child node entry referring to the node state. + * + * @param childEntry the entry to be removed. + * @return the removed entry or null if there is no such entry. + * @see ChildNodeEntries#remove(NodeEntry) + */ + public synchronized NodeEntry remove(NodeEntry childEntry) { + LinkedEntries.LinkNode ln = entries.removeNodeEntry(childEntry); + if (ln != null) { + entriesByName.remove(childEntry.getName(), ln); + return childEntry; + } else { + return null; + } + } + + /** + * Reorders an existing NodeState before another + * NodeState. If beforeNode is + * null insertNode is moved to the end of the + * child node entries. + * + * @param insertEntry the NodeEntry to move. + * @param beforeEntry the NodeEntry where insertNode is + * reordered to. + * @return the NodeEntry that followed the 'insertNode' before the reordering. + * @throws NoSuchElementException if insertNode or + * beforeNode does not have a NodeEntry + * in this ChildNodeEntries. + * @see ChildNodeEntries#reorder(NodeEntry, NodeEntry) + */ + public synchronized NodeEntry reorder(NodeEntry insertEntry, NodeEntry beforeEntry) { + // the link node to move + LinkedEntries.LinkNode insertLN = entries.getLinkNode(insertEntry); + if (insertLN == null) { + throw new NoSuchElementException(); + } + // the link node where insertLN is ordered before + LinkedEntries.LinkNode beforeLN = (beforeEntry != null) ? entries.getLinkNode(beforeEntry) : null; + if (beforeEntry != null && beforeLN == null) { + throw new NoSuchElementException(); + } + + NodeEntry previousBefore = insertLN.getNextLinkNode().getNodeEntry(); + if (previousBefore != beforeEntry) { + reorder(insertEntry.getName(), insertLN, beforeLN); + } + return previousBefore; + } + + /** + * @see ChildNodeEntries#reorderAfter(NodeEntry, NodeEntry) + */ + public void reorderAfter(NodeEntry insertEntry, NodeEntry afterEntry) { + // the link node to move + LinkedEntries.LinkNode insertLN = entries.getLinkNode(insertEntry); + if (insertLN == null) { + throw new NoSuchElementException(); + } + // the link node where insertLN is ordered before + LinkedEntries.LinkNode afterLN = (afterEntry != null) ? entries.getLinkNode(afterEntry) : null; + if (afterEntry != null && afterLN == null) { + throw new NoSuchElementException(); + } + + LinkedEntries.LinkNode previousLN = insertLN.getPreviousLinkNode(); + if (previousLN != afterLN) { + reorderAfter(insertLN, afterLN); + } // else: already in correct position. nothing to do + } + + /** + * + * @param insertName + * @param insertLN + * @param beforeLN + */ + private void reorder(Name insertName, LinkedEntries.LinkNode insertLN, + LinkedEntries.LinkNode beforeLN) { + // reorder named map + if (entriesByName.containsSiblings(insertName)) { + int position; + if (beforeLN == null) { + // reorder to the end -> use illegal position as marker + position = - 1; + } else { + // count all SNS-entries that are before 'beforeLN' in order to + // determine the new position of the reordered node regarding + // his siblings. + position = 0; + for (Iterator it = entries.linkNodeIterator(); it.hasNext(); ) { + LinkedEntries.LinkNode ln = it.next(); + if (ln == beforeLN) { + break; + } else if (ln != insertLN && insertName.equals(ln.qName)) { + position++; + } // else: ln == insertLN OR no SNS -> not relevant for position count + } + } + entriesByName.reorder(insertName, insertLN, position); + } + // reorder in linked list + entries.reorderNode(insertLN, beforeLN); + } + + /** + * + * @param insertLN + * @param afterLN + */ + private void reorderAfter(LinkedEntries.LinkNode insertLN, LinkedEntries.LinkNode afterLN) { + // the link node to move + if (insertLN == null) { + throw new NoSuchElementException(); + } + // the link node where insertLN is ordered after + if (afterLN == null) { + // move to first position + afterLN = entries.getHeader(); + } + + LinkedEntries.LinkNode currentAfter = afterLN.getNextLinkNode(); + if (currentAfter == insertLN) { + log.debug("Already ordered behind 'afterEntry'."); + // nothing to do + return; + } else { + // reorder named map + Name insertName = insertLN.qName; + if (entriesByName.containsSiblings(insertName)) { + int position = -1; // default: reorder to the end. + if (afterLN == entries.getHeader()) { + // move to the beginning + position = 0; + } else { + // count all SNS-entries that are before 'afterLN' in order to + // determine the new position of the reordered node regarding + // his siblings. + position = 0; + for (Iterator it = entries.linkNodeIterator(); it.hasNext(); ) { + LinkedEntries.LinkNode ln = it.next(); + if (insertName.equals(ln.qName) && (ln != insertLN)) { + position++; + } + if (ln == afterLN) { + break; + } + } + } + entriesByName.reorder(insertName, insertLN, position); + } + // reorder in linked list + entries.reorderNode(insertLN, currentAfter); + } + } + + //-------------------------------------------------< AbstractLinkedList >--- + /** + * An implementation of a linked list which provides access to the internal + * LinkNode which links the entries of the list. + */ + private final class LinkedEntries extends AbstractLinkedList { + + LinkedEntries() { + super(); + init(); + } + + /** + * Returns the matching LinkNode from a list or a single + * LinkNode. This method will return null + * if none of the entries matches either due to missing entry for given + * state name or due to missing availability of the NodeEntry. + * + * @param nodeEntry the NodeEntry that is compared to the + * resolution of any NodeEntry that matches by name. + * @return the matching LinkNode or null + */ + private LinkedEntries.LinkNode getLinkNode(NodeEntry nodeEntry) { + for (Iterator it = linkNodeIterator(); it.hasNext();) { + LinkedEntries.LinkNode ln = it.next(); + if (ln.getNodeEntry() == nodeEntry) { + return ln; + } + } + // not found + return null; + } + + private LinkedEntries.LinkNode getHeader() { + return (LinkedEntries.LinkNode) header; + } + + /** + * Adds a child node entry at the end of this list. + * + * @param cne the child node entry to add. + * @param index + * @return the LinkNode which refers to the added NodeEntry. + */ + LinkedEntries.LinkNode add(NodeEntry cne, int index) { + LinkedEntries.LinkNode ln = new LinkedEntries.LinkNode(cne, index); + addNode(ln, header); + return ln; + } + + /** + * Adds the given child node entry to this list after the specified + * entry or at the beginning if entry is + * null. + * + * @param cne the child node entry to add. + * @param index + * @param insertAfter after which to insert the new entry + * @return the LinkNode which refers to the added NodeEntry. + */ + LinkedEntries.LinkNode addAfter(NodeEntry cne, int index, LinkedEntries.LinkNode insertAfter) { + LinkedEntries.LinkNode newNode; + if (insertAfter == null) { + // insert at the beginning + newNode = new LinkedEntries.LinkNode(cne, index); + addNode(newNode, header); + } else if (insertAfter.getNextLinkNode() == null) { + newNode = add(cne, index); + } else { + newNode = new LinkedEntries.LinkNode(cne, index); + addNode(newNode, insertAfter.getNextLinkNode()); + } + return newNode; + } + + /** + * Remove the LinkEntry the contains the given NodeEntry as value. + * + * @param cne NodeEntry to be removed. + * @return LinkedEntries.LinkNode that has been removed. + */ + LinkedEntries.LinkNode removeNodeEntry(NodeEntry cne) { + LinkedEntries.LinkNode ln = getLinkNode(cne); + if (ln != null) { + ln.remove(); + } + return ln; + } + + /** + * Reorders an existing LinkNode before another existing + * LinkNode. If before is null + * the insert node is moved to the end of the list. + * + * @param insert the node to reorder. + * @param before the node where to reorder node insert. + */ + void reorderNode(LinkedEntries.LinkNode insert, LinkedEntries.LinkNode before) { + removeNode(insert); + if (before == null) { + addNode(insert, header); + } else { + addNode(insert, before); + } + } + + /** + * Create a new LinkNode for a given {@link NodeEntry} + * value. + * + * @param value a child node entry. + * @return a wrapping {@link LinkedEntries.LinkNode}. + * @see AbstractLinkedList#createNode(Object) + */ + @Override + protected Node createNode(Object value) { + return new LinkedEntries.LinkNode(value, Path.INDEX_DEFAULT); + } + + /** + * @return a new LinkNode. + * @see AbstractLinkedList#createHeaderNode() + */ + @Override + protected Node createHeaderNode() { + return new LinkedEntries.LinkNode(); + } + + /** + * @return iterator over all LinkNode entries in this list. + */ + private Iterator linkNodeIterator() { + return new LinkNodeIterator(); + } + + //---------------------------------------------------------------------- + /** + * Extends the AbstractLinkedList.Node. + */ + private final class LinkNode extends Node { + + private final Name qName; + + protected LinkNode() { + super(); + qName = null; + } + + protected LinkNode(Object value, int index) { + // add soft reference from linkNode to the NodeEntry (value) + // unless the entry is a SNSibling. TODO: review again. + super(index > Path.INDEX_DEFAULT ? value : new SoftReference(value)); + qName = ((NodeEntry) value).getName(); + } + + @Override + protected void setValue(Object value) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + protected Object getValue() { + Object val = super.getValue(); + NodeEntry ne; + if (val == null) { + ne = null; + } else if (val instanceof Reference) { + ne = (NodeEntry) ((Reference) val).get(); + } else { + ne = (NodeEntry) val; + } + // if the nodeEntry has been g-collected in the mean time + // create a new NodeEntry in order to avoid returning null. + if (ne == null && this != header) { + ne = factory.createNodeEntry(parent, qName, null); + super.setValue(new SoftReference(ne)); + } + return ne; + } + + /** + * @return the wrapped NodeEntry. + */ + public NodeEntry getNodeEntry() { + return (NodeEntry) getValue(); + } + + /** + * Removes this LinkNode from the linked list. + */ + public void remove() { + removeNode(this); + } + + /** + * @return the next LinkNode. + */ + public LinkedEntries.LinkNode getNextLinkNode() { + return (LinkedEntries.LinkNode) super.getNextNode(); + } + + /** + * @return the next LinkNode. + */ + public LinkedEntries.LinkNode getPreviousLinkNode() { + return (LinkedEntries.LinkNode) super.getPreviousNode(); + } + } + + //---------------------------------------------------------------------- + private class LinkNodeIterator implements Iterator { + + private LinkedEntries.LinkNode next = ((LinkedEntries.LinkNode) header).getNextLinkNode(); + private final int expectedModCount = modCount; + + public boolean hasNext() { + checkModCount(); + return next != header; + } + + public LinkedEntries.LinkNode next() { + checkModCount(); + if (!hasNext()) { + throw new NoSuchElementException(); + } + LinkedEntries.LinkNode n = next; + next = next.getNextLinkNode(); + return n; + } + + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + private void checkModCount() { + if (expectedModCount != modCount) { + throw new ConcurrentModificationException(); + } + } + } + } + + + + //-------------------------------------------------------------------------- + /** + * Mapping of Name to LinkNode OR List of LinkNode(s) in case of SNSiblings. + */ + private static class NameMap { + + private final Map> snsMap = new HashMap>(); + private final Map nameMap = new HashMap(); + + /** + * Return true if more than one NodeEntry with the given name exists. + * + * @param qName + * @return true if more than one NodeEntry with the given name exists. + */ + public boolean containsSiblings(Name qName) { + return snsMap.containsKey(qName); + } + + /** + * Returns a single NodeEntry or an unmodifiable + * List of NodeEntry objects. + * + * @param qName + * @return a single NodeEntry or a List of + * NodeEntry objects. + */ + private Object get(Name qName) { + LinkedEntries.LinkNode val = nameMap.get(qName); + if (val != null) { + return val.getNodeEntry(); + } else { + List l = snsMap.get(qName); + if (l != null) { + List nodeEntries = new ArrayList(l.size()); + for (Iterator it = l.iterator(); it.hasNext();) { + LinkedEntries.LinkNode ln = it.next(); + nodeEntries.add(ln.getNodeEntry()); + } + return nodeEntries; + } + } + return null; + } + + /** + * Returns a unmodifiable List of NodeEntry objects even if the name map + * only contains a single entry for the given name. If no matching entry + * exists for the given Name an empty list is returned. + * + * @param name + * @return list of entries or an empty list. + */ + @SuppressWarnings("unchecked") + public List getList(Name name) { + Object obj = get(name); + if (obj == null) { + return Collections.emptyList(); + } else if (obj instanceof List) { + List l = new ArrayList((List) obj); + return Collections.unmodifiableList(l); + } else { + // NodeEntry + return Collections.singletonList((NodeEntry)obj); + } + } + + @SuppressWarnings("unchecked") + public NodeEntry getNodeEntry(Name name, int index) { + Object obj = get(name); + if (obj == null) { + return null; + } + if (obj instanceof List) { + // map entry is a list of siblings + return findMatchingEntry((List) obj, index); + } else { + // map entry is a single child node entry + if (index == Path.INDEX_DEFAULT) { + return (NodeEntry) obj; + } + } + return null; + } + + public LinkedEntries.LinkNode getLinkNode(Name name, int index) { + if (index < Path.INDEX_DEFAULT) { + throw new IllegalArgumentException("Illegal index " + index); + } + + LinkedEntries.LinkNode val = nameMap.get(name); + if (val != null) { + return (index == Path.INDEX_DEFAULT) ? val : null; + } else { + // look in snsMap + List l = snsMap.get(name); + int pos = index - 1; // Index of NodeEntry is 1-based + return (l != null && pos < l.size()) ? l.get(pos) : null; + } + } + + public LinkedEntries.LinkNode getLinkNode(Name name, int index, String uniqueID) { + if (uniqueID != null) { + // -> try if any entry matches. + // if none matches it be might that entry doesn't have uniqueID + // set yet -> search without uniqueID + LinkedEntries.LinkNode val = nameMap.get(name); + if (val != null) { + if (uniqueID.equals(val.getNodeEntry().getUniqueID())) { + return val; + } + } else { + // look in snsMap + List l = snsMap.get(name); + if (l != null) { + for (Iterator it = l.iterator(); it.hasNext();) { + LinkedEntries.LinkNode ln = it.next(); + if (uniqueID.equals(ln.getNodeEntry().getUniqueID())) { + return ln; + } + } + } + } + } + // no uniqueID passed or not match. + // try to load the child entry by name and index. + return getLinkNode(name, index); + } + + public void put(Name name, int index, LinkedEntries.LinkNode value) { + // if 'nameMap' already contains a single entry -> move it to snsMap + LinkedEntries.LinkNode single = nameMap.remove(name); + List l; + if (single != null) { + l = new ArrayList(); + l.add(single); + snsMap.put(name, l); + } else { + // if 'snsMap' already contains list + l = snsMap.get(name); + } + + if (l == null) { + // no same name siblings -> simply put to the name map. + nameMap.put(name, value); + } else { + // sibling(s) already present -> insert into the list + int position = index - 1; + if (position < 0 || position > l.size()) { + l.add(value); // invalid position -> append at the end. + } else { + l.add(position, value); // insert with the correct index. + } + } + } + + public LinkedEntries.LinkNode remove(Name name, LinkedEntries.LinkNode value) { + LinkedEntries.LinkNode rm = nameMap.remove(name); + if (rm == null) { + List l = snsMap.get(name); + if (l != null && l.remove(value)) { + rm = value; + } + } + return rm; + } + + public void reorder(Name name, LinkedEntries.LinkNode insertValue, int position) { + List sns = snsMap.get(name); + if (sns == null) { + // no same name siblings -> no special handling required + return; + } + // reorder sns in the name-list + sns.remove(insertValue); + if (position < 0 || position > sns.size()) { + // simply move to end of list + sns.add(insertValue); + } else { + sns.add(position, insertValue); + } + } + + /** + * + * @param siblings + * @param index + * @return matching entry or null. + */ + private static NodeEntry findMatchingEntry(List siblings, int index) { + // shortcut if index can never match + if (index > siblings.size()) { + return null; + } else { + return siblings.get(index - 1); + } + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildPropertyEntries.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildPropertyEntries.java new file mode 100644 index 00000000000..2950d0a70a8 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildPropertyEntries.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.jcr2spi.hierarchy; + +import org.apache.jackrabbit.spi.Name; + +import java.util.Collection; + +/** + * ChildPropertyEntries... + */ +public interface ChildPropertyEntries { + + /** + * Returns true if a property entry with the given name exists. + * + * @param propertyName + * @return true if a property entry with the given name exists. + */ + public boolean contains(Name propertyName); + + /** + * Return the PropertyEntry with the given Name or + * null. + * + * @param propertyName + * @return + */ + public PropertyEntry get(Name propertyName); + + /** + * Returns an unmodifiable collection containing all PropertyEntry + * objects present. + * + * @return Collection of all PropertyEntry objects present. + */ + public Collection getPropertyEntries(); + + /** + * Returns an unmodifiable collection containing all existing property names. + * + * @return Collection of Name + */ + public Collection getPropertyNames(); + + /** + * Adds the new PropertyEntry to this ChildPropertyEntries. + * + * @param propertyEntry + */ + public void add(PropertyEntry propertyEntry); + + /** + * Adds all PropertyEntrys from the given collection to this + * ChildPropertyEntries. + * + * @param propertyEntries + */ + public void addAll(Collection propertyEntries); + + /** + * Remove the collection entry with the given Name. + * + * @param propertyEntry + * @return true If this ChildPropertyEntries contained the + * given entry. False otherwise. + */ + public boolean remove(PropertyEntry propertyEntry); +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildPropertyEntriesImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildPropertyEntriesImpl.java new file mode 100644 index 00000000000..2544090cef5 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/ChildPropertyEntriesImpl.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.jcr2spi.hierarchy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.spi.Name; + +import java.util.Map; +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +import java.util.Collections; +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; + +/** + * ChildPropertyEntriesImpl... + */ +public class ChildPropertyEntriesImpl implements ChildPropertyEntries { + + private static Logger log = LoggerFactory.getLogger(ChildPropertyEntriesImpl.class); + + private final Map> properties; + private final NodeEntry parent; + private final EntryFactory factory; + + ChildPropertyEntriesImpl(NodeEntry parent, EntryFactory factory) { + this.properties = new HashMap>(); + this.parent = parent; + this.factory = factory; + } + + /** + * @see ChildPropertyEntries#contains(Name) + */ + public boolean contains(Name propertyName) { + return properties.containsKey(propertyName); + } + + /** + * @see ChildPropertyEntries#get(Name) + */ + public PropertyEntry get(Name propertyName) { + Reference ref = properties.get(propertyName); + if (ref == null) { + // no entry exists with the given name + return null; + } + + PropertyEntry entry = ref.get(); + if (entry == null) { + // entry has been g-collected -> create new entry and return it. + entry = factory.createPropertyEntry(parent, propertyName); + add(entry); + } + return entry; + } + + /** + * @see ChildPropertyEntries#getPropertyEntries() + */ + public Collection getPropertyEntries() { + synchronized (properties) { + Set entries = new HashSet(properties.size()); + for (Name propName : properties.keySet()) { + entries.add(get(propName)); + } + return Collections.unmodifiableCollection(entries); + } + } + + /** + * @see ChildPropertyEntries#getPropertyNames() + */ + public Collection getPropertyNames() { + return Collections.unmodifiableCollection(properties.keySet()); + } + + /** + * @see ChildPropertyEntries#add(PropertyEntry) + */ + public void add(PropertyEntry propertyEntry) { + synchronized (properties) { + Reference ref = new SoftReference(propertyEntry); + properties.put(propertyEntry.getName(), ref); + } + } + + /** + * @see ChildPropertyEntries#addAll(Collection) + */ + public void addAll(Collection propertyEntries) { + for (PropertyEntry pe : propertyEntries) { + add(pe); + } + } + + /** + * @see ChildPropertyEntries#remove(PropertyEntry) + */ + public boolean remove(PropertyEntry propertyEntry) { + synchronized (properties) { + Name pName = propertyEntry.getName(); + PropertyEntry pe = get(pName); + if (pe == propertyEntry) { + properties.remove(pName); + return true; + } else { + return false; + } + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/EntryFactory.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/EntryFactory.java new file mode 100644 index 00000000000..0529d855fc2 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/EntryFactory.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import org.apache.jackrabbit.jcr2spi.state.TransientItemStateFactory; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * EntryFactory... + */ +public class EntryFactory { + + /** + * IdFactory to create an ItemId based on the parent NodeId. + */ + private final IdFactory idFactory; + + private final PathFactory pathFactory; + + private final NodeEntry rootEntry; + + /** + * Listener to creation and uid-changes of node entries. + */ + private final NodeEntryListener listener; + + /** + * The item state factory to create the the item state. + */ + private final TransientItemStateFactory isf; + + /** + * NamePathResolver used to generate human readable error messages. + */ + private NamePathResolver resolver; + + /** + * Strategy used for item state invalidation (refresh) + */ + private final InvalidationStrategy invalidationStrategy; + + /** + * Create a new instance of the EntryFactory. + * + * @param isf + * @param idFactory + * @param listener + * @param pathFactory + */ + public EntryFactory(TransientItemStateFactory isf, IdFactory idFactory, + NodeEntryListener listener, PathFactory pathFactory) { + this.idFactory = idFactory; + this.pathFactory = pathFactory; + this.isf = isf; + this.listener = listener; + + // todo: make this configurable if necessary + // this.invalidationStrategy = new NodeEntryImpl.EagerInvalidation(); + this.invalidationStrategy = new NodeEntryImpl.LazyInvalidation(); + this.rootEntry = NodeEntryImpl.createRootEntry(this); + } + + /** + * @return the root entry. + */ + public NodeEntry createRootEntry() { + return rootEntry; + } + + public NodeEntry createNodeEntry(NodeEntry parent, Name qName, String uniqueId) { + if (!(parent instanceof NodeEntryImpl)) { + throw new IllegalArgumentException(); + } + return NodeEntryImpl.createNodeEntry((NodeEntryImpl) parent, qName, uniqueId, this); + } + + public PropertyEntry createPropertyEntry(NodeEntry parent, Name qName) { + if (!(parent instanceof NodeEntryImpl)) { + throw new IllegalArgumentException(); + } + return PropertyEntryImpl.create((NodeEntryImpl) parent, qName, this); + } + + public IdFactory getIdFactory() { + return idFactory; + } + + public PathFactory getPathFactory() { + return pathFactory; + } + + public TransientItemStateFactory getItemStateFactory() { + return isf; + } + + public void notifyEntryCreated(NodeEntry entry) { + listener.entryCreated(entry); + } + + public void notifyIdChange(NodeEntry entry, String previousUniqueID) { + listener.uniqueIdChanged(entry, previousUniqueID); + } + + /** + * @return the strategy used for item state invalidation (refresh) + */ + public InvalidationStrategy getInvalidationStrategy() { + return invalidationStrategy; + } + + //-------------------------------------------------------------------------- + /** + * @param resolver + */ + void setResolver(NamePathResolver resolver) { + this.resolver = resolver; + } + + /** + * @param path + * @return jcr presentation of the specified path. + */ + String saveGetJCRPath(Path path) { + if (resolver == null) { + return path.toString(); + } else { + return LogUtil.safeGetJCRPath(path, resolver); + } + } + + //--------------------------------------------------< NodeEntryListener >--- + public interface NodeEntryListener { + + public void entryCreated(NodeEntry entry); + + public void uniqueIdChanged (NodeEntry entry, String previousUniqueID); + } + + // ----------------------------------------------< InvalidationStrategy >--- + /** + * Strategy for invalidating item states + */ + public interface InvalidationStrategy { + + /** + * Invalidate underlying {@link org.apache.jackrabbit.jcr2spi.state.ItemState} of this + * entry. Implementors may choose to delay the actual call to + * {@link org.apache.jackrabbit.jcr2spi.state.ItemState#invalidate()} for this + * entry and for any of its child entries. They need to ensure however that + * {@link #applyPending(HierarchyEntry)} properly invalidates the respective state when called. + * + * @param entry The HierarchyEntry to invalidate. + * @param recursive Invalidate state of child entries if true. + */ + public void invalidate(HierarchyEntry entry, boolean recursive); + + /** + * Apply any pending {@link org.apache.jackrabbit.jcr2spi.state.ItemState#invalidate() + * invalidation} of the underlying {@link org.apache.jackrabbit.jcr2spi.state.ItemState} of + * this entry. + * + * @param entry The affected NodeEntry. + */ + public void applyPending(HierarchyEntry entry); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/EntryValidation.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/EntryValidation.java new file mode 100644 index 00000000000..f3282333cb1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/EntryValidation.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.jcr2spi.hierarchy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.jcr2spi.state.Status; + +import javax.jcr.RepositoryException; +import javax.jcr.ItemNotFoundException; +import java.util.Iterator; + +/** + * EntryValidation... + */ +final class EntryValidation { + + private static Logger log = LoggerFactory.getLogger(EntryValidation.class); + + /** + * Returns true if the collection of child node + * entries contains at least one valid NodeEntry. + * + * @param nodeEntries Iterator of NodeEntries to check. + * @return true if one of the entries is valid; otherwise + * false. + */ + static boolean containsValidNodeEntry(Iterator nodeEntries) { + boolean hasValid = false; + while (nodeEntries.hasNext() && !hasValid) { + NodeEntry cne = nodeEntries.next(); + hasValid = isValidNodeEntry(cne); + } + return hasValid; + } + + /** + * Returns true if the given childnode entry is not + * null and resolves to a NodeState, that is valid or if the + * childnode entry has not been resolved up to now (assuming the corresponding + * nodestate is still valid). + * + * @param cne NodeEntry to check. + * @return true if the given entry is valid. + */ + static boolean isValidNodeEntry(NodeEntry cne) { + // shortcut. + if (cne == null) { + return false; + } + boolean isValid = false; + if (cne.isAvailable()) { + try { + isValid = cne.getNodeState().isValid(); + } catch (ItemNotFoundException e) { + // may occur if the cached state is marked 'INVALIDATED' and + // does not exist any more on the persistent layer -> invalid. + } catch (RepositoryException e) { + // should not occur, if the cne is available. + } + } else { + // assume entry is valid + // TODO: check if this assumption is correct + isValid = true; + } + + return isValid; + } + + /** + * Returns true if the given childnode entry is not + * null and resolves to a NodeState, that is neither NEW + * nor REMOVED. + * + * @param cne NodeEntry to check. + * @return true if the given entry is valid. + */ + static boolean isValidWorkspaceNodeEntry(NodeEntry cne) { + // shortcut. + if (cne == null) { + return false; + } + int status = cne.getStatus(); + return status != Status.NEW && status != Status.REMOVED; + } + + /** + * Returns true if the given childproperty entry is not + * null and resolves to a PropertyState, that is valid or if the + * childproperty entry has not been resolved up to now (assuming the corresponding + * PropertyState is still valid). + * + * @param cpe PropertyEntry to check. + * @return true if the given entry is valid. + */ + static boolean isValidPropertyEntry(PropertyEntry cpe) { + if (cpe == null) { + return false; + } + boolean isValid = false; + if (cpe.isAvailable()) { + try { + isValid = cpe.getPropertyState().isValid(); + } catch (ItemNotFoundException e) { + // may occur if the cached state is marked 'INVALIDATED' and + // does not exist any more on the persistent layer -> invalid. + } catch (RepositoryException e) { + // probably removed in the meantime. should not occur. + } + } else { + // assume entry is valid // TODO check if this assumption is correct. + isValid = true; + } + return isValid; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEntry.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEntry.java new file mode 100644 index 00000000000..eb7123bf09c --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEntry.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * HierarchyEntry... + */ +public interface HierarchyEntry { + + /** + * True if this HierarchyEntry would resolve to a NodeState. + * + * @return + */ + public boolean denotesNode(); + + /** + * @return the name of this hierarchy entry. + */ + public Name getName(); + + /** + * @return the path of this hierarchy entry. + */ + public Path getPath() throws RepositoryException; + + /** + * @return If this entry has not been modified this method returns the same + * as {@link #getPath()}. In case of moved items this method return the + * original path as it is present on the persistent layer. + */ + public Path getWorkspacePath() throws RepositoryException; + + /** + * Returns the NodeEntry being parent to this + * HierarchyEntry. + * + * @return the parent HierarchyEntry + */ + public NodeEntry getParent(); + + /** + * If this HierarchyEntry provides an underlying + * ItemState this method returns the status of that state, + * otherwise it returns {@link Status#_UNDEFINED_}. + * + * @return Status of the ItemState or {@link Status#_UNDEFINED_} if this + * entry has not been resolved yet. + * @see ItemState#getStatus() + */ + public int getStatus(); + + /** + * Returns true if the referenced ItemState is + * available. That is, the referenced ItemState has already + * been resolved.
        + * Note, that the validity of the ItemState is not checked. + * + * @return true if the ItemState is available; + * otherwise false. + * @see #getItemState() + */ + public boolean isAvailable(); + + /** + * If this HierarchyEntry has already been resolved before + * (see {@link #isAvailable()}), that ItemState is returned. + * Note however, that the validity of the State is not asserted.
        + * If the entry has not been resolved yet an attempt is made to resolve this + * entry, which may fail if there exists no accessible ItemState + * or if the corresponding state has been removed in the mean time. + * + * @return the referenced ItemState. + * @throws ItemNotFoundException if the ItemState does not + * exist anymore. + * @throws RepositoryException If an error occurs while retrieving the + * ItemState. + */ + public ItemState getItemState() throws ItemNotFoundException, RepositoryException; + + /** + * Set the ItemState this hierarchyEntry will be resolved to. + * + * @param state + */ + public void setItemState(ItemState state); + + /** + * Invalidates the underlying ItemState if available and if it + * is not transiently modified. If the recursive flag is true, + * also invalidates the child entries recursively.
        + * Note, that in contrast to {@link HierarchyEntry#reload(boolean)} + * this method only sets the status of this item state to {@link + * Status#INVALIDATED} and does not actually update it with the persistent + * state in the repository. + */ + public void invalidate(boolean recursive); + + /** + * Calculates the status of the underlying ItemState: any pending + * changes to the underlying ItemState are applied. + */ + public void calculateStatus(); + + /** + * Traverses the hierarchy and reverts all transient modifications such as + * adding, modifying or removing item states. 'Existing' item states + * are reverted to their initial state and their status is reset to {@link Status#EXISTING}. + * + * @throws RepositoryException if an error occurs. + */ + public void revert() throws RepositoryException; + + /** + * Reloads this hierarchy entry and the corresponding ItemState, if this + * entry has already been resolved. If 'recursive' the complete + * hierarchy below this entry is reloaded as well. + * + * @param recursive + */ + public void reload(boolean recursive); + + /** + * Traverses the hierarchy and marks all available item states as transiently + * removed. They will change their status to either {@link Status#EXISTING_REMOVED} if + * the item is existing in the persistent storage or {@link Status#REMOVED} + * if the item has been transiently added before. In the latter case, the + * corresponding HierarchyEntries can be removed as well from their parent. + * + * @throws InvalidItemStateException if this entry has been removed in the + * mean time. + * @throws RepositoryException if an error occurs while removing any of the item + * states e.g. an item state is not valid anymore. + */ + public void transientRemove() throws InvalidItemStateException, RepositoryException; + + /** + * Removes this HierarchyEntry from its parent and sets the + * status of the underlying ItemState to {@link Status#REMOVED} or to + * {@link Status#STALE_DESTROYED}, respectively. If this entry is a + * NodeEntry all descending ItemStates must get their status changed as well. + */ + public void remove(); + + /** + * Clean up this entry upon {@link Operation#undo()} or {@link Operation#persisted()}. + * + * @param transientOperation + */ + public void complete(Operation transientOperation) throws RepositoryException; + + /** + * The required generation of this HierarchyEntry . This is used by the + * {@link ItemInfoCache} to determine whether an item info in the cache is up to date or not. + * That is whether the generation of the item info in the cache is the same or more recent + * as the required generation of this entry. + */ + public long getGeneration(); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEntryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEntryImpl.java new file mode 100644 index 00000000000..669ad622e74 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEntryImpl.java @@ -0,0 +1,561 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateFactory; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.jcr2spi.state.TransientItemStateFactory; +import org.apache.jackrabbit.jcr2spi.state.ItemState.MergeResult; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * HierarchyEntryImpl implements base functionality for child node + * and property references. + */ +abstract class HierarchyEntryImpl implements HierarchyEntry { + + private static Logger log = LoggerFactory.getLogger(HierarchyEntryImpl.class); + + /** + * The required generation of this entry. This is used by the + * {@link ItemInfoCache} to determine whether an item info in the cache is + * up to date or not. That is whether the generation of the item info in the + * cache is the same or more recent as the required generation of this entry. + */ + private long generation; + + /** + * Cached soft reference to the target ItemState. + */ + private Reference target; + + /** + * The name of the target item state. + */ + protected Name name; + + /** + * Hard reference to the parent NodeEntry. + */ + protected NodeEntryImpl parent; + + /** + * The item state factory to create the item state. + */ + protected final EntryFactory factory; + + /** + * Creates a new HierarchyEntryImpl with the given parent + * NodeState. + * + * @param parent the NodeEntry that owns this child node + * reference. + * @param name the name of the child item. + * @param factory + */ + HierarchyEntryImpl(NodeEntryImpl parent, Name name, EntryFactory factory) { + this.parent = parent; + this.name = name; + this.factory = factory; + } + + /** + * Shortcut for {@link EntryFactory#getItemStateFactory()} + * @return + */ + protected TransientItemStateFactory getItemStateFactory() { + return factory.getItemStateFactory(); + } + + /** + * Shortcut for {@link EntryFactory#getPathFactory()} + * @return + */ + protected PathFactory getPathFactory() { + return factory.getPathFactory(); + } + + /** + * Shortcut for {@link EntryFactory#getIdFactory()} + * @return + */ + protected IdFactory getIdFactory() { + return factory.getIdFactory(); + } + + /** + * Resolves this HierarchyEntryImpl and returns the target + * ItemState of this reference. This method may return a + * cached ItemState if this method was called before already + * otherwise this method will forward the call to {@link #doResolve()} + * and cache its return value. If an existing state has been invalidated + * before, an attempt is made to reload it in order to make sure, that + * a call to {@link ItemState#isValid()} does not equivocally return false. + * + * @return the ItemState where this reference points to. + * @throws ItemNotFoundException if the referenced ItemState + * does not exist. + * @throws RepositoryException if an error occurs. + */ + ItemState resolve() throws ItemNotFoundException, RepositoryException { + // check if already resolved + ItemState state = internalGetItemState(); + // not yet resolved. retrieve and keep soft reference to state + if (state == null) { + try { + state = doResolve(); + // set the item state unless 'setItemState' has already been + // called by the ItemStateFactory (recall internalGetItemState) + if (internalGetItemState() == null) { + setItemState(state); + } + } catch (ItemNotFoundException e) { + remove(); + throw e; + } + } else if (state.getStatus() == Status.INVALIDATED) { + // completely reload this entry, but don't reload recursively + reload(false); + } + return state; + } + + /** + * Resolves this HierarchyEntryImpl and returns the target + * ItemState of this reference. + * + * @return the ItemState where this reference points to. + * @throws ItemNotFoundException if the referenced ItemState + * does not exist. + * @throws RepositoryException if another error occurs. + */ + abstract ItemState doResolve() throws ItemNotFoundException, RepositoryException; + + /** + * Build the Path of this entry + * + * @param workspacePath + * @return + * @throws RepositoryException + */ + abstract Path buildPath(boolean workspacePath) throws RepositoryException; + + /** + * @return the item state or null if the entry isn't resolved. + */ + ItemState internalGetItemState() { + ItemState state = null; + if (target != null) { + state = target.get(); + } + return state; + } + + protected EntryFactory.InvalidationStrategy getInvalidationStrategy() { + return factory.getInvalidationStrategy(); + } + + /** + * Invalidates the underlying {@link ItemState}. If recursive is + * true also invalidates the underlying item states of all child entries. + * @param recursive + */ + protected void invalidateInternal(boolean recursive) { + ItemState state = internalGetItemState(); + if (state == null) { + log.debug("Skip invalidation for unresolved HierarchyEntry " + name); + } else { + state.invalidate(); + } + } + + //-----------------------------------------------------< HierarchyEntry >--- + /** + * @see HierarchyEntry#getName() + */ + public Name getName() { + return name; + } + + /** + * @see HierarchyEntry#getPath() + */ + public Path getPath() throws RepositoryException { + return buildPath(false); + } + + /** + * @see HierarchyEntry#getWorkspacePath() + */ + public Path getWorkspacePath() throws RepositoryException { + return buildPath(true); + } + + /** + * @see HierarchyEntry#getParent() + */ + public NodeEntry getParent() { + return parent; + } + + /** + * @see HierarchyEntry#getStatus() + */ + public int getStatus() { + ItemState state = internalGetItemState(); + if (state == null) { + return Status._UNDEFINED_; + } else { + return state.getStatus(); + } + } + + /** + * @see HierarchyEntry#isAvailable() + */ + public boolean isAvailable() { + return internalGetItemState() != null; + } + + /** + * {@inheritDoc}
        + * @see HierarchyEntry#getItemState() + */ + public ItemState getItemState() throws ItemNotFoundException, RepositoryException { + ItemState state = resolve(); + return state; + } + + /** + * {@inheritDoc}
        + * @see HierarchyEntry#setItemState(ItemState) + */ + public synchronized void setItemState(ItemState state) { + ItemState currentState = internalGetItemState(); + if (state == null || state == currentState || denotesNode() != state.isNode()) { + throw new IllegalArgumentException(); + } + if (currentState == null) { + // not connected yet to an item state. either a new entry or + // an unresolved hierarchy entry. + target = new SoftReference(state); + } else { + // was already resolved before -> merge the existing state + // with the passed state. + int currentStatus = currentState.getStatus(); + boolean keepChanges = Status.isTransient(currentStatus) || Status.isStale(currentStatus); + MergeResult mergeResult = currentState.merge(state, keepChanges); + if (currentStatus == Status.INVALIDATED) { + currentState.setStatus(Status.EXISTING); + } else if (mergeResult.modified()) { + currentState.setStatus(Status.MODIFIED); + } // else: not modified. just leave status as it is. + mergeResult.dispose(); + } + } + + /** + * {@inheritDoc}
        + * @see HierarchyEntry#invalidate(boolean) + */ + public void invalidate(boolean recursive) { + getInvalidationStrategy().invalidate(this, recursive); + } + + public void calculateStatus() { + getInvalidationStrategy().applyPending(this); + } + + /** + * {@inheritDoc} + * @see HierarchyEntry#revert() + */ + public void revert() throws RepositoryException { + ItemState state = internalGetItemState(); + if (state == null) { + // nothing to do + return; + } + + int oldStatus = state.getStatus(); + switch (oldStatus) { + case Status.EXISTING_MODIFIED: + case Status.STALE_MODIFIED: + // revert state modifications + state.revert(); + state.setStatus(Status.EXISTING); + break; + case Status.EXISTING_REMOVED: + // revert state modifications + state.revert(); + state.setStatus(Status.EXISTING); + break; + case Status.NEW: + // reverting a NEW state is equivalent to its removal. + // however: no need remove the complete hierarchy as revert is + // always related to Item#refresh(false) which affects the + // complete tree (and all add-operations within it) anyway. + state.setStatus(Status.REMOVED); + parent.internalRemoveChildEntry(this); + break; + case Status.STALE_DESTROYED: + // state does not exist any more -> reverting of pending + // transient changes (that lead to the stale status) can be + // omitted and the entry is complete removed instead. + remove(); + break; + default: + // Cannot revert EXISTING, REMOVED, INVALIDATED, MODIFIED states. + // State was implicitly reverted or external modifications + // reverted the modification. + log.debug("State with status " + oldStatus + " cannot be reverted."); + } + } + + /** + * {@inheritDoc} + * @see HierarchyEntry#reload(boolean) + */ + public void reload(boolean recursive) { + int status = getStatus(); + if (status == Status._UNDEFINED_) { + // unresolved: entry will be loaded and validated upon resolution. + return; + } + if (Status.isTransient(status) || Status.isStale(status) || Status.isTerminal(status)) { + // transient || stale: avoid reloading + // new || terminal: cannot be reloaded from persistent layer anyway. + log.debug("Skip reload for item with status " + Status.getName(status) + "."); + return; + } + /** + * Retrieved a fresh ItemState from the persistent layer. Which will + * then be merged into the current state. + */ + try { + ItemStateFactory isf = getItemStateFactory(); + if (denotesNode()) { + NodeEntry ne = (NodeEntry) this; + isf.createNodeState(ne.getWorkspaceId(), ne); + } else { + PropertyEntry pe = (PropertyEntry) this; + isf.createPropertyState(pe.getWorkspaceId(), pe); + } + } catch (ItemNotFoundException e) { + // remove hierarchyEntry including all children + log.debug("Item '" + getName() + "' cannot be found on the persistent layer -> remove."); + remove(); + } catch (RepositoryException e) { + // TODO: rather throw? + log.error("Exception while reloading item: " + e); + } + } + + /** + * {@inheritDoc} + * @see HierarchyEntry#transientRemove() + */ + public void transientRemove() throws InvalidItemStateException, RepositoryException { + ItemState state = internalGetItemState(); + if (state == null) { + // nothing to do -> correct status must be set upon resolution. + return; + } + // if during recursive removal an invalidated entry is found, reload + // it in order to determine the current status. + if (state.getStatus() == Status.INVALIDATED) { + reload(false); + } + + switch (state.getStatus()) { + case Status.NEW: + state.setStatus(Status.REMOVED); + parent.internalRemoveChildEntry(this); + break; + case Status.EXISTING: + case Status.EXISTING_MODIFIED: + state.setStatus(Status.EXISTING_REMOVED); + // NOTE: parent does not need to be informed. an transiently + // removed propertyEntry is automatically moved to the 'attic' + // if a conflict with a new entry occurs. + break; + case Status.REMOVED: + case Status.STALE_DESTROYED: + throw new InvalidItemStateException("Item has already been removed by someone else. Status = " + Status.getName(state.getStatus())); + default: + throw new RepositoryException("Cannot transiently remove an ItemState with status " + Status.getName(state.getStatus())); + } + } + + /** + * @see HierarchyEntry#remove() + */ + public void remove() { + internalRemove(false); + } + + public long getGeneration() { + calculateStatus(); + return generation; + } + + //-------------------------------------------------------------------------- + + /** + * @param staleParent + */ + void internalRemove(boolean staleParent) { + ItemState state = internalGetItemState(); + int status = getStatus(); + if (state != null) { + if (status == Status.EXISTING_MODIFIED) { + state.setStatus(Status.STALE_DESTROYED); + } else if (status == Status.NEW && staleParent) { + // keep status NEW + } else { + state.setStatus(Status.REMOVED); + if (!staleParent) { + parent.internalRemoveChildEntry(this); + } + } + } else { + // unresolved + if (!staleParent && parent != null) { + parent.internalRemoveChildEntry(this); + } + } + } + + // ----------------------------------------------< InvalidationStrategy >--- + /** + * An implementation of InvalidationStrategy which lazily invalidates + * the underlying {@link ItemState}s. + */ + static class LazyInvalidation implements EntryFactory.InvalidationStrategy { + + /** + * Marker for entries with a pending recursive invalidation. + */ + private static long INVALIDATION_PENDING = -1; + + /** + * Number of the current generation + */ + private long currentGeneration; + + /** + * Increment for obtaining the next generation from the current generation. + */ + private int nextGeneration; + + /** + * A recursive invalidation is being processed if true. + * This flag is for preventing re-entrance. + */ + private boolean invalidating; + + /** + * Records a pending recursive {@link ItemState#invalidate() invalidation} for + * entry if recursive is true. Otherwise + * invalidates the entry right away. + * {@inheritDoc} + */ + public void invalidate(HierarchyEntry entry, boolean recursive) { + HierarchyEntryImpl he = (HierarchyEntryImpl) entry; + if (recursive) { + he.generation = INVALIDATION_PENDING; + if (!invalidating) { + nextGeneration = 1; + } + } else { + if (!invalidating) { + nextGeneration = 1; + } + he.invalidateInternal(false); + } + } + + /** + * Checks whether entry itself has a invalidation pending. + * If so, the entry is invalidated. Otherwise check + * whether an invalidation occurred after the entry has last been + * invalidated. If so, search the path to the root for an originator of + * the pending invalidation. + * If such an originator is found, invalidate each entry on the path. + * Otherwise this method does nothing. + * {@inheritDoc} + */ + public void applyPending(HierarchyEntry entry) { + if (!invalidating) { + invalidating = true; + currentGeneration += nextGeneration; + nextGeneration = 0; + try { + HierarchyEntryImpl he = (HierarchyEntryImpl) entry; + if (he.generation == INVALIDATION_PENDING) { + he.invalidateInternal(true); + he.generation = currentGeneration; + } else if (he.generation < currentGeneration) { + resolvePendingInvalidation(he); + } + } finally { + invalidating = false; + } + } + } + + /** + * Search the path to the root for an originator of a pending invalidation of + * this entry. If such an originator is found, invalidate each + * entry on the path. Otherwise do nothing. + * + * @param entry + */ + private void resolvePendingInvalidation(HierarchyEntryImpl entry) { + if (entry != null) { + + // First recursively travel up to the first parent node + // which has invalidation pending or to the root node if + // no such node exists. + if (entry.generation != INVALIDATION_PENDING) { + resolvePendingInvalidation(entry.parent); + } + + // Then travel the path backwards invalidating as required + if (entry.generation == INVALIDATION_PENDING) { + entry.invalidateInternal(true); + } + entry.generation = currentGeneration; + } + } + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEventListener.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEventListener.java new file mode 100644 index 00000000000..6475c70193c --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyEventListener.java @@ -0,0 +1,221 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.WorkspaceManager; +import org.apache.jackrabbit.jcr2spi.config.CacheBehaviour; +import org.apache.jackrabbit.jcr2spi.observation.InternalEventListener; +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * HierarchyEventListener... + */ +public class HierarchyEventListener implements InternalEventListener { + + private static Logger log = LoggerFactory.getLogger(HierarchyEventListener.class); + + private final HierarchyManager hierarchyMgr; + private final Collection eventFilter; + + public HierarchyEventListener(WorkspaceManager wspManager, + HierarchyManager hierarchyMgr, + CacheBehaviour cacheBehaviour) { + this.hierarchyMgr = hierarchyMgr; + if (cacheBehaviour == CacheBehaviour.OBSERVATION) { + EventFilter filter = null; + try { + // listen to all events except 'local' ones + Path root = wspManager.getPathFactory().getRootPath(); + filter = wspManager.createEventFilter(Event.ALL_TYPES, root, true, null, null, true); + } catch (RepositoryException e) { + // SPI does not support observation, or another error occurred. + log.debug("Creating event filter for cache behavior observation failed", e); + } + if (filter == null) { + this.eventFilter = Collections.emptyList(); + } else { + this.eventFilter = Collections.singletonList(filter); + } + } else { + this.eventFilter = Collections.emptyList(); + } + } + + //----------------------------------------------< InternalEventListener >--- + /** + * @see InternalEventListener#getEventFilters() + */ + public Collection getEventFilters() { + return eventFilter; + } + + /** + * Processes events and invalidates cached ItemStates + * accordingly. Note that this is performed for local changes only, + * since workspace operations are reported as local and have been applied already. + * + * @param eventBundle the events. + * @see InternalEventListener#onEvent(EventBundle) + */ + public void onEvent(EventBundle eventBundle) { + if (eventBundle.isLocal()) { + log.debug("Local event bundle -> not processed by HierarchyEventListener."); + return; + } + pushEvents(getEventCollection(eventBundle)); + } + + /** + * Retrieve the workspace state(s) affected by the given event and refresh + * them accordingly. + * + * @param events the events to process. + */ + private void pushEvents(Collection events) { + if (events.isEmpty()) { + log.debug("Empty event bundle"); + return; + } + + // TODO: handle new 283 event types and clean add/remove that is also present as move-event. + + // collect set of removed node ids + Set removedEvents = new HashSet(); + // separately collect the add events + Set addEvents = new HashSet(); + + for (Iterator it = events.iterator(); it.hasNext();) { + Event event = it.next(); + int type = event.getType(); + if (type == Event.NODE_REMOVED) { + // remember removed nodes separately for proper handling later on. + removedEvents.add(event.getItemId()); + } else if (type == Event.NODE_ADDED || type == Event.PROPERTY_ADDED) { + addEvents.add(event); + it.remove(); + } + } + + /* Process ADD-events. + In case of persisting transients modifications, the event-set may + still contain events that are not covered by the changeLog such as + new version-history or other autocreated properties and nodes. + + Add events need to be processed hierarchically, since its not possible + to add a new child reference to a state that is not yet present in + the state manager. + The 'progress' flag is used to make sure, that during each loop at + least one event has been processed and removed from the iterator. + If this is not the case, there are not parent states present in the + state manager that need to be updated and the remaining events may + be ignored. + */ + boolean progress = true; + while (!addEvents.isEmpty() && progress) { + progress = false; + for (Iterator it = addEvents.iterator(); it.hasNext();) { + Event ev = it.next(); + NodeId parentId = ev.getParentId(); + HierarchyEntry parent = null; + if (parentId != null) { + parent = hierarchyMgr.lookup(parentId); + if (parent == null && ev.getPath() != null && parentId.getUniqueID() != null) { + // parentID contains a uniqueID part -> try to lookup + // the parent by path. + try { + Path parentPath = ev.getPath().getAncestor(1); + parent = hierarchyMgr.lookup(parentPath); + } catch (RepositoryException e) { + // should not occur + log.debug(e.getMessage()); + } + } + } + if (parent != null && parent.denotesNode()) { + ((NodeEntry) parent).refresh(ev); + it.remove(); + progress = true; + } + } + } + + /* process all other events (removal, property changed) */ + for (Event event : events) { + int type = event.getType(); + + NodeId parentId = event.getParentId(); + NodeEntry parent = (parentId != null) ? (NodeEntry) hierarchyMgr.lookup(parentId) : null; + switch (type) { + case Event.NODE_REMOVED: + case Event.PROPERTY_REMOVED: + // notify parent about removal if its child-entry. + // - if parent is 'null' (i.e. not yet loaded) the child-entry does + // not exist either -> no need to inform child-entry + // - if parent got removed with the same event-bundle + // only remove the parent an skip this event. + if (parent != null && !removedEvents.contains(parentId)) { + parent.refresh(event); + } + break; + case Event.PROPERTY_CHANGED: + // notify parent in case jcr:mixinTypes or jcr:uuid was changed. + // if parent is 'null' (i.e. not yet loaded) the prop-entry does + // not exist either -> no need to inform propEntry + if (parent != null) { + parent.refresh(event); + } + break; + case Event.NODE_MOVED: + // TODO: implementation missing + throw new UnsupportedOperationException("Implementation missing"); + //break; + case Event.PERSIST: + // TODO: implementation missing + throw new UnsupportedOperationException("Implementation missing"); + //break; + default: + // should never occur + throw new IllegalArgumentException("Invalid event type: " + event.getType()); + } + } + } + + private static Collection getEventCollection(EventBundle eventBundle) { + List evs = new ArrayList(); + for (Iterator it = eventBundle.getEvents(); it.hasNext();) { + evs.add(it.next()); + } + return evs; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyManager.java new file mode 100644 index 00000000000..3ce19100f41 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyManager.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +/** + * HierarchyManager... + */ +public interface HierarchyManager { + + /** + * Dispose this HierarchyManager + */ + public void dispose(); + + /** + * @return the root entry. + */ + public NodeEntry getRootEntry(); + + /** + * Lookup of HierarchyEntry by its workspace Id that may be different + * if a entry (or any of its ancestors) has been transiently moved or + * reordered. + *

        + * If the Hierarchy already lists the entry with the given workspaceItemId it is + * returned otherwise null. See {@link #getNodeEntry(NodeId)} + * or {@link #getPropertyEntry(PropertyId)} for methods that resolves the + * ItemId including lookup in the persistence layer if the entry has not been + * loaded yet. + * + * @param workspaceItemId + * @return the HierarchyEntry with the given workspaceItemId. + */ + public HierarchyEntry lookup(ItemId workspaceItemId); + + /** + * Lookup of HierarchyEntry by its workspace path that may be different + * if a entry (or any of its ancestors) has been transiently moved or + * reordered. + *

        + * If the Hierarchy already lists the entry with the given path it is + * returned otherwise null. See {@link #getNodeEntry(Path)} + * or {@link #getPropertyEntry(Path)} for methods that resolves the path + * including lookup in the persistence layer if the entry has not been loaded yet. + * + * @param workspacePath + * @return the HierarchyEntry with the given workspacePath. + */ + public HierarchyEntry lookup(Path workspacePath); + + /** + * Resolves a itemId into a HierarchyEntry. + * + * @param nodeId + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public NodeEntry getNodeEntry(NodeId nodeId) throws ItemNotFoundException, RepositoryException; + + /** + * Resolves a path into a NodeEntry. + * + * @param qPath + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public NodeEntry getNodeEntry(Path qPath) throws PathNotFoundException, RepositoryException; + + /** + * Resolves a propertyId into a PropertyEntry. + * + * @param propertyId + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public PropertyEntry getPropertyEntry(PropertyId propertyId) throws ItemNotFoundException, RepositoryException; + + /** + * Resolves a path into a PropertyEntry. + * + * @param qPath + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public PropertyEntry getPropertyEntry(Path qPath) throws PathNotFoundException, RepositoryException; + + /** + * Retrieves the NodeEntry corresponding to the given + * path and resolves it to the underlying NodeState. + * + * @param qPath + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public NodeState getNodeState(Path qPath) throws PathNotFoundException, RepositoryException; + + /** + * Retrieves the PropertyEntry corresponding to the given + * path and resolves it to the underlying PropertyState. + * + * @param qPath + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + public PropertyState getPropertyState(Path qPath) throws PathNotFoundException, RepositoryException; + + /** + * Returns the depth of the specified item. The depth reflects the + * absolute hierarchy level. + * + * @param hierarchyEntry + * @return the depth of the specified item + * @throws RepositoryException if another error occurs + */ + public int getDepth(HierarchyEntry hierarchyEntry) throws ItemNotFoundException, RepositoryException; + + /** + * Returns the depth of the specified descendant relative to the given + * ancestor. If ancestor and descendant + * denote the same item 0 is returned. If ancestor does not + * denote an ancestor -1 is returned. + * + * @param ancestor NodeEntry that must be an ancestor of the descendant + * @param descendant HierarchyEntry + * @return the relative depth; -1 if ancestor does not + * denote an ancestor of the item denoted by descendant + * (or itself). + * @throws ItemNotFoundException If either of the specified id's does not + * denote an existing item. + * @throws RepositoryException If another error occurs. + */ + public int getRelativeDepth(NodeEntry ancestor, HierarchyEntry descendant) throws ItemNotFoundException, RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyManagerImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyManagerImpl.java new file mode 100644 index 00000000000..10ced1c3d04 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/HierarchyManagerImpl.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.TransientItemStateFactory; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +/** + * HierarchyManagerImpl implements the HierarchyManager + * interface. + */ +public class HierarchyManagerImpl implements HierarchyManager { + + private static Logger log = LoggerFactory.getLogger(HierarchyManagerImpl.class); + + private final NodeEntry rootEntry; + private final UniqueIdResolver uniqueIdResolver; + private final IdFactory idFactory; + private NamePathResolver resolver; + + public HierarchyManagerImpl(TransientItemStateFactory isf, IdFactory idFactory, + PathFactory pathFactory) { + uniqueIdResolver = new UniqueIdResolver(isf); + rootEntry = new EntryFactory(isf, idFactory, uniqueIdResolver, pathFactory).createRootEntry(); + this.idFactory = idFactory; + } + + public void setResolver(NamePathResolver resolver) { + this.resolver = resolver; + if (rootEntry instanceof HierarchyEntryImpl) { + ((HierarchyEntryImpl) rootEntry).factory.setResolver(resolver); + } + } + + //---------------------------------------------------< HierarchyManager >--- + /** + * @see HierarchyManager#dispose() + */ + public void dispose() { + uniqueIdResolver.dispose(); + } + + /** + * @see HierarchyManager#getRootEntry() + */ + public NodeEntry getRootEntry() { + return rootEntry; + } + + /** + * @see HierarchyManager#lookup(ItemId) + */ + public HierarchyEntry lookup(ItemId workspaceItemId) { + String uniqueID = workspaceItemId.getUniqueID(); + if (uniqueID == null) { + return rootEntry.lookupDeepEntry(workspaceItemId.getPath()); + } else { + NodeEntry nEntry = uniqueIdResolver.lookup(uniqueID); + Path path = workspaceItemId.getPath(); + if (path == null) { + return nEntry; + } else { + return nEntry != null ? nEntry.lookupDeepEntry(path) : null; + } + } + } + + /** + * @see HierarchyManager#lookup(Path) + */ + public HierarchyEntry lookup(Path workspacePath) { + return rootEntry.lookupDeepEntry(workspacePath); + } + + /** + * @see HierarchyManager#getNodeEntry(NodeId) + */ + public NodeEntry getNodeEntry(NodeId nodeId) + throws ItemNotFoundException, RepositoryException { + String uniqueID = nodeId.getUniqueID(); + if (uniqueID == null) { + return getNodeEntry(nodeId.getPath()); + } else { + if (nodeId.getPath() == null) { + NodeEntry nEntry = uniqueIdResolver.resolve(nodeId, rootEntry); + return nEntry; + } else { + NodeEntry nEntry = uniqueIdResolver.resolve(idFactory.createNodeId(uniqueID), rootEntry); + return nEntry.getDeepNodeEntry(nodeId.getPath()); + } + } + } + + /** + * @see HierarchyManager#getNodeEntry(Path) + */ + public NodeEntry getNodeEntry(Path qPath) throws PathNotFoundException, RepositoryException { + NodeEntry rootEntry = getRootEntry(); + // shortcut + if (qPath.denotesRoot()) { + return rootEntry; + } + if (!qPath.isCanonical()) { + String msg = "Path is not canonical"; + log.debug(msg); + throw new RepositoryException(msg); + } + return rootEntry.getDeepNodeEntry(qPath); + } + + /** + * @see HierarchyManager#getPropertyEntry(PropertyId) + */ + public PropertyEntry getPropertyEntry(PropertyId propertyId) + throws ItemNotFoundException, RepositoryException { + String uniqueID = propertyId.getUniqueID(); + if (uniqueID == null) { + return getPropertyEntry(propertyId.getPath()); + } else { + if (propertyId.getPath() == null) { + // a property id always contains a Path part. + throw new ItemNotFoundException("No property found for id " + LogUtil.saveGetIdString(propertyId, resolver)); + } else { + NodeEntry nEntry = uniqueIdResolver.resolve(idFactory.createNodeId(uniqueID), rootEntry); + return nEntry.getDeepPropertyEntry(propertyId.getPath()); + } + } + } + + /** + * @see HierarchyManager#getPropertyEntry(Path) + */ + public PropertyEntry getPropertyEntry(Path qPath) + throws PathNotFoundException, RepositoryException { + // shortcut + if (qPath.denotesRoot()) { + throw new PathNotFoundException("The root path never points to a Property."); + } + if (!qPath.isCanonical()) { + String msg = "Path is not canonical"; + log.debug(msg); + throw new RepositoryException(msg); + } + return getRootEntry().getDeepPropertyEntry(qPath); + } + + /** + * @see HierarchyManager#getNodeState(Path) + */ + public NodeState getNodeState(Path qPath) throws PathNotFoundException, RepositoryException { + NodeEntry entry = getNodeEntry(qPath); + try { + NodeState state = entry.getNodeState(); + if (state.isValid()) { + return state; + } else { + throw new PathNotFoundException(LogUtil.safeGetJCRPath(qPath, resolver)); + } + } catch (ItemNotFoundException e) { + throw new PathNotFoundException(e); + } + } + + /** + * @see HierarchyManager#getPropertyState(Path) + */ + public PropertyState getPropertyState(Path qPath) throws PathNotFoundException, RepositoryException { + PropertyEntry entry = getPropertyEntry(qPath); + try { + PropertyState state = entry.getPropertyState(); + if (state.isValid()) { + return state; + } else { + throw new PathNotFoundException(LogUtil.safeGetJCRPath(qPath, resolver)); + } + } catch (ItemNotFoundException e) { + throw new PathNotFoundException(e); + } + } + + /** + * @see HierarchyManager#getDepth(HierarchyEntry) + */ + public int getDepth(HierarchyEntry hierarchyEntry) throws ItemNotFoundException, RepositoryException { + int depth = Path.ROOT_DEPTH; + NodeEntry parentEntry = hierarchyEntry.getParent(); + while (parentEntry != null) { + depth++; + hierarchyEntry = parentEntry; + parentEntry = hierarchyEntry.getParent(); + } + return depth; + } + + /** + * @see HierarchyManager#getRelativeDepth(NodeEntry, HierarchyEntry) + */ + public int getRelativeDepth(NodeEntry ancestor, HierarchyEntry descendant) + throws ItemNotFoundException, RepositoryException { + if (ancestor.equals(descendant)) { + return 0; + } + int depth = 1; + NodeEntry parent = descendant.getParent(); + while (parent != null) { + if (parent.equals(ancestor)) { + return depth; + } + depth++; + descendant = parent; + parent = descendant.getParent(); + } + // not an ancestor + return -1; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/NodeEntry.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/NodeEntry.java new file mode 100644 index 00000000000..e2299fe6a23 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/NodeEntry.java @@ -0,0 +1,351 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.QValue; + +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.PathNotFoundException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.InvalidItemStateException; +import java.util.Iterator; +import java.util.List; +import java.util.Collection; + +/** + * NodeEntry... + */ +public interface NodeEntry extends HierarchyEntry { + + /** + * @return the NodeId of this child node entry. + */ + public NodeId getId() throws InvalidItemStateException, RepositoryException; + + /** + * Returns the ID that must be used for resolving this entry OR loading its + * children entries from the persistent layer. This is the same as + * getId() unless this entry or any of its ancestors has been + * transiently moved. + * + * @return + * @see #getId() + */ + public NodeId getWorkspaceId() throws InvalidItemStateException, RepositoryException; + + /** + * @return the unique ID of the node state which is referenced by this + * child node entry or null if the node state cannot be + * identified with a unique ID. + */ + public String getUniqueID(); + + /** + * + * @param uniqueID + */ + public void setUniqueID(String uniqueID); + + /** + * @return the index of this child node entry to support same-name siblings. + * If the index of this entry cannot be determined + * {@link org.apache.jackrabbit.spi.Path#INDEX_UNDEFINED} is returned. + * @throws InvalidItemStateException + * @throws RepositoryException + */ + public int getIndex() throws InvalidItemStateException, RepositoryException; + + /** + * @return the referenced NodeState. + * @throws ItemNotFoundException if the NodeState does not + * exist. + * @throws RepositoryException If an error occurs while retrieving the + * NodeState. + */ + public NodeState getNodeState() throws ItemNotFoundException, RepositoryException; + + /** + * Traverse the tree below this entry and return the child entry matching + * the given path. If that entry has not been loaded yet, try to do so. + * NOTE: In contrast to getNodeEntry, getNodeEntries this method may return + * invalid entries, i.e. entries connected to a removed or stale ItemState. + * + * @param path + * @return the entry at the given path. + * @throws PathNotFoundException + * @throws RepositoryException + */ + public NodeEntry getDeepNodeEntry(Path path) throws PathNotFoundException, RepositoryException; + + /** + * Traverse the tree below this entry and return the child entry matching + * the given path. If that entry has not been loaded yet, try to do so. + * NOTE: In contrast to getPropertyEntry and getPropertyEntries this method + * may return invalid entries, i.e. entries connected to a removed or stale + * ItemState. + * + * @param path + * @return the property entry at the given path. + * @throws PathNotFoundException + * @throws RepositoryException + */ + public PropertyEntry getDeepPropertyEntry(Path path) throws PathNotFoundException, RepositoryException; + + /** + * Traverse the tree below this entry and return the child entry matching + * the given 'workspacePath', i.e. transient modifications and new entries + * are ignored. + *

        + * If no matching entry can be found, null is return. + * + * @param workspacePath + * @return matching entry or null. + */ + public HierarchyEntry lookupDeepEntry(Path workspacePath); + + /** + * Determines if there is a valid NodeEntry with the + * specified nodeName. + * + * @param nodeName Name object specifying a node name + * @return true if there is a NodeEntry with + * the specified nodeName. + */ + public boolean hasNodeEntry(Name nodeName); + + /** + * Determines if there is a valid NodeEntry with the + * specified name and index. + * + * @param nodeName Name object specifying a node name. + * @param index 1-based index if there are same-name child node entries. + * @return true if there is a NodeEntry with + * the specified name and index. + */ + public boolean hasNodeEntry(Name nodeName, int index); + + /** + * Returns the valid NodeEntry with the specified name + * and index or null if there's no matching entry. + * + * @param nodeName Name object specifying a node name. + * @param index 1-based index if there are same-name child node entries. + * @return The NodeEntry with the specified name and index + * or null if there's no matching entry. + * @throws RepositoryException If an unexpected error occurs. + */ + public NodeEntry getNodeEntry(Name nodeName, int index) throws RepositoryException; + + /** + * Returns the valid NodeEntry with the specified name + * and index or null if there's no matching entry. If + * loadIfNotFound is true, the implementation must make + * sure, that it's list of child entries is up to date and eventually + * try to load the node entry. + * + * @param nodeName Name object specifying a node name. + * @param index 1-based index if there are same-name child node entries. + * @param loadIfNotFound + * @return The NodeEntry with the specified name and index + * or null if there's no matching entry. + * @throws RepositoryException If an unexpected error occurs. + */ + public NodeEntry getNodeEntry(Name nodeName, int index, boolean loadIfNotFound) throws RepositoryException; + + /** + * Returns a unmodifiable iterator of NodeEntry objects + * denoting the the valid child NodeEntries present on this NodeEntry. + * + * @return iterator of NodeEntry objects + * @throws RepositoryException If an unexpected error occurs. + */ + public Iterator getNodeEntries() throws RepositoryException; + + /** + * Returns a unmodifiable List of NodeEntrys with the + * specified name. + * + * @param nodeName name of the child node entries that should be returned + * @return list of NodeEntry objects + * @throws RepositoryException If an unexpected error occurs. + */ + public List getNodeEntries(Name nodeName) throws RepositoryException; + + /** + * Creates or updates the ChildNodeEntries of this node. + * + * @param childInfos + * @throws RepositoryException + */ + public void setNodeEntries(Iterator childInfos) throws RepositoryException; + + /** + * Adds a child NodeEntry to this entry if it not yet present with this + * node entry. + * + * @param nodeName + * @param index + * @param uniqueID + * @return the NodeEntry. + * @throws RepositoryException If an unexpected error occurs. + */ + public NodeEntry getOrAddNodeEntry(Name nodeName, int index, String uniqueID) throws RepositoryException; + + /** + * Adds a new, transient child NodeEntry + * + * @param nodeName + * @param uniqueID + * @param primaryNodeType + * @param definition + * @return + * @throws RepositoryException If an error occurs. + */ + public NodeEntry addNewNodeEntry(Name nodeName, String uniqueID, Name primaryNodeType, QNodeDefinition definition) throws RepositoryException; + + /** + * Determines if there is a property entry with the specified Name. + * + * @param propName Name object specifying a property name + * @return true if there is a property entry with the specified + * Name. + */ + public boolean hasPropertyEntry(Name propName); + + /** + * Returns the valid PropertyEntry with the specified name + * or null if no matching entry exists. + * + * @param propName Name object specifying a property name. + * @return The PropertyEntry with the specified name or + * null if no matching entry exists. + * @throws RepositoryException If an unexpected error occurs. + */ + public PropertyEntry getPropertyEntry(Name propName) throws RepositoryException; + + /** + * Returns the valid PropertyEntry with the specified name + * or null if no matching entry exists. If + * loadIfNotFound is true, the implementation must make + * sure, that it's list of property entries is up to date and eventually + * try to load the property entry with the given name. + * + * @param propName Name object specifying a property name. + * @param loadIfNotFound + * @return The PropertyEntry with the specified name or + * null if no matching entry exists. + * @throws RepositoryException If an unexpected error occurs. + */ + public PropertyEntry getPropertyEntry(Name propName, boolean loadIfNotFound) throws RepositoryException; + + /** + * Returns an unmodifiable Iterator over those children that represent valid + * PropertyEntries. + * + * @return an unmodifiable Iterator over those children that represent valid + * PropertyEntries. + */ + public Iterator getPropertyEntries(); + + /** + * Add an existing PropertyEntry with the given name if it is + * not yet contained in this NodeEntry. + * Please note the difference to {@link #addNewPropertyEntry(Name, QPropertyDefinition, QValue[], int)} + * which adds a new, transient entry. + * + * @param propName + * @return the PropertyEntry + * @throws ItemExistsException if a child item exists with the given name + * @throws RepositoryException if an unexpected error occurs. + */ + public PropertyEntry getOrAddPropertyEntry(Name propName) throws ItemExistsException, RepositoryException; + + /** + * Adds property entries for the given Names. It depends on + * the status of this NodeEntry, how conflicts are resolved + * and whether or not existing entries that are missing in the iterator + * get removed. + * + * @param propNames + * @throws ItemExistsException + * @throws RepositoryException if an unexpected error occurs. + */ + public void setPropertyEntries(Collection propNames) throws ItemExistsException, RepositoryException; + + /** + * Add a new, transient PropertyEntry to this NodeEntry + * and return the PropertyState associated with the new entry. + * + * @param propName + * @param definition + * @param values + * @param propertyType + * @return the new entry. + * @throws ItemExistsException + * @throws RepositoryException + */ + public PropertyEntry addNewPropertyEntry(Name propName, QPropertyDefinition definition, QValue[] values, int propertyType) throws ItemExistsException, RepositoryException; + + /** + * Reorders this NodeEntry before the sibling entry specified by the given + * beforeEntry. + * + * @param beforeEntry the child node where to insert the node before. If + * null this entry is moved to the end of its parents child node entries. + * @throws RepositoryException If an unexpected error occurs. + */ + public void orderBefore(NodeEntry beforeEntry) throws RepositoryException; + + /** + * Moves this NodeEntry as new child entry of the + * NodeEntry identified by newParent and/or renames + * it to newName. If transientMove is true, an + * implementation must make sure, that reverting this modification by calling + * {@link HierarchyEntry#revert()} on the common ancestor of both parents + * moves this NodeEntry back and resets the name to its original value. + * + * @param newName + * @param newParent + * @return the moved entry + * @throws RepositoryException If the entry to be moved is not a child of this + * NodeEntry or if an unexpected error occurs. + */ + public NodeEntry move(Name newName, NodeEntry newParent, boolean transientMove) throws RepositoryException; + + /** + * @return true if this NodeEntry is transiently moved. + */ + public boolean isTransientlyMoved(); + + /** + * The parent entry of a external event gets informed about the modification. + * Note, that {@link Event#getParentId()} of the given childEvent must point + * to this NodeEntry. + * + * @param childEvent + */ + public void refresh(Event childEvent) ; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/NodeEntryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/NodeEntryImpl.java new file mode 100644 index 00000000000..24e2f2eea36 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/NodeEntryImpl.java @@ -0,0 +1,1666 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.hierarchy; + +import java.util.ArrayList; +import java.util.Collection; +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 java.util.Set; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.commons.collections.iterators.IteratorChain; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.jcr2spi.operation.AddNode; +import org.apache.jackrabbit.jcr2spi.operation.AddProperty; +import org.apache.jackrabbit.jcr2spi.operation.Move; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.Remove; +import org.apache.jackrabbit.jcr2spi.operation.ReorderNodes; +import org.apache.jackrabbit.jcr2spi.operation.SetMixin; +import org.apache.jackrabbit.jcr2spi.operation.SetPrimaryType; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.jcr2spi.util.StateUtility; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NodeEntryImpl implements common functionality for child + * node entry implementations. + */ +public class NodeEntryImpl extends HierarchyEntryImpl implements NodeEntry { + + private static Logger log = LoggerFactory.getLogger(NodeEntryImpl.class); + + /** + * UniqueID identifying this NodeEntry or null if either + * the underlying state has not been loaded yet or if it cannot be + * identified with a unique ID. + */ + private String uniqueID; + + /** + * Insertion-ordered collection of NodeEntry objects. + */ + private final ChildNodeEntries childNodeEntries; + + /** + * Map used to remember transiently removed or moved childNodeEntries, that + * must not be retrieved from the persistent storage. + */ + private final ChildNodeAttic childNodeAttic; + + /** + * Map of properties.
        + * Key = {@link Name} of property,
        + * Value = {@link PropertyEntry}. + */ + private final ChildPropertyEntries properties; + + /** + * Map of properties which are deleted and have been re-created as transient + * property with the same name. + */ + private final Map propertiesInAttic; + + /** + * Upon transient 'move' ('rename') or 'reorder' of SNSs this + * NodeEntry remembers the original parent, name and index + * for later revert as well as for the creation of the + * {@link #getWorkspaceId() workspace id}. Finally the revertInfo is + * used to find the target of an Event indicating external + * modification. + * + * @see #refresh(Event) + */ + private RevertInfo revertInfo; + + /** + * Creates a new NodeEntryImpl + * + * @param parent the NodeEntry that owns this child item + * reference. + * @param name the name of the child node. + * @param factory the entry factory. + */ + private NodeEntryImpl(NodeEntryImpl parent, Name name, String uniqueID, + EntryFactory factory) { + super(parent, name, factory); + this.uniqueID = uniqueID; // NOTE: don't use setUniqueID (for mod only) + + properties = new ChildPropertyEntriesImpl(this, factory); + childNodeEntries = new ChildNodeEntriesImpl(this, factory, null); + + propertiesInAttic = new HashMap(); + childNodeAttic = new ChildNodeAttic(); + + factory.notifyEntryCreated(this); + } + + /** + * @return the entry corresponding to the root node. + */ + static NodeEntry createRootEntry(EntryFactory factory) { + return new NodeEntryImpl(null, NameConstants.ROOT, null, factory); + } + + /** + * @param parent + * @param name + * @param uniqueId + * @param factory + * @return the created entry. + */ + static NodeEntry createNodeEntry(NodeEntryImpl parent, Name name, String uniqueId, EntryFactory factory) { + return new NodeEntryImpl(parent, name, uniqueId, factory); + } + + //-----------------------------------------------------< HierarchyEntry >--- + /** + * Returns true. + * + * @see HierarchyEntry#denotesNode() + */ + public boolean denotesNode() { + return true; + } + + /** + * If 'recursive' is true, the complete hierarchy below this entry is + * traversed and reloaded. Otherwise only this entry and the direct + * descendants are reloaded. + * + * @see HierarchyEntry#reload(boolean) + */ + @Override + public void reload(boolean recursive) { + // reload this entry + super.reload(recursive); + + // reload all children unless 'recursive' is false and the reload above + // did not cause this entry to be removed -> therefore check status. + if (recursive && !Status.isTerminal(getStatus())) { + // recursively reload all entries including props that are in the attic. + for (Iterator it = getAllChildEntries(true); it.hasNext();) { + HierarchyEntry ce = it.next(); + ce.reload(recursive); + } + } + } + + /** + * Calls {@link HierarchyEntryImpl#revert()} and moves all properties from the + * attic back into the properties map. If this HierarchyEntry has been + * transiently moved, it is in addition moved back to its old parent. + * Similarly reordering of child node entries is reverted. + * + * @see HierarchyEntry#revert() + */ + @Override + public void revert() throws RepositoryException { + // move all properties from attic back to properties map + if (!propertiesInAttic.isEmpty()) { + properties.addAll(propertiesInAttic.values()); + propertiesInAttic.clear(); + } + // NOTE: childNodeAttic must not be cleared for the move of child entries + // will be separately reverted. + + // now make sure the attached state is reverted to the original state + super.revert(); + } + + /** + * @see HierarchyEntry#transientRemove() + */ + @Override + public void transientRemove() throws RepositoryException { + for (Iterator it = getAllChildEntries(false); it.hasNext();) { + HierarchyEntry ce = it.next(); + ce.transientRemove(); + } + + if (!propertiesInAttic.isEmpty()) { + // move all properties from attic back to properties map + properties.addAll(propertiesInAttic.values()); + propertiesInAttic.clear(); + } + + // execute for this entry as well + super.transientRemove(); + } + + /** + * @see HierarchyEntry#remove() + */ + @Override + public void remove() { + // handle this entry first + super.internalRemove(false); + boolean staleParent = (getStatus() == Status.STALE_DESTROYED); + // now remove all child-entries (or mark them accordingly) + for (Iterator it = getAllChildEntries(true); it.hasNext();) { + HierarchyEntryImpl ce = (HierarchyEntryImpl) it.next(); + ce.internalRemove(staleParent); + } + } + + @Override + void internalRemove(boolean staleParent) { + // handle this entry first + super.internalRemove(staleParent); + staleParent = (staleParent || (getStatus() == Status.STALE_DESTROYED)); + + // now remove all child-entries (or mark them accordingly) + for (Iterator it = getAllChildEntries(true); it.hasNext();) { + HierarchyEntryImpl ce = (HierarchyEntryImpl) it.next(); + ce.internalRemove(staleParent); + } + } + + /** + * @see HierarchyEntry#complete(Operation) + */ + public void complete(Operation operation) throws RepositoryException { + if (operation instanceof AddNode) { + complete((AddNode) operation); + } else if (operation instanceof AddProperty) { + complete((AddProperty) operation); + } else if (operation instanceof SetMixin) { + complete((SetMixin) operation); + } else if (operation instanceof SetPrimaryType) { + complete((SetPrimaryType) operation); + } else if (operation instanceof Remove) { + complete((Remove) operation); + } else if (operation instanceof ReorderNodes) { + complete((ReorderNodes) operation); + } else if (operation instanceof Move) { + complete((Move) operation); + } else { + throw new IllegalArgumentException(); + } + } + //----------------------------------------------------------< NodeEntry >--- + /** + * @see NodeEntry#getId() + */ + public NodeId getId() throws InvalidItemStateException, RepositoryException { + return getId(false); + } + + /** + * @see NodeEntry#getWorkspaceId() + */ + public NodeId getWorkspaceId() throws InvalidItemStateException, RepositoryException { + return getId(true); + } + + private NodeId getId(boolean wspId) throws RepositoryException { + if (parent == null) { // shortcut for root + return getIdFactory().createNodeId((String) null, getPathFactory().getRootPath()); + } + else if (uniqueID != null) { // shortcut for uniqueID based IDs + return getIdFactory().createNodeId(uniqueID); + } + else { + return buildNodeId(this, getPathFactory(), getIdFactory(), wspId); + } + } + + private static NodeId buildNodeId(NodeEntryImpl entry, PathFactory pathFactory, IdFactory idFactory, + boolean wspId) throws RepositoryException { + + PathBuilder pathBuilder = new PathBuilder(pathFactory); + while (entry.getParent() != null && entry.getUniqueID() == null) { + pathBuilder.addFirst(entry.getName(wspId), entry.getIndex(wspId)); + entry = (wspId && entry.revertInfo != null) + ? entry.revertInfo.oldParent + : entry.parent; + } + + // We either walked up to an entry below root or up to an uniqueID. In the former + // case we construct an NodeId with an absolute path. In the latter case we construct + // a NodeId from an uuid and a relative path. + if (entry.getParent() == null) { + pathBuilder.addRoot(); + return idFactory.createNodeId((String) null, pathBuilder.getPath()); + } + else { + return idFactory.createNodeId(entry.getUniqueID(), pathBuilder.getPath()); + } + } + + /** + * @see NodeEntry#getUniqueID() + */ + public String getUniqueID() { + return uniqueID; + } + + /** + * @see NodeEntry#setUniqueID(String) + */ + public void setUniqueID(String uniqueID) { + String old = this.uniqueID; + boolean mod = (uniqueID == null) ? old != null : !uniqueID.equals(old); + if (mod) { + this.uniqueID = uniqueID; + factory.notifyIdChange(this, old); + } + } + + /** + * @see NodeEntry#getIndex() + */ + public int getIndex() throws InvalidItemStateException, RepositoryException { + return getIndex(false); + } + + /** + * @see NodeEntry#getNodeState() + */ + public NodeState getNodeState() throws ItemNotFoundException, RepositoryException { + return (NodeState) getItemState(); + } + + /** + * @see NodeEntry#getDeepNodeEntry(Path) + */ + public NodeEntry getDeepNodeEntry(Path path) throws PathNotFoundException, RepositoryException { + NodeEntryImpl entry = this; + Path.Element[] elems = path.getElements(); + for (int i = 0; i < elems.length; i++) { + Path.Element elem = elems[i]; + // check for root element + if (elem.denotesRoot()) { + if (entry.getParent() != null) { + throw new RepositoryException("NodeEntry out of 'hierarchy' " + path.toString()); + } + continue; + } + + int index = elem.getNormalizedIndex(); + Name name = elem.getName(); + + // first try to resolve to known node or property entry + NodeEntry cne = entry.getNodeEntry(name, index, false); + if (cne != null) { + entry = (NodeEntryImpl) cne; + } else { + // no valid entry + // -> if cnes are complete -> assume that it doesn't exist. + // refresh will bring up new entries added in the mean time + // on the persistent layer. + if (entry.childNodeEntries.isComplete()) { + throw new PathNotFoundException(factory.saveGetJCRPath(path)); + } + // -> check for moved child entry in node-attic + // -> check if child points to a removed/moved sns + List siblings = entry.childNodeEntries.get(name); + if (entry.containsAtticChild(siblings, name, index)) { + throw new PathNotFoundException(factory.saveGetJCRPath(path)); + } + // shortcut: entry is NEW and still unresolved remaining path + // elements -> hierarchy doesn't exist anyway. + if (entry.getStatus() == Status.NEW) { + throw new PathNotFoundException(factory.saveGetJCRPath(path)); + } + /* + * Unknown entry (not-existing or not yet loaded): + * Skip all intermediate entries and directly try to load the ItemState + * (including building the intermediate entries. If that fails + * ItemNotFoundException is thrown. + * + * Since 'path' might be ambiguous (Node or Property): + * 1) first try Node + * 2) if the NameElement does not have SNS-index => try Property + * 3) else throw + */ + PathBuilder pb = new PathBuilder(getPathFactory()); + for (int j = i; j < elems.length; j++) { + pb.addLast(elems[j]); + } + Path remainingPath = pb.getPath(); + + NodeId parentId = entry.getWorkspaceId(); + IdFactory idFactory = factory.getIdFactory(); + + NodeId nodeId = idFactory.createNodeId(parentId, remainingPath); + NodeEntry ne = entry.loadNodeEntry(nodeId); + if (ne != null) { + return ne; + } else { + throw new PathNotFoundException(factory.saveGetJCRPath(path)); + } + } + } + return entry; + } + + /** + * @see NodeEntry#getDeepPropertyEntry(Path) + */ + public PropertyEntry getDeepPropertyEntry(Path path) throws PathNotFoundException, RepositoryException { + NodeEntryImpl entry = this; + Path.Element[] elems = path.getElements(); + int i = 0; + for (; i < elems.length-1; i++) { + Path.Element elem = elems[i]; + if (elems[i].denotesRoot()) { + if (entry.getParent() != null) { + throw new RepositoryException("NodeEntry out of 'hierarchy' " + path.toString()); + } + continue; + } + + int index = elem.getNormalizedIndex(); + Name name = elem.getName(); + + // first try to resolve to known node or property entry + NodeEntry cne = entry.getNodeEntry(name, index, false); + if (cne != null) { + entry = (NodeEntryImpl) cne; + } else { + // no valid ancestor node entry + // -> if cnes are complete -> assume that it doesn't exist. + // refresh will bring up new entries added in the mean time + // on the persistent layer. + if (entry.childNodeEntries.isComplete()) { + throw new PathNotFoundException(factory.saveGetJCRPath(path)); + } + // -> check for moved child entry in node-attic + // -> check if child points to a removed/moved sns + List siblings = entry.childNodeEntries.get(name); + if (entry.containsAtticChild(siblings, name, index)) { + throw new PathNotFoundException(factory.saveGetJCRPath(path)); + } + // break out of the loop and start deep loading the property + break; + } + } + + int st = entry.getStatus(); + PropertyEntry pe; + if (i == elems.length-1 && Status.INVALIDATED != st && Status._UNDEFINED_ != st) { + // all node entries present in the hierarchy and the direct ancestor + // has already been resolved and isn't invalidated -> no need to + // retrieve property entry from SPI + pe = entry.properties.get(path.getName()); + } else { + /* + * Unknown parent entry (not-existing or not yet loaded) or a parent + * entry that has been invalidated: + * Skip all intermediate entries and directly try to load the + * PropertyState (including building the intermediate entries. If that + * fails ItemNotFoundException is thrown. + */ + PathBuilder pb = new PathBuilder(getPathFactory()); + for (int j = i; j < elems.length; j++) { + pb.addLast(elems[j]); + } + Path remainingPath = pb.getPath(); + + IdFactory idFactory = getIdFactory(); + NodeId parentId = entry.getWorkspaceId(); + if (remainingPath.getLength() != 1) { + parentId = idFactory.createNodeId(parentId, remainingPath.getAncestor(1)); + } + PropertyId propId = idFactory.createPropertyId(parentId, remainingPath.getName()); + pe = entry.loadPropertyEntry(propId); + } + + if (pe == null) { + throw new PathNotFoundException(factory.saveGetJCRPath(path)); + } + return pe; + } + + /** + * @see NodeEntry#lookupDeepEntry(Path) + */ + public HierarchyEntry lookupDeepEntry(Path workspacePath) { + NodeEntryImpl entry = this; + for (int i = 0; i < workspacePath.getLength(); i++) { + Path.Element elem = workspacePath.getElements()[i]; + // check for root element + if (elem.denotesRoot()) { + if (getParent() != null) { + log.warn("NodeEntry out of 'hierarchy'" + workspacePath.toString()); + return null; + } + continue; + } + + int index = elem.getNormalizedIndex(); + Name childName = elem.getName(); + + // first try to resolve node + NodeEntry cne = entry.lookupNodeEntry(null, childName, index); + if (cne != null) { + entry = (NodeEntryImpl) cne; + } else if (index == Path.INDEX_DEFAULT && i == workspacePath.getLength() - 1) { + // property must not have index && must be final path element + return entry.lookupPropertyEntry(childName); + } else { + return null; + } + } + return entry; + } + + /** + * @see NodeEntry#hasNodeEntry(Name) + */ + public synchronized boolean hasNodeEntry(Name nodeName) { + List namedEntries = childNodeEntries.get(nodeName); + if (namedEntries.isEmpty()) { + return false; + } else { + return EntryValidation.containsValidNodeEntry(namedEntries.iterator()); + } + } + + /** + * @see NodeEntry#hasNodeEntry(Name, int) + */ + public synchronized boolean hasNodeEntry(Name nodeName, int index) { + try { + return getNodeEntry(nodeName, index) != null; + } catch (RepositoryException e) { + log.debug("Unable to determine if a child node with name " + nodeName + " exists."); + return false; + } + } + + /** + * @see NodeEntry#getNodeEntry(Name, int) + */ + public synchronized NodeEntry getNodeEntry(Name nodeName, int index) throws RepositoryException { + return getNodeEntry(nodeName, index, false); + } + + /** + * @see NodeEntry#getNodeEntry(Name, int, boolean) + */ + public NodeEntry getNodeEntry(Name nodeName, int index, boolean loadIfNotFound) throws RepositoryException { + List entries = childNodeEntries.get(nodeName); + NodeEntry cne = null; + if (entries.size() >= index) { + // position of entry might differ from index-1 if a SNS with lower + // index has been transiently removed. + int eIndex = 1; + for (int i = 0; i < entries.size() && cne == null; i++) { + NodeEntry ne = entries.get(i); + if (EntryValidation.isValidNodeEntry(ne)) { + if (eIndex == index) { + cne = ne; + } + eIndex++; + } + } + } + + if (cne == null && loadIfNotFound + && !containsAtticChild(entries, nodeName, index) + && !childNodeEntries.isComplete()) { + + NodeId cId = getIdFactory().createNodeId(getWorkspaceId(), + getPathFactory().create(nodeName, index)); + cne = loadNodeEntry(cId); + } + return cne; + } + + /** + * @see NodeEntry#getNodeEntries() + */ + public synchronized Iterator getNodeEntries() throws RepositoryException { + Collection entries = new ArrayList(); + for (Iterator it = getCompleteChildNodeEntries().iterator(); it.hasNext();) { + NodeEntry entry = it.next(); + if (EntryValidation.isValidNodeEntry(entry)) { + entries.add(entry); + } + } + return new RangeIteratorAdapter(Collections.unmodifiableCollection(entries)); + } + + /** + * @see NodeEntry#getNodeEntries(Name) + */ + public synchronized List getNodeEntries(Name nodeName) throws RepositoryException { + List namedEntries = getCompleteChildNodeEntries().get(nodeName); + if (namedEntries.isEmpty()) { + return Collections.emptyList(); + } else { + List entries = new ArrayList(); + // get array of the list, since during validation the childNodeEntries + // may be modified if upon NodeEntry.getItemState the entry gets removed. + NodeEntry[] arr = namedEntries.toArray(new NodeEntry[namedEntries.size()]); + for (int i = 0; i < arr.length; i++) { + NodeEntry cne = arr[i]; + if (EntryValidation.isValidNodeEntry(cne)) { + entries.add(cne); + } + } + return Collections.unmodifiableList(entries); + } + } + + /** + * @see NodeEntry#setNodeEntries(Iterator) + */ + public void setNodeEntries(Iterator childInfos) throws RepositoryException { + if (childNodeAttic.isEmpty()) { + ((ChildNodeEntriesImpl) childNodeEntries).update(childInfos); + } else { + // filter those entries that have been moved to the attic. + List remaining = new ArrayList(); + while (childInfos.hasNext()) { + ChildInfo ci = childInfos.next(); + if (!childNodeAttic.contains(ci.getName(), ci.getIndex(), ci.getUniqueID())) { + remaining.add(ci); + } + } + ((ChildNodeEntriesImpl) childNodeEntries).update(remaining.iterator()); + } + } + + /** + * @see NodeEntry#getOrAddNodeEntry(Name, int, String) + */ + public NodeEntry getOrAddNodeEntry(Name nodeName, int index, String uniqueID) throws RepositoryException { + NodeEntry ne = lookupNodeEntry(uniqueID, nodeName, index); + if (ne == null) { + ne = internalAddNodeEntry(nodeName, uniqueID, index); + } else { + log.debug("Child NodeEntry already exists -> didn't add."); + } + return ne; + } + + /** + * @see NodeEntry#addNewNodeEntry(Name, String, Name, QNodeDefinition) + */ + public NodeEntry addNewNodeEntry(Name nodeName, String uniqueID, + Name primaryNodeType, QNodeDefinition definition) throws RepositoryException { + NodeEntry entry = internalAddNodeEntry(nodeName, uniqueID, Path.INDEX_UNDEFINED); + NodeState state = getItemStateFactory().createNewNodeState(entry, primaryNodeType, definition); + entry.setItemState(state); + return entry; + } + + /** + * @see NodeEntry#hasPropertyEntry(Name) + */ + public synchronized boolean hasPropertyEntry(Name propName) { + PropertyEntry entry = properties.get(propName); + return EntryValidation.isValidPropertyEntry(entry); + } + + /** + * @see NodeEntry#getPropertyEntry(Name) + */ + public synchronized PropertyEntry getPropertyEntry(Name propName) { + PropertyEntry entry = properties.get(propName); + if (EntryValidation.isValidPropertyEntry(entry)) { + return entry; + } else { + return null; + } + } + + /** + * Ignores the loadIfNotFound flag due to the fact, that + * {@link org.apache.jackrabbit.spi.NodeInfo#getPropertyIds()} returns the + * complete list of property names currently available. + * @see NodeEntry#getPropertyEntry(Name, boolean) + */ + public PropertyEntry getPropertyEntry(Name propName, boolean loadIfNotFound) throws RepositoryException { + return getPropertyEntry(propName); + } + + /** + * @see NodeEntry#getPropertyEntries() + */ + public synchronized Iterator getPropertyEntries() { + Collection props; + if (getStatus() == Status.EXISTING_MODIFIED) { + // filter out removed properties + props = new ArrayList(); + // use array since upon validation the entry might be removed. + Object[] arr = properties.getPropertyEntries().toArray(); + for (int i = 0; i < arr.length; i++) { + PropertyEntry propEntry = (PropertyEntry) arr[i]; + if (EntryValidation.isValidPropertyEntry(propEntry)) { + props.add(propEntry); + } + } + } else { + // no need to filter out properties, there are no removed properties + props = properties.getPropertyEntries(); + } + return new RangeIteratorAdapter(Collections.unmodifiableCollection(props)); + } + + /** + * @see NodeEntry#getOrAddPropertyEntry(Name) + */ + public PropertyEntry getOrAddPropertyEntry(Name propName) throws ItemExistsException { + PropertyEntry pe = lookupPropertyEntry(propName); + if (pe == null) { + pe = internalAddPropertyEntry(propName, true); + } else { + log.debug("Child PropertyEntry already exists -> didn't add."); + } + return pe; + } + + /** + * @see NodeEntry#setPropertyEntries(Collection) + */ + public void setPropertyEntries(Collection propNames) throws ItemExistsException, RepositoryException { + Set diff = new HashSet(); + diff.addAll(properties.getPropertyNames()); + boolean containsExtra = diff.removeAll(propNames); + + // add all entries that are missing + for (Iterator it = propNames.iterator(); it.hasNext();) { + Name propName = it.next(); + if (!properties.contains(propName)) { + // TODO: check again. + // setPropertyEntries is used by WorkspaceItemStateFactory upon + // creating a NodeState, in which case the uuid/mixins are set + // anyway and not need exists to explicitly load the corresponding + // property state in order to retrieve the values. + internalAddPropertyEntry(propName, false); + } + } + + // if this entry has not yet been resolved or if it is 'invalidated' + // all property entries, that are not contained within the specified + // collection of property names are removed from this NodeEntry. + ItemState state = internalGetItemState(); + if (containsExtra && (state == null || state.getStatus() == Status.INVALIDATED)) { + for (Iterator it = diff.iterator(); it.hasNext();) { + Name propName = it.next(); + PropertyEntry pEntry = properties.get(propName); + if (pEntry != null) { + pEntry.remove(); + } + } + } + } + + /** + * @see NodeEntry#addNewPropertyEntry(Name, QPropertyDefinition, QValue[], int) + */ + public PropertyEntry addNewPropertyEntry(Name propName, QPropertyDefinition definition, QValue[] values, int propertyType) + throws ItemExistsException, RepositoryException { + // check for an existing property + PropertyEntry existing = properties.get(propName); + if (existing != null) { + try { + PropertyState existingState = existing.getPropertyState(); + int status = existingState.getStatus(); + if (Status.isTerminal(status)) { + // an old property-entry that is not valid any more + properties.remove(existing); + } else if (status == Status.EXISTING_REMOVED) { + // transiently removed -> move it to the attic + propertiesInAttic.put(propName, existing); + } else { + // existing is still existing -> cannot add same-named property + throw new ItemExistsException(propName.toString()); + } + } catch (ItemNotFoundException e) { + // entry does not exist on the persistent layer + // -> therefore remove from properties map + properties.remove(existing); + } catch (RepositoryException e) { + // some other error -> remove from properties map + properties.remove(existing); + } + } + + PropertyEntry entry = factory.createPropertyEntry(this, propName); + PropertyState state = getItemStateFactory().createNewPropertyState(entry, definition, values, propertyType); + entry.setItemState(state); + + // add the property entry if creating the new state was successful + properties.add(entry); + + return entry; + } + + /** + * @see NodeEntry#orderBefore(NodeEntry) + */ + public void orderBefore(NodeEntry beforeEntry) throws RepositoryException { + if (Status.NEW == getStatus()) { + // new states get remove upon revert + parent.childNodeEntries.reorder(this, beforeEntry); + } else { + createRevertInfo(); + // now reorder child entries on parent + parent.childNodeEntries.reorder(this, beforeEntry); + } + } + + /** + * @see NodeEntry#move(Name, NodeEntry, boolean) + */ + public NodeEntry move(Name newName, NodeEntry newParent, boolean transientMove) throws RepositoryException { + if (parent == null) { + // the root may never be moved + throw new RepositoryException("Root cannot be moved."); + } + + // for existing nodeEntry that are 'moved' for the first time, the + // original data must be stored and this entry is moved to the attic. + if (transientMove) { + createRevertInfo(); + if (Status.NEW != getStatus()) { + if (newParent != revertInfo.oldParent) { + revertInfo.oldParent.childNodeAttic.add(this); + } else { + // entry is either rename OR moved back to it's original + // parent. for the latter case make sure, there is no attic + // entry remaining referring to the entry that is being added. + revertInfo.oldParent.childNodeAttic.remove(this); + } + } + } + + NodeEntry entry = parent.childNodeEntries.remove(this); + if (entry != this) { + // should never occur + String msg = "Internal error. Attempt to move NodeEntry (" + getName() + ") which is not connected to its parent."; + log.error(msg); + throw new RepositoryException(msg); + } + // set name and parent to new values + parent = (NodeEntryImpl) newParent; + name = newName; + // register entry with its new parent + parent.childNodeEntries.add(this); + return this; + } + + /** + * @see NodeEntry#isTransientlyMoved() + */ + public boolean isTransientlyMoved() { + return revertInfo != null && revertInfo.isMoved(); + } + + /** + * @see NodeEntry#refresh(Event) + */ + public void refresh(Event childEvent) { + ItemId eventId = childEvent.getItemId(); + Path eventPath = childEvent.getPath(); + Name eventName = eventPath.getName(); + HierarchyEntry child = eventId == null ? null : lookupEntry(eventId, eventPath); + + switch (childEvent.getType()) { + case Event.NODE_ADDED: + case Event.PROPERTY_ADDED: + if (child == null || child.getStatus() == Status.REMOVED) { + // no such child or a colliding new child existed but got + // removed already -> add the new entry. + if (childEvent.getType() == Event.NODE_ADDED) { + String uniqueChildID = (eventId.getPath() == null) ? eventId.getUniqueID() : null; + int index = eventPath.getNormalizedIndex(); + internalAddNodeEntry(eventName, uniqueChildID, index); + } else { + internalAddPropertyEntry(eventName, true); + } + } else { + // item already present + int status = child.getStatus(); + if (Status.NEW == status) { + // event conflicts with a transiently added item on this + // node entry -> mark the parent node (this) stale. + internalGetItemState().setStatus(Status.MODIFIED); + } // else: child already added -> ignore + } + break; + + case Event.NODE_REMOVED: + case Event.PROPERTY_REMOVED: + if (child != null) { + int status = child.getStatus(); + if (Status.EXISTING_REMOVED == status) { + // colliding item removal -> mark parent stale + internalGetItemState().setStatus(Status.MODIFIED); + } + child.remove(); + } // else: child-Entry has not been loaded yet -> ignore + break; + + case Event.PROPERTY_CHANGED: + if (child == null) { + // prop-Entry has not been loaded yet -> add propEntry + internalAddPropertyEntry(eventName, true); + } else if (child.isAvailable()) { + int status = child.getStatus(); + // if the child has pending changes -> stale. + // Reload data from server and try to merge them with the + // current session-state. if the latter is transiently + // modified and merge fails it must be marked STALE afterwards. + if (Status.isStale(status)) { + // ignore. nothing to do. + } else if (Status.isTransient(child.getStatus())) { + // pending changes -> don't reload entry but rather + // mark it stale + ((HierarchyEntryImpl) child).internalGetItemState().setStatus(Status.MODIFIED); + } else { + // no pending changes -> invalidate and force reload + // upon next access. + child.invalidate(false); + // special cases: jcr:uuid and jcr:mixinTypes affect the + // parent (i.e. this NodeEntry) + if (StateUtility.isUuidOrMixin(eventName)) { + notifyUUIDorMIXINModified((PropertyEntry) child); + } + } + } // else: existing entry but state not yet built -> ignore event + break; + case Event.NODE_MOVED: + // TODO: implementation missing + throw new UnsupportedOperationException("Implementation missing"); + //break; + case Event.PERSIST: + // TODO: implementation missing + throw new UnsupportedOperationException("Implementation missing"); + default: + // ILLEGAL + throw new IllegalArgumentException("Illegal event type " + childEvent.getType() + " for NodeState."); + } + } + //-------------------------------------------------< HierarchyEntryImpl >--- + /** + * @see HierarchyEntryImpl#doResolve() + *

        + * Returns a NodeState. + */ + @Override + ItemState doResolve() throws ItemNotFoundException, RepositoryException { + return getItemStateFactory().createNodeState(getWorkspaceId(), this); + } + + /** + * @see HierarchyEntryImpl#buildPath(boolean) + */ + @Override + Path buildPath(boolean wspPath) throws RepositoryException { + PathFactory pf = getPathFactory(); + // shortcut for root state + if (parent == null) { + return pf.getRootPath(); + } + // build path otherwise + PathBuilder builder = new PathBuilder(pf); + buildPath(builder, this, wspPath); + return builder.getPath(); + } + + /** + * Adds the path element of an item id to the path currently being built. + * On exit, builder contains the path of this entry. + * + * @param builder + * @param nEntry NodeEntryImpl of the state the path should be built for. + * @param wspPath true if the workspace path should be built + */ + private static void buildPath(PathBuilder builder, NodeEntryImpl nEntry, boolean wspPath) throws RepositoryException { + NodeEntryImpl parentEntry = (wspPath && nEntry.revertInfo != null) ? nEntry.revertInfo.oldParent : nEntry.parent; + // shortcut for root state + if (parentEntry == null) { + builder.addRoot(); + return; + } + + // recursively build path of parent + buildPath(builder, parentEntry, wspPath); + + int index = nEntry.getIndex(wspPath); + Name name = nEntry.getName(wspPath); + builder.addLast(name, index); + } + + //-----------------------------------------------< private || protected >--- + /** + * @param nodeName + * @param uniqueID + * @param index + * @return the added entry. + */ + private NodeEntry internalAddNodeEntry(Name nodeName, String uniqueID, int index) { + NodeEntry entry = factory.createNodeEntry(this, nodeName, uniqueID); + childNodeEntries.add(entry, index); + return entry; + } + + /** + * Internal method that adds a PropertyEntry without checking of that entry + * exists. + * + * @param propName + * @param notifySpecial + * @return the added entry. + */ + private PropertyEntry internalAddPropertyEntry(Name propName, boolean notifySpecial) { + PropertyEntry entry = factory.createPropertyEntry(this, propName); + properties.add(entry); + + // if property-name is jcr:uuid or jcr:mixin this affects this entry + // and the attached nodeState. + if (notifySpecial && StateUtility.isUuidOrMixin(propName)) { + notifyUUIDorMIXINModified(entry); + } + return entry; + } + + /** + * + * @param childEntry + */ + void internalRemoveChildEntry(HierarchyEntry childEntry) { + if (childEntry.denotesNode()) { + if (childNodeEntries.remove((NodeEntry) childEntry) == null) { + childNodeAttic.remove((NodeEntry) childEntry); + } + } else { + Name propName = childEntry.getName(); + PropertyEntry atticEntry = propertiesInAttic.get(propName); + if (atticEntry == null) { + properties.remove((PropertyEntry) childEntry); + } else if (atticEntry == childEntry) { + propertiesInAttic.remove(propName); + } // else: no such prop-entry. should not get here + + // special properties + if (StateUtility.isUuidOrMixin(propName)) { + notifyUUIDorMIXINRemoved(propName); + } + } + } + + @Override + protected void invalidateInternal(boolean recursive) { + if (recursive) { + // invalidate all child entries including properties present in the + // attic (removed props shadowed by a new property with the same name). + for (Iterator it = getAllChildEntries(true); it.hasNext();) { + HierarchyEntry ce = it.next(); + ce.invalidate(true); + } + } + super.invalidateInternal(true); + } + + /** + * @param oldName + * @param oldIndex + * @return true if the given oldName and oldIndex match + * {@link #getName(boolean)} and {@link #getIndex(boolean)}, respectively. + */ + boolean matches(Name oldName, int oldIndex) { + try { + return getName(true).equals(oldName) && getIndex(true) == oldIndex; + } catch (RepositoryException e) { + // should not get here + return false; + } + } + + /** + * @param oldName + * @return true if the given oldName matches + * {@link #getName(boolean)}. + */ + boolean matches(Name oldName) { + return getName(true).equals(oldName); + } + + + private Name getName(boolean wspName) { + if (wspName && revertInfo != null) { + return revertInfo.oldName; + } else { + return name; + } + } + + private int getIndex(boolean wspIndex) throws InvalidItemStateException, RepositoryException { + if (parent == null) { + // the root state may never have siblings + return Path.INDEX_DEFAULT; + } + + if (wspIndex && revertInfo != null) { + return revertInfo.oldIndex; + } else { + NodeState state = (NodeState) internalGetItemState(); + if (state == null || !state.hasDefinition() || state.getDefinition().allowsSameNameSiblings()) { + return parent.getChildIndex(this, wspIndex); + } else { + return Path.INDEX_DEFAULT; + } + } + } + + /** + * + * @param childId + * @return the entry or null if building the corresponding + * NodeState failed with ItemNotFoundException. + */ + private NodeEntry loadNodeEntry(NodeId childId) throws RepositoryException { + try { + NodeState state = getItemStateFactory().createDeepNodeState(childId, this); + return state.getNodeEntry(); + } catch (ItemNotFoundException e) { + return null; + } + } + + /** + * @param childId + * @return the entry or null if building the corresponding + * PropertyState failed with ItemNotFoundException. + * @throws ItemNotFoundException + * @throws RepositoryException + */ + private PropertyEntry loadPropertyEntry(PropertyId childId) throws RepositoryException { + try { + PropertyState state = getItemStateFactory().createDeepPropertyState(childId, this); + return (PropertyEntry) state.getHierarchyEntry(); + } catch (ItemNotFoundException e) { + return null; + } + } + + /** + * Searches the child-entries of this NodeEntry for a matching child. + * Since {@link #refresh(Event)} must always be called on the parent + * NodeEntry, there is no need to check if a given event id would point + * to this NodeEntry itself. + * + * @param eventId + * @param eventPath + * @return the entry or null if the matching entry has a status + * Status#NEW. + */ + private HierarchyEntry lookupEntry(ItemId eventId, Path eventPath) { + Name childName = eventPath.getName(); + HierarchyEntry child; + if (eventId.denotesNode()) { + String uniqueChildID = (eventId.getPath() == null) ? eventId.getUniqueID() : null; + int index = eventPath.getNormalizedIndex(); + child = lookupNodeEntry(uniqueChildID, childName, index); + } else { + child = lookupPropertyEntry(childName); + } + return child; + } + + private NodeEntry lookupNodeEntry(String uniqueChildId, Name childName, int index) { + NodeEntry child = null; + if (uniqueChildId != null) { + child = childNodeAttic.get(uniqueChildId); + if (child == null) { + child = childNodeEntries.get(childName, uniqueChildId); + } + } + if (child == null) { + child = childNodeAttic.get(childName, index); + if (child == null && childNodeEntries != null) { + child = childNodeEntries.get(childName, index); + } + } + return child; + } + + private PropertyEntry lookupPropertyEntry(Name childName) { + // for external prop-removal the attic must be consulted first + // in order not access a NEW prop shadowing a transiently removed + // property with the same name. + PropertyEntry child = propertiesInAttic.get(childName); + if (child == null) { + child = properties.get(childName); + } + return child; + } + + /** + * Deals with modified jcr:uuid and jcr:mixinTypes property. + * See {@link #notifyUUIDorMIXINRemoved(Name)} + * + * @param child + */ + private void notifyUUIDorMIXINModified(PropertyEntry child) { + try { + if (NameConstants.JCR_UUID.equals(child.getName())) { + PropertyState ps = child.getPropertyState(); + setUniqueID(ps.getValue().getString()); + } else if (NameConstants.JCR_MIXINTYPES.equals(child.getName())) { + NodeState state = (NodeState) internalGetItemState(); + if (state != null) { + PropertyState ps = child.getPropertyState(); + state.setMixinTypeNames(StateUtility.getMixinNames(ps)); + } // nodestate not yet loaded -> ignore change + } + } catch (ItemNotFoundException e) { + log.debug("Property with name " + child.getName() + " does not exist (anymore)"); + } catch (RepositoryException e) { + log.debug("Unable to access child property " + child.getName(), e.getMessage()); + } + } + + /** + * Deals with removed jcr:uuid and jcr:mixinTypes property. + * See {@link #notifyUUIDorMIXINModified(PropertyEntry)} + * + * @param propName + */ + private void notifyUUIDorMIXINRemoved(Name propName) { + if (NameConstants.JCR_UUID.equals(propName)) { + setUniqueID(null); + } else if (NameConstants.JCR_MIXINTYPES.equals(propName)) { + NodeState state = (NodeState) internalGetItemState(); + if (state != null) { + state.setMixinTypeNames(Name.EMPTY_ARRAY); + } + } + } + + /** + * @return The ChildNodeEntries defined for this + * NodeEntry. Please note, that this method never returns + * null, since the child node entries are loaded/reloaded + * in case they have not been loaded yet. + */ + private ChildNodeEntries getCompleteChildNodeEntries() throws InvalidItemStateException, RepositoryException { + try { + childNodeEntries.reload(); + } catch (ItemNotFoundException e) { + log.debug("NodeEntry does not exist (anymore) -> remove."); + remove(); + throw new InvalidItemStateException(e); + } + return childNodeEntries; + } + + /** + * Returns an Iterator over all children entries, that currently are loaded + * with this NodeEntry. NOTE, that if the childNodeEntries have not been + * loaded yet, no attempt is made to do so. + * + * @param includeAttic + * @return iterator over all children entries, that currently are loaded + * with this NodeEntry + */ + private Iterator getAllChildEntries(boolean includeAttic) { + IteratorChain chain = new IteratorChain(); + // attic + if (includeAttic) { + Collection attic = propertiesInAttic.values(); + chain.addIterator(new ArrayList(attic).iterator()); + } + // add props + synchronized (properties) { + Collection props = properties.getPropertyEntries(); + chain.addIterator(props.iterator()); + } + // add childNodeEntries + synchronized (childNodeEntries) { + chain.addIterator(childNodeEntries.iterator()); + } + return chain; + } + + /** + * Returns the index of the given NodeEntry. + * + * @param cne the NodeEntry instance. + * @param wspIndex if true transiently removed siblings are respected. + * @return the index of the child node entry. + * @throws ItemNotFoundException if the given entry isn't a valid child of + * this NodeEntry. + */ + private int getChildIndex(NodeEntry cne, boolean wspIndex) throws ItemNotFoundException, RepositoryException { + List sns = new ArrayList(childNodeEntries.get(cne.getName())); + + if (wspIndex) { + List atticSiblings = childNodeAttic.get(cne.getName()); + for (Iterator it = atticSiblings.iterator(); it.hasNext();) { + NodeEntryImpl sibl = it.next(); + if (sibl.revertInfo != null) { + sns.add(sibl.revertInfo.oldIndex - 1, sibl); + } else { + log.error("Sibling in attic doesn't have revertInfo...."); + } + } + } + + if (sns.isEmpty()) { + // the given node entry is not connected with his parent any more + // -> throw + String msg = "NodeEntry " + cne.getName() + " is disconnected from its parent -> remove."; + cne.remove(); + throw new InvalidItemStateException(msg); + + } else if (sns.size() == 1) { + // no siblings -> simply return the default index. + return Path.INDEX_DEFAULT; + + } else { + // siblings exist. + int index = Path.INDEX_DEFAULT; + for (Iterator it = sns.iterator(); it.hasNext(); ) { + NodeEntry entry = it.next(); + if (entry == cne) { // TODO see below + return index; + } + // for wsp index ignore all transiently added items. + // otherwise: skip entries that belong to removed or invalid states. + // NOTE, that in this case the nodestate must be available from the cne. + boolean isValid = (wspIndex) ? + EntryValidation.isValidWorkspaceNodeEntry(entry) : + EntryValidation.isValidNodeEntry(entry); + if (isValid) { + index++; + } + } + // not found, since child entries are only connected with soft refs + // to the LinkNode in ChildNodeEntries, equality may not determine + // the correct matching entry -> return default index. + return Path.INDEX_DEFAULT; + } + } + + /** + * Returns true if the attic contains a matching child entry or + * if any of the remaining child entries present in the siblings list has + * been modified in a way that its original index is equal to the given + * child index. + * + * @param siblings + * @param childName + * @param childIndex + * @return true if there is a child entry in the attic that + * matches the given name/index or if the siblings list contain a reordered + * entry that matches. + */ + private boolean containsAtticChild(List siblings, Name childName, int childIndex) { + // check if a matching entry exists in the attic + if (childNodeAttic.contains(childName, childIndex)) { + return true; + } + // special treatment for potentially moved/reordered/removed sns + // TODO: check again + if (childIndex > Path.INDEX_DEFAULT) { + List siblingsInAttic = childNodeAttic.get(childName); + if (siblings.size() < childIndex && childIndex <= siblings.size() + siblingsInAttic.size()) { + return true; + } + } + if (getStatus() == Status.EXISTING_MODIFIED) { + for (Iterator it = siblings.iterator(); it.hasNext();) { + NodeEntry child = it.next(); + if (!EntryValidation.isValidNodeEntry(child) || ((NodeEntryImpl)child).revertInfo != null && ((NodeEntryImpl)child).revertInfo.oldIndex == childIndex) { + return true; + } + } + } + return false; + } + /** + * If 'revertInfo' is null it gets created from the current information + * present on this entry. + */ + private void createRevertInfo() throws RepositoryException { + if (revertInfo == null && getStatus() != Status.NEW) { + revertInfo = new RevertInfo(); + } + } + + private void complete(AddNode operation) throws RepositoryException { + if (operation.getParentState().getHierarchyEntry() != this) { + throw new IllegalArgumentException(); + } + + for (Iterator it = operation.getAddedStates().iterator(); it.hasNext();) { + HierarchyEntry he = it.next().getHierarchyEntry(); + if (he.getStatus() == Status.NEW) { + switch (operation.getStatus()) { + case Operation.STATUS_PERSISTED: + ((HierarchyEntryImpl) he).internalGetItemState().setStatus(Status.EXISTING); + he.invalidate(false); + break; + case Operation.STATUS_UNDO: + he.revert(); + break; + default: // ignore + } + } // entry isn't NEW any more -> ignore + } + } + + private void complete(AddProperty operation) throws RepositoryException { + if (operation.getParentState().getHierarchyEntry() != this) { + throw new IllegalArgumentException(); + } + PropertyEntry pe = getPropertyEntry(operation.getPropertyName()); + if (pe != null && pe.getStatus() == Status.NEW) { + switch (operation.getStatus()) { + case Operation.STATUS_PERSISTED: + // for autocreated/protected props, mark to be reloaded + // upon next access. + PropertyState addedState = (PropertyState) ((PropertyEntryImpl) pe).internalGetItemState(); + addedState.setStatus(Status.EXISTING); + QPropertyDefinition pd = addedState.getDefinition(); + if (pd.isAutoCreated() || pd.isProtected()) { + pe.invalidate(true); + } // else: assume added property is up to date. + break; + case Operation.STATUS_UNDO: + pe.revert(); + break; + default: // ignore + } + } // else: no such prop entry or entry has already been persisted + // e.g due to external modifications merged into this NodeEntry. + } + + private void complete(Remove operation) throws RepositoryException { + HierarchyEntry rmEntry = operation.getRemoveState().getHierarchyEntry(); + if (rmEntry.getParent() != this) { + throw new IllegalArgumentException(); + } + switch (operation.getStatus()) { + case Operation.STATUS_PERSISTED: + if (Status.isTerminal(rmEntry.getStatus())) { + log.debug("Removal of State " + rmEntry + " has already been completed."); + } + rmEntry.remove(); + break; + case Operation.STATUS_UNDO: + if (!rmEntry.denotesNode()) { + Name propName = rmEntry.getName(); + if (propertiesInAttic.containsKey(propName)) { + properties.add(propertiesInAttic.remove(propName)); + } // else: propEntry has never been moved to the attic (see 'addPropertyEntry') + } + rmEntry.revert(); + break; + default: // ignore + } + + } + + private void complete(SetMixin operation) throws RepositoryException { + if (operation.getNodeState().getHierarchyEntry() != this) { + throw new IllegalArgumentException(); + } + PropertyEntry pe = getPropertyEntry(NameConstants.JCR_MIXINTYPES); + if (pe != null) { + PropertyState pState = pe.getPropertyState(); + switch (operation.getStatus()) { + case Operation.STATUS_PERSISTED: + Name[] mixins = StateUtility.getMixinNames(pState); + getNodeState().setMixinTypeNames(mixins); + if (pState.getStatus() == Status.NEW || pState.getStatus() == Status.EXISTING_MODIFIED) { + pState.setStatus(Status.EXISTING); + } + break; + case Operation.STATUS_UNDO: + pe.revert(); + break; + default: // ignore + } + } // else: no such prop-Entry (should not occur) + } + + private void complete(SetPrimaryType operation) throws RepositoryException { + if (operation.getNodeState().getHierarchyEntry() != this) { + throw new IllegalArgumentException(); + } + PropertyEntry pe = getPropertyEntry(NameConstants.JCR_PRIMARYTYPE); + if (pe != null) { + PropertyState pState = pe.getPropertyState(); + switch (operation.getStatus()) { + case Operation.STATUS_PERSISTED: + // NOTE: invalidation of this node entry is performed by + // ChangeLog.persisted... + // TODO: check if correct + if (pState.getStatus() == Status.NEW || pState.getStatus() == Status.EXISTING_MODIFIED) { + pState.setStatus(Status.EXISTING); + } + break; + case Operation.STATUS_UNDO: + pe.revert(); + break; + default: // ignore + } + } // else: no such prop-Entry (should not occur) + } + + private void complete(ReorderNodes operation) throws RepositoryException { + HierarchyEntry he = operation.getInsertNode().getHierarchyEntry(); + if (he != this) { + throw new IllegalArgumentException(); + } + // NOTE: if reorder occurred in combination with a 'move' the clean-up + // of the revertInfo is postponed until {@link #complete(Move)}. + switch (operation.getStatus()) { + case Operation.STATUS_PERSISTED: + if (revertInfo != null && !revertInfo.isMoved()) { + revertInfo.dispose(true); + } + break; + case Operation.STATUS_UNDO: + if (he.getStatus() == Status.NEW) { + he.revert(); + } else if (revertInfo != null && !revertInfo.isMoved()) { + revertInfo.dispose(false); + } + break; + default: // ignore + } + } + + private void complete(Move operation) throws RepositoryException { + HierarchyEntry he = operation.getSourceState().getHierarchyEntry(); + if (he != this) { + throw new IllegalArgumentException(); + } + switch (operation.getStatus()) { + case Operation.STATUS_PERSISTED: + if (getStatus() != Status.NEW && revertInfo != null) { + revertInfo.oldParent.childNodeAttic.remove(this); + revertInfo.dispose(true); + } + // and mark the moved state existing + // internalGetItemState().setStatus(Status.EXISTING); + break; + case Operation.STATUS_UNDO: + if (getStatus() == Status.NEW) { + revert(); + } else if (revertInfo != null) { + revertMove(); + revertInfo.dispose(false); + } + break; + default: // ignore + } + } + + private void revertMove() { + NodeEntryImpl oldParent = revertInfo.oldParent; + if (oldParent == parent) { + // simple renaming + parent.childNodeEntries.remove(this); + } else { + // move NodeEntry back to its original parent + parent.childNodeEntries.remove(this); + oldParent.childNodeAttic.remove(this); + + // now restore moved entry with the old name and index and re-add + // it to its original parent (unless it got destroyed) + parent = oldParent; + } + // now restore moved entry with the old name and index and re-add + // it to its original parent + name = revertInfo.oldName; + parent.childNodeEntries.add(this, revertInfo.oldIndex, revertInfo.oldSuccessor); + } + + //--------------------------------------------------------< inner class >--- + /** + * Upon move or reorder of this entry the original hierarchy information is + * stored in the RevertInfo for later operation undo and in order to be able + * to build the workspace id / path. + */ + private class RevertInfo { + + private final NodeEntryImpl oldParent; + private final Name oldName; + private final int oldIndex; + private final NodeEntry oldSuccessor; + private final NodeEntry oldPredecessor; + + private RevertInfo() throws InvalidItemStateException, RepositoryException { + this.oldParent = parent; + this.oldName = name; + this.oldIndex = getIndex(); + this.oldSuccessor = ((ChildNodeEntriesImpl) parent.childNodeEntries).getNext(NodeEntryImpl.this); + this.oldPredecessor = ((ChildNodeEntriesImpl) parent.childNodeEntries).getPrevious(NodeEntryImpl.this); + } + + private boolean isMoved() { + return oldParent != getParent() || !getName().equals(oldName); + } + + private void dispose(boolean persisted) { + if (!persisted) { + NodeEntry ne = NodeEntryImpl.this; + ChildNodeEntriesImpl parentCNEs = (ChildNodeEntriesImpl) parent.childNodeEntries; + parentCNEs.reorderAfter(ne, revertInfo.oldPredecessor); + try { + if (oldIndex != ne.getIndex()) { + // TODO: TOBEFIXED + log.warn("Reverting didn't restore the correct index."); + } + } catch (RepositoryException e) { + log.warn("Unable to calculate index. {}", e.getMessage()); + } + } + revertInfo = null; + } + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/PropertyEntry.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/PropertyEntry.java new file mode 100644 index 00000000000..9be1e2856df --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/PropertyEntry.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.jcr2spi.hierarchy; + +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.spi.PropertyId; + +import javax.jcr.RepositoryException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.InvalidItemStateException; + +/** + * PropertyEntry... + */ +public interface PropertyEntry extends HierarchyEntry { + + /** + * @return the NodeId of this child node entry. + */ + public PropertyId getId() throws InvalidItemStateException, RepositoryException; + + /** + * Returns the ID that must be used for resolving this entry OR loading its + * children entries from the persistent layer. This is the same as + * getId() unless any of its ancestors has been transiently + * moved. + * + * @return + * @see #getId() + */ + public PropertyId getWorkspaceId() throws InvalidItemStateException, RepositoryException; + + /** + * @return the referenced PropertyState. + * @throws ItemNotFoundException if the PropertyState does not + * exist anymore. + * @throws RepositoryException if an error occurs while retrieving the + * PropertyState. + */ + public PropertyState getPropertyState() throws ItemNotFoundException, RepositoryException; + +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/PropertyEntryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/PropertyEntryImpl.java new file mode 100644 index 00000000000..2902f46d483 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/PropertyEntryImpl.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.jcr2spi.hierarchy; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.SetPropertyValue; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; + +/** + * PropertyEntryImpl implements a reference to a property state. + */ +public class PropertyEntryImpl extends HierarchyEntryImpl implements PropertyEntry { + + /** + * Creates a new PropertyEntryImpl. + * + * @param parent the parent NodeEntry where the property + * belongs to. + * @param name the name of the property. + * @param factory + */ + private PropertyEntryImpl(NodeEntryImpl parent, Name name, EntryFactory factory) { + super(parent, name, factory); + } + + /** + * Creates a new PropertyEntry. + * + * @param parent + * @param name + * @param factory + * @return new PropertyEntry + */ + static PropertyEntry create(NodeEntryImpl parent, Name name, EntryFactory factory) { + return new PropertyEntryImpl(parent, name, factory); + } + + //------------------------------------------------------< HierarchyEntryImpl >--- + /** + * @see HierarchyEntryImpl#doResolve() + *

        + * Returns a PropertyState. + */ + @Override + ItemState doResolve() throws ItemNotFoundException, RepositoryException { + return getItemStateFactory().createPropertyState(getWorkspaceId(), this); + } + + /** + * @see HierarchyEntryImpl#buildPath(boolean) + */ + @Override + Path buildPath(boolean workspacePath) throws RepositoryException { + Path parentPath = parent.buildPath(workspacePath); + return getPathFactory().create(parentPath, getName(), true); + } + + //------------------------------------------------------< PropertyEntry >--- + /** + * @see PropertyEntry#getId() + */ + public PropertyId getId() throws InvalidItemStateException, RepositoryException { + return getIdFactory().createPropertyId(parent.getId(), getName()); + } + + /** + * @see PropertyEntry#getWorkspaceId() + */ + public PropertyId getWorkspaceId() throws InvalidItemStateException, RepositoryException { + return getIdFactory().createPropertyId(parent.getWorkspaceId(), getName()); + } + + /** + * @see PropertyEntry#getPropertyState() + */ + public PropertyState getPropertyState() throws ItemNotFoundException, RepositoryException { + return (PropertyState) getItemState(); + } + + //-----------------------------------------------------< HierarchyEntry >--- + /** + * Returns false. + * + * @see HierarchyEntry#denotesNode() + */ + public boolean denotesNode() { + return false; + } + + /** + * @see HierarchyEntry#complete(Operation) + */ + public void complete(Operation operation) throws RepositoryException { + if (!(operation instanceof SetPropertyValue)) { + throw new IllegalArgumentException(); + } + SetPropertyValue op = (SetPropertyValue) operation; + if (op.getPropertyState().getHierarchyEntry() != this) { + throw new IllegalArgumentException(); + } + switch (operation.getStatus()) { + case Operation.STATUS_PERSISTED: + // Property can only be the change log target if it was existing + // and has been modified. This includes the case where a property + // was changed and then removed by removing its parent. See JCR-2462. + // Removal, add and implicit modification of protected + // properties must be persisted by save on parent. + PropertyState state = op.getPropertyState(); + if (state.getStatus() != Status.REMOVED) { + state.setStatus(Status.EXISTING); + } + break; + case Operation.STATUS_UNDO: + revert(); + break; + default: + // ignore + } + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/UniqueIdResolver.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/UniqueIdResolver.java new file mode 100644 index 00000000000..4af09a5ea6f --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/hierarchy/UniqueIdResolver.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.jcr2spi.hierarchy; + +import org.apache.jackrabbit.jcr2spi.state.ItemStateCreationListener; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateLifeCycleListener; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.jcr2spi.state.ItemStateFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.commons.collections.map.ReferenceMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.ItemNotFoundException; +import java.util.Map; + +/** + * UniqueIdResolver allows to retrieve NodeEntry instances + * that are identified by a uniqueID. + */ +public class UniqueIdResolver implements ItemStateCreationListener, EntryFactory.NodeEntryListener { + + private static Logger log = LoggerFactory.getLogger(UniqueIdResolver.class); + + private final ItemStateFactory isf; + + /** + * Maps a String uniqueID to a {@link NodeEntry}. + */ + private final Map lookUp; + + /** + * Creates a new UniqueIdResolver. + */ + public UniqueIdResolver(ItemStateFactory isf) { + this.lookUp = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT); + this.isf = isf; + isf.addCreationListener(this); + } + + public void dispose() { + isf.removeCreationListener(this); + lookUp.clear(); + } + + public NodeEntry lookup(String uniqueId) { + if (uniqueId == null) { + throw new IllegalArgumentException(); + } + return lookUp.get(uniqueId); + } + + public NodeEntry resolve(NodeId nodeId, NodeEntry rootEntry) throws ItemNotFoundException, RepositoryException { + NodeEntry entry = lookup(nodeId.getUniqueID()); + if (entry == null) { + NodeState state = isf.createDeepNodeState(nodeId, rootEntry); + entry = state.getNodeEntry(); + } + return entry; + } + + //-----------------------------------------< ItemStateLifeCycleListener >--- + /** + * Updates the internal id-lookup if the given state + * - is identify by a uniqueID and got removed + * - was modified and now is identified by a uniqueID + * - was modified and is not identified by a uniqueID any more + * + * @param state + * @param previousStatus + * @see ItemStateLifeCycleListener#statusChanged(ItemState, int) + */ + public void statusChanged(ItemState state, int previousStatus) { + synchronized (lookUp) { + if (Status.isTerminal((state.getStatus()))) { + if (state.isNode()) { + NodeEntry entry = (NodeEntry) state.getHierarchyEntry(); + String uniqueID = entry.getUniqueID(); + if (uniqueID != null) { + NodeEntry mapEntry = lookUp.get(uniqueID); + if (mapEntry == entry) { + lookUp.remove(uniqueID); + } // else: removed entry is not present in lookup but + // only it's replacement -> ignore + } + } + // stop listening if a state reached status REMOVED. + if (Status.REMOVED == state.getStatus()) { + state.removeListener(this); + } + } // else: any other status than REMOVED -> ignore. + } + } + + //------------------------------------------< ItemStateCreationListener >--- + /** + * Nothing to do. The lookUp is filled entry creation and/or modification + * of its uniqueID + * + * @param state + * @see ItemStateCreationListener#created(ItemState) + */ + public void created(ItemState state) { + if (state.isNode()) { + NodeEntry entry = (NodeEntry) state.getHierarchyEntry(); + String uniqueID = entry.getUniqueID(); + if (uniqueID != null) { + if (!lookUp.containsKey(uniqueID) || lookUp.get(uniqueID) != entry) { + log.error("Created NodeState identified by UniqueID that is not contained in the lookup."); + } + } + } + } + + //-------------------------------------< EntryFactory.NodeEntryListener >--- + /** + * @see EntryFactory.NodeEntryListener#entryCreated(NodeEntry) + */ + public void entryCreated(NodeEntry entry) { + synchronized (lookUp) { + String uniqueID = entry.getUniqueID(); + if (uniqueID != null) { + putToLookup(uniqueID, entry); + } + } + } + + /** + * @see EntryFactory.NodeEntryListener#uniqueIdChanged(NodeEntry, String) + */ + public void uniqueIdChanged(NodeEntry entry, String previousUniqueID) { + synchronized (lookUp) { + if (previousUniqueID != null) { + Object previous = lookUp.get(previousUniqueID); + if (previous == entry) { + lookUp.remove(previousUniqueID); + } // else: previousUniqueID points to another entry -> ignore + } + String uniqueID = entry.getUniqueID(); + if (uniqueID != null) { + putToLookup(uniqueID, entry); + } + } + } + + //------------------------------------------------------------< private >--- + private void putToLookup(String uniqueID, NodeEntry entry) { + Object previous = lookUp.put(uniqueID, entry); + if (previous != null) { + // some other entry existed before with the same uniqueID + if (!sameEntry((NodeEntry) previous, entry)) { + // if the new entry represents the externally moved/renamed + // correspondence of the previous the latter needs to marked + // removed/stale-destroyed. + // otherwise (both represent the same entry) the creation + // of entry is the result of gc of the node or any of the + // ancestors. in this case there is not need to 'remove' + // the previous entry. instead it is just removed from this + // cache and left for collection. + ((NodeEntry) previous).remove(); + } else { + log.debug("Replacement of NodeEntry identified by UniqueID"); + } + } + } + + private static boolean sameEntry(NodeEntry previous, NodeEntry entry) { + if (previous == entry) { + return true; + } else if (Status.REMOVED != previous.getStatus() && + previous.getName().equals(entry.getName())) { + + NodeEntry parent = previous.getParent(); + NodeEntry parent2 = entry.getParent(); + if (parent == parent2) { + return true; + } else { + try { + return parent.getPath().equals(parent2.getPath()); + } catch (RepositoryException e) { + // TODO: add some fallback + } + } + } + return false; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/lock/LockManagerImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/lock/LockManagerImpl.java new file mode 100644 index 00000000000..9e53b93d4bb --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/lock/LockManagerImpl.java @@ -0,0 +1,864 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.lock; + +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.SessionListener; +import org.apache.jackrabbit.jcr2spi.WorkspaceManager; +import org.apache.jackrabbit.jcr2spi.config.CacheBehaviour; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.operation.LockOperation; +import org.apache.jackrabbit.jcr2spi.operation.LockRefresh; +import org.apache.jackrabbit.jcr2spi.operation.LockRelease; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateLifeCycleListener; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Date; + +/** + * LockManagerImpl... + * TODO: TOBEFIXED. Lock objects obtained through this mgr are not informed if another session is or becomes lock-holder and removes the lock again. + */ +public class LockManagerImpl implements LockStateManager, SessionListener { + + private static Logger log = LoggerFactory.getLogger(LockManagerImpl.class); + + private static final long TIMEOUT_EXPIRED = -1; + private static final long TIMEOUT_INFINITE = Long.MAX_VALUE; + + /** + * WorkspaceManager used to apply and release locks as well as to retrieve + * Lock information for a given NodeState. + * NOTE: The workspace manager must not be used as ItemStateManager. + */ + private final WorkspaceManager wspManager; + private final ItemManager itemManager; + private final CacheBehaviour cacheBehaviour; + + /** + * Map holding all locks that where created by this Session upon + * calls to {@link LockStateManager#lock(NodeState,boolean,boolean)} or to + * {@link LockStateManager#getLock(NodeState)}. The map entries are removed + * only if a lock ends his life by {@link Node#unlock()} or by implicit + * unlock upon {@link Session#logout()}. + */ + private final Map lockMap; + + public LockManagerImpl(WorkspaceManager wspManager, ItemManager itemManager, + CacheBehaviour cacheBehaviour) { + this.wspManager = wspManager; + this.itemManager = itemManager; + this.cacheBehaviour = cacheBehaviour; + // use hard references in order to make sure, that entries referring + // to locks created by the current session are not removed. + lockMap = new HashMap(); + } + + //----------------< org.apache.jackrabbit.jcr2spi.lock.LockStateManager >--- + /** + * @see LockStateManager#lock(NodeState,boolean,boolean) + */ + public Lock lock(NodeState nodeState, boolean isDeep, boolean isSessionScoped) throws LockException, RepositoryException { + return lock(nodeState, isDeep, isSessionScoped, Long.MAX_VALUE, null); + } + + /** + * @see LockStateManager#lock(NodeState,boolean,boolean,long,String) + */ + public Lock lock(NodeState nodeState, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerHint) throws RepositoryException { + // retrieve node first + Node lhNode; + Item item = itemManager.getItem(nodeState.getHierarchyEntry()); + if (item.isNode()) { + lhNode = (Node) item; + } else { + throw new RepositoryException("Internal error: ItemManager returned Property from NodeState"); + } + + // execute the operation + LockOperation op = LockOperation.create(nodeState, isDeep, isSessionScoped, timeoutHint, ownerHint); + wspManager.execute(op); + + Lock lock = new LockImpl(new LockState(nodeState, op.getLockInfo()), lhNode); + return lock; + } + + /** + * @see LockStateManager#unlock(NodeState) + */ + public void unlock(NodeState nodeState) throws LockException, RepositoryException { + // execute the operation. Note, that its possible that the session is + // lock holder and still the lock was never accessed. thus the lockMap + // does not provide sufficient and reliable information. + Operation op = LockRelease.create(nodeState); + wspManager.execute(op); + + // if unlock was successful: clean up lock map and lock life cycle + // in case the corresponding Lock object exists (and thus has been + // added to the map. + if (lockMap.containsKey(nodeState)) { + LockImpl l = lockMap.remove(nodeState); + l.lockState.unlocked(); + } + } + + /** + * If the session created a lock on the node with the given state, we already + * know the lock. Otherwise, the node state and its ancestors are searched + * for properties indicating a lock.
        + * Note, that the flag indicating session-scoped lock cannot be retrieved + * unless the current session is the lock holder. + * + * @see LockStateManager#getLock(NodeState) + * @param nodeState + */ + public Lock getLock(NodeState nodeState) throws LockException, RepositoryException { + LockImpl l = getLockImpl(nodeState, false); + // no-lock found or lock doesn't apply to this state -> throw + if (l == null) { + throw new LockException("Node with id '" + nodeState.getNodeId() + "' is not locked."); + } + + // a lock exists either on the given node state or as deep lock inherited + // from any of the ancestor states. + return l; + } + + /** + * @see LockStateManager#isLocked(NodeState) + */ + public boolean isLocked(NodeState nodeState) throws RepositoryException { + LockImpl l = getLockImpl(nodeState, false); + return l != null; + } + + /** + * @see LockStateManager#checkLock(NodeState) + */ + public void checkLock(NodeState nodeState) throws LockException, RepositoryException { + // shortcut: new status indicates that a new state was already added + // thus, the parent state is not locked by foreign lock. + if (nodeState.getStatus() == Status.NEW) { + return; + } + + LockImpl l = getLockImpl(nodeState, true); + if (l != null && !l.isLockOwningSession()) { + // lock is present and token is null -> session is not lock-holder. + throw new LockException("Node with id '" + nodeState + "' is locked."); + } // else: state is not locked at all || session is lock-holder + } + + + /** + * Returns the lock tokens present on the SessionInfo this + * manager has been created with. + * + * @see LockStateManager#getLockTokens() + */ + public String[] getLockTokens() throws UnsupportedRepositoryOperationException, RepositoryException { + return wspManager.getLockTokens(); + } + + /** + * Delegates this call to {@link WorkspaceManager#addLockToken(String)}. + * If this succeeds this method will inform all locks stored in the local + * map in order to give them the chance to update their lock information. + * + * @see LockStateManager#addLockToken(String) + */ + public void addLockToken(String lt) throws LockException, RepositoryException { + wspManager.addLockToken(lt); + notifyTokenAdded(lt); + } + + /** + * If the lock addressed by the token is session-scoped, this method will + * throw a LockException, such as defined by JSR170 v.1.0.1 for + * {@link Session#removeLockToken(String)}.
        Otherwise the call is + * delegated to {@link WorkspaceManager#removeLockToken(String)}. + * All locks stored in the local lock map are notified by the removed + * token in order have them updated their lock information. + * + * @see LockStateManager#removeLockToken(String) + */ + public void removeLockToken(String lt) throws LockException, RepositoryException { + // JSR170 v. 1.0.1 defines that the token of a session-scoped lock may + // not be moved over to another session. Thus removal is not possible + // and the lock is always present in the lock map. + Iterator it = lockMap.values().iterator(); + boolean found = false; + // loop over cached locks to determine if the token belongs to a session + // scoped lock, in which case the removal must fail immediately. + while (it.hasNext() && !found) { + LockImpl l = it.next(); + if (lt.equals(l.getLockToken())) { + // break as soon as the lock associated with the given token was found. + found = true; + if (l.isSessionScoped()) { + throw new LockException("Cannot remove lock token associated with a session scoped lock."); + } + } + } + + // remove lock token from sessionInfo. call will fail, if the session + // is not lock holder. + wspManager.removeLockToken(lt); + // inform about this lt being removed from this session + notifyTokenRemoved(lt); + } + + //----------------------------------------------------< SessionListener >--- + /** + * @see SessionListener#loggingOut(Session) + */ + public void loggingOut(Session session) { + // remove any session scoped locks: + NodeState[] lhStates = lockMap.keySet().toArray(new NodeState[lockMap.size()]); + for (NodeState nState : lhStates) { + LockImpl l = lockMap.get(nState); + if (l.isSessionScoped() && l.isLockOwningSession()) { + try { + unlock(nState); + } catch (RepositoryException e) { + log.warn("Error while unlocking session scoped lock. Cleaning up local lock status."); + // at least clean up local lock map and the locks life cycle + l.lockState.unlocked(); + } + } + } + } + + /** + * @see SessionListener#loggedOut(Session) + */ + public void loggedOut(Session session) { + // release all remaining locks without modifying their lock status + LockImpl[] locks = lockMap.values().toArray(new LockImpl[lockMap.size()]); + for (LockImpl lock : locks) { + lock.lockState.release(); + } + } + + //------------------------------------------------------------< private >--- + + /** + * Search nearest ancestor that is locked. Returns null if neither + * the given state nor any of its ancestors is locked. + * Note, that this methods does NOT check if the given node state would + * be affected by the lock present on an ancestor state. + * Note, that in certain cases it might not be possible to detect a lock + * being present due to the fact that the hierarchy might be incomplete or + * not even readable completely. For this reason it seem equally reasonable + * to search for jcr:lockIsDeep property only and omitting all kind of + * verification regarding nodetypes present. + * + * @param nodeState NodeState from which searching starts. + * Note, that the given state must not have an overlaid state. + * @return a state holding a lock or null if neither the + * given state nor any of its ancestors is locked. + */ + private NodeState getLockHoldingState(NodeState nodeState) { + NodeEntry entry = nodeState.getNodeEntry(); + while (!entry.hasPropertyEntry(NameConstants.JCR_LOCKISDEEP)) { + NodeEntry parent = entry.getParent(); + if (parent == null) { + // reached root state without finding a locked node + return null; + } + entry = parent; + } + try { + return entry.getNodeState(); + } catch (RepositoryException e) { + // may occur if the nodeState is not accessible or some generic + // error occurred. + // for this case, assume that no lock exists and delegate final + // validation to the spi-implementation. + log.warn("Error while accessing lock holding NodeState: {}", e.getMessage()); + return null; + } + } + + private LockState buildLockState(NodeState nodeState) throws RepositoryException { + NodeId nId = nodeState.getNodeId(); + NodeState lockHoldingState; + LockInfo lockInfo = wspManager.getLockInfo(nId); + if (lockInfo == null) { + // no lock present + return null; + } + + NodeId lockNodeId = lockInfo.getNodeId(); + if (lockNodeId.equals(nId)) { + lockHoldingState = nodeState; + } else { + NodeEntry lockedEntry = wspManager.getHierarchyManager().getNodeEntry(lockNodeId); + try { + lockHoldingState = lockedEntry.getNodeState(); + } catch (RepositoryException e) { + log.warn("Cannot build LockState"); + throw new RepositoryException("Cannot build LockState", e); + } + } + + if (lockHoldingState == null) { + return null; + } else { + return new LockState(lockHoldingState, lockInfo); + } + } + + /** + * Returns the Lock that applies to the given node state (directly or + * by an inherited deep lock) or null if the state is not + * locked at all. + * + * @param nodeState + * @param lazyLockDiscovery If true, no extra check with the server is made in order to + * determine, whether there is really no lock present. Otherwise, the server + * is asked if a lock is present. + * @return LockImpl that applies to the given state or null. + * @throws RepositoryException + */ + private LockImpl getLockImpl(NodeState nodeState, boolean lazyLockDiscovery) throws RepositoryException { + NodeState nState = nodeState; + // access first non-NEW state + while (nState.getStatus() == Status.NEW) { + nState = nState.getParent(); + } + + // shortcut: check if a given state holds a lock, which has been + // store in the lock map. see below (LockImpl) for the conditions that + // must be met in order a lock can be stored. + LockImpl l = getLockFromMap(nState); + if (l != null && l.lockState.appliesToNodeState(nodeState)) { + return l; + } + + LockState lState; + if (lazyLockDiscovery) { + // try to retrieve a state (ev. a parent state) that holds a lock. + NodeState lockHoldingState = getLockHoldingState(nState); + if (lockHoldingState == null) { + // assume no lock is present (might not be correct due to incomplete hierarchy) + return null; + } else { + // check lockMap again with the lock-holding state + l = getLockFromMap(nState); + if (l != null) { + return l; + } + lState = buildLockState(lockHoldingState); + } + } else { + // need precise information about lock status -> retrieve lockInfo + // from the persistent layer. + lState = buildLockState(nState); + } + + if (lState != null) { + // Test again if a Lock object is stored in the lockmap. Otherwise + // build the lock object and retrieve lock holding node. note that this + // may fail if the session does not have permission to see this node. + LockImpl lock = getLockFromMap(lState.lockHoldingState); + if (lock != null) { + lock.lockState.setLockInfo(lState.lockInfo); + } else { + Item lockHoldingNode = itemManager.getItem(lState.lockHoldingState.getHierarchyEntry()); + lock = new LockImpl(lState, (Node)lockHoldingNode); + } + // test if lock applies to the original nodestate + if (lState.appliesToNodeState(nodeState)) { + return lock; + } else { + return null; // lock exists but doesn't apply to the given state + } + } else { + // no lock at all + return null; + } + } + + private LockImpl getLockFromMap(NodeState nodeState) { + try { + LockImpl l = lockMap.get(nodeState); + if (l != null && l.isLive()) { + return l; + } + } catch (RepositoryException e) { + // ignore + } + return null; + } + + //----------------------------< Notification about modified lock-tokens >--- + /** + * Notify all Locks that have been accessed so far about the + * new lock token present on the session and allow them to reload their + * lock info. + * + * @param lt + * @throws RepositoryException + */ + private void notifyTokenAdded(String lt) throws RepositoryException { + LockTokenListener[] listeners = lockMap.values().toArray(new LockTokenListener[lockMap.size()]); + for (LockTokenListener listener : listeners) { + listener.lockTokenAdded(lt); + } + } + + /** + * Notify all Locks that have been accessed so far about the + * removed lock token and allow them to reload their lock info, if necessary. + * + * @param lt + * @throws RepositoryException + */ + private void notifyTokenRemoved(String lt) throws RepositoryException { + LockTokenListener[] listeners = lockMap.values().toArray(new LockTokenListener[lockMap.size()]); + for (LockTokenListener listener : listeners) { + listener.lockTokenRemoved(lt); + } + } + + //-------------------------------------------------------------------------- + private class LockState implements ItemStateLifeCycleListener { + + private final NodeState lockHoldingState; + + private LockInfo lockInfo; + private boolean isLive = true; + private long expiration = TIMEOUT_INFINITE; + + private LockState(NodeState lockHoldingState, LockInfo lockInfo) { + this.lockHoldingState = lockHoldingState; + setLockInfo(lockInfo); + } + + private void refresh() throws RepositoryException { + // lock is still alive -> send refresh-lock operation. + Operation op = LockRefresh.create(lockHoldingState); + wspManager.execute(op); + } + + /** + * Returns true, if the given node state is the lockholding state of + * this Lock object OR if this Lock is deep. + * Note, that in the latter case this method does not assert, that the + * given node state is a child state of the lockholding state. + * + * @param nodeState that must be the same or a child of the lock holding + * state stored within this lock object. + * @return true if this lock applies to the given node state. + */ + private boolean appliesToNodeState(NodeState nodeState) { + if (nodeState.getStatus() == Status.NEW) { + return lockInfo.isDeep(); + } else { + if (lockHoldingState == nodeState) { + return true; + } else { + return lockInfo != null && lockInfo.isDeep(); + } + } + } + + /** + * Reload the lockInfo from the server. + * + * @throws RepositoryException + */ + private void reloadLockInfo() throws RepositoryException { + NodeId nId = lockHoldingState.getNodeEntry().getWorkspaceId(); + lockInfo = wspManager.getLockInfo(nId); + if (lockInfo == null) { + // lock has been released on the server + unlocked(); + } + } + + private void setLockInfo(LockInfo lockInfo) { + this.lockInfo = lockInfo; + long seconds = lockInfo.getSecondsRemaining(); + if (seconds <= TIMEOUT_EXPIRED) { + expiration = TIMEOUT_EXPIRED; + isLive = false; + } else if (seconds < TIMEOUT_INFINITE) { + // calculate timeout + expiration = new Date().getTime()/1000 + lockInfo.getSecondsRemaining(); + } else { + expiration = TIMEOUT_INFINITE; + } + } + + /** + * @return true if the lock is still alive. + */ + private boolean isLive() { + if (isLive) { + isLive = getSecondsRemaining() > 0; + } + return isLive; + } + + /** + * @return the number of seconds until the lock's timeout is reached, + * {@link Long#MAX_VALUE} if timeout is infinite or undefined and + * a negative value if timeout has already been reached or the lock + * has been otherwise released. + */ + private long getSecondsRemaining() { + if (!isLive) { + return TIMEOUT_EXPIRED; + } else if (expiration == TIMEOUT_INFINITE) { + return expiration; + } else { + long seconds = expiration - new Date().getTime()/1000; + if (seconds <= 0) { + isLive = false; + return TIMEOUT_EXPIRED; + } else { + return seconds; + } + } + } + + /** + * Release this lock by removing from the lock map and unregistering + * it from event listening + */ + private void release() { + if (lockMap.containsKey(lockHoldingState)) { + lockMap.remove(lockHoldingState); + } + stopListening(); + } + + /** + * This lock has been removed by the current Session or by an external + * unlock request. Since a lock will never come back to life after + * unlocking, it is released an its status is reset accordingly. + */ + private void unlocked() { + if (isLive()) { + release(); + isLive = false; + } + } + + private void startListening() { + // LockState must be aware of removal of the Node. + lockHoldingState.addListener(this); + + // in case of CacheBehaviour.OBSERVATION this lockstate can also + // be aware of another session removing the lock -> listen to + // status changes of the jcr:lockIsDeep property. + if (cacheBehaviour == CacheBehaviour.OBSERVATION) { + try { + if (!lockHoldingState.hasPropertyName(NameConstants.JCR_LOCKISDEEP)) { + // force reloading of the lock holding node. + itemManager.getItem(lockHoldingState.getNodeEntry()); + } + PropertyState ps = lockHoldingState.getPropertyState(NameConstants.JCR_LOCKISDEEP); + ps.addListener(this); + } catch (RepositoryException e) { + log.warn("Unable to retrieve jcr:isDeep property after lock creation. {}", e.getMessage()); + } + } + } + + private void stopListening() { + lockHoldingState.removeListener(this); + + if (cacheBehaviour == CacheBehaviour.OBSERVATION) { + try { + if (lockHoldingState.hasPropertyName(NameConstants.JCR_LOCKISDEEP)) { + PropertyState ps = lockHoldingState.getPropertyState(NameConstants.JCR_LOCKISDEEP); + ps.removeListener(this); + } + } catch (ItemNotFoundException e) { + log.debug("jcr:lockIsDeep doesn't exist any more."); + } catch (Exception e) { + log.warn(e.getMessage()); + } + } + } + + //-------------------------------------< ItemStateLifeCycleListener >--- + /** + * @see ItemStateLifeCycleListener#statusChanged(ItemState, int) + */ + public void statusChanged(ItemState state, int previousStatus) { + if (!isLive()) { + // since we only monitor the removal of the lock (by means + // of deletion of the jcr:lockIsDeep property, we are not interested + // if the lock is not active any more. + return; + } + + switch (state.getStatus()) { + case Status.REMOVED: + // this lock has been release by someone else (and not by + // a call to LockManager#unlock -> clean up and set isLive + // flag to false. + unlocked(); + break; + default: + // not interested + } + } + } + + //---------------------------------------------------------------< Lock >--- + /** + * Inner class implementing the {@link Lock} interface. + */ + private class LockImpl implements javax.jcr.lock.Lock, LockTokenListener { + + private final LockState lockState; + private final Node node; + private boolean reloadInfo = false; // TODO: find better solution + + /** + * + * @param lockState + * Note, that the given state must not have an overlaid state. + * @param lockHoldingNode the lock holding Node itself. + */ + public LockImpl(LockState lockState, Node lockHoldingNode) { + this.lockState = lockState; + this.node = lockHoldingNode; + + // if observation is supported OR if this is a session-scoped lock + // hold by this session -> store lock in the map + if (cacheBehaviour == CacheBehaviour.OBSERVATION) { + lockMap.put(lockState.lockHoldingState, this); + lockState.startListening(); + } else if (lockState.lockInfo.isLockOwner()) { + lockMap.put(lockState.lockHoldingState, this); + lockState.startListening(); + // open-scoped locks: the map entry and the lock information + // stored therein may become outdated if the token is transferred + // to another session -> info must be reloaded. + if (!isSessionScoped()) { + reloadInfo = true; + } + } else { + // foreign lock: info must be reloaded. + reloadInfo = true; + } + } + + /** + * @see Lock#getLockOwner() + */ + public String getLockOwner() { + LockInfo info = getLockInfo(); + if (info != null) { + return info.getOwner(); + } else { + return null; + } + } + + /** + * @see Lock#isDeep() + */ + public boolean isDeep() { + LockInfo info = getLockInfo(); + return info != null && info.isDeep(); + } + + /** + * @see Lock#getNode() + */ + public Node getNode() { + return node; + } + + /** + * @see Lock#getLockToken() + */ + public String getLockToken() { + // shortcut for jsr 283 session scoped locks: they never expose + // the lock token to the API users. + if (isSessionScoped()) { + return null; + } + + updateLockInfo(); + LockInfo info = getLockInfo(); + if (info != null) { + return info.getLockToken(); + } else { + return null; + } + } + + /** + * @see Lock#isLive() + */ + public boolean isLive() throws RepositoryException { + updateLockInfo(); + return lockState.isLive(); + } + + /** + * @see Lock#isSessionScoped() + */ + public boolean isSessionScoped() { + LockInfo info = getLockInfo(); + return info != null && info.isSessionScoped(); + } + + /** + * @see Lock#refresh() + */ + public void refresh() throws LockException, RepositoryException { + if (!isLive()) { + throw new LockException("Lock is not alive any more."); + } + + if (!isLockOwningSession()) { + // shortcut, since lock is always updated if the session became + // lock-holder of a foreign lock. + throw new LockException("Session does not hold lock."); + } else { + lockState.refresh(); + } + } + + /** + * @see javax.jcr.lock.Lock#getSecondsRemaining() + */ + public long getSecondsRemaining() throws RepositoryException { + updateLockInfo(); + return lockState.getSecondsRemaining(); + } + + /** + * @see javax.jcr.lock.Lock#isLockOwningSession() + */ + public boolean isLockOwningSession(){ + LockInfo info = getLockInfo(); + return info != null && info.isLockOwner(); + } + + //----------------------------------------------< LockTokenListener >--- + /** + * A lock token as been added to the current Session. If this Lock + * object is not yet hold by the Session (thus does not know whether + * the new lock token belongs to it), it must reload the LockInfo + * from the server. + * + * @param lockToken + * @throws RepositoryException + * @see LockTokenListener#lockTokenAdded(String) + */ + public void lockTokenAdded(String lockToken) throws RepositoryException { + if (!isSessionScoped() && !isLockOwningSession()) { + // unless this lock is session-scoped (token is never transferred) + // and the session isn't the owner yet (token already present), + // it could be that this affects this lock and session became + // lock holder -> reload info to assert. + lockState.reloadLockInfo(); + } + } + + /** + * + * @param lockToken + * @throws LockException + * @see LockTokenListener#lockTokenRemoved(String) + */ + public void lockTokenRemoved(String lockToken) throws RepositoryException { + // reload lock info, if session gave away its lock-holder status + // for this lock. this will never be true for session-scoped locks + // that are not exposed (thus cannot be removed). + if (lockToken.equals(getLockToken())) { + lockState.reloadLockInfo(); + } + } + + //--------------------------------------------------------< private >--- + /** + * @return LockInfo stored within the LockState + */ + private LockInfo getLockInfo() { + return lockState.lockInfo; + } + + /** + * Make sure the lock info is really up to date. + * TODO: find better solution. + */ + private void updateLockInfo() { + if (reloadInfo) { + try { + lockState.reloadLockInfo(); + } catch (RepositoryException e) { + // may occur if session has been logged out. rather throw? + log.warn("Unable to determine lock status. {}", e.getMessage()); + } + } // else: nothing to do. + } + } + + //--------------------------------------------------< LockTokenListener >--- + /** + * + */ + private interface LockTokenListener { + + /** + * + * @param lockToken + * @throws LockException + * @throws RepositoryException + */ + void lockTokenAdded(String lockToken) throws RepositoryException; + + /** + * + * @param lockToken + * @throws LockException + * @throws RepositoryException + */ + void lockTokenRemoved(String lockToken) throws RepositoryException; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/lock/LockStateManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/lock/LockStateManager.java new file mode 100644 index 00000000000..3873ff01181 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/lock/LockStateManager.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.jcr2spi.lock; + +import javax.jcr.RepositoryException; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; + +/** + * Defines the functionality needed for locking and unlocking nodes. + */ +public interface LockStateManager { + + /** + * Lock a node. Checks whether the node is not locked and then + * returns a lock object for this node. + * + * @param nodeState + * @param isDeep whether the lock applies to this node only + * @param isSessionScoped whether the lock is session scoped + * @return lock object + * @throws LockException if this node already is locked, or some descendant + * node is locked and isDeep is true + * @see javax.jcr.Node#lock + */ + Lock lock(NodeState nodeState, boolean isDeep, boolean isSessionScoped) + throws LockException, RepositoryException; + + /** + * Lock a node. Checks whether the node is not locked and then + * returns a lock object for this node. + * + * @param nodeState + * @param isDeep whether the lock applies to this node only + * @param isSessionScoped whether the lock is session scoped + * @param timeoutHint optional timeout hint. + * @param ownerHint optional String defining the lock owner info to be + * displayed. + * @return lock object + * @throws LockException if this node already is locked, or some descendant + * node is locked and isDeep is true + * @see javax.jcr.Node#lock + */ + Lock lock(NodeState nodeState, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerHint) + throws LockException, RepositoryException; + + /** + * Removes the lock on a node. + * + * @param nodeState + * @throws LockException if this node is not locked or the session does not + * have the correct lock token + * @see javax.jcr.Node#unlock + */ + void unlock(NodeState nodeState) throws LockException, RepositoryException; + + + /** + * Returns the Lock object that applies to a node. This may be either a lock + * on this node itself or a deep lock on a node above this node. + * + * @param nodeState + * @return lock object + * @throws LockException if this node is not locked + * @see javax.jcr.Node#getLock + */ + Lock getLock(NodeState nodeState) throws LockException, RepositoryException; + + /** + * Returns true if this node is locked either as a result + * of a lock held by this node or by a deep lock on a node above this + * node; otherwise returns false. + * + * @param nodeState + * @return true if this node is locked either as a result + * of a lock held by this node or by a deep lock on a node above this + * node; otherwise returns false + * @throws RepositoryException If an error occurs. + * @see javax.jcr.Node#isLocked + */ + boolean isLocked(NodeState nodeState) throws RepositoryException; + + /** + * Check whether the given node state is locked by somebody else than the + * current session. Access is allowed if the node is not locked or + * if the session itself holds the lock to this node, i.e. the session + * contains the lock token for the lock. If the node is not locked at + * all this method returns silently. + * + * @param nodeState + * @throws LockException if write access to the specified node is not allowed + * @throws RepositoryException if some other error occurs + */ + void checkLock(NodeState nodeState) throws LockException, RepositoryException; + + /** + * + * @return The lock tokens associated with the Session this + * lock manager has been created for. + */ + public String[] getLockTokens() throws RepositoryException; + + /** + * Invoked by a session to inform that a lock token has been added. + * + * @param lt added lock token + */ + void addLockToken(String lt) throws LockException, RepositoryException; + + /** + * Invoked by a session to inform that a lock token has been removed. + * + * @param lt removed lock token + */ + void removeLockToken(String lt) throws LockException, RepositoryException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/BitsetENTCacheImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/BitsetENTCacheImpl.java new file mode 100644 index 00000000000..a04cc1b4fab --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/BitsetENTCacheImpl.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.jcr2spi.nodetype; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.jackrabbit.spi.Name; + +/** + * Implements an effective node type cache that uses a bit set for storing the + * information about participating node types in a set. + */ +class BitsetENTCacheImpl implements EffectiveNodeTypeCache { + + /** + * constant for bits-per-word + */ + private static final int BPW = 64; + + /** + * OR mask for bit set + */ + private static final long[] OR_MASK = new long[BPW]; + static { + for (int i=0; i sortedKeys; + + /** + * cache of pre-built aggregations of node types + */ + private final HashMap aggregates; + + /** + * A lookup table for bit numbers for a given name. + * + * Note: further performance improvements could be made if this index would + * be stored in the node type registry since only registered node type names + * are allowed in the keys. + */ + private final ConcurrentHashMap nameIndex = new ConcurrentHashMap(); + + /** + * The reverse lookup table for bit numbers to names + */ + private Name[] names = new Name[1024]; + + /** + * Creates a new bitset effective node type cache + */ + BitsetENTCacheImpl() { + sortedKeys = new TreeSet(); + aggregates = new HashMap(); + } + + //---------------------------------------------< EffectiveNodeTypeCache >--- + /** + * @see EffectiveNodeTypeCache#getKey(Name[]) + */ + public Key getKey(Name[] ntNames) { + return new BitsetKey(ntNames, nameIndex.size() + ntNames.length); + } + + /** + * @see EffectiveNodeTypeCache#put(EffectiveNodeType) + */ + public void put(EffectiveNodeType ent) { + put(getKey(ent.getMergedNodeTypes()), ent); + } + + /** + * @see EffectiveNodeTypeCache#put(Key, EffectiveNodeType) + */ + public void put(Key key, EffectiveNodeType ent) { + aggregates.put(key, ent); + sortedKeys.add(key); + } + + /** + * @see EffectiveNodeTypeCache#findBest(Key) + */ + public Key findBest(Key key) { + // quick check for already cached key + if (contains(key)) { + return key; + } + for (Key k : sortedKeys) { + if (key.contains(k)) { + return k; + } + } + return null; + } + + /** + * @see EffectiveNodeTypeCache#invalidate(Name) + */ + public void invalidate(Name name) { + /** + * remove all affected effective node types from aggregates cache + * (copy keys first to prevent ConcurrentModificationException) + */ + ArrayList keys = new ArrayList(aggregates.keySet()); + for (Key k : keys) { + EffectiveNodeType ent = get(k); + if (ent.includesNodeType(name)) { + remove(k); + } + } + } + + /** + * @see EffectiveNodeTypeCache#contains(Key) + */ + public boolean contains(Key key) { + return aggregates.containsKey(key); + } + + /** + * @see EffectiveNodeTypeCache#get(Key) + */ + public EffectiveNodeType get(Key key) { + return aggregates.get(key); + } + + /** + * @see EffectiveNodeTypeCache#clear() + */ + public void clear() { + sortedKeys.clear(); + aggregates.clear(); + nameIndex.clear(); + } + + //------------------------------------------------------------< private >--- + /** + * Returns the bit number for the given name. If the name does not exist + * a new new bit number for that name is created. + * + * @param name the name to lookup + * @return the bit number for the given name + */ + private int getBitNumber(Name name) { + Integer i = nameIndex.get(name); + if (i == null) { + synchronized (nameIndex) { + i = nameIndex.get(name); + if (i == null) { + int idx = nameIndex.size(); + i = idx; + nameIndex.put(name, i); + if (idx >= names.length) { + Name[] newNames = new Name[names.length*2]; + System.arraycopy(names, 0, newNames, 0, names.length); + names = newNames; + } + names[idx] = name; + } + } + } + return i; + } + + /** + * Returns the node type name for a given bit number. + * @param n the bit number to lookup + * @return the node type name + */ + private Name getName(int n) { + return names[n]; + } + + /** + * Removes the effective node type for the given key from the cache. + * + * @param key the key of the effective node type to remove + * @return the removed effective node type or null if it was + * never cached. + */ + private EffectiveNodeType remove(Key key) { + EffectiveNodeType removed = aggregates.remove(key); + if (removed != null) { + // other than the original implementation, the weights in the + // treeset are now the same as in the given keys. so we can use + // the normal remove method + sortedKeys.remove(key); + } + return removed; + } + + //----------------------------------------------------------< Cloneable >--- + /** + * @see Cloneable#clone() + */ + @Override + public Object clone() { + BitsetENTCacheImpl clone = new BitsetENTCacheImpl(); + clone.sortedKeys.addAll(sortedKeys); + clone.aggregates.putAll(aggregates); + clone.names = new Name[names.length]; + System.arraycopy(names, 0, clone.names, 0, names.length); + clone.nameIndex.putAll(nameIndex); + return clone; + } + + //-------------------------------------------------------------< Object >--- + + /** + * Returns the the state of this instance in a human readable format. + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("EffectiveNodeTypeCache (" + super.toString() + ")\n"); + builder.append("EffectiveNodeTypes in cache:\n"); + for (Key k : sortedKeys) { + builder.append(k); + builder.append('\n'); + } + return builder.toString(); + } + + //----------------------------------------------------------------< Key >--- + /** + * Implements a {@link Key} by storing the node type aggregate information + * in a bit set. We do not use the {@link java.util.BitSet} because it + * does not suite all our needs. Every node type is represented by a bit + * in the set. This key is immutable. + */ + private class BitsetKey implements Key { + + /** + * The names of the node types that form this key. + */ + private final Name[] names; + + /** + * The array of longs that hold the bit information. + */ + private final long[] bits; + + /** + * the hashcode, only calculated once + */ + private final int hashCode; + + /** + * Creates a ew bitset key. + * @param names the node type names + * @param maxBit the approximate number of the greatest bit + */ + public BitsetKey(Name[] names, int maxBit) { + this.names = names; + bits = new long[maxBit/BPW+1]; + + for (int i=0; i= 0) { + names[j++] = BitsetENTCacheImpl.this.getName(i); + i = nextSetBit(i+1); + } + hashCode = calcHashCode(); + } + + /** + * Returns the bit number of the next bit that is set, starting at + * fromIndex inclusive. + * + * @param fromIndex the bit position to start the search + * @return the bit position of the bit or -1 if none found. + */ + private int nextSetBit(int fromIndex) { + int addr = fromIndex/BPW; + int off = fromIndex%BPW; + while (addr < bits.length) { + if (bits[addr] != 0) { + while (off < BPW) { + if ((bits[addr] & OR_MASK[off]) != 0) { + return addr * BPW + off; + } + off++; + } + off=0; + } + addr++; + } + return -1; + } + + /** + * Returns the number of bits set in val. + * For a derivation of this algorithm, see + * "Algorithms and data structures with applications to + * graphics and geometry", by Jurg Nievergelt and Klaus Hinrichs, + * Prentice Hall, 1993. + * + * @param val the value to calculate the bit count for + * @return the number of '1' bits in the value + */ + private int bitCount(long val) { + val -= (val & 0xaaaaaaaaaaaaaaaaL) >>> 1; + val = (val & 0x3333333333333333L) + ((val >>> 2) & 0x3333333333333333L); + val = (val + (val >>> 4)) & 0x0f0f0f0f0f0f0f0fL; + val += val >>> 8; + val += val >>> 16; + return ((int)(val) + (int)(val >>> 32)) & 0xff; + } + + /** + * Calculates the hashcode. + * @return the calculated hashcode + */ + private int calcHashCode() { + long h = 1234; + int addr = bits.length -1; + while (addr >=0 && bits[addr] == 0) { + addr--; + } + while (addr >=0) { + h ^= bits[addr] * (addr + 1); + addr--; + } + return (int)((h >> 32) ^ h); + } + + //------------------------------------------------------------< Key >--- + /** + * @see Key#getNames() + */ + public Name[] getNames() { + return names; + } + + /** + * @see Key#contains(Key) + */ + public boolean contains(Key otherKey) { + /* + * 0 - 0 => 0 + * 0 - 1 => 1 + * 1 - 0 => 0 + * 1 - 1 => 0 + * !a and b + */ + BitsetKey other = (BitsetKey) otherKey; + int len = Math.max(bits.length, other.bits.length); + for (int i=0; i 0 + * 0 - 1 => 0 + * 1 - 0 => 1 + * 1 - 1 => 0 + * a and !b + */ + BitsetKey other = (BitsetKey) otherKey; + int len = Math.max(bits.length, other.bits.length); + long[] newBits = new long[len]; + int numBits = 0; + for (int i=0; i--- + /** + * {@inheritDoc} + * + * This compares 1. the cardinality (number of set bits) and 2. the + * numeric value of the bitsets in descending order. + * + * @see Comparable#compareTo(Object) + */ + public int compareTo(Key other) { + BitsetKey o = (BitsetKey) other; + int res = o.names.length - names.length; + if (res == 0) { + int adr = Math.max(bits.length, o.bits.length) - 1; + while (adr >= 0) { + long w1 = adr>> 32; + long h2 = w2 >>> 32; + if (h1 == h2) { + h1 = w1 & 0x0ffffffffL; + h2 = w2 & 0x0ffffffffL; + } + return Long.signum(h2 - h1); + } + adr--; + } + } + return res; + } + + //---------------------------------------------------------< Object >--- + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BitsetKey) { + BitsetKey o = (BitsetKey) obj; + if (names.length != o.names.length) { + return false; + } + int adr = Math.max(bits.length, o.bits.length) - 1; + while (adr >= 0) { + long w1 = adrDefinitionValidator... + */ +class DefinitionValidator { + + private static Logger log = LoggerFactory.getLogger(DefinitionValidator.class); + + private final EffectiveNodeTypeProvider entProvider; + private final NamespaceRegistry nsRegistry; + + + DefinitionValidator(EffectiveNodeTypeProvider entProvider, NamespaceRegistry nsRegistry) { + this.entProvider = entProvider; + this.nsRegistry = nsRegistry; + } + + /** + * Validate each QNodeTypeDefinition present in the given collection. + * + * @param ntDefs + * @param validatedDefs + * @return Map mapping the definition to the resulting effective nodetype + * @throws InvalidNodeTypeDefinitionException + * @throws RepositoryException + */ + public Map validateNodeTypeDefs(Collection ntDefs, + Map validatedDefs) + throws InvalidNodeTypeDefinitionException, RepositoryException { + // tmp. map containing names/defs of validated nodetypes + Map tmpMap = new HashMap(validatedDefs); + for (QNodeTypeDefinition ntd : ntDefs) { + tmpMap.put(ntd.getName(), ntd); + } + + // map of nodetype definitions and effective nodetypes to be registered + Map ntMap = new HashMap(); + List list = new ArrayList(ntDefs); + + // iterate over definitions until there are no more definitions with + // unresolved (i.e. unregistered) dependencies or an error occurs; + + int count = -1; // number of validated nt's per iteration + while (list.size() > 0 && count != 0) { + count = 0; + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + QNodeTypeDefinition ntd = iterator.next(); + // check if definition has unresolved dependencies + /* Note: don't compared to 'registered' nodetypes since registr. is performed later on */ + Collection dependencies = ntd.getDependencies(); + if (tmpMap.keySet().containsAll(dependencies)) { + EffectiveNodeType ent = validateNodeTypeDef(ntd, tmpMap); + ntMap.put(ntd, ent); + // remove it from list + iterator.remove(); + // increase count + count++; + } + } + } + if (list.size() > 0) { + StringBuffer msg = new StringBuffer(); + msg.append("the following node types could not be registered because of unresolvable dependencies: "); + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + msg.append(iterator.next().getName()); + msg.append(" "); + } + log.error(msg.toString()); + throw new InvalidNodeTypeDefinitionException(msg.toString()); + } + return ntMap; + } + + /** + * + * @param ntDef + * @param validatedDefs Map of nodetype names and nodetype definitions + * that are known to be valid or are already registered. This map is used to + * validated dependencies and check for circular inheritance + * @return + * @throws InvalidNodeTypeDefinitionException + * @throws RepositoryException + */ + public EffectiveNodeType validateNodeTypeDef(QNodeTypeDefinition ntDef, Map validatedDefs) + throws InvalidNodeTypeDefinitionException, RepositoryException { + /** + * the effective (i.e. merged and resolved) node type resulting from + * the specified node type definition; + * the effective node type will finally be created after the definition + * has been verified and checked for conflicts etc.; in some cases it + * will be created already at an earlier stage during the validation + * of child node definitions + */ + EffectiveNodeType ent = null; + + Name name = ntDef.getName(); + if (name == null) { + String msg = "no name specified"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + checkNamespace(name); + + // validate supertypes + Name[] supertypes = ntDef.getSupertypes(); + if (supertypes.length > 0) { + for (int i = 0; i < supertypes.length; i++) { + checkNamespace(supertypes[i]); + /** + * simple check for infinite recursion + * (won't trap recursion on a deeper inheritance level) + */ + if (name.equals(supertypes[i])) { + String msg = "[" + name + "] invalid supertype: " + + supertypes[i] + " (infinite recursion))"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + /* compare to given nt-name set and not to registered nodetypes */ + if (!validatedDefs.containsKey(supertypes[i])) { + String msg = "[" + name + "] invalid supertype: " + supertypes[i]; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + } + + /** + * check for circularity in inheritance chain + * ('a' extends 'b' extends 'a') + */ + Stack inheritanceChain = new Stack(); + inheritanceChain.push(name); + checkForCircularInheritance(supertypes, inheritanceChain, validatedDefs); + } + + /** + * note that infinite recursion through inheritance is automatically + * being checked by the following call to getEffectiveNodeType() + * as it's impossible to register a node type definition which + * references a supertype that isn't registered yet... + */ + + /** + * build effective (i.e. merged and resolved) node type from supertypes + * and check for conflicts + */ + if (supertypes.length > 0) { + try { + EffectiveNodeType est = entProvider.getEffectiveNodeType(supertypes, validatedDefs); + // make sure that all primary types except nt:base extend from nt:base + if (!ntDef.isMixin() && !NameConstants.NT_BASE.equals(ntDef.getName()) + && !est.includesNodeType(NameConstants.NT_BASE)) { + String msg = "[" + name + "] all primary node types except" + + " nt:base itself must be (directly or indirectly) derived from nt:base"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + } catch (ConstraintViolationException e) { + String msg = "[" + name + "] failed to validate supertypes"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } catch (NoSuchNodeTypeException e) { + String msg = "[" + name + "] failed to validate supertypes"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } + } else { + // no supertypes specified: has to be either a mixin type or nt:base + if (!ntDef.isMixin() && !NameConstants.NT_BASE.equals(ntDef.getName())) { + String msg = "[" + name + + "] all primary node types except nt:base itself must be (directly or indirectly) derived from nt:base"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + } + + checkNamespace(ntDef.getPrimaryItemName()); + + // validate property definitions + QPropertyDefinition[] pda = ntDef.getPropertyDefs(); + for (int i = 0; i < pda.length; i++) { + QPropertyDefinition pd = pda[i]; + /** + * sanity check: + * make sure declaring node type matches name of node type definition + */ + if (!name.equals(pd.getDeclaringNodeType())) { + String msg = "[" + name + "#" + pd.getName() + "] invalid declaring node type specified"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + checkNamespace(pd.getName()); + // check that auto-created properties specify a name + if (pd.definesResidual() && pd.isAutoCreated()) { + String msg = "[" + name + "#" + pd.getName() + "] auto-created properties must specify a name"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + // check that auto-created properties specify a type + if (pd.getRequiredType() == PropertyType.UNDEFINED && pd.isAutoCreated()) { + String msg = "[" + name + "#" + pd.getName() + "] auto-created properties must specify a type"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + /* check default values: + * make sure type of value is consistent with required property type + * Note: default internal values are built from the required type, + * thus check for match with pd.getRequiredType is redundant. + */ + QValue[] defVals = pd.getDefaultValues(); + + /* check that default values satisfy value constraints. + * Note however, that no check is performed if autocreated property- + * definitions define a default value. JSR170 does not require this. + */ + ValueConstraint.checkValueConstraints(pd, defVals); + + /* ReferenceConstraint: + * the specified node type must be registered, with one notable + * exception: the node type just being registered + */ + QValueConstraint[] constraints = pd.getValueConstraints(); + if (constraints != null && constraints.length > 0) { + + if (pd.getRequiredType() == PropertyType.REFERENCE) { + for (QValueConstraint constraint : constraints) { + // TODO improve. don't rely on a specific factory impl + Name ntName = NameFactoryImpl.getInstance().create(constraint.getString()); + /* compare to given ntd map and not registered nts only */ + if (!name.equals(ntName) && !validatedDefs.containsKey(ntName)) { + String msg = "[" + name + "#" + pd.getName() + + "] invalid REFERENCE value constraint '" + + ntName + "' (unknown node type)"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + } + } + } + } + + // validate child-node definitions + QNodeDefinition[] cnda = ntDef.getChildNodeDefs(); + for (int i = 0; i < cnda.length; i++) { + QNodeDefinition cnd = cnda[i]; + /* make sure declaring node type matches name of node type definition */ + if (!name.equals(cnd.getDeclaringNodeType())) { + String msg = "[" + name + "#" + cnd.getName() + + "] invalid declaring node type specified"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + checkNamespace(cnd.getName()); + // check that auto-created child-nodes specify a name + if (cnd.definesResidual() && cnd.isAutoCreated()) { + String msg = "[" + name + "#" + cnd.getName() + + "] auto-created child-nodes must specify a name"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + // check that auto-created child-nodes specify a default primary type + if (cnd.getDefaultPrimaryType() == null + && cnd.isAutoCreated()) { + String msg = "[" + name + "#" + cnd.getName() + + "] auto-created child-nodes must specify a default primary type"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + // check default primary type + Name dpt = cnd.getDefaultPrimaryType(); + checkNamespace(dpt); + boolean referenceToSelf = false; + EffectiveNodeType defaultENT = null; + if (dpt != null) { + // check if this node type specifies itself as default primary type + if (name.equals(dpt)) { + referenceToSelf = true; + } + /** + * the default primary type must be registered, with one notable + * exception: the node type just being registered + */ + if (!name.equals(dpt) && !validatedDefs.containsKey(dpt)) { + String msg = "[" + name + "#" + cnd.getName() + + "] invalid default primary type '" + dpt + "'"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + /** + * build effective (i.e. merged and resolved) node type from + * default primary type and check for conflicts + */ + try { + if (!referenceToSelf) { + defaultENT = entProvider.getEffectiveNodeType(new Name[] {dpt}, validatedDefs); + } else { + /** + * the default primary type is identical with the node + * type just being registered; we have to instantiate it + * 'manually' + */ + ent = entProvider.getEffectiveNodeType(ntDef, validatedDefs); + defaultENT = ent; + } + if (cnd.isAutoCreated()) { + /** + * check for circularity through default primary types + * of auto-created child nodes (node type 'a' defines + * auto-created child node with default primary type 'a') + */ + Stack definingNTs = new Stack(); + definingNTs.push(name); + checkForCircularNodeAutoCreation(defaultENT, definingNTs, validatedDefs); + } + } catch (ConstraintViolationException e) { + String msg = "[" + name + "#" + cnd.getName() + + "] failed to validate default primary type"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } catch (NoSuchNodeTypeException e) { + String msg = "[" + name + "#" + cnd.getName() + + "] failed to validate default primary type"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } + } + + // check required primary types + Name[] reqTypes = cnd.getRequiredPrimaryTypes(); + if (reqTypes != null && reqTypes.length > 0) { + for (int n = 0; n < reqTypes.length; n++) { + Name rpt = reqTypes[n]; + checkNamespace(rpt); + referenceToSelf = false; + /** + * check if this node type specifies itself as required + * primary type + */ + if (name.equals(rpt)) { + referenceToSelf = true; + } + /** + * the required primary type must be registered, with one + * notable exception: the node type just being registered + */ + if (!name.equals(rpt) && !validatedDefs.containsKey(rpt)) { + String msg = "[" + name + "#" + cnd.getName() + + "] invalid required primary type: " + rpt; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + /** + * check if default primary type satisfies the required + * primary type constraint + */ + if (defaultENT != null && !defaultENT.includesNodeType(rpt)) { + String msg = "[" + name + "#" + cnd.getName() + + "] default primary type does not satisfy required primary type constraint " + + rpt; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg); + } + /** + * build effective (i.e. merged and resolved) node type from + * required primary type constraint and check for conflicts + */ + try { + if (!referenceToSelf) { + entProvider.getEffectiveNodeType(new Name[] {rpt}, validatedDefs); + } else { + /** + * the required primary type is identical with the + * node type just being registered; we have to + * instantiate it 'manually' + */ + if (ent == null) { + ent = entProvider.getEffectiveNodeType(ntDef, validatedDefs); + } + } + } catch (ConstraintViolationException e) { + String msg = "[" + name + "#" + cnd.getName() + + "] failed to validate required primary type constraint"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } catch (NoSuchNodeTypeException e) { + String msg = "[" + name + "#" + cnd.getName() + + "] failed to validate required primary type constraint"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } + } + } + } + + /** + * now build effective (i.e. merged and resolved) node type from + * this node type definition; this will potentially detect more + * conflicts or problems + */ + if (ent == null) { + try { + ent = entProvider.getEffectiveNodeType(ntDef, validatedDefs); + } catch (ConstraintViolationException e) { + String msg = "[" + name + "] failed to resolve node type definition"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } catch (NoSuchNodeTypeException e) { + String msg = "[" + name + "] failed to resolve node type definition"; + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } + } + return ent; + } + + /** + * + * @param supertypes + * @param inheritanceChain + * @param ntdMap + * @throws InvalidNodeTypeDefinitionException + * @throws RepositoryException + */ + private void checkForCircularInheritance(Name[] supertypes, Stack inheritanceChain, Map ntdMap) + throws InvalidNodeTypeDefinitionException, RepositoryException { + for (int i = 0; i < supertypes.length; i++) { + Name stName = supertypes[i]; + int pos = inheritanceChain.lastIndexOf(stName); + if (pos >= 0) { + StringBuffer buf = new StringBuffer(); + for (int j = 0; j < inheritanceChain.size(); j++) { + if (j == pos) { + buf.append("--> "); + } + buf.append(inheritanceChain.get(j)); + buf.append(" extends "); + } + buf.append("--> "); + buf.append(stName); + throw new InvalidNodeTypeDefinitionException("circular inheritance detected: " + buf.toString()); + } + + if (ntdMap.containsKey(stName)) { + Name[] sta = ntdMap.get(stName).getSupertypes(); + if (sta.length > 0) { + // check recursively + inheritanceChain.push(stName); + checkForCircularInheritance(sta, inheritanceChain, ntdMap); + inheritanceChain.pop(); + } + } else { + throw new InvalidNodeTypeDefinitionException("Unknown supertype: " + stName); + } + } + } + + /** + * + * @param childNodeENT + * @param definingParentNTs + * @param ntdMap + * @throws InvalidNodeTypeDefinitionException + */ + private void checkForCircularNodeAutoCreation(EffectiveNodeType childNodeENT, + Stack definingParentNTs, Map ntdMap) + throws InvalidNodeTypeDefinitionException { + // check for circularity through default node types of auto-created child nodes + // (node type 'a' defines auto-created child node with default node type 'a') + Name[] childNodeNTs = childNodeENT.getAllNodeTypes(); + for (int i = 0; i < childNodeNTs.length; i++) { + Name nt = childNodeNTs[i]; + int pos = definingParentNTs.lastIndexOf(nt); + if (pos >= 0) { + StringBuffer buf = new StringBuffer(); + for (int j = 0; j < definingParentNTs.size(); j++) { + if (j == pos) { + buf.append("--> "); + } + buf.append("node type "); + buf.append(definingParentNTs.get(j)); + buf.append(" defines auto-created child node with default "); + } + buf.append("--> "); + buf.append("node type "); + buf.append(nt); + throw new InvalidNodeTypeDefinitionException("circular node auto-creation detected: " + + buf.toString()); + } + } + + QNodeDefinition[] nodeDefs = childNodeENT.getAutoCreateQNodeDefinitions(); + for (int i = 0; i < nodeDefs.length; i++) { + Name dnt = nodeDefs[i].getDefaultPrimaryType(); + Name definingNT = nodeDefs[i].getDeclaringNodeType(); + try { + if (dnt != null) { + // check recursively + definingParentNTs.push(definingNT); + EffectiveNodeType ent = entProvider.getEffectiveNodeType(new Name[] {dnt}, ntdMap); + checkForCircularNodeAutoCreation(ent, definingParentNTs, ntdMap); + definingParentNTs.pop(); + } + } catch (NoSuchNodeTypeException e) { + String msg = definingNT + " defines invalid default node type for child node " + nodeDefs[i].getName(); + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } catch (ConstraintViolationException e) { + String msg = definingNT + " defines invalid default node type for child node " + nodeDefs[i].getName(); + log.debug(msg); + throw new InvalidNodeTypeDefinitionException(msg, e); + } + } + } + + /** + * Utility method for verifying that the namespace of a Name + * is registered; a null argument is silently ignored. + * @param name name whose namespace is to be checked + * @throws RepositoryException if the namespace of the given name is not + * registered or if an unspecified error occurred + */ + private void checkNamespace(Name name) throws RepositoryException { + if (name != null) { + // make sure namespace uri denotes a registered namespace + nsRegistry.getPrefix(name.getNamespaceURI()); + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeType.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeType.java new file mode 100644 index 00000000000..210c11ee103 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeType.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.jcr2spi.nodetype; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +/** + * EffectiveNodeType... + */ +public interface EffectiveNodeType { + + public Name[] getAllNodeTypes(); + + public Name[] getInheritedNodeTypes(); + + public Name[] getMergedNodeTypes(); + + /** + * Determines whether this effective node type representation includes + * (either through inheritance or aggregation) the given node type. + * + * @param nodeTypeName name of node type + * @return true if the given node type is included, otherwise + * false + */ + public boolean includesNodeType(Name nodeTypeName); + + /** + * Determines whether this effective node type supports adding + * the specified mixin. + * @param mixin name of mixin type + * @return true if the mixin type is supported, otherwise + * false + */ + public boolean supportsMixin(Name mixin); + + /** + * Determines whether this effective node type representation includes + * (either through inheritance or aggregation) all of the given node types. + * + * @param nodeTypeNames array of node type names + * @return true if all of the given node types are included, + * otherwise false + */ + public boolean includesNodeTypes(Name[] nodeTypeNames); + + public QNodeDefinition[] getAllQNodeDefinitions(); + + public QPropertyDefinition[] getAllQPropertyDefinitions(); + + public QNodeDefinition[] getAutoCreateQNodeDefinitions(); + + public QPropertyDefinition[] getAutoCreateQPropertyDefinitions(); + + public QNodeDefinition[] getMandatoryQNodeDefinitions(); + + public QPropertyDefinition[] getMandatoryQPropertyDefinitions(); + + public QNodeDefinition[] getNamedQNodeDefinitions(Name name); + + public QPropertyDefinition[] getNamedQPropertyDefinitions(Name name); + + public QNodeDefinition[] getUnnamedQNodeDefinitions(); + + public QPropertyDefinition[] getUnnamedQPropertyDefinitions(); + + /** + * @param name + * @param definitionProvider + * @throws ConstraintViolationException + */ + public void checkAddNodeConstraints(Name name, ItemDefinitionProvider definitionProvider) + throws ConstraintViolationException; + + /** + * @param name + * @param nodeTypeDefinition + *@param definitionProvider @throws ConstraintViolationException @throws NoSuchNodeTypeException + */ + public void checkAddNodeConstraints(Name name, QNodeTypeDefinition nodeTypeDefinition, ItemDefinitionProvider definitionProvider) + throws ConstraintViolationException, NoSuchNodeTypeException; + + /** + * @param name + * @throws ConstraintViolationException + * @deprecated Use {@link #hasRemoveNodeConstraint(Name)} and + * {@link #hasRemovePropertyConstraint(Name)} respectively. + */ + public void checkRemoveItemConstraints(Name name) throws ConstraintViolationException; + + /** + * Returns true if a single node definition matching the + * specified nodeName is either mandatory or protected. + * + * @param nodeName + * @return true if a single node definition matching the + * specified nodeName is either mandatory or protected. + */ + public boolean hasRemoveNodeConstraint(Name nodeName); + + /** + * Returns true if a single property definition matching the + * specified propertyName is either mandatory or protected. + * + * @param propertyName + * @return true if a single property definition matching the + * specified propertyName is either mandatory or protected. + */ + public boolean hasRemovePropertyConstraint(Name propertyName); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeCache.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeCache.java new file mode 100644 index 00000000000..aed05d00929 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeCache.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.jcr2spi.nodetype; + +import org.apache.jackrabbit.spi.Name; + +/** + * EffectiveNodeTypeCache defines the interface for a cache for + * effective node types. Effective node types are addressed by {@link Key}s. + */ +public interface EffectiveNodeTypeCache extends Cloneable { + + /** + * Puts an effective node type to the cache. The key is internally generated + * from the set of merged node types. + * @param ent the effective node type to put to the cache + */ + void put(EffectiveNodeType ent); + + /** + * Puts an effective node type to the cache for the given key. + * @param key the key for the effective node type + * @param ent the effective node type to put to the cache + */ + void put(Key key, EffectiveNodeType ent); + + /** + * Checks if the effective node type for the given key exists. + * @param key the key to check + * @return true if the effective node type is cached; + * false otherwise. + */ + boolean contains(Key key); + + /** + * Returns the effective node type for the given key or null if + * the desired node type is not cached. + * @param key the key for the effective node type. + * @return the effective node type or null + */ + EffectiveNodeType get(Key key); + + /** + * Returns a key for an effective node type that consists of the given + * node type names. + * @param ntNames the array of node type names for the effective node type + * @return the key to an effective node type. + */ + Key getKey(Name[] ntNames); + + /** + * Removes all effective node types that are aggregated with the node type + * of the given name. + * @param name the name of the node type. + */ + void invalidate(Name name); + + /** + * Searches the best key k for which the given key is a super + * set, i.e. for which {@link Key#contains(Key)}} returns + * true. If an already cached effective node type matches the + * key it is returned. + * + * @param key the key for which the subkey is to be searched + * @return the best key or null if no key could be found. + */ + Key findBest(Key key); + + /** + * Clears the cache. + */ + void clear(); + + /** + * An ENTKey uniquely identifies + * a combination (i.e. an aggregation) of one or more node types. + */ + interface Key extends Comparable { + + /** + * Returns the node type names of this key. + * @return the node type names of this key. + */ + Name[] getNames(); + + /** + * Checks if the otherKey is contained in this one. I.e. if + * this key contains all node type names of the other key. + * @param otherKey the other key to check + * @return true if this key contains the other key; + * false otherwise. + */ + boolean contains(Key otherKey); + + /** + * Creates a new key as a result of a subtract operation. i.e. removes all + * node type names that from the other key. + *

        + * Please note that no exception is thrown if the other key has node type + * names that are not contained in this key (i.e. {@link #contains(Key)} + * returns false). + * + * @param otherKey the other key to subtract + * @return the new key of the subtraction operation. + */ + Key subtract(Key otherKey); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeImpl.java new file mode 100644 index 00000000000..6d68ec8468e --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeImpl.java @@ -0,0 +1,679 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.nodetype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An EffectiveNodeType represents one or more + * NodeTypes as one 'effective' node type where inheritance + * is resolved. + *

        + * Instances of EffectiveNodeType are immutable. + */ +public class EffectiveNodeTypeImpl implements Cloneable, EffectiveNodeType { + private static Logger log = LoggerFactory.getLogger(EffectiveNodeTypeImpl.class); + + // list of explicitly aggregated {i.e. merged) node types + private final TreeSet mergedNodeTypes = new TreeSet(); + // list of implicitly aggregated {through inheritance) node types + private final TreeSet inheritedNodeTypes = new TreeSet(); + // list of all either explicitly (through aggregation) or implicitly + // (through inheritance) included node types. + private final TreeSet allNodeTypes = new TreeSet(); + // map of named item definitions (maps name to list of definitions) + private final Map> namedItemDefs = new HashMap>(); + // list of unnamed item definitions (i.e. residual definitions) + private final List unnamedItemDefs = new ArrayList(); + // (optional) set of additional mixins supported on node type + private Set supportedMixins; + + /** + * constructor. + */ + EffectiveNodeTypeImpl(TreeSet mergedNodeTypes, TreeSet inheritedNodeTypes, + TreeSet allNodeTypes, Map> namedItemDefs, + List unnamedItemDefs, Set supportedMixins) { + this.mergedNodeTypes.addAll(mergedNodeTypes); + this.inheritedNodeTypes.addAll(inheritedNodeTypes); + this.allNodeTypes.addAll(allNodeTypes); + for (Map.Entry> entry : namedItemDefs.entrySet()) { + this.namedItemDefs.put(entry.getKey(), new ArrayList(entry.getValue())); + } + this.unnamedItemDefs.addAll(unnamedItemDefs); + + if (supportedMixins != null) { + this.supportedMixins = new HashSet(); + this.supportedMixins.addAll(supportedMixins); + } + } + + //--------------------------------------------------< EffectiveNodeType >--- + /** + * @see EffectiveNodeType#getInheritedNodeTypes() + */ + public Name[] getInheritedNodeTypes() { + return inheritedNodeTypes.toArray(new Name[inheritedNodeTypes.size()]); + } + + /** + * @see EffectiveNodeType#getAllNodeTypes() + */ + public Name[] getAllNodeTypes() { + return allNodeTypes.toArray(new Name[allNodeTypes.size()]); + } + + /** + * @see EffectiveNodeType#getMergedNodeTypes() + */ + public Name[] getMergedNodeTypes() { + return mergedNodeTypes.toArray(new Name[mergedNodeTypes.size()]); + } + + /** + * @see EffectiveNodeType#getAllQNodeDefinitions() + */ + public QNodeDefinition[] getAllQNodeDefinitions() { + if (namedItemDefs.size() == 0 && unnamedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size() + unnamedItemDefs.size()); + for (QItemDefinition qDef : unnamedItemDefs) { + if (qDef.definesNode()) { + defs.add(qDef); + } + } + + for (List list : namedItemDefs.values()) { + for (QItemDefinition qDef : list) { + if (qDef.definesNode()) { + defs.add(qDef); + } + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getAllQPropertyDefinitions() + */ + public QPropertyDefinition[] getAllQPropertyDefinitions() { + if (namedItemDefs.size() == 0 && unnamedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size() + unnamedItemDefs.size()); + for (QItemDefinition qDef : unnamedItemDefs) { + if (!qDef.definesNode()) { + defs.add(qDef); + } + } + for (List list : namedItemDefs.values()) { + for (QItemDefinition qDef : list) { + if (!qDef.definesNode()) { + defs.add(qDef); + } + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getAutoCreateQNodeDefinitions() + */ + public QNodeDefinition[] getAutoCreateQNodeDefinitions() { + // since auto-create items must have a name, + // we're only searching the named item definitions + if (namedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition qDef : list) { + if (qDef.definesNode() && qDef.isAutoCreated()) { + defs.add(qDef); + } + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getAutoCreateQPropertyDefinitions() + */ + public QPropertyDefinition[] getAutoCreateQPropertyDefinitions() { + // since auto-create items must have a name, + // we're only searching the named item definitions + if (namedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition qDef : list) { + if (!qDef.definesNode() && qDef.isAutoCreated()) { + defs.add(qDef); + } + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getMandatoryQPropertyDefinitions() + */ + public QPropertyDefinition[] getMandatoryQPropertyDefinitions() { + // since mandatory items must have a name, + // we're only searching the named item definitions + if (namedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition qDef : list) { + if (!qDef.definesNode() && qDef.isMandatory()) { + defs.add(qDef); + } + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getMandatoryQNodeDefinitions() + */ + public QNodeDefinition[] getMandatoryQNodeDefinitions() { + // since mandatory items must have a name, + // we're only searching the named item definitions + if (namedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + for (QItemDefinition qDef : list) { + if (qDef.definesNode() && qDef.isMandatory()) { + defs.add(qDef); + } + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getNamedQNodeDefinitions(Name) + */ + public QNodeDefinition[] getNamedQNodeDefinitions(Name name) { + List list = namedItemDefs.get(name); + if (list == null || list.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(list.size()); + for (QItemDefinition qDef : list) { + if (qDef.definesNode()) { + defs.add(qDef); + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getUnnamedQNodeDefinitions() + */ + public QNodeDefinition[] getUnnamedQNodeDefinitions() { + if (unnamedItemDefs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(unnamedItemDefs.size()); + for (QItemDefinition qDef : unnamedItemDefs) { + if (qDef.definesNode()) { + defs.add(qDef); + } + } + if (defs.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QNodeDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getNamedQPropertyDefinitions(Name) + */ + public QPropertyDefinition[] getNamedQPropertyDefinitions(Name name) { + List list = namedItemDefs.get(name); + if (list == null || list.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(list.size()); + for (QItemDefinition qDef : list) { + if (!qDef.definesNode()) { + defs.add(qDef); + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + /** + * @see EffectiveNodeType#getUnnamedQPropertyDefinitions() + */ + public QPropertyDefinition[] getUnnamedQPropertyDefinitions() { + if (unnamedItemDefs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(unnamedItemDefs.size()); + for (QItemDefinition qDef : unnamedItemDefs) { + if (!qDef.definesNode()) { + defs.add(qDef); + } + } + if (defs.size() == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QPropertyDefinition[defs.size()]); + } + + public boolean includesNodeType(Name nodeTypeName) { + return allNodeTypes.contains(nodeTypeName); + } + + public boolean includesNodeTypes(Name[] nodeTypeNames) { + return allNodeTypes.containsAll(Arrays.asList(nodeTypeNames)); + } + + /** + * @see EffectiveNodeType#supportsMixin(Name) + */ + public boolean supportsMixin(Name mixin) { + if (supportedMixins == null) { + return true; + } + else { + return supportedMixins.contains(mixin); + } + } + + /** + * @see EffectiveNodeType#checkAddNodeConstraints(Name, ItemDefinitionProvider) + */ + public void checkAddNodeConstraints(Name name, ItemDefinitionProvider definitionProvider) + throws ConstraintViolationException { + try { + definitionProvider.getQNodeDefinition(this, name, null); + } catch (NoSuchNodeTypeException e) { + String msg = "internal error: inconsistent node type"; + log.debug(msg); + throw new ConstraintViolationException(msg, e); + } + } + + /** + * @see EffectiveNodeType#checkAddNodeConstraints(org.apache.jackrabbit.spi.Name,QNodeTypeDefinition, ItemDefinitionProvider) + */ + public void checkAddNodeConstraints(Name name, QNodeTypeDefinition nodeTypeDefinition, ItemDefinitionProvider definitionProvider) + throws ConstraintViolationException, NoSuchNodeTypeException { + if (nodeTypeDefinition.isAbstract()) { + throw new ConstraintViolationException("not allowed to add node " + name + ": " + nodeTypeDefinition.getName() + " is abstract and cannot be used as primary node type."); + } + if (nodeTypeDefinition.isMixin()) { + throw new ConstraintViolationException("not allowed to add node " + name + ":" + nodeTypeDefinition.getName() + " is a mixin and cannot be used as primary node type."); + } + QNodeDefinition nd = definitionProvider.getQNodeDefinition(this, name, nodeTypeDefinition.getName()); + if (nd.isProtected()) { + throw new ConstraintViolationException(name + " is protected."); + } + if (nd.isAutoCreated()) { + throw new ConstraintViolationException(name + " is auto-created and can not be manually added"); + } + } + + /** + * @see EffectiveNodeType#checkRemoveItemConstraints(Name) + */ + public void checkRemoveItemConstraints(Name name) throws ConstraintViolationException { + /** + * as there might be multiple definitions with the same name and we + * don't know which one is applicable, we check all of them + */ + QItemDefinition[] defs = getNamedItemDefs(name); + if (hasRemoveConstraint(defs)) { + throw new ConstraintViolationException("can't remove mandatory or protected item"); + } + } + + /** + * @see EffectiveNodeType#hasRemoveNodeConstraint(Name) + */ + public boolean hasRemoveNodeConstraint(Name nodeName) { + QNodeDefinition[] defs = getNamedQNodeDefinitions(nodeName); + return hasRemoveConstraint(defs); + } + + /** + * @see EffectiveNodeType#hasRemovePropertyConstraint(Name) + */ + public boolean hasRemovePropertyConstraint(Name propertyName) { + QPropertyDefinition[] defs = getNamedQPropertyDefinitions(propertyName); + return hasRemoveConstraint(defs); + } + + //---------------------------------------------< impl. specific methods >--- + /** + * Loop over the specified definitions and return true as soon + * as the first mandatory or protected definition is encountered. + * + * @param defs + * @return true if a mandatory or protected definition is present. + */ + private static boolean hasRemoveConstraint(QItemDefinition[] defs) { + /** + * as there might be multiple definitions with the same name that may be + * applicable, return true as soon as the first mandatory or protected + * definition is encountered. + */ + if (defs != null) { + for (int i = 0; i < defs.length; i++) { + if (defs[i].isMandatory()) { + return true; + } + if (defs[i].isProtected()) { + return true; + } + } + } + return false; + } + + private QItemDefinition[] getNamedItemDefs() { + if (namedItemDefs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + ArrayList defs = new ArrayList(namedItemDefs.size()); + for (List list : namedItemDefs.values()) { + defs.addAll(list); + } + if (defs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + return defs.toArray(new QItemDefinition[defs.size()]); + } + + private QItemDefinition[] getNamedItemDefs(Name name) { + List list = namedItemDefs.get(name); + if (list == null || list.size() == 0) { + return QNodeDefinition.EMPTY_ARRAY; + } + return list.toArray(new QItemDefinition[list.size()]); + } + + private QItemDefinition[] getUnnamedItemDefs() { + if (unnamedItemDefs.size() == 0) { + return QItemDefinition.EMPTY_ARRAY; + } + return unnamedItemDefs.toArray(new QItemDefinition[unnamedItemDefs.size()]); + } + + /** + * Merges another EffectiveNodeType with this one. + * Checks for merge conflicts. + * + * @param other + * @return + * @throws ConstraintViolationException + */ + EffectiveNodeTypeImpl merge(EffectiveNodeTypeImpl other) + throws ConstraintViolationException { + // create a clone of this instance and perform the merge on + // the 'clone' to avoid a potentially inconsistent state + // of this instance if an exception is thrown during + // the merge. + EffectiveNodeTypeImpl copy = (EffectiveNodeTypeImpl) clone(); + copy.internalMerge(other, false); + return copy; + } + + /** + * Internal helper method which merges another EffectiveNodeType + * instance with this instance. + *

        + * Warning: This instance might be in an inconsistent state if an exception + * is thrown. + * + * @param other + * @param supertype true if the merge is a result of inheritance, i.e. other + * represents one or more supertypes of this instance; otherwise false, i.e. + * the merge is the result of an explicit aggregation + * @throws ConstraintViolationException + */ + synchronized void internalMerge(EffectiveNodeTypeImpl other, boolean supertype) + throws ConstraintViolationException { + Name[] nta = other.getAllNodeTypes(); + int includedCount = 0; + for (int i = 0; i < nta.length; i++) { + if (includesNodeType(nta[i])) { + // redundant node type + log.debug("node type '" + nta[i] + "' is already contained."); + includedCount++; + } + } + if (includedCount == nta.length) { + // total overlap, ignore + return; + } + + // named item definitions + QItemDefinition[] defs = other.getNamedItemDefs(); + for (int i = 0; i < defs.length; i++) { + QItemDefinition qDef = defs[i]; + if (includesNodeType(qDef.getDeclaringNodeType())) { + // ignore redundant definitions + continue; + } + Name name = qDef.getName(); + List existingDefs = namedItemDefs.get(name); + if (existingDefs != null) { + if (existingDefs.size() > 0) { + // there already exists at least one definition with that name + for (int j = 0; j < existingDefs.size(); j++) { + QItemDefinition qItemDef = existingDefs.get(j); + // make sure none of them is auto-create + if (qDef.isAutoCreated() || qItemDef.isAutoCreated()) { + // conflict + String msg = "The item definition for '" + name + + "' in node type '" + + qDef.getDeclaringNodeType() + + "' conflicts with the one of node type '" + + qItemDef.getDeclaringNodeType() + + "': name collision with auto-create definition"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + // check ambiguous definitions + if (qDef.definesNode() == qItemDef.definesNode()) { + if (!qDef.definesNode()) { + // property definition + QPropertyDefinition pd = (QPropertyDefinition) qDef; + QPropertyDefinition epd = (QPropertyDefinition) qItemDef; + // compare type & multiValued flag + if (pd.getRequiredType() == epd.getRequiredType() + && pd.isMultiple() == epd.isMultiple()) { + // conflict + String msg = "The property definition for '" + + name + "' in node type '" + + qDef.getDeclaringNodeType() + + "' conflicts with the one of node type '" + + qItemDef.getDeclaringNodeType() + + "': ambiguous property definition. " + + "they must differ in required type " + + "or cardinality."; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } else { + // child node definition + // conflict + String msg = "The child node definition for '" + + name + "' in node type '" + + qDef.getDeclaringNodeType() + + "' conflicts with the one of node type '" + + qItemDef.getDeclaringNodeType() + + "': ambiguous child node definition. name must differ."; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + } + } + } else { + existingDefs = new ArrayList(); + namedItemDefs.put(name, existingDefs); + } + existingDefs.add(qDef); + } + + // residual item definitions + defs = other.getUnnamedItemDefs(); + for (int i = 0; i < defs.length; i++) { + QItemDefinition qDef = defs[i]; + if (includesNodeType(qDef.getDeclaringNodeType())) { + // ignore redundant definitions + continue; + } + for (QItemDefinition existing : unnamedItemDefs) { + // compare with existing definition + if (qDef.definesNode() == existing.definesNode()) { + if (!qDef.definesNode()) { + // property definition + QPropertyDefinition pd = (QPropertyDefinition) qDef; + QPropertyDefinition epd = (QPropertyDefinition) existing; + // compare type & multiValued flag + if (pd.getRequiredType() == epd.getRequiredType() + && pd.isMultiple() == epd.isMultiple() + && pd.getOnParentVersion() == epd.getOnParentVersion()) { + // conflict + // TODO: need to take more aspects into account + // TODO: getMatchingPropDef needs to check this as well + String msg = "A property definition in node type '" + + qDef.getDeclaringNodeType() + + "' conflicts with node type '" + + existing.getDeclaringNodeType() + + "': ambiguous residual property definition"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } else { + // child node definition + QNodeDefinition nd = (QNodeDefinition) qDef; + QNodeDefinition end = (QNodeDefinition) existing; + // compare required & default primary types + if (Arrays.equals(nd.getRequiredPrimaryTypes(), end.getRequiredPrimaryTypes()) + && (nd.getDefaultPrimaryType() == null + ? end.getDefaultPrimaryType() == null + : nd.getDefaultPrimaryType().equals(end.getDefaultPrimaryType()))) { + // conflict + String msg = "A child node definition in node type '" + + qDef.getDeclaringNodeType() + + "' conflicts with node type '" + + existing.getDeclaringNodeType() + + "': ambiguous residual child node definition"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + } + } + unnamedItemDefs.add(qDef); + } + for (int i = 0; i < nta.length; i++) { + allNodeTypes.add(nta[i]); + } + + if (supertype) { + // implicit merge as result of inheritance + + // add other merged node types as supertypes + nta = other.getMergedNodeTypes(); + for (int i = 0; i < nta.length; i++) { + inheritedNodeTypes.add(nta[i]); + } + // add supertypes of other merged node types as supertypes + nta = other.getInheritedNodeTypes(); + for (int i = 0; i < nta.length; i++) { + inheritedNodeTypes.add(nta[i]); + } + } else { + // explicit merge + + // merge with other merged node types + nta = other.getMergedNodeTypes(); + for (int i = 0; i < nta.length; i++) { + mergedNodeTypes.add(nta[i]); + } + // add supertypes of other merged node types as supertypes + nta = other.getInheritedNodeTypes(); + for (int i = 0; i < nta.length; i++) { + inheritedNodeTypes.add(nta[i]); + } + } + } + + @Override + protected Object clone() { + EffectiveNodeTypeImpl clone = new EffectiveNodeTypeImpl(mergedNodeTypes, + inheritedNodeTypes, allNodeTypes, namedItemDefs, unnamedItemDefs, + supportedMixins); + return clone; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeProvider.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeProvider.java new file mode 100644 index 00000000000..9fab68beedb --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/EffectiveNodeTypeProvider.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.jcr2spi.nodetype; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.ConstraintViolationException; +import java.util.Map; + +/** + * EffectiveNodeTypeProvider... + */ +public interface EffectiveNodeTypeProvider { + + /** + * Build the EffectiveNodeType from the given + * NodeType name. + * + * @param ntName + * @return + * @throws NoSuchNodeTypeException + */ + public EffectiveNodeType getEffectiveNodeType(Name ntName) + throws NoSuchNodeTypeException; + + /** + * Build the EffectiveNodeType from the given array of + * NodeType names. + * + * @param ntNames + * @return + * @throws ConstraintViolationException + * @throws NoSuchNodeTypeException + */ + public EffectiveNodeType getEffectiveNodeType(Name[] ntNames) + throws ConstraintViolationException, NoSuchNodeTypeException; + + /** + * @param ntNames + * @param ntdMap + * @return + * @throws ConstraintViolationException + * @throws NoSuchNodeTypeException + */ + public EffectiveNodeType getEffectiveNodeType(Name[] ntNames, Map ntdMap) + throws ConstraintViolationException, NoSuchNodeTypeException; + + /** + * Builds an effective node type representation from the given node type + * definition. Whereas all referenced node types must exist (i.e. must be + * present in the specified map), the definition itself is not required to + * be registered. + * + * @param ntd + * @param ntdMap + * @return + * @throws ConstraintViolationException + * @throws NoSuchNodeTypeException + */ + public EffectiveNodeType getEffectiveNodeType(QNodeTypeDefinition ntd, + Map ntdMap) + throws ConstraintViolationException, NoSuchNodeTypeException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/ItemDefinitionProvider.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/ItemDefinitionProvider.java new file mode 100644 index 00000000000..4df53206c1c --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/ItemDefinitionProvider.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.jcr2spi.nodetype; + +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.NodeId; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.ConstraintViolationException; + +/** + * ItemDefinitionProvider... + */ +public interface ItemDefinitionProvider { + + + /** + * Returns the QNodeDefinition for the root node. + * + * @return the QNodeDefinition for the root node. + * @throws RepositoryException + */ + public QNodeDefinition getRootNodeDefinition() throws RepositoryException; + + /** + * Returns the QNodeDefinition for the specified node state. + * + * @param parentNodeTypeNames + * @param nodeName + * @param ntName + * @param nodeId + * @return the QNodeDefinition for the specified node state. + * @throws RepositoryException + */ + public QNodeDefinition getQNodeDefinition(Name[] parentNodeTypeNames, + Name nodeName, Name ntName, + NodeId nodeId) throws RepositoryException; + + /** + * Returns the applicable child node definition for a child node with the + * specified name and node type. + * + * @param parentNodeTypeNames + * @param name + * @param nodeTypeName + * @return + * @throws NoSuchNodeTypeException + * @throws ConstraintViolationException if no applicable child node definition + * could be found + */ + public QNodeDefinition getQNodeDefinition(Name[] parentNodeTypeNames, + Name name, Name nodeTypeName) + throws NoSuchNodeTypeException, ConstraintViolationException; + + /** + * Returns the applicable child node definition for a child node with the + * specified name and node type. + * + * @param ent + * @param name + * @param nodeTypeName + * @return + * @throws NoSuchNodeTypeException + * @throws ConstraintViolationException if no applicable child node definition + * could be found + */ + public QNodeDefinition getQNodeDefinition(EffectiveNodeType ent, + Name name, Name nodeTypeName) + throws NoSuchNodeTypeException, ConstraintViolationException; + + /** + * Returns the QPropertyDefinition for the specified property state. + * @param propertyState + * @return the QPropertyDefinition for the specified property state. + * @throws RepositoryException + */ + /** + * Returns the QPropertyDefinition for the specified parameters. + * + * @param parentNodeTypeNames + * @param propertyName + * @param propertyType + * @param isMultiValued + * @param propertyId Used to retrieve the definition from the persistent + * layer if it cannot be determined from the information present. + * @return + * @throws RepositoryException + */ + public QPropertyDefinition getQPropertyDefinition(Name[] parentNodeTypeNames, + Name propertyName, + int propertyType, + boolean isMultiValued, + PropertyId propertyId) throws RepositoryException; + + /** + * Returns the applicable property definition for a property with the + * specified name, type and multiValued characteristic. If there more than + * one applicable definitions that would apply to the given params a + * ConstraintViolationException is thrown. + * + * @param ntName + * @param propName + * @param type + * @param multiValued + * @return + * @throws NoSuchNodeTypeException If no node type with name ntName + * exists. + * @throws ConstraintViolationException if no applicable property definition + * could be found + */ + public QPropertyDefinition getQPropertyDefinition(Name ntName, + Name propName, int type, + boolean multiValued) + throws ConstraintViolationException, NoSuchNodeTypeException; + + /** + * Returns the applicable property definition for a property with the + * specified name, type and multiValued characteristic. If there more than + * one applicable definitions then the following rules are applied: + *

          + *
        • named definitions are preferred to residual definitions
        • + *
        • definitions with specific required type are preferred to definitions + * with required type UNDEFINED
        • + *
        + * + * @param parentNodeTypeNames + * @param name + * @param type + * @param multiValued + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found. + */ + public QPropertyDefinition getQPropertyDefinition(Name[] parentNodeTypeNames, + Name name, int type, + boolean multiValued) + throws ConstraintViolationException, NoSuchNodeTypeException; + + /** + * Returns the applicable property definition for a property with the + * specified name and type. The multiValued flag is not taken into account + * in the selection algorithm. Other than + * {@link #getQPropertyDefinition(Name[], Name, int, boolean)} + * this method does not take the multiValued flag into account in the + * selection algorithm. If there more than one applicable definitions then + * the following rules are applied: + *
          + *
        • named definitions are preferred to residual definitions
        • + *
        • definitions with specific required type are preferred to definitions + * with required type UNDEFINED
        • + *
        • single-value definitions are preferred to multiple-value definitions
        • + *
        + * + * @param parentNodeTypeNames + * @param name + * @param type + * @return + * @throws ConstraintViolationException if no applicable property definition + * could be found + */ + public QPropertyDefinition getQPropertyDefinition(Name[] parentNodeTypeNames, + Name name, int type) + throws ConstraintViolationException, NoSuchNodeTypeException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/ItemDefinitionProviderImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/ItemDefinitionProviderImpl.java new file mode 100644 index 00000000000..a9f424a58db --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/ItemDefinitionProviderImpl.java @@ -0,0 +1,370 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.nodetype; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ItemDefinitionManagerImpl... + */ +public class ItemDefinitionProviderImpl implements ItemDefinitionProvider { + + private static Logger log = LoggerFactory.getLogger(ItemDefinitionProviderImpl.class); + + private final EffectiveNodeTypeProvider entProvider; + private final RepositoryService service; + private final SessionInfo sessionInfo; + private QNodeDefinition rootNodeDefinition; + + public ItemDefinitionProviderImpl(EffectiveNodeTypeProvider entProvider, + RepositoryService service, + SessionInfo sessionInfo) { + this.entProvider = entProvider; + this.service = service; + this.sessionInfo = sessionInfo; + } + + public QNodeDefinition getRootNodeDefinition() throws RepositoryException { + if (rootNodeDefinition == null) { + IdFactory idFactory = service.getIdFactory(); + PathFactory pf = service.getPathFactory(); + + rootNodeDefinition = service.getNodeDefinition( + sessionInfo, idFactory.createNodeId((String) null, pf.getRootPath())); + } + return rootNodeDefinition; + } + + public QNodeDefinition getQNodeDefinition(Name[] parentNodeTypeNames, + Name nodeName, Name ntName, + NodeId nodeId) throws RepositoryException { + if (parentNodeTypeNames == null) { + return getRootNodeDefinition(); + } + QNodeDefinition definition; + try { + EffectiveNodeType ent = entProvider.getEffectiveNodeType(parentNodeTypeNames); + EffectiveNodeType entTarget = getEffectiveNodeType(ntName); + definition = getQNodeDefinition(ent, entTarget, nodeName); + } catch (RepositoryException e) { + log.debug("Cannot determine effective node type of {}: {}", nodeId, e); + definition = getNodeDefinition(service, sessionInfo, nodeId); + } + return definition; + } + + public QNodeDefinition getQNodeDefinition(Name[] parentNodeTypeNames, Name name, Name nodeTypeName) + throws NoSuchNodeTypeException, ConstraintViolationException { + EffectiveNodeType ent = entProvider.getEffectiveNodeType(parentNodeTypeNames); + EffectiveNodeType entTarget = getEffectiveNodeType(nodeTypeName); + return getQNodeDefinition(ent, entTarget, name); + } + + public QNodeDefinition getQNodeDefinition(EffectiveNodeType ent, Name name, Name nodeTypeName) throws NoSuchNodeTypeException, ConstraintViolationException { + EffectiveNodeType entTarget = getEffectiveNodeType(nodeTypeName); + return getQNodeDefinition(ent, entTarget, name); + } + + public QPropertyDefinition getQPropertyDefinition(Name[] parentNodeTypeNames, + Name propertyName, + int propertyType, + boolean isMultiValued, + PropertyId propertyId) throws RepositoryException { + QPropertyDefinition definition; + try { + EffectiveNodeType ent = entProvider.getEffectiveNodeType(parentNodeTypeNames); + definition = getQPropertyDefinition(ent, propertyName, propertyType, isMultiValued, true); + } catch (RepositoryException e) { + log.debug("Cannot determine property definition of {}: {}", propertyId, e); + definition = getPropertyDefinition(service, sessionInfo, propertyId); + } + return definition; + } + + public QPropertyDefinition getQPropertyDefinition(Name ntName, Name propName, + int type, boolean multiValued) + throws ConstraintViolationException, NoSuchNodeTypeException { + EffectiveNodeType ent = entProvider.getEffectiveNodeType(ntName); + return getQPropertyDefinition(ent, propName, type, multiValued, false); + } + + public QPropertyDefinition getQPropertyDefinition(Name[] parentNodeTypeNames, + Name name, int type, + boolean multiValued) + throws ConstraintViolationException, NoSuchNodeTypeException { + EffectiveNodeType ent = entProvider.getEffectiveNodeType(parentNodeTypeNames); + return getQPropertyDefinition(ent, name, type, multiValued, false); + } + + public QPropertyDefinition getQPropertyDefinition(Name[] parentNodeTypeNames, + Name name, int type) + throws ConstraintViolationException, NoSuchNodeTypeException { + EffectiveNodeType ent = entProvider.getEffectiveNodeType(parentNodeTypeNames); + return getQPropertyDefinition(ent, name, type); + } + + //-------------------------------------------------------------------------- + private EffectiveNodeType getEffectiveNodeType(Name ntName) throws NoSuchNodeTypeException { + if (ntName != null) { + return entProvider.getEffectiveNodeType(ntName); + } else { + return null; + } + } + + /** + * + * @param ent + * @param entTarget + * @param name + * @return + * @throws ConstraintViolationException + */ + static QNodeDefinition getQNodeDefinition(EffectiveNodeType ent, + EffectiveNodeType entTarget, + Name name) + throws ConstraintViolationException { + + // try named node definitions first + QNodeDefinition[] defs = ent.getNamedQNodeDefinitions(name); + if (defs != null) { + for (int i = 0; i < defs.length; i++) { + QNodeDefinition nd = defs[i]; + // node definition with that name exists + if (entTarget != null && nd.getRequiredPrimaryTypes() != null) { + // check 'required primary types' constraint + if (entTarget.includesNodeTypes(nd.getRequiredPrimaryTypes())) { + // found named node definition + return nd; + } + } else { + if (nd.getDefaultPrimaryType() != null) { + // found node definition with default node type + return nd; + } + } + } + } + + // no item with that name defined; + // try residual node definitions + QNodeDefinition[] nda = ent.getUnnamedQNodeDefinitions(); + for (int i = 0; i < nda.length; i++) { + QNodeDefinition nd = nda[i]; + if (entTarget != null && nd.getRequiredPrimaryTypes() != null) { + // check 'required primary types' constraint + if (entTarget.includesNodeTypes(nd.getRequiredPrimaryTypes())) { + // found residual node definition + return nd; + } + } else { + // since no node type has been specified for the new node, + // it must be determined from the default node type; + if (nd.getDefaultPrimaryType() != null) { + // found residual node definition with default node type + return nd; + } + } + } + + // no applicable definition found + throw new ConstraintViolationException("no matching child node definition found for " + name); + } + + /** + * + * @param ent + * @param name + * @param type + * @param multiValued + * @return + * @throws ConstraintViolationException + */ + private static QPropertyDefinition getQPropertyDefinition(EffectiveNodeType ent, + Name name, int type, + boolean multiValued, boolean throwWhenAmbiguous) + throws ConstraintViolationException { + // try named property definitions first + QPropertyDefinition[] defs = ent.getNamedQPropertyDefinitions(name); + QPropertyDefinition match = getMatchingPropDef(defs, type, multiValued, throwWhenAmbiguous); + if (match != null) { + return match; + } + + // no item with that name defined; + // try residual property definitions + defs = ent.getUnnamedQPropertyDefinitions(); + match = getMatchingPropDef(defs, type, multiValued, throwWhenAmbiguous); + if (match != null) { + return match; + } + + // no applicable definition found + throw new ConstraintViolationException("no matching property definition found for " + name); + } + + /** + * + * @param ent + * @param name + * @param type + * @return + * @throws ConstraintViolationException + */ + private static QPropertyDefinition getQPropertyDefinition(EffectiveNodeType ent, + Name name, int type) + throws ConstraintViolationException { + // try named property definitions first + QPropertyDefinition[] defs = ent.getNamedQPropertyDefinitions(name); + QPropertyDefinition match = getMatchingPropDef(defs, type); + if (match != null) { + return match; + } + + // no item with that name defined; + // try residual property definitions + defs = ent.getUnnamedQPropertyDefinitions(); + match = getMatchingPropDef(defs, type); + if (match != null) { + return match; + } + + // no applicable definition found + throw new ConstraintViolationException("no matching property definition found for " + name); + } + + private static QPropertyDefinition getMatchingPropDef(QPropertyDefinition[] defs, int type) { + QPropertyDefinition match = null; + for (int i = 0; i < defs.length; i++) { + QItemDefinition qDef = defs[i]; + if (!qDef.definesNode()) { + QPropertyDefinition pd = (QPropertyDefinition) qDef; + int reqType = pd.getRequiredType(); + // match type + if (reqType == PropertyType.UNDEFINED + || type == PropertyType.UNDEFINED + || reqType == type) { + if (match == null) { + match = pd; + } else { + // check if this definition is a better match than + // the one we've already got + if (match.getRequiredType() != pd.getRequiredType()) { + if (match.getRequiredType() == PropertyType.UNDEFINED) { + // found better match + match = pd; + } + } else { + if (match.isMultiple() && !pd.isMultiple()) { + // found better match + match = pd; + } + } + } + if (match.getRequiredType() != PropertyType.UNDEFINED + && !match.isMultiple()) { + // found best possible match, get outta here + return match; + } + } + } + } + return match; + } + + private static QPropertyDefinition getMatchingPropDef(QPropertyDefinition[] defs, int type, + boolean multiValued, boolean throwWhenAmbiguous) + throws ConstraintViolationException { + QPropertyDefinition match = null; + for (int i = 0; i < defs.length; i++) { + QItemDefinition qDef = defs[i]; + if (!qDef.definesNode()) { + QPropertyDefinition pd = (QPropertyDefinition) qDef; + int reqType = pd.getRequiredType(); + // match type + if (reqType == PropertyType.UNDEFINED + || type == PropertyType.UNDEFINED + || reqType == type) { + // match multiValued flag + if (multiValued == pd.isMultiple()) { + // found match + if (pd.getRequiredType() != PropertyType.UNDEFINED) { + if (match != null && throwWhenAmbiguous) { + throw new ConstraintViolationException("ambiguous property definitions found: " + match + " vs " + pd); + } + + // If we already found a match, and that was of PropertyType.STRING, + // then do not overwrite it. The whole reason there are multiple + // potential matches is that the client did not specify the type, + // thus obviously specified a String. + if (match == null || match.getRequiredType() != PropertyType.STRING) { + // found best possible match + match = pd; + } + } else { + if (match == null) { + match = pd; + } + } + } + } + } + } + return match; + } + + private static QNodeDefinition getNodeDefinition(RepositoryService service, SessionInfo sessionInfo, + NodeId nodeId) throws RepositoryException { + + try { + return service.getNodeDefinition(sessionInfo, nodeId); + } + catch (RepositoryException e) { + log.error("Cannot determine node definition of {}: {}", nodeId, e); + throw e; + } + } + + private static QPropertyDefinition getPropertyDefinition(RepositoryService service, + SessionInfo sessionInfo, PropertyId propertyId) throws RepositoryException { + + try { + return service.getPropertyDefinition(sessionInfo, propertyId); + } + catch (RepositoryException e) { + log.error("Cannot determine property definition of {}: {}", propertyId, e); + throw e; + } + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeCache.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeCache.java new file mode 100644 index 00000000000..0a597ca00e8 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeCache.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.jcr2spi.nodetype; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeStorage; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import java.util.Map; +import java.util.Iterator; +import java.util.WeakHashMap; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; + +/** + * NodeTypeCache implements a cache for QNodeTypeDefinitions + * on a userId basis. + */ +public class NodeTypeCache { + + /** + * The caches per repository service instance + */ + private static final Map> CACHES_PER_SERVICE = new WeakHashMap>(); + + /** + * Maps node type Names to QNodeTypeDefinition + */ + private final Map nodeTypes = new HashMap(); + + /** + * @param service the repository service. + * @param userId the userId. If null this method will return a + * new cache instance for each such call. + * @return the NodeTypeCache instance for the given + * service and userId. + */ + public static NodeTypeCache getInstance(RepositoryService service, String userId) { + // if no userId is provided do not keep the cache + if (userId == null) { + return new NodeTypeCache(); + } + Map caches; + synchronized (CACHES_PER_SERVICE) { + caches = CACHES_PER_SERVICE.get(service); + if (caches == null) { + // use soft references for the node type caches + caches = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT); + CACHES_PER_SERVICE.put(service, caches); + } + } + synchronized (caches) { + NodeTypeCache cache = caches.get(userId); + if (cache == null) { + cache = new NodeTypeCache(); + caches.put(userId, cache); + } + return cache; + } + } + + private NodeTypeCache() { + } + + /** + * Returns an Iterator over all node type definitions registered. + * + * @return + * @throws javax.jcr.RepositoryException + */ + public Iterator getAllDefinitions(NodeTypeStorage storage) + throws RepositoryException { + Map allNts = new HashMap(); + for (Iterator it = storage.getAllDefinitions(); it.hasNext(); ) { + QNodeTypeDefinition def = it.next(); + allNts.put(def.getName(), def); + } + // update the cache + synchronized (nodeTypes) { + nodeTypes.clear(); + nodeTypes.putAll(allNts); + } + return allNts.values().iterator(); + } + + /** + * Returns the QNodeTypeDefinitions for the given node type + * names. The implementation is free to return additional definitions e.g. + * dependencies. + * + * @param nodeTypeNames + * @return + * @throws javax.jcr.nodetype.NoSuchNodeTypeException + * @throws RepositoryException + */ + public Iterator getDefinitions(NodeTypeStorage storage, Name[] nodeTypeNames) + throws NoSuchNodeTypeException, RepositoryException { + List nts = new ArrayList(); + List missing = null; + synchronized (nodeTypes) { + for (int i = 0; i < nodeTypeNames.length; i++) { + QNodeTypeDefinition def = nodeTypes.get(nodeTypeNames[i]); + if (def == null) { + if (missing == null) { + missing = new ArrayList(); + } + missing.add(nodeTypeNames[i]); + } else { + nts.add(def); + } + } + } + if (missing != null) { + Name[] ntNames = missing.toArray(new Name[missing.size()]); + Iterator it = storage.getDefinitions(ntNames); + synchronized (nodeTypes) { + while (it.hasNext()) { + QNodeTypeDefinition def = it.next(); + nts.add(def); + nodeTypes.put(def.getName(), def); + } + } + } + return nts.iterator(); + } + + public void registerNodeTypes(NodeTypeStorage storage, + QNodeTypeDefinition[] nodeTypeDefs, + boolean allowUpdate) + throws RepositoryException { + storage.registerNodeTypes(nodeTypeDefs, allowUpdate); + } + + public void unregisterNodeTypes(NodeTypeStorage storage, + Name[] nodeTypeNames) + throws NoSuchNodeTypeException, RepositoryException { + storage.unregisterNodeTypes(nodeTypeNames); + } + + /** + * Wraps this NodeTypeCache around the passed + * storage and exposes itself again as a + * NodeTypeStorage. + * + * @param storage the node type storage to wrap. + * @return node type storage instance using this cache. + */ + public NodeTypeStorage wrap(final NodeTypeStorage storage) { + return new NodeTypeStorage() { + public Iterator getAllDefinitions() throws RepositoryException { + return NodeTypeCache.this.getAllDefinitions(storage); + } + public Iterator getDefinitions(Name[] nodeTypeNames) + throws NoSuchNodeTypeException, RepositoryException { + return NodeTypeCache.this.getDefinitions(storage, nodeTypeNames); + } + public void registerNodeTypes(QNodeTypeDefinition[] nodeTypeDefs, boolean allowUpdate) + throws RepositoryException { + NodeTypeCache.this.registerNodeTypes(storage, nodeTypeDefs, allowUpdate); + } + public void unregisterNodeTypes(Name[] nodeTypeNames) + throws NoSuchNodeTypeException, RepositoryException { + NodeTypeCache.this.unregisterNodeTypes(storage, nodeTypeNames); + } + }; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeDefinitionProvider.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeDefinitionProvider.java new file mode 100644 index 00000000000..0d1616be742 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeDefinitionProvider.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.jcr2spi.nodetype; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.RepositoryException; + +/** + * NodeTypeDefinitionProvider... + */ +public interface NodeTypeDefinitionProvider { + + public QNodeTypeDefinition getNodeTypeDefinition(Name ntName) throws NoSuchNodeTypeException, RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeImpl.java new file mode 100644 index 00000000000..5e4ed10bef3 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeImpl.java @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.nodetype; + +import java.util.ArrayList; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.nodetype.AbstractNodeType; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.value.ValueHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NodeTypeImpl ... + */ +public class NodeTypeImpl extends AbstractNodeType implements NodeTypeDefinition { + + private static Logger log = LoggerFactory.getLogger(NodeTypeImpl.class); + + private final EffectiveNodeType ent; + private final NodeTypeManagerImpl ntMgr; + private final ManagerProvider mgrProvider; + + /** + * Package private constructor + *

        + * Creates a valid node type instance. + * We assume that the node type definition is valid and all referenced + * node types (supertypes, required node types etc.) do exist and are valid. + * + * @param ent the effective (i.e. merged and resolved) node type representation + * @param ntd the definition of this node type + * @param ntMgr the node type manager associated with this node type + * @param mgrProvider the manager provider + */ + NodeTypeImpl(EffectiveNodeType ent, QNodeTypeDefinition ntd, + NodeTypeManagerImpl ntMgr, ManagerProvider mgrProvider) { + super(ntd, ntMgr, mgrProvider.getNamePathResolver()); + this.ent = ent; + this.ntMgr = ntMgr; + this.mgrProvider = mgrProvider; + } + + private NamePathResolver resolver() { + return mgrProvider.getNamePathResolver(); + } + + private ItemDefinitionProvider definitionProvider() { + return mgrProvider.getItemDefinitionProvider(); + } + + /** + * Returns the applicable property definition for a property with the + * specified name and type. + * + * @param propertyName + * @param type + * @param multiValued + * @return + * @throws RepositoryException if no applicable property definition + * could be found + */ + private QPropertyDefinition getApplicablePropDef(Name propertyName, int type, boolean multiValued) + throws RepositoryException { + return definitionProvider().getQPropertyDefinition(ntd.getName(), propertyName, type, multiValued); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isNodeType(Name nodeTypeName) { + return ent.includesNodeType(nodeTypeName); + } + + /** + * Tests if the value constraints defined in the property definition + * def are satisfied by the the specified values. + *

        + * Note that the protected flag is not checked. Also note that no + * type conversions are attempted if the type of the given values does not + * match the required type as specified in the given definition. + * + * @param def The definition of the property + * @param values An array of QValue objects. + * @throws ConstraintViolationException If a constraint is violated. + * @throws RepositoryException If another error occurs. + */ + private static void checkSetPropertyValueConstraints(QPropertyDefinition def, + QValue[] values) + throws ConstraintViolationException, RepositoryException { + ValueConstraint.checkValueConstraints(def, values); + } + + //-------------------------------------------------< NodeTypeDefinition >--- + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#hasOrderableChildNodes() + */ + public boolean hasOrderableChildNodes() { + return ntd.hasOrderableChildNodes(); + } + + //-----------------------------------------------------------< NodeType >--- + + /** + * @see javax.jcr.nodetype.NodeType#getSupertypes() + */ + public NodeType[] getSupertypes() { + Name[] ntNames = ent.getInheritedNodeTypes(); + NodeType[] supertypes = new NodeType[ntNames.length]; + for (int i = 0; i < ntNames.length; i++) { + try { + supertypes[i] = ntMgr.getNodeType(ntNames[i]); + } catch (NoSuchNodeTypeException e) { + // should never get here + log.error("undefined supertype", e); + return new NodeType[0]; + } + } + return supertypes; + } + + /** + * @see javax.jcr.nodetype.NodeType#getChildNodeDefinitions() + */ + public NodeDefinition[] getChildNodeDefinitions() { + QNodeDefinition[] cnda = ent.getAllQNodeDefinitions(); + NodeDefinition[] nodeDefs = new NodeDefinition[cnda.length]; + for (int i = 0; i < cnda.length; i++) { + nodeDefs[i] = ntMgr.getNodeDefinition(cnda[i]); + } + return nodeDefs; + } + + /** + * @see javax.jcr.nodetype.NodeType#getPropertyDefinitions() + */ + public PropertyDefinition[] getPropertyDefinitions() { + QPropertyDefinition[] pda = ent.getAllQPropertyDefinitions(); + PropertyDefinition[] propDefs = new PropertyDefinition[pda.length]; + for (int i = 0; i < pda.length; i++) { + propDefs[i] = ntMgr.getPropertyDefinition(pda[i]); + } + return propDefs; + } + + /** + * @see javax.jcr.nodetype.NodeType#canSetProperty(String, Value) + */ + public boolean canSetProperty(String propertyName, Value value) { + if (value == null) { + // setting a property to null is equivalent of removing it + return canRemoveItem(propertyName); + } + try { + Name name = resolver().getQName(propertyName); + QPropertyDefinition def; + try { + // try to get definition that matches the given value type + def = getApplicablePropDef(name, value.getType(), false); + } catch (ConstraintViolationException cve) { + // fallback: ignore type + def = getApplicablePropDef(name, PropertyType.UNDEFINED, false); + } + if (def.isProtected()) { + return false; + } + if (def.isMultiple()) { + return false; + } + Value v; + if (def.getRequiredType() != PropertyType.UNDEFINED + && def.getRequiredType() != value.getType()) { + // type conversion required + v = ValueHelper.convert(value, def.getRequiredType(), mgrProvider.getJcrValueFactory()); + } else { + // no type conversion required + v = value; + } + // create QValue from Value + QValue qValue = ValueFormat.getQValue(v, resolver(), mgrProvider.getQValueFactory()); + checkSetPropertyValueConstraints(def, new QValue[]{qValue}); + return true; + } catch (NameException re) { + // fall through + } catch (RepositoryException e) { + // fall through + } + return false; + } + + /** + * @see javax.jcr.nodetype.NodeType#canSetProperty(String, Value[]) + */ + public boolean canSetProperty(String propertyName, Value[] values) { + if (values == null) { + // setting a property to null is equivalent of removing it + return canRemoveItem(propertyName); + } + try { + Name name = resolver().getQName(propertyName); + // determine type of values + int type = PropertyType.UNDEFINED; + for (int i = 0; i < values.length; i++) { + if (values[i] == null) { + // skip null values as those would be purged + continue; + } + if (type == PropertyType.UNDEFINED) { + type = values[i].getType(); + } else if (type != values[i].getType()) { + // inhomogeneous types + return false; + } + } + QPropertyDefinition def; + try { + // try to get definition that matches the given value type + def = getApplicablePropDef(name, type, true); + } catch (ConstraintViolationException cve) { + // fallback: ignore type + def = getApplicablePropDef(name, PropertyType.UNDEFINED, true); + } + + if (def.isProtected()) { + return false; + } + if (!def.isMultiple()) { + return false; + } + // determine target type + int targetType; + if (def.getRequiredType() != PropertyType.UNDEFINED + && def.getRequiredType() != type) { + // type conversion required + targetType = def.getRequiredType(); + } else { + // no type conversion required + targetType = type; + } + + ArrayList list = new ArrayList(); + // convert values and compact array (purge null entries) + for (int i = 0; i < values.length; i++) { + if (values[i] != null) { + // create QValue from Value and perform + // type conversion as necessary + Value v = ValueHelper.convert(values[i], targetType, mgrProvider.getJcrValueFactory()); + QValue qValue = ValueFormat.getQValue(v, resolver(), mgrProvider.getQValueFactory()); + list.add(qValue); + } + } + QValue[] internalValues = list.toArray(new QValue[list.size()]); + checkSetPropertyValueConstraints(def, internalValues); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * @see javax.jcr.nodetype.NodeType#canAddChildNode(String) + */ + public boolean canAddChildNode(String childNodeName) { + try { + ent.checkAddNodeConstraints(resolver().getQName(childNodeName), definitionProvider()); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * @see javax.jcr.nodetype.NodeType#canAddChildNode(String, String) + */ + public boolean canAddChildNode(String childNodeName, String nodeTypeName) { + try { + Name ntName = resolver().getQName(nodeTypeName); + QNodeTypeDefinition def = ntMgr.getNodeTypeDefinition(ntName); + ent.checkAddNodeConstraints(resolver().getQName(childNodeName), def, definitionProvider()); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * @see javax.jcr.nodetype.NodeType#canRemoveItem(String) + */ + public boolean canRemoveItem(String itemName) { + try { + ent.checkRemoveItemConstraints(resolver().getQName(itemName)); + return true; + } catch (NameException be) { + // implementation specific exception, fall through + } catch (RepositoryException re) { + // fall through + } + return false; + } + + /** + * @see javax.jcr.nodetype.NodeType#canRemoveNode(String) + */ + public boolean canRemoveNode(String nodeName) { + Name name; + try { + name = resolver().getQName(nodeName); + } catch (RepositoryException e) { + // should never get here + log.warn("Unable to determine if there are any remove constraints for a node with name " + nodeName); + return false; + } + return !ent.hasRemoveNodeConstraint(name); + + } + + /** + * @see javax.jcr.nodetype.NodeType#canRemoveProperty(String) + */ + public boolean canRemoveProperty(String propertyName) { + Name name; + try { + name = resolver().getQName(propertyName); + } catch (RepositoryException e) { + // should never get here + log.warn("Unable to determine if there are any remove constraints for a property with name " + propertyName); + return false; + } + return !ent.hasRemovePropertyConstraint(name); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeManagerImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeManagerImpl.java new file mode 100644 index 00000000000..41b438b4da3 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeManagerImpl.java @@ -0,0 +1,491 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.nodetype; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.NamespaceException; +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.NodeTypeDefinition; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.commons.iterator.NodeTypeIteratorAdapter; +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.nodetype.AbstractNodeTypeManager; +import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A NodeTypeManagerImpl implements a session dependant + * NodeTypeManager. + */ +public class NodeTypeManagerImpl extends AbstractNodeTypeManager implements NodeTypeDefinitionProvider, NodeTypeRegistryListener { + + /** + * Logger instance for this class + */ + private static Logger log = LoggerFactory.getLogger(NodeTypeManagerImpl.class); + + /** + * The ManagerProvider + */ + private final ManagerProvider mgrProvider; + + /** + * The wrapped node type registry. + */ + private final NodeTypeRegistry ntReg; + + /** + * The ValueFactory used to build property definitions. + */ + private final ValueFactory valueFactory; + + /** + * A cache for NodeType instances created by this + * NodeTypeManager + */ + private final Map ntCache; + + /** + * A cache for PropertyDefinition instances created by this + * NodeTypeManager + */ + private final Map pdCache; + + /** + * A cache for NodeDefinition instances created by this + * NodeTypeManager + */ + private final Map ndCache; + + /** + * Creates a new NodeTypeManagerImpl instance. + * + * @param ntReg node type registry + * @param mgrProvider the manager provider + * @throws RepositoryException If an error occurs. + */ + public NodeTypeManagerImpl(NodeTypeRegistry ntReg, + ManagerProvider mgrProvider) throws RepositoryException { + this.mgrProvider = mgrProvider; + this.ntReg = ntReg; + this.ntReg.addListener(this); + this.valueFactory = mgrProvider.getJcrValueFactory(); + + // setup caches with soft references to node type + ntCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT); + pdCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT); + ndCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT); + } + + private EffectiveNodeTypeProvider entProvider() { + return mgrProvider.getEffectiveNodeTypeProvider(); + } + + //-------------------------------------------------------------------------- + /** + * @see AbstractNodeTypeManager#getNodeType(org.apache.jackrabbit.spi.Name) + */ + @Override + public NodeTypeImpl getNodeType(Name name) throws NoSuchNodeTypeException { + synchronized (ntCache) { + NodeTypeImpl nt = ntCache.get(name); + if (nt == null) { + EffectiveNodeType ent = entProvider().getEffectiveNodeType(name); + QNodeTypeDefinition def = ntReg.getNodeTypeDefinition(name); + nt = new NodeTypeImpl(ent, def, this, mgrProvider); + ntCache.put(name, nt); + } + return nt; + } + } + + /** + * @see org.apache.jackrabbit.spi.commons.nodetype.AbstractNodeTypeManager#getNamePathResolver() + */ + @Override + public NamePathResolver getNamePathResolver() { + return mgrProvider.getNamePathResolver(); + } + + /** + * + * @param nodeTypeName + * @return + */ + public boolean hasNodeType(Name nodeTypeName) { + boolean isRegistered = ntCache.containsKey(nodeTypeName); + if (!isRegistered) { + isRegistered = ntReg.isRegistered(nodeTypeName); + } + return isRegistered; + } + + /** + * Retrieve the NodeDefinition for the given + * QNodeDefinition. + * + * @param def + * @return + */ + @Override + public NodeDefinition getNodeDefinition(QNodeDefinition def) { + synchronized (ndCache) { + NodeDefinition ndi = ndCache.get(def); + if (ndi == null) { + ndi = new NodeDefinitionImpl(def, this, getNamePathResolver()); + ndCache.put(def, ndi); + } + return ndi; + } + } + + /** + * Retrieve the PropertyDefinition for the given + * QPropertyDefinition. + * + * @param def + * @return + */ + @Override + public PropertyDefinition getPropertyDefinition(QPropertyDefinition def) { + synchronized (pdCache) { + PropertyDefinition pdi = pdCache.get(def); + if (pdi == null) { + pdi = new PropertyDefinitionImpl(def, this, getNamePathResolver(), valueFactory); + pdCache.put(def, pdi); + } + return pdi; + } + } + + /** + * @return the NodeTypeRegistry + */ + NodeTypeRegistry getNodeTypeRegistry() { + return ntReg; + } + + //-----------------------------------------< NodeTypeDefinitionProvider >--- + /** + * @see NodeTypeDefinitionProvider#getNodeTypeDefinition(org.apache.jackrabbit.spi.Name) + */ + public QNodeTypeDefinition getNodeTypeDefinition(Name ntName) throws NoSuchNodeTypeException, RepositoryException { + NodeTypeImpl nt = getNodeType(ntName); + return nt.getDefinition(); + } + + //-------------------------------------------< NodeTypeRegistryListener >--- + /** + * {@inheritDoc} + */ + public void nodeTypeRegistered(Name ntName) { + // not interested, ignore + } + + /** + * {@inheritDoc} + */ + public void nodeTypeReRegistered(Name ntName) { + // flush all affected cache entries + ntCache.remove(ntName); + try { + String name = getNamePathResolver().getJCRName(ntName); + synchronized (pdCache) { + Iterator iter = pdCache.values().iterator(); + while (iter.hasNext()) { + PropertyDefinition pd = iter.next(); + if (name.equals(pd.getDeclaringNodeType().getName())) { + iter.remove(); + } + } + } + synchronized (ndCache) { + Iterator iter = ndCache.values().iterator(); + while (iter.hasNext()) { + NodeDefinition nd = iter.next(); + if (name.equals(nd.getDeclaringNodeType().getName())) { + iter.remove(); + } + } + } + } catch (NamespaceException e) { + log.warn(e.getMessage() + " -> clear definition cache." ); + synchronized (pdCache) { + pdCache.clear(); + } + synchronized (ndCache) { + ndCache.clear(); + } + } + } + + /** + * {@inheritDoc} + */ + public void nodeTypeUnregistered(Name ntName) { + // flush all affected cache entries + ntCache.remove(ntName); + try { + String name = getNamePathResolver().getJCRName(ntName); + synchronized (pdCache) { + Iterator iter = pdCache.values().iterator(); + while (iter.hasNext()) { + PropertyDefinition pd = iter.next(); + if (name.equals(pd.getDeclaringNodeType().getName())) { + iter.remove(); + } + } + } + synchronized (ndCache) { + Iterator iter = ndCache.values().iterator(); + while (iter.hasNext()) { + NodeDefinition nd = iter.next(); + if (name.equals(nd.getDeclaringNodeType().getName())) { + iter.remove(); + } + } + } + } catch (NamespaceException e) { + log.warn(e.getMessage() + " -> clear definition cache." ); + synchronized (pdCache) { + pdCache.clear(); + } + synchronized (ndCache) { + ndCache.clear(); + } + } + } + + //----------------------------------------------------< NodeTypeManager >--- + /** + * {@inheritDoc} + */ + public NodeTypeIterator getAllNodeTypes() throws RepositoryException { + Name[] ntNames = ntReg.getRegisteredNodeTypes(); + ArrayList list = new ArrayList(ntNames.length); + for (Name ntName : ntNames) { + list.add(getNodeType(ntName)); + } + return new NodeTypeIteratorAdapter(list); + } + + /** + * {@inheritDoc} + */ + public NodeTypeIterator getPrimaryNodeTypes() throws RepositoryException { + Name[] ntNames = ntReg.getRegisteredNodeTypes(); + ArrayList list = new ArrayList(ntNames.length); + for (Name ntName : ntNames) { + NodeType nt = getNodeType(ntName); + if (!nt.isMixin()) { + list.add(nt); + } + } + return new NodeTypeIteratorAdapter(list); + } + + /** + * {@inheritDoc} + */ + public NodeTypeIterator getMixinNodeTypes() throws RepositoryException { + Name[] ntNames = ntReg.getRegisteredNodeTypes(); + ArrayList list = new ArrayList(ntNames.length); + for (Name ntName : ntNames) { + NodeType nt = getNodeType(ntName); + if (nt.isMixin()) { + list.add(nt); + } + } + return new NodeTypeIteratorAdapter(list); + } + + /** + * {@inheritDoc} + */ + public NodeType getNodeType(String nodeTypeName) + throws NoSuchNodeTypeException { + try { + Name qName = getNamePathResolver().getQName(nodeTypeName); + return getNodeType(qName); + } catch (NamespaceException e) { + throw new NoSuchNodeTypeException(nodeTypeName, e); + } catch (NameException e) { + throw new NoSuchNodeTypeException(nodeTypeName, e); + } + } + + /** + * @see NodeTypeManager#hasNodeType(String) + */ + public boolean hasNodeType(String name) throws RepositoryException { + try { + Name qName = getNamePathResolver().getQName(name); + return hasNodeType(qName); + } catch (NamespaceException e) { + return false; + } catch (NameException e) { + return false; + } + } + + /** + * @see NodeTypeManager#registerNodeTypes(javax.jcr.nodetype.NodeTypeDefinition[], boolean) + */ + public NodeTypeIterator registerNodeTypes(NodeTypeDefinition[] ntds, boolean allowUpdate) + throws RepositoryException { + List defs = new ArrayList(ntds.length); + for (NodeTypeDefinition definition : ntds) { + QNodeTypeDefinition qdef = new QNodeTypeDefinitionImpl(definition, getNamePathResolver(), mgrProvider.getQValueFactory()); + if (!allowUpdate && hasNodeType(qdef.getName())) { + throw new NodeTypeExistsException("NodeType " + definition.getName() + " already exists."); + } + defs.add(qdef); + } + + getNodeTypeRegistry().registerNodeTypes(defs, allowUpdate); + + List nts = new ArrayList(); + for (QNodeTypeDefinition def : defs) { + nts.add(getNodeType(def.getName())); + } + return new NodeTypeIteratorAdapter(nts); + + } + + /** + * @see NodeTypeManager#unregisterNodeTypes(String[]) + */ + public void unregisterNodeTypes(String[] names) throws RepositoryException { + HashSet ntNames = new HashSet(); + for (String name : names) { + ntNames.add(getNamePathResolver().getQName(name)); + } + getNodeTypeRegistry().unregisterNodeTypes(ntNames); + } + + //-------------------------------------------------------------< Object >--- + + /** + * Returns the the state of this instance in a human readable format. + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("NodeTypeManager (" + super.toString() + ")\n"); + builder.append("All NodeTypes:\n"); + try { + NodeTypeIterator iter = this.getAllNodeTypes(); + while (iter.hasNext()) { + NodeType nt = iter.nextNodeType(); + builder.append(nt.getName()); + builder.append("\n\tSupertypes"); + for (NodeType supertype : nt.getSupertypes()) { + builder.append("\n\t\t" + supertype.getName()); + } + builder.append("\n\tMixin\t" + nt.isMixin()); + builder.append("\n\tOrderableChildNodes\t" + nt.hasOrderableChildNodes()); + builder.append("\n\tPrimaryItemName\t" + (nt.getPrimaryItemName() == null ? "" : nt.getPrimaryItemName())); + for (PropertyDefinition aPd : nt.getPropertyDefinitions()) { + builder.append("\n\tPropertyDefinition"); + builder.append(" (declared in " + aPd.getDeclaringNodeType().getName() + ") "); + builder.append("\n\t\tName\t\t" + (aPd.getName())); + String type = aPd.getRequiredType() == 0 ? "null" : PropertyType.nameFromValue(aPd.getRequiredType()); + builder.append("\n\t\tRequiredType\t" + type); + String[] vca = aPd.getValueConstraints(); + StringBuffer constraints = new StringBuffer(); + if (vca == null) { + constraints.append(""); + } else { + for (String aVca : vca) { + if (constraints.length() > 0) { + constraints.append(", "); + } + constraints.append(aVca); + } + } + builder.append("\n\t\tValueConstraints\t" + constraints.toString()); + Value[] defVals = aPd.getDefaultValues(); + StringBuffer defaultValues = new StringBuffer(); + if (defVals == null) { + defaultValues.append(""); + } else { + for (Value defVal : defVals) { + if (defaultValues.length() > 0) { + defaultValues.append(", "); + } + defaultValues.append(defVal.getString()); + } + } + builder.append("\n\t\tDefaultValue\t" + defaultValues.toString()); + builder.append("\n\t\tAutoCreated\t" + aPd.isAutoCreated()); + builder.append("\n\t\tMandatory\t" + aPd.isMandatory()); + builder.append("\n\t\tOnVersion\t" + OnParentVersionAction.nameFromValue(aPd.getOnParentVersion())); + builder.append("\n\t\tProtected\t" + aPd.isProtected()); + builder.append("\n\t\tMultiple\t" + aPd.isMultiple()); + } + for (NodeDefinition aNd : nt.getChildNodeDefinitions()) { + builder.append("\n\tNodeDefinition"); + builder.append(" (declared in " + aNd.getDeclaringNodeType() + ") "); + builder.append("\n\t\tName\t\t" + aNd.getName()); + NodeType[] reqPrimaryTypes = aNd.getRequiredPrimaryTypes(); + if (reqPrimaryTypes != null && reqPrimaryTypes.length > 0) { + for (NodeType reqPrimaryType : reqPrimaryTypes) { + builder.append("\n\t\tRequiredPrimaryType\t" + reqPrimaryType.getName()); + } + } + NodeType defPrimaryType = aNd.getDefaultPrimaryType(); + if (defPrimaryType != null) { + builder.append("\n\t\tDefaultPrimaryType\t" + defPrimaryType.getName()); + } + builder.append("\n\t\tAutoCreated\t" + aNd.isAutoCreated()); + builder.append("\n\t\tMandatory\t" + aNd.isMandatory()); + builder.append("\n\t\tOnVersion\t" + OnParentVersionAction.nameFromValue(aNd.getOnParentVersion())); + builder.append("\n\t\tProtected\t" + aNd.isProtected()); + builder.append("\n\t\tAllowsSameNameSiblings\t" + aNd.allowsSameNameSiblings()); + } + } + } catch (RepositoryException e) { + builder.append(e.getMessage()); + } + return builder.toString(); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistry.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistry.java new file mode 100644 index 00000000000..7f7a726c49d --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistry.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.jcr2spi.nodetype; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.RepositoryException; +import java.util.Collection; + +/** + * NodeTypeRegistry... + * + */ +public interface NodeTypeRegistry { + + /** + * Returns the node type definition of the node type with the given name. + * + * @param nodeTypeName name of node type whose definition should be returned. + * @return the node type definition of the node type with the given name. + * @throws NoSuchNodeTypeException if a node type with the given name + * does not exist + */ + QNodeTypeDefinition getNodeTypeDefinition(Name nodeTypeName) + throws NoSuchNodeTypeException; + + /** + * Add a NodeTypeRegistryListener + * + * @param listener the new listener to be informed on (un)registration + * of node types + */ + void addListener(NodeTypeRegistryListener listener); + + /** + * Remove a NodeTypeRegistryListener + * + * @param listener an existing listener + */ + void removeListener(NodeTypeRegistryListener listener); + + /** + * @param ntName + * @return + */ + boolean isRegistered(Name ntName); + + /** + * Returns the names of all registered node types. That includes primary + * and mixin node types. + * + * @return the names of all registered node types. + */ + public Name[] getRegisteredNodeTypes() throws RepositoryException; + + /** + * Registers the specified node type definitions. If allowUpdate + * is true existing node types will be updated, otherwise + * an NodeTypeExistsException is thrown. + * + * @param ntDefs + * @param allowUpdate + * @throws NodeTypeExistsException + * @throws InvalidNodeTypeDefinitionException + * @throws RepositoryException + */ + public void registerNodeTypes(Collection ntDefs, boolean allowUpdate) throws NodeTypeExistsException, InvalidNodeTypeDefinitionException, RepositoryException; + + /** + * Unregisters a collection of node types. + * + * @param nodeTypeNames a collection of Name objects denoting the + * node types to be unregistered + * @throws NoSuchNodeTypeException if any of the specified names does not + * denote a registered node type. + * @throws RepositoryException if another error occurs + */ + public void unregisterNodeTypes(Collection nodeTypeNames) + throws NoSuchNodeTypeException, RepositoryException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistryImpl.java new file mode 100644 index 00000000000..6a579858e79 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistryImpl.java @@ -0,0 +1,822 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.nodetype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +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 java.util.Set; +import java.util.TreeSet; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeStorage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A NodeTypeRegistry ... + */ +public class NodeTypeRegistryImpl implements NodeTypeRegistry, EffectiveNodeTypeProvider { + + private static Logger log = LoggerFactory.getLogger(NodeTypeRegistryImpl.class); + + // cache of pre-built aggregations of node types + private final EffectiveNodeTypeCache entCache; + + // map of node type names and node type definitions + private final NodeTypeDefinitionMap registeredNTDefs; + + /** + * Object used to persist new nodetypes and modified nodetype definitions. + */ + private final NodeTypeStorage storage; + + /** + * Class used to validate NodeType definitions + */ + private final DefinitionValidator validator; + + /** + * Listeners (soft references) + */ + @SuppressWarnings("unchecked") + private final Map listeners = Collections.synchronizedMap(new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK)); + + /** + * Create a new NodeTypeRegistry + * + * @param storage + * @param nsRegistry + * @return NodeTypeRegistry object + */ + public static NodeTypeRegistryImpl create(NodeTypeStorage storage, NamespaceRegistry nsRegistry) { + NodeTypeRegistryImpl ntRegistry = new NodeTypeRegistryImpl(storage, nsRegistry); + return ntRegistry; + } + + /** + * Clears all caches. + */ + public synchronized void dispose() { + entCache.clear(); + registeredNTDefs.clear(); + listeners.clear(); + } + + /** + * Private constructor + * + * @param storage + * @param nsRegistry + */ + private NodeTypeRegistryImpl(NodeTypeStorage storage, NamespaceRegistry nsRegistry) { + this.storage = storage; + this.validator = new DefinitionValidator(this, nsRegistry); + + entCache = new BitsetENTCacheImpl(); + registeredNTDefs = new NodeTypeDefinitionMap(); + } + + //---------------------------------------------------< NodeTypeRegistry >--- + /** + * @see NodeTypeRegistry#addListener(NodeTypeRegistryListener) + */ + public void addListener(NodeTypeRegistryListener listener) { + if (!listeners.containsKey(listener)) { + listeners.put(listener, listener); + } + } + + /** + * @see NodeTypeRegistry#removeListener(NodeTypeRegistryListener) + */ + public void removeListener(NodeTypeRegistryListener listener) { + listeners.remove(listener); + } + + /** + * @see NodeTypeRegistry#getRegisteredNodeTypes() + */ + public Name[] getRegisteredNodeTypes() throws RepositoryException { + Set qNames = registeredNTDefs.keySet(); + return qNames.toArray(new Name[registeredNTDefs.size()]); + } + + + /** + * @see NodeTypeRegistry#isRegistered(Name) + */ + public boolean isRegistered(Name nodeTypeName) { + return registeredNTDefs.containsKey(nodeTypeName); + } + + /** + * @see NodeTypeRegistry#registerNodeTypes(Collection, boolean) + */ + public synchronized void registerNodeTypes(Collection ntDefs, boolean allowUpdate) throws NodeTypeExistsException, InvalidNodeTypeDefinitionException, RepositoryException { + List added = new ArrayList(); + List modified = new ArrayList(); + for (QNodeTypeDefinition def : ntDefs) { + Name name = def.getName(); + if (isRegistered(name)) { + modified.add(name); + } else { + added.add(name); + } + } + + // validate new nodetype definitions + Map defMap = validator.validateNodeTypeDefs(ntDefs, registeredNTDefs); + storage.registerNodeTypes(ntDefs.toArray(new QNodeTypeDefinition[ntDefs.size()]), allowUpdate); + + // update internal cache: + // unregister modified node type definition + internalUnregister(modified); + // register all new and modified definition + internalRegister(defMap); + + // notify listeners + for (Name ntName : added) { + notifyRegistered(ntName); + } + for (Name ntName : modified) { + notifyReRegistered(ntName); + } + } + + /** + * @see NodeTypeRegistry#unregisterNodeTypes(Collection) + */ + public synchronized void unregisterNodeTypes(Collection nodeTypeNames) + throws NoSuchNodeTypeException, RepositoryException { + // do some preliminary checks + for (Name ntName : nodeTypeNames) { + // Best effort check for node types other than those to be + // unregistered that depend on the given node types + Set dependents = registeredNTDefs.getDependentNodeTypes(ntName); + dependents.removeAll(nodeTypeNames); + if (dependents.size() > 0) { + StringBuffer msg = new StringBuffer(); + msg.append(ntName).append(" can not be removed because the following node types depend on it: "); + for (Name name : dependents) { + msg.append(name); + msg.append(" "); + } + throw new RepositoryException(msg.toString()); + } + } + + // persist removal of node type definitions + // NOTE: conflict with existing content not asserted on client + storage.unregisterNodeTypes(nodeTypeNames.toArray(new Name[nodeTypeNames.size()])); + + + // all preconditions are met, node types can now safely be unregistered + internalUnregister(nodeTypeNames); + + // notify listeners + for (Name ntName : nodeTypeNames) { + notifyUnregistered(ntName); + } + } + + /** + * @see NodeTypeRegistry#getNodeTypeDefinition(Name) + */ + public QNodeTypeDefinition getNodeTypeDefinition(Name nodeTypeName) + throws NoSuchNodeTypeException { + QNodeTypeDefinition def = registeredNTDefs.get(nodeTypeName); + if (def == null) { + throw new NoSuchNodeTypeException("Nodetype " + nodeTypeName + " doesn't exist"); + } + return def; + } + //------------------------------------------< EffectiveNodeTypeProvider >--- + /** + * @see EffectiveNodeTypeProvider#getEffectiveNodeType(Name) + */ + public synchronized EffectiveNodeType getEffectiveNodeType(Name ntName) + throws NoSuchNodeTypeException { + return getEffectiveNodeType(ntName, entCache, registeredNTDefs); + } + + /** + * @see EffectiveNodeTypeProvider#getEffectiveNodeType(Name[]) + */ + public synchronized EffectiveNodeType getEffectiveNodeType(Name[] ntNames) + throws ConstraintViolationException, NoSuchNodeTypeException { + return getEffectiveNodeType(ntNames, entCache, registeredNTDefs); + } + + /** + * @see EffectiveNodeTypeProvider#getEffectiveNodeType(Name[], Map) + */ + public EffectiveNodeType getEffectiveNodeType(Name[] ntNames, Map ntdMap) + throws ConstraintViolationException, NoSuchNodeTypeException { + return getEffectiveNodeType(ntNames, entCache, ntdMap); + } + + /** + * @see EffectiveNodeTypeProvider#getEffectiveNodeType(QNodeTypeDefinition, Map) + */ + public EffectiveNodeType getEffectiveNodeType(QNodeTypeDefinition ntd, Map ntdMap) + throws ConstraintViolationException, NoSuchNodeTypeException { + TreeSet mergedNodeTypes = new TreeSet(); + TreeSet inheritedNodeTypes = new TreeSet(); + TreeSet allNodeTypes = new TreeSet(); + Map> namedItemDefs = new HashMap>(); + List unnamedItemDefs = new ArrayList(); + Set supportedMixins = null; + + Name ntName = ntd.getName(); + // prepare new instance + mergedNodeTypes.add(ntName); + allNodeTypes.add(ntName); + + Name[] smixins = ntd.getSupportedMixinTypes(); + + if (smixins != null) { + supportedMixins = new HashSet(); + supportedMixins.addAll(Arrays.asList(smixins)); + } + + // map of all item definitions (maps id to definition) + // used to effectively detect ambiguous child definitions where + // ambiguity is defined in terms of definition identity + Set itemDefIds = new HashSet(); + + for (QNodeDefinition nd : ntd.getChildNodeDefs()) { + // check if child node definition would be ambiguous within + // this node type definition + if (itemDefIds.contains(nd)) { + // conflict + String msg; + if (nd.definesResidual()) { + msg = ntName + " contains ambiguous residual child node definitions"; + } else { + msg = ntName + " contains ambiguous definitions for child node named " + + nd.getName(); + } + log.debug(msg); + throw new ConstraintViolationException(msg); + } else { + itemDefIds.add(nd); + } + if (nd.definesResidual()) { + // residual node definition + unnamedItemDefs.add(nd); + } else { + // named node definition + Name name = nd.getName(); + List defs = namedItemDefs.get(name); + if (defs == null) { + defs = new ArrayList(); + namedItemDefs.put(name, defs); + } + if (defs.size() > 0) { + /** + * there already exists at least one definition with that + * name; make sure none of them is auto-create + */ + for (QItemDefinition qDef : defs) { + if (nd.isAutoCreated() || qDef.isAutoCreated()) { + // conflict + String msg = "There are more than one 'auto-create' item definitions for '" + + name + "' in node type '" + ntName + "'"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + } + defs.add(nd); + } + } + for (QPropertyDefinition pd : ntd.getPropertyDefs()) { + // check if property definition would be ambiguous within + // this node type definition + if (itemDefIds.contains(pd)) { + // conflict + String msg; + if (pd.definesResidual()) { + msg = ntName + " contains ambiguous residual property definitions"; + } else { + msg = ntName + " contains ambiguous definitions for property named " + + pd.getName(); + } + log.debug(msg); + throw new ConstraintViolationException(msg); + } else { + itemDefIds.add(pd); + } + if (pd.definesResidual()) { + // residual property definition + unnamedItemDefs.add(pd); + } else { + // named property definition + Name name = pd.getName(); + List defs = namedItemDefs.get(name); + if (defs == null) { + defs = new ArrayList(); + namedItemDefs.put(name, defs); + } + if (defs.size() > 0) { + /** + * there already exists at least one definition with that + * name; make sure none of them is auto-create + */ + for (QItemDefinition qDef : defs) { + if (pd.isAutoCreated() || qDef.isAutoCreated()) { + // conflict + String msg = "There are more than one 'auto-create' item definitions for '" + + name + "' in node type '" + ntName + "'"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + } + defs.add(pd); + } + } + + // create empty effective node type instance + EffectiveNodeTypeImpl ent = new EffectiveNodeTypeImpl(mergedNodeTypes, + inheritedNodeTypes, allNodeTypes, namedItemDefs, + unnamedItemDefs, supportedMixins); + + // resolve supertypes recursively + Name[] supertypes = ntd.getSupertypes(); + if (supertypes.length > 0) { + EffectiveNodeTypeImpl effSuperType = (EffectiveNodeTypeImpl) getEffectiveNodeType(supertypes, ntdMap); + ent.internalMerge(effSuperType, true); + } + return ent; + } + + /** + * + * @param ntName + * @param entCache + * @param ntdCache + * @return + * @throws NoSuchNodeTypeException + */ + private EffectiveNodeType getEffectiveNodeType(Name ntName, + EffectiveNodeTypeCache entCache, + Map ntdCache) + throws NoSuchNodeTypeException { + // 1. check if effective node type has already been built + EffectiveNodeTypeCache.Key key = entCache.getKey(new Name[]{ntName}); + EffectiveNodeType ent = entCache.get(key); + if (ent != null) { + return ent; + } + + // 2. make sure we've got the definition of the specified node type + QNodeTypeDefinition ntd = ntdCache.get(ntName); + if (ntd == null) { + throw new NoSuchNodeTypeException(ntName.toString()); + } + + // 3. build effective node type + synchronized (entCache) { + try { + ent = getEffectiveNodeType(ntd, ntdCache); + // store new effective node type + entCache.put(ent); + return ent; + } catch (ConstraintViolationException e) { + // should never get here as all known node types should be valid! + String msg = "Internal error: encountered invalid registered node type " + ntName; + log.debug(msg); + throw new NoSuchNodeTypeException(msg, e); + } + } + } + + /** + * @param ntNames + * @param entCache + * @param ntdCache + * @return + * @throws ConstraintViolationException + * @throws NoSuchNodeTypeException + */ + private EffectiveNodeType getEffectiveNodeType(Name[] ntNames, + EffectiveNodeTypeCache entCache, + Map ntdCache) + throws ConstraintViolationException, NoSuchNodeTypeException { + + EffectiveNodeTypeCache.Key key = entCache.getKey(ntNames); + // 1. check if aggregate has already been built + if (entCache.contains(key)) { + return entCache.get(key); + } + + // 2. make sure we've got the definitions of the specified node types + for (Name ntName : ntNames) { + if (!ntdCache.containsKey(ntName)) { + throw new NoSuchNodeTypeException(ntName.toString()); + } + } + + // 3. build aggregate + EffectiveNodeTypeCache.Key requested = key; + EffectiveNodeTypeImpl result = null; + synchronized (entCache) { + // build list of 'best' existing sub-aggregates + while (key.getNames().length > 0) { + // find the (sub) key that matches the current key the best + EffectiveNodeTypeCache.Key subKey = entCache.findBest(key); + if (subKey != null) { + EffectiveNodeTypeImpl ent = (EffectiveNodeTypeImpl) entCache.get(subKey); + if (result == null) { + result = ent; + } else { + result = result.merge(ent); + // store intermediate result + entCache.put(result); + } + // subtract the result from the temporary key + key = key.subtract(subKey); + } else { + /** + * no matching sub-aggregates found: + * build aggregate of remaining node types through iteration + */ + for (Name remainder : key.getNames()) { + QNodeTypeDefinition ntd = ntdCache.get(remainder); + EffectiveNodeType ent = getEffectiveNodeType(ntd, ntdCache); + // store new effective node type + entCache.put(ent); + if (result == null) { + result = (EffectiveNodeTypeImpl) ent; + } else { + result = result.merge((EffectiveNodeTypeImpl) ent); + // store intermediate result (sub-aggregate) + entCache.put(result); + } + } + break; + } + } + } + // also put the requested key, since the merge could have removed some + // the redundant nodetypes + if (!entCache.contains(requested)) { + entCache.put(requested, result); + } + // we're done + return result; + } + + //------------------------------------------------------------< private >--- + /** + * Notify the listeners that a node type ntName has been registered. + */ + private void notifyRegistered(Name ntName) { + for (NodeTypeRegistryListener ntrl : copyListeners()) { + if (ntrl != null) { + ntrl.nodeTypeRegistered(ntName); + } + } + } + + /** + * Notify the listeners that a node type ntName has been re-registered. + */ + private void notifyReRegistered(Name ntName) { + for (NodeTypeRegistryListener ntrl : copyListeners()) { + if (ntrl != null) { + ntrl.nodeTypeReRegistered(ntName); + } + } + } + + /** + * Notify the listeners that a node type ntName has been unregistered. + */ + private void notifyUnregistered(Name ntName) { + for (NodeTypeRegistryListener ntrl : copyListeners()) { + if (ntrl != null) { + ntrl.nodeTypeUnregistered(ntName); + } + } + } + + private NodeTypeRegistryListener[] copyListeners() { + // copy listeners to array to avoid ConcurrentModificationException + NodeTypeRegistryListener[] lstnrs = new NodeTypeRegistryListener[listeners.size()]; + int cnt = 0; + for (NodeTypeRegistryListener ntrl : listeners.values()) { + lstnrs[cnt++] = ntrl; + } + return lstnrs; + } + + private void internalRegister(Map defMap) { + for (Map.Entry entry : defMap.entrySet()) { + QNodeTypeDefinition ntd = entry.getKey(); + internalRegister(ntd, entry.getValue()); + } + } + + private void internalRegister(QNodeTypeDefinition ntd, EffectiveNodeType ent) { + // store new effective node type instance if present. otherwise it + // will be created on demand. + if (ent != null) { + entCache.put(ent); + } else { + log.debug("Effective node type for " + ntd + " not yet built."); + } + // register nt-definition + registeredNTDefs.put(ntd.getName(), ntd); + } + + private void internalUnregister(Name name) { + QNodeTypeDefinition ntd = registeredNTDefs.remove(name); + entCache.invalidate(name); + } + + private void internalUnregister(Collection ntNames) { + for (Name name : ntNames) { + internalUnregister(name); + } + } + + //-------------------------------------------------------------< Object >--- + + /** + * Returns the the state of this instance in a human readable format. + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("NodeTypeRegistry (").append(this).append(")\n"); + builder.append("Known NodeTypes:\n"); + builder.append(registeredNTDefs); + builder.append("\n"); + builder.append(entCache); + return builder.toString(); + } + + //--------------------------------------------------------< inner class >--- + /** + * Inner class representing the map of QNodeTypeDefinitions + * that have been loaded yet. + */ + private class NodeTypeDefinitionMap implements Map { + + // map of node type names and node type definitions + private Map nodetypeDefinitions = + new HashMap(); + + private Collection getValues() { + return nodetypeDefinitions.values(); + } + + private Set getKeySet() { + return nodetypeDefinitions.keySet(); + } + + /** + * Returns the names of those registered node types that have + * dependencies on the given node type.

        + * Note, that the returned Set may not be complete with respect + * to all node types registered within the repository. Instead it + * will only contain those node type definitions that are known so far. + * + * @param nodeTypeName node type name + * @return a set of node type Names + * @throws NoSuchNodeTypeException + */ + private Set getDependentNodeTypes(Name nodeTypeName) throws NoSuchNodeTypeException { + if (!nodetypeDefinitions.containsKey(nodeTypeName)) { + throw new NoSuchNodeTypeException(nodeTypeName.toString()); + } + // get names of those node types that have dependencies on the + // node type with the given nodeTypeName. + HashSet names = new HashSet(); + for (QNodeTypeDefinition ntd : getValues()) { + if (ntd.getDependencies().contains(nodeTypeName)) { + names.add(ntd.getName()); + } + } + return names; + } + + private void updateInternalMap(Iterator definitions) { + // since definition were retrieved from the storage, validation + // can be omitted -> register without building effective-nodetype. + // TODO: check if correct + while (definitions.hasNext()) { + internalRegister(definitions.next(), null); + } + } + + //------------------------------------------------------------< Map >--- + public int size() { + return nodetypeDefinitions.size(); + } + + public void clear() { + nodetypeDefinitions.clear(); + } + + public boolean isEmpty() { + return nodetypeDefinitions.isEmpty(); + } + + public boolean containsKey(Object key) { + if (!(key instanceof Name)) { + return false; + } + return get(key) != null; + } + + public boolean containsValue(Object value) { + if (!(value instanceof QNodeTypeDefinition)) { + return false; + } + return get(((QNodeTypeDefinition)value).getName()) != null; + } + + public Set keySet() { + // to be aware of all (recently) registered nodetypes retrieve + // complete set from the storage again and add missing / replace + // existing definitions. + try { + Iterator it = storage.getAllDefinitions(); + updateInternalMap(it); + } catch (RepositoryException e) { + log.error(e.getMessage()); + } + return getKeySet(); + } + + public Collection values() { + // make sure all node type definitions have been loaded. + keySet(); + // and retrieve the collection containing all definitions. + return getValues(); + } + + public QNodeTypeDefinition put(Name key, QNodeTypeDefinition value) { + return nodetypeDefinitions.put(key, value); + } + + public void putAll(Map t) { + throw new UnsupportedOperationException("Implementation missing"); + } + + public Set> entrySet() { + // make sure all node type definitions have been loaded. + keySet(); + return nodetypeDefinitions.entrySet(); + } + + public QNodeTypeDefinition get(Object key) { + if (!(key instanceof Name)) { + throw new IllegalArgumentException(); + } + QNodeTypeDefinition def = nodetypeDefinitions.get(key); + if (def == null) { + try { + // node type does either not exist or hasn't been loaded yet + Iterator it = storage.getDefinitions(new Name[] {(Name) key}); + updateInternalMap(it); + } catch (RepositoryException e) { + log.debug(e.getMessage()); + } + } + def = nodetypeDefinitions.get(key); + return def; + } + + public QNodeTypeDefinition remove(Object key) { + return nodetypeDefinitions.remove(key); + } + + //---------------------------------------------------------< Object >--- + + /** + * Returns the the state of this instance in a human readable format. + */ + public String toString() { + StringBuilder builder = new StringBuilder(); + for (QNodeTypeDefinition ntd : getValues()) { + builder.append(ntd.getName()); + Name[] supertypes = ntd.getSupertypes(); + builder.append("\n\tSupertypes"); + for (Name supertype : ntd.getSupertypes()) { + builder.append("\n\t\t").append(supertype); + } + builder.append("\n\tMixin\t").append(ntd.isMixin()); + builder.append("\n\tOrderableChildNodes\t").append(ntd.hasOrderableChildNodes()); + builder.append("\n\tPrimaryItemName\t").append(ntd.getPrimaryItemName() == null ? "" : ntd.getPrimaryItemName().toString()); + for (QPropertyDefinition pd : ntd.getPropertyDefs()) { + builder.append("\n\tPropertyDefinition"); + builder.append(" (declared in ").append(pd.getDeclaringNodeType()).append(") "); + builder.append("\n\t\tName\t\t").append(pd.definesResidual() ? "*" : pd.getName().toString()); + String type = "null"; + if (pd.getRequiredType() != 0) { + type = PropertyType.nameFromValue(pd.getRequiredType()); + } + builder.append("\n\t\tRequiredType\t").append(type); + builder.append("\n\t\tValueConstraints\t"); + QValueConstraint[] vca = pd.getValueConstraints(); + if (vca == null) { + builder.append(""); + } else { + for (int n = 0; n < vca.length; n++) { + if (n > 0) { + builder.append(", "); + } + builder.append(vca[n].getString()); + } + } + QValue[] defVals = pd.getDefaultValues(); + StringBuffer defaultValues = new StringBuffer(); + if (defVals == null) { + defaultValues.append(""); + } else { + for (QValue defVal : defVals) { + if (defaultValues.length() > 0) { + defaultValues.append(", "); + } + try { + defaultValues.append(defVal.getString()); + } catch (RepositoryException e) { + defaultValues.append(defVal.toString()); + } + } + } + builder.append("\n\t\tDefaultValue\t").append(defaultValues.toString()); + builder.append("\n\t\tAutoCreated\t").append(pd.isAutoCreated()); + builder.append("\n\t\tMandatory\t").append(pd.isMandatory()); + builder.append("\n\t\tOnVersion\t").append(OnParentVersionAction.nameFromValue(pd.getOnParentVersion())); + builder.append("\n\t\tProtected\t").append(pd.isProtected()); + builder.append("\n\t\tMultiple\t").append(pd.isMultiple()); + } + QNodeDefinition[] nd = ntd.getChildNodeDefs(); + for (QNodeDefinition aNd : nd) { + builder.append("\n\tNodeDefinition"); + builder.append(" (declared in ").append(aNd.getDeclaringNodeType()).append(") "); + builder.append("\n\t\tName\t\t").append(aNd.definesResidual() ? "*" : aNd.getName().toString()); + Name[] reqPrimaryTypes = aNd.getRequiredPrimaryTypes(); + if (reqPrimaryTypes != null && reqPrimaryTypes.length > 0) { + for (Name reqPrimaryType : reqPrimaryTypes) { + builder.append("\n\t\tRequiredPrimaryType\t").append(reqPrimaryType); + } + } + Name defPrimaryType = aNd.getDefaultPrimaryType(); + if (defPrimaryType != null) { + builder.append("\n\t\tDefaultPrimaryType\t").append(defPrimaryType); + } + builder.append("\n\t\tAutoCreated\t").append(aNd.isAutoCreated()); + builder.append("\n\t\tMandatory\t").append(aNd.isMandatory()); + builder.append("\n\t\tOnVersion\t").append(OnParentVersionAction.nameFromValue(aNd.getOnParentVersion())); + builder.append("\n\t\tProtected\t").append(aNd.isProtected()); + builder.append("\n\t\tAllowsSameNameSiblings\t").append(aNd.allowsSameNameSiblings()); + } + } + return builder.toString(); + } + + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistryListener.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistryListener.java new file mode 100644 index 00000000000..aeb158534fd --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeRegistryListener.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.jcr2spi.nodetype; + +import org.apache.jackrabbit.spi.Name; + +/** + * The NodeTypeRegistryListener interface allows an implementing + * object to be informed about node type (un)registration. + * + * @see NodeTypeRegistry#addListener(NodeTypeRegistryListener) + * @see NodeTypeRegistry#removeListener(NodeTypeRegistryListener) + */ +public interface NodeTypeRegistryListener { + + /** + * Called when a node type has been registered. + * + * @param ntName name of the node type that has been registered + */ + void nodeTypeRegistered(Name ntName); + + /** + * Called when a node type has been re-registered. + * + * @param ntName name of the node type that has been registered + */ + void nodeTypeReRegistered(Name ntName); + + /** + * Called when a node type has been deregistered. + * + * @param ntName name of the node type that has been unregistered + */ + void nodeTypeUnregistered(Name ntName); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/EventImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/EventImpl.java new file mode 100644 index 00000000000..7846c3bffc3 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/EventImpl.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.jcr2spi.observation; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.Event; + +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the {@link javax.jcr.observation.Event} interface. + */ +final class EventImpl implements Event { + + /** + * Logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(EventImpl.class); + + /** + * The session of the {@link javax.jcr.observation.EventListener} this + * event will be delivered to. + */ + private final NamePathResolver resolver; + + /** + * The IdFactory + */ + private final IdFactory idFactory; + + /** + * The underlying SPI event. + */ + private final org.apache.jackrabbit.spi.Event event; + + /** + * Cached String value of this Event instance. + */ + private String stringValue; + + /** + * Creates a new {@link javax.jcr.observation.Event} instance based on an + * {@link org.apache.jackrabbit.spi.Event SPI Event}. + * + * @param event the underlying SPI Event. + * @param resolver + * @param idFactory + */ + EventImpl(org.apache.jackrabbit.spi.Event event, + NamePathResolver resolver, IdFactory idFactory) { + this.event = event; + this.resolver = resolver; + this.idFactory = idFactory; + } + + //--------------------------------------------------------------< Event >--- + /** + * {@inheritDoc} + */ + public int getType() { + return event.getType(); + } + + /** + * {@inheritDoc} + */ + public String getPath() throws RepositoryException { + return event.getPath() != null ? resolver.getJCRPath(event.getPath()) : null; + } + + /** + * {@inheritDoc} + */ + public String getUserID() { + return event.getUserID(); + } + + /** + * @see javax.jcr.observation.Event#getIdentifier() + */ + public String getIdentifier() throws RepositoryException { + ItemId itemId = event.getItemId(); + if (itemId == null) { + return null; + } else { + NodeId nodeId = (itemId.denotesNode()) ? (NodeId) itemId : ((PropertyId) itemId).getParentId(); + return idFactory.toJcrIdentifier(nodeId); + } + } + + /** + * @see javax.jcr.observation.Event#getInfo() + */ + public Map getInfo() throws RepositoryException { + Map jcrInfo = new HashMap(); + for (Map.Entry entry : event.getInfo().entrySet()) { + Name key = entry.getKey(); + QValue value = entry.getValue(); + String strValue = null; + if (value != null) { + strValue = ValueFormat.getJCRString(value, resolver); + } + jcrInfo.put(resolver.getJCRName(key), strValue); + } + return jcrInfo; + } + + /** + * @see javax.jcr.observation.Event#getUserData() + */ + public String getUserData() throws RepositoryException { + return event.getUserData(); + } + + /** + * @see javax.jcr.observation.Event#getDate() + */ + public long getDate() throws RepositoryException { + return event.getDate(); + } + + //-------------------------------------------------------------< Object >--- + /** + * Returns a String representation of this Event. + * + * @return a String representation of this Event. + */ + @Override + public String toString() { + if (stringValue == null) { + StringBuffer sb = new StringBuffer(); + sb.append("Event: Path: "); + try { + sb.append(getPath()); + } catch (RepositoryException e) { + log.error("Exception retrieving path: " + e); + sb.append("[Error retrieving path]"); + } + sb.append(", ").append(valueOf(getType())).append(": "); + sb.append(", UserId: ").append(getUserID()); + stringValue = sb.toString(); + } + return stringValue; + } + + //----------------------------------< internal >---------------------------- + + /** + * Returns a String representation of eventType. + * + * @param eventType an event type defined by {@link Event}. + * @return a String representation of eventType. + */ + private static String valueOf(int eventType) { + if (eventType == Event.NODE_ADDED) { + return "NodeAdded"; + } else if (eventType == Event.NODE_REMOVED) { + return "NodeRemoved"; + } else if (eventType == Event.PROPERTY_ADDED) { + return "PropertyAdded"; + } else if (eventType == Event.PROPERTY_CHANGED) { + return "PropertyChanged"; + } else if (eventType == Event.PROPERTY_REMOVED) { + return "PropertyRemoved"; + } else if (eventType == Event.NODE_MOVED) { + return "NodeMoved"; + } else if (eventType == Event.PERSIST) { + return "Persist"; + } else { + return "UnknownEventType"; + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/EventJournalImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/EventJournalImpl.java new file mode 100644 index 00000000000..29caf1c8007 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/EventJournalImpl.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.observation; + +import java.util.List; +import java.util.LinkedList; +import java.util.NoSuchElementException; + +import javax.jcr.observation.EventJournal; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.jcr2spi.WorkspaceManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * EventJournalImpl implement the JSR 283 event journal over SPI. + */ +public class EventJournalImpl implements EventJournal { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(EventJournalImpl.class); + + private final WorkspaceManager wspMgr; + + private final EventFilter filter; + + private final NamePathResolver resolver; + + private List buffer = new LinkedList(); + + private long lastTimestamp = 0; + + /** + * Current position. + */ + private long position = 0; + + public EventJournalImpl(WorkspaceManager wspMgr, + EventFilter filter, + NamePathResolver resolver) { + this.wspMgr = wspMgr; + this.filter = filter; + this.resolver = resolver; + } + + /** + * {@inheritDoc} + */ + public void skipTo(long date) { + // first try to skip in buffer + while (!buffer.isEmpty()) { + long eDate; + try { + eDate = buffer.get(0).getDate(); + } catch (RepositoryException e) { + eDate = 0; + } + if (eDate <= date) { + buffer.remove(0); + } else { + return; + } + } + // if we get here then we need to refill the buffer after + // the given date + lastTimestamp = date; + refill(); + } + + /** + * {@inheritDoc} + */ + public javax.jcr.observation.Event nextEvent() { + return (javax.jcr.observation.Event) next(); + } + + /** + * {@inheritDoc} + */ + public void skip(long skipNum) { + while (skipNum-- > 0) { + next(); + } + } + + /** + * {@inheritDoc} + */ + public long getSize() { + return -1; + } + + /** + * {@inheritDoc} + */ + public long getPosition() { + return position; + } + + /** + * {@inheritDoc} + */ + public boolean hasNext() { + if (buffer.isEmpty()) { + refill(); + } + return !buffer.isEmpty(); + } + + /** + * {@inheritDoc} + */ + public Object next() { + if (hasNext()) { + position++; + return new EventImpl(buffer.remove(0), + resolver, wspMgr.getIdFactory()); + } else { + throw new NoSuchElementException(); + } + } + + /** + * {@inheritDoc} + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + //----------------------------< internal >---------------------------------- + + private void refill() { + try { + EventBundle bundle = wspMgr.getEvents(filter, lastTimestamp); + for (Event e : bundle) { + buffer.add(e); + lastTimestamp = e.getDate(); + } + } catch (RepositoryException e) { + log.warn("Exception while refilling event journal buffer", e); + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/FilteredEventIterator.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/FilteredEventIterator.java new file mode 100644 index 00000000000..6035d40ee9c --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/FilteredEventIterator.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.jcr2spi.observation; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; + +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * Implements an event iterator that converts SPI events into JCR events and + * filters out the ones that are not accepted by an {@link EventFilter}. + */ +class FilteredEventIterator implements EventIterator { + + /** + * The actual {@link org.apache.jackrabbit.spi.Event}s fired by the repository service + * (unfiltered). + */ + protected final Iterator actualEvents; + + /** + * For filtering the {@link javax.jcr.observation.Event}s. + */ + private final EventFilter filter; + + /** + * If true these events are local. + */ + private final boolean isLocal; + + /** + * The namespace resolver of the session that created this event iterator. + */ + private final NamePathResolver resolver; + + /** + * The IdFactory + */ + private final IdFactory idFactory; + + /** + * The next {@link javax.jcr.observation.Event} in this iterator + */ + private Event next; + + /** + * Current position + */ + private long pos = 0; + + /** + * Creates a new FilteredEventIterator. + * + * @param events the {@link org.apache.jackrabbit.spi.Event}s as an + * iterator. + * @param isLocal whether the events were caused by the local session. + * @param filter only event that pass the filter will be dispatched to + * the event listener. + * @param resolver the name path resolver. + * @param idFactory the id factory. + */ + public FilteredEventIterator(Iterator events, + boolean isLocal, + EventFilter filter, + NamePathResolver resolver, + IdFactory idFactory) { + this.actualEvents = events; + this.filter = filter; + this.isLocal = isLocal; + this.resolver = resolver; + this.idFactory = idFactory; + fetchNext(); + } + + /** + * {@inheritDoc} + */ + public Object next() { + if (next == null) { + throw new NoSuchElementException(); + } + Event e = next; + fetchNext(); + pos++; + return e; + } + + /** + * {@inheritDoc} + */ + public Event nextEvent() { + return (Event) next(); + } + + /** + * {@inheritDoc} + */ + public void skip(long skipNum) { + while (skipNum-- > 0) { + next(); + } + } + + /** + * Always returns -1. + * + * @return -1. + */ + public long getSize() { + return -1; + } + + /** + * {@inheritDoc} + */ + public long getPosition() { + return pos; + } + + /** + * This method is not supported. + * Always throws a UnsupportedOperationException. + */ + public void remove() { + throw new UnsupportedOperationException("EventIterator.remove()"); + } + + /** + * Returns true if the iteration has more elements. (In other + * words, returns true if next would return an element + * rather than throwing an exception.) + * + * @return true if the iterator has more elements. + */ + public boolean hasNext() { + return (next != null); + } + + /** + * Fetches the next Event from the collection of events + * passed in the constructor of FilteredEventIterator + * that is allowed by the {@link EventFilter}. + */ + private void fetchNext() { + org.apache.jackrabbit.spi.Event event; + next = null; + while (next == null && actualEvents.hasNext()) { + event = actualEvents.next(); + next = filter.accept(event, isLocal) ? new EventImpl(event, resolver, idFactory) : null; + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/InternalEventListener.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/InternalEventListener.java new file mode 100644 index 00000000000..38dcca211e6 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/InternalEventListener.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.jcr2spi.observation; + +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; + +import java.util.Collection; + +/** + * InternalEventListener receives changes as a result of a local + * or an external modification. + */ +public interface InternalEventListener { + + /** + * Gets called when an event occurs. + * + * @param eventBundle the event set received. + */ + public void onEvent(EventBundle eventBundle); + + /** + * Returns a collection of event filters which is in use by this event + * listener. The event bundles delivered to {@link #onEvent} will be filtered + * using the collection returned by this method. An event is included + * in an event bundles if it is accepted by at least one of the filters + * returned by this method. + * + * @return an unmodifiable collection of {@link EventFilter}s currently + * in use by this event listener. + */ + public Collection getEventFilters(); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/ObservationManagerImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/ObservationManagerImpl.java new file mode 100644 index 00000000000..d854c3d0435 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/observation/ObservationManagerImpl.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.jcr2spi.observation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.observation.EventJournal; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.EventListenerIterator; +import javax.jcr.observation.ObservationManager; + +import org.apache.jackrabbit.commons.iterator.EventListenerIteratorAdapter; +import org.apache.jackrabbit.jcr2spi.WorkspaceManager; +import org.apache.jackrabbit.jcr2spi.nodetype.NodeTypeRegistry; +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ObservationManagerImpl... + */ +public class ObservationManagerImpl implements ObservationManager, InternalEventListener { + + /** + * The logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(ObservationManagerImpl.class); + + /** + * The workspace manager. + */ + private final WorkspaceManager wspManager; + + /** + * The name and path resolver associated with the session this observation + * manager belongs to. + */ + private final NamePathResolver resolver; + + /** + * The NodeTypeRegistry of the session. + */ + private final NodeTypeRegistry ntRegistry; + + /** + * Live mapping of EventListener to EventFilter. + */ + private final Map subscriptions = new HashMap(); + + /** + * A read only mapping of EventListener to EventFilter. + */ + private Map readOnlySubscriptions; + + /** + * Creates a new observation manager for session. + * + * @param wspManager the WorkspaceManager. + * @param resolver the name path resolver for this session. + * @param ntRegistry The NodeTypeRegistry of the session. + */ + public ObservationManagerImpl(WorkspaceManager wspManager, + NamePathResolver resolver, + NodeTypeRegistry ntRegistry) { + this.wspManager = wspManager; + this.resolver = resolver; + this.ntRegistry = ntRegistry; + } + + public void addEventListener(EventListener listener, + int eventTypes, + String absPath, + boolean isDeep, + String[] uuids, + String[] nodeTypeNames, + boolean noLocal) throws RepositoryException { + EventFilter filter = createEventFilter(eventTypes, absPath, + isDeep, uuids, nodeTypeNames, noLocal); + synchronized (subscriptions) { + subscriptions.put(listener, filter); + readOnlySubscriptions = null; + } + + if (subscriptions.size() == 1) { + wspManager.addEventListener(this); + } else { + wspManager.updateEventFilters(); + } + } + + public void removeEventListener(EventListener listener) throws RepositoryException { + synchronized (subscriptions) { + if (subscriptions.remove(listener) != null) { + readOnlySubscriptions = null; + } + } + if (subscriptions.size() == 0) { + wspManager.removeEventListener(this); + } else { + wspManager.updateEventFilters(); + } + } + + public EventListenerIterator getRegisteredEventListeners() throws RepositoryException { + Map activeListeners; + synchronized (subscriptions) { + ensureReadOnlyMap(); + activeListeners = readOnlySubscriptions; + } + return new EventListenerIteratorAdapter(activeListeners.keySet()); + } + + /** + * @see javax.jcr.observation.ObservationManager#getEventJournal() + */ + public EventJournal getEventJournal() throws RepositoryException { + return getEventJournal(Event.ALL_TYPES, "/", true, null, null); + } + + /** + * @see javax.jcr.observation.ObservationManager#getEventJournal(int, String, boolean, String[], String[]) + */ + public EventJournal getEventJournal( + int eventTypes, String absPath, boolean isDeep, + String[] uuid, String[] nodeTypeName) + throws RepositoryException { + EventFilter filter = createEventFilter(eventTypes, absPath, isDeep, uuid, nodeTypeName, false); + return new EventJournalImpl(wspManager, filter, resolver); + } + + /** + * @see javax.jcr.observation.ObservationManager#setUserData(String) + */ + public void setUserData(String userData) throws RepositoryException { + wspManager.setUserData(userData); + } + + //-----------------------< InternalEventListener >-------------------------- + + public Collection getEventFilters() { + List filters = new ArrayList(); + synchronized (subscriptions) { + ensureReadOnlyMap(); + filters.addAll(readOnlySubscriptions.values()); + } + return filters; + } + + public void onEvent(EventBundle eventBundle) { + // get active listeners + Map activeListeners; + synchronized (subscriptions) { + ensureReadOnlyMap(); + activeListeners = readOnlySubscriptions; + } + for (Map.Entry entry : activeListeners.entrySet()) { + EventListener listener = entry.getKey(); + EventFilter filter = entry.getValue(); + FilteredEventIterator eventIter = new FilteredEventIterator( + eventBundle.getEvents(), eventBundle.isLocal(), filter, + resolver, wspManager.getIdFactory()); + if (eventIter.hasNext()) { + try { + listener.onEvent(eventIter); + } catch (Throwable t) { + log.warn("EventConsumer threw exception: " + t.toString()); + log.debug("Stacktrace: ", t); + // move on to the next listener + } + } + } + } + + //-------------------------< internal >------------------------------------- + + /** + * Ensures that {@link #readOnlySubscriptions} is set. Callers of this + * method must own {@link #subscriptions} as a monitor to avoid concurrent + * access to {@link #subscriptions}. + */ + private void ensureReadOnlyMap() { + if (readOnlySubscriptions == null) { + readOnlySubscriptions = new HashMap(subscriptions); + } + } + + /** + * Creates an SPI event filter from the given list of constraints. + * + * @param eventTypes the event types. + * @param absPath an absolute path. + * @param isDeep whether to include events for descendant items of + * the node at absPath. + * @param uuids uuid filters. + * @param nodeTypeNames node type filters. + * @param noLocal whether to exclude changes from the local session. + * @return the SPI event filter instance. + * @throws RepositoryException if an error occurs while creating the event + * filter. + */ + private EventFilter createEventFilter(int eventTypes, + String absPath, + boolean isDeep, + String[] uuids, + String[] nodeTypeNames, + boolean noLocal) + throws RepositoryException { + Path path; + try { + path = resolver.getQPath(absPath).getCanonicalPath(); + } catch (NameException e) { + throw new RepositoryException("Malformed path: " + absPath); + } + + // create NodeType instances from names + Name[] qNodeTypeNames; + if (nodeTypeNames == null) { + qNodeTypeNames = null; + } else { + try { + qNodeTypeNames = new Name[nodeTypeNames.length]; + for (int i = 0; i < nodeTypeNames.length; i++) { + Name ntName = resolver.getQName(nodeTypeNames[i]); + if (!ntRegistry.isRegistered(ntName)) { + throw new RepositoryException("unknown node type: " + nodeTypeNames[i]); + } + qNodeTypeNames[i] = ntName; + } + } catch (NameException e) { + throw new RepositoryException(e.getMessage()); + } + } + + return wspManager.createEventFilter(eventTypes, path, isDeep, + uuids, qNodeTypeNames, noLocal); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractCopy.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractCopy.java new file mode 100644 index 00000000000..d449b2d170a --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractCopy.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; + +/** + * AbstractCopy... + */ +public abstract class AbstractCopy extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(AbstractCopy.class); + + final NodeState destParentState; + private final NodeState srcState; + private final Name destName; + private final String srcWorkspaceName; + + /** + * + * @param srcPath + * @param destPath + * @param srcMgrProvider + */ + AbstractCopy(Path srcPath, Path destPath, String srcWorkspaceName, + ManagerProvider srcMgrProvider, ManagerProvider destMgrProvider) + throws RepositoryException { + + NodeState srcItemState = getNodeState(srcPath, srcMgrProvider.getHierarchyManager()); + this.srcState = srcItemState; + this.destParentState = getNodeState(destPath.getAncestor(1), destMgrProvider.getHierarchyManager()); + + // check for illegal index present in destination path + int index = destPath.getIndex(); + if (index != Path.INDEX_UNDEFINED) { + // subscript in name element + String msg = "invalid destination path (subscript in name element is not allowed)"; + log.debug(msg); + throw new RepositoryException(msg); + } + this.destName = destPath.getName(); + this.srcWorkspaceName = srcWorkspaceName; + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * Invalidate the destination parent NodeState. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + destParentState.getHierarchyEntry().invalidate(false); + } + + //----------------------------------------< Access Operation Parameters >--- + public String getWorkspaceName() { + return srcWorkspaceName; + } + + public NodeId getNodeId() throws RepositoryException { + return srcState.getNodeId(); + } + + public NodeId getDestinationParentId() throws RepositoryException { + return destParentState.getNodeId(); + } + + public Name getDestinationName() { + return destName; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractOperation.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractOperation.java new file mode 100644 index 00000000000..7e035f7cc11 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractOperation.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.jcr2spi.operation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Path; + +/** + * AbstractOperation... + */ +public abstract class AbstractOperation implements Operation { + + /** + * The collection of affected ItemStates. + */ + private final Collection affectedStates = new ArrayList(); + protected int status; + + /** + * Returns the name of the class + * + * @return the class name + * @see #getClass() + */ + public String getName() { + return getClass().getName(); + } + + public Collection getAffectedItemStates() { + if (affectedStates.isEmpty()) { + return Collections.emptySet(); + } + else { + return Collections.unmodifiableCollection(affectedStates); + } + } + + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + throw new UnsupportedOperationException("Undo not supported."); + } + + public int getStatus() { + return status; + } + + /** + * Adds an affected ItemState. + * + * @param affectedState the ItemStates of the affected item. + */ + protected void addAffectedItemState(ItemState affectedState) { + affectedStates.add(affectedState); + } + + /** + * + * @param nodePath + * @param hierMgr + * @return + * @throws PathNotFoundException + * @throws RepositoryException + */ + protected static NodeState getNodeState(Path nodePath, HierarchyManager hierMgr) throws PathNotFoundException, RepositoryException { + NodeState nodeState = hierMgr.getNodeState(nodePath); + return nodeState; + } + + /** + * Asserts that the NodeEntry of the given parent state has it's child node + * entries loaded. + * + * @param parentState + * @throws RepositoryException + */ + protected static void assertChildNodeEntries(NodeState parentState) throws RepositoryException { + parentState.getNodeEntry().getNodeEntries(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractRemove.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractRemove.java new file mode 100644 index 00000000000..40182a71eac --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AbstractRemove.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.ItemId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionException; + +/** + * AbstractRemove is the base class for non-transient remove + * operations executed on the workspace such as removing versions or activities. + */ +public abstract class AbstractRemove extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(Remove.class); + + protected ItemState removeState; + protected NodeState parent; + + protected AbstractRemove(ItemState removeState, NodeState parent) throws RepositoryException { + this.removeState = removeState; + this.parent = parent; + + addAffectedItemState(removeState); + addAffectedItemState(parent); + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_UNDO; + parent.getHierarchyEntry().complete(this); + } + + //----------------------------------------< Access Operation Parameters >--- + public ItemId getRemoveId() throws RepositoryException { + return removeState.getWorkspaceId(); + } + + public NodeState getParentState() { + return parent; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddLabel.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddLabel.java new file mode 100644 index 00000000000..8e74388c455 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddLabel.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +/** + * AddLabel... + */ +public class AddLabel extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(AddLabel.class); + + private final NodeState versionHistoryState; + private final NodeState versionState; + private final Name label; + private final boolean moveLabel; + + private AddLabel(NodeState versionHistoryState, NodeState versionState, Name label, boolean moveLabel) { + this.versionHistoryState = versionHistoryState; + this.versionState = versionState; + this.label = label; + this.moveLabel = moveLabel; + + // NOTE: affected-states only needed for transient modifications + } + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + * @throws RepositoryException + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws ItemExistsException + * @throws NoSuchNodeTypeException + * @throws UnsupportedRepositoryOperationException + * @throws VersionException + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the jcr:versionLabel nodestate present with the given + * version history. If 'moveLabel' is true, all descendant states + * (property states) are invalidated as well. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + try { + NodeEntry vhEntry = (NodeEntry) versionHistoryState.getHierarchyEntry(); + NodeEntry lnEntry = vhEntry.getNodeEntry(NameConstants.JCR_VERSIONLABELS, Path.INDEX_DEFAULT); + if (lnEntry != null) { + lnEntry.invalidate(moveLabel); + } + } catch (RepositoryException e) { + log.debug(e.getMessage()); + } + } + //----------------------------------------< Access Operation Parameters >--- + public NodeId getVersionHistoryId() throws RepositoryException { + return versionHistoryState.getNodeEntry().getWorkspaceId(); + } + + public NodeId getVersionId() throws RepositoryException { + return versionState.getNodeEntry().getWorkspaceId(); + } + + public Name getLabel() { + return label; + } + + public boolean moveLabel() { + return moveLabel; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param versionHistoryState + * @param versionState + * @param label + * @param moveLabel + * @return + */ + public static Operation create(NodeState versionHistoryState, NodeState versionState, Name label, boolean moveLabel) { + return new AddLabel(versionHistoryState, versionState, label, moveLabel); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddNode.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddNode.java new file mode 100644 index 00000000000..91c25b639e1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddNode.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.jcr2spi.operation; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AddNode... + */ +public class AddNode extends TransientOperation { + + private static Logger log = LoggerFactory.getLogger(AddNode.class); + + private final NodeId parentId; + private final NodeState parentState; + private final Name nodeName; + private final Name nodeTypeName; + private final String uuid; + + private final List addedStates = new ArrayList(); + + private AddNode(NodeState parentState, Name nodeName, Name nodeTypeName, String uuid) + throws RepositoryException { + this(parentState, nodeName, nodeTypeName, uuid, DEFAULT_OPTIONS); + } + + AddNode(NodeState parentState, Name nodeName, Name nodeTypeName, + String uuid, int options) throws RepositoryException { + super(options); + this.parentId = parentState.getNodeId(); + this.parentState = parentState; + this.nodeName = nodeName; + this.nodeTypeName = nodeTypeName; + this.uuid = uuid; + + addAffectedItemState(parentState); + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + */ + public void accept(OperationVisitor visitor) throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Throws UnsupportedOperationException + * + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + parentState.getHierarchyEntry().complete(this); + } + + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_UNDO; + parentState.getHierarchyEntry().complete(this); + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getParentId() { + return parentId; + } + + public NodeState getParentState() { + return parentState; + } + + public Name getNodeName() { + return nodeName; + } + + public Name getNodeTypeName() { + return nodeTypeName; + } + + public String getUuid() { + return uuid; + } + + public void addedState(List newStates) { + addedStates.addAll(newStates); + } + + public List getAddedStates() { + return addedStates; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param parentState + * @param nodeName + * @param nodeTypeName + * @param uuid + * @return a new AddNode operation. + */ + public static Operation create(NodeState parentState, Name nodeName, + Name nodeTypeName, String uuid) throws RepositoryException { + // make sure the parent hierarchy entry has its child entries loaded + // in order to be able to detect conflicts. + assertChildNodeEntries(parentState); + + AddNode an = new AddNode(parentState, nodeName, nodeTypeName, uuid); + return an; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddProperty.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddProperty.java new file mode 100644 index 00000000000..c78d797b968 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/AddProperty.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.jcr2spi.operation; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; + +/** + * AddProperty... + */ +public class AddProperty extends TransientOperation { + + private final NodeId parentId; + private final NodeState parentState; + private final Name propertyName; + private final int propertyType; + private final QValue[] values; + + private final QPropertyDefinition definition; + + private AddProperty(NodeState parentState, Name propName, + int propertyType, QValue[] values, + QPropertyDefinition definition) throws RepositoryException { + this(parentState, propName, propertyType, values, definition, DEFAULT_OPTIONS); + } + + AddProperty(NodeState parentState, Name propName, + int propertyType, QValue[] values, + QPropertyDefinition definition, int options) throws RepositoryException { + super(options); + this.parentId = parentState.getNodeId(); + this.parentState = parentState; + this.propertyName = propName; + this.propertyType = propertyType; + this.values = values; + this.definition = definition; + + addAffectedItemState(parentState); + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + */ + public void accept(OperationVisitor visitor) throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + parentState.getHierarchyEntry().complete(this); + } + + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + status = STATUS_UNDO; + parentState.getHierarchyEntry().complete(this); + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getParentId() { + return parentId; + } + + public NodeState getParentState() { + return parentState; + } + + public Name getPropertyName() { + return propertyName; + } + + public int getPropertyType() { + return propertyType; + } + + public QValue[] getValues() { + return values; + } + + public boolean isMultiValued() { + return definition.isMultiple(); + } + + public QPropertyDefinition getDefinition() { + return definition; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param parentState + * @param propName + * @param propertyType + * @param def + * @param values + * @return + */ + public static Operation create(NodeState parentState, Name propName, int propertyType, + QPropertyDefinition def, QValue[] values) throws RepositoryException { + AddProperty ap = new AddProperty(parentState, propName, propertyType, values, def); + return ap; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkin.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkin.java new file mode 100644 index 00000000000..1ce38085e44 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkin.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.spi.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; +import java.util.Iterator; + +/** + * Checkin... + */ +public class Checkin extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(Checkin.class); + + private final NodeState nodeState; + private final VersionManager mgr; + + private NodeId newVersionId; + + private Checkin(NodeState nodeState, VersionManager mgr) { + this.nodeState = nodeState; + this.mgr = mgr; + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidate the target NodeState. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + try { + NodeEntry vhe = mgr.getVersionHistoryEntry(nodeState); + if (vhe != null) { + vhe.invalidate(true); + } + } catch (RepositoryException e) { + log.debug("Failed to access Version history entry -> skip invalidation.", e); + } + Iterator entries = ((NodeEntry) nodeState.getHierarchyEntry()).getPropertyEntries(); + while (entries.hasNext()) { + PropertyEntry pe = entries.next(); + pe.invalidate(false); + } + nodeState.getHierarchyEntry().invalidate(false); + } + //----------------------------------------< Access Operation Parameters >--- + + /** + * + * @return The NodeId of the nodeState to be checked in. + */ + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeEntry().getWorkspaceId(); + } + + public void setNewVersionId(NodeId newVersionId) { + this.newVersionId = newVersionId; + } + + public NodeId getNewVersionId() { + return this.newVersionId; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @param mgr + * @return + */ + public static Checkin create(NodeState nodeState, VersionManager mgr) { + return new Checkin(nodeState, mgr); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkout.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkout.java new file mode 100644 index 00000000000..adbd0cab1cf --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkout.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.spi.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; +import java.util.Iterator; + +/** + * Checkout... + */ +public class Checkout extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(Checkout.class); + + private final NodeState nodeState; + private final NodeId activityId; + private final boolean supportsActivity; + private final VersionManager mgr; + + private Checkout(NodeState nodeState, VersionManager mgr) { + this.nodeState = nodeState; + this.mgr = mgr; + supportsActivity = false; + activityId = null; + // NOTE: affected-states only needed for transient modifications + } + + private Checkout(NodeState nodeState, NodeId activityId, VersionManager mgr) { + this.nodeState = nodeState; + this.activityId = activityId; + this.mgr = mgr; + supportsActivity = true; + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidate the target NodeState. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + try { + NodeEntry vhe = mgr.getVersionHistoryEntry(nodeState); + if (vhe != null) { + vhe.invalidate(true); + } + } catch (RepositoryException e) { + log.warn("Failed to access Version history entry -> skip invalidation.", e); + } + // non-recursive invalidation (but including all properties) + NodeEntry nodeEntry = (NodeEntry) nodeState.getHierarchyEntry(); + Iterator entries = nodeEntry.getPropertyEntries(); + while (entries.hasNext()) { + PropertyEntry pe = entries.next(); + pe.invalidate(false); + } + nodeEntry.invalidate(false); + } + + //----------------------------------------< Access Operation Parameters >--- + /** + * + * @return + */ + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeEntry().getWorkspaceId(); + } + + /** + * The id of the current activity present on the editing session or null. + * + * @return id of the current activity present on the editing session or null. + */ + public NodeId getActivityId() { + return activityId; + } + + /** + * Returns true, if activities are supported, + * false otherwise. + * + * @return true, if activities are supported, + * false otherwise. + */ + public boolean supportsActivity() { + return supportsActivity; + } + + //------------------------------------------------------------< Factory >--- + public static Operation create(NodeState nodeState, VersionManager mgr) { + return new Checkout(nodeState, mgr); + } + + public static Operation create(NodeState nodeState, NodeId activityId, VersionManager mgr) { + return new Checkout(nodeState, activityId, mgr); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkpoint.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkpoint.java new file mode 100644 index 00000000000..b30ea1326e3 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Checkpoint.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.spi.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; +import java.util.Iterator; + +/** + * Checkout... + */ +public class Checkpoint extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(Checkpoint.class); + + private final NodeState nodeState; + private final VersionManager mgr; + + private final NodeId activityId; + private final boolean supportsActivity; + + private NodeId newVersionId; + + private Checkpoint(NodeState nodeState, VersionManager mgr) { + this.nodeState = nodeState; + this.mgr = mgr; + // NOTE: affected-states only needed for transient modifications + supportsActivity = false; + activityId = null; + } + + private Checkpoint(NodeState nodeState, NodeId activityId, VersionManager mgr) { + this.nodeState = nodeState; + this.activityId = activityId; + this.mgr = mgr; + supportsActivity = true; + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidate the target NodeState. + * + * @see org.apache.jackrabbit.jcr2spi.operation.Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + try { + NodeEntry vhe = mgr.getVersionHistoryEntry(nodeState); + if (vhe != null) { + vhe.invalidate(true); + } + } catch (RepositoryException e) { + log.warn("Failed to access Version history entry -> skip invalidation.", e); + } + // non-recursive invalidation (but including all properties) + NodeEntry nodeEntry = (NodeEntry) nodeState.getHierarchyEntry(); + Iterator entries = nodeEntry.getPropertyEntries(); + while (entries.hasNext()) { + PropertyEntry pe = entries.next(); + pe.invalidate(false); + } + nodeEntry.invalidate(false); + } + + //----------------------------------------< Access Operation Parameters >--- + /** + * + * @return + */ + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeEntry().getWorkspaceId(); + } + + /** + * The id of the current activity present on the editing session or null. + * + * @return id of the current activity present on the editing session or null. + */ + public NodeId getActivityId() { + return activityId; + } + + /** + * Returns true, if activities are supported, + * false otherwise. + * + * @return true, if activities are supported, + * false otherwise. + */ + public boolean supportsActivity() { + return supportsActivity; + } + + public void setNewVersionId(NodeId newVersionId) { + this.newVersionId = newVersionId; + } + + public NodeId getNewVersionId() { + return this.newVersionId; + } + + //------------------------------------------------------------< Factory >--- + public static Checkpoint create(NodeState nodeState, VersionManager mgr) { + return new Checkpoint(nodeState, mgr); + } + + public static Checkpoint create(NodeState nodeState, NodeId activityId, VersionManager mgr) { + return new Checkpoint(nodeState, activityId, mgr); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Clone.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Clone.java new file mode 100644 index 00000000000..24b03bd33be --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Clone.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.jcr2spi.operation; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.spi.Path; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Clone... + */ +public class Clone extends AbstractCopy { + + private static Logger log = LoggerFactory.getLogger(Clone.class); + + private final boolean removeExisting; + + private Clone(Path srcPath, Path destPath, String srcWorkspaceName, + boolean removeExisting, ManagerProvider srcMgrProvider, + ManagerProvider destMgrProvider) + throws RepositoryException { + super(srcPath, destPath, srcWorkspaceName, srcMgrProvider, destMgrProvider); + + this.removeExisting = removeExisting; + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + */ + public void accept(OperationVisitor visitor) throws NoSuchWorkspaceException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * @see Operation#persisted() + */ + @Override + public void persisted() { + assert status == STATUS_PENDING; + if (removeExisting) { + status = STATUS_PERSISTED; + // invalidate the complete tree -> find root-hierarchy-entry + HierarchyEntry he = destParentState.getHierarchyEntry(); + while (he.getParent() != null) { + he = he.getParent(); + } + he.invalidate(true); + } else { + super.persisted(); + } + } + + //----------------------------------------< Access Operation Parameters >--- + public boolean isRemoveExisting() { + return removeExisting; + } + + //------------------------------------------------------------< Factory >--- + + public static Operation create(Path srcPath, Path destPath, + String srcWorkspaceName, boolean removeExisting, + ManagerProvider srcMgrProvider, + ManagerProvider destMgrProvider) + throws RepositoryException, ConstraintViolationException, AccessDeniedException, + ItemExistsException, VersionException { + + Clone cl = new Clone(srcPath, destPath, srcWorkspaceName, removeExisting, srcMgrProvider, destMgrProvider); + return cl; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Copy.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Copy.java new file mode 100644 index 00000000000..2d4652df8d4 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Copy.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.RepositoryException; +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.lock.LockException; +import javax.jcr.version.VersionException; + +/** + * Copy... + */ +public class Copy extends AbstractCopy { + + private Copy(Path srcPath, Path destPath, String srcWorkspaceName, + ManagerProvider srcMgrProvider, ManagerProvider destMgrProvider) throws RepositoryException { + super(srcPath, destPath, srcWorkspaceName, srcMgrProvider, destMgrProvider); + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + */ + public void accept(OperationVisitor visitor) throws NoSuchWorkspaceException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + //------------------------------------------------------------< Factory >--- + public static Operation create(Path srcPath, Path destPath, + String srcWorkspaceName, + ManagerProvider srcMgrProvider, + ManagerProvider destMgrProvider) + throws RepositoryException, ConstraintViolationException, AccessDeniedException, + ItemExistsException, VersionException { + Copy cp = new Copy(srcPath, destPath, srcWorkspaceName, srcMgrProvider, destMgrProvider); + return cp; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/CreateActivity.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/CreateActivity.java new file mode 100644 index 00000000000..e9fca1f22cf --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/CreateActivity.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.spi.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +/** + * Checkout... + */ +public class CreateActivity extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(CreateActivity.class); + + private final String title; + private final VersionManager mgr; + + private NodeId newActivityId; + + private CreateActivity(String title, VersionManager mgr) { + this.title = title; + this.mgr = mgr; + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidate the target NodeState. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + + // TODO: check if invalidation of the activity store is required. + } + + //----------------------------------------< Access Operation Parameters >--- + /** + * + * @return + */ + public String getTitle() throws RepositoryException { + return title; + } + + public void setNewActivityId(NodeId newActivityId) { + this.newActivityId = newActivityId; + } + + public NodeId getNewActivityId() { + return this.newActivityId; + } + + //------------------------------------------------------------< Factory >--- + public static CreateActivity create(String title, VersionManager mgr) { + return new CreateActivity(title, mgr); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/CreateConfiguration.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/CreateConfiguration.java new file mode 100644 index 00000000000..3079096146d --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/CreateConfiguration.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +/** + * CreateConfiguration... + */ +public class CreateConfiguration extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(CreateConfiguration.class); + + private final NodeState nodeState; + + private final VersionManager mgr; + + private NodeId newConfigurationId; + + private CreateConfiguration(NodeState nodeState, VersionManager mgr) { + this.nodeState = nodeState; + this.mgr = mgr; + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidate the target NodeState. + * + * @see org.apache.jackrabbit.jcr2spi.operation.Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + + // TODO: check if the configuration store needs to be invalidated. + + nodeState.getHierarchyEntry().invalidate(false); + } + + //----------------------------------------< Access Operation Parameters >--- + /** + * + * @return + * @throws RepositoryException + */ + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeEntry().getWorkspaceId(); + } + + public void setNewConfigurationId(NodeId newConfigurationId) { + this.newConfigurationId = newConfigurationId; + } + + public NodeId getNewConfigurationId() { + return newConfigurationId; + } + + //------------------------------------------------------------< Factory >--- + public static CreateConfiguration create(NodeState nodeState, VersionManager mgr) { + return new CreateConfiguration(nodeState, mgr); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/IgnoreOperation.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/IgnoreOperation.java new file mode 100644 index 00000000000..2f71140d231 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/IgnoreOperation.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.jcr2spi.operation; + +/** + * Marker interface that represent operations which must be ignored + * by the SessionItemStateManager for building the final ChangeLog. Instances + * of IgnoreOperation never appear in the ChangeLog. + */ +public interface IgnoreOperation { +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockOperation.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockOperation.java new file mode 100644 index 00000000000..26aa19026f4 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockOperation.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.NodeId; + +import javax.jcr.RepositoryException; +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +/** + * LockOperation... + */ +public class LockOperation extends AbstractOperation { + + private final NodeState nodeState; + private final boolean isDeep; + private final boolean isSessionScoped; + private final long timeoutHint; + private final String ownerHint; + + private LockInfo lockInfo = null; + + private LockOperation(NodeState nodeState, boolean isDeep, boolean isSessionScoped, + long timeoutHint, String ownerHint) { + this.nodeState = nodeState; + this.isDeep = isDeep; + this.isSessionScoped = isSessionScoped; + this.timeoutHint = timeoutHint; + this.ownerHint = ownerHint; + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the NodeState that has been locked. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + // non-recursive invalidation + nodeState.getHierarchyEntry().invalidate(false); + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeId(); + } + + public boolean isDeep() { + return isDeep; + } + + public boolean isSessionScoped() { + return isSessionScoped; + } + + public long getTimeoutHint() { + return timeoutHint; + } + + public String getOwnerHint() { + return ownerHint; + } + + public void setLockInfo(LockInfo lockInfo) { + if (lockInfo == null) { + throw new IllegalArgumentException("IdIterator must not be null."); + } + if (this.lockInfo != null) { + throw new IllegalStateException("Merge operation has already been executed -> FailedIds already set."); + } + this.lockInfo = lockInfo; + } + + public LockInfo getLockInfo() { + if (lockInfo == null) { + throw new IllegalStateException("Merge operation has not been executed yet."); + } + return lockInfo; + } + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @param isDeep + * @return + */ + public static LockOperation create(NodeState nodeState, boolean isDeep, boolean isSessionScoped) { + return create(nodeState, isDeep, isSessionScoped, Long.MAX_VALUE, null); + } + + public static LockOperation create(NodeState nodeState, boolean isDeep, boolean isSessionScoped, long timeoutHint, String ownerHint) { + LockOperation lck = new LockOperation(nodeState, isDeep, isSessionScoped, timeoutHint, ownerHint); + return lck; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockRefresh.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockRefresh.java new file mode 100644 index 00000000000..cdd9e4a3143 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockRefresh.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.jcr2spi.operation; + +import javax.jcr.RepositoryException; +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.UnsupportedRepositoryOperationException; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.NodeId; + +import javax.jcr.version.VersionException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +/** + * LockRefresh... + */ +public class LockRefresh extends AbstractOperation { + + private final NodeState nodeState; + + private LockRefresh(NodeState nodeState) { + this.nodeState = nodeState; + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + // nothing to do. + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeId(); + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @return + */ + public static Operation create(NodeState nodeState) { + Operation lck = new LockRefresh(nodeState); + return lck; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockRelease.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockRelease.java new file mode 100644 index 00000000000..86941300243 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/LockRelease.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.spi.NodeId; + +import javax.jcr.RepositoryException; +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import java.util.Iterator; + +/** + * LockRelease... + */ +public class LockRelease extends AbstractOperation { + + private final NodeState nodeState; + + private LockRelease(NodeState nodeState) { + this.nodeState = nodeState; + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the NodeState that has been unlocked and all its + * child properties. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + // non-recursive invalidation but including all properties + NodeEntry nodeEntry = nodeState.getNodeEntry(); + Iterator entries = nodeEntry.getPropertyEntries(); + while (entries.hasNext()) { + PropertyEntry pe = entries.next(); + pe.invalidate(false); + } + nodeEntry.invalidate(false); + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeId(); + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @return + */ + public static Operation create(NodeState nodeState) { + Operation lck = new LockRelease(nodeState); + return lck; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Merge.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Merge.java new file mode 100644 index 00000000000..ee60bd1125b --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Merge.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.jcr2spi.operation; + +import java.util.Iterator; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.apache.jackrabbit.spi.NodeId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Merge... + */ +public class Merge extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(Merge.class); + + private final NodeState nodeState; + private final String srcWorkspaceName; + private final boolean bestEffort; + private final boolean isShallow; + private final VersionManager mgr; + + private Iterator failedIds = null; + + private Merge(NodeState nodeState, String srcWorkspaceName, boolean bestEffort, boolean isShallow, VersionManager mgr) { + this.nodeState = nodeState; + this.srcWorkspaceName = srcWorkspaceName; + this.bestEffort = bestEffort; + this.isShallow = isShallow; + this.mgr = mgr; + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the target nodestate and all descendants. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + if (isActivityMerge()) { + // TODO be more specific about what needs to be invalidated + // look for the root entry and invalidate the complete tree + HierarchyEntry entry = nodeState.getNodeEntry(); + while (entry.getParent() != null) { + entry = entry.getParent(); + } + entry.invalidate(true); + } else { + try { + NodeEntry vhe = mgr.getVersionHistoryEntry(nodeState); + if (vhe != null) { + vhe.invalidate(true); + } + } catch (RepositoryException e) { + log.warn("Error while retrieving VersionHistory entry: {}", e.getMessage()); + } + nodeState.getHierarchyEntry().invalidate(true); + } + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeEntry().getWorkspaceId(); + } + + public String getSourceWorkspaceName() { + return srcWorkspaceName; + } + + public boolean bestEffort() { + return bestEffort; + } + + public boolean isShallow() { + return isShallow; + } + + public boolean isActivityMerge() { + return srcWorkspaceName == null; + } + + public void setFailedIds(Iterator failedIds) { + if (failedIds == null) { + throw new IllegalArgumentException("IdIterator must not be null."); + } + if (this.failedIds != null) { + throw new IllegalStateException("Merge operation has already been executed -> FailedIds already set."); + } + this.failedIds = failedIds; + } + + public Iterator getFailedIds() { + if (failedIds == null) { + throw new IllegalStateException("Merge operation has not been executed yet."); + } + return failedIds; + } + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @param srcWorkspaceName + * @return + */ + public static Merge create(NodeState nodeState, String srcWorkspaceName, boolean bestEffort, boolean isShallow, VersionManager mgr) { + return new Merge(nodeState, srcWorkspaceName, bestEffort, isShallow, mgr); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Move.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Move.java new file mode 100644 index 00000000000..4eec1d3b87f --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Move.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.operation; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Move... + */ +public class Move extends TransientOperation { + + private static Logger log = LoggerFactory.getLogger(Move.class); + + private static final int MOVE_OPTIONS = ItemStateValidator.CHECK_ACCESS + | ItemStateValidator.CHECK_LOCK + | ItemStateValidator.CHECK_VERSIONING + | ItemStateValidator.CHECK_CONSTRAINTS; + + private final NodeId srcId; + private final NodeId destParentId; + private final Name destName; + + private final NodeState srcState; + private final NodeState srcParentState; + private final NodeState destParentState; + + private final boolean sessionMove; + + private Move(NodeState srcNodeState, NodeState srcParentState, NodeState destParentState, Name destName, boolean sessionMove) + throws RepositoryException { + super(sessionMove ? MOVE_OPTIONS : ItemStateValidator.CHECK_NONE); + + this.srcId = (NodeId) srcNodeState.getId(); + this.destParentId = destParentState.getNodeId(); + this.destName = destName; + + this.srcState = srcNodeState; + this.srcParentState = srcParentState; + this.destParentState = destParentState; + + this.sessionMove = sessionMove; + + addAffectedItemState(srcNodeState); + addAffectedItemState(srcParentState); + addAffectedItemState(destParentState); + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + */ + public void accept(OperationVisitor visitor) throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Throws UnsupportedOperationException if this Move Operation is a transient + * modification. Otherwise, the moved state as well as both parent states + * are invalidated. + * + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + if (sessionMove) { + srcState.getNodeEntry().complete(this); + } else { + // non-recursive invalidation + try { + srcState.getNodeEntry().move(destName, destParentState.getNodeEntry(), false); + // TODO: TOBEFIXED. moved state ev. got a new definition. + } catch (RepositoryException e) { + // should not occur + log.error("Internal error", e); + srcParentState.getHierarchyEntry().invalidate(false); + destParentState.getHierarchyEntry().invalidate(false); + srcState.getHierarchyEntry().invalidate(false); + } + } + } + + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + if (sessionMove) { + status = STATUS_UNDO; + srcState.getHierarchyEntry().complete(this); + } else { + super.undo(); + } + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getSourceId() { + return srcId; + } + + public NodeId getDestinationParentId() { + return destParentId; + } + + public NodeState getSourceState() { + return srcState; + } + + public NodeState getSourceParentState() { + return srcParentState; + } + + public NodeState getDestinationParentState() { + return destParentState; + } + + public Name getDestinationName() { + return destName; + } + + //------------------------------------------------------------< Factory >--- + public static Operation create(Path srcPath, Path destPath, + HierarchyManager hierMgr, + PathResolver resolver, + boolean sessionMove) + throws ItemExistsException, NoSuchNodeTypeException, RepositoryException { + // src must not be ancestor of destination + if (srcPath.isAncestorOf(destPath)) { + String msg = "Invalid destination path: cannot be descendant of source path (" + LogUtil.safeGetJCRPath(destPath, resolver) + "," + LogUtil.safeGetJCRPath(srcPath, resolver) + ")"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // destination must not contain an index + int index = destPath.getIndex(); + if (index != Path.INDEX_UNDEFINED) { + // subscript in name element + String msg = "Invalid destination path: subscript in name element is not allowed (" + LogUtil.safeGetJCRPath(destPath, resolver) + ")"; + log.debug(msg); + throw new RepositoryException(msg); + } + // root node cannot be moved: + if (srcPath.denotesRoot() || destPath.denotesRoot()) { + String msg = "Cannot move the root node."; + log.debug(msg); + throw new RepositoryException(msg); + } + + NodeState srcState = getNodeState(srcPath, hierMgr); + NodeState srcParentState = getNodeState(srcPath.getAncestor(1), hierMgr); + NodeState destParentState = getNodeState(destPath.getAncestor(1), hierMgr); + Name destName = destPath.getName(); + + if (sessionMove) { + NodeEntry destEntry = (NodeEntry) destParentState.getHierarchyEntry(); + + // force child node entries list to be present before the move is executed + // on the hierarchy entry. + assertChildNodeEntries(srcParentState); + assertChildNodeEntries(destParentState); + + if (destEntry.hasNodeEntry(destName)) { + NodeEntry existing = destEntry.getNodeEntry(destName, Path.INDEX_DEFAULT); + if (existing != null && sessionMove) { + try { + if (!existing.getNodeState().getDefinition().allowsSameNameSiblings()) { + throw new ItemExistsException("Node existing at move destination does not allow same name siblings."); + } + } catch (ItemNotFoundException e) { + // existing apparent not valid any more -> probably no conflict + } + } + } + } + + Move move = new Move(srcState, srcParentState, destParentState, destName, sessionMove); + return move; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Operation.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Operation.java new file mode 100644 index 00000000000..b1bb4658522 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Operation.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.jcr2spi.operation; + +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; +import org.apache.jackrabbit.jcr2spi.state.ItemState; + +import javax.jcr.version.VersionException; +import java.util.Collection; + +/** + * Operation... + */ +public interface Operation { + + int STATUS_PENDING = 0; + int STATUS_PERSISTED = 1; + int STATUS_UNDO = 2; + + /** + * Returns the name of this operation. + * + * @return the name of this operation. + */ + public String getName(); + + /** + * Calls the appropriate visit method on visitor + * based on the type of this operation. + * + * @param visitor the visitor to call back. + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException; + + /** + * A collection of {@link ItemState}s that are affected by this operation. + * + * @return collection of affected ItemStates. + */ + public Collection getAffectedItemStates(); + + /** + * Informs this Operation that it has been successfully executed. + * + * @throws RepositoryException + */ + public void persisted() throws RepositoryException; + + /** + * Revert changes made by this operation. + * + * @throws RepositoryException + */ + public void undo() throws RepositoryException; + + /** + * Returns the status of this operation. + * + * @return status of this operation. + */ + public int getStatus(); +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/OperationVisitor.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/OperationVisitor.java new file mode 100644 index 00000000000..2d575eba72f --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/OperationVisitor.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.operation; + +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.AccessDeniedException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; +import javax.jcr.ItemExistsException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.PathNotFoundException; +import javax.jcr.MergeException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.version.VersionException; + +/** + * OperationVisitor... + */ +public interface OperationVisitor { + + public void visit(AddNode operation) throws RepositoryException; + + public void visit(AddProperty operation) throws RepositoryException; + + public void visit(Remove operation) throws RepositoryException; + + public void visit(SetMixin operation) throws RepositoryException; + + /** + * @since JCR 2.0 + */ + public void visit(SetPrimaryType operation) throws RepositoryException; + + public void visit(SetPropertyValue operation) throws RepositoryException; + + public void visit(ReorderNodes operation) throws RepositoryException; + + public void visit(SetTree operation) throws RepositoryException; + + public void visit(Clone operation) throws NoSuchWorkspaceException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException; + + public void visit(Copy operation) throws NoSuchWorkspaceException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException; + + public void visit(Move operation) throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException; + + public void visit(Update operation) throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException; + + public void visit(Checkout operation) throws RepositoryException, UnsupportedRepositoryOperationException; + + public void visit(Checkin operation) throws UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException; + + /** + * @since JCR 2.0 + */ + public void visit(Checkpoint operation) throws RepositoryException; + + public void visit(Restore operation) throws VersionException, PathNotFoundException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException; + + public void visit(Merge operation) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException; + + public void visit(ResolveMergeConflict operation) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException; + + public void visit(LockOperation operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException; + + public void visit(LockRefresh operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException; + + public void visit(LockRelease operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException; + + public void visit(AddLabel operation) throws VersionException, RepositoryException; + + public void visit(RemoveLabel operation) throws VersionException, RepositoryException; + + public void visit(RemoveVersion operation) throws VersionException, AccessDeniedException, ReferentialIntegrityException, RepositoryException; + + public void visit(WorkspaceImport operation) throws RepositoryException; + + /** + * @since JCR 2.0 + */ + public void visit(CreateActivity operation) throws RepositoryException; + + /** + * @since JCR 2.0 + */ + public void visit(RemoveActivity operation) throws RepositoryException; + + /** + * @since JCR 2.0 + */ + public void visit(CreateConfiguration operation) throws RepositoryException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Remove.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Remove.java new file mode 100644 index 00000000000..3d6b1ee7488 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Remove.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.operation; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.ItemId; + +/** + * Remove... + */ +public class Remove extends TransientOperation { + + private static final int REMOVE_OPTIONS = + ItemStateValidator.CHECK_LOCK + | ItemStateValidator.CHECK_VERSIONING + | ItemStateValidator.CHECK_CONSTRAINTS; + + private final ItemId removeId; + protected ItemState removeState; + protected NodeState parent; + + private Remove(ItemState removeState, NodeState parent, int options) throws RepositoryException { + super(options); + this.removeId = removeState.getId(); + this.removeState = removeState; + this.parent = parent; + + addAffectedItemState(removeState); + addAffectedItemState(parent); + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + parent.getHierarchyEntry().complete(this); + } + + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_UNDO; + parent.getHierarchyEntry().complete(this); + } + + //----------------------------------------< Access Operation Parameters >--- + public ItemId getRemoveId() throws RepositoryException { + return removeId; + } + + public ItemState getRemoveState() { + return removeState; + } + + public NodeState getParentState() { + return parent; + } + + //------------------------------------------------------------< Factory >--- + public static Operation create(ItemState state) throws RepositoryException { + return create(state, REMOVE_OPTIONS); + } + + public static Operation create(ItemState state, int options) throws RepositoryException { + if (state.isNode() && ((NodeState) state).getDefinition().allowsSameNameSiblings()) { + // in case of SNS-siblings make sure the parent hierarchy entry has + // its child entries loaded. + assertChildNodeEntries(state.getParent()); + } + return new Remove(state, state.getParent(), options); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveActivity.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveActivity.java new file mode 100644 index 00000000000..768e90722ec --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveActivity.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.operation; + +import java.util.Iterator; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.PropertyId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveActivity... + */ +public class RemoveActivity extends AbstractRemove { + + private static Logger log = LoggerFactory.getLogger(RemoveActivity.class); + + private final Iterator refs; + private final HierarchyManager hMgr; + + private RemoveActivity(NodeState removeActivity, HierarchyManager hierarchyMgr) + throws RepositoryException { + super(removeActivity, removeActivity.getParent()); + refs = removeActivity.getNodeReferences(null, false); + hMgr = hierarchyMgr; + } + + //----------------------------------------------------------< Operation >--- + /** + * @see org.apache.jackrabbit.jcr2spi.operation.Operation#accept(org.apache.jackrabbit.jcr2spi.operation.OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the NodeState that has been updated and all + * its descendants. Second, the parent state gets invalidated. + * + * @see org.apache.jackrabbit.jcr2spi.operation.Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + + // invalidate all references to the removed activity + while (refs.hasNext()) { + HierarchyEntry entry = hMgr.lookup(refs.next()); + if (entry != null) { + entry.invalidate(false); + } + } + + // invalidate the activities parent + parent.getNodeEntry().invalidate(false); + } + + //------------------------------------------------------------< Factory >--- + public static Operation create(NodeState activityState, HierarchyManager hierarchyMgr) + throws RepositoryException { + RemoveActivity rm = new RemoveActivity(activityState, hierarchyMgr); + return rm; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveLabel.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveLabel.java new file mode 100644 index 00000000000..392329a87fa --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveLabel.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +/** + * RemoveLabel... + */ +public class RemoveLabel extends AbstractOperation { + + private static Logger log = LoggerFactory.getLogger(RemoveLabel.class); + + private final NodeState versionHistoryState; + private final NodeState versionState; + private final Name label; + + private RemoveLabel(NodeState versionHistoryState, NodeState versionState, Name label) { + this.versionHistoryState = versionHistoryState; + this.versionState = versionState; + this.label = label; + + // NOTE: affected-states only needed for transient modifications + } + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + * @throws RepositoryException + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws ItemExistsException + * @throws NoSuchNodeTypeException + * @throws UnsupportedRepositoryOperationException + * @throws VersionException + */ + public void accept(OperationVisitor visitor) throws RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the jcr:versionLabel nodestate present with the given + * version history and all decendant states (property states). + * + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + status = STATUS_PERSISTED; + try { + NodeEntry vhEntry = (NodeEntry) versionHistoryState.getHierarchyEntry(); + NodeEntry lnEntry = vhEntry.getNodeEntry(NameConstants.JCR_VERSIONLABELS, Path.INDEX_DEFAULT); + if (lnEntry != null) { + lnEntry.invalidate(true); + } + } catch (RepositoryException e) { + log.debug(e.getMessage()); + } + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getVersionHistoryId() throws RepositoryException { + return versionHistoryState.getNodeEntry().getWorkspaceId(); + } + + public NodeId getVersionId() throws RepositoryException { + return versionState.getNodeEntry().getWorkspaceId(); + } + + public Name getLabel() { + return label; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param versionHistoryState + * @param versionState + * @param label + * @return + */ + public static Operation create(NodeState versionHistoryState, NodeState versionState, Name label) { + return new RemoveLabel(versionHistoryState, versionState, label); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveVersion.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveVersion.java new file mode 100644 index 00000000000..c3f28da8645 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/RemoveVersion.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.jcr2spi.operation; + +import java.util.Iterator; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.version.VersionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveVersion... + */ +public class RemoveVersion extends AbstractRemove { + + private static Logger log = LoggerFactory.getLogger(RemoveVersion.class); + + private NodeEntry versionableEntry = null; + + private RemoveVersion(ItemState removeState, NodeState parent, VersionManager mgr) + throws RepositoryException { + super(removeState, parent); + try { + versionableEntry = mgr.getVersionableNodeEntry((NodeState) removeState); + } catch (RepositoryException e) { + log.warn("Failed to retrieve the hierarchy entry of the versionable node.", e); + } + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the NodeState that has been updated and all + * its descendants. Second, the parent state gets invalidated. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + // Invalidate the versionable node as well (version related properties) + if (versionableEntry != null) { + Iterator propEntries = versionableEntry.getPropertyEntries(); + while (propEntries.hasNext()) { + PropertyEntry pe = propEntries.next(); + pe.invalidate(false); + } + versionableEntry.invalidate(false); + } + + // invalidate the versionhistory entry and all its children + // in order to have the v-graph recalculated + parent.getNodeEntry().invalidate(true); + } + + //------------------------------------------------------------< Factory >--- + public static Operation create(NodeState versionState, NodeState vhState, VersionManager mgr) + throws RepositoryException { + RemoveVersion rm = new RemoveVersion(versionState, vhState, mgr); + return rm; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/ReorderNodes.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/ReorderNodes.java new file mode 100644 index 00000000000..0c331f541c2 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/ReorderNodes.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.jcr2spi.operation; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; + +/** + * ReorderNodes... + */ +public class ReorderNodes extends TransientOperation { + + private final NodeId parentId; + private final NodeId insertId; + private final NodeId beforeId; + + private final NodeState parentState; + private final NodeState insert; + private final NodeState before; + + private ReorderNodes(NodeState parentState, NodeState insert, NodeState before) + throws RepositoryException { + super(NO_OPTIONS); + this.parentState = parentState; + this.insert = insert; + this.before = before; + + this.parentId = parentState.getNodeId(); + this.insertId = insert.getNodeId(); + this.beforeId = (before == null) ? null : before.getNodeId(); + + addAffectedItemState(parentState); + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + */ + public void accept(OperationVisitor visitor) throws ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Throws UnsupportedOperationException + * + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + insert.getHierarchyEntry().complete(this); + } + + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_UNDO; + insert.getHierarchyEntry().complete(this); + } + + //----------------------------------------< Access Operation Parameters >--- + + public NodeId getParentId() { + return parentId; + } + + public NodeId getInsertId() { + return insertId; + } + + public NodeId getBeforeId() { + return beforeId; + } + + public NodeState getParentState() { + return parentState; + } + + public NodeState getInsertNode() { + return insert; + } + + public NodeState getBeforeNode() { + return before; + } + + //------------------------------------------------------------< Factory >--- + + public static Operation create( + NodeState parentState, Path srcPath, Path beforePath) + throws ItemNotFoundException, RepositoryException { + // make sure the parent hierarchy entry has its child entries loaded + assertChildNodeEntries(parentState); + + NodeState insert = parentState.getChildNodeState( + srcPath.getName(), srcPath.getNormalizedIndex()); + NodeState before = null; + if (beforePath != null) { + before = parentState.getChildNodeState( + beforePath.getName(), beforePath.getNormalizedIndex()); + } + return new ReorderNodes(parentState, insert, before); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/ResolveMergeConflict.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/ResolveMergeConflict.java new file mode 100644 index 00000000000..c8582dcf9b3 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/ResolveMergeConflict.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.NodeId; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; +import java.util.Iterator; + +/** + * ResolveMergeConflict... + */ +public class ResolveMergeConflict extends AbstractOperation { + + private final NodeState nodeState; + private final NodeId[] mergeFailedIds; + private final NodeId[] predecessorIds; + private final boolean resolveDone; + + private ResolveMergeConflict(NodeState nodeState, NodeId[] mergeFailedIds, NodeId[] predecessorIds, boolean resolveDone) { + this.nodeState = nodeState; + this.mergeFailedIds = mergeFailedIds; + this.predecessorIds = predecessorIds; + this.resolveDone = resolveDone; + + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the NodeState that had a merge conflict pending + * and all its child properties. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + // non-recursive invalidation BUT including all properties + Iterator propEntries = ((NodeEntry) nodeState.getHierarchyEntry()).getPropertyEntries(); + while (propEntries.hasNext()) { + PropertyEntry pe = propEntries.next(); + pe.invalidate(false); + } + nodeState.getHierarchyEntry().invalidate(false); + } + //----------------------------------------< Access Operation Parameters >--- + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeEntry().getWorkspaceId(); + } + + public NodeId[] getMergeFailedIds() { + return mergeFailedIds; + } + + public NodeId[] getPredecessorIds() { + return predecessorIds; + } + + public boolean resolveDone() { + return resolveDone; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @param mergeFailedIds + * @param predecessorIds + * @param resolveDone + */ + public static Operation create(NodeState nodeState, NodeId[] mergeFailedIds, NodeId[] predecessorIds, boolean resolveDone) { + ResolveMergeConflict up = new ResolveMergeConflict(nodeState, mergeFailedIds, predecessorIds, resolveDone); + return up; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Restore.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Restore.java new file mode 100644 index 00000000000..9b5900409f6 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Restore.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.NodeId; + +import javax.jcr.RepositoryException; +import javax.jcr.ItemExistsException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.PathNotFoundException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.lock.LockException; +import javax.jcr.version.VersionException; +import javax.jcr.version.Version; +import javax.jcr.nodetype.ConstraintViolationException; + +/** + * Restore... + */ +public class Restore extends AbstractOperation { + + private final NodeState nodeState; + private final Path relQPath; + private final NodeState[] versionStates; + private final boolean removeExisting; + + private Restore(NodeState nodeState, Path relQPath, NodeState[] versionStates, boolean removeExisting) { + this.nodeState = nodeState; + this.relQPath = relQPath; + this.versionStates = versionStates; + this.removeExisting = removeExisting; + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws PathNotFoundException, ItemExistsException, VersionException, ConstraintViolationException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * In case of a workspace-restore or 'removeExisting' the complete tree gets + * invalidated, otherwise the given NodeState that has been + * updated and all its descendants. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + NodeEntry entry; + if (nodeState == null || removeExisting) { + // invalidate the complete tree + // -> start searching root-entry from any version-entry or + // from the given nodestate + entry = (nodeState == null) ? versionStates[0].getNodeEntry() : nodeState.getNodeEntry(); + while (entry.getParent() != null) { + entry = entry.getParent(); + } + } else { + entry = nodeState.getNodeEntry(); + } + entry.invalidate(true); + } + + //----------------------------------------< Access Operation Parameters >--- + /** + * Returns id of state or the closest existing state of the restore target or + * null in case of a {@link javax.jcr.Workspace#restore(Version[], boolean)} + * + * @return + */ + public NodeId getNodeId() throws RepositoryException { + return (nodeState == null) ? null : nodeState.getNodeEntry().getWorkspaceId(); + } + + /** + * Relative path to the non-existing restore target or null + * if the state identified by {@link #getNodeId()} is the target. + * + * @return + * @see javax.jcr.Node#restore(Version, String, boolean) + */ + public Path getRelativePath() { + return relQPath; + } + + public NodeId[] getVersionIds() throws RepositoryException { + NodeId[] versionIds = new NodeId[versionStates.length]; + for (int i = 0; i < versionStates.length; i++) { + versionIds[i] = versionStates[i].getNodeEntry().getWorkspaceId(); + } + return versionIds; + } + + public boolean removeExisting() { + return removeExisting; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @param versionState + * @return + */ + public static Operation create(NodeState nodeState, Path relQPath, NodeState versionState, boolean removeExisting) { + if (nodeState == null || versionState == null) { + throw new IllegalArgumentException("Neither nodeState nor versionState must be null."); + } + Restore up = new Restore(nodeState, relQPath, new NodeState[] {versionState}, removeExisting); + return up; + } + + /** + * + * @param versionStates + * @return + */ + public static Operation create(NodeState[] versionStates, boolean removeExisting) { + if (versionStates == null || versionStates.length == 0) { + throw new IllegalArgumentException("Version states must not be null."); + } + Restore up = new Restore(null, null, versionStates, removeExisting); + return up; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetMixin.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetMixin.java new file mode 100644 index 00000000000..a2dfe94b2cd --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetMixin.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.jcr2spi.operation; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * SetMixin... + */ +public class SetMixin extends TransientOperation { + + private static final int SET_MIXIN_OPTIONS = + ItemStateValidator.CHECK_LOCK + | ItemStateValidator.CHECK_VERSIONING; + + private final NodeId nodeId; + private final NodeState nodeState; + private final Name[] mixinNames; + + private SetMixin(NodeState nodeState, Name[] mixinNames) throws RepositoryException { + this(nodeState, mixinNames, SET_MIXIN_OPTIONS); + } + + private SetMixin(NodeState nodeState, Name[] mixinNames, int options) throws RepositoryException { + super(options); + this.nodeState = nodeState; + this.nodeId = nodeState.getNodeId(); + this.mixinNames = mixinNames; + + // remember node state as affected state + addAffectedItemState(nodeState); + // add the jcr:mixinTypes property state as affected if it already exists + // and therefore gets modified by this operation. + try { + if (nodeState.hasPropertyName(NameConstants.JCR_MIXINTYPES)) { + addAffectedItemState(nodeState.getPropertyState(NameConstants.JCR_MIXINTYPES)); + } + } catch (RepositoryException e) { + // jcr:mixinTypes does not exist -> ignore + } + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws AccessDeniedException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + nodeState.getHierarchyEntry().complete(this); + } + + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_UNDO; + nodeState.getHierarchyEntry().complete(this); + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeState getNodeState() { + return nodeState; + } + + public NodeId getNodeId() { + return nodeId; + } + + public Name[] getMixinNames() { + return mixinNames; + } + + //------------------------------------------------------------< Factory >--- + + public static Operation create(NodeState nodeState, Name[] mixinNames) + throws RepositoryException { + if (nodeState == null || mixinNames == null) { + throw new IllegalArgumentException(); + } + SetMixin sm = new SetMixin(nodeState, mixinNames); + return sm; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetPrimaryType.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetPrimaryType.java new file mode 100644 index 00000000000..2814c242251 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetPrimaryType.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.jcr2spi.operation; + +import javax.jcr.AccessDeniedException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; + +/** + * SetPrimaryNodeType... + */ +public class SetPrimaryType extends TransientOperation { + + private final static int SET_PRIMARY_TYPE_OPTIONS = + ItemStateValidator.CHECK_VERSIONING + | ItemStateValidator.CHECK_LOCK; + + private final NodeId nodeId; + private final NodeState nodeState; + private final Name primaryTypeName; + + private SetPrimaryType(NodeState nodeState, Name primaryTypeName) throws RepositoryException { + this(nodeState, primaryTypeName, SET_PRIMARY_TYPE_OPTIONS); + } + private SetPrimaryType(NodeState nodeState, Name primaryTypeName, int options) throws RepositoryException { + super(options); + this.nodeState = nodeState; + this.nodeId = nodeState.getNodeId(); + this.primaryTypeName = primaryTypeName; + + // remember node state as affected state + addAffectedItemState(nodeState); + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws AccessDeniedException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + nodeState.getHierarchyEntry().complete(this); + } + + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_UNDO; + nodeState.getHierarchyEntry().complete(this); + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeState getNodeState() { + return nodeState; + } + + public NodeId getNodeId() { + return nodeId; + } + + public Name getPrimaryTypeName() { + return primaryTypeName; + } + + //------------------------------------------------------------< Factory >--- + + public static Operation create(NodeState nodeState, Name primaryTypeName) + throws RepositoryException { + if (nodeState == null || primaryTypeName == null) { + throw new IllegalArgumentException(); + } + SetPrimaryType op = new SetPrimaryType(nodeState, primaryTypeName); + return op; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetPropertyValue.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetPropertyValue.java new file mode 100644 index 00000000000..47229e30f45 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetPropertyValue.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.jcr2spi.operation; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; + +/** + * SetPropertyValue... + */ +public class SetPropertyValue extends TransientOperation { + + private static final int SET_PROPERTY_OPTIONS = + ItemStateValidator.CHECK_LOCK + | ItemStateValidator.CHECK_VERSIONING + | ItemStateValidator.CHECK_CONSTRAINTS; + + private final PropertyId propertyId; + private final PropertyState propertyState; + private final QValue[] values; + private final int valueType; + + private final QValue[] oldValues; + + private SetPropertyValue(PropertyState propertyState, int valueType, QValue[] values) + throws RepositoryException { + this(propertyState, valueType, values, SET_PROPERTY_OPTIONS); + + } + + private SetPropertyValue(PropertyState propertyState, int valueType, + QValue[] values, int options) throws RepositoryException { + super(options); + this.propertyState = propertyState; + + propertyId = (PropertyId) propertyState.getId(); + this.valueType = valueType; + this.values = values; + + // remember original values + oldValues = propertyState.getValues(); + + addAffectedItemState(propertyState); + } + + //----------------------------------------------------------< Operation >--- + /** + * + * @param visitor + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * @see Operation#persisted() + */ + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + try { + propertyState.getHierarchyEntry().complete(this); + } finally { + // dispose the original values + for (QValue v : oldValues) { + v.discard(); + } + } + } + + /** + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_UNDO; + propertyState.getHierarchyEntry().complete(this); + + // NOTE: new values don't need to be disposed as the transient change + // has been reverted with implicit value disposal. + } + + //----------------------------------------< Access Operation Parameters >--- + public PropertyId getPropertyId() { + return propertyId; + } + + public PropertyState getPropertyState() { + return propertyState; + } + + public boolean isMultiValued() { + return propertyState.isMultiValued(); + } + + public int getValueType() { + return valueType; + } + + public QValue[] getValues() { + return values; + } + + //------------------------------------------------------------< Factory >--- + public static Operation create(PropertyState propState, QValue[] qValues, + int valueType) throws RepositoryException { + // compact array (purge null entries) + List list = new ArrayList(); + for (QValue qValue : qValues) { + if (qValue != null) { + list.add(qValue); + } + } + QValue[] cleanValues = list.toArray(new QValue[list.size()]); + SetPropertyValue sv = new SetPropertyValue(propState, valueType, cleanValues); + return sv; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetTree.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetTree.java new file mode 100644 index 00000000000..3cdccb9fe40 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/SetTree.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.operation; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.UpdatableItemStateManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; + +public class SetTree extends TransientOperation { + + /** + * List of operations added to this SetTree operation. + */ + private final List operations = new ArrayList(); + + private final NodeState treeState; + + private SetTree(NodeState treeState) throws RepositoryException { + super(ItemStateValidator.CHECK_NONE); + this.treeState = treeState; + } + + private SetTree(UpdatableItemStateManager itemStateMgr, NodeState parentState, Name nodeName, Name nodeTypeName, String uuid) throws RepositoryException { + super(ItemStateValidator.CHECK_NONE); + Operation addNode = InternalAddNode.create(parentState, nodeName, nodeTypeName, uuid); + operations.add(addNode); + + itemStateMgr.execute(addNode); + treeState = (NodeState) ((AddNode) addNode).getAddedStates().get(0); + } + + //-----------------------------------------------------------------< Operation >--- + /** + * @param visitor + */ + public void accept(OperationVisitor visitor) throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Persisting a SetPolicy operation involves persisting each individual operation added + * by this policy. The concerned operation will assert the status and set it accordingly. + * + * @see Operation#persisted() + */ + @Override + public void persisted() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + for (Operation op : operations) { + op.persisted(); + } + } + + /** + * Undoing a SetPolicy operation involves undoing all operations added by the SetPolicy. + * @see Operation#undo() + */ + @Override + public void undo() throws RepositoryException { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + for (Operation op : operations) { + op.undo(); + } + } + + public NodeId getParentId() throws RepositoryException { + return treeState.getParent().getNodeId(); + } + + public NodeState getParentState() throws RepositoryException { + return treeState.getParent(); + } + + public NodeState getTreeState() throws RepositoryException { + return treeState; + } + + /** + * Add a child node operation to this {@code setTree} instance. + * + * @param parentState + * @param nodeName + * @param nodeTypeName + * @param uuid + * @return + * @throws RepositoryException + */ + public Operation addChildNode(NodeState parentState, Name nodeName, Name nodeTypeName, String uuid) throws RepositoryException { + Operation addNode = InternalAddNode.create(parentState, nodeName, nodeTypeName, uuid); + operations.add(addNode); + return addNode; + } + /** + * Add a child property operation to this {@code setTree} instance. + * + * @param parentState + * @param propName + * @param propertyType + * @param values + * @param definition + * @return + * @throws RepositoryException + */ + public Operation addChildProperty(NodeState parentState, Name propName, + int propertyType, QValue[] values, + QPropertyDefinition definition) throws RepositoryException { + Operation addProperty = new InternalAddProperty(parentState, propName, propertyType, values, definition); + operations.add(addProperty); + return addProperty; + } + + //------------------------------------------------------------< factory >--- + + public static SetTree create(NodeState treeState) throws RepositoryException { + SetTree operation = new SetTree(treeState); + return operation; + } + + public static SetTree create(UpdatableItemStateManager itemStateMgr, NodeState parent, Name nodeName, Name nodeTypeName, String uuid) throws RepositoryException { + return new SetTree(itemStateMgr, parent, nodeName, nodeTypeName, uuid); + } + + //-------------------------------------------------------------------------- + + /** + * Inner class for adding a protected node. + */ + private static final class InternalAddNode extends AddNode implements IgnoreOperation { + /** + * Options that must not be violated for a successful set policy operation. + */ + private final static int ADD_NODE_OPTIONS = ItemStateValidator.CHECK_ACCESS | + ItemStateValidator.CHECK_LOCK | + ItemStateValidator.CHECK_COLLISION | + ItemStateValidator.CHECK_VERSIONING; + + private InternalAddNode(NodeState parentState, Name nodeName, Name nodeTypeName, String uuid) throws RepositoryException { + super(parentState, nodeName, nodeTypeName, uuid, ADD_NODE_OPTIONS); + } + + public static Operation create(NodeState parentState, Name nodeName, Name nodeTypeName, String uuid) throws RepositoryException { + assertChildNodeEntries(parentState); + InternalAddNode an = new InternalAddNode(parentState, nodeName, nodeTypeName, uuid); + return an; + } + } + + /** + * Inner class for adding a protected property. + */ + private static final class InternalAddProperty extends AddProperty implements IgnoreOperation { + private final static int ADD_PROPERTY_OPTIONS = ItemStateValidator.CHECK_ACCESS | + ItemStateValidator.CHECK_LOCK | + ItemStateValidator.CHECK_COLLISION | + ItemStateValidator.CHECK_VERSIONING; + + private InternalAddProperty(NodeState parentState, Name propName, int propertyType, QValue[] values, QPropertyDefinition definition) throws RepositoryException { + super(parentState, propName, propertyType, values, definition, ADD_PROPERTY_OPTIONS); + } + } +} + + diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/TransientOperation.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/TransientOperation.java new file mode 100644 index 00000000000..bb5f73be3e2 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/TransientOperation.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; + +/** + * TransientOperation... + */ +public abstract class TransientOperation extends AbstractOperation { + + static final int NO_OPTIONS = ItemStateValidator.CHECK_NONE; + static final int DEFAULT_OPTIONS = + ItemStateValidator.CHECK_LOCK | + ItemStateValidator.CHECK_COLLISION | + ItemStateValidator.CHECK_VERSIONING | + ItemStateValidator.CHECK_CONSTRAINTS; + + private final int options; + + TransientOperation(int options) { + this.options = options; + } + + /** + * Return the set of options that should be used to validate the transient + * modification. Valid options are a combination of any of the following + * options: + * + *

          + *
        • {@link org.apache.jackrabbit.jcr2spi.state.ItemStateValidator#CHECK_NONE CHECK_NONE} if no validation check is required,
        • + *
        • {@link org.apache.jackrabbit.jcr2spi.state.ItemStateValidator#CHECK_ACCESS CHECK_ACCESS},
        • + *
        • {@link org.apache.jackrabbit.jcr2spi.state.ItemStateValidator#CHECK_COLLISION CHECK_COLLISION},
        • + *
        • {@link org.apache.jackrabbit.jcr2spi.state.ItemStateValidator#CHECK_CONSTRAINTS CHECK_CONSTRAINTS},
        • + *
        • {@link org.apache.jackrabbit.jcr2spi.state.ItemStateValidator#CHECK_LOCK CHECK_LOCK},
        • + *
        • {@link org.apache.jackrabbit.jcr2spi.state.ItemStateValidator#CHECK_VERSIONING CHECK_VERSIONING},
        • + *
        • {@link org.apache.jackrabbit.jcr2spi.state.ItemStateValidator#CHECK_ALL CHECK_ALL} as shortcut for all options.
        • + *
        + * + * @return The set of options used to validate the transient modification. + * @see org.apache.jackrabbit.jcr2spi.state.ItemStateValidator + */ + public int getOptions() { + return options; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/TransientOperationVisitor.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/TransientOperationVisitor.java new file mode 100644 index 00000000000..11622c5d6e1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/TransientOperationVisitor.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.jcr2spi.operation; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.MergeException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PathNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +/** + * TransientOperationVisitor... + */ +public abstract class TransientOperationVisitor implements OperationVisitor { + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(Clone) + */ + public void visit(Clone operation) throws NoSuchWorkspaceException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + throw new UnsupportedOperationException("Internal error: Clone isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(Copy) + */ + public void visit(Copy operation) throws NoSuchWorkspaceException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + throw new UnsupportedOperationException("Internal error: Copy isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(Checkout) + */ + public void visit(Checkout operation) throws RepositoryException, UnsupportedRepositoryOperationException { + throw new UnsupportedOperationException("Internal error: Checkout isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(Checkin) + */ + public void visit(Checkin operation) throws UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedOperationException("Internal error: Checkin isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(Checkpoint) + */ + public void visit(Checkpoint operation) throws UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedOperationException("Internal error: Checkin isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(Update) + */ + public void visit(Update operation) throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedOperationException("Internal error: Update isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(Restore) + */ + public void visit(Restore operation) throws VersionException, PathNotFoundException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedOperationException("Internal error: Restore isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(Merge) + */ + public void visit(Merge operation) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedOperationException("Internal error: Merge isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(ResolveMergeConflict) + */ + public void visit(ResolveMergeConflict operation) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedOperationException("Internal error: ResolveMergeConflict isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(LockOperation) + */ + public void visit(LockOperation operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException { + throw new UnsupportedOperationException("Internal error: Lock isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(LockRefresh) + */ + public void visit(LockRefresh operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException { + throw new UnsupportedOperationException("Internal error: LockRefresh isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(LockRelease) + */ + public void visit(LockRelease operation) throws AccessDeniedException, InvalidItemStateException, UnsupportedRepositoryOperationException, LockException, RepositoryException { + throw new UnsupportedOperationException("Internal error: LockRelease isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(AddLabel) + */ + public void visit(AddLabel operation) throws VersionException, RepositoryException { + throw new UnsupportedOperationException("Internal error: AddLabel isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(RemoveLabel) + */ + public void visit(RemoveLabel operation) throws VersionException, RepositoryException { + throw new UnsupportedOperationException("Internal error: RemoveLabel isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(RemoveVersion) + */ + public void visit(RemoveVersion operation) throws VersionException, AccessDeniedException, ReferentialIntegrityException, RepositoryException { + throw new UnsupportedOperationException("Internal error: RemoveVersion isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(WorkspaceImport) + */ + public void visit(WorkspaceImport operation) throws RepositoryException { + throw new UnsupportedOperationException("Internal error: WorkspaceImport isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(CreateActivity) + */ + public void visit(CreateActivity operation) throws RepositoryException { + throw new UnsupportedOperationException("Internal error: CreateActivity isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(RemoveActivity) + */ + public void visit(RemoveActivity operation) throws RepositoryException { + throw new UnsupportedOperationException("Internal error: RemoveActivity isn't a transient operation."); + } + + /** + * @throws UnsupportedOperationException + * @see OperationVisitor#visit(CreateConfiguration) + */ + public void visit(CreateConfiguration operation) throws RepositoryException { + throw new UnsupportedOperationException("Internal error: CreateConfiguration isn't a transient operation."); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Update.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Update.java new file mode 100644 index 00000000000..4dc61760e54 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/Update.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.NodeId; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +/** + * Update... + */ +public class Update extends AbstractOperation { + + private final NodeState nodeState; + private final String srcWorkspaceName; + + private Update(NodeState nodeState, String srcWorkspaceName) { + this.nodeState = nodeState; + this.srcWorkspaceName = srcWorkspaceName; + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the NodeState that has been updated and all + * its descendants. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + nodeState.getHierarchyEntry().invalidate(true); + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeEntry().getWorkspaceId(); + } + + public String getSourceWorkspaceName() { + return srcWorkspaceName; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @param srcWorkspaceName + * @return + */ + public static Operation create(NodeState nodeState, String srcWorkspaceName) { + Update up = new Update(nodeState, srcWorkspaceName); + return up; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/WorkspaceImport.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/WorkspaceImport.java new file mode 100644 index 00000000000..5d5271fc02f --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/operation/WorkspaceImport.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.jcr2spi.operation; + +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.spi.NodeId; + +import javax.jcr.RepositoryException; +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.version.VersionException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import java.io.InputStream; + +/** + * WorkspaceImport... + */ +public class WorkspaceImport extends AbstractOperation { + + private final NodeState nodeState; + private final InputStream xmlStream; + private final int uuidBehaviour; + + private WorkspaceImport(NodeState nodeState, InputStream xmlStream, int uuidBehaviour) { + if (nodeState == null || xmlStream == null) { + throw new IllegalArgumentException(); + } + this.nodeState = nodeState; + this.xmlStream = xmlStream; + this.uuidBehaviour = uuidBehaviour; + + // NOTE: affected-states only needed for transient modifications + } + + //----------------------------------------------------------< Operation >--- + /** + * @see Operation#accept(OperationVisitor) + */ + public void accept(OperationVisitor visitor) throws RepositoryException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException { + assert status == STATUS_PENDING; + visitor.visit(this); + } + + /** + * Invalidates the NodeState that has been updated and all + * its descendants. + * + * @see Operation#persisted() + */ + public void persisted() { + assert status == STATUS_PENDING; + status = STATUS_PERSISTED; + NodeEntry entry; + if (uuidBehaviour == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING || + uuidBehaviour == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING) { + // invalidate the complete tree + entry = nodeState.getNodeEntry(); + while (entry.getParent() != null) { + entry = entry.getParent(); + } + entry.invalidate(true); + } else { + // import only added new items below the import target. therefore + // recursive invalidation is not required. // TODO correct? + nodeState.getNodeEntry().invalidate(false); + } + } + + //----------------------------------------< Access Operation Parameters >--- + public NodeId getNodeId() throws RepositoryException { + return nodeState.getNodeId(); + } + + public InputStream getXmlStream() { + return xmlStream; + } + + public int getUuidBehaviour() { + return uuidBehaviour; + } + + //------------------------------------------------------------< Factory >--- + /** + * + * @param nodeState + * @param xmlStream + * @return + */ + public static Operation create(NodeState nodeState, InputStream xmlStream, int uuidBehaviour) { + return new WorkspaceImport(nodeState, xmlStream, uuidBehaviour); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/NodeIteratorImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/NodeIteratorImpl.java new file mode 100644 index 00000000000..c2dfe72465b --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/NodeIteratorImpl.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.jcr2spi.query; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RangeIterator; + +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.QueryResultRow; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements a {@link javax.jcr.NodeIterator} returned by + * {@link javax.jcr.query.QueryResult#getNodes()}. + */ +public class NodeIteratorImpl implements ScoreNodeIterator { + + /** Logger instance for this class */ + private static final Logger log = LoggerFactory.getLogger(NodeIteratorImpl.class); + + /** ItemManager to turn Ids into Node instances */ + private final ItemManager itemMgr; + + /** */ + private final HierarchyManager hierarchyMgr; + + /** The QueryResultRows */ + private final RangeIterator rows; + + /** Current position of this node iterator */ + private int pos = -1; + + /** Number of invalid nodes */ + private int invalid = 0; + + /** Id of the next Node */ + private NodeId nextId; + + /** Reference to the next node instance */ + private Node next; + + /** Score for the next node */ + private double nextScore; + + /** + * Creates a new NodeIteratorImpl instance. + * + * @param itemMgr The ItemManager to build Node instances. + * @param hierarchyMgr The HierarchyManager used to retrieve the + * HierarchyEntry objects from the ids returned by the query. + * @param queryInfo the query result. + */ + public NodeIteratorImpl(ItemManager itemMgr, HierarchyManager hierarchyMgr, + QueryInfo queryInfo) { + this.itemMgr = itemMgr; + this.hierarchyMgr = hierarchyMgr; + this.rows = queryInfo.getRows(); + + fetchNext(); + } + + //------------------------------------------------------< ScoreIterator >--- + /** + * Returns the score of the node returned by {@link #nextNode()}. In other + * words, this method returns the score value of the next Node. + * + * @return the score of the node returned by {@link #nextNode()}. + * @throws NoSuchElementException if there is no next node. + * @see ScoreNodeIterator#getScore() + */ + public double getScore() throws NoSuchElementException { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return nextScore; + } + + //-------------------------------------------------------< NodeIterator >--- + /** + * Returns the next Node in the result set. + * + * @return the next Node in the result set. + * @throws NoSuchElementException if iteration has no more Nodes. + * @see javax.jcr.NodeIterator#nextNode() + */ + public Node nextNode() throws NoSuchElementException { + if (next == null) { + throw new NoSuchElementException(); + } + Node n = next; + fetchNext(); + return n; + } + + //------------------------------------------------------< RangeIterator >--- + /** + * Skip a number of Nodes in this iterator. + * + * @param skipNum the non-negative number of Nodes to skip + * @throws NoSuchElementException if skipped past the last Node + * in this iterator. + * @see javax.jcr.NodeIterator#skip(long) + */ + public void skip(long skipNum) throws NoSuchElementException { + if (skipNum < 0) { + throw new IllegalArgumentException("skipNum must not be negative"); + } + if (skipNum == 0) { + // do nothing + } else { + rows.skip(skipNum - 1); + pos += skipNum - 1; + fetchNext(); + } + } + + /** + * Returns the number of nodes in this iterator. + *

        + * Note: The number returned by this method may differ from the number + * of nodes actually returned by calls to hasNext() / getNextNode()! This + * is because this iterator works on a lazy instantiation basis and while + * iterating over the nodes some of them might have been deleted in the + * meantime. Those will not be returned by getNextNode(). As soon as an + * invalid node is detected, the size of this iterator is adjusted. + * + * @return the number of node in this iterator. + * @see javax.jcr.RangeIterator#getSize() + */ + public long getSize() { + if (rows.getSize() != -1) { + return rows.getSize() - invalid; + } else { + return -1; + } + } + + /** + * Returns the current position in this NodeIterator. + * + * @return the current position in this NodeIterator. + * @see javax.jcr.RangeIterator#getPosition() + */ + public long getPosition() { + return pos - invalid; + } + + /** + * Returns the next Node in the result set. + * + * @return the next Node in the result set. + * @throws NoSuchElementException if iteration has no more Nodes. + * @see java.util.Iterator#next() + */ + public Object next() throws NoSuchElementException { + return nextNode(); + } + + /** + * Returns true if there is another Node + * available; false otherwise. + * + * @return true if there is another Node + * available; false otherwise. + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() { + return next != null; + } + + /** + * @throws UnsupportedOperationException always. + * @see Iterator#remove() + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + //------------------------------------------------------------< private >--- + /** + * Clears {@link #next} and tries to fetch the next Node instance. + * When this method returns {@link #next} refers to the next available + * node instance in this iterator. If {@link #next} is null when this + * method returns, then there are no more valid element in this iterator. + */ + private void fetchNext() { + // reset + next = null; + nextScore = 0; + + while (next == null && rows.hasNext()) { + try { + QueryResultRow row = (QueryResultRow) rows.next(); + nextId = row.getNodeId(null); + Item tmp = itemMgr.getItem(hierarchyMgr.getNodeEntry(nextId)); + + if (tmp.isNode()) { + next = (Node) tmp; + nextScore = row.getScore(null); + } else { + log.warn("Item with Id is not a Node: " + nextId); + // try next + invalid++; + pos++; + } + } catch (Exception e) { + log.warn("Exception retrieving Node with Id: " + nextId); + // try next + invalid++; + pos++; + } + } + pos++; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryImpl.java new file mode 100644 index 00000000000..a76371ca8fd --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryImpl.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.query; + +import java.util.HashMap; +import java.util.Map; +import java.util.Arrays; +import java.util.Collection; + +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.jcr2spi.WorkspaceManager; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; + +/** + * Provides the default implementation for a JCR query. + */ +public class QueryImpl implements Query { + + /** + * The session of the user executing this query + */ + private final Session session; + + /** + * Provides various managers. + */ + private final ManagerProvider mgrProvider; + + /** + * The item manager of the session that executes this query. + */ + private final ItemManager itemManager; + + /** + * The query statement + */ + private String statement; + + /** + * The syntax of the query statement + */ + private String language; + + /** + * The node where this query is persisted. Only set when this is a persisted + * query. + */ + private Node node; + + /** + * The WorkspaceManager used to execute queries. + */ + private WorkspaceManager wspManager; + + /** + * The maximum result size + */ + private long limit = -1; + + /** + * The offset in the total result set + */ + private long offset = -1; + + /** + * The name/value pairs collected upon calls to {@link #bindValue(String, Value)}. + */ + private final Map boundValues = new HashMap(); + + /** + * The names of the bind variables as returned by the SPI implementation + * after checking the query statement. + */ + private final Collection varNames; + + /** + * Creates a new query. + * + * @param session the session that created this query. + * @param mgrProvider the manager provider. + * @param itemMgr the item manager of that session. + * @param wspManager the workspace manager that belongs to the session. + * @param statement the query statement. + * @param language the language of the query statement. + * @param node the node from where the query was read or + * null if this query is not a stored + * query. + * @throws InvalidQueryException if the query is invalid. + */ + public QueryImpl(Session session, + ManagerProvider mgrProvider, + ItemManager itemMgr, + WorkspaceManager wspManager, + String statement, + String language, + Node node) + throws InvalidQueryException, RepositoryException { + this.session = session; + this.mgrProvider = mgrProvider; + this.itemManager = itemMgr; + this.statement = statement; + this.language = language; + this.wspManager = wspManager; + this.varNames = Arrays.asList(this.wspManager.checkQueryStatement( + statement, language, getNamespaceMappings())); + this.node = node; + } + + /** + * @see Query#execute() + */ + public QueryResult execute() throws RepositoryException { + QueryInfo qI = wspManager.executeQuery( + statement, language, getNamespaceMappings(), limit, offset, boundValues); + return new QueryResultImpl(itemManager, mgrProvider, qI); + } + + /*** + * Utility method that returns the namespace mappings of the current + * session. + * + * @return namespace mappings (prefix -> uri) + * @throws RepositoryException if a repository error occurs + */ + private Map getNamespaceMappings() throws RepositoryException { + Map mappings = new HashMap(); + for (String prefix : session.getNamespacePrefixes()) { + mappings.put(prefix, session.getNamespaceURI(prefix)); + } + return mappings; + } + + /** + * @see Query#getStatement() + */ + public String getStatement() { + return statement; + } + + /** + * @see Query#getLanguage() + */ + public String getLanguage() { + return language; + } + + /** + * @see Query#getStoredQueryPath() + */ + public String getStoredQueryPath() throws ItemNotFoundException, RepositoryException { + if (node == null) { + throw new ItemNotFoundException("Not a persistent query."); + } + return node.getPath(); + } + + /** + * @see Query#storeAsNode(String) + */ + public Node storeAsNode(String absPath) throws ItemExistsException, + PathNotFoundException, VersionException, ConstraintViolationException, + LockException, UnsupportedRepositoryOperationException, RepositoryException { + + NamePathResolver resolver = mgrProvider.getNamePathResolver(); + try { + Path p = resolver.getQPath(absPath).getNormalizedPath(); + if (!p.isAbsolute()) { + throw new RepositoryException(absPath + " is not an absolute path"); + } + String jcrParent = resolver.getJCRPath(p.getAncestor(1)); + if (!session.itemExists(jcrParent)) { + throw new PathNotFoundException(jcrParent); + } + String relPath = resolver.getJCRPath(p).substring(1); + String ntName = resolver.getJCRName(NameConstants.NT_QUERY); + Node queryNode = session.getRootNode().addNode(relPath, ntName); + // set properties + queryNode.setProperty(resolver.getJCRName(NameConstants.JCR_LANGUAGE), getLanguage()); + queryNode.setProperty(resolver.getJCRName(NameConstants.JCR_STATEMENT), getStatement()); + node = queryNode; + return node; + } catch (NameException e) { + throw new RepositoryException(e.getMessage(), e); + } + } + + /** + * @see Query#getBindVariableNames() + */ + public String[] getBindVariableNames() throws RepositoryException { + return varNames.toArray(new String[varNames.size()]); + } + + /** + * @see Query#bindValue(String, Value) + */ + public void bindValue(String varName, Value value) throws RepositoryException { + if (!varNames.contains(varName)) { + throw new IllegalArgumentException(varName + " is not a known bind variable name in this query"); + } + if (value == null) { + boundValues.remove(varName); + } else { + boundValues.put(varName, ValueFormat.getQValue(value, mgrProvider.getNamePathResolver(), mgrProvider.getQValueFactory())); + } + } + + /** + * @see Query#setLimit(long) + */ + public void setLimit(long limit) { + this.limit = limit; + } + + /** + * @see Query#setOffset(long) + */ + public void setOffset(long offset) { + this.offset = offset; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryManagerImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryManagerImpl.java new file mode 100644 index 00000000000..1a3fd9b284c --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryManagerImpl.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.jcr2spi.query; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.ValueFactory; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.qom.QueryObjectModel; +import javax.jcr.query.qom.QueryObjectModelFactory; + +import org.apache.jackrabbit.commons.query.QueryObjectModelBuilder; +import org.apache.jackrabbit.commons.query.QueryObjectModelBuilderRegistry; +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.jcr2spi.WorkspaceManager; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.query.qom.QueryObjectModelFactoryImpl; +import org.apache.jackrabbit.spi.commons.query.qom.QueryObjectModelTree; + +/** + * This class implements the {@link QueryManager} interface. + */ +public class QueryManagerImpl implements QueryManager { + + /** + * The Session for this QueryManager. + */ + private final Session session; + + /** + * The value factory. + */ + private final ValueFactory valueFactory; + + /** + * Provides various managers. + */ + private final ManagerProvider mgrProvider; + + /** + * The ItemManager of for item retrieval in search results + */ + private final ItemManager itemMgr; + + /** + * The WorkspaceManager where queries are executed. + */ + private final WorkspaceManager wspManager; + + /** + * Creates a new QueryManagerImpl for the passed + * Session. + * + * @param session the current session. + * @param mgrProvider the manager provider. + * @param itemMgr the item manager of the current session. + * @param wspManager the workspace manager. + * @throws RepositoryException if an error occurs while initializing this + * query manager. + */ + public QueryManagerImpl(Session session, + ManagerProvider mgrProvider, + ItemManager itemMgr, + WorkspaceManager wspManager) throws RepositoryException { + this.session = session; + this.valueFactory = mgrProvider.getJcrValueFactory(); + this.mgrProvider = mgrProvider; + this.itemMgr = itemMgr; + this.wspManager = wspManager; + } + + /** + * @see QueryManager#createQuery(String, String) + */ + public Query createQuery(String statement, String language) + throws InvalidQueryException, RepositoryException { + checkIsAlive(); + return new QueryImpl(session, mgrProvider, itemMgr, wspManager, + statement, language, null); + } + + /** + * @see QueryManager#getQuery(Node) + */ + public Query getQuery(Node node) + throws InvalidQueryException, RepositoryException { + checkIsAlive(); + + NamePathResolver resolver = mgrProvider.getNamePathResolver(); + if (!node.isNodeType(resolver.getJCRName(NameConstants.NT_QUERY))) { + throw new InvalidQueryException("Node is not of type nt:query"); + } + if (node.getSession() != session) { + throw new InvalidQueryException("Node belongs to a different session."); + } + String statement = node.getProperty(resolver.getJCRName(NameConstants.JCR_STATEMENT)).getString(); + String language = node.getProperty(resolver.getJCRName(NameConstants.JCR_LANGUAGE)).getString(); + + if (Query.JCR_JQOM.equals(language)) { + QueryObjectModelFactory qomFactory = new QOMFactory(node, resolver); + QueryObjectModelBuilder builder = QueryObjectModelBuilderRegistry.getQueryObjectModelBuilder(language); + return builder.createQueryObjectModel(statement, qomFactory, valueFactory); + } else { + return new QueryImpl(session, mgrProvider, itemMgr, wspManager, + statement, language, node); + } + } + + /** + * @see QueryManager#getSupportedQueryLanguages() + */ + public String[] getSupportedQueryLanguages() throws RepositoryException { + return wspManager.getSupportedQueryLanguages(); + } + + /** + * @see QueryManager#getQOMFactory() + */ + public QueryObjectModelFactory getQOMFactory() { + return new QOMFactory(null, mgrProvider.getNamePathResolver()); + } + + //------------------------------------------------------------< private >--- + /** + * Checks if this QueryManagerImpl instance is still usable, + * otherwise throws a {@link javax.jcr.RepositoryException}. + * + * @throws RepositoryException if this query manager is not usable anymore, + * e.g. the corresponding session is closed. + */ + private void checkIsAlive() throws RepositoryException { + if (!session.isLive()) { + throw new RepositoryException("corresponding session has been closed"); + } + } + + private class QOMFactory extends QueryObjectModelFactoryImpl { + + private final Node node; + + public QOMFactory(Node node, NamePathResolver resolver) { + super(resolver); + this.node = node; + } + + @Override + protected QueryObjectModel createQuery(QueryObjectModelTree qomTree) + throws InvalidQueryException, RepositoryException { + return new QueryObjectModelImpl(session, mgrProvider, itemMgr, + wspManager, qomTree, node); + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryObjectModelImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryObjectModelImpl.java new file mode 100644 index 00000000000..716735c012a --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryObjectModelImpl.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.jcr2spi.query; + +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.query.InvalidQueryException; +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 javax.jcr.version.VersionException; + +import org.apache.jackrabbit.commons.query.QueryObjectModelBuilderRegistry; +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.jcr2spi.WorkspaceManager; +import org.apache.jackrabbit.spi.commons.query.qom.QueryObjectModelTree; + +/** + * QueryObjectModelImpl implements the jcr2spi query object model. + */ +public class QueryObjectModelImpl extends QueryImpl implements QueryObjectModel { + + /** + * The query object model tree. + */ + private final QueryObjectModelTree qomTree; + + public QueryObjectModelImpl(Session session, + ManagerProvider mgrProvider, + ItemManager itemMgr, + WorkspaceManager wspManager, + QueryObjectModelTree qomTree, + Node node) + throws InvalidQueryException, RepositoryException { + super(session, mgrProvider, itemMgr, wspManager, + getSQL2ForQOM(qomTree), Query.JCR_SQL2, node); + this.qomTree = qomTree; + } + + /** + * @return always {@link Query#JCR_JQOM}. + */ + @Override + public String getLanguage() { + return Query.JCR_JQOM; + } + + /** + * {@inheritDoc} + */ + public Source getSource() { + return qomTree.getSource(); + } + + /** + * {@inheritDoc} + */ + public Constraint getConstraint() { + return qomTree.getConstraint(); + } + + /** + * {@inheritDoc} + */ + public Ordering[] getOrderings() { + return qomTree.getOrderings(); + } + + /** + * {@inheritDoc} + */ + public Column[] getColumns() { + return qomTree.getColumns(); + } + + private static String getSQL2ForQOM(QueryObjectModelTree qomTree) + throws InvalidQueryException { + return QueryObjectModelBuilderRegistry.getQueryObjectModelBuilder(Query.JCR_JQOM).toString(new DummyQOM(qomTree)); + } + + private static class DummyQOM implements QueryObjectModel { + + /** + * The query object model tree. + */ + private final QueryObjectModelTree qomTree; + + public DummyQOM(QueryObjectModelTree qomTree) { + this.qomTree = qomTree; + } + + /** + * {@inheritDoc} + */ + public Source getSource() { + return qomTree.getSource(); + } + + /** + * {@inheritDoc} + */ + public Constraint getConstraint() { + return qomTree.getConstraint(); + } + + /** + * {@inheritDoc} + */ + public Ordering[] getOrderings() { + return qomTree.getOrderings(); + } + + /** + * {@inheritDoc} + */ + public Column[] getColumns() { + return qomTree.getColumns(); + } + + public QueryResult execute() + throws InvalidQueryException, RepositoryException { + throw new UnsupportedOperationException(); + } + + public void setLimit(long limit) { + throw new UnsupportedOperationException(); + } + + public void setOffset(long offset) { + throw new UnsupportedOperationException(); + } + + public String getStatement() { + throw new UnsupportedOperationException(); + } + + public String getLanguage() { + throw new UnsupportedOperationException(); + } + + public String getStoredQueryPath() + throws ItemNotFoundException, RepositoryException { + throw new UnsupportedOperationException(); + } + + public Node storeAsNode(String absPath) throws ItemExistsException, + PathNotFoundException, VersionException, + ConstraintViolationException, LockException, + UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedOperationException(); + } + + public void bindValue(String varName, Value value) + throws IllegalArgumentException, RepositoryException { + throw new UnsupportedOperationException(); + } + + public String[] getBindVariableNames() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryResultImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryResultImpl.java new file mode 100644 index 00000000000..fa1848f9f1f --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/QueryResultImpl.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.jcr2spi.query; + +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.spi.QueryInfo; + +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.QueryResult; +import javax.jcr.query.RowIterator; + +/** + * Implements the javax.jcr.query.QueryResult interface. + */ +class QueryResultImpl implements QueryResult { + + /** + * The item manager of the session executing the query + */ + private final ItemManager itemMgr; + + /** + * Provides various managers. + */ + private final ManagerProvider mgrProvider; + + /** + * The spi query result. + */ + private final QueryInfo queryInfo; + + /** + * Creates a new query result. + * + * @param itemMgr the item manager of the session executing the query. + * @param mgrProvider the manager provider. + * @param queryInfo the spi query result. + */ + QueryResultImpl(ItemManager itemMgr, + ManagerProvider mgrProvider, + QueryInfo queryInfo) { + this.itemMgr = itemMgr; + this.mgrProvider = mgrProvider; + this.queryInfo = queryInfo; + } + + /** + * {@inheritDoc} + */ + public String[] getSelectorNames() throws RepositoryException { + return queryInfo.getSelectorNames(); + } + + /** + * {@inheritDoc} + */ + public String[] getColumnNames() throws RepositoryException { + return queryInfo.getColumnNames(); + } + + /** + * {@inheritDoc} + */ + public NodeIterator getNodes() throws RepositoryException { + return getNodeIterator(); + } + + /** + * {@inheritDoc} + */ + public RowIterator getRows() throws RepositoryException { + return new RowIteratorImpl(queryInfo, mgrProvider.getNamePathResolver(), + mgrProvider.getJcrValueFactory(), itemMgr, + mgrProvider.getHierarchyManager()); + } + + /** + * Creates a node iterator over the result nodes. + * @return a node iterator over the result nodes. + */ + private ScoreNodeIterator getNodeIterator() { + return new NodeIteratorImpl(itemMgr, + mgrProvider.getHierarchyManager(), queryInfo); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/RowIteratorImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/RowIteratorImpl.java new file mode 100644 index 00000000000..2664348fce6 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/RowIteratorImpl.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.jcr2spi.query; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RangeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.query.Row; +import javax.jcr.query.RowIterator; + +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.QueryResultRow; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; + +/** + * Implements the {@link javax.jcr.query.RowIterator} interface returned by + * a {@link javax.jcr.query.QueryResult}. + */ +class RowIteratorImpl implements RowIterator { + + /** + * The result rows from the SPI implementation. + */ + private final RangeIterator rows; + + /** + * The column names. + */ + private final String[] columnNames; + + /** + * The NamePathResolver of the user Session. + */ + private final NamePathResolver resolver; + + /** + * The JCR value factory. + */ + private final ValueFactory vFactory; + + /** + * The item manager. + */ + private final ItemManager itemMgr; + + /** + * The hierarchy manager. + */ + private final HierarchyManager hmgr; + + /** + * Creates a new RowIteratorImpl that iterates over the result + * nodes. + * + * @param queryInfo the query info. + * @param resolver NameResolver of the user + * Session. + * @param vFactory the JCR value factory. + * @param itemMgr the item manager. + * @param hmgr the hierarchy manager. + */ + RowIteratorImpl(QueryInfo queryInfo, NamePathResolver resolver, + ValueFactory vFactory, ItemManager itemMgr, + HierarchyManager hmgr) { + this.rows = queryInfo.getRows(); + this.columnNames = queryInfo.getColumnNames(); + this.resolver = resolver; + this.vFactory = vFactory; + this.itemMgr = itemMgr; + this.hmgr = hmgr; + } + + //--------------------------------------------------------< RowIterator >--- + /** + * Returns the next Row in the iteration. + * + * @return the next Row in the iteration. + * @throws NoSuchElementException if iteration has no more Rows. + * @see RowIterator#nextRow() + */ + public Row nextRow() throws NoSuchElementException { + return new RowImpl((QueryResultRow) rows.next()); + } + + //------------------------------------------------------< RangeIterator >--- + /** + * Skip a number of Rows in this iterator. + * + * @param skipNum the non-negative number of Rows to skip + * @throws NoSuchElementException if skipped past the last Row + * in this iterator. + * @see javax.jcr.RangeIterator#skip(long) + */ + public void skip(long skipNum) throws NoSuchElementException { + rows.skip(skipNum); + } + + /** + * Returns the number of Rows in this iterator. + * + * @return the number of Rows in this iterator. + * @see RangeIterator#getSize() + */ + public long getSize() { + return rows.getSize(); + } + + /** + * Returns the current position within this iterator. The number + * returned is the 0-based index of the next Row in the iterator, + * i.e. the one that will be returned on the subsequent next call. + *

        + * Note that this method does not check if there is a next element, + * i.e. an empty iterator will always return 0. + * + * @return the current position withing this iterator. + * @see RangeIterator#getPosition() + */ + public long getPosition() { + return rows.getPosition(); + } + + /** + * @throws UnsupportedOperationException always. + * @see Iterator#remove() + */ + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + /** + * Returns true if the iteration has more Rows. + * (In other words, returns true if next would + * return an Row rather than throwing an exception.) + * + * @return true if the iterator has more elements. + * @see Iterator#hasNext() + */ + public boolean hasNext() { + return rows.hasNext(); + } + + /** + * Returns the next Row in the iteration. + * + * @return the next Row in the iteration. + * @throws NoSuchElementException if iteration has no more Rows. + * @see Iterator#next() + */ + public Object next() throws NoSuchElementException { + return nextRow(); + } + + //---------------------< inner class RowImpl >------------------------------ + /** + * Implements the {@link javax.jcr.query.Row} interface, which represents + * a row in the query result. + */ + class RowImpl implements Row { + + /** + * The underlying QueryResultRow. + */ + private final QueryResultRow row; + + /** + * Cached value array for returned by {@link #getValues()}. + */ + private Value[] values; + + /** + * Map of select property names. Key: String, Value: + * Integer, which refers to the array index in {@link #values}. + */ + private Map propertyMap; + + /** + * Creates a new RowImpl instance based on a SPI result + * row. + * + * @param row the underlying query result row + */ + private RowImpl(QueryResultRow row) { + this.row = row; + } + + //------------------------------------------------------------< Row >--- + /** + * Returns an array of all the values in the same order as the property + * names (column names) returned by + * {@link javax.jcr.query.QueryResult#getColumnNames()}. + * + * @return a Value array. + * @throws RepositoryException if an error occurs while retrieving the + * values from the Node. + * @see Row#getValues() + */ + public Value[] getValues() throws RepositoryException { + if (values == null) { + QValue[] qVals = row.getValues(); + Value[] tmp = new Value[qVals.length]; + for (int i = 0; i < qVals.length; i++) { + if (qVals[i] == null) { + tmp[i] = null; + } else { + tmp[i] = ValueFormat.getJCRValue( + qVals[i], resolver, vFactory); + } + } + values = tmp; + } + // return a copy of the array + Value[] ret = new Value[values.length]; + System.arraycopy(values, 0, ret, 0, values.length); + return ret; + } + + /** + * Returns the value of the indicated property in this Row. + *

        + * If propertyName is not among the column names of the + * query result table, an ItemNotFoundException is thrown. + * + * @return a Value + * @throws ItemNotFoundException if propertyName is not + * among the column names of the query result table. + * @throws RepositoryException if propertyName is not a + * valid property name. + * @see Row#getValue(String) + */ + public Value getValue(String propertyName) throws ItemNotFoundException, RepositoryException { + if (propertyMap == null) { + // create the map first + Map tmp = new HashMap(); + for (int i = 0; i < columnNames.length; i++) { + tmp.put(columnNames[i], i); + } + propertyMap = tmp; + } + try { + Integer idx = propertyMap.get(propertyName); + if (idx == null) { + throw new ItemNotFoundException(propertyName); + } + // make sure values are there + if (values == null) { + getValues(); + } + return values[idx]; + } catch (NameException e) { + throw new RepositoryException(e.getMessage(), e); + } + } + + /** + * @see Row#getNode() + */ + public Node getNode() throws RepositoryException { + return getNode(row.getNodeId(null)); + } + + /** + * @see Row#getNode(String) + */ + public Node getNode(String selectorName) throws RepositoryException { + return getNode(row.getNodeId(selectorName)); + } + + /** + * @see Row#getPath() + */ + public String getPath() throws RepositoryException { + String path = null; + Node n = getNode(); + if (n != null) { + path = n.getPath(); + } + return path; + } + + /** + * @see Row#getPath(String) + */ + public String getPath(String selectorName) throws RepositoryException { + String path = null; + Node n = getNode(selectorName); + if (n != null) { + path = n.getPath(); + } + return path; + } + + /** + * @see Row#getScore() + */ + public double getScore() throws RepositoryException { + return row.getScore(null); + } + + /** + * @see Row#getScore(String) + */ + public double getScore(String selectorName) throws RepositoryException { + return row.getScore(selectorName); + } + + /** + * Returns the node with the given id or null + * if id is null. + * + * @param id a node id or null. + * @return the node with the given id or null. + * @throws RepositoryException if an error occurs while retrieving the + * node. + */ + private Node getNode(NodeId id) throws RepositoryException { + Node node = null; + if (id != null) { + node = (Node) itemMgr.getItem(hmgr.getNodeEntry(id)); + } + return node; + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/ScoreNodeIterator.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/ScoreNodeIterator.java new file mode 100644 index 00000000000..a7bd35e2417 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/query/ScoreNodeIterator.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.jcr2spi.query; + +import javax.jcr.NodeIterator; + +/** + * Extends the {@link javax.jcr.NodeIterator} interface by adding a {@link + * #getScore()} method that returns the score for the node that is returned by + * {@link javax.jcr.NodeIterator#nextNode()}. + */ +public interface ScoreNodeIterator extends NodeIterator { + + /** + * Returns the score of the node returned by {@link #nextNode()}. In other + * words, this method returns the score value of the next + * Node. + * + * @return the score of the node returned by {@link #nextNode()}. + * @throws java.util.NoSuchElementException if there is no next node. + */ + public double getScore(); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/AccessManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/AccessManager.java new file mode 100644 index 00000000000..77e76a9d6ae --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/AccessManager.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.jcr2spi.security; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.jcr2spi.state.NodeState; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.RepositoryException; + +/** + * The AccessManager can be queried to determines whether permission + * is granted to perform a specific action on a specific item. + */ +public interface AccessManager { + + /** + * predefined action constants + */ + public String READ_ACTION = javax.jcr.Session.ACTION_READ; + public String REMOVE_ACTION = javax.jcr.Session.ACTION_REMOVE; + public String ADD_NODE_ACTION = javax.jcr.Session.ACTION_ADD_NODE; + public String SET_PROPERTY_ACTION = javax.jcr.Session.ACTION_SET_PROPERTY; + + public String[] READ = new String[] {READ_ACTION}; + public String[] REMOVE = new String[] {REMOVE_ACTION}; + + /** + * Determines whether the specified permissions are granted + * on the item with the specified path. + * + * @param parentState The node state of the next existing ancestor. + * @param relPath The relative path pointing to the non-existing target item. + * @param actions An array of actions that need to be checked. + * @return true if the actions are granted; otherwise false + * @throws ItemNotFoundException if the target item does not exist + * @throws RepositoryException if another error occurs + */ + boolean isGranted(NodeState parentState, Path relPath, String[] actions) throws ItemNotFoundException, RepositoryException; + + /** + * Determines whether the specified permissions are granted + * on the item with the specified path. + * + * @param itemState + * @param actions An array of actions that need to be checked. + * @return true if the actions are granted; otherwise false + * @throws ItemNotFoundException if the target item does not exist + * @throws RepositoryException if another error occurs + */ + boolean isGranted(ItemState itemState, String[] actions) throws ItemNotFoundException, RepositoryException; + + + /** + * Returns true if the existing item with the given ItemId can + * be read. + * + * @param itemState + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + boolean canRead(ItemState itemState) throws ItemNotFoundException, RepositoryException; + + /** + * Returns true if the existing item state can be removed. + * + * @param itemState + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + boolean canRemove(ItemState itemState) throws ItemNotFoundException, RepositoryException; + + /** + * Determines whether the subject of the current context is granted access + * to the given workspace. + * + * @param workspaceName name of workspace + * @return true if the subject of the current context is + * granted access to the given workspace; otherwise false. + * @throws NoSuchWorkspaceException if a workspace with the given name does not exist. + * @throws RepositoryException if another error occurs + */ + boolean canAccess(String workspaceName) throws NoSuchWorkspaceException, RepositoryException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/SecurityConstants.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/SecurityConstants.java new file mode 100644 index 00000000000..9054a6870c1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/SecurityConstants.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.jcr2spi.security; + +/** + * This interface defines miscellaneous security related constants. + */ +public interface SecurityConstants { + + /** + * Name of the internal SimpleCredentials attribute where + * the Subject of the impersonating Session + * is stored. + * + * @see javax.jcr.Session#impersonate(javax.jcr.Credentials) + */ + String IMPERSONATOR_ATTRIBUTE = + "org.apache.jackrabbit.jcr2spi.security.impersonator"; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/AccessControlProvider.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/AccessControlProvider.java new file mode 100644 index 00000000000..081b461e51f --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/AccessControlProvider.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.jcr2spi.security.authorization; + +import java.util.Map; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.state.UpdatableItemStateManager; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * AccessControlProvider... TODO + */ +public interface AccessControlProvider { + + void init(RepositoryConfig config) throws RepositoryException; + + /** + * The privileges corresponding to the specified name. + * + * @param sessionInfo + * @param resolver + * @return + * @see javax.jcr.security.AccessControlManager#privilegeFromName(String) + */ + Privilege privilegeFromName(SessionInfo sessionInfo, NamePathResolver resolver, String privilegeName) throws RepositoryException; + + /** + * Obtain the privileges supported at the specified path. + * + * @param sessionInfo + * @param nodeId The id of an existing node or {@code null} to obtain privileges + * that are supported for repository level access. + * @param npResolver + * @return + * @throws RepositoryException + * @see javax.jcr.security.AccessControlManager#getSupportedPrivileges(String) + */ + Map getSupportedPrivileges(SessionInfo sessionInfo, NodeId nodeId, NamePathResolver npResolver) throws RepositoryException; + + Set getPrivileges(SessionInfo sessionInfo, NodeId id, NamePathResolver npResolver) throws RepositoryException; + + AccessControlManager createAccessControlManager(SessionInfo sessionInfo, + UpdatableItemStateManager itemStateManager, + ItemManager itemManager, + ItemDefinitionProvider definitionProvider, + HierarchyManager hierarchyManager, + NamePathResolver npResolver) throws RepositoryException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/AccessControlProviderStub.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/AccessControlProviderStub.java new file mode 100644 index 00000000000..23ace3a6c02 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/AccessControlProviderStub.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.jcr2spi.security.authorization; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Stub class that provide clients with access to a concrete + * AccessControlProvider implementation. TODO: Explain the way the concrete + * provider is located, loaded and instantiated. + * + */ +public class AccessControlProviderStub { + + private static Logger log = LoggerFactory.getLogger(AccessControlProviderStub.class); + + /** + * The class property parameter determines the {@link AccessControlProvider} + * to load and instantiate. This is a fall-back parameter if the + * SYS_PROP_AC_PROVIDER_IMPL is not set. + */ + private static final String ACCESS_CONTROL_PROVIDER_PROPERTIES = "accessControlProvider.properties"; + + /** + * Key look-up. + */ + private static final String PROPERTY_ACCESSCONTROL_PROVIDER_CLASS = "org.apache.jackrabbit.jcr2spi.AccessControlProvider.class"; + + /** + * Avoid instantiation. + */ + private AccessControlProviderStub() { + } + + /** + * Instantiates and returns a concrete AccessControlProvider implementation. + * + * @param config + * The RepositoryConfig to read configuration parameters. + * @return + * @throws RepositoryException + */ + public static AccessControlProvider newInstance(RepositoryConfig config) throws RepositoryException { + + String className = getProviderClass(config); + if (className != null) { + try { + Class acProviderClass = Class.forName(className); + if (AccessControlProvider.class.isAssignableFrom(acProviderClass)) { + AccessControlProvider acProvider = (AccessControlProvider) acProviderClass.newInstance(); + acProvider.init(config); + return acProvider; + } else { + throw new RepositoryException("Fail to create AccessControlProvider from configuration."); + } + } catch (Exception e) { + throw new RepositoryException("Fail to create AccessControlProvider from configuration."); + } + } + + // ac not supported in this setup. + throw new UnsupportedRepositoryOperationException("Access control is not supported"); + } + + private static String getProviderClass(RepositoryConfig config) throws RepositoryException { + + String implClass = config.getConfiguration(PROPERTY_ACCESSCONTROL_PROVIDER_CLASS, null); + + if (implClass != null) { + return implClass; + } else { + try { + // not configured try to load as resource + Properties prop = new Properties(); + InputStream is = AccessControlProviderStub.class.getClassLoader().getResourceAsStream(ACCESS_CONTROL_PROVIDER_PROPERTIES); + if (is != null) { + prop.load(is); + // loads the concrete class to instantiate. + if (prop.containsKey(PROPERTY_ACCESSCONTROL_PROVIDER_CLASS)) { + return prop.getProperty(PROPERTY_ACCESSCONTROL_PROVIDER_CLASS); + } else { + log.debug("Missing AccessControlProvider configuration."); + } + } else { + log.debug("Fail to locate the access control provider properties file."); + } + } catch (IOException e) { + throw new RepositoryException("Fail to load AccessControlProvider configuration."); + } + } + return null; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/PrivilegeImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/PrivilegeImpl.java new file mode 100644 index 00000000000..745375495f8 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/PrivilegeImpl.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.jcr2spi.security.authorization; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +public class PrivilegeImpl implements Privilege { + + private final PrivilegeDefinition definition; + + private final Privilege[] declaredAggregates; + private final Privilege[] aggregates; + private static final Privilege[] EMPTY_ARRAY = new Privilege[0]; + + private final NamePathResolver npResolver; + + public PrivilegeImpl(PrivilegeDefinition definition, PrivilegeDefinition[] allDefs, NamePathResolver npResolver) throws RepositoryException { + this.definition = definition; + this.npResolver = npResolver; + + Set set = definition.getDeclaredAggregateNames(); + Name[] declAggrNames = set.toArray(new Name[set.size()]); + if (declAggrNames.length == 0) { + declaredAggregates = EMPTY_ARRAY; + aggregates = EMPTY_ARRAY; + } else { + declaredAggregates = new Privilege[declAggrNames.length]; + for (int i = 0; i < declAggrNames.length; i++) { + for (PrivilegeDefinition def : allDefs) { + if (def.getName().equals(declAggrNames[i])) { + declaredAggregates[i] = new PrivilegeImpl(def, allDefs, npResolver); + } + } + } + + Set aggr = new HashSet(); + for (Privilege decl : declaredAggregates) { + aggr.add(decl); + if (decl.isAggregate()) { + aggr.addAll(Arrays.asList(decl.getAggregatePrivileges())); + } + } + aggregates = aggr.toArray(new Privilege[aggr.size()]); + } + } + + /** + * @see Privilege#getName() + */ + public String getName() { + try { + return npResolver.getJCRName(definition.getName()); + } catch (NamespaceException e) { + // should not occur -> return internal name representation. + return definition.getName().toString(); + } + } + + /** + * @see Privilege#isAbstract() + */ + public boolean isAbstract() { + return definition.isAbstract(); + } + + /** + * @see Privilege#isAggregate() + */ + public boolean isAggregate() { + return declaredAggregates.length > 0; + } + + /** + * @see Privilege#getDeclaredAggregatePrivileges() + */ + public Privilege[] getDeclaredAggregatePrivileges() { + return declaredAggregates; + } + + /** + * @see Privilege#getAggregatePrivileges() + */ + public Privilege[] getAggregatePrivileges() { + return aggregates; + } + + //---------------------------------------------------------< Object >--- + @Override + public String toString() { + return getName(); + } + + @Override + public int hashCode() { + return definition.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof PrivilegeImpl) { + PrivilegeImpl other = (PrivilegeImpl) obj; + return definition.equals(other.definition); + } + return false; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/AccessControlConstants.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/AccessControlConstants.java new file mode 100644 index 00000000000..45ae7b3015e --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/AccessControlConstants.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.jcr2spi.security.authorization.jackrabbit; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +public interface AccessControlConstants { + /** + * Default name for a node of type rep:Policy. + */ + Name N_POLICY = NameConstants.REP_POLICY; + /** + * Name for a node of type 'rep:repoPolicy'. + */ + Name N_REPO_POLICY = NameConstants.REP_REPO_POLICY; + /** + * rep:RepoAccessControllable node type. + */ + Name NT_REP_REPO_ACCESS_CONTROLLABLE = NameConstants.REP_REPO_ACCESS_CONTROLLABLE; + /** + * rep:AccessControllable nodetype + */ + Name NT_REP_ACCESS_CONTROLLABLE = NameConstants.REP_ACCESS_CONTROLLABLE; + + Name NT_REP_GRANT_ACE = NameConstants.REP_GRANT_ACE; + + Name NT_REP_DENY_ACE = NameConstants.REP_DENY_ACE; + + Name NT_REP_ACL = NameConstants.REP_ACL; + + /** + * rep:principalName + */ + Name N_REP_PRINCIPAL_NAME = NameConstants.REP_PRINCIPAL_NAME; + + /** + * rep:glob property name used to restrict the number of child nodes + * or properties that are affected by an ACL inherited from a parent node. + */ + Name P_GLOB = NameConstants.REP_GLOB; + + /** + * rep:privileges + */ + Name N_REP_PRIVILEGES = NameConstants.REP_PRIVILEGES; + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlEntryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlEntryImpl.java new file mode 100644 index 00000000000..f1affbe5a95 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlEntryImpl.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.acl; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.QValueValue; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.value.ValueHelper; + +class AccessControlEntryImpl implements JackrabbitAccessControlEntry { + + /* + * The principal this entry has been created for. + */ + private final Principal principal; + + /* + * The privileges in this entry. + */ + private final Privilege[] privileges; + + /* + * Whether this entry is allowed/denied + */ + private final boolean isAllow; + + /* + * Restrictions that may apply with this entry. + */ + private final Map restrictions; + private final Map> mvRestrictions; + + private final NamePathResolver resolver; + + private final QValueFactory qvf; + + private int hashCode = -1; + private int privsHashCode = -1; + + /** + * + * @param principal + * @param privileges + * @param isAllow + * @param restrictions + * @throws RepositoryException + */ + AccessControlEntryImpl(Principal principal, Privilege[] privileges, boolean isAllow, + Map restrictions, Map> mvRestrictions, + NamePathResolver resolver, QValueFactory qvf) throws RepositoryException { + if (principal == null || (privileges != null && privileges.length == 0)) { + throw new AccessControlException("An Entry must not have a NULL principal or empty privileges"); + } + checkAbstract(privileges); + + this.principal = principal; + this.privileges = privileges; + this.isAllow = isAllow; + this.resolver = resolver; + this.qvf = qvf; + + if (restrictions == null) { + this.restrictions = Collections.emptyMap(); + } else { + this.restrictions = restrictions; + } + if (mvRestrictions == null) { + this.mvRestrictions = Collections.emptyMap(); + } else { + this.mvRestrictions = mvRestrictions; + } + } + + @Override + public Principal getPrincipal() { + return principal; + } + + @Override + public Privilege[] getPrivileges() { + return privileges; + } + + @Override + public boolean isAllow() { + return isAllow; + } + + @Override + public String[] getRestrictionNames() throws RepositoryException { + List restNames = new ArrayList(restrictions.size()); + for (Name restName : restrictions.keySet()) { + restNames.add(resolver.getJCRName(restName)); + } + return restNames.toArray(new String[restNames.size()]); + } + + @Override + public Value getRestriction(String restrictionName) + throws ValueFormatException, RepositoryException { + try { + Name restName = resolver.getQName(restrictionName); + if (!restrictions.containsKey(restName)) { + return null; + } + return createJcrValue(restrictions.get(restName)); + } catch (IllegalStateException e) { + throw new RepositoryException(e.getMessage()); + } + } + + /* + * As of Jackrabbit 2.8, this extention has been added to the Jackrabbit API. + * However, Jackrabbit (before) OAK doesn't support mv. restrictions. Thus simply + * return an array containing the single restriction value. + */ + @Override + public Value[] getRestrictions(String restrictionName) + throws RepositoryException { + return new Value[] {getRestriction(restrictionName)}; + } + + //-------------------------------------------------------------< Object >--- + @Override + public int hashCode() { + if (hashCode == -1) { + hashCode = buildHashCode(); + } + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof AccessControlEntryImpl) { + AccessControlEntryImpl other = (AccessControlEntryImpl) obj; + return principal.getName().equals(other.principal.getName()) && + isAllow == other.isAllow && + restrictions.equals(other.restrictions) && + mvRestrictions.equals(other.mvRestrictions) && + getPrivilegesHashCode() == other.getPrivilegesHashCode(); + } + return false; + } + + //-------------------------------------------------------------< private >--- + private int buildHashCode() { + int h = 17; + h = 37 * h + principal.getName().hashCode(); + h = 37 * h + getPrivilegesHashCode(); + h = 37 * h + Boolean.valueOf(isAllow).hashCode(); + h = 37 * h + restrictions.hashCode(); + h = 37 * h + mvRestrictions.hashCode(); + return h; + } + + private int getPrivilegesHashCode() { + if (privsHashCode == -1) { + Set prvs = new HashSet(Arrays.asList(privileges)); + for (Privilege p : privileges) { + if (p.isAggregate()) { + prvs.addAll(Arrays.asList(p.getAggregatePrivileges())); + } + } + privsHashCode = prvs.hashCode(); + } + return privsHashCode; + } + + private void checkAbstract(Privilege[] privileges) throws AccessControlException { + for (Privilege privilege : privileges) { + if (privilege.isAbstract()) { + throw new AccessControlException("An Entry cannot contain abstract privileges."); + } + } + } + + /** + * Creates a jcr Value from the given qvalue using the specified + * factory. + * @return the jcr value representing the qvalue. + */ + private Value createJcrValue(QValue qValue) throws RepositoryException { + + // build ValueFactory + ValueFactoryQImpl valueFactory = new ValueFactoryQImpl(qvf, resolver); + + // build jcr value + QValueValue jcrValue = new QValueValue(qValue, resolver); + + return ValueHelper.copy(jcrValue, valueFactory); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlListImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlListImpl.java new file mode 100644 index 00000000000..33c41b581b8 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlListImpl.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.jcr2spi.security.authorization.jackrabbit.acl; + +import java.security.Principal; +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 java.util.Map; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.AccessControlConstants; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class AccessControlListImpl implements JackrabbitAccessControlList, AccessControlConstants { + + private static final Logger log = LoggerFactory.getLogger(AccessControlListImpl.class); + + private final String jcrPath; + + private final List entries; + private final QValueFactory qValueFactory; + private final NamePathResolver resolver; + + /** + * Namespace sensitive name of the REP_GLOB property in standard JCR form. + */ + private final String jcrRepGlob; + + public AccessControlListImpl(String jcrPath, NamePathResolver resolver, QValueFactory qValueFactory) throws RepositoryException { + this.jcrPath = jcrPath; + this.entries = new ArrayList(); + this.qValueFactory = qValueFactory; + this.resolver = resolver; + + try { + jcrRepGlob = resolver.getJCRName(P_GLOB); + } catch (NamespaceException e) { + throw new RepositoryException(e.getMessage()); + } + } + + AccessControlListImpl(NodeState aclNode, String aclPath, NamePathResolver resolver, QValueFactory factory, AccessControlManager acm) throws RepositoryException { + this(aclPath, resolver, factory); + + NodeEntry entry = (NodeEntry) aclNode.getHierarchyEntry(); + Iterator it = entry.getNodeEntries(); + + while(it.hasNext()) { + NodeState aceNode = it.next().getNodeState(); + try { + + PropertyState ps = aceNode.getPropertyState(N_REP_PRINCIPAL_NAME); + + // rep:principal property + String principalName = ps.getValue().getString(); + Principal principal = createPrincipal(principalName); + + // rep:privileges property + ps = aceNode.getPropertyState(N_REP_PRIVILEGES); + + QValue[] values = ps.getValues(); + Privilege[] privileges = new Privilege[values.length]; + for (int i = 0; i < values.length; i++) { + privileges[i] = acm.privilegeFromName(values[i].getString()); + } + + // rep:glob property -> restrictions + Map restrictions = null; + if (aceNode.hasPropertyName(P_GLOB)) { + ps = aceNode.getPropertyState(P_GLOB); + restrictions = Collections.singletonMap(ps.getName(), ps.getValue()); + } + + // the isAllow flag + boolean isAllow = NT_REP_GRANT_ACE.equals(aceNode.getNodeTypeName()); + // build the entry + AccessControlEntry ace = new AccessControlEntryImpl(principal, privileges, isAllow, restrictions, Collections.EMPTY_MAP, resolver, qValueFactory); + entries.add(ace); + } catch (RepositoryException e) { + log.debug("Fail to create Entry for "+ aceNode.getName().toString()); + } + } + } + + //--------------------------------------------------< AccessControlList >--- + @Override + public AccessControlEntry[] getAccessControlEntries() + throws RepositoryException { + return entries.toArray(new AccessControlEntry[entries.size()]); + } + + @Override + public boolean addAccessControlEntry(Principal principal, Privilege[] privileges) throws AccessControlException, RepositoryException { + return addEntry(principal, privileges, true, Collections.emptyMap()); + } + + @Override + public void removeAccessControlEntry(AccessControlEntry ace) + throws AccessControlException, RepositoryException { + if (entries.contains(ace)) { + entries.remove(ace); + } else { + throw new AccessControlException("Entry not present in this list"); + } + } + + //----------------------------------------< JackrabbitAccessControlList >--- + @Override + public String getPath() { + return jcrPath; + } + + @Override + public boolean isEmpty() { + return entries.isEmpty(); + } + + @Override + public int size() { + return entries.size(); + } + + @Override + public boolean addEntry(Principal principal, Privilege[] privileges, + boolean isAllow) throws AccessControlException, RepositoryException { + return addEntry(principal, privileges, isAllow, Collections.emptyMap()); + } + + @Override + public boolean addEntry(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions, + Map mvRestrictions) throws AccessControlException, + RepositoryException { + + + // create entry to be added + Map rs = createRestrictions(restrictions); + Map> mvRs = createMvRestrictions(mvRestrictions); + AccessControlEntry entry = createEntry(principal, privileges, isAllow, rs, mvRs); + + return entries.add(entry); + + } + + @Override + public boolean addEntry(Principal principal, Privilege[] privileges, + boolean isAllow, Map restrictions) + throws AccessControlException, RepositoryException { + return addEntry(principal, privileges, isAllow, restrictions, Collections.EMPTY_MAP); + } + + @Override + public String[] getRestrictionNames() throws RepositoryException { + return new String[] {jcrRepGlob}; + } + + @Override + public int getRestrictionType(String restrictionName) throws RepositoryException { + if (!jcrRepGlob.equals(restrictionName)) { + // JR2 feature + return PropertyType.UNDEFINED; + } + return PropertyType.STRING; + } + + @Override + public boolean isMultiValueRestriction(String restrictionName) throws RepositoryException { + return false; + } + + @Override + public void orderBefore(AccessControlEntry srcEntry, + AccessControlEntry destEntry) throws AccessControlException, + UnsupportedRepositoryOperationException, RepositoryException { + // TODO + throw new UnsupportedRepositoryOperationException("not yet implemented"); + } + + //-------------------------------------------------------------< Object >--- + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return 0; + } + + /** + * Returns true if the path and the entries are equal; false otherwise. + * + * @param obj Object to be tested. + * @return true if the path and the entries are equal; false otherwise. + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof AccessControlListImpl) { + AccessControlListImpl acl = (AccessControlListImpl) obj; + return jcrPath.equals(acl.jcrPath) && entries.equals(acl.entries); + } + return false; + } + + //------------------------------------------------------------< private >--- + private AccessControlEntry createEntry(Principal principal, Privilege[] privileges, boolean isAllow, + Map restrictions, Map> mvRestrictions) throws RepositoryException { + return new AccessControlEntryImpl(principal, privileges, isAllow, restrictions, mvRestrictions, resolver, qValueFactory); + } + + private Map createRestrictions(Map restrictions) throws RepositoryException { + Map rs = new HashMap(restrictions.size()); + for (String restName : restrictions.keySet()) { + Value v = restrictions.get(restName); + rs.put(resolver.getQName(restName), ValueFormat.getQValue(v, resolver, qValueFactory)); + } + return rs; + } + + private Map> createMvRestrictions(Map restrictions) throws RepositoryException { + Map> rs = new HashMap>(restrictions.size()); + for (String restName : restrictions.keySet()) { + QValue[] qvs = ValueFormat.getQValues(restrictions.get(restName), resolver, qValueFactory); + rs.put(resolver.getQName(restName), Arrays.asList(qvs)); + } + return rs; + } + + private static Principal createPrincipal(final String name) { + return new Principal() { + @Override + public String getName() { + return name; + } + }; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlManagerImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlManagerImpl.java new file mode 100644 index 00000000000..985165ba909 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlManagerImpl.java @@ -0,0 +1,440 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.acl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.AccessDeniedException; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.commons.iterator.AccessControlPolicyIteratorAdapter; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.operation.AddNode; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.Remove; +import org.apache.jackrabbit.jcr2spi.operation.SetMixin; +import org.apache.jackrabbit.jcr2spi.operation.SetTree; +import org.apache.jackrabbit.jcr2spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.AccessControlConstants; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.UpdatableItemStateManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameParser; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Jackrabbit-core specific implementation of the {@code AccessControlManager}. + */ +class AccessControlManagerImpl implements AccessControlManager, AccessControlConstants { + + private static final Logger log = LoggerFactory.getLogger(AccessControlManagerImpl.class); + + private static int REMOVE_POLICY_OPTIONS = + ItemStateValidator.CHECK_ACCESS | + ItemStateValidator.CHECK_LOCK | + ItemStateValidator.CHECK_COLLISION | + ItemStateValidator.CHECK_VERSIONING; + + private final SessionInfo sessionInfo; + private final HierarchyManager hierarchyManager; + private final NamePathResolver npResolver; + private final QValueFactory qvf; + private final AccessControlProvider acProvider; + private final UpdatableItemStateManager itemStateMgr; + private final ItemDefinitionProvider definitionProvider; + + AccessControlManagerImpl(SessionInfo sessionInfo, + UpdatableItemStateManager itemStateMgr, + ItemDefinitionProvider definitionProvider, + HierarchyManager hierarchyManager, + NamePathResolver npResolver, + QValueFactory qvf, + AccessControlProvider acProvider) { + this.sessionInfo = sessionInfo; + this.hierarchyManager = hierarchyManager; + this.itemStateMgr = itemStateMgr; + this.npResolver = npResolver; + this.qvf = qvf; + this.acProvider = acProvider; + this.definitionProvider = definitionProvider; + } + + public Privilege[] getSupportedPrivileges(String absPath) throws PathNotFoundException, RepositoryException { + NodeState state = getNodeState(npResolver.getQPath(absPath)); + Map privileges = acProvider.getSupportedPrivileges(sessionInfo, state.getNodeId(), npResolver); + return privileges.values().toArray(new Privilege[privileges.size()]); + } + + public Privilege privilegeFromName(String privilegeName) throws AccessControlException, RepositoryException { + return acProvider.privilegeFromName(sessionInfo, npResolver, privilegeName); + } + + public boolean hasPrivileges(String absPath, Privilege[] privileges) throws PathNotFoundException, RepositoryException { + Set privs = acProvider.getPrivileges(sessionInfo, getNodeState(npResolver.getQPath(absPath)).getNodeId(), npResolver); + List toTest = Arrays.asList(privileges); + if (privs.containsAll(toTest)) { + return true; + } else { + Set agg = new HashSet(privs); + for (Privilege p : privs) { + if (p.isAggregate()) { + agg.addAll(Arrays.asList(p.getAggregatePrivileges())); + } + } + return agg.containsAll(toTest); + } + } + + public Privilege[] getPrivileges(String absPath) throws PathNotFoundException, RepositoryException { + Set privs = acProvider.getPrivileges(sessionInfo, getNodeState(npResolver.getQPath(absPath)).getNodeId(), npResolver); + return privs.toArray(new Privilege[privs.size()]); + } + + public AccessControlPolicy[] getEffectivePolicies(String absPath) throws RepositoryException { + checkValidNodePath(absPath); + checkAccessControlRead(absPath); + + // TODO : add proper implementation + return new AccessControlPolicy[] {new AccessControlPolicy() {}}; + } + + public AccessControlPolicyIterator getApplicablePolicies(String absPath) throws RepositoryException { + checkValidNodePath(absPath); + + AccessControlPolicy[] applicable = getApplicable(absPath); + if (applicable != null && applicable.length > 0) { + return new AccessControlPolicyIteratorAdapter(Arrays.asList(applicable)); + } else { + return AccessControlPolicyIteratorAdapter.EMPTY; + } + } + + public AccessControlPolicy[] getPolicies(String absPath) throws RepositoryException { + checkValidNodePath(absPath); + + List policies = new ArrayList(); + NodeState aclNode = getAclNode(absPath); + AccessControlList acl; + + if (aclNode != null) { + acl = new AccessControlListImpl(aclNode, absPath, npResolver, qvf, this); + policies.add(acl); + } + return policies.toArray(new AccessControlList[policies.size()]); + } + + public void setPolicy(String absPath, AccessControlPolicy policy) throws RepositoryException { + checkValidNodePath(absPath); + checkValidPolicy(policy); + checkAcccessControlItem(absPath); + + SetTree operation; + NodeState aclNode = getAclNode(absPath); + if (aclNode == null) { + // policy node doesn't exist at absPath -> create one. + Name name = (absPath == null) ? N_REPO_POLICY : N_POLICY; + + NodeState parent = null; + Name mixinType = null; + if (absPath == null) { + parent = getRootNodeState(); + mixinType = NT_REP_REPO_ACCESS_CONTROLLABLE; + } else { + parent = getNodeState(absPath); + mixinType = NT_REP_ACCESS_CONTROLLABLE; + } + setMixin(parent, mixinType); + + operation = SetTree.create(itemStateMgr, parent, name, NT_REP_ACL, null); + aclNode = operation.getTreeState(); + } else { + Iterator it = getNodeEntry(aclNode).getNodeEntries(); + while(it.hasNext()) { + it.next().transientRemove(); + } + operation = SetTree.create(aclNode); + } + + // create the entry nodes + for (AccessControlEntry entry : ((AccessControlListImpl) policy).getAccessControlEntries()) { + createAceNode(operation, aclNode, entry); + } + + itemStateMgr.execute(operation); + } + + public void removePolicy(String absPath, AccessControlPolicy policy) throws RepositoryException { + checkValidNodePath(absPath); + checkValidPolicy(policy); + + NodeState aclNode = getAclNode(absPath); + if (aclNode != null) { + removeNode(aclNode); + } else { + throw new AccessControlException("No policy exist at "+absPath); + } + } + + //--------------------------------------------------< private >--- + private AccessControlPolicy[] getApplicable(String absPath) throws RepositoryException { + NodeState controlledState; + if (absPath == null) { + controlledState = getRootNodeState(); + } else { + controlledState = getNodeState(absPath); + } + + AccessControlPolicy acl = null; + NodeState aclNode = getAclNode(controlledState, absPath); + if (aclNode == null) { + acl = new AccessControlListImpl(absPath, npResolver, qvf); + } + + return (acl == null) ? new AccessControlPolicy[0] : new AccessControlPolicy[] {acl}; + } + + private NodeState getAclNode(String controlledNodePath) throws RepositoryException { + NodeState controlledNode; + if (controlledNodePath == null) { + controlledNode = getRootNodeState(); + } else { + controlledNode = getNodeState(controlledNodePath); + } + return getAclNode(controlledNode, controlledNodePath); + } + + private NodeState getAclNode(NodeState aclNode, String controlledNodePath) throws RepositoryException { + NodeState acl = null; + if (controlledNodePath == null) { + if (isRepoAccessControlled(aclNode)) { + acl = aclNode.getChildNodeState(N_REPO_POLICY, 1); + } + } else { + if (isAccessControlled(aclNode)) { + acl = aclNode.getChildNodeState(N_POLICY, 1); + } + } + return acl; + } + + /** + * Test if the given node state is of node type + * {@link AccessControlConstants#NT_REP_REPO_ACCESS_CONTROLLABLE} + * and if it has a child node named + * {@link AccessControlConstants#N_REPO_POLICY}. + * + * @param nodeState the node state to be tested + * @return true if the node is access controlled and has a + * rep:policy child; false otherwise. + * @throws RepositoryException if an error occurs + */ + private boolean isRepoAccessControlled(NodeState nodeState) throws RepositoryException { + return isNodeType(nodeState, NT_REP_REPO_ACCESS_CONTROLLABLE) && + nodeState.hasChildNodeEntry(N_REPO_POLICY, 1); + } + + private boolean isAccessControlled(NodeState nodeState) throws RepositoryException { + return isNodeType(nodeState, NT_REP_ACCESS_CONTROLLABLE) && + nodeState.hasChildNodeEntry(N_POLICY, 1); + } + + /** + * Checks if the given node state has the specified mixin. + * NOTE: we take the transiently added mixins + * into consideration e.g if added during + * a setPolicies call and the changes are yet to be saved. + * @param nodeState + * @param mixinName + */ + private boolean isNodeType(NodeState nodeState, Name mixinName) throws RepositoryException { + List lst = Arrays.asList(nodeState.getAllNodeTypeNames()); + return (lst == null) ? false : lst.contains(mixinName); + } + + /** + * Checks whether if the given nodePath points to an access + * control policy or entry node. + * @param nodePath + * @throws AccessControlException + * @throws RepositoryException + */ + private void checkAcccessControlItem(String nodePath) throws AccessControlException, RepositoryException { + NodeState controlledState = getNodeState(nodePath); + Name ntName = controlledState.getNodeTypeName(); + boolean isAcItem = ntName.equals(NT_REP_ACL) || + ntName.equals(NT_REP_GRANT_ACE) || + ntName.equals(NT_REP_DENY_ACE); + if (isAcItem) { + throw new AccessControlException("The path: "+nodePath+" points to an access control content node"); + } + } + + private void checkAccessControlRead(String absPath) throws RepositoryException { + if (!hasPrivileges(absPath, new Privilege[] {privilegeFromName(Privilege.JCR_READ_ACCESS_CONTROL)})) { + throw new AccessDeniedException(); + } + } + + private void createAceNode(SetTree operation, NodeState parentState, AccessControlEntry entry) throws RepositoryException { + AccessControlEntryImpl ace = (AccessControlEntryImpl) entry; + + String uuid = null; + boolean isAllow = ace.isAllow(); + Name nodeName = getUniqueNodeName(parentState, (isAllow) ? "allow" : "deny"); + Name nodeTypeName = (isAllow) ? NT_REP_GRANT_ACE : NT_REP_DENY_ACE; + NodeState aceNode = addNode(operation, parentState, nodeName, uuid, nodeTypeName); + + // add rep:principalName property + String valueStr = ace.getPrincipal().getName(); + QValue value = qvf.create(valueStr, PropertyType.STRING); + addProperty(operation, aceNode, N_REP_PRINCIPAL_NAME, PropertyType.STRING, new QValue[] {value}); + + // add rep:privileges MvProperty + Privilege[] privs = ace.getPrivileges(); + QValue[] vls = new QValue[privs.length]; + Name privilegeName = null; + try { + for (int i = 0; i < privs.length; i++) { + privilegeName = npResolver.getQName(privs[i].getName()); + vls[i] = qvf.create(privilegeName.toString(), PropertyType.NAME); + } + } catch (ValueFormatException e) { + throw new RepositoryException(e.getMessage()); + } + + addProperty(operation, aceNode, N_REP_PRIVILEGES, PropertyType.NAME, vls); + + // TODO: add single and mv restrictions + } + + private NodeState getNodeState(String nodePath) throws RepositoryException { + return getNodeState(npResolver.getQPath(nodePath)); + } + + private NodeState getRootNodeState() throws RepositoryException { + return hierarchyManager.getRootEntry().getNodeState(); + } + + private NodeState getNodeState(Path qPath) throws RepositoryException { + return hierarchyManager.getNodeState(qPath); + } + + private NodeEntry getNodeEntry(NodeState nodeState) throws RepositoryException { + return hierarchyManager.getNodeEntry(nodeState.getPath()); + } + + private void checkValidNodePath(String absPath) throws PathNotFoundException, RepositoryException { + if (absPath != null) { + Path qPath = npResolver.getQPath(absPath); + if (!qPath.isAbsolute()) { + throw new RepositoryException("Absolute path expected. Found: " + absPath); + } + + if (hierarchyManager.getNodeEntry(qPath).getNodeState() == null) { + throw new PathNotFoundException(absPath); + } + } + } + + private void checkValidPolicy(AccessControlPolicy policy) throws AccessControlException { + if (policy == null || !(policy instanceof AccessControlListImpl)) { + throw new AccessControlException("Policy is not applicable "); + } + } + + private NodeState addNode(SetTree treeOperation, NodeState parent, Name nodeName, String uuid, Name nodeTypeName) throws RepositoryException { + Operation sp = treeOperation.addChildNode(parent, nodeName, nodeTypeName, uuid); + itemStateMgr.execute(sp); + return (NodeState) ((AddNode) sp).getAddedStates().get(0); + } + + private void addProperty(SetTree treeOperation, NodeState parent, Name propName, int propType, QValue[] values) throws RepositoryException { + QPropertyDefinition definition = definitionProvider.getQPropertyDefinition(parent.getAllNodeTypeNames(), propName, propType); + + Operation ap = treeOperation.addChildProperty(parent, propName, propType, values, definition); + itemStateMgr.execute(ap); + } + + private void removeNode(NodeState aclNode) throws RepositoryException { + Operation removePolicy = Remove.create(aclNode, REMOVE_POLICY_OPTIONS); + itemStateMgr.execute(removePolicy); + } + + private void setMixin(NodeState parent, Name mixinName) throws RepositoryException { + if (!isNodeType(parent, mixinName)){ + Operation sm = SetMixin.create(parent, new Name[]{mixinName}); + itemStateMgr.execute(sm); + } else { + log.debug(mixinName.toString()+" is already present on the given node state "+parent.getName().toString()); + } + } + + // copied from jackrabbit-core ACLEditor + /** + * Create a unique valid name for the Permission nodes to be save. + * + * @param node a name for the child is resolved + * @param name if missing the {@link #DEFAULT_ACE_NAME} is taken + * @return the name + * @throws RepositoryException if an error occurs + */ + private Name getUniqueNodeName(NodeState node, String name) throws RepositoryException { + + try { + NameParser.checkFormat(name); + } catch (NameException e) { + log.debug("Invalid path name for Permission: " + name + "."); + } + + int i = 0; + String check = name; + Name n = npResolver.getQName(check); + while (node.hasChildNodeEntry(n, 1)) { + check = name + i; + n = npResolver.getQName(check); + i++; + } + return n; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlProviderImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlProviderImpl.java new file mode 100644 index 00000000000..de5cc1304c4 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlProviderImpl.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.jcr2spi.security.authorization.jackrabbit.acl; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlException; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.jcr2spi.ItemManager; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyManager; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.security.authorization.AccessControlProvider; +import org.apache.jackrabbit.jcr2spi.security.authorization.PrivilegeImpl; +import org.apache.jackrabbit.jcr2spi.state.UpdatableItemStateManager; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +public class AccessControlProviderImpl implements AccessControlProvider { + + private RepositoryService service; + + private Map privileges = new HashMap(); + + @Override + public void init(RepositoryConfig config) throws RepositoryException { + this.service = config.getRepositoryService(); + } + + @Override + public Privilege privilegeFromName(SessionInfo sessionInfo, NamePathResolver resolver, String privilegeName) throws RepositoryException { + Name name = resolver.getQName(privilegeName); + Privilege priv = getPrivilegeFromName(sessionInfo, resolver, name); + + if (priv == null) { + throw new AccessControlException("Unknown privilege " + privilegeName); + } else { + return priv; + } + } + + @Override + public Map getSupportedPrivileges(SessionInfo sessionInfo, NodeId nodeId, NamePathResolver npResolver) throws RepositoryException { + PrivilegeDefinition[] pDefs = service.getSupportedPrivileges(sessionInfo, nodeId); + Map privilegeMap = new HashMap(pDefs.length); + for (PrivilegeDefinition def : pDefs) { + Privilege p = new PrivilegeImpl(def, pDefs, npResolver); + privilegeMap.put(p.getName(), p); + } + return privilegeMap; + } + + @Override + public Set getPrivileges(SessionInfo sessionInfo, NodeId id, NamePathResolver npResolver) throws RepositoryException { + Name[] privNames = service.getPrivilegeNames(sessionInfo, id); + Set pvs = new HashSet(privNames.length); + for (Name name : privNames) { + Privilege priv = getPrivilegeFromName(sessionInfo, npResolver, name); + if (priv != null) { + pvs.add(priv); + } + } + return pvs; + } + + @Override + public AccessControlManager createAccessControlManager( + SessionInfo sessionInfo, + UpdatableItemStateManager itemStateManager, + ItemManager itemManager, + ItemDefinitionProvider definitionProvider, + HierarchyManager hierarchyManager, NamePathResolver npResolver) throws RepositoryException { + return new AccessControlManagerImpl(sessionInfo, itemStateManager, definitionProvider, hierarchyManager, npResolver, service.getQValueFactory(), this); + } + + //-------------------------------------------------------------------------- + + private void readPrivilegesFromService(SessionInfo sessionInfo, NamePathResolver resolver) throws RepositoryException { + PrivilegeDefinition[] defs = service.getPrivilegeDefinitions(sessionInfo); + for (PrivilegeDefinition d : defs) { + privileges.put(d.getName(), new PrivilegeImpl(d, defs, resolver)); + } + } + + private Privilege getPrivilegeFromName(SessionInfo sessionInfo, NamePathResolver resolver, Name privilegeName) throws RepositoryException { + Privilege priv = privileges.get(privilegeName); + if (priv == null) { + readPrivilegesFromService(sessionInfo, resolver); + if (privileges.containsKey(privilegeName)) { + priv = privileges.get(privilegeName); + } + } + return priv; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/AbstractItemStateFactory.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/AbstractItemStateFactory.java new file mode 100644 index 00000000000..4394c05a7cb --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/AbstractItemStateFactory.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.jcr2spi.state; + +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractItemStateFactory... + */ +public abstract class AbstractItemStateFactory implements ItemStateFactory { + + private static Logger log = LoggerFactory.getLogger(AbstractItemStateFactory.class); + + private final Set creationListeners = new HashSet(); + + //---------------------------------------------------< ItemStateFactory >--- + /** + * @see ItemStateFactory#addCreationListener(ItemStateCreationListener) + */ + public void addCreationListener(ItemStateCreationListener listener) { + synchronized (creationListeners) { + creationListeners.add(listener); + } + } + + /** + * @see ItemStateFactory#removeCreationListener(ItemStateCreationListener) + */ + public void removeCreationListener(ItemStateCreationListener listener) { + synchronized (creationListeners) { + creationListeners.remove(listener); + } + } + + //------------------------------------------------< private | protected >--- + /** + * + * @return + */ + private ItemStateCreationListener[] getListeners() { + synchronized (creationListeners) { + return creationListeners.toArray(new ItemStateCreationListener[creationListeners.size()]); + } + } + + /** + * + * @param createdState + */ + void notifyCreated(ItemState createdState) { + ItemStateCreationListener[] listeners = getListeners(); + for (int i = 0; i < listeners.length; i++) { + // notify listeners when this item state is saved or invalidated + createdState.addListener(listeners[i]); + // now inform about creation + listeners[i].created(createdState); + } + } + + /** + * + * @param state + */ + void notifyUpdated(ItemState state, int previousStatus) { + ItemStateCreationListener[] listeners = getListeners(); + for (int i = 0; i < listeners.length; i++) { + // now inform about creation + listeners[i].statusChanged(state, previousStatus); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ChangeLog.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ChangeLog.java new file mode 100644 index 00000000000..0d7356309b0 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ChangeLog.java @@ -0,0 +1,221 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.SetMixin; +import org.apache.jackrabbit.jcr2spi.operation.SetPrimaryType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Registers changes made to states and references and consolidates + * empty changes. + */ +public class ChangeLog { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(ChangeLog.class); + + /** + * The changelog target: Root item of the tree whose changes are contained + * in this changelog. + */ + private final ItemState target; + + /** + * Set of operations + */ + private final Set operations; + + private final Set affectedStates; + + /** + * Create a new change log and populates it with operations and states + * that are within the scope of this change set. + * + * @param target + * @param operations + * @param affectedStates + * @throws InvalidItemStateException + * @throws ConstraintViolationException + */ + ChangeLog(ItemState target, Set operations, Set affectedStates) + throws InvalidItemStateException, ConstraintViolationException { + this.target = target; + this.operations = operations; + this.affectedStates = affectedStates; + } + + //-----------------------------------------------< Inform the ChangeLog >--- + /** + * Call this method when this change log has been successfully persisted. + * This implementation will call {@link Operation#persisted()} on the + * individual operations followed by setting all remaining modified + * states to EXISTING. + */ + public void persisted() throws RepositoryException { + List changedMixins = new ArrayList(); + List changedPrimaryTypes = new ArrayList(); + + Operation[] ops = operations.toArray(new Operation[operations.size()]); + for (int i = 0; i < ops.length; i++) { + ops[i].persisted(); + if (ops[i] instanceof SetMixin) { + changedMixins.add(((SetMixin) ops[i]).getNodeState()); + } else if (ops[i] instanceof SetPrimaryType) { + changedPrimaryTypes.add(((SetPrimaryType) ops[i]).getNodeState()); + } + } + // process all remaining states that were not covered by the + // operation persistence. + for (ItemState state : affectedStates) { + HierarchyEntry he = state.getHierarchyEntry(); + + switch (state.getStatus()) { + case Status.EXISTING_MODIFIED: + state.setStatus(Status.EXISTING); + if (state.isNode()) { + if (changedPrimaryTypes.contains(state)) { + // primary type changed for a node -> force reloading upon next + // access in order to be aware of modified definition etc... + he.invalidate(true); + } else if (changedMixins.contains(state)) { + // mixin changed for a node -> force reloading upon next + // access in order to be aware of modified uniqueID. + he.invalidate(false); + } + } + break; + case Status.EXISTING_REMOVED: + he.remove(); + break; + case Status.NEW: + // illegal. should not get here. + log.error("ChangeLog still contains NEW state: " + state.getName()); + state.setStatus(Status.EXISTING); + break; + case Status.MODIFIED: + case Status._UNDEFINED_: + case Status.STALE_DESTROYED: + case Status.STALE_MODIFIED: + // illegal. + log.error("ChangeLog contains state (" + state.getName() + ") with illegal status " + Status.getName(state.getStatus())); + break; + case Status.EXISTING: + if (state.isNode() && changedMixins.contains(state)) { + // mixin changed for a node -> force reloading upon next + // access in order to be aware of modified uniqueID. + he.invalidate(false); + } + // otherwise: ignore. operations already have been completed + break; + case Status.INVALIDATED: + case Status.REMOVED: + he.invalidate(false); + break; + } + } + } + + /** + * Revert the changes listed within this changelog + */ + public void undo() throws RepositoryException { + Operation[] ops = operations.toArray(new Operation[operations.size()]); + for (int i = ops.length - 1; i >= 0; i--) { + ops[i].undo(); + } + + // process all remaining states that were not covered by the + // operation undo. + for (ItemState state : affectedStates) { + switch (state.getStatus()) { + case Status.EXISTING_MODIFIED: + case Status.EXISTING_REMOVED: + case Status.STALE_MODIFIED: + case Status.STALE_DESTROYED: + state.getHierarchyEntry().revert(); + break; + case Status.NEW: + // illegal. should not get here. + log.error("ChangeLog still contains NEW state: " + state.getName()); + state.getHierarchyEntry().revert(); + break; + case Status.MODIFIED: + case Status._UNDEFINED_: + // illegal. + log.error("ChangeLog contains state (" + state.getName() + ") with illegal status " + Status.getName(state.getStatus())); + break; + case Status.EXISTING: + case Status.REMOVED: + case Status.INVALIDATED: + // ignore already processed + break; + } + } + } + //----------------------< Retrieve information present in the ChangeLog >--- + /** + * @return the target state + */ + public ItemState getTarget() { + return target; + } + + /** + * @return true if no operations are present. + */ + public boolean isEmpty() { + return operations.isEmpty(); + } + + /** + * @return set of operations. + */ + public Set getOperations() { + return operations; + } + + /** + * @return set of the affected states. + */ + public Set getAffectedStates() { + return affectedStates; + } + + /** + * Reset this change log, removing all members inside the + * maps we built. + */ + void reset() { + affectedStates.clear(); + // also clear all operations + operations.clear(); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemState.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemState.java new file mode 100644 index 00000000000..6eb635b5ae6 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemState.java @@ -0,0 +1,404 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.util.WeakIdentityCollection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ItemState represents the state of an Item. + */ +public abstract class ItemState { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(ItemState.class); + + /** + * the internal status of this item state + */ + private int status; + + /** + * The hierarchy entry this state belongs to. + */ + private final HierarchyEntry hierarchyEntry; + + /** + * Listeners (weak references) + */ + @SuppressWarnings("unchecked") + private final transient Collection listeners = new WeakIdentityCollection(5); + + /** + * The ItemStateFactory which is used to create new + * ItemState instances. + */ + final ItemStateFactory isf; + + final ItemDefinitionProvider definitionProvider; + + /** + * Constructs an item state + * + * @param entry + * @param isf + * @param definitionProvider + */ + protected ItemState(HierarchyEntry entry, ItemStateFactory isf, + ItemDefinitionProvider definitionProvider) { + this(getInitialStatus(entry.getParent()), entry, isf, definitionProvider); + } + + /** + * Constructs an item state + * + * @param entry + * @param isf + * @param definitionProvider + */ + protected ItemState(int initialStatus, HierarchyEntry entry, + ItemStateFactory isf, + ItemDefinitionProvider definitionProvider) { + if (entry == null) { + throw new IllegalArgumentException("Cannot build ItemState from 'null' HierarchyEntry"); + } + switch (initialStatus) { + case Status.EXISTING: + case Status.NEW: + case Status.EXISTING_REMOVED: + status = initialStatus; + break; + default: + String msg = "illegal status: " + initialStatus; + log.debug(msg); + throw new IllegalArgumentException(msg); + } + this.hierarchyEntry = entry; + this.isf = isf; + this.definitionProvider = definitionProvider; + } + + /** + * + * @param parent + * @return + */ + private static int getInitialStatus(NodeEntry parent) { + int status = Status.EXISTING; + // walk up hierarchy and check if any of the parents is transiently + // removed, in which case the status must be set to EXISTING_REMOVED. + while (parent != null) { + if (parent.getStatus() == Status.EXISTING_REMOVED) { + status = Status.EXISTING_REMOVED; + break; + } + parent = parent.getParent(); + } + return status; + } + + //----------------------------------------------------------< ItemState >--- + /** + * The HierarchyEntry corresponding to this ItemState. + * + * @return The HierarchyEntry corresponding to this ItemState. + */ + public HierarchyEntry getHierarchyEntry() { + return hierarchyEntry; + } + + /** + * Returns true if this item state is valid and can be accessed. + * @return + * @see Status#isValid(int) + * @see Status#isStale(int) + */ + public boolean isValid() { + return Status.isValid(getStatus()) || Status.isStale(getStatus()); + } + + /** + * Utility method: + * Determines if this item state represents a node. + * + * @return true if this item state represents a node, otherwise false. + */ + public abstract boolean isNode(); + + /** + * Utility method: + * Returns the name of this state. Shortcut for calling 'getName' on the + * {@link ItemState#getHierarchyEntry() hierarchy entry}. + * + * @return name of this state + */ + public Name getName() { + return getHierarchyEntry().getName(); + } + + /** + * Utility method: + * Returns the identifier of this item state. Shortcut for calling 'getId' + * on the {@link ItemState#getHierarchyEntry() hierarchy entry}. + * + * @return the identifier of this item state.. + */ + public abstract ItemId getId() throws RepositoryException; + + /** + * Utility method: + * Returns the identifier of this item state. Shortcut for calling 'getWorkspaceId' + * on the NodeEntry or PropertyEntry respectively. + * + * @return the identifier of this item state.. + */ + public abstract ItemId getWorkspaceId() throws RepositoryException; + + /** + * Utility method: + * Returns the path of this item state. Shortcut for calling + * 'getPath' on the {@link ItemState#getHierarchyEntry() hierarchy entry}. + * + * @return + * @throws RepositoryException if an error occurs + */ + public Path getPath() throws RepositoryException { + return getHierarchyEntry().getPath(); + } + + /** + * Utility method: Shortcut for calling + * 'getParent().getNodeState()' on the {@link ItemState#getHierarchyEntry() + * hierarchy entry}. + * + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public NodeState getParent() throws ItemNotFoundException, RepositoryException { + // safeguard against root node's null parent + NodeEntry parent = getHierarchyEntry().getParent(); + if (parent != null) { + return getHierarchyEntry().getParent().getNodeState(); + } + return null; + } + + /** + * Returns the status of this item. + * + * @return the status of this item. + */ + public final int getStatus() { + // Call calculateStatus to apply a possible pending invalidation + // in the entry hierarchy. + getHierarchyEntry().calculateStatus(); + return status; + } + + /** + * Sets the new status of this item. + * + * @param newStatus the new status + */ + public void setStatus(int newStatus) { + int oldStatus = status; + if (oldStatus == newStatus) { + return; + } + + if (oldStatus == Status.REMOVED) { + throw new IllegalStateException("State is already in terminal status " + Status.getName(oldStatus)); + } + if (Status.isValidStatusChange(oldStatus, newStatus)) { + status = Status.getNewStatus(oldStatus, newStatus); + } else { + throw new IllegalArgumentException("Invalid new status " + Status.getName(newStatus) + " for state with status " + Status.getName(oldStatus)); + } + // Notify listeners about status change + // copy listeners to array to avoid ConcurrentModificationException + ItemStateLifeCycleListener[] la; + synchronized (listeners) { + la = listeners.toArray(new ItemStateLifeCycleListener[listeners.size()]); + } + for (int i = 0; i < la.length; i++) { + if (la[i] != null) { + la[i].statusChanged(this, oldStatus); + } + } + if (status == Status.MODIFIED) { + /* + change back tmp MODIFIED status, that is used as marker only + inform listeners about (external) changes. + */ + status = Status.EXISTING; + } + } + + /** + * Merge all data from the given state into this state. If + * 'keepChanges' is true, transient modifications present on + * this state are not touched. Otherwise this state is completely reset + * according to the given other state. + * + * @param another + * @param keepChanges + * @return a MergeResult instance which represent the result of the merge operation + */ + public abstract MergeResult merge(ItemState another, boolean keepChanges); + + /** + * Revert all transient modifications made to this ItemState. + * + * @return true if this state has been modified i.e. if there was anything + * to revert. + */ + public abstract boolean revert(); + + /** + * Add an ItemStateLifeCycleListener + * + * @param listener the new listener to be informed on modifications + */ + public void addListener(ItemStateLifeCycleListener listener) { + synchronized (listeners) { + listeners.add(listener); + } + } + + /** + * Remove an ItemStateLifeCycleListener + * + * @param listener an existing listener + */ + public void removeListener(ItemStateLifeCycleListener listener) { + synchronized (listeners) { + listeners.remove(listener); + } + } + + /** + * Unmodifiable iterator over the listeners present on this item state. + * + * @return iterator over ItemStateLifeCycleListeners. + */ + public Iterator getListeners() { + return Collections.unmodifiableCollection(listeners).iterator(); + } + + /** + * Invalidates this state: set its {@link Status} to {@link Status#INVALIDATED} + * if the current status is {@link Status#EXISTING}. Does nothing otherwise. + */ + public void invalidate() { + if (status == Status.EXISTING) { + setStatus(Status.INVALIDATED); + } else { + log.debug("Skip invalidation for item {} with status {}", getName(), Status.getName(status)); + } + } + + /** + * Marks this item state as modified. + */ + void markModified() throws InvalidItemStateException { + switch (status) { + case Status.EXISTING: + setStatus(Status.EXISTING_MODIFIED); + break; + case Status.EXISTING_MODIFIED: + // already modified, do nothing + break; + case Status.NEW: + // still new, do nothing + break; + case Status.STALE_DESTROYED: + case Status.STALE_MODIFIED: + // should actually not get here because item should check before + // it modifies an item state. + throw new InvalidItemStateException("Cannot mark stale state modified."); + + case Status.EXISTING_REMOVED: + default: + String msg = "Cannot mark item state with status '" + Status.getName(status) + "' modified."; + throw new InvalidItemStateException(msg); + } + } + + // -----------------------------------------------------< MergeResult >--- + + /** + * A MergeResult represents the result of a {@link ItemState#merge(ItemState, boolean)} + * operation. + */ + public interface MergeResult { + + /** + * @return true iff the target state of {@link ItemState#merge(ItemState, boolean)} + * was modified. + */ + public boolean modified(); + + /** + * Dispose this MergeResult and release all internal resources that + * are not needed any more. + */ + public void dispose(); + } + + /** + * A SimpleMergeResult is just a holder for a modification status. + * The {@link #modified()} method just returns the modification status passed + * to the constructor. + */ + protected class SimpleMergeResult implements MergeResult { + private final boolean modified; + + /** + * @param modified modification status + */ + public SimpleMergeResult(boolean modified) { + this.modified = modified; + } + + public boolean modified() { + return modified; + } + + public void dispose() { + // nothing to do. + } + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateCreationListener.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateCreationListener.java new file mode 100644 index 00000000000..ceddc316c71 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateCreationListener.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.jcr2spi.state; + +/** + * ItemStateCreationListener... + */ +public interface ItemStateCreationListener extends ItemStateLifeCycleListener { + + /** + * + * @param state + */ + public void created(ItemState state); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateFactory.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateFactory.java new file mode 100644 index 00000000000..d78b4cc018f --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateFactory.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.jcr2spi.state; + +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; + +import javax.jcr.RepositoryException; +import javax.jcr.ItemNotFoundException; +import java.util.Iterator; + +/** + * ItemStateFactory provides methods to create child + * NodeStates and PropertyStates for a given + * NodeState. + */ +public interface ItemStateFactory { + + /** + * @param entry + * @return + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public NodeState createRootState(NodeEntry entry) throws ItemNotFoundException, RepositoryException; + + /** + * Creates the child NodeState with the given + * nodeId. + * + * @param nodeId the id of the NodeState to create. + * @param entry the HierarchyEntry the new state should + * be attached to. + * @return the created NodeState. + * @throws ItemNotFoundException if there is no such NodeState. + * @throws RepositoryException if an error occurs while retrieving the NodeState. + */ + public NodeState createNodeState(NodeId nodeId, NodeEntry entry) + throws ItemNotFoundException, RepositoryException; + + + /** + * Tries to retrieve the NodeState with the given NodeId + * and if the state exists, fills in the NodeEntries missing between the + * last known NodeEntry marked by anyParent. + * + * @param nodeId + * @param anyParent + * @return the created NodeState. + * @throws ItemNotFoundException if there is no such NodeState. + * @throws RepositoryException if an error occurs while retrieving the NodeState. + */ + public NodeState createDeepNodeState(NodeId nodeId, NodeEntry anyParent) + throws ItemNotFoundException, RepositoryException; + + + /** + * Creates the PropertyState with the given + * propertyId. + * + * @param propertyId the id of the PropertyState to create. + * @param entry the HierarchyEntry the new state should + * be attached to. + * @return the created PropertyState. + * @throws ItemNotFoundException if there is no such PropertyState. + * @throws RepositoryException if an error occurs while retrieving the + * PropertyState. + */ + public PropertyState createPropertyState(PropertyId propertyId, PropertyEntry entry) + throws ItemNotFoundException, RepositoryException; + + + /** + * Tries to retrieve the PropertyState with the given PropertyId + * and if the state exists, fills in the HierarchyEntries missing between the + * last known NodeEntry marked by anyParent. + * + * @param propertyId + * @param anyParent + * @return + * @throws ItemNotFoundException if there is no such NodeState. + * @throws RepositoryException if an error occurs while retrieving the NodeState. + */ + public PropertyState createDeepPropertyState(PropertyId propertyId, NodeEntry anyParent) throws ItemNotFoundException, RepositoryException; + + /** + * Returns an Iterator over ChildInfos for the given NodeState. + * + * @param nodeId + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public Iterator getChildNodeInfos(NodeId nodeId) throws ItemNotFoundException, RepositoryException; + + /** + * Returns the identifiers of all reference properties that point to + * the given node. + * + * @param nodeState reference target + * @param propertyName + * @param weak Boolean flag indicating whether weak references should be + * returned or not. + * @return reference property identifiers + */ + public Iterator getNodeReferences(NodeState nodeState, Name propertyName, boolean weak); + + /** + * Adds the given ItemStateCreationListener. + * + * @param listener + */ + public void addCreationListener(ItemStateCreationListener listener); + + /** + * Removes the given ItemStateCreationListener. + * + * @param listener + */ + public void removeCreationListener(ItemStateCreationListener listener); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateLifeCycleListener.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateLifeCycleListener.java new file mode 100644 index 00000000000..dc071290c36 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateLifeCycleListener.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.jcr2spi.state; + +/** + * ItemStateLifeCycleListener allows an implementing class to get + * notifications about the life cycle of an item state. + */ +public interface ItemStateLifeCycleListener { + + /** + * Called after an ItemState has changed its status. The new + * status can be retrieved by calling {@link ItemState#getStatus()}. + * + * @param state the item state, which changed its status. + * @param previousStatus the previous status of state. + */ + public void statusChanged(ItemState state, int previousStatus); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateValidator.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateValidator.java new file mode 100644 index 00000000000..37d42a321a1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/ItemStateValidator.java @@ -0,0 +1,609 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +import org.apache.jackrabbit.jcr2spi.ManagerProvider; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.jcr2spi.security.AccessManager; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +/** + * Utility class for validating an item state against constraints + * specified by its definition. + */ +public class ItemStateValidator { + + /** + * Logger instance for this class + */ + private static Logger log = LoggerFactory.getLogger(ItemStateValidator.class); + + /** + * option for {@link #checkAddNode} and + * {@link #checkRemoveItem} methods: + *

        + * check access rights + */ + public static final int CHECK_ACCESS = 1; + /** + * option for {@link #checkAddNode} and + * {@link #checkRemoveItem} methods: + *

        + * check lock status + */ + public static final int CHECK_LOCK = 2; + /** + * option for {@link #checkAddNode} and + * {@link #checkRemoveItem} methods: + *

        + * check checked-out status + */ + public static final int CHECK_VERSIONING = 4; + /** + * option for {@link #checkAddNode} and + * {@link #checkRemoveItem} methods: + *

        + * check constraints defined in node type + */ + public static final int CHECK_CONSTRAINTS = 8; + + /** + * option for {@link #checkRemoveItem} method: + *

        + * check that target node is not being referenced + */ + public static final int CHECK_COLLISION = 32; + + public static final int CHECK_NONE = 0; + public static final int CHECK_ALL = CHECK_ACCESS | CHECK_LOCK | CHECK_VERSIONING | CHECK_CONSTRAINTS | CHECK_COLLISION; + + /** + * manager provider + */ + private final ManagerProvider mgrProvider; + private final PathFactory pathFactory; + + /** + * Creates a new ItemStateValidator instance. + * + * @param mgrProvider manager provider + */ + public ItemStateValidator(ManagerProvider mgrProvider, PathFactory pathFactory) { + this.mgrProvider = mgrProvider; + this.pathFactory = pathFactory; + } + + /** + * Checks whether the given node state satisfies the constraints specified + * by its primary and mixin node types. The following validations/checks are + * performed: + *

          + *
        • check if its node type satisfies the 'required node types' constraint + * specified in its definition
        • + *
        • check if all 'mandatory' child items exist
        • + *
        • for every property: check if the property value satisfies the + * value constraints specified in the property's definition
        • + *
        + * + * @param nodeState state of node to be validated + * @throws ConstraintViolationException if any of the validations fail + * @throws RepositoryException if another error occurs + */ + public void validate(NodeState nodeState) throws ConstraintViolationException, + RepositoryException { + // effective primary node type + EffectiveNodeType entPrimary = mgrProvider.getEffectiveNodeTypeProvider().getEffectiveNodeType(nodeState.getNodeTypeName()); + QNodeDefinition def = nodeState.getDefinition(); + + // check if primary type satisfies the 'required node types' constraint + Name[] requiredPrimaryTypes = def.getRequiredPrimaryTypes(); + for (int i = 0; i < requiredPrimaryTypes.length; i++) { + if (!entPrimary.includesNodeType(requiredPrimaryTypes[i])) { + String msg = safeGetJCRPath(nodeState) + + ": missing required primary type " + + requiredPrimaryTypes[i]; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + // mandatory properties + // effective node type (primary type incl. mixins) + Name[] ntNames = nodeState.getAllNodeTypeNames(); + EffectiveNodeType entPrimaryAndMixins = mgrProvider.getEffectiveNodeTypeProvider().getEffectiveNodeType(ntNames); + QPropertyDefinition[] pda = entPrimaryAndMixins.getMandatoryQPropertyDefinitions(); + for (int i = 0; i < pda.length; i++) { + QPropertyDefinition pd = pda[i]; + if (!nodeState.hasPropertyName(pd.getName())) { + String msg = safeGetJCRPath(nodeState) + + ": mandatory property " + pd.getName() + + " does not exist"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + // mandatory child nodes + QNodeDefinition[] cnda = entPrimaryAndMixins.getMandatoryQNodeDefinitions(); + for (int i = 0; i < cnda.length; i++) { + QNodeDefinition cnd = cnda[i]; + if (!nodeState.getNodeEntry().hasNodeEntry(cnd.getName())) { + String msg = safeGetJCRPath(nodeState) + + ": mandatory child node " + cnd.getName() + + " does not exist"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + } + + //-------------------------------------------------< misc. helper methods > + /** + * Failsafe translation of internal ItemState to JCR path for use + * in error messages etc. + * + * @param itemState + * @return JCR path + * @see LogUtil#safeGetJCRPath(ItemState,org.apache.jackrabbit.spi.commons.conversion.PathResolver) + */ + private String safeGetJCRPath(ItemState itemState) { + return LogUtil.safeGetJCRPath(itemState, mgrProvider.getPathResolver()); + } + + //------------------------------------------------------< check methods >--- + /** + * + * @param parentState + * @param options + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ItemExistsException + * @throws PathNotFoundException + * @throws RepositoryException + */ + public void checkIsWritable(NodeState parentState, int options) throws VersionException, + LockException, ItemNotFoundException, ItemExistsException, PathNotFoundException, RepositoryException { + + if ((options & CHECK_ACCESS) == CHECK_ACCESS) { + // make sure current session is granted read access on parent node + if (!mgrProvider.getAccessManager().canRead(parentState)) { + throw new ItemNotFoundException(safeGetJCRPath(parentState)); + } + } + // make sure there's no foreign lock on parent node + if ((options & CHECK_LOCK) == CHECK_LOCK) { + checkLock(parentState); + } + // make sure parent node is checked-out + if ((options & CHECK_VERSIONING) == CHECK_VERSIONING) { + checkIsCheckedOut(parentState); + } + // constraints + if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) { + // make sure parent node is not protected + checkProtection(parentState); + } + } + + /** + * + * @param propState + * @param options bit-wise OR'ed flags specifying the checks that should be + * performed; any combination of the following constants: + *
          + *
        • {@link #CHECK_ACCESS}: make sure current session is + * granted read access on parent node and can add a child node with the + * given name.
        • + *
        • {@link #CHECK_LOCK}: make sure there's no foreign lock + * on parent node
        • + *
        • {@link #CHECK_VERSIONING}: make sure parent node is + * checked-out
        • + *
        • {@link #CHECK_CONSTRAINTS}: make sure no node type + * constraints would be violated
        • + *
        • {@link #CHECK_COLLISION}: check for collision with + * existing properties or nodes
        • + *
        + * + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ItemExistsException + * @throws PathNotFoundException + * @throws RepositoryException + */ + public void checkSetProperty(PropertyState propState, int options) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ItemExistsException, PathNotFoundException, RepositoryException { + + NodeState parent = propState.getParent(); + QPropertyDefinition def = propState.getDefinition(); + checkWriteProperty(parent, propState.getName(), def, options); + } + + /** + * + * @param parentState + * @param propertyName + * @param options bit-wise OR'ed flags specifying the checks that should be + * performed; any combination of the following constants: + *
          + *
        • {@link #CHECK_ACCESS}: make sure current session is + * granted read access on parent node and can add a child node with the + * given name.
        • + *
        • {@link #CHECK_LOCK}: make sure there's no foreign lock + * on parent node
        • + *
        • {@link #CHECK_VERSIONING}: make sure parent node is + * checked-out
        • + *
        • {@link #CHECK_CONSTRAINTS}: make sure no node type + * constraints would be violated
        • + *
        • {@link #CHECK_COLLISION}: check for collision with + * existing properties or nodes
        • + *
        + * + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ItemExistsException + * @throws PathNotFoundException + * @throws RepositoryException + */ + public void checkAddProperty(NodeState parentState, Name propertyName, QPropertyDefinition definition, int options) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ItemExistsException, PathNotFoundException, RepositoryException { + + checkWriteProperty(parentState, propertyName, definition, options); + } + + /** + * + * @param parentState + * @param propertyName + * @param definition + * @param options + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ItemExistsException + * @throws PathNotFoundException + * @throws RepositoryException + */ + private void checkWriteProperty(NodeState parentState, Name propertyName, QPropertyDefinition definition, int options) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ItemExistsException, PathNotFoundException, RepositoryException { + + checkIsWritable(parentState, options); + + // access restriction on prop. + if ((options & CHECK_ACCESS) == CHECK_ACCESS) { + // make sure current session is granted write access on new prop + Path relPath = pathFactory.create(propertyName); + if (!mgrProvider.getAccessManager().isGranted(parentState, relPath, new String[] {AccessManager.SET_PROPERTY_ACTION})) { + throw new AccessDeniedException(safeGetJCRPath(parentState) + ": not allowed to create property with name " + propertyName); + } + } + // constraints on property + if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) { + // if definition is available check if prop-def is not protected either. + checkProtection(definition); + } + // collisions + if ((options & CHECK_COLLISION) == CHECK_COLLISION) { + checkCollision(parentState, propertyName); + } + } + + /** + * Checks if adding a child node called nodeName of node type + * nodeTypeName to the given parent node is allowed in the + * current context. + * + * @param parentState + * @param nodeName + * @param nodeTypeName + * @param options bit-wise OR'ed flags specifying the checks that should be + * performed; any combination of the following constants: + *
          + *
        • {@link #CHECK_ACCESS}: make sure current session is + * granted read access on parent node and can add a child node with the + * given name.
        • + *
        • {@link #CHECK_LOCK}: make sure there's no foreign lock + * on parent node
        • + *
        • {@link #CHECK_VERSIONING}: make sure parent node is + * checked-out
        • + *
        • {@link #CHECK_CONSTRAINTS}: make sure no node type + * constraints would be violated
        • + *
        • {@link #CHECK_COLLISION}: check for collision with + * existing properties or nodes
        • + *
        + * + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ItemExistsException + * @throws RepositoryException + */ + public void checkAddNode(NodeState parentState, Name nodeName, + Name nodeTypeName, int options) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ItemExistsException, RepositoryException { + + checkIsWritable(parentState, options); + + // access restrictions on new node + if ((options & CHECK_ACCESS) == CHECK_ACCESS) { + // make sure current session is granted write access on parent node + Path relPath = pathFactory.create(nodeName); + if (!mgrProvider.getAccessManager().isGranted(parentState, relPath, new String[] {AccessManager.ADD_NODE_ACTION})) { + throw new AccessDeniedException(safeGetJCRPath(parentState) + ": not allowed to add child node '" + nodeName +"'"); + } + } + // node type constraints + if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) { + // make sure there's an applicable definition for new child node + Name[] ntNames = parentState.getAllNodeTypeNames(); + EffectiveNodeType entParent = mgrProvider.getEffectiveNodeTypeProvider().getEffectiveNodeType(ntNames); + QNodeTypeDefinition def = mgrProvider.getNodeTypeDefinitionProvider().getNodeTypeDefinition(nodeTypeName); + entParent.checkAddNodeConstraints(nodeName, def, mgrProvider.getItemDefinitionProvider()); + } + // collisions + if ((options & CHECK_COLLISION) == CHECK_COLLISION) { + checkCollision(parentState, nodeName, nodeTypeName); + } + } + + /** + * Checks if removing the given target state is allowed in the current context. + * + * @param targetState + * @param options bit-wise OR'ed flags specifying the checks that should be + * performed; any combination of the following constants: + *
          + *
        • {@link #CHECK_ACCESS}: make sure + * current session is granted read access on parent + * and remove privilege on target node
        • + *
        • {@link #CHECK_LOCK}: make sure + * there's no foreign lock on parent node
        • + *
        • {@link #CHECK_VERSIONING}: make sure + * parent node is checked-out
        • + *
        • {@link #CHECK_CONSTRAINTS}: + * make sure no node type constraints would be violated
        • + *
        + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws VersionException + * @throws LockException + * @throws ItemNotFoundException + * @throws ReferentialIntegrityException + * @throws RepositoryException + */ + public void checkRemoveItem(ItemState targetState, int options) + throws ConstraintViolationException, AccessDeniedException, + VersionException, LockException, ItemNotFoundException, + ReferentialIntegrityException, RepositoryException { + + if (targetState.isNode() && ((NodeState)targetState).isRoot()) { + // root node + throw new ConstraintViolationException("Cannot remove root node."); + } + // check parent + checkIsWritable(targetState.getParent(), options); + + // access rights + if ((options & CHECK_ACCESS) == CHECK_ACCESS) { + try { + // make sure current session is allowed to remove target node + if (!mgrProvider.getAccessManager().canRemove(targetState)) { + throw new AccessDeniedException(safeGetJCRPath(targetState) + ": not allowed to remove node"); + } + } catch (ItemNotFoundException e) { + String msg = "internal error: failed to check access rights for " + safeGetJCRPath(targetState); + log.debug(msg); + throw new RepositoryException(msg, e); + } + } + + // constraints given from the target + if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) { + // check if target not protected and not mandatory + checkRemoveConstraints(targetState); + } + } + + /** + * Verifies that the item represented by the given state is checked-out; + * throws a VersionException if that's not the case. + *

        + * A node is considered checked-out if it is versionable and + * checked-out, or is non-versionable but its nearest versionable ancestor + * is checked-out, or is non-versionable and there are no versionable + * ancestors. + * + * @param itemState state to check + * @throws PathNotFoundException + * @throws VersionException + * @throws RepositoryException + */ + private void checkIsCheckedOut(ItemState itemState) + throws PathNotFoundException, VersionException, RepositoryException { + + NodeState nodeState = (itemState.isNode()) ? (NodeState)itemState : itemState.getParent(); + mgrProvider.getVersionStateManager().checkIsCheckedOut(nodeState); + } + + /** + * Verifies that the given item state is not locked by + * somebody else than the current session. + * + * @param itemState state to be checked + * @throws PathNotFoundException + * @throws LockException if write access to the specified path is not allowed + * @throws RepositoryException if another error occurs + */ + private void checkLock(ItemState itemState) throws LockException, RepositoryException { + // make sure there's no foreign lock present the node (or the parent node + // in case the state represents a PropertyState). + NodeState nodeState = (itemState.isNode()) ? ((NodeState)itemState) : itemState.getParent(); + mgrProvider.getLockStateManager().checkLock(nodeState); + } + + /** + * Checks if the definition of the given item state indicates a protected + * status. + * + * @param itemState + * @throws ConstraintViolationException If the definition of the given + * item state indicates that the state is protected. + * @see QItemDefinition#isProtected() + */ + private void checkProtection(ItemState itemState) + throws ConstraintViolationException, RepositoryException { + QItemDefinition def; + if (itemState.isNode()) { + def = ((NodeState)itemState).getDefinition(); + } else { + def = ((PropertyState)itemState).getDefinition(); + } + checkProtection(def); + } + + /** + * Checks if the given {@link QItemDefinition#isProtected()} is true. + * + * @param definition + * @throws ConstraintViolationException If {@link QItemDefinition#isProtected()} + * returns true. + */ + private void checkProtection(QItemDefinition definition) throws ConstraintViolationException { + if (definition.isProtected()) { + throw new ConstraintViolationException("Item is protected"); + } + } + + /** + * An item state cannot be removed if it is protected. + * + * @param itemState + * @throws ConstraintViolationException + * @see #checkProtection(ItemState) + */ + private void checkRemoveConstraints(ItemState itemState) + throws ConstraintViolationException, RepositoryException { + QItemDefinition definition; + if (itemState.isNode()) { + definition = ((NodeState)itemState).getDefinition(); + } else { + definition = ((PropertyState)itemState).getDefinition(); + } + checkProtection(definition); + } + + /** + * + * @param parentState + * @param propertyName + * @throws ItemExistsException + * @throws RepositoryException + */ + private void checkCollision(NodeState parentState, Name propertyName) throws ItemExistsException, RepositoryException { + NodeEntry parentEntry = (NodeEntry) parentState.getHierarchyEntry(); + // NOTE: check for name collisions with existing child node has been + // removed as with JSR 283 having same-named node and property can be + // allowed. thus delegate the corresponding validation to the underlying + // SPI implementation. + + // check for name collisions with an existing property + PropertyEntry pe = parentEntry.getPropertyEntry(propertyName); + if (pe != null) { + try { + pe.getPropertyState(); + throw new ItemExistsException("Property '" + pe.getName() + "' already exists."); + } catch (ItemNotFoundException e) { + // apparently conflicting entry does not exist any more + // ignore and return + } + } + } + + /** + * + * @param parentState + * @param nodeName + * @param nodeTypeName + * @throws RepositoryException + * @throws ConstraintViolationException + * @throws NoSuchNodeTypeException + */ + private void checkCollision(NodeState parentState, Name nodeName, Name nodeTypeName) throws RepositoryException, ConstraintViolationException, NoSuchNodeTypeException { + // NOTE: check for name collisions with existing child property has been + // removed as with JSR 283 having same-named node and property may be + // allowed. thus delegate the corresponding validation to the underlying + // SPI implementation. + + // check for conflict with existing same-name sibling node. + if (parentState.hasChildNodeEntry(nodeName, Path.INDEX_DEFAULT)) { + // retrieve the existing node state that ev. conflicts with the new one. + try { + NodeState conflictingState = parentState.getChildNodeState(nodeName, Path.INDEX_DEFAULT); + QNodeDefinition conflictDef = conflictingState.getDefinition(); + QNodeDefinition newDef = mgrProvider.getItemDefinitionProvider().getQNodeDefinition(parentState.getAllNodeTypeNames(), nodeName, nodeTypeName); + + // check same-name sibling setting of both target and existing node + if (!(conflictDef.allowsSameNameSiblings() && newDef.allowsSameNameSiblings())) { + throw new ItemExistsException("Cannot add child node '" + + nodeName.getLocalName() + "' to " + + safeGetJCRPath(parentState) + + ": colliding with same-named existing node."); + } + } catch (ItemNotFoundException e) { + // ignore: conflicting doesn't exist any more + } + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/NodeState.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/NodeState.java new file mode 100644 index 00000000000..201764c3f0c --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/NodeState.java @@ -0,0 +1,471 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.util.StateUtility; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NodeState represents the state of a Node. + */ +public class NodeState extends ItemState { + + private static Logger log = LoggerFactory.getLogger(NodeState.class); + + /** + * the name of this node's primary type + */ + private Name nodeTypeName; + + /** + * Definition of this node state + */ + private QNodeDefinition definition; + + /** + * the names of this node's mixin types + */ + private Name[] mixinTypeNames = Name.EMPTY_ARRAY; + + /** + * Constructs a NEW NodeState + * + * @param entry + * @param nodeTypeName + * @param mixinTypeNames + * @param isf + * @param definition + * @param definitionProvider + */ + protected NodeState(NodeEntry entry, Name nodeTypeName, Name[] mixinTypeNames, + ItemStateFactory isf, QNodeDefinition definition, + ItemDefinitionProvider definitionProvider) { + super(Status.NEW, entry, isf, definitionProvider); + this.nodeTypeName = nodeTypeName; + setMixinTypeNames(mixinTypeNames); + this.definition = definition; + } + + /** + * Constructs an EXISTING NodeState + * + * @param entry + * @param nInfo + * @param isf + * @param definitionProvider + */ + protected NodeState(NodeEntry entry, NodeInfo nInfo, ItemStateFactory isf, + ItemDefinitionProvider definitionProvider) { + super(entry, isf, definitionProvider); + this.nodeTypeName = nInfo.getNodetype(); + setMixinTypeNames(nInfo.getMixins()); + } + + //----------------------------------------------------------< ItemState >--- + /** + * Determines if this item state represents a node. + * + * @return always true + * @see ItemState#isNode + */ + @Override + public final boolean isNode() { + return true; + } + + /** + * {@inheritDoc} + * @see ItemState#getId() + */ + @Override + public ItemId getId() throws RepositoryException { + return getNodeId(); + } + + /** + * {@inheritDoc} + * @see ItemState#getWorkspaceId() + */ + @Override + public ItemId getWorkspaceId() throws RepositoryException { + return getNodeEntry().getWorkspaceId(); + } + + /** + * @see ItemState#merge(ItemState, boolean) + */ + @Override + public MergeResult merge(ItemState another, boolean keepChanges) { + boolean modified = false; + if (another != null && another != this) { + if (!another.isNode()) { + throw new IllegalArgumentException("Attempt to merge node state with property state."); + } + synchronized (another) { + NodeState nState = (NodeState) another; + + if (!nodeTypeName.equals(nState.nodeTypeName)) { + nodeTypeName = nState.nodeTypeName; + modified = true; + } + + if (nState.definition != null && !nState.definition.equals(definition)) { + definition = nState.definition; + modified = true; + } + + // since 'mixinTypeNames' are modified upon save only, no special + // merging is required here. just reset the mixinTypeNames. + List mixN = Arrays.asList(nState.mixinTypeNames); + if (mixN.size() != mixinTypeNames.length || !mixN.containsAll(Arrays.asList(mixinTypeNames))) { + setMixinTypeNames(nState.mixinTypeNames); + modified = true; + } + } + } + return new SimpleMergeResult(modified); + } + + /** + * @see ItemState#revert() + * @return Always returns false unless the definition has been modified + * along with a move operation. + */ + @Override + public boolean revert() { + // TODO: ev. reset the 'markModified' flag + if (StateUtility.isMovedState(this)) { + try { + QNodeDefinition def = retrieveDefinition(); + if (!def.equals(definition)) { + definition = def; + return true; + } + } catch (RepositoryException e) { + // should never get here + log.warn("Internal error", e); + } + } + return false; + } + + //----------------------------------------------------------< NodeState >--- + /** + * @return The NodeEntry associated with this state. + */ + public NodeEntry getNodeEntry() { + return (NodeEntry) getHierarchyEntry(); + } + + /** + * Returns the id of this node state. + * + * @return the id of this node state. + */ + public NodeId getNodeId() throws RepositoryException { + return getNodeEntry().getId(); + } + + /** + * @return the unique ID of this node state or null if this + * node cannot be identified with a unique ID. + */ + public String getUniqueID() { + return getNodeEntry().getUniqueID(); + } + + /** + * Returns true, if this NodeState represent the root node. + * + * @return true if this NodeState represent the root node. + */ + public boolean isRoot() { + return getHierarchyEntry().getParent() == null; + } + + /** + * Returns the name of this node's node type. + * + * @return the name of this node's node type. + */ + public Name getNodeTypeName() { + return nodeTypeName; + } + + /** + * Returns the names of this node's mixin types. + * + * @return a set of the names of this node's mixin types. + */ + public Name[] getMixinTypeNames() { + return mixinTypeNames; + } + + /** + * Used by NodeEntryImpl and NodeState only + * + * @param mixinTypeNames + */ + public void setMixinTypeNames(Name[] mixinTypeNames) { + if (mixinTypeNames != null) { + this.mixinTypeNames = mixinTypeNames; + } + } + + /** + * Return all nodetype names that are defined to this NodeState + * including the primary nodetype and the mixins. + * + * @return array of NodeType names + */ + public synchronized Name[] getNodeTypeNames() { + // mixin types + Name[] mixinNames = getMixinTypeNames(); + Name[] types = new Name[mixinNames.length + 1]; + System.arraycopy(mixinNames, 0, types, 0, mixinNames.length); + // primary type + types[types.length - 1] = getNodeTypeName(); + return types; + } + + /** + * TODO: clarify usage + * In case the status of the given node state is not {@link Status#EXISTING} + * the transiently added mixin types are taken into account as well. + * + * @return + */ + public synchronized Name[] getAllNodeTypeNames() { + Name[] allNtNames; + if (getStatus() == Status.EXISTING) { + allNtNames = getNodeTypeNames(); + } else { + // TODO: check if correct (and only used for creating new) + Name primaryType = getNodeTypeName(); + allNtNames = new Name[] { primaryType }; // default + try { + PropertyEntry pe = getNodeEntry().getPropertyEntry(NameConstants.JCR_MIXINTYPES, true); + if (pe != null) { + PropertyState mixins = pe.getPropertyState(); + QValue[] values = mixins.getValues(); + allNtNames = new Name[values.length + 1]; + for (int i = 0; i < values.length; i++) { + allNtNames[i] = values[i].getName(); + } + allNtNames[values.length] = primaryType; + } // else: no jcr:mixinTypes property exists -> ignore + } catch (RepositoryException e) { + // unexpected error: ignore + } + } + return allNtNames; + } + + /** + * Returns true if the definition of this state has already been + * calculated. False otherwise. + * + * @return true if definition has already been calculated. + */ + public boolean hasDefinition() throws RepositoryException { + return definition != null; + } + + /** + * Returns the {@link QNodeDefinition definition} defined for this + * node state. Note, that the definition has been set upon creation or + * upon move. + * + * @return definition of this state + */ + public QNodeDefinition getDefinition() throws RepositoryException { + if (definition == null) { + definition = retrieveDefinition(); + } + return definition; + } + + /** + * Returns the identifiers of all reference properties that point to + * this node. + * + * @param propertyName name filter of referring properties to be returned; + * if null then all references are returned. + * @param weak Boolean flag indicating whether weak references should be + * returned or not. + * @return reference property identifiers + */ + public Iterator getNodeReferences(Name propertyName, boolean weak) { + return isf.getNodeReferences(this, propertyName, weak); + } + + /** + * Utility + * Determines if there is a valid NodeEntry with the + * specified name and index. + * + * @param name Name object specifying a node name. + * @param index 1-based index if there are same-name child node entries. + * @return true if there is a NodeEntry with + * the specified name and index. + */ + public boolean hasChildNodeEntry(Name name, int index) { + return getNodeEntry().hasNodeEntry(name, index); + } + + /** + * Utility + * Returns the child NodeState with the specified name + * and index. Throws ItemNotFoundException if there's no + * matching, valid entry. + * + * @param nodeName Name object specifying a node name. + * @param index 1-based index if there are same-name child node entries. + * @return The NodeState with the specified name and index + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public NodeState getChildNodeState(Name nodeName, int index) throws ItemNotFoundException, RepositoryException { + NodeEntry ne = getNodeEntry().getNodeEntry(nodeName, index, true); + if (ne != null) { + return ne.getNodeState(); + } else { + // does not exist (any more) or is a property + throw new ItemNotFoundException("Child node "+ nodeName +" with index " + index + " does not exist."); + } + } + + /** + * Utility + * + * @param propName Name object specifying a property name + * @return true if there is a valid property entry with the + * specified Name. + */ + public boolean hasPropertyName(Name propName) { + return getNodeEntry().hasPropertyEntry(propName); + } + + /** + * Utility method that returns the property state with the given name or + * throws an ItemNotFoundException if no matching, valid + * property could be found. + * + * @param propertyName The name of the property state to return. + * @throws ItemNotFoundException If there is no (valid) property state + * with the given name. + * @throws RepositoryException If an error occurs while retrieving the + * property state. + * + * @see NodeEntry#getPropertyEntry(Name, boolean) + * @see PropertyEntry#getPropertyState() + */ + public PropertyState getPropertyState(Name propertyName) throws ItemNotFoundException, RepositoryException { + PropertyEntry pe = getNodeEntry().getPropertyEntry(propertyName, true); + if (pe != null) { + return pe.getPropertyState(); + } else { + throw new ItemNotFoundException("Child Property with name " + propertyName + " does not exist."); + } + } + + /** + * Reorders the child node insertNode before the child node + * beforeNode. + * + * @param insertNode the child node to reorder. + * @param beforeNode the child node where to insert the node before. If + * null the child node insertNode is moved to the + * end of the child node entries. + * @throws ItemNotFoundException if insertNode or + * beforeNode is not a child node of this NodeState. + */ + synchronized void reorderChildNodeEntries(NodeState insertNode, NodeState beforeNode) + throws ItemNotFoundException, RepositoryException { + + NodeEntry before = (beforeNode == null) ? null : beforeNode.getNodeEntry(); + insertNode.getNodeEntry().orderBefore(before); + + // mark this state as modified + markModified(); + } + + /** + * Moves a NodeEntry to a new parent. If the new parent + * is this NodeState, the child state is renamed and moved + * to the end of the child entries collection. + * + * @param newParent + * @param childState + * @param newName Name object specifying the entry's new name + * @throws RepositoryException if the given child state is not a child + * of this node state. + */ + synchronized void moveChildNodeEntry(NodeState newParent, NodeState childState, + Name newName, QNodeDefinition newDefinition) + throws RepositoryException { + // move child entry + childState.getNodeEntry().move(newName, newParent.getNodeEntry(), true); + childState.definition = newDefinition; + + // mark both this and newParent modified + markModified(); + newParent.markModified(); + childState.markModified(); + } + + private QNodeDefinition retrieveDefinition() throws RepositoryException { + QNodeDefinition def; + if (isRoot()) { + def = definitionProvider.getRootNodeDefinition(); + } else { + /* + Don't use getAllNodeTypeNames() to retrieve the definition: + for NEW-states the definition is always set upon creation. + for all other states the definition must be retrieved only taking + the effective nodetypes present on the parent into account + any kind of transiently added mixins must not have an effect + on the definition retrieved for an state that has been persisted + before. The effective NT must be evaluated as if it had been + evaluated upon creating the workspace state. + */ + NodeState parent = getParent(); + NodeId wspId = (NodeId) getWorkspaceId(); + def = definitionProvider.getQNodeDefinition(parent.getNodeTypeNames(), getName(), getNodeTypeName(), wspId); + } + return def; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/PropertyState.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/PropertyState.java new file mode 100644 index 00000000000..a61196bad40 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/PropertyState.java @@ -0,0 +1,419 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * PropertyState represents the state of a Property. + */ +public class PropertyState extends ItemState { + + private static Logger log = LoggerFactory.getLogger(PropertyState.class); + + /** + * Property definition + */ + private QPropertyDefinition definition; + + /** + * True if this Property is multiValued + */ + private final boolean multiValued; + + /** + * Value(s) and type of an existing property that has been transiently + * modified. + */ + private PropertyData transientData; + + /** + * Original value(s) and type of an existing or a new property. + */ + private PropertyData data; + + /** + * Create a NEW PropertyState + * + * @param entry + * @param isf + * @param definition + * @param definitionProvider + */ + protected PropertyState(PropertyEntry entry, ItemStateFactory isf, + QPropertyDefinition definition, + ItemDefinitionProvider definitionProvider, + QValue[] values, int propertyType) + throws ConstraintViolationException, RepositoryException { + super(Status.NEW, entry, isf, definitionProvider); + this.multiValued = definition.isMultiple(); + this.definition = definition; + setValues(values, propertyType); + } + + /** + * Create an EXISTING PropertyState + * + * @param entry + * @param pInfo + * @param isf + * @param definitionProvider + */ + protected PropertyState(PropertyEntry entry, PropertyInfo pInfo, + ItemStateFactory isf, + ItemDefinitionProvider definitionProvider) { + super(entry, isf, definitionProvider); + this.multiValued = pInfo.isMultiValued(); + this.data = new PropertyData(pInfo); + this.transientData = null; + } + + //----------------------------------------------------------< ItemState >--- + /** + * Always returns false. + * + * @return always false + * @see ItemState#isNode + */ + @Override + public boolean isNode() { + return false; + } + + /** + * {@inheritDoc} + * @see ItemState#getId() + */ + @Override + public ItemId getId() throws RepositoryException { + return ((PropertyEntry) getHierarchyEntry()).getId(); + } + + /** + * {@inheritDoc} + * @see ItemState#getWorkspaceId() + */ + @Override + public ItemId getWorkspaceId() throws RepositoryException { + return ((PropertyEntry) getHierarchyEntry()).getWorkspaceId(); + } + + /** + * If keepChanges is true, this method only compares the existing + * values with the values from 'another' and returns true, if the underlying + * persistent state is different to the stored persistent values. Otherwise + * the transient changes will be discarded. + * + * @see ItemState#merge(ItemState, boolean) + */ + @Override + public MergeResult merge(ItemState another, boolean keepChanges) { + boolean modified = false; + if (another != null && another != this) { + if (another.isNode()) { + throw new IllegalArgumentException("Attempt to merge property state with node state."); + } + PropertyDiffer result = new PropertyDiffer(data, ((PropertyState) another).data); + + // reset the pInfo to point to the pInfo of another state. + this.data = ((PropertyState) another).data; + // if transient changes should be preserved OR if there are not + // transient changes, return the differ and postpone the effort of + // calculating the diff (the test if this state got internally changed)). + if (keepChanges || transientData == null) { + return result; + } else { + result.dispose(); + transientData.discardValues(); + transientData = null; + modified = true; + } + } + return new SimpleMergeResult(modified); + } + + /** + * @see ItemState#revert() + * @return true if + */ + @Override + public boolean revert() { + if (getStatus() == Status.NEW) { + throw new IllegalStateException("Cannot call revert on a NEW property state."); + } + if (transientData == null) { + return false; + } else { + transientData.discardValues(); + transientData = null; + return true; + } + } + + //------------------------------------------------------< PropertyState >--- + /** + * Returns the type of the property value(s). + * + * @return the type of the property value(s). + * @see PropertyType + * @see QPropertyDefinition#getRequiredType() for the type required by the + * property definition. The effective type may differ from the required + * type if the latter is {@link PropertyType#UNDEFINED}. + */ + public int getType() { + return (transientData == null) ? data.type : transientData.type; + } + + /** + * Returns true if this property is multi-valued, otherwise false. + * + * @return true if this property is multi-valued, otherwise false. + */ + public boolean isMultiValued() { + return multiValued; + } + + /** + * Returns the {@link QPropertyDefinition definition} defined for this + * property state. Note that the definition has been set upon creation of + * this PropertyState. + * + * @return definition of this state + * @throws RepositoryException If an error occurs. + */ + public QPropertyDefinition getDefinition() throws RepositoryException { + if (definition == null) { + /* + Don't pass 'all-node types from parent': + for NEW-states the definition is always set upon creation. + for all other states the definition must be retrieved only taking + the effective node types present on the parent into account + any kind of transiently added mixins must not have an effect + on the definition retrieved for an state that has been persisted + before. The effective NT must be evaluated as if it had been + evaluated upon creating the workspace state. + */ + definition = definitionProvider.getQPropertyDefinition(getParent().getNodeTypeNames(), getName(), getType(), multiValued, ((PropertyEntry) getHierarchyEntry()).getWorkspaceId()); + } + return definition; + } + + /** + * Returns the value(s) of this property. + * + * @return the value(s) of this property. + */ + public QValue[] getValues() { + // if transientData are null the data MUST be present (ev. add check) + return (transientData == null) ? data.values : transientData.values; + } + + /** + * Convenience method for single valued property states. + * + * @return the value of a single valued property. + * @throws ValueFormatException if {@link #isMultiValued()} returns true. + */ + public QValue getValue() throws ValueFormatException { + if (isMultiValued()) { + throw new ValueFormatException("'getValue' may not be called on a multi-valued state."); + } + QValue[] values = getValues(); + if (values == null || values.length == 0) { + return null; + } else { + return values[0]; + } + } + + /** + * Sets the value(s) of this property. + * + * @param values the new values + * @param type the value type + * @throws RepositoryException If an error occurs. + */ + void setValues(QValue[] values, int type) throws RepositoryException { + if (getStatus() == Status.NEW) { + if (data == null) { + data = new PropertyData(type, values, getDefinition()); + } else { + data.setValues(type, values, getDefinition()); + } + } else { + if (transientData == null) { + transientData = new PropertyData(type, values, getDefinition()); + } else { + transientData.setValues(type, values, getDefinition()); + } + markModified(); + } + } + + //------------------------------------------------------------< private >--- + /** + * Checks whether the given property parameters are consistent and satisfy + * the constraints specified by the given definition. The following + * validations/checks are performed: + *

          + *
        • make sure the type is not undefined and matches the type of all + * values given
        • + *
        • make sure all values have the same type.
        • + *
        • check if the type of the property values does comply with the + * requiredType specified in the property's definition
        • + *
        • check if the property values satisfy the value constraints + * specified in the property's definition
        • + *
        + * + * @param values + * @param propertyType + * @param definition + * @throws ConstraintViolationException If any of the validations fails. + * @throws RepositoryException If another error occurs. + */ + private static void validate(QValue[] values, int propertyType, QPropertyDefinition definition) + throws ConstraintViolationException, RepositoryException { + if (propertyType == PropertyType.UNDEFINED) { + throw new RepositoryException("'Undefined' is not a valid property type for existing values."); + } + for (int i = 0; i < values.length; i++) { + if (values[i] != null && propertyType != values[i].getType()) { + throw new ConstraintViolationException("Inconsistent value types: Required type = " + PropertyType.nameFromValue(propertyType) + "; Found value with type = " + PropertyType.nameFromValue(values[i].getType())); + } + } + if (definition.getRequiredType() != PropertyType.UNDEFINED && definition.getRequiredType() != propertyType) { + throw new ConstraintViolationException("RequiredType constraint is not satisfied"); + } + ValueConstraint.checkValueConstraints(definition, values); + } + + /** + * Returns true, if type and/or values of the given property states differ. + * + * @param p1 + * @param p2 + * @return if the 2 PropertyStates are different in terms of + * type and/or values. + */ + private static boolean diff(PropertyData p1, PropertyData p2) { + // compare type + if (p1.type != p2.type) { + return true; + } + + QValue[] vs1 = p1.values; + QValue[] vs2 = p2.values; + if (vs1.length != vs2.length) { + return true; + } else { + for (int i = 0; i < vs1.length; i++) { + boolean eq = (vs1[i] == null) ? vs2[i] == null : vs1[i].equals(vs2[i]); + if (!eq) { + return true; + } + } + } + // no difference + return false; + } + + //--------------------------------------------------------< inner class >--- + /** + * Inner class storing property values and their type. + */ + private static class PropertyData { + private int type; + private QValue[] values; + private boolean discarded; + + private PropertyData(PropertyInfo pInfo) { + this.type = pInfo.getType(); + this.values = pInfo.getValues(); + } + + private PropertyData(int type, QValue[] values, QPropertyDefinition definition) throws ConstraintViolationException, RepositoryException { + setValues(type, values, definition); + } + + private void setValues(int type, QValue[] values, QPropertyDefinition definition) throws ConstraintViolationException, RepositoryException { + // make sure the arguments are consistent and do not violate the + // given property definition. + validate(values, type, definition); + // note: discarding original values is deferred to operation completion + // -> see JCR-2880 + + this.type = type; + this.values = (values == null) ? QValue.EMPTY_ARRAY : values; + } + + private void discardValues() { + if (!discarded && values != null) { + for (int i = 0; i < values.length; i++) { + if (values[i] != null) { + // make sure temporarily allocated data is discarded + // before overwriting it (see QValue#discard()) + values[i].discard(); + } + } + discarded = true; + } + } + } + + /** + * Helper class for delayed determination of property differences. + */ + private static class PropertyDiffer implements MergeResult { + + private final PropertyData oldData; + private final PropertyData newData; + + PropertyDiffer(PropertyData oldData, PropertyData newData) { + super(); + this.oldData = oldData; + this.newData = newData; + } + + public boolean modified() { + if (oldData.discarded || newData.discarded) { + // cannot calculate the diff any more -> return true. + String msg = " Diff cannot be calculated: " + ((oldData.discarded) ? "Old property data" : "New property data") + " have already been discarded."; + log.debug(msg); + return true; + } + return diff(oldData, newData); + } + + public void dispose() { + oldData.discardValues(); + } + } + +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/SessionItemStateManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/SessionItemStateManager.java new file mode 100644 index 00000000000..5b1701610f9 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/SessionItemStateManager.java @@ -0,0 +1,609 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.SessionImpl; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.jcr2spi.operation.AddNode; +import org.apache.jackrabbit.jcr2spi.operation.AddProperty; +import org.apache.jackrabbit.jcr2spi.operation.IgnoreOperation; +import org.apache.jackrabbit.jcr2spi.operation.Move; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.OperationVisitor; +import org.apache.jackrabbit.jcr2spi.operation.Remove; +import org.apache.jackrabbit.jcr2spi.operation.ReorderNodes; +import org.apache.jackrabbit.jcr2spi.operation.SetMixin; +import org.apache.jackrabbit.jcr2spi.operation.SetTree; +import org.apache.jackrabbit.jcr2spi.operation.SetPrimaryType; +import org.apache.jackrabbit.jcr2spi.operation.SetPropertyValue; +import org.apache.jackrabbit.jcr2spi.operation.TransientOperationVisitor; +import org.apache.jackrabbit.jcr2spi.util.ReferenceChangeTracker; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SessionItemStateManager ... + */ +public class SessionItemStateManager extends TransientOperationVisitor implements UpdatableItemStateManager { + + private static Logger log = LoggerFactory.getLogger(SessionItemStateManager.class); + + /** + * State manager that allows updates + */ + private final UpdatableItemStateManager workspaceItemStateMgr; + + /** + * State manager for the transient items + */ + private final TransientItemStateManager transientStateMgr; + + private final ItemStateValidator validator; + + private final QValueFactory qValueFactory; + + private final SessionImpl mgrProvider; + + /** + * Creates a new SessionItemStateManager instance. + * + * @param workspaceItemStateMgr + * @param validator + * @param qValueFactory + * @param isf + * @param mgrProvider + */ + public SessionItemStateManager(UpdatableItemStateManager workspaceItemStateMgr, + ItemStateValidator validator, + QValueFactory qValueFactory, + ItemStateFactory isf, SessionImpl mgrProvider) { + + this.workspaceItemStateMgr = workspaceItemStateMgr; + this.transientStateMgr = new TransientItemStateManager(); + isf.addCreationListener(transientStateMgr); + + this.validator = validator; + this.qValueFactory = qValueFactory; + this.mgrProvider = mgrProvider; + } + + /** + * @return true if this manager has any transient state; + * false otherwise. + */ + public boolean hasPendingChanges() { + return transientStateMgr.hasPendingChanges(); + } + + /** + * This will save state and all descendants items of + * state that are transiently modified in a single step. If + * this operation fails, no item will have been saved. + * + * @param state the root state of the update operation + */ + public void save(ItemState state) throws ReferentialIntegrityException, + InvalidItemStateException, RepositoryException { + // shortcut, if no modifications are present + if (!transientStateMgr.hasPendingChanges()) { + return; + } + // collect the changes to be saved + ChangeLog changeLog = transientStateMgr.getChangeLog(state, true); + if (!changeLog.isEmpty()) { + // only pass changelog if there are transient modifications available + // for the specified item and its descendants. + workspaceItemStateMgr.execute(changeLog); + // remove states and operations just processed from the transient ISM + transientStateMgr.dispose(changeLog); + // now its save to clear the changeLog + changeLog.reset(); + } + } + + /** + * This will undo all changes made to state and descendant + * items of state inside this item state manager. + * + * @param itemState the root state of the cancel operation. + * @throws ConstraintViolationException + * @throws RepositoryException if undoing changes made to state + * and descendant items is not a closed set of changes. That is, at least + * another item needs to be canceled as well in another sub-tree. + */ + public void undo(ItemState itemState) throws ConstraintViolationException, RepositoryException { + // short cut + if (!transientStateMgr.hasPendingChanges()) { + return; + } + ChangeLog changeLog = transientStateMgr.getChangeLog(itemState, false); + if (!changeLog.isEmpty()) { + // let changelog revert all changes + changeLog.undo(); + // remove transient states and related operations from the t-statemanager + transientStateMgr.dispose(changeLog); + changeLog.reset(); + } + } + + /** + * Adjust references at the end of a successful + * {@link Session#importXML(String, InputStream, int) XML import}. + * + * @param refTracker + * @throws ConstraintViolationException + * @throws RepositoryException + */ + public void adjustReferences(ReferenceChangeTracker refTracker) throws ConstraintViolationException, RepositoryException { + Iterator it = refTracker.getReferences(); + while (it.hasNext()) { + PropertyState propState = it.next(); + boolean modified = false; + QValue[] values = propState.getValues(); + QValue[] newVals = new QValue[values.length]; + for (int i = 0; i < values.length; i++) { + QValue val = values[i]; + QValue adjusted = refTracker.getMappedReference(val, qValueFactory); + if (adjusted != null) { + newVals[i] = adjusted; + modified = true; + } else { + // reference doesn't need adjusting, just copy old value + newVals[i] = val; + } + } + if (modified) { + int options = ItemStateValidator.CHECK_LOCK | + ItemStateValidator.CHECK_VERSIONING | + ItemStateValidator.CHECK_CONSTRAINTS; + setPropertyStateValue(propState, newVals, PropertyType.REFERENCE, options); + } + } + // make sure all entries are removed + refTracker.clear(); + } + + //------------------------------------------< UpdatableItemStateManager >--- + /** + * {@inheritDoc} + * @see UpdatableItemStateManager#execute(Operation) + */ + public void execute(Operation operation) throws RepositoryException { + operation.accept(this); + } + + /** + * {@inheritDoc} + * @see UpdatableItemStateManager#execute(ChangeLog) + */ + public void execute(ChangeLog changes) throws RepositoryException { + throw new UnsupportedOperationException("Not implemented for SessionItemStateManager"); + } + + /** + * {@inheritDoc} + * @see UpdatableItemStateManager#dispose() + */ + public void dispose() { + // discard all transient changes + transientStateMgr.dispose(); + // dispose our (i.e. 'local') state manager + workspaceItemStateMgr.dispose(); + } + + //---------------------------------------------------< OperationVisitor >--- + /** + * @see OperationVisitor#visit(AddNode) + */ + public void visit(AddNode operation) throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + NodeState parent = operation.getParentState(); + ItemDefinitionProvider defProvider = mgrProvider.getItemDefinitionProvider(); + QNodeDefinition def = defProvider.getQNodeDefinition(parent.getAllNodeTypeNames(), operation.getNodeName(), operation.getNodeTypeName()); + List newStates = addNodeState(parent, operation.getNodeName(), operation.getNodeTypeName(), operation.getUuid(), def, operation.getOptions()); + operation.addedState(newStates); + + if (!(operation instanceof IgnoreOperation)) { + transientStateMgr.addOperation(operation); + } + } + + /** + * @see OperationVisitor#visit(AddProperty) + */ + public void visit(AddProperty operation) throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + NodeState parent = operation.getParentState(); + Name propertyName = operation.getPropertyName(); + QPropertyDefinition pDef = operation.getDefinition(); + int targetType = pDef.getRequiredType(); + if (targetType == PropertyType.UNDEFINED) { + targetType = operation.getPropertyType(); + if (targetType == PropertyType.UNDEFINED) { + targetType = PropertyType.STRING; + } + } + + addPropertyState(parent, propertyName, targetType, operation.getValues(), pDef, operation.getOptions()); + + if (!(operation instanceof IgnoreOperation)) { + transientStateMgr.addOperation(operation); + } + } + + /** + * @see OperationVisitor#visit(org.apache.jackrabbit.jcr2spi.operation.SetTree) + */ + public void visit(SetTree operation) throws RepositoryException { + transientStateMgr.addOperation(operation); + } + + /** + * @see OperationVisitor#visit(Move) + */ + public void visit(Move operation) throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + + // retrieve states and assert they are modifiable + NodeState srcState = operation.getSourceState(); + NodeState srcParent = operation.getSourceParentState(); + NodeState destParent = operation.getDestinationParentState(); + + // state validation: move-Source can be removed from old/added to new parent + validator.checkRemoveItem(srcState, operation.getOptions()); + validator.checkAddNode(destParent, operation.getDestinationName(), + srcState.getNodeTypeName(), operation.getOptions()); + + // retrieve applicable definition at the new place + ItemDefinitionProvider defProvider = mgrProvider.getItemDefinitionProvider(); + QNodeDefinition newDefinition = defProvider.getQNodeDefinition(destParent.getAllNodeTypeNames(), operation.getDestinationName(), srcState.getNodeTypeName()); + + // perform the move (modifying states) + srcParent.moveChildNodeEntry(destParent, srcState, operation.getDestinationName(), newDefinition); + + // remember operation + transientStateMgr.addOperation(operation); + } + + /** + * @see OperationVisitor#visit(Remove) + */ + public void visit(Remove operation) throws ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + ItemState state = operation.getRemoveState(); + removeItemState(state, operation.getOptions()); + + transientStateMgr.addOperation(operation); + operation.getParentState().markModified(); + } + + /** + * @see OperationVisitor#visit(SetMixin) + */ + public void visit(SetMixin operation) throws ConstraintViolationException, AccessDeniedException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + // NOTE: nodestate is only modified upon save of the changes! + Name[] mixinNames = operation.getMixinNames(); + NodeState nState = operation.getNodeState(); + NodeEntry nEntry = nState.getNodeEntry(); + + // assert the existence of the property entry and set the array of + // mixinNames to be set on the corresponding property state + PropertyEntry mixinEntry = nEntry.getPropertyEntry(NameConstants.JCR_MIXINTYPES); + if (mixinNames.length > 0) { + // update/create corresponding property state + if (mixinEntry != null) { + // execute value of existing property + PropertyState pState = mixinEntry.getPropertyState(); + setPropertyStateValue(pState, getQValues(mixinNames, qValueFactory), PropertyType.NAME, operation.getOptions()); + } else { + // create new jcr:mixinTypes property + ItemDefinitionProvider defProvider = mgrProvider.getItemDefinitionProvider(); + QPropertyDefinition pd = defProvider.getQPropertyDefinition(nState.getAllNodeTypeNames(), NameConstants.JCR_MIXINTYPES, PropertyType.NAME, true); + QValue[] mixinValue = getQValues(mixinNames, qValueFactory); + addPropertyState(nState, pd.getName(), pd.getRequiredType(), mixinValue, pd, operation.getOptions()); + } + nState.markModified(); + transientStateMgr.addOperation(operation); + } else if (mixinEntry != null) { + // remove the jcr:mixinTypes property state if already present + PropertyState pState = mixinEntry.getPropertyState(); + removeItemState(pState, operation.getOptions()); + + nState.markModified(); + transientStateMgr.addOperation(operation); + } // else: empty Name array and no mixin-prop-entry (should not occur) + } + + /** + * @see OperationVisitor#visit(SetPrimaryType) + */ + public void visit(SetPrimaryType operation) throws ConstraintViolationException, RepositoryException { + // NOTE: nodestate is only modified upon save of the changes! + Name primaryName = operation.getPrimaryTypeName(); + NodeState nState = operation.getNodeState(); + NodeEntry nEntry = nState.getNodeEntry(); + + // detect obvious node type conflicts + + EffectiveNodeTypeProvider entProvider = mgrProvider.getEffectiveNodeTypeProvider(); + + // try to build new effective node type (will throw in case of conflicts) + Name[] mixins = nState.getMixinTypeNames(); + List all = new ArrayList(Arrays.asList(mixins)); + all.add(primaryName); + // retrieve effective to assert validity of arguments + entProvider.getEffectiveNodeType(all.toArray(new Name[all.size()])); + + // modify the value of the jcr:primaryType property entry without + // changing the node state itself + PropertyEntry pEntry = nEntry.getPropertyEntry(NameConstants.JCR_PRIMARYTYPE); + PropertyState pState = pEntry.getPropertyState(); + setPropertyStateValue(pState, getQValues(new Name[] {primaryName}, qValueFactory), PropertyType.NAME, operation.getOptions()); + + // mark the affected node state modified and remember the operation + nState.markModified(); + transientStateMgr.addOperation(operation); + } + + /** + * @see OperationVisitor#visit(SetPropertyValue) + */ + public void visit(SetPropertyValue operation) throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + PropertyState pState = operation.getPropertyState(); + setPropertyStateValue(pState, operation.getValues(), operation.getValueType(), operation.getOptions()); + transientStateMgr.addOperation(operation); + } + + /** + * @see OperationVisitor#visit(ReorderNodes) + */ + public void visit(ReorderNodes operation) throws ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + NodeState parent = operation.getParentState(); + // modify the parent node state + parent.reorderChildNodeEntries(operation.getInsertNode(), operation.getBeforeNode()); + // remember the operation + transientStateMgr.addOperation(operation); + } + + //--------------------------------------------< Internal State Handling >--- + /** + * + * @param parent + * @param propertyName + * @param propertyType + * @param values + * @param pDef + * @param options int used to validate the given params. Note, that the options + * differ depending if the 'addProperty' is called regularly or to create + * auto-created (or protected) properties. + * @throws LockException + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws ItemExistsException + * @throws NoSuchNodeTypeException + * @throws UnsupportedRepositoryOperationException + * @throws VersionException + * @throws RepositoryException + */ + private PropertyState addPropertyState(NodeState parent, Name propertyName, + int propertyType, QValue[] values, + QPropertyDefinition pDef, int options) + throws LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, NoSuchNodeTypeException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + + validator.checkAddProperty(parent, propertyName, pDef, options); + // create property state + return transientStateMgr.createNewPropertyState(propertyName, parent, pDef, values, propertyType); + } + + private List addNodeState(NodeState parent, Name nodeName, Name nodeTypeName, + String uuid, QNodeDefinition definition, int options) + throws RepositoryException, ConstraintViolationException, AccessDeniedException, + UnsupportedRepositoryOperationException, NoSuchNodeTypeException, + ItemExistsException, VersionException { + + // check if add node is possible. note, that the options differ if + // the 'addNode' is called from inside a regular add-node to create + // autocreated child nodes that may be 'protected'. + validator.checkAddNode(parent, nodeName, nodeTypeName, options); + // a new NodeState doesn't have mixins defined yet -> ent is ent of primarytype + EffectiveNodeType ent = mgrProvider.getEffectiveNodeTypeProvider().getEffectiveNodeType(nodeTypeName); + + if (nodeTypeName == null) { + // no primary node type specified, + // try default primary type from definition + nodeTypeName = definition.getDefaultPrimaryType(); + if (nodeTypeName == null) { + String msg = "No applicable node type could be determined for " + nodeName; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + } + + List addedStates = new ArrayList(); + + // create new nodeState. NOTE, that the uniqueID is not added to the + // state for consistency between 'addNode' and importXML + NodeState nodeState = transientStateMgr.createNewNodeState(nodeName, null, nodeTypeName, definition, parent); + addedStates.add(nodeState); + if (uuid != null) { + QValue[] value = getQValues(uuid, qValueFactory); + ItemDefinitionProvider defProvider = mgrProvider.getItemDefinitionProvider(); + QPropertyDefinition pDef = defProvider.getQPropertyDefinition(NameConstants.MIX_REFERENCEABLE, NameConstants.JCR_UUID, PropertyType.STRING, false); + addedStates.add(addPropertyState(nodeState, NameConstants.JCR_UUID, PropertyType.STRING, value, pDef, 0)); + } + + // add 'auto-create' properties defined in node type + for (QPropertyDefinition pd : ent.getAutoCreateQPropertyDefinitions()) { + if (!nodeState.hasPropertyName(pd.getName())) { + QValue[] autoValue = computeSystemGeneratedPropertyValues(nodeState, pd); + if (autoValue != null) { + int propOptions = ItemStateValidator.CHECK_NONE; + // execute 'addProperty' without adding operation. + addedStates.add(addPropertyState(nodeState, pd.getName(), pd.getRequiredType(), autoValue, pd, propOptions)); + } + } + } + + // recursively add 'auto-create' child nodes defined in node type + for (QNodeDefinition nd : ent.getAutoCreateQNodeDefinitions()) { + // execute 'addNode' without adding the operation. + int opt = ItemStateValidator.CHECK_LOCK | ItemStateValidator.CHECK_COLLISION; + addedStates.addAll(addNodeState(nodeState, nd.getName(), nd.getDefaultPrimaryType(), null, nd, opt)); + } + return addedStates; + } + + private void removeItemState(ItemState itemState, int options) throws RepositoryException { + validator.checkRemoveItem(itemState, options); + // recursively remove the given state and all child states. + boolean success = false; + try { + itemState.getHierarchyEntry().transientRemove(); + success = true; + } finally { + if (!success) { + // TODO: TOBEFIXED undo state modifications + } + } + } + + /** + * + * @param propState + * @param iva + * @param valueType + * @throws ValueFormatException + * @throws LockException + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws ItemExistsException + * @throws UnsupportedRepositoryOperationException + * @throws VersionException + * @throws RepositoryException + */ + private void setPropertyStateValue(PropertyState propState, QValue[] iva, + int valueType, int options) + throws ValueFormatException, LockException, ConstraintViolationException, AccessDeniedException, ItemExistsException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + // assert that the property can be modified. + validator.checkSetProperty(propState, options); + propState.setValues(iva, valueType); + } + + /** + * Computes the values of well-known system (i.e. protected) properties + * as well as auto-created properties which define default value(s) + * + * @param parent + * @param def + * @return the computed values + */ + private QValue[] computeSystemGeneratedPropertyValues(NodeState parent, + QPropertyDefinition def) + throws RepositoryException { + QValue[] genValues = null; + QValue[] qDefaultValues = def.getDefaultValues(); + if (qDefaultValues != null && qDefaultValues.length > 0) { + genValues = qDefaultValues; + } else if (def.isAutoCreated()) { + // handle known predefined nodetypes that declare auto-created + // properties without default values + Name declaringNT = def.getDeclaringNodeType(); + Name name = def.getName(); + + if (NameConstants.JCR_PRIMARYTYPE.equals(name)) { + // jcr:primaryType property + genValues = new QValue[]{qValueFactory.create(parent.getNodeTypeName())}; + + } else if (NameConstants.JCR_MIXINTYPES.equals(name)) { + // jcr:mixinTypes property + Name[] mixins = parent.getMixinTypeNames(); + genValues = getQValues(mixins, qValueFactory); + + } else if (NameConstants.JCR_CREATED.equals(name) + && (NameConstants.MIX_CREATED.equals(declaringNT) || + NameConstants.NT_HIERARCHYNODE.equals(declaringNT))) { + + // jcr:created property of a mix:created or nt:hierarchyNode + genValues = new QValue[]{qValueFactory.create(Calendar.getInstance())}; + + } else if (NameConstants.JCR_CREATEDBY.equals(name) + && NameConstants.MIX_CREATED.equals(declaringNT)) { + // jcr:createdBy property of a mix:created + genValues = new QValue[]{qValueFactory.create(mgrProvider.getUserID(), PropertyType.STRING)}; + + } else if (NameConstants.JCR_LASTMODIFIED.equals(name) + && NameConstants.MIX_LASTMODIFIED.equals(declaringNT)) { + // jcr:lastModified property of a mix:lastModified + genValues = new QValue[]{qValueFactory.create(Calendar.getInstance())}; + + } else if (NameConstants.JCR_LASTMODIFIEDBY.equals(name) + && NameConstants.MIX_LASTMODIFIED.equals(declaringNT)) { + // jcr:lastModifiedBy property of a mix:lastModified + genValues = new QValue[]{qValueFactory.create(mgrProvider.getUserID(), PropertyType.STRING)}; + + } else { + // ask the SPI implementation for advice + genValues = qValueFactory.computeAutoValues(def); + } + } + return genValues; + } + + /** + * @param qNames + * @param factory + * @return An array of QValue objects from the given Names + */ + private static QValue[] getQValues(Name[] qNames, QValueFactory factory) throws RepositoryException { + QValue[] ret = new QValue[qNames.length]; + for (int i = 0; i < qNames.length; i++) { + ret[i] = factory.create(qNames[i]); + } + return ret; + } + + private static QValue[] getQValues(String uniqueID, QValueFactory factory) throws RepositoryException { + return new QValue[] {factory.create(uniqueID, PropertyType.STRING)}; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/Status.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/Status.java new file mode 100644 index 00000000000..fe4d903ba33 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/Status.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +/** + * Status... + */ +public final class Status { + + /** + * Avoid instantiation + */ + private Status() {} + + public static final int _UNDEFINED_ = -1; + + /** + * A state once read from persistent storage has been set to invalid. This + * means that the state needs to be re-fetched from persistent storage when + * accessed the next time. + */ + public static final int INVALIDATED = 0; + + /** + * 'existing', i.e. persistent state + */ + public static final int EXISTING = 1; + /** + * 'existing', i.e. persistent state that has been transiently modified (copy-on-write) + */ + public static final int EXISTING_MODIFIED = 2; + /** + * 'existing', i.e. persistent state that has been transiently removed (copy-on-write) + */ + public static final int EXISTING_REMOVED = 3; + /** + * 'new' state + */ + public static final int NEW = 4; + /** + * 'existing', i.e. persistent state that has been persistently modified by somebody else + */ + public static final int STALE_MODIFIED = 5; + /** + * 'existing', i.e. persistent state that has been destroyed by somebody else + */ + public static final int STALE_DESTROYED = 6; + + /** + * Temporary status used to mark a state, this is permanently modified + * either by saving transient changes, by workspace operations or by + * external modification. + */ + public static final int MODIFIED = 7; + + /** + * a new state was removed and is now 'removed' + * or an existing item has been removed by a workspace operation or + * by an external modification. + */ + public static final int REMOVED = 8; + + private static final String[] STATUS_NAMES = new String[] { + "INVALIDATED", + "EXISTING", + "EXISTING_MODIFIED", + "EXISTING_REMOVED", + "NEW", + "STALE_MODIFIED", + "STALE_DESTROYED", + "MODIFIED", + "REMOVED" + }; + + /** + * Returns true if the given status is a terminal status, i.e. + * the given status one of: + *
          + *
        • {@link #REMOVED}
        • + *
        • {@link #STALE_DESTROYED}
        • + *
        + * + * @param status + * @return true if the given status is terminal. + */ + public static boolean isTerminal(int status) { + return status == REMOVED || status == STALE_DESTROYED; + } + + /** + * Returns true if this item state is valid, that is its status + * is one of: + *
          + *
        • {@link #EXISTING}
        • + *
        • {@link #EXISTING_MODIFIED}
        • + *
        • {@link #NEW}
        • + *
        + * + * @param status + * @return true if the given status indicates a valid ItemState. + */ + public static boolean isValid(int status) { + return status == EXISTING || status == EXISTING_MODIFIED || status == NEW; + } + + /** + * Returns true if status is one of: + *
          + *
        • {@link #STALE_DESTROYED}
        • + *
        • {@link #STALE_MODIFIED}
        • + *
        + * + * @param status the status to check. + * @return true if status indicates that an item + * state is stale. + */ + public static boolean isStale(int status) { + return status == STALE_DESTROYED || status == STALE_MODIFIED; + } + + /** + * Returns true if status is one of: + *
          + *
        • {@link #EXISTING_MODIFIED}
        • + *
        • {@link #EXISTING_REMOVED}
        • + *
        • {@link #NEW}
        • + *
        + * + * @param status the status to check. + * @return true if status indicates that an item + * state is transiently modified. + */ + public static boolean isTransient(int status) { + return status == EXISTING_MODIFIED || status == EXISTING_REMOVED || status == NEW; + } + + /** + * Returns true, if the status of an item state can be changed from + * oldStatus to newStatus, and false if the + * change is illegal or if any of the given status flags is illegal. + * + * @param oldStatus + * @param newStatus + * @return true if a status change from oldStatus to + * newStatus is allowed or if the two status are the same. + */ + public static boolean isValidStatusChange(int oldStatus, int newStatus) { + if (oldStatus == newStatus) { + return true; + } + boolean isValid = false; + // valid status changes for session-states + switch (newStatus) { + case INVALIDATED: + isValid = (oldStatus == EXISTING); // invalidate + break; + case EXISTING: + switch (oldStatus) { + case INVALIDATED: /* refresh */ + case NEW: /* save */ + case EXISTING_MODIFIED: /* save, revert */ + case EXISTING_REMOVED: /* revert */ + case STALE_MODIFIED: /* revert */ + case MODIFIED: + isValid = true; + break; + /* REMOVED, STALE_DESTROYED -> false */ + } + break; + case EXISTING_MODIFIED: + isValid = (oldStatus == EXISTING); + break; + case EXISTING_REMOVED: + isValid = (oldStatus == EXISTING || oldStatus == EXISTING_MODIFIED); + break; + case STALE_MODIFIED: + case STALE_DESTROYED: + isValid = (oldStatus == EXISTING_MODIFIED || oldStatus == EXISTING_REMOVED || oldStatus == STALE_MODIFIED); + break; + case REMOVED: + // removal always possible -> getNewStatus(int, int) + isValid = true; + break; + case MODIFIED: + // except for NEW states an external modification is always valid + if (oldStatus != NEW) { + isValid = true; + } + break; + /* default: + NEW cannot change state to NEW -> false */ + } + return isValid; + } + + /** + * Returns the given newStatusHint unless the new status + * collides with a pending modification or removal which results in a + * stale item state. + * + * @param oldStatus + * @param newStatusHint + * @return new status that takes transient modification/removal into account. + */ + public static int getNewStatus(int oldStatus, int newStatusHint) { + int newStatus; + switch (newStatusHint) { + case Status.MODIFIED: + // underlying state has been modified by external changes + if (oldStatus == Status.EXISTING || oldStatus == Status.INVALIDATED) { + // temporarily set the state to MODIFIED in order to inform listeners. + newStatus = Status.MODIFIED; + } else if (oldStatus == Status.EXISTING_MODIFIED) { + newStatus = Status.STALE_MODIFIED; + } else { + // old status is EXISTING_REMOVED (or any other) => ignore. + // a NEW state may never be marked modified. + newStatus = oldStatus; + } + break; + case Status.REMOVED: + if (oldStatus == Status.EXISTING_MODIFIED || oldStatus == Status.STALE_MODIFIED) { + newStatus = Status.STALE_DESTROYED; + } else { + // applies both to NEW or to any other status + newStatus = newStatusHint; + } + break; + default: + newStatus = newStatusHint; + break; + + } + return newStatus; + } + + /** + * @param status A valid status constant. + * @return Human readable status name for the given int. + */ + public static String getName(int status) { + if (status == _UNDEFINED_) { + return "_UNDEFINED_"; + } + if (status < 0 || status >= STATUS_NAMES.length) { + throw new IllegalArgumentException("Invalid status " + status); + } + return STATUS_NAMES[status]; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientISFactory.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientISFactory.java new file mode 100644 index 00000000000..538829a3fc7 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientISFactory.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.jcr2spi.state; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Set; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TransientISFactory... + */ +public final class TransientISFactory extends AbstractItemStateFactory implements TransientItemStateFactory, ItemStateCreationListener { + + private static Logger log = LoggerFactory.getLogger(TransientISFactory.class); + + private final ItemStateFactory workspaceStateFactory; + private final ItemDefinitionProvider defProvider; + + public TransientISFactory(AbstractItemStateFactory workspaceStateFactory, ItemDefinitionProvider defProvider) { + this.workspaceStateFactory = workspaceStateFactory; + this.defProvider = defProvider; + // start listening to 'creations' on the workspaceStateFactory (and + // consequently skip an extra notification if the has been built by the + // workspaceStateFactory. + workspaceStateFactory.addCreationListener(this); + } + + //------------------------------------------< TransientItemStateFactory >--- + /** + * @see TransientItemStateFactory#createNewNodeState(NodeEntry , Name, QNodeDefinition) + */ + public NodeState createNewNodeState(NodeEntry entry, Name nodetypeName, + QNodeDefinition definition) { + + NodeState nodeState = new NodeState(entry, nodetypeName, Name.EMPTY_ARRAY, this, definition, defProvider); + + // notify listeners that a node state has been created + notifyCreated(nodeState); + + return nodeState; + } + + /** + * @see TransientItemStateFactory#createNewPropertyState(PropertyEntry, QPropertyDefinition, QValue[], int) + */ + public PropertyState createNewPropertyState(PropertyEntry entry, QPropertyDefinition definition, QValue[] values, int propertyType) throws RepositoryException { + PropertyState propState = new PropertyState(entry, this, definition, defProvider, values, propertyType); + // notify listeners that a property state has been created + notifyCreated(propState); + return propState; + } + + //---------------------------------------------------< ItemStateFactory >--- + /** + * @see ItemStateFactory#createRootState(NodeEntry) + */ + public NodeState createRootState(NodeEntry entry) throws ItemNotFoundException, RepositoryException { + NodeState state = workspaceStateFactory.createRootState(entry); + return state; + } + + /** + * @see ItemStateFactory#createNodeState(NodeId,NodeEntry) + */ + public NodeState createNodeState(NodeId nodeId, NodeEntry entry) + throws ItemNotFoundException, RepositoryException { + NodeState state = workspaceStateFactory.createNodeState(nodeId, entry); + return state; + } + + /** + * @see ItemStateFactory#createDeepNodeState(NodeId, NodeEntry) + */ + public NodeState createDeepNodeState(NodeId nodeId, NodeEntry anyParent) + throws ItemNotFoundException, RepositoryException { + NodeState state = workspaceStateFactory.createDeepNodeState(nodeId, anyParent); + return state; + } + + /** + * @see ItemStateFactory#createPropertyState(PropertyId, PropertyEntry) + */ + public PropertyState createPropertyState(PropertyId propertyId, + PropertyEntry entry) + throws ItemNotFoundException, RepositoryException { + PropertyState state = workspaceStateFactory.createPropertyState(propertyId, entry); + return state; + + } + + /** + * @see ItemStateFactory#createDeepPropertyState(PropertyId, NodeEntry) + */ + public PropertyState createDeepPropertyState(PropertyId propertyId, NodeEntry anyParent) throws ItemNotFoundException, RepositoryException { + PropertyState state = workspaceStateFactory.createDeepPropertyState(propertyId, anyParent); + return state; + } + + /** + * @see ItemStateFactory#getChildNodeInfos(NodeId) + */ + public Iterator getChildNodeInfos(NodeId nodeId) throws ItemNotFoundException, RepositoryException { + return workspaceStateFactory.getChildNodeInfos(nodeId); + } + + /** + * @see ItemStateFactory#getNodeReferences(NodeState,org.apache.jackrabbit.spi.Name,boolean) + */ + public Iterator getNodeReferences(NodeState nodeState, Name propertyName, boolean weak) { + if (nodeState.getStatus() == Status.NEW) { + Set t = Collections.emptySet(); + return t.iterator(); + } + return workspaceStateFactory.getNodeReferences(nodeState, propertyName, weak); + } + + //------------------------------------------< ItemStateCreationListener >--- + /** + * @see ItemStateCreationListener#created(ItemState) + */ + public void created(ItemState state) { + log.debug("ItemState created by WorkspaceItemStateFactory"); + notifyCreated(state); + } + + /** + * @see ItemStateCreationListener#statusChanged(ItemState, int) + */ + public void statusChanged(ItemState state, int previousStatus) { + // ignore + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientItemStateFactory.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientItemStateFactory.java new file mode 100644 index 00000000000..3d106b0baf0 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientItemStateFactory.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.jcr2spi.state; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; + +import javax.jcr.RepositoryException; + +/** + * TransientItemStateFactory extends the item state factory and + * adds new methods for creating node states and property states that are new. + */ +public interface TransientItemStateFactory extends ItemStateFactory { + + /** + * Creates a transient child NodeState with the given + * name. + * + * @param entry + * @param nodeTypeName + * @param definition + * @return the created NodeState + */ + public NodeState createNewNodeState(NodeEntry entry, + Name nodeTypeName, + QNodeDefinition definition); + + /** + * Creates a transient PropertyState. + * + * @param entry + * @param definition + * @param values + * @param propertyType + * @return the created PropertyState. + */ + public PropertyState createNewPropertyState(PropertyEntry entry, + QPropertyDefinition definition, + QValue[] values, int propertyType) + throws RepositoryException; +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientItemStateManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientItemStateManager.java new file mode 100644 index 00000000000..48ef658eca4 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/TransientItemStateManager.java @@ -0,0 +1,439 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.commons.collections.iterators.IteratorChain; +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TransientItemStateManager adds support for transient changes on + * {@link ItemState}s and also provides methods to create new item states. + * While all other modifications can be invoked on the item state instances itself, + * creating a new node state is done using + * {@link #createNewNodeState(Name, String, Name, QNodeDefinition, NodeState)} + * and + * {@link #createNewPropertyState(Name, NodeState, QPropertyDefinition, QValue[], int)}. + */ +public class TransientItemStateManager implements ItemStateCreationListener { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(TransientItemStateManager.class); + + /** + * Added states + */ + private final Set addedStates = new LinkedHashSet(); + + /** + * Modified states + */ + private final Set modifiedStates = new LinkedHashSet(); + + /** + * Removed states + */ + private final Set removedStates = new LinkedHashSet(); + /** + * Stale states + */ + private final Set staleStates = new LinkedHashSet(); + + /** + * Set of operations + */ + private final Set operations = new LinkedHashSet(); + + /** + * + */ + TransientItemStateManager() { + } + + /** + * @return the operations that have been recorded until now. + */ + Iterator getOperations() { + return operations.iterator(); + } + + /** + * Add the given operation to the list of operations to be recorded within + * this TransientItemStateManager. + * + * @param operation + */ + void addOperation(Operation operation) { + operations.add(operation); + } + + /** + * @return true if this transient ISM has pending changes. + */ + boolean hasPendingChanges() { + return !operations.isEmpty(); + } + + /** + * Create the change log for the tree starting at target. This + * includes a check if the ChangeLog to be created is totally 'self-contained' + * and independent; items within the scope of this update operation (i.e. + * below the target) must not have dependencies outside of this tree (e.g. + * moving a node requires that the target node including both old and new + * parents are saved). + * + * @param target + * @param throwOnStale Throws InvalidItemStateException if either the given + * ItemState or any of its descendants is stale and the flag is true. + * @return + * @throws InvalidItemStateException if a stale ItemState is + * encountered while traversing the state hierarchy. The changeLog + * might have been populated with some transient item states. A client should + * therefore not reuse the changeLog if such an exception is thrown. + * @throws RepositoryException if state is a new item state. + */ + ChangeLog getChangeLog(ItemState target, boolean throwOnStale) throws InvalidItemStateException, ConstraintViolationException, RepositoryException { + // fail-fast test: check status of this item's state + if (target.getStatus() == Status.NEW) { + String msg = "Cannot save/revert an item with status NEW (" +target+ ")."; + log.debug(msg); + throw new RepositoryException(msg); + } + if (throwOnStale && Status.isStale(target.getStatus())) { + String msg = "Attempt to save/revert an item, that has been externally modified (" +target+ ")."; + log.debug(msg); + throw new InvalidItemStateException(msg); + } + + Set ops = new LinkedHashSet(); + Set affectedStates = new LinkedHashSet(); + + HierarchyEntry he = target.getHierarchyEntry(); + if (he.getParent() == null) { + // the root entry -> the complete change log can be used for + // simplicity. collecting ops, states can be omitted. + if (throwOnStale && !staleStates.isEmpty()) { + String msg = "Cannot save changes: States has been modified externally."; + log.debug(msg); + throw new InvalidItemStateException(msg); + } else { + affectedStates.addAll(staleStates); + } + ops.addAll(operations); + affectedStates.addAll(addedStates); + affectedStates.addAll(modifiedStates); + affectedStates.addAll(removedStates); + } else { + // not root entry: + // - check if there is a stale state in the scope (save only) + if (throwOnStale) { + for (ItemState state : staleStates) { + if (containedInTree(target, state)) { + String msg = "Cannot save changes: States has been modified externally."; + log.debug(msg); + throw new InvalidItemStateException(msg); + } + } + } + // - collect all affected states within the scope of save/undo + Iterator[] its = new Iterator[] { + addedStates.iterator(), + removedStates.iterator(), + modifiedStates.iterator() + }; + IteratorChain chain = new IteratorChain(its); + if (!throwOnStale) { + chain.addIterator(staleStates.iterator()); + } + while (chain.hasNext()) { + ItemState state = (ItemState) chain.next(); + if (containedInTree(target, state)) { + affectedStates.add(state); + } + } + // - collect the set of operations and + // check if the affected states listed by the operations are all + // listed in the modified,removed or added states collected by this + // changelog. + for (Operation op : operations) { + Collection opStates = op.getAffectedItemStates(); + for (ItemState state : opStates) { + if (affectedStates.contains(state)) { + // operation needs to be included + if (!affectedStates.containsAll(opStates)) { + // incomplete changelog: need to save a parent as well + String msg = "ChangeLog is not self contained."; + throw new ConstraintViolationException(msg); + } + // no violation: add operation an stop iteration over + // all affected states present in the operation. + ops.add(op); + break; + } + } + } + } + + ChangeLog cl = new ChangeLog(target, ops, affectedStates); + return cl; + } + + /** + * Creates a new transient {@link NodeState} that does not overlay any other + * {@link NodeState}. + * + * @param nodeName the name of the NodeState to create. + * @param uniqueID the uniqueID of the NodeState to create or + * null if the created NodeState + * cannot be identified by a unique ID. + * @param nodeTypeName name of the node type of the new node state. + * @param definition The definition for the new node state. + * @param parent the parent of the new node state. + * @return a new transient {@link NodeState}. + */ + NodeState createNewNodeState(Name nodeName, String uniqueID, Name nodeTypeName, + QNodeDefinition definition, NodeState parent) + throws RepositoryException { + NodeEntry ne = ((NodeEntry) parent.getHierarchyEntry()).addNewNodeEntry(nodeName, uniqueID, nodeTypeName, definition); + try { + parent.markModified(); + } catch (RepositoryException e) { + ne.remove(); + throw e; + } + return ne.getNodeState(); + } + + /** + * Creates a new transient property state for a given parent + * node state. + * + * @param propName the name of the property state to create. + * @param parent the node state where to the new property is added. + * @param definition + * @return the created property state. + * @throws ItemExistsException if parent already has a property + * with the given name. + * @throws ConstraintViolationException + * @throws RepositoryException + */ + PropertyState createNewPropertyState(Name propName, NodeState parent, + QPropertyDefinition definition, + QValue[] values, int propertyType) + throws ItemExistsException, ConstraintViolationException, RepositoryException { + // NOTE: callers must make sure, the property type is not 'undefined' + NodeEntry nodeEntry = (NodeEntry) parent.getHierarchyEntry(); + PropertyEntry pe = nodeEntry.addNewPropertyEntry(propName, definition, values, propertyType); + try { + parent.markModified(); + } catch (RepositoryException e) { + pe.remove(); + throw e; + } + return pe.getPropertyState(); + } + + /** + * Disposes this transient item state manager. Clears all references to + * transiently modified item states. + */ + void dispose() { + addedStates.clear(); + modifiedStates.clear(); + removedStates.clear(); + staleStates.clear(); + // also clear all operations + operations.clear(); + } + + /** + * Remove the states and operations listed in the changeLog from internal + * list of modifications. + * + * @param subChangeLog + */ + void dispose(ChangeLog subChangeLog) { + Set affectedStates = subChangeLog.getAffectedStates(); + addedStates.removeAll(affectedStates); + modifiedStates.removeAll(affectedStates); + removedStates.removeAll(affectedStates); + staleStates.removeAll(affectedStates); + + operations.removeAll(subChangeLog.getOperations()); + } + + /** + * A state has been removed. If the state is not a new state + * (not in the collection of added ones), then remove + * it from the modified states collection and add it to the + * removed states collection. + * + * @param state state that has been removed + */ + private void removed(ItemState state) { + if (!addedStates.remove(state)) { + modifiedStates.remove(state); + } + removedStates.add(state); + } + + /** + * + * @param parent + * @param state + * @return + */ + private static boolean containedInTree(ItemState parent, ItemState state) { + HierarchyEntry he = state.getHierarchyEntry(); + HierarchyEntry pHe = parent.getHierarchyEntry(); + // short cuts first + if (he == pHe || he.getParent() == pHe) { + return true; + } + if (!parent.isNode() || he == pHe.getParent()) { + return false; + } + // none of the simple cases: walk up hierarchy + HierarchyEntry pe = he.getParent(); + while (pe != null) { + if (pe == pHe) { + return true; + } + pe = pe.getParent(); + } + + // state isn't descendant of 'parent' + return false; + } + + //-----------------------------------------< ItemStateLifeCycleListener >--- + /** + * Depending on status of the given state adapt change log. + * E.g. a revert on states will reset the status from 'existing modified' to + * 'existing'. A state which changes from 'existing' to 'existing modified' + * will go into the modified set of the change log, etc. + * + * @see ItemStateLifeCycleListener#statusChanged(ItemState, int) + */ + public void statusChanged(ItemState state, int previousStatus) { + /* + Update the collections of states that were transiently modified. + NOTE: cleanup of operations is omitted here. this is expected to + occur upon {@link ChangeLog#save()} and {@link ChangeLog#undo()}. + External modifications in contrast that clash with transient modifications + render the corresponding states stale. + */ + switch (state.getStatus()) { + case (Status.EXISTING): + switch (previousStatus) { + case Status.EXISTING_MODIFIED: + // was modified and got persisted or reverted + modifiedStates.remove(state); + break; + case Status.EXISTING_REMOVED: + // was transiently removed and is now reverted + removedStates.remove(state); + break; + case Status.STALE_MODIFIED: + // was modified and stale and is now reverted + staleStates.remove(state); + break; + case Status.NEW: + // was new and has been saved now + addedStates.remove(state); + break; + //default: + // INVALIDATED, MODIFIED ignore. no effect to transient modifications. + // any other status change is invalid -> see Status#isValidStatusChange(int, int + } + break; + case Status.EXISTING_MODIFIED: + // transition from EXISTING to EXISTING_MODIFIED + modifiedStates.add(state); + break; + case (Status.EXISTING_REMOVED): + // transition from EXISTING or EXISTING_MODIFIED to EXISTING_REMOVED + removed(state); + break; + case (Status.REMOVED): + switch (previousStatus) { + case Status.EXISTING_REMOVED: + // was transiently removed and removal was persisted. + // -> ignore + break; + case Status.NEW: + // a new entry was removed again: remember as removed + // in order to keep the operations and the affected + // states in sync + removed(state); + break; + } + // in any case: stop listening to status changes + state.removeListener(this); + break; + case Status.STALE_DESTROYED: + case Status.STALE_MODIFIED: + /** + state is stale due to external modification -> move it to + the collection of stale item states. + validation omitted for only 'existing_modified' states can + become stale see {@link Status#isValidStatusChange(int, int)} + */ + modifiedStates.remove(state); + staleStates.add(state); + break; + case Status.MODIFIED: + case Status.INVALIDATED: + // MODIFIED, INVALIDATED: ignore. + log.debug("Item " + state.getName() + " changed status from " + Status.getName(previousStatus) + " to " + Status.getName(state.getStatus()) + "."); + break; + default: + log.error("ItemState "+ state.getName() + " has invalid status: " + state.getStatus()); + } + } + + //-----------------------------------------< ItemStateCreationListener >--- + /** + * @see ItemStateCreationListener#created(ItemState) + */ + public void created(ItemState state) { + // new state has been created + if (state.getStatus() == Status.NEW) { + addedStates.add(state); + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/UpdatableItemStateManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/UpdatableItemStateManager.java new file mode 100644 index 00000000000..081ce6cbb36 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/UpdatableItemStateManager.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.jcr2spi.state; + +import org.apache.jackrabbit.jcr2spi.operation.Operation; + +import javax.jcr.RepositoryException; + +/** + * An ItemStateManager that deals with state modifications. + */ +public interface UpdatableItemStateManager { + + /** + * Executes the given operation and modifies the affected item states accordingly. + * + * @param operation + * @throws RepositoryException + */ + public void execute(Operation operation) throws RepositoryException; + + /** + * Executes the operations passed with the given change log and modifies the + * affected item states accordingly. + * + * @param changes + * @throws RepositoryException + */ + public void execute(ChangeLog changes) throws RepositoryException; + + /** + * Disposes this UpdatableItemStateManager and frees resources. + */ + void dispose(); +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/WorkspaceItemStateFactory.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/WorkspaceItemStateFactory.java new file mode 100644 index 00000000000..6ad50b7cdd6 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/state/WorkspaceItemStateFactory.java @@ -0,0 +1,516 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.state; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.ItemInfoCache.Entry; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * WorkspaceItemStateFactory... + */ +public class WorkspaceItemStateFactory extends AbstractItemStateFactory { + private static Logger log = LoggerFactory.getLogger(WorkspaceItemStateFactory.class); + + private final RepositoryService service; + private final SessionInfo sessionInfo; + private final ItemDefinitionProvider definitionProvider; + + private final ItemInfoCache cache; + + public WorkspaceItemStateFactory(RepositoryService service, SessionInfo sessionInfo, + ItemDefinitionProvider definitionProvider, ItemInfoCache cache) { + + this.service = service; + this.sessionInfo = sessionInfo; + this.definitionProvider = definitionProvider; + this.cache = cache; + } + + public NodeState createRootState(NodeEntry entry) throws ItemNotFoundException, RepositoryException { + IdFactory idFactory = service.getIdFactory(); + PathFactory pf = service.getPathFactory(); + + return createNodeState(idFactory.createNodeId((String) null, pf.getRootPath()), entry); + } + + /** + * Creates the node with information retrieved from the RepositoryService. + */ + public NodeState createNodeState(NodeId nodeId, NodeEntry entry) throws ItemNotFoundException, + RepositoryException { + + try { + Entry cached = cache.getNodeInfo(nodeId); + ItemInfo info; + if (isUpToDate(cached, entry)) { + info = cached.info; + } else { + // otherwise retrieve item info from service and cache the whole batch + Iterator infos = service.getItemInfos(sessionInfo, nodeId); + info = first(infos, cache, entry.getGeneration()); + if (info == null || !info.denotesNode()) { + throw new ItemNotFoundException("NodeId: " + nodeId); + } + } + + assertMatchingPath(info, entry); + return createNodeState((NodeInfo) info, entry); + } + catch (PathNotFoundException e) { + throw new ItemNotFoundException(e); + } + } + + /** + * Creates the node with information retrieved from the RepositoryService. + * Intermediate entries are created as needed. + */ + public NodeState createDeepNodeState(NodeId nodeId, NodeEntry anyParent) throws ItemNotFoundException, + RepositoryException { + + try { + // Get item info from cache + Iterator infos = null; + Entry cached = cache.getNodeInfo(nodeId); + ItemInfo info; + if (cached == null) { + // or from service if not in cache + infos = service.getItemInfos(sessionInfo, nodeId); + info = first(infos, null, 0); + if (info == null || !info.denotesNode()) { + throw new ItemNotFoundException("NodeId: " + nodeId); + } + } else { + info = cached.info; + } + + // Build the hierarchy entry for the item info + HierarchyEntry entry = createHierarchyEntries(info, anyParent); + if (entry == null || !entry.denotesNode()) { + throw new ItemNotFoundException( + "HierarchyEntry does not belong to any existing ItemInfo. No ItemState was created."); + } else { + // Now we can check whether the item info from the cache is up to date + long generation = entry.getGeneration(); + if (isOutdated(cached, entry)) { + // if not, retrieve the item info from the service and put the whole batch into the cache + infos = service.getItemInfos(sessionInfo, nodeId); + info = first(infos, cache, generation); + } else if (infos != null) { + // Otherwise put the whole batch retrieved from the service earlier into the cache + cache.put(info, generation); + first(infos, cache, generation); + } + + assertMatchingPath(info, entry); + return createNodeState((NodeInfo) info, (NodeEntry) entry); + } + } + catch (PathNotFoundException e) { + throw new ItemNotFoundException(e); + } + } + + /** + * Creates the PropertyState with information retrieved from the RepositoryService. + */ + public PropertyState createPropertyState(PropertyId propertyId, PropertyEntry entry) + throws ItemNotFoundException, RepositoryException { + + try { + // Get item info from cache and use it if up to date + Entry cached = cache.getPropertyInfo(propertyId); + ItemInfo info; + if (isUpToDate(cached, entry)) { + info = cached.info; + } else { + // otherwise retrieve item info from service and cache the whole batch + Iterator infos = service.getItemInfos(sessionInfo, propertyId); + info = first(infos, cache, entry.getGeneration()); + if (info == null || info.denotesNode()) { + throw new ItemNotFoundException("PropertyId: " + propertyId); + } + } + + assertMatchingPath(info, entry); + return createPropertyState((PropertyInfo) info, entry); + } + catch (PathNotFoundException e) { + throw new ItemNotFoundException(e); + } + } + + /** + * Creates the PropertyState with information retrieved from the RepositoryService. + * Intermediate entries are created as needed. + */ + public PropertyState createDeepPropertyState(PropertyId propertyId, NodeEntry anyParent) + throws RepositoryException { + + try { + // Get item info from cache + Iterator infos = null; + Entry cached = cache.getPropertyInfo(propertyId); + ItemInfo info; + if (cached == null) { + // or from service if not in cache + infos = service.getItemInfos(sessionInfo, propertyId); + info = first(infos, null, 0); + if (info == null || info.denotesNode()) { + throw new ItemNotFoundException("PropertyId: " + propertyId); + } + } else { + info = cached.info; + } + + // Build the hierarchy entry for the item info + HierarchyEntry entry = createHierarchyEntries(info, anyParent); + if (entry == null || entry.denotesNode()) { + throw new ItemNotFoundException( + "HierarchyEntry does not belong to any existing ItemInfo. No ItemState was created."); + } else { + long generation = entry.getGeneration(); + if (isOutdated(cached, entry)) { + // if not, retrieve the item info from the service and put the whole batch into the cache + infos = service.getItemInfos(sessionInfo, propertyId); + info = first(infos, cache, generation); + } else if (infos != null) { + // Otherwise put the whole batch retrieved from the service earlier into the cache + cache.put(info, generation); + first(infos, cache, generation); + } + + assertMatchingPath(info, entry); + return createPropertyState((PropertyInfo) info, (PropertyEntry) entry); + } + + } catch (PathNotFoundException e) { + throw new ItemNotFoundException(e); + } + } + + public Iterator getChildNodeInfos(NodeId nodeId) throws ItemNotFoundException, + RepositoryException { + + return service.getChildInfos(sessionInfo, nodeId); + } + + public Iterator getNodeReferences(NodeState nodeState, Name propertyName, boolean weak) { + NodeEntry entry = nodeState.getNodeEntry(); + + // Shortcut + if (entry.getUniqueID() == null || !entry.hasPropertyEntry(NameConstants.JCR_UUID)) { + // for sure not referenceable + Set t = Collections.emptySet(); + return t.iterator(); + } + + // Has a unique ID and is potentially mix:referenceable. Try to retrieve references + try { + return service.getReferences(sessionInfo, entry.getWorkspaceId(), propertyName, weak); + } catch (RepositoryException e) { + log.debug("Unable to determine references to {}", nodeState); + Set t = Collections.emptySet(); + return t.iterator(); + } + } + + //------------------------------------------------------------< private >--- + + /** + * Returns the first item in the iterator if it exists. Otherwise returns null. + * If cache is not null, caches all items by the given + * generation. + */ + private static ItemInfo first(Iterator infos, ItemInfoCache cache, long generation) { + ItemInfo first = null; + if (infos.hasNext()) { + first = infos.next(); + if (cache != null) { + cache.put(first, generation); + } + } + + if (cache != null) { + while (infos.hasNext()) { + cache.put(infos.next(), generation); + } + } + + return first; + } + + /** + * Create the node state with the information from info. + * + * @param info the NodeInfo to use to create the NodeState. + * @param entry the hierarchy entry for of this state + * @return the new NodeState. + * @throws ItemNotFoundException + * @throws RepositoryException + */ + private NodeState createNodeState(NodeInfo info, NodeEntry entry) throws ItemNotFoundException, + RepositoryException { + + // Make sure the entry has the correct ItemId + // this may not be the case, if the hierarchy has not been completely + // resolved yet -> if uniqueID is present, set it on this entry or on + // the appropriate parent entry + String uniqueID = info.getId().getUniqueID(); + Path path = info.getId().getPath(); + if (path == null) { + entry.setUniqueID(uniqueID); + } else if (uniqueID != null) { + // uniqueID that applies to a parent NodeEntry -> get parentEntry + NodeEntry parent = getAncestor(entry, path.getLength()); + parent.setUniqueID(uniqueID); + } + + int previousStatus = entry.getStatus(); + if (Status.isTransient(previousStatus) || Status.isStale(previousStatus)) { + log.debug("Node has pending changes; omit resetting the state."); + return entry.getNodeState(); + } + + // update NodeEntry from the information present in the NodeInfo (prop entries) + List propNames = new ArrayList(); + for (Iterator it = info.getPropertyIds(); it.hasNext(); ) { + PropertyId pId = it.next(); + Name propertyName = pId.getName(); + propNames.add(propertyName); + } + try { + entry.setPropertyEntries(propNames); + } catch (ItemExistsException e) { + // should not get here + log.error("Internal error", e); + } + + // unless the child-info are omitted by the SPI impl -> make sure + // the child entries the node entry are initialized or updated. + Iterator childInfos = info.getChildInfos(); + if (childInfos != null) { + entry.setNodeEntries(childInfos); + } + + // now build or update the nodestate itself + NodeState tmp = new NodeState(entry, info, this, definitionProvider); + entry.setItemState(tmp); + + NodeState nState = entry.getNodeState(); + if (previousStatus == Status._UNDEFINED_) { + // tmp state was used as resolution for the given entry i.e. the + // entry was not available before. otherwise the 2 states were + // merged. see HierarchyEntryImpl#setItemState + notifyCreated(nState); + } else { + notifyUpdated(nState, previousStatus); + } + return nState; + } + + /** + * Create the property state with the information from info. + * + * @param info the PropertyInfo to use to create the PropertyState. + * @param entry the hierarchy entry for of this state + * @return the new PropertyState. + * @throws RepositoryException + */ + private PropertyState createPropertyState(PropertyInfo info, PropertyEntry entry) + throws RepositoryException { + + // make sure uuid part of id is correct + String uniqueID = info.getId().getUniqueID(); + if (uniqueID != null) { + // uniqueID always applies to a parent NodeEntry -> get parentEntry + NodeEntry parent = getAncestor(entry, info.getId().getPath().getLength()); + parent.setUniqueID(uniqueID); + } + + int previousStatus = entry.getStatus(); + if (Status.isTransient(previousStatus) || Status.isStale(previousStatus)) { + log.debug("Property has pending changes; omit resetting the state."); + return entry.getPropertyState(); + } + + // now build or update the nodestate itself + PropertyState tmp = new PropertyState(entry, info, this, definitionProvider); + entry.setItemState(tmp); + + PropertyState pState = entry.getPropertyState(); + if (previousStatus == Status._UNDEFINED_) { + // tmp state was used as resolution for the given entry i.e. the + // entry was not available before. otherwise the 2 states were + // merged. see HierarchyEntryImpl#setItemState + notifyCreated(pState); + } else { + notifyUpdated(pState, previousStatus); + } + return pState; + } + + /** + * Create missing hierarchy entries on the path from anyParent to the path + * of the itemInfo. + * + * @param info + * @param anyParent + * @return the hierarchy entry for info + * @throws RepositoryException + */ + private HierarchyEntry createHierarchyEntries(ItemInfo info, NodeEntry anyParent) + throws RepositoryException { + + // Calculate relative path of missing entries + Path anyParentPath = anyParent.getWorkspacePath(); + Path relPath = anyParentPath.computeRelativePath(info.getPath()); + Path.Element[] missingElems = relPath.getElements(); + + NodeEntry entry = anyParent; + int last = missingElems.length - 1; + for (int i = 0; i <= last; i++) { + if (missingElems[i].denotesParent()) { + // Walk up the hierarchy for 'negative' paths + // until the smallest common root is found + entry = entry.getParent(); + } else if (missingElems[i].denotesName()) { + // Add missing elements starting from the smallest common root + Name name = missingElems[i].getName(); + int index = missingElems[i].getNormalizedIndex(); + + if (i == last && !info.denotesNode()) { + return entry.getOrAddPropertyEntry(name); + } else { + entry = createNodeEntry(entry, name, index); + } + } + } + return entry; + } + + private NodeEntry createNodeEntry(NodeEntry parentEntry, Name name, int index) throws RepositoryException { + Entry cached = cache.getNodeInfo(parentEntry.getWorkspaceId()); + if (isUpToDate(cached, parentEntry)) { + Iterator childInfos = cached.info.getChildInfos(); + if (childInfos != null) { + parentEntry.setNodeEntries(childInfos); + } + } + + return parentEntry.getOrAddNodeEntry(name, index, null); + } + + /** + * Returns true if cache is not null and + * the cached entry is up to date. + * @param cacheEntry + * @param entry + * @return + * @throws RepositoryException + */ + private static boolean isUpToDate(Entry cacheEntry, HierarchyEntry entry) throws RepositoryException { + return cacheEntry != null && + cacheEntry.generation >= entry.getGeneration() && + isMatchingPath(cacheEntry.info, entry); + } + + /** + * Returns true if cache is not null and + * the cached entry is not up to date. + * @param cacheEntry + * @param entry + * @return + * @throws RepositoryException + */ + private static boolean isOutdated(Entry cacheEntry, HierarchyEntry entry) throws RepositoryException { + return cacheEntry != null && + (cacheEntry.generation < entry.getGeneration() || + !isMatchingPath(cacheEntry.info, entry)); + } + + private static boolean isMatchingPath(ItemInfo info, HierarchyEntry entry) throws RepositoryException { + Path infoPath = info.getPath(); + Path wspPath = entry.getWorkspacePath(); + return infoPath.equals(wspPath); + } + + /** + * Validation check: Path of the given ItemInfo must match to the Path of + * the HierarchyEntry. This is required for Items that are identified by + * a uniqueID that may move within the hierarchy upon restore or clone. + * + * @param info + * @param entry + * @throws RepositoryException + */ + private static void assertMatchingPath(ItemInfo info, HierarchyEntry entry) throws RepositoryException { + if (!isMatchingPath(info, entry)) { + // TODO: handle external move of nodes (parents) identified by uniqueID + throw new ItemNotFoundException("HierarchyEntry " + entry.getWorkspacePath() + " does not match ItemInfo " + info.getPath()); + } + } + + /** + * @param entry + * @param degree + * @return the ancestor entry at the specified degree. + */ + private static NodeEntry getAncestor(HierarchyEntry entry, int degree) { + NodeEntry parent = entry.getParent(); + degree--; + while (parent != null && degree > 0) { + parent = parent.getParent(); + degree--; + } + if (degree != 0) { + log.error("Parent of degree {} does not exist.", degree); + throw new IllegalArgumentException(); + } + return parent; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/LogUtil.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/LogUtil.java new file mode 100644 index 00000000000..85266ccdd00 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/LogUtil.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.jcr2spi.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.jcr2spi.state.ItemState; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; + +import javax.jcr.RepositoryException; +import javax.jcr.NamespaceException; + +/** + * LogUtil... + */ +public final class LogUtil { + + private static Logger log = LoggerFactory.getLogger(LogUtil.class); + + /** + * Avoid instantiation + */ + private LogUtil() {} + + /** + * Failsafe conversion of internal Path to JCR path for use in + * error messages etc. + * + * @param qPath path to convert + * @param pathResolver + * @return JCR path + */ + public static String safeGetJCRPath(Path qPath, PathResolver pathResolver) { + try { + return pathResolver.getJCRPath(qPath); + } catch (NamespaceException e) { + log.error("failed to convert " + qPath + " to JCR path."); + // return string representation of internal path as a fallback + return qPath.toString(); + } + } + + /** + * Failsafe conversion of an ItemState to JCR path for use in + * error messages etc. + * + * @param itemState + * @param pathResolver + * @return JCR path + */ + public static String safeGetJCRPath(ItemState itemState, PathResolver pathResolver) { + try { + return safeGetJCRPath(itemState.getHierarchyEntry().getPath(), pathResolver); + } catch (RepositoryException e) { + log.error("failed to convert " + itemState.toString() + " to JCR path."); + return itemState.toString(); + } + } + + /** + * Failsafe conversion of a Name to a JCR name for use in + * error messages etc. + * + * @param qName + * @param nameResolver + * @return JCR name or String representation of the given Name + * in case the resolution fails. + */ + public static String saveGetJCRName(Name qName, NameResolver nameResolver) { + try { + return nameResolver.getJCRName(qName); + } catch (NamespaceException e) { + log.error("failed to convert " + qName + " to JCR name."); + return qName.toString(); + } + } + + /** + * Failsafe conversion of an ItemId to a human readable string + * resolving the path part of the specified id using the given path resolver. + * + * @param itemId + * @param pathResolver + * @return a String representation of the given ItemId. + */ + public static String saveGetIdString(ItemId itemId, PathResolver pathResolver) { + Path p = itemId.getPath(); + if (p == null || pathResolver == null) { + return itemId.toString(); + } else { + StringBuffer bf = new StringBuffer(); + String uniqueID = itemId.getUniqueID(); + if (uniqueID != null) { + bf.append(uniqueID).append(" - "); + } + String jcrPath; + try { + jcrPath = pathResolver.getJCRPath(p); + } catch (NamespaceException e) { + jcrPath = p.toString(); + } + bf.append(jcrPath); + return bf.toString(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/ReferenceChangeTracker.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/ReferenceChangeTracker.java new file mode 100644 index 00000000000..bd6a209c87a --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/ReferenceChangeTracker.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.jcr2spi.util; + +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Simple helper class that can be 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 correcting depending on the uuid mappings. + */ +public class ReferenceChangeTracker { + + private static Logger log = LoggerFactory.getLogger(ReferenceChangeTracker.class); + + /** + * mapping to of mix:referenceable nodes + */ + private final Map uuidMap = new HashMap(); + /** + * list of processed reference properties that might need correction + */ + private final List references = new ArrayList(); + + /** + * Creates a new instance. + */ + public ReferenceChangeTracker() { + } + + /** + * Resets all internal state. + */ + public void clear() { + uuidMap.clear(); + references.clear(); + } + + /** + * Store the given uuid mapping for later lookup using + * #adjustReferences(UpdatableItemStateManager, ItemStateValidator). + * + * @param oldUUID + * @param newUUID + */ + public void mappedUUIDs(String oldUUID, String newUUID) { + if (oldUUID == null || oldUUID.equals(newUUID)) { + // only remember if uuid exists and has changed + return; + } + uuidMap.put(oldUUID, newUUID); + } + + /** + * Returns the new UUID to which oldUUID has been mapped + * or null if no such mapping exists. + * + * @param oldReference old uuid represented by the given QValue. + * @param factory + * @return mapped new QValue of the reference value or null if no such mapping exists + * @see #mappedUUIDs(String,String) + */ + public QValue getMappedReference(QValue oldReference, QValueFactory factory) { + QValue remapped = null; + if (oldReference.getType() == PropertyType.REFERENCE) { + try { + String oldValue = oldReference.getString(); + if (uuidMap.containsKey(oldValue)) { + String newValue = uuidMap.get(oldValue); + remapped = factory.create(newValue, PropertyType.REFERENCE); + } + } catch (RepositoryException e) { + log.error("Unexpected error while creating internal value.", e); + } + } + return remapped; + } + + /** + * Store the given reference property for later resolution. + * + * @param refPropertyState reference property state + */ + public void processedReference(PropertyState refPropertyState) { + // make sure only not-null states of type Reference are remembered + if (refPropertyState != null && refPropertyState.getType() == PropertyType.REFERENCE) { + references.add(refPropertyState); + } + } + + public Iterator getReferences() { + return references.iterator(); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/StateUtility.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/StateUtility.java new file mode 100644 index 00000000000..b879c28a8c6 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/util/StateUtility.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.util; + +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +import javax.jcr.RepositoryException; + +/** + * StateUtility... + */ +public final class StateUtility { + + /** + * Avoid instantiation + */ + private StateUtility() {} + + /** + * + * @param ps + * @return + * @throws IllegalArgumentException if the name of the PropertyState is NOT + * {@link NameConstants#JCR_MIXINTYPES} + */ + public static Name[] getMixinNames(PropertyState ps) { + if (!NameConstants.JCR_MIXINTYPES.equals(ps.getName())) { + throw new IllegalArgumentException(); + } + if (ps.getStatus() == Status.REMOVED) { + return Name.EMPTY_ARRAY; + } else { + QValue[] values = ps.getValues(); + Name[] newMixins = new Name[values.length]; + for (int i = 0; i < values.length; i++) { + try { + newMixins[i] = values[i].getName(); + } catch (RepositoryException e) { + // ignore: should never occur. + } + } + return newMixins; + } + } + + public static Name getPrimaryTypeName(PropertyState ps) throws RepositoryException { + if (!NameConstants.JCR_PRIMARYTYPE.equals(ps.getName())) { + throw new IllegalArgumentException(); + } + QValue[] values = ps.getValues(); + return values[0].getName(); + } + + public static boolean isUuidOrMixin(Name propName) { + return NameConstants.JCR_UUID.equals(propName) || NameConstants.JCR_MIXINTYPES.equals(propName); + } + + public static boolean isMovedState(NodeState state) { + if (state.isRoot()) { + // the root state cannot be moved + return false; + } else { + NodeEntry ne = state.getNodeEntry(); + return ne.isTransientlyMoved(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionHistoryImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionHistoryImpl.java new file mode 100644 index 00000000000..401e495f649 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionHistoryImpl.java @@ -0,0 +1,421 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.version; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; + +import org.apache.jackrabbit.commons.iterator.FrozenNodeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.VersionIteratorAdapter; +import org.apache.jackrabbit.jcr2spi.ItemLifeCycleListener; +import org.apache.jackrabbit.jcr2spi.LazyItemIterator; +import org.apache.jackrabbit.jcr2spi.NodeImpl; +import org.apache.jackrabbit.jcr2spi.SessionImpl; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * VersionHistoryImpl... + */ +public class VersionHistoryImpl extends NodeImpl implements VersionHistory { + + private static Logger log = LoggerFactory.getLogger(VersionHistoryImpl.class); + + private final NodeEntry vhEntry; + private final NodeEntry labelNodeEntry; + + public VersionHistoryImpl(SessionImpl session, NodeState state, ItemLifeCycleListener[] listeners) + throws VersionException, RepositoryException { + super(session, state, listeners); + this.vhEntry = (NodeEntry) state.getHierarchyEntry(); + + // retrieve hierarchy entry of the jcr:versionLabels node + labelNodeEntry = vhEntry.getNodeEntry(NameConstants.JCR_VERSIONLABELS, Path.INDEX_DEFAULT, true); + if (labelNodeEntry == null) { + String msg = "Unexpected error: nt:versionHistory requires a mandatory, autocreated child node jcr:versionLabels."; + log.error(msg); + throw new VersionException(msg); + } + } + + //-----------------------------------------------------< VersionHistory >--- + /** + * @see VersionHistory#getVersionableUUID() + */ + public String getVersionableUUID() throws RepositoryException { + return getVersionableIdentifier(); + } + + /** + * @see VersionHistory#getRootVersion() + */ + public Version getRootVersion() throws RepositoryException { + checkStatus(); + NodeEntry vEntry = vhEntry.getNodeEntry(NameConstants.JCR_ROOTVERSION, Path.INDEX_DEFAULT, true); + if (vEntry == null) { + String msg = "Unexpected error: VersionHistory state does not contain a root version child node entry."; + log.error(msg); + throw new RepositoryException(msg); + } + return (Version) getItemManager().getItem(vEntry); + } + + /** + * @see VersionHistory#getAllVersions() + */ + public VersionIterator getAllVersions() throws RepositoryException { + checkStatus(); + refreshEntry(vhEntry); + Iterator childIter = vhEntry.getNodeEntries(); + List versionEntries = new ArrayList(); + // all child-nodes except from jcr:versionLabels point to Versions. + while (childIter.hasNext()) { + NodeEntry entry = childIter.next(); + if (!NameConstants.JCR_VERSIONLABELS.equals(entry.getName())) { + versionEntries.add(entry); + } + } + return new LazyItemIterator(getItemManager(), new RangeIteratorAdapter(versionEntries)); + } + + /** + * @see VersionHistory#getAllLinearVersions() + */ + public VersionIterator getAllLinearVersions() throws RepositoryException { + checkStatus(); + + // TODO: improve and use lazy loading of versions as needed. + // TODO: change session.getNodeByUUID to Session.getNodeByIdentifier as soon as implemented + + List versions = new ArrayList(); + Version rootV = getRootVersion(); + Node vn = session.getNodeByUUID(getVersionableUUID()); + Version v = vn.getBaseVersion(); + while (v != null && !rootV.isSame(v)) { + versions.add(0, v); + v = v.getLinearPredecessor(); + } + versions.add(0, rootV); + + return new VersionIteratorAdapter(versions); + } + + /** + * @see VersionHistory#getAllFrozenNodes() + */ + public NodeIterator getAllFrozenNodes() throws RepositoryException { + return new FrozenNodeIteratorAdapter(getAllVersions()); + } + + /** + * @see VersionHistory#getAllLinearFrozenNodes() + */ + public NodeIterator getAllLinearFrozenNodes() throws RepositoryException { + return new FrozenNodeIteratorAdapter(getAllLinearVersions()); + } + + /** + * @see VersionHistory#getVersion(String) + */ + public Version getVersion(String versionName) throws VersionException, RepositoryException { + checkStatus(); + NodeState vState = getVersionState(versionName); + return (Version) getItemManager().getItem(vState.getHierarchyEntry()); + } + + /** + * @see VersionHistory#getVersionByLabel(String) + */ + public Version getVersionByLabel(String label) throws RepositoryException { + checkStatus(); + return getVersionByLabel(getQLabel(label)); + } + + /** + * @see VersionHistory#addVersionLabel(String, String, boolean) + */ + public void addVersionLabel(String versionName, String label, boolean moveLabel) throws VersionException, RepositoryException { + checkStatus(); + Name qLabel = getQLabel(label); + NodeState vState = getVersionState(versionName); + // delegate to version manager that operates on workspace directly + session.getVersionStateManager().addVersionLabel((NodeState) getItemState(), vState, qLabel, moveLabel); + } + + /** + * @see VersionHistory#removeVersionLabel(String) + */ + public void removeVersionLabel(String label) throws VersionException, RepositoryException { + checkStatus(); + Name qLabel = getQLabel(label); + Version version = getVersionByLabel(qLabel); + NodeState vState = getVersionState(version.getName()); + // delegate to version manager that operates on workspace directly + session.getVersionStateManager().removeVersionLabel((NodeState) getItemState(), vState, qLabel); + } + + /** + * @see VersionHistory#hasVersionLabel(String) + */ + public boolean hasVersionLabel(String label) throws RepositoryException { + checkStatus(); + Name l = getQLabel(label); + Name[] qLabels = getQLabels(); + for (int i = 0; i < qLabels.length; i++) { + if (qLabels[i].equals(l)) { + return true; + } + } + return false; + } + + /** + * @see VersionHistory#hasVersionLabel(Version, String) + */ + public boolean hasVersionLabel(Version version, String label) throws VersionException, RepositoryException { + // check-status performed within checkValidVersion + checkValidVersion(version); + String vUUID = version.getUUID(); + Name l = getQLabel(label); + + Name[] qLabels = getQLabels(); + for (int i = 0; i < qLabels.length; i++) { + if (qLabels[i].equals(l)) { + String uuid = getVersionByLabel(qLabels[i]).getUUID(); + return vUUID.equals(uuid); + } + } + return false; + } + + /** + * @see VersionHistory#getVersionLabels() + */ + public String[] getVersionLabels() throws RepositoryException { + checkStatus(); + Name[] qLabels = getQLabels(); + String[] labels = new String[qLabels.length]; + + for (int i = 0; i < qLabels.length; i++) { + labels[i] = session.getNameResolver().getJCRName(qLabels[i]); + } + return labels; + } + + /** + * @see VersionHistory#getVersionLabels(Version) + */ + public String[] getVersionLabels(Version version) throws VersionException, RepositoryException { + // check-status performed within checkValidVersion + checkValidVersion(version); + String vUUID = version.getUUID(); + + List vlabels = new ArrayList(); + Name[] qLabels = getQLabels(); + for (int i = 0; i < qLabels.length; i++) { + String uuid = getVersionByLabel(qLabels[i]).getUUID(); + if (vUUID.equals(uuid)) { + vlabels.add(session.getNameResolver().getJCRName(qLabels[i])); + } + } + return vlabels.toArray(new String[vlabels.size()]); + } + + /** + * @see VersionHistory#removeVersion(String) + */ + public void removeVersion(String versionName) throws ReferentialIntegrityException, + AccessDeniedException, UnsupportedRepositoryOperationException, + VersionException, RepositoryException { + checkStatus(); + NodeState vState = getVersionState(versionName); + session.getVersionStateManager().removeVersion((NodeState) getItemState(), vState); + } + + /** + * @see VersionHistory#getVersionableIdentifier() + */ + public String getVersionableIdentifier() throws RepositoryException { + checkStatus(); + return getProperty(NameConstants.JCR_VERSIONABLEUUID).getString(); + } + + //---------------------------------------------------------------< Item >--- + /** + * + * @param otherItem + * @return + * @see Item#isSame(Item) + */ + @Override + public boolean isSame(Item otherItem) throws RepositoryException { + checkStatus(); + if (otherItem instanceof VersionHistoryImpl) { + // since all version histories are referenceable, protected and live + // in the same workspace, a simple comparison of the UUIDs is sufficient. + VersionHistoryImpl other = ((VersionHistoryImpl) otherItem); + return vhEntry.getUniqueID().equals(other.vhEntry.getUniqueID()); + } + return false; + } + + //-----------------------------------------------------------< ItemImpl >--- + /** + * + * @throws UnsupportedRepositoryOperationException + * @throws ConstraintViolationException + * @throws RepositoryException + */ + @Override + protected void checkIsWritable() throws UnsupportedRepositoryOperationException, ConstraintViolationException, RepositoryException { + super.checkIsWritable(); + throw new ConstraintViolationException("VersionHistory is protected"); + } + + /** + * Always returns false + * + * @throws RepositoryException + * @see NodeImpl#isWritable() + */ + @Override + protected boolean isWritable() throws RepositoryException { + super.isWritable(); + return false; + } + //------------------------------------------------------------< private >--- + /** + * + * @return + */ + private Name[] getQLabels() throws RepositoryException { + refreshEntry(labelNodeEntry); + List labelNames = new ArrayList(); + for (Iterator it = labelNodeEntry.getPropertyEntries(); it.hasNext(); ) { + PropertyEntry pe = it.next(); + if (! NameConstants.JCR_PRIMARYTYPE.equals(pe.getName()) && + ! NameConstants.JCR_MIXINTYPES.equals(pe.getName())) { + labelNames.add(pe.getName()); + } + } + return labelNames.toArray(new Name[labelNames.size()]); + } + + /** + * + * @param versionName + * @return + * @throws VersionException + * @throws RepositoryException + */ + private NodeState getVersionState(String versionName) throws VersionException, RepositoryException { + try { + Name vName = session.getNameResolver().getQName(versionName); + refreshEntry(vhEntry); + NodeEntry vEntry = vhEntry.getNodeEntry(vName, Path.INDEX_DEFAULT, true); + if (vEntry == null) { + throw new VersionException("Version '" + versionName + "' does not exist in this version history."); + } else { + return vEntry.getNodeState(); + } + } catch (org.apache.jackrabbit.spi.commons.conversion.NameException e) { + throw new RepositoryException(e); + } + } + + /** + * + * @param qLabel + * @return + * @throws VersionException + * @throws RepositoryException + */ + private Version getVersionByLabel(Name qLabel) throws VersionException, RepositoryException { + refreshEntry(labelNodeEntry); + // retrieve reference property value -> and retrieve referenced node + PropertyEntry pEntry = labelNodeEntry.getPropertyEntry(qLabel, true); + if (pEntry == null) { + throw new VersionException("Version with label '" + qLabel + "' does not exist."); + } + Node version = ((Property) getItemManager().getItem(pEntry)).getNode(); + return (Version) version; + } + + /** + * + * @param label + * @return + * @throws RepositoryException + */ + private Name getQLabel(String label) throws RepositoryException { + try { + return session.getNameResolver().getQName(label); + } catch (NameException e) { + String error = "Invalid version label: " + e.getMessage(); + log.error(error); + throw new RepositoryException(error, e); + } + } + + /** + * Checks if the specified version belongs to this VersionHistory. + * This method throws VersionException if {@link Version#getContainingHistory()} + * is not the same item than this VersionHistory. + * + * @param version + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.RepositoryException + */ + private void checkValidVersion(Version version) throws VersionException, RepositoryException { + if (!version.getContainingHistory().isSame(this)) { + throw new VersionException("Specified version '" + version.getName() + "' is not part of this history."); + } + } + + /** + * + * @param entry + * @throws RepositoryException + */ + private static void refreshEntry(NodeEntry entry) throws RepositoryException { + // TODO: check again.. is this correct? or should NodeEntry be altered + entry.getNodeState(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionImpl.java new file mode 100644 index 00000000000..a468841b479 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionImpl.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.jcr2spi.version; + +import java.util.Calendar; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; + +import org.apache.jackrabbit.jcr2spi.ItemLifeCycleListener; +import org.apache.jackrabbit.jcr2spi.NodeImpl; +import org.apache.jackrabbit.jcr2spi.SessionImpl; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * VersionImpl... + */ +public class VersionImpl extends NodeImpl implements Version { + + private static Logger log = LoggerFactory.getLogger(VersionImpl.class); + + public VersionImpl(SessionImpl session, NodeState state, + ItemLifeCycleListener[] listeners) { + super(session, state, listeners); + } + + //------------------------------------------------------------< Version >--- + /** + * @see Version#getContainingHistory() + */ + public VersionHistory getContainingHistory() throws RepositoryException { + return (VersionHistory) getParent(); + } + + /** + * @see Version#getCreated() + */ + public Calendar getCreated() throws RepositoryException { + return getProperty(NameConstants.JCR_CREATED).getDate(); + } + + /** + * @see Version#getSuccessors() + */ + public Version[] getSuccessors() throws RepositoryException { + return getVersions(NameConstants.JCR_SUCCESSORS); + } + + /** + * @see Version#getLinearSuccessor() + */ + public Version getLinearSuccessor() throws RepositoryException { + // TODO: improve. + VersionHistory vh = getContainingHistory(); + for (VersionIterator it = vh.getAllLinearVersions(); it.hasNext();) { + Version v = it.nextVersion(); + if (isSame(v.getLinearPredecessor())) { + return v; + } + } + + // no linear successor found + return null; + } + + /** + * @see Version#getPredecessors() + */ + public Version[] getPredecessors() throws RepositoryException { + return getVersions(NameConstants.JCR_PREDECESSORS); + } + + /** + * @see Version#getLinearPredecessor() + */ + public Version getLinearPredecessor() throws RepositoryException { + Value[] values = getProperty(NameConstants.JCR_PREDECESSORS).getValues(); + if (values != null && values.length > 0) { + Node n = session.getNodeByUUID(values[0].getString()); + if (n instanceof Version) { + return (Version) n; + } else { + throw new RepositoryException("Version property contains invalid value not pointing to a 'Version'"); + } + } else { + return null; + } + } + + /** + * @see Version#getFrozenNode() + */ + public Node getFrozenNode() throws RepositoryException { + return getNode(NameConstants.JCR_FROZENNODE, 1); + + } + + //---------------------------------------------------------------< Item >--- + /** + * + * @param otherItem + * @return + * @see Item#isSame(Item) + */ + @Override + public boolean isSame(Item otherItem) throws RepositoryException { + checkStatus(); + if (otherItem instanceof VersionImpl) { + // since all versions are referenceable, protected and live + // in the same workspace, a simple comparison of the UUIDs is sufficient + VersionImpl other = ((VersionImpl) otherItem); + try { + return getUUID().equals(other.getUUID()); + } catch (RepositoryException e) { + // should never occur + log.error("Internal error while retrieving UUID of version.", e); + } + } + return false; + } + + //-----------------------------------------------------------< ItemImpl >--- + /** + * Always throws ConstraintViolationException since the version storage is + * protected. + * + * @throws UnsupportedRepositoryOperationException + * @throws ConstraintViolationException + * @throws RepositoryException + */ + @Override + protected void checkIsWritable() throws UnsupportedRepositoryOperationException, ConstraintViolationException, RepositoryException { + super.checkIsWritable(); + throw new ConstraintViolationException("Version is protected"); + } + + /** + * Always returns false + * + * @throws RepositoryException + * @see NodeImpl#isWritable() + */ + @Override + protected boolean isWritable() throws RepositoryException { + super.isWritable(); + return false; + } + //------------------------------------------------------------< private >--- + /** + * + * @param propertyName + * @return + */ + private Version[] getVersions(Name propertyName) throws RepositoryException { + Version[] versions; + Value[] values = getProperty(propertyName).getValues(); + if (values != null) { + versions = new Version[values.length]; + for (int i = 0; i < values.length; i++) { + Node n = session.getNodeByUUID(values[i].getString()); + if (n instanceof Version) { + versions[i] = (Version) n; + } else { + throw new RepositoryException("Version property contains invalid value not pointing to a 'Version'"); + } + } + } else { + versions = new Version[0]; + } + return versions; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionManager.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionManager.java new file mode 100644 index 00000000000..ce92c0ab98b --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionManager.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.version; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.AccessDeniedException; +import javax.jcr.ItemExistsException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.MergeException; +import javax.jcr.lock.LockException; +import javax.jcr.version.VersionException; +import javax.jcr.version.Version; +import java.util.Iterator; + +/** + * VersionManager... + */ +public interface VersionManager { + + /** + * @param nodeState + * @return NodeEntry of newly created version + * @throws VersionException + * @throws UnsupportedRepositoryOperationException + * @throws InvalidItemStateException + * @throws LockException + * @throws RepositoryException + * @see javax.jcr.Node#checkin() + */ + public NodeEntry checkin(NodeState nodeState) throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException; + + /** + * @param nodeState + * @throws UnsupportedRepositoryOperationException + * @throws LockException + * @throws RepositoryException + * @see javax.jcr.Node#checkout() + */ + public void checkout(NodeState nodeState) throws UnsupportedRepositoryOperationException, LockException, RepositoryException; + + /** + * + * @param nodeState + * @param activityId + * @throws RepositoryException + */ + public void checkout(NodeState nodeState, NodeId activityId) throws RepositoryException; + + /** + * @param nodeState + * @throws RepositoryException + * @see javax.jcr.version.VersionManager#checkpoint(String) + */ + public NodeEntry checkpoint(NodeState nodeState) throws RepositoryException; + + /** + * @param nodeState + * @throws RepositoryException + * @see javax.jcr.version.VersionManager#checkpoint(String) + */ + public NodeEntry checkpoint(NodeState nodeState, NodeId activityId) throws RepositoryException; + /** + * @param nodeState + * @return + * @throws RepositoryException + * @see javax.jcr.Node#isCheckedOut() + */ + public boolean isCheckedOut(NodeState nodeState) throws RepositoryException; + + /** + * @param nodeState + * @throws VersionException If the Node represented by the given + * NodeState is checkedin. + * @throws RepositoryException If another error occurs. + * @see javax.jcr.Node#isCheckedOut() + */ + public void checkIsCheckedOut(NodeState nodeState) throws VersionException, RepositoryException; + + /** + * @param versionHistoryState + * @param versionState + * @throws ReferentialIntegrityException + * @throws AccessDeniedException + * @throws UnsupportedRepositoryOperationException + * @throws VersionException + * @throws RepositoryException + * @see javax.jcr.version.VersionHistory#removeVersion(String) + */ + public void removeVersion(NodeState versionHistoryState, NodeState versionState) throws ReferentialIntegrityException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException; + + /** + * @param versionHistoryState + * @param versionState + * @param qLabel + * @param moveLabel + * @throws VersionException + * @throws RepositoryException + * @see javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean) + */ + public void addVersionLabel(NodeState versionHistoryState, NodeState versionState, Name qLabel, boolean moveLabel) throws VersionException, RepositoryException; + + /** + * @param versionHistoryState + * @param versionState + * @param qLabel + * @throws VersionException + * @throws RepositoryException + * @see javax.jcr.version.VersionHistory#removeVersionLabel(String) + */ + public void removeVersionLabel(NodeState versionHistoryState, NodeState versionState, Name qLabel) throws VersionException, RepositoryException; + + /** + * @param nodeState + * @param relativePath + * @param versionState + * @param removeExisting + * @throws VersionException + * @throws ItemExistsException + * @throws UnsupportedRepositoryOperationException + * @throws LockException + * @throws InvalidItemStateException + * @throws RepositoryException + * @see javax.jcr.Node#restore(String, boolean) + * @see javax.jcr.Node#restore(Version, boolean) + * @see javax.jcr.Node#restore(Version, String, boolean) + * @see javax.jcr.Node#restoreByLabel(String, boolean) + */ + public void restore(NodeState nodeState, Path relativePath, NodeState versionState, boolean removeExisting) throws VersionException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException; + + /** + * @param versionStates + * @param removeExisting + * @throws ItemExistsException + * @throws UnsupportedRepositoryOperationException + * @throws VersionException + * @throws LockException + * @throws InvalidItemStateException + * @throws RepositoryException + * @see javax.jcr.Workspace#restore(Version[], boolean) + */ + public void restore(NodeState[] versionStates, boolean removeExisting) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException; + + /** + * @param nodeState + * @param workspaceName + * @param bestEffort + * @return An Iterator over NodeIds of all Nodes + * that failed to be merged and need manual resolution by the user of the API. + * @throws NoSuchWorkspaceException + * @throws AccessDeniedException + * @throws MergeException + * @throws LockException + * @throws InvalidItemStateException + * @throws RepositoryException + * @see #resolveMergeConflict(NodeState,NodeState,boolean) + * @see javax.jcr.Node#merge(String, boolean) + */ + public Iterator merge(NodeState nodeState, String workspaceName, boolean bestEffort) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException; + + /** + * @param nodeState + * @param workspaceName + * @param bestEffort + * @param isShallow + * @return An Iterator over NodeIds of all Nodes + * that failed to be merged and need manual resolution by the user of the API. + * @throws NoSuchWorkspaceException + * @throws AccessDeniedException + * @throws MergeException + * @throws LockException + * @throws InvalidItemStateException + * @throws RepositoryException + * @see #resolveMergeConflict(NodeState,NodeState,boolean) + * @see javax.jcr.Node#merge(String, boolean) + */ + public Iterator merge(NodeState nodeState, String workspaceName, boolean bestEffort, boolean isShallow) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException; + + + /** + * @param nodeState + * @param versionState + * @param done + * @throws VersionException + * @throws InvalidItemStateException + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + * @see javax.jcr.Node#cancelMerge(Version) + * @see javax.jcr.Node#doneMerge(Version) + */ + public void resolveMergeConflict(NodeState nodeState, NodeState versionState, boolean done) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * + * @param nodeState + * @return + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + */ + public NodeEntry createConfiguration(NodeState nodeState) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * + * @param title + * @return + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + */ + public NodeEntry createActivity(String title) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * + * @param activityState + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + */ + public void removeActivity(NodeState activityState) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * + * @param activityState + * @return + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + */ + public Iterator mergeActivity(NodeState activityState) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * + * @param versionState + * @return + */ + public NodeEntry getVersionableNodeEntry(NodeState versionState) throws RepositoryException; + + /** + * + * @param versionableState + * @return + */ + public NodeEntry getVersionHistoryEntry(NodeState versionableState) throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionManagerImpl.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionManagerImpl.java new file mode 100644 index 00000000000..dd12686b8c3 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/version/VersionManagerImpl.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.jcr2spi.version; + +import java.util.Iterator; + +import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.MergeException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.jcr2spi.WorkspaceManager; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.operation.AddLabel; +import org.apache.jackrabbit.jcr2spi.operation.Checkin; +import org.apache.jackrabbit.jcr2spi.operation.Checkout; +import org.apache.jackrabbit.jcr2spi.operation.Checkpoint; +import org.apache.jackrabbit.jcr2spi.operation.CreateActivity; +import org.apache.jackrabbit.jcr2spi.operation.CreateConfiguration; +import org.apache.jackrabbit.jcr2spi.operation.Merge; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.RemoveActivity; +import org.apache.jackrabbit.jcr2spi.operation.RemoveLabel; +import org.apache.jackrabbit.jcr2spi.operation.RemoveVersion; +import org.apache.jackrabbit.jcr2spi.operation.ResolveMergeConflict; +import org.apache.jackrabbit.jcr2spi.operation.Restore; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * VersionManagerImpl... + */ +public class VersionManagerImpl implements VersionManager { + + private final WorkspaceManager workspaceManager; + + public VersionManagerImpl(WorkspaceManager workspaceManager) { + this.workspaceManager = workspaceManager; + } + + public NodeEntry checkin(NodeState nodeState) throws RepositoryException { + Checkin ci = Checkin.create(nodeState, this); + workspaceManager.execute(ci); + return workspaceManager.getHierarchyManager().getNodeEntry(ci.getNewVersionId()); + } + + public void checkout(NodeState nodeState) throws RepositoryException { + Operation co = Checkout.create(nodeState, this); + workspaceManager.execute(co); + } + + public void checkout(NodeState nodeState, NodeId activityId) throws RepositoryException { + Operation co = Checkout.create(nodeState, activityId, this); + workspaceManager.execute(co); + } + + public NodeEntry checkpoint(NodeState nodeState) throws RepositoryException { + Checkpoint cp = Checkpoint.create(nodeState, this); + workspaceManager.execute(cp); + return workspaceManager.getHierarchyManager().getNodeEntry(cp.getNewVersionId()); + } + + public NodeEntry checkpoint(NodeState nodeState, NodeId activityId) throws RepositoryException { + Checkpoint cp = Checkpoint.create(nodeState, activityId, this); + workspaceManager.execute(cp); + return workspaceManager.getHierarchyManager().getNodeEntry(cp.getNewVersionId()); + } + + /** + * Search nearest ancestor that is versionable. If no versionable ancestor + * can be found, true is returned. + * + * @param nodeState + * @return + * @throws RepositoryException + */ + public boolean isCheckedOut(NodeState nodeState) throws RepositoryException { + // shortcut: if state is new, its ancestor must be checkout + if (nodeState.getStatus() == Status.NEW) { + return true; + } + + NodeEntry nodeEntry = nodeState.getNodeEntry(); + try { + // NOTE: since the hierarchy might not be completely loaded or some + // entry might even not be accessible, the check may not detect + // a checked-in parent. ok, as long as the 'server' finds out upon + // save or upon executing the workspace operation. + while (!nodeEntry.hasPropertyEntry(NameConstants.JCR_ISCHECKEDOUT)) { + NodeEntry parent = nodeEntry.getParent(); + if (parent == null) { + // reached root state without finding a jcr:isCheckedOut property + return true; + } + nodeEntry = parent; + } + PropertyState propState = nodeEntry.getPropertyEntry(NameConstants.JCR_ISCHECKEDOUT).getPropertyState(); + Boolean b = Boolean.valueOf(propState.getValue().getString()); + return b.booleanValue(); + } catch (ItemNotFoundException e) { + // error while accessing jcr:isCheckedOut property state. + // -> assume that checkedOut status is ok. see above for general + // notes about the capabilities of the jcr2spi implementation. + } + return true; + } + + public void checkIsCheckedOut(NodeState nodeState) throws VersionException, RepositoryException { + if (!isCheckedOut(nodeState)) { + throw new VersionException(nodeState + " is checked-in"); + } + } + + public void removeVersion(NodeState versionHistoryState, NodeState versionState) throws RepositoryException { + Operation op = RemoveVersion.create(versionState, versionHistoryState, this); + workspaceManager.execute(op); + } + + public void addVersionLabel(NodeState versionHistoryState, NodeState versionState, Name qLabel, boolean moveLabel) throws RepositoryException { + Operation op = AddLabel.create(versionHistoryState, versionState, qLabel, moveLabel); + workspaceManager.execute(op); + } + + public void removeVersionLabel(NodeState versionHistoryState, NodeState versionState, Name qLabel) throws RepositoryException { + Operation op = RemoveLabel.create(versionHistoryState, versionState, qLabel); + workspaceManager.execute(op); + } + + public void restore(NodeState nodeState, Path relativePath, NodeState versionState, boolean removeExisting) throws RepositoryException { + Operation op = Restore.create(nodeState, relativePath, versionState, removeExisting); + workspaceManager.execute(op); + } + + public void restore(NodeState[] versionStates, boolean removeExisting) throws RepositoryException { + Operation op = Restore.create(versionStates, removeExisting); + workspaceManager.execute(op); + } + + public Iterator merge(NodeState nodeState, String workspaceName, boolean bestEffort) throws RepositoryException { + return merge(nodeState, workspaceName, bestEffort, false); + } + + public Iterator merge(NodeState nodeState, String workspaceName, boolean bestEffort, boolean isShallow) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + Merge op = Merge.create(nodeState, workspaceName, bestEffort, isShallow, this); + workspaceManager.execute(op); + return op.getFailedIds(); + } + + public void resolveMergeConflict(NodeState nodeState, NodeState versionState, + boolean done) throws RepositoryException { + NodeId vId = versionState.getNodeId(); + + PropertyState mergeFailedState = nodeState.getPropertyState(NameConstants.JCR_MERGEFAILED); + QValue[] vs = mergeFailedState.getValues(); + + NodeId[] mergeFailedIds = new NodeId[vs.length - 1]; + for (int i = 0, j = 0; i < vs.length; i++) { + NodeId id = workspaceManager.getIdFactory().createNodeId(vs[i].getString()); + if (!id.equals(vId)) { + mergeFailedIds[j] = id; + j++; + } + // else: the version id is being solved by this call and not + // part of 'jcr:mergefailed' any more + } + + PropertyState predecessorState = nodeState.getPropertyState(NameConstants.JCR_PREDECESSORS); + vs = predecessorState.getValues(); + + int noOfPredecessors = (done) ? vs.length + 1 : vs.length; + NodeId[] predecessorIds = new NodeId[noOfPredecessors]; + + int i = 0; + while (i < vs.length) { + predecessorIds[i] = workspaceManager.getIdFactory().createNodeId(vs[i].getString()); + i++; + } + if (done) { + predecessorIds[i] = vId; + } + Operation op = ResolveMergeConflict.create(nodeState, mergeFailedIds, predecessorIds, done); + workspaceManager.execute(op); + } + + public NodeEntry createConfiguration(NodeState nodeState) throws UnsupportedRepositoryOperationException, RepositoryException { + CreateConfiguration op = CreateConfiguration.create(nodeState, this); + workspaceManager.execute(op); + return workspaceManager.getHierarchyManager().getNodeEntry(op.getNewConfigurationId()); + } + + public NodeEntry createActivity(String title) throws UnsupportedRepositoryOperationException, RepositoryException { + CreateActivity op = CreateActivity.create(title, this); + workspaceManager.execute(op); + return workspaceManager.getHierarchyManager().getNodeEntry(op.getNewActivityId()); + } + + public void removeActivity(NodeState activityState) throws UnsupportedRepositoryOperationException, RepositoryException { + Operation op = RemoveActivity.create(activityState, workspaceManager.getHierarchyManager()); + workspaceManager.execute(op); + } + + public Iterator mergeActivity(NodeState activityState) throws UnsupportedRepositoryOperationException, RepositoryException { + Merge op = Merge.create(activityState, null, false, false, this); + workspaceManager.execute(op); + return op.getFailedIds(); + } + + public NodeEntry getVersionableNodeEntry(NodeState versionState) throws RepositoryException { + NodeState ns = versionState.getChildNodeState(NameConstants.JCR_FROZENNODE, Path.INDEX_DEFAULT); + PropertyState ps = ns.getPropertyState(NameConstants.JCR_FROZENUUID); + String uniqueID = ps.getValue().getString(); + + NodeId versionableId = workspaceManager.getIdFactory().createNodeId(uniqueID); + return workspaceManager.getHierarchyManager().getNodeEntry(versionableId); + } + + public NodeEntry getVersionHistoryEntry(NodeState versionableState) throws RepositoryException { + PropertyState ps = versionableState.getPropertyState(NameConstants.JCR_VERSIONHISTORY); + String uniqueID = ps.getValue().getString(); + NodeId vhId = workspaceManager.getIdFactory().createNodeId(uniqueID); + return workspaceManager.getHierarchyManager().getNodeEntry(vhId); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/DocViewImportHandler.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/DocViewImportHandler.java new file mode 100644 index 00000000000..62cc23d9e28 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/DocViewImportHandler.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.jcr2spi.xml; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.util.ISO9075; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +/** + * DocViewImportHandler processes Document View XML SAX events + * and 'translates' them into {@link Importer} method calls. + */ +class DocViewImportHandler extends TargetImportHandler { + + private static Logger log = LoggerFactory.getLogger(DocViewImportHandler.class); + + private final NameFactory nameFactory; + /** + * stack of NodeInfo instances; an instance is pushed onto the stack + * in the startElement method and is popped from the stack in the + * endElement method. + */ + private final Stack stack = new Stack(); + // buffer used to merge adjacent character data + private BufferedStringValue textHandler = new BufferedStringValue(); + + /** + * Constructs a new DocViewImportHandler. + * + * @param importer + * @param resolver + * @param nameFactory + */ + DocViewImportHandler(Importer importer, NamePathResolver resolver, + NameFactory nameFactory) { + super(importer, resolver); + this.nameFactory = nameFactory; + } + + /** + * Appends the given character data to the internal buffer. + * + * @param ch the characters to be appended + * @param start the index of the first character to append + * @param length the number of characters to append + * @throws SAXException if an error occurs + * @see #characters(char[], int, int) + * @see #ignorableWhitespace(char[], int, int) + * @see #processCharacters() + */ + private void appendCharacters(char[] ch, int start, int length) + throws SAXException { + if (textHandler == null) { + textHandler = new BufferedStringValue(); + } + try { + textHandler.append(ch, start, length); + } catch (IOException ioe) { + String msg = "internal error while processing internal buffer data"; + log.error(msg, ioe); + throw new SAXException(msg, ioe); + } + } + + /** + * Translates character data reported by the + * {@link #characters(char[], int, int)} & + * {@link #ignorableWhitespace(char[], int, int)} SAX events + * into a jcr:xmltext child node with one + * jcr:xmlcharacters property. + * + * @throws SAXException if an error occurs + * @see #appendCharacters(char[], int, int) + */ + private void processCharacters() + throws SAXException { + try { + if (textHandler != null && textHandler.length() > 0) { + // there is character data that needs to be added to + // the current node + + // check for pure whitespace character data + Reader reader = textHandler.reader(); + try { + int ch; + while ((ch = reader.read()) != -1) { + if (ch > 0x20) { + break; + } + } + if (ch == -1) { + // the character data consists of pure whitespace, ignore + log.debug("ignoring pure whitespace character data..."); + // reset handler + textHandler.dispose(); + textHandler = null; + return; + } + } finally { + reader.close(); + } + + Importer.NodeInfo node = + new Importer.NodeInfo(NameConstants.JCR_XMLTEXT, null, null, null); + Importer.TextValue[] values = + new Importer.TextValue[]{textHandler}; + List props = new ArrayList(); + Importer.PropInfo prop = + new Importer.PropInfo(NameConstants.JCR_XMLCHARACTERS, PropertyType.STRING, values); + props.add(prop); + // call Importer + importer.startNode(node, props, resolver); + importer.endNode(node); + + // reset handler + textHandler.dispose(); + textHandler = null; + } + } catch (IOException ioe) { + String msg = "internal error while processing internal buffer data"; + log.error(msg, ioe); + throw new SAXException(msg, ioe); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + //-------------------------------------------------------< ContentHandler > + + /** + * {@inheritDoc} + *

        + * See also {@link org.apache.jackrabbit.commons.xml.DocumentViewExporter#exportProperty(String, String, int, javax.jcr.Value[])} + * regarding special handling of multi-valued properties on export. + */ + @Override + public void startElement(String namespaceURI, String localName, + String qName, Attributes atts) + throws SAXException { + // process buffered character data + processCharacters(); + + try { + String dcdLocalName = ISO9075.decode(localName); + Name nodeName = nameFactory.create(namespaceURI, dcdLocalName); + + // properties + String uuid = null; + Name nodeTypeName = null; + Name[] mixinTypes = null; + + List props = new ArrayList(atts.getLength()); + for (int i = 0; i < atts.getLength(); i++) { + if (atts.getURI(i).equals(Name.NS_XMLNS_URI)) { + // skip namespace declarations reported as attributes + // see http://issues.apache.org/jira/browse/JCR-620#action_12448164 + continue; + } + + dcdLocalName = ISO9075.decode(atts.getLocalName(i)); + Name propName = nameFactory.create(atts.getURI(i), dcdLocalName); + + // attribute value + String attrValue = atts.getValue(i); + if (propName.equals(NameConstants.JCR_PRIMARYTYPE)) { + // jcr:primaryType + if (attrValue.length() > 0) { + try { + nodeTypeName = resolver.getQName(attrValue); + } catch (NameException ne) { + throw new SAXException("illegal jcr:primaryType value: " + + attrValue, ne); + } + } + } else if (propName.equals(NameConstants.JCR_MIXINTYPES)) { + // jcr:mixinTypes + mixinTypes = parseNames(attrValue); + } else if (propName.equals(NameConstants.JCR_UUID)) { + // jcr:uuid + if (attrValue.length() > 0) { + uuid = attrValue; + } + } else { + // always assume single-valued property for the time being + // until a way of properly serializing/detecting multi-valued + // properties on re-import is found (see JCR-325); + // see also DocViewSAXEventGenerator#leavingProperties(Node, int) + // TODO: proper multi-value serialization support + Importer.TextValue[] propValues = new Importer.TextValue[1]; + propValues[0] = new StringValue(attrValue); + props.add(new Importer.PropInfo(propName, PropertyType.UNDEFINED, propValues)); + } + } + + Importer.NodeInfo node = new Importer.NodeInfo(nodeName, nodeTypeName, mixinTypes, uuid); + // all information has been collected, now delegate to importer + importer.startNode(node, props, resolver); + // push current node data onto stack + stack.push(node); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + /** + * Parses the given string as a list of JCR names. Any whitespace sequence + * is supported as a names separator instead of just a single space to + * be more liberal in what we accept. The current namespace context is + * used to convert the prefixed name strings to Names. + * + * @param value string value + * @return the parsed names + * @throws SAXException if an invalid name was encountered + */ + private Name[] parseNames(String value) throws SAXException { + String[] names = value.split("\\p{Space}+"); + Name[] qnames = new Name[names.length]; + for (int i = 0; i < names.length; i++) { + try { + qnames[i] = resolver.getQName(names[i]); + } catch (NameException ne) { + throw new SAXException("Invalid name: " + names[i], ne); + } catch (NamespaceException e) { + throw new SAXException("Invalid name: " + names[i], e); + } + } + return qnames; + } + + /** + * {@inheritDoc} + */ + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + /** + * buffer data reported by the characters event; + * will be processed on the next endElement or startElement event. + */ + appendCharacters(ch, start, length); + } + + /** + * {@inheritDoc} + */ + @Override + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + /** + * buffer data reported by the ignorableWhitespace event; + * will be processed on the next endElement or startElement event. + */ + appendCharacters(ch, start, length); + } + + /** + * {@inheritDoc} + */ + @Override + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + // process buffered character data + processCharacters(); + + Importer.NodeInfo node = stack.peek(); + try { + // call Importer + importer.endNode(node); + } catch (RepositoryException re) { + throw new SAXException(re); + } + // we're done with this node, pop it from stack + stack.pop(); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/ImportHandler.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/ImportHandler.java new file mode 100644 index 00000000000..e82ab04529d --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/ImportHandler.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.xml; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingNameResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingPathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; +import org.xml.sax.helpers.NamespaceSupport; + +/** + * An ImportHandler instance can be used to import serialized + * data in System View XML or Document View XML. Processing of the XML is + * handled by specialized ContentHandlers + * (i.e. SysViewImportHandler and DocViewImportHandler). + *

        + * The actual task of importing though is delegated to the implementation of + * the {@link Importer} interface. + *

        + * Important Note: + *

        + * These SAX Event Handlers expect that Namespace URI's and local names are + * reported in the start/endElement events and that + * start/endPrefixMapping events are reported + * (i.e. default SAX2 Namespace processing). + */ +public class ImportHandler extends DefaultHandler { + + private static Logger log = LoggerFactory.getLogger(ImportHandler.class); + + private final Importer importer; + private final NamespaceRegistry nsReg; + private final NamespaceResolver nsResolver; + private final NameFactory nameFactory; + + private ContentHandler targetHandler; + private boolean systemViewXML; + private boolean initialized; + + private final NamespaceContext nsContext; + private final NamePathResolver resolver; + + /** + * this flag is used to determine whether a namespace context needs to be + * started in the startElement event or if the namespace context has already + * been started in a preceding startPrefixMapping event; + * the flag is set per element in the first startPrefixMapping event and is + * cleared again in the following startElement event; + */ + protected boolean nsContextStarted; + + public ImportHandler(Importer importer, NamespaceResolver nsResolver, + NamespaceRegistry nsReg, NameFactory nameFactory, + PathFactory pathFactory) { + this.importer = importer; + this.nsResolver = nsResolver; + this.nsReg = nsReg; + this.nameFactory = nameFactory; + + nsContext = new NamespaceContext(); + NameResolver nr = new ParsingNameResolver(nameFactory, nsContext); + resolver = new DefaultNamePathResolver(nr, new ParsingPathResolver(pathFactory, nr)); + } + + //---------------------------------------------------------< ErrorHandler > + /** + * {@inheritDoc} + */ + @Override + public void warning(SAXParseException e) throws SAXException { + // log exception and carry on... + log.warn("warning encountered at line: " + e.getLineNumber() + + ", column: " + e.getColumnNumber() + + " while parsing XML stream", e); + } + + /** + * {@inheritDoc} + */ + @Override + public void error(SAXParseException e) throws SAXException { + // log exception and carry on... + log.error("error encountered at line: " + e.getLineNumber() + + ", column: " + e.getColumnNumber() + + " while parsing XML stream: " + e.toString()); + } + + /** + * {@inheritDoc} + */ + @Override + public void fatalError(SAXParseException e) throws SAXException { + // log and re-throw exception + log.error("fatal error encountered at line: " + e.getLineNumber() + + ", column: " + e.getColumnNumber() + + " while parsing XML stream: " + e.toString()); + throw e; + } + + //-------------------------------------------------------< ContentHandler > + /** + * {@inheritDoc} + */ + @Override + public void startDocument() throws SAXException { + systemViewXML = false; + initialized = false; + targetHandler = null; + + /** + * start initial context containing existing mappings reflected + * by nsResolver + */ + nsContext.reset(); + nsContext.pushContext(); + try { + String[] uris = nsReg.getURIs(); + for (int i = 0; i < uris.length; i++) { + nsContext.declarePrefix(nsResolver.getPrefix(uris[i]), uris[i]); + } + } catch (RepositoryException re) { + throw new SAXException(re); + } + + // initialize flag + nsContextStarted = false; + } + + /** + * {@inheritDoc} + */ + @Override + public void endDocument() throws SAXException { + // delegate to target handler + targetHandler.endDocument(); + // cleanup + nsContext.reset(); + } + + /** + * {@inheritDoc} + */ + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + // check if new context needs to be started + if (!nsContextStarted) { + // entering new namespace context + nsContext.pushContext(); + nsContextStarted = true; + } + + try { + // this will trigger NamespaceException if namespace is unknown + nsContext.getPrefix(uri); + } catch (NamespaceException nse) { + // namespace is not yet registered ... + try { + String newPrefix; + if ("".equals(prefix)) { + /** + * the xml document specifies a default namespace + * (i.e. an empty prefix); we need to create a random + * prefix as the empty prefix is reserved according + * to the JCR spec. + */ + newPrefix = getUniquePrefix(uri); + } else { + newPrefix = prefix; + } + // register new namespace + nsReg.registerNamespace(newPrefix, uri); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + // map namespace in this context to given prefix + nsContext.declarePrefix(prefix, uri); + } + + /** + * {@inheritDoc} + */ + @Override + public void endPrefixMapping(String prefix) throws SAXException { + /** + * nothing to do here as namespace context has already been popped + * in endElement event + */ + } + + /** + * {@inheritDoc} + */ + @Override + public void startElement(String namespaceURI, String localName, String qName, + Attributes atts) throws SAXException { + // check if new context needs to be started + if (!nsContextStarted) { + // there hasn't been a proceeding startPrefixMapping event + // so enter new namespace context + nsContext.pushContext(); + } else { + // reset flag + nsContextStarted = false; + } + + if (!initialized) { + // the namespace of the first element determines the type of XML + // (system view/document view) + systemViewXML = Name.NS_SV_URI.equals(namespaceURI); + + if (systemViewXML) { + targetHandler = new SysViewImportHandler(importer, resolver); + } else { + targetHandler = new DocViewImportHandler(importer, resolver, nameFactory); + } + targetHandler.startDocument(); + initialized = true; + } + + // delegate to target handler + targetHandler.startElement(namespaceURI, localName, qName, atts); + } + + /** + * {@inheritDoc} + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + // delegate to target handler + targetHandler.characters(ch, start, length); + } + + /** + * {@inheritDoc} + */ + @Override + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + // leaving element, pop namespace context + nsContext.popContext(); + + // delegate to target handler + targetHandler.endElement(namespaceURI, localName, qName); + } + + //--------------------------------------------------------< inner classes > + /** + * NamespaceContext supports scoped namespace declarations. + */ + class NamespaceContext implements NamespaceResolver { + + private final NamespaceSupport nsContext; + + /** + * NamespaceSupport doesn't accept "" as default uri; + * internally we're using " " instead + */ + private static final String DUMMY_DEFAULT_URI = " "; + + NamespaceContext() { + nsContext = new NamespaceSupport(); + } + + void popContext() { + nsContext.popContext(); + } + + void pushContext() { + nsContext.pushContext(); + } + + void reset() { + nsContext.reset(); + } + + boolean declarePrefix(String prefix, String uri) { + if (Name.NS_DEFAULT_URI.equals(uri)) { + uri = DUMMY_DEFAULT_URI; + } + return nsContext.declarePrefix(prefix, uri); + } + + //------------------------------------------------< NamespaceResolver > + /** + * {@inheritDoc} + */ + public String getURI(String prefix) throws NamespaceException { + String uri = nsContext.getURI(prefix); + if (uri == null) { + throw new NamespaceException("unknown prefix"); + } else if (DUMMY_DEFAULT_URI.equals(uri)) { + return Name.NS_DEFAULT_URI; + } else { + return uri; + } + } + + /** + * {@inheritDoc} + */ + public String getPrefix(String uri) throws NamespaceException { + if (Name.NS_DEFAULT_URI.equals(uri)) { + uri = DUMMY_DEFAULT_URI; + } + String prefix = nsContext.getPrefix(uri); + if (prefix == null) { + /** + * NamespaceSupport#getPrefix will never return the empty + * (default) prefix; we have to do a reverse-lookup to check + * whether it's the current default namespace + */ + if (uri.equals(nsContext.getURI(Name.NS_EMPTY_PREFIX))) { + return Name.NS_EMPTY_PREFIX; + } + throw new NamespaceException("unknown uri"); + } + return prefix; + } + } + + /** + * Returns a prefix that is unique among the already registered prefixes. + * + * @param uriHint namespace uri that serves as hint for the prefix generation + * @return a unique prefix + */ + public String getUniquePrefix(String uriHint) throws RepositoryException { + // TODO: smarter unique prefix generation + return "_pre" + (nsReg.getPrefixes().length + 1); + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/Importer.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/Importer.java new file mode 100644 index 00000000000..fe4f5620f44 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/Importer.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.jcr2spi.xml; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.io.Reader; +import java.util.List; + +/** + * The Importer interface ... + */ +public interface Importer { + + /** + * @throws RepositoryException + */ + void start() throws RepositoryException; + + /** + * @param nodeInfo + * @param propInfos list of PropInfo instances + * @param resolver NamePathResolver dealing with prefix mappings of current + * context. + * @throws RepositoryException + */ + void startNode(NodeInfo nodeInfo, List propInfos, NamePathResolver resolver) + throws RepositoryException; + + /** + * @param nodeInfo + * @throws RepositoryException + */ + void endNode(NodeInfo nodeInfo) throws RepositoryException; + + /** + * @throws RepositoryException + */ + void end() throws RepositoryException; + + //--------------------------------------------------------< inner classes > + static class NodeInfo { + private final Name name; + private final Name nodeTypeName; + private final Name[] mixinNames; + private String uuid; + + public NodeInfo(Name name, Name nodeTypeName, Name[] mixinNames, String uuid) { + this.name = name; + this.nodeTypeName = nodeTypeName; + this.mixinNames = mixinNames; + this.uuid = uuid; + } + + public Name getName() { + return name; + } + + public Name getNodeTypeName() { + return nodeTypeName; + } + + public Name[] getMixinNames() { + return mixinNames; + } + + public void setUUID(String uuid) { + this.uuid = uuid; + } + + public String getUUID() { + return uuid; + } + } + + static class PropInfo { + private final Name name; + private final int type; + private final TextValue[] values; + + public PropInfo(Name name, int type, TextValue[] values) { + this.name = name; + this.type = type; + this.values = values; + } + + public Name getName() { + return name; + } + + public int getType() { + return type; + } + + public TextValue[] getValues() { + return values; + } + } + + /** + * TextValue represents a serialized property value read + * from a System or Document View XML document. + */ + interface TextValue { + /** + * Returns the length of the serialized value. + * + * @return the length of the serialized value + * @throws IOException if an I/O error occurs + */ + long length() throws IOException; + + /** + * Retrieves the serialized value. + * + * @return the serialized value + * @throws IOException if an I/O error occurs + */ + String retrieve() throws IOException; + + /** + * Returns a Reader for reading the serialized value. + * + * @return a Reader for reading the serialized value. + * @throws IOException if an I/O error occurs + */ + Reader reader() throws IOException; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/SessionImporter.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/SessionImporter.java new file mode 100644 index 00000000000..728dc67a836 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/SessionImporter.java @@ -0,0 +1,648 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.xml; + +import org.apache.jackrabbit.jcr2spi.SessionImpl; +import org.apache.jackrabbit.jcr2spi.SessionListener; +import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry; +import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType; +import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeTypeProvider; +import org.apache.jackrabbit.jcr2spi.operation.AddNode; +import org.apache.jackrabbit.jcr2spi.operation.AddProperty; +import org.apache.jackrabbit.jcr2spi.operation.Operation; +import org.apache.jackrabbit.jcr2spi.operation.Remove; +import org.apache.jackrabbit.jcr2spi.operation.SetMixin; +import org.apache.jackrabbit.jcr2spi.operation.SetPropertyValue; +import org.apache.jackrabbit.jcr2spi.state.ItemStateValidator; +import org.apache.jackrabbit.jcr2spi.state.NodeState; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.jcr2spi.state.SessionItemStateManager; +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.jcr2spi.util.LogUtil; +import org.apache.jackrabbit.jcr2spi.util.ReferenceChangeTracker; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.util.Base64; +import org.apache.jackrabbit.util.TransientFileFactory; +import org.apache.jackrabbit.value.ValueHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Stack; +import java.util.UUID; + +/** + * SessionImporter... + */ +public class SessionImporter implements Importer, SessionListener { + + private static Logger log = LoggerFactory.getLogger(SessionImporter.class); + + private final NodeState importTarget; + private final int uuidBehavior; + + private final SessionImpl session; + private final SessionItemStateManager stateMgr; + + private final Stack parents; + + private boolean importerClosed; + private boolean sessionClosed; + + /** + * helper object that keeps track of remapped uuid's and imported reference + * properties that might need correcting depending on the uuid mappings + */ + private final ReferenceChangeTracker refTracker; + + /** + * Creates a new WorkspaceImporter instance. + * + * @param parentPath Path of target node where to add the imported + * subtree. + * @param session + * @param uuidBehavior Flag that governs how incoming UUIDs are handled. + * @throws PathNotFoundException If no node exists at parentPath + * or if the current session is not granted read access. + * @throws ConstraintViolationException If the node at parentPath + * is protected. + * @throws VersionException If the node at parentPath is not + * checked-out. + * @throws LockException If a lock prevents the addition of the subtree. + * @throws RepositoryException If another error occurs. + */ + public SessionImporter(Path parentPath, SessionImpl session, + SessionItemStateManager stateManager, int uuidBehavior) + throws PathNotFoundException, ConstraintViolationException, + VersionException, LockException, RepositoryException { + + this.session = session; + this.stateMgr = stateManager; + this.uuidBehavior = uuidBehavior; + + // perform preliminary checks + try { + importTarget = session.getHierarchyManager().getNodeState(parentPath); + + // check if import target is writable, not-locked and checked-out. + int options = ItemStateValidator.CHECK_ACCESS | ItemStateValidator.CHECK_LOCK | ItemStateValidator.CHECK_VERSIONING; + session.getValidator().checkIsWritable(importTarget, options); + + refTracker = new ReferenceChangeTracker(); + parents = new Stack(); + parents.push(importTarget); + } catch (ItemNotFoundException e) { + throw new PathNotFoundException(LogUtil.safeGetJCRPath(parentPath, session.getPathResolver())); + } + } + + //-----------------------------------------------------------< Importer >--- + /** + * {@inheritDoc} + */ + public void start() throws RepositoryException { + // explicitly set status of importer and start listening on session + setClosed(false); + } + + /** + * {@inheritDoc} + */ + public void startNode(NodeInfo nodeInfo, List propInfos, NamePathResolver resolver) + throws RepositoryException { + if (isClosed()) { + // workspace-importer only: ignore if import has been aborted before. + return; + } + checkSession(); + NodeState parent = parents.peek(); + if (parent == null) { + // parent node was skipped, skip this child node also + parents.push(null); // push null onto stack for skipped node + log.debug("Skipping node '" + nodeInfo.getName() + "'."); + return; + } + + NodeEntry parentEntry = (NodeEntry) parent.getHierarchyEntry(); + NodeState nodeState = null; + + if (parentEntry.hasNodeEntry(nodeInfo.getName())) { + try { + // a valid child node with that name already exists + NodeEntry entry = parentEntry.getNodeEntry(nodeInfo.getName(), Path.INDEX_DEFAULT); + NodeState existing = entry.getNodeState(); + + QNodeDefinition def = existing.getDefinition(); + if (!def.allowsSameNameSiblings()) { + // existing doesn't allow same-name siblings, check for conflicts + EffectiveNodeTypeProvider provider = session.getEffectiveNodeTypeProvider(); + Name[] ntNames = existing.getAllNodeTypeNames(); + EffectiveNodeType entExisting = provider.getEffectiveNodeType(ntNames); + if (def.isProtected() && entExisting.includesNodeType(nodeInfo.getNodeTypeName())) { + // skip protected node + parents.push(null); // push null onto stack for skipped node + log.debug("skipping protected node " + LogUtil.safeGetJCRPath(existing, session.getPathResolver())); + return; + } + if (def.isAutoCreated() && entExisting.includesNodeType(nodeInfo.getNodeTypeName())) { + // this node has already been auto-created, no need to create it + nodeState = existing; + } else { + throw new ItemExistsException(LogUtil.safeGetJCRPath(existing, session.getPathResolver())); + } + } + } catch (ItemNotFoundException e) { + // 'existing' doesn't exist any more -> ignore + } + } + + if (nodeState == null) { + // node does not exist -> create new one + if (nodeInfo.getUUID() == null) { + // no potential uuid conflict, add new node from given info + nodeState = importNode(nodeInfo, parent); + } else { + // make sure the import does not define a uuid without having + // a primaryType or mixin that makes the new node referenceable + checkIncludesMixReferenceable(nodeInfo); + + // potential uuid conflict + try { + NodeId conflictingId = session.getIdFactory().createNodeId(nodeInfo.getUUID()); + NodeEntry conflicting = session.getHierarchyManager().getNodeEntry(conflictingId); + // assert that the entry is available + conflicting.getItemState(); + + nodeState = resolveUUIDConflict(parent, conflicting, nodeInfo); + } catch (ItemNotFoundException e) { + // no conflict: create new with given uuid + nodeState = importNode(nodeInfo, parent); + } + } + } + + // node state may be 'null' if applicable def is protected + if (nodeState != null) { + // process properties + for (PropInfo pi : propInfos) { + importProperty(pi, nodeState, resolver); + } + } + + // push current nodeState onto stack of parents + parents.push(nodeState); + } + + /** + * {@inheritDoc} + */ + public void endNode(NodeInfo nodeInfo) throws RepositoryException { + if(isClosed()) { + // workspace-importer only: ignore if import has been aborted before. + return; + } + parents.pop(); + } + + /** + * {@inheritDoc} + */ + public void end() throws RepositoryException { + if(isClosed()) { + // workspace-importer only: ignore if import has been aborted before. + return; + } + + try { + checkSession(); + // adjust references referring to remapped uuids + stateMgr.adjustReferences(refTracker); + } finally { + // close this importer since we are done. + setClosed(true); + } + } + //----------------------------------------------------< SessionListener >--- + /** + * + * @param session + * @see SessionListener#loggingOut(Session) + */ + public void loggingOut(Session session) { + // the session will be be valid any more, thus any further calls on + // the importer must fail + sessionClosed = true; + } + + /** + * + * @param session + * @see SessionListener#loggedOut(Session) + */ + public void loggedOut(Session session) { + // ignore + } + + //--------------------------------------------< Importer/Session Status >--- + private void setClosed(boolean isClosed) { + importerClosed = isClosed; + if (isClosed) { + session.removeListener(this); + } else { + session.addListener(this); + } + } + + private boolean isClosed() { + return importerClosed; + } + + private void checkSession() throws RepositoryException { + if (sessionClosed) { + throw new RepositoryException("This session has been closed."); + } + } + + //----------------------------------------------------< Private methods >--- + /** + * @param parent + * @param conflicting + * @param nodeInfo + * @return + * @throws RepositoryException + */ + NodeState resolveUUIDConflict(NodeState parent, NodeEntry conflicting, + NodeInfo nodeInfo) throws ItemExistsException, RepositoryException { + NodeState nodeState; + switch (uuidBehavior) { + case ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW: + String originalUUID = nodeInfo.getUUID(); + String newUUID = UUID.randomUUID().toString(); + // reset id on nodeInfo to force creation with new uuid: + nodeInfo.setUUID(newUUID); + nodeState = importNode(nodeInfo, parent); + if (nodeState != null) { + // remember uuid mapping + refTracker.mappedUUIDs(originalUUID, newUUID); + } + break; + + case ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW: + String msg = "a node with uuid " + nodeInfo.getUUID() + " already exists!"; + log.debug(msg); + throw new ItemExistsException(msg); + + case ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING: + // make sure conflicting node is not importTarget or an ancestor thereof + Path p0 = importTarget.getPath(); + Path p1 = conflicting.getPath(); + if (p1.equals(p0) || p1.isAncestorOf(p0)) { + msg = "cannot remove ancestor node"; + log.debug(msg); + throw new ConstraintViolationException(msg); + } + // do remove conflicting (recursive) including validation check + try { + Operation op = Remove.create(conflicting.getNodeState()); + stateMgr.execute(op); + } catch (ItemNotFoundException e) { + // conflicting does not exist any more. no need for a removal + } + // create new with given uuid: + nodeState = importNode(nodeInfo, parent); + break; + + case ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING: + if (conflicting.getNodeState().isRoot()) { + msg = "Root node cannot be replaced"; + log.debug(msg); + throw new RepositoryException(msg); + } + + // 'replace' current parent with parent of conflicting + parent = conflicting.getParent().getNodeState(); + + // do remove conflicting (recursive), including validation checks + Operation op = Remove.create(conflicting.getNodeState()); + stateMgr.execute(op); + // create new with given uuid at same location as conflicting + nodeState = importNode(nodeInfo, parent); + break; + + default: + msg = "Unknown uuidBehavior: " + uuidBehavior; + log.debug(msg); + throw new RepositoryException(msg); + } + return nodeState; + } + + /** + * + * @param nodeInfo + * @param parent + * @return + * @throws ConstraintViolationException + * @throws ItemNotFoundException + * @throws RepositoryException + */ + private NodeState importNode(NodeInfo nodeInfo, NodeState parent) throws ConstraintViolationException, ItemNotFoundException, RepositoryException { + Name[] parentNtNames = parent.getAllNodeTypeNames(); + if (parent.hasPropertyName(nodeInfo.getName())) { + /** + * a property with the same name already exists; if this property + * has been imported as well (e.g. through document view import + * where an element can have the same name as one of the attributes + * of its parent element) we have to rename the conflicting property; + * + * see http://issues.apache.org/jira/browse/JCR-61 + */ + PropertyState conflicting = parent.getPropertyState(nodeInfo.getName()); + if (conflicting.getStatus() == Status.NEW) { + // assume this property has been imported as well; + // rename conflicting property + // TODO: use better reversible escaping scheme to create unique name + Name newName = session.getNameFactory().create(nodeInfo.getName().getNamespaceURI(), nodeInfo.getName().getLocalName() + "_"); + if (parent.hasPropertyName(newName)) { + newName = session.getNameFactory().create(newName.getNamespaceURI(), newName.getLocalName() + "_"); + } + // since name changes, need to find new applicable definition + QPropertyDefinition propDef; + if (conflicting.getValues().length == 1) { + // could be single- or multi-valued (n == 1) + try { + // try single-valued + propDef = session.getItemDefinitionProvider().getQPropertyDefinition(parentNtNames, newName, conflicting.getType(), false); + } catch (ConstraintViolationException cve) { + // try multi-valued + propDef = session.getItemDefinitionProvider().getQPropertyDefinition(parentNtNames, newName, conflicting.getType(), true); + } + } else { + // can only be multi-valued (n == 0 || n > 1) + propDef = session.getItemDefinitionProvider().getQPropertyDefinition(parentNtNames, newName, conflicting.getType(), true); + } + + Operation ap = AddProperty.create(parent, newName, conflicting.getType(), propDef, conflicting.getValues()); + stateMgr.execute(ap); + Operation rm = Remove.create(conflicting); + stateMgr.execute(rm); + } + } + + // do create new nodeState + QNodeDefinition def = session.getItemDefinitionProvider().getQNodeDefinition(parentNtNames, nodeInfo.getName(), nodeInfo.getNodeTypeName()); + if (def.isProtected()) { + log.debug("Skipping protected nodeState (" + nodeInfo.getName() + ")"); + return null; + } else { + Name ntName = nodeInfo.getNodeTypeName(); + if (ntName == null) { + // use default node type + ntName = def.getDefaultPrimaryType(); + } + Operation an = AddNode.create(parent, nodeInfo.getName(), ntName, nodeInfo.getUUID()); + stateMgr.execute(an); + // retrieve id of state that has been created during execution of AddNode + NodeState childState = (NodeState) ((AddNode) an).getAddedStates().get(0); + + // and set mixin types + Name[] mixinNames = nodeInfo.getMixinNames(); + if (mixinNames != null && mixinNames.length > 0) { + Operation sm = SetMixin.create(childState, nodeInfo.getMixinNames()); + stateMgr.execute(sm); + } + return childState; + } + } + + /** + * + * @param pi + * @param parentState + * @param resolver + * @throws RepositoryException + * @throws ConstraintViolationException + */ + private void importProperty(PropInfo pi, NodeState parentState, NamePathResolver resolver) throws RepositoryException, ConstraintViolationException { + Name propName = pi.getName(); + TextValue[] tva = pi.getValues(); + int infoType = pi.getType(); + + PropertyState propState = null; + QPropertyDefinition def = null; + + NodeEntry parentEntry = (NodeEntry) parentState.getHierarchyEntry(); + PropertyEntry pEntry = parentEntry.getPropertyEntry(propName); + if (pEntry != null) { + // a property with that name already exists... + try { + PropertyState existing = pEntry.getPropertyState(); + def = existing.getDefinition(); + if (def.isProtected()) { + // skip protected property + log.debug("skipping protected property " + LogUtil.safeGetJCRPath(existing, session.getPathResolver())); + return; + } + if (def.isAutoCreated() + && (existing.getType() == infoType || infoType == PropertyType.UNDEFINED) + && def.isMultiple() == existing.isMultiValued()) { + // this property has already been auto-created, no need to create it + propState = existing; + } else { + throw new ItemExistsException(LogUtil.safeGetJCRPath(existing, session.getPathResolver())); + } + } catch (ItemNotFoundException e) { + // property doesn't exist any more + // -> ignore + } + } + + Name[] parentNtNames = parentState.getAllNodeTypeNames(); + if (def == null) { + // there's no property with that name, find applicable definition + if (tva.length == 1) { + // could be single- or multi-valued (n == 1) + def = session.getItemDefinitionProvider().getQPropertyDefinition(parentNtNames, propName, infoType); + } else { + // can only be multi-valued (n == 0 || n > 1) + def = session.getItemDefinitionProvider().getQPropertyDefinition(parentNtNames, propName, infoType, true); + } + if (def.isProtected()) { + // skip protected property + log.debug("skipping protected property " + propName); + return; + } + } + + // retrieve the target property type needed for creation of QValue(s) + // including an eventual conversion. the targetType is then needed for + // setting/updating the type of the property-state. + int targetType = def.getRequiredType(); + if (targetType == PropertyType.UNDEFINED) { + if (infoType == PropertyType.UNDEFINED) { + targetType = PropertyType.STRING; + } else { + targetType = infoType; + } + } + + QValue[] values = getPropertyValues(pi, targetType, def.isMultiple(), resolver); + if (propState == null) { + // create new property + Operation ap = AddProperty.create(parentState, propName, targetType, def, values); + stateMgr.execute(ap); + propState = parentEntry.getPropertyEntry(propName).getPropertyState(); + } else { + // modify value of existing property + Operation sp = SetPropertyValue.create(propState, values, targetType); + stateMgr.execute(sp); + } + + // store reference for later resolution + if (propState.getType() == PropertyType.REFERENCE) { + refTracker.processedReference(propState); + } + } + + /** + * + * @param propertyInfo + * @param targetType + * @param isMultiple + * @param resolver The name/path resolver used to build QValues. + * @return + * @throws RepositoryException + */ + private QValue[] getPropertyValues(PropInfo propertyInfo, int targetType, + boolean isMultiple, NamePathResolver resolver) + throws RepositoryException { + TextValue[] tva = propertyInfo.getValues(); + // check multi-valued characteristic + if ((tva.length == 0 || tva.length > 1) && !isMultiple) { + throw new ConstraintViolationException(propertyInfo.getName() + " is not multi-valued."); + } + // convert serialized values to QValue objects + QValue[] iva = new QValue[tva.length]; + for (int i = 0; i < tva.length; i++) { + iva[i] = buildQValue(tva[i], targetType, resolver); + } + return iva; + } + + /** + * + * @param tv + * @param targetType + * @param resolver The name/path resolver used to build a QValue. + * @return + * @throws RepositoryException + */ + private QValue buildQValue(TextValue tv, int targetType, NamePathResolver resolver) throws RepositoryException { + QValue iv; + try { + switch (targetType) { + case PropertyType.BINARY: + // base64 encoded BINARY type + if (tv.length() < 0x10000) { + // < 65kb: deserialize BINARY type in memory + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Base64.decode(tv.retrieve(), baos); + // no need to close ByteArrayOutputStream + //baos.close(); + iv = session.getQValueFactory().create(baos.toByteArray()); + } else { + // >= 65kb: deserialize BINARY type + // using Reader and temporary file + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + File tmpFile = fileFactory.createTransientFile("bin", null, null); + FileOutputStream out = new FileOutputStream(tmpFile); + Reader reader = tv.reader(); + try { + Base64.decode(reader, out); + } finally { + reader.close(); + out.close(); + } + iv = session.getQValueFactory().create(tmpFile); + } + break; + default: + // build iv using namespace context of xml document + Value v = ValueHelper.convert(tv.retrieve(), targetType, session.getValueFactory()); + iv = ValueFormat.getQValue(v, resolver, session.getQValueFactory()); + break; + } + return iv; + } catch (IOException e) { + String msg = "failed to retrieve serialized value"; + log.debug(msg, e); + throw new RepositoryException(msg, e); + } + } + + /** + * Validate the given NodeInfo: make sure, that if a uuid is + * defined, the primary or the mixin types include mix:referenceable. + * + * @param nodeInfo + * @throws RepositoryException + */ + private void checkIncludesMixReferenceable(Importer.NodeInfo nodeInfo) throws RepositoryException { + List l = new ArrayList(); + l.add(nodeInfo.getNodeTypeName()); + Name[] mixinNames = nodeInfo.getMixinNames(); + if (mixinNames != null && mixinNames.length > 0) { + l.addAll(Arrays.asList(nodeInfo.getMixinNames())); + } + if (l.contains(NameConstants.MIX_REFERENCEABLE)) { + // shortcut + return; + } + Name[] ntNames = l.toArray(new Name[l.size()]); + EffectiveNodeType ent = session.getEffectiveNodeTypeProvider().getEffectiveNodeType(ntNames); + if (!ent.includesNodeType(NameConstants.MIX_REFERENCEABLE)) { + throw new ConstraintViolationException("XML defines jcr:uuid without defining import node to be referenceable."); + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/SysViewImportHandler.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/SysViewImportHandler.java new file mode 100644 index 00000000000..9f55f712462 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/SysViewImportHandler.java @@ -0,0 +1,343 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.xml; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +import javax.jcr.InvalidSerializedDataException; +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +/** + * SysViewImportHandler ... + */ +class SysViewImportHandler extends TargetImportHandler { + + /** Local part of sv:node. */ + private static final String NODE = "node"; + + /** Local part of sv:property. */ + private static final String PROPERTY = "property"; + + /** Local part of sv:value. */ + private static final String VALUE = "value"; + + /** Local part of sv:name. */ + private static final String NAME = "name"; + + /** Local part of sv:type. */ + private static final String TYPE = "type"; + + /** + * stack of ImportState instances; an instance is pushed onto the stack + * in the startElement method every time a sv:node element is encountered; + * the same instance is popped from the stack in the endElement method + * when the corresponding sv:node element is encountered. + */ + private final Stack stack = new Stack(); + + /** + * fields used temporarily while processing sv:property and sv:value elements + */ + private Name currentPropName; + private int currentPropType = PropertyType.UNDEFINED; + // list of AppendableValue objects + private final List currentPropValues = new ArrayList(); + private AppendableValue currentPropValue; + + /** + * Constructs a new SysViewImportHandler. + * + * @param importer + * @param resolver + */ + SysViewImportHandler(Importer importer, NamePathResolver resolver) { + super(importer, resolver); + } + + private void processNode(ImportState state, boolean start, boolean end) + throws SAXException { + if (!start && !end) { + return; + } + Name[] mixins = null; + if (state.mixinNames != null) { + mixins = state.mixinNames.toArray(new Name[state.mixinNames.size()]); + } + Importer.NodeInfo nodeInfo = new Importer.NodeInfo(state.nodeName, state.nodeTypeName, mixins, state.uuid); + + if (state.uuid != null) { + nodeInfo.setUUID(state.uuid); + } + // call Importer + try { + if (start) { + importer.startNode(nodeInfo, state.props, resolver); + // dispose temporary property values + for (Importer.PropInfo pi : state.props) { + disposePropertyValues(pi); + } + } + if (end) { + importer.endNode(nodeInfo); + } + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + //-------------------------------------------------------< ContentHandler > + /** + * {@inheritDoc} + */ + @Override + public void startElement(String namespaceURI, String localName, + String qName, Attributes atts) + throws SAXException { + // check namespace + if (!Name.NS_SV_URI.equals(namespaceURI)) { + throw new SAXException(new InvalidSerializedDataException("invalid namespace for element in system view xml document: " + + namespaceURI)); + } + // check element name + if (NODE.equals(localName)) { + // sv:node element + + // node name (value of sv:name attribute) + String name = atts.getValue(Name.NS_SV_URI, NAME); + if (name == null) { + throw new SAXException(new InvalidSerializedDataException( + "missing mandatory sv:name attribute of element sv:node")); + } + + if (!stack.isEmpty()) { + // process current node first + ImportState current = stack.peek(); + // need to start current node + if (!current.started) { + processNode(current, true, false); + current.started = true; + } + } + + // push new ImportState instance onto the stack + ImportState state = new ImportState(); + try { + state.nodeName = resolver.getQName(name); + } catch (NameException e) { + throw new SAXException(new InvalidSerializedDataException("illegal node name: " + name, e)); + } catch (NamespaceException e) { + throw new SAXException(new InvalidSerializedDataException("illegal node name: " + name, e)); + } + stack.push(state); + } else if (PROPERTY.equals(localName)) { + // sv:property element + + // reset temp fields + currentPropValues.clear(); + + // property name (value of sv:name attribute) + String name = atts.getValue(Name.NS_SV_URI, NAME); + if (name == null) { + throw new SAXException(new InvalidSerializedDataException( + "missing mandatory sv:name attribute of element sv:property")); + } + try { + currentPropName = resolver.getQName(name); + } catch (NameException e) { + throw new SAXException(new InvalidSerializedDataException("illegal property name: " + name, e)); + } catch (NamespaceException e) { + throw new SAXException(new InvalidSerializedDataException("illegal property name: " + name, e)); + } + // property type (sv:type attribute) + String type = atts.getValue(Name.NS_SV_URI, TYPE); + if (type == null) { + throw new SAXException(new InvalidSerializedDataException( + "missing mandatory sv:type attribute of element sv:property")); + } + currentPropType = PropertyType.valueFromName(type); + } else if (VALUE.equals(localName)) { + // sv:value element + + // reset temp fields + currentPropValue = new BufferedStringValue(); + } else { + throw new SAXException(new InvalidSerializedDataException("unexpected element found in system view xml document: " + + localName)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + if (currentPropValue != null) { + // property value (character data of sv:value element) + try { + currentPropValue.append(ch, start, length); + } catch (IOException ioe) { + throw new SAXException("error while processing property value", + ioe); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void ignorableWhitespace(char[] ch, int start, int length) + throws SAXException { + if (currentPropValue != null) { + // property value + + // data reported by the ignorableWhitespace event within + // sv:value tags is considered part of the value + try { + currentPropValue.append(ch, start, length); + } catch (IOException ioe) { + throw new SAXException("error while processing property value", + ioe); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void endElement(String namespaceURI, String localName, String qName) + throws SAXException { + // check element name + ImportState state = stack.peek(); + if (NODE.equals(localName)) { + // sv:node element + if (!state.started) { + // need to start & end current node + processNode(state, true, true); + state.started = true; + } else { + // need to end current node + processNode(state, false, true); + } + // pop current state from stack + stack.pop(); + } else if (PROPERTY.equals(localName)) { + // sv:property element + + // check if all system properties (jcr:primaryType, jcr:uuid etc.) + // have been collected and create node as necessary + if (currentPropName.equals(NameConstants.JCR_PRIMARYTYPE)) { + AppendableValue val = (AppendableValue) currentPropValues.get(0); + String s = null; + try { + s = val.retrieve(); + state.nodeTypeName = resolver.getQName(s); + } catch (IOException ioe) { + throw new SAXException("error while retrieving value", ioe); + } catch (NameException e) { + throw new SAXException(new InvalidSerializedDataException("illegal node type name: " + s, e)); + } catch (NamespaceException e) { + throw new SAXException(new InvalidSerializedDataException("illegal node type name: " + s, e)); + } + } else if (currentPropName.equals(NameConstants.JCR_MIXINTYPES)) { + if (state.mixinNames == null) { + state.mixinNames = new ArrayList(currentPropValues.size()); + } + for (int i = 0; i < currentPropValues.size(); i++) { + AppendableValue val = + (AppendableValue) currentPropValues.get(i); + String s = null; + try { + s = val.retrieve(); + Name mixin = resolver.getQName(s); + state.mixinNames.add(mixin); + } catch (IOException ioe) { + throw new SAXException("error while retrieving value", ioe); + } catch (NameException e) { + throw new SAXException(new InvalidSerializedDataException("illegal mixin type name: " + s, e)); + } catch (NamespaceException e) { + throw new SAXException(new InvalidSerializedDataException("illegal mixin type name: " + s, e)); + } + } + } else if (currentPropName.equals(NameConstants.JCR_UUID)) { + AppendableValue val = (AppendableValue) currentPropValues.get(0); + try { + state.uuid = val.retrieve(); + } catch (IOException ioe) { + throw new SAXException("error while retrieving value", ioe); + } + } else { + Importer.TextValue[] values = currentPropValues.toArray(new Importer.TextValue[currentPropValues.size()]); + Importer.PropInfo prop = new Importer.PropInfo(currentPropName, currentPropType, values); + state.props.add(prop); + } + // reset temp fields + currentPropValues.clear(); + } else if (VALUE.equals(localName)) { + // sv:value element + currentPropValues.add(currentPropValue); + // reset temp fields + currentPropValue = null; + } else { + throw new SAXException(new InvalidSerializedDataException("invalid element in system view xml document: " + localName)); + } + } + + //--------------------------------------------------------< inner classes > + class ImportState { + /** + * name of current node + */ + Name nodeName; + /** + * primary type of current node + */ + Name nodeTypeName; + /** + * list of mixin types of current node + */ + List mixinNames; + /** + * uuid of current node + */ + String uuid; + + /** + * list of PropInfo instances representing properties of current node + */ + List props = new ArrayList(); + + /** + * flag indicating whether startNode() has been called for current node + */ + boolean started = false; + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/TargetImportHandler.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/TargetImportHandler.java new file mode 100644 index 00000000000..5e1d64bbc03 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/TargetImportHandler.java @@ -0,0 +1,370 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.xml; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.util.TransientFileFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * TargetImportHandler serves as the base class for the concrete + * classes {@link DocViewImportHandler} and + * {@link SysViewImportHandler}. + */ +abstract class TargetImportHandler extends DefaultHandler { + + private static Logger log = LoggerFactory.getLogger(TargetImportHandler.class); + + protected final Importer importer; + protected final NamePathResolver resolver; + + protected TargetImportHandler(Importer importer, NamePathResolver resolver) { + this.importer = importer; + this.resolver = resolver; + } + + /** + * Disposes all instances of AppendableValue contained in the + * given property info's value array. + * + * @param prop property info + */ + protected void disposePropertyValues(Importer.PropInfo prop) { + Importer.TextValue[] vals = prop.getValues(); + for (int i = 0; i < vals.length; i++) { + if (vals[i] instanceof AppendableValue) { + try { + ((AppendableValue) vals[i]).dispose(); + } catch (IOException ioe) { + log.warn("error while disposing temporary value", ioe); + // fall through... + } + } + } + } + + //-------------------------------------------------------< ContentHandler > + + /** + * Initializes the underlying {@link Importer} instance. This method + * is called by the XML parser when the XML document starts. + * + * @throws SAXException if the importer can not be initialized + * @see DefaultHandler#startDocument() + */ + @Override + public void startDocument() throws SAXException { + try { + importer.start(); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + /** + * Closes the underlying {@link Importer} instance. This method + * is called by the XML parser when the XML document ends. + * + * @throws SAXException if the importer can not be closed + * @see DefaultHandler#endDocument() + */ + @Override + public void endDocument() throws SAXException { + try { + importer.end(); + } catch (RepositoryException re) { + throw new SAXException(re); + } + } + + //--------------------------------------------------------< inner classes > + /** + * AppendableValue represents a serialized value that is + * appendable. + *

        + * Important: Note that in order to free resources + * {@link #dispose()} should be called as soon as an + * AppendableValue object is not used anymore. + */ + public interface AppendableValue extends Importer.TextValue { + /** + * Append a portion of an array of characters. + * + * @param chars the characters to be appended + * @param start the index of the first character to append + * @param length the number of characters to append + * @throws IOException if an I/O error occurs + */ + void append(char[] chars, int start, int length) + throws IOException; + + /** + * Close this value. Once a value has been closed, + * further append() invocations will cause an IOException to be thrown. + * + * @throws IOException if an I/O error occurs + */ + void close() throws IOException; + + /** + * 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. + * + * @throws IOException if an I/O error occurs + */ + void dispose() throws IOException; + } + + /** + * StringValue represents an immutable serialized value. + */ + protected class StringValue implements Importer.TextValue { + + private final String value; + + /** + * Constructs a new StringValue representing the given + * value. + * + * @param value + */ + protected StringValue(String value) { + this.value = value; + } + + //--------------------------------------------------------< TextValue > + /** + * {@inheritDoc} + */ + public long length() { + return value.length(); + } + + /** + * {@inheritDoc} + */ + public String retrieve() { + return value; + } + + /** + * {@inheritDoc} + */ + public Reader reader() { + return new StringReader(value); + } + } + + /** + * BufferedStringValue represents an appendable + * serialized value that is either buffered in-memory or backed + * by a temporary file if its size exceeds a certain limit. + *

        + * Important: Note that in order to free resources + * {@link #dispose()} should be called as soon as + * BufferedStringValue instance is not used anymore. + */ + protected class BufferedStringValue implements AppendableValue { + + /** + * max size for buffering data in memory + */ + private static final int MAX_BUFFER_SIZE = 0x10000; + /** + * size of increment if capacity buffer needs to be enlarged + */ + private static final int BUFFER_INCREMENT = 0x2000; + /** + * in-memory buffer + */ + private char[] buffer; + /** + * current position within buffer (size of actual data in buffer) + */ + private int bufferPos; + + /** + * backing temporary file created when size of data exceeds + * MAX_BUFFER_SIZE + */ + private File tmpFile; + /** + * writer used to write to tmpFile; writer & tmpFile are always + * instantiated together, i.e. they are either both null or both not null. + */ + private Writer writer; + + /** + * Constructs a new empty BufferedStringValue. + */ + protected BufferedStringValue() { + buffer = new char[0x2000]; + bufferPos = 0; + tmpFile = null; + writer = null; + } + + //--------------------------------------------------------< TextValue > + /** + * {@inheritDoc} + */ + public long length() throws IOException { + if (buffer != null) { + return bufferPos; + } else if (tmpFile != null) { + // flush writer first + writer.flush(); + return tmpFile.length(); + } else { + throw new IOException("this instance has already been disposed"); + } + } + + /** + * {@inheritDoc} + */ + public String retrieve() throws IOException { + if (buffer != null) { + return new String(buffer, 0, bufferPos); + } else if (tmpFile != null) { + // flush writer first + writer.flush(); + if (tmpFile.length() > Integer.MAX_VALUE) { + throw new IOException("size of value is too big, use reader()"); + } + StringBuffer sb = new StringBuffer((int) tmpFile.length()); + char[] chunk = new char[0x2000]; + int read; + Reader reader = new FileReader(tmpFile); + try { + while ((read = reader.read(chunk)) > -1) { + sb.append(chunk, 0, read); + } + } finally { + reader.close(); + } + return sb.toString(); + } else { + throw new IOException("this instance has already been disposed"); + } + } + + /** + * {@inheritDoc} + */ + public Reader reader() throws IOException { + if (buffer != null) { + return new StringReader(new String(buffer, 0, bufferPos)); + } else if (tmpFile != null) { + // flush writer first + writer.flush(); + return new FileReader(tmpFile); + } else { + throw new IOException("this instance has already been disposed"); + } + } + + //--------------------------------------------------< AppendableValue > + /** + * {@inheritDoc} + */ + public void append(char[] chars, int start, int length) + throws IOException { + if (buffer != null) { + if (bufferPos + length > MAX_BUFFER_SIZE) { + // threshold for keeping data in memory exceeded; + // create temp file and spool buffer contents + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + tmpFile = fileFactory.createTransientFile("txt", null, null); + final FileOutputStream fout = new FileOutputStream(tmpFile); + writer = new OutputStreamWriter(fout) { + @Override + public void flush() throws IOException { + // flush this writer + super.flush(); + // force synchronization with underlying file + fout.getFD().sync(); + } + }; + writer.write(buffer, 0, bufferPos); + writer.write(chars, start, length); + // reset fields + buffer = null; + bufferPos = 0; + } else { + if (bufferPos + length > buffer.length) { + // reallocate new buffer and spool old buffer contents + char[] newBuffer = new char[ bufferPos + length + BUFFER_INCREMENT]; + System.arraycopy(buffer, 0, newBuffer, 0, bufferPos); + buffer = newBuffer; + } + System.arraycopy(chars, start, buffer, bufferPos, length); + bufferPos += length; + } + } else if (tmpFile != null) { + writer.write(chars, start, length); + } else { + throw new IOException("this instance has already been disposed"); + } + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + if (buffer != null) { + // nop + } else if (tmpFile != null) { + writer.close(); + } else { + throw new IOException("this instance has already been disposed"); + } + } + + /** + * {@inheritDoc} + */ + public void dispose() throws IOException { + if (buffer != null) { + buffer = null; + bufferPos = 0; + } else if (tmpFile != null) { + writer.close(); + tmpFile.delete(); + tmpFile = null; + writer = null; + } else { + throw new IOException("this instance has already been disposed"); + } + } + } +} diff --git a/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/WorkspaceContentHandler.java b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/WorkspaceContentHandler.java new file mode 100644 index 00000000000..1ee083119fa --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/java/org/apache/jackrabbit/jcr2spi/xml/WorkspaceContentHandler.java @@ -0,0 +1,133 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. The ASF licenses this file to You +* under the Apache License, Version 2.0 (the "License"); you may not +* use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.apache.jackrabbit.jcr2spi.xml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.jcr.RepositoryException; +import javax.jcr.Workspace; + +import org.apache.jackrabbit.commons.xml.SerializingContentHandler; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.ContentHandler; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * WorkspaceContentHandler... + */ +public class WorkspaceContentHandler extends DefaultHandler { + + private static Logger log = LoggerFactory.getLogger(WorkspaceContentHandler.class); + + private final String parentAbsPath; + private final int uuidBehavior; + private final Workspace workspace; + + private final File tmpFile; + private final ContentHandler delegatee; + + public WorkspaceContentHandler(Workspace workspace, String parentAbsPath, int uuidBehavior) throws RepositoryException { + this.workspace = workspace; + this.parentAbsPath = parentAbsPath; + this.uuidBehavior = uuidBehavior; + + try { + String tmpName = Text.md5(parentAbsPath); + this.tmpFile = File.createTempFile("___" + tmpName, ".xml"); + this.delegatee = SerializingContentHandler.getSerializer( + new FileOutputStream(tmpFile)); + } catch (FileNotFoundException e) { + throw new RepositoryException(e); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (SAXException e) { + throw new RepositoryException(e); + } + } + + @Override + public void endDocument() throws SAXException { + delegatee.endDocument(); + try { + workspace.importXML(parentAbsPath, new FileInputStream(tmpFile), uuidBehavior); + } catch (IOException e) { + throw new SAXException(e); + } catch (RepositoryException e) { + throw new SAXException(e); + } finally { + tmpFile.delete(); + } + } + + @Override + public void startDocument() throws SAXException { + delegatee.startDocument(); + } + + @Override + public void characters(char ch[], int start, int length) throws SAXException { + delegatee.characters(ch, start, length); + } + + @Override + public void ignorableWhitespace(char ch[], int start, int length) throws SAXException { + delegatee.ignorableWhitespace(ch, start, length); + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + delegatee.endPrefixMapping(prefix); + } + + @Override + public void skippedEntity(String name) throws SAXException { + delegatee.skippedEntity(name); + } + + @Override + public void setDocumentLocator(Locator locator) { + delegatee.setDocumentLocator(locator); + } + + @Override + public void processingInstruction(String target, String data) throws SAXException { + delegatee.processingInstruction(target, data); + } + + @Override + public void startPrefixMapping(String prefix, String uri) throws SAXException { + delegatee.startPrefixMapping(prefix, uri); + } + + @Override + public void endElement(String namespaceURI, String localName, String qName) throws SAXException { + delegatee.endElement(namespaceURI, localName, qName); + } + + @Override + public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { + delegatee.startElement(namespaceURI, localName, qName, atts); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory b/jackrabbit-jcr2spi/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory new file mode 100644 index 00000000000..840fad4a2e8 --- /dev/null +++ b/jackrabbit-jcr2spi/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory @@ -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.jcr2spi.Jcr2spiRepositoryFactory + diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractJCR2SPITest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractJCR2SPITest.java new file mode 100644 index 00000000000..c8832cd7bc9 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractJCR2SPITest.java @@ -0,0 +1,669 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.Path.Element; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.AbstractReadableRepositoryService; +import org.apache.jackrabbit.spi.commons.ItemInfoBuilder; +import org.apache.jackrabbit.spi.commons.ItemInfoBuilder.NodeInfoBuilder; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; + +/** + * Abstract base class for jcr2spi tests. This class implements {@link RepositoryService} + * by delegation to {@link AbstractReadableRepositoryService}. Implementors can override + * individual methods as needed. + */ +public abstract class AbstractJCR2SPITest extends TestCase implements RepositoryService { + private static final String DEFAULT_WSP = "default"; + + private RepositoryService repositoryService; + + protected ItemInfoStore itemInfoStore; + protected RepositoryConfig config; + protected Repository repository; + + @Override + public void setUp() throws Exception { + super.setUp(); + + itemInfoStore = new ItemInfoStore(); + ItemInfoBuilder.Listener listener = new ItemInfoBuilder.Listener() { + public void createPropertyInfo(PropertyInfo propertyInfo) { + itemInfoStore.addItemInfo(propertyInfo); + } + + public void createNodeInfo(NodeInfo nodeInfo) { + itemInfoStore.addItemInfo(nodeInfo); + } + + public void createChildInfos(NodeId id, Iterator childInfos) { + itemInfoStore.setChildInfos(id, childInfos); + } + }; + + initInfosStore(ItemInfoBuilder.nodeInfoBuilder(listener)); + repositoryService = getRepositoryService(); + config = getRepositoryConfig(); + repository = getRepository(); + } + + /** + * Convert the given path to a JCR path. + * @param path + * @return + */ + public static final String toJCRPath(Path path) { + Element[] elems = path.getElements(); + StringBuffer jcrPath = new StringBuffer(); + + for (int k = 0; k < elems.length; k++) { + jcrPath.append(elems[k].getName().getLocalName()); + if (k + 1 < elems.length || elems.length == 1) { + jcrPath.append('/'); + } + } + + return jcrPath.toString(); + } + + /** + * Initialize the mock repository using the builder. + * @param builder + * @throws RepositoryException + */ + protected abstract void initInfosStore(NodeInfoBuilder builder) throws RepositoryException; + + protected RepositoryService getRepositoryService() throws RepositoryException, ParseException { + return new AbstractReadableRepositoryService(getDescriptors(), getNameSpaces(), getCndReader(), + getWspNames(), DEFAULT_WSP) { + + @Override + protected void checkCredentials(Credentials credentials, String workspaceName) + throws LoginException { + + AbstractJCR2SPITest.this.checkCredentials(credentials, workspaceName); + } + + @Override + protected QNodeDefinition createRootNodeDefinition(SessionInfo sessionInfo) + throws RepositoryException { + + return AbstractJCR2SPITest.this.createRootNodeDefinition(); + } + + public Iterator getItemInfos(SessionInfo sessionInfo, ItemId itemId) + throws ItemNotFoundException, RepositoryException { + + return AbstractJCR2SPITest.this.getItemInfos(sessionInfo, itemId); + } + + public Iterator getChildInfos(SessionInfo sessionInfo, NodeId parentId) + throws ItemNotFoundException, RepositoryException { + + return AbstractJCR2SPITest.this.getChildInfos(sessionInfo, parentId); + } + + @Override + public PrivilegeDefinition[] getPrivilegeDefinitions(SessionInfo sessionInfo) throws RepositoryException { + return AbstractJCR2SPITest.this.getPrivilegeDefinitions(sessionInfo); + } + + @Override + public PrivilegeDefinition[] getSupportedPrivileges(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return AbstractJCR2SPITest.this.getSupportedPrivileges(sessionInfo, nodeId); + } + + @Override + public Name[] getPrivilegeNames(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return AbstractJCR2SPITest.this.getPrivilegeNames(sessionInfo, nodeId); + } + + @Override + public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws ItemNotFoundException, + RepositoryException { + + return AbstractJCR2SPITest.this.getNodeInfo(sessionInfo, nodeId); + } + + public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId) + throws ItemNotFoundException, RepositoryException { + + return AbstractJCR2SPITest.this.getPropertyInfo(sessionInfo, propertyId); + } + + public Iterator getReferences(SessionInfo sessionInfo, NodeId nodeId, + Name propertyName, boolean weakReferences) throws ItemNotFoundException, + RepositoryException { + + return AbstractJCR2SPITest.this.getReferences(sessionInfo, nodeId, propertyName, weakReferences); + } + + }; + } + + protected Reader getCndReader() throws RepositoryException { + String resourceName = "default-nodetypes.cnd"; + InputStream is = AbstractJCR2SPITest.class.getResourceAsStream(resourceName); + if (is == null) { + throw new RepositoryException(("Resource not found: " + resourceName)); + } + + return new InputStreamReader(new BufferedInputStream(is)); + } + + protected Map getNameSpaces() { + return Collections.emptyMap(); + } + + protected Map getDescriptors() throws RepositoryException { + Map descriptorKeys = new HashMap(); + + QValueFactory qvf = QValueFactoryImpl.getInstance(); + + descriptorKeys.put(Repository.REP_NAME_DESC, new QValue[] {qvf.create("Mock Repository", PropertyType.STRING)}); + descriptorKeys.put(Repository.REP_VENDOR_DESC, new QValue[] {qvf.create("Apache Software Foundation", PropertyType.STRING)}); + descriptorKeys.put(Repository.REP_VENDOR_URL_DESC, new QValue[] {qvf.create("http://www.apache.org/", PropertyType.STRING)}); + descriptorKeys.put(Repository.REP_VERSION_DESC, new QValue[] {qvf.create("2.0", PropertyType.STRING)}); + descriptorKeys.put(Repository.SPEC_NAME_DESC, new QValue[] {qvf.create("Content Repository API for Java(TM) Technology Specification", PropertyType.STRING)}); + descriptorKeys.put(Repository.SPEC_VERSION_DESC, new QValue[] {qvf.create("2.0", PropertyType.STRING)}); + + return descriptorKeys; + } + + protected List getWspNames() { + return Collections.singletonList(DEFAULT_WSP); + } + + protected RepositoryConfig getRepositoryConfig() { + return new AbstractRepositoryConfig() { + public RepositoryService getRepositoryService() throws RepositoryException { + return AbstractJCR2SPITest.this; + } + }; + } + + protected Repository getRepository() throws RepositoryException { + return RepositoryImpl.create(config); + } + + protected void checkCredentials(Credentials credentials, String workspaceName) { + // empty -> all credentials are valid by default + } + + // -----------------------------------------------------< RepositoryService >--- + + public IdFactory getIdFactory() throws RepositoryException { + return repositoryService.getIdFactory(); + } + + public NameFactory getNameFactory() throws RepositoryException { + return repositoryService.getNameFactory(); + } + + public PathFactory getPathFactory() throws RepositoryException { + return repositoryService.getPathFactory(); + } + + public QValueFactory getQValueFactory() throws RepositoryException { + return repositoryService.getQValueFactory(); + } + + public Map getRepositoryDescriptors() throws RepositoryException { + return repositoryService.getRepositoryDescriptors(); + } + + public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) throws RepositoryException { + return repositoryService.getItemInfoCache(sessionInfo); + } + + public PrivilegeDefinition[] getPrivilegeDefinitions( + SessionInfo sessionInfo) throws RepositoryException { + return repositoryService.getPrivilegeDefinitions(sessionInfo); + } + + public PrivilegeDefinition[] getSupportedPrivileges( + SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return repositoryService.getSupportedPrivileges(sessionInfo, nodeId); + } + + public Name[] getPrivilegeNames( + SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return repositoryService.getPrivilegeNames(sessionInfo, nodeId); + } + //-----------------------------------< SessionInfo creation and release >--- + + public SessionInfo obtain(Credentials credentials, String workspaceName) throws RepositoryException { + return repositoryService.obtain(credentials, workspaceName); + } + + public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName) throws RepositoryException { + return repositoryService.obtain(sessionInfo, workspaceName); + } + + public SessionInfo impersonate(SessionInfo sessionInfo, Credentials credentials) + throws RepositoryException { + + return repositoryService.impersonate(sessionInfo, credentials); + } + + public void dispose(SessionInfo sessionInfo) throws RepositoryException { + repositoryService.dispose(sessionInfo); + } + + public String[] getWorkspaceNames(SessionInfo sessionInfo) throws RepositoryException { + return repositoryService.getWorkspaceNames(sessionInfo); + } + + + //-----------------------------------------------------< Access Control >--- + + public boolean isGranted(SessionInfo sessionInfo, ItemId itemId, String[] actions) + throws RepositoryException { + + return repositoryService.isGranted(sessionInfo, itemId, actions); + } + + + //------------------------------------------------------< Reading items >--- + + public QNodeDefinition getNodeDefinition(SessionInfo sessionInfo, NodeId nodeId) + throws RepositoryException { + + return repositoryService.getNodeDefinition(sessionInfo, nodeId); + } + + public QPropertyDefinition getPropertyDefinition(SessionInfo sessionInfo, PropertyId propertyId) + throws RepositoryException { + + return repositoryService.getPropertyDefinition(sessionInfo, propertyId); + } + + protected abstract QNodeDefinition createRootNodeDefinition(); + + public abstract NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException; + + public abstract Iterator getItemInfos(SessionInfo sessionInfo, ItemId itemId) throws ItemNotFoundException, RepositoryException; + + public abstract Iterator getChildInfos(SessionInfo sessionInfo, NodeId parentId) throws ItemNotFoundException, RepositoryException; + + public abstract PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId) throws ItemNotFoundException, RepositoryException; + + public Iterator getReferences(SessionInfo sessionInfo, NodeId nodeId, Name propertyName, + boolean weakReferences) throws RepositoryException { + + return repositoryService.getReferences(sessionInfo, nodeId, propertyName, weakReferences); + } + + //-----------------------------------------------< general modification >--- + + public Batch createBatch(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException { + return repositoryService.createBatch(sessionInfo, itemId); + } + + public void submit(Batch batch) throws RepositoryException { + repositoryService.submit(batch); + } + + @Override + public Tree createTree(SessionInfo sessionInfo, Batch batch, Name nodeName, Name primaryTypeName, String uniqueId) throws RepositoryException { + return repositoryService.createTree(sessionInfo, batch, nodeName, primaryTypeName, uniqueId); + } + + //-------------------------------------------------------------< Import >--- + + public void importXml(SessionInfo sessionInfo, NodeId parentId, InputStream xmlStream, int uuidBehaviour) + throws RepositoryException { + + repositoryService.importXml(sessionInfo, parentId, xmlStream, uuidBehaviour); + } + + + //---------------------------------------------------------< Copy, Move >--- + + public void move(SessionInfo sessionInfo, NodeId srcNodeId, NodeId destParentNodeId, Name destName) + throws RepositoryException { + + repositoryService.move(sessionInfo, srcNodeId, destParentNodeId, destName); + } + + public void copy(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, + NodeId destParentNodeId, Name destName) throws RepositoryException { + + repositoryService.copy(sessionInfo, srcWorkspaceName, srcNodeId, destParentNodeId, destName); + } + + + //------------------------------------------------------< Update, Clone >--- + + public void update(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName) + throws RepositoryException { + + repositoryService.update(sessionInfo, nodeId, srcWorkspaceName); + } + + public void clone(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, + NodeId destParentNodeId, Name destName, boolean removeExisting) throws RepositoryException { + + repositoryService.clone(sessionInfo, srcWorkspaceName, srcNodeId, destParentNodeId, destName, removeExisting); + } + + + //------------------------------------------------------------< Locking >--- + + public LockInfo getLockInfo(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return repositoryService.getLockInfo(sessionInfo, nodeId); + } + + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, boolean sessionScoped) + throws RepositoryException { + + return repositoryService.lock(sessionInfo, nodeId, deep, sessionScoped); + } + + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, boolean sessionScoped, + long timeoutHint, String ownerHint) throws RepositoryException { + + return repositoryService.lock(sessionInfo, nodeId, deep, sessionScoped, timeoutHint, ownerHint); + } + + public void refreshLock(SessionInfo sessionInfo, NodeId nodeId) + throws RepositoryException { + + repositoryService.refreshLock(sessionInfo, nodeId); + } + + public void unlock(SessionInfo sessionInfo, NodeId nodeId) + throws RepositoryException { + + repositoryService.unlock(sessionInfo, nodeId); + } + + //---------------------------------------------------------< Versioning >--- + + public NodeId checkin(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return repositoryService.checkin(sessionInfo, nodeId); + } + + public void checkout(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + repositoryService.checkout(sessionInfo, nodeId); + } + + public void checkout(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) throws RepositoryException { + repositoryService.checkout(sessionInfo, nodeId, activityId); + } + + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId) + throws RepositoryException { + return repositoryService.checkpoint(sessionInfo, nodeId); + } + + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) + throws RepositoryException { + return repositoryService.checkpoint(sessionInfo, nodeId, activityId); + } + + public void removeVersion(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId) + throws RepositoryException { + + repositoryService.removeVersion(sessionInfo, versionHistoryId, versionId); + } + + public void restore(SessionInfo sessionInfo, NodeId[] versionIds, boolean removeExisting) + throws RepositoryException { + + repositoryService.restore(sessionInfo, versionIds, removeExisting); + } + + public void restore(SessionInfo sessionInfo, NodeId nodeId, NodeId versionId, boolean removeExisting) + throws RepositoryException { + + repositoryService.restore(sessionInfo, nodeId, versionId, removeExisting); + } + + public Iterator merge(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName, + boolean bestEffort) throws RepositoryException { + + return repositoryService.merge(sessionInfo, nodeId, srcWorkspaceName, bestEffort); + } + + public Iterator merge(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName, + boolean bestEffort, boolean isShallow) throws RepositoryException { + + return repositoryService.merge(sessionInfo, nodeId, srcWorkspaceName, bestEffort, isShallow); + } + + public void resolveMergeConflict(SessionInfo sessionInfo, NodeId nodeId, NodeId[] mergeFailedIds, + NodeId[] predecessorIds) throws RepositoryException { + + repositoryService.resolveMergeConflict(sessionInfo, nodeId, mergeFailedIds, predecessorIds); + } + + public void addVersionLabel(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId, + Name label, boolean moveLabel) throws RepositoryException { + + repositoryService.addVersionLabel(sessionInfo, versionHistoryId, versionId, label, moveLabel); + } + + public void removeVersionLabel(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId, + Name label) throws RepositoryException { + + repositoryService.removeVersionLabel(sessionInfo, versionHistoryId, versionId, label); + } + + public NodeId createActivity(SessionInfo sessionInfo, String title) + throws RepositoryException { + return repositoryService.createActivity(sessionInfo, title); + } + + public void removeActivity(SessionInfo sessionInfo, NodeId activityId) + throws RepositoryException { + + repositoryService.removeActivity(sessionInfo, activityId); + } + + public Iterator mergeActivity(SessionInfo sessionInfo, NodeId activityId) + throws RepositoryException { + + return repositoryService.mergeActivity(sessionInfo, activityId); + } + + public NodeId createConfiguration(SessionInfo sessionInfo, NodeId nodeId) + throws RepositoryException { + return repositoryService.createConfiguration(sessionInfo, nodeId); + } + + //----------------------------------------------------------< Searching >--- + + public String[] getSupportedQueryLanguages(SessionInfo sessionInfo) throws RepositoryException { + return repositoryService.getSupportedQueryLanguages(sessionInfo); + } + + + public String[] checkQueryStatement(SessionInfo sessionInfo, String statement, String language, + Map namespaces) throws RepositoryException { + + return repositoryService.checkQueryStatement(sessionInfo, statement, language, namespaces); + } + + public QueryInfo executeQuery(SessionInfo sessionInfo, String statement, String language, + Map namespaces, long limit, long offset, Map values) + throws RepositoryException { + + return repositoryService.executeQuery(sessionInfo, statement, language, namespaces, limit, offset, + values); + } + + + //--------------------------------------------------------< Observation >--- + + public EventFilter createEventFilter(SessionInfo sessionInfo, int eventTypes, Path absPath, + boolean isDeep, String[] uuid, Name[] nodeTypeName, boolean noLocal) throws RepositoryException { + + return repositoryService.createEventFilter(sessionInfo, eventTypes, absPath, isDeep, uuid, nodeTypeName, noLocal); + } + + public Subscription createSubscription(SessionInfo sessionInfo, EventFilter[] filters) + throws RepositoryException { + + return repositoryService.createSubscription(sessionInfo, filters); + } + + public void updateEventFilters(Subscription subscription, EventFilter[] filters) + throws RepositoryException { + + repositoryService.updateEventFilters(subscription, filters); + } + + public EventBundle[] getEvents(Subscription subscription, long timeout) throws RepositoryException, + InterruptedException { + + return repositoryService.getEvents(subscription, timeout); + } + + public EventBundle getEvents(SessionInfo sessionInfo, EventFilter filter, long after) + throws RepositoryException { + + return repositoryService.getEvents(sessionInfo, filter, after); + } + + public void dispose(Subscription subscription) throws RepositoryException { + repositoryService.dispose(subscription); + } + + + //---------------------------------------------------------< Namespaces >--- + + public Map getRegisteredNamespaces(SessionInfo sessionInfo) throws RepositoryException { + return repositoryService.getRegisteredNamespaces(sessionInfo); + } + + public String getNamespaceURI(SessionInfo sessionInfo, String prefix) + throws RepositoryException { + + return repositoryService.getNamespaceURI(sessionInfo, prefix); + } + + public String getNamespacePrefix(SessionInfo sessionInfo, String uri) + throws RepositoryException { + + return repositoryService.getNamespacePrefix(sessionInfo, uri); + } + + public void registerNamespace(SessionInfo sessionInfo, String prefix, String uri) + throws RepositoryException { + + repositoryService.registerNamespace(sessionInfo, prefix, uri); + } + + public void unregisterNamespace(SessionInfo sessionInfo, String uri) throws RepositoryException { + repositoryService.unregisterNamespace(sessionInfo, uri); + } + + + //----------------------------------------------------------< NodeTypes >--- + + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo) + throws RepositoryException { + + return repositoryService.getQNodeTypeDefinitions(sessionInfo); + } + + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo, Name[] nodeTypeNames) + throws RepositoryException { + + return repositoryService.getQNodeTypeDefinitions(sessionInfo, nodeTypeNames); + } + + public void registerNodeTypes(SessionInfo sessionInfo, QNodeTypeDefinition[] nodeTypeDefinitions, + boolean allowUpdate) throws RepositoryException { + + repositoryService.registerNodeTypes(sessionInfo, nodeTypeDefinitions, allowUpdate); + } + + public void unregisterNodeTypes(SessionInfo sessionInfo, Name[] nodeTypeNames) + throws UnsupportedRepositoryOperationException, NoSuchNodeTypeException, RepositoryException { + + repositoryService.unregisterNodeTypes(sessionInfo, nodeTypeNames); + } + + //-----------------------------------------------< Workspace Management >--- + + public void createWorkspace(SessionInfo sessionInfo, String name, String srcWorkspaceName) + throws RepositoryException { + + repositoryService.createWorkspace(sessionInfo, name, srcWorkspaceName); + } + + public void deleteWorkspace(SessionInfo sessionInfo, String name) throws RepositoryException { + repositoryService.deleteWorkspace(sessionInfo, name); + } +} + + + + diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractMoveTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractMoveTest.java new file mode 100644 index 00000000000..2cc6b693cc8 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractMoveTest.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.jcr2spi; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractMoveTest... + */ +abstract class AbstractMoveTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(AbstractMoveTest.class); + + protected Node srcParentNode; + protected Node destParentNode; + protected Node moveNode; + + protected String destinationPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create parent node + srcParentNode = testRootNode.addNode(nodeName1, testNodeType); + // create node to be moved + moveNode = srcParentNode.addNode(nodeName2, testNodeType); + // create a node that will serve as new parent + destParentNode = testRootNode.addNode(nodeName3, testNodeType); + // save the new nodes + testRootNode.save(); + + destinationPath = destParentNode.getPath() + "/" + nodeName2; + } + + @Override + protected void tearDown() throws Exception { + srcParentNode = null; + destParentNode = null; + moveNode = null; + super.tearDown(); + } + + protected abstract boolean isSessionMove(); + + protected void doMove(String srcPath, String destPath) + throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException { + if (isSessionMove()) { + superuser.move(srcPath, destPath); + } else { + superuser.getWorkspace().move(srcPath, destPath); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractMoveTreeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractMoveTreeTest.java new file mode 100644 index 00000000000..3c9f588bb2c --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractMoveTreeTest.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.jcr2spi; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractMoveTreeTest... + */ +abstract class AbstractMoveTreeTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(AbstractMoveTreeTest.class); + + protected Node childNode; + protected Node grandChildNode; + protected Property childProperty; + + protected Node srcParentNode; + protected Node destParentNode; + + protected String srcPath; + protected String destinationPath; + protected List childPaths = new ArrayList(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + srcParentNode = testRootNode.addNode(nodeName1, testNodeType); + Node moveNode = srcParentNode.addNode(nodeName2, testNodeType); + destParentNode = testRootNode.addNode(nodeName3, testNodeType); + + srcPath = moveNode.getPath(); + destinationPath = destParentNode.getPath() + "/" + nodeName4; + + childProperty = moveNode.setProperty(propertyName2, "anyString"); + childNode = moveNode.addNode(nodeName2, testNodeType); + grandChildNode = childNode.addNode(nodeName3, testNodeType); + + childPaths.add(grandChildNode.getPath()); + childPaths.add(childNode.getPath()); + + doMove(moveNode.getPath(), destinationPath); + } + + @Override + protected void tearDown() throws Exception { + childNode = null; + grandChildNode = null; + childProperty = null; + srcParentNode = null; + destParentNode = null; + super.tearDown(); + } + + protected abstract boolean saveBeforeMove(); + + protected abstract boolean isSessionMove(); + + protected void doMove(String srcPath, String destPath) throws RepositoryException, LockException, ConstraintViolationException, ItemExistsException, VersionException { + if (saveBeforeMove()) { + testRootNode.save(); + } + if (isSessionMove()) { + superuser.move(srcPath, destPath); + } else { + superuser.getWorkspace().move(srcPath, destPath); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractRepositoryConfig.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractRepositoryConfig.java new file mode 100644 index 00000000000..764b7661668 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AbstractRepositoryConfig.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.jcr2spi; + +import org.apache.jackrabbit.jcr2spi.config.CacheBehaviour; +import org.apache.jackrabbit.jcr2spi.config.RepositoryConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractRepositoryConfig... + */ +public abstract class AbstractRepositoryConfig implements RepositoryConfig { + + private static Logger log = LoggerFactory.getLogger(AbstractRepositoryConfig.class); + + private static final int DEFAULT_ITEM_CACHE_SIZE = 5000; + private static final int DEFAULT_INFO_CACHE_SIZE = 5000; + private static final int DEFAULT_POLL_TIMEOUT = 3000; // 3 seconds + + public CacheBehaviour getCacheBehaviour() { + return CacheBehaviour.INVALIDATE; + } + + public int getItemCacheSize() { + return DEFAULT_ITEM_CACHE_SIZE; + } + + public int getPollTimeout() { + return DEFAULT_POLL_TIMEOUT; + } + + @Override + public T getConfiguration(String name, T defaultValue) { + return null; + } + + public int getInfoCacheSize() { + return DEFAULT_INFO_CACHE_SIZE; + } + +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AccessByRelativePathTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AccessByRelativePathTest.java new file mode 100644 index 00000000000..261390fdc72 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AccessByRelativePathTest.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.jcr2spi; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.NodeIterator; + +/** + * AccessByRelativePathTest... + */ +public class AccessByRelativePathTest extends AbstractJCRTest { + + private static String DOT = "."; + private static String DOTDOT = ".."; + + /** + * Node.hasNode(".") applied to the root node must return + * true. + * + * @throws RepositoryException + */ + public void testRootHasNodeDot() throws RepositoryException { + Node root = superuser.getRootNode(); + assertTrue("Node.hasNode(\".\") must return true.", root.hasNode(DOT)); + } + + /** + * Node.getNode(".") applied to the root node must return + * the same Node again. + * + * @throws RepositoryException + */ + public void testRootGetNodeDot() throws RepositoryException { + Node root = superuser.getRootNode(); + assertTrue("Node.getNode(\".\") must return the same node", root.getNode(DOT).isSame(root)); + } + + /** + * Node.getNode("..") applied to the root node must throw + * PathNotFoundException. + * + * @throws RepositoryException + */ + public void testRootGetNodeDotDot() throws RepositoryException { + Node root = superuser.getRootNode(); + try { + root.getNode(DOTDOT); + fail("Root does not have a parent node. .getNode(\"..\") must fail."); + } catch (RepositoryException e) { + // ok. + } + } + + /** + * Node.hasNode(".") applied to any test node must return + * true. + * + * @throws RepositoryException + */ + public void testHasNodeDot() throws RepositoryException { + assertTrue("Node.hasNode(\".\") must return true.", testRootNode.hasNode(DOT)); + } + + /** + * Node.getNode(".") applied to any test node must return + * the same Node again. + * + * @throws RepositoryException + */ + public void GetNodeDot() throws RepositoryException { + assertTrue("Node.getNode(\".\") must return the same node.", testRootNode.getNode(DOT).isSame(testRootNode)); + } + + /** + * Node.getNode("..") applied to any test node must the same + * node as {@link Node#getParent()}. + * + * @throws RepositoryException + * @throws NotExecutableException if the parent node cannot be retrieved + * with {@link Node#getParent()}. + */ + public void testGetNodeDotDot() throws RepositoryException, NotExecutableException { + Node parent; + try { + parent = testRootNode.getParent(); + } catch (Exception e) { + throw new NotExecutableException(); + } + assertTrue("Node.getNode(\"..\") must return the parent.", testRootNode.getNode(DOTDOT).isSame(parent)); + } + + /** + * Node.hasProperty(".") applied to any test node must return + * false. + * + * @throws RepositoryException + */ + public void testHasPropertyDot() throws RepositoryException { + assertFalse("Node.hasProperty(\".\") must return false.", testRootNode.hasProperty(DOT)); + } + + /** + * Node.getProperty(".") applied to any test node must throw + * PathNotFoundException. + * + * @throws RepositoryException + */ + public void testGetPropertyDot() throws RepositoryException { + try { + testRootNode.getProperty(DOT); + fail("A node must never have a property \".\"."); + } catch (PathNotFoundException e) { + // ok. + } + } + + /** + * Node.hasProperty("..") applied to any test node must return + * false. + * + * @throws RepositoryException + */ + public void testHasPropertyDotDot() throws RepositoryException { + assertFalse("Node.hasProperty(\"..\") must return false.", testRootNode.hasProperty(DOTDOT)); + } + + /** + * Node.getProperty("..") applied to any test node must throw + * PathNotFoundException. + * + * @throws RepositoryException + */ + public void testGetPropertyDotDot() throws RepositoryException { + try { + testRootNode.getProperty(DOTDOT); + fail("A node must never have a property \"..\"."); + } catch (PathNotFoundException e) { + // ok. + } + } + + /** + * Node.getNode("./testNodeName") applied to the parent + * of any node with name 'testNodeName' must return the same node. + * + * @throws RepositoryException + * @throws NotExecutableException if the parent cannot be retrieved or if + * the parent has more than 1 node with the given name. + */ + public void testGetNodeDotSlashName() throws RepositoryException, NotExecutableException { + Node parent; + try { + parent = testRootNode.getParent(); + NodeIterator it = parent.getNodes(testRootNode.getName()); + int cnt = 0; + while (it.hasNext() && cnt <= 1) { + it.nextNode(); + cnt++; + } + if (cnt > 1) { + throw new NotExecutableException(); + } + } catch (Exception e) { + throw new NotExecutableException(); + } + String otherRelPath = DOT + "/" + testRootNode.getName(); + assertTrue(testRootNode.isSame(parent.getNode(otherRelPath))); + } + + /** + * Node.getNode("../" + Node.getName()) applied to any test + * node must return the test node. + * + * @throws RepositoryException + */ + public void testGetNodeDotDotSlashName() throws RepositoryException, NotExecutableException { + String otherRelPath = DOTDOT + "/" + testRootNode.getName(); + if (testRootNode.getIndex() > 1) { + otherRelPath = otherRelPath + "[" + testRootNode.getIndex() + "]"; + } + assertTrue(testRootNode.isSame(testRootNode.getNode(otherRelPath))); + } + + /** + * Node.getProperty("./jcr:primaryType") applied to any + * test node must return the same Property as + * {@link Node#getProperty(String) Node.getProperty("jcr:primaryType")}. + * + * @throws RepositoryException + */ + public void testGetPropertyDotSlashName() throws RepositoryException { + Property pt = testRootNode.getProperty(jcrPrimaryType); + String otherRelPath = DOT + "/" + jcrPrimaryType; + assertTrue(pt.isSame(testRootNode.getProperty(otherRelPath))); + } + + /** + * Node.getProperty("../jcr:primaryType") applied to any + * test node must return the same Property as + * {@link Node#getProperty(String) Node.getParent().getProperty("jcr:primaryType")}. + * + * @throws RepositoryException + * @throws NotExecutableException if the parent cannot be retrieved. + */ + public void testGetPropertyDotDotSlashName() throws RepositoryException, NotExecutableException { + Node parent; + try { + parent = testRootNode.getParent(); + } catch (Exception e) { + throw new NotExecutableException(); + } + + Property pt = parent.getProperty(jcrPrimaryType); + String otherRelPath = DOTDOT + "/" + jcrPrimaryType; + assertTrue(pt.isSame(testRootNode.getProperty(otherRelPath))); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddNewPropertyTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddNewPropertyTest.java new file mode 100644 index 00000000000..09c8209353f --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddNewPropertyTest.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.jcr2spi; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AddNewPropertyTest... + */ +public class AddNewPropertyTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(AddNewPropertyTest.class); + + private String propname; + + @Override + protected void tearDown() throws Exception { + testRootNode.refresh(false); + super.tearDown(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + String propName = propertyName1; + while (testRootNode.hasProperty(propName)) { + propName = propName + "_"; + } + this.propname = propName; + } + + public void testPropertyAccessibleAfterSave() throws NotExecutableException, RepositoryException { + Property p; + try { + p = testRootNode.setProperty(propname, "anyValue"); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + // check if p is valid and can be accessed from the parent node. + String name = p.getName(); + assertEquals("Added property must have the original name", name, propname); + assertTrue("Accessing the created property again must return the 'same' item.", p.isSame(testRootNode.getProperty(propname))); + } + + /** + * Implementation specific test: Node.setProperty for non-existing property + * with a null value must throw ItemNotFoundException + * + * @throws NotExecutableException + * @throws RepositoryException + * @throws LockException + * @throws ConstraintViolationException + * @throws VersionException + */ + public void testAddPropertyWithNullValue() throws NotExecutableException, RepositoryException, LockException, ConstraintViolationException, VersionException { + try { + testRootNode.setProperty(propname, (Value) null); + } catch (ItemNotFoundException e) { + // OK + } + } + + /** + * Implementation specific test: Node.setProperty for non-existing property + * with a null value array must throw ItemNotFoundException + * + * @throws NotExecutableException + * @throws RepositoryException + * @throws LockException + * @throws ConstraintViolationException + * @throws VersionException + */ + public void testAddPropertyWithNullValues() throws NotExecutableException, RepositoryException, LockException, ConstraintViolationException, VersionException { + try { + testRootNode.setProperty(propname, (Value[]) null); + } catch (ItemNotFoundException e) { + // OK + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddNodeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddNodeTest.java new file mode 100644 index 00000000000..fcb5b690cd8 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddNodeTest.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.jcr2spi; + +import java.io.ByteArrayInputStream; + +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * AddNodeTest... + */ +public class AddNodeTest extends AbstractJCRTest { + + /** + * Writing to a locked node must throw LockException even if the lock + * isn't detected withing Jcr2Spi. + * + * @throws Exception + * @see JCR-2585 + */ + public void testAddNodeOnLocked() throws Exception { + Session s = getHelper().getSuperuserSession(); + try { + Node node = s.getNode(testRootNode.getPath()); + Node n = node.addNode(nodeName1); + n.setProperty(propertyName1, "value"); + + testRootNode.lock(true, true); + + s.save(); + } catch (LockException e) { + // success + } finally { + s.logout(); + } + } + + public void testAddNodeNonASCII() throws Exception { + String testName = "test - \u20ac"; + Session s = getHelper().getSuperuserSession(); + try { + Node node = s.getNode(testRootNode.getPath()); + Node n = node.addNode(testName, "nt:file"); + Node c = n.addNode("jcr:content", "nt:resource"); + c.setProperty("jcr:data", s.getValueFactory().createBinary(new ByteArrayInputStream("hello world".getBytes("UTF-8")))); + s.save(); + } finally { + s.logout(); + } + + Session s2 = getHelper().getReadOnlySession(); + try { + Node node = s2.getNode(testRootNode.getPath()).getNode(testName); + assertEquals(testName, node.getName()); + } finally { + s2.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddPropertyTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddPropertyTest.java new file mode 100644 index 00000000000..945a7090f71 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/AddPropertyTest.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.jcr2spi; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** AddPropertyTest... */ +public class AddPropertyTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(AddPropertyTest.class); + + private Node testNode; + + @Override + protected void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1); + testNode.setProperty(propertyName1, "existingProp"); + testRootNode.save(); + } + + @Override + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + + private static void assertItemStatus(Item item, int status) throws NotExecutableException { + if (!(item instanceof ItemImpl)) { + throw new NotExecutableException("org.apache.jackrabbit.jcr2spi.ItemImpl expected"); + } + int st = ((ItemImpl) item).getItemState().getStatus(); + assertEquals("Expected status to be " + Status.getName(status) + ", was " + Status.getName(st), status, st); + } + + public void testReplacingProperty() throws RepositoryException, + NotExecutableException { + Property p1 = testNode.setProperty(propertyName1, "value1"); + p1.remove(); + + Property p2 = testNode.setProperty(propertyName1, "value2"); + p2.remove(); + + Property p3 = testNode.setProperty(propertyName1, "value3"); + testNode.save(); + + assertTrue(testNode.hasProperty(propertyName1)); + assertEquals("value3", testNode.getProperty(propertyName1).getString()); + + assertItemStatus(p1, Status.REMOVED); + assertItemStatus(p2, Status.REMOVED); + assertItemStatus(p3, Status.EXISTING); + } + + public void testReplacingProperty2() throws RepositoryException, + NotExecutableException { + Property p1 = testNode.setProperty(propertyName2, "value1"); + p1.remove(); + + Property p2 = testNode.setProperty(propertyName2, "value2"); + p2.remove(); + + Property p3 = testNode.setProperty(propertyName2, "value3"); + p3.remove(); + testNode.save(); + + assertFalse(testNode.hasProperty(propertyName2)); + + assertItemStatus(p1, Status.REMOVED); + assertItemStatus(p2, Status.REMOVED); + assertItemStatus(p3, Status.REMOVED); + } + + public void testRevertReplacingProperty() throws RepositoryException, + NotExecutableException { + String val = testNode.getProperty(propertyName1).getString(); + Property p1 = testNode.setProperty(propertyName1, "value1"); + p1.remove(); + + Property p2 = testNode.setProperty(propertyName1, "value2"); + p2.remove(); + + Property p3 = testNode.setProperty(propertyName1, "value3"); + testNode.refresh(false); + + assertTrue(testNode.hasProperty(propertyName1)); + assertEquals(val, p1.getString()); + + assertItemStatus(p1, Status.EXISTING); + assertItemStatus(p2, Status.REMOVED); + assertItemStatus(p3, Status.REMOVED); + } + + public void testAddingProperty() throws RepositoryException, + NotExecutableException { + Property p1 = testNode.setProperty(propertyName2, "value1"); + p1.remove(); + + Property p2 = testNode.setProperty(propertyName2, "value2"); + p2.remove(); + + Property p3 = testNode.setProperty(propertyName2, "value3"); + testNode.save(); + + assertTrue(testNode.hasProperty(propertyName2)); + + assertItemStatus(p1, Status.REMOVED); + assertItemStatus(p2, Status.REMOVED); + assertItemStatus(p3, Status.EXISTING); + } + + public void testAddingProperty2() throws RepositoryException, + NotExecutableException { + Property p1 = testNode.setProperty(propertyName2, "value1"); + p1.remove(); + + Property p2 = testNode.setProperty(propertyName2, "value2"); + p2.remove(); + + Property p3 = testNode.setProperty(propertyName2, "value3"); + p3.remove(); + testNode.save(); + + assertFalse(testNode.hasProperty(propertyName2)); + + assertItemStatus(p1, Status.REMOVED); + assertItemStatus(p2, Status.REMOVED); + assertItemStatus(p3, Status.REMOVED); + } + + public void testRevertAddingProperty() throws RepositoryException, + NotExecutableException { + Property p1 = testNode.setProperty(propertyName2, "value1"); + p1.remove(); + + Property p2 = testNode.setProperty(propertyName2, "value2"); + p2.remove(); + + Property p3 = testNode.setProperty(propertyName2, "value3"); + testNode.refresh(false); + + assertFalse(testNode.hasProperty(propertyName2)); + + assertItemStatus(p1, Status.REMOVED); + assertItemStatus(p2, Status.REMOVED); + assertItemStatus(p3, Status.REMOVED); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/BinaryTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/BinaryTest.java new file mode 100644 index 00000000000..a01d8209baa --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/BinaryTest.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.util.Random; + +import static org.junit.Assert.assertArrayEquals; + +import java.io.ByteArrayInputStream; + +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.Property; +import javax.jcr.Binary; +import javax.jcr.ValueFormatException; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.jcr2spi.state.PropertyState; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.test.AbstractJCRTest; + +/** + * BinaryTest... + */ +public class BinaryTest extends AbstractJCRTest { + + private static ByteArrayInputStream generateValue() { + byte[] data = new byte[1024 * 1024]; + new Random().nextBytes(data); + + return new ByteArrayInputStream(data); + } + + private static QValue getQValue(Property p) throws ValueFormatException { + return ((PropertyState) ((PropertyImpl) p).getItemState()).getValue(); + } + + private static void assertDisposed(QValue v) { + try { + v.getStream(); + fail("Value should have been disposed."); + } catch (Exception e) { + // success (interpret this as value was disposed) + } + } + + public void testStreamBinary() throws Exception { + Node test = testRootNode.addNode("test"); + Property p = test.setProperty("prop", generateValue()); + // check before save + checkBinary(p); + superuser.save(); + // check after save + checkBinary(p); + + // check from other session + Session s = getHelper().getReadOnlySession(); + try { + p = s.getNode(testRoot).getNode("test").getProperty("prop"); + checkBinary(p); + } finally { + s.logout(); + } + } + + public void testStreamBinary2() throws Exception { + Node test = testRootNode.addNode("test"); + Property p = test.setProperty("prop", generateValue()); + // check before save + checkBinary(p); + superuser.save(); + // check after save + checkBinary(p); + + // check from other session + Session s = getHelper().getReadOnlySession(); + try { + p = s.getProperty(testRoot + "/test/prop"); + checkBinary(p); + } finally { + s.logout(); + } + } + + public void testBinaryTwiceNewProperty() throws Exception { + Node test = testRootNode.addNode("test"); + Property p = test.setProperty("prop", generateValue()); + QValue qv1 = getQValue(p); + test.setProperty("prop", generateValue()); + QValue qv2 = getQValue(p); + + assertFalse(qv1.equals(qv2)); + + superuser.save(); + + assertEquals(qv2, getQValue(p)); + assertDisposed(qv1); + } + + public void testBinaryTwiceModifiedProperty() throws Exception { + Node test = testRootNode.addNode("test"); + Property p = test.setProperty("prop", generateValue()); + superuser.save(); + + // modify twice + test.setProperty("prop", generateValue()); + QValue qv1 = getQValue(p); + test.setProperty("prop", generateValue()); + QValue qv2 = getQValue(p); + + assertFalse(qv1.equals(qv2)); + + superuser.save(); + + assertEquals(qv2, getQValue(p)); + assertDisposed(qv1); + } + + public void testBinaryTwiceIntermediateSave() throws Exception { + Node test = testRootNode.addNode("test"); + Property p = test.setProperty("prop", generateValue()); + QValue qv1 = getQValue(p); + superuser.save(); + + test.setProperty("prop", generateValue()); + QValue qv2 = getQValue(p); + + assertFalse(qv1.equals(qv2)); + + superuser.save(); + + assertEquals(qv2, getQValue(p)); + assertDisposed(qv1); + } + + public void testRevertSettingExistingBinary() throws Exception { + Node test = testRootNode.addNode("test"); + + Binary b = superuser.getValueFactory().createBinary(generateValue()); + Property p = test.setProperty("prop", b); + QValue qv1 = getQValue(p); + superuser.save(); + + Binary b2 = superuser.getValueFactory().createBinary(generateValue()); + test.setProperty("prop", b2); + QValue qv2 = getQValue(p); + + assertFalse(qv1.equals(qv2)); + + superuser.refresh(false); + + assertEquals(qv1, getQValue(p)); + assertSame(qv1, getQValue(p)); + + assertFalse(qv2.equals(getQValue(p))); + } + + public void testStreamIntegrity() throws Exception { + Node test = testRootNode.addNode("test"); + byte bytes[] = new byte[256]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte)i; + } + ByteArrayInputStream testData = new ByteArrayInputStream(bytes); + Property p = test.setProperty("prop", superuser.getValueFactory().createBinary(testData)); + superuser.save(); + + // check from other session + Session s = getHelper().getReadOnlySession(); + try { + p = s.getNode(testRoot).getNode("test").getProperty("prop"); + + // check the binaries are indeed the same (JCR-4154) + byte[] result = new byte[bytes.length]; + IOUtils.readFully(p.getBinary().getStream(), result); + assertArrayEquals(bytes, result); + } finally { + s.logout(); + } + } + + protected void checkBinary(Property p) throws Exception { + for (int i = 0; i < 3; i++) { + Binary bin = p.getBinary(); + try { + //System.out.println(bin.getClass() + "@" + System.identityHashCode(bin)); + bin.read(new byte[1], 0); + } finally { + bin.dispose(); + } + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/CopyMoveToJsonTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/CopyMoveToJsonTest.java new file mode 100755 index 00000000000..9b7e6c26b4d --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/CopyMoveToJsonTest.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.jcr2spi; + +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.test.AbstractJCRTest; + +public class CopyMoveToJsonTest extends AbstractJCRTest { + + private String jsondata = "{\"foo\":\"bar\"}"; + + public void testCreateJson() throws Exception { + createJsonNode("test.json"); + + Session s = getHelper().getReadOnlySession(); + try { + Property p = s.getNode(testRoot).getNode("test.json").getNode(JcrConstants.JCR_CONTENT) + .getProperty(JcrConstants.JCR_DATA); + assertEquals(jsondata, IOUtils.toString(p.getBinary().getStream(), "UTF-8")); + } finally { + s.logout(); + } + } + + public void testCopyJson() throws Exception { + Node test = createJsonNode("test.json"); + test.getSession().getWorkspace().copy(test.getPath(), test.getParent().getPath() + "/target.json"); + + Session s = getHelper().getReadOnlySession(); + try { + Property p = s.getNode(testRoot).getNode("target.json").getNode(JcrConstants.JCR_CONTENT) + .getProperty(JcrConstants.JCR_DATA); + assertEquals(jsondata, IOUtils.toString(p.getBinary().getStream(), "UTF-8")); + } finally { + s.logout(); + } + } + + public void testMoveJson() throws Exception { + Node test = createJsonNode("test.json"); + test.getSession().getWorkspace().move(test.getPath(), test.getParent().getPath() + "/target.json"); + + Session s = getHelper().getReadOnlySession(); + try { + Property p = s.getNode(testRoot).getNode("target.json").getNode(JcrConstants.JCR_CONTENT) + .getProperty(JcrConstants.JCR_DATA); + assertEquals(jsondata, IOUtils.toString(p.getBinary().getStream(), "UTF-8")); + } finally { + s.logout(); + } + } + + private Node createJsonNode(String name) throws RepositoryException, UnsupportedEncodingException { + Node test = testRootNode.addNode(name, JcrConstants.NT_FILE); + Node content = test.addNode(JcrConstants.JCR_CONTENT, JcrConstants.NT_RESOURCE); + content.setProperty(JcrConstants.JCR_DATA, + test.getSession().getValueFactory().createBinary(new ByteArrayInputStream(jsondata.getBytes("UTF-8")))); + test.getSession().save(); + return test; + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ExternalModificationTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ExternalModificationTest.java new file mode 100644 index 00000000000..453c343011d --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ExternalModificationTest.java @@ -0,0 +1,403 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +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 org.apache.jackrabbit.jcr2spi.state.Status; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** ExternalModificationTest... */ +public class ExternalModificationTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(ExternalModificationTest.class); + + private Node destParentNode; + private Node refNode; + private Session testSession; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create a referenceable node and destination parent. + destParentNode = testRootNode.addNode(nodeName1, testNodeType); + refNode = testRootNode.addNode(nodeName2, getProperty("nodetype2")); + refNode.addMixin(mixReferenceable); + testRootNode.save(); + + testSession = getHelper().getReadWriteSession(); + } + + @Override + protected void tearDown() throws Exception { + if (testSession != null) { + testSession.logout(); + testSession = null; + } + destParentNode = null; + refNode = null; + super.tearDown(); + } + + private static boolean isItemStatus(Item item, int status) throws NotExecutableException { + if (!(item instanceof ItemImpl)) { + throw new NotExecutableException("org.apache.jackrabbit.jcr2spi.ItemImpl expected"); + } + int st = ((ItemImpl) item).getItemState().getStatus(); + return st == status; + } + + private static void assertItemStatus(Item item, int status) throws NotExecutableException { + if (!(item instanceof ItemImpl)) { + throw new NotExecutableException("org.apache.jackrabbit.jcr2spi.ItemImpl expected"); + } + int st = ((ItemImpl) item).getItemState().getStatus(); + assertEquals("Expected status to be " + Status.getName(status) + ", was " + Status.getName(st), status, st); + } + + public void testMovedReferenceableNode() throws RepositoryException, NotExecutableException { + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + + superuser.move(refNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + superuser.save(); + + try { + // modify some prop of the moved node with session 2 + refNode2.setProperty(propertyName1, "test"); + testSession.save(); + // node has been automatically moved to new place + // -> check if the parent is correct. + assertTrue(testSession.getItem(destParentNode.getPath()).isSame(refNode.getParent())); + } catch (InvalidItemStateException e) { + // no automatic move of the externally moved node. ok. + log.debug(e.getMessage()); + } + } + + public void testRefreshMovedReferenceableNode() throws RepositoryException, NotExecutableException { + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + + superuser.move(refNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + superuser.save(); + + try { + refNode2.refresh(true); + Node parent = refNode2.getParent(); + if (parent.isSame(testSession.getItem(destParentNode.getPath()))) { + // node has been automatically moved to new place + assertItemStatus(refNode2, Status.EXISTING); + } else { + assertItemStatus(refNode2, Status.REMOVED); + } + } catch (InvalidItemStateException e) { + // no automatic move of the externally moved node. ok. + log.debug(e.getMessage()); + // since node had no pending changes -> status should be changed + // to REMOVED. + assertItemStatus(refNode2, Status.REMOVED); + } + } + + public void testConflictingAddMixin() throws RepositoryException, NotExecutableException { + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + refNode2.addMixin(mixLockable); + + superuser.move(refNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + superuser.save(); + + try { + refNode2.refresh(true); + Node parent = refNode2.getParent(); + if (parent.isSame(testSession.getItem(destParentNode.getPath()))) { + // node has been automatically moved to new place + assertItemStatus(refNode2, Status.EXISTING_MODIFIED); + } else if (!isItemStatus(refNode2, Status.EXISTING_MODIFIED)) { + // external removal was detected either by observation or be + // batch-reading the parent -> status must be stale. + assertItemStatus(refNode2, Status.STALE_DESTROYED); + } + } catch (InvalidItemStateException e) { + // no automatic move of the externally moved node. ok. + log.debug(e.getMessage()); + // since refNode2 has pending modifications its status should be + // changed to STALE_DESTROYED. + assertItemStatus(refNode2, Status.STALE_DESTROYED); + Node refAgain = testSession.getNodeByUUID(refNode.getUUID()); + assertTrue(refAgain.getParent().isSame(testSession.getItem(destParentNode.getPath()))); + assertFalse(refAgain.isNodeType(mixLockable)); + } + } + + public void testStaleDestroyed() throws RepositoryException, NotExecutableException { + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + refNode2.addMixin(mixLockable); + + superuser.move(refNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + superuser.save(); + + testSession.getItem(destParentNode.getPath() + "/" + nodeName2); + + assertItemStatus(refNode2, Status.STALE_DESTROYED); + try { + refNode2.refresh(false); + fail(); + } catch (InvalidItemStateException e) { + // correct behaviour + } + } + + public void testStaleDestroyed2() throws RepositoryException, NotExecutableException { + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + refNode2.addMixin(mixLockable); + + superuser.move(refNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + superuser.save(); + + testSession.getItem(destParentNode.getPath() + "/" + nodeName2); + + assertItemStatus(refNode2, Status.STALE_DESTROYED); + testSession.refresh(false); + assertItemStatus(refNode2, Status.REMOVED); + } + + public void testStaleDestroyed3() throws RepositoryException, NotExecutableException { + String uuid = refNode.getUUID(); + + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + assertTrue(refNode2.isSame(testSession.getNodeByUUID(uuid))); + // add some modification + refNode2.addMixin(mixLockable); + + String srcPath = refNode.getPath(); + String destPath = destParentNode.getPath() + "/" + nodeName2; + superuser.move(srcPath, destPath); + superuser.save(); + + testSession.getItem(destPath); + + assertItemStatus(refNode2, Status.STALE_DESTROYED); + // the uuid must be transferred to the 'moved' node + Node n = testSession.getNodeByUUID(uuid); + assertTrue(n.isSame(testSession.getItem(destPath))); + // assertSame(refNode2, testSession.getItem(srcPath)); + assertTrue(refNode2.isSame(testSession.getItem(srcPath))); + } + + public void testExternalRemoval() throws RepositoryException, NotExecutableException { + String uuid = refNode.getUUID(); + Node refNode2 = testSession.getNodeByUUID(uuid); + + String srcPath = refNode.getPath(); + String destPath = destParentNode.getPath() + "/" + nodeName2; + superuser.move(srcPath, destPath); + superuser.save(); + + try { + refNode2.refresh(true); + Node parent = refNode2.getParent(); + } catch (InvalidItemStateException e) { + } + + assertItemStatus(refNode2, Status.REMOVED); + // the uuid must be transferred to the 'moved' node + Node n = testSession.getNodeByUUID(uuid); + assertTrue(n.isSame(testSession.getItem(destPath))); + } + + public void testExternalRemoval2() throws RepositoryException, NotExecutableException { + Node childN = refNode.addNode(nodeName3); + Property p = childN.setProperty(propertyName1, "anyvalue"); + refNode.save(); + + String uuid = refNode.getUUID(); + Node refNode2 = testSession.getNodeByUUID(uuid); + Node c2 = (Node) testSession.getItem(childN.getPath()); + Property p2 = (Property) testSession.getItem(p.getPath()); + // transiently remove the property -> test effect of external removal. + p2.remove(); + + String srcPath = refNode.getPath(); + String destPath = destParentNode.getPath() + "/" + nodeName2; + superuser.move(srcPath, destPath); + superuser.save(); + + try { + refNode2.refresh(true); + Node parent = refNode2.getParent(); + } catch (InvalidItemStateException e) { + } + + assertItemStatus(refNode2, Status.REMOVED); + assertItemStatus(c2, Status.STALE_DESTROYED); + assertItemStatus(p2, Status.REMOVED); + } + + public void testExternalRemoval3() throws RepositoryException, NotExecutableException { + Node childN = refNode.addNode(nodeName3); + Property p = childN.setProperty(propertyName1, "anyvalue"); + refNode.save(); + + String uuid = refNode.getUUID(); + Node refNode2 = testSession.getNodeByUUID(uuid); + Node c2 = (Node) testSession.getItem(childN.getPath()); + Property p2 = (Property) testSession.getItem(p.getPath()); + // transiently modify -> test effect of external removal. + p2.setValue("changedValue"); + + String srcPath = refNode.getPath(); + String destPath = destParentNode.getPath() + "/" + nodeName2; + superuser.move(srcPath, destPath); + superuser.save(); + + try { + refNode2.refresh(true); + Node parent = refNode2.getParent(); + } catch (InvalidItemStateException e) { + } + + assertItemStatus(refNode2, Status.REMOVED); + assertItemStatus(c2, Status.REMOVED); + assertItemStatus(p2, Status.STALE_DESTROYED); + assertEquals("changedValue", p2.getString()); + } + + public void testNewItemsUponStaleDestroyed() throws RepositoryException, NotExecutableException { + String uuid = refNode.getUUID(); + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + refNode2.addMixin(mixLockable); + + Node childN = refNode2.addNode(nodeName3); + String childNPath = childN.getPath(); + + Property childP = refNode2.setProperty(propertyName2, "someValue"); + String childPPath = childP.getPath(); + + String destPath = destParentNode.getPath() + "/" + nodeName2; + superuser.move(refNode.getPath(), destPath); + superuser.save(); + + testSession.refresh(true); + testSession.getItem(destPath); + + assertItemStatus(refNode2, Status.STALE_DESTROYED); + assertItemStatus(refNode2.getProperty(jcrMixinTypes), Status.STALE_DESTROYED); + assertItemStatus(childN, Status.NEW); + assertItemStatus(childP, Status.NEW); + assertItemStatus(childN.getProperty(jcrPrimaryType), Status.NEW); + + assertTrue(testSession.itemExists(childNPath)); + assertTrue(childN.isSame(testSession.getItem(childNPath))); + + assertTrue(testSession.itemExists(childPPath)); + assertTrue(childP.isSame(testSession.getItem(childPPath))); + + testSession.refresh(false); + + assertItemStatus(childN, Status.REMOVED); + assertItemStatus(childP, Status.REMOVED); + assertFalse(testSession.itemExists(childNPath)); + assertFalse(testSession.itemExists(childPPath)); + } + + public void testChildItemsUponStaleDestroyed() throws RepositoryException, NotExecutableException { + Node cNode = refNode.addNode(nodeName3); + Node cNode2 = cNode.addNode(nodeName4); + refNode.save(); + + String uuid = refNode.getUUID(); + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + refNode2.addMixin(mixLockable); + + Node child = (Node) testSession.getItem(cNode.getPath()); + Node child2 = (Node) testSession.getItem(cNode2.getPath()); + Node child3 = child2.addNode(nodeName4); + String child3Path = child3.getPath(); + + String destPath = destParentNode.getPath() + "/" + nodeName2; + superuser.move(refNode.getPath(), destPath); + superuser.save(); + + testSession.refresh(true); + testSession.getItem(destPath); + + assertItemStatus(refNode2, Status.STALE_DESTROYED); + assertItemStatus(refNode2.getProperty(jcrMixinTypes), Status.STALE_DESTROYED); + assertItemStatus(child, Status.REMOVED); + assertItemStatus(child2, Status.STALE_DESTROYED); + assertItemStatus(child3, Status.NEW); + assertItemStatus(child3.getProperty(jcrPrimaryType), Status.NEW); + + testSession.refresh(false); + + assertItemStatus(child2, Status.REMOVED); + assertItemStatus(child3, Status.REMOVED); + } + + public void testUnmodifiedAncestorRemoved() throws RepositoryException, NotExecutableException { + String uuid = refNode.getUUID(); + Node n3 = refNode.addNode(nodeName3, testNodeType); + refNode.save(); + + Node refNode2 = (Node) testSession.getItem(refNode.getPath()); + // add transient modification to non-referenceable child node + Node node3 = (Node) testSession.getItem(n3.getPath()); + node3.addMixin(mixLockable); + + // add new child node and child property below + Node childN = node3.addNode(nodeName3); + String childNPath = childN.getPath(); + + Property childP = node3.setProperty(propertyName2, "someValue"); + String childPPath = childP.getPath(); + + // externally move the 'refNode' in order to provoke uuid-conflict + // in testSession -> refNode2 gets removed, since it doesn't have + // transient modifications. + String destPath = destParentNode.getPath() + "/" + nodeName2; + superuser.move(refNode.getPath(), destPath); + superuser.save(); + + testSession.refresh(true); + testSession.getItem(destPath); + + assertItemStatus(refNode2, Status.REMOVED); + assertItemStatus(node3, Status.STALE_DESTROYED); + assertItemStatus(childN, Status.NEW); + assertItemStatus(childP, Status.NEW); + + // since 'refNode2' is removed -> child items must not be accessible + // any more. + assertFalse(testSession.itemExists(childNPath)); + assertFalse(testSession.itemExists(childPPath)); + + // revert all pending changes... + testSession.refresh(false); + // must mark all modified/new items as removed. + assertItemStatus(node3, Status.REMOVED); + assertItemStatus(childN, Status.REMOVED); + assertItemStatus(childP, Status.REMOVED); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/GetItemsTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/GetItemsTest.java new file mode 100644 index 00000000000..755d8d64f47 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/GetItemsTest.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.jcr2spi; + +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.ItemInfoBuilder.NodeInfoBuilder; +import org.apache.jackrabbit.spi.commons.iterator.Predicate; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.Iterator; + +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.filterIterator; +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.iteratorChain; +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.singleton; + +/** + * Test cases for {@link RepositoryService#getItemInfos(SessionInfo, NodeId)}. Specifically + * for JCR-1797. + */ +public class GetItemsTest extends AbstractJCR2SPITest { + private Session session; + + @Override + public void setUp() throws Exception { + super.setUp(); + session = repository.login("default"); + } + + @Override + protected void initInfosStore(NodeInfoBuilder builder) throws RepositoryException { + // build up a hierarchy of items + builder + .createNodeInfo("node1") + .createNodeInfo("node11").build() + .createNodeInfo("node12").build() + .createNodeInfo("node13").build() + .createPropertyInfo("property11", "value11").build() + .createPropertyInfo("property12", "value12").build() + .build() + .createNodeInfo("node2") + .createNodeInfo("node21") + .createNodeInfo("node211") + .createNodeInfo("node2111") + .createNodeInfo("node21111") + .createNodeInfo("node211111") + .createNodeInfo("node2111111").build() + .build() + .build() + .build() + .build() + .build() + .build() + .createNodeInfo("node3").build() + .build(); + } + + @Override + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + session = null; + } + super.tearDown(); + } + + /** + * Check whether we can traverse the hierarchy when the item info for root is + * retrieved first. + * @throws RepositoryException + */ + public void testGetItemInfosRootFirst() throws RepositoryException { + assertTrue(session.getRootNode().getDepth() == 0); + checkHierarchy(); + } + + /** + * Check whether we can traverse the hierarchy when the item info for a deep item + * is retrieved first. + * @throws RepositoryException + */ + public void testGetItemInfosDeepFirst() throws RepositoryException { + final String targetPath = "/node2/node21/node211/node2111/node21111/node211111/node2111111"; + assertEquals(targetPath, session.getItem(targetPath).getPath()); + checkHierarchy(); + } + + private void checkHierarchy() throws PathNotFoundException, RepositoryException, ItemNotFoundException, + AccessDeniedException { + + for (Iterator itemInfos = itemInfoStore.getItemInfos(); itemInfos.hasNext();) { + ItemInfo itemInfo = itemInfos.next(); + String jcrPath = toJCRPath(itemInfo.getPath()); + Item item = session.getItem(jcrPath); + assertEquals(jcrPath, item.getPath()); + + if (item.getDepth() > 0) { + Node parent = item.getParent(); + if (item.isNode()) { + assertTrue(item.isSame(parent.getNode(item.getName()))); + } + else { + assertTrue(item.isSame(parent.getProperty(item.getName()))); + } + } + + } + } + + @Override + protected QNodeDefinition createRootNodeDefinition() { + fail("Not implemented"); + return null; + } + + @Override + public Iterator getChildInfos(SessionInfo sessionInfo, NodeId parentId) + throws RepositoryException { + + fail("Not implemented"); + return null; + } + + @Override + public Iterator getItemInfos(SessionInfo sessionInfo, final ItemId itemId) + throws RepositoryException { + + return iteratorChain( + singleton(itemInfoStore.getItemInfo(itemId)), + filterIterator(itemInfoStore.getItemInfos(), new Predicate() { + public boolean evaluate(ItemInfo info) { + return !itemId.equals(info.getId()); + } + })); + } + + @Override + public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return itemInfoStore.getNodeInfo(nodeId); + } + + @Override + public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId) + throws ItemNotFoundException { + + return itemInfoStore.getPropertyInfo(propertyId); + } + +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/GetPropertyTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/GetPropertyTest.java new file mode 100644 index 00000000000..af0e96341d1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/GetPropertyTest.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.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** GetPropertyTest... */ +public class GetPropertyTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(GetPropertyTest.class); + + private String node1Path; + private String prop1Path; + private String prop2Path; + + private Session readOnly; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + Node n = testRootNode.addNode(nodeName1, testNodeType); + node1Path = n.getPath(); + + Property p = n.setProperty(propertyName1, "string1"); + prop1Path = p.getPath(); + + p = n.setProperty(propertyName2, "string2"); + prop2Path = p.getPath(); + + testRootNode.save(); + + readOnly = getHelper().getReadOnlySession(); + } + + @Override + protected void tearDown() throws Exception { + if (readOnly != null) { + readOnly.logout(); + readOnly = null; + } + super.tearDown(); + } + + public void testItemExists() throws RepositoryException { + assertTrue(readOnly.itemExists(prop1Path)); + assertTrue(readOnly.itemExists(prop2Path)); + } + + public void testGetItem() throws RepositoryException { + assertFalse(readOnly.getItem(prop1Path).isNode()); + assertFalse(readOnly.getItem(prop2Path).isNode()); + } + + public void testHasProperty() throws RepositoryException { + String testPath = testRootNode.getPath(); + Node trn = (Node) readOnly.getItem(testPath); + + assertTrue(trn.hasProperty(prop1Path.substring(testPath.length() + 1))); + assertTrue(trn.hasProperty(prop2Path.substring(testPath.length() + 1))); + } + + public void testGetProperty() throws RepositoryException { + String testPath = testRootNode.getPath(); + Node trn = (Node) readOnly.getItem(testPath); + + trn.getProperty(prop1Path.substring(testPath.length() + 1)); + trn.getProperty(prop2Path.substring(testPath.length() + 1)); + } + + public void testGetDeepProperty() throws RepositoryException { + Node n2 = testRootNode.getNode(nodeName1).addNode(nodeName2); + testRootNode.save(); + + // other session directly accesses n2. + // consequently n1 is not yet resolved + Node node2 = (Node) readOnly.getItem(n2.getPath()); + + // now try to access properties below n1 -> should be existing although + // n1 has not yet been resolved. + assertTrue(readOnly.itemExists(prop1Path)); + Property p1 = (Property) readOnly.getItem(prop1Path); + assertTrue(p1.isSame(node2.getProperty("../" + Text.getName(prop1Path)))); + + PropertyIterator it = node2.getParent().getProperties(); + assertTrue(it.getSize() >= 3); + } + + public void testGetExternallyAddedItems() throws RepositoryException { + Node node1 = (Node) readOnly.getItem(node1Path); + + Node n2 = testRootNode.getNode(nodeName1).addNode(nodeName2); + Property p3 = n2.setProperty(propertyName1, "test"); + testRootNode.save(); + + node1.refresh(true); + + assertTrue(readOnly.itemExists(n2.getPath())); + assertTrue(readOnly.itemExists(p3.getPath())); + } + + public void testGetExternallyChangedNode() throws RepositoryException { + // Access node1 through session 1 + Node node1 = (Node) readOnly.getItem(node1Path); + + // Add node and property through session 2 + Node n2 = testRootNode.getNode(nodeName1).addNode(nodeName2); + Property p3 = n2.setProperty(propertyName1, "test"); + testRootNode.save(); + + // Assert added nodes are visible in session 1 after refresh + node1.refresh(false); + assertTrue(readOnly.itemExists(n2.getPath())); + assertTrue(readOnly.itemExists(p3.getPath())); + + Item m2 = readOnly.getItem(n2.getPath()); + assertTrue(m2.isNode()); + assertTrue(((Node) m2).hasProperty(propertyName1)); + + // Remove property through session 2 + p3.remove(); + testRootNode.save(); + + // Assert removal is visible through session 1 + node1.refresh(false); + assertFalse(((Node) m2).hasProperty(propertyName1)); + } + + public void testGetExternallyChangedProperty() throws RepositoryException { + // Access node1 through session 1 + Node node1 = (Node) readOnly.getItem(node1Path); + + // Add node and property through session 2 + Node n2 = testRootNode.getNode(nodeName1).addNode(nodeName2); + Property p3 = n2.setProperty(propertyName1, "test"); + p3.setValue("v3"); + testRootNode.save(); + + // Assert added nodes are visible in session 1 after refresh + node1.refresh(false); + assertTrue(readOnly.itemExists(n2.getPath())); + assertTrue(readOnly.itemExists(p3.getPath())); + + Item q3 = readOnly.getItem(p3.getPath()); + assertFalse(q3.isNode()); + assertTrue("v3".equals(((Property) q3).getString())); + + // Change property value through session 2 + p3.setValue("v3_modified"); + testRootNode.save(); + + // Assert modification is visible through session 1 + node1.refresh(false); + assertTrue("v3_modified".equals(((Property) q3).getString())); + } + + public void testGetDeepSNSProperties() throws RepositoryException, NotExecutableException { + Node n = testRootNode.getNode(nodeName1); + if (!n.getDefinition().allowsSameNameSiblings()) { + throw new NotExecutableException(); + } + Node sib2 = testRootNode.addNode(nodeName1); + Property p2 = sib2.setProperty(propertyName1, "sib2-prop"); + + Node sib3 = testRootNode.addNode(nodeName1); + Property p3 = sib3.setProperty(propertyName1, "sib3-prop"); + testRootNode.save(); + + Session s = getHelper().getReadWriteSession(); + try { + Node sibNode = (Node) s.getItem(sib2.getPath()); + sibNode.remove(); + + // after transient removal of the sibling2, sibling3 must match its + // path -> the property must have the proper value. + Property pp3 = (Property) s.getItem(sib2.getPath() + "/" + propertyName1); + assertEquals("sib3-prop", pp3.getString()); + + // the tree starting with node[3] must not be accessible any more. + assertFalse(s.itemExists(p3.getPath())); + assertFalse(s.itemExists(sib3.getPath())); + } finally { + s.logout(); + } + } + + public void testGetDeepRefNodeProperties() throws RepositoryException, NotExecutableException { + Node n = testRootNode.getNode(nodeName1); + n.addMixin(mixReferenceable); + Node n2 = n.addNode(nodeName2); + Property p3 = n2.setProperty(propertyName1, "test"); + testRootNode.save(); + + // other session directly accesses p3. + // consequently n1 is not yet resolved + Property prop3 = (Property) readOnly.getItem(p3.getPath()); + + // now try to access properties below n1 -> should be existing although + // n1 has not yet been resolved. + assertTrue(readOnly.itemExists(prop2Path)); + Property p1 = (Property) readOnly.getItem(prop2Path); + + Node node1 = readOnly.getNodeByUUID(n.getUUID()); + assertTrue(p1.isSame(node1.getProperty(Text.getName(prop2Path)))); + } + + public void testGetPropertyOfRemovedAncestor() throws RepositoryException { + Session rw = getHelper().getReadWriteSession(); + try { + // add modification to a property. + Property p = (Property) rw.getItem(prop1Path); + p.setValue("changedValue"); + + // transiently remove the test root node + rw.getItem(testRootNode.getPath()).remove(); + + try { + p.getValue(); + fail("modified property must be marked removed upon parent removal"); + } catch (InvalidItemStateException e) { + // success + } + try { + rw.getItem(prop1Path); + fail("modified property must be marked removed upon parent removal"); + } catch (PathNotFoundException e) { + // success + } + try { + Property p2 = (Property) rw.getItem(prop2Path); + fail("existing property must be marked removed upon parent removal"); + } catch (PathNotFoundException e) { + // success + } + + // revert all transient modifications + rw.refresh(false); + Property pAgain = (Property) rw.getItem(prop1Path); + + // TODO: for generic jsr 170 test: change assert to p.isSame(pAgain) + assertTrue(p.isSame(pAgain)); + assertEquals("string1", p.getString()); + } finally { + rw.logout(); + } + } + + public void testGetDeepEmptyStringProperty() throws RepositoryException, NotExecutableException { + Node n = testRootNode.getNode(nodeName1); + Node n2 = n.addNode(nodeName2); + Node n3 = n2.addNode(nodeName3); + Node n4 = n3.addNode(nodeName4); + Property emptyProp = n4.setProperty(propertyName1, ""); + testRootNode.save(); + + Property p = readOnly.getProperty(emptyProp.getPath()); + assertEquals("", p.getString()); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/HierarchyNodeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/HierarchyNodeTest.java new file mode 100644 index 00000000000..3638162e357 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/HierarchyNodeTest.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.jcr2spi; + +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * HierarchyNodeTest... + */ +public class HierarchyNodeTest extends AbstractJCRTest { + + private final Set hierarchyNodeProps = new HashSet(); + private final Set resourceProps = new HashSet(); + + private String ntFolder; + private String ntFile; + private String ntResource; + + private Node fileNode; + + @Override + protected void setUp() throws Exception { + super.setUp(); + Session s = testRootNode.getSession(); + String jcrPrefix = s.getNamespacePrefix(NS_JCR_URI); + String ntPrefix = s.getNamespacePrefix(NS_NT_URI); + + ntFolder = ntPrefix + ":folder"; + ntFile = ntPrefix + ":file"; + ntResource = ntPrefix + ":resource"; + + hierarchyNodeProps.add(jcrPrefix+":primaryType"); + hierarchyNodeProps.add(jcrPrefix+":created"); + hierarchyNodeProps.add(jcrPrefix+":createdBy"); + + resourceProps.add(jcrPrefix+":primaryType"); + resourceProps.add(jcrPrefix+":lastModified"); + resourceProps.add(jcrPrefix+":lastModifiedBy"); + resourceProps.add(jcrPrefix+":mimeType"); + resourceProps.add(jcrPrefix+":data"); + resourceProps.add(jcrPrefix+":uuid"); + + try { + Node folder = testRootNode.addNode("folder", ntFolder); + fileNode = folder.addNode("file", ntFile); + + Node content = fileNode.addNode(jcrPrefix + ":content", ntResource); + content.setProperty(jcrPrefix + ":mimeType", "text/plain"); + content.setProperty(jcrPrefix + ":data", "some plain text"); + + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot create hierarchy nodes."); + } + } + + @Override + protected void tearDown() throws Exception { + fileNode = null; + super.tearDown(); + } + + public void testGetProperties() throws RepositoryException { + Session readSession = getHelper().getReadOnlySession(); + try { + dump((Node) readSession.getItem(fileNode.getPath())); + } finally { + readSession.logout(); + } + } + + /** Recursively outputs the contents of the given node. */ + private void dump(Node node) throws RepositoryException { + + // Then output the properties + PropertyIterator properties = node.getProperties(); + Set set = new HashSet(); + while (properties.hasNext()) { + Property property = properties.nextProperty(); + set.add(property.getName()); + } + + if (node.getPrimaryNodeType().getName().equals(ntFolder)) { + assertTrue(hierarchyNodeProps.size() == set.size() && hierarchyNodeProps.containsAll(set)); + } else if (node.getPrimaryNodeType().getName().equals(ntFile)) { + assertTrue(hierarchyNodeProps.size() == set.size() && hierarchyNodeProps.containsAll(set)); + } else if (node.getPrimaryNodeType().getName().equals(ntResource)) { + assertTrue(resourceProps.size() == set.size() && resourceProps.containsAll(set)); + } + + // Finally output all the child nodes recursively + NodeIterator nodes = node.getNodes(); + while (nodes.hasNext()) { + dump(nodes.nextNode()); + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/IsSameTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/IsSameTest.java new file mode 100644 index 00000000000..ab370236126 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/IsSameTest.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.jcr2spi; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.Calendar; + +/** IsSameTest... */ +public class IsSameTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(IsSameTest.class); + + public void testIsSameProperty() throws RepositoryException { + + Node n = testRootNode.addNode(nodeName1, testNodeType); + Property p = n.setProperty(propertyName1, "anyvalue"); + testRootNode.save(); + + // access same property through different session + Session otherSession = getHelper().getReadOnlySession(); + try { + Property otherProperty = (Property) otherSession.getItem(p.getPath()); + assertTrue(p.isSame(otherProperty)); + } finally { + otherSession.logout(); + } + } + + public void testIsSameProperty2() throws RepositoryException { + + Node n = testRootNode.addNode(nodeName1, testNodeType); + Property p = n.setProperty(propertyName1, "anyvalue"); + testRootNode.save(); + + // add transient modification to the property: + p.setValue("someOtherValue"); + + // access same property through different session + Session otherSession = getHelper().getReadOnlySession(); + try { + Property otherProperty = (Property) otherSession.getItem(p.getPath()); + assertTrue(p.isSame(otherProperty)); + } finally { + otherSession.logout(); + } + } + + public void testIsSameProperty3() throws RepositoryException { + // create a node (nt:resource) that implicitly is referenceable + Node n = testRootNode.addNode("aFile", "nt:file"); + n = n.addNode("jcr:content", "nt:resource"); + n.setProperty("jcr:lastModified", Calendar.getInstance()); + n.setProperty("jcr:mimeType", "text/plain"); + Property jcrData = n.setProperty("jcr:data", "abc", PropertyType.BINARY); + testRootNode.save(); + + // access same property through different session + Session otherSession = getHelper().getReadOnlySession(); + try { + Property otherProperty = (Property) otherSession.getItem(jcrData.getPath()); + assertTrue(jcrData.isSame(otherProperty)); + } finally { + otherSession.logout(); + } + } + + public void testIsSameProperty4() throws RepositoryException { + // create a node (nt:resource) that implicitly is referenceable + Node n = testRootNode.addNode("aFile", "nt:file"); + n = n.addNode("jcr:content", "nt:resource"); + n.setProperty("jcr:lastModified", Calendar.getInstance()); + n.setProperty("jcr:mimeType", "text/plain"); + Property jcrData = n.setProperty("jcr:data", "abc", PropertyType.BINARY); + testRootNode.save(); + + // access same property through different session + Session otherSession = getHelper().getReadOnlySession(); + try { + Property otherProperty = (Property) otherSession.getItem(jcrData.getPath()); + assertTrue(n.getProperty("jcr:data").isSame(otherProperty)); + } finally { + otherSession.logout(); + } + } + + public void testIsSameNode() throws RepositoryException { + // create a node (nt:resource) that implicitly is referenceable + Node n = testRootNode.addNode("aFile", "nt:file"); + n = n.addNode("jcr:content", "nt:resource"); + n.setProperty("jcr:lastModified", Calendar.getInstance()); + n.setProperty("jcr:mimeType", "text/plain"); + n.setProperty("jcr:data", "abc", PropertyType.BINARY); + testRootNode.save(); + + // access nt:resource node through different session + Session otherSession = getHelper().getReadOnlySession(); + try { + Node otherNode = (Node) otherSession.getItem(n.getPath()); + assertTrue(n.isSame(otherNode)); + } finally { + otherSession.logout(); + } + } + + public void testIsSameNode2() throws RepositoryException { + // create a node (nt:resource) that implicitly is referenceable + Node n = testRootNode.addNode("aFile", "nt:file"); + n = n.addNode("jcr:content", "nt:resource"); + n.setProperty("jcr:lastModified", Calendar.getInstance()); + n.setProperty("jcr:mimeType", "text/plain"); + n.setProperty("jcr:data", "abc", PropertyType.BINARY); + testRootNode.save(); + + // access nt:resource node through different session + Session otherSession = getHelper().getReadOnlySession(); + try { + Node otherNode = (Node) otherSession.getItem(n.getPath()); + assertTrue(otherNode.isSame(n)); + } finally { + otherSession.logout(); + } + } + + public void testIsSameNode3() throws RepositoryException { + + Node n = testRootNode.addNode(nodeName1, testNodeType); + Property p = n.setProperty(propertyName1, "anyvalue"); + testRootNode.save(); + + String srcPath = n.getPath(); + String destPath = testRootNode.getPath() + "/" + nodeName2; + + // transiently move the node. + testRootNode.getSession().move(srcPath, destPath); + assertTrue(n.isSame(superuser.getItem(destPath))); + } + + public void testIsSameNode4() throws RepositoryException { + + Node n = testRootNode.addNode(nodeName1, testNodeType); + Property p = n.setProperty(propertyName1, "anyvalue"); + testRootNode.save(); + + // transiently move the node. + String srcPath = n.getPath(); + String destPath = testRootNode.getPath() + "/" + nodeName2; + testRootNode.getSession().move(srcPath, destPath); + + Session otherSession = getHelper().getReadOnlySession(); + try { + Node otherNode = (Node) otherSession.getItem(srcPath); + assertTrue(n.isSame(otherNode)); + assertTrue(superuser.getItem(destPath).isSame(otherNode)); + } finally { + otherSession.logout(); + } + } + + public void testIsSameNode5() throws RepositoryException { + + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // transiently move the node. + String srcPath = n.getPath(); + String destPath = testRootNode.getPath() + "/" + nodeName2; + + Session otherSession = getHelper().getReadOnlySession(); + try { + Node otherNode = (Node) otherSession.getItem(srcPath); + + testRootNode.getSession().getWorkspace().move(srcPath, destPath); + + assertTrue(otherNode.isSame(n)); + assertTrue(n.isSame(otherNode)); + } finally { + otherSession.logout(); + } + } + + public void testIsSameNode6() throws RepositoryException { + + Node n = testRootNode.addNode(nodeName1, testNodeType); + n.addMixin(mixReferenceable); + testRootNode.save(); + + // transiently move the node. + String srcPath = n.getPath(); + String destPath = testRootNode.getPath() + "/" + nodeName2; + + Session otherSession = getHelper().getReadOnlySession(); + try { + Node otherNode = (Node) otherSession.getItem(srcPath); + + testRootNode.getSession().getWorkspace().move(srcPath, destPath); + + otherNode.refresh(false); + try { + assertTrue(n.isSame(otherNode)); + } catch (InvalidItemStateException e) { + // ok as well. + } + try { + assertTrue(otherNode.isSame(n)); + } catch (InvalidItemStateException e) { + // ok as well. + } + } finally { + otherSession.logout(); + } + } + + public void testIsSameNode7() throws RepositoryException { + + Node n = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = n.addNode(nodeName2); + Node n3 = n2.addNode(nodeName3); + testRootNode.save(); + + n.addMixin(mixReferenceable); + testRootNode.save(); + + Session otherSession = getHelper().getReadOnlySession(); + try { + Node otherNode3 = (Node) otherSession.getItem(n3.getPath()); + + assertTrue(otherNode3.isSame(n3)); + Node parent = otherNode3.getParent(); + assertTrue(parent.isSame(n2)); + parent = parent.getParent(); + assertTrue(parent.isSame(n)); + parent = parent.getParent(); + assertTrue(testRootNode.isSame(parent)); + } finally { + otherSession.logout(); + } + } + + public void testSameInstanceIsSame() throws RepositoryException { + assertTrue(testRootNode.isSame(testRootNode)); + + Property p = testRootNode.getProperty(jcrPrimaryType); + assertTrue(p.isSame(p)); + } + + public void testNewNodeIsSame() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + assertTrue(n.isSame(n)); + + n.remove(); + Node n2 = testRootNode.addNode(nodeName1, testNodeType); + + try { + assertFalse(n2.isSame(n)); + } catch (InvalidItemStateException e) { + // ok as well + } + try { + assertFalse(n.isSame(n2)); + } catch (InvalidItemStateException e) { + // ok as well + } + } + + public void testNewPropertyIsSame() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + Property p = n.setProperty(propertyName1, "anyValue"); + + assertTrue(p.isSame(p)); + } + + public void testNewItemFromDifferentSessions() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + Property p = n.setProperty(propertyName1, "anyValue"); + + Session s2 = getHelper().getReadWriteSession(); + try { + Node trn = (Node) s2.getItem(testRootNode.getPath()); + Node n2 = trn.addNode(nodeName1, testNodeType); + Property p2 = n2.setProperty(propertyName1, "anyValue"); + + assertFalse(n.isSame(n2)); + assertFalse(n2.isSame(n)); + assertFalse(p.isSame(p2)); + assertFalse(p2.isSame(p)); + } finally { + s2.logout(); + } + } + + public void testDifferentItemType() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + Property p = n.setProperty(propertyName1, "anyValue"); + + assertFalse(p.isSame(n)); + assertFalse(n.isSame(p)); + } + + public void testShadowingItems() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + + Property p = n.setProperty(propertyName1, "anyValue"); + testRootNode.save(); + + testRootNode.getSession().move(n.getPath(), n2.getPath() + "/destination"); + + Node replaceNode = testRootNode.addNode(nodeName1, testNodeType); + Property replaceProp = replaceNode.setProperty(propertyName1, "anyValue"); + + assertFalse(replaceNode.isSame(n)); + assertFalse(replaceProp.isSame(p)); + } + + public void testShadowingItems2() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + Node n2 = testRootNode.addNode(nodeName2, testNodeType); + Property p = n.setProperty(propertyName1, "anyValue"); + + testRootNode.getSession().move(n.getPath(), n2.getPath() + "/destination"); + + Node replaceNode = testRootNode.addNode(nodeName1, testNodeType); + Property replaceProp = replaceNode.setProperty(propertyName1, "anyValue"); + + assertFalse(replaceNode.isSame(n)); + assertFalse(replaceProp.isSame(p)); + } + + public void testShadowingItems3() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + Property p = n.setProperty(propertyName1, "anyValue"); + testRootNode.save(); + + p.remove(); + Property p2 = n.setProperty(propertyName1, "anyValue"); + try { + assertFalse(p2.isSame(p)); + } catch (InvalidItemStateException e) { + // ok as well. + } + } + + /** + * 283 specific test where node and prop with same name can be siblings. + * + * @throws RepositoryException + */ + /* + public void testIsSameDifferentItemType() throws RepositoryException { + Node n = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.save(); + + Session s2 = helper.getReadWriteSession(); + try { + Node trn = (Node) s2.getItem(testRootNode.getPath()); + Property p = trn.setProperty(nodeName1, "anyValue"); + trn.save(); + + assertFalse(n.isSame(p)); + + } finally { + s2.logout(); + } + } + */ + + +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ItemInfoStore.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ItemInfoStore.java new file mode 100644 index 00000000000..c4367d7c2ac --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ItemInfoStore.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; + +import javax.jcr.ItemNotFoundException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * In memory store for {@link ItemInfo}s. + */ +public class ItemInfoStore { + private final Mapinfos = new HashMap(); + private final Map> batches = new HashMap>(); + private final Map> childInfos = new HashMap>(); + + /** + * Retrieve an item by its id. + * @param id + * @return + * @throws ItemNotFoundException if no such item exists + */ + public ItemInfo getItemInfo(ItemId id) throws ItemNotFoundException { + ItemInfo itemInfo = infos.get(id); + + return itemInfo == null + ? ItemInfoStore.notFound(id) + : itemInfo; + } + + /** + * Retrieve an iterator over all items + * @return + */ + public Iterator getItemInfos() { + return infos.values().iterator(); + } + + /** + * Retrieve a node by its id. + * + * @param id + * @return + * @throws ItemNotFoundException if no such node exists + */ + public NodeInfo getNodeInfo(NodeId id) throws ItemNotFoundException { + ItemInfo itemInfo = getItemInfo(id); + + return itemInfo.denotesNode() + ? (NodeInfo) itemInfo + : ItemInfoStore.notFound(id); + } + + /** + * Retrieve a property by its id. + * + * @param id + * @return + * @throws ItemNotFoundException if no such property exists + */ + public PropertyInfo getPropertyInfo(PropertyId id) throws ItemNotFoundException { + ItemInfo itemInfo = getItemInfo(id); + + return itemInfo.denotesNode() + ? ItemInfoStore.notFound(id) + : (PropertyInfo) itemInfo; + } + + /** + * Retrieve all items of a batch + * @see RepositoryService#getItemInfos(org.apache.jackrabbit.spi.SessionInfo, NodeId) + * + * @param id + * @return + */ + public Iterator getBatch(ItemId id) { + Iterable batch = batches.get(id); + + return batch == null + ? Iterators.empty() + : batch.iterator(); + } + + /** + * Retrieve the {@link ChildInfo}s of a node + * + * @param id + * @return + * @throws ItemNotFoundException if no such node exists + */ + public Iterator getChildInfos(NodeId id) throws ItemNotFoundException { + Iterable childs = childInfos.get(id); + + return childs == null + ? ItemInfoStore.>notFound(id) + : childs.iterator(); + } + + /** + * Add an {@link ItemInfo} + * + * @param info + */ + public void addItemInfo(ItemInfo info) { + infos.put(info.getId(), info); + } + + /** + * Add a {@link ItemInfo} to a batch + * + * @param id + * @param info + */ + public void updateBatch(ItemId id, ItemInfo info) { + if (!batches.containsKey(id)) { + batches.put(id, new ArrayList()); + } + + batches.get(id).add(info); + } + + /** + * Add a {@link ChildInfo} to a node + * @param id + * @param info + */ + public void updateChilds(ItemId id, ChildInfo info) { + if (!childInfos.containsKey(id)) { + childInfos.put(id, new ArrayList()); + } + + childInfos.get(id).add(info); + } + + /** + * Set the {@link ChildInfo}s of a node + * + * @param id + * @param infos + */ + public void setChildInfos(NodeId id, Iterator infos) { + childInfos.put(id, toList(infos)); + } + + // -----------------------------------------------------< private >--- + + private static T notFound(ItemId itemId) throws ItemNotFoundException { + throw new ItemNotFoundException(itemId.toString()); + } + + private static List toList(Iterator infos) { + List list = new ArrayList(); + + while (infos.hasNext()) { + list.add(infos.next()); + } + return list; + } + +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/Jcr2SpiTestSuite.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/Jcr2SpiTestSuite.java new file mode 100644 index 00000000000..53332841113 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/Jcr2SpiTestSuite.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.jcr2spi; + +import junit.framework.TestSuite; + +/** + * Jcr2SpiTestSuite... + */ +public class Jcr2SpiTestSuite extends TestSuite { + + public Jcr2SpiTestSuite() { + super("JCR2SPI tests"); + + // all jcr2spi tests + addTest(org.apache.jackrabbit.jcr2spi.TestAll.suite()); + addTest(org.apache.jackrabbit.jcr2spi.lock.TestAll.suite()); + addTest(org.apache.jackrabbit.jcr2spi.name.TestAll.suite()); + addTest(org.apache.jackrabbit.jcr2spi.nodetype.TestAll.suite()); + addTest(org.apache.jackrabbit.jcr2spi.observation.TestAll.suite()); + addTest(org.apache.jackrabbit.jcr2spi.query.TestAll.suite()); + addTest(org.apache.jackrabbit.jcr2spi.version.TestAll.suite()); + addTest(org.apache.jackrabbit.jcr2spi.xml.TestAll.suite()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/LazyItemIteratorTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/LazyItemIteratorTest.java new file mode 100644 index 00000000000..ad59ad514b4 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/LazyItemIteratorTest.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.jcr2spi; + +import javax.jcr.Node; +import javax.jcr.RangeIterator; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * LazyItemIteratorTest contains implementation specific test + * cases, that check if the LazyItemIterator returns a better + * estimate for the number of Items to be available in the + * iteration than -1. + */ +public class LazyItemIteratorTest extends AbstractJCRTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + public void testSizeGreaterMinusOne() throws RepositoryException { + RangeIterator it = testRootNode.getProperties(); + // a node always has at least a single property + assertTrue(it.getSize() > 0); + + if (testRootNode.hasNodes()) { + it = testRootNode.getNodes(); + // a node always has at least a single property + assertTrue(it.getSize() > 0); + } + } + + public void testSizeOfEmptyIteratorIsZero() throws RepositoryException { + int i = 0; + String nameHint = "noExisting"; + String name = nameHint; + while (testRootNode.hasProperty(name)) { + name = name + i; + i++; + } + // retrieve PropertyIterator for a name that does not exist as Property + RangeIterator it = testRootNode.getProperties(name); + assertTrue(it.getSize() == 0); + + name = nameHint; + while (testRootNode.hasNode(name)) { + name = name + i; + i++; + } + // retrieve NodeIterator for a name that does not exist as Node + it = testRootNode.getNodes(name); + assertTrue(it.getSize() == 0); + } + + public void testSizeShrinksIfInvalidItemFound() throws NotExecutableException, RepositoryException { + RangeIterator it; + try { + testRootNode.addNode(nodeName1, testNodeType); + testRootNode.addNode(nodeName2, testNodeType); + Node child = testRootNode.addNode(nodeName3, testNodeType); + testRootNode.save(); + + it = testRootNode.getNodes(); + // remove 1 child -> force the iterator to contain an entry that + // cannot be resolved into a node. + child.remove(); + + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + // now the original size is off by one and will be adjusted automatically + long size = it.getSize(); + long zise = 0; + while (it.hasNext()) { + it.next(); + zise++; + } + // original size is bigger by 1 than the calculated size during the + // iteration. + assertTrue(size == zise+1); + // retrieve size again and check if it has been been adjusted. + assertTrue(it.getSize() == zise); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/LoginTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/LoginTest.java new file mode 100644 index 00000000000..05cda2d16bb --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/LoginTest.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.jcr2spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.Session; +import javax.jcr.RepositoryException; + +/** LoginTest... */ +public class LoginTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(LoginTest.class); + + public void testNullLogin() throws RepositoryException { + Session s = getHelper().getRepository().login(); + try { + assertNotNull(s.getWorkspace().getName()); + } finally { + s.logout(); + } + } + + public void testNullWorkspaceLogin() throws RepositoryException { + Session s = getHelper().getRepository().login((String) null); + try { + assertNotNull(s.getWorkspace().getName()); + } finally { + s.logout(); + } + } + + public void testNullCredentialsNullWorkspaceLogin() throws RepositoryException { + Session s = getHelper().getRepository().login(null, null); + try { + assertNotNull(s.getWorkspace().getName()); + } finally { + s.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MixinModificationTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MixinModificationTest.java new file mode 100644 index 00000000000..bddb807b32b --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MixinModificationTest.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.jcr2spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.jcr2spi.state.Status; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Session; +import javax.jcr.ItemNotFoundException; + +/** MixinModificationTest... */ +public class MixinModificationTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(MixinModificationTest.class); + + private static void assertItemStatus(Item item, int status) throws NotExecutableException { + if (!(item instanceof ItemImpl)) { + throw new NotExecutableException("org.apache.jackrabbit.jcr2spi.ItemImpl expected"); + } + int st = ((ItemImpl) item).getItemState().getStatus(); + assertEquals("Expected status to be " + Status.getName(status) + ", was " + Status.getName(st), status, st); + } + + public void testAddMixin() throws RepositoryException, + NotExecutableException { + Node n = testRootNode.addNode(nodeName1); + testRootNode.save(); + + if (n.isNodeType(mixVersionable)) { + throw new NotExecutableException(); + } + try { + n.addMixin(mixVersionable); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + assertItemStatus(n, Status.EXISTING_MODIFIED); + assertTrue(n.hasProperty(jcrMixinTypes)); + Property p = n.getProperty(jcrMixinTypes); + + n.save(); + + // after saving the affected target node must be marked 'invalidated'. + // the property however should be set existing. + assertItemStatus(n, Status.INVALIDATED); + assertItemStatus(p, Status.EXISTING); + } + + public void testAddMixin2() throws RepositoryException, + NotExecutableException { + Node n; + try { + n = testRootNode.addNode(nodeName1); + n.addMixin(mixVersionable); + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + // after saving the affected target node must be marked 'invalidated' + // even if adding the node and setting a mixin was achieved in the + // same batch. + assertItemStatus(n, Status.INVALIDATED); + } + + public void testRemoveMixin() throws RepositoryException, NotExecutableException { + String nPath; + try { + Node n = testRootNode.addNode(nodeName1); + nPath = n.getPath(); + n.addMixin(mixReferenceable); + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + Session testSession = getHelper().getReadWriteSession(); + try { + Node n = (Node) testSession.getItem(nPath); + String uuid = n.getUUID(); + + // remove the mixin again. + n.removeMixin(mixReferenceable); + assertFalse(n.hasProperty(jcrMixinTypes)); + n.save(); + + // accessing node by uuid should not be possible any more. + try { + Node n2 = testSession.getNodeByUUID(uuid); + fail(); + } catch (ItemNotFoundException e) { + // ok + } + + // however: the added node should still be valid. but not referenceable + assertItemStatus(n, Status.EXISTING); + assertFalse(n.isNodeType(mixReferenceable)); + assertTrue(testSession.itemExists(nPath)); + + try { + Node n2 = superuser.getNodeByUUID(uuid); + fail(); + } catch (ItemNotFoundException e) { + // ok + } + } finally { + if (testSession != null) { + testSession.logout(); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveCombinedTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveCombinedTest.java new file mode 100644 index 00000000000..b51281f7841 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveCombinedTest.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.jcr2spi; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** MoveCombinedTest... */ +public class MoveCombinedTest extends AbstractMoveTest { + + private static Logger log = LoggerFactory.getLogger(MoveCombinedTest.class); + + private Session testSession; + + @Override + protected void setUp() throws Exception { + super.setUp(); + testSession = getHelper().getReadOnlySession(); + } + + @Override + protected void tearDown() throws Exception { + if (testSession != null) { + testSession.logout(); + testSession = null; + } + super.tearDown(); + } + + @Override + protected boolean isSessionMove() { + return true; + } + + public void testMoveAndAddNode() throws RepositoryException { + doMove(moveNode.getPath(), destinationPath); + Node n = moveNode.addNode(nodeName3); + superuser.save(); + + assertTrue(testSession.itemExists(n.getPath())); + } + + public void testMoveAndAddProperty() throws RepositoryException { + doMove(moveNode.getPath(), destinationPath); + Property p = moveNode.setProperty(propertyName1, "someValue"); + superuser.save(); + + assertTrue(testSession.itemExists(p.getPath())); + } + + public void testMoveAndSetPropertyValue() throws RepositoryException { + Property p = moveNode.setProperty(propertyName1, "someValue"); + moveNode.save(); + + doMove(moveNode.getPath(), destinationPath); + p = moveNode.setProperty(propertyName1, "changedValue"); + superuser.save(); + + assertTrue(testSession.itemExists(p.getPath())); + } + + public void testMoveAndRemove() throws RepositoryException { + Node n = moveNode.addNode(nodeName3); + String nPath = n.getPath(); + superuser.save(); + + doMove(moveNode.getPath(), destinationPath); + n.remove(); + superuser.save(); + + assertFalse(testSession.itemExists(nPath)); + assertFalse(testSession.itemExists(destinationPath + "/" + nodeName3)); + } + + public void testMoveAndSetMixin() throws RepositoryException { + + doMove(moveNode.getPath(), destinationPath); + moveNode.addMixin(mixVersionable); + superuser.save(); + + Node n = (Node) testSession.getItem(destinationPath); + assertTrue(n.isNodeType(mixVersionable)); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveMultipleTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveMultipleTest.java new file mode 100644 index 00000000000..ddeaf22f21d --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveMultipleTest.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MoveMultipleTest... + */ +public class MoveMultipleTest extends AbstractMoveTest { + + private static Logger log = LoggerFactory.getLogger(MoveMultipleTest.class); + + private String originalPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + originalPath = moveNode.getPath(); + } + + @Override + protected boolean isSessionMove() { + return true; + } + + /** + * Transiently move a persisted node multiple times and check results + * after each move as well as after saving. + */ + public void testMultipleMove() throws RepositoryException { + // 1. move + doMove(moveNode.getPath(), destinationPath); + + // 2. move + String destPath2 = srcParentNode.getPath()+"/"+nodeName1; + doMove(destinationPath, destPath2); + assertTrue(moveNode.getParent().isSame(srcParentNode)); + assertEquals(moveNode.getName(), Text.getName(destPath2)); + assertEquals(moveNode.getPath(), destPath2); + assertFalse(destParentNode.hasNode(Text.getName(destinationPath))); + + // 3. move + String destPath3 = destParentNode.getPath()+"/"+nodeName4; + doMove(destPath2, destPath3); + assertTrue(moveNode.getParent().isSame(destParentNode)); + assertEquals(moveNode.getName(), Text.getName(destPath3)); + assertEquals(moveNode.getPath(), destPath3); + assertFalse(srcParentNode.hasNode(Text.getName(destPath2))); + + testRootNode.save(); + + assertTrue(moveNode.getParent().isSame(destParentNode)); + assertEquals(moveNode.getName(), Text.getName(destPath3)); + assertEquals(moveNode.getPath(), destPath3); + assertFalse(srcParentNode.hasNode(Text.getName(destPath2))); + } + + /** + * Test revert of persisted node after multiple transient moves + */ + public void testRevertingMultipleMove() throws RepositoryException { + doMove(moveNode.getPath(), destinationPath); + String destPath2 = srcParentNode.getPath()+"/"+nodeName1; + doMove(destinationPath, destPath2); + String destPath3 = destParentNode.getPath()+"/"+nodeName4; + doMove(destPath2, destPath3); + + superuser.refresh(false); + + assertEquals(moveNode.getPath(), originalPath); + assertTrue(srcParentNode.hasNode(Text.getName(originalPath))); + assertFalse(srcParentNode.hasNode(Text.getName(destPath2))); + assertFalse(destParentNode.hasNodes()); + } + + /** + * Move a new node multiple times and check the hierarchy after saving. + */ + public void testMultipleMoveNewNode() throws RepositoryException { + // add additional nodes + Node moveNode2 = moveNode.addNode(nodeName3, testNodeType); + + doMove(moveNode2.getPath(), destinationPath); + String destPath2 = destParentNode.getPath()+"/"+nodeName4; + doMove(moveNode2.getPath(), destPath2); + String destPath3 = srcParentNode.getPath()+"/"+nodeName4; + doMove(moveNode2.getPath(), destPath3); + doMove(moveNode2.getPath(), destinationPath); + + testRootNode.save(); + + assertTrue(moveNode2.getParent().isSame(destParentNode)); + assertEquals(moveNode2.getName(), Text.getName(destinationPath)); + assertEquals(moveNode2.getPath(), destinationPath); + assertFalse(moveNode2.hasNodes()); + + superuser.save(); + } + + /** + * Move destination after moving the target node. + */ + public void testMoveDestination() throws RepositoryException { + doMove(moveNode.getPath(), destinationPath); + doMove(destParentNode.getPath(), srcParentNode.getPath() + "/" + destParentNode.getName()); + + superuser.save(); + assertTrue(destParentNode.getParent().isSame(srcParentNode)); + assertTrue(moveNode.getParent().isSame(destParentNode)); + } + + /** + * Separately move the persisted 'moveNode' and its transiently added + * child node. + */ + public void testMoveParentAndChild() throws RepositoryException { + // add additional nodes + Node moveNode2 = moveNode.addNode(nodeName3, testNodeType); + Property childProperty = moveNode2.setProperty(propertyName2, "anyString"); + Node childNode = moveNode2.addNode(nodeName4, testNodeType); + + doMove(moveNode.getPath(), destinationPath); + doMove(moveNode2.getPath(), srcParentNode.getPath() + "/" + moveNode2.getName()); + + assertFalse(moveNode.hasNode(moveNode2.getName())); + assertFalse(moveNode.hasNodes()); + assertTrue(srcParentNode.getNode(moveNode2.getName()).isSame(moveNode2)); + + doMove(moveNode.getPath(), originalPath); + + assertEquals(moveNode.getPath(), originalPath); + assertFalse(destParentNode.hasNode(Text.getName(destinationPath))); + + assertFalse(moveNode.hasNode(moveNode2.getName())); + assertFalse(moveNode.hasNodes()); + assertTrue(srcParentNode.getNode(moveNode2.getName()).isSame(moveNode2)); + + superuser.save(); + + assertFalse(moveNode.hasNodes()); + assertTrue(moveNode2.hasNode(childNode.getName())); + assertTrue(moveNode2.hasProperty(childProperty.getName())); + + assertTrue(srcParentNode.getNode(moveNode.getName()).isSame(moveNode)); + assertTrue(srcParentNode.getNode(moveNode2.getName()).isSame(moveNode2)); + } + + /** + * Move a node that has a child node and finally revert the 'move' operations. + */ + public void testRevertingMoveParentAndChild() throws RepositoryException { + Node moveNode2 = moveNode.addNode(nodeName3, testNodeType); + // moveNode2 must be persisted in order not to have it removed upon + // refresh(false). + moveNode.save(); + + doMove(moveNode.getPath(), destinationPath); + doMove(moveNode2.getPath(), srcParentNode.getPath() + "/" + moveNode2.getName()); + doMove(moveNode.getPath(), originalPath); + + testRootNode.refresh(false); + + // now all 3 move ops must be reverted + assertTrue(moveNode2.getParent().isSame(moveNode)); + assertTrue(moveNode.getParent().isSame(srcParentNode)); + assertFalse(destParentNode.hasNodes()); + assertFalse(srcParentNode.hasNode(moveNode2.getName())); + } + + /** + * Separately move the new 'moveNode' and its child node. Save 'add' and + * 'move' ops in one step. + */ + public void testMoveNewParentAndNewChild() throws RepositoryException { + Node moveNode2 = moveNode.addNode("moveNode2", testNodeType); + Property childProperty = moveNode2.setProperty(propertyName2, "anyString"); + Node childNode = moveNode2.addNode("childNode", testNodeType); + + doMove(moveNode2.getPath(), destinationPath); + doMove(childNode.getPath(), srcParentNode.getPath() + "/" + childNode.getName()); + doMove(moveNode2.getPath(), srcParentNode.getPath() + "/" + nodeName4); + + superuser.save(); + + assertTrue(moveNode2.getName().equals(nodeName4)); + assertFalse(moveNode2.hasNodes()); + assertTrue(moveNode2.hasProperty(childProperty.getName())); + + assertTrue(moveNode2.getParent().isSame(srcParentNode)); + assertTrue(childNode.getParent().isSame(srcParentNode)); + } + + /** + * Separately move the persisted 'moveNode' and its 'new' child node. + * Check if reverting the changes removes the 'new' child and moves + * the persisted moveNode back. + */ + public void testRevertingMoveParentAndNewChild() throws RepositoryException { + Node moveNode2 = moveNode.addNode(nodeName3, testNodeType); + + doMove(moveNode.getPath(), destinationPath); + doMove(moveNode2.getPath(), srcParentNode.getPath() + "/" + moveNode2.getName()); + doMove(moveNode.getPath(), originalPath); + + testRootNode.refresh(false); + + // moveNode2 which has never been saved, must be removed + try { + moveNode2.getParent(); + fail("Reverting the move of a 'new' node must remove the new node as well."); + } catch (InvalidItemStateException e) { + // ok + } + // the persistent 'moveNode' must be moved back to its original position. + assertTrue(moveNode.getParent().isSame(srcParentNode)); + assertFalse(destParentNode.hasNodes()); + } + + /** + * Move a node with child items without having loaded the children before. + * Test if children can be accessed afterwards. + */ + public void testAccessChildrenAfterMove() throws RepositoryException { + Property childProperty = moveNode.setProperty(propertyName2, "anyString"); + Node childNode = moveNode.addNode(nodeName2, testNodeType); + testRootNode.save(); + + Session otherSession = getHelper().getReadWriteSession(); + try { + otherSession.move(originalPath, destinationPath); + Node mv = (Node) otherSession.getItem(destinationPath); + + testRootNode.refresh(false); + assertTrue(childNode.isSame(mv.getNode(nodeName2))); + assertTrue(childProperty.isSame(mv.getProperty(propertyName2))); + } finally { + otherSession.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveNewTreeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveNewTreeTest.java new file mode 100644 index 00000000000..a10ef3086cb --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveNewTreeTest.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.jcr2spi; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Item; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MoveTreeTest... + */ +public class MoveNewTreeTest extends AbstractMoveTreeTest { + + private static Logger log = LoggerFactory.getLogger(MoveNewTreeTest.class); + + @Override + protected boolean saveBeforeMove() { + return false; + } + + @Override + protected boolean isSessionMove() { + return true; + } + + public void testTreeAncestors() throws RepositoryException { + int degree = destParentNode.getDepth(); + Item ancestor = childNode.getAncestor(degree); + assertTrue("Moving a node must move all child items as well.", ancestor.isSame(destParentNode)); + ancestor = childProperty.getAncestor(degree); + assertTrue("Moving a node must move all child items as well.", ancestor.isSame(destParentNode)); + ancestor = grandChildNode.getAncestor(degree); + assertTrue("Moving a node must move all child items as well.", ancestor.isSame(destParentNode)); + } + + public void testTreeEntries() throws RepositoryException { + Item item = superuser.getItem(destinationPath + "/" + nodeName2); + assertTrue("Moving a node must move all child items as well.", childNode.isSame(item)); + item = superuser.getItem(destinationPath + "/" + propertyName2); + assertTrue("Moving a node must move all child items as well.", childProperty.isSame(item)); + item = superuser.getItem(destinationPath + "/" + nodeName2 + "/" + nodeName3); + assertTrue("Moving a node must move all child items as well.", grandChildNode.isSame(item)); + } + + public void testOldPath() throws RepositoryException { + try { + superuser.getItem(srcPath + "/" + nodeName2 + "/" + nodeName3); + fail("Moving a node must move all child items as well."); + } catch (PathNotFoundException e) { + // ok + } + } + + /** + * Reverting the MOVE of a NEW-node must also remove the Node at its + * original position. + * + * @throws RepositoryException + */ + public void testRevertRemovedFromSrc() throws RepositoryException { + superuser.refresh(false); + assertFalse("Reverting move of a new node must remove the node from both positions.", superuser.itemExists(srcPath)); + } + + /** + * Reverting the MOVE of a NEW-node must remove the Node from the destination. + * + * @throws RepositoryException + */ + public void testRevertRemovedFromDestination() throws RepositoryException { + superuser.refresh(false); + assertFalse("Reverting move of a new node must remove the node from both positions.", superuser.itemExists(destinationPath)); + } + + public void testRevertInvalidatedMovedTree() throws RepositoryException { + superuser.refresh(false); + try { + childNode.getAncestor(0); + fail("Reverting move of a new node must remove the tree completely"); + } catch (RepositoryException e) { + // OK + } + try { + childProperty.getAncestor(0); + fail("Reverting move of a new node must remove the tree completely"); + } catch (RepositoryException e) { + // OK + } + try { + grandChildNode.getAncestor(0); + fail("Reverting move of a new node must remove the tree completely"); + } catch (RepositoryException e) { + // OK + } + } + + public void testRefreshMovedTree() throws RepositoryException { + testRootNode.refresh(true); + String msg = "Refresh must not revert a moved tree."; + + assertFalse(msg, superuser.itemExists(srcPath + "/" + nodeName2 + "/" + nodeName3)); + int degree = destParentNode.getDepth(); + + List l = new ArrayList(); + l.add(childNode); + l.add(childProperty); + l.add(grandChildNode); + + for (Iterator it = l.iterator(); it.hasNext();) { + Item item = it.next(); + assertTrue(msg, item.isNew()); + assertTrue(msg, childNode.getAncestor(degree).isSame(destParentNode)); + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveReferenceableTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveReferenceableTest.java new file mode 100644 index 00000000000..2a713d924fe --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveReferenceableTest.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.jcr2spi; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MoveReferenceableTest... + */ +public class MoveReferenceableTest extends AbstractMoveTest { + + private static Logger log = LoggerFactory.getLogger(MoveReferenceableTest.class); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (!moveNode.canAddMixin(mixReferenceable)) { + throw new NotExecutableException("Cannot add mix:referencable to node to be moved."); + } + // prepare move-node + moveNode.addMixin(mixReferenceable); + moveNode.save(); + } + + @Override + protected boolean isSessionMove() { + return true; + } + + /** + * Test if a moved referenceable node still has the same uuid. + */ + public void testMovedReferenceable() throws RepositoryException, NotExecutableException { + + String uuid = moveNode.getUUID(); + //move the node + doMove(moveNode.getPath(), destinationPath); + assertEquals("After successful moving a referenceable node node, the uuid must not have changed.", uuid, moveNode.getUUID()); + } + + /** + * Same as {@link #testMovedReferenceable()}, but calls save before + * executing the comparison. + */ + public void testMovedReferenceable2() throws RepositoryException, NotExecutableException { + + String uuid = moveNode.getUUID(); + //move the node + doMove(moveNode.getPath(), destinationPath); + superuser.save(); + assertEquals("After successful moving a referenceable node node, the uuid must not have changed.", uuid, moveNode.getUUID()); + } + + /** + * Test if a moved referenceable node returns the same item than the moved + * node. + */ + public void testAccessMovedReferenceableByUUID() throws RepositoryException, NotExecutableException { + + String uuid = moveNode.getUUID(); + //move the node + doMove(moveNode.getPath(), destinationPath); + + Node n = superuser.getNodeByUUID(uuid); + assertTrue("After successful moving a referenceable node node, accessing the node by uuid must return the same node.", n.isSame(moveNode)); + } + + /** + * Same as {@link #testAccessMovedReferenceableByUUID()} but calls save() + * before accessing the node again. + */ + public void testAccessMovedReferenceableByUUID2() throws RepositoryException, NotExecutableException { + + String uuid = moveNode.getUUID(); + //move the node + doMove(moveNode.getPath(), destinationPath); + superuser.save(); + + Node n = superuser.getNodeByUUID(uuid); + assertTrue("After successful moving a referenceable node node, accessing the node by uuid must return the same node.", n.isSame(moveNode)); + } + + /** + * Move a versionable (referenceable) node twice + * + * @throws RepositoryException + * @see JCR-2572 + */ + public void testMoveTwice() throws RepositoryException { + moveNode.addMixin(mixVersionable); + superuser.save(); + + // move the node + doMove(moveNode.getPath(), destinationPath); + superuser.save(); + + // move second time + String destinationPath2 = destParentNode.getPath() + "/" + nodeName3; + doMove(destinationPath, destinationPath2); + superuser.save(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveSNSTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveSNSTest.java new file mode 100644 index 00000000000..123a01e6880 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveSNSTest.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MoveSNSTest (Implementation specific tests. JSR170 only + * expects orderable same-name-siblings to have a consistent and testable + * order.) + */ +public class MoveSNSTest extends AbstractMoveTest { + + private static Logger log = LoggerFactory.getLogger(MoveSNSTest.class); + + private Node sourceSibling; + private Node destSibling; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (destParentNode.hasNode(nodeName2)) { + fail("Setup: Move destination already contains a child node with name " + nodeName2); + } + + if (!moveNode.getDefinition().allowsSameNameSiblings()) { + fail("Setup: Unable to create SNS-node for MoveSNSTest."); + } + sourceSibling = srcParentNode.addNode(nodeName2, testNodeType); + destSibling = destParentNode.addNode(nodeName2, testNodeType); + + if (!destSibling.getDefinition().allowsSameNameSiblings()) { + fail("Setup: Unable to create SNS-node at move destination."); + } + testRootNode.save(); + } + + @Override + protected void tearDown() throws Exception { + sourceSibling = null; + destSibling = null; + super.tearDown(); + } + + @Override + protected boolean isSessionMove() { + return true; + } + + /** + * Implementation specific: + * Test if the path of a moved node, contains the index of the last sibling. + */ + public void testMovedNodeGetPath() throws RepositoryException, NotExecutableException { + int index = destSibling.getIndex() + 1; + //move the node + doMove(moveNode.getPath(),destinationPath); + assertEquals("After successful move the moved node must return the destination path.", destinationPath + "["+ index +"]", moveNode.getPath()); + } + + /** + * Implementation specific: + * Same as {@link #testMovedNodeGetPath()}, but calls save prior to the + * test. + */ + public void testMovedNodeGetPath2() throws RepositoryException, NotExecutableException { + int index = destSibling.getIndex() + 1; + //move the node + doMove(moveNode.getPath(), destParentNode.getPath() + "/" + nodeName2); + superuser.save(); + assertEquals("After successful move the moved node must return the destination path.", destinationPath + "["+ index +"]", moveNode.getPath()); + } + + + /** + * Test if a moved node is 'replaced' by its SNS. + */ + public void testAccessMovedNodeByOldPath() throws RepositoryException, NotExecutableException { + String oldPath = moveNode.getPath(); + //move the node + doMove(oldPath, destinationPath); + try { + Item item = superuser.getItem(oldPath); + // Implementation specific: + assertTrue("A moved SNS node must be 'replaced' but is successor sibling.", item.isSame(sourceSibling)); + } catch (PathNotFoundException e) { + fail("A moved SNS node must be 'replaced' but is successor sibling."); + } + } + + /** + * Same as {@link #testAccessMovedNodeByOldPath()} but calls save() prior to + * the test. + */ + public void testAccessMovedNodeByOldPath2() throws RepositoryException, NotExecutableException { + String oldPath = moveNode.getPath(); + //move the node + doMove(oldPath, destinationPath); + superuser.save(); + try { + Item item = superuser.getItem(oldPath); + // Implementation specific: + assertTrue("A moved SNS node must be 'replaced' but is successor sibling.", item.isSame(sourceSibling)); + } catch (PathNotFoundException e) { + fail("A moved SNS node must be 'replaced' but is successor sibling."); + } + } + + /** + * Implementation specific: + * Test if the moved node is appended to the list of SNSs at the destination. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testMovedNodeIsSame() throws RepositoryException, NotExecutableException { + //move the node + doMove(moveNode.getPath(), destinationPath); + + int cnt = 0; + for (NodeIterator it = destParentNode.getNodes(nodeName2); it.hasNext();) { + Node n = it.nextNode(); + if (cnt == 0) { + assertTrue("Moved node must be appended to list of SNSs.", destSibling.isSame(n)); + } else { + assertTrue("Moved node must be appended to list of SNSs.", moveNode.isSame(n)); + } + cnt++; + } + } + + /** + * Implementation specific: + * Same as {@link #testMovedNodeIsSame()}, but calls save() before executing + * the comparison. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testMovedNodeIsSame2() throws RepositoryException, NotExecutableException { + //move the node + doMove(moveNode.getPath(), destinationPath); + superuser.save(); + + int cnt = 0; + for (NodeIterator it = destParentNode.getNodes(nodeName2); it.hasNext();) { + Node n = it.nextNode(); + if (cnt == 0) { + assertTrue("Moved node must be appended to list of SNSs.", destSibling.isSame(n)); + } else { + assertTrue("Moved node must be appended to list of SNSs.", moveNode.isSame(n)); + } + cnt++; + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveTest.java new file mode 100644 index 00000000000..70c91bf4649 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveTest.java @@ -0,0 +1,321 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.io.ByteArrayInputStream; + +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MoveTest... + */ +public class MoveTest extends AbstractMoveTest { + + private static Logger log = LoggerFactory.getLogger(MoveTest.class); + + @Override + protected boolean isSessionMove() { + return true; + } + + public void testMoveRoot() throws RepositoryException { + Node root = superuser.getRootNode(); + try { + doMove(root.getPath(), destinationPath); + fail("Moving the root node must fail with RepositoryException."); + } catch (RepositoryException e) { + // OK + } + } + + public void testMoveBelowDescendant() throws RepositoryException { + try { + doMove(srcParentNode.getPath(), moveNode.getPath() + "/" + nodeName2); + fail("Moving the ancestor node below descendant must fail with RepositoryException."); + } catch (RepositoryException e) { + // OK + } + } + + public void testMoveDestinationWithIndex() throws RepositoryException { + try { + doMove(moveNode.getPath(), destinationPath + "[1]"); + fail("Moving to destination with index must fail with RepositoryException."); + } catch (RepositoryException e) { + // OK + } + } + + /** + * Test if a moved node returns the specified destination path and by the + * way test, if the moved node is still valid. + */ + public void testMovedNodeGetPath() throws RepositoryException, NotExecutableException { + String oldPath = moveNode.getPath(); + + if (destParentNode.hasNode(nodeName2)) { + throw new NotExecutableException("Move destination already contains a child node with name " + nodeName2); + } + //move the node + doMove(oldPath, destinationPath); + assertEquals("After successful move the moved node must return the destination path.", destinationPath, moveNode.getPath()); + } + + /** + * Same as {@link #testMovedNodeGetPath()}, but calls save prior to the + * test. + */ + public void testMovedNodeGetPath2() throws RepositoryException, NotExecutableException { + String oldPath = moveNode.getPath(); + + if (destParentNode.hasNode(nodeName2)) { + throw new NotExecutableException("Move destination already contains a child node with name " + nodeName2); + } + //move the node + doMove(oldPath, destParentNode.getPath() + "/" + nodeName2); + superuser.save(); + assertEquals("After successful move the moved node must return the destination path.", destinationPath, moveNode.getPath()); + } + + /** + * Test if a moved node is not accessible by its old path any more + */ + public void testAccessMovedNodeByOldPath() throws RepositoryException, NotExecutableException { + NodeIterator it = srcParentNode.getNodes(moveNode.getName()); + int cnt = 0; + while (it.hasNext()) { + it.nextNode(); + cnt++; + } + if (cnt > 1) { + throw new NotExecutableException("Move source parent has multiple child nodes with name " + moveNode.getName()); + } + + String oldPath = moveNode.getPath(); + + //move the node + doMove(oldPath, destinationPath); + try { + superuser.getItem(oldPath); + fail("A moved node must not be accessible by its old path any more."); + } catch (PathNotFoundException e) { + // ok. + } + } + + /** + * Same as {@link #testAccessMovedNodeByOldPath()} but calls save() prior to + * the test. + */ + public void testAccessMovedNodeByOldPath2() throws RepositoryException, NotExecutableException { + NodeIterator it = srcParentNode.getNodes(moveNode.getName()); + int cnt = 0; + while (it.hasNext()) { + it.nextNode(); + cnt++; + } + if (cnt > 1) { + throw new NotExecutableException("Move source parent has multiple child nodes with name " + moveNode.getName()); + } + + String oldPath = moveNode.getPath(); + + //move the node + doMove(oldPath, destinationPath); + superuser.save(); + try { + superuser.getItem(oldPath); + fail("A moved node must not be accessible by its old path any more."); + } catch (PathNotFoundException e) { + // ok. + } + } + + /** + * Test if the accessing the moved node from the session returns the same + * Node object, than the Node which was moved before. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testMovedNodeIsSame() throws RepositoryException, NotExecutableException { + if (destParentNode.hasNode(nodeName2)) { + throw new NotExecutableException(destParentNode + " already has child node " + ". Test cannot be preformed if SNS is present."); + } + + String oldPath = moveNode.getPath(); + String newPath = destParentNode.getPath() + "/" + nodeName2; + + //move the node + doMove(oldPath, destinationPath); + Item movedItem = superuser.getItem(newPath); + assertTrue("Moved Node must be the same after the move.", movedItem.isSame(moveNode)); + } + + /** + * Same as {@link #testMovedNodeIsSame()}, but calls save() before executing + * the comparison. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testMovedNodeIsSame2() throws RepositoryException, NotExecutableException { + if (destParentNode.hasNode(nodeName2)) { + throw new NotExecutableException(destParentNode + " already has child node " + ". Test cannot be preformed if SNS is present."); + } + + String oldPath = moveNode.getPath(); + + //move the node + doMove(oldPath, destinationPath); + superuser.save(); + + Item movedItem = superuser.getItem(destinationPath); + assertTrue("Moved Node must be the same after the move.", movedItem.isSame(moveNode)); + } + + /** + * Test if after the move, Node.getParent() returns the + * destination parent. + * + * @throws RepositoryException + */ + public void testMovedNodeParent() throws RepositoryException { + //move the node + doMove(moveNode.getPath(), destinationPath); + assertTrue("Parent of moved node must be the destination parent node.", moveNode.getParent().isSame(destParentNode)); + } + + /** + * Same as {@link #testMovedNodeParent()}, but calls save before executing + * the comparison. + * + * @throws RepositoryException + */ + public void testMovedNodeParent2() throws RepositoryException { + //move the node + doMove(moveNode.getPath(), destinationPath); + superuser.save(); + + assertTrue("Parent of moved node must be the destination parent node.", moveNode.getParent().isSame(destParentNode)); + } + + /** + * Tries to move a node using {@link javax.jcr.Session#move(String src, String dest)} + * to a location where a property already exists with same name. + *
        + * With JCR 1.0 this should throw an {@link javax.jcr.ItemExistsException}. + * With JCR 2.0 the support for same-named property and node is optional and + * the expected behaviour depends on the + * {@link Repository#OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED} descriptor. + */ + public void testMovePropertyExists() throws RepositoryException, NotExecutableException { + // try to create a property with the name of the node to be moved + // to the destination parent + Property destProperty; + try { + destProperty = destParentNode.setProperty(nodeName2, "anyString"); + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot create property with name '" +nodeName2+ "' and value 'anyString' at move destination."); + } + + // TODO: fix 2.0 behaviour according to the OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED descriptor + if ("1.0".equals(getHelper().getRepository().getDescriptor(Repository.SPEC_VERSION_DESC))) { + try { + // move the node + doMove(moveNode.getPath(), destProperty.getPath()); + fail("Moving a node to a location where a property exists must throw ItemExistsException"); + } catch (ItemExistsException e) { + // ok, works as expected + } + } else { + // move the node: same name property and node must be supported + // see Issue 725 + doMove(moveNode.getPath(), destProperty.getPath()); + } + } + + /** + * Regression tests for JCR-2528 + * @throws RepositoryException + */ + public void testMoveReferenceableNode() throws RepositoryException { + moveNode.addMixin(JcrConstants.MIX_REFERENCEABLE); + moveNode.getSession().save(); + + superuser.move(moveNode.getPath(), destParentNode.getPath() + "/" + moveNode.getName()); + superuser.save(); + destParentNode.remove(); + + // JCR-2528 caused this call to throw a javax.jcr.InvalidItemStateException: Item has already + // been removed by someone else. Status = REMOVED + destParentNode.getSession().save(); + } + + + public void testMoveFile() throws RepositoryException, NotExecutableException { + // create a new file + String parentPath; + String filePath; + try { + Node parent = testRootNode.addNode("parent"); + Node n = JcrUtils.putFile(parent, "file", "text/plain", new ByteArrayInputStream("data".getBytes())); + parentPath = parent.getPath(); + filePath = n.getPath(); + superuser.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + Session s = getHelper().getSuperuserSession(); + try { + Node n1 = s.getNode(filePath); + Node n2 = n1.getNode("jcr:content"); + n2.setProperty("jcr:data", new java.io.ByteArrayInputStream("data2".getBytes())); + n2.save(); + + String destPath = parentPath + "1"; + if (isSessionMove()) { + s.move(parentPath, destPath); + s.save(); + } else { + s.getWorkspace().move(parentPath, destPath); + } + Node n3 = s.getNode(destPath + "/file"); + Node n4 = n3.getNode("jcr:content"); + n4.refresh(false); + // call must succeed (see JCR-2472) + Node n5 = n3.getNode("jcr:content"); + } finally { + s.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveToNewTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveToNewTest.java new file mode 100644 index 00000000000..8dfd8c254bb --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveToNewTest.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.util.Text; + +/** MoveToNewTest... */ +public class MoveToNewTest extends AbstractJCRTest { + + protected Node srcParentNode; + protected Node destParentNode; + protected Node moveNode; + protected String destinationPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create parent node + srcParentNode = testRootNode.addNode(nodeName1, testNodeType); + // create node to be moved + moveNode = srcParentNode.addNode(nodeName2, testNodeType); + // save the new nodes + testRootNode.save(); + + // create a NEW node that will serve as destination parent + destParentNode = testRootNode.addNode(nodeName3, testNodeType); + destinationPath = destParentNode.getPath() + "/" + nodeName2; + } + + @Override + protected void tearDown() throws Exception { + srcParentNode = null; + destParentNode = null; + moveNode = null; + super.tearDown(); + } + + public void testMove() throws RepositoryException { + String srcPath = moveNode.getPath(); + testRootNode.getSession().move(srcPath, destinationPath); + + assertTrue(destParentNode.isNew()); + assertTrue(moveNode.isModified()); + + assertTrue(testRootNode.getSession().itemExists(destinationPath)); + assertFalse(testRootNode.getSession().itemExists(srcPath)); + } + + public void testMoveSaved() throws RepositoryException { + String srcPath = moveNode.getPath(); + testRootNode.getSession().move(srcPath, destinationPath); + testRootNode.save(); + + assertFalse(destParentNode.isNew()); + assertFalse(srcParentNode.isModified()); + assertFalse(moveNode.isModified()); + + assertTrue(testRootNode.getSession().itemExists(destinationPath)); + assertFalse(testRootNode.getSession().itemExists(srcPath)); + } + + public void testRevertMovedNode() throws RepositoryException { + String srcPath = moveNode.getPath(); + testRootNode.getSession().move(srcPath, destinationPath); + + try { + destParentNode.refresh(false); + fail("Incomplete 'changelog'"); + } catch (RepositoryException e) { + // ok + } + } + + public void testRemoveDestParent() throws RepositoryException { + String srcPath = moveNode.getPath(); + testRootNode.getSession().move(srcPath, destinationPath); + destParentNode.remove(); + + assertFalse(destParentNode.isNew()); + assertFalse(destParentNode.isModified()); + + assertFalse(moveNode.isModified()); + assertTrue(srcParentNode.isModified()); + assertFalse(testRootNode.getSession().itemExists(srcPath)); + } + + public void testRevertRemoveDestParent() throws RepositoryException { + String srcPath = moveNode.getPath(); + testRootNode.getSession().move(srcPath, destinationPath); + destParentNode.remove(); + testRootNode.refresh(false); + + assertFalse(destParentNode.isModified()); + assertFalse(destParentNode.isNew()); + + try { + destParentNode.hasNode(nodeName2); + fail("The new destParent must have been removed."); + } catch (InvalidItemStateException e) { + // success + } + assertTrue(srcParentNode.hasNode(nodeName2)); + assertFalse(srcParentNode.isModified()); + + assertFalse(testRootNode.getSession().itemExists(destinationPath)); + assertTrue(testRootNode.getSession().itemExists(srcPath)); + } + + public void testMoveTwice() throws RepositoryException { + Session s = testRootNode.getSession(); + + String srcPath = moveNode.getPath(); + s.move(srcPath, destinationPath); + + srcParentNode.remove(); + + // create new parent + Node newParent = testRootNode.addNode(nodeName1); + s.move(destinationPath, srcPath); + + assertTrue(newParent.isNew()); + assertTrue(newParent.hasNode(nodeName2)); + + assertTrue(destParentNode.isNew()); + assertFalse(destParentNode.hasNode(nodeName2)); + + // remove the tmp destination parent node. + destParentNode.remove(); + + assertTrue(newParent.isNew()); + assertTrue(newParent.hasNode(nodeName2)); + assertTrue(moveNode.isModified()); + + testRootNode.save(); + + assertFalse(s.itemExists(Text.getRelativeParent(destinationPath, 1))); + assertTrue(s.itemExists(srcPath)); + + assertFalse(moveNode.isModified() || newParent.isNew() || srcParentNode.isModified()); + try { + srcParentNode.getNode(nodeName2); + fail("src parent must be removed"); + } catch (InvalidItemStateException e) { + // ok. + } + assertTrue(moveNode.isSame(newParent.getNode(nodeName2))); + } + + public void testMoveTwiceWithSecondSession() throws RepositoryException { + Session s = testRootNode.getSession(); + String srcPath = moveNode.getPath(); + + // move away the 'moveNode' + s.move(srcPath, destinationPath); + // rm the original parent + srcParentNode.remove(); + // create new parent and move the 'moveNode' back + Node newParent = testRootNode.addNode(nodeName1); + newParent.setProperty(propertyName1, "marker"); + s.move(destinationPath, srcPath); + // remove the tmp. destination parent + destParentNode.remove(); + + testRootNode.save(); + + Session readOnly = getHelper().getReadOnlySession(); + try { + Node trn = (Node) readOnly.getItem(testRootNode.getPath()); + NodeIterator it = trn.getNodes(nodeName1); + + String msg = "testRootNode must have a single child node with name " + nodeName1; + if (it.hasNext()) { + Node parent = it.nextNode(); + assertTrue(parent.hasProperty(propertyName1)); + assertEquals("The 'newParent' must have the marker property","marker", parent.getProperty(propertyName1).getString()); + assertTrue("moveNode must be present below the 'newParent'.", parent.hasNode(Text.getName(srcPath))); + assertFalse(msg, it.hasNext()); + } else { + fail(msg); + } + } finally { + readOnly.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveTreeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveTreeTest.java new file mode 100644 index 00000000000..417b587de8f --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MoveTreeTest.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.jcr2spi; + +import javax.jcr.Item; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MoveTreeTest... + */ +public class MoveTreeTest extends AbstractMoveTreeTest { + + private static Logger log = LoggerFactory.getLogger(MoveTreeTest.class); + + @Override + protected boolean saveBeforeMove() { + return true; + } + + @Override + protected boolean isSessionMove() { + return true; + } + + public void testTreeAncestors() throws RepositoryException { + int degree = destParentNode.getDepth(); + Item ancestor = childNode.getAncestor(degree); + assertTrue("Moving a node must move all child items as well.", ancestor.isSame(destParentNode)); + ancestor = childProperty.getAncestor(degree); + assertTrue("Moving a node must move all child items as well.", ancestor.isSame(destParentNode)); + ancestor = grandChildNode.getAncestor(degree); + assertTrue("Moving a node must move all child items as well.", ancestor.isSame(destParentNode)); + } + + public void testTreeEntries() throws RepositoryException { + Item item = superuser.getItem(destinationPath + "/" + nodeName2); + assertTrue("Moving a node must move all child items as well.", childNode.isSame(item)); + item = superuser.getItem(destinationPath + "/" + propertyName2); + assertTrue("Moving a node must move all child items as well.", childProperty.isSame(item)); + item = superuser.getItem(destinationPath + "/" + nodeName2 + "/" + nodeName3); + assertTrue("Moving a node must move all child items as well.", grandChildNode.isSame(item)); + } + + public void testOldPath() throws RepositoryException { + try { + superuser.getItem(srcPath + "/" + nodeName2); + fail("Moving a node must move all child items as well."); + } catch (PathNotFoundException e) { + // ok + } + } + + public void testOldPropertyPath() throws RepositoryException { + try { + superuser.getItem(srcPath + "/" + propertyName2); + fail("Moving a node must move all child items as well."); + } catch (PathNotFoundException e) { + // ok + } + } + + public void testOldChildPath() throws RepositoryException { + for (int i = 0; i < childPaths.size(); i++) { + String path = childPaths.get(i).toString(); + assertFalse(superuser.itemExists(path)); + try { + superuser.getItem(path); + fail("Moving a node must move all child items as well."); + } catch (PathNotFoundException e) { + // ok + } + } + } + + public void testOldChildPropertyPath() throws RepositoryException { + for (int i = 0; i < childPaths.size(); i++) { + String propPath = childPaths.get(i).toString() + "/" + jcrPrimaryType; + assertFalse(superuser.itemExists(propPath)); + try { + superuser.getItem(propPath); + fail("Moving a node must move all child items as well."); + } catch (PathNotFoundException e) { + // ok + } + } + } + + public void testAncestorAfterRevert() throws RepositoryException { + superuser.refresh(false); + Item ancestor = grandChildNode.getAncestor(srcParentNode.getDepth()); + assertTrue("Reverting a move-operation must move the tree back.", ancestor.isSame(srcParentNode)); + } + + public void testDestinationAfterRevert() throws RepositoryException { + superuser.refresh(false); + try { + superuser.getItem(destinationPath + "/" + propertyName2); + fail("Reverting a move-operation must move the tree back."); + } catch (PathNotFoundException e) { + // ok + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MultiValuedPropertyTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MultiValuedPropertyTest.java new file mode 100644 index 00000000000..3742cde36cd --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/MultiValuedPropertyTest.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.jcr2spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.api.PropertyUtil; + +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.ValueFormatException; + +/** + * MultiValuedPropertyTest... + */ +public class MultiValuedPropertyTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(MultiValuedPropertyTest.class); + + /** + * Tests if Property.getStream() fails with ValueFormatException for + * multivalued properties. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testGetStreamFromMultivalued() throws RepositoryException, NotExecutableException { + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("No multivalued property found."); + } + else { + try { + prop.getStream(); + fail("Property.getStream() must fail with ValueFormatException for any multivalued property."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests if Property.getString() fails with ValueFormatException for + * multivalued properties. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testGetStringFromMultivalued() throws RepositoryException, NotExecutableException { + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("No multivalued property found."); + } + else { + try { + prop.getString(); + fail("Property.getString() must fail with ValueFormatException for any multivalued property."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests if Property.getDate() fails multivalued properties. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testGetDateFromMultivalued() throws RepositoryException, NotExecutableException { + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("No multivalued property found."); + } + else { + try { + prop.getDate(); + fail("Property.getDate() must fail with ValueFormatException for any multivalued property."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests if Property.getDouble() fails for multivalued properties. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testGetDoubleFromMultivalued() throws RepositoryException, NotExecutableException { + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("No multivalued property found."); + } + else { + try { + prop.getDouble(); + fail("Property.getDouble() must fail with ValueFormatException for any multivalued property."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests if Property.getLong() fails for multivalued properties. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testGetLongFromMultivalued() throws RepositoryException, NotExecutableException { + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("No multivalued property found."); + } + else { + try { + prop.getLong(); + fail("Property.getLong() must fail with ValueFormatException for any multivalued property."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests if Property.getBoolean() fails for multivalued properties. + * + * @throws RepositoryException + * @throws NotExecutableException + */ + public void testGetBooleanFromMultivalued() throws RepositoryException, NotExecutableException { + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("No multivalued property found."); + } + else { + try { + prop.getBoolean(); + fail("Property.getLong() must fail with ValueFormatException for any multivalued property."); + } catch (ValueFormatException vfe) { + // ok + } + } + } + + /** + * Tests if Property.getLength() fails for multivalued property. + */ + public void testGetLengthFromMultivalued() throws RepositoryException, NotExecutableException { + Property prop = PropertyUtil.searchMultivalProp(testRootNode); + if (prop == null) { + throw new NotExecutableException("No multivalued property found."); + } + try { + prop.getLength(); + fail("Property.getLength() called on a multivalue property must fail (ValueFormatException)."); + } catch (ValueFormatException vfe) { + // ok + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/NodeOrderTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/NodeOrderTest.java new file mode 100644 index 00000000000..7744848ecef --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/NodeOrderTest.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.jcr2spi; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NodeOrderTest... + */ +public class NodeOrderTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(NodeOrderTest.class); + + private Node[] children; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!testRootNode.getPrimaryNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Test node does not have orderable children."); + } + children = new Node[4]; + children[0] = testRootNode.addNode(nodeName1, testNodeType); + children[1] = testRootNode.addNode(nodeName2, testNodeType); + children[2] = testRootNode.addNode(nodeName3, testNodeType); + children[3] = testRootNode.addNode(nodeName4, testNodeType); + testRootNode.save(); + } + + @Override + protected void tearDown() throws Exception { + children = null; + super.tearDown(); + } + + private static void checkOrder(NodeIterator it, Node[] children) throws RepositoryException { + int i = 0; + while (it.hasNext()) { + if (i >= children.length) { + fail("Node.getNodes() return more child nodes, that have been created."); + } + Node n = it.nextNode(); + assertTrue("Wrong order of child nodes returned by Node.getNodes().", n.isSame(children[i])); + i++; + } + + if (i != children.length) { + fail("Node.getNodes() did not return all child nodes."); + } + } + + /** + * Test if the order of Nodes is maintained across multiple calls to + * Node.getNodes(). + */ + public void testOrder() throws RepositoryException { + NodeIterator it = testRootNode.getNodes(); + checkOrder(it, children); + it = testRootNode.getNodes(); + checkOrder(it, children); + } + + /** + * Test if the order of Nodes is the same when accessed through another + * Session. + */ + public void testOrder2() throws RepositoryException { + Session another = getHelper().getReadOnlySession(); + try { + NodeIterator it = ((Node) another.getItem(testRootNode.getPath())).getNodes(); + checkOrder(it, children); + } finally { + another.logout(); + } + } + + /** + * Test if the order of Nodes is the same when accessed through another + * Session after having accessed some of the nodes individually. + */ + public void testOrderAfterIndividualAccess() throws RepositoryException { + Session another = getHelper().getReadOnlySession(); + try { + Node n2 = (Node) another.getItem(children[2].getPath()); + Node n0 = (Node) another.getItem(children[0].getPath()); + NodeIterator it = ((Node) another.getItem(testRootNode.getPath())).getNodes(); + checkOrder(it, children); + } finally { + another.logout(); + } + } + + /** + * Test if the order of Nodes is the same when accessed through another + * Session after having accessed some of the nodes individually. + */ + public void testOrderAfterIndividualAccess2() throws RepositoryException { + Session another = getHelper().getReadOnlySession(); + try { + Node n2 = (Node) another.getItem(children[3].getPath()); + Node n3 = (Node) another.getItem(children[1].getPath()); + NodeIterator it = ((Node) another.getItem(testRootNode.getPath())).getNodes(); + checkOrder(it, children); + } finally { + another.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/PropertyLengthTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/PropertyLengthTest.java new file mode 100644 index 00000000000..0776b53263c --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/PropertyLengthTest.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.jcr2spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.PropertyUtil; + +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import java.util.Calendar; + +/** + * PropertyLengthTest... + */ +public class PropertyLengthTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(PropertyLengthTest.class); + + private static long getValueLength(Value val) throws RepositoryException { + long valLength; + if (val.getType() == PropertyType.BINARY) { + valLength = PropertyUtil.countBytes(val); + } else { + valLength = val.getString().length(); + } + return valLength; + } + + private Property getProperty(int propertyType) throws RepositoryException, NotExecutableException { + Property p = PropertyUtil.searchProp(testRootNode.getSession(), testRootNode, propertyType, null); + if (p == null) { + try { + Value val; + ValueFactory factory = testRootNode.getSession().getValueFactory(); + switch (propertyType) { + case PropertyType.BINARY: + val = factory.createValue("binaryValue", PropertyType.BINARY); + break; + case PropertyType.BOOLEAN: + val = factory.createValue(true); + break; + case PropertyType.DATE: + val = factory.createValue(Calendar.getInstance()); + break; + case PropertyType.DOUBLE: + val = factory.createValue(new Double(134).doubleValue()); + break; + case PropertyType.LONG: + val = factory.createValue(new Long(134).longValue()); + break; + case PropertyType.NAME: + val = factory.createValue(ntBase, PropertyType.NAME); + break; + case PropertyType.PATH: + val = factory.createValue(testRootNode.getPath(), PropertyType.PATH); + break; + case PropertyType.REFERENCE: + Node refNode = testRootNode.addNode(nodeName1); + if (refNode.canAddMixin(mixReferenceable)) { + testRootNode.addMixin(mixReferenceable); + } + testRootNode.save(); + val = factory.createValue(refNode); + break; + case PropertyType.STRING: + val = factory.createValue("StringValue"); + break; + default: + throw new IllegalArgumentException("Invalid property value type" + propertyType); + } + p = testRootNode.setProperty(propertyName1, val); + } catch (RepositoryException e) { + log.error("Unable to create Property of type " + propertyType); + throw new NotExecutableException(); + } + } + return p; + } + + private static void checkLength(Property p) throws RepositoryException { + if (p.isMultiple()) { + Value[] vals = p.getValues(); + long[] lengths = p.getLengths(); + for (int i = 0; i < lengths.length; i++) { + assertTrue("Wrong property length", lengths[i] == getValueLength(vals[i])); + } + } else { + assertTrue("Wrong property length", p.getLength() == getValueLength(p.getValue())); + } + } + + public void testLengthOfBinary() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.BINARY); + checkLength(p); + } + + public void testLengthOfBoolean() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.BOOLEAN); + checkLength(p); + } + public void testLengthOfDate() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.DATE); + checkLength(p); + } + public void testLengthOfDouble() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.DOUBLE); + checkLength(p); + } + public void testLengthOfLong() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.LONG); + checkLength(p); + } + public void testLengthOfName() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.NAME); + checkLength(p); + } + public void testLengthOfPath() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.PATH); + checkLength(p); + } + public void testLengthOfReference() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.REFERENCE); + checkLength(p); + } + public void testLengthOfString() throws RepositoryException, NotExecutableException { + Property p = getProperty(PropertyType.STRING); + checkLength(p); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshFalseTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshFalseTest.java new file mode 100644 index 00000000000..5e5b92b53b3 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshFalseTest.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.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RefreshFalseTest... + */ +public class RefreshFalseTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(RefreshFalseTest.class); + + private Value testValue; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (testRootNode.hasProperty(propertyName1)) { + testRootNode.getProperty(propertyName1).remove(); + testRootNode.save(); + } + + testValue = testRootNode.getSession().getValueFactory().createValue("anyString"); + if (!testRootNode.getPrimaryNodeType().canSetProperty(propertyName1, testValue)) { + throw new NotExecutableException(""); + } + } + + @Override + protected void tearDown() throws Exception { + testValue = null; + super.tearDown(); + } + + public void testNewProperty() throws RepositoryException, LockException, ConstraintViolationException, VersionException { + Property p = testRootNode.setProperty(propertyName1, testValue); + testRootNode.refresh(false); + + try { + p.getString(); + fail("Refresh 'false' must invalidate a new child property"); + } catch (InvalidItemStateException e) { + // ok + } + assertFalse("Refresh 'false' must remove a new child property", testRootNode.hasProperty(propertyName1)); + } + + public void testRemovedNewProperty() throws RepositoryException, LockException, ConstraintViolationException, VersionException { + Property p = testRootNode.setProperty(propertyName1, testValue); + p.remove(); + + testRootNode.refresh(false); + + try { + p.getString(); + fail("Refresh 'false' must not bring a removed new child property back to life."); + } catch (InvalidItemStateException e) { + // ok + } + assertFalse("Refresh 'false' must not bring a removed new child property back to life.", testRootNode.hasProperty(propertyName1)); + } + + public void testRemovedProperty() throws RepositoryException, LockException, ConstraintViolationException, VersionException { + Property p = testRootNode.setProperty(propertyName1, testValue); + testRootNode.save(); + + p.remove(); + testRootNode.refresh(false); + + // Property p must be reverted to 'existing' -> getString must succeed. + p.getString(); + // similarly accessing the property again must succeed. + testRootNode.getProperty(propertyName1); + } + + public void testShadowingProperty() throws RepositoryException, LockException, ConstraintViolationException, VersionException { + Property p = testRootNode.setProperty(propertyName1, testValue); + testRootNode.save(); + + p.remove(); + Property pNew = testRootNode.setProperty(propertyName1, "SomeOtherTestValue"); + + testRootNode.refresh(false); + + try { + pNew.getString(); + fail("Refresh 'false' must remove a new (shadowing) property and bring 'removed' persistent property back to life."); + } catch (InvalidItemStateException e) { + // ok + } + + // Property p must be reverted to 'existing' -> getString must succeed. + p.getString(); + // similarly accessing the property again must succeed. + Property pAgain = testRootNode.getProperty(propertyName1); + assertTrue("Refresh 'false' must remove a new property and bring 'removed' persistent property back to life.", p.isSame(pAgain)); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshMovedTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshMovedTest.java new file mode 100644 index 00000000000..f841c3b260b --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshMovedTest.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.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RefreshMovedTest... + */ +public class RefreshMovedTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(RefreshMovedTest.class); + + protected Node moveNode; + protected String srcPath; + protected String destinationPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create parent node + Node srcParentNode = testRootNode.addNode(nodeName1, testNodeType); + // create node to be moved + moveNode = srcParentNode.addNode(nodeName2, testNodeType); + // create a node that will serve as new parent + Node destParentNode = testRootNode.addNode(nodeName3, testNodeType); + // save the new nodes + testRootNode.save(); + + srcPath = moveNode.getPath(); + destinationPath = destParentNode.getPath() + "/" + nodeName2; + } + + @Override + protected void tearDown() throws Exception { + moveNode = null; + super.tearDown(); + } + + /** + * Test if refresh(true) does not affect a moved node. + * + * @throws RepositoryException + */ + public void testRefreshTrue() throws RepositoryException { + testRootNode.getSession().move(srcPath, destinationPath); + testRootNode.getSession().refresh(true); + + assertTrue("Refresh with pending move operation must not remove the node at destination path.", testRootNode.getSession().itemExists(destinationPath)); + assertFalse("Refresh with pending move operation must not re-add the node at its original position.", testRootNode.getSession().itemExists(srcPath)); + assertFalse("Refresh with pending move operation must not re-add the node at its original position.", srcPath.equals(moveNode.getPath())); + } + + /** + * Test if refresh(false) affecting a node that has been moved by another + * session invalidates the node properly in termes of either moving it to + * the new destination or marking it 'removed'. + * + * @throws RepositoryException + */ + public void testRefreshOtherSession() throws RepositoryException { + Session readSession = getHelper().getReadOnlySession(); + try { + Node anotherNode = (Node) readSession.getItem(srcPath); + // workspace move + testRootNode.getSession().getWorkspace().move(srcPath, destinationPath); + + readSession.refresh(false); + try { + String p = anotherNode.getPath(); + // unless InvalidItemStateException is thrown the node must have + // been 'moved' to its new position. + assertTrue("Upon refresh of a node moved by another session it must be moved to the new destination (or removed).", p.equals(destinationPath)); + } catch (InvalidItemStateException e) { + // ok as well. + } + } finally { + readSession.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshTrueTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshTrueTest.java new file mode 100644 index 00000000000..c266d5be200 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RefreshTrueTest.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.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RefreshTrue... + */ +public class RefreshTrueTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(RefreshTrueTest.class); + + private Value testValue; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + testValue = testRootNode.getSession().getValueFactory().createValue("anyString"); + if (!testRootNode.getPrimaryNodeType().canSetProperty(propertyName1, testValue)) { + throw new NotExecutableException(""); + } + } + + @Override + protected void tearDown() throws Exception { + testValue = null; + super.tearDown(); + } + + public void testNewNode() throws RepositoryException { + Node n = testRootNode.addNode(nodeName2); + Property p = n.setProperty(propertyName1, testValue); + testRootNode.refresh(true); + + // n must still be new and accessible + String msg = "Refresh 'true' must not affect the new Node/Property."; + assertTrue(msg, testRootNode.hasNode(nodeName2)); + assertTrue(msg, n.isNew()); + assertTrue(msg, n.hasProperty(propertyName1)); + + // p must still be accessible + p.getString(); + assertTrue(msg, p.isSame(n.getProperty(propertyName1))); + } + + public void testNewProperty() throws RepositoryException { + Property p = testRootNode.setProperty(propertyName1, testValue); + testRootNode.refresh(true); + + // p must still be accessible + p.getString(); + assertTrue("Refresh 'true' must not affect a new Property.", testRootNode.hasProperty(propertyName1)); + Property pAgain = testRootNode.getProperty(propertyName1); + assertTrue("Refresh 'true' must not affect a new Property.", p.isSame(pAgain)); + } + + public void testRemovedProperty() throws RepositoryException { + Property p = testRootNode.setProperty(propertyName1, testValue); + testRootNode.save(); + + p.remove(); + testRootNode.refresh(true); + + // Property p must remain removed + try { + p.getString(); + fail("Refresh 'true' must not revert removal of an item."); + } catch (InvalidItemStateException e) { + //ok + } + assertFalse("Refresh 'true' must not revert removal of an item.", testRootNode.hasProperty(propertyName1)); + } + + public void testRemovedNewItem() throws RepositoryException { + Node n = testRootNode.addNode(nodeName2); + Property p = n.setProperty(propertyName1, testValue); + n.remove(); + + testRootNode.refresh(true); + + // n must still be new and accessible + String msg = "Refresh 'true' must revert the removal of new a Node/Property."; + assertFalse(msg, testRootNode.hasNode(nodeName2)); + assertFalse(msg, n.isNew() && n.isModified()); + assertFalse(msg, p.isNew() && p.isModified()); + try { + n.hasProperty(propertyName1); + fail(msg); + } catch (InvalidItemStateException e) { + // success + } + try { + p.getString(); + fail(msg); + } catch (InvalidItemStateException e) { + // success + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveItemTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveItemTest.java new file mode 100644 index 00000000000..47736d8d876 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveItemTest.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.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveItemTest... + */ +public abstract class RemoveItemTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(RemoveItemTest.class); + + protected Item removeItem; + protected String removePath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + removeItem = createRemoveItem(); + removePath = removeItem.getPath(); + } + + @Override + protected void tearDown() throws Exception { + removeItem = null; + super.tearDown(); + } + + protected abstract Item createRemoveItem() throws NotExecutableException, RepositoryException, LockException, ConstraintViolationException, ItemExistsException, NoSuchNodeTypeException, VersionException; + + /** + * Transiently removes a persisted item using {@link javax.jcr.Item#remove()} + * and test, whether that item cannot be access from the session any more. + */ + public void testRemoveItem() throws RepositoryException { + removeItem.remove(); + + // check if the node has been properly removed + try { + superuser.getItem(removePath); + fail("A transiently removed item should no longer be accessible from the session."); + } catch (PathNotFoundException e) { + // ok , works as expected + } + } + + /** + * Same as {@link #testRemoveItem()}, but calls save() (persisting the removal) + * before executing the test. + */ + public void testRemoveItem2() throws RepositoryException, NotExecutableException { + removeItem.remove(); + testRootNode.save(); + try { + superuser.getItem(removePath); + fail("Persistently removed node should no longer be accessible from the session."); + } catch (PathNotFoundException e) { + // ok , works as expected + } + } + /** + * Test if a node, that has been transiently removed is not 'New'. + */ + public void testNotNewRemovedItem() throws RepositoryException { + removeItem.remove(); + assertFalse("Transiently removed node must not be 'new'.", removeItem.isNew()); + } + + /** + * Same as {@link #testNotNewRemovedItem()} but calls save() before + * executing the test. + */ + public void testNotNewRemovedItem2() throws RepositoryException { + removeItem.remove(); + testRootNode.save(); + assertFalse("Removed node must not be 'new'.", removeItem.isNew()); + } + + /** + * Test if a node, that has be transiently remove is not 'Modified'. + */ + public void testNotModifiedRemovedItem() throws RepositoryException { + removeItem.remove(); + assertFalse("Transiently removed node must not be 'modified'.", removeItem.isModified()); + } + + /** + * Same as {@link #testNotModifiedRemovedItem()} but calls save() before + * executing the test. + */ + public void testNotModifiedRemovedItem2() throws RepositoryException { + removeItem.remove(); + testRootNode.save(); + assertFalse("Removed node must not be 'modified'.", removeItem.isModified()); + } + + /** + * A removed item must throw InvalidItemStateException upon any call to an + * item specific method. + */ + public void testInvalidStateRemovedItem() throws RepositoryException { + removeItem.remove(); + try { + removeItem.getName(); + fail("Calling getName() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + removeItem.getPath(); + fail("Calling getPath() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + removeItem.save(); + fail("Calling save() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + } + + /** + * Same as {@link #testInvalidStateRemovedItem()} but calls save() before + * executing the test. + */ + public void testInvalidStateRemovedItem2() throws RepositoryException { + removeItem.remove(); + testRootNode.save(); + try { + removeItem.getName(); + fail("Calling getName() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + removeItem.getPath(); + fail("Calling getPath() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + removeItem.save(); + fail("Calling save() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveMovedNodeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveMovedNodeTest.java new file mode 100644 index 00000000000..6b956483c60 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveMovedNodeTest.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.jcr2spi; + +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** RemoveMovedNodeTest... */ +public class RemoveMovedNodeTest extends AbstractMoveTest { + + private static Logger log = LoggerFactory.getLogger(RemoveMovedNodeTest.class); + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + @Override + protected boolean isSessionMove() { + return true; + } + + public void testRemove() throws RepositoryException { + String srcPath = moveNode.getPath(); + doMove(srcPath, destinationPath); + + // now remove the moved node + testRootNode.getSession().getItem(destinationPath).remove(); + assertFalse(superuser.itemExists(srcPath)); + assertFalse(superuser.itemExists(destinationPath)); + + testRootNode.save(); + + assertFalse(superuser.itemExists(srcPath)); + assertFalse(superuser.itemExists(destinationPath)); + assertFalse(destParentNode.isModified()); + assertFalse(srcParentNode.isModified()); + } + + public void testRevert() throws RepositoryException { + String srcPath = moveNode.getPath(); + doMove(srcPath, destinationPath); + + // now remove the moved node + testRootNode.getSession().getItem(destinationPath).remove(); + testRootNode.refresh(false); + + assertTrue(superuser.itemExists(srcPath)); + assertFalse(superuser.itemExists(destinationPath)); + + assertFalse(moveNode.isModified()); + assertFalse(destParentNode.isModified()); + assertFalse(srcParentNode.isModified()); + } + + public void testRefresh() throws RepositoryException { + String srcPath = moveNode.getPath(); + doMove(srcPath, destinationPath); + + // now remove the moved node + testRootNode.getSession().getItem(destinationPath).remove(); + testRootNode.refresh(true); + + assertFalse(superuser.itemExists(srcPath)); + assertFalse(superuser.itemExists(destinationPath)); + + // after removal the 'modified' flag is removed from the moved node + assertFalse(moveNode.isModified()); + // however: parent states are still modified + assertTrue(destParentNode.isModified()); // TODO: check if correct. + assertTrue(srcParentNode.isModified()); + } + + public void testRemoveSrcParent() throws RepositoryException { + String srcPath = moveNode.getPath(); + doMove(srcPath, destinationPath); + + // now remove the moved node + srcParentNode.remove(); + + assertFalse(superuser.itemExists(srcPath)); + assertTrue(superuser.itemExists(destinationPath)); + + assertTrue(moveNode.isModified()); + assertTrue(destParentNode.isModified()); + + // a removed item is not modified (although it was modified by the move above) + assertFalse(srcParentNode.isModified()); + + testRootNode.refresh(false); + assertTrue(superuser.itemExists(srcPath)); + assertFalse(superuser.itemExists(destinationPath)); + + assertFalse(moveNode.isModified()); + assertFalse(destParentNode.isModified()); + assertFalse(srcParentNode.isModified()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveNewNodeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveNewNodeTest.java new file mode 100644 index 00000000000..8e89cae205c --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveNewNodeTest.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.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveNodeTest... + */ +public class RemoveNewNodeTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(RemoveNewNodeTest.class); + + protected Node removeNode; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (testRootNode.hasNode(nodeName1)) { + throw new NotExecutableException("Parent node must not yet contain a child node '" + nodeName1 + "'."); + } + removeNode = testRootNode.addNode(nodeName1, testNodeType); + } + + @Override + protected void tearDown() throws Exception { + removeNode = null; + super.tearDown(); + } + + /** + * Removes a transient node using {@link javax.jcr.Node#remove()}. + */ + public void testRemoveNode() throws RepositoryException { + // create the transient node + removeNode.remove(); + try { + testRootNode.getNode(nodeName1); + fail("Removed transient node should no longer be accessible from parent node."); + } catch (PathNotFoundException e) { + // ok , works as expected + } + } + + /** + * Test if a node, that has be transiently added and removed is not 'New'. + */ + public void testNotNewRemovedNode() throws RepositoryException { + removeNode.remove(); + assertFalse("Removed transient node must not be 'new'.", removeNode.isNew()); + } + + /** + * Test if a node, that has be transiently added and removed is not 'Modified'. + */ + public void testNotModifiedRemovedNode() throws RepositoryException { + removeNode.remove(); + assertFalse("Removed transient node must not be 'modified'.", removeNode.isModified()); + } + + /** + * A removed transient node must throw InvalidItemStateException upon any call to a + * node specific method. + */ + public void testInvalidStateRemovedNode() throws RepositoryException { + removeNode.remove(); + try { + removeNode.getName(); + fail("Calling getName() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + removeNode.getPath(); + fail("Calling getPath() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + removeNode.getPrimaryNodeType(); + fail("Calling getPrimaryNodeType() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + removeNode.getProperty(jcrPrimaryType); + fail("Calling getProperty(String) on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + removeNode.save(); + fail("Calling save() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveNodeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveNodeTest.java new file mode 100644 index 00000000000..34b1eb73950 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveNodeTest.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveNodeTest... + */ +public class RemoveNodeTest extends RemoveItemTest { + + private static Logger log = LoggerFactory.getLogger(RemoveNodeTest.class); + + @Override + protected Item createRemoveItem() throws NotExecutableException, RepositoryException { + if (testRootNode.hasNode(nodeName1)) { + throw new NotExecutableException("Parent node must not yet contain a child node '" + nodeName1 + "'."); + } + Node removeNode = testRootNode.addNode(nodeName1, testNodeType); + // make sure the new node is persisted. + testRootNode.save(); + + return removeNode; + } + + /** + * Transiently removes a persisted node using {@link javax.jcr.Node#remove()} + * and test, whether that node cannot be access from its parent. + */ + public void testRemoveNode() throws RepositoryException { + removeItem.remove(); + + // check if the node has been properly removed + try { + String relPath = removePath.substring(removePath.lastIndexOf('/') + 1); + testRootNode.getNode(relPath); + fail("Transiently removed node should no longer be accessible from parent node."); + } catch (PathNotFoundException e) { + // ok , works as expected + } + } + + /** + * Same as {@link #testRemoveNode()}, but calls save() (persisting the removal) + * before executing the test. + */ + public void testRemoveNode2() throws RepositoryException, NotExecutableException { + removeItem.remove(); + testRootNode.save(); + try { + String relPath = removePath.substring(removePath.lastIndexOf('/') + 1); + testRootNode.getNode(relPath); + fail("Persistently removed node should no longer be accessible from parent node."); + } catch (PathNotFoundException e) { + // ok , works as expected + } + } + + /** + * A removed node must throw InvalidItemStateException upon any call to a + * node specific method. + */ + public void testInvalidStateRemovedNode() throws RepositoryException { + removeItem.remove(); + try { + ((Node)removeItem).getPrimaryNodeType(); + fail("Calling getPrimaryNodeType() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + ((Node)removeItem).getProperty(jcrPrimaryType); + fail("Calling getProperty(String) on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + } + + /** + * Same as {@link #testInvalidStateRemovedNode()} but calls save() before + * executing the test. + */ + public void testInvalidStateRemovedNode2() throws RepositoryException { + removeItem.remove(); + testRootNode.save(); + + try { + ((Node)removeItem).getPrimaryNodeType(); + fail("Calling getPrimaryNodeType() on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + ((Node)removeItem).getProperty(jcrPrimaryType); + fail("Calling getProperty(String) on a removed node must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + } + + public void testInvalidStateRemovedNode3() throws RepositoryException { + Node childNode = testRootNode.addNode(nodeName1, testNodeType); + superuser.save(); + + // get the node with session 2 + Session otherSession = getHelper().getReadWriteSession(); + try { + Node childNode2 = (Node) otherSession.getItem(childNode.getPath()); + + childNode.remove(); + superuser.save(); + + // try to remove already removed node with session 2 + try { + childNode2.refresh(false); + childNode2.remove(); + otherSession.save(); + fail("Removing a node already removed by other session should throw an InvalidItemStateException!"); + } catch (InvalidItemStateException e) { + //ok, works as expected + } + } finally { + otherSession.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemovePropertyTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemovePropertyTest.java new file mode 100644 index 00000000000..281aad33724 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemovePropertyTest.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.jcr2spi; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveNodeTest... + */ +public class RemovePropertyTest extends RemoveItemTest { + + private static Logger log = LoggerFactory.getLogger(RemovePropertyTest.class); + + @Override + protected Item createRemoveItem() throws NotExecutableException, RepositoryException, LockException, ConstraintViolationException, ItemExistsException, NoSuchNodeTypeException, VersionException { + Property removeProperty; + if (testRootNode.hasProperty(propertyName1)) { + removeProperty = testRootNode.getProperty(propertyName1); + if (removeProperty.getDefinition().isProtected() || removeProperty.getDefinition().isMandatory()) { + throw new NotExecutableException("Property to be remove must be mandatory nor protected '" + propertyName1 + "'."); + } + } else { + removeProperty = testRootNode.setProperty(propertyName1, "anyString"); + } + // make sure the new node is persisted. + testRootNode.save(); + return removeProperty; + } + + /** + * Transiently removes a persisted property using {@link Property#remove()} + * and test, whether that property cannot be access from its parent. + */ + public void testRemoveProperty() throws RepositoryException { + removeItem.remove(); + // check if the property has been properly removed + try { + testRootNode.getProperty(propertyName1); + fail("Transiently removed property should no longer be accessible from parent node."); + } catch (PathNotFoundException e) { + // ok , works as expected + } + } + + /** + * Same as {@link #testRemoveProperty()}, but calls save() (persisting the removal) + * before executing the test. + */ + public void testRemoveProperty2() throws RepositoryException, NotExecutableException { + removeItem.remove(); + testRootNode.save(); + try { + testRootNode.getProperty(propertyName1); + fail("Permanently removed property should no longer be accessible from parent node."); + } catch (PathNotFoundException e) { + // ok , works as expected + } + } + + /** + * A removed property must throw InvalidItemStateException upon any call to a + * property specific method. + */ + public void testInvalidStateRemovedProperty() throws RepositoryException { + removeItem.remove(); + + try { + ((Property)removeItem).getType(); + fail("Calling getType() on a removed property must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + ((Property)removeItem).getValue(); + fail("Calling getValue() on a removed property must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + } + + /** + * Same as {@link #testInvalidStateRemovedProperty()} but calls save() before + * executing the test. + */ + public void testInvalidStateRemovedProperty2() throws RepositoryException { + removeItem.remove(); + testRootNode.save(); + try { + ((Property)removeItem).getType(); + fail("Calling getType() on a removed property must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + + try { + ((Property)removeItem).getValue(); + fail("Calling getValue() on a removed property must throw InvalidItemStateException."); + } catch (InvalidItemStateException e) { + //ok + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveReferenceableNodeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveReferenceableNodeTest.java new file mode 100644 index 00000000000..871aa124802 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveReferenceableNodeTest.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.jcr2spi; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveNodeTest... + */ +public class RemoveReferenceableNodeTest extends RemoveNodeTest { + + private static Logger log = LoggerFactory.getLogger(RemoveReferenceableNodeTest.class); + + private String uuid; + + @Override + protected Item createRemoveItem() throws NotExecutableException, RepositoryException { + Node removeItem = (Node) super.createRemoveItem(); + // assert removeNode is referenceable + if (!removeItem.isNodeType(mixReferenceable)) { + if (!removeItem.canAddMixin(mixReferenceable)) { + throw new NotExecutableException("Cannot make remove-node '" + nodeName1 + "' mix:referenceable."); + } + removeItem.addMixin(mixReferenceable); + } + + // make sure the new node is persisted. + testRootNode.save(); + uuid = removeItem.getUUID(); + return removeItem; + } + + /** + * Transiently removes a persisted node using {@link javax.jcr.Node#remove()} + * and test, whether that node cannot be access by the UUID any more. + */ + public void testAccessByUUID() throws RepositoryException { + removeItem.remove(); + // check if the node has been properly removed + try { + superuser.getNodeByUUID(uuid); + fail("Transiently removed node should no longer be accessible from parent node."); + } catch (ItemNotFoundException e) { + // ok , works as expected + } + } + + /** + * Same as {@link #testRemoveNode()}, but calls save() before executing the + * test. + */ + public void testAccessByUUID2() throws RepositoryException, NotExecutableException { + removeItem.remove(); + testRootNode.save(); + try { + superuser.getNodeByUUID(uuid); + fail("Permanently removed node should no longer be accessible from parent node."); + } catch (ItemNotFoundException e) { + // ok , works as expected + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveSNSTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveSNSTest.java new file mode 100644 index 00000000000..7afd7828da2 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RemoveSNSTest.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.jcr2spi; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveSNSTest (Implementation specific tests. JSR170 only + * expects orderable same-name-siblings to have a consistent and testable + * order.) + */ +public class RemoveSNSTest extends RemoveNodeTest { + + private static Logger log = LoggerFactory.getLogger(RemoveSNSTest.class); + + private Node firstSiblingNode; + private String firstSiblingPath; + + @Override + protected void tearDown() throws Exception { + firstSiblingNode = null; + super.tearDown(); + } + + @Override + protected Item createRemoveItem() throws NotExecutableException, RepositoryException { + if (testRootNode.hasNode(nodeName1)) { + fail("Setup: Parent node must not yet contain a child node '" + nodeName1 + "'."); + } + firstSiblingNode = testRootNode.addNode(nodeName1, testNodeType); + if (!firstSiblingNode.getDefinition().allowsSameNameSiblings()) { + fail("Setup: RemoveSNSTest cannot be execute. Unable to create SameNameSiblings."); + } + firstSiblingPath = firstSiblingNode.getPath(); + + Node removeNode = testRootNode.addNode(nodeName1, testNodeType); + // make sure the new node is persisted. + testRootNode.save(); + return removeNode; + } + + /** + * Transiently removes the first SNS-node using {@link javax.jcr.Node#remove()} + * and test, whether the remaining sibling 'replaces' the removed node and + * is the same as the node added as second sibling. + */ + public void testRemoveFirstSibling() throws RepositoryException { + firstSiblingNode.remove(); + + // check if the node has been properly removed + try { + Node secondSibling = testRootNode.getNode(nodeName1); + // implementation specific: + assertTrue("", removeItem.isSame(secondSibling)); + } catch (PathNotFoundException e) { + fail("Second sibling must still be available."); + } + } + + /** + * Same as {@link #testRemoveNode()}, but calls save() (persisting the removal) + * before executing the test. + */ + public void testRemoveFirstSibling2() throws RepositoryException, NotExecutableException { + firstSiblingNode.remove(); + testRootNode.save(); + + // check if the node has been properly removed + try { + Node secondSibling = testRootNode.getNode(nodeName1); + // implementation specific: + assertTrue("", removeItem.isSame(secondSibling)); + } catch (PathNotFoundException e) { + fail("Second sibling must still be available."); + } + } + + /** + * Transiently removes a persisted item using {@link javax.jcr.Item#remove()} + * and test, whether the successor sibling is returned when retrieving the + * item with the path of the removed node. + */ + public void testRemoveFirstSibling3() throws RepositoryException { + firstSiblingNode.remove(); + + // check if the node has been properly removed + try { + Item secondSibling = superuser.getItem(firstSiblingPath); + // implementation specific: + assertTrue("", removeItem.isSame(secondSibling)); + } catch (PathNotFoundException e) { + fail("Removing a SNS Node -> successor must be accessible from the session by removed path."); + } + } + + /** + * Same as {@link #testRemoveFirstSibling3()} but calls save() before + * executing the test. + */ + public void testRemoveFirstSibling4() throws RepositoryException { + firstSiblingNode.remove(); + testRootNode.save(); + + // check if the node has been properly removed + try { + Item secondSibling = superuser.getItem(firstSiblingPath); + // implementation specific: + assertTrue("", removeItem.isSame(secondSibling)); + } catch (PathNotFoundException e) { + fail("Removing a SNS Node -> successor must be accessible from the session by removed path."); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RenameTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RenameTest.java new file mode 100644 index 00000000000..ba30d69132b --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RenameTest.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.jcr2spi; + +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RenameTest... + */ +public class RenameTest extends AbstractMoveTest { + + private static Logger log = LoggerFactory.getLogger(RenameTest.class); + + private String renamedName; + private String renamePath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + renamedName = "renamed"; + renamePath = srcParentNode.getPath() + "/" + renamedName; + } + + @Override + protected boolean isSessionMove() { + return true; + } + + public void testRename() throws RepositoryException { + doMove(moveNode.getPath(), renamePath); + assertEquals(moveNode.getName(), renamedName); + superuser.save(); + assertEquals(moveNode.getName(), renamedName); + } + + public void testRevertRename() throws RepositoryException { + doMove(moveNode.getPath(), renamePath); + assertEquals(moveNode.getName(), renamedName); + + superuser.refresh(false); + assertEquals(moveNode.getName(), nodeName2); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderMixedTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderMixedTest.java new file mode 100644 index 00000000000..b9135e6175e --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderMixedTest.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.jcr2spi; + +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReorderMixedTest... + */ +public class ReorderMixedTest extends ReorderTest { + + private static Logger log = LoggerFactory.getLogger(ReorderMixedTest.class); + + @Override + protected void createOrderableChildren() throws RepositoryException, LockException, ConstraintViolationException, NoSuchNodeTypeException, ItemExistsException, VersionException { + child1 = testRootNode.addNode(nodeName2, testNodeType); + child2 = testRootNode.addNode(nodeName4, testNodeType); + child3 = testRootNode.addNode(nodeName2, testNodeType); + child4 = testRootNode.addNode(nodeName2, testNodeType); + + testRootNode.save(); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderMoveTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderMoveTest.java new file mode 100644 index 00000000000..5cef01f6f7a --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderMoveTest.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReorderMoveTest testing various combinations of move/rename + * and reorder with and without intermediate save, revert and other transient + * modifications. + */ +public class ReorderMoveTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(ReorderMoveTest.class); + + private Node destParent; + private Node srcParent; + private String destPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!testRootNode.getPrimaryNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Test node does not have orderable children."); + } + + // create move-destination + destParent = testRootNode.addNode(nodeName4, testNodeType); + srcParent = testRootNode.addNode(nodeName2, testNodeType); + + destPath = destParent.getPath() + "/" + nodeName3; + testRootNode.save(); + } + + @Override + protected void tearDown() throws Exception { + destParent = null; + srcParent = null; + super.tearDown(); + } + + private Node[] createOrderableChildren(boolean sns) throws RepositoryException { + String[] childNames; + if (sns) { + childNames = new String[] {nodeName2, nodeName2, nodeName2, nodeName2}; + } else { + childNames = new String[] {nodeName1, nodeName2, nodeName3, nodeName4}; + } + Node[] children = new Node[4]; + children[0] = srcParent.addNode(childNames[0], testNodeType); + children[1] = srcParent.addNode(childNames[1], testNodeType); + children[2] = srcParent.addNode(childNames[2], testNodeType); + children[3] = srcParent.addNode(childNames[3], testNodeType); + + testRootNode.save(); + return children; + } + + private static String getRelPath(Node child) throws RepositoryException { + if (child == null) { + return null; + } + String path = child.getPath(); + return path.substring(path.lastIndexOf('/')+1); + } + + private static void testOrder(Node parent, Node[] children) throws RepositoryException { + NodeIterator it = parent.getNodes(); + int i = 0; + while (it.hasNext()) { + Node child = it.nextNode(); + assertTrue(child.isSame(children[i])); + i++; + } + } + + /** + * Move a orderable child node and reorder the remaining nodes. + */ + public void testMoveAndReorder() throws RepositoryException { + Node[] children = createOrderableChildren(false); + String oldName = children[2].getName(); + // move + testRootNode.getSession().move(children[2].getPath(), destPath); + // reorder + srcParent.orderBefore(getRelPath(children[1]), null); + testOrder(srcParent, new Node[] {children[0], children[3], children[1]}); + + testRootNode.save(); + testOrder(srcParent, new Node[] {children[0], children[3], children[1]}); + assertFalse(srcParent.hasNode(oldName)); + } + + /** + * Move a orderable SNS-node and reorder the remaining nodes at source-parent. + */ + public void testMoveAndReorderSNS() throws RepositoryException { + Node[] children = createOrderableChildren(true); + String snsName = children[0].getName(); + + // move + testRootNode.getSession().move(children[2].getPath(), destPath); + testRootNode.getSession().move(children[1].getPath(), destPath); + + // reorder + srcParent.orderBefore(getRelPath(children[0]), null); + testOrder(srcParent, new Node[] {children[3], children[0]}); + assertTrue(srcParent.hasNode(snsName+"[1]")); + assertTrue(srcParent.hasNode(snsName+"[2]")); + assertFalse(srcParent.hasNode(snsName+"[3]")); + assertFalse(srcParent.hasNode(snsName+"[4]")); + assertFalse(srcParent.hasNode(snsName+"[5]")); + + testRootNode.save(); + testOrder(srcParent, new Node[] {children[3], children[0]}); + assertTrue(srcParent.hasNode(snsName+"[1]")); + assertTrue(srcParent.hasNode(snsName+"[2]")); + assertFalse(srcParent.hasNode(snsName+"[3]")); + assertFalse(srcParent.hasNode(snsName+"[4]")); + assertFalse(srcParent.hasNode(snsName+"[5]")); + + // check if move have been successful + assertEquals(children[2].getPath(), destPath); + assertTrue(children[2].getIndex() == Path.INDEX_DEFAULT); + assertEquals(children[1].getPath(), destPath+"[2]"); + } + + /** + * Reorder nodes and move one of the reordered siblings + * away. Test the ordering of the remaining siblings. + */ + public void testReorderAndMove() throws RepositoryException { + Node[] children = createOrderableChildren(false); + + // reorder first + srcParent.orderBefore(getRelPath(children[0]), null); + srcParent.orderBefore(getRelPath(children[3]), getRelPath(children[1])); + // move + testRootNode.getSession().move(children[3].getPath(), destPath); + + testOrder(srcParent, new Node[] {children[1], children[2], children[0]}); + + testRootNode.save(); + testOrder(srcParent, new Node[] {children[1], children[2], children[0]}); + } + + /** + * Reorder same-name-sibling nodes and move one of the reordered siblings + * away. Test the ordering of the remaining siblings. + */ + public void testReorderAndMoveSNS() throws RepositoryException { + Node[] children = createOrderableChildren(true); + + // reorder first + srcParent.orderBefore(getRelPath(children[0]), null); + srcParent.orderBefore(getRelPath(children[3]), getRelPath(children[1])); + // move + testRootNode.getSession().move(children[3].getPath(), destPath); + + testOrder(srcParent, new Node[] {children[1], children[2], children[0]}); + + testRootNode.save(); + testOrder(srcParent, new Node[] {children[1], children[2], children[0]}); + } + + /** + * Any attempt reorder a moved node at its original position must fail. + */ + public void testReorderMovedNode() throws RepositoryException { + Node[] children = createOrderableChildren(false); + + String relPath = getRelPath(children[2]); + testRootNode.getSession().move(children[2].getPath(), destPath); + + try { + srcParent.orderBefore(relPath, null); + fail("Reordering a child node that has been moved away must fail."); + } catch (ItemNotFoundException e) { + // ok + } + } + + /** + * Move a SNS-node and reorder its original siblings afterwards. + * Test if reverting the changes results in the original ordering and + * hierarchy. + */ + public void testRevertMoveAndReorderSNS() throws RepositoryException { + Node[] children = createOrderableChildren(true); + // move then reorder + testRootNode.getSession().move(children[2].getPath(), destPath); + srcParent.orderBefore(getRelPath(children[1]), null); + srcParent.orderBefore(getRelPath(children[3]), getRelPath(children[0])); + + testRootNode.refresh(false); + testOrder(srcParent, new Node[] {children[0], children[1], children[2], children[3]}); + assertFalse(destParent.hasNode(Text.getName(destPath))); + } + + /** + * Move a SNS-node, that got its siblings reordered before. + * Test if reverting the changes results in the original ordering and + * hierarchy. + */ + public void testRevertReorderAndMoveSNS() throws RepositoryException { + Node[] children = createOrderableChildren(true); + // reorder then move + srcParent.orderBefore(getRelPath(children[1]), null); + srcParent.orderBefore(getRelPath(children[3]), getRelPath(children[2])); + srcParent.getSession().move(children[2].getPath(), destPath); + + testRootNode.refresh(false); + testOrder(srcParent, new Node[] {children[0], children[1], children[2], children[3]}); + assertFalse(destParent.hasNode(Text.getName(destPath))); + } + + /** + * Move a SNS-node, that has been reordered before. + * Test if reverting the changes results in the original ordering and + * hierarchy. + */ + public void testRevertMoveReorderedSNS() throws RepositoryException { + Node[] children = createOrderableChildren(true); + // reorder then move + srcParent.orderBefore(getRelPath(children[1]), null); + srcParent.orderBefore(getRelPath(children[3]), getRelPath(children[2])); + srcParent.getSession().move(children[1].getPath(), destPath); + + testRootNode.refresh(false); + testOrder(srcParent, new Node[] {children[0], children[1], children[2], children[3]}); + assertFalse(destParent.hasNode(Text.getName(destPath))); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewAndSavedTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewAndSavedTest.java new file mode 100644 index 00000000000..ea6f1e36f7c --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewAndSavedTest.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.jcr2spi; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReorderNewAndSavedTest... + */ +public class ReorderNewAndSavedTest extends ReorderTest { + + private static Logger log = LoggerFactory.getLogger(ReorderNewAndSavedTest.class); + + @Override + protected void createOrderableChildren() throws RepositoryException, LockException, ConstraintViolationException, NoSuchNodeTypeException, ItemExistsException, VersionException { + child1 = testRootNode.addNode(nodeName1, testNodeType); + child2 = testRootNode.addNode(nodeName2, testNodeType); + testRootNode.save(); + + child3 = testRootNode.addNode(nodeName3, testNodeType); + child4 = testRootNode.addNode(nodeName4, testNodeType); + } + + @Override + public void testRevertReorder() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child4), getRelPath(child2)); + testOrder(testRootNode, new Node[] { child1, child4, child2, child3}); + + testRootNode.refresh(false); + testOrder(testRootNode, new Node[] { child1, child2 }); + } + + @Override + public void testRevertReorderToEnd() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child1), null); + testOrder(testRootNode, new Node[] { child2, child3, child4, child1}); + + testRootNode.refresh(false); + testOrder(testRootNode, new Node[] { child1, child2 }); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewSNSTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewSNSTest.java new file mode 100644 index 00000000000..fc38d6108e1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewSNSTest.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.jcr2spi; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReorderNewSNSTest... + */ +public class ReorderNewSNSTest extends ReorderTest { + + private static Logger log = LoggerFactory.getLogger(ReorderNewSNSTest.class); + + @Override + protected void createOrderableChildren() throws RepositoryException, LockException, ConstraintViolationException, NoSuchNodeTypeException, ItemExistsException, VersionException { + child1 = testRootNode.addNode(nodeName2, testNodeType); + child2 = testRootNode.addNode(nodeName2, testNodeType); + child3 = testRootNode.addNode(nodeName2, testNodeType); + child4 = testRootNode.addNode(nodeName2, testNodeType); + } + + @Override + public void testRevertReorder() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child4), getRelPath(child2)); + testOrder(testRootNode, new Node[] { child1, child4, child2, child3}); + + // NEW child nodes -> must be removed upon refresh + testRootNode.refresh(false); + NodeIterator it = testRootNode.getNodes(nodeName2); + if (it.hasNext()) { + fail("Reverting creation and reordering of new SNSs must remove the children again."); + } + } + + @Override + public void testRevertReorderToEnd() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child1), null); + testOrder(testRootNode, new Node[] { child2, child3, child4, child1}); + + // NEW child nodes -> must be removed upon refresh + testRootNode.refresh(false); + NodeIterator it = testRootNode.getNodes(nodeName2); + if (it.hasNext()) { + fail("Reverting creation and reordering of new SNSs must remove the children again."); + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewTest.java new file mode 100644 index 00000000000..87ffab6b318 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderNewTest.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.jcr2spi; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReorderNewTest... + */ +public class ReorderNewTest extends ReorderTest { + + private static Logger log = LoggerFactory.getLogger(ReorderNewTest.class); + + @Override + protected void createOrderableChildren() throws RepositoryException, LockException, ConstraintViolationException, NoSuchNodeTypeException, ItemExistsException, VersionException { + child1 = testRootNode.addNode(nodeName1, testNodeType); + child2 = testRootNode.addNode(nodeName2, testNodeType); + child3 = testRootNode.addNode(nodeName3, testNodeType); + child4 = testRootNode.addNode(nodeName4, testNodeType); + } + + @Override + public void testRevertReorder() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child4), getRelPath(child2)); + testOrder(testRootNode, new Node[] { child1, child4, child2, child3}); + + // NEW child nodes -> must be removed upon refresh + testRootNode.refresh(false); + NodeIterator it = testRootNode.getNodes(); + if (it.hasNext()) { + fail("Reverting creation and reordering of new children must remove the children again."); + } + } + + @Override + public void testRevertReorderToEnd() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child1), null); + testOrder(testRootNode, new Node[] { child2, child3, child4, child1}); + + // NEW child nodes -> must be removed upon refresh + testRootNode.refresh(false); + NodeIterator it = testRootNode.getNodes(); + if (it.hasNext()) { + fail("Reverting creation and reordering of new children must remove the children again."); + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderReferenceableSNSTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderReferenceableSNSTest.java new file mode 100644 index 00000000000..2b8dc2200be --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderReferenceableSNSTest.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.jcr2spi; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReorderSNSTest... + */ +public class ReorderReferenceableSNSTest extends ReorderTest { + + private static Logger log = LoggerFactory.getLogger(ReorderReferenceableSNSTest.class); + + @Override + protected void createOrderableChildren() throws RepositoryException, NotExecutableException { + child1 = testRootNode.addNode(nodeName2, testNodeType); + child2 = testRootNode.addNode(nodeName2, testNodeType); + child3 = testRootNode.addNode(nodeName2, testNodeType); + child4 = testRootNode.addNode(nodeName2, testNodeType); + Node[] children = new Node[] { child1, child2, child3, child4}; + for (int i = 0; i < children.length; i++) { + if (children[i].canAddMixin(mixReferenceable)) { + children[i].addMixin(mixReferenceable); + } else { + throw new NotExecutableException(); + } + } + testRootNode.save(); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderSNSTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderSNSTest.java new file mode 100644 index 00000000000..5c3acf42e58 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderSNSTest.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.jcr2spi; + +import javax.jcr.Item; +import javax.jcr.ItemExistsException; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReorderSNSTest... + */ +public class ReorderSNSTest extends ReorderTest { + + private static Logger log = LoggerFactory.getLogger(ReorderSNSTest.class); + + @Override + protected void createOrderableChildren() throws RepositoryException, LockException, ConstraintViolationException, NoSuchNodeTypeException, ItemExistsException, VersionException { + child1 = testRootNode.addNode(nodeName2, testNodeType); + child2 = testRootNode.addNode(nodeName2, testNodeType); + child3 = testRootNode.addNode(nodeName2, testNodeType); + child4 = testRootNode.addNode(nodeName2, testNodeType); + + testRootNode.save(); + } + + public void testIndexAfterReorder() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child1), getRelPath(child3)); + assertTrue(child1.getIndex() == 2); + assertTrue(child2.getIndex() == 1); + assertTrue(child3.getIndex() == 3); + assertTrue(child4.getIndex() == 4); + + testRootNode.save(); + assertTrue(child1.getIndex() == 2); + assertTrue(child2.getIndex() == 1); + assertTrue(child3.getIndex() == 3); + assertTrue(child4.getIndex() == 4); + } + + public void testReorder3() throws RepositoryException { + String pathBefore = child3.getPath(); + + testRootNode.orderBefore(getRelPath(child3), getRelPath(child1)); + testRootNode.save(); + + Item itemIndex3 = testRootNode.getSession().getItem(pathBefore); + assertTrue(itemIndex3.isSame(child2)); + + Item item3 = testRootNode.getSession().getItem(child3.getPath()); + assertTrue(item3.isSame(child3)); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderTest.java new file mode 100644 index 00000000000..163b2dc5287 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReorderTest.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import javax.jcr.ItemExistsException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReorderTest... + */ +public class ReorderTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(ReorderTest.class); + + protected Node child1; + protected Node child2; + protected Node child3; + protected Node child4; + + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!testRootNode.getPrimaryNodeType().hasOrderableChildNodes()) { + throw new NotExecutableException("Test node does not have orderable children."); + } + NodeIterator it = testRootNode.getNodes(); + if (it.hasNext()) { + throw new NotExecutableException("Test node already contains child nodes"); + } + createOrderableChildren(); + } + + @Override + protected void tearDown() throws Exception { + child1 = null; + child2 = null; + child3 = null; + child4 = null; + super.tearDown(); + } + + protected void createOrderableChildren() throws RepositoryException, LockException, ConstraintViolationException, NoSuchNodeTypeException, ItemExistsException, VersionException, NotExecutableException { + child1 = testRootNode.addNode(nodeName1, testNodeType); + child2 = testRootNode.addNode(nodeName2, testNodeType); + child3 = testRootNode.addNode(nodeName3, testNodeType); + child4 = testRootNode.addNode(nodeName4, testNodeType); + + testRootNode.save(); + } + + protected static String getRelPath(Node child) throws RepositoryException { + if (child == null) { + return null; + } + String path = child.getPath(); + return path.substring(path.lastIndexOf('/')+1); + } + + protected static void testOrder(Node parent, Node[] children) throws RepositoryException { + NodeIterator it = parent.getNodes(); + int i = 0; + while (it.hasNext()) { + Node child = it.nextNode(); + if (i >= children.length) { + fail("Reorder added a child node."); + } + assertTrue("Wrong order of children: " + child + " is not the same as " + children[i], child.isSame(children[i])); + i++; + } + + if (i < children.length-1) { + fail("Reorder removed a child node."); + } + } + + public void testReorder() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child1), getRelPath(child3)); + testOrder(testRootNode, new Node[] { child2, child1, child3, child4}); + + testRootNode.save(); + testOrder(testRootNode, new Node[] { child2, child1, child3, child4}); + } + + public void testReorderToEnd() throws RepositoryException, ConstraintViolationException, UnsupportedRepositoryOperationException, VersionException { + testRootNode.orderBefore(getRelPath(child2), null); + testOrder(testRootNode, new Node[] { child1, child3, child4, child2}); + + testRootNode.save(); + testOrder(testRootNode, new Node[] { child1, child3, child4, child2}); + } + + public void testRevertReorder() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child4), getRelPath(child2)); + testOrder(testRootNode, new Node[] { child1, child4, child2, child3}); + + testRootNode.refresh(false); + testOrder(testRootNode, new Node[] { child1, child2, child3, child4}); + } + + public void testRevertReorderToEnd() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child1), null); + testOrder(testRootNode, new Node[] { child2, child3, child4, child1}); + + testRootNode.refresh(false); + testOrder(testRootNode, new Node[] { child1, child2, child3, child4}); + } + + public void testReorder2() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child3), getRelPath(child1)); + testRootNode.save(); + + Session otherSession = getHelper().getReadOnlySession(); + try { + testOrder((Node) otherSession.getItem(testRootNode.getPath()), new Node[] {child3, child1, child2, child4}); + } finally { + otherSession.logout(); + } + } + + public void testReorderTwice() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child2), null); + testRootNode.orderBefore(getRelPath(child4), getRelPath(child1)); + + testOrder(testRootNode, new Node[] { child4, child1, child3, child2}); + testRootNode.save(); + testOrder(testRootNode, new Node[] { child4, child1, child3, child2}); + } + + public void testReorderFinallyOriginalOrder() throws RepositoryException { + testRootNode.orderBefore(getRelPath(child4), getRelPath(child1)); + testRootNode.orderBefore(getRelPath(child3), getRelPath(child4)); + testRootNode.orderBefore(getRelPath(child2), getRelPath(child3)); + testRootNode.orderBefore(getRelPath(child1), getRelPath(child2)); + + testOrder(testRootNode, new Node[] { child1, child2, child3, child4}); + testRootNode.save(); + testOrder(testRootNode, new Node[] { child1, child2, child3, child4}); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReplaceNodeTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReplaceNodeTest.java new file mode 100644 index 00000000000..849f2de0ecd --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/ReplaceNodeTest.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ReplaceNodeTest + */ +public class ReplaceNodeTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(ReplaceNodeTest.class); + + private Node removeNode; + private String uuid; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + if (testRootNode.hasNode(nodeName1)) { + throw new NotExecutableException("Parent node must not yet contain a child node '" + nodeName1 + "'."); + } + removeNode = testRootNode.addNode(nodeName1, testNodeType); + // make sure the new node is persisted. + testRootNode.save(); + // assert removeNode is referenceable + if (!removeNode.isNodeType(mixReferenceable)) { + if (!removeNode.canAddMixin(mixReferenceable)) { + throw new NotExecutableException("Cannot make remove-node '" + nodeName1 + "' mix:referenceable."); + } + removeNode.addMixin(mixReferenceable); + testRootNode.save(); + } + uuid = removeNode.getUUID(); + } + + @Override + protected void tearDown() throws Exception { + removeNode = null; + super.tearDown(); + } + + public void testAddReplacementAfterRemove() throws RepositoryException { + // transient removal of the 'removeNode' + removeNode.remove(); + // add node that replaces the transiently removed node + Node n = testRootNode.addNode(nodeName2, testNodeType); + // ... and a child node. + n.addNode(nodeName3, testNodeType); + testRootNode.save(); + + try { + // if (for impl reasons) 'n' is referenceable -> it must have a + // different uuid. + assertFalse(uuid.equals(n.getUUID())); + } catch (UnsupportedRepositoryOperationException e) { + // n has not been made referenceable before -> OK. + } + } + + public void testAddReplacementAfterMove() throws RepositoryException { + // transiently move the 'removeNode' + superuser.move(removeNode.getPath(), testRootNode.getPath() + "/" + nodeName4); + // add node that replaces the moved node + Node n = testRootNode.addNode(nodeName1, testNodeType); + // ... and a child node. + n.addNode(nodeName2, testNodeType); + testRootNode.save(); + + try { + // if (for impl reasons) 'n' is referenceable -> it must have a + // different uuid. + assertFalse(uuid.equals(n.getUUID())); + } catch (UnsupportedRepositoryOperationException e) { + // n has not been made referenceable before -> OK. + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RevertMoveTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RevertMoveTest.java new file mode 100644 index 00000000000..6391589066f --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/RevertMoveTest.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.jcr2spi; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RevertMove... + */ +public class RevertMoveTest extends AbstractMoveTest { + + private static Logger log = LoggerFactory.getLogger(RevertMoveTest.class); + + @Override + protected boolean isSessionMove() { + return true; + } + + /** + * Implementation specific test, that expects that the scope of a refresh(false) + * must include all nodes affected by the operations that affected the + * subtree to be refreshed. + * + * @throws RepositoryException + */ + public void testRevertMovedNode() throws RepositoryException { + String srcPath = moveNode.getPath(); + doMove(srcPath, destinationPath); + Node afterMoveNode = (Node) testRootNode.getSession().getItem(destinationPath); + + try { + afterMoveNode.refresh(false); + fail("Node.refresh() on a transiently moved node should fail such as a 'save' would fail."); + } catch (RepositoryException e) { + // ok: works as expected. scope of 'refresh' is not complete + } + } + + /** + * Test if reverting all transient changes moves a moved node back to its + * original position. + * + * @throws RepositoryException + */ + public void testRevertMoveOperation() throws RepositoryException { + String srcPath = moveNode.getPath(); + doMove(srcPath, destinationPath); + + testRootNode.getSession().refresh(false); + assertFalse("Reverting the move operation must remove the node at destination path.", testRootNode.getSession().itemExists(destinationPath)); + assertTrue("Reverting the move operation must re-add the node at its original position.", testRootNode.getSession().itemExists(srcPath)); + assertTrue("Reverting the move operation must re-add the node at its original position.", srcPath.equals(moveNode.getPath())); + + assertFalse("The former destination must not be modified.", destParentNode.isModified()); + assertFalse("The parent must not be modified.", srcParentNode.isModified()); + assertFalse("The move-node must not be modified.", moveNode.isModified()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/SNSIndexTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/SNSIndexTest.java new file mode 100644 index 00000000000..cf800482e40 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/SNSIndexTest.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SNSIndexTest... + */ +public class SNSIndexTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(SNSIndexTest.class); + + private String snsName; + + private Node parent; + + private Node sns1; + private Node sns2; + private Node sns3; + private Node sns4; + + private String snsPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + snsName = nodeName2; + + parent = testRootNode.addNode(nodeName1, testNodeType); + // create sns-siblings + sns1 = parent.addNode(snsName, testNodeType); + sns2 = parent.addNode(snsName, testNodeType); + sns3 = parent.addNode(snsName, testNodeType); + sns4 = parent.addNode(snsName, testNodeType); + + testRootNode.save(); + + snsPath = testRootNode.getPath() + "/" + nodeName1 + "/" + snsName; + } + + @Override + protected void tearDown() throws Exception { + parent = null; + sns1 = null; + sns2 = null; + sns3 = null; + sns4 = null; + super.tearDown(); + } + + /** + * Test if index of the created nodes are as expected. + */ + public void testIndex() throws RepositoryException { + checkIndex(sns1, Path.INDEX_DEFAULT); + checkIndex(sns2, Path.INDEX_DEFAULT + 1); + checkIndex(sns3, Path.INDEX_DEFAULT + 2); + checkIndex(sns4, Path.INDEX_DEFAULT + 3); + } + + /** + * Test if index of the created nodes are as expected if they are accessed + * by another session. + */ + public void testIndexByOtherSession() throws RepositoryException { + Session otherSession = getHelper().getReadOnlySession(); + try { + for (int index = Path.INDEX_DEFAULT; index < 4; index++) { + Node sns = (Node) otherSession.getItem(buildPath(index)); + checkIndex(sns, index); + } + } finally { + otherSession.logout(); + } + } + + /** + * Test if passing an bigger index throws exception + */ + public void testNonExistingIndex() throws RepositoryException { + try { + superuser.getItem(buildPath(10)); + fail("Accessing item with non-existing index must throw PathNotFoundException."); + } catch (PathNotFoundException e) { + // ok + } + } + + /** + * Test if accessing a child node by sns-Name, the node with the default + * index is returned. + */ + public void testDefaultIndex() throws RepositoryException { + Node sns = parent.getNode(snsName); + checkIndex(sns, Path.INDEX_DEFAULT); + } + + /** + * Test if index of any node is correctly set, if the node is accessed + * without loading SNSs with lower index before + */ + public void testNodeEntriesFilledCorrectly() throws RepositoryException { + Session otherSession = getHelper().getReadOnlySession(); + try { + Node sns = (Node) otherSession.getItem(buildPath(3)); + checkIndex(sns, 3); + + sns = (Node) otherSession.getItem(buildPath(2)); + checkIndex(sns, 2); + + sns = (Node) otherSession.getItem(buildPath(4)); + checkIndex(sns, 4); + + // check 3 again + sns = (Node) otherSession.getItem(buildPath(3)); + checkIndex(sns, 3); + + // check default + sns = (Node) otherSession.getItem(buildPath(1)); + checkIndex(sns, 1); + } finally { + otherSession.logout(); + } + } + + /** + * Test if accessing the created nodes by name really returns all nodes. + */ + public void testGetNodesByName() throws RepositoryException { + NodeIterator it = parent.getNodes(snsName); + long size = it.getSize(); + if (size != -1) { + assertTrue("4 SNSs have been added -> but iterator size is " + size + ".", size == 4); + } + int expectedIndex = 1; + while (it.hasNext()) { + Node sns = it.nextNode(); + checkIndex(sns, expectedIndex); + expectedIndex++; + } + assertTrue("4 SNSs have been added -> but iterator size is " + size + ".", size == 4); + } + + /** + * Test if accessing the created nodes by name really returns all nodes. + */ + public void testGetNodesByNameByOtherSession() throws RepositoryException { + Session otherSession = getHelper().getReadOnlySession(); + try { + NodeIterator it = ((Node) otherSession.getItem(parent.getPath())).getNodes(snsName); + long size = it.getSize(); + if (size != -1) { + assertTrue("4 SNSs have been added -> but iterator size is " + size + ".", size == 4); + } + int expectedIndex = 1; + while (it.hasNext()) { + Node sns = it.nextNode(); + checkIndex(sns, expectedIndex); + expectedIndex++; + } + assertTrue("4 SNSs have been added -> but iterator size is " + size + ".", size == 4); + } finally { + otherSession.logout(); + } + + } + + private String buildPath(int index) { + return snsPath + "[" + index + "]"; + } + + private static void checkIndex(Node node, int expectedIndex) throws RepositoryException { + int index = node.getIndex(); + if (index != expectedIndex) { + fail("Unexpected index " + index + ". Expected index was " + expectedIndex); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/SingleValuedPropertyTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/SingleValuedPropertyTest.java new file mode 100644 index 00000000000..37c07bf1f43 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/SingleValuedPropertyTest.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.jcr2spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.PropertyUtil; + +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.ValueFormatException; + +/** + * SingleValuedPropertyTest... + */ +public class SingleValuedPropertyTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(SingleValuedPropertyTest.class); + + /** + * Tests if Property.getLengths() fails for single value property. + */ + public void testGetLengthsFromSingleValued() throws RepositoryException, NotExecutableException { + Property singleProp = PropertyUtil.searchSingleValuedProperty(testRootNode); + if (singleProp == null) { + throw new NotExecutableException("No single valued property found."); + } + try { + singleProp.getLengths(); + fail("Property.getLengths() called on a single value property must fail (ValueFormatException)."); + } catch (ValueFormatException vfe) { + // ok + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/TestAll.java new file mode 100644 index 00000000000..84f66eda6a6 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/TestAll.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.jcr2spi; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import org.apache.jackrabbit.jcr2spi.observation.ObservationTest; + +/** + * TestAll... + */ +public class TestAll extends TestCase { + + public static Test suite() { + + TestSuite suite = new TestSuite("jcr2spi tests"); + + suite.addTestSuite(AccessByRelativePathTest.class); + suite.addTestSuite(GetItemsTest.class); + + // get node(s) + suite.addTestSuite(SNSIndexTest.class); + suite.addTestSuite(NodeOrderTest.class); + + // add nodes + suite.addTestSuite(AddNodeTest.class); + + // set/add property + suite.addTestSuite(GetPropertyTest.class); + suite.addTestSuite(AddPropertyTest.class); + suite.addTestSuite(AddNewPropertyTest.class); + suite.addTestSuite(SingleValuedPropertyTest.class); + suite.addTestSuite(MultiValuedPropertyTest.class); + suite.addTestSuite(BinaryTest.class); + + // change mixin types + suite.addTestSuite(MixinModificationTest.class); + + // move + suite.addTestSuite(MoveTest.class); + suite.addTestSuite(MoveReferenceableTest.class); + suite.addTestSuite(MoveSNSTest.class); + suite.addTestSuite(MoveTreeTest.class); + suite.addTestSuite(MoveNewTreeTest.class); + suite.addTestSuite(MoveMultipleTest.class); + suite.addTestSuite(WorkspaceMoveTest.class); + suite.addTestSuite(RevertMoveTest.class); + suite.addTestSuite(MoveToNewTest.class); + suite.addTestSuite(MoveCombinedTest.class); + + // refresh + suite.addTestSuite(RefreshFalseTest.class); + suite.addTestSuite(RefreshTrueTest.class); + suite.addTestSuite(RefreshMovedTest.class); + + // remove + suite.addTestSuite(RemoveNodeTest.class); + suite.addTestSuite(RemovePropertyTest.class); + suite.addTestSuite(RemoveReferenceableNodeTest.class); + suite.addTestSuite(RemoveSNSTest.class); + suite.addTestSuite(RemoveMovedNodeTest.class); + + // rename + suite.addTestSuite(RenameTest.class); + + // reorder + suite.addTestSuite(ReorderTest.class); + suite.addTestSuite(ReorderReferenceableSNSTest.class); + suite.addTestSuite(ReorderSNSTest.class); + suite.addTestSuite(ReorderNewSNSTest.class); + suite.addTestSuite(ReorderNewTest.class); + suite.addTestSuite(ReorderNewAndSavedTest.class); + suite.addTestSuite(ReorderMixedTest.class); + suite.addTestSuite(ReorderMoveTest.class); + + // update + suite.addTestSuite(UpdateTest.class); + + // various + suite.addTestSuite(ReplaceNodeTest.class); + suite.addTestSuite(HierarchyNodeTest.class); + suite.addTestSuite(LazyItemIteratorTest.class); + suite.addTestSuite(ExternalModificationTest.class); + suite.addTestSuite(IsSameTest.class); + + // repository + suite.addTestSuite(LoginTest.class); + + // observation + suite.addTestSuite(ObservationTest.class); + + // workspace mgt + suite.addTestSuite(WorkspaceTest.class); + + // json (because of remoting servlet) + suite.addTestSuite(CopyMoveToJsonTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/TestConnect.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/TestConnect.java new file mode 100644 index 00000000000..8e2d789ebfe --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/TestConnect.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import org.apache.jackrabbit.test.AbstractJCRTest; + +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; + +/** + * TestConnect... + */ +public class TestConnect extends AbstractJCRTest { + + public void testConnect() throws RepositoryException { + System.out.println("UserID: " + superuser.getUserID()); + for (NodeIterator it = superuser.getRootNode().getNodes(); it.hasNext(); ) { + System.out.println(it.nextNode().getPath()); + } + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/UpdateTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/UpdateTest.java new file mode 100644 index 00000000000..3db306c75c0 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/UpdateTest.java @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi; + +import java.util.Arrays; +import java.util.List; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * UpdateTest... + */ +public class UpdateTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(UpdateTest.class); + + private String currentWorkspace; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + currentWorkspace = testRootNode.getSession().getWorkspace().getName(); + } + + public void testInvalidSrcWorkspace() throws RepositoryException { + String nonExistingWorkspace = "nonExistingWorkspace"; + String[] accessibleWorkspaces = testRootNode.getSession().getWorkspace().getAccessibleWorkspaceNames(); + List l = Arrays.asList(accessibleWorkspaces); + while (l.contains(nonExistingWorkspace)) { + nonExistingWorkspace = nonExistingWorkspace + "_"; + } + + try { + testRootNode.update(nonExistingWorkspace); + } catch (NoSuchWorkspaceException e) { + // ok + } + } + + public void testNoCorrespondingNode() throws RepositoryException, NotExecutableException { + Node n = testRootNode.addNode(nodeName2, testNodeType); + testRootNode.save(); + + String srcWorkspace = null; + String wspName = getHelper().getProperty("org.apache.jackrabbit.jcr2spi.workspace2.name"); + if (wspName == null) { + throw new NotExecutableException("Cannot run update. Missing config param."); + } + try { + n.getCorrespondingNodePath(wspName); + } catch (ItemNotFoundException e) { + srcWorkspace = wspName; + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot run update. Workspace " + srcWorkspace + " does not exist or is not accessible."); + } + if (srcWorkspace == null) { + throw new NotExecutableException("Cannot run update. No workspace found, that misses the corresponding node."); + } + + try { + // update without corresponding node must be a nop + testRootNode.update(srcWorkspace); + } catch (RepositoryException e) { + fail("Update with workspace that doesn't contain the corresponding node must work."); + } + } + + public void testSameWorkspace() throws RepositoryException, NotExecutableException { + try { + // update without corresponding node must be a nop + testRootNode.update(currentWorkspace); + } catch (RepositoryException e) { + fail("Update with srcWorkspace == this workspace must return silently."); + } + } + + public void testPendingChangesSameWorkspace() throws RepositoryException, NotExecutableException { + testRootNode.addNode(nodeName2, testNodeType); + + try { + testRootNode.update(currentWorkspace); + fail("Update while changes are pending must fail with InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // ok + } + } + + public void testPendingChanges() throws RepositoryException, NotExecutableException { + testRootNode.addNode(nodeName2, testNodeType); + + String srcWorkspace = getAnotherWorkspace(); + try { + testRootNode.update(srcWorkspace); + fail("Update while changes are pending must fail with InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // ok + } + } + + public void testPendingChangesOnOtherNode() throws RepositoryException, NotExecutableException { + try { + Node root = testRootNode.getSession().getRootNode(); + if (root.isSame(testRootNode)) { + throw new NotExecutableException(); + } + if (root.canAddMixin(mixLockable)) { + root.addMixin(mixLockable); + } else { + root.setProperty(propertyName1, "anyValue"); + } + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + String srcWorkspace = getAnotherWorkspace(); + try { + testRootNode.update(srcWorkspace); + fail("Update while changes are pending must fail with InvalidItemStateException"); + } catch (InvalidItemStateException e) { + // ok + } + } + + public void testUpdateRemovesExtraProperty() throws RepositoryException, NotExecutableException { + // create test node in default workspace + testRootNode.setProperty(propertyName2, "test"); + testRootNode.save(); + + String srcWorkspace = getAnotherWorkspace(); + // get the root node in the second workspace + Session session2 = getHelper().getSuperuserSession(srcWorkspace); + try { + // make sure the source-session has the corresponding node. + Node testRootW2 = (Node) session2.getItem(testRootNode.getCorrespondingNodePath(srcWorkspace)); + if (testRootW2.hasProperty(propertyName2)) { + throw new NotExecutableException(); + } + + // call the update method on test node in default workspace + testRootNode.update(srcWorkspace); + + // ok first check if node has no longer properties + assertFalse("Node updated with Node.update() should have property removed", testRootNode.hasProperty(propertyName2)); + } catch (PathNotFoundException e) { + throw new NotExecutableException(); + } catch (ItemNotFoundException e) { + throw new NotExecutableException(); + } finally { + session2.logout(); + } + } + + public void testUpdateAddsMissingSubtree() throws RepositoryException, NotExecutableException { + String srcWorkspace = getAnotherWorkspace(); + // get the root node in the second workspace + Session session2 = getHelper().getSuperuserSession(srcWorkspace); + try { + // make sure the source-session has the corresponding node. + Node testRootW2 = (Node) session2.getItem(testRootNode.getCorrespondingNodePath(srcWorkspace)); + + // create test node in second workspace + Node aNode2 = testRootW2.addNode(nodeName1, testNodeType); + aNode2.addNode(nodeName2, testNodeType); + aNode2.setProperty(propertyName2, "test"); + Property p2 = testRootW2.setProperty(propertyName1, "test"); + testRootW2.save(); + + // call the update method on test node in default workspace + testRootNode.update(srcWorkspace); + + // ok check if the child has been added + boolean allPresent = testRootNode.hasNode(nodeName1) && + testRootNode.hasNode(nodeName1+"/"+nodeName2) && + testRootNode.hasProperty(nodeName1+"/"+propertyName2) && + testRootNode.hasProperty(propertyName1); + assertTrue("Node updated with Node.update() should have received childrens", allPresent); + } catch (PathNotFoundException e) { + throw new NotExecutableException(); + } catch (ItemNotFoundException e) { + throw new NotExecutableException(); + } finally { + session2.logout(); + } + } + + /** + * See JCR-2462 + */ + public void testSetSamePropertyTwice() throws RepositoryException { + Node node = this.testRootNode.addNode( "test" ); + Session session = node.getSession(); + node.setProperty( "prop", "value1"); + node.setProperty( "prop", "value2"); + node.remove(); + session.save(); + } + + private String getAnotherWorkspace() throws NotExecutableException, RepositoryException { + String srcWorkspace = getHelper().getProperty("org.apache.jackrabbit.jcr2spi.workspace2.name");; + if (srcWorkspace == null || srcWorkspace.equals(currentWorkspace)) { + throw new NotExecutableException("no alternative workspace configured"); + } + + String[] accessible = testRootNode.getSession().getWorkspace().getAccessibleWorkspaceNames(); + for (int i = 0; i < accessible.length; i++) { + if (accessible[i].equals(srcWorkspace)) { + return srcWorkspace; + } + } + throw new NotExecutableException("configured workspace does not exist."); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/WorkspaceMoveTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/WorkspaceMoveTest.java new file mode 100644 index 00000000000..ad542974814 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/WorkspaceMoveTest.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.jcr2spi; + +import javax.jcr.ItemExistsException; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * WorkspaceMoveTest... + */ +public class WorkspaceMoveTest extends MoveTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected boolean isSessionMove() { + return false; + } + + /** + * Tries to move a node using to a location where a property already exists + * with same name. + *

        + * With JCR 1.0 this should throw an {@link javax.jcr.ItemExistsException}. + * With JCR 2.0 the support for same-named property and node is optional and + * the expected behaviour depends on the + * {@link Repository#OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED} descriptor. + */ + @Override + public void testMovePropertyExists() throws RepositoryException, NotExecutableException { + // try to create a property with the name of the node to be moved + // to the destination parent + Property destProperty; + try { + destProperty = destParentNode.setProperty(nodeName2, "anyString"); + destParentNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot create property with name '" +nodeName2+ "' and value 'anyString' at move destination."); + } + + // TODO: fix 2.0 behaviour according to the OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED descriptor + if ("1.0".equals(getHelper().getRepository().getDescriptor(Repository.SPEC_VERSION_DESC))) { + try { + // move the node + doMove(moveNode.getPath(), destProperty.getPath()); + fail("Moving a node to a location where a property exists must throw ItemExistsException"); + } catch (ItemExistsException e) { + // ok, works as expected + } + } else { + // JCR 2.0 move the node: same name property and node must be supported + doMove(moveNode.getPath(), destProperty.getPath()); + } + } + + public void testMoveTransientPropertyExists() throws RepositoryException, NotExecutableException { + // try to create a property with the name of the node to be moved + // to the destination parent + Property destProperty; + try { + destProperty = destParentNode.setProperty(nodeName2, "anyString"); + } catch (RepositoryException e) { + throw new NotExecutableException("Cannot create property with name '" +nodeName2+ "' and value 'anyString' at move destination."); + } + + // TODO: fix 2.0 behaviour according to the OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED descriptor + // workspace-move the node (must succeed) + doMove(moveNode.getPath(), destProperty.getPath()); + if ("1.0".equals(getHelper().getRepository().getDescriptor(Repository.SPEC_VERSION_DESC))) { + try { + // saving transient new property must fail + destParentNode.save(); + fail("Saving new transient property must fail"); + } catch (RepositoryException e) { + // ok. + } + } else { + // JCR 2.0: saving must succeed. + destParentNode.save(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/WorkspaceTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/WorkspaceTest.java new file mode 100644 index 00000000000..2bfab6c1f0a --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/WorkspaceTest.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.jcr2spi; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Workspace; +import java.util.Arrays; +import java.util.List; + +/** + * WorkspaceTest... + */ +public class WorkspaceTest extends AbstractJCRTest { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(WorkspaceTest.class); + + private static String getNewWorkspaceName(Workspace wsp) throws RepositoryException { + List names = Arrays.asList(wsp.getAccessibleWorkspaceNames()); + int index = 0; + while (names.contains("testWsp_" + index)) { + index++; + } + return "testWsp_" + index; + } + + public void testCreateWorkspace() throws Exception { + Session s = null; + try { + Workspace wsp = superuser.getWorkspace(); + String name = getNewWorkspaceName(wsp); + wsp.createWorkspace(name); + + List wsps = Arrays.asList(wsp.getAccessibleWorkspaceNames()); + assertTrue(wsps.contains(name)); + + s = getHelper().getSuperuserSession(name); + Workspace newW = s.getWorkspace(); + assertEquals(name, newW.getName()); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(); + } catch (UnsupportedOperationException e) { + throw new NotExecutableException(); + } finally { + if (s != null) { + s.logout(); + } + } + } + + public void testCreateWorkspaceFromSource() throws Exception { + Session s = null; + try { + Workspace wsp = superuser.getWorkspace(); + String name = getNewWorkspaceName(wsp); + + wsp.createWorkspace(name, wsp.getName()); + + List wsps = Arrays.asList(wsp.getAccessibleWorkspaceNames()); + assertTrue(wsps.contains(name)); + + s = getHelper().getSuperuserSession(name); + Workspace newW = s.getWorkspace(); + assertEquals(name, newW.getName()); + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(); + } catch (UnsupportedOperationException e) { + throw new NotExecutableException(); + } finally { + if (s != null) { + s.logout(); + } + } + } + + public void testDeleteWorkspace() throws Exception { + try { + Workspace wsp = superuser.getWorkspace(); + String name = getNewWorkspaceName(wsp); + + wsp.createWorkspace(name, wsp.getName()); + + List wsps = Arrays.asList(wsp.getAccessibleWorkspaceNames()); + assertTrue(wsps.contains(name)); + + wsp.deleteWorkspace(name); + + wsps = Arrays.asList(wsp.getAccessibleWorkspaceNames()); + assertFalse(wsps.contains(name)); + + Session s = null; + try { + s = getHelper().getSuperuserSession(name); + fail(name + " has been deleted."); + } catch (NoSuchWorkspaceException e) { + // success + } finally { + if (s != null) { + s.logout(); + } + } + } catch (UnsupportedRepositoryOperationException e) { + throw new NotExecutableException(); + } catch (UnsupportedOperationException e) { + throw new NotExecutableException(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/benchmark/ReadPerformanceTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/benchmark/ReadPerformanceTest.java new file mode 100644 index 00000000000..d8483238faa --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/benchmark/ReadPerformanceTest.java @@ -0,0 +1,353 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.benchmark; + +import org.apache.jackrabbit.jcr2spi.AbstractJCR2SPITest; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.ItemInfoBuilder.NodeInfoBuilder; +import org.apache.jackrabbit.spi.commons.ItemInfoBuilder.PropertyInfoBuilder; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi.commons.iterator.Predicate; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; + +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.filterIterator; +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.iteratorChain; +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.singleton; + +/** + * Utility for testing jcr2spi read performance + */ +public class ReadPerformanceTest extends AbstractJCR2SPITest { + + /** + * Depth of the content tree + */ + private static int TREE_DEPTH = 3; + + /** + * Number of child nodes of each internal node + */ + private static int NODE_COUNT = 6; + + /** + * Number of properties of each node + */ + private static int PROPERTY_COUNT = 60; + + /** + * Number of JCR operations to perform per run + */ + private static int OP_COUNT = 500; + + /** + * Size of the item info cache. + * @see ItemInfoCache + */ + private static int ITEM_INFO_CACHE_SIZE = 50000; + + /** + * Ratios of the number of items in the whole content tree compared to the number of items + * in a batch of a {@link RepositoryService#getItemInfos(SessionInfo, NodeId)} call. + * The array contains one ratio per run + */ + private static int[] BATCH_RATIOS = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384}; + + /** + * Valid paths of nodes in the mock repository + */ + private final List nodePaths = new ArrayList(); + + /** + * Valid paths of properties in the mock repository + */ + private final List propertyPaths = new ArrayList(); + + private final Random rnd = new Random(12345); + + /** + * This implementation overrides the default cache size with the value of + * {@value #ITEM_INFO_CACHE_SIZE} + */ + @Override + public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) throws RepositoryException { + return new ItemInfoCacheImpl(ITEM_INFO_CACHE_SIZE); + } + + /** + * This implementation adds a tree of nodes and properties up to certain {@link #TREE_DEPTH}. + * Each node has {@link #NODE_COUNT} child nodes and {@link #PROPERTY_COUNT} properties. + * {@inheritDoc} + */ + @Override + protected void initInfosStore(NodeInfoBuilder builder) throws RepositoryException { + addNodes(builder, ""); + } + + private void addNodes(NodeInfoBuilder builder, String name) throws RepositoryException { + if (name.length() >= TREE_DEPTH) { + builder.build(); + nodePaths.add(toJCRPath(builder.getNodeInfo().getPath())); + return; + } + + for (int k = 0; k <= NODE_COUNT; k++) { + String n = name + k; + addNodes(addProperties(builder.createNodeInfo(n), PROPERTY_COUNT), n); + } + builder.build(); + } + + private NodeInfoBuilder addProperties(NodeInfoBuilder builder, int count) throws RepositoryException { + for (int k = 0; k < count; k++) { + PropertyInfoBuilder pBuilder = builder.createPropertyInfo("property_" + k, "Just some string value " + k); + pBuilder.build(); + propertyPaths.add(toJCRPath(pBuilder.getPropertyInfo().getPath())); + } + + return builder; + } + + /** + * Create count JCR operations for a session + * @param session + * @param count + * @return + */ + protected Iterable> getOperations(final Session session, int count) { + ArrayList> callables = new ArrayList>(); + final List items = new ArrayList(); + + for (int k = 0; k < count; k ++) { + switch (rnd.nextInt(4)) { + case 0: { // getItem + callables.add(new Callable() { + public Long call() throws Exception { + int i = rnd.nextInt(nodePaths.size() + propertyPaths.size()); + String path = i < nodePaths.size() + ? nodePaths.get(i) + : propertyPaths.get(i - nodePaths.size()); + long t1 = System.currentTimeMillis(); + Item item = session.getItem(path); + long t2 = System.currentTimeMillis(); + items.add(item); + return t2 - t1; + } + + @Override + public String toString() { + return "getItem"; + } + }); + break; + } + + case 1: { // getNode + callables.add(new Callable() { + public Long call() throws Exception { + String path = nodePaths.get(rnd.nextInt(nodePaths.size())); + long t1 = System.currentTimeMillis(); + Node node = session.getNode(path); + long t2 = System.currentTimeMillis(); + items.add(node); + return t2 - t1; + } + + @Override + public String toString() { + return "getNode"; + } + }); + break; + } + + case 2: { // getProperty + callables.add(new Callable() { + public Long call() throws Exception { + String path = propertyPaths.get(rnd.nextInt(propertyPaths.size())); + long t1 = System.currentTimeMillis(); + Property property = session.getProperty(path); + long t2 = System.currentTimeMillis(); + items.add(property); + return t2 - t1; + } + + @Override + public String toString() { + return "getProperty"; + } + }); + break; + } + + case 3: { // refresh + callables.add(new Callable() { + public Long call() throws Exception { + if (items.isEmpty()) { + return 0L; + } + Item item = items.get(rnd.nextInt(items.size())); + long t1 = System.currentTimeMillis(); + item.refresh(rnd.nextBoolean()); + long t2 = System.currentTimeMillis(); + return t2 - t1; + } + + @Override + public String toString() { + return "refresh"; + } + }); + break; + } + + default: + fail("Invalid case in switch"); + } + } + + return callables; + } + + private int roundTripCount; + private int batchRatio; + + /** + * Perform {@link #OP_COUNT} JCR operations on a fresh session once for each batch ratio value + * given in {@link #BATCH_RATIOS}. + * @throws Exception + */ + public void testReadOperations() throws Exception { + for (int ratio : BATCH_RATIOS) { + testReadOperations(OP_COUNT, ratio); + } + } + + private void testReadOperations(int opCount, int batchRatio) throws Exception { + this.batchRatio = batchRatio; + this.roundTripCount = 0; + + Map opCounts = new HashMap(); + Map opTimes = new HashMap(); + Session session = repository.login(); + + Iterable> operations = getOperations(session, opCount); + for (Callable operation : operations) { + String opName = operation.toString(); + Long t = operation.call(); + + if (opCounts.containsKey(opName)) { + opCounts.put(opName, opCounts.get(opName) + 1); + opTimes.put(opName, opTimes.get(opName) + t); + } + else { + opCounts.put(opName, 1); + opTimes.put(opName, t); + } + } + + System.out.println("Batch ratio: " + batchRatio); + System.out.println("Round trips: " + roundTripCount); + + int count = 0; + long time = 0L; + for (String opName : opCounts.keySet()) { + int c = opCounts.get(opName); + count += c; + System.out.println(opName + " count: " + c); + + long t = opTimes.get(opName); + time += t; + System.out.println(opName + " time: " + t); + } + + System.out.println("Total count: " + count); + System.out.println("Total time: " + time); + + session.logout(); + } + + private Iterator getBatch() { + return filterIterator(itemInfoStore.getItemInfos(), new Predicate() { + public boolean evaluate(ItemInfo value) { + return rnd.nextInt(batchRatio) == 0; + } + }); + } + + // -----------------------------------------------------< RepositoryService >--- + + @Override + protected QNodeDefinition createRootNodeDefinition() { + fail("not implemented: createRootNodeDefinition"); + return null; + } + + @Override + public Iterator getItemInfos(SessionInfo sessionInfo, ItemId itemId) + throws ItemNotFoundException, RepositoryException { + + roundTripCount++; + ItemInfo itemInfo = itemInfoStore.getItemInfo(itemId); + return iteratorChain(singleton(itemInfo), getBatch()); + } + + @Override + public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + roundTripCount++; + return itemInfoStore.getNodeInfo(nodeId); + } + + @Override + public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId) + throws ItemNotFoundException { + + roundTripCount++; + return itemInfoStore.getPropertyInfo(propertyId); + } + + @Override + public Iterator getChildInfos(SessionInfo sessionInfo, NodeId parentId) + throws ItemNotFoundException, RepositoryException { + + roundTripCount++; + return itemInfoStore.getChildInfos(parentId); + } + +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/AbstractLockTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/AbstractLockTest.java new file mode 100644 index 00000000000..5964b972a6f --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/AbstractLockTest.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.lock; + +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractLockTest... + */ +public abstract class AbstractLockTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(AbstractLockTest.class); + + Node lockedNode; + Node childNode; + Lock lock; + + Session otherSession; + + abstract boolean isSessionScoped(); + + @Override + protected void setUp() throws Exception { + super.setUp(); + + otherSession = getHelper().getSuperuserSession(); + + lockedNode = testRootNode.addNode(nodeName1, testNodeType); + lockedNode.addMixin(mixLockable); + childNode = lockedNode.addNode(nodeName2, testNodeType); + testRootNode.save(); + + lock = lockedNode.lock(false, isSessionScoped()); + } + + @Override + protected void tearDown() throws Exception { + // make sure all locks are removed + try { + lockedNode.unlock(); + } catch (RepositoryException e) { + // ignore + } + if (otherSession.isLive()) { + otherSession.logout(); + otherSession = null; + } + lockedNode = null; + childNode = null; + lock = null; + super.tearDown(); + } + + public void testParentChildLock() throws Exception { + childNode.addMixin(mixLockable); + testRootNode.save(); + + // lock child node + try { + childNode.lock(false, isSessionScoped()); + // unlock parent node + lockedNode.unlock(); + // child node must still hold lock + assertTrue("child node must still hold lock", childNode.isLocked() && childNode.holdsLock()); + } finally { + childNode.unlock(); + } + } + + public void testParentChildLock2() throws Exception { + childNode.addMixin(mixLockable); + testRootNode.save(); + try { + Lock l = childNode.lock(false, isSessionScoped()); + assertTrue("child node must still hold lock", l.getNode().isSame(childNode)); + } finally { + childNode.unlock(); + } + } + + /** + * Tests if a locked, checked-in node can be unlocked + */ + public void testCheckedInUnlock() throws Exception { + if (!isSupported(Repository.OPTION_VERSIONING_SUPPORTED)) { + throw new NotExecutableException("Repository does not support versioning."); + } + + lockedNode.addMixin(mixVersionable); + lockedNode.save(); + + // lock and check-in + lockedNode.checkin(); + + // do the unlock + lockedNode.unlock(); + assertFalse("Could not unlock a locked, checked-in node", lockedNode.holdsLock()); + } + + public void testReorder() throws Exception { + testRootNode.addNode(nodeName2); + testRootNode.addNode(nodeName3); + testRootNode.save(); + + // move last node in front of first + testRootNode.orderBefore(lockedNode.getName(), nodeName3); + testRootNode.save(); + + assertTrue("Node must remain locked upon reordering", testRootNode.getNode(lockedNode.getName()).isLocked()); + } + + public void testReorderSNS() throws Exception { + // create 2 additional nodes with same name + testRootNode.addNode(nodeName1); + testRootNode.addNode(nodeName1); + testRootNode.save(); + + // assert: first node locked + assertTrue("First child node locked", testRootNode.getNode(nodeName1 + "[1]").isLocked()); + + // move first node to last + testRootNode.orderBefore(nodeName1 + "[1]", null); + testRootNode.save(); + + // assert: third node locked + assertTrue("Third child node locked", testRootNode.getNode(nodeName1 + "[3]").isLocked()); + } + + /** + * Tests if move preserves lock state (JIRA issue JCR-207). A node that has + * been locked must still appear locked when it has been moved or renamed, + * regardless whether the changes have already been made persistent. + */ + public void testMoveLocked() throws Exception { + + Session session = testRootNode.getSession(); + + childNode.addMixin(mixLockable); + childNode.save(); + + try { + // lock child node + childNode.lock(false, isSessionScoped()); + + // assert: child node locked + assertTrue("Child node locked", childNode.isLocked()); + + // move child node up + String newPath = testRootNode.getPath() + "/" + childNode.getName(); + session.move(childNode.getPath(), newPath); + + // assert: child node locked, before save + assertTrue("Child node locked before save", childNode.isLocked()); + session.save(); + + // assert: child node locked, after save + assertTrue("Child node locked after save", childNode.isLocked()); + + } finally { + session.refresh(false); + childNode.unlock(); + } + } + + /** + * Tests if unlocking the first of two locked same-name sibling nodes does + * not unlock the second (JIRA issue JCR-284). + */ + public void testUnlockSameNameSibling() throws RepositoryException { + Session session = testRootNode.getSession(); + + // create two same-name sibling nodes + Node lockedNode2 = testRootNode.addNode(nodeName1); + lockedNode2.addMixin("mix:lockable"); + session.save(); + + // lock both nodes + lockedNode2.lock(false, isSessionScoped()); + + try { + // assert: both nodes are locked + assertTrue("First node locked: ", lockedNode.isLocked()); + assertTrue("Second node locked: ", lockedNode2.isLocked()); + } catch (RepositoryException e) { + // make sure all locks are release again + lockedNode.unlock(); + lockedNode2.unlock(); + throw new RepositoryException(e); + } + + try { + // unlock first sibling + lockedNode.unlock(); + + // assert: first node unlocked, second node still locked + assertFalse("First node unlocked: ", lockedNode.isLocked()); + assertTrue("Second node locked: ", lockedNode2.isLocked()); + + } finally { + // make sure all locks are release again + lockedNode2.unlock(); + } + } + + /** + * If a locked nodes is unlocked again, any Lock instance retrieved by + * another session must change the lock-status. Similarly, the previously + * locked node must not be marked locked any more. + */ + public void testUnlockByOtherSession() throws RepositoryException { + Node ln2 = (Node) otherSession.getItem(lockedNode.getPath()); + Lock l2 = ln2.getLock(); + + lockedNode.unlock(); + + assertFalse("Lock must be informed if Node is unlocked.", l2.isLive()); + } + + /** + * If a locked nodes is unlocked again, any Lock instance retrieved by + * another session must change the lock-status. Similarly, the previously + * locked node must not be marked locked any more. + */ + public void testUnlockByOtherSession2() throws RepositoryException { + Node ln2 = (Node) otherSession.getItem(lockedNode.getPath()); + + lockedNode.unlock(); + + assertFalse("Node is not locked any more", ln2.isLocked()); + assertFalse("Node is not locked any more", ln2.holdsLock()); + try { + ln2.getLock(); + fail("Node is not locked any more"); + } catch (LockException e) { + // OK + } + } + + public void testRemoveLockedNode() throws RepositoryException { + Node n = (Node) otherSession.getItem(lockedNode.getPath()); + + // since removing a node is a modification of the non-locked parent + // the removal must succeed. + n.remove(); + otherSession.save(); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/DeepLockTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/DeepLockTest.java new file mode 100644 index 00000000000..ce820deb0d1 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/DeepLockTest.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.jcr2spi.lock; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * DeepLockTest... + */ +public class DeepLockTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(DeepLockTest.class); + + private final boolean isSessionScoped = false; + private final boolean isDeep = true; + + private Node lockedNode; + private Node childNode; + private Lock lock; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + lockedNode = testRootNode.addNode(nodeName1, testNodeType); + lockedNode.addMixin(mixLockable); + childNode = lockedNode.addNode(nodeName2, testNodeType); + testRootNode.save(); + + lock = lockedNode.lock(isDeep, isSessionScoped); + } + + @Override + protected void tearDown() throws Exception { + try { + lockedNode.unlock(); + } catch (RepositoryException e) { + log.warn(e.getMessage()); + } + lockedNode = null; + childNode = null; + lock = null; + super.tearDown(); + } + + public void testLockHoldingNode() throws RepositoryException { + assertTrue("Lock.getNode() must be lockholding node.", lock.getNode().isSame(lockedNode)); + } + + public void testLockIsDeep() throws RepositoryException { + assertTrue("Lock.isDeep() if lock has been created deeply.", lock.isDeep()); + } + + public void testNodeIsLocked() throws RepositoryException { + assertTrue("Creating a deep lock must create a lock on the lock-holding node", lockedNode.isLocked()); + assertTrue("Creating a deep lock must create a lock on the lock-holding node", lockedNode.holdsLock()); + } + + public void testIsLockedChild() throws RepositoryException { + assertTrue("Child node below deep lock must be locked", childNode.isLocked()); + } + + public void testIsLockedNewChild() throws RepositoryException { + Node newChild = lockedNode.addNode(nodeName3, testNodeType); + assertTrue("Child node below deep lock must be locked even if its is NEW", newChild.isLocked()); + } + + public void testNotHoldsLockChild() throws RepositoryException { + assertFalse("Child node below deep lock must not be lock holder", childNode.holdsLock()); + } + + public void testGetLockOnChild() throws RepositoryException { + // get lock must succeed even if child is not lockable. + childNode.getLock(); + } + + public void testGetLockOnNewChild() throws RepositoryException { + // get lock must succeed even if child is not lockable. + Node newChild = lockedNode.addNode(nodeName3, testNodeType); + newChild.getLock(); + } + + public void testGetNodeOnLockObtainedFromChild() throws RepositoryException { + Lock lock = childNode.getLock(); + assertTrue("Lock.getNode() must return the lock holding node even if lock is obtained from child node.", lock.getNode().isSame(lockedNode)); + } + + public void testGetNodeOnLockObtainedFromNewChild() throws RepositoryException { + Node newChild = lockedNode.addNode(nodeName3, testNodeType); + Lock lock = newChild.getLock(); + assertTrue("Lock.getNode() must return the lock holding node even if lock is obtained from child node.", lock.getNode().isSame(lockedNode)); + } + + public void testParentChildDeepLock() throws RepositoryException { + childNode.addMixin(mixLockable); + testRootNode.save(); + + // try to lock child node + try { + childNode.lock(false, isSessionScoped); + fail("child node is already locked by deep lock on parent."); + } catch (LockException e) { + // ok + } + } + + public void testDeepLockAboveLockedChild() throws RepositoryException, NotExecutableException { + try { + Node parent = lockedNode.getParent(); + if (!parent.isNodeType(mixLockable)) { + try { + parent.addMixin(mixLockable); + parent.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + } + + parent.lock(true, isSessionScoped); + fail("Creating a deep lock on a parent of a locked node must fail."); + } catch (LockException e) { + // expected + } + } + + public void testRemoveLockedChild() throws RepositoryException { + Session otherSession = getHelper().getReadWriteSession(); + try { + Node child = (Node) otherSession.getItem(childNode.getPath()); + child.remove(); + otherSession.save(); + fail("A node below a deeply locked node cannot be removed by another Session."); + } catch (LockException e) { + // success + } finally { + otherSession.logout(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/OpenScopedLockTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/OpenScopedLockTest.java new file mode 100644 index 00000000000..9ca784c3280 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/OpenScopedLockTest.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.jcr2spi.lock; + +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; +import javax.jcr.nodetype.NodeType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OpenScopedLockTest... + */ +public class OpenScopedLockTest extends AbstractLockTest { + + private static Logger log = LoggerFactory.getLogger(OpenScopedLockTest.class); + + @Override + boolean isSessionScoped() { + return false; + } + + public void testLogoutHasNoEffect() throws Exception { + // create a second session session. since logout of the 'superuser' + // will cause all inherited tear-down to fail + Node testRoot2 = (Node) otherSession.getItem(testRootNode.getPath()); + + Node lockedNode2 = testRoot2.addNode(nodeName2, testNodeType); + lockedNode2.addMixin(mixLockable); + testRoot2.save(); + + Lock lock2 = lockedNode2.lock(false, isSessionScoped()); + + // force reloading of the testroot in order to be aware of the + // locked node added by another session + testRootNode.refresh(false); + Node n2 = (Node) superuser.getItem(lockedNode2.getPath()); + try { + String lockToken = lock2.getLockToken(); + otherSession.removeLockToken(lockToken); + superuser.addLockToken(lockToken); + otherSession.logout(); + + assertTrue("After logout a open-scoped node must still be locked.", lock2.isLive()); + assertTrue("After logout a open-scoped node must still be locked.", n2.isLocked()); + } finally { + n2.unlock(); + } + } + + /** + * Test if the lock token has been automatically added to the set of lock + * tokens present with the Session that created the new Lock. + * + * @throws RepositoryException + */ + public void testLockTokenPresentWithSession() throws RepositoryException { + String token = lock.getLockToken(); + String[] allTokens = lockedNode.getSession().getLockTokens(); + for (int i = 0; i < allTokens.length; i++) { + if (allTokens[i].equals(token)) { + // lock token is present with the session that applied the lock + // OK + return; + } + } + + // lock token not present within tokens returned by Session.getLockTokens. + fail("Upon successful call to Node.lock, the lock token must automatically be added to the set of tokens held by the Session."); + } + + public void testTokenTransfer() throws Exception { + String lockToken = lock.getLockToken(); + try { + superuser.removeLockToken(lockToken); + + String nlt = lock.getLockToken(); + assertTrue("freshly obtained lock token must either be null or the same as the one returned earlier", + nlt == null || nlt.equals(lockToken)); + } finally { + // move lock token back in order to have lock removed properly + superuser.addLockToken(lockToken); + } + } + + public void testRefreshAfterTokenTransfer() throws Exception { + String lockToken = lock.getLockToken(); + try { + superuser.removeLockToken(lockToken); + lock.refresh(); + fail("After transfering lock token the original lock object cannot be refresh by session, that does hold lock any more."); + } catch (LockException e) { + // oK + } finally { + // move lock token back in order to have lock removed properly + superuser.addLockToken(lockToken); + } + } + + public void testRefreshAfterTokenTransfer2() throws Exception { + String lockToken = lock.getLockToken(); + + Node n2 = (Node) otherSession.getItem(lockedNode.getPath()); + try { + superuser.removeLockToken(lockToken); + otherSession.addLockToken(lockToken); + + n2.getLock().refresh(); + } finally { + // move lock token back in order to have lock removed properly + otherSession.removeLockToken(lockToken); + superuser.addLockToken(lockToken); + } + } + + public void testLockHolderAfterTokenTransfer() throws Exception { + String lockToken = lock.getLockToken(); + Node n2 = (Node) otherSession.getItem(lockedNode.getPath()); + try { + superuser.removeLockToken(lockToken); + otherSession.addLockToken(lockToken); + + assertTrue("After lockToken transfer, the new lockHolder must get a non-null token", n2.getLock().getLockToken() != null); + assertTrue("After lockToken transfer, the new lockHolder must get the same token.", n2.getLock().getLockToken().equals(lockToken)); + } finally { + // move lock token back in order to have lock removed properly + otherSession.removeLockToken(lockToken); + superuser.addLockToken(lockToken); + } + } + + public void testUnlockAfterTokenTransfer() throws Exception { + String lockToken = lock.getLockToken(); + try { + superuser.removeLockToken(lockToken); + lockedNode.unlock(); + fail("After transfering lock token the original lock object cannot be unlocked by session, that does hold lock any more."); + } catch (LockException e) { + // oK + } finally { + // move lock token back in order to have lock removed properly + superuser.addLockToken(lockToken); + } + } + + public void testUnlockAfterTokenTransfer2() throws Exception { + String lockToken = lock.getLockToken(); + try { + superuser.removeLockToken(lockToken); + otherSession.addLockToken(lockToken); + + // otherSession is now lockHolder -> unlock must succeed. + Node n2 = (Node) otherSession.getItem(lockedNode.getPath()); + n2.unlock(); + } catch (RepositoryException e) { + // only in case of failure: + // move lock token back in order to have lock removed properly + // if test succeeds, moving back tokens is not necessary. + otherSession.removeLockToken(lockToken); + superuser.addLockToken(lockToken); + + // and rethrow + throw e; + } + } + + /** + * Test if a Lock created by one session gets properly invalidated + * if the lock token has been transfered to another session, which + * unlocks the Node. + */ + public void testUnlockAfterTokenTransfer3() throws Exception { + String lockToken = lock.getLockToken(); + try { + superuser.removeLockToken(lockToken); + otherSession.addLockToken(lockToken); + + // otherSession is now lockHolder -> unlock must succeed. + Node n2 = (Node) otherSession.getItem(lockedNode.getPath()); + n2.unlock(); + + assertFalse("Lock has been release by another session.", lockedNode.holdsLock()); + + assertFalse("Lock has been release by another session.", lock.isLive()); + assertFalse("Lock has been release by another session.", lock.getNode().isLocked()); + try { + lockedNode.getLock(); + fail("Lock has been release by another session."); + } catch (LockException e) { + // ok + } + } catch (RepositoryException e) { + // only in case of failure: + // move lock token back in order to have lock removed properly + // if test succeeds, moving back tokens is not necessary. + otherSession.removeLockToken(lockToken); + superuser.addLockToken(lockToken); + + // and rethrow + throw e; + } + } + + public void testIsLockedWhileAnotherLockIsPresent() throws Exception { + + Session s = lockedNode.getSession(); + LockManager lm = s.getWorkspace().getLockManager(); + + String l2token = null; + String l2path = null; + + String path = lockedNode.getPath(); + String lockToken = lock.getLockToken(); + assertTrue(lm.isLocked(path)); + assertTrue(lm.holdsLock(path)); + lm.removeLockToken(lockToken); + + Session anotherSession = null; + try { + // check lock is seen by new session + anotherSession = getHelper().getSuperuserSession(); + LockManager anotherLockManager = anotherSession.getWorkspace().getLockManager(); + assertTrue(anotherLockManager.isLocked(path)); + assertTrue(anotherLockManager.holdsLock(path)); + + // create a second lock + Node l2node = anotherSession.getNode(path).getParent().addNode("second-lock"); + l2node.addMixin(NodeType.MIX_LOCKABLE); + anotherSession.save(); + l2path = l2node.getPath(); + + Lock l2 = anotherLockManager.lock(l2path, false, false, Long.MAX_VALUE, "foobar"); + l2token = l2.getLockToken(); + assertNotNull(l2token); + anotherSession.save(); + + anotherSession.refresh(false); + assertTrue(anotherLockManager.isLocked(path)); + assertTrue(anotherLockManager.holdsLock(path)); + + // try to unlock the lock obtained from the other session + anotherLockManager.addLockToken(lockToken); + anotherLockManager.unlock(path); + anotherSession.save(); + + // unlock "my" lock + anotherLockManager.unlock(l2path); + anotherSession.save(); + l2path = null; + } + finally { + if (anotherSession != null) { + anotherSession.logout(); + } + if (l2path != null && l2token != null) { + superuser.refresh(false); + LockManager sulm = superuser.getWorkspace().getLockManager(); + sulm.addLockToken(l2token); + sulm.unlock(l2path); + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/SessionScopedLockTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/SessionScopedLockTest.java new file mode 100644 index 00000000000..fb7610d719d --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/SessionScopedLockTest.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.jcr2spi.lock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SessionScopedLockTest... + */ +public class SessionScopedLockTest extends AbstractLockTest { + + private static Logger log = LoggerFactory.getLogger(SessionScopedLockTest.class); + + @Override + boolean isSessionScoped() { + return true; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/TestAll.java new file mode 100644 index 00000000000..0072ad67c3d --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/lock/TestAll.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.jcr2spi.lock; + +import junit.framework.Test; +import junit.framework.TestSuite; +import junit.framework.TestCase; + +/** + * TestAll... + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("jcr2spi lock tests"); + + suite.addTestSuite(SessionScopedLockTest.class); + suite.addTestSuite(OpenScopedLockTest.class); + suite.addTestSuite(DeepLockTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/name/NamespaceRegistryTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/name/NamespaceRegistryTest.java new file mode 100644 index 00000000000..b62d66d0f27 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/name/NamespaceRegistryTest.java @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.name; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * NamespaceRegistryTest... + */ +public class NamespaceRegistryTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(NamespaceRegistryTest.class); + + /** Default value of test prefix */ + private static final String TEST_PREFIX = "test"; + + /** Default value of test namespace uri */ + private static final String TEST_URI = "http://www.apache.org/jackrabbit/test/namespaceRegistryTest"; + + private NamespaceRegistry nsRegistry; + private String testPrefix; + private String testURI; + + @Override + protected void setUp() throws Exception { + super.setUp(); + nsRegistry = superuser.getWorkspace().getNamespaceRegistry(); + + testPrefix = getUnusedPrefix(); + testURI = getUnusedURI(); + + boolean level2 = Boolean.valueOf(superuser.getRepository().getDescriptor(Repository.LEVEL_2_SUPPORTED)).booleanValue(); + if (!level2) { + throw new NotExecutableException("Cannot test namespace registration/unregistration. Repository is a Level 1 only."); + } + } + + @Override + protected void tearDown() throws Exception { + nsRegistry = null; + super.tearDown(); + } + + /** + * Test if a new registered namespace is immediately visible through another + * session object. + * + * @throws RepositoryException + */ + public void testRegisteredNamespaceVisibility() throws RepositoryException { + Session otherSession = getHelper().getReadOnlySession(); + try { + NamespaceRegistry other = otherSession.getWorkspace().getNamespaceRegistry(); + + nsRegistry.registerNamespace(testPrefix, testURI); + String otherUri = other.getURI(testPrefix); + String otherPrefix = other.getPrefix(testURI); + assertTrue("Namespace registered must be immediately visible to any other session.", testURI.equals(otherUri) && testPrefix.equals(otherPrefix)); + } finally { + otherSession.logout(); + } + } + + /** + * Test if a replace namespace prefix cannot be used as key any more to + * retrieve the uri. + * + * @throws RepositoryException + */ + public void testReRegisteredNamespace() throws RepositoryException { + nsRegistry.registerNamespace(testPrefix, testURI); + String replacePrefix = getUnusedPrefix(); + nsRegistry.registerNamespace(replacePrefix, testURI); + try { + nsRegistry.getURI(testPrefix); + fail("Namespace with prefix " + testPrefix + " has been reregistered with new prefix " + replacePrefix); + } catch (NamespaceException e) { + // OK + } + } + + /** + * Test if a replace namespace prefix cannot be used as key any more to + * retrieve the uri in the NamespaceRegistry retrieved by + * another Session object. + * + * @throws RepositoryException + */ + public void testReRegisteredNamespace2() throws RepositoryException { + Session otherSession = getHelper().getReadOnlySession(); + try { + NamespaceRegistry other = otherSession.getWorkspace().getNamespaceRegistry(); + + nsRegistry.registerNamespace(testPrefix, testURI); + other.getPrefix(testURI); + + String replacePrefix = getUnusedPrefix(); + nsRegistry.registerNamespace(replacePrefix, testURI); + + String otherPrefix = other.getPrefix(testURI); + assertEquals("Namespace with prefix " + testPrefix + " has been reregistered with new prefix " + replacePrefix, replacePrefix, otherPrefix); + } finally { + otherSession.logout(); + } + } + + /** + * Test if a replaced namespace prefix is immediately visible in the + * NamespaceRegistry obtained from another session object. + * + * @throws RepositoryException + */ + public void testReRegisteredNamespaceVisibility() throws RepositoryException { + Session otherSession = getHelper().getReadOnlySession(); + try { + NamespaceRegistry other = otherSession.getWorkspace().getNamespaceRegistry(); + + nsRegistry.registerNamespace(testPrefix, testURI); + other.getPrefix(testURI); + + String replacePrefix = getUnusedPrefix(); + nsRegistry.registerNamespace(replacePrefix, testURI); + + String otherUri = other.getURI(replacePrefix); + String otherPrefix = other.getPrefix(testURI); + assertTrue("Namespace registered must be immediately visible to any other session.", testURI.equals(otherUri) && replacePrefix.equals(otherPrefix)); + + try { + other.getURI(testPrefix); + fail("Namespace with prefix " + testPrefix + " has been reregistered with new prefix " + replacePrefix); + } catch (NamespaceException e) { + // OK + } + } finally { + otherSession.logout(); + } + } + + /** + * Test if unregistering a namespace is propagated to all other sessions. + * + * @throws RepositoryException + */ + public void testUnregisteredNamespaceVisibility() throws RepositoryException, NotExecutableException { + String prefix = getUnusedPrefix(); + String uri = getUnusedURI(); + + Session otherSession = getHelper().getReadOnlySession(); + try { + NamespaceRegistry other = otherSession.getWorkspace().getNamespaceRegistry(); + + nsRegistry.registerNamespace(prefix, uri); + try { + nsRegistry.unregisterNamespace(prefix); + } catch (NamespaceException e) { + throw new NotExecutableException("Repository does not support unregistration of namespaces."); + } + + String otherUri = other.getURI(prefix); + String otherPrefix = other.getPrefix(uri); + assertTrue("Namespace registered must be immediately visible to any other session.", uri.equals(otherUri) && prefix.equals(otherPrefix)); + } finally { + otherSession.logout(); + } + } + + /** + * Returns a namespace prefix that currently not used in the namespace + * registry. + * @return an unused namespace prefix. + */ + private String getUnusedPrefix() throws RepositoryException { + Set prefixes = new HashSet(Arrays.asList(nsRegistry.getPrefixes())); + String prefix = TEST_PREFIX; + int i = 0; + while (prefixes.contains(prefix)) { + prefix = TEST_PREFIX + i++; + } + return prefix; + } + + /** + * Returns a namespace URI that currently not used in the namespace + * registry. + * @return an unused namespace URI. + */ + private String getUnusedURI() throws RepositoryException { + Set uris = new HashSet(Arrays.asList(nsRegistry.getURIs())); + String uri = TEST_URI; + int i = 0; + while (uris.contains(uri)) { + uri = TEST_URI + i++; + } + return uri; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/name/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/name/TestAll.java new file mode 100644 index 00000000000..ea91289b86b --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/name/TestAll.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.jcr2spi.name; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * TestAll... + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("jcr2spi name tests"); + + suite.addTestSuite(NamespaceRegistryTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/AddMixinTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/AddMixinTest.java new file mode 100644 index 00000000000..235e5a952e4 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/AddMixinTest.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.jcr2spi.nodetype; + +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AddMixinTest... + */ +public class AddMixinTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(AddMixinTest.class); + + private NodeTypeManager ntMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + ntMgr = testRootNode.getSession().getWorkspace().getNodeTypeManager(); + } + + + @Override + protected void tearDown() throws Exception { + testRootNode.refresh(false); + ntMgr = null; + super.tearDown(); + } + + /** + * Implementation specific test for 'addMixin' only taking effect upon + * save. + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testAddMixinToNewNode() throws NotExecutableException, RepositoryException { + Node newNode; + try { + newNode = testRootNode.addNode(nodeName1, testNodeType); + newNode.addMixin(mixReferenceable); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + assertFalse("Mixin must not be active before Node has been saved.", newNode.isNodeType(mixReferenceable)); + NodeType[] mixins = newNode.getMixinNodeTypes(); + for (int i = 0; i < mixins.length; i++) { + if (mixins[i].getName().equals(testNodeType)) { + fail("Mixin must not be active before Node has been saved."); + } + } + } + + /** + * Implementation specific test adding a new Node with a nodeType, that has + * a mixin-supertype. The mixin must only take effect upon save. + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testImplicitMixinOnNewNode() throws NotExecutableException, RepositoryException { + Node newNode; + try { + String ntResource = superuser.getNamespacePrefix(NS_NT_URI) + ":resource"; + newNode = testRootNode.addNode(nodeName1, ntResource); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + assertFalse("Implict Mixin inherited by primary Nodetype must not be active before Node has been saved.", newNode.isNodeType(mixReferenceable)); + NodeType[] mixins = newNode.getMixinNodeTypes(); + for (int i = 0; i < mixins.length; i++) { + if (mixins[i].getName().equals(testNodeType)) { + fail("Implict Mixin inherited by primary Nodetype must not be active before Node has been saved."); + } + } + } + + /** + * Implementation specific test adding a new Node with a nodeType, that has + * a mixin-supertype. The mixin must only take effect upon save. + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testAddMultipleAtOnce() throws NotExecutableException, RepositoryException { + Node node; + try { + node = testRootNode.addNode(nodeName1, testNodeType); + node.addMixin(mixReferenceable); + node.addMixin(mixLockable); + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + assertTrue("Adding 2 mixins at once -> both must be present.", node.isNodeType(mixReferenceable) && node.isNodeType(mixLockable)); + } + + /** + * Implementation specific test adding a new Node with a nodeType, that has + * a mixin-supertype. The mixin must only take effect upon save. + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testAddMultipleAtOnce2() throws NotExecutableException, RepositoryException { + Node node; + try { + node = testRootNode.addNode(nodeName1, testNodeType); + node.addMixin(mixReferenceable); + node.addMixin(mixLockable); + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + List mixins = Arrays.asList(node.getMixinNodeTypes()); + assertTrue("Adding 2 mixins at once -> both must be present.", mixins.contains(ntMgr.getNodeType(mixReferenceable)) && mixins.contains(ntMgr.getNodeType(mixLockable))); + } + + /** + * Implementation specific test adding a new Node with a nodeType, that has + * a mixin-supertype. The mixin must only take effect upon save. + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testAddMultipleSeparately() throws NotExecutableException, RepositoryException { + Node node; + try { + node = testRootNode.addNode(nodeName1, testNodeType); + node.addMixin(mixReferenceable); + testRootNode.save(); + node.addMixin(mixLockable); + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + assertTrue("Adding 2 mixins at once -> both must be present.", node.isNodeType(mixReferenceable) && node.isNodeType(mixLockable)); + List mixins = Arrays.asList(node.getMixinNodeTypes()); + assertTrue("Adding 2 mixins at once -> both must be present.", mixins.contains(ntMgr.getNodeType(mixReferenceable)) && mixins.contains(ntMgr.getNodeType(mixLockable))); + } + + public void testAddItemsDefinedByMixin() throws NotExecutableException, RepositoryException { + // register mixin + NodeTypeManager ntm = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeTemplate ntd = ntm.createNodeTypeTemplate(); + ntd.setName("testMixin"); + ntd.setMixin(true); + NodeDefinitionTemplate nodeDef = ntm.createNodeDefinitionTemplate(); + nodeDef.setName("child"); + nodeDef.setRequiredPrimaryTypeNames(new String[] {"nt:folder"}); + ntd.getNodeDefinitionTemplates().add(nodeDef); + ntm.registerNodeType(ntd, true); + + // create node and add mixin + Node node = testRootNode.addNode(nodeName1, "nt:resource"); + node.setProperty("jcr:data", "abc"); + node.addMixin("testMixin"); + superuser.save(); + + // create a child node defined by the mixin + node.addNode("child", "nt:folder"); + node.save(); + } + + + public void testAddItemsDefinedByMixin2() throws NotExecutableException, RepositoryException { + // register mixin + NodeTypeManager ntm = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeTemplate ntd = ntm.createNodeTypeTemplate(); + ntd.setName("testMixin"); + ntd.setMixin(true); + NodeDefinitionTemplate nodeDef = ntm.createNodeDefinitionTemplate(); + nodeDef.setName("child"); + nodeDef.setRequiredPrimaryTypeNames(new String[] {"nt:folder"}); + ntd.getNodeDefinitionTemplates().add(nodeDef); + ntm.registerNodeType(ntd, true); + + // create node and add mixin + Node node = testRootNode.addNode(nodeName1, "nt:resource"); + node.setProperty("jcr:data", "abc"); + node.addMixin("testMixin"); + superuser.save(); + + // create a child node defined by the mixin without specifying the + // node type + try { + node.addNode("child"); + fail(); + } catch (ConstraintViolationException e) { + // success as ChildNode Definition doesn't specify a default primary + // type -> see comment in ItemDefinitionProvider#getQNodeDefinition + } + } + + public void testAddItemsDefinedByMixin3() throws NotExecutableException, RepositoryException { + // register mixin + NodeTypeManager ntm = superuser.getWorkspace().getNodeTypeManager(); + NodeTypeTemplate ntd = ntm.createNodeTypeTemplate(); + ntd.setName("testMixin"); + ntd.setMixin(true); + NodeDefinitionTemplate nodeDef = ntm.createNodeDefinitionTemplate(); + nodeDef.setName("child"); + nodeDef.setRequiredPrimaryTypeNames(new String[] {"nt:folder"}); + nodeDef.setDefaultPrimaryTypeName("nt:folder"); + ntd.getNodeDefinitionTemplates().add(nodeDef); + ntm.registerNodeType(ntd, true); + + // create node and add mixin + Node node = testRootNode.addNode(nodeName1, "nt:resource"); + node.setProperty("jcr:data", "abc"); + node.addMixin("testMixin"); + superuser.save(); + + // create a child node defined by the mixin without specifying the + // node type -> must succeed since default primary type is specified + // in the child node def + Node c = node.addNode("child"); + assertEquals("nt:folder", c.getPrimaryNodeType().getName()); + superuser.save(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/MandatoryItemTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/MandatoryItemTest.java new file mode 100644 index 00000000000..bb0c04323e0 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/MandatoryItemTest.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.jcr2spi.nodetype; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** MandatoryItemTest... */ +public class MandatoryItemTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(MandatoryItemTest.class); + + private NodeDefinition childNodeDef; + private PropertyDefinition childPropDef; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + NodeType nt = superuser.getWorkspace().getNodeTypeManager().getNodeType(testNodeType); + NodeDefinition[] ndefs = nt.getChildNodeDefinitions(); + for (int i = 0; i < ndefs.length; i++) { + if (ndefs[i].isMandatory() && !ndefs[i].isProtected() && !ndefs[i].isAutoCreated()) { + childNodeDef = ndefs[i]; + break; + } + } + PropertyDefinition[] pdefs = nt.getPropertyDefinitions(); + for (int i = 0; i < pdefs.length; i++) { + if (pdefs[i].isMandatory() && !pdefs[i].isProtected() && !pdefs[i].isAutoCreated()) { + childPropDef = pdefs[i]; + break; + } + } + if (childPropDef == null && childNodeDef == null) { + cleanUp(); + throw new NotExecutableException(); + } + } + + public void testCreation() throws NotExecutableException, RepositoryException { + Node n; + try { + n = testRootNode.addNode(nodeName1, testNodeType); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + try { + testRootNode.save(); + fail("Saving without having added the mandatory child items must fail."); + } catch (ConstraintViolationException e) { + // success + } + + if (childNodeDef != null) { + n.addNode(childNodeDef.getName(), childNodeDef.getDefaultPrimaryType().getName()); + } + if (childPropDef != null) { + // TODO: check if definition defines default values + n.setProperty(childPropDef.getName(), "any value"); + } + // now save must succeed. + testRootNode.save(); + } + + public void testRemoval() throws NotExecutableException, RepositoryException { + Node n; + Node childN = null; + Property childP = null; + try { + n = testRootNode.addNode(nodeName1, testNodeType); + if (childNodeDef != null) { + childN = n.addNode(childNodeDef.getName(), childNodeDef.getDefaultPrimaryType().getName()); + } + if (childPropDef != null) { + // TODO: check if definition defines default values + childP = n.setProperty(childPropDef.getName(), "any value"); + } + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + // remove the mandatory items ((must succeed)) + if (childN != null) { + childN.remove(); + } + if (childP != null) { + childP.remove(); + } + // ... however, saving must not be allowed. + try { + testRootNode.save(); + fail("removing mandatory child items without re-adding them must fail."); + } catch (ConstraintViolationException e) { + // success. + } + + // re-add the mandatory items + if (childNodeDef != null) { + childN = n.addNode(childNodeDef.getName(), childNodeDef.getDefaultPrimaryType().getName()); + } + if (childPropDef != null) { + // TODO: check if definition defines default values + childP = n.setProperty(childPropDef.getName(), "any value"); + } + // save must succeed now. + testRootNode.save(); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeImplTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeImplTest.java new file mode 100644 index 00000000000..d9f9240a2df --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeImplTest.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.jcr2spi.nodetype; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; + +/** + * NodeTypeImplTest... + */ +public class NodeTypeImplTest extends AbstractJCRTest { + + private NodeTypeManager ntMgr; + private NodeTypeImpl nodeType; + private NameResolver resolver; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + ntMgr = superuser.getWorkspace().getNodeTypeManager(); + NodeType nt = ntMgr.getNodeType(testNodeType); + if (nt instanceof NodeTypeImpl) { + nodeType = (NodeTypeImpl) nt; + } else { + cleanUp(); + throw new NotExecutableException("NodeTypeImpl expected."); + } + + if (superuser instanceof NameResolver) { + resolver = (NameResolver) superuser; + } else { + cleanUp(); + throw new NotExecutableException(); + } + } + + public void testIsNodeType() throws RepositoryException { + NodeType[] superTypes = nodeType.getSupertypes(); + + for (int i = 0; i < superTypes.length; i++) { + String name = superTypes[i].getName(); + assertTrue(nodeType.isNodeType(resolver.getQName(name))); + } + + // unknown nt + String unknownName = "unknown"; + assertFalse(nodeType.isNodeType(unknownName)); + + // all non-mixin node types must be derived from nt base. + if (!nodeType.isMixin()) { + assertTrue(nodeType.isNodeType("nt:base")); + } + } + +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeManagerImplTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeManagerImplTest.java new file mode 100644 index 00000000000..fa4861a9840 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/NodeTypeManagerImplTest.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.jcr2spi.nodetype; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; + +/** + * NodeTypeManagerImplTest... + */ +public class NodeTypeManagerImplTest extends AbstractJCRTest { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(NodeTypeManagerImplTest.class); + + private NodeTypeManager ntMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + ntMgr = superuser.getWorkspace().getNodeTypeManager(); + } + + public void testRegisterNodeTypes() throws RepositoryException { + NodeTypeTemplate test = ntMgr.createNodeTypeTemplate(); + test.setName("testNodeType"); + + ntMgr.registerNodeType(test, true); + + NodeType nt = ntMgr.getNodeType("testNodeType"); + assertNotNull(nt); + assertEquals("testNodeType", nt.getName()); + + test.setOrderableChildNodes(true); + + ntMgr.registerNodeType(test, true); + + nt = ntMgr.getNodeType("testNodeType"); + assertNotNull(nt); + assertEquals("testNodeType", nt.getName()); + assertEquals(test.hasOrderableChildNodes(), nt.hasOrderableChildNodes()); + + test.setDeclaredSuperTypeNames(new String[] {"nt:unstructured"}); + + try { + ntMgr.registerNodeType(test, false); + fail("NodeTypeExistsException expected"); + } catch (NodeTypeExistsException e) { + // success + } + } + + public void testUnregisterNodeTypes() throws RepositoryException { + NodeTypeTemplate test = ntMgr.createNodeTypeTemplate(); + test.setName("testNodeType2"); + + ntMgr.registerNodeType(test, true); + + NodeType nt = ntMgr.getNodeType("testNodeType2"); + assertNotNull(nt); + assertEquals("testNodeType2", nt.getName()); + + boolean supported = false; + try { + ntMgr.unregisterNodeType(test.getName()); + supported = true; + } catch (UnsupportedRepositoryOperationException e) { + // ok + } catch (RepositoryException e) { + // TODO improve + if (e.getMessage().contains("not yet implemented")) { + // ok (original message in jr-core) + } else { + throw e; + } + + } + + if (supported) { + try { + ntMgr.getNodeType("testNodeType2"); + fail("should not be available any more"); + } catch (NoSuchNodeTypeException e) { + // success + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/RemoveMixinTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/RemoveMixinTest.java new file mode 100644 index 00000000000..b4065ad5dc2 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/RemoveMixinTest.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.jcr2spi.nodetype; + +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeManager; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoveMixinTest... + */ +public class RemoveMixinTest extends AbstractJCRTest { + + private static Logger log = LoggerFactory.getLogger(RemoveMixinTest.class); + + private NodeTypeManager ntMgr; + + @Override + protected void setUp() throws Exception { + super.setUp(); + ntMgr = testRootNode.getSession().getWorkspace().getNodeTypeManager(); + } + + @Override + protected void tearDown() throws Exception { + ntMgr = null; + super.tearDown(); + } + + /** + * Implementation specific test for 'removeMixin' only taking effect upon + * save. + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testRemoveMixinTakingAffectUponSave() throws NotExecutableException, RepositoryException { + Node node; + try { + node = testRootNode.addNode(nodeName1, testNodeType); + node.addMixin(mixReferenceable); + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + node.removeMixin(mixReferenceable); + assertTrue("Removing Mixin must not take effect but after Node has been saved.", node.isNodeType(mixReferenceable)); + List mixins = Arrays.asList(node.getMixinNodeTypes()); + assertTrue("Removing Mixin must not take effect but after Node has been saved.", mixins.contains(ntMgr.getNodeType(mixReferenceable))); + } + + /** + * Implementation specific test + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testAddAndRemoveMixinFromNew() throws NotExecutableException, RepositoryException { + Node node; + try { + node = testRootNode.addNode(nodeName1, testNodeType); + node.addMixin(mixReferenceable); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + node.removeMixin(mixReferenceable); + testRootNode.save(); + + assertFalse("Adding + Removing a mixin within the same batch must have not effect.", node.isNodeType(mixReferenceable)); + List mixins = Arrays.asList(node.getMixinNodeTypes()); + assertFalse("Adding + Removing a mixin within the same batch must have not effect.", mixins.contains(ntMgr.getNodeType(mixReferenceable))); + } + + /** + * Implementation specific test + * + * @throws NotExecutableException + * @throws RepositoryException + */ + public void testAddAndRemoveMixin() throws NotExecutableException, RepositoryException { + Node node; + try { + node = testRootNode.addNode(nodeName1, testNodeType); + testRootNode.save(); + } catch (RepositoryException e) { + throw new NotExecutableException(); + } + + node.addMixin(mixReferenceable); + node.removeMixin(mixReferenceable); + testRootNode.save(); + + assertFalse("Adding + Removing a mixin within the same batch must have not effect.", node.isNodeType(mixReferenceable)); + List mixins = Arrays.asList(node.getMixinNodeTypes()); + assertFalse("Adding + Removing a mixin within the same batch must have not effect.", mixins.contains(ntMgr.getNodeType(mixReferenceable))); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/TestAll.java new file mode 100644 index 00000000000..98728534936 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/nodetype/TestAll.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.jcr2spi.nodetype; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * TestAll... + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("jcr2spi nodetype tests"); + + suite.addTestSuite(AddMixinTest.class); + suite.addTestSuite(RemoveMixinTest.class); + suite.addTestSuite(MandatoryItemTest.class); + + suite.addTestSuite(NodeTypeImplTest.class); + suite.addTestSuite(NodeTypeManagerImplTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/observation/ObservationTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/observation/ObservationTest.java new file mode 100644 index 00000000000..f1db4952e0f --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/observation/ObservationTest.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.jcr2spi.observation; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; + +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ObservationTest extends AbstractJCRTest { + private static Logger log = LoggerFactory.getLogger(ObservationTest.class); + + private Node testNode; + + @Override + protected void setUp() throws Exception { + super.setUp(); + testNode = testRootNode.addNode(nodeName1); + testRootNode.save(); + } + + @Override + protected void tearDown() throws Exception { + testNode = null; + super.tearDown(); + } + + interface WaitableEventListener extends EventListener { + public void waitForEvent(int timeout) throws InterruptedException, RepositoryException; + } + + /** + * Check whether an item with the path of an add node event exists. + * Regression test for JCR-2293. + * @throws RepositoryException + * @throws InterruptedException + */ + public void testJCR_2293() throws RepositoryException, InterruptedException { + final String parentPath = testNode.getPath(); + final String folderName = "folder_" + System.currentTimeMillis(); + final Session session = getHelper().getReadWriteSession(); + + final Session session2 = getHelper().getReadOnlySession(); + session2.getItem(parentPath); // Don't remove. See JCR-2293. + + WaitableEventListener eventListener = new WaitableEventListener() { + private RepositoryException failure; + private boolean done; + + public synchronized void onEvent(final EventIterator events) { + try { + while (events.hasNext()) { + Event event = events.nextEvent(); + Item item2 = session2.getItem(event.getPath()); + assertEquals(parentPath + "/" + folderName, item2.getPath()); + } + } + catch (RepositoryException e) { + failure = e; + } + finally { + done = true; + notifyAll(); + } + } + + public synchronized void waitForEvent(int timeout) throws InterruptedException, RepositoryException { + if (!done) { + wait(timeout); + } + if (!done) { + fail("Event listener not called"); + } + if (failure != null) { + throw failure; + } + } + }; + + session2.getWorkspace().getObservationManager() + .addEventListener(eventListener, Event.NODE_ADDED, + parentPath, true, null, null, false); + + Node parent = (Node) session.getItem(parentPath); + Node toDelete = parent.addNode(folderName, "nt:folder"); + parent.save(); + + try { + eventListener.waitForEvent(60000); + } + finally { + toDelete.remove(); + parent.save(); + assertFalse(parent.hasNode(folderName)); + } + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/observation/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/observation/TestAll.java new file mode 100644 index 00000000000..b3a0d35ad20 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/observation/TestAll.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.jcr2spi.observation; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * TestAll... + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("jcr2spi observation tests"); + + suite.addTestSuite(ObservationTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/query/QueryTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/query/QueryTest.java new file mode 100644 index 00000000000..945273274cd --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/query/QueryTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. The ASF licenses this file to You + * under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.jcr2spi.query; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.query.AbstractQueryTest; + +import javax.jcr.RepositoryException; +import javax.jcr.NodeIterator; +import javax.jcr.Node; +import javax.jcr.NamespaceRegistry; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import java.util.List; +import java.util.ArrayList; + +/** + * QueryTest performs various query tests. + */ +public class QueryTest extends AbstractQueryTest { + + /** + * Queries the child nodes of the root node. + * @throws NotExecutableException + */ + public void testQueryChildNodesOfRoot() throws RepositoryException, NotExecutableException { + List nodes = new ArrayList(); + for (NodeIterator it = superuser.getRootNode().getNodes(); it.hasNext(); ) { + nodes.add(it.nextNode()); + } + Node[] children = nodes.toArray(new Node[nodes.size()]); + executeXPathQuery(superuser, "/jcr:root/*", children); + } + + public void testRemappedNamespace() throws RepositoryException, NotExecutableException { + String namespaceURI = "http://jackrabbit.apache.org/spi/test"; + String defaultPrefix = "spiTest"; + + NamespaceRegistry nsReg = superuser.getWorkspace().getNamespaceRegistry(); + try { + nsReg.getPrefix(namespaceURI); + } catch (RepositoryException e) { + nsReg.registerNamespace(defaultPrefix, namespaceURI); + } + + Node n = testRootNode.addNode("spiTest:node"); + superuser.save(); + + for (int i = 0; i < 10; i++) { + String prefix = defaultPrefix + i; + superuser.setNamespacePrefix(prefix, namespaceURI); + executeXPathQuery(superuser, testPath + "/" + prefix + ":node", new Node[]{n}); + } + } + + /** + * https://issues.apache.org/jira/browse/JCR-3089 + */ + public void testSQL2Simple() throws Exception { + Query q = qm.createQuery("SELECT * FROM [nt:unstructured]", + Query.JCR_SQL2); + QueryResult r = q.execute(); + assertTrue(r.getNodes().hasNext()); + } + + /** + * https://issues.apache.org/jira/browse/JCR-2543 + */ + public void testSQL2Limit() throws Exception { + Query q = qm.createQuery("SELECT * FROM [nt:unstructured]", + Query.JCR_SQL2); + q.setLimit(1); + QueryResult r = q.execute(); + + NodeIterator it = r.getNodes(); + assertTrue(it.hasNext()); + it.next(); + assertFalse(it.hasNext()); + } + + /** + * https://issues.apache.org/jira/browse/JCR-3089 + */ + public void testSQL2Join() throws Exception { + // he query is not supposed to return anything, it will just check that + // the back and forth between the client and the server works + Query q = qm + .createQuery( + "SELECT * FROM [nt:unstructured] AS a INNER JOIN [nt:unstructured] AS b ON b.[refid] = a.[jcr:uuid]", + Query.JCR_SQL2); + assertNotNull(q.execute()); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/query/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/query/TestAll.java new file mode 100644 index 00000000000..83855ae799f --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/query/TestAll.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.jcr2spi.query; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * TestAll... + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("jcr2spi query tests"); + + suite.addTestSuite(QueryTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/Jcr2SpiSecurityTestSuite.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/Jcr2SpiSecurityTestSuite.java new file mode 100644 index 00000000000..2847fa36b53 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/Jcr2SpiSecurityTestSuite.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.jcr2spi.security; + +import junit.framework.TestSuite; + +/** + * Jcr2SpiTestSuite... + */ +public class Jcr2SpiSecurityTestSuite extends TestSuite { + + public Jcr2SpiSecurityTestSuite() { + super("JCR2SPI Security tests"); + + // all jcr2spi security tests + addTest(org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.acl.TestAll.suite()); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlListImplTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlListImplTest.java new file mode 100644 index 00000000000..9c6a9789d71 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlListImplTest.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.jcr2spi.security.authorization.jackrabbit.acl; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.Privilege; + +import junit.framework.Assert; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; + +/** + * Tests the functionality of the JCR AccessControlList API implementation. The + * purpose is to test the consistency of the access control list by a, adding , + * deleting and modifying entries in the list. + */ +public class AccessControlListImplTest extends AbstractAccessControlTest { + + private QValueFactory vFactory; + + private Principal unknownPrincipal; + private Principal knownPrincipal; + + private NamePathResolver resolver; + + @Override + public void setUp() throws Exception { + super.setUp(); + + resolver = new DefaultNamePathResolver(superuser); + vFactory = QValueFactoryImpl.getInstance(); + + unknownPrincipal = getHelper().getUnknownPrincipal(superuser); + knownPrincipal = new Principal() { + @Override + public String getName() { + return "everyone"; + } + }; + } + + private JackrabbitAccessControlList createAccessControList(String aclPath) + throws RepositoryException { + return new AccessControlListImpl(aclPath, resolver, vFactory); + } + + private Map createEmptyRestriction() { + return Collections. emptyMap(); + } + + public void testAddingDifferentEntries() throws Exception { + JackrabbitAccessControlList acl = createAccessControList(testRoot); + + // allow read to unknownPrincipal + Privilege[] p = privilegesFromName(Privilege.JCR_READ); + acl.addAccessControlEntry(unknownPrincipal, p); + + // allow addChildNodes to secondPrincipal + p = privilegesFromName(Privilege.JCR_ADD_CHILD_NODES); + acl.addAccessControlEntry(knownPrincipal, p); + + // deny modifyAccessControl to 'unknown' principal + p = privilegesFromName(Privilege.JCR_MODIFY_ACCESS_CONTROL); + acl.addEntry(unknownPrincipal, p, false); + + // deny jcr:nodeTypeManagement to secondPrincipal + p = privilegesFromName(Privilege.JCR_NODE_TYPE_MANAGEMENT); + acl.addEntry(knownPrincipal, p, false); + + // four different entries + Assert.assertEquals(4, acl.size()); + + // UnknownPrincipal entries + AccessControlEntry[] pentries = getEntries(acl, unknownPrincipal); + Assert.assertEquals(2, pentries.length); + + // secondPrincipal entries + AccessControlEntry[] sentries = getEntries(acl, knownPrincipal); + Assert.assertEquals(2, sentries.length); + + } + + public void testMultipleEntryEffect() throws Exception { + JackrabbitAccessControlList acl = createAccessControList(testRoot); + Privilege[] privileges = privilegesFromName(Privilege.JCR_READ); + + // GRANT 'read' privilege to the Admin user -> list now contains one + // allow entry + assertTrue(acl.addAccessControlEntry(unknownPrincipal, privileges)); + + // policy contains a single entry + assertEquals(1, acl.size()); + + AccessControlEntry[] entries = acl.getAccessControlEntries(); + + // ... and the entry grants a single privilege + assertEquals(1, entries[0].getPrivileges().length); + assertEquals("jcr:read", entries[0].getPrivileges()[0].getName()); + + // GRANT 'add_child_node' privilege for the admin user -> same entry but + // with an additional 'add_child_node' privilege. + privileges = privilegesFromNames(new String[] {Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_READ }); + assertTrue(acl.addAccessControlEntry(unknownPrincipal, privileges)); + + // A new Entry was added -> entries count should be 2. + assertEquals(2, acl.size()); + + // The single entry should now contain both 'read' and 'add_child_nodes' + // privileges for the same principal. + assertEquals(1, acl.getAccessControlEntries()[0].getPrivileges().length); + assertEquals(2, acl.getAccessControlEntries()[1].getPrivileges().length); + + // adding a privilege that's already granted for the same principal -> + // again modified as the client doesn't care about possible compaction the + // server may want to make. + privileges = privilegesFromNames(new String[] { Privilege.JCR_READ }); + assertTrue(acl.addAccessControlEntry(unknownPrincipal, privileges)); + assertEquals(3, acl.size()); + + // revoke the read privilege + assertTrue("Fail to revoke read privilege", acl.addEntry(unknownPrincipal, privileges, false, createEmptyRestriction())); + + // should now be 3 entries -> 2 allow entry + a deny entry + assertEquals(4, acl.size()); + } + + public void testMultipleEntryEffect2() throws Exception { + JackrabbitAccessControlList acl = createAccessControList(testRoot); + // GRANT a read privilege + Privilege[] privileges = privilegesFromNames(new String[] { Privilege.JCR_READ }); + assertTrue("New Entry -> grants read privilege", acl.addAccessControlEntry(unknownPrincipal, privileges)); + + assertTrue("Fail to revoke the read privilege", acl.addEntry(unknownPrincipal, privileges, false, createEmptyRestriction())); + Assert.assertEquals(2, acl.size()); + } + + // -------------------------------------------------------< utility methods >--- + + private AccessControlEntry[] getEntries(AccessControlList acl, Principal princ) throws RepositoryException { + AccessControlEntry[] entries = acl.getAccessControlEntries(); + List entriesPerPrincipal = new ArrayList(2); + for (AccessControlEntry entry : entries) { + if (entry.getPrincipal().getName().equals(princ.getName())) { + entriesPerPrincipal.add(entry); + } + } + return entriesPerPrincipal.toArray(new AccessControlEntry[entriesPerPrincipal.size()]); + } + } diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlManagerImplTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlManagerImplTest.java new file mode 100644 index 00000000000..8243dbaa27a --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/AccessControlManagerImplTest.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.jcr2spi.security.authorization.jackrabbit.acl; + +import java.security.Principal; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.api.security.AbstractAccessControlTest; + +public class AccessControlManagerImplTest extends AbstractAccessControlTest { + + @Override + public void setUp() throws Exception { + super.setUp(); + } + + private Principal getUnknownPrincipal() throws NotExecutableException, RepositoryException { + return getHelper().getUnknownPrincipal(superuser); + } + + public void testGetAndHasPrivileges() throws Exception { + Privilege[] privileges = acMgr.getPrivileges(testRoot); + assertNotNull(privileges); + assertTrue(acMgr.hasPrivileges(testRoot, privileges)); + } + + /** + * Tests the binding state of a policy. + * @throws Exception + */ + public void testGetPolicesAfterSetPoliciesCall() throws Exception { + try { + AccessControlPolicyIterator policies = acMgr.getApplicablePolicies(testRoot); + AccessControlPolicy policy = null; + while (policies.hasNext()) { + policy = policies.nextAccessControlPolicy(); + acMgr.setPolicy(testRoot, policy); + AccessControlPolicy[] acl = acMgr.getPolicies(testRoot); + assertNotNull(acl); + } + } finally { + superuser.refresh(false); + } + } + + /** + * This should be able to return the policies that has been transiently added + * to the node at testRoot, as the getPolicies api specifies that the method should + * take the transient changes into account. + * @throws Exception + */ + public void testRemovePolicyAfterASetPoliciesCall() throws Exception { + try { + AccessControlPolicyIterator policies = acMgr.getApplicablePolicies(testRoot); + while (policies.hasNext()) { + AccessControlList acl = (AccessControlListImpl) policies.nextAccessControlPolicy(); + + // GRANT read privilege + acl.addAccessControlEntry(getUnknownPrincipal(), privilegesFromName(Privilege.JCR_READ)); + + acMgr.setPolicy(testRoot, acl); + + AccessControlPolicy[] transientPolicy = acMgr.getPolicies(testRoot); + + acMgr.removePolicy(testRoot, transientPolicy[0]); + + assertEquals(0, acMgr.getPolicies(testRoot).length); + } + + } finally { + superuser.refresh(false); + } + } + + /** + * Test removing an effective policy. + */ + public void testRemovePolicyAfterASaveCall() throws Exception { + try { + AccessControlList[] acl = (AccessControlList[]) acMgr.getPolicies(testRoot); + if (acl.length > 0) { + acMgr.removePolicy(testRoot, acl[0]); + } else { + AccessControlPolicy policy = acMgr.getApplicablePolicies(testRoot).nextAccessControlPolicy(); + acMgr.setPolicy(testRoot, policy); + acMgr.removePolicy(testRoot, policy); + } + + // transient removal + AccessControlPolicy[] noPolicies = acMgr.getPolicies(testRoot); + assertEquals(0, noPolicies.length); + + // save changes -> removal of protected items on jcr-server + superuser.save(); + } catch (Exception e) { + throw new RepositoryException(e.getMessage()); + } finally { + superuser.refresh(false); + } + } + + /** + * JCR mandates that the path specified for getPrivileges method must + * be absolute and points to an existing node. + * @throws Exception + */ + public void testGetPrivilegesOnNonExistingNode() throws Exception { + try { + acMgr.getPrivileges(getPathToNonExistingNode()); + fail("Must throw a PathNotFoundException"); + } catch (PathNotFoundException e) { + // success + } + } + + /** + * Add an AccessControlList with four entries. This will result in having the result in: + * Transient-space: An ACL node that has four child-nodes. + * Persistent-state: An ACL node that has one child-node. + * NOTE: That Jackrabbit-core tries to internally merge the entries that belongs to the same + * principal, which is not the case for the client-side ACM implementation. + */ + public void testAddingFourAccessControlEntries() throws Exception { + try { + AccessControlList acl = (AccessControlList) getACL(testRoot); + + // check precondition,see JCR-3995 + if (testRootNode.hasNode("rep:policy")) { + assertEquals("should not have any ace nodes at this point", 0, + testRootNode.getNode("rep:policy").getNodes().getSize()); + } + + acl.addAccessControlEntry(getUnknownPrincipal(), privilegesFromName(Privilege.JCR_READ)); + acl.addAccessControlEntry(getUnknownPrincipal(), privilegesFromName(Privilege.JCR_READ)); + acl.addAccessControlEntry(getUnknownPrincipal(), privilegesFromName(Privilege.JCR_READ)); + acl.addAccessControlEntry(getUnknownPrincipal(), privilegesFromName(Privilege.JCR_READ)); + + acMgr.setPolicy(testRoot, acl); + + // Transient-space: Must contain FOUR ace nodes. + assertEquals(4, testRootNode.getNode("rep:policy").getNodes().getSize()); + + superuser.save(); + + // Persistent-state: Must contain a single ace node -> entries were + // merged + assertEquals(1, testRootNode.getNode("rep:policy").getNodes().getSize()); + } finally { + superuser.refresh(false); + } + } + + /** + * Test retrieving a policy after a save call. + * @throws Exception + */ + public void testGetPoliciesAfterASaveCall() throws Exception { + try { + JackrabbitAccessControlList policy = (JackrabbitAccessControlList) getACL(testRoot); + + String aclPath = policy.getPath(); + assertEquals(aclPath, testRoot); + + // GRANT 'read' privilege to principal + policy.addAccessControlEntry(getUnknownPrincipal(), privilegesFromName(Privilege.JCR_READ)); + + // GRANT 'add_child_nodes' privilege + policy.addAccessControlEntry(getUnknownPrincipal(), privilegesFromName(Privilege.JCR_ADD_CHILD_NODES)); + + // bind the policy and save changes + acMgr.setPolicy(testRoot, policy); + superuser.save(); + + Node aclNode = testRootNode.getNode("rep:policy"); + assertNotNull(aclNode); + + NodeIterator nit = aclNode.getNodes(); + + // Jackrabbit-core will merge the two entries -> only a single aceNode will be created. + assertEquals(1, nit.getSize()); + } finally { + superuser.refresh(false); + } + } + + private AccessControlPolicy getACL(String absPath) throws RepositoryException { + AccessControlList acl = null; + if (acMgr.getPolicies(absPath).length > 0) { + acl = (AccessControlList) acMgr.getPolicies(absPath)[0]; + } else { + acl = (AccessControlList) acMgr.getApplicablePolicies(absPath).nextAccessControlPolicy(); + } + return acl; + } + +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/TestAll.java new file mode 100644 index 00000000000..05d05f950d4 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/security/authorization/jackrabbit/acl/TestAll.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.jcr2spi.security.authorization.jackrabbit.acl; + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class TestAll { + + public static Test suite() { + + TestSuite suite = new TestSuite("jcr2spi jackrabbit security tests"); + + suite.addTestSuite(AccessControlListImplTest.class); + suite.addTestSuite(AccessControlManagerImplTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/version/LabelTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/version/LabelTest.java new file mode 100644 index 00000000000..59867926ae6 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/version/LabelTest.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.jcr2spi.version; + +import java.util.Arrays; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; + +import org.apache.jackrabbit.test.api.version.VersionLabelTest; + +public class LabelTest extends VersionLabelTest { + + public void testRemovedLabel2() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + vHistory.removeVersionLabel(versionLabel); + + List labels = Arrays.asList(vHistory.getVersionLabels()); + assertFalse("VersionHistory.getVersionLabels() must not return a removed label.",labels.contains(versionLabel)); + } + + public void testRemovedLabel3() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + vHistory.removeVersionLabel(versionLabel); + + List labels = Arrays.asList(vHistory.getVersionLabels(version)); + assertFalse("VersionHistory.getVersionLabels(Version) must not return a removed label.",labels.contains(versionLabel)); + } + + public void testMoveLabel2() throws RepositoryException { + vHistory.addVersionLabel(version.getName(), versionLabel, false); + + versionableNode.checkout(); + Version v = versionableNode.checkin(); + vHistory.addVersionLabel(v.getName(), versionLabel, true); + + List labels = Arrays.asList(vHistory.getVersionLabels(v)); + assertTrue(labels.contains(versionLabel)); + } + + public void testMoveLabel3() throws RepositoryException { + versionableNode.checkout(); + Version v = versionableNode.checkin(); + + vHistory.addVersionLabel(version.getName(), versionLabel, false); + vHistory.addVersionLabel(v.getName(), versionLabel, true); + + List labels = Arrays.asList(vHistory.getVersionLabels(version)); + assertFalse(labels.contains(versionLabel)); + } + + public void testMoveLabel4() throws RepositoryException { + versionableNode.checkout(); + Version v = versionableNode.checkin(); + + vHistory.addVersionLabel(version.getName(), versionLabel, false); + vHistory.addVersionLabel(v.getName(), versionLabel, true); + + Version v2 = vHistory.getVersionByLabel(versionLabel); + assertTrue(v2.isSame(v)); + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/version/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/version/TestAll.java new file mode 100644 index 00000000000..13be364f98c --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/version/TestAll.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.jcr2spi.version; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * TestAll... + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("jcr2spi version tests"); + + suite.addTestSuite(LabelTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/xml/SessionImportTest.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/xml/SessionImportTest.java new file mode 100644 index 00000000000..148b5ca31de --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/xml/SessionImportTest.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.jcr2spi.xml; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.test.AbstractJCRTest; +import org.apache.jackrabbit.test.NotExecutableException; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** + * SessionImportTest... + */ +public class SessionImportTest extends AbstractJCRTest { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testImportNameValueWithUnregisteredNamespace() throws RepositoryException, SAXException { + String prefix = getUniquePrefix(superuser); + String uri = getUnknownURI(superuser, "anyURI"); + String testValue = prefix + ":someLocalName"; + + String svuri = Name.NS_SV_URI; + String svprefix = Name.NS_SV_PREFIX + ":"; + + ContentHandler ch = superuser.getImportContentHandler(testRootNode.getPath(), ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW); + ch.startDocument(); + ch.startPrefixMapping(prefix, uri); + + String nN = "node"; + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute(svuri, "name", svprefix + "name", "CDATA", nodeName1); + ch.startElement(svuri, nN, svprefix + nN, attrs); + + // primary node type + String pN = "property"; + attrs = new AttributesImpl(); + attrs.addAttribute(svuri, "name", svprefix + "name", "CDATA", JcrConstants.JCR_PRIMARYTYPE); + attrs.addAttribute(svuri, "type", svprefix + "type", "CDATA", PropertyType.nameFromValue(PropertyType.NAME)); + ch.startElement(svuri, pN, svprefix + pN, attrs); + ch.startElement(svuri, "value", svprefix + "value", new AttributesImpl()); + char[] val = testNodeType.toCharArray(); + ch.characters(val, 0, val.length); + ch.endElement(svuri, "value", svprefix + "value"); + ch.endElement(svuri, pN, prefix + pN); + + // another name value + attrs = new AttributesImpl(); + attrs.addAttribute(svuri, "name", svprefix + "name", "CDATA", propertyName1); + attrs.addAttribute(svuri, "type", svprefix + "type", "CDATA", PropertyType.nameFromValue(PropertyType.NAME)); + ch.startElement(svuri, pN, svprefix + pN, attrs); + ch.startElement(svuri, "value", svprefix + "value", new AttributesImpl()); + val = testValue.toCharArray(); + ch.characters(val, 0, val.length); + ch.endElement(svuri, "value", svprefix + "value"); + ch.endElement(svuri, pN, svprefix + pN); + + ch.endElement(svuri, nN, svprefix + nN); + ch.endDocument(); + + // test if property has been imported with correct namespace + String assignedPrefix = superuser.getNamespacePrefix(uri); + assertTrue(superuser.getNamespaceURI(assignedPrefix).equals(uri)); + String path = testRootNode.getPath() + "/" + nodeName1 + "/" + propertyName1; + + assertTrue(superuser.itemExists(path)); + Item item = superuser.getItem(path); + if (item.isNode()) { + fail("Item with path " + path + " must be a property."); + } else { + Property prop = (Property) item; + assertTrue(prop.getValue().getType() == PropertyType.NAME); + String expectedValue = assignedPrefix + ":someLocalName"; + assertTrue(prop.getValue().getString().equals(expectedValue)); + } + + // try to save + superuser.save(); + } + + /** + * Test case for issue JCR-1857 + * + * @throws IOException + * @throws RepositoryException + */ + public void testEmptyMixins() throws IOException, RepositoryException { + String xml = "\n" + + "\n" + + " \n" + + " nt:unstructured\n" + + " \n" + + " \n" + + " Test Node\n" + + " \n" + + " \n" + + " 1234\n" + + " \n" + + ""; + + InputStream in = new ByteArrayInputStream(xml.getBytes()); + try { + superuser.importXML(testRootNode.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + fail("jcr:uuid cannot be created if mix:referenceable is not part of the effective nodetype."); + } catch (ConstraintViolationException e) { + // ok. + } + } + + /** + * Test case for issue JCR-1857 + * + * @throws IOException + * @throws RepositoryException + */ + public void testEmptyMixins2() throws IOException, RepositoryException, NotExecutableException { + /* + look for a a node type that includes mix:referenceable but isn't any + of the known internal nodetypes that ev. cannot be created through a + session-import + */ + String referenceableNt = null; + NodeTypeIterator it = superuser.getWorkspace().getNodeTypeManager().getPrimaryNodeTypes(); + while (it.hasNext() && referenceableNt == null) { + NodeType nt = it.nextNodeType(); + String ntName = nt.getName(); + if (nt.isNodeType(mixReferenceable) && + !nt.isAbstract() && + // TODO: improve.... + // ignore are built-in nodetypes (mostly version related) + !ntName.startsWith("nt:") && + // also skip all internal node types... + !ntName.startsWith("rep:")) { + referenceableNt = ntName; + } + } + if (referenceableNt == null) { + throw new NotExecutableException("No primary type found that extends from mix:referenceable."); + } + /* + TODO: retrieve valid jcr:uuid value from test-properties. + */ + String uuid = UUID.randomUUID().toString(); + String xml = "\n" + + "\n" + + " \n" + + " " + referenceableNt + "\n" + + " \n" + + " \n" + + " " + uuid + "\n" + + " \n" + + ""; + + InputStream in = new ByteArrayInputStream(xml.getBytes()); + superuser.importXML(testRootNode.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + } + + /** + * + * @throws IOException + * @throws RepositoryException + */ + public void testMixVersionable() throws IOException, RepositoryException { + String xml = "" + + "" + + "" + + " nt:unstructured" + + "" + + "" + + " mix:versionable" + + "" + + "" + + " 75806b92-317f-4cb3-bc3d-ee87a95cf21f" + + "" + + "" + + " 6b91c6e5-1b83-4921-94a1-5d92ca389b3f" + + "" + + "" + + " true" + + "" + + "" + + " 6b91c6e5-1b83-4921-94a1-5d92ca389b3f" + + "" + + "" + + " 99b5ec0f-49cb-4ccf-b9fd-9fba82349420" + + "" + + ""; + + InputStream in = new ByteArrayInputStream(xml.getBytes()); + superuser.importXML(testRootNode.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + superuser.save(); + + assertTrue("test node must be present", testRootNode.hasNode("test")); + Node n = testRootNode.getNode("test"); + assertTrue("node must be mix:versionable", n.isNodeType(mixVersionable)); + assertTrue("node must be mix:referenceable", n.isNodeType(mixReferenceable)); + assertEquals("75806b92-317f-4cb3-bc3d-ee87a95cf21f", n.getUUID()); + } + + private static String getUnknownURI(Session session, String uriHint) throws RepositoryException { + String uri = uriHint; + int index = 0; + List uris = Arrays.asList(session.getWorkspace().getNamespaceRegistry().getURIs()); + while (uris.contains(uri)) { + uri = uriHint + index; + index++; + } + return uri; + } + + /** + * Returns a prefix that is unique among the already registered prefixes. + * + * @param session + * @return a unique prefix + */ + public static String getUniquePrefix(Session session) throws RepositoryException { + return "_pre" + (session.getNamespacePrefixes().length + 1); + } +} \ No newline at end of file diff --git a/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/xml/TestAll.java b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/xml/TestAll.java new file mode 100644 index 00000000000..eaa7b80b309 --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/java/org/apache/jackrabbit/jcr2spi/xml/TestAll.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.jcr2spi.xml; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * TestAll... + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("jcr2spi xml tests"); + + suite.addTestSuite(SessionImportTest.class); + + return suite; + } +} diff --git a/jackrabbit-jcr2spi/src/test/resources/accessControlProvider.properties b/jackrabbit-jcr2spi/src/test/resources/accessControlProvider.properties new file mode 100644 index 00000000000..d89cc0e157c --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/resources/accessControlProvider.properties @@ -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.jcr2spi.AccessControlProvider.class=org.apache.jackrabbit.jcr2spi.security.authorization.jackrabbit.acl.AccessControlProviderImpl diff --git a/jackrabbit-jcr2spi/src/test/resources/logback-test.xml b/jackrabbit-jcr2spi/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..15d98f9b73f --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/resources/logback-test.xml @@ -0,0 +1,31 @@ + + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-jcr2spi/src/test/resources/org/apache/jackrabbit/jcr2spi/default-nodetypes.cnd b/jackrabbit-jcr2spi/src/test/resources/org/apache/jackrabbit/jcr2spi/default-nodetypes.cnd new file mode 100644 index 00000000000..35c5f47a3af --- /dev/null +++ b/jackrabbit-jcr2spi/src/test/resources/org/apache/jackrabbit/jcr2spi/default-nodetypes.cnd @@ -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. + */ +<''=''> + + + + + + + + + +//------------------------------------------------------------------------------ +// B A S E T Y P E S +//------------------------------------------------------------------------------ + +[nt:base] + - jcr:primaryType (name) mandatory autocreated protected compute + - jcr:mixinTypes (name) protected multiple compute + +[nt:unstructured] > nt:base + orderable + - * (undefined) multiple + - * (undefined) + + * (nt:base) = nt:unstructured multiple version + +[rep:root] > nt:unstructured + diff --git a/jackrabbit-parent/pom.xml b/jackrabbit-parent/pom.xml new file mode 100644 index 00000000000..4b67b13fa8d --- /dev/null +++ b/jackrabbit-parent/pom.xml @@ -0,0 +1,1008 @@ + + + + + + 4.0.0 + + + + + + + org.apache + apache + 18 + + + + org.apache.jackrabbit + jackrabbit-parent + Jackrabbit Parent POM + 2.17.5-SNAPSHOT + pom + + + Jira + http://issues.apache.org/jira/browse/JCR + + + + -Xmx256m + + ${test.opts.modules} ${test.opts.coverage} ${test.opts.memory} -enableassertions + 9.2.24.v20180105 + 1.18 + ${project.build.sourceEncoding} + 1.7.25 + 1.7.25 + 1.2.3 + 1.8 + java18 + true + 0.8.1 + + + http://jackrabbit.apache.org/ + 2004 + + The Apache Jackrabbit™ content repository is a fully conforming + implementation of the Content Repository for Java Technology API + (JCR, specified in JSR 170 and 283). A content repository is a + hierarchical content store with support for structured and unstructured + content, full text search, versioning, transactions, observation, and more. + Apache Jackrabbit is a project of the Apache Software Foundation. + + + + + + + 3.2.1 + + + + + pedantic + + + + org.apache.rat + apache-rat-plugin + + + verify + + check + + + + + + + + + jdk-tools + + ${java.home}/../lib/tools.jar + + + + + maven-antrun-plugin + + + sun + tools + 1.0 + system + ${java.home}/../lib/tools.jar + + + + + + + + integrationTesting + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + + + coverage + + false + + + + + + + + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.16 + + + org.codehaus.mojo.signature + ${java.version.signature} + 1.0 + + + + + compile + + check + + + + + + + maven-javadoc-plugin + + ${java.version} + true + + https://docs.adobe.com/docs/en/spec/javax.jcr/javadocs/jcr-2.0/ + https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/ + https://hc.apache.org/httpcomponents-client-ga/httpmime/apidocs/ + https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs + + + + + + + maven-idea-plugin + + true + ${java.version} + + + + + maven-eclipse-plugin + + true + + + + maven-release-plugin + + true + + + + org.apache.rat + apache-rat-plugin + + + release.properties + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + default-prepare-agent + + prepare-agent + + + ${skip.coverage} + test.opts.coverage + + + + default-prepare-agent-integration + + prepare-agent-integration + + + ${skip.coverage} + test.opts.coverage + + + + default-report + + report + + + ${skip.coverage} + + + + default-report-integration + + report-integration + + + + default-check + + check + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + false + deploy + -Papache-release,pedantic ${arguments} + + + + maven-surefire-plugin + 2.22.0 + + + maven-failsafe-plugin + 2.22.0 + + + maven-jar-plugin + 3.1.0 + + + maven-war-plugin + 3.2.0 + + + maven-assembly-plugin + 3.1.0 + + + maven-idea-plugin + 2.2.1 + + + maven-eclipse-plugin + 2.10 + + + + org.apache.felix + maven-bundle-plugin + 3.3.0 + true + + + ${project.groupId}.${project.artifactId} + jcr,jackrabbit + + http://jackrabbit.apache.org + + + + + + verify + + baseline + + + + 2.16.2 + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + 2.5 + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.22.0 + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.0.0 + + + org.apache.maven.plugins + maven-pmd-plugin + 3.10.0 + + ${java.version} + + + + com.github.spotbugs + spotbugs-maven-plugin + 3.1.5 + + + + + + + + javax.jcr + jcr + 2.0 + provided + + + org.apache.jackrabbit + jackrabbit-jcr-benchmark + 1.5.0 + + + concurrent + concurrent + 1.3.4 + + + commons-collections + commons-collections + 3.2.2 + + + commons-io + commons-io + 2.6 + + + javax.transaction + javax.transaction-api + 1.3 + + + org.slf4j + slf4j-api + ${slf4j.api.version} + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + org.apache.lucene + lucene-core + 3.6.0 + + + org.apache.tika + tika-core + ${tika.version} + + + org.apache.tika + tika-parsers + ${tika.version} + + + + + + edu.ucar + netcdf + + + commons-httpclient + commons-httpclient + + + + + + org.apache.james + apache-mime4j-core + + + org.apache.james + apache-mime4j-dom + + + + + org.apache.commons + commons-compress + + + + + + asm + asm + + + + + + com.drewnoakes + metadata-extractor + + + + + rome + rome + + + + + de.l3s.boilerpipe + boilerpipe + + + + + org.apache.derby + derby + 10.14.2.0 + + + org.apache.geronimo.specs + geronimo-j2ee-connector_1.5_spec + 1.0 + + + javax.servlet + servlet-api + 2.5 + provided + + + commons-fileupload + commons-fileupload + 1.3.3 + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + org.eclipse.jetty + jetty-servlet + ${jetty.version} + + + org.eclipse.jetty + jetty-webapp + ${jetty.version} + + + org.eclipse.jetty + jetty-jsp + ${jetty.version} + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.osgi + org.osgi.annotation + 6.0.0 + + + cglib + cglib + 2.1_3 + + + org.easymock + easymock + 3.4 + + + junit + junit + 4.12 + + + + com.google.code.findbugs + jsr305 + 2.0.0 + provided + + + org.mockito + mockito-core + 1.10.19 + + + + + + + + + + Jackrabbit Announce List + announce-subscribe@jackrabbit.apache.org + announce-unsubscribe@jackrabbit.apache.org + + http://mail-archives.apache.org/mod_mbox/jackrabbit-announce/ + + + + http://jackrabbit.markmail.org/ + + + + + Jackrabbit Users List + users-subscribe@jackrabbit.apache.org + users-unsubscribe@jackrabbit.apache.org + users at jackrabbit.apache.org + + http://mail-archives.apache.org/mod_mbox/jackrabbit-users/ + + + + http://jackrabbit.markmail.org/ + + + http://dir.gmane.org/gmane.comp.apache.jackrabbit.user + + + http://www.mail-archive.com/users@jackrabbit.apache.org/ + + + http://www.nabble.com/Jackrabbit---Users-f14897.html + + + + + Jackrabbit Development List + dev-subscribe@jackrabbit.apache.org + dev-unsubscribe@jackrabbit.apache.org + dev at jackrabbit.apache.org + + http://mail-archives.apache.org/mod_mbox/jackrabbit-dev/ + + + + http://jackrabbit.markmail.org/ + + + http://dir.gmane.org/gmane.comp.apache.jackrabbit.devel + + + http://www.mail-archive.com/dev@jackrabbit.apache.org/ + + + http://www.mail-archive.com/jackrabbit-dev@incubator.apache.org/ + + + http://www.nabble.com/Jackrabbit---Dev-f371.html + + + + + Jackrabbit Source Control List + commits-subscribe@jackrabbit.apache.org + commits-unsubscribe@jackrabbit.apache.org + + http://mail-archives.apache.org/mod_mbox/jackrabbit-commits/ + + + + http://jackrabbit.markmail.org/ + + + + + + + + + + + Alexander Klimetschek + alexkli + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Alex Parvulescu + alexparvulescu + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Angela Schreiber + angela + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Ard Schrijvers + ard + Hippo + http://www.hippo.nl/ + + committer + PMC member + + +1 + + + Christoph Kiehl + ckiehl + + committer + PMC member + + +1 + + + Christophe Lombart + clombart + + committer + PMC member + + + + Claus Köll + ckoell + + committer + PMC member + + + + David Nuescheler + uncled + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Dominique Pfister + dpfister + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Edgar Poce + edgarpoce + + committer + PMC member + + + + Esteban Franqueiro + eaf + + committer + PMC member + + + + Felix Meschberger + fmeschbe + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Jukka Zitting + jukka + Adobe + http://www.day.com/ + + committer + PMC chair + + +1 + + + Julian Reschke + reschke + greenbytes GmbH + http://www.greenbytes.de/ + + committer + PMC member + + +1 + + + Marcel Reutegger + mreutegg + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Martijn Hendriks + martijnh + GX + http://www.gx.nl/ + + committer + PMC member + + +1 + + + Michael Dürig + mduerig + Adobe + http://www.day.com/ + + committer + PMC member + + +0 + + + Peeter Piegaze + ppiegaze + Adobe + http://www.day.com/ + + committer + PMC member + + -5 + + + Przemo Pakulski + ppakulski + Cognifide + http://www.cognifide.com/ + + committer + PMC member + + +1 + + + Roy T. Fielding + fielding + Adobe + http://www.day.com/ + + committer + PMC member + + -8 + + + Sébastien Launay + sebastien + Anyware Technologies + http://www.anyware-tech.com/ + + committer + PMC member + + +1 + + + Serge Huber + shuber + + committer + PMC member + + +1 + + + Stefan Guggisberg + stefan + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Sylvain Wallez + sylvain + + committer + PMC member + + +1 + + + Thomas Müller + thomasm + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + Tobias Bocanegra + tripod + Adobe + http://www.day.com/ + + committer + PMC member + + +1 + + + + + Andrew Savory + asavory + + emeritus + + +0 + + + Brian Moseley + moseley + Open Source Applications Foundation (OSAF) + http://www.osafoundation.org/ + + emeritus + + -8 + + + Gianugo Rabellino + gianugo + + emeritus + + +1 + + + Paul Russell + prussell + + emeritus + + +0 + + + Stefano Mazzocchi + stefano + + emeritus + + -5 + + + Tim Reilly + treilly + + emeritus + + -5 + + + diff --git a/jackrabbit-spi-commons/README.txt b/jackrabbit-spi-commons/README.txt new file mode 100644 index 00000000000..26e1d855a15 --- /dev/null +++ b/jackrabbit-spi-commons/README.txt @@ -0,0 +1,10 @@ +================================= +Welcome to Jackrabbit SPI Commons +================================= + +This is the SPI Commons component of the Apache Jackrabbit project. +This component contains generic utility classes that might be used +to build an SPI implementation. +In addition this component provides utilities used to convert JCR +name and path Strings as well as values to their corresponding SPI +representation. diff --git a/jackrabbit-spi-commons/pom.xml b/jackrabbit-spi-commons/pom.xml new file mode 100644 index 00000000000..97d26020ce2 --- /dev/null +++ b/jackrabbit-spi-commons/pom.xml @@ -0,0 +1,182 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-spi-commons + Jackrabbit SPI Commons + bundle + + + org/apache/jackrabbit/spi/commons/query/sql/*.java,org/apache/jackrabbit/spi/commons/query/xpath/*.java + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.rat + apache-rat-plugin + + + src/main/javacc/xpath/* + + + + + org.codehaus.mojo + cobertura-maven-plugin + + + + org/apache/jackrabbit/spi/commons/query/xpath/XPathTokenManager.class + + + + + + + + + + org.osgi + org.osgi.annotation + provided + + + javax.jcr + jcr + + + org.apache.jackrabbit + jackrabbit-spi + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + commons-collections + commons-collections + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + test + + + junit + junit + test + + + + + + javacc + + + + org.codehaus.mojo + javacc-maven-plugin + 2.6 + + + sql + + ${basedir}/src/main/javacc/sql + org.apache.jackrabbit.spi.commons.query.sql + + + jjtree-javacc + + + + xpath + + ${basedir}/src/main/javacc/xpath + org.apache.jackrabbit.spi.commons.query.xpath + + + jjtree-javacc + + + + + + maven-antrun-plugin + + + process-sources + + + Remove files that have been customized in Jackrabbit + + + + + + + + + + + + + + + + run + + + + + + ant + ant-optional + 1.5.3-1 + + + + + + + + + diff --git a/jackrabbit-spi-commons/src/main/appended-resources/META-INF/LICENSE b/jackrabbit-spi-commons/src/main/appended-resources/META-INF/LICENSE new file mode 100644 index 00000000000..d01561a9899 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/appended-resources/META-INF/LICENSE @@ -0,0 +1,60 @@ +APACHE JACKRABBIT SUBCOMPONENTS + +This component includes parts with separate copyright notices and license +terms. Your use of these subcomponents is subject to the terms and conditions +of the following licenses: + + XPath 2.0/XQuery 1.0 Parser: + http://www.w3.org/2002/11/xquery-xpath-applets/xgrammar.zip + + Copyright (C) 2002 World Wide Web Consortium, (Massachusetts Institute of + Technology, European Research Consortium for Informatics and Mathematics, + Keio University). All Rights Reserved. + + This work is distributed under the W3C(R) Software License in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + W3C(R) SOFTWARE NOTICE AND LICENSE + http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + + This work (and included software, documentation such as READMEs, or + other related items) is being provided by the copyright holders under + the following license. By obtaining, using and/or copying this work, + you (the licensee) agree that you have read, understood, and will comply + with the following terms and conditions. + + Permission to copy, modify, and distribute this software and its + documentation, with or without modification, for any purpose and + without fee or royalty is hereby granted, provided that you include + the following on ALL copies of the software and documentation or + portions thereof, including modifications: + + 1. The full text of this NOTICE in a location viewable to users + of the redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, + or terms and conditions. If none exist, the W3C Software Short + Notice should be included (hypertext is preferred, text is + permitted) within the body of any redistributed or derivative code. + + 3. Notice of any changes or modifications to the files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT + HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR + DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, + TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and + any associated documentation will at all times remain with + copyright holders. diff --git a/jackrabbit-spi-commons/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-spi-commons/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AbstractReadableRepositoryService.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AbstractReadableRepositoryService.java new file mode 100644 index 00000000000..8852c7d53b5 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AbstractReadableRepositoryService.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Credentials; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.SessionInfo; + +/** + * AbstractReadableRepositoryService provides an abstract base + * class where all methods that attempt to write throw an + * {@link UnsupportedRepositoryOperationException}. This class useful for + * repository service implementation that only provide read access to the + * underlying content. + */ +public abstract class AbstractReadableRepositoryService extends AbstractRepositoryService { + + protected static final Set WRITE_ACTIONS = new HashSet( + Arrays.asList("add_node", "set_property", "remove")); + + /** + * The list of workspaces that this repository service exposes. + */ + protected final List wspNames; + + /** + * The name of the default workspace + */ + protected final String defaulWsp; + + /** + * Creates a new AbstractReadableRepositoryService. + * + * @param descriptors the repository descriptors. Maps descriptor keys to + * descriptor values. + * @param namespaces the namespaces. Maps namespace prefixes to namespace + * URIs. + * @param cnd a reader on the compact node type definition. + * @param wspNames a list of workspace names. + * @param defaultWsp name of the default workspace + * @throws RepositoryException if the namespace mappings are invalid. + * @throws ParseException if an error occurs while parsing the CND. + * @throws IllegalArgumentException if defaultWsp is null + */ + public AbstractReadableRepositoryService(Map descriptors, + Map namespaces, + Reader cnd, + List wspNames, + String defaultWsp) + throws RepositoryException, ParseException, IllegalArgumentException { + + super(descriptors, namespaces, cnd); + + if (defaultWsp == null) { + throw new IllegalArgumentException("Default workspace is null"); + } + + this.wspNames = Collections.unmodifiableList(new ArrayList(wspNames)); + this.defaulWsp = defaultWsp; + } + + //------------------------------------< may be overwritten by subclasses>--- + /** + * Checks whether the workspaceName is valid. + * @param workspaceName name of the workspace to check + * @throws NoSuchWorkspaceException if workspaceName is neither in the + * list of workspace nor null (i.e. default workspace). + */ + @Override + protected void checkWorkspace(String workspaceName) throws NoSuchWorkspaceException { + if (workspaceName != null && !wspNames.contains(workspaceName)) { + throw new NoSuchWorkspaceException(workspaceName); + } + } + + @Override + protected SessionInfo createSessionInfo(Credentials credentials, String workspaceName) + throws RepositoryException { + + return super.createSessionInfo(credentials, workspaceName == null? defaulWsp : workspaceName); + } + + // -------------------------------------------------------------< cache >--- + /** + * @param sessionInfo + * @return a new instance of ItemInfoCacheImpl + */ + public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) { + return new ItemInfoCacheImpl(); + } + + //------------------------------------------------------------< reading >--- + /** + * This default implementation returns the first item returned by the call to + * {@link #getItemInfos(SessionInfo, ItemId)}. The underlying assumption here is that + * the implementation and the persistence layer are optimized for batch reading. That is, + * a call to getItemInfos is no more expensive than retrieving the single + * NodeInfo only. If this assumption does not hold, subclasses should override + * this method. + */ + public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws ItemNotFoundException, + RepositoryException { + + Iterator infos = getItemInfos(sessionInfo, nodeId); + if (infos.hasNext()) { + return (NodeInfo) infos.next(); + } + else { + throw new ItemNotFoundException(); + } + } + + //----------------------------------------------------< workspace names >--- + /** + * This default implementation first calls {@link #checkSessionInfo(SessionInfo)} + * with the sessionInfo, then returns the workspaces that were + * passed to the constructor of this repository service. + */ + public String[] getWorkspaceNames(SessionInfo sessionInfo) throws RepositoryException { + checkSessionInfo(sessionInfo); + return wspNames.toArray(new String[wspNames.size()]); + } + + //-----------------------------------------------------< access control >--- + /** + * This default implementation first calls {@link #checkSessionInfo(SessionInfo)} + * with the sessionInfo, then returns false if + * the any of the actions are in {@link #WRITE_ACTIONS}; + * otherwise returns true. + */ + public boolean isGranted(SessionInfo sessionInfo, + ItemId itemId, + String[] actions) throws RepositoryException { + checkSessionInfo(sessionInfo); + // deny all but read + for (String action : actions) { + if (WRITE_ACTIONS.contains(action)) { + return false; + } + } + return true; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AbstractRepositoryService.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AbstractRepositoryService.java new file mode 100644 index 00000000000..70cfaff83d0 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AbstractRepositoryService.java @@ -0,0 +1,893 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons; + +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.GuestCredentials; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.MergeException; +import javax.jcr.NamespaceException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.version.VersionException; + +import org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefReader; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.identifier.IdFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeStorage; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeStorageImpl; +import org.apache.jackrabbit.spi.commons.nodetype.QDefinitionBuilderFactory; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; + +/** + * AbstractRepositoryService provides an abstract base class for + * repository service implementations. This class provides default + * implementations for the following methods: + *

        + */ +public abstract class AbstractRepositoryService implements RepositoryService { + + /** + * The repository descriptors. + */ + protected final Map descriptors = new HashMap(); + + /** + * The fixed set of namespaces known to the repository service. + */ + protected final NamespaceMapping namespaces = new NamespaceMapping(); + + /** + * The fixed set of node type definitions known to the repository service. + */ + protected final NodeTypeStorage nodeTypeDefs = new NodeTypeStorageImpl(); + + /** + * The node definition of the root node. + */ + protected QNodeDefinition rootNodeDefinition; + + /** + * @return {@link IdFactoryImpl#getInstance()}. + * @throws RepositoryException if an error occurs. + */ + public IdFactory getIdFactory() throws RepositoryException { + return IdFactoryImpl.getInstance(); + } + + /** + * @return {@link NameFactoryImpl#getInstance()}. + * @throws RepositoryException if an error occurs. + */ + public NameFactory getNameFactory() throws RepositoryException { + return NameFactoryImpl.getInstance(); + } + + /** + * @return {@link PathFactoryImpl#getInstance()}. + * @throws RepositoryException if an error occurs. + */ + public PathFactory getPathFactory() throws RepositoryException { + return PathFactoryImpl.getInstance(); + } + + /** + * @return {@link QValueFactoryImpl#getInstance()}. + * @throws RepositoryException if an error occurs. + */ + public QValueFactory getQValueFactory() throws RepositoryException { + return QValueFactoryImpl.getInstance(); + } + + protected AbstractRepositoryService() throws RepositoryException { + QValueFactory qvf = QValueFactoryImpl.getInstance(); + QValue[] vFalse = new QValue[] {qvf.create(false)}; + + descriptors.put(Repository.WRITE_SUPPORTED, vFalse); + descriptors.put(Repository.IDENTIFIER_STABILITY, + new QValue[] {qvf.create(Repository.IDENTIFIER_STABILITY_SAVE_DURATION, PropertyType.STRING)}); + descriptors.put(Repository.OPTION_XML_IMPORT_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_XML_EXPORT_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_UNFILED_CONTENT_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_VERSIONING_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_SIMPLE_VERSIONING_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_ACCESS_CONTROL_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_LOCKING_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_OBSERVATION_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_JOURNALED_OBSERVATION_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_RETENTION_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_LIFECYCLE_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_TRANSACTIONS_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_WORKSPACE_MANAGEMENT_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_UPDATE_PRIMARY_NODE_TYPE_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_UPDATE_MIXIN_NODE_TYPES_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_SHAREABLE_NODES_SUPPORTED, vFalse); + descriptors.put(Repository.OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED, vFalse); + + descriptors.put(Repository.QUERY_LANGUAGES, new QValue[0]); + descriptors.put(Repository.QUERY_STORED_QUERIES_SUPPORTED, vFalse); + descriptors.put(Repository.QUERY_FULL_TEXT_SEARCH_SUPPORTED, vFalse); + descriptors.put(Repository.QUERY_JOINS, + new QValue[] {qvf.create(Repository.QUERY_JOINS_NONE, PropertyType.STRING)}); + + descriptors.putAll(descriptors); + } + + public AbstractRepositoryService(Map descriptors, + Map namespaces, + QNodeTypeDefinition[] nodeTypeDefs) + throws RepositoryException { + + this(); + this.descriptors.putAll(descriptors); + + for (Map.Entry entry : namespaces.entrySet()) { + this.namespaces.setMapping(entry.getKey(), entry.getValue()); + } + + this.nodeTypeDefs.registerNodeTypes(nodeTypeDefs, true); + } + + public AbstractRepositoryService(Map descriptors, + Map namespaces, + Reader cnd) + throws RepositoryException { + + this(); + this.descriptors.putAll(descriptors); + + for (Map.Entry entry : namespaces.entrySet()) { + this.namespaces.setMapping(entry.getKey(), entry.getValue()); + } + + CompactNodeTypeDefReader reader; + try { + reader = new CompactNodeTypeDefReader(cnd, "", + this.namespaces, new QDefinitionBuilderFactory()); + + List ntds = reader.getNodeTypeDefinitions(); + nodeTypeDefs.registerNodeTypes(ntds.toArray(new QNodeTypeDefinition[ntds.size()]), true); + } + catch (ParseException e) { + throw new RepositoryException("Error reading node type definitions", e); + } + } + + //---------------------------< subclass responsibility >-------------------- + + /** + * Create the root node definition. + * @param sessionInfo the session info. + * @return the root node definition for a workspace. + * @throws RepositoryException if an error occurs. + */ + protected abstract QNodeDefinition createRootNodeDefinition(SessionInfo sessionInfo) + throws RepositoryException; + + //---------------------< may be overwritten by subclasses>------------------ + + /** + * Checks if the given credentials are valid. This default + * implementation is empty thus allowing all credentials. + * + * @param credentials the credentials to check. + * @param workspaceName the workspace to access. + * @throws LoginException if the credentials are invalid. + */ + protected void checkCredentials(Credentials credentials, String workspaceName) throws LoginException { + // empty + } + + /** + * Checks if the given workspace is available. The default implementation is empty + * thus admitting every workspace name. + * @param workspaceName Name of the workspace to check + * @throws NoSuchWorkspaceException If workspaceName is not available. + */ + protected void checkWorkspace(String workspaceName) throws NoSuchWorkspaceException { + // empty + } + + /** + * Creates a session info instance for the given credentials and + * workspaceName. This default implementation creates a + * {@link SessionInfoImpl} instance and sets the userId and + * workspaceName. The user userId is null or the + * userId from credentials if it is of type + * {@link SimpleCredentials}. + * + * @param credentials the credentials. + * @param workspaceName the name of the workspace to access or null + * for the default workspace. + * @return a session info instance for the given credentials and + * workspaceName. + * @throws RepositoryException + */ + protected SessionInfo createSessionInfo(Credentials credentials, String workspaceName) + throws RepositoryException { + + String userId = null; + if (credentials instanceof SimpleCredentials) { + userId = ((SimpleCredentials) credentials).getUserID(); + } + else if (credentials instanceof GuestCredentials) { + userId = "anonymous"; + } + + SessionInfoImpl s = new SessionInfoImpl(); + s.setUserID(userId); + s.setWorkspacename(workspaceName); + return s; + } + + /** + * Creates a session info instance for the given sessionInfo and + * workspaceName. This default implementation creates a + * {@link SessionInfoImpl} instance and sets the userId and + * workspaceName. The user userId is set to the return value of + * {@link SessionInfo#getUserID()}. + * + * @param sessionInfo the sessionInfo. + * @param workspaceName the name of the workspace to access. + * @return a session info instance for the given credentials and + * workspaceName. + * @throws RepositoryException + */ + protected SessionInfo createSessionInfo(SessionInfo sessionInfo, String workspaceName) + throws RepositoryException { + + String userId = sessionInfo.getUserID(); + + SessionInfoImpl s = new SessionInfoImpl(); + s.setUserID(userId); + s.setWorkspacename(workspaceName); + return s; + } + + /** + * Checks the type of the sessionInfo instance. This default + * implementation checks if sessionInfo is of type + * {@link SessionInfoImpl}, otherwise throws a {@link RepositoryException}. + * + * @param sessionInfo the session info to check. + * @throws RepositoryException if the given sessionInfo is not + * of the required type for this repository + * service implementation. + */ + protected void checkSessionInfo(SessionInfo sessionInfo) + throws RepositoryException { + if (sessionInfo instanceof SessionInfoImpl) { + return; + } + throw new RepositoryException("SessionInfo not of type " + + SessionInfoImpl.class.getName()); + } + + //--------------------------< descriptors >--------------------------------- + + /** + * This default implementation returns the descriptors that were passed + * to the constructor of this repository service. + */ + public Map getRepositoryDescriptors() throws RepositoryException { + return descriptors; + } + + //----------------------------< login >------------------------------------- + + /** + * This default implementation does: + *
          + *
        • calls {@link #checkCredentials(Credentials, String)}
        • + *
        • calls {@link #checkWorkspace(String)}
        • + *
        • calls {@link #createSessionInfo(Credentials, String)}
        • + *
        + * @param credentials the credentials for the login. + * @param workspaceName the name of the workspace to log in. + * @return the session info. + * @throws LoginException if the credentials are invalid. + * @throws NoSuchWorkspaceException if workspaceName is unknown. + * @throws RepositoryException if another error occurs. + */ + public SessionInfo obtain(Credentials credentials, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException { + checkCredentials(credentials, workspaceName); + checkWorkspace(workspaceName); + return createSessionInfo(credentials, workspaceName); + } + + /** + * This default implementation returns the session info returned by the call + * to {@link #createSessionInfo(SessionInfo, String)}. + */ + public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException { + return createSessionInfo(sessionInfo, workspaceName); + } + + + /** + * This default implementation returns the session info returned by the call + * to {@link #obtain(Credentials, String)} with the workspaceName taken from + * the passed sessionInfo. + */ + public SessionInfo impersonate(SessionInfo sessionInfo, Credentials credentials) + throws LoginException, RepositoryException { + return obtain(credentials, sessionInfo.getWorkspaceName()); + } + + /** + * This default implementation does nothing. + */ + public void dispose(SessionInfo sessionInfo) throws RepositoryException { + // do nothing + } + + //-----------------------------< node types >------------------------------- + + /** + * This default implementation first calls {@link #checkSessionInfo(SessionInfo)} + * with the sessionInfo, + */ + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo) throws + RepositoryException { + + checkSessionInfo(sessionInfo); + return nodeTypeDefs.getAllDefinitions(); + } + + /** + * This default implementation first calls {@link #checkSessionInfo(SessionInfo)} + * with the sessionInfo, then gathers the {@link QNodeTypeDefinition}s + * with the given nodetypeNames. If one of the nodetypeNames + * is not a valid node type definition then a {@link RepositoryException} + * is thrown. + */ + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo, Name[] nodetypeNames) + throws RepositoryException { + + checkSessionInfo(sessionInfo); + return nodeTypeDefs.getDefinitions(nodetypeNames); + } + + /** + * This default implementation first calls {@link #checkSessionInfo(SessionInfo)} + * with the sessionInfo, then lazily initializes {@link #rootNodeDefinition} + * if nodeId denotes the root node; otherwise throws a + * {@link UnsupportedRepositoryOperationException}. + */ + public QNodeDefinition getNodeDefinition(SessionInfo sessionInfo, + NodeId nodeId) + throws RepositoryException { + checkSessionInfo(sessionInfo); + if (nodeId.getUniqueID() == null && nodeId.getPath().denotesRoot()) { + synchronized (this) { + if (rootNodeDefinition == null) { + rootNodeDefinition = createRootNodeDefinition(sessionInfo); + } + return rootNodeDefinition; + } + } + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public QPropertyDefinition getPropertyDefinition(SessionInfo sessionInfo, + PropertyId propertyId) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void registerNodeTypes(SessionInfo sessionInfo, QNodeTypeDefinition[] nodeTypeDefinitions, boolean allowUpdate) throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void unregisterNodeTypes(SessionInfo sessionInfo, Name[] nodeTypeNames) throws UnsupportedRepositoryOperationException, NoSuchNodeTypeException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + //-----------------------------< namespaces >------------------------------- + + /** + * This default implementation first calls {@link #checkSessionInfo(SessionInfo)} + * with the sessionInfo, then returns the prefix to namespace + * URL mapping that was provided in the constructor of this repository + * service. + */ + public Map getRegisteredNamespaces(SessionInfo sessionInfo) throws + RepositoryException { + checkSessionInfo(sessionInfo); + return namespaces.getPrefixToURIMapping(); + } + + /** + * This default implementation first calls {@link #checkSessionInfo(SessionInfo)} + * with the sessionInfo, then returns the namespace URI for the + * given prefix. + */ + public String getNamespaceURI(SessionInfo sessionInfo, String prefix) + throws NamespaceException, RepositoryException { + checkSessionInfo(sessionInfo); + return namespaces.getURI(prefix); + } + + /** + * This default implementation first calls {@link #checkSessionInfo(SessionInfo)} + * with the sessionInfo, then return the namespace prefix for + * the given uri. + */ + public String getNamespacePrefix(SessionInfo sessionInfo, String uri) + throws NamespaceException, RepositoryException { + checkSessionInfo(sessionInfo); + return namespaces.getPrefix(uri); + } + + //-----------------------------< write methods >---------------------------- + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public Batch createBatch(SessionInfo sessionInfo, ItemId itemId) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void submit(Batch batch) throws PathNotFoundException, ItemNotFoundException, NoSuchNodeTypeException, ValueFormatException, VersionException, LockException, ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + @Override + public Tree createTree(SessionInfo sessionInfo, Batch batch, Name nodeName, Name primaryTypeName, String uniqueId) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void importXml(SessionInfo sessionInfo, + NodeId parentId, + InputStream xmlStream, + int uuidBehaviour) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void move(SessionInfo sessionInfo, + NodeId srcNodeId, + NodeId destParentNodeId, + Name destName) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void copy(SessionInfo sessionInfo, + String srcWorkspaceName, + NodeId srcNodeId, + NodeId destParentNodeId, + Name destName) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void update(SessionInfo sessionInfo, + NodeId nodeId, + String srcWorkspaceName) + throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void clone(SessionInfo sessionInfo, + String srcWorkspaceName, + NodeId srcNodeId, + NodeId destParentNodeId, + Name destName, + boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public LockInfo lock(SessionInfo sessionInfo, + NodeId nodeId, + boolean deep, + boolean sessionScoped) + throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, + boolean sessionScoped, long timeoutHint, String ownerHint) + throws UnsupportedRepositoryOperationException, LockException, + AccessDeniedException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @return null. + */ + public LockInfo getLockInfo(SessionInfo sessionInfo, NodeId nodeId) + throws AccessDeniedException, RepositoryException { + return null; + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void refreshLock(SessionInfo sessionInfo, NodeId nodeId) + throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void unlock(SessionInfo sessionInfo, NodeId nodeId) + throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public NodeId checkin(SessionInfo sessionInfo, NodeId nodeId) + throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void checkout(SessionInfo sessionInfo, NodeId nodeId) + throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void checkout(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) + throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId) + throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) + throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void removeVersion(SessionInfo sessionInfo, + NodeId versionHistoryId, + NodeId versionId) + throws ReferentialIntegrityException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void restore(SessionInfo sessionInfo, + NodeId nodeId, + NodeId versionId, + boolean removeExisting) throws VersionException, PathNotFoundException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void restore(SessionInfo sessionInfo, + NodeId[] versionIds, + boolean removeExisting) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException + * always. + */ + public Iterator merge(SessionInfo sessionInfo, + NodeId nodeId, + String srcWorkspaceName, + boolean bestEffort) throws + NoSuchWorkspaceException, AccessDeniedException, MergeException, + LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException + * always. + */ + public Iterator merge(SessionInfo sessionInfo, + NodeId nodeId, + String srcWorkspaceName, + boolean bestEffort, + boolean isShallow) throws + NoSuchWorkspaceException, AccessDeniedException, MergeException, + LockException, InvalidItemStateException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void resolveMergeConflict(SessionInfo sessionInfo, + NodeId nodeId, + NodeId[] mergeFailedIds, + NodeId[] predecessorIds) + throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void addVersionLabel(SessionInfo sessionInfo, + NodeId versionHistoryId, + NodeId versionId, + Name label, + boolean moveLabel) throws VersionException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void removeVersionLabel(SessionInfo sessionInfo, + NodeId versionHistoryId, + NodeId versionId, + Name label) throws VersionException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public NodeId createActivity(SessionInfo sessionInfo, String title) throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void removeActivity(SessionInfo sessionInfo, NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public Iterator mergeActivity(SessionInfo sessionInfo, NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public NodeId createConfiguration(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + //-----------------------------< observation >------------------------------ + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public EventFilter createEventFilter(SessionInfo sessionInfo, + int eventTypes, + Path absPath, + boolean isDeep, + String[] uuid, + Name[] nodeTypeName, + boolean noLocal) + throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public Subscription createSubscription(SessionInfo sessionInfo, + EventFilter[] filters) + throws UnsupportedRepositoryOperationException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void updateEventFilters(Subscription subscription, + EventFilter[] filters) + throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public EventBundle[] getEvents(Subscription subscription, long timeout) + throws RepositoryException, InterruptedException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public EventBundle getEvents(SessionInfo sessionInfo, EventFilter filter, + long after) throws + RepositoryException, UnsupportedRepositoryOperationException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void dispose(Subscription subscription) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + //-------------------------------------------------< namespace registry >--- + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void registerNamespace(SessionInfo sessionInfo, + String prefix, + String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void unregisterNamespace(SessionInfo sessionInfo, String uri) + throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + //-----------------------------------------------< Workspace Management >--- + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void createWorkspace(SessionInfo sessionInfo, String name, String srcWorkspaceName) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + /** + * @throws UnsupportedRepositoryOperationException always. + */ + public void deleteWorkspace(SessionInfo sessionInfo, String name) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + //-------------------------------< query >---------------------------------- + + public String[] getSupportedQueryLanguages(SessionInfo sessionInfo) throws RepositoryException { + checkSessionInfo(sessionInfo); + return new String[0]; + } + + public String[] checkQueryStatement(SessionInfo sessionInfo, String statement, + String language, Map namespaces) throws + InvalidQueryException, RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + + public QueryInfo executeQuery(SessionInfo sessionInfo, String statement, + String language, Map namespaces, long limit, + long offset, Map values) throws RepositoryException { + throw new UnsupportedRepositoryOperationException(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AdditionalEventInfo.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AdditionalEventInfo.java new file mode 100644 index 00000000000..2a5cf20e907 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/AdditionalEventInfo.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.spi.commons; + +import java.util.Set; + +import javax.jcr.UnsupportedRepositoryOperationException; + +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.Name; + +/** + * Provides additional information for an {@link Event}. + */ +public interface AdditionalEventInfo { + + /** + * @return the name of the primary node type of the node associated with the event + */ + public Name getPrimaryNodeTypeName() throws UnsupportedRepositoryOperationException; + + /** + * @return the names of the mixin node types of the node associated with the event + */ + public Set getMixinTypeNames() throws UnsupportedRepositoryOperationException; + + /** + * @return the specified Session attribute + */ + public Object getSessionAttribute(String name) throws UnsupportedRepositoryOperationException; +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ChildInfoImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ChildInfoImpl.java new file mode 100644 index 00000000000..1b1a91fef62 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ChildInfoImpl.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.spi.commons; + +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.Name; + +import java.io.Serializable; + +/** + * ChildInfoImpl implements a serializable ChildInfo. + */ +public class ChildInfoImpl implements ChildInfo, Serializable { + + /** + * The name of this child info. + */ + private final Name name; + + /** + * The unique id for this child info or null if it does not + * have a unique id. + */ + private final String uniqueId; + + /** + * 1-based index of this child info. + */ + private final int index; + + /** + * Creates a new serializable ChildInfoImpl. + * + * @param name the name of the child node. + * @param uniqueId the unique id of the child node or null. + * @param index the index of the child node. + */ + public ChildInfoImpl(Name name, String uniqueId, int index) { + this.name = name; + this.uniqueId = uniqueId; + this.index = index; + } + + /** + * {@inheritDoc} + */ + public Name getName() { + return name; + } + + /** + * {@inheritDoc} + */ + public String getUniqueID() { + return uniqueId; + } + + /** + * {@inheritDoc} + */ + public int getIndex() { + return index; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventBundleImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventBundleImpl.java new file mode 100644 index 00000000000..a45afff568c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventBundleImpl.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.spi.commons; + +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.Event; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; + +/** + * EventBundleImpl implements a serializable {@link EventBundle}. + */ +public class EventBundleImpl implements EventBundle, Serializable { + + /** + * Indicates if this bundle was created due to a local change. + */ + private final boolean isLocal; + + /** + * The events in this bundle. + */ + private final Collection events; + + /** + * Creates a new event bundle with events. + * + * @param events the events for this bundle. + * @param isLocal if this events were created due to a local change. + */ + public EventBundleImpl(Collection events, boolean isLocal) { + this.events = events; + this.isLocal = isLocal; + } + + /** + * {@inheritDoc} + */ + public Iterator getEvents() { + return events.iterator(); + } + + /** + * {@inheritDoc} + */ + public boolean isLocal() { + return isLocal; + } + + //-----------------------------------------------------------< Iterable >--- + + /** + * {@inheritDoc} + */ + public Iterator iterator() { + return getEvents(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventFilterImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventFilterImpl.java new file mode 100644 index 00000000000..d0c4edb5298 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventFilterImpl.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.spi.commons; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; + +/** + * EventFilterImpl is the simple bean style implementation of an + * {@link EventFilter}. + */ +public class EventFilterImpl implements EventFilter, Serializable { + + private final int eventTypes; + + private final boolean isDeep; + + private final Path absPath; + + private final Set uuids; + + private final Set nodeTypeNames; + + private final boolean noLocal; + + /** + * Creates a new EventFilterImpl. + * + * @param eventTypes the event types this filter is interested in. + * @param absPath filter events that are below this path. + * @param isDeep whether this filter is applied deep. + * @param uuids the jcr:uuid of the nodes this filter allows. + * @param nodeTypeNames the Names of the already resolved node types this + * filter allows. + * @param noLocal whether this filter accepts local events or not. + */ + public EventFilterImpl(int eventTypes, + Path absPath, + boolean isDeep, + String[] uuids, + Set nodeTypeNames, + boolean noLocal) { + this.eventTypes = eventTypes; + this.absPath = absPath; + this.isDeep = isDeep; + this.uuids = uuids != null ? new HashSet(Arrays.asList(uuids)) : null; + this.nodeTypeNames = nodeTypeNames != null ? new HashSet(nodeTypeNames) : null; + this.noLocal = noLocal; + } + + /** + * {@inheritDoc} + */ + public boolean accept(Event event, boolean isLocal) { + int type = event.getType(); + // check type + if ((type & eventTypes) == 0) { + return false; + } + + // check local flag + if (isLocal && noLocal) { + return false; + } + + // UUIDs, types, and paths do not need to match for persist + if (event.getType() == Event.PERSIST) { + return true; + } + + // check UUIDs + NodeId parentId = event.getParentId(); + if (uuids != null) { + if (parentId.getPath() == null) { + if (!uuids.contains(parentId.getUniqueID())) { + return false; + } + } else { + return false; + } + } + + // check node types + if (nodeTypeNames != null) { + Set eventTypes = new HashSet(); + eventTypes.addAll(Arrays.asList(event.getMixinTypeNames())); + eventTypes.add(event.getPrimaryNodeTypeName()); + // create intersection + eventTypes.retainAll(nodeTypeNames); + if (eventTypes.isEmpty()) { + return false; + } + } + + // finally check path + try { + Path eventPath = event.getPath().getAncestor(1); + boolean match = eventPath.equals(absPath); + if (!match && isDeep) { + match = eventPath.isDescendantOf(absPath); + } + return match; + } catch (RepositoryException e) { + // should never get here + } + // if we get here an exception occurred while checking for the path + return false; + } + + /** + * @return the event types this event filter accepts. + */ + public int getEventTypes() { + return eventTypes; + } + + /** + * @return true if this event filter is deep. + */ + public boolean isDeep() { + return isDeep; + } + + /** + * @return the path to the item where events are filtered. + */ + public Path getAbsPath() { + return absPath; + } + + /** + * @return the uuids of the nodes of this filter or null if + * this filter does not care about uuids. + */ + public String[] getUUIDs() { + if (uuids == null) { + return null; + } else { + return uuids.toArray(new String[uuids.size()]); + } + } + + /** + * @return an unmodifiable set of node type names or null if + * this filter does not care about node types. + */ + public Set getNodeTypeNames() { + if (nodeTypeNames == null) { + return null; + } else { + return Collections.unmodifiableSet(nodeTypeNames); + } + } + + /** + * @return if this filter accepts local events. + */ + public boolean getNoLocal() { + return noLocal; + } + + /** + * Returns a string representation of this EventFilter instance. + * {@inheritDoc} + */ + @Override + public String toString() { + return new StringBuffer(getClass().getName()) + .append("[") + .append("eventTypes: ").append(eventTypes).append(", ") + .append("absPath: ").append(absPath).append(", ") + .append("isDeep: ").append(isDeep).append(", ") + .append("uuids: ").append(uuids).append(", ") + .append("nodeTypeNames: ").append(nodeTypeNames).append(", ") + .append("noLocal: ").append(noLocal) + .append("]") + .toString(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventImpl.java new file mode 100644 index 00000000000..2baa3c8baa7 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/EventImpl.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons; + +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; + +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Map; +import java.util.Collections; +import java.util.HashMap; + +/** + * EventImpl implements a serializable SPI + * {@link org.apache.jackrabbit.spi.Event}. + */ +public class EventImpl implements Event, Serializable { + + /** + * The SPI event type. + * @see Event + */ + private final int type; + + /** + * The path of the affected item. + */ + private final Path path; + + /** + * The id of the affected item. + */ + private final ItemId itemId; + + /** + * The id of the affected item. + */ + private final NodeId parentId; + + /** + * The name of the primary node type of the 'associated' node of this event. + */ + private final Name primaryNodeTypeName; + + /** + * The names of the mixin types of the 'associated' node of this event. + */ + private final Name[] mixinTypeNames; + + /** + * The user ID connected with this event. + */ + private final String userId; + + private final String userData; + private final long timestamp; + private final Map info; + + /** + * Creates a new serializable event. + * @deprecated + */ + public EventImpl(int type, Path path, ItemId itemId, NodeId parentId, + Name primaryNodeTypeName, Name[] mixinTypeNames, + String userId) { + this(type, path, itemId, parentId, primaryNodeTypeName, mixinTypeNames, userId, null, Long.MIN_VALUE, Collections.EMPTY_MAP); + } + + /** + * Creates a new serializable event. + */ + public EventImpl(int type, Path path, ItemId itemId, NodeId parentId, + Name primaryNodeTypeName, Name[] mixinTypeNames, + String userId, String userData, long timestamp, + Map info) { + this.type = type; + this.path = path; + this.itemId = itemId; + this.parentId = parentId; + this.primaryNodeTypeName = primaryNodeTypeName; + this.mixinTypeNames = mixinTypeNames; + this.userId = userId; + + this.userData = userData; + this.info = new HashMap(info); + this.timestamp = timestamp; + } + + //--------------------------------------------------------------< Event >--- + /** + * {@inheritDoc} + */ + public int getType() { + return type; + } + + /** + * {@inheritDoc} + */ + public Path getPath() { + return path; + } + + /** + * {@inheritDoc} + */ + public ItemId getItemId() { + return itemId; + } + + /** + * {@inheritDoc} + */ + public NodeId getParentId() { + return parentId; + } + + /** + * {@inheritDoc} + */ + public Name getPrimaryNodeTypeName() { + return primaryNodeTypeName; + } + + /** + * {@inheritDoc} + */ + public Name[] getMixinTypeNames() { + Name[] mixins = new Name[mixinTypeNames.length]; + System.arraycopy(mixinTypeNames, 0, mixins, 0, mixinTypeNames.length); + return mixins; + } + + /** + * {@inheritDoc} + */ + public String getUserID() { + return userId; + } + + /** + * {@inheritDoc} + */ + public Map getInfo() throws RepositoryException { + return info; + } + + /** + * {@inheritDoc} + */ + public String getUserData() { + return userData; + } + + /** + * {@inheritDoc} + */ + public long getDate() throws RepositoryException { + if (timestamp == Long.MIN_VALUE) { + throw new UnsupportedRepositoryOperationException("Event.getDate() not supported"); + } else { + return timestamp; + } + } + + //-------------------------------------------------------------< Object >--- + @Override + public String toString() { + return new StringBuffer(getClass().getName()) + .append("[") + .append("eventTypes: ").append(type).append(", ") + .append("absPath: ").append(path).append(", ") + .append("itemId: ").append(itemId).append(", ") + .append("parentId: ").append(parentId).append(", ") + .append("primaryNodeTypeName: ").append(primaryNodeTypeName).append(", ") + .append("mixinTypeNames: ").append(Arrays.toString(mixinTypeNames)).append(", ") + .append("userId").append(userId) + .append("]") + .toString(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoBuilder.java new file mode 100644 index 00000000000..bd57b2a1d5f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoBuilder.java @@ -0,0 +1,803 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons; + +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.filterIterator; +import static org.apache.jackrabbit.spi.commons.iterator.Iterators.transformIterator; + +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.identifier.IdFactoryImpl; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; +import org.apache.jackrabbit.spi.commons.iterator.Predicate; +import org.apache.jackrabbit.spi.commons.iterator.Transformer; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.net.URI; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Iterator; +import java.util.List; + +/** + * Utility class providing a {@link NodeInfoBuilder} for building {@link NodeInfo}. + * Example usage: + *
        + * ItemInfoBuilder.nodeInfoBuilder()
        + *     .createNodeInfo("node1")
        + *         .createPropertyInfo("prop1", "value1").build()
        + *         .createPropertyInfo("prop2")
        + *             .addValue(1.2)
        + *             .addValue(2.3)
        + *         .build()
        + *     .build()
        + *     .createNodeInfo("node2")
        + *         .setPrimaryType(NameConstants.NT_BASE)
        + *         .addMixin(NameConstants.MIX_LOCKABLE)
        + *         .createPropertyInfo("prop3")
        + *             .setType(PropertyType.BINARY)
        + *         .build()
        + *     .build()
        + * .build();
        + * 
        + */ +public final class ItemInfoBuilder { + + private ItemInfoBuilder() { + super(); + } + + /** + * Same as nodeInfoBuilder("", listener) + * @param listener + * @return + */ + public static NodeInfoBuilder nodeInfoBuilder(Listener listener) { + return nodeInfoBuilder("", listener); + } + + /** + * Same as nodeInfoBuilder("", null) + * @return + */ + public static NodeInfoBuilder nodeInfoBuilder() { + return nodeInfoBuilder("", null); + } + + /** + * Same as nodeInfoBuilder(localName, null) + * @param localName + * @return + */ + public static NodeInfoBuilder nodeInfoBuilder(String localName) { + return nodeInfoBuilder(localName, null); + } + + /** + * Return a {@link NodeInfoBuilder} for a node with a given localName. + * @param localName localName of the node + * @param listener {@link Listener} to receive notifications about {@link NodeInfo}s, + * {@link PropertyInfo}s and {@link ChildInfo}s built. + * @return + */ + public static NodeInfoBuilder nodeInfoBuilder(String localName, Listener listener) { + return new NodeInfoBuilder(null, localName, listener); + } + + /** + * Return a {@link NodeInfoBuilder} for a node with a given name. + * @param name name of the node + * @param listener {@link Listener} to receive notifications about {@link NodeInfo}s, + * {@link PropertyInfo}s and {@link ChildInfo}s built. + * @return + */ + public static NodeInfoBuilder nodeInfoBuilder(Name name, Listener listener) { + return new NodeInfoBuilder(null, name, listener); + } + + /** + * A listener for receiving notifications about items built by the builders in this class. + */ + public interface Listener { + + /** + * Notification that a new {@link NodeInfo} has been built. + * @param nodeInfo + */ + void createNodeInfo(NodeInfo nodeInfo); + + /** + * Notification that new {@link ChildInfo}s have been built. + * @param id Id of the parent to which the childInfos belong + * @param childInfos + */ + void createChildInfos(NodeId id, Iterator childInfos); + + /** + * Notification that a new {@link PropertyInfo} has been built. + * @param propertyInfo + */ + void createPropertyInfo(PropertyInfo propertyInfo); + } + + /** + * Builder for {@link NodeInfo}s. Use one of the {@link ItemInfoBuilder#nodeInfoBuilder()} + * methods to create instances of this class. + */ + public static class NodeInfoBuilder { + private final NodeInfoBuilder parent; + private final Listener listener; + + private Path parentPath; + private String localName; + private String namespace; + private Name name; + private int index = Path.INDEX_DEFAULT; + private String uuid; + private Name primaryTypeName = NameConstants.NT_UNSTRUCTURED; + private final List mixins = new ArrayList(); + private boolean includeChildInfos = true; + + private boolean stale; + private final List itemInfos = new ArrayList(); + private NodeInfo nodeInfo; + + private NodeInfoBuilder(NodeInfoBuilder nodeInfoBuilder, String localName, Listener listener) { + super(); + parent = nodeInfoBuilder; + this.localName = localName; + this.listener = listener; + } + + private NodeInfoBuilder(NodeInfoBuilder nodeInfoBuilder, Name name, Listener listener) { + super(); + parent = nodeInfoBuilder; + this.name = name; + this.listener = listener; + } + + /** + * Create a new child {@link PropertyInfo} with a given localName and a given + * value of type String on this {@link NodeInfo}. + * + * @param localName + * @param value + * @return this + * @throws RepositoryException + */ + public PropertyInfoBuilder createPropertyInfo(String localName, String value) throws RepositoryException { + PropertyInfoBuilder pBuilder = new PropertyInfoBuilder(this, localName, listener); + pBuilder.addValue(value); + return pBuilder; + } + + /** + * Create a new child {@link PropertyInfo} with a given + * localName on this {@link NodeInfo}. + * + * @param localName + * @return this + */ + public PropertyInfoBuilder createPropertyInfo(String localName) { + return new PropertyInfoBuilder(this, localName, listener); + } + + /** + * Create a new child {@link PropertyInfo} on this {@link NodeInfo}. + * + * @return this + */ + public PropertyInfoBuilder createPropertyInfo() { + return new PropertyInfoBuilder(this, null, listener); + } + + /** + * Create a new child {@link NodeInfo} on this NodeInfo with a given localName. + * @param localName + * @return this + */ + public NodeInfoBuilder createNodeInfo(String localName) { + return new NodeInfoBuilder(this, localName, listener); + } + + /** + * Create a new child {@link NodeInfo} on this NodeInfo. + + * @return this + */ + public NodeInfoBuilder createNodeInfo() { + return new NodeInfoBuilder(this, (String) null, listener); + } + + /** + * Set the name of the node + * + * @param name + * @return + */ + public NodeInfoBuilder setName(Name name) { + this.name = name; + return this; + } + + /** + * Set the localName of the node + * + * @param localName + * @return + */ + public NodeInfoBuilder setName(String localName) { + this.localName = localName; + return this; + } + /** + * Set the namespace + * + * @param namespace + * @return + */ + public NodeInfoBuilder setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + /** + * Set the index. + * @see NodeInfo#getIndex() + * + * @param index + * @return + */ + public NodeInfoBuilder setIndex(int index) { + this.index = index; + return this; + } + + /** + * Set the uuid + * + * @param uuid + * @return + */ + public NodeInfoBuilder setUUID(String uuid) { + this.uuid = uuid; + return this; + } + + /** + * Set the parent's path of the node + * + * @param parentPath + * @return + */ + public NodeInfoBuilder setParentPath(Path parentPath) { + this.parentPath = parentPath; + return this; + } + + /** + * Set the name of the primary type. + * @param name + * @see NodeInfo#getNodetype() + * + * @return + */ + public NodeInfoBuilder setPrimaryType(Name name) { + primaryTypeName = name; + return this; + } + + /** + * Add a mixin type + * @see NodeInfo#getMixins() + * + * @param name + * @return + */ + public NodeInfoBuilder addMixin(Name name) { + mixins.add(name); + return this; + } + + /** + * Whether the {@link ChildInfo}s should be included or not. + * @see NodeInfo#getChildInfos() + * + * @param include + * @return + */ + public NodeInfoBuilder includeChildInfos(boolean include) { + includeChildInfos = include; + return this; + } + + /** + * Build the {@link NodeInfo}. If a {@link Listener} is associated with this + * instance, then its {@link Listener#createChildInfos(NodeId, Iterator)} and + * its {@link Listener#createNodeInfo(NodeInfo)} methods are called. + * + * @return the parent builder of this builder + * @throws RepositoryException + * @throws IllegalStateException if build has been called before + */ + public NodeInfoBuilder build() throws RepositoryException { + if (stale) { + throw new IllegalStateException("Builder is stale"); + } + else { + stale = true; + NodeId id = getId(); + + nodeInfo = new NodeInfoImpl(getPath(), id, index, primaryTypeName, + mixins.toArray(new Name[mixins.size()]), Iterators.empty(), + getPropertyIds(), includeChildInfos ? getChildInfos() : null); + + if (listener != null) { + listener.createNodeInfo(nodeInfo); + listener.createChildInfos(id, getChildInfos()); + } + + if (parent == null) { + return this; + } + else { + parent.addNodeInfo(nodeInfo); + return parent; + } + } + } + + /** + * @return the parent builder of this builder + */ + public NodeInfoBuilder getParent() { + return parent; + } + + /** + * Returns the {@link NodeInfo} which has been built by this builder. + * + * @return + * @throws IllegalStateException if {@link #build()} has not been called before. + */ + public NodeInfo getNodeInfo() { + if (!stale) { + throw new IllegalStateException("NodeInfo not built yet"); + } + return nodeInfo; + } + + /** + * Add a {@link PropertyInfo} + * + * @param propertyInfo + * @return this + */ + public NodeInfoBuilder addPropertyInfo(PropertyInfo propertyInfo) { + itemInfos.add(propertyInfo); + return this; + } + + /** + * Add a {@link NodeInfo} + * + * @param nodeInfo + * @return this + */ + public NodeInfoBuilder addNodeInfo(NodeInfo nodeInfo) { + itemInfos.add(nodeInfo); + return this; + } + + private NodeId getId() throws RepositoryException { + if (uuid == null) { + return IdFactoryImpl.getInstance().createNodeId((String) null, getPath()); + } + else { + return IdFactoryImpl.getInstance().createNodeId(uuid); + } + } + + private Path getPath() throws RepositoryException { + if (localName == null && name == null) { + throw new IllegalStateException("Name not set"); + } + + if (parent == null && parentPath == null) { + return PathFactoryImpl.getInstance().getRootPath(); + } + else { + Path path = parentPath == null ? parent.getPath() : parentPath; + if (name == null) { + String ns = namespace == null ? Name.NS_DEFAULT_URI : namespace; + name = NameFactoryImpl.getInstance().create(ns, localName); + } + return PathFactoryImpl.getInstance().create(path, name, true); + } + } + + private Iterator getChildInfos() { + return transformIterator(filterIterator(itemInfos.iterator(), + new Predicate(){ + public boolean evaluate(ItemInfo info) { + return info.denotesNode(); + } + }), + new Transformer(){ + public ChildInfo transform(ItemInfo info) { + return new ChildInfoImpl( + info.getPath().getName(), null, + Path.INDEX_DEFAULT); + } + }); + } + + private Iterator getPropertyIds() { + return transformIterator(filterIterator(itemInfos.iterator(), + new Predicate(){ + public boolean evaluate(ItemInfo info) { + return !info.denotesNode(); + } + }), + new Transformer(){ + public PropertyId transform(ItemInfo info) { + return (PropertyId) info.getId(); + } + }); + } + + } + + /** + * Builder for {@link PropertyInfo}s. Use {@link NodeInfoBuilder#createPropertyInfo(String)} + * to create an instance of this class. + */ + public static class PropertyInfoBuilder { + private final NodeInfoBuilder parent; + private final Listener listener; + + private Name name; + private String localName; + private String namespace; + private final List values = new ArrayList(); + private int type = PropertyType.UNDEFINED; + private boolean isMultivalued = true; + + private boolean stale; + private PropertyInfo propertyInfo; + + private PropertyInfoBuilder(NodeInfoBuilder nodeInfoBuilder, String localName, Listener listener) { + super(); + parent = nodeInfoBuilder; + this.localName = localName; + this.listener = listener; + } + + /** + * Set the name of this property + * + * @param name + * @return + */ + public PropertyInfoBuilder setName(Name name) { + this.name = name; + return this; + } + + /** + * Set the localName of this property + * + * @param localName + * @return + */ + public PropertyInfoBuilder setName(String localName) { + this.localName = localName; + return this; + } + + /** + * Set the namespace + * + * @param namespace + * @return + */ + public PropertyInfoBuilder setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + /** + * Set the {@link PropertyType type} of this property + * + * @param type + * @return this + * @throws IllegalStateException if a property of a different type has been added before. + */ + public PropertyInfoBuilder setType(int type) { + if (values.size() > 0 && type != values.get(0).getType()) { + throw new IllegalStateException("Type mismatch. " + + "Required " + PropertyType.nameFromValue(values.get(0).getType()) + + " found " + PropertyType.nameFromValue(type)); + } + + this.type = type; + return this; + } + + /** + * Add a value to this property. Sets this property to single valued if + * this is the first value. Otherwise sets this property to multi-valued. + * + * @param value + * @return this + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(QValue value) { + int actualType = value.getType(); + if (type != PropertyType.UNDEFINED && type != actualType) { + throw new IllegalStateException("Type mismatch. " + + "Required " + PropertyType.nameFromValue(type) + + " found " + PropertyType.nameFromValue(value.getType())); + } + + values.add(value); + type = actualType; + isMultivalued = values.size() != 1; + return this; + } + + /** + * Add a {@link PropertyType#STRING} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(String value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value, PropertyType.STRING)); + } + + /** + * Add a {@link PropertyType#DATE} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(Calendar value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#DOUBLE} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(double value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#LONG} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(long value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#BOOLEAN} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(boolean value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#NAME} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(Name value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#PATH} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(Path value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#DECIMAL} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(BigDecimal value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#URI} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(URI value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#BINARY} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(byte[] value) throws RepositoryException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#BINARY} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(InputStream value) throws RepositoryException, IOException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Add a {@link PropertyType#BINARY} value to this property. + * + * @param value + * @return this + * @throws RepositoryException + * @throws IllegalStateException if the type of the value does not match the type of this property + */ + public PropertyInfoBuilder addValue(File value) throws RepositoryException, IOException { + return addValue(QValueFactoryImpl.getInstance().create(value)); + } + + /** + * Set this property to multi-values. + * + * @param on + * @return this + * @throws IllegalStateException if this property does not contain exactly on value + */ + public PropertyInfoBuilder setMultivalued(boolean on) { + if (!on && values.size() != 1) { + throw new IllegalStateException( + "Cannot create single valued property when multiple values are present"); + } + isMultivalued = true; + return this; + } + + /** + * Build the {@link PropertyInfo}. If a {@link Listener} is associated with this + * instance, then its {@link Listener#createPropertyInfo(PropertyInfo)} methods + * is called. + * + * @return the parent builder of this builder + * @throws RepositoryException + * @throws IllegalStateException if build has been called before + * @throws IllegalStateException if the type is not set + */ + public NodeInfoBuilder build() throws RepositoryException { + if (stale) { + throw new IllegalStateException("Builder is stale"); + } + else if (type == PropertyType.UNDEFINED) { + throw new IllegalStateException("Type not set"); + } + else if (localName == null && name == null) { + throw new IllegalStateException("Name not set"); + } + else { + stale = true; + + NodeId parentId = parent.getId(); + if (name == null) { + String ns = namespace == null ? Name.NS_DEFAULT_URI : namespace; + name = NameFactoryImpl.getInstance().create(ns, localName); + } + Path path = PathFactoryImpl.getInstance().create(parent.getPath(), name, true); + PropertyId id = IdFactoryImpl.getInstance().createPropertyId(parentId, name); + + propertyInfo = new PropertyInfoImpl(path, id, type, isMultivalued, + values.toArray(new QValue[values.size()])); + + if (listener != null) { + listener.createPropertyInfo(propertyInfo); + } + return parent.addPropertyInfo(propertyInfo); + } + } + + /** + * @return the parent builder of this builder + */ + public NodeInfoBuilder getParent() { + return parent; + } + + /** + * Returns the {@link PropertyInfo} which has been built by this builder. + * + * @return + * @throws IllegalStateException if {@link #build()} has not been called before. + */ + public PropertyInfo getPropertyInfo() { + if (!stale) { + throw new IllegalStateException("PropertyInfo not built yet"); + } + return propertyInfo; + } + + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoCacheImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoCacheImpl.java new file mode 100644 index 00000000000..426b194343f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoCacheImpl.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.spi.commons; + +import org.apache.commons.collections.map.LinkedMap; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.RepositoryService; + +/** + * This implementation of {@link ItemInfoCache} has a default size of 5000 items. + * Item infos are put into the cache after they have been read from the {@link RepositoryService}. + * If the cache is full, the oldest item is discarded. Reading items removes them + * from the cache. + * + * The underlying idea here is, that {@link ItemInfo}s which are supplied by the + * RepositoryService but not immediately needed are put into the + * cache to avoid further round trips to RepositoryService. + * When they are needed later, they are read from the cache. There is no need to + * keep them in this cache after that point since they are present in the + * hierarchy from then on. + */ +public class ItemInfoCacheImpl implements ItemInfoCache { + + /** + * Default size of the cache. + */ + public static final int DEFAULT_CACHE_SIZE = 5000; + + private final int cacheSize; + private final LinkedMap entries; + + /** + * Create a new instance with the default cache size. + * @see #DEFAULT_CACHE_SIZE + */ + public ItemInfoCacheImpl() { + this(DEFAULT_CACHE_SIZE); + } + + /** + * Create a new instance with a given cache size. + * @param cacheSize + */ + public ItemInfoCacheImpl(int cacheSize) { + super(); + this.cacheSize = cacheSize; + entries = new LinkedMap(cacheSize); + } + + /** + * This implementation removes the item from the cache + * if it is present. Furthermore if the nodeId + * id uuid based, and no item is found by the nodeId + * a second lookup is done by the path. + */ + public Entry getNodeInfo(NodeId nodeId) { + Object entry = entries.remove(nodeId); + if (entry == null) { + entry = entries.remove(nodeId.getPath()); + } else { + // there might be a corresponding path-indexed entry, clear it as well + entries.remove(node(entry).info.getPath()); + } + + return node(entry); + } + + /** + * This implementation removes the item from the cache + * if it is present. Furthermore if the propertyId + * id uuid based, and no item is found by the propertyId + * a second lookup is done by the path. + */ + public Entry getPropertyInfo(PropertyId propertyId) { + Object entry = entries.remove(propertyId); + if (entry == null) { + entry = entries.remove(propertyId.getPath()); + } else { + // there might be a corresponding path-indexed entry, clear it as well + entries.remove(property(entry).info.getPath()); + } + + return property(entry); + } + + /** + * This implementation cached the item by its id and if the id + * is uuid based but has no path, also by its path. + */ + public void put(ItemInfo info, long generation) { + ItemId id = info.getId(); + Entry entry = info.denotesNode() + ? new Entry((NodeInfo) info, generation) + : new Entry((PropertyInfo) info, generation); + + put(id, entry); + if (id.getUniqueID() != null && id.getPath() == null) { + put(info.getPath(), entry); + } + } + + public void dispose() { + entries.clear(); + } + + // -----------------------------------------------------< private >--- + + private void put(Object key, Entry entry) { + entries.remove(key); + if (entries.size() >= cacheSize) { + entries.remove(entries.firstKey()); // xxx AbstractLinkedMap#firstKey() Javadoc is wrong. See COLLECTIONS-353 + } + entries.put(key, entry); + } + + @SuppressWarnings("unchecked") + private static Entry node(Object entry) { + if (entry != null && ((Entry) entry).info.denotesNode()) { + return (Entry) entry; + } + else { + return null; + } + } + + @SuppressWarnings("unchecked") + private static Entry property(Object entry) { + if (entry != null && !((Entry) entry).info.denotesNode()) { + return (Entry) entry; + } + else { + return null; + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoImpl.java new file mode 100644 index 00000000000..0c8b0edef58 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/ItemInfoImpl.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.spi.commons; + +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.NodeId; + +import java.io.Serializable; + +/** + * ItemInfoImpl is a base class for ItemInfo + * implementations. + */ +public abstract class ItemInfoImpl implements ItemInfo, Serializable { + + /** + * The path of this item info. + */ + private final Path path; + + /** + * Flag indicating whether this is a node or a property info. + */ + private final boolean isNode; + + /** + * Creates a new item info from the given name, path and boolean flag. + * + * @param parentId the parent id. + * @param name the name of this item. + * @param path the path to this item. + * @param isNode if this item is a node. + * @deprecated Use {@link #ItemInfoImpl(Path, boolean)} instead. The + * parentId is not used any more and the corresponding getter has been + * removed. + */ + public ItemInfoImpl(NodeId parentId, Name name, Path path, boolean isNode) { + this(path, isNode); + } + + /** + * Creates a new item info from the given name, path and boolean flag. + * + * @param path the path to this item. + * @param isNode if this item is a node. + */ + public ItemInfoImpl(Path path, boolean isNode) { + this.path = path; + this.isNode = isNode; + } + + /** + * {@inheritDoc} + */ + public boolean denotesNode() { + return isNode; + } + + /** + * {@inheritDoc} + */ + public Path getPath() { + return path; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/LockInfoImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/LockInfoImpl.java new file mode 100644 index 00000000000..04906cad5d9 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/LockInfoImpl.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.spi.commons; + +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.NodeId; + +import java.io.Serializable; + +/** + * LockInfoImpl implements a serializable LockInfo + * based on another lock info. + */ +public class LockInfoImpl implements LockInfo, Serializable { + + /** + * The lock token for this lock info. + */ + private final String lockToken; + + /** + * The owner of the lock. + */ + private final String lockOwner; + + /** + * The isDeep flag. + */ + private final boolean isDeep; + + /** + * The isSessionScoped flag. + */ + private final boolean isSessionScoped; + + /** + * Number of seconds until the lock time outs. + */ + private final long secondsRemaining; + + /** + * Flag indicating if the session is lock owner or not. + */ + private final boolean isLockOwner; + + /** + * The NodeId of the locked node. + */ + private final NodeId nodeId; + + /** + * Creates a new lock info for the given lock info. + * + * @param lockToken the lock token + * @param lockOwner the lock owner + * @param isDeep whether this lock is deep or not + * @param isSessionScoped whether this lock is session scoped or not + * @param nodeId the node id of the locked node. + * @deprecated Use {@link #LockInfoImpl(String, String, boolean, boolean, long, boolean, NodeId)} instaed. + */ + public LockInfoImpl(String lockToken, String lockOwner, boolean isDeep, + boolean isSessionScoped, NodeId nodeId) { + this(lockToken, lockOwner, isDeep, isSessionScoped, Long.MAX_VALUE, lockToken != null, nodeId); + } + + /** + * Creates a new lock info for the given lock info. + * + * @param lockToken the lock token + * @param lockOwner the lock owner + * @param isDeep whether this lock is deep or not + * @param isSessionScoped whether this lock is session scoped or not + * @param secondsRemaining Number of seconds until the lock timeout is reached. + * @param isLockOwner true if the calling session is lock + * owner; false otherwise. + * @param nodeId the node id of the locked node. + * @since JCR 2.0 + */ + public LockInfoImpl(String lockToken, String lockOwner, boolean isDeep, + boolean isSessionScoped, long secondsRemaining, + boolean isLockOwner, NodeId nodeId) { + this.lockToken = lockToken; + this.lockOwner = lockOwner; + this.isDeep = isDeep; + this.isSessionScoped = isSessionScoped; + this.secondsRemaining = secondsRemaining; + this.isLockOwner = isLockOwner; + this.nodeId = nodeId; + } + + /** + * {@inheritDoc} + */ + public String getLockToken() { + return lockToken; + } + + /** + * {@inheritDoc} + */ + public String getOwner() { + return lockOwner; + } + + /** + * {@inheritDoc} + */ + public boolean isDeep() { + return isDeep; + } + + /** + * {@inheritDoc} + */ + public boolean isSessionScoped() { + return isSessionScoped; + } + + /** + * {@inheritDoc} + */ + public long getSecondsRemaining() { + return secondsRemaining; + } + + /** + * {@inheritDoc} + */ + public boolean isLockOwner() { + return isLockOwner; + } + + /** + * {@inheritDoc} + */ + public NodeId getNodeId() { + return nodeId; + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/NodeInfoImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/NodeInfoImpl.java new file mode 100644 index 00000000000..c4c0f8319c8 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/NodeInfoImpl.java @@ -0,0 +1,251 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons; + +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * NodeInfoImpl implements a serializable NodeInfo + * based on another node info. + */ +public class NodeInfoImpl extends ItemInfoImpl implements NodeInfo { + + /** + * The node id of the underlying node. + */ + private final NodeId id; + + /** + * 1-based index of the underlying node. + */ + private final int index; + + /** + * The name of the primary node type. + */ + private final Name primaryTypeName; + + /** + * The names of assigned mixins. + */ + private final Name[] mixinNames; + + /** + * The list of {@link PropertyId}s that reference this node info. + */ + private final List references; + + /** + * The list of {@link PropertyId}s of this node info. + */ + private final List propertyIds; + + /** + * The list of {@link ChildInfo}s of this node info. + */ + private final List childInfos; + + /** + * Creates a new serializable NodeInfo for the given + * NodeInfo. + * + * @param nodeInfo + */ + public static NodeInfo createSerializableNodeInfo( + NodeInfo nodeInfo, final IdFactory idFactory) { + if (nodeInfo instanceof Serializable) { + return nodeInfo; + } else { + List serRefs = new ArrayList(); + for (PropertyId ref : nodeInfo.getReferences()) { + NodeId parentId = ref.getParentId(); + parentId = idFactory.createNodeId( + parentId.getUniqueID(), parentId.getPath()); + serRefs.add(idFactory.createPropertyId(parentId, ref.getName())); + } + NodeId nodeId = nodeInfo.getId(); + nodeId = idFactory.createNodeId(nodeId.getUniqueID(), nodeId.getPath()); + final Iterator propIds = nodeInfo.getPropertyIds(); + final Iterator childInfos = nodeInfo.getChildInfos(); + return new NodeInfoImpl(nodeInfo.getPath(), nodeId, + nodeInfo.getIndex(), nodeInfo.getNodetype(), + nodeInfo.getMixins(), serRefs.iterator(), + new Iterator() { + public boolean hasNext() { + return propIds.hasNext(); + } + public PropertyId next() { + PropertyId propId = propIds.next(); + NodeId parentId = propId.getParentId(); + idFactory.createNodeId( + parentId.getUniqueID(), parentId.getPath()); + return idFactory.createPropertyId( + parentId, propId.getName()); + } + public void remove() { + throw new UnsupportedOperationException(); + } + }, + ((childInfos == null) ? null : + new Iterator() { + public boolean hasNext() { + return childInfos.hasNext(); + } + public ChildInfo next() { + ChildInfo cInfo = childInfos.next(); + if (cInfo instanceof Serializable) { + return cInfo; + } else { + return new ChildInfoImpl(cInfo.getName(), cInfo.getUniqueID(), cInfo.getIndex()); + } + } + public void remove() { + throw new UnsupportedOperationException(); + } + }) + ); + } + } + + /** + * Creates a new node info from the given parameters. + * + * @param parentId the parent id. + * @param name the name of this item. + * @param path the path to this item. + * @param id the id of this item. + * @param index the index of this item. + * @param primaryTypeName the name of the primary node type. + * @param mixinNames the names of the assigned mixins. + * @param references the references to this node. + * @param propertyIds the properties of this node. + * @param childInfos the child infos of this node or null. + * @deprecated Use {@link #NodeInfoImpl(Path, NodeId, int, Name, Name[], Iterator, Iterator, Iterator)} + * instead. The parentId is not used any more. + */ + public NodeInfoImpl(NodeId parentId, Name name, Path path, NodeId id, + int index, Name primaryTypeName, Name[] mixinNames, + Iterator references, Iterator propertyIds, + Iterator childInfos) { + this(path, id, index, primaryTypeName, mixinNames, references, propertyIds, childInfos); + } + + /** + * Creates a new node info from the given parameters. + * + * @param path the path to this item. + * @param id the id of this item. + * @param index the index of this item. + * @param primaryTypeName the name of the primary node type. + * @param mixinNames the names of the assigned mixins. + * @param references the references to this node. + * @param propertyIds the properties of this node. + */ + public NodeInfoImpl(Path path, NodeId id, int index, Name primaryTypeName, + Name[] mixinNames, Iterator references, + Iterator propertyIds, + Iterator childInfos) { + super(path, true); + this.id = id; + this.index = index; + this.primaryTypeName = primaryTypeName; + this.mixinNames = mixinNames; + if (!references.hasNext()) { + this.references = Collections.emptyList(); + } else { + this.references = new ArrayList(); + while (references.hasNext()) { + this.references.add(references.next()); + } + } + this.propertyIds = new ArrayList(); + while (propertyIds.hasNext()) { + this.propertyIds.add(propertyIds.next()); + } + if (childInfos == null) { + this.childInfos = null; + } else { + this.childInfos = new ArrayList(); + while (childInfos.hasNext()) { + this.childInfos.add(childInfos.next()); + } + } + } + + //-------------------------------< NodeInfo >------------------------------- + + /** + * {@inheritDoc} + */ + public NodeId getId() { + return id; + } + + /** + * {@inheritDoc} + */ + public int getIndex() { + return index; + } + + /** + * {@inheritDoc} + */ + public Name getNodetype() { + return primaryTypeName; + } + + /** + * {@inheritDoc} + */ + public Name[] getMixins() { + return mixinNames; + } + + /** + * {@inheritDoc} + */ + public PropertyId[] getReferences() { + return references.toArray(new PropertyId[references.size()]); + } + + /** + * {@inheritDoc} + */ + public Iterator getPropertyIds() { + return propertyIds.iterator(); + } + + /** + * {@inheritDoc} + */ + public Iterator getChildInfos() { + return (childInfos == null) ? null : childInfos.iterator(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/PropertyInfoImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/PropertyInfoImpl.java new file mode 100644 index 00000000000..8abf38b66ec --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/PropertyInfoImpl.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.spi.commons; + +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +import java.io.Serializable; + +/** + * PropertyInfoImpl implements a serializable + * PropertyInfo based on another property info. + */ +public class PropertyInfoImpl extends ItemInfoImpl implements PropertyInfo { + + /** + * The property info of the underlying property. + */ + private final PropertyId propertyId; + + /** + * The type of the property. + */ + private final int type; + + /** + * The multiValued flag. + */ + private final boolean isMultiValued; + + /** + * The values of this property info. + */ + private final QValue[] values; + + /** + * Creates a new serializable property info for the given + * PropertyInfo. + * + * @param propertyInfo + */ + public static PropertyInfo createSerializablePropertyInfo( + PropertyInfo propertyInfo, IdFactory idFactory) { + if (propertyInfo instanceof Serializable) { + return propertyInfo; + } else { + NodeId parentId = propertyInfo.getId().getParentId(); + parentId = idFactory.createNodeId( + parentId.getUniqueID(), parentId.getPath()); + PropertyId propId = idFactory.createPropertyId( + parentId, propertyInfo.getId().getName()); + return new PropertyInfoImpl(propertyInfo.getPath(), + propId, propertyInfo.getType(), + propertyInfo.isMultiValued(), propertyInfo.getValues()); + } + } + + /** + * Creates a new property info for the given parameters. + * + * @param parentId the parent id. + * @param name the name of this property. + * @param path the path to this property. + * @param id the id of this property. + * @param type the type of this property. + * @param isMultiValued whether this property is multi-valued. + * @param values the values. + * @deprecated Use {@link #PropertyInfoImpl(Path, PropertyId, int, boolean, QValue[])} + * instead. The parentId is not used any more. + */ + public PropertyInfoImpl(NodeId parentId, Name name, Path path, + PropertyId id, int type, boolean isMultiValued, + QValue[] values) { + this(path, id, type, isMultiValued, values); + } + + /** + * Creates a new property info for the given parameters. + * + * @param path the path to this property. + * @param id the id of this property. + * @param type the type of this property. + * @param isMultiValued whether this property is multi-valued. + * @param values the values. + */ + public PropertyInfoImpl(Path path, PropertyId id, int type, + boolean isMultiValued, QValue[] values) { + super(path, false); + this.propertyId = id; + this.type = type; + this.isMultiValued = isMultiValued; + this.values = values; + } + + /** + * {@inheritDoc} + */ + public PropertyId getId() { + return propertyId; + } + + /** + * {@inheritDoc} + */ + public int getType() { + return type; + } + + /** + * {@inheritDoc} + */ + public boolean isMultiValued() { + return isMultiValued; + } + + /** + * {@inheritDoc} + */ + public QValue[] getValues() { + QValue[] vals = new QValue[values.length]; + System.arraycopy(values, 0, vals, 0, values.length); + return vals; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QItemDefinitionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QItemDefinitionImpl.java new file mode 100644 index 00000000000..92e72633852 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QItemDefinitionImpl.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons; + +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.Name; + +import java.io.Serializable; + +/** + * This abstract class implements the QItemDefinition + * interface and additionally provides setter methods for the + * various item definition attributes. + */ +public abstract class QItemDefinitionImpl implements QItemDefinition, Serializable { + + /** + * The name of the child item. + */ + private final Name name; + + /** + * The name of the declaring node type. + */ + private final Name declaringNodeType; + + /** + * The 'autoCreated' flag. + */ + private final boolean autoCreated; + + /** + * The 'onParentVersion' attribute. + */ + private final int onParentVersion; + + /** + * The 'protected' flag. + */ + private final boolean writeProtected; + + /** + * The 'mandatory' flag. + */ + private final boolean mandatory; + + /** + * HashCode of this object + */ + protected transient int hashCode = 0; + + /** + * Creates a new QItemDefinitionImpl. + * + * @param name the name of the child item. + * @param declaringNodeType the declaring node type + * @param isAutoCreated if this item is auto created. + * @param isMandatory if this is a mandatory item. + * @param onParentVersion the on parent version behaviour. + * @param isProtected if this item is protected. + */ + QItemDefinitionImpl(Name name, Name declaringNodeType, + boolean isAutoCreated, boolean isMandatory, + int onParentVersion, boolean isProtected) { + this.name = name; + this.declaringNodeType = declaringNodeType; + this.autoCreated = isAutoCreated; + this.mandatory = isMandatory; + this.onParentVersion = onParentVersion; + this.writeProtected = isProtected; + } + + //--------------------------------------------------------------< QItemDefinition > + /** + * {@inheritDoc} + */ + public Name getDeclaringNodeType() { + return declaringNodeType; + } + + /** + * {@inheritDoc} + */ + public Name getName() { + return name; + } + + /** + * {@inheritDoc} + */ + public boolean isAutoCreated() { + return autoCreated; + } + + /** + * {@inheritDoc} + */ + public int getOnParentVersion() { + return onParentVersion; + } + + /** + * {@inheritDoc} + */ + public boolean isProtected() { + return writeProtected; + } + + /** + * {@inheritDoc} + */ + public boolean isMandatory() { + return mandatory; + } + + /** + * {@inheritDoc} + */ + public boolean definesResidual() { + return Name.NS_DEFAULT_URI.equals(name.getNamespaceURI()) && "*".equals(name.getLocalName()); + } + + //-------------------------------------------< java.lang.Object overrides > + /** + * Compares two item definitions for equality. Returns true + * if the given object is an item definition and has the same attributes + * as this item definition. + * + * @param obj the object to compare this item definition with + * @return true if the object is equal to this item definition, + * false otherwise + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof QItemDefinition) { + QItemDefinition other = (QItemDefinition) obj; + return (declaringNodeType == null + ? other.getDeclaringNodeType() == null + : declaringNodeType.equals(other.getDeclaringNodeType())) + && (name == null ? other.getName() == null : name.equals(other.getName())) + && autoCreated == other.isAutoCreated() + && onParentVersion == other.getOnParentVersion() + && writeProtected == other.isProtected() + && mandatory == other.isMandatory(); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + int h = 17; + h = 37 * h + getDeclaringNodeType().hashCode(); + h = 37 * h + getName().hashCode(); + h = 37 * h + getOnParentVersion(); + h = 37 * h + (isProtected() ? 11 : 43); + h = 37 * h + (isMandatory() ? 11 : 43); + h = 37 * h + (isAutoCreated() ? 11 : 43); + return h; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QNodeDefinitionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QNodeDefinitionImpl.java new file mode 100644 index 00000000000..eb5d47d1588 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QNodeDefinitionImpl.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.spi.commons; + +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +import java.util.Arrays; +import java.util.TreeSet; +import java.util.Set; +import java.util.HashSet; + +import javax.jcr.NamespaceException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; + +/** + * QNodeDefinitionImpl implements a QNodeDefinition. + */ +public class QNodeDefinitionImpl extends QItemDefinitionImpl implements QNodeDefinition { + + private static final long serialVersionUID = -3671882394577685657L; + + /** + * The name of the default primary type. + */ + private final Name defaultPrimaryType; + /** + * The names of the required primary types. + */ + private final Set requiredPrimaryTypes = new HashSet(); + /** + * The 'allowsSameNameSiblings' flag. + */ + private final boolean allowsSameNameSiblings; + + /** + * Copy constructor. + * + * @param nodeDef some other node definition. + */ + public QNodeDefinitionImpl(QNodeDefinition nodeDef) { + this(nodeDef.getName(), nodeDef.getDeclaringNodeType(), + nodeDef.isAutoCreated(), nodeDef.isMandatory(), + nodeDef.getOnParentVersion(), nodeDef.isProtected(), + nodeDef.getDefaultPrimaryType(), + nodeDef.getRequiredPrimaryTypes(), + nodeDef.allowsSameNameSiblings()); + } + + /** + * Creates a new SPI node definition based on a JCR NodeDefinition. + * + * @param name the name of the child item. + * @param declaringNodeType the declaring node type + * @param isAutoCreated if this item is auto created. + * @param isMandatory if this is a mandatory item. + * @param onParentVersion the on parent version behaviour. + * @param isProtected if this item is protected. + * @param defaultPrimaryType the default primary type name + * @param requiredPrimaryTypes the required primary type name + * @param allowsSameNameSiblings if this node allows SNS + */ + public QNodeDefinitionImpl(Name name, Name declaringNodeType, + boolean isAutoCreated, boolean isMandatory, + int onParentVersion, boolean isProtected, + Name defaultPrimaryType, Name[] requiredPrimaryTypes, + boolean allowsSameNameSiblings) { + super(name, declaringNodeType, isAutoCreated, isMandatory, + onParentVersion, isProtected); + this.defaultPrimaryType = defaultPrimaryType; + this.requiredPrimaryTypes.addAll(Arrays.asList(requiredPrimaryTypes)); + // sanitize field value + if (this.requiredPrimaryTypes.isEmpty()) { + this.requiredPrimaryTypes.add(NameConstants.NT_BASE); + } + this.allowsSameNameSiblings = allowsSameNameSiblings; + } + + /** + * Creates a new node definition based on a JCR NodeDefinition. + * + * @param nodeDef the node definition. + * @param resolver the name/path resolver of the session that provided the + * node definition + * @throws NameException if nodeDef contains an illegal + * name. + * @throws NamespaceException if nodeDef contains a name with + * an namespace prefix that is unknown to + * resolver. + */ + public QNodeDefinitionImpl(NodeDefinition nodeDef, + NamePathResolver resolver) + throws NameException, NamespaceException { + this(nodeDef.getName().equals(NameConstants.ANY_NAME.getLocalName()) ? NameConstants.ANY_NAME : resolver.getQName(nodeDef.getName()), + nodeDef.getDeclaringNodeType() != null ? resolver.getQName(nodeDef.getDeclaringNodeType().getName()) : null, + nodeDef.isAutoCreated(), nodeDef.isMandatory(), + nodeDef.getOnParentVersion(), nodeDef.isProtected(), + nodeDef.getDefaultPrimaryType() != null ? resolver.getQName(nodeDef.getDefaultPrimaryType().getName()) : null, + getNodeTypeNames(nodeDef.getRequiredPrimaryTypes(), resolver), + nodeDef.allowsSameNameSiblings()); + } + + //----------------------------------------------------< QNodeDefinition >--- + /** + * {@inheritDoc} + */ + public Name getDefaultPrimaryType() { + return defaultPrimaryType; + } + + /** + * {@inheritDoc} + */ + public Name[] getRequiredPrimaryTypes() { + return requiredPrimaryTypes.toArray(new Name[requiredPrimaryTypes.size()]); + } + + /** + * {@inheritDoc} + */ + public boolean allowsSameNameSiblings() { + return allowsSameNameSiblings; + } + + /** + * {@inheritDoc} + * + * @return always true + */ + public boolean definesNode() { + return true; + } + + //-------------------------------------------------------------< Object >--- + /** + * Compares two node definitions for equality. Returns true + * if the given object is a node definition and has the same attributes + * as this node definition. + * + * @param obj the object to compare this node definition with + * @return true if the object is equal to this node definition, + * false otherwise + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof QNodeDefinition) { + QNodeDefinition other = (QNodeDefinition) obj; + return super.equals(obj) + && requiredPrimaryTypes.equals(new HashSet( + Arrays.asList(other.getRequiredPrimaryTypes()))) + && (defaultPrimaryType == null + ? other.getDefaultPrimaryType() == null + : defaultPrimaryType.equals(other.getDefaultPrimaryType())) + && allowsSameNameSiblings == other.allowsSameNameSiblings(); + } + return false; + } + + /** + * Overwrites {@link QItemDefinitionImpl#hashCode()}. + * + * @return the hash code + */ + @Override + public int hashCode() { + if (hashCode == 0) { + int h = super.hashCode(); + h = 37 * h + (defaultPrimaryType == null ? 0 : defaultPrimaryType.hashCode()); + h = 37 * h + requiredPrimaryTypes.hashCode(); + h = 37 * h + (allowsSameNameSiblings ? 11 : 43); + hashCode = h; + } + return hashCode; + + } + + //-----------------------------------------------------------< internal >--- + /** + * Returns the names of the passed node types using the namespace resolver + * to parse the names. + * + * @param nt the node types + * @param resolver the name/path resolver of the session that provided + * nt. + * @return the names of the node types. + * @throws NameException if a node type returns an illegal name. + * @throws NamespaceException if the name of a node type contains a prefix + * that is not known to resolver. + */ + private static Name[] getNodeTypeNames(NodeType[] nt, + NamePathResolver resolver) + throws NameException, NamespaceException { + Name[] names = new Name[nt.length]; + for (int i = 0; i < nt.length; i++) { + Name ntName = resolver.getQName(nt[i].getName()); + names[i] = ntName; + } + return names; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QNodeTypeDefinitionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QNodeTypeDefinitionImpl.java new file mode 100644 index 00000000000..3c49fa4a2e0 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/QNodeTypeDefinitionImpl.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.spi.commons; + +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; + +import javax.jcr.PropertyType; +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.NodeDefinition; +import java.util.Collection; +import java.util.HashSet; +import java.util.Collections; +import java.util.Arrays; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.Set; +import java.io.Serializable; + +/** + * QNodeTypeDefinitionImpl implements a serializable SPI node + * type definition. + */ +public class QNodeTypeDefinitionImpl implements QNodeTypeDefinition, Serializable { + + private static final long serialVersionUID = -4065300714874671511L; + + /** + * The name of the node definition. + */ + private final Name name; + + /** + * The names of the declared super types of this node type definition. + */ + private final Name[] supertypes; + + /** + * The names of the supported mixins on this node type (or null) + */ + private final Name[] supportedMixins; + + /** + * Indicates whether this is a mixin node type definition. + */ + private final boolean isMixin; + + /** + * Indicates whether this is an abstract node type definition. + */ + private final boolean isAbstract; + + /** + * Indicates whether this is a queryable node type definition. + */ + private final boolean isQueryable; + + /** + * Indicates whether this node type definition has orderable child nodes. + */ + private final boolean hasOrderableChildNodes; + + /** + * The name of the primary item or null if none is defined. + */ + private final Name primaryItemName; + + /** + * The list of child node definitions. + */ + private final Set propertyDefs; + + /** + * The list of property definitions. + */ + private final Set childNodeDefs; + + /** + * Unmodifiable collection of dependent node type Names. + * Calculated on demand. + * + * @see #getDependencies() + */ + private transient volatile Collection dependencies; + + /** + * Default constructor. + */ + public QNodeTypeDefinitionImpl() { + this(null, Name.EMPTY_ARRAY, null, false, false, true, false, null, + QPropertyDefinition.EMPTY_ARRAY, QNodeDefinition.EMPTY_ARRAY); + } + + /** + * Copy constructor. + * + * @param nt the node type definition. + */ + public QNodeTypeDefinitionImpl(QNodeTypeDefinition nt) { + this(nt.getName(), nt.getSupertypes(), nt.getSupportedMixinTypes(), + nt.isMixin(), nt.isAbstract(), nt.isQueryable(), + nt.hasOrderableChildNodes(), nt.getPrimaryItemName(), + nt.getPropertyDefs(), nt.getChildNodeDefs()); + } + + /** + * Creates a new serializable SPI node type definition. + * + * @param name the name of the node type + * @param supertypes the names of the supertypes + * @param supportedMixins the names of supported mixins (or null) + * @param isMixin if this is a mixin node type + * @param isAbstract if this is an abstract node type definition. + * @param isQueryable if this is a queryable node type definition. + * @param hasOrderableChildNodes if this node type has orderable child + * nodes. + * @param primaryItemName the name of the primary item, or + * null. + * @param declaredPropDefs the declared property definitions. + * @param declaredNodeDefs the declared child node definitions. + */ + public QNodeTypeDefinitionImpl(Name name, + Name[] supertypes, + Name[] supportedMixins, + boolean isMixin, + boolean isAbstract, + boolean isQueryable, + boolean hasOrderableChildNodes, + Name primaryItemName, + QPropertyDefinition[] declaredPropDefs, + QNodeDefinition[] declaredNodeDefs) { + this.name = name; + this.supportedMixins = supportedMixins; + this.isMixin = isMixin; + this.isAbstract = isAbstract; + this.isQueryable = isQueryable; + this.hasOrderableChildNodes = hasOrderableChildNodes; + this.primaryItemName = primaryItemName; + this.propertyDefs = getSerializablePropertyDefs(declaredPropDefs); + this.childNodeDefs = getSerializableNodeDefs(declaredNodeDefs); + // make sure super types are sorted + SortedSet types = new TreeSet(); + types.addAll(Arrays.asList(supertypes)); + this.supertypes = types.toArray(new Name[types.size()]); + } + + /** + * Create a a new QNodeTypeDefinitionImpl from a JCR + * NodeType definition. + * + * @param def node type definition + * @param resolver resolver + * @param qValueFactory value factory + * @throws RepositoryException if an error occurs + */ + public QNodeTypeDefinitionImpl(NodeTypeDefinition def, + NamePathResolver resolver, + QValueFactory qValueFactory) + throws RepositoryException { + this(resolver.getQName(def.getName()), def, resolver, qValueFactory); + } + + /** + * Internal constructor to avoid resolving def.getName() 3 times. + * @param name name of the definition + * @param def node type definition + * @param resolver resolver + * @param qValueFactory value factory + * @throws RepositoryException if an error occurs + */ + private QNodeTypeDefinitionImpl(Name name, NodeTypeDefinition def, + NamePathResolver resolver, + QValueFactory qValueFactory) + throws RepositoryException { + this(name, + getNames(def.getDeclaredSupertypeNames(), resolver), + null, + def.isMixin(), + def.isAbstract(), + def.isQueryable(), + def.hasOrderableChildNodes(), + def.getPrimaryItemName() == null ? null : resolver.getQName(def.getPrimaryItemName()), + createQPropertyDefinitions(name, def.getDeclaredPropertyDefinitions(), resolver, qValueFactory), + createQNodeDefinitions(name, def.getDeclaredChildNodeDefinitions(), resolver)); + } + + //------------------------------------------------< QNodeTypeDefinition >--- + + /** + * {@inheritDoc} + */ + public Name getName() { + return name; + } + + /** + * {@inheritDoc} + */ + public Name[] getSupertypes() { + if (supertypes.length > 0 + || isMixin() || NameConstants.NT_BASE.equals(getName())) { + return supertypes; + } else { + return new Name[] { NameConstants.NT_BASE }; + } + } + + /** + * {@inheritDoc} + */ + public boolean isMixin() { + return isMixin; + } + + /** + * {@inheritDoc} + */ + public boolean isAbstract() { + return isAbstract; + } + + /** + * {@inheritDoc} + */ + public boolean isQueryable() { + return isQueryable; + } + + /** + * {@inheritDoc} + */ + public boolean hasOrderableChildNodes() { + return hasOrderableChildNodes; + } + + /** + * {@inheritDoc} + */ + public Name getPrimaryItemName() { + return primaryItemName; + } + + /** + * {@inheritDoc} + */ + public QPropertyDefinition[] getPropertyDefs() { + return propertyDefs.toArray(new QPropertyDefinition[propertyDefs.size()]); + } + + /** + * {@inheritDoc} + */ + public QNodeDefinition[] getChildNodeDefs() { + return childNodeDefs.toArray(new QNodeDefinition[childNodeDefs.size()]); + } + + /** + * {@inheritDoc} + */ + public Collection getDependencies() { + if (dependencies == null) { + Collection deps = new HashSet(); + // supertypes + deps.addAll(Arrays.asList(supertypes)); + // child node definitions + for (QNodeDefinition childNodeDef : childNodeDefs) { + // default primary type + Name ntName = childNodeDef.getDefaultPrimaryType(); + if (ntName != null && !name.equals(ntName)) { + deps.add(ntName); + } + // required primary type + Name[] ntNames = childNodeDef.getRequiredPrimaryTypes(); + for (Name ntName1 : ntNames) { + if (ntName1 != null && !name.equals(ntName1)) { + deps.add(ntName1); + } + } + } + // property definitions + for (QPropertyDefinition propertyDef : propertyDefs) { + // [WEAK]REFERENCE value constraints + if (propertyDef.getRequiredType() == PropertyType.REFERENCE + || propertyDef.getRequiredType() == PropertyType.WEAKREFERENCE) { + QValueConstraint[] ca = propertyDef.getValueConstraints(); + if (ca != null) { + for (QValueConstraint aCa : ca) { + NameFactory factory = NameFactoryImpl.getInstance(); + Name ntName = factory.create(aCa.getString()); + if (!name.equals(ntName)) { + deps.add(ntName); + } + } + } + } + } + dependencies = Collections.unmodifiableCollection(deps); + } + return dependencies; + } + + /** + * {@inheritDoc} + */ + public Name[] getSupportedMixinTypes() { + if (supportedMixins == null) { + return null; + } + else { + Name[] mixins = new Name[supportedMixins.length]; + System.arraycopy(supportedMixins, 0, mixins, 0, supportedMixins.length); + return mixins; + } + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof QNodeTypeDefinitionImpl) { + QNodeTypeDefinitionImpl other = (QNodeTypeDefinitionImpl) obj; + return (name == null ? other.name == null : name.equals(other.name)) + && (primaryItemName == null ? other.primaryItemName == null : primaryItemName.equals(other.primaryItemName)) + && new HashSet(Arrays.asList(getSupertypes())).equals(new HashSet(Arrays.asList(other.getSupertypes()))) + && isMixin == other.isMixin + && hasOrderableChildNodes == other.hasOrderableChildNodes + && isAbstract == other.isAbstract + && isQueryable == other.isQueryable + && propertyDefs.equals(other.propertyDefs) + && childNodeDefs.equals(other.childNodeDefs); + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return 0; + } + + //-----------------------------------------------------------< internal >--- + /** + * Returns a set of serializable property definitions for + * propDefs. + * + * @param propDefs the SPI property definitions. + * @return a set of serializable property definitions. + */ + private static Set getSerializablePropertyDefs( + QPropertyDefinition[] propDefs) { + Set defs = new HashSet(); + for (QPropertyDefinition pd : propDefs) { + if (pd instanceof Serializable) { + defs.add(pd); + } else { + defs.add(new QPropertyDefinitionImpl(pd)); + } + } + return defs; + } + + /** + * Returns a set of serializable node definitions for + * nodeDefs. + * + * @param nodeDefs the node definitions. + * @return a set of serializable node definitions. + */ + private static Set getSerializableNodeDefs( + QNodeDefinition[] nodeDefs) { + Set defs = new HashSet(); + for (QNodeDefinition nd : nodeDefs) { + if (nd instanceof Serializable) { + defs.add(nd); + } else { + defs.add(new QNodeDefinitionImpl(nd)); + } + } + return defs; + } + + private static Name[] getNames(String[] jcrNames, NamePathResolver resolver) throws NamespaceException, IllegalNameException { + Name[] names = new Name[jcrNames.length]; + for (int i = 0; i < jcrNames.length; i++) { + names[i] = resolver.getQName(jcrNames[i]); + } + return names; + } + + private static QPropertyDefinition[] createQPropertyDefinitions(Name declName, + PropertyDefinition[] pds, + NamePathResolver resolver, + QValueFactory qValueFactory) + throws RepositoryException { + if (pds == null || pds.length == 0) { + return QPropertyDefinition.EMPTY_ARRAY; + } + QPropertyDefinition[] declaredPropDefs = new QPropertyDefinition[pds.length]; + for (int i = 0; i < pds.length; i++) { + PropertyDefinition propDef = pds[i]; + Name name = propDef.getName().equals(NameConstants.ANY_NAME.getLocalName()) + ? NameConstants.ANY_NAME + : resolver.getQName(propDef.getName()); + // check if propDef provides declaring node type and if it matches 'this' one. + if (propDef.getDeclaringNodeType() != null) { + if (!declName.equals(resolver.getQName(propDef.getDeclaringNodeType().getName()))) { + throw new RepositoryException("Property definition specified invalid declaring nodetype: " + + propDef.getDeclaringNodeType().getName() + ", but should be " + declName); + } + } + QValue[] defVls = propDef.getDefaultValues() == null + ? QValue.EMPTY_ARRAY + : ValueFormat.getQValues(propDef.getDefaultValues(), resolver, qValueFactory); + String[] jcrConstraints = propDef.getValueConstraints(); + QValueConstraint[] constraints = QValueConstraint.EMPTY_ARRAY; + if (jcrConstraints != null && jcrConstraints.length > 0) { + constraints = new QValueConstraint[jcrConstraints.length]; + for (int j=0; jQPropertyDefinitionImpl
        implements SPI property + * definition interface. + */ +public class QPropertyDefinitionImpl extends QItemDefinitionImpl + implements QPropertyDefinition { + + private static final long serialVersionUID = 1064686456661663541L; + + /** + * The required type. + */ + private final int requiredType; + + /** + * The value constraints. + */ + private final QValueConstraint[] valueConstraints; + + /** + * The default values. + */ + private final QValue[] defaultValues; + + /** + * The 'multiple' flag + */ + private final boolean multiple; + + /** + * The available query operators + */ + private final String[] availableQueryOperators; + + /** + * The 'fullTextSearcheable' flag + */ + private final boolean fullTextSearchable; + /** + * The 'queryOrderable' flag + */ + private final boolean queryOrderable; + + /** + * Copy constructor. + * + * @param propDef some other property definition. + */ + public QPropertyDefinitionImpl(QPropertyDefinition propDef) { + this(propDef.getName(), propDef.getDeclaringNodeType(), + propDef.isAutoCreated(), propDef.isMandatory(), + propDef.getOnParentVersion(), propDef.isProtected(), + propDef.getDefaultValues(), propDef.isMultiple(), + propDef.getRequiredType(), propDef.getValueConstraints(), + propDef.getAvailableQueryOperators(), + propDef.isFullTextSearchable(), + propDef.isQueryOrderable()); + } + + /** + * Creates a new serializable property definition. + * + * @param name the name of the child item. + * @param declaringNodeType the declaring node type + * @param isAutoCreated if this item is auto created. + * @param isMandatory if this is a mandatory item. + * @param onParentVersion the on parent version behaviour. + * @param isProtected if this item is protected. + * @param defaultValues the default values or null if there + * are none. + * @param isMultiple if this property is multi-valued. + * @param requiredType the required type for this property. + * @param valueConstraints the value constraints for this property. If none + * exist an empty array must be passed. + * @param availableQueryOperators the available query operators + * @param isFullTextSearchable if this is fulltext searchable + * @param isQueryOrderable if this is queryable + * @throws NullPointerException if valueConstraints or + * availableQueryOperators is + * null. + * @since JCR 2.0 + */ + public QPropertyDefinitionImpl(Name name, Name declaringNodeType, + boolean isAutoCreated, boolean isMandatory, + int onParentVersion, boolean isProtected, + QValue[] defaultValues, boolean isMultiple, + int requiredType, + QValueConstraint[] valueConstraints, + String[] availableQueryOperators, + boolean isFullTextSearchable, + boolean isQueryOrderable) { + super(name, declaringNodeType, isAutoCreated, isMandatory, + onParentVersion, isProtected); + if (valueConstraints == null) { + throw new NullPointerException("valueConstraints"); + } + if (availableQueryOperators == null) { + throw new NullPointerException("availableQueryOperators"); + } + this.defaultValues = defaultValues; + this.multiple = isMultiple; + this.requiredType = requiredType; + this.valueConstraints = valueConstraints; + this.availableQueryOperators = availableQueryOperators; + this.fullTextSearchable = isFullTextSearchable; + this.queryOrderable = isQueryOrderable; + } + + /** + * Creates a new property definition based on propDef. + * + * @param propDef the JCR property definition. + * @param resolver the name/path resolver of the session that provided + * the property definition. + * @param qValueFactory the QValue factory. + * @throws RepositoryException if an error occurs while reading from + * propDef. + */ + public QPropertyDefinitionImpl(PropertyDefinition propDef, + NamePathResolver resolver, + QValueFactory qValueFactory) + throws RepositoryException { + this(propDef.getName().equals(NameConstants.ANY_NAME.getLocalName()) ? NameConstants.ANY_NAME : resolver.getQName(propDef.getName()), + resolver.getQName(propDef.getDeclaringNodeType().getName()), + propDef.isAutoCreated(), propDef.isMandatory(), + propDef.getOnParentVersion(), propDef.isProtected(), + convertValues(propDef.getDefaultValues(), resolver, qValueFactory), + propDef.isMultiple(), propDef.getRequiredType(), + ValueConstraint.create(propDef.getRequiredType(), propDef.getValueConstraints(), resolver), + propDef.getAvailableQueryOperators(), + propDef.isFullTextSearchable(), propDef.isQueryOrderable()); + } + + //------------------------------------------------< QPropertyDefinition >--- + /** + * {@inheritDoc} + */ + public int getRequiredType() { + return requiredType; + } + + /** + * {@inheritDoc} + */ + public QValueConstraint[] getValueConstraints() { + return valueConstraints; + } + + /** + * {@inheritDoc} + */ + public QValue[] getDefaultValues() { + return defaultValues; + } + + /** + * {@inheritDoc} + */ + public boolean isMultiple() { + return multiple; + } + + /** + * {@inheritDoc} + */ + public String[] getAvailableQueryOperators() { + return availableQueryOperators; + } + + /** + * {@inheritDoc} + */ + public boolean isFullTextSearchable() { + return fullTextSearchable; + } + + /** + * {@inheritDoc} + */ + public boolean isQueryOrderable() { + return queryOrderable; + } + + /** + * {@inheritDoc} + * + * @return always false + */ + public boolean definesNode() { + return false; + } + + //-------------------------------------------------------------< Object >--- + /** + * Compares two property definitions for equality. Returns true + * if the given object is a property definition and has the same attributes + * as this property definition. + * + * @param obj the object to compare this property definition with + * @return true if the object is equal to this property definition, + * false otherwise + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof QPropertyDefinition) { + QPropertyDefinition other = (QPropertyDefinition) obj; + + return super.equals(obj) + && requiredType == other.getRequiredType() + && multiple == other.isMultiple() + && fullTextSearchable == other.isFullTextSearchable() + && queryOrderable == other.isQueryOrderable() + && ((valueConstraints == null || other.getValueConstraints() == null) ? (valueConstraints == other.getValueConstraints()) + : new HashSet(Arrays.asList(valueConstraints)).equals(new HashSet(Arrays.asList(other.getValueConstraints())))) + && ((defaultValues == null || other.getDefaultValues() == null) ? (defaultValues == other.getDefaultValues()) + : new HashSet(Arrays.asList(defaultValues)).equals(new HashSet(Arrays.asList(other.getDefaultValues())))) + && new HashSet(Arrays.asList(availableQueryOperators)).equals(new HashSet(Arrays.asList(other.getAvailableQueryOperators()))); + } + return false; + } + + /** + * Overrides {@link QItemDefinitionImpl#hashCode()}. + * + * @return the hash code + */ + @Override + public int hashCode() { + if (hashCode == 0) { + int h = super.hashCode(); + h = 37 * h + requiredType; + h = 37 * h + (multiple ? 11 : 43); + h = 37 * h + (queryOrderable ? 11 : 43); + h = 37 * h + (fullTextSearchable ? 11 : 43); + h = 37 * h + ((valueConstraints != null) ? new HashSet(Arrays.asList(valueConstraints)).hashCode() : 0); + h = 37 * h + ((defaultValues != null) ? new HashSet(Arrays.asList(defaultValues)).hashCode() : 0); + h = 37 * h + new HashSet(Arrays.asList(availableQueryOperators)).hashCode(); + hashCode = h; + } + return hashCode; + } + + //-----------------------------------------------------------< internal >--- + + /** + * Converts JCR {@link Value}s to {@link QValue}s. + * + * @param values the JCR values. + * @param resolver the name/path resolver of the session that provided the + * values. + * @param factory the QValue factory. + * @return the converted values. + * @throws RepositoryException if an error occurs while converting the + * values. + */ + private static QValue[] convertValues(Value[] values, + NamePathResolver resolver, + QValueFactory factory) + throws RepositoryException { + if (values != null) { + QValue[] defaultValues = new QValue[values.length]; + for (int i = 0; i < values.length; i++) { + defaultValues[i] = ValueFormat.getQValue(values[i], resolver, factory); + } + return defaultValues; + } else { + return null; + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SerializableBatch.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SerializableBatch.java new file mode 100644 index 00000000000..57183f293ce --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SerializableBatch.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Tree; + +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.PathNotFoundException; +import javax.jcr.AccessDeniedException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.lock.LockException; +import javax.jcr.version.VersionException; +import java.io.Serializable; +import java.util.List; +import java.util.ArrayList; + +/** + * SerializableBatch implements a serializable SPI Batch, which + * simply records all calls and replays them when asked for. The client of + * this batch must ensure that the passed {@link QValue} instances are + * serializable, otherwise the serializing the Batch will fail! + */ +public class SerializableBatch implements Batch, Serializable { + + private List recording = new ArrayList(); + + private final ItemId itemId; + + /** + * Creates a new SerializableBatch. + * + * @param itemId the id of the item where save was called. To indicate that + * save was called on the session, the id of the root node + * must be passed. + */ + public SerializableBatch(ItemId itemId) { + this.itemId = itemId; + } + + /** + * @return the item id where save was called for this batch. + */ + public ItemId getSaveTarget() { + return itemId; + } + + /** + * Replays this batch on the given batch. For a description of + * the exception see {@link org.apache.jackrabbit.spi.RepositoryService#submit(Batch)}. + * + * @param batch the target batch. + */ + public void replay(Batch batch) throws PathNotFoundException, ItemNotFoundException, NoSuchNodeTypeException, ValueFormatException, VersionException, LockException, ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + for (Operation operation : recording) { + operation.replay(batch); + } + } + + //----------------------------< Batch >------------------------------------- + + public void addNode(NodeId parentId, + Name nodeName, + Name nodetypeName, + String uuid) { + recording.add(new AddNode(parentId, nodeName, nodetypeName, uuid)); + } + + public void addProperty(NodeId parentId, Name propertyName, QValue value) { + recording.add(new AddProperty(parentId, propertyName, + new QValue[]{value}, false)); + } + + public void addProperty(NodeId parentId, + Name propertyName, + QValue[] values) { + recording.add(new AddProperty(parentId, propertyName, values, true)); + } + + public void setValue(PropertyId propertyId, QValue value) { + recording.add(new SetValue(propertyId, new QValue[]{value}, false)); + } + + public void setValue(PropertyId propertyId, QValue[] values) { + recording.add(new SetValue(propertyId, values, true)); + } + + public void remove(ItemId itemId) { + recording.add(new Remove(itemId)); + } + + public void reorderNodes(NodeId parentId, + NodeId srcNodeId, + NodeId beforeNodeId) { + recording.add(new ReorderNodes(parentId, srcNodeId, beforeNodeId)); + } + + public void setMixins(NodeId nodeId, Name[] mixinNodeTypeIds) { + recording.add(new SetMixins(nodeId, mixinNodeTypeIds)); + } + + public void setPrimaryType(NodeId nodeId, Name primaryNodeTypeName) throws RepositoryException { + recording.add(new SetPrimaryType(nodeId, primaryNodeTypeName)); + } + + public void move(NodeId srcNodeId, + NodeId destParentNodeId, + Name destName) { + recording.add(new Move(srcNodeId, destParentNodeId, destName)); + } + + public void setTree(NodeId parentId, Tree contentTree) + throws RepositoryException { + recording.add(new SetTree(parentId, contentTree)); + } + //----------------------------< internal >---------------------------------- + + public interface Operation extends Serializable { + + /** + * Replays this operation on the given batch. + * + * @param batch the batch. + * @throws RepositoryException if an error occurs replaying the + * operation. + */ + public void replay(Batch batch) throws RepositoryException; + } + + private static class AddNode implements Operation { + + private final NodeId parentId; + + private final Name nodeName; + + private final Name nodetypeName; + + private final String uuid; + + AddNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) { + this.parentId = parentId; + this.nodeName = nodeName; + this.nodetypeName = nodetypeName; + this.uuid = uuid; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + batch.addNode(parentId, nodeName, nodetypeName, uuid); + } + } + + private static class SetTree implements Operation { + + private final NodeId parentId; + + private final Tree contentTree; + + SetTree(NodeId parentId, Tree contentTree) { + this.parentId = parentId; + this.contentTree = contentTree; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + batch.setTree(parentId, contentTree); + } + } + + private static class AddProperty implements Operation { + + private final NodeId parentId; + + private final Name propertyName; + + private final QValue[] values; + + private final boolean isMultiValued; + + AddProperty(NodeId parentId, Name propertyName, + QValue[] values, boolean isMultiValued) { + this.parentId = parentId; + this.propertyName = propertyName; + this.values = values; + this.isMultiValued = isMultiValued; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + if (isMultiValued) { + batch.addProperty(parentId, propertyName, values); + } else { + batch.addProperty(parentId, propertyName, values[0]); + } + } + } + + private static class SetValue implements Operation { + + private final PropertyId propertyId; + + private final QValue[] values; + + private final boolean isMultiValued; + + SetValue(PropertyId propertyId, QValue[] values, boolean isMultiValued) { + this.propertyId = propertyId; + this.values = values; + this.isMultiValued = isMultiValued; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + if (isMultiValued) { + batch.setValue(propertyId, values); + } else { + batch.setValue(propertyId, values[0]); + } + } + } + + private static class Remove implements Operation { + + private final ItemId itemId; + + Remove(ItemId itemId) { + this.itemId = itemId; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + batch.remove(itemId); + } + } + + private static class ReorderNodes implements Operation { + + private final NodeId parentId; + + private final NodeId srcNodeId; + + private final NodeId beforeNodeId; + + ReorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) { + this.parentId = parentId; + this.srcNodeId = srcNodeId; + this.beforeNodeId = beforeNodeId; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + batch.reorderNodes(parentId, srcNodeId, beforeNodeId); + } + } + + private static class SetMixins implements Operation { + + private final NodeId nodeId; + + private final Name[] mixinNodeTypeNames; + + SetMixins(NodeId nodeId, Name[] mixinNodeTypeNames) { + this.nodeId = nodeId; + this.mixinNodeTypeNames = mixinNodeTypeNames; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + batch.setMixins(nodeId, mixinNodeTypeNames); + } + } + + private static class SetPrimaryType implements Operation { + + private final NodeId nodeId; + + private final Name primaryNodeTypeName; + + SetPrimaryType(NodeId nodeId, Name primaryNodeTypeName) { + this.nodeId = nodeId; + this.primaryNodeTypeName = primaryNodeTypeName; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + batch.setPrimaryType(nodeId, primaryNodeTypeName); + } + } + + private static class Move implements Operation { + + private final NodeId srcNodeId; + + private final NodeId destParentNodeId; + + private final Name destName; + + Move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) { + this.srcNodeId = srcNodeId; + this.destParentNodeId = destParentNodeId; + this.destName = destName; + } + + /** + * {@inheritDoc} + */ + public void replay(Batch batch) throws RepositoryException { + batch.move(srcNodeId, destParentNodeId, destName); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SessionExtensions.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SessionExtensions.java new file mode 100644 index 00000000000..b7790a2dda3 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SessionExtensions.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.spi.commons; + +import javax.jcr.Session; + +/** + * Provides additional methods for {@link Session} access.. + */ +public interface SessionExtensions { + + /** + * Sets the specified {@link Session} attribute. + * @param name attribute name + * @param value attribute value + */ + public void setAttribute(String name, Object value); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SessionInfoImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SessionInfoImpl.java new file mode 100644 index 00000000000..f4e9f0d3e97 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/SessionInfoImpl.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.spi.commons; + +import org.apache.jackrabbit.spi.SessionInfo; + +import javax.jcr.RepositoryException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * SessionInfoImpl is a serializable bean based implementation of + * SessionInfo. + */ +public class SessionInfoImpl implements SessionInfo, Serializable { + + /** + * The userId or null if unknown. + */ + private String userId; + + /** + * The user data or null. + */ + private String userData; + + /** + * The name of the workspace to connect to or null if this + * session info refers to the default workspace. + */ + private String workspaceName; + + /** + * The list of lock tokens. + */ + private List lockTokens = new ArrayList(); + + /** + * Default constructor + */ + public SessionInfoImpl() { + } + + /** + * Sets the userId. + * + * @param userId the userId or null if unknown. + */ + public void setUserID(String userId) { + this.userId = userId; + } + + /** + * Sets the name of the workspace to connect to. + * + * @param workspaceName the name of the workspace or null if + * this session info refers to the default workspace. + */ + public void setWorkspacename(String workspaceName) { + this.workspaceName = workspaceName; + } + + //-------------------------< SessionInfo >---------------------------------- + + /** + * {@inheritDoc} + */ + public String getUserID() { + return userId; + } + + /** + * {@inheritDoc} + */ + public String getWorkspaceName() { + return workspaceName; + } + + /** + * {@inheritDoc} + */ + public String[] getLockTokens() { + return lockTokens.toArray(new String[lockTokens.size()]); + } + + /** + * {@inheritDoc} + */ + public void addLockToken(String s) { + lockTokens.add(s); + } + + /** + * {@inheritDoc} + */ + public void removeLockToken(String s) { + lockTokens.remove(s); + } + + /** + * {@inheritDoc} + */ + public void setUserData(String userData) throws RepositoryException { + this.userData = userData; + } + + /** + * Return the user data set via {@link #setUserData(String)} + * + * @return userData + */ + public String getUserData() { + return userData; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/AbstractChangeLog.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/AbstractChangeLog.java new file mode 100644 index 00000000000..d7e085791c3 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/AbstractChangeLog.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.spi.commons.batch; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Batch; + +/** + * This base class for {@link ChangeLog} implementations maintains a list of operations + * of type type T. + * @param + */ +public abstract class AbstractChangeLog implements ChangeLog { + + /** + * {@link Operation}s kept in this change log. + */ + protected final List operations = new LinkedList(); + + /** + * Added an operation to the list of {@link #operations}. + * @param op {@link Operation} to add + * @throws RepositoryException + */ + public void addOperation(T op) throws RepositoryException { + operations.add(op); + } + + /** + * This implementation applies each of the operation maintained by + * this change log to the passed batch. + * {@inheritDoc} + */ + public Batch apply(Batch batch) throws RepositoryException { + if (batch == null) { + throw new IllegalArgumentException("Batch must not be null"); + } + for (Iterator it = operations.iterator(); it.hasNext(); ) { + Operation op = it.next(); + op.apply(batch); + } + return batch; + } + + @Override + public String toString() { + StringBuffer b = new StringBuffer(); + for (Iterator it = operations.iterator(); it.hasNext(); ) { + b.append(it.next()); + if (it.hasNext()) { + b.append(", "); + } + } + return b.toString(); + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof AbstractChangeLog) { + return equals((AbstractChangeLog) other); + } + return false; + } + + public boolean equals(AbstractChangeLog other) { + return operations.equals(other.operations); + } + + @Override + public int hashCode() { + throw new IllegalArgumentException("Not hashable"); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ChangeLog.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ChangeLog.java new file mode 100644 index 00000000000..0c56dcda863 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ChangeLog.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.spi.commons.batch; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Batch; + + +/** + * A ChangeLog is a specialized {@link Batch} which + * keeps a list of {@link Operation}s. The {@link #apply(Batch)} method + * applies these operations to another batch. + */ +public interface ChangeLog extends Batch { + + /** + * Applies the {@link Operation}s contained in this change log to + * the passed batch. + * @param batch + * @return The batch passed in as argument with the + * operations from this change log applied. + * @throws RepositoryException + */ + public Batch apply(Batch batch) throws RepositoryException; +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ChangeLogImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ChangeLogImpl.java new file mode 100644 index 00000000000..b3362caf0b6 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ChangeLogImpl.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.spi.commons.batch; + + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.Tree; + +/** + * This {@link ChangeLog} implementation simply keeps back all calls to its {@link Batch} methods as + * a list of {@link #operations} (with item of type {@link Operation}). When {@link #apply(Batch) + * applied} to a batch, all operations in the list are {@link Operation#apply(Batch) applied} to that + * batch. + */ +public class ChangeLogImpl extends AbstractChangeLog { + + public void addNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) + throws RepositoryException { + + addOperation(Operations.addNode(parentId, nodeName, nodetypeName, uuid)); + } + + public void addProperty(NodeId parentId, Name propertyName, QValue value) throws RepositoryException { + addOperation(Operations.addProperty(parentId, propertyName, value)); + } + + public void addProperty(NodeId parentId, Name propertyName, QValue[] values) + throws RepositoryException { + + addOperation(Operations.addProperty(parentId, propertyName, values)); + } + + public void move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + addOperation(Operations.move(srcNodeId, destParentNodeId, destName)); + } + + public void remove(ItemId itemId) throws RepositoryException { + addOperation(Operations.remove(itemId)); + } + + public void reorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) + throws RepositoryException { + + addOperation(Operations.reorderNodes(parentId, srcNodeId, beforeNodeId)); + } + + public void setMixins(NodeId nodeId, Name[] mixinNodeTypeNames) throws RepositoryException { + addOperation(Operations.setMixins(nodeId, mixinNodeTypeNames)); + } + + public void setPrimaryType(NodeId nodeId, Name primaryNodeTypeName) throws RepositoryException { + addOperation(Operations.setPrimaryType(nodeId, primaryNodeTypeName)); + } + + public void setValue(PropertyId propertyId, QValue value) throws RepositoryException { + addOperation(Operations.setValue(propertyId, value)); + } + + public void setValue(PropertyId propertyId, QValue[] values) throws RepositoryException { + addOperation(Operations.setValue(propertyId, values)); + } + + @Override + public void setTree(NodeId parentId, Tree contentTree) throws RepositoryException { + addOperation(Operations.setTree(parentId, contentTree)); + } +} + diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ConsolidatingChangeLog.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ConsolidatingChangeLog.java new file mode 100644 index 00000000000..42c2702d935 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/ConsolidatingChangeLog.java @@ -0,0 +1,824 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.batch; + +import java.util.Iterator; +import java.util.ListIterator; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; + +/** + * A {@link ChangeLog} implementation which does basic consolidation on its + * {@link org.apache.jackrabbit.spi.commons.batch.Operation Operation}s. That is, cancelling + * operations are removed if possible. In general this is not possible across + * {@link org.apache.jackrabbit.spi.commons.batch.Operations.Move move} operations. The individual + * {@link CancelableOperation CancelableOperation} implementations document their behavior + * concerning cancellation. + */ +public class ConsolidatingChangeLog extends AbstractChangeLog { + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + /** + * Create a new instance of a consolidating change log. + */ + public ConsolidatingChangeLog() { + super(); + } + + /** + * Create a {@link Path} from the {@link NodeId} of a parent and the {@link Name} of a + * child. + * @param parentId node id of the parent + * @param name name of the child + * @return the path of the item name or null if parentId's + * path is not absolute + * @throws RepositoryException + */ + protected static Path getPath(NodeId parentId, Name name) throws RepositoryException { + Path parent = parentId.getPath(); + if (!parent.isAbsolute()) { + return null; + } + + return PATH_FACTORY.create(parent, name, true); + } + + /** + * Determine the {@link Path} from an {@link ItemId}. + * @param itemId + * @return path of the item itemId or null if itemId's + * path is not absolute + */ + protected static Path getPath(ItemId itemId) { + Path path = itemId.getPath(); + if (path != null && !path.isAbsolute()) { + return null; + } + return path; + } + + // -----------------------------------------------------< ChangeLog >--- + + public void addNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) + throws RepositoryException { + + addOperation(CancelableOperations.addNode(parentId, nodeName, nodetypeName, uuid)); + } + + public void addProperty(NodeId parentId, Name propertyName, QValue value) throws RepositoryException { + addOperation(CancelableOperations.addProperty(parentId, propertyName, value)); + } + + public void addProperty(NodeId parentId, Name propertyName, QValue[] values) throws RepositoryException { + addOperation(CancelableOperations.addProperty(parentId, propertyName, values)); + } + + public void move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + addOperation(CancelableOperations.move(srcNodeId, destParentNodeId, destName)); + } + + public void remove(ItemId itemId) throws RepositoryException { + addOperation(CancelableOperations.remove(itemId)); + } + + public void reorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) throws RepositoryException { + addOperation(CancelableOperations.reorderNodes(parentId, srcNodeId, beforeNodeId)); + } + + public void setMixins(NodeId nodeId, Name[] mixinNodeTypeNames) throws RepositoryException { + addOperation(CancelableOperations.setMixins(nodeId, mixinNodeTypeNames)); + } + + public void setPrimaryType(NodeId nodeId, Name primaryNodeTypeName) throws RepositoryException { + addOperation(CancelableOperations.setPrimaryType(nodeId, primaryNodeTypeName)); + } + + public void setValue(PropertyId propertyId, QValue value) throws RepositoryException { + addOperation(CancelableOperations.setValue(propertyId, value)); + } + + public void setValue(PropertyId propertyId, QValue[] values) throws RepositoryException { + addOperation(CancelableOperations.setValue(propertyId, values)); + } + + @Override + public void setTree(NodeId parentId, Tree contentTree) throws RepositoryException { + addOperation(CancelableOperations.setTree(parentId, contentTree)); + } + + /** + * Determines the cancellation behavior from the list of {@link ChangeLogImpl#operations operations} + * and the current operation op: + *
          + *
        • When the current operation is cancelled by the last operation, the list of operations + * is not modified.
        • + *
        • When the current operation and the last operation cancel each other, the last operation is + * removed from the list of operations.
        • + *
        • When the last operation is cancelled by this operation, the last operation is removed from + * the list of operations and determination of cancellation starts from scratch.
        • + *
        • Otherwise add the current operation to the list of operations.
        • + *
        + */ + @Override + public void addOperation(CancelableOperation op) throws RepositoryException { + CancelableOperation otherOp = op; + for (OperationsBackwardWithSentinel it = new OperationsBackwardWithSentinel(); it.hasNext(); ) { + CancelableOperation thisOp = it.next(); + switch (thisOp.cancel(otherOp)) { + case CancelableOperation.CANCEL_THIS: + it.remove(); + continue; + case CancelableOperation.CANCEL_OTHER: + return; + case CancelableOperation.CANCEL_BOTH: + it.remove(); + return; + case CancelableOperation.CANCEL_NONE: + super.addOperation(otherOp); + return; + default: + assert false : "Invalid case in switch"; + } + } + } + + // -----------------------------------------------------< private >--- + + private class OperationsBackwardWithSentinel implements Iterator { + private final ListIterator it = operations.listIterator(operations.size()); + private boolean last = !it.hasPrevious(); + private boolean done; + + public boolean hasNext() { + return it.hasPrevious() || last; + } + + public CancelableOperation next() { + if (last) { + done = true; + return CancelableOperations.empty(); + } + else { + CancelableOperation o = it.previous(); + last = !it.hasPrevious(); + return o; + } + } + + public void remove() { + if (done) { + throw new IllegalStateException("Cannot remove last element"); + } + else { + it.remove(); + } + } + } + + // -----------------------------------------------------< CancelableOperations >--- + + /** + * This class represent an {@link Operation} which can be cancelled by another operation + * or which cancels another operation. + */ + protected interface CancelableOperation extends Operation { + + /** + * The other operation cancels this operations + */ + public static final int CANCEL_THIS = 0; + + /** + * This operation cancels the other operation + */ + public static final int CANCEL_OTHER = 1; + + /** + * This operation and the other operation cancel each other mutually + */ + public static final int CANCEL_BOTH = 2; + + /** + * No cancellation + */ + public static final int CANCEL_NONE = 3; + + /** + * Determines the cancellation behavior of the other operation + * on this operation. + * @param other + * @return Either {@link #CANCEL_THIS}, {@link #CANCEL_OTHER}, {@link #CANCEL_OTHER} + * or {@link #CANCEL_NONE} + * @throws RepositoryException + */ + public int cancel(CancelableOperation other) throws RepositoryException; + } + + /** + * Factory for creating {@link ConsolidatingChangeLog.CancelableOperation CancelableOperation}s. + * The inner classes of this class all implement the CancelableOperation interface. + * + * @see Operation + */ + protected static final class CancelableOperations { + private CancelableOperations() { + super(); + } + + // -----------------------------------------------------< Empty >--- + + /** + * An Empty operation never cancels another operation and is never + * cancelled by any other operation. + */ + public static class Empty extends Operations.Empty implements CancelableOperation { + + /** + * @return {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE} + */ + public int cancel(CancelableOperation other) throws RepositoryException { + return CANCEL_NONE; + } + } + + /** + * Factory method for creating an {@link Empty Empty} operation. + * @return + */ + public static CancelableOperation empty() { + return new Empty(); + } + + // -----------------------------------------------------< AddNode >--- + + /** + * An AddNode operation is is cancelled by a + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} operation higher up the tree. + * The remove operation is also cancelled if it is targeted at the same node than this add + * operation. + */ + public static class AddNode extends Operations.AddNode implements CancelableOperation { + + public AddNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) { + super(parentId, nodeName, nodetypeName, uuid); + } + + /** + * @return + *
          + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_BOTH CANCEL_BOTH} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and has this node + * as target.
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_THIS CANCEL_THIS} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and has an node higher up + * the hierarchy as target.
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE CANCEL_NONE} otherwise.
        • + *
        + */ + public int cancel(CancelableOperation other) throws RepositoryException { + if (other instanceof Remove) { + Path thisPath = ConsolidatingChangeLog.getPath(parentId, nodeName); + Path otherPath = ConsolidatingChangeLog.getPath(((Remove) other).itemId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + if (thisPath.equals(otherPath)) { + return CANCEL_BOTH; + } + return (thisPath.isDescendantOf(otherPath)) + ? CANCEL_THIS + : CANCEL_NONE; + } + return CANCEL_NONE; + } + } + + /** + * Factory method for creating an {@link AddNode AddNode} operation. + * @see Batch#addNode(NodeId, Name, Name, String) + * + * @param parentId + * @param nodeName + * @param nodetypeName + * @param uuid + * @return + */ + public static CancelableOperation addNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) { + return new AddNode(parentId, nodeName, nodetypeName, uuid); + } + + // ---------------------------------------------------< AddProperty >--- + /** + * AddProperty operations might cancel with + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and + * {@link ConsolidatingChangeLog.CancelableOperations.SetValue SetValue} operations. + */ + public static class AddProperty extends Operations.AddProperty implements CancelableOperation { + + public AddProperty(NodeId parentId, Name propertyName, QValue value) { + super(parentId, propertyName, value); + } + + public AddProperty(NodeId parentId, Name propertyName, QValue[] values) { + super(parentId, propertyName, values); + } + + /** + * @return + *
          + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_BOTH CANCEL_BOTH} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and has this property as + * target or if other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.SetValue SetValue} for a value of + * null and has this property as target.
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_THIS CANCEL_THIS} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and has a node higher up + * the hierarchy as target.
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_OTHER CANCEL_OTHER} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.SetValue SetValue} and has this + * property as target.
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE CANCEL_NONE} otherwise.
        • + *
        + */ + public int cancel(CancelableOperation other) throws RepositoryException { + if (other instanceof Remove) { + Path thisPath = ConsolidatingChangeLog.getPath(parentId, propertyName); + Path otherPath = ConsolidatingChangeLog.getPath(((Remove) other).itemId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + if (thisPath.equals(otherPath)) { + return CANCEL_BOTH; + } + return (thisPath.isDescendantOf(otherPath)) + ? CANCEL_THIS + : CANCEL_NONE; + } + if (other instanceof SetValue) { + SetValue setValue = (SetValue) other; + Path thisPath = ConsolidatingChangeLog.getPath(parentId, propertyName); + Path otherPath = ConsolidatingChangeLog.getPath(setValue.propertyId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + if (thisPath.equals(otherPath)) { + if (!isMultivalued && setValue.values[0] == null) { + return CANCEL_BOTH; + } + else if (values.length == setValue.values.length) { + for (int k = 0; k < values.length; k++) { + if (!values[k].equals(setValue.values[k])) { + return CANCEL_NONE; + } + } + return CANCEL_OTHER; + } + } + } + return CANCEL_NONE; + } + } + + /** + * Factory method for creating an {@link AddProperty AddProperty} operation. + * + * @see Batch#addProperty(NodeId, Name, QValue) + * @param parentId + * @param propertyName + * @param value + * @return + */ + public static CancelableOperation addProperty(NodeId parentId, Name propertyName, QValue value) { + return new AddProperty(parentId, propertyName, value); + } + + /** + * Factory method for creating an {@link AddProperty AddProperty} operation. + * + * @see Batch#addProperty(NodeId, Name, QValue[]) + * @param parentId + * @param propertyName + * @param values + * @return + */ + public static CancelableOperation addProperty(NodeId parentId, Name propertyName, QValue[] values) { + return new AddProperty(parentId, propertyName, values); + } + + // ----------------------------------------------------------< Move >--- + /** + * An Move operation never cancels another operation and is never + * cancelled by any other operation. + */ + public static class Move extends Operations.Move implements CancelableOperation { + + public Move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) { + super(srcNodeId, destParentNodeId, destName); + } + + /** + * @return {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE CANCEL_NONE} + */ + public int cancel(CancelableOperation other) { + return CANCEL_NONE; + } + } + + /** + * Factory method for creating a {@link Move Move} operation. + * + * @see Batch#move(NodeId, NodeId, Name) + * @param srcNodeId + * @param destParentNodeId + * @param destName + * @return + */ + public static CancelableOperation move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) { + return new Move(srcNodeId, destParentNodeId, destName); + } + + // --------------------------------------------------------< Remove >--- + /** + * An Remove operation never cancels another operation and is never + * cancelled by any other operation. + */ + public static class Remove extends Operations.Remove implements CancelableOperation { + + public Remove(ItemId itemId) { + super(itemId); + } + + /** + * @return {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE CANCEL_NONE} + */ + public int cancel(CancelableOperation other) { + return CANCEL_NONE; + } + } + + /** + * Factory method for creating a {@link Remove Remove} operation. + * + * @see Batch#move(NodeId, NodeId, Name) + * @param itemId + * @return + */ + public static CancelableOperation remove(ItemId itemId) { + return new Remove(itemId); + } + + // -------------------------------------------------< Reorder Nodes >--- + /** + * A ReorderNodes operation might cancel with + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and + * {@link ConsolidatingChangeLog.CancelableOperations.ReorderNodes ReorderNodes} operations. + */ + public static class ReorderNodes extends Operations.ReorderNodes implements CancelableOperation { + + public ReorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) { + super(parentId, srcNodeId, beforeNodeId); + } + + /** + * @return + *
          + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_THIS CANCEL_THIS} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and has an node higher up + * the hierarchy or this node as target. Or if other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.ReorderNodes ReorderNodes} which + * has this node as target and neither srcNodeId nor beforeNodeId + * has same name siblings.
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE CANCEL_NONE} otherwise.
        • + *
        + */ + public int cancel(CancelableOperation other) throws RepositoryException { + if (other instanceof Remove) { + Path thisPath = ConsolidatingChangeLog.getPath(srcNodeId); + Path otherPath = ConsolidatingChangeLog.getPath(((Remove) other).itemId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + return thisPath.isDescendantOf(otherPath) || thisPath.equals(otherPath) + ? CANCEL_THIS + : CANCEL_NONE; + } + if (other instanceof ReorderNodes) { + Path thisPath = ConsolidatingChangeLog.getPath(parentId); + Path otherPath = ConsolidatingChangeLog.getPath(((ReorderNodes) other).parentId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + return thisPath.equals(otherPath) && !hasSNS(srcNodeId) && !hasSNS(beforeNodeId) + ? CANCEL_THIS + : CANCEL_NONE; + } + return CANCEL_NONE; + } + + private boolean hasSNS(NodeId nodeId) { + if (nodeId != null) { + Path path = ConsolidatingChangeLog.getPath(nodeId); + return path != null && path.getIndex() > 1; + } + + return false; + } + } + + /** + * Factory method for creating a {@link ReorderNodes ReorderNodes} operation. + * + * @see Batch#reorderNodes(NodeId, NodeId, NodeId) + * @param parentId + * @param srcNodeId + * @param beforeNodeId + * @return + */ + public static CancelableOperation reorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) { + return new ReorderNodes(parentId, srcNodeId, beforeNodeId); + } + + // -----------------------------------------------------< SetMixins >--- + /** + * A SetMixins operation might cancel with + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and + * {@link ConsolidatingChangeLog.CancelableOperations.SetMixins SetMixins} operations. + */ + public static class SetMixins extends Operations.SetMixins implements CancelableOperation { + + public SetMixins(NodeId nodeId, Name[] mixinNodeTypeNames) { + super(nodeId, mixinNodeTypeNames); + } + + /** + * @return + *
          + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_THIS CANCEL_THIS} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and has an node higher up + * the hierarchy or this node as target. Or if other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.SetMixins SetMixins} which has this node + * as target and has the same mixinNodeTypeNames.
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE CANCEL_NONE} otherwise.
        • + *
        + */ + public int cancel(CancelableOperation other) throws RepositoryException { + if (other instanceof Remove) { + Path thisPath = ConsolidatingChangeLog.getPath(nodeId); + Path otherPath = ConsolidatingChangeLog.getPath(((Remove) other).itemId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + return thisPath.isDescendantOf(otherPath) || thisPath.equals(otherPath) + ? CANCEL_THIS + : CANCEL_NONE; + } + if (other instanceof SetMixins) { + SetMixins setMixin = (SetMixins) other; + if (mixinNodeTypeNames.length == setMixin.mixinNodeTypeNames.length) { + Path thisPath = ConsolidatingChangeLog.getPath(nodeId); + Path otherPath = ConsolidatingChangeLog.getPath(setMixin.nodeId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + if (thisPath.equals(otherPath)) { + for (int k = 0; k < mixinNodeTypeNames.length; k++) { + if (!mixinNodeTypeNames[k].equals(setMixin.mixinNodeTypeNames[k])) { + return CANCEL_NONE; + } + } + return CANCEL_THIS; + } + } + } + return CANCEL_NONE; + } + } + + /** + * Factory method for creating a {@link SetMixins} operation. + * + * @see Batch#setMixins(NodeId, Name[]) + * @param nodeId + * @param mixinNodeTypeNames + * @return + */ + public static CancelableOperation setMixins(NodeId nodeId, Name[] mixinNodeTypeNames) { + return new SetMixins(nodeId, mixinNodeTypeNames); + } + + // -----------------------------------------------------< SetMixins >--- + /** + * A SetPrimaryType operation might cancel with + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and + * {@link ConsolidatingChangeLog.CancelableOperations.SetPrimaryType SetPrimaryType} operations. + */ + public static class SetPrimaryType extends Operations.SetPrimaryType implements CancelableOperation { + + public SetPrimaryType(NodeId nodeId, Name primaryTypeName) { + super(nodeId, primaryTypeName); + } + + /** + * @return + *
          + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_THIS CANCEL_THIS} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and has an node higher up + * the hierarchy or this node as target. Or if other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.SetMixins SetMixins} which has this node + * as target and has the same mixinNodeTypeNames.
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE CANCEL_NONE} otherwise.
        • + *
        + */ + public int cancel(CancelableOperation other) throws RepositoryException { + if (other instanceof Remove) { + Path thisPath = ConsolidatingChangeLog.getPath(nodeId); + Path otherPath = ConsolidatingChangeLog.getPath(((Remove) other).itemId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + return thisPath.isDescendantOf(otherPath) || thisPath.equals(otherPath) + ? CANCEL_THIS + : CANCEL_NONE; + } + if (other instanceof SetPrimaryType) { + SetPrimaryType setPrimaryType = (SetPrimaryType) other; + if (primaryTypeName.equals(setPrimaryType.primaryTypeName)) { + Path thisPath = ConsolidatingChangeLog.getPath(nodeId); + Path otherPath = ConsolidatingChangeLog.getPath(setPrimaryType.nodeId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + if (thisPath.equals(otherPath)) { + return CANCEL_THIS; + } + } + } + return CANCEL_NONE; + } + } + + /** + * Factory method for creating a {@link SetPrimaryType} operation. + * + * @see Batch#setPrimaryType(NodeId, Name) + * @param nodeId + * @param primaryTypeName + * @return + */ + public static CancelableOperation setPrimaryType(NodeId nodeId, Name primaryTypeName) { + return new SetPrimaryType(nodeId, primaryTypeName); + } + + // ------------------------------------------------------< SetValue >--- + /** + * A SetValue operation might cancel with + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and + * {@link ConsolidatingChangeLog.CancelableOperations.SetValue SetValue} operations. + */ + public static class SetValue extends Operations.SetValue implements CancelableOperation { + public SetValue(PropertyId propertyId, QValue value) { + super(propertyId, value); + } + + public SetValue(PropertyId propertyId, QValue[] values) { + super(propertyId, values); + } + + /** + * @return + *
          + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_THIS CANCEL_THIS} if + * other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.Remove Remove} and has an node higher up + * the hierarchy or this node as target. Or if other is an instance of + * {@link ConsolidatingChangeLog.CancelableOperations.SetValue SetValue} which has this + * property as target
        • + *
        • {@link ConsolidatingChangeLog.CancelableOperation#CANCEL_NONE CANCEL_NONE} otherwise.
        • + *
        + */ + public int cancel(CancelableOperation other) throws RepositoryException { + if (other instanceof Remove) { + Path thisPath = ConsolidatingChangeLog.getPath(propertyId); + Path otherPath = ConsolidatingChangeLog.getPath(((Remove) other).itemId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + return thisPath.isDescendantOf(otherPath) || thisPath.equals(otherPath) + ? CANCEL_THIS + : CANCEL_NONE; + } + if (other instanceof SetValue) { + Path thisPath = ConsolidatingChangeLog.getPath(propertyId); + Path otherPath = ConsolidatingChangeLog.getPath(((SetValue) other).propertyId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + if (thisPath.equals(otherPath)) { + return CANCEL_THIS; + } + } + return CANCEL_NONE; + } + } + + /** + * Factory method for creating a {@link SetValue SetValue} operation. + * + * @see Batch#setValue(PropertyId, QValue) + * @param propertyId + * @param value + * @return + */ + public static CancelableOperation setValue(PropertyId propertyId, QValue value) { + return new SetValue(propertyId, value); + } + + /** + * Factory method for creating a {@link SetValue SetValue} operation. + * + * @see Batch#setValue(PropertyId, QValue[]) + * @param propertyId + * @param values + * @return + */ + public static CancelableOperation setValue(PropertyId propertyId, QValue[] values) { + return new SetValue(propertyId, values); + } + + + //--------------------------------------------------------< SetTree >--- + public static class SetTree extends Operations.SetTree implements CancelableOperation { + + public SetTree(NodeId parentId, Tree contentTree) { + super(parentId, contentTree); + } + + /** + * The cancellation only considers canceling the parent node, which corresponds + * to the policy node. + */ + public int cancel(CancelableOperation other) throws RepositoryException { + if (other instanceof Remove) { + Path thisPath = ConsolidatingChangeLog.getPath(parentId, tree.getName()); + Path otherPath = ConsolidatingChangeLog.getPath(((Remove) other).itemId); + if (thisPath == null || otherPath == null) { + return CANCEL_NONE; + } + if (thisPath.equals(otherPath)) { + return CANCEL_BOTH; + } + return (thisPath.isDescendantOf(otherPath)) + ? CANCEL_THIS + : CANCEL_NONE; + } + return CANCEL_NONE; + } + } + + /** + * Factory method for creating an {@link SetTree} operation. + * @see Batch#setTree(NodeId, Tree) + * + * @param parentId + * @param tree + * @return + */ + public static CancelableOperation setTree(NodeId parentId, Tree tree) { + return new SetTree(parentId, tree); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/Operation.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/Operation.java new file mode 100644 index 00000000000..ebf85d7b9b1 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/Operation.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.spi.commons.batch; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Batch; + +/** + * An Operation represents a method call on a {@link Batch}. + */ +public interface Operation { + + /** + * Apply this operation to the given {@link Batch} + * @param batch + * @throws RepositoryException + */ + public void apply(Batch batch) throws RepositoryException; +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/Operations.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/Operations.java new file mode 100644 index 00000000000..96a01f20a4e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/Operations.java @@ -0,0 +1,846 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.batch; + +import java.util.Arrays; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.Tree; + +/** + * Factory for creating {@link Operation}s. The inner classes of this class + * all implement the Operation interface. They are representatives + * for the method calls on a {@link Batch}. In addition {@link Empty} represents + * the empty operation which does nothing. + */ +public final class Operations { + private Operations() { + super(); + } + + // -------------------------------------------------------------< Empty >--- + /** + * Representative of the empty {@link Operation} which does nothing when + * applied to a {@link Batch}. + */ + public static class Empty implements Operation { + private static final Empty INSTANCE = new Empty(); + + protected Empty() { + super(); + } + + /** + * This method has no effect. + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { /* nothing to do */ } + + @Override + public String toString() { + return "Empty[]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + return other instanceof Empty; + } + + @Override + public int hashCode() { + return Empty.class.hashCode(); + } + } + + /** + * Factory method for creating an {@link Empty} operation. + * @return + */ + public static Operation empty() { + return Empty.INSTANCE; + } + + // -----------------------------------------------------------< AddNode >--- + /** + * Representative of an add-node {@link Operation} which calls + * {@link Batch#addNode(NodeId, Name, Name, String)} when applied to a {@link Batch}. + */ + public static class AddNode implements Operation { + protected final NodeId parentId; + protected final Name nodeName; + protected final Name nodetypeName; + protected final String uuid; + + /** + * Create a new add-node {@link Operation} for the given arguments. + * @see Batch#addNode(NodeId, Name, Name, String) + * + * @param parentId + * @param nodeName + * @param nodetypeName + * @param uuid + */ + public AddNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) { + super(); + this.parentId = parentId; + this.nodeName = nodeName; + this.nodetypeName = nodetypeName; + this.uuid = uuid; + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + batch.addNode(parentId, nodeName, nodetypeName, uuid); + } + + @Override + public String toString() { + return "AddNode[" + parentId + ", " + nodeName + ", " + nodetypeName + ", " + uuid + "]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof AddNode) { + return equals((AddNode) other); + } + return false; + } + + public boolean equals(AddNode other) { + return Operations.equals(parentId, other.parentId) + && Operations.equals(nodeName, other.nodeName) + && Operations.equals(nodetypeName, other.nodetypeName) + && Operations.equals(uuid, other.uuid); + } + + @Override + public int hashCode() { + return 41 * ( + 41 * ( + 41 * ( + 41 + Operations.hashCode(parentId)) + + Operations.hashCode(nodeName)) + + Operations.hashCode(nodetypeName)) + + Operations.hashCode(uuid); + } + + } + + /** + * Factory method for creating an {@link AddNode} operation. + * @see Batch#addNode(NodeId, Name, Name, String) + * + * @param parentId + * @param nodeName + * @param nodetypeName + * @param uuid + * @return + */ + public static Operation addNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) { + return new AddNode(parentId, nodeName, nodetypeName, uuid); + } + + // -------------------------------------------------------< AddProperty >--- + /** + * Representative of an add-property {@link Operation} which calls + * {@link Batch#addProperty(NodeId, Name, QValue)} or {@link Batch#addProperty(NodeId, Name, QValue[])} + * depending on whether the property is multi valued or not when applied to a {@link Batch}. + */ + public static class AddProperty implements Operation { + protected final NodeId parentId; + protected final Name propertyName; + protected final QValue[] values; + protected final boolean isMultivalued; + + private AddProperty(NodeId parentId, Name propertyName, QValue[] values, boolean isMultivalued) { + super(); + this.parentId = parentId; + this.propertyName = propertyName; + this.values = values; + this.isMultivalued = isMultivalued; + } + + /** + * Create a new add-property {@link Operation} for the given arguments. + * @see Batch#addProperty(NodeId, Name, QValue) + * + * @param parentId + * @param propertyName + * @param value + */ + public AddProperty(NodeId parentId, Name propertyName, QValue value) { + this(parentId, propertyName, new QValue[] { value }, false); + } + + /** + * Create a new add-property {@link Operation} for the given arguments. + * @see Batch#addProperty(NodeId, Name, QValue[]) + * + * @param parentId + * @param propertyName + * @param values + */ + public AddProperty(NodeId parentId, Name propertyName, QValue[] values) { + this(parentId, propertyName, values, true); + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + if (isMultivalued) { + batch.addProperty(parentId, propertyName, values); + } + else { + batch.addProperty(parentId, propertyName, values[0]); + } + } + + @Override + public String toString() { + return "AddProperty[" + parentId + ", " + propertyName + ", " + Arrays.toString(values) + "]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof AddProperty) { + return equals((AddProperty) other); + } + return false; + } + + public boolean equals(AddProperty other) { + return Operations.equals(parentId, other.parentId) + && Operations.equals(propertyName, other.propertyName) + && isMultivalued == other.isMultivalued + && Arrays.equals(values, other.values); + } + + @Override + public int hashCode() { + return 41 * ( + 41 * ( + 41 + Operations.hashCode(parentId)) + + Operations.hashCode(propertyName)) + + Operations.hashCode(values); + } + } + + /** + * Factory method for creating an {@link AddProperty} operation. + * + * @see Batch#addProperty(NodeId, Name, QValue) + * @param parentId + * @param propertyName + * @param value + * @return + */ + public static Operation addProperty(NodeId parentId, Name propertyName, QValue value) { + return new AddProperty(parentId, propertyName, value); + } + + /** + * Factory method for creating an {@link AddProperty} operation. + * + * @see Batch#addProperty(NodeId, Name, QValue[]) + * @param parentId + * @param propertyName + * @param values + * @return + */ + public static Operation addProperty(NodeId parentId, Name propertyName, QValue[] values) { + return new AddProperty(parentId, propertyName, values); + } + + // --------------------------------------------------------------< Move >--- + /** + * Representative of a move {@link Operation} which calls + * {@link Batch#move(NodeId, NodeId, Name)} when applied to a {@link Batch}. + */ + public static class Move implements Operation { + protected final NodeId srcNodeId; + protected final NodeId destParentNodeId; + protected final Name destName; + + /** + * Create a new move {@link Operation} for the given arguments. + * + * @see Batch#move(NodeId, NodeId, Name) + * @param srcNodeId + * @param destParentNodeId + * @param destName + */ + public Move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) { + super(); + this.srcNodeId = srcNodeId; + this.destParentNodeId = destParentNodeId; + this.destName = destName; + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + batch.move(srcNodeId, destParentNodeId, destName); + } + + @Override + public String toString() { + return "Move[" + srcNodeId + ", " + destParentNodeId + ", " + destName + "]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof Move) { + return equals((Move) other); + } + return false; + } + + public boolean equals(Move other) { + return Operations.equals(srcNodeId, other.srcNodeId) + && Operations.equals(destParentNodeId, other.destParentNodeId) + && Operations.equals(destName, other.destName); + } + + @Override + public int hashCode() { + return 41 * ( + 41 * ( + 41 + Operations.hashCode(srcNodeId)) + + Operations.hashCode(destParentNodeId)) + + Operations.hashCode(destName); + } + } + + /** + * Factory method for creating a {@link Move} operation. + * + * @see Batch#move(NodeId, NodeId, Name) + * @param srcNodeId + * @param destParentNodeId + * @param destName + * @return + */ + public static Operation move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) { + return new Move(srcNodeId, destParentNodeId, destName); + } + + // ------------------------------------------------------------< Remove >--- + /** + * Representative of a remove {@link Operation} which calls {@link Batch#remove(ItemId)} when + * applied to a {@link Batch}. + */ + public static class Remove implements Operation { + protected final ItemId itemId; + + /** + * Create a new remove {@link Operation} for the given arguments. + * + * @see Batch#move(NodeId, NodeId, Name) + * @param itemId + */ + public Remove(ItemId itemId) { + super(); + this.itemId = itemId; + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + batch.remove(itemId); + } + + @Override + public String toString() { + return "Remove[" + itemId + "]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof Remove) { + return equals((Remove) other); + } + return false; + } + + public boolean equals(Remove other) { + return Operations.equals(itemId, other.itemId); + } + + @Override + public int hashCode() { + return 41 + Operations.hashCode(itemId); + } + } + + /** + * Factory method for creating a {@link Remove} operation. + * + * @see Batch#move(NodeId, NodeId, Name) + * @param itemId + * @return + */ + public static Operation remove(ItemId itemId) { + return new Remove(itemId); + } + + // ------------------------------------------------------< ReorderNodes >--- + /** + * Representative of a reorder-nodes {@link Operation} which calls + * {@link Batch#reorderNodes(NodeId, NodeId, NodeId)} when applied to a {@link Batch}. + */ + public static class ReorderNodes implements Operation { + protected final NodeId parentId; + protected final NodeId srcNodeId; + protected final NodeId beforeNodeId; + + /** + * Create a new reorder-nodes {@link Operation} for the given arguments. + * + * @see Batch#reorderNodes(NodeId, NodeId, NodeId) + * @param parentId + * @param srcNodeId + * @param beforeNodeId + */ + public ReorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) { + super(); + this.parentId = parentId; + this.srcNodeId = srcNodeId; + this.beforeNodeId = beforeNodeId; + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + batch.reorderNodes(parentId, srcNodeId, beforeNodeId); + } + + @Override + public String toString() { + return "ReorderNodes[" + parentId + ", " + srcNodeId + ", " + beforeNodeId + "]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof ReorderNodes) { + return equals((ReorderNodes) other); + } + return false; + } + + public boolean equals(ReorderNodes other) { + return Operations.equals(parentId, other.parentId) + && Operations.equals(srcNodeId, other.srcNodeId) + && Operations.equals(beforeNodeId, other.beforeNodeId); + } + + @Override + public int hashCode() { + return 41 * ( + 41 * ( + 41 + Operations.hashCode(parentId)) + + Operations.hashCode(srcNodeId)) + + Operations.hashCode(beforeNodeId); + } + } + + /** + * Factory method for creating a reorder-nodes {@link Operation} for the given arguments. + * + * @see Batch#reorderNodes(NodeId, NodeId, NodeId) + * @param parentId + * @param srcNodeId + * @param beforeNodeId + * @return + */ + public static Operation reorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) { + return new ReorderNodes(parentId, srcNodeId, beforeNodeId); + } + + // ---------------------------------------------------------< SetMixins >--- + /** + * Representative of a set-mixin {@link Operation} which calls + * {@link Batch#setMixins(NodeId, Name[])} when applied to a {@link Batch}. + */ + public static class SetMixins implements Operation { + protected final NodeId nodeId; + protected final Name[] mixinNodeTypeNames; + + /** + * Create a new set-mixin {@link Operation} for the given arguments. + * + * @see Batch#setMixins(NodeId, Name[]) + * @param nodeId + * @param mixinNodeTypeNames + */ + public SetMixins(NodeId nodeId, Name[] mixinNodeTypeNames) { + super(); + this.nodeId = nodeId; + this.mixinNodeTypeNames = mixinNodeTypeNames; + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + batch.setMixins(nodeId, mixinNodeTypeNames); + } + + @Override + public String toString() { + return "SetMixins[" + nodeId + ", " + Arrays.toString(mixinNodeTypeNames) + "]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof SetMixins) { + return equals((SetMixins) other); + } + return false; + } + + public boolean equals(SetMixins other) { + return Operations.equals(nodeId, other.nodeId) + && Arrays.equals(mixinNodeTypeNames, other.mixinNodeTypeNames); + } + + @Override + public int hashCode() { + return 41 * ( + 41 + Operations.hashCode(nodeId)) + + Operations.hashCode(mixinNodeTypeNames); + } + } + + /** + * Factory method for creating a set-mixin {@link Operation} for the given arguments. + * + * @see Batch#setMixins(NodeId, Name[]) + * @param nodeId + * @param mixinNodeTypeNames + * @return + */ + public static Operation setMixins(NodeId nodeId, Name[] mixinNodeTypeNames) { + return new SetMixins(nodeId, mixinNodeTypeNames); + } + + // ----------------------------------------------------< SetPrimaryType >--- + /** + * Representative of a set-mixin {@link Operation} which calls + * {@link Batch#setMixins(NodeId, Name[])} when applied to a {@link Batch}. + */ + public static class SetPrimaryType implements Operation { + protected final NodeId nodeId; + protected final Name primaryTypeName; + + /** + * Create a new set-mixin {@link Operation} for the given arguments. + * + * @see Batch#setMixins(NodeId, Name[]) + * @param nodeId + * @param primaryTypeName + */ + public SetPrimaryType(NodeId nodeId, Name primaryTypeName) { + super(); + this.nodeId = nodeId; + this.primaryTypeName = primaryTypeName; + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + batch.setPrimaryType(nodeId, primaryTypeName); + } + + @Override + public String toString() { + return "SetPrimaryType[" + nodeId + ", " + primaryTypeName + "]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof SetPrimaryType) { + return equals((SetPrimaryType) other); + } + return false; + } + + public boolean equals(SetPrimaryType other) { + return Operations.equals(nodeId, other.nodeId) + && primaryTypeName.equals(other.primaryTypeName); + } + + @Override + public int hashCode() { + return 41 * ( + 41 + Operations.hashCode(nodeId)) + + Operations.hashCode(primaryTypeName); + } + } + + /** + * Factory method for creating a set-primaryType {@link Operation} for the given arguments. + * + * @see Batch#setPrimaryType(NodeId, Name) + * @param nodeId + * @param primaryTypeName + * @return + */ + public static Operation setPrimaryType(NodeId nodeId, Name primaryTypeName) { + return new SetPrimaryType(nodeId, primaryTypeName); + } + + // ----------------------------------------------------------< SetValue >--- + /** + * Representative of a set-value {@link Operation} which calls + * {@link Batch#setValue(PropertyId, QValue)} or {@link Batch#setValue(PropertyId, QValue[])} + * depending on whether the property is multi valued or not when applied to a {@link Batch}. + */ + public static class SetValue implements Operation { + protected final PropertyId propertyId; + protected final QValue[] values; + protected final boolean isMultivalued; + + private SetValue(PropertyId propertyId, QValue[] values, boolean isMultivalued) { + super(); + this.propertyId = propertyId; + this.values = values; + this.isMultivalued = isMultivalued; + } + + /** + * Create a new set-value {@link Operation} for the given arguments. + * + * @see Batch#setValue(PropertyId, QValue) + * @param propertyId + * @param value + */ + public SetValue(PropertyId propertyId, QValue value) { + this(propertyId, new QValue[]{ value }, false); + } + + /** + * Create a new set-value {@link Operation} for the given arguments. + * + * @see Batch#setValue(PropertyId, QValue[]) + * @param propertyId + * @param values + */ + public SetValue(PropertyId propertyId, QValue[] values) { + this(propertyId, values, true); + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + if (isMultivalued) { + batch.setValue(propertyId, values); + } + else { + batch.setValue(propertyId, values[0]); + } + } + + @Override + public String toString() { + return "SetValue[" + propertyId + ", " + Arrays.toString(values) + "]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof SetValue) { + return equals((SetValue) other); + } + return false; + } + + public boolean equals(SetValue other) { + return Operations.equals(propertyId, other.propertyId) + && isMultivalued == other.isMultivalued + && Arrays.equals(values, other.values); + } + + @Override + public int hashCode() { + return 41 * ( + 41 + Operations.hashCode(propertyId)) + + Operations.hashCode(values); + } + } + + /** + * Factory method for creating set-value {@link Operation} for the given arguments. + * + * @see Batch#setValue(PropertyId, QValue) + * @param propertyId + * @param value + * @return + */ + public static Operation setValue(PropertyId propertyId, QValue value) { + return new SetValue(propertyId, value); + } + + /** + * Factory method for creating a set-value {@link Operation} for the given arguments. + * + * @see Batch#setValue(PropertyId, QValue[]) + * @param propertyId + * @param values + * @return + */ + public static Operation setValue(final PropertyId propertyId, final QValue[] values) { + return new SetValue(propertyId, values); + } + + // -----------------------------------------------------------< private >--- + + protected static boolean equals(Object o1, Object o2) { + return o1 == null + ? o2 == null + : o1.equals(o2); + } + + protected static int hashCode(Object o) { + return o == null + ? 0 + : o.hashCode(); + } + + //--------------------------------------------------------------< SetTree >--- + public static class SetTree implements Operation { + protected final NodeId parentId; + protected final Tree tree; + + public SetTree(NodeId parentId, Tree tree) { + super(); + this.parentId = parentId; + this.tree = tree; + } + + /** + * {@inheritDoc} + */ + public void apply(Batch batch) throws RepositoryException { + batch.setTree(parentId, tree); + } + + //----------------------------< Object >--- + @Override + public String toString() { + return "SetTree[" + parentId + ", " + tree+"]"; + } + + @Override + public boolean equals(Object other) { + if (null == other) { + return false; + } + if (this == other) { + return true; + } + if (other instanceof SetTree) { + return equals((SetTree) other); + } + return false; + } + + public boolean equals(SetTree other) { + return Operations.equals(parentId, other.parentId) + && Operations.equals(tree, other.tree); + } + + @Override + public int hashCode() { + return 41 * ( + 41 + Operations.hashCode(parentId)) + + Operations.hashCode(tree); + } + } + + /** + * Factory method for creating an {@link SetTree} operation. + * @see Batch#addNode(NodeId, Name, Name, String) + */ + public static Operation setTree(NodeId parentId, Tree contentTree) { + return new SetTree(parentId, contentTree); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/package-info.java new file mode 100644 index 00000000000..6931b7830b4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/batch/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.batch; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/CachingNameResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/CachingNameResolver.java new file mode 100644 index 00000000000..30e9b874bc1 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/CachingNameResolver.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.spi.commons.conversion; + +import org.apache.jackrabbit.spi.Name; + +import javax.jcr.NamespaceException; + +/** + * Name resolver decorator that uses a generational cache to speed up + * parsing and formatting of JCR names. Uncached names are resolved using + * the underlying decorated name resolver. + */ +public class CachingNameResolver implements NameResolver { + + /** + * Decorated name resolver. + */ + private final NameResolver resolver; + + /** + * Generational cache. + */ + private final GenerationalCache cache; + + /** + * Creates a caching decorator for the given name resolver. The given + * generational cache is used for caching. + * + * @param resolver decorated name resolver + * @param cache generational cache + */ + public CachingNameResolver(NameResolver resolver, GenerationalCache cache) { + this.resolver = resolver; + this.cache = cache; + } + + /** + * Creates a caching decorator for the given name resolver. + * + * @param resolver name resolver + */ + public CachingNameResolver(NameResolver resolver) { + this(resolver, new GenerationalCache()); + } + + //-------------------------------------------------------< NameResolver >--- + /** + * Returns a Name for the given prefixed JCR name. The name + * is first looked up form the generational cache and the call gets + * delegated to the decorated name resolver only if the cache misses. + * + * @param jcrName A JCR name String. + * @return A Name object. + * @throws IllegalNameException if the JCR name format is invalid + * @throws NamespaceException if the namespace prefix can not be resolved + */ + public Name getQName(String jcrName) + throws IllegalNameException, NamespaceException { + Name name = (Name) cache.get(jcrName); + if (name == null) { + name = resolver.getQName(jcrName); + cache.put(jcrName, name); + } + return name; + } + + + /** + * Returns the prefixed JCR name for the given Name. The name + * is first looked up form the generational cache and the call gets + * delegated to the decorated name resolver only if the cache misses. + * + * @param name The name object. + * @return qualified JCR name in the form prefix:localName. + * @throws NamespaceException if the namespace URI can not be resolved + */ + public String getJCRName(Name name) throws NamespaceException { + if (name.getNamespaceURI().length() == 0) { + return name.getLocalName(); + } + String jcrName = (String) cache.get(name); + if (jcrName == null) { + jcrName = resolver.getJCRName(name); + cache.put(name, jcrName); + } + return jcrName; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/CachingPathResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/CachingPathResolver.java new file mode 100644 index 00000000000..a60aab33974 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/CachingPathResolver.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.spi.commons.conversion; + +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.NamespaceException; + +/** + * Path resolver decorator that uses a generational cache to speed up + * parsing and formatting of JCR paths. Uncached paths are resolved using + * the underlying decorated path resolver. + */ +public class CachingPathResolver implements PathResolver { + + /** + * Decorated path resolver. + */ + private final PathResolver resolver; + + /** + * Generational cache. + */ + private final GenerationalCache cache; + + /** + * Creates a caching decorator for the given path resolver. The given + * generational cache is used for caching. + * + * @param resolver decorated path resolver + * @param cache generational cache + */ + public CachingPathResolver(PathResolver resolver, GenerationalCache cache) { + this.resolver = resolver; + this.cache = cache; + } + + /** + * Creates a caching decorator for the given path resolver. + * + * @param resolver name resolver + */ + public CachingPathResolver(PathResolver resolver) { + this(resolver, new GenerationalCache()); + } + + //--------------------------------------------------------< PathResolver > + + /** + * Returns the Path object for the given JCR path String. + * The path is first looked up form the generational cache and the call gets + * delegated to the decorated path resolver only if the cache misses. + * + * @param path A JCR path String. + * @return A Path object. + * @throws MalformedPathException if the JCR path format is invalid + * @throws IllegalNameException if any of the JCR names contained in the + * path are invalid. + * @throws NamespaceException if a namespace prefix can not be resolved. + * @see PathResolver#getQPath(String) + */ + public Path getQPath(String path) throws MalformedPathException, IllegalNameException, NamespaceException { + return getQPath(path, true); + } + + /** + * @see PathResolver#getQPath(String, boolean) + */ + public Path getQPath(String path, boolean normalizeIdentifier) throws MalformedPathException, IllegalNameException, NamespaceException { + Path qpath; + /* + * Jcr paths consisting of an identifier segment have 2 different + * path object representations depending on the given resolution flag: + * 1) a normalized absolute path if resolveIdentifier is true + * 2) a path denoting an identifier if resolveIdentifier is false. + * The latter are not cached in order not to return a wrong resolution + * when calling getQPath with the same identifier-jcr-path. + */ + if (path.startsWith("[") && !normalizeIdentifier) { + qpath = resolver.getQPath(path, normalizeIdentifier); + } else { + qpath = (Path) cache.get(path); + if (qpath == null) { + qpath = resolver.getQPath(path, normalizeIdentifier); + cache.put(path, qpath); + } + } + return qpath; + + } + + + /** + * Returns the JCR path String for the given Path. The path + * is first looked up form the generational cache and the call gets + * delegated to the decorated path resolver only if the cache misses. + * + * @param path A Path object. + * @return A JCR path String in the standard form. + * @throws NamespaceException if a namespace URI can not be resolved. + * @see PathResolver#getJCRPath(org.apache.jackrabbit.spi.Path) + */ + public String getJCRPath(Path path) throws NamespaceException { + String jcrPath = (String) cache.get(path); + if (jcrPath == null) { + jcrPath = resolver.getJCRPath(path); + cache.put(path, jcrPath); + } + return jcrPath; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/DefaultNamePathResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/DefaultNamePathResolver.java new file mode 100644 index 00000000000..50cfd5b37e5 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/DefaultNamePathResolver.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.spi.commons.conversion; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Session; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.RegistryNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.SessionNamespaceResolver; + +/** + * DefaultNamePathResolver... + */ +public class DefaultNamePathResolver implements NamePathResolver { + + private final NameResolver nResolver; + + private final PathResolver pResolver; + + public DefaultNamePathResolver(NamespaceResolver nsResolver) { + this(nsResolver, false); + } + + public DefaultNamePathResolver(Session session) { + this(new SessionNamespaceResolver(session), ((session instanceof IdentifierResolver)? (IdentifierResolver) session : null), false); + } + + public DefaultNamePathResolver(NamespaceRegistry registry) { + this(new RegistryNamespaceResolver(registry)); + } + + public DefaultNamePathResolver(NamespaceResolver nsResolver, boolean enableCaching) { + this(nsResolver, null, enableCaching); + } + + public DefaultNamePathResolver(NamespaceResolver nsResolver, IdentifierResolver idResolver, boolean enableCaching) { + NameResolver nr = new ParsingNameResolver(NameFactoryImpl.getInstance(), nsResolver); + PathResolver pr = new ParsingPathResolver(PathFactoryImpl.getInstance(), nr, idResolver); + if (enableCaching) { + this.nResolver = new CachingNameResolver(nr); + this.pResolver = new CachingPathResolver(pr); + } else { + this.nResolver = nr; + this.pResolver = pr; + } + } + + public DefaultNamePathResolver(NameResolver nResolver, PathResolver pResolver) { + this.nResolver = nResolver; + this.pResolver = pResolver; + } + + public Name getQName(String name) throws IllegalNameException, NamespaceException { + return nResolver.getQName(name); + } + + public String getJCRName(Name name) throws NamespaceException { + return nResolver.getJCRName(name); + } + + public Path getQPath(String path) throws MalformedPathException, IllegalNameException, NamespaceException { + return pResolver.getQPath(path); + } + + public Path getQPath(String path, boolean normalizeIdentifier) throws MalformedPathException, IllegalNameException, NamespaceException { + return pResolver.getQPath(path, normalizeIdentifier); + } + + public String getJCRPath(Path path) throws NamespaceException { + return pResolver.getJCRPath(path); + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/GenerationalCache.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/GenerationalCache.java new file mode 100644 index 00000000000..bcaf7ec2361 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/GenerationalCache.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.spi.commons.conversion; + +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; + +/** + * Generational cache. The cache implemented by this class consists of three + * parts: a long term cache and two generations of recent entries. The two + * generations are used to collect recent new entries, and those entries that + * are used within two successive generations get promoted to the long term + * cache. The entries within the long term cache are discarded only when the + * size of the cache exceeds the given maximum cache size. + */ +class GenerationalCache { + + /** + * Default maximum cache size. + */ + private static final int DEFAULT_CACHE_SIZE = 1000; + + /** + * Divisor used to determine the default generation age from the + * maximum cache size. + */ + private static final int DEFAULT_SIZE_AGE_RATIO = 10; + + /** + * Maximum size of the name cache. + */ + private final int maxSize; + + /** + * Maximum age of a cache generation. + */ + private final int maxAge; + + /** + * Long term cache. Read only. + */ + private Map cache = new HashMap(); + + /** + * Old cache generation. + */ + private Map old = new HashMap(); + + /** + * Young cache generation. + */ + private Map young = new HashMap(); + + /** + * Age of the young cache generation. + */ + private int age = 0; + + /** + * Creates a caching resolver. + * + * @param maxSize maximum size of the long term cache + * @param maxAge maximum age of a cache generation + */ + public GenerationalCache(int maxSize, int maxAge) { + this.maxSize = maxSize; + this.maxAge = maxAge; + } + + /** + * Creates a caching resolver using the default generation age for + * the given cache size. + * + * @param maxSize maximum size of the long term cache + */ + public GenerationalCache(int maxSize) { + this(maxSize, maxSize / DEFAULT_SIZE_AGE_RATIO); + } + + /** + * Creates a caching resolver using the default size and generation age. + */ + public GenerationalCache() { + this(DEFAULT_CACHE_SIZE); + } + + /** + * Returns the cached value (if any) for the given key. The value is + * looked up both from the long term cache and the old cache generation. + * If the value is only found in the old cache generation, it gets added + * to the young generation via a call to {@link #put(Object, Object)}. + * + * @param key key of the cache entry + * @return value of the cache entry, or null + */ + public Object get(Object key) { + Object value = cache.get(key); + if (value == null) { + value = old.get(key); + if (value != null) { + put(key, value); + } + } + return value; + } + + /** + * Caches the given key-value pair and increases the age of the current + * cache generation. When the maximum age of a generation is reached, + * the following steps are taken: + *
          + *
        1. The union of the two cache generations is calculated
        2. + *
        3. The union is added to the long term name cache
        4. + *
        5. If the cache size exceeds the maximum, only the union is kept
        6. + *
        7. A new cache generation is started
        8. + *
        + * + * @param key key of the cache entry + * @param value value of the cache entry + */ + public synchronized void put(Object key, Object value) { + young.put(key, value); + + if (++age == maxAge) { + Map union = new HashMap(); + Iterator iterator = old.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + if (young.containsKey(entry.getKey())) { + union.put(entry.getKey(), entry.getValue()); + } + } + + if (!union.isEmpty()) { + if (cache.size() + union.size() <= maxSize) { + union.putAll(cache); + } + cache = union; + } + + old = young; + young = new HashMap(); + age = 0; + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/IdentifierResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/IdentifierResolver.java new file mode 100644 index 00000000000..d40edad490d --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/IdentifierResolver.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.spi.commons.conversion; + +import org.apache.jackrabbit.spi.Path; + +/** + * IdentifierResolver .... + */ +public interface IdentifierResolver { + + public Path getPath(String identifier) throws MalformedPathException; + + public void checkFormat(String identifier) throws MalformedPathException; +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/IllegalNameException.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/IllegalNameException.java new file mode 100644 index 00000000000..366b45e0695 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/IllegalNameException.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.spi.commons.conversion; + +/** + * Thrown when an illegal JCR name string is encountered. This exception is + * thrown when attempting to parse a JCR name string that does not match the + * JCR name syntax, or is otherwise not a legal name. Note that an + * {@link javax.jcr.NamespaceException} is thrown if the prefix of the JCR name + * string is syntactically valid but not bound to any namespace. + *

        + * See the section 4.6 of the JCR 1.0 specification for details of the + * JCR name syntax. + */ +public class IllegalNameException extends NameException { + + /** + * Creates an IllegalNameException with the given error message. + * + * @param message error message + */ + public IllegalNameException(String message) { + super(message); + } + + /** + * Creates an IllegalNameException with the given error message and + * root cause exception. + * + * @param message error message + * @param rootCause root cause exception + */ + public IllegalNameException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/MalformedPathException.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/MalformedPathException.java new file mode 100644 index 00000000000..4aedc7ddc1c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/MalformedPathException.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.spi.commons.conversion; + +/** + * Thrown when a malformed JCR path string is encountered. This exception is + * thrown when attempting to parse a JCR path string that does not match the + * JCR path syntax, contains an invalid path element, or is otherwise not + * well formed. + *

        + * See the section 4.6 of the JCR 1.0 specification for details of the + * JCR path syntax. + */ +public class MalformedPathException extends NameException { + + /** + * Creates a MalformedPathException with the given error message. + * + * @param message error message + */ + public MalformedPathException(String message) { + super(message); + } + + /** + * Creates a MalformedPathException with the given error message + * and root cause exception. + * + * @param message error message + * @param rootCause root cause exception + */ + public MalformedPathException(String message, Throwable rootCause) { + super(message, rootCause); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/NameException.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/NameException.java new file mode 100644 index 00000000000..82596c32ff4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/NameException.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.spi.commons.conversion; + +import javax.jcr.RepositoryException; + +/** + * Base class for exceptions about malformed or otherwise + * invalid JCR names and paths. + */ +public class NameException extends RepositoryException { + + /** + * Creates a NameException with the given error message. + * + * @param message error message + */ + public NameException(String message) { + super(message); + } + + /** + * Creates a NameException with the given error message and + * root cause exception. + * + * @param message error message + * @param rootCause root cause exception + */ + public NameException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/NameParser.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/NameParser.java new file mode 100644 index 00000000000..9eabfab0610 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/NameParser.java @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.conversion; + +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.util.XMLChar; + +import javax.jcr.NamespaceException; + +/** + * NameParser parses a {@link String jcrName} using a + * {@link NamespaceResolver} and a {@link NameFactory}. + */ +public class NameParser { + + // 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; + + /** + * Parses the jcrName (either qualified or expanded) and + * returns a new Name. + * + * @param jcrName the name to be parsed. The jcrName may either be in the + * qualified or in the expanded form. + * @param resolver NamespaceResolver use to retrieve the + * namespace URI from the prefix contained in the given JCR name. + * @return qName the new Name + * @throws IllegalNameException If jcrName is not a valid + * JCR-style name. + * @throws NamespaceException If the jcr name contains an unknown prefix. + */ + public static Name parse(String jcrName, NamespaceResolver resolver, NameFactory factory) + throws IllegalNameException, NamespaceException { + // trivial check + int len = jcrName == null ? 0 : jcrName.length(); + if (len == 0) { + throw new IllegalNameException("empty name"); + } + if (".".equals(jcrName) || "..".equals(jcrName)) { + throw new IllegalNameException(jcrName); + } + + // parse the name + String prefix = ""; + String uri = null; + int nameStart = 0; + int state = STATE_PREFIX_START; + boolean trailingSpaces = false; + boolean checkFormat = (resolver == null); + + for (int i = 0; i < len; i++) { + char c = jcrName.charAt(i); + if (c == ':') { + if (state == STATE_PREFIX_START) { + throw new IllegalNameException("Prefix must not be empty"); + } else if (state == STATE_PREFIX) { + if (trailingSpaces) { + throw new IllegalNameException("Trailing spaces not allowed"); + } + prefix = jcrName.substring(0, i); + if (!XMLChar.isValidNCName(prefix)) { + throw new IllegalNameException("Invalid name prefix: "+ prefix); + } + state = STATE_NAME_START; + } else if (state == STATE_URI) { + // ignore -> validation of uri later on. + } else { + throw new IllegalNameException("'" + c + "' not allowed in name"); + } + trailingSpaces = false; + } else if (c == ' ') { + if (state == STATE_PREFIX_START || state == STATE_NAME_START) { + throw new IllegalNameException("'" + c + "' not valid name start"); + } + trailingSpaces = true; + } else if (Character.isWhitespace(c) || c == '[' || c == ']' || c == '*' || c == '|') { + throw new IllegalNameException("'" + c + "' not allowed in name"); + } else if (c == '/') { + if (state == STATE_URI_START) { + state = STATE_URI; + } else if (state != STATE_URI) { + throw new IllegalNameException("'" + c + "' not allowed in name"); + } + 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.length() == 0 || 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. + uri = tmp; + state = STATE_NAME_START; + } else if (tmp.equals("internal")) { + // As a special Jackrabbit backwards compatibility + // feature, support {internal} as a valid URI prefix + uri = tmp; + 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 { + throw new IllegalNameException( + "The URI prefix of the name " + jcrName + + " is neither a valid URI nor a valid part" + + " of a local name."); + } + } 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)) { + throw new IllegalNameException("Local name may not contain ':' nor '/'"); + } + + if (nameStart == len || state == STATE_NAME_START) { + throw new IllegalNameException("Local name must not be empty"); + } + if (trailingSpaces) { + throw new IllegalNameException("Trailing spaces not allowed"); + } + + // if namespace is null, this is just a check for format. this can only + // happen if invoked internally + if (checkFormat) { + return null; + } + + // resolve prefix to uri + if (uri == null) { + uri = resolver.getURI(prefix); + } + + String localName = (nameStart == 0 ? jcrName : jcrName.substring(nameStart, len)); + return factory.create(uri, localName); + } + + /** + * Parses an array of jcrName and returns the respective + * array of Name. + * + * @param jcrNames the array of names to be parsed + * @param resolver NamespaceResolver use to retrieve the + * namespace URI from the prefix contained in the given JCR name. + * @param factory + * @return the new array of Name + * @throws IllegalNameException If jcrName is not a valid + * JCR-style name. + * @throws NamespaceException If the jcr name contains an unknown prefix. + */ + public static Name[] parse(String jcrNames[], NamespaceResolver resolver, NameFactory factory) + throws NameException, NamespaceException { + + Name[] ret = new Name[jcrNames.length]; + for (int i=0; iName for the given JCR name String. + * + * @param name A JCR name String. + * @return A Name object. + * @throws IllegalNameException if the JCR name format is invalid + * @throws NamespaceException if the namespace prefix can not be resolved + */ + Name getQName(String name) throws IllegalNameException, NamespaceException; + + /** + * Returns the qualified JCR name String for the given Name object. + * + * @param name A Name object. + * @return The qualified JCR name String consisting of + * prefix:localName or + * localName in case of the empty namespace. + * @throws NamespaceException if the namespace URI can not be resolved + */ + String getJCRName(Name name) throws NamespaceException; + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/ParsingNameResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/ParsingNameResolver.java new file mode 100644 index 00000000000..27db1ea758e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/ParsingNameResolver.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.spi.commons.conversion; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.NamespaceException; + +/** + * Name resolver that parsers and formats prefixed JCR names. + * A {@link NamespaceResolver} is used for resolving the namespace prefixes. + */ +public class ParsingNameResolver implements NameResolver { + + /** + * Name factory. + */ + private final NameFactory nameFactory; + + /** + * Namespace resolver. + */ + private final NamespaceResolver resolver; + + /** + * Creates a parsing name resolver. + * + * @param nameFactory the name factory. + * @param resolver namespace resolver + */ + public ParsingNameResolver(NameFactory nameFactory, NamespaceResolver resolver) { + this.nameFactory = nameFactory; + this.resolver = resolver; + } + + //--------------------------------------------------------< NameResolver > + + /** + * Parses the given JCR name and returns the resolved Name object. + * + * @param jcrName A JCR name String + * @return A Name object. + * @throws IllegalNameException if the JCR name format is invalid + * @throws NamespaceException if the namespace prefix can not be resolved. + * @see NameResolver#getQName(String) + */ + public Name getQName(String jcrName) throws IllegalNameException, NamespaceException { + return NameParser.parse(jcrName, resolver, nameFactory); + } + + /** + * Returns the qualified JCR name for the given Name object. + * If the name is in the default namespace, then the local name + * is returned without a prefix. Otherwise the prefix for the + * namespace is resolved and used to construct the JCR name. + * + * @param name A Name object. + * @return A qualified JCR name string. + * @throws NamespaceException if the namespace URI can not be resolved. + * @see NameResolver#getJCRName(org.apache.jackrabbit.spi.Name) + */ + public String getJCRName(Name name) throws NamespaceException { + String uri = name.getNamespaceURI(); + if (uri.length() == 0) { + return name.getLocalName(); + } else { + return resolver.getPrefix(uri) + ":" + name.getLocalName(); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/ParsingPathResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/ParsingPathResolver.java new file mode 100644 index 00000000000..1a6d78e91cc --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/ParsingPathResolver.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.spi.commons.conversion; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; + +import javax.jcr.NamespaceException; + +/** + * Path resolver that parsers and formats prefixed JCR paths. + * A {@link NameResolver} is used for resolving the path element names. + */ +public class ParsingPathResolver implements PathResolver { + + /** + * Path factory. + */ + private final PathFactory pathFactory; + + /** + * Name resolver. + */ + private final NameResolver nameResolver; + + /** + * Identifier resolver. + */ + private final IdentifierResolver idResolver; + + /** + * Creates a parsing path resolver. + * + * @param pathFactory path factory. + * @param resolver name resolver + */ + public ParsingPathResolver(PathFactory pathFactory, NameResolver resolver) { + this(pathFactory, resolver, null); + } + + /** + * Creates a parsing path resolver. + * + * @param pathFactory path factory. + * @param nameResolver name resolver. + * @param idResolver identifier resolver. + * @since JCR 2.0 + */ + public ParsingPathResolver(PathFactory pathFactory, NameResolver nameResolver, + IdentifierResolver idResolver) { + this.pathFactory = pathFactory; + this.nameResolver = nameResolver; + this.idResolver = idResolver; + } + + /** + * Parses the given JCR path into a Path object. + * + * @param jcrPath A JCR path String. + * @return A Path object. + * @throws MalformedPathException if the JCR path format is invalid. + * @throws IllegalNameException if any of the JCR names contained in the path are invalid. + * @throws NamespaceException if a namespace prefix can not be resolved + */ + public Path getQPath(String jcrPath) throws MalformedPathException, IllegalNameException, NamespaceException { + return PathParser.parse(jcrPath, nameResolver, idResolver, pathFactory); + } + + /** + * Calls {@link PathParser#parse(String, NameResolver, IdentifierResolver, org.apache.jackrabbit.spi.PathFactory)} + * from the given jcrPath. + * + * @see PathResolver#getQPath(String, boolean) + */ + public Path getQPath(String jcrPath, boolean normalizeIdentifier) throws MalformedPathException, IllegalNameException, NamespaceException { + return PathParser.parse(jcrPath, nameResolver, idResolver, pathFactory, normalizeIdentifier); + } + + + /** + * Returns the JCR path representation for the given Path object. + * + * @param path A Path object. + * @return A JCR path String in the standard form. + * @throws NamespaceException if a namespace URI can not be resolved. + * @see PathResolver#getJCRPath(org.apache.jackrabbit.spi.Path) + */ + public String getJCRPath(Path path) throws NamespaceException { + StringBuffer buffer = new StringBuffer(); + + Path.Element[] elements = path.getElements(); + for (int i = 0; i < elements.length; i++) { + if (i > 0) { + buffer.append('/'); + } + if (i == 0 && elements.length == 1 && elements[i].denotesRoot()) { + buffer.append('/'); + } else if (elements[i].denotesCurrent()) { + buffer.append('.'); + } else if (elements[i].denotesParent()) { + buffer.append(".."); + } else if (elements[i].denotesIdentifier()) { + buffer.append(elements[i].getString()); + } else { + buffer.append(nameResolver.getJCRName(elements[i].getName())); + /** + * FIXME the [1] subscript should only be suppressed if the + * item in question can't have same-name siblings. + */ + if (elements[i].getIndex() > Path.INDEX_DEFAULT) { + buffer.append('['); + buffer.append(elements[i].getIndex()); + buffer.append(']'); + } + } + } + return buffer.toString(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/PathParser.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/PathParser.java new file mode 100644 index 00000000000..6893815f289 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/PathParser.java @@ -0,0 +1,466 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.conversion; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; + +import javax.jcr.NamespaceException; + +/** + * PathParser formats a {@link Path} using a + * {@link NameResolver} and a {@link PathFactory}. + */ +public class PathParser { + + // 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_IDENTIFIER = 8; + private static final int STATE_URI = 9; + private static final int STATE_URI_END = 10; + + private static final char EOF = (char) -1; + + /** + * Parses jcrPath into a Path object using + * resolver to convert prefixes into namespace URIs. If + * resolver is null this method only checks the format of the + * passed String and returns null. + * + * @param jcrPath the jcr path. + * @param resolver the namespace resolver. + * @param factory PathFactory to be used. + * @return A path object. + * @throws MalformedPathException If the jcrPath is malformed. + * @throws IllegalNameException if any of the jcrNames is malformed. + * @throws NamespaceException If an unresolvable prefix is encountered. + */ + public static Path parse(String jcrPath, NameResolver resolver, PathFactory factory) + throws MalformedPathException, IllegalNameException, NamespaceException { + return parse(null, jcrPath, resolver, factory); + } + + /** + * Parses jcrPath into a Path object using + * resolver to convert prefixes into namespace URIs. If the + * specified jcrPath is an identifier based absolute path + * beginning with an identifier segment the specified + * IdentifierResolver will be used to resolve it to an + * absolute path. + *

        + * If namResolver is null or if identifierResolver + * is null and the path starts with an identifier segment, this + * method only checks the format of the string and returns null. + * + * @param jcrPath the jcr path. + * @param nameResolver the namespace resolver. + * @param identifierResolver the resolver to validate any trailing identifier + * segment and resolve to an absolute path. + * @param factory + * @return A path object. + * @throws MalformedPathException If the jcrPath is malformed. + * @throws IllegalNameException if any of the jcrNames is malformed. + * @throws NamespaceException If an unresolvable prefix is encountered. + * @since JCR 2.0 + */ + public static Path parse(String jcrPath, NameResolver nameResolver, + IdentifierResolver identifierResolver, PathFactory factory) + throws MalformedPathException, IllegalNameException, NamespaceException { + return parse(null, jcrPath, nameResolver, identifierResolver, factory); + } + + /** + * Parses jcrPath into a Path object using + * resolver to convert prefixes into namespace URIs. If the + * specified jcrPath is an identifier based absolute path + * beginning with an identifier segment the specified + * IdentifierResolver will be used to resolve it to an + * absolute path. + *

        + * If namResolver is null or if identifierResolver + * is null and the path starts with an identifier segment, this + * method only checks the format of the string and returns null. + * + * @param jcrPath the jcr path. + * @param nameResolver the namespace resolver. + * @param identifierResolver the resolver to validate any trailing identifier + * segment and resolve to an absolute path. + * @param factory + * @param normalizeIdentifier + * @return A path object. + * @throws MalformedPathException If the jcrPath is malformed. + * @throws IllegalNameException if any of the jcrNames is malformed. + * @throws NamespaceException If an unresolvable prefix is encountered. + * @since JCR 2.0 + */ + public static Path parse(String jcrPath, NameResolver nameResolver, + IdentifierResolver identifierResolver, + PathFactory factory, boolean normalizeIdentifier) + throws MalformedPathException, IllegalNameException, NamespaceException { + return parse(null, jcrPath, nameResolver, identifierResolver, factory, normalizeIdentifier); + } + + /** + * Parses the given jcrPath and returns a Path. If + * parent is not null, it is prepended to the + * built path before it is returned. If resolver is + * null, this method only checks the format of the string and + * returns null. + * + * @param parent the parent path + * @param jcrPath the JCR path + * @param resolver the namespace resolver to get prefixes for namespace + * URIs. + * @param factory + * @return the Path object. + * @throws MalformedPathException If the jcrPath is malformed. + * @throws IllegalNameException if any of the jcrNames is malformed. + * @throws NamespaceException If an unresolvable prefix is encountered. + */ + public static Path parse(Path parent, String jcrPath, + NameResolver resolver, + PathFactory factory) throws MalformedPathException, IllegalNameException, NamespaceException { + return parse(parent, jcrPath, resolver, null, factory); + } + + /** + * Parses the given jcrPath and returns a Path. If + * parent is not null, it is prepended to the + * built path before it is returned. If the specified jcrPath + * is an identifier based absolute path beginning with an identifier segment + * the given identifierResolver will be used to resolve it to an + * absolute path. + *

        + * If nameResolver is null or if identifierResolver + * is null and the path starts with an identifier segment, this + * method only checks the format of the string and returns null. + * + * @param parent the parent path. + * @param jcrPath the jcr path. + * @param nameResolver the namespace resolver. + * @param identifierResolver the resolver to validate any trailing identifier + * segment and resolve it to an absolute path. + * @param factory The path factory. + * @return the Path object. + * @throws MalformedPathException + * @throws IllegalNameException + * @throws NamespaceException + */ + public static Path parse(Path parent, String jcrPath, NameResolver nameResolver, + IdentifierResolver identifierResolver, PathFactory factory) + throws MalformedPathException, IllegalNameException, NamespaceException { + return parse(parent, jcrPath, nameResolver, identifierResolver, factory, true); + } + + /** + * Parses the given jcrPath and returns a Path. If + * parent is not null, it is prepended to the + * built path before it is returned. If the specified jcrPath + * is an identifier based absolute path beginning with an identifier segment + * the given identifierResolver will be used to resolve it to an + * absolute path.

        + * If nameResolver is null or if identifierResolver + * is null and the path starts with an identifier segment, this + * method only checks the format of the string and returns null. + * + * @param parent the parent path. + * @param jcrPath the jcr path. + * @param nameResolver the namespace resolver. + * @param identifierResolver the resolver to validate any trailing identifier + * segment and resolve it to an absolute path. + * @param factory The path factory. + * @param normalizeIdentifier + * @return the Path object. + * @throws MalformedPathException + * @throws IllegalNameException + * @throws NamespaceException + */ + private static Path parse(Path parent, String jcrPath, NameResolver nameResolver, + IdentifierResolver identifierResolver, PathFactory factory, + boolean normalizeIdentifier) + throws MalformedPathException, IllegalNameException, NamespaceException { + // check for length + int len = jcrPath == null ? 0 : jcrPath.length(); + + // shortcut + if (len == 1 && jcrPath.charAt(0) == '/') { + return factory.getRootPath(); + } + + if (len == 0) { + throw new MalformedPathException("empty path"); + } + + // check if absolute path + PathBuilder builder = new PathBuilder(factory); + int pos = 0; + if (jcrPath.charAt(0) == '/') { + if (parent != null) { + throw new MalformedPathException("'" + jcrPath + "' is not a relative path."); + } + builder.addRoot(); + pos++; + } + + // add master if present + if (parent != null) { + builder.addAll(parent.getElements()); + } + + // parse the path + int state; + if (jcrPath.charAt(0) == '[') { + if (parent != null) { + throw new MalformedPathException("'" + jcrPath + "' is not a relative path."); + } + state = STATE_IDENTIFIER; + pos++; + } else { + state = STATE_PREFIX_START; + } + + int lastPos = pos; + + String name = null; + + int index = Path.INDEX_UNDEFINED; + boolean wasSlash = false; + + boolean checkFormat = (nameResolver == null); + + while (pos <= len) { + char c = pos == len ? EOF : jcrPath.charAt(pos); + char rawCharacter = c; + pos++; + // special check for whitespace + if (c != ' ' && Character.isWhitespace(c)) { + c = '\t'; + } + switch (c) { + case '/': + case EOF: + if (state == STATE_PREFIX_START && c != EOF) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. double slash '//' not allowed."); + } + if (state == STATE_URI && c == EOF) { + // this handles the case where URI state was entered but the end of the segment was reached (JCR-3562) + state = STATE_URI_END; + } + if (state == STATE_PREFIX + || state == STATE_NAME + || state == STATE_INDEX_END + || state == STATE_URI_END) { + + // eof pathelement + if (name == null) { + if (wasSlash) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Trailing slashes not allowed in prefixes and names."); + } + name = jcrPath.substring(lastPos, pos - 1); + } + + // only add element if resolver not null. otherwise this + // is just a check for valid format. + if (checkFormat) { + NameParser.checkFormat(name); + } else { + Name qName = nameResolver.getQName(name); + builder.addLast(qName, index); + } + state = STATE_PREFIX_START; + lastPos = pos; + name = null; + index = Path.INDEX_UNDEFINED; + } else if (state == STATE_IDENTIFIER) { + if (c == EOF) { + // eof identifier reached + if (jcrPath.charAt(pos - 2) != ']') { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Unterminated identifier segment."); + } + String identifier = jcrPath.substring(lastPos, pos - 2); + if (checkFormat) { + if (identifierResolver != null) { + identifierResolver.checkFormat(identifier); + } // else ignore. TODO: rather throw? + } else if (identifierResolver == null) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Identifier segments are not supported."); + } else if (normalizeIdentifier) { + builder.addAll(identifierResolver.getPath(identifier).getElements()); + } else { + identifierResolver.checkFormat(identifier); + builder.addLast(factory.createElement(identifier)); + } + state = STATE_PREFIX_START; + lastPos = pos; + } + } else if (state == STATE_DOT) { + builder.addLast(factory.getCurrentElement()); + lastPos = pos; + state = STATE_PREFIX_START; + } else if (state == STATE_DOTDOT) { + builder.addLast(factory.getParentElement()); + lastPos = pos; + state = STATE_PREFIX_START; + } else if (state != STATE_URI + && !(state == STATE_PREFIX_START && c == EOF)) { // ignore trailing slash + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not a valid name character."); + } + 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) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid after index. '/' expected."); + } + break; + + case ':': + if (state == STATE_PREFIX_START) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. Prefix must not be empty"); + } else if (state == STATE_PREFIX) { + if (wasSlash) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Trailing slashes not allowed in prefixes and names."); + } + state = STATE_NAME_START; + // don't reset the lastPos/pos since prefix+name are passed together to the NameResolver + } else if (state != STATE_IDENTIFIER && state != STATE_URI) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid name character"); + } + break; + + case '[': + if (state == STATE_PREFIX || state == STATE_NAME) { + if (wasSlash) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Trailing slashes not allowed in prefixes and names."); + } + state = STATE_INDEX; + name = jcrPath.substring(lastPos, pos - 1); + lastPos = pos; + } else if (state != STATE_IDENTIFIER) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not a valid name character."); + } + break; + + case ']': + if (state == STATE_INDEX) { + try { + index = Integer.parseInt(jcrPath.substring(lastPos, pos - 1)); + } catch (NumberFormatException e) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. NumberFormatException in index: " + jcrPath.substring(lastPos, pos - 1)); + } + if (index < Path.INDEX_DEFAULT) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. Index number invalid: " + index); + } + state = STATE_INDEX_END; + } else if (state != STATE_IDENTIFIER) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not a valid name character."); + } + break; + + case ' ': + if (state == STATE_PREFIX_START || state == STATE_NAME_START) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid name start"); + } else if (state == STATE_INDEX_END) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid after index. '/' expected."); + } else if (state == STATE_DOT || state == STATE_DOTDOT) { + state = STATE_PREFIX; + } + break; + + case '\t': + if (state != STATE_IDENTIFIER) { + String message = String.format("'%s' is not a valid path. Whitespace other than SP (U+0020) not a allowed in a name, but U+%04x was found at position %d.", + jcrPath, (long) rawCharacter, pos - 1); + throw new MalformedPathException(message); + } + case '*': + case '|': + if (state != STATE_IDENTIFIER) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not a valid name character."); + } + 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) { + throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid after index. '/' expected."); + } + } + wasSlash = c == ' '; + } + + if (checkFormat) { + // this was only for checking the format + return null; + } else { + return builder.getPath(); + } + } + + /** + * Check the format of the given jcr path. Note, the neither name nor + * namespace validation (resolution of prefix to URI) is performed and + * therefore will not be detected. + * + * @param jcrPath + * @throws MalformedPathException If the jcrPath is malformed. + */ + public static void checkFormat(String jcrPath) throws MalformedPathException { + try { + // since no path is created -> use default factory + parse(jcrPath, null, null, PathFactoryImpl.getInstance()); + } catch (NamespaceException e) { + // will never occur + } catch (IllegalNameException e) { + // will never occur + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/PathResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/PathResolver.java new file mode 100644 index 00000000000..09c7a68451f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/PathResolver.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.spi.commons.conversion; + +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.NamespaceException; + +/** + * Resolver for JCR paths. + */ +public interface PathResolver { + + /** + * Returns the path object for the given JCR path string. + * + * @param path prefixed JCR path + * @return a Path object. + * @throws MalformedPathException if the JCR path format is invalid. + * @throws IllegalNameException if any of the JCR names contained in the path are invalid. + * @throws NamespaceException if a namespace prefix can not be resolved. + */ + Path getQPath(String path) throws MalformedPathException, IllegalNameException, NamespaceException; + + /** + * Returns the path object for the given JCR path string. + * + * @param path prefixed JCR path + * @param normalizeIdentifier + * @return a Path object. + * @throws MalformedPathException if the JCR path format is invalid. + * @throws IllegalNameException if any of the JCR names contained in the path are invalid. + * @throws NamespaceException if a namespace prefix can not be resolved. + */ + Path getQPath(String path, boolean normalizeIdentifier) throws MalformedPathException, IllegalNameException, NamespaceException; + + /** + * Returns the given JCR path string for the given path object. + * + * @param path a Path object. + * @return a JCR path string + * @throws NamespaceException if a namespace URI can not be resolved + */ + String getJCRPath(Path path) throws NamespaceException; + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/package-info.java new file mode 100644 index 00000000000..1f1034ce774 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.conversion; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/AbstractIdFactory.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/AbstractIdFactory.java new file mode 100644 index 00000000000..d283161481e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/AbstractIdFactory.java @@ -0,0 +1,296 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.identifier; + +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.RepositoryException; +import java.io.Serializable; + +/** + * AbstractIdFactory... + */ +public abstract class AbstractIdFactory implements IdFactory { + + private static final char DELIMITER = '@'; + + //----------------------------------------------------------< IdFactory >--- + /** + * {@inheritDoc} + * @see IdFactory#createNodeId(NodeId, Path) + */ + public NodeId createNodeId(NodeId parentId, Path path) { + try { + return new NodeIdImpl(parentId, path, getPathFactory()); + } catch (RepositoryException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + /** + * {@inheritDoc} + * @see IdFactory#createNodeId(String, Path) + */ + public NodeId createNodeId(String uniqueID, Path path) { + return new NodeIdImpl(uniqueID, path); + } + + /** + * {@inheritDoc} + * @see IdFactory#createNodeId(String) + */ + public NodeId createNodeId(String uniqueID) { + return new NodeIdImpl(uniqueID); + } + + /** + * {@inheritDoc} + * @see IdFactory#createPropertyId(NodeId,Name) + */ + public PropertyId createPropertyId(NodeId parentId, Name propertyName) { + try { + return new PropertyIdImpl(parentId, propertyName, getPathFactory()); + } catch (RepositoryException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + /** + * @see IdFactory#toJcrIdentifier(NodeId) + */ + public String toJcrIdentifier(NodeId nodeId) { + // TODO improve + String uniqueId = nodeId.getUniqueID(); + Path path = nodeId.getPath(); + if (path == null) { + return uniqueId; + } else if (uniqueId == null) { + return DELIMITER + path.toString(); + } else { + StringBuffer bf = new StringBuffer(); + bf.append(Text.escape(uniqueId, DELIMITER)); + bf.append(DELIMITER); + bf.append(path.toString()); + return bf.toString(); + } + } + + /** + * @see IdFactory#fromJcrIdentifier(String) + */ + public NodeId fromJcrIdentifier(String jcrIdentifier) { + // TODO improve + int pos = jcrIdentifier.indexOf(DELIMITER); + switch (pos) { + case -1: + return createNodeId(jcrIdentifier); + case 0: + return createNodeId((String) null, getPathFactory().create(jcrIdentifier.substring(1))); + default: + String uniqueId = Text.unescape(jcrIdentifier.substring(0, pos), DELIMITER); + Path path = getPathFactory().create(jcrIdentifier.substring(pos+1)); + return createNodeId(uniqueId, path); + } + } + + //-------------------------------------------------------------------------- + /** + * Subclassed need to define a PathFactory used to create IDs + * + * @return a implementation of PathFactory. + */ + protected abstract PathFactory getPathFactory(); + + //------------------------------------------------------< Inner classes >--- + + private static abstract class ItemIdImpl implements ItemId, Serializable { + + private final String uniqueID; + private final Path path; + + private transient int hashCode = 0; + + private ItemIdImpl(String uniqueID, Path path) { + if (uniqueID == null && path == null) { + throw new IllegalArgumentException("Only uniqueID or relative path might be null."); + } + this.uniqueID = uniqueID; + this.path = path; + } + + private ItemIdImpl(NodeId parentId, Name name, PathFactory factory) + throws RepositoryException { + if (parentId == null || name == null) { + throw new IllegalArgumentException("Invalid ItemIdImpl: parentId and name must not be null."); + } + this.uniqueID = parentId.getUniqueID(); + Path parentPath = parentId.getPath(); + if (parentPath != null) { + this.path = factory.create(parentPath, name, true); + } else { + this.path = factory.create(name); + } + } + + public abstract boolean denotesNode(); + + public String getUniqueID() { + return uniqueID; + } + + public Path getPath() { + return path; + } + + /** + * ItemIdImpl objects are equal if the have the same uuid and relative path. + * + * @param obj + * @return + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof ItemId) { + ItemId other = (ItemId) obj; + return equals(other); + } + return false; + } + + boolean equals(ItemId other) { + return (uniqueID == null ? other.getUniqueID() == null : uniqueID.equals(other.getUniqueID())) + && (path == null ? other.getPath() == null : path.equals(other.getPath())); + } + + /** + * Returns the hash code of the uuid and the path. The computed hash code + * is memorized for better performance. + * + * @return hash code + * @see Object#hashCode() + */ + @Override + public int hashCode() { + // since the ItemIdImpl is immutable, store the computed hash code value + if (hashCode == 0) { + int result = 17; + result = 37 * result + (uniqueID != null ? uniqueID.hashCode() : 0); + result = 37 * result + (path != null ? path.hashCode() : 0); + hashCode = result; + } + return hashCode; + } + + /** + * Combination of uuid and relative path + * + * @return + */ + @Override + public String toString() { + StringBuffer b = new StringBuffer(); + if (uniqueID != null) { + b.append(uniqueID); + } + if (path != null) { + b.append(path.toString()); + } + return b.toString(); + } + } + + private static class NodeIdImpl extends ItemIdImpl implements NodeId { + + private static final long serialVersionUID = -360276648861146631L; + + public NodeIdImpl(String uniqueID) { + super(uniqueID, null); + } + + public NodeIdImpl(String uniqueID, Path path) { + super(uniqueID, path); + } + + public NodeIdImpl(NodeId parentId, Path path, PathFactory factory) + throws RepositoryException { + super(parentId.getUniqueID(), (parentId.getPath() != null) ? factory.create(parentId.getPath(), path, true) : path); + } + + @Override + public boolean denotesNode() { + return true; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof NodeId) { + return super.equals((NodeId)obj); + } + return false; + } + } + + private static class PropertyIdImpl extends ItemIdImpl implements PropertyId, Serializable { + + private static final long serialVersionUID = -1953124047770776444L; + + private final NodeId parentId; + + private PropertyIdImpl(NodeId parentId, Name name, PathFactory factory) + throws RepositoryException { + super(parentId, name, factory); + this.parentId = parentId; + } + + @Override + public boolean denotesNode() { + return false; + } + + public NodeId getParentId() { + return parentId; + } + + public Name getName() { + return getPath().getName(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof PropertyId) { + return super.equals((PropertyId)obj); + } + return false; + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/IdFactoryImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/IdFactoryImpl.java new file mode 100644 index 00000000000..a03121c06e4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/IdFactoryImpl.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.spi.commons.identifier; + +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; + +/** + * IdFactoryImpl... + */ +public final class IdFactoryImpl extends AbstractIdFactory { + + private static IdFactory INSTANCE; + + private IdFactoryImpl() { + } + + public static IdFactory getInstance() { + if (INSTANCE == null) { + INSTANCE = new IdFactoryImpl(); + } + return IdFactoryImpl.INSTANCE; + } + + /** + * @see org.apache.jackrabbit.spi.commons.identifier.AbstractIdFactory#getPathFactory() + */ + @Override + protected PathFactory getPathFactory() { + return PathFactoryImpl.getInstance(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/package-info.java new file mode 100644 index 00000000000..0cf215b7bdd --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/identifier/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.identifier; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/BoundedIterator.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/BoundedIterator.java new file mode 100644 index 00000000000..aea3970b2a8 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/BoundedIterator.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.spi.commons.iterator; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Implements a bounded iterator which only returns a maximum number of element from an underlying iterator + * starting at a given offset. + * + * @param element type + */ +public class BoundedIterator implements Iterator { + private final Iterator iterator; + private final long offset; + private final long max; + private int pos; + private T next; + + /** + * Create a new bounded iterator 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 -1 for all + * @param iterator the underlying iterator + * @throws IllegalArgumentException if offset is negative + */ + public BoundedIterator(long offset, long max, Iterator iterator) { + if (offset < 0) { + throw new IllegalArgumentException("Offset must not be negative"); + } + + this.iterator = iterator; + this.offset = offset; + this.max = max; + } + + /** + * Factory for creating a bounded iterator. + * @see #BoundedIterator(long, long, java.util.Iterator) + * + * @param offset offset to start iteration at. Must be non negative + * @param max maximum elements this iterator should return. Set to -1 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 == 0 && max == -1) { + return iterator; + } + else { + return new BoundedIterator(offset, max, iterator); + } + } + + public boolean hasNext() { + if (next == null) { + fetchNext(); + } + + return next != null; + } + + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + return consumeNext(); + } + + 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; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Iterators.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Iterators.java new file mode 100644 index 00000000000..2dc4267d88e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Iterators.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.jackrabbit.spi.commons.iterator; + +import org.apache.commons.collections.iterators.ArrayIterator; +import org.apache.commons.collections.iterators.EmptyIterator; +import org.apache.commons.collections.iterators.FilterIterator; +import org.apache.commons.collections.iterators.IteratorChain; +import org.apache.commons.collections.iterators.SingletonIterator; +import org.apache.commons.collections.iterators.TransformIterator; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Utility class containing type safe adapters for some of the iterators of + * commons-collections. + */ +public final class Iterators { + + private Iterators() { + super(); + } + + /** + * Returns an iterator containing the single element element of + * type T. + * + * @param + * @param element + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator singleton(T element) { + return new SingletonIterator(element); + } + + /** + * Returns an empty iterator of type T. + * + * @param + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator empty() { + return EmptyIterator.INSTANCE; + } + + /** + * Returns an iterator for the concatenation of iterator1 and + * iterator2. + * + * @param + * @param iterator1 + * @param iterator2 + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator iteratorChain(Iterator iterator1, Iterator iterator2) { + return new IteratorChain(iterator1, iterator2); + } + + /** + * Returns an iterator for the concatenation of all the given iterators. + * + * @param + * @param iterators + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator iteratorChain(Iterator[] iterators) { + return new IteratorChain(iterators); + } + + /** + * Returns an iterator for the concatenation of all the given iterators. + * + * @param + * @param iterators + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator iteratorChain(Collection iterators) { + return new IteratorChain(iterators); + } + + /** + * Returns an iterator for elements of an array of values. + * + * @param + * @param values the array to iterate over. + * @param from the index to start iterating at. + * @param to the index to finish iterating at. + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator arrayIterator(T[] values, int from, int to) { + return new ArrayIterator(values, from, to); + } + + /** + * Returns an iterator with elements from an original iterator where the + * given predicate matches removed. + * + * @param + * @param iterator + * @param predicate + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator filterIterator(Iterator iterator, + final Predicate predicate) { + + return new FilterIterator(iterator, new org.apache.commons.collections.Predicate() { + public boolean evaluate(Object object) { + return predicate.evaluate((T) object); + } + }); + } + + /** + * Returns an iterator with elements of an original iterator transformed by + * a transformer. + * + * @param + * @param + * @param + * @param iterator + * @param transformer + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator transformIterator(Iterator iterator, + final Transformer transformer) { + + return new TransformIterator(iterator, new org.apache.commons.collections.Transformer() { + public Object transform(Object input) { + return transformer.transform((S) input); + } + }); + } + + /** + * Returns an iterator of {@link Property} from a {@link PropertyIterator}. + * + * @param propertyIterator + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator properties(PropertyIterator propertyIterator) { + return propertyIterator; + } + + /** + * Returns an iterator of {@link Node} from a {@link NodeIterator}. + * @param nodeIterator + * @return + */ + @SuppressWarnings("unchecked") + public static Iterator nodes(NodeIterator nodeIterator) { + return nodeIterator; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Predicate.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Predicate.java new file mode 100644 index 00000000000..c02421e4373 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Predicate.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.spi.commons.iterator; + +/** + * Type safe counter part of {@link org.apache.commons.collections.Predicate}. + * + * @param type of values this predicate is defined on + */ +public interface Predicate { + + /** + * Use the specified parameter to perform a test that returns true or false. + * + * @param arg the predicate to evaluate, should not be changed + * @return true or false + */ + public boolean evaluate(T arg); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Predicates.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Predicates.java new file mode 100644 index 00000000000..63ec629aa2e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Predicates.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.spi.commons.iterator; + +/** + * Utility class containing pre defined {@link Predicate}s + */ +public final class Predicates { + + /** + * A predicate which is always true + */ + public static final Predicate TRUE = new Predicate() { + public boolean evaluate(Object arg) { + return true; + } + }; + + /** + * A predicate which is always false + */ + public static final Predicate FALSE = new Predicate() { + public boolean evaluate(Object arg) { + return false; + } + }; + + private Predicates() { + // no instances allowed + } + + /** + * A predicate which is always true + * @param + * @return + */ + @SuppressWarnings("unchecked") + public static Predicate TRUE() { + return TRUE; + } + + /** + * A predicate which is always false + * @param + * @return + */ + @SuppressWarnings("unchecked") + public static Predicate FALSE() { + return FALSE; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Transformer.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Transformer.java new file mode 100644 index 00000000000..058605b5cfc --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/Transformer.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.spi.commons.iterator; + +/** + * Type safe counter part of {@link org.apache.commons.collections.Transformer}. + * + * @param argument type to transform from + * @param result type to transform to + */ +public interface Transformer { + + /** + * Transforms the input object (leaving it unchanged) into some output object. + * + * @param argument the object to be transformed, should be left unchanged + * @return a transformed object + */ + public R transform(A argument); + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/package-info.java new file mode 100644 index 00000000000..bb7c8070dbc --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/iterator/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.iterator; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/lock/Locked.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/lock/Locked.java new file mode 100644 index 00000000000..b9447957c5c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/lock/Locked.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.lock; + +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameException; + +import javax.jcr.RepositoryException; +import javax.jcr.Node; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Session; +import javax.jcr.Repository; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.Event; +import javax.jcr.lock.Lock; +import javax.jcr.lock.LockException; + +/** + * Locked is a utility to synchronize modifications on a lockable + * node. The modification is applied while the lock on the node is held, thus + * ensuring that the modification will never fail with an {@link + * javax.jcr.InvalidItemStateException}. This utility can be used with any + * JCR Repository, not just Jackrabbit. + *

        + * The following example shows how this utility can be used to implement + * a persistent counter: + *

        + * Node counter = ...;
        + * long nextValue = ((Long) new Locked() {
        + *     protected Object run(Node counter) throws RepositoryException {
        + *         Property seqProp = counter.getProperty("value");
        + *         long value = seqProp.getLong();
        + *         seqProp.setValue(++value);
        + *         seqProp.save();
        + *         return new Long(value);
        + *     }
        + * }.with(counter, false)).longValue();
        + * 
        + * If you specify a timeout you need to check the return value + * whether the run method could be executed within the timeout + * period: + *
        + * Node counter = ...;
        + * Object ret = new Locked() {
        + *     protected Object run(Node counter) throws RepositoryException {
        + *         Property seqProp = counter.getProperty("value");
        + *         long value = seqProp.getLong();
        + *         seqProp.setValue(++value);
        + *         seqProp.save();
        + *         return new Long(value);
        + *     }
        + * }.with(counter, false);
        + * if (ret == Locked.TIMED_OUT) {
        + *     // do whatever you think is appropriate in this case
        + * } else {
        + *     // get the value
        + *     long nextValue = ((Long) ret).longValue();
        + * }
        + * 
        + * + * @deprecated Use org.apache.jackrabbit.util.Locked instead. + */ +public abstract class Locked { + + /** + * Object returned when timeout is reached without being able to call + * {@link #run} while holding the lock. + */ + public static final Object TIMED_OUT = new Object(); + + /** + * Executes {@link #run} while the lock on lockable is held. + * This method will block until {@link #run} is executed while holding the + * lock on node lockable. + * + * @param lockable a lockable node. + * @param isDeep true if lockable will be locked + * deep. + * @return the object returned by {@link #run}. + * @throws IllegalArgumentException if lockable is not + * mix:lockable. + * @throws RepositoryException if {@link #run} throws an exception. + * @throws InterruptedException if this thread is interrupted while waiting + * for the lock on node lockable. + */ + public Object with(Node lockable, boolean isDeep) + throws RepositoryException, InterruptedException { + return with(lockable, isDeep, Long.MAX_VALUE); + } + + /** + * Executes the method {@link #run} within the scope of a lock held on + * lockable. + * + * @param lockable the node where the lock is obtained from. + * @param isDeep true if lockable will be locked + * deep. + * @param timeout time in milliseconds to wait at most to aquire the lock. + * @return the object returned by {@link #run} or {@link #TIMED_OUT} if the + * lock on lockable could not be aquired within the + * specified timeout. + * @throws IllegalArgumentException if timeout is negative or + * lockable is not + * mix:lockable. + * @throws RepositoryException if {@link #run} throws an exception. + * @throws UnsupportedRepositoryOperationException + * if this repository does not support + * locking. + * @throws InterruptedException if this thread is interrupted while + * waiting for the lock on node + * lockable. + */ + public Object with(Node lockable, boolean isDeep, long timeout) + throws UnsupportedRepositoryOperationException, RepositoryException, InterruptedException { + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be >= 0"); + } + + Session session = lockable.getSession(); + NamePathResolver resolver = new DefaultNamePathResolver(session); + + Lock lock; + EventListener listener = null; + try { + // check whether the lockable can be locked at all + if (!lockable.isNodeType(resolver.getJCRName(NameConstants.MIX_LOCKABLE))) { + throw new IllegalArgumentException("Node is not lockable"); + } + + lock = tryLock(lockable, isDeep); + if (lock != null) { + return runAndUnlock(lock); + } + + if (timeout == 0) { + return TIMED_OUT; + } + + long timelimit; + if (timeout == Long.MAX_VALUE) { + timelimit = Long.MAX_VALUE; + } else { + timelimit = System.currentTimeMillis() + timeout; + } + + // node is locked by other session -> register event listener if possible + if (isObservationSupported(session)) { + ObservationManager om = session.getWorkspace().getObservationManager(); + listener = new EventListener() { + public void onEvent(EventIterator events) { + synchronized (this) { + this.notify(); + } + } + }; + om.addEventListener(listener, Event.PROPERTY_REMOVED, + lockable.getPath(), false, null, null, true); + } + + // now keep trying to aquire the lock + // using 'this' as a monitor allows the event listener to notify + // the current thread when the lockable node is possibly unlocked + for (; ;) { + synchronized (this) { + lock = tryLock(lockable, isDeep); + if (lock != null) { + return runAndUnlock(lock); + } else { + // check timeout + if (System.currentTimeMillis() > timelimit) { + return TIMED_OUT; + } + if (listener != null) { + // event listener *should* wake us up, however + // there is a chance that removal of the lockOwner + // property is notified before the node is acutally + // unlocked. therefore we use a safety net to wait + // at most 1000 millis. + this.wait(Math.min(1000, timeout)); + } else { + // repository does not support observation + // wait at most 50 millis then retry + this.wait(Math.min(50, timeout)); + } + } + } + } + } catch (NameException e) { + throw new RepositoryException(e); + } finally { + if (listener != null) { + session.getWorkspace().getObservationManager().removeEventListener(listener); + } + } + } + + /** + * This method is executed while holding the lock. + * @param node The Node on which the lock is placed. + * @return an object which is then returned by {@link #with with()}. + * @throws RepositoryException if an error occurs. + */ + protected abstract Object run(Node node) throws RepositoryException; + + /** + * Executes {@link #run} and unlocks the lockable node in any case, even + * when an exception is thrown. + * + * @param lock The Lock to unlock in any case before returning. + * + * @return the object returned by {@link #run}. + * @throws RepositoryException if an error occurs. + */ + private Object runAndUnlock(Lock lock) throws RepositoryException { + try { + return run(lock.getNode()); + } finally { + lock.getNode().unlock(); + } + } + + /** + * Tries to aquire a session scoped lock on lockable. + * + * @param lockable the lockable node + * @param isDeep true if the lock should be deep + * @return The Lock or null if the + * lockable cannot be locked. + * @throws UnsupportedRepositoryOperationException + * if this repository does not support locking. + * @throws RepositoryException if an error occurs + */ + private static Lock tryLock(Node lockable, boolean isDeep) + throws UnsupportedRepositoryOperationException, RepositoryException { + try { + return lockable.lock(isDeep, true); + } catch (LockException e) { + // locked by some other session + } + return null; + } + + /** + * Returns true if the repository supports observation. + * + * @param s a session of the repository. + * @return true if the repository supports observation. + */ + private static boolean isObservationSupported(Session s) { + return "true".equalsIgnoreCase(s.getRepository().getDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED)); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/lock/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/lock/package-info.java new file mode 100644 index 00000000000..e38edbba835 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/lock/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.lock; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/AbstractLogger.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/AbstractLogger.java new file mode 100644 index 00000000000..1493e4f3a59 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/AbstractLogger.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.spi.commons.logging; + +import javax.jcr.RepositoryException; + +/** + * Common base class for all log wrappers of SPI entities. + */ +public class AbstractLogger { + + /** + * The {@link LogWriter} used by this instance for persisting log messages. + */ + protected final LogWriter writer; + + /** + * Create a new instance of this log wrapper which uses writer + * for persisting log messages. + * @param writer + */ + public AbstractLogger(LogWriter writer) { + super(); + this.writer = writer; + } + + /** + * Execute a thunk of a method which might throw a {@link RepositoryException}. The call + * is logged to {@link #writer} right before it is actually performed and after it returns. Any + * exception thrown by the call is logged before it is re-thrown. + * @param thunk thunk of the method to execute + * @param methodName the name of the method + * @param args the arguments passed to the method + * @return the value returned from executing the thunk + * @throws RepositoryException if executing the thunk throws an Exception the + * exception is re-thrown. + */ + protected Object execute(Callable thunk, String methodName, Object[] args) throws RepositoryException { + writer.enter(methodName, args); + Object result = null; + try { + result = thunk.call(); + writer.leave(methodName, args, result); + return result; + } + catch (RepositoryException e) { + writer.error(methodName, args, e); + throw e; + } + catch (RuntimeException e) { + writer.error(methodName, args, e); + throw e; + } + } + + /** + * Execute a thunk of a method which does not throw any checked exception. The + * call is logged to {@link #writer} right before it is actually performed and after it returns. + * @param thunk thunk of the method to execute + * @param methodName the name of the method + * @param args the arguments passed to the method + * @return the value returned from executing the thunk + */ + protected Object execute(SafeCallable thunk, String methodName, Object[] args) { + writer.enter(methodName, args); + Object result; + try { + result = thunk.call(); + writer.leave(methodName, args, result); + return result; + } + catch (RuntimeException e) { + writer.error(methodName, args, e); + throw e; + } + } + + /** + * Type of thunk used in {@link AbstractLogger#execute(Callable, String, Object[])} + */ + protected interface Callable { + public Object call() throws RepositoryException; + } + + /** + * Type of thunk used in {@link AbstractLogger#execute(SafeCallable, String, Object[])} + */ + protected interface SafeCallable { + public Object call(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/BatchLogger.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/BatchLogger.java new file mode 100644 index 00000000000..d831607010c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/BatchLogger.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.spi.commons.logging; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.Tree; + +/** + * Log wrapper for a {@link Batch}. + */ +public class BatchLogger extends AbstractLogger implements Batch { + private final Batch batch; + + /** + * Create a new instance for the given batch which uses + * writer for persisting log messages. + * @param batch + * @param writer + */ + public BatchLogger(Batch batch, LogWriter writer) { + super(writer); + this.batch = batch; + } + + /** + * @return the wrapped Batch + */ + public Batch getBatch() { + return batch; + } + + // -----------------------------------------------------< Batch >--- + + public void addNode(final NodeId parentId, final Name nodeName, final Name nodetypeName, final String uuid) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + batch.addNode(parentId, nodeName, nodetypeName, uuid); + return null; + }}, "addNode(NodeId, Name, Name, String)", new Object[]{parentId, nodeName, nodetypeName, uuid}); + } + + public void addProperty(final NodeId parentId, final Name propertyName, final QValue value) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + batch.addProperty(parentId, propertyName, value); + return null; + }}, "addProperty(NodeId, Name, QValue)", new Object[]{parentId, propertyName, value}); + } + + public void addProperty(final NodeId parentId, final Name propertyName, final QValue[] values) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + batch.addProperty(parentId, propertyName, values); + return null; + }}, "addProperty(NodeId, Name, QValue[])", new Object[]{parentId, propertyName, values}); + } + + public void setValue(final PropertyId propertyId, final QValue value) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + batch.setValue(propertyId, value); + return null; + }}, "setValue(PropertyId, QValue)", new Object[]{propertyId, value}); + } + + public void setValue(final PropertyId propertyId, final QValue[] values) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + batch.setValue(propertyId, values); + return null; + }}, "setValue(PropertyId, QValue[])", new Object[]{propertyId, values}); + } + + public void remove(final ItemId itemId) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + batch.remove(itemId); + return null; + }}, "remove(ItemId)", new Object[]{itemId}); + } + + public void reorderNodes(final NodeId parentId, final NodeId srcNodeId, final NodeId beforeNodeId) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + batch.reorderNodes(parentId, srcNodeId, beforeNodeId); + return null; + }}, "reorderNodes(NodeId, NodeId, NodeId)", new Object[]{parentId, srcNodeId, beforeNodeId}); + } + + public void setMixins(final NodeId nodeId, final Name[] mixinNodeTypeNames) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + batch.setMixins(nodeId, mixinNodeTypeNames); + return null; + }}, "setMixins(NodeId, Name[])", new Object[]{nodeId, mixinNodeTypeNames}); + } + + public void setPrimaryType(final NodeId nodeId, final Name primaryNodeTypeName) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + batch.setPrimaryType(nodeId, primaryNodeTypeName); + return null; + }}, "setPrimaryType(NodeId, Name)", new Object[]{nodeId, primaryNodeTypeName}); + } + + public void move(final NodeId srcNodeId, final NodeId destParentNodeId, final Name destName) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + batch.move(srcNodeId, destParentNodeId, destName); + return null; + }}, "move(NodeId, NodeId, Name)", new Object[]{srcNodeId, destParentNodeId, destName}); + } + + @Override + public void setTree(final NodeId parentId, final Tree contentTree) + throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + batch.setTree(parentId, contentTree); + return null; + }}, "setTree(NodeId, Tree)", new Object[]{parentId, contentTree}); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/IdFactoryLogger.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/IdFactoryLogger.java new file mode 100644 index 00000000000..4e0857642cb --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/IdFactoryLogger.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.spi.commons.logging; + +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; + +/** + * Log wrapper for an {@link IdFactory}. + */ +public class IdFactoryLogger extends AbstractLogger implements IdFactory { + private final IdFactory idFactory; + + /** + * Create a new instance for the given idFactory which uses + * writer for persisting log messages. + * @param idFactory + * @param writer + */ + public IdFactoryLogger(IdFactory idFactory, LogWriter writer) { + super(writer); + this.idFactory = idFactory; + } + + /** + * @return the wrapped IdFactory + */ + public IdFactory getIdFactory() { + return idFactory; + } + + public PropertyId createPropertyId(final NodeId parentId, final Name propertyName) { + return (PropertyId) execute(new SafeCallable() { + public Object call() { + return idFactory.createPropertyId(parentId, propertyName); + }}, "createPropertyId(NodeId, Name)", new Object[]{parentId, propertyName}); + } + + public NodeId createNodeId(final NodeId parentId, final Path path) { + return (NodeId) execute(new SafeCallable() { + public Object call() { + return idFactory.createNodeId(parentId, path); + }}, "createNodeId(NodeId, Path)", new Object[]{parentId, path}); + } + + public NodeId createNodeId(final String uniqueID, final Path path) { + return (NodeId) execute(new SafeCallable() { + public Object call() { + return idFactory.createNodeId(uniqueID, path); + }}, "createNodeId(String, Path)", new Object[]{uniqueID, path}); + } + + public NodeId createNodeId(final String uniqueID) { + return (NodeId) execute(new SafeCallable() { + public Object call() { + return idFactory.createNodeId(uniqueID); + }}, "createNodeId(String)", new Object[]{uniqueID}); + } + + public String toJcrIdentifier(final NodeId nodeId) { + return (String) execute(new SafeCallable() { + public Object call() { + return idFactory.toJcrIdentifier(nodeId); + }}, "toJcrIdentifier(String)", new Object[]{nodeId}); + } + + public NodeId fromJcrIdentifier(final String jcrIdentifier) { + return (NodeId) execute(new SafeCallable() { + public Object call() { + return idFactory.fromJcrIdentifier(jcrIdentifier); + }}, "fromJcrIdentifier(String)", new Object[]{jcrIdentifier}); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/LogWriter.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/LogWriter.java new file mode 100644 index 00000000000..2be297b110a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/LogWriter.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.spi.commons.logging; + +/** + * A LogWriter provides methods for persisting log messages by some implementation + * specific means. Implementations must be thread safe. That is, implementations must + * cope with any thread concurrently calling any method of this interface at any time. + */ +public interface LogWriter { + + /** + * Implementation specific time stamp which is logged along with each log + * message. The values returned by this method should be monotone with respect + * to the time they represent. + * @return + */ + public long systemTime(); + + /** + * Called right before a method of a SPI entity is called. + * @param methodName name of the method which a about to be called + * @param args arguments passed to the methods which is about to be called. + */ + public void enter(String methodName, Object[] args); + + /** + * Called right after a method of a SPI entity has been called if no + * exception was thrown. + * @param methodName name of the method which has been called + * @param args arguments passed to the method which has been called + * @param result return value of the method which has been called + */ + public void leave(String methodName, Object[] args, Object result); + + /** + * Called right after a method of a SPI entity has been called and an + * exception was thrown. + * @param methodName name of the method which has been called + * @param args arguments passed to the method which has been called + * @param e exception which was thrown by the method which has + * been called + */ + public void error(String methodName, Object[] args, Exception e); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/LogWriterProvider.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/LogWriterProvider.java new file mode 100644 index 00000000000..bf30ebd3496 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/LogWriterProvider.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.spi.commons.logging; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; + +/** + * LogWriterProvider instances provide {@link LogWriter}s for the individual + * SPI entities. + */ +public interface LogWriterProvider { + + /** + * @param service + * @return A LogWriter for logging calls to service. + */ + public LogWriter getLogWriter(RepositoryService service); + + /** + * @param nameFactory + * @return A LogWriter for logging calls to nameFactory. + */ + public LogWriter getLogWriter(NameFactory nameFactory); + + /** + * @param pathFactory + * @return A LogWriter for logging calls to pathFactory. + */ + public LogWriter getLogWriter(PathFactory pathFactory); + + /** + * @param idFactory + * @return A LogWriter for logging calls to idFactory. + */ + public LogWriter getLogWriter(IdFactory idFactory); + + /** + * @param qValueFactory + * @return A LogWriter for logging calls to qValueFactory. + */ + public LogWriter getLogWriter(QValueFactory qValueFactory); + + /** + * @param sessionInfo + * @return A LogWriter for logging calls to sessionInfo. + */ + public LogWriter getLogWriter(SessionInfo sessionInfo); + + /** + * @param batch + * @return A LogWriter for logging calls to batch. + */ + public LogWriter getLogWriter(Batch batch); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/NameFactoryLogger.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/NameFactoryLogger.java new file mode 100644 index 00000000000..5ec6a19d867 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/NameFactoryLogger.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.spi.commons.logging; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; + +/** + * Log wrapper for a {@link NameFactory}. + */ +public class NameFactoryLogger extends AbstractLogger implements NameFactory { + private final NameFactory nameFactory; + + /** + * Create a new instance for the given nameFactory which uses + * writer for persisting log messages. + * @param nameFactory + * @param writer + */ + public NameFactoryLogger(NameFactory nameFactory, LogWriter writer) { + super(writer); + this.nameFactory = nameFactory; + } + + /** + * @return the wrapped NameFactory + */ + public NameFactory getNameFactory() { + return nameFactory; + } + + public Name create(final String namespaceURI, final String localName) { + return (Name) execute(new SafeCallable() { + public Object call() { + return nameFactory.create(namespaceURI, localName); + }}, "create(String, String)", new Object[]{namespaceURI, localName}); + } + + public Name create(final String nameString) { + return (Name) execute(new SafeCallable() { + public Object call() { + return nameFactory.create(nameString); + }}, "create(String)", new Object[]{nameString}); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/PathFactoryLogger.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/PathFactoryLogger.java new file mode 100644 index 00000000000..e6a0c5fc349 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/PathFactoryLogger.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.spi.commons.logging; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.Path.Element; + +/** + * Log wrapper for a {@link PathFactory}. + */ +public class PathFactoryLogger extends AbstractLogger implements PathFactory { + private final PathFactory pathFactory; + + /** + * Create a new instance for the given pathFactory which uses + * writer for persisting log messages. + * @param pathFactory + * @param writer + */ + public PathFactoryLogger(PathFactory pathFactory, LogWriter writer) { + super(writer); + this.pathFactory = pathFactory; + } + + /** + * @return the wrapped PathFactory + */ + public PathFactory getPathFactory() { + return pathFactory; + } + + public Path create(final Path parent, final Path relPath, final boolean normalize) + throws RepositoryException { + + return (Path) execute(new Callable() { + public Object call() throws RepositoryException { + return pathFactory.create(parent, relPath, normalize); + }}, "create(Path, Path, boolean)", new Object[]{parent, relPath, Boolean.valueOf(normalize)}); + } + + public Path create(final Path parent, final Name name, final boolean normalize) + throws RepositoryException { + + return (Path) execute(new Callable() { + public Object call() throws RepositoryException { + return pathFactory.create(parent, name, normalize); + }}, "create(Path, Name, boolean)", new Object[]{parent, name, Boolean.valueOf(normalize)}); + } + + public Path create(final Path parent, final Name name, final int index, final boolean normalize) + throws RepositoryException { + + return (Path) execute(new Callable() { + public Object call() throws RepositoryException { + return pathFactory.create(parent, name, index, normalize); + }}, "create(Path, Name, int, boolean)", new Object[]{parent, name, new Integer(index), + Boolean.valueOf(normalize)}); + } + + public Path create(final Name name) { + return (Path) execute(new SafeCallable() { + public Object call() { + return pathFactory.create(name); + }}, "create(Name)", new Object[]{name}); + } + + public Path create(final Name name, final int index) { + return (Path) execute(new SafeCallable() { + public Object call() { + return pathFactory.create(name, index); + }}, "create(Name, int)", new Object[]{name, new Integer(index)}); + } + + public Path create(final Element element) { + return (Path) execute(new SafeCallable() { + public Object call() { + return pathFactory.create(element); + }}, "create(Element)", new Object[]{element}); + } + + public Path create(final Element[] elements) { + return (Path) execute(new SafeCallable() { + public Object call() { + return pathFactory.create(elements); + }}, "create(Element[])", new Object[]{elements}); + } + + public Path create(final String pathString) { + return (Path) execute(new SafeCallable() { + public Object call() { + return pathFactory.create(pathString); + }}, "create(String)", new Object[]{pathString}); + } + + public Element createElement(final Name name) { + return (Element) execute(new SafeCallable() { + public Object call() { + return pathFactory.createElement(name); + }}, "createElement(Name)", new Object[]{name}); + } + + public Element createElement(final Name name, final int index) { + return (Element) execute(new SafeCallable() { + public Object call() { + return pathFactory.createElement(name, index); + }}, "createElement(Name)", new Object[]{name, new Integer(index)}); + } + + public Element createElement(final String identifier) throws IllegalArgumentException { + return (Element) execute(new SafeCallable() { + public Object call() { + return pathFactory.createElement(identifier); + }}, "createElement(String)", new Object[]{identifier}); + } + + public Element getCurrentElement() { + return (Element) execute(new SafeCallable() { + public Object call() { + return pathFactory.getCurrentElement(); + }}, "getCurrentElement()", new Object[]{}); + } + + public Element getParentElement() { + return (Element) execute(new SafeCallable() { + public Object call() { + return pathFactory.getParentElement(); + }}, "getParentElement()", new Object[]{}); + } + + public Element getRootElement() { + return (Element) execute(new SafeCallable() { + public Object call() { + return pathFactory.getRootElement(); + }}, "getRootElement()", new Object[]{}); + } + + public Path getRootPath() { + return (Path) execute(new SafeCallable() { + public Object call() { + return pathFactory.getRootPath(); + }}, "getRootPath()", new Object[]{}); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/QValueFactoryLogger.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/QValueFactoryLogger.java new file mode 100644 index 00000000000..2d8c1fa22ca --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/QValueFactoryLogger.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.spi.commons.logging; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; +import java.net.URI; +import java.math.BigDecimal; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; + +/** + * Log wrapper for a {@link QValueFactory}. + */ +public class QValueFactoryLogger extends AbstractLogger implements QValueFactory { + private final QValueFactory qValueFactory; + + /** + * Create a new instance for the given qValueFactory which uses + * writer for persisting log messages. + * @param qValueFactory + * @param writer + */ + public QValueFactoryLogger(QValueFactory qValueFactory, LogWriter writer) { + super(writer); + this.qValueFactory = qValueFactory; + } + + /** + * @return the wrapped QValueFactory + */ + public QValueFactory getQValueFactory() { + return qValueFactory; + } + + public QValue create(final String value, final int type) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value, type); + }}, "create(String, int)", new Object[]{value, new Integer(type)}); + } + + public QValue create(final Calendar value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(Calendar)", new Object[]{value}); + } + + public QValue create(final double value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(double)", new Object[]{new Double(value)}); + } + + public QValue create(final long value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(long)", new Object[]{new Long(value)}); + } + + public QValue create(final boolean value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(boolean)", new Object[]{Boolean.valueOf(value)}); + } + + public QValue create(final Name value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(Name)", new Object[]{value}); + } + + public QValue create(final Path value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(Path)", new Object[]{value}); + } + + public QValue create(final URI value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(URI)", new Object[]{value}); + } + + public QValue create(final BigDecimal value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(BigDecimal)", new Object[]{value}); + } + + public QValue create(final byte[] value) throws RepositoryException { + return (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.create(value); + }}, "create(byte[])", new Object[]{value}); + } + + public QValue create(final InputStream value) throws RepositoryException, IOException { + final String methodName = "create(InputStream)"; + final Object[] args = new Object[]{value}; + final IOException[] ex = new IOException[1]; + + QValue result = (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + try { + return qValueFactory.create(value); + } + catch (IOException e) { + ex[0] = e; + return null; + } + }}, methodName, args); + + if (ex[0] != null) { + throw ex[0]; + } + + return result; + } + + public QValue create(final File value) throws RepositoryException, IOException { + final String methodName = "create(File)"; + final Object[] args = new Object[]{value}; + final IOException[] ex = new IOException[1]; + + QValue result = (QValue) execute(new Callable() { + public Object call() throws RepositoryException { + try { + return qValueFactory.create(value); + } + catch (IOException e) { + ex[0] = e; + return null; + } + }}, methodName, args); + + if (ex[0] != null) { + throw ex[0]; + } + + return result; + } + + public QValue[] computeAutoValues(final QPropertyDefinition propertyDefinition) throws RepositoryException { + return (QValue[]) execute(new Callable() { + public Object call() throws RepositoryException { + return qValueFactory.computeAutoValues(propertyDefinition); + }}, "computeAutoValues(QPropertyDefinition)", new Object[]{propertyDefinition}); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/RepositoryServiceLogger.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/RepositoryServiceLogger.java new file mode 100644 index 00000000000..fc9a15d6f7a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/RepositoryServiceLogger.java @@ -0,0 +1,838 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.logging; + +import java.io.InputStream; +import java.util.Iterator; +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeExistsException; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.Tree; + +/** + * Log wrapper for a {@link RepositoryService}. + */ +public class RepositoryServiceLogger extends AbstractLogger implements RepositoryService { + private final RepositoryService service; + + /** + * Create a new instance for the given service which uses + * writer for persisting log messages. + * @param service + * @param writer + */ + public RepositoryServiceLogger(RepositoryService service, LogWriter writer) { + super(writer); + this.service = service; + } + + /** + * @return the wrapped RepositoryService + */ + public RepositoryService getRepositoryService() { + return service; + } + + public NameFactory getNameFactory() throws RepositoryException { + return (NameFactory) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getNameFactory(); + } + }, "getNameFactory()", new Object[]{}); + } + + public PathFactory getPathFactory() throws RepositoryException { + return (PathFactory) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getPathFactory(); + } + }, "getPathFactory()", new Object[]{}); + } + + public IdFactory getIdFactory() throws RepositoryException { + return (IdFactory) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getIdFactory(); + } + }, "getIdFactory()", new Object[]{}); + } + + public QValueFactory getQValueFactory() throws RepositoryException { + return (QValueFactory) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getQValueFactory(); + } + }, "getQValueFactory()", new Object[]{}); + } + + public Map getRepositoryDescriptors() throws RepositoryException { + return (Map) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getRepositoryDescriptors(); + } + }, "getRepositoryDescriptors()", new Object[]{}); + } + + public ItemInfoCache getItemInfoCache(final SessionInfo sessionInfo) throws RepositoryException { + return (ItemInfoCache) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getItemInfoCache(sessionInfo); + } + }, "getItemInfoCache(SessionInfo)", new Object[]{sessionInfo}); + } + + public SessionInfo obtain(final Credentials credentials, final String workspaceName) + throws RepositoryException { + + return (SessionInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.obtain(credentials, workspaceName); + } + }, "obtain(Credentials, String)", new Object[]{credentials, workspaceName}); + } + + public SessionInfo obtain(final SessionInfo sessionInfo, final String workspaceName) + throws RepositoryException { + + return (SessionInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.obtain(unwrap(sessionInfo), workspaceName); + } + }, "obtain(SessionInfo, String)", new Object[]{unwrap(sessionInfo), workspaceName}); + } + + public SessionInfo impersonate(final SessionInfo sessionInfo, final Credentials credentials) + throws RepositoryException { + + return (SessionInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.impersonate(unwrap(sessionInfo), credentials); + } + }, "impersonate(SessionInfo, Credentials)", new Object[]{unwrap(sessionInfo), credentials}); + } + + public void dispose(final SessionInfo sessionInfo) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + service.dispose(unwrap(sessionInfo)); + return null; + } + }, "dispose(SessionInfo)", new Object[]{unwrap(sessionInfo)}); + } + + public String[] getWorkspaceNames(final SessionInfo sessionInfo) throws RepositoryException { + return (String[]) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getWorkspaceNames(unwrap(sessionInfo)); + } + }, "getWorkspaceNames(SessionInfo)", new Object[]{unwrap(sessionInfo)}); + } + + public boolean isGranted(final SessionInfo sessionInfo, final ItemId itemId, final String[] actions) + throws RepositoryException { + + return (Boolean) execute(new Callable() { + public Object call() throws RepositoryException { + return Boolean.valueOf(service.isGranted(unwrap(sessionInfo), itemId, actions)); + } + }, "isGranted(SessionInfo, ItemId, String[])", new Object[] { unwrap(sessionInfo), itemId, actions }); + } + + @Override + public PrivilegeDefinition[] getPrivilegeDefinitions(final SessionInfo sessionInfo) throws RepositoryException { + return (PrivilegeDefinition[]) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getPrivilegeDefinitions(unwrap(sessionInfo)); + } + }, "getSupportedPrivileges(SessionInfo)", new Object[]{unwrap(sessionInfo)}); + } + + public PrivilegeDefinition[] getSupportedPrivileges(final SessionInfo sessionInfo, final NodeId nodeId) throws RepositoryException { + return (PrivilegeDefinition[]) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getSupportedPrivileges(unwrap(sessionInfo), nodeId); + } + }, "getSupportedPrivileges(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public Name[] getPrivilegeNames(final SessionInfo sessionInfo, final NodeId nodeId) throws RepositoryException { + return (Name[]) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getPrivilegeNames(unwrap(sessionInfo), nodeId); + } + }, "getPrivileges(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public QNodeDefinition getNodeDefinition(final SessionInfo sessionInfo, final NodeId nodeId) + throws RepositoryException { + + return (QNodeDefinition) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getNodeDefinition(unwrap(sessionInfo), nodeId); + } + }, "getNodeDefinition(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public QPropertyDefinition getPropertyDefinition(final SessionInfo sessionInfo, + final PropertyId propertyId) throws RepositoryException { + + return (QPropertyDefinition) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getPropertyDefinition(unwrap(sessionInfo), propertyId); + } + }, "getPropertyDefinition(SessionInfo, PropertyId)", new Object[]{unwrap(sessionInfo), propertyId}); + } + + public NodeInfo getNodeInfo(final SessionInfo sessionInfo, final NodeId nodeId) + throws RepositoryException { + + return (NodeInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getNodeInfo(unwrap(sessionInfo), nodeId); + } + }, "getNodeInfo(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public Iterator getItemInfos(final SessionInfo sessionInfo, final ItemId itemId) + throws RepositoryException { + + return (Iterator) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getItemInfos(unwrap(sessionInfo), itemId); + } + }, "getItemInfos(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), itemId}); + } + + public Iterator getChildInfos(final SessionInfo sessionInfo, final NodeId parentId) + throws RepositoryException { + + return (Iterator) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getChildInfos(unwrap(sessionInfo), parentId); + } + }, "getChildInfos(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), parentId}); + } + + public Iterator getReferences(final SessionInfo sessionInfo, final NodeId nodeId, final Name propertyName, final boolean weakReferences) throws RepositoryException { + return (Iterator) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getReferences(unwrap(sessionInfo), nodeId, propertyName, weakReferences); + } + }, "getReferences(SessionInfo, NodeId, Name, boolean)", new Object[]{unwrap(sessionInfo), nodeId, propertyName, weakReferences}); + } + + public PropertyInfo getPropertyInfo(final SessionInfo sessionInfo, final PropertyId propertyId) + throws RepositoryException { + + return (PropertyInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getPropertyInfo(unwrap(sessionInfo), propertyId); + } + }, "getPropertyInfo(SessionInfo,PropertyId)", new Object[]{unwrap(sessionInfo), propertyId}); + } + + public Batch createBatch(final SessionInfo sessionInfo, final ItemId itemId) throws RepositoryException { + return (Batch) execute(new Callable() { + public Object call() throws RepositoryException { + return service.createBatch(unwrap(sessionInfo), itemId); + } + }, "createBatch(SessionInfo, ItemId)", new Object[]{unwrap(sessionInfo), itemId}); + } + + public void submit(final Batch batch) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.submit(unwrap(batch)); + return null; + } + }, "submit(Batch)", new Object[]{unwrap(batch)}); + } + + @Override + public Tree createTree(final SessionInfo sessionInfo, final Batch batch, final Name nodeName, final Name primaryTypeName, final String uniqueId) throws RepositoryException { + return (Tree) execute(new Callable() { + public Object call() throws RepositoryException { + return service.createTree(sessionInfo, batch, nodeName, primaryTypeName, uniqueId); + }}, "createTree(SessionInfo, Batch, Name, Name, String)", new Object[]{sessionInfo, batch, nodeName, primaryTypeName, uniqueId}); + } + + public void importXml(final SessionInfo sessionInfo, final NodeId parentId, final InputStream xmlStream, + final int uuidBehaviour) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.importXml(unwrap(sessionInfo), parentId, xmlStream, uuidBehaviour); + return null; + } + }, "importXml(SessionInfo, NodeId, InputStream, int)", + new Object[]{unwrap(sessionInfo), parentId, xmlStream, uuidBehaviour}); + } + + public void move(final SessionInfo sessionInfo, final NodeId srcNodeId, final NodeId destParentNodeId, + final Name destName) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.move(unwrap(sessionInfo), srcNodeId, destParentNodeId, destName); + return null; + } + }, "move(SessionInfo, NodeId, NodeId, Name)", + new Object[]{unwrap(sessionInfo), srcNodeId, destParentNodeId, destName}); + } + + public void copy(final SessionInfo sessionInfo, final String srcWorkspaceName, final NodeId srcNodeId, + final NodeId destParentNodeId, final Name destName) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.copy(unwrap(sessionInfo), srcWorkspaceName, srcNodeId, destParentNodeId, destName); + return null; + } + }, "copy(SessionInfo, String, NodeId, NodeId, Name)", + new Object[]{unwrap(sessionInfo), srcWorkspaceName, srcNodeId, destParentNodeId, destName}); + } + + public void update(final SessionInfo sessionInfo, final NodeId nodeId, final String srcWorkspaceName) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.update(unwrap(sessionInfo), nodeId, srcWorkspaceName); + return null; + } + }, "update(SessionInfo, NodeId, String)", new Object[]{unwrap(sessionInfo), nodeId, srcWorkspaceName}); + } + + public void clone(final SessionInfo sessionInfo, final String srcWorkspaceName, final NodeId srcNodeId, + final NodeId destParentNodeId, final Name destName, final boolean removeExisting) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.clone(unwrap(sessionInfo), srcWorkspaceName, srcNodeId, destParentNodeId, destName, + removeExisting); + return null; + } + }, "clone(SessionInfo, String, NodeId, NodeId, Name, boolean)", + new Object[] { unwrap(sessionInfo), srcWorkspaceName, srcNodeId, destParentNodeId, destName, removeExisting}); + } + + public LockInfo getLockInfo(final SessionInfo sessionInfo, final NodeId nodeId) + throws RepositoryException { + + return (LockInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getLockInfo(unwrap(sessionInfo), nodeId); + } + }, "getLockInfo(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public LockInfo lock(final SessionInfo sessionInfo, final NodeId nodeId, final boolean deep, + final boolean sessionScoped) throws RepositoryException { + + return (LockInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.lock(unwrap(sessionInfo), nodeId, deep, sessionScoped); + } + }, "lock(SessionInfo, NodeId, boolean, boolean)", + new Object[]{unwrap(sessionInfo), nodeId, deep, sessionScoped}); + } + + public LockInfo lock(final SessionInfo sessionInfo, final NodeId nodeId, final boolean deep, + final boolean sessionScoped, final long timeoutHint, final String ownerHint) + throws RepositoryException { + + return (LockInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.lock(unwrap(sessionInfo), nodeId, deep, sessionScoped, timeoutHint, ownerHint); + } + }, "lock(SessionInfo, NodeId, boolean, boolean, long, String)", + new Object[] { unwrap(sessionInfo), + nodeId, deep, sessionScoped, timeoutHint, + ownerHint }); + } + + public void refreshLock(final SessionInfo sessionInfo, final NodeId nodeId) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.refreshLock(unwrap(sessionInfo), nodeId); + return null; + } + }, "refreshLock(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public void unlock(final SessionInfo sessionInfo, final NodeId nodeId) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.unlock(unwrap(sessionInfo), nodeId); + return null; + } + }, "unlock(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public NodeId checkin(final SessionInfo sessionInfo, final NodeId nodeId) throws RepositoryException { + + return (NodeId) execute(new Callable() { + public Object call() throws RepositoryException { + return service.checkin(unwrap(sessionInfo), nodeId); + } + }, "checkin(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public void checkout(final SessionInfo sessionInfo, final NodeId nodeId) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.checkout(unwrap(sessionInfo), nodeId); + return null; + } + }, "checkout(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public void checkout(final SessionInfo sessionInfo, final NodeId nodeId, final NodeId activityId) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.checkout(unwrap(sessionInfo), nodeId, activityId); + return null; + } + }, "checkout(SessionInfo, NodeId, NodeId)", new Object[]{unwrap(sessionInfo), nodeId, activityId}); + } + + public NodeId checkpoint(final SessionInfo sessionInfo, final NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException { + return (NodeId) execute(new Callable() { + public Object call() throws RepositoryException { + return service.checkpoint(unwrap(sessionInfo), nodeId); + } + }, "checkpoint(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public NodeId checkpoint(final SessionInfo sessionInfo, final NodeId nodeId, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + return (NodeId) execute(new Callable() { + public Object call() throws RepositoryException { + return service.checkpoint(unwrap(sessionInfo), nodeId, activityId); + } + }, "checkpoint(SessionInfo, NodeId, NodeId)", new Object[]{unwrap(sessionInfo), nodeId, activityId}); + } + + public void removeVersion(final SessionInfo sessionInfo, final NodeId versionHistoryId, + final NodeId versionId) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.removeVersion(unwrap(sessionInfo), versionHistoryId, versionId); + return null; + } + }, "removeVersion(SessionInfo, NodeId, NodeId)", + new Object[]{unwrap(sessionInfo), versionHistoryId, versionId}); + } + + public void restore(final SessionInfo sessionInfo, final NodeId nodeId, final NodeId versionId, + final boolean removeExisting) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.restore(unwrap(sessionInfo), nodeId, versionId, removeExisting); + return null; + } + }, "restore(SessionInfo, NodeId, NodeId, boolean)", + new Object[]{unwrap(sessionInfo), nodeId, versionId, removeExisting}); + } + + public void restore(final SessionInfo sessionInfo, final NodeId[] nodeIds, final boolean removeExisting) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.restore(unwrap(sessionInfo), nodeIds, removeExisting); + return null; + } + }, "restore(SessionInfo, NodeId[], boolean)", + new Object[]{unwrap(sessionInfo), nodeIds, removeExisting}); + } + + public Iterator merge(final SessionInfo sessionInfo, final NodeId nodeId, final String srcWorkspaceName, + final boolean bestEffort) throws RepositoryException { + + return (Iterator) execute(new Callable() { + public Object call() throws RepositoryException { + return service.merge(unwrap(sessionInfo), nodeId, srcWorkspaceName, bestEffort); + } + }, "merge(SessionInfo, NodeId, String, boolean)", + new Object[]{unwrap(sessionInfo), nodeId, srcWorkspaceName, bestEffort}); + } + + public Iterator merge(final SessionInfo sessionInfo, final NodeId nodeId, final String srcWorkspaceName, + final boolean bestEffort, final boolean isShallow) throws RepositoryException { + + return (Iterator) execute(new Callable() { + public Object call() throws RepositoryException { + return service.merge(unwrap(sessionInfo), nodeId, srcWorkspaceName, bestEffort, isShallow); + } + }, "merge(SessionInfo, NodeId, String, boolean, boolean)", + new Object[]{unwrap(sessionInfo), nodeId, srcWorkspaceName, bestEffort}); + } + + public void resolveMergeConflict(final SessionInfo sessionInfo, final NodeId nodeId, + final NodeId[] mergeFailedIds, final NodeId[] predecessorIds) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.resolveMergeConflict(unwrap(sessionInfo), nodeId, mergeFailedIds, predecessorIds); + return null; + } + }, "resolveMergeConflict(SessionInfo, NodeId, NodeId[], NodeId[])", + new Object[]{unwrap(sessionInfo), nodeId, mergeFailedIds, predecessorIds}); + } + + public void addVersionLabel(final SessionInfo sessionInfo, final NodeId versionHistoryId, + final NodeId versionId, final Name label, final boolean moveLabel) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.addVersionLabel(unwrap(sessionInfo), versionHistoryId, versionId, label, moveLabel); + return null; + } + }, "addVersionLabel(SessionInfo, NodeId, NodeId, Name, boolean)", + new Object[]{unwrap(sessionInfo), versionHistoryId, versionId, label, moveLabel}); + } + + public void removeVersionLabel(final SessionInfo sessionInfo, final NodeId versionHistoryId, + final NodeId versionId, final Name label) throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.removeVersionLabel(unwrap(sessionInfo), versionHistoryId, versionId, label); + return null; + } + }, "removeVersionLabel(SessionInfo, NodeId, NodeId, Name)", + new Object[]{unwrap(sessionInfo), versionHistoryId, versionId, label}); + } + + public NodeId createActivity(final SessionInfo sessionInfo, final String title) throws UnsupportedRepositoryOperationException, RepositoryException { + return (NodeId) execute(new Callable() { + public Object call() throws RepositoryException { + return service.createActivity(unwrap(sessionInfo), title); + } + }, "createActivity(SessionInfo, String)", new Object[]{unwrap(sessionInfo), title}); + } + + public void removeActivity(final SessionInfo sessionInfo, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + service.removeActivity(unwrap(sessionInfo), activityId); + return null; + } + }, "removeActivity(SessionInfo, NodeId)", + new Object[]{unwrap(sessionInfo), activityId}); + } + + public Iterator mergeActivity(final SessionInfo sessionInfo, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + return (Iterator) execute(new Callable() { + public Object call() throws RepositoryException { + return service.mergeActivity(unwrap(sessionInfo), activityId); + } + }, "mergeActivity(SessionInfo, NodeId)", new Object[]{unwrap(sessionInfo), activityId}); + } + + public NodeId createConfiguration(final SessionInfo sessionInfo, final NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException { + return (NodeId) execute(new Callable() { + public Object call() throws RepositoryException { + return service.createConfiguration(unwrap(sessionInfo), nodeId); + } + }, "createConfiguration(SessionInfo, NodeId, NodeId)", new Object[]{unwrap(sessionInfo), nodeId}); + } + + public String[] getSupportedQueryLanguages(final SessionInfo sessionInfo) throws RepositoryException { + return (String[]) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getSupportedQueryLanguages(unwrap(sessionInfo)); + } + }, "getSupportedQueryLanguages(SessionInfo)", new Object[]{unwrap(sessionInfo)}); + } + + public String[] checkQueryStatement(final SessionInfo sessionInfo, final String statement, + final String language, final Map namespaces) throws RepositoryException { + + return (String[]) execute(new Callable() { + public Object call() throws RepositoryException { + return service.checkQueryStatement(unwrap(sessionInfo), statement, language, namespaces); + } + }, "checkQueryStatement(SessionInfo, String, String, Map)", + new Object[]{unwrap(sessionInfo), statement, language, namespaces}); + } + + public QueryInfo executeQuery(final SessionInfo sessionInfo, final String statement, + final String language, final Map namespaces, final long limit, final long offset, final Map values) throws RepositoryException { + + return (QueryInfo) execute(new Callable() { + public Object call() throws RepositoryException { + return service.executeQuery(unwrap(sessionInfo), statement, language, namespaces, limit, offset, values); + } + }, "executeQuery(SessionInfo, String, String, Map, long, long, Map)", + new Object[]{unwrap(sessionInfo), statement, language, namespaces, limit, offset, values}); + } + + public EventFilter createEventFilter(final SessionInfo sessionInfo, final int eventTypes, + final Path absPath, final boolean isDeep, final String[] uuid, final Name[] qnodeTypeName, + final boolean noLocal) throws RepositoryException { + + return (EventFilter) execute(new Callable() { + public Object call() throws RepositoryException { + return service.createEventFilter(unwrap(sessionInfo), eventTypes, absPath, isDeep, uuid, + qnodeTypeName, noLocal); + } + }, "createEventFilter(SessionInfo, int, Path, boolean, String[], Name[], boolean)", + new Object[]{unwrap(sessionInfo), eventTypes, absPath, isDeep, uuid, + qnodeTypeName, noLocal}); + } + + public Subscription createSubscription(final SessionInfo sessionInfo, final EventFilter[] filters) + throws RepositoryException { + + return (Subscription) execute(new Callable() { + public Object call() throws RepositoryException { + return service.createSubscription(unwrap(sessionInfo), filters); + } + }, "createSubscription(SessionInfo, EventFilter[])", + new Object[]{unwrap(sessionInfo), filters}); + } + + public EventBundle[] getEvents(final Subscription subscription, final long timeout) + throws RepositoryException, InterruptedException { + + final String methodName = "getEvents(Subscription, long)"; + final Object[] args = new Object[]{subscription, timeout}; + final InterruptedException[] ex = new InterruptedException[1]; + + EventBundle[] result = (EventBundle[]) execute(new Callable() { + public Object call() throws RepositoryException { + try { + return service.getEvents(subscription, timeout); + } catch (InterruptedException e) { + writer.error(methodName, args, e); + ex[0] = e; + return null; + } + } + }, methodName, args); + + if (ex[0] != null) { + throw ex[0]; + } + + return result; + } + + public EventBundle getEvents(final SessionInfo sessionInfo, + final EventFilter filter, + final long after) throws RepositoryException, + UnsupportedRepositoryOperationException { + return (EventBundle) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getEvents(sessionInfo, filter, after); + } + }, "getEvents(SessionInfo, EventFilter, long)", + new Object[]{unwrap(sessionInfo), filter, after}); + } + + public void updateEventFilters(final Subscription subscription, final EventFilter[] eventFilters) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.updateEventFilters(subscription, eventFilters); + return null; + } + }, "updateEventFilters(Subscription, EventFilter[])", + new Object[]{subscription, eventFilters}); + } + + public void dispose(final Subscription subscription) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + service.dispose(subscription); + return null; + } + }, "dispose(Subscription)", new Object[]{}); + } + + public Map getRegisteredNamespaces(final SessionInfo sessionInfo) throws RepositoryException { + return (Map) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getRegisteredNamespaces(unwrap(sessionInfo)); + } + }, "getRegisteredNamespaces(SessionInfo)", new Object[]{unwrap(sessionInfo)}); + } + + public String getNamespaceURI(final SessionInfo sessionInfo, final String prefix) + throws RepositoryException { + return (String) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getNamespaceURI(unwrap(sessionInfo), prefix); + } + }, "getNamespaceURI(SessionInfo, String)", new Object[]{unwrap(sessionInfo), prefix}); + } + + public String getNamespacePrefix(final SessionInfo sessionInfo, final String uri) + throws RepositoryException { + + return (String) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getNamespacePrefix(unwrap(sessionInfo), uri); + } + }, "getNamespacePrefix(SessionInfo, String)", new Object[]{unwrap(sessionInfo), uri}); + } + + public void registerNamespace(final SessionInfo sessionInfo, final String prefix, final String uri) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.registerNamespace(unwrap(sessionInfo), prefix, uri); + return null; + } + }, "registerNamespace(SessionInfo, String, String)", new Object[]{unwrap(sessionInfo), prefix, uri}); + } + + public void unregisterNamespace(final SessionInfo sessionInfo, final String uri) + throws RepositoryException { + + execute(new Callable() { + public Object call() throws RepositoryException { + service.unregisterNamespace(unwrap(sessionInfo), uri); + return null; + } + }, "unregisterNamespace(SessionInfo, String)", new Object[]{unwrap(sessionInfo), uri}); + } + + public Iterator getQNodeTypeDefinitions(final SessionInfo sessionInfo) throws RepositoryException { + return (Iterator) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getQNodeTypeDefinitions(unwrap(sessionInfo)); + } + }, "getQNodeTypeDefinitions(SessionInfo)", new Object[]{unwrap(sessionInfo)}); + } + + public Iterator getQNodeTypeDefinitions(final SessionInfo sessionInfo, final Name[] nodetypeNames) + throws RepositoryException { + + return (Iterator) execute(new Callable() { + public Object call() throws RepositoryException { + return service.getQNodeTypeDefinitions(unwrap(sessionInfo), nodetypeNames); + } + }, "getQNodeTypeDefinitions(SessionInfo, Name[])", new Object[]{unwrap(sessionInfo), nodetypeNames}); + } + + public void registerNodeTypes(final SessionInfo sessionInfo, final QNodeTypeDefinition[] nodeTypeDefinitions, final boolean allowUpdate) throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, UnsupportedRepositoryOperationException, RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + service.registerNodeTypes(unwrap(sessionInfo), nodeTypeDefinitions, allowUpdate); + return null; + } + }, "registerNodeTypes(SessionInfo, QNodeTypeDefinition[], boolean)", new Object[]{unwrap(sessionInfo), nodeTypeDefinitions, allowUpdate}); + } + + public void unregisterNodeTypes(final SessionInfo sessionInfo, final Name[] nodeTypeNames) throws UnsupportedRepositoryOperationException, NoSuchNodeTypeException, RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + service.unregisterNodeTypes(unwrap(sessionInfo), nodeTypeNames); + return null; + } + }, "unregisterNodeTypes(SessionInfo, Name[])", new Object[]{unwrap(sessionInfo), nodeTypeNames}); + } + + public void createWorkspace(final SessionInfo sessionInfo, final String name, final String srcWorkspaceName) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + service.createWorkspace(unwrap(sessionInfo), name, srcWorkspaceName); + return null; + } + }, "createWorkspace(SessionInfo, String, String)", new Object[]{unwrap(sessionInfo), name, srcWorkspaceName}); + } + + public void deleteWorkspace(final SessionInfo sessionInfo, final String name) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + service.deleteWorkspace(unwrap(sessionInfo), name); + return null; + } + }, "deleteWorkspace(SessionInfo, String, String)", new Object[]{unwrap(sessionInfo), name}); + + } + + // -----------------------------------------------------< private >--- + + private static SessionInfo unwrap(SessionInfo sessionInfo) { + if (sessionInfo instanceof SessionInfoLogger) { + return ((SessionInfoLogger) sessionInfo).getSessionInfo(); + } + else { + return sessionInfo; + } + } + + private static Batch unwrap(Batch batch) { + if (batch instanceof BatchLogger) { + return ((BatchLogger) batch).getBatch(); + } + else { + return batch; + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/SessionInfoLogger.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/SessionInfoLogger.java new file mode 100644 index 00000000000..96fc7881ec5 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/SessionInfoLogger.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.spi.commons.logging; + +import org.apache.jackrabbit.spi.SessionInfo; + +import javax.jcr.RepositoryException; + +/** + * Log wrapper for a {@link SessionInfo}. + */ +public class SessionInfoLogger extends AbstractLogger implements SessionInfo { + private final SessionInfo sessionInfo; + + /** + * Create a new instance for the given sessionInfo which uses + * writer for persisting log messages. + * @param sessionInfo + * @param writer + */ + public SessionInfoLogger(SessionInfo sessionInfo, LogWriter writer) { + super(writer); + this.sessionInfo = sessionInfo; + } + + /** + * @return the wrapped SessionInfo + */ + public SessionInfo getSessionInfo() { + return sessionInfo; + } + + // -----------------------------------------------------< SessionInfo >--- + + public String getUserID() { + return (String) execute(new SafeCallable() { + public Object call() { + return sessionInfo.getUserID(); + } + }, "getUserID()", new Object[]{}); + } + + public String getWorkspaceName() { + return (String) execute(new SafeCallable() { + public Object call() { + return sessionInfo.getWorkspaceName(); + } + }, "getWorkspaceName()", new Object[]{}); + } + + public String[] getLockTokens() throws RepositoryException { + return (String[]) execute(new Callable() { + public Object call() throws RepositoryException { + return sessionInfo.getLockTokens(); + } + }, "getLockTokens()", new Object[]{}); + } + + public void addLockToken(final String lockToken) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + sessionInfo.addLockToken(lockToken); + return null; + } + }, "addLockToken(String)", new Object[]{lockToken}); + } + + public void removeLockToken(final String lockToken) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + sessionInfo.removeLockToken(lockToken); + return null; + } + }, "removeLockToken(String)", new Object[]{lockToken}); + } + + public void setUserData(final String userData) throws RepositoryException { + execute(new Callable() { + public Object call() throws RepositoryException { + sessionInfo.setUserData(userData); + return null; + } + }, "setUserData(String)", new Object[]{userData}); + } + + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/Slf4jLogWriter.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/Slf4jLogWriter.java new file mode 100644 index 00000000000..77a96ef1850 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/Slf4jLogWriter.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.spi.commons.logging; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.slf4j.Logger; + +/** + * {@link LogWriter} implementation which uses SLF4J for persisting log messages. + */ +public class Slf4jLogWriter implements LogWriter { + private final Logger log; + + /** + * Create a new instance which uses the passed SLF4J logger for persisting + * the log messages. + * @param log + */ + public Slf4jLogWriter(Logger log) { + super(); + this.log = log; + } + + /** + * Returns + *
        +     *   System.currentTimeMillis();
        +     * 
        + * {@inheritDoc} + */ + public long systemTime() { + return System.currentTimeMillis(); + } + + /** + * Logs the call at debug level is debug level is enabled. + * {@inheritDoc} + */ + public void enter(final String methodName, final Object[] args) { + if (log.isDebugEnabled()) { + log.debug("ENTER(" + systemTime() + ") | " + methodName + "(" + formatArgs(args) + ")"); + } + } + + /** + * Logs the call at debug level is debug level is enabled. + * {@inheritDoc} + */ + public void leave(final String methodName, final Object[] args, final Object result) { + if (log.isDebugEnabled()) { + log.debug("LEAVE(" + systemTime() + ") | " + methodName + "(" + formatArgs(args) + ") = " + + formatResult(result)); + } + } + + /** + * Logs the exception including a stack trace at debug level is debug level is enabled. + * {@inheritDoc} + */ + public void error(final String methodName, final Object[] args, final Exception e) { + if (log.isDebugEnabled()) { + log.debug("ERROR(" + systemTime() + ") | " + methodName + "(" + formatArgs(args) + ") | " + + formatException(e)); + } + } + + // -----------------------------------------------------< private >--- + + private String formatArgs(Object[] args) { + StringBuffer b = new StringBuffer(); + formatArgs(args, b); + return b.toString(); + } + + private String formatResult(Object result) { + StringBuffer b = new StringBuffer(); + formatArg(result, b); + return b.toString(); + } + + private String formatException(Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + private void formatArgs(Object[] args, StringBuffer b) { + String separator = ""; + for (int k = 0; k < args.length; k++) { + b.append(separator); + formatArg(args[k], b); + separator = ", "; + } + } + + private void formatArg(Object arg, StringBuffer b) { + if (arg instanceof Object[]) { + b.append('['); + formatArgs((Object[]) arg, b); + b.append(']'); + } + else { + b.append(arg); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/Slf4jLogWriterProvider.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/Slf4jLogWriterProvider.java new file mode 100644 index 00000000000..cdba87ae7fd --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/Slf4jLogWriterProvider.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.spi.commons.logging; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link LogWriterProvider} instance which provides {@link Slf4jLogWriter}s. + */ +public class Slf4jLogWriterProvider implements LogWriterProvider { + + /** + * Returns a {@link Slf4jLogWriter} if the logger for + * service.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(RepositoryService service) { + return getLogWriterInternal(service); + } + + /** + * Returns a {@link Slf4jLogWriter} if the logger for + * nameFactory.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(NameFactory nameFactory) { + return getLogWriterInternal(nameFactory); + } + + /** + * Returns a {@link Slf4jLogWriter} if the logger for + * pathFactory.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(PathFactory pathFactory) { + return getLogWriterInternal(pathFactory); + } + + /** + * Returns a {@link Slf4jLogWriter} if the logger for + * idFactory.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(IdFactory idFactory) { + return getLogWriterInternal(idFactory); + } + + /** + * Returns a {@link Slf4jLogWriter} if the logger for + * valueFactory.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(QValueFactory valueFactory) { + return getLogWriterInternal(valueFactory); + } + + /** + * Returns a {@link Slf4jLogWriter} if the logger for + * sessionInfo.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(SessionInfo sessionInfo) { + return getLogWriterInternal(sessionInfo); + } + + /** + * Returns a {@link Slf4jLogWriter} if the logger for + * batch.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(Batch batch) { + return getLogWriterInternal(batch); + } + + // -----------------------------------------------------< private >--- + + private static LogWriter getLogWriterInternal(Object object) { + Logger log = LoggerFactory.getLogger(object.getClass()); + return log.isDebugEnabled() + ? new Slf4jLogWriter(log) + : null; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/SpiLoggerFactory.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/SpiLoggerFactory.java new file mode 100644 index 00000000000..87d7c9ecde1 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/SpiLoggerFactory.java @@ -0,0 +1,353 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.logging; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; + +/** + * Static factory for creating instances of the various spi loggers derived from + * {@link AbstractLogger}. + * In the most simple case + *
        + *   RepositoryService loggingService = SpiLoggerFactory.create(service);
        + * 
        + * creates a log wrapper for service which logs all calls to its methods + * if logging at the debug level is enabled. If logging is not enabled, no log wrapper + * is created at all and service itself is returned. There is thus virtually + * no overhead from disabled loggers. Loggers are enabled and disabled via the + * configuration mechanism of the logging framework which is in place. + *

        + * There are log wrappers for the following SPI entities: + * + * + * + * + * + * + * + * + * + *
        SPI entitylog wrapper
        {@link RepositoryService}{@link RepositoryServiceLogger}
        {@link NameFactory}{@link NameFactoryLogger}
        {@link PathFactory}{@link PathFactoryLogger}
        {@link IdFactory}{@link IdFactoryLogger}
        {@link QValueFactory}{@link QValueFactoryLogger}
        {@link SessionInfo}{@link SessionInfoLogger}
        {@link Batch}{@link BatchLogger}
        + * + * The more general form + *

        + *   RepositoryService loggingService = SpiLoggerFactory.create(service, logWriterProvider);
        + * 
        + * allows specification of a {@link LogWriterProvider}. A LogWriterProvider provides the + * {@link LogWriter}s for the individual SPI entities. If the LogWriter does not provide a + * LogWriter for a certain SPI entity no log wrapper is created for that entity. In the case + * of {@link Slf4jLogWriterProvider}, a LogWriter is only provided if the logger of the + * implementation class of the respective SPI entity is names after the class and has debug + * level enabled. + */ +public final class SpiLoggerFactory { + + private SpiLoggerFactory() { + super(); + } + + /** + * Shortcut for + *
        +     *   create(service, new Slf4jLogWriterProvider());
        +     * 
        + * @see #create(RepositoryService, LogWriterProvider) + * @param service + * @return + */ + public static RepositoryService create(RepositoryService service) { + return create(service, new Slf4jLogWriterProvider()); + } + + /** + * Returns a log wrapper for the given service which logs a calls to its + * methods if logWriterProvider returns a {@link LogWriter} instance for + * service. Otherwise returns service. + * @param service + * @param logWriterProvider + * @return + * @throws IllegalArgumentException if either argument is null + */ + public static RepositoryService create(RepositoryService service, LogWriterProvider logWriterProvider) { + if (service == null) { + throw new IllegalArgumentException("Service must not be null"); + } + if (logWriterProvider == null) { + throw new IllegalArgumentException("LogWriterProvider must not be null"); + } + + LogWriter logWriter = logWriterProvider.getLogWriter(service); + if (logWriter == null) { + return service; + } + else { + return new ServiceLogger(service, logWriterProvider, logWriter); + } + } + + /** + * Returns a log wrapper for the given nameFactory which logs a calls to its + * methods if logWriterProvider returns a {@link LogWriter} instance for + * nameFactory. Otherwise returns nameFactory. + * @param nameFactory + * @param logWriterProvider + * @return + * @throws IllegalArgumentException if either argument is null + */ + public static NameFactory create(NameFactory nameFactory, LogWriterProvider logWriterProvider) { + if (nameFactory == null) { + throw new IllegalArgumentException("NameFactory must not be null"); + } + if (logWriterProvider == null) { + throw new IllegalArgumentException("LogWriterProvider must not be null"); + } + + LogWriter logWriter = logWriterProvider.getLogWriter(nameFactory); + if (logWriter == null) { + return nameFactory; + } + else { + return new NameFactoryLogger(nameFactory, logWriter); + } + } + + /** + * Returns a log wrapper for the given pathFactory which logs a calls to its + * methods if logWriterProvider returns a {@link LogWriter} instance for + * pathFactory. Otherwise returns pathFactory. + * @param pathFactory + * @param logWriterProvider + * @return + * @throws IllegalArgumentException if either argument is null + */ + public static PathFactory create(PathFactory pathFactory, LogWriterProvider logWriterProvider) { + if (pathFactory == null) { + throw new IllegalArgumentException("PathFactory must not be null"); + } + if (logWriterProvider == null) { + throw new IllegalArgumentException("LogWriterProvider must not be null"); + } + + LogWriter logWriter = logWriterProvider.getLogWriter(pathFactory); + if (logWriter == null) { + return pathFactory; + } + else { + return new PathFactoryLogger(pathFactory, logWriter); + } + } + + /** + * Returns a log wrapper for the given idFactory which logs a calls to its + * methods if logWriterProvider returns a {@link LogWriter} instance for + * idFactory. Otherwise returns idFactory. + * @param idFactory + * @param logWriterProvider + * @return + * @throws IllegalArgumentException if either argument is null + */ + public static IdFactory create(IdFactory idFactory, LogWriterProvider logWriterProvider) { + if (idFactory == null) { + throw new IllegalArgumentException("IdFactory must not be null"); + } + if (logWriterProvider == null) { + throw new IllegalArgumentException("LogWriterProvider must not be null"); + } + + LogWriter logWriter = logWriterProvider.getLogWriter(idFactory); + if (logWriter == null) { + return idFactory; + } + else { + return new IdFactoryLogger(idFactory, logWriter); + } + } + + /** + * Returns a log wrapper for the given qValueFactory which logs a calls to its + * methods if logWriterProvider returns a {@link LogWriter} instance for + * qValueFactory. Otherwise returns qValueFactory. + * @param qValueFactory + * @param logWriterProvider + * @return + * @throws IllegalArgumentException if either argument is null + */ + public static QValueFactory create(QValueFactory qValueFactory, LogWriterProvider logWriterProvider) { + if (qValueFactory == null) { + throw new IllegalArgumentException("QValueFactory must not be null"); + } + if (logWriterProvider == null) { + throw new IllegalArgumentException("LogWriterProvider must not be null"); + } + + LogWriter logWriter = logWriterProvider.getLogWriter(qValueFactory); + if (logWriter == null) { + return qValueFactory; + } + else { + return new QValueFactoryLogger(qValueFactory, logWriter); + } + } + + /** + * Returns a log wrapper for the given sessionInfo which logs a calls to its + * methods if logWriterProvider returns a {@link LogWriter} instance for + * sessionInfo. Otherwise returns sessionInfo. + * @param sessionInfo + * @param logWriterProvider + * @return + * @throws IllegalArgumentException if either argument is null + */ + public static SessionInfo create(SessionInfo sessionInfo, LogWriterProvider logWriterProvider) { + if (sessionInfo == null) { + throw new IllegalArgumentException("SessionInfo must not be null"); + } + if (logWriterProvider == null) { + throw new IllegalArgumentException("LogWriterProvider must not be null"); + } + + LogWriter logWriter = logWriterProvider.getLogWriter(sessionInfo); + if (logWriter == null) { + return sessionInfo; + } + else { + return new SessionInfoLogger(sessionInfo, logWriter); + } + } + + /** + * Returns a log wrapper for the given batch which logs a calls to its + * methods if logWriterProvider returns a {@link LogWriter} instance for + * batch. Otherwise returns batch. + * @param batch + * @param logWriterProvider + * @return + * @throws IllegalArgumentException if either argument is null + */ + public static Batch create(Batch batch, LogWriterProvider logWriterProvider) { + if (batch == null) { + throw new IllegalArgumentException("Batch must not be null"); + } + if (logWriterProvider == null) { + throw new IllegalArgumentException("LogWriterProvider must not be null"); + } + + LogWriter logWriter = logWriterProvider.getLogWriter(batch); + if (logWriter == null) { + return batch; + } + else { + return new BatchLogger(batch, logWriter); + } + } + + //------------------------------------------------------------< private >--- + /** + * Helper class which wraps SPI entities returned from calls to {@link RepositoryService} + * into log wrappers if the {@link LogWriterProvider} can provide a {@link LogWriter}. + */ + private static class ServiceLogger extends RepositoryServiceLogger { + private final LogWriterProvider logWriterProvider; + + public ServiceLogger(RepositoryService service, LogWriterProvider logWriterProvider, LogWriter logWriter) { + super(service, logWriter); + this.logWriterProvider = logWriterProvider; + } + + @Override + public NameFactory getNameFactory() throws RepositoryException { + NameFactory result = super.getNameFactory(); + return result == null + ? null + : create(result, logWriterProvider); + } + + @Override + public PathFactory getPathFactory() throws RepositoryException { + PathFactory result = super.getPathFactory(); + return result == null + ? null + : create(result, logWriterProvider); + } + + @Override + public IdFactory getIdFactory() throws RepositoryException { + IdFactory result = super.getIdFactory(); + return result == null + ? null + : create(result, logWriterProvider); + } + + @Override + public QValueFactory getQValueFactory() throws RepositoryException { + QValueFactory result = super.getQValueFactory(); + return result == null + ? null + : create(result, logWriterProvider); + } + + @Override + public SessionInfo obtain(final Credentials credentials, final String workspaceName) + throws RepositoryException { + + SessionInfo result = super.obtain(credentials, workspaceName); + return result == null + ? null + : create(result, logWriterProvider); + } + + @Override + public SessionInfo obtain(final SessionInfo sessionInfo, final String workspaceName) + throws RepositoryException { + + SessionInfo result = super.obtain(sessionInfo, workspaceName); + return result == null + ? null + : create(result, logWriterProvider); + } + + @Override + public SessionInfo impersonate(final SessionInfo sessionInfo, final Credentials credentials) + throws RepositoryException { + + SessionInfo result = super.impersonate(sessionInfo, credentials); + return result == null + ? null + : create(result, logWriterProvider); + } + + @Override + public Batch createBatch(final SessionInfo sessionInfo, final ItemId itemId) + throws RepositoryException { + + Batch result = super.createBatch(sessionInfo, itemId); + return result == null + ? null + : create(result, logWriterProvider); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/WriterLogWriter.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/WriterLogWriter.java new file mode 100644 index 00000000000..1aa0883e257 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/WriterLogWriter.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.spi.commons.logging; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +/** + * {@link LogWriter} implementation which uses a {@link Writer} for persisting log messages. + */ +public class WriterLogWriter implements LogWriter { + + private final PrintWriter log; + + private final String category; + + /** + * Create a new instance which uses the passed writer logger for persisting + * the log messages. + * @param log writer for output + * @param category log category + */ + public WriterLogWriter(Writer log, String category) { + super(); + this.log = new PrintWriter(log); + this.category = category; + } + + /** + * Returns + *
        +     *   System.currentTimeMillis();
        +     * 
        + * {@inheritDoc} + */ + public long systemTime() { + return System.currentTimeMillis(); + } + + /** + * Logs the call at debug level is debug level is enabled. + * {@inheritDoc} + */ + public void enter(final String methodName, final Object[] args) { + print("ENTER(" + systemTime() + ") | " + methodName + "(" + formatArgs(args) + ")"); + } + + /** + * Logs the call at debug level is debug level is enabled. + * {@inheritDoc} + */ + public void leave(final String methodName, final Object[] args, final Object result) { + print("LEAVE(" + systemTime() + ") | " + methodName + "(" + formatArgs(args) + ") = " + + formatResult(result)); + } + + /** + * Logs the exception including a stack trace at debug level is debug level is enabled. + * {@inheritDoc} + */ + public void error(final String methodName, final Object[] args, final Exception e) { + print("ERROR(" + systemTime() + ") | " + methodName + "(" + formatArgs(args) + ") | " + + formatException(e)); + } + + private void print(String msg) { + log.print(category); + log.print(": "); + log.println(msg); + log.flush(); + } + // -----------------------------------------------------< private >--- + + private String formatArgs(Object[] args) { + StringBuffer b = new StringBuffer(); + formatArgs(args, b); + return b.toString(); + } + + private String formatResult(Object result) { + StringBuffer b = new StringBuffer(); + formatArg(result, b); + return b.toString(); + } + + private String formatException(Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + private void formatArgs(Object[] args, StringBuffer b) { + String separator = ""; + for (Object arg : args) { + b.append(separator); + formatArg(arg, b); + separator = ", "; + } + } + + private void formatArg(Object arg, StringBuffer b) { + if (arg instanceof Object[]) { + b.append('['); + formatArgs((Object[]) arg, b); + b.append(']'); + } + else { + b.append(arg); + } + } + +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/WriterLogWriterProvider.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/WriterLogWriterProvider.java new file mode 100644 index 00000000000..5ff6d3865d3 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/WriterLogWriterProvider.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.spi.commons.logging; + +import java.io.Writer; + +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; + +/** + * {@link LogWriterProvider} instance which provides {@link WriterLogWriter}s. + */ +public class WriterLogWriterProvider implements LogWriterProvider { + + /** + * internal writer + */ + private final Writer log; + + /** + * Creates a new WriterLogWriterProvider based on the given writer + * @param log the writer + */ + public WriterLogWriterProvider(Writer log) { + this.log = log; + } + + /** + * Returns a {@link WriterLogWriter} if the logger for + * service.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(RepositoryService service) { + return getLogWriterInternal(log, service); + } + + /** + * Returns a {@link WriterLogWriter} if the logger for + * nameFactory.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(NameFactory nameFactory) { + return getLogWriterInternal(log, nameFactory); + } + + /** + * Returns a {@link WriterLogWriter} if the logger for + * pathFactory.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(PathFactory pathFactory) { + return getLogWriterInternal(log, pathFactory); + } + + /** + * Returns a {@link WriterLogWriter} if the logger for + * idFactory.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(IdFactory idFactory) { + return getLogWriterInternal(log, idFactory); + } + + /** + * Returns a {@link WriterLogWriter} if the logger for + * valueFactory.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(QValueFactory valueFactory) { + return getLogWriterInternal(log, valueFactory); + } + + /** + * Returns a {@link WriterLogWriter} if the logger for + * sessionInfo.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(SessionInfo sessionInfo) { + return getLogWriterInternal(log, sessionInfo); + } + + /** + * Returns a {@link WriterLogWriter} if the logger for + * batch.getClass() has debug level enabled. Returns + * null otherwise. + * {@inheritDoc} + */ + public LogWriter getLogWriter(Batch batch) { + return getLogWriterInternal(log, batch); + } + + // -----------------------------------------------------< private >--- + + private static LogWriter getLogWriterInternal(Writer log, Object object) { + return new WriterLogWriter(log, object.getClass().getSimpleName()); + } + +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/package-info.java new file mode 100644 index 00000000000..8224ff29340 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/logging/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.logging; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/AbstractPath.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/AbstractPath.java new file mode 100644 index 00000000000..b14b90f7491 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/AbstractPath.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.name; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Path; + +/** + * Abstract base class for paths. + */ +abstract class AbstractPath implements Path, Path.Element { + + /** Serial version UID */ + private static final long serialVersionUID = 3018771833963770499L; + + /** + * Returns {@link Path#INDEX_UNDEFINED}, except when overridden by the + * {@link NamePath} subclass. + * + * @return {@link Path#INDEX_UNDEFINED} + */ + public int getIndex() { + return INDEX_UNDEFINED; + } + + /** + * Returns {@link Path#INDEX_DEFAULT}, except when overridden by the + * {@link NamePath} subclass. + * + * @return {@link Path#INDEX_DEFAULT} + */ + public int getNormalizedIndex() { + return INDEX_DEFAULT; + } + + /** + * Returns null, except when overridden by the + * {@link IdentifierPath} subclass. + * + * @return null + */ + public String getIdentifier() { + return null; + } + + /** + * Returns false, except when overridden by the + * {@link RootPath} subclass. + * + * @return false + */ + public boolean denotesRoot() { + return false; + } + + /** + * Returns false, except when overridden by the + * {@link IdentifierPath} subclass. + * + * @return false + */ + public boolean denotesIdentifier() { + return false; + } + + /** + * Returns false, except when overridden by the + * {@link ParentPath} subclass. + * + * @return false + */ + public boolean denotesParent() { + return false; + } + + /** + * Returns false, except when overridden by the + * {@link CurrentPath} subclass. + * + * @return false + */ + public boolean denotesCurrent() { + return false; + } + + /** + * Returns false, except when overridden by the + * {@link NamePath} subclass. + * + * @return false + */ + public boolean denotesName() { + return false; + } + + public Element getNameElement() { + return getLastElement(); + } + + /** + * Returns this path, except when overridden by the {@link RelativePath} + * subclasses. + * + * @return this path + */ + public AbstractPath getLastElement() { + return this; + } + + /** + * Returns null, except when overridden by the + * {@link RelativePath} subclass. + * + * @return null + */ + public Path getFirstElements() { + return null; + } + + public final Path resolve(Element element) { + if (element.denotesName()) { + return new NamePath(this, element.getName(), element.getIndex()); + } else if (element.denotesParent()) { + if (isAbsolute() && getDepth() == 0) { + throw new IllegalArgumentException( + "An absolute paths with negative depth is not allowed"); + } + return new ParentPath(this); + } else if (element.denotesCurrent()) { + return new CurrentPath(this); + } else if (element.denotesRoot()) { + return RootPath.ROOT_PATH; + } else if (element.denotesIdentifier()) { + return new IdentifierPath(element.getIdentifier()); + } else { + throw new IllegalArgumentException( + "Unknown path element type: " + element); + } + } + + public final Path resolve(Path relative) { + if (relative.isAbsolute()) { + return relative; + } else if (relative.getLength() > 1) { + Path first = relative.getFirstElements(); + Path last = relative.getLastElement(); + return resolve(first).resolve(last); + } else if (relative.denotesCurrent()) { + return new CurrentPath(this); + } else if (relative.denotesParent()) { + return new ParentPath(this); + } else if (relative.denotesName()) { + return new NamePath(this, relative.getName(), relative.getIndex()); + } else { + throw new IllegalArgumentException( + "Unknown path type: " + relative); + } + } + + /** + * Computes the relative path from this path to the given other path. + * Both paths must be absolute. + * + * @param other other path + * @return relative path + * @throws RepositoryException if the relative path can not be computed + */ + public final Path computeRelativePath(Path other) + throws RepositoryException { + if (other != null && isAbsolute() && other.isAbsolute()) { + Element[] a = getElements(); + Element[] b = other.getElements(); + + // The first elements (root or identifier) must be equal + if (a.length > 0 && b.length > 0 && a[0].equals(b[0])) { + int ai = 1; + int bi = 1; + + while (ai < a.length && bi < b.length) { + if (a[ai].equals(b[bi])) { + ai++; + bi++; + } else if (a[ai].denotesCurrent()) { + ai++; + } else if (b[bi].denotesCurrent()) { + bi++; + } else { + break; + } + } + + Path path = null; + + while (ai < a.length) { + if (a[ai].denotesName()) { + path = new ParentPath(path); + ai++; + } else if (a[ai].denotesCurrent()) { + ai++; + } else { + throw new RepositoryException( + "Unexpected path element: " + a[ai]); + } + } + + if (path == null) { + path = new CurrentPath(null); + } + + while (bi < b.length) { + path = path.resolve(b[bi++]); + } + + return path; + } + } + throw new RepositoryException( + "No relative path from " + this + " to " + other); + } + + /** + * Determines if this path is equivalent to the given other path by + * comparing the normalized paths for equality. + * + * @param other other path + * @return true if this path is equivalent to the other path, + * false otherwise + * @throws IllegalArgumentException if the other path is null + * @throws RepositoryException if an error occurs + */ + public final boolean isEquivalentTo(Path other) + throws IllegalArgumentException, RepositoryException { + if (other != null) { + return getNormalizedPath().equals(other.getNormalizedPath()); + } else { + throw new IllegalArgumentException( + this + ".isEquivalentTo(" + other + ")"); + } + } + + /** + * Determines if this path is a ancestor of the given other path + * by comparing the depths of the paths and checking if the corresponding + * ancestor of the given other path is equivalent to this path. + * + * @param other other path + * @return true if this path is an ancestor of the other path, + * false otherwise + * @throws IllegalArgumentException if the other path is null, + * or relative when this path is absolute, + * or vice versa + * @throws RepositoryException if an error occurs + */ + public final boolean isAncestorOf(Path other) + throws IllegalArgumentException, RepositoryException { + if (other != null + && isAbsolute() == other.isAbsolute() + && isIdentifierBased() == other.isIdentifierBased()) { + int d = other.getDepth() - getDepth(); + return d > 0 && isEquivalentTo(other.getAncestor(d)); + } else { + throw new IllegalArgumentException( + this + ".isAncestorOf(" + other + ")"); + } + } + + /** + * Determines if this path is a descendant of the given other path + * by comparing the depths of the paths and checking if the corresponding + * ancestor of this path is equivalent to the given other path. + * + * @param other other path + * @return true if this path is a descendant of the other path, + * false otherwise + * @throws IllegalArgumentException if the other path is null, + * or relative when this path is absolute, + * or vice versa + * @throws RepositoryException if an error occurs + */ + public final boolean isDescendantOf(Path other) + throws IllegalArgumentException, RepositoryException { + if (other != null + && isAbsolute() == other.isAbsolute() + && isIdentifierBased() == other.isIdentifierBased()) { + int d = getDepth() - other.getDepth(); + return d > 0 && getAncestor(d).isEquivalentTo(other); + } else { + throw new IllegalArgumentException( + this + ".isDescendantOf(" + other + ")"); + } + } + + //--------------------------------------------------------------< Object > + + /** + * Returns the string representation of this path. + * + * @return path string + */ + public final String toString() { + return getString(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/CurrentPath.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/CurrentPath.java new file mode 100644 index 00000000000..949712fb805 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/CurrentPath.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.spi.commons.name; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * A relative path whose last element is the current path element, i.e. ".". + */ +final class CurrentPath extends RelativePath { + + /** Serial version UID */ + private static final long serialVersionUID = 1729196441091297231L; + + /** The current path "." */ + public static final CurrentPath CURRENT_PATH = new CurrentPath(null); + + /** Name of the current element */ + public static final Name NAME = + NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, "."); + + public CurrentPath(Path parent) { + super(parent); + } + + protected int getDepthModifier() { + return 0; + } + + protected Path getParent() throws RepositoryException { + if (parent != null) { + return parent.getAncestor(1); + } else { + return new ParentPath(null); + } + } + + protected String getElementString() { + return NAME.getLocalName(); + } + + public Name getName() { + return NAME; + } + + /** + * Returns true as this path ends in the current element. + * + * @return true + */ + @Override + public boolean denotesCurrent() { + return true; + } + + /** + * Returns false as a path with a "." element is + * never canonical. + * + * @return false + */ + public boolean isCanonical() { + return false; + } + + public boolean isNormalized() { + return parent == null; + } + + public Path getNormalizedPath() throws RepositoryException { + if (parent != null) { + return parent.getNormalizedPath(); + } else { + return this; + } + } + + public Path getCanonicalPath() throws RepositoryException { + if (parent != null) { + return parent.getCanonicalPath(); + } else { + throw new RepositoryException( + "There is no canonical representation of ."); + } + } + + /** + * Returns the current path ".". + * + * @return current path + */ + @Override + public AbstractPath getLastElement() { + return CURRENT_PATH; + } + + //--------------------------------------------------------------< Object > + + @Override + public final boolean equals(Object that) { + if (this == that) { + return true; + } else if (that instanceof Path) { + Path path = (Path) that; + return path.denotesCurrent() && super.equals(that); + } else { + return false; + } + } + + @Override + public final int hashCode() { + return super.hashCode() + 1; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/HashCache.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/HashCache.java new file mode 100644 index 00000000000..85b5d7c75ad --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/HashCache.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.spi.commons.name; + +/** + * Simple utility class that implements a fixed-size and thread-safe + * (non-blocking) cache of objects. The cache is simply an array + * of objects, indexed by their hash codes. If more than one objects + * hash to the same location, only the most recently accessed object is + * kept in the cache. + * + * @see
        JCR-1663 + */ +public class HashCache { + + /** + * Array of cached objects, indexed by their hash codes + * (module size of the array). + */ + private final T[] array; + + /** + * Creates a hash cache with 1024 slots. + */ + public HashCache() { + this(10); + } + + /** + * Creates a hash cache with 2^exponent slots. + * + * @param exponent the exponent. + */ + @SuppressWarnings("unchecked") + public HashCache(int exponent) { + this.array = (T[]) new Object[2 << exponent]; + } + + /** + * If a cached copy of the given object already exists, then returns + * that copy. Otherwise the given object is cached and returned. + * + * @param object object to return from the cache + * @return the given object or a previously cached copy + */ + public T get(T object) { + int position = object.hashCode() & (array.length - 1); + T previous = array[position]; + if (object.equals(previous)) { + return previous; + } else { + array[position] = object; + return object; + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/IdentifierPath.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/IdentifierPath.java new file mode 100644 index 00000000000..a1cffaa1dcd --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/IdentifierPath.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.spi.commons.name; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +final class IdentifierPath extends AbstractPath { + + /** Serial version UID */ + private static final long serialVersionUID = 1602959709588338642L; + + private final String identifier; + + public IdentifierPath(String identifier) { + assert identifier != null; + this.identifier = identifier; + } + + /** + * Returns null as an identifier element has no name. + * + * @return null + */ + public Name getName() { + return null; + } + + public String getIdentifier() { + return identifier; + } + + /** + * Returns true as this is an identifier-based path. + * + * @return true + */ + @Override + public boolean denotesIdentifier() { + return true; + } + + /** + * Returns true as this is an identifier-based path. + * + * @return true + */ + public boolean isIdentifierBased() { + return true; + } + + /** + * Returns true as an identifier-based path with no other + * elements is absolute. + * + * @return true + */ + public boolean isAbsolute() { + return true; + } + + /** + * Returns true as an identifier-based path with no other + * elements is canonical. + * + * @return true + */ + public boolean isCanonical() { + return true; + } + + /** + * Returns false as an identifier-based path is never + * normalized. + * + * @return false + */ + public boolean isNormalized() { + return false; + } + + public Path getNormalizedPath() throws RepositoryException { + throw new RepositoryException( + "Cannot normalize the identifier-based path " + this); + } + + public Path getCanonicalPath() { + return this; + } + + public Path getAncestor(int degree) + throws IllegalArgumentException, RepositoryException { + if (degree < 0) { + throw new IllegalArgumentException( + this + ".getAncestor(" + degree + ")"); + } else if (degree > 0) { + throw new RepositoryException( + "Cannot construct ancestor path from an identifier"); + } else { + return this; + } + } + + public int getAncestorCount() { + return 0; + } + + public int getLength() { + return 1; + } + + public int getDepth() { + return 0; + } + + public Path subPath(int from, int to) throws IllegalArgumentException { + if (from == 0 && to == 1) { + return this; + } else { + throw new IllegalArgumentException( + this + ".subPath(" + from + ", " + to + ")"); + } + } + + public Element[] getElements() { + return new Element[] { getNameElement() }; + } + + public String getString() { + return "[" + identifier + "]"; + } + + //--------------------------------------------------------------< Object > + + public final boolean equals(Object that) { + if (this == that) { + return true; + } else if (that instanceof Path) { + Path path = (Path) that; + return path.denotesIdentifier() + && identifier.equals(path.getIdentifier()); + } else { + return false; + } + } + + public final int hashCode() { + return identifier.hashCode(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/MatchResult.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/MatchResult.java new file mode 100644 index 00000000000..4b09f96b7a6 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/MatchResult.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.spi.commons.name; + +import org.apache.jackrabbit.spi.Path; + +/** + * A MatchResult instance represents the result of matching a {@link Pattern} against + * a {@link Path}. + */ +public class MatchResult { + private final Path path; + private final int pathLength; + private int matchPos; + private final int matchLength; + + MatchResult(Path path, int length) { + this(path, 0, length); + } + + MatchResult(Path path, int pos, int length) { + super(); + if (!path.isNormalized()) { + throw new IllegalArgumentException("Path not normalized"); + } + this.path = path; + this.matchPos = pos; + this.matchLength = length; + this.pathLength = path.getLength(); + } + + /** + * Returns the remaining path after the matching part. + * @return The remaining path after the matching part such that the path constructed from + * {@link #getMatch()} followed by {@link #getRemainder()} is the original path or + * null if {@link #isFullMatch()} is true. + */ + public Path getRemainder() { + if (matchPos + matchLength >= pathLength) { + return null; + } else { + return path.subPath(matchPos + matchLength, pathLength); + } + } + + /** + * Returns the path which was matched by the {@link Pattern}. + * @return The path which was matched such that the path constructed from + * {@link #getMatch()} followed by {@link #getRemainder()} is the original path or + * null if {@link #getMatchLength()} is 0. + */ + public Path getMatch() { + if (matchLength == 0) { + return null; + } else { + return path.subPath(matchPos, matchPos + matchLength); + } + } + + /** + * Returns the position of the match + * @return + */ + public int getMatchPos() { + return matchPos; + } + + /** + * Returns the number of elements which where matched by the {@link Pattern}. + * @return + */ + public int getMatchLength() { + return matchLength; + } + + /** + * Returns true if the {@link Pattern} matched anything or false otherwise. + * @return + */ + public boolean isMatch() { + return matchLength > 0; + } + + /** + * Returns true if the {@link Pattern} matched the whole {@link Path}. + * @return + */ + public boolean isFullMatch() { + return pathLength == matchLength; + } + + MatchResult setPos(int matchPos) { + this.matchPos = matchPos; + return this; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/Matcher.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/Matcher.java new file mode 100644 index 00000000000..fd3347369f4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/Matcher.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.spi.commons.name; + +import org.apache.jackrabbit.spi.Path; + +/** + * Utility class for matching {@link Pattern}s against {@link Path}es. + */ +public final class Matcher { + + private Matcher() { + // Don't instantiate + } + + /** + * Match a pattern against an input path and return the remaining path. + * @param pattern + * @param input + * @return The remaining path after the match or null if the whole path + * was matched. + * @see MatchResult#getRemainder() + */ + public static Path match(Pattern pattern, Path input) { + return pattern.match(input).getRemainder(); + } + + /** + * Checks whether a pattern matches an input path. + * @param pattern + * @param input + * @return true if pattern matches the whole input. + * @see MatchResult#isFullMatch() + */ + public static boolean matches(Pattern pattern, Path input) { + return pattern.match(input).isFullMatch(); + } + + /** + * Find the first match of a pattern in a path. + * @param pattern + * @param input + * @return A {@link MatchResult} or null if the pattern does not occur in the + * input. + * @throws IllegalArgumentException if input is not normalized. + */ + public static MatchResult findMatch(Pattern pattern, Path input) { + return findMatch(pattern, input, 0); + } + + /** + * Find the first match of a pattern in a path starting at a given position. + * @param pattern + * @param input + * @param pos + * @return A {@link MatchResult} or null if the pattern does not occur in the + * input. + * @throws IllegalArgumentException if input is not normalized. + */ + public static MatchResult findMatch(Pattern pattern, Path input, int pos) { + int length = input.getLength(); + if (pos < 0 || pos >= length) { + throw new IllegalArgumentException("Index out of bounds"); + } + + for (int k = pos; k < length; k++) { + Path path = input.subPath(k, length); + MatchResult result = pattern.match(path); + if (result.isMatch()) { + return new MatchResult(input, k, result.getMatchLength()); + } + } + return null; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NameConstants.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NameConstants.java new file mode 100644 index 00000000000..d1cfa36f06f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NameConstants.java @@ -0,0 +1,757 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.name; + +import javax.jcr.Property; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; + +/** + * NameConstants... + */ +public class NameConstants { + + private static final NameFactory FACTORY = NameFactoryImpl.getInstance(); + + /** + * Extra Name for the root node + */ + public static final Name ROOT = FACTORY.create(Name.NS_DEFAULT_URI,""); + + /** + * jcr:system + */ + public static final Name JCR_SYSTEM = FACTORY.create(Name.NS_JCR_URI, "system"); + + /** + * jcr:nodeTypes + */ + public static final Name JCR_NODETYPES = FACTORY.create(Name.NS_JCR_URI, "nodeTypes"); + + /** + * jcr:uuid + */ + public static final Name JCR_UUID = FACTORY.create(Name.NS_JCR_URI, "uuid"); + + /** + * jcr:primaryType + */ + public static final Name JCR_PRIMARYTYPE = FACTORY.create(Name.NS_JCR_URI, "primaryType"); + + /** + * jcr:mixinTypes + */ + public static final Name JCR_MIXINTYPES = FACTORY.create(Name.NS_JCR_URI, "mixinTypes"); + + /** + * jcr:created + */ + public static final Name JCR_CREATED = FACTORY.create(Name.NS_JCR_URI, "created"); + + /** + * jcr:createdBy + */ + public static final Name JCR_CREATEDBY = FACTORY.create(Name.NS_JCR_URI, "createdBy"); + + /** + * jcr:lastModified + */ + public static final Name JCR_LASTMODIFIED = FACTORY.create(Name.NS_JCR_URI, "lastModified"); + + /** + * jcr:lastModifiedBy + */ + public static final Name JCR_LASTMODIFIEDBY = FACTORY.create(Name.NS_JCR_URI, "lastModifiedBy"); + + /** + * jcr:encoding + */ + public static final Name JCR_ENCODING = FACTORY.create(Name.NS_JCR_URI, "encoding"); + + /** + * jcr:mimeType + */ + public static final Name JCR_MIMETYPE = FACTORY.create(Name.NS_JCR_URI, "mimeType"); + + /** + * jcr:data + */ + public static final Name JCR_DATA = FACTORY.create(Name.NS_JCR_URI, "data"); + + /** + * jcr:content + */ + public static final Name JCR_CONTENT = FACTORY.create(Name.NS_JCR_URI, "content"); + + /** + * jcr:etag + */ + public static final Name JCR_ETAG = FACTORY.create(Name.NS_JCR_URI, "etag"); + + /** + * jcr:protocol + */ + public static final Name JCR_PROTOCOL = FACTORY.create(Name.NS_JCR_URI, "protocol"); + + /** + * jcr:host + */ + public static final Name JCR_HOST = FACTORY.create(Name.NS_JCR_URI, "host"); + + /** + * jcr:port + */ + public static final Name JCR_PORT = FACTORY.create(Name.NS_JCR_URI, "port"); + + /** + * jcr:repository + */ + public static final Name JCR_REPOSITORY = FACTORY.create(Name.NS_JCR_URI, "repository"); + + /** + * jcr:workspace + */ + public static final Name JCR_WORKSPACE = FACTORY.create(Name.NS_JCR_URI, "workspace"); + + /** + * jcr:id + */ + public static final Name JCR_ID = FACTORY.create(Name.NS_JCR_URI, "id"); + + /** + * jcr:description + */ + public static final Name JCR_DESCRIPTION = FACTORY.create(Property.JCR_DESCRIPTION); + + /** + * jcr:title + */ + public static final Name JCR_TITLE = FACTORY.create(Property.JCR_TITLE); + + //--------------------------------------< xml related item name constants > + + /** + * jcr:root (dummy name for root node used in XML serialization) + */ + public static final Name JCR_ROOT = FACTORY.create(Name.NS_JCR_URI, "root"); + + /** + * jcr:xmltext + */ + public static final Name JCR_XMLTEXT = FACTORY.create(Name.NS_JCR_URI, "xmltext"); + + /** + * jcr:xmlcharacters + */ + public static final Name JCR_XMLCHARACTERS = FACTORY.create(Name.NS_JCR_URI, "xmlcharacters"); + + //-----------------------------------------< query related name constants > + + /** + * jcr:score + */ + public static final Name JCR_SCORE = FACTORY.create(Name.NS_JCR_URI, "score"); + + /** + * jcr:path + */ + public static final Name JCR_PATH = FACTORY.create(Name.NS_JCR_URI, "path"); + + /** + * jcr:statement + */ + public static final Name JCR_STATEMENT = FACTORY.create(Name.NS_JCR_URI, "statement"); + + /** + * jcr:language + */ + public static final Name JCR_LANGUAGE = FACTORY.create(Name.NS_JCR_URI, "language"); + + //----------------------------------< locking related item name constants > + + /** + * jcr:lockOwner + */ + public static final Name JCR_LOCKOWNER = FACTORY.create(Name.NS_JCR_URI, "lockOwner"); + + /** + * jcr:lockIsDeep + */ + public static final Name JCR_LOCKISDEEP = FACTORY.create(Name.NS_JCR_URI, "lockIsDeep"); + + //-------------------------------< versioning related item name constants > + + /** + * jcr:versionStorage + */ + public static final Name JCR_VERSIONSTORAGE = FACTORY.create(Name.NS_JCR_URI, "versionStorage"); + + /** + * jcr:mergeFailed + */ + public static final Name JCR_MERGEFAILED = FACTORY.create(Name.NS_JCR_URI, "mergeFailed"); + + /** + * jcr:frozenNode + */ + public static final Name JCR_FROZENNODE = FACTORY.create(Name.NS_JCR_URI, "frozenNode"); + + /** + * jcr:frozenUuid + */ + public static final Name JCR_FROZENUUID = FACTORY.create(Name.NS_JCR_URI, "frozenUuid"); + + /** + * jcr:frozenPrimaryType + */ + public static final Name JCR_FROZENPRIMARYTYPE = FACTORY.create(Name.NS_JCR_URI, "frozenPrimaryType"); + + /** + * jcr:frozenMixinTypes + */ + public static final Name JCR_FROZENMIXINTYPES = FACTORY.create(Name.NS_JCR_URI, "frozenMixinTypes"); + + /** + * jcr:predecessors + */ + public static final Name JCR_PREDECESSORS = FACTORY.create(Name.NS_JCR_URI, "predecessors"); + + /** + * jcr:versionLabels + */ + public static final Name JCR_VERSIONLABELS = FACTORY.create(Name.NS_JCR_URI, "versionLabels"); + + /** + * jcr:successors + */ + public static final Name JCR_SUCCESSORS = FACTORY.create(Name.NS_JCR_URI, "successors"); + + /** + * jcr:isCheckedOut + */ + public static final Name JCR_ISCHECKEDOUT = FACTORY.create(Name.NS_JCR_URI, "isCheckedOut"); + + /** + * jcr:versionHistory + */ + public static final Name JCR_VERSIONHISTORY = FACTORY.create(Name.NS_JCR_URI, "versionHistory"); + + /** + * jcr:baseVersion + */ + public static final Name JCR_BASEVERSION = FACTORY.create(Name.NS_JCR_URI, "baseVersion"); + + /** + * jcr:childVersionHistory + */ + public static final Name JCR_CHILDVERSIONHISTORY = FACTORY.create(Name.NS_JCR_URI, "childVersionHistory"); + + /** + * jcr:rootVersion + */ + public static final Name JCR_ROOTVERSION = FACTORY.create(Name.NS_JCR_URI, "rootVersion"); + + /** + * jcr:versionableUuid + */ + public static final Name JCR_VERSIONABLEUUID = FACTORY.create(Name.NS_JCR_URI, "versionableUuid"); + + /** + * jcr:copiedFrom + * @since 2.0 + */ + public static final Name JCR_COPIEDFROM = FACTORY.create(Name.NS_JCR_URI, "copiedFrom"); + + /** + * jcr:activities + * @since 2.0 + */ + public static final Name JCR_ACTIVITIES = FACTORY.create(Name.NS_JCR_URI, "activities"); + + /** + * jcr:activity + * @since 2.0 + */ + public static final Name JCR_ACTIVITY = FACTORY.create(Name.NS_JCR_URI, "activity"); + + /** + * jcr:activityTitle + * @since 2.0 + */ + public static final Name JCR_ACTIVITY_TITLE = FACTORY.create(Name.NS_JCR_URI, "activityTitle"); + + /** + * jcr:configurations + * @since 2.0 + */ + public static final Name JCR_CONFIGURATIONS = FACTORY.create(Name.NS_JCR_URI, "configurations"); + + /** + * jcr:configuration + * @since 2.0 + */ + public static final Name JCR_CONFIGURATION = FACTORY.create(Name.NS_JCR_URI, "configuration"); + + + //--------------------------------< node type related item name constants > + + /** + * jcr:nodeTypeName + */ + public static final Name JCR_NODETYPENAME = FACTORY.create(Name.NS_JCR_URI, "nodeTypeName"); + + /** + * jcr:hasOrderableChildNodes + */ + public static final Name JCR_HASORDERABLECHILDNODES = FACTORY.create(Name.NS_JCR_URI, "hasOrderableChildNodes"); + + /** + * jcr:isMixin + */ + public static final Name JCR_ISMIXIN = FACTORY.create(Name.NS_JCR_URI, "isMixin"); + + /** + * jcr:supertypes + */ + public static final Name JCR_SUPERTYPES = FACTORY.create(Name.NS_JCR_URI, "supertypes"); + + /** + * jcr:propertyDefinition + */ + public static final Name JCR_PROPERTYDEFINITION = FACTORY.create(Name.NS_JCR_URI, "propertyDefinition"); + + /** + * jcr:name + */ + public static final Name JCR_NAME = FACTORY.create(Name.NS_JCR_URI, "name"); + + /** + * jcr:mandatory + */ + public static final Name JCR_MANDATORY = FACTORY.create(Name.NS_JCR_URI, "mandatory"); + + /** + * jcr:protected + */ + public static final Name JCR_PROTECTED = FACTORY.create(Name.NS_JCR_URI, "protected"); + + /** + * jcr:requiredType + */ + public static final Name JCR_REQUIREDTYPE = FACTORY.create(Name.NS_JCR_URI, "requiredType"); + + /** + * jcr:onParentVersion + */ + public static final Name JCR_ONPARENTVERSION = FACTORY.create(Name.NS_JCR_URI, "onParentVersion"); + + /** + * jcr:primaryItemName + */ + public static final Name JCR_PRIMARYITEMNAME = FACTORY.create(Name.NS_JCR_URI, "primaryItemName"); + + /** + * jcr:multiple + */ + public static final Name JCR_MULTIPLE = FACTORY.create(Name.NS_JCR_URI, "multiple"); + + /** + * jcr:valueConstraints + */ + public static final Name JCR_VALUECONSTRAINTS = FACTORY.create(Name.NS_JCR_URI, "valueConstraints"); + + /** + * jcr:defaultValues + */ + public static final Name JCR_DEFAULTVALUES = FACTORY.create(Name.NS_JCR_URI, "defaultValues"); + + /** + * jcr:autoCreated + */ + public static final Name JCR_AUTOCREATED = FACTORY.create(Name.NS_JCR_URI, "autoCreated"); + + /** + * jcr:childNodeDefinition + */ + public static final Name JCR_CHILDNODEDEFINITION = FACTORY.create(Name.NS_JCR_URI, "childNodeDefinition"); + + /** + * jcr:sameNameSiblings + */ + public static final Name JCR_SAMENAMESIBLINGS = FACTORY.create(Name.NS_JCR_URI, "sameNameSiblings"); + + /** + * jcr:defaultPrimaryType + */ + public static final Name JCR_DEFAULTPRIMARYTYPE = FACTORY.create(Name.NS_JCR_URI, "defaultPrimaryType"); + + /** + * jcr:requiredPrimaryTypes + */ + public static final Name JCR_REQUIREDPRIMARYTYPES = FACTORY.create(Name.NS_JCR_URI, "requiredPrimaryTypes"); + + + //-------------------------------< lifecycle related item name constants > + + /** + * jcr:lifecyclePolicy: This property is a reference to + * another node that contains lifecycle policy information. + * @since JCR 2.0 + */ + public static final Name JCR_LIFECYCLE_POLICY = + FACTORY.create(Name.NS_JCR_URI, "lifecyclePolicy"); + + /** + * jcr:currentLifecycleState: This property is a string + * identifying the current lifecycle state of this node. + * @since JCR 2.0 + */ + public static final Name JCR_CURRENT_LIFECYCLE_STATE = + FACTORY.create(Name.NS_JCR_URI, "currentLifecycleState"); + + //-------------------------------------------< node type name constants >--- + /** + * nt:unstructured + */ + public static final Name NT_UNSTRUCTURED = FACTORY.create(Name.NS_NT_URI, "unstructured"); + + /** + * nt:base + */ + public static final Name NT_BASE = FACTORY.create(Name.NS_NT_URI, "base"); + + /** + * nt:hierarchyNode + */ + public static final Name NT_HIERARCHYNODE = FACTORY.create(Name.NS_NT_URI, "hierarchyNode"); + + /** + * nt:resource + */ + public static final Name NT_RESOURCE = FACTORY.create(Name.NS_NT_URI, "resource"); + + /** + * nt:file + */ + public static final Name NT_FILE = FACTORY.create(Name.NS_NT_URI, "file"); + + /** + * nt:folder + */ + public static final Name NT_FOLDER = FACTORY.create(Name.NS_NT_URI, "folder"); + + /** + * mix:created + */ + public static final Name MIX_CREATED = FACTORY.create(Name.NS_MIX_URI, "created"); + + /** + * mix:lastModified + */ + public static final Name MIX_LASTMODIFIED = FACTORY.create(Name.NS_MIX_URI, "lastModified"); + + /** + * mix:title + */ + public static final Name MIX_TITLE = FACTORY.create(Name.NS_MIX_URI, "title"); + + /** + * mix:language + */ + public static final Name MIX_LANGUAGE = FACTORY.create(Name.NS_MIX_URI, "language"); + + /** + * mix:mimeType + */ + public static final Name MIX_MIMETYPE = FACTORY.create(Name.NS_MIX_URI, "mimeType"); + + /** + * mix:etag + */ + public static final Name MIX_ETAG = FACTORY.create(Name.NS_MIX_URI, "etag"); + + /** + * nt:address + */ + public static final Name NT_ADDRESS = FACTORY.create(Name.NS_NT_URI, "address"); + + /** + * nt:query + */ + public static final Name NT_QUERY = FACTORY.create(Name.NS_NT_URI, "query"); + + /** + * nt:share + */ + public static final Name NT_SHARE = FACTORY.create(Name.NS_NT_URI, "share"); + + /** + * mix:referenceable + */ + public static final Name MIX_REFERENCEABLE = FACTORY.create(Name.NS_MIX_URI, "referenceable"); + /** + * mix:referenceable + */ + public static final Name MIX_LOCKABLE = FACTORY.create(Name.NS_MIX_URI, "lockable"); + /** + * mix:versionable + */ + public static final Name MIX_VERSIONABLE = FACTORY.create(Name.NS_MIX_URI, "versionable"); + /** + * mix:simpleVersionable + */ + public static final Name MIX_SIMPLE_VERSIONABLE = FACTORY.create(Name.NS_MIX_URI, "simpleVersionable"); + /** + * mix:shareable + */ + public static final Name MIX_SHAREABLE = FACTORY.create(Name.NS_MIX_URI, "shareable"); + /** + * nt:versionHistory + */ + public static final Name NT_VERSIONHISTORY = FACTORY.create(Name.NS_NT_URI, "versionHistory"); + /** + * nt:version + */ + public static final Name NT_VERSION = FACTORY.create(Name.NS_NT_URI, "version"); + /** + * nt:versionLabels + */ + public static final Name NT_VERSIONLABELS = FACTORY.create(Name.NS_NT_URI, "versionLabels"); + /** + * nt:versionedChild + */ + public static final Name NT_VERSIONEDCHILD = FACTORY.create(Name.NS_NT_URI, "versionedChild"); + /** + * nt:frozenNode + */ + public static final Name NT_FROZENNODE = FACTORY.create(Name.NS_NT_URI, "frozenNode"); + /** + * nt:nodeType + */ + public static final Name NT_NODETYPE = FACTORY.create(Name.NS_NT_URI, "nodeType"); + /** + * nt:propertyDefinition + */ + public static final Name NT_PROPERTYDEFINITION = FACTORY.create(Name.NS_NT_URI, "propertyDefinition"); + /** + * nt:childNodeDefinition + */ + public static final Name NT_CHILDNODEDEFINITION = FACTORY.create(Name.NS_NT_URI, "childNodeDefinition"); + + /** + * mix:lifecycle: Only nodes with mixin node type + * mix:lifecycle may participate in a lifecycle. + * @since JCR 2.0 + */ + public static final Name MIX_LIFECYCLE = + FACTORY.create(Name.NS_MIX_URI, "lifecycle"); + + /** + * nt:activity + * @since 2.0 + */ + public static final Name NT_ACTIVITY = FACTORY.create(Name.NS_NT_URI, "activity"); + + /** + * nt:configuration + * @since 2.0 + */ + public static final Name NT_CONFIGURATION = FACTORY.create(Name.NS_NT_URI, "configuration"); + + //-------------------------------------------------------------------------- + + private static final Name rep(String local) { + return FACTORY.create(Name.NS_REP_URI, local); + } + + /** rep:root */ + public static final Name REP_ROOT = rep("root"); + + /** rep:system */ + public static final Name REP_SYSTEM = rep("system"); + + /** rep:versionStorage */ + public static final Name REP_VERSIONSTORAGE = rep("versionStorage"); + + /** rep:Activities */ + public static final Name REP_ACTIVITIES = rep("Activities"); + + /** rep:Configurations */ + public static final Name REP_CONFIGURATIONS = rep("Configurations"); + + /** rep:baseVersions */ + public static final Name REP_BASEVERSIONS = rep("baseVersions"); + + /** rep:VersionReference */ + public static final Name REP_VERSION_REFERENCE = rep("VersionReference"); + + /** rep:versions */ + public static final Name REP_VERSIONS = rep("versions"); + + /** rep:nodeTypes */ + public static final Name REP_NODETYPES = rep("nodeTypes"); + + /** rep:policy */ + public static final Name REP_POLICY = rep("policy"); + + /** rep:repoPolicy */ + public static final Name REP_REPO_POLICY = rep("repoPolicy"); + + /** rep:accesscontrol */ + public static final Name REP_ACCESSCONTROL = rep("accesscontrol"); + + /** rep:privileges */ + public static final Name REP_PRIVILEGES = rep("privileges"); + + /** rep:principalName */ + public static final Name REP_PRINCIPAL_NAME = rep("principalName"); + + /** rep:glob */ + public static final Name REP_GLOB = rep("glob"); + + /** rep:AccessControllable */ + public static final Name REP_ACCESS_CONTROLLABLE = rep("AccessControllable"); + + /** rep:RepoAccessControllable */ + public static final Name REP_REPO_ACCESS_CONTROLLABLE = rep("RepoAccessControllable"); + + /** rep:ACL */ + public static final Name REP_ACL = rep("ACL"); + + /** rep:ACE */ + public static final Name REP_ACE = rep("ACE"); + + /** rep:GrantACE */ + public static final Name REP_GRANT_ACE = rep("GrantACE"); + + /** rep:DenyACE */ + public static final Name REP_DENY_ACE = rep("DenyACE"); + + /** rep:AccessControl */ + public static final Name REP_ACCESS_CONTROL = rep("AccessControl"); + + /** rep:PrincipalAccessControl */ + public static final Name REP_PRINCIPAL_ACCESS_CONTROL = + rep("PrincipalAccessControl"); + + /** rep:nodePath */ + public static final Name REP_NODE_PATH = rep("nodePath"); + + /** + * The special wildcard name used as the name of residual item definitions. + */ + public static final Name ANY_NAME = FACTORY.create("", "*"); + + //------------------------------------------< system view name constants > + /** + * sv:node + */ + public static final Name SV_NODE = FACTORY.create(Name.NS_SV_URI, "node"); + /** + * sv:property + */ + public static final Name SV_PROPERTY = FACTORY.create(Name.NS_SV_URI, "property"); + /** + * sv:value + */ + public static final Name SV_VALUE = FACTORY.create(Name.NS_SV_URI, "value"); + /** + * sv:type + */ + public static final Name SV_TYPE = FACTORY.create(Name.NS_SV_URI, "type"); + /** + * sv:name + */ + public static final Name SV_NAME = FACTORY.create(Name.NS_SV_URI, "name"); + /** + * sv:multiple + */ + public static final Name SV_MULTIPLE = FACTORY.create(Name.NS_SV_URI, "multiple"); + + //--------------------------------------------< privilege name constants > + + /** jcr:read */ + public static final Name JCR_READ = + FACTORY.create(Privilege.JCR_READ); + + /** jcr:modifyProperties */ + public static final Name JCR_MODIFY_PROPERTIES = + FACTORY.create(Privilege.JCR_MODIFY_PROPERTIES); + + /** jcr:addChildNodes */ + public static final Name JCR_ADD_CHILD_NODES = + FACTORY.create(Privilege.JCR_ADD_CHILD_NODES); + + /** jcr:removeChildNodes */ + public static final Name JCR_REMOVE_CHILD_NODES = + FACTORY.create(Privilege.JCR_REMOVE_CHILD_NODES); + + /** jcr:removeNode */ + public static final Name JCR_REMOVE_NODE = + FACTORY.create(Privilege.JCR_REMOVE_NODE); + + /** jcr:readAccessControl */ + public static final Name JCR_READ_ACCESS_CONTROL = + FACTORY.create(Privilege.JCR_READ_ACCESS_CONTROL); + + /** jcr:modifyAccessControl */ + public static final Name JCR_MODIFY_ACCESS_CONTROL = + FACTORY.create(Privilege.JCR_MODIFY_ACCESS_CONTROL); + + /** jcr:nodeTypeManagement */ + public static final Name JCR_NODE_TYPE_MANAGEMENT = + FACTORY.create(Privilege.JCR_NODE_TYPE_MANAGEMENT); + + /** jcr:versionManagement */ + public static final Name JCR_VERSION_MANAGEMENT = + FACTORY.create(Privilege.JCR_VERSION_MANAGEMENT); + + /** jcr:lockManagement */ + public static final Name JCR_LOCK_MANAGEMENT = + FACTORY.create(Privilege.JCR_LOCK_MANAGEMENT); + + /** jcr:lifecycleManagement */ + public static final Name JCR_LIFECYCLE_MANAGEMENT = + FACTORY.create(Privilege.JCR_LIFECYCLE_MANAGEMENT); + + /** jcr:retentionManagement */ + public static final Name JCR_RETENTION_MANAGEMENT = + FACTORY.create(Privilege.JCR_RETENTION_MANAGEMENT); + + /** jcr:workspaceManagement */ + // TODO replace with Privilege constant once next JCR version is released + public static final Name JCR_WORKSPACE_MANAGEMENT = + FACTORY.create("{http://www.jcp.org/jcr/1.0}workspaceManagement"); + + /** jcr:nodeTypeDefinitionManagement */ + // TODO replace with Privilege constant once next JCR version is released + public static final Name JCR_NODE_TYPE_DEFINITION_MANAGEMENT = + FACTORY.create("{http://www.jcp.org/jcr/1.0}nodeTypeDefinitionManagement"); + + /** jcr:namespaceManagement */ + // TODO replace with Privilege constant once next JCR version is released + public static final Name JCR_NAMESPACE_MANAGEMENT = + FACTORY.create("{http://www.jcp.org/jcr/1.0}namespaceManagement"); + + /** jcr:write */ + public static final Name JCR_WRITE = FACTORY.create(Privilege.JCR_WRITE); + + /** jcr:all */ + public static final Name JCR_ALL = FACTORY.create(Privilege.JCR_ALL); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NameFactoryImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NameFactoryImpl.java new file mode 100644 index 00000000000..0e8d0d677ab --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NameFactoryImpl.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.name; + +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.Name; + +/** + * NameFactoryImpl... + */ +public class NameFactoryImpl implements NameFactory { + + private static final NameFactory INSTANCE = new NameFactoryImpl(); + + /** + * Cache of flyweight name instances. + * + * @see JCR-1663 + */ + private final HashCache cache = new HashCache(); + + private NameFactoryImpl() {}; + + public static NameFactory getInstance() { + return INSTANCE; + } + + //--------------------------------------------------------< NameFactory >--- + /** + * @see NameFactory#create(String, String) + */ + public Name create(String namespaceURI, String localName) throws IllegalArgumentException { + // NOTE: an empty localName and/or URI is valid (e.g. the root node name) + if (namespaceURI == null) { + throw new IllegalArgumentException("No namespaceURI specified"); + } + if (localName == null) { + throw new IllegalArgumentException("No localName specified"); + } + return cache.get(new NameImpl(namespaceURI, localName)); + } + + /** + * @see NameFactory#create(String) + */ + public Name create(String nameString) throws IllegalArgumentException { + if (nameString == null || "".equals(nameString)) { + throw new IllegalArgumentException("No Name literal specified"); + } + if (nameString.charAt(0) != '{') { + throw new IllegalArgumentException( + "Invalid Name literal: " + nameString); + } + int i = nameString.indexOf('}'); + if (i == -1) { + throw new IllegalArgumentException( + "Invalid Name literal: " + nameString); + } + if (i == nameString.length() - 1) { + throw new IllegalArgumentException( + "Invalid Name literal: " + nameString); + } + return (Name) cache.get(new NameImpl( + nameString.substring(1, i), nameString.substring(i + 1))); + } + + //--------------------------------------------------------< inner class >--- + /** + * Inner class implementing the Name interface. + */ + private static class NameImpl implements Name { + + /** The empty namespace uri */ + private static final String EMPTY = "".intern(); + + /** The memorized hash code of this name. */ + private transient int hash; + + /** The memorized string representation of this name. */ + private transient String string; + + /** The internalized namespace URI of this name. */ + private final String namespaceURI; + + /** The local part of this name. */ + private final String localName; + + private NameImpl(String namespaceURI, String localName) { + // internalize namespaceURI to improve performance of comparisons. + if (namespaceURI.length() == 0) { + // see JCR-2464 + this.namespaceURI = EMPTY; + } else { + this.namespaceURI = namespaceURI.intern(); + } + // localName is not internalized in order not to risk huge perm + // space for large repositories + this.localName = localName; + hash = 0; + } + + //-----------------------------------------------------------< Name >--- + /** + * @see Name#getLocalName() + */ + public String getLocalName() { + return localName; + } + + /** + * @see Name#getNamespaceURI() + */ + public String getNamespaceURI() { + return namespaceURI; + } + + //---------------------------------------------------------< Object >--- + /** + * Returns the string representation of this Name in the + * following format: + *

        + * {namespaceURI}localName + * + * @return the string representation of this Name. + * @see NameFactory#create(String) + * @see Object#toString() + */ + @Override + public String toString() { + // Name is immutable, we can store the string representation + if (string == null) { + string = '{' + namespaceURI + '}' + localName; + } + return string; + } + + /** + * Compares two names for equality. Returns true + * if the given object is a Name and has the same namespace + * URI and local part as this Name. + * + * @param obj the object to compare. + * @return true if the object is equal to this Name, + * false otherwise. + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof NameImpl) { + NameImpl other = (NameImpl) obj; + // we can use == operator for namespaceURI since it is internalized + return namespaceURI == other.namespaceURI && localName.equals(other.localName); + } + // some other Name implementation + if (obj instanceof Name) { + Name other = (Name) obj; + return namespaceURI.equals(other.getNamespaceURI()) && localName.equals(other.getLocalName()); + } + return false; + } + + /** + * Returns the hash code of this name. The hash code is + * computed from the namespace URI and local part of the + * name and memorized for better performance. + * + * @return hash code + * @see Object#hashCode() + */ + @Override + public int hashCode() { + // Name is immutable, we can store the computed hash code value + int h = hash; + if (h == 0) { + h = 17; + h = 37 * h + namespaceURI.hashCode(); + h = 37 * h + localName.hashCode(); + hash = h; + } + return h; + } + + //------------------------------------------------------< Cloneable >--- + /** + * Creates a clone of this Name. + * Overridden in order to make clone() public. + * + * @return a clone of this instance + * @throws CloneNotSupportedException never thrown + * @see Object#clone() + */ + @Override + public Object clone() throws CloneNotSupportedException { + // Name is immutable, no special handling required + return super.clone(); + } + + //-----------------------------------------------------< Comparable >--- + /** + * Compares two Names. + * + * @param o the object to compare. + * @return comparison result + * @throws ClassCastException if the given object is not a Name. + * @see Comparable#compareTo(Object) + */ + public int compareTo(Object o) { + if (this == o) { + return 0; + } + Name other = (Name) o; + if (namespaceURI.equals(other.getNamespaceURI())) { + return localName.compareTo(other.getLocalName()); + } else { + return namespaceURI.compareTo(other.getNamespaceURI()); + } + } + + //---------------------------------------------------< Serializable >--- + /** + * Creates a new Name instance using the proper constructor + * during deserialization in order to make sure that internalized strings + * are used where appropriate. + */ + private Object readResolve() { + return new NameImpl(namespaceURI, localName); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NamePath.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NamePath.java new file mode 100644 index 00000000000..4cd386afa9b --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/NamePath.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.spi.commons.name; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +final class NamePath extends RelativePath { + + /** Serial version UID */ + private static final long serialVersionUID = -2887665244213430950L; + + /** + * Name of the last path element. + */ + private final Name name; + + /** + * Optional index of the last path element. Set to + * {@link Path#INDEX_UNDEFINED} if not explicitly specified, + * otherwise contains the 1-based index. + */ + private final int index; + + public NamePath(Path parent, Name name, int index) { + super(parent); + assert name != null; + assert index >= 0; + this.name = name; + this.index = index; + } + + protected int getDepthModifier() { + return 1; + } + + protected Path getParent() throws RepositoryException { + if (parent != null) { + return parent; + } else { + return new CurrentPath(null); + } + } + + protected String getElementString() { + if (index > Path.INDEX_DEFAULT) { + return name + "[" + index + "]"; + } else { + return name.toString(); + } + } + + public Name getName() { + return name; + } + + @Override + public int getIndex() { + return index; + } + + @Override + public int getNormalizedIndex() { + if (index != INDEX_UNDEFINED) { + return index; + } else { + return INDEX_DEFAULT; + } + } + + /** + * Returns true as this path ends in a named element. + * + * @return true + */ + @Override + public boolean denotesName() { + return true; + } + + public boolean isCanonical() { + return parent != null && parent.isCanonical(); + } + + public boolean isNormalized() { + return parent == null + || (parent.isNormalized() + && !parent.denotesCurrent()); + } + + public Path getNormalizedPath() throws RepositoryException { + if (isNormalized()) { + return this; + } else { + // parent is guaranteed to be !null + Path normalized = parent.getNormalizedPath(); + if (normalized.denotesCurrent()) { + normalized = null; // special case: ./a + } + return new NamePath(normalized, name, index); + } + } + + public Path getCanonicalPath() throws RepositoryException { + if (isCanonical()) { + return this; + } else if (parent != null) { + return new NamePath(parent.getCanonicalPath(), name, index); + } else { + throw new RepositoryException( + "There is no canonical representation of " + this); + } + } + + /** + * Returns the last element of this path. + * + * @return last element of this path + */ + @Override + public AbstractPath getLastElement() { + if (parent != null) { + return new NamePath(null, name, index); + } else { + return this; + } + } + + //--------------------------------------------------------------< Object > + + @Override + public final boolean equals(Object that) { + if (this == that) { + return true; + } else if (that instanceof Path) { + Path path = (Path) that; + return path.denotesName() + && name.equals(path.getName()) + && getNormalizedIndex() == path.getNormalizedIndex() + && super.equals(that); + } else { + return false; + } + } + + @Override + public final int hashCode() { + return super.hashCode() * 37 + name.hashCode() + getNormalizedIndex(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/ParentPath.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/ParentPath.java new file mode 100644 index 00000000000..7b6d0a5e3bf --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/ParentPath.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.spi.commons.name; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * A relative path whose last element is the parent path element, i.e. "..". + */ +final class ParentPath extends RelativePath { + + /** Serial version UID */ + private static final long serialVersionUID = -688611157827116290L; + + /** The parent path ".." */ + public static final ParentPath PARENT_PATH = new ParentPath(null); + + /** Name of the parent element */ + public static final Name NAME = + NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, ".."); + + public ParentPath(Path parent) { + super(parent); + } + + protected int getDepthModifier() { + return -1; + } + + protected Path getParent() throws RepositoryException { + if (isNormalized()) { + return new ParentPath(this); + } else { + return parent.getAncestor(2); + } + } + + protected String getElementString() { + return NAME.getLocalName(); + } + + public Name getName() { + return NAME; + } + + /** + * Returns true as this path ends in the parent element. + * + * @return true + */ + @Override + public boolean denotesParent() { + return true; + } + + /** + * Returns false as a path with a ".." element is + * never canonical. + * + * @return false + */ + public boolean isCanonical() { + return false; + } + + public boolean isNormalized() { + return parent == null + || (parent.isNormalized() && parent.denotesParent()); + } + + public Path getNormalizedPath() throws RepositoryException { + if (isNormalized()) { + return this; + } else { + // parent is guaranteed to be !null + Path normalized = parent.getNormalizedPath(); + if (normalized.denotesParent()) { + return new ParentPath(normalized); // special case: ../.. + } else if (normalized.denotesCurrent()) { + return new ParentPath(null); // special case: ./.. + } else { + return normalized.getAncestor(1); + } + } + } + + public Path getCanonicalPath() throws RepositoryException { + if (parent != null) { + return parent.getCanonicalPath().getAncestor(1); + } else { + throw new RepositoryException( + "There is no canonical representation of .."); + } + } + + /** + * Returns the parent path "..". + * + * @return parent path + */ + @Override + public AbstractPath getLastElement() { + return PARENT_PATH; + } + + //--------------------------------------------------------------< Object > + + @Override + public final boolean equals(Object that) { + if (this == that) { + return true; + } else if (that instanceof Path) { + Path path = (Path) that; + return path.denotesParent() && super.equals(that); + } else { + return false; + } + } + + @Override + public final int hashCode() { + return super.hashCode() + 2; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathBuilder.java new file mode 100644 index 00000000000..4152b5b443b --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathBuilder.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.spi.commons.name; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; + +/** + * Helper class used to build a path from pre-parsed path elements. + *

        + * Note that this class does neither validate the format of the path elements nor + * does it validate the format of the entire path. + * This class should therefore only be used in situations, where the elements + * and the resulting path are known to be valid. The regular way of creating + * a Path object is by calling any of the + * PathFactory.create()methods. + */ +public final class PathBuilder { + + /** + * The path factory + */ + private final PathFactory factory; + + /** + * The current path + */ + private Path path = null; + + /** + * Creates a new PathBuilder to create a Path using the + * {@link PathFactoryImpl default PathFactory}. See + * {@link PathBuilder#PathBuilder(PathFactory)} for a constructor explicitly + * specifying the factory to use. + */ + public PathBuilder() { + this(PathFactoryImpl.getInstance()); + } + + /** + * Creates a new PathBuilder. + * + * @param factory The PathFactory used to create the elements and the final path. + */ + public PathBuilder(PathFactory factory) { + this.factory = factory; + } + + /** + * Creates a new PathBuilder and initialized it with the given path + * elements. + * + * @param elements + */ + public PathBuilder(Path.Element[] elements) { + this(); + path = factory.create(elements); + } + + /** + * Creates a new PathBuilder and initialized it with elements of the + * given path. + * + * @param parent + */ + public PathBuilder(Path parent) { + this(); + path = parent; + } + + /** + * Adds the {@link org.apache.jackrabbit.spi.PathFactory#getRootElement()}. + */ + public void addRoot() { + if (path != null) { + path = RootPath.ROOT_PATH.resolve(path); + } else { + path = RootPath.ROOT_PATH; + } + } + + /** + * Adds the given elements + * + * @param elements + */ + public void addAll(Path.Element[] elements) { + for (Path.Element element : elements) { + if (path != null) { + path = path.resolve(element); + } else { + path = factory.create(element); + } + } + } + + /** + * Inserts the element at the beginning of the path to be built. + * + * @param elem + */ + public void addFirst(Path.Element elem) { + Path first = factory.create(elem); + if (path != null) { + path = first.resolve(path); + } else { + path = first; + } + } + + /** + * Inserts the element at the beginning of the path to be built. + * + * @param name + */ + public void addFirst(Name name) { + addFirst(factory.createElement(name)); + } + + /** + * Inserts the element at the beginning of the path to be built. + * + * @param name + * @param index + */ + public void addFirst(Name name, int index) { + addFirst(factory.createElement(name, index)); + } + + /** + * Inserts the element at the end of the path to be built. + * + * @param elem + */ + public void addLast(Path.Element elem) { + if (path != null) { + path = path.resolve(elem); + } else { + path = factory.create(elem); + } + } + + /** + * Inserts the element at the end of the path to be built. + * + * @param name + */ + public void addLast(Name name) { + addLast(name, Path.INDEX_UNDEFINED); + } + + /** + * Inserts the element at the end of the path to be built. + * + * @param name + * @param index + */ + public void addLast(Name name, int index) { + path = new NamePath(path, name, index); + } + + /** + * Assembles the built path and returns a new {@link Path}. + * + * @return a new {@link Path} + * @throws MalformedPathException if the internal path element queue is empty. + */ + public Path getPath() throws MalformedPathException { + if (path != null) { + return path; + } else { + throw new MalformedPathException("empty path"); + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathFactoryImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathFactoryImpl.java new file mode 100644 index 00000000000..7a2326c1ce2 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathFactoryImpl.java @@ -0,0 +1,310 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.name; + +import java.util.ArrayList; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; + +/** + * PathFactoryImpl... + */ +public class PathFactoryImpl implements PathFactory { + + private static PathFactory FACTORY = new PathFactoryImpl(); + + private PathFactoryImpl() {} + + public static PathFactory getInstance() { + return FACTORY; + } + + //--------------------------------------------------------< PathFactory >--- + /** + * @see PathFactory#create(Path, Path, boolean) + */ + public Path create(Path parent, Path relPath, boolean normalize) throws IllegalArgumentException, RepositoryException { + if (relPath.isAbsolute()) { + throw new IllegalArgumentException( + "relPath is not a relative path: " + relPath); + } else { + Path path = parent.resolve(relPath); + if (normalize) { + return path.getNormalizedPath(); + } else { + return path; + } + } + } + + /** + * @see PathFactory#create(Path, Name, boolean) + */ + public Path create(Path parent, Name name, boolean normalize) throws RepositoryException { + return create(parent, name, Path.INDEX_UNDEFINED, normalize); + } + + /** + * @see PathFactory#create(Path, Name, int, boolean) + */ + public Path create(Path parent, Name name, int index, boolean normalize) throws IllegalArgumentException, RepositoryException { + if (RootPath.NAME.equals(name)) { + throw new IllegalArgumentException(); + } + Path path = new NamePath(parent, name, index); + if (normalize) { + return path.getNormalizedPath(); + } else { + return path; + } + } + + /** + * @see PathFactory#create(Name) + */ + public Path create(Name name) throws IllegalArgumentException { + if (name != null) { + return create(name, Path.INDEX_UNDEFINED); + } else { + throw new IllegalArgumentException("PathFactory.create(null)"); + } + } + + /** + * @see PathFactory#create(Name, int) + */ + public Path create(Name name, int index) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("PathFactory.create(null, index"); + } else if (index < Path.INDEX_UNDEFINED) { + throw new IllegalArgumentException( + "Index must not be negative: " + name + "[" + index + "]"); + } else if (CurrentPath.NAME.equals(name)) { + if (index == Path.INDEX_UNDEFINED) { + return CurrentPath.CURRENT_PATH; + } else { + throw new IllegalArgumentException(); + } + } else if (ParentPath.NAME.equals(name)) { + if (index == Path.INDEX_UNDEFINED) { + return ParentPath.PARENT_PATH; + } else { + throw new IllegalArgumentException(); + } + } else if (RootPath.NAME.equals(name)) { + if (index == Path.INDEX_UNDEFINED) { + return RootPath.ROOT_PATH; + } else { + throw new IllegalArgumentException(); + } + } else { + return new NamePath(null, name, index); + } + } + + public Path create(Path.Element element) { + if (element.denotesCurrent()) { + return CurrentPath.CURRENT_PATH; + } else if (element.denotesIdentifier()) { + return new IdentifierPath(element.getIdentifier()); + } else if (element.denotesName()) { + return new NamePath(null, element.getName(), element.getIndex()); + } else if (element.denotesParent()) { + return ParentPath.PARENT_PATH; + } else if (element.denotesRoot()) { + return RootPath.ROOT_PATH; + } else { + throw new IllegalArgumentException( + "Unknown path element type: " + element); + } + } + + /** + * @see PathFactory#create(org.apache.jackrabbit.spi.Path.Element[]) + */ + public Path create(Path.Element[] elements) throws IllegalArgumentException { + Path path = null; + for (Path.Element element : elements) { + if (element.denotesCurrent()) { + path = new CurrentPath(path); + } else if (element.denotesIdentifier()) { + if (path != null) { + throw new IllegalArgumentException(); + } + path = new IdentifierPath(element.getIdentifier()); + } else if (element.denotesName()) { + path = new NamePath(path, element.getName(), element.getIndex()); + } else if (element.denotesParent()) { + if (path != null && path.isAbsolute() && path.getDepth() == 0) { + throw new IllegalArgumentException(); + } + path = new ParentPath(path); + } else if (element.denotesRoot()) { + if (path != null) { + throw new IllegalArgumentException(); + } + path = RootPath.ROOT_PATH; + } + } + return path; + } + + /** + * @see PathFactory#create(String) + */ + public Path create(String pathString) throws IllegalArgumentException { + if (pathString == null || "".equals(pathString)) { + throw new IllegalArgumentException("No Path literal specified"); + } + // split into path elements + int lastPos = 0; + int pos = pathString.indexOf(Path.DELIMITER); + ArrayList list = new ArrayList(); + while (lastPos >= 0) { + Path.Element elem; + if (pos >= 0) { + elem = createElementFromString(pathString.substring(lastPos, pos)); + lastPos = pos + 1; + pos = pathString.indexOf(Path.DELIMITER, lastPos); + } else { + elem = createElementFromString(pathString.substring(lastPos)); + lastPos = -1; + } + list.add(elem); + } + return create(list.toArray(new Path.Element[list.size()])); + } + + /** + * @see PathFactory#createElement(Name) + */ + public Path.Element createElement(Name name) throws IllegalArgumentException { + if (name == null) { + throw new IllegalArgumentException("name must not be null"); + } else if (name.equals(ParentPath.NAME)) { + return ParentPath.PARENT_PATH; + } else if (name.equals(CurrentPath.NAME)) { + return CurrentPath.CURRENT_PATH; + } else if (name.equals(RootPath.NAME)) { + return RootPath.ROOT_PATH; + } else { + return new NamePath(null, name, Path.INDEX_UNDEFINED); + } + } + + /** + * @see PathFactory#createElement(Name, int) + */ + public Path.Element createElement(Name name, int index) throws IllegalArgumentException { + if (index < Path.INDEX_UNDEFINED) { + throw new IllegalArgumentException( + "The index may not be negative: " + name + "[" + index + "]"); + } else if (name == null) { + throw new IllegalArgumentException("The name must not be null"); + } else if (name.equals(ParentPath.NAME) + || name.equals(CurrentPath.NAME) + || name.equals(RootPath.NAME)) { + throw new IllegalArgumentException( + "Special path elements (root, '.' and '..') can not have an explicit index: " + + name + "[" + index + "]"); + } else { + return new NamePath(null, name, index); + } + } + + public Path.Element createElement(String identifier) throws IllegalArgumentException { + if (identifier == null) { + throw new IllegalArgumentException("The id must not be null."); + } else { + return new IdentifierPath(identifier); + } + } + + /** + * Create an element from the element string + */ + private Path.Element createElementFromString(String elementString) { + if (elementString == null) { + throw new IllegalArgumentException("null PathElement literal"); + } + if (elementString.equals(RootPath.NAME.toString())) { + return RootPath.ROOT_PATH; + } else if (elementString.equals(CurrentPath.CURRENT_PATH.getString())) { + return CurrentPath.CURRENT_PATH; + } else if (elementString.equals(ParentPath.PARENT_PATH.getString())) { + return ParentPath.PARENT_PATH; + } else if (elementString.startsWith("[") && elementString.endsWith("]") && elementString.length() > 2) { + return new IdentifierPath( + elementString.substring(1, elementString.length()-1)); + } + + NameFactory factory = NameFactoryImpl.getInstance(); + int pos = elementString.indexOf('['); + if (pos == -1) { + Name name = factory.create(elementString); + return new NamePath(null, name, Path.INDEX_UNDEFINED); + } + Name name = factory.create(elementString.substring(0, pos)); + int pos1 = elementString.indexOf(']'); + if (pos1 == -1) { + throw new IllegalArgumentException("invalid PathElement literal: " + elementString + " (missing ']')"); + } + try { + int index = Integer.valueOf(elementString.substring(pos + 1, pos1)); + if (index < 1) { + throw new IllegalArgumentException("invalid PathElement literal: " + elementString + " (index is 1-based)"); + } + return new NamePath(null, name, index); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid PathElement literal: " + elementString + " (" + e.getMessage() + ")"); + } + } + + /** + * @see PathFactory#getCurrentElement() + */ + public Path.Element getCurrentElement() { + return CurrentPath.CURRENT_PATH; + } + + /** + * @see PathFactory#getParentElement() + */ + public Path.Element getParentElement() { + return ParentPath.PARENT_PATH; + } + + /** + * @see PathFactory#getRootElement() + */ + public Path.Element getRootElement() { + return RootPath.ROOT_PATH; + } + + /** + * @see PathFactory#getRootPath() + */ + public Path getRootPath() { + return RootPath.ROOT_PATH; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathMap.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathMap.java new file mode 100644 index 00000000000..d4aba5327ea --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/PathMap.java @@ -0,0 +1,658 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.name; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; + +import java.util.List; +import java.util.Map; +import java.util.ArrayList; +import java.util.HashMap; + + +/** + * Generic path map that associates information with the individual path elements + * of a path. + */ +public class PathMap { + + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + /** + * Root element + */ + private final Element root = + new Element(PATH_FACTORY.getRootElement()); + + /** + * Map a path to a child. If exact is false, + * returns the last available item along the path that is stored in the map. + * @param path path to map + * @param exact flag indicating whether an exact match is required + * @return child, maybe null if exact is + * true + */ + public Element map(Path path, boolean exact) { + Path.Element[] elements = path.getElements(); + Element current = root; + + for (int i = 1; i < elements.length; i++) { + Element next = current.getChild(elements[i]); + if (next == null) { + if (exact) { + return null; + } + break; + } + current = next; + } + return current; + } + + /** + * Create an element given by its path. The path map will create any necessary + * intermediate elements. + * @param path path to child + * @param obj object to store at destination + */ + public Element put(Path path, T obj) { + Element element = put(path); + element.obj = obj; + return element; + } + + /** + * Put an element given by its path. The path map will create any necessary + * intermediate elements. + * @param path path to child + * @param element element to store at destination + */ + public void put(Path path, Element element) { + Path.Element[] elements = path.getElements(); + Element current = root; + + for (int i = 1; i < elements.length - 1; i++) { + Element next = current.getChild(elements[i]); + if (next == null) { + next = current.createChild(elements[i]); + } + current = next; + } + current.put(path.getNameElement(), element); + } + + /** + * Create an empty child given by its path. + * @param path path to child + */ + public Element put(Path path) { + Path.Element[] elements = path.getElements(); + Element current = root; + + for (int i = 1; i < elements.length; i++) { + Element next = current.getChild(elements[i]); + if (next == null) { + next = current.createChild(elements[i]); + } + current = next; + } + return current; + } + + /** + * Traverse the path map and call back requester. This method visits the root + * first, then its children. + * @param includeEmpty if true invoke call back on every child + * regardless, whether the associated object is empty + * or not; otherwise call back on non-empty children + * only + */ + public void traverse(ElementVisitor visitor, boolean includeEmpty) { + root.traverse(visitor, includeEmpty); + } + + /** + * Internal class holding the object associated with a certain + * path element. + */ + public final static class Element { + + /** + * Parent element + */ + private Element parent; + + /** + * Map of immediate children + */ + private Map>> children; + + /** + * Number of non-empty children + */ + private int childrenCount; + + /** + * Object associated with this element + */ + private T obj; + + /** + * Path.Element suitable for path construction associated with this + * element. The path element will never have a default index. Instead an + * undefined index value is set in that case. + */ + private Path.Element pathElement; + + /** + * 1-based index associated with this element where index=0 is + * equivalent to index=1. + */ + private int index; + + /** + * Create a new instance of this class with a path element. + * @param nameIndex path element of this child + */ + private Element(Path.Element nameIndex) { + this.index = nameIndex.getIndex(); + if (nameIndex.denotesName()) { + updatePathElement(nameIndex.getName(), index); + } else { + // root, current or parent + this.pathElement = nameIndex; + } + } + + /** + * Create a child of this node inside the path map. + * @param nameIndex position where child is created + * @return child + */ + private Element createChild(Path.Element nameIndex) { + Element element = new Element(nameIndex); + put(nameIndex, element); + return element; + } + + /** + * Updates the {@link #pathElement} with a new name and index value. + * + * @param name the new name. + * @param index the new index. + */ + private void updatePathElement(Name name, int index) { + if (index == Path.INDEX_DEFAULT) { + pathElement = PATH_FACTORY.createElement(name); + } else { + pathElement = PATH_FACTORY.createElement(name, index); + } + } + + /** + * Insert an empty child. Will shift all children having an index + * greater than or equal to the child inserted to the right. + * @param nameIndex position where child is inserted + */ + public void insert(Path.Element nameIndex) { + // convert 1-based index value to 0-base value + int index = getZeroBasedIndex(nameIndex); + if (children != null) { + List> list = children.get(nameIndex.getName()); + if (list != null && list.size() > index) { + for (int i = index; i < list.size(); i++) { + Element element = list.get(i); + if (element != null) { + element.index = element.getNormalizedIndex() + 1; + element.updatePathElement(element.getName(), element.index); + } + } + list.add(index, null); + } + } + } + + /** + * Return an element matching a name and index. + * @param nameIndex position where child is located + * @return element matching nameIndex or null if + * none exists. + */ + private Element getChild(Path.Element nameIndex) { + // convert 1-based index value to 0-base value + int index = getZeroBasedIndex(nameIndex); + Element element = null; + + if (children != null) { + List> list = children.get(nameIndex.getName()); + if (list != null && list.size() > index) { + element = list.get(index); + } + } + return element; + } + + /** + * Link a child of this node. Position is given by nameIndex. + * @param nameIndex position where child should be located + * @param element element to add + */ + public void put(Path.Element nameIndex, Element element) { + // convert 1-based index value to 0-base value + int index = getZeroBasedIndex(nameIndex); + if (children == null) { + children = new HashMap>>(); + } + List> list = children.get(nameIndex.getName()); + if (list == null) { + list = new ArrayList>(); + children.put(nameIndex.getName(), list); + } + while (list.size() < index) { + list.add(null); + } + if (list.size() == index) { + list.add(element); + } else { + list.set(index, element); + } + + element.parent = this; + element.index = nameIndex.getIndex(); + element.updatePathElement(nameIndex.getName(), element.index); + + childrenCount++; + } + + /** + * Remove a child. Will shift all children having an index greater than + * the child removed to the left. If there are no more children left in + * this element and no object is associated with this element, the + * element itself gets removed. + * + * @param nameIndex child's path element + * @return removed child, may be null + */ + public Element remove(Path.Element nameIndex) { + return remove(nameIndex, true, true); + } + + /** + * Remove a child. If shift is set to true, + * will shift all children having an index greater than the child + * removed to the left. If removeIfEmpty is set to + * true and there are no more children left in + * this element and no object is associated with this element, the + * element itself gets removed. + * + * @param nameIndex child's path element + * @param shift whether to shift same name siblings having a greater + * index to the left + * @param removeIfEmpty remove this element itself if it contains + * no more children and is not associated to + * an element + * @return removed child, may be null + */ + private Element remove(Path.Element nameIndex, boolean shift, + boolean removeIfEmpty) { + + // convert 1-based index value to 0-base value + int index = getZeroBasedIndex(nameIndex); + if (children == null) { + return null; + } + List> list = children.get(nameIndex.getName()); + if (list == null || list.size() <= index) { + return null; + } + Element element = list.set(index, null); + if (shift) { + for (int i = index + 1; i < list.size(); i++) { + Element sibling = list.get(i); + if (sibling != null) { + sibling.index--; + sibling.updatePathElement(sibling.getName(), sibling.index); + } + } + list.remove(index); + } + if (element != null) { + element.parent = null; + childrenCount--; + } + if (removeIfEmpty && childrenCount == 0 && obj == null && parent != null) { + parent.remove(getPathElement(), shift, true); + } + return element; + } + + /** + * Remove this element. Delegates the call to the parent item. + * Index of same name siblings will be shifted! + */ + public void remove() { + remove(true); + } + + /** + * Remove this element. Delegates the call to the parent item. + * @param shift if index of same name siblings will be shifted. + */ + public void remove(boolean shift) { + if (parent != null) { + parent.remove(getPathElement(), shift, true); + } else { + // Removing the root node is not possible: if it has become + // invalid, remove all its children and the associated object + children = null; + childrenCount = 0; + obj = null; + } + } + + /** + * Remove all children of this element. Removes this element itself + * if this element does not contain associated information. + */ + public void removeAll() { + children = null; + childrenCount = 0; + + if (obj == null && parent != null) { + parent.remove(getPathElement(), false, true); + } + } + + /** + * Sets a new list of children of this element. + * + * @param children map of children; keys are of type + * Path.PathElement and values + * are of type Element + */ + public void setChildren(Map> children) { + // Remove all children without removing the element itself + this.children = null; + childrenCount = 0; + + // Now add back all items + for (Map.Entry> entry : children.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + + // Special case: if map was empty, handle like removeAll() + if (childrenCount == 0 && obj == null && parent != null) { + parent.remove(getPathElement(), false, true); + } + } + + /** + * Return the object associated with this element + * @return object associated with this element + */ + public T get() { + return obj; + } + + /** + * Set the object associated with this element + * @param obj object associated with this element + */ + public void set(T obj) { + this.obj = obj; + + if (obj == null && childrenCount == 0 && parent != null) { + parent.remove(getPathElement(), false, true); + } + } + + /** + * Return the name of this element + * @return name + */ + public Name getName() { + return pathElement.getName(); + } + + /** + * Return the non-normalized 1-based index of this element. Note that + * this method can return a value of 0 which should be treated as 1. + * @return index + * @see #getNormalizedIndex() + */ + public int getIndex() { + return index; + } + + /** + * Return the 1-based index of this element. + * Same as {@link #getIndex()} except that an {@link Path#INDEX_UNDEFINED + * undefined index} value is automatically converted to the + * {@link Path#INDEX_DEFAULT default index} value. + * @return 1-based index + */ + public int getNormalizedIndex() { + return pathElement.getNormalizedIndex(); + } + + /** + * Return a path element pointing to this element + * @return path element + */ + public Path.Element getPathElement() { + if (index < Path.INDEX_DEFAULT) { + return PATH_FACTORY.createElement(getName()); + } else { + return PATH_FACTORY.createElement(getName(), index); + } + } + + /** + * Return the path of this element. + * @return path + * @throws MalformedPathException if building the path fails + */ + public Path getPath() throws MalformedPathException { + if (parent == null) { + return PATH_FACTORY.getRootPath(); + } + + PathBuilder builder = new PathBuilder(); + getPath(builder); + return builder.getPath(); + } + + /** + * Internal implementation of {@link #getPath()} that populates entries + * in a builder. On exit, builder contains the path + * of this element + */ + private void getPath(PathBuilder builder) { + if (parent == null) { + builder.addRoot(); + return; + } + parent.getPath(builder); + builder.addLast(pathElement); + } + + /** + * Checks whether this element has the specified path. Introduced to + * avoid catching a MalformedPathException for simple + * path comparisons. + * @param path path to compare to + * @return true if this child has the path + * path, false otherwise + */ + public boolean hasPath(Path path) { + return hasPath(path.getElements(), path.getLength()); + } + + /** + * Checks whether this element has the specified path, given by + * path elements. + * @param elements path elements to compare to + * @param len number of elements to compare to + * @return true if this element has the path given; + * otherwise false + */ + private boolean hasPath(Path.Element[] elements, int len) { + if (getPathElement().equals(elements[len - 1])) { + if (parent != null) { + return parent.hasPath(elements, len - 1); + } + return true; + } + return false; + } + + /** + * Return 0-based index of a path element. + */ + private static int getZeroBasedIndex(Path.Element nameIndex) { + return nameIndex.getNormalizedIndex() - 1; + } + + /** + * Recursively invoked traversal method. This method visits the element + * first, then its children. + * @param visitor visitor to invoke + * @param includeEmpty if true invoke call back on every + * element regardless, whether the associated object is empty + * or not; otherwise call back on non-empty children only + */ + public void traverse(ElementVisitor visitor, boolean includeEmpty) { + if (includeEmpty || obj != null) { + visitor.elementVisited(this); + } + if (children != null) { + for (List>list : children.values()) { + for (Element element : list) { + if (element != null) { + element.traverse(visitor, includeEmpty); + } + } + } + } + } + + /** + * Return the depth of this element. Defined to be 0 for the + * root element and n + 1 for some element if the depth of + * its parent is n. + */ + public int getDepth() { + if (parent != null) { + return parent.getDepth() + 1; + } + // Root + return Path.ROOT_DEPTH; + } + + /** + * Return a flag indicating whether the specified node is a + * child of this node. + * @param other node to check + */ + public boolean isAncestorOf(Element other) { + Element parent = other.parent; + while (parent != null) { + if (parent == this) { + return true; + } + parent = parent.parent; + } + return false; + } + + /** + * Return the parent of this element + * @return parent or null if this is the root element + */ + public Element getParent() { + return parent; + } + + /** + * Return the children count of this element + * @return children count + */ + public int getChildrenCount() { + return childrenCount; + } + + /** + * Return an iterator over all of this element's children. Every + * element returned by this iterator is of type {@link Element}. + */ + public List> getChildren() { + ArrayList> result = new ArrayList>(); + if (children != null) { + for (List> list : children.values()) { + for (Element element : list) { + if (element != null) { + result.add(element); + } + } + } + } + return result; + } + + /** + * Map a relPath starting at this Element. If + * exact is false, returns the last available + * item along the relPath that is stored in the map. + * + * @param relPath relPath to map + * @param exact flag indicating whether an exact match is required + * @return descendant, maybe null if exact is + * true + */ + public Element getDescendant(Path relPath, boolean exact) { + Path.Element[] elements = relPath.getElements(); + Element current = this; + + for (int i = 0; i < elements.length; i++) { + Element next = current.getChild(elements[i]); + if (next == null) { + if (exact) { + return null; + } + break; + } + current = next; + } + return current; + } + } + + /** + * Element visitor used in {@link PathMap#traverse} + */ + public interface ElementVisitor { + + /** + * Invoked for every element visited on a tree traversal + * @param element element visited + */ + void elementVisited(Element element); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/Pattern.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/Pattern.java new file mode 100644 index 00000000000..4ea720ab48b --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/Pattern.java @@ -0,0 +1,505 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.name; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Path.Element; + +/** + * Pattern to match normalized {@link Path}s. + * A pattern matches either a constant path, a name of a path element, a selection of + * either of two patterns or a sequence of two patterns. The matching process is greedy. + * That is, whenever a match is not unique only the longest match is considered. + * Matching consumes as many elements from the beginning of an input path as possible and + * returns what's left as an instance of {@link MatchResult}. + * Use the {@link Matcher} class for matching a whole path or finding matches inside a path. + */ +public abstract class Pattern { + + /** + * Matches this pattern against the input. + * @param input path to match with this pattern + * @return result from the matching pattern against input + * @throws IllegalArgumentException if input is not normalized + */ + public MatchResult match(Path input) { + try { + return match(new Context(input)).getMatchResult(); + } + catch (RepositoryException e) { + throw (IllegalArgumentException) new IllegalArgumentException("Path not normalized") + .initCause(e); + } + } + + protected abstract Context match(Context input) throws RepositoryException; + + /** + * Construct a new pattern which matches an exact path + * @param path + * @return A pattern which matches path and nothing else + * @throws IllegalArgumentException if path is null + */ + public static Pattern path(Path path) { + if (path == null) { + throw new IllegalArgumentException("path cannot be null"); + } + return new PathPattern(path); + } + + /** + * Construct a new pattern which matches a path element of a given name + * @param name + * @return A pattern which matches a path element with name name + * @throws IllegalArgumentException if name is null + */ + public static Pattern name(Name name) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null"); + } + return new NamePattern(name); + } + + /** + * Constructs a pattern which matches a path elements against regular expressions. + * @param namespaceUri A regular expression used for matching the name space URI of + * a path element. + * @param localName A regular expression used for matching the local name of a path + * element + * @return A pattern which matches a path element if namespaceUri matches the + * name space URI of the path element and localName matches the local name of the + * path element. + * @throws IllegalArgumentException if either namespaceUri or + * localName is null + * + * @see java.util.regex.Pattern + */ + public static Pattern name(String namespaceUri, String localName) { + if (namespaceUri == null || localName == null) { + throw new IllegalArgumentException("neither namespaceUri nor localName can be null"); + } + return new RegexPattern(namespaceUri, localName); + } + + private static final Pattern ALL_PATTERN = new Pattern() { + protected Context match(Context input) { + return input.matchToEnd(); + } + + public String toString() { + return "[ALL]"; + } + + }; + + /** + * A pattern which matches all input. + * @return + */ + public static Pattern all() { + return ALL_PATTERN; + } + + private static final Pattern NOTHING_PATTERN = new Pattern() { + protected Context match(Context input) { + return input.match(0); + } + + public String toString() { + return "[NOTHING]"; + } + }; + + /** + * A pattern which matches nothing. + * @return + */ + public static Pattern nothing() { + return NOTHING_PATTERN; + } + + /** + * A pattern which matches pattern1 followed by pattern2 and + * returns the longer of the two matches. + * @param pattern1 + * @param pattern2 + * @return + * @throws IllegalArgumentException if either argument is null + */ + public static Pattern selection(Pattern pattern1, Pattern pattern2) { + if (pattern1 == null || pattern2 == null) { + throw new IllegalArgumentException("Neither pattern can be null"); + } + return new SelectPattern(pattern1, pattern2); + } + + /** + * A pattern which matches pattern1 followed by pattern2. + * @param pattern1 + * @param pattern2 + * @return + */ + public static Pattern sequence(Pattern pattern1, Pattern pattern2) { + if (pattern1 == null || pattern2 == null) { + throw new IllegalArgumentException("Neither pattern can be null"); + } + return new SequencePattern(pattern1, pattern2); + } + + /** + * A pattern which matches pattern as many times as possible + * @param pattern + * @return + */ + public static Pattern repeat(Pattern pattern) { + if (pattern == null) { + throw new IllegalArgumentException("Pattern can not be null"); + } + return new RepeatPattern(pattern); + } + + /** + * A pattern which matches pattern as many times as possible + * but at least min times and at most max times. + * @param pattern + * @param min + * @param max + * @return + */ + public static Pattern repeat(Pattern pattern, int min, int max) { + if (pattern == null) { + throw new IllegalArgumentException("Pattern can not be null"); + } + return new RepeatPattern(pattern, min, max); + } + + // -----------------------------------------------------< Context >--- + + private static class Context { + private final Path path; + private final int length; + private final int pos; + private final boolean isMatch; + + public Context(Path path) { + super(); + this.path = path; + length = path.getLength(); + isMatch = false; + pos = 0; + } + + public Context(Context context, int pos, boolean matched) { + path = context.path; + length = context.length; + this.pos = pos; + this.isMatch = matched; + if (pos > length) { + throw new IllegalArgumentException("Cannot match beyond end of input"); + } + } + + public Context matchToEnd() { + return new Context(this, length, true); + } + + public Context match(int count) { + return new Context(this, pos + count, true); + } + + public Context noMatch() { + return new Context(this, this.pos, false); + } + + public boolean isMatch() { + return isMatch; + } + + public Path getRemainder() throws RepositoryException { + if (pos >= length) { + return null; + } + else { + return path.subPath(pos, length); + } + } + + public boolean isExhausted() { + return pos == length; + } + + public MatchResult getMatchResult() { + return new MatchResult(path, isMatch? pos : 0); + } + + public String toString() { + return pos + " @ " + path; + } + + } + + // -----------------------------------------------------< SelectPattern >--- + + private static class SelectPattern extends Pattern { + private final Pattern pattern1; + private final Pattern pattern2; + + public SelectPattern(Pattern pattern1, Pattern pattern2) { + super(); + this.pattern1 = pattern1; + this.pattern2 = pattern2; + } + + protected Context match(Context input) throws RepositoryException { + Context remainder1 = pattern1.match(input); + Context remainder2 = pattern2.match(input); + return remainder1.pos > remainder2.pos ? + remainder1 : remainder2; + } + + public String toString() { + return new StringBuffer() + .append("(") + .append(pattern1) + .append("|") + .append(pattern2) + .append(")") + .toString(); + } + } + + // -----------------------------------------------------< SequencePattern >--- + + private static class SequencePattern extends Pattern { + private final Pattern pattern1; + private final Pattern pattern2; + + public SequencePattern(Pattern pattern1, Pattern pattern2) { + super(); + this.pattern1 = pattern1; + this.pattern2 = pattern2; + } + + protected Context match(Context input) throws RepositoryException { + Context context1 = pattern1.match(input); + if (context1.isMatch()) { + return pattern2.match(context1); + } + else { + return input.noMatch(); + } + } + + public String toString() { + return new StringBuffer() + .append("(") + .append(pattern1) + .append(", ") + .append(pattern2) + .append(")") + .toString(); + } + } + + // -----------------------------------------------------< RepeatPattern >--- + + private static class RepeatPattern extends Pattern { + private final Pattern pattern; + private final int min; + private final int max; + private boolean hasBounds; + + public RepeatPattern(Pattern pattern) { + this(pattern, 0, 0); + this.hasBounds = false; + } + + public RepeatPattern(Pattern pattern, int min, int max) { + super(); + this.pattern = pattern; + this.min = min; + this.max = max; + this.hasBounds = true; + } + + protected Context match(Context input) throws RepositoryException { + Context nextInput; + Context output = input.match(0); + int matchCount = -1; + do { + nextInput = output; + output = pattern.match(nextInput); + matchCount++; + } while (output.isMatch() && (output.pos > nextInput.pos)); + + if (!hasBounds() || (min <= matchCount && matchCount <= max)) { + return nextInput; + } + else { + return input.noMatch(); + } + } + + private boolean hasBounds() { + return hasBounds; + } + + public String toString() { + return new StringBuffer() + .append("(") + .append(pattern) + .append(")*") + .toString(); + } + + } + + // -----------------------------------------------------< PathPattern >--- + + private static class PathPattern extends Pattern { + private final Path path; + private final Element[] patternElements; + + public PathPattern(Path path) { + super(); + this.path = path; + patternElements = path.getElements(); + } + + protected Context match(Context input) throws RepositoryException { + if (input.isExhausted()) { + return input; + } + + Path inputPath = input.getRemainder(); + if (!inputPath.isNormalized()) { + throw new IllegalArgumentException("Not normalized"); + } + + Element[] inputElements = inputPath.getElements(); + int inputLength = inputElements.length; + int patternLength = patternElements.length; + if (patternLength > inputLength) { + return input.noMatch(); + } + + for (int k = 0; k < patternLength; k++) { + if (!patternElements[k].equals(inputElements[k])) { + return input.noMatch(); + } + } + + return input.match(patternLength); + } + + public String toString() { + return new StringBuffer() + .append("\"") + .append(path) + .append("\"") + .toString(); + } + } + + // -----------------------------------------------------< AbstractNamePattern >--- + + private static abstract class AbstractNamePattern extends Pattern { + protected abstract boolean matches(Element element); + + protected Context match(Context input) throws RepositoryException { + if (input.isExhausted()) { + return input.noMatch(); + } + + Path inputPath = input.getRemainder(); + if (!inputPath.isNormalized()) { + throw new IllegalArgumentException("Not normalized"); + } + + Element[] inputElements = inputPath.getElements(); + if (inputElements.length < 1 || !matches(inputElements[0])) { + return input.noMatch(); + } + + return input.match(1); + } + + } + + // -----------------------------------------------------< NameNamePattern >--- + + private static class NamePattern extends AbstractNamePattern { + private final Name name; + + public NamePattern(Name name) { + super(); + this.name = name; + } + + protected boolean matches(Element element) { + return name.equals(element.getName()); + } + + public String toString() { + return new StringBuffer() + .append("\"") + .append(name) + .append("\"") + .toString(); + } + } + + // -----------------------------------------------------< StringNamePattern >--- + + private static class RegexPattern extends AbstractNamePattern { + private final java.util.regex.Pattern namespaceUri; + private final java.util.regex.Pattern localName; + private final String localNameStr; + private final String namespaceUriStr; + + public RegexPattern(String namespaceUri, String localName) { + super(); + + this.namespaceUri = java.util.regex.Pattern.compile(namespaceUri); + this.localName = java.util.regex.Pattern.compile(localName); + this.namespaceUriStr = namespaceUri; + this.localNameStr = localName; + } + + protected boolean matches(Element element) { + Name name = element.getName(); + boolean nsMatches = namespaceUri.matcher(name.getNamespaceURI()).matches(); + boolean localMatches = localName.matcher(name.getLocalName()).matches(); + return nsMatches && localMatches; + } + + public String toString() { + return new StringBuffer() + .append("\"{") + .append(namespaceUriStr) + .append("}") + .append(localNameStr) + .append("\"") + .toString(); + } + } + +} + diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/RelativePath.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/RelativePath.java new file mode 100644 index 00000000000..bb568bcaa64 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/RelativePath.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.spi.commons.name; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Path; + +abstract class RelativePath extends AbstractPath { + + /** Serial version UID */ + private static final long serialVersionUID = 5707676677044863127L; + + protected final Path parent; + + private final boolean absolute; + + private final boolean identifier; + + private final int depth; + + private final int length; + + protected RelativePath(Path parent) { + this.parent = parent; + if (parent != null) { + this.absolute = parent.isAbsolute(); + this.identifier = parent.isIdentifierBased(); + this.depth = parent.getDepth() + getDepthModifier(); + this.length = parent.getLength() + 1; + } else { + this.absolute = false; + this.identifier = false; + this.depth = getDepthModifier(); + this.length = 1; + } + } + + protected abstract int getDepthModifier(); + + protected abstract Path getParent() throws RepositoryException; + + protected abstract String getElementString(); + + public final boolean isIdentifierBased() { + return identifier; + } + + public final boolean isAbsolute() { + return absolute; + } + + public final Path getAncestor(int degree) throws RepositoryException { + if (degree < 0) { + throw new IllegalArgumentException( + "Invalid ancestor degree " + degree); + } else if (degree == 0) { + return getNormalizedPath(); + } else { + return getParent().getAncestor(degree - 1); + } + } + + public final int getAncestorCount() { + if (absolute) { + return depth; + } else { + return -1; + } + } + + public final int getDepth() { + return depth; + } + + public final int getLength() { + return length; + } + + public final Path subPath(int from, int to) { + if (from < 0 || length < to || to <= from) { + throw new IllegalArgumentException( + this + ".subPath(" + from + ", " + to + ")"); + } else if (from == 0 && to == length) { + // this is only case where parent can be null (from = 0, to = 1) + return this; + } else if (to < length) { + return parent.subPath(from, to); + } else if (from < to - 1) { + return parent.subPath(from, to - 1).resolve(getNameElement()); + } else { + return getLastElement(); + } + } + + public final Element[] getElements() { + Element[] elements = new Element[length]; + Path path = this; + for (int i = 1; i <= length; i++) { + elements[length - i] = path.getNameElement(); + path = path.getFirstElements(); + } + return elements; + } + + /** + * Returns the first elements of this path. + * + * @return first elements of this path, or null + */ + @Override + public Path getFirstElements() { + return parent; + } + + public String getString() { + if (parent != null) { + return parent.getString() + Path.DELIMITER + getElementString(); + } else { + return getElementString(); + } + } + + //--------------------------------------------------------------< Object > + + public boolean equals(Object that) { + if (this == that) { + return true; + } else if (that instanceof RelativePath) { + RelativePath path = (RelativePath) that; + if (parent != null) { + return parent.equals(path.parent); + } else { + return path.parent == null; + } + } else { + return false; + } + } + + public int hashCode() { + if (parent != null) { + return parent.hashCode(); + } else { + return 17; + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/RootPath.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/RootPath.java new file mode 100644 index 00000000000..b8255b00ada --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/RootPath.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.name; + +import javax.jcr.PathNotFoundException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +final class RootPath extends AbstractPath { + + /** Singleton instance */ + public static final RootPath ROOT_PATH = new RootPath(); + + /** Serial version UID */ + private static final long serialVersionUID = 8621451607549214925L; + + /** Name of the root element */ + public static final Name NAME = + NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, ""); + + /** Hidden constructor */ + private RootPath() { + } + + public Name getName() { + return NAME; + } + + /** + * Returns true as this is the root path. + * + * @return true + */ + @Override + public boolean denotesRoot() { + return true; + } + + /** + * Returns false as this is the root path. + * + * @return false + */ + public boolean isIdentifierBased() { + return false; + } + + /** + * Returns true as this is the root path. + * + * @return true + */ + public boolean isAbsolute() { + return true; + } + + /** + * Returns true as this is the root path. + * + * @return true + */ + public boolean isCanonical() { + return true; + } + + /** + * Returns true as this is the root path. + * + * @return true + */ + public boolean isNormalized() { + return true; + } + + /** + * Returns this path as this is the root path. + * + * @return root path + */ + public Path getNormalizedPath() { + return this; + } + + /** + * Returns this path as this is the root path. + * + * @return root path + */ + public Path getCanonicalPath() { + return this; + } + + public Path getAncestor(int degree) + throws IllegalArgumentException, PathNotFoundException { + if (degree < 0) { + throw new IllegalArgumentException( + "/.getAncestor(" + degree + ")"); + } else if (degree > 0) { + throw new PathNotFoundException( + "/.getAncestor(" + degree + ")"); + } else { + return this; + } + } + + /** + * Returns zero as this is the root path. + * + * @return zero + */ + public int getAncestorCount() { + return 0; + } + + /** + * Returns one as this is the root path. + * + * @return one + */ + public int getLength() { + return 1; + } + + /** + * Returns zero as this is the root path. + * + * @return zero + */ + public int getDepth() { + return 0; + } + + public Path subPath(int from, int to) throws IllegalArgumentException { + if (from == 0 && to == 1) { + return this; + } else { + throw new IllegalArgumentException( + "/.subPath(" + from + ", " + to + ")"); + } + } + + public Element[] getElements() { + return new Element[] { ROOT_PATH }; + } + + public Element getNameElement() { + return ROOT_PATH; + } + + public String getString() { + return "{}"; + } + + //--------------------------------------------------------< Serializable > + + /** Returns the singleton instance of this class */ + public Object readResolve() { + return ROOT_PATH; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/package-info.java new file mode 100644 index 00000000000..aadb278536d --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/name/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.name; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/AbstractNamespaceResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/AbstractNamespaceResolver.java new file mode 100644 index 00000000000..bdcb95227c4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/AbstractNamespaceResolver.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.spi.commons.namespace; + +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; + +/** + * Provides default implementations for the methods: + *

          + *
        • {@link #addListener(NamespaceListener)}
        • + *
        • {@link #removeListener(NamespaceListener)}
        • + *
        + * Subclasses may overwrite those methods with more efficient implementations + * e.g. using caching. This class also adds optional support for + * {@link NamespaceListener}s. To enable listener support call the constructor + * with supportListeners set to true. The default + * constructor will not enable listener support and all listener related + * methods will throw an {@link UnsupportedOperationException} in that case. + * + * @deprecated https://issues.apache.org/jira/browse/JCR-1700 + */ +public abstract class AbstractNamespaceResolver implements NamespaceResolver { + + private final Set listeners; + + /** + * Creates a AbstractNamespaceResolver without listener + * support. + */ + public AbstractNamespaceResolver() { + this(false); + } + + /** + * Creates a AbstractNamespaceResolver with listener support if + * supportListeners is set to true. + * + * @param supportListeners if true listener are supported by + * this instance. + */ + public AbstractNamespaceResolver(boolean supportListeners) { + if (supportListeners) { + listeners = new HashSet(); + } else { + listeners = null; + } + } + + //--------------------------------------------< NamespaceListener support > + + /** + * Registers listener to get notifications when namespace + * mappings change. + * + * @param listener the listener to register. + * @throws UnsupportedOperationException if listener support is not enabled + * for this AbstractNamespaceResolver. + */ + public void addListener(NamespaceListener listener) { + if (listeners == null) { + throw new UnsupportedOperationException("addListener"); + } + synchronized (listeners) { + listeners.add(listener); + } + } + + /** + * Removes the listener from this NamespaceRegistery. + * + * @param listener the listener to remove. + * @throws UnsupportedOperationException if listener support is not enabled + * for this AbstractNamespaceResolver. + */ + public void removeListener(NamespaceListener listener) { + if (listeners == null) { + throw new UnsupportedOperationException("removeListener"); + } + synchronized (listeners) { + listeners.remove(listener); + } + } + + /** + * Notifies the listeners that a new namespace uri has been + * added and mapped to prefix. + * + * @param prefix the prefix. + * @param uri the namespace uri. + */ + protected void notifyNamespaceAdded(String prefix, String uri) { + if (listeners == null) { + throw new UnsupportedOperationException("notifyNamespaceAdded"); + } + // addition is infrequent compared to listener registration + // -> use copy-on-read + NamespaceListener[] currentListeners; + synchronized (listeners) { + int i = 0; + currentListeners = new NamespaceListener[listeners.size()]; + for (Iterator it = listeners.iterator(); it.hasNext();) { + currentListeners[i++] = (NamespaceListener) it.next(); + } + } + for (int i = 0; i < currentListeners.length; i++) { + currentListeners[i].namespaceAdded(prefix, uri); + } + } + + /** + * Notifies listeners that an existing namespace uri has been remapped + * to a new prefix. + * + * @param oldPrefix the old prefix. + * @param newPrefix the new prefix. + * @param uri the associated namespace uri. + */ + protected void notifyNamespaceRemapped(String oldPrefix, + String newPrefix, + String uri) { + if (listeners == null) { + throw new UnsupportedOperationException("notifyNamespaceRemapped"); + } + // remapping is infrequent compared to listener registration + // -> use copy-on-read + NamespaceListener[] currentListeners; + synchronized (listeners) { + int i = 0; + currentListeners = new NamespaceListener[listeners.size()]; + for (Iterator it = listeners.iterator(); it.hasNext();) { + currentListeners[i++] = (NamespaceListener) it.next(); + } + } + for (int i = 0; i < currentListeners.length; i++) { + currentListeners[i].namespaceRemapped(oldPrefix, newPrefix, uri); + } + } + + /** + * Notifies the listeners that the namespace with the given uri + * has been removed from the mapping. + * + * @param uri the namespace uri. + * @see NamespaceListener#namespaceRemoved(String) + */ + protected void notifyNamespaceRemoved(String uri) { + if (listeners == null) { + throw new UnsupportedOperationException("notifyNamespaceRemapped"); + } + // removal is infrequent compared to listener registration + // -> use copy-on-read + NamespaceListener[] currentListeners; + synchronized (listeners) { + int i = 0; + currentListeners = new NamespaceListener[listeners.size()]; + for (Iterator it = listeners.iterator(); it.hasNext();) { + currentListeners[i++] = (NamespaceListener) it.next(); + } + } + for (int i = 0; i < currentListeners.length; i++) { + currentListeners[i].namespaceRemoved(uri); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceAdder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceAdder.java new file mode 100644 index 00000000000..883aa7742cc --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceAdder.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.spi.commons.namespace; + +import javax.jcr.NamespaceRegistry; +import javax.jcr.NamespaceException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; +import java.util.Map; +import java.util.Iterator; + +public class NamespaceAdder { + + private final NamespaceRegistry registry; + + public NamespaceAdder(NamespaceRegistry nsr) { + registry = nsr; + } + + public void addNamespaces(NamespaceMapping nsm) + throws NamespaceException, UnsupportedRepositoryOperationException, RepositoryException { + Map m = nsm.getPrefixToURIMapping(); + for (Iterator i = m.entrySet().iterator(); i.hasNext();) { + Map.Entry e = (Map.Entry) i.next(); + String prefix = (String) e.getKey(); + String uri = (String) e.getValue(); + registry.registerNamespace(prefix, uri); + } + } + + public void addNamespace(String prefix, String uri) + throws NamespaceException, UnsupportedRepositoryOperationException, RepositoryException { + registry.registerNamespace(prefix, uri); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceExtractor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceExtractor.java new file mode 100644 index 00000000000..54bd1fd7d71 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceExtractor.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.spi.commons.namespace; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.ContentHandler; +import org.xml.sax.XMLReader; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.XMLReaderFactory; +import org.xml.sax.helpers.DefaultHandler; + +import javax.jcr.NamespaceException; +import java.util.Map; +import java.util.HashMap; +import java.io.FileInputStream; + +/** + * Extracts namespace mapping information from an XML file. + * XML file is parsed and all startPrefixMapping events + * are intercepted. Scoping of prefix mapping within the XML file + * may result in multiple namespace using the same prefix. This + * is handled by mangling the prefix when required. + * + * The resulting NamespaceMapping implements NamespaceResolver + * and can be used by tools (such as o.a.j.tools.nodetype.CompactNodeTypeDefWriter) + * to resolve namespaces. + */ +public class NamespaceExtractor { + private static Logger log = LoggerFactory.getLogger(NamespaceExtractor.class); + private final NamespaceMapping mapping = new NamespaceMapping(); + private final Map basePrefixes = new HashMap(); + private String defaultBasePrefix; + + /** + * Constructor + * @param fileName + * @param dpb + * @throws NamespaceException + */ + public NamespaceExtractor(String fileName, String dpb) throws NamespaceException { + defaultBasePrefix = dpb; + try{ + ContentHandler handler = new NamespaceExtractor.NamespaceHandler(); + XMLReader parser = XMLReaderFactory.createXMLReader(); + parser.setContentHandler(handler); + parser.parse(new InputSource(new FileInputStream(fileName))); + } catch(Exception e){ + throw new NamespaceException(); + } + } + + /** + * getNamespaceMapping + * @return a NamespaceMapping + */ + public NamespaceMapping getNamespaceMapping(){ + return mapping; + } + + /** + * SAX ContentHandler that reacts to namespace mappings in incoming XML. + */ + private class NamespaceHandler extends DefaultHandler { + public void startPrefixMapping(String prefix, String uri) throws SAXException { + if (uri == null) uri = ""; + + //Replace the empty prefix with the defaultBasePrefix + if (prefix == null || prefix.equals("")){ + prefix = defaultBasePrefix; + } + + try{ + // if prefix already used + if (mapping.hasPrefix(prefix)){ + int c; + Integer co = (Integer) basePrefixes.get(prefix); + if (co == null) { + basePrefixes.put(prefix, new Integer(1)); + c = 1; + } else { + c = co.intValue() + 1; + basePrefixes.put(prefix, new Integer(c)); + } + prefix = prefix + "_" + c; + } + mapping.setMapping(prefix, uri); + } catch(NamespaceException e){ + String msg = e.getMessage(); + log.debug(msg); + } + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceListener.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceListener.java new file mode 100644 index 00000000000..a3ee219e596 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceListener.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.spi.commons.namespace; + +/** + * Receives notifications when a namespace mapping changes. + * + * @deprecated https://issues.apache.org/jira/browse/JCR-1700 + */ +public interface NamespaceListener { + + /** + * Notifies the listeners that an existing namespace uri has + * been re-mapped from oldPrefix to newPrefix. + * + * @param oldPrefix the old prefix. + * @param newPrefix the new prefix. + * @param uri the associated namespace uri. + */ + public void namespaceRemapped(String oldPrefix, String newPrefix, String uri); + + /** + * Notifies the listeners that a new namespace uri has been + * added and mapped to prefix. + * + * @param prefix the prefix. + * @param uri the namespace uri. + */ + public void namespaceAdded(String prefix, String uri); + + /** + * Notifies the listeners that the namespace with the given uri has been + * unregistered. + * + * @param uri the namespace uri. + */ + public void namespaceRemoved(String uri); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceMapping.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceMapping.java new file mode 100644 index 00000000000..8ef8200910e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceMapping.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.spi.commons.namespace; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.NamespaceException; + +/** + * A Simple Namespace Mapping table. Mappings can be added + * and then the object can be used as a NamespaceResolver. Additionally, it can + * be based on a underlying NamespaceResolver + */ +public class NamespaceMapping implements NamespaceResolver { + + /** local uris */ + private final Map prefixToURI = new HashMap(); + + /** local prefix */ + private final Map URIToPrefix = new HashMap(); + + /** base */ + private final NamespaceResolver base; + + public NamespaceMapping() { + this.base = null; + } + + /** + * Constructor + * @param base fallback resolver + */ + public NamespaceMapping(NamespaceResolver base) { + this.base = base; + } + + + //--------------------------------------------------< NamespaceResolver >--- + /** + * {@inheritDoc} + */ + public String getPrefix(String uri) throws NamespaceException { + if (URIToPrefix.containsKey(uri)) { + return URIToPrefix.get(uri); + } else if (base == null) { + throw new NamespaceException("No prefix for URI '" + uri + "' declared."); + } else { + return base.getPrefix(uri); + } + } + + /** + * {@inheritDoc} + */ + public String getURI(String prefix) throws NamespaceException { + if (prefixToURI.containsKey(prefix)) { + return prefixToURI.get(prefix); + } else if (base == null) { + throw new NamespaceException("No URI for prefix '" + prefix + "' declared."); + } else { + return base.getURI(prefix); + } + } + + //-------------------------------------------------------------< public >--- + /** + * Returns true if prefix is already mapped to some URI. Returns false otherwise. + * @param prefix prefix to check + * @return true if prefix is mapped + */ + public boolean hasPrefix(String prefix) { + return prefixToURI.containsKey(prefix); + } + + /** + * Set a prefix == URI one-to-one mapping + * + * @param prefix prefix to map + * @param uri uri to map + * @throws NamespaceException if an error occurs + */ + public void setMapping(String prefix, String uri) throws NamespaceException { + if (prefix == null) { + throw new NamespaceException("Prefix must not be null"); + } + if (uri == null) { + throw new NamespaceException("URI must not be null"); + } + if (URIToPrefix.containsKey(uri)) { + // remove mapping + prefixToURI.remove(URIToPrefix.remove(uri)); + } + if (prefixToURI.containsKey(prefix)) { + // remove mapping + URIToPrefix.remove(prefixToURI.remove(prefix)); + } + prefixToURI.put(prefix, uri); + URIToPrefix.put(uri, prefix); + } + + /** + * Clear the mapping for an URI + * + * @param uri URI to clear the mapping for + * @return The prefix the URI was mapped to or null if it was not mapped. + */ + public String removeMapping(String uri) { + String prefix = URIToPrefix.remove(uri); + if (prefix != null) { + prefixToURI.remove(prefix); + } + + return prefix; + } + + /** + * Return a Map of prefix to URI mappings currently registered. + * The returned Map is a copy of the internal Map. + * @return Map + */ + public Map getPrefixToURIMapping() { + return new HashMap(prefixToURI); + } + + /** + * Return a Map of URI to prefix mappings currently registered. + * The returned Map is a copy of the internal Map. + * @return Map + */ + public Map getURIToPrefixMapping() { + return new HashMap(URIToPrefix); + } + + //-------------------------------------------------------------< Object >--- + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof NamespaceMapping) { + NamespaceMapping other = (NamespaceMapping) obj; + return this.getPrefixToURIMapping().equals(other.getPrefixToURIMapping()) + && this.getURIToPrefixMapping().equals(other.getURIToPrefixMapping()); + } + return false; + } + + /** + * Override {@link Object#toString()} + * + * @return String + */ + @Override + public String toString() { + String s = ""; + for (Map.Entry entry: prefixToURI.entrySet()) { + String prefix = entry.getKey(); + String uri = entry.getValue(); + s += "'" + prefix + "' == '" + uri + "'\n"; + } + return s; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceResolver.java new file mode 100644 index 00000000000..9c88d1e0b52 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/NamespaceResolver.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.spi.commons.namespace; + +import javax.jcr.NamespaceException; + +/** + * Interface for resolving namespace URIs and prefixes. Unlike the JCR + * {@link javax.jcr.NamespaceRegistry} interface, this interface contains + * no functionality other than the basic namespace URI and prefix resolution + * methods. This interface is therefore used internally in many places where + * the full namespace registry is either not available or some other mechanism + * is used for resolving namespaces. + */ +public interface NamespaceResolver { + + /** + * Returns the URI to which the given prefix is mapped. + * + * @param prefix namespace prefix + * @return the namespace URI to which the given prefix is mapped. + * @throws NamespaceException if the prefix is unknown. + */ + String getURI(String prefix) throws NamespaceException; + + /** + * Returns the prefix which is mapped to the given URI. + * + * @param uri namespace URI + * @return the prefix mapped to the given URI. + * @throws NamespaceException if the URI is unknown. + */ + String getPrefix(String uri) throws NamespaceException; +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/RegistryNamespaceResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/RegistryNamespaceResolver.java new file mode 100644 index 00000000000..e40df9a00b2 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/RegistryNamespaceResolver.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.spi.commons.namespace; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.RepositoryException; + +/** + * Namespace resolver based on the repository-wide namespace mappings + * stored in a namespace registry. + */ +public class RegistryNamespaceResolver implements NamespaceResolver { + + /** + * Namespace registry + */ + private final NamespaceRegistry registry; + + /** + * Creates a new namespace resolver based on the given namespace registry. + * + * @param registry namespace registry + */ + public RegistryNamespaceResolver(NamespaceRegistry registry) { + this.registry = registry; + } + + public String getPrefix(String uri) throws NamespaceException { + try { + return registry.getPrefix(uri); + } catch (RepositoryException e) { + if (!(e instanceof NamespaceException)) { + e = new NamespaceException( + "Failed to resolve namespace URI: " + uri, e); + } + throw (NamespaceException) e; + } + } + + public String getURI(String prefix) throws NamespaceException { + try { + return registry.getURI(prefix); + } catch (RepositoryException e) { + if (!(e instanceof NamespaceException)) { + e = new NamespaceException( + "Failed to resolve namespace prefix: " + prefix, e); + } + throw (NamespaceException) e; + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/SessionNamespaceResolver.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/SessionNamespaceResolver.java new file mode 100644 index 00000000000..4d13fab49e4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/SessionNamespaceResolver.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.spi.commons.namespace; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +/** + * helper class that exposes the NamespaceResolver + * interface on a Session. + */ +public class SessionNamespaceResolver implements NamespaceResolver { + + /** + * the session for the namespace lookups + */ + private final Session session; + + /** + * Creates a new namespace resolver based on a session + * @param session + */ + public SessionNamespaceResolver(Session session) { + this.session = session; + } + + /** + * {@inheritDoc} + */ + public String getPrefix(String uri) throws NamespaceException { + try { + return session.getNamespacePrefix(uri); + } catch (RepositoryException e) { + // should never get here... + throw new NamespaceException("internal error: failed to resolve namespace uri", e); + } + } + + /** + * {@inheritDoc} + */ + public String getURI(String prefix) throws NamespaceException { + try { + return session.getNamespaceURI(prefix); + } catch (RepositoryException e) { + // should never get here... + throw new NamespaceException("internal error: failed to resolve namespace prefix", e); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/package-info.java new file mode 100644 index 00000000000..a626c8f119e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/namespace/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.namespace; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractItemDefinitionTemplate.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractItemDefinitionTemplate.java new file mode 100644 index 00000000000..8c65525ff28 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractItemDefinitionTemplate.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.spi.commons.nodetype; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.RepositoryException; +import javax.jcr.NamespaceException; +import javax.jcr.version.OnParentVersionAction; + +/** + * AbstractItemDefinitionTemplate serves as base class for + * NodeDefinitionTemplateImpl and + * PropertyDefinitionTemplateImpl. + */ +abstract class AbstractItemDefinitionTemplate implements ItemDefinition { + + private static final Logger log = LoggerFactory.getLogger(AbstractItemDefinitionTemplate.class); + + private Name name; + private boolean autoCreated; + private boolean mandatory; + private int opv = OnParentVersionAction.COPY; + private boolean protectedStatus; + + protected final NamePathResolver resolver; + + /** + * Package private constructor + * + * @param resolver + */ + AbstractItemDefinitionTemplate(NamePathResolver resolver) { + this.resolver = resolver; + } + + /** + * Package private constructor + * + * @param def + * @param resolver + * @throws javax.jcr.nodetype.ConstraintViolationException + */ + AbstractItemDefinitionTemplate(ItemDefinition def, NamePathResolver resolver) throws ConstraintViolationException { + this.resolver = resolver; + + if (def instanceof ItemDefinitionImpl) { + name = ((ItemDefinitionImpl) def).itemDef.getName(); + } else { + setName(def.getName()); + } + autoCreated = def.isAutoCreated(); + mandatory = def.isMandatory(); + opv = def.getOnParentVersion(); + protectedStatus = def.isProtected(); + } + + //-----------------------------------------------< ItemDefinition setters > + /** + * Sets the name of the child item. + * + * @param name a String. + * @throws ConstraintViolationException + */ + public void setName(String name) throws ConstraintViolationException { + if (ItemDefinitionImpl.ANY_NAME.equals(name)) { + // handle the * special case that isn't a valid JCR name but a valid + // name for a ItemDefinition (residual). + this.name = NameConstants.ANY_NAME; + } else { + try { + this.name = resolver.getQName(name); + } catch (RepositoryException e) { + throw new ConstraintViolationException(e); + } + } + } + + /** + * Sets the auto-create status of the child item. + * + * @param autoCreated a boolean. + */ + public void setAutoCreated(boolean autoCreated) { + this.autoCreated = autoCreated; + } + + /** + * Sets the mandatory status of the child item. + * + * @param mandatory a boolean. + */ + public void setMandatory(boolean mandatory) { + this.mandatory = mandatory; + } + + /** + * Sets the on-parent-version status of the child item. + * + * @param opv an int constant member of OnParentVersionAction. + * @throws IllegalArgumentException If the given opv flag isn't valid. + */ + public void setOnParentVersion(int opv) { + // validate the given opv-action + OnParentVersionAction.nameFromValue(opv); + this.opv = opv; + } + + /** + * Sets the protected status of the child item. + * + * @param protectedStatus a boolean. + */ + public void setProtected(boolean protectedStatus) { + this.protectedStatus = protectedStatus; + } + + //-------------------------------------------------------< ItemDefinition > + /** + * {@inheritDoc} + */ + public String getName() { + if (name == null) { + return null; + } else { + try { + return resolver.getJCRName(name); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in item definition name", e); + return name.toString(); + } + } + } + + /** + * {@inheritDoc} + */ + public NodeType getDeclaringNodeType() { + return null; + } + + /** + * {@inheritDoc} + */ + public boolean isAutoCreated() { + return autoCreated; + } + + /** + * {@inheritDoc} + */ + public boolean isMandatory() { + return mandatory; + } + + /** + * {@inheritDoc} + */ + public int getOnParentVersion() { + return opv; + } + + /** + * {@inheritDoc} + */ + public boolean isProtected() { + return protectedStatus; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractNodeType.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractNodeType.java new file mode 100644 index 00000000000..621ee689670 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractNodeType.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.spi.commons.nodetype; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.commons.iterator.NodeTypeIteratorAdapter; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameException; + +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.RepositoryException; +import javax.jcr.NamespaceException; + +import java.util.ArrayList; + +/** + * AbstractNodeType... + */ +/** + * AbstractNodeType... + */ +public abstract class AbstractNodeType implements NodeType { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(AbstractNodeType.class); + + protected final AbstractNodeTypeManager ntMgr; + + protected final QNodeTypeDefinition ntd; + + protected final NamePathResolver resolver; + + /** + * Create a new AbstractNodeType. + * + * @param ntd the underlying node type definition. + * @param ntMgr the node type manager. + * @param resolver the name/path resolver of the session that created this + * node type instance. + */ + public AbstractNodeType(QNodeTypeDefinition ntd, + AbstractNodeTypeManager ntMgr, + NamePathResolver resolver) { + this.ntd = ntd; + this.ntMgr = ntMgr; + this.resolver = resolver; + } + + /** + * Returns the node type definition. + * + * @return the internal node type definition. + */ + public QNodeTypeDefinition getDefinition() { + return ntd; + } + + //-----------------------------------------------------------< NodeType >--- + + /** + * {@inheritDoc} + */ + public String getName() { + try { + return resolver.getJCRName(ntd.getName()); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in node type name", e); + return ntd.getName().toString(); + } + } + + /** + * {@inheritDoc} + */ + public boolean isAbstract() { + return ntd.isAbstract(); + } + + /** + * {@inheritDoc} + */ + public boolean isMixin() { + return ntd.isMixin(); + } + + /** + * {@inheritDoc} + */ + public boolean isQueryable() { + return ntd.isQueryable(); + } + + /** + * {@inheritDoc} + */ + public String[] getDeclaredSupertypeNames() { + Name[] ntNames = ntd.getSupertypes(); + String[] supertypes = new String[ntNames.length]; + for (int i = 0; i < ntNames.length; i++) { + try { + supertypes[i] = resolver.getJCRName(ntNames[i]); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in node type name", e); + supertypes[i] = ntNames[i].toString(); + } + } + return supertypes; + } + + /** + * {@inheritDoc} + */ + public NodeType[] getDeclaredSupertypes() { + Name[] ntNames = ntd.getSupertypes(); + NodeType[] supertypes = new NodeType[ntNames.length]; + for (int i = 0; i < ntNames.length; i++) { + try { + supertypes[i] = ntMgr.getNodeType(ntNames[i]); + } catch (NoSuchNodeTypeException e) { + // should never get here + log.error("undefined supertype", e); + return new NodeType[0]; + } + } + return supertypes; + } + + /** + * @see javax.jcr.nodetype.NodeType#getDeclaredSubtypes() + */ + public NodeTypeIterator getDeclaredSubtypes() { + return getSubtypes(true); + } + + /** + * @see javax.jcr.nodetype.NodeType#getSubtypes() + */ + public NodeTypeIterator getSubtypes() { + return getSubtypes(false); + } + + /** + * {@inheritDoc} + */ + public NodeDefinition[] getDeclaredChildNodeDefinitions() { + QNodeDefinition[] cnda = ntd.getChildNodeDefs(); + NodeDefinition[] nodeDefs = new NodeDefinition[cnda.length]; + for (int i = 0; i < cnda.length; i++) { + nodeDefs[i] = ntMgr.getNodeDefinition(cnda[i]); + } + return nodeDefs; + } + + /** + * {@inheritDoc} + */ + public String getPrimaryItemName() { + // TODO JCR-1947: JSR 283: Node Type Attribute Subtyping Rules + try { + Name piName = ntd.getPrimaryItemName(); + if (piName != null) { + return resolver.getJCRName(piName); + } else { + return null; + } + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in name of primary item", e); + return ntd.getName().toString(); + } + } + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#getDeclaredPropertyDefinitions() + */ + public PropertyDefinition[] getDeclaredPropertyDefinitions() { + QPropertyDefinition[] pda = ntd.getPropertyDefs(); + PropertyDefinition[] propDefs = new PropertyDefinition[pda.length]; + for (int i = 0; i < pda.length; i++) { + propDefs[i] = ntMgr.getPropertyDefinition(pda[i]); + } + return propDefs; + } + + /** + * {@inheritDoc} + */ + public boolean isNodeType(String nodeTypeName) { + Name ntName; + try { + ntName = resolver.getQName(nodeTypeName); + } catch (NamespaceException e) { + log.warn("invalid node type name: " + nodeTypeName, e); + return false; + } catch (NameException e) { + log.warn("invalid node type name: " + nodeTypeName, e); + return false; + } + return isNodeType(ntName); + } + + /** + * Test if this nodetype equals or is directly or indirectly derived from + * the node type with the specified nodeTypeName, without + * checking of a node type of that name really exists. + * + * @param nodeTypeName A node type name. + * @return true if this node type represents the type with the given + * nodeTypeName or if it is directly or indirectly derived + * from it; otherwise false. If no node type exists with the + * specified name this method will also return false. + */ + public abstract boolean isNodeType(Name nodeTypeName); + + //-------------------------------------------------------------------------- + + /** + * Returns the node types derived from this node type. + * + * @param directOnly if true only direct subtypes will be considered + * + * @return an NodeTypeIterator. + * @see NodeType#getSubtypes + * @see NodeType#getDeclaredSubtypes + */ + public NodeTypeIterator getSubtypes(boolean directOnly) { + NodeTypeIterator iter; + try { + iter = ntMgr.getAllNodeTypes(); + } catch (RepositoryException e) { + // should never get here + log.error("failed to retrieve registered node types", e); + return NodeTypeIteratorAdapter.EMPTY; + } + + ArrayList result = new ArrayList(); + String thisName = getName(); + while (iter.hasNext()) { + NodeType nt = iter.nextNodeType(); + if (!nt.getName().equals(thisName)) { + if (directOnly) { + // direct subtypes only + for (String name : nt.getDeclaredSupertypeNames()) { + if (name.equals(thisName)) { + result.add(nt); + break; + } + } + } else { + // direct and indirect subtypes + if (nt.isNodeType(thisName)) { + result.add(nt); + } + } + } + } + return new NodeTypeIteratorAdapter(result); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractNodeTypeManager.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractNodeTypeManager.java new file mode 100644 index 00000000000..625505dec86 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/AbstractNodeTypeManager.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.spi.commons.nodetype; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.PropertyDefinitionTemplate; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; + +/** + * AbstractNodeTypeManager covers creation of node type templates + * and definition templates. + */ +public abstract class AbstractNodeTypeManager implements NodeTypeManager { + + /** + * Return the node type with the specified ntName. + * + * @param ntName Name of the node type to be returned. + * @return the node type with the specified ntName. + * @throws NoSuchNodeTypeException If no such node type exists. + */ + public abstract NodeType getNodeType(Name ntName) throws NoSuchNodeTypeException; + + /** + * Retrieve the NodeDefinition for the given + * QNodeDefinition. + * + * @param nd the QNodeDefinition. + * @return the node definition. + */ + public abstract NodeDefinition getNodeDefinition(QNodeDefinition nd); + + /** + * Retrieve the PropertyDefinition for the given + * QPropertyDefinition. + * + * @param pd the QPropertyDefinition. + * @return the property definition. + */ + public abstract PropertyDefinition getPropertyDefinition(QPropertyDefinition pd); + + /** + * Returns the NamePathResolver used to validate JCR names. + * + * @return the NamePathResolver used to convert JCR names/paths to internal + * onces and vice versa. The resolver may also be used to validate names + * passed to the various templates. + */ + public abstract NamePathResolver getNamePathResolver(); + + //----------------------------------------------------< NodeTypeManager >--- + /** + * @see javax.jcr.nodetype.NodeTypeManager#createNodeTypeTemplate() + */ + public NodeTypeTemplate createNodeTypeTemplate() + throws UnsupportedRepositoryOperationException, RepositoryException { + return new NodeTypeTemplateImpl(getNamePathResolver()); + } + + /** + * @see javax.jcr.nodetype.NodeTypeManager#createNodeTypeTemplate(NodeTypeDefinition) + */ + public NodeTypeTemplate createNodeTypeTemplate(NodeTypeDefinition ntd) + throws UnsupportedRepositoryOperationException, RepositoryException { + return new NodeTypeTemplateImpl(ntd, getNamePathResolver()); + } + + /** + * @see javax.jcr.nodetype.NodeTypeManager#createNodeDefinitionTemplate() + */ + public NodeDefinitionTemplate createNodeDefinitionTemplate() + throws UnsupportedRepositoryOperationException, RepositoryException { + return new NodeDefinitionTemplateImpl(getNamePathResolver()); + } + + /** + * @see javax.jcr.nodetype.NodeTypeManager#createPropertyDefinitionTemplate() + */ + public PropertyDefinitionTemplate createPropertyDefinitionTemplate() + throws UnsupportedRepositoryOperationException, RepositoryException { + return new PropertyDefinitionTemplateImpl(getNamePathResolver()); + } + + /** + * @see javax.jcr.nodetype.NodeTypeManager#registerNodeType(NodeTypeDefinition, boolean) + */ + public NodeType registerNodeType(NodeTypeDefinition ntd, boolean allowUpdate) + throws RepositoryException { + NodeTypeDefinition[] ntds = new NodeTypeDefinition[] { ntd }; + return registerNodeTypes(ntds, allowUpdate).nextNodeType(); + } + + /** + * @see javax.jcr.nodetype.NodeTypeManager#unregisterNodeType(String) + */ + public void unregisterNodeType(String name) + throws UnsupportedRepositoryOperationException, NoSuchNodeTypeException, RepositoryException { + unregisterNodeTypes(new String[] {name}); + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/InvalidConstraintException.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/InvalidConstraintException.java new file mode 100644 index 00000000000..f891b771cf9 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/InvalidConstraintException.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.spi.commons.nodetype; + +import javax.jcr.RepositoryException; + +/** + * The InvalidConstraintException ... + */ +public class InvalidConstraintException extends RepositoryException { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public InvalidConstraintException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public InvalidConstraintException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/ItemDefinitionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/ItemDefinitionImpl.java new file mode 100644 index 00000000000..fb057ca0f45 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/ItemDefinitionImpl.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype; + +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.NamespaceException; + +/** + * This class implements the ItemDefinition interface. + * All method calls are delegated to the wrapped {@link org.apache.jackrabbit.spi.QItemDefinition}, + * performing the translation from Names to JCR names + * (and vice versa) where necessary. + */ +abstract class ItemDefinitionImpl implements ItemDefinition { + + /** + * Logger instance for this class + */ + private static Logger log = LoggerFactory.getLogger(ItemDefinitionImpl.class); + + /** + * Literal for 'any name'. + */ + protected static final String ANY_NAME = "*"; + + /** + * The namespace resolver used to translate Names to JCR name strings. + */ + protected final NamePathResolver resolver; + + /** + * The node type manager of this session. + */ + protected final AbstractNodeTypeManager ntMgr; + + /** + * The wrapped item definition. + */ + protected final QItemDefinition itemDef; + + /** + * Package private constructor to create a definition that is based on + * a template. + * + * @param itemDef item definition + * @param resolver + */ + ItemDefinitionImpl(QItemDefinition itemDef, NamePathResolver resolver) { + this(itemDef, null, resolver); + } + + /** + * Package private constructor to create a definition that is based on + * an existing node type. + * + * @param itemDef + * @param ntMgr + * @param resolver + */ + ItemDefinitionImpl(QItemDefinition itemDef, AbstractNodeTypeManager ntMgr, NamePathResolver resolver) { + this.itemDef = itemDef; + this.resolver = resolver; + this.ntMgr = ntMgr; + } + + //-----------------------------------------------------< ItemDefinition >--- + /** + * {@inheritDoc} + */ + public NodeType getDeclaringNodeType() { + if (ntMgr == null) { + // only a template + return null; + } else { + try { + return ntMgr.getNodeType(itemDef.getDeclaringNodeType()); + } catch (NoSuchNodeTypeException e) { + // should never get here + log.error("declaring node type does not exist", e); + return null; + } + } + } + + /** + * {@inheritDoc} + */ + public String getName() { + if (itemDef.definesResidual()) { + return ANY_NAME; + } else { + try { + return resolver.getJCRName(itemDef.getName()); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in property name", e); + // not correct, but an acceptable fallback + return itemDef.getName().toString(); + } + } + } + + /** + * {@inheritDoc} + */ + public int getOnParentVersion() { + return itemDef.getOnParentVersion(); + } + + /** + * {@inheritDoc} + */ + public boolean isAutoCreated() { + return itemDef.isAutoCreated(); + } + + /** + * {@inheritDoc} + */ + public boolean isMandatory() { + return itemDef.isMandatory(); + } + + /** + * {@inheritDoc} + */ + public boolean isProtected() { + return itemDef.isProtected(); + } + + //-------------------------------------------------------------< Object >--- + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ItemDefinitionImpl)) { + return false; + } + return itemDef.equals(((ItemDefinitionImpl) o).itemDef); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return itemDef.hashCode(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeDefinitionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeDefinitionImpl.java new file mode 100644 index 00000000000..add4104504f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeDefinitionImpl.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.NamespaceException; + +/** + * This class implements the NodeDefinition interface. + * All method calls are delegated to the wrapped {@link QNodeDefinition}, + * performing the translation from Names to JCR names + * where necessary. + */ +public class NodeDefinitionImpl extends ItemDefinitionImpl implements NodeDefinition { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(NodeDefinitionImpl.class); + + /** + * Constructor to create a definition that is based on a template. + * + * @param itemDef item definition + * @param resolver + */ + public NodeDefinitionImpl(QItemDefinition itemDef, NamePathResolver resolver) { + super(itemDef, resolver); + } + + /** + * Constructor to create a definition that is based on an + * existing node type. + * + * @param itemDef item definition + * @param resolver + */ + public NodeDefinitionImpl(QItemDefinition itemDef, AbstractNodeTypeManager ntMgr, NamePathResolver resolver) { + super(itemDef, ntMgr, resolver); + } + + /** + * Returns the wrapped node definition. + * + * @return the wrapped node definition. + */ + public QNodeDefinition unwrap() { + return (QNodeDefinition) itemDef; + } + + //-------------------------------------------------------< NodeDefinition > + + /** + * {@inheritDoc} + */ + public boolean allowsSameNameSiblings() { + return ((QNodeDefinition) itemDef).allowsSameNameSiblings(); + } + + /** + * @see NodeDefinition#getDefaultPrimaryTypeName() + * @since JCR 2.0 + */ + public String getDefaultPrimaryTypeName() { + Name ntName = ((QNodeDefinition) itemDef).getDefaultPrimaryType(); + if (ntName == null) { + return null; + } + try { + return resolver.getJCRName(ntName); + } catch (NamespaceException e) { + // should never get here + log.error("invalid default node type " + ntName, e); + return null; + } + } + + /** + * {@inheritDoc} + */ + public NodeType getDefaultPrimaryType() { + if (ntMgr == null) { + // not attached to an existing node type + return null; + } + Name ntName = ((QNodeDefinition) itemDef).getDefaultPrimaryType(); + if (ntName == null) { + return null; + } + try { + return ntMgr.getNodeType(ntName); + } catch (NoSuchNodeTypeException e) { + // should never get here + log.error("invalid default node type " + ntName, e); + return null; + } + } + + /** + * {@inheritDoc} + */ + public NodeType[] getRequiredPrimaryTypes() { + if (ntMgr == null) { + // not attached to an existing node type + return null; + } + Name[] ntNames = ((QNodeDefinition) itemDef).getRequiredPrimaryTypes(); + try { + if (ntNames == null || ntNames.length == 0) { + // return "nt:base" + return new NodeType[] { ntMgr.getNodeType(NameConstants.NT_BASE) }; + } else { + NodeType[] nodeTypes = new NodeType[ntNames.length]; + for (int i = 0; i < ntNames.length; i++) { + nodeTypes[i] = ntMgr.getNodeType(ntNames[i]); + } + return nodeTypes; + } + } catch (NoSuchNodeTypeException e) { + // should never get here + log.error("required node type does not exist", e); + return new NodeType[0]; + } + } + + /** + * @see NodeDefinition#getRequiredPrimaryTypeNames() + * @since JCR 2.0 + */ + public String[] getRequiredPrimaryTypeNames() { + Name[] ntNames = ((QNodeDefinition) itemDef).getRequiredPrimaryTypes(); + try { + if (ntNames == null || ntNames.length == 0) { + // return "nt:base" + return new String[] { resolver.getJCRName(NameConstants.NT_BASE) }; + } else { + String[] jcrNames = new String[ntNames.length]; + for (int i = 0; i < ntNames.length; i++) { + jcrNames[i] = resolver.getJCRName(ntNames[i]); + } + return jcrNames; + } + } catch (NamespaceException e) { + // should never get here + log.error("required node type does not exist", e); + return new String[0]; + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeDefinitionTemplateImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeDefinitionTemplateImpl.java new file mode 100644 index 00000000000..b8f3dc4818f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeDefinitionTemplateImpl.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.spi.commons.nodetype; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.nodetype.NodeDefinitionTemplate; + +import javax.jcr.RepositoryException; +import javax.jcr.NamespaceException; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.ConstraintViolationException; + +/** + * A NodeDefinitionTemplateImpl ... + */ +class NodeDefinitionTemplateImpl + extends AbstractItemDefinitionTemplate + implements NodeDefinitionTemplate { + + private static final Logger log = LoggerFactory.getLogger(NodeDefinitionTemplateImpl.class); + + private NodeType[] requiredPrimaryTypes; + private Name[] requiredPrimaryTypeNames; + private Name defaultPrimaryTypeName; + private boolean allowSameNameSiblings; + + /** + * Package private constructor + * + * @param resolver + * @throws RepositoryException + */ + NodeDefinitionTemplateImpl(NamePathResolver resolver) throws RepositoryException { + super(resolver); + requiredPrimaryTypes = null; + requiredPrimaryTypeNames = null; + } + + /** + * Package private constructor + * + * @param def + * @param resolver + * @throws javax.jcr.nodetype.ConstraintViolationException + */ + NodeDefinitionTemplateImpl(NodeDefinition def, NamePathResolver resolver) throws ConstraintViolationException { + super(def, resolver); + requiredPrimaryTypes = def.getRequiredPrimaryTypes(); + allowSameNameSiblings = def.allowsSameNameSiblings(); + + if (def instanceof NodeDefinitionImpl) { + QNodeDefinition qDef = (QNodeDefinition) ((NodeDefinitionImpl) def).itemDef; + requiredPrimaryTypeNames = qDef.getRequiredPrimaryTypes(); + defaultPrimaryTypeName = qDef.getDefaultPrimaryType(); + } else { + setRequiredPrimaryTypeNames(def.getRequiredPrimaryTypeNames()); + setDefaultPrimaryTypeName(def.getDefaultPrimaryTypeName()); + } + } + + //-----------------------------------------------< NodeDefinitionTemplate > + /** + * {@inheritDoc} + */ + public void setRequiredPrimaryTypeNames(String[] requiredPrimaryTypeNames) throws ConstraintViolationException { + if (requiredPrimaryTypeNames == null) { + throw new ConstraintViolationException("null isn't a valid array of JCR names."); + } else { + this.requiredPrimaryTypeNames = new Name[requiredPrimaryTypeNames.length]; + for (int i = 0; i < requiredPrimaryTypeNames.length; i++) { + try { + this.requiredPrimaryTypeNames[i] = resolver.getQName(requiredPrimaryTypeNames[i]); + } catch (RepositoryException e) { + throw new ConstraintViolationException(e); + } + } + } + } + + /** + * {@inheritDoc} + */ + public void setDefaultPrimaryTypeName(String defaultPrimaryType) throws ConstraintViolationException { + try { + this.defaultPrimaryTypeName = defaultPrimaryType == null + ? null + : resolver.getQName(defaultPrimaryType); + } catch (RepositoryException e) { + throw new ConstraintViolationException(e); + } + } + + /** + * {@inheritDoc} + */ + public void setSameNameSiblings(boolean allowSameNameSiblings) { + this.allowSameNameSiblings = allowSameNameSiblings; + } + + //-------------------------------------------------------< NodeDefinition > + /** + * {@inheritDoc} + */ + public NodeType[] getRequiredPrimaryTypes() { + return requiredPrimaryTypes; + } + + /** + * {@inheritDoc} + */ + public String[] getRequiredPrimaryTypeNames() { + if (requiredPrimaryTypeNames == null) { + return null; + } else { + String[] rptNames = new String[requiredPrimaryTypeNames.length]; + for (int i = 0; i < requiredPrimaryTypeNames.length; i++) { + try { + rptNames[i] = resolver.getJCRName(requiredPrimaryTypeNames[i]); + } catch (NamespaceException e) { + // should never get here + log.error("invalid node type name: " + requiredPrimaryTypeNames[i], e); + rptNames[i] = requiredPrimaryTypeNames[i].toString(); + } + } + return rptNames; + } + } + + /** + * {@inheritDoc} + */ + public NodeType getDefaultPrimaryType() { + return null; + } + + /** + * {@inheritDoc} + */ + public String getDefaultPrimaryTypeName() { + if (defaultPrimaryTypeName == null) { + return null; + } else { + try { + return resolver.getJCRName(defaultPrimaryTypeName); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in default primary type name", e); + return defaultPrimaryTypeName.toString(); + } + } + } + + /** + * {@inheritDoc} + */ + + public boolean allowsSameNameSiblings() { + return allowSameNameSiblings; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeConflictException.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeConflictException.java new file mode 100644 index 00000000000..af8f527748a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeConflictException.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.spi.commons.nodetype; + +import javax.jcr.RepositoryException; + +/** + * The NodeTypeConflictException ... + */ +public class NodeTypeConflictException extends RepositoryException { + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public NodeTypeConflictException(String message) { + super(message); + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public NodeTypeConflictException(String message, Throwable rootCause) { + super(message, rootCause); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefDiff.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefDiff.java new file mode 100644 index 00000000000..588639b61cc --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefDiff.java @@ -0,0 +1,756 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +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.PropertyType; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * A NodeTypeDefDiff represents the result of the comparison of + * two node type definitions. + *

        + * The result of the comparison can be categorized as one of the following types: + *

        + * NONE indicates that there is no modification at all. + *

        + * A TRIVIAL modification has no impact on the consistency + * of existing content. The following modifications are considered + * TRIVIAL: + *

          + *
        • changing node type orderableChildNodes flag + *
        • changing node type primaryItemName value + *
        • adding non-mandatory property/child node + *
        • changing property/child node protected flag + *
        • changing property/child node onParentVersion value + *
        • changing property/child node mandatory flag to false + *
        • changing property/child node autoCreated flag + *
        • changing specific property/child node name to * + *
        • changing child node defaultPrimaryType + *
        • changing child node sameNameSiblings flag to true + *
        • weaken child node requiredPrimaryTypes (e.g. by removing) + *
        • weaken property valueConstraints (e.g. by removing a constraint + * or by making a specific constraint less restrictive) + *
        • changing property defaultValues + *
        • changing specific property requiredType to undefined + *
        • changing property multiple flag to true + *
        + *

        + * A MAJOR modification potentially affects the + * consistency of existing content. + * + * All modifications that are not TRIVIAL are considered + * MAJOR. + * + * @see #getType() + */ +public class NodeTypeDefDiff { + + /** + * no modification + */ + public static final int NONE = 0; + /** + * trivial modification: does not affect consistency of existing content + */ + public static final int TRIVIAL = 1; + /** + * major modification: does affect consistency of existing content + */ + public static final int MAJOR = 2; + + private final QNodeTypeDefinition oldDef; + private final QNodeTypeDefinition newDef; + private int type; + + private final List propDefDiffs = new ArrayList(); + private final List childNodeDefDiffs = new ArrayList(); + + /** + * Constructor + * @param oldDef old definition + * @param newDef new definition + */ + private NodeTypeDefDiff(QNodeTypeDefinition oldDef, QNodeTypeDefinition newDef) { + this.oldDef = oldDef; + this.newDef = newDef; + if (oldDef.equals(newDef)) { + // definitions are identical + type = NONE; + } else { + // definitions are not identical, determine type of modification + + // assume TRIVIAL change by default + type = TRIVIAL; + + // check supertypes + int tmpType = supertypesDiff(); + if (tmpType > type) { + type = tmpType; + } + + // check mixin flag (MAJOR modification) + tmpType = mixinFlagDiff(); + if (tmpType > type) { + type = tmpType; + } + + // check abstract flag (MAJOR modification) + tmpType = abstractFlagDiff(); + if (tmpType > type) { + type = tmpType; + } + + // no need to check orderableChildNodes flag (TRIVIAL modification) + // no need to check queryable flag (TRIVIAL modification) + + // check property definitions + PropDefDiffBuilder propDefDiffBuilder = new PropDefDiffBuilder(oldDef.getPropertyDefs(), newDef.getPropertyDefs()); + propDefDiffs.addAll(propDefDiffBuilder.getChildItemDefDiffs()); + tmpType = propDefDiffBuilder.getMaxType(); + if (tmpType > type) { + type = tmpType; + } + + // check child node definitions + ChildNodeDefDiffBuilder childNodeDefDiffBuilder = new ChildNodeDefDiffBuilder(oldDef.getChildNodeDefs(), newDef.getChildNodeDefs()); + childNodeDefDiffs.addAll(childNodeDefDiffBuilder.getChildItemDefDiffs()); + tmpType = childNodeDefDiffBuilder.getMaxType(); + if (tmpType > type) { + type = tmpType; + } + } + } + + /** + * @param oldDef old definition + * @param newDef new definition + * @return the diff + */ + public static NodeTypeDefDiff create(QNodeTypeDefinition oldDef, QNodeTypeDefinition newDef) { + if (oldDef == null || newDef == null) { + throw new IllegalArgumentException("arguments can not be null"); + } + if (!oldDef.getName().equals(newDef.getName())) { + throw new IllegalArgumentException("at least node type names must be matching"); + } + return new NodeTypeDefDiff(oldDef, newDef); + } + + /** + * @return true if modified + */ + public boolean isModified() { + return type != NONE; + } + + /** + * @return true if trivial + */ + public boolean isTrivial() { + return type == TRIVIAL; + } + + /** + * @return true if major + */ + public boolean isMajor() { + return type == MAJOR; + } + + /** + * Returns the type of modification as expressed by the following constants: + *

          + *
        • NONE: no modification at all + *
        • TRIVIAL: does not affect consistency of + * existing content + *
        • MAJOR: does affect consistency of existing + * content + *
        + * + * @return the type of modification + */ + public int getType() { + return type; + } + + /** + * @return true if mixin flag diff + */ + public int mixinFlagDiff() { + return oldDef.isMixin() != newDef.isMixin() ? MAJOR : NONE; + } + + /** + * @return true if abstract flag diff + */ + public int abstractFlagDiff() { + return oldDef.isAbstract() && !newDef.isAbstract() ? MAJOR : NONE; + } + + /** + * @return true if supertypes diff + */ + public int supertypesDiff() { + Set set1 = new HashSet(Arrays.asList(oldDef.getSupertypes())); + Set set2 = new HashSet(Arrays.asList(newDef.getSupertypes())); + return !set1.equals(set2) ? MAJOR : NONE; + } + + @Override + public String toString() { + String result = getClass().getName() + "[\n\tnodeTypeName=" + + oldDef.getName(); + + result += ",\n\tmixinFlagDiff=" + modificationTypeToString(mixinFlagDiff()); + result += ",\n\tsupertypesDiff=" + modificationTypeToString(supertypesDiff()); + + result += ",\n\tpropertyDifferences=[\n"; + result += toString(propDefDiffs); + result += "\t]"; + + result += ",\n\tchildNodeDifferences=[\n"; + result += toString(childNodeDefDiffs); + result += "\t]\n"; + result += "]\n"; + + return result; + } + + private String toString(List childItemDefDiffs) { + String result = ""; + for (Iterator iter = childItemDefDiffs.iterator(); iter.hasNext();) { + ChildItemDefDiff propDefDiff = (ChildItemDefDiff) iter.next(); + result += "\t\t" + propDefDiff; + if (iter.hasNext()) { + result += ","; + } + result += "\n"; + } + return result; + } + + private String modificationTypeToString(int modificationType) { + String typeString = "unknown"; + switch (modificationType) { + case NONE: + typeString = "NONE"; + break; + case TRIVIAL: + typeString = "TRIVIAL"; + break; + case MAJOR: + typeString = "MAJOR"; + break; + } + return typeString; + } + + + //--------------------------------------------------------< inner classes > + + private abstract class ChildItemDefDiffBuilder> { + + private final List childItemDefDiffs = new ArrayList(); + + private ChildItemDefDiffBuilder(T[] oldDefs, T[] newDefs) { + buildChildItemDefDiffs(collectChildNodeDefs(oldDefs), collectChildNodeDefs(newDefs)); + } + + private void buildChildItemDefDiffs(Map> oldDefs, Map> newDefs) { + for (Object defId : oldDefs.keySet()) { + this.childItemDefDiffs.addAll(getChildItemDefDiffs(oldDefs.get(defId), newDefs.get(defId))); + newDefs.remove(defId); + } + for (Object defId : newDefs.keySet()) { + this.childItemDefDiffs.addAll(getChildItemDefDiffs(null, newDefs.get(defId))); + } + } + + private Map> collectChildNodeDefs(final T[] defs) { + Map> result = new HashMap>(); + for (T def : defs) { + final Object defId = createQItemDefinitionId(def); + List list = result.get(defId); + if (list == null) { + list = new ArrayList(); + result.put(defId, list); + } + list.add(def); + } + return result; + } + + abstract Object createQItemDefinitionId(T def); + + abstract V createChildItemDefDiff(T def1, T def2); + + Collection getChildItemDefDiffs(List defs1, List defs2) { + defs1 = defs1 != null ? defs1 : Collections.emptyList(); + defs2 = defs2 != null ? defs2 : Collections.emptyList(); + // collect all possible combinations of diffs + final List diffs = new ArrayList(); + for (T def1 : defs1) { + for (T def2 : defs2) { + diffs.add(createChildItemDefDiff(def1, def2)); + } + } + if (defs2.size() < defs1.size()) { + for (T def1 : defs1) { + diffs.add(createChildItemDefDiff(def1, null)); + } + } + if (defs1.size() < defs2.size()) { + for (T def2 : defs2) { + diffs.add(createChildItemDefDiff(null, def2)); + } + } + // sort them according to decreasing compatibility + Collections.sort(diffs, new Comparator() { + @Override + public int compare(final V o1, final V o2) { + return o1.getType() - o2.getType(); + } + }); + // select the most compatible ones + final int size = defs1.size() > defs2.size() ? defs1.size() : defs2.size(); + int allowedNewNull = defs1.size() - defs2.size(); + int allowedOldNull = defs2.size() - defs1.size(); + final List results = new ArrayList(); + for (V diff : diffs) { + if (!alreadyMatched(results, diff.getNewDef(), diff.getOldDef(), allowedNewNull, allowedOldNull)) { + results.add(diff); + if (diff.getNewDef() == null) { + allowedNewNull--; + } + if (diff.getOldDef() == null) { + allowedOldNull--; + } + } + if (results.size() == size) { + break; + } + } + return results; + } + + private boolean alreadyMatched(final List result, final T newDef, final T oldDef, final int allowedNewNull, final int allowedOldNull) { + boolean containsNewDef = false, containsOldDef = false; + for (V d : result) { + if (d.getNewDef() != null && d.getNewDef().equals(newDef)) { + containsNewDef = true; + break; + } + if (d.getOldDef() != null && d.getOldDef().equals(oldDef)) { + containsOldDef = true; + break; + } + } + if (oldDef == null) { + if (allowedOldNull < 1) { + containsOldDef = true; + } + } + if (newDef == null) { + if (allowedNewNull < 1) { + containsNewDef = true; + } + } + + return containsNewDef || containsOldDef; + } + + List getChildItemDefDiffs() { + return childItemDefDiffs; + } + + int getMaxType() { + int maxType = NONE; + for (V childItemDefDiff : childItemDefDiffs) { + if (childItemDefDiff.getType() > maxType) { + maxType = childItemDefDiff.getType(); + } + } + return maxType; + } + } + + private abstract class ChildItemDefDiff { + protected final T oldDef; + protected final T newDef; + protected int type; + + private ChildItemDefDiff(T oldDef, T newDef) { + this.oldDef = oldDef; + this.newDef = newDef; + init(); + } + + protected void init() { + // determine type of modification + if (isAdded()) { + if (!newDef.isMandatory()) { + // adding a non-mandatory child item is a TRIVIAL change + type = TRIVIAL; + } else { + // adding a mandatory child item is a MAJOR change + type = MAJOR; + } + } else if (isRemoved()) { + // removing a child item is a MAJOR change + type = MAJOR; + } else { + /** + * neither added nor removed => has to be either identical + * or modified + */ + if (oldDef.equals(newDef)) { + // identical + type = NONE; + } else { + // modified + if (oldDef.isMandatory() != newDef.isMandatory() + && newDef.isMandatory()) { + // making a child item mandatory is a MAJOR change + type = MAJOR; + } else { + if (!oldDef.definesResidual() + && newDef.definesResidual()) { + // just making a child item residual is a TRIVIAL change + type = TRIVIAL; + } else { + if (!oldDef.getName().equals(newDef.getName())) { + // changing the name of a child item is a MAJOR change + type = MAJOR; + } else { + // all other changes are TRIVIAL + type = TRIVIAL; + } + } + } + } + } + } + + T getOldDef() { + return oldDef; + } + + T getNewDef() { + return newDef; + } + + int getType() { + return type; + } + + boolean isAdded() { + return oldDef == null && newDef != null; + } + + boolean isRemoved() { + return oldDef != null && newDef == null; + } + + boolean isModified() { + return oldDef != null && newDef != null + && !oldDef.equals(newDef); + } + + @Override + public String toString() { + String typeString = modificationTypeToString(getType()); + + String operationString; + if (isAdded()) { + operationString = "ADDED"; + } else if (isModified()) { + operationString = "MODIFIED"; + } else if (isRemoved()) { + operationString = "REMOVED"; + } else { + operationString = "NONE"; + } + + QItemDefinition itemDefinition = (oldDef != null) ? oldDef : newDef; + + return getClass().getName() + "[itemName=" + + itemDefinition.getName() + ", type=" + typeString + + ", operation=" + operationString + "]"; + } + + } + + private class PropDefDiff extends ChildItemDefDiff { + + private PropDefDiff(QPropertyDefinition oldDef, QPropertyDefinition newDef) { + super(oldDef, newDef); + } + + @Override + protected void init() { + super.init(); + /** + * only need to do comparison if base class implementation + * detected a non-MAJOR (i.e. TRIVIAL) modification; + * no need to check for additions or removals as this is already + * handled in base class implementation. + */ + if (isModified() && type == TRIVIAL) { + // check if valueConstraints were made more restrictive + QValueConstraint[] vca1 = getOldDef().getValueConstraints(); + Set set1 = new HashSet(); + for (QValueConstraint aVca1 : vca1) { + set1.add(aVca1.getString()); + } + QValueConstraint[] vca2 = getNewDef().getValueConstraints(); + Set set2 = new HashSet(); + for (QValueConstraint aVca2 : vca2) { + set2.add(aVca2.getString()); + } + + if (!set1.equals(set2)) { + // valueConstraints have been modified + if (set2.isEmpty()) { + // all existing constraints have been cleared + // => TRIVIAL change + type = TRIVIAL; + } else if (set1.isEmpty()) { + // constraints have been set on a previously unconstrained property + // => MAJOR change + type = MAJOR; + } else if (set2.containsAll(set1)) { + // new set is a superset of old set, + // i.e. constraints have been weakened + // (since constraints are OR'ed) + // => TRIVIAL change + type = TRIVIAL; + } else { + // constraint have been removed/modified (MAJOR change); + // since we're unable to semantically compare + // value constraints (e.g. regular expressions), all + // such modifications are considered a MAJOR change. + type = MAJOR; + } + } + + // no need to check defaultValues (TRIVIAL change) + // no need to check availableQueryOperators (TRIVIAL change) + // no need to check queryOrderable (TRIVIAL change) + + if (type == TRIVIAL) { + int t1 = getOldDef().getRequiredType(); + int t2 = getNewDef().getRequiredType(); + if (t1 != t2) { + if (t2 == PropertyType.UNDEFINED) { + // changed getRequiredType to UNDEFINED (TRIVIAL change) + type = TRIVIAL; + } else { + // changed getRequiredType to specific type (MAJOR change) + type = MAJOR; + } + } + boolean b1 = getOldDef().isMultiple(); + boolean b2 = getNewDef().isMultiple(); + if (b1 != b2) { + if (b2) { + // changed multiple flag to true (TRIVIAL change) + type = TRIVIAL; + } else { + // changed multiple flag to false (MAJOR change) + type = MAJOR; + } + } + } + } + } + } + + private class ChildNodeDefDiff extends ChildItemDefDiff { + + private ChildNodeDefDiff(QNodeDefinition oldDef, QNodeDefinition newDef) { + super(oldDef, newDef); + } + + @Override + protected void init() { + super.init(); + /** + * only need to do comparison if base class implementation + * detected a non-MAJOR (i.e. TRIVIAL) modification; + * no need to check for additions or removals as this is already + * handled in base class implementation. + */ + if (isModified() && type == TRIVIAL) { + + boolean b1 = getOldDef().allowsSameNameSiblings(); + boolean b2 = getNewDef().allowsSameNameSiblings(); + if (b1 != b2 && !b2) { + // changed sameNameSiblings flag to false (MAJOR change) + type = MAJOR; + } + + // no need to check defaultPrimaryType (TRIVIAL change) + + if (type == TRIVIAL) { + Set s1 = new HashSet(Arrays.asList(getOldDef().getRequiredPrimaryTypes())); + Set s2 = new HashSet(Arrays.asList(getNewDef().getRequiredPrimaryTypes())); + // normalize sets by removing nt:base (adding/removing nt:base is irrelevant for the diff) + s1.remove(NameConstants.NT_BASE); + s2.remove(NameConstants.NT_BASE); + if (!s1.equals(s2)) { + // requiredPrimaryTypes have been modified + if (s1.containsAll(s2)) { + // old list is a superset of new list + // => removed requiredPrimaryType (TRIVIAL change) + type = TRIVIAL; + } else { + // added/modified requiredPrimaryType (MAJOR change) + // todo check whether aggregate of old requiredTypes would include aggregate of new requiredTypes => trivial change + type = MAJOR; + } + } + } + } + } + } + + /** + * Identifier used to identify corresponding property definitions + */ + private static class QPropertyDefinitionId { + + private Name declaringNodeType; + private Name name; + + private QPropertyDefinitionId(QPropertyDefinition def) { + declaringNodeType = def.getDeclaringNodeType(); + name = def.getName(); + } + + //---------------------------------------< java.lang.Object overrides > + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof QPropertyDefinitionId) { + QPropertyDefinitionId other = (QPropertyDefinitionId) obj; + return declaringNodeType.equals(other.declaringNodeType) + && name.equals(other.name); + } + return false; + } + + @Override + public int hashCode() { + int h = 17; + h = 37 * h + declaringNodeType.hashCode(); + h = 37 * h + name.hashCode(); + return h; + } + } + + /** + * Identifier used to identify corresponding node definitions + */ + private static class QNodeDefinitionId { + + private Name declaringNodeType; + private Name name; + + private QNodeDefinitionId(QNodeDefinition def) { + declaringNodeType = def.getDeclaringNodeType(); + name = def.getName(); + } + + //---------------------------------------< java.lang.Object overrides > + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof QNodeDefinitionId) { + QNodeDefinitionId other = (QNodeDefinitionId) obj; + return declaringNodeType.equals(other.declaringNodeType) + && name.equals(other.name); + } + return false; + } + + @Override + public int hashCode() { + int h = 17; + h = 37 * h + declaringNodeType.hashCode(); + h = 37 * h + name.hashCode(); + return h; + } + } + + private class ChildNodeDefDiffBuilder extends ChildItemDefDiffBuilder { + + private ChildNodeDefDiffBuilder(final QNodeDefinition[] defs1, final QNodeDefinition[] defs2) { + super(defs1, defs2); + } + + @Override + Object createQItemDefinitionId(final QNodeDefinition def) { + return new QNodeDefinitionId(def); + } + + @Override + ChildNodeDefDiff createChildItemDefDiff(final QNodeDefinition def1, final QNodeDefinition def2) { + return new ChildNodeDefDiff(def1, def2); + } + } + + private class PropDefDiffBuilder extends ChildItemDefDiffBuilder { + + private PropDefDiffBuilder(final QPropertyDefinition[] defs1, final QPropertyDefinition[] defs2) { + super(defs1, defs2); + } + + @Override + Object createQItemDefinitionId(final QPropertyDefinition def) { + return new QPropertyDefinitionId(def); + } + + @Override + PropDefDiff createChildItemDefDiff(final QPropertyDefinition def1, final QPropertyDefinition def2) { + return new PropDefDiff(def1, def2); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefinitionFactory.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefinitionFactory.java new file mode 100644 index 00000000000..e40d60d7365 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefinitionFactory.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.spi.commons.nodetype; + +import java.util.List; +import java.util.Collection; +import java.util.ArrayList; + +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.PropertyDefinitionTemplate; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.NamespaceException; +import javax.jcr.Value; + +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; +import org.apache.jackrabbit.spi.commons.value.QValueValue; + +/** + * NodeTypeDefinitionFactory can be used to convert the internal + * SPI node type definitions to JCR {@link NodeTypeDefinition}s. + */ +public class NodeTypeDefinitionFactory { + + private final NodeTypeManager ntMgr; + + private final NamePathResolver resolver; + + /** + * Creates a new node type definition factory that operates on the given + * session to create the templates. + * + * @param session repository session. + * @throws RepositoryException if an error occurs. + */ + public NodeTypeDefinitionFactory(Session session) + throws RepositoryException { + this.ntMgr = session.getWorkspace().getNodeTypeManager(); + this.resolver = new DefaultNamePathResolver(session); + } + + /** + * Create a list of {@link NodeTypeDefinition JCR node type definitions} + * from a collection of {@link QNodeTypeDefinition}. + * + * @param defs the SPI node type definitions. + * @return the JCR node type definitions. + * @throws RepositoryException if an error occurs. + */ + public List create(Collection defs) + throws RepositoryException { + List list = new ArrayList(defs.size()); + for (QNodeTypeDefinition qNtd: defs) { + list.add(create(qNtd)); + } + return list; + } + + /** + * Create a new JCR node type definition from the given + * QNodeTypeDefinition. + * + * @param qNtd A SPI node type definition. + * @return the corresponding JCR node type definition. + * @throws RepositoryException if an error occurs. + */ + @SuppressWarnings("unchecked") + public NodeTypeDefinition create(QNodeTypeDefinition qNtd) + throws RepositoryException { + NodeTypeTemplate nt = ntMgr.createNodeTypeTemplate(); + nt.setName(getJCRName(qNtd.getName())); + nt.setDeclaredSuperTypeNames(getJCRNames(qNtd.getSupertypes())); + nt.setAbstract(qNtd.isAbstract()); + nt.setMixin(qNtd.isMixin()); + nt.setOrderableChildNodes(qNtd.hasOrderableChildNodes()); + nt.setPrimaryItemName(getJCRName(qNtd.getPrimaryItemName())); + nt.setQueryable(qNtd.isQueryable()); + List nodeDefs = nt.getNodeDefinitionTemplates(); + for (QNodeDefinition qNd: qNtd.getChildNodeDefs()) { + nodeDefs.add(create(qNd)); + } + List propDefs = nt.getPropertyDefinitionTemplates(); + for (QPropertyDefinition qPd: qNtd.getPropertyDefs()) { + propDefs.add(create(qPd)); + } + return nt; + } + + /** + * Create a new JCR node definition from the given QNodeDefinition. + * + * @param qNd A node definition. + * @return The corresponding JCR node definition. + * @throws RepositoryException if an error occurs. + */ + public NodeDefinition create(QNodeDefinition qNd) + throws RepositoryException { + NodeDefinitionTemplate nt = ntMgr.createNodeDefinitionTemplate(); + nt.setName(getJCRName(qNd.getName())); + nt.setAutoCreated(qNd.isAutoCreated()); + nt.setMandatory(qNd.isMandatory()); + nt.setOnParentVersion(qNd.getOnParentVersion()); + nt.setProtected(qNd.isProtected()); + nt.setSameNameSiblings(qNd.allowsSameNameSiblings()); + nt.setDefaultPrimaryTypeName(getJCRName(qNd.getDefaultPrimaryType())); + nt.setRequiredPrimaryTypeNames(getJCRNames(qNd.getRequiredPrimaryTypes())); + return nt; + } + + /** + * Create a new JCR property definition from the given QPropertyDefinition. + * + * @param qPd A SPI property definition. + * @return the corresponding JCR property definition. + * @throws RepositoryException if an error occurs. + */ + public PropertyDefinition create(QPropertyDefinition qPd) throws RepositoryException { + PropertyDefinitionTemplate pt = ntMgr.createPropertyDefinitionTemplate(); + pt.setName(getJCRName(qPd.getName())); + pt.setAutoCreated(qPd.isAutoCreated()); + pt.setMandatory(qPd.isMandatory()); + pt.setOnParentVersion(qPd.getOnParentVersion()); + pt.setProtected(qPd.isProtected()); + pt.setRequiredType(qPd.getRequiredType()); + pt.setMultiple(qPd.isMultiple()); + pt.setFullTextSearchable(qPd.isFullTextSearchable()); + pt.setValueConstraints(createValueConstraints(qPd.getRequiredType(), qPd.getValueConstraints())); + pt.setAvailableQueryOperators(qPd.getAvailableQueryOperators()); + pt.setQueryOrderable(qPd.isQueryOrderable()); + pt.setDefaultValues(createValues(qPd.getDefaultValues())); + return pt; + } + + private String[] getJCRNames(Name[] names) throws NamespaceException { + if (names == null) { + return null; + } + String[] ret = new String[names.length]; + for (int i=0; iAbstractNodeTypeDefinition... + */ +public class NodeTypeDefinitionImpl implements NodeTypeDefinition { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(NodeTypeDefinitionImpl.class); + + protected final QNodeTypeDefinition ntd; + private final NamePathResolver resolver; + private final ValueFactory valueFactory; + + /** + * + * @param ntd + * @param resolver + * @param valueFactory + */ + public NodeTypeDefinitionImpl(QNodeTypeDefinition ntd, NamePathResolver resolver, + ValueFactory valueFactory) { + this.ntd = ntd; + this.resolver = resolver; + this.valueFactory = valueFactory; + } + + //-------------------------------------------------< NodeTypeDefinition >--- + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#getName() + */ + public String getName() { + try { + return resolver.getJCRName(ntd.getName()); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in node type name", e); + return ntd.getName().toString(); + } + } + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#getPrimaryItemName() + */ + public String getPrimaryItemName() { + try { + Name piName = ntd.getPrimaryItemName(); + if (piName != null) { + return resolver.getJCRName(piName); + } else { + return null; + } + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in name of primary item", e); + return ntd.getName().toString(); + } + } + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#isMixin() + */ + public boolean isMixin() { + return ntd.isMixin(); + } + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#hasOrderableChildNodes() + */ + public boolean hasOrderableChildNodes() { + return ntd.hasOrderableChildNodes(); + } + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#isAbstract() + */ + public boolean isAbstract() { + return ntd.isAbstract(); + } + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#isQueryable() + */ + public boolean isQueryable() { + return ntd.isQueryable(); + } + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#getDeclaredPropertyDefinitions() + */ + public PropertyDefinition[] getDeclaredPropertyDefinitions() { + QPropertyDefinition[] pds = ntd.getPropertyDefs(); + PropertyDefinition[] propDefs = new PropertyDefinition[pds.length]; + for (int i = 0; i < pds.length; i++) { + propDefs[i] = new PropertyDefinitionImpl(pds[i], resolver, valueFactory); + } + return propDefs; + } + + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#getDeclaredChildNodeDefinitions() + */ + public NodeDefinition[] getDeclaredChildNodeDefinitions() { + QNodeDefinition[] cnda = ntd.getChildNodeDefs(); + NodeDefinition[] nodeDefs = new NodeDefinition[cnda.length]; + for (int i = 0; i < cnda.length; i++) { + nodeDefs[i] = new NodeDefinitionImpl(cnda[i], resolver); + } + return nodeDefs; + } + + /** + * @see javax.jcr.nodetype.NodeTypeDefinition#getDeclaredSupertypeNames() + */ + public String[] getDeclaredSupertypeNames() { + Name[] stNames = ntd.getSupertypes(); + String[] dstn = new String[stNames.length]; + for (int i = 0; i < stNames.length; i++) { + try { + dstn[i] = resolver.getJCRName(stNames[i]); + } catch (NamespaceException e) { + // should never get here + log.error("invalid node type name: " + stNames[i], e); + dstn[i] = stNames[i].toString(); + } + } + return dstn; + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeStorage.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeStorage.java new file mode 100644 index 00000000000..03e96296117 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeStorage.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.spi.commons.nodetype; + +import java.util.Iterator; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeExistsException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +/** + * NodeTypeStorage provides means for storing {@link QNodeTypeDefinition}s. + */ +public interface NodeTypeStorage { + + /** + * Returns an Iterator over all node type definitions registered. + * + * @return + * @throws RepositoryException + */ + public Iterator getAllDefinitions() throws RepositoryException; + + /** + * Returns the QNodeTypeDefinitions for the given node type + * names. The implementation is free to return additional definitions e.g. + * dependencies. + * + * @param nodeTypeNames + * @return + * @throws NoSuchNodeTypeException + * @throws RepositoryException + */ + public Iterator getDefinitions(Name[] nodeTypeNames) throws NoSuchNodeTypeException, RepositoryException; + + /** + * Add all {@link QNodeTypeDefinition}s provided to the store. If allowUpdate is true + * previously registered node QNodeTypeDefinitions will be overwritten. + * @param nodeTypeDefs QNodeTypeDefinitions to add to the store + * @param allowUpdate Whether to overwrite existing QNodeTypeDefinitions + * @throws RepositoryException + * @throws NodeTypeExistsException If allowUpdate is true and a QNodeTypeDefinitions + * of that name already exists. In this case, none of the provided QNodeTypeDefinitions is registered. + */ + public void registerNodeTypes(QNodeTypeDefinition[] nodeTypeDefs, boolean allowUpdate) + throws RepositoryException, NodeTypeExistsException; + + /** + * Remove all {@link QNodeTypeDefinition}s provided from the store. + * @param nodeTypeNames QNodeTypeDefinitions to remove from the store + * @throws RepositoryException + * @throws NoSuchNodeTypeException If any of the QNodeTypeDefinitions does not exist. In this case + * none of the provided is unregistered. + */ + public void unregisterNodeTypes(Name[] nodeTypeNames) throws NoSuchNodeTypeException, RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeStorageImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeStorageImpl.java new file mode 100644 index 00000000000..de2c92f51d1 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeStorageImpl.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.spi.commons.nodetype; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeExistsException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; + +/** + * This implementation of {@link NodeTypeStorage} keeps a map of the registered {@link QNodeTypeDefinition} + * in memory. + */ +public class NodeTypeStorageImpl implements NodeTypeStorage { + + private final Map definitions = new HashMap(); + + public Iterator getAllDefinitions() throws RepositoryException { + return definitions.values().iterator(); + } + + /** + * This implementation returns an iterator over all registered {@link QNodeTypeDefinition}s if + * nodeTypeNames is null. + * {@inheritDoc} + */ + public Iterator getDefinitions(Name[] nodeTypeNames) throws NoSuchNodeTypeException, + RepositoryException { + + if (nodeTypeNames == null) { + return definitions.values().iterator(); + } + + Collection defs = new ArrayList(nodeTypeNames.length); + + for (Name name : nodeTypeNames) { + if (definitions.containsKey(name)) { + defs.add(definitions.get(name)); + } + else { + throw new NoSuchNodeTypeException("{" + name.getNamespaceURI() + "}" + name.getLocalName()); + } + } + + return defs.iterator(); + } + + public void registerNodeTypes(QNodeTypeDefinition[] nodeTypeDefs, boolean allowUpdate) + throws RepositoryException { + + if (nodeTypeDefs == null) { + throw new IllegalArgumentException("nodeTypeDefs must not be null"); + } + + if (!allowUpdate) { + for (QNodeTypeDefinition ntd : nodeTypeDefs) { + Name name = ntd.getName(); + if (definitions.containsKey(name)) { + throw new NodeTypeExistsException("{" + name.getNamespaceURI() + "}" + name.getLocalName()); + } + } + } + + for (QNodeTypeDefinition ntd : nodeTypeDefs) { + definitions.put(ntd.getName(), ntd); + } + } + + public void unregisterNodeTypes(Name[] nodeTypeNames) throws NoSuchNodeTypeException, RepositoryException { + for (Name name : nodeTypeNames) { + if (!definitions.containsKey(name)) { + throw new NoSuchNodeTypeException("{" + name.getNamespaceURI() + "}" + name.getLocalName()); + } + } + + for (Name name : nodeTypeNames) { + definitions.remove(name); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeTemplateImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeTemplateImpl.java new file mode 100644 index 00000000000..bf1b14fce6f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeTemplateImpl.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.spi.commons.nodetype; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedList; +import java.util.List; + +import javax.jcr.nodetype.NodeDefinition; +import javax.jcr.nodetype.NodeDefinitionTemplate; +import javax.jcr.nodetype.NodeTypeTemplate; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.PropertyDefinitionTemplate; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeTypeDefinition; +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; + +/** + * A NodeTypeTemplateImpl ... + */ +public class NodeTypeTemplateImpl implements NodeTypeTemplate { + + private static final Logger log = LoggerFactory.getLogger(NodeTypeTemplateImpl.class); + + private Name name; + private Name[] superTypeNames; + private Name primaryItemName; + private boolean abstractStatus; + private boolean queryable; + private boolean mixin; + private boolean orderableChildNodes; + private List nodeDefinitionTemplates; + private List propertyDefinitionTemplates; + + private final NamePathResolver resolver; + + /** + * Package private constructor + * + * @param resolver + */ + NodeTypeTemplateImpl(NamePathResolver resolver) { + // TODO: see https://jsr-283.dev.java.net/issues/show_bug.cgi?id=798 + queryable = true; + // TODO see https://jsr-283.dev.java.net/issues/show_bug.cgi?id=797 + superTypeNames = Name.EMPTY_ARRAY; + this.resolver = resolver; + } + + /** + * Package private constructor + * + * @param def + * @param resolver + */ + NodeTypeTemplateImpl(NodeTypeDefinition def, NamePathResolver resolver) throws RepositoryException { + this.resolver = resolver; + + if (def instanceof NodeTypeDefinitionImpl) { + QNodeTypeDefinition qDef = ((NodeTypeDefinitionImpl) def).ntd; + name = qDef.getName(); + superTypeNames = qDef.getSupertypes(); + primaryItemName = qDef.getPrimaryItemName(); + } else { + setName(def.getName()); + setDeclaredSuperTypeNames(def.getDeclaredSupertypeNames()); + setPrimaryItemName(def.getPrimaryItemName()); + } + + abstractStatus = def.isAbstract(); + mixin = def.isMixin(); + queryable = def.isQueryable(); + orderableChildNodes = def.hasOrderableChildNodes(); + + NodeDefinition[] nodeDefs = def.getDeclaredChildNodeDefinitions(); + if (nodeDefs != null) { + List list = getNodeDefinitionTemplates(); + for (NodeDefinition nodeDef : nodeDefs) { + list.add(new NodeDefinitionTemplateImpl(nodeDef, resolver)); + } + } + PropertyDefinition[] propDefs = def.getDeclaredPropertyDefinitions(); + if (propDefs != null) { + List list = getPropertyDefinitionTemplates(); + for (PropertyDefinition propDef : propDefs) { + list.add(new PropertyDefinitionTemplateImpl(propDef, resolver)); + } + } + } + + //-----------------------------------------------------< NodeTypeTemplate > + /** + * {@inheritDoc} + */ + public void setName(String name) throws ConstraintViolationException { + try { + this.name = resolver.getQName(name); + } catch (RepositoryException e) { + throw new ConstraintViolationException(e); + } + } + + /** + * {@inheritDoc} + */ + public void setDeclaredSuperTypeNames(String[] names) throws ConstraintViolationException { + // TODO see https://jsr-283.dev.java.net/issues/show_bug.cgi?id=797 + if (names == null) { + throw new ConstraintViolationException("null isn't a valid array of JCR names."); + } else { + superTypeNames = new Name[names.length]; + for (int i = 0; i < names.length; i++) { + try { + superTypeNames[i] = resolver.getQName(names[i]); + } catch (RepositoryException e) { + throw new ConstraintViolationException(e); + } + } + } + } + + /** + * {@inheritDoc} + */ + public void setAbstract(boolean abstractStatus) { + this.abstractStatus = abstractStatus; + } + + /** + * {@inheritDoc} + */ + public void setMixin(boolean mixin) { + this.mixin = mixin; + } + + /** + * {@inheritDoc} + */ + public void setOrderableChildNodes(boolean orderable) { + orderableChildNodes = orderable; + } + + /** + * {@inheritDoc} + */ + public void setPrimaryItemName(String name) throws ConstraintViolationException { + if (name == null) { + primaryItemName = null; + } else { + try { + primaryItemName = resolver.getQName(name); + } catch (RepositoryException e) { + throw new ConstraintViolationException(e); + } + } + } + + /** + * {@inheritDoc} + */ + public List getPropertyDefinitionTemplates() { + if (propertyDefinitionTemplates == null) { + propertyDefinitionTemplates = new LinkedList(); + } + return propertyDefinitionTemplates; + } + + /** + * {@inheritDoc} + */ + public List getNodeDefinitionTemplates() { + if (nodeDefinitionTemplates == null) { + nodeDefinitionTemplates = new LinkedList(); + } + return nodeDefinitionTemplates; + } + + /** + * {@inheritDoc} + */ + public void setQueryable(boolean queryable) { + this.queryable = queryable; + } + + //---------------------------------------------------< NodeTypeDefinition > + /** + * {@inheritDoc} + */ + public String getName() { + if (name == null) { + return null; + } else { + try { + return resolver.getJCRName(name); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in node type name", e); + return name.toString(); + } + } + } + + /** + * {@inheritDoc} + */ + public String[] getDeclaredSupertypeNames() { + String[] names = new String[superTypeNames.length]; + for (int i = 0; i < superTypeNames.length; i++) { + try { + names[i] = resolver.getJCRName(superTypeNames[i]); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in super type name", e); + names[i] = superTypeNames[i].toString(); + } + } + return names; + } + + /** + * {@inheritDoc} + */ + public boolean isAbstract() { + return abstractStatus; + } + + /** + * {@inheritDoc} + */ + public boolean isMixin() { + return mixin; + } + + public boolean isQueryable() { + return queryable; + } + + /** + * {@inheritDoc} + */ + public boolean hasOrderableChildNodes() { + return orderableChildNodes; + } + + /** + * {@inheritDoc} + */ + public String getPrimaryItemName() { + if (primaryItemName == null) { + return null; + } else { + try { + return resolver.getJCRName(primaryItemName); + } catch (NamespaceException e) { + // should never get here + log.error("encountered unregistered namespace in primary type name", e); + return primaryItemName.toString(); + } + } + } + + /** + * {@inheritDoc} + */ + public PropertyDefinition[] getDeclaredPropertyDefinitions() { + if (propertyDefinitionTemplates == null) { + return null; + } else { + return propertyDefinitionTemplates.toArray( + new PropertyDefinition[propertyDefinitionTemplates.size()]); + } + } + + /** + * {@inheritDoc} + */ + public NodeDefinition[] getDeclaredChildNodeDefinitions() { + if (nodeDefinitionTemplates == null) { + return null; + } else { + return nodeDefinitionTemplates.toArray( + new NodeDefinition[nodeDefinitionTemplates.size()]); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/PropertyDefinitionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/PropertyDefinitionImpl.java new file mode 100644 index 00000000000..3feeda985bc --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/PropertyDefinitionImpl.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class implements the PropertyDefinition interface. + * All method calls are delegated to the wrapped {@link QPropertyDefinition}, + * performing the translation from Names to JCR names + * (and vice versa) where necessary. + */ +public class PropertyDefinitionImpl extends ItemDefinitionImpl implements PropertyDefinition { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(PropertyDefinitionImpl.class); + + private final ValueFactory valueFactory; + + /** + * Package private constructor + * + * @param propDef property definition + * @param resolver the name-path resolver + * @param valueFactory a value factory + */ + public PropertyDefinitionImpl(QPropertyDefinition propDef, NamePathResolver resolver, + ValueFactory valueFactory) { + this(propDef, null, resolver, valueFactory); + } + + /** + * + * @param propDef underlying propdef + * @param ntMgr nodetype manager + * @param resolver name-path resolver + * @param valueFactory value factory (for default values) + */ + public PropertyDefinitionImpl(QPropertyDefinition propDef, + AbstractNodeTypeManager ntMgr, + NamePathResolver resolver, + ValueFactory valueFactory) { + super(propDef, ntMgr, resolver); + this.valueFactory = valueFactory; + } + + /** + * Returns the wrapped property definition. + * + * @return the wrapped property definition. + */ + public QPropertyDefinition unwrap() { + return (QPropertyDefinition) itemDef; + } + + //-------------------------------------------------< PropertyDefinition >--- + + /** + * {@inheritDoc} + */ + public Value[] getDefaultValues() { + QPropertyDefinition pDef = ((QPropertyDefinition) itemDef); + QValue[] defVals = pDef.getDefaultValues(); + if (defVals == null) { + return null; + } + + Value[] values = new Value[defVals.length]; + for (int i = 0; i < defVals.length; i++) { + try { + values[i] = ValueFormat.getJCRValue(defVals[i], resolver, valueFactory); + } catch (RepositoryException e) { + // should never get here + String propName = (getName() == null) ? "[null]" : getName(); + log.error("illegal default value specified for property " + propName + " in node type " + getDeclaringNodeType(), e); + return null; + } + } + return values; + } + + /** + * {@inheritDoc} + */ + public int getRequiredType() { + return ((QPropertyDefinition) itemDef).getRequiredType(); + } + + /** + * {@inheritDoc} + */ + public String[] getValueConstraints() { + QPropertyDefinition pd = (QPropertyDefinition) itemDef; + QValueConstraint[] constraints = pd.getValueConstraints(); + if (constraints == null || constraints.length == 0) { + return new String[0]; + } + String[] vca = new String[constraints.length]; + for (int i = 0; i < constraints.length; i++) { + try { + ValueConstraint vc = ValueConstraint.create(pd.getRequiredType(), constraints[i].getString()); + vca[i] = vc.getDefinition(resolver); + } catch (InvalidConstraintException e) { + log.warn("Internal error during conversion of constraint.", e); + vca[i] = constraints[i].getString(); + } + } + return vca; + } + + /** + * {@inheritDoc} + */ + public boolean isMultiple() { + return ((QPropertyDefinition) itemDef).isMultiple(); + } + + /** + * @see javax.jcr.nodetype.PropertyDefinition#getAvailableQueryOperators() + */ + public String[] getAvailableQueryOperators() { + return ((QPropertyDefinition) itemDef).getAvailableQueryOperators(); + } + + /** + * @see javax.jcr.nodetype.PropertyDefinition#isFullTextSearchable() + */ + public boolean isFullTextSearchable() { + return ((QPropertyDefinition) itemDef).isFullTextSearchable(); + } + + /** + * @see javax.jcr.nodetype.PropertyDefinition#isQueryOrderable() + */ + public boolean isQueryOrderable() { + return ((QPropertyDefinition) itemDef).isQueryOrderable(); + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/PropertyDefinitionTemplateImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/PropertyDefinitionTemplateImpl.java new file mode 100644 index 00000000000..da76cbf4ce6 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/PropertyDefinitionTemplateImpl.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.spi.commons.nodetype; + +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.PropertyType; +import javax.jcr.Value; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.nodetype.PropertyDefinitionTemplate; +import javax.jcr.nodetype.ConstraintViolationException; + +/** + * A PropertyDefinitionTemplateImpl ... + */ +class PropertyDefinitionTemplateImpl + extends AbstractItemDefinitionTemplate + implements PropertyDefinitionTemplate { + + private int type; + private String[] constraints; + private Value[] defaultValues; + private boolean multiple; + private boolean fullTextSearchable; + private boolean queryOrderable; + private String[] queryOperators; + + /** + * Package private constructor + * + * @param resolver + */ + PropertyDefinitionTemplateImpl(NamePathResolver resolver) { + super(resolver); + type = PropertyType.STRING; + fullTextSearchable = true; + queryOrderable = true; + queryOperators = Operator.getAllQueryOperators(); + } + + /** + * Package private constructor + * + * @param def + * @param resolver + * @throws javax.jcr.nodetype.ConstraintViolationException + */ + PropertyDefinitionTemplateImpl(PropertyDefinition def, NamePathResolver resolver) throws ConstraintViolationException { + super(def, resolver); + type = def.getRequiredType(); + defaultValues = def.getDefaultValues(); + multiple = def.isMultiple(); + fullTextSearchable = def.isFullTextSearchable(); + queryOrderable = def.isQueryOrderable(); + queryOperators = def.getAvailableQueryOperators(); + setValueConstraints(def.getValueConstraints()); + } + + //-------------------------------------------< PropertyDefinitionTemplate > + /** + * {@inheritDoc} + * + * @throws IllegalArgumentException If an invalid type is passed. + */ + public void setRequiredType(int type) { + // validate + PropertyType.nameFromValue(type); + this.type = type; + } + + /** + * {@inheritDoc} + */ + public void setValueConstraints(String[] constraints) { + // TODO: see https://jsr-283.dev.java.net/issues/show_bug.cgi?id=794 + this.constraints = constraints; + } + + /** + * {@inheritDoc} + */ + public void setDefaultValues(Value[] defaultValues) { + this.defaultValues = defaultValues; + } + + /** + * {@inheritDoc} + */ + public void setMultiple(boolean multiple) { + this.multiple = multiple; + } + + /** + * {@inheritDoc} + */ + public void setAvailableQueryOperators(String[] operators) { + queryOperators = operators; + } + + /** + * {@inheritDoc} + */ + public void setFullTextSearchable(boolean searchable) { + fullTextSearchable = searchable; + } + + /** + * {@inheritDoc} + */ + public void setQueryOrderable(boolean orderable) { + queryOrderable = orderable; + } + + //---------------------------------------------------< PropertyDefinition > + /** + * {@inheritDoc} + */ + public int getRequiredType() { + return type; + } + + /** + * {@inheritDoc} + */ + public String[] getValueConstraints() { + // TODO: see https://jsr-283.dev.java.net/issues/show_bug.cgi?id=794 + return constraints; + } + + /** + * {@inheritDoc} + */ + public Value[] getDefaultValues() { + return defaultValues; + } + + /** + * {@inheritDoc} + */ + public boolean isMultiple() { + return multiple; + } + + /** + * {@inheritDoc} + */ + public String[] getAvailableQueryOperators() { + return queryOperators; + } + + /** + * {@inheritDoc} + */ + public boolean isFullTextSearchable() { + return fullTextSearchable; + } + + /** + * {@inheritDoc} + */ + public boolean isQueryOrderable() { + return queryOrderable; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QDefinitionBuilderFactory.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QDefinitionBuilderFactory.java new file mode 100644 index 00000000000..4a80a8918de --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QDefinitionBuilderFactory.java @@ -0,0 +1,345 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.commons.cnd.DefinitionBuilderFactory; +import org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefReader; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.util.ISO9075; + +/** + * This implementation of {@link DefinitionBuilderFactory} can be used with + * the {@link CompactNodeTypeDefReader} to produce node type definitions of type + * {@link QNodeTypeDefinition} and a namespace map of type {@link NamespaceMapping}. + * It uses {@link QNodeTypeDefinitionBuilderImpl} for building node type definitions, + * {@link QPropertyDefinitionBuilderImpl} for building property definitions, and + * {@link QNodeDefinitionBuilderImpl} for building node definitions. It further uses + * {@link NameFactoryImpl} for creating Names and {@link QValueFactoryImpl} for + * creating QValues. + */ +public class QDefinitionBuilderFactory extends DefinitionBuilderFactory { + + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + + /** + * Default namespace mappings + */ + public static final NamespaceMapping NS_DEFAULTS; + static { + try { + NS_DEFAULTS = new NamespaceMapping(); + NS_DEFAULTS.setMapping(Name.NS_EMPTY_PREFIX, Name.NS_DEFAULT_URI); + NS_DEFAULTS.setMapping(Name.NS_JCR_PREFIX, Name.NS_JCR_URI); + NS_DEFAULTS.setMapping(Name.NS_MIX_PREFIX, Name.NS_MIX_URI); + NS_DEFAULTS.setMapping(Name.NS_NT_PREFIX, Name.NS_NT_URI); + NS_DEFAULTS.setMapping(Name.NS_REP_PREFIX, Name.NS_REP_URI); + } catch (NamespaceException e) { + throw new InternalError(e.toString()); + } + } + + private NamespaceMapping nsMappings = new NamespaceMapping(NS_DEFAULTS); + private NamePathResolver resolver = new DefaultNamePathResolver(nsMappings); + + @Override + public AbstractNodeTypeDefinitionBuilder newNodeTypeDefinitionBuilder() { + return new QNodeTypeDefinitionBuilderImpl(); + } + + @Override + public void setNamespaceMapping(NamespaceMapping nsMapping) { + this.nsMappings = nsMapping; + this.resolver = new DefaultNamePathResolver(nsMapping); + } + + @Override + public NamespaceMapping getNamespaceMapping() { + return nsMappings; + } + + @Override + public void setNamespace(String prefix, String uri) { + try { + nsMappings.setMapping(prefix, uri); + } + catch (NamespaceException e) { + // ignore + } + } + + private class QNodeTypeDefinitionBuilderImpl extends AbstractNodeTypeDefinitionBuilder { + private Name name; + private final List supertypes = new ArrayList(); + private Name primaryItem; + private final List propertyDefs = new ArrayList(); + private final List childNodeDefs = new ArrayList(); + + @Override + public AbstractNodeDefinitionBuilder newNodeDefinitionBuilder() { + return new QNodeDefinitionBuilderImpl(this); + } + + @Override + public AbstractPropertyDefinitionBuilder newPropertyDefinitionBuilder() { + return new QPropertyDefinitionBuilderImpl(this); + } + + @Override + public QNodeTypeDefinition build() { + if (!isMixin && !NameConstants.NT_BASE.equals(name)) { + supertypes.add(NameConstants.NT_BASE); + } + + return new QNodeTypeDefinitionImpl( + name, + supertypes.toArray(new Name[supertypes.size()]), + null, + super.isMixin, + super.isAbstract, + super.queryable, + super.isOrderable, + primaryItem, + propertyDefs.toArray(new QPropertyDefinition[propertyDefs.size()]), + childNodeDefs.toArray(new QNodeDefinition[childNodeDefs.size()])); + } + + @Override + public void setName(String name) throws RepositoryException { + super.setName(name); + this.name = toName(name); + } + + @Override + public void addSupertype(String name) throws IllegalNameException, NamespaceException { + supertypes.add(toName(name)); + } + + @Override + public void setPrimaryItemName(String name) throws IllegalNameException, NamespaceException { + primaryItem = toName(name); + } + + } + + private class QPropertyDefinitionBuilderImpl extends AbstractPropertyDefinitionBuilder { + + private final QNodeTypeDefinitionBuilderImpl ntd; + private final QPropertyDefinitionBuilder builder = new QPropertyDefinitionBuilder(); + + public QPropertyDefinitionBuilderImpl(QNodeTypeDefinitionBuilderImpl ntd) { + super(); + this.ntd = ntd; + } + + @Override + public void setName(String name) throws RepositoryException { + super.setName(name); + if ("*".equals(name)) { + builder.setName(NameConstants.ANY_NAME); + } + else { + builder.setName(toName(name)); + } + } + + @Override + public void setRequiredType(int type) throws RepositoryException { + super.setRequiredType(type); + builder.setRequiredType(type); + } + + @Override + public void setMultiple(boolean isMultiple) throws RepositoryException { + super.setMultiple(isMultiple); + builder.setMultiple(isMultiple); + } + + @Override + public void setFullTextSearchable(boolean fullTextSearchable) + throws RepositoryException { + super.setFullTextSearchable(fullTextSearchable); + builder.setFullTextSearchable(fullTextSearchable); + } + + @Override + public void setQueryOrderable(boolean queryOrderable) + throws RepositoryException { + super.setQueryOrderable(queryOrderable); + builder.setQueryOrderable(queryOrderable); + } + + @Override + public void setAvailableQueryOperators(String[] queryOperators) + throws RepositoryException { + super.setAvailableQueryOperators(queryOperators); + builder.setAvailableQueryOperators(queryOperators); + } + + @Override + public void setAutoCreated(boolean autocreate) + throws RepositoryException { + super.setAutoCreated(autocreate); + builder.setAutoCreated(autocreate); + } + + @Override + public void setOnParentVersion(int onParent) + throws RepositoryException { + super.setOnParentVersion(onParent); + builder.setOnParentVersion(onParent); + } + + @Override + public void setProtected(boolean isProtected) + throws RepositoryException { + super.setProtected(isProtected); + builder.setProtected(isProtected); + } + + @Override + public void setMandatory(boolean isMandatory) + throws RepositoryException { + super.setMandatory(isMandatory); + builder.setMandatory(isMandatory); + } + + @Override + public void addDefaultValues(String value) throws RepositoryException { + builder.addDefaultValue(ValueFormat.getQValue(value, getRequiredType(), resolver, QValueFactoryImpl.getInstance())); + } + + @Override + public void addValueConstraint(String constraint) throws InvalidConstraintException { + builder.addValueConstraint(ValueConstraint.create(getRequiredType(), constraint, resolver)); + } + + @Override + public void setDeclaringNodeType(String name) throws IllegalNameException, NamespaceException { + builder.setDeclaringNodeType(toName(name)); + } + + @Override + public void build() throws IllegalStateException { + ntd.propertyDefs.add(builder.build()); + } + } + + private class QNodeDefinitionBuilderImpl extends AbstractNodeDefinitionBuilder { + private final QNodeTypeDefinitionBuilderImpl ntd; + + private final QNodeDefinitionBuilder builder = new QNodeDefinitionBuilder(); + + public QNodeDefinitionBuilderImpl(QNodeTypeDefinitionBuilderImpl ntd) { + super(); + this.ntd = ntd; + } + + @Override + public void setName(String name) throws RepositoryException { + super.setName(name); + if ("*".equals(name)) { + builder.setName(NameConstants.ANY_NAME); + } + else { + builder.setName(toName(name)); + } + } + + @Override + public void setAllowsSameNameSiblings(boolean allowSns) + throws RepositoryException { + super.setAllowsSameNameSiblings(allowSns); + builder.setAllowsSameNameSiblings(allowSns); + } + + @Override + public void setAutoCreated(boolean autocreate) + throws RepositoryException { + super.setAutoCreated(autocreate); + builder.setAutoCreated(autocreate); + } + + @Override + public void setOnParentVersion(int onParent) + throws RepositoryException { + super.setOnParentVersion(onParent); + builder.setOnParentVersion(onParent); + } + + @Override + public void setProtected(boolean isProtected) + throws RepositoryException { + super.setProtected(isProtected); + builder.setProtected(isProtected); + } + + @Override + public void setMandatory(boolean isMandatory) + throws RepositoryException { + super.setMandatory(isMandatory); + builder.setMandatory(isMandatory); + } + + @Override + public void addRequiredPrimaryType(String name) throws IllegalNameException, NamespaceException { + builder.addRequiredPrimaryType(toName(name)); + } + + @Override + public void setDefaultPrimaryType(String name) throws IllegalNameException, NamespaceException { + builder.setDefaultPrimaryType(toName(name)); + } + + @Override + public void setDeclaringNodeType(String name) throws IllegalNameException, NamespaceException { + builder.setDeclaringNodeType(toName(name)); + } + + @Override + public void build() { + ntd.childNodeDefs.add(builder.build()); + } + } + + + private Name toName(String name) throws IllegalNameException, NamespaceException { + Name n = resolver.getQName(name); + String decodedLocalName = ISO9075.decode(n.getLocalName()); + return NAME_FACTORY.create(n.getNamespaceURI(), decodedLocalName); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QItemDefinitionBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QItemDefinitionBuilder.java new file mode 100644 index 00000000000..bea5d96ad95 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QItemDefinitionBuilder.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.spi.commons.nodetype; + +import javax.jcr.nodetype.ItemDefinition; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * A builder for {@link QItemDefinition}. + */ +public abstract class QItemDefinitionBuilder { + + private Name name = NameConstants.ANY_NAME; + private Name declaringType = null; + private boolean isAutocreated = false; + private int onParentVersion = OnParentVersionAction.COPY; + private boolean isProtected = false; + private boolean isMandatory = false; + + /** + * @param name the name of the child item definition being build + * @see ItemDefinition#getName() + */ + public void setName(Name name) { + this.name = name; + } + + /** + * @return the name of the child item definition being build. + * @see ItemDefinition#getName() + */ + public Name getName() { + return name; + } + + /** + * @param type the name of the declaring node type. + * @see ItemDefinition#getDeclaringNodeType() + */ + public void setDeclaringNodeType(Name type) { + declaringType = type; + } + + /** + * @return the name of the declaring node type. + * @see ItemDefinition#getDeclaringNodeType() + */ + public Name getDeclaringNodeType() { + return declaringType; + } + + /** + * @param autocreate true if building a 'autocreate' child item + * definition, false otherwise. + * @see ItemDefinition#isAutoCreated() + */ + public void setAutoCreated(boolean autocreate) { + isAutocreated = autocreate; + } + + /** + * @return true if building a 'autocreate' child item + * definition, false otherwise. + * @see ItemDefinition#isAutoCreated() + */ + public boolean getAutoCreated() { + return isAutocreated; + } + + /** + * @param onParent the 'onParentVersion' attribute of the child item definition being built + * @see ItemDefinition#getOnParentVersion() + */ + public void setOnParentVersion(int onParent) { + onParentVersion = onParent; + } + + /** + * @return the 'onParentVersion' attribute of the child item definition being built + * @see ItemDefinition#getOnParentVersion() + */ + public int getOnParentVersion() { + return onParentVersion; + } + + /** + * @param isProtected true if building a 'protected' child + * item definition, false otherwise. + * @see ItemDefinition#isProtected() + */ + public void setProtected(boolean isProtected) { + this.isProtected = isProtected; + } + + /** + * @return true if building a 'protected' child item + * definition, false otherwise. + * @see ItemDefinition#isProtected() + */ + public boolean getProtected() { + return isProtected; + } + + /** + * @param isMandatory true if building a 'mandatory' child + * item definition, false otherwise. + * @see ItemDefinition#isMandatory() + */ + public void setMandatory(boolean isMandatory) { + this.isMandatory = isMandatory; + } + + /** + * @return true if building a 'mandatory' child item + * definition, false otherwise. + * @see ItemDefinition#isMandatory() + */ + public boolean getMandatory() { + return isMandatory; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QNodeDefinitionBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QNodeDefinitionBuilder.java new file mode 100644 index 00000000000..a885699145f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QNodeDefinitionBuilder.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.spi.commons.nodetype; + +import java.util.Arrays; +import java.util.Set; +import java.util.HashSet; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.commons.QNodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * A builder for a {@link QNodeDefinition}. + */ +public class QNodeDefinitionBuilder extends QItemDefinitionBuilder { + + private Name defaultPrimaryType; + private Set requiredPrimaryTypes = new HashSet(); + private boolean allowsSameNameSiblings; + + /** + * @param name the name of the default primary type of the node definition + * being built. + */ + public void setDefaultPrimaryType(Name name) { + defaultPrimaryType = name; + } + + /** + * @return the name of the default primary type of the node definition being + * built. + */ + public Name getDefaultPrimaryType() { + return defaultPrimaryType; + } + + /** + * Adds a required primary type of the node definition being built. + * + * @param name the name of a required primary type. + */ + public void addRequiredPrimaryType(Name name) { + requiredPrimaryTypes.add(name); + } + + /** + * @param names array of names of the required primary types of the node + * definition being built. + */ + public void setRequiredPrimaryTypes(Name[] names) { + requiredPrimaryTypes.clear(); + if (names != null) { + requiredPrimaryTypes.addAll(Arrays.asList(names)); + } + } + + /** + * @return array of names of the required primary types of the node + * definition being built. + */ + public Name[] getRequiredPrimaryTypes() { + if (requiredPrimaryTypes.isEmpty()) { + return new Name[]{NameConstants.NT_BASE}; + } else { + return requiredPrimaryTypes.toArray(new Name[requiredPrimaryTypes.size()]); + } + } + + /** + * @param allowSns true if building a node definition with same name + * siblings, false otherwise. + */ + public void setAllowsSameNameSiblings(boolean allowSns) { + allowsSameNameSiblings = allowSns; + } + + /** + * @return true if building a node definition with same name siblings, false + * otherwise. + */ + public boolean getAllowsSameNameSiblings() { + return allowsSameNameSiblings; + } + + /** + * Creates a new {@link QNodeDefinition} instance based on the state of this + * builder. + * + * @return a new {@link QNodeDefinition} instance. + * @throws IllegalStateException if the instance has not the necessary + * information to build the QNodeDefinition + * instance. + */ + public QNodeDefinition build() throws IllegalStateException { + return new QNodeDefinitionImpl(getName(), getDeclaringNodeType(), + getAutoCreated(), getMandatory(), getOnParentVersion(), + getProtected(), getDefaultPrimaryType(), + getRequiredPrimaryTypes(), getAllowsSameNameSiblings()); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QNodeTypeDefinitionBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QNodeTypeDefinitionBuilder.java new file mode 100644 index 00000000000..2bee933272b --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QNodeTypeDefinitionBuilder.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype; + +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.jcr.nodetype.NodeTypeDefinition; + +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl; + +/** + * A builder for {@link QNodeTypeDefinition}. + */ +public class QNodeTypeDefinitionBuilder { + + private Name name = null; + private List supertypes = new ArrayList(); + private boolean isMixin = false; + private boolean isOrderable = false; + private Name primaryItemName = null; + private List propertyDefinitions = new ArrayList(); + private List childNodeDefinitions = new ArrayList(); + private boolean isAbstract = false; + private boolean isQueryable = true; + private List supportedMixins = null; + + + /** + * Set the name of the node type definition being built + * @param name the name + * @see NodeTypeDefinition#getName() + */ + public void setName(Name name) { + this.name = name; + } + + /** + * @return the name of the node type definition being built or + * null if not set. + * @see NodeTypeDefinition#getName() + */ + public Name getName() { + return name; + } + + /** + * Specifies the supertypes of the node type definition being built + * @param supertypes the supertypes + * @see NodeTypeDefinition#getDeclaredSupertypeNames() + */ + public void setSupertypes(Name[] supertypes) { + this.supertypes.clear(); + this.supertypes.addAll(Arrays.asList(supertypes)); + } + + /** + * Returns an array containing the names of the supertypes of the node + * type definition being built. + * + * @return an array of supertype names + * @see NodeTypeDefinition#getDeclaredSupertypeNames() + */ + public Name[] getSuperTypes() { + if (supertypes.size() > 0 + || isMixin() || NameConstants.NT_BASE.equals(getName())) { + return supertypes.toArray(new Name[supertypes.size()]); + } else { + return new Name[] { NameConstants.NT_BASE }; + } + } + + /** + * @param isMixin true if building a mixin node type + * definition; false otherwise. + * @see NodeTypeDefinition#isMixin() + */ + public void setMixin(boolean isMixin) { + this.isMixin = isMixin; + } + + /** + * @return true if building a mixin node type definition; + * false otherwise. + * @see NodeTypeDefinition#isMixin() + */ + public boolean isMixin() { + return isMixin; + } + + /** + * Sets the names of additional mixin types supported on this node type. + * + * @param names an array of mixin type names, or null when + * there are no known constraints + */ + public void setSupportedMixinTypes(Name[] names) { + if (names == null) { + supportedMixins = null; + } else { + supportedMixins = new ArrayList(Arrays.asList(names)); + } + } + + /** + * Returns an array containing the names of additional mixin types supported + * on this node type. + * + * @return an array of mixin type names, or null when there are + * no known constraints. + */ + public Name[] getSupportedMixinTypes() { + if (supportedMixins == null) { + return null; + } else { + return supportedMixins.toArray(new Name[supportedMixins.size()]); + } + } + + /** + * @param isOrderable true if building a node type having + * orderable child nodes; false otherwise. + * @see NodeTypeDefinition#hasOrderableChildNodes() + */ + public void setOrderableChildNodes(boolean isOrderable) { + this.isOrderable = isOrderable; + } + + /** + * @return true if building a node type having orderable + * child nodes; false otherwise. + * @see NodeTypeDefinition#hasOrderableChildNodes() + */ + public boolean hasOrderableChildNodes() { + return isOrderable; + } + + /** + * @param primaryItemName the name of the primary item or + * null if not set. + * @see NodeTypeDefinition#getPrimaryItemName() + */ + public void setPrimaryItemName(Name primaryItemName) { + this.primaryItemName = primaryItemName; + } + + /** + * @return the name of the primary item or null if not set. + * @see NodeTypeDefinition#getPrimaryItemName() + */ + public Name getPrimaryItemName() { + return primaryItemName; + } + + /** + * @return true if the node type is abstract. + * @see NodeTypeDefinition#isAbstract() + */ + public boolean isAbstract() { + return isAbstract; + } + + /** + * @param isAbstract true if building a node type that is abstract. + * @see NodeTypeDefinition#isAbstract() + */ + public void setAbstract(boolean isAbstract) { + this.isAbstract = isAbstract; + } + + /** + * @return true if the node type is queryable + * @see NodeTypeDefinition#isQueryable() + */ + public boolean isQueryable() { + return isQueryable; + } + + /** + * @param queryable true if building a node type that is queryable + * @see NodeTypeDefinition#isQueryable() + */ + public void setQueryable(boolean queryable) { + isQueryable = queryable; + } + + /** + * @param propDefs an array containing the property definitions of the node type definition + * being built. + * @see NodeTypeDefinition#getDeclaredPropertyDefinitions() + */ + public void setPropertyDefs(QPropertyDefinition[] propDefs) { + propertyDefinitions.clear(); + propertyDefinitions.addAll(Arrays.asList(propDefs)); + } + + /** + * @return an array containing the property definitions of the node type + * definition being built. + * @see NodeTypeDefinition#getDeclaredPropertyDefinitions() + */ + public QPropertyDefinition[] getPropertyDefs() { + return propertyDefinitions.toArray(new QPropertyDefinition[propertyDefinitions.size()]); + } + + /** + * @param childDefs an array containing the child node definitions of the node type + * definition being. + * @see NodeTypeDefinition#getDeclaredChildNodeDefinitions() + */ + public void setChildNodeDefs(QNodeDefinition[] childDefs) { + childNodeDefinitions.clear(); + childNodeDefinitions.addAll(Arrays.asList(childDefs)); + } + + /** + * @return an array containing the child node definitions of the node type + * definition being built. + * @see NodeTypeDefinition#getDeclaredChildNodeDefinitions() + */ + public QNodeDefinition[] getChildNodeDefs() { + return childNodeDefinitions.toArray(new QNodeDefinition[childNodeDefinitions.size()]); + } + + /** + * Creates a new {@link QNodeTypeDefinition} instance based on the state of this builder. + * + * @return a new {@link QNodeTypeDefinition} instance. + * @throws IllegalStateException if the instance has not the necessary information to build + * the QNodeTypeDefinition instance. + */ + public QNodeTypeDefinition build() throws IllegalStateException { + return new QNodeTypeDefinitionImpl(getName(), getSuperTypes(), + getSupportedMixinTypes(), isMixin(), isAbstract(), + isQueryable(), hasOrderableChildNodes(), getPrimaryItemName(), + getPropertyDefs(), getChildNodeDefs()); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QPropertyDefinitionBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QPropertyDefinitionBuilder.java new file mode 100644 index 00000000000..687b4f10d52 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/QPropertyDefinitionBuilder.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.PropertyType; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.commons.QPropertyDefinitionImpl; + +/** + * A builder for {@link QPropertyDefinition}. + */ +public class QPropertyDefinitionBuilder extends QItemDefinitionBuilder { + + private int requiredType = PropertyType.UNDEFINED; + private List valueConstraints = new ArrayList(); + private List defaultValues; + private boolean isMultiple = false; + private boolean fullTextSearchable = true; + private boolean queryOrderable = true; + private String[] queryOperators = Operator.getAllQueryOperators(); + + /** + * @param type the required type of the property definition being built. + * @see PropertyDefinition#getRequiredType() + */ + public void setRequiredType(int type) { + requiredType = type; + } + + /** + * @return the required type of the property definition being built. + * @see PropertyDefinition#getRequiredType() + */ + public int getRequiredType() { + return requiredType; + } + + /** + * Adds a value constraint of the property definition being built. + * + * @param constraint the constraint. + */ + public void addValueConstraint(QValueConstraint constraint) { + valueConstraints.add(constraint); + } + + /** + * @param constraints array of value constraints of the property definition + * being built. + * @see PropertyDefinition#getValueConstraints() + */ + public void setValueConstraints(QValueConstraint[] constraints) { + valueConstraints.clear(); + valueConstraints.addAll(Arrays.asList(constraints)); + } + + /** + * @return array of value constraints of the property definition being + * built. + * @see PropertyDefinition#getValueConstraints() + */ + public QValueConstraint[] getValueConstraints() { + return valueConstraints.toArray(new QValueConstraint[valueConstraints.size()]); + } + + /** + * Adds a default value of the property definition being built. + * + * @param value a default value. + */ + public void addDefaultValue(QValue value) { + if (defaultValues == null) { + defaultValues = new ArrayList(); + } + defaultValues.add(value); + } + + /** + * @param values array of default values of the property definition being + * built. + * @see PropertyDefinition#getDefaultValues() + */ + public void setDefaultValues(QValue[] values) { + if (values == null) { + defaultValues = null; + } else { + // replace + defaultValues = new ArrayList(Arrays.asList(values)); + } + } + + /** + * @return array of default values of the property definition being built or + * null if no default values are defined. + * @see PropertyDefinition#getDefaultValues() + */ + public QValue[] getDefaultValues() { + if (defaultValues == null) { + return null; + } else { + return defaultValues.toArray(new QValue[defaultValues.size()]); + } + } + + /** + * @param isMultiple true if building a 'multiple' property definition. + * @see PropertyDefinition#isMultiple() + */ + public void setMultiple(boolean isMultiple) { + this.isMultiple = isMultiple; + } + + /** + * @return true if building a 'multiple' property definition. + * @see PropertyDefinition#isMultiple() + */ + public boolean getMultiple() { + return isMultiple; + } + + /** + * @return true if the property is fulltext searchable + * @see PropertyDefinition#isFullTextSearchable() + */ + public boolean getFullTextSearchable() { + return fullTextSearchable; + } + + /** + * @param fullTextSearchable true if building a 'fulltext + * searchable' property definition + * @see PropertyDefinition#isFullTextSearchable() + */ + public void setFullTextSearchable(boolean fullTextSearchable) { + this.fullTextSearchable = fullTextSearchable; + } + + /** + * @return true if the property is orderable in a query + * @see PropertyDefinition#isQueryOrderable() + */ + public boolean getQueryOrderable() { + return queryOrderable; + } + + /** + * @param queryOrderable true if the property is orderable in a + * query + * @see PropertyDefinition#isQueryOrderable() + */ + public void setQueryOrderable(boolean queryOrderable) { + this.queryOrderable = queryOrderable; + } + + /** + * @return the query operators of the property + * @see PropertyDefinition#getAvailableQueryOperators() + */ + public String[] getAvailableQueryOperators() { + return queryOperators; + } + + /** + * @param queryOperators the query operators of the property + * @see PropertyDefinition#getAvailableQueryOperators() + */ + public void setAvailableQueryOperators(String[] queryOperators) { + this.queryOperators = queryOperators; + } + + /** + * Creates a new {@link QPropertyDefinition} instance based on the state of + * this builder. + * + * @return a new {@link QPropertyDefinition} instance. + * @throws IllegalStateException if the instance has not the necessary + * information to build the QPropertyDefinition + * instance. + */ + public QPropertyDefinition build() throws IllegalStateException { + return new QPropertyDefinitionImpl(getName(), getDeclaringNodeType(), getAutoCreated(), getMandatory(), getOnParentVersion(), getProtected(), getDefaultValues(), getMultiple(), getRequiredType(), getValueConstraints(), getAvailableQueryOperators(), getFullTextSearchable(), getQueryOrderable()); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefWriter.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefWriter.java new file mode 100644 index 00000000000..7d94238b57f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefWriter.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype.compact; + +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.SessionNamespaceResolver; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; + +import javax.jcr.NamespaceException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeTypeDefinition; +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; + +/** + * Prints node type defs in a compact notation + * Print Format: + * <ex = "http://apache.org/jackrabbit/example"> + * [ex:NodeType] > ex:ParentType1, ex:ParentType2 + * orderable mixin + * - ex:property (STRING) = 'default1', 'default2' + * primary mandatory autocreated protected multiple VERSION + * < 'constraint1', 'constraint2' + * + ex:node (ex:reqType1, ex:reqType2) = ex:defaultType + * mandatory autocreated protected multiple VERSION + */ +public class CompactNodeTypeDefWriter extends org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefWriter { + + /** + * the current name/path resolver + */ + private final NamePathResolver npResolver; + + /** + * Creates a new nodetype writer based on a session + * + * @param out the underlying writer + * @param s repository session + * @param includeNS if true all used namespace declarations + * are also written to the writer + */ + public CompactNodeTypeDefWriter(Writer out, Session s, boolean includeNS) { + this(out, new SessionNamespaceResolver(s), new DefaultNamePathResolver(s), includeNS); + } + + /** + * Creates a new nodetype writer based on a namespace resolver + * + * @param out the underlying writer + * @param r the namespace resolver + * @param includeNS if true all used namespace decl. are also + * written to the writer + */ + public CompactNodeTypeDefWriter(Writer out, NamespaceResolver r, boolean includeNS) { + this(out, r, new DefaultNamePathResolver(r), includeNS); + } + + /** + * Creates a new nodetype writer that does not include namespaces. + * + * @param out the underlying writer + * @param r the namespace resolver + * @param npResolver name-path resolver + */ + public CompactNodeTypeDefWriter(Writer out, + NamespaceResolver r, + NamePathResolver npResolver) { + this(out, r, npResolver, false); + } + + /** + * Creates a new nodetype writer + * + * @param out the underlying writer + * @param r the namespace resolver + * @param npResolver name-path resolver + * @param includeNS if true all used namespace decl. are also + * written to the writer + */ + public CompactNodeTypeDefWriter(Writer out, + final NamespaceResolver r, + NamePathResolver npResolver, + boolean includeNS) { + super(out, createNsMapping(r), includeNS); + this.npResolver = npResolver; + } + + /** + * Writes the given list of QNodeTypeDefinition to the output writer including the + * used namespaces. + * + * @param defs collection of definitions + * @param r namespace resolver + * @param npResolver name-path resolver + * @param out output writer + * @throws IOException if an I/O error occurs + */ + public static void write(Collection defs, + NamespaceResolver r, + NamePathResolver npResolver, + Writer out) + throws IOException { + CompactNodeTypeDefWriter w = new CompactNodeTypeDefWriter(out, r, npResolver, true); + for (QNodeTypeDefinition def : defs) { + w.write(def); + } + w.close(); + } + + /** + * Write one QNodeTypeDefinition to this writer + * + * @param ntd node type definition + * @throws IOException if an I/O error occurs + */ + public void write(QNodeTypeDefinition ntd) throws IOException { + NodeTypeDefinition def = new NodeTypeDefinitionImpl(ntd, npResolver, new ValueFactoryQImpl(QValueFactoryImpl.getInstance(), npResolver)); + super.write(def); + } + + /** + * Write a collection of QNodeTypeDefinitions to this writer + * + * @param defs node type definitions + * @throws IOException if an I/O error occurs + */ + public void write(Collection defs) throws IOException { + for (QNodeTypeDefinition def : defs) { + write(def); + } + } + + private static NamespaceMapping createNsMapping(final NamespaceResolver namespaceResolver) { + return new org.apache.jackrabbit.commons.cnd.CompactNodeTypeDefWriter.NamespaceMapping() { + public String getNamespaceURI(String prefix) { + try { + return namespaceResolver.getURI(prefix); + } catch (NamespaceException e) { + throw new RuntimeException(e); + } + } + }; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/package-info.java new file mode 100644 index 00000000000..301a206c56f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.nodetype.compact; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/BooleanConstraint.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/BooleanConstraint.java new file mode 100644 index 00000000000..fd1a0ebbd2f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/BooleanConstraint.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.spi.commons.nodetype.constraint; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; + +/** + * BooleanConstraint ... + */ +class BooleanConstraint extends ValueConstraint { + + private final boolean reqBool; + + public BooleanConstraint(String definition) throws InvalidConstraintException { + super(definition); + + // constraint format: 'true' or 'false' + if (definition.equals("true")) { + reqBool = true; + } else if (definition.equals("false")) { + reqBool = false; + } else { + String msg = "'" + definition + + "' is not a valid value constraint format for BOOLEAN values"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + + /** + * @see org.apache.jackrabbit.spi.QValueConstraint#check(QValue) + */ + public void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getString() + "'"); + } + switch (value.getType()) { + case PropertyType.BOOLEAN: + boolean b = Boolean.valueOf(value.getString()); + if (b != reqBool) { + throw new ConstraintViolationException("'" + b + "' does not satisfy the constraint '" + getString() + "'"); + } + return; + + default: + String msg = "BOOLEAN constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/DateConstraint.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/DateConstraint.java new file mode 100644 index 00000000000..9e4dd70c3bb --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/DateConstraint.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.spi.commons.nodetype.constraint; + +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; +import org.apache.jackrabbit.value.DateValue; + +/** + * DateConstraint ... + */ +class DateConstraint extends ValueConstraint { + + private final boolean lowerInclusive; + + private final Calendar lowerLimit; + + private final boolean upperInclusive; + + private final Calendar upperLimit; + + public DateConstraint(String definition) throws InvalidConstraintException { + super(definition); + + // format: '(, )', '[, ]', '[, ]' etc. + Pattern pattern = Pattern.compile("([\\(\\[]) *([0-9TZ\\.\\+-:]*)? *, *([0-9TZ\\.\\+-:]*)? *([\\)\\]])"); + Matcher matcher = pattern.matcher(definition); + if (matcher.matches()) { + try { + // group 1 is lower inclusive/exclusive + String s = matcher.group(1); + lowerInclusive = s.equals("["); + // group 2 is lower limit + s = matcher.group(2); + if (s == null || s.length() == 0) { + lowerLimit = null; + } else { + lowerLimit = DateValue.valueOf(matcher.group(2)).getDate(); + } + // group 3 is upper limit + s = matcher.group(3); + if (s == null || s.length() == 0) { + upperLimit = null; + } else { + upperLimit = DateValue.valueOf(matcher.group(3)).getDate(); + } + // group 4 is upper inclusive/exclusive + s = matcher.group(4); + upperInclusive = s.equals("]"); + + if (lowerLimit == null && upperLimit == null) { + String msg = "'" + definition + + "' is not a valid value constraint format for dates: neither min- nor max-date specified"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + if (lowerLimit != null && upperLimit != null) { + if (lowerLimit.after(upperLimit)) { + String msg = "'" + definition + + "' is not a valid value constraint format for dates: min-date > max-date"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + } catch (ValueFormatException vfe) { + String msg = "'" + definition + + "' is not a valid value constraint format for dates"; + log.debug(msg); + throw new InvalidConstraintException(msg, vfe); + } catch (RepositoryException re) { + String msg = "'" + definition + + "' is not a valid value constraint format for dates"; + log.debug(msg); + throw new InvalidConstraintException(msg, re); + } + } else { + String msg = "'" + definition + + "' is not a valid value constraint format for dates"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + + private void check(Calendar cal) throws ConstraintViolationException { + if (cal == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getString() + "'"); + } + if (lowerLimit != null) { + if (lowerInclusive) { + if (cal.getTimeInMillis() < lowerLimit.getTimeInMillis()) { + throw new ConstraintViolationException(cal + + " does not satisfy the constraint '" + + getString() + "'"); + } + } else { + if (cal.getTimeInMillis() <= lowerLimit.getTimeInMillis()) { + throw new ConstraintViolationException(cal + + " does not satisfy the constraint '" + + getString() + "'"); + } + } + } + if (upperLimit != null) { + if (upperInclusive) { + if (cal.getTimeInMillis() > upperLimit.getTimeInMillis()) { + throw new ConstraintViolationException(cal + + " does not satisfy the constraint '" + + getString() + "'"); + } + } else { + if (cal.getTimeInMillis() >= upperLimit.getTimeInMillis()) { + throw new ConstraintViolationException(cal + + " does not satisfy the constraint '" + + getString() + "'"); + } + } + } + } + + /** + * @see org.apache.jackrabbit.spi.QValueConstraint#check(QValue) + */ + public void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getString() + "'"); + } + switch (value.getType()) { + case PropertyType.DATE: + check(value.getCalendar()); + return; + + default: + String msg = "DATE constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/NameConstraint.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/NameConstraint.java new file mode 100644 index 00000000000..3929a36d0b7 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/NameConstraint.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype.constraint; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; + +/** + * NameConstraint ... + */ +class NameConstraint extends ValueConstraint { + + private final Name name; + + static NameConstraint create(String nameString) { + // constraint format: String representation of a Name object + return new NameConstraint(nameString, NAME_FACTORY.create(nameString)); + } + + static NameConstraint create(String jcrName, NameResolver resolver) + throws InvalidConstraintException { + // constraint format: A JCR name string. + try { + Name name = resolver.getQName(jcrName); + return new NameConstraint(name.toString(), name); + } catch (NameException e) { + String msg = "Invalid name constraint: " + jcrName; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } catch (NamespaceException e) { + String msg = "Invalid name constraint: " + jcrName; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } + } + + private NameConstraint(String nameString, Name name) { + super(nameString); + this.name = name; + } + + /** + * Uses {@link NamePathResolver#getJCRName(Name)} to convert the + * Name identifying this constraint into a JCR name String. + * + * @see ValueConstraint#getDefinition(NamePathResolver) + * @param resolver name-path resolver + */ + @Override + public String getDefinition(NamePathResolver resolver) { + try { + return resolver.getJCRName(name); + } catch (NamespaceException e) { + // should never get here, return raw definition as fallback + return getString(); + } + } + + /** + * @see org.apache.jackrabbit.spi.QValueConstraint#check(QValue) + */ + public void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getString() + "'"); + } + switch (value.getType()) { + case PropertyType.NAME: + Name n = value.getName(); + if (!name.equals(n)) { + throw new ConstraintViolationException(n + + " does not satisfy the constraint '" + + getString() + "'"); + } + return; + + default: + String msg = "NAME constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/NumericConstraint.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/NumericConstraint.java new file mode 100644 index 00000000000..a916473517a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/NumericConstraint.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.spi.commons.nodetype.constraint; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; + +/** + * NumericConstraint ... + */ +class NumericConstraint extends ValueConstraint { + + private final boolean lowerInclusive; + + private final Double lowerLimit; + + private final boolean upperInclusive; + + private final Double upperLimit; + + public NumericConstraint(String definition) throws InvalidConstraintException { + super(definition); + + // format: '(, )', '[, ]', '(, )' etc. + Pattern pattern = Pattern.compile("([\\(\\[]) *(\\-?\\d+\\.?\\d*)? *, *(\\-?\\d+\\.?\\d*)? *([\\)\\]])"); + Matcher matcher = pattern.matcher(definition); + if (matcher.matches()) { + try { + // group 1 is lower inclusive/exclusive + String s = matcher.group(1); + lowerInclusive = s.equals("["); + // group 2 is lower limit + s = matcher.group(2); + if (s == null || s.length() == 0) { + lowerLimit = null; + } else { + lowerLimit = Double.valueOf(matcher.group(2)); + } + // group 3 is upper limit + s = matcher.group(3); + if (s == null || s.length() == 0) { + upperLimit = null; + } else { + upperLimit = Double.valueOf(matcher.group(3)); + } + // group 4 is lower inclusive/exclusive + s = matcher.group(4); + upperInclusive = s.equals("]"); + if (lowerLimit == null && upperLimit == null) { + String msg = "'" + definition + "' is not a valid value constraint" + + " format for numeric types: neither lower- nor upper-limit specified"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + if (lowerLimit != null && upperLimit != null) { + if (lowerLimit > upperLimit) { + String msg = "'" + definition + + "' is not a valid value constraint format for numeric types: lower-limit exceeds upper-limit"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + } catch (NumberFormatException nfe) { + String msg = "'" + definition + + "' is not a valid value constraint format for numeric types"; + log.debug(msg); + throw new InvalidConstraintException(msg, nfe); + } + } else { + String msg = "'" + definition + + "' is not a valid value constraint format for numeric values"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + + private void check(double number) throws ConstraintViolationException { + if (lowerLimit != null) { + if (lowerInclusive) { + if (number < lowerLimit) { + throw new ConstraintViolationException(number + + " does not satisfy the constraint '" + + getString() + "'"); + } + } else { + if (number <= lowerLimit) { + throw new ConstraintViolationException(number + + " does not satisfy the constraint '" + + getString() + "'"); + } + } + } + if (upperLimit != null) { + if (upperInclusive) { + if (number > upperLimit) { + throw new ConstraintViolationException(number + + " does not satisfy the constraint '" + + getString() + "'"); + } + } else { + if (number >= upperLimit) { + throw new ConstraintViolationException(number + + " does not satisfy the constraint '" + + getString() + "'"); + } + } + } + } + + /** + * @see org.apache.jackrabbit.spi.QValueConstraint#check(QValue) + */ + public void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + + getString() + "'"); + } + switch (value.getType()) { + case PropertyType.LONG: + check(value.getLong()); + return; + + case PropertyType.DOUBLE: + check(value.getDouble()); + return; + + case PropertyType.DECIMAL: + check(value.getDouble()); + return; + + case PropertyType.BINARY: + long length = value.getLength(); + if (length != -1) { + check(length); + } else { + log.warn("failed to determine length of binary value"); + } + return; + + default: + String msg = "numeric constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/PathConstraint.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/PathConstraint.java new file mode 100644 index 00000000000..b7b16c864ca --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/PathConstraint.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype.constraint; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; + +/** + * PathConstraint ... + */ +class PathConstraint extends ValueConstraint { + + static final String WILDCARD = Path.DELIMITER + NameConstants.ANY_NAME.toString(); + static final String JCR_WILDCARD = "/*"; + // TODO improve. don't rely on a specific factory impl + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + private final Path path; + private final boolean deep; + + static PathConstraint create(String pathString) { + // constraint format: String representation of an absolute or relative + // Path object with optionally having a trailing wild card + if (WILDCARD.equals(pathString)) { + return new PathConstraint(pathString, PATH_FACTORY.getRootPath(), true); + } else { + boolean deep = pathString.endsWith(WILDCARD); + Path path; + if (deep) { + path = PATH_FACTORY.create(pathString.substring(0, pathString.length() - WILDCARD.length())); + } else { + path = PATH_FACTORY.create(pathString); + } + return new PathConstraint(pathString, path, deep); + } + } + + static PathConstraint create(String jcrPath, PathResolver resolver) + throws InvalidConstraintException { + try { + // constraint format: absolute or relative path with optional + // trailing wild card + boolean deep = jcrPath.endsWith(JCR_WILDCARD); + Path path; + if (JCR_WILDCARD.equals(jcrPath)) { + path = PATH_FACTORY.getRootPath(); + } else { + if (deep) { + // trim trailing wild card before building path + jcrPath = jcrPath.substring(0, jcrPath.length() - JCR_WILDCARD.length()); + } + path = resolver.getQPath(jcrPath); + } + StringBuffer definition = new StringBuffer(path.getString()); + if (deep) { + definition.append(WILDCARD); + } + return new PathConstraint(definition.toString(), path, deep); + } catch (NameException e) { + String msg = "Invalid path expression specified as value constraint: " + jcrPath; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } catch (NamespaceException e) { + String msg = "Invalid path expression specified as value constraint: " + jcrPath; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } + } + + private PathConstraint(String pathString, Path path, boolean deep) { + super(pathString); + this.path = path; + this.deep = deep; + } + + /** + * Uses {@link NamePathResolver#getJCRPath(Path)} to convert the + * Path present with this constraint into a JCR path. + * + * @see ValueConstraint#getDefinition(NamePathResolver) + * @param resolver name-path resolver + */ + @Override + public String getDefinition(NamePathResolver resolver) { + try { + String p = resolver.getJCRPath(path); + if (!deep) { + return p; + } else if (path.denotesRoot()) { + return p + "*"; + } else { + return p + "/*"; + } + } catch (NamespaceException e) { + // should never get here, return raw definition as fallback + return getString(); + } + } + + /** + * @see org.apache.jackrabbit.spi.QValueConstraint#check(QValue) + */ + public void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getString() + "'"); + } + switch (value.getType()) { + case PropertyType.PATH: + Path p = value.getPath(); + // normalize paths before comparing them + Path p0, p1; + try { + p0 = path.getNormalizedPath(); + p1 = p.getNormalizedPath(); + } catch (RepositoryException e) { + throw new ConstraintViolationException("path not valid: " + e); + } + if (deep) { + try { + if (!p0.isAncestorOf(p1)) { + throw new ConstraintViolationException(p + + " does not satisfy the constraint '" + + getString() + "'"); + } + } catch (RepositoryException e) { + // can't compare relative with absolute path + throw new ConstraintViolationException(p + + " does not satisfy the constraint '" + + getString() + "'"); + } + } else { + // exact match required + if (!p0.equals(p1)) { + throw new ConstraintViolationException(p + + " does not satisfy the constraint '" + + getString() + "'"); + } + } + return; + + default: + String msg = "PATH constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/ReferenceConstraint.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/ReferenceConstraint.java new file mode 100644 index 00000000000..9d2a46e602f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/ReferenceConstraint.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.spi.commons.nodetype.constraint; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; + +/** + * ReferenceConstraint ... + */ +class ReferenceConstraint extends ValueConstraint { + + private final Name ntName; + + static ReferenceConstraint create(String nameString) { + // constraint format: String representation of Name object. + return new ReferenceConstraint(nameString, NAME_FACTORY.create(nameString)); + } + + static ReferenceConstraint create(String jcrName, NameResolver resolver) + throws InvalidConstraintException { + // constraint format: JCR name in prefix form + try { + Name name = resolver.getQName(jcrName); + return new ReferenceConstraint(name.toString(), name); + } catch (NameException e) { + String msg = "Invalid name constraint: " + jcrName; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } catch (NamespaceException e) { + String msg = "Invalid name constraint: " + jcrName; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } + } + + private ReferenceConstraint(String nameString, Name ntName) { + super(nameString); + this.ntName = ntName; + } + + /** + * Uses {@link NamePathResolver#getJCRName(Name)} to convert the node type + * Name present with this constraint into a JCR name String. + * + * @see ValueConstraint#getDefinition(NamePathResolver) + * @param resolver name-path resolver + */ + @Override + public String getDefinition(NamePathResolver resolver) { + try { + return resolver.getJCRName(ntName); + } catch (NamespaceException e) { + // should never get here, return raw definition as fallback + return getString(); + } + } + + /** + * @see org.apache.jackrabbit.spi.QValueConstraint#check(QValue) + */ + public void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("Null value does not satisfy the constraint '" + getString() + "'"); + } + switch (value.getType()) { + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + // TODO check value constraint (requires a session) + log.warn("validation of reference constraint is not yet implemented"); + return; + + default: + String msg = "Reference constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/StringConstraint.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/StringConstraint.java new file mode 100644 index 00000000000..204bdd244f5 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/StringConstraint.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.spi.commons.nodetype.constraint; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; + +/** + * StringConstraint ... + */ +class StringConstraint extends ValueConstraint { + + private final Pattern pattern; + + public StringConstraint(String definition) throws InvalidConstraintException { + super(definition); + + // constraint format: regexp + try { + pattern = Pattern.compile(definition); + } catch (PatternSyntaxException pse) { + String msg = "'" + definition + "' is not valid regular expression syntax"; + log.debug(msg); + throw new InvalidConstraintException(msg, pse); + } + } + + /** + * @see org.apache.jackrabbit.spi.QValueConstraint#check(QValue) + */ + public void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getString() + "'"); + } + switch (value.getType()) { + case PropertyType.STRING: + case PropertyType.URI: + String text = value.getString(); + Matcher matcher = pattern.matcher(text); + if (!matcher.matches()) { + throw new ConstraintViolationException("'" + text + "' does not satisfy the constraint '" + getString() + "'"); + } + return; + + default: + String msg = "String constraint can not be applied to value of type: " + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/ValueConstraint.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/ValueConstraint.java new file mode 100644 index 00000000000..17e7bc18131 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/ValueConstraint.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.nodetype.constraint; + + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueConstraint; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ValueConstraint and its subclasses are used to check the + * syntax of a value constraint and to test if a specific value satisfies + * it. + */ +public abstract class ValueConstraint implements QValueConstraint { + + protected static Logger log = LoggerFactory.getLogger(ValueConstraint.class); + + public static final ValueConstraint[] EMPTY_ARRAY = new ValueConstraint[0]; + + // TODO improve. don't rely on a specific factory impl + static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + + private final String definition; + + protected ValueConstraint(String definition) { + this.definition = definition; + } + + /** + * For constraints that are not namespace prefix mapping sensitive this + * method returns the same result as {@link #getString()}. + *

        + * Those that are namespace prefix mapping sensitive (e.g. + * NameConstraint, PathConstraint and + * ReferenceConstraint) use the given nsResolver + * to reflect the current mapping in the returned value. + * In other words: subclasses, that need to make a conversion to JCR value + * must overwrite this and return a value that has the Names + * or Path properly resolved to their JCR representation. + * + * @return the definition of this constraint. + * @see #getString () + * @param resolver name-path resolver + * @see NamePathResolver#getJCRName(org.apache.jackrabbit.spi.Name) + * @see NamePathResolver#getJCRPath(org.apache.jackrabbit.spi.Path) + */ + public String getDefinition(NamePathResolver resolver) { + return definition; + } + + //---------------------------------------------------< QValueConstraint >--- + /** + * @see org.apache.jackrabbit.spi.QValueConstraint#getString() + */ + public String getString() { + return definition; + } + + //---------------------------------------------------< java.lang.Object >--- + /** + * Same as {@link #getString()} + * @return the internal definition String + * @see Object#toString() + */ + @Override + public String toString() { + return getString(); + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object other) { + return other == this + || other instanceof ValueConstraint + && definition.equals(((ValueConstraint) other).definition); + } + + /** + * Returns the hashCode of the definition String + * + * @return the hashCode of the definition String + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return definition.hashCode(); + } + + //-----------------------------------< static factory and check methods >--- + /** + * Create a new ValueConstraint from the String representation. + * Note, that the definition must be independent of session specific namespace + * mappings in case of the following constraint types: + *

        • {@link PropertyType#NAME},
        • + *
        • {@link PropertyType#PATH} or
        • + *
        • {@link PropertyType#REFERENCE}
        • + *
        + * + * @param type required type + * @param definition The internal definition string. + * @return a new value constraint + * @throws InvalidConstraintException if the constraint is not valid. + * @see #create(int, String, NamePathResolver) for the corresponding + * method that allows to pass the JCR representation of a constraint + * definition. + */ + public static ValueConstraint create(int type, String definition) + throws InvalidConstraintException { + if (definition == null) { + throw new IllegalArgumentException("illegal definition (null)"); + } + switch (type) { + // constraints which are not qName sensitive + case PropertyType.STRING: + case PropertyType.URI: + return new StringConstraint(definition); + + case PropertyType.BOOLEAN: + return new BooleanConstraint(definition); + + case PropertyType.BINARY: + return new NumericConstraint(definition); + + case PropertyType.DATE: + return new DateConstraint(definition); + + case PropertyType.LONG: + case PropertyType.DOUBLE: + case PropertyType.DECIMAL: + return new NumericConstraint(definition); + + case PropertyType.NAME: + return NameConstraint.create(definition); + + case PropertyType.PATH: + return PathConstraint.create(definition); + + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + return ReferenceConstraint.create(definition); + + default: + throw new IllegalArgumentException("unknown/unsupported target type for constraint: " + + PropertyType.nameFromValue(type)); + } + } + + /** + * Create a new ValueConstraint array from the String + * representation. Note, that the definition must be in the internal format + * in case of the following types: + *
        • {@link PropertyType#NAME},
        • + *
        • {@link PropertyType#PATH} or
        • + *
        • {@link PropertyType#REFERENCE}
        • + *
        + * + * @param type the required type + * @param definition internal definition strings + * @return the array of constraints + * @throws InvalidConstraintException if one of the constraints is invalid + */ + public static ValueConstraint[] create(int type, String[] definition) + throws InvalidConstraintException { + if (definition == null || definition.length == 0) { + return ValueConstraint.EMPTY_ARRAY; + } + ValueConstraint[] ret = new ValueConstraint[definition.length]; + for (int i=0; iValueConstraint array from the specified JCR + * representations. + * + * @param type the required type + * @param jcrDefinition The definition strings as exposed through the JCR API. + * @param resolver name-path resolver + * @return the array of constraints + * @throws InvalidConstraintException if one of the constraints is invalid + */ + public static ValueConstraint[] create(int type, String jcrDefinition[], NamePathResolver resolver) + throws InvalidConstraintException { + if (jcrDefinition == null || jcrDefinition.length == 0) { + return ValueConstraint.EMPTY_ARRAY; + } + ValueConstraint[] ret = new ValueConstraint[jcrDefinition.length]; + for (int i=0; ipd are satisfied by the the specified values. + *

        + * Note that the protected flag is not checked. Also note that no + * type conversions are attempted if the type of the given values does not + * match the required type as specified in the given definition. + * + * @param pd property definition + * @param values values to check + * @throws ConstraintViolationException if the constraints are violated + */ + public static void checkValueConstraints(QPropertyDefinition pd, QValue[] values) + throws ConstraintViolationException, RepositoryException { + // check multi-value flag + if (!pd.isMultiple() && values != null && values.length > 1) { + throw new ConstraintViolationException("the property is not multi-valued"); + } + + QValueConstraint[] constraints = pd.getValueConstraints(); + if (constraints == null || constraints.length == 0) { + // no constraints to check + return; + } + if (values != null && values.length > 0) { + // check value constraints on every value + for (QValue value : values) { + // constraints are OR-ed together + boolean satisfied = false; + ConstraintViolationException cve = null; + for (int j = 0; j < constraints.length && !satisfied; j++) { + try { + constraints[j].check(value); + satisfied = true; + } catch (ConstraintViolationException e) { + cve = e; + } catch (InvalidConstraintException e) { + cve = new ConstraintViolationException(e.getMessage(), e); + } + } + if (!satisfied) { + // re-throw last exception we encountered + throw cve; + } + } + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/package-info.java new file mode 100644 index 00000000000..d69babda99e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/constraint/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.nodetype.constraint; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/package-info.java new file mode 100644 index 00000000000..a0e055437d7 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/nodetype/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.nodetype; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/package-info.java new file mode 100644 index 00000000000..ce24916950c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.1") +package org.apache.jackrabbit.spi.commons; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/ParseException.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/ParseException.java new file mode 100644 index 00000000000..19d337c8354 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/ParseException.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.spi.commons.privilege; + +/** + * ParseException... + */ +public class ParseException extends Exception { + + public ParseException(Throwable throwable) { + super(throwable); + } + + public ParseException(String s, Throwable throwable) { + super(s, throwable); + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionImpl.java new file mode 100644 index 00000000000..dbcc13c91c6 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionImpl.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.spi.commons.privilege; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PrivilegeDefinition; + +import java.util.Collections; +import java.util.Set; + +/** + * PrivilegeDefinition + */ +public class PrivilegeDefinitionImpl implements PrivilegeDefinition { + + private final Name name; + private final boolean isAbstract; + private final Set declaredAggregateNames; + + public PrivilegeDefinitionImpl(Name name, boolean isAbstract, Set declaredAggregateNames) { + this.name = name; + this.isAbstract = isAbstract; + this.declaredAggregateNames = declaredAggregateNames == null ? Collections.emptySet() : Collections.unmodifiableSet(declaredAggregateNames); + } + + //------------------------------------------------< PrivilegeDefinition >--- + /** + * @see PrivilegeDefinition#getName() + */ + public Name getName() { + return name; + } + + /** + * @see PrivilegeDefinition#isAbstract() + */ + public boolean isAbstract() { + return isAbstract; + } + + /** + * @see PrivilegeDefinition#getDeclaredAggregateNames() + */ + public Set getDeclaredAggregateNames() { + return declaredAggregateNames; + } + + //-------------------------------------------------------------< Object >--- + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PrivilegeDefinitionImpl that = (PrivilegeDefinitionImpl) o; + + if (isAbstract != that.isAbstract) return false; + if (name != null ? !name.equals(that.name) : that.name != null) return false; + return declaredAggregateNames.equals(that.declaredAggregateNames); + + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (isAbstract ? 1 : 0); + result = 31 * result + (declaredAggregateNames != null ? declaredAggregateNames.hashCode() : 0); + return result; + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionReader.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionReader.java new file mode 100644 index 00000000000..3915bfbc69c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionReader.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.spi.commons.privilege; + +import org.apache.jackrabbit.spi.PrivilegeDefinition; + +import java.io.InputStream; +import java.io.Reader; +import java.util.HashMap; +import java.util.Map; + +/** + * Reads privilege definitions for the specified InputStream. Note, + * that this reader will not apply any validation. + */ +public class PrivilegeDefinitionReader { + + private final PrivilegeDefinition[] privilegeDefinitions; + private final Map namespaces = new HashMap(); + + /** + * Creates a new PrivilegeDefinitionReader for the given + * input stream. The specified content type is used in order to determine + * the type of privilege serialization. + * + * @param in The input stream to read the privilege definitions from. + * @param contentType Currently only types supported by + * {@link PrivilegeXmlHandler#isSupportedContentType(String)} + * are allowed. + * @throws ParseException If an error occurs. + * @throws IllegalArgumentException if the specified content type is not supported. + */ + public PrivilegeDefinitionReader(InputStream in, String contentType) throws ParseException { + if (PrivilegeXmlHandler.isSupportedContentType(contentType)) { + PrivilegeHandler pxh = new PrivilegeXmlHandler(); + privilegeDefinitions = pxh.readDefinitions(in, namespaces); + } else { + // not yet supported + throw new IllegalArgumentException("Unsupported content type " + contentType); + } + } + + /** + * Creates a new PrivilegeDefinitionReader for the given + * input stream. The specified content type is used in order to determine + * the type of privilege serialization. + * + * @param reader The reader to read the privilege definitions from. + * @param contentType Currently only types supported by + * {@link PrivilegeXmlHandler#isSupportedContentType(String)} + * are allowed. + * @throws ParseException If an error occurs. + * @throws IllegalArgumentException if the specified content type is not supported. + */ + public PrivilegeDefinitionReader(Reader reader, String contentType) throws ParseException { + if (PrivilegeXmlHandler.isSupportedContentType(contentType)) { + PrivilegeHandler pxh = new PrivilegeXmlHandler(); + privilegeDefinitions = pxh.readDefinitions(reader, namespaces); + } else { + // not yet supported + throw new IllegalArgumentException("Unsupported content type " + contentType); + } + } + + /** + * Returns the privilege definitions retrieved from the input stream. + * + * @return an array of PrivilegeDefinition + */ + public PrivilegeDefinition[] getPrivilegeDefinitions() { + return privilegeDefinitions; + } + + /** + * Returns the namespace mappings such as retrieved during parsing. + * + * @return a mapping of namespace prefix to uri used by the privilege + * definitions. + */ + public Map getNamespaces() { + return namespaces; + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionWriter.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionWriter.java new file mode 100644 index 00000000000..19ada938114 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeDefinitionWriter.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.spi.commons.privilege; + +import org.apache.jackrabbit.spi.PrivilegeDefinition; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Writer; +import java.util.Map; + +/** + * Writes privilege definitions to an output stream. + */ +public class PrivilegeDefinitionWriter { + + private final PrivilegeHandler ph; + + /** + * Creates a new PrivilegeDefinitionWriter. + * + * @param contentType The content type used to determine the type of + * serialization. + * @throws IllegalArgumentException if the specified content type is not + * supported. + */ + public PrivilegeDefinitionWriter(String contentType) { + if (PrivilegeXmlHandler.isSupportedContentType(contentType)) { + ph = new PrivilegeXmlHandler(); + } else { + // not yet supported + throw new IllegalArgumentException("Unsupported content type"); + } + } + + /** + * Writes the privilege definitions to the specified output stream. + * + * @param out The output stream. + * @param privilegeDefinitions The privilege definitions to write to the + * given output stream. + * @param namespaces The namespace mapping (prefix to uri) used by the + * specified definitions. + * @throws IOException If an error occurs. + */ + public void writeDefinitions(OutputStream out, PrivilegeDefinition[] privilegeDefinitions, Map namespaces) throws IOException { + ph.writeDefinitions(out, privilegeDefinitions, namespaces); + } + + /** + * Writes the privilege definitions to the specified output stream. + * + * @param writer The writer. + * @param privilegeDefinitions The privilege definitions to write to the + * given output stream. + * @param namespaces The namespace mapping (prefix to uri) used by the + * specified definitions. + * @throws IOException If an error occurs. + */ + public void writeDefinitions(Writer writer, PrivilegeDefinition[] privilegeDefinitions, Map namespaces) throws IOException { + ph.writeDefinitions(writer, privilegeDefinitions, namespaces); + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeHandler.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeHandler.java new file mode 100644 index 00000000000..17411c012de --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeHandler.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.spi.commons.privilege; + +import org.apache.jackrabbit.spi.PrivilegeDefinition; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Map; + +/** + * Interface used to define the (de)serialization mode of the privilege definitions. + */ +public interface PrivilegeHandler { + + /** + * Read the privilege definitions and update the specified namespace mapping. + * + * @param in + * @param namespaces + * @return the privilege definitions contained in the specified stream. + * @throws ParseException + */ + PrivilegeDefinition[] readDefinitions(InputStream in, Map namespaces) throws ParseException; + + /** + * Read the privilege definitions and update the specified namespace mapping. + * + * @param reader + * @param namespaces + * @return the privilege definitions contained in the specified stream. + * @throws ParseException + */ + PrivilegeDefinition[] readDefinitions(Reader reader, Map namespaces) throws ParseException; + + /** + * Write the specified privilege definitions to the given output stream. + * + * @param out + * @param definitions + * @param namespaces + * @throws IOException + */ + void writeDefinitions(OutputStream out, PrivilegeDefinition[] definitions, Map namespaces) throws IOException; + + /** + * Write the specified privilege definitions to the given writer. + * + * @param writer + * @param definitions + * @param namespaces + * @throws IOException + */ + void writeDefinitions(Writer writer, PrivilegeDefinition[] definitions, Map namespaces) throws IOException; +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeXmlHandler.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeXmlHandler.java new file mode 100644 index 00000000000..ffa24fe2001 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/PrivilegeXmlHandler.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.privilege; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.util.Text; +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 javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The PrivilegeXmlHandler loads and stores 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> + * ]> + * + */ +class PrivilegeXmlHandler implements PrivilegeHandler { + + 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 final String LICENSE_HEADER = createLicenseHeader(); + + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + + private static String createLicenseHeader() { + return "\n" + + " Licensed to the Apache Software Foundation (ASF) under one or more\n" + + " contributor license agreements. See the NOTICE file distributed with\n" + + " this work for additional information regarding copyright ownership.\n" + + " The ASF licenses this file to You under the Apache License, Version 2.0\n" + + " (the \"License\"); you may not use this file except in compliance with\n" + + " the License. You may obtain a copy of the License at\n" + + "\n" + + " http://www.apache.org/licenses/LICENSE-2.0\n" + + "\n" + + " Unless required by applicable law or agreed to in writing, software\n" + + " distributed under the License is distributed on an \"AS IS\" BASIS,\n" + + " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n" + + " See the License for the specific language governing permissions and\n" + + " limitations under the License.\n"; + } + + /** + * Constant for DocumentBuilderFactory. + */ + 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; + } + + /** + * Constant for TransformerFactory + */ + private static TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance(); + + /** + * Create a new instance. + */ + PrivilegeXmlHandler() { + + } + + /** + * Returns true if the specified content type can be handled by this class. + * + * @param contentType + * @return true if the specified content type can be handled + * by this class; false otherwise. + */ + static boolean isSupportedContentType(String contentType) { + return TEXT_XML.equals(contentType) || APPLICATION_XML.equals(contentType); + } + + //---------------------------------------------------< PrivilegeHandler >--- + /** + * @see PrivilegeHandler#readDefinitions(java.io.InputStream, java.util.Map) + */ + public PrivilegeDefinition[] readDefinitions(InputStream in, Map namespaces) throws ParseException { + return readDefinitions(new InputSource(in), namespaces); + + } + + /** + * @see PrivilegeHandler#readDefinitions(java.io.Reader, java.util.Map) + */ + public PrivilegeDefinition[] readDefinitions(Reader reader, Map namespaces) throws ParseException { + return readDefinitions(new InputSource(reader), namespaces); + } + + private PrivilegeDefinition[] readDefinitions(InputSource input, Map namespaces) throws ParseException { + 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, namespaces); + + NodeList nl = root.getElementsByTagName(XML_PRIVILEGE); + for (int i = 0; i < nl.getLength(); i++) { + Node n = nl.item(i); + PrivilegeDefinition def = parseDefinition(n, namespaces); + if (def != null) { + defs.add(def); + } + } + return defs.toArray(new PrivilegeDefinition[defs.size()]); + + } catch (SAXException e) { + throw new ParseException(e); + } catch (IOException e) { + throw new ParseException(e); + } catch (ParserConfigurationException e) { + throw new ParseException(e); + } + } + + /** + * @see PrivilegeHandler#writeDefinitions(java.io.OutputStream, PrivilegeDefinition[], java.util.Map) + */ + public void writeDefinitions(OutputStream out, PrivilegeDefinition[] definitions, Map namespaces) throws IOException { + writeDefinitions(new StreamResult(out), definitions, namespaces); + } + + /** + * @see PrivilegeHandler#writeDefinitions(java.io.Writer, PrivilegeDefinition[], java.util.Map) + */ + public void writeDefinitions(Writer writer, PrivilegeDefinition[] definitions, Map namespaces) throws IOException { + writeDefinitions(new StreamResult(writer), definitions, namespaces); + } + + private void writeDefinitions(Result result, PrivilegeDefinition[] definitions, Map namespaces) throws IOException { + try { + Map uriToPrefix = new HashMap(namespaces.size()); + DocumentBuilder builder = createDocumentBuilder(); + Document doc = builder.newDocument(); + doc.appendChild(doc.createComment(LICENSE_HEADER)); + Element privileges = (Element) doc.appendChild(doc.createElement(XML_PRIVILEGES)); + + for (String prefix : namespaces.keySet()) { + String uri = namespaces.get(prefix); + privileges.setAttribute(ATTR_XMLNS + prefix, uri); + uriToPrefix.put(uri, prefix); + } + + for (PrivilegeDefinition def : definitions) { + Element priv = (Element) privileges.appendChild(doc.createElement(XML_PRIVILEGE)); + priv.setAttribute(ATTR_NAME, getQualifiedName(def.getName(), uriToPrefix)); + priv.setAttribute(ATTR_ABSTRACT, Boolean.valueOf(def.isAbstract()).toString()); + + for (Name aggrName : def.getDeclaredAggregateNames()) { + Element contains = (Element) priv.appendChild(doc.createElement(XML_CONTAINS)); + contains.setAttribute(ATTR_NAME, getQualifiedName(aggrName, uriToPrefix)); + } + } + + Transformer transformer = TRANSFORMER_FACTORY.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.STANDALONE, "no"); + transformer.transform(new DOMSource(doc), result); + + } catch (Exception e) { + IOException io = new IOException(e.getMessage()); + io.initCause(e); + throw io; + } + } + + //-------------------------------------------------------------------------- + /** + * Build a new PrivilegeDefinition from the given XML node. + * @param n + * @param namespaces + * @return + */ + private PrivilegeDefinition parseDefinition(Node n, Map namespaces) { + if (n.getNodeType() == Node.ELEMENT_NODE) { + Element elem = (Element) n; + + updateNamespaceMapping(elem, namespaces); + + Name name = getName(elem.getAttribute(ATTR_NAME), namespaces); + 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(getName(aggrName, namespaces)); + } + } + } + return new PrivilegeDefinitionImpl(name, isAbstract, aggrNames); + } + + // could not parse into privilege definition + return null; + } + + /** + * Create a new DocumentBuilder + * + * @return a new DocumentBuilder + * @throws ParserConfigurationException + */ + private static DocumentBuilder createDocumentBuilder() throws ParserConfigurationException { + DocumentBuilder builder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); + builder.setErrorHandler(new DefaultHandler()); + return builder; + } + + /** + * Update the specified namespace mappings with the namespace declarations + * defined by the given XML element. + * + * @param elem + * @param namespaces + */ + private static void updateNamespaceMapping(Element elem, Map namespaces) { + 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(); + namespaces.put(prefix, uri); + } + } + } + + /** + * Returns true if the given XML node is an element. + * + * @param n + * @return true if the given XML node is an element; false otherwise. + */ + private static boolean isElement(Node n) { + return n.getNodeType() == Node.ELEMENT_NODE; + } + + private Name getName(String jcrName, Map namespaces) { + String prefix = Text.getNamespacePrefix(jcrName); + String uri = (Name.NS_EMPTY_PREFIX.equals(prefix)) ? Name.NS_DEFAULT_URI : namespaces.get(prefix); + return NAME_FACTORY.create(uri, Text.getLocalName(jcrName)); + } + + private String getQualifiedName(Name name, Map uriToPrefix) { + String uri = name.getNamespaceURI(); + String prefix = (Name.NS_DEFAULT_URI.equals(uri)) ? Name.NS_EMPTY_PREFIX : uriToPrefix.get(uri); + return prefix + ":" + name.getLocalName(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/package-info.java new file mode 100644 index 00000000000..560c0f3a74e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/privilege/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.privilege; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/AndQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/AndQueryNode.java new file mode 100644 index 00000000000..abeba888945 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/AndQueryNode.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +/** + * Implements a query node that defines an AND operation between arbitrary + * other {@link QueryNode}s. + */ +public class AndQueryNode extends NAryQueryNode { + + /** + * Creates a new AndQueryNode with a parent + * query node. + * + * @param parent the parent of this AndQueryNode. + */ + protected AndQueryNode(QueryNode parent) { + super(parent); + } + + /** + * This method can return null to indicate that this + * AndQueryNode does not contain any operands. + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_AND; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof AndQueryNode) { + return super.equals(obj); + } + return false; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/ConstantNameProvider.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/ConstantNameProvider.java new file mode 100644 index 00000000000..b30cc4b5845 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/ConstantNameProvider.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.spi.commons.query; + +/** + * Provides descriptive names for integer constants + */ +public interface ConstantNameProvider { + + /** + * Returns a descriptive name for the given constant. + * @param constant A integer constant + * @return A descriptive name + */ + public String getName(int constant); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DefaultQueryNodeFactory.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DefaultQueryNodeFactory.java new file mode 100644 index 00000000000..1acd5bf1a8b --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DefaultQueryNodeFactory.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.spi.commons.query; + +import java.util.Collection; + +import org.apache.jackrabbit.spi.Name; + +/** + * Default implementation of a {@link QueryNodeFactory}. + */ +public class DefaultQueryNodeFactory implements QueryNodeFactory { + + /** + * Valid node type names under /jcr:system + */ + private final Collection validJcrSystemNodeTypeNames; + + /** + * Creates a DefaultQueryNodeFactory with the given node types under + * /jcr:system . + */ + public DefaultQueryNodeFactory( + Collection validJcrSystemNodeTypeNames) { + this.validJcrSystemNodeTypeNames = validJcrSystemNodeTypeNames; + } + + /** + * {@inheritDoc} + */ + public NodeTypeQueryNode createNodeTypeQueryNode(QueryNode parent, + Name nodeType) { + return new NodeTypeQueryNode(parent, nodeType); + } + + /** + * {@inheritDoc} + */ + public AndQueryNode createAndQueryNode(QueryNode parent) { + return new AndQueryNode(parent); + } + + /** + * {@inheritDoc} + */ + public LocationStepQueryNode createLocationStepQueryNode(QueryNode parent) { + return new LocationStepQueryNode(parent); + } + + /** + * {@inheritDoc} + */ + public DerefQueryNode createDerefQueryNode(QueryNode parent, + Name nameTest, + boolean descendants) { + return new DerefQueryNode(parent, nameTest, descendants); + } + + /** + * {@inheritDoc} + */ + public NotQueryNode createNotQueryNode(QueryNode parent) { + return new NotQueryNode(parent); + } + + /** + * {@inheritDoc} + */ + public OrQueryNode createOrQueryNode(QueryNode parent) { + return new OrQueryNode(parent); + } + + /** + * {@inheritDoc} + */ + public RelationQueryNode createRelationQueryNode(QueryNode parent, + int operation) { + return new RelationQueryNode(parent, operation, this); + } + + /** + * {@inheritDoc} + */ + public PathQueryNode createPathQueryNode(QueryNode parent) { + return new PathQueryNode(parent, validJcrSystemNodeTypeNames); + } + + /** + * {@inheritDoc} + */ + public OrderQueryNode createOrderQueryNode(QueryNode parent) { + return new OrderQueryNode(parent); + } + + /** + * {@inheritDoc} + */ + public PropertyFunctionQueryNode createPropertyFunctionQueryNode( + QueryNode parent, String functionName) { + return new PropertyFunctionQueryNode(parent, functionName); + } + + /** + * {@inheritDoc} + */ + public QueryRootNode createQueryRootNode() { + return new QueryRootNode(); + } + + /** + * {@inheritDoc} + */ + public TextsearchQueryNode createTextsearchQueryNode(QueryNode parent, + String query) { + return new TextsearchQueryNode(parent, query); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DefaultQueryNodeVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DefaultQueryNodeVisitor.java new file mode 100644 index 00000000000..bcc36d7a9e8 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DefaultQueryNodeVisitor.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +/** + * Implements the QueryNodeVisitor interface with default behaviour. + * All methods are no-ops and return the data argument. + */ +public class DefaultQueryNodeVisitor implements QueryNodeVisitor { + + public Object visit(QueryRootNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(OrQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(AndQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(NotQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(ExactQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(NodeTypeQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(TextsearchQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(PathQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(LocationStepQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(RelationQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(OrderQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(DerefQueryNode node, Object data) throws RepositoryException { + return data; + } + + public Object visit(PropertyFunctionQueryNode node, Object data) throws RepositoryException { + return data; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DerefQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DerefQueryNode.java new file mode 100644 index 00000000000..ffdf5cc36d4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/DerefQueryNode.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; + +/** + * Represents query node that dereferences a reference property into a node and + * does an optional name test on the target node. + */ +public class DerefQueryNode extends LocationStepQueryNode { + + /** The name of the reference property */ + private Name refProperty; + + /** + * Creates a new DerefQueryNode without a name set for the + * reference property. + * @param parent the parent query node. + * @param nameTest the name test on the target node, or null + * if no name test should be performed on the target node. + * @param descendants if true this location step uses the + * descendant-or-self axis; otherwise the child axis. + */ + protected DerefQueryNode(QueryNode parent, Name nameTest, boolean descendants) { + super(parent); + setNameTest(nameTest); + setIncludeDescendants(descendants); + } + + /** + * Sets a new name for the reference property. + * @param propertyName the name of the reference property. + */ + public void setRefProperty(Name propertyName) { + refProperty = propertyName; + } + + /** + * Returns the name of the reference property or null if + * none is set. + * @return the name of the reference property or null if + * none is set. + */ + public Name getRefProperty() { + return refProperty; + } + + /** + * {@inheritDoc} + */ + public int getType() { + return TYPE_DEREF; + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof DerefQueryNode) { + DerefQueryNode other = (DerefQueryNode) obj; + return super.equals(obj) + && refProperty == null ? other.refProperty == null : refProperty.equals(other.refProperty); + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean needsSystemTree() { + // Always return true since we don't know if the referenced nodes path + // is a child of /jcr:system + return true; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/ExactQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/ExactQueryNode.java new file mode 100644 index 00000000000..066ba0aef1c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/ExactQueryNode.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; + +/** + * Implements a query node that defines an exact match of a property and a + * value. + */ +public class ExactQueryNode extends QueryNode { + + /** + * The name of the property to match + */ + private final Name property; + + /** + * The value of the property to match + */ + private final Name value; + + /** + * Creates a new ExactQueryNode instance. + * + * @param parent the parent node for this ExactQueryNode. + * @param property the name of the property to match. + * @param value the value of the property to match. + */ + public ExactQueryNode(QueryNode parent, Name property, Name value) { + super(parent); + if (parent == null) { + throw new NullPointerException("parent"); + } + this.property = property; + this.value = value; + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * {@inheritDoc} + */ + public int getType() { + return QueryNode.TYPE_EXACT; + } + + /** + * Returns the name of the property to match. + * + * @return the name of the property to match. + */ + public Name getPropertyName() { + return property; + } + + /** + * Returns the value of the property to match. + * + * @return the value of the property to match. + */ + public Name getValue() { + return value; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof ExactQueryNode) { + ExactQueryNode other = (ExactQueryNode) obj; + return (value == null ? other.value == null : value.equals(other.value)) + && (property == null ? other.property == null : property.equals(other.property)); + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean needsSystemTree() { + return false; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/LocationStepQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/LocationStepQueryNode.java new file mode 100644 index 00000000000..4d3429978a5 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/LocationStepQueryNode.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; + +/** + * Defines a location step for querying the path of a node. + *

        + * + * /foo -> descendants = false, nameTest = foo
        + * //foo -> descendants = true, nameTest = foo
        + * //* -> descendants = true, nameTest = null
        + * /* -> descendants = false, nameTest = null
        + * / -> descendants = false, nameTest = "" + *
        + */ +public class LocationStepQueryNode extends NAryQueryNode { + + /** Constant value for position index = last() */ + public static final int LAST = Integer.MIN_VALUE; + + /** Constant value to indicate no position index */ + public static final int NONE = Integer.MIN_VALUE + 1; + + /** + * The empty name used in matching the root node. This is an implementation + * specific constant as the empty name is not a valid JCR name. + * TODO: The root location step should be refactored somehow + */ + public static final Name EMPTY_NAME = NameFactoryImpl.getInstance().create("", ""); + + /** Empty QueryNode array for us as return value */ + private static final QueryNode[] EMPTY = new QueryNode[0]; + + /** + * Name test for this location step. A null value indicates + * a '*' name test. + */ + private Name nameTest; + + /** + * If set to true this location step uses the descendant-or-self + * axis. + */ + private boolean includeDescendants; + + /** + * The context position index. Initially {@link #NONE}. + */ + private int index = NONE; + + /** + * Creates a new LocationStepQueryNode that matches only the + * empty name (the repository root). The created location step uses only the + * child axis. + * + * @param parent the parent of this query node. + */ + protected LocationStepQueryNode(QueryNode parent) { + super(parent); + this.nameTest = EMPTY_NAME; + this.includeDescendants = false; + } + + /** + * Returns the label of the node for this location step, or null + * if the name test is '*'. + * @return the label of the node for this location step. + */ + public Name getNameTest() { + return nameTest; + } + + /** + * Sets a new name test. + * @param nameTest the name test or null to match all names. + */ + public void setNameTest(Name nameTest) { + this.nameTest = nameTest; + } + + /** + * Returns true if this location step uses the + * descendant-or-self axis, false if this step uses the child + * axis. + * @return true if this step uses the descendant-or-self axis. + */ + public boolean getIncludeDescendants() { + return includeDescendants; + } + + /** + * Sets a new value for the includeDescendants property. + * @param include the new value. + * @see #getIncludeDescendants() + */ + public void setIncludeDescendants(boolean include) { + this.includeDescendants = include; + } + + /** + * Adds a predicate node to this location step. + * @param predicate the node to add. + */ + public void addPredicate(QueryNode predicate) { + addOperand(predicate); + } + + /** + * Returns the predicate nodes for this location step. This method may + * also return a position predicate. + * @return the predicate nodes or an empty array if there are no predicates + * for this location step. + */ + public QueryNode[] getPredicates() { + if (operands == null) { + return EMPTY; + } else { + return operands.toArray(new QueryNode[operands.size()]); + } + } + + /** + * Sets the position index for this step. A value of {@link #NONE} indicates + * that this location step has no position index assigned. That is, the + * step selects all same name siblings. + * @param index the position index. + */ + public void setIndex(int index) { + this.index = index; + } + + /** + * Returns the position index for this step. A value of {@link #NONE} indicates + * that this location step has no position index assigned. That is, the + * step selects all same name siblings. + * @return the position index for this step. + */ + public int getIndex() { + return index; + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * {@inheritDoc} + */ + public int getType() { + return QueryNode.TYPE_LOCATION; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof LocationStepQueryNode) { + LocationStepQueryNode other = (LocationStepQueryNode) obj; + return super.equals(other) + && includeDescendants == other.includeDescendants + && index == other.index + && (nameTest == null ? other.nameTest == null : nameTest.equals(other.nameTest)); + } + return false; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NAryQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NAryQueryNode.java new file mode 100644 index 00000000000..23c2654fb81 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NAryQueryNode.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.spi.commons.query; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.RepositoryException; + +/** + * Defines an abstract query node for nodes that have child nodes. + */ +public abstract class NAryQueryNode extends QueryNode { + + /** + * Empty result. + */ + private static final Object[] EMPTY = new Object[0]; + + /** + * The list of operands / children + */ + protected List operands = null; + + /** + * Creates a new NAryQueryNode with a reference to a parent + * {@link QueryNode}. + * + * @param parent the parent node. + */ + public NAryQueryNode(QueryNode parent) { + super(parent); + } + + /** + * Creates a new NAryQueryNode with a reference to a parent + * {@link QueryNode} and initial operands. + * + * @param parent the parent node. + * @param operands child nodes of this NAryQueryNode. + */ + public NAryQueryNode(QueryNode parent, T[] operands) { + super(parent); + if (operands.length > 0) { + this.operands = new ArrayList(); + this.operands.addAll(Arrays.asList(operands)); + } + } + + /** + * Adds a new operand (child node) to this query node. + * + * @param operand the child {@link QueryNode} to add. + */ + public void addOperand(T operand) { + if (operands == null) { + operands = new ArrayList(); + } + operands.add(operand); + } + + /** + * Removes an operand (child node) from this query node. + * + * @param operand the child to remove. + * @return true if the operand was in the list of child nodes + * and has been removed; false if this node does not contain + * operand as a child node. + */ + public boolean removeOperand(T operand) { + if (operands == null) { + return false; + } + // JCR-1650 search the operand without relying on Object#equals(Object) + Iterator it = operands.iterator(); + while (it.hasNext()) { + if (it.next() == operand) { + it.remove(); + return true; + } + } + return false; + } + + /** + * Returns an array of currently set QueryNode operands of this + * QueryNode. Returns an empty array if no operands are set. + * + * @return currently set QueryNode operands. + */ + public QueryNode[] getOperands() { + if (operands == null) { + return new QueryNode[0]; + } else { + return operands.toArray(new QueryNode[operands.size()]); + } + } + + /** + * Returns the number of operands. + * @return the number of operands. + */ + public int getNumOperands() { + if (operands == null) { + return 0; + } else { + return operands.size(); + } + } + + /** + * Helper class to accept a visitor for all operands + * of this NAryQueryNode. + * + * @param visitor the visitor to call back. + * @param data arbitrary data for the visitor. + * @return the return values of the visitor.visit() calls. + * @throws RepositoryException if an error occurs. + */ + public Object[] acceptOperands(QueryNodeVisitor visitor, Object data) throws RepositoryException { + if (operands == null) { + return EMPTY; + } + + List result = new ArrayList(operands.size()); + for (T operand : operands) { + Object r = operand.accept(visitor, data); + if (r != null) { + result.add(r); + } + } + return result.toArray(); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof NAryQueryNode) { + NAryQueryNode other = (NAryQueryNode) obj; + return operands == null ? other.operands == null : operands.equals(other.operands); + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean needsSystemTree() { + if (operands == null) { + return false; + } + for (T operand : operands) { + if (operand.needsSystemTree()) { + return true; + } + } + return false; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NodeTypeQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NodeTypeQueryNode.java new file mode 100644 index 00000000000..1193d078c7e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NodeTypeQueryNode.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * Implements a query node that defines a node type match. + */ +public class NodeTypeQueryNode extends ExactQueryNode { + + /** + * Creates a new NodeTypeQueryNode. + * + * @param parent the parent node for this query node. + * @param nodeType the name of the node type. + */ + protected NodeTypeQueryNode(QueryNode parent, Name nodeType) { + // we only use the jcr primary type as a dummy value + // the property name is actually replaced in the query builder + // when the runtime query is created to search the index. + super(parent, NameConstants.JCR_PRIMARYTYPE, nodeType); + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_NODETYPE; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof NodeTypeQueryNode) { + return super.equals(obj); + } + return false; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NotQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NotQueryNode.java new file mode 100644 index 00000000000..ef638a6a106 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/NotQueryNode.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +/** + * Implements a query node that defines a not operation on the child query. + */ +public class NotQueryNode extends NAryQueryNode { + + /** + * Creates a new NotQueryNode instance. + * + * @param parent the parent node for this query node. + */ + protected NotQueryNode(QueryNode parent) { + super(parent); + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_NOT; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof NotQueryNode) { + return super.equals(obj); + } + return false; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/OrQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/OrQueryNode.java new file mode 100644 index 00000000000..eea0a9cf102 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/OrQueryNode.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +/** + * Implements a query node that defines an OR operation between arbitrary + * other {@link QueryNode}s. + */ +public class OrQueryNode extends NAryQueryNode { + + /** + * Creates a new OrQueryNode with a parent + * query node. + * + * @param parent the parent of this OrQueryNode. + */ + protected OrQueryNode(QueryNode parent) { + super(parent); + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_OR; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof OrQueryNode) { + return super.equals(obj); + } + return false; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/OrderQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/OrderQueryNode.java new file mode 100644 index 00000000000..f5a1d244073 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/OrderQueryNode.java @@ -0,0 +1,359 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; + +import javax.jcr.RepositoryException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements a query node that defines the order of nodes according to the + * values of properties. + */ +public class OrderQueryNode extends QueryNode { + + /** + * The order specs + */ + private final List specs = new ArrayList(); + + /** + * Creates a new OrderQueryNode with a reference to a parent + * node and sort properties. + * + * @param parent the parent node of this query node. + */ + protected OrderQueryNode(QueryNode parent) { + super(parent); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_ORDER; + } + + /** + * Create and add a new (empty) order specification to this query node. + */ + public void newOrderSpec() { + specs.add(new OrderSpec((Path) null, true)); + } + + /** + * Set the last order specification of this query node to ascending/descending + * @see OrderSpec#setAscending(boolean) + * + * @param value true for ascending and false for + * descending. + * @throws IllegalStateException if no order specification is set + */ + public void setAscending(boolean value) { + if (specs.size() == 0) { + throw new IllegalStateException("No order specification set"); + } + + OrderSpec orderSpec = specs.get(specs.size() - 1); + orderSpec.setAscending(value); + } + + /** + * Set the path of the last order specification of this query node. + * @see OrderSpec#setPath(org.apache.jackrabbit.spi.Path) + * + * @param path a path + * @throws IllegalStateException if no order specification is set + */ + public void setPath(Path path) { + if (specs.size() == 0) { + throw new IllegalStateException("No order specification set"); + } + + OrderSpec orderSpec = specs.get(specs.size() - 1); + orderSpec.setPath(path); + } + + /** + * Set the function of the last order specification of this query node. + * @see OrderSpec#setFunction(String) + * + * @param name a function name + * @throws IllegalStateException if no order specification is set + */ + public void setFunction(String name) { + if (specs.size() == 0) { + throw new IllegalStateException("No order specification set"); + } + + OrderSpec orderSpec = specs.get(specs.size() - 1); + orderSpec.setFunction(name); + } + + /** + * Checks whether all order specifications of this query node have at least + * its path specified (i.e. non null.) + * + * @return true iff all order specification of this query node are valid. + */ + public boolean isValid() { + for (OrderSpec spec : specs) { + if (spec.getPropertyPath() == null) { + return false; + } + } + + return true; + } + + /** + * Adds an order specification to this query node. + * + * @param property the name of the property. + * @param ascending if true values of this properties are + * ordered ascending; descending if false. + * @deprecated use {@link #addOrderSpec(Path , boolean)} instead. + */ + public void addOrderSpec(Name property, boolean ascending) { + addOrderSpec(createPath(property), ascending); + } + + /** + * Adds an order specification to this query node. + * + * @param property the relative path of the property. + * @param ascending if true values of this properties are + * ordered ascending; descending if false. + */ + public void addOrderSpec(Path property, boolean ascending) { + specs.add(new OrderSpec(property, ascending)); + } + + /** + * Adds an order specification to this query node. + * + * @param spec the order spec. + */ + public void addOrderSpec(OrderSpec spec) { + specs.add(spec); + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns true if the property i should be ordered + * ascending. If false the property is ordered descending. + * + * @param i index of the property + * @return the order spec for the property i. + * @throws IndexOutOfBoundsException if there is no property with + * index i. + */ + public boolean isAscending(int i) throws IndexOutOfBoundsException { + return specs.get(i).ascending; + } + + /** + * Returns a OrderSpec array that contains order by + * specifications. + * + * @return order by specs. + */ + public OrderSpec[] getOrderSpecs() { + return specs.toArray(new OrderSpec[specs.size()]); + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof OrderQueryNode) { + OrderQueryNode other = (OrderQueryNode) obj; + return specs.equals(other.specs); + } + return false; + } + + //------------------< OrderSpec class >------------------------------------- + + /** + * Implements a single order specification. Contains a property name + * and whether it is ordered ascending or descending. + */ + public static final class OrderSpec { + + /** + * The relative path to of the property + */ + private Path property; + + /** + * If true this property is ordered ascending + */ + private boolean ascending; + + /** + * The function applied to the property + */ + private String function; + + /** + * Creates a new OrderSpec for property. + * + * @param property the name of the property. + * @param ascending if true the property is ordered + * ascending, otherwise descending. + * @deprecated use {@link OrderSpec#OrderSpec(Path, boolean)} instead. + */ + public OrderSpec(Name property, boolean ascending) { + this(createPath(property), ascending); + } + + /** + * Creates a new OrderSpec for property. + * + * @param property the relative path of the property. + * @param ascending if true the property is ordered + * ascending, otherwise descending. + */ + public OrderSpec(Path property, boolean ascending) { + this.property = property; + this.ascending = ascending; + } + + /** + * Returns the name of the property. + * + * @return the name of the property. + * @deprecated use {@link #getPropertyPath()} instead. + */ + public Name getProperty() { + return property.getName(); + } + + /** + * Returns the relative path of the property. + * + * @return the relative path of the property. + */ + public Path getPropertyPath() { + return property; + } + + /** + * If true the property is ordered ascending, otherwise + * descending. + * + * @return true for ascending; false for + * descending. + */ + public boolean isAscending() { + return ascending; + } + + /** + * Sets the new value for the ascending property. + * + * @param ascending true for ascending; false + * for descending. + */ + public void setAscending(boolean ascending) { + this.ascending = ascending; + } + + /** + * Set a new value for the path + * + * @param path a path + */ + public void setPath(Path path) { + this.property = path; + } + + /** + * Set a new value for a function + * + * @param name a function name + */ + public void setFunction(String name) { + this.function = name; + } + + /** + * @return name of the function + */ + public String getFunction() { + return function; + } + + /** + * Returns true if this order spec is equal + * to obj + * @param obj the reference object with which to compare. + * @return true if this order spec is equal + * to obj; false otherwise. + */ + public boolean equals(Object obj) { + if (obj instanceof OrderSpec) { + OrderSpec other = (OrderSpec) obj; + return (property == null ? other.property == null : property.equals(other.property)) + && ascending == other.ascending; + } + return false; + } + + } + + /** + * {@inheritDoc} + */ + public boolean needsSystemTree() { + return false; + } + + //--------------------------------< internal >------------------------------ + + /** + * Creates a path with a single element out of the given name. + * + * @param name the name to create the path from. + * @return a path with a single element. + */ + private static Path createPath(Name name) { + try { + PathBuilder builder = new PathBuilder(); + builder.addLast(name); + return builder.getPath(); + } catch (MalformedPathException e) { + // never happens, we just added an element + throw new InternalError(); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/PathQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/PathQueryNode.java new file mode 100644 index 00000000000..6305a3ead08 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/PathQueryNode.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.spi.commons.query; + +import java.util.Collection; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * Implements a query node that defines a path restriction. + */ +public class PathQueryNode extends NAryQueryNode { + + /** + * Flag indicating whether this path is absolute. + */ + private boolean absolute = false; + + /** + * Valid node type names under /jcr:system. Used to determine if a + * query needs to be executed also against the /jcr:system tree. + */ + private final Collection validJcrSystemNodeTypeNames; + + /** + * Empty step node array. + */ + private static final LocationStepQueryNode[] EMPTY = new LocationStepQueryNode[0]; + + /** + * Creates a relative PathQueryNode with no location steps and + * the collection of node types under /jcr:system. + * + * @param parent the parent query node. + * @param validJcrSystemNodeTypeNames valid node types under /jcr:system + */ + protected PathQueryNode( + QueryNode parent, Collection validJcrSystemNodeTypeNames) { + super(parent); + this.validJcrSystemNodeTypeNames = validJcrSystemNodeTypeNames; + } + + /** + * Returns the collection of valid node types under /jcr:system. + * + * @return valid node types under /jcr:system. + */ + public Collection getValidJcrSystemNodeTypeNames() { + return validJcrSystemNodeTypeNames; + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_PATH; + } + + /** + * Adds a path step to this PathQueryNode. + * + * @param step the step to add. + */ + public void addPathStep(LocationStepQueryNode step) { + addOperand(step); + } + + /** + * Returns an array of all currently set location step nodes. + * + * @return an array of all currently set location step nodes. + */ + public LocationStepQueryNode[] getPathSteps() { + if (operands == null) { + return EMPTY; + } else { + return operands.toArray(new LocationStepQueryNode[operands.size()]); + } + } + + /** + * If absolute is true sets this + * PathQueryNode to an absolute path. If absolute + * is false this path is considered relative. + * + * @param absolute sets the absolute property to this new value. + */ + public void setAbsolute(boolean absolute) { + this.absolute = absolute; + } + + /** + * Returns true if this is an absolute path; false + * otherwise. + * + * @return true if this is an absolute path; false + * otherwise. + */ + public boolean isAbsolute() { + return absolute; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof PathQueryNode) { + PathQueryNode other = (PathQueryNode) obj; + return super.equals(obj) && absolute == other.absolute; + } + return false; + } + + + /** + * {@inheritDoc} + */ + public boolean needsSystemTree() { + + LocationStepQueryNode[] pathSteps = getPathSteps(); + if (pathSteps == null || pathSteps.length == 0) { + return true; + } + + Name firstPathStepName = pathSteps[0].getNameTest(); + if (firstPathStepName == null) { + // If the first operand of the path steps is a node type query + // we do not need to include the system index if the node type is + // none of the node types that may occur in the system index. + QueryNode[] pathStepOperands = pathSteps[0].getOperands(); + if (pathStepOperands.length > 0) { + if (pathStepOperands[0] instanceof NodeTypeQueryNode) { + NodeTypeQueryNode nodeTypeQueryNode = (NodeTypeQueryNode) pathStepOperands[0]; + if (!validJcrSystemNodeTypeNames.contains(nodeTypeQueryNode.getValue())) { + return false; + } + } + } + // If the first location step has a null name test we need to include + // the system tree ("*") + return true; + } + + // Calculate the first workspace relative location step + LocationStepQueryNode firstWorkspaceRelativeStep = pathSteps[0]; + if (firstPathStepName.equals(NameConstants.ROOT)) { + // path starts with "/jcr:root" + if (pathSteps.length > 1) { + firstWorkspaceRelativeStep = pathSteps[1]; + } + } + + // First path step starts with "//" + if (firstWorkspaceRelativeStep.getIncludeDescendants()) { + return true; + } + + // If the first workspace relative location step is jcr:system we need + // to include the system tree + Name firstWorkspaceRelativeName = firstWorkspaceRelativeStep.getNameTest(); + if (firstWorkspaceRelativeName == null + || firstWorkspaceRelativeName.equals(NameConstants.JCR_SYSTEM)) { + return true; + } + + return super.needsSystemTree(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/PropertyFunctionQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/PropertyFunctionQueryNode.java new file mode 100644 index 00000000000..6b66487dbeb --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/PropertyFunctionQueryNode.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.spi.commons.query; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.RepositoryException; + +/** + * PropertyFunctionQueryNode allows to place function calls on properties + * in a query. Supported function names are: + * + */ +public class PropertyFunctionQueryNode extends QueryNode { + + /** + * Requests that property values in a {@link RelationQueryNode} are + * converted to upper case before they are matched with the literal. + */ + public static final String UPPER_CASE = "upper-case"; + + /** + * Requests that property values in a {@link RelationQueryNode} are + * converted to lower case before they are matched with the literal. + */ + public static final String LOWER_CASE = "lower-case"; + + /** + * The set of supported function names. + */ + private static final Set SUPPORTED_FUNCTION_NAMES; + + static { + Set tmp = new HashSet(); + tmp.add(UPPER_CASE); + tmp.add(LOWER_CASE); + SUPPORTED_FUNCTION_NAMES = Collections.unmodifiableSet(tmp); + } + + /** + * The function name. + */ + private final String functionName; + + /** + * Creates a property function query node. This query node describes a + * function which is applied to a property parameter of the + * parent query node. + * + * @param parent the query node where this function is applied to. + * @param functionName the name of the function which is applied to + * parent. + * @throws IllegalArgumentException if functionName is not a + * supported function. + */ + protected PropertyFunctionQueryNode(QueryNode parent, String functionName) + throws IllegalArgumentException { + super(parent); + if (!SUPPORTED_FUNCTION_NAMES.contains(functionName)) { + throw new IllegalArgumentException("unknown function name"); + } + this.functionName = functionName; + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_PROP_FUNCTION; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof PropertyFunctionQueryNode) { + PropertyFunctionQueryNode other = (PropertyFunctionQueryNode) obj; + return functionName.equals(other.functionName); + } + return false; + } + + /** + * @return the name of this function. + */ + public String getFunctionName() { + return functionName; + } + + /** + * {@inheritDoc} + */ + public boolean needsSystemTree() { + return false; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryConstants.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryConstants.java new file mode 100644 index 00000000000..fcfc023ae4a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryConstants.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.spi.commons.query; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * This interface defines constants for data types and operation types + * used in queries. + */ +public interface QueryConstants { + + /** + * long data type + */ + int TYPE_LONG = 1; + + /** + * Name of long data type + */ + final String TYPE_NAME_LONG = "LONG"; + + /** + * double data type + */ + int TYPE_DOUBLE = 2; + + /** + * Name of double data type + */ + final String TYPE_NAME_DOUBLE = "DOUBLE"; + + /** + * string data type + */ + int TYPE_STRING = 3; + + /** + * Name of string data type + */ + final String TYPE_NAME_STRING = "STRING"; + + /** + * date data type + */ + int TYPE_DATE = 4; + + /** + * Name of date data type + */ + final String TYPE_NAME_DATE = "DATE"; + + /** + * timestamp data type + */ + int TYPE_TIMESTAMP = 5; + + /** + * Name of timestamp data type + */ + final String TYPE_NAME_TIMESTAMP = "TIMESTAMP"; + + /** + * position index type + */ + int TYPE_POSITION = 6; + + /** + * Name of position index + */ + final String TYPE_NAME_POSITION = "POS"; + + /** + * Name for unknown data types + */ + final String TYPE_NAME_UNKNOWN = "UNKNOWN TYPE"; + + + int OPERATIONS = 10; + + /** + * equal operation: eq + */ + int OPERATION_EQ_VALUE = OPERATIONS + 1; + + /** + * Name of equal operation + */ + final String OP_NAME_EQ_VALUE = "eq"; + + /** + * equal operation: = + * general comparison + */ + int OPERATION_EQ_GENERAL = OPERATION_EQ_VALUE + 1; + + /** + * Name of equal operation (general comparison) + */ + final String OP_NAME_EQ_GENERAL = "="; + + /** + * not equal operation: ne + */ + int OPERATION_NE_VALUE = OPERATION_EQ_GENERAL + 1; + + /** + * Name of not equal operation + */ + final String OP_NAME_NE_VALUE = "ne"; + + /** + * not equal operation: <> + * general comparison + */ + int OPERATION_NE_GENERAL = OPERATION_NE_VALUE + 1; + + /** + * Name of not equal operation (general comparison) + */ + final String OP_NAME_NE_GENERAL = "<>"; + + /** + * less than operation: lt + */ + int OPERATION_LT_VALUE = OPERATION_NE_GENERAL + 1; + + /** + * Name of less than operation + */ + final String OP_NAME_LT_VALUE = "lt"; + + /** + * less than operation: < + * general comparison + */ + int OPERATION_LT_GENERAL = OPERATION_LT_VALUE + 1; + + /** + * Name of less than operation (general comparison) + */ + final String OP_NAME_LT_GENERAL = "<"; + + /** + * greater than operation: gt + */ + int OPERATION_GT_VALUE = OPERATION_LT_GENERAL + 1; + + /** + * Name o^f greater than operation + */ + final String OP_NAME_GT_VALUE = "gt"; + + /** + * greater than operation: > + * general comparison + */ + int OPERATION_GT_GENERAL = OPERATION_GT_VALUE + 1; + + /** + * Name of greater than operation (general comparison) + */ + final String OP_NAME_GT_GENERAL = ">"; + + /** + * greater or equal operation: ge + */ + int OPERATION_GE_VALUE = OPERATION_GT_GENERAL + 1; + + /** + * Name of greater or equal operation + */ + final String OP_NAME_GE_VALUE = "ge"; + + /** + * greater or equal operation: >= + * general comparison + */ + int OPERATION_GE_GENERAL = OPERATION_GE_VALUE + 1; + + /** + * Name of greater or equal operation (general comparison) + */ + final String OP_NAME_GE_GENERAL = ">="; + + /** + * less than or equal operation: le + */ + int OPERATION_LE_VALUE = OPERATION_GE_GENERAL + 1; + + /** + * Name of less than or equal operation + */ + final String OP_NAME_LE_VALUE = "le"; + + /** + * less than or equal operation: <= + * general comparison + */ + int OPERATION_LE_GENERAL = OPERATION_LE_VALUE + 1; + + /** + * Name of less than or equal operation (general comparison) + */ + final String OP_NAME_LE_GENERAL = "<="; + + /** + * like operation: identifier LIKE string_literal + */ + int OPERATION_LIKE = OPERATION_LE_GENERAL + 1; + + /** + * Name of like operation + */ + final String OP_NAME_LIKE = "LIKE"; + + /** + * between operation: identifier [ NOT ] BETWEEN literal AND literal + */ + int OPERATION_BETWEEN = OPERATION_LIKE + 1; + + /** + * Name of between operation + */ + final String OP_NAME_BETWEEN = "BETWEEN"; + + /** + * in operation: identifier [ NOT ] IN ( literal {, literal}* ) + */ + int OPERATION_IN = OPERATION_BETWEEN + 1; + + /** + * Name of in operation + */ + final String OP_NAME_IN = "IN"; + + /** + * is null operation: identifier IS NULL + */ + int OPERATION_NULL = OPERATION_IN + 1; + + /** + * Name of is null operation + */ + final String OP_NAME_NULL = "IS NULL"; + + /** + * is not null operation: identifier IS NOT NULL + */ + int OPERATION_NOT_NULL = OPERATION_NULL + 1; + + /** + * Name of is not null operation + */ + final String OP_NAME_NOT_NULL = "NOT NULL"; + + /** + * similar operation: + * XPath: rep:similar(path_string) + * SQL: SIMILAR(path_string) + */ + int OPERATION_SIMILAR = OPERATION_NOT_NULL + 1; + + /** + * Name of similar operation + */ + final String OP_NAME_SIMILAR = "similarity"; + + /** + * spellcheck operation: + * XPath: rep:spellcheck(string_literal) + * SQL: SPELLCHECK(string_literal) + */ + int OPERATION_SPELLCHECK = OPERATION_SIMILAR + 1; + + /** + * Name of spellcheck operation + */ + final String OP_NAME_SPELLCHECK = "spellcheck"; + + /** + * Name of unknown operations + */ + final String OP_NAME_UNKNOW = "UNKNOWN OPERATION"; + + /** + * Operation names + */ + final ConstantNameProvider OPERATION_NAMES = new ConstantNameProvider() { + private final Map operationNames; + + { + Map map = new HashMap(); + map.put(new Integer(OPERATION_BETWEEN), OP_NAME_BETWEEN); + map.put(new Integer(OPERATION_EQ_VALUE), OP_NAME_EQ_VALUE); + map.put(new Integer(OPERATION_EQ_GENERAL), OP_NAME_EQ_GENERAL); + map.put(new Integer(OPERATION_GE_GENERAL), OP_NAME_GE_GENERAL); + map.put(new Integer(OPERATION_GE_VALUE), OP_NAME_GE_VALUE); + map.put(new Integer(OPERATION_GT_GENERAL), OP_NAME_GT_GENERAL); + map.put(new Integer(OPERATION_GT_VALUE), OP_NAME_GT_VALUE); + map.put(new Integer(OPERATION_IN), OP_NAME_IN); + map.put(new Integer(OPERATION_LE_GENERAL), OP_NAME_LE_GENERAL); + map.put(new Integer(OPERATION_LE_VALUE), OP_NAME_LE_VALUE); + map.put(new Integer(OPERATION_LIKE), OP_NAME_LIKE); + map.put(new Integer(OPERATION_LT_GENERAL), OP_NAME_LT_GENERAL); + map.put(new Integer(OPERATION_LT_VALUE), OP_NAME_LT_VALUE); + map.put(new Integer(OPERATION_NE_GENERAL), OP_NAME_NE_GENERAL); + map.put(new Integer(OPERATION_NE_VALUE), OP_NAME_NE_VALUE); + map.put(new Integer(OPERATION_NOT_NULL), OP_NAME_NOT_NULL); + map.put(new Integer(OPERATION_NULL), OP_NAME_NULL); + map.put(new Integer(OPERATION_SIMILAR), OP_NAME_SIMILAR); + map.put(new Integer(OPERATION_SPELLCHECK), OP_NAME_SPELLCHECK); + operationNames = Collections.unmodifiableMap(map); + } + + public String getName(int constant) { + String name = (String) operationNames.get(new Integer(constant)); + return name == null? OP_NAME_UNKNOW : name; + } + }; + + /** + * Type names + */ + final ConstantNameProvider TYPE_NAMES = new ConstantNameProvider() { + private final Map typeNames; + + { + Map map = new HashMap(); + map.put(new Integer(TYPE_DATE), TYPE_NAME_DATE); + map.put(new Integer(TYPE_DOUBLE), TYPE_NAME_DOUBLE); + map.put(new Integer(TYPE_LONG), TYPE_NAME_LONG); + map.put(new Integer(TYPE_POSITION), TYPE_NAME_POSITION); + map.put(new Integer(TYPE_STRING), TYPE_NAME_STRING); + map.put(new Integer(TYPE_TIMESTAMP), TYPE_NAME_TIMESTAMP); + typeNames = Collections.unmodifiableMap(map); + } + + public String getName(int constant) { + String name = (String) typeNames.get(new Integer(constant)); + return name == null? TYPE_NAME_UNKNOWN : name; + } + }; + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNode.java new file mode 100644 index 00000000000..2bc86c3cf8b --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNode.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +/** + * Implements an abstract base class for nodes of a query tree that represents + * a query. The query tree is independent from the query syntax which is used + * to search the repository. + */ +public abstract class QueryNode { + + /** Type value for {@link QueryRootNode} */ + public static final int TYPE_ROOT = 1; + + /** Type value for {@link RelationQueryNode} */ + public static final int TYPE_RELATION = 2; + + /** Type value for {@link OrderQueryNode} */ + public static final int TYPE_ORDER = 3; + + /** Type value for {@link TextsearchQueryNode} */ + public static final int TYPE_TEXTSEARCH = 4; + + /** Type value for {@link ExactQueryNode} */ + public static final int TYPE_EXACT = 5; + + /** Type value for {@link NodeTypeQueryNode} */ + public static final int TYPE_NODETYPE = 6; + + /** Type value for {@link AndQueryNode} */ + public static final int TYPE_AND = 7; + + /** Type value for {@link OrQueryNode} */ + public static final int TYPE_OR = 8; + + /** Type value for {@link NotQueryNode} */ + public static final int TYPE_NOT = 9; + + /** Type value for {@link LocationStepQueryNode} */ + public static final int TYPE_LOCATION = 10; + + /** Type value for {@link PathQueryNode} */ + public static final int TYPE_PATH = 11; + + /** Type value for {@link DerefQueryNode} */ + public static final int TYPE_DEREF = 12; + + /** Type value for {@link PropertyFunctionQueryNode} */ + public static final int TYPE_PROP_FUNCTION = 13; + + /** + * References the parent of this QueryNode. If this is the root + * of a query tree, then parent is null. + */ + private final QueryNode parent; + + /** + * Constructs a new QueryNode with a reference to it's parent. + * + * @param parent the parent node, or null if this is the root + * node of a query tree. + */ + public QueryNode(QueryNode parent) { + this.parent = parent; + } + + /** + * Returns the parent QueryNode or null if this is + * the root node of a query tree. + * + * @return the parent QueryNode or null if this is + * the root node of a query tree. + */ + public QueryNode getParent() { + return parent; + } + + /** + * Dumps this QueryNode and its child nodes to a String. + * @return the query tree as a String. + * @throws RepositoryException + */ + public String dump() throws RepositoryException { + StringBuffer tmp = new StringBuffer(); + QueryTreeDump.dump(this, tmp); + return tmp.toString(); + } + + /** + * Accepts a {@link QueryNodeVisitor} and calls the appropriate visit + * method on the visitor depending on the concrete implementation of + * this QueryNode. + * + * @param visitor the visitor to call back. + * @param data arbitrary data for the visitor. + * @return the return value of the visitor.visit() call. + * @throws RepositoryException + */ + public abstract Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException; + + /** + * Returns the type of this query node. + * @return the type of this query node. + */ + public abstract int getType(); + + /** + * Returns true if obj is the same type of + * QueryNode as this node and is equal to + * this node. + * @param obj the reference object with which to compare. + * @return true if obj is equal to + * this; false otherwise. + */ + public abstract boolean equals(Object obj); + + /** + * Returns true if this query node needs items under + * /jcr:system to be queried. + * + * @return true if this query node needs content under + * /jcr:system to be queried; false otherwise. + */ + public abstract boolean needsSystemTree(); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNodeFactory.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNodeFactory.java new file mode 100644 index 00000000000..b860a88a974 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNodeFactory.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.spi.commons.query; + +import org.apache.jackrabbit.spi.Name; + +/** + * A factory for {@link QueryNode}s. + */ +public interface QueryNodeFactory { + + /** + * Creates a {@link NodeTypeQueryNode} instance. + * + * @param parent the parent node. + * @param nodeType the name of the node type. + * @return a {@link NodeTypeQueryNode}. + */ + NodeTypeQueryNode createNodeTypeQueryNode(QueryNode parent, Name nodeType); + + /** + * Creates a {@link AndQueryNode} instance. + * + * @param parent the parent node. + * @return a {@link AndQueryNode}. + */ + AndQueryNode createAndQueryNode(QueryNode parent); + + /** + * Creates a {@link LocationStepQueryNode} instance. + * + * @param parent the parent node. + * @return a {@link LocationStepQueryNode}. + */ + LocationStepQueryNode createLocationStepQueryNode(QueryNode parent); + + /** + * Creates a {@link DerefQueryNode} instance. + * + * @param parent the parent node. + * @param nameTest the name test on the referenced target node. + * @param descendants if the axis is // + * @return a {@link DerefQueryNode}. + */ + DerefQueryNode createDerefQueryNode( + QueryNode parent, Name nameTest, boolean descendants); + + /** + * Creates a {@link NotQueryNode} instance. + * + * @param parent the parent node. + * @return a {@link NotQueryNode}. + */ + NotQueryNode createNotQueryNode(QueryNode parent); + + /** + * Creates a {@link OrQueryNode} instance. + * + * @param parent the parent node. + * @return a {@link OrQueryNode}. + */ + OrQueryNode createOrQueryNode(QueryNode parent); + + /** + * Creates a {@link RelationQueryNode} instance. + * + * @param parent the parent node. + * @param operation the operation type. + * @return a {@link RelationQueryNode}. + */ + RelationQueryNode createRelationQueryNode(QueryNode parent, int operation); + + /** + * Creates a {@link PathQueryNode} instance. + * + * @param parent the parent node. + * @return a {@link PathQueryNode}. + */ + PathQueryNode createPathQueryNode(QueryNode parent); + + /** + * Creates a {@link OrderQueryNode} instance. + * + * @param parent the parent node. + * @return a {@link OrderQueryNode}. + */ + OrderQueryNode createOrderQueryNode(QueryNode parent); + + /** + * Creates a {@link PropertyFunctionQueryNode} instance. + * + * @param parent the parent node. + * @param functionName the name of the function. + * @return a {@link PropertyFunctionQueryNode}. + */ + PropertyFunctionQueryNode createPropertyFunctionQueryNode( + QueryNode parent, String functionName); + + /** + * Creates a {@link QueryRootNode} instance. + * + * @return a {@link QueryRootNode}. + */ + QueryRootNode createQueryRootNode(); + + /** + * Creates a {@link TextsearchQueryNode} instance. + * + * @param parent the parent node. + * @param query the textsearch statement. + * @return a {@link TextsearchQueryNode}. + */ + TextsearchQueryNode createTextsearchQueryNode( + QueryNode parent, String query); + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNodeVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNodeVisitor.java new file mode 100644 index 00000000000..e4a481d1983 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryNodeVisitor.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +/** + * Defines the interface for a QueryNodeVisitor. + */ +public interface QueryNodeVisitor { + + Object visit(QueryRootNode node, Object data) throws RepositoryException; + + Object visit(OrQueryNode node, Object data) throws RepositoryException; + + Object visit(AndQueryNode node, Object data) throws RepositoryException; + + Object visit(NotQueryNode node, Object data) throws RepositoryException; + + Object visit(ExactQueryNode node, Object data) throws RepositoryException; + + Object visit(NodeTypeQueryNode node, Object data) throws RepositoryException; + + Object visit(TextsearchQueryNode node, Object data) throws RepositoryException; + + Object visit(PathQueryNode node, Object data) throws RepositoryException; + + Object visit(LocationStepQueryNode node, Object data) throws RepositoryException; + + Object visit(RelationQueryNode node, Object data) throws RepositoryException; + + Object visit(OrderQueryNode node, Object data) throws RepositoryException; + + Object visit(DerefQueryNode node, Object data) throws RepositoryException; + + Object visit(PropertyFunctionQueryNode node, Object data) throws RepositoryException; +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryParser.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryParser.java new file mode 100644 index 00000000000..58b2825cbee --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryParser.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.spi.commons.query; + + +import javax.jcr.query.InvalidQueryException; + +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; + +/** + * This class acts as the central entry point for parsing query statements from + * different query syntaxes into a query tree. + */ +public class QueryParser { + + /** + * This class cannot be instanciated. + */ + private QueryParser() { + } + + /** + * Parses a query statement according to a query + * language into a query tree. + *

        + * language must be one of: {@link javax.jcr.query.Query#SQL}, + * {@link javax.jcr.query.Query#XPATH}. + * + * @param statement the query statement. + * @param language the language of the query statement. + * @param factory the query node factory. + * @return the root node of the generated query tree. + * @throws InvalidQueryException if an error occurs while parsing the + * statement. + */ + public static QueryRootNode parse(String statement, + String language, + NameResolver resolver, + QueryNodeFactory factory) + throws InvalidQueryException { + + QueryTreeBuilder builder = QueryTreeBuilderRegistry.getQueryTreeBuilder(language); + return builder.createQueryTree(statement, resolver, factory); + } + + /** + * Creates a String representation of the QueryNode tree argument + * root. The argument language specifies the + * syntax. + * See also: {@link javax.jcr.query.QueryManager#getSupportedQueryLanguages()}. + * + * @param root the query node tree. + * @param language one of the languages returned by: + * {@link javax.jcr.query.QueryManager#getSupportedQueryLanguages()}. + * @param resolver to resolve QNames. + * + * @return a String representation of the query node tree. + * + * @throws InvalidQueryException if the query node tree cannot be converted + * into a String representation of the given language. This might be due to + * syntax restrictions of the given language. This exception is also thrown + * if language is not one of the supported query languages + * returned by the {@link javax.jcr.query.QueryManager}. + */ + public static String toString(QueryRootNode root, + String language, + NameResolver resolver) + throws InvalidQueryException { + + QueryTreeBuilder builder = QueryTreeBuilderRegistry.getQueryTreeBuilder(language); + return builder.toString(root, resolver); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryRootNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryRootNode.java new file mode 100644 index 00000000000..dfef775e0c3 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryRootNode.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.spi.commons.query; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; + +/** + * Implements the root node of a query tree. + */ +public class QueryRootNode extends QueryNode { + + /** + * The path sub query + */ + private PathQueryNode locationNode; + + /** + * The list of property names (as {@link org.apache.jackrabbit.spi.Name}s + * to select. + */ + private final List selectProperties = new ArrayList(); + + /** + * The list of property names to order the result nodes. Might be null + */ + private OrderQueryNode orderNode; + + /** + * Creates a new QueryRootNode instance. + */ + protected QueryRootNode() { + super(null); + } + + /** + * Returns the {@link PathQueryNode} or null if this query does + * not have a location node. + * + * @return the {@link PathQueryNode} or null if this query does + * not have a location node. + */ + public PathQueryNode getLocationNode() { + return locationNode; + } + + /** + * Sets the location node. + * + * @param locationNode the new location node. + */ + public void setLocationNode(PathQueryNode locationNode) { + this.locationNode = locationNode; + } + + /** + * Adds a new select property to the query. + * + * @param propName the name of the property to select. + */ + public void addSelectProperty(Name propName) { + selectProperties.add(propName); + } + + /** + * Returns an array of select properties. + * + * @return an array of select properties. + */ + public Name[] getSelectProperties() { + return (Name[]) selectProperties.toArray(new Name[selectProperties.size()]); + } + + /** + * Returns the order node or null if no order is specified. + * + * @return the order node. + */ + public OrderQueryNode getOrderNode() { + return orderNode; + } + + /** + * Sets a new order node. + * + * @param orderNode the new order node. + */ + public void setOrderNode(OrderQueryNode orderNode) { + this.orderNode = orderNode; + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_ROOT; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof QueryRootNode) { + QueryRootNode other = (QueryRootNode) obj; + return (locationNode == null ? other.locationNode == null : locationNode.equals(other.locationNode)) + && selectProperties.equals(other.selectProperties) + && (orderNode == null ? other.orderNode == null : orderNode.equals(other.orderNode)); + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean needsSystemTree() { + return (locationNode != null && locationNode.needsSystemTree()) || (orderNode != null && orderNode.needsSystemTree()); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeBuilder.java new file mode 100644 index 00000000000..cc6713a27ed --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeBuilder.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.spi.commons.query; + +import javax.jcr.query.InvalidQueryException; + +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; + +/** + * Specifies an interface for a query tree builder. + */ +public interface QueryTreeBuilder { + + /** + * Creates a QueryNode tree from a statement using the passed + * query node factory. + * + * @param statement the statement. + * @param resolver the name resolver to use. + * @param factory the query node factory to use. + * @return the QueryNode tree for the statement. + * @throws javax.jcr.query.InvalidQueryException + * if the statement is malformed. + */ + QueryRootNode createQueryTree(String statement, + NameResolver resolver, + QueryNodeFactory factory) + throws InvalidQueryException; + + /** + * Returns true if this query tree builder can handle a + * statement in language. + * + * @param language the language of a query statement to build a query tree. + * @return true if this builder can handle language; + * false otherwise. + */ + boolean canHandle(String language); + + /** + * Returns the set of query languages supported by this builder. + * + * @return String array containing the names of the supported languages. + */ + String[] getSupportedLanguages(); + + /** + * Creates a String representation of the query node tree in the syntax this + * QueryTreeBuilder can handle. + * + * @param root the root of the query node tree. + * @param resolver to resolve Names. + * @return a String representation of the query node tree. + * @throws InvalidQueryException if the query node tree cannot be converted + * into a String representation due to + * restrictions in this syntax. + */ + String toString(QueryRootNode root, NameResolver resolver) + throws InvalidQueryException; + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeBuilderRegistry.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeBuilderRegistry.java new file mode 100644 index 00000000000..a5d62695cf5 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeBuilderRegistry.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.spi.commons.query; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.query.InvalidQueryException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Iterator; +import java.util.Set; + +/** + * Implements a central access to QueryTreeBuilder instances. + */ +public class QueryTreeBuilderRegistry { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(QueryTreeBuilderRegistry.class); + + /** + * List of QueryTreeBuilder instances known to the classloader. + */ + private static final List BUILDERS = new ArrayList(); + + /** + * Set of languages known to the registered builders. + */ + private static final Set LANGUAGES; + + static { + Set languages = new HashSet(); + try { + Iterator it = ServiceLoader.load(QueryTreeBuilder.class, + QueryTreeBuilderRegistry.class.getClassLoader()).iterator(); + while (it.hasNext()) { + QueryTreeBuilder qtb = (QueryTreeBuilder) it.next(); + BUILDERS.add(qtb); + languages.addAll(Arrays.asList(qtb.getSupportedLanguages())); + } + } catch (Error e) { + log.warn("Unable to load providers for QueryTreeBuilder: " + e); + } + LANGUAGES = Collections.unmodifiableSet(languages); + } + + /** + * Returns the QueryTreeBuilder for language. + * + * @param language the language of the query statement. + * @return the QueryTreeBuilder for language. + * @throws InvalidQueryException if there is no query tree builder for + * language. + */ + public static QueryTreeBuilder getQueryTreeBuilder(String language) + throws InvalidQueryException { + for (int i = 0; i < BUILDERS.size(); i++) { + QueryTreeBuilder builder = (QueryTreeBuilder) BUILDERS.get(i); + if (builder.canHandle(language)) { + return builder; + } + } + throw new InvalidQueryException("Unsupported language: " + language); + } + + /** + * Returns the set of query languages supported by all registered + * {@link QueryTreeBuilder} implementations. + * + * @return String array containing the names of the supported languages. + */ + public static String[] getSupportedLanguages() { + return (String[]) LANGUAGES.toArray(new String[LANGUAGES.size()]); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeDump.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeDump.java new file mode 100644 index 00000000000..a6c372915c5 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/QueryTreeDump.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.spi.commons.query; + +import java.util.Arrays; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * Utility class to dump a {@link QueryNode} tree to a StringBuffer. + */ +public class QueryTreeDump implements QueryNodeVisitor { + + /** + * Current indentation level + */ + private int indent; + + /** + * Padding array filled with spaces + */ + private static char[] PADDING = new char[255]; + + /** + * The padding character: whitespace. + */ + private static final char PADDING_CHAR = ' '; + + static { + Arrays.fill(PADDING, PADDING_CHAR); + } + + /** + * Dumps the node tree to buffer. + * @param node the root node. + * @param buffer where to dump the tree. + * @throws RepositoryException + */ + private QueryTreeDump(QueryNode node, StringBuffer buffer) throws RepositoryException { + node.accept(this, buffer); + } + + /** + * Dumps a query node tree to the string buffer. + * @param node the root node of a query tree. + * @param buffer a string buffer where to dump the tree structure. + * @throws RepositoryException + */ + public static void dump(QueryNode node, StringBuffer buffer) throws RepositoryException { + new QueryTreeDump(node, buffer); + } + + public Object visit(QueryRootNode node, Object data) throws RepositoryException { + StringBuffer buffer = (StringBuffer) data; + buffer.append("+ Root node"); + buffer.append("\n"); + // select properties + Name[] select = node.getSelectProperties(); + buffer.append("+ Select properties: "); + if (select.length == 0) { + buffer.append("*"); + } else { + String comma = ""; + for (int i = 0; i < select.length; i++) { + buffer.append(comma); + buffer.append(select[i].toString()); + comma = ", "; + } + } + buffer.append("\n"); + // path + traverse(new QueryNode[]{node.getLocationNode()}, buffer); + // order by + OrderQueryNode order = node.getOrderNode(); + if (order != null) { + traverse(new QueryNode[]{order}, buffer); + } + return buffer; + } + + public Object visit(OrQueryNode node, Object data) throws RepositoryException { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ OrQueryNode"); + buffer.append("\n"); + traverse(node.getOperands(), buffer); + return buffer; + } + + public Object visit(AndQueryNode node, Object data) throws RepositoryException { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ AndQueryNode"); + buffer.append("\n"); + traverse(node.getOperands(), buffer); + return buffer; + } + + public Object visit(NotQueryNode node, Object data) throws RepositoryException { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ NotQueryNode"); + buffer.append("\n"); + traverse(node.getOperands(), buffer); + return buffer; + } + + public Object visit(ExactQueryNode node, Object data) { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ ExactQueryNode: "); + buffer.append(" Prop=").append(node.getPropertyName()); + buffer.append(" Value=").append(node.getValue()); + buffer.append("\n"); + return buffer; + } + + public Object visit(NodeTypeQueryNode node, Object data) { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ NodeTypeQueryNode: "); + buffer.append(" Prop=").append(node.getPropertyName()); + buffer.append(" Value=").append(node.getValue()); + buffer.append("\n"); + return buffer; + } + + public Object visit(TextsearchQueryNode node, Object data) { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ TextsearchQueryNode: "); + buffer.append(" Path="); + Path relPath = node.getRelativePath(); + if (relPath == null) { + buffer.append("."); + } else { + Path.Element[] elements = relPath.getElements(); + String slash = ""; + for (int i = 0; i < elements.length; i++) { + buffer.append(slash); + slash = "/"; + if (node.getReferencesProperty() && i == elements.length - 1) { + buffer.append("@"); + } + buffer.append(elements[i]); + } + } + buffer.append(" Query=").append(node.getQuery()); + buffer.append("\n"); + return buffer; + } + + public Object visit(PathQueryNode node, Object data) throws RepositoryException { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ PathQueryNode"); + buffer.append("\n"); + traverse(node.getOperands(), buffer); + return buffer; + } + + public Object visit(LocationStepQueryNode node, Object data) throws RepositoryException { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ LocationStepQueryNode: "); + buffer.append(" NodeTest="); + if (node.getNameTest() == null) { + buffer.append("*"); + } else { + buffer.append(node.getNameTest()); + } + buffer.append(" Descendants=").append(node.getIncludeDescendants()); + buffer.append(" Index="); + if (node.getIndex() == LocationStepQueryNode.NONE) { + buffer.append("NONE"); + } else if (node.getIndex() == LocationStepQueryNode.LAST) { + buffer.append("last()"); + } else { + buffer.append(node.getIndex()); + } + buffer.append("\n"); + traverse(node.getOperands(), buffer); + return buffer; + } + + public Object visit(RelationQueryNode node, Object data) throws RepositoryException { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ RelationQueryNode: Op: "); + buffer.append(QueryConstants.OPERATION_NAMES.getName(node.getOperation())); + buffer.append(" Prop=["); + PathQueryNode relPath = node.getRelativePath(); + if (relPath == null) { + buffer.append(relPath); + } else { + visit(relPath, buffer); + } + buffer.append("] Type=").append(QueryConstants.TYPE_NAMES.getName(node.getValueType())); + if (node.getValueType() == QueryConstants.TYPE_DATE) { + buffer.append(" Value=").append(node.getDateValue()); + } else if (node.getValueType() == QueryConstants.TYPE_DOUBLE) { + buffer.append(" Value=").append(node.getDoubleValue()); + } else if (node.getValueType() == QueryConstants.TYPE_LONG) { + buffer.append(" Value=").append(node.getLongValue()); + } else if (node.getValueType() == QueryConstants.TYPE_POSITION) { + buffer.append(" Value=").append(node.getPositionValue()); + } else if (node.getValueType() == QueryConstants.TYPE_STRING) { + buffer.append(" Value=").append(node.getStringValue()); + } else if (node.getValueType() == QueryConstants.TYPE_TIMESTAMP) { + buffer.append(" Value=").append(node.getDateValue()); + } + + buffer.append("\n"); + traverse(node.getOperands(), buffer); + return buffer; + } + + public Object visit(OrderQueryNode node, Object data) { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ OrderQueryNode"); + buffer.append("\n"); + OrderQueryNode.OrderSpec[] specs = node.getOrderSpecs(); + for (int i = 0; i < specs.length; i++) { + buffer.append(PADDING, 0, indent); + buffer.append(" "); + appendPath(specs[i].getPropertyPath(), buffer); + buffer.append(" asc=").append(specs[i].isAscending()); + buffer.append("\n"); + } + return buffer; + } + + public Object visit(DerefQueryNode node, Object data) throws RepositoryException { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ DerefQueryNode: "); + buffer.append(" NodeTest="); + if (node.getNameTest() == null) { + buffer.append("*"); + } else { + buffer.append(node.getNameTest()); + } + buffer.append(" Descendants=").append(node.getIncludeDescendants()); + buffer.append(" Index="); + if (node.getIndex() == LocationStepQueryNode.NONE) { + buffer.append("NONE"); + } else if (node.getIndex() == LocationStepQueryNode.LAST) { + buffer.append("last()"); + } else { + buffer.append(node.getIndex()); + } + buffer.append("\n"); + traverse(node.getOperands(), buffer); + return buffer; + } + + public Object visit(PropertyFunctionQueryNode node, Object data) { + StringBuffer buffer = (StringBuffer) data; + buffer.append(PADDING, 0, indent); + buffer.append("+ PropertyFunctionQueryNode: "); + buffer.append(node.getFunctionName()); + buffer.append("()\n"); + return buffer; + } + + private void traverse(QueryNode[] node, StringBuffer buffer) throws RepositoryException { + indent += 2; + if (indent > PADDING.length) { + char[] tmp = new char[indent * 2]; + Arrays.fill(tmp, PADDING_CHAR); + PADDING = tmp; + } + for (int i = 0; i < node.length; i++) { + node[i].accept(this, buffer); + } + indent -= 2; + } + + /** + * Appends the relative path to the buffer using '/' as the + * delimiter for path elements. + * + * @param relPath a relative path. + * @param buffer the buffer where to append the path. + */ + private static void appendPath(Path relPath, StringBuffer buffer) { + Path.Element[] elements = relPath.getElements(); + String slash = ""; + for (int i = 0; i < elements.length; i++) { + buffer.append(slash); + slash = "/"; + buffer.append(elements[i]); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/RelationQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/RelationQueryNode.java new file mode 100644 index 00000000000..96a1eacb8d7 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/RelationQueryNode.java @@ -0,0 +1,310 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query; + +import java.util.Date; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Path.Element; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +/** + * Implements a query node that defines property value relation. + */ +public class RelationQueryNode extends NAryQueryNode implements QueryConstants { + + /** + * Acts as an synthetic placeholder for a location step that matches any + * name. This is required because a JCR path does not allow a Name with + * a single '*' (star) character. + */ + public static final Name STAR_NAME_TEST = NameFactoryImpl.getInstance().create(Name.NS_REP_URI, "__star__"); + + /** + * The relative path to the property. + */ + private PathQueryNode relPath; + + /** + * If true this relation query node contains a value preceded + * with an unary minus. + */ + private boolean unaryMinus; + + /** + * The long value of the relation if this is a query is of type + * long + */ + private long valueLong; + + /** + * The int value of the position index. + */ + private int valuePosition; + + /** + * The double value of the relation if this is a query is of + * type double + */ + private double valueDouble; + + /** + * The String value of the relation if this is a query is of + * type String + */ + private String valueString; + + /** + * The Date value of the relation if this is a query is of type + * Date + */ + private Date valueDate; + + /** + * The operation type of this relation. One of the operation values defined + * in {@link QueryConstants}. + */ + private final int operation; + + /** + * The value type of this relation. One of {@link #TYPE_DATE}, {@link + * #TYPE_DOUBLE}, {@link #TYPE_LONG}, {@link #TYPE_STRING}, {@link #TYPE_POSITION}. + */ + private int type; + + private final QueryNodeFactory factory; + + /** + * Creates a new RelationQueryNode without a type nor value + * assigned. + * + * @param parent the parent node for this query node. + * @param operation the operation. + * @param factory the query node factory. + */ + protected RelationQueryNode(QueryNode parent, + int operation, + QueryNodeFactory factory) { + super(parent); + this.operation = operation; + this.factory = factory; + this.relPath = factory.createPathQueryNode(this); + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_RELATION; + } + + /** + * If b is true then the value in this relation + * node contains a receding unary minus. + * + * @param b true if this relation contains a unary minus. + */ + public void setUnaryMinus(boolean b) { + unaryMinus = b; + } + + /** + * Returns the type of the value. + * + * @return the type of the value. + */ + public int getValueType() { + return type; + } + + /** + * @return the relative path that references the property in this relation. + */ + public PathQueryNode getRelativePath() { + return relPath; + } + + /** + * Sets the relative path to the property in this relation. + * + * @param relPath the relative path to a property. + * @throws IllegalArgumentException if relPath is absolute. + */ + public void setRelativePath(Path relPath) { + if (relPath.isAbsolute()) { + throw new IllegalArgumentException("relPath must be relative"); + } + + Element[] elements = relPath.getElements(); + for (Element element : elements) { + addPathElement(element); + } + } + + /** + * Adds a path element to the existing relative path. To add a path element + * which matches all node names use {@link #STAR_NAME_TEST}. + * + * @param element the path element to append. + */ + public void addPathElement(Path.Element element) { + LocationStepQueryNode step = factory.createLocationStepQueryNode(relPath); + if (element.getName().equals(STAR_NAME_TEST)) { + step.setNameTest(null); + } else { + step.setNameTest(element.getName()); + } + relPath.addPathStep(step); + } + + /** + * Returns the long value if this relation if of type + * {@link #TYPE_LONG}. + * + * @return the long value. + */ + public long getLongValue() { + return valueLong; + } + + /** + * Sets a new value of type long. + * + * @param value the new value. + */ + public void setLongValue(long value) { + valueLong = unaryMinus ? -value : value; + type = TYPE_LONG; + } + + /** + * Returns the int position index value if this relation is + * of type {@link #TYPE_POSITION}. + * @return the position index value. + */ + public int getPositionValue() { + return valuePosition; + } + + /** + * Sets a new value for the position index. + * + * @param value the new value. + */ + public void setPositionValue(int value) { + valuePosition = value; + type = TYPE_POSITION; + } + + /** + * Returns the double value if this relation if of type + * {@link #TYPE_DOUBLE}. + * + * @return the double value. + */ + public double getDoubleValue() { + return valueDouble; + } + + /** + * Sets a new value of type double. + * + * @param value the new value. + */ + public void setDoubleValue(double value) { + valueDouble = unaryMinus ? -value : value; + type = TYPE_DOUBLE; + } + + /** + * Returns the String value if this relation if of type + * {@link #TYPE_STRING}. + * + * @return the String value. + */ + public String getStringValue() { + return valueString; + } + + /** + * Sets a new value of type String. + * + * @param value the new value. + */ + public void setStringValue(String value) { + valueString = value; + type = TYPE_STRING; + } + + /** + * Returns the Date value if this relation if of type + * {@link #TYPE_DATE}. + * + * @return the Date value. + */ + public Date getDateValue() { + return valueDate; + } + + /** + * Sets a new value of type Date. + * + * @param value the new value. + */ + public void setDateValue(Date value) { + valueDate = value; + type = TYPE_DATE; + } + + /** + * Returns the operation type. + * + * @return the operation type. + */ + public int getOperation() { + return operation; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof RelationQueryNode) { + RelationQueryNode other = (RelationQueryNode) obj; + return type == other.type + && (valueDate == null ? other.valueDate == null : valueDate.equals(other.valueDate)) + && valueDouble == other.valueDouble + && valueLong == other.valueLong + && valuePosition == other.valuePosition + && (valueString == null ? other.valueString == null : valueString.equals(other.valueString)) + && (relPath == null ? other.relPath == null : relPath.equals(other.relPath)); + } + return false; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/TextsearchQueryNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/TextsearchQueryNode.java new file mode 100644 index 00000000000..15d1686e64f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/TextsearchQueryNode.java @@ -0,0 +1,208 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; + +/** + * Implements a query node that defines a textsearch clause. + */ +public class TextsearchQueryNode extends QueryNode { + + /** + * The query statement inside the textsearch clause + */ + private final String query; + + /** + * Limits the scope of this textsearch clause to a node or a property with + * the given relative path. + * If null the scope of this textsearch clause is the fulltext + * index of all properties of the context node. + */ + private Path relPath; + + /** + * If set to true {@link #relPath} references a property, + * otherwise references a node. + */ + private boolean propertyRef; + + /** + * Creates a new TextsearchQueryNode with a parent + * and a textsearch query statement. The scope of the query + * is the fulltext index of the node, that contains all properties. + * + * @param parent the parent node of this query node. + * @param query the textsearch statement. + */ + protected TextsearchQueryNode(QueryNode parent, String query) { + super(parent); + this.query = query; + this.relPath = null; + this.propertyRef = false; + } + + /** + * {@inheritDoc} + * @throws RepositoryException + */ + public Object accept(QueryNodeVisitor visitor, Object data) throws RepositoryException { + return visitor.visit(this, data); + } + + /** + * Returns the type of this node. + * + * @return the type of this node. + */ + public int getType() { + return QueryNode.TYPE_TEXTSEARCH; + } + + /** + * Returns the textsearch statement. + * + * @return the textsearch statement. + */ + public String getQuery() { + return query; + } + + /** + * Returns a property name if the scope is limited to just a single property + * or null if the scope is spawned across all properties of a + * node. Please note that this method does not return the full relative path + * that reference the item to match, but only the name of the final name + * element of the path returned by {@link #getRelativePath()}. + * + * @return property name or null. + * @deprecated Use {@link #getRelativePath()} instead. + */ + public Name getPropertyName() { + return relPath == null ? null : relPath.getName(); + } + + /** + * Sets a new name as the search scope for this fulltext query. + * + * @param property the name of the property. + * @deprecated Use {@link #setRelativePath(Path)} instead. + */ + public void setPropertyName(Name property) { + PathBuilder builder = new PathBuilder(); + builder.addLast(property); + try { + this.relPath = builder.getPath(); + this.propertyRef = true; + } catch (MalformedPathException e) { + // path is always valid + } + } + + /** + * @return the relative path that references the item where the textsearch + * is performed. Returns null if the textsearch is + * performed on the context node. + */ + public Path getRelativePath() { + return relPath; + } + + /** + * Sets the relative path to the item where the textsearch is performed. If + * relPath is null the textsearch is performed on + * the context node. + * + * @param relPath the relative path to an item. + * @throws IllegalArgumentException if relPath is absolute. + */ + public void setRelativePath(Path relPath) { + if (relPath != null && relPath.isAbsolute()) { + throw new IllegalArgumentException("relPath must be relative"); + } + this.relPath = relPath; + if (relPath == null) { + // context node is never a property + propertyRef = false; + } + } + + /** + * Adds a path element to the existing relative path. To add a path element + * which matches all node names use {@link RelationQueryNode#STAR_NAME_TEST}. + * + * @param element the path element to append. + */ + public void addPathElement(Path.Element element) { + PathBuilder builder = new PathBuilder(); + if (relPath != null) { + builder.addAll(relPath.getElements()); + } + builder.addLast(element); + try { + relPath = builder.getPath(); + } catch (MalformedPathException e) { + // path is always valid + } + } + + /** + * @return true if {@link #getRelativePath()} references a + * property, returns false if it references a node. + */ + public boolean getReferencesProperty() { + return propertyRef; + } + + /** + * Is set to true, indicates that {@link #getRelativePath()} + * references a property, if set to false indicates that it + * references a node. + * + * @param b flag whether a property is referenced. + */ + public void setReferencesProperty(boolean b) { + propertyRef = b; + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (obj instanceof TextsearchQueryNode) { + TextsearchQueryNode other = (TextsearchQueryNode) obj; + return (query == null ? other.query == null : query.equals(other.query)) + && (relPath == null ? other.relPath == null : relPath.equals(other.relPath) + && propertyRef == other.propertyRef); + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean needsSystemTree() { + return false; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/TraversingQueryNodeVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/TraversingQueryNodeVisitor.java new file mode 100644 index 00000000000..ba38062735c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/TraversingQueryNodeVisitor.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.spi.commons.query; + +import javax.jcr.RepositoryException; + +/** + * TraversingQueryNodeVisitor implements a base class for a + * traversing query node visitor. + */ +public class TraversingQueryNodeVisitor extends DefaultQueryNodeVisitor { + + public Object visit(OrQueryNode node, Object data) throws RepositoryException { + return node.acceptOperands(this, data); + } + + public Object visit(AndQueryNode node, Object data) throws RepositoryException { + return node.acceptOperands(this, data); + } + + public Object visit(QueryRootNode node, Object data) throws RepositoryException { + PathQueryNode pathNode = node.getLocationNode(); + if (pathNode != null) { + pathNode.accept(this, data); + } + OrderQueryNode orderNode = node.getOrderNode(); + if (orderNode != null) { + orderNode.accept(this, data); + } + return data; + } + + public Object visit(NotQueryNode node, Object data) throws RepositoryException { + return node.acceptOperands(this, data); + } + + public Object visit(PathQueryNode node, Object data) throws RepositoryException { + return node.acceptOperands(this, data); + } + + public Object visit(LocationStepQueryNode node, Object data) throws RepositoryException { + return node.acceptOperands(this, data); + } + + public Object visit(DerefQueryNode node, Object data) throws RepositoryException { + return node.acceptOperands(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/package-info.java new file mode 100644 index 00000000000..7d5b763867b --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.query; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/AbstractQOMNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/AbstractQOMNode.java new file mode 100644 index 00000000000..5ab73d9058f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/AbstractQOMNode.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.spi.commons.query.qom; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.NamespaceException; + +/** + * AbstractQOMNode... + */ +public abstract class AbstractQOMNode { + + protected final NamePathResolver resolver; + + public AbstractQOMNode(NamePathResolver resolver) { + this.resolver = resolver; + } + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + * @param data user defined data, which is passed to the visit method. + */ + public abstract Object accept(QOMTreeVisitor visitor, Object data) throws Exception; + + //---------------------------< internal >----------------------------------- + + /** + * Returns the JCR name string for the given Name or + * null if name is null. + * + * @param name the Name. + * @return the prefixed JCR name or name.toString() if an + * unknown namespace URI is encountered. + */ + protected String getJCRName(Name name) { + if (name == null) { + return null; + } + try { + return resolver.getJCRName(name); + } catch (NamespaceException e) { + return name.toString(); + } + } + + /** + * Returns the JCR path String for the given Path object or + * null if path is null. + * + * @param path A Path object. + * @return JCR path in the standard form or path.toString() + * if an unknown namespace URI is encountered. + */ + protected String getJCRPath(Path path) { + if (path == null) { + return null; + } + try { + return resolver.getJCRPath(path); + } catch (NamespaceException e) { + return path.toString(); + } + } + + protected String quote(Name name) { + String str = getJCRName(name); + if (str.indexOf(':') != -1) { + return "[" + str + "]"; + } else { + return str; + } + } + + protected String quote(Path path) { + String str = getJCRPath(path); + if (str.indexOf(':') != -1 || str.indexOf('/') != -1) { + return "[" + str + "]"; + } else { + return str; + } + } + + protected String protect(Object expression) { + String str = expression.toString(); + if (str.indexOf(" ") != -1) { + return "(" + str + ")"; + } else { + return str; + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/AndImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/AndImpl.java new file mode 100644 index 00000000000..6e8d17fb336 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/AndImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.And; +import javax.jcr.query.qom.Constraint; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * AndImpl... + */ +public class AndImpl extends ConstraintImpl implements And { + + /** + * The first constraint. + */ + private final ConstraintImpl constraint1; + + /** + * The second constraint. + */ + private final ConstraintImpl constraint2; + + AndImpl(NamePathResolver resolver, ConstraintImpl c1, ConstraintImpl c2) { + super(resolver); + this.constraint1 = c1; + this.constraint2 = c2; + } + + /** + * Gets the first constraint. + * + * @return the constraint; non-null + */ + public Constraint getConstraint1() { + return constraint1; + } + + /** + * Gets the second constraint. + * + * @return the constraint; non-null + */ + public Constraint getConstraint2() { + return constraint2; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return protect(constraint1) + " AND " + protect(constraint2); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/BindVariableValueImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/BindVariableValueImpl.java new file mode 100644 index 00000000000..ff637a75e5e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/BindVariableValueImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.BindVariableValue; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +/** + * BindVariableValueImpl... + */ +public class BindVariableValueImpl + extends StaticOperandImpl + implements BindVariableValue { + + /** + * The name of the bind variable. + */ + private final Name variableName; + + BindVariableValueImpl(NamePathResolver resolver, Name variableName) { + super(resolver); + this.variableName = variableName; + } + + /** + * Gets the name of the bind variable. + * + * @return the bind variable name; non-null + */ + public Name getBindVariableQName() { + return variableName; + } + + //-------------------------< BindVariableValue >---------------------------- + + /** + * Gets the name of the bind variable. + * + * @return the bind variable name; non-null + */ + public String getBindVariableName() { + return getJCRName(variableName); + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "$" + getBindVariableName(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ChildNodeImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ChildNodeImpl.java new file mode 100644 index 00000000000..5fe6e0bb43c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ChildNodeImpl.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.qom; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.ChildNode; +import javax.jcr.RepositoryException; + +/** + * ChildNodeImpl... + */ +public class ChildNodeImpl extends ConstraintImpl implements ChildNode { + + /** + * The name of a selector. + */ + private final Name selectorName; + + /** + * An absolute path. + */ + private final Path path; + + ChildNodeImpl(NamePathResolver resolver, Name selectorName, Path path) + throws InvalidQueryException, RepositoryException { + super(resolver); + this.selectorName = selectorName; + this.path = path; + if (!path.isAbsolute()) { + throw new InvalidQueryException(resolver.getJCRPath(path) + + " is not an absolute path"); + } + } + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public String getSelectorName() { + return getJCRName(selectorName); + } + + /** + * Gets the absolute path. + * + * @return the path; non-null + */ + public String getParentPath() { + return getJCRPath(path); + } + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public Name getSelectorQName() { + return selectorName; + } + + /** + * Gets the absolute path. + * + * @return the path; non-null + */ + public Path getQPath() { + return path; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "ISCHILDNODE(" + getSelectorName() + ", " + quote(path) + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ChildNodeJoinConditionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ChildNodeJoinConditionImpl.java new file mode 100644 index 00000000000..880d8567605 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ChildNodeJoinConditionImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.ChildNodeJoinCondition; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +/** + * ChildNodeJoinConditionImpl... + */ +public class ChildNodeJoinConditionImpl + extends JoinConditionImpl + implements ChildNodeJoinCondition { + + /** + * The name of the child selector. + */ + private final Name childSelectorName; + + /** + * The name of the parent selector. + */ + private final Name parentSelectorName; + + ChildNodeJoinConditionImpl(NamePathResolver resolver, + Name childSelectorName, + Name parentSelectorName) { + super(resolver); + this.childSelectorName = childSelectorName; + this.parentSelectorName = parentSelectorName; + } + + /** + * Gets the name of the child selector. + * + * @return the selector name; non-null + */ + public String getChildSelectorName() { + return getJCRName(childSelectorName); + } + + /** + * Gets the name of the parent selector. + * + * @return the selector name; non-null + */ + public String getParentSelectorName() { + return getJCRName(parentSelectorName); + } + + /** + * Gets the name of the child selector. + * + * @return the selector name; non-null + */ + public Name getChildSelectorQName() { + return childSelectorName; + } + + /** + * Gets the name of the parent selector. + * + * @return the selector name; non-null + */ + public Name getParentSelectorQName() { + return parentSelectorName; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + String child = getChildSelectorName(); + String parent = getParentSelectorName(); + return "ISCHILDNODE(" + child + ", " + parent + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ColumnImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ColumnImpl.java new file mode 100644 index 00000000000..b3ebb120222 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ColumnImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Column; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * ColumnImpl... + */ +public class ColumnImpl extends AbstractQOMNode implements Column { + + /** + * Empty ColumnImpl array. + */ + public static final ColumnImpl[] EMPTY_ARRAY = new ColumnImpl[0]; + + /** + * The name of the selector. + */ + private final Name selectorName; + + /** + * The name of the property. + */ + private final Name propertyName; + + /** + * The name of the column. + */ + private final String columnName; + + ColumnImpl(NamePathResolver resolver, + Name selectorName, + Name propertyName, + String columnName) { + super(resolver); + this.selectorName = selectorName; + this.propertyName = propertyName; + this.columnName = columnName; + } + + /** + * Gets the name of the selector. + * + * @return the selector name; non-null + */ + public Name getSelectorQName() { + return selectorName; + } + + /** + * Gets the name of the property. + * + * @return the property name, or null to include a column for each + * single-value non-residual property of the selector's node type + */ + public Name getPropertyQName() { + return propertyName; + } + + //---------------------------< Column >------------------------------------- + + /** + * Gets the name of the selector. + * + * @return the selector name; non-null + */ + public String getSelectorName() { + return getJCRName(selectorName); + } + + /** + * Gets the name of the property. + * + * @return the property name, or null to include a column for each + * single-value non-residual property of the selector's node type + */ + public String getPropertyName() { + return getJCRName(propertyName); + } + + /** + * Gets the column name. + *

        + * + * @return the column name; must be null if getPropertyName is + * null and non-null otherwise + */ + public String getColumnName() { + return columnName; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + if (propertyName != null) { + return getSelectorName() + "." + getPropertyName() + + " AS " + getColumnName(); + } else { + return getSelectorName() + ".*"; + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ComparisonImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ComparisonImpl.java new file mode 100644 index 00000000000..c615aec1c42 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ComparisonImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Comparison; +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.StaticOperand; + +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * ComparisonImpl... + */ +public class ComparisonImpl extends ConstraintImpl implements Comparison { + + /** + * The first operand. + */ + private final DynamicOperandImpl operand1; + + /** + * The operator. + */ + private final Operator operator; + + /** + * The second operand. + */ + private final StaticOperandImpl operand2; + + ComparisonImpl(NamePathResolver resolver, + DynamicOperandImpl operand1, + Operator operator, + StaticOperandImpl operand2) { + super(resolver); + this.operand1 = operand1; + this.operator = operator; + this.operand2 = operand2; + } + + public Operator getOperatorInstance() { + return operator; + } + + //----------------------------------------------------------< Comparison > + + /** + * Gets the first operand. + * + * @return the operand; non-null + */ + public DynamicOperand getOperand1() { + return operand1; + } + + /** + * Gets the operator. + * + * @return either

        • {@link QueryObjectModelConstants#JCR_OPERATOR_EQUAL_TO},
        • + *
        • {@link QueryObjectModelConstants#JCR_OPERATOR_NOT_EQUAL_TO},
        • + *
        • {@link QueryObjectModelConstants#JCR_OPERATOR_LESS_THAN},
        • + *
        • {@link QueryObjectModelConstants#JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO},
        • + *
        • {@link QueryObjectModelConstants#JCR_OPERATOR_GREATER_THAN},
        • + *
        • {@link QueryObjectModelConstants#JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO}, + * or
        • {@link QueryObjectModelConstants#JCR_OPERATOR_LIKE}
        • + *
        + */ + public String getOperator() { + return operator.toString(); + } + + /** + * Gets the second operand. + * + * @return the operand; non-null + */ + public StaticOperand getOperand2() { + return operand2; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return operator.formatSql(operand1.toString(), operand2.toString()); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ConstraintImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ConstraintImpl.java new file mode 100644 index 00000000000..e4a5854f711 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/ConstraintImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Constraint; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * ConstraintImpl is the common basis for classes that implement + * the {@link Constraint} interface. + */ +public abstract class ConstraintImpl + extends AbstractQOMNode + implements Constraint { + + public ConstraintImpl(NamePathResolver resolver) { + super(resolver); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DefaultQOMTreeVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DefaultQOMTreeVisitor.java new file mode 100644 index 00000000000..346690d7957 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DefaultQOMTreeVisitor.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.spi.commons.query.qom; + +/** + * DefaultQOMTreeVisitor default implementation of a {@link + * QOMTreeVisitor}. + */ +public class DefaultQOMTreeVisitor implements QOMTreeVisitor { + + /** + * Does nothing and returns data. + */ + public Object visit(AndImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(BindVariableValueImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(ChildNodeImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(ChildNodeJoinConditionImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(ColumnImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(ComparisonImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(DescendantNodeImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(DescendantNodeJoinConditionImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(EquiJoinConditionImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(FullTextSearchImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(FullTextSearchScoreImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(JoinImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(LengthImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(LiteralImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(LowerCaseImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(NodeLocalNameImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(NodeNameImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(NotImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(OrderingImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(OrImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(PropertyExistenceImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(PropertyValueImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(QueryObjectModelTree node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(SameNodeImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(SameNodeJoinConditionImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(SelectorImpl node, Object data) throws Exception { + return data; + } + + /** + * Does nothing and returns data. + */ + public Object visit(UpperCaseImpl node, Object data) throws Exception { + return data; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DefaultTraversingQOMTreeVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DefaultTraversingQOMTreeVisitor.java new file mode 100644 index 00000000000..ec241cb8b4f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DefaultTraversingQOMTreeVisitor.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.spi.commons.query.qom; + +/** + * DefaultTraversingQOMTreeVisitor default implementation of a + * traversing {@link QOMTreeVisitor}. + */ +public class DefaultTraversingQOMTreeVisitor extends DefaultQOMTreeVisitor { + + /** + * Calls accept on each of the attached constraints of the AND node. + */ + public final Object visit(AndImpl node, Object data) throws Exception { + ((ConstraintImpl) node.getConstraint1()).accept(this, data); + ((ConstraintImpl) node.getConstraint2()).accept(this, data); + return data; + } + + /** + * Calls accept on the two operands in the comparison node. + */ + public Object visit(ComparisonImpl node, Object data) throws Exception { + ((DynamicOperandImpl) node.getOperand1()).accept(this, data); + ((StaticOperandImpl) node.getOperand2()).accept(this, data); + return data; + } + + /** + * Calls accept on the static operand in the fulltext search constraint. + */ + public Object visit(FullTextSearchImpl node, Object data) throws Exception { + ((StaticOperandImpl) node.getFullTextSearchExpression()).accept(this, data); + return data; + } + + /** + * Calls accept on the two sources and the join condition in the join node. + */ + public Object visit(JoinImpl node, Object data) throws Exception { + ((SourceImpl) node.getRight()).accept(this, data); + ((SourceImpl) node.getLeft()).accept(this, data); + ((JoinConditionImpl) node.getJoinCondition()).accept(this, data); + return data; + } + + /** + * Calls accept on the property value in the length node. + */ + public Object visit(LengthImpl node, Object data) throws Exception { + ((PropertyValueImpl) node.getPropertyValue()).accept(this, data); + return data; + } + + /** + * Calls accept on the dynamic operand in the lower-case node. + */ + public Object visit(LowerCaseImpl node, Object data) throws Exception { + ((DynamicOperandImpl) node.getOperand()).accept(this, data); + return data; + } + + /** + * Calls accept on the constraint in the NOT node. + */ + public Object visit(NotImpl node, Object data) throws Exception { + ((ConstraintImpl) node.getConstraint()).accept(this, data); + return data; + } + + /** + * Calls accept on the dynamic operand in the ordering node. + */ + public Object visit(OrderingImpl node, Object data) throws Exception { + ((DynamicOperandImpl) node.getOperand()).accept(this, data); + return data; + } + + /** + * Calls accept on each of the attached constraints of the OR node. + */ + public Object visit(OrImpl node, Object data) throws Exception { + ((ConstraintImpl) node.getConstraint1()).accept(this, data); + ((ConstraintImpl) node.getConstraint2()).accept(this, data); + return data; + } + + /** + * Calls accept on the following contained QOM nodes: + *
          + *
        • Source
        • + *
        • Constraints
        • + *
        • Orderings
        • + *
        • Columns
        • + *
        + */ + public Object visit(QueryObjectModelTree node, Object data) throws Exception { + node.getSource().accept(this, data); + ConstraintImpl constraint = node.getConstraint(); + if (constraint != null) { + constraint.accept(this, data); + } + OrderingImpl[] orderings = node.getOrderings(); + for (int i = 0; i < orderings.length; i++) { + orderings[i].accept(this, data); + } + ColumnImpl[] columns = node.getColumns(); + for (int i = 0; i < columns.length; i++) { + columns[i].accept(this, data); + } + return data; + } + + /** + * Calls accept on the dynamic operand in the lower-case node. + */ + public Object visit(UpperCaseImpl node, Object data) throws Exception { + ((DynamicOperandImpl) node.getOperand()).accept(this, data); + return data; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DescendantNodeImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DescendantNodeImpl.java new file mode 100644 index 00000000000..524dbf0b3c4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DescendantNodeImpl.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.spi.commons.query.qom; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.DescendantNode; +import javax.jcr.NamespaceException; + +/** + * DescendantNodeImpl... + */ +public class DescendantNodeImpl + extends ConstraintImpl + implements DescendantNode { + + /** + * A selector name. + */ + private final Name selectorName; + + /** + * An absolute path. + */ + private final Path path; + + DescendantNodeImpl(NamePathResolver resolver, + Name selectorName, + Path path) + throws InvalidQueryException, NamespaceException { + super(resolver); + this.selectorName = selectorName; + this.path = path; + if (!path.isAbsolute()) { + throw new InvalidQueryException(resolver.getJCRPath(path) + + " is not an absolute path"); + } + } + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public String getSelectorName() { + return getJCRName(selectorName); + } + + /** + * Gets the absolute path. + * + * @return the path; non-null + */ + public String getAncestorPath() { + return getJCRPath(path); + } + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public Name getSelectorQName() { + return selectorName; + } + + /** + * Gets the absolute path. + * + * @return the path; non-null + */ + public Path getQPath() { + return path; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "ISDESCENDANTNODE(" + getSelectorName() + ", " + quote(path) + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DescendantNodeJoinConditionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DescendantNodeJoinConditionImpl.java new file mode 100644 index 00000000000..4f32deea4e1 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DescendantNodeJoinConditionImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.DescendantNodeJoinCondition; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +/** + * DescendantNodeJoinConditionImpl... + */ +public class DescendantNodeJoinConditionImpl + extends JoinConditionImpl + implements DescendantNodeJoinCondition { + + /** + * Name of the descendant selector. + */ + private final Name descendantSelectorName; + + /** + * Name of the ancestor selector. + */ + private final Name ancestorSelectorName; + + DescendantNodeJoinConditionImpl(NamePathResolver resolver, + Name descendantSelectorName, + Name ancestorSelectorName) { + super(resolver); + this.descendantSelectorName = descendantSelectorName; + this.ancestorSelectorName = ancestorSelectorName; + } + + /** + * Gets the name of the descendant selector. + * + * @return the selector name; non-null + */ + public String getDescendantSelectorName() { + return getJCRName(descendantSelectorName); + } + + /** + * Gets the name of the ancestor selector. + * + * @return the selector name; non-null + */ + public String getAncestorSelectorName() { + return getJCRName(ancestorSelectorName); + } + + /** + * Gets the name of the descendant selector. + * + * @return the selector name; non-null + */ + public Name getDescendantSelectorQName() { + return descendantSelectorName; + } + + /** + * Gets the name of the ancestor selector. + * + * @return the selector name; non-null + */ + public Name getAncestorSelectorQName() { + return ancestorSelectorName; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + String descendant = getDescendantSelectorName(); + String ancestor = getAncestorSelectorName(); + return "ISDESCENDANTNODE(" + descendant + ", " + ancestor + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DynamicOperandImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DynamicOperandImpl.java new file mode 100644 index 00000000000..b5896b2b58e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/DynamicOperandImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.DynamicOperand; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import org.apache.jackrabbit.spi.Name; + +/** + * DynamicOperandImpl... + */ +public abstract class DynamicOperandImpl + extends AbstractQOMNode + implements DynamicOperand { + + /** + * The name of a selector. + */ + private final Name selectorName; + + public DynamicOperandImpl(NamePathResolver resolver, Name selectorName) { + super(resolver); + this.selectorName = selectorName; + } + + /** + * Gets the name of the selector against which to evaluate this operand. + * + * @return the selector name; non-null + */ + public String getSelectorName() { + return getJCRName(selectorName); + } + + /** + * Gets the name of the selector against which to evaluate this operand. + * + * @return the selector name; non-null + */ + public Name getSelectorQName() { + return selectorName; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/EquiJoinConditionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/EquiJoinConditionImpl.java new file mode 100644 index 00000000000..813fb69b7f7 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/EquiJoinConditionImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.EquiJoinCondition; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * EquiJoinConditionImpl... + */ +public class EquiJoinConditionImpl + extends JoinConditionImpl + implements EquiJoinCondition { + + /** + * Name of the first selector. + */ + private final Name selector1Name; + + /** + * Property name in the first selector. + */ + private final Name property1Name; + + /** + * Name of the second selector. + */ + private final Name selector2Name; + + /** + * Property name in the second selector. + */ + private final Name property2Name; + + EquiJoinConditionImpl(NamePathResolver resolver, + Name selector1Name, + Name property1Name, + Name selector2Name, + Name property2Name) { + super(resolver); + this.selector1Name = selector1Name; + this.property1Name = property1Name; + this.selector2Name = selector2Name; + this.property2Name = property2Name; + } + + /** + * Gets the name of the first selector. + * + * @return the selector name; non-null + */ + public String getSelector1Name() { + return getJCRName(selector1Name); + } + + /** + * Gets the property name in the first selector. + * + * @return the property name; non-null + */ + public String getProperty1Name() { + return getJCRName(property1Name); + } + + /** + * Gets the name of the second selector. + * + * @return the selector name; non-null + */ + public String getSelector2Name() { + return getJCRName(selector2Name); + } + + /** + * Gets the property name in the second selector. + * + * @return the property name; non-null + */ + public String getProperty2Name() { + return getJCRName(property2Name); + } + + /** + * Gets the name of the first selector. + * + * @return the selector name; non-null + */ + public Name getSelector1QName() { + return selector1Name; + } + + /** + * Gets the name of the second selector. + * + * @return the selector name; non-null + */ + public Name getSelector2QName() { + return selector2Name; + } + + /** + * Gets the property name in the first selector. + * + * @return the property name; non-null + */ + public Name getProperty1QName() { + return property1Name; + } + + /** + * Gets the property name in the second selector. + * + * @return the property name; non-null + */ + public Name getProperty2QName() { + return property2Name; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return getSelector1Name() + "." + quote(getProperty1QName()) + + " = " + getSelector2Name() + "." + quote(getProperty2QName()); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/FullTextSearchImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/FullTextSearchImpl.java new file mode 100644 index 00000000000..1b20f437721 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/FullTextSearchImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.FullTextSearch; +import javax.jcr.query.qom.StaticOperand; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * FullTextSearchImpl... + */ +public class FullTextSearchImpl + extends ConstraintImpl + implements FullTextSearch { + + /** + * Name of the selector against which to apply this constraint + */ + private final Name selectorName; + + /** + * Name of the property. + */ + private final Name propertyName; + + /** + * Full text search expression. + */ + private final StaticOperand fullTextSearchExpression; + + FullTextSearchImpl(NamePathResolver resolver, + Name selectorName, + Name propertyName, + StaticOperand fullTextSearchExpression) { + super(resolver); + this.selectorName = selectorName; + this.propertyName = propertyName; + this.fullTextSearchExpression = fullTextSearchExpression; + } + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public Name getSelectorQName() { + return selectorName; + } + + /** + * Gets the name of the property. + * + * @return the property name if the full-text search scope is a property, + * otherwise null if the full-text search scope is the node (or node + * subtree, in some implementations). + */ + public Name getPropertyQName() { + return propertyName; + } + + //--------------------------< FullTextSearch >------------------------------ + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public String getSelectorName() { + return getJCRName(selectorName); + } + + /** + * Gets the name of the property. + * + * @return the property name if the full-text search scope is a property, + * otherwise null if the full-text search scope is the node (or node + * subtree, in some implementations). + */ + public String getPropertyName() { + return getJCRName(propertyName); + } + + /** + * Gets the full-text search expression. + * + * @return the full-text search expression; non-null + */ + public StaticOperand getFullTextSearchExpression() { + return fullTextSearchExpression; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CONTAINS("); + builder.append(getSelectorName()); + if (propertyName != null) { + builder.append("."); + builder.append(quote(propertyName)); + builder.append(", "); + } else { + builder.append(".*, "); + } + builder.append(getFullTextSearchExpression()); + builder.append(")"); + return builder.toString(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/FullTextSearchScoreImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/FullTextSearchScoreImpl.java new file mode 100644 index 00000000000..cccc26c0c87 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/FullTextSearchScoreImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.FullTextSearchScore; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +/** + * FullTextSearchScoreImpl... + */ +public class FullTextSearchScoreImpl + extends DynamicOperandImpl + implements FullTextSearchScore { + + FullTextSearchScoreImpl(NamePathResolver resolver, Name selectorName) { + super(resolver, selectorName); + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "SCORE(" + getSelectorName() + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/JoinConditionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/JoinConditionImpl.java new file mode 100644 index 00000000000..b58a4331c49 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/JoinConditionImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.JoinCondition; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * JoinConditionImpl... + */ +public abstract class JoinConditionImpl + extends AbstractQOMNode + implements JoinCondition { + + public JoinConditionImpl(NamePathResolver resolver) { + super(resolver); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/JoinImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/JoinImpl.java new file mode 100644 index 00000000000..4a4a3b0d6d9 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/JoinImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Join; +import javax.jcr.query.qom.JoinCondition; +import javax.jcr.query.qom.QueryObjectModelConstants; +import javax.jcr.query.qom.Source; + +import org.apache.jackrabbit.commons.query.qom.JoinType; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * JoinImpl... + */ +public class JoinImpl extends SourceImpl implements Join { + + /** + * The left node-tuple source. + */ + private final SourceImpl left; + + /** + * The right node-tuple source. + */ + private final SourceImpl right; + + /** + * The join type. + */ + private final JoinType joinType; + + /** + * The join condition. + */ + private final JoinConditionImpl joinCondition; + + JoinImpl(NamePathResolver resolver, + SourceImpl left, + SourceImpl right, + JoinType joinType, + JoinConditionImpl joinCondition) { + super(resolver); + this.left = left; + this.right = right; + this.joinType = joinType; + this.joinCondition = joinCondition; + } + + public JoinType getJoinTypeInstance() { + return joinType; + } + + /** + * Gets the left node-tuple source. + * + * @return the left source; non-null + */ + public Source getLeft() { + return left; + } + + /** + * Gets the right node-tuple source. + * + * @return the right source; non-null + */ + public Source getRight() { + return right; + } + + /** + * Gets the join type. + * + * @return either
        • {@link QueryObjectModelConstants#JCR_JOIN_TYPE_INNER},
        • + *
        • {@link QueryObjectModelConstants#JCR_JOIN_TYPE_LEFT_OUTER},
        • + *
        • {@link QueryObjectModelConstants#JCR_JOIN_TYPE_RIGHT_OUTER}
        • + *
        + */ + public String getJoinType() { + return joinType.toString(); + } + + /** + * Gets the join condition. + * + * @return the join condition; non-null + */ + public JoinCondition getJoinCondition() { + return joinCondition; + } + + //---------------------------< SourceImpl >--------------------------------- + + /** + * {@inheritDoc} + */ + public SelectorImpl[] getSelectors() { + SelectorImpl[] leftSelectors = left.getSelectors(); + SelectorImpl[] rightSelectors = right.getSelectors(); + SelectorImpl[] both = + new SelectorImpl[leftSelectors.length + rightSelectors.length]; + System.arraycopy(leftSelectors, 0, both, 0, leftSelectors.length); + System.arraycopy(rightSelectors, 0, both, leftSelectors.length, rightSelectors.length); + return both; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return joinType.formatSql(left, right, joinCondition); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LengthImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LengthImpl.java new file mode 100644 index 00000000000..b5f22f926f0 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LengthImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Length; +import javax.jcr.query.qom.PropertyValue; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * LengthImpl... + */ +public class LengthImpl extends DynamicOperandImpl implements Length { + + /** + * Property value for which to compute the length. + */ + private final PropertyValueImpl propertyValue; + + LengthImpl(NamePathResolver resolver, PropertyValueImpl propertyValue) { + super(resolver, propertyValue.getSelectorQName()); + this.propertyValue = propertyValue; + } + + /** + * Gets the property value for which to compute the length. + * + * @return the property value; non-null + */ + public PropertyValue getPropertyValue() { + return propertyValue; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "LENGTH(" + getPropertyValue() + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LiteralImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LiteralImpl.java new file mode 100644 index 00000000000..b2cc6660b1b --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LiteralImpl.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.qom; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.qom.Literal; + +/** + * LiteralImpl... + */ +public class LiteralImpl extends StaticOperandImpl implements Literal { + + private final Value value; + + public LiteralImpl(NamePathResolver resolver, Value value) { + super(resolver); + this.value = value; + } + + /** + * @return the value of this literal. + */ + public Value getLiteralValue() { + return value; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + 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(); + case PropertyType.URI: + return cast("URI"); + case PropertyType.WEAKREFERENCE: + return cast("WEAKREFERENCE"); + default: + return escape(); + } + } catch (RepositoryException e) { + return value.toString(); + } + } + + private String cast(String type) throws RepositoryException { + return "CAST(" + escape() + " AS " + type + ")"; + } + + private String escape() throws RepositoryException { + return "'" + value.getString().replace("'", "''") + "'"; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LowerCaseImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LowerCaseImpl.java new file mode 100644 index 00000000000..34ea17bbbc4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/LowerCaseImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.LowerCase; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * LowerCaseImpl... + */ +public class LowerCaseImpl extends DynamicOperandImpl implements LowerCase { + + /** + * The operand whose value is converted to a lower-case string. + */ + private final DynamicOperandImpl operand; + + LowerCaseImpl(NamePathResolver resolver, DynamicOperandImpl operand) { + super(resolver, operand.getSelectorQName()); + this.operand = operand; + } + + /** + * Gets the operand whose value is converted to a lower-case string. + * + * @return the operand; non-null + */ + public DynamicOperand getOperand() { + return operand; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "LOWER(" + operand + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NodeLocalNameImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NodeLocalNameImpl.java new file mode 100644 index 00000000000..d7151b9493a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NodeLocalNameImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.NodeLocalName; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +/** + * NodeLocalNameImpl... + */ +public class NodeLocalNameImpl + extends DynamicOperandImpl + implements NodeLocalName { + + NodeLocalNameImpl(NamePathResolver resolver, Name selectorName) { + super(resolver, selectorName); + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "LOCALNAME(" + getSelectorName() + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NodeNameImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NodeNameImpl.java new file mode 100644 index 00000000000..e101a402371 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NodeNameImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.NodeName; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +/** + * NodeNameImpl... + */ +public class NodeNameImpl extends DynamicOperandImpl implements NodeName { + + NodeNameImpl(NamePathResolver resolver, Name selectorName) { + super(resolver, selectorName); + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "NAME(" + getSelectorName() + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NotImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NotImpl.java new file mode 100644 index 00000000000..c2cd1d097e5 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/NotImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Not; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * NotImpl... + */ +public class NotImpl extends ConstraintImpl implements Not { + + /** + * The constraint negated by this Not constraint. + */ + private final ConstraintImpl constraint; + + NotImpl(NamePathResolver resolver, ConstraintImpl constraint) { + super(resolver); + this.constraint = constraint; + } + + /** + * Gets the constraint negated by this Not constraint. + * + * @return the constraint; non-null + */ + public Constraint getConstraint() { + return constraint; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "NOT " + protect(constraint); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/OrImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/OrImpl.java new file mode 100644 index 00000000000..2e3696d99d4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/OrImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Constraint; +import javax.jcr.query.qom.Or; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * OrImpl... + */ +public class OrImpl extends ConstraintImpl implements Or { + + /** + * The first constraint. + */ + private final ConstraintImpl constraint1; + + /** + * The second constraint. + */ + private final ConstraintImpl constraint2; + + OrImpl(NamePathResolver resolver, ConstraintImpl c1, ConstraintImpl c2) { + super(resolver); + this.constraint1 = c1; + this.constraint2 = c2; + } + + /** + * Gets the first constraint. + * + * @return the constraint; non-null + */ + public Constraint getConstraint1() { + return constraint1; + } + + /** + * Gets the second constraint. + * + * @return the constraint; non-null + */ + public Constraint getConstraint2() { + return constraint2; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return protect(constraint1) + " OR " + protect(constraint2); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/OrderingImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/OrderingImpl.java new file mode 100644 index 00000000000..3ef10353119 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/OrderingImpl.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.qom; + +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.Ordering; +import javax.jcr.query.qom.QueryObjectModelConstants; + +import org.apache.jackrabbit.commons.query.qom.Order; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * OrderingImpl... + */ +public class OrderingImpl extends AbstractQOMNode implements Ordering { + + /** + * Empty OrderingImpl array. + */ + public static final OrderingImpl[] EMPTY_ARRAY = new OrderingImpl[0]; + + /** + * Operand by which to order. + */ + private final DynamicOperandImpl operand; + + /** + * The order. + */ + private final Order order; + + OrderingImpl(NamePathResolver resolver, + DynamicOperandImpl operand, + String order) { + super(resolver); + this.operand = operand; + this.order = Order.getOrderByName(order); + } + + /** + * The operand by which to order. + * + * @return the operand; non-null + */ + public DynamicOperand getOperand() { + return operand; + } + + /** + * Gets the order. + * + * @return either
        • {@link QueryObjectModelConstants#JCR_ORDER_ASCENDING} + * or
        • {@link QueryObjectModelConstants#JCR_ORDER_DESCENDING}
        • + *
        + */ + public String getOrder() { + return order.getName(); + } + + /** + * @return true if this ordering is ascending. Returns + * false if ordering is descending. + */ + public boolean isAscending() { + return order == Order.ASCENDING; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + if (order == Order.ASCENDING) { + return operand + " ASC"; + } else { + return operand + " DESC"; + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/PropertyExistenceImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/PropertyExistenceImpl.java new file mode 100644 index 00000000000..8e2d2e6cf1f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/PropertyExistenceImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.PropertyExistence; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * PropertyExistenceImpl... + */ +public class PropertyExistenceImpl + extends ConstraintImpl + implements PropertyExistence { + + /** + * The name of the selector against which to apply this constraint. + */ + private final Name selectorName; + + /** + * The name of the property. + */ + private final Name propertyName; + + PropertyExistenceImpl(NamePathResolver resolver, + Name selectorName, + Name propertyName) { + super(resolver); + this.selectorName = selectorName; + this.propertyName = propertyName; + } + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public Name getSelectorQName() { + return selectorName; + } + + /** + * Gets the name of the property. + * + * @return the property name; non-null + */ + public Name getPropertyQName() { + return propertyName; + } + + //------------------------------< PropertyExistence >----------------------- + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public String getSelectorName() { + return getJCRName(selectorName); + } + + /** + * Gets the name of the property. + * + * @return the property name; non-null + */ + public String getPropertyName() { + return getJCRName(propertyName); + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return getSelectorName() + "." + quote(propertyName) + " IS NOT NULL"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/PropertyValueImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/PropertyValueImpl.java new file mode 100644 index 00000000000..0576a1666df --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/PropertyValueImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.PropertyValue; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +/** + * PropertyValueImpl... + */ +public class PropertyValueImpl + extends DynamicOperandImpl + implements PropertyValue { + + /** + * The name of the property. + */ + private final Name propertyName; + + PropertyValueImpl(NamePathResolver resolver, + Name selectorName, + Name propertyName) { + super(resolver, selectorName); + this.propertyName = propertyName; + } + + /** + * Gets the name of the property. + * + * @return the property name; non-null + */ + public Name getPropertyQName() { + return propertyName; + } + + //------------------------------< PropertyValue >--------------------------- + + /** + * Gets the name of the property. + * + * @return the property name; non-null + */ + public String getPropertyName() { + return getJCRName(propertyName); + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return getSelectorName() + "." + quote(propertyName); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QOMTreeVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QOMTreeVisitor.java new file mode 100644 index 00000000000..26885c49eb4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QOMTreeVisitor.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.spi.commons.query.qom; + +/** + * QOMTreeVisitor... + */ +public interface QOMTreeVisitor { + + Object visit(AndImpl node, Object data) throws Exception; + + Object visit(BindVariableValueImpl node, Object data) throws Exception; + + Object visit(ChildNodeImpl node, Object data) throws Exception; + + Object visit(ChildNodeJoinConditionImpl node, Object data) throws Exception; + + Object visit(ColumnImpl node, Object data) throws Exception; + + Object visit(ComparisonImpl node, Object data) throws Exception; + + Object visit(DescendantNodeImpl node, Object data) throws Exception; + + Object visit(DescendantNodeJoinConditionImpl node, Object data) throws Exception; + + Object visit(EquiJoinConditionImpl node, Object data) throws Exception; + + Object visit(FullTextSearchImpl node, Object data) throws Exception; + + Object visit(FullTextSearchScoreImpl node, Object data) throws Exception; + + Object visit(JoinImpl node, Object data) throws Exception; + + Object visit(LengthImpl node, Object data) throws Exception; + + Object visit(LiteralImpl node, Object data) throws Exception; + + Object visit(LowerCaseImpl node, Object data) throws Exception; + + Object visit(NodeLocalNameImpl node, Object data) throws Exception; + + Object visit(NodeNameImpl node, Object data) throws Exception; + + Object visit(NotImpl node, Object data) throws Exception; + + Object visit(OrderingImpl node, Object data) throws Exception; + + Object visit(OrImpl node, Object data) throws Exception; + + Object visit(PropertyExistenceImpl node, Object data) throws Exception; + + Object visit(PropertyValueImpl node, Object data) throws Exception; + + Object visit(QueryObjectModelTree node, Object data) throws Exception; + + Object visit(SameNodeImpl node, Object data) throws Exception; + + Object visit(SameNodeJoinConditionImpl node, Object data) throws Exception; + + Object visit(SelectorImpl node, Object data) throws Exception; + + Object visit(UpperCaseImpl node, Object data) throws Exception; + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QueryObjectModelFactoryImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QueryObjectModelFactoryImpl.java new file mode 100644 index 00000000000..d6f32cc49f0 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QueryObjectModelFactoryImpl.java @@ -0,0 +1,1046 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.qom; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.query.InvalidQueryException; +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.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.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.StaticOperand; +import javax.jcr.query.qom.UpperCase; + +import org.apache.jackrabbit.commons.query.qom.JoinType; +import org.apache.jackrabbit.commons.query.qom.Operator; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * QueryObjectModelFactoryImpl implements the query object model + * factory from JSR 283. + */ +public abstract class QueryObjectModelFactoryImpl implements QueryObjectModelFactory { + + /** + * The name and path resolver for this QOM factory. + */ + private final NamePathResolver resolver; + + public QueryObjectModelFactoryImpl(NamePathResolver resolver) { + this.resolver = resolver; + } + + /** + * Creates a query object model from the internal tree representation. + * + * @param qomTree the qom tree. + * @return a query object model that can be executed. + * @throws InvalidQueryException the the query object model tree is + * considered invalid by the query handler + * implementation. + * @throws RepositoryException if any other error occurs. + */ + protected abstract QueryObjectModel createQuery(QueryObjectModelTree qomTree) + throws InvalidQueryException, RepositoryException; + + /** + * Creates a query with one selector. + *

        + * The specified selector will be the default selector of the query. + * + * @param selector the selector; non-null + * @param constraint the constraint, or null if none + * @param orderings zero or more orderings; null is equivalent to a + * zero-length array + * @param columns the columns; null is equivalent to a zero-length array + * @return the query; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public QueryObjectModel createQuery // CM + (Selector selector, + Constraint constraint, + Ordering[] orderings, + Column[] columns) throws InvalidQueryException, RepositoryException { + return createQuery((Source) selector, constraint, orderings, columns); + } + + /** + * Creates a query with one or more selectors. + *

        + * If source is a selector, that selector is the default + * selector of the query. Otherwise the query does not have a default + * selector. + * + * @param source the node-tuple source; non-null + * @param constraint the constraint, or null if none + * @param orderings zero or more orderings; null is equivalent to a + * zero-length array + * @param columns the columns; null is equivalent to a zero-length array + * @return the query; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public QueryObjectModel createQuery(Source source, + Constraint constraint, + Ordering[] orderings, + Column[] columns) + throws InvalidQueryException, RepositoryException { + if (source == null) { + throw new InvalidQueryException("source must not be null"); + } + if (!(source instanceof SourceImpl)) { + throw new RepositoryException("Unknown Source implementation"); + } + if (constraint != null && !(constraint instanceof ConstraintImpl)) { + throw new RepositoryException("Unknown Constraint implementation"); + } + OrderingImpl[] ords; + if (orderings != null) { + ords = new OrderingImpl[orderings.length]; + for (int i = 0; i < orderings.length; i++) { + if (!(orderings[i] instanceof OrderingImpl)) { + throw new RepositoryException("Unknown Ordering implementation"); + } + ords[i] = (OrderingImpl) orderings[i]; + } + } else { + ords = OrderingImpl.EMPTY_ARRAY; + } + ColumnImpl[] cols; + if (columns != null) { + cols = new ColumnImpl[columns.length]; + for (int i = 0; i < columns.length; i++) { + if (!(columns[i] instanceof ColumnImpl)) { + throw new RepositoryException("Unknown Column implementation"); + } + cols[i] = (ColumnImpl) columns[i]; + } + } else { + cols = ColumnImpl.EMPTY_ARRAY; + } + QueryObjectModelTree qomTree = new QueryObjectModelTree( + resolver, (SourceImpl) source, + (ConstraintImpl) constraint, ords, cols); + return createQuery(qomTree); + } + + /** + * Selects a subset of the nodes in the repository based on node type. + *

        + * The selector name is the node type name. + * + * @param nodeTypeName the name of the required node type; non-null + * @return the selector; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Selector selector(String nodeTypeName) // CM + throws InvalidQueryException, RepositoryException { + Name ntName = checkNodeTypeName(nodeTypeName); + return new SelectorImpl(resolver, ntName, ntName); + } + + /** + * Selects a subset of the nodes in the repository based on node type. + * + * @param nodeTypeName the name of the required node type; non-null + * @param selectorName the selector name; non-null + * @return the selector; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Selector selector(String nodeTypeName, String selectorName) + throws InvalidQueryException, RepositoryException { + return new SelectorImpl(resolver, checkNodeTypeName(nodeTypeName), + checkSelectorName(selectorName)); + } + + /** + * Performs a join between two node-tuple sources. + * + * @param left the left node-tuple source; non-null + * @param right the right node-tuple source; non-null + * @param joinTypeName either

        • {@link QueryObjectModelConstants#JCR_JOIN_TYPE_INNER},
        • + *
        • {@link QueryObjectModelConstants#JCR_JOIN_TYPE_LEFT_OUTER},
        • + *
        • {@link QueryObjectModelConstants#JCR_JOIN_TYPE_RIGHT_OUTER}
        • + *
        + * @param joinCondition the join condition; non-null + * @return the join; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Join join(Source left, + Source right, + String joinTypeName, + JoinCondition joinCondition) + throws InvalidQueryException, RepositoryException { + if (!(left instanceof SourceImpl) || !(right instanceof SourceImpl)) { + throw new RepositoryException("Unknown Source implementation"); + } + if (!(joinCondition instanceof JoinConditionImpl)) { + throw new RepositoryException("Unknown JoinCondition implementation"); + } + return new JoinImpl( + resolver, + (SourceImpl) left, + (SourceImpl) right, + JoinType.getJoinTypeByName(joinTypeName), + (JoinConditionImpl) joinCondition); + } + + /** + * Tests whether the value of a property in a first selector is equal to the + * value of a property in a second selector. + * + * @param selector1Name the name of the first selector; non-null + * @param property1Name the property name in the first selector; non-null + * @param selector2Name the name of the second selector; non-null + * @param property2Name the property name in the second selector; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public EquiJoinCondition equiJoinCondition(String selector1Name, + String property1Name, + String selector2Name, + String property2Name) + throws InvalidQueryException, RepositoryException { + return new EquiJoinConditionImpl(resolver, + checkSelectorName(selector1Name), + checkPropertyName(property1Name), + checkSelectorName(selector2Name), + checkPropertyName(property2Name)); + } + + /** + * Tests whether a first selector's node is the same as a second selector's + * node. + * + * @param selector1Name the name of the first selector; non-null + * @param selector2Name the name of the second selector; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public SameNodeJoinCondition sameNodeJoinCondition(String selector1Name, + String selector2Name) + throws InvalidQueryException, RepositoryException // CM + { + return new SameNodeJoinConditionImpl(resolver, + checkSelectorName(selector1Name), + checkSelectorName(selector2Name), + null); + } + + /** + * Tests whether a first selector's node is the same as a node identified by + * relative path from a second selector's node. + * + * @param selector1Name the name of the first selector; non-null + * @param selector2Name the name of the second selector; non-null + * @param selector2Path the path relative to the second selector; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public SameNodeJoinCondition sameNodeJoinCondition(String selector1Name, + String selector2Name, + String selector2Path) + throws InvalidQueryException, RepositoryException { + return new SameNodeJoinConditionImpl(resolver, + checkSelectorName(selector1Name), + checkSelectorName(selector2Name), + checkPath(selector2Path)); + } + + /** + * Tests whether a first selector's node is a child of a second selector's + * node. + * + * @param childSelectorName the name of the child selector; non-null + * @param parentSelectorName the name of the parent selector; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public ChildNodeJoinCondition childNodeJoinCondition( + String childSelectorName, String parentSelectorName) + throws InvalidQueryException, RepositoryException { + return new ChildNodeJoinConditionImpl(resolver, + checkSelectorName(childSelectorName), + checkSelectorName(parentSelectorName)); + } + + /** + * Tests whether a first selector's node is a descendant of a second + * selector's node. + * + * @param descendantSelectorName the name of the descendant selector; + * non-null + * @param ancestorSelectorName the name of the ancestor selector; + * non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public DescendantNodeJoinCondition descendantNodeJoinCondition( + String descendantSelectorName, String ancestorSelectorName) + throws InvalidQueryException, RepositoryException { + return new DescendantNodeJoinConditionImpl(resolver, + checkSelectorName(descendantSelectorName), + checkSelectorName(ancestorSelectorName)); + } + + /** + * Performs a logical conjunction of two other constraints. + * + * @param constraint1 the first constraint; non-null + * @param constraint2 the second constraint; non-null + * @return the And constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public And and(Constraint constraint1, Constraint constraint2) + throws InvalidQueryException, RepositoryException { + if (constraint1 == null || constraint2 == null) { + throw new InvalidQueryException("Constraints must not be null"); + } + if (constraint1 instanceof ConstraintImpl + && constraint2 instanceof ConstraintImpl) { + return new AndImpl(resolver, + (ConstraintImpl) constraint1, + (ConstraintImpl) constraint2); + } else { + throw new RepositoryException("Unknown constraint implementation"); + } + } + + /** + * Performs a logical disjunction of two other constraints. + * + * @param constraint1 the first constraint; non-null + * @param constraint2 the second constraint; non-null + * @return the Or constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Or or(Constraint constraint1, Constraint constraint2) + throws InvalidQueryException, RepositoryException { + if (constraint1 == null || constraint2 == null) { + throw new InvalidQueryException("Constraints must not be null"); + } + if (constraint1 instanceof ConstraintImpl + && constraint2 instanceof ConstraintImpl) { + return new OrImpl(resolver, + (ConstraintImpl) constraint1, + (ConstraintImpl) constraint2); + } else { + throw new RepositoryException("Unknown constraint implementation"); + } + } + + /** + * Performs a logical negation of another constraint. + * + * @param constraint the constraint to be negated; non-null + * @return the Not constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Not not(Constraint constraint) throws InvalidQueryException, RepositoryException { + if (!(constraint instanceof ConstraintImpl)) { + throw new RepositoryException("Unknown Constraint implementation"); + } + return new NotImpl(resolver, (ConstraintImpl) constraint); + } + + /** + * Filters node-tuples based on the outcome of a binary operation. + * + * @param left the first operand; non-null + * @param operatorName the operator; either
        • {@link #JCR_OPERATOR_EQUAL_TO},
        • + *
        • {@link #JCR_OPERATOR_NOT_EQUAL_TO},
        • {@link + * #JCR_OPERATOR_LESS_THAN},
        • {@link #JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO},
        • + *
        • {@link #JCR_OPERATOR_GREATER_THAN},
        • {@link + * #JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO}, or
        • {@link + * #JCR_OPERATOR_LIKE}
        + * @param right the second operand; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Comparison comparison( + DynamicOperand left, String operatorName, StaticOperand right) + throws InvalidQueryException, RepositoryException { + if (!(left instanceof DynamicOperandImpl)) { + throw new RepositoryException("Invalid left operand: " + left); + } + if (!(right instanceof StaticOperandImpl)) { + throw new RepositoryException("Invalid right operand: " + right); + } + + return new ComparisonImpl( + resolver, + (DynamicOperandImpl) left, + Operator.getOperatorByName(operatorName), + (StaticOperandImpl) right); + } + + /** + * Tests the existence of a property in the default selector. + * + * @param propertyName the property name; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public PropertyExistence propertyExistence(String propertyName) // CM + throws InvalidQueryException, RepositoryException { + return new PropertyExistenceImpl( + resolver, null, checkPropertyName(propertyName)); + } + + /** + * Tests the existence of a property in the specified selector. + * + * @param selectorName the selector name; non-null + * @param propertyName the property name; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public PropertyExistence propertyExistence(String selectorName, + String propertyName) + throws InvalidQueryException, RepositoryException { + return new PropertyExistenceImpl(resolver, + checkSelectorName(selectorName), + checkPropertyName(propertyName)); + } + + /** + * Performs a full-text search against the default selector. + * + * @param propertyName the property name, or null to search all + * full-text indexed properties of the node + * (or node subtree, in some implementations) + * @param fullTextSearchExpression the full-text search expression; + * non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public FullTextSearch fullTextSearch(String propertyName, + StaticOperand fullTextSearchExpression) + throws InvalidQueryException, RepositoryException // CM + { + Name propName = null; + if (propertyName != null) { + propName = checkPropertyName(propertyName); + } + return new FullTextSearchImpl(resolver, null, propName, + checkFullTextSearchExpression(fullTextSearchExpression)); + } + + /** + * Performs a full-text search against the specified selector. + * + * @param selectorName the selector name; non-null + * @param propertyName the property name, or null to search all + * full-text indexed properties of the node + * (or node subtree, in some implementations) + * @param fullTextSearchExpression the full-text search expression; + * non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public FullTextSearch fullTextSearch(String selectorName, + String propertyName, + StaticOperand fullTextSearchExpression) + throws InvalidQueryException, RepositoryException { + if (fullTextSearchExpression == null) { + throw new IllegalArgumentException( + "Full text search expression is null"); + } + Name propName = null; + if (propertyName != null) { + propName = checkPropertyName(propertyName); + } + return new FullTextSearchImpl(resolver, + checkSelectorName(selectorName), propName, + checkFullTextSearchExpression(fullTextSearchExpression)); + } + + /** + * Tests whether a node in the default selector is reachable by a specified + * absolute path. + * + * @param path an absolute path; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public SameNode sameNode(String path) throws InvalidQueryException, RepositoryException // CM + { + return new SameNodeImpl(resolver, null, checkPath(path)); + } + + /** + * Tests whether a node in the specified selector is reachable by a + * specified absolute path. + * + * @param selectorName the selector name; non-null + * @param path an absolute path; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public SameNode sameNode(String selectorName, String path) + throws InvalidQueryException, RepositoryException { + return new SameNodeImpl( + resolver, checkSelectorName(selectorName), checkPath(path)); + } + + /** + * Tests whether a node in the default selector is a child of a node + * reachable by a specified absolute path. + * + * @param path an absolute path; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public ChildNode childNode(String path) throws InvalidQueryException, RepositoryException // CM + { + return new ChildNodeImpl(resolver, null, checkPath(path)); + } + + /** + * Tests whether a node in the specified selector is a child of a node + * reachable by a specified absolute path. + * + * @param selectorName the selector name; non-null + * @param path an absolute path; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public ChildNode childNode(String selectorName, String path) + throws InvalidQueryException, RepositoryException { + return new ChildNodeImpl( + resolver, checkSelectorName(selectorName), checkPath(path)); + } + + /** + * Tests whether a node in the default selector is a descendant of a node + * reachable by a specified absolute path. + * + * @param path an absolute path; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public DescendantNode descendantNode(String path) + throws InvalidQueryException, RepositoryException { + return new DescendantNodeImpl(resolver, null, checkPath(path)); + } + + /** + * Tests whether a node in the specified selector is a descendant of a node + * reachable by a specified absolute path. + * + * @param selectorName the selector name; non-null + * @param path an absolute path; non-null + * @return the constraint; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public DescendantNode descendantNode(String selectorName, String path) + throws InvalidQueryException, RepositoryException { + return new DescendantNodeImpl(resolver, + checkSelectorName(selectorName), checkPath(path)); + } + + /** + * Evaluates to the value (or values, if multi-valued) of a property of the + * default selector. + * + * @param propertyName the property name; non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public PropertyValue propertyValue(String propertyName) // CM + throws InvalidQueryException, RepositoryException { + return new PropertyValueImpl(resolver, null, checkPropertyName(propertyName)); + } + + /** + * Evaluates to the value (or values, if multi-valued) of a property in the + * specified selector. + * + * @param selectorName the selector name; non-null + * @param propertyName the property name; non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public PropertyValue propertyValue(String selectorName, + String propertyName) + throws InvalidQueryException, RepositoryException { + return new PropertyValueImpl(resolver, + checkSelectorName(selectorName), + checkPropertyName(propertyName)); + } + + /** + * Evaluates to the length (or lengths, if multi-valued) of a property. + * + * @param propertyValue the property value for which to compute the length; + * non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Length length(PropertyValue propertyValue) + throws InvalidQueryException, RepositoryException { + if (!(propertyValue instanceof PropertyValueImpl)) { + throw new RepositoryException("Unknown PropertyValue implementation"); + } + return new LengthImpl(resolver, (PropertyValueImpl) propertyValue); + } + + /** + * Evaluates to a NAME value equal to the prefix-qualified name + * of a node in the default selector. + * + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public NodeName nodeName() // CM + throws InvalidQueryException, RepositoryException { + return new NodeNameImpl(resolver, null); + } + + /** + * Evaluates to a NAME value equal to the prefix-qualified name + * of a node in the specified selector. + * + * @param selectorName the selector name; non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public NodeName nodeName(String selectorName) throws InvalidQueryException, RepositoryException { + return new NodeNameImpl(resolver, checkSelectorName(selectorName)); + } + + /** + * Evaluates to a NAME value equal to the local (unprefixed) + * name of a node in the default selector. + * + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public NodeLocalName nodeLocalName() // CM + throws InvalidQueryException, RepositoryException { + return new NodeLocalNameImpl(resolver, null); + } + + /** + * Evaluates to a NAME value equal to the local (unprefixed) + * name of a node in the specified selector. + * + * @param selectorName the selector name; non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public NodeLocalName nodeLocalName(String selectorName) + throws InvalidQueryException, RepositoryException { + return new NodeLocalNameImpl(resolver, checkSelectorName(selectorName)); + } + + /** + * Evaluates to a DOUBLE value equal to the full-text search + * score of a node in the default selector. + * + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public FullTextSearchScore fullTextSearchScore() // CM + throws InvalidQueryException, RepositoryException { + return new FullTextSearchScoreImpl(resolver, null); + } + + /** + * Evaluates to a DOUBLE value equal to the full-text search + * score of a node in the specified selector. + * + * @param selectorName the selector name; non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public FullTextSearchScore fullTextSearchScore(String selectorName) + throws InvalidQueryException, RepositoryException { + return new FullTextSearchScoreImpl( + resolver, checkSelectorName(selectorName)); + } + + /** + * Evaluates to the lower-case string value (or values, if multi-valued) of + * an operand. + * + * @param operand the operand whose value is converted to a lower-case + * string; non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public LowerCase lowerCase(DynamicOperand operand) + throws InvalidQueryException, RepositoryException { + if (!(operand instanceof DynamicOperandImpl)) { + throw new RepositoryException("Unknown DynamicOperand implementation"); + } + return new LowerCaseImpl(resolver, (DynamicOperandImpl) operand); + } + + /** + * Evaluates to the upper-case string value (or values, if multi-valued) of + * an operand. + * + * @param operand the operand whose value is converted to a upper-case + * string; non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public UpperCase upperCase(DynamicOperand operand) + throws InvalidQueryException, RepositoryException { + if (!(operand instanceof DynamicOperandImpl)) { + throw new RepositoryException("Unknown DynamicOperand implementation"); + } + return new UpperCaseImpl(resolver, (DynamicOperandImpl) operand); + } + + /** + * Evaluates to the value of a bind variable. + * + * @param bindVariableName the bind variable name; non-null + * @return the operand; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public BindVariableValue bindVariable(String bindVariableName) + throws InvalidQueryException, RepositoryException { + if (bindVariableName == null) { + throw new InvalidQueryException("bindVariableName must not be null"); + } + try { + return new BindVariableValueImpl( + resolver, resolver.getQName(bindVariableName)); + } catch (NameException e) { + throw new InvalidQueryException(e.getMessage()); + } + } + + /** + * Evaluates to a literal value. + * + * @param value a JCR value; non-null + * @return the operand; non-null + * @throws InvalidQueryException if the query is invalid + * @throws RepositoryException if the operation otherwise fails + */ + public Literal literal(Value value) + throws InvalidQueryException, RepositoryException { + if (value == null) { + throw new InvalidQueryException("value must not be null"); + } + return new LiteralImpl(resolver, value); + } + + /** + * Orders by the value of the specified operand, in ascending order. + * + * @param operand the operand by which to order; non-null + * @return the ordering + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Ordering ascending(DynamicOperand operand) + throws InvalidQueryException, RepositoryException { + if (!(operand instanceof DynamicOperandImpl)) { + throw new RepositoryException("Unknown DynamicOperand implementation"); + } + return new OrderingImpl(resolver, (DynamicOperandImpl) operand, + QueryObjectModelConstants.JCR_ORDER_ASCENDING); + } + + /** + * Orders by the value of the specified operand, in descending order. + * + * @param operand the operand by which to order; non-null + * @return the ordering + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Ordering descending(DynamicOperand operand) + throws InvalidQueryException, RepositoryException { + if (!(operand instanceof DynamicOperandImpl)) { + throw new RepositoryException("Unknown DynamicOperand implementation"); + } + return new OrderingImpl(resolver, (DynamicOperandImpl) operand, + QueryObjectModelConstants.JCR_ORDER_DESCENDING); + } + + /** + * Identifies a property in the default selector to include in the tabular + * view of query results. + *

        + * The column name is the property name. + * + * @param propertyName the property name, or null to include a column for + * each single-value non-residual property of the + * selector's node type + * @return the column; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Column column(String propertyName) // CM + throws InvalidQueryException, RepositoryException { + Name propName = null; + if (propertyName != null) { + try { + propName = resolver.getQName(propertyName); + } catch (NameException e) { + throw new InvalidQueryException(e.getMessage()); + } + } + return new ColumnImpl(resolver, null, propName, propertyName); + } + + /** + * Identifies a property in the default selector to include in the tabular + * view of query results. + * + * @param propertyName the property name, or null to include a column for + * each single-value non-residual property of the + * selector's node type + * @param columnName the column name; must be null if propertyName + * is null + * @return the column; non-null + * @throws javax.jcr.query.InvalidQueryException + * if the query has no default + * selector or is otherwise invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Column column(String propertyName, String columnName) // CM + throws InvalidQueryException, RepositoryException { + if (propertyName == null && columnName != null) { + throw new InvalidQueryException( + "columnName must be null if propertyName is null"); + } + Name propName = null; + if (propertyName != null) { + try { + propName = resolver.getQName(propertyName); + } catch (NameException e) { + throw new InvalidQueryException(e.getMessage()); + } + } + return new ColumnImpl(resolver, null, propName, columnName); + } + + /** + * Identifies a property in the specified selector to include in the tabular + * view of query results. + * + * @param selectorName the selector name; non-null + * @param propertyName the property name, or null to include a column for + * each single-value non-residual property of the + * selector's node type + * @param columnName the column name; if null, defaults to + * propertyName; must be null if + * propertyName is null + * @throws javax.jcr.query.InvalidQueryException + * if the query is invalid + * @throws javax.jcr.RepositoryException if the operation otherwise fails + */ + public Column column(String selectorName, + String propertyName, + String columnName) throws InvalidQueryException, RepositoryException { + if (propertyName == null && columnName != null) { + throw new InvalidQueryException( + "columnName must be null if propertyName is null"); + } + Name propName = null; + if (propertyName != null) { + try { + propName = resolver.getQName(propertyName); + } catch (NameException e) { + throw new InvalidQueryException(e.getMessage()); + } + } + return new ColumnImpl(resolver, checkSelectorName(selectorName), + propName, columnName); + } + + //------------------------------< internal >-------------------------------- + + private Name checkSelectorName(String selectorName) + throws RepositoryException { + if (selectorName == null) { + throw new InvalidQueryException("selectorName must not be null"); + } + try { + return resolver.getQName(selectorName); + } catch (NameException e) { + throw new InvalidQueryException(e.getMessage()); + } + } + + private Name checkNodeTypeName(String nodeTypeName) + throws RepositoryException { + if (nodeTypeName == null) { + throw new InvalidQueryException("nodeTypeName must not be null"); + } + try { + return resolver.getQName(nodeTypeName); + } catch (NameException e) { + throw new InvalidQueryException(e.getMessage()); + } + } + + private Path checkPath(String path) throws RepositoryException { + if (path == null) { + throw new InvalidQueryException("path must not be null"); + } + try { + return resolver.getQPath(path); + } catch (NameException e) { + throw new InvalidQueryException(e.getMessage()); + } + } + + private Name checkPropertyName(String propertyName) + throws RepositoryException { + if (propertyName == null) { + throw new InvalidQueryException("propertyName must not be null"); + } + try { + return resolver.getQName(propertyName); + } catch (NameException e) { + throw new InvalidQueryException(e.getMessage()); + } + } + + private StaticOperand checkFullTextSearchExpression(StaticOperand fullTextSearchExpression) + throws RepositoryException { + if (fullTextSearchExpression == null) { + throw new InvalidQueryException( + "fullTextSearchExpression must not be null"); + } + return fullTextSearchExpression; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QueryObjectModelTree.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QueryObjectModelTree.java new file mode 100644 index 00000000000..e583a3d26a8 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/QueryObjectModelTree.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.qom; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +import javax.jcr.query.InvalidQueryException; + +import java.util.Map; +import java.util.HashMap; +import java.util.Arrays; +import java.util.Iterator; + +/** + * QueryObjectModelTree implements the root node of an object + * query model tree. + */ +public class QueryObjectModelTree extends AbstractQOMNode { + + /** + * The node-tuple source for this query. + */ + private final SourceImpl source; + + /** + * The constraint for this query. + */ + private final ConstraintImpl constraint; + + /** + * The orderings for this query. + */ + private final OrderingImpl[] orderings; + + /** + * The columns for this query. + */ + private final ColumnImpl[] columns; + + /** + * All selectors available in this query object model. Key=Name + */ + private final Map selectors = new HashMap(); + + public QueryObjectModelTree(NamePathResolver resolver, + SourceImpl source, + ConstraintImpl constraint, + OrderingImpl[] orderings, + ColumnImpl[] columns) + throws InvalidQueryException { + super(resolver); + this.source = source; + this.constraint = constraint; + this.orderings = orderings; + this.columns = columns; + for (Iterator it = Arrays.asList(source.getSelectors()).iterator(); it.hasNext(); ) { + SelectorImpl selector = (SelectorImpl) it.next(); + if (selectors.put(selector.getSelectorQName(), selector) != null) { + throw new InvalidQueryException("Duplicate selector name: " + + selector.getSelectorName()); + } + } + if (selectors.size() == 1) { + // there is only one selector, which is also a default selector + selectors.put(null, selectors.values().iterator().next()); + } + checkQuery(); + } + + /** + * Gets the node-tuple source for this query. + * + * @return the node-tuple source; non-null + */ + public SourceImpl getSource() { + return source; + } + + /** + * Gets the constraint for this query. + * + * @return the constraint, or null if none + */ + public ConstraintImpl getConstraint() { + return constraint; + } + + /** + * Gets the orderings for this query. + * + * @return an array of zero or more orderings; non-null + */ + public OrderingImpl[] getOrderings() { + OrderingImpl[] temp = new OrderingImpl[orderings.length]; + System.arraycopy(orderings, 0, temp, 0, orderings.length); + return temp; + } + + /** + * Gets the columns for this query. + * + * @return an array of zero or more columns; non-null + */ + public ColumnImpl[] getColumns() { + ColumnImpl[] temp = new ColumnImpl[columns.length]; + System.arraycopy(columns, 0, temp, 0, columns.length); + return temp; + } + + /** + * Returns the selector with the given name or + * null if there is no selector with this name. + * + * @param name the name of a selector. + * @return the selector or null if there is no such selector. + */ + public SelectorImpl getSelector(Name name) { + return (SelectorImpl) selectors.get(name); + } + + //-----------------------< AbstractQOMNode >-------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + /** + * Checks if this QOM is valid. + * + * @throws InvalidQueryException if the QOM is invalid. + */ + private void checkQuery() throws InvalidQueryException { + // TODO: validate query completely. + // checks currently implemented: + // - check for selector names + try { + accept(new DefaultTraversingQOMTreeVisitor() { + public Object visit(ChildNodeImpl node, Object data) throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(ColumnImpl node, Object data) throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(DescendantNodeImpl node, Object data) throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(EquiJoinConditionImpl node, Object data) + throws Exception { + checkSelector(node.getSelector1QName()); + return checkSelector(node.getSelector2QName()); + } + + public Object visit(FullTextSearchImpl node, Object data) throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(FullTextSearchScoreImpl node, Object data) + throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(NodeLocalNameImpl node, Object data) throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(NodeNameImpl node, Object data) throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(PropertyExistenceImpl node, Object data) + throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(PropertyValueImpl node, Object data) throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(SameNodeImpl node, Object data) throws Exception { + return checkSelector(node.getSelectorQName()); + } + + public Object visit(SameNodeJoinConditionImpl node, Object data) + throws Exception { + checkSelector(node.getSelector1QName()); + return checkSelector(node.getSelector2QName()); + } + + private Object checkSelector(Name selectorName) + throws InvalidQueryException { + if (!selectors.containsKey(selectorName)) { + String msg = "Unknown selector: "; + if (selectorName != null) { + msg += QueryObjectModelTree.this.getJCRName(selectorName); + } else { + msg += ""; + } + throw new InvalidQueryException(msg); + } + return null; + } + }, null); + } catch (Exception e) { + throw new InvalidQueryException(e.getMessage()); + } + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + StringBuilder builder = new StringBuilder(); + + builder.append("SELECT "); + if (columns != null && columns.length > 0) { + for (int i = 0; i < columns.length; i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(columns[i]); + } + } else { + builder.append("*"); + } + + builder.append(" FROM "); + builder.append(source); + + if (constraint != null) { + builder.append(" WHERE "); + builder.append(constraint); + } + + if (orderings != null && orderings.length > 0) { + builder.append(" ORDER BY "); + for (int i = 0; i < orderings.length; i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(orderings[i]); + } + } + + return builder.toString(); + } + + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SameNodeImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SameNodeImpl.java new file mode 100644 index 00000000000..24c3fc0da52 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SameNodeImpl.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.qom; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.qom.SameNode; +import javax.jcr.RepositoryException; + +/** + * SameNodeImpl... + */ +public class SameNodeImpl extends ConstraintImpl implements SameNode { + + /** + * The name of a selector. + */ + private final Name selectorName; + + /** + * An absolute path. + */ + private final Path path; + + SameNodeImpl(NamePathResolver resolver, + Name selectorName, + Path path) throws InvalidQueryException, RepositoryException { + super(resolver); + this.selectorName = selectorName; + this.path = path; + if (!path.isAbsolute()) { + throw new InvalidQueryException(resolver.getJCRPath(path) + + " is not an absolute path"); + } + } + + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public String getSelectorName() { + return getJCRName(selectorName); + } + + /** + * Gets the absolute path. + * + * @return the path; non-null + */ + public String getPath() { + return getJCRPath(path); + } + + /** + * Gets the name of the selector against which to apply this constraint. + * + * @return the selector name; non-null + */ + public Name getSelectorQName() { + return selectorName; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "ISSAMENODE(" + getSelectorName() + ", " + quote(path) + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SameNodeJoinConditionImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SameNodeJoinConditionImpl.java new file mode 100644 index 00000000000..2ea62e48a61 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SameNodeJoinConditionImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.SameNodeJoinCondition; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * SameNodeJoinConditionImpl... + */ +public class SameNodeJoinConditionImpl + extends JoinConditionImpl + implements SameNodeJoinCondition { + + /** + * The name of the first selector. + */ + private final Name selector1Name; + + /** + * The name of the second selector. + */ + private final Name selector2Name; + + /** + * The path relative to the second selector. + */ + private final Path selector2Path; + + SameNodeJoinConditionImpl(NamePathResolver resolver, + Name selector1Name, + Name selector2Name, + Path selector2Path) { + super(resolver); + this.selector1Name = selector1Name; + this.selector2Name = selector2Name; + this.selector2Path = selector2Path; + } + + /** + * Gets the name of the first selector. + * + * @return the selector name; non-null + */ + public String getSelector1Name() { + return getJCRName(selector1Name); + } + + /** + * Gets the name of the second selector. + * + * @return the selector name; non-null + */ + public String getSelector2Name() { + return getJCRName(selector2Name); + } + + /** + * Gets the path relative to the second selector. + * + * @return the relative path, or null for none + */ + public String getSelector2Path() { + return getJCRPath(selector2Path); + } + + /** + * Gets the name of the first selector. + * + * @return the selector name; non-null + */ + public Name getSelector1QName() { + return selector1Name; + } + + /** + * Gets the name of the second selector. + * + * @return the selector name; non-null + */ + public Name getSelector2QName() { + return selector2Name; + } + + /** + * Gets the path relative to the second selector. + * + * @return the relative path, or null for none + */ + public Path getSelector2QPath() { + return selector2Path; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ISSAMENODE("); + builder.append(getSelector1Name()); + builder.append(", "); + builder.append(getSelector2Name()); + if (selector2Path != null) { + builder.append(", "); + builder.append(quote(selector2Path)); + } + builder.append(")"); + return builder.toString(); + } + + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SelectorImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SelectorImpl.java new file mode 100644 index 00000000000..14356973ce3 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SelectorImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Selector; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; + +/** + * SelectorImpl... + */ +public class SelectorImpl extends SourceImpl implements Selector { + + /** + * The name of the required node type. + */ + private final Name nodeTypeName; + + /** + * The selector name. + */ + private final Name selectorName; + + SelectorImpl(NamePathResolver resolver, + Name nodeTypeName, + Name selectorName) { + super(resolver); + this.nodeTypeName = nodeTypeName; + this.selectorName = selectorName; + } + + /** + * Gets the name of the required node type. + * + * @return the node type name; non-null + */ + public Name getNodeTypeQName() { + return nodeTypeName; + } + + /** + * Gets the selector name. + *

        + * A selector's name can be used elsewhere in the query to identify the + * selector. + * + * @return the selector name; non-null + */ + public Name getSelectorQName() { + return selectorName; + } + + //---------------------------< SourceImpl >--------------------------------- + + /** + * {@inheritDoc} + */ + public SelectorImpl[] getSelectors() { + return new SelectorImpl[]{this}; + } + + //-----------------------------< Selector >--------------------------------- + + /** + * Gets the name of the required node type. + * + * @return the node type name; non-null + */ + public String getNodeTypeName() { + return getJCRName(nodeTypeName); + } + + /** + * Gets the selector name. + *

        + * A selector's name can be used elsewhere in the query to identify the + * selector. + * + * @return the selector name; non-null + */ + public String getSelectorName() { + return getJCRName(selectorName); + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return quote(nodeTypeName) + " AS " + getSelectorName(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SourceImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SourceImpl.java new file mode 100644 index 00000000000..4b0e4d97d2c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/SourceImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.Source; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * SourceImpl... + */ +public abstract class SourceImpl extends AbstractQOMNode implements Source { + + public SourceImpl(NamePathResolver resolver) { + super(resolver); + } + + /** + * @return the selectors that are contained in this source. + */ + public abstract SelectorImpl[] getSelectors(); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/StaticOperandImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/StaticOperandImpl.java new file mode 100644 index 00000000000..3f2976fce20 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/StaticOperandImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.StaticOperand; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * StaticOperandImpl... + */ +public abstract class StaticOperandImpl + extends AbstractQOMNode + implements StaticOperand { + + public StaticOperandImpl(NamePathResolver resolver) { + super(resolver); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/UpperCaseImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/UpperCaseImpl.java new file mode 100644 index 00000000000..84d44e344d6 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/UpperCaseImpl.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.spi.commons.query.qom; + +import javax.jcr.query.qom.DynamicOperand; +import javax.jcr.query.qom.UpperCase; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * UpperCaseImpl... + */ +public class UpperCaseImpl extends DynamicOperandImpl implements UpperCase { + + /** + * The operand whose value is converted to a upper-case string. + */ + private final DynamicOperandImpl operand; + + UpperCaseImpl(NamePathResolver resolver, DynamicOperandImpl operand) { + super(resolver, operand.getSelectorQName()); + this.operand = operand; + } + + /** + * Gets the operand whose value is converted to a upper-case string. + * + * @return the operand; non-null + */ + public DynamicOperand getOperand() { + return operand; + } + + //------------------------< AbstractQOMNode >------------------------------- + + /** + * Accepts a visitor and calls the appropriate visit method + * depending on the type of this QOM node. + * + * @param visitor the visitor. + */ + public Object accept(QOMTreeVisitor visitor, Object data) throws Exception { + return visitor.visit(this, data); + } + + //------------------------< Object >---------------------------------------- + + public String toString() { + return "UPPER(" + operand + ")"; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/package-info.java new file mode 100644 index 00000000000..14de38ccc00 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/qom/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.query.qom; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTAndExpression.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTAndExpression.java new file mode 100644 index 00000000000..dc69dc01656 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTAndExpression.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTAndExpression.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTAndExpression extends SimpleNode { + public ASTAndExpression(int id) { + super(id); + } + + public ASTAndExpression(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTAscendingOrderSpec.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTAscendingOrderSpec.java new file mode 100644 index 00000000000..4f238efd8a6 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTAscendingOrderSpec.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTAscendingOrderSpec.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTAscendingOrderSpec extends SimpleNode { + public ASTAscendingOrderSpec(int id) { + super(id); + } + + public ASTAscendingOrderSpec(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTBracketExpression.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTBracketExpression.java new file mode 100644 index 00000000000..5dd502ad9d8 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTBracketExpression.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTBracketExpression.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTBracketExpression extends SimpleNode { + public ASTBracketExpression(int id) { + super(id); + } + + public ASTBracketExpression(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTContainsExpression.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTContainsExpression.java new file mode 100644 index 00000000000..b72d1d37ab3 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTContainsExpression.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.spi.commons.query.sql; + +import org.apache.jackrabbit.spi.Name; + +public class ASTContainsExpression extends SimpleNode { + + private String query; + + private Name property; + + public ASTContainsExpression(int id) { + super(id); + } + + public ASTContainsExpression(JCRSQLParser p, int id) { + super(p, id); + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Name getPropertyName() { + return property; + } + + public void setPropertyName(Name property) { + this.property = property; + } + + /** + * Accept the visitor. * + */ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTDescendingOrderSpec.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTDescendingOrderSpec.java new file mode 100644 index 00000000000..7e88d21cb62 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTDescendingOrderSpec.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTDescendingOrderSpec.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTDescendingOrderSpec extends SimpleNode { + public ASTDescendingOrderSpec(int id) { + super(id); + } + + public ASTDescendingOrderSpec(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTExcerptFunction.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTExcerptFunction.java new file mode 100644 index 00000000000..e1f3eb80485 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTExcerptFunction.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTExcerptFunction.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTExcerptFunction extends SimpleNode { + public ASTExcerptFunction(int id) { + super(id); + } + + public ASTExcerptFunction(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTFromClause.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTFromClause.java new file mode 100644 index 00000000000..d72d8436733 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTFromClause.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTFromClause.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTFromClause extends SimpleNode { + public ASTFromClause(int id) { + super(id); + } + + public ASTFromClause(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTIdentifier.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTIdentifier.java new file mode 100644 index 00000000000..ac2cf7f158f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTIdentifier.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.spi.commons.query.sql; + +import org.apache.jackrabbit.spi.Name; + +public class ASTIdentifier extends SimpleNode { + + private Name name; + + public ASTIdentifier(int id) { + super(id); + } + + public ASTIdentifier(JCRSQLParser p, int id) { + super(p, id); + } + + public void setName(Name name) { + this.name = name; + } + + public Name getName() { + return name; + } + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + public String toString() { + return super.toString() + ": " + name; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTLiteral.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTLiteral.java new file mode 100644 index 00000000000..1ff1b0c223a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTLiteral.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.spi.commons.query.sql; + +public class ASTLiteral extends SimpleNode { + + private String value; + + private int type; + + + public ASTLiteral(int id) { + super(id); + } + + public ASTLiteral(JCRSQLParser p, int id) { + super(p, id); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + + /** + * Accept the visitor. * + */ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + public String toString() { + return super.toString() + ": " + value + " type:" + type; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTLowerFunction.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTLowerFunction.java new file mode 100644 index 00000000000..c4a5a838794 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTLowerFunction.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTLowerFunction.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTLowerFunction extends SimpleNode { + public ASTLowerFunction(int id) { + super(id); + } + + public ASTLowerFunction(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTNotExpression.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTNotExpression.java new file mode 100644 index 00000000000..15bc8a66347 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTNotExpression.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTNotExpression.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTNotExpression extends SimpleNode { + public ASTNotExpression(int id) { + super(id); + } + + public ASTNotExpression(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrExpression.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrExpression.java new file mode 100644 index 00000000000..6d6232d5e24 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrExpression.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTOrExpression.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTOrExpression extends SimpleNode { + public ASTOrExpression(int id) { + super(id); + } + + public ASTOrExpression(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrderByClause.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrderByClause.java new file mode 100644 index 00000000000..7033b6524d1 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrderByClause.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTOrderByClause.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTOrderByClause extends SimpleNode { + public ASTOrderByClause(int id) { + super(id); + } + + public ASTOrderByClause(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrderSpec.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrderSpec.java new file mode 100644 index 00000000000..2a66d59617d --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTOrderSpec.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTOrderSpec.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTOrderSpec extends SimpleNode { + public ASTOrderSpec(int id) { + super(id); + } + + public ASTOrderSpec(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTPredicate.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTPredicate.java new file mode 100644 index 00000000000..9c7d9d506d2 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTPredicate.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.spi.commons.query.sql; + +import org.apache.jackrabbit.spi.Name; + +public class ASTPredicate extends SimpleNode { + + private int operationType; + + private boolean negate = false; + + private Name identifier; + + private String identifierOperand; + + private String escapeString; + + public ASTPredicate(int id) { + super(id); + } + + public ASTPredicate(JCRSQLParser p, int id) { + super(p, id); + } + + public void setOperationType(int type) { + this.operationType = type; + } + + public int getOperationType() { + return operationType; + } + + public void setNegate(boolean b) { + this.negate = b; + } + + public boolean isNegate() { + return this.negate; + } + + public void setIdentifier(Name identifier) { + this.identifier = identifier; + } + + public Name getIdentifier() { + return identifier; + } + + public void setIdentifierOperand(String identifier) { + this.identifierOperand = identifier; + } + + public String getIdentifierOperand() { + return identifierOperand; + } + + public void setEscapeString(String esc) { + this.escapeString = esc; + } + + public String getEscapeString() { + return escapeString; + } + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + public String toString() { + return super.toString() + " type: " + operationType + " negate: " + negate; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTQuery.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTQuery.java new file mode 100644 index 00000000000..101d9cf4ada --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTQuery.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTQuery.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTQuery extends SimpleNode { + public ASTQuery(int id) { + super(id); + } + + public ASTQuery(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTSelectList.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTSelectList.java new file mode 100644 index 00000000000..3e28028e01e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTSelectList.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTSelectList.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTSelectList extends SimpleNode { + public ASTSelectList(int id) { + super(id); + } + + public ASTSelectList(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTUpperFunction.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTUpperFunction.java new file mode 100644 index 00000000000..cfd16e722dd --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTUpperFunction.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTUpperFunction.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTUpperFunction extends SimpleNode { + public ASTUpperFunction(int id) { + super(id); + } + + public ASTUpperFunction(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTWhereClause.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTWhereClause.java new file mode 100644 index 00000000000..5c21f7eca6c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ASTWhereClause.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTWhereClause.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class ASTWhereClause extends SimpleNode { + public ASTWhereClause(int id) { + super(id); + } + + public ASTWhereClause(JCRSQLParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/DefaultParserVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/DefaultParserVisitor.java new file mode 100644 index 00000000000..7efca4d6623 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/DefaultParserVisitor.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.sql; + +/** + * Implements a {@link JCRSQLParserVisitor} with default method implementations. + * All visit method simply return the data parameter. + */ +class DefaultParserVisitor implements JCRSQLParserVisitor { + + public Object visit(SimpleNode node, Object data) { + return data; + } + + public Object visit(ASTQuery node, Object data) { + return data; + } + + public Object visit(ASTSelectList node, Object data) { + return data; + } + + public Object visit(ASTFromClause node, Object data) { + return data; + } + + public Object visit(ASTWhereClause node, Object data) { + return data; + } + + public Object visit(ASTPredicate node, Object data) { + return data; + } + + public Object visit(ASTOrExpression node, Object data) { + return data; + } + + public Object visit(ASTAndExpression node, Object data) { + return data; + } + + public Object visit(ASTNotExpression node, Object data) { + return data; + } + + public Object visit(ASTBracketExpression node, Object data) { + return data; + } + + public Object visit(ASTLiteral node, Object data) { + return data; + } + + public Object visit(ASTIdentifier node, Object data) { + return data; + } + + public Object visit(ASTOrderByClause node, Object data) { + return data; + } + + public Object visit(ASTContainsExpression node, Object data) { + return data; + } + + public Object visit(ASTOrderSpec node, Object data) { + return data; + } + + public Object visit(ASTAscendingOrderSpec node, Object data) { + return data; + } + + public Object visit(ASTDescendingOrderSpec node, Object data) { + return data; + } + + public Object visit(ASTLowerFunction node, Object data) { + return data; + } + + public Object visit(ASTUpperFunction node, Object data) { + return data; + } + + public Object visit(ASTExcerptFunction node, Object data) { + return data; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParser.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParser.java new file mode 100644 index 00000000000..8fc749dcd04 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParser.java @@ -0,0 +1,1533 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. JCRSQLParser.java */ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.sql; + +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.query.QueryConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.Name; + +import javax.jcr.NamespaceException; + +public class JCRSQLParser/*@bgen(jjtree)*/implements JCRSQLParserTreeConstants, JCRSQLParserConstants {/*@bgen(jjtree)*/ + protected JJTJCRSQLParserState jjtree = new JJTJCRSQLParserState(); + private String statement; + + private NameResolver resolver; + + public static void main(String args[]) throws ParseException { + JCRSQLParser parser = new JCRSQLParser(System.in); + parser.Query().dump(""); + } + + public static ASTQuery parse(String statement, NameResolver resolver) throws ParseException { + java.io.StringReader sReader = new java.io.StringReader(statement); + JCRSQLParser parser = new JCRSQLParser(sReader); + parser.setNameResolver(resolver); + return parser.Query(); + } + + void setNameResolver(NameResolver resolver) { + this.resolver = resolver; + } + +// QUERY EXPRESSIONS, 7 + final public ASTQuery Query() throws ParseException { + /*@bgen(jjtree) Query */ + ASTQuery jjtn000 = new ASTQuery(JJTQUERY); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(SELECT); + SelectList(); + TableExpression(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ORDER: + OrderByClause(); + break; + default: + jj_la1[0] = jj_gen; + ; + } + jj_consume_token(0); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + {if (true) return jjtn000;} + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + throw new Error("Missing return statement in function"); + } + + final public void SelectList() throws ParseException { + /*@bgen(jjtree) SelectList */ + ASTSelectList jjtn000 = new ASTSelectList(JJTSELECTLIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ASTERISK: + jj_consume_token(ASTERISK); + break; + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case ORDER: + case WHERE: + case SELECT: + case BETWEEN: + case EXCERPT: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + SelectItem(); + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case COMMA: + ; + break; + default: + jj_la1[1] = jj_gen; + break label_1; + } + jj_consume_token(COMMA); + SelectItem(); + } + break; + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void SelectItem() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EXCERPT: + ExcerptFunction(); + break; + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case ORDER: + case WHERE: + case SELECT: + case BETWEEN: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + Identifier(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PERIOD: + jj_consume_token(PERIOD); + Identifier(); + Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); + break; + default: + jj_la1[3] = jj_gen; + ; + } + break; + default: + jj_la1[4] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void TableExpression() throws ParseException { + FromClause(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case WHERE: + WhereClause(); + break; + default: + jj_la1[5] = jj_gen; + ; + } + } + + final public void FromClause() throws ParseException { + /*@bgen(jjtree) FromClause */ + ASTFromClause jjtn000 = new ASTFromClause(JJTFROMCLAUSE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(FROM); + Identifier(); + label_2: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case COMMA: + ; + break; + default: + jj_la1[6] = jj_gen; + break label_2; + } + jj_consume_token(COMMA); + Identifier(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void WhereClause() throws ParseException { + /*@bgen(jjtree) WhereClause */ + ASTWhereClause jjtn000 = new ASTWhereClause(JJTWHERECLAUSE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(WHERE); + SearchCondition(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + +// PREDICATES 8.1 + final public void Predicate() throws ParseException { + /*@bgen(jjtree) Predicate */ + ASTPredicate jjtn000 = new ASTPredicate(JJTPREDICATE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);int operationType; + Name identifier; + String value; + String escapeString; + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case LOWER: + case ORDER: + case UPPER: + case WHERE: + case SELECT: + case BETWEEN: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case ORDER: + case WHERE: + case SELECT: + case BETWEEN: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + identifier = Identifier(); + jjtn000.setIdentifier(identifier); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PERIOD: + jj_consume_token(PERIOD); + identifier = Identifier(); + Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); jjtn000.setIdentifier(identifier); + break; + default: + jj_la1[7] = jj_gen; + ; + } + break; + case LOWER: + case UPPER: + identifier = PropertyFunction(); + jjtn000.setIdentifier(identifier); + break; + default: + jj_la1[8] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case LT_OPERATOR: + case EQ_OPERATOR: + case GT_OPERATOR: + case NE_OPERATOR: + case GE_OPERATOR: + case LE_OPERATOR: + operationType = ComparisonOperation(); + jjtn000.setOperationType(operationType); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EXACT_NUMERIC_LITERAL: + case APPROXIMATE_NUMERIC_LITERAL: + case DATETIME_LITERAL: + case CHAR_STRING_LITERAL: + Literal(); + break; + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case ORDER: + case WHERE: + case SELECT: + case BETWEEN: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + identifier = Identifier(); + jjtn000.setIdentifier(identifier); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PERIOD: + jj_consume_token(PERIOD); + identifier = Identifier(); + Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); jjtn000.setIdentifier(identifier); + break; + default: + jj_la1[9] = jj_gen; + ; + } + break; + default: + jj_la1[10] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + case NOT: + case LIKE: + case BETWEEN: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NOT: + jj_consume_token(NOT); + jjtn000.setNegate(true); + break; + default: + jj_la1[11] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case BETWEEN: + jj_consume_token(BETWEEN); + jjtn000.setOperationType(QueryConstants.OPERATION_BETWEEN); + Literal(); + jj_consume_token(AND); + Literal(); + break; + case LIKE: + jj_consume_token(LIKE); + jjtn000.setOperationType(QueryConstants.OPERATION_LIKE); + value = CharStringLiteral(); + ASTLiteral s = new ASTLiteral(JJTLITERAL); + s.setType(QueryConstants.TYPE_STRING); + s.setValue(value); + jjtree.pushNode(s); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ESCAPE: + jj_consume_token(ESCAPE); + escapeString = CharStringLiteral(); + jjtn000.setEscapeString(escapeString); + break; + default: + jj_la1[12] = jj_gen; + ; + } + break; + default: + jj_la1[13] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + case IS: + jj_consume_token(IS); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NOT: + jj_consume_token(NOT); + jjtn000.setNegate(true); + break; + default: + jj_la1[14] = jj_gen; + ; + } + jj_consume_token(NULL); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + jjtn000.setOperationType(jjtn000.isNegate() ? QueryConstants.OPERATION_NOT_NULL : QueryConstants.OPERATION_NULL); + break; + default: + jj_la1[15] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + case EXACT_NUMERIC_LITERAL: + case APPROXIMATE_NUMERIC_LITERAL: + case DATETIME_LITERAL: + case CHAR_STRING_LITERAL: + Literal(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NOT: + jj_consume_token(NOT); + jjtn000.setNegate(true); + break; + default: + jj_la1[16] = jj_gen; + ; + } + jj_consume_token(IN); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case ORDER: + case WHERE: + case SELECT: + case BETWEEN: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + identifier = Identifier(); + break; + case LOWER: + case UPPER: + identifier = PropertyFunction(); + break; + default: + jj_la1[17] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + jjtn000.setIdentifier(identifier); + jjtn000.setOperationType(jjtn000.isNegate() ? QueryConstants.OPERATION_NE_GENERAL : QueryConstants.OPERATION_EQ_GENERAL); + break; + case SIMILAR: + jj_consume_token(SIMILAR); + jj_consume_token(LEFT_PAREN); + jjtn000.setOperationType(QueryConstants.OPERATION_SIMILAR); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PERIOD: + jj_consume_token(PERIOD); + break; + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case ORDER: + case WHERE: + case SELECT: + case BETWEEN: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + identifier = Identifier(); + jjtn000.setIdentifier(identifier); + break; + default: + jj_la1[18] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(COMMA); + value = CharStringLiteral(); + ASTLiteral s = new ASTLiteral(JJTLITERAL); + s.setType(QueryConstants.TYPE_STRING); + s.setValue(value); + jjtree.pushNode(s); + jj_consume_token(RIGHT_PAREN); + break; + case SPELLCHECK: + jj_consume_token(SPELLCHECK); + jj_consume_token(LEFT_PAREN); + jjtn000.setOperationType(QueryConstants.OPERATION_SPELLCHECK); + value = CharStringLiteral(); + ASTLiteral stmt = new ASTLiteral(JJTLITERAL); + stmt.setType(QueryConstants.TYPE_STRING); + stmt.setValue(value); + jjtree.pushNode(stmt); + jj_consume_token(RIGHT_PAREN); + break; + default: + jj_la1[19] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public Name PropertyFunction() throws ParseException { + Name identifier; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case LOWER: + identifier = LowerFunction(); + break; + case UPPER: + identifier = UpperFunction(); + break; + default: + jj_la1[20] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return identifier;} + throw new Error("Missing return statement in function"); + } + + final public Name LowerFunction() throws ParseException { + /*@bgen(jjtree) LowerFunction */ + ASTLowerFunction jjtn000 = new ASTLowerFunction(JJTLOWERFUNCTION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Name identifier; + try { + jj_consume_token(LOWER); + jj_consume_token(LEFT_PAREN); + identifier = Identifier(); + jj_consume_token(RIGHT_PAREN); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + {if (true) return identifier;} + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + throw new Error("Missing return statement in function"); + } + + final public Name UpperFunction() throws ParseException { + /*@bgen(jjtree) UpperFunction */ + ASTUpperFunction jjtn000 = new ASTUpperFunction(JJTUPPERFUNCTION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Name identifier; + try { + jj_consume_token(UPPER); + jj_consume_token(LEFT_PAREN); + identifier = Identifier(); + jj_consume_token(RIGHT_PAREN); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + {if (true) return identifier;} + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + throw new Error("Missing return statement in function"); + } + + final public int ComparisonOperation() throws ParseException { + int operationType; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EQ_OPERATOR: + jj_consume_token(EQ_OPERATOR); + operationType = QueryConstants.OPERATION_EQ_GENERAL; + break; + case NE_OPERATOR: + jj_consume_token(NE_OPERATOR); + operationType = QueryConstants.OPERATION_NE_GENERAL; + break; + case LT_OPERATOR: + jj_consume_token(LT_OPERATOR); + operationType = QueryConstants.OPERATION_LT_GENERAL; + break; + case GT_OPERATOR: + jj_consume_token(GT_OPERATOR); + operationType = QueryConstants.OPERATION_GT_GENERAL; + break; + case LE_OPERATOR: + jj_consume_token(LE_OPERATOR); + operationType = QueryConstants.OPERATION_LE_GENERAL; + break; + case GE_OPERATOR: + jj_consume_token(GE_OPERATOR); + operationType = QueryConstants.OPERATION_GE_GENERAL; + break; + default: + jj_la1[21] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return operationType;} + throw new Error("Missing return statement in function"); + } + +// SEARCH CONDITION 8.12 + final public void SearchCondition() throws ParseException { + OrExpression(); + } + + final public void OrExpression() throws ParseException { + ASTOrExpression jjtn001 = new ASTOrExpression(JJTOREXPRESSION); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + AndExpression(); + label_3: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OR: + ; + break; + default: + jj_la1[22] = jj_gen; + break label_3; + } + jj_consume_token(OR); + AndExpression(); + } + } catch (Throwable jjte001) { + if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte001;} + } + if (jjte001 instanceof ParseException) { + {if (true) throw (ParseException)jjte001;} + } + {if (true) throw (Error)jjte001;} + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, jjtree.nodeArity() > 1); + } + } + } + + final public void AndExpression() throws ParseException { + ASTAndExpression jjtn001 = new ASTAndExpression(JJTANDEXPRESSION); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + UnaryExpression(); + label_4: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AND: + ; + break; + default: + jj_la1[23] = jj_gen; + break label_4; + } + jj_consume_token(AND); + UnaryExpression(); + } + } catch (Throwable jjte001) { + if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte001;} + } + if (jjte001 instanceof ParseException) { + {if (true) throw (ParseException)jjte001;} + } + {if (true) throw (Error)jjte001;} + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, jjtree.nodeArity() > 1); + } + } + } + + final public void UnaryExpression() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NOT: + ASTNotExpression jjtn001 = new ASTNotExpression(JJTNOTEXPRESSION); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jj_consume_token(NOT); + UnaryExpression(); + } catch (Throwable jjte001) { + if (jjtc001) { + jjtree.clearNodeScope(jjtn001); + jjtc001 = false; + } else { + jjtree.popNode(); + } + if (jjte001 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte001;} + } + if (jjte001 instanceof ParseException) { + {if (true) throw (ParseException)jjte001;} + } + {if (true) throw (Error)jjte001;} + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case LOWER: + case ORDER: + case UPPER: + case WHERE: + case SELECT: + case BETWEEN: + case SIMILAR: + case CONTAINS: + case SPELLCHECK: + case LEFT_PAREN: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + case EXACT_NUMERIC_LITERAL: + case APPROXIMATE_NUMERIC_LITERAL: + case DATETIME_LITERAL: + case CHAR_STRING_LITERAL: + PrimaryExpression(); + break; + default: + jj_la1[24] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void PrimaryExpression() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case LOWER: + case ORDER: + case UPPER: + case WHERE: + case SELECT: + case BETWEEN: + case SIMILAR: + case SPELLCHECK: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + case EXACT_NUMERIC_LITERAL: + case APPROXIMATE_NUMERIC_LITERAL: + case DATETIME_LITERAL: + case CHAR_STRING_LITERAL: + Predicate(); + break; + case LEFT_PAREN: + BracketExpression(); + break; + case CONTAINS: + ContainsExpression(); + break; + default: + jj_la1[25] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void BracketExpression() throws ParseException { + /*@bgen(jjtree) BracketExpression */ + ASTBracketExpression jjtn000 = new ASTBracketExpression(JJTBRACKETEXPRESSION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(LEFT_PAREN); + SearchCondition(); + jj_consume_token(RIGHT_PAREN); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ContainsExpression() throws ParseException { + /*@bgen(jjtree) ContainsExpression */ + ASTContainsExpression jjtn000 = new ASTContainsExpression(JJTCONTAINSEXPRESSION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + Name name = null; + try { + jj_consume_token(CONTAINS); + jj_consume_token(LEFT_PAREN); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ASTERISK: + jj_consume_token(ASTERISK); + break; + case PERIOD: + jj_consume_token(PERIOD); + break; + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case ORDER: + case WHERE: + case SELECT: + case BETWEEN: + case REGULAR_IDENTIFIER: + case DELIMITED_IDENTIFIER: + name = Identifier(); + jjtn000.setPropertyName(name); + break; + default: + jj_la1[26] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(COMMA); + t = jj_consume_token(CHAR_STRING_LITERAL); + jjtn000.setQuery(t.image.substring(1, t.image.length() - 1).replaceAll("''", "'")); + jj_consume_token(RIGHT_PAREN); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Literal() throws ParseException { + /*@bgen(jjtree) Literal */ + ASTLiteral jjtn000 = new ASTLiteral(JJTLITERAL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + String value; + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case APPROXIMATE_NUMERIC_LITERAL: + t = jj_consume_token(APPROXIMATE_NUMERIC_LITERAL); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + jjtn000.setType(QueryConstants.TYPE_DOUBLE); jjtn000.setValue(t.image); + break; + case EXACT_NUMERIC_LITERAL: + // can contain a dot -> use double + t = jj_consume_token(EXACT_NUMERIC_LITERAL); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + if (t.image.indexOf('.') > -1) { + jjtn000.setType(QueryConstants.TYPE_DOUBLE); + } else { + jjtn000.setType(QueryConstants.TYPE_LONG); + } + jjtn000.setValue(t.image); + break; + case CHAR_STRING_LITERAL: + value = CharStringLiteral(); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + jjtn000.setType(QueryConstants.TYPE_STRING); jjtn000.setValue(value); + break; + case DATETIME_LITERAL: + t = jj_consume_token(DATETIME_LITERAL); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + if (t.image.startsWith("TIMESTAMP")) { + jjtn000.setValue(t.image.substring(t.image.indexOf('\'') + 1, t.image.length() - 1)); + if (jjtn000.getValue().indexOf(" ") == 10) { + // replace SQL 92 timesamp string with ISO8601 + StringBuffer tmp = new StringBuffer(); + tmp.append(jjtn000.getValue().substring(0, 10)); + tmp.append("T").append(jjtn000.getValue().substring(11)); + jjtn000.setValue(tmp.toString()); + } + jjtn000.setType(QueryConstants.TYPE_TIMESTAMP); + /* + } else if (t.image.startsWith("TIME")) { + jjtThis.setValue(t.image.substring(t.image.indexOf('\'') + 1, t.image.length() - 1)); + jjtThis.setType(QueryConstants.TYPE_TIME); + */ + } else { + jjtn000.setValue(t.image.substring(t.image.indexOf('\'') + 1, t.image.length() - 1)); + jjtn000.setType(QueryConstants.TYPE_DATE); + } + break; + default: + jj_la1[27] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public String CharStringLiteral() throws ParseException { + Token t; + String value = ""; + t = jj_consume_token(CHAR_STRING_LITERAL); + value += t.image.substring(1, t.image.length() - 1); + label_5: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CHAR_STRING_LITERAL: + ; + break; + default: + jj_la1[28] = jj_gen; + break label_5; + } + t = jj_consume_token(CHAR_STRING_LITERAL); + value += t.image.substring(1, t.image.length() - 1); + } + // strip any quote escapes + {if (true) return value.replaceAll("''", "'");} + throw new Error("Missing return statement in function"); + } + + final public Name Identifier() throws ParseException { + /*@bgen(jjtree) Identifier */ + ASTIdentifier jjtn000 = new ASTIdentifier(JJTIDENTIFIER); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);Token t = null; + Name name = null; + boolean pseudoProperty = false; + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case REGULAR_IDENTIFIER: + t = jj_consume_token(REGULAR_IDENTIFIER); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case LEFT_PAREN: + jj_consume_token(LEFT_PAREN); + jj_consume_token(RIGHT_PAREN); + pseudoProperty = true; + break; + default: + jj_la1[29] = jj_gen; + ; + } + try { + String jcrName = t.image; + if (pseudoProperty) { + jcrName += "()"; + } + jjtn000.setName(resolver.getQName(jcrName)); + } catch (NameException e) { + {if (true) throw new ParseException(e.getMessage());} + } catch (NamespaceException e) { + {if (true) throw new ParseException(e.getMessage());} + } + break; + case DELIMITED_IDENTIFIER: + t = jj_consume_token(DELIMITED_IDENTIFIER); + try { + jjtn000.setName(resolver.getQName(t.image.substring(1, t.image.length()-1))); + } catch (NameException e) { + {if (true) throw new ParseException(e.getMessage());} + } catch (NamespaceException e) { + {if (true) throw new ParseException(e.getMessage());} + } + break; + case BY: + case IN: + case OR: + case IS: + case AND: + case LIKE: + case NULL: + case FROM: + case ORDER: + case WHERE: + case SELECT: + case BETWEEN: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case BY: + // or any keyword + t = jj_consume_token(BY); + break; + case IN: + t = jj_consume_token(IN); + break; + case OR: + t = jj_consume_token(OR); + break; + case IS: + t = jj_consume_token(IS); + break; + case AND: + t = jj_consume_token(AND); + break; + case LIKE: + t = jj_consume_token(LIKE); + break; + case NULL: + t = jj_consume_token(NULL); + break; + case FROM: + t = jj_consume_token(FROM); + break; + case ORDER: + t = jj_consume_token(ORDER); + break; + case WHERE: + t = jj_consume_token(WHERE); + break; + case SELECT: + t = jj_consume_token(SELECT); + break; + case BETWEEN: + t = jj_consume_token(BETWEEN); + break; + default: + jj_la1[30] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + try { + jjtn000.setName(resolver.getQName(t.image)); + } catch (NameException e) { + {if (true) throw new ParseException(e.getMessage());} + } catch (NamespaceException e) { + {if (true) throw new ParseException(e.getMessage());} + } + break; + default: + jj_la1[31] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + {if (true) return jjtn000.getName();} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + throw new Error("Missing return statement in function"); + } + + final public Name ExcerptFunction() throws ParseException { + /*@bgen(jjtree) ExcerptFunction */ + ASTExcerptFunction jjtn000 = new ASTExcerptFunction(JJTEXCERPTFUNCTION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(EXCERPT); + jj_consume_token(LEFT_PAREN); + jj_consume_token(PERIOD); + jj_consume_token(RIGHT_PAREN); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + {if (true) return NameFactoryImpl.getInstance().create(Name.NS_REP_URI, "excerpt(.)");} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + throw new Error("Missing return statement in function"); + } + + final public void OrderByClause() throws ParseException { + /*@bgen(jjtree) OrderByClause */ + ASTOrderByClause jjtn000 = new ASTOrderByClause(JJTORDERBYCLAUSE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(ORDER); + jj_consume_token(BY); + OrderSpec(); + label_6: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case COMMA: + ; + break; + default: + jj_la1[32] = jj_gen; + break label_6; + } + jj_consume_token(COMMA); + OrderSpec(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void OrderSpec() throws ParseException { + /*@bgen(jjtree) OrderSpec */ + ASTOrderSpec jjtn000 = new ASTOrderSpec(JJTORDERSPEC); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + Identifier(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ASC: + case DESC: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ASC: + AscendingOrderSpec(); + break; + case DESC: + DescendingOrderSpec(); + break; + default: + jj_la1[33] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[34] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void AscendingOrderSpec() throws ParseException { + /*@bgen(jjtree) AscendingOrderSpec */ + ASTAscendingOrderSpec jjtn000 = new ASTAscendingOrderSpec(JJTASCENDINGORDERSPEC); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(ASC); + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DescendingOrderSpec() throws ParseException { + /*@bgen(jjtree) DescendingOrderSpec */ + ASTDescendingOrderSpec jjtn000 = new ASTDescendingOrderSpec(JJTDESCENDINGORDERSPEC); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DESC); + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + public JCRSQLParserTokenManager token_source; + SimpleCharStream jj_input_stream; + public Token token, jj_nt; + private int jj_ntk; + private int jj_gen; + final private int[] jj_la1 = new int[35]; + static private int[] jj_la1_0; + static private int[] jj_la1_1; + static private int[] jj_la1_2; + static private int[] jj_la1_3; + static { + jj_la1_0(); + jj_la1_1(); + jj_la1_2(); + jj_la1_3(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x200000,0x0,0xeae3e00,0x0,0xeae3e00,0x800000,0x0,0x0,0x6fe3e00,0x0,0x6ae3e00,0x8000,0x1000000,0x4020000,0x8000,0x4029000,0x8000,0x6fe3e00,0x6ae3e00,0x56fe3e00,0x500000,0x0,0x800,0x2000,0x76febe00,0x76fe3e00,0x6ae3e00,0x0,0x0,0x0,0x6ae3e00,0x6ae3e00,0x0,0x14000,0x14000,}; + } + private static void jj_la1_1() { + jj_la1_1 = new int[] {0x0,0x4000,0x10001000,0x10000,0x10000000,0x0,0x4000,0x10000,0x10000000,0x10000,0x10000000,0x0,0x0,0x0,0x0,0x700000,0x0,0x10000000,0x10010000,0x10000000,0x0,0x700000,0x0,0x0,0x10000400,0x10000400,0x10011000,0x0,0x0,0x400,0x0,0x10000000,0x4000,0x0,0x0,}; + } + private static void jj_la1_2() { + jj_la1_2 = new int[] {0x0,0x0,0x1,0x0,0x1,0x0,0x0,0x0,0x1,0x0,0x21401,0x0,0x0,0x0,0x0,0xe0,0x0,0x1,0x1,0x21401,0x0,0xe0,0x0,0x0,0x21401,0x21401,0x1,0x21400,0x0,0x0,0x0,0x1,0x0,0x0,0x0,}; + } + private static void jj_la1_3() { + jj_la1_3 = new int[] {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x4,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x4,0x0,0x0,0x0,0x0,0x4,0x4,0x0,0x4,0x4,0x0,0x0,0x0,0x0,0x0,0x0,}; + } + + public JCRSQLParser(java.io.InputStream stream) { + this(stream, null); + } + public JCRSQLParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new JCRSQLParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 35; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 35; i++) jj_la1[i] = -1; + } + + public JCRSQLParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new JCRSQLParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 35; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 35; i++) jj_la1[i] = -1; + } + + public JCRSQLParser(JCRSQLParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 35; i++) jj_la1[i] = -1; + } + + public void ReInit(JCRSQLParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 35; i++) jj_la1[i] = -1; + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.Vector jj_expentries = new java.util.Vector(); + private int[] jj_expentry; + private int jj_kind = -1; + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[101]; + for (int i = 0; i < 101; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 35; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1<", + "\" \"", + "\"\\r\"", + "\"\\n\"", + "\"\\t\"", + "", + "", + "", + "", + "\"BY\"", + "\"IN\"", + "\"OR\"", + "\"IS\"", + "\"AND\"", + "\"ASC\"", + "\"NOT\"", + "\"DESC\"", + "\"LIKE\"", + "\"NULL\"", + "\"FROM\"", + "\"LOWER\"", + "\"ORDER\"", + "\"UPPER\"", + "\"WHERE\"", + "\"ESCAPE\"", + "\"SELECT\"", + "\"BETWEEN\"", + "\"EXCERPT\"", + "\"SIMILAR\"", + "\"CONTAINS\"", + "\"SPELLCHECK\"", + "", + "", + "", + "", + "", + "", + "", + "\"\\\"\"", + "\"%\"", + "\"&\"", + "\"\\\'\"", + "\"(\"", + "\")\"", + "\"*\"", + "\"+\"", + "\",\"", + "\"-\"", + "\".\"", + "\"/\"", + "\":\"", + "\";\"", + "\"<\"", + "\"=\"", + "\">\"", + "\"?\"", + "\"_\"", + "\"|\"", + "\"[\"", + "\"]\"", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "\"<>\"", + "\">=\"", + "\"<=\"", + "\"||\"", + "\"..\"", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + }; + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserTokenManager.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserTokenManager.java new file mode 100644 index 00000000000..e8c04042d0c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserTokenManager.java @@ -0,0 +1,1712 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. JCRSQLParserTokenManager.java */ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.sql; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.query.QueryConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.Name; +import javax.jcr.NamespaceException; + +public class JCRSQLParserTokenManager implements JCRSQLParserConstants +{ + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 9: + jjmatchedKind = 4; + return jjMoveNfa_0(0, 0); + case 10: + jjmatchedKind = 3; + return jjMoveNfa_0(0, 0); + case 13: + jjmatchedKind = 2; + return jjMoveNfa_0(0, 0); + case 32: + jjmatchedKind = 1; + return jjMoveNfa_0(0, 0); + case 34: + jjmatchedKind = 38; + return jjMoveNfa_0(0, 0); + case 37: + jjmatchedKind = 39; + return jjMoveNfa_0(0, 0); + case 38: + jjmatchedKind = 40; + return jjMoveNfa_0(0, 0); + case 39: + jjmatchedKind = 41; + return jjMoveNfa_0(0, 0); + case 40: + jjmatchedKind = 42; + return jjMoveNfa_0(0, 0); + case 41: + jjmatchedKind = 43; + return jjMoveNfa_0(0, 0); + case 42: + jjmatchedKind = 44; + return jjMoveNfa_0(0, 0); + case 43: + jjmatchedKind = 45; + return jjMoveNfa_0(0, 0); + case 44: + jjmatchedKind = 46; + return jjMoveNfa_0(0, 0); + case 45: + jjmatchedKind = 47; + return jjMoveNfa_0(0, 0); + case 46: + jjmatchedKind = 48; + return jjMoveStringLiteralDfa1_0(0x0L, 0x200L); + case 47: + jjmatchedKind = 49; + return jjMoveNfa_0(0, 0); + case 58: + jjmatchedKind = 50; + return jjMoveNfa_0(0, 0); + case 59: + jjmatchedKind = 51; + return jjMoveNfa_0(0, 0); + case 60: + jjmatchedKind = 52; + return jjMoveStringLiteralDfa1_0(0x0L, 0xa0L); + case 61: + jjmatchedKind = 53; + return jjMoveNfa_0(0, 0); + case 62: + jjmatchedKind = 54; + return jjMoveStringLiteralDfa1_0(0x0L, 0x40L); + case 63: + jjmatchedKind = 55; + return jjMoveNfa_0(0, 0); + case 65: + return jjMoveStringLiteralDfa1_0(0x6000L, 0x0L); + case 66: + return jjMoveStringLiteralDfa1_0(0x4000200L, 0x0L); + case 67: + return jjMoveStringLiteralDfa1_0(0x20000000L, 0x0L); + case 68: + return jjMoveStringLiteralDfa1_0(0x10000L, 0x0L); + case 69: + return jjMoveStringLiteralDfa1_0(0x9000000L, 0x0L); + case 70: + return jjMoveStringLiteralDfa1_0(0x80000L, 0x0L); + case 73: + return jjMoveStringLiteralDfa1_0(0x1400L, 0x0L); + case 76: + return jjMoveStringLiteralDfa1_0(0x120000L, 0x0L); + case 78: + return jjMoveStringLiteralDfa1_0(0x48000L, 0x0L); + case 79: + return jjMoveStringLiteralDfa1_0(0x200800L, 0x0L); + case 83: + return jjMoveStringLiteralDfa1_0(0x52000000L, 0x0L); + case 85: + return jjMoveStringLiteralDfa1_0(0x400000L, 0x0L); + case 87: + return jjMoveStringLiteralDfa1_0(0x800000L, 0x0L); + case 91: + jjmatchedKind = 58; + return jjMoveNfa_0(0, 0); + case 93: + jjmatchedKind = 59; + return jjMoveNfa_0(0, 0); + case 95: + jjmatchedKind = 56; + return jjMoveNfa_0(0, 0); + case 97: + return jjMoveStringLiteralDfa1_0(0x6000L, 0x0L); + case 98: + return jjMoveStringLiteralDfa1_0(0x4000200L, 0x0L); + case 99: + return jjMoveStringLiteralDfa1_0(0x20000000L, 0x0L); + case 100: + return jjMoveStringLiteralDfa1_0(0x10000L, 0x0L); + case 101: + return jjMoveStringLiteralDfa1_0(0x9000000L, 0x0L); + case 102: + return jjMoveStringLiteralDfa1_0(0x80000L, 0x0L); + case 105: + return jjMoveStringLiteralDfa1_0(0x1400L, 0x0L); + case 108: + return jjMoveStringLiteralDfa1_0(0x120000L, 0x0L); + case 110: + return jjMoveStringLiteralDfa1_0(0x48000L, 0x0L); + case 111: + return jjMoveStringLiteralDfa1_0(0x200800L, 0x0L); + case 115: + return jjMoveStringLiteralDfa1_0(0x52000000L, 0x0L); + case 117: + return jjMoveStringLiteralDfa1_0(0x400000L, 0x0L); + case 119: + return jjMoveStringLiteralDfa1_0(0x800000L, 0x0L); + case 124: + jjmatchedKind = 57; + return jjMoveStringLiteralDfa1_0(0x0L, 0x100L); + default : + return jjMoveNfa_0(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_0(long active0, long active1) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 0); + } + switch(curChar) + { + case 46: + if ((active1 & 0x200L) != 0L) + { + jjmatchedKind = 73; + jjmatchedPos = 1; + } + break; + case 61: + if ((active1 & 0x40L) != 0L) + { + jjmatchedKind = 70; + jjmatchedPos = 1; + } + else if ((active1 & 0x80L) != 0L) + { + jjmatchedKind = 71; + jjmatchedPos = 1; + } + break; + case 62: + if ((active1 & 0x20L) != 0L) + { + jjmatchedKind = 69; + jjmatchedPos = 1; + } + break; + case 69: + return jjMoveStringLiteralDfa2_0(active0, 0x6010000L, active1, 0L); + case 72: + return jjMoveStringLiteralDfa2_0(active0, 0x800000L, active1, 0L); + case 73: + return jjMoveStringLiteralDfa2_0(active0, 0x10020000L, active1, 0L); + case 78: + if ((active0 & 0x400L) != 0L) + { + jjmatchedKind = 10; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x2000L, active1, 0L); + case 79: + return jjMoveStringLiteralDfa2_0(active0, 0x20108000L, active1, 0L); + case 80: + return jjMoveStringLiteralDfa2_0(active0, 0x40400000L, active1, 0L); + case 82: + if ((active0 & 0x800L) != 0L) + { + jjmatchedKind = 11; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x280000L, active1, 0L); + case 83: + if ((active0 & 0x1000L) != 0L) + { + jjmatchedKind = 12; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x1004000L, active1, 0L); + case 85: + return jjMoveStringLiteralDfa2_0(active0, 0x40000L, active1, 0L); + case 88: + return jjMoveStringLiteralDfa2_0(active0, 0x8000000L, active1, 0L); + case 89: + if ((active0 & 0x200L) != 0L) + { + jjmatchedKind = 9; + jjmatchedPos = 1; + } + break; + case 101: + return jjMoveStringLiteralDfa2_0(active0, 0x6010000L, active1, 0L); + case 104: + return jjMoveStringLiteralDfa2_0(active0, 0x800000L, active1, 0L); + case 105: + return jjMoveStringLiteralDfa2_0(active0, 0x10020000L, active1, 0L); + case 110: + if ((active0 & 0x400L) != 0L) + { + jjmatchedKind = 10; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x2000L, active1, 0L); + case 111: + return jjMoveStringLiteralDfa2_0(active0, 0x20108000L, active1, 0L); + case 112: + return jjMoveStringLiteralDfa2_0(active0, 0x40400000L, active1, 0L); + case 114: + if ((active0 & 0x800L) != 0L) + { + jjmatchedKind = 11; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x280000L, active1, 0L); + case 115: + if ((active0 & 0x1000L) != 0L) + { + jjmatchedKind = 12; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0x1004000L, active1, 0L); + case 117: + return jjMoveStringLiteralDfa2_0(active0, 0x40000L, active1, 0L); + case 120: + return jjMoveStringLiteralDfa2_0(active0, 0x8000000L, active1, 0L); + case 121: + if ((active0 & 0x200L) != 0L) + { + jjmatchedKind = 9; + jjmatchedPos = 1; + } + break; + case 124: + if ((active1 & 0x100L) != 0L) + { + jjmatchedKind = 72; + jjmatchedPos = 1; + } + break; + default : + break; + } + return jjMoveNfa_0(0, 1); +} +private final int jjMoveStringLiteralDfa2_0(long old0, long active0, long old1, long active1) +{ + if (((active0 &= old0) | (active1 &= old1)) == 0L) + return jjMoveNfa_0(0, 1); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 1); + } + switch(curChar) + { + case 67: + if ((active0 & 0x4000L) != 0L) + { + jjmatchedKind = 14; + jjmatchedPos = 2; + } + return jjMoveStringLiteralDfa3_0(active0, 0x9000000L); + case 68: + if ((active0 & 0x2000L) != 0L) + { + jjmatchedKind = 13; + jjmatchedPos = 2; + } + return jjMoveStringLiteralDfa3_0(active0, 0x200000L); + case 69: + return jjMoveStringLiteralDfa3_0(active0, 0x40800000L); + case 75: + return jjMoveStringLiteralDfa3_0(active0, 0x20000L); + case 76: + return jjMoveStringLiteralDfa3_0(active0, 0x2040000L); + case 77: + return jjMoveStringLiteralDfa3_0(active0, 0x10000000L); + case 78: + return jjMoveStringLiteralDfa3_0(active0, 0x20000000L); + case 79: + return jjMoveStringLiteralDfa3_0(active0, 0x80000L); + case 80: + return jjMoveStringLiteralDfa3_0(active0, 0x400000L); + case 83: + return jjMoveStringLiteralDfa3_0(active0, 0x10000L); + case 84: + if ((active0 & 0x8000L) != 0L) + { + jjmatchedKind = 15; + jjmatchedPos = 2; + } + return jjMoveStringLiteralDfa3_0(active0, 0x4000000L); + case 87: + return jjMoveStringLiteralDfa3_0(active0, 0x100000L); + case 99: + if ((active0 & 0x4000L) != 0L) + { + jjmatchedKind = 14; + jjmatchedPos = 2; + } + return jjMoveStringLiteralDfa3_0(active0, 0x9000000L); + case 100: + if ((active0 & 0x2000L) != 0L) + { + jjmatchedKind = 13; + jjmatchedPos = 2; + } + return jjMoveStringLiteralDfa3_0(active0, 0x200000L); + case 101: + return jjMoveStringLiteralDfa3_0(active0, 0x40800000L); + case 107: + return jjMoveStringLiteralDfa3_0(active0, 0x20000L); + case 108: + return jjMoveStringLiteralDfa3_0(active0, 0x2040000L); + case 109: + return jjMoveStringLiteralDfa3_0(active0, 0x10000000L); + case 110: + return jjMoveStringLiteralDfa3_0(active0, 0x20000000L); + case 111: + return jjMoveStringLiteralDfa3_0(active0, 0x80000L); + case 112: + return jjMoveStringLiteralDfa3_0(active0, 0x400000L); + case 115: + return jjMoveStringLiteralDfa3_0(active0, 0x10000L); + case 116: + if ((active0 & 0x8000L) != 0L) + { + jjmatchedKind = 15; + jjmatchedPos = 2; + } + return jjMoveStringLiteralDfa3_0(active0, 0x4000000L); + case 119: + return jjMoveStringLiteralDfa3_0(active0, 0x100000L); + default : + break; + } + return jjMoveNfa_0(0, 2); +} +private final int jjMoveStringLiteralDfa3_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(0, 2); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 2); + } + switch(curChar) + { + case 65: + return jjMoveStringLiteralDfa4_0(active0, 0x1000000L); + case 67: + if ((active0 & 0x10000L) != 0L) + { + jjmatchedKind = 16; + jjmatchedPos = 3; + } + break; + case 69: + if ((active0 & 0x20000L) != 0L) + { + jjmatchedKind = 17; + jjmatchedPos = 3; + } + return jjMoveStringLiteralDfa4_0(active0, 0xa700000L); + case 73: + return jjMoveStringLiteralDfa4_0(active0, 0x10000000L); + case 76: + if ((active0 & 0x40000L) != 0L) + { + jjmatchedKind = 18; + jjmatchedPos = 3; + } + return jjMoveStringLiteralDfa4_0(active0, 0x40000000L); + case 77: + if ((active0 & 0x80000L) != 0L) + { + jjmatchedKind = 19; + jjmatchedPos = 3; + } + break; + case 82: + return jjMoveStringLiteralDfa4_0(active0, 0x800000L); + case 84: + return jjMoveStringLiteralDfa4_0(active0, 0x20000000L); + case 87: + return jjMoveStringLiteralDfa4_0(active0, 0x4000000L); + case 97: + return jjMoveStringLiteralDfa4_0(active0, 0x1000000L); + case 99: + if ((active0 & 0x10000L) != 0L) + { + jjmatchedKind = 16; + jjmatchedPos = 3; + } + break; + case 101: + if ((active0 & 0x20000L) != 0L) + { + jjmatchedKind = 17; + jjmatchedPos = 3; + } + return jjMoveStringLiteralDfa4_0(active0, 0xa700000L); + case 105: + return jjMoveStringLiteralDfa4_0(active0, 0x10000000L); + case 108: + if ((active0 & 0x40000L) != 0L) + { + jjmatchedKind = 18; + jjmatchedPos = 3; + } + return jjMoveStringLiteralDfa4_0(active0, 0x40000000L); + case 109: + if ((active0 & 0x80000L) != 0L) + { + jjmatchedKind = 19; + jjmatchedPos = 3; + } + break; + case 114: + return jjMoveStringLiteralDfa4_0(active0, 0x800000L); + case 116: + return jjMoveStringLiteralDfa4_0(active0, 0x20000000L); + case 119: + return jjMoveStringLiteralDfa4_0(active0, 0x4000000L); + default : + break; + } + return jjMoveNfa_0(0, 3); +} +private final int jjMoveStringLiteralDfa4_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(0, 3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 3); + } + switch(curChar) + { + case 65: + return jjMoveStringLiteralDfa5_0(active0, 0x20000000L); + case 67: + return jjMoveStringLiteralDfa5_0(active0, 0x2000000L); + case 69: + if ((active0 & 0x800000L) != 0L) + { + jjmatchedKind = 23; + jjmatchedPos = 4; + } + return jjMoveStringLiteralDfa5_0(active0, 0x4000000L); + case 76: + return jjMoveStringLiteralDfa5_0(active0, 0x50000000L); + case 80: + return jjMoveStringLiteralDfa5_0(active0, 0x1000000L); + case 82: + if ((active0 & 0x100000L) != 0L) + { + jjmatchedKind = 20; + jjmatchedPos = 4; + } + else if ((active0 & 0x200000L) != 0L) + { + jjmatchedKind = 21; + jjmatchedPos = 4; + } + else if ((active0 & 0x400000L) != 0L) + { + jjmatchedKind = 22; + jjmatchedPos = 4; + } + return jjMoveStringLiteralDfa5_0(active0, 0x8000000L); + case 97: + return jjMoveStringLiteralDfa5_0(active0, 0x20000000L); + case 99: + return jjMoveStringLiteralDfa5_0(active0, 0x2000000L); + case 101: + if ((active0 & 0x800000L) != 0L) + { + jjmatchedKind = 23; + jjmatchedPos = 4; + } + return jjMoveStringLiteralDfa5_0(active0, 0x4000000L); + case 108: + return jjMoveStringLiteralDfa5_0(active0, 0x50000000L); + case 112: + return jjMoveStringLiteralDfa5_0(active0, 0x1000000L); + case 114: + if ((active0 & 0x100000L) != 0L) + { + jjmatchedKind = 20; + jjmatchedPos = 4; + } + else if ((active0 & 0x200000L) != 0L) + { + jjmatchedKind = 21; + jjmatchedPos = 4; + } + else if ((active0 & 0x400000L) != 0L) + { + jjmatchedKind = 22; + jjmatchedPos = 4; + } + return jjMoveStringLiteralDfa5_0(active0, 0x8000000L); + default : + break; + } + return jjMoveNfa_0(0, 4); +} +private final int jjMoveStringLiteralDfa5_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(0, 4); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 4); + } + switch(curChar) + { + case 65: + return jjMoveStringLiteralDfa6_0(active0, 0x10000000L); + case 67: + return jjMoveStringLiteralDfa6_0(active0, 0x40000000L); + case 69: + if ((active0 & 0x1000000L) != 0L) + { + jjmatchedKind = 24; + jjmatchedPos = 5; + } + return jjMoveStringLiteralDfa6_0(active0, 0x4000000L); + case 73: + return jjMoveStringLiteralDfa6_0(active0, 0x20000000L); + case 80: + return jjMoveStringLiteralDfa6_0(active0, 0x8000000L); + case 84: + if ((active0 & 0x2000000L) != 0L) + { + jjmatchedKind = 25; + jjmatchedPos = 5; + } + break; + case 97: + return jjMoveStringLiteralDfa6_0(active0, 0x10000000L); + case 99: + return jjMoveStringLiteralDfa6_0(active0, 0x40000000L); + case 101: + if ((active0 & 0x1000000L) != 0L) + { + jjmatchedKind = 24; + jjmatchedPos = 5; + } + return jjMoveStringLiteralDfa6_0(active0, 0x4000000L); + case 105: + return jjMoveStringLiteralDfa6_0(active0, 0x20000000L); + case 112: + return jjMoveStringLiteralDfa6_0(active0, 0x8000000L); + case 116: + if ((active0 & 0x2000000L) != 0L) + { + jjmatchedKind = 25; + jjmatchedPos = 5; + } + break; + default : + break; + } + return jjMoveNfa_0(0, 5); +} +private final int jjMoveStringLiteralDfa6_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(0, 5); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 5); + } + switch(curChar) + { + case 72: + return jjMoveStringLiteralDfa7_0(active0, 0x40000000L); + case 78: + if ((active0 & 0x4000000L) != 0L) + { + jjmatchedKind = 26; + jjmatchedPos = 6; + } + return jjMoveStringLiteralDfa7_0(active0, 0x20000000L); + case 82: + if ((active0 & 0x10000000L) != 0L) + { + jjmatchedKind = 28; + jjmatchedPos = 6; + } + break; + case 84: + if ((active0 & 0x8000000L) != 0L) + { + jjmatchedKind = 27; + jjmatchedPos = 6; + } + break; + case 104: + return jjMoveStringLiteralDfa7_0(active0, 0x40000000L); + case 110: + if ((active0 & 0x4000000L) != 0L) + { + jjmatchedKind = 26; + jjmatchedPos = 6; + } + return jjMoveStringLiteralDfa7_0(active0, 0x20000000L); + case 114: + if ((active0 & 0x10000000L) != 0L) + { + jjmatchedKind = 28; + jjmatchedPos = 6; + } + break; + case 116: + if ((active0 & 0x8000000L) != 0L) + { + jjmatchedKind = 27; + jjmatchedPos = 6; + } + break; + default : + break; + } + return jjMoveNfa_0(0, 6); +} +private final int jjMoveStringLiteralDfa7_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(0, 6); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 6); + } + switch(curChar) + { + case 69: + return jjMoveStringLiteralDfa8_0(active0, 0x40000000L); + case 83: + if ((active0 & 0x20000000L) != 0L) + { + jjmatchedKind = 29; + jjmatchedPos = 7; + } + break; + case 101: + return jjMoveStringLiteralDfa8_0(active0, 0x40000000L); + case 115: + if ((active0 & 0x20000000L) != 0L) + { + jjmatchedKind = 29; + jjmatchedPos = 7; + } + break; + default : + break; + } + return jjMoveNfa_0(0, 7); +} +private final int jjMoveStringLiteralDfa8_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(0, 7); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 7); + } + switch(curChar) + { + case 67: + return jjMoveStringLiteralDfa9_0(active0, 0x40000000L); + case 99: + return jjMoveStringLiteralDfa9_0(active0, 0x40000000L); + default : + break; + } + return jjMoveNfa_0(0, 8); +} +private final int jjMoveStringLiteralDfa9_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjMoveNfa_0(0, 8); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + return jjMoveNfa_0(0, 8); + } + switch(curChar) + { + case 75: + if ((active0 & 0x40000000L) != 0L) + { + jjmatchedKind = 30; + jjmatchedPos = 9; + } + break; + case 107: + if ((active0 & 0x40000000L) != 0L) + { + jjmatchedKind = 30; + jjmatchedPos = 9; + } + break; + default : + break; + } + return jjMoveNfa_0(0, 9); +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +static final long[] jjbitVec0 = { + 0xfffffffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec2 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec3 = { + 0x0L, 0xffffffffffffc000L, 0xfffff0007fffffffL, 0x7fffffL +}; +static final long[] jjbitVec4 = { + 0x0L, 0x0L, 0x0L, 0xff7fffffff7fffffL +}; +static final long[] jjbitVec5 = { + 0x7ff3ffffffffffffL, 0x7ffffffffffffdfeL, 0xffffffffffffffffL, 0xfc31ffffffffe00fL +}; +static final long[] jjbitVec6 = { + 0xffffffL, 0xffffffffffff0000L, 0xf80001ffffffffffL, 0x3L +}; +static final long[] jjbitVec7 = { + 0x0L, 0x0L, 0xfffffffbffffd740L, 0xffffd547f7fffL +}; +static final long[] jjbitVec8 = { + 0xffffffffffffdffeL, 0xffffffffdffeffffL, 0xffffffffffff0003L, 0x33fcfffffff199fL +}; +static final long[] jjbitVec9 = { + 0xfffe000000000000L, 0xfffffffe027fffffL, 0x7fL, 0x707ffffff0000L +}; +static final long[] jjbitVec10 = { + 0x7fffffe00000000L, 0xfffe0000000007feL, 0x7cffffffffffffffL, 0x60002f7fffL +}; +static final long[] jjbitVec11 = { + 0x23ffffffffffffe0L, 0x3ff000000L, 0x3c5fdfffff99fe0L, 0x30003b0000000L +}; +static final long[] jjbitVec12 = { + 0x36dfdfffff987e0L, 0x1c00005e000000L, 0x23edfdfffffbafe0L, 0x100000000L +}; +static final long[] jjbitVec13 = { + 0x23cdfdfffff99fe0L, 0x3b0000000L, 0x3bfc718d63dc7e0L, 0x0L +}; +static final long[] jjbitVec14 = { + 0x3effdfffffddfe0L, 0x300000000L, 0x3effdfffffddfe0L, 0x340000000L +}; +static final long[] jjbitVec15 = { + 0x3fffdfffffddfe0L, 0x300000000L, 0x0L, 0x0L +}; +static final long[] jjbitVec16 = { + 0xd7ffffffffffeL, 0x3fL, 0x200d6caefef02596L, 0x1fL +}; +static final long[] jjbitVec17 = { + 0x0L, 0x3fffffffeffL, 0x0L, 0x0L +}; +static final long[] jjbitVec18 = { + 0x0L, 0x0L, 0xffffffff00000000L, 0x7fffffffff003fL +}; +static final long[] jjbitVec19 = { + 0x500000000007daedL, 0x2c62ab82315001L, 0xf580c90040000000L, 0x201080000000007L +}; +static final long[] jjbitVec20 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffff0fffffffL, 0x3ffffffffffffffL +}; +static final long[] jjbitVec21 = { + 0xffffffff3f3fffffL, 0x3fffffffaaff3f3fL, 0x5fdfffffffffffffL, 0x1fdc1fff0fcf1fdcL +}; +static final long[] jjbitVec22 = { + 0x4c4000000000L, 0x0L, 0x7L, 0x0L +}; +static final long[] jjbitVec23 = { + 0x3fe00000080L, 0xfffffffffffffffeL, 0xfffffffe001fffffL, 0x7ffffffffffffffL +}; +static final long[] jjbitVec24 = { + 0x1fffffffffe0L, 0x0L, 0x0L, 0x0L +}; +static final long[] jjbitVec25 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0x3fffffffffL, 0x0L +}; +static final long[] jjbitVec26 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0xfffffffffL, 0x0L +}; +private final int jjMoveNfa_0(int startState, int curPos) +{ + int strKind = jjmatchedKind; + int strPos = jjmatchedPos; + int seenUpto; + input_stream.backup(seenUpto = curPos + 1); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { throw new Error("Internal Error"); } + curPos = 0; + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 109; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 74) + kind = 74; + jjCheckNAddStates(0, 4); + } + else if ((0x280000000000L & l) != 0L) + jjCheckNAddTwoStates(15, 18); + else if (curChar == 46) + jjCheckNAddTwoStates(26, 27); + else if (curChar == 39) + jjCheckNAddTwoStates(12, 13); + else if (curChar == 34) + jjCheckNAddTwoStates(7, 10); + if (curChar == 45) + jjCheckNAdd(1); + break; + case 1: + if (curChar == 45) + jjCheckNAddStates(5, 7); + break; + case 2: + jjCheckNAddTwoStates(2, 3); + break; + case 3: + if (curChar == 10 && kind > 5) + kind = 5; + break; + case 5: + if ((0x7ff000000000000L & l) == 0L) + break; + if (kind > 60) + kind = 60; + jjAddStates(8, 9); + break; + case 6: + if (curChar == 34) + jjCheckNAddTwoStates(7, 10); + break; + case 7: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(7, 8); + break; + case 8: + if (curChar != 34) + break; + if (kind > 64) + kind = 64; + jjCheckNAdd(9); + break; + case 9: + if (curChar == 34) + jjCheckNAddTwoStates(7, 8); + break; + case 10: + if (curChar == 34) + jjCheckNAdd(9); + break; + case 11: + if (curChar == 39) + jjCheckNAddTwoStates(12, 13); + break; + case 12: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(12, 13); + break; + case 13: + if (curChar != 39) + break; + if (kind > 98) + kind = 98; + jjstateSet[jjnewStateCnt++] = 11; + break; + case 14: + if ((0x280000000000L & l) != 0L) + jjCheckNAddTwoStates(15, 18); + break; + case 15: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 74) + kind = 74; + jjCheckNAddTwoStates(15, 16); + break; + case 16: + if (curChar != 46) + break; + if (kind > 74) + kind = 74; + jjCheckNAdd(17); + break; + case 17: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 74) + kind = 74; + jjCheckNAdd(17); + break; + case 18: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(10, 12); + break; + case 19: + if (curChar == 46) + jjCheckNAddTwoStates(20, 21); + break; + case 20: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(20, 21); + break; + case 22: + if ((0x280000000000L & l) != 0L) + jjCheckNAdd(23); + break; + case 23: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 76) + kind = 76; + jjCheckNAdd(23); + break; + case 24: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 74) + kind = 74; + jjCheckNAddStates(0, 4); + break; + case 25: + if (curChar == 46) + jjCheckNAddTwoStates(26, 27); + break; + case 26: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 74) + kind = 74; + jjCheckNAdd(26); + break; + case 27: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(27, 21); + break; + case 30: + if (curChar == 32) + jjAddStates(13, 14); + break; + case 31: + if (curChar == 39) + jjCheckNAdd(32); + break; + case 32: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(32, 33); + break; + case 33: + if (curChar == 45) + jjCheckNAdd(34); + break; + case 34: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(34, 35); + break; + case 35: + if (curChar == 45) + jjCheckNAdd(36); + break; + case 36: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(36, 37); + break; + case 37: + if (curChar == 39 && kind > 81) + kind = 81; + break; + case 41: + if (curChar == 32) + jjAddStates(15, 16); + break; + case 42: + if (curChar == 39) + jjCheckNAdd(43); + break; + case 43: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(43, 44); + break; + case 44: + if (curChar == 45) + jjCheckNAdd(45); + break; + case 45: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(45, 46); + break; + case 46: + if (curChar == 45) + jjCheckNAdd(47); + break; + case 47: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(47, 48); + break; + case 48: + if (curChar == 39 && kind > 82) + kind = 82; + break; + case 53: + if (curChar == 32) + jjAddStates(17, 18); + break; + case 54: + if (curChar == 39) + jjCheckNAdd(55); + break; + case 55: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(55, 56); + break; + case 56: + if (curChar == 45) + jjCheckNAdd(57); + break; + case 57: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(57, 58); + break; + case 58: + if (curChar == 45) + jjCheckNAdd(59); + break; + case 59: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(59, 60); + break; + case 60: + if (curChar == 32) + jjCheckNAdd(61); + break; + case 61: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(61, 62); + break; + case 62: + if (curChar == 58) + jjCheckNAdd(63); + break; + case 63: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(63, 64); + break; + case 64: + if (curChar == 58) + jjCheckNAdd(65); + break; + case 65: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(19, 23); + break; + case 66: + if (curChar == 46) + jjCheckNAddStates(24, 27); + break; + case 67: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(24, 27); + break; + case 69: + if ((0x280000000000L & l) != 0L) + jjCheckNAdd(70); + break; + case 70: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(70, 71); + break; + case 71: + if (curChar == 58) + jjCheckNAdd(72); + break; + case 72: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(72, 37); + break; + case 81: + if (curChar == 32) + jjAddStates(28, 29); + break; + case 82: + if (curChar == 39) + jjCheckNAdd(83); + break; + case 83: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(83, 84); + break; + case 84: + if (curChar == 45) + jjCheckNAdd(85); + break; + case 85: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(85, 86); + break; + case 86: + if (curChar == 45) + jjCheckNAdd(87); + break; + case 87: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(87, 88); + break; + case 88: + if (curChar == 32) + jjCheckNAdd(89); + break; + case 89: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(89, 90); + break; + case 90: + if (curChar == 58) + jjCheckNAdd(91); + break; + case 91: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(91, 92); + break; + case 92: + if (curChar == 58) + jjCheckNAdd(93); + break; + case 93: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(30, 34); + break; + case 94: + if (curChar == 46) + jjCheckNAddStates(35, 38); + break; + case 95: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(35, 38); + break; + case 97: + if (curChar == 39 && kind > 83) + kind = 83; + break; + case 98: + if ((0x280000000000L & l) != 0L) + jjCheckNAdd(99); + break; + case 99: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(99, 100); + break; + case 100: + if (curChar == 58) + jjCheckNAdd(101); + break; + case 101: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(101, 97); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 60) + kind = 60; + jjCheckNAddTwoStates(5, 4); + } + if (curChar == 84) + jjAddStates(39, 40); + else if (curChar == 68) + jjAddStates(41, 42); + break; + case 2: + jjAddStates(43, 44); + break; + case 4: + if ((0x7fffffe07fffffeL & l) == 0L) + break; + if (kind > 60) + kind = 60; + jjCheckNAddTwoStates(5, 4); + break; + case 5: + if (curChar != 95) + break; + if (kind > 60) + kind = 60; + jjCheckNAddTwoStates(5, 4); + break; + case 7: + jjAddStates(45, 46); + break; + case 12: + jjAddStates(47, 48); + break; + case 21: + if ((0x2000000020L & l) != 0L) + jjAddStates(49, 50); + break; + case 28: + if (curChar == 68) + jjAddStates(41, 42); + break; + case 29: + if (curChar == 69) + jjstateSet[jjnewStateCnt++] = 30; + break; + case 38: + if (curChar == 84) + jjstateSet[jjnewStateCnt++] = 29; + break; + case 39: + if (curChar == 65) + jjstateSet[jjnewStateCnt++] = 38; + break; + case 40: + if (curChar == 69) + jjstateSet[jjnewStateCnt++] = 41; + break; + case 49: + if (curChar == 84) + jjstateSet[jjnewStateCnt++] = 40; + break; + case 50: + if (curChar == 65) + jjstateSet[jjnewStateCnt++] = 49; + break; + case 51: + if (curChar == 84) + jjAddStates(39, 40); + break; + case 52: + if (curChar == 80) + jjstateSet[jjnewStateCnt++] = 53; + break; + case 60: + if (curChar == 84) + jjstateSet[jjnewStateCnt++] = 61; + break; + case 68: + if (curChar == 90) + jjstateSet[jjnewStateCnt++] = 37; + break; + case 73: + if (curChar == 77) + jjstateSet[jjnewStateCnt++] = 52; + break; + case 74: + if (curChar == 65) + jjstateSet[jjnewStateCnt++] = 73; + break; + case 75: + if (curChar == 84) + jjstateSet[jjnewStateCnt++] = 74; + break; + case 76: + if (curChar == 83) + jjstateSet[jjnewStateCnt++] = 75; + break; + case 77: + if (curChar == 69) + jjstateSet[jjnewStateCnt++] = 76; + break; + case 78: + if (curChar == 77) + jjstateSet[jjnewStateCnt++] = 77; + break; + case 79: + if (curChar == 73) + jjstateSet[jjnewStateCnt++] = 78; + break; + case 80: + if (curChar == 80) + jjstateSet[jjnewStateCnt++] = 81; + break; + case 88: + if (curChar == 84) + jjstateSet[jjnewStateCnt++] = 89; + break; + case 96: + if (curChar == 90) + jjstateSet[jjnewStateCnt++] = 97; + break; + case 102: + if (curChar == 77) + jjstateSet[jjnewStateCnt++] = 80; + break; + case 103: + if (curChar == 65) + jjstateSet[jjnewStateCnt++] = 102; + break; + case 104: + if (curChar == 84) + jjstateSet[jjnewStateCnt++] = 103; + break; + case 105: + if (curChar == 83) + jjstateSet[jjnewStateCnt++] = 104; + break; + case 106: + if (curChar == 69) + jjstateSet[jjnewStateCnt++] = 105; + break; + case 107: + if (curChar == 77) + jjstateSet[jjnewStateCnt++] = 106; + break; + case 108: + if (curChar == 73) + jjstateSet[jjnewStateCnt++] = 107; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 4: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 60) + kind = 60; + jjCheckNAddTwoStates(5, 4); + break; + case 2: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjAddStates(43, 44); + break; + case 7: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjAddStates(45, 46); + break; + case 12: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjAddStates(47, 48); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 109 - (jjnewStateCnt = startsAt))) + break; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { break; } + } + if (jjmatchedPos > strPos) + return curPos; + + int toRet = Math.max(curPos, seenUpto); + + if (curPos < toRet) + for (i = toRet - Math.min(curPos, seenUpto); i-- > 0; ) + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { throw new Error("Internal Error : Please send a bug report."); } + + if (jjmatchedPos < strPos) + { + jjmatchedKind = strKind; + jjmatchedPos = strPos; + } + else if (jjmatchedPos == strPos && jjmatchedKind > strKind) + jjmatchedKind = strKind; + + return toRet; +} +static final int[] jjnextStates = { + 15, 16, 18, 19, 21, 1, 2, 3, 5, 4, 18, 19, 21, 30, 31, 41, + 42, 53, 54, 65, 66, 68, 69, 37, 67, 68, 69, 37, 81, 82, 93, 94, + 96, 98, 97, 95, 96, 98, 97, 79, 108, 39, 50, 2, 3, 7, 8, 12, + 13, 22, 23, +}; +private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec2[i2] & l2) != 0L); + default : + if ((jjbitVec0[i1] & l1) != 0L) + return true; + return false; + } +} +private static final boolean jjCanMove_1(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec4[i2] & l2) != 0L); + case 1: + return ((jjbitVec5[i2] & l2) != 0L); + case 2: + return ((jjbitVec6[i2] & l2) != 0L); + case 3: + return ((jjbitVec7[i2] & l2) != 0L); + case 4: + return ((jjbitVec8[i2] & l2) != 0L); + case 5: + return ((jjbitVec9[i2] & l2) != 0L); + case 6: + return ((jjbitVec10[i2] & l2) != 0L); + case 9: + return ((jjbitVec11[i2] & l2) != 0L); + case 10: + return ((jjbitVec12[i2] & l2) != 0L); + case 11: + return ((jjbitVec13[i2] & l2) != 0L); + case 12: + return ((jjbitVec14[i2] & l2) != 0L); + case 13: + return ((jjbitVec15[i2] & l2) != 0L); + case 14: + return ((jjbitVec16[i2] & l2) != 0L); + case 15: + return ((jjbitVec17[i2] & l2) != 0L); + case 16: + return ((jjbitVec18[i2] & l2) != 0L); + case 17: + return ((jjbitVec19[i2] & l2) != 0L); + case 30: + return ((jjbitVec20[i2] & l2) != 0L); + case 31: + return ((jjbitVec21[i2] & l2) != 0L); + case 33: + return ((jjbitVec22[i2] & l2) != 0L); + case 48: + return ((jjbitVec23[i2] & l2) != 0L); + case 49: + return ((jjbitVec24[i2] & l2) != 0L); + case 159: + return ((jjbitVec25[i2] & l2) != 0L); + case 215: + return ((jjbitVec26[i2] & l2) != 0L); + default : + if ((jjbitVec3[i1] & l1) != 0L) + return true; + return false; + } +} +public static final String[] jjstrLiteralImages = { +"", null, null, null, null, null, null, null, null, null, null, null, null, +null, null, null, null, null, null, null, null, null, null, null, null, null, null, +null, null, null, null, null, null, null, null, null, null, null, "\42", "\45", +"\46", "\47", "\50", "\51", "\52", "\53", "\54", "\55", "\56", "\57", "\72", "\73", +"\74", "\75", "\76", "\77", "\137", "\174", "\133", "\135", null, null, null, null, +null, null, null, null, null, "\74\76", "\76\75", "\74\75", "\174\174", "\56\56", +null, null, null, null, null, null, null, null, null, null, null, null, null, null, +null, null, null, null, null, null, null, null, null, null, null, null, null, }; +public static final String[] lexStateNames = { + "DEFAULT", +}; +static final long[] jjtoToken = { + 0x1fffffc07ffffe01L, 0x4000e17e1L, +}; +static final long[] jjtoSkip = { + 0x3eL, 0x0L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[109]; +private final int[] jjstateSet = new int[218]; +protected char curChar; +public JCRSQLParserTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} +public JCRSQLParserTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 109; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 1 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + return matchedToken; + } + + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + return matchedToken; + } + else + { + continue EOFLoop; + } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } +} + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserTreeConstants.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserTreeConstants.java new file mode 100644 index 00000000000..14bbf3e1e95 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserTreeConstants.java @@ -0,0 +1,51 @@ +/* Generated By:JJTree: Do not edit this line. /home/jukka/src/jackrabbit/jackrabbit-spi-commons/target/generated-sources/jjtree/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserTreeConstants.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public interface JCRSQLParserTreeConstants +{ + public int JJTQUERY = 0; + public int JJTSELECTLIST = 1; + public int JJTVOID = 2; + public int JJTFROMCLAUSE = 3; + public int JJTWHERECLAUSE = 4; + public int JJTPREDICATE = 5; + public int JJTLOWERFUNCTION = 6; + public int JJTUPPERFUNCTION = 7; + public int JJTOREXPRESSION = 8; + public int JJTANDEXPRESSION = 9; + public int JJTNOTEXPRESSION = 10; + public int JJTBRACKETEXPRESSION = 11; + public int JJTCONTAINSEXPRESSION = 12; + public int JJTLITERAL = 13; + public int JJTIDENTIFIER = 14; + public int JJTEXCERPTFUNCTION = 15; + public int JJTORDERBYCLAUSE = 16; + public int JJTORDERSPEC = 17; + public int JJTASCENDINGORDERSPEC = 18; + public int JJTDESCENDINGORDERSPEC = 19; + + + public String[] jjtNodeName = { + "Query", + "SelectList", + "void", + "FromClause", + "WhereClause", + "Predicate", + "LowerFunction", + "UpperFunction", + "OrExpression", + "AndExpression", + "NotExpression", + "BracketExpression", + "ContainsExpression", + "Literal", + "Identifier", + "ExcerptFunction", + "OrderByClause", + "OrderSpec", + "AscendingOrderSpec", + "DescendingOrderSpec", + }; +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserVisitor.java new file mode 100644 index 00000000000..b3733843cbf --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserVisitor.java @@ -0,0 +1,27 @@ +/* Generated By:JJTree: Do not edit this line. /home/jukka/src/jackrabbit/jackrabbit-spi-commons/target/generated-sources/jjtree/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLParserVisitor.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public interface JCRSQLParserVisitor +{ + public Object visit(SimpleNode node, Object data); + public Object visit(ASTQuery node, Object data); + public Object visit(ASTSelectList node, Object data); + public Object visit(ASTFromClause node, Object data); + public Object visit(ASTWhereClause node, Object data); + public Object visit(ASTPredicate node, Object data); + public Object visit(ASTLowerFunction node, Object data); + public Object visit(ASTUpperFunction node, Object data); + public Object visit(ASTOrExpression node, Object data); + public Object visit(ASTAndExpression node, Object data); + public Object visit(ASTNotExpression node, Object data); + public Object visit(ASTBracketExpression node, Object data); + public Object visit(ASTContainsExpression node, Object data); + public Object visit(ASTLiteral node, Object data); + public Object visit(ASTIdentifier node, Object data); + public Object visit(ASTExcerptFunction node, Object data); + public Object visit(ASTOrderByClause node, Object data); + public Object visit(ASTOrderSpec node, Object data); + public Object visit(ASTAscendingOrderSpec node, Object data); + public Object visit(ASTDescendingOrderSpec node, Object data); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLQueryBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLQueryBuilder.java new file mode 100644 index 00000000000..01835182fbf --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JCRSQLQueryBuilder.java @@ -0,0 +1,1009 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.sql; + +import org.apache.jackrabbit.spi.commons.query.AndQueryNode; +import org.apache.jackrabbit.spi.commons.query.LocationStepQueryNode; +import org.apache.jackrabbit.spi.commons.query.NAryQueryNode; +import org.apache.jackrabbit.spi.commons.query.NodeTypeQueryNode; +import org.apache.jackrabbit.spi.commons.query.NotQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrderQueryNode; +import org.apache.jackrabbit.spi.commons.query.PathQueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryConstants; +import org.apache.jackrabbit.spi.commons.query.QueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.RelationQueryNode; +import org.apache.jackrabbit.spi.commons.query.TextsearchQueryNode; +import org.apache.jackrabbit.spi.commons.query.PropertyFunctionQueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryNodeFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.util.ISO8601; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.commons.collections.map.ReferenceMap; + +import javax.jcr.query.InvalidQueryException; +import javax.jcr.NamespaceException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Map; +import java.io.StringReader; + +/** + * Implements the query builder for the JCR SQL syntax. + */ +public class JCRSQLQueryBuilder implements JCRSQLParserVisitor { + + /** + * logger instance for this class + */ + private static final Logger log = LoggerFactory.getLogger(JCRSQLQueryBuilder.class); + + /** + * DateFormat pattern for type + * {@link org.apache.jackrabbit.spi.commons.query.QueryConstants#TYPE_DATE}. + */ + private static final String DATE_PATTERN = "yyyy-MM-dd"; + + /** + * Map of reusable JCRSQL parser instances indexed by NamespaceResolver. + */ + private static Map parsers = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK); + + /** + * The root node of the sql query syntax tree + */ + private final ASTQuery stmt; + + /** + * The root query node + */ + private QueryRootNode root; + + /** + * To resolve QNames + */ + private NameResolver resolver; + + /** + * Query node to gather the constraints defined in the WHERE clause + */ + private final AndQueryNode constraintNode; + + /** + * The Name of the node type in the from clause. + */ + private Name nodeTypeName; + + /** + * List of PathQueryNode constraints that need to be merged + */ + private final List pathConstraints = new ArrayList(); + + /** + * The query node factory. + */ + private final QueryNodeFactory factory; + + /** + * Creates a new JCRSQLQueryBuilder. + * + * @param statement the root node of the SQL syntax tree. + * @param resolver a namespace resolver to use for names in the + * statement. + * @param factory the query node factory. + */ + private JCRSQLQueryBuilder(ASTQuery statement, + NameResolver resolver, + QueryNodeFactory factory) { + this.stmt = statement; + this.resolver = resolver; + this.factory = factory; + this.constraintNode = factory.createAndQueryNode(null); + } + + /** + * Creates a QueryNode tree from a SQL statement + * using the passed query node factory. + * + * @param statement the SQL statement. + * @param resolver the namespace resolver to use. + * @return the QueryNode tree. + * @throws InvalidQueryException if statement is malformed. + */ + public static QueryRootNode createQuery(String statement, + NameResolver resolver, + QueryNodeFactory factory) + throws InvalidQueryException { + try { + // get parser + JCRSQLParser parser; + synchronized (parsers) { + parser = (JCRSQLParser) parsers.get(resolver); + if (parser == null) { + parser = new JCRSQLParser(new StringReader(statement)); + parser.setNameResolver(resolver); + parsers.put(resolver, parser); + } + } + + JCRSQLQueryBuilder builder; + // guard against concurrent use within same session + synchronized (parser) { + parser.ReInit(new StringReader(statement)); + builder = new JCRSQLQueryBuilder(parser.Query(), resolver, factory); + } + return builder.getRootNode(); + } catch (ParseException e) { + throw new InvalidQueryException(e.getMessage()); + } catch (IllegalArgumentException e) { + throw new InvalidQueryException(e.getMessage()); + } catch (Throwable t) { + // javacc parser may also throw an error in some cases + throw new InvalidQueryException(t.getMessage()); + } + } + + /** + * Creates a String representation of the query node tree in SQL syntax. + * + * @param root the root of the query node tree. + * @param resolver to resolve QNames. + * @return a String representation of the query node tree. + * @throws InvalidQueryException if the query node tree cannot be converted + * into a String representation due to restrictions in SQL. + */ + public static String toString(QueryRootNode root, NameResolver resolver) + throws InvalidQueryException { + return QueryFormat.toString(root, resolver); + } + + /** + * Parses the statement and returns the root node of the QueryNode + * tree. + * + * @return the root node of the QueryNode tree. + */ + private QueryRootNode getRootNode() { + if (root == null) { + stmt.jjtAccept(this, null); + } + return root; + } + + //----------------< JCRSQLParserVisitor >------------------------------------ + + public Object visit(SimpleNode node, Object data) { + // do nothing, should never be called actually + return data; + } + + public Object visit(ASTQuery node, Object data) { + root = factory.createQueryRootNode(); + root.setLocationNode(factory.createPathQueryNode(root)); + + // pass to select, from, where, ... + node.childrenAccept(this, root); + + // use //* if no path has been set + PathQueryNode pathNode = root.getLocationNode(); + pathNode.setAbsolute(true); + if (pathConstraints.size() == 0) { + LocationStepQueryNode step = factory.createLocationStepQueryNode(pathNode); + step.setNameTest(null); + step.setIncludeDescendants(true); + pathNode.addPathStep(step); + } else { + try { + while (pathConstraints.size() > 1) { + // merge path nodes + MergingPathQueryNode path = null; + for (Iterator it = pathConstraints.iterator(); it.hasNext();) { + path = (MergingPathQueryNode) it.next(); + if (path.needsMerge()) { + break; + } else { + path = null; + } + } + if (path == null) { + throw new IllegalArgumentException("Invalid combination of jcr:path clauses"); + } else { + pathConstraints.remove(path); + MergingPathQueryNode[] paths = (MergingPathQueryNode[]) pathConstraints.toArray(new MergingPathQueryNode[pathConstraints.size()]); + paths = path.doMerge(paths); + pathConstraints.clear(); + pathConstraints.addAll(Arrays.asList(paths)); + } + } + } catch (NoSuchElementException e) { + throw new IllegalArgumentException("Invalid combination of jcr:path clauses"); + } + MergingPathQueryNode path = (MergingPathQueryNode) pathConstraints.get(0); + LocationStepQueryNode[] steps = path.getPathSteps(); + for (int i = 0; i < steps.length; i++) { + LocationStepQueryNode step = factory.createLocationStepQueryNode(pathNode); + step.setNameTest(steps[i].getNameTest()); + step.setIncludeDescendants(steps[i].getIncludeDescendants()); + step.setIndex(steps[i].getIndex()); + pathNode.addPathStep(step); + } + } + + if (constraintNode.getNumOperands() == 1) { + // attach operand to last path step + LocationStepQueryNode[] steps = pathNode.getPathSteps(); + steps[steps.length - 1].addPredicate(constraintNode.getOperands()[0]); + } else if (constraintNode.getNumOperands() > 1) { + // attach constraint to last path step + LocationStepQueryNode[] steps = pathNode.getPathSteps(); + steps[steps.length - 1].addPredicate(constraintNode); + } + + if (nodeTypeName != null) { + // add node type constraint + LocationStepQueryNode[] steps = pathNode.getPathSteps(); + NodeTypeQueryNode nodeType + = factory.createNodeTypeQueryNode(steps[steps.length - 1], nodeTypeName); + steps[steps.length - 1].addPredicate(nodeType); + } + + return root; + } + + public Object visit(ASTSelectList node, Object data) { + final QueryRootNode root = (QueryRootNode) data; + + node.childrenAccept(new DefaultParserVisitor() { + public Object visit(ASTIdentifier node, Object data) { + root.addSelectProperty(node.getName()); + return data; + } + + public Object visit(ASTExcerptFunction node, Object data) { + root.addSelectProperty(NameFactoryImpl.getInstance().create(Name.NS_REP_URI, "excerpt(.)")); + return data; + } + }, root); + + return data; + } + + public Object visit(ASTFromClause node, Object data) { + QueryRootNode root = (QueryRootNode) data; + + return node.childrenAccept(new DefaultParserVisitor() { + public Object visit(ASTIdentifier node, Object data) { + if (!node.getName().equals(NameConstants.NT_BASE)) { + // node is either primary or mixin node type + nodeTypeName = node.getName(); + } + return data; + } + }, root); + } + + public Object visit(ASTWhereClause node, Object data) { + return node.childrenAccept(this, constraintNode); + } + + public Object visit(ASTPredicate node, Object data) { + NAryQueryNode parent = (NAryQueryNode) data; + + int type = node.getOperationType(); + QueryNode predicateNode; + + try { + final Name[] tmp = new Name[2]; + final ASTLiteral[] value = new ASTLiteral[1]; + node.childrenAccept(new DefaultParserVisitor() { + public Object visit(ASTIdentifier node, Object data) { + if (tmp[0] == null) { + tmp[0] = node.getName(); + } else if (tmp[1] == null) { + tmp[1] = node.getName(); + } + return data; + } + + public Object visit(ASTLiteral node, Object data) { + value[0] = node; + return data; + } + + public Object visit(ASTLowerFunction node, Object data) { + getIdentifier(node); + return data; + } + + public Object visit(ASTUpperFunction node, Object data) { + getIdentifier(node); + return data; + } + + private void getIdentifier(SimpleNode node) { + if (node.jjtGetNumChildren() > 0) { + Node n = node.jjtGetChild(0); + if (n instanceof ASTIdentifier) { + ASTIdentifier identifier = (ASTIdentifier) n; + if (tmp[0] == null) { + tmp[0] = identifier.getName(); + } else if (tmp[1] == null) { + tmp[1] = identifier.getName(); + } + } + } + } + }, data); + Name identifier = tmp[0]; + + if (identifier != null && identifier.equals(NameConstants.JCR_PATH)) { + if (tmp[1] != null) { + // simply ignore, this is a join of a mixin node type + } else { + createPathQuery(value[0].getValue(), parent.getType()); + } + // done + return data; + } + + if (type == QueryConstants.OPERATION_BETWEEN) { + AndQueryNode between = factory.createAndQueryNode(parent); + RelationQueryNode rel = createRelationQueryNode(between, + identifier, QueryConstants.OPERATION_GE_GENERAL, (ASTLiteral) node.children[1]); + node.childrenAccept(this, rel); + between.addOperand(rel); + rel = createRelationQueryNode(between, + identifier, QueryConstants.OPERATION_LE_GENERAL, (ASTLiteral) node.children[2]); + node.childrenAccept(this, rel); + between.addOperand(rel); + predicateNode = between; + } else if (type == QueryConstants.OPERATION_GE_GENERAL + || type == QueryConstants.OPERATION_GT_GENERAL + || type == QueryConstants.OPERATION_LE_GENERAL + || type == QueryConstants.OPERATION_LT_GENERAL + || type == QueryConstants.OPERATION_NE_GENERAL + || type == QueryConstants.OPERATION_EQ_GENERAL) { + predicateNode = createRelationQueryNode(parent, + identifier, type, value[0]); + node.childrenAccept(this, predicateNode); + } else if (type == QueryConstants.OPERATION_LIKE) { + ASTLiteral pattern = value[0]; + if (node.getEscapeString() != null) { + if (node.getEscapeString().length() == 1) { + // backslash is the escape character we use internally + pattern.setValue(translateEscaping(pattern.getValue(), node.getEscapeString().charAt(0), '\\')); + } else { + throw new IllegalArgumentException("ESCAPE string value must have length 1: '" + node.getEscapeString() + "'"); + } + } else { + // no escape character specified. + // if the pattern contains any backslash characters we need + // to escape them. + pattern.setValue(pattern.getValue().replaceAll("\\\\", "\\\\\\\\")); + } + predicateNode = createRelationQueryNode(parent, + identifier, type, pattern); + node.childrenAccept(this, predicateNode); + } else if (type == QueryConstants.OPERATION_IN) { + OrQueryNode in = factory.createOrQueryNode(parent); + for (int i = 1; i < node.children.length; i++) { + RelationQueryNode rel = createRelationQueryNode(in, + identifier, QueryConstants.OPERATION_EQ_VALUE, (ASTLiteral) node.children[i]); + node.childrenAccept(this, rel); + in.addOperand(rel); + } + predicateNode = in; + } else if (type == QueryConstants.OPERATION_NULL + || type == QueryConstants.OPERATION_NOT_NULL) { + predicateNode = createRelationQueryNode(parent, + identifier, type, null); + } else if (type == QueryConstants.OPERATION_SIMILAR) { + ASTLiteral literal; + if (node.children.length == 1) { + literal = (ASTLiteral) node.children[0]; + } else { + literal = (ASTLiteral) node.children[1]; + } + predicateNode = createRelationQueryNode(parent, identifier, type, literal); + } else if (type == QueryConstants.OPERATION_SPELLCHECK) { + predicateNode = createRelationQueryNode(parent, + NameConstants.JCR_PRIMARYTYPE, type, + (ASTLiteral) node.children[0]); + } else { + throw new IllegalArgumentException("Unknown operation type: " + type); + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Too few arguments in predicate"); + } + + if (predicateNode != null) { + parent.addOperand(predicateNode); + } + + return data; + } + + public Object visit(ASTOrExpression node, Object data) { + NAryQueryNode parent = (NAryQueryNode) data; + OrQueryNode orQuery = factory.createOrQueryNode(parent); + // pass to operands + node.childrenAccept(this, orQuery); + + if (orQuery.getNumOperands() > 0) { + parent.addOperand(orQuery); + } + return parent; + } + + public Object visit(ASTAndExpression node, Object data) { + NAryQueryNode parent = (NAryQueryNode) data; + AndQueryNode andQuery = factory.createAndQueryNode(parent); + // pass to operands + node.childrenAccept(this, andQuery); + + if (andQuery.getNumOperands() > 0) { + parent.addOperand(andQuery); + } + return parent; + } + + public Object visit(ASTNotExpression node, Object data) { + NAryQueryNode parent = (NAryQueryNode) data; + NotQueryNode notQuery = factory.createNotQueryNode(parent); + // pass to operand + node.childrenAccept(this, notQuery); + + if (notQuery.getNumOperands() > 0) { + parent.addOperand(notQuery); + } + return parent; + } + + public Object visit(ASTBracketExpression node, Object data) { + // bracket expression only has influence on how the syntax tree + // is created. + // simply pass on to children + return node.childrenAccept(this, data); + } + + public Object visit(ASTLiteral node, Object data) { + // do nothing + return data; + } + + public Object visit(ASTIdentifier node, Object data) { + // do nothing + return data; + } + + public Object visit(ASTOrderByClause node, Object data) { + QueryRootNode root = (QueryRootNode) data; + + OrderQueryNode order = factory.createOrderQueryNode(root); + root.setOrderNode(order); + node.childrenAccept(this, order); + return root; + } + + public Object visit(ASTOrderSpec node, Object data) { + OrderQueryNode order = (OrderQueryNode) data; + + final Name[] identifier = new Name[1]; + + // collect identifier + node.childrenAccept(new DefaultParserVisitor() { + public Object visit(ASTIdentifier node, Object data) { + identifier[0] = node.getName(); + return data; + } + }, data); + + OrderQueryNode.OrderSpec spec = new OrderQueryNode.OrderSpec(identifier[0], true); + order.addOrderSpec(spec); + + node.childrenAccept(this, spec); + + return data; + } + + public Object visit(ASTAscendingOrderSpec node, Object data) { + // do nothing ascending is default anyway + return data; + } + + public Object visit(ASTDescendingOrderSpec node, Object data) { + OrderQueryNode.OrderSpec spec = (OrderQueryNode.OrderSpec) data; + spec.setAscending(false); + return data; + } + + public Object visit(ASTContainsExpression node, Object data) { + NAryQueryNode parent = (NAryQueryNode) data; + try { + Path relPath = null; + if (node.getPropertyName() != null) { + PathBuilder builder = new PathBuilder(); + builder.addLast(node.getPropertyName()); + relPath = builder.getPath(); + } + TextsearchQueryNode tsNode = factory.createTextsearchQueryNode(parent, node.getQuery()); + tsNode.setRelativePath(relPath); + tsNode.setReferencesProperty(true); + parent.addOperand(tsNode); + } catch (MalformedPathException e) { + // path is always valid + } + return parent; + } + + public Object visit(ASTLowerFunction node, Object data) { + RelationQueryNode parent = (RelationQueryNode) data; + if (parent.getValueType() != QueryConstants.TYPE_STRING) { + String msg = "LOWER() function is only supported for String literal"; + throw new IllegalArgumentException(msg); + } + parent.addOperand(factory.createPropertyFunctionQueryNode(parent, PropertyFunctionQueryNode.LOWER_CASE)); + return parent; + } + + public Object visit(ASTUpperFunction node, Object data) { + RelationQueryNode parent = (RelationQueryNode) data; + if (parent.getValueType() != QueryConstants.TYPE_STRING) { + String msg = "UPPER() function is only supported for String literal"; + throw new IllegalArgumentException(msg); + } + parent.addOperand(factory.createPropertyFunctionQueryNode(parent, PropertyFunctionQueryNode.UPPER_CASE)); + return parent; + } + + public Object visit(ASTExcerptFunction node, Object data) { + // do nothing + return data; + } + + //------------------------< internal >-------------------------------------- + + /** + * Creates a new {@link org.apache.jackrabbit.spi.commons.query.RelationQueryNode}. + * + * @param parent the parent node for the created RelationQueryNode. + * @param propertyName the property name for the relation. + * @param operationType the operation type. + * @param literal the literal value for the relation or + * null if the relation does not have a + * literal (e.g. IS NULL). + * @return a RelationQueryNode. + * @throws IllegalArgumentException if the literal value does not conform + * to its type. E.g. a malformed String representation of a date. + */ + private RelationQueryNode createRelationQueryNode(QueryNode parent, + Name propertyName, + int operationType, + ASTLiteral literal) + throws IllegalArgumentException { + + RelationQueryNode node = null; + + try { + Path relPath = null; + if (propertyName != null) { + PathBuilder builder = new PathBuilder(); + builder.addLast(propertyName); + relPath = builder.getPath(); + } + if (literal == null) { + node = factory.createRelationQueryNode(parent, operationType); + node.setRelativePath(relPath); + } else if (literal.getType() == QueryConstants.TYPE_DATE) { + SimpleDateFormat format = new SimpleDateFormat(DATE_PATTERN); + Date date = format.parse(literal.getValue()); + node = factory.createRelationQueryNode(parent, operationType); + node.setRelativePath(relPath); + node.setDateValue(date); + } else if (literal.getType() == QueryConstants.TYPE_DOUBLE) { + double d = Double.parseDouble(literal.getValue()); + node = factory.createRelationQueryNode(parent, operationType); + node.setRelativePath(relPath); + node.setDoubleValue(d); + } else if (literal.getType() == QueryConstants.TYPE_LONG) { + long l = Long.parseLong(literal.getValue()); + node = factory.createRelationQueryNode(parent, operationType); + node.setRelativePath(relPath); + node.setLongValue(l); + } else if (literal.getType() == QueryConstants.TYPE_STRING) { + node = factory.createRelationQueryNode(parent, operationType); + node.setRelativePath(relPath); + node.setStringValue(literal.getValue()); + } else if (literal.getType() == QueryConstants.TYPE_TIMESTAMP) { + Calendar c = ISO8601.parse(literal.getValue()); + node = factory.createRelationQueryNode(parent, operationType); + node.setRelativePath(relPath); + node.setDateValue(c.getTime()); + } + } catch (java.text.ParseException e) { + throw new IllegalArgumentException(e.toString()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(e.toString()); + } catch (MalformedPathException e) { + // path is always valid, but throw anyway + throw new IllegalArgumentException(e.getMessage()); + } + + if (node == null) { + throw new IllegalArgumentException("Unknown type for literal: " + literal.getType()); + } + return node; + } + + /** + * Creates LocationStepQueryNodes from a path. + * + * @param path the path pattern + * @param operation the type of the parent node + */ + private void createPathQuery(String path, int operation) { + MergingPathQueryNode pathNode = new MergingPathQueryNode(operation, + factory.createPathQueryNode(null).getValidJcrSystemNodeTypeNames()); + pathNode.setAbsolute(true); + + if (path.equals("/")) { + pathNode.addPathStep(factory.createLocationStepQueryNode(pathNode)); + pathConstraints.add(pathNode); + return; + } + + String[] names = path.split("/"); + + for (int i = 0; i < names.length; i++) { + if (names[i].length() == 0) { + if (i == 0) { + // root + pathNode.addPathStep(factory.createLocationStepQueryNode(pathNode)); + } else { + // descendant '//' -> invalid path + // todo throw or ignore? + // we currently do not throw and add location step for an + // empty name (which is basically the root node) + pathNode.addPathStep(factory.createLocationStepQueryNode(pathNode)); + } + } else { + int idx = names[i].indexOf('['); + String name; + int index = LocationStepQueryNode.NONE; + if (idx > -1) { + // contains index + name = names[i].substring(0, idx); + String suffix = names[i].substring(idx); + String indexStr = suffix.substring(1, suffix.length() - 1); + if (indexStr.equals("%")) { + // select all same name siblings + index = LocationStepQueryNode.NONE; + } else { + try { + index = Integer.parseInt(indexStr); + } catch (NumberFormatException e) { + log.warn("Unable to parse index for path element: " + names[i]); + } + } + if (name.equals("%")) { + name = null; + } + } else { + // no index specified + // - index defaults to 1 if there is an explicit name test + // - index defaults to NONE if name test is % + name = names[i]; + if (name.equals("%")) { + name = null; + } else { + index = 1; + } + } + Name qName = null; + if (name != null) { + try { + qName = resolver.getQName(name); + } catch (NamespaceException e) { + throw new IllegalArgumentException("Illegal name: " + name); + } catch (NameException e) { + throw new IllegalArgumentException("Illegal name: " + name); + } + } + // if name test is % this means also search descendants + boolean descendant = name == null; + LocationStepQueryNode step = factory.createLocationStepQueryNode(pathNode); + step.setNameTest(qName); + step.setIncludeDescendants(descendant); + if (index > 0) { + step.setIndex(index); + } + pathNode.addPathStep(step); + } + } + pathConstraints.add(pathNode); + } + + /** + * Translates a pattern using the escape character from into + * a pattern using the escape character to. + * + * @param pattern the pattern to translate + * @param from the currently used escape character. + * @param to the new escape character to use. + * @return the new pattern using the escape character to. + */ + private static String translateEscaping(String pattern, char from, char to) { + // if escape characters are the same OR pattern does not contain any + // escape characters -> simply return pattern as is. + if (from == to || (pattern.indexOf(from) < 0 && pattern.indexOf(to) < 0)) { + return pattern; + } + StringBuffer translated = new StringBuffer(pattern.length()); + boolean escaped = false; + for (int i = 0; i < pattern.length(); i++) { + if (pattern.charAt(i) == from) { + if (escaped) { + translated.append(from); + escaped = false; + } else { + escaped = true; + } + } else if (pattern.charAt(i) == to) { + if (escaped) { + translated.append(to).append(to); + escaped = false; + } else { + translated.append(to).append(to); + } + } else { + if (escaped) { + translated.append(to); + escaped = false; + } + translated.append(pattern.charAt(i)); + } + } + return translated.toString(); + } + + /** + * Extends the PathQueryNode with merging capability. A + * PathQueryNode n1 can be merged with another + * node n2 in the following case: + *

        + * n1 contains a location step at position X with + * a name test that matches any node and has the descending flag set. Where + * X < number of location steps. + * n2 contains no location step to match any node name and + * the sequence of name tests is the same as the sequence of name tests + * of n1. + * The merged node then contains a location step at position X + * with the name test of the location step at position X+1 and + * the descending flag set. + *

        + * The following path patterns:
        + * /foo/%/bar OR /foo/bar
        + * are merged into:
        + * /foo//bar. + *

        + * The path patterns:
        + * /foo/% AND NOT /foo/%/%
        + * are merged into:
        + * /foo/* + */ + private static class MergingPathQueryNode extends PathQueryNode { + + /** + * The operation type of the parent node + */ + private int operation; + + /** + * Creates a new MergingPathQueryNode with the operation + * type of a parent node. operation must be one of: + * {@link org.apache.jackrabbit.spi.commons.query.QueryNode#TYPE_OR}, + * {@link org.apache.jackrabbit.spi.commons.query.QueryNode#TYPE_AND} or + * {@link org.apache.jackrabbit.spi.commons.query.QueryNode#TYPE_NOT}. + * + * @param operation the operation type of the parent node. + * @param validJcrSystemNodeTypeNames names of valid node types under + * /jcr:system. + */ + MergingPathQueryNode( + int operation, Collection validJcrSystemNodeTypeNames) { + super(null, validJcrSystemNodeTypeNames); + if (operation != QueryNode.TYPE_OR && operation != QueryNode.TYPE_AND && operation != QueryNode.TYPE_NOT) { + throw new IllegalArgumentException("operation"); + } + this.operation = operation; + } + + /** + * Merges this node with a node from nodes. If a merge + * is not possible an NoSuchElementException is thrown. + * + * @param nodes the nodes to try to merge with. + * @return the merged array containing a merged version of this node. + */ + MergingPathQueryNode[] doMerge(MergingPathQueryNode[] nodes) { + if (operation == QueryNode.TYPE_OR) { + return doOrMerge(nodes); + } else { + return doAndMerge(nodes); + } + } + + /** + * Merges two nodes into a node which selects any child nodes of a + * given node. + *

        + * Example:
        + * The path patterns:
        + * /foo/% AND NOT /foo/%/%
        + * are merged into:
        + * /foo/* + * + * @param nodes the nodes to merge with. + * @return the merged nodes. + */ + private MergingPathQueryNode[] doAndMerge(MergingPathQueryNode[] nodes) { + if (operation == QueryNode.TYPE_AND) { + // check if there is an node with operation OP_AND_NOT + MergingPathQueryNode n = null; + for (int i = 0; i < nodes.length; i++) { + if (nodes[i].operation == QueryNode.TYPE_NOT) { + n = nodes[i]; + nodes[i] = this; + } + } + if (n == null) { + throw new NoSuchElementException("Merging not possible with any node"); + } else { + return n.doAndMerge(nodes); + } + } + // check if this node is valid as an operand + if (operands.size() < 3) { + throw new NoSuchElementException("Merging not possible"); + } + int size = operands.size(); + LocationStepQueryNode n1 = (LocationStepQueryNode) operands.get(size - 1); + LocationStepQueryNode n2 = (LocationStepQueryNode) operands.get(size - 2); + if (n1.getNameTest() != null || n2.getNameTest() != null + || !n1.getIncludeDescendants() || !n2.getIncludeDescendants()) { + throw new NoSuchElementException("Merging not possible"); + } + // find a node to merge with + MergingPathQueryNode matchedNode = null; + for (int i = 0; i < nodes.length; i++) { + if (nodes[i].operands.size() == operands.size() - 1) { + boolean match = true; + for (int j = 0; j < operands.size() - 1 && match; j++) { + LocationStepQueryNode step = (LocationStepQueryNode) operands.get(j); + LocationStepQueryNode other = (LocationStepQueryNode) nodes[i].operands.get(j); + match &= (step.getNameTest() == null) ? other.getNameTest() == null : step.getNameTest().equals(other.getNameTest()); + } + if (match) { + matchedNode = nodes[i]; + break; + } + } + } + if (matchedNode == null) { + throw new NoSuchElementException("Merging not possible with any node"); + } + // change descendants flag to only match child nodes + // that's the result of the merge. + ((LocationStepQueryNode) matchedNode.operands.get(matchedNode.operands.size() - 1)).setIncludeDescendants(false); + return nodes; + } + + /** + * Merges two nodes into one node selecting a node on the + * descendant-or-self axis. + *

        + * Example:
        + * The following path patterns:
        + * /foo/%/bar OR /foo/bar
        + * are merged into:
        + * /foo//bar. + * + * @param nodes the node to merge. + * @return the merged nodes. + */ + private MergingPathQueryNode[] doOrMerge(MergingPathQueryNode[] nodes) { + // compact this + MergingPathQueryNode compacted = new MergingPathQueryNode( + QueryNode.TYPE_OR, getValidJcrSystemNodeTypeNames()); + for (Iterator it = operands.iterator(); it.hasNext();) { + LocationStepQueryNode step = (LocationStepQueryNode) it.next(); + if (step.getIncludeDescendants() && step.getNameTest() == null) { + // check if has next + if (it.hasNext()) { + LocationStepQueryNode next = (LocationStepQueryNode) it.next(); + next.setIncludeDescendants(true); + compacted.addPathStep(next); + } else { + compacted.addPathStep(step); + } + } else { + compacted.addPathStep(step); + } + } + + MergingPathQueryNode matchedNode = null; + for (int i = 0; i < nodes.length; i++) { + // loop over the steps and compare the names + if (nodes[i].operands.size() == compacted.operands.size()) { + boolean match = true; + Iterator compactedSteps = compacted.operands.iterator(); + Iterator otherSteps = nodes[i].operands.iterator(); + while (match && compactedSteps.hasNext()) { + LocationStepQueryNode n1 = (LocationStepQueryNode) compactedSteps.next(); + LocationStepQueryNode n2 = (LocationStepQueryNode) otherSteps.next(); + match &= (n1.getNameTest() == null) ? n2.getNameTest() == null : n1.getNameTest().equals(n2.getNameTest()); + } + if (match) { + matchedNode = nodes[i]; + break; + } + } + } + if (matchedNode == null) { + throw new NoSuchElementException("Merging not possible with any node."); + } + // construct new list + List mergedList = new ArrayList(Arrays.asList(nodes)); + mergedList.remove(matchedNode); + mergedList.add(compacted); + return (MergingPathQueryNode[]) mergedList.toArray(new MergingPathQueryNode[mergedList.size()]); + } + + /** + * Returns true if this node needs merging; false + * otherwise. + * + * @return true if this node needs merging; false + * otherwise. + */ + boolean needsMerge() { + for (Iterator it = operands.iterator(); it.hasNext();) { + LocationStepQueryNode step = (LocationStepQueryNode) it.next(); + if (step.getIncludeDescendants() && step.getNameTest() == null) { + return true; + } + } + return false; + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JJTJCRSQLParserState.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JJTJCRSQLParserState.java new file mode 100644 index 00000000000..80b78ed101c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/JJTJCRSQLParserState.java @@ -0,0 +1,123 @@ +/* Generated By:JJTree: Do not edit this line. /home/jukka/src/jackrabbit/jackrabbit-spi-commons/target/generated-sources/jjtree/org/apache/jackrabbit/spi/commons/query/sql/JJTJCRSQLParserState.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +class JJTJCRSQLParserState { + private java.util.Stack nodes; + private java.util.Stack marks; + + private int sp; // number of nodes on stack + private int mk; // current mark + private boolean node_created; + + JJTJCRSQLParserState() { + nodes = new java.util.Stack(); + marks = new java.util.Stack(); + sp = 0; + mk = 0; + } + + /* Determines whether the current node was actually closed and + pushed. This should only be called in the final user action of a + node scope. */ + boolean nodeCreated() { + return node_created; + } + + /* Call this to reinitialize the node stack. It is called + automatically by the parser's ReInit() method. */ + void reset() { + nodes.removeAllElements(); + marks.removeAllElements(); + sp = 0; + mk = 0; + } + + /* Returns the root node of the AST. It only makes sense to call + this after a successful parse. */ + Node rootNode() { + return (Node)nodes.elementAt(0); + } + + /* Pushes a node on to the stack. */ + void pushNode(Node n) { + nodes.push(n); + ++sp; + } + + /* Returns the node on the top of the stack, and remove it from the + stack. */ + Node popNode() { + if (--sp < mk) { + mk = ((Integer)marks.pop()).intValue(); + } + return (Node)nodes.pop(); + } + + /* Returns the node currently on the top of the stack. */ + Node peekNode() { + return (Node)nodes.peek(); + } + + /* Returns the number of children on the stack in the current node + scope. */ + int nodeArity() { + return sp - mk; + } + + + void clearNodeScope(Node n) { + while (sp > mk) { + popNode(); + } + mk = ((Integer)marks.pop()).intValue(); + } + + + void openNodeScope(Node n) { + marks.push(new Integer(mk)); + mk = sp; + n.jjtOpen(); + } + + + /* A definite node is constructed from a specified number of + children. That number of nodes are popped from the stack and + made the children of the definite node. Then the definite node + is pushed on to the stack. */ + void closeNodeScope(Node n, int num) { + mk = ((Integer)marks.pop()).intValue(); + while (num-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, num); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } + + + /* A conditional node is constructed if its condition is true. All + the nodes that have been pushed since the node was opened are + made children of the the conditional node, which is then pushed + on to the stack. If the condition is false the node is not + constructed and they are left on the stack. */ + void closeNodeScope(Node n, boolean condition) { + if (condition) { + int a = nodeArity(); + mk = ((Integer)marks.pop()).intValue(); + while (a-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, a); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } else { + mk = ((Integer)marks.pop()).intValue(); + node_created = false; + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/Node.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/Node.java new file mode 100644 index 00000000000..71da1dba1a9 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/Node.java @@ -0,0 +1,37 @@ +/* Generated By:JJTree: Do not edit this line. Node.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +/* All AST nodes must implement this interface. It provides basic + machinery for constructing the parent and child relationships + between nodes. */ + +public interface Node { + + /** This method is called after the node has been made the current + node. It indicates that child nodes can now be added to it. */ + public void jjtOpen(); + + /** This method is called after all the child nodes have been + added. */ + public void jjtClose(); + + /** This pair of methods are used to inform the node of its + parent. */ + public void jjtSetParent(Node n); + public Node jjtGetParent(); + + /** This method tells the node to add its argument to the node's + list of children. */ + public void jjtAddChild(Node n, int i); + + /** This method returns a child node. The children are numbered + from zero, left to right. */ + public Node jjtGetChild(int i); + + /** Return the number of children the node has. */ + public int jjtGetNumChildren(); + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ParseException.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ParseException.java new file mode 100644 index 00000000000..4a51710046a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/ParseException.java @@ -0,0 +1,208 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.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.spi.commons.query.sql; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * {@code ParseException: } + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/QueryBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/QueryBuilder.java new file mode 100644 index 00000000000..469e0e6db06 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/QueryBuilder.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.spi.commons.query.sql; + +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.query.QueryNodeFactory; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.QueryTreeBuilder; + +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; + +/** + * Implements the JCR SQL query tree builder. + */ +public class QueryBuilder implements QueryTreeBuilder { + + /** + * {@inheritDoc} + */ + public QueryRootNode createQueryTree(String statement, + NameResolver resolver, + QueryNodeFactory factory) + throws InvalidQueryException { + return JCRSQLQueryBuilder.createQuery(statement, resolver, factory); + } + + /** + * {@inheritDoc} + */ + public boolean canHandle(String language) { + return Query.SQL.equals(language); + } + + /** + * This builder supports {@link Query#SQL}. + * {@inheritDoc} + */ + public String[] getSupportedLanguages() { + return new String[]{Query.SQL}; + } + + /** + * {@inheritDoc} + */ + public String toString(QueryRootNode root, NameResolver resolver) + throws InvalidQueryException { + return JCRSQLQueryBuilder.toString(root, resolver); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/QueryFormat.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/QueryFormat.java new file mode 100644 index 00000000000..6cae7250f6a --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/QueryFormat.java @@ -0,0 +1,607 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.sql; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Iterator; +import java.util.List; +import java.util.TimeZone; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.query.AndQueryNode; +import org.apache.jackrabbit.spi.commons.query.DerefQueryNode; +import org.apache.jackrabbit.spi.commons.query.ExactQueryNode; +import org.apache.jackrabbit.spi.commons.query.LocationStepQueryNode; +import org.apache.jackrabbit.spi.commons.query.NodeTypeQueryNode; +import org.apache.jackrabbit.spi.commons.query.NotQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrderQueryNode; +import org.apache.jackrabbit.spi.commons.query.PathQueryNode; +import org.apache.jackrabbit.spi.commons.query.PropertyFunctionQueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryConstants; +import org.apache.jackrabbit.spi.commons.query.QueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryNodeVisitor; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.RelationQueryNode; +import org.apache.jackrabbit.spi.commons.query.TextsearchQueryNode; +import org.apache.jackrabbit.util.ISO8601; + +/** + * Implements the query node tree serialization into a String. + */ +class QueryFormat implements QueryNodeVisitor, QueryConstants { + + /** + * Will be used to resolve QNames + */ + private final NameResolver resolver; + + /** + * The String representation of the query node tree + */ + private final String statement; + + /** + * List of exception objects created while creating the SQL string + */ + private final List exceptions = new ArrayList(); + + /** + * List of node types + */ + private final List nodeTypes = new ArrayList(); + + private QueryFormat(QueryRootNode root, NameResolver resolver) + throws RepositoryException { + this.resolver = resolver; + statement = root.accept(this, new StringBuffer()).toString(); + if (exceptions.size() > 0) { + Exception e = (Exception) exceptions.get(0); + throw new InvalidQueryException(e.getMessage(), e); + } + } + + /** + * Creates a SQL String representation of the QueryNode tree + * argument root. + * + * @param root the query node tree. + * @param resolver to resolve QNames. + * @return the SQL string representation of the QueryNode tree. + * @throws InvalidQueryException the query node tree cannot be represented + * as a SQL String. + */ + public static String toString(QueryRootNode root, NameResolver resolver) + throws InvalidQueryException { + try { + return new QueryFormat(root, resolver).toString(); + } + catch (RepositoryException e) { + throw new InvalidQueryException(e); + } + } + + /** + * Returns the string representation. + * + * @return the string representation. + */ + public String toString() { + return statement; + } + + //-------------< QueryNodeVisitor interface >------------------------------- + + public Object visit(QueryRootNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + try { + sb.append("SELECT"); + + Name[] selectProps = node.getSelectProperties(); + if (selectProps.length == 0) { + sb.append(" *"); + } else { + String comma = ""; + for (int i = 0; i < selectProps.length; i++) { + sb.append(comma).append(" "); + appendName(selectProps[i], resolver, sb); + comma = ","; + } + } + + sb.append(" FROM"); + + // node type restrictions are within predicates of location nodes + // therefore we write the where clause first to a temp string to + // collect the node types. + StringBuffer tmp = new StringBuffer(); + LocationStepQueryNode[] steps = node.getLocationNode().getPathSteps(); + QueryNode[] predicates = steps[steps.length - 1].getPredicates(); + // are there any relevant predicates? + for (int i = 0; i < predicates.length; i++) { + if (predicates[i].getType() != QueryNode.TYPE_NODETYPE) { + tmp.append(" WHERE "); + } + } + String and = ""; + for (int i = 0; i < predicates.length; i++) { + if (predicates[i].getType() != QueryNode.TYPE_NODETYPE) { + tmp.append(and); + and = " AND "; + } + predicates[i].accept(this, tmp); + } + + // node types have been collected by now + String comma = ""; + int ntCount = 0; + for (Iterator it = nodeTypes.iterator(); it.hasNext(); ntCount++) { + Name nt = (Name) it.next(); + sb.append(comma).append(" "); + appendName(nt, resolver, sb); + comma = ","; + } + + if (ntCount == 0) { + sb.append(" "); + sb.append(resolver.getJCRName(NameConstants.NT_BASE)); + } + + // append WHERE clause + sb.append(tmp.toString()); + + if (steps.length == 2 + && steps[1].getIncludeDescendants() + && steps[1].getNameTest() == null) { + // then this query selects all paths + } else if (steps.length == 1 + && steps[0].getIncludeDescendants() + && steps[0].getNameTest() == null) { + // then this query selects all paths + } else { + if (predicates.length > 0) { + sb.append(" AND "); + } else { + sb.append(" WHERE "); + } + node.getLocationNode().accept(this, sb); + } + } catch (NamespaceException e) { + exceptions.add(e); + } + + if (node.getOrderNode() != null) { + node.getOrderNode().accept(this, sb); + } + + return sb; + } + + public Object visit(OrQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + boolean bracket = false; + if (node.getParent() instanceof LocationStepQueryNode + || node.getParent() instanceof AndQueryNode + || node.getParent() instanceof NotQueryNode) { + bracket = true; + } + if (bracket) { + sb.append("("); + } + String or = ""; + QueryNode[] operands = node.getOperands(); + for (int i = 0; i < operands.length; i++) { + sb.append(or); + operands[i].accept(this, sb); + or = " OR "; + } + if (bracket) { + sb.append(")"); + } + return sb; + } + + public Object visit(AndQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + boolean bracket = false; + if (node.getParent() instanceof NotQueryNode) { + bracket = true; + } + if (bracket) { + sb.append("("); + } + String and = ""; + QueryNode[] operands = node.getOperands(); + for (int i = 0; i < operands.length; i++) { + sb.append(and); + int len = sb.length(); + operands[i].accept(this, sb); + // check if something has been written at all + // might have been a node type query node + if (sb.length() - len > 0) { + and = " AND "; + } else { + and = ""; + } + } + if (bracket) { + sb.append(")"); + } + return sb; + } + + public Object visit(NotQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + QueryNode[] operands = node.getOperands(); + if (operands.length > 0) { + sb.append("NOT "); + operands[0].accept(this, sb); + } + return sb; + } + + public Object visit(ExactQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + try { + appendName(node.getPropertyName(), resolver, sb); + } catch (NamespaceException e) { + exceptions.add(e); + } + sb.append("='").append(node.getValue()).append("'"); + return sb; + } + + public Object visit(NodeTypeQueryNode node, Object data) { + nodeTypes.add(node.getValue()); + return data; + } + + public Object visit(TextsearchQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + // escape quote + String query = node.getQuery().replaceAll("'", "''"); + sb.append("CONTAINS("); + if (node.getRelativePath() == null) { + sb.append("*"); + } else { + if (node.getRelativePath().getLength() > 1 + || !node.getReferencesProperty()) { + exceptions.add(new InvalidQueryException("Child axis not supported in SQL")); + } else { + try { + appendName(node.getRelativePath().getName(), resolver, sb); + } catch (NamespaceException e) { + exceptions.add(e); + } + } + } + sb.append(", '"); + sb.append(query).append("')"); + return sb; + } + + public Object visit(PathQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + try { + if (containsDescendantOrSelf(node)) { + sb.append("("); + sb.append(resolver.getJCRName(NameConstants.JCR_PATH)); + sb.append(" LIKE '"); + LocationStepQueryNode[] steps = node.getPathSteps(); + for (int i = 0; i < steps.length; i++) { + if (steps[i].getNameTest() == null + || steps[i].getNameTest().getLocalName().length() > 0) { + sb.append('/'); + } + if (steps[i].getIncludeDescendants()) { + sb.append("%/"); + } + steps[i].accept(this, sb); + } + sb.append('\''); + sb.append(" OR "); + sb.append(resolver.getJCRName(NameConstants.JCR_PATH)); + sb.append(" LIKE '"); + for (int i = 0; i < steps.length; i++) { + if (steps[i].getNameTest() == null + || steps[i].getNameTest().getLocalName().length() > 0) { + sb.append('/'); + } + if (steps[i].getNameTest() != null) { + steps[i].accept(this, sb); + } + } + sb.append("')"); + } else if (containsAllChildrenMatch(node)) { + sb.append(resolver.getJCRName(NameConstants.JCR_PATH)); + sb.append(" LIKE '"); + StringBuffer path = new StringBuffer(); + LocationStepQueryNode[] steps = node.getPathSteps(); + for (int i = 0; i < steps.length; i++) { + if (steps[i].getNameTest() == null + || steps[i].getNameTest().getLocalName().length() > 0) { + path.append('/'); + } + steps[i].accept(this, path); + } + sb.append(path); + sb.append('\''); + sb.append(" AND NOT "); + sb.append(resolver.getJCRName(NameConstants.JCR_PATH)); + sb.append(" LIKE '"); + sb.append(path).append("/%").append('\''); + } else { + // just do a best effort + sb.append(resolver.getJCRName(NameConstants.JCR_PATH)); + sb.append(" LIKE '"); + LocationStepQueryNode[] steps = node.getPathSteps(); + for (int i = 0; i < steps.length; i++) { + if (steps[i].getNameTest() == null + || steps[i].getNameTest().getLocalName().length() > 0) { + sb.append('/'); + } + steps[i].accept(this, sb); + } + sb.append('\''); + } + } catch (NamespaceException e) { + exceptions.add(e); + } + return sb; + } + + public Object visit(LocationStepQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + if (node.getNameTest() == null) { + sb.append("%"); + } else { + if (node.getNameTest().getLocalName().length() > 0) { + try { + sb.append(resolver.getJCRName(node.getNameTest())); + } catch (NamespaceException e) { + exceptions.add(e); + } + if (node.getIndex() == LocationStepQueryNode.NONE) { + sb.append("[%]"); + } else if (node.getIndex() == 1) { + // do nothing + } else { + sb.append('[').append(node.getIndex()).append(']'); + } + } else { + // empty name test indicates root node + } + } + return sb; + } + + public Object visit(DerefQueryNode node, Object data) { + exceptions.add(new InvalidQueryException("jcr:deref() function not supported in SQL")); + return data; + } + + public Object visit(RelationQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + try { + StringBuffer propName = new StringBuffer(); + PathQueryNode relPath = node.getRelativePath(); + if (relPath == null) { + propName.append("."); + } else if (relPath.getPathSteps().length > 1) { + exceptions.add(new InvalidQueryException("Child axis not supported in SQL")); + return data; + } else { + visit(relPath, data); + } + // surround name with property function + node.acceptOperands(this, propName); + + if (node.getOperation() == OPERATION_EQ_VALUE || node.getOperation() == OPERATION_EQ_GENERAL) { + sb.append(propName); + sb.append(" = "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_GE_VALUE || node.getOperation() == OPERATION_GE_GENERAL) { + sb.append(propName); + sb.append(" >= "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_GT_VALUE || node.getOperation() == OPERATION_GT_GENERAL) { + sb.append(propName); + sb.append(" > "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_LE_VALUE || node.getOperation() == OPERATION_LE_GENERAL) { + sb.append(propName); + sb.append(" <= "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_LIKE) { + sb.append(propName); + sb.append(" LIKE "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_LT_VALUE || node.getOperation() == OPERATION_LT_GENERAL) { + sb.append(propName); + sb.append(" < "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_NE_VALUE || node.getOperation() == OPERATION_NE_GENERAL) { + sb.append(propName); + sb.append(" <> "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_NULL) { + sb.append(propName); + sb.append(" IS NULL"); + } else if (node.getOperation() == OPERATION_NOT_NULL) { + sb.append(propName); + sb.append(" IS NOT NULL"); + } else if (node.getOperation() == OPERATION_SIMILAR) { + sb.append("SIMILAR("); + sb.append(propName); + sb.append(", "); + appendValue(node, sb); + sb.append(")"); + } else if (node.getOperation() == OPERATION_SPELLCHECK) { + sb.append("SPELLCHECK("); + appendValue(node, sb); + sb.append(")"); + } else { + exceptions.add(new InvalidQueryException("Invalid operation: " + node.getOperation())); + } + + if (node.getOperation() == OPERATION_LIKE && node.getStringValue().indexOf('\\') > -1) { + sb.append(" ESCAPE '\\'"); + } + } catch (NamespaceException e) { + exceptions.add(e); + } + return sb; + } + + public Object visit(OrderQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + sb.append(" ORDER BY"); + OrderQueryNode.OrderSpec[] specs = node.getOrderSpecs(); + if (specs.length > 0) { + try { + String comma = ""; + for (int i = 0; i < specs.length; i++) { + sb.append(comma).append(" "); + Path propPath = specs[i].getPropertyPath(); + if (propPath.getLength() > 1) { + exceptions.add(new InvalidQueryException("SQL does not support relative paths in order by clause")); + return sb; + } + appendName(propPath.getName(), resolver, sb); + if (!specs[i].isAscending()) { + sb.append(" DESC"); + } + comma = ","; + } + } catch (NamespaceException e) { + exceptions.add(e); + } + } else { + sb.append(" SCORE"); + } + return sb; + } + + public Object visit(PropertyFunctionQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + String functionName = node.getFunctionName(); + if (functionName.equals(PropertyFunctionQueryNode.LOWER_CASE)) { + sb.insert(0, "LOWER(").append(")"); + } else if (functionName.equals(PropertyFunctionQueryNode.UPPER_CASE)) { + sb.insert(0, "UPPER(").append(")"); + } else { + exceptions.add(new InvalidQueryException("Unsupported function: " + functionName)); + } + return sb; + } + + //------------------------< internal >-------------------------------------- + + /** + * Appends the name to the StringBuffer + * b using the NamespaceResolver + * resolver. The name is put in double quotes + * if the local part of name contains a space character. + * + * @param name the Name to print. + * @param resolver to resolve name. + * @param b where to output the name. + * @throws NamespaceException if name contains a uri + * that is not declared in resolver. + */ + private static void appendName(Name name, + NameResolver resolver, + StringBuffer b) + throws NamespaceException { + boolean quote = name.getLocalName().indexOf(' ') > -1; + if (quote) { + b.append('"'); + } + b.append(resolver.getJCRName(name)); + if (quote) { + b.append('"'); + } + } + + private void appendValue(RelationQueryNode node, StringBuffer b) { + if (node.getValueType() == TYPE_LONG) { + b.append(node.getLongValue()); + } else if (node.getValueType() == TYPE_DOUBLE) { + b.append(node.getDoubleValue()); + } else if (node.getValueType() == TYPE_STRING) { + b.append("'").append(node.getStringValue().replaceAll("'", "''")).append("'"); + } else if (node.getValueType() == TYPE_DATE || node.getValueType() == TYPE_TIMESTAMP) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setTime(node.getDateValue()); + b.append("TIMESTAMP '").append(ISO8601.format(cal)).append("'"); + } else { + exceptions.add(new InvalidQueryException("Invalid type: " + node.getValueType())); + } + + } + + /** + * Returns true if path contains exactly one + * step with a descendant-or-self axis and an explicit name test; returns + * false otherwise. + * + * @param path the path node. + * @return true if path contains exactly one + * step with a descendant-or-self axis. + */ + private static boolean containsDescendantOrSelf(PathQueryNode path) { + LocationStepQueryNode[] steps = path.getPathSteps(); + int count = 0; + for (int i = 0; i < steps.length; i++) { + if (steps[i].getNameTest() != null && steps[i].getIncludeDescendants()) { + count++; + } + } + return count == 1; + } + + /** + * Returns true if path contains exactly one + * location step which matches all node names. That is, matches any children + * of a given node. That location step must be the last one in the sequence + * of location steps. + * + * @param path the path node. + * @return true if the last step matches any node name. + */ + private static boolean containsAllChildrenMatch(PathQueryNode path) { + LocationStepQueryNode[] steps = path.getPathSteps(); + int count = 0; + for (int i = 0; i < steps.length; i++) { + if (steps[i].getNameTest() == null && !steps[i].getIncludeDescendants()) { + if (i == steps.length - 1 && count == 0) { + return true; + } + count++; + } + } + return false; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/SimpleCharStream.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/SimpleCharStream.java new file mode 100644 index 00000000000..2e085993bd7 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/SimpleCharStream.java @@ -0,0 +1,455 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.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.spi.commons.query.sql; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, + bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, + available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return (c); + } + + /** + * @deprecated + * @see #getEndColumn + */ + + public int getColumn() { + return bufcolumn[bufpos]; + } + + /** + * @deprecated + * @see #getEndLine + */ + + public int getLine() { + return bufline[bufpos]; + } + + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + public int getEndLine() { + return bufline[bufpos]; + } + + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + public int getBeginLine() { + return bufline[tokenBegin]; + } + + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && + bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/SimpleNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/SimpleNode.java new file mode 100644 index 00000000000..027be73a7c8 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/SimpleNode.java @@ -0,0 +1,87 @@ +/* Generated By:JJTree: Do not edit this line. SimpleNode.java */ + +package org.apache.jackrabbit.spi.commons.query.sql; + +public class SimpleNode implements Node { + protected Node parent; + protected Node[] children; + protected int id; + protected JCRSQLParser parser; + + public SimpleNode(int i) { + id = i; + } + + public SimpleNode(JCRSQLParser p, int i) { + this(i); + parser = p; + } + + public void jjtOpen() { + } + + public void jjtClose() { + } + + public void jjtSetParent(Node n) { parent = n; } + public Node jjtGetParent() { return parent; } + + public void jjtAddChild(Node n, int i) { + if (children == null) { + children = new Node[i + 1]; + } else if (i >= children.length) { + Node c[] = new Node[i + 1]; + System.arraycopy(children, 0, c, 0, children.length); + children = c; + } + children[i] = n; + } + + public Node jjtGetChild(int i) { + return children[i]; + } + + public int jjtGetNumChildren() { + return (children == null) ? 0 : children.length; + } + + /** Accept the visitor. **/ + public Object jjtAccept(JCRSQLParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + /** Accept the visitor. **/ + public Object childrenAccept(JCRSQLParserVisitor visitor, Object data) { + if (children != null) { + for (int i = 0; i < children.length; ++i) { + children[i].jjtAccept(visitor, data); + } + } + return data; + } + + /* You can override these two methods in subclasses of SimpleNode to + customize the way the node appears when the tree is dumped. If + your output uses more than one line you should override + toString(String), otherwise overriding toString() is probably all + you need to do. */ + + public String toString() { return JCRSQLParserTreeConstants.jjtNodeName[id]; } + public String toString(String prefix) { return prefix + toString(); } + + /* Override this method if you want to customize how the node dumps + out its children. */ + + public void dump(String prefix) { + System.out.println(toString(prefix)); + if (children != null) { + for (int i = 0; i < children.length; ++i) { + SimpleNode n = (SimpleNode)children[i]; + if (n != null) { + n.dump(prefix + " "); + } + } + } + } +} + diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/Token.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/Token.java new file mode 100644 index 00000000000..e443a3638b2 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/Token.java @@ -0,0 +1,97 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.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.spi.commons.query.sql; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/TokenMgrError.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/TokenMgrError.java new file mode 100644 index 00000000000..bd462b4fe13 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/TokenMgrError.java @@ -0,0 +1,149 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.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.spi.commons.query.sql; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/package-info.java new file mode 100644 index 00000000000..ba7dd6fdaaf --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.1") +package org.apache.jackrabbit.spi.commons.query.sql; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql2/Parser.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql2/Parser.java new file mode 100644 index 00000000000..d8603db6197 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql2/Parser.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.spi.commons.query.sql2; + +import javax.jcr.ValueFactory; +import javax.jcr.query.qom.QueryObjectModelFactory; + +/** + * The SQL2 parser can convert a JCR-SQL2 query to a QueryObjectModel. + * + * @deprecated use {@link org.apache.jackrabbit.commons.query.sql2.Parser} + * instead. + */ +public class Parser extends org.apache.jackrabbit.commons.query.sql2.Parser { + + + /** + * Create a new parser. A parser can be re-used, but it is not thread safe. + * + * @param factory the query object model factory + * @param valueFactory the value factory + */ + public Parser(QueryObjectModelFactory factory, ValueFactory valueFactory) { + super(factory, valueFactory); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql2/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql2/package-info.java new file mode 100644 index 00000000000..4f3bbef055d --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/sql2/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.query.sql2; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/JJTXPathState.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/JJTXPathState.java new file mode 100644 index 00000000000..81f76a22b82 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/JJTXPathState.java @@ -0,0 +1,123 @@ +/* Generated By:JJTree: Do not edit this line. /home/jukka/src/jackrabbit/jackrabbit-spi-commons/target/generated-sources/jjtree/org/apache/jackrabbit/spi/commons/query/xpath/JJTXPathState.java */ + +package org.apache.jackrabbit.spi.commons.query.xpath; + +class JJTXPathState { + private java.util.Stack nodes; + private java.util.Stack marks; + + private int sp; // number of nodes on stack + private int mk; // current mark + private boolean node_created; + + JJTXPathState() { + nodes = new java.util.Stack(); + marks = new java.util.Stack(); + sp = 0; + mk = 0; + } + + /* Determines whether the current node was actually closed and + pushed. This should only be called in the final user action of a + node scope. */ + boolean nodeCreated() { + return node_created; + } + + /* Call this to reinitialize the node stack. It is called + automatically by the parser's ReInit() method. */ + void reset() { + nodes.removeAllElements(); + marks.removeAllElements(); + sp = 0; + mk = 0; + } + + /* Returns the root node of the AST. It only makes sense to call + this after a successful parse. */ + Node rootNode() { + return (Node)nodes.elementAt(0); + } + + /* Pushes a node on to the stack. */ + void pushNode(Node n) { + nodes.push(n); + ++sp; + } + + /* Returns the node on the top of the stack, and remove it from the + stack. */ + Node popNode() { + if (--sp < mk) { + mk = ((Integer)marks.pop()).intValue(); + } + return (Node)nodes.pop(); + } + + /* Returns the node currently on the top of the stack. */ + Node peekNode() { + return (Node)nodes.peek(); + } + + /* Returns the number of children on the stack in the current node + scope. */ + int nodeArity() { + return sp - mk; + } + + + void clearNodeScope(Node n) { + while (sp > mk) { + popNode(); + } + mk = ((Integer)marks.pop()).intValue(); + } + + + void openNodeScope(Node n) { + marks.push(new Integer(mk)); + mk = sp; + n.jjtOpen(); + } + + + /* A definite node is constructed from a specified number of + children. That number of nodes are popped from the stack and + made the children of the definite node. Then the definite node + is pushed on to the stack. */ + void closeNodeScope(Node n, int num) { + mk = ((Integer)marks.pop()).intValue(); + while (num-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, num); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } + + + /* A conditional node is constructed if its condition is true. All + the nodes that have been pushed since the node was opened are + made children of the the conditional node, which is then pushed + on to the stack. If the condition is false the node is not + constructed and they are left on the stack. */ + void closeNodeScope(Node n, boolean condition) { + if (condition) { + int a = nodeArity(); + mk = ((Integer)marks.pop()).intValue(); + while (a-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, a); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } else { + mk = ((Integer)marks.pop()).intValue(); + node_created = false; + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/Node.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/Node.java new file mode 100644 index 00000000000..196a38fdfd2 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/Node.java @@ -0,0 +1,37 @@ +/* Generated By:JJTree: Do not edit this line. Node.java */ + +package org.apache.jackrabbit.spi.commons.query.xpath; + +/* All AST nodes must implement this interface. It provides basic + machinery for constructing the parent and child relationships + between nodes. */ + +public interface Node { + + /** This method is called after the node has been made the current + node. It indicates that child nodes can now be added to it. */ + public void jjtOpen(); + + /** This method is called after all the child nodes have been + added. */ + public void jjtClose(); + + /** This pair of methods are used to inform the node of its + parent. */ + public void jjtSetParent(Node n); + public Node jjtGetParent(); + + /** This method tells the node to add its argument to the node's + list of children. */ + public void jjtAddChild(Node n, int i); + + /** This method returns a child node. The children are numbered + from zero, left to right. */ + public Node jjtGetChild(int i); + + /** Return the number of children the node has. */ + public int jjtGetNumChildren(); + + /** Accept the visitor. **/ + public Object jjtAccept(XPathVisitor visitor, Object data); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/ParseException.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/ParseException.java new file mode 100644 index 00000000000..80ee485dd4c --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/ParseException.java @@ -0,0 +1,192 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +package org.apache.jackrabbit.spi.commons.query.xpath; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * {@code ParseException: } + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/QueryBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/QueryBuilder.java new file mode 100644 index 00000000000..2bc5f357cf4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/QueryBuilder.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.spi.commons.query.xpath; + +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.query.QueryNodeFactory; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.QueryTreeBuilder; + +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.Query; + +/** + * Implements the XPath query tree builder. + */ +public class QueryBuilder implements QueryTreeBuilder { + + /** + * {@inheritDoc} + */ + public QueryRootNode createQueryTree(String statement, + NameResolver resolver, + QueryNodeFactory factory) + throws InvalidQueryException { + return XPathQueryBuilder.createQuery(statement, resolver, factory); + } + + /** + * {@inheritDoc} + */ + public boolean canHandle(String language) { + return Query.XPATH.equals(language); + } + + /** + * This builder supports {@link Query#XPATH}. + * {@inheritDoc} + */ + public String[] getSupportedLanguages() { + return new String[]{Query.XPATH}; + } + + /** + * {@inheritDoc} + */ + public String toString(QueryRootNode root, NameResolver resolver) + throws InvalidQueryException { + return XPathQueryBuilder.toString(root, resolver); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/QueryFormat.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/QueryFormat.java new file mode 100644 index 00000000000..d345adcaaae --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/QueryFormat.java @@ -0,0 +1,531 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.xpath; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.TimeZone; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.query.AndQueryNode; +import org.apache.jackrabbit.spi.commons.query.DefaultQueryNodeVisitor; +import org.apache.jackrabbit.spi.commons.query.DerefQueryNode; +import org.apache.jackrabbit.spi.commons.query.ExactQueryNode; +import org.apache.jackrabbit.spi.commons.query.LocationStepQueryNode; +import org.apache.jackrabbit.spi.commons.query.NodeTypeQueryNode; +import org.apache.jackrabbit.spi.commons.query.NotQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrderQueryNode; +import org.apache.jackrabbit.spi.commons.query.PathQueryNode; +import org.apache.jackrabbit.spi.commons.query.PropertyFunctionQueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryConstants; +import org.apache.jackrabbit.spi.commons.query.QueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryNodeVisitor; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.RelationQueryNode; +import org.apache.jackrabbit.spi.commons.query.TextsearchQueryNode; +import org.apache.jackrabbit.util.ISO8601; +import org.apache.jackrabbit.util.ISO9075; + +/** + * Implements the query node tree serialization into a String. + */ +class QueryFormat implements QueryNodeVisitor, QueryConstants { + + /** + * Will be used to resolve QNames + */ + private final NameResolver resolver; + + /** + * The String representation of the query node tree + */ + private final String statement; + + /** + * List of exception objects created while creating the XPath string + */ + private final List exceptions = new ArrayList(); + + private QueryFormat(QueryRootNode root, NameResolver resolver) + throws RepositoryException { + this.resolver = resolver; + statement = root.accept(this, new StringBuffer()).toString(); + if (exceptions.size() > 0) { + Exception e = (Exception) exceptions.get(0); + throw new InvalidQueryException(e.getMessage(), e); + } + } + + /** + * Creates a XPath String representation of the QueryNode tree + * argument root. + * + * @param root the query node tree. + * @param resolver to resolve QNames. + * @return the XPath string representation of the QueryNode tree. + * @throws InvalidQueryException the query node tree cannot be represented + * as a XPath String. + */ + public static String toString(QueryRootNode root, NameResolver resolver) + throws InvalidQueryException { + try { + return new QueryFormat(root, resolver).toString(); + } + catch (RepositoryException e) { + throw new InvalidQueryException(e); + } + } + + /** + * Returns the string representation. + * + * @return the string representation. + */ + public String toString() { + return statement; + } + + //-------------< QueryNodeVisitor interface >------------------------------- + + public Object visit(QueryRootNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + node.getLocationNode().accept(this, data); + Name[] selectProps = node.getSelectProperties(); + if (selectProps.length > 0) { + sb.append('/'); + boolean union = selectProps.length > 1; + if (union) { + sb.append('('); + } + String pipe = ""; + for (int i = 0; i < selectProps.length; i++) { + try { + sb.append(pipe); + sb.append('@'); + sb.append(resolver.getJCRName(encode(selectProps[i]))); + pipe = "|"; + } catch (NamespaceException e) { + exceptions.add(e); + } + } + if (union) { + sb.append(')'); + } + } + if (node.getOrderNode() != null) { + node.getOrderNode().accept(this, data); + } + return data; + } + + public Object visit(OrQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + boolean bracket = false; + if (node.getParent() instanceof AndQueryNode) { + bracket = true; + } + if (bracket) { + sb.append("("); + } + String or = ""; + QueryNode[] operands = node.getOperands(); + for (int i = 0; i < operands.length; i++) { + sb.append(or); + operands[i].accept(this, sb); + or = " or "; + } + if (bracket) { + sb.append(")"); + } + return sb; + } + + public Object visit(AndQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + String and = ""; + QueryNode[] operands = node.getOperands(); + for (int i = 0; i < operands.length; i++) { + sb.append(and); + operands[i].accept(this, sb); + and = " and "; + } + return sb; + } + + public Object visit(NotQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + QueryNode[] operands = node.getOperands(); + if (operands.length > 0) { + try { + sb.append(resolver.getJCRName(XPathQueryBuilder.FN_NOT_10)); + sb.append("("); + operands[0].accept(this, sb); + sb.append(")"); + } catch (NamespaceException e) { + exceptions.add(e); + } + } + return sb; + } + + public Object visit(ExactQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + sb.append("@"); + try { + Name name = encode(node.getPropertyName()); + sb.append(resolver.getJCRName(name)); + sb.append("='"); + sb.append(resolver.getJCRName(node.getValue())); + } catch (NamespaceException e) { + exceptions.add(e); + } + sb.append("'"); + return sb; + } + + public Object visit(NodeTypeQueryNode node, Object data) { + // handled in location step visit + return data; + } + + public Object visit(TextsearchQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + try { + sb.append(resolver.getJCRName(XPathQueryBuilder.JCR_CONTAINS)); + sb.append("("); + Path relPath = node.getRelativePath(); + if (relPath == null) { + sb.append("."); + } else { + Path.Element[] elements = relPath.getElements(); + String slash = ""; + for (int i = 0; i < elements.length; i++) { + sb.append(slash); + slash = "/"; + if (node.getReferencesProperty() && i == elements.length - 1) { + sb.append("@"); + } + if (elements[i].getName().equals(RelationQueryNode.STAR_NAME_TEST)) { + sb.append("*"); + } else { + Name n = encode(elements[i].getName()); + sb.append(resolver.getJCRName(n)); + } + if (elements[i].getIndex() != 0) { + sb.append("[").append(elements[i].getIndex()).append("]"); + } + } + } + sb.append(", '"); + sb.append(node.getQuery().replaceAll("'", "''")); + sb.append("')"); + } catch (NamespaceException e) { + exceptions.add(e); + } + return sb; + } + + public Object visit(PathQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + if (node.isAbsolute()) { + sb.append("/"); + } + LocationStepQueryNode[] steps = node.getPathSteps(); + String slash = ""; + for (int i = 0; i < steps.length; i++) { + sb.append(slash); + steps[i].accept(this, sb); + slash = "/"; + } + return sb; + } + + public Object visit(LocationStepQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + if (node.getIncludeDescendants()) { + sb.append('/'); + } + final Name[] nodeType = new Name[1]; + node.acceptOperands(new DefaultQueryNodeVisitor() { + public Object visit(NodeTypeQueryNode node, Object data) { + nodeType[0] = node.getValue(); + return data; + } + }, null); + + if (nodeType[0] != null) { + sb.append("element("); + } + + if (node.getNameTest() == null) { + sb.append("*"); + } else { + try { + if (node.getNameTest().getLocalName().length() == 0) { + sb.append(resolver.getJCRName(XPathQueryBuilder.JCR_ROOT)); + } else { + sb.append(resolver.getJCRName(encode(node.getNameTest()))); + } + } catch (NamespaceException e) { + exceptions.add(e); + } + } + + if (nodeType[0] != null) { + sb.append(", "); + try { + sb.append(resolver.getJCRName(encode(nodeType[0]))); + } catch (NamespaceException e) { + exceptions.add(e); + } + sb.append(")"); + } + + if (node.getIndex() != LocationStepQueryNode.NONE) { + sb.append('[').append(node.getIndex()).append(']'); + } + QueryNode[] predicates = node.getPredicates(); + for (int i = 0; i < predicates.length; i++) { + // ignore node type query nodes + if (predicates[i].getType() == QueryNode.TYPE_NODETYPE) { + continue; + } + sb.append('['); + predicates[i].accept(this, sb); + sb.append(']'); + } + return sb; + } + + public Object visit(DerefQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + try { + sb.append(resolver.getJCRName(XPathQueryBuilder.JCR_DEREF)); + sb.append("(@"); + sb.append(resolver.getJCRName(encode(node.getRefProperty()))); + sb.append(", '"); + if (node.getNameTest() == null) { + sb.append("*"); + } else { + sb.append(resolver.getJCRName(encode(node.getNameTest()))); + } + sb.append("')"); + } catch (NamespaceException e) { + exceptions.add(e); + } + return sb; + } + + public Object visit(RelationQueryNode node, Object data) throws RepositoryException { + StringBuffer sb = (StringBuffer) data; + try { + + StringBuffer propPath = new StringBuffer(); + // only encode if not position function + PathQueryNode relPath = node.getRelativePath(); + if (relPath == null) { + propPath.append("."); + } else if (relPath.getNumOperands() > 0 && XPathQueryBuilder.FN_POSITION_FULL.equals(relPath.getPathSteps()[0].getNameTest())) { + propPath.append(resolver.getJCRName(XPathQueryBuilder.FN_POSITION_FULL)); + } else { + LocationStepQueryNode[] steps = relPath.getPathSteps(); + String slash = ""; + for (int i = 0; i < steps.length; i++) { + propPath.append(slash); + slash = "/"; + if (i == steps.length - 1 && node.getOperation() != OPERATION_SIMILAR) { + // last step + propPath.append("@"); + } + visit(steps[i], propPath); + } + } + + // surround name with property function + node.acceptOperands(this, propPath); + + if (node.getOperation() == OPERATION_EQ_VALUE) { + sb.append(propPath).append(" eq "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_EQ_GENERAL) { + sb.append(propPath).append(" = "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_GE_GENERAL) { + sb.append(propPath).append(" >= "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_GE_VALUE) { + sb.append(propPath).append(" ge "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_GT_GENERAL) { + sb.append(propPath).append(" > "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_GT_VALUE) { + sb.append(propPath).append(" gt "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_LE_GENERAL) { + sb.append(propPath).append(" <= "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_LE_VALUE) { + sb.append(propPath).append(" le "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_LIKE) { + sb.append(resolver.getJCRName(XPathQueryBuilder.JCR_LIKE)); + sb.append("(").append(propPath).append(", "); + appendValue(node, sb); + sb.append(")"); + } else if (node.getOperation() == OPERATION_LT_GENERAL) { + sb.append(propPath).append(" < "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_LT_VALUE) { + sb.append(propPath).append(" lt "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_NE_GENERAL) { + sb.append(propPath).append(" != "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_NE_VALUE) { + sb.append(propPath).append(" ne "); + appendValue(node, sb); + } else if (node.getOperation() == OPERATION_NULL) { + sb.append(resolver.getJCRName(XPathQueryBuilder.FN_NOT)); + sb.append("(").append(propPath).append(")"); + } else if (node.getOperation() == OPERATION_NOT_NULL) { + sb.append(propPath); + } else if (node.getOperation() == OPERATION_SIMILAR) { + sb.append(resolver.getJCRName(XPathQueryBuilder.REP_SIMILAR)); + sb.append("(").append(propPath).append(", "); + appendValue(node, sb); + sb.append(")"); + } else if (node.getOperation() == OPERATION_SPELLCHECK) { + sb.append(resolver.getJCRName(XPathQueryBuilder.REP_SPELLCHECK)); + sb.append("("); + appendValue(node, sb); + sb.append(")"); + } else { + exceptions.add(new InvalidQueryException("Invalid operation: " + node.getOperation())); + } + } catch (NamespaceException e) { + exceptions.add(e); + } + return sb; + } + + public Object visit(OrderQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + sb.append(" order by"); + OrderQueryNode.OrderSpec[] specs = node.getOrderSpecs(); + String comma = ""; + try { + for (int i = 0; i < specs.length; i++) { + sb.append(comma); + Path propPath = specs[i].getPropertyPath(); + Path.Element[] elements = propPath.getElements(); + sb.append(" "); + String slash = ""; + for (int j = 0; j < elements.length; j++) { + sb.append(slash); + slash = "/"; + Path.Element element = elements[j]; + Name name = encode(element.getName()); + if (j == elements.length - 1) { + // last + sb.append("@"); + } + sb.append(resolver.getJCRName(name)); + } + if (!specs[i].isAscending()) { + sb.append(" descending"); + } + comma = ","; + } + } catch (NamespaceException e) { + exceptions.add(e); + } + return data; + } + + public Object visit(PropertyFunctionQueryNode node, Object data) { + StringBuffer sb = (StringBuffer) data; + String functionName = node.getFunctionName(); + try { + if (functionName.equals(PropertyFunctionQueryNode.LOWER_CASE)) { + sb.insert(0, resolver.getJCRName(XPathQueryBuilder.FN_LOWER_CASE) + "("); + sb.append(")"); + } else if (functionName.equals(PropertyFunctionQueryNode.UPPER_CASE)) { + sb.insert(0, resolver.getJCRName(XPathQueryBuilder.FN_UPPER_CASE) + "("); + sb.append(")"); + } else { + exceptions.add(new InvalidQueryException("Unsupported function: " + functionName)); + } + } catch (NamespaceException e) { + exceptions.add(e); + } + return sb; + } + + //----------------------------< internal >---------------------------------- + + /** + * Appends the value of a relation node to the StringBuffer + * sb. + * + * @param node the relation node. + * @param b where to append the value. + * @throws NamespaceException if a prefix declaration is missing for + * a namespace URI. + */ + private void appendValue(RelationQueryNode node, StringBuffer b) + throws NamespaceException { + if (node.getValueType() == TYPE_LONG) { + b.append(node.getLongValue()); + } else if (node.getValueType() == TYPE_DOUBLE) { + b.append(node.getDoubleValue()); + } else if (node.getValueType() == TYPE_STRING) { + b.append("'").append(node.getStringValue().replaceAll("'", "''")).append("'"); + } else if (node.getValueType() == TYPE_DATE || node.getValueType() == TYPE_TIMESTAMP) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setTime(node.getDateValue()); + b.append(resolver.getJCRName(XPathQueryBuilder.XS_DATETIME)); + b.append("('").append(ISO8601.format(cal)).append("')"); + } else if (node.getValueType() == TYPE_POSITION) { + if (node.getPositionValue() == LocationStepQueryNode.LAST) { + b.append("last()"); + } else { + b.append(node.getPositionValue()); + } + } else { + exceptions.add(new InvalidQueryException("Invalid type: " + node.getValueType())); + } + } + + private static Name encode(Name name) { + String encoded = ISO9075.encode(name.getLocalName()); + if (encoded.equals(name.getLocalName())) { + return name; + } else { + return NameFactoryImpl.getInstance().create(name.getNamespaceURI(), encoded); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/SimpleCharStream.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/SimpleCharStream.java new file mode 100644 index 00000000000..1547afa8cb8 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/SimpleCharStream.java @@ -0,0 +1,439 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */ +package org.apache.jackrabbit.spi.commons.query.xpath; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, + bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, + available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return (c); + } + + /** + * @deprecated + * @see #getEndColumn + */ + + public int getColumn() { + return bufcolumn[bufpos]; + } + + /** + * @deprecated + * @see #getEndLine + */ + + public int getLine() { + return bufline[bufpos]; + } + + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + public int getEndLine() { + return bufline[bufpos]; + } + + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + public int getBeginLine() { + return bufline[tokenBegin]; + } + + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && + bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/SimpleNode.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/SimpleNode.java new file mode 100644 index 00000000000..d47b37c8ac1 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/SimpleNode.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.spi.commons.query.xpath; + + +/** + * Implements a JavaCC Node interface. + * This Class was initially created by JavaCC and then adapted for our needs. + */ +public class SimpleNode implements Node { + protected Node parent; + protected Node[] children; + protected int id; + protected XPath parser; + + public SimpleNode(int i) { + id = i; + } + + public SimpleNode(XPath p, int i) { + this(i); + parser = p; + } + + // Factory method + public static Node jjtCreate(XPath p, int id) { + return new SimpleNode(p, id); + } + + public void jjtOpen() { + } + + public void jjtClose() { + } + + public void jjtSetParent(Node n) { + parent = n; + } + + public Node jjtGetParent() { + return parent; + } + + public void jjtAddChild(Node n, int i) { + if (children == null) { + children = new Node[i + 1]; + } else if (i >= children.length) { + Node[] c = new Node[i + 1]; + System.arraycopy(children, 0, c, 0, children.length); + children = c; + } + children[i] = n; + } + + public Node jjtGetChild(int i) { + return children[i]; + } + + public int jjtGetNumChildren() { + return (children == null) ? 0 : children.length; + } + + /** + * Accept the visitor. * + */ + public Object jjtAccept(XPathVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + /** + * Accept the visitor. * + */ + public Object childrenAccept(XPathVisitor visitor, Object data) { + if (children != null) { + for (int i = 0; i < children.length; ++i) { + data = children[i].jjtAccept(visitor, data); + } + } + return data; + } + + /* You can override these two methods in subclasses of SimpleNode to + customize the way the node appears when the tree is dumped. If + your output uses more than one line you should override + toString(String), otherwise overriding toString() is probably all + you need to do. */ + + public String toString() { + return XPathTreeConstants.jjtNodeName[id]; + } + + public String toString(String prefix) { + return prefix + toString(); + } + + public void dump(String prefix) { + dump(prefix, System.out); + } + + public void dump(String prefix, java.io.PrintStream ps) { + ps.print(toString(prefix)); + printValue(ps); + ps.println(); + if (children != null) { + for (int i = 0; i < children.length; ++i) { + SimpleNode n = (SimpleNode) children[i]; + if (n != null) { + n.dump(prefix + " ", ps); + } + } + } + } + + + // Manually inserted code begins here + + protected String m_value; + + public void processToken(Token t) { + m_value = t.image; + } + + public void printValue(java.io.PrintStream ps) { + if (null != m_value) { + ps.print(" " + m_value); + } + } + + public int getId() { + return id; + } + + public String getValue() { + return m_value; + } + +} + + diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/Token.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/Token.java new file mode 100644 index 00000000000..31d52e185e8 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/Token.java @@ -0,0 +1,81 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */ +package org.apache.jackrabbit.spi.commons.query.xpath; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/TokenMgrError.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/TokenMgrError.java new file mode 100644 index 00000000000..8811d2aeb4f --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/TokenMgrError.java @@ -0,0 +1,133 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */ +package org.apache.jackrabbit.spi.commons.query.xpath; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPath.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPath.java new file mode 100644 index 00000000000..67bbf02f577 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPath.java @@ -0,0 +1,9259 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. XPath.java */ + package org.apache.jackrabbit.spi.commons.query.xpath; + +import java.io.*; +import java.util.Stack; +import java.util.Vector; + +public class XPath/*@bgen(jjtree)*/implements XPathTreeConstants, XPathConstants {/*@bgen(jjtree)*/ + protected JJTXPathState jjtree = new JJTXPathState(); + + boolean m_isMatchPattern = false; + boolean isStep = false; + + Stack binaryTokenStack = new Stack(); + + public Node createNode(int id) { + return null; + } + + + + public static void main(String args[]) + throws Exception + { + int numberArgsLeft = args.length; + int argsStart = 0; + boolean isMatchParser = false; + if(numberArgsLeft > 0) + { + if(args[argsStart].equals("-match")) + { + isMatchParser = true; + System.out.println("Match Pattern Parser"); + argsStart++; + numberArgsLeft--; + } + } + if(numberArgsLeft > 0) + { + try + { + final boolean dumpTree = true; + if(args[0].endsWith(".xquery")) + { + System.out.println("Running test for: "+args[0]); + File file = new File(args[0]); + FileInputStream fis = new FileInputStream(file); + XPath parser = new XPath(fis); + SimpleNode tree = parser.XPath2(); + if(dumpTree) + tree.dump("|") ; + } + else + { + for(int i = argsStart; i < args.length; i++) + { + System.out.println(); + System.out.println("Test["+i+"]: "+args[i]); + XPath parser = new XPath(new java.io.StringBufferInputStream(args[i])); + SimpleNode tree; + if(isMatchParser) + { + tree = parser.XPath2(); + } + else + { + tree = parser.XPath2(); + } + ((SimpleNode)tree.jjtGetChild(0)).dump("|") ; + } + System.out.println("Success!!!!"); + } + } + catch(ParseException pe) + { + System.err.println(pe.getMessage()); + } + return; + } + java.io.DataInputStream dinput = new java.io.DataInputStream(System.in); + while(true) + { + try + { + System.err.println("Type Expression: "); + String input = dinput.readLine(); + if(null == input || input.trim().length() == 0) + break; + XPath parser = new XPath(new java.io.StringBufferInputStream(input)); + SimpleNode tree; + if(isMatchParser) + { + tree = parser.XPath2(); + } + else + { + tree = parser.XPath2(); + } + ((SimpleNode)tree.jjtGetChild(0)).dump("|") ; + } + catch(ParseException pe) + { + System.err.println(pe.getMessage()); + } + catch(Exception e) + { + System.err.println(e.getMessage()); + } + } + } + + final public SimpleNode XPath2() throws ParseException { + /*@bgen(jjtree) XPath2 */ + SimpleNode jjtn000 = new SimpleNode(this, JJTXPATH2); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + QueryList(); + jj_consume_token(0); + jjtree.closeNodeScope(jjtn000, true); + jjtc000 = false; + {if (true) return jjtn000 ;} + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + throw new Error("Missing return statement in function"); + } + + final public void QueryList() throws ParseException { + /*@bgen(jjtree) QueryList */ + SimpleNode jjtn000 = new SimpleNode(this, JJTQUERYLIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + Module(); + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case QuerySeparator: + ; + break; + default: + jj_la1[0] = jj_gen; + break label_1; + } + jj_consume_token(QuerySeparator); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case XQueryVersion: + case ModuleNamespace: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case DefineFunction: + case DeclareOrdering: + case DeclareDefaultOrderingEmpty: + case DeclareInheritNamespaces: + case VariableIndicator: + case DeclareConstruction: + case DeclareXMLSpace: + case DeclareBaseURI: + case DeclareNamespace: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case DeclareCollation: + case DeclareDefaultElement: + case DeclareDefaultFunction: + case ImportSchemaToken: + case ImportModuleToken: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case UnaryMinus: + case UnaryPlus: + case Lpar: + case At: + case Some: + case Every: + case ForVariable: + case LetVariable: + case ValidateLbrace: + case ValidateSchemaMode: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case IfLpar: + case TypeswitchLpar: + case Dot: + case DotDot: + case DefineVariable: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + Module(); + break; + default: + jj_la1[1] = jj_gen; + ; + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Module() throws ParseException { + /*@bgen(jjtree) Module */ + SimpleNode jjtn000 = new SimpleNode(this, JJTMODULE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case XQueryVersion: + VersionDecl(); + break; + default: + jj_la1[2] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case DefineFunction: + case DeclareOrdering: + case DeclareDefaultOrderingEmpty: + case DeclareInheritNamespaces: + case VariableIndicator: + case DeclareConstruction: + case DeclareXMLSpace: + case DeclareBaseURI: + case DeclareNamespace: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case DeclareCollation: + case DeclareDefaultElement: + case DeclareDefaultFunction: + case ImportSchemaToken: + case ImportModuleToken: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case UnaryMinus: + case UnaryPlus: + case Lpar: + case At: + case Some: + case Every: + case ForVariable: + case LetVariable: + case ValidateLbrace: + case ValidateSchemaMode: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case IfLpar: + case TypeswitchLpar: + case Dot: + case DotDot: + case DefineVariable: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + MainModule(); + break; + case ModuleNamespace: + LibraryModule(); + break; + default: + jj_la1[3] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void VersionDecl() throws ParseException { + /*@bgen(jjtree) VersionDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTVERSIONDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(XQueryVersion); + SimpleNode jjtn001 = new SimpleNode(this, JJTXQUERYVERSION); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(StringLiteralForVersion); + SimpleNode jjtn002 = new SimpleNode(this, JJTSTRINGLITERALFORVERSION); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case XQueryEncoding: + jj_consume_token(XQueryEncoding); + SimpleNode jjtn003 = new SimpleNode(this, JJTXQUERYENCODING); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + jj_consume_token(StringLiteralForVersion); + SimpleNode jjtn004 = new SimpleNode(this, JJTSTRINGLITERALFORVERSION); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + default: + jj_la1[4] = jj_gen; + ; + } + Separator(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void MainModule() throws ParseException { + /*@bgen(jjtree) MainModule */ + SimpleNode jjtn000 = new SimpleNode(this, JJTMAINMODULE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + Prolog(); + QueryBody(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void LibraryModule() throws ParseException { + /*@bgen(jjtree) LibraryModule */ + SimpleNode jjtn000 = new SimpleNode(this, JJTLIBRARYMODULE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + ModuleDecl(); + Prolog(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ModuleDecl() throws ParseException { + /*@bgen(jjtree) ModuleDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTMODULEDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(ModuleNamespace); + SimpleNode jjtn001 = new SimpleNode(this, JJTMODULENAMESPACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(NCNameForPrefix); + SimpleNode jjtn002 = new SimpleNode(this, JJTNCNAMEFORPREFIX); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + jj_consume_token(AssignEquals); + SimpleNode jjtn003 = new SimpleNode(this, JJTASSIGNEQUALS); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + jj_consume_token(URLLiteral); + SimpleNode jjtn004 = new SimpleNode(this, JJTURLLITERAL); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + Separator(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Prolog() throws ParseException { + /*@bgen(jjtree) Prolog */ + SimpleNode jjtn000 = new SimpleNode(this, JJTPROLOG); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + label_2: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DeclareOrdering: + case DeclareDefaultOrderingEmpty: + case DeclareInheritNamespaces: + case DeclareConstruction: + case DeclareXMLSpace: + case DeclareBaseURI: + case DeclareCollation: + ; + break; + default: + jj_la1[5] = jj_gen; + break label_2; + } + Setter(); + Separator(); + } + label_3: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DeclareNamespace: + case DeclareDefaultElement: + case DeclareDefaultFunction: + case ImportSchemaToken: + case ImportModuleToken: + ; + break; + default: + jj_la1[6] = jj_gen; + break label_3; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ImportSchemaToken: + case ImportModuleToken: + Import(); + break; + case DeclareNamespace: + NamespaceDecl(); + break; + case DeclareDefaultElement: + case DeclareDefaultFunction: + DefaultNamespaceDecl(); + break; + default: + jj_la1[7] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + Separator(); + } + label_4: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DefineFunction: + case DefineVariable: + ; + break; + default: + jj_la1[8] = jj_gen; + break label_4; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DefineVariable: + VarDecl(); + break; + case DefineFunction: + FunctionDecl(); + break; + default: + jj_la1[9] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + Separator(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Setter() throws ParseException { + /*@bgen(jjtree) Setter */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSETTER); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DeclareXMLSpace: + XMLSpaceDecl(); + break; + case DeclareCollation: + DefaultCollationDecl(); + break; + case DeclareBaseURI: + BaseURIDecl(); + break; + case DeclareConstruction: + ConstructionDecl(); + break; + case DeclareOrdering: + OrderingModeDecl(); + break; + case DeclareDefaultOrderingEmpty: + EmptyOrderingDecl(); + break; + case DeclareInheritNamespaces: + InheritNamespacesDecl(); + break; + default: + jj_la1[10] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Import() throws ParseException { + /*@bgen(jjtree) Import */ + SimpleNode jjtn000 = new SimpleNode(this, JJTIMPORT); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ImportSchemaToken: + SchemaImport(); + break; + case ImportModuleToken: + ModuleImport(); + break; + default: + jj_la1[11] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Separator() throws ParseException { + /*@bgen(jjtree) Separator */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSEPARATOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(SemiColon); + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void NamespaceDecl() throws ParseException { + /*@bgen(jjtree) NamespaceDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTNAMESPACEDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DeclareNamespace); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLARENAMESPACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(NCNameForPrefix); + SimpleNode jjtn002 = new SimpleNode(this, JJTNCNAMEFORPREFIX); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + jj_consume_token(AssignEquals); + SimpleNode jjtn003 = new SimpleNode(this, JJTASSIGNEQUALS); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + jj_consume_token(URLLiteral); + SimpleNode jjtn004 = new SimpleNode(this, JJTURLLITERAL); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void XMLSpaceDecl() throws ParseException { + /*@bgen(jjtree) XMLSpaceDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTXMLSPACEDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DeclareXMLSpace); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLAREXMLSPACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case XMLSpacePreserve: + jj_consume_token(XMLSpacePreserve); + SimpleNode jjtn002 = new SimpleNode(this, JJTXMLSPACEPRESERVE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case XMLSpaceStrip: + jj_consume_token(XMLSpaceStrip); + SimpleNode jjtn003 = new SimpleNode(this, JJTXMLSPACESTRIP); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[12] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DefaultNamespaceDecl() throws ParseException { + /*@bgen(jjtree) DefaultNamespaceDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDEFAULTNAMESPACEDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DeclareDefaultElement: + jj_consume_token(DeclareDefaultElement); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLAREDEFAULTELEMENT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case DeclareDefaultFunction: + jj_consume_token(DeclareDefaultFunction); + SimpleNode jjtn002 = new SimpleNode(this, JJTDECLAREDEFAULTFUNCTION); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[13] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(Namespace); + SimpleNode jjtn003 = new SimpleNode(this, JJTNAMESPACE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + jj_consume_token(URLLiteral); + SimpleNode jjtn004 = new SimpleNode(this, JJTURLLITERAL); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void OrderingModeDecl() throws ParseException { + /*@bgen(jjtree) OrderingModeDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTORDERINGMODEDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DeclareOrdering); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLAREORDERING); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Ordered: + jj_consume_token(Ordered); + SimpleNode jjtn002 = new SimpleNode(this, JJTORDERED); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case Unordered: + jj_consume_token(Unordered); + SimpleNode jjtn003 = new SimpleNode(this, JJTUNORDERED); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[14] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void EmptyOrderingDecl() throws ParseException { + /*@bgen(jjtree) EmptyOrderingDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTEMPTYORDERINGDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DeclareDefaultOrderingEmpty); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLAREDEFAULTORDERINGEMPTY); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EmptyGreatest: + jj_consume_token(EmptyGreatest); + SimpleNode jjtn002 = new SimpleNode(this, JJTEMPTYGREATEST); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case EmptyLeast: + jj_consume_token(EmptyLeast); + SimpleNode jjtn003 = new SimpleNode(this, JJTEMPTYLEAST); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[15] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void InheritNamespacesDecl() throws ParseException { + /*@bgen(jjtree) InheritNamespacesDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTINHERITNAMESPACESDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DeclareInheritNamespaces); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLAREINHERITNAMESPACES); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Yes: + jj_consume_token(Yes); + SimpleNode jjtn002 = new SimpleNode(this, JJTYES); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case No: + jj_consume_token(No); + SimpleNode jjtn003 = new SimpleNode(this, JJTNO); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[16] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DefaultCollationDecl() throws ParseException { + /*@bgen(jjtree) DefaultCollationDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDEFAULTCOLLATIONDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DeclareCollation); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLARECOLLATION); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(URLLiteral); + SimpleNode jjtn002 = new SimpleNode(this, JJTURLLITERAL); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void BaseURIDecl() throws ParseException { + /*@bgen(jjtree) BaseURIDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTBASEURIDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DeclareBaseURI); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLAREBASEURI); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(URLLiteral); + SimpleNode jjtn002 = new SimpleNode(this, JJTURLLITERAL); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void SchemaImport() throws ParseException { + /*@bgen(jjtree) SchemaImport */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSCHEMAIMPORT); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(ImportSchemaToken); + SimpleNode jjtn001 = new SimpleNode(this, JJTIMPORTSCHEMATOKEN); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Namespace: + case DefaultElement: + SchemaPrefix(); + break; + default: + jj_la1[17] = jj_gen; + ; + } + jj_consume_token(URLLiteral); + SimpleNode jjtn002 = new SimpleNode(this, JJTURLLITERAL); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AtStringLiteral: + jj_consume_token(AtStringLiteral); + SimpleNode jjtn003 = new SimpleNode(this, JJTATSTRINGLITERAL); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + label_5: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[18] = jj_gen; + break label_5; + } + jj_consume_token(Comma); + jj_consume_token(StringLiteral); + SimpleNode jjtn004 = new SimpleNode(this, JJTSTRINGLITERAL); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + } + break; + default: + jj_la1[19] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void SchemaPrefix() throws ParseException { + /*@bgen(jjtree) SchemaPrefix */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSCHEMAPREFIX); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Namespace: + jj_consume_token(Namespace); + SimpleNode jjtn001 = new SimpleNode(this, JJTNAMESPACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(NCNameForPrefix); + SimpleNode jjtn002 = new SimpleNode(this, JJTNCNAMEFORPREFIX); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + jj_consume_token(AssignEquals); + SimpleNode jjtn003 = new SimpleNode(this, JJTASSIGNEQUALS); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + case DefaultElement: + jj_consume_token(DefaultElement); + SimpleNode jjtn004 = new SimpleNode(this, JJTDEFAULTELEMENT); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + jj_consume_token(Namespace); + SimpleNode jjtn005 = new SimpleNode(this, JJTNAMESPACE); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + break; + default: + jj_la1[20] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ModuleImport() throws ParseException { + /*@bgen(jjtree) ModuleImport */ + SimpleNode jjtn000 = new SimpleNode(this, JJTMODULEIMPORT); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(ImportModuleToken); + SimpleNode jjtn001 = new SimpleNode(this, JJTIMPORTMODULETOKEN); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Namespace: + jj_consume_token(Namespace); + SimpleNode jjtn002 = new SimpleNode(this, JJTNAMESPACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + jj_consume_token(NCNameForPrefix); + SimpleNode jjtn003 = new SimpleNode(this, JJTNCNAMEFORPREFIX); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + jj_consume_token(AssignEquals); + SimpleNode jjtn004 = new SimpleNode(this, JJTASSIGNEQUALS); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + default: + jj_la1[21] = jj_gen; + ; + } + jj_consume_token(URLLiteral); + SimpleNode jjtn005 = new SimpleNode(this, JJTURLLITERAL); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AtStringLiteral: + jj_consume_token(AtStringLiteral); + SimpleNode jjtn006 = new SimpleNode(this, JJTATSTRINGLITERAL); + boolean jjtc006 = true; + jjtree.openNodeScope(jjtn006); + try { + jjtree.closeNodeScope(jjtn006, true); + jjtc006 = false; + jjtn006.processToken(token); + } finally { + if (jjtc006) { + jjtree.closeNodeScope(jjtn006, true); + } + } + label_6: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[22] = jj_gen; + break label_6; + } + jj_consume_token(Comma); + jj_consume_token(StringLiteral); + SimpleNode jjtn007 = new SimpleNode(this, JJTSTRINGLITERAL); + boolean jjtc007 = true; + jjtree.openNodeScope(jjtn007); + try { + jjtree.closeNodeScope(jjtn007, true); + jjtc007 = false; + jjtn007.processToken(token); + } finally { + if (jjtc007) { + jjtree.closeNodeScope(jjtn007, true); + } + } + } + break; + default: + jj_la1[23] = jj_gen; + ; + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void VarDecl() throws ParseException { + /*@bgen(jjtree) VarDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTVARDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DefineVariable); + SimpleNode jjtn001 = new SimpleNode(this, JJTDEFINEVARIABLE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(VarName); + SimpleNode jjtn002 = new SimpleNode(this, JJTVARNAME); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + TypeDeclaration(); + break; + default: + jj_la1[24] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ColonEquals: + jj_consume_token(ColonEquals); + SimpleNode jjtn003 = new SimpleNode(this, JJTCOLONEQUALS); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + ExprSingle(); + break; + case External: + jj_consume_token(External); + SimpleNode jjtn004 = new SimpleNode(this, JJTEXTERNAL); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + default: + jj_la1[25] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ConstructionDecl() throws ParseException { + /*@bgen(jjtree) ConstructionDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCONSTRUCTIONDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DeclareConstruction); + SimpleNode jjtn001 = new SimpleNode(this, JJTDECLARECONSTRUCTION); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(SchemaModeForDeclareConstruction); + SimpleNode jjtn002 = new SimpleNode(this, JJTSCHEMAMODEFORDECLARECONSTRUCTION); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void FunctionDecl() throws ParseException { + /*@bgen(jjtree) FunctionDecl */ + SimpleNode jjtn000 = new SimpleNode(this, JJTFUNCTIONDECL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DefineFunction); + SimpleNode jjtn001 = new SimpleNode(this, JJTDEFINEFUNCTION); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(QNameLpar); + SimpleNode jjtn002 = new SimpleNode(this, JJTQNAMELPAR); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case VariableIndicator: + ParamList(); + break; + default: + jj_la1[26] = jj_gen; + ; + } + jj_consume_token(Rpar); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + jj_consume_token(As); + SimpleNode jjtn003 = new SimpleNode(this, JJTAS); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + SequenceType(); + break; + default: + jj_la1[27] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Lbrace: + case LbraceExprEnclosure: + EnclosedExpr(); + break; + case External: + jj_consume_token(External); + SimpleNode jjtn004 = new SimpleNode(this, JJTEXTERNAL); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + default: + jj_la1[28] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ParamList() throws ParseException { + /*@bgen(jjtree) ParamList */ + SimpleNode jjtn000 = new SimpleNode(this, JJTPARAMLIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + Param(); + label_7: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[29] = jj_gen; + break label_7; + } + jj_consume_token(Comma); + Param(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Param() throws ParseException { + /*@bgen(jjtree) Param */ + SimpleNode jjtn000 = new SimpleNode(this, JJTPARAM); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(VariableIndicator); + jj_consume_token(VarName); + SimpleNode jjtn001 = new SimpleNode(this, JJTVARNAME); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + TypeDeclaration(); + break; + default: + jj_la1[30] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void EnclosedExpr() throws ParseException { + /*@bgen(jjtree) EnclosedExpr */ + SimpleNode jjtn000 = new SimpleNode(this, JJTENCLOSEDEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Lbrace: + jj_consume_token(Lbrace); + SimpleNode jjtn001 = new SimpleNode(this, JJTLBRACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case LbraceExprEnclosure: + jj_consume_token(LbraceExprEnclosure); + SimpleNode jjtn002 = new SimpleNode(this, JJTLBRACEEXPRENCLOSURE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[31] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn003 = new SimpleNode(this, JJTRBRACE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void QueryBody() throws ParseException { + /*@bgen(jjtree) QueryBody */ + SimpleNode jjtn000 = new SimpleNode(this, JJTQUERYBODY); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + Expr(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Expr() throws ParseException { + /*@bgen(jjtree) Expr */ + SimpleNode jjtn000 = new SimpleNode(this, JJTEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + ExprSingle(); + label_8: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[32] = jj_gen; + break label_8; + } + jj_consume_token(Comma); + ExprSingle(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ExprSingle() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ForVariable: + case LetVariable: + FLWORExpr(); + break; + case Some: + case Every: + QuantifiedExpr(); + break; + case TypeswitchLpar: + TypeswitchExpr(); + break; + case IfLpar: + IfExpr(); + break; + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case UnaryMinus: + case UnaryPlus: + case Lpar: + case At: + case ValidateLbrace: + case ValidateSchemaMode: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + OrExpr(); + break; + default: + jj_la1[33] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void FLWORExpr() throws ParseException { + /*@bgen(jjtree) FLWORExpr */ + SimpleNode jjtn000 = new SimpleNode(this, JJTFLWOREXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + label_9: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ForVariable: + ForClause(); + break; + case LetVariable: + LetClause(); + break; + default: + jj_la1[34] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ForVariable: + case LetVariable: + ; + break; + default: + jj_la1[35] = jj_gen; + break label_9; + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Where: + WhereClause(); + break; + default: + jj_la1[36] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OrderBy: + case OrderByStable: + OrderByClause(); + break; + default: + jj_la1[37] = jj_gen; + ; + } + jj_consume_token(Return); + ExprSingle(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ForClause() throws ParseException { + jj_consume_token(ForVariable); + jj_consume_token(VarName); + SimpleNode jjtn001 = new SimpleNode(this, JJTVARNAME); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + TypeDeclaration(); + break; + default: + jj_la1[38] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AtWord: + PositionalVar(); + break; + default: + jj_la1[39] = jj_gen; + ; + } + jj_consume_token(In); + SimpleNode jjtn002 = new SimpleNode(this, JJTIN); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + ExprSingle(); + label_10: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[40] = jj_gen; + break label_10; + } + jj_consume_token(Comma); + jj_consume_token(VariableIndicator); + jj_consume_token(VarName); + SimpleNode jjtn003 = new SimpleNode(this, JJTVARNAME); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + TypeDeclaration(); + break; + default: + jj_la1[41] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AtWord: + PositionalVar(); + break; + default: + jj_la1[42] = jj_gen; + ; + } + jj_consume_token(In); + SimpleNode jjtn004 = new SimpleNode(this, JJTIN); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + ExprSingle(); + } + } + + final public void PositionalVar() throws ParseException { + /*@bgen(jjtree) PositionalVar */ + SimpleNode jjtn000 = new SimpleNode(this, JJTPOSITIONALVAR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(AtWord); + SimpleNode jjtn001 = new SimpleNode(this, JJTATWORD); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(VariableIndicator); + jj_consume_token(VarName); + SimpleNode jjtn002 = new SimpleNode(this, JJTVARNAME); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void LetClause() throws ParseException { + /*@bgen(jjtree) LetClause */ + SimpleNode jjtn000 = new SimpleNode(this, JJTLETCLAUSE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(LetVariable); + SimpleNode jjtn001 = new SimpleNode(this, JJTLETVARIABLE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + jj_consume_token(VarName); + SimpleNode jjtn002 = new SimpleNode(this, JJTVARNAME); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + TypeDeclaration(); + break; + default: + jj_la1[43] = jj_gen; + ; + } + jj_consume_token(ColonEquals); + SimpleNode jjtn003 = new SimpleNode(this, JJTCOLONEQUALS); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + ExprSingle(); + label_11: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[44] = jj_gen; + break label_11; + } + jj_consume_token(Comma); + jj_consume_token(VariableIndicator); + jj_consume_token(VarName); + SimpleNode jjtn004 = new SimpleNode(this, JJTVARNAME); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + TypeDeclaration(); + break; + default: + jj_la1[45] = jj_gen; + ; + } + jj_consume_token(ColonEquals); + SimpleNode jjtn005 = new SimpleNode(this, JJTCOLONEQUALS); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + ExprSingle(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void WhereClause() throws ParseException { + /*@bgen(jjtree) WhereClause */ + SimpleNode jjtn000 = new SimpleNode(this, JJTWHERECLAUSE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(Where); + SimpleNode jjtn001 = new SimpleNode(this, JJTWHERE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + ExprSingle(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void OrderByClause() throws ParseException { + /*@bgen(jjtree) OrderByClause */ + SimpleNode jjtn000 = new SimpleNode(this, JJTORDERBYCLAUSE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OrderBy: + jj_consume_token(OrderBy); + SimpleNode jjtn001 = new SimpleNode(this, JJTORDERBY); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case OrderByStable: + jj_consume_token(OrderByStable); + SimpleNode jjtn002 = new SimpleNode(this, JJTORDERBYSTABLE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[46] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + OrderSpecList(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void OrderSpecList() throws ParseException { + /*@bgen(jjtree) OrderSpecList */ + SimpleNode jjtn000 = new SimpleNode(this, JJTORDERSPECLIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + OrderSpec(); + label_12: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[47] = jj_gen; + break label_12; + } + jj_consume_token(Comma); + OrderSpec(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void OrderSpec() throws ParseException { + /*@bgen(jjtree) OrderSpec */ + SimpleNode jjtn000 = new SimpleNode(this, JJTORDERSPEC); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + ExprSingle(); + OrderModifier(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void OrderModifier() throws ParseException { + /*@bgen(jjtree) OrderModifier */ + SimpleNode jjtn000 = new SimpleNode(this, JJTORDERMODIFIER); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Ascending: + case Descending: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Ascending: + jj_consume_token(Ascending); + SimpleNode jjtn001 = new SimpleNode(this, JJTASCENDING); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case Descending: + jj_consume_token(Descending); + SimpleNode jjtn002 = new SimpleNode(this, JJTDESCENDING); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[48] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[49] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EmptyGreatest: + case EmptyLeast: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EmptyGreatest: + jj_consume_token(EmptyGreatest); + SimpleNode jjtn003 = new SimpleNode(this, JJTEMPTYGREATEST); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + case EmptyLeast: + jj_consume_token(EmptyLeast); + SimpleNode jjtn004 = new SimpleNode(this, JJTEMPTYLEAST); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + default: + jj_la1[50] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[51] = jj_gen; + ; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Collation: + jj_consume_token(Collation); + SimpleNode jjtn005 = new SimpleNode(this, JJTCOLLATION); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + jj_consume_token(StringLiteral); + SimpleNode jjtn006 = new SimpleNode(this, JJTSTRINGLITERAL); + boolean jjtc006 = true; + jjtree.openNodeScope(jjtn006); + try { + jjtree.closeNodeScope(jjtn006, true); + jjtc006 = false; + jjtn006.processToken(token); + } finally { + if (jjtc006) { + jjtree.closeNodeScope(jjtn006, true); + } + } + break; + default: + jj_la1[52] = jj_gen; + ; + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void QuantifiedExpr() throws ParseException { + /*@bgen(jjtree) QuantifiedExpr */ + SimpleNode jjtn000 = new SimpleNode(this, JJTQUANTIFIEDEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Some: + jj_consume_token(Some); + SimpleNode jjtn001 = new SimpleNode(this, JJTSOME); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case Every: + jj_consume_token(Every); + SimpleNode jjtn002 = new SimpleNode(this, JJTEVERY); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[53] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(VarName); + SimpleNode jjtn003 = new SimpleNode(this, JJTVARNAME); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + TypeDeclaration(); + break; + default: + jj_la1[54] = jj_gen; + ; + } + jj_consume_token(In); + SimpleNode jjtn004 = new SimpleNode(this, JJTIN); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + ExprSingle(); + label_13: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[55] = jj_gen; + break label_13; + } + jj_consume_token(Comma); + jj_consume_token(VariableIndicator); + jj_consume_token(VarName); + SimpleNode jjtn005 = new SimpleNode(this, JJTVARNAME); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case As: + TypeDeclaration(); + break; + default: + jj_la1[56] = jj_gen; + ; + } + jj_consume_token(In); + SimpleNode jjtn006 = new SimpleNode(this, JJTIN); + boolean jjtc006 = true; + jjtree.openNodeScope(jjtn006); + try { + jjtree.closeNodeScope(jjtn006, true); + jjtc006 = false; + jjtn006.processToken(token); + } finally { + if (jjtc006) { + jjtree.closeNodeScope(jjtn006, true); + } + } + ExprSingle(); + } + jj_consume_token(Satisfies); + SimpleNode jjtn007 = new SimpleNode(this, JJTSATISFIES); + boolean jjtc007 = true; + jjtree.openNodeScope(jjtn007); + try { + jjtree.closeNodeScope(jjtn007, true); + jjtc007 = false; + jjtn007.processToken(token); + } finally { + if (jjtc007) { + jjtree.closeNodeScope(jjtn007, true); + } + } + ExprSingle(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void TypeswitchExpr() throws ParseException { + /*@bgen(jjtree) TypeswitchExpr */ + SimpleNode jjtn000 = new SimpleNode(this, JJTTYPESWITCHEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(TypeswitchLpar); + Expr(); + jj_consume_token(Rpar); + label_14: + while (true) { + CaseClause(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Case: + ; + break; + default: + jj_la1[57] = jj_gen; + break label_14; + } + } + jj_consume_token(Default); + SimpleNode jjtn001 = new SimpleNode(this, JJTDEFAULT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case VariableIndicator: + jj_consume_token(VariableIndicator); + jj_consume_token(VarName); + SimpleNode jjtn002 = new SimpleNode(this, JJTVARNAME); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[58] = jj_gen; + ; + } + jj_consume_token(Return); + ExprSingle(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CaseClause() throws ParseException { + /*@bgen(jjtree) CaseClause */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCASECLAUSE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(Case); + SimpleNode jjtn001 = new SimpleNode(this, JJTCASE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case VariableIndicator: + jj_consume_token(VariableIndicator); + jj_consume_token(VarName); + SimpleNode jjtn002 = new SimpleNode(this, JJTVARNAME); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + jj_consume_token(As); + SimpleNode jjtn003 = new SimpleNode(this, JJTAS); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[59] = jj_gen; + ; + } + SequenceType(); + jj_consume_token(Return); + ExprSingle(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void IfExpr() throws ParseException { + /*@bgen(jjtree) IfExpr */ + SimpleNode jjtn000 = new SimpleNode(this, JJTIFEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(IfLpar); + Expr(); + jj_consume_token(Rpar); + jj_consume_token(Then); + ExprSingle(); + jj_consume_token(Else); + ExprSingle(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void OperatorExpr() throws ParseException { + OrExpr(); + } + + final public void OrExpr() throws ParseException { + /*@bgen(jjtree) #OrExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTOREXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + AndExpr(); + label_15: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Or: + ; + break; + default: + jj_la1[60] = jj_gen; + break label_15; + } + jj_consume_token(Or); + binaryTokenStack.push(token); + AndExpr(); + SimpleNode jjtn001 = new SimpleNode(this, JJTOREXPR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, 2); + jjtc001 = false; + try + { + jjtn001.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + {if (true) throw e;} + } + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void AndExpr() throws ParseException { + /*@bgen(jjtree) #AndExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTANDEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + ComparisonExpr(); + label_16: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case And: + ; + break; + default: + jj_la1[61] = jj_gen; + break label_16; + } + jj_consume_token(And); + binaryTokenStack.push(token); + ComparisonExpr(); + SimpleNode jjtn001 = new SimpleNode(this, JJTANDEXPR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, 2); + jjtc001 = false; + try + { + jjtn001.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + {if (true) throw e;} + } + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void ComparisonExpr() throws ParseException { + /*@bgen(jjtree) #ComparisonExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMPARISONEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + RangeExpr(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Equals: + case Is: + case NotEquals: + case LtEquals: + case LtLt: + case GtEquals: + case GtGt: + case FortranEq: + case FortranNe: + case FortranGt: + case FortranGe: + case FortranLt: + case FortranLe: + case Lt: + case Gt: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FortranEq: + case FortranNe: + case FortranGt: + case FortranGe: + case FortranLt: + case FortranLe: + ValueComp(); + break; + case Equals: + case NotEquals: + case LtEquals: + case GtEquals: + case Lt: + case Gt: + GeneralComp(); + break; + case Is: + case LtLt: + case GtGt: + NodeComp(); + break; + default: + jj_la1[62] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + RangeExpr(); + SimpleNode jjtn001 = new SimpleNode(this, JJTCOMPARISONEXPR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, 2); + jjtc001 = false; + try + { + jjtn001.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + {if (true) throw e;} + } + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + break; + default: + jj_la1[63] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void RangeExpr() throws ParseException { + /*@bgen(jjtree) #RangeExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTRANGEEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + AdditiveExpr(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case To: + jj_consume_token(To); + binaryTokenStack.push(token); + AdditiveExpr(); + SimpleNode jjtn001 = new SimpleNode(this, JJTRANGEEXPR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, 2); + jjtc001 = false; + try + { + jjtn001.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + {if (true) throw e;} + } + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + break; + default: + jj_la1[64] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void AdditiveExpr() throws ParseException { + /*@bgen(jjtree) #AdditiveExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTADDITIVEEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + MultiplicativeExpr(); + label_17: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Minus: + case Plus: + ; + break; + default: + jj_la1[65] = jj_gen; + break label_17; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Plus: + jj_consume_token(Plus); + binaryTokenStack.push(token); + break; + case Minus: + jj_consume_token(Minus); + binaryTokenStack.push(token); + break; + default: + jj_la1[66] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + MultiplicativeExpr(); + SimpleNode jjtn001 = new SimpleNode(this, JJTADDITIVEEXPR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, 2); + jjtc001 = false; + try + { + jjtn001.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + {if (true) throw e;} + } + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void MultiplicativeExpr() throws ParseException { + /*@bgen(jjtree) #MultiplicativeExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTMULTIPLICATIVEEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + UnionExpr(); + label_18: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Div: + case Idiv: + case Mod: + case Multiply: + ; + break; + default: + jj_la1[67] = jj_gen; + break label_18; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Multiply: + jj_consume_token(Multiply); + binaryTokenStack.push(token); + break; + case Div: + jj_consume_token(Div); + binaryTokenStack.push(token); + break; + case Idiv: + jj_consume_token(Idiv); + binaryTokenStack.push(token); + break; + case Mod: + jj_consume_token(Mod); + binaryTokenStack.push(token); + break; + default: + jj_la1[68] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + UnionExpr(); + SimpleNode jjtn001 = new SimpleNode(this, JJTMULTIPLICATIVEEXPR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, 2); + jjtc001 = false; + try + { + jjtn001.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + {if (true) throw e;} + } + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void UnionExpr() throws ParseException { + /*@bgen(jjtree) #UnionExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTUNIONEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + IntersectExceptExpr(); + label_19: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Union: + case Vbar: + ; + break; + default: + jj_la1[69] = jj_gen; + break label_19; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Union: + jj_consume_token(Union); + binaryTokenStack.push(token); + break; + case Vbar: + jj_consume_token(Vbar); + binaryTokenStack.push(token); + break; + default: + jj_la1[70] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + IntersectExceptExpr(); + SimpleNode jjtn001 = new SimpleNode(this, JJTUNIONEXPR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, 2); + jjtc001 = false; + try + { + jjtn001.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + {if (true) throw e;} + } + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void IntersectExceptExpr() throws ParseException { + /*@bgen(jjtree) #IntersectExceptExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTINTERSECTEXCEPTEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + InstanceofExpr(); + label_20: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Intersect: + case Except: + ; + break; + default: + jj_la1[71] = jj_gen; + break label_20; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Intersect: + jj_consume_token(Intersect); + binaryTokenStack.push(token); + break; + case Except: + jj_consume_token(Except); + binaryTokenStack.push(token); + break; + default: + jj_la1[72] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + InstanceofExpr(); + SimpleNode jjtn001 = new SimpleNode(this, JJTINTERSECTEXCEPTEXPR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, 2); + jjtc001 = false; + try + { + jjtn001.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + {if (true) throw e;} + } + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, 2); + } + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void InstanceofExpr() throws ParseException { + /*@bgen(jjtree) #InstanceofExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTINSTANCEOFEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + TreatExpr(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Instanceof: + jj_consume_token(Instanceof); + SequenceType(); + break; + default: + jj_la1[73] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void TreatExpr() throws ParseException { + /*@bgen(jjtree) #TreatExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTTREATEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + CastableExpr(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case TreatAs: + jj_consume_token(TreatAs); + SequenceType(); + break; + default: + jj_la1[74] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void CastableExpr() throws ParseException { + /*@bgen(jjtree) #CastableExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCASTABLEEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + CastExpr(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Castable: + jj_consume_token(Castable); + SingleType(); + break; + default: + jj_la1[75] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void CastExpr() throws ParseException { + /*@bgen(jjtree) #CastExpr(> 1) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCASTEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + UnaryExpr(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CastAs: + jj_consume_token(CastAs); + SimpleNode jjtn001 = new SimpleNode(this, JJTCASTAS); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + SingleType(); + break; + default: + jj_la1[76] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1); + } + } + } + + final public void UnaryExpr() throws ParseException { + /*@bgen(jjtree) #UnaryExpr( keepUnary) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTUNARYEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);boolean keepUnary=false; + try { + label_21: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case UnaryMinus: + case UnaryPlus: + ; + break; + default: + jj_la1[77] = jj_gen; + break label_21; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case UnaryMinus: + jj_consume_token(UnaryMinus); + SimpleNode jjtn001 = new SimpleNode(this, JJTUNARYMINUS); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + keepUnary=true;jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case UnaryPlus: + jj_consume_token(UnaryPlus); + SimpleNode jjtn002 = new SimpleNode(this, JJTUNARYPLUS); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + keepUnary=true;jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[78] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + ValueExpr(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, keepUnary); + } + } + } + + final public void ValueExpr() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ValidateLbrace: + case ValidateSchemaMode: + ValidateExpr(); + break; + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case Lpar: + case At: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + PathExpr(); + break; + default: + jj_la1[79] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void GeneralComp() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Equals: + jj_consume_token(Equals); + binaryTokenStack.push(token); + break; + case NotEquals: + jj_consume_token(NotEquals); + binaryTokenStack.push(token); + break; + case Lt: + jj_consume_token(Lt); + binaryTokenStack.push(token); + break; + case LtEquals: + jj_consume_token(LtEquals); + binaryTokenStack.push(token); + break; + case Gt: + jj_consume_token(Gt); + binaryTokenStack.push(token); + break; + case GtEquals: + jj_consume_token(GtEquals); + binaryTokenStack.push(token); + break; + default: + jj_la1[80] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void ValueComp() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case FortranEq: + jj_consume_token(FortranEq); + binaryTokenStack.push(token); + break; + case FortranNe: + jj_consume_token(FortranNe); + binaryTokenStack.push(token); + break; + case FortranLt: + jj_consume_token(FortranLt); + binaryTokenStack.push(token); + break; + case FortranLe: + jj_consume_token(FortranLe); + binaryTokenStack.push(token); + break; + case FortranGt: + jj_consume_token(FortranGt); + binaryTokenStack.push(token); + break; + case FortranGe: + jj_consume_token(FortranGe); + binaryTokenStack.push(token); + break; + default: + jj_la1[81] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void NodeComp() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Is: + jj_consume_token(Is); + binaryTokenStack.push(token); + break; + case LtLt: + jj_consume_token(LtLt); + binaryTokenStack.push(token); + break; + case GtGt: + jj_consume_token(GtGt); + binaryTokenStack.push(token); + break; + default: + jj_la1[82] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void ValidateExpr() throws ParseException { + /*@bgen(jjtree) ValidateExpr */ + SimpleNode jjtn000 = new SimpleNode(this, JJTVALIDATEEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ValidateLbrace: + jj_consume_token(ValidateLbrace); + SimpleNode jjtn001 = new SimpleNode(this, JJTVALIDATELBRACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case ValidateSchemaMode: + jj_consume_token(ValidateSchemaMode); + SimpleNode jjtn002 = new SimpleNode(this, JJTVALIDATESCHEMAMODE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + jj_consume_token(LbraceExprEnclosure); + SimpleNode jjtn003 = new SimpleNode(this, JJTLBRACEEXPRENCLOSURE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[83] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn004 = new SimpleNode(this, JJTRBRACE); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void PathExpr() throws ParseException { + /*@bgen(jjtree) #PathExpr(> 0) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTPATHEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Root: + jj_consume_token(Root); + SimpleNode jjtn001 = new SimpleNode(this, JJTROOT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Lpar: + case At: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + RelativePathExpr(); + break; + default: + jj_la1[84] = jj_gen; + ; + } + break; + case RootDescendants: + jj_consume_token(RootDescendants); + SimpleNode jjtn002 = new SimpleNode(this, JJTROOTDESCENDANTS); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + RelativePathExpr(); + break; + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Lpar: + case At: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + RelativePathExpr(); + break; + default: + jj_la1[85] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 0); + } + } + } + + final public void RelativePathExpr() throws ParseException { + StepExpr(); + label_22: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Slash: + case SlashSlash: + ; + break; + default: + jj_la1[86] = jj_gen; + break label_22; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Slash: + jj_consume_token(Slash); + break; + case SlashSlash: + jj_consume_token(SlashSlash); + SimpleNode jjtn001 = new SimpleNode(this, JJTSLASHSLASH); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[87] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + StepExpr(); + } + } + + final public void StepExpr() throws ParseException { + /*@bgen(jjtree) #StepExpr(> 1 || isStep) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSTEPEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000);boolean savedIsStep = isStep; isStep=false; + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case Star: + case NCNameColonStar: + case StarColonNCName: + case At: + case DocumentLpar: + case DocumentLparForKindTest: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case DotDot: + case QName: + isStep=true; + AxisStep(); + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1 || isStep); + jjtc000 = false; + isStep = savedIsStep; + break; + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case VariableIndicator: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Lpar: + case DocumentLbrace: + case Dot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + FilterExpr(); + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1 || isStep); + jjtc000 = false; + isStep = savedIsStep; + break; + default: + jj_la1[88] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 1 || isStep); + } + } + } + + final public void AxisStep() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AxisChild: + case AxisDescendant: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisFollowingSibling: + case AxisFollowing: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case Star: + case NCNameColonStar: + case StarColonNCName: + case At: + case DocumentLpar: + case DocumentLparForKindTest: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case QName: + ForwardStep(); + break; + case AxisParent: + case AxisAncestor: + case AxisPrecedingSibling: + case AxisPreceding: + case AxisAncestorOrSelf: + case DotDot: + ReverseStep(); + break; + default: + jj_la1[89] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + PredicateList(); + } + + final public void ForwardStep() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AxisChild: + case AxisDescendant: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisFollowingSibling: + case AxisFollowing: + ForwardAxis(); + NodeTest(); + break; + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case Star: + case NCNameColonStar: + case StarColonNCName: + case At: + case DocumentLpar: + case DocumentLparForKindTest: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case QName: + AbbrevForwardStep(); + break; + default: + jj_la1[90] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void ForwardAxis() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AxisChild: + jj_consume_token(AxisChild); + SimpleNode jjtn001 = new SimpleNode(this, JJTAXISCHILD); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case AxisDescendant: + jj_consume_token(AxisDescendant); + SimpleNode jjtn002 = new SimpleNode(this, JJTAXISDESCENDANT); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case AxisAttribute: + jj_consume_token(AxisAttribute); + SimpleNode jjtn003 = new SimpleNode(this, JJTAXISATTRIBUTE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + case AxisSelf: + jj_consume_token(AxisSelf); + SimpleNode jjtn004 = new SimpleNode(this, JJTAXISSELF); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + case AxisDescendantOrSelf: + jj_consume_token(AxisDescendantOrSelf); + SimpleNode jjtn005 = new SimpleNode(this, JJTAXISDESCENDANTORSELF); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + break; + case AxisFollowingSibling: + jj_consume_token(AxisFollowingSibling); + SimpleNode jjtn006 = new SimpleNode(this, JJTAXISFOLLOWINGSIBLING); + boolean jjtc006 = true; + jjtree.openNodeScope(jjtn006); + try { + jjtree.closeNodeScope(jjtn006, true); + jjtc006 = false; + jjtn006.processToken(token); + } finally { + if (jjtc006) { + jjtree.closeNodeScope(jjtn006, true); + } + } + break; + case AxisFollowing: + jj_consume_token(AxisFollowing); + SimpleNode jjtn007 = new SimpleNode(this, JJTAXISFOLLOWING); + boolean jjtc007 = true; + jjtree.openNodeScope(jjtn007); + try { + jjtree.closeNodeScope(jjtn007, true); + jjtc007 = false; + jjtn007.processToken(token); + } finally { + if (jjtc007) { + jjtree.closeNodeScope(jjtn007, true); + } + } + break; + default: + jj_la1[91] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void AbbrevForwardStep() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case At: + jj_consume_token(At); + SimpleNode jjtn001 = new SimpleNode(this, JJTAT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[92] = jj_gen; + ; + } + NodeTest(); + } + + final public void ReverseStep() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AxisParent: + case AxisAncestor: + case AxisPrecedingSibling: + case AxisPreceding: + case AxisAncestorOrSelf: + ReverseAxis(); + NodeTest(); + break; + case DotDot: + AbbrevReverseStep(); + break; + default: + jj_la1[93] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void ReverseAxis() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AxisParent: + jj_consume_token(AxisParent); + SimpleNode jjtn001 = new SimpleNode(this, JJTAXISPARENT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case AxisAncestor: + jj_consume_token(AxisAncestor); + SimpleNode jjtn002 = new SimpleNode(this, JJTAXISANCESTOR); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case AxisPrecedingSibling: + jj_consume_token(AxisPrecedingSibling); + SimpleNode jjtn003 = new SimpleNode(this, JJTAXISPRECEDINGSIBLING); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + case AxisPreceding: + jj_consume_token(AxisPreceding); + SimpleNode jjtn004 = new SimpleNode(this, JJTAXISPRECEDING); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + case AxisAncestorOrSelf: + jj_consume_token(AxisAncestorOrSelf); + SimpleNode jjtn005 = new SimpleNode(this, JJTAXISANCESTORORSELF); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + break; + default: + jj_la1[94] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void AbbrevReverseStep() throws ParseException { + jj_consume_token(DotDot); + SimpleNode jjtn001 = new SimpleNode(this, JJTDOTDOT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + } + + final public void NodeTest() throws ParseException { + /*@bgen(jjtree) NodeTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTNODETEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case DocumentLpar: + case DocumentLparForKindTest: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + KindTest(); + break; + case Star: + case NCNameColonStar: + case StarColonNCName: + case QName: + NameTest(); + break; + default: + jj_la1[95] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void NameTest() throws ParseException { + /*@bgen(jjtree) NameTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTNAMETEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case QName: + jj_consume_token(QName); + SimpleNode jjtn001 = new SimpleNode(this, JJTQNAME); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case Star: + case NCNameColonStar: + case StarColonNCName: + Wildcard(); + break; + default: + jj_la1[96] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Wildcard() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Star: + jj_consume_token(Star); + SimpleNode jjtn001 = new SimpleNode(this, JJTSTAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case NCNameColonStar: + jj_consume_token(NCNameColonStar); + SimpleNode jjtn002 = new SimpleNode(this, JJTNCNAMECOLONSTAR); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case StarColonNCName: + jj_consume_token(StarColonNCName); + SimpleNode jjtn003 = new SimpleNode(this, JJTSTARCOLONNCNAME); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[97] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void FilterExpr() throws ParseException { + PrimaryExpr(); + PredicateList(); + } + + final public void PredicateList() throws ParseException { + /*@bgen(jjtree) #PredicateList(> 0) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTPREDICATELIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + label_23: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Lbrack: + ; + break; + default: + jj_la1[98] = jj_gen; + break label_23; + } + Predicate(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 0); + } + } + } + + final public void Predicate() throws ParseException { + /*@bgen(jjtree) #Predicate(> 0) */ + SimpleNode jjtn000 = new SimpleNode(this, JJTPREDICATE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(Lbrack); + Expr(); + jj_consume_token(Rbrack); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, jjtree.nodeArity() > 0); + } + } + } + + final public void PrimaryExpr() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + Literal(); + break; + case VariableIndicator: + VarRef(); + break; + case Lpar: + ParenthesizedExpr(); + break; + case Dot: + isStep=true; + ContextItemExpr(); + break; + case QNameLpar: + FunctionCall(); + break; + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case DocumentLbrace: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + Constructor(); + break; + case OrderedOpen: + OrderedExpr(); + break; + case UnorderedOpen: + UnorderedExpr(); + break; + default: + jj_la1[99] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void Literal() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + NumericLiteral(); + break; + case StringLiteral: + jj_consume_token(StringLiteral); + SimpleNode jjtn001 = new SimpleNode(this, JJTSTRINGLITERAL); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[100] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void NumericLiteral() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + jj_consume_token(IntegerLiteral); + SimpleNode jjtn001 = new SimpleNode(this, JJTINTEGERLITERAL); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case DecimalLiteral: + jj_consume_token(DecimalLiteral); + SimpleNode jjtn002 = new SimpleNode(this, JJTDECIMALLITERAL); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case DoubleLiteral: + jj_consume_token(DoubleLiteral); + SimpleNode jjtn003 = new SimpleNode(this, JJTDOUBLELITERAL); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[101] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void VarRef() throws ParseException { + jj_consume_token(VariableIndicator); + jj_consume_token(VarName); + SimpleNode jjtn001 = new SimpleNode(this, JJTVARNAME); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + } + + final public void ParenthesizedExpr() throws ParseException { + jj_consume_token(Lpar); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case UnaryMinus: + case UnaryPlus: + case Lpar: + case At: + case Some: + case Every: + case ForVariable: + case LetVariable: + case ValidateLbrace: + case ValidateSchemaMode: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case IfLpar: + case TypeswitchLpar: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + Expr(); + break; + default: + jj_la1[102] = jj_gen; + ; + } + jj_consume_token(Rpar); + } + + final public void ContextItemExpr() throws ParseException { + jj_consume_token(Dot); + SimpleNode jjtn001 = new SimpleNode(this, JJTDOT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + } + + final public void OrderedExpr() throws ParseException { + jj_consume_token(OrderedOpen); + SimpleNode jjtn001 = new SimpleNode(this, JJTORDEREDOPEN); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn002 = new SimpleNode(this, JJTRBRACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } + + final public void UnorderedExpr() throws ParseException { + jj_consume_token(UnorderedOpen); + SimpleNode jjtn001 = new SimpleNode(this, JJTUNORDEREDOPEN); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn002 = new SimpleNode(this, JJTRBRACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } + + final public void FunctionCall() throws ParseException { + /*@bgen(jjtree) FunctionCall */ + SimpleNode jjtn000 = new SimpleNode(this, JJTFUNCTIONCALL); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(QNameLpar); + SimpleNode jjtn001 = new SimpleNode(this, JJTQNAMELPAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case UnaryMinus: + case UnaryPlus: + case Lpar: + case At: + case Some: + case Every: + case ForVariable: + case LetVariable: + case ValidateLbrace: + case ValidateSchemaMode: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case IfLpar: + case TypeswitchLpar: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + ExprSingle(); + label_24: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Comma: + ; + break; + default: + jj_la1[103] = jj_gen; + break label_24; + } + jj_consume_token(Comma); + ExprSingle(); + } + break; + default: + jj_la1[104] = jj_gen; + ; + } + jj_consume_token(Rpar); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void Constructor() throws ParseException { + /*@bgen(jjtree) Constructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + DirectConstructor(); + break; + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case DocumentLbrace: + ComputedConstructor(); + break; + default: + jj_la1[105] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirectConstructor() throws ParseException { + /*@bgen(jjtree) DirectConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRECTCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case StartTagOpen: + case StartTagOpenRoot: + DirElemConstructor(); + break; + case XmlCommentStart: + case XmlCommentStartForElementContent: + DirCommentConstructor(); + break; + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + DirPIConstructor(); + break; + default: + jj_la1[106] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirElemConstructor() throws ParseException { + /*@bgen(jjtree) DirElemConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRELEMCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case StartTagOpenRoot: + jj_consume_token(StartTagOpenRoot); + SimpleNode jjtn001 = new SimpleNode(this, JJTSTARTTAGOPENROOT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case StartTagOpen: + jj_consume_token(StartTagOpen); + SimpleNode jjtn002 = new SimpleNode(this, JJTSTARTTAGOPEN); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[107] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(TagQName); + SimpleNode jjtn003 = new SimpleNode(this, JJTTAGQNAME); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + DirAttributeList(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EmptyTagClose: + jj_consume_token(EmptyTagClose); + SimpleNode jjtn004 = new SimpleNode(this, JJTEMPTYTAGCLOSE); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + case StartTagClose: + jj_consume_token(StartTagClose); + SimpleNode jjtn005 = new SimpleNode(this, JJTSTARTTAGCLOSE); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + label_25: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case CdataSectionStart: + case CdataSectionStartForElementContent: + case PredefinedEntityRef: + case CharRef: + case StartTagOpen: + case StartTagOpenRoot: + case Lbrace: + case LbraceExprEnclosure: + case LCurlyBraceEscape: + case RCurlyBraceEscape: + case ElementContentChar: + case XmlCommentStart: + case XmlCommentStartForElementContent: + ; + break; + default: + jj_la1[108] = jj_gen; + break label_25; + } + DirElemContent(); + } + jj_consume_token(EndTagOpen); + SimpleNode jjtn006 = new SimpleNode(this, JJTENDTAGOPEN); + boolean jjtc006 = true; + jjtree.openNodeScope(jjtn006); + try { + jjtree.closeNodeScope(jjtn006, true); + jjtc006 = false; + jjtn006.processToken(token); + } finally { + if (jjtc006) { + jjtree.closeNodeScope(jjtn006, true); + } + } + jj_consume_token(TagQName); + SimpleNode jjtn007 = new SimpleNode(this, JJTTAGQNAME); + boolean jjtc007 = true; + jjtree.openNodeScope(jjtn007); + try { + jjtree.closeNodeScope(jjtn007, true); + jjtc007 = false; + jjtn007.processToken(token); + } finally { + if (jjtc007) { + jjtree.closeNodeScope(jjtn007, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case S: + jj_consume_token(S); + SimpleNode jjtn008 = new SimpleNode(this, JJTS); + boolean jjtc008 = true; + jjtree.openNodeScope(jjtn008); + try { + jjtree.closeNodeScope(jjtn008, true); + jjtc008 = false; + jjtn008.processToken(token); + } finally { + if (jjtc008) { + jjtree.closeNodeScope(jjtn008, true); + } + } + break; + default: + jj_la1[109] = jj_gen; + ; + } + jj_consume_token(EndTagClose); + SimpleNode jjtn009 = new SimpleNode(this, JJTENDTAGCLOSE); + boolean jjtc009 = true; + jjtree.openNodeScope(jjtn009); + try { + jjtree.closeNodeScope(jjtn009, true); + jjtc009 = false; + jjtn009.processToken(token); + } finally { + if (jjtc009) { + jjtree.closeNodeScope(jjtn009, true); + } + } + break; + default: + jj_la1[110] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirAttributeList() throws ParseException { + /*@bgen(jjtree) DirAttributeList */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRATTRIBUTELIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + label_26: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case S: + ; + break; + default: + jj_la1[111] = jj_gen; + break label_26; + } + jj_consume_token(S); + SimpleNode jjtn001 = new SimpleNode(this, JJTS); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case TagQName: + jj_consume_token(TagQName); + SimpleNode jjtn002 = new SimpleNode(this, JJTTAGQNAME); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case S: + jj_consume_token(S); + SimpleNode jjtn003 = new SimpleNode(this, JJTS); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[112] = jj_gen; + ; + } + jj_consume_token(ValueIndicator); + SimpleNode jjtn004 = new SimpleNode(this, JJTVALUEINDICATOR); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case S: + jj_consume_token(S); + SimpleNode jjtn005 = new SimpleNode(this, JJTS); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + break; + default: + jj_la1[113] = jj_gen; + ; + } + DirAttributeValue(); + break; + default: + jj_la1[114] = jj_gen; + ; + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirAttributeValue() throws ParseException { + /*@bgen(jjtree) DirAttributeValue */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRATTRIBUTEVALUE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OpenQuot: + jj_consume_token(OpenQuot); + SimpleNode jjtn001 = new SimpleNode(this, JJTOPENQUOT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + label_27: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PredefinedEntityRef: + case CharRef: + case Lbrace: + case LbraceExprEnclosure: + case LCurlyBraceEscape: + case RCurlyBraceEscape: + case EscapeQuot: + case QuotAttrContentChar: + ; + break; + default: + jj_la1[115] = jj_gen; + break label_27; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EscapeQuot: + jj_consume_token(EscapeQuot); + SimpleNode jjtn002 = new SimpleNode(this, JJTESCAPEQUOT); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case PredefinedEntityRef: + case CharRef: + case Lbrace: + case LbraceExprEnclosure: + case LCurlyBraceEscape: + case RCurlyBraceEscape: + case QuotAttrContentChar: + QuotAttrValueContent(); + break; + default: + jj_la1[116] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + jj_consume_token(CloseQuot); + SimpleNode jjtn003 = new SimpleNode(this, JJTCLOSEQUOT); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + case OpenApos: + jj_consume_token(OpenApos); + SimpleNode jjtn004 = new SimpleNode(this, JJTOPENAPOS); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + label_28: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PredefinedEntityRef: + case CharRef: + case Lbrace: + case LbraceExprEnclosure: + case LCurlyBraceEscape: + case RCurlyBraceEscape: + case EscapeApos: + case AposAttrContentChar: + ; + break; + default: + jj_la1[117] = jj_gen; + break label_28; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case EscapeApos: + jj_consume_token(EscapeApos); + SimpleNode jjtn005 = new SimpleNode(this, JJTESCAPEAPOS); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + break; + case PredefinedEntityRef: + case CharRef: + case Lbrace: + case LbraceExprEnclosure: + case LCurlyBraceEscape: + case RCurlyBraceEscape: + case AposAttrContentChar: + AposAttrValueContent(); + break; + default: + jj_la1[118] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + jj_consume_token(CloseApos); + SimpleNode jjtn006 = new SimpleNode(this, JJTCLOSEAPOS); + boolean jjtc006 = true; + jjtree.openNodeScope(jjtn006); + try { + jjtree.closeNodeScope(jjtn006, true); + jjtc006 = false; + jjtn006.processToken(token); + } finally { + if (jjtc006) { + jjtree.closeNodeScope(jjtn006, true); + } + } + break; + default: + jj_la1[119] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void QuotAttrValueContent() throws ParseException { + /*@bgen(jjtree) QuotAttrValueContent */ + SimpleNode jjtn000 = new SimpleNode(this, JJTQUOTATTRVALUECONTENT); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case QuotAttrContentChar: + jj_consume_token(QuotAttrContentChar); + SimpleNode jjtn001 = new SimpleNode(this, JJTQUOTATTRCONTENTCHAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case PredefinedEntityRef: + case CharRef: + case Lbrace: + case LbraceExprEnclosure: + case LCurlyBraceEscape: + case RCurlyBraceEscape: + CommonContent(); + break; + default: + jj_la1[120] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void AposAttrValueContent() throws ParseException { + /*@bgen(jjtree) AposAttrValueContent */ + SimpleNode jjtn000 = new SimpleNode(this, JJTAPOSATTRVALUECONTENT); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AposAttrContentChar: + jj_consume_token(AposAttrContentChar); + SimpleNode jjtn001 = new SimpleNode(this, JJTAPOSATTRCONTENTCHAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case PredefinedEntityRef: + case CharRef: + case Lbrace: + case LbraceExprEnclosure: + case LCurlyBraceEscape: + case RCurlyBraceEscape: + CommonContent(); + break; + default: + jj_la1[121] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirElemContent() throws ParseException { + /*@bgen(jjtree) DirElemContent */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRELEMCONTENT); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + DirectConstructor(); + break; + case ElementContentChar: + jj_consume_token(ElementContentChar); + SimpleNode jjtn001 = new SimpleNode(this, JJTELEMENTCONTENTCHAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case CdataSectionStart: + case CdataSectionStartForElementContent: + CDataSection(); + break; + case PredefinedEntityRef: + case CharRef: + case Lbrace: + case LbraceExprEnclosure: + case LCurlyBraceEscape: + case RCurlyBraceEscape: + CommonContent(); + break; + default: + jj_la1[122] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CommonContent() throws ParseException { + /*@bgen(jjtree) CommonContent */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMMONCONTENT); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PredefinedEntityRef: + jj_consume_token(PredefinedEntityRef); + SimpleNode jjtn001 = new SimpleNode(this, JJTPREDEFINEDENTITYREF); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case CharRef: + jj_consume_token(CharRef); + SimpleNode jjtn002 = new SimpleNode(this, JJTCHARREF); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case LCurlyBraceEscape: + jj_consume_token(LCurlyBraceEscape); + SimpleNode jjtn003 = new SimpleNode(this, JJTLCURLYBRACEESCAPE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + case RCurlyBraceEscape: + jj_consume_token(RCurlyBraceEscape); + SimpleNode jjtn004 = new SimpleNode(this, JJTRCURLYBRACEESCAPE); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + case Lbrace: + case LbraceExprEnclosure: + EnclosedExpr(); + break; + default: + jj_la1[123] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirCommentConstructor() throws ParseException { + /*@bgen(jjtree) DirCommentConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRCOMMENTCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case XmlCommentStartForElementContent: + jj_consume_token(XmlCommentStartForElementContent); + SimpleNode jjtn001 = new SimpleNode(this, JJTXMLCOMMENTSTARTFORELEMENTCONTENT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case XmlCommentStart: + jj_consume_token(XmlCommentStart); + SimpleNode jjtn002 = new SimpleNode(this, JJTXMLCOMMENTSTART); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[124] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + DirCommentContents(); + jj_consume_token(XmlCommentEnd); + SimpleNode jjtn003 = new SimpleNode(this, JJTXMLCOMMENTEND); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirCommentContents() throws ParseException { + /*@bgen(jjtree) DirCommentContents */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRCOMMENTCONTENTS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + label_29: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CommentContentChar: + case CommentContentCharDash: + ; + break; + default: + jj_la1[125] = jj_gen; + break label_29; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CommentContentChar: + jj_consume_token(CommentContentChar); + SimpleNode jjtn001 = new SimpleNode(this, JJTCOMMENTCONTENTCHAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case CommentContentCharDash: + jj_consume_token(CommentContentCharDash); + SimpleNode jjtn002 = new SimpleNode(this, JJTCOMMENTCONTENTCHARDASH); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[126] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirPIConstructor() throws ParseException { + /*@bgen(jjtree) DirPIConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRPICONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ProcessingInstructionStartForElementContent: + jj_consume_token(ProcessingInstructionStartForElementContent); + SimpleNode jjtn001 = new SimpleNode(this, JJTPROCESSINGINSTRUCTIONSTARTFORELEMENTCONTENT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case ProcessingInstructionStart: + jj_consume_token(ProcessingInstructionStart); + SimpleNode jjtn002 = new SimpleNode(this, JJTPROCESSINGINSTRUCTIONSTART); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[127] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(PITarget); + SimpleNode jjtn003 = new SimpleNode(this, JJTPITARGET); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case SForPI: + jj_consume_token(SForPI); + SimpleNode jjtn004 = new SimpleNode(this, JJTSFORPI); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + DirPIContents(); + break; + default: + jj_la1[128] = jj_gen; + ; + } + jj_consume_token(ProcessingInstructionEnd); + SimpleNode jjtn005 = new SimpleNode(this, JJTPROCESSINGINSTRUCTIONEND); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DirPIContents() throws ParseException { + /*@bgen(jjtree) DirPIContents */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDIRPICONTENTS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + label_30: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PIContentChar: + ; + break; + default: + jj_la1[129] = jj_gen; + break label_30; + } + jj_consume_token(PIContentChar); + SimpleNode jjtn001 = new SimpleNode(this, JJTPICONTENTCHAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CDataSection() throws ParseException { + /*@bgen(jjtree) CDataSection */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCDATASECTION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CdataSectionStartForElementContent: + jj_consume_token(CdataSectionStartForElementContent); + SimpleNode jjtn001 = new SimpleNode(this, JJTCDATASECTIONSTARTFORELEMENTCONTENT); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case CdataSectionStart: + jj_consume_token(CdataSectionStart); + SimpleNode jjtn002 = new SimpleNode(this, JJTCDATASECTIONSTART); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[130] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + CDataSectionContents(); + jj_consume_token(CdataSectionEnd); + SimpleNode jjtn003 = new SimpleNode(this, JJTCDATASECTIONEND); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CDataSectionContents() throws ParseException { + /*@bgen(jjtree) CDataSectionContents */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCDATASECTIONCONTENTS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + label_31: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CDataSectionChar: + ; + break; + default: + jj_la1[131] = jj_gen; + break label_31; + } + jj_consume_token(CDataSectionChar); + SimpleNode jjtn001 = new SimpleNode(this, JJTCDATASECTIONCHAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ComputedConstructor() throws ParseException { + /*@bgen(jjtree) ComputedConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMPUTEDCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DocumentLbrace: + CompDocConstructor(); + break; + case ElementQNameLbrace: + case ElementLbrace: + CompElemConstructor(); + break; + case AttributeQNameLbrace: + case AttributeLbrace: + CompAttrConstructor(); + break; + case TextLbrace: + CompTextConstructor(); + break; + case CommentLbrace: + CompCommentConstructor(); + break; + case PINCNameLbrace: + case PILbrace: + CompPIConstructor(); + break; + default: + jj_la1[132] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CompDocConstructor() throws ParseException { + /*@bgen(jjtree) CompDocConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMPDOCCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(DocumentLbrace); + SimpleNode jjtn001 = new SimpleNode(this, JJTDOCUMENTLBRACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn002 = new SimpleNode(this, JJTRBRACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CompElemConstructor() throws ParseException { + /*@bgen(jjtree) CompElemConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMPELEMCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ElementQNameLbrace: + jj_consume_token(ElementQNameLbrace); + SimpleNode jjtn001 = new SimpleNode(this, JJTELEMENTQNAMELBRACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case ElementLbrace: + jj_consume_token(ElementLbrace); + SimpleNode jjtn002 = new SimpleNode(this, JJTELEMENTLBRACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn003 = new SimpleNode(this, JJTRBRACE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + jj_consume_token(LbraceExprEnclosure); + SimpleNode jjtn004 = new SimpleNode(this, JJTLBRACEEXPRENCLOSURE); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + default: + jj_la1[133] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case UnaryMinus: + case UnaryPlus: + case Lpar: + case At: + case Some: + case Every: + case ForVariable: + case LetVariable: + case ValidateLbrace: + case ValidateSchemaMode: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case IfLpar: + case TypeswitchLpar: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + ContentExpr(); + break; + default: + jj_la1[134] = jj_gen; + ; + } + jj_consume_token(Rbrace); + SimpleNode jjtn005 = new SimpleNode(this, JJTRBRACE); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ContentExpr() throws ParseException { + /*@bgen(jjtree) ContentExpr */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCONTENTEXPR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + Expr(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CompAttrConstructor() throws ParseException { + /*@bgen(jjtree) CompAttrConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMPATTRCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AttributeQNameLbrace: + jj_consume_token(AttributeQNameLbrace); + SimpleNode jjtn001 = new SimpleNode(this, JJTATTRIBUTEQNAMELBRACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case AttributeLbrace: + jj_consume_token(AttributeLbrace); + SimpleNode jjtn002 = new SimpleNode(this, JJTATTRIBUTELBRACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn003 = new SimpleNode(this, JJTRBRACE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + jj_consume_token(LbraceExprEnclosure); + SimpleNode jjtn004 = new SimpleNode(this, JJTLBRACEEXPRENCLOSURE); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + default: + jj_la1[135] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case UnaryMinus: + case UnaryPlus: + case Lpar: + case At: + case Some: + case Every: + case ForVariable: + case LetVariable: + case ValidateLbrace: + case ValidateSchemaMode: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case IfLpar: + case TypeswitchLpar: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + Expr(); + break; + default: + jj_la1[136] = jj_gen; + ; + } + jj_consume_token(Rbrace); + SimpleNode jjtn005 = new SimpleNode(this, JJTRBRACE); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CompTextConstructor() throws ParseException { + /*@bgen(jjtree) CompTextConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMPTEXTCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(TextLbrace); + SimpleNode jjtn001 = new SimpleNode(this, JJTTEXTLBRACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn002 = new SimpleNode(this, JJTRBRACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CompCommentConstructor() throws ParseException { + /*@bgen(jjtree) CompCommentConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMPCOMMENTCONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(CommentLbrace); + SimpleNode jjtn001 = new SimpleNode(this, JJTCOMMENTLBRACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn002 = new SimpleNode(this, JJTRBRACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CompPIConstructor() throws ParseException { + /*@bgen(jjtree) CompPIConstructor */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMPPICONSTRUCTOR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case PINCNameLbrace: + jj_consume_token(PINCNameLbrace); + SimpleNode jjtn001 = new SimpleNode(this, JJTPINCNAMELBRACE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case PILbrace: + jj_consume_token(PILbrace); + SimpleNode jjtn002 = new SimpleNode(this, JJTPILBRACE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + Expr(); + jj_consume_token(Rbrace); + SimpleNode jjtn003 = new SimpleNode(this, JJTRBRACE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + jj_consume_token(LbraceExprEnclosure); + SimpleNode jjtn004 = new SimpleNode(this, JJTLBRACEEXPRENCLOSURE); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + break; + default: + jj_la1[137] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case IntegerLiteral: + case DecimalLiteral: + case DoubleLiteral: + case StringLiteral: + case ProcessingInstructionStart: + case ProcessingInstructionStartForElementContent: + case AxisChild: + case AxisDescendant: + case AxisParent: + case AxisAttribute: + case AxisSelf: + case AxisDescendantOrSelf: + case AxisAncestor: + case AxisFollowingSibling: + case AxisPrecedingSibling: + case AxisFollowing: + case AxisPreceding: + case AxisAncestorOrSelf: + case VariableIndicator: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case OrderedOpen: + case UnorderedOpen: + case ElementQNameLbrace: + case AttributeQNameLbrace: + case PINCNameLbrace: + case PILbrace: + case CommentLbrace: + case ElementLbrace: + case AttributeLbrace: + case TextLbrace: + case Star: + case NCNameColonStar: + case StarColonNCName: + case Root: + case RootDescendants: + case UnaryMinus: + case UnaryPlus: + case Lpar: + case At: + case Some: + case Every: + case ForVariable: + case LetVariable: + case ValidateLbrace: + case ValidateSchemaMode: + case DocumentLpar: + case DocumentLparForKindTest: + case DocumentLbrace: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case IfLpar: + case TypeswitchLpar: + case Dot: + case DotDot: + case QNameLpar: + case StartTagOpen: + case StartTagOpenRoot: + case XmlCommentStart: + case XmlCommentStartForElementContent: + case QName: + Expr(); + break; + default: + jj_la1[138] = jj_gen; + ; + } + jj_consume_token(Rbrace); + SimpleNode jjtn005 = new SimpleNode(this, JJTRBRACE); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void SingleType() throws ParseException { + /*@bgen(jjtree) SingleType */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSINGLETYPE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + AtomicType(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OccurrenceZeroOrOne: + jj_consume_token(OccurrenceZeroOrOne); + SimpleNode jjtn001 = new SimpleNode(this, JJTOCCURRENCEZEROORONE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[139] = jj_gen; + ; + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void TypeDeclaration() throws ParseException { + /*@bgen(jjtree) TypeDeclaration */ + SimpleNode jjtn000 = new SimpleNode(this, JJTTYPEDECLARATION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(As); + SimpleNode jjtn001 = new SimpleNode(this, JJTAS); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + SequenceType(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void SequenceType() throws ParseException { + /*@bgen(jjtree) SequenceType */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSEQUENCETYPE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Item: + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case DocumentLpar: + case DocumentLparForKindTest: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + case QNameForSequenceType: + case QNameForAtomicType: + ItemType(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OccurrenceZeroOrOne: + case OccurrenceZeroOrMore: + case OccurrenceOneOrMore: + OccurrenceIndicator(); + break; + default: + jj_la1[140] = jj_gen; + ; + } + break; + case EmptyTok: + jj_consume_token(EmptyTok); + SimpleNode jjtn001 = new SimpleNode(this, JJTEMPTYTOK); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[141] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void OccurrenceIndicator() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OccurrenceZeroOrOne: + jj_consume_token(OccurrenceZeroOrOne); + SimpleNode jjtn001 = new SimpleNode(this, JJTOCCURRENCEZEROORONE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case OccurrenceZeroOrMore: + jj_consume_token(OccurrenceZeroOrMore); + SimpleNode jjtn002 = new SimpleNode(this, JJTOCCURRENCEZEROORMORE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case OccurrenceOneOrMore: + jj_consume_token(OccurrenceOneOrMore); + SimpleNode jjtn003 = new SimpleNode(this, JJTOCCURRENCEONEORMORE); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[142] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void ItemType() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case QNameForSequenceType: + case QNameForAtomicType: + AtomicType(); + break; + case ElementType: + case AttributeType: + case SchemaElementType: + case SchemaAttributeType: + case DocumentLpar: + case DocumentLparForKindTest: + case NodeLpar: + case CommentLpar: + case TextLpar: + case ProcessingInstructionLpar: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case AttributeTypeForKindTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + case SchemaAttributeTypeForKindTest: + case ProcessingInstructionLparForKindTest: + case TextLparForKindTest: + case CommentLparForKindTest: + case NodeLparForKindTest: + KindTest(); + break; + case Item: + jj_consume_token(Item); + SimpleNode jjtn001 = new SimpleNode(this, JJTITEM); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[143] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void AtomicType() throws ParseException { + /*@bgen(jjtree) AtomicType */ + SimpleNode jjtn000 = new SimpleNode(this, JJTATOMICTYPE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case QNameForAtomicType: + jj_consume_token(QNameForAtomicType); + SimpleNode jjtn001 = new SimpleNode(this, JJTQNAMEFORATOMICTYPE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case QNameForSequenceType: + jj_consume_token(QNameForSequenceType); + SimpleNode jjtn002 = new SimpleNode(this, JJTQNAMEFORSEQUENCETYPE); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[144] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void KindTest() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DocumentLpar: + case DocumentLparForKindTest: + DocumentTest(); + break; + case ElementType: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + ElementTest(); + break; + case AttributeType: + case AttributeTypeForKindTest: + AttributeTest(); + break; + case SchemaElementType: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + SchemaElementTest(); + break; + case SchemaAttributeType: + case SchemaAttributeTypeForKindTest: + SchemaAttributeTest(); + break; + case ProcessingInstructionLpar: + case ProcessingInstructionLparForKindTest: + PITest(); + break; + case CommentLpar: + case CommentLparForKindTest: + CommentTest(); + break; + case TextLpar: + case TextLparForKindTest: + TextTest(); + break; + case NodeLpar: + case NodeLparForKindTest: + AnyKindTest(); + break; + default: + jj_la1[145] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + + final public void AnyKindTest() throws ParseException { + /*@bgen(jjtree) AnyKindTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTANYKINDTEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NodeLpar: + jj_consume_token(NodeLpar); + break; + case NodeLparForKindTest: + jj_consume_token(NodeLparForKindTest); + SimpleNode jjtn001 = new SimpleNode(this, JJTNODELPARFORKINDTEST); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[146] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(RparForKindTest); + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void DocumentTest() throws ParseException { + /*@bgen(jjtree) DocumentTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTDOCUMENTTEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DocumentLpar: + jj_consume_token(DocumentLpar); + SimpleNode jjtn001 = new SimpleNode(this, JJTDOCUMENTLPAR); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case DocumentLparForKindTest: + jj_consume_token(DocumentLparForKindTest); + SimpleNode jjtn002 = new SimpleNode(this, JJTDOCUMENTLPARFORKINDTEST); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[147] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ElementType: + case SchemaElementType: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ElementType: + case ElementTypeForKindTest: + case ElementTypeForDocumentTest: + ElementTest(); + break; + case SchemaElementType: + case SchemaElementTypeForKindTest: + case SchemaElementTypeForDocumentTest: + SchemaElementTest(); + break; + default: + jj_la1[148] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[149] = jj_gen; + ; + } + jj_consume_token(RparForKindTest); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void TextTest() throws ParseException { + /*@bgen(jjtree) TextTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTTEXTTEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case TextLpar: + jj_consume_token(TextLpar); + break; + case TextLparForKindTest: + jj_consume_token(TextLparForKindTest); + SimpleNode jjtn001 = new SimpleNode(this, JJTTEXTLPARFORKINDTEST); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[150] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(RparForKindTest); + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void CommentTest() throws ParseException { + /*@bgen(jjtree) CommentTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTCOMMENTTEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CommentLpar: + jj_consume_token(CommentLpar); + break; + case CommentLparForKindTest: + jj_consume_token(CommentLparForKindTest); + SimpleNode jjtn001 = new SimpleNode(this, JJTCOMMENTLPARFORKINDTEST); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[151] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + jj_consume_token(RparForKindTest); + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void PITest() throws ParseException { + /*@bgen(jjtree) PITest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTPITEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ProcessingInstructionLpar: + jj_consume_token(ProcessingInstructionLpar); + break; + case ProcessingInstructionLparForKindTest: + jj_consume_token(ProcessingInstructionLparForKindTest); + SimpleNode jjtn001 = new SimpleNode(this, JJTPROCESSINGINSTRUCTIONLPARFORKINDTEST); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[152] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case StringLiteralForKindTest: + case NCNameForPI: + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case NCNameForPI: + jj_consume_token(NCNameForPI); + SimpleNode jjtn002 = new SimpleNode(this, JJTNCNAMEFORPI); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case StringLiteralForKindTest: + jj_consume_token(StringLiteralForKindTest); + SimpleNode jjtn003 = new SimpleNode(this, JJTSTRINGLITERALFORKINDTEST); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[153] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[154] = jj_gen; + ; + } + jj_consume_token(RparForKindTest); + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void AttributeTest() throws ParseException { + /*@bgen(jjtree) AttributeTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTATTRIBUTETEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AttributeType: + jj_consume_token(AttributeType); + SimpleNode jjtn001 = new SimpleNode(this, JJTATTRIBUTETYPE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case AttributeTypeForKindTest: + jj_consume_token(AttributeTypeForKindTest); + SimpleNode jjtn002 = new SimpleNode(this, JJTATTRIBUTETYPEFORKINDTEST); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[155] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AnyName: + case QNameForItemType: + AttribNameOrWildcard(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CommaForKindTest: + jj_consume_token(CommaForKindTest); + SimpleNode jjtn003 = new SimpleNode(this, JJTCOMMAFORKINDTEST); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + TypeName(); + break; + default: + jj_la1[156] = jj_gen; + ; + } + break; + default: + jj_la1[157] = jj_gen; + ; + } + jj_consume_token(RparForKindTest); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void AttribNameOrWildcard() throws ParseException { + /*@bgen(jjtree) AttribNameOrWildcard */ + SimpleNode jjtn000 = new SimpleNode(this, JJTATTRIBNAMEORWILDCARD); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case QNameForItemType: + AttributeName(); + break; + case AnyName: + jj_consume_token(AnyName); + SimpleNode jjtn001 = new SimpleNode(this, JJTANYNAME); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[158] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void SchemaAttributeTest() throws ParseException { + /*@bgen(jjtree) SchemaAttributeTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSCHEMAATTRIBUTETEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case SchemaAttributeType: + jj_consume_token(SchemaAttributeType); + SimpleNode jjtn001 = new SimpleNode(this, JJTSCHEMAATTRIBUTETYPE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case SchemaAttributeTypeForKindTest: + jj_consume_token(SchemaAttributeTypeForKindTest); + SimpleNode jjtn002 = new SimpleNode(this, JJTSCHEMAATTRIBUTETYPEFORKINDTEST); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + default: + jj_la1[159] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + AttributeDeclaration(); + jj_consume_token(RparForKindTest); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void AttributeDeclaration() throws ParseException { + /*@bgen(jjtree) AttributeDeclaration */ + SimpleNode jjtn000 = new SimpleNode(this, JJTATTRIBUTEDECLARATION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + AttributeName(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ElementTest() throws ParseException { + /*@bgen(jjtree) ElementTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTELEMENTTEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ElementType: + jj_consume_token(ElementType); + SimpleNode jjtn001 = new SimpleNode(this, JJTELEMENTTYPE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case ElementTypeForKindTest: + jj_consume_token(ElementTypeForKindTest); + SimpleNode jjtn002 = new SimpleNode(this, JJTELEMENTTYPEFORKINDTEST); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case ElementTypeForDocumentTest: + jj_consume_token(ElementTypeForDocumentTest); + SimpleNode jjtn003 = new SimpleNode(this, JJTELEMENTTYPEFORDOCUMENTTEST); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[160] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case AnyName: + case QNameForItemType: + ElementNameOrWildcard(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case CommaForKindTest: + jj_consume_token(CommaForKindTest); + SimpleNode jjtn004 = new SimpleNode(this, JJTCOMMAFORKINDTEST); + boolean jjtc004 = true; + jjtree.openNodeScope(jjtn004); + try { + jjtree.closeNodeScope(jjtn004, true); + jjtc004 = false; + jjtn004.processToken(token); + } finally { + if (jjtc004) { + jjtree.closeNodeScope(jjtn004, true); + } + } + TypeName(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case Nillable: + jj_consume_token(Nillable); + SimpleNode jjtn005 = new SimpleNode(this, JJTNILLABLE); + boolean jjtc005 = true; + jjtree.openNodeScope(jjtn005); + try { + jjtree.closeNodeScope(jjtn005, true); + jjtc005 = false; + jjtn005.processToken(token); + } finally { + if (jjtc005) { + jjtree.closeNodeScope(jjtn005, true); + } + } + break; + default: + jj_la1[161] = jj_gen; + ; + } + break; + default: + jj_la1[162] = jj_gen; + ; + } + break; + default: + jj_la1[163] = jj_gen; + ; + } + jj_consume_token(RparForKindTest); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ElementNameOrWildcard() throws ParseException { + /*@bgen(jjtree) ElementNameOrWildcard */ + SimpleNode jjtn000 = new SimpleNode(this, JJTELEMENTNAMEORWILDCARD); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case QNameForItemType: + ElementName(); + break; + case AnyName: + jj_consume_token(AnyName); + SimpleNode jjtn001 = new SimpleNode(this, JJTANYNAME); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + default: + jj_la1[164] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void SchemaElementTest() throws ParseException { + /*@bgen(jjtree) SchemaElementTest */ + SimpleNode jjtn000 = new SimpleNode(this, JJTSCHEMAELEMENTTEST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case SchemaElementType: + jj_consume_token(SchemaElementType); + SimpleNode jjtn001 = new SimpleNode(this, JJTSCHEMAELEMENTTYPE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + break; + case SchemaElementTypeForKindTest: + jj_consume_token(SchemaElementTypeForKindTest); + SimpleNode jjtn002 = new SimpleNode(this, JJTSCHEMAELEMENTTYPEFORKINDTEST); + boolean jjtc002 = true; + jjtree.openNodeScope(jjtn002); + try { + jjtree.closeNodeScope(jjtn002, true); + jjtc002 = false; + jjtn002.processToken(token); + } finally { + if (jjtc002) { + jjtree.closeNodeScope(jjtn002, true); + } + } + break; + case SchemaElementTypeForDocumentTest: + jj_consume_token(SchemaElementTypeForDocumentTest); + SimpleNode jjtn003 = new SimpleNode(this, JJTSCHEMAELEMENTTYPEFORDOCUMENTTEST); + boolean jjtc003 = true; + jjtree.openNodeScope(jjtn003); + try { + jjtree.closeNodeScope(jjtn003, true); + jjtc003 = false; + jjtn003.processToken(token); + } finally { + if (jjtc003) { + jjtree.closeNodeScope(jjtn003, true); + } + } + break; + default: + jj_la1[165] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + ElementDeclaration(); + jj_consume_token(RparForKindTest); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ElementDeclaration() throws ParseException { + /*@bgen(jjtree) ElementDeclaration */ + SimpleNode jjtn000 = new SimpleNode(this, JJTELEMENTDECLARATION); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + ElementName(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void AttributeName() throws ParseException { + /*@bgen(jjtree) AttributeName */ + SimpleNode jjtn000 = new SimpleNode(this, JJTATTRIBUTENAME); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(QNameForItemType); + SimpleNode jjtn001 = new SimpleNode(this, JJTQNAMEFORITEMTYPE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void ElementName() throws ParseException { + /*@bgen(jjtree) ElementName */ + SimpleNode jjtn000 = new SimpleNode(this, JJTELEMENTNAME); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(QNameForItemType); + SimpleNode jjtn001 = new SimpleNode(this, JJTQNAMEFORITEMTYPE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + final public void TypeName() throws ParseException { + /*@bgen(jjtree) TypeName */ + SimpleNode jjtn000 = new SimpleNode(this, JJTTYPENAME); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + try { + jj_consume_token(QNameForItemType); + SimpleNode jjtn001 = new SimpleNode(this, JJTQNAMEFORITEMTYPE); + boolean jjtc001 = true; + jjtree.openNodeScope(jjtn001); + try { + jjtree.closeNodeScope(jjtn001, true); + jjtc001 = false; + jjtn001.processToken(token); + } finally { + if (jjtc001) { + jjtree.closeNodeScope(jjtn001, true); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + } + } + } + + public XPathTokenManager token_source; + SimpleCharStream jj_input_stream; + public Token token, jj_nt; + private int jj_ntk; + private int jj_gen; + final private int[] jj_la1 = new int[166]; + static private int[] jj_la1_0; + static private int[] jj_la1_1; + static private int[] jj_la1_2; + static private int[] jj_la1_3; + static private int[] jj_la1_4; + static private int[] jj_la1_5; + static private int[] jj_la1_6; + static private int[] jj_la1_7; + static { + jj_la1_0(); + jj_la1_1(); + jj_la1_2(); + jj_la1_3(); + jj_la1_4(); + jj_la1_5(); + jj_la1_6(); + jj_la1_7(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x0,0xfffd885e,0x40,0xfffd881e,0x100,0x80000000,0x0,0x0,0x40000000,0x40000000,0x80000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x200,0x0,0x0,0x0,0x200,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3ffd801e,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3ffd801e,0x0,0x0,0x0,0x0,0x3ffd801e,0x3ffd801e,0x0,0x0,0x3ffd801e,0x3ffc0000,0xaec0000,0xaec0000,0x0,0x35100000,0x35100000,0x0,0x0,0x0,0x0,0x1801e,0x1e,0xe,0x3ffd801e,0x0,0x3ffd801e,0x18000,0x18000,0x0,0x18000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x18000,0x0,0x0,0x0,0x0,0x18000,0x0,0x0,0x0,0x0,0x0,0x0,0x3ffd801e,0x0,0x3ffd801e,0x0,0x3ffd801e,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20,0x20,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,}; + } + private static void jj_la1_1() { + jj_la1_1 = new int[] {0x0,0x3042000c,0x0,0x3042000c,0x0,0x3040000c,0x0,0x0,0x0,0x0,0x3040000c,0x0,0xc0000000,0x0,0x3,0x0,0x30,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x40,0x20000,0x0,0x40,0x0,0x0,0x0,0x0,0x20000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20000,0x20000,0x80,0x100,0x0,0x0,0x0,0x0,0x0,0x1e00,0x1e00,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20000,0x0,0x0,0x0,0x0,0x20000,0x20000,0x0,0x0,0x20000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20000,0x0,0x0,0x20000,0x0,0x20000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20000,0x0,0x20000,0x0,0x20000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x200000,0x0,0x0,0x0,0x0,}; + } + private static void jj_la1_2() { + jj_la1_2 = new int[] {0x0,0xdfffc002,0x0,0xdfffc002,0x0,0x10000000,0xc0000002,0xc0000002,0x0,0x0,0x10000000,0x0,0x0,0xc0000000,0x0,0x0,0x0,0x20000001,0x0,0x0,0x20000001,0x1,0x0,0x0,0x100,0x0,0x0,0x100,0x0,0x0,0x100,0x0,0x0,0xfffc000,0x0,0x0,0x8,0x0,0x100,0x200,0x0,0x100,0x200,0x100,0x0,0x100,0x0,0x0,0x0,0x0,0x0,0x0,0x10,0x0,0x100,0x0,0x100,0x400,0x0,0x0,0x0,0x0,0x0,0x0,0x4,0x0,0x0,0x0,0x0,0x40,0x40,0xa0,0xa0,0x800,0x0,0x1000,0x0,0x0,0x0,0xfffc000,0x0,0x0,0x0,0x0,0xfffc000,0xfffc000,0x0,0x0,0xfffc000,0x3c000,0x3c000,0x0,0x0,0x0,0x0,0x3c000,0x0,0x0,0x0,0xffc0000,0x0,0x0,0xfffc000,0x0,0xfffc000,0xff00000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff00000,0x2100000,0xfffc000,0x4200000,0xfffc000,0xc00000,0xfffc000,0x0,0x0,0x3e000,0x0,0x3e000,0x0,0x3c000,0x0,0x0,0x14000,0x14000,0x0,0x0,0x0,0x0,0x0,0x8000,0x0,0x0,0x0,0x20000,0x4000,0x0,0x0,0x0,0x0,0x10000,}; + } + private static void jj_la1_3() { + jj_la1_3 = new int[] {0x0,0x7a6,0x0,0x7a6,0x0,0x0,0x6,0x6,0x0,0x0,0x0,0x6,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x8000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x7a0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x37ffa000,0x37ffa000,0x0,0xc0000000,0xc0000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x7a0,0x300b2000,0x7e00000,0x148000,0x0,0x1a0,0x7a0,0x1800,0x1800,0x1a0,0x1a0,0x1a0,0x0,0x0,0x0,0x0,0x1a0,0x1a0,0x1a0,0x0,0x0,0x0,0x0,0x7a0,0x0,0x7a0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x7a0,0x0,0x7a0,0x0,0x7a0,0x0,0x0,0x1,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x40,0x40,0x0,0x0,0x0,0x0,0x40,0x40,0x0,}; + } + private static void jj_la1_4() { + jj_la1_4 = new int[] {0x0,0xffecf0c3,0x0,0xffecf0c3,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xffecf0c3,0xc000,0xc000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x20,0x20,0x0,0x0,0x0,0x20000,0x0,0x10000,0x3,0x3,0xffec00c0,0x0,0x0,0x0,0xc0000,0xffe000c0,0xffe000c0,0x0,0x0,0xffe000c0,0xff600080,0xff600080,0x0,0x80,0x0,0x0,0xff600000,0x0,0x0,0x100,0x800040,0x0,0x0,0xffecf0c3,0x0,0xffecf0c3,0x800000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x800000,0x0,0xffecf0c3,0x0,0xffecf0c3,0x0,0xffecf0c3,0x4,0x1c,0xff600000,0x1c,0xff600000,0x0,0xff600000,0x1000000,0x600000,0xb0000000,0xb0000000,0x4000000,0x2000000,0x8000000,0x0,0x0,0x40000000,0x0,0x0,0x0,0x0,0x30000000,0x0,0x0,0x0,0x0,0x80000000,}; + } + private static void jj_la1_5() { + jj_la1_5 = new int[] {0x800,0x840c0ff,0x0,0x840c0ff,0x0,0x0,0x0,0x0,0x400000,0x400000,0x0,0x0,0x0,0x0,0x0,0x300000,0x0,0x0,0x100,0x0,0x0,0x0,0x100,0x0,0x0,0x0,0x0,0x0,0x0,0x100,0x0,0x0,0x100,0x800c0ff,0x0,0x0,0x0,0x30000,0x0,0x0,0x100,0x0,0x0,0x0,0x100,0x0,0x30000,0x100,0xc0000,0xc0000,0x300000,0x300000,0x0,0x0,0x0,0x100,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x800c03f,0x0,0x0,0x0,0x0,0x800c03f,0x800c03f,0x0,0x0,0x800c03f,0x803f,0x3f,0x0,0x0,0x8000,0x0,0x3f,0x0,0x0,0x0,0x8004000,0x0,0x0,0x800c0ff,0x100,0x800c0ff,0x0,0x0,0x0,0xc0000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x1000,0x0,0x0,0xc0000000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xc0000000,0x0,0x0,0x0,0x800c0ff,0x0,0x800c0ff,0x0,0x800c0ff,0x0,0x0,0x180003f,0x0,0x180003f,0x1800000,0x3f,0x20,0x0,0x1,0x1,0x8,0x10,0x4,0x20000000,0x20000000,0x0,0x200,0x2000000,0x2000000,0x2,0x0,0x0,0x200,0x2000000,0x2000000,0x1,}; + } + private static void jj_la1_6() { + jj_la1_6 = new int[] {0x0,0x30,0x0,0x30,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x3000,0x0,0x0,0x3000,0x0,0x30,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x30,0x0,0x0,0x0,0x0,0x30,0x30,0x0,0x0,0x30,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x30,0x0,0x0,0x30,0x0,0x30,0x30,0x30,0x30,0x4f036,0x0,0xc0,0x0,0x0,0x0,0x800,0x9f006,0x9f006,0x12f006,0x12f006,0x2000000,0x8f006,0x10f006,0x4f036,0xf006,0x0,0x600000,0x600000,0x0,0x0,0x800000,0x0,0x1000000,0x0,0x0,0x30,0x0,0x30,0x0,0x30,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,}; + } + private static void jj_la1_7() { + jj_la1_7 = new int[] {0x0,0xb00,0x0,0xb00,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xb00,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xb00,0x0,0x0,0x0,0x0,0xb00,0xb00,0x0,0x0,0xb00,0x800,0x800,0x0,0x0,0x0,0x0,0x800,0x800,0x0,0x0,0x300,0x0,0x0,0xb00,0x0,0xb00,0x300,0x300,0x0,0x300,0x2000,0x0,0x2000,0x2000,0x2000,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x300,0x0,0x300,0x0,0x0,0x0,0x4000,0x0,0x0,0x0,0x0,0x0,0xb00,0x0,0xb00,0x0,0xb00,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,}; + } + + public XPath(java.io.InputStream stream) { + this(stream, null); + } + public XPath(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new XPathTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 166; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 166; i++) jj_la1[i] = -1; + } + + public XPath(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new XPathTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 166; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 166; i++) jj_la1[i] = -1; + } + + public XPath(XPathTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 166; i++) jj_la1[i] = -1; + } + + public void ReInit(XPathTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 166; i++) jj_la1[i] = -1; + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.Vector jj_expentries = new java.util.Vector(); + private int[] jj_expentry; + private int jj_kind = -1; + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[251]; + for (int i = 0; i < 251; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 166; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1<", + "", + "", + "", + "", + "", + "", + "", + "\"encoding\"", + "", + "", + "", + "", + "", + "", + "\"\"", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "\"ordered\"", + "\"unordered\"", + "", + "", + "\"yes\"", + "\"no\"", + "\"external\"", + "\"or\"", + "\"and\"", + "\"div\"", + "\"idiv\"", + "\"mod\"", + "\"*\"", + "\"in\"", + "", + "", + "", + "\"$\"", + "", + "", + "", + "\"?\"", + "", + "\"satisfies\"", + "\"return\"", + "\"then\"", + "\"else\"", + "\"default\"", + "", + "", + "\"preserve\"", + "\"strip\"", + "\"namespace\"", + "", + "\"to\"", + "\"where\"", + "\"collation\"", + "\"intersect\"", + "\"union\"", + "\"except\"", + "\"as\"", + "\"at\"", + "\"case\"", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "\"*\"", + "\"*\"", + "", + "", + "\"/\"", + "\"//\"", + "\"/\"", + "\"//\"", + "\"=\"", + "\"=\"", + "\"is\"", + "\"!=\"", + "\"<=\"", + "\"<<\"", + "\">=\"", + "\">>\"", + "\"eq\"", + "\"ne\"", + "\"gt\"", + "\"ge\"", + "\"lt\"", + "\"le\"", + "\":=\"", + "\"<\"", + "\">\"", + "\"-\"", + "\"+\"", + "\"-\"", + "\"+\"", + "\"?\"", + "\"*\"", + "\"+\"", + "\"|\"", + "\"(\"", + "\"@\"", + "\"[\"", + "\"]\"", + "\")\"", + "\")\"", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "\",\"", + "\",\"", + "\";\"", + "\"%%%\"", + "\"\\\"\"", + "\"\\\"\"", + "\".\"", + "\"..\"", + "", + "", + "\"ascending\"", + "\"descending\"", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "\"", + "", + "", + "", + "\"<\"", + "\"<\"", + "\">\"", + "\"/>\"", + "\"\"", + "\"=\"", + "", + "\"{\"", + "\"{\"", + "\"{{\"", + "\"}}\"", + "\"\\\"\\\"\"", + "\"\\\'\\\'\"", + "", + "", + "", + "", + "", + "", + "", + "\"\\\'\"", + "\"\\\'\"", + "", + "", + "", + "\"(::\"", + "", + "\"::)\"", + "", + "", + "\"(:\"", + "", + "\":)\"", + "\"pragma\"", + "\"extension\"", + "\"\"", + "", + "", + "", + "", + "", + "", + "\"}\"", + "", + "", + "", + "", + "", + "", + "", + "", + "", + }; + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathQueryBuilder.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathQueryBuilder.java new file mode 100644 index 00000000000..013bf1a5d95 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathQueryBuilder.java @@ -0,0 +1,1256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.query.xpath; + +import org.apache.commons.collections.map.ReferenceMap; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.query.DefaultQueryNodeVisitor; +import org.apache.jackrabbit.spi.commons.query.DerefQueryNode; +import org.apache.jackrabbit.spi.commons.query.LocationStepQueryNode; +import org.apache.jackrabbit.spi.commons.query.NAryQueryNode; +import org.apache.jackrabbit.spi.commons.query.NodeTypeQueryNode; +import org.apache.jackrabbit.spi.commons.query.NotQueryNode; +import org.apache.jackrabbit.spi.commons.query.OrderQueryNode; +import org.apache.jackrabbit.spi.commons.query.PathQueryNode; +import org.apache.jackrabbit.spi.commons.query.PropertyFunctionQueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryConstants; +import org.apache.jackrabbit.spi.commons.query.QueryNode; +import org.apache.jackrabbit.spi.commons.query.QueryNodeFactory; +import org.apache.jackrabbit.spi.commons.query.QueryRootNode; +import org.apache.jackrabbit.spi.commons.query.RelationQueryNode; +import org.apache.jackrabbit.spi.commons.query.TextsearchQueryNode; +import org.apache.jackrabbit.util.ISO8601; +import org.apache.jackrabbit.util.ISO9075; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import javax.jcr.query.InvalidQueryException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Map; + +/** + * Query builder that translates a XPath statement into a query tree structure. + */ +public class XPathQueryBuilder implements XPathVisitor, XPathTreeConstants { + + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + + /** + * Namespace uri for xpath functions. See also class SearchManager + */ + static final String NS_FN_URI = "http://www.w3.org/2005/xpath-functions"; + + /** + * Name for 'fn:not' + */ + static final Name FN_NOT = NAME_FACTORY.create(NS_FN_URI, "not"); + + /** + * Name for 'fn:lower-case' + */ + static final Name FN_LOWER_CASE = NAME_FACTORY.create(NS_FN_URI, "lower-case"); + + /** + * Name for 'fn:upper-case' + */ + static final Name FN_UPPER_CASE = NAME_FACTORY.create(NS_FN_URI, "upper-case"); + + /** + * Name for 'rep:normalize' + */ + static final Name REP_NORMALIZE = NAME_FACTORY.create(Name.NS_REP_URI, "normalize"); + + /** + * Name for 'not' as defined in XPath 1.0 (no prefix) + */ + static final Name FN_NOT_10 = NAME_FACTORY.create("", "not"); + + /** + * Name for true function. + */ + static final Name FN_TRUE = NAME_FACTORY.create("", "true"); + + /** + * Name for false function. + */ + static final Name FN_FALSE = NAME_FACTORY.create("", "false"); + + /** + * Name for position function. + */ + static final Name FN_POSITION = NAME_FACTORY.create("", "position"); + + /** + * Name for element function. + */ + static final Name FN_ELEMENT = NAME_FACTORY.create("", "element"); + + /** + * Name for the full position function including bracket + */ + static final Name FN_POSITION_FULL = NAME_FACTORY.create("", "position()"); + + /** + * Name for jcr:xmltext + */ + static final Name JCR_XMLTEXT = NAME_FACTORY.create(Name.NS_JCR_URI, "xmltext"); + + /** + * Name for last function. + */ + static final Name FN_LAST = NAME_FACTORY.create("", "last"); + + /** + * Name for first function. + */ + static final Name FN_FIRST = NAME_FACTORY.create("", "first"); + + /** + * Name for xs:dateTime + */ + static final Name XS_DATETIME = NAME_FACTORY.create("http://www.w3.org/2001/XMLSchema", "dateTime"); + + /** + * Name for jcr:like + */ + static final Name JCR_LIKE = NAME_FACTORY.create(Name.NS_JCR_URI, "like"); + + /** + * Name for jcr:deref + */ + static final Name JCR_DEREF = NAME_FACTORY.create(Name.NS_JCR_URI, "deref"); + + /** + * Name for jcr:contains + */ + static final Name JCR_CONTAINS = NAME_FACTORY.create(Name.NS_JCR_URI, "contains"); + + /** + * Name for jcr:root + */ + static final Name JCR_ROOT = NAME_FACTORY.create(Name.NS_JCR_URI, "root"); + + /** + * Name for jcr:score + */ + static final Name JCR_SCORE = NAME_FACTORY.create(Name.NS_JCR_URI, "score"); + + /** + * Name for rep:similar + */ + static final Name REP_SIMILAR = NAME_FACTORY.create(Name.NS_REP_URI, "similar"); + + /** + * Name for rep:spellcheck + */ + static final Name REP_SPELLCHECK = NAME_FACTORY.create(Name.NS_REP_URI, "spellcheck"); + + /** + * String constant for operator 'eq' + */ + private static final String OP_EQ = "eq"; + + /** + * String constant for operator 'ne' + */ + private static final String OP_NE = "ne"; + + /** + * String constant for operator 'gt' + */ + private static final String OP_GT = "gt"; + + /** + * String constant for operator 'ge' + */ + private static final String OP_GE = "ge"; + + /** + * String constant for operator 'lt' + */ + private static final String OP_LT = "lt"; + + /** + * String constant for operator 'le' + */ + private static final String OP_LE = "le"; + + /** + * String constant for operator '=' + */ + private static final String OP_SIGN_EQ = "="; + + /** + * String constant for operator '!=' + */ + private static final String OP_SIGN_NE = "!="; + + /** + * String constant for operator '>' + */ + private static final String OP_SIGN_GT = ">"; + + /** + * String constant for operator '>=' + */ + private static final String OP_SIGN_GE = ">="; + + /** + * String constant for operator '<' + */ + private static final String OP_SIGN_LT = "<"; + + /** + * String constant for operator '<=' + */ + private static final String OP_SIGN_LE = "<="; + + /** + * Map of reusable XPath parser instances indexed by NamespaceResolver. + */ + private static final Map parsers = new ReferenceMap(ReferenceMap.WEAK, ReferenceMap.WEAK); + + /** + * The root QueryNode + */ + private final QueryRootNode root; + + /** + * The {@link NameResolver} in use + */ + private final NameResolver resolver; + + /** + * List of exceptions that are created while building the query tree + */ + private final List exceptions = new ArrayList(); + + /** + * Temporary relative path + */ + private PathBuilder tmpRelPath; + + /** + * The query node factory. + */ + private final QueryNodeFactory factory; + + /** + * Creates a new XPathQueryBuilder instance. + * + * @param statement the XPath statement. + * @param resolver the name resolver to use. + * @param factory the query node factory. + * @throws InvalidQueryException if the XPath statement is malformed. + */ + private XPathQueryBuilder(String statement, + NameResolver resolver, + QueryNodeFactory factory) + throws InvalidQueryException { + this.resolver = resolver; + this.factory = factory; + this.root = factory.createQueryRootNode(); + try { + // create an XQuery statement because we're actually using an + // XQuery parser. + statement = "for $v in " + statement + " return $v"; + // get parser + XPath parser; + synchronized (parsers) { + parser = (XPath) parsers.get(resolver); + if (parser == null) { + parser = new XPath(new StringReader(statement)); + parsers.put(resolver, parser); + } + } + + SimpleNode query; + // guard against concurrent use within same session + synchronized (parser) { + parser.ReInit(new StringReader(statement)); + query = parser.XPath2(); + } + query.jjtAccept(this, root); + } catch (ParseException e) { + throw new InvalidQueryException(e.getMessage() + " for statement: " + statement, e); + } catch (Throwable t) { + // also catch any other exception + throw new InvalidQueryException(t.getMessage() + " for statement: " + statement, t); + } + if (exceptions.size() > 0) { + // simply report the first one + Exception e = (Exception) exceptions.get(0); + if (e instanceof InvalidQueryException) { + // just re-throw + throw (InvalidQueryException) e; + } else { + // otherwise package + throw new InvalidQueryException(e.getMessage(), e); + } + } + } + + /** + * Creates a QueryNode tree from a XPath statement using the + * passed query node factory. + * + * @param statement the XPath statement. + * @param resolver the name resolver to use. + * @param factory the query node factory. + * @return the QueryNode tree for the XPath statement. + * @throws InvalidQueryException if the XPath statement is malformed. + */ + public static QueryRootNode createQuery(String statement, + NameResolver resolver, + QueryNodeFactory factory) + throws InvalidQueryException { + return new XPathQueryBuilder(statement, resolver, factory).getRootNode(); + } + + /** + * Creates a String representation of the query node tree in XPath syntax. + * + * @param root the root of the query node tree. + * @param resolver to resolve Names. + * @return a String representation of the query node tree. + * @throws InvalidQueryException if the query node tree cannot be converted + * into a String representation due to restrictions in XPath. + */ + public static String toString(QueryRootNode root, NameResolver resolver) + throws InvalidQueryException { + return QueryFormat.toString(root, resolver); + } + + /** + * Returns the root node of the QueryNode tree. + * + * @return the root node of the QueryNode tree. + */ + QueryRootNode getRootNode() { + return root; + } + + //---------------------< XPathVisitor >------------------------------------- + + /** + * Implements the generic visit method for this XPathVisitor. + * + * @param node the current node as created by the XPath parser. + * @param data the current QueryNode created by this + * XPathVisitor. + * @return the current QueryNode. Can be different from + * data. + */ + public Object visit(SimpleNode node, Object data) { + QueryNode queryNode = (QueryNode) data; + switch (node.getId()) { + case JJTXPATH2: + queryNode = createPathQueryNode(node); + break; + case JJTROOT: + case JJTROOTDESCENDANTS: + if (queryNode instanceof PathQueryNode) { + ((PathQueryNode) queryNode).setAbsolute(true); + } else { + exceptions.add(new InvalidQueryException( + "Unsupported root level query node: " + queryNode)); + } + break; + case JJTSTEPEXPR: + if (isAttributeAxis(node)) { + if (queryNode.getType() == QueryNode.TYPE_RELATION + || (queryNode.getType() == QueryNode.TYPE_DEREF && ((DerefQueryNode) queryNode).getRefProperty() == null) + || queryNode.getType() == QueryNode.TYPE_ORDER + || queryNode.getType() == QueryNode.TYPE_PATH + || queryNode.getType() == QueryNode.TYPE_TEXTSEARCH) { + // traverse + node.childrenAccept(this, queryNode); + } else if (queryNode.getType() == QueryNode.TYPE_NOT) { + // is null expression + RelationQueryNode isNull + = factory.createRelationQueryNode(queryNode, + RelationQueryNode.OPERATION_NULL); + applyRelativePath(isNull); + node.childrenAccept(this, isNull); + NotQueryNode notNode = (NotQueryNode) queryNode; + NAryQueryNode parent = (NAryQueryNode) notNode.getParent(); + parent.removeOperand(notNode); + parent.addOperand(isNull); + } else { + // not null expression + RelationQueryNode notNull = + factory.createRelationQueryNode(queryNode, + RelationQueryNode.OPERATION_NOT_NULL); + applyRelativePath(notNull); + node.childrenAccept(this, notNull); + ((NAryQueryNode) queryNode).addOperand(notNull); + } + } else { + if (queryNode.getType() == QueryNode.TYPE_PATH) { + createLocationStep(node, (NAryQueryNode) queryNode); + } else if (queryNode.getType() == QueryNode.TYPE_TEXTSEARCH + || queryNode.getType() == QueryNode.TYPE_RELATION) { + node.childrenAccept(this, queryNode); + } else { + // step within a predicate + RelationQueryNode tmp = factory.createRelationQueryNode( + null, RelationQueryNode.OPERATION_NOT_NULL); + node.childrenAccept(this, tmp); + if (tmpRelPath == null) { + tmpRelPath = new PathBuilder(); + } + PathQueryNode relPath = tmp.getRelativePath(); + LocationStepQueryNode[] steps = relPath.getPathSteps(); + + Name nameTest = steps[steps.length-1].getNameTest(); + if (nameTest==null) { + // see LocationStepQueryNode javadoc on when getNameTest()==null: when it was a star (asterisk) + nameTest = RelationQueryNode.STAR_NAME_TEST; + } + tmpRelPath.addLast(nameTest); + } + } + break; + case JJTNAMETEST: + if (queryNode.getType() == QueryNode.TYPE_LOCATION + || queryNode.getType() == QueryNode.TYPE_DEREF + || queryNode.getType() == QueryNode.TYPE_RELATION + || queryNode.getType() == QueryNode.TYPE_TEXTSEARCH + || queryNode.getType() == QueryNode.TYPE_PATH) { + createNodeTest(node, queryNode); + } else if (queryNode.getType() == QueryNode.TYPE_ORDER) { + setOrderSpecPath(node, (OrderQueryNode) queryNode); + } else { + // traverse + node.childrenAccept(this, queryNode); + } + break; + case JJTELEMENTNAMEORWILDCARD: + if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + SimpleNode child = (SimpleNode) node.jjtGetChild(0); + if (child.getId() != JJTANYNAME) { + createNodeTest(child, queryNode); + } + } + break; + case JJTTEXTTEST: + if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + LocationStepQueryNode loc = (LocationStepQueryNode) queryNode; + loc.setNameTest(JCR_XMLTEXT); + } + break; + case JJTTYPENAME: + if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + LocationStepQueryNode loc = (LocationStepQueryNode) queryNode; + String ntName = ((SimpleNode) node.jjtGetChild(0)).getValue(); + try { + Name nt = resolver.getQName(ntName); + NodeTypeQueryNode nodeType = factory.createNodeTypeQueryNode(loc, nt); + loc.addPredicate(nodeType); + } catch (NameException e) { + exceptions.add(new InvalidQueryException("Not a valid name: " + ntName)); + } catch (NamespaceException e) { + exceptions.add(new InvalidQueryException("Not a valid name: " + ntName)); + } + } + break; + case JJTOREXPR: + NAryQueryNode parent = (NAryQueryNode) queryNode; + QueryNode orQueryNode = factory.createOrQueryNode(parent); + parent.addOperand(orQueryNode); + // traverse + node.childrenAccept(this, orQueryNode); + break; + case JJTANDEXPR: + parent = (NAryQueryNode) queryNode; + QueryNode andQueryNode = factory.createAndQueryNode(parent); + parent.addOperand(andQueryNode); + // traverse + node.childrenAccept(this, andQueryNode); + break; + case JJTCOMPARISONEXPR: + createExpression(node, (NAryQueryNode) queryNode); + break; + case JJTSTRINGLITERAL: + case JJTDECIMALLITERAL: + case JJTDOUBLELITERAL: + case JJTINTEGERLITERAL: + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + assignValue(node, (RelationQueryNode) queryNode); + } else if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + if (node.getId() == JJTINTEGERLITERAL) { + int index = Integer.parseInt(node.getValue()); + ((LocationStepQueryNode) queryNode).setIndex(index); + } else { + exceptions.add(new InvalidQueryException("LocationStep only allows integer literal as position index")); + } + } else { + exceptions.add(new InvalidQueryException("Parse error: data is not a RelationQueryNode")); + } + break; + case JJTUNARYMINUS: + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + ((RelationQueryNode) queryNode).setUnaryMinus(true); + } else { + exceptions.add(new InvalidQueryException("Parse error: data is not a RelationQueryNode")); + } + break; + case JJTFUNCTIONCALL: + queryNode = createFunction(node, queryNode); + break; + case JJTORDERBYCLAUSE: + root.setOrderNode(factory.createOrderQueryNode(root)); + queryNode = root.getOrderNode(); + node.childrenAccept(this, queryNode); + break; + case JJTORDERSPEC: + OrderQueryNode orderQueryNode = (OrderQueryNode) queryNode; + orderQueryNode.newOrderSpec(); + node.childrenAccept(this, queryNode); + if (!orderQueryNode.isValid()) { + exceptions.add(new InvalidQueryException("Invalid order specification. (Missing @?)")); + } + break; + case JJTORDERMODIFIER: + if (node.jjtGetNumChildren() > 0 + && ((SimpleNode) node.jjtGetChild(0)).getId() == JJTDESCENDING) { + ((OrderQueryNode) queryNode).setAscending(false); + } + break; + case JJTPREDICATELIST: + if (queryNode.getType() == QueryNode.TYPE_PATH) { + // switch to last location + QueryNode[] operands = ((PathQueryNode) queryNode).getOperands(); + queryNode = operands[operands.length - 1]; + } + node.childrenAccept(this, queryNode); + break; + case JJTPREDICATE: + if (queryNode.getType() == QueryNode.TYPE_LOCATION + || queryNode.getType() == QueryNode.TYPE_DEREF) { + node.childrenAccept(this, queryNode); + } else { + // predicate not allowed here + exceptions.add(new InvalidQueryException("Unsupported location for predicate")); + } + break; + case JJTDOTDOT: + if (queryNode instanceof LocationStepQueryNode) { + ((LocationStepQueryNode) queryNode).setNameTest(PATH_FACTORY.getParentElement().getName()); + } else { + ((RelationQueryNode) queryNode).addPathElement(PATH_FACTORY.getParentElement()); + } + break; + default: + // per default traverse + node.childrenAccept(this, queryNode); + } + return queryNode; + } + + //----------------------< internal >---------------------------------------- + + /** + * Applies {@link #tmpRelPath} to node and reset the path to + * null. + * + * @param node a relation query node. + */ + private void applyRelativePath(RelationQueryNode node) { + Path relPath = getRelativePath(); + if (relPath != null) { + for (int i = 0; i < relPath.getLength(); i++) { + node.addPathElement(relPath.getElements()[i]); + } + } + } + + /** + * Returns {@link #tmpRelPath} or null if there is none set. + * When this method returns {@link #tmpRelPath} will have been set + * null. + * + * @return {@link #tmpRelPath}. + */ + private Path getRelativePath() { + try { + if (tmpRelPath != null) { + return tmpRelPath.getPath(); + } + } catch (MalformedPathException e) { + // should never happen + } finally { + tmpRelPath = null; + } + return null; + } + + /** + * Creates a LocationStepQueryNode at the current position + * in parent. + * + * @param node the current node in the xpath syntax tree. + * @param parent the parent PathQueryNode. + * @return the created LocationStepQueryNode. + */ + private LocationStepQueryNode createLocationStep(SimpleNode node, NAryQueryNode parent) { + LocationStepQueryNode queryNode = null; + boolean descendant = false; + Node p = node.jjtGetParent(); + for (int i = 0; i < p.jjtGetNumChildren(); i++) { + SimpleNode c = (SimpleNode) p.jjtGetChild(i); + if (c == node) { + queryNode = factory.createLocationStepQueryNode(parent); + queryNode.setNameTest(null); + queryNode.setIncludeDescendants(descendant); + parent.addOperand(queryNode); + break; + } + descendant = (c.getId() == JJTSLASHSLASH + || c.getId() == JJTROOTDESCENDANTS); + } + + node.childrenAccept(this, queryNode); + + return queryNode; + } + + /** + * Assigns a Name to one of the following QueryNodes: + * {@link RelationQueryNode}, {@link DerefQueryNode}, {@link RelationQueryNode}, + * {@link PathQueryNode}, {@link OrderQueryNode}, {@link TextsearchQueryNode}. + * + * @param node the current node in the xpath syntax tree. + * @param queryNode the query node. + */ + private void createNodeTest(SimpleNode node, QueryNode queryNode) { + if (node.jjtGetNumChildren() > 0) { + SimpleNode child = (SimpleNode) node.jjtGetChild(0); + if (child.getId() == JJTQNAME || child.getId() == JJTQNAMEFORITEMTYPE) { + try { + Name name = decode(resolver.getQName(child.getValue())); + if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + if (name.equals(JCR_ROOT)) { + name = LocationStepQueryNode.EMPTY_NAME; + } + ((LocationStepQueryNode) queryNode).setNameTest(name); + } else if (queryNode.getType() == QueryNode.TYPE_DEREF) { + ((DerefQueryNode) queryNode).setRefProperty(name); + } else if (queryNode.getType() == QueryNode.TYPE_RELATION) { + Path.Element element = PATH_FACTORY.createElement(name); + ((RelationQueryNode) queryNode).addPathElement(element); + } else if (queryNode.getType() == QueryNode.TYPE_PATH) { + root.addSelectProperty(name); + } else if (queryNode.getType() == QueryNode.TYPE_ORDER) { + root.getOrderNode().addOrderSpec(name, true); + } else if (queryNode.getType() == QueryNode.TYPE_TEXTSEARCH) { + TextsearchQueryNode ts = (TextsearchQueryNode) queryNode; + ts.addPathElement(PATH_FACTORY.createElement(name)); + if (isAttributeNameTest(node)) { + ts.setReferencesProperty(true); + } + } + } catch (RepositoryException e) { + exceptions.add(new InvalidQueryException("Illegal name: " + child.getValue())); + } + } else if (child.getId() == JJTSTAR) { + if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + ((LocationStepQueryNode) queryNode).setNameTest(null); + } else if (queryNode.getType() == QueryNode.TYPE_RELATION) { + ((RelationQueryNode) queryNode).addPathElement( + PATH_FACTORY.createElement(RelationQueryNode.STAR_NAME_TEST)); + } else if (queryNode.getType() == QueryNode.TYPE_TEXTSEARCH) { + ((TextsearchQueryNode) queryNode).addPathElement( + PATH_FACTORY.createElement(RelationQueryNode.STAR_NAME_TEST)); + } + } else { + exceptions.add(new InvalidQueryException("Unsupported location for name test: " + child)); + } + } + } + + /** + * Creates a new {@link org.apache.jackrabbit.spi.commons.query.RelationQueryNode} + * with queryNode as its parent node. + * + * @param node a comparison expression node. + * @param queryNode the current QueryNode. + */ + private void createExpression(SimpleNode node, NAryQueryNode queryNode) { + if (node.getId() != JJTCOMPARISONEXPR) { + throw new IllegalArgumentException("node must be of type ComparisonExpr"); + } + // get operation type + String opType = node.getValue(); + int type = 0; + if (opType.equals(OP_EQ)) { + type = RelationQueryNode.OPERATION_EQ_VALUE; + } else if (opType.equals(OP_SIGN_EQ)) { + type = RelationQueryNode.OPERATION_EQ_GENERAL; + } else if (opType.equals(OP_GT)) { + type = RelationQueryNode.OPERATION_GT_VALUE; + } else if (opType.equals(OP_SIGN_GT)) { + type = RelationQueryNode.OPERATION_GT_GENERAL; + } else if (opType.equals(OP_GE)) { + type = RelationQueryNode.OPERATION_GE_VALUE; + } else if (opType.equals(OP_SIGN_GE)) { + type = RelationQueryNode.OPERATION_GE_GENERAL; + } else if (opType.equals(OP_LE)) { + type = RelationQueryNode.OPERATION_LE_VALUE; + } else if (opType.equals(OP_SIGN_LE)) { + type = RelationQueryNode.OPERATION_LE_GENERAL; + } else if (opType.equals(OP_LT)) { + type = RelationQueryNode.OPERATION_LT_VALUE; + } else if (opType.equals(OP_SIGN_LT)) { + type = RelationQueryNode.OPERATION_LT_GENERAL; + } else if (opType.equals(OP_NE)) { + type = RelationQueryNode.OPERATION_NE_VALUE; + } else if (opType.equals(OP_SIGN_NE)) { + type = RelationQueryNode.OPERATION_NE_GENERAL; + } else { + exceptions.add(new InvalidQueryException("Unsupported ComparisonExpr type:" + node.getValue())); + } + + final RelationQueryNode rqn = factory.createRelationQueryNode(queryNode, type); + + // traverse + node.childrenAccept(this, rqn); + + // check if string transformation is valid + try { + rqn.acceptOperands(new DefaultQueryNodeVisitor() { + public Object visit(PropertyFunctionQueryNode node, Object data) { + String functionName = node.getFunctionName(); + if ((functionName.equals(PropertyFunctionQueryNode.LOWER_CASE) + || functionName.equals(PropertyFunctionQueryNode.UPPER_CASE)) + && rqn.getValueType() != QueryConstants.TYPE_STRING) { + String msg = "Upper and lower case function are only supported with String literals"; + exceptions.add(new InvalidQueryException(msg)); + } + return data; + } + }, null); + } + catch (RepositoryException e) { + exceptions.add(e); + } + + queryNode.addOperand(rqn); + } + + /** + * Creates the primary path query node. + * + * @param node xpath node representing the root of the parsed tree. + * @return the path query node + */ + private PathQueryNode createPathQueryNode(SimpleNode node) { + root.setLocationNode(factory.createPathQueryNode(root)); + node.childrenAccept(this, root.getLocationNode()); + return root.getLocationNode(); + } + + /** + * Assigns a value to the queryNode. + * + * @param node must be of type string, decimal, double or integer; otherwise + * an InvalidQueryException is added to {@link #exceptions}. + * @param queryNode current node in the query tree. + */ + private void assignValue(SimpleNode node, RelationQueryNode queryNode) { + if (node.getId() == JJTSTRINGLITERAL) { + queryNode.setStringValue(unescapeQuotes(node.getValue())); + } else if (node.getId() == JJTDECIMALLITERAL) { + queryNode.setDoubleValue(Double.parseDouble(node.getValue())); + } else if (node.getId() == JJTDOUBLELITERAL) { + queryNode.setDoubleValue(Double.parseDouble(node.getValue())); + } else if (node.getId() == JJTINTEGERLITERAL) { + // if this is an expression that contains position() do not change + // the type. + if (queryNode.getValueType() == QueryConstants.TYPE_POSITION) { + queryNode.setPositionValue(Integer.parseInt(node.getValue())); + } else { + queryNode.setLongValue(Long.parseLong(node.getValue())); + } + } else { + exceptions.add(new InvalidQueryException("Unsupported literal type:" + node.toString())); + } + } + + /** + * Creates a function based on node. + * + * @param node the function node from the xpath tree. + * @param queryNode the current query node. + * @return the function node + */ + private QueryNode createFunction(SimpleNode node, QueryNode queryNode) { + // find out function name + String tmp = ((SimpleNode) node.jjtGetChild(0)).getValue(); + String fName = tmp.substring(0, tmp.length() - 1); + try { + Name funName = resolver.getQName(fName); + + if (FN_NOT.equals(funName) || FN_NOT_10.equals(funName)) { + if (queryNode instanceof NAryQueryNode) { + QueryNode not = factory.createNotQueryNode(queryNode); + ((NAryQueryNode) queryNode).addOperand(not); + // @todo is this needed? + queryNode = not; + // traverse + if (node.jjtGetNumChildren() == 2) { + node.jjtGetChild(1).jjtAccept(this, queryNode); + } else { + exceptions.add(new InvalidQueryException("fn:not only supports one expression argument")); + } + } else { + exceptions.add(new InvalidQueryException("Unsupported location for function fn:not")); + } + } else if (XS_DATETIME.equals(funName)) { + // check arguments + if (node.jjtGetNumChildren() == 2) { + if (queryNode instanceof RelationQueryNode) { + RelationQueryNode rel = (RelationQueryNode) queryNode; + SimpleNode literal = (SimpleNode) node.jjtGetChild(1).jjtGetChild(0); + if (literal.getId() == JJTSTRINGLITERAL) { + String value = literal.getValue(); + // strip quotes + value = value.substring(1, value.length() - 1); + Calendar c = ISO8601.parse(value); + if (c == null) { + exceptions.add(new InvalidQueryException("Unable to parse string literal for xs:dateTime: " + value)); + } else { + rel.setDateValue(c.getTime()); + } + } else { + exceptions.add(new InvalidQueryException("Wrong argument type for xs:dateTime")); + } + } else { + exceptions.add(new InvalidQueryException("Unsupported location for function xs:dateTime")); + } + } else { + // wrong number of arguments + exceptions.add(new InvalidQueryException("Wrong number of arguments for xs:dateTime")); + } + } else if (JCR_CONTAINS.equals(funName)) { + // check number of arguments + if (node.jjtGetNumChildren() == 3) { + if (queryNode instanceof NAryQueryNode) { + SimpleNode literal = (SimpleNode) node.jjtGetChild(2).jjtGetChild(0); + if (literal.getId() == JJTSTRINGLITERAL) { + TextsearchQueryNode contains = factory.createTextsearchQueryNode( + queryNode, unescapeQuotes(literal.getValue())); + // assign property name + SimpleNode path = (SimpleNode) node.jjtGetChild(1); + path.jjtAccept(this, contains); + ((NAryQueryNode) queryNode).addOperand(contains); + } else { + exceptions.add(new InvalidQueryException("Wrong argument type for jcr:contains")); + } + } + } else { + // wrong number of arguments + exceptions.add(new InvalidQueryException("Wrong number of arguments for jcr:contains")); + } + } else if (JCR_LIKE.equals(funName)) { + // check number of arguments + if (node.jjtGetNumChildren() == 3) { + if (queryNode instanceof NAryQueryNode) { + RelationQueryNode like = factory.createRelationQueryNode( + queryNode, RelationQueryNode.OPERATION_LIKE); + ((NAryQueryNode) queryNode).addOperand(like); + + // assign property name + node.jjtGetChild(1).jjtAccept(this, like); + // check property name + if (like.getRelativePath() == null) { + exceptions.add(new InvalidQueryException("Wrong first argument type for jcr:like")); + } + + SimpleNode literal = (SimpleNode) node.jjtGetChild(2).jjtGetChild(0); + if (literal.getId() == JJTSTRINGLITERAL) { + like.setStringValue(unescapeQuotes(literal.getValue())); + } else { + exceptions.add(new InvalidQueryException("Wrong second argument type for jcr:like")); + } + } else { + exceptions.add(new InvalidQueryException("Unsupported location for function jcr:like")); + } + } else { + // wrong number of arguments + exceptions.add(new InvalidQueryException("Wrong number of arguments for jcr:like")); + } + } else if (FN_TRUE.equals(funName)) { + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + RelationQueryNode rel = (RelationQueryNode) queryNode; + rel.setStringValue("true"); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for true()")); + } + } else if (FN_FALSE.equals(funName)) { + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + RelationQueryNode rel = (RelationQueryNode) queryNode; + rel.setStringValue("false"); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for false()")); + } + } else if (FN_POSITION.equals(funName)) { + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + RelationQueryNode rel = (RelationQueryNode) queryNode; + if (rel.getOperation() == RelationQueryNode.OPERATION_EQ_GENERAL) { + // set dummy value to set type of relation query node + // will be overwritten when the tree is further parsed. + rel.setPositionValue(1); + rel.addPathElement(PATH_FACTORY.createElement(FN_POSITION_FULL)); + } else { + exceptions.add(new InvalidQueryException("Unsupported expression with position(). Only = is supported.")); + } + } else { + exceptions.add(new InvalidQueryException("Unsupported location for position()")); + } + } else if (FN_FIRST.equals(funName)) { + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + ((RelationQueryNode) queryNode).setPositionValue(1); + } else if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + ((LocationStepQueryNode) queryNode).setIndex(1); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for first()")); + } + } else if (FN_LAST.equals(funName)) { + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + ((RelationQueryNode) queryNode).setPositionValue(LocationStepQueryNode.LAST); + } else if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + ((LocationStepQueryNode) queryNode).setIndex(LocationStepQueryNode.LAST); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for last()")); + } + } else if (JCR_DEREF.equals(funName)) { + // check number of arguments + if (node.jjtGetNumChildren() == 3) { + boolean descendant = false; + if (queryNode.getType() == QueryNode.TYPE_LOCATION) { + LocationStepQueryNode loc = (LocationStepQueryNode) queryNode; + // remember if descendant axis + descendant = loc.getIncludeDescendants(); + queryNode = loc.getParent(); + ((NAryQueryNode) queryNode).removeOperand(loc); + } + if (queryNode.getType() == QueryNode.TYPE_PATH) { + PathQueryNode pathNode = (PathQueryNode) queryNode; + + pathNode.addPathStep(createDerefQueryNode(node, descendant, pathNode)); + } else if (queryNode.getType() == QueryNode.TYPE_RELATION) { + RelationQueryNode relNode = (RelationQueryNode) queryNode; + DerefQueryNode deref = createDerefQueryNode(node, descendant, relNode.getRelativePath()); + relNode.getRelativePath().addPathStep(deref); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for jcr:deref()")); + } + } + } else if (JCR_SCORE.equals(funName)) { + if (queryNode.getType() == QueryNode.TYPE_ORDER) { + setOrderSpecPath(node, (OrderQueryNode) queryNode); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for jcr:score()")); + } + } else if (FN_LOWER_CASE.equals(funName)) { + if (node.jjtGetNumChildren() == 2) { + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + RelationQueryNode relNode = (RelationQueryNode) queryNode; + relNode.addOperand(factory.createPropertyFunctionQueryNode( + relNode, PropertyFunctionQueryNode.LOWER_CASE)); + // get property name + node.jjtGetChild(1).jjtAccept(this, relNode); + } else if (queryNode.getType() == QueryNode.TYPE_ORDER) { + ((OrderQueryNode) queryNode).setFunction(FN_LOWER_CASE.getLocalName()); + node.childrenAccept(this, queryNode); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for fn:lower-case()")); + } + } else { + exceptions.add(new InvalidQueryException("Wrong number of argument for fn:lower-case()")); + } + } else if (FN_UPPER_CASE.equals(funName)) { + if (node.jjtGetNumChildren() == 2) { + if (queryNode.getType() == QueryNode.TYPE_RELATION) { + RelationQueryNode relNode = (RelationQueryNode) queryNode; + relNode.addOperand(factory.createPropertyFunctionQueryNode( + relNode, PropertyFunctionQueryNode.UPPER_CASE)); + // get property name + node.jjtGetChild(1).jjtAccept(this, relNode); + } else if (queryNode.getType() == QueryNode.TYPE_ORDER) { + ((OrderQueryNode) queryNode).setFunction(FN_UPPER_CASE.getLocalName()); + node.childrenAccept(this, queryNode); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for fn:upper-case()")); + } + } else { + exceptions.add(new InvalidQueryException("Wrong number of argument for fn:upper-case()")); + } + } else if (REP_NORMALIZE.equals(funName)) { + if (node.jjtGetNumChildren() == 2) { + if (queryNode.getType() == QueryNode.TYPE_ORDER) { + ((OrderQueryNode) queryNode).setFunction(REP_NORMALIZE.getLocalName()); + node.childrenAccept(this, queryNode); + } else { + exceptions.add(new InvalidQueryException("Unsupported location for rep:normalize()")); + } + } else { + exceptions.add(new InvalidQueryException("Wrong number of argument for rep:normalize()")); + } + } else if (REP_SIMILAR.equals(funName)) { + if (node.jjtGetNumChildren() == 3) { + if (queryNode instanceof NAryQueryNode) { + NAryQueryNode parent = (NAryQueryNode) queryNode; + RelationQueryNode rel = factory.createRelationQueryNode( + parent, RelationQueryNode.OPERATION_SIMILAR); + parent.addOperand(rel); + // assign path + node.jjtGetChild(1).jjtAccept(this, rel); + + // get path string + node.jjtGetChild(2).jjtAccept(this, rel); + // check if string is set + if (rel.getStringValue() == null) { + exceptions.add(new InvalidQueryException( + "Second argument for rep:similar() must be of type string")); + } + } else { + exceptions.add(new InvalidQueryException( + "Unsupported location for rep:similar()")); + } + } else { + exceptions.add(new InvalidQueryException( + "Wrong number of arguments for rep:similar()")); + } + } else if (REP_SPELLCHECK.equals(funName) + && queryNode.getType() != QueryNode.TYPE_PATH) { + if (node.jjtGetNumChildren() == 2) { + if (queryNode instanceof NAryQueryNode) { + NAryQueryNode parent = (NAryQueryNode) queryNode; + RelationQueryNode rel = factory.createRelationQueryNode( + parent, RelationQueryNode.OPERATION_SPELLCHECK); + parent.addOperand(rel); + + // get string to check + node.jjtGetChild(1).jjtAccept(this, rel); + // check if string is set + if (rel.getStringValue() == null) { + exceptions.add(new InvalidQueryException( + "Argument for rep:spellcheck() must be of type string")); + } + + // set a dummy property name + rel.addPathElement(PATH_FACTORY.createElement(NameConstants.JCR_PRIMARYTYPE)); + } else { + exceptions.add(new InvalidQueryException( + "Unsupported location for rep:spellcheck()")); + } + } else { + exceptions.add(new InvalidQueryException( + "Wrong number of arguments for rep:spellcheck()")); + } + } else if (queryNode.getType() == QueryNode.TYPE_RELATION) { + // use function name as name of a pseudo property in a relation + try { + Name name = resolver.getQName(fName + "()"); + Path.Element element = PATH_FACTORY.createElement(name); + RelationQueryNode relNode = (RelationQueryNode) queryNode; + relNode.addPathElement(element); + } catch (NameException e) { + exceptions.add(e); + } + } else if (queryNode.getType() == QueryNode.TYPE_PATH) { + // use function name as name of a pseudo property in select clause + try { + Name name = resolver.getQName(fName + "()"); + root.addSelectProperty(name); + } catch (NameException e) { + exceptions.add(e); + } + } else { + exceptions.add(new InvalidQueryException("Unsupported function: " + fName)); + } + } catch (NamespaceException e) { + exceptions.add(e); + } catch (IllegalNameException e) { + exceptions.add(e); + } + return queryNode; + } + + private DerefQueryNode createDerefQueryNode(SimpleNode node, boolean descendant, QueryNode pathNode) + throws NamespaceException { + DerefQueryNode derefNode = factory.createDerefQueryNode(pathNode, null, false); + + // assign property name + node.jjtGetChild(1).jjtAccept(this, derefNode); + // check property name + if (derefNode.getRefProperty() == null) { + exceptions.add(new InvalidQueryException("Wrong first argument type for jcr:deref")); + } + + SimpleNode literal = (SimpleNode) node.jjtGetChild(2).jjtGetChild(0); + if (literal.getId() == JJTSTRINGLITERAL) { + String value = literal.getValue(); + // strip quotes + value = value.substring(1, value.length() - 1); + if (!value.equals("*")) { + Name name = null; + try { + name = decode(resolver.getQName(value)); + } catch (NameException e) { + exceptions.add(new InvalidQueryException("Illegal name: " + value)); + } + derefNode.setNameTest(name); + } + } else { + exceptions.add(new InvalidQueryException("Second argument for jcr:deref must be a String")); + } + + // check if descendant + if (!descendant) { + Node p = node.jjtGetParent(); + for (int i = 0; i < p.jjtGetNumChildren(); i++) { + SimpleNode c = (SimpleNode) p.jjtGetChild(i); + if (c == node) { + break; + } + descendant = (c.getId() == JJTSLASHSLASH + || c.getId() == JJTROOTDESCENDANTS); + } + } + derefNode.setIncludeDescendants(descendant); + return derefNode; + } + + private void setOrderSpecPath(SimpleNode node, OrderQueryNode queryNode) { + SimpleNode child = (SimpleNode) node.jjtGetChild(0); + try { + String propName = child.getValue(); + if (child.getId() == JJTQNAMELPAR) { + // function name + // cut off left parenthesis at end + propName = propName.substring(0, propName.length() - 1); + } + Path.Element element = PathFactoryImpl.getInstance().createElement( + decode(resolver.getQName(propName))); + Path path = getRelativePath(); + if (path != null) { + path = path.resolve(element); + } else { + path = PathFactoryImpl.getInstance().create(element); + } + queryNode.setPath(path); + } catch (NameException e) { + exceptions.add(new InvalidQueryException("Illegal name: " + child.getValue())); + } catch (NamespaceException e) { + exceptions.add(new InvalidQueryException("Illegal name: " + child.getValue())); + } + } + + /** + * Returns true if node has a child node which is the attribute + * axis. + * + * @param node a node with type {@link #JJTSTEPEXPR}. + * @return true if this step expression uses the attribute axis. + */ + private boolean isAttributeAxis(SimpleNode node) { + for (int i = 0; i < node.jjtGetNumChildren(); i++) { + if (((SimpleNode) node.jjtGetChild(i)).getId() == JJTAT) { + return true; + } + } + return false; + } + + /** + * Returns true if the NodeTest node is an + * attribute name test. + * Example: + *

        +     * StepExpr
        +     *     At @
        +     *     NodeTest
        +     *         NameTest
        +     *             Name foo
        +     * 
        + * @param node a node with type {@link #JJTNAMETEST}. + * @return true if the name test node is on the + * attribute axis. + */ + private boolean isAttributeNameTest(SimpleNode node) { + SimpleNode stepExpr = (SimpleNode) node.jjtGetParent().jjtGetParent(); + if (stepExpr.getId() == JJTSTEPEXPR) { + return ((SimpleNode) stepExpr.jjtGetChild(0)).getId() == JJTAT; + } + return false; + } + + /** + * Unescapes single or double quotes depending on how literal + * is enclosed and strips enclosing quotes. + * + *

        + * Examples:
        + * "foo""bar" -> foo"bar
        + * 'foo''bar' -> foo'bar
        + * but:
        + * 'foo""bar' -> foo""bar + * + * @param literal the string literal to unescape + * @return the unescaped and stripped literal. + */ + private String unescapeQuotes(String literal) { + String value = literal.substring(1, literal.length() - 1); + if (value.length() == 0) { + // empty string + return value; + } + if (literal.charAt(0) == '"') { + value = value.replaceAll("\"\"", "\""); + } else { + value = value.replaceAll("''", "'"); + } + return value; + } + + private static Name decode(Name name) { + String decodedLN = ISO9075.decode(name.getLocalName()); + if (decodedLN.equals(name.getLocalName())) { + return name; + } else { + return NAME_FACTORY.create(name.getNamespaceURI(), decodedLN); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathTokenManager.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathTokenManager.java new file mode 100644 index 00000000000..f46cedd6675 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathTokenManager.java @@ -0,0 +1,13698 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. XPathTokenManager.java */ +package org.apache.jackrabbit.spi.commons.query.xpath; +import java.io.*; +import java.util.Stack; +import java.util.Vector; + +public class XPathTokenManager implements XPathConstants +{ + private Stack stateStack = new Stack(); + // private Vector persistentLexStates = new Vector(); + static final int PARENMARKER = 2000; + + /** + * Push the current state onto the state stack. + */ + private void pushState() + { + // System.err.println("pushing: "+curLexState); printLinePos(); + stateStack.addElement(new Integer(curLexState)); + } + + /** + * Push the given state onto the state stack. + * @param state Must be a valid state. + */ + private void pushState(int state) + { + stateStack.push(new Integer(state)); + } + + /** + * Pop the state on the state stack, and switch to that state. + */ + private void popState() + { + if (stateStack.size() == 0) + { + printLinePos(); + } + + int nextState = ((Integer) stateStack.pop()).intValue(); + // System.err.println("pop "+nextState); printLinePos(); + if(nextState == PARENMARKER) + printLinePos(); + SwitchTo(nextState); + } + + /** + * Push the given state onto the state stack. + * @param state Must be a valid state. + */ + private boolean isState(int state) + { + for (int i = 0; i < stateStack.size(); i++) { + if(((Integer) stateStack.elementAt(i)).intValue() == state) + { + return true; + } + } + return false; + } + + /** + * Push a parenthesis state. This pushes, in addition to the + * lexical state value, a special marker that lets + * resetParenStateOrSwitch(int state) + * know if it should pop and switch. Used for the comma operator. + */ + private void pushParenState(int commaState, int rparState) + { + stateStack.push(new Integer(rparState)); + stateStack.push(new Integer(commaState)); + stateStack.push(new Integer(PARENMARKER)); + SwitchTo(commaState); + } + + + /** + * Print the current line position. + */ + public void printLinePos() + { + System.err.println("Line: " + input_stream.getEndLine()); + } + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_14(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_14(int pos, long active0) +{ + return jjMoveNfa_14(jjStopStringLiteralDfa_14(pos, active0), pos + 1); +} +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjStartNfaWithStates_14(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_14(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_14() +{ + switch(curChar) + { + case 63: + return jjMoveStringLiteralDfa1_14(0x20000L); + default : + return jjMoveNfa_14(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_14(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_14(0, active0); + return 1; + } + switch(curChar) + { + case 62: + if ((active0 & 0x20000L) != 0L) + return jjStopAtPos(1, 17); + break; + default : + break; + } + return jjStartNfa_14(0, active0); +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +static final long[] jjbitVec0 = { + 0x0L, 0xffffffffffffc000L, 0xfffff0007fffffffL, 0x7fffffL +}; +static final long[] jjbitVec2 = { + 0x0L, 0x0L, 0x0L, 0xff7fffffff7fffffL +}; +static final long[] jjbitVec3 = { + 0x7ff3ffffffffffffL, 0x7ffffffffffffdfeL, 0xffffffffffffffffL, 0xfc31ffffffffe00fL +}; +static final long[] jjbitVec4 = { + 0xffffffL, 0xffffffffffff0000L, 0xf80001ffffffffffL, 0x3L +}; +static final long[] jjbitVec5 = { + 0x0L, 0x0L, 0xfffffffbffffd740L, 0xffffd547f7fffL +}; +static final long[] jjbitVec6 = { + 0xffffffffffffdffeL, 0xffffffffdffeffffL, 0xffffffffffff0003L, 0x33fcfffffff199fL +}; +static final long[] jjbitVec7 = { + 0xfffe000000000000L, 0xfffffffe027fffffL, 0x7fL, 0x707ffffff0000L +}; +static final long[] jjbitVec8 = { + 0x7fffffe00000000L, 0xfffe0000000007feL, 0x7cffffffffffffffL, 0x60002f7fffL +}; +static final long[] jjbitVec9 = { + 0x23ffffffffffffe0L, 0x3ff000000L, 0x3c5fdfffff99fe0L, 0x30003b0000000L +}; +static final long[] jjbitVec10 = { + 0x36dfdfffff987e0L, 0x1c00005e000000L, 0x23edfdfffffbafe0L, 0x100000000L +}; +static final long[] jjbitVec11 = { + 0x23cdfdfffff99fe0L, 0x3b0000000L, 0x3bfc718d63dc7e0L, 0x0L +}; +static final long[] jjbitVec12 = { + 0x3effdfffffddfe0L, 0x300000000L, 0x3effdfffffddfe0L, 0x340000000L +}; +static final long[] jjbitVec13 = { + 0x3fffdfffffddfe0L, 0x300000000L, 0x0L, 0x0L +}; +static final long[] jjbitVec14 = { + 0xd7ffffffffffeL, 0x3fL, 0x200d6caefef02596L, 0x1fL +}; +static final long[] jjbitVec15 = { + 0x0L, 0x3fffffffeffL, 0x0L, 0x0L +}; +static final long[] jjbitVec16 = { + 0x0L, 0x0L, 0xffffffff00000000L, 0x7fffffffff003fL +}; +static final long[] jjbitVec17 = { + 0x500000000007daedL, 0x2c62ab82315001L, 0xf580c90040000000L, 0x201080000000007L +}; +static final long[] jjbitVec18 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffff0fffffffL, 0x3ffffffffffffffL +}; +static final long[] jjbitVec19 = { + 0xffffffff3f3fffffL, 0x3fffffffaaff3f3fL, 0x5fdfffffffffffffL, 0x1fdc1fff0fcf1fdcL +}; +static final long[] jjbitVec20 = { + 0x4c4000000000L, 0x0L, 0x7L, 0x0L +}; +static final long[] jjbitVec21 = { + 0x3fe00000080L, 0xfffffffffffffffeL, 0xfffffffe001fffffL, 0x7ffffffffffffffL +}; +static final long[] jjbitVec22 = { + 0x1fffffffffe0L, 0x0L, 0x0L, 0x0L +}; +static final long[] jjbitVec23 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0x3fffffffffL, 0x0L +}; +static final long[] jjbitVec24 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0xfffffffffL, 0x0L +}; +static final long[] jjbitVec25 = { + 0x0L, 0x0L, 0x80000000000000L, 0xff7fffffff7fffffL +}; +static final long[] jjbitVec26 = { + 0xffffffL, 0xffffffffffff0000L, 0xf80001ffffffffffL, 0x30003L +}; +static final long[] jjbitVec27 = { + 0xffffffffffffffffL, 0x30000003fL, 0xfffffffbffffd7c0L, 0xffffd547f7fffL +}; +static final long[] jjbitVec28 = { + 0xffffffffffffdffeL, 0xffffffffdffeffffL, 0xffffffffffff007bL, 0x33fcfffffff199fL +}; +static final long[] jjbitVec29 = { + 0xfffe000000000000L, 0xfffffffe027fffffL, 0xbbfffffbfffe007fL, 0x707ffffff0016L +}; +static final long[] jjbitVec30 = { + 0x7fffffe00000000L, 0xffff03ff0007ffffL, 0x7cffffffffffffffL, 0x3ff3dffffef7fffL +}; +static final long[] jjbitVec31 = { + 0xf3ffffffffffffeeL, 0xffcfff1e3fffL, 0xd3c5fdfffff99feeL, 0x3ffcfb080399fL +}; +static final long[] jjbitVec32 = { + 0xd36dfdfffff987e4L, 0x1fffc05e003987L, 0xf3edfdfffffbafeeL, 0xffc100003bbfL +}; +static final long[] jjbitVec33 = { + 0xf3cdfdfffff99feeL, 0xffc3b0c0398fL, 0xc3bfc718d63dc7ecL, 0xff8000803dc7L +}; +static final long[] jjbitVec34 = { + 0xc3effdfffffddfeeL, 0xffc300603ddfL, 0xc3effdfffffddfecL, 0xffc340603ddfL +}; +static final long[] jjbitVec35 = { + 0xc3fffdfffffddfecL, 0xffc300803dcfL, 0x0L, 0x0L +}; +static final long[] jjbitVec36 = { + 0x7ff7ffffffffffeL, 0x3ff7fffL, 0x3bff6caefef02596L, 0x3ff3f5fL +}; +static final long[] jjbitVec37 = { + 0xc2a003ff03000000L, 0xfffe03fffffffeffL, 0x2fe3ffffebf0fdfL, 0x0L +}; +static final long[] jjbitVec38 = { + 0x0L, 0x0L, 0x0L, 0x21fff0000L +}; +static final long[] jjbitVec39 = { + 0x3efffe000000a0L, 0xfffffffffffffffeL, 0xfffffffe661fffffL, 0x77ffffffffffffffL +}; +private final int jjMoveNfa_14(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((0x100002600L & l) == 0L) + break; + kind = 238; + jjCheckNAdd(2); + break; + case 1: + if ((0x3ff600000000000L & l) == 0L) + break; + kind = 46; + jjstateSet[jjnewStateCnt++] = 1; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 1: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 46) + kind = 46; + jjCheckNAdd(1); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 46) + kind = 46; + jjCheckNAdd(1); + break; + case 1: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 46) + kind = 46; + jjCheckNAdd(1); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_3(int pos, long active0, long active1, long active2) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_3(int pos, long active0, long active1, long active2) +{ + return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0, active1, active2), pos + 1); +} +private final int jjStartNfaWithStates_3(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_3(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_3() +{ + switch(curChar) + { + case 59: + return jjStopAtPos(0, 170); + case 101: + return jjMoveStringLiteralDfa1_3(0x100L); + default : + return jjMoveNfa_3(7, 0); + } +} +private final int jjMoveStringLiteralDfa1_3(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_3(0, active0, 0L, 0L); + return 1; + } + switch(curChar) + { + case 110: + return jjMoveStringLiteralDfa2_3(active0, 0x100L); + default : + break; + } + return jjStartNfa_3(0, active0, 0L, 0L); +} +private final int jjMoveStringLiteralDfa2_3(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_3(0, old0, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_3(1, active0, 0L, 0L); + return 2; + } + switch(curChar) + { + case 99: + return jjMoveStringLiteralDfa3_3(active0, 0x100L); + default : + break; + } + return jjStartNfa_3(1, active0, 0L, 0L); +} +private final int jjMoveStringLiteralDfa3_3(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_3(1, old0, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_3(2, active0, 0L, 0L); + return 3; + } + switch(curChar) + { + case 111: + return jjMoveStringLiteralDfa4_3(active0, 0x100L); + default : + break; + } + return jjStartNfa_3(2, active0, 0L, 0L); +} +private final int jjMoveStringLiteralDfa4_3(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_3(2, old0, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_3(3, active0, 0L, 0L); + return 4; + } + switch(curChar) + { + case 100: + return jjMoveStringLiteralDfa5_3(active0, 0x100L); + default : + break; + } + return jjStartNfa_3(3, active0, 0L, 0L); +} +private final int jjMoveStringLiteralDfa5_3(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_3(3, old0, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_3(4, active0, 0L, 0L); + return 5; + } + switch(curChar) + { + case 105: + return jjMoveStringLiteralDfa6_3(active0, 0x100L); + default : + break; + } + return jjStartNfa_3(4, active0, 0L, 0L); +} +private final int jjMoveStringLiteralDfa6_3(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_3(4, old0, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_3(5, active0, 0L, 0L); + return 6; + } + switch(curChar) + { + case 110: + return jjMoveStringLiteralDfa7_3(active0, 0x100L); + default : + break; + } + return jjStartNfa_3(5, active0, 0L, 0L); +} +private final int jjMoveStringLiteralDfa7_3(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_3(5, old0, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_3(6, active0, 0L, 0L); + return 7; + } + switch(curChar) + { + case 103: + if ((active0 & 0x100L) != 0L) + return jjStopAtPos(7, 8); + break; + default : + break; + } + return jjStartNfa_3(6, active0, 0L, 0L); +} +static final long[] jjbitVec40 = { + 0xfffffffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +static final long[] jjbitVec41 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private final int jjMoveNfa_3(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 7; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 7: + if ((0x100002600L & l) != 0L) + { + if (kind > 12) + kind = 12; + jjCheckNAdd(6); + } + else if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + else if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 0: + if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 1: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(1, 2); + break; + case 2: + if (curChar != 34) + break; + if (kind > 7) + kind = 7; + jjstateSet[jjnewStateCnt++] = 0; + break; + case 3: + if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + break; + case 4: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(4, 5); + break; + case 5: + if (curChar != 39) + break; + if (kind > 7) + kind = 7; + jjstateSet[jjnewStateCnt++] = 3; + break; + case 6: + if ((0x100002600L & l) == 0L) + break; + if (kind > 12) + kind = 12; + jjCheckNAdd(6); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + jjAddStates(0, 1); + break; + case 4: + jjAddStates(2, 3); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(0, 1); + break; + case 4: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(2, 3); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 7 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_15(int pos, long active0) +{ + switch (pos) + { + case 0: + if ((active0 & 0x20000L) != 0L) + { + jjmatchedKind = 215; + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_15(int pos, long active0) +{ + return jjMoveNfa_15(jjStopStringLiteralDfa_15(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_15(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_15(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_15() +{ + switch(curChar) + { + case 63: + return jjMoveStringLiteralDfa1_15(0x20000L); + default : + return jjMoveNfa_15(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_15(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_15(0, active0); + return 1; + } + switch(curChar) + { + case 62: + if ((active0 & 0x20000L) != 0L) + return jjStopAtPos(1, 17); + break; + default : + break; + } + return jjStartNfa_15(0, active0); +} +static final long[] jjbitVec42 = { + 0xfffffffffffffffeL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0x7fffffffffffffffL +}; +static final long[] jjbitVec43 = { + 0xffffffffffffffffL, 0xffffffffffffffffL, 0xffffffffffffffffL, 0x3fffffffffffffffL +}; +private final int jjMoveNfa_15(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 1; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffff00002600L & l) != 0L) + kind = 215; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + kind = 215; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 215) + kind = 215; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 1 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_21(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_21(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_21(jjStopStringLiteralDfa_21(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_21(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_21(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_21() +{ + switch(curChar) + { + case 62: + return jjStopAtPos(0, 201); + default : + return jjMoveNfa_21(1, 0); + } +} +private final int jjMoveNfa_21(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 6; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + case 0: + if ((0x100002600L & l) == 0L) + break; + kind = 237; + jjCheckNAdd(0); + break; + case 2: + if ((0x3ff600000000000L & l) != 0L) + jjAddStates(4, 5); + break; + case 3: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 5: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 203) + kind = 203; + jjstateSet[jjnewStateCnt++] = 5; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 203) + kind = 203; + jjCheckNAddStates(6, 8); + break; + case 2: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + case 5: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 203) + kind = 203; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 203) + kind = 203; + jjCheckNAddStates(6, 8); + break; + case 2: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 203) + kind = 203; + jjCheckNAdd(5); + break; + case 5: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 203) + kind = 203; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 6 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_22(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active3 & 0x40000000000L) != 0L) + { + jjmatchedKind = 213; + return 2; + } + return -1; + case 1: + if ((active3 & 0x40000000000L) != 0L) + { + jjmatchedKind = 214; + jjmatchedPos = 1; + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_22(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_22(jjStopStringLiteralDfa_22(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_22(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_22(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_22() +{ + switch(curChar) + { + case 45: + return jjMoveStringLiteralDfa1_22(0x40000000000L); + default : + return jjMoveNfa_22(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_22(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_22(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 45: + return jjMoveStringLiteralDfa2_22(active3, 0x40000000000L); + default : + break; + } + return jjStartNfa_22(0, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_22(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_22(0, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_22(1, 0L, 0L, 0L, active3); + return 2; + } + switch(curChar) + { + case 62: + if ((active3 & 0x40000000000L) != 0L) + return jjStopAtPos(2, 234); + break; + default : + break; + } + return jjStartNfa_22(1, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_22(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffff00002600L & l) != 0L) + { + if (kind > 213) + kind = 213; + } + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 2; + break; + case 1: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 2; + break; + case 2: + if ((0xffffffff00002600L & l) != 0L && kind > 214) + kind = 214; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 213) + kind = 213; + break; + case 2: + if (kind > 214) + kind = 214; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 213) + kind = 213; + break; + case 2: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 214) + kind = 214; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_24(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active3 & 0x100000000L) != 0L) + { + jjmatchedKind = 223; + return -1; + } + return -1; + case 1: + if ((active3 & 0x100000000L) != 0L) + { + if (jjmatchedPos == 0) + { + jjmatchedKind = 223; + jjmatchedPos = 0; + } + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_24(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_24(jjStopStringLiteralDfa_24(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_24(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_24(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_24() +{ + switch(curChar) + { + case 58: + return jjMoveStringLiteralDfa1_24(0x100000000L); + default : + return jjMoveNfa_24(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_24(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_24(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + return jjMoveStringLiteralDfa2_24(active3, 0x100000000L); + default : + break; + } + return jjStartNfa_24(0, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_24(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_24(0, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_24(1, 0L, 0L, 0L, active3); + return 2; + } + switch(curChar) + { + case 41: + if ((active3 & 0x100000000L) != 0L) + return jjStopAtPos(2, 224); + break; + default : + break; + } + return jjStartNfa_24(1, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_24(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 2; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffff00002600L & l) != 0L) + { + if (kind > 223) + kind = 223; + } + if ((0x100002600L & l) != 0L) + { + if (kind > 239) + kind = 239; + jjCheckNAdd(1); + } + break; + case 1: + if ((0x100002600L & l) == 0L) + break; + if (kind > 239) + kind = 239; + jjCheckNAdd(1); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + kind = 223; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 223) + kind = 223; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 2 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_7(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_7(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_7(jjStopStringLiteralDfa_7(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_7(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_7(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_7() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_7(0x800000000L); + case 41: + return jjStopAtPos(0, 139); + case 42: + return jjStopAtPos(0, 102); + case 123: + return jjStopAtPos(0, 205); + default : + return jjMoveNfa_7(9, 0); + } +} +private final int jjMoveStringLiteralDfa1_7(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_7(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + return jjStopAtPos(1, 227); + break; + default : + break; + } + return jjStartNfa_7(0, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_7(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 31; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 9: + case 0: + if ((0x100002600L & l) == 0L) + break; + if (kind > 12) + kind = 12; + jjCheckNAdd(0); + break; + case 2: + if ((0x100002600L & l) != 0L) + jjAddStates(4, 5); + break; + case 3: + if (curChar == 40 && kind > 157) + kind = 157; + break; + case 11: + if ((0x100002600L & l) != 0L) + jjAddStates(9, 10); + break; + case 12: + if (curChar == 40 && kind > 160) + kind = 160; + break; + case 19: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 18; + break; + case 27: + if ((0x3ff600000000000L & l) != 0L) + jjAddStates(11, 12); + break; + case 28: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 29; + break; + case 30: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 185) + kind = 185; + jjstateSet[jjnewStateCnt++] = 30; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 9: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 185) + kind = 185; + jjCheckNAddStates(13, 15); + } + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 24; + else if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 8; + break; + case 1: + if (curChar == 116) + jjAddStates(4, 5); + break; + case 4: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 5: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 6: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 5; + break; + case 7: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 6; + break; + case 8: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 7; + break; + case 10: + if (curChar == 116) + jjAddStates(9, 10); + break; + case 13: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 10; + break; + case 14: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 13; + break; + case 15: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 14; + break; + case 16: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 15; + break; + case 17: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 16; + break; + case 18: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 17; + break; + case 20: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 19; + break; + case 21: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 20; + break; + case 22: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 21; + break; + case 23: + if (curChar == 104) + jjstateSet[jjnewStateCnt++] = 22; + break; + case 24: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 23; + break; + case 25: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 24; + break; + case 26: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 185) + kind = 185; + jjCheckNAddStates(13, 15); + break; + case 27: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(27, 28); + break; + case 29: + case 30: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 185) + kind = 185; + jjCheckNAdd(30); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 9: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 185) + kind = 185; + jjCheckNAddStates(13, 15); + break; + case 27: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(27, 28); + break; + case 29: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 185) + kind = 185; + jjCheckNAdd(30); + break; + case 30: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 185) + kind = 185; + jjCheckNAdd(30); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 31 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_25(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_25(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_25(jjStopStringLiteralDfa_25(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_25(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_25(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_25() +{ + switch(curChar) + { + case 101: + return jjMoveStringLiteralDfa1_25(0x8000000000L); + case 112: + return jjMoveStringLiteralDfa1_25(0x4000000000L); + default : + return jjMoveNfa_25(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_25(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_25(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 114: + return jjMoveStringLiteralDfa2_25(active3, 0x4000000000L); + case 120: + return jjMoveStringLiteralDfa2_25(active3, 0x8000000000L); + default : + break; + } + return jjStartNfa_25(0, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_25(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_25(0, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_25(1, 0L, 0L, 0L, active3); + return 2; + } + switch(curChar) + { + case 97: + return jjMoveStringLiteralDfa3_25(active3, 0x4000000000L); + case 116: + return jjMoveStringLiteralDfa3_25(active3, 0x8000000000L); + default : + break; + } + return jjStartNfa_25(1, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa3_25(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_25(1, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_25(2, 0L, 0L, 0L, active3); + return 3; + } + switch(curChar) + { + case 101: + return jjMoveStringLiteralDfa4_25(active3, 0x8000000000L); + case 103: + return jjMoveStringLiteralDfa4_25(active3, 0x4000000000L); + default : + break; + } + return jjStartNfa_25(2, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa4_25(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_25(2, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_25(3, 0L, 0L, 0L, active3); + return 4; + } + switch(curChar) + { + case 109: + return jjMoveStringLiteralDfa5_25(active3, 0x4000000000L); + case 110: + return jjMoveStringLiteralDfa5_25(active3, 0x8000000000L); + default : + break; + } + return jjStartNfa_25(3, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa5_25(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_25(3, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_25(4, 0L, 0L, 0L, active3); + return 5; + } + switch(curChar) + { + case 97: + if ((active3 & 0x4000000000L) != 0L) + return jjStopAtPos(5, 230); + break; + case 115: + return jjMoveStringLiteralDfa6_25(active3, 0x8000000000L); + default : + break; + } + return jjStartNfa_25(4, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa6_25(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_25(4, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_25(5, 0L, 0L, 0L, active3); + return 6; + } + switch(curChar) + { + case 105: + return jjMoveStringLiteralDfa7_25(active3, 0x8000000000L); + default : + break; + } + return jjStartNfa_25(5, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa7_25(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_25(5, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_25(6, 0L, 0L, 0L, active3); + return 7; + } + switch(curChar) + { + case 111: + return jjMoveStringLiteralDfa8_25(active3, 0x8000000000L); + default : + break; + } + return jjStartNfa_25(6, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa8_25(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_25(6, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_25(7, 0L, 0L, 0L, active3); + return 8; + } + switch(curChar) + { + case 110: + if ((active3 & 0x8000000000L) != 0L) + return jjStopAtPos(8, 231); + break; + default : + break; + } + return jjStartNfa_25(7, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_25(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 1; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x100002600L & l) == 0L) + break; + kind = 239; + jjstateSet[jjnewStateCnt++] = 0; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 1 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_23(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active3 & 0x2840000000L) != 0L) + { + jjmatchedKind = 228; + return -1; + } + return -1; + case 1: + if ((active3 & 0x2840000000L) != 0L) + { + if (jjmatchedPos == 0) + { + jjmatchedKind = 228; + jjmatchedPos = 0; + } + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_23(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_23(jjStopStringLiteralDfa_23(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_23(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_23(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_23() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_23(0x840000000L); + case 58: + return jjMoveStringLiteralDfa1_23(0x2000000000L); + default : + return jjMoveNfa_23(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_23(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_23(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 41: + if ((active3 & 0x2000000000L) != 0L) + return jjStopAtPos(1, 229); + break; + case 58: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 227; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_23(active3, 0x40000000L); + default : + break; + } + return jjStartNfa_23(0, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_23(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_23(0, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_23(1, 0L, 0L, 0L, active3); + return 2; + } + switch(curChar) + { + case 58: + if ((active3 & 0x40000000L) != 0L) + return jjStopAtPos(2, 222); + break; + default : + break; + } + return jjStartNfa_23(1, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_23(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 1; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffff00002600L & l) != 0L) + kind = 228; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + kind = 228; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 228) + kind = 228; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 1 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_4(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active0 & 0x100080000000000L) != 0L || (active1 & 0x780000000000048L) != 0L) + { + jjmatchedKind = 183; + return 202; + } + if ((active0 & 0x200000000000000L) != 0L || (active1 & 0x4L) != 0L) + { + jjmatchedKind = 183; + return 140; + } + if ((active1 & 0x40000000000000L) != 0L) + { + jjmatchedKind = 183; + return 44; + } + if ((active1 & 0x400L) != 0L) + { + jjmatchedKind = 183; + return 101; + } + if ((active0 & 0x240000000000L) != 0L || (active1 & 0x800000000020L) != 0L) + { + jjmatchedKind = 183; + return 83; + } + if ((active0 & 0x20000000000L) != 0L) + { + jjmatchedKind = 183; + return 14; + } + if ((active0 & 0x10000000000L) != 0L || (active1 & 0x300L) != 0L) + { + jjmatchedKind = 183; + return 55; + } + if ((active0 & 0x80000000000000L) != 0L) + { + jjmatchedKind = 183; + return 161; + } + if ((active0 & 0x400004000000000L) != 0L || (active1 & 0x20000000000080L) != 0L) + { + jjmatchedKind = 183; + return 124; + } + if ((active0 & 0x8000000000L) != 0L) + { + jjmatchedKind = 183; + return 52; + } + return -1; + case 1: + if ((active0 & 0x8000000000L) != 0L) + return 51; + if ((active0 & 0x400000000000000L) != 0L) + { + if (jjmatchedPos != 1) + { + jjmatchedKind = 183; + jjmatchedPos = 1; + } + return 131; + } + if ((active0 & 0x200000000000L) != 0L || (active1 & 0x20L) != 0L) + return 82; + if ((active1 & 0x7e0800000000104L) != 0L) + return 202; + if ((active1 & 0x400L) != 0L) + { + if (jjmatchedPos != 1) + { + jjmatchedKind = 183; + jjmatchedPos = 1; + } + return 100; + } + if ((active1 & 0x200L) != 0L) + return 71; + if ((active0 & 0x3800f4000000000L) != 0L || (active1 & 0xc8L) != 0L) + { + if (jjmatchedPos != 1) + { + jjmatchedKind = 183; + jjmatchedPos = 1; + } + return 202; + } + return -1; + case 2: + if ((active0 & 0x780044000000000L) != 0L || (active1 & 0xe8L) != 0L) + { + jjmatchedKind = 183; + jjmatchedPos = 2; + return 202; + } + if ((active1 & 0x400L) != 0L) + { + jjmatchedKind = 183; + jjmatchedPos = 2; + return 99; + } + if ((active0 & 0xb0000000000L) != 0L) + return 202; + return -1; + case 3: + if ((active0 & 0x600040000000000L) != 0L || (active1 & 0x400L) != 0L) + return 202; + if ((active0 & 0x180004000000000L) != 0L || (active1 & 0xe8L) != 0L) + { + jjmatchedKind = 183; + jjmatchedPos = 3; + return 202; + } + return -1; + case 4: + if ((active0 & 0x180004000000000L) != 0L || (active1 & 0xa0L) != 0L) + { + jjmatchedKind = 183; + jjmatchedPos = 4; + return 202; + } + if ((active1 & 0x48L) != 0L) + return 202; + return -1; + case 5: + if ((active0 & 0x80004000000000L) != 0L || (active1 & 0x20L) != 0L) + { + jjmatchedKind = 183; + jjmatchedPos = 5; + return 202; + } + if ((active0 & 0x100000000000000L) != 0L || (active1 & 0x80L) != 0L) + return 202; + return -1; + case 6: + if ((active0 & 0x80004000000000L) != 0L || (active1 & 0x20L) != 0L) + { + jjmatchedKind = 183; + jjmatchedPos = 6; + return 202; + } + return -1; + case 7: + if ((active0 & 0x80000000000000L) != 0L || (active1 & 0x20L) != 0L) + { + jjmatchedKind = 183; + jjmatchedPos = 7; + return 202; + } + if ((active0 & 0x4000000000L) != 0L) + return 202; + return -1; + default : + return -1; + } +} +private final int jjStartNfa_4(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_4(jjStopStringLiteralDfa_4(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_4(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_4(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_4() +{ + switch(curChar) + { + case 33: + return jjMoveStringLiteralDfa1_4(0x0L, 0x1000000000000L, 0x0L); + case 36: + return jjStopAtPos(0, 49); + case 40: + jjmatchedKind = 134; + return jjMoveStringLiteralDfa1_4(0x0L, 0x0L, 0x840000000L); + case 44: + return jjStopAtPos(0, 168); + case 45: + return jjStopAtPos(0, 126); + case 58: + return jjMoveStringLiteralDfa1_4(0x0L, 0x800000000000000L, 0x0L); + case 59: + return jjStopAtPos(0, 170); + case 60: + jjmatchedKind = 124; + return jjMoveStringLiteralDfa1_4(0x0L, 0x6000000000000L, 0x0L); + case 61: + return jjStopAtPos(0, 109); + case 62: + jjmatchedKind = 125; + return jjMoveStringLiteralDfa1_4(0x0L, 0x18000000000000L, 0x0L); + case 91: + return jjStopAtPos(0, 136); + case 97: + return jjMoveStringLiteralDfa1_4(0x10000000000L, 0x300L, 0x0L); + case 99: + return jjMoveStringLiteralDfa1_4(0x0L, 0x400L, 0x0L); + case 100: + return jjMoveStringLiteralDfa1_4(0x20000000000L, 0x0L, 0x0L); + case 101: + return jjMoveStringLiteralDfa1_4(0x400004000000000L, 0x20000000000080L, 0x0L); + case 103: + return jjMoveStringLiteralDfa1_4(0x0L, 0x180000000000000L, 0x0L); + case 105: + return jjMoveStringLiteralDfa1_4(0x240000000000L, 0x800000000020L, 0x0L); + case 108: + return jjMoveStringLiteralDfa1_4(0x0L, 0x600000000000000L, 0x0L); + case 109: + return jjMoveStringLiteralDfa1_4(0x80000000000L, 0x0L, 0x0L); + case 110: + return jjMoveStringLiteralDfa1_4(0x0L, 0x40000000000000L, 0x0L); + case 111: + return jjMoveStringLiteralDfa1_4(0x8000000000L, 0x0L, 0x0L); + case 114: + return jjMoveStringLiteralDfa1_4(0x100000000000000L, 0x0L, 0x0L); + case 115: + return jjMoveStringLiteralDfa1_4(0x80000000000000L, 0x0L, 0x0L); + case 116: + return jjMoveStringLiteralDfa1_4(0x200000000000000L, 0x4L, 0x0L); + case 117: + return jjMoveStringLiteralDfa1_4(0x0L, 0x40L, 0x0L); + case 119: + return jjMoveStringLiteralDfa1_4(0x0L, 0x8L, 0x0L); + case 124: + return jjStopAtPos(0, 133); + default : + return jjMoveNfa_4(15, 0); + } +} +private final int jjMoveStringLiteralDfa1_4(long active0, long active1, long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_4(0, active0, active1, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 227; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_4(active0, 0L, active1, 0L, active3, 0x40000000L); + case 60: + if ((active1 & 0x4000000000000L) != 0L) + return jjStopAtPos(1, 114); + break; + case 61: + if ((active1 & 0x1000000000000L) != 0L) + return jjStopAtPos(1, 112); + else if ((active1 & 0x2000000000000L) != 0L) + return jjStopAtPos(1, 113); + else if ((active1 & 0x8000000000000L) != 0L) + return jjStopAtPos(1, 115); + else if ((active1 & 0x800000000000000L) != 0L) + return jjStopAtPos(1, 123); + break; + case 62: + if ((active1 & 0x10000000000000L) != 0L) + return jjStopAtPos(1, 116); + break; + case 97: + return jjMoveStringLiteralDfa2_4(active0, 0x80000000000000L, active1, 0x400L, active3, 0L); + case 100: + return jjMoveStringLiteralDfa2_4(active0, 0x40000000000L, active1, 0L, active3, 0L); + case 101: + if ((active1 & 0x40000000000000L) != 0L) + return jjStartNfaWithStates_4(1, 118, 202); + else if ((active1 & 0x100000000000000L) != 0L) + return jjStartNfaWithStates_4(1, 120, 202); + else if ((active1 & 0x400000000000000L) != 0L) + return jjStartNfaWithStates_4(1, 122, 202); + return jjMoveStringLiteralDfa2_4(active0, 0x100000000000000L, active1, 0L, active3, 0L); + case 104: + return jjMoveStringLiteralDfa2_4(active0, 0x200000000000000L, active1, 0x8L, active3, 0L); + case 105: + return jjMoveStringLiteralDfa2_4(active0, 0x20000000000L, active1, 0L, active3, 0L); + case 108: + return jjMoveStringLiteralDfa2_4(active0, 0x400000000000000L, active1, 0L, active3, 0L); + case 110: + if ((active0 & 0x200000000000L) != 0L) + { + jjmatchedKind = 45; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_4(active0, 0x10000000000L, active1, 0x60L, active3, 0L); + case 111: + if ((active1 & 0x4L) != 0L) + return jjStartNfaWithStates_4(1, 66, 202); + return jjMoveStringLiteralDfa2_4(active0, 0x80000000000L, active1, 0L, active3, 0L); + case 113: + if ((active1 & 0x20000000000000L) != 0L) + return jjStartNfaWithStates_4(1, 117, 202); + break; + case 114: + if ((active0 & 0x8000000000L) != 0L) + return jjStartNfaWithStates_4(1, 39, 51); + break; + case 115: + if ((active1 & 0x100L) != 0L) + return jjStartNfaWithStates_4(1, 72, 202); + else if ((active1 & 0x800000000000L) != 0L) + return jjStartNfaWithStates_4(1, 111, 202); + break; + case 116: + if ((active1 & 0x200L) != 0L) + return jjStartNfaWithStates_4(1, 73, 71); + else if ((active1 & 0x80000000000000L) != 0L) + return jjStartNfaWithStates_4(1, 119, 202); + else if ((active1 & 0x200000000000000L) != 0L) + return jjStartNfaWithStates_4(1, 121, 202); + break; + case 120: + return jjMoveStringLiteralDfa2_4(active0, 0x4000000000L, active1, 0x80L, active3, 0L); + default : + break; + } + return jjStartNfa_4(0, active0, active1, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_4(long old0, long active0, long old1, long active1, long old3, long active3) +{ + if (((active0 &= old0) | (active1 &= old1) | (active3 &= old3)) == 0L) + return jjStartNfa_4(0, old0, old1, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_4(1, active0, active1, 0L, active3); + return 2; + } + switch(curChar) + { + case 58: + if ((active3 & 0x40000000L) != 0L) + return jjStopAtPos(2, 222); + break; + case 99: + return jjMoveStringLiteralDfa3_4(active0, 0L, active1, 0x80L, active3, 0L); + case 100: + if ((active0 & 0x10000000000L) != 0L) + return jjStartNfaWithStates_4(2, 40, 202); + else if ((active0 & 0x80000000000L) != 0L) + return jjStartNfaWithStates_4(2, 43, 202); + break; + case 101: + return jjMoveStringLiteralDfa3_4(active0, 0x200000000000000L, active1, 0x8L, active3, 0L); + case 105: + return jjMoveStringLiteralDfa3_4(active0, 0x40000000000L, active1, 0x40L, active3, 0L); + case 115: + return jjMoveStringLiteralDfa3_4(active0, 0x400000000000000L, active1, 0x400L, active3, 0L); + case 116: + return jjMoveStringLiteralDfa3_4(active0, 0x180004000000000L, active1, 0x20L, active3, 0L); + case 118: + if ((active0 & 0x20000000000L) != 0L) + return jjStartNfaWithStates_4(2, 41, 202); + break; + default : + break; + } + return jjStartNfa_4(1, active0, active1, 0L, active3); +} +private final int jjMoveStringLiteralDfa3_4(long old0, long active0, long old1, long active1, long old3, long active3) +{ + if (((active0 &= old0) | (active1 &= old1) | (active3 &= old3)) == 0L) + return jjStartNfa_4(1, old0, old1, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_4(2, active0, active1, 0L, 0L); + return 3; + } + switch(curChar) + { + case 101: + if ((active0 & 0x400000000000000L) != 0L) + return jjStartNfaWithStates_4(3, 58, 202); + else if ((active1 & 0x400L) != 0L) + return jjStartNfaWithStates_4(3, 74, 202); + return jjMoveStringLiteralDfa4_4(active0, 0x4000000000L, active1, 0xa0L); + case 105: + return jjMoveStringLiteralDfa4_4(active0, 0x80000000000000L, active1, 0L); + case 110: + if ((active0 & 0x200000000000000L) != 0L) + return jjStartNfaWithStates_4(3, 57, 202); + break; + case 111: + return jjMoveStringLiteralDfa4_4(active0, 0L, active1, 0x40L); + case 114: + return jjMoveStringLiteralDfa4_4(active0, 0L, active1, 0x8L); + case 117: + return jjMoveStringLiteralDfa4_4(active0, 0x100000000000000L, active1, 0L); + case 118: + if ((active0 & 0x40000000000L) != 0L) + return jjStartNfaWithStates_4(3, 42, 202); + break; + default : + break; + } + return jjStartNfa_4(2, active0, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa4_4(long old0, long active0, long old1, long active1) +{ + if (((active0 &= old0) | (active1 &= old1)) == 0L) + return jjStartNfa_4(2, old0, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_4(3, active0, active1, 0L, 0L); + return 4; + } + switch(curChar) + { + case 101: + if ((active1 & 0x8L) != 0L) + return jjStartNfaWithStates_4(4, 67, 202); + break; + case 110: + if ((active1 & 0x40L) != 0L) + return jjStartNfaWithStates_4(4, 70, 202); + break; + case 112: + return jjMoveStringLiteralDfa5_4(active0, 0L, active1, 0x80L); + case 114: + return jjMoveStringLiteralDfa5_4(active0, 0x100004000000000L, active1, 0x20L); + case 115: + return jjMoveStringLiteralDfa5_4(active0, 0x80000000000000L, active1, 0L); + default : + break; + } + return jjStartNfa_4(3, active0, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa5_4(long old0, long active0, long old1, long active1) +{ + if (((active0 &= old0) | (active1 &= old1)) == 0L) + return jjStartNfa_4(3, old0, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_4(4, active0, active1, 0L, 0L); + return 5; + } + switch(curChar) + { + case 102: + return jjMoveStringLiteralDfa6_4(active0, 0x80000000000000L, active1, 0L); + case 110: + if ((active0 & 0x100000000000000L) != 0L) + return jjStartNfaWithStates_4(5, 56, 202); + return jjMoveStringLiteralDfa6_4(active0, 0x4000000000L, active1, 0L); + case 115: + return jjMoveStringLiteralDfa6_4(active0, 0L, active1, 0x20L); + case 116: + if ((active1 & 0x80L) != 0L) + return jjStartNfaWithStates_4(5, 71, 202); + break; + default : + break; + } + return jjStartNfa_4(4, active0, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa6_4(long old0, long active0, long old1, long active1) +{ + if (((active0 &= old0) | (active1 &= old1)) == 0L) + return jjStartNfa_4(4, old0, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_4(5, active0, active1, 0L, 0L); + return 6; + } + switch(curChar) + { + case 97: + return jjMoveStringLiteralDfa7_4(active0, 0x4000000000L, active1, 0L); + case 101: + return jjMoveStringLiteralDfa7_4(active0, 0L, active1, 0x20L); + case 105: + return jjMoveStringLiteralDfa7_4(active0, 0x80000000000000L, active1, 0L); + default : + break; + } + return jjStartNfa_4(5, active0, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa7_4(long old0, long active0, long old1, long active1) +{ + if (((active0 &= old0) | (active1 &= old1)) == 0L) + return jjStartNfa_4(5, old0, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_4(6, active0, active1, 0L, 0L); + return 7; + } + switch(curChar) + { + case 99: + return jjMoveStringLiteralDfa8_4(active0, 0L, active1, 0x20L); + case 101: + return jjMoveStringLiteralDfa8_4(active0, 0x80000000000000L, active1, 0L); + case 108: + if ((active0 & 0x4000000000L) != 0L) + return jjStartNfaWithStates_4(7, 38, 202); + break; + default : + break; + } + return jjStartNfa_4(6, active0, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa8_4(long old0, long active0, long old1, long active1) +{ + if (((active0 &= old0) | (active1 &= old1)) == 0L) + return jjStartNfa_4(6, old0, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_4(7, active0, active1, 0L, 0L); + return 8; + } + switch(curChar) + { + case 115: + if ((active0 & 0x80000000000000L) != 0L) + return jjStartNfaWithStates_4(8, 55, 202); + break; + case 116: + if ((active1 & 0x20L) != 0L) + return jjStartNfaWithStates_4(8, 69, 202); + break; + default : + break; + } + return jjStartNfa_4(7, active0, active1, 0L, 0L); +} +private final int jjMoveNfa_4(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 202; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 51: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 101: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 161: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 44: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 14: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 202: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 71: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if ((0x100002600L & l) != 0L) + jjCheckNAddStates(16, 18); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + else if (curChar == 39) + jjCheckNAddTwoStates(61, 62); + else if (curChar == 34) + jjCheckNAddTwoStates(58, 59); + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 99: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 124: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 83: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 131: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 55: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 82: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 52: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 140: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 15: + case 0: + if ((0x100002600L & l) == 0L) + break; + if (kind > 12) + kind = 12; + jjCheckNAdd(0); + break; + case 100: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 2: + if ((0x100002600L & l) != 0L) + jjAddStates(4, 5); + break; + case 3: + if (curChar == 40 && kind > 150) + kind = 150; + break; + case 7: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 6; + break; + case 17: + if ((0x100002600L & l) != 0L) + jjAddStates(19, 20); + break; + case 18: + if (curChar == 40 && kind > 162) + kind = 162; + break; + case 29: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 28; + break; + case 41: + if ((0x100002600L & l) != 0L) + jjAddStates(21, 22); + break; + case 42: + if (curChar == 40 && kind > 165) + kind = 165; + break; + case 47: + if ((0x100002600L & l) != 0L) + jjAddStates(23, 24); + break; + case 56: + if ((0x100002600L & l) != 0L) + jjCheckNAddStates(16, 18); + break; + case 57: + if (curChar == 34) + jjCheckNAddTwoStates(58, 59); + break; + case 58: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(58, 59); + break; + case 59: + if (curChar != 34) + break; + if (kind > 9) + kind = 9; + jjCheckNAdd(57); + break; + case 60: + if (curChar == 39) + jjCheckNAddTwoStates(61, 62); + break; + case 61: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(61, 62); + break; + case 62: + if (curChar != 39) + break; + if (kind > 9) + kind = 9; + jjCheckNAdd(60); + break; + case 64: + if ((0x100002600L & l) != 0L) + jjAddStates(25, 26); + break; + case 65: + if (curChar == 40 && kind > 158) + kind = 158; + break; + case 75: + if ((0x100002600L & l) != 0L) + jjAddStates(27, 28); + break; + case 85: + if ((0x100002600L & l) != 0L) + jjAddStates(29, 30); + break; + case 86: + if (curChar == 40) + jjCheckNAddTwoStates(87, 88); + break; + case 87: + if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(87, 88); + break; + case 88: + if (curChar == 41 && kind > 77) + kind = 77; + break; + case 93: + if ((0x100002600L & l) != 0L) + jjAddStates(31, 32); + break; + case 103: + if ((0x100002600L & l) != 0L) + jjAddStates(33, 34); + break; + case 109: + if ((0x100002600L & l) != 0L) + jjAddStates(35, 36); + break; + case 110: + if (curChar == 40 && kind > 164) + kind = 164; + break; + case 118: + if ((0x100002600L & l) != 0L) + jjAddStates(37, 38); + break; + case 119: + if (curChar == 40) + jjCheckNAddTwoStates(120, 121); + break; + case 120: + if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(120, 121); + break; + case 121: + if (curChar == 41 && kind > 96) + kind = 96; + break; + case 126: + if ((0x100002600L & l) != 0L) + jjAddStates(39, 40); + break; + case 127: + if (curChar == 40 && kind > 156) + kind = 156; + break; + case 135: + if ((0x100002600L & l) != 0L) + jjAddStates(41, 42); + break; + case 142: + if ((0x100002600L & l) != 0L) + jjAddStates(43, 44); + break; + case 143: + if (curChar == 40 && kind > 163) + kind = 163; + break; + case 148: + if ((0x100002600L & l) != 0L) + jjAddStates(45, 46); + break; + case 149: + if (curChar == 40 && kind > 159) + kind = 159; + break; + case 156: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 155; + break; + case 163: + if ((0x100002600L & l) != 0L) + jjAddStates(47, 48); + break; + case 164: + if (curChar == 40 && kind > 161) + kind = 161; + break; + case 173: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 172; + break; + case 180: + if ((0x100002600L & l) != 0L) + jjAddStates(49, 50); + break; + case 182: + if ((0x100002600L & l) != 0L) + jjAddStates(51, 52); + break; + case 194: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 195: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 196; + break; + case 197: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + break; + case 198: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(198, 199); + break; + case 199: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 200; + break; + case 201: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 51: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 50; + break; + case 101: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 114; + else if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 106; + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 100; + break; + case 161: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 191; + else if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 177; + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 160; + break; + case 44: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 43; + break; + case 14: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 13; + break; + case 202: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 71: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 70; + break; + case 99: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 103; + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 98; + break; + case 124: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 131; + else if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 123; + break; + case 83: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 89; + else if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 82; + break; + case 131: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 130; + break; + case 55: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 71; + if (curChar == 116) + jjAddStates(16, 18); + break; + case 82: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 81; + break; + case 52: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 51; + break; + case 140: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 144; + else if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 139; + break; + case 15: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAddStates(53, 58); + } + if (curChar == 115) + jjAddStates(59, 61); + else if (curChar == 116) + jjAddStates(62, 63); + else if (curChar == 101) + jjAddStates(64, 65); + else if (curChar == 99) + jjAddStates(66, 68); + else if (curChar == 105) + jjAddStates(69, 70); + else if (curChar == 97) + jjAddStates(71, 72); + else if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 52; + else if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 44; + else if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 38; + else if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 14; + break; + case 100: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 102; + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 99; + break; + case 1: + if (curChar == 101) + jjAddStates(4, 5); + break; + case 4: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 5: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 6: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 5; + break; + case 8: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 7; + break; + case 9: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 8; + break; + case 10: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 9; + break; + case 11: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 10; + break; + case 12: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 13: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 12; + break; + case 16: + if (curChar == 110) + jjAddStates(19, 20); + break; + case 19: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 16; + break; + case 20: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 19; + break; + case 21: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 20; + break; + case 22: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 21; + break; + case 23: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 22; + break; + case 24: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 23; + break; + case 25: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 24; + break; + case 26: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 25; + break; + case 27: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 26; + break; + case 28: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 27; + break; + case 30: + if (curChar == 103) + jjstateSet[jjnewStateCnt++] = 29; + break; + case 31: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 30; + break; + case 32: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 31; + break; + case 33: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 32; + break; + case 34: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 33; + break; + case 35: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 34; + break; + case 36: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 35; + break; + case 37: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 36; + break; + case 38: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 37; + break; + case 39: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 38; + break; + case 40: + if (curChar == 101) + jjAddStates(21, 22); + break; + case 43: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 40; + break; + case 45: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 44; + break; + case 46: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 47; + break; + case 48: + if (curChar == 121 && kind > 176) + kind = 176; + break; + case 49: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 48; + break; + case 50: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 46; + break; + case 53: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 52; + break; + case 54: + if (curChar == 97) + jjAddStates(71, 72); + break; + case 58: + jjAddStates(73, 74); + break; + case 61: + jjAddStates(75, 76); + break; + case 63: + if (curChar == 101) + jjAddStates(25, 26); + break; + case 66: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 63; + break; + case 67: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 66; + break; + case 68: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 67; + break; + case 69: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 68; + break; + case 70: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 69; + break; + case 72: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 71; + break; + case 73: + if (curChar == 105) + jjAddStates(69, 70); + break; + case 74: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 75; + break; + case 76: + if (curChar == 102 && kind > 75) + kind = 75; + break; + case 77: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 76; + break; + case 78: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 74; + break; + case 79: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 78; + break; + case 80: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 79; + break; + case 81: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 80; + break; + case 84: + if (curChar == 109) + jjAddStates(29, 30); + break; + case 89: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 84; + break; + case 90: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 89; + break; + case 91: + if (curChar == 99) + jjAddStates(66, 68); + break; + case 92: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 93; + break; + case 94: + if (curChar == 115 && kind > 76) + kind = 76; + break; + case 95: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 94; + break; + case 96: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 92; + break; + case 97: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 96; + break; + case 98: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 97; + break; + case 102: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 103; + break; + case 104: + if (curChar == 115 && kind > 144) + kind = 144; + break; + case 105: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 104; + break; + case 106: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 102; + break; + case 107: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 106; + break; + case 108: + if (curChar == 116) + jjAddStates(35, 36); + break; + case 111: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 108; + break; + case 112: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 111; + break; + case 113: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 112; + break; + case 114: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 113; + break; + case 115: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 114; + break; + case 116: + if (curChar == 101) + jjAddStates(64, 65); + break; + case 117: + if (curChar == 121) + jjAddStates(37, 38); + break; + case 122: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 117; + break; + case 123: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 122; + break; + case 125: + if (curChar == 116) + jjAddStates(39, 40); + break; + case 128: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 125; + break; + case 129: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 128; + break; + case 130: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 129; + break; + case 132: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 131; + break; + case 133: + if (curChar == 116) + jjAddStates(62, 63); + break; + case 134: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 135; + break; + case 136: + if (curChar == 115 && kind > 145) + kind = 145; + break; + case 137: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 136; + break; + case 138: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 134; + break; + case 139: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 138; + break; + case 141: + if (curChar == 116) + jjAddStates(43, 44); + break; + case 144: + if (curChar == 120) + jjstateSet[jjnewStateCnt++] = 141; + break; + case 145: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 144; + break; + case 146: + if (curChar == 115) + jjAddStates(59, 61); + break; + case 147: + if (curChar == 116) + jjAddStates(45, 46); + break; + case 150: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 147; + break; + case 151: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 150; + break; + case 152: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 151; + break; + case 153: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 152; + break; + case 154: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 153; + break; + case 155: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 154; + break; + case 157: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 156; + break; + case 158: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 157; + break; + case 159: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 158; + break; + case 160: + if (curChar == 104) + jjstateSet[jjnewStateCnt++] = 159; + break; + case 162: + if (curChar == 101) + jjAddStates(47, 48); + break; + case 165: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 162; + break; + case 166: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 165; + break; + case 167: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 166; + break; + case 168: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 167; + break; + case 169: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 168; + break; + case 170: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 169; + break; + case 171: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 170; + break; + case 172: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 171; + break; + case 174: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 173; + break; + case 175: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 174; + break; + case 176: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 175; + break; + case 177: + if (curChar == 104) + jjstateSet[jjnewStateCnt++] = 176; + break; + case 178: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 177; + break; + case 179: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 180; + break; + case 181: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 182; + break; + case 183: + if (curChar == 121 && kind > 177) + kind = 177; + break; + case 184: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 183; + break; + case 185: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 181; + break; + case 186: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 185; + break; + case 187: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 186; + break; + case 188: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 187; + break; + case 189: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 179; + break; + case 190: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 189; + break; + case 191: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 190; + break; + case 192: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 191; + break; + case 193: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 183) + kind = 183; + jjCheckNAddStates(53, 58); + break; + case 194: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(194, 195); + break; + case 196: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + break; + case 197: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + break; + case 198: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(198, 199); + break; + case 200: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + break; + case 201: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 51: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 101: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 161: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 44: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 14: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 202: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 71: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 99: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 124: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 83: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 131: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 55: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 82: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 52: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 140: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 15: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 183) + kind = 183; + jjCheckNAddStates(53, 58); + break; + case 100: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + } + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + } + break; + case 58: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(73, 74); + break; + case 61: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(75, 76); + break; + case 194: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(194, 195); + break; + case 196: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + break; + case 197: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 183) + kind = 183; + jjCheckNAdd(197); + break; + case 198: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(198, 199); + break; + case 200: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + break; + case 201: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 184) + kind = 184; + jjCheckNAdd(201); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 202 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjMoveStringLiteralDfa0_18() +{ + return jjMoveNfa_18(1, 0); +} +private final int jjMoveNfa_18(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 6; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + case 0: + if ((0x100002600L & l) == 0L) + break; + kind = 239; + jjCheckNAdd(0); + break; + case 2: + if ((0x3ff600000000000L & l) != 0L) + jjAddStates(4, 5); + break; + case 3: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 5: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 186) + kind = 186; + jjstateSet[jjnewStateCnt++] = 5; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 186) + kind = 186; + jjCheckNAddStates(6, 8); + break; + case 2: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + case 5: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 186) + kind = 186; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 186) + kind = 186; + jjCheckNAddStates(6, 8); + break; + case 2: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 186) + kind = 186; + jjCheckNAdd(5); + break; + case 5: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 186) + kind = 186; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 6 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_13(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active3 & 0x8000L) != 0L) + { + jjmatchedKind = 210; + return -1; + } + return -1; + case 1: + if ((active3 & 0x8000L) != 0L) + { + if (jjmatchedPos == 0) + { + jjmatchedKind = 210; + jjmatchedPos = 0; + } + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_13(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_13(jjStopStringLiteralDfa_13(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_13(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_13(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_13() +{ + switch(curChar) + { + case 60: + jjmatchedKind = 196; + return jjMoveStringLiteralDfa1_13(0x10000L, 0x8000000000000000L, 0x20000000100L); + case 123: + jjmatchedKind = 204; + return jjMoveStringLiteralDfa1_13(0x0L, 0x0L, 0x4000L); + case 125: + return jjMoveStringLiteralDfa1_13(0x0L, 0x0L, 0x8000L); + default : + return jjMoveNfa_13(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_13(long active0, long active2, long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_13(0, active0, 0L, active2, active3); + return 1; + } + switch(curChar) + { + case 33: + return jjMoveStringLiteralDfa2_13(active0, 0L, active2, 0x8000000000000000L, active3, 0x20000000000L); + case 47: + if ((active3 & 0x100L) != 0L) + return jjStopAtPos(1, 200); + break; + case 63: + if ((active0 & 0x10000L) != 0L) + return jjStopAtPos(1, 16); + break; + case 123: + if ((active3 & 0x4000L) != 0L) + return jjStopAtPos(1, 206); + break; + case 125: + if ((active3 & 0x8000L) != 0L) + return jjStopAtPos(1, 207); + break; + default : + break; + } + return jjStartNfa_13(0, active0, 0L, active2, active3); +} +private final int jjMoveStringLiteralDfa2_13(long old0, long active0, long old2, long active2, long old3, long active3) +{ + if (((active0 &= old0) | (active2 &= old2) | (active3 &= old3)) == 0L) + return jjStartNfa_13(0, old0, 0L, old2, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_13(1, 0L, 0L, active2, active3); + return 2; + } + switch(curChar) + { + case 45: + return jjMoveStringLiteralDfa3_13(active2, 0L, active3, 0x20000000000L); + case 91: + return jjMoveStringLiteralDfa3_13(active2, 0x8000000000000000L, active3, 0L); + default : + break; + } + return jjStartNfa_13(1, 0L, 0L, active2, active3); +} +private final int jjMoveStringLiteralDfa3_13(long old2, long active2, long old3, long active3) +{ + if (((active2 &= old2) | (active3 &= old3)) == 0L) + return jjStartNfa_13(1, 0L, 0L, old2, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_13(2, 0L, 0L, active2, active3); + return 3; + } + switch(curChar) + { + case 45: + if ((active3 & 0x20000000000L) != 0L) + return jjStopAtPos(3, 233); + break; + case 67: + return jjMoveStringLiteralDfa4_13(active2, 0x8000000000000000L, active3, 0L); + default : + break; + } + return jjStartNfa_13(2, 0L, 0L, active2, active3); +} +private final int jjMoveStringLiteralDfa4_13(long old2, long active2, long old3, long active3) +{ + if (((active2 &= old2) | (active3 &= old3)) == 0L) + return jjStartNfa_13(2, 0L, 0L, old2, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_13(3, 0L, 0L, active2, 0L); + return 4; + } + switch(curChar) + { + case 68: + return jjMoveStringLiteralDfa5_13(active2, 0x8000000000000000L); + default : + break; + } + return jjStartNfa_13(3, 0L, 0L, active2, 0L); +} +private final int jjMoveStringLiteralDfa5_13(long old2, long active2) +{ + if (((active2 &= old2)) == 0L) + return jjStartNfa_13(3, 0L, 0L, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_13(4, 0L, 0L, active2, 0L); + return 5; + } + switch(curChar) + { + case 65: + return jjMoveStringLiteralDfa6_13(active2, 0x8000000000000000L); + default : + break; + } + return jjStartNfa_13(4, 0L, 0L, active2, 0L); +} +private final int jjMoveStringLiteralDfa6_13(long old2, long active2) +{ + if (((active2 &= old2)) == 0L) + return jjStartNfa_13(4, 0L, 0L, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_13(5, 0L, 0L, active2, 0L); + return 6; + } + switch(curChar) + { + case 84: + return jjMoveStringLiteralDfa7_13(active2, 0x8000000000000000L); + default : + break; + } + return jjStartNfa_13(5, 0L, 0L, active2, 0L); +} +private final int jjMoveStringLiteralDfa7_13(long old2, long active2) +{ + if (((active2 &= old2)) == 0L) + return jjStartNfa_13(5, 0L, 0L, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_13(6, 0L, 0L, active2, 0L); + return 7; + } + switch(curChar) + { + case 65: + return jjMoveStringLiteralDfa8_13(active2, 0x8000000000000000L); + default : + break; + } + return jjStartNfa_13(6, 0L, 0L, active2, 0L); +} +private final int jjMoveStringLiteralDfa8_13(long old2, long active2) +{ + if (((active2 &= old2)) == 0L) + return jjStartNfa_13(6, 0L, 0L, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_13(7, 0L, 0L, active2, 0L); + return 8; + } + switch(curChar) + { + case 91: + if ((active2 & 0x8000000000000000L) != 0L) + return jjStopAtPos(8, 191); + break; + default : + break; + } + return jjStartNfa_13(7, 0L, 0L, active2, 0L); +} +private final int jjMoveNfa_13(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 21; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffff00002600L & l) != 0L) + { + if (kind > 210) + kind = 210; + } + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 14; + if (curChar == 38) + jjAddStates(77, 80); + break; + case 2: + if (curChar == 59 && kind > 193) + kind = 193; + break; + case 14: + if (curChar == 35) + jjCheckNAddTwoStates(15, 17); + break; + case 15: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(15, 16); + break; + case 16: + if (curChar == 59 && kind > 194) + kind = 194; + break; + case 18: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(18, 16); + break; + case 19: + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 14; + break; + case 20: + if ((0xffffffff00002600L & l) != 0L && kind > 210) + kind = 210; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 210) + kind = 210; + break; + case 1: + if (curChar == 116) + jjCheckNAdd(2); + break; + case 3: + if (curChar == 108) + jjCheckNAdd(1); + break; + case 4: + if (curChar == 103) + jjCheckNAdd(1); + break; + case 5: + if (curChar == 111) + jjCheckNAdd(1); + break; + case 6: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 5; + break; + case 7: + if (curChar == 113) + jjstateSet[jjnewStateCnt++] = 6; + break; + case 8: + if (curChar == 97) + jjAddStates(81, 82); + break; + case 9: + if (curChar == 112) + jjCheckNAdd(2); + break; + case 10: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 9; + break; + case 11: + if (curChar == 115) + jjCheckNAdd(2); + break; + case 12: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 13: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 12; + break; + case 17: + if (curChar == 120) + jjCheckNAdd(18); + break; + case 18: + if ((0x7e0000007eL & l) != 0L) + jjCheckNAddTwoStates(18, 16); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 210) + kind = 210; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 21 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active0 & 0x80000000000000L) != 0L) + { + jjmatchedKind = 249; + return 57; + } + if ((active0 & 0x240000000000L) != 0L || (active1 & 0x800000000020L) != 0L) + { + jjmatchedKind = 249; + return 24; + } + if ((active1 & 0x600000000000000L) != 0L) + { + jjmatchedKind = 249; + return 34; + } + if ((active1 & 0x410L) != 0L) + { + jjmatchedKind = 249; + return 82; + } + if ((active0 & 0x400004000000000L) != 0L || (active1 & 0x20000000000080L) != 0L) + { + jjmatchedKind = 249; + return 102; + } + if ((active0 & 0x9000b3200000000L) != 0L || (active1 & 0x1c0000000000348L) != 0L || (active2 & 0xc000000000000L) != 0L) + { + jjmatchedKind = 249; + return 52; + } + if ((active0 & 0x200000000000000L) != 0L || (active1 & 0x4L) != 0L) + { + jjmatchedKind = 249; + return 42; + } + if ((active0 & 0x8100000000L) != 0L) + { + jjmatchedKind = 249; + return 50; + } + return -1; + case 1: + if ((active0 & 0x8100000000L) != 0L) + return 49; + if ((active0 & 0x200000000000L) != 0L || (active1 & 0x20L) != 0L) + return 23; + if ((active1 & 0x400000000000000L) != 0L) + return 31; + if ((active0 & 0x2000000000L) != 0L || (active1 & 0x3e0800000000304L) != 0L || (active2 & 0x4000000000000L) != 0L) + return 52; + if ((active1 & 0x400L) != 0L) + { + if (jjmatchedPos != 1) + { + jjmatchedKind = 249; + jjmatchedPos = 1; + } + return 81; + } + if ((active0 & 0xf800f5200000000L) != 0L || (active1 & 0xd8L) != 0L || (active2 & 0x8000000000000L) != 0L) + { + if (jjmatchedPos != 1) + { + jjmatchedKind = 249; + jjmatchedPos = 1; + } + return 52; + } + return -1; + case 2: + if ((active0 & 0xf80044200000000L) != 0L || (active1 & 0xf8L) != 0L || (active2 & 0xc000000000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 2; + return 52; + } + if ((active1 & 0x400L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 2; + return 80; + } + if ((active0 & 0xb1000000000L) != 0L) + return 52; + if ((active0 & 0x100000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 2; + return 48; + } + return -1; + case 3: + if ((active0 & 0x980004200000000L) != 0L || (active1 & 0xf8L) != 0L || (active2 & 0xc000000000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 3; + return 52; + } + if ((active0 & 0x100000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 3; + return 44; + } + if ((active0 & 0x600040000000000L) != 0L || (active1 & 0x400L) != 0L) + return 52; + return -1; + case 4: + if ((active0 & 0x100000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 4; + return 113; + } + if ((active1 & 0x48L) != 0L) + return 52; + if ((active0 & 0x980004200000000L) != 0L || (active1 & 0xb0L) != 0L || (active2 & 0xc000000000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 4; + return 52; + } + return -1; + case 5: + if ((active0 & 0x880004300000000L) != 0L || (active1 & 0x30L) != 0L || (active2 & 0xc000000000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 5; + return 52; + } + if ((active0 & 0x100000000000000L) != 0L || (active1 & 0x80L) != 0L) + return 52; + return -1; + case 6: + if ((active0 & 0x80004200000000L) != 0L || (active1 & 0x30L) != 0L || (active2 & 0xc000000000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 6; + return 52; + } + if ((active0 & 0x800000100000000L) != 0L) + return 52; + return -1; + case 7: + if ((active0 & 0x4000000000L) != 0L) + return 52; + if ((active0 & 0x80000200000000L) != 0L || (active1 & 0x30L) != 0L || (active2 & 0xc000000000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 7; + return 52; + } + return -1; + case 8: + if ((active2 & 0x8000000000000L) != 0L) + { + jjmatchedKind = 249; + jjmatchedPos = 8; + return 52; + } + if ((active0 & 0x80000200000000L) != 0L || (active1 & 0x30L) != 0L || (active2 & 0x4000000000000L) != 0L) + return 52; + return -1; + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 33: + return jjMoveStringLiteralDfa1_1(0x0L, 0x1000000000000L, 0x0L, 0x0L); + case 36: + return jjStopAtPos(0, 49); + case 37: + return jjMoveStringLiteralDfa1_1(0x0L, 0x0L, 0x80000000000L, 0x0L); + case 40: + return jjMoveStringLiteralDfa1_1(0x0L, 0x0L, 0x0L, 0x840000000L); + case 41: + return jjStopAtPos(0, 138); + case 42: + return jjStopAtPos(0, 44); + case 43: + return jjStopAtPos(0, 127); + case 44: + return jjStopAtPos(0, 168); + case 45: + return jjStopAtPos(0, 126); + case 47: + jjmatchedKind = 107; + return jjMoveStringLiteralDfa1_1(0x0L, 0x100000000000L, 0x0L, 0x0L); + case 58: + return jjMoveStringLiteralDfa1_1(0x0L, 0x800000000000000L, 0x0L, 0x0L); + case 59: + return jjStopAtPos(0, 170); + case 60: + jjmatchedKind = 124; + return jjMoveStringLiteralDfa1_1(0x0L, 0x6000000000000L, 0x0L, 0x0L); + case 61: + return jjStopAtPos(0, 109); + case 62: + jjmatchedKind = 125; + return jjMoveStringLiteralDfa1_1(0x0L, 0x18000000000000L, 0x0L, 0x0L); + case 63: + return jjStopAtPos(0, 130); + case 91: + return jjStopAtPos(0, 136); + case 93: + return jjStopAtPos(0, 137); + case 97: + return jjMoveStringLiteralDfa1_1(0x10000000000L, 0x300L, 0x4000000000000L, 0x0L); + case 99: + return jjMoveStringLiteralDfa1_1(0x0L, 0x410L, 0x0L, 0x0L); + case 100: + return jjMoveStringLiteralDfa1_1(0x800020000000000L, 0x0L, 0x8000000000000L, 0x0L); + case 101: + return jjMoveStringLiteralDfa1_1(0x400004000000000L, 0x20000000000080L, 0x0L, 0x0L); + case 103: + return jjMoveStringLiteralDfa1_1(0x0L, 0x180000000000000L, 0x0L, 0x0L); + case 105: + return jjMoveStringLiteralDfa1_1(0x240000000000L, 0x800000000020L, 0x0L, 0x0L); + case 108: + return jjMoveStringLiteralDfa1_1(0x0L, 0x600000000000000L, 0x0L, 0x0L); + case 109: + return jjMoveStringLiteralDfa1_1(0x80000000000L, 0x0L, 0x0L, 0x0L); + case 110: + return jjMoveStringLiteralDfa1_1(0x2000000000L, 0x40000000000000L, 0x0L, 0x0L); + case 111: + return jjMoveStringLiteralDfa1_1(0x8100000000L, 0x0L, 0x0L, 0x0L); + case 114: + return jjMoveStringLiteralDfa1_1(0x100000000000000L, 0x0L, 0x0L, 0x0L); + case 115: + return jjMoveStringLiteralDfa1_1(0x80000000000000L, 0x0L, 0x0L, 0x0L); + case 116: + return jjMoveStringLiteralDfa1_1(0x200000000000000L, 0x4L, 0x0L, 0x0L); + case 117: + return jjMoveStringLiteralDfa1_1(0x200000000L, 0x40L, 0x0L, 0x0L); + case 119: + return jjMoveStringLiteralDfa1_1(0x0L, 0x8L, 0x0L, 0x0L); + case 121: + return jjMoveStringLiteralDfa1_1(0x1000000000L, 0x0L, 0x0L, 0x0L); + case 123: + return jjStopAtPos(0, 205); + case 124: + return jjStopAtPos(0, 133); + case 125: + return jjStopAtPos(0, 241); + default : + return jjMoveNfa_1(14, 0); + } +} +private final int jjMoveStringLiteralDfa1_1(long active0, long active1, long active2, long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(0, active0, active1, active2, active3); + return 1; + } + switch(curChar) + { + case 37: + return jjMoveStringLiteralDfa2_1(active0, 0L, active1, 0L, active2, 0x80000000000L, active3, 0L); + case 47: + if ((active1 & 0x100000000000L) != 0L) + return jjStopAtPos(1, 108); + break; + case 58: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 227; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_1(active0, 0L, active1, 0L, active2, 0L, active3, 0x40000000L); + case 60: + if ((active1 & 0x4000000000000L) != 0L) + return jjStopAtPos(1, 114); + break; + case 61: + if ((active1 & 0x1000000000000L) != 0L) + return jjStopAtPos(1, 112); + else if ((active1 & 0x2000000000000L) != 0L) + return jjStopAtPos(1, 113); + else if ((active1 & 0x8000000000000L) != 0L) + return jjStopAtPos(1, 115); + else if ((active1 & 0x800000000000000L) != 0L) + return jjStopAtPos(1, 123); + break; + case 62: + if ((active1 & 0x10000000000000L) != 0L) + return jjStopAtPos(1, 116); + break; + case 97: + return jjMoveStringLiteralDfa2_1(active0, 0x80000000000000L, active1, 0x400L, active2, 0L, active3, 0L); + case 100: + return jjMoveStringLiteralDfa2_1(active0, 0x40000000000L, active1, 0L, active2, 0L, active3, 0L); + case 101: + if ((active1 & 0x40000000000000L) != 0L) + return jjStartNfaWithStates_1(1, 118, 52); + else if ((active1 & 0x100000000000000L) != 0L) + return jjStartNfaWithStates_1(1, 120, 52); + else if ((active1 & 0x400000000000000L) != 0L) + return jjStartNfaWithStates_1(1, 122, 31); + return jjMoveStringLiteralDfa2_1(active0, 0x900001000000000L, active1, 0L, active2, 0x8000000000000L, active3, 0L); + case 104: + return jjMoveStringLiteralDfa2_1(active0, 0x200000000000000L, active1, 0x8L, active2, 0L, active3, 0L); + case 105: + return jjMoveStringLiteralDfa2_1(active0, 0x20000000000L, active1, 0L, active2, 0L, active3, 0L); + case 108: + return jjMoveStringLiteralDfa2_1(active0, 0x400000000000000L, active1, 0L, active2, 0L, active3, 0L); + case 110: + if ((active0 & 0x200000000000L) != 0L) + { + jjmatchedKind = 45; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_1(active0, 0x10200000000L, active1, 0x60L, active2, 0L, active3, 0L); + case 111: + if ((active0 & 0x2000000000L) != 0L) + return jjStartNfaWithStates_1(1, 37, 52); + else if ((active1 & 0x4L) != 0L) + return jjStartNfaWithStates_1(1, 66, 52); + return jjMoveStringLiteralDfa2_1(active0, 0x80000000000L, active1, 0x10L, active2, 0L, active3, 0L); + case 113: + if ((active1 & 0x20000000000000L) != 0L) + return jjStartNfaWithStates_1(1, 117, 52); + break; + case 114: + if ((active0 & 0x8000000000L) != 0L) + { + jjmatchedKind = 39; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_1(active0, 0x100000000L, active1, 0L, active2, 0L, active3, 0L); + case 115: + if ((active1 & 0x100L) != 0L) + { + jjmatchedKind = 72; + jjmatchedPos = 1; + } + else if ((active1 & 0x800000000000L) != 0L) + return jjStartNfaWithStates_1(1, 111, 52); + return jjMoveStringLiteralDfa2_1(active0, 0L, active1, 0L, active2, 0x4000000000000L, active3, 0L); + case 116: + if ((active1 & 0x200L) != 0L) + return jjStartNfaWithStates_1(1, 73, 52); + else if ((active1 & 0x80000000000000L) != 0L) + return jjStartNfaWithStates_1(1, 119, 52); + else if ((active1 & 0x200000000000000L) != 0L) + return jjStartNfaWithStates_1(1, 121, 52); + break; + case 120: + return jjMoveStringLiteralDfa2_1(active0, 0x4000000000L, active1, 0x80L, active2, 0L, active3, 0L); + default : + break; + } + return jjStartNfa_1(0, active0, active1, active2, active3); +} +private final int jjMoveStringLiteralDfa2_1(long old0, long active0, long old1, long active1, long old2, long active2, long old3, long active3) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2) | (active3 &= old3)) == 0L) + return jjStartNfa_1(0, old0, old1, old2, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(1, active0, active1, active2, active3); + return 2; + } + switch(curChar) + { + case 37: + if ((active2 & 0x80000000000L) != 0L) + return jjStopAtPos(2, 171); + break; + case 58: + if ((active3 & 0x40000000L) != 0L) + return jjStopAtPos(2, 222); + break; + case 99: + return jjMoveStringLiteralDfa3_1(active0, 0L, active1, 0x80L, active2, 0x4000000000000L, active3, 0L); + case 100: + if ((active0 & 0x10000000000L) != 0L) + return jjStartNfaWithStates_1(2, 40, 52); + else if ((active0 & 0x80000000000L) != 0L) + return jjStartNfaWithStates_1(2, 43, 52); + return jjMoveStringLiteralDfa3_1(active0, 0x100000000L, active1, 0L, active2, 0L, active3, 0L); + case 101: + return jjMoveStringLiteralDfa3_1(active0, 0x200000000000000L, active1, 0x8L, active2, 0L, active3, 0L); + case 102: + return jjMoveStringLiteralDfa3_1(active0, 0x800000000000000L, active1, 0L, active2, 0L, active3, 0L); + case 105: + return jjMoveStringLiteralDfa3_1(active0, 0x40000000000L, active1, 0x40L, active2, 0L, active3, 0L); + case 108: + return jjMoveStringLiteralDfa3_1(active0, 0L, active1, 0x10L, active2, 0L, active3, 0L); + case 111: + return jjMoveStringLiteralDfa3_1(active0, 0x200000000L, active1, 0L, active2, 0L, active3, 0L); + case 115: + if ((active0 & 0x1000000000L) != 0L) + return jjStartNfaWithStates_1(2, 36, 52); + return jjMoveStringLiteralDfa3_1(active0, 0x400000000000000L, active1, 0x400L, active2, 0x8000000000000L, active3, 0L); + case 116: + return jjMoveStringLiteralDfa3_1(active0, 0x180004000000000L, active1, 0x20L, active2, 0L, active3, 0L); + case 118: + if ((active0 & 0x20000000000L) != 0L) + return jjStartNfaWithStates_1(2, 41, 52); + break; + default : + break; + } + return jjStartNfa_1(1, active0, active1, active2, active3); +} +private final int jjMoveStringLiteralDfa3_1(long old0, long active0, long old1, long active1, long old2, long active2, long old3, long active3) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2) | (active3 &= old3)) == 0L) + return jjStartNfa_1(1, old0, old1, old2, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(2, active0, active1, active2, 0L); + return 3; + } + switch(curChar) + { + case 97: + return jjMoveStringLiteralDfa4_1(active0, 0x800000000000000L, active1, 0L, active2, 0L); + case 99: + return jjMoveStringLiteralDfa4_1(active0, 0L, active1, 0L, active2, 0x8000000000000L); + case 101: + if ((active0 & 0x400000000000000L) != 0L) + return jjStartNfaWithStates_1(3, 58, 52); + else if ((active1 & 0x400L) != 0L) + return jjStartNfaWithStates_1(3, 74, 52); + return jjMoveStringLiteralDfa4_1(active0, 0x4100000000L, active1, 0xa0L, active2, 0x4000000000000L); + case 105: + return jjMoveStringLiteralDfa4_1(active0, 0x80000000000000L, active1, 0L, active2, 0L); + case 108: + return jjMoveStringLiteralDfa4_1(active0, 0L, active1, 0x10L, active2, 0L); + case 110: + if ((active0 & 0x200000000000000L) != 0L) + return jjStartNfaWithStates_1(3, 57, 52); + break; + case 111: + return jjMoveStringLiteralDfa4_1(active0, 0L, active1, 0x40L, active2, 0L); + case 114: + return jjMoveStringLiteralDfa4_1(active0, 0x200000000L, active1, 0x8L, active2, 0L); + case 117: + return jjMoveStringLiteralDfa4_1(active0, 0x100000000000000L, active1, 0L, active2, 0L); + case 118: + if ((active0 & 0x40000000000L) != 0L) + return jjStartNfaWithStates_1(3, 42, 52); + break; + default : + break; + } + return jjStartNfa_1(2, active0, active1, active2, 0L); +} +private final int jjMoveStringLiteralDfa4_1(long old0, long active0, long old1, long active1, long old2, long active2) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2)) == 0L) + return jjStartNfa_1(2, old0, old1, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(3, active0, active1, active2, 0L); + return 4; + } + switch(curChar) + { + case 97: + return jjMoveStringLiteralDfa5_1(active0, 0L, active1, 0x10L, active2, 0L); + case 100: + return jjMoveStringLiteralDfa5_1(active0, 0x200000000L, active1, 0L, active2, 0L); + case 101: + if ((active1 & 0x8L) != 0L) + return jjStartNfaWithStates_1(4, 67, 52); + return jjMoveStringLiteralDfa5_1(active0, 0L, active1, 0L, active2, 0x8000000000000L); + case 110: + if ((active1 & 0x40L) != 0L) + return jjStartNfaWithStates_1(4, 70, 52); + return jjMoveStringLiteralDfa5_1(active0, 0L, active1, 0L, active2, 0x4000000000000L); + case 112: + return jjMoveStringLiteralDfa5_1(active0, 0L, active1, 0x80L, active2, 0L); + case 114: + return jjMoveStringLiteralDfa5_1(active0, 0x100004100000000L, active1, 0x20L, active2, 0L); + case 115: + return jjMoveStringLiteralDfa5_1(active0, 0x80000000000000L, active1, 0L, active2, 0L); + case 117: + return jjMoveStringLiteralDfa5_1(active0, 0x800000000000000L, active1, 0L, active2, 0L); + default : + break; + } + return jjStartNfa_1(3, active0, active1, active2, 0L); +} +private final int jjMoveStringLiteralDfa5_1(long old0, long active0, long old1, long active1, long old2, long active2) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2)) == 0L) + return jjStartNfa_1(3, old0, old1, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(4, active0, active1, active2, 0L); + return 5; + } + switch(curChar) + { + case 100: + return jjMoveStringLiteralDfa6_1(active0, 0L, active1, 0L, active2, 0x4000000000000L); + case 101: + return jjMoveStringLiteralDfa6_1(active0, 0x300000000L, active1, 0L, active2, 0L); + case 102: + return jjMoveStringLiteralDfa6_1(active0, 0x80000000000000L, active1, 0L, active2, 0L); + case 108: + return jjMoveStringLiteralDfa6_1(active0, 0x800000000000000L, active1, 0L, active2, 0L); + case 110: + if ((active0 & 0x100000000000000L) != 0L) + return jjStartNfaWithStates_1(5, 56, 52); + return jjMoveStringLiteralDfa6_1(active0, 0x4000000000L, active1, 0L, active2, 0x8000000000000L); + case 115: + return jjMoveStringLiteralDfa6_1(active0, 0L, active1, 0x20L, active2, 0L); + case 116: + if ((active1 & 0x80L) != 0L) + return jjStartNfaWithStates_1(5, 71, 52); + return jjMoveStringLiteralDfa6_1(active0, 0L, active1, 0x10L, active2, 0L); + default : + break; + } + return jjStartNfa_1(4, active0, active1, active2, 0L); +} +private final int jjMoveStringLiteralDfa6_1(long old0, long active0, long old1, long active1, long old2, long active2) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2)) == 0L) + return jjStartNfa_1(4, old0, old1, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(5, active0, active1, active2, 0L); + return 6; + } + switch(curChar) + { + case 97: + return jjMoveStringLiteralDfa7_1(active0, 0x4000000000L, active1, 0L, active2, 0L); + case 100: + if ((active0 & 0x100000000L) != 0L) + return jjStartNfaWithStates_1(6, 32, 52); + return jjMoveStringLiteralDfa7_1(active0, 0L, active1, 0L, active2, 0x8000000000000L); + case 101: + return jjMoveStringLiteralDfa7_1(active0, 0L, active1, 0x20L, active2, 0L); + case 105: + return jjMoveStringLiteralDfa7_1(active0, 0x80000000000000L, active1, 0x10L, active2, 0x4000000000000L); + case 114: + return jjMoveStringLiteralDfa7_1(active0, 0x200000000L, active1, 0L, active2, 0L); + case 116: + if ((active0 & 0x800000000000000L) != 0L) + return jjStartNfaWithStates_1(6, 59, 52); + break; + default : + break; + } + return jjStartNfa_1(5, active0, active1, active2, 0L); +} +private final int jjMoveStringLiteralDfa7_1(long old0, long active0, long old1, long active1, long old2, long active2) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2)) == 0L) + return jjStartNfa_1(5, old0, old1, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(6, active0, active1, active2, 0L); + return 7; + } + switch(curChar) + { + case 99: + return jjMoveStringLiteralDfa8_1(active0, 0L, active1, 0x20L, active2, 0L); + case 101: + return jjMoveStringLiteralDfa8_1(active0, 0x80000200000000L, active1, 0L, active2, 0L); + case 105: + return jjMoveStringLiteralDfa8_1(active0, 0L, active1, 0L, active2, 0x8000000000000L); + case 108: + if ((active0 & 0x4000000000L) != 0L) + return jjStartNfaWithStates_1(7, 38, 52); + break; + case 110: + return jjMoveStringLiteralDfa8_1(active0, 0L, active1, 0L, active2, 0x4000000000000L); + case 111: + return jjMoveStringLiteralDfa8_1(active0, 0L, active1, 0x10L, active2, 0L); + default : + break; + } + return jjStartNfa_1(6, active0, active1, active2, 0L); +} +private final int jjMoveStringLiteralDfa8_1(long old0, long active0, long old1, long active1, long old2, long active2) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2)) == 0L) + return jjStartNfa_1(6, old0, old1, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(7, active0, active1, active2, 0L); + return 8; + } + switch(curChar) + { + case 100: + if ((active0 & 0x200000000L) != 0L) + return jjStartNfaWithStates_1(8, 33, 52); + break; + case 103: + if ((active2 & 0x4000000000000L) != 0L) + return jjStartNfaWithStates_1(8, 178, 52); + break; + case 110: + if ((active1 & 0x10L) != 0L) + return jjStartNfaWithStates_1(8, 68, 52); + return jjMoveStringLiteralDfa9_1(active0, 0L, active1, 0L, active2, 0x8000000000000L); + case 115: + if ((active0 & 0x80000000000000L) != 0L) + return jjStartNfaWithStates_1(8, 55, 52); + break; + case 116: + if ((active1 & 0x20L) != 0L) + return jjStartNfaWithStates_1(8, 69, 52); + break; + default : + break; + } + return jjStartNfa_1(7, active0, active1, active2, 0L); +} +private final int jjMoveStringLiteralDfa9_1(long old0, long active0, long old1, long active1, long old2, long active2) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2)) == 0L) + return jjStartNfa_1(7, old0, old1, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_1(8, 0L, 0L, active2, 0L); + return 9; + } + switch(curChar) + { + case 103: + if ((active2 & 0x8000000000000L) != 0L) + return jjStartNfaWithStates_1(9, 179, 52); + break; + default : + break; + } + return jjStartNfa_1(8, 0L, 0L, active2, 0L); +} +private final int jjMoveNfa_1(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 113; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 44: + case 52: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 81: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 24: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 49: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 42: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 113: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + else if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(45, 47); + break; + case 31: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 57: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 34: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 80: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 23: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 14: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + else if ((0x100002600L & l) != 0L) + { + if (kind > 12) + kind = 12; + jjCheckNAdd(6); + } + else if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + else if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 48: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 50: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 82: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 102: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 0: + if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 1: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(1, 2); + break; + case 2: + if (curChar != 34) + break; + if (kind > 4) + kind = 4; + jjstateSet[jjnewStateCnt++] = 0; + break; + case 3: + if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + break; + case 4: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(4, 5); + break; + case 5: + if (curChar != 39) + break; + if (kind > 4) + kind = 4; + jjstateSet[jjnewStateCnt++] = 3; + break; + case 6: + if ((0x100002600L & l) == 0L) + break; + if (kind > 12) + kind = 12; + jjCheckNAdd(6); + break; + case 16: + if ((0x100002600L & l) != 0L) + jjAddStates(83, 84); + break; + case 27: + if ((0x100002600L & l) != 0L) + jjAddStates(11, 12); + break; + case 28: + if (curChar == 36 && kind > 142) + kind = 142; + break; + case 32: + if ((0x100002600L & l) != 0L) + jjAddStates(85, 86); + break; + case 33: + if (curChar == 36 && kind > 143) + kind = 143; + break; + case 37: + if ((0x100002600L & l) != 0L) + jjAddStates(87, 88); + break; + case 45: + if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(45, 47); + break; + case 59: + if ((0x100002600L & l) != 0L) + jjAddStates(89, 90); + break; + case 61: + if ((0x100002600L & l) != 0L) + jjAddStates(91, 92); + break; + case 74: + if ((0x100002600L & l) != 0L) + jjAddStates(93, 94); + break; + case 84: + if ((0x100002600L & l) != 0L) + jjAddStates(95, 96); + break; + case 91: + if ((0x100002600L & l) != 0L) + jjAddStates(97, 98); + break; + case 104: + if ((0x100002600L & l) != 0L) + jjAddStates(99, 100); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 44: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 45; + break; + case 81: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 83; + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 80; + break; + case 24: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 23; + break; + case 49: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 48; + break; + case 42: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 41; + break; + case 113: + case 52: + if ((0x7fffffe07fffffeL & l) == 0L) + break; + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + break; + case 31: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 116) + jjAddStates(85, 86); + break; + case 57: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 70; + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 56; + break; + case 34: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 31; + break; + case 80: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 84; + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 79; + break; + case 23: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 22; + break; + case 14: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 101) + jjAddStates(101, 102); + else if (curChar == 99) + jjAddStates(103, 104); + else if (curChar == 115) + jjAddStates(105, 106); + else if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 50; + else if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 42; + else if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 34; + else if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 29; + else if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 24; + else if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 13; + break; + case 48: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 44; + break; + case 50: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 49; + break; + case 82: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 87; + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 81; + break; + case 102: + if ((0x7fffffe07fffffeL & l) != 0L) + { + if (kind > 249) + kind = 249; + jjCheckNAdd(52); + } + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 111; + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 101; + break; + case 1: + jjAddStates(0, 1); + break; + case 4: + jjAddStates(2, 3); + break; + case 7: + if (curChar == 101 && kind > 52) + kind = 52; + break; + case 8: + if (curChar == 118) + jjstateSet[jjnewStateCnt++] = 7; + break; + case 9: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 8; + break; + case 10: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 9; + break; + case 11: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 10; + break; + case 12: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 13: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 12; + break; + case 15: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 16; + break; + case 17: + if (curChar == 102 && kind > 75) + kind = 75; + break; + case 18: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 17; + break; + case 19: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 15; + break; + case 20: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 19; + break; + case 21: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 20; + break; + case 22: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 21; + break; + case 25: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 24; + break; + case 26: + if (curChar == 114) + jjAddStates(11, 12); + break; + case 29: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 26; + break; + case 30: + if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 29; + break; + case 35: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 34; + break; + case 36: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 37; + break; + case 38: + if (curChar == 115 && kind > 145) + kind = 145; + break; + case 39: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 38; + break; + case 40: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 36; + break; + case 41: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 40; + break; + case 43: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 42; + break; + case 46: + if (curChar == 121 && kind > 176) + kind = 176; + break; + case 47: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 46; + break; + case 51: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 50; + break; + case 53: + if (curChar == 115) + jjAddStates(105, 106); + break; + case 54: + if (curChar == 112 && kind > 52) + kind = 52; + break; + case 55: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 54; + break; + case 56: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 55; + break; + case 58: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 59; + break; + case 60: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 61; + break; + case 62: + if (curChar == 121 && kind > 177) + kind = 177; + break; + case 63: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 62; + break; + case 64: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 60; + break; + case 65: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 64; + break; + case 66: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 65; + break; + case 67: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 66; + break; + case 68: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 58; + break; + case 69: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 68; + break; + case 70: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 69; + break; + case 71: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 70; + break; + case 72: + if (curChar == 99) + jjAddStates(103, 104); + break; + case 73: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 74; + break; + case 75: + if (curChar == 115 && kind > 76) + kind = 76; + break; + case 76: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 75; + break; + case 77: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 73; + break; + case 78: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 77; + break; + case 79: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 78; + break; + case 83: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 84; + break; + case 85: + if (curChar == 115 && kind > 144) + kind = 144; + break; + case 86: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 85; + break; + case 87: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 83; + break; + case 88: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 87; + break; + case 89: + if (curChar == 101) + jjAddStates(101, 102); + break; + case 90: + if (curChar == 121) + jjstateSet[jjnewStateCnt++] = 91; + break; + case 92: + if (curChar == 116 && kind > 180) + kind = 180; + break; + case 93: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 92; + break; + case 94: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 93; + break; + case 95: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 94; + break; + case 96: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 95; + break; + case 97: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 96; + break; + case 98: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 97; + break; + case 99: + if (curChar == 103) + jjstateSet[jjnewStateCnt++] = 98; + break; + case 100: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 90; + break; + case 101: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 100; + break; + case 103: + if (curChar == 121) + jjstateSet[jjnewStateCnt++] = 104; + break; + case 105: + if (curChar == 116 && kind > 181) + kind = 181; + break; + case 106: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 105; + break; + case 107: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 106; + break; + case 108: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 107; + break; + case 109: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 108; + break; + case 110: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 103; + break; + case 111: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 110; + break; + case 112: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 111; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(0, 1); + break; + case 4: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(2, 3); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 113 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjMoveStringLiteralDfa0_19() +{ + return jjMoveNfa_19(0, 0); +} +private final int jjMoveNfa_19(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 4; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffff00002600L & l) != 0L && kind > 216) + kind = 216; + break; + case 2: + if (curChar == 62 && kind > 192) + kind = 192; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 216) + kind = 216; + if (curChar == 93) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (curChar == 93) + jjstateSet[jjnewStateCnt++] = 2; + break; + case 3: + if (kind > 216) + kind = 216; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 216) + kind = 216; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 4 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_6(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_6(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_6(jjStopStringLiteralDfa_6(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_6(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_6(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_6() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_6(0x0L, 0x840000000L); + case 110: + return jjMoveStringLiteralDfa1_6(0x1L, 0x0L); + default : + return jjMoveNfa_6(21, 0); + } +} +private final int jjMoveStringLiteralDfa1_6(long active1, long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_6(0, 0L, active1, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 227; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_6(active1, 0L, active3, 0x40000000L); + case 97: + return jjMoveStringLiteralDfa2_6(active1, 0x1L, active3, 0L); + default : + break; + } + return jjStartNfa_6(0, 0L, active1, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_6(long old1, long active1, long old3, long active3) +{ + if (((active1 &= old1) | (active3 &= old3)) == 0L) + return jjStartNfa_6(0, 0L, old1, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_6(1, 0L, active1, 0L, active3); + return 2; + } + switch(curChar) + { + case 58: + if ((active3 & 0x40000000L) != 0L) + return jjStopAtPos(2, 222); + break; + case 109: + return jjMoveStringLiteralDfa3_6(active1, 0x1L, active3, 0L); + default : + break; + } + return jjStartNfa_6(1, 0L, active1, 0L, active3); +} +private final int jjMoveStringLiteralDfa3_6(long old1, long active1, long old3, long active3) +{ + if (((active1 &= old1) | (active3 &= old3)) == 0L) + return jjStartNfa_6(1, 0L, old1, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_6(2, 0L, active1, 0L, 0L); + return 3; + } + switch(curChar) + { + case 101: + return jjMoveStringLiteralDfa4_6(active1, 0x1L); + default : + break; + } + return jjStartNfa_6(2, 0L, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa4_6(long old1, long active1) +{ + if (((active1 &= old1)) == 0L) + return jjStartNfa_6(2, 0L, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_6(3, 0L, active1, 0L, 0L); + return 4; + } + switch(curChar) + { + case 115: + return jjMoveStringLiteralDfa5_6(active1, 0x1L); + default : + break; + } + return jjStartNfa_6(3, 0L, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa5_6(long old1, long active1) +{ + if (((active1 &= old1)) == 0L) + return jjStartNfa_6(3, 0L, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_6(4, 0L, active1, 0L, 0L); + return 5; + } + switch(curChar) + { + case 112: + return jjMoveStringLiteralDfa6_6(active1, 0x1L); + default : + break; + } + return jjStartNfa_6(4, 0L, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa6_6(long old1, long active1) +{ + if (((active1 &= old1)) == 0L) + return jjStartNfa_6(4, 0L, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_6(5, 0L, active1, 0L, 0L); + return 6; + } + switch(curChar) + { + case 97: + return jjMoveStringLiteralDfa7_6(active1, 0x1L); + default : + break; + } + return jjStartNfa_6(5, 0L, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa7_6(long old1, long active1) +{ + if (((active1 &= old1)) == 0L) + return jjStartNfa_6(5, 0L, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_6(6, 0L, active1, 0L, 0L); + return 7; + } + switch(curChar) + { + case 99: + return jjMoveStringLiteralDfa8_6(active1, 0x1L); + default : + break; + } + return jjStartNfa_6(6, 0L, active1, 0L, 0L); +} +private final int jjMoveStringLiteralDfa8_6(long old1, long active1) +{ + if (((active1 &= old1)) == 0L) + return jjStartNfa_6(6, 0L, old1, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_6(7, 0L, active1, 0L, 0L); + return 8; + } + switch(curChar) + { + case 101: + if ((active1 & 0x1L) != 0L) + return jjStopAtPos(8, 64); + break; + default : + break; + } + return jjStartNfa_6(7, 0L, active1, 0L, 0L); +} +private final int jjMoveNfa_6(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 22; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 21: + if ((0x100002600L & l) != 0L) + { + if (kind > 12) + kind = 12; + jjCheckNAdd(6); + } + else if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + else if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 0: + if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 1: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(1, 2); + break; + case 2: + if (curChar != 34) + break; + if (kind > 10) + kind = 10; + jjstateSet[jjnewStateCnt++] = 0; + break; + case 3: + if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + break; + case 4: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(4, 5); + break; + case 5: + if (curChar != 39) + break; + if (kind > 10) + kind = 10; + jjstateSet[jjnewStateCnt++] = 3; + break; + case 6: + if ((0x100002600L & l) == 0L) + break; + if (kind > 12) + kind = 12; + jjCheckNAdd(6); + break; + case 8: + if ((0x100002600L & l) != 0L) + jjAddStates(107, 108); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 21: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 20; + break; + case 1: + jjAddStates(0, 1); + break; + case 4: + jjAddStates(2, 3); + break; + case 7: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 8; + break; + case 9: + if (curChar == 116 && kind > 93) + kind = 93; + break; + case 10: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 9; + break; + case 11: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 10; + break; + case 12: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 13: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 12; + break; + case 14: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 13; + break; + case 15: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 14; + break; + case 16: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 7; + break; + case 17: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 16; + break; + case 18: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 17; + break; + case 19: + if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 18; + break; + case 20: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 19; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(0, 1); + break; + case 4: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(2, 3); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 22 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_10(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_10(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_10(jjStopStringLiteralDfa_10(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_10(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_10(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_10() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_10(0x840000000L); + default : + return jjMoveNfa_10(1, 0); + } +} +private final int jjMoveStringLiteralDfa1_10(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_10(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 227; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_10(active3, 0x40000000L); + default : + break; + } + return jjStartNfa_10(0, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_10(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_10(0, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_10(1, 0L, 0L, 0L, active3); + return 2; + } + switch(curChar) + { + case 58: + if ((active3 & 0x40000000L) != 0L) + return jjStopAtPos(2, 222); + break; + default : + break; + } + return jjStartNfa_10(1, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_10(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 6; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + case 0: + if ((0x100002600L & l) == 0L) + break; + kind = 12; + jjCheckNAdd(0); + break; + case 2: + if ((0x3ff600000000000L & l) != 0L) + jjAddStates(4, 5); + break; + case 3: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 5: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 50) + kind = 50; + jjstateSet[jjnewStateCnt++] = 5; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 50) + kind = 50; + jjCheckNAddStates(6, 8); + break; + case 2: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + case 5: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 50) + kind = 50; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 50) + kind = 50; + jjCheckNAddStates(6, 8); + break; + case 2: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 50) + kind = 50; + jjCheckNAdd(5); + break; + case 5: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 50) + kind = 50; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 6 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_16(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_16(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_16(jjStopStringLiteralDfa_16(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_16(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_16(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_16() +{ + switch(curChar) + { + case 34: + return jjStopAtPos(0, 172); + case 39: + return jjStopAtPos(0, 217); + case 47: + return jjMoveStringLiteralDfa1_16(0x80L); + case 61: + return jjStopAtPos(0, 202); + case 62: + return jjStopAtPos(0, 198); + default : + return jjMoveNfa_16(1, 0); + } +} +private final int jjMoveStringLiteralDfa1_16(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_16(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 62: + if ((active3 & 0x80L) != 0L) + return jjStopAtPos(1, 199); + break; + default : + break; + } + return jjStartNfa_16(0, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_16(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 6; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + case 0: + if ((0x100002600L & l) == 0L) + break; + kind = 237; + jjCheckNAdd(0); + break; + case 2: + if ((0x3ff600000000000L & l) != 0L) + jjAddStates(4, 5); + break; + case 3: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 5: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 203) + kind = 203; + jjstateSet[jjnewStateCnt++] = 5; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 203) + kind = 203; + jjCheckNAddStates(6, 8); + break; + case 2: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + case 5: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 203) + kind = 203; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 203) + kind = 203; + jjCheckNAddStates(6, 8); + break; + case 2: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 203) + kind = 203; + jjCheckNAdd(5); + break; + case 5: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 203) + kind = 203; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 6 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_9(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_9(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_9(jjStopStringLiteralDfa_9(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_9(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_9(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_9() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_9(0x800000000L); + default : + return jjMoveNfa_9(1, 0); + } +} +private final int jjMoveStringLiteralDfa1_9(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_9(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + return jjStopAtPos(1, 227); + break; + default : + break; + } + return jjStartNfa_9(0, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_9(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 6; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + case 0: + if ((0x100002600L & l) == 0L) + break; + kind = 12; + jjCheckNAdd(0); + break; + case 2: + if ((0x3ff600000000000L & l) != 0L) + jjAddStates(4, 5); + break; + case 3: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 4; + break; + case 5: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 184) + kind = 184; + jjstateSet[jjnewStateCnt++] = 5; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 184) + kind = 184; + jjCheckNAddStates(6, 8); + break; + case 2: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + case 5: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 184) + kind = 184; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 184) + kind = 184; + jjCheckNAddStates(6, 8); + break; + case 2: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(2, 3); + break; + case 4: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 184) + kind = 184; + jjCheckNAdd(5); + break; + case 5: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 184) + kind = 184; + jjCheckNAdd(5); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 6 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_5(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_5(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_5(jjStopStringLiteralDfa_5(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_5(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_5(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_5() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_5(0x840000000L); + case 61: + return jjStopAtPos(0, 110); + default : + return jjMoveNfa_5(7, 0); + } +} +private final int jjMoveStringLiteralDfa1_5(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_5(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 227; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_5(active3, 0x40000000L); + default : + break; + } + return jjStartNfa_5(0, 0L, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_5(long old3, long active3) +{ + if (((active3 &= old3)) == 0L) + return jjStartNfa_5(0, 0L, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_5(1, 0L, 0L, 0L, active3); + return 2; + } + switch(curChar) + { + case 58: + if ((active3 & 0x40000000L) != 0L) + return jjStopAtPos(2, 222); + break; + default : + break; + } + return jjStartNfa_5(1, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_5(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 9; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 7: + if ((0x100002600L & l) != 0L) + { + if (kind > 12) + kind = 12; + jjCheckNAdd(6); + } + else if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + else if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 0: + if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 1: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(1, 2); + break; + case 2: + if (curChar != 34) + break; + if (kind > 10) + kind = 10; + jjstateSet[jjnewStateCnt++] = 0; + break; + case 3: + if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + break; + case 4: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(4, 5); + break; + case 5: + if (curChar != 39) + break; + if (kind > 10) + kind = 10; + jjstateSet[jjnewStateCnt++] = 3; + break; + case 6: + if ((0x100002600L & l) == 0L) + break; + if (kind > 12) + kind = 12; + jjCheckNAdd(6); + break; + case 8: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 188) + kind = 188; + jjstateSet[jjnewStateCnt++] = 8; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 7: + case 8: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 188) + kind = 188; + jjCheckNAdd(8); + break; + case 1: + jjAddStates(0, 1); + break; + case 4: + jjAddStates(2, 3); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 7: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 188) + kind = 188; + jjCheckNAdd(8); + break; + case 1: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(0, 1); + break; + case 4: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(2, 3); + break; + case 8: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 188) + kind = 188; + jjCheckNAdd(8); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 9 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_2(0x800000000L); + case 41: + return jjStopAtPos(0, 139); + default : + return jjMoveNfa_2(6, 0); + } +} +private final int jjMoveStringLiteralDfa1_2(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_2(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + return jjStopAtPos(1, 227); + break; + default : + break; + } + return jjStartNfa_2(0, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_2(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 8; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 6: + if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + else if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 0: + if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 1: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(1, 2); + break; + case 2: + if (curChar != 34) + break; + if (kind > 5) + kind = 5; + jjstateSet[jjnewStateCnt++] = 0; + break; + case 3: + if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + break; + case 4: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(4, 5); + break; + case 5: + if (curChar != 39) + break; + if (kind > 5) + kind = 5; + jjstateSet[jjnewStateCnt++] = 3; + break; + case 7: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 189) + kind = 189; + jjstateSet[jjnewStateCnt++] = 7; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 6: + case 7: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 189) + kind = 189; + jjCheckNAdd(7); + break; + case 1: + jjAddStates(0, 1); + break; + case 4: + jjAddStates(2, 3); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 6: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 189) + kind = 189; + jjCheckNAdd(7); + break; + case 1: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(0, 1); + break; + case 4: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(2, 3); + break; + case 7: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 189) + kind = 189; + jjCheckNAdd(7); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 8 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_8(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_8(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_8(jjStopStringLiteralDfa_8(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_8(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_8(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_8() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_8(0x0L, 0x840000000L); + case 112: + return jjMoveStringLiteralDfa1_8(0x4000000000000000L, 0x0L); + case 115: + return jjMoveStringLiteralDfa1_8(0x8000000000000000L, 0x0L); + default : + return jjMoveNfa_8(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_8(long active0, long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_8(0, active0, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 227; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_8(active0, 0L, active3, 0x40000000L); + case 114: + return jjMoveStringLiteralDfa2_8(active0, 0x4000000000000000L, active3, 0L); + case 116: + return jjMoveStringLiteralDfa2_8(active0, 0x8000000000000000L, active3, 0L); + default : + break; + } + return jjStartNfa_8(0, active0, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa2_8(long old0, long active0, long old3, long active3) +{ + if (((active0 &= old0) | (active3 &= old3)) == 0L) + return jjStartNfa_8(0, old0, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_8(1, active0, 0L, 0L, active3); + return 2; + } + switch(curChar) + { + case 58: + if ((active3 & 0x40000000L) != 0L) + return jjStopAtPos(2, 222); + break; + case 101: + return jjMoveStringLiteralDfa3_8(active0, 0x4000000000000000L, active3, 0L); + case 114: + return jjMoveStringLiteralDfa3_8(active0, 0x8000000000000000L, active3, 0L); + default : + break; + } + return jjStartNfa_8(1, active0, 0L, 0L, active3); +} +private final int jjMoveStringLiteralDfa3_8(long old0, long active0, long old3, long active3) +{ + if (((active0 &= old0) | (active3 &= old3)) == 0L) + return jjStartNfa_8(1, old0, 0L, 0L, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_8(2, active0, 0L, 0L, 0L); + return 3; + } + switch(curChar) + { + case 105: + return jjMoveStringLiteralDfa4_8(active0, 0x8000000000000000L); + case 115: + return jjMoveStringLiteralDfa4_8(active0, 0x4000000000000000L); + default : + break; + } + return jjStartNfa_8(2, active0, 0L, 0L, 0L); +} +private final int jjMoveStringLiteralDfa4_8(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_8(2, old0, 0L, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_8(3, active0, 0L, 0L, 0L); + return 4; + } + switch(curChar) + { + case 101: + return jjMoveStringLiteralDfa5_8(active0, 0x4000000000000000L); + case 112: + if ((active0 & 0x8000000000000000L) != 0L) + return jjStopAtPos(4, 63); + break; + default : + break; + } + return jjStartNfa_8(3, active0, 0L, 0L, 0L); +} +private final int jjMoveStringLiteralDfa5_8(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_8(3, old0, 0L, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_8(4, active0, 0L, 0L, 0L); + return 5; + } + switch(curChar) + { + case 114: + return jjMoveStringLiteralDfa6_8(active0, 0x4000000000000000L); + default : + break; + } + return jjStartNfa_8(4, active0, 0L, 0L, 0L); +} +private final int jjMoveStringLiteralDfa6_8(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_8(4, old0, 0L, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_8(5, active0, 0L, 0L, 0L); + return 6; + } + switch(curChar) + { + case 118: + return jjMoveStringLiteralDfa7_8(active0, 0x4000000000000000L); + default : + break; + } + return jjStartNfa_8(5, active0, 0L, 0L, 0L); +} +private final int jjMoveStringLiteralDfa7_8(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_8(5, old0, 0L, 0L, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_8(6, active0, 0L, 0L, 0L); + return 7; + } + switch(curChar) + { + case 101: + if ((active0 & 0x4000000000000000L) != 0L) + return jjStopAtPos(7, 62); + break; + default : + break; + } + return jjStartNfa_8(6, active0, 0L, 0L, 0L); +} +private final int jjMoveNfa_8(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 1; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x100002600L & l) == 0L) + break; + kind = 12; + jjstateSet[jjnewStateCnt++] = 0; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 1 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_0(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active1 & 0x2000000000L) != 0L) + return 58; + if ((active1 & 0x100L) != 0L) + { + jjmatchedKind = 235; + return 96; + } + if ((active2 & 0xc00000000000L) != 0L) + return 803; + return -1; + case 1: + if ((active1 & 0x100L) != 0L) + return 804; + return -1; + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 36: + return jjStopAtPos(0, 49); + case 37: + return jjMoveStringLiteralDfa1_0(0x0L, 0x0L, 0x80000000000L, 0x0L); + case 40: + jjmatchedKind = 134; + return jjMoveStringLiteralDfa1_0(0x0L, 0x0L, 0x0L, 0x840000000L); + case 41: + return jjStopAtPos(0, 138); + case 42: + return jjStartNfaWithStates_0(0, 101, 58); + case 43: + return jjStopAtPos(0, 129); + case 44: + return jjStopAtPos(0, 168); + case 45: + return jjStopAtPos(0, 128); + case 46: + jjmatchedKind = 174; + return jjMoveStringLiteralDfa1_0(0x0L, 0x0L, 0x800000000000L, 0x0L); + case 47: + jjmatchedKind = 105; + return jjMoveStringLiteralDfa1_0(0x0L, 0x40000000000L, 0x0L, 0x0L); + case 59: + return jjStopAtPos(0, 170); + case 60: + jjmatchedKind = 197; + return jjMoveStringLiteralDfa1_0(0x8000L, 0x0L, 0x4000000000000000L, 0x10000000000L); + case 64: + return jjStopAtPos(0, 135); + case 97: + return jjMoveStringLiteralDfa1_0(0x0L, 0x100L, 0x0L, 0x0L); + case 123: + return jjStopAtPos(0, 205); + case 125: + return jjStopAtPos(0, 241); + default : + return jjMoveNfa_0(19, 0); + } +} +private final int jjMoveStringLiteralDfa1_0(long active0, long active1, long active2, long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(0, active0, active1, active2, active3); + return 1; + } + switch(curChar) + { + case 33: + return jjMoveStringLiteralDfa2_0(active0, 0L, active1, 0L, active2, 0x4000000000000000L, active3, 0x10000000000L); + case 37: + return jjMoveStringLiteralDfa2_0(active0, 0L, active1, 0L, active2, 0x80000000000L, active3, 0L); + case 46: + if ((active2 & 0x800000000000L) != 0L) + return jjStopAtPos(1, 175); + break; + case 47: + if ((active1 & 0x40000000000L) != 0L) + return jjStopAtPos(1, 106); + break; + case 58: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 227; + jjmatchedPos = 1; + } + return jjMoveStringLiteralDfa2_0(active0, 0L, active1, 0L, active2, 0L, active3, 0x40000000L); + case 63: + if ((active0 & 0x8000L) != 0L) + return jjStopAtPos(1, 15); + break; + case 115: + if ((active1 & 0x100L) != 0L) + return jjStartNfaWithStates_0(1, 72, 804); + break; + default : + break; + } + return jjStartNfa_0(0, active0, active1, active2, active3); +} +private final int jjMoveStringLiteralDfa2_0(long old0, long active0, long old1, long active1, long old2, long active2, long old3, long active3) +{ + if (((active0 &= old0) | (active1 &= old1) | (active2 &= old2) | (active3 &= old3)) == 0L) + return jjStartNfa_0(0, old0, old1, old2, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(1, 0L, 0L, active2, active3); + return 2; + } + switch(curChar) + { + case 37: + if ((active2 & 0x80000000000L) != 0L) + return jjStopAtPos(2, 171); + break; + case 45: + return jjMoveStringLiteralDfa3_0(active2, 0L, active3, 0x10000000000L); + case 58: + if ((active3 & 0x40000000L) != 0L) + return jjStopAtPos(2, 222); + break; + case 91: + return jjMoveStringLiteralDfa3_0(active2, 0x4000000000000000L, active3, 0L); + default : + break; + } + return jjStartNfa_0(1, 0L, 0L, active2, active3); +} +private final int jjMoveStringLiteralDfa3_0(long old2, long active2, long old3, long active3) +{ + if (((active2 &= old2) | (active3 &= old3)) == 0L) + return jjStartNfa_0(1, 0L, 0L, old2, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(2, 0L, 0L, active2, active3); + return 3; + } + switch(curChar) + { + case 45: + if ((active3 & 0x10000000000L) != 0L) + return jjStopAtPos(3, 232); + break; + case 67: + return jjMoveStringLiteralDfa4_0(active2, 0x4000000000000000L, active3, 0L); + default : + break; + } + return jjStartNfa_0(2, 0L, 0L, active2, active3); +} +private final int jjMoveStringLiteralDfa4_0(long old2, long active2, long old3, long active3) +{ + if (((active2 &= old2) | (active3 &= old3)) == 0L) + return jjStartNfa_0(2, 0L, 0L, old2, old3); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(3, 0L, 0L, active2, 0L); + return 4; + } + switch(curChar) + { + case 68: + return jjMoveStringLiteralDfa5_0(active2, 0x4000000000000000L); + default : + break; + } + return jjStartNfa_0(3, 0L, 0L, active2, 0L); +} +private final int jjMoveStringLiteralDfa5_0(long old2, long active2) +{ + if (((active2 &= old2)) == 0L) + return jjStartNfa_0(3, 0L, 0L, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(4, 0L, 0L, active2, 0L); + return 5; + } + switch(curChar) + { + case 65: + return jjMoveStringLiteralDfa6_0(active2, 0x4000000000000000L); + default : + break; + } + return jjStartNfa_0(4, 0L, 0L, active2, 0L); +} +private final int jjMoveStringLiteralDfa6_0(long old2, long active2) +{ + if (((active2 &= old2)) == 0L) + return jjStartNfa_0(4, 0L, 0L, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(5, 0L, 0L, active2, 0L); + return 6; + } + switch(curChar) + { + case 84: + return jjMoveStringLiteralDfa7_0(active2, 0x4000000000000000L); + default : + break; + } + return jjStartNfa_0(5, 0L, 0L, active2, 0L); +} +private final int jjMoveStringLiteralDfa7_0(long old2, long active2) +{ + if (((active2 &= old2)) == 0L) + return jjStartNfa_0(5, 0L, 0L, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(6, 0L, 0L, active2, 0L); + return 7; + } + switch(curChar) + { + case 65: + return jjMoveStringLiteralDfa8_0(active2, 0x4000000000000000L); + default : + break; + } + return jjStartNfa_0(6, 0L, 0L, active2, 0L); +} +private final int jjMoveStringLiteralDfa8_0(long old2, long active2) +{ + if (((active2 &= old2)) == 0L) + return jjStartNfa_0(6, 0L, 0L, old2, 0L); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(7, 0L, 0L, active2, 0L); + return 8; + } + switch(curChar) + { + case 91: + if ((active2 & 0x4000000000000000L) != 0L) + return jjStopAtPos(8, 190); + break; + default : + break; + } + return jjStartNfa_0(7, 0L, 0L, active2, 0L); +} +private final int jjMoveNfa_0(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 803; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 804: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + } + else if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(770, 771); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 774; + else if (curChar == 40) + { + if (kind > 187) + kind = 187; + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(772, 773); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 768; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddStates(109, 111); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 765; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(766, 767); + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(763, 764); + break; + case 96: + if ((0x3ff600000000000L & l) != 0L) + { + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + } + else if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(770, 771); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 774; + else if (curChar == 40) + { + if (kind > 187) + kind = 187; + } + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(772, 773); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 768; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddStates(109, 111); + else if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 765; + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(766, 767); + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(763, 764); + break; + case 19: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 1) + kind = 1; + jjCheckNAddStates(112, 121); + } + else if ((0x100002600L & l) != 0L) + { + if (kind > 12) + kind = 12; + jjCheckNAdd(36); + } + else if (curChar == 46) + jjCheckNAddStates(122, 124); + else if (curChar == 42) + jjstateSet[jjnewStateCnt++] = 58; + else if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + else if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 803: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(125, 127); + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(93, 80); + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 2) + kind = 2; + jjCheckNAdd(92); + } + break; + case 0: + if (curChar == 34) + jjCheckNAddTwoStates(1, 2); + break; + case 1: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(1, 2); + break; + case 2: + if (curChar != 34) + break; + if (kind > 4) + kind = 4; + jjstateSet[jjnewStateCnt++] = 0; + break; + case 3: + if (curChar == 39) + jjCheckNAddTwoStates(4, 5); + break; + case 4: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(4, 5); + break; + case 5: + if (curChar != 39) + break; + if (kind > 4) + kind = 4; + jjstateSet[jjnewStateCnt++] = 3; + break; + case 7: + if ((0x100002600L & l) != 0L) + jjAddStates(128, 129); + break; + case 21: + if ((0x100002600L & l) != 0L) + jjAddStates(130, 131); + break; + case 36: + if ((0x100002600L & l) == 0L) + break; + if (kind > 12) + kind = 12; + jjCheckNAdd(36); + break; + case 38: + if ((0x100002600L & l) != 0L) + jjAddStates(132, 133); + break; + case 47: + if ((0x100002600L & l) != 0L) + jjAddStates(134, 135); + break; + case 57: + if (curChar == 42) + jjstateSet[jjnewStateCnt++] = 58; + break; + case 58: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 59; + break; + case 60: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 104) + kind = 104; + jjstateSet[jjnewStateCnt++] = 60; + break; + case 62: + if ((0x100002600L & l) != 0L) + jjAddStates(136, 137); + break; + case 63: + if (curChar == 36 && kind > 143) + kind = 143; + break; + case 67: + if ((0x100002600L & l) != 0L) + jjAddStates(138, 139); + break; + case 68: + if (curChar == 40 && kind > 152) + kind = 152; + break; + case 72: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 1) + kind = 1; + jjCheckNAddStates(112, 121); + break; + case 73: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 1) + kind = 1; + jjCheckNAdd(73); + break; + case 74: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(74, 75); + break; + case 75: + if (curChar != 46) + break; + if (kind > 2) + kind = 2; + jjCheckNAdd(76); + break; + case 76: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 2) + kind = 2; + jjCheckNAdd(76); + break; + case 77: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(140, 142); + break; + case 78: + if (curChar == 46) + jjCheckNAddTwoStates(79, 80); + break; + case 79: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(79, 80); + break; + case 81: + if ((0x280000000000L & l) != 0L) + jjCheckNAdd(82); + break; + case 82: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 3) + kind = 3; + jjCheckNAdd(82); + break; + case 83: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(143, 146); + break; + case 84: + if (curChar == 46) + jjCheckNAddStates(147, 149); + break; + case 85: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(147, 149); + break; + case 87: + if ((0x280000000000L & l) != 0L) + jjCheckNAdd(88); + break; + case 88: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(88, 89); + break; + case 90: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 250) + kind = 250; + jjstateSet[jjnewStateCnt++] = 90; + break; + case 91: + if (curChar == 46) + jjCheckNAddStates(122, 124); + break; + case 92: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 2) + kind = 2; + jjCheckNAdd(92); + break; + case 93: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(93, 80); + break; + case 94: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddStates(125, 127); + break; + case 97: + if ((0x100002600L & l) != 0L) + jjCheckNAddStates(150, 152); + break; + case 98: + if (curChar == 34) + jjCheckNAddTwoStates(99, 100); + break; + case 99: + if ((0xfffffffbffffffffL & l) != 0L) + jjCheckNAddTwoStates(99, 100); + break; + case 100: + if (curChar != 34) + break; + if (kind > 9) + kind = 9; + jjCheckNAdd(98); + break; + case 101: + if (curChar == 39) + jjCheckNAddTwoStates(102, 103); + break; + case 102: + if ((0xffffff7fffffffffL & l) != 0L) + jjCheckNAddTwoStates(102, 103); + break; + case 103: + if (curChar != 39) + break; + if (kind > 9) + kind = 9; + jjCheckNAdd(101); + break; + case 105: + if ((0x100002600L & l) != 0L) + jjAddStates(153, 154); + break; + case 106: + if (curChar == 58 && kind > 21) + kind = 21; + break; + case 107: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 106; + break; + case 116: + if ((0x100002600L & l) != 0L) + jjAddStates(155, 156); + break; + case 117: + if (curChar == 58 && kind > 24) + kind = 24; + break; + case 118: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 117; + break; + case 126: + if ((0x100002600L & l) != 0L) + jjAddStates(157, 158); + break; + case 127: + if (curChar == 58 && kind > 29) + kind = 29; + break; + case 128: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 127; + break; + case 132: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 131; + break; + case 135: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 134; + break; + case 144: + if ((0x100002600L & l) != 0L) + jjAddStates(159, 160); + break; + case 145: + if (curChar == 40 && kind > 79) + kind = 79; + break; + case 154: + if ((0x100002600L & l) != 0L) + jjAddStates(161, 162); + break; + case 156: + if ((0x3ff600000000000L & l) != 0L) + jjAddStates(163, 164); + break; + case 157: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 158; + break; + case 159: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddStates(165, 167); + break; + case 160: + if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(160, 161); + break; + case 170: + if ((0x100002600L & l) != 0L) + jjAddStates(168, 169); + break; + case 181: + if ((0x100002600L & l) != 0L) + jjAddStates(170, 171); + break; + case 182: + if (curChar == 58 && kind > 18) + kind = 18; + break; + case 183: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 182; + break; + case 188: + if ((0x100002600L & l) != 0L) + jjAddStates(172, 173); + break; + case 196: + if ((0x100002600L & l) != 0L) + jjAddStates(174, 175); + break; + case 197: + if (curChar == 40 && kind > 153) + kind = 153; + break; + case 205: + if ((0x100002600L & l) != 0L) + jjAddStates(176, 177); + break; + case 206: + if (curChar == 58 && kind > 19) + kind = 19; + break; + case 207: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 206; + break; + case 217: + if ((0x100002600L & l) != 0L) + jjAddStates(178, 179); + break; + case 218: + if (curChar == 58 && kind > 23) + kind = 23; + break; + case 219: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 218; + break; + case 223: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 222; + break; + case 226: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 225; + break; + case 237: + if ((0x100002600L & l) != 0L) + jjAddStates(180, 181); + break; + case 252: + if ((0x100002600L & l) != 0L) + jjAddStates(182, 183); + break; + case 267: + if ((0x100002600L & l) != 0L) + jjAddStates(184, 185); + break; + case 269: + if ((0x100002600L & l) != 0L) + jjAddStates(186, 187); + break; + case 287: + if ((0x100002600L & l) != 0L) + jjAddStates(188, 189); + break; + case 298: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 297; + break; + case 312: + if ((0x100002600L & l) != 0L) + jjAddStates(190, 191); + break; + case 331: + if ((0x100002600L & l) != 0L) + jjAddStates(192, 193); + break; + case 346: + if ((0x100002600L & l) != 0L) + jjAddStates(194, 195); + break; + case 350: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 349; + break; + case 361: + if ((0x100002600L & l) != 0L) + jjAddStates(196, 197); + break; + case 377: + if ((0x100002600L & l) != 0L) + jjAddStates(198, 199); + break; + case 379: + if ((0x100002600L & l) != 0L) + jjAddStates(200, 201); + break; + case 401: + if ((0x100002600L & l) != 0L) + jjAddStates(202, 203); + break; + case 403: + if ((0x100002600L & l) != 0L) + jjAddStates(204, 205); + break; + case 423: + if ((0x100002600L & l) != 0L) + jjAddStates(206, 207); + break; + case 425: + if ((0x100002600L & l) != 0L) + jjAddStates(208, 209); + break; + case 446: + if ((0x100002600L & l) != 0L) + jjAddStates(210, 211); + break; + case 447: + if (curChar == 40 && kind > 149) + kind = 149; + break; + case 451: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 450; + break; + case 460: + if ((0x100002600L & l) != 0L) + jjAddStates(212, 213); + break; + case 469: + if ((0x100002600L & l) != 0L) + jjAddStates(214, 215); + break; + case 471: + if ((0x100002600L & l) != 0L) + jjAddStates(216, 217); + break; + case 472: + if (curChar == 36 && kind > 182) + kind = 182; + break; + case 487: + if ((0x100002600L & l) != 0L) + jjAddStates(218, 219); + break; + case 488: + if (curChar == 58 && kind > 20) + kind = 20; + break; + case 489: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 488; + break; + case 495: + if ((0x100002600L & l) != 0L) + jjAddStates(220, 221); + break; + case 496: + if (curChar == 58 && kind > 26) + kind = 26; + break; + case 497: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 496; + break; + case 504: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 503; + break; + case 514: + if ((0x100002600L & l) != 0L) + jjAddStates(222, 223); + break; + case 515: + if (curChar == 58 && kind > 28) + kind = 28; + break; + case 516: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 515; + break; + case 525: + if ((0x100002600L & l) != 0L) + jjAddStates(224, 225); + break; + case 527: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddStates(226, 228); + break; + case 528: + if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(528, 529); + break; + case 540: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 539; + break; + case 551: + if ((0x100002600L & l) != 0L) + jjAddStates(229, 230); + break; + case 563: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 562; + break; + case 574: + if ((0x100002600L & l) != 0L) + jjAddStates(231, 232); + break; + case 575: + if (curChar == 40 && kind > 155) + kind = 155; + break; + case 586: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 585; + break; + case 598: + if ((0x100002600L & l) != 0L) + jjAddStates(233, 234); + break; + case 599: + if (curChar == 58 && kind > 22) + kind = 22; + break; + case 600: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 599; + break; + case 604: + if ((0x100002600L & l) != 0L) + jjAddStates(235, 236); + break; + case 605: + if (curChar == 40 && kind > 80) + kind = 80; + break; + case 612: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 611; + break; + case 619: + if ((0x100002600L & l) != 0L) + jjAddStates(237, 238); + break; + case 620: + if (curChar == 40 && kind > 81) + kind = 81; + break; + case 629: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 628; + break; + case 636: + if ((0x100002600L & l) != 0L) + jjAddStates(239, 240); + break; + case 637: + if (curChar == 36 && kind > 140) + kind = 140; + break; + case 642: + if ((0x100002600L & l) != 0L) + jjAddStates(241, 242); + break; + case 643: + if (curChar == 58 && kind > 25) + kind = 25; + break; + case 644: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 643; + break; + case 651: + if (curChar == 45) + jjstateSet[jjnewStateCnt++] = 650; + break; + case 661: + if ((0x100002600L & l) != 0L) + jjAddStates(243, 244); + break; + case 662: + if (curChar == 58 && kind > 27) + kind = 27; + break; + case 663: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 662; + break; + case 672: + if ((0x100002600L & l) != 0L) + jjAddStates(245, 246); + break; + case 673: + if (curChar == 36 && kind > 142) + kind = 142; + break; + case 677: + if ((0x100002600L & l) != 0L) + jjAddStates(247, 248); + break; + case 678: + if (curChar == 40 && kind > 78) + kind = 78; + break; + case 685: + if ((0x100002600L & l) != 0L) + jjAddStates(249, 250); + break; + case 687: + if ((0x3ff600000000000L & l) != 0L) + jjAddStates(251, 252); + break; + case 688: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 689; + break; + case 690: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddStates(253, 255); + break; + case 691: + if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(691, 692); + break; + case 699: + if ((0x100002600L & l) != 0L) + jjAddStates(256, 257); + break; + case 707: + if ((0x100002600L & l) != 0L) + jjAddStates(258, 259); + break; + case 708: + if (curChar == 36 && kind > 141) + kind = 141; + break; + case 714: + if ((0x100002600L & l) != 0L) + jjAddStates(260, 261); + break; + case 719: + if ((0x100002600L & l) != 0L) + jjAddStates(262, 263); + break; + case 720: + if (curChar == 40 && kind > 154) + kind = 154; + break; + case 724: + if ((0x100002600L & l) != 0L) + jjAddStates(264, 265); + break; + case 725: + if (curChar == 40 && kind > 167) + kind = 167; + break; + case 736: + if ((0x100002600L & l) != 0L) + jjAddStates(266, 267); + break; + case 748: + if ((0x100002600L & l) != 0L) + jjAddStates(268, 269); + break; + case 760: + if ((0x100002600L & l) != 0L) + jjAddStates(270, 271); + break; + case 761: + if (curChar == 40 && kind > 166) + kind = 166; + break; + case 763: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(763, 764); + break; + case 764: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 765; + break; + case 765: + if (curChar == 42 && kind > 103) + kind = 103; + break; + case 766: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(766, 767); + break; + case 767: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 768; + break; + case 769: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddStates(109, 111); + break; + case 770: + if ((0x100002600L & l) != 0L) + jjCheckNAddTwoStates(770, 771); + break; + case 771: + if (curChar == 40 && kind > 187) + kind = 187; + break; + case 772: + if ((0x3ff600000000000L & l) != 0L) + jjCheckNAddTwoStates(772, 773); + break; + case 773: + if (curChar == 58) + jjstateSet[jjnewStateCnt++] = 774; + break; + case 775: + if ((0x3ff600000000000L & l) == 0L) + break; + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + break; + case 778: + if ((0x100002600L & l) != 0L) + jjAddStates(272, 273); + break; + case 787: + if ((0x100002600L & l) != 0L) + jjAddStates(274, 276); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 804: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(772, 773); + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(109, 111); + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(766, 767); + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(763, 764); + break; + case 96: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + } + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(772, 773); + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(109, 111); + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(766, 767); + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(763, 764); + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 177; + else if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 141; + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 167; + else if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 123; + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 151; + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 113; + if (curChar == 116) + jjAddStates(150, 152); + break; + case 19: + if ((0x7fffffe87fffffeL & l) != 0L) + { + if (kind > 235) + kind = 235; + jjCheckNAddStates(277, 286); + } + if (curChar == 118) + jjAddStates(287, 288); + else if (curChar == 105) + jjAddStates(289, 291); + else if (curChar == 116) + jjAddStates(292, 294); + else if (curChar == 101) + jjAddStates(295, 298); + else if (curChar == 102) + jjAddStates(299, 301); + else if (curChar == 115) + jjAddStates(302, 305); + else if (curChar == 112) + jjAddStates(306, 311); + else if (curChar == 100) + jjAddStates(312, 327); + else if (curChar == 99) + jjAddStates(328, 330); + else if (curChar == 97) + jjAddStates(331, 337); + else if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 70; + else if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 64; + else if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 55; + else if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 44; + else if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 34; + else if (curChar == 120) + jjstateSet[jjnewStateCnt++] = 18; + break; + case 1: + jjAddStates(0, 1); + break; + case 4: + jjAddStates(2, 3); + break; + case 6: + if (curChar == 121) + jjAddStates(128, 129); + break; + case 8: + if (curChar == 110 && kind > 6) + kind = 6; + break; + case 9: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 8; + break; + case 10: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 9; + break; + case 11: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 10; + break; + case 12: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 13: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 12; + break; + case 14: + if (curChar == 118) + jjstateSet[jjnewStateCnt++] = 13; + break; + case 15: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 6; + break; + case 16: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 15; + break; + case 17: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 16; + break; + case 18: + if (curChar == 113) + jjstateSet[jjnewStateCnt++] = 17; + break; + case 20: + if (curChar == 101) + jjAddStates(130, 131); + break; + case 22: + if (curChar == 101 && kind > 11) + kind = 11; + break; + case 23: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 22; + break; + case 24: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 23; + break; + case 25: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 24; + break; + case 26: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 25; + break; + case 27: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 26; + break; + case 28: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 27; + break; + case 29: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 28; + break; + case 30: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 29; + break; + case 31: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 20; + break; + case 32: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 31; + break; + case 33: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 32; + break; + case 34: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 33; + break; + case 35: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 34; + break; + case 37: + if (curChar == 100) + jjAddStates(132, 133); + break; + case 39: + if (curChar == 123 && kind > 82) + kind = 82; + break; + case 40: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 37; + break; + case 41: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 40; + break; + case 42: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 41; + break; + case 43: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 42; + break; + case 44: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 43; + break; + case 45: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 44; + break; + case 46: + if (curChar == 100) + jjAddStates(134, 135); + break; + case 48: + if (curChar == 123 && kind > 83) + kind = 83; + break; + case 49: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 46; + break; + case 50: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 49; + break; + case 51: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 50; + break; + case 52: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 51; + break; + case 53: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 52; + break; + case 54: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 53; + break; + case 55: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 54; + break; + case 56: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 55; + break; + case 59: + case 60: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 104) + kind = 104; + jjCheckNAdd(60); + break; + case 61: + if (curChar == 116) + jjAddStates(136, 137); + break; + case 64: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 61; + break; + case 65: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 64; + break; + case 66: + if (curChar == 101) + jjAddStates(138, 139); + break; + case 69: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 66; + break; + case 70: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 69; + break; + case 71: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 70; + break; + case 80: + if ((0x2000000020L & l) != 0L) + jjAddStates(338, 339); + break; + case 86: + if ((0x2000000020L & l) != 0L) + jjAddStates(340, 341); + break; + case 89: + case 90: + if ((0x7fffffe07fffffeL & l) == 0L) + break; + if (kind > 250) + kind = 250; + jjCheckNAdd(90); + break; + case 95: + if (curChar == 97) + jjAddStates(331, 337); + break; + case 99: + jjAddStates(342, 343); + break; + case 102: + jjAddStates(344, 345); + break; + case 104: + if (curChar == 101) + jjAddStates(153, 154); + break; + case 108: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 104; + break; + case 109: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 108; + break; + case 110: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 109; + break; + case 111: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 110; + break; + case 112: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 111; + break; + case 113: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 112; + break; + case 114: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 113; + break; + case 115: + if (curChar == 114) + jjAddStates(155, 156); + break; + case 119: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 115; + break; + case 120: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 119; + break; + case 121: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 120; + break; + case 122: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 121; + break; + case 123: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 122; + break; + case 124: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 123; + break; + case 125: + if (curChar == 102) + jjAddStates(157, 158); + break; + case 129: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 125; + break; + case 130: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 129; + break; + case 131: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 130; + break; + case 133: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 132; + break; + case 134: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 133; + break; + case 136: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 135; + break; + case 137: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 136; + break; + case 138: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 137; + break; + case 139: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 138; + break; + case 140: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 139; + break; + case 141: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 140; + break; + case 142: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 141; + break; + case 143: + if (curChar == 101) + jjAddStates(159, 160); + break; + case 146: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 143; + break; + case 147: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 146; + break; + case 148: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 147; + break; + case 149: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 148; + break; + case 150: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 149; + break; + case 151: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 150; + break; + case 152: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 151; + break; + case 153: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 154; + break; + case 155: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(346, 350); + break; + case 156: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(156, 157); + break; + case 158: + case 159: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(165, 167); + break; + case 161: + if (curChar == 123 && kind > 85) + kind = 85; + break; + case 162: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 153; + break; + case 163: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 162; + break; + case 164: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 163; + break; + case 165: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 164; + break; + case 166: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 165; + break; + case 167: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 166; + break; + case 168: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 167; + break; + case 169: + if (curChar == 101) + jjAddStates(168, 169); + break; + case 171: + if (curChar == 123 && kind > 90) + kind = 90; + break; + case 172: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 169; + break; + case 173: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 172; + break; + case 174: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 173; + break; + case 175: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 174; + break; + case 176: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 175; + break; + case 177: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 176; + break; + case 178: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 177; + break; + case 179: + if (curChar == 99) + jjAddStates(328, 330); + break; + case 180: + if (curChar == 100) + jjAddStates(170, 171); + break; + case 184: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 180; + break; + case 185: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 184; + break; + case 186: + if (curChar == 104) + jjstateSet[jjnewStateCnt++] = 185; + break; + case 187: + if (curChar == 116) + jjAddStates(172, 173); + break; + case 189: + if (curChar == 123 && kind > 88) + kind = 88; + break; + case 190: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 187; + break; + case 191: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 190; + break; + case 192: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 191; + break; + case 193: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 192; + break; + case 194: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 193; + break; + case 195: + if (curChar == 116) + jjAddStates(174, 175); + break; + case 198: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 195; + break; + case 199: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 198; + break; + case 200: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 199; + break; + case 201: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 200; + break; + case 202: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 201; + break; + case 203: + if (curChar == 100) + jjAddStates(312, 327); + break; + case 204: + if (curChar == 116) + jjAddStates(176, 177); + break; + case 208: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 204; + break; + case 209: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 208; + break; + case 210: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 209; + break; + case 211: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 210; + break; + case 212: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 211; + break; + case 213: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 212; + break; + case 214: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 213; + break; + case 215: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 214; + break; + case 216: + if (curChar == 102) + jjAddStates(178, 179); + break; + case 220: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 216; + break; + case 221: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 220; + break; + case 222: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 221; + break; + case 224: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 223; + break; + case 225: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 224; + break; + case 227: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 226; + break; + case 228: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 227; + break; + case 229: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 228; + break; + case 230: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 229; + break; + case 231: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 230; + break; + case 232: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 231; + break; + case 233: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 232; + break; + case 234: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 233; + break; + case 235: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 234; + break; + case 236: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 237; + break; + case 238: + if (curChar == 110 && kind > 30) + kind = 30; + break; + case 239: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 238; + break; + case 240: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 239; + break; + case 241: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 240; + break; + case 242: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 241; + break; + case 243: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 242; + break; + case 244: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 243; + break; + case 245: + if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 244; + break; + case 246: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 236; + break; + case 247: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 246; + break; + case 248: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 247; + break; + case 249: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 248; + break; + case 250: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 249; + break; + case 251: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 252; + break; + case 253: + if (curChar == 103 && kind > 31) + kind = 31; + break; + case 254: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 253; + break; + case 255: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 254; + break; + case 256: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 255; + break; + case 257: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 256; + break; + case 258: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 257; + break; + case 259: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 258; + break; + case 260: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 259; + break; + case 261: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 251; + break; + case 262: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 261; + break; + case 263: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 262; + break; + case 264: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 263; + break; + case 265: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 264; + break; + case 266: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 267; + break; + case 268: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 269; + break; + case 270: + if (curChar == 114 && kind > 34) + kind = 34; + break; + case 271: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 270; + break; + case 272: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 271; + break; + case 273: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 272; + break; + case 274: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 273; + break; + case 275: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 268; + break; + case 276: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 275; + break; + case 277: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 276; + break; + case 278: + if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 277; + break; + case 279: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 278; + break; + case 280: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 279; + break; + case 281: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 266; + break; + case 282: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 281; + break; + case 283: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 282; + break; + case 284: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 283; + break; + case 285: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 284; + break; + case 286: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 287; + break; + case 288: + if (curChar == 115 && kind > 35) + kind = 35; + break; + case 289: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 288; + break; + case 290: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 289; + break; + case 291: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 290; + break; + case 292: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 291; + break; + case 293: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 292; + break; + case 294: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 293; + break; + case 295: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 294; + break; + case 296: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 295; + break; + case 297: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 296; + break; + case 299: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 298; + break; + case 300: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 299; + break; + case 301: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 300; + break; + case 302: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 301; + break; + case 303: + if (curChar == 104) + jjstateSet[jjnewStateCnt++] = 302; + break; + case 304: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 303; + break; + case 305: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 304; + break; + case 306: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 286; + break; + case 307: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 306; + break; + case 308: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 307; + break; + case 309: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 308; + break; + case 310: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 309; + break; + case 311: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 312; + break; + case 313: + if (curChar == 110 && kind > 54) + kind = 54; + break; + case 314: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 313; + break; + case 315: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 314; + break; + case 316: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 315; + break; + case 317: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 316; + break; + case 318: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 317; + break; + case 319: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 318; + break; + case 320: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 319; + break; + case 321: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 320; + break; + case 322: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 321; + break; + case 323: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 322; + break; + case 324: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 323; + break; + case 325: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 311; + break; + case 326: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 325; + break; + case 327: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 326; + break; + case 328: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 327; + break; + case 329: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 328; + break; + case 330: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 331; + break; + case 332: + if (curChar == 101 && kind > 60) + kind = 60; + break; + case 333: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 332; + break; + case 334: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 333; + break; + case 335: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 334; + break; + case 336: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 335; + break; + case 337: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 336; + break; + case 338: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 337; + break; + case 339: + if (curChar == 120) + jjstateSet[jjnewStateCnt++] = 338; + break; + case 340: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 330; + break; + case 341: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 340; + break; + case 342: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 341; + break; + case 343: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 342; + break; + case 344: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 343; + break; + case 345: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 346; + break; + case 347: + if (curChar == 105 && kind > 61) + kind = 61; + break; + case 348: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 347; + break; + case 349: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 348; + break; + case 351: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 350; + break; + case 352: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 351; + break; + case 353: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 352; + break; + case 354: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 353; + break; + case 355: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 345; + break; + case 356: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 355; + break; + case 357: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 356; + break; + case 358: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 357; + break; + case 359: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 358; + break; + case 360: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 361; + break; + case 362: + if (curChar == 101 && kind > 65) + kind = 65; + break; + case 363: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 362; + break; + case 364: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 363; + break; + case 365: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 364; + break; + case 366: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 365; + break; + case 367: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 366; + break; + case 368: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 367; + break; + case 369: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 368; + break; + case 370: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 369; + break; + case 371: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 360; + break; + case 372: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 371; + break; + case 373: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 372; + break; + case 374: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 373; + break; + case 375: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 374; + break; + case 376: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 377; + break; + case 378: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 379; + break; + case 380: + if (curChar == 110 && kind > 92) + kind = 92; + break; + case 381: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 380; + break; + case 382: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 381; + break; + case 383: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 382; + break; + case 384: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 383; + break; + case 385: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 384; + break; + case 386: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 385; + break; + case 387: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 386; + break; + case 388: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 387; + break; + case 389: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 378; + break; + case 390: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 389; + break; + case 391: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 390; + break; + case 392: + if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 391; + break; + case 393: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 392; + break; + case 394: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 393; + break; + case 395: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 376; + break; + case 396: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 395; + break; + case 397: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 396; + break; + case 398: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 397; + break; + case 399: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 398; + break; + case 400: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 401; + break; + case 402: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 403; + break; + case 404: + if (curChar == 116 && kind > 94) + kind = 94; + break; + case 405: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 404; + break; + case 406: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 405; + break; + case 407: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 406; + break; + case 408: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 407; + break; + case 409: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 408; + break; + case 410: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 409; + break; + case 411: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 402; + break; + case 412: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 411; + break; + case 413: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 412; + break; + case 414: + if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 413; + break; + case 415: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 414; + break; + case 416: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 415; + break; + case 417: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 400; + break; + case 418: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 417; + break; + case 419: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 418; + break; + case 420: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 419; + break; + case 421: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 420; + break; + case 422: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 423; + break; + case 424: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 425; + break; + case 426: + if (curChar == 110 && kind > 95) + kind = 95; + break; + case 427: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 426; + break; + case 428: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 427; + break; + case 429: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 428; + break; + case 430: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 429; + break; + case 431: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 430; + break; + case 432: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 431; + break; + case 433: + if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 432; + break; + case 434: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 424; + break; + case 435: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 434; + break; + case 436: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 435; + break; + case 437: + if (curChar == 102) + jjstateSet[jjnewStateCnt++] = 436; + break; + case 438: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 437; + break; + case 439: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 438; + break; + case 440: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 422; + break; + case 441: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 440; + break; + case 442: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 441; + break; + case 443: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 442; + break; + case 444: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 443; + break; + case 445: + if (curChar == 101) + jjAddStates(210, 211); + break; + case 448: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 445; + break; + case 449: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 448; + break; + case 450: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 449; + break; + case 452: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 451; + break; + case 453: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 452; + break; + case 454: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 453; + break; + case 455: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 454; + break; + case 456: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 455; + break; + case 457: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 456; + break; + case 458: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 457; + break; + case 459: + if (curChar == 116) + jjAddStates(212, 213); + break; + case 461: + if (curChar == 123 && kind > 151) + kind = 151; + break; + case 462: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 459; + break; + case 463: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 462; + break; + case 464: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 463; + break; + case 465: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 464; + break; + case 466: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 465; + break; + case 467: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 466; + break; + case 468: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 469; + break; + case 470: + if (curChar == 101) + jjAddStates(216, 217); + break; + case 473: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 470; + break; + case 474: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 473; + break; + case 475: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 474; + break; + case 476: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 475; + break; + case 477: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 476; + break; + case 478: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 477; + break; + case 479: + if (curChar == 118) + jjstateSet[jjnewStateCnt++] = 478; + break; + case 480: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 468; + break; + case 481: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 480; + break; + case 482: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 481; + break; + case 483: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 482; + break; + case 484: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 483; + break; + case 485: + if (curChar == 112) + jjAddStates(306, 311); + break; + case 486: + if (curChar == 116) + jjAddStates(218, 219); + break; + case 490: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 486; + break; + case 491: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 490; + break; + case 492: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 491; + break; + case 493: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 492; + break; + case 494: + if (curChar == 103) + jjAddStates(220, 221); + break; + case 498: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 494; + break; + case 499: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 498; + break; + case 500: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 499; + break; + case 501: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 500; + break; + case 502: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 501; + break; + case 503: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 502; + break; + case 505: + if (curChar == 103) + jjstateSet[jjnewStateCnt++] = 504; + break; + case 506: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 505; + break; + case 507: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 506; + break; + case 508: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 507; + break; + case 509: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 508; + break; + case 510: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 509; + break; + case 511: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 510; + break; + case 512: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 511; + break; + case 513: + if (curChar == 103) + jjAddStates(222, 223); + break; + case 517: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 513; + break; + case 518: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 517; + break; + case 519: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 518; + break; + case 520: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 519; + break; + case 521: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 520; + break; + case 522: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 521; + break; + case 523: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 522; + break; + case 524: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 525; + break; + case 526: + case 527: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(226, 228); + break; + case 529: + if (curChar == 123 && kind > 86) + kind = 86; + break; + case 530: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 524; + break; + case 531: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 530; + break; + case 532: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 531; + break; + case 533: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 532; + break; + case 534: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 533; + break; + case 535: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 534; + break; + case 536: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 535; + break; + case 537: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 536; + break; + case 538: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 537; + break; + case 539: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 538; + break; + case 541: + if (curChar == 103) + jjstateSet[jjnewStateCnt++] = 540; + break; + case 542: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 541; + break; + case 543: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 542; + break; + case 544: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 543; + break; + case 545: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 544; + break; + case 546: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 545; + break; + case 547: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 546; + break; + case 548: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 547; + break; + case 549: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 548; + break; + case 550: + if (curChar == 110) + jjAddStates(229, 230); + break; + case 552: + if (curChar == 123 && kind > 87) + kind = 87; + break; + case 553: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 550; + break; + case 554: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 553; + break; + case 555: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 554; + break; + case 556: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 555; + break; + case 557: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 556; + break; + case 558: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 557; + break; + case 559: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 558; + break; + case 560: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 559; + break; + case 561: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 560; + break; + case 562: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 561; + break; + case 564: + if (curChar == 103) + jjstateSet[jjnewStateCnt++] = 563; + break; + case 565: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 564; + break; + case 566: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 565; + break; + case 567: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 566; + break; + case 568: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 567; + break; + case 569: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 568; + break; + case 570: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 569; + break; + case 571: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 570; + break; + case 572: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 571; + break; + case 573: + if (curChar == 110) + jjAddStates(231, 232); + break; + case 576: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 573; + break; + case 577: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 576; + break; + case 578: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 577; + break; + case 579: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 578; + break; + case 580: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 579; + break; + case 581: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 580; + break; + case 582: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 581; + break; + case 583: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 582; + break; + case 584: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 583; + break; + case 585: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 584; + break; + case 587: + if (curChar == 103) + jjstateSet[jjnewStateCnt++] = 586; + break; + case 588: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 587; + break; + case 589: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 588; + break; + case 590: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 589; + break; + case 591: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 590; + break; + case 592: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 591; + break; + case 593: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 592; + break; + case 594: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 593; + break; + case 595: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 594; + break; + case 596: + if (curChar == 115) + jjAddStates(302, 305); + break; + case 597: + if (curChar == 102) + jjAddStates(233, 234); + break; + case 601: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 597; + break; + case 602: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 601; + break; + case 603: + if (curChar == 116) + jjAddStates(235, 236); + break; + case 606: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 603; + break; + case 607: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 606; + break; + case 608: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 607; + break; + case 609: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 608; + break; + case 610: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 609; + break; + case 611: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 610; + break; + case 613: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 612; + break; + case 614: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 613; + break; + case 615: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 614; + break; + case 616: + if (curChar == 104) + jjstateSet[jjnewStateCnt++] = 615; + break; + case 617: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 616; + break; + case 618: + if (curChar == 101) + jjAddStates(237, 238); + break; + case 621: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 618; + break; + case 622: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 621; + break; + case 623: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 622; + break; + case 624: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 623; + break; + case 625: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 624; + break; + case 626: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 625; + break; + case 627: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 626; + break; + case 628: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 627; + break; + case 630: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 629; + break; + case 631: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 630; + break; + case 632: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 631; + break; + case 633: + if (curChar == 104) + jjstateSet[jjnewStateCnt++] = 632; + break; + case 634: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 633; + break; + case 635: + if (curChar == 101) + jjAddStates(239, 240); + break; + case 638: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 635; + break; + case 639: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 638; + break; + case 640: + if (curChar == 102) + jjAddStates(299, 301); + break; + case 641: + if (curChar == 103) + jjAddStates(241, 242); + break; + case 645: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 641; + break; + case 646: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 645; + break; + case 647: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 646; + break; + case 648: + if (curChar == 98) + jjstateSet[jjnewStateCnt++] = 647; + break; + case 649: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 648; + break; + case 650: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 649; + break; + case 652: + if (curChar == 103) + jjstateSet[jjnewStateCnt++] = 651; + break; + case 653: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 652; + break; + case 654: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 653; + break; + case 655: + if (curChar == 119) + jjstateSet[jjnewStateCnt++] = 654; + break; + case 656: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 655; + break; + case 657: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 656; + break; + case 658: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 657; + break; + case 659: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 658; + break; + case 660: + if (curChar == 103) + jjAddStates(243, 244); + break; + case 664: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 660; + break; + case 665: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 664; + break; + case 666: + if (curChar == 119) + jjstateSet[jjnewStateCnt++] = 665; + break; + case 667: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 666; + break; + case 668: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 667; + break; + case 669: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 668; + break; + case 670: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 669; + break; + case 671: + if (curChar == 114) + jjAddStates(245, 246); + break; + case 674: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 671; + break; + case 675: + if (curChar == 101) + jjAddStates(295, 298); + break; + case 676: + if (curChar == 116) + jjAddStates(247, 248); + break; + case 679: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 676; + break; + case 680: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 679; + break; + case 681: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 680; + break; + case 682: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 681; + break; + case 683: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 682; + break; + case 684: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 685; + break; + case 686: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(351, 355); + break; + case 687: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(687, 688); + break; + case 689: + case 690: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(253, 255); + break; + case 692: + if (curChar == 123 && kind > 84) + kind = 84; + break; + case 693: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 684; + break; + case 694: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 693; + break; + case 695: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 694; + break; + case 696: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 695; + break; + case 697: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 696; + break; + case 698: + if (curChar == 116) + jjAddStates(256, 257); + break; + case 700: + if (curChar == 123 && kind > 89) + kind = 89; + break; + case 701: + if (curChar == 110) + jjstateSet[jjnewStateCnt++] = 698; + break; + case 702: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 701; + break; + case 703: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 702; + break; + case 704: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 703; + break; + case 705: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 704; + break; + case 706: + if (curChar == 121) + jjAddStates(258, 259); + break; + case 709: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 706; + break; + case 710: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 709; + break; + case 711: + if (curChar == 118) + jjstateSet[jjnewStateCnt++] = 710; + break; + case 712: + if (curChar == 116) + jjAddStates(292, 294); + break; + case 713: + if (curChar == 116) + jjAddStates(260, 261); + break; + case 715: + if (curChar == 123 && kind > 91) + kind = 91; + break; + case 716: + if (curChar == 120) + jjstateSet[jjnewStateCnt++] = 713; + break; + case 717: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 716; + break; + case 718: + if (curChar == 116) + jjAddStates(262, 263); + break; + case 721: + if (curChar == 120) + jjstateSet[jjnewStateCnt++] = 718; + break; + case 722: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 721; + break; + case 723: + if (curChar == 104) + jjAddStates(264, 265); + break; + case 726: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 723; + break; + case 727: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 726; + break; + case 728: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 727; + break; + case 729: + if (curChar == 119) + jjstateSet[jjnewStateCnt++] = 728; + break; + case 730: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 729; + break; + case 731: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 730; + break; + case 732: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 731; + break; + case 733: + if (curChar == 121) + jjstateSet[jjnewStateCnt++] = 732; + break; + case 734: + if (curChar == 105) + jjAddStates(289, 291); + break; + case 735: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 736; + break; + case 737: + if (curChar == 97 && kind > 97) + kind = 97; + break; + case 738: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 737; + break; + case 739: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 738; + break; + case 740: + if (curChar == 104) + jjstateSet[jjnewStateCnt++] = 739; + break; + case 741: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 740; + break; + case 742: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 741; + break; + case 743: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 735; + break; + case 744: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 743; + break; + case 745: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 744; + break; + case 746: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 745; + break; + case 747: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 748; + break; + case 749: + if (curChar == 101 && kind > 98) + kind = 98; + break; + case 750: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 749; + break; + case 751: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 750; + break; + case 752: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 751; + break; + case 753: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 752; + break; + case 754: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 753; + break; + case 755: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 747; + break; + case 756: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 755; + break; + case 757: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 756; + break; + case 758: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 757; + break; + case 759: + if (curChar == 102) + jjAddStates(270, 271); + break; + case 762: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 235) + kind = 235; + jjCheckNAddStates(277, 286); + break; + case 763: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(763, 764); + break; + case 766: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(766, 767); + break; + case 768: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(109, 111); + break; + case 769: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddStates(109, 111); + break; + case 772: + if ((0x7fffffe87fffffeL & l) != 0L) + jjCheckNAddTwoStates(772, 773); + break; + case 774: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + break; + case 775: + if ((0x7fffffe87fffffeL & l) == 0L) + break; + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + break; + case 776: + if (curChar == 118) + jjAddStates(287, 288); + break; + case 777: + if (curChar == 101) + jjAddStates(272, 273); + break; + case 779: + if (curChar == 123 && kind > 146) + kind = 146; + break; + case 780: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 777; + break; + case 781: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 780; + break; + case 782: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 781; + break; + case 783: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 782; + break; + case 784: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 783; + break; + case 785: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 784; + break; + case 786: + if (curChar == 101) + jjstateSet[jjnewStateCnt++] = 787; + break; + case 788: + if (curChar == 120 && kind > 147) + kind = 147; + break; + case 789: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 788; + break; + case 790: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 789; + break; + case 791: + if (curChar == 116 && kind > 147) + kind = 147; + break; + case 792: + if (curChar == 99) + jjstateSet[jjnewStateCnt++] = 791; + break; + case 793: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 792; + break; + case 794: + if (curChar == 114) + jjstateSet[jjnewStateCnt++] = 793; + break; + case 795: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 794; + break; + case 796: + if (curChar == 115) + jjstateSet[jjnewStateCnt++] = 795; + break; + case 797: + if (curChar == 116) + jjstateSet[jjnewStateCnt++] = 786; + break; + case 798: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 797; + break; + case 799: + if (curChar == 100) + jjstateSet[jjnewStateCnt++] = 798; + break; + case 800: + if (curChar == 105) + jjstateSet[jjnewStateCnt++] = 799; + break; + case 801: + if (curChar == 108) + jjstateSet[jjnewStateCnt++] = 800; + break; + case 802: + if (curChar == 97) + jjstateSet[jjnewStateCnt++] = 801; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 804: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(763, 764); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(766, 767); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(109, 111); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(772, 773); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + } + break; + case 96: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(763, 764); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(766, 767); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(109, 111); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(772, 773); + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + { + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + } + break; + case 19: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 235) + kind = 235; + jjCheckNAddStates(277, 286); + break; + case 1: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(0, 1); + break; + case 4: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(2, 3); + break; + case 59: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 104) + kind = 104; + jjCheckNAdd(60); + break; + case 60: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 104) + kind = 104; + jjCheckNAdd(60); + break; + case 99: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(342, 343); + break; + case 102: + if (jjCanMove_2(hiByte, i1, i2, l1, l2)) + jjAddStates(344, 345); + break; + case 155: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(346, 350); + break; + case 156: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(156, 157); + break; + case 158: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(165, 167); + break; + case 159: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(165, 167); + break; + case 526: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(226, 228); + break; + case 527: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(226, 228); + break; + case 686: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(351, 355); + break; + case 687: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(687, 688); + break; + case 689: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(253, 255); + break; + case 690: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(253, 255); + break; + case 763: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(763, 764); + break; + case 766: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(766, 767); + break; + case 768: + if (jjCanMove_0(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(109, 111); + break; + case 769: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddStates(109, 111); + break; + case 772: + if (jjCanMove_1(hiByte, i1, i2, l1, l2)) + jjCheckNAddTwoStates(772, 773); + break; + case 774: + if (!jjCanMove_0(hiByte, i1, i2, l1, l2)) + break; + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + break; + case 775: + if (!jjCanMove_1(hiByte, i1, i2, l1, l2)) + break; + if (kind > 235) + kind = 235; + jjCheckNAdd(775); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 803 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_20(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active3 & 0x4028000L) != 0L) + { + jjmatchedKind = 212; + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_20(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_20(jjStopStringLiteralDfa_20(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_20(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_20(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_20() +{ + switch(curChar) + { + case 39: + jjmatchedKind = 212; + return jjMoveStringLiteralDfa1_20(0x20000L); + case 123: + jjmatchedKind = 204; + return jjMoveStringLiteralDfa1_20(0x4000L); + case 125: + return jjMoveStringLiteralDfa1_20(0x8000L); + default : + return jjMoveNfa_20(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_20(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_20(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 39: + if ((active3 & 0x20000L) != 0L) + return jjStopAtPos(1, 209); + break; + case 123: + if ((active3 & 0x4000L) != 0L) + return jjStopAtPos(1, 206); + break; + case 125: + if ((active3 & 0x8000L) != 0L) + return jjStopAtPos(1, 207); + break; + default : + break; + } + return jjStartNfa_20(0, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_20(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 21; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffff00002600L & l) != 0L) + { + if (kind > 212) + kind = 212; + } + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 14; + if (curChar == 38) + jjAddStates(77, 80); + break; + case 2: + if (curChar == 59 && kind > 193) + kind = 193; + break; + case 14: + if (curChar == 35) + jjCheckNAddTwoStates(15, 17); + break; + case 15: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(15, 16); + break; + case 16: + if (curChar == 59 && kind > 194) + kind = 194; + break; + case 18: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(18, 16); + break; + case 19: + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 14; + break; + case 20: + if ((0xffffffff00002600L & l) != 0L && kind > 212) + kind = 212; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 212) + kind = 212; + break; + case 1: + if (curChar == 116) + jjCheckNAdd(2); + break; + case 3: + if (curChar == 108) + jjCheckNAdd(1); + break; + case 4: + if (curChar == 103) + jjCheckNAdd(1); + break; + case 5: + if (curChar == 111) + jjCheckNAdd(1); + break; + case 6: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 5; + break; + case 7: + if (curChar == 113) + jjstateSet[jjnewStateCnt++] = 6; + break; + case 8: + if (curChar == 97) + jjAddStates(81, 82); + break; + case 9: + if (curChar == 112) + jjCheckNAdd(2); + break; + case 10: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 9; + break; + case 11: + if (curChar == 115) + jjCheckNAdd(2); + break; + case 12: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 13: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 12; + break; + case 17: + if (curChar == 120) + jjCheckNAdd(18); + break; + case 18: + if ((0x7e0000007eL & l) != 0L) + jjCheckNAddTwoStates(18, 16); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 212) + kind = 212; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 21 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_11(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active3 & 0x800000000L) != 0L) + { + jjmatchedKind = 14; + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_11(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_11(jjStopStringLiteralDfa_11(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_11(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_11(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_11() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_11(0x800000000L); + case 42: + return jjStopAtPos(0, 131); + case 43: + return jjStopAtPos(0, 132); + case 63: + return jjStopAtPos(0, 130); + default : + return jjMoveNfa_11(1, 0); + } +} +private final int jjMoveStringLiteralDfa1_11(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_11(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + return jjStopAtPos(1, 227); + break; + default : + break; + } + return jjStartNfa_11(0, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_11(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 2; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if ((0x7ffff3ffffffffffL & l) != 0L) + { + if (kind > 14) + kind = 14; + } + if ((0x100002600L & l) != 0L) + { + if (kind > 12) + kind = 12; + jjCheckNAdd(0); + } + break; + case 0: + if ((0x100002600L & l) == 0L) + break; + if (kind > 12) + kind = 12; + jjCheckNAdd(0); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + kind = 14; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if (jjCanMove_2(hiByte, i1, i2, l1, l2) && kind > 14) + kind = 14; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 2 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_17(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + case 0: + if ((active3 & 0x8000L) != 0L) + { + jjmatchedKind = 211; + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_17(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_17(jjStopStringLiteralDfa_17(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_17(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_17(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_17() +{ + switch(curChar) + { + case 34: + jjmatchedKind = 173; + return jjMoveStringLiteralDfa1_17(0x10000L); + case 123: + jjmatchedKind = 204; + return jjMoveStringLiteralDfa1_17(0x4000L); + case 125: + return jjMoveStringLiteralDfa1_17(0x8000L); + default : + return jjMoveNfa_17(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_17(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_17(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 34: + if ((active3 & 0x10000L) != 0L) + return jjStopAtPos(1, 208); + break; + case 123: + if ((active3 & 0x4000L) != 0L) + return jjStopAtPos(1, 206); + break; + case 125: + if ((active3 & 0x8000L) != 0L) + return jjStopAtPos(1, 207); + break; + default : + break; + } + return jjStartNfa_17(0, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_17(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 21; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffff00002600L & l) != 0L) + { + if (kind > 211) + kind = 211; + } + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 14; + if (curChar == 38) + jjAddStates(77, 80); + break; + case 2: + if (curChar == 59 && kind > 193) + kind = 193; + break; + case 14: + if (curChar == 35) + jjCheckNAddTwoStates(15, 17); + break; + case 15: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(15, 16); + break; + case 16: + if (curChar == 59 && kind > 194) + kind = 194; + break; + case 18: + if ((0x3ff000000000000L & l) != 0L) + jjCheckNAddTwoStates(18, 16); + break; + case 19: + if (curChar == 38) + jjstateSet[jjnewStateCnt++] = 14; + break; + case 20: + if ((0xffffffff00002600L & l) != 0L && kind > 211) + kind = 211; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 211) + kind = 211; + break; + case 1: + if (curChar == 116) + jjCheckNAdd(2); + break; + case 3: + if (curChar == 108) + jjCheckNAdd(1); + break; + case 4: + if (curChar == 103) + jjCheckNAdd(1); + break; + case 5: + if (curChar == 111) + jjCheckNAdd(1); + break; + case 6: + if (curChar == 117) + jjstateSet[jjnewStateCnt++] = 5; + break; + case 7: + if (curChar == 113) + jjstateSet[jjnewStateCnt++] = 6; + break; + case 8: + if (curChar == 97) + jjAddStates(81, 82); + break; + case 9: + if (curChar == 112) + jjCheckNAdd(2); + break; + case 10: + if (curChar == 109) + jjstateSet[jjnewStateCnt++] = 9; + break; + case 11: + if (curChar == 115) + jjCheckNAdd(2); + break; + case 12: + if (curChar == 111) + jjstateSet[jjnewStateCnt++] = 11; + break; + case 13: + if (curChar == 112) + jjstateSet[jjnewStateCnt++] = 12; + break; + case 17: + if (curChar == 120) + jjCheckNAdd(18); + break; + case 18: + if ((0x7e0000007eL & l) != 0L) + jjCheckNAddTwoStates(18, 16); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (jjCanMove_3(hiByte, i1, i2, l1, l2) && kind > 211) + kind = 211; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 21 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_12(int pos, long active0, long active1, long active2, long active3) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_12(int pos, long active0, long active1, long active2, long active3) +{ + return jjMoveNfa_12(jjStopStringLiteralDfa_12(pos, active0, active1, active2, active3), pos + 1); +} +private final int jjStartNfaWithStates_12(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_12(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_12() +{ + switch(curChar) + { + case 40: + return jjMoveStringLiteralDfa1_12(0x800000000L); + case 41: + return jjStopAtPos(0, 139); + case 44: + return jjStopAtPos(0, 169); + case 63: + return jjStopAtPos(0, 53); + case 123: + return jjStopAtPos(0, 205); + default : + return jjMoveNfa_12(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_12(long active3) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_12(0, 0L, 0L, 0L, active3); + return 1; + } + switch(curChar) + { + case 58: + if ((active3 & 0x800000000L) != 0L) + return jjStopAtPos(1, 227); + break; + default : + break; + } + return jjStartNfa_12(0, 0L, 0L, 0L, active3); +} +private final int jjMoveNfa_12(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 1; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x100002600L & l) == 0L) + break; + kind = 12; + jjstateSet[jjnewStateCnt++] = 0; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + else + { + int hiByte = (int)(curChar >> 8); + int i1 = hiByte >> 6; + long l1 = 1L << (hiByte & 077); + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 1 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { + 1, 2, 4, 5, 2, 3, 2, 3, 5, 11, 12, 27, 28, 27, 28, 30, + 56, 57, 60, 17, 18, 41, 42, 47, 49, 64, 65, 75, 77, 85, 86, 93, + 95, 103, 105, 109, 110, 118, 119, 126, 127, 135, 137, 142, 143, 148, 149, 163, + 164, 180, 188, 182, 184, 194, 195, 197, 198, 199, 201, 161, 178, 192, 140, 145, + 124, 132, 101, 107, 115, 83, 90, 55, 72, 58, 59, 61, 62, 3, 4, 7, + 8, 10, 13, 16, 18, 32, 33, 37, 39, 59, 67, 61, 63, 74, 76, 84, + 86, 91, 99, 104, 109, 102, 112, 82, 88, 57, 71, 8, 15, 769, 770, 771, + 73, 74, 75, 77, 78, 80, 83, 84, 86, 89, 92, 93, 94, 94, 86, 89, + 7, 14, 21, 30, 38, 39, 47, 48, 62, 63, 67, 68, 77, 78, 80, 83, + 84, 86, 89, 85, 86, 89, 97, 98, 101, 105, 107, 116, 118, 126, 128, 144, + 145, 154, 155, 156, 157, 159, 160, 161, 170, 171, 181, 183, 188, 189, 196, 197, + 205, 207, 217, 219, 237, 245, 252, 260, 267, 280, 269, 274, 287, 305, 312, 324, + 331, 339, 346, 354, 361, 370, 377, 394, 379, 388, 401, 416, 403, 410, 423, 439, + 425, 433, 446, 447, 460, 461, 469, 479, 471, 472, 487, 489, 495, 497, 514, 516, + 525, 526, 527, 528, 529, 551, 552, 574, 575, 598, 600, 604, 605, 619, 620, 636, + 637, 642, 644, 661, 663, 672, 673, 677, 678, 685, 686, 687, 688, 690, 691, 692, + 699, 700, 707, 708, 714, 715, 719, 720, 724, 725, 736, 742, 748, 754, 760, 761, + 778, 779, 787, 790, 796, 763, 764, 766, 767, 769, 770, 771, 772, 773, 775, 785, + 802, 746, 758, 759, 717, 722, 733, 683, 697, 705, 711, 659, 670, 674, 602, 617, + 634, 639, 493, 512, 523, 549, 572, 595, 215, 235, 250, 265, 285, 310, 329, 344, + 359, 375, 399, 421, 444, 458, 467, 484, 186, 194, 202, 96, 114, 124, 142, 152, + 168, 178, 81, 82, 87, 88, 99, 100, 102, 103, 156, 157, 159, 160, 161, 687, + 688, 690, 691, 692, +}; +private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec2[i2] & l2) != 0L); + case 1: + return ((jjbitVec3[i2] & l2) != 0L); + case 2: + return ((jjbitVec4[i2] & l2) != 0L); + case 3: + return ((jjbitVec5[i2] & l2) != 0L); + case 4: + return ((jjbitVec6[i2] & l2) != 0L); + case 5: + return ((jjbitVec7[i2] & l2) != 0L); + case 6: + return ((jjbitVec8[i2] & l2) != 0L); + case 9: + return ((jjbitVec9[i2] & l2) != 0L); + case 10: + return ((jjbitVec10[i2] & l2) != 0L); + case 11: + return ((jjbitVec11[i2] & l2) != 0L); + case 12: + return ((jjbitVec12[i2] & l2) != 0L); + case 13: + return ((jjbitVec13[i2] & l2) != 0L); + case 14: + return ((jjbitVec14[i2] & l2) != 0L); + case 15: + return ((jjbitVec15[i2] & l2) != 0L); + case 16: + return ((jjbitVec16[i2] & l2) != 0L); + case 17: + return ((jjbitVec17[i2] & l2) != 0L); + case 30: + return ((jjbitVec18[i2] & l2) != 0L); + case 31: + return ((jjbitVec19[i2] & l2) != 0L); + case 33: + return ((jjbitVec20[i2] & l2) != 0L); + case 48: + return ((jjbitVec21[i2] & l2) != 0L); + case 49: + return ((jjbitVec22[i2] & l2) != 0L); + case 159: + return ((jjbitVec23[i2] & l2) != 0L); + case 215: + return ((jjbitVec24[i2] & l2) != 0L); + default : + if ((jjbitVec0[i1] & l1) != 0L) + return true; + return false; + } +} +private static final boolean jjCanMove_1(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec25[i2] & l2) != 0L); + case 1: + return ((jjbitVec3[i2] & l2) != 0L); + case 2: + return ((jjbitVec26[i2] & l2) != 0L); + case 3: + return ((jjbitVec27[i2] & l2) != 0L); + case 4: + return ((jjbitVec28[i2] & l2) != 0L); + case 5: + return ((jjbitVec29[i2] & l2) != 0L); + case 6: + return ((jjbitVec30[i2] & l2) != 0L); + case 9: + return ((jjbitVec31[i2] & l2) != 0L); + case 10: + return ((jjbitVec32[i2] & l2) != 0L); + case 11: + return ((jjbitVec33[i2] & l2) != 0L); + case 12: + return ((jjbitVec34[i2] & l2) != 0L); + case 13: + return ((jjbitVec35[i2] & l2) != 0L); + case 14: + return ((jjbitVec36[i2] & l2) != 0L); + case 15: + return ((jjbitVec37[i2] & l2) != 0L); + case 16: + return ((jjbitVec16[i2] & l2) != 0L); + case 17: + return ((jjbitVec17[i2] & l2) != 0L); + case 30: + return ((jjbitVec18[i2] & l2) != 0L); + case 31: + return ((jjbitVec19[i2] & l2) != 0L); + case 32: + return ((jjbitVec38[i2] & l2) != 0L); + case 33: + return ((jjbitVec20[i2] & l2) != 0L); + case 48: + return ((jjbitVec39[i2] & l2) != 0L); + case 49: + return ((jjbitVec22[i2] & l2) != 0L); + case 159: + return ((jjbitVec23[i2] & l2) != 0L); + case 215: + return ((jjbitVec24[i2] & l2) != 0L); + default : + if ((jjbitVec0[i1] & l1) != 0L) + return true; + return false; + } +} +private static final boolean jjCanMove_2(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec41[i2] & l2) != 0L); + default : + if ((jjbitVec40[i1] & l1) != 0L) + return true; + return false; + } +} +private static final boolean jjCanMove_3(int hiByte, int i1, int i2, long l1, long l2) +{ + switch(hiByte) + { + case 0: + return ((jjbitVec41[i2] & l2) != 0L); + case 255: + return ((jjbitVec43[i2] & l2) != 0L); + default : + if ((jjbitVec42[i1] & l1) != 0L) + return true; + return false; + } +} +public static final String[] jjstrLiteralImages = { +"", null, null, null, null, null, null, null, +"\145\156\143\157\144\151\156\147", null, null, null, null, null, null, "\74\77", "\74\77", "\77\76", null, null, +null, null, null, null, null, null, null, null, null, null, null, null, +"\157\162\144\145\162\145\144", "\165\156\157\162\144\145\162\145\144", null, null, "\171\145\163", +"\156\157", "\145\170\164\145\162\156\141\154", "\157\162", "\141\156\144", +"\144\151\166", "\151\144\151\166", "\155\157\144", "\52", "\151\156", null, null, null, +"\44", null, null, null, "\77", null, "\163\141\164\151\163\146\151\145\163", +"\162\145\164\165\162\156", "\164\150\145\156", "\145\154\163\145", "\144\145\146\141\165\154\164", null, +null, "\160\162\145\163\145\162\166\145", "\163\164\162\151\160", +"\156\141\155\145\163\160\141\143\145", null, "\164\157", "\167\150\145\162\145", +"\143\157\154\154\141\164\151\157\156", "\151\156\164\145\162\163\145\143\164", "\165\156\151\157\156", +"\145\170\143\145\160\164", "\141\163", "\141\164", "\143\141\163\145", null, null, null, null, null, null, +null, null, null, null, null, null, null, null, null, null, null, null, null, null, +null, null, null, null, null, null, "\52", "\52", null, null, "\57", "\57\57", +"\57", "\57\57", "\75", "\75", "\151\163", "\41\75", "\74\75", "\74\74", "\76\75", +"\76\76", "\145\161", "\156\145", "\147\164", "\147\145", "\154\164", "\154\145", +"\72\75", "\74", "\76", "\55", "\53", "\55", "\53", "\77", "\52", "\53", "\174", "\50", +"\100", "\133", "\135", "\51", "\51", null, null, null, null, null, null, null, null, +null, null, null, null, null, null, null, null, null, null, null, null, null, null, +null, null, null, null, null, null, "\54", "\54", "\73", "\45\45\45", "\42", "\42", +"\56", "\56\56", null, null, "\141\163\143\145\156\144\151\156\147", +"\144\145\163\143\145\156\144\151\156\147", null, null, null, null, null, null, null, null, null, null, +"\74\41\133\103\104\101\124\101\133", "\74\41\133\103\104\101\124\101\133", null, null, null, null, "\74", "\74", +"\76", "\57\76", "\74\57", "\76", "\75", null, "\173", "\173", "\173\173", +"\175\175", "\42\42", "\47\47", null, null, null, null, null, null, null, "\47", "\47", +null, null, null, null, null, null, null, null, null, null, null, null, null, +"\74\41\55\55", "\74\41\55\55", "\55\55\76", null, null, null, null, null, null, "\175", null, +null, null, null, null, null, null, null, null, }; +public static final String[] lexStateNames = { + "DEFAULT", + "OPERATOR", + "KINDTESTFORPI", + "XQUERYVERSION", + "ITEMTYPE", + "NAMESPACEDECL", + "NAMESPACEKEYWORD", + "KINDTEST", + "XMLSPACE_DECL", + "SINGLETYPE", + "VARNAME", + "OCCURRENCEINDICATOR", + "CLOSEKINDTEST", + "ELEMENT_CONTENT", + "PROCESSING_INSTRUCTION", + "PROCESSING_INSTRUCTION_CONTENT", + "START_TAG", + "QUOT_ATTRIBUTE_CONTENT", + "EXT_NAME", + "CDATA_SECTION", + "APOS_ATTRIBUTE_CONTENT", + "END_TAG", + "XML_COMMENT", + "EXPR_COMMENT", + "EXT_CONTENT", + "EXT_KEY", +}; +public static final int[] jjnewLexState = { + -1, 1, 1, 1, 1, -1, 3, -1, -1, 0, 0, 5, -1, -1, 1, 14, 14, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, 10, + 1, -1, 0, -1, 1, 0, 0, 0, 0, -1, 8, 5, 0, 0, 5, 5, 0, 0, -1, 0, 0, 0, 4, 0, 4, + 4, 9, 11, 7, 7, 7, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 5, -1, 6, 6, 1, 6, 6, -1, + -1, 1, 12, 1, 1, -1, -1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -1, -1, 1, 1, 1, 0, 0, -1, 0, -1, 1, -1, 10, 10, 10, 10, 9, 4, -1, -1, -1, 7, + 7, -1, 7, 7, 7, 2, 7, -1, 7, 7, -1, 7, 2, 7, 7, 7, -1, -1, 0, 7, 0, 0, 17, 16, 1, + 1, 0, 0, -1, -1, -1, -1, 10, 11, 1, 12, 24, -1, -1, -1, 19, 19, -1, -1, -1, -1, 16, 16, 13, -1, + 21, -1, -1, -1, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 20, 16, -1, -1, -1, 25, -1, -1, + -1, -1, 23, -1, -1, 18, 18, 22, 22, -1, 1, -1, -1, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 1, +}; +static final long[] jjtoToken = { + 0xfff67fffffff8fffL, 0xffffffe7ffffffffL, 0xfbffffffffefffffL, 0x26f0007fffff7L, +}; +static final long[] jjtoSkip = { + 0x5000L, 0x0L, 0x400000000000000L, 0x60080f9c0000000L, +}; +static final long[] jjtoSpecial = { + 0x0L, 0x0L, 0x400000000000000L, 0x80f9c0000000L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[803]; +private final int[] jjstateSet = new int[1606]; +StringBuffer image; +int jjimageLen; +int lengthOfMatch; +protected char curChar; +public XPathTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} +public XPathTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 803; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 26 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + image = null; + jjimageLen = 0; + + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + case 3: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_3(); + break; + case 4: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_4(); + break; + case 5: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_5(); + break; + case 6: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_6(); + break; + case 7: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_7(); + break; + case 8: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_8(); + break; + case 9: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_9(); + break; + case 10: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_10(); + break; + case 11: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_11(); + break; + case 12: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_12(); + break; + case 13: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_13(); + break; + case 14: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_14(); + break; + case 15: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_15(); + break; + case 16: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_16(); + break; + case 17: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_17(); + break; + case 18: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_18(); + break; + case 19: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_19(); + break; + case 20: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_20(); + break; + case 21: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_21(); + break; + case 22: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_22(); + break; + case 23: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_23(); + break; + case 24: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_24(); + break; + case 25: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_25(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + TokenLexicalActions(matchedToken); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else + { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + SkipLexicalActions(matchedToken); + } + else + SkipLexicalActions(null); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } +} + +void SkipLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + case 14 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + input_stream.backup(1); + break; + case 222 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(); + break; + case 224 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + popState(); + break; + case 227 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(); + break; + case 229 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + popState(); + break; + default : + break; + } +} +void TokenLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + case 15 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[15]); + pushState(OPERATOR); + break; + case 16 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[16]); + pushState(); + break; + case 17 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[17]); + popState(); + break; + case 78 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 79 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 80 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 81 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 82 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 83 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 84 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 85 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 86 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 87 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 88 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 89 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 90 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 91 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 139 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[139]); + popState(); + break; + case 146 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 147 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 149 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 150 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 151 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 152 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 153 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 154 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 155 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OPERATOR); + break; + case 156 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 157 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(KINDTEST); + break; + case 158 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 159 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 160 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(KINDTEST); + break; + case 161 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 162 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 163 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 164 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 165 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + pushState(OCCURRENCEINDICATOR); + break; + case 190 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[190]); + pushState(OPERATOR); + break; + case 191 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[191]); + pushState(); + break; + case 192 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + popState(); + break; + case 196 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[196]); + pushState(); + break; + case 197 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[197]); + pushState(OPERATOR); + break; + case 199 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[199]); + popState(); + break; + case 201 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[201]); + popState(); + break; + case 204 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[204]); + pushState(); + break; + case 205 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[205]); + pushState(OPERATOR); + break; + case 232 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[232]); + pushState(OPERATOR); + break; + case 233 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[233]); + pushState(); + break; + case 234 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[234]); + popState(); + break; + case 241 : + if (image == null) + image = new StringBuffer(); + image.append(jjstrLiteralImages[241]); + popState(); + break; + default : + break; + } +} +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathTreeConstants.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathTreeConstants.java new file mode 100644 index 00000000000..f268eff8add --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathTreeConstants.java @@ -0,0 +1,551 @@ +/* Generated By:JJTree: Do not edit this line. /home/jukka/src/jackrabbit/jackrabbit-spi-commons/target/generated-sources/jjtree/org/apache/jackrabbit/spi/commons/query/xpath/XPathTreeConstants.java */ + +package org.apache.jackrabbit.spi.commons.query.xpath; + +public interface XPathTreeConstants +{ + public int JJTXPATH2 = 0; + public int JJTQUERYLIST = 1; + public int JJTMODULE = 2; + public int JJTVERSIONDECL = 3; + public int JJTXQUERYVERSION = 4; + public int JJTSTRINGLITERALFORVERSION = 5; + public int JJTXQUERYENCODING = 6; + public int JJTMAINMODULE = 7; + public int JJTLIBRARYMODULE = 8; + public int JJTMODULEDECL = 9; + public int JJTMODULENAMESPACE = 10; + public int JJTNCNAMEFORPREFIX = 11; + public int JJTASSIGNEQUALS = 12; + public int JJTURLLITERAL = 13; + public int JJTPROLOG = 14; + public int JJTSETTER = 15; + public int JJTIMPORT = 16; + public int JJTSEPARATOR = 17; + public int JJTNAMESPACEDECL = 18; + public int JJTDECLARENAMESPACE = 19; + public int JJTXMLSPACEDECL = 20; + public int JJTDECLAREXMLSPACE = 21; + public int JJTXMLSPACEPRESERVE = 22; + public int JJTXMLSPACESTRIP = 23; + public int JJTDEFAULTNAMESPACEDECL = 24; + public int JJTDECLAREDEFAULTELEMENT = 25; + public int JJTDECLAREDEFAULTFUNCTION = 26; + public int JJTNAMESPACE = 27; + public int JJTORDERINGMODEDECL = 28; + public int JJTDECLAREORDERING = 29; + public int JJTORDERED = 30; + public int JJTUNORDERED = 31; + public int JJTEMPTYORDERINGDECL = 32; + public int JJTDECLAREDEFAULTORDERINGEMPTY = 33; + public int JJTEMPTYGREATEST = 34; + public int JJTEMPTYLEAST = 35; + public int JJTINHERITNAMESPACESDECL = 36; + public int JJTDECLAREINHERITNAMESPACES = 37; + public int JJTYES = 38; + public int JJTNO = 39; + public int JJTDEFAULTCOLLATIONDECL = 40; + public int JJTDECLARECOLLATION = 41; + public int JJTBASEURIDECL = 42; + public int JJTDECLAREBASEURI = 43; + public int JJTSCHEMAIMPORT = 44; + public int JJTIMPORTSCHEMATOKEN = 45; + public int JJTATSTRINGLITERAL = 46; + public int JJTSTRINGLITERAL = 47; + public int JJTSCHEMAPREFIX = 48; + public int JJTDEFAULTELEMENT = 49; + public int JJTMODULEIMPORT = 50; + public int JJTIMPORTMODULETOKEN = 51; + public int JJTVARDECL = 52; + public int JJTDEFINEVARIABLE = 53; + public int JJTVARNAME = 54; + public int JJTCOLONEQUALS = 55; + public int JJTEXTERNAL = 56; + public int JJTCONSTRUCTIONDECL = 57; + public int JJTDECLARECONSTRUCTION = 58; + public int JJTSCHEMAMODEFORDECLARECONSTRUCTION = 59; + public int JJTFUNCTIONDECL = 60; + public int JJTDEFINEFUNCTION = 61; + public int JJTQNAMELPAR = 62; + public int JJTAS = 63; + public int JJTPARAMLIST = 64; + public int JJTPARAM = 65; + public int JJTENCLOSEDEXPR = 66; + public int JJTLBRACE = 67; + public int JJTLBRACEEXPRENCLOSURE = 68; + public int JJTRBRACE = 69; + public int JJTQUERYBODY = 70; + public int JJTEXPR = 71; + public int JJTVOID = 72; + public int JJTFLWOREXPR = 73; + public int JJTIN = 74; + public int JJTPOSITIONALVAR = 75; + public int JJTATWORD = 76; + public int JJTLETCLAUSE = 77; + public int JJTLETVARIABLE = 78; + public int JJTWHERECLAUSE = 79; + public int JJTWHERE = 80; + public int JJTORDERBYCLAUSE = 81; + public int JJTORDERBY = 82; + public int JJTORDERBYSTABLE = 83; + public int JJTORDERSPECLIST = 84; + public int JJTORDERSPEC = 85; + public int JJTORDERMODIFIER = 86; + public int JJTASCENDING = 87; + public int JJTDESCENDING = 88; + public int JJTCOLLATION = 89; + public int JJTQUANTIFIEDEXPR = 90; + public int JJTSOME = 91; + public int JJTEVERY = 92; + public int JJTSATISFIES = 93; + public int JJTTYPESWITCHEXPR = 94; + public int JJTDEFAULT = 95; + public int JJTCASECLAUSE = 96; + public int JJTCASE = 97; + public int JJTIFEXPR = 98; + public int JJTOREXPR = 99; + public int JJTANDEXPR = 100; + public int JJTCOMPARISONEXPR = 101; + public int JJTRANGEEXPR = 102; + public int JJTADDITIVEEXPR = 103; + public int JJTMULTIPLICATIVEEXPR = 104; + public int JJTUNIONEXPR = 105; + public int JJTINTERSECTEXCEPTEXPR = 106; + public int JJTINSTANCEOFEXPR = 107; + public int JJTTREATEXPR = 108; + public int JJTCASTABLEEXPR = 109; + public int JJTCASTEXPR = 110; + public int JJTCASTAS = 111; + public int JJTUNARYEXPR = 112; + public int JJTUNARYMINUS = 113; + public int JJTUNARYPLUS = 114; + public int JJTVALIDATEEXPR = 115; + public int JJTVALIDATELBRACE = 116; + public int JJTVALIDATESCHEMAMODE = 117; + public int JJTPATHEXPR = 118; + public int JJTROOT = 119; + public int JJTROOTDESCENDANTS = 120; + public int JJTSLASHSLASH = 121; + public int JJTSTEPEXPR = 122; + public int JJTAXISCHILD = 123; + public int JJTAXISDESCENDANT = 124; + public int JJTAXISATTRIBUTE = 125; + public int JJTAXISSELF = 126; + public int JJTAXISDESCENDANTORSELF = 127; + public int JJTAXISFOLLOWINGSIBLING = 128; + public int JJTAXISFOLLOWING = 129; + public int JJTAT = 130; + public int JJTAXISPARENT = 131; + public int JJTAXISANCESTOR = 132; + public int JJTAXISPRECEDINGSIBLING = 133; + public int JJTAXISPRECEDING = 134; + public int JJTAXISANCESTORORSELF = 135; + public int JJTDOTDOT = 136; + public int JJTNODETEST = 137; + public int JJTNAMETEST = 138; + public int JJTQNAME = 139; + public int JJTSTAR = 140; + public int JJTNCNAMECOLONSTAR = 141; + public int JJTSTARCOLONNCNAME = 142; + public int JJTPREDICATELIST = 143; + public int JJTPREDICATE = 144; + public int JJTINTEGERLITERAL = 145; + public int JJTDECIMALLITERAL = 146; + public int JJTDOUBLELITERAL = 147; + public int JJTDOT = 148; + public int JJTORDEREDOPEN = 149; + public int JJTUNORDEREDOPEN = 150; + public int JJTFUNCTIONCALL = 151; + public int JJTCONSTRUCTOR = 152; + public int JJTDIRECTCONSTRUCTOR = 153; + public int JJTDIRELEMCONSTRUCTOR = 154; + public int JJTSTARTTAGOPENROOT = 155; + public int JJTSTARTTAGOPEN = 156; + public int JJTTAGQNAME = 157; + public int JJTEMPTYTAGCLOSE = 158; + public int JJTSTARTTAGCLOSE = 159; + public int JJTENDTAGOPEN = 160; + public int JJTS = 161; + public int JJTENDTAGCLOSE = 162; + public int JJTDIRATTRIBUTELIST = 163; + public int JJTVALUEINDICATOR = 164; + public int JJTDIRATTRIBUTEVALUE = 165; + public int JJTOPENQUOT = 166; + public int JJTESCAPEQUOT = 167; + public int JJTCLOSEQUOT = 168; + public int JJTOPENAPOS = 169; + public int JJTESCAPEAPOS = 170; + public int JJTCLOSEAPOS = 171; + public int JJTQUOTATTRVALUECONTENT = 172; + public int JJTQUOTATTRCONTENTCHAR = 173; + public int JJTAPOSATTRVALUECONTENT = 174; + public int JJTAPOSATTRCONTENTCHAR = 175; + public int JJTDIRELEMCONTENT = 176; + public int JJTELEMENTCONTENTCHAR = 177; + public int JJTCOMMONCONTENT = 178; + public int JJTPREDEFINEDENTITYREF = 179; + public int JJTCHARREF = 180; + public int JJTLCURLYBRACEESCAPE = 181; + public int JJTRCURLYBRACEESCAPE = 182; + public int JJTDIRCOMMENTCONSTRUCTOR = 183; + public int JJTXMLCOMMENTSTARTFORELEMENTCONTENT = 184; + public int JJTXMLCOMMENTSTART = 185; + public int JJTXMLCOMMENTEND = 186; + public int JJTDIRCOMMENTCONTENTS = 187; + public int JJTCOMMENTCONTENTCHAR = 188; + public int JJTCOMMENTCONTENTCHARDASH = 189; + public int JJTDIRPICONSTRUCTOR = 190; + public int JJTPROCESSINGINSTRUCTIONSTARTFORELEMENTCONTENT = 191; + public int JJTPROCESSINGINSTRUCTIONSTART = 192; + public int JJTPITARGET = 193; + public int JJTSFORPI = 194; + public int JJTPROCESSINGINSTRUCTIONEND = 195; + public int JJTDIRPICONTENTS = 196; + public int JJTPICONTENTCHAR = 197; + public int JJTCDATASECTION = 198; + public int JJTCDATASECTIONSTARTFORELEMENTCONTENT = 199; + public int JJTCDATASECTIONSTART = 200; + public int JJTCDATASECTIONEND = 201; + public int JJTCDATASECTIONCONTENTS = 202; + public int JJTCDATASECTIONCHAR = 203; + public int JJTCOMPUTEDCONSTRUCTOR = 204; + public int JJTCOMPDOCCONSTRUCTOR = 205; + public int JJTDOCUMENTLBRACE = 206; + public int JJTCOMPELEMCONSTRUCTOR = 207; + public int JJTELEMENTQNAMELBRACE = 208; + public int JJTELEMENTLBRACE = 209; + public int JJTCONTENTEXPR = 210; + public int JJTCOMPATTRCONSTRUCTOR = 211; + public int JJTATTRIBUTEQNAMELBRACE = 212; + public int JJTATTRIBUTELBRACE = 213; + public int JJTCOMPTEXTCONSTRUCTOR = 214; + public int JJTTEXTLBRACE = 215; + public int JJTCOMPCOMMENTCONSTRUCTOR = 216; + public int JJTCOMMENTLBRACE = 217; + public int JJTCOMPPICONSTRUCTOR = 218; + public int JJTPINCNAMELBRACE = 219; + public int JJTPILBRACE = 220; + public int JJTSINGLETYPE = 221; + public int JJTOCCURRENCEZEROORONE = 222; + public int JJTTYPEDECLARATION = 223; + public int JJTSEQUENCETYPE = 224; + public int JJTEMPTYTOK = 225; + public int JJTOCCURRENCEZEROORMORE = 226; + public int JJTOCCURRENCEONEORMORE = 227; + public int JJTITEM = 228; + public int JJTATOMICTYPE = 229; + public int JJTQNAMEFORATOMICTYPE = 230; + public int JJTQNAMEFORSEQUENCETYPE = 231; + public int JJTANYKINDTEST = 232; + public int JJTNODELPARFORKINDTEST = 233; + public int JJTDOCUMENTTEST = 234; + public int JJTDOCUMENTLPAR = 235; + public int JJTDOCUMENTLPARFORKINDTEST = 236; + public int JJTTEXTTEST = 237; + public int JJTTEXTLPARFORKINDTEST = 238; + public int JJTCOMMENTTEST = 239; + public int JJTCOMMENTLPARFORKINDTEST = 240; + public int JJTPITEST = 241; + public int JJTPROCESSINGINSTRUCTIONLPARFORKINDTEST = 242; + public int JJTNCNAMEFORPI = 243; + public int JJTSTRINGLITERALFORKINDTEST = 244; + public int JJTATTRIBUTETEST = 245; + public int JJTATTRIBUTETYPE = 246; + public int JJTATTRIBUTETYPEFORKINDTEST = 247; + public int JJTCOMMAFORKINDTEST = 248; + public int JJTATTRIBNAMEORWILDCARD = 249; + public int JJTANYNAME = 250; + public int JJTSCHEMAATTRIBUTETEST = 251; + public int JJTSCHEMAATTRIBUTETYPE = 252; + public int JJTSCHEMAATTRIBUTETYPEFORKINDTEST = 253; + public int JJTATTRIBUTEDECLARATION = 254; + public int JJTELEMENTTEST = 255; + public int JJTELEMENTTYPE = 256; + public int JJTELEMENTTYPEFORKINDTEST = 257; + public int JJTELEMENTTYPEFORDOCUMENTTEST = 258; + public int JJTNILLABLE = 259; + public int JJTELEMENTNAMEORWILDCARD = 260; + public int JJTSCHEMAELEMENTTEST = 261; + public int JJTSCHEMAELEMENTTYPE = 262; + public int JJTSCHEMAELEMENTTYPEFORKINDTEST = 263; + public int JJTSCHEMAELEMENTTYPEFORDOCUMENTTEST = 264; + public int JJTELEMENTDECLARATION = 265; + public int JJTATTRIBUTENAME = 266; + public int JJTQNAMEFORITEMTYPE = 267; + public int JJTELEMENTNAME = 268; + public int JJTTYPENAME = 269; + + + public String[] jjtNodeName = { + "XPath2", + "QueryList", + "Module", + "VersionDecl", + "XQueryVersion", + "StringLiteralForVersion", + "XQueryEncoding", + "MainModule", + "LibraryModule", + "ModuleDecl", + "ModuleNamespace", + "NCNameForPrefix", + "AssignEquals", + "URLLiteral", + "Prolog", + "Setter", + "Import", + "Separator", + "NamespaceDecl", + "DeclareNamespace", + "XMLSpaceDecl", + "DeclareXMLSpace", + "XMLSpacePreserve", + "XMLSpaceStrip", + "DefaultNamespaceDecl", + "DeclareDefaultElement", + "DeclareDefaultFunction", + "Namespace", + "OrderingModeDecl", + "DeclareOrdering", + "Ordered", + "Unordered", + "EmptyOrderingDecl", + "DeclareDefaultOrderingEmpty", + "EmptyGreatest", + "EmptyLeast", + "InheritNamespacesDecl", + "DeclareInheritNamespaces", + "Yes", + "No", + "DefaultCollationDecl", + "DeclareCollation", + "BaseURIDecl", + "DeclareBaseURI", + "SchemaImport", + "ImportSchemaToken", + "AtStringLiteral", + "StringLiteral", + "SchemaPrefix", + "DefaultElement", + "ModuleImport", + "ImportModuleToken", + "VarDecl", + "DefineVariable", + "VarName", + "ColonEquals", + "External", + "ConstructionDecl", + "DeclareConstruction", + "SchemaModeForDeclareConstruction", + "FunctionDecl", + "DefineFunction", + "QNameLpar", + "As", + "ParamList", + "Param", + "EnclosedExpr", + "Lbrace", + "LbraceExprEnclosure", + "Rbrace", + "QueryBody", + "Expr", + "void", + "FLWORExpr", + "In", + "PositionalVar", + "AtWord", + "LetClause", + "LetVariable", + "WhereClause", + "Where", + "OrderByClause", + "OrderBy", + "OrderByStable", + "OrderSpecList", + "OrderSpec", + "OrderModifier", + "Ascending", + "Descending", + "Collation", + "QuantifiedExpr", + "Some", + "Every", + "Satisfies", + "TypeswitchExpr", + "Default", + "CaseClause", + "Case", + "IfExpr", + "OrExpr", + "AndExpr", + "ComparisonExpr", + "RangeExpr", + "AdditiveExpr", + "MultiplicativeExpr", + "UnionExpr", + "IntersectExceptExpr", + "InstanceofExpr", + "TreatExpr", + "CastableExpr", + "CastExpr", + "CastAs", + "UnaryExpr", + "UnaryMinus", + "UnaryPlus", + "ValidateExpr", + "ValidateLbrace", + "ValidateSchemaMode", + "PathExpr", + "Root", + "RootDescendants", + "SlashSlash", + "StepExpr", + "AxisChild", + "AxisDescendant", + "AxisAttribute", + "AxisSelf", + "AxisDescendantOrSelf", + "AxisFollowingSibling", + "AxisFollowing", + "At", + "AxisParent", + "AxisAncestor", + "AxisPrecedingSibling", + "AxisPreceding", + "AxisAncestorOrSelf", + "DotDot", + "NodeTest", + "NameTest", + "QName", + "Star", + "NCNameColonStar", + "StarColonNCName", + "PredicateList", + "Predicate", + "IntegerLiteral", + "DecimalLiteral", + "DoubleLiteral", + "Dot", + "OrderedOpen", + "UnorderedOpen", + "FunctionCall", + "Constructor", + "DirectConstructor", + "DirElemConstructor", + "StartTagOpenRoot", + "StartTagOpen", + "TagQName", + "EmptyTagClose", + "StartTagClose", + "EndTagOpen", + "S", + "EndTagClose", + "DirAttributeList", + "ValueIndicator", + "DirAttributeValue", + "OpenQuot", + "EscapeQuot", + "CloseQuot", + "OpenApos", + "EscapeApos", + "CloseApos", + "QuotAttrValueContent", + "QuotAttrContentChar", + "AposAttrValueContent", + "AposAttrContentChar", + "DirElemContent", + "ElementContentChar", + "CommonContent", + "PredefinedEntityRef", + "CharRef", + "LCurlyBraceEscape", + "RCurlyBraceEscape", + "DirCommentConstructor", + "XmlCommentStartForElementContent", + "XmlCommentStart", + "XmlCommentEnd", + "DirCommentContents", + "CommentContentChar", + "CommentContentCharDash", + "DirPIConstructor", + "ProcessingInstructionStartForElementContent", + "ProcessingInstructionStart", + "PITarget", + "SForPI", + "ProcessingInstructionEnd", + "DirPIContents", + "PIContentChar", + "CDataSection", + "CdataSectionStartForElementContent", + "CdataSectionStart", + "CdataSectionEnd", + "CDataSectionContents", + "CDataSectionChar", + "ComputedConstructor", + "CompDocConstructor", + "DocumentLbrace", + "CompElemConstructor", + "ElementQNameLbrace", + "ElementLbrace", + "ContentExpr", + "CompAttrConstructor", + "AttributeQNameLbrace", + "AttributeLbrace", + "CompTextConstructor", + "TextLbrace", + "CompCommentConstructor", + "CommentLbrace", + "CompPIConstructor", + "PINCNameLbrace", + "PILbrace", + "SingleType", + "OccurrenceZeroOrOne", + "TypeDeclaration", + "SequenceType", + "EmptyTok", + "OccurrenceZeroOrMore", + "OccurrenceOneOrMore", + "Item", + "AtomicType", + "QNameForAtomicType", + "QNameForSequenceType", + "AnyKindTest", + "NodeLparForKindTest", + "DocumentTest", + "DocumentLpar", + "DocumentLparForKindTest", + "TextTest", + "TextLparForKindTest", + "CommentTest", + "CommentLparForKindTest", + "PITest", + "ProcessingInstructionLparForKindTest", + "NCNameForPI", + "StringLiteralForKindTest", + "AttributeTest", + "AttributeType", + "AttributeTypeForKindTest", + "CommaForKindTest", + "AttribNameOrWildcard", + "AnyName", + "SchemaAttributeTest", + "SchemaAttributeType", + "SchemaAttributeTypeForKindTest", + "AttributeDeclaration", + "ElementTest", + "ElementType", + "ElementTypeForKindTest", + "ElementTypeForDocumentTest", + "Nillable", + "ElementNameOrWildcard", + "SchemaElementTest", + "SchemaElementType", + "SchemaElementTypeForKindTest", + "SchemaElementTypeForDocumentTest", + "ElementDeclaration", + "AttributeName", + "QNameForItemType", + "ElementName", + "TypeName", + }; +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathVisitor.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathVisitor.java new file mode 100644 index 00000000000..fc851a15de4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/XPathVisitor.java @@ -0,0 +1,8 @@ +/* Generated By:JJTree: Do not edit this line. /home/jukka/src/jackrabbit/jackrabbit-spi-commons/target/generated-sources/jjtree/org/apache/jackrabbit/spi/commons/query/xpath/XPathVisitor.java */ + +package org.apache.jackrabbit.spi.commons.query.xpath; + +public interface XPathVisitor +{ + public Object visit(SimpleNode node, Object data); +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/package-info.java new file mode 100644 index 00000000000..6ac4419985e --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/query/xpath/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.1") +package org.apache.jackrabbit.spi.commons.query.xpath; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/tree/AbstractTree.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/tree/AbstractTree.java new file mode 100644 index 00000000000..b55f7f0c24d --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/tree/AbstractTree.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.spi.commons.tree; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +public abstract class AbstractTree implements Tree { + + private final Name nodeName; + private final Name ntName; + private final String uniqueId; + + private final NamePathResolver resolver; + + private List children; + + protected AbstractTree(Name nodeName, Name ntName, String uniqueId, NamePathResolver resolver) { + this.nodeName = nodeName; + this.ntName = ntName; + this.uniqueId = uniqueId; + this.children = new ArrayList(); + this.resolver = resolver; + } + + protected NamePathResolver getResolver() { + return resolver; + } + + protected List getChildren() { + return children; + } + + protected abstract Tree createChild(Name name, Name primaryTypeName, String uniqueId); + + //---------------------------------------------------------------< Tree >--- + @Override + public Name getName() { + return nodeName; + } + + @Override + public Name getPrimaryTypeName() { + return ntName; + } + + @Override + public String getUniqueId() { + return uniqueId; + } + + @Override + public Tree addChild(Name childName, Name primaryTypeName, String uniqueId) { + Tree child = createChild(childName, primaryTypeName, uniqueId); + children.add(child); + + return child; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/tree/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/tree/package-info.java new file mode 100755 index 00000000000..daccd5079b2 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/tree/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.spi.commons.tree; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/util/StringCache.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/util/StringCache.java new file mode 100644 index 00000000000..db50c636e95 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/util/StringCache.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.util; + +import java.lang.ref.SoftReference; + +/** + * A few String utility functions. + */ +public class StringCache { + + public static final boolean OBJECT_CACHE = getBooleanSetting("jackrabbit.stringCache", true); + public static final int OBJECT_CACHE_SIZE = nextPowerOf2(getIntSetting("jackrabbit.stringCacheSize", 1024)); + + private static SoftReference softCache = new SoftReference(null); + + private StringCache() { + // utility class + } + + private static int nextPowerOf2(int x) { + long i = 1; + while (i < x && i < (Integer.MAX_VALUE / 2)) { + i += i; + } + return (int) i; + } + + private static boolean getBooleanSetting(String name, boolean defaultValue) { + String s = getProperty(name); + if (s != null) { + try { + return Boolean.valueOf(s).booleanValue(); + } catch (NumberFormatException e) { + // ignore + } + } + return defaultValue; + } + + private static int getIntSetting(String name, int defaultValue) { + String s = getProperty(name); + if (s != null) { + try { + return Integer.decode(s).intValue(); + } catch (NumberFormatException e) { + // ignore + } + } + return defaultValue; + } + + private static String getProperty(String name) { + try { + return System.getProperty(name); + } catch (Exception e) { + // SecurityException + // applets may not do that - ignore + return null; + } + } + + private static String[] getCache() { + String[] cache; + // softCache can be null due to a Tomcat problem + // a workaround is disable the system property org.apache. + // catalina.loader.WebappClassLoader.ENABLE_CLEAR_REFERENCES + if (softCache != null) { + cache = softCache.get(); + if (cache != null) { + return cache; + } + } + try { + cache = new String[OBJECT_CACHE_SIZE]; + } catch (OutOfMemoryError e) { + return null; + } + softCache = new SoftReference(cache); + return cache; + } + + /** + * Get the string from the cache if possible. If the string has not been + * found, it is added to the cache. If there is such a string in the cache, + * that one is returned. + * + * @param s the original string + * @return a string with the same content, if possible from the cache + */ + public static String cache(String s) { + if (!OBJECT_CACHE) { + return s; + } + if (s == null) { + return s; + } else if (s.length() == 0) { + return ""; + } + int hash = s.hashCode(); + String[] cache = getCache(); + if (cache != null) { + int index = hash & (OBJECT_CACHE_SIZE - 1); + String cached = cache[index]; + if (cached != null) { + if (s.equals(cached)) { + return cached; + } + } + cache[index] = s; + } + return s; + } + + /** + * Get a string from the cache, and if no such string has been found, create + * a new one with only this content. This solves out of memory problems if + * the string is a substring of another, large string. In Java, strings are + * shared, which could lead to memory problems. This avoid such problems. + * + * @param s the string + * @return a string that is guaranteed not be a substring of a large string + */ + public static String fromCacheOrNew(String s) { + if (!OBJECT_CACHE) { + return s; + } + if (s == null) { + return s; + } else if (s.length() == 0) { + return ""; + } + int hash = s.hashCode(); + String[] cache = getCache(); + int index = hash & (OBJECT_CACHE_SIZE - 1); + if (cache == null) { + return s; + } + String cached = cache[index]; + if (cached != null) { + if (s.equals(cached)) { + return cached; + } + } + // create a new object that is not shared + // (to avoid out of memory if it is a substring of a big String) + // NOPMD + s = new String(s); + cache[index] = s; + return s; + } + + /** + * Clear the cache. This method is used for testing. + */ + public static void clearCache() { + softCache = new SoftReference(null); + } + +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/util/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/util/package-info.java new file mode 100644 index 00000000000..4d5d9131178 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/util/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.util; diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/AbstractQValue.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/AbstractQValue.java new file mode 100644 index 00000000000..6d9a14b62f0 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/AbstractQValue.java @@ -0,0 +1,465 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.value; + +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.util.ISO8601; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.Binary; + +import java.util.Calendar; +import java.util.TimeZone; +import java.math.BigDecimal; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.io.InputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.Writer; + +/** + * AbstractQValue... + */ +public abstract class AbstractQValue implements QValue, Serializable { + + private static final long serialVersionUID = 6976433831974695272L; + + protected final Object val; + protected final int type; + + /** + * Create a new AbstractQValue. + * + * @param value The value. + * @param type The property type. + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(Object value, int type) { + if (value == null) { + throw new IllegalArgumentException("null value"); + } + this.val = value; + this.type = type; + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @param type + * @throws IllegalArgumentException if the passed value + * is null or if the type is neither STRING nor + * REFERENCE/WEAKREFERENCE. + */ + protected AbstractQValue(String value, int type) { + if (value == null) { + throw new IllegalArgumentException("null value"); + } + if (!(type == PropertyType.STRING + || type == PropertyType.DATE // JCR-3083 + || type == PropertyType.REFERENCE + || type == PropertyType.WEAKREFERENCE)) { + throw new IllegalArgumentException(); + } + this.val = value; + this.type = type; + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(Long value) { + this(value, PropertyType.LONG); + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(Double value) { + this(value, PropertyType.DOUBLE); + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(Boolean value) { + this(value, PropertyType.BOOLEAN); + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(Calendar value) { + val = ISO8601.format(value); + type = PropertyType.DATE; + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(Name value) { + this(value, PropertyType.NAME); + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(Path value) { + this(value, PropertyType.PATH); + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(BigDecimal value) { + this(value, PropertyType.DECIMAL); + } + + /** + * Create a new AbstractQValue. + * + * @param value + * @throws IllegalArgumentException if the passed value + * is null. + */ + protected AbstractQValue(URI value) { + this(value, PropertyType.URI); + } + + //---------------------------------------------------------< QValue >--- + /** + * @see QValue#getType() + */ + public int getType() { + return type; + } + + /** + * @see QValue#getLength() + */ + public long getLength() throws RepositoryException { + return getString().length(); + } + + /** + * @see QValue#getName() + */ + public Name getName() throws RepositoryException { + if (type == PropertyType.NAME) { + return (Name) val; + } else { + try { + return AbstractQValueFactory.NAME_FACTORY.create(getString()); + } catch (IllegalArgumentException e) { + throw new ValueFormatException("not a valid Name value: " + getString(), e); + } + } + } + + /** + * @see QValue#getCalendar() + */ + public Calendar getCalendar() throws RepositoryException { + if (type == PropertyType.DOUBLE) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + cal.setTimeInMillis(((Double) val).longValue()); + return cal; + } else if (type == PropertyType.LONG) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + cal.setTimeInMillis((Long) val); + return cal; + } else if (type == PropertyType.DECIMAL) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + cal.setTimeInMillis(((BigDecimal) val).longValue()); + return cal; + } else { + Calendar cal = ISO8601.parse(getString()); + if (cal == null) { + throw new ValueFormatException("not a date string: " + getString()); + } else { + return cal; + } + } + } + + /** + * @see QValue#getDecimal() + */ + public BigDecimal getDecimal() throws RepositoryException { + if (type == PropertyType.DECIMAL) { + return (BigDecimal) val; + } else if (type == PropertyType.DOUBLE) { + return new BigDecimal((Double) val); + } else if (type == PropertyType.LONG) { + return new BigDecimal((Long) val); + } else if (type == PropertyType.DATE) { + return new BigDecimal(((Calendar) val).getTimeInMillis()); + } else { + try { + return new BigDecimal(getString()); + } catch (NumberFormatException e) { + throw new ValueFormatException("not a valid decimal string: " + getString(), e); + } + } + } + + /** + * @see QValue#getURI() + */ + public URI getURI() throws RepositoryException { + if (type == PropertyType.URI) { + return (URI) val; + } else { + try { + return URI.create(getString()); + } catch (IllegalArgumentException e) { + throw new ValueFormatException("not a valid uri: " + getString(), e); + } + } + } + + /** + * @see QValue#getDouble() + */ + public double getDouble() throws RepositoryException { + if (type == PropertyType.DOUBLE) { + return (Double) val; + } else if (type == PropertyType.LONG) { + return ((Long) val).doubleValue(); + } else if (type == PropertyType.DATE) { + return getCalendar().getTimeInMillis(); + } else if (type == PropertyType.DECIMAL) { + return ((BigDecimal) val).doubleValue(); + } else { + try { + return Double.parseDouble(getString()); + } catch (NumberFormatException ex) { + throw new ValueFormatException("not a double: " + getString(), ex); + } + } + } + + /** + * @see QValue#getLong() + */ + public long getLong() throws RepositoryException { + if (type == PropertyType.LONG) { + return (Long) val; + } else if (type == PropertyType.DOUBLE) { + return ((Double) val).longValue(); + } else if (type == PropertyType.DECIMAL) { + return ((BigDecimal) val).longValue(); + } else if (type == PropertyType.DATE) { + return getCalendar().getTimeInMillis(); + } else { + try { + return Long.parseLong(getString()); + } catch (NumberFormatException ex) { + throw new ValueFormatException("not a long: " + getString(), ex); + } + } + } + + /** + * @throws RepositoryException + * @see QValue#getBoolean() + */ + public boolean getBoolean() throws RepositoryException { + if (type == PropertyType.BOOLEAN) { + return (Boolean) val; + } else { + return Boolean.valueOf(getString()); + } + } + + /** + * @see QValue#getPath() + */ + public Path getPath() throws RepositoryException { + if (type == PropertyType.PATH) { + return (Path) val; + } else { + try { + return AbstractQValueFactory.PATH_FACTORY.create(getString()); + } catch (IllegalArgumentException e) { + throw new ValueFormatException("not a valid Path: " + getString(), e); + } + } + } + + /** + * @see QValue#getPath() + */ + public String getString() throws RepositoryException { + if (type == PropertyType.BINARY) { + try { + InputStream stream = getStream(); + try { + Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8); + Writer writer = new StringWriter(); + char[] buffer = new char[1024]; + int n = reader.read(buffer); + while (n != -1) { + writer.write(buffer, 0, n); + n = reader.read(buffer); + } + return writer.toString(); + } finally { + stream.close(); + } + } catch (IOException e) { + throw new RepositoryException("conversion from stream to string failed", e); + } + } else if (type == PropertyType.DATE) { + return (String) val; + } else { + return val.toString(); + } + } + + /** + * This implementation creates a binary instance that uses + * {@link #getStream()} and skipping on the given stream as its underlying + * mechanism to provide random access defined on {@link Binary}. + * + * @see QValue#getBinary() + */ + public Binary getBinary() throws RepositoryException { + return new Binary() { + public InputStream getStream() throws RepositoryException { + return AbstractQValue.this.getStream(); + } + + public int read(byte[] b, long position) throws IOException, RepositoryException { + InputStream in = getStream(); + try { + long skip = position; + while (skip > 0) { + long skipped = in.skip(skip); + if (skipped <= 0) { + return -1; + } + skip -= skipped; + } + return in.read(b); + } finally { + in.close(); + } + } + + public long getSize() throws RepositoryException { + return getLength(); + } + + public void dispose() { + } + }; + } + + /** + * @see QValue#discard() + */ + public void discard() { + // nothing to do + } + + //-------------------------------------------------------------< Object >--- + /** + * Returns the string representation of this internal value. + * + * @return string representation of this internal value + */ + @Override + public String toString() { + if (type == PropertyType.DATE) { + return (String) val; + } else { + return val.toString(); + } + } + + /** + * Default implementation of the equals method. Subclasses may optimize + * this e.g. by special handling for DATE properties. + * + * @param obj + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof AbstractQValue) { + AbstractQValue other = (AbstractQValue) obj; + if (type != other.type) { + return false; + } + return val.equals(other.val); + } + return false; + } + + /** + * Default calculation of the hashCode. Subclasses may optimize + * this e.g. by special handling for DATE properties. + * + * @return the hashCode of the internal value object. + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return val.hashCode(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/AbstractQValueFactory.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/AbstractQValueFactory.java new file mode 100644 index 00000000000..c47cd746a52 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/AbstractQValueFactory.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.value; + +import java.util.Calendar; +import java.util.UUID; +import java.math.BigDecimal; +import java.net.URI; +import java.io.UnsupportedEncodingException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.util.ISO8601; + +/** + * AbstractQValueFactory... + */ +public abstract class AbstractQValueFactory implements QValueFactory { + + /** + * the default encoding + */ + public static final String DEFAULT_ENCODING = "UTF-8"; + + protected static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + protected static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + + + //------------------------------------------------------< QValueFactory >--- + /** + * @see QValueFactory#computeAutoValues(org.apache.jackrabbit.spi.QPropertyDefinition) + */ + public QValue[] computeAutoValues(QPropertyDefinition propertyDefinition) throws RepositoryException { + Name declaringNT = propertyDefinition.getDeclaringNodeType(); + Name name = propertyDefinition.getName(); + + if (NameConstants.JCR_UUID.equals(name) + && NameConstants.MIX_REFERENCEABLE.equals(declaringNT)) { + // jcr:uuid property of a mix:referenceable + return new QValue[]{create(UUID.randomUUID().toString(), PropertyType.STRING)}; + + } else { + throw new RepositoryException("createFromDefinition not implemented for: " + name); + } + } + + /** + * @see QValueFactory#create(String, int) + */ + public QValue create(String value, int type) throws RepositoryException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + + try { + switch (type) { + case PropertyType.BOOLEAN: + return create(Boolean.valueOf(value)); + case PropertyType.DATE: { + Calendar cal = ISO8601.parse(value); + if (cal == null) { + throw new ValueFormatException("not a valid date: " + value); + } + return create(cal); + } + case PropertyType.DOUBLE: + return create(Double.valueOf(value)); + case PropertyType.LONG: + return create(Long.valueOf(value)); + case PropertyType.DECIMAL: + return create(new BigDecimal(value)); + case PropertyType.URI: + return create(URI.create(value)); + case PropertyType.PATH: + return create(PATH_FACTORY.create(value)); + case PropertyType.NAME: + return create(NAME_FACTORY.create(value)); + case PropertyType.STRING: + return createString(value); + case PropertyType.REFERENCE: + return createReference(value, false); + case PropertyType.WEAKREFERENCE: + return createReference(value, true); + case PropertyType.BINARY: + return create(value.getBytes(DEFAULT_ENCODING)); + // default: invalid type specified -> see below. + } + } catch (IllegalArgumentException ex) { + // given String value cannot be converted to Long/Double/Path/Name + throw new ValueFormatException(ex); + } catch (UnsupportedEncodingException ex) { + throw new RepositoryException(ex); + } + + // invalid type specified: + throw new IllegalArgumentException("illegal type " + type); + } + + /** + * @see QValueFactory#create(Calendar) + */ + public QValue create(Calendar value) throws RepositoryException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new DefaultQValue(value); + } + + /** + * @see QValueFactory#create(double) + */ + public QValue create(double value) throws RepositoryException { + return new DefaultQValue(value); + } + + /** + * @see QValueFactory#create(long) + */ + public QValue create(long value) throws RepositoryException { + return new DefaultQValue(value); + } + + /** + * @see QValueFactory#create(boolean) + */ + public QValue create(boolean value) throws RepositoryException { + if (value) { + return DefaultQValue.TRUE; + } else { + return DefaultQValue.FALSE; + } + } + + /** + * @see QValueFactory#create(Name) + */ + public QValue create(Name value) throws RepositoryException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new DefaultQValue(value); + } + + /** + * @see QValueFactory#create(Path) + */ + public QValue create(Path value) throws RepositoryException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new DefaultQValue(value); + } + + /** + * @see QValueFactory#create(URI) + */ + public QValue create(URI value) throws RepositoryException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new DefaultQValue(value); + } + + /** + * @see QValueFactory#create(URI) + */ + public QValue create(BigDecimal value) throws RepositoryException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new DefaultQValue(value); + } + + /** + * Creates a new QValue of type STRING. + * + * @param value the string value. + * @return a new QValue. + */ + protected QValue createString(String value) { + return new DefaultQValue(value, PropertyType.STRING); + } + + /** + * Creates a new QValue of type REFERENCE or WEAKREFERENCE. + * + * @param ref the reference value. + * @param weak whether the reference is weak. + * @return a new QValue. + */ + protected QValue createReference(String ref, boolean weak) { + if (weak) { + return new DefaultQValue(ref, PropertyType.WEAKREFERENCE); + } else { + return new DefaultQValue(ref, PropertyType.REFERENCE); + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/DefaultQValue.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/DefaultQValue.java new file mode 100644 index 00000000000..ecbf7661015 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/DefaultQValue.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.spi.commons.value; + +import java.io.Serializable; +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.net.URI; +import java.util.Calendar; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** + * QValue implementation for all valid PropertyTypes + * except for BINARY. + */ +public class DefaultQValue extends AbstractQValue implements Serializable { + + private static final long serialVersionUID = -3887529703765183611L; + + protected static final QValue TRUE = new DefaultQValue(Boolean.TRUE); + protected static final QValue FALSE = new DefaultQValue(Boolean.FALSE); + + public DefaultQValue(String value, int type) { + super(value, type); + } + + public DefaultQValue(Long value) { + super(value); + } + + public DefaultQValue(Double value) { + super(value); + } + + public DefaultQValue(BigDecimal value) { + super(value); + } + + public DefaultQValue(Boolean value) { + super(value); + } + + public DefaultQValue(Name value) { + super(value); + } + + public DefaultQValue(Path value) { + super(value); + } + + public DefaultQValue(URI value) { + super(value); + } + + protected DefaultQValue(Calendar value) { + super(value); + } + + //-------------------------------------------------------------< QValue >--- + + /** + * @see QValue#getStream() + */ + public InputStream getStream() throws RepositoryException { + try { + // convert via string + return new ByteArrayInputStream(getString().getBytes( + AbstractQValueFactory.DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(QValueFactoryImpl.DEFAULT_ENCODING + + " is not supported encoding on this platform", e); + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/QValueFactoryImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/QValueFactoryImpl.java new file mode 100644 index 00000000000..3c7ed3fc6c2 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/QValueFactoryImpl.java @@ -0,0 +1,422 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.value; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.util.TransientFileFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.io.ByteArrayInputStream; +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.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Arrays; + +/** + * QValueFactoryImpl... + */ +public class QValueFactoryImpl extends AbstractQValueFactory { + + private static final QValueFactory INSTANCE = new QValueFactoryImpl(); + + protected QValueFactoryImpl() { + } + + public static QValueFactory getInstance() { + return INSTANCE; + } + + /** + * @see QValueFactory#create(byte[]) + */ + public QValue create(byte[] value) { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new BinaryQValue(value); + } + + /** + * @see QValueFactory#create(InputStream) + */ + public QValue create(InputStream value) throws IOException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new BinaryQValue(value); + } + + /** + * @see QValueFactory#create(File) + */ + public QValue create(File value) throws IOException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new BinaryQValue(value); + } + + //--------------------------------------------------------< Inner Class >--- + + /** + * BinaryQValue represents a binary Value which is + * backed by a resource or byte[]. Unlike BinaryValue it has no + * state, i.e. the getStream() method always returns a fresh + * InputStream instance. + */ + private static class BinaryQValue extends AbstractQValue implements Serializable { + + /** + * A dummy value for calling the constructor of AbstractQValue + */ + private static final Object DUMMY_VALUE = new Serializable() { + private static final long serialVersionUID = 2849470089518940117L; + }; + + /** + * empty array + */ + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * max size for keeping tmp data in memory + */ + private static final int MAX_BUFFER_SIZE = 0x10000; + + /** + * underlying file + */ + private transient File file; + + /** + * flag indicating if this instance represents a temporary value + * whose dynamically allocated resources can be explicitly freed on + * {@link #discard()}. + */ + private transient boolean temp; + + /** + * Buffer for small-sized data + */ + private byte[] buffer = BinaryQValue.EMPTY_BYTE_ARRAY; + + /** + * Creates a new BinaryQValue instance from an + * InputStream. The contents of the stream is spooled + * to a temporary file or to a byte buffer if its size is smaller than + * {@link #MAX_BUFFER_SIZE}. + *

        + * The new instance represents a temporary value whose dynamically + * allocated resources will be freed explicitly on {@link #discard()}. + * + * @param in stream to be represented as a BinaryQValue instance + * @throws IOException if an error occurs while reading from the stream or + * writing to the temporary file + */ + private BinaryQValue(InputStream in) throws IOException { + this(in, true); + } + + /** + * Creates a new BinaryQValue instance from an + * InputStream. The contents of the stream is spooled + * to a temporary file or to a byte buffer if its size is smaller than + * {@link #MAX_BUFFER_SIZE}. + *

        + * The temp parameter governs whether dynamically allocated + * resources will be freed explicitly on {@link #discard()}. Note that any + * dynamically allocated resources (temp file/buffer) will be freed + * implicitly once this instance has been gc'ed. + * + * @param in stream to be represented as a BinaryQValue instance + * @param temp flag indicating whether this instance represents a + * temporary value whose resources can be explicitly freed + * on {@link #discard()}. + * @throws IOException if an error occurs while reading from the stream or + * writing to the temporary file + */ + private BinaryQValue(InputStream in, boolean temp) throws IOException { + super(DUMMY_VALUE, PropertyType.BINARY); + byte[] spoolBuffer = new byte[0x2000]; + int read; + int len = 0; + OutputStream out = null; + File spoolFile = null; + try { + while ((read = in.read(spoolBuffer)) > 0) { + if (out != null) { + // spool to temp file + out.write(spoolBuffer, 0, read); + len += read; + } else if (len + read > BinaryQValue.MAX_BUFFER_SIZE) { + // threshold for keeping data in memory exceeded; + // create temp file and spool buffer contents + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + spoolFile = fileFactory.createTransientFile("bin", null, null); + out = new FileOutputStream(spoolFile); + out.write(buffer, 0, len); + out.write(spoolBuffer, 0, read); + buffer = null; + len += read; + } else { + // reallocate new buffer and spool old buffer contents + byte[] newBuffer = new byte[len + read]; + System.arraycopy(buffer, 0, newBuffer, 0, len); + System.arraycopy(spoolBuffer, 0, newBuffer, len, read); + buffer = newBuffer; + len += read; + } + } + } finally { + in.close(); + if (out != null) { + out.close(); + } + } + + // init vars + file = spoolFile; + this.temp = temp; + // buffer is EMPTY_BYTE_ARRAY (default value) + } + + /** + * Creates a new BinaryQValue instance from a + * byte[] array. + * + * @param bytes byte array to be represented as a BinaryQValue + * instance + */ + private BinaryQValue(byte[] bytes) { + super(DUMMY_VALUE, PropertyType.BINARY); + buffer = bytes; + file = null; + // this instance is not backed by a temporarily allocated buffer + temp = false; + } + + /** + * Creates a new BinaryQValue instance from a File. + * + * @param file file to be represented as a BinaryQValue instance + * @throws IOException if the file can not be read + */ + private BinaryQValue(File file) throws IOException { + super(DUMMY_VALUE, PropertyType.BINARY); + String path = file.getCanonicalPath(); + if (!file.isFile()) { + throw new IOException(path + ": the specified file does not exist"); + } + if (!file.canRead()) { + throw new IOException(path + ": the specified file can not be read"); + } + // this instance is backed by a 'real' file + this.file = file; + // this instance is not backed by temporarily allocated resource/buffer + temp = false; + // buffer is EMPTY_BYTE_ARRAY (default value) + } + + //---------------------------------------------------------< QValue >--- + + /** + * Returns the length of this BinaryQValue. + * + * @return The length, in bytes, of this BinaryQValue, + * or -1L if the length can't be determined. + * @see QValue#getLength() + */ + @Override + public long getLength() { + if (file != null) { + // this instance is backed by a 'real' file + if (file.exists()) { + return file.length(); + } else { + return -1; + } + } else { + // this instance is backed by an in-memory buffer + return buffer.length; + } + } + + /** + * @see QValue#getStream() + */ + public InputStream getStream() throws RepositoryException { + // always return a 'fresh' stream + if (file != null) { + // this instance is backed by a 'real' file + try { + return new FileInputStream(file); + } catch (FileNotFoundException fnfe) { + throw new RepositoryException("file backing binary value not found", + fnfe); + } + } else { + return new ByteArrayInputStream(buffer); + } + } + + /** + * @see QValue#getName() + */ + @Override + public Name getName() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + /** + * @see QValue#getPath() + */ + @Override + public Path getPath() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + /** + * Frees temporarily allocated resources such as temporary file, buffer, etc. + * If this BinaryQValue is backed by a persistent resource + * calling this method will have no effect. + * @see QValue#discard() + */ + @Override + public void discard() { + if (!temp) { + // do nothing if this instance is not backed by temporarily + // allocated resource/buffer + return; + } + if (file != null) { + // this instance is backed by a temp file + file.delete(); + } else if (buffer != null) { + // this instance is backed by an in-memory buffer + buffer = EMPTY_BYTE_ARRAY; + } + } + + //-----------------------------------------------< java.lang.Object >--- + /** + * Returns a string representation of this BinaryQValue + * instance. The string representation of a resource backed value is + * the path of the underlying resource. If this instance is backed by an + * in-memory buffer the generic object string representation of the byte + * array will be used instead. + * + * @return A string representation of this BinaryQValue instance. + */ + @Override + public String toString() { + if (file != null) { + // this instance is backed by a 'real' file + return file.toString(); + } else { + // this instance is backed by an in-memory buffer + return buffer.toString(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BinaryQValue) { + BinaryQValue other = (BinaryQValue) obj; + return ((file == null ? other.file == null : file.equals(other.file)) + && Arrays.equals(buffer, other.buffer)); + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return 0; + } + + //---------------------------------------------------< Serializable >--- + private void writeObject(ObjectOutputStream out) + throws IOException { + out.defaultWriteObject(); + // write hasFile marker + out.writeBoolean(file != null); + // then write file if necessary + if (file != null) { + byte[] buffer = new byte[4096]; + int bytes; + InputStream stream = new FileInputStream(file); + while ((bytes = stream.read(buffer)) >= 0) { + // Write a segment of the input stream + if (bytes > 0) { + // just to ensure that no 0 is written + out.writeInt(bytes); + out.write(buffer, 0, bytes); + } + } + // Write the end of stream marker + out.writeInt(0); + // close stream + stream.close(); + } + } + + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + boolean hasFile = in.readBoolean(); + if (hasFile) { + file = File.createTempFile("binary-qvalue", "bin"); + + OutputStream out = new FileOutputStream(file); + byte[] buffer = new byte[4096]; + for (int bytes = in.readInt(); bytes > 0; bytes = in.readInt()) { + if (buffer.length < bytes) { + buffer = new byte[bytes]; + } + in.readFully(buffer, 0, bytes); + out.write(buffer, 0, bytes); + } + out.close(); + } + // deserialized value is always temp + temp = true; + } + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/QValueValue.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/QValueValue.java new file mode 100644 index 00000000000..2f06a1a0bbb --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/QValueValue.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.value; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +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.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.QValue; + +/** + * A QValueValue provides an implementation + * of the Value interface representing an SPI + * QValue. + */ +public final class QValueValue implements Value { + + // wrapped QValue + private final QValue qvalue; + + // used for keeping track of input streams that have already been passed back + private InputStream stream = null; + + // for converting the internal NAME/PATH format to JCR format + private final NamePathResolver resolver; + + /** + * Constructs a QValueValue object representing an SPI + * QValue. + * + * @param qvalue the QValue this QValueValue should represent + * @param resolver fore resolving namespace URIs to prefixes in NAME/PATH properties + */ + public QValueValue(QValue qvalue, NamePathResolver resolver) { + this.qvalue = qvalue; + this.resolver = resolver; + } + + /** + * Returns the embedded QValue. + * + * @return the embedded QValue + */ + public QValue getQValue() { + return qvalue; + } + + //--------------------------------------------------------------< Value >--- + /** + * @see javax.jcr.Value#getBoolean() + */ + public boolean getBoolean() throws RepositoryException { + if (getType() == PropertyType.STRING || getType() == PropertyType.BINARY || getType() == PropertyType.BOOLEAN) { + return Boolean.valueOf(qvalue.getString()); + } else { + throw new ValueFormatException("incompatible type " + PropertyType.nameFromValue(qvalue.getType())); + } + } + + /** + * @see javax.jcr.Value#getDecimal() + */ + public BigDecimal getDecimal() throws ValueFormatException, IllegalStateException, RepositoryException { + switch (getType()) { + case PropertyType.DECIMAL: + case PropertyType.DOUBLE: + case PropertyType.LONG: + case PropertyType.DATE: + case PropertyType.STRING: + return qvalue.getDecimal(); + default: + throw new ValueFormatException("incompatible type " + PropertyType.nameFromValue(qvalue.getType())); + } + } + + /** + * @see javax.jcr.Value#getBinary() + */ + public Binary getBinary() throws RepositoryException { + // JCR-2511 Value#getBinary() and #getStream() return internal representation for type PATH and NAME + if (getType() == PropertyType.NAME || getType() == PropertyType.PATH) { + // qualified name/path value needs to be resolved, + // delegate conversion to getString() method + final byte[] value = getString().getBytes(StandardCharsets.UTF_8); + return new Binary() { + public int read(byte[] b, long position) { + if (position >= value.length) { + return -1; + } else { + int p = (int) position; + int n = Math.min(b.length, value.length - p); + System.arraycopy(value, p, b, 0, n); + return n; + } + } + public InputStream getStream() { + return new ByteArrayInputStream(value); + } + public long getSize() { + return value.length; + } + public void dispose() { + } + }; + } else { + return qvalue.getBinary(); + } + } + + /** + * @see javax.jcr.Value#getDate() + */ + public Calendar getDate() throws RepositoryException { + return qvalue.getCalendar(); + } + + /** + * @see javax.jcr.Value#getDouble() + */ + public double getDouble() throws RepositoryException { + return qvalue.getDouble(); + } + + /** + * @see javax.jcr.Value#getLong() + */ + public long getLong() throws RepositoryException { + return qvalue.getLong(); + } + + /** + * @see javax.jcr.Value#getStream() + */ + public InputStream getStream() throws IllegalStateException, RepositoryException { + if (stream == null) { + if (getType() == PropertyType.NAME || getType() == PropertyType.PATH) { + // qualified name/path value needs to be resolved + stream = new ByteArrayInputStream(getString().getBytes(StandardCharsets.UTF_8)); + } else { + stream = qvalue.getStream(); + } + } + return stream; + } + + /** + * @see javax.jcr.Value#getString() + */ + public String getString() throws RepositoryException { + if (getType() == PropertyType.NAME) { + // qualified name value needs to be resolved + return resolver.getJCRName(qvalue.getName()); + } else if (getType() == PropertyType.PATH) { + // qualified path value needs to be resolved + return resolver.getJCRPath(qvalue.getPath()); + } else { + return qvalue.getString(); + } + } + + /** + * @see javax.jcr.Value#getType() + */ + public int getType() { + return qvalue.getType(); + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof QValueValue) { + return qvalue.equals(((QValueValue) obj).qvalue); + } else { + return false; + } + } + + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return qvalue.hashCode(); + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/ValueFactoryQImpl.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/ValueFactoryQImpl.java new file mode 100644 index 00000000000..7e03997fab4 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/ValueFactoryQImpl.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.commons.value; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Calendar; + +import javax.jcr.Binary; +import javax.jcr.NamespaceException; +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 org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.util.ISO8601; + +/** + * This class implements the ValueFactory interface, + * wrapping an existing SPI QValueFactory and a + * NamePathResolver. + * + * @see ValueFactory + * @see QValueFactory + */ +public class ValueFactoryQImpl implements ValueFactory { + + private final QValueFactory qfactory; + private final NamePathResolver resolver; + + /** + * Constructs a new ValueFactoryQImpl based + * on an existing SPI QValueFactory and a + * NamePathResolver. + * @param qfactory wrapped QValueFactory + * @param resolver wrapped NamePathResolver + */ + public ValueFactoryQImpl(QValueFactory qfactory, NamePathResolver resolver) { + this.qfactory = qfactory; + this.resolver = resolver; + } + + /** + * The QValueFactory that is wrapped by this ValueFactory + * instance. + * + * @return qfactory The QValueFactory wrapped by this instance. + */ + public QValueFactory getQValueFactory() { + return qfactory; + } + + /** + * Create a new Value based on an existing + * QValue + * @param qvalue existing QValue + * @return a Value representing the QValue + */ + public Value createValue(QValue qvalue) { + return new QValueValue(qvalue, resolver); + } + + //---------------------------------------------------------< ValueFactory > + + /** + * {@inheritDoc} + */ + public Value createValue(String value) { + try { + QValue qvalue = qfactory.create(value, PropertyType.STRING); + return new QValueValue(qvalue, resolver); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + /** + * {@inheritDoc} + */ + public Value createValue(long value) { + try { + QValue qvalue = qfactory.create(value); + return new QValueValue(qvalue, resolver); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + /** + * {@inheritDoc} + */ + public Value createValue(double value) { + try { + QValue qvalue = qfactory.create(value); + return new QValueValue(qvalue, resolver); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + /** + * {@inheritDoc} + */ + public Value createValue(boolean value) { + try { + QValue qvalue = qfactory.create(value); + return new QValueValue(qvalue, resolver); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + /** + * {@inheritDoc} + */ + public Value createValue(Calendar value) { + try { + ISO8601.getYear(value); + QValue qvalue = qfactory.create(value); + return new QValueValue(qvalue, resolver); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + /** + * {@inheritDoc} + */ + public Value createValue(InputStream value) { + try { + try { + QValue qvalue = qfactory.create(value); + return new QValueValue(qvalue, resolver); + } finally { + value.close(); // JCR-2903 + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + /** + * {@inheritDoc} + */ + public Value createValue(Node value) throws RepositoryException { + return createValue(value, false); + } + + /** + * {@inheritDoc} + */ + public Value createValue(String value, int type) throws ValueFormatException { + try { + QValue qvalue; + + if (type == PropertyType.NAME) { + Name name = resolver.getQName(value); + qvalue = qfactory.create(name); + } else if (type == PropertyType.PATH) { + Path path = resolver.getQPath(value, false); + qvalue = qfactory.create(path); + } else { + qvalue = qfactory.create(value, type); + } + + return new QValueValue(qvalue, resolver); + } catch (IllegalNameException ex) { + throw new ValueFormatException(ex); + } catch (MalformedPathException ex) { + throw new ValueFormatException(ex); + } catch (NamespaceException ex) { + throw new ValueFormatException(ex); + } catch (ValueFormatException ex) { + throw ex; + } catch (RepositoryException ex) { + throw new ValueFormatException(ex); + } + } + + public Binary createBinary(InputStream stream) throws RepositoryException { + // TODO review/optimize/refactor + try { + try { + QValue qvalue = qfactory.create(stream); + return qvalue.getBinary(); + } finally { + stream.close(); // JCR-2903 + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + public Value createValue(Binary value) { + // TODO review/optimize/refactor + try { + return createValue(value.getStream()); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + public Value createValue(BigDecimal value) { + try { + QValue qvalue = qfactory.create(value); + return new QValueValue(qvalue, resolver); + } catch (RepositoryException ex) { + throw new RuntimeException(ex); + } + } + + public Value createValue(Node value, boolean weak) throws RepositoryException { + QValue qvalue = qfactory.create(value.getUUID(), weak ? PropertyType.WEAKREFERENCE : PropertyType.REFERENCE); + return new QValueValue(qvalue, resolver); + } + +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/ValueFormat.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/ValueFormat.java new file mode 100644 index 00000000000..d8c9edc2411 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/ValueFormat.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.spi.commons.value; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.PropertyType; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +/** + * ValueFormat... + */ +public class ValueFormat { + + /** + * + * @param jcrValue + * @param resolver + * @param factory + * @return + * @throws RepositoryException + */ + public static QValue getQValue(Value jcrValue, NamePathResolver resolver, + QValueFactory factory) throws RepositoryException { + if (jcrValue == null) { + throw new IllegalArgumentException("null value"); + } else if (jcrValue instanceof QValueValue) { + return ((QValueValue)jcrValue).getQValue(); + } else if (jcrValue.getType() == PropertyType.BINARY) { + // TODO: jsr 283 binary property conversion + try { + //return factory.create(jcrValue.getBinary()); + return factory.create(jcrValue.getStream()); + } catch (IOException e) { + throw new RepositoryException(e); + } + } else if (jcrValue.getType() == PropertyType.DATE) { + return factory.create(jcrValue.getDate()); + } else if (jcrValue.getType() == PropertyType.DOUBLE) { + return factory.create(jcrValue.getDouble()); + } else if (jcrValue.getType() == PropertyType.LONG) { + return factory.create(jcrValue.getLong()); + } else if (jcrValue.getType() == PropertyType.DECIMAL) { + return factory.create(jcrValue.getDecimal()); + } else { + return getQValue(jcrValue.getString(), jcrValue.getType(), resolver, factory); + } + } + + /** + * + * @param jcrValues + * @param resolver + * @param factory + * @return + * @throws RepositoryException + */ + public static QValue[] getQValues(Value[] jcrValues, + NamePathResolver resolver, + QValueFactory factory) throws RepositoryException { + if (jcrValues == null) { + throw new IllegalArgumentException("null value"); + } + List qValues = new ArrayList(); + for (Value jcrValue : jcrValues) { + if (jcrValue != null) { + qValues.add(getQValue(jcrValue, resolver, factory)); + } + } + return qValues.toArray(new QValue[qValues.size()]); + } + + /** + * + * @param jcrValue + * @param propertyType + * @param resolver + * @param factory + * @return + * @throws RepositoryException + */ + public static QValue getQValue(String jcrValue, int propertyType, + NamePathResolver resolver, + QValueFactory factory) throws RepositoryException { + QValue qValue; + switch (propertyType) { + case PropertyType.STRING: + case PropertyType.BOOLEAN: + case PropertyType.DOUBLE: + case PropertyType.LONG: + case PropertyType.DECIMAL: + case PropertyType.DATE: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + case PropertyType.URI: + qValue = factory.create(jcrValue, propertyType); + break; + case PropertyType.BINARY: + qValue = factory.create(jcrValue.getBytes()); + break; + case PropertyType.NAME: + Name qName = resolver.getQName(jcrValue); + qValue = factory.create(qName); + break; + case PropertyType.PATH: + Path qPath = resolver.getQPath(jcrValue, false); + qValue = factory.create(qPath); + break; + default: + throw new IllegalArgumentException("Invalid property type."); + } + return qValue; + } + + /** + * @param value + * @param resolver + * @param factory + * @return the JCR value created from the given QValue. + * @throws RepositoryException + */ + public static Value getJCRValue(QValue value, + NamePathResolver resolver, + ValueFactory factory) throws RepositoryException { + if (factory instanceof ValueFactoryQImpl) { + return ((ValueFactoryQImpl)factory).createValue(value); + } else { + Value jcrValue; + int propertyType = value.getType(); + switch (propertyType) { + case PropertyType.STRING: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + case PropertyType.URI: + jcrValue = factory.createValue(value.getString(), propertyType); + break; + case PropertyType.PATH: + Path qPath = value.getPath(); + jcrValue = factory.createValue(resolver.getJCRPath(qPath), propertyType); + break; + case PropertyType.NAME: + Name qName = value.getName(); + jcrValue = factory.createValue(resolver.getJCRName(qName), propertyType); + break; + case PropertyType.BOOLEAN: + jcrValue = factory.createValue(value.getBoolean()); + break; + case PropertyType.BINARY: + jcrValue = factory.createValue(value.getBinary()); + break; + case PropertyType.DATE: + jcrValue = factory.createValue(value.getCalendar()); + break; + case PropertyType.DOUBLE: + jcrValue = factory.createValue(value.getDouble()); + break; + case PropertyType.LONG: + jcrValue = factory.createValue(value.getLong()); + break; + case PropertyType.DECIMAL: + jcrValue = factory.createValue(value.getDecimal()); + break; + default: + throw new RepositoryException("illegal internal value type"); + } + return jcrValue; + } + } + + /** + * Returns the JCR string representation of the given QValue. + * This method is a shortcut for + * {@link #getJCRValue(QValue, NamePathResolver, ValueFactory)} followed by + * {@link Value#getString()}. + * + * @param value + * @param resolver + * @return the JCR String representation for the given QValue. + * @throws RepositoryException + */ + public static String getJCRString(QValue value, + NamePathResolver resolver) throws RepositoryException { + String jcrString; + int propertyType = value.getType(); + switch (propertyType) { + case PropertyType.STRING: + case PropertyType.REFERENCE: + case PropertyType.WEAKREFERENCE: + case PropertyType.URI: + case PropertyType.BOOLEAN: + case PropertyType.DATE: + case PropertyType.DOUBLE: + case PropertyType.LONG: + case PropertyType.DECIMAL: + case PropertyType.BINARY: + jcrString = value.getString(); + break; + case PropertyType.PATH: + Path qPath = value.getPath(); + jcrString = resolver.getJCRPath(qPath); + break; + case PropertyType.NAME: + Name qName = value.getName(); + jcrString = resolver.getJCRName(qName); + break; + default: + throw new RepositoryException("illegal internal value type"); + } + return jcrString; + } +} diff --git a/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/package-info.java b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/package-info.java new file mode 100644 index 00000000000..1478dd286cd --- /dev/null +++ b/jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/value/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("2.4.0") +package org.apache.jackrabbit.spi.commons.value; diff --git a/src/grammar/sql/JCRSQL.jjt b/jackrabbit-spi-commons/src/main/javacc/sql/JCRSQL.jjt similarity index 77% rename from src/grammar/sql/JCRSQL.jjt rename to jackrabbit-spi-commons/src/main/javacc/sql/JCRSQL.jjt index e942ef61334..947c8e02328 100644 --- a/src/grammar/sql/JCRSQL.jjt +++ b/jackrabbit-spi-commons/src/main/javacc/sql/JCRSQL.jjt @@ -18,18 +18,18 @@ options { FORCE_LA_CHECK = true; MULTI = true; - NODE_PACKAGE = "org.apache.jackrabbit.core.search.sql"; + NODE_PACKAGE = "org.apache.jackrabbit.spi.commons.query.sql"; VISITOR = true; } PARSER_BEGIN(JCRSQLParser) /* - * Copyright 2004-2005 The Apache Software Foundation or its licensors, - * as applicable. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -39,33 +39,35 @@ PARSER_BEGIN(JCRSQLParser) * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.core.search.sql; +package org.apache.jackrabbit.spi.commons.query.sql; + +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.query.QueryConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.Name; -import org.apache.jackrabbit.core.IllegalNameException; -import org.apache.jackrabbit.core.NamespaceResolver; -import org.apache.jackrabbit.core.QName; -import org.apache.jackrabbit.core.UnknownPrefixException; -import org.apache.jackrabbit.core.search.QueryConstants; +import javax.jcr.NamespaceException; public class JCRSQLParser { private String statement; - private NamespaceResolver resolver; + private NameResolver resolver; public static void main(String args[]) throws ParseException { JCRSQLParser parser = new JCRSQLParser(System.in); parser.Query().dump(""); } - public static ASTQuery parse(String statement, NamespaceResolver resolver) throws ParseException { + public static ASTQuery parse(String statement, NameResolver resolver) throws ParseException { java.io.StringReader sReader = new java.io.StringReader(statement); JCRSQLParser parser = new JCRSQLParser(sReader); - parser.setNamespaceResolver(resolver); + parser.setNameResolver(resolver); return parser.Query(); } - private void setNamespaceResolver(NamespaceResolver resolver) { + void setNameResolver(NameResolver resolver) { this.resolver = resolver; } @@ -99,12 +101,17 @@ TOKEN [ IGNORE_CASE ] : | < LIKE: "LIKE" > | < NULL: "NULL" > | < FROM: "FROM" > +| < LOWER: "LOWER" > | < ORDER: "ORDER" > +| < UPPER: "UPPER" > | < WHERE: "WHERE" > | < ESCAPE: "ESCAPE" > | < SELECT: "SELECT" > | < BETWEEN: "BETWEEN" > +| < EXCERPT: "EXCERPT" > +| < SIMILAR: "SIMILAR" > | < CONTAINS: "CONTAINS" > +| < SPELLCHECK: "SPELLCHECK" > } @@ -270,7 +277,13 @@ void SelectList() : {} { () - | (Identifier() ( Identifier() { Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); } )? ( Identifier() ( Identifier() { Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); } )? )*) + | ( SelectItem() ( SelectItem() )* ) +} + +void SelectItem() #void : +{} +{ + (ExcerptFunction() | Identifier() ( Identifier() { Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); } )?) } void TableExpression() #void : @@ -296,14 +309,22 @@ void WhereClause() : void Predicate() : { int operationType; - QName identifier; + Name identifier; String value; String escapeString; } { ( ( - identifier = Identifier() { jjtThis.setIdentifier(identifier); } ( identifier = Identifier() { Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); jjtThis.setIdentifier(identifier); } )? + ( + ( + identifier = Identifier() { jjtThis.setIdentifier(identifier); } ( identifier = Identifier() { Node n = jjtree.popNode(); jjtree.popNode(); jjtree.pushNode(n); jjtThis.setIdentifier(identifier); } )? + ) + | + ( + identifier = PropertyFunction() { jjtThis.setIdentifier(identifier); } + ) + ) ( ( operationType = ComparisonOperation() { jjtThis.setOperationType(operationType); } @@ -338,16 +359,82 @@ void Predicate() : ( Literal() ( { jjtThis.setNegate(true); })? ( - identifier = Identifier() + ( + identifier = Identifier() + | identifier = PropertyFunction() + ) { jjtThis.setIdentifier(identifier); jjtThis.setOperationType(jjtThis.isNegate() ? QueryConstants.OPERATION_NE_GENERAL : QueryConstants.OPERATION_EQ_GENERAL); } ) ) + | + ( + "(" { jjtThis.setOperationType(QueryConstants.OPERATION_SIMILAR); } + ("." | identifier = Identifier() { jjtThis.setIdentifier(identifier); }) + "," + value = CharStringLiteral() + { + ASTLiteral s = new ASTLiteral(JJTLITERAL); + s.setType(QueryConstants.TYPE_STRING); + s.setValue(value); + jjtree.pushNode(s); + } ")" + ) + | + ( + "(" { jjtThis.setOperationType(QueryConstants.OPERATION_SPELLCHECK); } + value = CharStringLiteral() + { + ASTLiteral stmt = new ASTLiteral(JJTLITERAL); + stmt.setType(QueryConstants.TYPE_STRING); + stmt.setValue(value); + jjtree.pushNode(stmt); + } ")" + ) ) } +Name PropertyFunction() #void : +{ + Name identifier; +} +{ + ( + identifier = LowerFunction() + | identifier = UpperFunction() + ) + { + return identifier; + } +} + +Name LowerFunction() : +{ + Name identifier; +} +{ + ( + "(" identifier = Identifier() ")" + ) + { + return identifier; + } +} + +Name UpperFunction() : +{ + Name identifier; +} +{ + ( + "(" identifier = Identifier() ")" + ) + { + return identifier; + } +} int ComparisonOperation() #void : { @@ -428,9 +515,14 @@ void BracketExpression() : void ContainsExpression() : { Token t = null; + Name name = null; } { - t = { jjtThis.setQuery(t.image.substring(1, t.image.length() - 1).replaceAll("''", "'")); } + + + ( | | (name = Identifier() { jjtThis.setPropertyName(name); }) ) "," + t = { jjtThis.setQuery(t.image.substring(1, t.image.length() - 1).replaceAll("''", "'")); } + } void Literal() : @@ -459,6 +551,13 @@ void Literal() : { if (t.image.startsWith("TIMESTAMP")) { jjtThis.setValue(t.image.substring(t.image.indexOf('\'') + 1, t.image.length() - 1)); + if (jjtThis.getValue().indexOf(" ") == 10) { + // replace SQL 92 timesamp string with ISO8601 + StringBuffer tmp = new StringBuffer(); + tmp.append(jjtThis.getValue().substring(0, 10)); + tmp.append("T").append(jjtThis.getValue().substring(11)); + jjtThis.setValue(tmp.toString()); + } jjtThis.setType(QueryConstants.TYPE_TIMESTAMP); /* } else if (t.image.startsWith("TIME")) { @@ -488,20 +587,25 @@ String CharStringLiteral() #void : } } -QName Identifier() : +Name Identifier() : { Token t = null; - QName name = null; + Name name = null; + boolean pseudoProperty = false; } { ( - t = + t = ( { pseudoProperty = true; } ) ? { try { - jjtThis.setName(QName.fromJCRName(t.image, resolver)); - } catch (IllegalNameException e) { + String jcrName = t.image; + if (pseudoProperty) { + jcrName += "()"; + } + jjtThis.setName(resolver.getQName(jcrName)); + } catch (NameException e) { throw new ParseException(e.getMessage()); - } catch (UnknownPrefixException e) { + } catch (NamespaceException e) { throw new ParseException(e.getMessage()); } } @@ -509,10 +613,10 @@ QName Identifier() : t = { try { - jjtThis.setName(QName.fromJCRName(t.image.substring(1, t.image.length()-1), resolver)); - } catch (IllegalNameException e) { + jjtThis.setName(resolver.getQName(t.image.substring(1, t.image.length()-1))); + } catch (NameException e) { throw new ParseException(e.getMessage()); - } catch (UnknownPrefixException e) { + } catch (NamespaceException e) { throw new ParseException(e.getMessage()); } } @@ -536,17 +640,26 @@ QName Identifier() : ) { try { - jjtThis.setName(QName.fromJCRName(t.image, resolver)); - } catch (IllegalNameException e) { + jjtThis.setName(resolver.getQName(t.image)); + } catch (NameException e) { throw new ParseException(e.getMessage()); - } catch (UnknownPrefixException e) { + } catch (NamespaceException e) { throw new ParseException(e.getMessage()); } } ) ) { - return name; + return jjtThis.getName(); + } +} + +Name ExcerptFunction() : +{} +{ + "(" ")" + { + return NameFactoryImpl.getInstance().create(Name.NS_REP_URI, "excerpt(.)"); } } diff --git a/jackrabbit-spi-commons/src/main/javacc/xpath/XPath.jjt b/jackrabbit-spi-commons/src/main/javacc/xpath/XPath.jjt new file mode 100644 index 00000000000..6fc0878c1f2 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/javacc/xpath/XPath.jjt @@ -0,0 +1,2729 @@ +/* + * Generated using "maven jackrabbit:generage-xpath-parser-jjt" from the + * the following files with the following copyright and license notices: + * + * javacc.xsl, jjtree.xsl, and strip.xsl: + * -------------------------------------- + * Copyright (c) 2002 World Wide Web Consortium, + * (Massachusetts Institute of Technology, Institut National de + * Recherche en Informatique et en Automatique, Keio University). All + * Rights Reserved. This program is distributed under the W3C's Software + * Intellectual Property License. This program is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. + * See W3C License http://www.w3.org/Consortium/Legal/ for more details. + * + * jjtree-jackrabbit.xsl: + * ---------------------- + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * xpath-grammar.xml: + * ------------------ + * Copyright (c) 2002 W3C(r) (http://www.w3.org/) (MIT (http://www.lcs.mit.edu/), + * INRIA (http://www.inria.fr/), Keio (http://www.keio.ac.jp/)), + * All Rights Reserved. + * See http://www.w3.org/Consortium/Legal/ipr-notice-20000612#Copyright. + * W3C liability + * (http://www.w3.org/Consortium/Legal/ipr-notice-20000612#Legal_Disclaimer), + * trademark + * (http://www.w3.org/Consortium/Legal/ipr-notice-20000612#W3C_Trademarks), + * document use + * (http://www.w3.org/Consortium/Legal/copyright-documents-19990405), + * and software licensing rules + * (http://www.w3.org/Consortium/Legal/copyright-software-19980720) + * apply. + */ +options { + + + STATIC = false; + MULTI=false; + VISITOR=true ; // invokes the JJTree Visitor support + NODE_SCOPE_HOOK=false; + NODE_USES_PARSER=true; + +} + + + PARSER_BEGIN(XPath) + + + package org.apache.jackrabbit.spi.commons.query.xpath; + +import java.io.*; +import java.util.Stack; +import java.util.Vector; + +public class XPath { + + + boolean m_isMatchPattern = false; + boolean isStep = false; + + Stack binaryTokenStack = new Stack(); + + public Node createNode(int id) { + return null; + } + + + + public static void main(String args[]) + throws Exception + { + int numberArgsLeft = args.length; + int argsStart = 0; + boolean isMatchParser = false; + if(numberArgsLeft > 0) + { + if(args[argsStart].equals("-match")) + { + isMatchParser = true; + System.out.println("Match Pattern Parser"); + argsStart++; + numberArgsLeft--; + } + } + if(numberArgsLeft > 0) + { + try + { + final boolean dumpTree = true; + if(args[0].endsWith(".xquery")) + { + System.out.println("Running test for: "+args[0]); + File file = new File(args[0]); + FileInputStream fis = new FileInputStream(file); + XPath parser = new XPath(fis); + SimpleNode tree = parser.XPath2(); + if(dumpTree) + tree.dump("|") ; + } + else + { + for(int i = argsStart; i < args.length; i++) + { + System.out.println(); + System.out.println("Test["+i+"]: "+args[i]); + XPath parser = new XPath(new java.io.StringBufferInputStream(args[i])); + SimpleNode tree; + if(isMatchParser) + { + tree = parser.XPath2(); + } + else + { + tree = parser.XPath2(); + } + ((SimpleNode)tree.jjtGetChild(0)).dump("|") ; + } + System.out.println("Success!!!!"); + } + } + catch(ParseException pe) + { + System.err.println(pe.getMessage()); + } + return; + } + java.io.DataInputStream dinput = new java.io.DataInputStream(System.in); + while(true) + { + try + { + System.err.println("Type Expression: "); + String input = dinput.readLine(); + if(null == input || input.trim().length() == 0) + break; + XPath parser = new XPath(new java.io.StringBufferInputStream(input)); + SimpleNode tree; + if(isMatchParser) + { + tree = parser.XPath2(); + } + else + { + tree = parser.XPath2(); + } + ((SimpleNode)tree.jjtGetChild(0)).dump("|") ; + } + catch(ParseException pe) + { + System.err.println(pe.getMessage()); + } + catch(Exception e) + { + System.err.println(e.getMessage()); + } + } + } + } + + PARSER_END(XPath) + + + +TOKEN_MGR_DECLS : { + private Stack stateStack = new Stack(); + // private Vector persistentLexStates = new Vector(); + static final int PARENMARKER = 2000; + + /** + * Push the current state onto the state stack. + */ + private void pushState() + { + // System.err.println("pushing: "+curLexState); printLinePos(); + stateStack.addElement(new Integer(curLexState)); + } + + /** + * Push the given state onto the state stack. + * @param state Must be a valid state. + */ + private void pushState(int state) + { + stateStack.push(new Integer(state)); + } + + /** + * Pop the state on the state stack, and switch to that state. + */ + private void popState() + { + if (stateStack.size() == 0) + { + printLinePos(); + } + + int nextState = ((Integer) stateStack.pop()).intValue(); + // System.err.println("pop "+nextState); printLinePos(); + if(nextState == PARENMARKER) + printLinePos(); + SwitchTo(nextState); + } + + /** + * Push the given state onto the state stack. + * @param state Must be a valid state. + */ + private boolean isState(int state) + { + for (int i = 0; i < stateStack.size(); i++) { + if(((Integer) stateStack.elementAt(i)).intValue() == state) + { + return true; + } + } + return false; + } + + /** + * Push a parenthesis state. This pushes, in addition to the + * lexical state value, a special marker that lets + * resetParenStateOrSwitch(int state) + * know if it should pop and switch. Used for the comma operator. + */ + private void pushParenState(int commaState, int rparState) + { + stateStack.push(new Integer(rparState)); + stateStack.push(new Integer(commaState)); + stateStack.push(new Integer(PARENMARKER)); + SwitchTo(commaState); + } + + + /** + * Print the current line position. + */ + public void printLinePos() + { + System.err.println("Line: " + input_stream.getEndLine()); + } +} + + + SimpleNode XPath2() : + {} + { + QueryList() + { return jjtThis ; } + } + + +TOKEN : +{ + < IntegerLiteral : > : OPERATOR +} + + +TOKEN : +{ + < DecimalLiteral : (("." ) | ( "." (["0" - "9"])*)) > : OPERATOR +} + + +TOKEN : +{ + < DoubleLiteral : (("." ) | ( ("." (["0" - "9"])*)?)) ["e", "E"] (["+", "-"])? > : OPERATOR +} + + +TOKEN : +{ + < StringLiteral : (("\"" ((("\"" "\"") | ~["\""]))* "\"") | ("'" ((("'" "'") | ~["'"]))* "'")) > : OPERATOR +} + + +TOKEN : +{ + < StringLiteralForKindTest : (("\"" ((("\"" "\"") | ~["\""]))* "\"") | ("'" ((("'" "'") | ~["'"]))* "'")) > : KINDTESTFORPI +} + + +TOKEN : +{ + < XQueryVersion : "xquery" ()* "version" > : XQUERYVERSION +} + + +TOKEN : +{ + < StringLiteralForVersion : (("\"" ((("\"" "\"") | ~["\""]))* "\"") | ("'" ((("'" "'") | ~["'"]))* "'")) > : XQUERYVERSION +} + + +TOKEN : +{ + < XQueryEncoding : "encoding" > : XQUERYVERSION +} + + +TOKEN : +{ + < AtStringLiteral : "at" ()* > : DEFAULT +} + + +TOKEN : +{ + < URLLiteral : (("\"" ((("\"" "\"") | ~["\""]))* "\"") | ("'" ((("'" "'") | ~["'"]))* "'")) > : DEFAULT +} + + +TOKEN : +{ + < ModuleNamespace : "module" ()* "namespace" > : NAMESPACEDECL +} + +SKIP: +{ + < > +} + +TOKEN : +{ + < #skip_ : ((()+ )) > +} + + + +SKIP : +{ + < NotOccurrenceIndicator : ~["*", "?", "+"] > { input_stream.backup(1); } : OPERATOR +} + + +TOKEN : +{ + < ProcessingInstructionStart : " { pushState(OPERATOR); } : PROCESSING_INSTRUCTION +} + + +TOKEN : +{ + < ProcessingInstructionStartForElementContent : " { pushState(); } : PROCESSING_INSTRUCTION +} + + +TOKEN : +{ + < ProcessingInstructionEnd : "?>" > { popState(); } +} + + +TOKEN : +{ + < AxisChild : "child" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisDescendant : "descendant" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisParent : "parent" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisAttribute : "attribute" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisSelf : "self" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisDescendantOrSelf : "descendant-or-self" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisAncestor : "ancestor" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisFollowingSibling : "following-sibling" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisPrecedingSibling : "preceding-sibling" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisFollowing : "following" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisPreceding : "preceding" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < AxisAncestorOrSelf : "ancestor-or-self" ()* "::" > : DEFAULT +} + + +TOKEN : +{ + < DefineFunction : "declare" ()+ "function" > : DEFAULT +} + + +TOKEN : +{ + < DeclareOrdering : "declare" ()+ "ordering" > : OPERATOR +} + + +TOKEN : +{ + < Ordered : "ordered" > : DEFAULT +} + + +TOKEN : +{ + < Unordered : "unordered" > : DEFAULT +} + + +TOKEN : +{ + < DeclareDefaultOrderingEmpty : "declare" ()+ "default" ()+ "order" > : OPERATOR +} + + +TOKEN : +{ + < DeclareInheritNamespaces : "declare" ()+ "inherit-namespaces" > : OPERATOR +} + + +TOKEN : +{ + < Yes : "yes" > : DEFAULT +} + + +TOKEN : +{ + < No : "no" > : DEFAULT +} + + +TOKEN : +{ + < External : "external" > : DEFAULT +} + + +TOKEN : +{ + < Or : "or" > : DEFAULT +} + + +TOKEN : +{ + < And : "and" > : DEFAULT +} + + +TOKEN : +{ + < Div : "div" > : DEFAULT +} + + +TOKEN : +{ + < Idiv : "idiv" > : DEFAULT +} + + +TOKEN : +{ + < Mod : "mod" > : DEFAULT +} + + +TOKEN : +{ + < Multiply : "*" > : DEFAULT +} + + +TOKEN : +{ + < In : "in" > : DEFAULT +} + + +TOKEN : +{ + < PITarget : > : PROCESSING_INSTRUCTION +} + +TOKEN : +{ + < #Prefix : > +} + +TOKEN : +{ + < #LocalPart : > +} + + +TOKEN : +{ + < VariableIndicator : "$" > : VARNAME +} + + +TOKEN : +{ + < VarName : > : OPERATOR +} + +TOKEN : +{ + < #ValidationMode : ("lax" | "strict") > +} + + +TOKEN : +{ + < SchemaModeForDeclareConstruction : ("preserve" | "strip") > : DEFAULT +} + + +TOKEN : +{ + < Nillable : "?" > : CLOSEKINDTEST +} + + +TOKEN : +{ + < DeclareConstruction : "declare" ()+ "construction" > : OPERATOR +} + + +TOKEN : +{ + < Satisfies : "satisfies" > : DEFAULT +} + + +TOKEN : +{ + < Return : "return" > : DEFAULT +} + + +TOKEN : +{ + < Then : "then" > : DEFAULT +} + + +TOKEN : +{ + < Else : "else" > : DEFAULT +} + + +TOKEN : +{ + < Default : "default" > : OPERATOR +} + + +TOKEN : +{ + < DeclareXMLSpace : "declare" ()+ "xmlspace" > : XMLSPACE_DECL +} + + +TOKEN : +{ + < DeclareBaseURI : "declare" ()+ "base-uri" > : NAMESPACEDECL +} + + +TOKEN : +{ + < XMLSpacePreserve : "preserve" > : DEFAULT +} + + +TOKEN : +{ + < XMLSpaceStrip : "strip" > : DEFAULT +} + + +TOKEN : +{ + < Namespace : "namespace" > : NAMESPACEDECL +} + + +TOKEN : +{ + < DeclareNamespace : "declare" ()+ "namespace" > : NAMESPACEDECL +} + + +TOKEN : +{ + < To : "to" > : DEFAULT +} + + +TOKEN : +{ + < Where : "where" > : DEFAULT +} + + +TOKEN : +{ + < Collation : "collation" > : OPERATOR +} + + +TOKEN : +{ + < Intersect : "intersect" > : DEFAULT +} + + +TOKEN : +{ + < Union : "union" > : DEFAULT +} + + +TOKEN : +{ + < Except : "except" > : DEFAULT +} + + +TOKEN : +{ + < As : "as" > : ITEMTYPE +} + + +TOKEN : +{ + < AtWord : "at" > : DEFAULT +} + + +TOKEN : +{ + < Case : "case" > : ITEMTYPE +} + + +TOKEN : +{ + < Instanceof : "instance" ()+ "of" > : ITEMTYPE +} + + +TOKEN : +{ + < Castable : "castable" ()+ "as" > : SINGLETYPE +} + + +TOKEN : +{ + < Item : "item" ()* "(" ()* ")" > : OCCURRENCEINDICATOR +} + + +TOKEN : +{ + < ElementType : "element" ()* "(" > { pushState(OPERATOR); } : KINDTEST +} + + +TOKEN : +{ + < AttributeType : "attribute" ()* "(" > { pushState(OPERATOR); } : KINDTEST +} + + +TOKEN : +{ + < SchemaElementType : "schema-element" ()* "(" > { pushState(OPERATOR); } : KINDTEST +} + + +TOKEN : +{ + < SchemaAttributeType : "schema-attribute" ()* "(" > { pushState(OPERATOR); } : KINDTEST +} + + +TOKEN : +{ + < OrderedOpen : "ordered" ()* "{" > { pushState(OPERATOR); } : DEFAULT +} + + +TOKEN : +{ + < UnorderedOpen : "unordered" ()* "{" > { pushState(OPERATOR); } : DEFAULT +} + + +TOKEN : +{ + < ElementQNameLbrace : "element" ()+ ()* "{" > { pushState(OPERATOR); } +} + + +TOKEN : +{ + < AttributeQNameLbrace : "attribute" ()+ ()* "{" > { pushState(OPERATOR); } +} + + +TOKEN : +{ + < PINCNameLbrace : "processing-instruction" ()+ ()* "{" > { pushState(OPERATOR); } +} + + +TOKEN : +{ + < PILbrace : "processing-instruction" ()* "{" > { pushState(OPERATOR); } +} + + +TOKEN : +{ + < CommentLbrace : "comment" ()* "{" > { pushState(OPERATOR); } +} + + +TOKEN : +{ + < ElementLbrace : "element" ()* "{" > { pushState(OPERATOR); } : DEFAULT +} + + +TOKEN : +{ + < AttributeLbrace : "attribute" ()* "{" > { pushState(OPERATOR); } : DEFAULT +} + + +TOKEN : +{ + < TextLbrace : "text" ()* "{" > { pushState(OPERATOR); } +} + + +TOKEN : +{ + < DeclareCollation : "declare" ()+ "default" ()+ "collation" > : NAMESPACEDECL +} + + +TOKEN : +{ + < DefaultElement : "default" ()+ "element" > : NAMESPACEKEYWORD +} + + +TOKEN : +{ + < DeclareDefaultElement : "declare" ()+ "default" ()+ "element" > : NAMESPACEKEYWORD +} + + +TOKEN : +{ + < DeclareDefaultFunction : "declare" ()+ "default" ()+ "function" > : NAMESPACEKEYWORD +} + + +TOKEN : +{ + < EmptyTok : "empty" ()* "(" ()* ")" > : OPERATOR +} + + +TOKEN : +{ + < ImportSchemaToken : "import" ()+ "schema" > : NAMESPACEKEYWORD +} + + +TOKEN : +{ + < ImportModuleToken : "import" ()+ "module" > : NAMESPACEKEYWORD +} + +TOKEN : +{ + < #Nmstart : ( | "_") > +} + +TOKEN : +{ + < #Nmchar : ( | | | | "." | "-" | "_") > +} + + +TOKEN : +{ + < Star : "*" > : OPERATOR +} + + +TOKEN : +{ + < AnyName : "*" > : CLOSEKINDTEST +} + + +TOKEN : +{ + < NCNameColonStar : ":" "*" > : OPERATOR +} + + +TOKEN : +{ + < StarColonNCName : "*" ":" > : OPERATOR +} + + +TOKEN : +{ + < Root : "/" > : DEFAULT +} + + +TOKEN : +{ + < RootDescendants : "//" > : DEFAULT +} + + +TOKEN : +{ + < Slash : "/" > : DEFAULT +} + + +TOKEN : +{ + < SlashSlash : "//" > : DEFAULT +} + + +TOKEN : +{ + < Equals : "=" > : DEFAULT +} + + +TOKEN : +{ + < AssignEquals : "=" > : NAMESPACEDECL +} + + +TOKEN : +{ + < Is : "is" > : DEFAULT +} + + +TOKEN : +{ + < NotEquals : "!=" > : DEFAULT +} + + +TOKEN : +{ + < LtEquals : "<=" > : DEFAULT +} + + +TOKEN : +{ + < LtLt : "<<" > : DEFAULT +} + + +TOKEN : +{ + < GtEquals : ">=" > : DEFAULT +} + + +TOKEN : +{ + < GtGt : ">>" > : DEFAULT +} + + +TOKEN : +{ + < FortranEq : "eq" > : DEFAULT +} + + +TOKEN : +{ + < FortranNe : "ne" > : DEFAULT +} + + +TOKEN : +{ + < FortranGt : "gt" > : DEFAULT +} + + +TOKEN : +{ + < FortranGe : "ge" > : DEFAULT +} + + +TOKEN : +{ + < FortranLt : "lt" > : DEFAULT +} + + +TOKEN : +{ + < FortranLe : "le" > : DEFAULT +} + + +TOKEN : +{ + < ColonEquals : ":=" > : DEFAULT +} + + +TOKEN : +{ + < Lt : "<" > : DEFAULT +} + + +TOKEN : +{ + < Gt : ">" > : DEFAULT +} + + +TOKEN : +{ + < Minus : "-" > : DEFAULT +} + + +TOKEN : +{ + < Plus : "+" > : DEFAULT +} + + +TOKEN : +{ + < UnaryMinus : "-" > : DEFAULT +} + + +TOKEN : +{ + < UnaryPlus : "+" > : DEFAULT +} + + +TOKEN : +{ + < OccurrenceZeroOrOne : "?" > : OPERATOR +} + + +TOKEN : +{ + < OccurrenceZeroOrMore : "*" > : OPERATOR +} + + +TOKEN : +{ + < OccurrenceOneOrMore : "+" > : OPERATOR +} + + +TOKEN : +{ + < Vbar : "|" > : DEFAULT +} + + +TOKEN : +{ + < Lpar : "(" > : DEFAULT +} + + +TOKEN : +{ + < At : "@" > : DEFAULT +} + + +TOKEN : +{ + < Lbrack : "[" > : DEFAULT +} + + +TOKEN : +{ + < Rbrack : "]" > : OPERATOR +} + + +TOKEN : +{ + < Rpar : ")" > : OPERATOR +} + + +TOKEN : +{ + < RparForKindTest : ")" > { popState(); } +} + + +TOKEN : +{ + < Some : "some" ()* > : VARNAME +} + + +TOKEN : +{ + < Every : "every" ()* > : VARNAME +} + + +TOKEN : +{ + < ForVariable : "for" ()* > : VARNAME +} + + +TOKEN : +{ + < LetVariable : "let" ()* > : VARNAME +} + + +TOKEN : +{ + < CastAs : "cast" ()+ "as" > : SINGLETYPE +} + + +TOKEN : +{ + < TreatAs : "treat" ()+ "as" > : ITEMTYPE +} + + +TOKEN : +{ + < ValidateLbrace : "validate" ()* "{" > { pushState(OPERATOR); } : DEFAULT +} + + +TOKEN : +{ + < ValidateSchemaMode : "validate" ()+ > { pushState(OPERATOR); } : DEFAULT +} + +TOKEN : +{ + < #Digits : (["0" - "9"])+ > +} + + +TOKEN : +{ + < DocumentLpar : "document-node" ()* "(" > { pushState(OPERATOR); } : KINDTEST +} + + +TOKEN : +{ + < DocumentLparForKindTest : "document-node" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTEST +} + + +TOKEN : +{ + < DocumentLbrace : "document" ()* "{" > { pushState(OPERATOR); } +} + + +TOKEN : +{ + < NodeLpar : "node" ()* "(" > { pushState(OPERATOR); } : KINDTEST +} + + +TOKEN : +{ + < CommentLpar : "comment" ()* "(" > { pushState(OPERATOR); } : KINDTEST +} + + +TOKEN : +{ + < TextLpar : "text" ()* "(" > { pushState(OPERATOR); } : KINDTEST +} + + +TOKEN : +{ + < ProcessingInstructionLpar : "processing-instruction" ()* "(" > { pushState(OPERATOR); } : KINDTESTFORPI +} + + +TOKEN : +{ + < ElementTypeForKindTest : "element" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTEST +} + + +TOKEN : +{ + < ElementTypeForDocumentTest : "element" ()* "(" > { pushState(KINDTEST); } : KINDTEST +} + + +TOKEN : +{ + < AttributeTypeForKindTest : "attribute" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTEST +} + + +TOKEN : +{ + < SchemaElementTypeForKindTest : "schema-element" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTEST +} + + +TOKEN : +{ + < SchemaElementTypeForDocumentTest : "schema-element" ()* "(" > { pushState(KINDTEST); } : KINDTEST +} + + +TOKEN : +{ + < SchemaAttributeTypeForKindTest : "schema-attribute" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTEST +} + + +TOKEN : +{ + < ProcessingInstructionLparForKindTest : "processing-instruction" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTESTFORPI +} + + +TOKEN : +{ + < TextLparForKindTest : "text" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTEST +} + + +TOKEN : +{ + < CommentLparForKindTest : "comment" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTEST +} + + +TOKEN : +{ + < NodeLparForKindTest : "node" ()* "(" > { pushState(OCCURRENCEINDICATOR); } : KINDTEST +} + + +TOKEN : +{ + < IfLpar : "if" ()* "(" > : DEFAULT +} + + +TOKEN : +{ + < TypeswitchLpar : "typeswitch" ()* "(" > : DEFAULT +} + + +TOKEN : +{ + < Comma : "," > : DEFAULT +} + + +TOKEN : +{ + < CommaForKindTest : "," > : KINDTEST +} + + +TOKEN : +{ + < SemiColon : ";" > : DEFAULT +} + + +TOKEN : +{ + < QuerySeparator : "%%%" > : DEFAULT +} + + +TOKEN : +{ + < OpenQuot : "\"" > : QUOT_ATTRIBUTE_CONTENT +} + + +TOKEN : +{ + < CloseQuot : "\"" > : START_TAG +} + + +TOKEN : +{ + < Dot : "." > : OPERATOR +} + + +TOKEN : +{ + < DotDot : ".." > : OPERATOR +} + + +TOKEN : +{ + < OrderBy : "order" ()+ "by" > : DEFAULT +} + + +TOKEN : +{ + < OrderByStable : "stable" ()+ "order" ()+ "by" > : DEFAULT +} + + +TOKEN : +{ + < Ascending : "ascending" > : OPERATOR +} + + +TOKEN : +{ + < Descending : "descending" > : OPERATOR +} + + +TOKEN : +{ + < EmptyGreatest : "empty" ()+ "greatest" > : OPERATOR +} + + +TOKEN : +{ + < EmptyLeast : "empty" ()+ "least" > : OPERATOR +} + + +TOKEN : +{ + < DefineVariable : "declare" ()+ "variable" ()* > : VARNAME +} + + +TOKEN : +{ + < QNameForSequenceType : ( ":")? > : OCCURRENCEINDICATOR +} + + +TOKEN : +{ + < QNameForAtomicType : ( ":")? > : OPERATOR +} + + +TOKEN : +{ + < QNameForItemType : ( ":")? > : CLOSEKINDTEST +} + + +SPECIAL_TOKEN : +{ + < ExtensionQName : ( ":")? > : EXT_CONTENT +} + + +TOKEN : +{ + < QNameLpar : ()* "(" > : DEFAULT +} + + +TOKEN : +{ + < NCNameForPrefix : ()* > : NAMESPACEDECL +} + + +TOKEN : +{ + < NCNameForPI : ()* > : KINDTESTFORPI +} + + +TOKEN : +{ + < CdataSectionStart : " { pushState(OPERATOR); } : CDATA_SECTION +} + + +TOKEN : +{ + < CdataSectionStartForElementContent : " { pushState(); } : CDATA_SECTION +} + + +TOKEN : +{ + < CdataSectionEnd : ("]" "]" ">") > { popState(); } +} + + +TOKEN : +{ + < PredefinedEntityRef : "&" ("lt" | "gt" | "amp" | "quot" | "apos") ";" > +} + + +TOKEN : +{ + < CharRef : "&#" ( | ("x" )) ";" > +} + +TOKEN : +{ + < #HexDigits : (["0" - "9", "a" - "f", "A" - "F"])+ > +} + + +TOKEN : +{ + < StartTagOpen : "<" > { pushState(); } : START_TAG +} + + +TOKEN : +{ + < StartTagOpenRoot : "<" > { pushState(OPERATOR); } : START_TAG +} + + +TOKEN : +{ + < StartTagClose : ">" > : ELEMENT_CONTENT +} + + +TOKEN : +{ + < EmptyTagClose : "/>" > { popState(); } +} + + +TOKEN : +{ + < EndTagOpen : " : END_TAG +} + + +TOKEN : +{ + < EndTagClose : ">" > { popState(); } +} + + +TOKEN : +{ + < ValueIndicator : "=" > : START_TAG +} + + +TOKEN : +{ + < TagQName : > +} + + +TOKEN : +{ + < Lbrace : "{" > { pushState(); } : DEFAULT +} + + +TOKEN : +{ + < LbraceExprEnclosure : "{" > { pushState(OPERATOR); } : DEFAULT +} + + +TOKEN : +{ + < LCurlyBraceEscape : "{{" > +} + + +TOKEN : +{ + < RCurlyBraceEscape : "}}" > +} + + +TOKEN : +{ + < EscapeQuot : "\"\"" > : QUOT_ATTRIBUTE_CONTENT +} + + +TOKEN : +{ + < EscapeApos : "''" > : APOS_ATTRIBUTE_CONTENT +} + + +TOKEN : +{ + < ElementContentChar : > : ELEMENT_CONTENT +} + + +TOKEN : +{ + < QuotAttrContentChar : > : QUOT_ATTRIBUTE_CONTENT +} + + +TOKEN : +{ + < AposAttrContentChar : > : APOS_ATTRIBUTE_CONTENT +} + + +TOKEN : +{ + < CommentContentChar : > : XML_COMMENT +} + + +TOKEN : +{ + < CommentContentCharDash : "-" > : XML_COMMENT +} + + +TOKEN : +{ + < PIContentChar : ["\t", "\r", "\n", "\u0020" - "\uFFFD"] > : PROCESSING_INSTRUCTION_CONTENT +} + + +TOKEN : +{ + < CDataSectionChar : ["\t", "\r", "\n", "\u0020" - "\uFFFD"] > : CDATA_SECTION +} + + +TOKEN : +{ + < OpenApos : "'" > : APOS_ATTRIBUTE_CONTENT +} + + +TOKEN : +{ + < CloseApos : "'" > : START_TAG +} + +SPECIAL_TOKEN : +{ + < #Pragma : ()? ( )? > +} + +SPECIAL_TOKEN : +{ + < #MUExtension : ()? ( )? > +} + +SPECIAL_TOKEN : +{ + < #ExtensionContents : ()* > +} + + +SPECIAL_TOKEN : +{ + < ExtensionStart : "(::" > { pushState(); } : EXT_KEY +} + + +SPECIAL_TOKEN : +{ + < ExtensionContentChar : > +} + + +SPECIAL_TOKEN : +{ + < ExtensionEnd : "::)" > { popState(); } +} + +SKIP : +{ + < #Comment : (())* > +} + +SPECIAL_TOKEN : +{ + < #CommentContents : ()+ > +} + + +SPECIAL_TOKEN : +{ + < CommentStart : "(:" > { pushState(); } : EXPR_COMMENT +} + + +SPECIAL_TOKEN : +{ + < CommentContent : > +} + + +SPECIAL_TOKEN : +{ + < CommentEnd : ":)" > { popState(); } +} + + +SPECIAL_TOKEN : +{ + < PragmaKeyword : "pragma" > : EXT_NAME +} + + +SPECIAL_TOKEN : +{ + < Extension : "extension" > : EXT_NAME +} + + +TOKEN : +{ + < XmlCommentStart : "" > { popState(); } +} + + +TOKEN : +{ + < QName : ( ":")? > : OPERATOR +} + +TOKEN : +{ + < #NCName : ()* > +} + + +TOKEN : +{ + < S : ()+ > +} + + +TOKEN : +{ + < SForPI : ()+ > : PROCESSING_INSTRUCTION_CONTENT +} + + +SPECIAL_TOKEN : +{ + < SForExt : ()+ > +} + +TOKEN : +{ + < #Char : ["\t", "\r", "\n", "\u0020" - "\uFFFD"] > +} + + +TOKEN : +{ + < Rbrace : "}" > { popState(); } +} + +TOKEN : +{ + < #WhitespaceChar : ["\t", "\r", "\n", " "] > +} + +TOKEN : +{ + < #Letter : ( | ) > +} + +TOKEN : +{ + < #BaseChar : ["\u0041" - "\u005a", "\u0061" - "\u007a", "\u00c0" - "\u00d6", "\u00d8" - "\u00f6", "\u00f8" - "\u00ff", "\u0100" - "\u0131", "\u0134" - "\u013e", "\u0141" - "\u0148", "\u014a" - "\u017e", "\u0180" - "\u01c3", "\u01cd" - "\u01f0", "\u01f4" - "\u01f5", "\u01fa" - "\u0217", "\u0250" - "\u02a8", "\u02bb" - "\u02c1", "\u0386", "\u0388" - "\u038a", "\u038c", "\u038e" - "\u03a1", "\u03a3" - "\u03ce", "\u03d0" - "\u03d6", "\u03da", "\u03dc", "\u03de", "\u03e0", "\u03e2" - "\u03f3", "\u0401" - "\u040c", "\u040e" - "\u044f", "\u0451" - "\u045c", "\u045e" - "\u0481", "\u0490" - "\u04c4", "\u04c7" - "\u04c8", "\u04cb" - "\u04cc", "\u04d0" - "\u04eb", "\u04ee" - "\u04f5", "\u04f8" - "\u04f9", "\u0531" - "\u0556", "\u0559", "\u0561" - "\u0586", "\u05d0" - "\u05ea", "\u05f0" - "\u05f2", "\u0621" - "\u063a", "\u0641" - "\u064a", "\u0671" - "\u06b7", "\u06ba" - "\u06be", "\u06c0" - "\u06ce", "\u06d0" - "\u06d3", "\u06d5", "\u06e5" - "\u06e6", "\u0905" - "\u0939", "\u093d", "\u0958" - "\u0961", "\u0985" - "\u098c", "\u098f" - "\u0990", "\u0993" - "\u09a8", "\u09aa" - "\u09b0", "\u09b2", "\u09b6" - "\u09b9", "\u09dc" - "\u09dd", "\u09df" - "\u09e1", "\u09f0" - "\u09f1", "\u0a05" - "\u0a0a", "\u0a0f" - "\u0a10", "\u0a13" - "\u0a28", "\u0a2a" - "\u0a30", "\u0a32" - "\u0a33", "\u0a35" - "\u0a36", "\u0a38" - "\u0a39", "\u0a59" - "\u0a5c", "\u0a5e", "\u0a72" - "\u0a74", "\u0a85" - "\u0a8b", "\u0a8d", "\u0a8f" - "\u0a91", "\u0a93" - "\u0aa8", "\u0aaa" - "\u0ab0", "\u0ab2" - "\u0ab3", "\u0ab5" - "\u0ab9", "\u0abd", "\u0ae0", "\u0b05" - "\u0b0c", "\u0b0f" - "\u0b10", "\u0b13" - "\u0b28", "\u0b2a" - "\u0b30", "\u0b32" - "\u0b33", "\u0b36" - "\u0b39", "\u0b3d", "\u0b5c" - "\u0b5d", "\u0b5f" - "\u0b61", "\u0b85" - "\u0b8a", "\u0b8e" - "\u0b90", "\u0b92" - "\u0b95", "\u0b99" - "\u0b9a", "\u0b9c", "\u0b9e" - "\u0b9f", "\u0ba3" - "\u0ba4", "\u0ba8" - "\u0baa", "\u0bae" - "\u0bb5", "\u0bb7" - "\u0bb9", "\u0c05" - "\u0c0c", "\u0c0e" - "\u0c10", "\u0c12" - "\u0c28", "\u0c2a" - "\u0c33", "\u0c35" - "\u0c39", "\u0c60" - "\u0c61", "\u0c85" - "\u0c8c", "\u0c8e" - "\u0c90", "\u0c92" - "\u0ca8", "\u0caa" - "\u0cb3", "\u0cb5" - "\u0cb9", "\u0cde", "\u0ce0" - "\u0ce1", "\u0d05" - "\u0d0c", "\u0d0e" - "\u0d10", "\u0d12" - "\u0d28", "\u0d2a" - "\u0d39", "\u0d60" - "\u0d61", "\u0e01" - "\u0e2e", "\u0e30", "\u0e32" - "\u0e33", "\u0e40" - "\u0e45", "\u0e81" - "\u0e82", "\u0e84", "\u0e87" - "\u0e88", "\u0e8a", "\u0e8d", "\u0e94" - "\u0e97", "\u0e99" - "\u0e9f", "\u0ea1" - "\u0ea3", "\u0ea5", "\u0ea7", "\u0eaa" - "\u0eab", "\u0ead" - "\u0eae", "\u0eb0", "\u0eb2" - "\u0eb3", "\u0ebd", "\u0ec0" - "\u0ec4", "\u0f40" - "\u0f47", "\u0f49" - "\u0f69", "\u10a0" - "\u10c5", "\u10d0" - "\u10f6", "\u1100", "\u1102" - "\u1103", "\u1105" - "\u1107", "\u1109", "\u110b" - "\u110c", "\u110e" - "\u1112", "\u113c", "\u113e", "\u1140", "\u114c", "\u114e", "\u1150", "\u1154" - "\u1155", "\u1159", "\u115f" - "\u1161", "\u1163", "\u1165", "\u1167", "\u1169", "\u116d" - "\u116e", "\u1172" - "\u1173", "\u1175", "\u119e", "\u11a8", "\u11ab", "\u11ae" - "\u11af", "\u11b7" - "\u11b8", "\u11ba", "\u11bc" - "\u11c2", "\u11eb", "\u11f0", "\u11f9", "\u1e00" - "\u1e9b", "\u1ea0" - "\u1ef9", "\u1f00" - "\u1f15", "\u1f18" - "\u1f1d", "\u1f20" - "\u1f45", "\u1f48" - "\u1f4d", "\u1f50" - "\u1f57", "\u1f59", "\u1f5b", "\u1f5d", "\u1f5f" - "\u1f7d", "\u1f80" - "\u1fb4", "\u1fb6" - "\u1fbc", "\u1fbe", "\u1fc2" - "\u1fc4", "\u1fc6" - "\u1fcc", "\u1fd0" - "\u1fd3", "\u1fd6" - "\u1fdb", "\u1fe0" - "\u1fec", "\u1ff2" - "\u1ff4", "\u1ff6" - "\u1ffc", "\u2126", "\u212a" - "\u212b", "\u212e", "\u2180" - "\u2182", "\u3041" - "\u3094", "\u30a1" - "\u30fa", "\u3105" - "\u312c", "\uac00" - "\ud7a3"] > +} + +TOKEN : +{ + < #Ideographic : ["\u4e00" - "\u9fa5", "\u3007", "\u3021" - "\u3029"] > +} + +TOKEN : +{ + < #CombiningChar : ["\u0300" - "\u0345", "\u0360" - "\u0361", "\u0483" - "\u0486", "\u0591" - "\u05a1", "\u05a3" - "\u05b9", "\u05bb" - "\u05bd", "\u05bf", "\u05c1" - "\u05c2", "\u05c4", "\u064b" - "\u0652", "\u0670", "\u06d6" - "\u06dc", "\u06dd" - "\u06df", "\u06e0" - "\u06e4", "\u06e7" - "\u06e8", "\u06ea" - "\u06ed", "\u0901" - "\u0903", "\u093c", "\u093e" - "\u094c", "\u094d", "\u0951" - "\u0954", "\u0962" - "\u0963", "\u0981" - "\u0983", "\u09bc", "\u09be", "\u09bf", "\u09c0" - "\u09c4", "\u09c7" - "\u09c8", "\u09cb" - "\u09cd", "\u09d7", "\u09e2" - "\u09e3", "\u0a02", "\u0a3c", "\u0a3e", "\u0a3f", "\u0a40" - "\u0a42", "\u0a47" - "\u0a48", "\u0a4b" - "\u0a4d", "\u0a70" - "\u0a71", "\u0a81" - "\u0a83", "\u0abc", "\u0abe" - "\u0ac5", "\u0ac7" - "\u0ac9", "\u0acb" - "\u0acd", "\u0b01" - "\u0b03", "\u0b3c", "\u0b3e" - "\u0b43", "\u0b47" - "\u0b48", "\u0b4b" - "\u0b4d", "\u0b56" - "\u0b57", "\u0b82" - "\u0b83", "\u0bbe" - "\u0bc2", "\u0bc6" - "\u0bc8", "\u0bca" - "\u0bcd", "\u0bd7", "\u0c01" - "\u0c03", "\u0c3e" - "\u0c44", "\u0c46" - "\u0c48", "\u0c4a" - "\u0c4d", "\u0c55" - "\u0c56", "\u0c82" - "\u0c83", "\u0cbe" - "\u0cc4", "\u0cc6" - "\u0cc8", "\u0cca" - "\u0ccd", "\u0cd5" - "\u0cd6", "\u0d02" - "\u0d03", "\u0d3e" - "\u0d43", "\u0d46" - "\u0d48", "\u0d4a" - "\u0d4d", "\u0d57", "\u0e31", "\u0e34" - "\u0e3a", "\u0e47" - "\u0e4e", "\u0eb1", "\u0eb4" - "\u0eb9", "\u0ebb" - "\u0ebc", "\u0ec8" - "\u0ecd", "\u0f18" - "\u0f19", "\u0f35", "\u0f37", "\u0f39", "\u0f3e", "\u0f3f", "\u0f71" - "\u0f84", "\u0f86" - "\u0f8b", "\u0f90" - "\u0f95", "\u0f97", "\u0f99" - "\u0fad", "\u0fb1" - "\u0fb7", "\u0fb9", "\u20d0" - "\u20dc", "\u20e1", "\u302a" - "\u302f", "\u3099", "\u309a"] > +} + +TOKEN : +{ + < #Digit : ["\u0030" - "\u0039", "\u0660" - "\u0669", "\u06f0" - "\u06f9", "\u0966" - "\u096f", "\u09e6" - "\u09ef", "\u0a66" - "\u0a6f", "\u0ae6" - "\u0aef", "\u0b66" - "\u0b6f", "\u0be7" - "\u0bef", "\u0c66" - "\u0c6f", "\u0ce6" - "\u0cef", "\u0d66" - "\u0d6f", "\u0e50" - "\u0e59", "\u0ed0" - "\u0ed9", "\u0f20" - "\u0f29"] > +} + +TOKEN : +{ + < #Extender : ["\u00b7", "\u02d0", "\u02d1", "\u0387", "\u0640", "\u0e46", "\u0ec6", "\u3005", "\u3031" - "\u3035", "\u309d" - "\u309e", "\u30fc" - "\u30fe"] > +} + + +SKIP : +{ + < NotOperatorKeyword : (["0" - "9", "a" - "z", "A" - "Z"])+ > +} + + +SKIP : +{ + < NotNumber : (("." ) | ( ("." (["0" - "9"])*)?)) (["e", "E"] (["+", "-"])? )? ["a" - "z", "A" - "Z"] (["0" - "9", "a" - "z", "A" - "Z"])* > : OPERATOR +} +void QueryList() : +{} +{ + Module() ( [Module()])* +} + +void Module() : +{} +{ + [VersionDecl()] (MainModule() | LibraryModule()) +} + +void VersionDecl() : +{} +{ + {jjtThis.processToken(token);} #XQueryVersion(true) {jjtThis.processToken(token);} #StringLiteralForVersion(true) [{jjtThis.processToken(token);} #XQueryEncoding(true) {jjtThis.processToken(token);} #StringLiteralForVersion(true)] Separator() +} + +void MainModule() : +{} +{ + Prolog() QueryBody() +} + +void LibraryModule() : +{} +{ + ModuleDecl() Prolog() +} + +void ModuleDecl() : +{} +{ + {jjtThis.processToken(token);} #ModuleNamespace(true) {jjtThis.processToken(token);} #NCNameForPrefix(true) {jjtThis.processToken(token);} #AssignEquals(true) {jjtThis.processToken(token);} #URLLiteral(true) Separator() +} + +void Prolog() : +{} +{ + (Setter() Separator())* ((Import() | NamespaceDecl() | DefaultNamespaceDecl()) Separator())* ((VarDecl() | FunctionDecl()) Separator())* +} + +void Setter() : +{} +{ + (XMLSpaceDecl() | DefaultCollationDecl() | BaseURIDecl() | ConstructionDecl() | OrderingModeDecl() | EmptyOrderingDecl() | InheritNamespacesDecl()) +} + +void Import() : +{} +{ + (SchemaImport() | ModuleImport()) +} + +void Separator() : +{} +{ + +} + +void NamespaceDecl() : +{} +{ + {jjtThis.processToken(token);} #DeclareNamespace(true) {jjtThis.processToken(token);} #NCNameForPrefix(true) {jjtThis.processToken(token);} #AssignEquals(true) {jjtThis.processToken(token);} #URLLiteral(true) +} + +void XMLSpaceDecl() : +{} +{ + {jjtThis.processToken(token);} #DeclareXMLSpace(true) ({jjtThis.processToken(token);} #XMLSpacePreserve(true) | {jjtThis.processToken(token);} #XMLSpaceStrip(true)) +} + +void DefaultNamespaceDecl() : +{} +{ + ({jjtThis.processToken(token);} #DeclareDefaultElement(true) | {jjtThis.processToken(token);} #DeclareDefaultFunction(true)) {jjtThis.processToken(token);} #Namespace(true) {jjtThis.processToken(token);} #URLLiteral(true) +} + +void OrderingModeDecl() : +{} +{ + {jjtThis.processToken(token);} #DeclareOrdering(true) ({jjtThis.processToken(token);} #Ordered(true) | {jjtThis.processToken(token);} #Unordered(true)) +} + +void EmptyOrderingDecl() : +{} +{ + {jjtThis.processToken(token);} #DeclareDefaultOrderingEmpty(true) ({jjtThis.processToken(token);} #EmptyGreatest(true) | {jjtThis.processToken(token);} #EmptyLeast(true)) +} + +void InheritNamespacesDecl() : +{} +{ + {jjtThis.processToken(token);} #DeclareInheritNamespaces(true) ({jjtThis.processToken(token);} #Yes(true) | {jjtThis.processToken(token);} #No(true)) +} + +void DefaultCollationDecl() : +{} +{ + {jjtThis.processToken(token);} #DeclareCollation(true) {jjtThis.processToken(token);} #URLLiteral(true) +} + +void BaseURIDecl() : +{} +{ + {jjtThis.processToken(token);} #DeclareBaseURI(true) {jjtThis.processToken(token);} #URLLiteral(true) +} + +void SchemaImport() : +{} +{ + {jjtThis.processToken(token);} #ImportSchemaToken(true) [SchemaPrefix()] {jjtThis.processToken(token);} #URLLiteral(true) [{jjtThis.processToken(token);} #AtStringLiteral(true) ( {jjtThis.processToken(token);} #StringLiteral(true))*] +} + +void SchemaPrefix() : +{} +{ + (({jjtThis.processToken(token);} #Namespace(true) {jjtThis.processToken(token);} #NCNameForPrefix(true) {jjtThis.processToken(token);} #AssignEquals(true)) | ({jjtThis.processToken(token);} #DefaultElement(true) {jjtThis.processToken(token);} #Namespace(true))) +} + +void ModuleImport() : +{} +{ + {jjtThis.processToken(token);} #ImportModuleToken(true) [{jjtThis.processToken(token);} #Namespace(true) {jjtThis.processToken(token);} #NCNameForPrefix(true) {jjtThis.processToken(token);} #AssignEquals(true)] {jjtThis.processToken(token);} #URLLiteral(true) [{jjtThis.processToken(token);} #AtStringLiteral(true) ( {jjtThis.processToken(token);} #StringLiteral(true))*] +} + +void VarDecl() : +{} +{ + {jjtThis.processToken(token);} #DefineVariable(true) {jjtThis.processToken(token);} #VarName(true) [TypeDeclaration()] (({jjtThis.processToken(token);} #ColonEquals(true) ExprSingle()) | {jjtThis.processToken(token);} #External(true)) +} + +void ConstructionDecl() : +{} +{ + {jjtThis.processToken(token);} #DeclareConstruction(true) {jjtThis.processToken(token);} #SchemaModeForDeclareConstruction(true) +} + +void FunctionDecl() : +{} +{ + {jjtThis.processToken(token);} #DefineFunction(true) {jjtThis.processToken(token);} #QNameLpar(true) [ParamList()] ( ( {jjtThis.processToken(token);} #As(true) SequenceType())?) (EnclosedExpr() | {jjtThis.processToken(token);} #External(true)) +} + +void ParamList() : +{} +{ + Param() ( Param())* +} + +void Param() : +{} +{ + {jjtThis.processToken(token);} #VarName(true) [TypeDeclaration()] +} + +void EnclosedExpr() : +{} +{ + ({jjtThis.processToken(token);} #Lbrace(true) | {jjtThis.processToken(token);} #LbraceExprEnclosure(true)) Expr() {jjtThis.processToken(token);} #Rbrace(true) +} + +void QueryBody() : +{} +{ + (Expr()) +} + +void Expr() : +{} +{ + ExprSingle() ( ExprSingle())* +} + +void ExprSingle() #void : +{} +{ + (FLWORExpr() | QuantifiedExpr() | TypeswitchExpr() | IfExpr() | OrExpr()) +} + +void FLWORExpr() : +{} +{ + ((ForClause() | LetClause()))+ [WhereClause()] [OrderByClause()] ExprSingle() +} + +void ForClause() #void : +{} +{ + {jjtThis.processToken(token);} #VarName(true) [TypeDeclaration()] [PositionalVar()] {jjtThis.processToken(token);} #In(true) ExprSingle() ( {jjtThis.processToken(token);} #VarName(true) [TypeDeclaration()] [PositionalVar()] {jjtThis.processToken(token);} #In(true) ExprSingle())* +} + +void PositionalVar() : +{} +{ + {jjtThis.processToken(token);} #AtWord(true) {jjtThis.processToken(token);} #VarName(true) +} + +void LetClause() : +{} +{ + {jjtThis.processToken(token);} #LetVariable(true) {jjtThis.processToken(token);} #VarName(true) [TypeDeclaration()] {jjtThis.processToken(token);} #ColonEquals(true) ExprSingle() ( {jjtThis.processToken(token);} #VarName(true) [TypeDeclaration()] {jjtThis.processToken(token);} #ColonEquals(true) ExprSingle())* +} + +void WhereClause() : +{} +{ + {jjtThis.processToken(token);} #Where(true) ExprSingle() +} + +void OrderByClause() : +{} +{ + ({jjtThis.processToken(token);} #OrderBy(true) | {jjtThis.processToken(token);} #OrderByStable(true)) OrderSpecList() +} + +void OrderSpecList() : +{} +{ + OrderSpec() ( OrderSpec())* +} + +void OrderSpec() : +{} +{ + ExprSingle() OrderModifier() +} + +void OrderModifier() : +{} +{ + [({jjtThis.processToken(token);} #Ascending(true) | {jjtThis.processToken(token);} #Descending(true))] [({jjtThis.processToken(token);} #EmptyGreatest(true) | {jjtThis.processToken(token);} #EmptyLeast(true))] [{jjtThis.processToken(token);} #Collation(true) {jjtThis.processToken(token);} #StringLiteral(true)] +} + +void QuantifiedExpr() : +{} +{ + ({jjtThis.processToken(token);} #Some(true) | {jjtThis.processToken(token);} #Every(true)) {jjtThis.processToken(token);} #VarName(true) [TypeDeclaration()] {jjtThis.processToken(token);} #In(true) ExprSingle() ( {jjtThis.processToken(token);} #VarName(true) [TypeDeclaration()] {jjtThis.processToken(token);} #In(true) ExprSingle())* {jjtThis.processToken(token);} #Satisfies(true) ExprSingle() +} + +void TypeswitchExpr() : +{} +{ + Expr() (CaseClause())+ {jjtThis.processToken(token);} #Default(true) [ {jjtThis.processToken(token);} #VarName(true)] ExprSingle() +} + +void CaseClause() : +{} +{ + {jjtThis.processToken(token);} #Case(true) [ {jjtThis.processToken(token);} #VarName(true) {jjtThis.processToken(token);} #As(true)] SequenceType() ExprSingle() +} + +void IfExpr() : +{} +{ + Expr() ExprSingle() ExprSingle() +} + +void OperatorExpr() #void : +{} +{ + OrExpr() +} + +void OrExpr() #OrExpr(> 1) : +{} +{ + AndExpr() ( + + {binaryTokenStack.push(token);} + AndExpr() + { + try + { + jjtThis.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + throw e; + } + } + #OrExpr(2))* +} + +void AndExpr() #AndExpr(> 1) : +{} +{ + ComparisonExpr() ( + + {binaryTokenStack.push(token);} + ComparisonExpr() + { + try + { + jjtThis.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + throw e; + } + } + #AndExpr(2))* +} + +void ComparisonExpr() #ComparisonExpr(> 1) : +{} +{ + RangeExpr() ((ValueComp() | GeneralComp() | NodeComp()) RangeExpr() + { + try + { + jjtThis.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + throw e; + } + } + #ComparisonExpr(2))? +} + +void RangeExpr() #RangeExpr(> 1) : +{} +{ + AdditiveExpr() ( + + {binaryTokenStack.push(token);} + AdditiveExpr() + { + try + { + jjtThis.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + throw e; + } + } + #RangeExpr(2))? +} + +void AdditiveExpr() #AdditiveExpr(> 1) : +{} +{ + MultiplicativeExpr() (( + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + ) MultiplicativeExpr() + { + try + { + jjtThis.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + throw e; + } + } + #AdditiveExpr(2))* +} + +void MultiplicativeExpr() #MultiplicativeExpr(> 1) : +{} +{ + UnionExpr() (( + + {binaryTokenStack.push(token);} + |

        + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + ) UnionExpr() + { + try + { + jjtThis.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + throw e; + } + } + #MultiplicativeExpr(2))* +} + +void UnionExpr() #UnionExpr(> 1) : +{} +{ + IntersectExceptExpr() (( + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + ) IntersectExceptExpr() + { + try + { + jjtThis.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + throw e; + } + } + #UnionExpr(2))* +} + +void IntersectExceptExpr() #IntersectExceptExpr(> 1) : +{} +{ + InstanceofExpr() (( + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + ) InstanceofExpr() + { + try + { + jjtThis.processToken((Token)binaryTokenStack.pop()); + } + catch(java.util.EmptyStackException e) + { + token_source.printLinePos(); + e.printStackTrace(); + throw e; + } + } + #IntersectExceptExpr(2))* +} + +void InstanceofExpr() #InstanceofExpr(> 1) : +{} +{ + TreatExpr() ( SequenceType())? +} + +void TreatExpr() #TreatExpr(> 1) : +{} +{ + CastableExpr() ( SequenceType())? +} + +void CastableExpr() #CastableExpr(> 1) : +{} +{ + CastExpr() ( SingleType())? +} + +void CastExpr() #CastExpr(> 1) : +{} +{ + UnaryExpr() ({jjtThis.processToken(token);} #CastAs(true) SingleType())? +} + +void UnaryExpr() #UnaryExpr(keepUnary) : +{boolean keepUnary=false;} +{ + ({keepUnary=true;jjtThis.processToken(token);} #UnaryMinus(true) | {keepUnary=true;jjtThis.processToken(token);} #UnaryPlus(true))* ValueExpr() +} + +void ValueExpr() #void : +{} +{ + (ValidateExpr() | PathExpr()) +} + +void GeneralComp() #void : +{} +{ + ( + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + ) +} + +void ValueComp() #void : +{} +{ + ( + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + ) +} + +void NodeComp() #void : +{} +{ + ( + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + | + + {binaryTokenStack.push(token);} + ) +} + +void ValidateExpr() : +{} +{ + ({jjtThis.processToken(token);} #ValidateLbrace(true) | ({jjtThis.processToken(token);} #ValidateSchemaMode(true) {jjtThis.processToken(token);} #LbraceExprEnclosure(true))) Expr() {jjtThis.processToken(token);} #Rbrace(true) +} + +void PathExpr() #PathExpr(>0) : +{} +{ + (({jjtThis.processToken(token);} #Root(true) [RelativePathExpr()]) | ({jjtThis.processToken(token);} #RootDescendants(true) RelativePathExpr()) | RelativePathExpr()) +} + +void RelativePathExpr() #void : +{} +{ + StepExpr() (( | {jjtThis.processToken(token);} #SlashSlash(true)) StepExpr())* +} + +void StepExpr() #StepExpr(>1 || isStep) : +{boolean savedIsStep = isStep; isStep=false;} +{ + ({isStep=true;}AxisStep(){isStep = savedIsStep;} | FilterExpr(){isStep = savedIsStep;}) +} + +void AxisStep() #void : +{} +{ + (ForwardStep() | ReverseStep()) PredicateList() +} + +void ForwardStep() #void : +{} +{ + ((ForwardAxis() NodeTest()) | AbbrevForwardStep()) +} + +void ForwardAxis() #void : +{} +{ + ({jjtThis.processToken(token);} #AxisChild(true) | {jjtThis.processToken(token);} #AxisDescendant(true) | {jjtThis.processToken(token);} #AxisAttribute(true) | {jjtThis.processToken(token);} #AxisSelf(true) | {jjtThis.processToken(token);} #AxisDescendantOrSelf(true) | {jjtThis.processToken(token);} #AxisFollowingSibling(true) | {jjtThis.processToken(token);} #AxisFollowing(true)) +} + +void AbbrevForwardStep() #void : +{} +{ + [{jjtThis.processToken(token);} #At(true)] NodeTest() +} + +void ReverseStep() #void : +{} +{ + ((ReverseAxis() NodeTest()) | AbbrevReverseStep()) +} + +void ReverseAxis() #void : +{} +{ + ({jjtThis.processToken(token);} #AxisParent(true) | {jjtThis.processToken(token);} #AxisAncestor(true) | {jjtThis.processToken(token);} #AxisPrecedingSibling(true) | {jjtThis.processToken(token);} #AxisPreceding(true) | {jjtThis.processToken(token);} #AxisAncestorOrSelf(true)) +} + +void AbbrevReverseStep() #void : +{} +{ + {jjtThis.processToken(token);} #DotDot(true) +} + +void NodeTest() : +{} +{ + (KindTest() | NameTest()) +} + +void NameTest() : +{} +{ + ({jjtThis.processToken(token);} #QName(true) | Wildcard()) +} + +void Wildcard() #void : +{} +{ + ({jjtThis.processToken(token);} #Star(true) | {jjtThis.processToken(token);} #NCNameColonStar(true) | {jjtThis.processToken(token);} #StarColonNCName(true)) +} + +void FilterExpr() #void : +{} +{ + PrimaryExpr() PredicateList() +} + +void PredicateList() #PredicateList(> 0) : +{} +{ + (Predicate())* +} + +void Predicate() #Predicate(> 0) : +{} +{ + Expr() +} + +void PrimaryExpr() #void : +{} +{ + (Literal() | VarRef() | ParenthesizedExpr() | {isStep=true;}ContextItemExpr() | FunctionCall() | Constructor() | OrderedExpr() | UnorderedExpr()) +} + +void Literal() #void : +{} +{ + (NumericLiteral() | {jjtThis.processToken(token);} #StringLiteral(true)) +} + +void NumericLiteral() #void : +{} +{ + ({jjtThis.processToken(token);} #IntegerLiteral(true) | {jjtThis.processToken(token);} #DecimalLiteral(true) | {jjtThis.processToken(token);} #DoubleLiteral(true)) +} + +void VarRef() #void : +{} +{ + {jjtThis.processToken(token);} #VarName(true) +} + +void ParenthesizedExpr() #void : +{} +{ + [Expr()] +} + +void ContextItemExpr() #void : +{} +{ + {jjtThis.processToken(token);} #Dot(true) +} + +void OrderedExpr() #void : +{} +{ + {jjtThis.processToken(token);} #OrderedOpen(true) Expr() {jjtThis.processToken(token);} #Rbrace(true) +} + +void UnorderedExpr() #void : +{} +{ + {jjtThis.processToken(token);} #UnorderedOpen(true) Expr() {jjtThis.processToken(token);} #Rbrace(true) +} + +void FunctionCall() : +{} +{ + ({jjtThis.processToken(token);} #QNameLpar(true)) [ExprSingle() ( ExprSingle())*] +} + +void Constructor() : +{} +{ + (DirectConstructor() | ComputedConstructor()) +} + +void DirectConstructor() : +{} +{ + (DirElemConstructor() | DirCommentConstructor() | DirPIConstructor()) +} + +void DirElemConstructor() : +{} +{ + ({jjtThis.processToken(token);} #StartTagOpenRoot(true) | {jjtThis.processToken(token);} #StartTagOpen(true)) {jjtThis.processToken(token);} #TagQName(true) DirAttributeList() ({jjtThis.processToken(token);} #EmptyTagClose(true) | ({jjtThis.processToken(token);} #StartTagClose(true) (DirElemContent())* {jjtThis.processToken(token);} #EndTagOpen(true) {jjtThis.processToken(token);} #TagQName(true) [{jjtThis.processToken(token);} #S(true)] {jjtThis.processToken(token);} #EndTagClose(true))) +} + +void DirAttributeList() : +{} +{ + ({jjtThis.processToken(token);} #S(true) [{jjtThis.processToken(token);} #TagQName(true) [{jjtThis.processToken(token);} #S(true)] {jjtThis.processToken(token);} #ValueIndicator(true) [{jjtThis.processToken(token);} #S(true)] DirAttributeValue()])* +} + +void DirAttributeValue() : +{} +{ + (({jjtThis.processToken(token);} #OpenQuot(true) (({jjtThis.processToken(token);} #EscapeQuot(true) | QuotAttrValueContent()))* {jjtThis.processToken(token);} #CloseQuot(true)) | ({jjtThis.processToken(token);} #OpenApos(true) (({jjtThis.processToken(token);} #EscapeApos(true) | AposAttrValueContent()))* {jjtThis.processToken(token);} #CloseApos(true))) +} + +void QuotAttrValueContent() : +{} +{ + ({jjtThis.processToken(token);} #QuotAttrContentChar(true) | CommonContent()) +} + +void AposAttrValueContent() : +{} +{ + ({jjtThis.processToken(token);} #AposAttrContentChar(true) | CommonContent()) +} + +void DirElemContent() : +{} +{ + (DirectConstructor() | {jjtThis.processToken(token);} #ElementContentChar(true) | CDataSection() | CommonContent()) +} + +void CommonContent() : +{} +{ + ({jjtThis.processToken(token);} #PredefinedEntityRef(true) | {jjtThis.processToken(token);} #CharRef(true) | {jjtThis.processToken(token);} #LCurlyBraceEscape(true) | {jjtThis.processToken(token);} #RCurlyBraceEscape(true) | EnclosedExpr()) +} + +void DirCommentConstructor() : +{} +{ + ({jjtThis.processToken(token);} #XmlCommentStartForElementContent(true) | {jjtThis.processToken(token);} #XmlCommentStart(true)) DirCommentContents() {jjtThis.processToken(token);} #XmlCommentEnd(true) +} + +void DirCommentContents() : +{} +{ + (({jjtThis.processToken(token);} #CommentContentChar(true) | {jjtThis.processToken(token);} #CommentContentCharDash(true)))* +} + +void DirPIConstructor() : +{} +{ + ({jjtThis.processToken(token);} #ProcessingInstructionStartForElementContent(true) | {jjtThis.processToken(token);} #ProcessingInstructionStart(true)) {jjtThis.processToken(token);} #PITarget(true) [{jjtThis.processToken(token);} #SForPI(true) DirPIContents()] {jjtThis.processToken(token);} #ProcessingInstructionEnd(true) +} + +void DirPIContents() : +{} +{ + ({jjtThis.processToken(token);} #PIContentChar(true))* +} + +void CDataSection() : +{} +{ + ({jjtThis.processToken(token);} #CdataSectionStartForElementContent(true) | {jjtThis.processToken(token);} #CdataSectionStart(true)) CDataSectionContents() {jjtThis.processToken(token);} #CdataSectionEnd(true) +} + +void CDataSectionContents() : +{} +{ + ({jjtThis.processToken(token);} #CDataSectionChar(true))* +} + +void ComputedConstructor() : +{} +{ + (CompDocConstructor() | CompElemConstructor() | CompAttrConstructor() | CompTextConstructor() | CompCommentConstructor() | CompPIConstructor()) +} + +void CompDocConstructor() : +{} +{ + {jjtThis.processToken(token);} #DocumentLbrace(true) Expr() {jjtThis.processToken(token);} #Rbrace(true) +} + +void CompElemConstructor() : +{} +{ + ({jjtThis.processToken(token);} #ElementQNameLbrace(true) | ({jjtThis.processToken(token);} #ElementLbrace(true) Expr() {jjtThis.processToken(token);} #Rbrace(true) {jjtThis.processToken(token);} #LbraceExprEnclosure(true))) [ContentExpr()] {jjtThis.processToken(token);} #Rbrace(true) +} + +void ContentExpr() : +{} +{ + Expr() +} + +void CompAttrConstructor() : +{} +{ + ({jjtThis.processToken(token);} #AttributeQNameLbrace(true) | ({jjtThis.processToken(token);} #AttributeLbrace(true) Expr() {jjtThis.processToken(token);} #Rbrace(true) {jjtThis.processToken(token);} #LbraceExprEnclosure(true))) [Expr()] {jjtThis.processToken(token);} #Rbrace(true) +} + +void CompTextConstructor() : +{} +{ + {jjtThis.processToken(token);} #TextLbrace(true) Expr() {jjtThis.processToken(token);} #Rbrace(true) +} + +void CompCommentConstructor() : +{} +{ + {jjtThis.processToken(token);} #CommentLbrace(true) Expr() {jjtThis.processToken(token);} #Rbrace(true) +} + +void CompPIConstructor() : +{} +{ + ({jjtThis.processToken(token);} #PINCNameLbrace(true) | ({jjtThis.processToken(token);} #PILbrace(true) Expr() {jjtThis.processToken(token);} #Rbrace(true) {jjtThis.processToken(token);} #LbraceExprEnclosure(true))) [Expr()] {jjtThis.processToken(token);} #Rbrace(true) +} + +void SingleType() : +{} +{ + AtomicType() [{jjtThis.processToken(token);} #OccurrenceZeroOrOne(true)] +} + +void TypeDeclaration() : +{} +{ + {jjtThis.processToken(token);} #As(true) SequenceType() +} + +void SequenceType() : +{} +{ + ((ItemType() [OccurrenceIndicator()]) | {jjtThis.processToken(token);} #EmptyTok(true)) +} + +void OccurrenceIndicator() #void : +{} +{ + ({jjtThis.processToken(token);} #OccurrenceZeroOrOne(true) | {jjtThis.processToken(token);} #OccurrenceZeroOrMore(true) | {jjtThis.processToken(token);} #OccurrenceOneOrMore(true)) +} + +void ItemType() #void : +{} +{ + (AtomicType() | KindTest() | {jjtThis.processToken(token);} #Item(true)) +} + +void AtomicType() : +{} +{ + ({jjtThis.processToken(token);} #QNameForAtomicType(true) | {jjtThis.processToken(token);} #QNameForSequenceType(true)) +} + +void KindTest() #void : +{} +{ + (DocumentTest() | ElementTest() | AttributeTest() | SchemaElementTest() | SchemaAttributeTest() | PITest() | CommentTest() | TextTest() | AnyKindTest()) +} + +void AnyKindTest() : +{} +{ + ( | {jjtThis.processToken(token);} #NodeLparForKindTest(true)) +} + +void DocumentTest() : +{} +{ + ({jjtThis.processToken(token);} #DocumentLpar(true) | {jjtThis.processToken(token);} #DocumentLparForKindTest(true)) [(ElementTest() | SchemaElementTest())] +} + +void TextTest() : +{} +{ + ( | {jjtThis.processToken(token);} #TextLparForKindTest(true)) +} + +void CommentTest() : +{} +{ + ( | {jjtThis.processToken(token);} #CommentLparForKindTest(true)) +} + +void PITest() : +{} +{ + ( | {jjtThis.processToken(token);} #ProcessingInstructionLparForKindTest(true)) [({jjtThis.processToken(token);} #NCNameForPI(true) | {jjtThis.processToken(token);} #StringLiteralForKindTest(true))] +} + +void AttributeTest() : +{} +{ + ({jjtThis.processToken(token);} #AttributeType(true) | {jjtThis.processToken(token);} #AttributeTypeForKindTest(true)) [(AttribNameOrWildcard() [{jjtThis.processToken(token);} #CommaForKindTest(true) TypeName()])] +} + +void AttribNameOrWildcard() : +{} +{ + (AttributeName() | {jjtThis.processToken(token);} #AnyName(true)) +} + +void SchemaAttributeTest() : +{} +{ + ({jjtThis.processToken(token);} #SchemaAttributeType(true) | {jjtThis.processToken(token);} #SchemaAttributeTypeForKindTest(true)) AttributeDeclaration() +} + +void AttributeDeclaration() : +{} +{ + AttributeName() +} + +void ElementTest() : +{} +{ + ({jjtThis.processToken(token);} #ElementType(true) | {jjtThis.processToken(token);} #ElementTypeForKindTest(true) | {jjtThis.processToken(token);} #ElementTypeForDocumentTest(true)) [(ElementNameOrWildcard() [{jjtThis.processToken(token);} #CommaForKindTest(true) TypeName() [{jjtThis.processToken(token);} #Nillable(true)]])] +} + +void ElementNameOrWildcard() : +{} +{ + (ElementName() | {jjtThis.processToken(token);} #AnyName(true)) +} + +void SchemaElementTest() : +{} +{ + ({jjtThis.processToken(token);} #SchemaElementType(true) | {jjtThis.processToken(token);} #SchemaElementTypeForKindTest(true) | {jjtThis.processToken(token);} #SchemaElementTypeForDocumentTest(true)) ElementDeclaration() +} + +void ElementDeclaration() : +{} +{ + ElementName() +} + +void AttributeName() : +{} +{ + {jjtThis.processToken(token);} #QNameForItemType(true) +} + +void ElementName() : +{} +{ + {jjtThis.processToken(token);} #QNameForItemType(true) +} + +void TypeName() : +{} +{ + {jjtThis.processToken(token);} #QNameForItemType(true) +} + diff --git a/src/grammar/xpath/javacc.xsl b/jackrabbit-spi-commons/src/main/javacc/xpath/javacc.xsl similarity index 99% rename from src/grammar/xpath/javacc.xsl rename to jackrabbit-spi-commons/src/main/javacc/xpath/javacc.xsl index c920dbb6d69..28df9fdf3ab 100644 --- a/src/grammar/xpath/javacc.xsl +++ b/jackrabbit-spi-commons/src/main/javacc/xpath/javacc.xsl @@ -485,7 +485,7 @@ TOKEN_MGR_DECLS : { SKIP: { - <<skip_>> + < <skip_>> } TOKEN : diff --git a/jackrabbit-spi-commons/src/main/javacc/xpath/jjtree-jackrabbit.xsl b/jackrabbit-spi-commons/src/main/javacc/xpath/jjtree-jackrabbit.xsl new file mode 100644 index 00000000000..5a6862a7cae --- /dev/null +++ b/jackrabbit-spi-commons/src/main/javacc/xpath/jjtree-jackrabbit.xsl @@ -0,0 +1,27 @@ + + + + + + + + + package org.apache.jackrabbit.spi.commons.query.xpath; + + + \ No newline at end of file diff --git a/src/grammar/xpath/jjtree.xsl b/jackrabbit-spi-commons/src/main/javacc/xpath/jjtree.xsl similarity index 100% rename from src/grammar/xpath/jjtree.xsl rename to jackrabbit-spi-commons/src/main/javacc/xpath/jjtree.xsl diff --git a/jackrabbit-spi-commons/src/main/javacc/xpath/strip.xsl b/jackrabbit-spi-commons/src/main/javacc/xpath/strip.xsl new file mode 100644 index 00000000000..95a8801e531 --- /dev/null +++ b/jackrabbit-spi-commons/src/main/javacc/xpath/strip.xsl @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/grammar/xpath/xpath-grammar.xml b/jackrabbit-spi-commons/src/main/javacc/xpath/xpath-grammar.xml similarity index 99% rename from src/grammar/xpath/xpath-grammar.xml rename to jackrabbit-spi-commons/src/main/javacc/xpath/xpath-grammar.xml index 7e1373abb0e..bd09704f6d0 100644 --- a/src/grammar/xpath/xpath-grammar.xml +++ b/jackrabbit-spi-commons/src/main/javacc/xpath/xpath-grammar.xml @@ -1,6 +1,6 @@ - + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/privilege/readtest.xml b/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/privilege/readtest.xml new file mode 100644 index 00000000000..088d4e845a8 --- /dev/null +++ b/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/privilege/readtest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/privilege/writetest.xml b/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/privilege/writetest.xml new file mode 100644 index 00000000000..165a7c3730b --- /dev/null +++ b/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/privilege/writetest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/query/sql2/test.sql2.txt b/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/query/sql2/test.sql2.txt new file mode 100644 index 00000000000..c7c7d755d02 --- /dev/null +++ b/jackrabbit-spi-commons/src/test/resources/org/apache/jackrabbit/spi/commons/query/sql2/test.sql2.txt @@ -0,0 +1,236 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# File encoding: UTF-8 + +# See also jcr-spec.pdf + +select * from test where name=$nt:name + +# 6.7.1 Query (p 99) +select * from test +SELECT * FROM TEST +SeLeCt * FrOm test +select * from test where id=1 +select * from test where id=1 order by id +select * from test order by id + +# 6.7.2 Source (p 99) +# 6.7.3 Selector (p 100) +# 6.7.4 Name (p 100) +select * from test as t +select * from ["Test"] +select * from [test] +select * from [test] as [t] +select * from test as ["t"] +select * from ["test"] as ["t"] + +# 6.7.5 Join (p 102) +# 6.7.6 JoinType (p 102) +# 6.7.7 JoinCondition (p 103) +# 6.7.8 EquiJoinCondition (p 103) +select * from parent inner join child on parent.id=child.parentid +select * from parent as p inner join child as c on p.id=c.parentid +select * from parent as p inner join child as c on p.id=c.parentid +select * from parent as p left outer join child as c on p.id=c.parentid +select * from parent as p right outer join child as c on p.id=c.parentid + +# 6.7.9 SameNodeJoinCondition (p 105) +select * from parent as p inner join child as c on issamenode(p, c) +select * from parent as p inner join child as c on issamenode(p, c, a) +select * from parent as p inner join child as c on issamenode(p, c, [/a/b/c]) +select * from parent as p inner join child as c on issamenode(p, c, ['/a/b/c']) + +# 6.7.10 ChildNodeJoinCondition (p 106) +select * from parent as p inner join child as c on ischildnode(p, c) + +# 6.7.11 DescendantNodeJoinCondition (p 107) +select * from parent as p inner join child as c on isdescendantnode(p, c) +select * from parent as p right outer join child as c on p.id=c.parentid inner join other as x on p.id = x.id + +# 6.7.12 Constraint (p 108) +# 6.7.13 And (p 108) +# 6.7.14 Or (p 109) +select * from test where id<1 and id>1 +select * from test where id=2 or name='Hello' +select * from test where id<1 and (id>1 or id<2) +select * from test where (id<1 and id>1) or id<2 +select * from test where id<1 and id>1 or id<2 +select * from test where (id<1 or id>1) and id<2 +select * from test where id<1 or (id>1 and id<2) +select * from test where id<1 or id>1 and id<2 + +# 6.7.15 Not (p 110) +select * from test where not id=2 +select * from test where not (id=2 and name='Hello') +select * from test where not ([id]=2 and [name]='Hello') +select * from test where id=2 or not (name='Hello' and id=3) +select * from test where (not name='Hello') and id=3 +select * from test where id = 3 and (not name='Hello') + +# 6.7.16 Comparison (p 110) +# 6.7.17 Operator (p 112) +select * from test where id<=2 or id>=3 and name<'a' or name>'c' +select * from test where id<>2 +select * from [test] where [id]<>2 +select * from test where name like 'H%' + +# 6.7.18 PropertyExistence (p 113) +select * from test where name is not null +select * from test as t where t.name is not null and t.id<>0 +select * from test as t where not t.name is not null +select * from test as t where t.name is null +select * from test as t where not t.name is null + +# 6.7.19 FullTextSearch (p 113) +select * from test where contains(name, 'hello -world') +select * from test where contains(name, $x) +select * from test as t where contains(t.*, 'hello -world') +select * from test as t where contains([t].name, 'hello -world') + +# 6.7.20 SameNode (p 115) +select * from test where issamenode([/a/b/c]) +select * from test as a where issamenode(['/a']) +select * from test as x where issamenode(x, ['/a[2]/b/c']) + +# 6.7.21 ChildNode (p 116) +select * from test where ischildnode([/a[1]/b]) +select * from test as a where ischildnode(['/a']) +select * from test as x where ischildnode(x, [/]) +select * from test as x where ischildnode(x, ['/a[1]']) + +# 6.7.22 DescendantNode (p 117) +# 6.7.23 Path (p 118) +select * from test where ISDESCENDANTNODE([/a[1]]) +select * from test as a where ISDESCENDANTNODE([/a]) +select * from test as x where ISDESCENDANTNODE(x, [/a/b/c]) + +# 6.7.24 Operand (p 118) +# 6.7.25 StaticOperand (p 119) +# 6.7.26 DynamicOperand (p 119) +# 6.7.27 PropertyValue (p 119) +# 6.7.28 Length (p 120) +select * from test where length(name)=5 +select * from test as t where length(t.name)=5 +select * from test as t where length(name)=5 +SELECT * FROM [my:thing] WHERE [my:property] = 'abc' +SELECT * FROM [my:thing] AS thing WHERE [my:property] = 'abc' +SELECT * FROM [my:thing] AS [thing] WHERE [thing].[my:property] = 'abc' + +# 6.7.29 NodeName (p 121) +select * from test where name()='test' +select * from test as x where name(x)='test' + +# 6.7.30 NodeLocalName (p 121) +select * from test where localname()='test' +select * from test as x where localname(x)='test' + +# 6.7.31 FullTextSearchScore (p 122) +select * from test where score()>4 +select * from test as x where score(x)<1 + +# 6.7.32 LowerCase (p 122) +select * from test where lower(name)='test' +select * from test where lower(upper(name))='test' +select * from test where lower(localname(test))='test' +select * from test where lower(name(test))='test' +select * from test as x where lower(x.name)='test' + +# 6.7.33 UpperCase (p 123) +select * from test where upper(name)='test' +select * from test where upper(lower(name))='test' +select * from test where upper(localname(test))='test' +select * from test where upper(name(test))='test' +select * from test as x where upper(x.name)='test' + +# 6.7.34 Literal (p 123) +select * from test where amount=0.01 +select * from test where amount=10. +select * from test where amount=.01 +select * from test where amount=.01e-1 +select * from test where amount=-.01e1 +select * from test where amount=-0.01e1 +select * from test where amount=+10 +select * from test where amount=-10e10 +select * from test where amount=+10e-10 +select * from test where amount=+10e+10 +select * from test where active=true +select * from test where active=false +select * from test where name='test''test' +select * from test where name='''' +select * from test where active="True" +select * from test where active="" +select * from test where a=cast('0.01' as string) +select * from test where a=cast('abcdef' as binary) +select * from test where a=cast('+2001-01-01T01:02:03.000Z' as date) +select * from test where a=cast('10' as long) +select * from test where a=cast('100.5' as double) +select * from test where a=cast('3.11' as decimal) +select * from test where a=cast('true' as boolean) +select * from test where a=cast('firstName' as name) +select * from test where a=cast('a/b/c' as path) +select * from test where a=cast('[123]' as reference) +select * from test where a=cast('[123]' as weakreference) +select * from test where a=cast("x://y/z" as uri) + +# 6.7.35 BindVariable (p 124) +select * from test where name=$name +select * from test where name=$x and id=$y +select * from test where name=$x14 +select * from test where name=$_ + +# 6.7.36 Prefix (p 124) +select * from test where name=$nt:name +select * from test where name=$_:name +select * from test where name=$_1:name + +# 6.7.37 Ordering (p 125) +# 6.7.38 Order (p 126) +select * from test order by name +select * from test order by name asc +select * from test order by name desc +select * from test order by id, name +select * from test order by id, name, id, name +select * from test order by id desc, name asc, id, name desc +select * from test order by id desc, name asc, id asc + +# 6.7.39 Column (p 127) +select name from test +select id, name from test +select x.id from test as x +select x.id, name from test as x +select x.id, y.id from test as x inner join test as y on x.id=y.id +select x.id as i from test as x inner join test as y on x.id=y.id +select x.id as i, y.name as n from test as x inner join test as y on x.id=y.id +select x.id, y.name as n from test as x inner join test as y on x.id=y.id +select x.* from test as x +select x.*, y.* from test as x inner join test as y on x.id=y.id +select x.*, x.id as i, y.*, y.name from test as x inner join test as y on x.id=y.id + +#errors +select * from parent as p inner join child as c on issamenode(p, c, a/b) +> exception: Query: select * from parent as p inner join child as c on issamenode(p, c, a/(*)b); expected: ) +select * from parent as p inner join child as c on issamenode(p, c, d, e) +> exception: Query: select * from parent as p inner join child as c on issamenode(p, c, d,(*)e); expected: ) +select * from +> exception: Query: select * fro(*)m; expected: a token +select * from parent as p inner join child as c on ischildnode(p, c, a) +> exception: Query: select * from parent as p inner join child as c on ischildnode(p, c,(*)a); expected: ) +select * from parent as p inner join child as c on isdescendantnode(p) +> exception: Query: select * from parent as p inner join child as c on isdescendantnode(p(*)); expected: , +select * from parent as p inner join child as c on isdescendantnode(a, b, c) +> exception: Query: select * from parent as p inner join child as c on isdescendantnode(a, b,(*)c); expected: ) +select id from parent inner join child on parent.id=child.parentid +> exception: Query: select id(*)from parent inner join child on parent.id=child.parentid; expected: Need to specify the selector name for "id" because the query contains more than one selector. \ No newline at end of file diff --git a/jackrabbit-spi/README.txt b/jackrabbit-spi/README.txt new file mode 100644 index 00000000000..369b1adb73b --- /dev/null +++ b/jackrabbit-spi/README.txt @@ -0,0 +1,19 @@ +========================= +Welcome to Jackrabbit SPI +========================= + +This is the SPI component of the Apache Jackrabbit project. + +The SPI defines a layer within a JSR-170 implementation that separates +the transient space from the persistent layer. The main goals were: + +(1) Defined support of a client/server architecture +A flat SPI-API lends itself to protocol mappings to protocols +like WebDAV, SOAP or others in a straightforward yet meaningful way. + +(2) Implementation Support +Drawing the boundaries between the repository client and the +repository server allows repository implementation to implement +only the "server" portion and leverage existing generic +(opensource) clients for things like the "transient space" etc. +This should ease the implementation of the JSR-170 api. diff --git a/jackrabbit-spi/assembly.xml b/jackrabbit-spi/assembly.xml new file mode 100644 index 00000000000..258992e2293 --- /dev/null +++ b/jackrabbit-spi/assembly.xml @@ -0,0 +1,30 @@ + + + + tests + false + + jar + + + + ${project.build.testOutputDirectory} + + + + diff --git a/jackrabbit-spi/pom.xml b/jackrabbit-spi/pom.xml new file mode 100644 index 00000000000..1ffba33ef71 --- /dev/null +++ b/jackrabbit-spi/pom.xml @@ -0,0 +1,90 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-spi + Jackrabbit SPI + bundle + + + + + org.apache.felix + maven-bundle-plugin + true + + + maven-assembly-plugin + + + package + + single + + + + assembly.xml + + + + + + + maven-surefire-plugin + + true + + + + + + + + org.osgi + org.osgi.annotation + provided + + + javax.jcr + jcr + + + junit + junit + test + + + org.slf4j + slf4j-api + test + + + + diff --git a/jackrabbit-spi/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-spi/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-spi/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Batch.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Batch.java new file mode 100644 index 00000000000..6878797f51a --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Batch.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi; + +import javax.jcr.ItemExistsException; +import javax.jcr.PathNotFoundException; +import javax.jcr.AccessDeniedException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.version.VersionException; + +/** + * The Batch defines an ordered list of of operations that must be + * executed at once on the persistent layer. If any of the modifications added + * to the batch fails, none of the other changes must be persisted, thus leaving + * the persistent layer unaffected. + *

        + * The Batch object is obtained by calling + * {@link RepositoryService#createBatch(SessionInfo,ItemId)}. The following + * methods can then be called on the returned Batch object to queue + * the corresponding operations: + *

          + *
        • addNode,
        • + *
        • addProperty,
        • + *
        • setValue,
        • + *
        • remove,
        • + *
        • reorderNodes,
        • + *
        • setMixins,
        • + *
        • move
        • + *
        + * The operations collected in a Batch are persisted upon a successful call to + * {@link RepositoryService#submit(Batch)}. The operations queued in the batch + * must be validated as a single unit and (if validation succeeds) applied to + * the persistent layer. If validation fails submitting the Batch + * is aborted and an exception is thrown. + *

        + * The Batch mechanism is required because there are sets of operations for + * which the following are both true: + *

          + *
        • The set can only be validated and put into effect as a single logical unit.
          + * For example, adding mutually referring reference properties.
        • + *
        • The set contains operations that can only be validated on the persistent + * layer.
          For example, operations that require a referential integrity check.
        • + *
        + * In addition the Batch mechanism is desirable in order to + * minimize calls to the persistent layer, which enables client-server + * implementations to reduce the number of network roundtrips. + *

        + * Since the batch records the delta of pending changes within the scope of + * an {@link javax.jcr.Item#save()} (or a {@link javax.jcr.Session#save()} it is + * intended to be constructed upon save (not before) and then submitted to the + * persistent layer as a single logical operation (see above). + *

        + * Note however, that methods of the JCR API that have immediate effect on the + * persistent storage have to call that storage, validate and return. The batch + * does not play a role in these operations, instead they are covered by the + * {@link RepositoryService}. + */ +public interface Batch { + + /** + * Add a new node to the persistent layer. + * + * @param parentId NodeId identifying the parent node. + * @param nodeName Name of the node to be created. + * @param nodetypeName Primary node type name of the node to be created. + * @param uuid Value for the jcr:uuid property of the node to be created or + * null. If due to an import the uuid of the resulting node is + * already defined, it must be passed as separate uuid parameter, indicating + * a binding value for the server. Otherwise the uuid must be null. + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.PathNotFoundException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.nodetype.NoSuchNodeTypeException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#addNode(String) + * @see javax.jcr.Node#addNode(String, String) + * @see javax.jcr.Session#importXML(String, java.io.InputStream, int) + * @see javax.jcr.query.Query#storeAsNode(String) + */ + public void addNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) throws RepositoryException; + + /** + * Add a new property to the persistent layer. + *

        + * Note: this call should succeed in case the property already exists. + * + * @param parentId NodeId identifying the parent node. + * @param propertyName Name of the property to be created. + * @param value The value of the property to be created. + * @throws ValueFormatException + * @throws VersionException + * @throws LockException + * @throws ConstraintViolationException + * @throws PathNotFoundException + * @throws ItemExistsException + * @throws AccessDeniedException + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + * @see javax.jcr.Node#setProperty(String, javax.jcr.Value) + * @see javax.jcr.Node#setProperty(String, javax.jcr.Value, int) + * @see javax.jcr.Node#setProperty(String, String) + * @see javax.jcr.Node#setProperty(String, String, int) + * @see javax.jcr.Node#setProperty(String, java.util.Calendar) + * @see javax.jcr.Node#setProperty(String, boolean) + * @see javax.jcr.Node#setProperty(String, double) + * @see javax.jcr.Node#setProperty(String, long) + * @see javax.jcr.Node#setProperty(String, javax.jcr.Node) + * @see javax.jcr.Session#importXML(String, java.io.InputStream, int) + * @see javax.jcr.query.Query#storeAsNode(String) + */ + public void addProperty(NodeId parentId, Name propertyName, QValue value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, PathNotFoundException, ItemExistsException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Add a new multi-valued property to the persistent layer. + *

        + * Note: this call should succeed in case the property already exists. + * + * @param parentId NodeId identifying the parent node. + * @param propertyName Name of the property to be created. + * @param values The values of the property to be created. + * @throws javax.jcr.ValueFormatException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.PathNotFoundException + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#setProperty(String, javax.jcr.Value[]) + * @see javax.jcr.Node#setProperty(String, javax.jcr.Value[], int) + * @see javax.jcr.Node#setProperty(String, String[]) + * @see javax.jcr.Node#setProperty(String, String[], int) + * @see javax.jcr.Session#importXML(String, java.io.InputStream, int) + */ + public void addProperty(NodeId parentId, Name propertyName, QValue[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, PathNotFoundException, ItemExistsException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Modify the value of an existing property. Note that in contrast to + * the JCR API this method should not accept a null value. + * Removing a property is achieved by calling {@link #remove(ItemId)}. + * + * @param propertyId PropertyId identifying the property to be modified. + * @param value The new value. + * @throws ValueFormatException + * @throws VersionException + * @throws LockException + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + * @see javax.jcr.Property#setValue(javax.jcr.Value) + * @see javax.jcr.Property#setValue(String) + * @see javax.jcr.Property#setValue(long) + * @see javax.jcr.Property#setValue(double) + * @see javax.jcr.Property#setValue(java.util.Calendar) + * @see javax.jcr.Property#setValue(boolean) + * @see javax.jcr.Property#setValue(javax.jcr.Node) + */ + public void setValue(PropertyId propertyId, QValue value) throws RepositoryException; + + /** + * Modify the value of an existing, multi-valued property. Note that in + * contrast to the JCR API this method should not accept a null + * value. Removing a property is achieved by calling {@link #remove(ItemId)}. + * + * @param propertyId PropertyId identifying the property to be modified. + * @param values The new values. + * @throws javax.jcr.ValueFormatException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Property#setValue(javax.jcr.Value[]) + * @see javax.jcr.Property#setValue(String[]) + */ + public void setValue(PropertyId propertyId, QValue[] values) throws RepositoryException; + + /** + * Remove an existing item. + * + * @param itemId ItemId identifying the item to be removed. + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Item#remove() + */ + public void remove(ItemId itemId) throws RepositoryException; + + /** + * Modify the order of the child nodes identified by the given + * NodeIds. + * + * @param parentId NodeId identifying the parent node. + * @param srcNodeId NodeId identifying the node to be reordered. + * @param beforeNodeId NodeId identifying the child node, before which the + * source node must be placed. + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.ItemNotFoundException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#orderBefore(String, String) + */ + public void reorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) throws RepositoryException; + + /** + * Modify the set of mixin node types present on the node identified by the + * given id. + * + * @param nodeId NodeId identifying the node to be modified. + * @param mixinNodeTypeNames The new set of mixin types. Compared to the + * previous values this may result in both adding and/or removing mixin types. + * @throws javax.jcr.nodetype.NoSuchNodeTypeException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#addMixin(String) + * @see javax.jcr.Node#removeMixin(String) + */ + public void setMixins(NodeId nodeId, Name[] mixinNodeTypeNames) throws RepositoryException; + + /** + * Change the primary type of the node identified by the given nodeId. + * + * @param nodeId NodeId identifying the node to be modified. + * @param primaryNodeTypeName + * @throws RepositoryException + * @see javax.jcr.Node#setPrimaryType(String) + */ + public void setPrimaryType(NodeId nodeId, Name primaryNodeTypeName) throws RepositoryException; + + /** + * Move the node identified by the given srcNodeId to the + * new parent identified by destParentNodeId and change its + * name to destName. + * + * @param srcNodeId NodeId identifying the node to be moved. + * @param destParentNodeId NodeId identifying the new parent. + * @param destName The new name of the moved node. + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.PathNotFoundException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Session#move(String, String) + */ + public void move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException; + + /** + * Add a new content tree to the persistent layer. + * + * @param parentId + * @param contentTree + * @throws RepositoryException + */ + public void setTree(NodeId parentId, Tree contentTree) throws RepositoryException; +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ChildInfo.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ChildInfo.java new file mode 100644 index 00000000000..5e40f5d57d5 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ChildInfo.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.spi; + +/** + * ChildInfo... + */ +public interface ChildInfo { + + /** + * Returns the name of the child Node. + * + * @return The name of the child Node. + */ + public Name getName(); + + /** + * Returns the uniqueID of the child Node or null + * if the Node is not identified by a uniqueID. + * + * @return The uniqueID of the child Node or null. + * @see ItemId ItemId for a description of the uniqueID defined by the SPI + * item identifiers. + */ + public String getUniqueID(); + + /** + * Returns the index of the child Node. Note, that the index + * is 1-based. In other words: the Node represented + * by this ChildInfo has same name siblings this method will + * always return the default value (1). + * + * @return Returns the index of the child Node. + */ + public int getIndex(); +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Event.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Event.java new file mode 100644 index 00000000000..17a43182475 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Event.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.spi; + +import javax.jcr.RepositoryException; +import java.util.Map; + +/** + * Event is similar to the regular JCR Event and adds additional + * information about the affected item. + */ +public interface Event { + + /** + * An event of this type is generated when a node is added. + */ + public static final int NODE_ADDED = javax.jcr.observation.Event.NODE_ADDED; + + /** + * An event of this type is generated when a node is removed. + */ + public static final int NODE_REMOVED = javax.jcr.observation.Event.NODE_REMOVED; + + /** + * An event of this type is generated when a property is added. + */ + public static final int PROPERTY_ADDED = javax.jcr.observation.Event.PROPERTY_ADDED; + + /** + * An event of this type is generated when a property is removed. + */ + public static final int PROPERTY_REMOVED = javax.jcr.observation.Event.PROPERTY_REMOVED; + + /** + * An event of this type is generated when a property is changed. + */ + public static final int PROPERTY_CHANGED = javax.jcr.observation.Event.PROPERTY_CHANGED; + + + /** + * An event of this type is generated when a node is moved. + * + * @since JCR 2.0 + */ + public static final int NODE_MOVED = javax.jcr.observation.Event.NODE_MOVED; + + /** + * If event bundling is supported, this event is used to indicate a + * bundle boundary within the event journal. + * + * @since JCR 2.0 + */ + public static final int PERSIST = javax.jcr.observation.Event.PERSIST; + + /** + * Constant for observation listener interested in all types of events. + */ + public static final int ALL_TYPES = Event.NODE_ADDED | Event.NODE_REMOVED | + Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED | + Event.NODE_MOVED | Event.PERSIST; + + /** + * Returns the type of this event: a constant defined by this interface. + * One of: + *

          + *
        • {@link #NODE_ADDED}
        • + *
        • {@link #NODE_REMOVED}
        • + *
        • {@link #PROPERTY_ADDED}
        • + *
        • {@link #PROPERTY_REMOVED}
        • + *
        • {@link #PROPERTY_CHANGED}
        • + *
        • {@link #NODE_MOVED}
        • + *
        • {@link #PERSIST}
        • + *
        + * + * @return the type of this event. + */ + public int getType(); + + /** + * @return the path of the affected item. E.g. the added/removed node or the + * property that was added/removed/changed. + */ + public Path getPath(); + + /** + * @return the id of the affected item. + */ + public ItemId getItemId(); + + /** + * @return the id of the parent node of the affected item. + */ + public NodeId getParentId(); + + /** + * @return the name of the primary node type of the 'associated' node of + * this event. + * @see javax.jcr.observation.ObservationManager#addEventListener + */ + public Name getPrimaryNodeTypeName(); + + /** + * @return the names of the mixin types of the 'associated' node of this + * event. + * @see javax.jcr.observation.ObservationManager#addEventListener + */ + public Name[] getMixinTypeNames(); + + /** + * Returns the user ID connected with this event. This is the string + * returned by getUserID of the session that caused the event. + * + * @return a String. + */ + public String getUserID(); + + /** + * Returns the information map associated with this event. + * + * @return A Map containing parameter information. + * @throws RepositoryException if an error occurs. + * @see javax.jcr.observation.Event#getInfo() + * @since JCR 2.0 + */ + public Map getInfo() throws RepositoryException; + + /** + * Returns the user data. + * + * @return the user data + * @see javax.jcr.observation.Event#getUserData() + * @since JCR 2.0 + */ + public String getUserData(); + + /** + * Returns the date when the change was persisted that caused this event. + * + * @return the date when the change was persisted that caused this event. + * @throws javax.jcr.RepositoryException if an error occurs. + * @see javax.jcr.observation.Event#getDate() + * @since JCR 2.0 + */ + public long getDate() throws RepositoryException; +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/EventBundle.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/EventBundle.java new file mode 100644 index 00000000000..0d462944ecc --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/EventBundle.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.spi; + +import java.util.Iterator; + +/** + * An EventBundle is similar to the + * {@link javax.jcr.observation.EventIterator} interface. Other than the + * EventIterator an EventBundle allows to retrieve + * the events multiple times using the {@link #getEvents} method. + */ +public interface EventBundle extends Iterable { + + /** + * Returns the events of this bundle. + * + * @return the {@link Event events} of this bundle. + */ + public Iterator getEvents(); + + /** + * Returns true if this event bundle is associated with a + * change that was initiated by a local session info. Event bundles for + * external changes will aways return false. + * + * @return true if this event bundle is associated with a local + * change, false if this event bundle contains external + * changes. + */ + public boolean isLocal(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/EventFilter.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/EventFilter.java new file mode 100644 index 00000000000..d9bd1f4ec60 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/EventFilter.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.spi; + +import java.io.Serializable; + +/** + * An EventFilter is applied to the events as generated on the + * repository server. Event filter instances can be created with {@link + * RepositoryService#createEventFilter(SessionInfo, int, + * Path, boolean, String[], Name[], boolean)}. + * Some repository implementations may + * also support event filters that are directly instantiated by the client. + */ +public interface EventFilter extends Serializable { + + /** + * If an implementation returns true the event + * will be included in the event bundle returned by {@link + * RepositoryService#getEvents(Subscription, long)}. A return + * value of false indicates that the client is not interested + * in the event. + * + * @param event the event in question. + * @param isLocal flag indicating whether this is a local event. + * @return true if the event is accepted by the filter; + * false otherwise. + */ + public boolean accept(Event event, boolean isLocal); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/IdFactory.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/IdFactory.java new file mode 100644 index 00000000000..a2a33371401 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/IdFactory.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.spi; + +/** + * IdFactory defines methods to construct new ItemIds. + * This factory is intended to build ItemIds from the parameters + * passed to the various create methods and should not make an attempt to + * apply additional logic such as e.g. roundtrips to the server or resolution of + * Paths. Similarly the SPI implementation namely the + * {@link RepositoryService} must be able to deal with the various formats of + * an ItemId, since a caller may not (yet) be aware of the uniqueID + * part of an ItemId. + */ +public interface IdFactory { + + /** + * Creates a new PropertyId from the given parent id and + * property name. + * + * @param parentId + * @param propertyName + * @return a new PropertyId. + */ + public PropertyId createPropertyId(NodeId parentId, Name propertyName); + + /** + * Creates a new NodeId from the given parent id and + * the given Path object. + * + * @param parentId + * @param path + * @return a new NodeId. + */ + public NodeId createNodeId(NodeId parentId, Path path); + + /** + * Creates a new NodeId from the given unique id (which identifies + * an ancestor Node) and the given Path object. + * + * @param uniqueID + * @param path + * @return a new NodeId. + * @see ItemId ItemId for a description of the uniqueID defined by the SPI + * item identifiers. + */ + public NodeId createNodeId(String uniqueID, Path path); + + /** + * Creates a new NodeId from the given unique id. + * + * @param uniqueID + * @return a new NodeId. + * @see ItemId ItemId for a description of the uniqueID defined by the SPI + * item identifiers. + */ + public NodeId createNodeId(String uniqueID); + + /** + * Returns the JCR string representation of the given nodeId. + * + * @return a JCR node identifier string. + * @see #fromJcrIdentifier(String) + */ + public String toJcrIdentifier(NodeId nodeId); + + /** + * Create a new NodeId from the given JCR string representation. + * + * @param jcrIdentifier + * @return a new NodeId. + * @see #toJcrIdentifier(NodeId) + */ + public NodeId fromJcrIdentifier(String jcrIdentifier); +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemId.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemId.java new file mode 100644 index 00000000000..56e2497662b --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemId.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.spi; + +/** + * An ItemId identifies an item using a combination of unique ID + * and path. There are three basic forms of an ItemId. The following + * table shows each of the allowed combinations where an X in + * the column indicates that a value is set and a - indicates + * that the value is null: + * + * + * + * + * + * + * + * + *
        UniqueIDPathUsage
        X-The item can be identified with a unique ID. In most cases such an item + * is also mix:referenceable but there is no restriction in that respect. An + * SPI implementation may also use a unique ID to identify non-referenceable nodes. + * Whether a node is referenceable is purely governed by its node type or + * the assigned mixin types. Note, that the format of the ID it is left to the + * implementation.
        -XThe item can not be identified with a unique ID and none of its ancestors + * can be identified with a unique ID. The item is identified by an absolute path. + *
        XXThe item can not be identified with a unique ID but one of its ancestors + * can. {@link #getUniqueID} returns the unique ID of the nearest ancestor, which + * can be identified with a unique ID. The relative path provides a navigation + * path from the above mentioned ancestor to the item identified by the + * ItemId. + *
        + *

        + * Two ItemIds should be considered equal if both the unique part + * and the path part are equal AND if they denote the same + * {@link #denotesNode() type} of ItemId. + */ +public interface ItemId { + + /** + * @return true if this ItemId identifies a node; + * otherwise false. + */ + public boolean denotesNode(); + + /** + * @return the uniqueID part of this item id or null if the + * identified item nor any of its ancestors can be identified with a + * uniqueID. + */ + public String getUniqueID(); + + /** + * @return the path part of this item id. Returns null + * if this item can be identified solely with a uniqueID. + */ + public Path getPath(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemInfo.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemInfo.java new file mode 100644 index 00000000000..ef488502a1f --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemInfo.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.spi; + +/** + * The ItemInfo is the base interface of {@link NodeInfo} + * and {@link PropertyInfo}. + */ +public interface ItemInfo { + + /** + * @return identifier for the item that is based on this info object. the id + * can either be an absolute path or a uniqueID (+ relative path). + * @see RepositoryService#getNodeInfo(SessionInfo, NodeId) + */ + public ItemId getId(); + + /** + * Returns true if this ItemInfo denotes a node, false otherwise. + * + * @return true if this ItemInfo denotes a node, false otherwise. + */ + public boolean denotesNode(); + + /** + * Returns the Path of the item represented by this + * ItemInfo. + * + * @return the Path of the item represented by this + * item info. + */ + public Path getPath(); +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemInfoCache.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemInfoCache.java new file mode 100644 index 00000000000..7daad5169e4 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/ItemInfoCache.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.spi; + +/** + * ItemInfoCache instances are responsible for caching + * {@link ItemInfo}s along with an opaque generation counter. Implementations + * are free on the particular caching policy. That is, how long (if at all) item + * infos are cached. + * + * An ItemInfoCache is supplied per session from the {@link RepositoryService}. It is used + * to cache ItemInfos read from the RepositoryService. + * + * @see RepositoryService#getItemInfos(SessionInfo, ItemId) + */ +public interface ItemInfoCache { + + /** + * This class represents a cache entry. + * @param Either a {@link NodeInfo} or a {@link PropertyInfo}. + */ + class Entry { + + /** + * The {@link ItemInfo} + */ + public final T info; + + /** + * The generation + */ + public final long generation; + + /** + * Create a new cache entry containing info with a given generation. + * @param info + * @param generation + */ + public Entry(T info, long generation) { + this.info = info; + this.generation = generation; + } + + @Override + public int hashCode() { + return info.hashCode() + (int) generation; + } + + @Override + public boolean equals(Object that) { + if (that == null) { + return false; + } + + if (that == this) { + return true; + } + + if (that instanceof ItemInfoCache.Entry) { + ItemInfoCache.Entry other = (ItemInfoCache.Entry) that; + return generation == other.generation && info.equals(other.info); + } + + return false; + } + } + + /** + * Retrieve a cache entry for the given nodeId or null + * if no such entry is in the cache. + * + * @param nodeId id of the entry to lookup. + * @return a Entry<NodeInfo> instance or null + * if not found. + */ + ItemInfoCache.Entry getNodeInfo(NodeId nodeId); + + /** + * Retrieve a cache entry for the given propertyId or null + * if no such entry is in the cache. + * + * @param propertyId id of the entry to lookup. + * @return a Entry<PropertyInfo> instance or + * null if not found. + */ + ItemInfoCache.Entry getPropertyInfo(PropertyId propertyId); + + /** + * Create a {@link Entry} for info and generation and put it into + * the cache. + * @param info + * @param generation + */ + void put(ItemInfo info, long generation); + + /** + * Clear the cache and dispose all entries. + */ + void dispose(); +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/LockInfo.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/LockInfo.java new file mode 100644 index 00000000000..4a5d38157e6 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/LockInfo.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.spi; + +/** + * LockInfo is used to transport lock information across the SPI + * boundary. + * + * @see RepositoryService#getLockInfo(SessionInfo, NodeId) + * @see RepositoryService#lock(SessionInfo, NodeId, boolean, boolean) + */ +public interface LockInfo { + + /** + * Returns the lock token for this lock or null if the token + * should not be exposed to the API user. + * + * @return lock token or null + * @see javax.jcr.lock.Lock#getLockToken() + */ + public String getLockToken(); + + /** + * Returns the user ID of the user who owns this lock or some user defined + * information about the lock owner. + * + * @return user ID of the user who owns this lock. + * @see javax.jcr.lock.Lock#getLockOwner() + */ + public String getOwner(); + + /** + * Returns true if the Lock is deep. False otherwise. + * + * @return true if the Lock is deep. False otherwise. + * @see javax.jcr.lock.Lock#isDeep() + */ + public boolean isDeep(); + + /** + * Returns true if the Lock is session scoped. False otherwise. + * + * @return true if the Lock is session scoped. False otherwise. + * @see javax.jcr.lock.Lock#isSessionScoped() + */ + public boolean isSessionScoped(); + + /** + * Returns the seconds remaining until the lock times out or + * ({@link Long#MAX_VALUE} if the timeout is unknown or infinite). + * + * @return number of seconds until the lock times out. + * @see javax.jcr.lock.Lock#getSecondsRemaining() + * @since JCR 2.0 + */ + public long getSecondsRemaining(); + + /** + * Returns true if the SessionInfo used to + * retrieve this LockInfo is the lock holder and thus enabled + * to refresh or release the lock. + * + * @return true if the SessionInfo used to + * retrieve this LockInfo is the lock holder. + * @see javax.jcr.lock.Lock#isLockOwningSession() + * @since JCR 2.0 + */ + public boolean isLockOwner(); + + /** + * Returns the NodeId of the lock-holding Node. + * + * @return the NodeId of the lock-holding Node. + */ + public NodeId getNodeId(); +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Name.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Name.java new file mode 100644 index 00000000000..9a8b0238d52 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Name.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.spi; + +import java.io.Serializable; + +/** + * A Name is a combination of a namespace URI and a local part. + * Instances of this class are used to internally represent the names of JCR + * content items and other objects within a content repository. + *

        + * A Name is immutable once created. + *

        + * The String representation of a Name object must be in the + * format "{namespaceURI}localPart". + *

        + * An implementation of the Name interface must implement the + * {@link Object#equals(Object)} method such that two Name objects are equal if + * both the namespace URI and the local part are equal. + */ +public interface Name extends Comparable, Cloneable, Serializable { + + // default namespace (empty uri) + public static final String NS_EMPTY_PREFIX = ""; + public static final String NS_DEFAULT_URI = ""; + + // reserved namespace for repository internal node types + public static final String NS_REP_PREFIX = "rep"; + public static final String NS_REP_URI = "internal"; + + // reserved namespace for items defined by built-in node types + public static final String NS_JCR_PREFIX = "jcr"; + public static final String NS_JCR_URI = "http://www.jcp.org/jcr/1.0"; + + // reserved namespace for built-in primary node types + public static final String NS_NT_PREFIX = "nt"; + public static final String NS_NT_URI = "http://www.jcp.org/jcr/nt/1.0"; + + // reserved namespace for built-in mixin node types + public static final String NS_MIX_PREFIX = "mix"; + public static final String NS_MIX_URI = "http://www.jcp.org/jcr/mix/1.0"; + + // reserved namespace used in the system view XML serialization format + public static final String NS_SV_PREFIX = "sv"; + public static final String NS_SV_URI = "http://www.jcp.org/jcr/sv/1.0"; + + // reserved namespaces that must not be redefined and should not be used + public static final String NS_XML_PREFIX = "xml"; + public static final String NS_XML_URI = "http://www.w3.org/XML/1998/namespace"; + public static final String NS_XMLNS_PREFIX = "xmlns"; + public static final String NS_XMLNS_URI = "http://www.w3.org/2000/xmlns/"; + + /** + * Empty array of Name + */ + public static final Name[] EMPTY_ARRAY = new Name[0]; + + /** + * Returns the local part of this Name object. + * + * @return local name + */ + public String getLocalName(); + + /** + * Returns the namespace URI of this Name object. + * + * @return namespace URI + */ + public String getNamespaceURI(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NameFactory.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NameFactory.java new file mode 100644 index 00000000000..1e85888e071 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NameFactory.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.spi; + +/** + * NameFactory... + */ +public interface NameFactory { + + /** + * Returns a Name with the given namespace URI and + * local part and validates the given parameters. + * + * @param namespaceURI namespace uri + * @param localName local part + * @throws IllegalArgumentException if namespaceURI or + * localName is invalid. + */ + public Name create(String namespaceURI, String localName) throws IllegalArgumentException; + + /** + * Returns a Name holding the value of the specified + * string. The string must be in the format returned by the + * Name.toString() method, i.e. + *

        + * {namespaceURI}localName + * + * @param nameString a String containing the Name + * representation to be parsed. + * @return the Name represented by the argument + * @throws IllegalArgumentException if the specified string can not be parsed + * as a Name. + */ + public Name create(String nameString) throws IllegalArgumentException; +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NodeId.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NodeId.java new file mode 100644 index 00000000000..fd997a99051 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NodeId.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.spi; + +/** + * NodeId identifies a node on the SPI layer. The interface does + * not add additional methods. + */ +public interface NodeId extends ItemId { + +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NodeInfo.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NodeInfo.java new file mode 100644 index 00000000000..da56567d520 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/NodeInfo.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.spi; + +import java.util.Iterator; + +/** + * The NodeInfo provides the basic information required to build + * nodes making up the repository hierarchy. + *

        + * Note however, that the list of child nodes does not form part of a + * NodeInfo. It is retrieved by calling + * {@link RepositoryService#getChildInfos(SessionInfo, NodeId)}. In case of + * {@link RepositoryService#getItemInfos(SessionInfo, ItemId) batch read} the + * child nodes might be part of the returned Iterator. + */ +public interface NodeInfo extends ItemInfo { + + /** + * Returns the NodeId for the node that is based on this info + * object. + * + * @return identifier for the item that is based on this info object. the id + * can either be an absolute path or a uniqueID (+ relative path). + * @see RepositoryService#getNodeInfo(SessionInfo, NodeId) + */ + public NodeId getId(); + + /** + * Index of the node. + * + * @return the index. + */ + public int getIndex(); + + /** + * @return Name representing the name of the primary nodetype. + */ + public Name getNodetype(); + + /** + * @return Array of Names representing the names of mixin nodetypes. + * This includes only explicitly assigned mixin nodetypes. It does not include + * mixin types inherited through the addition of supertypes to the primary + * type hierarchy. If there are no mixin node types assigned an empty + * array will be returned. + */ + public Name[] getMixins(); + + /** + * Return the {@link PropertyId Id}s of the properties that are referencing the + * node based on this info object. + * + * @return {@link PropertyId Id}s of the properties that are referencing the + * node based on this info object or an empty array if the node is not + * referenceable or no references exist. + * @see PropertyInfo#getId() + * @deprecated Use {@link RepositoryService#getReferences(SessionInfo, NodeId, Name, boolean)} instead. + */ + public PropertyId[] getReferences(); + + /** + * @return {@link PropertyId Id}s of children properties + * @see PropertyInfo#getId() + */ + public Iterator getPropertyIds(); + + /** + * Return all ChildInfos of the node represent by + * this info, an empty iterator if that node doesn't have any child nodes + * or null if the implementation is not able or for some + * internal reasons not willing to compute the ChildInfo + * iterator. In the latter case the user of this API must call + * {@link RepositoryService#getChildInfos(SessionInfo, NodeId)} in order + * to determine the existence and identity of the child nodes. + * + * @return An iterator of ChildInfos or null if + * the implementation is not able or willing to compute the set of + * ChildInfos (e.g. an implementation may choose to return + * null if there is a huge amount of child nodes). In this + * case {@link RepositoryService#getChildInfos(SessionInfo, NodeId)} will + * be used to load the ChildInfos. + */ + public Iterator getChildInfos(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Path.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Path.java new file mode 100644 index 00000000000..6b071a5165a --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Path.java @@ -0,0 +1,589 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi; + +import java.io.Serializable; + +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +/** + * The Path interface defines the SPI level representation of + * a JCR path. It consists of an ordered list of {@link Path.Element} objects + * and is immutable. + *

        + * A {@link Path.Element} is either {@link Path.Element#denotesName() named} + * or one of the following special elements: + *

          + *
        • the {@link Element#denotesCurrent() current} element (Notation: "."),
        • + *
        • the {@link Element#denotesParent() parent} element (Notation: ".."),
        • + *
        • the {@link Element#denotesRoot() root} element (Notation: {}), which can + * only occur as the first element in a path, or
        • + *
        • an {@link Element#denotesIdentifier() identifier} element, which can + * only occur as the first element in a path.
        • + *
        + *

        + * A Path is defined to have the following characteristics: + *

        + * Equality:
        + * Two paths are equal if they consist of the same elements. + *

        + * Length:
        + * The {@link Path#getLength() length} of a path is the number of its elements. + *

        + * Depth:
        + * The {@link Path#getDepth() depth} of a path is + *

          + *
        • 0 for the root path,
        • + *
        • 0 for a path consisting of an identifier element only,
        • + *
        • 0 for the path consisting of the current element only,
        • + *
        • -1 for the path consisting of the parent element only,
        • + *
        • 1 for the path consisting of a single named element,
        • + *
        • depth(P) + depth(Q) for the path P/Q.
        • + *
        + *

        + * The depth of a normalized absolute path equals its length minus 1. + *

        + * Absolute vs. Relative
        + * A path can be absolute or relative:
        + * A path {@link #isAbsolute() is absolute} if its first element is the root + * or an identifier element. A path is relative if it is not absolute. + *

          + *
        • An absolute path is valid if its depth is greater or equals 0. A relative + * path is always valid.
        • + *
        • Two absolute paths are equivalent if "they refer to the same item in the + * hierarchy".
        • + *
        • Two relative paths P and Q are equivalent if for every absolute path R such + * that R/P and R/Q are valid, R/P and R/Q are equivalent.
        • + *
        + *

        + * Normalization:
        + * A path P {@link Path#isNormalized() is normalized} if P has minimal length + * amongst the set of all paths Q which are equivalent to P.
        + * This means that '.' and '..' elements are resolved as much as possible. + * An absolute path it is normalized if it is not identifier-based and + * contains no current or parent elements. The normalization of a path + * is unique.
        + *

        + * Equivalence:
        + * Path P is {@link Path#isEquivalentTo(Path) equivalent} to path Q (in the above sense) + * if the normalization of P is equal to the normalization of Q. This is + * an equivalence relation (i.e. reflexive, transitive, + * and symmetric). + *

        + * Canonical Paths:
        + * A path {@link Path#isCanonical() is canonical} if its absolute and normalized. + *

        + * Hierarchical Relationship:
        + * The ancestor relationship is a strict partial order (i.e. irreflexive, transitive, + * and asymmetric). Path P is a direct ancestor of path Q if P is equivalent to Q/.. + *
        + * Path P is an {@link Path#isAncestorOf(Path) ancestor} of path Q if + *

          + *
        • P is a direct ancestor of Q or
        • + *
        • P is a direct ancestor of some path S which is an ancestor of Q.
        • + *
        + *

        + * Path P is an {@link Path#isDescendantOf(Path) descendant} of path Q if + *

          + *
        • Path P is a descendant of path Q if Q is an ancestor of P.
        • + *
        + */ +public interface Path extends Serializable { + + /** + * Constant representing an undefined index value + */ + public static final int INDEX_UNDEFINED = 0; + + /** + * Constant representing the default (initial) index value. + */ + public static final int INDEX_DEFAULT = 1; + + /** + * Constant defining the depth of the root path + */ + public static final int ROOT_DEPTH = 0; + + /** + * Delimiter used in order to concatenate the Path.Element objects + * upon {@link Path#getString()}. + */ + public static final char DELIMITER = '\t'; + + /** + * Returns the name of the last path element, or null + * for an identifier. The names of the special root, current and parent + * elements are "", "." and ".." in the default namespace. + * + * @return name of the last path element, or null + */ + Name getName(); + + /** + * Returns the index of the last path element, or {@link #INDEX_UNDEFINED} + * if the index is not defined or not applicable. The index of an + * identifier or the special root, current or parent element is always + * undefined. + * + * @return index of the last path element, or {@link #INDEX_UNDEFINED} + */ + int getIndex(); + + /** + * Returns the normalized index of the last path element. The normalized + * index of an element with an undefined index is {@link #INDEX_DEFAULT}. + * + * @return normalized index of the last path element + */ + int getNormalizedIndex(); + + /** + * Returns the identifier of a single identifier element. Returns null + * for non-identifier paths or identifier paths with other relative path + * elements. + * + * @return identifier, or null + */ + String getIdentifier(); + + /** + * Tests whether this is the root path, i.e. "/". + * + * @return true if this is the root path, + * false otherwise. + */ + boolean denotesRoot(); + + /** + * Test if this path consists of a single identifier element. + * + * @return true if this path is an identifier + */ + boolean denotesIdentifier(); + + /** + * Checks if the last path element is the parent element (".."). + * + * @return true if the last path element is the parent element, + * false otherwise + */ + boolean denotesParent(); + + /** + * Checks if the last path element is the current element ("."). + * + * @return true if the last path element is the current element, + * false otherwise + */ + boolean denotesCurrent(); + + /** + * Checks if the last path element is a named and optionally indexed + * element. + * + * @return true if the last path element is a named element, + * false otherwise + */ + boolean denotesName(); + + /** + * Test if this path represents an unresolved identifier-based path. + * + * @return true if this path represents an unresolved + * identifier-based path. + */ + boolean isIdentifierBased(); + + /** + * Tests whether this path is absolute, i.e. whether it starts with "/" or + * is an identifier based path. + * + * @return true if this path is absolute; false otherwise. + */ + public boolean isAbsolute(); + + /** + * Tests whether this path is canonical, i.e. whether it is absolute and + * does not contain redundant elements such as "." and "..". + * + * @return true if this path is canonical; false otherwise. + * @see #isAbsolute() + */ + public boolean isCanonical(); + + /** + * Tests whether this path is normalized, i.e. whether it does not + * contain redundant elements such as "." and "..". + *

        + * Note that a normalized path can still contain ".." elements if they are + * not redundant, e.g. "../../a/b/c" would be a normalized relative path, + * whereas "../a/../../a/b/c" wouldn't (although they're semantically + * equivalent). + * + * @return true if this path is normalized; false otherwise. + * @see #getNormalizedPath() + */ + public boolean isNormalized(); + + /** + * Returns the normalized path representation of this path. + *

        + * If the path cannot be normalized (e.g. if an absolute path is normalized + * that would result in a 'negative' path) a RepositoryException is thrown. + * + * @return a normalized path representation of this path. + * @throws RepositoryException if the path cannot be normalized. + * @see #isNormalized() + */ + public Path getNormalizedPath() throws RepositoryException; + + /** + * Returns the canonical path representation of this path. + *

        + * If the path is relative or cannot be normalized a RepositoryException + * is thrown. + * + * @return a canonical path representation of this path. + * @throws RepositoryException if this path can not be canonicalized + * (e.g. if it is relative). + */ + public Path getCanonicalPath() throws RepositoryException; + + /** + * Resolves the given path element against this path. If the given + * element is absolute (i.e. the root or an identifier element), then + * a path containing just that element is returned. Otherwise the + * returned path consists of this path followed by the given element. + * + * @param element path element + * @return resolved path + */ + Path resolve(Element element); + + /** + * Resolves the given path against this path. If the given path is + * absolute, then it is returned as-is. Otherwise the path is resolved + * relative to this path and the resulting resolved path is returned. + * + * @param relative the path to be resolved + * @return resolved path + */ + Path resolve(Path relative); + + /** + * Computes the relative path from this absolute path to + * other. + * + * @param other an absolute path. + * @return the relative path from this path to other + * path. + * @throws RepositoryException if either this or + * other path is not absolute. + */ + public Path computeRelativePath(Path other) throws RepositoryException; + + /** + * Normalizes this path and returns the ancestor path of the specified + * relative degree. + *

        + * An ancestor of relative degree x is the path that is x + * levels up along the path. + *

          + *
        • degree = 0 returns this path. + *
        • degree = 1 returns the parent of this path. + *
        • degree = 2 returns the grandparent of this path. + *
        • And so on to degree = n, where n is the depth + * of this path, which returns the root path. + *
        + *

        + * If this path is relative the implementation may not be able to determine + * if the ancestor at degree exists. Such an implementation + * should properly build the ancestor (i.e. parent of .. is ../..) and + * leave if it the caller to throw PathNotFoundException. + * + * @param degree the relative degree of the requested ancestor. + * @return the normalized ancestor path of the specified degree. + * @throws IllegalArgumentException if degree is negative. + * @throws PathNotFoundException if the implementation is able to determine + * that there is no ancestor of the specified degree. In case of this + * being an absolute path, this would be the case if degree is + * greater that the {@link #getDepth() depth} of this path. + * @throws RepositoryException If the implementation is not able to determine + * the ancestor of the specified degree for some other reason. + */ + public Path getAncestor(int degree) throws IllegalArgumentException, PathNotFoundException, RepositoryException; + + /** + * Returns the number of ancestors of this path. This is the equivalent + * of {@link #getDepth()} in case of a absolute path. + * For relative path the number of ancestors cannot be determined and + * -1 should be returned. + * + * @return the number of ancestors of this path or -1 if the number of + * ancestors cannot be determined. + * @see #getDepth() + * @see #getLength() + * @see #isCanonical() + */ + public int getAncestorCount(); + + /** + * Returns the length of this path, i.e. the number of its elements. + * Note that the root element "/" counts as a separate element, e.g. + * the length of "/a/b/c" is 4 whereas the length of "a/b/c" is 3. + *

        + * Also note that the special elements "." and ".." are not treated + * specially, e.g. both "/a/./.." and "/a/b/c" have a length of 4 + * but this value does not necessarily reflect the true hierarchy level as + * returned by {@link #getDepth()}. + * + * @return the length of this path + * @see #getDepth() + * @see #getAncestorCount() + */ + public int getLength(); + + /** + * Returns the depth of this path. The depth reflects the absolute or + * relative hierarchy level this path is representing, depending on whether + * this path is an absolute or a relative path. The depth also takes '.' + * and '..' elements into account. The depth of the root path, an + * identifier and the current path must be 0. + *

        + * Note that the returned value might be negative if this path is not + * canonical, e.g. the depth of "../../a" is -1. + * + * @return the depth this path + * @see #getLength() + * @see #getAncestorCount() + */ + public int getDepth(); + + /** + * Determines if the the other path would be equal to this + * path if both of them are normalized. + * + * @param other Another path. + * @return true if the given other path is equivalent to this path. + * @throws IllegalArgumentException if the given path is null + * or if not both paths are either absolute or relative. + * @throws RepositoryException if any of the path cannot be normalized. + */ + public boolean isEquivalentTo(Path other) throws IllegalArgumentException, RepositoryException; + + /** + * Determines if this path is an ancestor of the specified path, + * based on their (absolute or relative) hierarchy level as returned by + * {@link #getDepth()}. In case of undefined ancestor/descendant + * relationship that might occur with relative paths, the return value + * should be false. + * + * @return true if other is a descendant; + * otherwise false. + * @throws IllegalArgumentException if the given path is null + * or if not both paths are either absolute or relative. + * @throws RepositoryException if any of the path cannot be normalized. + * @see #getDepth() + */ + public boolean isAncestorOf(Path other) throws IllegalArgumentException, RepositoryException; + + /** + * Determines if this path is a descendant of the specified path, + * based on their (absolute or relative) hierarchy level as returned by + * {@link #getDepth()}. In case of undefined ancestor/descendant + * relationship that might occur with relative paths, the return value + * should be false. + * + * @return true if other is an ancestor; + * otherwise false. + * @throws IllegalArgumentException if the given path is null + * or if not both paths are either absolute or relative. + * @throws RepositoryException if any of the path cannot be normalized. + * @see #isAncestorOf(Path) + */ + public boolean isDescendantOf(Path other) throws IllegalArgumentException, RepositoryException; + + /** + * Returns a new Path consisting of those Path.Element objects + * between the given from, inclusive, and the given to, + * exclusive. An IllegalArgumentException is thrown if from + * is greater or equal than to or if any of both params is + * out of the possible range. + * + * @param from index of the element to start with and low endpoint + * (inclusive) within the list of elements to use for the sub-path. + * @param to index of the element outside of the range i.e. high endpoint + * (exclusive) within the list of elements to use for the sub-path. + * @return a new Path consisting of those Path.Element objects + * between the given from, inclusive, and the given + * to, exclusive. + * @throws IllegalArgumentException if from + * is greater or equal than to or if any of both params is + * out of the possible range. + */ + public Path subPath(int from, int to) throws IllegalArgumentException; + + /** + * Returns the elements of this path. + * + * @return the elements of this path. + */ + public Element[] getElements(); + + /** + * Returns the name element (i.e. the last element) of this path. + * + * @return the name element of this path + */ + public Element getNameElement(); + + /** + * Returns a path that consists of only the last element of this path. + * + * @see #getFirstElements() + * @return last element of this path + */ + Path getLastElement(); + + /** + * Returns a path that consists of all but the last element of this path. + * Returns null if this path contains just a single element. + * + * @see #getLastElement() + * @return first elements of this path, or null + */ + Path getFirstElements(); + + /** + * Returns the String representation of this Path as it is used + * by {@link PathFactory#create(String)}. + *

        + * The String representation must consist of the String representation of + * its elements separated by {@link Path#DELIMITER}. + * + * @see Path.Element#getString() + * @return Returns the String representation of this Path. + */ + public String getString(); + + //----------------------------------------------------< inner interface >--- + /** + * Object representation of a single JCR path element. An Element + * object contains the Name and optional index of a single + * JCR path element. + *

        + * Once created, a Element object must be immutable. + *

        + * The String presentation of an Element must be in the format + * "{namespaceURI}localPart" or + * "{namespaceURI}localPart[index]" case of an index greater + * than {@link Path#INDEX_DEFAULT}. + *

        + * Note, that the implementation must implement the equals method such, that + * two Element objects having equals Names and the + * same normalized index must be equal. + */ + public interface Element extends Serializable { + + /** + * Returns the name of this path element. + * + * @return The name of this path element. + */ + public Name getName(); + + /** + * Returns the index of the element as it has been assigned upon creation. + * + * @return index of the element as it has been assigned upon creation. + */ + public int getIndex(); + + /** + * Returns the normalized index of this path element, i.e. the index + * is always equals or greater that {@link Path#INDEX_DEFAULT}. + * + * @return the normalized index. + */ + public int getNormalizedIndex(); + + /** + * Returns the identifier of an identifier element, or + * null for other kinds of elements. + * + * @return identifier, or null + */ + String getIdentifier(); + + /** + * Returns true if this element denotes the root element, + * otherwise returns false. + * + * @return true if this element denotes the root + * element; otherwise false + */ + public boolean denotesRoot(); + + /** + * Returns true if this element denotes the parent + * ('..') element, otherwise returns false. + * + * @return true if this element denotes the parent + * element; otherwise false + */ + public boolean denotesParent(); + + /** + * Returns true if this element denotes the current + * ('.') element, otherwise returns false. + * + * @return true if this element denotes the current + * element; otherwise false + */ + public boolean denotesCurrent(); + + /** + * Returns true if this element represents a regular name + * (i.e. neither root, '.' nor '..'), otherwise returns false. + * + * @return true if this element represents a regular name; + * otherwise false + */ + public boolean denotesName(); + + /** + * Returns true if this element represents an identifier element. + * + * @return true if this element represents an identifier element. + * @since JCR 2.0 + */ + public boolean denotesIdentifier(); + + /** + * Return the String presentation of a {@link Path.Element}. It must be + * in the format "{namespaceURI}localPart" or + * "{namespaceURI}localPart[index]" in case of an index + * greater than {@link Path#INDEX_DEFAULT}. + * + * @return String representation of a {@link Path.Element}. + */ + public String getString(); + + } +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PathFactory.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PathFactory.java new file mode 100644 index 00000000000..27d6c6aef4b --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PathFactory.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi; + +import javax.jcr.RepositoryException; + +/** + * PathFactory... + */ +public interface PathFactory { + + /** + * Return a new Path out of the given parent path + * and the given relative path. If normalize is + * true, the returned path will be normalized (or + * canonicalized, if the parent path is absolute). + * + * @param parent + * @param relPath + * @param normalize + * @return + * @throws IllegalArgumentException if relPath is absolute. + * @throws RepositoryException If the normalized is + * true and the resulting path cannot be normalized. + */ + public Path create(Path parent, Path relPath, boolean normalize) throws IllegalArgumentException, RepositoryException; + + /** + * Creates a new Path out of the given parent path + * and the give name. If normalize is true, + * the returned path will be normalized (or canonicalized, if the parent + * path is absolute). Use {@link PathFactory#create(Path, Name, int, boolean)} + * in order to build a Path having an index with his name element. + * + * @param parent the parent path + * @param name the name of the new path element. + * @param normalize If true the Path is normalized before being returned. + * @return + * @throws RepositoryException If the normalized is + * true and the resulting path cannot be normalized. + */ + public Path create(Path parent, Name name, boolean normalize) throws RepositoryException; + + /** + * Creates a new Path out of the given parent path + * and the give name and normalized index. See also + * {@link PathFactory#create(Path, Name, boolean)}. + * + * @param parent the parent path. + * @param name the name of the new path element. + * @param index the index of the new path element. + * @param normalize If true the Path is normalized before being returned. + * @return + * @throws IllegalArgumentException If the given index is lower than + * {@link Path#INDEX_UNDEFINED}. + * @throws RepositoryException If the normalized is + * true and the resulting path cannot be normalized. + */ + public Path create(Path parent, Name name, int index, boolean normalize) throws IllegalArgumentException, RepositoryException; + + /** + * Creates a relative path based on a {@link Name}. + * + * @param name single {@link Name} for this relative path. + * @return the relative path created from name. + * @throws IllegalArgumentException if the name is null. + */ + public Path create(Name name) throws IllegalArgumentException; + + /** + * Creates a relative path based on a {@link Name} and a normalized index. + * Same as {@link #create(Name)} but allows to explicitly specify an + * index. + * + * @param name single {@link Name} for this relative path. + * @param index index of the single name element. + * @return the relative path created from name and normalizedIndex. + * @throws IllegalArgumentException if index is lower + * than {@link Path#INDEX_UNDEFINED} or if the name is not valid. + */ + public Path create(Name name, int index) throws IllegalArgumentException; + + /** + * Creates a path from the given element. + * + * @param element path element + * @return the created path + * @throws IllegalArgumentException if the given element is null + */ + Path create(Path.Element element) throws IllegalArgumentException; + + /** + * Create a new Path from the given elements. + * + * @param elements + * @return the Path created from the elements. + * @throws IllegalArgumentException If the given elements are null + * or have a length of 0 or would otherwise constitute an invalid path. + */ + public Path create(Path.Element[] elements) throws IllegalArgumentException; + + /** + * Returns a Path holding the value of the specified + * string. The string must be in the format returned by the + * Path.getString() method. + * + * @param pathString a String containing the Path + * representation to be parsed. + * @return the Path represented by the argument + * @throws IllegalArgumentException if the specified string can not be parsed + * as a Path. + * @see Path#getString() + * @see Path#DELIMITER + */ + public Path create(String pathString) throws IllegalArgumentException; + + /** + * Creates a path element from the given name. + * The created path element does not contain an explicit index. + *

        + * If the specified name denotes a special path element (either + * {@link PathFactory#getParentElement()}, {@link PathFactory#getCurrentElement()} or + * {@link PathFactory#getRootElement()}) then the associated constant is returned. + * + * @param name the name of the element + * @return a path element + * @throws IllegalArgumentException if the name is null + */ + public Path.Element createElement(Name name) throws IllegalArgumentException; + + /** + * Same as {@link #createElement(Name)} except that an explicit index can be + * specified. + *

        + * Note that an IllegalArgumentException will be thrown if the specified + * name denotes a special path element (either + * {@link PathFactory#getParentElement()}, {@link PathFactory#getCurrentElement()} or + * {@link PathFactory#getRootElement()}) since an explicit index is not allowed + * in this context. + * + * @param name the name of the element + * @param index the index if the element. + * @return a path element + * @throws IllegalArgumentException if the name is null, + * if the given index is lower than {@link Path#INDEX_UNDEFINED} or if name + * denoting a special path element. + */ + public Path.Element createElement(Name name, int index) throws IllegalArgumentException; + + /** + * Creates a path element from the given identifier. + * + * @param identifier Node identifier for which the path element should be created. + * @return a path element. + * @throws IllegalArgumentException If the identifier is null. + * @since JCR 2.0 + */ + public Path.Element createElement(String identifier) throws IllegalArgumentException; + + /** + * Return the current element. + * + * @return the current element. + */ + public Path.Element getCurrentElement(); + + /** + * Return the parent element. + * + * @return the parent element. + */ + public Path.Element getParentElement(); + + /** + * Return the root element. + * + * @return the root element. + */ + public Path.Element getRootElement(); + + /** + * Return the Path of the root node. + * + * @return the Path of the root node. + */ + public Path getRootPath(); +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PrivilegeDefinition.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PrivilegeDefinition.java new file mode 100644 index 00000000000..6fae75ff8a4 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PrivilegeDefinition.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.spi; + +import java.util.Set; + +/** + * PrivilegeDefinition... + */ +public interface PrivilegeDefinition { + + Name getName(); + + public boolean isAbstract(); + + public Set getDeclaredAggregateNames(); +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PropertyId.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PropertyId.java new file mode 100644 index 00000000000..2143c6822eb --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PropertyId.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.spi; + +/** + * PropertyId identifies a property on the SPI layer. + */ +public interface PropertyId extends ItemId { + + /** + * Returns the NodeId of the parent. + * + * @return The {@link NodeId parentId} of this PropertyId. + */ + public NodeId getParentId(); + + /** + * Returns the {@link Name} of the property identified by this id. + * + * @return The name of the property that is identified by this PropertyId. + */ + public Name getName(); +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PropertyInfo.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PropertyInfo.java new file mode 100644 index 00000000000..80351778d90 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/PropertyInfo.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.spi; + +/** + * The PropertyInfo provides the basic information to build a + * Property. The definition must be calculated from the parent + * node type or retrieved from the RepositoryService. + */ +public interface PropertyInfo extends ItemInfo { + + /** + * @return identifier for the item that is based on this info object. the id + * can either be an absolute path or a uniqueID (+ relative path). + * @see RepositoryService#getNodeInfo(SessionInfo, NodeId) + */ + public PropertyId getId(); + + /** + * @return The {@link javax.jcr.PropertyType type} of the Property + * base on this PropertyInfo. Note, that + * {@link javax.jcr.PropertyType#UNDEFINED} will never be returned as the + * value of a Property always has a defined type. + * @see javax.jcr.PropertyType + */ + public int getType(); + + /** + * @return true if the Property based on this info object is + * multivalue. + * @see javax.jcr.nodetype.PropertyDefinition#isMultiple() + */ + public boolean isMultiValued(); + + /** + * @return The values present on this PropertyInfo. + */ + public QValue[] getValues(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QItemDefinition.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QItemDefinition.java new file mode 100644 index 00000000000..67393ccf2b8 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QItemDefinition.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.spi; + +import javax.jcr.nodetype.ItemDefinition; + +/** + * QItemDefinition is the SPI representation of + * an {@link ItemDefinition item definition}. It refers to Names + * only and is thus isolated from session-specific namespace mappings. + * + * @see javax.jcr.nodetype.ItemDefinition + */ +public interface QItemDefinition { + + /** + * Empty array of QItemDefinition. + */ + public static final QItemDefinition[] EMPTY_ARRAY = new QItemDefinition[0]; + + /** + * Gets the name of the child item. + * + * @return the name of the child item. + */ + public Name getName(); + + /** + * Gets the name of the declaring node type. + * + * @return the name of the declaring node type. + */ + public Name getDeclaringNodeType(); + + /** + * Determines whether the item is 'autoCreated'. + * + * @return the 'autoCreated' flag. + */ + public boolean isAutoCreated(); + + /** + * Gets the 'onParentVersion' attribute of the item. + * + * @return the 'onParentVersion' attribute. + */ + public int getOnParentVersion(); + + /** + * Determines whether the item is 'protected'. + * + * @return the 'protected' flag. + */ + public boolean isProtected(); + + /** + * Determines whether the item is 'mandatory'. + * + * @return the 'mandatory' flag. + */ + public boolean isMandatory(); + + /** + * Determines whether this item definition defines a residual set of + * child items. + * + * @return true if this definition defines a residual set; + * false otherwise. + */ + public boolean definesResidual(); + + /** + * Determines whether this item definition defines a node. + * + * @return true if this is a node definition; + * false otherwise (i.e. it is a property definition). + */ + public boolean definesNode(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QNodeDefinition.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QNodeDefinition.java new file mode 100644 index 00000000000..8afccad430b --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QNodeDefinition.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.spi; + +import javax.jcr.nodetype.NodeDefinition; + +/** + * QNodeDefinition is the SPI representation of + * a {@link NodeDefinition node definition}. It refers to Names only + * and is thus isolated from session-specific namespace mappings. + * + * @see javax.jcr.nodetype.NodeDefinition + */ +public interface QNodeDefinition extends QItemDefinition { + + /** + * Empty array of QNodeDefinition. + */ + public static final QNodeDefinition[] EMPTY_ARRAY = new QNodeDefinition[0]; + + /** + * Returns the name of the default primary type. + * + * @return the name of the default primary type. + */ + public Name getDefaultPrimaryType(); + + /** + * Returns the array of names of the required primary types. + * + * @return the array of names of the required primary types. + */ + public Name[] getRequiredPrimaryTypes(); + + /** + * Reports whether this node can have same-name siblings. + * + * @return the 'allowsSameNameSiblings' flag. + */ + public boolean allowsSameNameSiblings(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QNodeTypeDefinition.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QNodeTypeDefinition.java new file mode 100644 index 00000000000..4e085035963 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QNodeTypeDefinition.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.spi; + +import java.util.Collection; + +/** + * QNodeTypeDefinition is the SPI representation of a + * {@link javax.jcr.nodetype.NodeType node type}. It refers to Names + * only and is therefore independent of session-specific namespace mappings. + * + * @see javax.jcr.nodetype.NodeType + */ +public interface QNodeTypeDefinition { + + /** + * Returns the name of the node type being defined or + * null if not set. + * + * @return the name of the node type or null if not set. + */ + public Name getName(); + + /** + * Returns an array containing the names of the supertypes. If no + * supertypes have been specified, then an empty array is returned + * for mixin types and the nt:base primary type and + * an array containing just nt:base for other primary types. + *

        + * The returned array must not be modified by the application. + * + * @return an array of supertype names + */ + public Name[] getSupertypes(); + + /** + * Returns an array containing the names of additional mixin types + * supported on this node type. + *

        + * The returned array must not be modified by the application. + * + * @return an array of mixin type names, or null + * when there are no known constraints. + */ + public Name[] getSupportedMixinTypes(); + + /** + * Returns the value of the mixin flag. + * + * @return true if this is a mixin node type; false otherwise. + */ + public boolean isMixin(); + + /** + * Returns true if the definition is abstract; false otherwise. + * + * @return true if the definition is abstract; false otherwise. + * @since JCR 2.0 + */ + public boolean isAbstract(); + + /** + * Returns true if the definition is queryable; false otherwise. + * + * @return true if the definition is queryable; false otherwise. + * @since JCR 2.0 + */ + public boolean isQueryable(); + + /** + * Returns the value of the orderableChildNodes flag. + * + * @return true if nodes of this node type can have orderable child nodes; false otherwise. + */ + public boolean hasOrderableChildNodes(); + + /** + * Returns the name of the primary item (one of the child items of the + * node's of this node type) or null if not set. + * + * @return the name of the primary item or null if not set. + */ + public Name getPrimaryItemName(); + + /** + * Returns an array containing the property definitions. + * + * @return an array containing the property definitions. + */ + public QPropertyDefinition[] getPropertyDefs(); + + /** + * Returns an array containing the child node definitions. + * + * @return an array containing the child node definitions. + */ + public QNodeDefinition[] getChildNodeDefs(); + + /** + * Returns a collection of node type Names that are being + * referenced by this node type definition (e.g. as supertypes, as + * required/default primary types in child node definitions, as REFERENCE + * value constraints in property definitions). + *

        + * Note that self-references (e.g. a child node definition that specifies + * the declaring node type as the default primary type) are not considered + * dependencies. + * + * @return a collection of node type Names + */ + public Collection getDependencies(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QPropertyDefinition.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QPropertyDefinition.java new file mode 100644 index 00000000000..9d3283596c3 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QPropertyDefinition.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.spi; + +import javax.jcr.nodetype.PropertyDefinition; + +/** + * QPropertyDefinition is the SPI representation of + * a {@link PropertyDefinition property definition}. It refers to Names, + * SPI default values and value constraints only and is thus isolated + * from session-specific namespace mappings. + * + * @see javax.jcr.nodetype.PropertyDefinition + */ +public interface QPropertyDefinition extends QItemDefinition { + + /** + * Empty array of QPropertyDefinition. + */ + public static final QPropertyDefinition[] EMPTY_ARRAY = new QPropertyDefinition[0]; + + /** + * Returns the required type. + * + * @return the required type. + */ + public int getRequiredType(); + + /** + * Returns the array of value constraints. + * + * @return the array of value constraints. + */ + public QValueConstraint[] getValueConstraints(); + + /** + * Returns the array of default values or null if no default + * values are defined. + * + * @return the array of default values or null + */ + public QValue[] getDefaultValues(); + + /** + * Reports whether this property can have multiple values. + * + * @return the 'multiple' flag. + */ + public boolean isMultiple(); + + /** + * Returns the available query operators. + * + * @return the available query operators. + * @since JCR 2.0 + */ + public String[] getAvailableQueryOperators(); + + /** + * Reports whether this property definition is full text searchable. + * + * @return true if this property definition is full text searchable. + * @since JCR 2.0 + */ + public boolean isFullTextSearchable(); + + /** + * Reports whether this property definition is query-orderable. + * + * @return true if this property definition is query-orderable. + * @since JCR 2.0 + */ + public boolean isQueryOrderable(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValue.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValue.java new file mode 100644 index 00000000000..3c272a46197 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValue.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.spi; + +import java.io.InputStream; +import java.util.Calendar; +import java.math.BigDecimal; +import java.net.URI; + +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Binary; + +/** + * QValue is the SPI representation of a + * {@link javax.jcr.Value jcr value}. It therefore refers to Names + * and Paths only and is thus isolated from session-specific + * namespace mappings. + */ +public interface QValue { + + public static final QValue[] EMPTY_ARRAY = new QValue[0]; + + /** + * Returns the PropertyType of this QValue object. + * It may be either of the value property types defined by the JSR 283: + *

          + *
        • {@link PropertyType#STRING}
        • + *
        • {@link PropertyType#BINARY}
        • + *
        • {@link PropertyType#BOOLEAN}
        • + *
        • {@link PropertyType#DATE}
        • + *
        • {@link PropertyType#DECIMAL}
        • + *
        • {@link PropertyType#DOUBLE}
        • + *
        • {@link PropertyType#LONG}
        • + *
        • {@link PropertyType#NAME}
        • + *
        • {@link PropertyType#PATH}
        • + *
        • {@link PropertyType#REFERENCE}
        • + *
        • {@link PropertyType#URI}
        • + *
        • {@link PropertyType#WEAKREFERENCE}
        • + *
        + * + * @return the PropertyType of this QValue object. + */ + public int getType(); + + /** + * Returns the length of the internal value or -1 if the implementation + * cannot determine the length at this time.
        + * NOTE: for {@link PropertyType#NAME} and {@link PropertyType#PATH} the + * length of the internal value must not be used for indicating the length + * of a property such as retrieved by calling {@link Property#getLength()} + * and {@link Property#getLengths()}. + * + * @return length of this QValue object. + * @throws RepositoryException + */ + public long getLength() throws RepositoryException; + + /** + * Returns a String representation of this QValue + * object. + * + * @return A String representation of this QValue + * object. + * @throws RepositoryException + */ + public String getString() throws RepositoryException; + + /** + * Returns an InputStream representation of this QValue + * object. This method always returns a new stream. + * + * @return A stream representation of this value. + * @throws RepositoryException + */ + public InputStream getStream() throws RepositoryException; + + /** + * Returns a Binary representation of this QValue + * object. + * + * @return A Binary representation of this value. + * @throws RepositoryException + */ + public Binary getBinary() throws RepositoryException; + + /** + * Returns a Calendar representation of this value. + * + * @return A Calendar representation of this value. + * @throws RepositoryException if an error occurs. + */ + public Calendar getCalendar() throws RepositoryException; + + /** + * Returns a BigDecimal representation of this value. + * + * @return A BigDecimal representation of this value. + * @throws RepositoryException if an error occurs. + */ + public BigDecimal getDecimal() throws RepositoryException; + + /** + * Returns a double representation of this value. + * + * @return A double representation of this value. + * @throws RepositoryException if an error occurs. + */ + public double getDouble() throws RepositoryException; + + /** + * Returns a long representation of this value. + * + * @return A long representation of this value. + * @throws RepositoryException if an error occurs. + */ + public long getLong() throws RepositoryException; + + /** + * Returns a boolean representation of this value. + * + * @return A boolean representation of this value. + * @throws RepositoryException if an error occurs. + */ + public boolean getBoolean() throws RepositoryException; + + + /** + * Returns a Name representation of this value. + * + * @return A Name representation of this value. + * @throws RepositoryException if an error occurs. + */ + public Name getName() throws RepositoryException; + + /** + * Returns a Path representation of this value. + * + * @return A Path representation of this value. + * @throws RepositoryException if an error occurs. + */ + public Path getPath() throws RepositoryException; + + /** + * Returns an URI representation of this value. + * + * @return A URI representation of this value. + * @throws RepositoryException if an error occurs. + */ + public URI getURI() throws RepositoryException; + + /** + * Frees temporarily allocated resources such as temporary file, buffer, etc. + */ + public void discard(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValueConstraint.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValueConstraint.java new file mode 100644 index 00000000000..6f1448ca6dc --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValueConstraint.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.spi; + +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.PropertyDefinition; +import javax.jcr.RepositoryException; + +/** + * QValueConstraint is used to check the syntax of a value + * constraint and to test if a specific value satisfies it. + * + * @see PropertyDefinition#getValueConstraints() + */ +public interface QValueConstraint { + + /** + * Empty array of QValueConstraint. + */ + public static final QValueConstraint[] EMPTY_ARRAY = new QValueConstraint[0]; + + /** + * Check if the specified value matches this constraint. + * + * @param value The value to be tested. + * @throws ConstraintViolationException If the specified value is + * null or does not matches the constraint. + * @throws RepositoryException If another error occurs. + */ + void check(QValue value) throws ConstraintViolationException, RepositoryException; + + /** + * For constraints that are not namespace prefix mapping sensitive this + * method returns the same defined in + * {@link PropertyDefinition#getValueConstraints()}. + *

        + * Those that are namespace prefix mapping sensitive (e.g. + * NameConstraint, PathConstraint and + * ReferenceConstraint) return an internal string. + * + * @return the internal definition String + */ + String getString(); + +} \ No newline at end of file diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValueFactory.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValueFactory.java new file mode 100644 index 00000000000..3cc3a97e5af --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QValueFactory.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; +import java.math.BigDecimal; +import java.net.URI; + +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +/** + * QValueFactory defines methods to create QValue + * instances. + */ +public interface QValueFactory { + + /** + * Create a new QValue using the given String representation + * of the value and its {@link javax.jcr.PropertyType type}. + * + * @param value String representation of the new QValue. Note, + * that the given String must never be null. + * @param type A valid {@link javax.jcr.PropertyType type}. + * @return a new QValue. + * @throws ValueFormatException If the given value cannot be + * converted to the specified type. + * @throws RepositoryException If another error occurs. + * @see QValue#getType() + */ + public QValue create(String value, int type) throws ValueFormatException, RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#DATE}. + * + * @param value A non-null Calendar object acting as value + * of the new QValue. + * @return a new QValue. + */ + public QValue create(Calendar value) throws RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#DOUBLE}. + * + * @param value A double containing the value + * of the new QValue. + * @return a new QValue. + */ + public QValue create(double value) throws RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#LONG}. + * + * @param value A long containing the value + * of the new QValue. + * @return a new QValue. + */ + public QValue create(long value) throws RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#BOOLEAN}. + * + * @param value A boolean containing the value + * of the new QValue. + * @return a new QValue. + */ + public QValue create(boolean value) throws RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#NAME}. + * + * @param value A non-null Name. + * @return a new QValue. + */ + public QValue create(Name value) throws RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#PATH}. + * + * @param value A non-null Path. + * @return a new QValue. + */ + public QValue create(Path value) throws RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#DECIMAL}. + * + * @param value A non-null BigDecimal. + * @return a new QValue. + */ + public QValue create(BigDecimal value) throws RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#URI}. + * + * @param value A non-null URI. + * @return a new QValue. + */ + public QValue create(URI value) throws RepositoryException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#BINARY}. + * + * @param value + * @return a new QValue. + */ + public QValue create(byte[] value) throws RepositoryException; + + /** + * Creates a QValue that contains the given binary stream. + * The given stream is consumed and closed by this method. The type of the + * resulting QValue will be {@link javax.jcr.PropertyType#BINARY}. + * + * @param value binary stream + * @return a new binary QValue. + * @throws RepositoryException if the value could not be created + * @throws IOException if the stream can not be consumed + */ + public QValue create(InputStream value) throws RepositoryException, IOException; + + /** + * Create a new QValue with type {@link javax.jcr.PropertyType#BINARY}. + * + * @param value + * @return a new binary QValue. + * @throws IOException + */ + public QValue create(File value) throws RepositoryException, IOException; + + /** + * Given the QPropertyDefinition of an autocreated + * property, compute suitable values to be used in transient space until + * the newly created node gets saved. + * + * @param propertyDefinition definition of property for which values should be created + * @return computed value + * @throws RepositoryException + */ + public QValue[] computeAutoValues(QPropertyDefinition propertyDefinition) throws RepositoryException; +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QueryInfo.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QueryInfo.java new file mode 100644 index 00000000000..fe42d247acf --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QueryInfo.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.spi; + +import javax.jcr.RangeIterator; +import javax.jcr.RepositoryException; + +import java.util.Map; + +/** + * QueryInfo is the the return value of + * {@link RepositoryService#executeQuery(SessionInfo, String, String, Map, long, long, Map)} + * which is used to run a query on the RepositoryService. It + * provides access to the rows of the query result as well as to the column + * names. + * + * @see javax.jcr.query.QueryResult#getRows() + * @see javax.jcr.query.QueryResult#getColumnNames() + * @see javax.jcr.query.QueryResult#getNodes() + */ +public interface QueryInfo { + + /** + * @return an iterator over the {@link QueryResultRow}s. + * @see javax.jcr.query.QueryResult#getRows() + */ + public RangeIterator getRows(); + + /** + * @return an array of Strings representing the column names of + * the query result. + * @see javax.jcr.query.QueryResult#getColumnNames() + */ + public String[] getColumnNames(); + + /** + * @return an array of Strings representing the selector names of + * the query result. + * @see javax.jcr.query.QueryResult#getSelectorNames() + */ + public String[] getSelectorNames(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QueryResultRow.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QueryResultRow.java new file mode 100644 index 00000000000..1750814d073 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/QueryResultRow.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.spi; + +/** + * QueryResultRow represents the SPI equivalent of a query result + * row. It provides access to the id of the Node this row represents as well + * as to the score and to the values represented in this result row. + */ +public interface QueryResultRow { + + /** + * Returns {@link NodeId} of node for a given selectorname this + * result row represents. + * + * @param selectorName the name of a selector or null for the + * default selector. + * @return node id of the Node this result row represents or + * null if there is no node present in this row for the + * given selector name. + * @see javax.jcr.query.Row#getNode() + * @see javax.jcr.query.Row#getNode(String) + */ + public NodeId getNodeId(String selectorName); + + /** + * Returns score for the given selectorName of this result row. + * + * @param selectorName the name of a selector or null for the + * default selector. + * @return score for the given selector in this result row. + */ + public double getScore(String selectorName); + + /** + * Returns an array of QValues. + * + * @return an array of QValues representing the values present + * in this result row. + * @see javax.jcr.query.Row#getValue(String) + * @see javax.jcr.query.Row#getValues() + */ + public QValue[] getValues(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/RepositoryService.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/RepositoryService.java new file mode 100644 index 00000000000..a01385bec7b --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/RepositoryService.java @@ -0,0 +1,1328 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi; + +import java.io.InputStream; +import java.util.Iterator; +import java.util.Map; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemExistsException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.MergeException; +import javax.jcr.NamespaceException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ValueFormatException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.version.Version; +import javax.jcr.version.VersionException; + +/** + * The RepositoryService interface defines methods used to + * retrieve information from the persistent layer of the repository as well + * as the methods that modify its persistent state. + * The implementation of this interface is intended to hold only the state of + * the persistent layer, no session-related state should be held. Consequently, + * each method that alters persistent state always includes all the information + * necessary to fully specify and authorize a change. + *

        + * For example, consider the method + *

        + *    void RepositoryService.copy(SessionInfo sessionInfo,
        + *                                String srcWorkspaceName,
        + *                                NodeId srcNodeId, NodeId destParentNodeId,
        + *                                Name destName)
        + * 
        + * This method performs an immediate persistent copy of the node identified by + * srcNodeId and that node's subtree to a position as child of the node + * identified by destParentNodeId and assigns the newly copied node the name + * destName.
        + * The SessionInfo object provides user and workspace identification + * as well as eventual lock tokens required to execute the copy.
        + * If srcWorkspaceName differs from the workspace name present with + * the SessionInfo, the copy is corresponds to a copy across workspaces. + * The source and destination of the copy operation are specified by + * {@link NodeId}s. The Name holds the new name. Taken together, + * this information is sufficient to completely specify and authorize the copy + * operations. + *

        + * The RepositoryService in addition allows to create and submit {@link Batch} + * objects, that cover lists of operations that have to be applied to the + * persistent layer at once. + */ +public interface RepositoryService { + + /** + * Return the IdFactory. + * + * @return The IdFactory. + * @throws RepositoryException If an error occurs. + */ + public IdFactory getIdFactory() throws RepositoryException; + + /** + * Return the NameFactory. + * + * @return The NameFactory. + * @throws RepositoryException If an error occurs. + */ + public NameFactory getNameFactory() throws RepositoryException; + + /** + * Return the PathFactory. + * + * @return The PathFactory. + * @throws RepositoryException If an error occurs. + */ + public PathFactory getPathFactory() throws RepositoryException; + + /** + * Return the QValueFactory defined with this SPI implementation. + * + * @return The QValueFactory. + * @throws RepositoryException If an error occurs. + */ + public QValueFactory getQValueFactory() throws RepositoryException; + + /** + * Returns a {@link ItemInfoCache} for the given SessionInfo. + * @param sessionInfo + * @return + * @throws RepositoryException + */ + public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) throws RepositoryException; + + //-------------------------------------------------------------------------- + /** + * Returns all property descriptors that can be exposed with the + * {@link javax.jcr.Repository Repository} implementation built on top of + * this RepositoryService. + * + * @return key-value pairs for repository descriptor keys and values. + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Repository#getDescriptorKeys() + * @see javax.jcr.Repository#getDescriptor(String) + */ + public Map getRepositoryDescriptors() throws RepositoryException; + + //-----------------------------------< SessionInfo creation and release >--- + /** + * Returns a SessionInfo that will be used by other methods + * on the RepositoryService. + * An implementation may choose to authenticate the user using the supplied + * credentials. + * + * @param credentials the credentials of the user. + * @param workspaceName the name of the workspace the SessionInfo + * should be built for. If the specified workspaceName is null + * the implementation should select a default workspace. + * @return a SessionInfo if authentication was successful. + * @throws LoginException if authentication of the user fails. + * @throws NoSuchWorkspaceException if the specified workspaceName + * is not recognized. + * @throws RepositoryException if an error occurs. + */ + public SessionInfo obtain(Credentials credentials, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException; + + /** + * Returns a new SessionInfo for the given workspace name that + * will be used by other methods on the RepositoryService. + * + * @param sessionInfo for another workspace + * @param workspaceName the name of the workspace the new SessionInfo + * should be built for. If the specified workspaceName is null + * the implementation should select a default workspace. + * @return a SessionInfo if authentication was successful. + * @throws LoginException if authentication of the user fails. + * @throws NoSuchWorkspaceException if the specified workspaceName + * is not recognized. + * @throws RepositoryException if an error occurs. + */ + public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException; + + /** + * Returns a SessionInfo that will be used by other methods + * on the RepositoryService. + * + * @param sessionInfo + * @param credentials + * @return a SessionInfo if impersonate was successful. + * @throws LoginException + * @throws RepositoryException + * @see javax.jcr.Session#impersonate(javax.jcr.Credentials) + */ + public SessionInfo impersonate(SessionInfo sessionInfo, Credentials credentials) throws LoginException, RepositoryException; + + /** + * Indicates to the RepositoryService, that the given SessionInfo + * will not be used any more. + * + * @param sessionInfo + * @throws javax.jcr.RepositoryException + */ + public void dispose(SessionInfo sessionInfo) throws RepositoryException; + + //-------------------------------------------------------------------------- + /** + * Return all workspace names available for the given SessionInfo. + * + * @param sessionInfo + * @return An array of workspace names. + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Workspace#getAccessibleWorkspaceNames() + * @see javax.jcr.Workspace#getName() + */ + public String[] getWorkspaceNames(SessionInfo sessionInfo) throws RepositoryException; + + //-----------------------------------------------------< Access Control >--- + /** + * Returns true if all actions defined in the specified array are granted + * to given SessionInfo. False otherwise. + * + * @param sessionInfo + * @param itemId + * @param actions + * @return true if the session with the given SessionInfo has + * the specified rights for the given item. + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Session#checkPermission(String, String) + * @see javax.jcr.Session#hasPermission(String, String) + */ + public boolean isGranted(SessionInfo sessionInfo, ItemId itemId, String[] actions) throws RepositoryException; + + /** + * TODO + * + * + * @param sessionInfo + * @return + * @throws RepositoryException + */ + public PrivilegeDefinition[] getPrivilegeDefinitions(SessionInfo sessionInfo) throws RepositoryException; + + /** + * TODO + * + * + * @param sessionInfo + * @param id + * @return + * @throws RepositoryException + */ + public Name[] getPrivilegeNames(SessionInfo sessionInfo, NodeId id) throws RepositoryException; + + /** + * TODO + * + * @param sessionInfo + * @param nodeId + * @return + * @throws RepositoryException + */ + public PrivilegeDefinition[] getSupportedPrivileges(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException; + + //------------------------------------------------------< Reading items >--- + /** + * Returns the QNodeDefinition for the Node + * identified by the given id. This method should only be used if the + * caller is not able to unambiguously determine the applicable definition + * from the parent node type definition or if no parent exists (i.e. for + * the root). + * + * @param sessionInfo + * @param nodeId + * @return The node definition applicable to the Node identified + * by the given id. + * @throws javax.jcr.RepositoryException + */ + public QNodeDefinition getNodeDefinition(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException; + + /** + * Returns the QPropertyDefinition for the Property + * identified by the given id. This method should only be used if the + * caller is not able to unambiguously determine the applicable definition + * from the parent node type definition. + * + * @param sessionInfo + * @param propertyId + * @return The property definition applicable for the Property + * identified by the given id. + * @throws javax.jcr.RepositoryException + */ + public QPropertyDefinition getPropertyDefinition(SessionInfo sessionInfo, PropertyId propertyId) throws RepositoryException; + + /** + * Retrieve the NodeInfo for the node identified by the given + * NodeId. See {@link #getItemInfos(SessionInfo, ItemId)} for + * a similar method that in addition may return ItemInfos of + * children Items. + * + * @param sessionInfo + * @param nodeId + * @return The NodeInfo for the node identified by the given id. + * @throws javax.jcr.ItemNotFoundException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Session#getItem(String) + * @see javax.jcr.Node#getNode(String) + * @see javax.jcr.version.VersionHistory#getAllVersions() + * @see javax.jcr.version.VersionHistory#getVersion(String) + * @see javax.jcr.version.VersionHistory#getVersionByLabel(String) + * @see javax.jcr.version.VersionHistory#getRootVersion() + * @see javax.jcr.Node#getBaseVersion() + * @see javax.jcr.Node#getVersionHistory() + * @see javax.jcr.version.Version#getContainingHistory() + * @deprecated Use {@link #getItemInfos(SessionInfo, ItemId)} + */ + public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws ItemNotFoundException, RepositoryException; + + /** + * Method used to 'batch-read' from the persistent storage. It returns the + * ItemInfo for the given ItemId as the first + * element in the Iterator. In addition the iterator may contain + * arbitrary ItemInfos. + * + * @param sessionInfo + * @param itemId + * @return An Iterator of ItemInfos containing + * at least a single element: the ItemInfo that represents + * the Item identified by the given ItemId. If the Iterator + * contains multiple elements, the first is expected to represent the Item + * identified by the given ItemId. + * @throws javax.jcr.ItemNotFoundException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Session#getItem(String) + * @see javax.jcr.Node#getNode(String) + * @see javax.jcr.version.VersionHistory#getAllVersions() + * @see javax.jcr.version.VersionHistory#getVersion(String) + * @see javax.jcr.version.VersionHistory#getVersionByLabel(String) + * @see javax.jcr.version.VersionHistory#getRootVersion() + * @see javax.jcr.Node#getBaseVersion() + * @see javax.jcr.Node#getVersionHistory() + * @see javax.jcr.version.Version#getContainingHistory() + */ + public Iterator getItemInfos(SessionInfo sessionInfo, ItemId itemId) throws ItemNotFoundException, RepositoryException; + + /** + * Returns an Iterator of ChildInfos present on the + * Node represented by the given parentId. + * + * @param sessionInfo + * @param parentId + * @return An Iterator of ChildInfos present on the + * Node represented by the given parentId. + * @throws ItemNotFoundException + * @throws RepositoryException + */ + public Iterator getChildInfos(SessionInfo sessionInfo, NodeId parentId) throws ItemNotFoundException, RepositoryException; + + /** + * Returns the {@link PropertyId Id}s of the properties that are referencing + * the node identified by the given nodeId. If + * weakReferences is true the ids of + * {@link javax.jcr.PropertyType#WEAKREFERENCE WEAKREFERENCE} properties are + * returned, otherwise the property must be of type {@link javax.jcr.PropertyType#REFERENCE REFERENCE}. + * + * @param sessionInfo + * @param nodeId + * @param propertyName name filter of referring properties to be returned; + * if null then all references are returned. + * @param weakReferences If true the properties must be of type + * {@link javax.jcr.PropertyType#WEAKREFERENCE}, otherwise of type + * {@link javax.jcr.PropertyType#REFERENCE}. + * @return An Iterator of {@link PropertyId Id}s of the properties that are + * referencing the node identified by the given nodeId or an + * empty iterator if the node is not referenceable or no references exist. + * @throws ItemNotFoundException + * @throws RepositoryException + * @see PropertyInfo#getId() + * @since JCR 2.0 + */ + public Iterator getReferences(SessionInfo sessionInfo, NodeId nodeId, Name propertyName, boolean weakReferences) throws ItemNotFoundException, RepositoryException; + + /** + * Returns the PropertyInfo for the Property + * identified by the given id. + * + * @param sessionInfo + * @param propertyId + * @return The PropertyInfo for the Property + * identified by the given id. + * @throws javax.jcr.ItemNotFoundException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Session#getItem(String) + * @see javax.jcr.Node#getProperty(String) + * @deprecated Use {@link #getItemInfos(SessionInfo, ItemId)} + */ + public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId) throws ItemNotFoundException, RepositoryException; + + //-----------------------------------------------< general modification >--- + /** + * Indicates the start of a set of operations that cause modifications + * on the underlying persistence layer. All modification called on the + * {@link Batch} must be executed at once or non must be executed upon + * calling {@link #submit(Batch)}. + * + * @param sessionInfo + * @param itemId Id of the Item that is a common ancestor of all + * Items affected upon batch execution. This Item + * might itself be modified within the scope of the Batch. + * @return A Batch indicating the start of a set of transient modifications + * that will be execute at once upon {@link #submit(Batch)}. + * @throws RepositoryException + * @see javax.jcr.Item#save() + * @see javax.jcr.Session#save() + * @see Batch + */ + public Batch createBatch(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException; + + /** + * Completes the given {@link Batch} or discard all the previous modifications. + * See {@link #createBatch(SessionInfo,ItemId)} for additional information + * regarding batch creation. + * + * @param batch + * @throws PathNotFoundException + * @throws ItemNotFoundException + * @throws NoSuchNodeTypeException + * @throws ValueFormatException + * @throws VersionException + * @throws LockException + * @throws ConstraintViolationException + * @throws AccessDeniedException + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + * @see Batch + */ + public void submit(Batch batch) throws PathNotFoundException, ItemNotFoundException, NoSuchNodeTypeException, ValueFormatException, VersionException, LockException, ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Creates a new {@code Tree} that can be populated and later on be applied + * to the specified {@code Batch} by calling {@code #setTree}. + * + * @param nodeName + * @param primaryTypeName + * @param uniqueId + * @return a new {@code Tree} instance. + * @throws RepositoryException + */ + public Tree createTree(SessionInfo sessionInfo, Batch batch, Name nodeName, Name primaryTypeName, String uniqueId) throws RepositoryException; + + //-------------------------------------------------------------< Import >--- + /** + * Imports the data present in the given InputStream into the + * persistent layer. Note, that the implementation is responsible for + * validating the data presented and for the integrity of the repository + * upon completion. + * + * @param sessionInfo + * @param parentId + * @param xmlStream + * @param uuidBehaviour + * @throws ItemExistsException + * @throws PathNotFoundException + * @throws VersionException + * @throws ConstraintViolationException + * @throws LockException + * @throws AccessDeniedException + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + * @see javax.jcr.Workspace#importXML(String, java.io.InputStream, int) + */ + public void importXml(SessionInfo sessionInfo, NodeId parentId, InputStream xmlStream, int uuidBehaviour) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException; + + //---------------------------------------------------------< Copy, Move >--- + /** + * Moves the node identified by the given srcNodeId (and its + * entire subtree) to the new location defined by destParentNodeId + * and a new name (destName). + * + * @param sessionInfo + * @param srcNodeId + * @param destParentNodeId + * @param destName + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.PathNotFoundException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Workspace#move(String, String) + */ + public void move(SessionInfo sessionInfo, NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Clone the subtree identified by the given srcNodeId + * in workspace named srcWorkspaceName to the destination + * in the workspace specified by the given SessionInfo. The + * destination is composed by the given parent id and the new name + * as indicated by destName. + *

        + * Note, that srcWorkspaceName may be the same as the one + * specified within the SessionInfo. In this case the copy + * corresponds to a copy within a single workspace. + * + * @param sessionInfo + * @param srcWorkspaceName + * @param srcNodeId + * @param destParentNodeId + * @param destName + * @throws javax.jcr.NoSuchWorkspaceException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.PathNotFoundException + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Workspace#copy(String, String) + * @see javax.jcr.Workspace#copy(String, String, String) + */ + public void copy(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException; + + //------------------------------------------------------< Update, Clone >--- + /** + * Updates the node identified by the given NodeId replacing + * it (an the complete subtree) with a clone of its corresponding node + * present in the workspace with the given srcWorkspaceName. + * + * @param sessionInfo + * @param nodeId + * @param srcWorkspaceName + * @throws javax.jcr.NoSuchWorkspaceException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.InvalidItemStateException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#update(String) + */ + public void update(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName) throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException; + + /** + * Clone the subtree identified by the given srcNodeId + * in workspace named srcWorkspaceName to the destination + * in the workspace specified by the given SessionInfo. The + * destination is composed by the given parent id and the new name + * as indicated by destName. + * + * @param sessionInfo + * @param srcWorkspaceName + * @param srcNodeId + * @param destParentNodeId + * @param destName + * @param removeExisting + * @throws javax.jcr.NoSuchWorkspaceException + * @throws javax.jcr.nodetype.ConstraintViolationException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.PathNotFoundException + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Workspace#clone(String, String, String, boolean) + */ + public void clone(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException; + + //------------------------------------------------------------< Locking >--- + /** + * Returns the lock information that applies to Node identified + * by the given NodeId or null. If the implementation + * does not support locking at all, this method always returns null. + * + * @param sessionInfo + * @param nodeId + * @return The lock information for the Node identified by the given + * nodeId or null if no lock applies to that Node. + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.RepositoryException If some other error occurs. + * @see Node#getLock() + */ + public LockInfo getLockInfo(SessionInfo sessionInfo, NodeId nodeId) throws AccessDeniedException, RepositoryException; + + /** + * Create a lock on the Node identified by the given id. + * + * @param sessionInfo + * @param nodeId + * @param deep + * @param sessionScoped + * @return The LockInfo associated with the new lock + * that has been created. + * @throws javax.jcr.UnsupportedRepositoryOperationException If this SPI + * implementation does not support locking at all. + * @throws javax.jcr.lock.LockException If the Node identified by the given + * id cannot be locked due to an existing lock or due to missing mixin type. + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.RepositoryException If another error occurs. + * @see javax.jcr.Node#lock(boolean, boolean) + */ + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, boolean sessionScoped) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException; + + /** + * Create a lock on the Node identified by the given id. + * + * @param sessionInfo + * @param nodeId + * @param deep + * @param sessionScoped + * @param timeoutHint long indicating the desired lock timeout in seconds. + * The implementation is free to ignore the hint. + * @param ownerHint String indicating the desired lockOwner info. The + * implementation is free to ignore the hint. + * @return The LockInfo associated with the new lock + * that has been created. + * @throws javax.jcr.UnsupportedRepositoryOperationException If this SPI + * implementation does not support locking at all. + * @throws javax.jcr.lock.LockException If the Node identified by the given + * id cannot be locked due to an existing lock or due to missing mixin type. + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.RepositoryException If another error occurs. + * @see javax.jcr.lock.LockManager#lock(String, boolean, boolean, long, String) + * @since JCR 2.0 + */ + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, boolean sessionScoped, long timeoutHint, String ownerHint) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException; + + /** + * Explicit refresh of an existing lock. Existing locks should be refreshed + * implicitly with all read and write methods listed here. + * + * @param sessionInfo + * @param nodeId + * @throws javax.jcr.UnsupportedRepositoryOperationException If this SPI + * implementation does not support locking at all. + * @throws javax.jcr.lock.LockException If the Node identified by the given + * id is not locked (any more) or if the SessionInfo does not + * contain the token associated with the lock to be refreshed. + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.RepositoryException If another error occurs. + * @see javax.jcr.lock.Lock + */ + public void refreshLock(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException; + + /** + * Releases the lock on the Node identified by the given + * NodeId. + *

        + * Please note, that on {@link javax.jcr.Session#logout() logout} all + * session-scoped locks must be released by calling unlock. + * + * @param sessionInfo + * @param nodeId + * @throws javax.jcr.UnsupportedRepositoryOperationException If this SPI + * implementation does not support locking at all. + * @throws javax.jcr.lock.LockException If the Node identified by the given + * id is not locked or if the SessionInfo does not contain the + * token associated with the lock to be released. + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.RepositoryException If another error occurs. + * @see javax.jcr.Node#unlock() + */ + public void unlock(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException; + + //---------------------------------------------------------< Versioning >--- + /** + * Performs a checkin for the Node identified by the given + * NodeId. + * + * @param sessionInfo + * @param nodeId + * @return NodeId of newly created version + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.InvalidItemStateException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#checkin() + */ + public NodeId checkin(SessionInfo sessionInfo, NodeId nodeId) throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException; + + /** + * Performs a checkout for the Node identified by the given + * NodeId. Same as {@link #checkout(SessionInfo, NodeId, NodeId)} + * where the activityId is null. + * + * @param sessionInfo + * @param nodeId + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#checkout() + */ + public void checkout(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, LockException, RepositoryException; + + /** + * Performs a checkout for the Node identified by the given + * NodeId and for activity identified by the specified + * activityId. If the activityId is null + * this corresponds to {@link #checkout(SessionInfo, NodeId)} + * + * @param sessionInfo + * @param nodeId + * @param activityId Id of the activity node set to the editing session or + * null if no activity is in effect. + * @throws UnsupportedRepositoryOperationException + * @throws LockException + * @throws RepositoryException + * @since JCR 2.0 + */ + public void checkout(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) throws UnsupportedRepositoryOperationException, LockException, RepositoryException; + + /** + * Performs a checkpoint for the Node identified by the given + * NodeId. + * + * @param sessionInfo + * @param nodeId + * @return NodeId of newly created version + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.version.VersionManager#checkpoint(String) + * @since JCR 2.0 + */ + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Performs a checkpoint for the Node identified by the given + * NodeId. For the checkout part the specified activityId + * is taken into account as specified in {@link #checkout(SessionInfo, NodeId, NodeId)}. + * + * @param sessionInfo + * @param nodeId + * @param activityId Id of the activity node set to the editing session or + * null if no activity is in effect. + * @throws UnsupportedRepositoryOperationException + * @throws LockException + * @throws RepositoryException + * @since JCR 2.0 + */ + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException; + /** + * Remove the version identified by the specified versionId. + * + * @param sessionInfo + * @param versionHistoryId NodeId identifying the version + * history the version identified by versionId belongs to. + * @param versionId + * @throws ReferentialIntegrityException + * @throws AccessDeniedException + * @throws UnsupportedRepositoryOperationException + * @throws VersionException + * @throws RepositoryException + * @see javax.jcr.version.VersionHistory#removeVersion(String) + */ + public void removeVersion(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId) throws ReferentialIntegrityException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException; + + /** + * Restores the node identified by nodeId to the state defined + * by the version with the specified versionId. + * + * @param sessionInfo + * @param nodeId + * @param versionId + * @param removeExisting boolean flag indicating how to deal with an + * identifier collision that may occur if a node exists outside the subtree + * to be restored with the same identified as a node that would be + * introduces by the restore. If the removeExisting is + * true the restored node takes precedence and the + * existing node is removed. Otherwise the restore fails. + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.PathNotFoundException + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.InvalidItemStateException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#restore(String, boolean) + * @see javax.jcr.Node#restore(javax.jcr.version.Version, boolean) + * @see javax.jcr.Node#restore(javax.jcr.version.Version, String, boolean) + * @see javax.jcr.Node#restoreByLabel(String, boolean) + */ + public void restore(SessionInfo sessionInfo, NodeId nodeId, NodeId versionId, boolean removeExisting) throws VersionException, PathNotFoundException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException; + + /** + * Restore multiple versions at once. The versions to be restored are + * identified by the given array of NodeIds. + * + * @param sessionInfo + * @param versionIds + * @param removeExisting boolean flag indicating how to deal with an + * identifier collision that may occur if a node exists outside the subtrees + * to be restored with the same identified as any node that would be + * introduces by the restore. If the removeExisting is + * true the node to be restored takes precedence and the + * existing node is removed. Otherwise the restore fails. + * @throws javax.jcr.ItemExistsException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.InvalidItemStateException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Workspace#restore(javax.jcr.version.Version[], boolean) + */ + public void restore(SessionInfo sessionInfo, NodeId[] versionIds, boolean removeExisting) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException; + + /** + * Merge the node identified by the given NodeId and its subtree + * with the corresponding node present in the workspace with the name of + * srcWorkspaceName. + * + * @param sessionInfo + * @param nodeId + * @param srcWorkspaceName + * @param bestEffort + * @return an Iterator over the {@link NodeId}s of all nodes that + * received a merge result of "fail" in the course of this operation. + * @throws javax.jcr.NoSuchWorkspaceException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.MergeException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.InvalidItemStateException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#merge(String, boolean) + */ + public Iterator merge(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName, boolean bestEffort) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException; + + /** + * Merge the node identified by the given NodeId and its subtree + * with the corresponding node present in the workspace with the name of + * srcWorkspaceName. + * + * @param sessionInfo + * @param nodeId + * @param srcWorkspaceName + * @param bestEffort + * @return an Iterator over the {@link NodeId}s of all nodes that + * received a merge result of "fail" in the course of this operation. + * @throws javax.jcr.NoSuchWorkspaceException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.MergeException + * @throws javax.jcr.lock.LockException + * @throws javax.jcr.InvalidItemStateException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.version.VersionManager#merge(String, String, boolean, boolean) + * @since JCR 2.0 + */ + public Iterator merge(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName, boolean bestEffort, boolean isShallow) throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException; + + /** + * Resolve an existing merge conflict present with the node identified by + * the given NodeId. + * + * @param sessionInfo + * @param nodeId + * @param mergeFailedIds The NodeIds remaining in the jcr:mergeFailed + * REFERENCE property. The version id(s) to be resolved were removed from the + * array and added to the predecessor ids in case of {@link Node#doneMerge(Version)}. + * In case of a {@link Node#cancelMerge(Version)} the version id only gets + * removed from the list. + * @param predecessorIds The complete set of predecessor id including those + * that have been added in order to resolve a merge conflict. + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.InvalidItemStateException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Node#cancelMerge(javax.jcr.version.Version) + * @see javax.jcr.Node#doneMerge(javax.jcr.version.Version) + */ + public void resolveMergeConflict(SessionInfo sessionInfo, NodeId nodeId, NodeId[] mergeFailedIds, NodeId[] predecessorIds) throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Add the given version label in the persistent layer. + * + * @param sessionInfo + * @param versionHistoryId NodeId identifying the version + * history the version identified by versionId belongs to. + * @param versionId NodeId identifying the version the + * label belongs to. + * @param label The label to be added. + * @param moveLabel If the label is already assigned to a version within + * the same version history this parameter has the following effect: If true + * the label already present gets moved to be now be a label of the version + * indicated by versionId. If false this method + * fails and the label remains with the original version. + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean) + */ + public void addVersionLabel(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId, Name label, boolean moveLabel) throws VersionException, RepositoryException; + + /** + * Remove the given version label in the persistent layer. + * + * @param sessionInfo + * @param versionHistoryId NodeId identifying the version + * history the version identified by versionId belongs to. + * @param versionId NodeId identifying the version the + * label belongs to. + * @param label The label to be removed. + * @throws javax.jcr.version.VersionException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.version.VersionHistory#removeVersionLabel(String) + */ + public void removeVersionLabel(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId, Name label) throws VersionException, RepositoryException; + + /** + * Create a new activity. + * + * @param sessionInfo + * @param title + * @return the NodeId of the new activity node. + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.version.VersionManager#createActivity(String) + * @since JCR 2.0 + */ + public NodeId createActivity(SessionInfo sessionInfo, String title) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Removes the activity identified by the specified activityId. + * + * @param sessionInfo + * @param activityId + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.version.VersionManager#removeActivity(Node) + * @since JCR 2.0 + */ + public void removeActivity(SessionInfo sessionInfo, NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Merges the activity identified by the given activityId into + * the workspace the specified sessionInfo has been created for. + * + * @param sessionInfo + * @param activityId + * @return an Iterator over the {@link NodeId}s of all nodes that + * received a merge result of "fail" in the course of this operation. + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + */ + public Iterator mergeActivity(SessionInfo sessionInfo, NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * + * @param sessionInfo + * @param nodeId + * @return + * @throws UnsupportedRepositoryOperationException + * @throws RepositoryException + * @see javax.jcr.version.VersionManager#createConfiguration(String) + */ + public NodeId createConfiguration(SessionInfo sessionInfo, NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException; + + //----------------------------------------------------------< Searching >--- + /** + * Returns a String array identifying all query languages supported by this + * SPI implementation. + * + * @param sessionInfo + * @return String array identifying all query languages supported by this + * SPI implementation. + * @throws javax.jcr.RepositoryException + * @see javax.jcr.query.QueryManager#getSupportedQueryLanguages() + */ + public String[] getSupportedQueryLanguages(SessionInfo sessionInfo) throws RepositoryException; + + /** + * Checks if the query statement is valid according to the + * specified query language and returns the bind variable + * names found in the query statement. + * + * @param sessionInfo the session info. + * @param statement the query statement to check. + * @param language the query language. + * @param namespaces the locally re-mapped namespace which may be used in + * the query statement. + * @return the bind variable names. + * @throws InvalidQueryException if the query statement is invalid or the + * language is not supported. + * @throws RepositoryException if an error occurs while checking the + * statement. + * @see javax.jcr.query.QueryManager#createQuery(String, String) + */ + public String[] checkQueryStatement(SessionInfo sessionInfo, String statement, String language, Map namespaces) throws InvalidQueryException, RepositoryException; + + /** + * Execute the given query statement with the specified query language. The + * additional namespaces parameter provides a mapping of prefix + * to namespace uri in order to be able to properly resolve prefix:localname + * patterns present within the query statement. + * + * @param sessionInfo the session info that wants to execute the query. + * @param statement the query statement to be execute. + * @param language the query language used to parse the query + * statement. + * @param namespaces the locally re-mapped namespace which may be used in + * the query statement. + * @param limit The maximum result size or -1 is no + * maximum is set. + * @param offset The offset in the total result set or -1 + * is no offset is set. + * @param values A Map of name/value pairs collected upon calls to + * {@link javax.jcr.query.Query#bindValue(String, + * javax.jcr.Value)}. + * @return The query info. + * @throws RepositoryException if an error occurs. + * @see javax.jcr.query.Query#execute() + */ + public QueryInfo executeQuery(SessionInfo sessionInfo, + String statement, + String language, + Map namespaces, + long limit, + long offset, + Map values) + throws RepositoryException; + + //--------------------------------------------------------< Observation >--- + /** + * Creates an event filter. If the repository supports observation, the + * filter created is based on the parameters available in {@link + * javax.jcr.observation.ObservationManager#addEventListener}. + *

        + * Note, that an SPI implementation may support observation even if + * the corresponding {@link javax.jcr.Repository#OPTION_OBSERVATION_SUPPORTED repository descriptor} + * does not return 'true'. + * + * @param sessionInfo the session info which requests an event filter. + * @param eventTypes A combination of one or more event type constants + * encoded as a bitmask. + * @param absPath An absolute path. + * @param isDeep A boolean. + * @param uuid Array of jcr:uuid properties. + * @param nodeTypeName Array of node type names. + * @param noLocal A boolean. + * @return the event filter instance with the given parameters. + * @throws UnsupportedRepositoryOperationException if this SPI implementation + * does not allow to create event filters. + * @throws RepositoryException if an error occurs while creating the + * EventFilter. + * @see javax.jcr.observation.ObservationManager#addEventListener(javax.jcr.observation.EventListener, int, String, boolean, String[], String[], boolean) + */ + public EventFilter createEventFilter(SessionInfo sessionInfo, int eventTypes, + Path absPath, boolean isDeep, + String[] uuid, Name[] nodeTypeName, + boolean noLocal) + throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Creates a new {@link Subscription} for events with an initial set of + * {@link EventFilter}s. The returned subscription must provide events from + * the time when the subscription was created. If an empty array of filters + * is passed no events will be available through the created subscription + * unless the filters are later updated by calling + * {@link RepositoryService#updateEventFilters(Subscription, EventFilter[])}. + * + * @param sessionInfo the session info. + * @param filters the initial event filters for the subscription. + * @return + * @throws UnsupportedRepositoryOperationException + * if this SPI implementation does not support + * observation. + * @throws RepositoryException if an error occurs while creating the + * Subscription. + */ + public Subscription createSubscription(SessionInfo sessionInfo, EventFilter[] filters) + throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Updates events filters on the subscription. When this method returns all + * events that go through the passed subscription and have been generated + * after this method call must be filtered using the passed + * filters. + *

        + * An implementation is required to accept at least event filter instances + * created by {@link RepositoryService#createEventFilter}. Optionally an + * implementation may also support event filters instanciated by the client + * itself. An implementation may require special deployment in that case, + * e.g. to make the event filter implementation class available to the + * repository server. + *

        + * Note on thread-safety: it is permissible to call this methods + * while another thread is blocked in calling {@link + * RepositoryService#getEvents(Subscription, long)} using the same + * subscription instance as a parameter. + * + * @param subscription the subscription where the event filters are + * applied. + * @param filters the filters that are applied to the events as they + * occurred on the repository. An event is included in an + * event bundle if it is {@link EventFilter#accept(Event, + * boolean) accept}ed by at least one of the supplied + * filters. If an empty array is passed none of the potential + * events are include in an event bundle. This allows a + * client to skip or ignore events for a certain period of + * time. + * @throws RepositoryException if an error occurs while updating the event + * filters. + */ + public void updateEventFilters(Subscription subscription, EventFilter[] filters) + throws RepositoryException; + + /** + * Retrieves the events that occurred since the last call to this method for + * the passed subscription. + *

        + * Note, that an SPI implementation may support observation even if the + * corresponding {@link javax.jcr.Repository#OPTION_OBSERVATION_SUPPORTED + * repository descriptor} does return 'false'. + *

        + * An implementation should un-block a calling thread and let it return if + * the associated subscription is disposed by another thread. + * + * @param subscription a subscription. + * @param timeout a timeout in milliseconds to wait at most for an + * event bundle. If timeout is up and no + * event occurred meanwhile an empty array is returned. + * @return an array of EventBundles representing the events + * that occurred. + * @throws RepositoryException if an error occurs while retrieving the + * event bundles. + * @throws InterruptedException if the calling thread is interrupted while + * waiting for events within the specified + * timeout. + */ + public EventBundle[] getEvents(Subscription subscription, long timeout) + throws RepositoryException, InterruptedException; + + /** + * Returns events from the EventJournal after a given point in + * time. The returned event bundle may only contain events up to a given + * time. In order to retrieve more events a client must call this method + * again with the timestamp from the last event bundle. An empty bundle + * indicates that there are no more events. + * + * @param sessionInfo the session info. + * @param filter the event filter to apply. Please note: the + * noLocal flag is ignored. + * @param after retrieve events that occurred after the given + * timestamp. + * @return the event bundle. + * @throws RepositoryException if an error occurs. + * @throws UnsupportedRepositoryOperationException + * if the underlying implementation does not + * support event journaling. + */ + public EventBundle getEvents(SessionInfo sessionInfo, EventFilter filter, long after) + throws RepositoryException, UnsupportedRepositoryOperationException; + + /** + * Indicates that the passed subscription is no longer needed. + *

        + * Note on thread-safety: it is permissible to call this methods + * while another thread is blocked in calling {@link + * RepositoryService#getEvents(Subscription, long)} using the same + * subscription instance as a parameter. + * + * @throws RepositoryException if an error occurs while the subscription is + * disposed. + */ + public void dispose(Subscription subscription) throws RepositoryException; + + //---------------------------------------------------------< Namespaces >--- + /** + * Retrieve all registered namespaces. The namespace to prefix mapping is + * done using the prefix as key and the namespace as value in the Map. + * + * @param sessionInfo + * @return + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Workspace#getNamespaceRegistry() + * @see javax.jcr.NamespaceRegistry#getPrefixes() + * @see javax.jcr.NamespaceRegistry#getURIs() + */ + public Map getRegisteredNamespaces(SessionInfo sessionInfo) throws RepositoryException; + + /** + * Returns the namespace URI for the given namespace prefix. + * + * @param sessionInfo the session info. + * @param prefix a namespace prefix to resolve. + * @return the namespace URI for the given namespace prefix. + * @throws NamespaceException if prefix is not mapped to a namespace URI. + * @throws RepositoryException if another error occurs. + * @see javax.jcr.NamespaceRegistry#getURI(String) + */ + public String getNamespaceURI(SessionInfo sessionInfo, String prefix) + throws NamespaceException, RepositoryException; + + /** + * Returns the namespace prefix for the given namespace uri. + * + * @param sessionInfo the session info. + * @param uri the namespace URI. + * @return the namespace prefix. + * @throws NamespaceException if the URI unknown. + * @throws RepositoryException if another error occurs. + * @see javax.jcr.NamespaceRegistry#getPrefix(String) + */ + public String getNamespacePrefix(SessionInfo sessionInfo, String uri) + throws NamespaceException, RepositoryException; + + /** + * Register a new namespace with the given prefix and uri. + * + * @param sessionInfo + * @param prefix Prefix of the namespace to be registered. + * @param uri Namespace URI to be registered. + * @throws javax.jcr.NamespaceException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.NamespaceRegistry#registerNamespace(String, String) + */ + public void registerNamespace(SessionInfo sessionInfo, String prefix, String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException; + + /** + * Unregister the namespace identified by the given uri + * + * @param sessionInfo + * @param uri Namespace URI to be unregistered. + * @throws javax.jcr.NamespaceException + * @throws javax.jcr.UnsupportedRepositoryOperationException + * @throws javax.jcr.AccessDeniedException + * @throws javax.jcr.RepositoryException + * @see javax.jcr.NamespaceRegistry#unregisterNamespace(String) + */ + public void unregisterNamespace(SessionInfo sessionInfo, String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException; + + //----------------------------------------------------------< NodeTypes >--- + /** + * Retrieve the QNodeTypeDefinitions of all registered nodetypes. + * + * @param sessionInfo + * @return Iterator of {@link QNodeTypeDefinition}s. + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Workspace#getNodeTypeManager() + * @see javax.jcr.nodetype.NodeTypeManager#getAllNodeTypes() + * @see javax.jcr.nodetype.NodeTypeManager#getMixinNodeTypes() + * @see javax.jcr.nodetype.NodeTypeManager#getPrimaryNodeTypes() + * @see javax.jcr.nodetype.NodeTypeManager#getNodeType(String) + */ + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo) throws RepositoryException; + + /** + * Retrieve QNodeTypeDefinitions for the given names. The + * implementation is free to return additional definitions which will (probably) + * be needed by the caller due to node type inheritance. The caller must be + * able to deal with any kind of additional QNodeTypeDefinitions + * present in the Iterator irrespective whether they have been + * loaded before or not. + * + * @param sessionInfo + * @param nodetypeNames names of node types to retrieve + * @return {@link QNodeTypeDefinition} + * @throws javax.jcr.nodetype.NoSuchNodeTypeException if for any of the given + * names no QNodeTypeDefinition exists. + * @throws javax.jcr.RepositoryException + * @see javax.jcr.Workspace#getNodeTypeManager() + * @see javax.jcr.nodetype.NodeTypeManager#getAllNodeTypes() + * @see javax.jcr.nodetype.NodeTypeManager#getMixinNodeTypes() + * @see javax.jcr.nodetype.NodeTypeManager#getPrimaryNodeTypes() + * @see javax.jcr.nodetype.NodeTypeManager#getNodeType(String) + */ + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo, Name[] nodetypeNames) throws RepositoryException; + + /** + * Registers the node types with the specified QNodeTypeDefinitions. + * If allowUpdate is true this method may also be + * used to reregister existing node types with a modified definition, otherwise + * this method will fail with NodeTypeExistsException if any of + * the specified definition has the name of an already registered node type. + * + * @param sessionInfo + * @param nodeTypeDefinitions + * @param allowUpdate + * @throws InvalidNodeTypeDefinitionException If any of the specified definitions + * is invalid. + * @throws NodeTypeExistsException If any of the specified definitions has the + * name of an already registered node type and allowUpdate is false. + * @throws UnsupportedRepositoryOperationException If registering node types + * is not supported. + * @throws RepositoryException If another error occurs. + * @see javax.jcr.nodetype.NodeTypeManager#registerNodeTypes(javax.jcr.nodetype.NodeTypeDefinition[], boolean) + */ + public void registerNodeTypes(SessionInfo sessionInfo, QNodeTypeDefinition[] nodeTypeDefinitions, boolean allowUpdate) throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Unregisters the node types with the specified names. + * + * @param sessionInfo + * @param nodeTypeNames + * @throws UnsupportedRepositoryOperationException If unregistering node types + * is not supported. + * @throws NoSuchNodeTypeException If any of the specified names has no + * corresponding registered node type. + * @throws RepositoryException If another error occurs. + * @see javax.jcr.nodetype.NodeTypeManager#unregisterNodeTypes(String[]) + */ + public void unregisterNodeTypes(SessionInfo sessionInfo, Name[] nodeTypeNames) throws UnsupportedRepositoryOperationException, NoSuchNodeTypeException, RepositoryException; + + //-----------------------------------------------< Workspace Management >--- + /** + * Create a new workspace with the specified name. If + * srcWorkspaceName isn't null the content of + * that workspace is 'cloned' to the new workspace as inital content, + * otherwise an empty workspace will be created. + * + * @param sessionInfo + * @param name The name of the new workspace. + * @param srcWorkspaceName The name of the workspace from which the initial + * content of the new workspace will be 'cloned'. + * @throws AccessDeniedException + * @throws UnsupportedRepositoryOperationException + * @throws NoSuchWorkspaceException + * @throws RepositoryException + * @see javax.jcr.Workspace#createWorkspace(String) + * @see javax.jcr.Workspace#createWorkspace(String, String) + * @since JCR 2.0 + */ + public void createWorkspace(SessionInfo sessionInfo, String name, String srcWorkspaceName) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException; + + /** + * Deletes the workspace with the specified name. + * + * @param sessionInfo + * @param name The name of the workspace to be deleted. + * @throws AccessDeniedException + * @throws UnsupportedRepositoryOperationException + * @throws NoSuchWorkspaceException + * @throws RepositoryException + * @see javax.jcr.Workspace#deleteWorkspace(String) + * @since JCR 2.0 + */ + public void deleteWorkspace(SessionInfo sessionInfo, String name) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException; + +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/RepositoryServiceFactory.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/RepositoryServiceFactory.java new file mode 100644 index 00000000000..917d8a8aef3 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/RepositoryServiceFactory.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.spi; + +import java.util.Map; + +import javax.jcr.RepositoryException; + +/** + * Factory for creating {@link RepositoryService} instances. Implementations must + * provide a no argument constructor. + */ +public interface RepositoryServiceFactory { + + /** + * Create a new {@link RepositoryService}. If the factory does not understand the + * passed parameters it must return null. + * @param parameters implementation specific set of parameters + * @return a fresh RepositoryService instance or null. + * @throws RepositoryException If there was an error creating the + * RepositoryService instance + */ + public RepositoryService createRepositoryService(Map parameters) throws RepositoryException; +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/SessionInfo.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/SessionInfo.java new file mode 100644 index 00000000000..0c638332abb --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/SessionInfo.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.spi; + +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; + +/** + * SessionInfo is created upon + * {@link RepositoryService#obtain(javax.jcr.Credentials, String)} or + * {@link RepositoryService#obtain(SessionInfo, String)} and will be used for + * any call on the RepositoryService that requires user and workspace + * identification. + *

        + * In addition the SessionInfo acts as primary container for + * lock tokens. They will assert that a given SessionInfo is able to execute + * operations on the RepositoryService that are affected by existing locks. + */ +public interface SessionInfo { + + /** + * Returns the user id. + * + * @return The user identification. + * @see RepositoryService#obtain(javax.jcr.Credentials, String) + */ + public String getUserID(); + + /** + * Returns the workspace name. + * + * @return The name of the {@link javax.jcr.Workspace workspace} this + * SessionInfo has been built for. + * @see RepositoryService#obtain(javax.jcr.Credentials, String) + * @see javax.jcr.Workspace#getName() + */ + public String getWorkspaceName(); + + /** + * Returns the lock tokens present on this SessionInfo. + * + * @return lock tokens present on this SessionInfo. + * @throws UnsupportedRepositoryOperationException If locking is not supported. + * @throws RepositoryException If another error occurs. + */ + public String[] getLockTokens() throws UnsupportedRepositoryOperationException, RepositoryException; + + /** + * Add the given lock token to this SessionInfo. The token will + * enable the SessionInfo to operate on Items that are affected by the + * lock identified by the given token. + * + * @param lockToken to be added. + * @throws UnsupportedRepositoryOperationException If locking is not supported. + * @throws LockException If the token cannot be added. + * @throws RepositoryException If another error occurs. + */ + public void addLockToken(String lockToken) throws UnsupportedRepositoryOperationException, LockException, RepositoryException; + + /** + * Removes the given lock token from this SessionInfo. + * This must happen if the associated Session successfully removes the Lock + * from a Node or if the token is removed from the Session itself by calling + * {@link javax.jcr.Session#removeLockToken(String)}. Consequently all + * RepositoryService operations affected by a lock will fail + * with LockException provided the lock hasn't been released. + * + * @param lockToken to be removed. + * @throws UnsupportedRepositoryOperationException If locking is not supported. + * @throws LockException If the token cannot be removed. + * @throws RepositoryException If another error occurs. + */ + public void removeLockToken(String lockToken) throws UnsupportedRepositoryOperationException, LockException, RepositoryException; + + /** + * Sets the user data used for {@link org.apache.jackrabbit.spi.Event#getUserData()}. + * + * @param userData + * @throws RepositoryException + * @see javax.jcr.observation.ObservationManager#setUserData(String) + */ + public void setUserData(String userData) throws RepositoryException; +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Subscription.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Subscription.java new file mode 100644 index 00000000000..62b0549e384 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Subscription.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.spi; + +/** + * Subscription defines a marker interface for an event + * subscription. An implementation will likely keep information in this object + * about the last consumed events and other implementation specific data. A + * client will usually first create an event filter and then a subscription + * based on the filter. Events can then be retrieved by calling {@link + * RepositoryService#getEvents(Subscription, long)}. If a subscription is no + * longer needed a client should call {@link RepositoryService#dispose(Subscription)}. + */ +public interface Subscription { +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Tree.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Tree.java new file mode 100644 index 00000000000..6ce3a481f94 --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/Tree.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.spi; + +import javax.jcr.RepositoryException; + +/** + * Interface for building a hierarchy of JCR items on + * the SPI layer. + */ +public interface Tree { + + public Name getName(); + + public Name getPrimaryTypeName(); + + public String getUniqueId(); + + public void addProperty(NodeId parentId, Name propertyName, int propertyType, QValue value) throws RepositoryException; + + public void addProperty(NodeId parentId, Name propertyName, int propertyType, QValue[] values) throws RepositoryException; + + public Tree addChild(Name childName, Name primaryTypeName, String uniqueId); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/XASessionInfo.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/XASessionInfo.java new file mode 100644 index 00000000000..22d817ab51c --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/XASessionInfo.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.spi; + +import javax.transaction.xa.XAResource; + +/** + * XASessionInfo extends the SessionInfo and provides + * access to the XAResource of the session info. + */ +public interface XASessionInfo extends SessionInfo { + + /** + * Retrieves an XAResource object that the transaction manager + * will use to manage this XASessionInfo object's participation + * in a distributed transaction. + * + * @return the XAResource object. + */ + public XAResource getXAResource(); +} diff --git a/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/package-info.java b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/package-info.java new file mode 100644 index 00000000000..f07096b7bbf --- /dev/null +++ b/jackrabbit-spi/src/main/java/org/apache/jackrabbit/spi/package-info.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. + */ + +/** + * Defines the interfaces of the JCR SPI (Service Provider Interface). + * + *

        + * The SPI cuts the JCR stack into two parts: + *

          + *
        • Above the SPI an implementation that wishes to expose the JCR API again + * needs to implement the transient item space, the session local namespace + * mapping and various conversions from the value representation in the SPI to + * the resolved values in the JCR API.
        • + *
        • An implementation of the SPI interfaces has to deal with the persistent + * view of a JCR repository. This includes almost all aspects of the JSR 170 + * specification, except the previously stated transient space and the session + * local namespace resolution to prefixes.
        • + *
        + * + *

        Observation

        + * Because one of the goals of this SPI is to make it easier to implement a + * remoting layer using various existing protocols, the observation mechanism + * has been design with this goal in mind. Instead of a listener registration + * with a callback for each event bundle, the SPI uses a polling mechanism + * with a timeout: {@link org.apache.jackrabbit.spi.RepositoryService#getEvents + * RepositoryService.getEvents()}. With every call to this method the + * repository is advised to return the events that occurred since the last + * call. As a reference to the last retrieved + * {@link org.apache.jackrabbit.spi.EventBundle} the + * {@link org.apache.jackrabbit.spi.SessionInfo} contains a bundle identifier + * which is automatically updated on each call to + * RepositoryService.getEvents(). While this design allows for + * a polling implementation on top of the SPI it is also well suited for a + * listener based observation implementation on top of the SPI. With only + * little thread synchronization overhead events can be acquired using a + * timeout of {@link java.lang.Long#MAX_VALUE}. + *

        + * If an SPI implementation does not support observation, the method + * RepositoryService.getEvents() will always throw an + * {@link javax.jcr.UnsupportedRepositoryOperationException}. + */ +@org.osgi.annotation.versioning.Version("3.0.0") +package org.apache.jackrabbit.spi; diff --git a/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/AbstractSPITest.java b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/AbstractSPITest.java new file mode 100644 index 00000000000..34500b4a9ff --- /dev/null +++ b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/AbstractSPITest.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.spi; + +import junit.framework.TestCase; + +import javax.jcr.RepositoryException; + +/** AbstractSPITest... */ +public class AbstractSPITest extends TestCase { + + /** + * Helper object to access repository service transparently + */ + public static Helper helper = new Helper(); + + /** + * SessionInfo with superuser permission + */ + protected SessionInfo sessionInfo; + + /** + * Returns the value of the configuration property with propName. + * The sequence how configuration properties are read is the following: + *

          + *
        1. org.apache.jackrabbit.spi.<testClassName>.<testCaseName>.<propName>
        2. + *
        3. org.apache.jackrabbit.spi.<testClassName>.<propName>
        4. + *
        5. org.apache.jackrabbit.spi.<packageName>.<propName>
        6. + *
        7. org.apache.jackrabbit.spi.<propName>
        8. + *
        + * Where: + *
          + *
        • <testClassName> is the name of the test class without package prefix.
        • + *
        • <testMethodName> is the name of the test method
        • + *
        • <packageName> is the name of the package of the test class. + *
        + * @param propName the propName of the configuration property. + * @return the value of the property or null if the property + * does not exist. + * @throws RepositoryException if an error occurs while reading from + * the configuration. + */ + public String getProperty(String propName) throws RepositoryException { + String testCaseName = getName(); + String testClassName = getClass().getName(); + String testPackName = ""; + int idx; + if ((idx = testClassName.lastIndexOf('.')) > -1) { + testPackName = testClassName.substring(testClassName.lastIndexOf('.', idx - 1) + 1, idx); + testClassName = testClassName.substring(idx + 1); + } + + // 1) test case specific property first + String value = helper.getProperty(RepositoryServiceStub.PROP_PREFIX + "." + + testClassName + "." + testCaseName + "." + propName); + if (value != null) { + return value; + } + + // 2) check test class property + value = helper.getProperty(RepositoryServiceStub.PROP_PREFIX + "." + + testClassName + "." + propName); + if (value != null) { + return value; + } + + // 3) check package property + value = helper.getProperty(RepositoryServiceStub.PROP_PREFIX + "." + + testPackName + "." + propName); + if (value != null) { + return value; + } + + // finally try global property + return helper.getProperty(RepositoryServiceStub.PROP_PREFIX + "." + propName); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + sessionInfo = helper.getAdminSessionInfo(); + } + + @Override + protected void tearDown() throws Exception { + if (sessionInfo != null) { + helper.getRepositoryService().dispose(sessionInfo); + } + super.tearDown(); + } +} \ No newline at end of file diff --git a/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/Helper.java b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/Helper.java new file mode 100644 index 00000000000..11c4044a24b --- /dev/null +++ b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/Helper.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.spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import javax.jcr.Credentials; +import java.util.HashMap; +import java.util.Map; + +/** Helper... */ +public class Helper { + + private static Logger log = LoggerFactory.getLogger(Helper.class); + + /** + * RepositoryService stub reference. + */ + private RepositoryServiceStub repoServiceStub; + + /** + * Overlay configuration. + */ + private Map configuration = new HashMap(); + + /** + * Creates a repository helper with configuration from + * repositoryStubImpl.properties file. + */ + public Helper() { + } + + /** + * Creates a repository helper with additional configuration parameters. + * + * @param config configuration which overlays the values from the property + * file. + */ + public Helper(Map config) { + configuration.putAll(config); + } + + /** + * Returns the repository service instance to test. + * @return the repository service instance to test. + * @throws RepositoryException if the repository could not be obtained. + */ + public RepositoryService getRepositoryService() throws RepositoryException { + if (repoServiceStub == null) { + repoServiceStub = RepositoryServiceStub.getInstance(configuration); + } + return repoServiceStub.getRepositoryService(); + } + + /** + * Returns the value of the configuration property with specified + * name. If the property does not exist null is + * returned. + *

        + * Configuration properties are defined in the file: + * repositoryStubImpl.properties. + * + * @param name the name of the property to retrieve. + * @return the value of the property or null if non existent. + * @throws RepositoryException if the configuration file cannot be found. + */ + public String getProperty(String name) throws RepositoryException { + // force assignment of repoStub + getRepositoryService(); + return repoServiceStub.getProperty(name); + } + + public Credentials getAdminCredentials() throws RepositoryException { + // force assignment of repoStub + getRepositoryService(); + return repoServiceStub.getAdminCredentials(); + } + + public Credentials getReadOnlyCredentials() throws RepositoryException { + // force assignment of repoStub + getRepositoryService(); + return repoServiceStub.getReadOnlyCredentials(); + } + + public SessionInfo getAdminSessionInfo() throws RepositoryException { + // force assignment of repoStub + getRepositoryService(); + String propName = RepositoryServiceStub.PROP_PREFIX + "." + RepositoryServiceStub.PROP_WORKSPACE; + String wspName = repoServiceStub.getProperty(propName); + return getRepositoryService().obtain(getAdminCredentials(), wspName); + } + + public SessionInfo getReadOnlySessionInfo() throws RepositoryException { + // force assignment of repoStub + getRepositoryService(); + String propName = RepositoryServiceStub.PROP_PREFIX + "." + RepositoryServiceStub.PROP_WORKSPACE; + String wspName = repoServiceStub.getProperty(propName); + return getRepositoryService().obtain(getReadOnlyCredentials(), wspName); + } +} \ No newline at end of file diff --git a/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/QValueFactoryTest.java b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/QValueFactoryTest.java new file mode 100644 index 00000000000..b749a708154 --- /dev/null +++ b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/QValueFactoryTest.java @@ -0,0 +1,515 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Calendar; + +/** QValueFactoryTest... */ +public class QValueFactoryTest extends AbstractSPITest { + + private static Logger log = LoggerFactory.getLogger(QValueFactoryTest.class); + + protected QValueFactory factory; + + private final Calendar calendar = Calendar.getInstance(); + protected Path rootPath; + protected Name testName; + protected String reference; + + protected void setUp() throws Exception { + super.setUp(); + RepositoryService service = helper.getRepositoryService(); + factory = service.getQValueFactory(); + + rootPath = service.getPathFactory().getRootPath(); + testName = service.getNameFactory().create(Name.NS_JCR_URI, "data"); + reference = getProperty("reference"); + } + + private static void assertValueLength(QValue v, long expectedLength) throws RepositoryException { + long length = v.getLength(); + if (length != -1) { + assertEquals(expectedLength, length); + } + } + + public void testIllegalType() throws RepositoryException { + try { + factory.create("any", 54); + fail("54 is not a valid property type"); + } catch (IllegalArgumentException e) { + // ok + } + } + + //-------------------------------------------------------------< DOUBLE >--- + + public void testCreateInvalidDoubleValue() throws RepositoryException { + try { + factory.create("any", PropertyType.DOUBLE); + fail("'any' cannot be converted to a valid double value."); + } catch (ValueFormatException e) { + // ok + } + } + + public void testGetDoubleOnBooleanValue() throws RepositoryException { + try { + QValue v = factory.create(true); + v.getDouble(); + fail("'true' cannot be converted to a valid double value."); + } catch (ValueFormatException e) { + // ok + } + } + + //---------------------------------------------------------------< LONG >--- + + public void testCreateInvalidLongValue() throws RepositoryException { + try { + factory.create("any", PropertyType.LONG); + fail("'any' cannot be converted to a valid long value."); + } catch (ValueFormatException e) { + // ok + } + } + + public void testGetLongOnBooleanValue() throws RepositoryException { + try { + QValue v = factory.create(true); + v.getLong(); + fail("'true' cannot be converted to a valid long value."); + } catch (ValueFormatException e) { + // ok + } + } + + //------------------------------------------------------------< BOOLEAN >--- + /** + * QValueImpl has a final static constant for the TRUE and the FALSE boolean + * values. Test if the various create methods use the constants (thus always + * return the 'same' object. + * + * @throws RepositoryException + */ + public void testFinalBooleanValue() throws RepositoryException { + assertSame(factory.create(true), factory.create(Boolean.TRUE.toString(), PropertyType.BOOLEAN)); + assertSame(factory.create(true), factory.create(true)); + + assertSame(factory.create(false), factory.create(Boolean.FALSE.toString(), PropertyType.BOOLEAN)); + assertSame(factory.create(false), factory.create(false)); + } + + /** + * Test if creating Boolean QValue from boolean and from String with boolean + * type return equal objects. + * + * @throws RepositoryException + */ + public void testCreateBooleanValueFromString() throws RepositoryException { + QValue v = factory.create(Boolean.TRUE.toString(), PropertyType.BOOLEAN); + assertEquals("Creating boolean type QValue from boolean or String must be equal.", + factory.create(true), v); + + v = factory.create(Boolean.FALSE.toString(), PropertyType.BOOLEAN); + assertEquals("Creating boolean type QValue from boolean or String must be equal.", + factory.create(false), v); + } + + public void testCreateTrueBooleanValue() throws RepositoryException { + QValue v = factory.create(true); + assertEquals("Boolean value must be true", Boolean.TRUE.toString(), v.getString()); + assertEquals("Boolean value must be true", true, v.getBoolean()); + } + + public void testCreateFalseBooleanValue() throws RepositoryException { + QValue v = factory.create(false); + assertEquals("Boolean value must be false", Boolean.FALSE.toString(), v.getString()); + assertEquals("Boolean value must be false", false, v.getBoolean()); + } + + public void testCreateTrueFromString() throws ValueFormatException, RepositoryException { + QValue v = factory.create(Boolean.TRUE.toString(), PropertyType.STRING); + assertEquals("Boolean value must be true", true, v.getBoolean()); + } + + public void testCreateFalseFromString() throws ValueFormatException, RepositoryException { + QValue v = factory.create("any", PropertyType.STRING); + assertEquals("Boolean value must be false", false, v.getBoolean()); + } + + public void testReadBooleanAsLong() throws RepositoryException { + try { + QValue v = factory.create(true); + v.getLong(); + } + catch (ValueFormatException e) { + return; // ok + } + assertTrue("Cannot convert value to long", false); + } + + //---------------------------------------------------------------< DATE >--- + public void testNullDateValue() throws IOException, RepositoryException { + try { + factory.create((Calendar) null); + fail(); + } catch (IllegalArgumentException e) { + // ok + } catch (NullPointerException e) { + // ok + } + try { + factory.create(null, PropertyType.DATE); + fail(); + } catch (IllegalArgumentException e) { + // ok + } catch (NullPointerException e) { + // ok + } + } + + public void testDateValueType() throws RepositoryException { + QValue v = factory.create(calendar); + assertTrue("Type of a date value must be PropertyType.DATE", v.getType() == PropertyType.DATE); + } + public void testDateValueEquality() throws RepositoryException { + QValue v = factory.create(calendar); + QValue otherV = factory.create(calendar); + assertEquals("Equality of date value must be calculated based on their String representation.", v, otherV); + } + + public void testDateValueEquality2() throws RepositoryException { + QValue v = factory.create(calendar); + QValue otherV = factory.create(v.getString(), PropertyType.DATE); + assertEquals("Equality of date value must be calculated based on their String representation.", v, otherV); + } + + //----------------------------------------------------------< REFERENCE >--- + + public void testNullReferenceValue() throws IOException, RepositoryException { + try { + factory.create(null, PropertyType.REFERENCE); + fail(); + } catch (IllegalArgumentException e) { + // ok + } catch (NullPointerException e) { + // ok + } + } + + public void testReferenceValueType() throws RepositoryException { + if (reference != null) { + QValue v = factory.create(reference, PropertyType.REFERENCE); + assertTrue("Type of a date value must be PropertyType.REFERENCE.", v.getType() == PropertyType.REFERENCE); + } else { + log.warn("Configuration entry 'QValueFactoryTest.reference' is missing -> skip test 'testReferenceValueType'."); + } + } + + public void testReferenceValueEquality() throws RepositoryException { + if (reference != null) { + QValue v = factory.create(reference, PropertyType.REFERENCE); + QValue otherV = factory.create(reference, PropertyType.REFERENCE); + assertEquals("Reference values created from the same string must be equal.", v, otherV); + } else { + log.warn("Configuration entry 'QValueFactoryTest.reference' is missing -> skip test 'testReferenceValueEquality'."); + } + } + + public void testEqualityDifferentTypes() throws RepositoryException { + if (reference != null) { + QValue v = factory.create(reference, PropertyType.REFERENCE); + QValue v2 = factory.create(reference, PropertyType.STRING); + assertFalse(v.equals(v2)); + } else { + log.warn("Configuration entry 'QValueFactoryTest.reference' is missing -> skip test 'testEqualityDifferentTypes'."); + } + } + + + //---------------------------------------------------------------< Name >--- + + public void testNullNameValue() throws IOException, RepositoryException { + try { + factory.create((Name) null); + fail(); + } catch (IllegalArgumentException e) { + // ok + } catch (NullPointerException e) { + // ok + } + } + + public void testNameValueType() throws IOException, RepositoryException { + QValue v = factory.create(testName); + assertTrue(v.getType() == PropertyType.NAME); + v = factory.create(testName.toString(), PropertyType.NAME); + assertTrue(v.getType() == PropertyType.NAME); + } + + public void testNameValueEquality() throws IOException, RepositoryException { + QValue v = factory.create(testName); + QValue v2 = factory.create(testName.toString(), PropertyType.NAME); + assertTrue(v.equals(v2)); + } + + public void testNameValueGetString() throws IOException, RepositoryException { + QValue v = factory.create(testName); + assertTrue(v.getString().equals(testName.toString())); + } + + public void testNameValueGetName() throws RepositoryException { + QValue v = factory.create(testName); + assertTrue(v.getName().equals(testName)); + } + + public void testInvalidNameValue() throws RepositoryException { + try { + factory.create("abc", PropertyType.NAME); + fail("'abc' is not a valid Name -> creating QValue should fail."); + } catch (ValueFormatException e) { + // ok + } + } + + public void testAnyValueGetName() throws RepositoryException { + try { + factory.create(12345).getName(); + fail("12345 is not a valid Name value -> QValue.getName() should fail."); + } catch (ValueFormatException e) { + // ok + } + } + + //---------------------------------------------------------------< Path >--- + + public void testNullPathValue() throws IOException, RepositoryException { + try { + factory.create((Path) null); + fail(); + } catch (IllegalArgumentException e) { + // ok + } catch (NullPointerException e) { + // ok + } + } + + public void testPathValueType() throws IOException, RepositoryException { + QValue v = factory.create(rootPath); + assertTrue(v.getType() == PropertyType.PATH); + v = factory.create(rootPath.toString(), PropertyType.PATH); + assertTrue(v.getType() == PropertyType.PATH); + } + + + public void testPathValueEquality() throws IOException, RepositoryException { + QValue v = factory.create(rootPath); + QValue v2 = factory.create(rootPath.toString(), PropertyType.PATH); + assertTrue(v.equals(v2)); + } + + public void testPathValueGetString() throws IOException, RepositoryException { + QValue v = factory.create(rootPath); + assertTrue(v.getString().equals(rootPath.toString())); + } + + public void testPathValueGetPath() throws RepositoryException { + QValue v = factory.create(rootPath); + assertTrue(v.getPath().equals(rootPath)); + } + + public void testInvalidPathValue() throws RepositoryException { + try { + factory.create("abc", PropertyType.PATH); + fail("'abc' is not a valid Path -> creating QValue should fail."); + } catch (ValueFormatException e) { + // ok + } + } + + public void testAnyValueGetPath() throws RepositoryException { + try { + factory.create(12345).getPath(); + fail("12345 is not a valid Path value -> QValue.getPath() should fail."); + } catch (ValueFormatException e) { + // ok + } + } + + //-------------------------------------------------------------< BINARY >--- + + public void testNullBinaryValue() throws IOException, RepositoryException { + try { + factory.create((byte[]) null); + fail(); + } catch (IllegalArgumentException e) { + // ok + } catch (NullPointerException e) { + // ok + } + try { + factory.create((InputStream) null); + fail(); + } catch (IllegalArgumentException e) { + // ok + } catch (NullPointerException e) { + // ok + } + try { + factory.create((File) null); + fail(); + } catch (IllegalArgumentException e) { + // ok + } catch (NullPointerException e) { + // ok + } + } + + public void testBinaryValueType() throws IOException, RepositoryException { + QValue v = factory.create(new byte[] {'a', 'b', 'c'}); + assertTrue(v.getType() == PropertyType.BINARY); + } + + + public void testBinaryFromByteArray() throws RepositoryException, IOException { + QValue v = factory.create(new byte[] {'a', 'b', 'c'}); + + assertEquals(PropertyType.BINARY, v.getType()); + assertValueLength(v, 3); + + assertEquals("abc", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("abc", new String(out.toByteArray())); + } + + public void testEmptyBinaryFromByteArray() throws RepositoryException, IOException { + QValue v = factory.create(new byte[0]); + + assertEquals(PropertyType.BINARY, v.getType()); + assertValueLength(v, 0); + + assertEquals("", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("", new String(out.toByteArray())); + } + + public void testBinaryFromInputStream() throws RepositoryException, IOException { + InputStream in = new ByteArrayInputStream(new byte[] {'a', 'b', 'c'}); + + QValue v = factory.create(in); + + assertEquals(PropertyType.BINARY, v.getType()); + assertValueLength(v, 3); + + assertEquals("abc", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("abc", new String(out.toByteArray())); + } + + public void testEmptyBinaryFromInputStream() throws RepositoryException, IOException { + InputStream in = new ByteArrayInputStream(new byte[0]); + + QValue v = factory.create(in); + + assertEquals(PropertyType.BINARY, v.getType()); + assertValueLength(v, 0); + + assertEquals("", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("", new String(out.toByteArray())); + } + + + public void testBinaryFromFile() throws RepositoryException, IOException { + File f = File.createTempFile("QValueFactoryImplTest", ".txt"); + f.deleteOnExit(); + FileWriter fw = new FileWriter(f); + fw.write("abc"); + fw.close(); + + QValue v = factory.create(f); + + assertEquals(PropertyType.BINARY, v.getType()); + assertValueLength(v, 3); + + assertEquals("abc", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("abc", new String(out.toByteArray())); + } + + public void testEmptyBinaryFromFile() throws RepositoryException, IOException { + File f = File.createTempFile("QValueFactoryImplTest", ".txt"); + f.deleteOnExit(); + + QValue v = factory.create(f); + + assertEquals(PropertyType.BINARY, v.getType()); + assertValueLength(v, 0); + + assertEquals("", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("", new String(out.toByteArray())); + } + + /** + * + * @param out + * @param in + * @throws RepositoryException + * @throws IOException + */ + private static void spool(OutputStream out, InputStream in) throws RepositoryException, IOException { + try { + byte[] buffer = new byte[0x2000]; + int read; + while ((read = in.read(buffer)) > 0) { + out.write(buffer, 0, read); + } + } finally { + try { + in.close(); + } catch (IOException ignore) { + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/QValueTest.java b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/QValueTest.java new file mode 100644 index 00000000000..bea3838c551 --- /dev/null +++ b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/QValueTest.java @@ -0,0 +1,494 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.FileWriter; +import java.util.Calendar; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +/** + * QValueTest... + */ +public class QValueTest extends AbstractSPITest { + + private static Logger log = LoggerFactory.getLogger(QValueTest.class); + + private final Calendar CALENDAR = Calendar.getInstance(); + protected Path rootPath; + protected Name testName; + protected String reference; + protected QValueFactory factory; + + protected void setUp() throws Exception { + super.setUp(); + RepositoryService service = helper.getRepositoryService(); + factory = service.getQValueFactory(); + + rootPath = service.getPathFactory().getRootPath(); + testName = service.getNameFactory().create(Name.NS_JCR_URI, "data"); + reference = getProperty("reference"); + } + + public void testIllegalType() throws RepositoryException { + try { + factory.create("any", 54); + fail("54 is not a valid property type"); + } catch (IllegalArgumentException e) { + // ok + } + } + + //-------------------------------------------------------------< DOUBLE >--- + + public void testCreateInvalidDoubleValue() throws RepositoryException { + try { + factory.create("any", PropertyType.DOUBLE); + fail("'any' cannot be converted to a valid double value."); + } catch (ValueFormatException e) { + // ok + } + } + + public void testGetDoubleOnBooleanValue() throws RepositoryException { + try { + QValue v = factory.create(true); + v.getDouble(); + fail("'true' cannot be converted to a valid double value."); + } catch (ValueFormatException e) { + // ok + } + } + + //---------------------------------------------------------------< LONG >--- + + public void testCreateInvalidLongValue() throws RepositoryException { + try { + factory.create("any", PropertyType.LONG); + fail("'any' cannot be converted to a valid long value."); + } catch (ValueFormatException e) { + // ok + } + } + + public void testGetLongOnBooleanValue() throws RepositoryException { + try { + QValue v = factory.create(true); + v.getLong(); + fail("'true' cannot be converted to a valid long value."); + } catch (ValueFormatException e) { + // ok + } + } + + //------------------------------------------------------------< BOOLEAN >--- + /** + * QValueImpl has a final static constant for the TRUE and the FALSE boolean + * values. Test if the various create methods use the constants (thus always + * return the 'same' object. + * + * @throws javax.jcr.RepositoryException + */ + public void testFinalBooleanValue() throws RepositoryException { + assertSame(factory.create(true), factory.create(Boolean.TRUE.toString(), PropertyType.BOOLEAN)); + assertSame(factory.create(true), factory.create(true)); + + assertSame(factory.create(false), factory.create(Boolean.FALSE.toString(), PropertyType.BOOLEAN)); + assertSame(factory.create(false), factory.create(false)); + } + + /** + * Test if creating Boolean QValue from boolean and from String with boolean + * type return equal objects. + * + * @throws javax.jcr.RepositoryException + */ + public void testCreateBooleanValueFromString() throws RepositoryException { + QValue v = factory.create(Boolean.TRUE.toString(), PropertyType.BOOLEAN); + assertEquals("Creating boolean type QValue from boolean or String must be equal.", + factory.create(true), v); + + v = factory.create(Boolean.FALSE.toString(), PropertyType.BOOLEAN); + assertEquals("Creating boolean type QValue from boolean or String must be equal.", + factory.create(false), v); + } + + public void testCreateTrueBooleanValue() throws RepositoryException { + QValue v = factory.create(true); + assertEquals("Boolean value must be true", Boolean.TRUE.toString(), v.getString()); + assertEquals("Boolean value must be true", true, v.getBoolean()); + } + + public void testCreateFalseBooleanValue() throws RepositoryException { + QValue v = factory.create(false); + assertEquals("Boolean value must be false", Boolean.FALSE.toString(), v.getString()); + assertEquals("Boolean value must be false", false, v.getBoolean()); + } + + public void testCreateTrueFromString() throws ValueFormatException, RepositoryException { + QValue v = factory.create(Boolean.TRUE.toString(), PropertyType.STRING); + assertEquals("Boolean value must be true", true, v.getBoolean()); + } + + public void testCreateFalseFromString() throws ValueFormatException, RepositoryException { + QValue v = factory.create("any", PropertyType.STRING); + assertEquals("Boolean value must be false", false, v.getBoolean()); + } + + public void testReadBooleanAsLong() throws RepositoryException { + try { + QValue v = factory.create(true); + v.getLong(); + } + catch (ValueFormatException e) { + return; // ok + } + assertTrue("Cannot convert value to long", false); + } + + //---------------------------------------------------------------< DATE >--- + public void testNullDateValue() throws IOException, RepositoryException { + try { + factory.create((Calendar) null); + fail(); + } catch (RuntimeException e) { + // ok + } + try { + factory.create(null, PropertyType.DATE); + fail(); + } catch (RuntimeException e) { + // ok + } + } + + public void testDateValueType() throws RepositoryException { + QValue v = factory.create(CALENDAR); + assertTrue("Type of a date value must be PropertyType.DATE", v.getType() == PropertyType.DATE); + } + public void testDateValueEquality() throws RepositoryException { + QValue v = factory.create(CALENDAR); + QValue otherV = factory.create(CALENDAR); + assertEquals("Equality of date value must be calculated based on their String representation.", v, otherV); + } + + public void testDateValueEquality2() throws RepositoryException { + QValue v = factory.create(CALENDAR); + QValue otherV = factory.create(v.getString(), PropertyType.DATE); + assertEquals("Equality of date value must be calculated based on their String representation.", v, otherV); + } + + //----------------------------------------------------------< REFERENCE >--- + + public void testNullReferenceValue() throws IOException, RepositoryException { + try { + factory.create(null, PropertyType.REFERENCE); + fail(); + } catch (RuntimeException e) { + // ok + } + } + + public void testReferenceValueType() throws RepositoryException { + if (reference != null) { + QValue v = factory.create(reference, PropertyType.REFERENCE); + assertTrue("Type of a date value must be PropertyType.REFERENCE.", v.getType() == PropertyType.REFERENCE); + } else { + log.warn("Configuration entry 'QValueFactoryTest.reference' is missing -> skip test 'testReferenceValueType'."); + } + } + + public void testReferenceValueEquality() throws RepositoryException { + if (reference != null) { + QValue v = factory.create(reference, PropertyType.REFERENCE); + QValue otherV = factory.create(reference, PropertyType.REFERENCE); + assertEquals("Reference values created from the same string must be equal.", v, otherV); + } else { + log.warn("Configuration entry 'QValueFactoryTest.reference' is missing -> skip test 'testReferenceValueEquality'."); + } + } + + public void testEqualityDifferentTypes() throws RepositoryException { + if (reference != null) { + QValue v = factory.create(reference, PropertyType.REFERENCE); + QValue v2 = factory.create(reference, PropertyType.STRING); + assertFalse(v.equals(v2)); + } else { + log.warn("Configuration entry 'QValueFactoryTest.reference' is missing -> skip test 'testEqualityDifferentTypes'."); + } + } + + + //---------------------------------------------------------------< Name >--- + + public void testNullNameValue() throws IOException, RepositoryException { + try { + factory.create((Name) null); + fail(); + } catch (RuntimeException e) { + // ok + } + } + + public void testNameValueType() throws IOException, RepositoryException { + QValue v = factory.create(testName); + assertTrue(v.getType() == PropertyType.NAME); + v = factory.create(testName.toString(), PropertyType.NAME); + assertTrue(v.getType() == PropertyType.NAME); + } + + public void testNameValueEquality() throws IOException, RepositoryException { + QValue v = factory.create(testName); + QValue v2 = factory.create(testName.toString(), PropertyType.NAME); + assertTrue(v.equals(v2)); + } + + public void testNameValueGetString() throws IOException, RepositoryException { + QValue v = factory.create(testName); + assertTrue(v.getString().equals(testName.toString())); + } + + public void testNameValueGetName() throws RepositoryException { + QValue v = factory.create(testName); + assertTrue(v.getName().equals(testName)); + } + + public void testInvalidNameValue() throws RepositoryException { + try { + factory.create("abc", PropertyType.NAME); + fail("'abc' is not a valid Name -> creating QValue should fail."); + } catch (ValueFormatException e) { + // ok + } + } + + public void testAnyValueGetName() throws RepositoryException { + try { + factory.create(12345).getName(); + fail("12345 is not a valid Name value -> QValue.getName() should fail."); + } catch (ValueFormatException e) { + // ok + } + } + + //---------------------------------------------------------------< Path >--- + + public void testNullPathValue() throws IOException, RepositoryException { + try { + factory.create((Path) null); + fail(); + } catch (RuntimeException e) { + // ok + } + } + + public void testPathValueType() throws IOException, RepositoryException { + QValue v = factory.create(rootPath); + assertTrue(v.getType() == PropertyType.PATH); + v = factory.create(rootPath.toString(), PropertyType.PATH); + assertTrue(v.getType() == PropertyType.PATH); + } + + + public void testPathValueEquality() throws IOException, RepositoryException { + QValue v = factory.create(rootPath); + QValue v2 = factory.create(rootPath.toString(), PropertyType.PATH); + assertTrue(v.equals(v2)); + } + + public void testPathValueGetString() throws IOException, RepositoryException { + QValue v = factory.create(rootPath); + assertTrue(v.getString().equals(rootPath.toString())); + } + + public void testPathValueGetPath() throws RepositoryException { + QValue v = factory.create(rootPath); + assertTrue(v.getPath().equals(rootPath)); + } + + public void testInvalidPathValue() throws RepositoryException { + try { + factory.create("abc", PropertyType.PATH); + fail("'abc' is not a valid Path -> creating QValue should fail."); + } catch (ValueFormatException e) { + // ok + } + } + + public void testAnyValueGetPath() throws RepositoryException { + try { + factory.create(12345).getPath(); + fail("12345 is not a valid Path value -> QValue.getPath() should fail."); + } catch (ValueFormatException e) { + // ok + } + } + + //-------------------------------------------------------------< BINARY >--- + + public void testNullBinaryValue() throws IOException, RepositoryException { + try { + factory.create((byte[]) null); + fail(); + } catch (RuntimeException e) { + // ok + } + try { + factory.create((InputStream) null); + fail(); + } catch (RuntimeException e) { + // ok + } + try { + factory.create((File) null); + fail(); + } catch (RuntimeException e) { + // ok + } + } + + public void testBinaryValueType() throws IOException, RepositoryException { + QValue v = factory.create(new byte[] {'a', 'b', 'c'}); + assertTrue(v.getType() == PropertyType.BINARY); + } + + + public void testBinaryFromByteArray() throws RepositoryException, IOException { + QValue v = factory.create(new byte[] {'a', 'b', 'c'}); + + assertEquals(PropertyType.BINARY, v.getType()); + assertEquals(3, v.getLength()); + + assertEquals("abc", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("abc", new String(out.toByteArray())); + } + + public void testEmptyBinaryFromByteArray() throws RepositoryException, IOException { + QValue v = factory.create(new byte[0]); + + assertEquals(PropertyType.BINARY, v.getType()); + assertEquals(0, v.getLength()); + + assertEquals("", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("", new String(out.toByteArray())); + } + + public void testBinaryFromInputStream() throws RepositoryException, IOException { + InputStream in = new ByteArrayInputStream(new byte[] {'a', 'b', 'c'}); + + QValue v = factory.create(in); + + assertEquals(PropertyType.BINARY, v.getType()); + assertEquals(3, v.getLength()); + + assertEquals("abc", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("abc", new String(out.toByteArray())); + } + + public void testEmptyBinaryFromInputStream() throws RepositoryException, IOException { + InputStream in = new ByteArrayInputStream(new byte[0]); + + QValue v = factory.create(in); + + assertEquals(PropertyType.BINARY, v.getType()); + assertEquals(0, v.getLength()); + + assertEquals("", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("", new String(out.toByteArray())); + } + + + public void testBinaryFromFile() throws RepositoryException, IOException { + File f = File.createTempFile("QValueFactoryImplTest", ".txt"); + f.deleteOnExit(); + FileWriter fw = new FileWriter(f); + fw.write("abc"); + fw.close(); + + QValue v = factory.create(f); + + assertEquals(PropertyType.BINARY, v.getType()); + assertEquals(3, v.getLength()); + + assertEquals("abc", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("abc", new String(out.toByteArray())); + } + + public void testEmptyBinaryFromFile() throws RepositoryException, IOException { + File f = File.createTempFile("QValueFactoryImplTest", ".txt"); + f.deleteOnExit(); + + QValue v = factory.create(f); + + assertEquals(PropertyType.BINARY, v.getType()); + assertEquals(0, v.getLength()); + + assertEquals("", v.getString()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + spool(out, v.getStream()); + assertEquals("", new String(out.toByteArray())); + } + + /** + * + * @param out + * @param in + * @throws javax.jcr.RepositoryException + * @throws java.io.IOException + */ + private static void spool(OutputStream out, InputStream in) throws RepositoryException, IOException { + try { + byte[] buffer = new byte[0x2000]; + int read; + while ((read = in.read(buffer)) > 0) { + out.write(buffer, 0, read); + } + } finally { + try { + in.close(); + } catch (IOException ignore) { + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/RepositoryServiceStub.java b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/RepositoryServiceStub.java new file mode 100644 index 00000000000..d04a42fa218 --- /dev/null +++ b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/RepositoryServiceStub.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi; + +import javax.jcr.RepositoryException; +import javax.jcr.Credentials; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; +import java.util.Properties; + +/** + * The RepositoryServiceStub is the entry point to the SPI + * RepositoryService. + */ +public abstract class RepositoryServiceStub { + + public static final String STUB_IMPL_PROPS = "repositoryServiceStubImpl.properties"; + + public static final String PROP_PREFIX = "org.apache.jackrabbit.spi"; + + public static final String STUB_IMPL_SYS_PROPS = PROP_PREFIX + ".properties"; + + public static final String PROP_STUB_IMPL_CLASS = PROP_PREFIX + ".repository_service_stub_impl"; + + public static final String PROP_ADMIN_PWD = "admin.pwd"; + + public static final String PROP_ADMIN_NAME = "admin.name"; + + public static final String PROP_READONLY_PWD = "readonly.pwd"; + + public static final String PROP_READONLY_NAME = "readonly.name"; + + public static final String PROP_WORKSPACE = "workspacename"; + + protected final Properties environment; + + /** + * Implementations of this class must overwrite this constructor. + * + * @param env the environment variables. This parameter must not be null. + */ + protected RepositoryServiceStub(Properties env) { + if (env == null) { + throw new IllegalArgumentException("Parameter 'env' must not be null!"); + } + environment = env; + } + + public static RepositoryServiceStub getInstance(Map configuration) throws RepositoryException { + Properties props = null; + RepositoryServiceStub stub = null; + String implProp = System.getProperty(RepositoryServiceStub.STUB_IMPL_SYS_PROPS); + if (implProp != null) { + File implPropFile = new File(implProp); + if (implPropFile.exists()) { + props = new Properties(); + try { + props.load(new FileInputStream(implPropFile)); + } catch (IOException e) { + throw new RepositoryException("Unable to load config file: " + + implProp + " " + e.toString()); + } + } else { + throw new RepositoryException("File does not exist: " + implProp); + } + } + + if (props == null) { + props = new Properties(); + InputStream is = RepositoryServiceStub.class.getClassLoader().getResourceAsStream(RepositoryServiceStub.STUB_IMPL_PROPS); + if (is != null) { + try { + props.load(is); + } catch (IOException e) { + throw new RepositoryException("Exception reading " + + RepositoryServiceStub.STUB_IMPL_PROPS + ": " + e.toString()); + } + } + } + + // overlay with configuration parameter + props.putAll(configuration); + + try { + String className = props.getProperty(RepositoryServiceStub.PROP_STUB_IMPL_CLASS); + if (className == null || className.length() == 0) { + throw new RepositoryException("Property " + RepositoryServiceStub.PROP_STUB_IMPL_CLASS + " not defined!"); + } + Class stubClass = Class.forName(className); + Constructor constr = stubClass.getConstructor(new Class[]{Properties.class}); + stub = (RepositoryServiceStub) constr.newInstance(new Object[]{props}); + } catch (ClassCastException e) { + throw new RepositoryException(e.toString()); + } catch (NoSuchMethodException e) { + throw new RepositoryException(e.toString()); + } catch (ClassNotFoundException e) { + throw new RepositoryException(e.toString()); + } catch (InstantiationException e) { + throw new RepositoryException(e.toString()); + } catch (IllegalAccessException e) { + throw new RepositoryException(e.toString()); + } catch (InvocationTargetException e) { + throw new RepositoryException(e.toString()); + } + + return stub; + } + + /** + * Returns a reference to the RepositoryService provided by this + * RepositoryServiceStub. + * + * @return + */ + public abstract RepositoryService getRepositoryService() throws RepositoryException; + + /** + * Returns the property with the specified name. If a + * property with the given name does not exist, null is + * returned. + * @param name the name of the property. + * @return the property, or null if the property does not + * exist. + */ + public String getProperty(String name) { + return environment.getProperty(name); + } + + public abstract Credentials getAdminCredentials() throws RepositoryException; + + public abstract Credentials getReadOnlyCredentials() throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/RepositoryServiceTest.java b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/RepositoryServiceTest.java new file mode 100644 index 00000000000..0830cb3b0d5 --- /dev/null +++ b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/RepositoryServiceTest.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.spi; + +import javax.jcr.RepositoryException; +import java.util.Map; +import java.util.Arrays; + +/** RepositoryServiceTest... */ +public class RepositoryServiceTest extends AbstractSPITest { + + private RepositoryService service; + + protected void setUp() throws Exception { + super.setUp(); + service = helper.getRepositoryService(); + } + + public void testGetIdFactory() throws RepositoryException { + assertNotNull(service.getIdFactory()); + } + + public void testGetQValueFactory() throws RepositoryException { + assertNotNull(service.getQValueFactory()); + } + + public void testGetNameFactory() throws RepositoryException { + assertNotNull(service.getNameFactory()); + } + + public void testGetPathFactory() throws RepositoryException { + assertNotNull(service.getPathFactory()); + } + + public void testGetRepositoryDescriptors() throws RepositoryException { + Map descriptors = service.getRepositoryDescriptors(); + assertNotNull(descriptors); + assertTrue(!descriptors.isEmpty()); + } + + public void testGetWorkspaceNames() throws RepositoryException { + String[] workspaceNames = service.getWorkspaceNames(sessionInfo); + assertNotNull("Workspace names must not be null", workspaceNames); + assertTrue("Workspace names must contain at least a single workspace", workspaceNames.length > 0); + + String wspName = getProperty(RepositoryServiceStub.PROP_WORKSPACE); + if (wspName != null) { + assertTrue("Workspace name used for retrieving the SessionInfo must be included in the available workspaces.", Arrays.asList(workspaceNames).contains(wspName)); + } + } + + public void testNullWorkspaceName() throws RepositoryException { + SessionInfo sInfo = service.obtain(helper.getAdminCredentials(), null); + try { + assertNotNull(sInfo.getWorkspaceName()); + } finally { + service.dispose(sInfo); + } + } + + // TODO: add more tests +} \ No newline at end of file diff --git a/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/SessionInfoTest.java b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/SessionInfoTest.java new file mode 100644 index 00000000000..1e7f4137e15 --- /dev/null +++ b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/SessionInfoTest.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.spi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** SessionInfoTest... */ +public class SessionInfoTest extends AbstractSPITest { + + private static Logger log = LoggerFactory.getLogger(SessionInfoTest.class); + + private String workspaceName; + private SessionInfo sessionInfo; + + protected void setUp() throws Exception { + super.setUp(); + + workspaceName = getProperty(RepositoryServiceStub.PROP_WORKSPACE); + sessionInfo = helper.getRepositoryService().obtain(helper.getAdminCredentials(), workspaceName); + } + + protected void tearDown() throws Exception { + if (sessionInfo != null) { + helper.getRepositoryService().dispose(sessionInfo); + } + super.tearDown(); + } + + public void testGetWorkspaceName() { + if (workspaceName == null) { + assertNotNull(sessionInfo.getWorkspaceName()); + } else { + assertEquals(workspaceName, sessionInfo.getWorkspaceName()); + } + } + + // TODO: add more tests +} \ No newline at end of file diff --git a/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/TestAll.java b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/TestAll.java new file mode 100644 index 00000000000..6bb315532b9 --- /dev/null +++ b/jackrabbit-spi/src/test/java/org/apache/jackrabbit/spi/TestAll.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.spi; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** TestAll... */ +public class TestAll extends TestCase { + + public static Test suite() { + + TestSuite suite = new TestSuite("SPI tests"); + + suite.addTestSuite(RepositoryServiceTest.class); + suite.addTestSuite(SessionInfoTest.class); + suite.addTestSuite(QValueFactoryTest.class); + suite.addTestSuite(QValueTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/README.txt b/jackrabbit-spi2dav/README.txt new file mode 100644 index 00000000000..6034460ebca --- /dev/null +++ b/jackrabbit-spi2dav/README.txt @@ -0,0 +1,5 @@ +================================= +Welcome to Jackrabbit SPI2DAV +================================= + +This is the SPI2DAV component of the Apache Jackrabbit project. diff --git a/jackrabbit-spi2dav/assembly.xml b/jackrabbit-spi2dav/assembly.xml new file mode 100644 index 00000000000..62fcd709c24 --- /dev/null +++ b/jackrabbit-spi2dav/assembly.xml @@ -0,0 +1,35 @@ + + + + tests + false + + jar + + + + ${project.build.testOutputDirectory} + + + repositoryServiceStubImpl.properties + repositoryStubImpl.properties + log4j.properties + + + + diff --git a/jackrabbit-spi2dav/pom.xml b/jackrabbit-spi2dav/pom.xml new file mode 100644 index 00000000000..d2b713e43f4 --- /dev/null +++ b/jackrabbit-spi2dav/pom.xml @@ -0,0 +1,189 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-spi2dav + Jackrabbit SPI to WebDAV + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + **/TestAll.java + + once + ${test.opts} + + + known.issues + + + + + org.apache.jackrabbit.test.api.SerializationTest#testNodeTypeConstraintViolationWorkspace + + org.apache.jackrabbit.test.api.ImpersonateTest + + org.apache.jackrabbit.test.api.ShareableNodeTest + + org.apache.jackrabbit.test.api.LifecycleTest + + org.apache.jackrabbit.test.api.lock.LockManagerTest#testAddInvalidLockToken + org.apache.jackrabbit.test.api.lock.LockManagerTest#testAddLockTokenToAnotherSession + org.apache.jackrabbit.test.api.lock.LockManagerTest#testLockTransfer2 + + org.apache.jackrabbit.test.api.query.CreateQueryTest#testUnknownQueryLanguage + + org.apache.jackrabbit.test.api.query.qom.BindVariableValueTest + + org.apache.jackrabbit.test.api.version.simple + + org.apache.jackrabbit.test.api.version.ActivitiesTest + org.apache.jackrabbit.test.api.version.MergeActivityTest#testMergeActivity + org.apache.jackrabbit.test.api.version.ConfigurationsTest + + org.apache.jackrabbit.jcr2spi.IsSameTest#testIsSameProperty3 + org.apache.jackrabbit.jcr2spi.IsSameTest#testIsSameProperty4 + org.apache.jackrabbit.jcr2spi.IsSameTest#testIsSameNode7 + + org.apache.jackrabbit.jcr2spi.name.NamespaceRegistryTest#testReRegisteredNamespace + org.apache.jackrabbit.jcr2spi.name.NamespaceRegistryTest#testReRegisteredNamespaceVisibility + + + + + + + maven-assembly-plugin + + + package + + single + + + + assembly.xml + + + + + + + + + + org.apache.jackrabbit + jackrabbit-spi + ${project.version} + + + + org.apache.jackrabbit + jackrabbit-spi-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-webdav + ${project.version} + + + javax.jcr + jcr + + + org.slf4j + slf4j-api + + + javax.servlet + servlet-api + + + org.apache.httpcomponents + httpmime + 4.5.6 + + + junit + junit + test + + + org.apache.jackrabbit + jackrabbit-jcr-tests + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-jcr2spi + ${project.version} + + test + + + org.apache.jackrabbit + jackrabbit-jcr2spi + ${project.version} + tests + test + + + org.apache.jackrabbit + jackrabbit-spi + tests + ${project.version} + test + + + diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/BatchUtils.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/BatchUtils.java new file mode 100644 index 00000000000..9c3c236f688 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/BatchUtils.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.spi2dav; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +final class BatchUtils { + + /** + * The XML elements and attributes used in serialization + */ + private static final Namespace SV_NAMESPACE = Namespace.getNamespace(Name.NS_SV_PREFIX, Name.NS_SV_URI); + private static final String NODE_ELEMENT = "node"; + private static final String PROPERTY_ELEMENT = "property"; + private static final String VALUE_ELEMENT = "value"; + private static final String NAME_ATTRIBUTE = "name"; + private static final String TYPE_ATTRIBUTE = "type"; + + private BatchUtils() {}; + + static Element createNodeElement(Node parent, Name nodeName, Name primaryTypeName, String uniqueId, NamePathResolver resolver) throws NamespaceException { + Element nodeElement = DomUtil.addChildElement(parent, NODE_ELEMENT, SV_NAMESPACE); + String nameAttr = resolver.getJCRName(nodeName); + DomUtil.setAttribute(nodeElement, NAME_ATTRIBUTE, SV_NAMESPACE, nameAttr); + + // nodetype must never be null + Element propElement = DomUtil.addChildElement(nodeElement, PROPERTY_ELEMENT, SV_NAMESPACE); + String name = resolver.getJCRName(NameConstants.JCR_PRIMARYTYPE); + DomUtil.setAttribute(propElement, NAME_ATTRIBUTE, SV_NAMESPACE, name); + DomUtil.setAttribute(propElement, TYPE_ATTRIBUTE, SV_NAMESPACE, PropertyType.nameFromValue(PropertyType.NAME)); + name = resolver.getJCRName(primaryTypeName); + DomUtil.addChildElement(propElement, VALUE_ELEMENT, SV_NAMESPACE, name); + // optional uuid + if (uniqueId != null) { + propElement = DomUtil.addChildElement(nodeElement, PROPERTY_ELEMENT, SV_NAMESPACE); + name = resolver.getJCRName(NameConstants.JCR_UUID); + DomUtil.setAttribute(propElement, NAME_ATTRIBUTE, SV_NAMESPACE, name); + DomUtil.setAttribute(propElement, TYPE_ATTRIBUTE, SV_NAMESPACE, PropertyType.nameFromValue(PropertyType.STRING)); + DomUtil.addChildElement(propElement, VALUE_ELEMENT, SV_NAMESPACE, uniqueId); + } + return nodeElement; + } + + static void importProperty(Element nodeElement, Name propertyName, int type, QValue[] values, NamePathResolver resolver) throws RepositoryException { + Element propElement = DomUtil.addChildElement(nodeElement, PROPERTY_ELEMENT, SV_NAMESPACE); + DomUtil.setAttribute(propElement, NAME_ATTRIBUTE, SV_NAMESPACE, resolver.getJCRName(propertyName)); + DomUtil.setAttribute(propElement, TYPE_ATTRIBUTE, SV_NAMESPACE, PropertyType.nameFromValue(type)); + + // build all the values. + for (QValue value : values) { + DomUtil.addChildElement(propElement, VALUE_ELEMENT, SV_NAMESPACE, value.getString()); + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/CredentialsWrapper.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/CredentialsWrapper.java new file mode 100644 index 00000000000..f37b72e6c55 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/CredentialsWrapper.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.spi2dav; + +import javax.jcr.SimpleCredentials; + +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; + +/** + * CredentialsWrapper... + */ +class CredentialsWrapper { + + private final String userId; + private final UsernamePasswordCredentials credentials; + + CredentialsWrapper(javax.jcr.Credentials creds) { + + if (creds == null) { + // NOTE: null credentials only work if 'missing-auth-mapping' param is set on the server + userId = ""; + this.credentials = null; + } else if (creds instanceof SimpleCredentials) { + SimpleCredentials sCred = (SimpleCredentials) creds; + userId = sCred.getUserID(); + this.credentials = new UsernamePasswordCredentials(userId, String.valueOf(sCred.getPassword())); + } else { + userId = ""; + this.credentials = new UsernamePasswordCredentials(creds.toString()); + } + } + + String getUserId() { + return userId; + } + + Credentials getHttpCredentials() { + return credentials; + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/DefinitionUtil.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/DefinitionUtil.java new file mode 100644 index 00000000000..cc7d005207d --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/DefinitionUtil.java @@ -0,0 +1,302 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2dav; + +import org.apache.jackrabbit.commons.webdav.NodeTypeConstants; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.nodetype.QItemDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.QNodeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.QPropertyDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.QNodeTypeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.constraint.ValueConstraint; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.w3c.dom.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.version.OnParentVersionAction; +import java.util.ArrayList; +import java.util.List; + +/** + * Utility to build QNodeTypeDefinition, QNodeDefinition + * and QPropertyDefinition from the information retrieved from the + * WebDAV response body. + */ +class DefinitionUtil implements NodeTypeConstants { + + private static Logger log = LoggerFactory.getLogger(DefinitionUtil.class); + + /** + * Avoid instantiation. + */ + private DefinitionUtil() {} + + /** + * + * @param declaringNodeType + * @param ndefElement + * @param resolver + * @return + * @throws RepositoryException + */ + static QNodeDefinition createQNodeDefinition(Name declaringNodeType, Element ndefElement, NamePathResolver resolver) throws RepositoryException { + QNodeDefinitionBuilder builder = new QNodeDefinitionBuilder(); + + buildQItemDefinition(declaringNodeType, ndefElement, resolver, builder); + + // TODO: webdav server sends jcr names -> nsResolver required. improve this. + // NOTE: the server should send the namespace-mappings as addition ns-defininitions + try { + + if (ndefElement.hasAttribute(DEFAULTPRIMARYTYPE_ATTRIBUTE)) { + Name defaultPrimaryType = resolver.getQName(ndefElement.getAttribute(DEFAULTPRIMARYTYPE_ATTRIBUTE)); + builder.setDefaultPrimaryType(defaultPrimaryType); + } + + Element reqPrimaryTypes = DomUtil.getChildElement(ndefElement, REQUIREDPRIMARYTYPES_ELEMENT, null); + if (reqPrimaryTypes != null) { + ElementIterator it = DomUtil.getChildren(reqPrimaryTypes, REQUIREDPRIMARYTYPE_ELEMENT, null); + while (it.hasNext()) { + builder.addRequiredPrimaryType(resolver.getQName(DomUtil.getTextTrim(it.nextElement()))); + } + } else { + builder.addRequiredPrimaryType(NameConstants.NT_BASE); + } + + if (ndefElement.hasAttribute(SAMENAMESIBLINGS_ATTRIBUTE)) { + builder.setAllowsSameNameSiblings(Boolean.valueOf(ndefElement.getAttribute(SAMENAMESIBLINGS_ATTRIBUTE))); + } + } catch (NameException e) { + throw new RepositoryException(e); + } + return builder.build(); + } + + /** + * + * @param declaringNodeType + * @param pdefElement + * @param resolver + * @param qValueFactory + * @return + * @throws RepositoryException + */ + static QPropertyDefinition createQPropertyDefinition(Name declaringNodeType, Element pdefElement, + NamePathResolver resolver, QValueFactory qValueFactory) throws RepositoryException { + QPropertyDefinitionBuilder builder = new QPropertyDefinitionBuilder(); + + buildQItemDefinition(declaringNodeType, pdefElement, resolver, builder); + + if (pdefElement.hasAttribute(REQUIREDTYPE_ATTRIBUTE)) { + builder.setRequiredType(PropertyType.valueFromName(pdefElement.getAttribute(REQUIREDTYPE_ATTRIBUTE))); + } + + if (pdefElement.hasAttribute(MULTIPLE_ATTRIBUTE)) { + builder.setMultiple(Boolean.valueOf(pdefElement.getAttribute(MULTIPLE_ATTRIBUTE))); + } + + if (pdefElement.hasAttribute(FULL_TEXT_SEARCHABLE_ATTRIBUTE)) { + builder.setFullTextSearchable(Boolean.valueOf(pdefElement.getAttribute(FULL_TEXT_SEARCHABLE_ATTRIBUTE))); + } + if (pdefElement.hasAttribute(QUERY_ORDERABLE_ATTRIBUTE)) { + builder.setQueryOrderable(Boolean.valueOf(pdefElement.getAttribute(QUERY_ORDERABLE_ATTRIBUTE))); + } + + int requiredType = builder.getRequiredType(); + Element child = DomUtil.getChildElement(pdefElement, DEFAULTVALUES_ELEMENT, null); + if (child != null) { + ElementIterator it = DomUtil.getChildren(child, DEFAULTVALUE_ELEMENT, null); + while (it.hasNext()) { + String jcrVal = DomUtil.getText(it.nextElement()); + if (jcrVal == null) { + jcrVal = ""; + } + QValue qValue; + if (requiredType == PropertyType.BINARY) { + // TODO: improve + Value v = new ValueFactoryQImpl(qValueFactory, resolver).createValue(jcrVal, requiredType); + qValue = ValueFormat.getQValue(v, resolver, qValueFactory); + } else { + qValue = ValueFormat.getQValue(jcrVal, requiredType, resolver, qValueFactory); + } + builder.addDefaultValue(qValue); + } + } // else: no default values defined. + + child = DomUtil.getChildElement(pdefElement, VALUECONSTRAINTS_ELEMENT, null); + if (child != null) { + ElementIterator it = DomUtil.getChildren(child, VALUECONSTRAINT_ELEMENT, null); + while (it.hasNext()) { + String qValue = DomUtil.getText(it.nextElement()); + // in case of name and path constraint, the value must be + // converted to SPI values + // TODO: tobefixed. path-constraint may contain trailing * + builder.addValueConstraint(ValueConstraint.create(requiredType, qValue, resolver)); + } + } + + child = DomUtil.getChildElement(pdefElement, AVAILABLE_QUERY_OPERATORS_ELEMENT, null); + if (child == null) { + builder.setAvailableQueryOperators(new String[0]); + } else { + List names = new ArrayList(); + ElementIterator it = DomUtil.getChildren(child, AVAILABLE_QUERY_OPERATOR_ELEMENT, null); + while (it.hasNext()) { + String str = DomUtil.getText(it.nextElement()); + names.add(str); + } + builder.setAvailableQueryOperators(names.toArray(new String[names.size()])); + } + + return builder.build(); + } + + static QNodeTypeDefinition createQNodeTypeDefinition(Element ntdElement, NamePathResolver resolver, + QValueFactory qValueFactory) + throws RepositoryException { + QNodeTypeDefinitionBuilder builder = new QNodeTypeDefinitionBuilder(); + + // TODO: webdav-server currently sends jcr-names -> conversion needed + // NOTE: the server should send the namespace-mappings as addition ns-defininitions + try { + if (ntdElement.hasAttribute(NAME_ATTRIBUTE)) { + builder.setName(resolver.getQName(ntdElement.getAttribute(NAME_ATTRIBUTE))); + } + + if (ntdElement.hasAttribute(PRIMARYITEMNAME_ATTRIBUTE)) { + builder.setPrimaryItemName(resolver.getQName(ntdElement.getAttribute(PRIMARYITEMNAME_ATTRIBUTE))); + } + + Element child = DomUtil.getChildElement(ntdElement, SUPERTYPES_ELEMENT, null); + if (child != null) { + ElementIterator stIter = DomUtil.getChildren(child, SUPERTYPE_ELEMENT, null); + List qNames = new ArrayList(); + while (stIter.hasNext()) { + Name st = resolver.getQName(DomUtil.getTextTrim(stIter.nextElement())); + qNames.add(st); + } + builder.setSupertypes(qNames.toArray(new Name[qNames.size()])); + } + if (ntdElement.hasAttribute(ISMIXIN_ATTRIBUTE)) { + builder.setMixin(Boolean.valueOf(ntdElement.getAttribute(ISMIXIN_ATTRIBUTE))); + } + if (ntdElement.hasAttribute(HASORDERABLECHILDNODES_ATTRIBUTE)) { + builder.setOrderableChildNodes(Boolean.valueOf(ntdElement.getAttribute(HASORDERABLECHILDNODES_ATTRIBUTE))); + } + if (ntdElement.hasAttribute(ISABSTRACT_ATTRIBUTE)) { + builder.setAbstract(Boolean.valueOf(ntdElement.getAttribute(ISABSTRACT_ATTRIBUTE))); + } + if (ntdElement.hasAttribute(ISQUERYABLE_ATTRIBUTE)) { + builder.setQueryable(Boolean.valueOf(ntdElement.getAttribute(ISQUERYABLE_ATTRIBUTE))); + } + + // nodeDefinitions + ElementIterator it = DomUtil.getChildren(ntdElement, CHILDNODEDEFINITION_ELEMENT, null); + List nds = new ArrayList(); + while (it.hasNext()) { + nds.add(createQNodeDefinition(builder.getName(), it.nextElement(), resolver)); + } + builder.setChildNodeDefs(nds.toArray(new QNodeDefinition[nds.size()])); + + // propertyDefinitions + it = DomUtil.getChildren(ntdElement, PROPERTYDEFINITION_ELEMENT, null); + List pds = new ArrayList(); + while (it.hasNext()) { + pds.add(createQPropertyDefinition(builder.getName(), it.nextElement(), resolver, qValueFactory)); + } + builder.setPropertyDefs(pds.toArray(new QPropertyDefinition[pds.size()])); + } catch (NameException e) { + log.error(e.getMessage()); + throw new RepositoryException(e); + } + + return builder.build(); + } + + /** + * + * @param declaringNodeType + * @param itemDefElement + * @param resolver + * @param builder + * @throws RepositoryException + */ + private static void buildQItemDefinition(Name declaringNodeType, + Element itemDefElement, + NamePathResolver resolver, + QItemDefinitionBuilder builder) throws RepositoryException { + try { + String attr = itemDefElement.getAttribute(DECLARINGNODETYPE_ATTRIBUTE); + if (attr != null) { + Name dnt = resolver.getQName(attr); + if (declaringNodeType != null && !declaringNodeType.equals(dnt)) { + throw new RepositoryException("Declaring nodetype mismatch: In element = '" + dnt + "', Declaring nodetype = '" + declaringNodeType + "'"); + } + builder.setDeclaringNodeType(dnt); + } else { + builder.setDeclaringNodeType(declaringNodeType); + } + + if (itemDefElement.hasAttribute(NAME_ATTRIBUTE)) { + String nAttr = itemDefElement.getAttribute(NAME_ATTRIBUTE); + if (nAttr.length() > 0) { + builder.setName((isAnyName(nAttr)) ? NameConstants.ANY_NAME : resolver.getQName(nAttr)); + } else { + builder.setName(NameConstants.ROOT); + } + } else { + // TODO: check if correct.. + builder.setName(NameConstants.ANY_NAME); + } + } catch (NameException e) { + throw new RepositoryException(e); + } + + if (itemDefElement.hasAttribute(AUTOCREATED_ATTRIBUTE)) { + builder.setAutoCreated(Boolean.valueOf(itemDefElement.getAttribute(AUTOCREATED_ATTRIBUTE))); + } + if (itemDefElement.hasAttribute(MANDATORY_ATTRIBUTE)) { + builder.setMandatory(Boolean.valueOf(itemDefElement.getAttribute(MANDATORY_ATTRIBUTE))); + } + if (itemDefElement.hasAttribute(PROTECTED_ATTRIBUTE)) { + builder.setProtected(Boolean.valueOf(itemDefElement.getAttribute(PROTECTED_ATTRIBUTE))); + } + + if (itemDefElement.hasAttribute(ONPARENTVERSION_ATTRIBUTE)) { + builder.setOnParentVersion(OnParentVersionAction.valueFromName(itemDefElement.getAttribute(ONPARENTVERSION_ATTRIBUTE))); + } + } + + private static boolean isAnyName(String nameAttribute) { + return NameConstants.ANY_NAME.getLocalName().equals(nameAttribute); + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/DocumentTree.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/DocumentTree.java new file mode 100644 index 00000000000..7a698b3d5dc --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/DocumentTree.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.spi2dav; + +import java.util.ArrayList; +import java.util.List; +import javax.jcr.RepositoryException; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.tree.AbstractTree; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +class DocumentTree extends AbstractTree { + + private final List properties = new ArrayList(); + + protected DocumentTree(Name nodeName, Name ntName, String uniqueId, NamePathResolver resolver) { + super(nodeName, ntName, uniqueId, resolver); + } + + //-------------------------------------------------------< AbstractTree >--- + @Override + protected Tree createChild(Name name, Name primaryTypeName, String uniqueId) { + return new DocumentTree(name, primaryTypeName, uniqueId, getResolver()); + } + + //---------------------------------------------------------------< Tree >--- + @Override + public void addProperty(NodeId parentId, Name propertyName, int propertyType, QValue value) throws RepositoryException { + addProperty(parentId, propertyName, propertyType, new QValue[]{value}); + } + + @Override + public void addProperty(NodeId parentId, Name propertyName, int propertyType, QValue[] values) throws RepositoryException { + properties.add(new Property(propertyName, propertyType, values)); + } + + //-------------------------------------------------------------------------- + Document toDocument() throws RepositoryException { + try { + Document body = DomUtil.createDocument(); + buildNodeInfo(body, this); + return body; + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } + } + + //-------------------------------------------------------------------------- + private void buildNodeInfo(Node parent, DocumentTree tree) throws RepositoryException { + Element node = BatchUtils.createNodeElement(parent, tree.getName(), tree.getPrimaryTypeName(), tree.getUniqueId(), getResolver()); + for (Property prop : properties) { + BatchUtils.importProperty(node, prop.name, prop.type, prop.values, getResolver()); + } + for (Tree child : tree.getChildren()) { + buildNodeInfo(node, (DocumentTree) child); + } + } + + private final static class Property { + + private final Name name; + private final int type; + private final QValue[] values; + + private Property(Name name, int type, QValue[] values) { + this.name = name; + this.type = type; + this.values = values; + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/EventImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/EventImpl.java new file mode 100644 index 00000000000..820e21c6000 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/EventImpl.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.spi2dav; + +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * EventImpl... + */ +public class EventImpl + extends org.apache.jackrabbit.spi.commons.EventImpl + implements ObservationConstants { + + private static Logger log = LoggerFactory.getLogger(EventImpl.class); + + private static final NameFactory N_FACTORY = NameFactoryImpl.getInstance(); + + public EventImpl(ItemId eventId, Path eventPath, NodeId parentId, int eventType, String userId, + Element eventElement, NamePathResolver resolver, QValueFactory qvFactory) throws NamespaceException, + IllegalNameException { + super(getSpiEventType(eventType), eventPath, eventId, parentId, getNameSafe( + DomUtil.getChildTextTrim(eventElement, N_EVENTPRIMARYNODETYPE), resolver), getNames( + DomUtil.getChildren(eventElement, N_EVENTMIXINNODETYPE), resolver), userId, DomUtil.getChildTextTrim( + eventElement, N_EVENTUSERDATA), Long.parseLong(DomUtil.getChildTextTrim(eventElement, N_EVENTDATE)), + getEventInfo(DomUtil.getChildElement(eventElement, N_EVENTINFO), resolver, qvFactory)); + } + + //-------------------------------------------------------------------------- + private static int getSpiEventType(int jcrEventType) { + switch (jcrEventType) { + case javax.jcr.observation.Event.NODE_ADDED: + return Event.NODE_ADDED; + case javax.jcr.observation.Event.NODE_REMOVED: + return Event.NODE_REMOVED; + case javax.jcr.observation.Event.PROPERTY_ADDED: + return Event.PROPERTY_ADDED; + case javax.jcr.observation.Event.PROPERTY_CHANGED: + return Event.PROPERTY_CHANGED; + case javax.jcr.observation.Event.PROPERTY_REMOVED: + return Event.PROPERTY_REMOVED; + case javax.jcr.observation.Event.NODE_MOVED: + return Event.NODE_MOVED; + case javax.jcr.observation.Event.PERSIST: + return Event.PERSIST; + default: + throw new IllegalArgumentException("Invalid event type: " + jcrEventType); + } + } + + private static Map getEventInfo(Element infoElement, + NamePathResolver resolver, + QValueFactory qvFactory) { + if (infoElement == null) { + return Collections.emptyMap(); + } + + Map info = new HashMap(); + ElementIterator it = DomUtil.getChildren(infoElement); + while (it.hasNext()) { + Element el = it.nextElement(); + String uri = el.getNamespaceURI(); + if (uri == null) { + uri = Namespace.EMPTY_NAMESPACE.getURI(); + } + String localName = el.getLocalName(); + String value = DomUtil.getTextTrim(el); + try { + Name n = N_FACTORY.create(uri, localName); + QValue qv = null; + if (value != null) { + qv = ValueFormat.getQValue(value, PropertyType.PATH, resolver, qvFactory); + } + info.put(n, qv); + } catch (RepositoryException e) { + log.error("Internal Error: {}", e.getMessage()); + } + } + return info; + } + + private static Name getNameSafe(String name, NamePathResolver resolver) throws IllegalNameException, NamespaceException { + if (name == null) { + return null; + } + else { + return resolver.getQName(name); + } + } + + private static Name[] getNames(ElementIterator elements, + NamePathResolver resolver) { + + List results = Collections.emptyList(); + + while (elements.hasNext()) { + String rawname = DomUtil.getText(elements.nextElement()); + Name name = null; + + try { + name = resolver.getQName(rawname); + + if (results.size() == 0) { + results = Collections.singletonList(name); + } else if (results.size() == 1) { + results = new ArrayList(results); + results.add(name); + } else { + results.add(name); + } + } catch (Exception ex) { + log.error("Exception converting name " + rawname, ex); + } + } + + return results.toArray(new Name[results.size()]); + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/EventSubscriptionImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/EventSubscriptionImpl.java new file mode 100644 index 00000000000..66528bbe9b1 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/EventSubscriptionImpl.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.spi2dav; + +import org.apache.jackrabbit.spi.Subscription; + +/** + * EventSubscriptionImpl... + */ +public class EventSubscriptionImpl implements Subscription { + + /** + * The subscription id. + */ + private final String id; + + /** + * The session info that was used to create this subscription. + */ + private final SessionInfoImpl sessionInfo; + + public EventSubscriptionImpl(String id, SessionInfoImpl sessionInfo) { + this.id = id; + this.sessionInfo = sessionInfo; + } + + /** + * @return the subscription id. + */ + String getId() { + return id; + } + + /** + * @return the session info that was used to create this subscription. + */ + SessionInfoImpl getSessionInfo() { + return sessionInfo; + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ExceptionConverter.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ExceptionConverter.java new file mode 100644 index 00000000000..e0346c7bebb --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ExceptionConverter.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.spi2dav; + +import java.lang.reflect.Constructor; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import javax.jcr.nodetype.ConstraintViolationException; + +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Element; + +/** + * ExceptionConverter... + */ +public class ExceptionConverter { + + // avoid instantiation + private ExceptionConverter() {} + + public static RepositoryException generate(DavException davExc) { + return generate(davExc, null); + } + + public static RepositoryException generate(DavException davExc, HttpRequestBase request) { + String name = (request == null) ? "_undefined_" : request.getMethod(); + int code = DavMethods.getMethodCode(name); + return generate(davExc, code, name); + } + + public static RepositoryException generate(DavException davExc, int methodCode, String name) { + String msg = davExc.getMessage(); + if (davExc.hasErrorCondition()) { + try { + Element error = davExc.toXml(DomUtil.createDocument()); + if (DomUtil.matches(error, DavException.XML_ERROR, DavConstants.NAMESPACE)) { + if (DomUtil.hasChildElement(error, "exception", null)) { + Element exc = DomUtil.getChildElement(error, "exception", null); + if (DomUtil.hasChildElement(exc, "message", null)) { + msg = DomUtil.getChildText(exc, "message", null); + } + if (DomUtil.hasChildElement(exc, "class", null)) { + Class cl = Class.forName(DomUtil.getChildText(exc, "class", null)); + Constructor excConstr = cl.getConstructor(String.class); + if (excConstr != null) { + Object o = excConstr.newInstance(msg); + if (o instanceof PathNotFoundException && methodCode == DavMethods.DAV_POST) { + // see JCR-2536 + return new InvalidItemStateException(msg); + } else if (o instanceof RepositoryException) { + return (RepositoryException) o; + } else if (o instanceof Exception) { + return new RepositoryException(msg, (Exception)o); + } + } + } + } + } + } catch (Exception e) { + return new RepositoryException(e); + } + } + + // make sure an exception is generated + switch (davExc.getErrorCode()) { + // TODO: mapping DAV_error to jcr-exception is ambiguous. to be improved + case DavServletResponse.SC_NOT_FOUND : + switch (methodCode) { + case DavMethods.DAV_DELETE: + case DavMethods.DAV_MKCOL: + case DavMethods.DAV_PUT: + case DavMethods.DAV_POST: + // target item has probably while transient changes have + // been made. + return new InvalidItemStateException(msg, davExc); + default: + return new ItemNotFoundException(msg, davExc); + } + case DavServletResponse.SC_LOCKED : + return new LockException(msg, davExc); + case DavServletResponse.SC_METHOD_NOT_ALLOWED : + return new ConstraintViolationException(msg, davExc); + case DavServletResponse.SC_CONFLICT : + return new InvalidItemStateException(msg, davExc); + case DavServletResponse.SC_PRECONDITION_FAILED : + return new LockException(msg, davExc); + case DavServletResponse.SC_NOT_IMPLEMENTED: + if (methodCode > 0 && name != null) { + return new UnsupportedRepositoryOperationException( + "Missing implementation: Method " + + name + " could not be executed", davExc); + } else { + return new UnsupportedRepositoryOperationException( + "Missing implementation", davExc); + } + default: + return new RepositoryException(msg, davExc); + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/IdURICache.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/IdURICache.java new file mode 100644 index 00000000000..a489fa780ec --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/IdURICache.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.spi2dav; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.jackrabbit.spi.ItemId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * IdURICache... + */ +class IdURICache { + private static Logger log = LoggerFactory.getLogger(IdURICache.class); + + /** + * @see JCR-3305: limit cache size + */ + private static final int CACHESIZE = 10000; + + private final String workspaceUri; + private Map idToUriCache; + private Map uriToIdCache; + + IdURICache(String workspaceUri) { + this.workspaceUri = workspaceUri; + idToUriCache = new LinkedHashMap(CACHESIZE, 1) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return this.size() > CACHESIZE; + } + }; + uriToIdCache = new LinkedHashMap(CACHESIZE, 1) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return this.size() > CACHESIZE; + } + }; + } + + public ItemId getItemId(String uri) { + return uriToIdCache.get(getCleanUri(uri)); + } + + public String getUri(ItemId itemId) { + return idToUriCache.get(itemId); + } + + public boolean containsUri(String uri) { + return uriToIdCache.containsKey(getCleanUri(uri)); + } + + public boolean containsItemId(ItemId itemId) { + return idToUriCache.containsKey(itemId); + } + + public void add(String uri, ItemId itemId) { + if (!uri.startsWith(workspaceUri)) { + throw new IllegalArgumentException("Workspace mismatch: '" + uri + "' not under '" + workspaceUri + "'"); + } + String cleanUri = getCleanUri(uri); + uriToIdCache.put(cleanUri, itemId); + idToUriCache.put(itemId, cleanUri); + log.debug("Added: ItemId = " + itemId + " URI = " + cleanUri); + } + + public void remove(String uri) { + String cleanUri = getCleanUri(uri); + ItemId itemId = uriToIdCache.remove(cleanUri); + if (itemId != null) { + idToUriCache.remove(itemId); + } + log.debug("Removed: ItemId = " + itemId + " URI = " + cleanUri); + } + + public void remove(ItemId itemId) { + String uri = idToUriCache.remove(itemId); + if (uri != null) { + uriToIdCache.remove(uri); + } + log.debug("Removed: ItemId = " + itemId + " URI = " + uri); + } + + public void clear() { + idToUriCache.clear(); + uriToIdCache.clear(); + } + + private static String getCleanUri(String uri) { + if (uri.endsWith("/")) { + return uri.substring(0, uri.length() - 1); + } else { + return uri; + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ItemInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ItemInfoImpl.java new file mode 100644 index 00000000000..956fd00bf20 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ItemInfoImpl.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.spi2dav; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertySet; + +import javax.jcr.NamespaceException; + +/** + * ItemInfoImpl... + */ +abstract class ItemInfoImpl implements ItemInfo { + + private final Path path; + + ItemInfoImpl(Path path) { + this.path = path; + } + + ItemInfoImpl(DavPropertySet propSet, NamePathResolver resolver) + throws NameException, NamespaceException { + + DavProperty pathProp = propSet.get(JcrRemotingConstants.JCR_PATH_LN, ItemResourceConstants.NAMESPACE); + String jcrPath = pathProp.getValue().toString(); + path = resolver.getQPath(jcrPath); + } + + /** + * @see ItemInfo#getPath() + */ + public Path getPath() { + return path; + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ItemResourceConstants.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ItemResourceConstants.java new file mode 100644 index 00000000000..a21a4ea3e5d --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/ItemResourceConstants.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.spi2dav; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * ItemResourceConstants provides constants for any resources + * representing repository items. + */ +public interface ItemResourceConstants extends JcrRemotingConstants { + + /** + * The namespace for all jcr specific extensions. + */ + public static final Namespace NAMESPACE = Namespace.getNamespace(NS_PREFIX, NS_URI); + + /** + * Extension to the WebDAV 'exclusive' lock, that allows to distinguish + * the session-scoped and open-scoped locks on a JCR node. + * + * @see javax.jcr.Node#lock(boolean, boolean) + */ + public static final Scope EXCLUSIVE_SESSION = Scope.create(XML_EXCLUSIVE_SESSION_SCOPED, NAMESPACE); +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/LockInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/LockInfoImpl.java new file mode 100644 index 00000000000..c9a5469862f --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/LockInfoImpl.java @@ -0,0 +1,82 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. The ASF licenses this file to You +* under the Apache License, Version 2.0 (the "License"); you may not +* use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package org.apache.jackrabbit.spi2dav; + +import java.util.Set; + +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * LockInfoImpl... + */ +public class LockInfoImpl implements LockInfo { + + private static Logger log = LoggerFactory.getLogger(LockInfoImpl.class); + + private final ActiveLock activeLock; + private final NodeId nodeId; + private final Set sessionLockTokens; + + public LockInfoImpl(ActiveLock activeLock, NodeId nodeId, Set sessionLockTokens) { + this.activeLock = activeLock; + this.nodeId = nodeId; + this.sessionLockTokens = sessionLockTokens; + } + + ActiveLock getActiveLock() { + return activeLock; + } + + //-----------------------------------------------------------< LockInfo >--- + public String getLockToken() { + return (isSessionScoped()) ? null : activeLock.getToken(); + } + + public String getOwner() { + return activeLock.getOwner(); + } + + public boolean isDeep() { + return activeLock.isDeep(); + } + + public boolean isSessionScoped() { + return ItemResourceConstants.EXCLUSIVE_SESSION.equals(activeLock.getScope()); + } + + public long getSecondsRemaining() { + long timeout = activeLock.getTimeout(); + return (timeout == DavConstants.INFINITE_TIMEOUT) ? Long.MAX_VALUE : timeout / 1000; + } + + public boolean isLockOwner() { + String lt = activeLock.getToken(); + if (lt == null) { + return false; + } else { + return sessionLockTokens.contains(lt); + } + } + + public NodeId getNodeId() { + return nodeId; + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/NamespaceResolverImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/NamespaceResolverImpl.java new file mode 100644 index 00000000000..8b32d1ab8ea --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/NamespaceResolverImpl.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.spi2dav; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.NamespaceException; +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; + +/** + * NamespaceResolverImpl... + */ +class NamespaceResolverImpl extends AbstractNamespaceResolver { + + private static Logger log = LoggerFactory.getLogger(NamespaceResolverImpl.class); + + // TODO: TO_BE_FIXED. missing notification and subsequent reloading of namespaces causes this resolver to throw NameException + + private Map prefixToURI = new HashMap(); + private Map uriToPrefix = new HashMap(); + + void add(String prefix, String uri) { + prefixToURI.put(prefix, uri); + uriToPrefix.put(uri, prefix); + } + + void remove(String prefix, String uri) { + prefixToURI.remove(prefix); + uriToPrefix.remove(uri); + } + + Map getNamespaces() { + return Collections.unmodifiableMap(prefixToURI); + } + + //--------------------------------------------------< NamespaceResolver >--- + /** + * @see NamespaceResolver#getURI(String) + */ + public String getURI(String prefix) throws NamespaceException { + String uri = prefixToURI.get(prefix); + if (uri == null) { + throw new NamespaceException(prefix + ": is not a registered namespace prefix."); + } + return uri; + } + + /** + * @see org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver#getPrefix(String) + */ + public String getPrefix(String uri) throws NamespaceException { + String prefix = uriToPrefix.get(uri); + if (prefix == null) { + throw new NamespaceException(uri + ": is not a registered namespace uri."); + } + return prefix; + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/NodeInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/NodeInfoImpl.java new file mode 100644 index 00000000000..fce657c9e4f --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/NodeInfoImpl.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.spi2dav; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.commons.webdav.NodeTypeUtil; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.ChildInfo; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import javax.jcr.RepositoryException; +import java.util.Collection; +import java.util.List; +import java.util.Iterator; +import java.util.ArrayList; + +/** + * NodeInfoImpl... + */ +public class NodeInfoImpl extends ItemInfoImpl implements NodeInfo { + + private static Logger log = LoggerFactory.getLogger(NodeInfoImpl.class); + + private final NodeId id; + private final int index; + + private final Name primaryNodeTypeName; + private final Name[] mixinNodeTypeNames; + + private final List references = new ArrayList(); + private final List propertyIds = new ArrayList(); + private List childInfos = null; + + public NodeInfoImpl(NodeId id, DavPropertySet propSet, + NamePathResolver resolver) throws RepositoryException, NameException { + super(propSet, resolver); + + // set id + this.id = id; + + DavProperty indexProp = propSet.get(JcrRemotingConstants.JCR_INDEX_LN, ItemResourceConstants.NAMESPACE); + if (indexProp != null && indexProp.getValue() != null) { + index = Integer.parseInt(indexProp.getValue().toString()); + } else { + index = Path.INDEX_DEFAULT; + } + + // retrieve properties + try { + DavProperty prop = propSet.get(JcrRemotingConstants.JCR_PRIMARYNODETYPE_LN, ItemResourceConstants.NAMESPACE); + if (prop != null) { + Iterator it = NodeTypeUtil.ntNamesFromXml(prop.getValue()).iterator(); + if (it.hasNext()) { + String jcrName = it.next(); + primaryNodeTypeName = resolver.getQName(jcrName); + } else { + throw new RepositoryException("Missing primary nodetype for node " + id + "."); + } + } else { + throw new RepositoryException("Missing primary nodetype for node " + id); + } + + prop = propSet.get(JcrRemotingConstants.JCR_MIXINNODETYPES_LN, ItemResourceConstants.NAMESPACE); + if (prop != null) { + Collection mixinNames = NodeTypeUtil.ntNamesFromXml(prop.getValue()); + mixinNodeTypeNames = new Name[mixinNames.size()]; + int i = 0; + for (String jcrName : mixinNames) { + mixinNodeTypeNames[i] = resolver.getQName(jcrName); + i++; + } + } else { + mixinNodeTypeNames = Name.EMPTY_ARRAY; + } + } catch (NameException e) { + throw new RepositoryException("Error while resolving nodetype names: " + e.getMessage()); + } + } + + //-----------------------------------------------------------< ItemInfo >--- + public boolean denotesNode() { + return true; + } + + //-----------------------------------------------------------< NodeInfo >--- + public NodeId getId() { + return id; + } + + public int getIndex() { + return index; + } + + public Name getNodetype() { + return primaryNodeTypeName; + } + + public Name[] getMixins() { + return mixinNodeTypeNames; + } + + public PropertyId[] getReferences() { + return references.toArray(new PropertyId[references.size()]); + } + + public Iterator getPropertyIds() { + return propertyIds.iterator(); + } + + public Iterator getChildInfos() { + return (childInfos == null) ? null : childInfos.iterator(); + } + + //-------------------------------------------------------------------------- + void addReference(PropertyId referenceId) { + references.add(referenceId); + } + + void addPropertyId(PropertyId childId) { + propertyIds.add(childId); + } + + void addChildInfo(ChildInfo childInfo) { + if (childInfos == null) { + childInfos = new ArrayList(); + } + childInfos.add(childInfo); + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/PropertyInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/PropertyInfoImpl.java new file mode 100644 index 00000000000..1ac7238fe19 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/PropertyInfoImpl.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.spi2dav; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.commons.webdav.ValueUtil; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertySet; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import java.io.IOException; + +/** + * PropertyInfoImpl... + */ +public class PropertyInfoImpl extends ItemInfoImpl implements PropertyInfo { + + private final PropertyId id; + + private int type; + private boolean isMultiValued; + private QValue[] values; + + public PropertyInfoImpl(PropertyId id, Path path, int type, + boolean isMultiValued, QValue[] values) { + super(path); + this.id = id; + this.type = type; + this.isMultiValued = isMultiValued; + this.values = values; + } + + public PropertyInfoImpl(PropertyId id, DavPropertySet propSet, + NamePathResolver resolver, ValueFactory valueFactory, + QValueFactory qValueFactory) + throws RepositoryException, IOException, NameException { + + super(propSet, resolver); + // set id + this.id = id; + + // retrieve properties + String typeName = propSet.get(JcrRemotingConstants.JCR_TYPE_LN, ItemResourceConstants.NAMESPACE).getValue().toString(); + type = PropertyType.valueFromName(typeName); + + // values from jcr-server must be converted to SPI values. + DavProperty prop = propSet.get(JcrRemotingConstants.JCR_VALUE_LN, ItemResourceConstants.NAMESPACE); + if (prop != null) { + Value[] jcrValues = ValueUtil.valuesFromXml(prop.getValue(), type, valueFactory); + if (jcrValues == null || jcrValues.length == 0) { + // TODO: should never occur. since 'null' single values are not allowed. rather throw? + values = QValue.EMPTY_ARRAY; + } else { + QValue qv; + if (type == PropertyType.BINARY) { + qv = qValueFactory.create(jcrValues[0].getStream()); + } else { + qv = ValueFormat.getQValue(jcrValues[0], resolver, qValueFactory); + } + values = new QValue[] {qv}; + } + } else { + isMultiValued = true; + prop = propSet.get(JcrRemotingConstants.JCR_VALUES_LN, ItemResourceConstants.NAMESPACE); + if (prop == null) { + throw new RepositoryException("Item with id " + id.toString() + " doesn't represent a valid property."); + } + Value[] jcrValues = ValueUtil.valuesFromXml(prop.getValue(), type, valueFactory); + values = new QValue[jcrValues.length]; + for (int i = 0; i < jcrValues.length; i++) { + if (type == PropertyType.BINARY) { + values[i] = qValueFactory.create(jcrValues[i].getStream()); + } else { + values[i] = ValueFormat.getQValue(jcrValues[i], resolver, qValueFactory); + } + } + } + } + + //-----------------------------------------------------------< ItemInfo >--- + public boolean denotesNode() { + return false; + } + + //-------------------------------------------------------< PropertyInfo >--- + public PropertyId getId() { + return id; + } + + public int getType() { + return type; + } + + public boolean isMultiValued() { + return isMultiValued; + } + + public QValue[] getValues() { + return values; + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/QueryInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/QueryInfoImpl.java new file mode 100644 index 00000000000..53cd48f6fed --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/QueryInfoImpl.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.spi2dav; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.ValueFactory; +import javax.jcr.RangeIterator; + +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QueryResultRow; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.util.ISO9075; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; + +/** + * QueryInfoImpl... + */ +public class QueryInfoImpl implements QueryInfo { + + private static final String COLUMNS = "Columns: "; + + private static final String SELECTORS = "Selectors: "; + + private final List columnNames = new ArrayList(); + + private final List selectorNames = new ArrayList(); + + private final List results = new ArrayList(); + + public QueryInfoImpl(MultiStatus ms, IdFactory idFactory, + NamePathResolver resolver, ValueFactory valueFactory, + QValueFactory qValueFactory) + throws RepositoryException { + + String responseDescription = ms.getResponseDescription(); + if (responseDescription == null) { + throw new RepositoryException( + "Missing column infos: Unable to build QueryInfo object."); + } + if (responseDescription.startsWith(COLUMNS)) { + for (String line : responseDescription.split("\n")) { + if (line.startsWith(COLUMNS)) { + decode(line.substring(COLUMNS.length()), columnNames); + } else if (line.startsWith(SELECTORS)) { + decode(line.substring(SELECTORS.length()), selectorNames); + } + } + } else { + // Backwards compatibility with old servers that only provide + // the list of columns as the response description + decode(responseDescription, columnNames); + } + + for (MultiStatusResponse response : ms.getResponses()) { + results.add(new QueryResultRowImpl( + response, getColumnNames(), resolver, + qValueFactory, valueFactory, idFactory)); + } + } + + /** + * Splits the given string at spaces and ISO9075-decodes the parts. + * + * @param string source string + * @param list where the decoded parts get added + */ + private void decode(String string, List list) { + String[] parts = string.split(" "); + for (int i = 0; i < parts.length; i++) { + list.add(ISO9075.decode(parts[i])); + } + } + + /** + * @see QueryInfo#getRows() + */ + public RangeIterator getRows() { + return new RangeIteratorAdapter(results); + } + + /** + * @see QueryInfo#getColumnNames() + */ + public String[] getColumnNames() { + return columnNames.toArray(new String[columnNames.size()]); + } + + /** + * @see QueryInfo#getSelectorNames() + */ + public String[] getSelectorNames() { + return selectorNames.toArray(new String[selectorNames.size()]); + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/QueryResultRowImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/QueryResultRowImpl.java new file mode 100644 index 00000000000..e83112d1dc7 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/QueryResultRowImpl.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.spi2dav; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.commons.webdav.QueryUtil; +import org.apache.jackrabbit.spi.QueryResultRow; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +/** + * QueryResultRowImpl implements a QueryResultRow that is + * initialized from a multistatus response. + */ +public class QueryResultRowImpl implements QueryResultRow { + + private static final Logger log = LoggerFactory.getLogger(QueryResultRowImpl.class); + + private static final DavPropertyName SEARCH_RESULT_PROPERTY = DavPropertyName.create(JcrRemotingConstants.JCR_QUERY_RESULT_LN, ItemResourceConstants.NAMESPACE); + + private final Map nodeIds = new HashMap(); + + private final Map scores = new HashMap(); + + private final Map qValues = new HashMap(); + + private final String[] columnNames; + + public QueryResultRowImpl(MultiStatusResponse response, + String[] columnNames, + NamePathResolver resolver, + QValueFactory qValueFactory, + ValueFactory valueFactory, + IdFactory idFactory) + throws RepositoryException { + this.columnNames = columnNames; + + DavPropertySet okSet = response.getProperties(DavServletResponse.SC_OK); + + String jcrPath = resolver.getJCRName(NameConstants.JCR_PATH); + String jcrScore = resolver.getJCRName(NameConstants.JCR_SCORE); + DavProperty davProp = okSet.get(SEARCH_RESULT_PROPERTY); + + List colList = new ArrayList(); + List selList = new ArrayList(); + List valList = new ArrayList(); + QueryUtil.parseResultPropertyValue(davProp.getValue(), colList, selList, valList, valueFactory); + + String[] names = colList.toArray(new String[colList.size()]); + Value[] values = valList.toArray(new Value[valList.size()]); + + for (int i = 0; i < values.length; i++) { + try { + String selectorName = selList.get(i); + QValue v = (values[i] == null) ? null : ValueFormat.getQValue(values[i], resolver, qValueFactory); + if (jcrScore.equals(names[i])) { + Double score = 0.0; + if (v != null) { + score = v.getDouble(); + } + scores.put(selectorName, score); + } else if (jcrPath.equals(names[i])) { + NodeId id = null; + if (v != null) { + id = idFactory.createNodeId((String) null, v.getPath()); + } + nodeIds.put(selectorName, id); + } + qValues.put(names[i], v); + } catch (RepositoryException e) { + // should not occur + log.error("Malformed value: " + values[i].toString()); + } + } + } + + public NodeId getNodeId(String selectorName) { + if (selectorName == null && scores.size() == 1) { + return nodeIds.values().iterator().next(); + } + + NodeId id = nodeIds.get(selectorName); + if (id == null && !nodeIds.containsKey(selectorName)) { + throw new IllegalArgumentException(selectorName + " is not a valid selectorName"); + } + return id; + } + + public double getScore(String selectorName) { + if (selectorName == null && scores.size() == 1) { + return scores.values().iterator().next(); + } + + Double score = scores.get(selectorName); + if (score == null && !nodeIds.containsKey(selectorName)) { + throw new IllegalArgumentException(selectorName + " is not a valid selectorName"); + } + return score; + } + + public QValue[] getValues() { + QValue[] values = new QValue[columnNames.length]; + for (int i = 0; i < columnNames.length; i++) { + values[i] = qValues.get(columnNames[i]); + } + return values; + } + +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/RepositoryServiceImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/RepositoryServiceImpl.java new file mode 100644 index 00000000000..4ab0ab3a4e5 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/RepositoryServiceImpl.java @@ -0,0 +1,3513 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2dav; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +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.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.jcr.AccessDeniedException; +import javax.jcr.Credentials; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.LoginException; +import javax.jcr.NamespaceException; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.lock.LockException; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.AuthCache; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.jackrabbit.commons.webdav.AtomFeedConstants; +import org.apache.jackrabbit.commons.webdav.EventUtil; +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.commons.webdav.JcrValueType; +import org.apache.jackrabbit.commons.webdav.NodeTypeConstants; +import org.apache.jackrabbit.commons.webdav.NodeTypeUtil; +import org.apache.jackrabbit.commons.webdav.ValueUtil; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.ChildInfoImpl; +import org.apache.jackrabbit.spi.commons.EventBundleImpl; +import org.apache.jackrabbit.spi.commons.EventFilterImpl; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi.commons.conversion.IdentifierResolver; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingNameResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingPathResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.nodetype.compact.CompactNodeTypeDefWriter; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.value.QValueValue; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.BaseDavRequest; +import org.apache.jackrabbit.webdav.client.methods.HttpCheckin; +import org.apache.jackrabbit.webdav.client.methods.HttpCheckout; +import org.apache.jackrabbit.webdav.client.methods.HttpCopy; +import org.apache.jackrabbit.webdav.client.methods.HttpDelete; +import org.apache.jackrabbit.webdav.client.methods.HttpLabel; +import org.apache.jackrabbit.webdav.client.methods.HttpLock; +import org.apache.jackrabbit.webdav.client.methods.HttpMerge; +import org.apache.jackrabbit.webdav.client.methods.HttpMkcol; +import org.apache.jackrabbit.webdav.client.methods.HttpMkworkspace; +import org.apache.jackrabbit.webdav.client.methods.HttpMove; +import org.apache.jackrabbit.webdav.client.methods.HttpOptions; +import org.apache.jackrabbit.webdav.client.methods.HttpOrderpatch; +import org.apache.jackrabbit.webdav.client.methods.HttpPoll; +import org.apache.jackrabbit.webdav.client.methods.HttpPropfind; +import org.apache.jackrabbit.webdav.client.methods.HttpProppatch; +import org.apache.jackrabbit.webdav.client.methods.HttpReport; +import org.apache.jackrabbit.webdav.client.methods.HttpSearch; +import org.apache.jackrabbit.webdav.client.methods.HttpSubscribe; +import org.apache.jackrabbit.webdav.client.methods.HttpUnlock; +import org.apache.jackrabbit.webdav.client.methods.HttpUnsubscribe; +import org.apache.jackrabbit.webdav.client.methods.HttpUpdate; +import org.apache.jackrabbit.webdav.client.methods.XmlEntity; +import org.apache.jackrabbit.webdav.header.CodedUrlHeader; +import org.apache.jackrabbit.webdav.header.IfHeader; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockDiscovery; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.observation.DefaultEventType; +import org.apache.jackrabbit.webdav.observation.EventDiscovery; +import org.apache.jackrabbit.webdav.observation.EventType; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.observation.SubscriptionInfo; +import org.apache.jackrabbit.webdav.ordering.OrderPatch; +import org.apache.jackrabbit.webdav.ordering.OrderingConstants; +import org.apache.jackrabbit.webdav.ordering.Position; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.search.SearchInfo; +import org.apache.jackrabbit.webdav.security.CurrentUserPrivilegeSetProperty; +import org.apache.jackrabbit.webdav.security.Privilege; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.security.SupportedPrivilege; +import org.apache.jackrabbit.webdav.security.SupportedPrivilegeSetProperty; +import org.apache.jackrabbit.webdav.transaction.TransactionConstants; +import org.apache.jackrabbit.webdav.transaction.TransactionInfo; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.apache.jackrabbit.webdav.version.LabelInfo; +import org.apache.jackrabbit.webdav.version.MergeInfo; +import org.apache.jackrabbit.webdav.version.UpdateInfo; +import org.apache.jackrabbit.webdav.version.VersionControlledResource; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * RepositoryServiceImpl... + */ +// TODO: encapsulate URI building, escaping, unescaping... +// TODO: TO-BE-FIXED. caches don't get adjusted upon removal/move of items +public class RepositoryServiceImpl implements RepositoryService, DavConstants { + + private static Logger log = LoggerFactory.getLogger(RepositoryServiceImpl.class); + + private static final SubscriptionInfo S_INFO = new SubscriptionInfo(DefaultEventType.create(EventUtil.EVENT_ALL, ItemResourceConstants.NAMESPACE), true, INFINITE_TIMEOUT); + + /** + * Key for the client map during repo creation (no sessionInfo present) + */ + private static final String CLIENT_KEY = "repoCreation"; + + /** + * Default value for the maximum number of connections per host such as + * configured with {@link PoolingHttpClientConnectionManager#setDefaultMaxPerRoute(int)}. + */ + public static final int MAX_CONNECTIONS_DEFAULT = 20; + + private final IdFactory idFactory; + private final NameFactory nameFactory; + private final PathFactory pathFactory; + private final QValueFactory qValueFactory; + private final ValueFactory valueFactory; + + private final int itemInfoCacheSize; + + private final NamespaceCache nsCache; + private final URIResolverImpl uriResolver; + + private final HttpHost httpHost; + private final ConcurrentMap clients; + private final HttpClientBuilder httpClientBuilder; + + private final Map nodeTypeDefinitions = new HashMap(); + + /** Repository descriptors. */ + private final Map descriptors = + new HashMap(); + + /** Observation features. */ + private boolean remoteServerProvidesNodeTypes = false; + private boolean remoteServerProvidesNoLocalFlag = false; + + /* DAV conformance levels */ + private Set remoteDavComplianceClasses = null; + + /** + * Same as {@link #RepositoryServiceImpl(String, IdFactory, NameFactory, PathFactory, QValueFactory, int, int)} + * using {@link ItemInfoCacheImpl#DEFAULT_CACHE_SIZE} as size for the item + * cache and {@link #MAX_CONNECTIONS_DEFAULT} for the maximum number of + * connections on the client. + * + * @param uri The server uri. + * @param idFactory The id factory. + * @param nameFactory The name factory. + * @param pathFactory The path factory. + * @param qValueFactory The value factory. + * @throws RepositoryException If an error occurs. + */ + public RepositoryServiceImpl(String uri, IdFactory idFactory, + NameFactory nameFactory, PathFactory pathFactory, + QValueFactory qValueFactory) throws RepositoryException { + this(uri, idFactory, nameFactory, pathFactory, qValueFactory, ItemInfoCacheImpl.DEFAULT_CACHE_SIZE); + } + + /** + * Same as {@link #RepositoryServiceImpl(String, IdFactory, NameFactory, PathFactory, QValueFactory, int, int)} + * using {@link #MAX_CONNECTIONS_DEFAULT} for the maximum number of + * connections on the client. + * + * @param uri The server uri. + * @param idFactory The id factory. + * @param nameFactory The name factory. + * @param pathFactory The path factory. + * @param qValueFactory The value factory. + * @param itemInfoCacheSize The size of the item info cache. + * @throws RepositoryException If an error occurs. + */ + public RepositoryServiceImpl(String uri, IdFactory idFactory, + NameFactory nameFactory, PathFactory pathFactory, + QValueFactory qValueFactory, int itemInfoCacheSize) throws RepositoryException { + this(uri, idFactory, nameFactory, pathFactory, qValueFactory, itemInfoCacheSize, MAX_CONNECTIONS_DEFAULT); + } + + /** + * Creates a new instance of this repository service. + * + * @param uri The server uri. + * @param idFactory The id factory. + * @param nameFactory The name factory. + * @param pathFactory The path factory. + * @param qValueFactory The value factory. + * @param itemInfoCacheSize The size of the item info cache. + * @param maximumHttpConnections A int >0 defining the maximum number of + * connections per host to be configured on + * {@link PoolingHttpClientConnectionManager#setMaxTotal(int)}. + * @throws RepositoryException If an error occurs. + */ + public RepositoryServiceImpl(String uri, IdFactory idFactory, + NameFactory nameFactory, PathFactory pathFactory, + QValueFactory qValueFactory, int itemInfoCacheSize, + int maximumHttpConnections ) throws RepositoryException { + if (uri == null || "".equals(uri)) { + throw new RepositoryException("Invalid repository uri '" + uri + "'."); + } + + if (idFactory == null || qValueFactory == null) { + throw new RepositoryException("IdFactory and QValueFactory may not be null."); + } + this.idFactory = idFactory; + this.nameFactory = nameFactory; + this.pathFactory = pathFactory; + this.qValueFactory = qValueFactory; + this.itemInfoCacheSize = itemInfoCacheSize; + + try { + URI repositoryUri = computeRepositoryUri(uri); + httpHost = new HttpHost(repositoryUri.getHost(), repositoryUri.getPort()); + + nsCache = new NamespaceCache(); + uriResolver = new URIResolverImpl(repositoryUri, this, DomUtil.createDocument()); + NamePathResolver resolver = new NamePathResolverImpl(nsCache); + valueFactory = new ValueFactoryQImpl(qValueFactory, resolver); + + } catch (URISyntaxException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } + + PoolingHttpClientConnectionManager cmgr = new PoolingHttpClientConnectionManager(); + if (maximumHttpConnections > 0) { + cmgr.setDefaultMaxPerRoute(maximumHttpConnections); + cmgr.setMaxTotal(maximumHttpConnections); + } + HttpClientBuilder hcb = HttpClients.custom().setConnectionManager(cmgr); + if (Boolean.getBoolean("jackrabbit.client.useSystemProperties")) { + // support Java system proxy? (JCR-3211) + hcb = hcb.useSystemProperties(); + } + httpClientBuilder = hcb; + + // This configuration of the clients cache assumes that the level of + // concurrency on this map will be equal to the default number of maximum + // connections allowed on the httpClient level. + // TODO: review again + int concurrencyLevel = MAX_CONNECTIONS_DEFAULT; + int initialCapacity = MAX_CONNECTIONS_DEFAULT; + if (maximumHttpConnections > 0) { + concurrencyLevel = maximumHttpConnections; + initialCapacity = maximumHttpConnections; + } + clients = new ConcurrentHashMap(concurrencyLevel, .75f, initialCapacity); + } + + private static void checkSessionInfo(SessionInfo sessionInfo) throws RepositoryException { + if (!(sessionInfo instanceof SessionInfoImpl)) { + throw new RepositoryException("Unknown SessionInfo implementation."); + } + } + + /** + * Resolve the given URI against a base URI (usually the request URI of an HTTP request) + */ + private static String resolve(String baseUri, String relUri) throws RepositoryException { + try { + java.net.URI base = new java.net.URI(baseUri); + java.net.URI rel = new java.net.URI(relUri); + return base.resolve(rel).toString(); + } catch (URISyntaxException ex) { + throw new RepositoryException(ex); + } + } + + private static void checkSubscription(Subscription subscription) throws RepositoryException { + if (!(subscription instanceof EventSubscriptionImpl)) { + throw new RepositoryException("Unknown Subscription implementation."); + } + } + + private static boolean isUnLockMethod(HttpUriRequest request) { + int code = DavMethods.getMethodCode(request.getMethod()); + return DavMethods.DAV_UNLOCK == code; + } + + protected static void initMethod(HttpUriRequest request, SessionInfo sessionInfo, boolean addIfHeader) throws RepositoryException { + if (addIfHeader) { + checkSessionInfo(sessionInfo); + Set allLockTokens = ((SessionInfoImpl) sessionInfo).getAllLockTokens(); + // TODO: ev. build tagged if header + if (!allLockTokens.isEmpty()) { + String[] locktokens = allLockTokens.toArray(new String[allLockTokens.size()]); + IfHeader ifH = new IfHeader(locktokens); + request.setHeader(ifH.getHeaderName(), ifH.getHeaderValue()); + } + } + + initMethod(request, sessionInfo); + } + + // set of HTTP methods that will not change the remote state + private static final Set readMethods; + static { + Set tmp = new HashSet(); + tmp.add("GET"); + tmp.add("HEAD"); + tmp.add("PROPFIND"); + tmp.add("POLL"); + tmp.add("REPORT"); + tmp.add("SEARCH"); + readMethods = Collections.unmodifiableSet(tmp); + } + + // set headers for user data and session identification + protected static void initMethod(HttpUriRequest request, SessionInfo sessionInfo) throws RepositoryException { + + boolean isReadAccess = readMethods.contains(request.getMethod()); + boolean needsSessionId = !isReadAccess || "POLL".equals(request.getMethod()); + + if (sessionInfo instanceof SessionInfoImpl && needsSessionId) { + request.addHeader("Link", generateLinkHeaderFieldValue(sessionInfo, isReadAccess)); + } + } + + private static String generateLinkHeaderFieldValue(SessionInfo sessionInfo, boolean isReadAccess) { + StringBuilder linkHeaderField = new StringBuilder(); + + String sessionIdentifier = ((SessionInfoImpl) sessionInfo).getSessionIdentifier(); + linkHeaderField.append("<").append(sessionIdentifier).append(">; rel=\"") + .append(JcrRemotingConstants.RELATION_REMOTE_SESSION_ID).append("\""); + + String userdata = ((SessionInfoImpl) sessionInfo).getUserData(); + if (userdata != null && !isReadAccess) { + String escaped = Text.escape(userdata); + linkHeaderField.append(", ; rel=\"").append(JcrRemotingConstants.RELATION_USER_DATA) + .append("\""); + } + return linkHeaderField.toString(); + } + + private static void initMethod(HttpUriRequest request, BatchImpl batchImpl, boolean addIfHeader) throws RepositoryException { + initMethod(request, batchImpl.sessionInfo, addIfHeader); + + // add batchId as separate header, TODO: could probably re-use session id Link relation + CodedUrlHeader ch = new CodedUrlHeader(TransactionConstants.HEADER_TRANSACTIONID, batchImpl.batchId); + request.setHeader(ch.getHeaderName(), ch.getHeaderValue()); + } + + private static boolean isSameResource(String requestURI, MultiStatusResponse response) { + try { + String href = resolve(requestURI, response.getHref()); + if (href.endsWith("/") && !requestURI.endsWith("/")) { + href = href.substring(0, href.length() - 1); + } + return requestURI.equals(href); + } catch (RepositoryException e) { + return false; + } + } + + private String saveGetIdString(ItemId id, SessionInfo sessionInfo) { + NamePathResolver resolver = null; + try { + resolver = getNamePathResolver(sessionInfo); + } catch (RepositoryException e) { + // ignore. + } + return saveGetIdString(id, resolver); + } + + private String saveGetIdString(ItemId id, NamePathResolver resolver) { + StringBuffer bf = new StringBuffer(); + String uid = id.getUniqueID(); + if (uid != null) { + bf.append(uid); + } + Path p = id.getPath(); + if (p != null) { + if (resolver == null) { + bf.append(p.toString()); + } else { + try { + bf.append(resolver.getJCRPath(p)); + } catch (NamespaceException e) { + bf.append(p.toString()); + } + } + } + return bf.toString(); + } + + protected NamePathResolver getNamePathResolver(SessionInfo sessionInfo) throws RepositoryException { + checkSessionInfo(sessionInfo); + return getNamePathResolver(((SessionInfoImpl) sessionInfo)); + } + + private NamePathResolver getNamePathResolver(SessionInfoImpl sessionInfo) { + NamePathResolver resolver = sessionInfo.getNamePathResolver(); + if (resolver == null) { + resolver = new NamePathResolverImpl(sessionInfo); + sessionInfo.setNamePathResolver(resolver); + } + return resolver; + } + + /** + * Returns a key for the httpClient hash. The key is either the specified + * SessionInfo or a marker if the session info is null (used during + * repository instantiation). + * + * @param sessionInfo + * @return Key for the client map. + */ + private static Object getClientKey(SessionInfo sessionInfo) { + return (sessionInfo == null) ? CLIENT_KEY : sessionInfo; + } + + protected HttpClient getClient(SessionInfo sessionInfo) throws RepositoryException { + Object clientKey = getClientKey(sessionInfo); + HttpClient client = clients.get(clientKey); + if (client == null) { + client = httpClientBuilder.build(); + if (sessionInfo != null) { + checkSessionInfo(sessionInfo); + clients.put(clientKey, client); + log.debug("Created Client " + client + " for SessionInfo " + sessionInfo); + } + } + return client; + } + + protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException { + HttpClientContext result = HttpClientContext.create(); + if (sessionInfo != null) { + checkSessionInfo(sessionInfo); + org.apache.http.auth.Credentials creds = ((SessionInfoImpl) sessionInfo).getCredentials().getHttpCredentials(); + if (creds != null) { + CredentialsProvider credsProvider = new BasicCredentialsProvider(); + credsProvider.setCredentials(new org.apache.http.auth.AuthScope(httpHost.getHostName(), httpHost.getPort()), creds); + BasicScheme basicAuth = new BasicScheme(); + AuthCache authCache = new BasicAuthCache(); + authCache.put(httpHost, basicAuth); + result.setCredentialsProvider(credsProvider); + result.setAuthCache(authCache); + } + } + return result; + } + + private void removeClient(SessionInfo sessionInfo) { + HttpClient cl = clients.remove(getClientKey(sessionInfo)); + log.debug("Removed Client " + cl + " for SessionInfo " + sessionInfo); + } + + protected String getItemUri(ItemId itemId, SessionInfo sessionInfo) throws RepositoryException { + return getItemUri(itemId, sessionInfo, sessionInfo.getWorkspaceName()); + } + + protected String getItemUri(ItemId itemId, SessionInfo sessionInfo, String workspaceName) throws RepositoryException { + return uriResolver.getItemUri(itemId, workspaceName, sessionInfo); + } + + /** + * Clear all URI mappings. This is required after hierarchy operations such + * as e.g. MOVE. + * + * @param sessionInfo + */ + protected void clearItemUriCache(SessionInfo sessionInfo) { + uriResolver.clearCacheEntries(sessionInfo); + } + + private String getItemUri(NodeId parentId, Name childName, + SessionInfo sessionInfo) throws RepositoryException { + String parentUri = uriResolver.getItemUri(parentId, sessionInfo.getWorkspaceName(), sessionInfo); + NamePathResolver resolver = getNamePathResolver(sessionInfo); + // JCR-2920: don't append '/' to a trailing '/' + if (!parentUri.endsWith("/")) { + parentUri += "/"; + } + return parentUri + Text.escape(resolver.getJCRName(childName)); + } + + private NodeId getParentId(String baseUri, DavPropertySet propSet, SessionInfo sessionInfo) + throws RepositoryException { + NodeId parentId = null; + DavProperty p = propSet.get(JcrRemotingConstants.JCR_PARENT_LN, ItemResourceConstants.NAMESPACE); + if (p != null) { + HrefProperty parentProp = new HrefProperty(p); + String parentHref = parentProp.getHrefs().get(0); + if (parentHref != null && parentHref.length() > 0) { + parentId = uriResolver.getNodeId(resolve(baseUri, parentHref), sessionInfo); + } + } + return parentId; + } + + String getUniqueID(DavPropertySet propSet) { + DavProperty prop = propSet.get(JcrRemotingConstants.JCR_UUID_LN, ItemResourceConstants.NAMESPACE); + if (prop != null) { + return prop.getValue().toString(); + } else { + return null; + } + } + + Name getQName(DavPropertySet propSet, NamePathResolver resolver) throws RepositoryException { + DavProperty nameProp = propSet.get(JcrRemotingConstants.JCR_NAME_LN, ItemResourceConstants.NAMESPACE); + if (nameProp != null && nameProp.getValue() != null) { + // not root node. Note that 'unespacing' is not required since + // the jcr:name property does not provide the value in escaped form. + String jcrName = nameProp.getValue().toString(); + try { + return resolver.getQName(jcrName); + } catch (NameException e) { + throw new RepositoryException(e); + } + } else { + return NameConstants.ROOT; + } + } + + int getIndex(DavPropertySet propSet) { + int index = Path.INDEX_UNDEFINED; + DavProperty indexProp = propSet.get(JcrRemotingConstants.JCR_INDEX_LN, ItemResourceConstants.NAMESPACE); + if (indexProp != null && indexProp.getValue() != null) { + index = Integer.parseInt(indexProp.getValue().toString()); + } + return index; + } + + //-------------------------------------------------------------------------- + /** + * Execute a 'Workspace' operation. + */ + private HttpResponse execute(BaseDavRequest request, SessionInfo sessionInfo) throws RepositoryException { + try { + initMethod(request, sessionInfo, !isUnLockMethod(request)); + + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + return response; + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e, request); + } + } + + //--------------------------------------------------< RepositoryService >--- + + @Override + public IdFactory getIdFactory() { + return idFactory; + } + + @Override + public NameFactory getNameFactory() { + return nameFactory; + } + + @Override + public PathFactory getPathFactory() { + return pathFactory; + } + + @Override + public QValueFactory getQValueFactory() { + return qValueFactory; + } + + @Override + public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) throws RepositoryException { + return new ItemInfoCacheImpl(itemInfoCacheSize); + } + + @Override + public Map getRepositoryDescriptors() throws RepositoryException { + if (descriptors.isEmpty()) { + ReportInfo info = new ReportInfo(JcrRemotingConstants.REPORT_REPOSITORY_DESCRIPTORS, ItemResourceConstants.NAMESPACE); + HttpReport request = null; + try { + request = new HttpReport(uriResolver.getRepositoryUri(), info); + HttpResponse response = executeRequest(null, request); + int sc = response.getStatusLine().getStatusCode(); + if (sc == HttpStatus.SC_UNAUTHORIZED + || sc == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { + // JCR-3076: Mandatory authentication prevents us from + // accessing the descriptors on the server, so instead + // of failing with an exception we simply return an empty + // set of descriptors + log.warn("Authentication required to access repository descriptors"); + return descriptors; + } + + request.checkSuccess(response); + Document doc = request.getResponseBodyAsDocument(response.getEntity()); + + if (doc != null) { + Element rootElement = doc.getDocumentElement(); + ElementIterator nsElems = DomUtil.getChildren(rootElement, JcrRemotingConstants.XML_DESCRIPTOR, ItemResourceConstants.NAMESPACE); + while (nsElems.hasNext()) { + Element elem = nsElems.nextElement(); + String key = DomUtil.getChildText(elem, JcrRemotingConstants.XML_DESCRIPTORKEY, ItemResourceConstants.NAMESPACE); + ElementIterator it = DomUtil.getChildren(elem, JcrRemotingConstants.XML_DESCRIPTORVALUE, ItemResourceConstants.NAMESPACE); + List vs = new ArrayList(); + while (it.hasNext()) { + Element dv = it.nextElement(); + String descriptor = DomUtil.getText(dv); + if (key != null && descriptor != null) { + String typeStr = (DomUtil.getAttribute(dv, JcrRemotingConstants.ATTR_VALUE_TYPE, null)); + int type = (typeStr == null) ? PropertyType.STRING : PropertyType.valueFromName(typeStr); + vs.add(getQValueFactory().create(descriptor, type)); + } else { + log.error("Invalid descriptor key / value pair: " + key + " -> " + descriptor); + } + + } + descriptors.put(key, vs.toArray(new QValue[vs.size()])); + } + } + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + return descriptors; + } + + @Override + public SessionInfo obtain(Credentials credentials, String workspaceName) + throws RepositoryException { + CredentialsWrapper dc = new CredentialsWrapper(credentials); + return obtain(dc, workspaceName); + } + + @Override + public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName) + throws RepositoryException { + checkSessionInfo(sessionInfo); + return obtain(((SessionInfoImpl) sessionInfo).getCredentials(), workspaceName); + } + + @Override + public SessionInfo impersonate(SessionInfo sessionInfo, Credentials credentials) throws RepositoryException { + throw new UnsupportedOperationException("Not implemented yet."); + } + + private SessionInfo obtain(CredentialsWrapper credentials, String workspaceName) + throws RepositoryException { + // check if the workspace with the given name is accessible + HttpPropfind request = null; + SessionInfoImpl sessionInfo = new SessionInfoImpl(credentials, workspaceName); + try { + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + // for backwards compat. -> retrieve DAV:workspace if the newly + // added property (workspaceName) is not supported by the server. + nameSet.add(DeltaVConstants.WORKSPACE); + nameSet.add(JcrRemotingConstants.JCR_WORKSPACE_NAME_LN, ItemResourceConstants.NAMESPACE); + + request = new HttpPropfind(uriResolver.getWorkspaceUri(workspaceName), nameSet, DEPTH_0); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] responses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (responses.length != 1) { + throw new LoginException("Login failed: Unknown workspace '" + workspaceName + "'."); + } + + DavPropertySet props = responses[0].getProperties(DavServletResponse.SC_OK); + DavProperty prop = props.get(JcrRemotingConstants.JCR_WORKSPACE_NAME_LN, ItemResourceConstants.NAMESPACE); + if (prop != null) { + String wspName = prop.getValue().toString(); + if (workspaceName == null) { + // login with 'null' workspace name -> retrieve the effective + // workspace name from the property and recreate the SessionInfo. + sessionInfo = new SessionInfoImpl(credentials, wspName); + } else if (!wspName.equals(workspaceName)) { + throw new LoginException("Login failed: Invalid workspace name '" + workspaceName + "'."); + } + } else if (props.contains(DeltaVConstants.WORKSPACE)) { + String wspHref = new HrefProperty(props.get(DeltaVConstants.WORKSPACE)).getHrefs().get(0); + String wspName = Text.unescape(Text.getName(wspHref, true)); + if (!wspName.equals(workspaceName)) { + throw new LoginException("Login failed: Invalid workspace name " + workspaceName); + } + } else { + throw new LoginException("Login failed: Unknown workspace '" + workspaceName + "'."); + } + } catch (IOException e) { + throw new RepositoryException(e.getMessage()); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + + // make sure the general namespace mappings have been loaded once + // before additional requests are executed that rely on the namespace + // mappings. + if (nsCache.prefixToURI.isEmpty()) { + try { + getRegisteredNamespaces(sessionInfo); + } catch (RepositoryException e) { + // ignore + } + } + + // return the sessionInfo + return sessionInfo; + } + + @Override + public void dispose(SessionInfo sessionInfo) throws RepositoryException { + checkSessionInfo(sessionInfo); + removeClient(sessionInfo); + } + + @Override + public String[] getWorkspaceNames(SessionInfo sessionInfo) throws RepositoryException { + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(DeltaVConstants.WORKSPACE); + HttpPropfind request = null; + try { + request = new HttpPropfind(uriResolver.getRepositoryUri(), nameSet, DEPTH_1); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + Set wspNames = new HashSet(); + for (MultiStatusResponse mresponse : mresponses) { + DavPropertySet props = mresponse.getProperties(DavServletResponse.SC_OK); + if (props.contains(DeltaVConstants.WORKSPACE)) { + HrefProperty hp = new HrefProperty(props.get(DeltaVConstants.WORKSPACE)); + String wspHref = hp.getHrefs().get(0); + String name = Text.unescape(Text.getName(wspHref, true)); + wspNames.add(name); + } + } + return wspNames.toArray(new String[wspNames.size()]); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public boolean isGranted(SessionInfo sessionInfo, ItemId itemId, String[] actions) throws RepositoryException { + HttpReport request = null; + try { + String uri = obtainAbsolutePathFromUri(getItemUri(itemId, sessionInfo)); + ReportInfo reportInfo = new ReportInfo(JcrRemotingConstants.REPORT_PRIVILEGES, ItemResourceConstants.NAMESPACE); + reportInfo.setContentElement(DomUtil.hrefToXml(uri, DomUtil.createDocument())); + + request = new HttpReport(uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()), reportInfo); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] responses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (responses.length < 1) { + throw new ItemNotFoundException("Unable to retrieve permissions for item " + saveGetIdString(itemId, sessionInfo)); + } + DavProperty p = responses[0].getProperties(DavServletResponse.SC_OK).get(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + if (p == null) { + return false; + } + // build set of privileges from given actions. NOTE: since the actions + // have no qualifying namespace, the {@link ItemResourceConstants#NAMESPACE} + // is used. + Set requiredPrivileges = new HashSet(); + for (String action : actions) { + requiredPrivileges.add(Privilege.getPrivilege(action, ItemResourceConstants.NAMESPACE)); + } + // build set of privileges granted to the current user. + CurrentUserPrivilegeSetProperty privSet = new CurrentUserPrivilegeSetProperty(p); + Collection privileges = privSet.getValue(); + + // check privileges present against required privileges. + return privileges.containsAll(requiredPrivileges); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public PrivilegeDefinition[] getPrivilegeDefinitions(SessionInfo sessionInfo) throws RepositoryException { + return internalGetPrivilegeDefinitions(sessionInfo, uriResolver.getRepositoryUri()); + } + + @Override + public PrivilegeDefinition[] getSupportedPrivileges(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + String uri = (nodeId == null) ? uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()) : getItemUri(nodeId, sessionInfo); + return internalGetPrivilegeDefinitions(sessionInfo, uri); + } + + @Override + public Name[] getPrivilegeNames(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + String uri = (nodeId == null) ? uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()) : getItemUri(nodeId, sessionInfo); + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + + HttpPropfind propfindRequest = null; + try { + propfindRequest = new HttpPropfind(uri, nameSet, DEPTH_0); + HttpResponse response = execute(propfindRequest, sessionInfo); + propfindRequest.checkSuccess(response); + + MultiStatusResponse[] mresponses = propfindRequest.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length < 1) { + throw new PathNotFoundException("Unable to retrieve privileges definitions."); + } + + DavPropertyName displayName = SecurityConstants.CURRENT_USER_PRIVILEGE_SET; + DavProperty p = mresponses[0].getProperties(DavServletResponse.SC_OK).get(displayName); + if (p == null) { + return new Name[0]; + } else { + Collection privs = new CurrentUserPrivilegeSetProperty(p).getValue(); + Set privNames = new HashSet(privs.size()); + for (Privilege priv : privs) { + privNames.add(nameFactory.create(priv.getNamespace().getURI(), priv.getName())); + } + return privNames.toArray(new Name[privNames.size()]); + } + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (propfindRequest != null) { + propfindRequest.releaseConnection(); + } + } + } + + private PrivilegeDefinition[] internalGetPrivilegeDefinitions(SessionInfo sessionInfo, String uri) throws RepositoryException { + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(SecurityConstants.SUPPORTED_PRIVILEGE_SET); + HttpPropfind request = null; + try { + request = new HttpPropfind(uri, nameSet, DEPTH_0); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length < 1) { + throw new PathNotFoundException("Unable to retrieve privileges definitions."); + } + + DavPropertyName displayName = SecurityConstants.SUPPORTED_PRIVILEGE_SET; + DavProperty p = mresponses[0].getProperties(DavServletResponse.SC_OK).get(displayName); + if (p == null) { + return new PrivilegeDefinition[0]; + } else { + // build PrivilegeDefinition(s) from the supported-privileges dav property + Map spMap = new HashMap(); + fillSupportedPrivilegeMap(new SupportedPrivilegeSetProperty(p).getValue(), spMap, getNameFactory()); + + List pDefs = new ArrayList(); + for (Name privilegeName : spMap.keySet()) { + SupportedPrivilege sp = spMap.get(privilegeName); + Set aggrnames = null; + SupportedPrivilege[] aggregates = sp.getSupportedPrivileges(); + if (aggregates != null && aggregates.length > 0) { + aggrnames = new HashSet(); + for (SupportedPrivilege aggregate : aggregates) { + Name aggregateName = nameFactory.create(aggregate.getPrivilege().getNamespace().getURI(), + aggregate.getPrivilege().getName()); + aggrnames.add(aggregateName); + } + } + PrivilegeDefinition def = new PrivilegeDefinitionImpl(privilegeName, sp.isAbstract(), aggrnames); + pDefs.add(def); + } + return pDefs.toArray(new PrivilegeDefinition[pDefs.size()]); + } + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + private static void fillSupportedPrivilegeMap(List sps, Map spMap, NameFactory nameFactory) throws NamespaceException, IllegalNameException { + for (SupportedPrivilege sp : sps) { + Privilege p = sp.getPrivilege(); + Name privName = nameFactory.create(p.getNamespace().getURI(), p.getName()); + spMap.put(privName, sp); + List agg = Arrays.asList(sp.getSupportedPrivileges()); + if (!agg.isEmpty()) { + fillSupportedPrivilegeMap(agg, spMap, nameFactory); + } + } + } + + @Override + public QNodeDefinition getNodeDefinition(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + return (QNodeDefinition) getItemDefinition(sessionInfo, nodeId); + } + + @Override + public QPropertyDefinition getPropertyDefinition(SessionInfo sessionInfo, PropertyId propertyId) throws RepositoryException { + return (QPropertyDefinition) getItemDefinition(sessionInfo, propertyId); + } + + /** + * + * @param sessionInfo + * @param itemId + * @return + * @throws RepositoryException + */ + private QItemDefinition getItemDefinition(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException { + // set of properties to be retrieved + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(JcrRemotingConstants.JCR_DEFINITION_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(DavPropertyName.RESOURCETYPE); + + HttpPropfind request = null; + try { + String uri = getItemUri(itemId, sessionInfo); + request = new HttpPropfind(uri, nameSet, DEPTH_0); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length < 1) { + throw new ItemNotFoundException( + "Unable to retrieve the item definition for " + saveGetIdString(itemId, sessionInfo)); + } + if (mresponses.length > 1) { + throw new RepositoryException( + "Internal error: ambigous item definition found '" + saveGetIdString(itemId, sessionInfo) + "'."); + } + DavPropertySet propertySet = mresponses[0].getProperties(DavServletResponse.SC_OK); + + // check if definition matches the type of the id + DavProperty rType = propertySet.get(DavPropertyName.RESOURCETYPE); + if (rType.getValue() == null && itemId.denotesNode()) { + throw new RepositoryException("Internal error: requested node definition and got property definition."); + } + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + + // build the definition + QItemDefinition definition = null; + DavProperty prop = propertySet.get(JcrRemotingConstants.JCR_DEFINITION_LN, ItemResourceConstants.NAMESPACE); + if (prop != null) { + Object value = prop.getValue(); + if (value != null && value instanceof Element) { + Element idfElem = (Element) value; + if (itemId.denotesNode()) { + definition = DefinitionUtil.createQNodeDefinition(null, idfElem, resolver); + } else { + definition = DefinitionUtil.createQPropertyDefinition(null, idfElem, resolver, getQValueFactory()); + } + } + } + if (definition == null) { + throw new RepositoryException("Unable to retrieve definition for item with id '" + saveGetIdString(itemId, resolver) + "'."); + } + return definition; + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + // set of properties to be retrieved + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(JcrRemotingConstants.JCR_INDEX_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_PARENT_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_NAME_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_PRIMARYNODETYPE_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_MIXINNODETYPES_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_REFERENCES_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_UUID_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_PATH_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(DavPropertyName.RESOURCETYPE); + + HttpPropfind request = null; + try { + String uri = getItemUri(nodeId, sessionInfo); + request = new HttpPropfind(uri, nameSet, DEPTH_1); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length < 1) { + throw new ItemNotFoundException("Unable to retrieve the node with id " + saveGetIdString(nodeId, sessionInfo)); + } + + MultiStatusResponse nodeResponse = null; + List childResponses = new ArrayList(); + for (MultiStatusResponse mresponse : mresponses) { + if (isSameResource(uri, mresponse)) { + nodeResponse = mresponse; + } else { + childResponses.add(mresponse); + } + } + + if (nodeResponse == null) { + throw new ItemNotFoundException("Unable to retrieve the node " + saveGetIdString(nodeId, sessionInfo)); + } + + DavPropertySet propSet = nodeResponse.getProperties(DavServletResponse.SC_OK); + Object type = propSet.get(DavPropertyName.RESOURCETYPE).getValue(); + if (type == null) { + // the given id points to a Property instead of a Node + throw new ItemNotFoundException("No node for id " + saveGetIdString(nodeId, sessionInfo)); + } + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + NodeId parentId = getParentId(uri, propSet, sessionInfo); + + NodeInfoImpl nInfo = buildNodeInfo(uri, nodeResponse, parentId, propSet, sessionInfo, resolver); + + for (MultiStatusResponse resp : childResponses) { + DavPropertySet childProps = resp.getProperties(DavServletResponse.SC_OK); + if (childProps.contains(DavPropertyName.RESOURCETYPE) && + childProps.get(DavPropertyName.RESOURCETYPE).getValue() != null) { + // any other resource type than default (empty) is represented by a node item + // --> build child info object + nInfo.addChildInfo(buildChildInfo(childProps, sessionInfo)); + } else { + PropertyId childId = uriResolver.buildPropertyId(nInfo.getId(), resp, sessionInfo.getWorkspaceName(), getNamePathResolver(sessionInfo)); + nInfo.addPropertyId(childId); + } + } + return nInfo; + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } catch (NameException e) { + throw new RepositoryException(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public Iterator getItemInfos(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException { + // TODO: implement batch read properly: + // currently: missing 'value/values' property PropertyInfo cannot be built + // currently: missing prop-names with child-NodeInfo + if (itemId.denotesNode()) { + List l = new ArrayList(); + NodeInfo nInfo = getNodeInfo(sessionInfo, (NodeId) itemId); + l.add(nInfo); + // at least add propertyInfos for the meta-props already known from the + // nodeInfo. + l.addAll(buildPropertyInfos(nInfo)); + return l.iterator(); + } else { + PropertyInfo pInfo = getPropertyInfo(sessionInfo, (PropertyId) itemId); + return Iterators.singleton(pInfo); + } + } + + private NodeInfoImpl buildNodeInfo(String baseUri, MultiStatusResponse nodeResponse, + NodeId parentId, DavPropertySet propSet, + SessionInfo sessionInfo, + NamePathResolver resolver) throws RepositoryException { + NodeId id = uriResolver.buildNodeId(parentId, baseUri, nodeResponse, sessionInfo.getWorkspaceName(), getNamePathResolver(sessionInfo)); + NodeInfoImpl nInfo = new NodeInfoImpl(id, propSet, resolver); + DavProperty p = propSet.get(JcrRemotingConstants.JCR_REFERENCES_LN, ItemResourceConstants.NAMESPACE); + if (p != null) { + HrefProperty refProp = new HrefProperty(p); + for (String propertyHref : refProp.getHrefs()) { + PropertyId propertyId = uriResolver.getPropertyId(propertyHref, sessionInfo); + nInfo.addReference(propertyId); + } + } + return nInfo; + } + + private List buildPropertyInfos(NodeInfo nInfo) throws RepositoryException { + List l = new ArrayList(3); + NodeId nid = nInfo.getId(); + Path nPath = nInfo.getPath(); + + if (nid.getPath() == null) { + PropertyId id = getIdFactory().createPropertyId(nid, NameConstants.JCR_UUID); + QValue[] vs = new QValue[] { getQValueFactory().create(nid.getUniqueID(), PropertyType.STRING) }; + Path p = getPathFactory().create(nPath, NameConstants.JCR_UUID, true); + PropertyInfo pi = new PropertyInfoImpl(id, p, PropertyType.STRING, false, vs); + l.add(pi); + } + + Name pName = NameConstants.JCR_PRIMARYTYPE; + QValue[] vs = new QValue[] { getQValueFactory().create(nInfo.getNodetype()) }; + PropertyInfo pi = new PropertyInfoImpl(getIdFactory().createPropertyId(nid, pName), + getPathFactory().create(nPath, pName, true), PropertyType.NAME, false, vs); + l.add(pi); + + Name[] mixins = nInfo.getMixins(); + if (mixins.length > 0) { + pName = NameConstants.JCR_MIXINTYPES; + vs = new QValue[mixins.length]; + for (int i = 0; i < mixins.length; i++) { + vs[i] = getQValueFactory().create(mixins[i]); + } + pi = new PropertyInfoImpl(getIdFactory().createPropertyId(nid, pName), + getPathFactory().create(nPath, pName, true), PropertyType.NAME, + true, vs); + l.add(pi); + } + + return l; + } + + @Override + public Iterator getChildInfos(SessionInfo sessionInfo, NodeId parentId) throws RepositoryException { + // set of properties to be retrieved + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(JcrRemotingConstants.JCR_NAME_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_INDEX_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_PARENT_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_UUID_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(DavPropertyName.RESOURCETYPE); + + HttpPropfind request = null; + try { + String uri = getItemUri(parentId, sessionInfo); + request = new HttpPropfind(uri, nameSet, DEPTH_1); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + List childEntries; + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length < 1) { + throw new ItemNotFoundException("Unable to retrieve the node with id " + saveGetIdString(parentId, sessionInfo)); + } else if (mresponses.length == 1) { + // no child nodes nor properties + childEntries = Collections.emptyList(); + return childEntries.iterator(); + } + + childEntries = new ArrayList(); + for (MultiStatusResponse mresponse : mresponses) { + if (!isSameResource(uri, mresponse)) { + DavPropertySet childProps = mresponse.getProperties(DavServletResponse.SC_OK); + if (childProps.contains(DavPropertyName.RESOURCETYPE) && + childProps.get(DavPropertyName.RESOURCETYPE).getValue() != null) { + childEntries.add(buildChildInfo(childProps, sessionInfo)); + } // else: property -> ignore + } // else: ignore the response related to the parent + } + return childEntries.iterator(); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + private ChildInfo buildChildInfo(DavPropertySet properties, SessionInfo sessionInfo) throws RepositoryException { + Name qName = getQName(properties, getNamePathResolver(sessionInfo)); + int index = getIndex(properties); + String uuid = getUniqueID(properties); + + return new ChildInfoImpl(qName, uuid, index); + } + + @Override + public Iterator getReferences(SessionInfo sessionInfo, NodeId nodeId, Name propertyName, boolean weakReferences) throws RepositoryException { + // set of properties to be retrieved + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + String refType = weakReferences ? JcrRemotingConstants.JCR_WEAK_REFERENCES_LN : JcrRemotingConstants.JCR_REFERENCES_LN; + nameSet.add(refType, ItemResourceConstants.NAMESPACE); + + HttpPropfind request = null; + try { + String uri = getItemUri(nodeId, sessionInfo); + request = new HttpPropfind(uri, nameSet, DEPTH_0); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length < 1) { + throw new ItemNotFoundException("Unable to retrieve the node with id " + saveGetIdString(nodeId, sessionInfo)); + } + + List refIds = Collections.emptyList(); + for (MultiStatusResponse mresponse : mresponses) { + if (isSameResource(uri, mresponse)) { + DavPropertySet props = mresponse.getProperties(DavServletResponse.SC_OK); + DavProperty p = props.get(refType, ItemResourceConstants.NAMESPACE); + + if (p != null) { + refIds = new ArrayList(); + HrefProperty hp = new HrefProperty(p); + for (String propHref : hp.getHrefs()) { + PropertyId propId = uriResolver.getPropertyId(resolve(uri, propHref), sessionInfo); + if (propertyName == null || propertyName.equals(propId.getName())) { + refIds.add(propId); + } + } + } + } + } + return refIds.iterator(); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId) throws RepositoryException { + HttpGet request = null; + try { + String uri = getItemUri(propertyId, sessionInfo); + request = new HttpGet(uri); + HttpResponse response = executeRequest(sessionInfo, request); + + int status = response.getStatusLine().getStatusCode(); + if (status != DavServletResponse.SC_OK) { + throw ExceptionConverter.generate(new DavException(status, response.getStatusLine().getReasonPhrase())); + } + + Path path = uriResolver.getQPath(uri, sessionInfo); + + HttpEntity entity = response.getEntity(); + ContentType ct = ContentType.get(entity); + + boolean isMultiValued; + QValue[] values; + int type; + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + + if (ct != null && ct.getMimeType().startsWith("jcr-value")) { + type = JcrValueType.typeFromContentType(ct.getMimeType()); + QValue v; + if (type == PropertyType.BINARY) { + v = getQValueFactory().create(entity.getContent()); + } else { + Reader reader = new InputStreamReader(entity.getContent(), ct.getCharset()); + StringBuffer sb = new StringBuffer(); + int c; + while ((c = reader.read()) > -1) { + sb.append((char) c); + } + Value jcrValue = valueFactory.createValue(sb.toString(), type); + if (jcrValue instanceof QValueValue) { + v = ((QValueValue) jcrValue).getQValue(); + } else { + v = ValueFormat.getQValue(jcrValue, resolver, getQValueFactory()); + } + } + values = new QValue[] { v }; + isMultiValued = false; + } else if (ct != null && ct.getMimeType().equals("text/xml")) { + // jcr:values property spooled + values = getValues(entity.getContent(), resolver, propertyId); + type = (values.length > 0) ? values[0].getType() : loadType(uri, getClient(sessionInfo), propertyId, sessionInfo, resolver); + isMultiValued = true; + } else { + throw new ItemNotFoundException("Unable to retrieve the property with id " + saveGetIdString(propertyId, resolver)); + } + + return new PropertyInfoImpl(propertyId, path, type, isMultiValued, values); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } catch (NameException e) { + throw new RepositoryException(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + private QValue[] getValues(InputStream response, NamePathResolver resolver, ItemId id) throws RepositoryException { + try { + Document doc = DomUtil.parseDocument(response); + Element prop = DomUtil.getChildElement(doc, JcrRemotingConstants.JCR_VALUES_LN, ItemResourceConstants.NAMESPACE); + if (prop == null) { + // no jcr-values present in the response body -> apparently + // not representation of a jcr-property + throw new ItemNotFoundException("No property found at " + saveGetIdString(id, resolver)); + } else { + DavProperty p = DefaultDavProperty.createFromXml(prop); + Value[] jcrVs = ValueUtil.valuesFromXml(p.getValue(), PropertyType.STRING, valueFactory); + QValue[] qvs = new QValue[jcrVs.length]; + int type = (jcrVs.length > 0) ? jcrVs[0].getType() : PropertyType.STRING; + + for (int i = 0; i < jcrVs.length; i++) { + if (jcrVs[i] instanceof QValueValue) { + qvs[i] = ((QValueValue) jcrVs[i]).getQValue(); + } else if (type == PropertyType.BINARY) { + qvs[i] = qValueFactory.create(jcrVs[i].getStream()); + } else { + qvs[i] = ValueFormat.getQValue(jcrVs[i], resolver, qValueFactory); + } + } + return qvs; + } + } catch (SAXException e) { + log.warn("Internal error: {}", e.getMessage()); + throw new RepositoryException(e); + } catch (IOException e) { + log.warn("Internal error: {}", e.getMessage()); + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + log.warn("Internal error: {}", e.getMessage()); + throw new RepositoryException(e); + } + } + + private int loadType(String propertyURI, HttpClient client, PropertyId propertyId, SessionInfo sessionInfo, NamePathResolver resolver) throws IOException, DavException, RepositoryException { + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(JcrRemotingConstants.JCR_TYPE_LN, ItemResourceConstants.NAMESPACE); + + HttpPropfind request = null; + try { + request = new HttpPropfind(propertyURI, nameSet, DEPTH_0); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length == 1) { + DavPropertySet props = mresponses[0].getProperties(DavServletResponse.SC_OK); + DavProperty type = props.get(JcrRemotingConstants.JCR_TYPE_LN, ItemResourceConstants.NAMESPACE); + if (type != null) { + return PropertyType.valueFromName(type.getValue().toString()); + } else { + throw new RepositoryException("Internal error. Cannot retrieve property type at " + saveGetIdString(propertyId, resolver)); + } + } else { + throw new ItemNotFoundException("Internal error. Cannot retrieve property type at " + saveGetIdString(propertyId, resolver)); + } + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public Batch createBatch(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException { + checkSessionInfo(sessionInfo); + return new BatchImpl(itemId, sessionInfo); + } + + @Override + public void submit(Batch batch) throws RepositoryException { + if (!(batch instanceof BatchImpl)) { + throw new RepositoryException("Unknown Batch implementation."); + } + BatchImpl batchImpl = (BatchImpl) batch; + if (batchImpl.isEmpty()) { + batchImpl.dispose(); + return; + } + + HttpRequestBase request = null; + try { + HttpClient client = batchImpl.start(); + boolean success = false; + + try { + Iterator it = batchImpl.requests(); + while (it.hasNext()) { + request = it.next(); + initMethod(request, batchImpl, true); + + HttpResponse response = client.execute(request); + if (request instanceof BaseDavRequest) { + ((BaseDavRequest) request).checkSuccess(response); + } else { + // use generic HTTP status code checking + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode < 200 || statusCode >= 300) { + throw new DavException(statusCode, "Unexpected status code " + statusCode + " in response to " + + request.getMethod() + " request."); + } + } + request.releaseConnection(); + } + success = true; + } finally { + // make sure the lock is removed. if any of the methods + // failed the unlock is used to abort any pending changes + // on the server. + batchImpl.end(client, success); + } + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e, request); + } finally { + batchImpl.dispose(); + } + } + + @Override + public Tree createTree(SessionInfo sessionInfo, Batch batch, Name nodeName, Name primaryTypeName, String uniqueId) throws RepositoryException { + return new DocumentTree(nodeName, primaryTypeName, uniqueId, getNamePathResolver(sessionInfo)); + } + + @Override + public void importXml(SessionInfo sessionInfo, NodeId parentId, InputStream xmlStream, int uuidBehaviour) throws RepositoryException { + // TODO: improve. currently random name is built instead of retrieving name of new resource from top-level xml element within stream + Name nodeName = getNameFactory().create(Name.NS_DEFAULT_URI, UUID.randomUUID().toString()); + String uri = getItemUri(parentId, nodeName, sessionInfo); + HttpMkcol mkcolRequest = new HttpMkcol(uri); + mkcolRequest.addHeader(JcrRemotingConstants.IMPORT_UUID_BEHAVIOR, Integer.toString(uuidBehaviour)); + mkcolRequest.setEntity(new InputStreamEntity(xmlStream, ContentType.create("text/xml"))); + execute(mkcolRequest, sessionInfo); + } + + @Override + public void move(SessionInfo sessionInfo, NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + String uri = getItemUri(srcNodeId, sessionInfo); + String destUri = getItemUri(destParentNodeId, destName, sessionInfo); + if (isDavClass3(sessionInfo)) { + destUri = obtainAbsolutePathFromUri(destUri); + } + HttpMove request = new HttpMove(uri, destUri, false); + try { + initMethod(request, sessionInfo); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + // need to clear the cache as the move may have affected nodes with + // uuid. + clearItemUriCache(sessionInfo); + } catch (IOException ex) { + throw new RepositoryException(ex); + } catch (DavException e) { + throw ExceptionConverter.generate(e, request); + } finally { + request.releaseConnection(); + } + } + + @Override + public void copy(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + String uri = uriResolver.getItemUri(srcNodeId, srcWorkspaceName, sessionInfo); + String destUri = getItemUri(destParentNodeId, destName, sessionInfo); + if (isDavClass3(sessionInfo)) { + destUri = obtainAbsolutePathFromUri(destUri); + } + HttpCopy request = new HttpCopy(uri, destUri, false, false); + try { + initMethod(request, sessionInfo); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException ex) { + throw new RepositoryException(ex); + } catch (DavException e) { + throw ExceptionConverter.generate(e, request); + } finally { + request.releaseConnection(); + } + } + + @Override + public void update(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName) throws RepositoryException { + String uri = getItemUri(nodeId, sessionInfo); + String workspUri = uriResolver.getWorkspaceUri(srcWorkspaceName); + + update(uri, null, new String[] { workspUri }, UpdateInfo.UPDATE_BY_WORKSPACE, false, sessionInfo); + } + + @Override + public void clone(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName, boolean removeExisting) throws RepositoryException { + // TODO: missing implementation + throw new UnsupportedOperationException("Missing implementation"); + } + + @Override + public LockInfo getLockInfo(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + // set of Dav-properties to be retrieved + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(DavPropertyName.LOCKDISCOVERY); + nameSet.add(JcrRemotingConstants.JCR_PARENT_LN, ItemResourceConstants.NAMESPACE); + + HttpPropfind request = null; + try { + String uri = getItemUri(nodeId, sessionInfo); + request = new HttpPropfind(uri, nameSet, DEPTH_0); + initMethod(request, sessionInfo, false); + + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length != 1) { + throw new ItemNotFoundException( + "Unable to retrieve the LockInfo. No such node " + saveGetIdString(nodeId, sessionInfo)); + } + + DavPropertySet ps = mresponses[0].getProperties(DavServletResponse.SC_OK); + if (ps.contains(DavPropertyName.LOCKDISCOVERY)) { + DavProperty p = ps.get(DavPropertyName.LOCKDISCOVERY); + LockDiscovery ld = LockDiscovery.createFromXml(p.toXml(DomUtil.createDocument())); + NodeId parentId = getParentId(uri, ps, sessionInfo); + return retrieveLockInfo(ld, sessionInfo, nodeId, parentId); + } else { + // no lock present + log.debug("No Lock present on node with id " + saveGetIdString(nodeId, sessionInfo)); + return null; + } + } catch (IOException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, + boolean sessionScoped) throws RepositoryException { + return lock(sessionInfo, nodeId, deep, sessionScoped, Long.MAX_VALUE, null); + } + + @Override + public LockInfo lock(SessionInfo sessionInfo, NodeId nodeId, boolean deep, boolean sessionScoped, long timeoutHint, String ownerHint) throws RepositoryException { + HttpLock request = null; + try { + checkSessionInfo(sessionInfo); + long davTimeout = (timeoutHint == Long.MAX_VALUE) ? INFINITE_TIMEOUT : timeoutHint * 1000; + String ownerInfo = (ownerHint == null) ? sessionInfo.getUserID() : ownerHint; + + String uri = getItemUri(nodeId, sessionInfo); + Scope scope = (sessionScoped) ? ItemResourceConstants.EXCLUSIVE_SESSION : Scope.EXCLUSIVE; + request = new HttpLock(uri, + new org.apache.jackrabbit.webdav.lock.LockInfo(scope, Type.WRITE, ownerInfo, davTimeout, deep)); + HttpResponse response = execute(request, sessionInfo); + + String lockToken = request.getLockToken(response); + ((SessionInfoImpl) sessionInfo).addLockToken(lockToken, sessionScoped); + + LockDiscovery disc = request.getResponseBodyAsLockDiscovery(response); + return retrieveLockInfo(disc, sessionInfo, nodeId, null); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public void refreshLock(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + checkSessionInfo(sessionInfo); + String uri = getItemUri(nodeId, sessionInfo); + // since sessionInfo does not allow to retrieve token by NodeId, + // pass all available lock tokens to the LOCK method (TODO: correct?) + Set allLockTokens = ((SessionInfoImpl) sessionInfo).getAllLockTokens(); + String[] locktokens = allLockTokens.toArray(new String[allLockTokens.size()]); + HttpLock httpLock = null; + try { + httpLock = new HttpLock(uri, INFINITE_TIMEOUT, locktokens); + execute(httpLock, sessionInfo); + } finally { + if (httpLock != null) { + httpLock.releaseConnection(); + } + } + } + + @Override + public void unlock(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + checkSessionInfo(sessionInfo); + String uri = getItemUri(nodeId, sessionInfo); + // Note: since sessionInfo does not allow to identify the id of the + // lock holding node, we need to access the token via lockInfo + // TODO: review this. + LockInfoImpl lInfo = (LockInfoImpl) getLockInfo(sessionInfo, nodeId); + if (lInfo == null) { + throw new LockException("No Lock present on Node with id " + saveGetIdString(nodeId, sessionInfo)); + } + + String lockToken = lInfo.getActiveLock().getToken(); + boolean isSessionScoped = lInfo.isSessionScoped(); + + if (!((SessionInfoImpl) sessionInfo).getAllLockTokens().contains(lockToken)) { + throw new LockException("Lock " + lockToken + " not owned by this session"); + } + + HttpUnlock unlockRequest = new HttpUnlock(uri, lockToken); + try { + execute(unlockRequest, sessionInfo); + ((SessionInfoImpl) sessionInfo).removeLockToken(lockToken, isSessionScoped); + } finally { + unlockRequest.releaseConnection(); + } + } + + private LockInfo retrieveLockInfo(LockDiscovery lockDiscovery, SessionInfo sessionInfo, + NodeId nodeId, NodeId parentId) throws RepositoryException { + checkSessionInfo(sessionInfo); + List activeLocks = lockDiscovery.getValue(); + ActiveLock activeLock = null; + for (ActiveLock l : activeLocks) { + Scope sc = l.getScope(); + if (l.getType() == Type.WRITE && (Scope.EXCLUSIVE.equals(sc) || sc == ItemResourceConstants.EXCLUSIVE_SESSION)) { + if (activeLock != null) { + throw new RepositoryException("Node " + saveGetIdString(nodeId, sessionInfo) + " contains multiple exclusive write locks."); + } else { + activeLock = l; + } + } + } + if (activeLock == null) { + log.debug("No lock present on node " + saveGetIdString(nodeId, sessionInfo)); + return null; + } + + NodeId holder = null; + String lockroot = activeLock.getLockroot(); + if (activeLock.getLockroot() != null) { + holder = uriResolver.getNodeId(lockroot, sessionInfo); + } + + if (activeLock.isDeep() && holder == null && parentId != null) { + // deep lock, parent known, but holder is not + LockInfo pLockInfo = getLockInfo(sessionInfo, parentId); + if (pLockInfo != null) { + return pLockInfo; + } + } + return new LockInfoImpl(activeLock, holder == null ? nodeId : holder, ((SessionInfoImpl) sessionInfo).getAllLockTokens()); + } + + @Override + public NodeId checkin(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + String uri = getItemUri(nodeId, sessionInfo); + HttpCheckin request = new HttpCheckin(uri); + try { + initMethod(request, sessionInfo, !isUnLockMethod(request)); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + org.apache.http.Header rh = response.getFirstHeader(DeltaVConstants.HEADER_LOCATION); + return uriResolver.getNodeId(resolve(uri, rh.getValue()), sessionInfo); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException ex) { + throw ExceptionConverter.generate(ex); + } finally { + request.releaseConnection(); + } + } + + @Override + public void checkout(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + String uri = getItemUri(nodeId, sessionInfo); + HttpCheckout request = new HttpCheckout(uri); + try { + initMethod(request, sessionInfo, !isUnLockMethod(request)); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException ex) { + throw ExceptionConverter.generate(ex); + } finally { + request.releaseConnection(); + } + } + + @Override + public void checkout(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) throws RepositoryException { + if (activityId == null) { + checkout(sessionInfo, nodeId); + } else { + // TODO + throw new UnsupportedOperationException("JCR-2104: JSR 283 Versioning. Implementation missing"); + } + } + + @Override + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + // TODO review again. + NodeId vID = checkin(sessionInfo, nodeId); + checkout(sessionInfo, nodeId); + return vID; + } + + @Override + public NodeId checkpoint(SessionInfo sessionInfo, NodeId nodeId, NodeId activityId) throws RepositoryException { + if (activityId == null) { + return checkpoint(sessionInfo, nodeId); + } else { + // TODO + throw new UnsupportedOperationException("JCR-2104: JSR 283 Versioning. Implementation missing"); + } + } + + @Override + public void removeVersion(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId) throws RepositoryException { + String uri = getItemUri(versionId, sessionInfo); + HttpDelete request = new HttpDelete(uri); + try { + initMethod(request, sessionInfo, !isUnLockMethod(request)); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException ex) { + throw new RepositoryException(ex); + } catch (DavException ex) { + throw ExceptionConverter.generate(ex); + } finally { + request.releaseConnection(); + } + } + + @Override + public void restore(SessionInfo sessionInfo, NodeId nodeId, NodeId versionId, boolean removeExisting) throws RepositoryException { + String uri = getItemUri(nodeId, sessionInfo); + String vUri = getItemUri(versionId, sessionInfo); + + Path relPath = null; + if (!exists(sessionInfo, uri)) { + // restore with rel-Path part + Path path = nodeId.getPath(); + if (nodeId.getUniqueID() != null) { + uri = getItemUri(idFactory.createNodeId(nodeId.getUniqueID(), null), sessionInfo); + relPath = (path.isAbsolute()) ? getPathFactory().getRootPath().computeRelativePath(path) : path; + } else { + int degree = 0; + while (degree < path.getLength()) { + Path ancestorPath = path.getAncestor(degree); + NodeId parentId = idFactory.createNodeId(nodeId.getUniqueID(), ancestorPath); + if (exists(sessionInfo, getItemUri(parentId, sessionInfo))) { + uri = getItemUri(parentId, sessionInfo); + relPath = ancestorPath.computeRelativePath(path); + break; + } + degree++; + } + } + } + + update(uri, relPath, new String[] { vUri }, UpdateInfo.UPDATE_BY_VERSION, removeExisting, sessionInfo); + } + + private boolean exists(SessionInfo sInfo, String uri) { + HttpHead request = new HttpHead(uri); + try { + int statusCode = executeRequest(sInfo, request).getStatusLine().getStatusCode(); + return (statusCode == DavServletResponse.SC_OK); + } catch (IOException e) { + log.error("Unexpected error while testing existence of item.", e); + return false; + } catch (RepositoryException e) { + log.error(e.getMessage()); + return false; + } finally { + request.releaseConnection(); + } + } + + @Override + public void restore(SessionInfo sessionInfo, NodeId[] versionIds, boolean removeExisting) throws RepositoryException { + String uri = uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()); + String[] vUris = new String[versionIds.length]; + for (int i = 0; i < versionIds.length; i++) { + vUris[i] = getItemUri(versionIds[i], sessionInfo); + } + + update(uri, null, vUris, UpdateInfo.UPDATE_BY_VERSION, removeExisting, sessionInfo); + } + + private void update(String uri, Path relPath, String[] updateSource, int updateType, boolean removeExisting, SessionInfo sessionInfo) throws RepositoryException { + HttpUpdate request = null; + try { + UpdateInfo uInfo; + String tmpUpdateSource[] = obtainAbsolutePathsFromUris(updateSource); + if (removeExisting || relPath != null) { + Element uElem = UpdateInfo.createUpdateElement(tmpUpdateSource, updateType, DomUtil.createDocument()); + if (removeExisting) { + DomUtil.addChildElement(uElem, JcrRemotingConstants.XML_REMOVEEXISTING, ItemResourceConstants.NAMESPACE); + } + if (relPath != null) { + DomUtil.addChildElement(uElem, JcrRemotingConstants.XML_RELPATH, ItemResourceConstants.NAMESPACE, getNamePathResolver(sessionInfo).getJCRPath(relPath)); + } + + uInfo = new UpdateInfo(uElem); + } else { + uInfo = new UpdateInfo(tmpUpdateSource, updateType, new DavPropertyNameSet()); + } + + request = new HttpUpdate(uri, uInfo); + initMethod(request, sessionInfo, !isUnLockMethod(request)); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public Iterator merge(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName, boolean bestEffort) throws RepositoryException { + return merge(sessionInfo, nodeId, srcWorkspaceName, bestEffort, false); + } + + @Override + public Iterator merge(SessionInfo sessionInfo, NodeId nodeId, String srcWorkspaceName, boolean bestEffort, boolean isShallow) throws RepositoryException { + HttpMerge request = null; + try { + Document doc = DomUtil.createDocument(); + String wspHref = obtainAbsolutePathFromUri(uriResolver.getWorkspaceUri(srcWorkspaceName)); + Element mElem = MergeInfo.createMergeElement(new String[] { wspHref }, !bestEffort, false, doc); + if (isShallow) { + mElem.appendChild(DomUtil.depthToXml(false, doc)); + } + MergeInfo mInfo = new MergeInfo(mElem); + + String uri = getItemUri(nodeId, sessionInfo); + request = new HttpMerge(uri, mInfo); + initMethod(request, sessionInfo, !isUnLockMethod(request)); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] resps = request.getResponseBodyAsMultiStatus(response).getResponses(); + List failedIds = new ArrayList(resps.length); + for (MultiStatusResponse resp : resps) { + String href = resolve(uri, resp.getHref()); + failedIds.add(uriResolver.getNodeId(href, sessionInfo)); + } + return failedIds.iterator(); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public void resolveMergeConflict(SessionInfo sessionInfo, NodeId nodeId, NodeId[] mergeFailedIds, NodeId[] predecessorIds) throws RepositoryException { + HttpProppatch request = null; + try { + List changeList = new ArrayList(); + String[] mergeFailedHref = new String[mergeFailedIds.length]; + for (int i = 0; i < mergeFailedIds.length; i++) { + mergeFailedHref[i] = getItemUri(mergeFailedIds[i], sessionInfo); + } + changeList.add(new HrefProperty(VersionControlledResource.AUTO_MERGE_SET, mergeFailedHref, false)); + + if (predecessorIds != null && predecessorIds.length > 0) { + String[] pdcHrefs = new String[predecessorIds.length]; + for (int i = 0; i < predecessorIds.length; i++) { + pdcHrefs[i] = getItemUri(predecessorIds[i], sessionInfo); + } + changeList.add(new HrefProperty(VersionControlledResource.PREDECESSOR_SET, pdcHrefs, false)); + } + + request = new HttpProppatch(getItemUri(nodeId, sessionInfo), changeList); + initMethod(request, sessionInfo, !isUnLockMethod(request)); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public void addVersionLabel(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId, Name label, boolean moveLabel) throws RepositoryException { + HttpLabel request = null; + try { + String uri = getItemUri(versionId, sessionInfo); + String strLabel = getNamePathResolver(sessionInfo).getJCRName(label); + request = new HttpLabel(uri, new LabelInfo(strLabel, moveLabel ? LabelInfo.TYPE_SET : LabelInfo.TYPE_ADD)); + initMethod(request, sessionInfo, !isUnLockMethod(request)); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException ex) { + throw ExceptionConverter.generate(ex); + } finally { + request.releaseConnection(); + } + } + + @Override + public void removeVersionLabel(SessionInfo sessionInfo, NodeId versionHistoryId, NodeId versionId, Name label) throws RepositoryException { + HttpLabel request = null; + try { + String uri = getItemUri(versionId, sessionInfo); + String strLabel = getNamePathResolver(sessionInfo).getJCRName(label); + request = new HttpLabel(uri, new LabelInfo(strLabel, LabelInfo.TYPE_REMOVE)); + initMethod(request, sessionInfo, !isUnLockMethod(request)); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException ex) { + throw ExceptionConverter.generate(ex); + } finally { + request.releaseConnection(); + } + } + + @Override + public NodeId createActivity(SessionInfo sessionInfo, String title) throws RepositoryException { + // TODO + throw new UnsupportedOperationException("JCR-2104: JSR 283 Versioning. Implementation missing"); + } + + @Override + public void removeActivity(SessionInfo sessionInfo, NodeId activityId) throws RepositoryException { + // TODO + throw new UnsupportedOperationException("JCR-2104: JSR 283 Versioning. Implementation missing"); + } + + @Override + public Iterator mergeActivity(SessionInfo sessionInfo, NodeId activityId) throws RepositoryException { + // TODO + throw new UnsupportedOperationException("JCR-2104: JSR 283 Versioning. Implementation missing"); + } + + @Override + public NodeId createConfiguration(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + // TODO + throw new UnsupportedOperationException("JCR-2104: JSR 283 Versioning. Implementation missing"); + } + + @Override + public String[] getSupportedQueryLanguages(SessionInfo sessionInfo) throws RepositoryException { + HttpOptions request = new HttpOptions(uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName())); + try { + HttpResponse response = executeRequest(sessionInfo, request); + int status = response.getStatusLine().getStatusCode(); + if (status != DavServletResponse.SC_OK) { + throw new DavException(status); + } + return request.getSearchGrammars(response).toArray(new String[0]); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + request.releaseConnection(); + } + } + + @Override + public String[] checkQueryStatement(SessionInfo sessionInfo, + String statement, + String language, + Map namespaces) + throws RepositoryException { + // TODO implement + return new String[0]; + } + + @Override + public QueryInfo executeQuery(SessionInfo sessionInfo, String statement, String language, Map namespaces, long limit, long offset, Map values) throws RepositoryException { + HttpSearch request = null; + try { + String uri = uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()); + SearchInfo sInfo = new SearchInfo( + language, ItemResourceConstants.NAMESPACE, + statement, namespaces); + + if (limit != -1) { + sInfo.setNumberResults(limit); + } + if (offset != -1) { + sInfo.setOffset(offset); + } + + if (!(values == null || values.isEmpty())) { + throw new UnsupportedOperationException("Implementation missing: JCR-2107"); + } + + request = new HttpSearch(uri, sInfo); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatus ms = request.getResponseBodyAsMultiStatus(response); + NamePathResolver resolver = getNamePathResolver(sessionInfo); + return new QueryInfoImpl(ms, idFactory, resolver, valueFactory, getQValueFactory()); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public EventFilter createEventFilter(SessionInfo sessionInfo, + int eventTypes, + Path absPath, + boolean isDeep, + String[] uuids, + Name[] nodeTypeNames, + boolean noLocal) + throws RepositoryException { + // resolve node type names + // todo what if new node types become available while event filter is still in use? + Set resolvedTypeNames = null; + if (nodeTypeNames != null) { + resolvedTypeNames = new HashSet(); + // make sure node type definitions are available + if (nodeTypeDefinitions.size() == 0) { + getQNodeTypeDefinitions(sessionInfo); + } + synchronized (nodeTypeDefinitions) { + for (Name nodeTypeName : nodeTypeNames) { + resolveNodeType(resolvedTypeNames, nodeTypeName); + } + } + } + return new EventFilterImpl(eventTypes, absPath, isDeep, uuids, + resolvedTypeNames, noLocal); + } + + @Override + public EventBundle[] getEvents(Subscription subscription, long timeout) + throws RepositoryException { + checkSubscription(subscription); + + EventSubscriptionImpl subscr = (EventSubscriptionImpl) subscription; + String rootUri = uriResolver.getRootItemUri(subscr.getSessionInfo().getWorkspaceName()); + + return poll(rootUri, subscr.getId(), timeout, subscr.getSessionInfo()); + } + + @Override + public EventBundle getEvents(SessionInfo sessionInfo, EventFilter filter, long after) throws RepositoryException { + // TODO: use filters remotely (JCR-3179) + + HttpGet request = null; + String rootUri = uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()); + rootUri += "?type=journal"; // TODO should have a way to discover URI template + + try { + request = new HttpGet(rootUri); + request.addHeader("If-None-Match", "\"" + Long.toHexString(after) + "\""); // TODO + initMethod(request, sessionInfo); + + HttpResponse response = executeRequest(sessionInfo, request); + int status = response.getStatusLine().getStatusCode(); + if (status != 200) { + throw new RepositoryException("getEvents to " + rootUri + " failed with " + response.getStatusLine()); + } + + HttpEntity entity = response.getEntity(); + InputStream in = entity.getContent(); + Document doc = null; + if (in != null) { + // read response and try to build a xml document + try { + doc = DomUtil.parseDocument(in); + } catch (ParserConfigurationException e) { + throw new IOException("XML parser configuration error", e); + } catch (SAXException e) { + throw new IOException("XML parsing error", e); + } finally { + in.close(); + } + } + + List events = new ArrayList(); + + ElementIterator entries = DomUtil.getChildren(doc.getDocumentElement(), AtomFeedConstants.N_ENTRY); + while (entries.hasNext()) { + Element entryElem = entries.next(); + + Element contentElem = DomUtil.getChildElement(entryElem, AtomFeedConstants.N_CONTENT); + if (contentElem != null + && "application/vnd.apache.jackrabbit.event+xml".equals(contentElem.getAttribute("type"))) { + List el = buildEventList(contentElem, (SessionInfoImpl) sessionInfo, rootUri); + for (Event e : el) { + if (e.getDate() > after && (filter == null || filter.accept(e, false))) { + events.add(e); + } + } + } + } + + return new EventBundleImpl(events, false); + } catch (Exception ex) { + log.error("extracting events from journal feed", ex); + throw new RepositoryException("extracting events from journal feed: " + ex.getMessage(), ex); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public Subscription createSubscription(SessionInfo sessionInfo, + EventFilter[] filters) + throws RepositoryException { + + checkSessionInfo(sessionInfo); + String rootUri = uriResolver.getRootItemUri(sessionInfo.getWorkspaceName()); + String subscriptionId = subscribe(rootUri, S_INFO, null, sessionInfo, null); + log.debug("Subscribed on server for session info " + sessionInfo); + + try { + checkEventFilterSupport(filters); + } catch (UnsupportedRepositoryOperationException ex) { + unsubscribe(rootUri, subscriptionId, sessionInfo); + throw (ex); + } + return new EventSubscriptionImpl(subscriptionId, (SessionInfoImpl) sessionInfo); + } + + @Override + public void updateEventFilters(Subscription subscription, + EventFilter[] filters) + throws RepositoryException { + // do nothing ... + // this is actually not correct because we listen for everything and + // rely on the client of the repository service to filter the events + checkEventFilterSupport(filters); + } + + private void checkEventFilterSupport(EventFilter[] filters) + throws UnsupportedRepositoryOperationException { + for (EventFilter ef : filters) { + if (ef instanceof EventFilterImpl) { + EventFilterImpl efi = (EventFilterImpl) ef; + if (efi.getNodeTypeNames() != null + && !remoteServerProvidesNodeTypes) { + throw new UnsupportedRepositoryOperationException( + "Remote server does not provide node type information in events"); + } + if (efi.getNoLocal() && !remoteServerProvidesNoLocalFlag) { + throw new UnsupportedRepositoryOperationException( + "Remote server does not provide local flag in events"); + } + } + } + } + + @Override + public void dispose(Subscription subscription) throws RepositoryException { + checkSubscription(subscription); + EventSubscriptionImpl subscr = (EventSubscriptionImpl) subscription; + String rootUri = uriResolver.getRootItemUri( + subscr.getSessionInfo().getWorkspaceName()); + unsubscribe(rootUri, subscr.getId(), subscr.getSessionInfo()); + } + + private String subscribe(String uri, SubscriptionInfo subscriptionInfo, + String subscriptionId, SessionInfo sessionInfo, + String batchId) throws RepositoryException { + HttpSubscribe request = null; + try { + request = new HttpSubscribe(uri, subscriptionInfo, subscriptionId); + initMethod(request, sessionInfo); + + if (batchId != null) { + // add batchId as separate header + CodedUrlHeader ch = new CodedUrlHeader(TransactionConstants.HEADER_TRANSACTIONID, batchId); + request.setHeader(ch.getHeaderName(), ch.getHeaderValue()); + } + + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + org.apache.jackrabbit.webdav.observation.Subscription[] subs = request.getResponseBodyAsSubscriptionDiscovery(response) + .getValue(); + if (subs.length == 1) { + this.remoteServerProvidesNodeTypes = subs[0].eventsProvideNodeTypeInformation(); + this.remoteServerProvidesNoLocalFlag = subs[0].eventsProvideNoLocalFlag(); + } + + return request.getSubscriptionId(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + private void unsubscribe(String uri, String subscriptionId, SessionInfo sessionInfo) throws RepositoryException { + HttpUnsubscribe request = null; + try { + request = new HttpUnsubscribe(uri, subscriptionId); + initMethod(request, sessionInfo); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + private void resolveNodeType(Set resolved, Name ntName) { + if (!resolved.add(ntName)) { + return; + } + QNodeTypeDefinition def = nodeTypeDefinitions.get(ntName); + if (def != null) { + for (Name supertype : def.getSupertypes()) { + resolveNodeType(resolved, supertype); + } + } + } + + private EventBundle[] poll(String uri, String subscriptionId, long timeout, SessionInfoImpl sessionInfo) throws RepositoryException { + HttpPoll request = null; + try { + request = new HttpPoll(uri, subscriptionId, timeout); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + EventDiscovery disc = request.getResponseBodyAsEventDiscovery(response); + EventBundle[] events; + if (disc.isEmpty()) { + events = new EventBundle[0]; + } else { + Element discEl = disc.toXml(DomUtil.createDocument()); + ElementIterator it = DomUtil.getChildren(discEl, + ObservationConstants.N_EVENTBUNDLE); + List bundles = new ArrayList(); + while (it.hasNext()) { + Element bundleElement = it.nextElement(); + String value = DomUtil.getAttribute(bundleElement, + ObservationConstants.XML_EVENT_LOCAL, null); + // check if it matches a batch id recently submitted + boolean isLocal = false; + if (value != null) { + isLocal = Boolean.parseBoolean(value); + } + bundles.add(new EventBundleImpl( + buildEventList(bundleElement, sessionInfo, uri), + isLocal)); + } + events = bundles.toArray(new EventBundle[bundles.size()]); + } + return events; + } catch (IOException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + private List buildEventList(Element bundleElement, SessionInfoImpl sessionInfo, String baseUri) + throws RepositoryException { + List events = new ArrayList(); + ElementIterator eventElementIterator = DomUtil.getChildren(bundleElement, ObservationConstants.N_EVENT); + + String userId = null; + + // get user id from enclosing Atom entry element in case this was a feed + if (DomUtil.matches(bundleElement, AtomFeedConstants.N_ENTRY)) { + Element authorEl = DomUtil.getChildElement(bundleElement, AtomFeedConstants.N_AUTHOR); + Element nameEl = authorEl != null ? DomUtil.getChildElement(authorEl, AtomFeedConstants.N_NAME) : null; + if (nameEl != null) { + userId = DomUtil.getTextTrim(nameEl); + } + } + + while (eventElementIterator.hasNext()) { + Element evElem = eventElementIterator.nextElement(); + Element typeEl = DomUtil.getChildElement(evElem, ObservationConstants.N_EVENTTYPE); + EventType[] et = DefaultEventType.createFromXml(typeEl); + if (et.length == 0 || et.length > 1) { + // should not occur. + log.error("Ambiguous event type definition: expected one single event type."); + continue; + } + + String href = DomUtil.getChildTextTrim(evElem, XML_HREF, NAMESPACE); + + int type = EventUtil.getJcrEventType(et[0].getName()); + Path eventPath = null; + ItemId eventId = null; + NodeId parentId = null; + + if (href != null) { + href = resolve(baseUri, href); + try { + eventPath = uriResolver.getQPath(href, sessionInfo); + } catch (RepositoryException e) { + // should not occur + log.error("Internal error while building Event: ()", e.getMessage()); + continue; + } + + boolean isForNode = (type == Event.NODE_ADDED + || type == Event.NODE_REMOVED || type == Event.NODE_MOVED); + + try { + if (isForNode) { + eventId = uriResolver.getNodeIdAfterEvent(href, + sessionInfo, type == Event.NODE_REMOVED); + } else { + eventId = uriResolver.getPropertyId(href, sessionInfo); + } + } catch (RepositoryException e) { + if (isForNode) { + eventId = idFactory.createNodeId((String) null, eventPath); + } else { + try { + eventId = idFactory.createPropertyId( + idFactory.createNodeId((String) null, + eventPath.getAncestor(1)), + eventPath.getName()); + } catch (RepositoryException e1) { + log.warn("Unable to build event itemId: {}", + e.getMessage()); + } + } + } + + String parentHref = Text.getRelativeParent(href, 1, true); + try { + parentId = uriResolver.getNodeId(parentHref, sessionInfo); + } catch (RepositoryException e) { + log.warn("Unable to build event parentId: {}", e.getMessage()); + } + } + + if (userId == null) { + // user id not retrieved from container + userId = DomUtil.getChildTextTrim(evElem, ObservationConstants.N_EVENTUSERID); + } + + events.add(new EventImpl(eventId, eventPath, parentId, type, userId, evElem, + getNamePathResolver(sessionInfo), getQValueFactory())); + } + + return events; + } + + @Override + public Map getRegisteredNamespaces(SessionInfo sessionInfo) throws RepositoryException { + ReportInfo info = new ReportInfo(JcrRemotingConstants.REPORT_REGISTERED_NAMESPACES, ItemResourceConstants.NAMESPACE); + HttpReport request = null; + try { + request = new HttpReport(uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()), info); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + Document doc = request.getResponseBodyAsDocument(response.getEntity()); + Map namespaces = new HashMap(); + if (doc != null) { + Element rootElement = doc.getDocumentElement(); + ElementIterator nsElems = DomUtil.getChildren(rootElement, JcrRemotingConstants.XML_NAMESPACE, ItemResourceConstants.NAMESPACE); + while (nsElems.hasNext()) { + Element elem = nsElems.nextElement(); + String prefix = DomUtil.getChildText(elem, JcrRemotingConstants.XML_PREFIX, ItemResourceConstants.NAMESPACE); + String uri = DomUtil.getChildText(elem, JcrRemotingConstants.XML_URI, ItemResourceConstants.NAMESPACE); + // default namespace + if (prefix == null && uri == null) { + prefix = uri = ""; + } + // any other uri must not be null + if (uri != null) { + namespaces.put(prefix, uri); + // TODO: not correct since nsRegistry is retrieved from each session + nsCache.add(prefix, uri); + } else { + log.error("Invalid prefix / uri pair: " + prefix + " -> " + uri); + } + } + } + return namespaces; + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public String getNamespaceURI(SessionInfo sessionInfo, String prefix) + throws RepositoryException { + try { + return nsCache.getURI(prefix); + } catch (NamespaceException e) { + // refresh namespaces and try again + getRegisteredNamespaces(sessionInfo); + return nsCache.getURI(prefix); + } + } + + @Override + public String getNamespacePrefix(SessionInfo sessionInfo, String uri) + throws RepositoryException { + try { + return nsCache.getPrefix(uri); + } catch (NamespaceException e) { + // refresh namespaces and try again + getRegisteredNamespaces(sessionInfo); + return nsCache.getPrefix(uri); + } + } + + @Override + public void registerNamespace(SessionInfo sessionInfo, String prefix, String uri) throws RepositoryException { + // make sure we have them all + getRegisteredNamespaces(sessionInfo); + + Map namespaces = new HashMap(nsCache.getNamespaces()); + // add new pair that needs to be registered. + namespaces.put(prefix, uri); + + internalSetNamespaces(sessionInfo, namespaces); + // adjust internal mappings: + // TODO: not correct since nsRegistry is retrieved from each session + nsCache.add(prefix, uri); + } + + @Override + public void unregisterNamespace(SessionInfo sessionInfo, String uri) throws RepositoryException { + // make sure we have them all + getRegisteredNamespaces(sessionInfo); + + String prefix = nsCache.getPrefix(uri); + Map namespaces = new HashMap(nsCache.getNamespaces()); + // remove pair that needs to be unregistered + namespaces.remove(prefix); + + internalSetNamespaces(sessionInfo, namespaces); + // adjust internal mappings: + nsCache.remove(prefix, uri); + } + + /** + * + * @param sessionInfo + * @param namespaces + * @throws NamespaceException + * @throws UnsupportedRepositoryOperationException + * @throws AccessDeniedException + * @throws RepositoryException + */ + private void internalSetNamespaces(SessionInfo sessionInfo, Map namespaces) throws RepositoryException { + DavPropertySet setProperties = new DavPropertySet(); + setProperties.add(createNamespaceProperty(namespaces)); + + HttpProppatch request = null; + try { + String uri = uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()); + + request = new HttpProppatch(uri, setProperties, new DavPropertyNameSet()); + initMethod(request, sessionInfo, true); + + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo) throws RepositoryException { + HttpReport request = null; + try { + ReportInfo info = new ReportInfo(JcrRemotingConstants.REPORT_NODETYPES, ItemResourceConstants.NAMESPACE); + info.setContentElement(DomUtil.createElement(DomUtil.createDocument(), NodeTypeConstants.XML_REPORT_ALLNODETYPES, ItemResourceConstants.NAMESPACE)); + + String workspaceUri = uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()); + request = new HttpReport(workspaceUri, info); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + Document reportDoc = request.getResponseBodyAsDocument(response.getEntity()); + return retrieveQNodeTypeDefinitions(sessionInfo, reportDoc); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo, Name[] nodetypeNames) throws RepositoryException { + // in order to avoid individual calls for every nodetype, retrieve + // the complete set from the server (again). + return getQNodeTypeDefinitions(sessionInfo); + } + + @Override + public void registerNodeTypes(SessionInfo sessionInfo, QNodeTypeDefinition[] nodeTypeDefinitions, boolean allowUpdate) throws RepositoryException { + HttpProppatch request = null; + try { + DavPropertySet setProperties = new DavPropertySet(); + setProperties.add(createRegisterNodeTypesProperty(sessionInfo, nodeTypeDefinitions, allowUpdate)); + String uri = uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()); + request = new HttpProppatch(uri, setProperties, new DavPropertyNameSet()); + initMethod(request, sessionInfo, true); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public void unregisterNodeTypes(SessionInfo sessionInfo, Name[] nodeTypeNames) throws RepositoryException { + HttpProppatch request = null; + try { + DavPropertySet setProperties = new DavPropertySet(); + setProperties.add(createUnRegisterNodeTypesProperty(sessionInfo, nodeTypeNames)); + String uri = uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName()); + request = new HttpProppatch(uri, setProperties, new DavPropertyNameSet()); + initMethod(request, sessionInfo, true); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public void createWorkspace(SessionInfo sessionInfo, String name, String srcWorkspaceName) throws RepositoryException { + if (srcWorkspaceName != null) { + throw new UnsupportedOperationException("JCR-2003. Implementation missing"); + } + + HttpMkworkspace request = null; + try { + request = new HttpMkworkspace(uriResolver.getWorkspaceUri(name)); + initMethod(request, sessionInfo, true); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public void deleteWorkspace(SessionInfo sessionInfo, String name) throws RepositoryException { + HttpDelete request = null; + try { + request = new HttpDelete(uriResolver.getWorkspaceUri(name)); + initMethod(request, sessionInfo, true); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + /** + * Compute the repository URI (while dealing with trailing / and port number + * defaulting) + */ + public static URI computeRepositoryUri(String uri) throws URISyntaxException { + URI repositoryUri = URI.create((uri.endsWith("/")) ? uri : uri + "/"); + // workaround for JCR-3228: normalize default port numbers because of + // the weak URI matching code elsewhere (the remote server is unlikely + // to include the port number in URIs when it's the default for the + // protocol) + boolean useDefaultPort = ("http".equalsIgnoreCase(repositoryUri.getScheme()) && repositoryUri.getPort() == 80) + || (("https".equalsIgnoreCase(repositoryUri.getScheme()) && repositoryUri.getPort() == 443)); + if (useDefaultPort) { + repositoryUri = new URI(repositoryUri.getScheme(), repositoryUri.getUserInfo(), repositoryUri.getHost(), -1, + repositoryUri.getPath(), repositoryUri.getQuery(), repositoryUri.getFragment()); + } + + return repositoryUri; + } + + public HttpResponse executeRequest(SessionInfo sessionInfo, HttpUriRequest request) throws IOException, RepositoryException { + return getClient(sessionInfo).execute(request, getContext(sessionInfo)); + } + + /** + * + * @param sessionInfo + * @param reportDoc + * @return + * @throws RepositoryException + */ + private Iterator retrieveQNodeTypeDefinitions(SessionInfo sessionInfo, Document reportDoc) throws RepositoryException { + ElementIterator it = DomUtil.getChildren(reportDoc.getDocumentElement(), NodeTypeConstants.NODETYPE_ELEMENT, null); + List ntDefs = new ArrayList(); + NamePathResolver resolver = getNamePathResolver(sessionInfo); + while (it.hasNext()) { + ntDefs.add(DefinitionUtil.createQNodeTypeDefinition(it.nextElement(), resolver, getQValueFactory())); + } + // refresh node type definitions map + synchronized (nodeTypeDefinitions) { + nodeTypeDefinitions.clear(); + for (Object ntDef : ntDefs) { + QNodeTypeDefinition def = (QNodeTypeDefinition) ntDef; + nodeTypeDefinitions.put(def.getName(), def); + } + } + return ntDefs.iterator(); + } + + private DavProperty> createRegisterNodeTypesProperty(SessionInfo sessionInfo, QNodeTypeDefinition[] nodeTypeDefinitions, final boolean allowUpdate) throws IOException { + // create xml elements for both cnd and allow update value. + List val = new ArrayList(); + + StringWriter sw = new StringWriter(); + CompactNodeTypeDefWriter writer = new CompactNodeTypeDefWriter(sw, new NamespaceResolverImpl(sessionInfo), true); + writer.write(Arrays.asList(nodeTypeDefinitions)); + writer.close(); + + final String cnd = sw.toString(); + val.add(new XmlSerializable() { + public Element toXml(Document document) { + Element cndElem = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + JcrRemotingConstants.XML_CND); + DomUtil.setText(cndElem, cnd); + return cndElem; + } + }); + val.add(new XmlSerializable() { + public Element toXml(Document document) { + Element allowElem = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + JcrRemotingConstants.XML_ALLOWUPDATE); + DomUtil.setText(allowElem, Boolean.toString(allowUpdate)); + return allowElem; + } + }); + + return new DefaultDavProperty>(JcrRemotingConstants.JCR_NODETYPES_CND_LN, val, ItemResourceConstants.NAMESPACE, false); + } + + private DavProperty> createUnRegisterNodeTypesProperty(SessionInfo sessionInfo, Name[] nodeTypeNames) throws IOException, RepositoryException { + NamePathResolver resolver = getNamePathResolver(sessionInfo); + List val = new ArrayList(); + for (Name ntName : nodeTypeNames) { + final String jcrName = resolver.getJCRName(ntName); + val.add(new XmlSerializable() { + public Element toXml(Document document) { + Element ntNameElem = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + JcrRemotingConstants.XML_NODETYPENAME); + org.w3c.dom.Text txt = document.createTextNode(jcrName); + ntNameElem.appendChild(txt); + return ntNameElem; + } + }); + } + return new DefaultDavProperty>(JcrRemotingConstants.JCR_NODETYPES_CND_LN, val, ItemResourceConstants.NAMESPACE, false); + } + + private static DavProperty> createValuesProperty(Value[] jcrValues) { + // convert the specified jcr values to a xml-serializable value + List val = new ArrayList(); + for (final Value jcrValue : jcrValues) { + val.add(new XmlSerializable() { + public Element toXml(Document document) { + try { + return ValueUtil.valueToXml(jcrValue, document); + } catch (RepositoryException e) { + throw new RuntimeException(e); + } + } + }); + } + return new DefaultDavProperty>(JcrRemotingConstants.JCR_VALUES_LN, val, ItemResourceConstants.NAMESPACE, false); + } + + private static DavProperty> createNamespaceProperty(final Map namespaces) { + // convert the specified namespace to a xml-serializable value + List val = new ArrayList(); + for (final String prefix : namespaces.keySet()) { + val.add(new XmlSerializable() { + + public Element toXml(Document document) { + Element nsElem = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + JcrRemotingConstants.XML_NAMESPACE); + Element prefixElem = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + JcrRemotingConstants.XML_PREFIX); + org.w3c.dom.Text txt = document.createTextNode(prefix); + prefixElem.appendChild(txt); + + final String uri = namespaces.get(prefix); + Element uriElem = document.createElementNS(JcrRemotingConstants.NS_URI, JcrRemotingConstants.NS_PREFIX + ":" + JcrRemotingConstants.XML_URI); + org.w3c.dom.Text txt2 = document.createTextNode(uri); + uriElem.appendChild(txt2); + + nsElem.appendChild(prefixElem); + nsElem.appendChild(uriElem); + + return nsElem; + } + }); + } + return new DefaultDavProperty>(JcrRemotingConstants.JCR_NAMESPACES_LN, val, ItemResourceConstants.NAMESPACE, false); + } + + private static DavProperty> createNodeTypeProperty(String localName, String[] ntNames) { + // convert the specified node type names to a xml-serializable value + List val = new ArrayList(); + for (final String ntName : ntNames) { + val.add(new XmlSerializable() { + public Element toXml(Document document) { + return NodeTypeUtil.ntNameToXml(ntName, document); + } + }); + } + return new DefaultDavProperty>(localName, val, ItemResourceConstants.NAMESPACE, false); + } + + private Set getDavComplianceClasses(SessionInfo sessionInfo) throws RepositoryException { + if (this.remoteDavComplianceClasses == null) { + HttpOptions request = new HttpOptions(uriResolver.getWorkspaceUri(sessionInfo.getWorkspaceName())); + try { + HttpResponse response = executeRequest(sessionInfo, request); + int status = response.getStatusLine().getStatusCode(); + if (status != DavServletResponse.SC_OK) { + throw new DavException(status); + } + this.remoteDavComplianceClasses = request.getDavComplianceClasses(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + request.releaseConnection(); + } + } + return this.remoteDavComplianceClasses; + } + + private boolean isDavClass3(SessionInfo sessionInfo) { + try { + return getDavComplianceClasses(sessionInfo).contains("3"); + } catch (RepositoryException ex) { + log.warn("failure to obtain OPTIONS response", ex); + return false; + } + } + + private static String obtainAbsolutePathFromUri(String uri) { + try { + java.net.URI u = new java.net.URI(uri); + StringBuilder sb = new StringBuilder(); + sb.append(u.getRawPath()); + if (u.getRawQuery() != null) { + sb.append("?").append(u.getRawQuery()); + } + return sb.toString(); + } catch (java.net.URISyntaxException ex) { + log.warn("parsing " + uri, ex); + return uri; + } + } + + private static String[] obtainAbsolutePathsFromUris(String[] uris) { + if (uris == null) { + return null; + } else { + String result[] = new String[uris.length]; + + for (int i = 0; i < result.length; i++) { + result[i] = obtainAbsolutePathFromUri(uris[i]); + } + return result; + } + } + + //------------------------------------------------< Inner Class 'Batch' >--- + private class BatchImpl implements Batch { + + private final SessionInfo sessionInfo; + private final ItemId targetId; + private final List requests = new ArrayList(); + private final NamePathResolver resolver; + + private String batchId; + + private boolean isConsumed = false; + private boolean clear = false; + + private BatchImpl(ItemId targetId, SessionInfo sessionInfo) throws RepositoryException { + this.targetId = targetId; + this.sessionInfo = sessionInfo; + this.resolver = getNamePathResolver(sessionInfo); + } + + private HttpClient start() throws RepositoryException { + checkConsumed(); + String uri = getItemUri(targetId, sessionInfo); + HttpLock request = null; + try { + // start special 'lock' + request = new HttpLock(uri, new org.apache.jackrabbit.webdav.lock.LockInfo(TransactionConstants.LOCAL, TransactionConstants.TRANSACTION, null, + INFINITE_TIMEOUT, true)); + initMethod(request, sessionInfo, true); + + HttpClient client = getClient(sessionInfo); + HttpResponse response = client.execute(request,getContext(sessionInfo)); + if (response.getStatusLine().getStatusCode() == DavServletResponse.SC_PRECONDITION_FAILED) { + throw new InvalidItemStateException("Unable to persist transient changes."); + } + request.checkSuccess(response); + + batchId = request.getLockToken(response); + + return client; + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + private void end(HttpClient client, boolean commit) throws RepositoryException { + checkConsumed(); + + String uri = getItemUri(targetId, sessionInfo); + HttpUnlock request = null; + try { + // make sure the lock initially created is removed again on the + // server, asking the server to persist the modifications + request = new HttpUnlock(uri, batchId); + initMethod(request, sessionInfo, true); + + // in contrast to standard UNLOCK, the tx-unlock provides a + // request body. + request.setEntity(XmlEntity.create(new TransactionInfo(commit))); + HttpResponse response = client.execute(request, getContext(sessionInfo)); + request.checkSuccess(response); + if (sessionInfo instanceof SessionInfoImpl) { + ((SessionInfoImpl) sessionInfo).setLastBatchId(batchId); + } + if (clear) { + clearItemUriCache(sessionInfo); + } + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + // release UNLOCK method + request.releaseConnection(); + } + } + } + + private void dispose() { + requests.clear(); + isConsumed = true; + } + + private void checkConsumed() { + if (isConsumed) { + throw new IllegalStateException("Batch has already been consumed."); + } + } + + private boolean isEmpty() { + return requests.isEmpty(); + } + + private Iterator requests() { + return requests.iterator(); + } + + //----------------------------------------------------------< Batch >--- + @Override + public void addNode(NodeId parentId, Name nodeName, Name nodetypeName, String uuid) throws RepositoryException { + checkConsumed(); + try { + // TODO: TOBEFIXED. WebDAV does not allow MKCOL for existing resource -> problem with SNS + // use fake name instead (see also #importXML) + Name fakeName = getNameFactory().create(Name.NS_DEFAULT_URI, UUID.randomUUID().toString()); + String uri = getItemUri(parentId, fakeName, sessionInfo); + HttpMkcol request = new HttpMkcol(uri); + + // build 'sys-view' for the node to create and append it as request body + Document body = DomUtil.createDocument(); + BatchUtils.createNodeElement(body, nodeName, nodetypeName, uuid, resolver); + request.setEntity(XmlEntity.create(body)); + + requests.add(request); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (ParserConfigurationException e) { + throw new RepositoryException(e); + } + } + + @Override + public void addProperty(NodeId parentId, Name propertyName, QValue value) throws RepositoryException { + checkConsumed(); + String uri = getItemUri(parentId, propertyName, sessionInfo); + + HttpPut request = new HttpPut(uri); + request.setHeader(HEADER_CONTENT_TYPE, JcrValueType.contentTypeFromType(value.getType())); + request.setEntity(getEntity(value)); + requests.add(request); + } + + @Override + public void addProperty(NodeId parentId, Name propertyName, QValue[] values) throws RepositoryException { + checkConsumed(); + // TODO: avoid usage of the ValuesProperty. specially for binary props. + // TODO: replace by a multipart-POST + try { + String uri = getItemUri(parentId, propertyName, sessionInfo); + Value[] jcrValues = new Value[values.length]; + for (int i = 0; i < values.length; i++) { + jcrValues[i] = ValueFormat.getJCRValue(values[i], resolver, valueFactory); + } + DavProperty> vp = createValuesProperty(jcrValues); + HttpPut request = new HttpPut(uri); + request.setEntity(XmlEntity.create(vp)); + + requests.add(request); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + + @Override + public void setValue(PropertyId propertyId, QValue value) throws RepositoryException { + checkConsumed(); + if (value == null) { + // setting property value to 'null' is identical to a removal + remove(propertyId); + } else { + HttpEntity ent = getEntity(value); + String uri = getItemUri(propertyId, sessionInfo); + // TODO: use PUT in order to avoid the ValuesProperty-PROPPATCH call. + // TODO: actually not quite correct for PROPPATCH assert that prop really exists. + HttpPut request = new HttpPut(uri); + request.setHeader(HEADER_CONTENT_TYPE, JcrValueType.contentTypeFromType(value.getType())); + request.setEntity(ent); + requests.add(request); + } + } + + @Override + public void setValue(PropertyId propertyId, QValue[] values) throws RepositoryException { + checkConsumed(); + if (values == null) { + // setting property value to 'null' is identical to a removal + remove(propertyId); + } else { + // TODO: use multipart-POST instead of ValuesProperty + DavPropertySet setProperties = new DavPropertySet(); + // SPI values must be converted to jcr values + Value[] jcrValues = new Value[values.length]; + for (int i = 0; i < values.length; i++) { + jcrValues[i] = ValueFormat.getJCRValue(values[i], resolver, valueFactory); + } + setProperties.add(createValuesProperty(jcrValues)); + try { + String uri = getItemUri(propertyId, sessionInfo); + HttpProppatch request = new HttpProppatch(uri, setProperties, new DavPropertyNameSet()); + requests.add(request); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + } + + private HttpEntity getEntity(QValue value) throws RepositoryException { + // SPI value must be converted to jcr value + int type = value.getType(); + String contentType = JcrValueType.contentTypeFromType(type); + HttpEntity ent; + switch (type) { + case PropertyType.NAME: + case PropertyType.PATH: + String str = ValueFormat.getJCRString(value, resolver); + ent = new StringEntity(str, ContentType.create(contentType, "UTF-8")); + break; + case PropertyType.BINARY: + InputStream in = value.getStream(); + ent = new InputStreamEntity(in, ContentType.create(contentType)); + break; + default: + str = value.getString(); + ent = new StringEntity(str, ContentType.create(contentType, "UTF-8")); + break; + } + return ent; + } + + @Override + public void remove(ItemId itemId) throws RepositoryException { + checkConsumed(); + String uri = getItemUri(itemId, sessionInfo); + HttpDelete request = new HttpDelete(uri); + + requests.add(request); + if (itemId.getPath() == null) { + clear = true; + } + } + + @Override + public void reorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) throws RepositoryException { + checkConsumed(); + try { + String uri = getItemUri(parentId, sessionInfo); + String srcUri = getItemUri(srcNodeId, sessionInfo); + String srcSegment = Text.getName(srcUri, true); + + Position p; + if (beforeNodeId == null) { + // move src to the end + p = new Position(OrderingConstants.XML_LAST); + } else { + // insert src before the targetSegment + String beforeUri = getItemUri(beforeNodeId, sessionInfo); + String targetSegment = Text.getName(beforeUri, true); + p = new Position(OrderingConstants.XML_BEFORE, targetSegment); + } + OrderPatch op = new OrderPatch(OrderingConstants.ORDERING_TYPE_CUSTOM, new OrderPatch.Member(srcSegment, p)); + HttpOrderpatch request = new HttpOrderpatch(uri, op); + requests.add(request); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + + @Override + public void setMixins(NodeId nodeId, Name[] mixinNodeTypeIds) throws RepositoryException { + checkConsumed(); + try { + DavPropertySet setProperties; + DavPropertyNameSet removeProperties; + if (mixinNodeTypeIds == null || mixinNodeTypeIds.length == 0) { + setProperties = new DavPropertySet(); + removeProperties = new DavPropertyNameSet(); + removeProperties.add(JcrRemotingConstants.JCR_MIXINNODETYPES_LN, ItemResourceConstants.NAMESPACE); + } else { + String[] ntNames = new String[mixinNodeTypeIds.length]; + for (int i = 0; i < mixinNodeTypeIds.length; i++) { + ntNames[i] = resolver.getJCRName(mixinNodeTypeIds[i]); + } + setProperties = new DavPropertySet(); + setProperties.add(createNodeTypeProperty(JcrRemotingConstants.JCR_MIXINNODETYPES_LN, ntNames)); + removeProperties = new DavPropertyNameSet(); + } + + String uri = getItemUri(nodeId, sessionInfo); + HttpProppatch request = new HttpProppatch(uri, setProperties, removeProperties); + + requests.add(request); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + + @Override + public void setPrimaryType(NodeId nodeId, Name primaryNodeTypeName) throws RepositoryException { + checkConsumed(); + try { + DavPropertySet setProperties = new DavPropertySet(); + setProperties.add(createNodeTypeProperty(JcrRemotingConstants.JCR_PRIMARYNODETYPE_LN, new String[] {resolver.getJCRName(primaryNodeTypeName)})); + + String uri = getItemUri(nodeId, sessionInfo); + HttpProppatch request = new HttpProppatch(uri, setProperties, new DavPropertyNameSet()); + + requests.add(request); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + + @Override + public void move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + checkConsumed(); + String uri = getItemUri(srcNodeId, sessionInfo); + String destUri = getItemUri(destParentNodeId, destName, sessionInfo); + if (isDavClass3(sessionInfo)) { + destUri = obtainAbsolutePathFromUri(destUri); + } + HttpMove request = new HttpMove(uri, destUri, false); + + requests.add(request); + clear = true; + } + + @Override + public void setTree(NodeId parentId, Tree tree) throws RepositoryException { + checkConsumed(); + + if (!(tree instanceof DocumentTree)) { + throw new RepositoryException("Invalid tree implementation " + tree.getClass().getName()); + } + try { + // TODO: TOBEFIXED. WebDAV does not allow MKCOL for existing resource -> problem with SNS + // use fake name instead (see also #importXML) + Name fakeName = getNameFactory().create(Name.NS_DEFAULT_URI, UUID.randomUUID().toString()); + String uri = getItemUri(parentId, fakeName, sessionInfo); + HttpMkcol request = new HttpMkcol(uri); + + request.setEntity(XmlEntity.create(((DocumentTree) tree).toDocument())); + + requests.add(request); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + } + + //----------------------------------------------< NamespaceResolverImpl >--- + /** + * NamespaceResolver implementation that uses a sessionInfo to determine + * namespace mappings either from cache or from the server. + */ + private class NamespaceResolverImpl implements NamespaceResolver { + + private final SessionInfo sessionInfo; + + /** + * Creates a new namespace resolver using the given session info. + * + * @param sessionInfo the session info to contact the repository. + */ + private NamespaceResolverImpl(SessionInfo sessionInfo) { + this.sessionInfo = sessionInfo; + } + + @Override + public String getURI(String prefix) throws NamespaceException { + try { + return getNamespaceURI(sessionInfo, prefix); + } catch (RepositoryException e) { + String msg = "Error retrieving namespace uri"; + throw new NamespaceException(msg, e); + } + } + + @Override + public String getPrefix(String uri) throws NamespaceException { + try { + return getNamespacePrefix(sessionInfo, uri); + } catch (RepositoryException e) { + String msg = "Error retrieving namespace prefix"; + throw new NamespaceException(msg, e); + } + } + } + + //---------------------------------------------< IdentifierResolverImpl >--- + private class IdentifierResolverImpl implements IdentifierResolver { + + private final SessionInfo sessionInfo; + + private IdentifierResolverImpl(SessionInfo sessionInfo) { + this.sessionInfo = sessionInfo; + } + + private Path buildPath(String uniqueID) throws RepositoryException { + String uri = uriResolver.getItemUri(getIdFactory().createNodeId(uniqueID), sessionInfo.getWorkspaceName(), sessionInfo); + return uriResolver.getQPath(uri, sessionInfo); + } + + private Path resolvePath(String jcrPath) throws RepositoryException { + return ((SessionInfoImpl) sessionInfo).getNamePathResolver().getQPath(jcrPath); + } + + @Override + public Path getPath(String identifier) throws MalformedPathException { + try { + int pos = identifier.indexOf('/'); + if (pos == -1) { + // unique id identifier + return buildPath(identifier); + } else if (pos == 0) { + // jcr-path identifier + return resolvePath(identifier); + } else { + Path p1 = buildPath(identifier.substring(0, pos)); + Path p2 = resolvePath(identifier.substring(pos)); + return getPathFactory().create(p1, p2, true); + } + } catch (RepositoryException e) { + throw new MalformedPathException(identifier); + } + } + + @Override + public void checkFormat(String identifier) throws MalformedPathException { + // cannot be determined. assume ok. + } + } + //-----------------------------------------------< NamePathResolverImpl >--- + /** + * Implements a namespace resolver based on a session info. + */ + private class NamePathResolverImpl implements NamePathResolver { + + private final NameResolver nResolver; + private final PathResolver pResolver; + + private NamePathResolverImpl(SessionInfo sessionInfo) { + NamespaceResolver nsResolver = new NamespaceResolverImpl(sessionInfo); + nResolver = new ParsingNameResolver(getNameFactory(), nsResolver); + IdentifierResolver idResolver = new IdentifierResolverImpl(sessionInfo); + pResolver = new ParsingPathResolver(getPathFactory(), nResolver, idResolver); + } + + private NamePathResolverImpl(NamespaceResolver nsResolver) { + nResolver = new ParsingNameResolver(getNameFactory(), nsResolver); + pResolver = new ParsingPathResolver(getPathFactory(), nResolver); + } + + @Override + public Name getQName(String jcrName) throws IllegalNameException, NamespaceException { + return nResolver.getQName(jcrName); + } + + @Override + public String getJCRName(Name qName) throws NamespaceException { + return nResolver.getJCRName(qName); + } + + @Override + public Path getQPath(String path) throws MalformedPathException, IllegalNameException, NamespaceException { + return pResolver.getQPath(path); + } + + @Override + public Path getQPath(String path, boolean normalizeIdentifier) throws MalformedPathException, IllegalNameException, NamespaceException { + return pResolver.getQPath(path, normalizeIdentifier); + } + + @Override + public String getJCRPath(Path path) throws NamespaceException { + return pResolver.getJCRPath(path); + } + } + + /** + * Namespace Cache + */ + private static class NamespaceCache extends AbstractNamespaceResolver { + + private final HashMap prefixToURI = new HashMap(); + private final HashMap uriToPrefix = new HashMap(); + + public Map getNamespaces() { + return new HashMap(prefixToURI); + } + + public void add(String prefix, String uri) { + prefixToURI.put(prefix, uri); + uriToPrefix.put(uri, prefix); + } + + public void remove(String prefix, String uri) { + prefixToURI.remove(prefix); + uriToPrefix.remove(uri); + } + + //----------------------------------------------< NamespaceResolver >--- + + @Override + public String getURI(String prefix) throws NamespaceException { + String uri = prefixToURI.get(prefix); + if (uri != null) { + return uri; + } else { + throw new NamespaceException(prefix + ": is not a registered namespace prefix."); + } + } + + @Override + public String getPrefix(String uri) throws NamespaceException { + String prefix = uriToPrefix.get(uri); + if (prefix != null) { + return prefix; + } else { + throw new NamespaceException(uri + ": is not a registered namespace uri."); + } + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/SessionInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/SessionInfoImpl.java new file mode 100644 index 00000000000..725d46c36b7 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/SessionInfoImpl.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.spi2dav; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.Arrays; +import java.util.UUID; + +/** + * SessionInfoImpl... + */ +public class SessionInfoImpl extends org.apache.jackrabbit.spi.commons.SessionInfoImpl { + + private final CredentialsWrapper credentials; + private final Set sessionScopedTokens = new HashSet(); + + // a globally unique URI identifying this session + private final String sessionIdentifier = "urn:uuid:" + UUID.randomUUID(); + + private String lastBatchId; + private NamePathResolver resolver; + + SessionInfoImpl(CredentialsWrapper creds, String workspaceName) { + this.credentials = creds; + + super.setWorkspacename(workspaceName); + } + + //--------------------------------------------------------< SessionInfo >--- + /** + * {@inheritDoc} + */ + @Override + public String getUserID() { + return credentials.getUserId(); + } + + //-------------------------------------------------------------------------- + + CredentialsWrapper getCredentials() { + return credentials; + } + + String getSessionIdentifier() { + return sessionIdentifier; + } + + /** + * Returns the id of the most recently submitted batch or null + * it no batch has been submitted yet. + * + * @return the batch id of the most recently submitted batch. + */ + String getLastBatchId() { + return lastBatchId; + } + + /** + * Sets the id of the most recently submitted batch. + * + * @param batchId the batch id. + */ + void setLastBatchId(String batchId) { + lastBatchId = batchId; + } + + NamePathResolver getNamePathResolver() { + return resolver; + } + + void setNamePathResolver(NamePathResolver resolver) { + this.resolver = resolver; + } + + /** + * @return All tokens that this session info needs to communicate with + * the DAV-server. This includes all tokens obtained through both LOCK + * request(s) as well as those tokens that have been added to the + * corresponding JCR session. + * Note, that the sessionScopedTokens are only used for + * communication with the DAV server and are never exposed through the + * JCR API for they belong to session-scoped locks. + */ + Set getAllLockTokens() { + Set s = new HashSet(Arrays.asList(getLockTokens())); + s.addAll(sessionScopedTokens); + return Collections.unmodifiableSet(s); + } + + void addLockToken(String token, boolean sessionScoped) { + if (sessionScoped) { + sessionScopedTokens.add(token); + } else { + super.addLockToken(token); + } + } + + void removeLockToken(String token, boolean sessionScoped) { + if (sessionScoped) { + sessionScopedTokens.remove(token); + } else { + super.removeLockToken(token); + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/Spi2davRepositoryServiceFactory.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/Spi2davRepositoryServiceFactory.java new file mode 100644 index 00000000000..ae051c8457e --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/Spi2davRepositoryServiceFactory.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2dav; + +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi.commons.identifier.IdFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; + +/** + * This {@link RepositoryServiceFactory} implementation is responsible + * for creating {@link RepositoryServiceImpl} instances. + */ +public class Spi2davRepositoryServiceFactory implements RepositoryServiceFactory { + + /** + * Mandatory configuration parameter: It's value is expected to specify the + * URI of the JCR server implementation. + */ + public static final String PARAM_REPOSITORY_URI = "org.apache.jackrabbit.spi2dav.uri"; + + /** + * Optional configuration parameter: It's value is expected to be an instance + * of {@link IdFactory}. If missing {@link IdFactoryImpl} is used. + */ + public static final String PARAM_ID_FACTORY = "org.apache.jackrabbit.spi2dav.IdFactory"; + + /** + * Optional configuration parameter: It's value is expected to be an instance + * of {@link NameFactory}. If missing {@link NameFactoryImpl} is used. + */ + public static final String PARAM_NAME_FACTORY = "org.apache.jackrabbit.spi2dav.NameFactory"; + + /** + * Optional configuration parameter: It's value is expected to be an instance + * of {@link PathFactory}. If missing {@link PathFactoryImpl} is used. + */ + public static final String PARAM_PATH_FACTORY = "org.apache.jackrabbit.spi2dav.PathFactory"; + + /** + * Optional configuration parameter: It's value is expected to be an instance + * of {@link QValueFactory}. If missing {@link QValueFactoryImpl} is used. + */ + public static final String PARAM_QVALUE_FACTORY = "org.apache.jackrabbit.spi2dav.QValueFactory"; + + /** + * Optional configuration parameter: It's value determines the size of the + * {@link ItemInfoCache} cache. Defaults to {@link ItemInfoCacheImpl#DEFAULT_CACHE_SIZE}. + */ + public static final String PARAM_ITEMINFO_CACHE_SIZE = "org.apache.jackrabbit.spi2dav.ItemInfoCacheSize"; + + /** + * Optional configuration parameter: It's value defines the + * maximumConnectionsPerHost value on the HttpClient configuration and + * must be an int greater than zero. + */ + public static final String PARAM_MAX_CONNECTIONS = "org.apache.jackrabbit.spi2dav.MaxConnections"; + + public RepositoryService createRepositoryService(Map parameters) throws RepositoryException { + if (parameters == null) { + throw new RepositoryException("Parameter " + PARAM_REPOSITORY_URI + " missing"); + } + + String uri; + if (parameters.get(PARAM_REPOSITORY_URI) == null) { + throw new RepositoryException("Parameter " + PARAM_REPOSITORY_URI + " missing"); + } + else { + uri = parameters.get(PARAM_REPOSITORY_URI).toString(); + } + + IdFactory idFactory; + Object param = parameters.get(PARAM_ID_FACTORY); + if (param != null && param instanceof IdFactory) { + idFactory = (IdFactory) param; + } else { + idFactory = IdFactoryImpl.getInstance(); + } + + NameFactory nameFactory; + param = parameters.get(PARAM_NAME_FACTORY); + if (param != null && param instanceof NameFactory) { + nameFactory = (NameFactory) param; + } else { + nameFactory = NameFactoryImpl.getInstance(); + } + + PathFactory pathFactory; + param = parameters.get(PARAM_PATH_FACTORY); + if (param != null && param instanceof PathFactory) { + pathFactory = (PathFactory) param; + } else { + pathFactory = PathFactoryImpl.getInstance(); + } + + QValueFactory vFactory; + param = parameters.get(PARAM_QVALUE_FACTORY); + if (param != null && param instanceof QValueFactory) { + vFactory = (QValueFactory) param; + } else { + vFactory = QValueFactoryImpl.getInstance(); + } + + int itemInfoCacheSize = ItemInfoCacheImpl.DEFAULT_CACHE_SIZE; + param = parameters.get(PARAM_ITEMINFO_CACHE_SIZE); + if (param != null) { + try { + itemInfoCacheSize = Integer.parseInt(param.toString()); + } catch (NumberFormatException e) { + // ignore, use default + } + } + + // max connections config + int maximumHttpConnections = 0; + param = parameters.get(PARAM_MAX_CONNECTIONS); + if (param != null) { + try { + maximumHttpConnections = Integer.parseInt(param.toString()); + } catch ( NumberFormatException e ) { + // using default + } + } + + if (maximumHttpConnections > 0) { + return new RepositoryServiceImpl(uri, idFactory, nameFactory, pathFactory, vFactory, itemInfoCacheSize, maximumHttpConnections); + } else { + return new RepositoryServiceImpl(uri, idFactory, nameFactory, pathFactory, vFactory, itemInfoCacheSize); + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/URIResolver.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/URIResolver.java new file mode 100644 index 00000000000..86d32367471 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/URIResolver.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.spi2dav; + +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.SessionInfo; + +import javax.jcr.RepositoryException; + +/** + * URIResolver used to build ItemIds from URIs. + */ +interface URIResolver { + + Path getQPath(String uri, SessionInfo sessionInfo) throws RepositoryException; + + NodeId getNodeId(String uri, SessionInfo sessionInfo) throws RepositoryException; + + NodeId getNodeIdAfterEvent(String uri, SessionInfo sessionInfo, boolean nodeIsGone) throws RepositoryException; + + PropertyId getPropertyId(String uri, SessionInfo sessionInfo) throws RepositoryException; +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/URIResolverImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/URIResolverImpl.java new file mode 100644 index 00000000000..ff2c3f37c89 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2dav/URIResolverImpl.java @@ -0,0 +1,369 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2dav; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.HttpPropfind; +import org.apache.jackrabbit.webdav.client.methods.HttpReport; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Document; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +/** + * URIResolverImpl... + */ +class URIResolverImpl implements URIResolver { + + private final URI repositoryUri; + private final RepositoryServiceImpl service; + private final Document domFactory; + + // TODO: to-be-fixed. uri/id-caches don't get updated + // for each workspace a separate idUri-cache is created + private final Map idURICaches = new HashMap(); + + URIResolverImpl(URI repositoryUri, RepositoryServiceImpl service, Document domFactory) { + this.repositoryUri = repositoryUri; + this.service = service; + this.domFactory = domFactory; + } + + private IdURICache getCache(String workspaceName) { + if (idURICaches.containsKey(workspaceName)) { + return idURICaches.get(workspaceName); + } else { + IdURICache c = new IdURICache(getWorkspaceUri(workspaceName)); + idURICaches.put(workspaceName, c); + return c; + } + } + + String getRepositoryUri() { + return repositoryUri.toASCIIString(); + } + + String getWorkspaceUri(String workspaceName) { + String workspaceUri = getRepositoryUri(); + if (workspaceName != null) { + workspaceUri += Text.escape(workspaceName); + } + return workspaceUri; + } + + String getRootItemUri(String workspaceName) { + return getWorkspaceUri(workspaceName) + Text.escapePath(JcrRemotingConstants.ROOT_ITEM_RESOURCEPATH); + } + + String getItemUri(ItemId itemId, String workspaceName, SessionInfo sessionInfo) + throws RepositoryException { + IdURICache cache = getCache(workspaceName); + // check if uri is available from cache + if (cache.containsItemId(itemId)) { + return cache.getUri(itemId); + } else { + StringBuffer uriBuffer = new StringBuffer(); + + Path path = itemId.getPath(); + String uniqueID = itemId.getUniqueID(); + + // resolver uuid part + if (uniqueID != null) { + ItemId uuidId = (path == null) ? itemId : service.getIdFactory().createNodeId(uniqueID); + if (path != null && cache.containsItemId(uuidId)) { + // append uri of parent node, that is already cached + uriBuffer.append(cache.getUri(uuidId)); + } else { + // try to request locate-by-uuid report to build the uri + ReportInfo rInfo = new ReportInfo(JcrRemotingConstants.REPORT_LOCATE_BY_UUID, ItemResourceConstants.NAMESPACE); + rInfo.setContentElement(DomUtil.hrefToXml(uniqueID, domFactory)); + + HttpReport request = null; + try { + String wspUri = getWorkspaceUri(workspaceName); + request = new HttpReport(wspUri, rInfo); + + HttpResponse response = service.executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatus ms = request.getResponseBodyAsMultiStatus(response); + + if (ms.getResponses().length == 1) { + String absoluteUri = resolve(wspUri, ms.getResponses()[0].getHref()); + uriBuffer.append(absoluteUri); + cache.add(absoluteUri, uuidId); + } else { + throw new ItemNotFoundException("Cannot identify item with uniqueID " + uniqueID); + } + + } catch (IOException e) { + throw new RepositoryException(e.getMessage()); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + } else { + // start build uri from root-item + uriBuffer.append(getRootItemUri(workspaceName)); + } + // resolve relative-path part unless it denotes the root-item + if (path != null && !path.denotesRoot()) { + String jcrPath = service.getNamePathResolver(sessionInfo).getJCRPath(path); + if (!path.isAbsolute() && !uriBuffer.toString().endsWith("/")) { + uriBuffer.append("/"); + } + uriBuffer.append(Text.escapePath(jcrPath)); + } + String itemUri = uriBuffer.toString(); + if (!cache.containsItemId(itemId)) { + cache.add(itemUri, itemId); + } + return itemUri; + } + } + + /** + * Resolve the given href obtained from multistatus against base URI + */ + private static String resolve(String wspUri, String href) throws RepositoryException { + try { + java.net.URI base = new java.net.URI(wspUri); + java.net.URI rel = new java.net.URI(href); + return base.resolve(rel).toString(); + } + catch (URISyntaxException ex) { + throw new RepositoryException(ex); + } + } + + protected NodeId buildNodeId(NodeId parentId, String baseUri, MultiStatusResponse response, + String workspaceName, NamePathResolver resolver) throws RepositoryException { + IdURICache cache = getCache(workspaceName); + + NodeId nodeId; + DavPropertySet propSet = response.getProperties(DavServletResponse.SC_OK); + + String uniqueID = service.getUniqueID(propSet); + if (uniqueID != null) { + nodeId = service.getIdFactory().createNodeId(uniqueID); + } else { + Name qName = service.getQName(propSet, resolver); + if (NameConstants.ROOT.equals(qName)) { + nodeId = service.getIdFactory().createNodeId((String) null, service.getPathFactory().getRootPath()); + } else { + int index = service.getIndex(propSet); + nodeId = service.getIdFactory().createNodeId(parentId, service.getPathFactory().create(qName, index)); + } + } + // cache + cache.add(resolve(baseUri, response.getHref()), nodeId); + return nodeId; + } + + PropertyId buildPropertyId(NodeId parentId, MultiStatusResponse response, + String workspaceName, NamePathResolver resolver) throws RepositoryException { + IdURICache cache = getCache(workspaceName); + if (cache.containsUri(response.getHref())) { + ItemId id = cache.getItemId(response.getHref()); + if (!id.denotesNode()) { + return (PropertyId) id; + } + } + + try { + DavPropertySet propSet = response.getProperties(DavServletResponse.SC_OK); + Name name = resolver.getQName(propSet.get(JcrRemotingConstants.JCR_NAME_LN, ItemResourceConstants.NAMESPACE).getValue().toString()); + PropertyId propertyId = service.getIdFactory().createPropertyId(parentId, name); + + cache.add(response.getHref(), propertyId); + return propertyId; + } catch (NameException e) { + throw new RepositoryException(e); + } + } + + void clearCacheEntries(ItemId itemId, SessionInfo sessionInfo) { + IdURICache cache = getCache(sessionInfo.getWorkspaceName()); + if (cache.containsItemId(itemId)) { + cache.remove(itemId); + } + } + + void clearCacheEntries(SessionInfo sessionInfo) { + IdURICache cache = getCache(sessionInfo.getWorkspaceName()); + cache.clear(); + } + + private static boolean isSameURI(String uri1, String uri2) { + return getCleanURI(uri1).equals(getCleanURI(uri2)); + + } + + private static String getCleanURI(String uri) { + if (uri.endsWith("/")) { + return uri.substring(0, uri.length() - 1); + } else { + return uri; + } + } + + private NodeId getNodeId(String uri, SessionInfo sessionInfo, boolean nodeIsGone) throws RepositoryException { + + IdURICache cache = getCache(sessionInfo.getWorkspaceName()); + if (cache.containsUri(uri)) { + // id has been accessed before and is cached + ItemId id = cache.getItemId(uri); + if (id.denotesNode()) { + return (NodeId) id; + } + } + + if (nodeIsGone) { + throw new RepositoryException("Can't reconstruct nodeId from URI when the remote node is gone."); + } + + // retrieve parentId from cache or by recursive calls + NodeId parentId; + if (isSameURI(uri, getRootItemUri(sessionInfo.getWorkspaceName()))) { + parentId = null; + } else { + String parentUri = Text.getRelativeParent(uri, 1, true); + parentId = getNodeId(parentUri, sessionInfo, false); + } + + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(JcrRemotingConstants.JCR_UUID_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_NAME_LN, ItemResourceConstants.NAMESPACE); + nameSet.add(JcrRemotingConstants.JCR_INDEX_LN, ItemResourceConstants.NAMESPACE); + HttpPropfind request = null; + try { + request = new HttpPropfind(uri, nameSet, DavConstants.DEPTH_0); + + HttpResponse response = service.executeRequest(sessionInfo, request); + if (response.getStatusLine().getStatusCode() != DavServletResponse.SC_MULTI_STATUS) { + throw new ItemNotFoundException("Unable to retrieve the node with id " + uri + ", response status was: " + + response.getStatusLine().getStatusCode()); + } + MultiStatusResponse[] responses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (responses.length != 1) { + throw new ItemNotFoundException("Unable to retrieve the node with id " + uri); + } + return buildNodeId(parentId, uri, responses[0], sessionInfo.getWorkspaceName(), service.getNamePathResolver(sessionInfo)); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + //-------------------------------------------------------< URI resolver >--- + /** + * @inheritDoc + */ + public Path getQPath(String uri, SessionInfo sessionInfo) throws RepositoryException { + String rootUri = getRootItemUri(sessionInfo.getWorkspaceName()); + String jcrPath; + if (uri.startsWith(rootUri)) { + jcrPath = uri.substring(rootUri.length()); + } else { + // todo: probably rather an error? + jcrPath = uri; + } + try { + return service.getNamePathResolver(sessionInfo).getQPath(Text.unescape(jcrPath)); + } catch (NameException e) { + throw new RepositoryException(e); + } + } + + /** + * @inheritDoc + */ + public NodeId getNodeId(String uri, SessionInfo sessionInfo) throws RepositoryException { + return getNodeId(uri, sessionInfo, false); + } + + /** + * @inheritDoc + */ + public NodeId getNodeIdAfterEvent(String uri, SessionInfo sessionInfo, boolean nodeIsGone) throws RepositoryException { + return getNodeId(uri, sessionInfo, nodeIsGone); + } + + /** + * @inheritDoc + */ + public PropertyId getPropertyId(String uri, SessionInfo sessionInfo) throws RepositoryException { + IdURICache cache = getCache(sessionInfo.getWorkspaceName()); + ItemId id = cache.getItemId(uri); + if (id != null) { + if (!id.denotesNode()) { + return (PropertyId) id; + } + } + + // separate parent uri and property JCRName + String parentUri = Text.getRelativeParent(uri, 1, true); + // make sure propName is unescaped + String propName = Text.unescape(Text.getName(uri, true)); + // retrieve parent node id + NodeId parentId = getNodeId(parentUri, sessionInfo, false); + // build property id + try { + Name name = service.getNamePathResolver(sessionInfo).getQName(propName); + PropertyId propertyId = service.getIdFactory().createPropertyId(parentId, name); + cache.add(uri, propertyId); + + return propertyId; + } catch (NameException e) { + throw new RepositoryException(e); + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/BatchReadConfig.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/BatchReadConfig.java new file mode 100644 index 00000000000..0b0cf94c804 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/BatchReadConfig.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.spi2davex; + +import javax.jcr.NamespaceException; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; + +/** + * BatchReadConfig defines if and how deep child item + * information should be retrieved, when accessing a Node. + * The configuration is based on path. + */ +public interface BatchReadConfig { + + /** + * Return the depth for the given path. + * + * @param path + * @param resolver + * @return -1 if all child infos should be return or any value greater + * than -1 if only parts of the subtree should be returned. If there is no + * matching configuration entry some implementation specific default depth + * will be returned. + */ + public int getDepth(Path path, PathResolver resolver) throws NamespaceException; + +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ChildInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ChildInfoImpl.java new file mode 100644 index 00000000000..e831f185b69 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ChildInfoImpl.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.spi2davex; + +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +/** ChildInfoImpl... */ +public class ChildInfoImpl implements ChildInfo { + + /** + * The name of this child info. + */ + private final Name name; + + /** + * The unique id for this child info or null if it does not + * have a unique id. + */ + private final String uniqueId; + + /** + * 1-based index of this child info. + */ + private final int index; + + private int hashCode; + + /** + * Creates a new serializable ChildInfoImpl. + * + * @param name the name of the child node. + * @param uniqueId the unique id of the child node or null. + * @param index the index of the child node. + */ + public ChildInfoImpl(Name name, String uniqueId, int index) { + if (name == null || index < Path.INDEX_UNDEFINED) { + throw new IllegalArgumentException(); + } + this.name = name; + this.uniqueId = uniqueId; + this.index = (index == Path.INDEX_UNDEFINED) ? Path.INDEX_DEFAULT : index; + } + + /** + * {@inheritDoc} + */ + public Name getName() { + return name; + } + + /** + * {@inheritDoc} + */ + public String getUniqueID() { + return uniqueId; + } + + /** + * {@inheritDoc} + */ + public int getIndex() { + return index; + } + + //-------------------------------------------------------------< Object >--- + /** + * @see Object#hashCode() + */ + @Override + public int hashCode() { + // build hashCode (format: ///) + if (hashCode == 0) { + StringBuffer sb = new StringBuffer(); + sb.append(name.toString()); + sb.append("/"); + sb.append(index); + sb.append("/"); + if (uniqueId != null) { + sb.append(uniqueId); + } + hashCode = sb.toString().hashCode(); + } + return hashCode; + } + + /** + * @see Object#equals(Object) + */ + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (object instanceof ChildInfoImpl) { + ChildInfoImpl ci = (ChildInfoImpl) object; + boolean sameUID = (uniqueId == null) ? ci.uniqueId == null : uniqueId.equals(ci.uniqueId); + return sameUID && name.equals(ci.name) && index == ci.index; + } + return false; + } + + /** + * @see Object#toString() + */ + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(name.toString()); + sb.append(" : ").append(index); + sb.append(" : ").append((uniqueId == null) ? "-" : uniqueId); + return sb.toString(); + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/HttpPost.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/HttpPost.java new file mode 100755 index 00000000000..703031b88c6 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/HttpPost.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.spi2davex; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.client.methods.BaseDavRequest; + +public class HttpPost extends BaseDavRequest { + + public HttpPost(URI uri) { + super(uri); + } + + public HttpPost(String uri) { + super(URI.create(uri)); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_POST; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK || statusCode == DavServletResponse.SC_NO_CONTENT + || statusCode == DavServletResponse.SC_CREATED; + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ItemInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ItemInfoImpl.java new file mode 100644 index 00000000000..93884e5d062 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ItemInfoImpl.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.spi2davex; + +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; + +import javax.jcr.RepositoryException; +import java.io.Serializable; + +/** + * ItemInfoImpl is a base class for ItemInfo + * implementations. + */ +public abstract class ItemInfoImpl implements ItemInfo, Serializable { + + /** + * The path of this item info. + */ + private final Path path; + + /** + * Flag indicating whether this is a node or a property info. + */ + private final boolean isNode; + + /** + * Creates a new ItemInfo. + * + * @param path the path to this item. + * @param isNode if this item is a node. + * @throws javax.jcr.RepositoryException + */ + public ItemInfoImpl(Path path, boolean isNode) throws RepositoryException { + if (path == null) { + throw new RepositoryException(); + } + this.path = path; + this.isNode = isNode; + } + + /** + * {@inheritDoc} + */ + public Name getName() { + return path.getName(); + } + + /** + * {@inheritDoc} + */ + public boolean denotesNode() { + return isNode; + } + + /** + * {@inheritDoc} + */ + public Path getPath() { + return path; + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ItemInfoJSONHandler.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ItemInfoJSONHandler.java new file mode 100644 index 00000000000..6d5c39ce467 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ItemInfoJSONHandler.java @@ -0,0 +1,362 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2davex; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.commons.json.JsonHandler; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.util.StringCache; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ItemInfoJSONHandler... + */ +class ItemInfoJsonHandler implements JsonHandler { + + private static Logger log = LoggerFactory.getLogger(ItemInfoJsonHandler.class); + + private static final String LEAF_NODE_HINT = "::NodeIteratorSize"; + + private final List itemInfos; + private final NamePathResolver resolver; + private final String rootURI; + + private final QValueFactoryImpl vFactory; + private final PathFactory pFactory; + private final IdFactory idFactory; + + private boolean expectingHintValue = false; + + private Name name; + private int index = Path.INDEX_DEFAULT; + + // temp. property state + private int propertyType; + private boolean multiValuedProperty = false; + private List propValues = new ArrayList(); + + private Stack nodeInfos = new Stack(); + + private Stack> propInfoLists = new Stack>(); + + ItemInfoJsonHandler(NamePathResolver resolver, NodeInfo nInfo, + String rootURI, + QValueFactoryImpl vFactory, + PathFactory pFactory, + IdFactory idFactory) { + this.resolver = resolver; + this.rootURI = rootURI; + + this.vFactory = vFactory; + this.pFactory = pFactory; + this.idFactory = idFactory; + + itemInfos = new ArrayList(); + itemInfos.add(nInfo); + nodeInfos.push(nInfo); + propInfoLists.push(new ArrayList(8)); + } + + public void object() throws IOException { + if (name != null) { + try { + NodeInfo current = getCurrentNodeInfo(); + Path relPath = pFactory.create(name, index); + NodeId id = idFactory.createNodeId(current.getId(), relPath); + Path currentPath = current.getPath(); + Path p = pFactory.create(currentPath, relPath, true); + NodeInfo nInfo = new NodeInfoImpl(id, p); + nodeInfos.push(nInfo); + propInfoLists.push(new ArrayList(8)); + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } + } + } + + public void endObject() throws IOException { + try { + NodeInfoImpl nInfo = (NodeInfoImpl) nodeInfos.pop(); + List props = propInfoLists.pop(); + // all required information to create a node info should now be gathered + nInfo.setPropertyInfos(props.toArray(new PropertyInfoImpl[props.size()]), idFactory); + NodeInfo parent = getCurrentNodeInfo(); + if (parent != null) { + if (nInfo.getPath().getAncestor(1).equals(parent.getPath())) { + ChildInfo ci = new ChildInfoImpl(nInfo.getName(), nInfo.getUniqueID(), nInfo.getIndex()); + ((NodeInfoImpl) parent).addChildInfo(ci); + } else { + log.debug("NodeInfo '"+ nInfo.getPath() + "' out of hierarchy. Parent path = " + parent.getPath()); + } + } + if (nInfo.isCompleted()) { + itemInfos.addAll(props); + itemInfos.add(nInfo); + } else { + log.debug("Incomplete NodeInfo '"+ nInfo.getPath() + "' -> Only present as ChildInfo with its parent."); + } + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } finally { + // reset all node-related handler state + name = null; + index = Path.INDEX_DEFAULT; + } + } + + public void array() throws IOException { + multiValuedProperty = true; + propValues.clear(); + } + + public void endArray() throws IOException { + try { + if (propertyType == PropertyType.UNDEFINED) { + if (propValues.isEmpty()) { + // make sure that type is set for mv-properties with empty value array. + propertyType = vFactory.retrieveType(getValueURI()); + } else { + propertyType = propValues.get(0).getType(); + } + } + // create multi-valued property info + NodeInfoImpl parent = getCurrentNodeInfo(); + Path p = pFactory.create(parent.getPath(), name, true); + PropertyId id = idFactory.createPropertyId(parent.getId(), name); + PropertyInfoImpl propInfo = new PropertyInfoImpl(id, p, propertyType, propValues.toArray(new QValue[propValues.size()])); + propInfo.checkCompleted(); + getCurrentPropInfos().add(propInfo); + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } finally { + // reset property-related handler state + propertyType = PropertyType.UNDEFINED; + multiValuedProperty = false; + propValues.clear(); + name = null; + } + } + + public void key(String key) throws IOException { + expectingHintValue = false; + try { + if (key.equals(LEAF_NODE_HINT)) { + expectingHintValue = true; + // TODO: remember name of hint if there will be additional types of hints + name = null; + } else if (key.startsWith(":")) { + expectingHintValue = true; + // either + // : : "PropertyTypeName" + // or + // : : + //name = resolver.getQName(key.substring(1)); + name = resolver.getQName(StringCache.fromCacheOrNew(key.substring(1))); + index = Path.INDEX_DEFAULT; + } else if (key.endsWith("]")) { + // sns-node name + int pos = key.lastIndexOf('['); + //name = resolver.getQName(key.substring(0, pos)); + name = resolver.getQName(StringCache.fromCacheOrNew(key.substring(0, pos))); + propertyType = PropertyType.UNDEFINED; + index = Integer.parseInt(key.substring(pos + 1, key.length() - 1)); + } else { + // either node or property + name = resolver.getQName(StringCache.cache(key)); + index = Path.INDEX_DEFAULT; + } + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * there is currently one special string-value hint: + * + * : : "PropertyTypeName" + * + * @param value The value. + * @throws IOException + */ + public void value(String value) throws IOException { + if (expectingHintValue) { + // : : "PropertyTypeName" + propertyType = PropertyType.valueFromName(value); + return; + } + try { + QValue v; + switch (propertyType) { + case PropertyType.UNDEFINED: + if (!NameConstants.JCR_UUID.equals(name)) { + value = StringCache.cache(value); + } + v = vFactory.create(value, PropertyType.STRING); + break; + case PropertyType.NAME: + v = vFactory.create(resolver.getQName(value)); + break; + case PropertyType.PATH: + v = vFactory.create(resolver.getQPath(value)); + break; + default: + v = vFactory.create(value, propertyType); + } + value(v); + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } + } + + public void value(boolean value) throws IOException { + if (expectingHintValue) { + // there are currently no special boolean value hints: + return; + } + try { + value(vFactory.create(value)); + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } + } + + /** + * there are currently 2 types of special long value hints: + * + * a) ::NodeIteratorSize : 0 + * ==> denotes the current node as leaf node + * + * b) : : + * + * @param value The value. + * @throws IOException + */ + public void value(long value) throws IOException { + if (expectingHintValue) { + if (name == null) { + // ::NodeIteratorSize : 0 + NodeInfoImpl parent = getCurrentNodeInfo(); + if (parent != null) { + parent.markAsLeafNode(); + } + } else { + // : : + propertyType = PropertyType.BINARY; + try { + int indx = (!multiValuedProperty) ? -1 : propValues.size(); + value(vFactory.create(value, getValueURI(), indx)); + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } + } + return; + } + try { + value(vFactory.create(value)); + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } + } + + public void value(double value) throws IOException { + if (expectingHintValue) { + // currently no special double value pair -> ignore + return; + } + try { + value(vFactory.create(value)); + } catch (RepositoryException e) { + throw new IOException(e.getMessage(), e); + } + } + + //-------------------------------------------------------------------------- + /** + * + * @param value + * @throws RepositoryException + */ + private void value(QValue value) throws RepositoryException { + if (!multiValuedProperty) { + try { + if (propertyType == PropertyType.UNDEFINED) { + propertyType = value.getType(); + } + // create single-valued property info + NodeInfoImpl parent = getCurrentNodeInfo(); + Path p = pFactory.create(parent.getPath(), name, true); + PropertyId id = idFactory.createPropertyId(parent.getId(), name); + PropertyInfoImpl propInfo = new PropertyInfoImpl(id, p, propertyType, value); + propInfo.checkCompleted(); + // add property info to current list, will be processed on endObject() event + getCurrentPropInfos().add(propInfo); + } finally { + // reset property-related handler state + propertyType = PropertyType.UNDEFINED; + multiValuedProperty = false; + propValues.clear(); + name = null; + expectingHintValue = false; + } + } else { + // multi-valued property + // add value to current list, will be processed on endArray() event + propValues.add(value); + } + } + + Iterator getItemInfos() { + return Collections.unmodifiableList(itemInfos).iterator(); + } + + private NodeInfoImpl getCurrentNodeInfo() { + return (nodeInfos.isEmpty()) ? null : (NodeInfoImpl) nodeInfos.peek(); + } + + private List getCurrentPropInfos() { + return (propInfoLists.isEmpty()) ? null : propInfoLists.peek(); + } + + private String getValueURI() throws RepositoryException { + Path propertyPath = pFactory.create(getCurrentNodeInfo().getPath(), name, true); + StringBuffer sb = new StringBuffer(rootURI); + sb.append(Text.escapePath(resolver.getJCRPath(propertyPath))); + return sb.toString(); + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/NodeInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/NodeInfoImpl.java new file mode 100644 index 00000000000..a8660a6d768 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/NodeInfoImpl.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.spi2davex; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.name.NameConstants; + +/** + * NodeInfoImpl... + */ +public class NodeInfoImpl extends ItemInfoImpl implements NodeInfo { + + // data deduced from property values + private NodeId id; + private Name primaryNodeTypeName; + private Name[] mixinNodeTypeNames = Name.EMPTY_ARRAY; + + private final List propertyIds = new ArrayList(8); + private List childInfos = null; + + /** + * Creates a new NodeInfo. + * + * @param id The node id. + * @param path the path to this item. + * @throws javax.jcr.RepositoryException If an error occurs. + */ + public NodeInfoImpl(NodeId id, Path path) throws RepositoryException { + super(path, true); + this.id = id; + } + + //-----------------------------------------------------------< NodeInfo >--- + public NodeId getId() { + return id; + } + + public int getIndex() { + return getPath().getNormalizedIndex(); + } + + public Name getNodetype() { + return primaryNodeTypeName; + } + + public Name[] getMixins() { + return mixinNodeTypeNames; + } + + public PropertyId[] getReferences() { + return new PropertyId[0]; + } + + public Iterator getPropertyIds() { + return propertyIds.iterator(); + } + + public Iterator getChildInfos() { + return (childInfos == null) ? null : childInfos.iterator(); + } + + //-------------------------------------------------------------------------- + void setPropertyInfos(PropertyInfoImpl[] propInfos, IdFactory idFactory) throws RepositoryException { + boolean resolveUUID = false; + for (PropertyInfoImpl propInfo : propInfos) { + Name pn = propInfo.getId().getName(); + if (NameConstants.JCR_UUID.equals(pn)) { + id = idFactory.createNodeId(propInfo.getValues()[0].getString()); + resolveUUID = true; + } else if (NameConstants.JCR_PRIMARYTYPE.equals(pn)) { + primaryNodeTypeName = propInfo.getValues()[0].getName(); + } else if (NameConstants.JCR_MIXINTYPES.equals(pn)) { + QValue[] vs = propInfo.getValues(); + Name[] mixins = new Name[vs.length]; + for (int i = 0; i < vs.length; i++) { + mixins[i] = vs[i].getName(); + } + mixinNodeTypeNames = mixins; + } + } + + propertyIds.clear(); + for (PropertyInfoImpl propInfo : propInfos) { + if (resolveUUID) { + propInfo.setId(idFactory.createPropertyId(id, propInfo.getName())); + } + propertyIds.add(propInfo.getId()); + } + + } + + void addChildInfo(ChildInfo childInfo) { + if (childInfos == null) { + childInfos = new ArrayList(); + } + childInfos.add(childInfo); + } + + void markAsLeafNode() { + childInfos = Collections.emptyList(); + } + + boolean isCompleted() { + return (id != null && primaryNodeTypeName != null && !propertyIds.isEmpty()); + } + + String getUniqueID() { + if (id.getUniqueID() != null && id.getPath() == null) { + return id.getUniqueID(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/PropertyInfoImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/PropertyInfoImpl.java new file mode 100644 index 00000000000..a772edb08c9 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/PropertyInfoImpl.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.spi2davex; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QValue; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +/** + * PropertyInfoImpl... + */ +public class PropertyInfoImpl extends ItemInfoImpl implements PropertyInfo { + + private final boolean multiValued; + + private PropertyId id; + private int propertyType; + private QValue[] values = QValue.EMPTY_ARRAY; + + public PropertyInfoImpl(PropertyId id, Path path, int propertyType, + QValue value) throws RepositoryException { + super(path, false); + this.id = id; + this.propertyType = propertyType; + multiValued = false; + values = new QValue[]{value}; + } + + public PropertyInfoImpl(PropertyId id, Path path, int propertyType, QValue[] values) throws RepositoryException { + super(path, false); + this.id = id; + this.propertyType = propertyType; + this.values = values; + multiValued = true; + } + + //-------------------------------------------------------< PropertyInfo >--- + public PropertyId getId() { + return id; + } + + public int getType() { + if (propertyType == PropertyType.UNDEFINED) { + // in case of empty-value-array of a multivalued property the type + // must always be set. + propertyType = getValues()[0].getType(); + } + return propertyType; + } + + public boolean isMultiValued() { + return multiValued; + } + + public QValue[] getValues() { + return values; + } + + //-------------------------------------------------------------------------- + void setId(PropertyId id) { + this.id = id; + } + + void checkCompleted() throws RepositoryException { + if (id == null) { + throw new RepositoryException("Incomplete PropertyInfo: id missing."); + } + if (propertyType == PropertyType.UNDEFINED) { + throw new RepositoryException("Incomplete PropertyInfo: missing type of property."); + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/QValueFactoryImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/QValueFactoryImpl.java new file mode 100644 index 00000000000..ac1edbf81d3 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/QValueFactoryImpl.java @@ -0,0 +1,568 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2davex; + +import static org.apache.jackrabbit.webdav.DavConstants.HEADER_ETAG; +import static org.apache.jackrabbit.webdav.DavConstants.HEADER_LAST_MODIFIED; + +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.commons.webdav.ValueUtil; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.value.AbstractQValue; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.spi2dav.ItemResourceConstants; +import org.apache.jackrabbit.util.TransientFileFactory; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.xml.parsers.ParserConfigurationException; + +import java.io.ByteArrayInputStream; +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.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Map; + +/** + * ValueFactoryImpl... + */ +class QValueFactoryImpl extends org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl { + + /** + * A dummy value for calling the constructor of AbstractQValue + */ + private static final Object DUMMY_VALUE = new Serializable() { + private static final long serialVersionUID = -5667366239976271493L; + }; + + /** + * empty array + */ + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + static final int NO_INDEX = -1; + + private final ValueLoader loader; + private final ValueFactory vf; + + public QValueFactoryImpl() { + this(null, null); + } + + QValueFactoryImpl(NamePathResolver resolver, ValueLoader loader) { + this.loader = loader; + vf = new ValueFactoryQImpl(this, resolver); + } + + /** + * Create a BINARY QValue with the given length and the given uri used + * to retrieve the value. + * + * @param length Length of the binary value. + * @param uri Uri from which the the binary value can be accessed. + * @param index The index of the value within the values array. + * @return a new BINARY QValue. + */ + QValue create(long length, String uri, int index) { + if (loader == null) { + throw new IllegalStateException(); + } + return new BinaryQValue(length, uri, index); + } + + /** + * + * @param uri The Uri from which the type info can be retrieved. + * @return the type of the property with the given uri. + * @throws IOException If an error occurs. + * @throws RepositoryException If an error occurs. + */ + int retrieveType(String uri) throws IOException, RepositoryException { + return loader.loadType(uri); + } + + //--------------------------------------------------------< Inner Class >--- + + /** + * BinaryQValue represents a binary Value which is + * backed by a resource or byte[]. Unlike BinaryValue it has no + * state, i.e. the getStream() method always returns a fresh + * InputStream instance. + */ + private class BinaryQValue extends AbstractQValue implements ValueLoader.Target { + + private static final long serialVersionUID = 2736654000266713469L; + + /** + * max size for keeping tmp data in memory + */ + private static final int MAX_BUFFER_SIZE = 0x10000; + + /** + * underlying file + */ + private transient File file; + + /** + * flag indicating if this instance represents a temporary value + * whose dynamically allocated resources can be explicitly freed on + * {@link #discard()}. + */ + private transient boolean temp; + + /** + * Buffer for small-sized data + */ + private byte[] buffer; + + private Map headers; + + /** + * URI to retrieve the value from + */ + private final String uri; + private final long length; + private final int index; + private boolean initialized = true; + + private BinaryQValue(long length, String uri, int index) { + super(DUMMY_VALUE, PropertyType.BINARY); + this.length = length; + this.uri = uri; + this.index = index; + initialized = false; + } + + /** + * Creates a new BinaryQValue instance from an + * InputStream. The contents of the stream is spooled + * to a temporary file or to a byte buffer if its size is smaller than + * {@link #MAX_BUFFER_SIZE}. + *

        + * The temp parameter governs whether dynamically allocated + * resources will be freed explicitly on {@link #discard()}. Note that any + * dynamically allocated resources (temp file/buffer) will be freed + * implicitly once this instance has been gc'ed. + * + * @param in stream to be represented as a BinaryQValue instance + * @param temp flag indicating whether this instance represents a + * temporary value whose resources can be explicitly freed + * on {@link #discard()}. + * @throws IOException if an error occurs while reading from the stream or + * writing to the temporary file + */ + private void init(InputStream in, boolean temp) throws IOException { + byte[] spoolBuffer = new byte[0x2000]; + int read; + int len = 0; + OutputStream out = null; + File spoolFile = null; + try { + while ((read = in.read(spoolBuffer)) > 0) { + if (out != null) { + // spool to temp file + out.write(spoolBuffer, 0, read); + len += read; + } else if (len + read > BinaryQValue.MAX_BUFFER_SIZE) { + // threshold for keeping data in memory exceeded; + // create temp file and spool buffer contents + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + spoolFile = fileFactory.createTransientFile("bin", null, null); + out = new FileOutputStream(spoolFile); + out.write(buffer, 0, len); + out.write(spoolBuffer, 0, read); + buffer = null; + len += read; + } else { + // reallocate new buffer and spool old buffer contents + if (buffer == null) { + buffer = EMPTY_BYTE_ARRAY; + } + byte[] newBuffer = new byte[len + read]; + System.arraycopy(buffer, 0, newBuffer, 0, len); + System.arraycopy(spoolBuffer, 0, newBuffer, len, read); + buffer = newBuffer; + len += read; + } + } + } finally { + in.close(); + if (out != null) { + out.close(); + } + } + + if (spoolFile == null && buffer == null) { + // input stream was empty -> initialize an empty binary value + this.temp = false; + buffer = EMPTY_BYTE_ARRAY; + } else { + // init vars + file = spoolFile; + this.temp = temp; + } + initialized = true; + } + + //---------------------------------------------------------< QValue >--- + + /** + * Returns the length of this BinaryQValue. + * + * @return The length, in bytes, of this BinaryQValue, + * or -1L if the length can't be determined. + * @see QValue#getLength() + */ + @Override + public long getLength() { + if (file != null) { + // this instance is backed by a 'real' file + if (file.exists()) { + return file.length(); + } else { + return -1; + } + } else if (buffer != null) { + // this instance is backed by an in-memory buffer + return buffer.length; + } else { + // value has not yet been read from the server. + return length; + } + } + + /** + * @see QValue#getStream() + */ + public InputStream getStream() throws RepositoryException { + // if the value has not yet been loaded -> retrieve it first in + // order to make sure that either 'file' or 'buffer' is set. + if (file == null && buffer == null) { + try { + loadBinary(); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + + // always return a 'fresh' stream + if (file != null) { + // this instance is backed by a 'real' file + try { + return new FileInputStream(file); + } catch (FileNotFoundException fnfe) { + throw new RepositoryException("file backing binary value not found", + fnfe); + } + } else { + return new ByteArrayInputStream(buffer); + } + } + + /** + * @see QValue#getName() + */ + @Override + public Name getName() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + /** + * @see QValue#getPath() + */ + @Override + public Path getPath() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + /** + * Frees temporarily allocated resources such as temporary file, buffer, etc. + * If this BinaryQValue is backed by a persistent resource + * calling this method will have no effect. + * @see QValue#discard() + */ + @Override + public void discard() { + if (!temp) { + // do nothing if this instance is not backed by temporarily + // allocated resource/buffer + return; + } + if (file != null) { + // this instance is backed by a temp file + file.delete(); + } else if (buffer != null) { + // this instance is backed by an in-memory buffer + buffer = EMPTY_BYTE_ARRAY; + } + } + + /** + * Resets the state of this value. a subsequent call to init() can be + * used to load the binary again. + * + * If this BinaryQValue is backed by a persistent resource + * calling this method will have no effect. + * @see QValue#discard() + */ + public void reset() { + if (!temp) { + // do nothing if this instance is not backed by temporarily + // allocated resource/buffer + return; + } + if (file != null) { + // this instance is backed by a temp file + file.delete(); + } + file = null; + buffer = null; + initialized = false; + } + + //-----------------------------------------------< java.lang.Object >--- + /** + * Returns a string representation of this BinaryQValue + * instance. The string representation of a resource backed value is + * the path of the underlying resource. If this instance is backed by an + * in-memory buffer the generic object string representation of the byte + * array will be used instead. + * + * @return A string representation of this BinaryQValue instance. + */ + @Override + public String toString() { + if (file != null) { + // this instance is backed by a 'real' file + return file.toString(); + } else if (buffer != null) { + // this instance is backed by an in-memory buffer + return buffer.toString(); + } else { + return super.toString(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BinaryQValue) { + BinaryQValue other = (BinaryQValue) obj; + + // Consider unequal urls as unequal values and both urls null as equal values + if (this.uri == null) { + return other.uri == null; + } + if (!this.uri.equals(other.uri)) { + return false; + } + + // Consider both uninitialized as equal values + if (!this.preInitialized() && !other.preInitialized()) { + return true; + } + + try { + // Initialized the one which is not + if (!this.preInitialized()) { + this.preInitialize(new String[] {HEADER_ETAG, HEADER_LAST_MODIFIED}); + } else if (!other.preInitialized()) { + other.preInitialize(new String[] {HEADER_ETAG, HEADER_LAST_MODIFIED}); + } + } catch (RepositoryException e) { + return false; + } catch (IOException e) { + return false; + } + + // If we have headers try to determine equality from them + if (headers != null && !headers.isEmpty()) { + + // Values are (un)equal if we have equal Etags + if (containKey(HEADER_ETAG, this.headers, other.headers)) { + return equalValue(HEADER_ETAG, this.headers, other.headers); + } + + // Values are unequal if we have different Last-modified values + if (containKey(HEADER_LAST_MODIFIED, this.headers, other.headers)) { + if (!equalValue(HEADER_LAST_MODIFIED, this.headers, other.headers)) { + return false; + } + } + + // Otherwise compare binaries + } else { + return ((file == null ? other.file == null : file.equals(other.file)) + && Arrays.equals(buffer, other.buffer)); + } + } + return false; + } + + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + @Override + public int hashCode() { + return 0; + } + + //---------------------------------------------------------------------- + + private synchronized void loadBinary() throws RepositoryException, IOException { + if (uri == null) { + throw new IllegalStateException(); + } + loader.loadBinary(uri, index, this); + } + + /** + * Load the header with the given names. If none of the named headers exist, load binary. + */ + private void preInitialize(String[] headerNames) throws IOException, RepositoryException { + headers = loader.loadHeaders(uri, headerNames); + if (headers.isEmpty()) { + loadBinary(); + } + } + + /** + * @return true if either initialized or headers have been + * loaded, false otherwise. + */ + private boolean preInitialized() { + return initialized || headers != null; + } + + /** + * @return true if both maps contain the same value for + * key, false otherwise. The + * key must not map to null in either + * map. + */ + private boolean equalValue(String key, Map map1, Map map2) { + return map1.get(key).equals(map2.get(key)); + } + + /** + * @return true if both maps contains the key, + * false otherwise. + */ + private boolean containKey(String key, Map map1, Map map2) { + return map1.containsKey(key) && map2.containsKey(key); + } + + //-----------------------------< Serializable >------------------------- + private void writeObject(ObjectOutputStream out) + throws IOException { + out.defaultWriteObject(); + // write hasFile marker + out.writeBoolean(file != null); + // then write file if necessary + if (file != null) { + byte[] buffer = new byte[4096]; + int bytes; + InputStream stream = new FileInputStream(file); + while ((bytes = stream.read(buffer)) >= 0) { + // Write a segment of the input stream + if (bytes > 0) { + // just to ensure that no 0 is written + out.writeInt(bytes); + out.write(buffer, 0, bytes); + } + } + // Write the end of stream marker + out.writeInt(0); + // close stream + stream.close(); + } + } + + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + boolean hasFile = in.readBoolean(); + if (hasFile) { + file = File.createTempFile("binary-qvalue", "bin"); + + OutputStream out = new FileOutputStream(file); + byte[] buffer = new byte[4096]; + for (int bytes = in.readInt(); bytes > 0; bytes = in.readInt()) { + if (buffer.length < bytes) { + buffer = new byte[bytes]; + } + in.readFully(buffer, 0, bytes); + out.write(buffer, 0, bytes); + } + out.close(); + } + // deserialized value is always temp + temp = true; + } + + //---------------------------------------------------------< Target >--- + public void setStream(InputStream in) throws IOException { + if (index == NO_INDEX) { + init(in, true); + } else { + // TODO: improve. jcr-server sends XML for multivalued properties + try { + Document doc = DomUtil.parseDocument(in); + Element prop = DomUtil.getChildElement(doc, JcrRemotingConstants.JCR_VALUES_LN, ItemResourceConstants.NAMESPACE); + DavProperty p = DefaultDavProperty.createFromXml(prop); + Value[] jcrVs = ValueUtil.valuesFromXml(p.getValue(), PropertyType.BINARY, vf); + init(jcrVs[index].getStream(), true); + } catch (RepositoryException e) { + throw new IOException(e.getMessage()); + } catch (SAXException e) { + throw new IOException(e.getMessage()); + } catch (ParserConfigurationException e) { + throw new IOException(e.getMessage()); + } + } + } + } +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/RepositoryServiceImpl.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/RepositoryServiceImpl.java new file mode 100644 index 00000000000..213d76968fd --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/RepositoryServiceImpl.java @@ -0,0 +1,1028 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2davex; + +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.commons.json.JsonParser; +import org.apache.jackrabbit.commons.json.JsonUtil; +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.commons.webdav.ValueUtil; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.identifier.IdFactoryImpl; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.tree.AbstractTree; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi2dav.ExceptionConverter; +import org.apache.jackrabbit.spi2dav.ItemResourceConstants; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.HttpPropfind; +import org.apache.jackrabbit.webdav.header.IfHeader; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RepositoryServiceImpl... + */ +public class RepositoryServiceImpl extends org.apache.jackrabbit.spi2dav.RepositoryServiceImpl { + + private static Logger log = LoggerFactory.getLogger(RepositoryServiceImpl.class); + + private static final String PARAM_DIFF = ":diff"; + private static final String PARAM_COPY = ":copy"; + private static final String PARAM_CLONE = ":clone"; + + private static final char SYMBOL_ADD_NODE = '+'; + private static final char SYMBOL_MOVE = '>'; + private static final char SYMBOL_REMOVE = '-'; + private static final char SYMBOL_SET_PROPERTY = '^'; + + private static final String ORDER_POSITION_LAST = "#last"; + private static final String ORDER_POSITION_BEFORE = "#before"; + + private static final DavPropertyName JCR_TYPE = + DavPropertyName.create(ItemResourceConstants.JCR_TYPE_LN, ItemResourceConstants.NAMESPACE); + + private static final DavPropertyName JCR_LENGTH = + DavPropertyName.create(ItemResourceConstants.JCR_LENGTH_LN, ItemResourceConstants.NAMESPACE); + + private static final DavPropertyName JCR_LENGTHS = + DavPropertyName.create(ItemResourceConstants.JCR_LENGTHS_LN, ItemResourceConstants.NAMESPACE); + + private static final DavPropertyName JCR_GET_STRING = + DavPropertyName.create(ItemResourceConstants.JCR_GET_STRING_LN, ItemResourceConstants.NAMESPACE); + + private static final DavPropertyNameSet LAZY_PROPERTY_NAME_SET = new DavPropertyNameSet(){{ + add(JCR_TYPE); + add(JCR_LENGTH); + add(JCR_LENGTHS); + add(JCR_GET_STRING); + }}; + + /** + * base uri to the extended jcr-server that can handle the GET and POST + * (or PATCH) requests sent by this service implementation. + */ + private final String jcrServerURI; + + /** + * the name of the default workspace or null. + * NOTE: with JCR-1842 the RepositoryConfiguration doesn't provide the + * default workspace name any more. In order to provide backwards + * compatibility with jcr-server < 1.5.0 the workspace name can be + * passed to the RepositoryService implementation. + */ + private final String defaultWorkspaceName; + + /** + * The configuration map used to determine the maximal depth of child + * items to be accessed upon a call to {@link #getNodeInfo(SessionInfo, NodeId)}. + */ + private final BatchReadConfig batchReadConfig; + + private final Map qvFactories = new HashMap(); + + /** + * Same as {@link #RepositoryServiceImpl(String, String, BatchReadConfig, int, int)} + * using null workspace name, {@link ItemInfoCacheImpl#DEFAULT_CACHE_SIZE} + * as size for the item cache and {@link #MAX_CONNECTIONS_DEFAULT} for the + * maximum number of connections on the client. + * + * @param jcrServerURI The server uri. + * @param batchReadConfig The batch read configuration. + * @throws RepositoryException If an exception occurs. + */ + public RepositoryServiceImpl(String jcrServerURI, BatchReadConfig batchReadConfig) throws RepositoryException { + this(jcrServerURI, null, batchReadConfig, ItemInfoCacheImpl.DEFAULT_CACHE_SIZE); + } + + /** + * Same as {@link #RepositoryServiceImpl(String, String, BatchReadConfig, int, int)} + * using {@link #MAX_CONNECTIONS_DEFAULT} for the maximum number of + * connections on the client. + * + * @param jcrServerURI The server uri. + * @param defaultWorkspaceName The default workspace name. + * @param batchReadConfig The batch read configuration. + * @param itemInfoCacheSize The size of the item info cache. + * @throws RepositoryException If an exception occurs. + */ + public RepositoryServiceImpl(String jcrServerURI, String defaultWorkspaceName, + BatchReadConfig batchReadConfig, int itemInfoCacheSize) throws RepositoryException { + this(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize, MAX_CONNECTIONS_DEFAULT); + } + + /** + * Creates a new instance of this repository service. + * + * @param jcrServerURI The server uri. + * @param defaultWorkspaceName The default workspace name. + * @param batchReadConfig The batch read configuration. + * @param itemInfoCacheSize The size of the item info cache. + * @param maximumHttpConnections maximumHttpConnections A int >0 defining + * the maximum number of connections per host to be configured on + * {@link org.apache.http.impl.conn.PoolingHttpClientConnectionManager#setDefaultMaxPerRoute(int)}. + * @throws RepositoryException If an exception occurs. + */ + public RepositoryServiceImpl(String jcrServerURI, String defaultWorkspaceName, + BatchReadConfig batchReadConfig, int itemInfoCacheSize, + int maximumHttpConnections) throws RepositoryException { + + super(jcrServerURI, IdFactoryImpl.getInstance(), NameFactoryImpl.getInstance(), + PathFactoryImpl.getInstance(), new QValueFactoryImpl(), itemInfoCacheSize, maximumHttpConnections); + + try { + URI repositoryUri = computeRepositoryUri(jcrServerURI); + this.jcrServerURI = repositoryUri.toString(); + } catch (URISyntaxException e) { + throw new RepositoryException(e); + } + + this.defaultWorkspaceName = defaultWorkspaceName; + if (batchReadConfig == null) { + this.batchReadConfig = new BatchReadConfig() { + public int getDepth(Path path, PathResolver resolver) { + return 0; + } + }; + } else { + this.batchReadConfig = batchReadConfig; + } + } + + private Path getPath(ItemId itemId, SessionInfo sessionInfo) throws RepositoryException { + return getPath(itemId, sessionInfo, sessionInfo.getWorkspaceName()); + } + + private Path getPath(ItemId itemId, SessionInfo sessionInfo, String workspaceName) throws RepositoryException { + if (itemId.denotesNode()) { + Path p = itemId.getPath(); + String uid = itemId.getUniqueID(); + if (uid == null) { + return p; + } else { + NamePathResolver resolver = getNamePathResolver(sessionInfo); + String uri = super.getItemUri(itemId, sessionInfo, workspaceName); + String rootUri = getRootURI(sessionInfo); + String jcrPath; + if (uri.startsWith(rootUri)) { + jcrPath = uri.substring(rootUri.length()); + } else { + log.warn("ItemURI " + uri + " doesn't start with rootURI (" + rootUri + ")."); + // fallback: + // calculated uri does not start with the rootURI + // -> search /jcr:root and start sub-string behind. + String rootSegment = Text.escapePath(JcrRemotingConstants.ROOT_ITEM_RESOURCEPATH); + jcrPath = uri.substring(uri.indexOf(rootSegment) + rootSegment.length()); + } + jcrPath = Text.unescape(jcrPath); + return resolver.getQPath(jcrPath); + } + } else { + PropertyId pId = (PropertyId) itemId; + Path parentPath = getPath(pId.getParentId(), sessionInfo, workspaceName); + return getPathFactory().create(parentPath, pId.getName(), true); + } + } + + private String getURI(Path path, SessionInfo sessionInfo) throws RepositoryException { + StringBuilder sb = new StringBuilder(getRootURI(sessionInfo)); + String jcrPath = getNamePathResolver(sessionInfo).getJCRPath(path); + sb.append(Text.escapePath(jcrPath)); + return sb.toString(); + } + + private String getURI(ItemId itemId, SessionInfo sessionInfo) throws RepositoryException { + Path p = getPath(itemId, sessionInfo); + if (p == null) { + return super.getItemUri(itemId, sessionInfo); + } else { + return getURI(p, sessionInfo); + } + } + + private String getRootURI(SessionInfo sessionInfo) { + StringBuilder sb = new StringBuilder(getWorkspaceURI(sessionInfo)); + sb.append(Text.escapePath(JcrRemotingConstants.ROOT_ITEM_RESOURCEPATH)); + return sb.toString(); + } + + private String getWorkspaceURI(SessionInfo sessionInfo) { + StringBuilder sb = new StringBuilder(); + sb.append(jcrServerURI); + sb.append(Text.escape(sessionInfo.getWorkspaceName())); + return sb.toString(); + } + + /** + * @see RepositoryService#getQValueFactory() + */ + private QValueFactoryImpl getQValueFactory(SessionInfo sessionInfo) throws RepositoryException { + QValueFactoryImpl qv; + if (qvFactories.containsKey(sessionInfo)) { + qv = qvFactories.get(sessionInfo); + } else { + ValueLoader loader = new ValueLoader(getClient(sessionInfo), getContext(sessionInfo)); + qv = new QValueFactoryImpl(getNamePathResolver(sessionInfo), loader); + qvFactories.put(sessionInfo, qv); + } + return qv; + } + + //--------------------------------------------------< RepositoryService >--- + + // exists && getPropertyInfo -> to be done + // getNodeInfo: omitted for requires list of 'references' + + /** + * If the specified workspaceName the default workspace name + * is used for backwards compatibility with jackrabbit-jcr-server < 1.5.0 + * + * @see RepositoryService#obtain(Credentials, String) + */ + @Override + public SessionInfo obtain(Credentials credentials, String workspaceName) + throws RepositoryException { + // for backwards compatibility with jcr-server < 1.5.0 + String wspName = (workspaceName == null) ? defaultWorkspaceName : workspaceName; + return super.obtain(credentials, wspName); + } + + /** + * If the specified workspaceName the default workspace name + * is used for backwards compatibility with jackrabbit-jcr-server < 1.5.0 + * + * @see RepositoryService#obtain(SessionInfo, String) + */ + @Override + public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName) + throws RepositoryException { + // for backwards compatibility with jcr-server < 1.5.0 + String wspName = (workspaceName == null) ? defaultWorkspaceName : workspaceName; + return super.obtain(sessionInfo, wspName); + } + + /** + * @see RepositoryService#dispose(SessionInfo) + */ + @Override + public void dispose(SessionInfo sessionInfo) throws RepositoryException { + super.dispose(sessionInfo); + // remove the qvalue factory created for the given SessionInfo from the + // map of valuefactories. + qvFactories.remove(sessionInfo); + } + + /** + * @see RepositoryService#getItemInfos(SessionInfo, ItemId) + */ + @Override + public Iterator getItemInfos(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException { + if (!itemId.denotesNode()) { + PropertyInfo propertyInfo = getPropertyInfo(sessionInfo, (PropertyId) itemId); + return Iterators.singleton(propertyInfo); + } else { + NodeId nodeId = (NodeId) itemId; + Path path = getPath(itemId, sessionInfo); + String uri = getURI(path, sessionInfo); + int depth = batchReadConfig.getDepth(path, this.getNamePathResolver(sessionInfo)); + + HttpGet request = new HttpGet(uri + "." + depth + ".json"); + HttpResponse response = null; + try { + response = executeRequest(sessionInfo, request); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == DavServletResponse.SC_OK) { + HttpEntity entity = response.getEntity(); + if (entity.getContentLength() == 0) { + // no JSON response -> no such node on the server + throw new ItemNotFoundException("No such item " + nodeId); + } + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + NodeInfoImpl nInfo = new NodeInfoImpl(nodeId, path); + + ItemInfoJsonHandler handler = new ItemInfoJsonHandler(resolver, nInfo, getRootURI(sessionInfo), getQValueFactory(sessionInfo), getPathFactory(), getIdFactory()); + JsonParser ps = new JsonParser(handler); + ps.parse(entity.getContent(), ContentType.get(entity).getCharset().name()); + + Iterator it = handler.getItemInfos(); + if (!it.hasNext()) { + throw new ItemNotFoundException("No such node " + uri); + } + return handler.getItemInfos(); + } else { + throw ExceptionConverter.generate(new DavException(statusCode, "Unable to retrieve NodeInfo for " + uri), request); + } + } catch (IOException e) { + log.error("Internal error while retrieving NodeInfo for " + uri + ".", e); + throw new RepositoryException(e.getMessage(), e); + } finally { + request.releaseConnection(); + } + } + } + + /** + * @see RepositoryService#getPropertyInfo(SessionInfo, PropertyId) + */ + @Override + public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, PropertyId propertyId) throws RepositoryException { + Path p = getPath(propertyId, sessionInfo); + String uri = getURI(p, sessionInfo); + HttpPropfind request = null; + try { + request = new HttpPropfind(uri, LAZY_PROPERTY_NAME_SET, DavConstants.DEPTH_0); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + + MultiStatusResponse[] mresponses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (mresponses.length != 1) { + throw new ItemNotFoundException("Unable to retrieve the PropertyInfo. No such property " + uri); + } + + MultiStatusResponse mresponse = mresponses[0]; + DavPropertySet props = mresponse.getProperties(DavServletResponse.SC_OK); + int propertyType = PropertyType.valueFromName(props.get(JCR_TYPE).getValue().toString()); + + if (propertyType == PropertyType.BINARY) { + DavProperty lengthsProp = props.get(JCR_LENGTHS); + if (lengthsProp != null) { + // multivalued binary property + long[] lengths = ValueUtil.lengthsFromXml(lengthsProp.getValue()); + QValue[] qValues = new QValue[lengths.length]; + for (int i = 0 ; i < lengths.length ; i ++) { + qValues[i] = getQValueFactory(sessionInfo).create(lengths[i], uri, i); + } + return new PropertyInfoImpl(propertyId, p, propertyType, qValues); + } else { + // single valued binary property + long length = Long.parseLong(props.get(JCR_LENGTH).getValue().toString()); + QValue qValue = getQValueFactory(sessionInfo).create(length, uri, QValueFactoryImpl.NO_INDEX) ; + return new PropertyInfoImpl(propertyId, p, propertyType, qValue); + } + } else if (props.contains(JCR_GET_STRING)) { + // single valued non-binary property + Object v = props.get(JCR_GET_STRING).getValue(); + String str = (v == null) ? "" : v.toString(); + QValue qValue = ValueFormat.getQValue(str, propertyType, getNamePathResolver(sessionInfo), getQValueFactory(sessionInfo)); + return new PropertyInfoImpl(propertyId, p, propertyType, qValue); + } else { + // multivalued non-binary property or some other property that + // didn't expose the JCR_GET_STRING dav property. + return super.getPropertyInfo(sessionInfo, propertyId); + } + } catch (IOException e) { + log.error("Internal error while retrieving ItemInfo for " + uri + ".", e); + throw new RepositoryException(e.getMessage(), e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public Batch createBatch(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException { + return new BatchImpl(itemId, sessionInfo); + } + + @Override + public void submit(Batch batch) throws RepositoryException { + if (!(batch instanceof BatchImpl)) { + throw new RepositoryException("Unknown Batch implementation."); + } + BatchImpl batchImpl = (BatchImpl) batch; + try { + if (!batchImpl.isEmpty()) { + batchImpl.start(); + } + } finally { + batchImpl.dispose(); + } + } + + @Override + public Tree createTree(SessionInfo sessionInfo, Batch batch, Name nodeName, Name primaryTypeName, String uniqueId) throws RepositoryException { + return new JsonTree(sessionInfo, nodeName, primaryTypeName, uniqueId, getNamePathResolver(sessionInfo)); + } + + @Override + public void copy(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + if (srcWorkspaceName.equals(sessionInfo.getWorkspaceName())) { + super.copy(sessionInfo, srcWorkspaceName, srcNodeId, destParentNodeId, destName); + return; + } + HttpPost request = null; + try { + request = new HttpPost(getWorkspaceURI(sessionInfo)); + request.setHeader("Referer", request.getURI().toASCIIString()); + addIfHeader(sessionInfo, request); + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + + StringBuilder args = new StringBuilder(); + args.append(srcWorkspaceName); + args.append(","); + args.append(resolver.getJCRPath(getPath(srcNodeId, sessionInfo, srcWorkspaceName))); + args.append(","); + String destParentPath = resolver.getJCRPath(getPath(destParentNodeId, sessionInfo)); + String destPath = (destParentPath.endsWith("/") ? + destParentPath + resolver.getJCRName(destName) : + destParentPath + "/" + resolver.getJCRName(destName)); + args.append(destPath); + List nvps = Collections.singletonList(new BasicNameValuePair(PARAM_COPY, args.toString())); + HttpEntity entity = new UrlEncodedFormEntity(nvps, Charset.forName("UTF-8")); + request.setEntity(entity); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e, request); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + @Override + public void clone(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName, boolean removeExisting) throws RepositoryException { + HttpPost request = null; + try { + request = new HttpPost(getWorkspaceURI(sessionInfo)); + request.setHeader("Referer", request.getURI().toASCIIString()); + addIfHeader(sessionInfo, request); + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + StringBuilder args = new StringBuilder(); + args.append(srcWorkspaceName); + args.append(","); + args.append(resolver.getJCRPath(getPath(srcNodeId, sessionInfo, srcWorkspaceName))); + args.append(","); + String destParentPath = resolver.getJCRPath(getPath(destParentNodeId, sessionInfo)); + String destPath = (destParentPath.endsWith("/") ? + destParentPath + resolver.getJCRName(destName) : + destParentPath + "/" + resolver.getJCRName(destName)); + args.append(destPath); + args.append(","); + args.append(Boolean.toString(removeExisting)); + List nvps = Collections.singletonList(new BasicNameValuePair(PARAM_CLONE, args.toString())); + HttpEntity entity = new UrlEncodedFormEntity(nvps, Charset.forName("UTF-8")); + request.setEntity(entity); + HttpResponse response = executeRequest(sessionInfo, request); + request.checkSuccess(response); + if (removeExisting) { + clearItemUriCache(sessionInfo); + } + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e, request); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + private static void addIfHeader(SessionInfo sInfo, HttpUriRequest request) { + try { + initMethod(request, sInfo, true); + } catch (RepositoryException e) { + // should never get here + log.error("Unable to retrieve lock tokens: omitted from request header."); + } + } + + //-------------------------------------------------------------------------- + + private class BatchImpl implements Batch { + + private final ItemId targetId; + private final SessionInfo sessionInfo; + private final List parts; + private final List binaries; + private final List diff; + /* + If this batch needs to remove multiple same-name-siblings starting + from lower index, the index of the following siblings must be reset + in order to avoid PathNotFoundException. + */ + private final Map removed = new HashMap(); + + private HttpPost request; // TODO: use PATCH request instead. + private boolean isConsumed; + // flag to determine if the uri-lookup needs to be cleared... e.g. + // after a move operation. + private boolean clear; + + private BatchImpl(ItemId targetId, SessionInfo sessionInfo) { + this.targetId = targetId; + this.sessionInfo = sessionInfo; + this.parts = new ArrayList(); + this.binaries = new ArrayList(); + this.diff = new ArrayList(); + } + + private void start() throws RepositoryException { + checkConsumed(); + + request.setHeader("Referer", request.getURI().toASCIIString()); + + // add lock tokens + addIfHeader(sessionInfo, request); + + // insert the content of 'batchMap' part containing the ordered list + // of methods to be executed: + StringBuilder buf = new StringBuilder(); + for (Iterator it = diff.iterator(); it.hasNext();) { + buf.append(it.next()); + if (it.hasNext()) { + buf.append("\r"); + } + } + + // add the diff part - always do multipart in case the receiving servlet + // engine has a form-size restriction (JCR-3726) + Utils.addPart(PARAM_DIFF, buf.toString(), parts); + + // JCR-4317: need RFC6532 mode so that values are encoded in UTF-8 + MultipartEntityBuilder b = MultipartEntityBuilder.create().setMode(HttpMultipartMode.RFC6532); + for (FormBodyPart p : parts) { + b.addPart(p.getName(), p.getBody()); + } + request.setEntity(b.build()); + + org.apache.http.client.HttpClient client = getClient(sessionInfo); + try { + HttpResponse response = client.execute(request, getContext(sessionInfo)); + request.checkSuccess(response); + if (clear) { + RepositoryServiceImpl.super.clearItemUriCache(sessionInfo); + } + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e, request); + } finally { + request.releaseConnection(); + } + } + + private void dispose() { + request = null; + isConsumed = true; + // discard binary parts (JCR-2582) + for (QValue bin : binaries) { + if (bin instanceof ValueLoader.Target) { + ((ValueLoader.Target) bin).reset(); + } + } + } + + private void checkConsumed() { + if (isConsumed) { + throw new IllegalStateException("Batch has already been consumed."); + } + } + + private boolean isEmpty() { + return request == null; + } + + private void assertMethod() throws RepositoryException { + if (request == null) { + String uri = getURI(targetId, sessionInfo); + request = new HttpPost(uri); + // ship lock-tokens as if-header to circumvent problems with + // locks created by this session. + String[] locktokens = sessionInfo.getLockTokens(); + if (locktokens != null && locktokens.length > 0) { + IfHeader ifH = new IfHeader(locktokens); + request.setHeader(ifH.getHeaderName(), ifH.getHeaderValue()); + } + } + } + + //----------------------------------------------------------< Batch >--- + @Override + public void addNode(NodeId parentId, Name nodeName, Name nodetypeName, + String uuid) throws RepositoryException { + assertMethod(); + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + Path p = getPathFactory().create(getPath(parentId, sessionInfo), nodeName, true); + String jcrPath = resolver.getJCRPath(p); + + StringWriter wr = new StringWriter(); + wr.write('{'); + wr.write(Utils.getJsonKey(JcrConstants.JCR_PRIMARYTYPE)); + wr.write(JsonUtil.getJsonString(getNamePathResolver(sessionInfo).getJCRName(nodetypeName))); + if (uuid != null) { + wr.write(','); + wr.write(Utils.getJsonKey(JcrConstants.JCR_UUID)); + wr.write(JsonUtil.getJsonString(uuid)); + } + wr.write('}'); + appendDiff(SYMBOL_ADD_NODE, jcrPath, wr.toString()); + } + + @Override + public void addProperty(NodeId parentId, Name propertyName, QValue value) throws RepositoryException { + assertMethod(); + Path p = getPathFactory().create(getPath(parentId, sessionInfo), propertyName, true); + setProperty(p, value, false); + } + + @Override + public void addProperty(NodeId parentId, Name propertyName, QValue[] values) throws RepositoryException { + assertMethod(); + Path p = getPathFactory().create(getPath(parentId, sessionInfo), propertyName, true); + setProperty(p, values, false); + } + + @Override + public void setValue(PropertyId propertyId, QValue value) throws RepositoryException { + assertMethod(); + Path p = getPath(propertyId, sessionInfo); + setProperty(p, value, true); + } + + @Override + public void setValue(PropertyId propertyId, QValue[] values) throws RepositoryException { + assertMethod(); + Path p = getPath(propertyId, sessionInfo); + setProperty(p, values, true); + } + + @Override + public void remove(ItemId itemId) throws RepositoryException { + assertMethod(); + + Path rmPath = getPath(itemId, sessionInfo); + if (itemId.denotesNode()) { + rmPath = calcRemovePath(rmPath); + } + String rmJcrPath = getNamePathResolver(sessionInfo).getJCRPath(rmPath); + appendDiff(SYMBOL_REMOVE, rmJcrPath, null); + + // clear the uri-lookup in case the itemID contains a uniqueID part. + if (itemId.getPath() == null) { + clear = true; + } + } + + @Override + public void reorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) throws RepositoryException { + assertMethod(); + + // TODO: multiple reorder of SNS nodes requires readjustment of path -> see remove() + String srcPath = getNamePathResolver(sessionInfo).getJCRPath(getPath(srcNodeId, sessionInfo)); + + StringBuilder val = new StringBuilder(); + if (beforeNodeId != null) { + Path beforePath = getPath(beforeNodeId, sessionInfo); + String beforeJcrPath = getNamePathResolver(sessionInfo).getJCRPath(beforePath); + val.append(Text.getName(beforeJcrPath)); + val.append(ORDER_POSITION_BEFORE); + } else { + val.append(ORDER_POSITION_LAST); + } + appendDiff(SYMBOL_MOVE, srcPath, val.toString()); + + // clear the uri-lookup in case the itemID contains a uniqueID part. + if (srcNodeId.getPath() == null || (beforeNodeId != null && beforeNodeId.getPath() == null)) { + clear = true; + } + } + + @Override + public void setMixins(NodeId nodeId, Name[] mixinNodeTypeNames) throws RepositoryException { + assertMethod(); + + QValue[] vs = new QValue[mixinNodeTypeNames.length]; + for (int i = 0; i < mixinNodeTypeNames.length; i++) { + vs[i] = getQValueFactory(sessionInfo).create(mixinNodeTypeNames[i]); + } + Path p = getPathFactory().create(getPath(nodeId, sessionInfo), NameConstants.JCR_MIXINTYPES, true); + // register the diff entry including clearing previous calls to + // setMixins for the same node. + setProperty(p, vs, true); + } + + @Override + public void setPrimaryType(NodeId nodeId, Name primaryNodeTypeName) throws RepositoryException { + assertMethod(); + + QValue v = getQValueFactory(sessionInfo).create(primaryNodeTypeName); + Path p = getPathFactory().create(getPath(nodeId, sessionInfo), NameConstants.JCR_PRIMARYTYPE, true); + // register the diff entry including clearing previous calls to + // setPrimaryType for the same node. + setProperty(p, v, true); + } + + @Override + public void move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + assertMethod(); + + String srcPath = getNamePathResolver(sessionInfo).getJCRPath(getPath(srcNodeId, sessionInfo)); + Path destPath = getPathFactory().create(getPath(destParentNodeId, sessionInfo), destName, true); + String destJcrPath = getNamePathResolver(sessionInfo).getJCRPath(destPath); + + appendDiff(SYMBOL_MOVE, srcPath, destJcrPath); + + clear = true; + } + + @Override + public void setTree(NodeId parentId, Tree contentTree) throws RepositoryException { + assertMethod(); + if (!(contentTree instanceof JsonTree)) { + throw new RepositoryException("Invalid Tree implementation : " + contentTree.getClass().getName()); + } + + Path normalizedPath = getPathFactory().create(getPath(parentId, sessionInfo), contentTree.getName(), true); + String jcrPath = getNamePathResolver(sessionInfo).getJCRPath(normalizedPath); + appendDiff(SYMBOL_ADD_NODE, jcrPath, ((JsonTree) contentTree).toJsonString(parts, binaries)); + } + + //---------------------------------------------------------------------- + /** + * + * @param symbol + * @param targetPath + * @param value + */ + private void appendDiff(char symbol, String targetPath, String value) { + StringBuilder bf = new StringBuilder(); + bf.append(symbol).append(targetPath).append(" : "); + if (value != null) { + bf.append(value); + } + diff.add(bf.toString()); + } + + /** + * + * @param propPath + * @param value + * @throws RepositoryException + */ + private void setProperty(Path propPath, QValue value, boolean clearPrevious) throws RepositoryException { + NamePathResolver resolver = getNamePathResolver(sessionInfo); + String jcrPropPath = resolver.getJCRPath(propPath); + if (clearPrevious) { + clearPreviousSetProperty(jcrPropPath); + } + + String strValue = Utils.getJsonString(value); + appendDiff(SYMBOL_SET_PROPERTY, jcrPropPath, strValue); + if (strValue == null) { + Utils.addPart(jcrPropPath, value, resolver, parts, binaries); + } + } + + private void setProperty(Path propPath, QValue[] values, boolean clearPrevious) throws RepositoryException { + NamePathResolver resolver = getNamePathResolver(sessionInfo); + String jcrPropPath = resolver.getJCRPath(propPath); + if (clearPrevious) { + clearPreviousSetProperty(jcrPropPath); + } + + StringBuilder strVal = new StringBuilder("["); + for (int i = 0; i < values.length; i++) { + String str = Utils.getJsonString(values[i]); + if (str == null) { + Utils.addPart(jcrPropPath, values[i], resolver, parts, binaries); + } else { + String delim = (i == 0) ? "" : ","; + strVal.append(delim).append(str); + } + } + strVal.append("]"); + appendDiff(SYMBOL_SET_PROPERTY, jcrPropPath, strVal.toString()); + } + + private void clearPreviousSetProperty(String jcrPropPath) { + String key = SYMBOL_SET_PROPERTY + jcrPropPath + " : "; + // make sure that multiple calls to setProperty for a given path + // are only reflected once in the multipart, otherwise this will + // cause consistency problems as the various calls cannot be separated + // (missing unique identifier for the parts). + for (Iterator it = diff.iterator(); it.hasNext();) { + String entry = it.next(); + if (entry.startsWith(key)) { + it.remove(); + Utils.removeParts(jcrPropPath, parts); + return; + } + } + } + + private Path calcRemovePath(Path removedNodePath) throws RepositoryException { + removed.put(removedNodePath, removedNodePath); + Name name = removedNodePath.getName(); + int index = removedNodePath.getNormalizedIndex(); + if (index > Path.INDEX_DEFAULT) { + Path.Element[] elems = removedNodePath.getElements(); + PathBuilder pb = new PathBuilder(); + for (int i = 0; i <= elems.length - 2; i++) { + pb.addLast(elems[i]); + } + Path parent = pb.getPath(); + while (index > Path.INDEX_UNDEFINED) { + Path siblingP = getPathFactory().create(parent, name, --index, true); + if (removed.containsKey(siblingP)) { + // as the previous sibling has been remove -> the same index + // must be used to remove the node at removedNodePath. + siblingP = removed.get(siblingP); + removed.put(removedNodePath, siblingP); + return siblingP; + } + } + } + // first of siblings or no sibling at all + return removedNodePath; + } + } + + //-------------------------------------------------------------------------- + class JsonTree extends AbstractTree { + + private final StringBuilder properties = new StringBuilder(); + private final List parts = new ArrayList(); + private final List binaries = new ArrayList(); + private final SessionInfo sessionInfo; + + JsonTree(SessionInfo sessionInfo, Name nodeName, Name ntName, String uniqueId, NamePathResolver resolver) { + super(nodeName, ntName, uniqueId, resolver); + this.sessionInfo = sessionInfo; + } + + //-------------------------------------------------------< AbstractTree >--- + @Override + protected Tree createChild(Name name, Name primaryTypeName, String uniqueId) { + return new JsonTree(sessionInfo, name, primaryTypeName, uniqueId, getResolver()); + } + + //---------------------------------------------------------------< Tree >--- + @Override + public void addProperty(NodeId parentId, Name propertyName, int propertyType, QValue value) throws RepositoryException { + properties.append(','); + properties.append(Utils.getJsonKey(getResolver().getJCRName(propertyName))); + + String valueStr = Utils.getJsonString(value); + if (valueStr == null) { + String jcrPropPath = createPath(parentId, propertyName); + Utils.addPart(jcrPropPath, value, getResolver(), parts, binaries); + } else { + properties.append(valueStr); + } + } + + @Override + public void addProperty(NodeId parentId, Name propertyName, int propertyType, QValue[] values) throws RepositoryException { + String name = getResolver().getJCRName(propertyName); + properties.append(','); + properties.append(Utils.getJsonKey(name)); + int index = 0; + properties.append('['); + for (QValue value : values) { + String valueStr = Utils.getJsonString(value); + if (valueStr == null) { + String jcrPropPath = createPath(parentId, propertyName); + Utils.addPart(jcrPropPath, value, getResolver(), parts, binaries); + } else { + String delim = (index++ == 0) ? "" : ","; + properties.append(delim).append('"').append(valueStr).append('"'); + } + } + properties.append(']'); + } + + private String createPath(NodeId parentId, Name propertyName) throws RepositoryException { + Path propPath = getPathFactory().create(getPath(parentId, sessionInfo), propertyName, true); + return getResolver().getJCRPath(propPath); + } + + //-------------------------------------------------------------------------- + String toJsonString(List batchParts, List bins) throws RepositoryException { + batchParts.addAll(this.parts); + bins.addAll(this.binaries); + for (Tree child : this.getChildren()) { + batchParts.addAll(((JsonTree) child).getParts()); + bins.addAll(((JsonTree) child).getBinaries()); + } + + StringBuilder json = new StringBuilder(); + createJsonNodeFragment(json, this, true); + return json.toString(); + } + + //-------------------------------------------------------------------------- + private String createJsonNodeFragment(StringBuilder json, JsonTree tree, boolean start) throws RepositoryException { + if (!start) { + json.append(','); + json.append(Utils.getJsonKey(getResolver().getJCRName(tree.getName()))); + } + json.append('{'); + json.append(Utils.getJsonKey(JcrConstants.JCR_PRIMARYTYPE)); + json.append(JsonUtil.getJsonString(getResolver().getJCRName(tree.getPrimaryTypeName()))); + String uuid = tree.getUniqueId(); + if (uuid != null) { + json.append(','); + json.append(Utils.getJsonKey(JcrConstants.JCR_UUID)); + json.append(JsonUtil.getJsonString(uuid)); + } + // write all the properties. + json.append(tree.getProperties()); + for (Tree child : tree.getChildren()) { + createJsonNodeFragment(json, (JsonTree) child, false); + } + json.append('}'); + return json.toString(); + } + + private StringBuilder getProperties() { + return properties; + } + + private List getParts() { + return parts; + } + + private List getBinaries() { + return binaries; + } +} +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/Spi2davexRepositoryServiceFactory.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/Spi2davexRepositoryServiceFactory.java new file mode 100644 index 00000000000..d3d203cd28b --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/Spi2davexRepositoryServiceFactory.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.spi2davex; + +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; + +/** + * This {@link RepositoryServiceFactory} implementation is responsible + * for creating {@link RepositoryServiceImpl} instances. + */ +public class Spi2davexRepositoryServiceFactory implements RepositoryServiceFactory { + + /** + * Mandatory configuration parameter: It's value is expected to specify the + * URI of the JCR server implementation. {@link #DEFAULT_REPOSITORY_URI} is used as + * fallback if no parameters or uri has been specified and the uri could not + * been retrieved from system props either. + */ + public static final String PARAM_REPOSITORY_URI = "org.apache.jackrabbit.spi2davex.uri"; + + /** + * Default URI for the {@link #PARAM_REPOSITORY_URI} configuration + * parameter. + */ + public static final String DEFAULT_REPOSITORY_URI = "http://localhost:8080/jackrabbit/server"; + + /** + * Optional batch read configuration parameter: If present it's value is + * expected to be an instance of {@link BatchReadConfig} + */ + public static final String PARAM_BATCHREAD_CONFIG = "org.apache.jackrabbit.spi2davex.BatchReadConfig"; + + /** + * Optional configuration parameter: It's value determines the size of the + * {@link ItemInfoCache} cache. Defaults to {@link ItemInfoCacheImpl#DEFAULT_CACHE_SIZE}. + */ + public static final String PARAM_ITEMINFO_CACHE_SIZE = "org.apache.jackrabbit.spi2davex.ItemInfoCacheSize"; + + /** + * Optional configuration parameter: It's value defines the + * maximumConnectionsPerHost value on the HttpClient configuration and + * must be an int greater than zero. + */ + public static final String PARAM_MAX_CONNECTIONS = "org.apache.jackrabbit.spi2davex.MaxConnections"; + + + public static final String PARAM_WORKSPACE_NAME_DEFAULT = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault"; + + public static final String DEFAULT_WORKSPACE_NAME_DEFAULT = "default"; + + public RepositoryService createRepositoryService(Map parameters) throws RepositoryException { + // retrieve the repository uri + String uri; + if (parameters == null) { + uri = System.getProperty(PARAM_REPOSITORY_URI); + } else { + Object repoUri = parameters.get(PARAM_REPOSITORY_URI); + uri = (repoUri == null) ? null : repoUri.toString(); + } + if (uri == null) { + uri = DEFAULT_REPOSITORY_URI; + } + + // load other optional configuration parameters + BatchReadConfig brc = null; + int itemInfoCacheSize = ItemInfoCacheImpl.DEFAULT_CACHE_SIZE; + int maximumHttpConnections = 0; + + String workspaceNameDefault = DEFAULT_WORKSPACE_NAME_DEFAULT; + + if (parameters != null) { + // batchRead config + Object param = parameters.get(PARAM_BATCHREAD_CONFIG); + if (param != null && param instanceof BatchReadConfig) { + brc = (BatchReadConfig) param; + } + + // itemCache size config + param = parameters.get(PARAM_ITEMINFO_CACHE_SIZE); + if (param != null) { + try { + itemInfoCacheSize = Integer.parseInt(param.toString()); + } catch (NumberFormatException e) { + // ignore, use default + } + } + + // max connections config + param = parameters.get(PARAM_MAX_CONNECTIONS); + if (param != null) { + try { + maximumHttpConnections = Integer.parseInt(param.toString()); + } catch ( NumberFormatException e ) { + // using default + } + } + + param = parameters.get(PARAM_WORKSPACE_NAME_DEFAULT); + if (param != null) { + workspaceNameDefault = param.toString(); + } + } + + if (maximumHttpConnections > 0) { + return new RepositoryServiceImpl(uri, workspaceNameDefault, brc, itemInfoCacheSize, maximumHttpConnections); + } else { + return new RepositoryServiceImpl(uri, workspaceNameDefault, brc, itemInfoCacheSize); + } + } + +} diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/Utils.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/Utils.java new file mode 100644 index 00000000000..7f1e93cccc4 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/Utils.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.spi2davex; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Binary; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.FormBodyPartBuilder; +import org.apache.http.entity.mime.content.InputStreamBody; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.jackrabbit.commons.json.JsonUtil; +import org.apache.jackrabbit.commons.webdav.JcrValueType; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +final class Utils { + + private static final String DEFAULT_CHARSET = "UTF-8"; + private static final ContentType DEFAULT_TYPE = ContentType.create("text/plain", DEFAULT_CHARSET); + + private Utils() {}; + + static String getJsonKey(String str) { + return JsonUtil.getJsonString(str) + ":"; + } + + static String getJsonString(QValue value) throws RepositoryException { + String str; + switch (value.getType()) { + case PropertyType.STRING: + str = JsonUtil.getJsonString(value.getString()); + break; + case PropertyType.BOOLEAN: + case PropertyType.LONG: + str = value.getString(); + break; + case PropertyType.DOUBLE: + double d = value.getDouble(); + if (Double.isNaN(d) || Double.isInfinite(d)) { + // JSON cannot specifically handle this property type... + str = null; + } else { + str = value.getString(); + if (str.indexOf('.') == -1) { + str += ".0"; + } + } + break; + default: + // JSON cannot specifically handle this property type... + str = null; + } + return str; + } + + /** + * + * @param paramName + * @param value + */ + static void addPart(String paramName, String value, List parts) { + parts.add(FormBodyPartBuilder.create().setName(paramName).setBody(new StringBody(value, DEFAULT_TYPE)).build()); + } + + /** + * + * @param paramName + * @param value + * @param resolver + * @throws RepositoryException + */ + static void addPart(String paramName, QValue value, NamePathResolver resolver, List parts, List binaries) throws RepositoryException { + FormBodyPartBuilder builder = FormBodyPartBuilder.create().setName(paramName); + ContentType ctype = ContentType.create(JcrValueType.contentTypeFromType(value.getType()), DEFAULT_CHARSET); + + FormBodyPart part; + switch (value.getType()) { + case PropertyType.BINARY: + binaries.add(value); + // server detects binaries based on presence of filename parameters (JCR-4154) + part = builder.setBody(new InputStreamBody(value.getStream(), ctype, paramName)).build(); + break; + case PropertyType.NAME: + part = builder.setBody(new StringBody(resolver.getJCRName(value.getName()), ctype)).build(); + break; + case PropertyType.PATH: + part = builder.setBody(new StringBody(resolver.getJCRPath(value.getPath()), ctype)).build(); + break; + default: + part = builder.setBody(new StringBody(value.getString(), ctype)).build(); + } + + parts.add(part); + } + + static void removeParts(String paramName, List parts) { + for (Iterator it = parts.iterator(); it.hasNext();) { + FormBodyPart part = it.next(); + if (part.getName().equals(paramName)) { + it.remove(); + if (part.getBody() instanceof InputStreamBody) { + try { + ((InputStreamBody) part.getBody()).getInputStream().close(); + } catch (IOException ex) { + // best effort + } + } + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ValueLoader.java b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ValueLoader.java new file mode 100644 index 00000000000..e309e970f15 --- /dev/null +++ b/jackrabbit-spi2dav/src/main/java/org/apache/jackrabbit/spi2davex/ValueLoader.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.spi2davex; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.protocol.HttpContext; +import org.apache.jackrabbit.commons.webdav.JcrRemotingConstants; +import org.apache.jackrabbit.spi2dav.ExceptionConverter; +import org.apache.jackrabbit.spi2dav.ItemResourceConstants; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.HttpPropfind; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; + +/** + * ValueLoader... + */ +class ValueLoader { + + private final HttpClient client; + private final HttpContext context; + + ValueLoader(HttpClient client, HttpContext context) { + this.client = client; + this.context = context; + } + + void loadBinary(String uri, int index, Target target) throws RepositoryException, IOException { + HttpGet request = new HttpGet(uri); + try { + HttpResponse response = client.execute(request, context); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == DavServletResponse.SC_OK) { + target.setStream(response.getEntity().getContent()); + } else { + throw ExceptionConverter.generate(new DavException(statusCode, ("Unable to load binary at " + uri + " - Status line = " + response.getStatusLine()))); + } + } finally { + request.releaseConnection(); + } + } + + public Map loadHeaders(String uri, String[] headerNames) throws IOException, + RepositoryException { + HttpHead request = new HttpHead(uri); + try { + HttpResponse response = client.execute(request, context); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == DavServletResponse.SC_OK) { + Map headers = new HashMap(); + for (String name : headerNames) { + Header hdr = response.getFirstHeader(name); + if (hdr != null) { + headers.put(name, hdr.getValue()); + } + } + return headers; + } else { + throw ExceptionConverter.generate(new DavException(statusCode, ("Unable to load headers at " + uri + " - Status line = " + response.getStatusLine().toString()))); + } + } finally { + request.releaseConnection(); + } + } + + int loadType(String uri) throws RepositoryException, IOException { + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(JcrRemotingConstants.JCR_TYPE_LN, ItemResourceConstants.NAMESPACE); + + HttpPropfind request = null; + try { + request = new HttpPropfind(uri, nameSet, DavConstants.DEPTH_0); + HttpResponse response = client.execute(request, context); + request.checkSuccess(response); + + MultiStatusResponse[] responses = request.getResponseBodyAsMultiStatus(response).getResponses(); + if (responses.length == 1) { + DavPropertySet props = responses[0].getProperties(DavServletResponse.SC_OK); + DavProperty type = props.get(JcrRemotingConstants.JCR_TYPE_LN, ItemResourceConstants.NAMESPACE); + if (type != null) { + return PropertyType.valueFromName(type.getValue().toString()); + } else { + throw new RepositoryException("Internal error. Cannot retrieve property type at " + uri); + } + } else { + throw new ItemNotFoundException("Internal error. Cannot retrieve property type at " + uri); + } + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (request != null) { + request.releaseConnection(); + } + } + } + + //-------------------------------------------------------------------------- + /** + * Internal interface + */ + interface Target { + /** + * @param in + * @throws IOException + */ + void setStream(InputStream in) throws IOException; + + /** + * Resets (clears) the state and this target is in the state as + * prior to setStream() + */ + void reset(); + } + +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/DavPropertyTest.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/DavPropertyTest.java new file mode 100644 index 00000000000..65201d09d61 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/DavPropertyTest.java @@ -0,0 +1,1449 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2dav; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Calendar; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.spi.AbstractSPITest; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.RepositoryServiceStub; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.client.methods.HttpPropfind; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.ordering.OrderingConstants; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.apache.jackrabbit.webdav.version.VersionControlledResource; +import org.apache.jackrabbit.webdav.version.VersionHistoryResource; +import org.apache.jackrabbit.webdav.version.VersionResource; + +/** + * DavPropertyTest... + */ +public class DavPropertyTest extends AbstractSPITest implements ItemResourceConstants { + + private final String testPath = "/test"; + private RepositoryServiceImpl rs; + private SessionInfo si; + private NamePathResolver resolver; + + private String repoURI; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + repoURI = helper.getProperty(ServiceStubImpl.PROP_REPOSITORY_URI); + + rs = (RepositoryServiceImpl) helper.getRepositoryService(); + si = helper.getAdminSessionInfo(); + + NamespaceResolver nsResolver = new AbstractNamespaceResolver() { + public String getURI(String prefix) { + return ("jcr".equals(prefix)) ? "http://www.jcp.org/jcr/1.0" : prefix; + } + public String getPrefix(String uri) { + return ("http://www.jcp.org/jcr/1.0".equals(uri)) ? "jcr" : uri; + } + }; + resolver = new DefaultNamePathResolver(nsResolver); + + try { + rs.getNodeInfo(si, getNodeId(testPath)); + } catch (RepositoryException e) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.addNode(getNodeId("/"), resolver.getQName("test"), NameConstants.NT_UNSTRUCTURED, null); + QValueFactory qvf = rs.getQValueFactory(); + b.addProperty(getNodeId("/test"), resolver.getQName("prop"), qvf.create("value", PropertyType.STRING)); + b.addProperty(getNodeId("/test"), resolver.getQName("propMV"), new QValue[] {qvf.create(1), qvf.create(2)}); + rs.submit(b); + } + } + + @Override + protected void tearDown() throws Exception { + try { + Batch b = rs.createBatch(si, getNodeId("/")); + b.remove(getNodeId(testPath)); + rs.submit(b); + } finally { + rs.dispose(si); + super.tearDown(); + } + } + + private NodeId getNodeId(String path) throws RepositoryException { + return rs.getIdFactory().createNodeId((String) null, + resolver.getQPath(path)); + } + + private PropertyId getPropertyId(NodeId nodeID, Name propName) throws RepositoryException { + return rs.getIdFactory().createPropertyId(nodeID, propName); + } + + private static void assertPropertyNames(DavPropertyNameSet expected, DavPropertyNameSet result) { + assertEquals(expected.getContentSize(), result.getContentSize()); + + if (!(expected.getContent().containsAll(result.getContent()))) { + StringBuilder missing = new StringBuilder(); + for (DavPropertyName name : expected.getContent()) { + if (!result.contains(name)) { + missing.append("- ").append(name.toString()).append('\n'); + } + } + fail("Missing properties : \n" + missing); + } + } + + private DavPropertyNameSet doPropFindNames(String uri) throws Exception { + HttpPropfind request = new HttpPropfind(uri, DavConstants.PROPFIND_PROPERTY_NAMES, DavConstants.DEPTH_0); + HttpClient cl = rs.getClient(si); + HttpResponse response = cl.execute(request, rs.getContext(si)); + request.checkSuccess(response); + + MultiStatus ms = request.getResponseBodyAsMultiStatus(response); + assertEquals(1, ms.getResponses().length); + return ms.getResponses()[0].getPropertyNames(HttpStatus.SC_OK); + } + + private DavPropertyNameSet doPropFindAll(String uri) throws Exception { + HttpPropfind request = new HttpPropfind(uri, DavConstants.PROPFIND_ALL_PROP, DavConstants.DEPTH_0); + HttpClient cl = rs.getClient(si); + HttpResponse response = cl.execute(request, rs.getContext(si)); + request.checkSuccess(response); + + MultiStatus ms = request.getResponseBodyAsMultiStatus(response); + assertEquals(1, ms.getResponses().length); + return ms.getResponses()[0].getPropertyNames(HttpStatus.SC_OK); + } + + private DavPropertyNameSet doPropFindByProp(String uri, DavPropertyNameSet props) throws Exception { + HttpPropfind request = new HttpPropfind(uri, DavConstants.PROPFIND_BY_PROPERTY, props, DavConstants.DEPTH_0); + HttpClient cl = rs.getClient(si); + HttpResponse response = cl.execute(request, rs.getContext(si)); + request.checkSuccess(response); + + MultiStatus ms = request.getResponseBodyAsMultiStatus(response); + assertEquals(1, ms.getResponses().length); + return ms.getResponses()[0].getPropertyNames(HttpStatus.SC_OK); + } + + public void testRepositoryRoot() throws Exception { + DavPropertyNameSet names = doPropFindNames(repoURI); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}creator-displayname + {DAV:}comment + {DAV:}creationdate + {DAV:}supported-report-set + {DAV:}displayname + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}resourcetype + {DAV:}lockdiscovery + {DAV:}supported-method-set + {DAV:}iscollection + */ + assertPropertyNames(expected, names); + + DavPropertyNameSet all = doPropFindAll(repoURI); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + /* + Expected all-props + + {DAV:}getlastmodified + {DAV:}creationdate + {DAV:}displayname + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}resourcetype + {DAV:}lockdiscovery + {DAV:}iscollection + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + DavPropertyNameSet result = doPropFindByProp(repoURI, props); + assertPropertyNames(props, result); + } + + public void testWorkspace() throws Exception { + StringBuilder uri = new StringBuilder(repoURI); + uri.append("/").append(helper.getProperty(RepositoryServiceStub.PROP_PREFIX + "." +RepositoryServiceStub.PROP_WORKSPACE)); + + DavPropertyNameSet set = doPropFindNames(uri.toString()); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(WORKSPACE_SET); + + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}workspace + {DAV:}comment + {DAV:}displayname + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}supported-method-set + {DAV:}iscollection + {DAV:}creator-displayname + {DAV:}creationdate + {DAV:}supported-report-set + {DAV:}resourcetype + {DAV:}lockdiscovery + {http://www.day.com/jcr/webdav/1.0}namespaces + {http://www.day.com/jcr/webdav/1.0}nodetypes-cnd + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri.toString()); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(JCR_NODETYPES_CND); + /* + Expected all-props + + {DAV:}getlastmodified + {DAV:}creationdate + {DAV:}displayname + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}resourcetype + {DAV:}lockdiscovery + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}namespaces + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(JCR_NODETYPES_CND); + DavPropertyNameSet result = doPropFindByProp(uri.toString(), props); + assertPropertyNames(props, result); + + } + + public void testProperty() throws Exception { + String uri = rs.getItemUri(getPropertyId(getNodeId("/test"), resolver.getQName("prop")), si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(PROPERTY_SET); + expected.add(JCR_PARENT); + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}length + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {DAV:}supported-method-set + {DAV:}iscollection + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}supported-report-set + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}path + {DAV:}lockdiscovery + {DAV:}resourcetype + {http://www.day.com/jcr/webdav/1.0}type + {http://www.day.com/jcr/webdav/1.0}value + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_LENGTH); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}iscollection + {DAV:}getcontenttype + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}type + {DAV:}resourcetype + {DAV:}lockdiscovery + {http://www.day.com/jcr/webdav/1.0}value + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(JCR_DEFINITION); + props.add(JCR_LENGTH); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testMVProperty() throws Exception { + String uri = rs.getItemUri(getPropertyId(getNodeId("/test"), resolver.getQName("propMV")), si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(PROPERTY_MV_SET); + expected.add(JCR_PARENT); + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}values + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {DAV:}supported-method-set + {DAV:}iscollection + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}supported-report-set + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}path + {DAV:}lockdiscovery + {DAV:}resourcetype + {http://www.day.com/jcr/webdav/1.0}type + {http://www.day.com/jcr/webdav/1.0}lengths + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_LENGTHS); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}values + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}parent + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}supportedlock + {DAV:}displayname + {DAV:}iscollection + {DAV:}getcontenttype + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}type + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(JCR_DEFINITION); + props.add(JCR_LENGTHS); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testRootNode() throws Exception { + String uri = rs.getItemUri(getNodeId("/"), si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(NODE_SET); + expected.add(OrderingConstants.ORDERING_TYPE); + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}ordering-type + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}references + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}resourcetype + {DAV:}lockdiscovery + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}index + {DAV:}supportedlock + {DAV:}supported-method-set + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}weakreferences + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {DAV:}supported-report-set + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(OrderingConstants.ORDERING_TYPE); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_INDEX); + expected.remove(JCR_REFERENCES); + expected.remove(JCR_WEAK_REFERENCES); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}displayname + {DAV:}supportedlock + {DAV:}iscollection + {DAV:}getcontenttype + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(OrderingConstants.ORDERING_TYPE); + props.add(JCR_DEFINITION); + props.add(JCR_INDEX); + props.add(JCR_REFERENCES); + props.add(JCR_WEAK_REFERENCES); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testNode() throws Exception { + String uri = rs.getItemUri(getNodeId("/test"), si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(NODE_SET); + expected.add(OrderingConstants.ORDERING_TYPE); + expected.add(JCR_PARENT); + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}ordering-type + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}references + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}index + {DAV:}supportedlock + {DAV:}supported-method-set + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}weakreferences + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {DAV:}supported-report-set + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(OrderingConstants.ORDERING_TYPE); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_INDEX); + expected.remove(JCR_REFERENCES); + expected.remove(JCR_WEAK_REFERENCES); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}supportedlock + {DAV:}iscollection + {DAV:}getcontenttype + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(OrderingConstants.ORDERING_TYPE); + props.add(JCR_DEFINITION); + props.add(JCR_INDEX); + props.add(JCR_REFERENCES); + props.add(JCR_WEAK_REFERENCES); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testReferenceableNode() throws Exception { + NodeId nid = getNodeId("/test"); + Batch b = rs.createBatch(si, nid); + b.setMixins(nid, new Name[] {NameConstants.MIX_REFERENCEABLE}); + rs.submit(b); + + String uri = rs.getItemUri(nid, si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(NODE_SET); + expected.add(OrderingConstants.ORDERING_TYPE); + expected.add(JCR_PARENT); + expected.add(JCR_UUID); + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}ordering-type + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}references + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {http://www.day.com/jcr/webdav/1.0}uuid + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}index + {DAV:}supportedlock + {DAV:}supported-method-set + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}weakreferences + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {DAV:}supported-report-set + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(OrderingConstants.ORDERING_TYPE); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_INDEX); + expected.remove(JCR_REFERENCES); + expected.remove(JCR_WEAK_REFERENCES); + expected.remove(JCR_UUID); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}supportedlock + {DAV:}iscollection + {DAV:}getcontenttype + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(OrderingConstants.ORDERING_TYPE); + props.add(JCR_DEFINITION); + props.add(JCR_INDEX); + props.add(JCR_REFERENCES); + props.add(JCR_WEAK_REFERENCES); + props.add(JCR_UUID); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testNodeWithPrimaryItem() throws Exception { + // create file node + NodeId nid = getNodeId("/test"); + Name fileName = resolver.getQName("test.txt"); + Batch b = rs.createBatch(si, nid); + b.addNode(nid, fileName, NameConstants.NT_FILE, null); + + String filePath = testPath + "/" + fileName.getLocalName(); + NodeId fileID = getNodeId(filePath); + b.addNode(fileID, NameConstants.JCR_CONTENT, NameConstants.NT_RESOURCE, null); + + NodeId content = getNodeId(filePath + "/" + NameConstants.JCR_CONTENT); + + QValue lastModified = rs.getQValueFactory().create(Calendar.getInstance()); + QValue mimeType = rs.getQValueFactory().create("text/plain", PropertyType.STRING); + QValue enc = rs.getQValueFactory().create("utf-8", PropertyType.STRING); + b.addProperty(content, resolver.getQName(JcrConstants.JCR_LASTMODIFIED), lastModified); + b.addProperty(content, resolver.getQName(JcrConstants.JCR_MIMETYPE), mimeType); + b.addProperty(content, resolver.getQName(JcrConstants.JCR_ENCODING), enc); + + InputStream data = new ByteArrayInputStream("\u0633\u0634".getBytes("UTF-8")); + b.addProperty(content, resolver.getQName(JcrConstants.JCR_DATA), rs.getQValueFactory().create(data)); + + rs.submit(b); + + // test properties of the file node + String uri = rs.getItemUri(fileID, si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(NODE_SET); + expected.add(JCR_PARENT); + expected.add(JCR_PRIMARYITEM); + /* + Expected property names + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}references + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}resourcetype + {DAV:}lockdiscovery + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}primaryitem + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}index + {DAV:}supportedlock + {DAV:}supported-method-set + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}weakreferences + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {DAV:}supported-report-set + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_INDEX); + expected.remove(JCR_REFERENCES); + expected.remove(JCR_WEAK_REFERENCES); + expected.remove(JCR_PRIMARYITEM); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}supportedlock + {DAV:}iscollection + {DAV:}getcontenttype + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(JCR_DEFINITION); + props.add(JCR_INDEX); + props.add(JCR_REFERENCES); + props.add(JCR_WEAK_REFERENCES); + props.add(JCR_PRIMARYITEM); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testCheckedOutVersionableNode() throws Exception { + NodeId nid = getNodeId("/test"); + Batch b = rs.createBatch(si, nid); + b.setMixins(nid, new Name[] {NameConstants.MIX_VERSIONABLE}); + rs.submit(b); + + String uri = rs.getItemUri(nid, si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(NODE_SET); + expected.addAll(VERSIONABLE_SET); + expected.add(OrderingConstants.ORDERING_TYPE); + expected.add(JCR_PARENT); + expected.add(JCR_UUID); + expected.add(VersionControlledResource.CHECKED_OUT); + expected.add(VersionControlledResource.PREDECESSOR_SET); + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}ordering-type + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}references + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {http://www.day.com/jcr/webdav/1.0}uuid + {DAV:}predecessor-set + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}version-history + {DAV:}lockdiscovery + {DAV:}resourcetype + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}index + {DAV:}supportedlock + {DAV:}supported-method-set + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}weakreferences + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {DAV:}supported-report-set + {DAV:}auto-version + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {DAV:}checked-out + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(OrderingConstants.ORDERING_TYPE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_INDEX); + expected.remove(JCR_REFERENCES); + expected.remove(JCR_WEAK_REFERENCES); + expected.remove(JCR_UUID); + expected.remove(VersionControlledResource.CHECKED_OUT); + expected.remove(VersionControlledResource.VERSION_HISTORY); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}supportedlock + {DAV:}iscollection + {DAV:}getcontenttype + {DAV:}predecessor-set + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}auto-version + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(OrderingConstants.ORDERING_TYPE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(JCR_DEFINITION); + props.add(JCR_INDEX); + props.add(JCR_REFERENCES); + props.add(JCR_WEAK_REFERENCES); + props.add(JCR_UUID); + props.add(VersionControlledResource.CHECKED_OUT); + props.add(VersionControlledResource.PREDECESSOR_SET); + props.add(VersionControlledResource.VERSION_HISTORY); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testCheckedInVersionableNode() throws Exception { + NodeId nid = getNodeId("/test"); + Batch b = rs.createBatch(si, nid); + b.setMixins(nid, new Name[] {NameConstants.MIX_VERSIONABLE}); + rs.submit(b); + rs.checkin(si, nid); + + String uri = rs.getItemUri(nid, si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(NODE_SET); + expected.addAll(VERSIONABLE_SET); + expected.add(OrderingConstants.ORDERING_TYPE); + expected.add(JCR_PARENT); + expected.add(JCR_UUID); + expected.add(VersionControlledResource.CHECKED_IN); + + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}ordering-type + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}references + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {http://www.day.com/jcr/webdav/1.0}uuid + {DAV:}checked-in + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}version-history + {DAV:}lockdiscovery + {DAV:}resourcetype + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}index + {DAV:}supportedlock + {DAV:}supported-method-set + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}weakreferences + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {DAV:}supported-report-set + {DAV:}auto-version + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(OrderingConstants.ORDERING_TYPE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_INDEX); + expected.remove(JCR_REFERENCES); + expected.remove(JCR_WEAK_REFERENCES); + expected.remove(JCR_UUID); + expected.remove(VersionControlledResource.CHECKED_IN); + expected.remove(VersionControlledResource.VERSION_HISTORY); + /* + Expected all-props + + {DAV:}getlastmodified + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}supportedlock + {DAV:}iscollection + {DAV:}getcontenttype + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}creationdate + {DAV:}auto-version + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(OrderingConstants.ORDERING_TYPE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(JCR_DEFINITION); + props.add(JCR_INDEX); + props.add(JCR_REFERENCES); + props.add(JCR_WEAK_REFERENCES); + props.add(JCR_UUID); + props.add(VersionControlledResource.CHECKED_IN); + props.add(VersionControlledResource.VERSION_HISTORY); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testVersion() throws Exception { + NodeId nid = getNodeId("/test"); + Batch b = rs.createBatch(si, nid); + b.setMixins(nid, new Name[] {NameConstants.MIX_VERSIONABLE}); + rs.submit(b); + NodeId vID = rs.checkin(si, nid); + + String uri = rs.getItemUri(vID, si); + + DavPropertyNameSet set = doPropFindNames(uri); + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(NODE_SET); + expected.addAll(VERSION_SET); + expected.add(JCR_PARENT); + expected.add(JCR_UUID); + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}version-name + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}references + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {http://www.day.com/jcr/webdav/1.0}uuid + {DAV:}checkout-set + {DAV:}predecessor-set + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}version-history + {DAV:}successor-set + {DAV:}lockdiscovery + {DAV:}resourcetype + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}index + {DAV:}label-name-set + {DAV:}supportedlock + {DAV:}supported-method-set + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}weakreferences + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {DAV:}supported-report-set + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_INDEX); + expected.remove(JCR_REFERENCES); + expected.remove(JCR_WEAK_REFERENCES); + expected.remove(JCR_UUID); + expected.remove(VersionResource.VERSION_NAME); + expected.remove(VersionResource.LABEL_NAME_SET); + expected.remove(VersionResource.PREDECESSOR_SET); + expected.remove(VersionResource.SUCCESSOR_SET); + expected.remove(VersionResource.VERSION_HISTORY); + expected.remove(VersionResource.CHECKOUT_SET); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}supportedlock + {DAV:}iscollection + {DAV:}getcontenttype + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(JCR_DEFINITION); + props.add(JCR_INDEX); + props.add(JCR_REFERENCES); + props.add(JCR_WEAK_REFERENCES); + props.add(JCR_UUID); + props.add(VersionResource.VERSION_NAME); + props.add(VersionResource.LABEL_NAME_SET); + props.add(VersionResource.PREDECESSOR_SET); + props.add(VersionResource.SUCCESSOR_SET); + props.add(VersionResource.VERSION_HISTORY); + props.add(VersionResource.CHECKOUT_SET); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + public void testVersionHistory() throws Exception { + NodeId nid = getNodeId("/test"); + Batch b = rs.createBatch(si, nid); + b.setMixins(nid, new Name[] {NameConstants.MIX_VERSIONABLE}); + rs.submit(b); + NodeId vID = rs.checkin(si, nid); + + String uri = Text.getRelativeParent(rs.getItemUri(vID, si), 1); + DavPropertyNameSet set = doPropFindNames(uri); + + DavPropertyNameSet expected = new DavPropertyNameSet(BASE_SET); + expected.addAll(EXISTING_ITEM_BASE_SET); + expected.addAll(NODE_SET); + expected.addAll(VERSIONHISTORY_SET); + expected.add(JCR_PARENT); + expected.add(JCR_UUID); + + /* + Expected property names + + {DAV:}getlastmodified + {DAV:}root-version + {DAV:}version-set + {http://www.day.com/jcr/webdav/1.0}definition + {DAV:}comment + {http://www.day.com/jcr/webdav/1.0}references + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}workspaceName + {http://www.day.com/jcr/webdav/1.0}subscriptiondiscovery + {http://www.day.com/jcr/webdav/1.0}uuid + {http://www.day.com/jcr/webdav/1.0}name + {DAV:}current-user-privilege-set + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + {DAV:}workspace + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}index + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}versionableuuid + {DAV:}supported-method-set + {DAV:}iscollection + {http://www.day.com/jcr/webdav/1.0}weakreferences + {DAV:}creator-displayname + {DAV:}getcontenttype + {DAV:}creationdate + {DAV:}supported-report-set + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + */ + assertPropertyNames(expected, set); + + DavPropertyNameSet all = doPropFindAll(uri); + expected.remove(DeltaVConstants.COMMENT); + expected.remove(DeltaVConstants.CREATOR_DISPLAYNAME); + expected.remove(DeltaVConstants.SUPPORTED_METHOD_SET); + expected.remove(DeltaVConstants.SUPPORTED_REPORT_SET); + expected.remove(DeltaVConstants.WORKSPACE); + expected.remove(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + expected.remove(ObservationConstants.SUBSCRIPTIONDISCOVERY); + expected.remove(JCR_DEFINITION); + expected.remove(JCR_INDEX); + expected.remove(JCR_REFERENCES); + expected.remove(JCR_WEAK_REFERENCES); + expected.remove(JCR_UUID); + expected.remove(VersionHistoryResource.ROOT_VERSION); + expected.remove(VersionHistoryResource.VERSION_SET); + /* + Expected all-props + + {DAV:}getlastmodified + {http://www.day.com/jcr/webdav/1.0}depth + {http://www.day.com/jcr/webdav/1.0}workspaceName + {DAV:}displayname + {http://www.day.com/jcr/webdav/1.0}parent + {DAV:}supportedlock + {http://www.day.com/jcr/webdav/1.0}versionableuuid + {DAV:}iscollection + {DAV:}getcontenttype + {DAV:}creationdate + {http://www.day.com/jcr/webdav/1.0}name + {http://www.day.com/jcr/webdav/1.0}mixinnodetypes + {http://www.day.com/jcr/webdav/1.0}path + {http://www.day.com/jcr/webdav/1.0}primarynodetype + {DAV:}lockdiscovery + {DAV:}resourcetype + */ + assertPropertyNames(expected , all); + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(DeltaVConstants.COMMENT); + props.add(DeltaVConstants.CREATOR_DISPLAYNAME); + props.add(DeltaVConstants.SUPPORTED_METHOD_SET); + props.add(DeltaVConstants.SUPPORTED_REPORT_SET); + props.add(DeltaVConstants.WORKSPACE); + props.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + props.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + props.add(JCR_DEFINITION); + props.add(JCR_INDEX); + props.add(JCR_REFERENCES); + props.add(JCR_WEAK_REFERENCES); + props.add(JCR_UUID); + props.add(VersionHistoryResource.ROOT_VERSION); + props.add(VersionHistoryResource.VERSION_SET); + DavPropertyNameSet result = doPropFindByProp(uri, props); + assertPropertyNames(props, result); + } + + + + //========================================================================== + // copied from jcr-server sources + //========================================================================== + private static final DavPropertyName JCR_WORKSPACE_NAME = DavPropertyName.create(JCR_WORKSPACE_NAME_LN, NAMESPACE); + private static final DavPropertyName JCR_NAME = DavPropertyName.create(JCR_NAME_LN, NAMESPACE); + private static final DavPropertyName JCR_PATH = DavPropertyName.create(JCR_PATH_LN, NAMESPACE); + private static final DavPropertyName JCR_DEPTH = DavPropertyName.create(JCR_DEPTH_LN, NAMESPACE); + private static final DavPropertyName JCR_PARENT = DavPropertyName.create(JCR_PARENT_LN, NAMESPACE); + private static final DavPropertyName JCR_DEFINITION = DavPropertyName.create(JCR_DEFINITION_LN, NAMESPACE); + private static final DavPropertyName JCR_PRIMARYNODETYPE = DavPropertyName.create(JCR_PRIMARYNODETYPE_LN, NAMESPACE); + private static final DavPropertyName JCR_MIXINNODETYPES = DavPropertyName.create(JCR_MIXINNODETYPES_LN, NAMESPACE); + private static final DavPropertyName JCR_INDEX = DavPropertyName.create(JCR_INDEX_LN, NAMESPACE); + private static final DavPropertyName JCR_REFERENCES = DavPropertyName.create(JCR_REFERENCES_LN, NAMESPACE); + private static final DavPropertyName JCR_WEAK_REFERENCES = DavPropertyName.create(JCR_WEAK_REFERENCES_LN, NAMESPACE); + private static final DavPropertyName JCR_UUID = DavPropertyName.create(JCR_UUID_LN, NAMESPACE); + private static final DavPropertyName JCR_PRIMARYITEM = DavPropertyName.create(JCR_PRIMARYITEM_LN, NAMESPACE); + private static final DavPropertyName JCR_TYPE = DavPropertyName.create(JCR_TYPE_LN, NAMESPACE); + private static final DavPropertyName JCR_VALUE = DavPropertyName.create(JCR_VALUE_LN, NAMESPACE); + private static final DavPropertyName JCR_VALUES = DavPropertyName.create(JCR_VALUES_LN, NAMESPACE); + private static final DavPropertyName JCR_LENGTH = DavPropertyName.create(JCR_LENGTH_LN, NAMESPACE); + private static final DavPropertyName JCR_LENGTHS = DavPropertyName.create(JCR_LENGTHS_LN, NAMESPACE); + private static final DavPropertyName JCR_NAMESPACES = DavPropertyName.create(JCR_NAMESPACES_LN, NAMESPACE); + private static final DavPropertyName JCR_NODETYPES_CND = DavPropertyName.create(JCR_NODETYPES_CND_LN, NAMESPACE); + private static final DavPropertyName JCR_VERSIONABLEUUID = DavPropertyName.create(JCR_VERSIONABLEUUID_LN, NAMESPACE); + + /** + * Default property names present with all resources. + */ + private static final DavPropertyNameSet BASE_SET = new DavPropertyNameSet(); + static { + BASE_SET.add(DavPropertyName.DISPLAYNAME); + BASE_SET.add(DavPropertyName.RESOURCETYPE); + BASE_SET.add(DavPropertyName.ISCOLLECTION); + BASE_SET.add(DavPropertyName.GETLASTMODIFIED); + BASE_SET.add(DavPropertyName.CREATIONDATE); + BASE_SET.add(DavPropertyName.SUPPORTEDLOCK); + BASE_SET.add(DavPropertyName.LOCKDISCOVERY); + BASE_SET.add(DeltaVConstants.SUPPORTED_METHOD_SET); + BASE_SET.add(DeltaVConstants.SUPPORTED_REPORT_SET); + BASE_SET.add(DeltaVConstants.CREATOR_DISPLAYNAME); + BASE_SET.add(DeltaVConstants.COMMENT); + BASE_SET.add(JCR_WORKSPACE_NAME); + } + + /** + * Property names defined for JCR workspace resources. + */ + private static final DavPropertyNameSet WORKSPACE_SET = new DavPropertyNameSet(); + static { + WORKSPACE_SET.add(DeltaVConstants.WORKSPACE); + WORKSPACE_SET.add(JCR_NAMESPACES); + WORKSPACE_SET.add(JCR_NODETYPES_CND); + } + + /** + * Additional property names defined for existing and non-existing item resources. + */ + private static final DavPropertyNameSet ITEM_BASE_SET = new DavPropertyNameSet(); + static { + ITEM_BASE_SET.add(DavPropertyName.GETCONTENTTYPE); + ITEM_BASE_SET.add(DeltaVConstants.WORKSPACE); + ITEM_BASE_SET.add(ObservationConstants.SUBSCRIPTIONDISCOVERY); + ITEM_BASE_SET.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + } + + /** + * Additional property names defined for existing item resources. + */ + private static final DavPropertyNameSet EXISTING_ITEM_BASE_SET = new DavPropertyNameSet(ITEM_BASE_SET); + static { + EXISTING_ITEM_BASE_SET.add(JCR_NAME); + EXISTING_ITEM_BASE_SET.add(JCR_PATH); + EXISTING_ITEM_BASE_SET.add(JCR_DEPTH); + EXISTING_ITEM_BASE_SET.add(JCR_DEFINITION); + } + + /** + * Additional property names defined by single value JCR properties. + */ + private static final DavPropertyNameSet PROPERTY_SET = new DavPropertyNameSet(); + static { + PROPERTY_SET.add(JCR_TYPE); + PROPERTY_SET.add(JCR_VALUE); + PROPERTY_SET.add(JCR_LENGTH); + } + + /** + * Additional property names defined by single value JCR properties. + */ + private static final DavPropertyNameSet PROPERTY_MV_SET = new DavPropertyNameSet(); + static { + PROPERTY_MV_SET.add(JCR_TYPE); + PROPERTY_MV_SET.add(JCR_VALUES); + PROPERTY_MV_SET.add(JCR_LENGTHS); + } + + /** + * Additional property names defined by regular JCR nodes. + */ + private static final DavPropertyNameSet NODE_SET = new DavPropertyNameSet(); + static { + NODE_SET.add(JCR_PRIMARYNODETYPE); + NODE_SET.add(JCR_MIXINNODETYPES); + NODE_SET.add(JCR_INDEX); + NODE_SET.add(JCR_REFERENCES); + NODE_SET.add(JCR_WEAK_REFERENCES); + } + + /** + * Additional property names defined by versionable JCR nodes. + */ + private static final DavPropertyNameSet VERSIONABLE_SET = new DavPropertyNameSet(); + static { + VERSIONABLE_SET.add(VersionControlledResource.VERSION_HISTORY); + VERSIONABLE_SET.add(VersionControlledResource.AUTO_VERSION); + } + + /** + * Additional property names defined by JCR version nodes. + */ + private static final DavPropertyNameSet VERSION_SET = new DavPropertyNameSet(); + static { + VERSION_SET.add(VersionResource.VERSION_NAME); + VERSION_SET.add(VersionResource.LABEL_NAME_SET); + VERSION_SET.add(VersionResource.PREDECESSOR_SET); + VERSION_SET.add(VersionResource.SUCCESSOR_SET); + VERSION_SET.add(VersionResource.VERSION_HISTORY); + VERSION_SET.add(VersionResource.CHECKOUT_SET); + } + + /** + * Additional property names defined by JCR version history nodes. + */ + private static final DavPropertyNameSet VERSIONHISTORY_SET = new DavPropertyNameSet(); + static { + VERSIONHISTORY_SET.add(VersionHistoryResource.ROOT_VERSION); + VERSIONHISTORY_SET.add(VersionHistoryResource.VERSION_SET); + VERSIONHISTORY_SET.add(JCR_VERSIONABLEUUID); + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/RepositoryStubImpl.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/RepositoryStubImpl.java new file mode 100644 index 00000000000..24164f0fd68 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/RepositoryStubImpl.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2dav; + +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStub; +import org.apache.jackrabbit.test.RepositoryStubException; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.identifier.IdFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.apache.jackrabbit.jcr2spi.AbstractRepositoryConfig; +import org.apache.jackrabbit.jcr2spi.RepositoryImpl; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import java.security.Principal; +import java.util.Properties; + +/** + * RepositoryStubImpl... + */ +public class RepositoryStubImpl extends RepositoryStub { + + /** + * Property for the repository url + */ + public static final String PROP_REPOSITORY_URL = "org.apache.jackrabbit.jcr2spi.repository.url"; + + /** + * The repository instance + */ + private Repository repository; + + /** + * Overwritten constructor from base class. + */ + public RepositoryStubImpl(Properties env) { + super(env); + } + + @Override + public synchronized Repository getRepository() throws RepositoryStubException { + if (repository == null) { + try { + String url = environment.getProperty(PROP_REPOSITORY_URL); + final RepositoryService service = createService(url); + repository = RepositoryImpl.create(new AbstractRepositoryConfig() { + public RepositoryService getRepositoryService() { + return service; + } + }); + } catch (Exception e) { + throw new RepositoryStubException(e); + } + } + return repository; + } + + protected RepositoryService createService(String uri) throws RepositoryException { + IdFactory idFactory = IdFactoryImpl.getInstance(); + NameFactory nFactory = NameFactoryImpl.getInstance(); + PathFactory pFactory = PathFactoryImpl.getInstance(); + QValueFactory vFactory = QValueFactoryImpl.getInstance(); + return new RepositoryServiceImpl(uri, idFactory, nFactory, pFactory, vFactory); + } + + @Override + public Principal getKnownPrincipal(Session session) throws RepositoryException { + // TODO Auto-generated method stub + throw new RepositoryException("TBD"); + } + + @Override + public Principal getUnknownPrincipal(Session session) throws RepositoryException, NotExecutableException { + // TODO Auto-generated method stub + throw new RepositoryException("TBD"); + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/ServiceStubImpl.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/ServiceStubImpl.java new file mode 100644 index 00000000000..14f50a55428 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/ServiceStubImpl.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2dav; + +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceStub; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.identifier.IdFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; +import java.util.Properties; + +/** ServiceStubImpl... */ +public class ServiceStubImpl extends RepositoryServiceStub { + + private static Logger log = LoggerFactory.getLogger(ServiceStubImpl.class); + + public static final String PROP_REPOSITORY_URI = "org.apache.jackrabbit.spi.uri"; + + private RepositoryService service; + private Credentials adminCredentials; + private Credentials readOnlyCredentials; + + /** + * Implementations of this class must overwrite this constructor. + * + * @param env the environment variables. This parameter must not be null. + */ + public ServiceStubImpl(Properties env) { + super(env); + } + + @Override + public RepositoryService getRepositoryService() throws RepositoryException { + if (service == null) { + String uri = getProperty(PROP_REPOSITORY_URI); + IdFactory idFactory = IdFactoryImpl.getInstance(); + NameFactory nFactory = NameFactoryImpl.getInstance(); + PathFactory pFactory = PathFactoryImpl.getInstance(); + QValueFactory vFactory = QValueFactoryImpl.getInstance(); + service = new RepositoryServiceImpl(uri, idFactory, nFactory, pFactory, vFactory); + } + return service; + } + + @Override + public Credentials getAdminCredentials() { + if (adminCredentials == null) { + adminCredentials = new SimpleCredentials(getProperty(RepositoryServiceStub.PROP_PREFIX + "." + RepositoryServiceStub.PROP_ADMIN_NAME), + getProperty(RepositoryServiceStub.PROP_PREFIX + "." + RepositoryServiceStub.PROP_ADMIN_PWD).toCharArray()); + } + return adminCredentials; + } + + @Override + public Credentials getReadOnlyCredentials() { + if (readOnlyCredentials == null) { + readOnlyCredentials = new SimpleCredentials(getProperty(RepositoryServiceStub.PROP_PREFIX + "." + RepositoryServiceStub.PROP_READONLY_NAME), + getProperty(RepositoryServiceStub.PROP_PREFIX + "." + RepositoryServiceStub.PROP_READONLY_PWD).toCharArray()); + } + return readOnlyCredentials; + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/TestAll.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/TestAll.java new file mode 100644 index 00000000000..3f5cb18a9a6 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2dav/TestAll.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.spi2dav; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all testcases for package org.apache.jackrabbit.spi2dav. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("org.apache.jackrabbit.spi2dav tests"); + + // spi tests + suite.addTest(org.apache.jackrabbit.spi.TestAll.suite()); + + return suite; + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/BatchTest.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/BatchTest.java new file mode 100644 index 00000000000..97339e2f8dd --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/BatchTest.java @@ -0,0 +1,760 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2davex; + +import org.apache.jackrabbit.spi.AbstractSPITest; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + +/** + * ConnectionTest... + */ +public class BatchTest extends AbstractSPITest { + + private final String testPath = "/test"; + private NamePathResolver resolver; + private RepositoryService rs; + private SessionInfo si; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + rs = helper.getRepositoryService(); + si = helper.getAdminSessionInfo(); + NamespaceResolver nsResolver = new AbstractNamespaceResolver() { + public String getURI(String prefix) { + return ("jcr".equals(prefix)) ? "http://www.jcp.org/jcr/1.0" : prefix; + } + public String getPrefix(String uri) { + return ("http://www.jcp.org/jcr/1.0".equals(uri)) ? "jcr" : uri; + } + }; + resolver = new DefaultNamePathResolver(nsResolver); + + try { + rs.getNodeInfo(si, getNodeId(testPath)); + } catch (RepositoryException e) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.addNode(getNodeId("/"), resolver.getQName("test"), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + } + } + + @Override + protected void tearDown() throws Exception { + try { + Batch b = rs.createBatch(si, getNodeId("/")); + b.remove(getNodeId(testPath)); + rs.submit(b); + } finally { + rs.dispose(si); + super.tearDown(); + } + } + + public void testAddNode() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Batch b = rs.createBatch(si, nid); + + b.addNode(nid, resolver.getQName("aNode"), NameConstants.NT_UNSTRUCTURED, null); + b.addProperty(nid, resolver.getQName("aString"), rs.getQValueFactory().create("ba", PropertyType.STRING)); + b.addProperty(nid, resolver.getQName("aName"), new QValue[] {rs.getQValueFactory().create(NameConstants.JCR_ENCODING), rs.getQValueFactory().create(NameConstants.JCR_DATA)}); + b.addProperty(nid, resolver.getQName("aBinary"), rs.getQValueFactory().create(new byte[] { 'a', 'b', 'c'})); + + rs.submit(b); + + NodeId id = rs.getIdFactory().createNodeId(nid, resolver.getQPath("aNode")); + Iterator it = rs.getItemInfos(si, id); + while (it.hasNext()) { + ItemInfo info = it.next(); + if (info.denotesNode()) { + NodeInfo nInfo = (NodeInfo) info; + assertEquals(NameConstants.NT_UNSTRUCTURED, nInfo.getNodetype()); + Iterator childIt = nInfo.getChildInfos(); + assertTrue(childIt == null || !childIt.hasNext()); + assertEquals(id, nInfo.getId()); + } + } + + b = rs.createBatch(si, nid); + b.remove(id); + rs.submit(b); + } + + public void testImport() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Batch b = rs.createBatch(si, nid); + + String uuid = UUID.randomUUID().toString(); + b.addNode(nid, resolver.getQName("testUUIDNode"), NameConstants.NT_UNSTRUCTURED, uuid); + + NodeId id = getNodeId(testPath + "/testUUIDNode"); + b.setMixins(id, new Name[] {NameConstants.MIX_REFERENCEABLE}); + + rs.submit(b); + + NodeInfo nInfo = rs.getNodeInfo(si, id); + assertEquals(uuid, nInfo.getId().getUniqueID()); + Name[] mixins = nInfo.getMixins(); + assertEquals(1, mixins.length); + assertEquals(NameConstants.MIX_REFERENCEABLE, mixins[0]); + + b = rs.createBatch(si, nid); + b.remove(rs.getIdFactory().createNodeId(uuid)); + rs.submit(b); + + try { + rs.getItemInfos(si, id); + fail(); + } catch (RepositoryException e) { + // success + } + try { + rs.getItemInfos(si, rs.getIdFactory().createNodeId(uuid)); + fail(); + } catch (RepositoryException e) { + // success + } + } + + public void testSetMixin() throws RepositoryException { + NodeId nid = getNodeId(testPath); + + Batch b = rs.createBatch(si, nid); + b.addNode(nid, resolver.getQName("anyNode"), NameConstants.NT_UNSTRUCTURED, null); + NodeId id = getNodeId(testPath + "/anyNode"); + b.setMixins(id, new Name[] {NameConstants.MIX_LOCKABLE}); + rs.submit(b); + + b = rs.createBatch(si, id); + b.setMixins(id, new Name[0]); + rs.submit(b); + + NodeInfo nInfo = rs.getNodeInfo(si, id); + assertEquals(0, nInfo.getMixins().length); + } + + public void testMove() throws RepositoryException { + NodeId nid = getNodeId(testPath); + + Batch b = rs.createBatch(si, nid); + b.addNode(nid, resolver.getQName("anyNode"), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + + NodeId id = getNodeId(testPath + "/anyNode"); + + b = rs.createBatch(si, nid); + b.move(id, nid, resolver.getQName("moved")); + rs.submit(b); + + try { + rs.getItemInfos(si, id); + fail(); + } catch (RepositoryException e) { + // ok + } + + rs.getNodeInfo(si, getNodeId(testPath + "/moved")); + } + + public void testReorder() throws RepositoryException { + NodeId nid = getNodeId(testPath); + + Batch b = rs.createBatch(si, nid); + b.addNode(nid, resolver.getQName("1"), NameConstants.NT_UNSTRUCTURED, null); + b.addNode(nid, resolver.getQName("3"), NameConstants.NT_UNSTRUCTURED, null); + b.addNode(nid, resolver.getQName("2"), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + + b = rs.createBatch(si, nid); + b.reorderNodes(nid, getNodeId(testPath + "/3"), null); + rs.submit(b); + + Iterator it = rs.getChildInfos(si, nid); + int i = 1; + while (it.hasNext()) { + ChildInfo ci = it.next(); + assertEquals(i, Integer.parseInt(ci.getName().getLocalName())); + i++; + } + } + + public void testReorder1() throws RepositoryException { + NodeId nid = getNodeId(testPath); + + Batch b = rs.createBatch(si, nid); + b.addNode(nid, resolver.getQName("2"), NameConstants.NT_UNSTRUCTURED, null); + b.addNode(nid, resolver.getQName("3"), NameConstants.NT_UNSTRUCTURED, null); + b.addNode(nid, resolver.getQName("1"), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + + b = rs.createBatch(si, nid); + b.reorderNodes(nid, getNodeId(testPath + "/1"), getNodeId(testPath + "/2")); + rs.submit(b); + + Iterator it = rs.getChildInfos(si, nid); + int i = 1; + while (it.hasNext()) { + ChildInfo ci = it.next(); + assertEquals(i, Integer.parseInt(ci.getName().getLocalName())); + i++; + } + } + + public void testRemove() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Batch b = rs.createBatch(si, nid); + + NodeId id = getNodeId(testPath + "/aTestNode"); + b.addNode(nid, resolver.getQName("aTestNode"), NameConstants.NT_UNSTRUCTURED, null); + b.addProperty(id, resolver.getQName("aString"), rs.getQValueFactory().create("ba", PropertyType.STRING)); + rs.submit(b); + + PropertyId pid = getPropertyId(id, resolver.getQName("aString")); + b = rs.createBatch(si, nid); + b.remove(pid); + rs.submit(b); + + try { + rs.getPropertyInfo(si, pid); + fail(); + } catch (RepositoryException e) { + // success + } + } + + public void testEmptyValueArray() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("mvProperty"); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, new QValue[0]); + rs.submit(b); + + PropertyId pid = getPropertyId(nid, propName); + PropertyInfo pi = rs.getPropertyInfo(si, pid); + assertTrue(pi.isMultiValued()); + assertEquals(Arrays.asList(new QValue[0]), Arrays.asList(pi.getValues())); + assertFalse(pi.getType() == PropertyType.UNDEFINED); + + Iterator it = rs.getItemInfos(si, nid); + while (it.hasNext()) { + ItemInfo info = it.next(); + if (!info.denotesNode()) { + PropertyInfo pInfo = (PropertyInfo) info; + if (propName.equals((pInfo.getId().getName()))) { + assertTrue(pi.isMultiValued()); + assertEquals(Arrays.asList(new QValue[0]), Arrays.asList(pi.getValues())); + assertFalse(pi.getType() == PropertyType.UNDEFINED); + break; + } + } + } + } + + public void testEmptyValueArray2() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("mvProperty"); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, new QValue[] { rs.getQValueFactory().create(true)}); + rs.submit(b); + + PropertyId pid = getPropertyId(nid, propName); + b = rs.createBatch(si, pid); + b.setValue(pid, new QValue[0]); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, pid); + assertTrue(pi.isMultiValued()); + assertEquals(Arrays.asList(new QValue[0]), Arrays.asList(pi.getValues())); + } + + public void testMultiValuedProperty() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("mvProperty2"); + QValue[] vs = new QValue[] {rs.getQValueFactory().create(111), rs.getQValueFactory().create(222), rs.getQValueFactory().create(333)}; + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, vs); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertTrue(pi.isMultiValued()); + assertEquals(Arrays.asList(vs), Arrays.asList(pi.getValues())); + assertEquals(PropertyType.LONG, pi.getType()); + } + + public void testSetBinaryValue() throws RepositoryException, IOException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("binProp"); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, rs.getQValueFactory().create(new byte[] {'a', 'b', 'c'})); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertEquals("abc", pi.getValues()[0].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + } + + public void testSetEmptyBinaryValue() throws RepositoryException, IOException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("binProp"); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, rs.getQValueFactory().create(new byte[0])); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + InputStream in = pi.getValues()[0].getStream(); + assertTrue(in.read() == -1); + assertEquals("", pi.getValues()[0].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertFalse(pi.isMultiValued()); + in = pi.getValues()[0].getStream(); + assertTrue(in.read() == -1); + assertEquals("", pi.getValues()[0].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + } + + public void testSetBinaryValues() throws RepositoryException, IOException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("binPropMV"); + + Batch b = rs.createBatch(si, nid); + QValue[] vs = new QValue[] { + rs.getQValueFactory().create(new byte[] {'a', 'b', 'c'}), + rs.getQValueFactory().create(new byte[] {'d', 'e', 'f'}), + rs.getQValueFactory().create(new byte[] {'g', 'h', 'i'}) + }; + b.addProperty(nid, propName, vs); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertTrue(pi.isMultiValued()); + vs = pi.getValues(); + assertEquals("abc", vs[0].getString()); + assertEquals("def", vs[1].getString()); + assertEquals("ghi", vs[2].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + + pi = getPropertyInfo(nid, propName); + vs = pi.getValues(); + assertEquals("abc", vs[0].getString()); + assertEquals("def", vs[1].getString()); + assertEquals("ghi", vs[2].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + } + + public void testSetMixedBinaryValues() throws RepositoryException, IOException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("binPropMV"); + + Batch b = rs.createBatch(si, nid); + QValue[] vs = new QValue[] { + rs.getQValueFactory().create(new byte[] {'a', 'b', 'c'}), + rs.getQValueFactory().create(new byte[0]), + rs.getQValueFactory().create(new byte[] {'g', 'h', 'i'}) + }; + b.addProperty(nid, propName, vs); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertTrue(pi.isMultiValued()); + vs = pi.getValues(); + assertEquals("abc", vs[0].getString()); + assertEquals("", vs[1].getString()); + assertEquals("ghi", vs[2].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + + pi = getPropertyInfo(nid, propName); + vs = pi.getValues(); + assertEquals("abc", vs[0].getString()); + assertEquals("", vs[1].getString()); + assertEquals("ghi", vs[2].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + } + + public void testSetEmptyBinaryValues() throws RepositoryException, IOException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("binPropMV"); + + Batch b = rs.createBatch(si, nid); + QValue[] vs = new QValue[] { + rs.getQValueFactory().create(new byte[0]), + rs.getQValueFactory().create(new byte[0]), + rs.getQValueFactory().create(new byte[0]) + }; + b.addProperty(nid, propName, vs); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertTrue(pi.isMultiValued()); + vs = pi.getValues(); + assertEquals("", vs[0].getString()); + assertEquals("", vs[1].getString()); + assertEquals("", vs[2].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + + pi = getPropertyInfo(nid, propName); + vs = pi.getValues(); + assertEquals("", vs[0].getString()); + assertEquals("", vs[1].getString()); + assertEquals("", vs[2].getString()); + assertEquals(PropertyType.BINARY, pi.getType()); + } + + public void testBinary() throws RepositoryException, IOException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("binProp"); + + Batch b = rs.createBatch(si, nid); + ClassLoader loader = getClass().getClassLoader(); + InputStream in = loader.getResourceAsStream("org/apache/jackrabbit/spi/spi2davex/image.bmp"); + if (in != null) { + try { + QValue v = rs.getQValueFactory().create(in); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + String str1 = pi.getValues()[0].getString(); + + pi = getPropertyInfo(nid, propName); + String str2 = pi.getValues()[0].getString(); + assertEquals(str1, str2); + } finally { + in.close(); + } + } + } + + public void testSetDoubleValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("doubleProp"); + + QValue v = rs.getQValueFactory().create((double) 12); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertEquals(v, pi.getValues()[0]); + assertEquals(v.getString(), pi.getValues()[0].getString()); + assertEquals(PropertyType.DOUBLE, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertEquals(v, pi.getValues()[0]); + assertEquals(v.getString(), pi.getValues()[0].getString()); + assertEquals(PropertyType.DOUBLE, pi.getType()); + } + + public void testSetLongValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("doubleProp"); + + QValue v = rs.getQValueFactory().create(234567); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertEquals(v, pi.getValues()[0]); + assertEquals(v.getString(), pi.getValues()[0].getString()); + assertEquals(PropertyType.LONG, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertEquals(v, pi.getValues()[0]); + assertEquals(v.getString(), pi.getValues()[0].getString()); + assertEquals(PropertyType.LONG, pi.getType()); + } + + public void testSetDateValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("dateProp"); + + QValue v = rs.getQValueFactory().create(Calendar.getInstance()); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertEquals(v, pi.getValues()[0]); + assertEquals(v.getString(), pi.getValues()[0].getString()); + assertEquals(PropertyType.DATE, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertEquals(v, pi.getValues()[0]); + assertEquals(v.getString(), pi.getValues()[0].getString()); + assertEquals(PropertyType.DATE, pi.getType()); + } + + public void testSetStringValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("stringProp"); + QValueFactory vf = rs.getQValueFactory(); + + List l = new ArrayList(); + l.add("String value containing \"double quotes\" and \'single\' and \"undeterminated quote."); + l.add("String value \ncontaining \n\rline \r\nseparators and \t tab."); + l.add("String value containing \r\n\r\r\n\r\n multiple \r\n\r\n line separators in sequence."); + l.add("String value containing >diff -char +act ^ters."); + l.add("String value containing \n>line sep \r+and \r\n-diff\n\r^chars."); + l.add("String value containing \u0633\u0634 unicode chars."); + + for (String val : l) { + QValue v = vf.create(val, PropertyType.STRING); + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = getPropertyInfo(nid, propName); + assertEquals(v, pi.getValues()[0]); + assertEquals(v.getString(), pi.getValues()[0].getString()); + assertEquals(PropertyType.STRING, pi.getType()); + + pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertEquals(v, pi.getValues()[0]); + assertEquals(v.getString(), pi.getValues()[0].getString()); + assertEquals(PropertyType.STRING, pi.getType()); + } + } + + public void testSetNameValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("nameProp"); + + QValue[] vs = new QValue[] { + rs.getQValueFactory().create(NameConstants.JCR_BASEVERSION), + rs.getQValueFactory().create(NameConstants.JCR_DEFAULTPRIMARYTYPE), + rs.getQValueFactory().create(NameConstants.MIX_LOCKABLE), + rs.getQValueFactory().create(NameConstants.JCR_PRIMARYTYPE), + rs.getQValueFactory().create(NameConstants.NT_VERSION) + }; + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, vs); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertTrue(pi.isMultiValued()); + assertEquals(Arrays.asList(vs), Arrays.asList(pi.getValues())); + assertEquals(PropertyType.NAME, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertEquals(vs[0].getName(), pi.getValues()[0].getName()); + assertEquals(Arrays.asList(vs), Arrays.asList(pi.getValues())); + assertEquals(PropertyType.NAME, pi.getType()); + } + + public void testSetPathValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("pathProp"); + + QValue v = rs.getQValueFactory().create(resolver.getQPath(testPath)); + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertEquals(v, pi.getValues()[0]); + assertEquals(PropertyType.PATH, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertEquals(v.getPath(), pi.getValues()[0].getPath()); + assertEquals(v, pi.getValues()[0]); + assertEquals(PropertyType.PATH, pi.getType()); + } + + public void testSetBooleanValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("booleanProp"); + + QValue v = rs.getQValueFactory().create(false); + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertFalse(pi.getValues()[0].getBoolean()); + assertEquals(PropertyType.BOOLEAN, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertFalse(pi.getValues()[0].getBoolean()); + assertEquals(PropertyType.BOOLEAN, pi.getType()); + } + + public void testSetReferenceValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + NodeInfo nInfo = rs.getNodeInfo(si, nid); + if (!Arrays.asList(nInfo.getMixins()).contains(NameConstants.MIX_REFERENCEABLE)) { + Batch b = rs.createBatch(si, nid); + b.setMixins(nid, new Name[] {NameConstants.MIX_REFERENCEABLE}); + rs.submit(b); + } + + String ref = rs.getNodeInfo(si, nid).getId().getUniqueID(); + Name propName = resolver.getQName("refProp"); + QValue v = rs.getQValueFactory().create(ref, PropertyType.REFERENCE); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertEquals(v, pi.getValues()[0]); + assertEquals(PropertyType.REFERENCE, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertEquals(v, pi.getValues()[0]); + assertEquals(PropertyType.REFERENCE, pi.getType()); + } + + public void testSetWeakReferenceValue() throws RepositoryException { + NodeId nid = getNodeId(testPath); + NodeInfo nInfo = rs.getNodeInfo(si, nid); + if (!Arrays.asList(nInfo.getMixins()).contains(NameConstants.MIX_REFERENCEABLE)) { + Batch b = rs.createBatch(si, nid); + b.setMixins(nid, new Name[] {NameConstants.MIX_REFERENCEABLE}); + rs.submit(b); + } + + String ref = rs.getNodeInfo(si, nid).getId().getUniqueID(); + Name propName = resolver.getQName("weakRefProp"); + QValue v = rs.getQValueFactory().create(ref, PropertyType.WEAKREFERENCE); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertEquals(v, pi.getValues()[0]); + assertEquals(PropertyType.WEAKREFERENCE, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertEquals(v, pi.getValues()[0]); + assertEquals(PropertyType.WEAKREFERENCE, pi.getType()); + } + + public void testSetPropertyTwice() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Name propName = resolver.getQName("nameProp"); + PropertyId pid = getPropertyId(nid, propName); + + QValue v = rs.getQValueFactory().create(NameConstants.JCR_AUTOCREATED); + QValue v2 = rs.getQValueFactory().create(NameConstants.JCR_BASEVERSION); + QValue v3 = rs.getQValueFactory().create(NameConstants.JCR_CONTENT); + + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, propName, v); + b.setValue(pid, v2); + b.setValue(pid, v3); + rs.submit(b); + + PropertyInfo pi = rs.getPropertyInfo(si, getPropertyId(nid, propName)); + assertFalse(pi.isMultiValued()); + assertEquals(1, pi.getValues().length); + assertEquals(v3, pi.getValues()[0]); + assertEquals(PropertyType.NAME, pi.getType()); + + pi = getPropertyInfo(nid, propName); + assertFalse(pi.isMultiValued()); + assertEquals(1, pi.getValues().length); + assertEquals(v3, pi.getValues()[0]); + assertEquals(PropertyType.NAME, pi.getType()); + } + + public void testUseConsumedBatch() throws RepositoryException { + NodeId nid = getNodeId(testPath); + Batch b = rs.createBatch(si, nid); + b.addProperty(nid, resolver.getQName("any"), rs.getQValueFactory().create(1.34)); + rs.submit(b); + + try { + b.remove(nid); + rs.submit(b); + fail(); + } catch (IllegalStateException e) { + // success + } + } + //-------------------------------------------------------------------------- + private NodeId getNodeId(String path) throws NamespaceException, RepositoryException { + return rs.getIdFactory().createNodeId((String) null, resolver.getQPath(path)); + } + + private PropertyId getPropertyId(NodeId nId, Name propName) throws RepositoryException { + return rs.getIdFactory().createPropertyId(nId, propName); + } + + private PropertyInfo getPropertyInfo(NodeId parentId, Name propName) throws RepositoryException { + Iterator it = rs.getItemInfos(si, parentId); + while (it.hasNext()) { + ItemInfo info = it.next(); + if (!info.denotesNode()) { + PropertyInfo pInfo = (PropertyInfo) info; + if (propName.equals((pInfo.getId().getName()))) { + return pInfo; + } + } + } + throw new ItemNotFoundException(); + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CloneTest.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CloneTest.java new file mode 100644 index 00000000000..18a358c051f --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CloneTest.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.spi2davex; + +import org.apache.jackrabbit.spi.AbstractSPITest; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import java.util.Iterator; + +/** + * CloneTest... + */ +public class CloneTest extends AbstractSPITest { + + private final String testPath = "/test"; + private NamePathResolver resolver; + private RepositoryService rs; + private SessionInfo si; + + private SessionInfo sInfo; + private NodeId clonedId; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + rs = helper.getRepositoryService(); + si = helper.getAdminSessionInfo(); + NamespaceResolver nsResolver = new AbstractNamespaceResolver() { + public String getURI(String prefix) throws NamespaceException { + return ("jcr".equals(prefix)) ? "http://www.jcp.org/jcr/1.0" : prefix; + } + public String getPrefix(String uri) throws NamespaceException { + return ("http://www.jcp.org/jcr/1.0".equals(uri)) ? "jcr" : uri; + } + }; + resolver = new DefaultNamePathResolver(nsResolver); + + try { + rs.getNodeInfo(si, getNodeId(testPath)); + } catch (RepositoryException e) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.addNode(getNodeId("/"), resolver.getQName("test"), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + } + + // todo: retrieve second wsp-name from config + sInfo = rs.obtain(si, "test"); + } + + @Override + protected void tearDown() throws Exception { + try { + if (si != null) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.remove(getNodeId(testPath)); + rs.submit(b); + } + if (sInfo != null && clonedId != null) { + Batch b = rs.createBatch(sInfo, getNodeId("/")); + b.remove(clonedId); + rs.submit(b); + } + } catch (RepositoryException e) { + // cleanup failed... ignore. + } finally { + if (sInfo != null) { + rs.dispose(sInfo); + } + if (si != null) { + rs.dispose(si); + } + super.tearDown(); + } + } + + private NodeId getNodeId(String path) throws RepositoryException { + return rs.getIdFactory().createNodeId((String) null, resolver.getQPath(path)); + } + + public void testClone() throws RepositoryException { + NodeId srcId = getNodeId(testPath); + NodeId destParentId = getNodeId("/"); + rs.clone(sInfo, si.getWorkspaceName(), srcId, destParentId, resolver.getQName("destname"), true); + + clonedId = getNodeId("/destname"); + NodeInfo nInfo = rs.getNodeInfo(sInfo, clonedId); + Iterator it = rs.getItemInfos(sInfo, clonedId); + + assertTrue(it.hasNext()); + NodeInfo nInfo2 = (NodeInfo) it.next(); + assertEquals(nInfo.getId(), nInfo2.getId()); + assertEquals(nInfo.getNodetype(), nInfo2.getNodetype()); + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ConnectionTest.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ConnectionTest.java new file mode 100644 index 00000000000..ef4d7baa13b --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ConnectionTest.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.spi2davex; + +import org.apache.jackrabbit.spi.AbstractSPITest; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.LoginException; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +/** + * ConnectionTest... + */ +public class ConnectionTest extends AbstractSPITest { + + private NamePathResolver resolver; + private RepositoryService rs; + + @Override + protected void setUp() throws Exception { + super.setUp(); + rs = helper.getRepositoryService(); + NamespaceResolver nsResolver = new AbstractNamespaceResolver() { + public String getURI(String prefix) { + return ("jcr".equals(prefix)) ? "http://www.jcp.org/jcr/1.0" : prefix; + } + public String getPrefix(String uri) { + return ("http://www.jcp.org/jcr/1.0".equals(uri)) ? "jcr" : uri; + } + }; + resolver = new DefaultNamePathResolver(nsResolver); + } + + public void testObtainWithNullWorkspaceName() throws RepositoryException, LoginException { + SessionInfo sInfo = rs.obtain(new SimpleCredentials("admin", "admin".toCharArray()), null); + try { + assertEquals("default",sInfo.getWorkspaceName()); + } finally { + rs.dispose(sInfo); + } + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CopyTest.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CopyTest.java new file mode 100644 index 00000000000..8386c5d686f --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CopyTest.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.spi2davex; + +import org.apache.jackrabbit.spi.AbstractSPITest; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.RepositoryException; +import java.util.Iterator; + +/** + * CopyTest... + */ +public class CopyTest extends AbstractSPITest { + private final String testPath = "/test"; + private NamePathResolver resolver; + private RepositoryService rs; + private SessionInfo si; + + private SessionInfo sInfo; + private NodeId copiedId; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + rs = helper.getRepositoryService(); + si = helper.getAdminSessionInfo(); + NamespaceResolver nsResolver = new AbstractNamespaceResolver() { + public String getURI(String prefix) { + return ("jcr".equals(prefix)) ? "http://www.jcp.org/jcr/1.0" : prefix; + } + public String getPrefix(String uri) { + return ("http://www.jcp.org/jcr/1.0".equals(uri)) ? "jcr" : uri; + } + }; + resolver = new DefaultNamePathResolver(nsResolver); + + try { + rs.getNodeInfo(si, getNodeId(testPath)); + } catch (RepositoryException e) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.addNode(getNodeId("/"), resolver.getQName("test"), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + } + } + + @Override + protected void tearDown() throws Exception { + try { + if (si != null) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.remove(getNodeId(testPath)); + rs.submit(b); + } + if (sInfo != null && copiedId != null) { + Batch b = rs.createBatch(sInfo, getNodeId("/")); + b.remove(copiedId); + rs.submit(b); + } + } catch (RepositoryException e) { + // cleanup failed... ignore. + } finally { + if (sInfo != null) { + rs.dispose(sInfo); + } + if (si != null) { + rs.dispose(si); + } + super.tearDown(); + } + } + + private NodeId getNodeId(String path) throws RepositoryException { + return rs.getIdFactory().createNodeId((String) null, resolver.getQPath(path)); + } + + public void testCopy() throws RepositoryException { + NodeId nid = null; + try { + NodeId srcId = getNodeId(testPath); + NodeId destParentId = getNodeId("/"); + rs.copy(si, si.getWorkspaceName(), srcId, destParentId, resolver.getQName("destname")); + + nid = getNodeId("/destname"); + NodeInfo nInfo = rs.getNodeInfo(si, nid); + Iterator it = rs.getItemInfos(si, nid); + + assertTrue(it.hasNext()); + NodeInfo nInfo2 = (NodeInfo) it.next(); + assertEquals(nInfo.getId(), nInfo2.getId()); + assertEquals(nInfo.getNodetype(), nInfo2.getNodetype()); + } finally { + if (nid != null) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.remove(nid); + rs.submit(b); + } + } + } + + public void testCopyAcrossWorkspaces() throws RepositoryException { + // todo: retrieve second wsp-name from config + sInfo = rs.obtain(si, "test"); + + NodeId srcId = getNodeId(testPath); + NodeId destParentId = getNodeId("/"); + rs.copy(sInfo, si.getWorkspaceName(), srcId, destParentId, resolver.getQName("destname")); + + copiedId = getNodeId("/destname"); + NodeInfo nInfo = rs.getNodeInfo(sInfo, copiedId); + Iterator it = rs.getItemInfos(sInfo, copiedId); + + assertTrue(it.hasNext()); + NodeInfo nInfo2 = (NodeInfo) it.next(); + assertEquals(nInfo.getId(), nInfo2.getId()); + assertEquals(nInfo.getNodetype(), nInfo2.getNodetype()); + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CreateFileTest.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CreateFileTest.java new file mode 100644 index 00000000000..1edb665e402 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/CreateFileTest.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.spi2davex; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.spi.AbstractSPITest; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +/** + * CreateFileTest... + */ +public class CreateFileTest extends AbstractSPITest { + + private final String testPath = "/test"; + private NamePathResolver resolver; + private RepositoryService rs; + private SessionInfo si; + + private QValue lastModified; + private QValue mimeType; + private QValue enc; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + rs = helper.getRepositoryService(); + si = helper.getAdminSessionInfo(); + NamespaceResolver nsResolver = new AbstractNamespaceResolver() { + public String getURI(String prefix) { + return ("jcr".equals(prefix)) ? "http://www.jcp.org/jcr/1.0" : prefix; + } + public String getPrefix(String uri) { + return ("http://www.jcp.org/jcr/1.0".equals(uri)) ? "jcr" : uri; + } + }; + resolver = new DefaultNamePathResolver(nsResolver); + + try { + rs.getNodeInfo(si, getNodeId(testPath)); + } catch (RepositoryException e) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.addNode(getNodeId("/"), resolver.getQName("test"), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + } + + lastModified = rs.getQValueFactory().create(Calendar.getInstance()); + mimeType = rs.getQValueFactory().create("text/plain", PropertyType.STRING); + enc = rs.getQValueFactory().create("utf-8", PropertyType.STRING); + } + + @Override + protected void tearDown() throws Exception { + try { + Batch b = rs.createBatch(si, getNodeId("/")); + b.remove(getNodeId(testPath)); + rs.submit(b); + } finally { + rs.dispose(si); + super.tearDown(); + } + } + + private NodeId getNodeId(String path) throws RepositoryException { + return rs.getIdFactory().createNodeId((String) null, + resolver.getQPath(path)); + } + + public void testCreateFile() throws RepositoryException, IOException { + Name fileName = resolver.getQName("test.txt"); + createFile(fileName); + } + + public void testCreateFileWithNonLatinCharacters() throws RepositoryException, IOException { + Name fileName = resolver.getQName("\u0633\u0634.txt"); + createFile(fileName); + } + + public void testPropertiesWithNonLatinCharacters() throws RepositoryException, IOException { + Name fileName = resolver.getQName("\u0633\u0634.txt"); + createFile(fileName); + + NodeId nid = getNodeId(testPath + "/\u0633\u0634.txt/jcr:content"); + + PropertyInfo pi = rs.getPropertyInfo(si, rs.getIdFactory().createPropertyId(nid, NameConstants.JCR_LASTMODIFIED)); + assertEquals(lastModified, pi.getValues()[0]); + + pi = rs.getPropertyInfo(si, rs.getIdFactory().createPropertyId(nid, NameConstants.JCR_MIMETYPE)); + assertEquals(mimeType, pi.getValues()[0]); + + pi = rs.getPropertyInfo(si, rs.getIdFactory().createPropertyId(nid, NameConstants.JCR_ENCODING)); + assertEquals(enc, pi.getValues()[0]); + + pi = rs.getPropertyInfo(si, rs.getIdFactory().createPropertyId(nid, NameConstants.JCR_DATA)); + assertEquals("\u0633\u0634", pi.getValues()[0].getString()); + } + + private void createFile(Name fileName) throws RepositoryException, IOException { + NodeId root = getNodeId(testPath); + + Batch b = rs.createBatch(si, root); + b.addNode(root, fileName, NameConstants.NT_FILE, null); + + String filePath = testPath + "/" + fileName.getLocalName(); + NodeId file = getNodeId(filePath); + b.addNode(file, NameConstants.JCR_CONTENT, NameConstants.NT_RESOURCE, null); + + NodeId content = getNodeId(filePath + "/" + NameConstants.JCR_CONTENT); + b.addProperty(content, resolver.getQName(JcrConstants.JCR_LASTMODIFIED), lastModified); + b.addProperty(content, resolver.getQName(JcrConstants.JCR_MIMETYPE), mimeType); + b.addProperty(content, resolver.getQName(JcrConstants.JCR_ENCODING), enc); + + InputStream data = new ByteArrayInputStream("\u0633\u0634".getBytes("UTF-8")); + b.addProperty(content, resolver.getQName(JcrConstants.JCR_DATA), rs.getQValueFactory().create(data)); + + rs.submit(b); + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ExtensionTest.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ExtensionTest.java new file mode 100644 index 00000000000..85a579f62d0 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ExtensionTest.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.spi2davex; + +import org.apache.jackrabbit.spi.AbstractSPITest; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.util.Text; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import java.util.Iterator; + +/** + * ExtensionTest... + */ +public class ExtensionTest extends AbstractSPITest { + + private String testPath; + private NamePathResolver resolver; + private RepositoryService rs; + private SessionInfo si; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + rs = helper.getRepositoryService(); + si = helper.getAdminSessionInfo(); + NamespaceResolver nsResolver = new AbstractNamespaceResolver() { + public String getURI(String prefix) { + return ("jcr".equals(prefix)) ? "http://www.jcp.org/jcr/1.0" : prefix; + } + public String getPrefix(String uri) { + return ("http://www.jcp.org/jcr/1.0".equals(uri)) ? "jcr" : uri; + } + }; + resolver = new DefaultNamePathResolver(nsResolver); + } + + @Override + protected void tearDown() throws Exception { + try { + removeTestNode(testPath); + } finally { + rs.dispose(si); + super.tearDown(); + } + } + + private void createTestNode(String testPath) throws RepositoryException { + Batch b = rs.createBatch(si, getNodeId("/")); + String name = Text.getName(testPath); + b.addNode(getNodeId("/"), resolver.getQName(name), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + } + + private void removeTestNode(String path) throws RepositoryException { + Batch b = rs.createBatch(si, getNodeId("/")); + b.remove(getNodeId(path)); + rs.submit(b); + } + + private NodeId getNodeId(String path) throws NamespaceException, RepositoryException { + return rs.getIdFactory().createNodeId((String) null, resolver.getQPath(path)); + } + + private void assertProperDepthExtensionHandling(String path, String name, boolean create) throws RepositoryException { + Name testName = resolver.getQName(name); + if (create) { + createTestNode(path); + testPath = path; + } + + NodeInfo nInfo = rs.getNodeInfo(si, getNodeId(path)); + //System.out.println("NodeInfo: " + nInfo.getPath().getNameElement().getName()); + assertEquals(testName, nInfo.getPath().getName()); + + Iterator it = rs.getItemInfos(si, getNodeId(path)); + assertTrue(it.hasNext()); + nInfo = (NodeInfo) it.next(); + //System.out.println("ItemInfo: " + nInfo.getPath().getNameElement().getName()); + assertEquals(testName, nInfo.getPath().getName()); + } + + public void testJsonExtension() throws RepositoryException { + assertProperDepthExtensionHandling("/test.json", "test.json", true); + } + + public void testNumberExtension() throws RepositoryException { + assertProperDepthExtensionHandling("/test.24", "test.24", true); + } + + public void testNumberJsonExtension() throws RepositoryException { + assertProperDepthExtensionHandling("/test.5.json", "test.5.json", true); + } + + public void testNumberJsonExtension2() throws RepositoryException { + assertProperDepthExtensionHandling("/test.5.json.json", "test.5.json.json", true); + } + + public void testMultipleNodes() throws RepositoryException { + createTestNode("/test"); + try { + assertProperDepthExtensionHandling("/test.json", "test.json", true); + assertProperDepthExtensionHandling("/test", "test", false); + } finally { + removeTestNode("/test"); + } + } + + public void testIndex() throws RepositoryException { + Name testName = resolver.getQName("test"); + testPath = "/test"; + createTestNode("/test"); + createTestNode("/test"); + + NodeInfo nInfo = rs.getNodeInfo(si, getNodeId("/test[2]")); + //System.out.println("NodeInfo: " + nInfo.getPath().getNameElement().getName()); + assertEquals(testName, nInfo.getPath().getName()); + + Iterator it = rs.getItemInfos(si, getNodeId("/test[2]")); + assertTrue(it.hasNext()); + nInfo = (NodeInfo) it.next(); + //System.out.println("ItemInfo: " + nInfo.getPath().getNameElement().getName()); + assertEquals(testName, nInfo.getPath().getName()); + + removeTestNode("/test[2]"); + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ReadTest.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ReadTest.java new file mode 100644 index 00000000000..7438c8003e7 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ReadTest.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.spi2davex; + +import org.apache.jackrabbit.spi.AbstractSPITest; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.AbstractNamespaceResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; + +/** + * ConnectionTest... + */ +public class ReadTest extends AbstractSPITest { + + private final String testPath = "/test"; + private NamePathResolver resolver; + private RepositoryService rs; + private SessionInfo si; + + @Override + protected void setUp() throws Exception { + super.setUp(); + rs = helper.getRepositoryService(); + si = helper.getAdminSessionInfo(); + + NamespaceResolver nsResolver = new AbstractNamespaceResolver() { + public String getURI(String prefix) { + return ("jcr".equals(prefix)) ? "http://www.jcp.org/jcr/1.0" : prefix; + } + public String getPrefix(String uri) { + return ("http://www.jcp.org/jcr/1.0".equals(uri)) ? "jcr" : uri; + } + }; + resolver = new DefaultNamePathResolver(nsResolver); + + try { + rs.getNodeInfo(si, getNodeId(testPath)); + } catch (RepositoryException e) { + Batch b = rs.createBatch(si, getNodeId("/")); + b.addNode(getNodeId("/"), resolver.getQName("test"), NameConstants.NT_UNSTRUCTURED, null); + rs.submit(b); + } + } + + @Override + protected void tearDown() throws Exception { + try { + Batch b = rs.createBatch(si, getNodeId("/")); + b.remove(getNodeId(testPath)); + rs.submit(b); + } finally { + rs.dispose(si); + super.tearDown(); + } + } + + public void testReadNode() throws RepositoryException { + NodeId nid = getNodeId(testPath); + rs.getItemInfos(si, nid); + } + + public void testReadNonExistingNode() throws RepositoryException { + NodeId nid = getNodeId(testPath + "/non-existing"); + try { + rs.getItemInfos(si, nid); + fail(); + } catch (ItemNotFoundException e) { + // ok + } + } + + public void testReadNonExistingNode2() throws RepositoryException { + NodeId nid = getNodeId(testPath + "/non-existing"); + try { + rs.getNodeInfo(si, nid); + fail(); + } catch (ItemNotFoundException e) { + // ok + } + } + + public void testReadPropertyAsNode() throws RepositoryException { + NodeId nid = getNodeId(testPath + "/jcr:primaryType"); + try { + rs.getItemInfos(si, nid); + fail(); + } catch (ItemNotFoundException e) { + // ok + } + } + + public void testReadNonExistingProperty() throws RepositoryException { + NodeId nid = getNodeId(testPath); + PropertyId pid = getPropertyId(nid, NameConstants.JCR_CHILDNODEDEFINITION); + + try { + rs.getPropertyInfo(si, pid); + fail(); + } catch (ItemNotFoundException e) { + // ok + } + } + + //-------------------------------------------------------------------------- + private NodeId getNodeId(String path) throws NamespaceException, RepositoryException { + return rs.getIdFactory().createNodeId((String) null, resolver.getQPath(path)); + } + + private PropertyId getPropertyId(NodeId nId, Name propName) throws RepositoryException { + return rs.getIdFactory().createPropertyId(nId, propName); + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/RepositoryStubImpl.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/RepositoryStubImpl.java new file mode 100644 index 00000000000..a73f5631a5d --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/RepositoryStubImpl.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.spi2davex; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; + +import javax.jcr.NamespaceException; +import javax.jcr.RepositoryException; +import java.util.Properties; + +/** + * RepositoryStubImpl... + */ +public class RepositoryStubImpl extends org.apache.jackrabbit.spi2dav.RepositoryStubImpl { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(RepositoryStubImpl.class); + + /** + * Overwritten constructor from base class. + */ + public RepositoryStubImpl(Properties env) { + super(env); + } + + @Override + protected RepositoryService createService(String uri) throws RepositoryException { + BatchReadConfig brc = new BatchReadConfig() { + public int getDepth(Path path, PathResolver resolver) throws NamespaceException { + return 4; + } + }; + return new RepositoryServiceImpl(uri, brc); + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ServiceStubImpl.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ServiceStubImpl.java new file mode 100644 index 00000000000..73d05959a5c --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/ServiceStubImpl.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.spi2davex; + +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.RepositoryException; +import java.util.Properties; + +/** ServiceStubImpl... */ +public class ServiceStubImpl extends org.apache.jackrabbit.spi2dav.ServiceStubImpl { + + private static Logger log = LoggerFactory.getLogger(ServiceStubImpl.class); + + public static final String PROP_DEFAULT_DEPTH = "org.apache.jackrabbit.spi2davex.defaultDepth"; + + private RepositoryService service; + + /** + * Implementations of this class must overwrite this constructor. + * + * @param env the environment variables. This parameter must not be null. + */ + public ServiceStubImpl(Properties env) { + super(env); + } + + @Override + public RepositoryService getRepositoryService() throws RepositoryException { + if (service == null) { + String uri = getProperty(PROP_REPOSITORY_URI); + service = new RepositoryServiceImpl(uri, new BatchReadConfig() { + public int getDepth(Path path, PathResolver resolver) { + String depthStr = getProperty(PROP_DEFAULT_DEPTH); + if (depthStr != null) { + try { + return Integer.parseInt(depthStr); + } catch (Exception e) { + // ignore + } + } + return 4; + } + }); + } + return service; + } +} diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/TestAll.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/TestAll.java new file mode 100644 index 00000000000..94fbdab7923 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/spi2davex/TestAll.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.spi2davex; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +public class TestAll extends TestCase { + + public static Test suite() { + + TestSuite suite = new TestSuite("org.apache.jackrabbit.spi2davex tests"); + + // spi tests + suite.addTest(org.apache.jackrabbit.spi.TestAll.suite()); + + // impl specific spi tests + suite.addTestSuite(ConnectionTest.class); + suite.addTestSuite(ReadTest.class); + suite.addTestSuite(BatchTest.class); + suite.addTestSuite(CopyTest.class); + suite.addTestSuite(CloneTest.class); + suite.addTestSuite(ExtensionTest.class); + suite.addTestSuite(CreateFileTest.class); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/test/TestAll.java b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/test/TestAll.java new file mode 100644 index 00000000000..da8095afdfe --- /dev/null +++ b/jackrabbit-spi2dav/src/test/java/org/apache/jackrabbit/test/TestAll.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.test; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Execute all API tests from jackrabbit-jcr-test and jackrabbit-jcr2spi. + */ +public class TestAll extends TestCase { + + public static Test suite() { + TestSuite suite = new TestSuite("JCR Test : jcr-test and jcr2spi"); + suite.addTest(new org.apache.jackrabbit.test.JCRTestSuite()); + suite.addTest(new org.apache.jackrabbit.jcr2spi.Jcr2SpiTestSuite()); + + return suite; + } +} \ No newline at end of file diff --git a/jackrabbit-spi2dav/src/test/resources/log4j.properties b/jackrabbit-spi2dav/src/test/resources/log4j.properties new file mode 100644 index 00000000000..620328316b5 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/resources/log4j.properties @@ -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. + +# Set root logger level to INFO and its only appender to file. +log4j.rootLogger=INFO, file +#log4j.rootLogger=DEBUG, stdout, file +#log4j.rootLogger=ERROR, stdout, file + +log4j.logger.org.apache.jackrabbit.test=DEBUG + +# 'stdout' is set to be a ConsoleAppender. +log4j.appender.stdout=org.apache.log4j.ConsoleAppender + +# 'stdout' uses PatternLayout +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n + +# 'file' is set to be a FileAppender. +log4j.appender.file=org.apache.log4j.FileAppender +log4j.appender.file.File=target/jcr.log + +# 'file' uses PatternLayout. +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n diff --git a/jackrabbit-spi2dav/src/test/resources/org/apache/jackrabbit/spi2davex/image.bmp b/jackrabbit-spi2dav/src/test/resources/org/apache/jackrabbit/spi2davex/image.bmp new file mode 100644 index 00000000000..22e4d4afa24 Binary files /dev/null and b/jackrabbit-spi2dav/src/test/resources/org/apache/jackrabbit/spi2davex/image.bmp differ diff --git a/jackrabbit-spi2dav/src/test/resources/repositoryServiceStubImpl.properties b/jackrabbit-spi2dav/src/test/resources/repositoryServiceStubImpl.properties new file mode 100644 index 00000000000..e9b67992f1f --- /dev/null +++ b/jackrabbit-spi2dav/src/test/resources/repositoryServiceStubImpl.properties @@ -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. + +# +# This is the configuration file for the jackrabbit repositoryservice test stub. +# + +# the ServiceStubImpl used for SPI tests. +#org.apache.jackrabbit.spi.repository_service_stub_impl=org.apache.jackrabbit.spi2dav.ServiceStubImpl +org.apache.jackrabbit.spi.repository_service_stub_impl=org.apache.jackrabbit.spi2davex.ServiceStubImpl + +org.apache.jackrabbit.spi.admin.pwd=admin +org.apache.jackrabbit.spi.admin.name=admin +org.apache.jackrabbit.spi.readonly.pwd=anonymous +org.apache.jackrabbit.spi.readonly.name= +org.apache.jackrabbit.spi.workspacename=default + +org.apache.jackrabbit.spi.QValueFactoryTest.reference=cafebabe-cafe-babe-cafe-babecafebabe + +org.apache.jackrabbit.spi.uri=http://localhost:8080/jackrabbit/server +org.apache.jackrabbit.spi.spi2davex.defaultDepth=4 diff --git a/jackrabbit-spi2dav/src/test/resources/repositoryStubImpl.properties b/jackrabbit-spi2dav/src/test/resources/repositoryStubImpl.properties new file mode 100644 index 00000000000..02781e08581 --- /dev/null +++ b/jackrabbit-spi2dav/src/test/resources/repositoryStubImpl.properties @@ -0,0 +1,475 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 the configuration file for the jackrabbit repository test stub. +# + +# Stub implementation class for testing on spi2dav or spi2davex +#javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.spi2dav.RepositoryStubImpl +javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.spi2davex.RepositoryStubImpl + +# config for RepositoryStub (SPI2DAV, SPI2DAV_ex) +org.apache.jackrabbit.jcr2spi.repository.url=http://localhost:8080/jackrabbit/server/ + +# alternative workspace used for update tests etc. +org.apache.jackrabbit.jcr2spi.workspace2.name=test + +# credential configuration +javax.jcr.tck.superuser.name=admin +javax.jcr.tck.superuser.pwd=admin +javax.jcr.tck.readwrite.name=user +javax.jcr.tck.readwrite.pwd=user +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.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.workspacename=test + +# namespace configuration +javax.jcr.tck.namespaces=test +javax.jcr.tck.namespaces.test=http://www.apache.org/jackrabbit/test + +# 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: 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 + +# 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:hierarchyNode + +# 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:base +# 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: 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:hierarchyNode + +# 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=/testroot + +# Test class: ReferenceableRootNodesTest +javax.jcr.tck.ReferenceableRootNodesTest.testroot=/testroot + +# 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.QUERY CONFIGURATION +# ============================================================================== + +javax.jcr.tck.nodetype.testroot=/testdata + +# ============================================================================== +# 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 +# ============================================================================== + +# nodetye 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.propertyValue=aPropertyValue + +# 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 + +# 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 diff --git a/jackrabbit-spi2jcr/README.txt b/jackrabbit-spi2jcr/README.txt new file mode 100644 index 00000000000..1f00cdeb35a --- /dev/null +++ b/jackrabbit-spi2jcr/README.txt @@ -0,0 +1,7 @@ +================================= +Welcome to Jackrabbit SPI2JCR +================================= + +This is the SPI2JCR component of the Apache Jackrabbit project. +This component contains a SPI implementation wrapping around an +implementation of JSR-170. diff --git a/jackrabbit-spi2jcr/pom.xml b/jackrabbit-spi2jcr/pom.xml new file mode 100644 index 00000000000..3ad26bce3ce --- /dev/null +++ b/jackrabbit-spi2jcr/pom.xml @@ -0,0 +1,181 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-spi2jcr + Jackrabbit SPI to JCR + + + + + maven-surefire-plugin + + + **/TestAll.java + + once + ${test.opts} + + + derby.system.durability + test + + + derby.stream.error.file + target/derby.log + + + known.issues + + org.apache.jackrabbit.jcr2spi.name.NamespaceRegistryTest#testReRegisteredNamespaceVisibility + org.apache.jackrabbit.jcr2spi.name.NamespaceRegistryTest#testRegisteredNamespaceVisibility + org.apache.jackrabbit.test.api.ShareableNodeTest + org.apache.jackrabbit.test.api.observation.NodeReorderTest#testNodeReorderMove + org.apache.jackrabbit.test.api.version.simple + org.apache.jackrabbit.test.api.LifecycleTest +org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testRightOuterJoin1 +org.apache.jackrabbit.test.api.query.qom.EquiJoinConditionTest#testLeftOuterJoin2 + + + + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAggregatedPrivilegesSeparately + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryInvalidPrincipal + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryInvalidPrivilege + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryTwice + org.apache.jackrabbit.test.api.security.AccessControlListTest#testAddAccessControlEntryAgain + + org.apache.jackrabbit.test.api.security.RSessionAccessControlPolicyTest#testGetApplicablePolicies + org.apache.jackrabbit.test.api.security.RSessionAccessControlPolicyTest#testGetPolicy + + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testStringLiteralInvalidName + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testPathLiteral + org.apache.jackrabbit.test.api.query.qom.NodeLocalNameTest#testURILiteral + + + + reversealphabetical + + + + org.apache.rat + apache-rat-plugin + + + src/test/resources/jaas.config + src/test/resources/testdata.xml + *.log + + + + + + + + + javax.jcr + jcr + + + org.apache.jackrabbit + jackrabbit-api + ${project.version} + + + org.apache.jackrabbit + jackrabbit-spi + ${project.version} + + + + org.apache.jackrabbit + jackrabbit-spi + ${project.version} + tests + test + + + org.apache.jackrabbit + jackrabbit-spi-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + org.slf4j + slf4j-api + + + junit + junit + test + + + org.apache.jackrabbit + jackrabbit-jcr-tests + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-jcr-benchmark + test + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + test + + + org.apache.jackrabbit + jackrabbit-jcr2spi + ${project.version} + tests + test + + + org.apache.jackrabbit + jackrabbit-jcr2spi + ${project.version} + + test + + + ch.qos.logback + logback-classic + test + + + diff --git a/jackrabbit-spi2jcr/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-spi2jcr/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/BatchReadConfig.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/BatchReadConfig.java new file mode 100644 index 00000000000..48391829d08 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/BatchReadConfig.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.spi2jcr; + +import org.apache.jackrabbit.spi.Name; + +import java.util.Map; +import java.util.HashMap; + +/** + * BatchReadConfig defines if and how deep child item + * information should be retrieved, when accessing a Node. + * The configuration is based on node type names. + */ +public class BatchReadConfig { + + public static final int DEPTH_DEFAULT = 0; + public static final int DEPTH_INFINITE = -1; + + private Map depthMap = new HashMap(0); + + /** + * Return the depth for the given node type name. If the name is + * not defined in this configuration, the {@link #DEPTH_DEFAULT default value} + * is returned. + * + * @param ntName + * @return {@link #DEPTH_INFINITE -1} If all child infos should be return or + * any value greater than {@link #DEPTH_DEFAULT 0} if only parts of the + * subtree should be returned. If the given nodetype name is not defined + * in this configuration, the default depth {@link #DEPTH_DEFAULT 0} will + * be returned. + */ + public int getDepth(Name ntName) { + if (depthMap.containsKey(ntName)) { + return depthMap.get(ntName); + } else { + return DEPTH_DEFAULT; + } + } + + /** + * Define the batch-read depth for the given node type name. + * + * @param ntName + * @param depth + */ + public void setDepth(Name ntName, int depth) { + if (ntName == null || depth < DEPTH_INFINITE) { + throw new IllegalArgumentException(); + } + depthMap.put(ntName, depth); + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/ChildInfoImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/ChildInfoImpl.java new file mode 100644 index 00000000000..abe69c8c9a5 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/ChildInfoImpl.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.spi2jcr; + +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.NamespaceException; + +/** + * ChildInfoImpl implements a ChildInfo and provides + * information about a child node. + */ +class ChildInfoImpl extends org.apache.jackrabbit.spi.commons.ChildInfoImpl { + + /** + * Creates a new ChildInfoImpl for node. + * + * @param node the JCR node. + * @param resolver + * @throws RepositoryException if an error occurs while reading from + * node. + * @throws NameException if the node name is illegal. + * @throws NamespaceException if the name of the node + * contains a prefix not known to + * nsResolver. + */ + public ChildInfoImpl(Node node, NamePathResolver resolver) + throws NamespaceException, NameException, RepositoryException { + super(resolver.getQName(node.getName()), + getUniqueId(node), node.getIndex()); + } + + /** + * @param node the JCR node. + * @return the unique id for the node or null if + * the node does not have a unique id. + * @throws RepositoryException if an error occurs while reading the unique + * id. + */ + private static String getUniqueId(Node node) throws RepositoryException { + String uuid = null; + try { + uuid = node.getUUID(); + } catch (UnsupportedRepositoryOperationException e) { + // not referenceable + } + return uuid; + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/EventFactory.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/EventFactory.java new file mode 100644 index 00000000000..f9329f76a98 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/EventFactory.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.spi2jcr; + +import java.util.Map; +import java.util.HashMap; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.PropertyType; +import javax.jcr.Session; +import javax.jcr.NamespaceException; +import javax.jcr.nodetype.NodeType; + +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi.commons.EventImpl; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; + +/** + * EventFactory implements a factory for SPI Event instances. + */ +class EventFactory { + + private final Session session; + + private final NamePathResolver resolver; + + private final IdFactory idFactory; + + private final QValueFactory qValueFactory; + + public EventFactory(Session session, + NamePathResolver resolver, + IdFactory idFactory, + QValueFactory qValueFactory) { + this.session = session; + this.resolver = resolver; + this.idFactory = idFactory; + this.qValueFactory = qValueFactory; + } + + public Event fromJCREvent(javax.jcr.observation.Event e) + throws RepositoryException { + Path p = e.getPath() != null ? resolver.getQPath(e.getPath()) : null; + Path parent = p != null ? p.getAncestor(1) : null; + int type = e.getType(); + + NodeId parentId = parent != null ?idFactory.createNodeId((String) null, parent) : null; + String identifier = e.getIdentifier(); + ItemId itemId; + Node node = null; + if (identifier != null) { + itemId = idFactory.fromJcrIdentifier(e.getIdentifier()); + try { + node = session.getItem(e.getPath()).getParent(); + } catch (RepositoryException re) { + // ignore. TODO improve + } + } else { + switch (type) { + case Event.NODE_ADDED: + case Event.NODE_MOVED: + node = session.getItem(e.getPath()).getParent(); + case Event.NODE_REMOVED: + itemId = idFactory.createNodeId((String) null, p); + break; + case Event.PROPERTY_ADDED: + case Event.PROPERTY_CHANGED: + node = session.getItem(e.getPath()).getParent(); + case Event.PROPERTY_REMOVED: + itemId = idFactory.createPropertyId(parentId, p.getName()); + break; + case Event.PERSIST: + default: + itemId = null; + } + } + + Name nodeTypeName = null; + Name[] mixinTypes = Name.EMPTY_ARRAY; + if (node != null) { + try { + parentId = idFactory.createNodeId(node.getUUID(), null); + } catch (UnsupportedRepositoryOperationException ex) { + // not referenceable + } + nodeTypeName = resolver.getQName(node.getPrimaryNodeType().getName()); + mixinTypes = getNodeTypeNames(node.getMixinNodeTypes(), resolver); + } + Map info = new HashMap(); + Map jcrInfo = e.getInfo(); + for (Map.Entry entry : jcrInfo.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + Name name = resolver.getQName(key); + if (value != null) { + // event information is generated for NODE_MOVED only in which + // case all values are of type PATH. + QValue v = ValueFormat.getQValue(value.toString(), PropertyType.PATH, resolver, qValueFactory); + info.put(name, v); + } else { + info.put(name, null); + } + } + return new EventImpl(e.getType(), p, itemId, parentId, nodeTypeName, + mixinTypes, e.getUserID(), e.getUserData(), e.getDate(), info); + } + + /** + * Returns the names of the passed node types using the namespace resolver + * to parse the names. + * + * @param nt the node types + * @param resolver the name resolver. + * @return the names of the node types. + * @throws NameException if a node type returns an illegal name. + * @throws NamespaceException if the name of a node type contains a prefix + * that is not known to resolver. + */ + private static Name[] getNodeTypeNames(NodeType[] nt, NameResolver resolver) + throws NameException, NamespaceException { + Name[] names = new Name[nt.length]; + for (int i = 0; i < nt.length; i++) { + Name ntName = resolver.getQName(nt[i].getName()); + names[i] = ntName; + } + return names; + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/EventSubscription.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/EventSubscription.java new file mode 100644 index 00000000000..ce31e7bb2a9 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/EventSubscription.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.spi2jcr; + +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.EventBundleImpl; +import org.apache.jackrabbit.spi.commons.EventFilterImpl; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; +import javax.jcr.RepositoryException; +import java.util.ArrayList; +import java.util.List; +import java.util.Iterator; +import java.util.Arrays; +import java.util.Collections; + +/** + * EventSubscription listens for JCR events and creates SPI event + * bundles for them. + */ +class EventSubscription implements Subscription, EventListener { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(EventSubscription.class); + + /** + * Mask for all events. + */ + static final int ALL_EVENTS = javax.jcr.observation.Event.NODE_ADDED + | javax.jcr.observation.Event.NODE_REMOVED + | javax.jcr.observation.Event.PROPERTY_ADDED + | javax.jcr.observation.Event.PROPERTY_CHANGED + | javax.jcr.observation.Event.PROPERTY_REMOVED + | javax.jcr.observation.Event.NODE_MOVED + | javax.jcr.observation.Event.PERSIST; + + private final List eventBundles = new ArrayList(); + + private final SessionInfoImpl sessionInfo; + + /** + * Current list of filters. Copy on write is performed on this list. + */ + private volatile List filters; + + /** + * Set to true if this subscription has been disposed. + */ + private volatile boolean disposed = false; + + /** + * The event factory. + */ + private final EventFactory eventFactory; + + /** + * Creates a new subscription for the passed session. + * + * @param idFactory the id factory. + * @param qValueFactory the QValueFactory. + * @param sessionInfo the session info. + * @param filters the filters that should be applied to the generated + * events. + * @throws RepositoryException if an error occurs while an event listener is + * registered with the session. + */ + EventSubscription(IdFactory idFactory, + QValueFactory qValueFactory, + SessionInfoImpl sessionInfo, + EventFilter[] filters) throws RepositoryException { + this.sessionInfo = sessionInfo; + this.eventFactory = new EventFactory(sessionInfo.getSession(), + sessionInfo.getNamePathResolver(), idFactory, qValueFactory); + setFilters(filters); + ObservationManager obsMgr = sessionInfo.getSession().getWorkspace().getObservationManager(); + obsMgr.addEventListener(this, EventSubscription.ALL_EVENTS, "/", true, null, null, true); + } + + /** + * Sets a new list of event filters for this subscription. + * + * @param filters the new filters. + * @throws RepositoryException if the filters array contains a unknown + * implementation of EventFilters. + */ + void setFilters(EventFilter[] filters) throws RepositoryException { + // check type + for (EventFilter filter : filters) { + if (!(filter instanceof EventFilterImpl)) { + throw new RepositoryException("Unknown filter implementation"); + } + } + List tmp = new ArrayList(Arrays.asList(filters)); + this.filters = Collections.unmodifiableList(tmp); + + } + + /** + * Removes this subscription as a listener from the observation manager and + * marks itself as disposed. + */ + void dispose() throws RepositoryException { + sessionInfo.removeSubscription(this); + sessionInfo.getSession().getWorkspace().getObservationManager().removeEventListener(this); + disposed = true; + synchronized (eventBundles) { + eventBundles.notify(); + } + } + + //--------------------------< EventListener >------------------------------- + + /** + * Adds the events to the list of pending event bundles. + * + * @param events the events that occurred. + */ + public void onEvent(javax.jcr.observation.EventIterator events) { + createEventBundle(events, false); + } + + /** + * @return a temporary event listener that will create local event bundles + * for delivered events. + */ + EventListener getLocalEventListener() { + return new EventListener() { + public void onEvent(javax.jcr.observation.EventIterator events) { + createEventBundle(events, true); + } + }; + } + + /** + * @return all the pending event bundles. + */ + EventBundle[] getEventBundles(long timeout) { + EventBundle[] bundles; + synchronized (eventBundles) { + if (eventBundles.isEmpty()) { + try { + eventBundles.wait(timeout); + } catch (InterruptedException e) { + // continue + } + } + bundles = eventBundles.toArray(new EventBundle[eventBundles.size()]); + eventBundles.clear(); + } + EventFilter[] eventFilters = filters.toArray(new EventFilter[filters.size()]); + // apply filters to bundles + for (int i = 0; i < bundles.length; i++) { + List filteredEvents = new ArrayList(); + for (Iterator it = bundles[i].getEvents(); it.hasNext(); ) { + Event e = it.next(); + // TODO: this is actually not correct. if filters are empty no event should go out + if (eventFilters == null || eventFilters.length == 0) { + filteredEvents.add(e); + } else { + for (EventFilter eventFilter : eventFilters) { + if (eventFilter.accept(e, bundles[i].isLocal())) { + filteredEvents.add(e); + break; + } + } + } + } + bundles[i] = new EventBundleImpl(filteredEvents, bundles[i].isLocal()); + } + return bundles; + } + + //--------------------------------< internal >------------------------------ + + private void createEventBundle(javax.jcr.observation.EventIterator events, + boolean isLocal) { + // do not create events when disposed + if (disposed) { + return; + } + List spiEvents = new ArrayList(); + while (events.hasNext()) { + try { + Event spiEvent = eventFactory.fromJCREvent(events.nextEvent()); + spiEvents.add(spiEvent); + } catch (Exception ex) { + log.warn("Unable to create SPI Event: " + ex); + } + } + EventBundle bundle = new EventBundleImpl(spiEvents, isLocal); + synchronized (eventBundles) { + eventBundles.add(bundle); + eventBundles.notify(); + } + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/IdFactoryImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/IdFactoryImpl.java new file mode 100644 index 00000000000..25c3134bac5 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/IdFactoryImpl.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.spi2jcr; + +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.identifier.AbstractIdFactory; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Property; + +/** + * IdFactoryImpl... + */ +class IdFactoryImpl extends AbstractIdFactory { + + private static final IdFactory INSTANCE = new IdFactoryImpl(); + + private IdFactoryImpl() {} + + public static IdFactory getInstance() { + return INSTANCE; + } + + @Override + protected PathFactory getPathFactory() { + return PathFactoryImpl.getInstance(); + } + /** + * Creates a NodeId for the given node. + * + * @param node the JCR Node. + * @return the NodeId for node. + * @throws RepositoryException if an error occurs while reading from + * node. + */ + public NodeId createNodeId(Node node) throws RepositoryException { + String uniqueId = node.getIdentifier(); + return createNodeId(uniqueId); + } + + /** + * Creates a PropertyId for the given property. + * + * @param property the JCR Property. + * @param resolver + * @return the PropertyId for property. + * @throws RepositoryException if an error occurs while reading from + * property. + */ + public PropertyId createPropertyId(Property property, + NamePathResolver resolver) + throws RepositoryException { + Node parent = property.getParent(); + NodeId nodeId = createNodeId(parent); + String jcrName = property.getName(); + Name name; + try { + name = resolver.getQName(jcrName); + } catch (NameException e) { + throw new RepositoryException(e.getMessage(), e); + } + return createPropertyId(nodeId, name); + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/LockInfoImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/LockInfoImpl.java new file mode 100644 index 00000000000..7cb7844feaa --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/LockInfoImpl.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.spi2jcr; + +import javax.jcr.RepositoryException; +import javax.jcr.lock.Lock; + +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +/** + * LockInfoImpl implements a LockInfo on top of a + * JCR repository. + */ +class LockInfoImpl extends org.apache.jackrabbit.spi.commons.LockInfoImpl { + + /** + * Creates a new lock info for the given JCR lock object. + * + * @param lock the lock. + * @param idFactory the id factory. + * @throws RepositoryException if an error occurs while the node from the + * given lock or while creating the node id. + */ + private LockInfoImpl(Lock lock, IdFactoryImpl idFactory) throws RepositoryException { + super(lock.getLockToken(), lock.getLockOwner(), lock.isDeep(), + lock.isSessionScoped(), lock.getSecondsRemaining(), lock.isLockOwningSession(), + idFactory.createNodeId(lock.getNode())); + } + + /** + * Create a new LockInfo from the given parameters. + * + * @param lock the JCR lock. + * @param idFactory the id factory. + * @return a new LockInfo + * @throws RepositoryException If an error occurs while creating the info. + */ + public static LockInfo createLockInfo(Lock lock, IdFactoryImpl idFactory) throws RepositoryException { + return new LockInfoImpl(lock, idFactory); + } +} \ No newline at end of file diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/NodeInfoImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/NodeInfoImpl.java new file mode 100644 index 00000000000..09569e327c3 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/NodeInfoImpl.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.spi2jcr; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * NodeInfoImpl implements a NodeInfo on top of a JCR + * repository. + */ +class NodeInfoImpl extends org.apache.jackrabbit.spi.commons.NodeInfoImpl { + + /** + * Creates a new node info for the given node. + * + * @param node the JCR node. + * @param idFactory the id factory. + * @param resolver + * @throws RepositoryException if an error occurs while reading from + * node. + */ + public NodeInfoImpl(Node node, + IdFactoryImpl idFactory, + NamePathResolver resolver) + throws RepositoryException, NameException { + super(resolver.getQPath(node.getPath()), + idFactory.createNodeId(node), node.getIndex(), + resolver.getQName(node.getPrimaryNodeType().getName()), + getNodeTypeNames(node.getMixinNodeTypes(), resolver), + getPropertyIds(node.getReferences(), resolver, idFactory), + getPropertyIds(node.getProperties(), resolver, idFactory), + getChildInfos(node.getNodes(), resolver)); + } + + /** + * Returns the names of the passed node types using the namespace + * resolver to parse the names. + * + * @param nt the node types from which the names should be retrieved. + * @param resolver The name and path resolver. + * @return the names of the node types. + * @throws NameException if a node type returns an illegal name. + * @throws NamespaceException if the name of a node type contains a + * prefix that is not known to resolver. + */ + private static Name[] getNodeTypeNames(NodeType[] nt, + NamePathResolver resolver) + throws NameException, NamespaceException { + Name[] names = new Name[nt.length]; + for (int i = 0; i < nt.length; i++) { + Name ntName = resolver.getQName(nt[i].getName()); + names[i] = ntName; + } + return names; + } + + /** + * Returns property ids for the passed JCR properties. + * + * @param props the JCR properties. + * @param resolver + * @param idFactory the id factory. + * @return the property ids for the passed JCR properties. + * @throws RepositoryException if an error occurs while reading from the + * properties. + */ + private static Iterator getPropertyIds(PropertyIterator props, + NamePathResolver resolver, + IdFactoryImpl idFactory) + throws RepositoryException { + List references = new ArrayList(); + while (props.hasNext()) { + references.add(idFactory.createPropertyId(props.nextProperty(), resolver)); + } + return references.iterator(); + } + + private static Iterator getChildInfos(NodeIterator childNodes, + NamePathResolver resolver) throws RepositoryException { + List childInfos = new ArrayList(); + while (childNodes.hasNext()) { + childInfos.add(new ChildInfoImpl(childNodes.nextNode(), resolver)); + } + return childInfos.iterator(); + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/PropertyInfoImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/PropertyInfoImpl.java new file mode 100644 index 00000000000..5460e607066 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/PropertyInfoImpl.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.spi2jcr; + +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; + +import javax.jcr.RepositoryException; +import javax.jcr.Property; +import javax.jcr.Value; + +/** + * PropertyInfoImpl implements a PropertyInfo on top + * of a JCR repository. + */ +class PropertyInfoImpl + extends org.apache.jackrabbit.spi.commons.PropertyInfoImpl { + + /** + * Creates a new property info for the given property. + * + * @param property the JCR property. + * @param idFactory the id factory. + * @param resolver + * @param qValueFactory the QValue factory. + * @throws RepositoryException if an error occurs while reading from + * property. + */ + public PropertyInfoImpl(Property property, + IdFactoryImpl idFactory, + NamePathResolver resolver, + QValueFactory qValueFactory) throws RepositoryException, NameException { + super(resolver.getQPath(property.getPath()), + idFactory.createPropertyId(property, resolver), + property.getType(), property.isMultiple(), + getValues(property, resolver, qValueFactory)); + } + + /** + * Returns the QValues for the property. + * + * @param property the property. + * @param resolver the name and path resolver. + * @param factory the value factory. + * @return the values of the property. + * @throws RepositoryException if an error occurs while reading the values. + */ + private static QValue[] getValues(Property property, + NamePathResolver resolver, + QValueFactory factory) throws RepositoryException { + boolean isMultiValued = property.isMultiple(); + QValue[] values; + if (isMultiValued) { + Value[] jcrValues = property.getValues(); + values = new QValue[jcrValues.length]; + for (int i = 0; i < jcrValues.length; i++) { + values[i] = ValueFormat.getQValue(jcrValues[i], resolver, factory); + } + } else { + values = new QValue[]{ + ValueFormat.getQValue(property.getValue(), resolver, factory) + }; + } + return values; + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/QueryInfoImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/QueryInfoImpl.java new file mode 100644 index 00000000000..c3f2e08f52d --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/QueryInfoImpl.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.spi2jcr; + +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter; +import org.apache.jackrabbit.commons.iterator.RangeIteratorDecorator; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.query.QueryResult; +import javax.jcr.query.Row; +import javax.jcr.RepositoryException; +import javax.jcr.RangeIterator; +import java.util.NoSuchElementException; + +/** + * QueryInfoImpl implements a QueryInfo based on a + * JCR {@link javax.jcr.query.QueryResult}. + */ +class QueryInfoImpl implements QueryInfo { + + /** + * Logger instance for this class. + */ + private static final Logger log = LoggerFactory.getLogger(QueryInfoImpl.class); + + /** + * The underlying query result. + */ + private final QueryResult result; + + /** + * The id factory. + */ + private final IdFactoryImpl idFactory; + + /** + * The namespace resolver. + */ + private final NamePathResolver resolver; + + /** + * The QValue factory. + */ + private final QValueFactory qValueFactory; + + /** + * The names of the columns in the query result. + */ + private final String[] columnNames; + + /** + * The names of the selectors in the query result. + */ + private final String[] selectorNames; + + /** + * Creates a new query info based on a given result. + * + * @param result the JCR query result. + * @param idFactory the id factory. + * @param resolver the name path resolver. + * @param qValueFactory the QValue factory. + * @throws RepositoryException if an error occurs while reading from + * result. + */ + public QueryInfoImpl(QueryResult result, + IdFactoryImpl idFactory, + NamePathResolver resolver, + QValueFactory qValueFactory) + throws RepositoryException { + this.result = result; + this.idFactory = idFactory; + this.resolver = resolver; + this.qValueFactory = qValueFactory; + this.columnNames = result.getColumnNames(); + this.selectorNames = result.getSelectorNames(); + } + + /** + * {@inheritDoc} + */ + public RangeIterator getRows() { + try { + return new RangeIteratorDecorator(result.getRows()) { + @Override + public Object next() { + try { + return new QueryResultRowImpl( + (Row) super.next(), columnNames, selectorNames, + idFactory, resolver, qValueFactory); + } catch (RepositoryException e) { + log.warn("Exception when creating QueryResultRowImpl: " + + e.getMessage(), e); + throw new NoSuchElementException(); + } + } + }; + } catch (RepositoryException e) { + return RangeIteratorAdapter.EMPTY; + } + } + + /** + * {@inheritDoc} + */ + public String[] getColumnNames() { + return columnNames; + } + + /** + * {@inheritDoc} + */ + public String[] getSelectorNames() { + return selectorNames; + } + +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/QueryResultRowImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/QueryResultRowImpl.java new file mode 100644 index 00000000000..96fb62b4248 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/QueryResultRowImpl.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.spi2jcr; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +import org.apache.jackrabbit.spi.QueryResultRow; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; + +import javax.jcr.query.Row; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.Node; + +/** + * QueryResultRowImpl implements a QueryResultRow + * based on a JCR {@link javax.jcr.query.Row}. + */ +class QueryResultRowImpl implements QueryResultRow { + + /** + * The node ids of the underlying row. + */ + private final Map nodeIds = new HashMap(); + + /** + * The score values for this row. + */ + private final Map scores = new HashMap(); + + /** + * The QValues for this row. + */ + private final QValue[] values; + + /** + * Creates a new query result row for the given row. + * + * @param row the JCR row. + * @param columnNames the resolved names of the columns. + * @param selectorNames the selector names. + * @param idFactory the id factory. + * @param resolver the name path resolver. + * @param qValueFactory the QValue factory. + * @throws RepositoryException if an error occurs while reading from + * row. + */ + public QueryResultRowImpl(Row row, + String[] columnNames, + String[] selectorNames, + IdFactoryImpl idFactory, + NamePathResolver resolver, + QValueFactory qValueFactory) throws RepositoryException { + this.values = new QValue[columnNames.length]; + for (int i = 0; i < columnNames.length; i++) { + Value v = row.getValue(columnNames[i]); + if (v == null) { + values[i] = null; + } else { + values[i] = ValueFormat.getQValue(v, resolver, qValueFactory); + } + } + List selNames = new ArrayList(); + selNames.addAll(Arrays.asList(selectorNames)); + if (selNames.isEmpty()) { + selNames.add(null); // default selector + } + for (String sn : selNames) { + Node n; + double score; + if (sn == null) { + n = row.getNode(); + score = row.getScore(); + } else { + n = row.getNode(sn); + score = row.getScore(sn); + } + NodeId id = null; + if (n != null) { + id = idFactory.fromJcrIdentifier(n.getIdentifier()); + } + nodeIds.put(sn, id); + scores.put(sn, score); + } + } + + public NodeId getNodeId(String selectorName) { + if (nodeIds.containsKey(selectorName)) { + return nodeIds.get(selectorName); + } else { + if (nodeIds.size() == 1) { + return nodeIds.values().iterator().next(); + } else { + throw new IllegalArgumentException(selectorName + " is not a valid selectorName"); + } + } + } + + public double getScore(String selectorName) { + Double score; + if (scores.containsKey(selectorName)) { + score = scores.get(selectorName); + } else { + if (scores.size() == 1) { + score = scores.values().iterator().next(); + } else { + throw new IllegalArgumentException(selectorName + " is not a valid selectorName"); + } + } + if (score == null) { + return Double.NaN; + } else { + return score; + } + } + + /** + * {@inheritDoc} + */ + public QValue[] getValues() { + QValue[] vals = new QValue[values.length]; + System.arraycopy(values, 0, vals, 0, values.length); + return vals; + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/RepositoryServiceImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/RepositoryServiceImpl.java new file mode 100644 index 00000000000..25e7008f7e7 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/RepositoryServiceImpl.java @@ -0,0 +1,2032 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi2jcr; + +import org.apache.jackrabbit.api.JackrabbitWorkspace; +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.PrivilegeDefinition; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.NodeInfo; +import org.apache.jackrabbit.spi.PropertyInfo; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.LockInfo; +import org.apache.jackrabbit.spi.QueryInfo; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.EventBundle; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.Event; +import org.apache.jackrabbit.spi.ItemInfo; +import org.apache.jackrabbit.spi.ChildInfo; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.EventFilterImpl; +import org.apache.jackrabbit.spi.commons.EventBundleImpl; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi.commons.QPropertyDefinitionImpl; +import org.apache.jackrabbit.spi.commons.QNodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.iterator.Iterators; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.privilege.PrivilegeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl; +import org.apache.jackrabbit.JcrConstants; + +import javax.jcr.RepositoryException; +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.ItemNotFoundException; +import javax.jcr.PathNotFoundException; +import javax.jcr.ValueFormatException; +import javax.jcr.AccessDeniedException; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.ItemExistsException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ReferentialIntegrityException; +import javax.jcr.MergeException; +import javax.jcr.NamespaceException; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.NodeIterator; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Workspace; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Value; +import javax.jcr.ItemVisitor; +import javax.jcr.ValueFactory; +import javax.jcr.GuestCredentials; +import javax.jcr.PropertyIterator; +import javax.jcr.security.Privilege; +import javax.jcr.util.TraversingItemVisitor; +import javax.jcr.observation.ObservationManager; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.EventJournal; +import javax.jcr.query.InvalidQueryException; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Query; +import javax.jcr.lock.LockException; +import javax.jcr.lock.Lock; +import javax.jcr.version.VersionException; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.Version; +import javax.jcr.version.VersionManager; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeTypeManager; +import javax.jcr.nodetype.NodeTypeIterator; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.InvalidNodeTypeDefinitionException; +import javax.jcr.nodetype.NodeTypeExistsException; +import javax.jcr.nodetype.NodeTypeDefinition; +import java.util.Map; +import java.util.Iterator; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.Arrays; +import java.util.Collections; +import java.util.Collection; +import java.io.InputStream; +import java.io.IOException; +import java.io.ByteArrayInputStream; +import java.security.AccessControlException; + +/** + * RepositoryServiceImpl implements a repository service on top + * of a JCR Repository. + */ +public class RepositoryServiceImpl implements RepositoryService { + + /** + * The JCR Repository. + */ + private final Repository repository; + + /** + * The configuration map used to determine the maximal depth of child + * items to be accessed upon a call to {@link #getNodeInfo(SessionInfo, NodeId)}. + */ + private final BatchReadConfig batchReadConfig; + + /** + * The id factory. + */ + private final IdFactoryImpl idFactory = (IdFactoryImpl) IdFactoryImpl.getInstance(); + + /** + * The QValueFactory + */ + private QValueFactory qValueFactory = QValueFactoryImpl.getInstance(); + + /** + * Set to true if the underlying JCR repository supports + * observation. + */ + private final boolean supportsObservation; + + private final int itemInfoCacheSize; + + public RepositoryServiceImpl(Repository repository, BatchReadConfig batchReadConfig) { + this(repository, batchReadConfig, ItemInfoCacheImpl.DEFAULT_CACHE_SIZE); + } + + /** + * Creates a new repository service based on the given + * repository. + * + * @param repository a JCR repository instance. + * @param batchReadConfig + * {@link #getNodeInfo(SessionInfo, NodeId)}. + */ + public RepositoryServiceImpl(Repository repository, BatchReadConfig batchReadConfig, int itemInfoCacheSize) { + this.repository = repository; + this.batchReadConfig = batchReadConfig; + this.supportsObservation = "true".equals(repository.getDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED)); + this.itemInfoCacheSize = itemInfoCacheSize; + + try { + Session s = repository.login(new GuestCredentials()); + ValueFactory vf = s.getValueFactory(); + if (vf instanceof ValueFactoryQImpl) { + qValueFactory = ((ValueFactoryQImpl) vf).getQValueFactory(); + } + } catch (RepositoryException e) { + // ignore + } + } + + /** + * {@inheritDoc} + */ + public IdFactory getIdFactory() { + return idFactory; + } + + /** + * {@inheritDoc} + */ + public NameFactory getNameFactory() { + return NameFactoryImpl.getInstance(); + } + + /** + * {@inheritDoc} + */ + public PathFactory getPathFactory() { + return PathFactoryImpl.getInstance(); + } + + /** + * {@inheritDoc} + */ + public QValueFactory getQValueFactory() { + return qValueFactory; + } + + /** + * {@inheritDoc} + */ + public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) throws RepositoryException { + return new ItemInfoCacheImpl(itemInfoCacheSize); + } + + /** + * {@inheritDoc} + */ + public Map getRepositoryDescriptors() throws RepositoryException { + Map descriptors = new HashMap(); + for (String key : repository.getDescriptorKeys()) { + if (key.equals(Repository.OPTION_TRANSACTIONS_SUPPORTED)) { + descriptors.put(key, new QValue[] {qValueFactory.create(false)}); + } else { + Value[] vs = repository.getDescriptorValues(key); + QValue[] qvs = new QValue[vs.length]; + for (int i = 0; i < vs.length; i++) { + // Name and path resolver that uses a dummy namespace resolver + // as Name/Path values are not expected to occur in the + // descriptors. TODO: check again. + NamePathResolver resolver = new DefaultNamePathResolver(new NamespaceResolver() { + public String getURI(String prefix) { + return prefix; + } + public String getPrefix(String uri) { + return uri; + } + }); + qvs[i] = ValueFormat.getQValue(vs[i], resolver, qValueFactory); + } + descriptors.put(key, qvs); + } + } + return descriptors; + } + + /** + * {@inheritDoc} + */ + public SessionInfo obtain(Credentials credentials, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException { + Credentials duplicate = SessionInfoImpl.duplicateCredentials(credentials); + return new SessionInfoImpl(repository.login(credentials, workspaceName), duplicate, getNameFactory(), getPathFactory()); + } + + /** + * {@inheritDoc} + */ + public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Session s = repository.login(sInfo.getCredentials(), workspaceName); + return new SessionInfoImpl(s, sInfo.getCredentials(), getNameFactory(), getPathFactory()); + } + + /** + * {@inheritDoc} + */ + public SessionInfo impersonate(SessionInfo sessionInfo, Credentials credentials) throws LoginException, RepositoryException { + Credentials duplicate = SessionInfoImpl.duplicateCredentials(credentials); + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return new SessionInfoImpl(sInfo.getSession().impersonate(credentials), duplicate, getNameFactory(), getPathFactory()); + } + + /** + * {@inheritDoc} + */ + public void dispose(SessionInfo sessionInfo) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + for (EventSubscription s : sInfo.getSubscriptions()) { + s.dispose(); + } + sInfo.getSession().logout(); + } + + /** + * {@inheritDoc} + */ + public String[] getWorkspaceNames(SessionInfo sessionInfo) + throws RepositoryException { + Session s = getSessionInfoImpl(sessionInfo).getSession(); + return s.getWorkspace().getAccessibleWorkspaceNames(); + } + + /** + * {@inheritDoc} + */ + public boolean isGranted(SessionInfo sessionInfo, + ItemId itemId, + String[] actions) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + String path = pathForId(itemId, sInfo); + + try { + String actStr; + if (actions.length == 1) { + actStr = actions[0]; + } else { + String comma = ""; + actStr = ""; + for (String action : actions) { + actStr += comma; + actStr += action; + comma = ","; + } + } + sInfo.getSession().checkPermission(path, actStr); + return true; + } catch (AccessControlException e) { + return false; + } + } + + @Override + public PrivilegeDefinition[] getPrivilegeDefinitions(SessionInfo sessionInfo) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Session session = sInfo.getSession(); + Workspace wsp = session.getWorkspace(); + Collection privs; + if (wsp instanceof JackrabbitWorkspace) { + privs = Arrays.asList(((JackrabbitWorkspace) wsp).getPrivilegeManager().getRegisteredPrivileges()); + } else { + Privilege jcrAll = session.getAccessControlManager().privilegeFromName(Privilege.JCR_ALL); + privs = new HashSet(); + privs.add(jcrAll); + for (Privilege p : jcrAll.getAggregatePrivileges()) { + privs.add(p); + } + } + + PrivilegeDefinition[] pDefs = new PrivilegeDefinition[privs.size()]; + NamePathResolver npResolver = sInfo.getNamePathResolver(); + int i = 0; + for (Privilege p : privs) { + Set aggrnames = null; + if (p.isAggregate()) { + aggrnames = new HashSet(); + for (Privilege dap : p.getDeclaredAggregatePrivileges()) { + aggrnames.add(npResolver.getQName(dap.getName())); + } + } + PrivilegeDefinition def = new PrivilegeDefinitionImpl(npResolver.getQName(p.getName()), p.isAbstract(), aggrnames); + pDefs[i++] = def; + } + return pDefs; + } + + @Override + public Name[] getPrivilegeNames(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + String path = (nodeId == null) ? null : pathForId(nodeId, sInfo); + NamePathResolver npResolver = sInfo.getNamePathResolver(); + + Privilege[] privs = sInfo.getSession().getAccessControlManager().getPrivileges(path); + List names = new ArrayList(privs.length); + for (Privilege priv : privs) { + names.add(npResolver.getQName(priv.getName())); + } + return names.toArray(new Name[names.size()]); + } + + public PrivilegeDefinition[] getSupportedPrivileges(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + String path = (nodeId == null) ? null : pathForId(nodeId, sInfo); + + Privilege[] privs = sInfo.getSession().getAccessControlManager().getSupportedPrivileges(path); + PrivilegeDefinition[] pDefs = new PrivilegeDefinition[privs.length]; + NamePathResolver npResolver = sInfo.getNamePathResolver(); + for (int i = 0; i < privs.length; i++) { + Set aggrnames = null; + if (privs[i].isAggregate()) { + aggrnames = new HashSet(); + for (Privilege dap : privs[i].getDeclaredAggregatePrivileges()) { + aggrnames.add(npResolver.getQName(dap.getName())); + } + } + PrivilegeDefinition def = new PrivilegeDefinitionImpl(npResolver.getQName(privs[i].getName()), privs[i].isAbstract(), aggrnames); + pDefs[i] = def; + } + return pDefs; + } + + /** + * {@inheritDoc} + */ + public QNodeDefinition getNodeDefinition(SessionInfo sessionInfo, + NodeId nodeId) + throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + try { + return new QNodeDefinitionImpl(getNode(nodeId, sInfo).getDefinition(), + sInfo.getNamePathResolver()); + } catch (NameException e) { + throw new RepositoryException(e); + } + } + + /** + * {@inheritDoc} + */ + public QPropertyDefinition getPropertyDefinition(SessionInfo sessionInfo, + PropertyId propertyId) + throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + try { + return new QPropertyDefinitionImpl( + getProperty(propertyId, sInfo).getDefinition(), + sInfo.getNamePathResolver(), + getQValueFactory()); + } catch (NameException e) { + throw new RepositoryException(e); + } + } + + /** + * {@inheritDoc} + */ + public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId) + throws ItemNotFoundException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Node node = getNode(nodeId, sInfo); + try { + return new NodeInfoImpl(node, idFactory, sInfo.getNamePathResolver()); + } catch (NameException e) { + throw new RepositoryException(e); + } + } + + /** + * {@inheritDoc} + */ + public Iterator getItemInfos(SessionInfo sessionInfo, ItemId itemId) + throws ItemNotFoundException, RepositoryException { + + if (!itemId.denotesNode()) { + PropertyId propertyId = (PropertyId) itemId; + PropertyInfo propertyInfo = getPropertyInfo(sessionInfo, propertyId); + return Iterators.singleton(propertyInfo); + } + else { + NodeId nodeId = (NodeId) itemId; + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Node node = getNode(nodeId, sInfo); + Name ntName = null; + try { + ntName = sInfo.getNamePathResolver().getQName(node.getProperty(JcrConstants.JCR_PRIMARYTYPE).getString()); + } catch (NameException e) { + // ignore. should never occur + } + int depth = batchReadConfig.getDepth(ntName); + if (depth == BatchReadConfig.DEPTH_DEFAULT) { + NodeInfo info; + try { + info = new NodeInfoImpl(node, idFactory, sInfo.getNamePathResolver()); + } catch (NameException e) { + throw new RepositoryException(e); + } + return Collections.singletonList(info).iterator(); + } else { + final List itemInfos = new ArrayList(); + ItemVisitor visitor = new TraversingItemVisitor(false, depth) { + @Override + protected void entering(Property property, int i) throws RepositoryException { + try { + itemInfos.add(new PropertyInfoImpl(property, idFactory, sInfo.getNamePathResolver(), getQValueFactory())); + } catch (NameException e) { + throw new RepositoryException(e); + } + } + @Override + protected void entering(Node node, int i) throws RepositoryException { + try { + itemInfos.add(new NodeInfoImpl(node, idFactory, sInfo.getNamePathResolver())); + } catch (NameException e) { + throw new RepositoryException(e); + } + } + @Override + protected void leaving(Property property, int i) { + // nothing to do + } + @Override + protected void leaving(Node node, int i) { + // nothing to do + } + }; + visitor.visit(node); + return itemInfos.iterator(); + } + } + } + + /** + * {@inheritDoc} + */ + public Iterator getChildInfos(SessionInfo sessionInfo, NodeId parentId) + throws ItemNotFoundException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + NodeIterator children = getNode(parentId, sInfo).getNodes(); + List childInfos = new ArrayList(); + try { + while (children.hasNext()) { + childInfos.add(new ChildInfoImpl(children.nextNode(), + sInfo.getNamePathResolver())); + } + } catch (NameException e) { + throw new RepositoryException(e); + } + return childInfos.iterator(); + } + + /** + * {@inheritDoc} + */ + public Iterator getReferences(SessionInfo sessionInfo, NodeId nodeId, Name propertyName, boolean weakReferences) throws ItemNotFoundException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Node node = getNode(nodeId, sInfo); + String jcrName = (propertyName == null) ? null : sInfo.getNamePathResolver().getJCRName(propertyName); + + List ids = new ArrayList(); + PropertyIterator it; + if (weakReferences) { + it = node.getWeakReferences(jcrName); + } else { + it = node.getReferences(jcrName); + } + while (it.hasNext()) { + ids.add(idFactory.createPropertyId(it.nextProperty(), sInfo.getNamePathResolver())); + } + return ids.iterator(); + } + + /** + * {@inheritDoc} + */ + public PropertyInfo getPropertyInfo(SessionInfo sessionInfo, + PropertyId propertyId) + throws ItemNotFoundException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + try { + return new PropertyInfoImpl(getProperty(propertyId, sInfo), idFactory, + sInfo.getNamePathResolver(), getQValueFactory()); + } catch (NameException e) { + throw new RepositoryException(e); + } + } + + /** + * {@inheritDoc} + */ + public Batch createBatch(SessionInfo sessionInfo, ItemId itemId) + throws RepositoryException { + return new BatchImpl(getSessionInfoImpl(sessionInfo)); + } + + /** + * {@inheritDoc} + */ + public void submit(Batch batch) throws PathNotFoundException, ItemNotFoundException, NoSuchNodeTypeException, ValueFormatException, VersionException, LockException, ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + if (batch instanceof BatchImpl) { + ((BatchImpl) batch).end(); + } else { + throw new RepositoryException("Unknown Batch implementation: " + + batch.getClass().getName()); + } + } + + @Override + public Tree createTree(SessionInfo sessionInfo, Batch batch, Name nodeName, Name primaryTypeName, String uniqueId) throws RepositoryException { + return new XmlTree(nodeName, primaryTypeName, uniqueId, getSessionInfoImpl(sessionInfo).getNamePathResolver()); + } + + /** + * {@inheritDoc} + */ + public void importXml(final SessionInfo sessionInfo, + final NodeId parentId, + final InputStream xmlStream, + final int uuidBehaviour) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + String path = pathForId(parentId, sInfo); + try { + sInfo.getSession().getWorkspace().importXML(path, xmlStream, uuidBehaviour); + } catch (IOException e) { + throw new RepositoryException(e.getMessage(), e); + } + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void move(final SessionInfo sessionInfo, + final NodeId srcNodeId, + final NodeId destParentNodeId, + final Name destName) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + String srcPath = pathForId(srcNodeId, sInfo); + StringBuffer destPath = new StringBuffer(pathForId(destParentNodeId, sInfo)); + if (destPath.length() > 1) { + destPath.append("/"); + } + destPath.append(sInfo.getNamePathResolver().getJCRName(destName)); + sInfo.getSession().getWorkspace().move(srcPath, destPath.toString()); + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void copy(final SessionInfo sessionInfo, + final String srcWorkspaceName, + final NodeId srcNodeId, + final NodeId destParentNodeId, + final Name destName) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + Workspace ws = sInfo.getSession().getWorkspace(); + String destPath = getDestinationPath(destParentNodeId, destName, sInfo); + if (ws.getName().equals(srcWorkspaceName)) { + // inner-workspace copy + String srcPath = pathForId(srcNodeId, sInfo); + ws.copy(srcPath, destPath); + } else { + SessionInfoImpl srcInfo = getSessionInfoImpl(obtain(sInfo, srcWorkspaceName)); + try { + String srcPath = pathForId(srcNodeId, srcInfo); + ws.copy(srcWorkspaceName, srcPath, destPath); + } finally { + dispose(srcInfo); + } + } + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void update(final SessionInfo sessionInfo, + final NodeId nodeId, + final String srcWorkspaceName) + throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + getNode(nodeId, sInfo).update(srcWorkspaceName); + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void clone(final SessionInfo sessionInfo, + final String srcWorkspaceName, + final NodeId srcNodeId, + final NodeId destParentNodeId, + final Name destName, + final boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + SessionInfoImpl srcInfo = getSessionInfoImpl(obtain(sessionInfo, srcWorkspaceName)); + try { + String srcPath = pathForId(srcNodeId, srcInfo); + String destPath = getDestinationPath(destParentNodeId, destName, sInfo); + + Workspace wsp = sInfo.getSession().getWorkspace(); + wsp.clone(srcWorkspaceName, srcPath, destPath, removeExisting); + } finally { + dispose(srcInfo); + } + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public LockInfo getLockInfo(SessionInfo sessionInfo, NodeId nodeId) + throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + try { + Lock lock = getNode(nodeId, sInfo).getLock(); + return LockInfoImpl.createLockInfo(lock, idFactory); + } catch (LockException e) { + // no lock present on this node. + return null; + } + } + + /** + * {@inheritDoc} + */ + public LockInfo lock(final SessionInfo sessionInfo, + final NodeId nodeId, + final boolean deep, + final boolean sessionScoped) + throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return (LockInfo) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + Node n = getNode(nodeId, sInfo); + Lock lock = n.lock(deep, sessionScoped); + return LockInfoImpl.createLockInfo(lock, idFactory); + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public LockInfo lock(SessionInfo sessionInfo, final NodeId nodeId, final boolean deep, final boolean sessionScoped, final long timeoutHint, final String ownerHint) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return (LockInfo) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + Node n = getNode(nodeId, sInfo); + Lock lock; + javax.jcr.lock.LockManager lMgr = (sInfo.getSession().getWorkspace()).getLockManager(); + lock = lMgr.lock(n.getPath(), deep, sessionScoped, timeoutHint, ownerHint); + return LockInfoImpl.createLockInfo(lock, idFactory); + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void refreshLock(SessionInfo sessionInfo, NodeId nodeId) + throws LockException, RepositoryException { + getNode(nodeId, getSessionInfoImpl(sessionInfo)).getLock().refresh(); + } + + /** + * {@inheritDoc} + */ + public void unlock(final SessionInfo sessionInfo, final NodeId nodeId) + throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + getNode(nodeId, sInfo).unlock(); + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public NodeId checkin(final SessionInfo sessionInfo, final NodeId nodeId) + throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Version newVersion = (Version) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + return getNode(nodeId, getSessionInfoImpl(sessionInfo)).checkin(); + } + }, sInfo); + return idFactory.createNodeId(newVersion); + } + + /** + * {@inheritDoc} + */ + public void checkout(final SessionInfo sessionInfo, final NodeId nodeId) + throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + getNode(nodeId, sInfo).checkout(); + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void checkout(final SessionInfo sessionInfo, final NodeId nodeId, NodeId activityId) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Node activity = (activityId == null) ? null : getNode(activityId, sInfo); + VersionManager vMgr = sInfo.getSession().getWorkspace().getVersionManager(); + vMgr.setActivity(activity); + try { + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + getNode(nodeId, sInfo).checkout(); + return null; + } + }, sInfo); + } finally { + vMgr.setActivity(null); + } + } + + /** + * {@inheritDoc} + */ + public NodeId checkpoint(SessionInfo sessionInfo, final NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Version newVersion = (Version) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + VersionManager vMgr = sInfo.getSession().getWorkspace().getVersionManager(); + return vMgr.checkpoint(getNodePath(nodeId, sInfo)); + } + }, sInfo); + return idFactory.createNodeId(newVersion); + } + + /** + * {@inheritDoc} + */ + public NodeId checkpoint(SessionInfo sessionInfo, final NodeId nodeId, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Node activity = (activityId == null) ? null : getNode(activityId, sInfo); + VersionManager vMgr = sInfo.getSession().getWorkspace().getVersionManager(); + vMgr.setActivity(activity); + try { + Version newVersion = (Version) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + VersionManager vMgr = sInfo.getSession().getWorkspace().getVersionManager(); + return vMgr.checkpoint(getNodePath(nodeId, sInfo)); + } + }, sInfo); + return idFactory.createNodeId(newVersion); + } finally { + vMgr.setActivity(null); + } + } + + /** + * {@inheritDoc} + */ + public void removeVersion(final SessionInfo sessionInfo, + final NodeId versionHistoryId, + final NodeId versionId) + throws ReferentialIntegrityException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + Node vHistory = getNode(versionHistoryId, sInfo); + Node version = getNode(versionId, sInfo); + if (vHistory instanceof VersionHistory) { + ((VersionHistory) vHistory).removeVersion(version.getName()); + } else { + throw new RepositoryException("versionHistoryId does not reference a VersionHistor node"); + } + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void restore(final SessionInfo sessionInfo, + final NodeId nodeId, + final NodeId versionId, + final boolean removeExisting) throws VersionException, PathNotFoundException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + Version v = (Version) getNode(versionId, sInfo); + if (hasNode(sessionInfo, nodeId)) { + Node n = getNode(nodeId, sInfo); + n.restore(v, removeExisting); + } else { + // restore with rel-Path part + Node n = null; + Path relPath = null; + Path path = nodeId.getPath(); + if (nodeId.getUniqueID() != null) { + n = getNode(idFactory.createNodeId(nodeId.getUniqueID()), sInfo); + relPath = (path.isAbsolute()) ? getPathFactory().getRootPath().computeRelativePath(nodeId.getPath()) : path; + } else { + int degree = 0; + while (degree < path.getLength()) { + Path ancestorPath = path.getAncestor(degree); + NodeId parentId = idFactory.createNodeId(nodeId.getUniqueID(), ancestorPath); + if (hasNode(sessionInfo, parentId)) { + n = getNode(parentId, sInfo); + relPath = ancestorPath.computeRelativePath(path); + } + degree++; + } + } + if (n == null) { + throw new PathNotFoundException("Path not found " + nodeId); + } else { + n.restore(v, sInfo.getNamePathResolver().getJCRPath(relPath), removeExisting); + } + } + return null; + } + }, sInfo); + } + + private boolean hasNode(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + try { + getNode(nodeId, sInfo); + } catch (ItemNotFoundException e) { + return false; + } catch (PathNotFoundException e) { + return false; + } + return true; + } + + /** + * {@inheritDoc} + */ + public void restore(final SessionInfo sessionInfo, + final NodeId[] versionIds, + final boolean removeExisting) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + Version[] versions = new Version[versionIds.length]; + for (int i = 0; i < versions.length; i++) { + Node n = getNode(versionIds[i], sInfo); + if (n instanceof Version) { + versions[i] = (Version) n; + } else { + throw new RepositoryException(n.getPath() + + " does not reference a Version node"); + } + } + sInfo.getSession().getWorkspace().restore(versions, removeExisting); + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public Iterator merge(final SessionInfo sessionInfo, + final NodeId nodeId, + final String srcWorkspaceName, + final boolean bestEffort) + throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return (Iterator) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + String nodePath = getNodePath(nodeId, sInfo); + NodeIterator it = getVersionManager(sInfo).merge(nodePath, srcWorkspaceName, bestEffort); + List ids = new ArrayList(); + while (it.hasNext()) { + ids.add(idFactory.createNodeId(it.nextNode() + )); + } + return ids.iterator(); + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public Iterator merge(final SessionInfo sessionInfo, + final NodeId nodeId, + final String srcWorkspaceName, + final boolean bestEffort, + final boolean isShallow) + throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return (Iterator) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + String nodePath = getNodePath(nodeId, sInfo); + NodeIterator it = getVersionManager(sInfo).merge(nodePath, srcWorkspaceName, bestEffort, isShallow); + List ids = new ArrayList(); + while (it.hasNext()) { + ids.add(idFactory.createNodeId(it.nextNode() + )); + } + return ids.iterator(); + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void resolveMergeConflict(final SessionInfo sessionInfo, + final NodeId nodeId, + final NodeId[] mergeFailedIds, + final NodeId[] predecessorIds) + throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + Node node = getNode(nodeId, sInfo); + Version version = null; + boolean cancel; + NamePathResolver resolver = sInfo.getNamePathResolver(); + List l = Arrays.asList(mergeFailedIds); + Property mergeFailed = node.getProperty(resolver.getJCRName(NameConstants.JCR_MERGEFAILED)); + for (Value value : mergeFailed.getValues()) { + String uuid = value.getString(); + if (!l.contains(idFactory.createNodeId(uuid))) { + version = (Version) sInfo.getSession().getNodeByIdentifier(uuid); + break; + } + } + + l = new ArrayList(predecessorIds.length); + l.addAll(Arrays.asList(predecessorIds)); + Property predecessors = node.getProperty(resolver.getJCRName(NameConstants.JCR_PREDECESSORS)); + for (Value value : predecessors.getValues()) { + NodeId vId = idFactory.createNodeId(value.getString()); + l.remove(vId); + } + cancel = l.isEmpty(); + if (cancel) { + node.cancelMerge(version); + } else { + node.doneMerge(version); + } + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void addVersionLabel(final SessionInfo sessionInfo, + final NodeId versionHistoryId, + final NodeId versionId, + final Name label, + final boolean moveLabel) throws VersionException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + String jcrLabel; + jcrLabel = sInfo.getNamePathResolver().getJCRName(label); + Node version = getNode(versionId, sInfo); + Node vHistory = getNode(versionHistoryId, sInfo); + if (vHistory instanceof VersionHistory) { + ((VersionHistory) vHistory).addVersionLabel( + version.getName(), jcrLabel, moveLabel); + } else { + throw new RepositoryException("versionHistoryId does not reference a VersionHistory node"); + } + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public void removeVersionLabel(final SessionInfo sessionInfo, + final NodeId versionHistoryId, + final NodeId versionId, + final Name label) throws VersionException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + String jcrLabel; + jcrLabel = sInfo.getNamePathResolver().getJCRName((label)); + Node vHistory = getNode(versionHistoryId, sInfo); + if (vHistory instanceof VersionHistory) { + ((VersionHistory) vHistory).removeVersionLabel(jcrLabel); + } else { + throw new RepositoryException("versionHistoryId does not reference a VersionHistory node"); + } + return null; + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public NodeId createActivity(SessionInfo sessionInfo, final String title) throws UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + final VersionManager vMgr = getVersionManager(sInfo); + Node activity = (Node) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + return vMgr.createActivity(title); + } + }, getSessionInfoImpl(sessionInfo)); + return idFactory.createNodeId(activity); + } + + /** + * {@inheritDoc} + */ + public void removeActivity(SessionInfo sessionInfo, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + final VersionManager vMgr = getVersionManager(sInfo); + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + vMgr.removeActivity(getNode(activityId, sInfo)); + return null; + } + }, getSessionInfoImpl(sessionInfo)); + } + + /** + * {@inheritDoc} + */ + public Iterator mergeActivity(SessionInfo sessionInfo, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return (Iterator) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + Node node = getNode(activityId, sInfo); + NodeIterator it = getVersionManager(sInfo).merge(node); + List ids = new ArrayList(); + while (it.hasNext()) { + ids.add(idFactory.createNodeId(it.nextNode() + )); + } + return ids.iterator(); + } + }, sInfo); + } + + /** + * {@inheritDoc} + */ + public NodeId createConfiguration(SessionInfo sessionInfo, final NodeId nodeId) + throws UnsupportedRepositoryOperationException, RepositoryException { + final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + final VersionManager vMgr = getVersionManager(sInfo); + Node configuration = (Node) executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + return vMgr.createConfiguration(getNodePath(nodeId, sInfo)); + } + }, getSessionInfoImpl(sessionInfo)); + return idFactory.createNodeId(configuration); + } + + /** + * {@inheritDoc} + */ + public String[] getSupportedQueryLanguages(SessionInfo sessionInfo) + throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return sInfo.getSession().getWorkspace().getQueryManager().getSupportedQueryLanguages(); + } + + /** + * {@inheritDoc} + */ + public String[] checkQueryStatement(SessionInfo sessionInfo, + String statement, + String language, + Map namespaces) + throws InvalidQueryException, RepositoryException { + Query q = createQuery(getSessionInfoImpl(sessionInfo).getSession(), + statement, language, namespaces); + return q.getBindVariableNames(); + } + + /** + * {@inheritDoc} + */ + public QueryInfo executeQuery(SessionInfo sessionInfo, String statement, String language, Map namespaces, long limit, long offset, Map values) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Query query = createQuery(sInfo.getSession(), statement, + language, namespaces); + if (limit != -1) { + query.setLimit(limit); + } + if (offset != -1) { + query.setOffset(offset); + } + if (values != null && !values.isEmpty()) { + for (Map.Entry entry : values.entrySet()) { + Value value = ValueFormat.getJCRValue(entry.getValue(), sInfo.getNamePathResolver(), sInfo.getSession().getValueFactory()); + query.bindValue(entry.getKey(), value); + } + } + return new QueryInfoImpl(query.execute(), idFactory, + sInfo.getNamePathResolver(), getQValueFactory()); + } + + /** + * {@inheritDoc} + */ + public EventFilter createEventFilter(SessionInfo sessionInfo, + int eventTypes, + Path absPath, + boolean isDeep, + String[] uuid, + Name[] nodeTypeName, + boolean noLocal) + throws UnsupportedRepositoryOperationException, RepositoryException { + Set ntNames = null; + if (nodeTypeName != null) { + ntNames = new HashSet(Arrays.asList(nodeTypeName)); + } + return new EventFilterImpl(eventTypes, absPath, isDeep, uuid, ntNames, noLocal); + } + + /** + * {@inheritDoc} + */ + public Subscription createSubscription(SessionInfo sessionInfo, + EventFilter[] filters) + throws UnsupportedRepositoryOperationException, RepositoryException { + return getSessionInfoImpl(sessionInfo).createSubscription(idFactory, qValueFactory, filters); + } + + /** + * {@inheritDoc} + */ + public EventBundle[] getEvents(Subscription subscription, long timeout) + throws RepositoryException, UnsupportedRepositoryOperationException, InterruptedException { + if (subscription instanceof EventSubscription) { + return ((EventSubscription) subscription).getEventBundles(timeout); + } else { + throw new RepositoryException("Unknown subscription implementation: " + + subscription.getClass().getName()); + } + } + + /** + * {@inheritDoc} + */ + public EventBundle getEvents(SessionInfo sessionInfo, + EventFilter filter, + long after) + throws RepositoryException, UnsupportedRepositoryOperationException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + EventJournal journal = sInfo.getSession().getWorkspace().getObservationManager().getEventJournal(); + if (journal == null) { + throw new UnsupportedRepositoryOperationException(); + } + EventFactory factory = new EventFactory(sInfo.getSession(), + sInfo.getNamePathResolver(), idFactory, qValueFactory); + journal.skipTo(after); + List events = new ArrayList(); + int batchSize = 1024; + boolean distinctDates = true; + long lastDate = Long.MIN_VALUE; + while (journal.hasNext() && (batchSize > 0 || !distinctDates)) { + Event e = factory.fromJCREvent(journal.nextEvent()); + if (filter.accept(e, false)) { + distinctDates = lastDate != e.getDate(); + lastDate = e.getDate(); + events.add(e); + batchSize--; + } + } + return new EventBundleImpl(events, false); + } + + /** + * {@inheritDoc} + */ + public void updateEventFilters(Subscription subscription, + EventFilter[] filters) + throws RepositoryException { + getEventSubscription(subscription).setFilters(filters); + } + + /** + * {@inheritDoc} + */ + public void dispose(Subscription subscription) throws RepositoryException { + getEventSubscription(subscription).dispose(); + } + + /** + * {@inheritDoc} + */ + public Map getRegisteredNamespaces(SessionInfo sessionInfo) + throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + NamespaceRegistry nsReg = sInfo.getSession().getWorkspace().getNamespaceRegistry(); + Map namespaces = new HashMap(); + for (String prefix : nsReg.getPrefixes()) { + namespaces.put(prefix, nsReg.getURI(prefix)); + } + return namespaces; + } + + /** + * {@inheritDoc} + */ + public String getNamespaceURI(SessionInfo sessionInfo, String prefix) + throws NamespaceException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return sInfo.getSession().getWorkspace().getNamespaceRegistry().getURI(prefix); + } + + /** + * {@inheritDoc} + */ + public String getNamespacePrefix(SessionInfo sessionInfo, String uri) + throws NamespaceException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + return sInfo.getSession().getWorkspace().getNamespaceRegistry().getPrefix(uri); + } + + /** + * {@inheritDoc} + */ + public void registerNamespace(SessionInfo sessionInfo, + String prefix, + String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + Session session = getSessionInfoImpl(sessionInfo).getSession(); + NamespaceRegistry nsReg = session.getWorkspace().getNamespaceRegistry(); + nsReg.registerNamespace(prefix, uri); + } + + /** + * {@inheritDoc} + */ + public void unregisterNamespace(SessionInfo sessionInfo, String uri) + throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException { + Session session = getSessionInfoImpl(sessionInfo).getSession(); + NamespaceRegistry nsReg = session.getWorkspace().getNamespaceRegistry(); + nsReg.unregisterNamespace(nsReg.getPrefix(uri)); + } + + /** + * {@inheritDoc} + */ + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + NodeTypeManager ntMgr = sInfo.getSession().getWorkspace().getNodeTypeManager(); + List nodeTypes = new ArrayList(); + try { + for (NodeTypeIterator it = ntMgr.getAllNodeTypes(); it.hasNext(); ) { + NodeType nt = it.nextNodeType(); + nodeTypes.add(new QNodeTypeDefinitionImpl(nt, + sInfo.getNamePathResolver(), getQValueFactory())); + } + } catch (NameException e) { + throw new RepositoryException(e); + } + return nodeTypes.iterator(); + } + + /** + * {@inheritDoc} + */ + public Iterator getQNodeTypeDefinitions(SessionInfo sessionInfo, Name[] nodetypeNames) throws RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + NodeTypeManager ntMgr = sInfo.getSession().getWorkspace().getNodeTypeManager(); + List defs = new ArrayList(); + for (Name nodetypeName : nodetypeNames) { + try { + String ntName = sInfo.getNamePathResolver().getJCRName(nodetypeName); + NodeType nt = ntMgr.getNodeType(ntName); + defs.add(new QNodeTypeDefinitionImpl(nt, + sInfo.getNamePathResolver(), getQValueFactory())); + + // in addition pack all supertypes into the return value + NodeType[] supertypes = nt.getSupertypes(); + for (NodeType supertype : supertypes) { + defs.add(new QNodeTypeDefinitionImpl(supertype, + sInfo.getNamePathResolver(), getQValueFactory())); + } + } catch (NameException e) { + throw new RepositoryException(e); + } + } + return defs.iterator(); + } + + /** + * {@inheritDoc} + */ + public void registerNodeTypes(SessionInfo sessionInfo, QNodeTypeDefinition[] nodeTypeDefinitions, boolean allowUpdate) throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, UnsupportedRepositoryOperationException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + NodeTypeManager ntMgr = sInfo.getSession().getWorkspace().getNodeTypeManager(); + + NodeTypeDefinition[] defs = new NodeTypeDefinition[nodeTypeDefinitions.length]; + for (int i = 0; i < nodeTypeDefinitions.length; i++) { + defs[i] = new NodeTypeDefinitionImpl(nodeTypeDefinitions[i], sInfo.getNamePathResolver(), sInfo.getSession().getValueFactory()) { + }; + } + ntMgr.registerNodeTypes(defs, allowUpdate); + } + + /** + * {@inheritDoc} + */ + public void unregisterNodeTypes(SessionInfo sessionInfo, Name[] nodeTypeNames) throws UnsupportedRepositoryOperationException, NoSuchNodeTypeException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + NodeTypeManager ntMgr = sInfo.getSession().getWorkspace().getNodeTypeManager(); + String[] names = new String[nodeTypeNames.length]; + for (int i = 0; i < nodeTypeNames.length; i++) { + names[i] = sInfo.getNamePathResolver().getJCRName(nodeTypeNames[i]); + } + ntMgr.unregisterNodeTypes(names); + } + + /** + * {@inheritDoc} + */ + public void createWorkspace(SessionInfo sessionInfo, String name, String srcWorkspaceName) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Workspace wsp = sInfo.getSession().getWorkspace(); + if (srcWorkspaceName == null) { + wsp.createWorkspace(name); + } else { + wsp.createWorkspace(name, srcWorkspaceName); + } + } + + /** + * {@inheritDoc} + */ + public void deleteWorkspace(SessionInfo sessionInfo, String name) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException { + SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo); + Workspace wsp = sInfo.getSession().getWorkspace(); + wsp.deleteWorkspace(name); + } + + //----------------------------< internal >---------------------------------- + + private final class BatchImpl implements Batch { + + private final SessionInfoImpl sInfo; + /* If this batch needs to remove multiple same-name-siblings starting + from lower index, the index of the following siblings must be reset + in order to avoid PathNotFoundException. + */ + private final Set removedNodeIds = new HashSet(); + + private boolean failed = false; + + BatchImpl(SessionInfoImpl sInfo) { + this.sInfo = sInfo; + } + + //----------------------------------------------------------< Batch >--- + @Override + public void addNode(final NodeId parentId, + final Name nodeName, + final Name nodetypeName, + final String uuid) throws RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Session s = sInfo.getSession(); + Node parent = getParent(parentId, sInfo); + + String jcrName = getJcrName(nodeName); + String ntName = getJcrName(nodetypeName); + if (uuid == null) { + if (ntName == null) { + parent.addNode(jcrName); + } else { + parent.addNode(jcrName, ntName); + } + } else { + String xml = createXMLFragment(jcrName, ntName, uuid); + InputStream in = new ByteArrayInputStream(xml.getBytes()); + try { + s.importXML(parent.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + } catch (IOException e) { + throw new RepositoryException(e.getMessage(), e); + } + } + return null; + } + }); + } + + @Override + public void addProperty(final NodeId parentId, + final Name propertyName, + final QValue value) + throws ValueFormatException, VersionException, LockException, ConstraintViolationException, PathNotFoundException, ItemExistsException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Session s = sInfo.getSession(); + Node parent = getParent(parentId, sInfo); + Value jcrValue = ValueFormat.getJCRValue(value, + sInfo.getNamePathResolver(), s.getValueFactory()); + parent.setProperty(getJcrName(propertyName), jcrValue); + return null; + } + }); + } + + @Override + public void addProperty(final NodeId parentId, + final Name propertyName, + final QValue[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, PathNotFoundException, ItemExistsException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Session s = sInfo.getSession(); + Node n = getParent(parentId, sInfo); + Value[] jcrValues = new Value[values.length]; + for (int i = 0; i < jcrValues.length; i++) { + jcrValues[i] = ValueFormat.getJCRValue(values[i], + sInfo.getNamePathResolver(), s.getValueFactory()); + } + n.setProperty(getJcrName(propertyName), jcrValues); + return null; + } + }); + } + + @Override + public void setValue(final PropertyId propertyId, final QValue value) + throws RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Session s = sInfo.getSession(); + Value jcrValue = ValueFormat.getJCRValue(value, + sInfo.getNamePathResolver(), s.getValueFactory()); + getProperty(propertyId, sInfo).setValue(jcrValue); + return null; + } + }); + } + + @Override + public void setValue(final PropertyId propertyId, final QValue[] values) + throws RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Session s = sInfo.getSession(); + Value[] jcrValues = new Value[values.length]; + for (int i = 0; i < jcrValues.length; i++) { + jcrValues[i] = ValueFormat.getJCRValue(values[i], + sInfo.getNamePathResolver(), s.getValueFactory()); + } + getProperty(propertyId, sInfo).setValue(jcrValues); + return null; + } + }); + } + + @Override + public void remove(final ItemId itemId) throws RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + try { + if (itemId.denotesNode()) { + NodeId nodeId = calcRemoveNodeId(itemId); + getNode(nodeId, sInfo).remove(); + } else { + getProperty((PropertyId) itemId, sInfo).remove(); + } + } catch (ItemNotFoundException e) { + // item was present in jcr2spi but got removed on the + // persistent layer in the mean time. + throw new InvalidItemStateException(e); + } catch (PathNotFoundException e) { + // item was present in jcr2spi but got removed on the + // persistent layer in the mean time. + throw new InvalidItemStateException(e); + } + return null; + } + }); + } + + private NodeId calcRemoveNodeId(ItemId itemId) throws MalformedPathException { + NodeId nodeId = (NodeId) itemId; + Path p = itemId.getPath(); + if (p != null) { + removedNodeIds.add(nodeId); + int index = p.getNormalizedIndex(); + if (index > Path.INDEX_DEFAULT) { + Path.Element[] elems = p.getElements(); + PathBuilder pb = new PathBuilder(); + for (int i = 0; i <= elems.length - 2; i++) { + pb.addLast(elems[i]); + } + pb.addLast(p.getName(), index - 1); + + NodeId prevSibling = idFactory.createNodeId(itemId.getUniqueID(), pb.getPath()); + if (removedNodeIds.contains(prevSibling)) { + nodeId = prevSibling; + } + } + } + return nodeId; + } + + @Override + public void reorderNodes(final NodeId parentId, + final NodeId srcNodeId, + final NodeId beforeNodeId) + throws RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Node parent = getParent(parentId, sInfo); + Node srcNode = getNode(srcNodeId, sInfo); + Node beforeNode = null; + if (beforeNodeId != null) { + beforeNode = getNode(beforeNodeId, sInfo); + } + String srcPath = srcNode.getName(); + if (srcNode.getIndex() > 1) { + srcPath += "[" + srcNode.getIndex() + "]"; + } + String beforePath = null; + if (beforeNode != null) { + beforePath = beforeNode.getName(); + if (beforeNode.getIndex() > 1) { + beforePath += "[" + beforeNode.getIndex() + "]"; + } + } + parent.orderBefore(srcPath, beforePath); + return null; + } + }); + } + + @Override + public void setMixins(final NodeId nodeId, + final Name[] mixinNodeTypeIds) + throws RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Set mixinNames = new HashSet(); + for (Name mixinNodeTypeId : mixinNodeTypeIds) { + mixinNames.add(getJcrName(mixinNodeTypeId)); + } + Node n = getNode(nodeId, sInfo); + Set currentMixins = new HashSet(); + for (NodeType nt : n.getMixinNodeTypes()) { + currentMixins.add(nt.getName()); + } + Set remove = new HashSet(currentMixins); + remove.removeAll(mixinNames); + mixinNames.removeAll(currentMixins); + for (String mixName : remove) { + n.removeMixin(mixName); + } + for (String mixName : mixinNames) { + n.addMixin(mixName); + } + return null; + } + }); + } + + @Override + public void setPrimaryType(final NodeId nodeId, final Name primaryNodeTypeName) throws RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Node n = getNode(nodeId, sInfo); + n.setPrimaryType(getJcrName(primaryNodeTypeName)); + return null; + } + }); + } + + @Override + public void move(final NodeId srcNodeId, + final NodeId destParentNodeId, + final Name destName) throws RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + String srcPath = pathForId(srcNodeId, sInfo); + String destPath = pathForId(destParentNodeId, sInfo); + if (destPath.length() > 1) { + destPath += "/"; + } + destPath += getJcrName(destName); + sInfo.getSession().move(srcPath, destPath); + return null; + } + }); + } + + @Override + public void setTree(final NodeId parentId, Tree tree) throws RepositoryException { + if (!(tree instanceof XmlTree)) { + throw new RepositoryException("Unknown Tree implementation: " + tree.getClass().getName()); + } + + final XmlTree xmlTree = (XmlTree) tree; + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + Session s = sInfo.getSession(); + Node parent = getParent(parentId, sInfo); + String xml = xmlTree.toXML();; + InputStream in = new ByteArrayInputStream(xml.getBytes()); + try { + s.importXML(parent.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + } catch (IOException e) { + throw new RepositoryException(e.getMessage(), e); + } finally { + try { + in.close(); + } catch (IOException e) { + throw new RepositoryException(e.getMessage(), e); + } + } + return null; + } + }); + } + + //---------------------------------------------------------------------- + private void executeGuarded(Callable call) throws RepositoryException { + if (failed) { + return; + } + try { + call.run(); + } catch (RepositoryException e) { + failed = true; + sInfo.getSession().refresh(false); + throw e; + } catch (RuntimeException e) { + failed = true; + sInfo.getSession().refresh(false); + throw e; + } + } + + private String getJcrName(Name name) throws RepositoryException { + if (name == null) { + return null; + } + return sInfo.getNamePathResolver().getJCRName((name)); + } + + private String createXMLFragment(String nodeName, String ntName, String uuid) { + StringBuffer xml = new StringBuffer(""); + xml.append(""); + // jcr:primaryType + xml.append(""); + xml.append("").append(ntName).append(""); + xml.append(""); + // jcr:uuid + xml.append(""); + xml.append("").append(uuid).append(""); + xml.append(""); + xml.append(""); + return xml.toString(); + } + + private void end() throws AccessDeniedException, ItemExistsException, + ConstraintViolationException, InvalidItemStateException, + VersionException, LockException, NoSuchNodeTypeException, + RepositoryException { + executeGuarded(new Callable() { + public Object run() throws RepositoryException { + executeWithLocalEvents(new Callable() { + public Object run() throws RepositoryException { + sInfo.getSession().save(); + return null; + } + }, sInfo); + return null; + } + }); + } + } + + private interface Callable { + public Object run() throws RepositoryException; + } + + private SessionInfoImpl getSessionInfoImpl(SessionInfo sessionInfo) + throws RepositoryException { + if (sessionInfo instanceof SessionInfoImpl) { + return (SessionInfoImpl) sessionInfo; + } else { + throw new RepositoryException("Unknown SessionInfo implementation: " + + sessionInfo.getClass().getName()); + } + } + + private EventSubscription getEventSubscription(Subscription subscription) + throws RepositoryException { + if (subscription instanceof EventSubscription) { + return (EventSubscription) subscription; + } else { + throw new RepositoryException("Unknown Subscription implementation: " + + subscription.getClass().getName()); + } + } + + private String getDestinationPath(NodeId destParentNodeId, Name destName, SessionInfoImpl sessionInfo) throws RepositoryException { + StringBuffer destPath = new StringBuffer(pathForId(destParentNodeId, sessionInfo)); + if (destPath.length() > 1) { + destPath.append("/"); + } + destPath.append(sessionInfo.getNamePathResolver().getJCRName(destName)); + return destPath.toString(); + } + + private String pathForId(ItemId id, SessionInfoImpl sessionInfo) + throws RepositoryException { + Session session = sessionInfo.getSession(); + StringBuffer path = new StringBuffer(); + if (id.getUniqueID() != null) { + path.append(session.getNodeByIdentifier(id.getUniqueID()).getPath()); + } else { + path.append("/"); + } + + if (id.getPath() == null) { + // we're done + return path.toString(); + } + + if (id.getPath().isAbsolute()) { + if (path.length() == 1) { + // root path ends with slash + path.setLength(0); + } + } else { + // path is relative + if (path.length() > 1) { + path.append("/"); + } + } + path.append(sessionInfo.getNamePathResolver().getJCRPath(id.getPath())); + return path.toString(); + } + + private Node getParent(NodeId parentId, SessionInfoImpl sessionInfo) throws InvalidItemStateException, RepositoryException { + try { + return getNode(parentId, sessionInfo); + } catch (PathNotFoundException e) { + // if the parent of an batch operation is not available, this indicates + // that it has been destroyed by another session. + throw new InvalidItemStateException(e); + } catch (ItemNotFoundException e) { + // if the parent of an batch operation is not available, this indicates + // that it has been destroyed by another session. + throw new InvalidItemStateException(e); + } + } + + private Node getNode(NodeId id, SessionInfoImpl sessionInfo) throws ItemNotFoundException, PathNotFoundException, RepositoryException { + Session session = sessionInfo.getSession(); + Node n; + if (id.getUniqueID() != null) { + n = session.getNodeByIdentifier(id.getUniqueID()); + } else { + n = session.getRootNode(); + } + Path path = id.getPath(); + if (path == null || path.denotesRoot()) { + return n; + } + String jcrPath; + jcrPath = sessionInfo.getNamePathResolver().getJCRPath(path); + if (path.isAbsolute()) { + jcrPath = jcrPath.substring(1, jcrPath.length()); + } + return n.getNode(jcrPath); + } + + private String getNodePath(NodeId nodeId, SessionInfoImpl sessionInfo) throws RepositoryException { + // TODO: improve. avoid round trip over node access. + return getNode(nodeId, sessionInfo).getPath(); + } + + private Property getProperty(PropertyId id, SessionInfoImpl sessionInfo) throws ItemNotFoundException, PathNotFoundException, RepositoryException { + Session session = sessionInfo.getSession(); + Node n; + if (id.getUniqueID() != null) { + n = session.getNodeByIdentifier(id.getUniqueID()); + } else { + n = session.getRootNode(); + } + Path path = id.getPath(); + String jcrPath = sessionInfo.getNamePathResolver().getJCRPath(path); + if (path.isAbsolute()) { + jcrPath = jcrPath.substring(1, jcrPath.length()); + } + return n.getProperty(jcrPath); + } + + private VersionManager getVersionManager(SessionInfoImpl sessionInfo) throws RepositoryException { + return sessionInfo.getSession().getWorkspace().getVersionManager(); + } + + private Query createQuery(Session session, + String statement, + String language, + Map namespaces) + throws InvalidQueryException, RepositoryException { + QueryManager qMgr = session.getWorkspace().getQueryManager(); + + // apply namespace mappings to session + Map previous = setNamespaceMappings(session, namespaces); + try { + return qMgr.createQuery(statement, language); + } finally { + // reset namespace mappings + setNamespaceMappings(session, previous); + } + } + + /** + * Utility method that changes the namespace mappings of the + * given sessions to include the given prefix to URI mappings. + * + * @param session current session + * @param namespaces prefix to URI mappings + * @return the previous namespace mappings that were modified + * @throws RepositoryException if a repository error occurs + */ + private Map setNamespaceMappings(Session session, Map namespaces) + throws RepositoryException { + Map previous = new HashMap(); + + for (Map.Entry entry : namespaces.entrySet()) { + String uri = entry.getValue(); + String prefix = entry.getKey(); + + // Get the previous prefix for this URI, throws if + // URI not found (which is OK, as that's an error) + String oldPrefix = session.getNamespacePrefix(uri); + // If the prefixes are different, we need to remap the namespace + if (!prefix.equals(oldPrefix)) { + // Check if the new prefix is mapped to some other URI + String oldURI = safeGetURI(session, prefix); + if (oldURI != null) { + // Find an unused prefix and map the old URI to it + int i = 2; + String tmpPrefix = oldPrefix + i++; + while (safeGetURI(session, tmpPrefix) != null + || namespaces.containsKey(tmpPrefix)) { + tmpPrefix = oldPrefix + i++; + } + session.setNamespacePrefix(tmpPrefix, oldURI); + previous.put(prefix, oldURI); // remember the old URI + } + // It's now safe to remap + session.setNamespacePrefix(prefix, uri); + previous.put(oldPrefix, uri); // remember the old prefix + } + } + + return previous; + } + + /** + * Utility method that returns the namespace URI mapped to the given + * prefix, or null if the prefix is not mapped. + * + * @param session current session + * @param prefix namespace prefix + * @return namespace URI or null + * @throws RepositoryException if a repository error occurs + */ + private String safeGetURI(Session session, String prefix) + throws RepositoryException { + try { + return session.getNamespaceURI(prefix); + } catch (NamespaceException e) { + return null; + } + } + + private Object executeWithLocalEvents(Callable call, SessionInfoImpl sInfo) + throws RepositoryException { + if (supportsObservation) { + // register local event listener + Collection subscr = sInfo.getSubscriptions(); + if (subscr.size() != 0) { + ObservationManager obsMgr = sInfo.getSession().getWorkspace().getObservationManager(); + List listeners = new ArrayList(subscr.size()); + try { + for (EventSubscription s : subscr) { + EventListener listener = s.getLocalEventListener(); + listeners.add(listener); + obsMgr.addEventListener(listener, EventSubscription.ALL_EVENTS, + "/", true, null, null, false); + } + return call.run(); + } finally { + for (EventListener listener : listeners) { + try { + obsMgr.removeEventListener(listener); + } catch (RepositoryException e) { + // ignore and remove next + } + } + } + } + } + // if we get here simply run as is + return call.run(); + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/SessionInfoImpl.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/SessionInfoImpl.java new file mode 100644 index 00000000000..2c1045f52dc --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/SessionInfoImpl.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.spi2jcr; + +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.Subscription; +import org.apache.jackrabbit.spi.EventFilter; +import org.apache.jackrabbit.spi.IdFactory; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingNameResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.conversion.ParsingPathResolver; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; + +import javax.jcr.NamespaceException; +import javax.jcr.NamespaceRegistry; +import javax.jcr.Session; +import javax.jcr.RepositoryException; +import javax.jcr.Credentials; +import javax.jcr.UnsupportedRepositoryOperationException; +import javax.jcr.lock.LockException; +import java.io.ObjectInputStream; +import java.io.ByteArrayInputStream; +import java.io.ObjectOutputStream; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Collection; +import java.util.Collections; + +/** + * SessionInfoImpl implements a session info based on a JCR + * {@link Session}. + */ +class SessionInfoImpl implements SessionInfo { + + /** + * The underlying JCR session. + */ + private final Session session; + + /** + * The namespace resolver for this session info. + */ + private final NamePathResolver resolver; + + /** + * A copy of the credentials that were used to obtain the JCR session. + */ + private Credentials credentials; + + /** + * The subscriptions that are currently in place for this session info. + */ + private List subscriptions = Collections.emptyList(); + + /** + * Monitor object for subscription changes. + */ + private final Object subscriptionChange = new Object(); + + /** + * Creates a new session info based on the given session. + * + * @param session the JCR session. + * @param credentials a copy of the credentials that were used to obtain the + * @param nameFactory + * @param pathFactory + * @throws RepositoryException + */ + SessionInfoImpl(Session session, Credentials credentials, + NameFactory nameFactory, PathFactory pathFactory) throws RepositoryException { + this.session = session; + this.credentials = credentials; + + final NamespaceRegistry nsReg = session.getWorkspace().getNamespaceRegistry(); + final NamespaceResolver nsResolver = new NamespaceResolver() { + public String getPrefix(String uri) throws NamespaceException { + try { + return nsReg.getPrefix(uri); + } + catch (RepositoryException e) { + // should never get here... + throw new NamespaceException("internal error: failed to resolve namespace uri", e); + } + } + + public String getURI(String prefix) throws NamespaceException { + try { + return nsReg.getURI(prefix); + } + catch (RepositoryException e) { + // should never get here... + throw new NamespaceException("internal error: failed to resolve namespace prefix", e); + } + } + }; + + final NameResolver nResolver = new ParsingNameResolver(nameFactory, nsResolver); + final PathResolver pResolver = new ParsingPathResolver(pathFactory, nResolver); + + this.resolver = new DefaultNamePathResolver(nResolver, pResolver); + } + + /** + * @return the underlying session. + */ + Session getSession() { + return session; + } + + /** + * @return the namespace resolver for this session info. + */ + NamePathResolver getNamePathResolver() { + return resolver; + } + + /** + * @return credentials that were used to obtain this session info. + * @throws RepositoryException if the credentials cannot be duplicated. + */ + Credentials getCredentials() throws RepositoryException { + // return a duplicate + return duplicateCredentials(credentials); + } + + Collection getSubscriptions() { + synchronized (subscriptionChange) { + return subscriptions; + } + } + + /** + * Creates a subscriptions for this session info. + * + * @param idFactory the id factory. + * @param qValueFactory + * @param filters the initial list of filters. + * @return a subscription. + * @throws RepositoryException + */ + Subscription createSubscription(IdFactory idFactory, QValueFactory qValueFactory, EventFilter[] filters) + throws RepositoryException { + synchronized (subscriptionChange) { + List tmp = new ArrayList(subscriptions); + EventSubscription s = new EventSubscription(idFactory, qValueFactory, this, filters); + tmp.add(s); + subscriptions = Collections.unmodifiableList(tmp); + return s; + } + } + + /** + * Removes the subscription from this session info is it exists. + * + * @param subscription the subscription to remove. + */ + void removeSubscription(EventSubscription subscription) { + synchronized (subscriptionChange) { + List tmp = new ArrayList(subscriptions); + tmp.remove(subscription); + subscriptions = Collections.unmodifiableList(tmp); + } + } + + /** + * Returns a duplicate of the passed credentials + * + * @param credentials the credentials to duplicate. + * @return a duplicate of the passed credentials. + * @throws RepositoryException if an error occurs while duplicating the + * credentials. + */ + public static Credentials duplicateCredentials(Credentials credentials) + throws RepositoryException { + if (credentials == null) { + return null; + } + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream oOut = new ObjectOutputStream(out); + oOut.writeObject(credentials); + oOut.close(); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + ObjectInputStream oIn = new ObjectInputStream(in); + return (Credentials) oIn.readObject(); + } catch (Exception e) { + throw new RepositoryException(e); + } + } + + //--------------------------------------------------------< SessionInfo >--- + + /** + * @inheritDoc + */ + public String getUserID() { + return session.getUserID(); + } + + /** + * @inheritDoc + */ + public String getWorkspaceName() { + return session.getWorkspace().getName(); + } + + /** + * @inheritDoc + */ + public String[] getLockTokens() throws UnsupportedRepositoryOperationException, RepositoryException { + return session.getWorkspace().getLockManager().getLockTokens(); + } + + /** + * @inheritDoc + */ + public void addLockToken(String lockToken) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + session.getWorkspace().getLockManager().addLockToken(lockToken); + } + + /** + * @inheritDoc + */ + public void removeLockToken(String lockToken) throws UnsupportedRepositoryOperationException, LockException, RepositoryException { + session.getWorkspace().getLockManager().removeLockToken(lockToken); + } + + public void setUserData(String userData) throws RepositoryException { + session.getWorkspace().getObservationManager().setUserData(userData); + } +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/Spi2jcrRepositoryServiceFactory.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/Spi2jcrRepositoryServiceFactory.java new file mode 100644 index 00000000000..18cad70d2f2 --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/Spi2jcrRepositoryServiceFactory.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.spi2jcr; + +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.ItemInfoCache; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; + +public class Spi2jcrRepositoryServiceFactory implements RepositoryServiceFactory { + + /** + * Mandatory repository parameter: expects value to be an instance of {@link javax.jcr.Repository}. + */ + public static final String PARAM_REPOSITORY = "org.apache.jackrabbit.spi2jcr.Repository"; + + /** + * Optional batch read configuration parameter. If it is present the value is + * expected to be an instance of {@link org.apache.jackrabbit.spi2jcr.BatchReadConfig}. + */ + public static final String PARAM_BATCH_READ_CONFIG = "org.apache.jackrabbit.spi2jcr.BatchReadConfig"; + + /** + * Optional configuration parameter: It's value determines the size of the + * {@link ItemInfoCache} cache. Defaults to {@link ItemInfoCacheImpl#DEFAULT_CACHE_SIZE}. + */ + public static final String PARAM_ITEMINFO_CACHE_SIZE = "org.apache.jackrabbit.spi2jcr.ItemInfoCacheSize"; + + public RepositoryService createRepositoryService(Map parameters) throws RepositoryException { + if (parameters == null) { + throw new RepositoryException("Parameter " + PARAM_REPOSITORY + " missing"); + } + + Object repo = parameters.get(PARAM_REPOSITORY); + if (repo == null || !(repo instanceof Repository)) { + throw new RepositoryException("Parameter " + PARAM_REPOSITORY + " missing or not an instance of Repository"); + } + + BatchReadConfig brConfig; + Object obj = parameters.get(PARAM_BATCH_READ_CONFIG); + if (obj != null && obj instanceof BatchReadConfig) { + brConfig = (BatchReadConfig) obj; + } + else { + brConfig = new BatchReadConfig(); + } + + int itemInfoCacheSize = ItemInfoCacheImpl.DEFAULT_CACHE_SIZE; + Object param = parameters.get(PARAM_ITEMINFO_CACHE_SIZE); + if (param != null) { + try { + itemInfoCacheSize = Integer.parseInt(param.toString()); + } + catch (NumberFormatException e) { + // ignore, use default + } + } + + return new RepositoryServiceImpl((Repository) repo, brConfig, itemInfoCacheSize); + } + +} diff --git a/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/XmlTree.java b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/XmlTree.java new file mode 100644 index 00000000000..6f8a629ee9e --- /dev/null +++ b/jackrabbit-spi2jcr/src/main/java/org/apache/jackrabbit/spi2jcr/XmlTree.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.spi2jcr; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.Tree; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.tree.AbstractTree; + +class XmlTree extends AbstractTree { + + private final StringBuilder properties = new StringBuilder(); + + protected XmlTree(Name nodeName, Name ntName, String uniqueId, NamePathResolver resolver) { + super(nodeName, ntName, uniqueId, resolver); + } + + //-------------------------------------------------------< AbstractTree >--- + @Override + protected Tree createChild(Name name, Name primaryTypeName, String uniqueId) { + return new XmlTree(name, primaryTypeName, uniqueId, getResolver()); + } + + //---------------------------------------------------------------< Tree >--- + @Override + public void addProperty(NodeId parentId, Name propertyName, int propertyType, QValue value) throws RepositoryException { + addProperty(parentId, propertyName, propertyType, new QValue[] {value}); + + } + + @Override + public void addProperty(NodeId parentId, Name propertyName, int propertyType, QValue[] values) throws RepositoryException { + properties.append(""); + for (QValue value : values) { + properties.append("").append(value.getString()).append(""); + } + properties.append(""); + } + + //-------------------------------------------------------------------------- + String toXML() throws RepositoryException { + StringBuilder xml = new StringBuilder(""); + createXMLNodeFragment(xml, this, getResolver(), true); + return xml.toString(); + } + + private static void createXMLNodeFragment(StringBuilder xml, XmlTree tree, NamePathResolver resolver, boolean includeNsInfo) throws RepositoryException { + xml.append(""); + // jcr:primaryType + xml.append(""); + xml.append("").append(resolver.getJCRName(tree.getPrimaryTypeName())).append(""); + xml.append(""); + // jcr:uuid + String uniqueId = tree.getUniqueId(); + if (uniqueId != null) { + xml.append(""); + xml.append("").append(uniqueId).append(""); + xml.append(""); + } + + // create the xml fragment for all the child properties. + xml.append(tree.properties); + + // create xml for all child nodes + for (Tree child : tree.getChildren()) { + createXMLNodeFragment(xml, (XmlTree) child, resolver, false); + } + + xml.append(""); + } +} diff --git a/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/RepositoryStubImpl.java b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/RepositoryStubImpl.java new file mode 100644 index 00000000000..bbb249b348f --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/RepositoryStubImpl.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.spi2jcr; + +import org.apache.jackrabbit.core.JackrabbitRepositoryStub; +import org.apache.jackrabbit.core.security.principal.EveryonePrincipal; +import org.apache.jackrabbit.jcr2spi.AbstractRepositoryConfig; +import org.apache.jackrabbit.jcr2spi.RepositoryImpl; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.test.NotExecutableException; +import org.apache.jackrabbit.test.RepositoryStubException; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import java.security.Principal; +import java.util.Properties; + +/** + * RepositoryStubImpl implements a repository stub that + * initializes a Jackrabbit repository and wraps it with a SPI2JCR layer and + * a JCR2SPI layer. + */ +public class RepositoryStubImpl extends JackrabbitRepositoryStub { + + /** + * The Jackrabbit repository. + */ + private Repository repo; + + /** + * Constructor required by TCK. + * + * @param env the environment. + */ + public RepositoryStubImpl(Properties env) { + super(env); + } + + /** + * @return the repository instance to test. + * @throws RepositoryStubException if an error occurs while starting up the + * repository. + */ + public Repository getRepository() throws RepositoryStubException { + if (repo == null) { + try { + final RepositoryService service = getRepositoryService(); + repo = RepositoryImpl.create(new AbstractRepositoryConfig() { + public RepositoryService getRepositoryService() { + return service; + } + }); + } catch (RepositoryException e) { + throw new RepositoryStubException(e); + } + } + return repo; + } + + @Override + public Principal getKnownPrincipal(Session session) throws RepositoryException { + return EveryonePrincipal.getInstance(); + } + + /** + * + * @return + * @throws RepositoryStubException + */ + public RepositoryService getRepositoryService() throws RepositoryStubException { + Repository jackrabbitRepo = super.getRepository(); + + // TODO: make configurable + BatchReadConfig brconfig = new BatchReadConfig(); + brconfig.setDepth(NameConstants.NT_UNSTRUCTURED, BatchReadConfig.DEPTH_INFINITE); + + return new RepositoryServiceImpl(jackrabbitRepo, brconfig); + } +} diff --git a/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/ServiceStubImpl.java b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/ServiceStubImpl.java new file mode 100644 index 00000000000..b10cd431ad7 --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/ServiceStubImpl.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.spi2jcr; + +import java.util.Properties; +import java.util.Collections; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceStub; +import org.apache.jackrabbit.test.RepositoryStubException; +import org.apache.jackrabbit.test.RepositoryStub; + +/** ServiceStubImpl... */ +public class ServiceStubImpl extends RepositoryServiceStub { + + private RepositoryService service; + private Credentials adminCredentials; + private Credentials readOnlyCredentials; + + /** + * Implementations of this class must overwrite this constructor. + * + * @param env the environment variables. This parameter must not be null. + */ + public ServiceStubImpl(Properties env) { + super(env); + } + + public RepositoryService getRepositoryService() throws RepositoryException { + if (service == null) { + Repository repository; + try { + repository = RepositoryStub.getInstance(Collections.EMPTY_MAP).getRepository(); + } catch (RepositoryStubException e) { + throw new RepositoryException(e); + } + service = new RepositoryServiceImpl(repository, new BatchReadConfig()); + } + return service; + } + + public Credentials getAdminCredentials() { + if (adminCredentials == null) { + adminCredentials = new SimpleCredentials(getProperty(PROP_PREFIX + "." + PROP_ADMIN_NAME), + getProperty(PROP_PREFIX + "." + PROP_ADMIN_PWD).toCharArray()); + } + return adminCredentials; + } + + public Credentials getReadOnlyCredentials() { + if (readOnlyCredentials == null) { + readOnlyCredentials = new SimpleCredentials(getProperty(PROP_PREFIX + "." + PROP_READONLY_NAME), + getProperty(PROP_PREFIX + "." + PROP_READONLY_PWD).toCharArray()); + } + return readOnlyCredentials; + } +} \ No newline at end of file diff --git a/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/jcr2spi/TestAll.java b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/jcr2spi/TestAll.java new file mode 100644 index 00000000000..eb0724d27bc --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/jcr2spi/TestAll.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.spi2jcr.jcr2spi; + +import junit.framework.TestCase; +import junit.framework.Test; +import org.apache.jackrabbit.jcr2spi.Jcr2SpiTestSuite; + +/** TestAll... */ +public class TestAll extends TestCase { + + public static Test suite() { + return new Jcr2SpiTestSuite(); + } +} \ No newline at end of file diff --git a/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/spi/TestAll.java b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/spi/TestAll.java new file mode 100644 index 00000000000..bb3af3b2ef0 --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/spi2jcr/spi/TestAll.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.spi2jcr.spi; + +import junit.framework.TestCase; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** TestAll... */ +public class TestAll extends TestCase { + + public static Test suite() { + return new SpiTestSuite(); + } + + private static class SpiTestSuite extends TestSuite { + + private SpiTestSuite() { + super("SPI tests"); + + // all spi tests + addTest(org.apache.jackrabbit.spi.TestAll.suite()); + } + } +} diff --git a/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/test/JCRBenchmark.java b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/test/JCRBenchmark.java new file mode 100644 index 00000000000..76b62533781 --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/test/JCRBenchmark.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.test; + +import junit.framework.Test; +import junit.framework.TestCase; +import org.apache.jackrabbit.benchmark.BenchmarkSuite; + +/** + * Test suite that includes all test suites from jackrabbit-jcr-benchmark. + */ +public class JCRBenchmark extends TestCase { + + public static Test suite() { + return new BenchmarkSuite(); + } +} \ No newline at end of file diff --git a/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/test/TestAll.java b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/test/TestAll.java new file mode 100644 index 00000000000..27847098485 --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/java/org/apache/jackrabbit/test/TestAll.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.test; + +import junit.framework.TestCase; +import junit.framework.Test; +import org.apache.jackrabbit.test.JCRTestSuite; + +public class TestAll extends TestCase { + + public static Test suite() { + return new JCRTestSuite(); + } +} diff --git a/jackrabbit-spi2jcr/src/test/resources/logback-test.xml b/jackrabbit-spi2jcr/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..15d98f9b73f --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/resources/logback-test.xml @@ -0,0 +1,31 @@ + + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-spi2jcr/src/test/resources/repository.xml b/jackrabbit-spi2jcr/src/test/resources/repository.xml new file mode 100644 index 00000000000..37d5c61663e --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/resources/repository.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-spi2jcr/src/test/resources/repositoryServiceStubImpl.properties b/jackrabbit-spi2jcr/src/test/resources/repositoryServiceStubImpl.properties new file mode 100644 index 00000000000..e0f2a92592a --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/resources/repositoryServiceStubImpl.properties @@ -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. + +# +# This is the configuration file for the jackrabbit repositoryservice test stub. +# + +# the ServiceStubImpl used for SPI tests. +org.apache.jackrabbit.spi.repository_service_stub_impl=org.apache.jackrabbit.spi2jcr.ServiceStubImpl + +org.apache.jackrabbit.spi.admin.pwd=admin +org.apache.jackrabbit.spi.admin.name=admin +org.apache.jackrabbit.spi.readonly.pwd=anonymous +org.apache.jackrabbit.spi.readonly.name= +org.apache.jackrabbit.spi.workspacename=default + +org.apache.jackrabbit.spi.QValueFactoryTest.reference=cafebabe-cafe-babe-cafe-babecafebabe diff --git a/jackrabbit-spi2jcr/src/test/resources/repositoryStubImpl.properties b/jackrabbit-spi2jcr/src/test/resources/repositoryStubImpl.properties new file mode 100644 index 00000000000..8ed25565131 --- /dev/null +++ b/jackrabbit-spi2jcr/src/test/resources/repositoryStubImpl.properties @@ -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. + +# Use this stub implementation instead for testing SPI2JCR on Jackrabbit +javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.spi2jcr.RepositoryStubImpl + +# Use this stub implementation for test the JCR api via RMI +#javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.spi2jcr.RMIRepositoryStub + +# Use this stub implementation for testing Jackrabbit-Core without intermediate SPI +# javax.jcr.tck.repository_stub_impl=org.apache.jackrabbit.spi2jcr.DefaultRepositoryStub + +# the repository home +org.apache.jackrabbit.repository.home=target/repository + +# the repository configuration +org.apache.jackrabbit.repository.config=target/test-classes/repository.xml + +# alternative workspace used for update tests etc. +org.apache.jackrabbit.jcr2spi.workspace2.name=test + +# config for RMIRepositoryStub +# format: //{rmi-host}:{rmi-port}/{repository-name} +org.apache.jackrabbit.rmi.repository.uri=//localhost:1099/jackrabbit.repository diff --git a/jackrabbit-standalone/pom.xml b/jackrabbit-standalone/pom.xml new file mode 100644 index 00000000000..9b563737d8e --- /dev/null +++ b/jackrabbit-standalone/pom.xml @@ -0,0 +1,160 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-standalone + bundle + Jackrabbit Standalone + Runnable jar packaging of Apache Jackrabbit + + + + + maven-deploy-plugin + + true + + + + org.apache.felix + maven-bundle-plugin + true + + + + org.apache.jackrabbit.standalone + + + *;inline=*.txt|*.html|*.jsp|*.xml|*.jar|*.properties|remoting/**|bootstrap/**|javax/**|repackage/**|images/**|com/**|ch/**|jline/**|Resources/**|css/**|schema*/**|EDU/**|error/**|org/**|META-INF/*.tld|META-INF/maven/**|META-INF/services/**|WEB-INF/config.xml|WEB-INF/*.properties|WEB-INF/templates/** + + true + org.apache.jackrabbit.standalone.Main + + + + + verify + + baseline + + + + true + + + + + + + + + + javax.jcr + jcr + compile + + + org.osgi + org.osgi.annotation + provided + + + org.apache.jackrabbit + jackrabbit-webapp + ${project.version} + war + + + org.apache.jackrabbit + jackrabbit-webapp + ${project.version} + jar + + + org.apache.jackrabbit + jackrabbit-jcr2dav + ${project.version} + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-webapp + + + org.eclipse.jetty + jetty-jsp + + + org.eclipse.jetty + jetty-util + + + commons-cli + commons-cli + 1.2 + + + commons-chain + commons-chain + 1.2 + + + commons-logging + commons-logging + + + + + commons-digester + commons-digester + 1.8.1 + + + jline + jline + 0.9.94 + + + commons-jexl + commons-jexl + 1.1 + + + commons-logging + commons-logging + + + + + + diff --git a/jackrabbit-standalone/src/main/appended-resources/META-INF/LICENSE b/jackrabbit-standalone/src/main/appended-resources/META-INF/LICENSE new file mode 100644 index 00000000000..c83d80d7433 --- /dev/null +++ b/jackrabbit-standalone/src/main/appended-resources/META-INF/LICENSE @@ -0,0 +1,1884 @@ +APACHE JACKRABBIT SUBCOMPONENTS + +This component includes parts with separate copyright notices and license +terms. Your use of these subcomponents is subject to the terms and conditions +of the following licenses: + +XPath parser (jackrabbit-spi-commons) + + XPath 2.0/XQuery 1.0 Parser: + http://www.w3.org/2002/11/xquery-xpath-applets/xgrammar.zip + + Copyright (C) 2002 World Wide Web Consortium, (Massachusetts Institute of + Technology, European Research Consortium for Informatics and Mathematics, + Keio University). All Rights Reserved. + + This work is distributed under the W3C(R) Software License in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + W3C(R) SOFTWARE NOTICE AND LICENSE + http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + + This work (and included software, documentation such as READMEs, or + other related items) is being provided by the copyright holders under + the following license. By obtaining, using and/or copying this work, + you (the licensee) agree that you have read, understood, and will comply + with the following terms and conditions. + + Permission to copy, modify, and distribute this software and its + documentation, with or without modification, for any purpose and + without fee or royalty is hereby granted, provided that you include + the following on ALL copies of the software and documentation or + portions thereof, including modifications: + + 1. The full text of this NOTICE in a location viewable to users + of the redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, + or terms and conditions. If none exist, the W3C Software Short + Notice should be included (hypertext is preferred, text is + permitted) within the body of any redistributed or derivative code. + + 3. Notice of any changes or modifications to the files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT + HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR + DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, + TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and + any associated documentation will at all times remain with + copyright holders. + +PDFBox libraries (pdfbox, jempbox, fontbox) + + Copyright (c) 2002-2007, www.pdfbox.org + Copyright (c) 2006-2007, www.jempbox.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of pdfbox; nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Adobe Font Metrics (AFM) for PDF Core 14 Fonts + + This file and the 14 PostScript(R) AFM files it accompanies may be used, + copied, and distributed for any purpose and without charge, with or without + modification, provided that all copyright notices are retained; that the + AFM files are not distributed without this file; that all modifications + to this file or any of the AFM files are prominently noted in the modified + file(s); and that this paragraph is not modified. Adobe Systems has no + responsibility or obligation to support the use of the AFM files. + +CMaps for PDF Fonts (http://www.adobe.com/devnet/font/#pcfi and +ftp://ftp.oreilly.com/pub/examples/nutshell/cjkv/adobe/) + + Copyright 1990-2001 Adobe Systems Incorporated. + All Rights Reserved. + + Patents Pending + + NOTICE: All information contained herein is the property + of Adobe Systems Incorporated. + + Permission is granted for redistribution of this file + provided this copyright notice is maintained intact and + that the contents of this file are not altered in any + way from its original form. + + PostScript and Display PostScript are trademarks of + Adobe Systems Incorporated which may be registered in + certain jurisdictions. + +Glyphlist (http://www.adobe.com/devnet/opentype/archives/glyph.html) + + Copyright (c) 1997,1998,2002,2007 Adobe Systems Incorporated + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this documentation file to use, copy, publish, distribute, + sublicense, and/or sell copies of the documentation, and to permit + others to do the same, provided that: + - No modification, editing or other alteration of this document is + allowed; and + - The above copyright notice and this permission notice shall be + included in all copies of the documentation. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this documentation file, to create their own derivative works + from the content of this document to use, copy, publish, distribute, + sublicense, and/or sell the derivative works, and to permit others to do + the same, provided that the derived work is not represented as being a + copy or version of this document. + + Adobe shall not be liable to any party for any loss of revenue or profit + or for indirect, incidental, special, consequential, or other similar + damages, whether based on tort (including without limitation negligence + or strict liability), contract or other legal or equitable grounds even + if Adobe has been advised or had reason to know of the possibility of + such damages. The Adobe materials are provided on an "AS IS" basis. + Adobe specifically disclaims all express, statutory, or implied + warranties relating to the Adobe materials, including but not limited to + those concerning merchantability or fitness for a particular purpose or + non-infringement of any third party rights regarding the Adobe + materials. + +The International Components for Unicode (http://site.icu-project.org/) + + Copyright (c) 1995-2009 International Business Machines Corporation + and others + + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, and/or sell copies of the Software, and to permit persons + to whom the Software is furnished to do so, provided that the above + copyright notice(s) and this permission notice appear in all copies + of the Software and that both the above copyright notice(s) and this + permission notice appear in supporting documentation. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE + BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, + OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + SOFTWARE. + + Except as contained in this notice, the name of a copyright holder shall + not be used in advertising or otherwise to promote the sale, use or other + dealings in this Software without prior written authorization of the + copyright holder. + +MIME type information from file-4.26.tar.gz (http://www.darwinsys.com/file/) + + Copyright (c) Ian F. Darwin 1986, 1987, 1989, 1990, 1991, 1992, 1994, 1995. + Software written by Ian F. Darwin and others; + maintained 1994- Christos Zoulas. + + This software is not subject to any export provision of the United States + Department of Commerce, and may be exported to any country or planet. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice immediately at the beginning of the file, without modification, + this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Metadata extractor library (metadata-extractor) + + This is public domain software - that is, you can do whatever you want + with it, and include it software that is licensed under the GNU or the + BSD license, or whatever other licence you choose, including proprietary + closed source licenses. I do ask that you leave this header in tact. + + If you make modifications to this code that you think would benefit the + wider community, please send me a copy and I'll post it on my site. + + If you make use of this code, I'd appreciate hearing about it. + metadata_extractor [at] drewnoakes [dot] com + Latest version of this software kept at + http://drewnoakes.com/ + +ASM bytecode manipulation library (asm) + + Copyright (c) 2000-2005 INRIA, France Telecom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +SLF4J libraries (slf4j-api, log4j-over-slf4j, jcl-over-slf4j) + + Copyright (c) 2004-2008 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Logback library (logback-core, logback-classic) + + Logback: the reliable, generic, fast and flexible logging framework. + Copyright (C) 1999-2009, QOS.ch. All rights reserved. + + This program and the accompanying materials are dual-licensed under + either the terms of the Eclipse Public License v1.0 as published by + the Eclipse Foundation + + or (per the licensee's choosing) + + under the terms of the GNU Lesser General Public License version 2.1 + as published by the Free Software Foundation. + +XML API library, org.w3c classes (xml-apis) + + DOM Java Language Binding: + http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/java-binding.html + + W3C IPR SOFTWARE NOTICE + Copyright (C) 2000 World Wide Web Consortium, (Massachusetts Institute of + Technology, Institut National de Recherche en Informatique et en + Automatique, Keio University). All Rights Reserved. + + The DOM bindings are published under the W3C Software Copyright Notice + and License. The software license requires "Notice of any changes or + modifications to the W3C files, including the date changes were made." + Consequently, modified versions of the DOM bindings must document that + they do not conform to the W3C standard; in the case of the IDL binding, + the pragma prefix can no longer be 'w3c.org'; in the case of the Java + binding, the package names can no longer be in the 'org.w3c' package. + + Note: The original version of the W3C Software Copyright Notice and + License could be found at + http://www.w3.org/Consortium/Legal/copyright-software-19980720 + + Copyright (C) 1994-2000 World Wide Web Consortium, (Massachusetts + Institute of Technology, Institut National de Recherche en Informatique + et en Automatique, Keio University). All Rights Reserved. + http://www.w3.org/Consortium/Legal/ + + This W3C work (including software, documents, or other related items) is + being provided by the copyright holders under the following license. By + obtaining, using and/or copying this work, you (the licensee) agree that + you have read, understood, and will comply with the following terms and + conditions: + + Permission to use, copy, and modify this software and its documentation, + with or without modification, for any purpose and without fee or royalty + is hereby granted, provided that you include the following on ALL copies + of the software and documentation or portions thereof, including + modifications, that you make: + + 1. The full text of this NOTICE in a location viewable to users of the + redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, or + terms and conditions. If none exist, a short notice of the following + form (hypertext is preferred, text is permitted) should be used + within the body of any redistributed or derivative code: + "Copyright (C) [$date-of-software] World Wide Web Consortium, + (Massachusetts Institute of Technology, Institut National de + Recherche en Informatique et en Automatique, Keio University). + All Rights Reserved. http://www.w3.org/Consortium/Legal/" + + 3. Notice of any changes or modifications to the W3C files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS + MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT + NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR + PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE + ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and any + associated documentation will at all times remain with copyright holders. + +XML API library, org.xml.sax classes (xml-apis) + + SAX2 is Free! + + I hereby abandon any property rights to SAX 2.0 (the Simple API for + XML), and release all of the SAX 2.0 source code, compiled code, and + documentation contained in this distribution into the Public Domain. + SAX comes with NO WARRANTY or guarantee of fitness for any purpose. + + David Megginson, david@megginson.com + 2000-05-05 + +Concurrent library (concurrent-1.3.4.jar) + + http://g.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html + + All classes are released to the public domain and may be used for any + purpose whatsoever without permission or acknowledgment. Portions of + the CopyOnWriteArrayList and ConcurrentReaderHashMap classes are adapted + from Sun JDK source code. These are copyright of Sun Microsystems, Inc, + and are used with their kind permission, as described in this license: + + TECHNOLOGY LICENSE FROM SUN MICROSYSTEMS, INC. TO DOUG LEA + + Whereas Doug Lea desires to utlized certain Java Software technologies + in the util.concurrent technology; and Whereas Sun Microsystems, Inc. + ("Sun") desires that Doug Lea utilize certain Java Software technologies + in the util.concurrent technology; + + Therefore the parties agree as follows, effective May 31, 2002: + + "Java Software technologies" means + + classes/java/util/ArrayList.java, and + classes/java/util/HashMap.java. + + The Java Software technologies are Copyright (c) 1994-2000 Sun + Microsystems, Inc. All rights reserved. + + Sun hereby grants Doug Lea a non-exclusive, worldwide, non-transferrable + license to use, reproduce, create derivate works of, and distribute the + Java Software and derivative works thereof in source and binary forms + as part of a larger work, and to sublicense the right to use, reproduce + and distribute the Java Software and Doug Lea's derivative works as the + part of larger works through multiple tiers of sublicensees provided that + the following conditions are met: + + -Neither the name of or trademarks of Sun may be used to endorse or + promote products including or derived from the Java Software technology + without specific prior written permission; and + -Redistributions of source or binary code must contain the above + copyright notice, this notice and and the following disclaimers: + + This software is provided "AS IS," without a warranty of any kind. + ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + MICROSYSTEMS, INC. AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES + SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING + THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN MICROSYSTEMS, INC. + OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, + HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF + THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN MICROSYSTEMS, INC. + HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + You acknowledge that Software is not designed,licensed or intended for + use in the design, construction, operation or maintenance of any nuclear + facility. + +Office Open XML schemas (ooxml-schemas-1.0.jar) + + The Office Open XML schema definitions used by Apache POI are + a part of the Office Open XML ECMA Specification (ECMA-376, [1]). + As defined in section 9.4 of the ECMA bylaws [2], this specification + is available to all interested parties without restriction: + + 9.4 All documents when approved shall be made available to + all interested parties without restriction. + + Furthermore, both Microsoft and Adobe have granted patent licenses + to this work [3,4,5]. + + [1] http://www.ecma-international.org/publications/standards/Ecma-376.htm + [2] http://www.ecma-international.org/memento/Ecmabylaws.htm + [3] http://www.microsoft.com/interop/osp/ + [4] http://www.ecma-international.org/publications/files/ECMA-ST/Ecma%20PATENT/ECMA-376%20Edition%201%20Microsoft%20Patent%20Declaration.pdf + [5] http://www.ecma-international.org/publications/files/ECMA-ST/Ecma%20PATENT/ga-2006-191.pdf + +DOM4J library (dom4j-1.6.1.jar) + + Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved. + + Redistribution and use of this software and associated documentation + ("Software"), with or without modification, are permitted provided + that the following conditions are met: + + 1. Redistributions of source code must retain copyright + statements and notices. Redistributions must also contain a + copy of this document. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + + 3. The name "DOM4J" must not be used to endorse or promote + products derived from this Software without prior written + permission of MetaStuff, Ltd. For written permission, + please contact dom4j-info@metastuff.com. + + 4. Products derived from this Software may not be called "DOM4J" + nor may "DOM4J" appear in their names without prior written + permission of MetaStuff, Ltd. DOM4J is a registered + trademark of MetaStuff, Ltd. + + 5. Due credit should be given to the DOM4J Project - + http://www.dom4j.org + + THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + +Unicode conversion code in Lucene Java (lucene-core) + + Copyright 2001-2004 Unicode, Inc. + + Disclaimer + + This source code is provided as is by Unicode, Inc. No claims are + made as to fitness for any particular purpose. No warranties of any + kind are expressed or implied. The recipient agrees to determine + applicability of information provided. If this file has been + purchased on magnetic or optical media from Unicode, Inc., the + sole remedy for any claim will be exchange of defective media + within 90 days of receipt. + + Limitations on Rights to Redistribute This Code + + Unicode, Inc. hereby grants the right to freely use the information + supplied in this file in the creation of products supporting the + Unicode Standard, and to make copies of this file in any form + for internal or external distribution as long as this notice + remains attached. + +Array utility code in Lucene Java (lucene-core) + + PSF LICENSE AGREEMENT FOR PYTHON 2.4 + ------------------------------------ + + 1. This LICENSE AGREEMENT is between the Python Software Foundation + ("PSF"), and the Individual or Organization ("Licensee") accessing and + otherwise using Python 2.4 software in source or binary form and its + associated documentation. + + 2. Subject to the terms and conditions of this License Agreement, PSF + hereby grants Licensee a nonexclusive, royalty-free, world-wide + license to reproduce, analyze, test, perform and/or display publicly, + prepare derivative works, distribute, and otherwise use Python 2.4 + alone or in any derivative version, provided, however, that PSF's + License Agreement and PSF's notice of copyright, i.e., "Copyright (c) + 2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved" + are retained in Python 2.4 alone or in any derivative version prepared + by Licensee. + + 3. In the event Licensee prepares a derivative work that is based on + or incorporates Python 2.4 or any part thereof, and wants to make + the derivative work available to others as provided herein, then + Licensee hereby agrees to include in any such work a brief summary of + the changes made to Python 2.4. + + 4. PSF is making Python 2.4 available to Licensee on an "AS IS" + basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.4 WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON + 2.4 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS + A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.4, + OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 7. Nothing in this License Agreement shall be deemed to create any + relationship of agency, partnership, or joint venture between PSF and + Licensee. This License Agreement does not grant permission to use PSF + trademarks or trade name in a trademark sense to endorse or promote + products or services of Licensee, or any third party. + + 8. By copying, installing or otherwise using Python 2.4, Licensee + agrees to be bound by the terms and conditions of this License + Agreement. + + BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 + ------------------------------------------- + + BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + + 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an + office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the + Individual or Organization ("Licensee") accessing and otherwise using + this software in source or binary form and its associated + documentation ("the Software"). + + 2. Subject to the terms and conditions of this BeOpen Python License + Agreement, BeOpen hereby grants Licensee a non-exclusive, + royalty-free, world-wide license to reproduce, analyze, test, perform + and/or display publicly, prepare derivative works, distribute, and + otherwise use the Software alone or in any derivative version, + provided, however, that the BeOpen Python License is retained in the + Software, alone or in any derivative version prepared by Licensee. + + 3. BeOpen is making the Software available to Licensee on an "AS IS" + basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE + SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS + AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY + DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 5. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 6. This License Agreement shall be governed by and interpreted in all + respects by the law of the State of California, excluding conflict of + law provisions. Nothing in this License Agreement shall be deemed to + create any relationship of agency, partnership, or joint venture + between BeOpen and Licensee. This License Agreement does not grant + permission to use BeOpen trademarks or trade names in a trademark + sense to endorse or promote products or services of Licensee, or any + third party. As an exception, the "BeOpen Python" logos available at + http://www.pythonlabs.com/logos.html may be used according to the + permissions granted on that web page. + + 7. By copying, installing or otherwise using the software, Licensee + agrees to be bound by the terms and conditions of this License + Agreement. + + CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 + --------------------------------------- + + 1. This LICENSE AGREEMENT is between the Corporation for National + Research Initiatives, having an office at 1895 Preston White Drive, + Reston, VA 20191 ("CNRI"), and the Individual or Organization + ("Licensee") accessing and otherwise using Python 1.6.1 software in + source or binary form and its associated documentation. + + 2. Subject to the terms and conditions of this License Agreement, CNRI + hereby grants Licensee a nonexclusive, royalty-free, world-wide + license to reproduce, analyze, test, perform and/or display publicly, + prepare derivative works, distribute, and otherwise use Python 1.6.1 + alone or in any derivative version, provided, however, that CNRI's + License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) + 1995-2001 Corporation for National Research Initiatives; All Rights + Reserved" are retained in Python 1.6.1 alone or in any derivative + version prepared by Licensee. Alternately, in lieu of CNRI's License + Agreement, Licensee may substitute the following text (omitting the + quotes): "Python 1.6.1 is made available subject to the terms and + conditions in CNRI's License Agreement. This Agreement together with + Python 1.6.1 may be located on the Internet using the following + unique, persistent identifier (known as a handle): 1895.22/1013. This + Agreement may also be obtained from a proxy server on the Internet + using the following URL: http://hdl.handle.net/1895.22/1013". + + 3. In the event Licensee prepares a derivative work that is based on + or incorporates Python 1.6.1 or any part thereof, and wants to make + the derivative work available to others as provided herein, then + Licensee hereby agrees to include in any such work a brief summary of + the changes made to Python 1.6.1. + + 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" + basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON + 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS + A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, + OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 7. This License Agreement shall be governed by the federal + intellectual property law of the United States, including without + limitation the federal copyright law, and, to the extent such + U.S. federal law does not apply, by the law of the Commonwealth of + Virginia, excluding Virginia's conflict of law provisions. + Notwithstanding the foregoing, with regard to derivative works based + on Python 1.6.1 that incorporate non-separable material that was + previously distributed under the GNU General Public License (GPL), the + law of the Commonwealth of Virginia shall govern this License + Agreement only as to issues arising under or with respect to + Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this + License Agreement shall be deemed to create any relationship of + agency, partnership, or joint venture between CNRI and Licensee. This + License Agreement does not grant permission to use CNRI trademarks or + trade name in a trademark sense to endorse or promote products or + services of Licensee, or any third party. + + 8. By clicking on the "ACCEPT" button where indicated, or by copying, + installing or otherwise using Python 1.6.1, Licensee agrees to be + bound by the terms and conditions of this License Agreement. + + ACCEPT + + + CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 + -------------------------------------------------- + + Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, + The Netherlands. All rights reserved. + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the name of Stichting Mathematisch + Centrum or CWI not be used in advertising or publicity pertaining to + distribution of the software without specific, written prior + permission. + + STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE + FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +JCR 2.0 API (jcr-2.0.jar) + + [Day Specification License] + + Day Management AG ("Licensor") is willing to license this specification + to you ONLY UPON THE CONDITION THAT YOU ACCEPT ALL OF THE TERMS CONTAINED + IN THIS LICENSE AGREEMENT ("Agreement"). Please read the terms and + conditions of this Agreement carefully. + + Content Repository for JavaTM Technology API Specification ("Specification") + Version: 2.0 + Status: FCS + Release: 10 August 2009 + + Copyright 2009 Day Management AG + Barfüsserplatz 6, 4001 Basel, Switzerland. + All rights reserved. + + NOTICE; LIMITED LICENSE GRANTS + + 1. License for Purposes of Evaluation and Developing Applications. + Licensor hereby grants you a fully-paid, non-exclusive, non-transferable, + worldwide, limited license (without the right to sublicense), under + Licensor's applicable intellectual property rights to view, download, + use and reproduce the Specification only for the purpose of internal + evaluation. This includes developing applications intended to run on an + implementation of the Specification provided that such applications do + not themselves implement any portion(s) of the Specification. + + 2. License for the Distribution of Compliant Implementations. Licensor + also grants you a perpetual, non-exclusive, non-transferable, worldwide, + fully paid-up, royalty free, limited license (without the right to + sublicense) under any applicable copyrights or, subject to the provisions + of subsection 4 below, patent rights it may have covering the + Specification to create and/or distribute an Independent Implementation + of the Specification that: (a) fully implements the Specification + including all its required interfaces and functionality; (b) does not + modify, subset, superset or otherwise extend the Licensor Name Space, or + include any public or protected packages, classes, Java interfaces, fields + or methods within the Licensor Name Space other than those + required/authorized by the Specification or Specifications being + implemented; and (c) passes the Technology Compatibility Kit (including + satisfying the requirements of the applicable TCK Users Guide) for such + Specification ("Compliant Implementation"). In addition, the foregoing + license is expressly conditioned on your not acting outside its scope. + No license is granted hereunder for any other purpose (including, for + example, modifying the Specification, other than to the extent of your + fair use rights, or distributing the Specification to third parties). + + 3. Pass-through Conditions. You need not include limitations (a)-(c) from + the previous paragraph or any other particular "pass through" requirements + in any license You grant concerning the use of your Independent + Implementation or products derived from it. However, except with respect + to Independent Implementations (and products derived from them) that + satisfy limitations (a)-(c) from the previous paragraph, You may neither: + (a) grant or otherwise pass through to your licensees any licenses under + Licensor's applicable intellectual property rights; nor (b) authorize your + licensees to make any claims concerning their implementation's compliance + with the Specification. + + 4. Reciprocity Concerning Patent Licenses. With respect to any patent + claims covered by the license granted under subparagraph 2 above that + would be infringed by all technically feasible implementations of the + Specification, such license is conditioned upon your offering on fair, + reasonable and non-discriminatory terms, to any party seeking it from + You, a perpetual, non-exclusive, non-transferable, worldwide license + under Your patent rights that are or would be infringed by all technically + feasible implementations of the Specification to develop, distribute and + use a Compliant Implementation. + + 5. Definitions. For the purposes of this Agreement: "Independent + Implementation" shall mean an implementation of the Specification that + neither derives from any of Licensor's source code or binary code + materials nor, except with an appropriate and separate license from + Licensor, includes any of Licensor's source code or binary code materials; + "Licensor Name Space" shall mean the public class or interface + declarations whose names begin with "java", "javax", "javax.jcr" or their + equivalents in any subsequent naming convention adopted by Licensor + through the Java Community Process, or any recognized successors or + replacements thereof; and "Technology Compatibility Kit" or "TCK" shall + mean the test suite and accompanying TCK User's Guide provided by + Licensor which corresponds to the particular version of the Specification + being tested. + + 6. Termination. This Agreement will terminate immediately without notice + from Licensor if you fail to comply with any material provision of or act + outside the scope of the licenses granted above. + + 7. Trademarks. No right, title, or interest in or to any trademarks, + service marks, or trade names of Licensor is granted hereunder. Java is + a registered trademark of Sun Microsystems, Inc. in the United States and + other countries. + + 8. Disclaimer of Warranties. The Specification is provided "AS IS". + LICENSOR MAKES NO REPRESENTATIONS OR WARRANTIES, EITHER EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT (INCLUDING AS A + CONSEQUENCE OF ANY PRACTICE OR IMPLEMENTATION OF THE SPECIFICATION), OR + THAT THE CONTENTS OF THE SPECIFICATION ARE SUITABLE FOR ANY PURPOSE. + This document does not represent any commitment to release or implement + any portion of the Specification in any product. + + The Specification could include technical inaccuracies or typographical + errors. Changes are periodically added to the information therein; these + changes will be incorporated into new versions of the Specification, if + any. Licensor may make improvements and/or changes to the product(s) + and/or the program(s) described in the Specification at any time. Any + use of such changes in the Specification will be governed by the + then-current license for the applicable version of the Specification. + + 9. Limitation of Liability. TO THE EXTENT NOT PROHIBITED BY LAW, IN NO + EVENT WILL LICENSOR BE LIABLE FOR ANY DAMAGES, INCLUDING WITHOUT + LIMITATION, LOST REVENUE, PROFITS OR DATA, OR FOR SPECIAL, INDIRECT, + CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND + REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF OR RELATED TO ANY + FURNISHING, PRACTICING, MODIFYING OR ANY USE OF THE SPECIFICATION, EVEN + IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 10. Report. If you provide Licensor with any comments or suggestions in + connection with your use of the Specification ("Feedback"), you hereby: + (i) agree that such Feedback is provided on a non-proprietary and + non-confidential basis, and (ii) grant Licensor a perpetual, + non-exclusive, worldwide, fully paid-up, irrevocable license, with the + right to sublicense through multiple levels of sublicensees, to + incorporate, disclose, and use without limitation the Feedback for any + purpose related to the Specification and future versions, + implementations, and test suites thereof. + + [Addendum to the Day Specification License] + + In addition to the permissions granted under the Specification + License, Day Management AG hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + license to reproduce, publicly display, publicly perform, + sublicense, and distribute unmodified copies of the Content + Repository for Java Technology API (JCR 2.0) Java Archive (JAR) + file ("jcr-2.0.jar") and to make, have made, use, offer to sell, + sell, import, and otherwise transfer said file on its own or + as part of a larger work that makes use of the JCR API. + + With respect to any patent claims covered by this license + that would be infringed by all technically feasible implementations + of the Specification, such license is conditioned upon your + offering on fair, reasonable and non-discriminatory terms, + to any party seeking it from You, a perpetual, non-exclusive, + non-transferable, worldwide license under Your patent rights + that are or would be infringed by all technically feasible + implementations of the Specification to develop, distribute + and use a Compliant Implementation. + +Eclipse JDT Core (core-3.1.1.jar) +AspectJ runtime library (aspectjrt) + + Eclipse Public License - v 1.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF + THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial code and + documentation distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + + i) changes to the Program, and + + ii) additions to the Program; + + where such changes and/or additions to the Program originate from and + are distributed by that particular Contributor. A Contribution + 'originates' from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include additions to the Program which: (i) are + separate modules of software distributed in conjunction with the + Program under their own license agreement, and (ii) are not derivative + works of the Program. + + "Contributor" means any person or entity that distributes the Program. + + "Licensed Patents " mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone or + when combined with the Program. + + "Program" means the Contributions distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement, + including all Contributors. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free copyright license to + reproduce, prepare derivative works of, publicly display, publicly + perform, distribute and sublicense the Contribution of such + Contributor, if any, and such derivative works, in source code and + object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free patent license under + Licensed Patents to make, use, sell, offer to sell, import and + otherwise transfer the Contribution of such Contributor, if any, in + source code and object code form. This patent license shall apply to + the combination of the Contribution and the Program if, at the time + the Contribution is added by the Contributor, such addition of the + Contribution causes such combination to be covered by the Licensed + Patents. The patent license shall not apply to any other combinations + which include the Contribution. No hardware per se is licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. Each + Contributor disclaims any liability to Recipient for claims brought by + any other entity based on infringement of intellectual property rights + or otherwise. As a condition to exercising the rights and licenses + granted hereunder, each Recipient hereby assumes sole responsibility + to secure any other intellectual property rights needed, if any. For + example, if a third party patent license is required to allow Recipient + to distribute the Program, it is Recipient's responsibility to acquire + that license before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + + 3. REQUIREMENTS + + A Contributor may choose to distribute the Program in object code form + under its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + + i) effectively disclaims on behalf of all Contributors all warranties + and conditions, express and implied, including warranties or + conditions of title and non-infringement, and implied warranties + or conditions of merchantability and fitness for a particular + purpose; + + ii) effectively excludes on behalf of all Contributors all liability + for damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + + iii) states that any provisions which differ from this Agreement are + offered by that Contributor alone and not by any other party; and + + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a + reasonable manner on or through a medium customarily used for + software exchange. + + When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the + Program. + + Contributors may not remove or alter any copyright notices contained + within the Program. + + Each Contributor must identify itself as the originator of its + Contribution, if any, in a manner that reasonably allows subsequent + Recipients to identify the originator of the Contribution. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product offering + should do so in a manner which does not create potential liability for + other Contributors. Therefore, if a Contributor includes the Program in + a commercial product offering, such Contributor ("Commercial Contributor") + hereby agrees to defend and indemnify every other Contributor + ("Indemnified Contributor") against any losses, damages and costs + (collectively "Losses") arising from claims, lawsuits and other legal + actions brought by a third party against the Indemnified Contributor to + the extent caused by the acts or omissions of such Commercial Contributor + in connection with its distribution of the Program in a commercial + product offering. The obligations in this section do not apply to any + claims or Losses relating to any actual or alleged intellectual property + infringement. In order to qualify, an Indemnified Contributor must: + a) promptly notify the Commercial Contributor in writing of such claim, + and b) allow the Commercial Contributor to control, and cooperate with + the Commercial Contributor in, the defense and any related settlement + negotiations. The Indemnified Contributor may participate in any such + claim at its own expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance claims, + or offers warranties related to Product X, those performance claims and + warranties are such Commercial Contributor's responsibility alone. Under + this section, the Commercial Contributor would have to defend claims + against the other Contributors related to those performance claims and + warranties, and if a court requires any other Contributor to pay any + damages as a result, the Commercial Contributor must pay those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED + ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER + EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR + CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A + PARTICULAR PURPOSE. Each Recipient is solely responsible for determining + the appropriateness of using and distributing the Program and assumes all + risks associated with its exercise of rights under this Agreement , + including but not limited to the risks and costs of program errors, + compliance with applicable laws, damage to or loss of data, programs or + equipment, and unavailability or interruption of operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR + ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING + WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR + DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED + HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further action + by the parties hereto, such provision shall be reformed to the minimum + extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity (including + a cross-claim or counterclaim in a lawsuit) alleging that the Program + itself (excluding combinations of the Program with other software or + hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it fails + to comply with any of the material terms or conditions of this Agreement + and does not cure such failure in a reasonable period of time after + becoming aware of such noncompliance. If all Recipient's rights under + this Agreement terminate, Recipient agrees to cease use and distribution + of the Program as soon as reasonably practicable. However, Recipient's + obligations under this Agreement and any licenses granted by Recipient + relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and may + only be modified in the following manner. The Agreement Steward reserves + the right to publish new versions (including revisions) of this Agreement + from time to time. No one other than the Agreement Steward has the right + to modify this Agreement. The Eclipse Foundation is the initial Agreement + Steward. The Eclipse Foundation may assign the responsibility to serve as + the Agreement Steward to a suitable separate entity. Each new version of + the Agreement will be given a distinguishing version number. The Program + (including Contributions) may always be distributed subject to the version + of the Agreement under which it was received. In addition, after a new + version of the Agreement is published, Contributor may elect to distribute + the Program (including its Contributions) under the new version. Except as + expressly stated in Sections 2(a) and 2(b) above, Recipient receives no + rights or licenses to the intellectual property of any Contributor under + this Agreement, whether expressly, by implication, estoppel or otherwise. + All rights in the Program not expressly granted under this Agreement + are reserved. + + This Agreement is governed by the laws of the State of New York and the + intellectual property laws of the United States of America. No party to + this Agreement will bring a legal action under this Agreement more than + one year after the cause of action arose. Each party waives its rights to + a jury trial in any resulting litigation. + +Servlet Specification 2.5 API (servlet-api-2.5-6.1.14.jar), +Classfish Jasper API (jsp-api-2.1-6.1.14.jar), and +JSP2.1 Jasper implementation from Glassfish (jsp-2.1-6.1.14.jar) + + COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 + + 1. Definitions. + + 1.1. Contributor means each individual or entity that creates or + contributes to the creation of Modifications. + + 1.2. Contributor Version means the combination of the Original Software, + prior Modifications used by a Contributor (if any), and the Modifications + made by that particular Contributor. + + 1.3. Covered Software means (a) the Original Software, or + (b) Modifications, or (c) the combination of files containing Original + Software with files containing Modifications, in each case including + portions thereof. + + 1.4. Executable means the Covered Software in any form other than Source + Code. + + 1.5. Initial Developer means the individual or entity that first makes + Original Software available under this License. + + 1.6. Larger Work means a work which combines Covered Software or portions + thereof with code not governed by the terms of this License. + + 1.7. License means this document. + + 1.8. Licensable means having the right to grant, to the maximum extent + possible, whether at the time of the initial grant or subsequently + acquired, any and all of the rights conveyed herein. + + 1.9. Modifications means the Source Code and Executable form of any of + the following: A. Any file that results from an addition to, deletion + from or modification of the contents of a file containing Original + Software or previous Modifications; B. Any new file that contains any + part of the Original Software or previous Modification; or C. Any new + file that is contributed or otherwise made available under the terms of + this License. + + 1.10. Original Software means the Source Code and Executable form of + computer software code that is originally released under this License. + + 1.11. Patent Claims means any patent claim(s), now owned or hereafter + acquired, including without limitation, method, process, and apparatus + claims, in any patent Licensable by grantor. + + 1.12. Source Code means (a) the common form of computer software code in + which modifications are made and (b) associated documentation included in + or with such code. + + 1.13. You (or Your) means an individual or a legal entity exercising + rights under, and complying with all of the terms of, this License. For + legal entities, You includes any entity which controls, is controlled by, + or is under common control with You. For purposes of this definition, + control means (a) the power, direct or indirect, to cause the direction + or management of such entity, whether by contract or otherwise, or + (b) ownership of more than fifty percent (50%) of the outstanding shares + or beneficial ownership of such entity. + + 2. License Grants. + + 2.1. The Initial Developer Grant. Conditioned upon Your compliance with + Section 3.1 below and subject to third party intellectual property + claims, the Initial Developer hereby grants You a world-wide, + royalty-free, non-exclusive license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by Initial Developer, to use, reproduce, modify, display, + perform, sublicense and distribute the Original Software (or portions + thereof), with or without Modifications, and/or as part of a Larger + Work; and + + (b) under Patent Claims infringed by the making, using or selling of + Original Software, to make, have made, use, practice, sell, and offer + for sale, and/or otherwise dispose of the Original Software (or + portions thereof); + + (c) The licenses granted in Sections 2.1(a) and (b) are effective on the + date Initial Developer first distributes or otherwise makes the + Original Software available to a third party under the terms of + this License; + + (d) Notwithstanding Section 2.1(b) above, no patent license is granted: + (1) for code that You delete from the Original Software, or (2) for + infringements caused by: (i) the modification of the Original + Software, or (ii) the combination of the Original Software with other + software or devices. + + 2.2. Contributor Grant. Conditioned upon Your compliance with Section 3.1 + below and subject to third party intellectual property claims, each + Contributor hereby grants You a world-wide, royalty-free, non-exclusive + license: + + (a) under intellectual property rights (other than patent or trademark) + Licensable by Contributor to use, reproduce, modify, display, perform, + sublicense and distribute the Modifications created by such + Contributor (or portions thereof), either on an unmodified basis, + with other Modifications, as Covered Software and/or as part of a + Larger Work; and + + (b) under Patent Claims infringed by the making, using, or selling of + Modifications made by that Contributor either alone and/or in + combination with its Contributor Version (or portions of such + combination), to make, use, sell, offer for sale, have made, and/or + otherwise dispose of: (1) Modifications made by that Contributor (or + portions thereof); and (2) the combination of Modifications made by + that Contributor with its Contributor Version (or portions of such + combination). + + (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on + the date Contributor first distributes or otherwise makes the + Modifications available to a third party. + + (d) Notwithstanding Section 2.2(b) above, no patent license is granted: + (1) for any code that Contributor has deleted from the Contributor + Version; (2) for infringements caused by: (i) third party + modifications of Contributor Version, or (ii) the combination of + Modifications made by that Contributor with other software (except + as part of the Contributor Version) or other devices; or (3) under + Patent Claims infringed by Covered Software in the absence of + Modifications made by that Contributor. + + 3. Distribution Obligations. + + 3.1. Availability of Source Code. Any Covered Software that You distribute + or otherwise make available in Executable form must also be made available + in Source Code form and that Source Code form must be distributed only + under the terms of this License. You must include a copy of this License + with every copy of the Source Code form of the Covered Software You + distribute or otherwise make available. You must inform recipients of any + such Covered Software in Executable form as to how they can obtain such + Covered Software in Source Code form in a reasonable manner on or through + a medium customarily used for software exchange. + + 3.2. Modifications. The Modifications that You create or to which You + contribute are governed by the terms of this License. You represent that + You believe Your Modifications are Your original creation(s) and/or You + have sufficient rights to grant the rights conveyed by this License. + + 3.3. Required Notices. You must include a notice in each of Your + Modifications that identifies You as the Contributor of the Modification. + You may not remove or alter any copyright, patent or trademark notices + contained within the Covered Software, or any notices of licensing or any + descriptive text giving attribution to any Contributor or the Initial + Developer. + + 3.4. Application of Additional Terms. You may not offer or impose any + terms on any Covered Software in Source Code form that alters or restricts + the applicable version of this License or the recipients rights hereunder. + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, you may do so only on Your own behalf, and not on + behalf of the Initial Developer or any Contributor. You must make it + absolutely clear that any such warranty, support, indemnity or liability + obligation is offered by You alone, and You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, support, + indemnity or liability terms You offer. + + 3.5. Distribution of Executable Versions. You may distribute the + Executable form of the Covered Software under the terms of this License or + under the terms of a license of Your choice, which may contain terms + different from this License, provided that You are in compliance with the + terms of this License and that the license for the Executable form does + not attempt to limit or alter the recipients rights in the Source Code + form from the rights set forth in this License. If You distribute the + Covered Software in Executable form under a different license, You must + make it absolutely clear that any terms which differ from this License + are offered by You alone, not by the Initial Developer or Contributor. + You hereby agree to indemnify the Initial Developer and every Contributor + for any liability incurred by the Initial Developer or such Contributor as + a result of any such terms You offer. + + 3.6. Larger Works. You may create a Larger Work by combining Covered + Software with other code not governed by the terms of this License and + distribute the Larger Work as a single product. In such a case, You must + make sure the requirements of this License are fulfilled for the Covered + Software. + + 4. Versions of the License. + + 4.1. New Versions. Sun Microsystems, Inc. is the initial license steward + and may publish revised and/or new versions of this License from time to + time. Each version will be given a distinguishing version number. Except + as provided in Section 4.3, no one other than the license steward has the + right to modify this License. + + 4.2. Effect of New Versions. You may always continue to use, distribute + or otherwise make the Covered Software available under the terms of the + version of the License under which You originally received the Covered + Software. If the Initial Developer includes a notice in the Original + Software prohibiting it from being distributed or otherwise made + available under any subsequent version of the License, You must + distribute and make the Covered Software available under the terms of + the version of the License under which You originally received the + Covered Software. Otherwise, You may also choose to use, distribute or + otherwise make the Covered Software available under the terms of any + subsequent version of the License published by the license steward. + + 4.3. Modified Versions. When You are an Initial Developer and You want + to create a new license for Your Original Software, You may create and + use a modified version of this License if You: (a) rename the license and + remove any references to the name of the license steward (except to note + that the license differs from this License); and (b) otherwise make it + clear that the license contains terms which differ from this License. + + 5. DISCLAIMER OF WARRANTY. COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE + ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR + IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED + SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE + OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF + THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE + DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER + CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR + CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF + THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER + EXCEPT UNDER THIS DISCLAIMER. + + 6. TERMINATION. + + 6.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. Provisions + which, by their nature, must remain in effect beyond the termination of + this License shall survive. + + 6.2. If You assert a patent infringement claim (excluding declaratory + judgment actions) against Initial Developer or a Contributor (the Initial + Developer or Contributor against whom You assert such claim is referred + to as Participant) alleging that the Participant Software (meaning the + Contributor Version where the Participant is a Contributor or the + Original Software where the Participant is the Initial Developer) + directly or indirectly infringes any patent, then any and all rights + granted directly or indirectly to You by such Participant, the Initial + Developer (if the Initial Developer is not the Participant) and all + Contributors under Sections 2.1 and/or 2.2 of this License shall, upon + 60 days notice from Participant terminate prospectively and automatically + at the expiration of such 60 day notice period, unless if within such + 60 day period You withdraw Your claim with respect to the Participant + Software against such Participant either unilaterally or pursuant to a + written agreement with Participant. + + 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end + user licenses that have been validly granted by You or any distributor + hereunder prior to termination (excluding licenses granted to You by any + distributor) shall survive termination. + + 7. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL + THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL + YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF + COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY + PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF + ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOST PROFITS, + LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY + AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE + BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTYS NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION + OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION + AND LIMITATION MAY NOT APPLY TO YOU. + + 8. U.S. GOVERNMENT END USERS. The Covered Software is a commercial item, + as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of + commercial computer software (as that term is defined at 48 C.F.R. + 252.227-7014(a)(1)) and commercial computer software documentation as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. + 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. + Government End Users acquire Covered Software with only those rights set + forth herein. This U.S. Government Rights clause is in lieu of, and + supersedes, any other FAR, DFAR, or other clause or provision that + addresses Government rights in computer software under this License. + + 9. MISCELLANEOUS. This License represents the complete agreement + concerning subject matter hereof. If any provision of this License is + held to be unenforceable, such provision shall be reformed only to the + extent necessary to make it enforceable. This License shall be governed + by the law of the jurisdiction specified in a notice contained within + the Original Software (except to the extent applicable law, if any, + provides otherwise), excluding such jurisdictions conflict-of-law + provisions. Any litigation relating to this License shall be subject to + the jurisdiction of the courts located in the jurisdiction and venue + specified in a notice contained within the Original Software, with the + losing party responsible for costs, including, without limitation, court + costs and reasonable attorneys fees and expenses. The application of the + United Nations Convention on Contracts for the International Sale of + Goods is expressly excluded. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall + not apply to this License. You agree that You alone are responsible for + compliance with the United States export administration regulations (and + the export control laws and regulation of any other countries) when You + use, distribute or otherwise make available any Covered Software. + + 10. RESPONSIBILITY FOR CLAIMS. As between Initial Developer and the + Contributors, each party is responsible for claims and damages arising, + directly or indirectly, out of its utilization of rights under this + License and You agree to work with Initial Developer and Contributors + to distribute such responsibility on an equitable basis. Nothing herein + is intended or shall be deemed to constitute any admission of liability. + + NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION + LICENSE (CDDL) The code released under the CDDL shall be governed by the + laws of the State of California (excluding conflict-of-law provisions). + Any litigation relating to this License shall be subject to the + jurisdiction of the Federal Courts of the Northern District of California + and the state courts of the State of California, with venue lying in + Santa Clara County, California. + +juniversalchardet library (juniversalchardet) + + MOZILLA PUBLIC LICENSE + Version 1.1 + + --------------- + + 1. Definitions. + + 1.0.1. "Commercial Use" means distribution or otherwise making the + Covered Code available to a third party. + + 1.1. "Contributor" means each entity that creates or contributes to + the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Code, prior Modifications used by a Contributor, and the Modifications + made by that particular Contributor. + + 1.3. "Covered Code" means the Original Code or Modifications or the + combination of the Original Code and Modifications, in each case + including portions thereof. + + 1.4. "Electronic Distribution Mechanism" means a mechanism generally + accepted in the software development community for the electronic + transfer of data. + + 1.5. "Executable" means Covered Code in any form other than Source + Code. + + 1.6. "Initial Developer" means the individual or entity identified + as the Initial Developer in the Source Code notice required by Exhibit + A. + + 1.7. "Larger Work" means a work which combines Covered Code or + portions thereof with code not governed by the terms of this License. + + 1.8. "License" means this document. + + 1.8.1. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed herein. + + 1.9. "Modifications" means any addition to or deletion from the + substance or structure of either the Original Code or any previous + Modifications. When Covered Code is released as a series of files, a + Modification is: + A. Any addition to or deletion from the contents of a file + containing Original Code or previous Modifications. + + B. Any new file that contains any part of the Original Code or + previous Modifications. + + 1.10. "Original Code" means Source Code of computer software code + which is described in the Source Code notice required by Exhibit A as + Original Code, and which, at the time of its release under this + License is not already Covered Code governed by this License. + + 1.10.1. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, process, + and apparatus claims, in any patent Licensable by grantor. + + 1.11. "Source Code" means the preferred form of the Covered Code for + making modifications to it, including all modules it contains, plus + any associated interface definition files, scripts used to control + compilation and installation of an Executable, or source code + differential comparisons against either the Original Code or another + well known, available Covered Code of the Contributor's choice. The + Source Code can be in a compressed or archival form, provided the + appropriate decompression or de-archiving software is widely available + for no charge. + + 1.12. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms of, this + License or a future version of this License issued under Section 6.1. + For legal entities, "You" includes any entity which controls, is + controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, + to cause the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty percent + (50%) of the outstanding shares or beneficial ownership of such + entity. + + 2. Source Code License. + + 2.1. The Initial Developer Grant. + The Initial Developer hereby grants You a world-wide, royalty-free, + non-exclusive license, subject to third party intellectual property + claims: + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer to use, reproduce, + modify, display, perform, sublicense and distribute the Original + Code (or portions thereof) with or without Modifications, and/or + as part of a Larger Work; and + + (b) under Patents Claims infringed by the making, using or + selling of Original Code, to make, have made, use, practice, + sell, and offer for sale, and/or otherwise dispose of the + Original Code (or portions thereof). + + (c) the licenses granted in this Section 2.1(a) and (b) are + effective on the date Initial Developer first distributes + Original Code under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: 1) for code that You delete from the Original Code; 2) + separate from the Original Code; or 3) for infringements caused + by: i) the modification of the Original Code or ii) the + combination of the Original Code with other software or devices. + + 2.2. Contributor Grant. + Subject to third party intellectual property claims, each Contributor + hereby grants You a world-wide, royalty-free, non-exclusive license + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor, to use, reproduce, modify, + display, perform, sublicense and distribute the Modifications + created by such Contributor (or portions thereof) either on an + unmodified basis, with other Modifications, as Covered Code + and/or as part of a Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either alone + and/or in combination with its Contributor Version (or portions + of such combination), to make, use, sell, offer for sale, have + made, and/or otherwise dispose of: 1) Modifications made by that + Contributor (or portions thereof); and 2) the combination of + Modifications made by that Contributor with its Contributor + Version (or portions of such combination). + + (c) the licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first makes Commercial Use of + the Covered Code. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: 1) for any code that Contributor has deleted from the + Contributor Version; 2) separate from the Contributor Version; + 3) for infringements caused by: i) third party modifications of + Contributor Version or ii) the combination of Modifications made + by that Contributor with other software (except as part of the + Contributor Version) or other devices; or 4) under Patent Claims + infringed by Covered Code in the absence of Modifications made by + that Contributor. + + 3. Distribution Obligations. + + 3.1. Application of License. + The Modifications which You create or to which You contribute are + governed by the terms of this License, including without limitation + Section 2.2. The Source Code version of Covered Code may be + distributed only under the terms of this License or a future version + of this License released under Section 6.1, and You must include a + copy of this License with every copy of the Source Code You + distribute. You may not offer or impose any terms on any Source Code + version that alters or restricts the applicable version of this + License or the recipients' rights hereunder. However, You may include + an additional document offering the additional rights described in + Section 3.5. + + 3.2. Availability of Source Code. + Any Modification which You create or to which You contribute must be + made available in Source Code form under the terms of this License + either on the same media as an Executable version or via an accepted + Electronic Distribution Mechanism to anyone to whom you made an + Executable version available; and if made available via Electronic + Distribution Mechanism, must remain available for at least twelve (12) + months after the date it initially became available, or at least six + (6) months after a subsequent version of that particular Modification + has been made available to such recipients. You are responsible for + ensuring that the Source Code version remains available even if the + Electronic Distribution Mechanism is maintained by a third party. + + 3.3. Description of Modifications. + You must cause all Covered Code to which You contribute to contain a + file documenting the changes You made to create that Covered Code and + the date of any change. You must include a prominent statement that + the Modification is derived, directly or indirectly, from Original + Code provided by the Initial Developer and including the name of the + Initial Developer in (a) the Source Code, and (b) in any notice in an + Executable version or related documentation in which You describe the + origin or ownership of the Covered Code. + + 3.4. Intellectual Property Matters + (a) Third Party Claims. + If Contributor has knowledge that a license under a third party's + intellectual property rights is required to exercise the rights + granted by such Contributor under Sections 2.1 or 2.2, + Contributor must include a text file with the Source Code + distribution titled "LEGAL" which describes the claim and the + party making the claim in sufficient detail that a recipient will + know whom to contact. If Contributor obtains such knowledge after + the Modification is made available as described in Section 3.2, + Contributor shall promptly modify the LEGAL file in all copies + Contributor makes available thereafter and shall take other steps + (such as notifying appropriate mailing lists or newsgroups) + reasonably calculated to inform those who received the Covered + Code that new knowledge has been obtained. + + (b) Contributor APIs. + If Contributor's Modifications include an application programming + interface and Contributor has knowledge of patent licenses which + are reasonably necessary to implement that API, Contributor must + also include this information in the LEGAL file. + + (c) Representations. + Contributor represents that, except as disclosed pursuant to + Section 3.4(a) above, Contributor believes that Contributor's + Modifications are Contributor's original creation(s) and/or + Contributor has sufficient rights to grant the rights conveyed by + this License. + + 3.5. Required Notices. + You must duplicate the notice in Exhibit A in each file of the Source + Code. If it is not possible to put such notice in a particular Source + Code file due to its structure, then You must include such notice in a + location (such as a relevant directory) where a user would be likely + to look for such a notice. If You created one or more Modification(s) + You may add your name as a Contributor to the notice described in + Exhibit A. You must also duplicate this License in any documentation + for the Source Code where You describe recipients' rights or ownership + rights relating to Covered Code. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Code. However, You + may do so only on Your own behalf, and not on behalf of the Initial + Developer or any Contributor. You must make it absolutely clear than + any such warranty, support, indemnity or liability obligation is + offered by You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, + support, indemnity or liability terms You offer. + + 3.6. Distribution of Executable Versions. + You may distribute Covered Code in Executable form only if the + requirements of Section 3.1-3.5 have been met for that Covered Code, + and if You include a notice stating that the Source Code version of + the Covered Code is available under the terms of this License, + including a description of how and where You have fulfilled the + obligations of Section 3.2. The notice must be conspicuously included + in any notice in an Executable version, related documentation or + collateral in which You describe recipients' rights relating to the + Covered Code. You may distribute the Executable version of Covered + Code or ownership rights under a license of Your choice, which may + contain terms different from this License, provided that You are in + compliance with the terms of this License and that the license for the + Executable version does not attempt to limit or alter the recipient's + rights in the Source Code version from the rights set forth in this + License. If You distribute the Executable version under a different + license You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial + Developer or any Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by + the Initial Developer or such Contributor as a result of any such + terms You offer. + + 3.7. Larger Works. + You may create a Larger Work by combining Covered Code with other code + not governed by the terms of this License and distribute the Larger + Work as a single product. In such a case, You must make sure the + requirements of this License are fulfilled for the Covered Code. + + 4. Inability to Comply Due to Statute or Regulation. + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Code due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description + must be included in the LEGAL file described in Section 3.4 and must + be included with all distributions of the Source Code. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + + 5. Application of this License. + + This License applies to code to which the Initial Developer has + attached the notice in Exhibit A and to related Covered Code. + + 6. Versions of the License. + + 6.1. New Versions. + Netscape Communications Corporation ("Netscape") may publish revised + and/or new versions of the License from time to time. Each version + will be given a distinguishing version number. + + 6.2. Effect of New Versions. + Once Covered Code has been published under a particular version of the + License, You may always continue to use it under the terms of that + version. You may also choose to use such Covered Code under the terms + of any subsequent version of the License published by Netscape. No one + other than Netscape has the right to modify the terms applicable to + Covered Code created under this License. + + 6.3. Derivative Works. + If You create or use a modified version of this License (which you may + only do in order to apply it to code which is not already Covered Code + governed by this License), You must (a) rename Your license so that + the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", + "MPL", "NPL" or any confusingly similar phrase do not appear in your + license (except to note that your license differs from this License) + and (b) otherwise make it clear that Your version of the license + contains terms which differ from the Mozilla Public License and + Netscape Public License. (Filling in the name of the Initial + Developer, Original Code or Contributor in the notice described in + Exhibit A shall not of themselves be deemed to be modifications of + this License.) + + 7. DISCLAIMER OF WARRANTY. + + COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, + WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF + DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. + THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE + IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE + COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + + 8. TERMINATION. + + 8.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. All + sublicenses to the Covered Code which are properly granted shall + survive any termination of this License. Provisions which, by their + nature, must remain in effect beyond the termination of this License + shall survive. + + 8.2. If You initiate litigation by asserting a patent infringement + claim (excluding declatory judgment actions) against Initial Developer + or a Contributor (the Initial Developer or Contributor against whom + You file such action is referred to as "Participant") alleging that: + + (a) such Participant's Contributor Version directly or indirectly + infringes any patent, then any and all rights granted by such + Participant to You under Sections 2.1 and/or 2.2 of this License + shall, upon 60 days notice from Participant terminate prospectively, + unless if within 60 days after receipt of notice You either: (i) + agree in writing to pay Participant a mutually agreeable reasonable + royalty for Your past and future use of Modifications made by such + Participant, or (ii) withdraw Your litigation claim with respect to + the Contributor Version against such Participant. If within 60 days + of notice, a reasonable royalty and payment arrangement are not + mutually agreed upon in writing by the parties or the litigation claim + is not withdrawn, the rights granted by Participant to You under + Sections 2.1 and/or 2.2 automatically terminate at the expiration of + the 60 day notice period specified above. + + (b) any software, hardware, or device, other than such Participant's + Contributor Version, directly or indirectly infringes any patent, then + any rights granted to You by such Participant under Sections 2.1(b) + and 2.2(b) are revoked effective as of the date You first made, used, + sold, distributed, or had made, Modifications made by that + Participant. + + 8.3. If You assert a patent infringement claim against Participant + alleging that such Participant's Contributor Version directly or + indirectly infringes any patent where such claim is resolved (such as + by license or settlement) prior to the initiation of patent + infringement litigation, then the reasonable value of the licenses + granted by such Participant under Sections 2.1 or 2.2 shall be taken + into account in determining the amount or value of any payment or + license. + + 8.4. In the event of termination under Sections 8.1 or 8.2 above, + all end user license agreements (excluding distributors and resellers) + which have been validly granted by You or any distributor hereunder + prior to termination shall survive termination. + + 9. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL + DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, + OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR + ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY + CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, + WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO + THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + + 10. U.S. GOVERNMENT END USERS. + + The Covered Code is a "commercial item," as that term is defined in + 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer + software" and "commercial computer software documentation," as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 + C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), + all U.S. Government End Users acquire Covered Code with only those + rights set forth herein. + + 11. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed by + California law provisions (except to the extent applicable law, if + any, provides otherwise), excluding its conflict-of-law provisions. + With respect to disputes in which at least one party is a citizen of, + or an entity chartered or registered to do business in the United + States of America, any litigation relating to this License shall be + subject to the jurisdiction of the Federal Courts of the Northern + District of California, with venue lying in Santa Clara County, + California, with the losing party responsible for costs, including + without limitation, court costs and reasonable attorneys' fees and + expenses. The application of the United Nations Convention on + Contracts for the International Sale of Goods is expressly excluded. + Any law or regulation which provides that the language of a contract + shall be construed against the drafter shall not apply to this + License. + + 12. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or indirectly, + out of its utilization of rights under this License and You agree to + work with Initial Developer and Contributors to distribute such + responsibility on an equitable basis. Nothing herein is intended or + shall be deemed to constitute any admission of liability. + + 13. MULTIPLE-LICENSED CODE. + + Initial Developer may designate portions of the Covered Code as + "Multiple-Licensed". "Multiple-Licensed" means that the Initial + Developer permits you to utilize portions of the Covered Code under + Your choice of the NPL or the alternative licenses, if any, specified + by the Initial Developer in the file described in Exhibit A. + + EXHIBIT A -Mozilla Public License. + + ``The contents of this file are subject to the Mozilla Public License + Version 1.1 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + http://www.mozilla.org/MPL/ + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + License for the specific language governing rights and limitations + under the License. + + The Original Code is ______________________________________. + + The Initial Developer of the Original Code is ________________________. + Portions created by ______________________ are Copyright (C) ______ + _______________________. All Rights Reserved. + + Contributor(s): ______________________________________. + + Alternatively, the contents of this file may be used under the terms + of the _____ license (the "[___] License"), in which case the + provisions of [______] License are applicable instead of those + above. If you wish to allow use of your version of this file only + under the terms of the [____] License and not to allow others to use + your version of this file under the MPL, indicate your decision by + deleting the provisions above and replace them with the notice and + other provisions required by the [___] License. If you do not delete + the provisions above, a recipient may use your version of this file + under either the MPL or the [___] License." + + [NOTE: The text of this Exhibit A may differ slightly from the text of + the notices in the Source Code files of the Original Code. You should + use the text of this Exhibit A rather than the text found in the + Original Code Source Code for Your Modifications.] diff --git a/jackrabbit-standalone/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-standalone/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..01495733fbf --- /dev/null +++ b/jackrabbit-standalone/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,35 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). + +This product includes software from the following contributions: + +Original BZip2 classes contributed by Keiron Liddle +, Aftex Software to the Apache Ant project + +Original Tar classes from contributors of the Apache Ant project + +Original Zip classes from contributors of the Apache Ant project + +Original CPIO classes contributed by Markus Kuss and the jRPM project +(jrpm.sourceforge.net) + +Portions of Derby were originally developed by International Business +Machines Corporation and are licensed to the Apache Software Foundation +under the "Software Grant and Corporate Contribution License Agreement", +informally known as the "Derby CLA". The following copyright notice(s) +were affixed to portions of the code with which this file is now or was +at one time distributed and are placed here unaltered. + + (C) Copyright 1997,2004 International Business Machines Corporation. + All rights reserved. + + (C) Copyright IBM Corp. 2003. + +The JDBC apis for small devices and JDBC3 (under java/stubs/jsr169 and +java/stubs/jdbc3) were produced by trimming sources supplied by the +Apache Harmony project. The following notice covers the Harmony sources: + + Portions of Harmony were originally developed by + Intel Corporation and are licensed to the Apache Software + Foundation under the "Software Grant and Corporate Contribution + License Agreement", informally known as the "Intel Harmony CLA". diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/Main.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/Main.java new file mode 100644 index 00000000000..5e9bea8c079 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/Main.java @@ -0,0 +1,303 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.chain.Context; +import org.apache.commons.chain.impl.ContextBase; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.GnuParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.core.RepositoryCopier; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.servlet.jackrabbit.JackrabbitRepositoryServlet; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; +import org.apache.jackrabbit.standalone.cli.JcrClient; +import org.eclipse.jetty.server.NCSARequestLog; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.RequestLogHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.webapp.WebAppContext; + +/** + * + */ +public class Main { + + /** + * @param args + */ + public static void main(String[] args) throws Exception { + new Main(args).run(); + } + + private final Options options = new Options(); + + private final CommandLine command; + + private final RequestLogHandler accessLog = new RequestLogHandler(); + + private final WebAppContext webapp = new WebAppContext(); + + private final Server server = new Server(); + + private final ServerConnector connector = new ServerConnector(server); + + private Main(String[] args) throws ParseException { + options.addOption("?", "help", false, "print this message"); + options.addOption("n", "notice", false, "print copyright notices"); + options.addOption("l", "license", false, "print license information"); + options.addOption( + "b", "backup", false, "create a backup of the repository"); + options.addOption( + "i", "cli", true, "command line access to a remote repository"); + + options.addOption("q", "quiet", false, "disable console output"); + options.addOption("d", "debug", false, "enable debug logging"); + + options.addOption("h", "host", true, "IP address of the HTTP server"); + options.addOption("p", "port", true, "TCP port of the HTTP server (8080)"); + options.addOption("f", "file", true, "location of this jar file"); + options.addOption("r", "repo", true, "repository directory (jackrabbit)"); + options.addOption("c", "conf", true, "repository configuration file"); + options.addOption( + "R", "backup-repo", true, + "backup repository directory (jackrabbit-backupN)"); + options.addOption( + "C", "backup-conf", true, + "backup repository configuration file"); + + command = new GnuParser().parse(options, args); + } + + public void run() throws Exception { + String defaultFile = "jackrabbit-standalone.jar"; + URL location = + Main.class.getProtectionDomain().getCodeSource().getLocation(); + if (location != null && "file".equals(location.getProtocol())) { + File file = new File(location.getPath()); + if (file.isFile()) { + defaultFile = location.getPath(); + } + } + File file = new File(command.getOptionValue("file", defaultFile)); + + if (command.hasOption("help")) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("java -jar " + file.getName(), options, true); + } else if (command.hasOption("notice")) { + copyToOutput("/META-INF/NOTICE.txt"); + } else if (command.hasOption("license")) { + copyToOutput("/META-INF/LICENSE.txt"); + } else if (command.hasOption("cli")) { + System.setProperty("logback.configurationFile", "logback-cli.xml"); + + String uri = command.getOptionValue("cli"); + Repository repository = JcrUtils.getRepository(uri); + + Context context = new ContextBase(); + CommandHelper.setRepository(context, repository, uri); + try { + Session session = repository.login(); + CommandHelper.setSession(context, session); + CommandHelper.setCurrentNode(context, session.getRootNode()); + } catch (RepositoryException ignore) { + // anonymous login not possible + } + + new JcrClient(context).runInteractive(); + + try { + CommandHelper.getSession(context).logout(); + } catch (CommandException ignore) { + // already logged out + } + } else { + message("Welcome to Apache Jackrabbit!"); + message("-------------------------------"); + + File repository = + new File(command.getOptionValue("repo", "jackrabbit")); + message("Using repository directory " + repository); + repository.mkdirs(); + File tmp = new File(repository, "tmp"); + tmp.mkdir(); + File log = new File(repository, "log"); + log.mkdir(); + + message("Writing log messages to " + log); + prepareServerLog(log); + + if (command.hasOption("backup")) { + backup(repository); + } else { + message("Starting the server..."); + prepareWebapp(file, repository, tmp); + accessLog.setHandler(webapp); + prepareAccessLog(log); + server.setHandler(accessLog); + prepareConnector(); + server.addConnector(connector); + prepareShutdown(); + + try { + server.start(); + + String host = connector.getHost(); + if (host == null) { + host = "localhost"; + } + message("Apache Jackrabbit is now running at " + +"http://" + host + ":" + connector.getPort() + "/"); + } catch (Throwable t) { + System.err.println( + "Unable to start the server: " + t.getMessage()); + System.exit(1); + } + } + } + } + + private void backup(File sourceDir) throws Exception { + RepositoryConfig source; + if (command.hasOption("conf")) { + source = RepositoryConfig.create( + new File(command.getOptionValue("conf")), sourceDir); + } else { + source = RepositoryConfig.create(sourceDir); + } + + File targetDir; + if (command.hasOption("backup-repo")) { + targetDir = new File(command.getOptionValue("backup-repo")); + } else { + int i = 1; + do { + targetDir = new File("jackrabbit-backup" + i++); + } while (targetDir.exists()); + } + + RepositoryConfig target; + if (command.hasOption("backup-conf")) { + target = RepositoryConfig.install( + new File(command.getOptionValue("backup-conf")), targetDir); + } else { + target = RepositoryConfig.install(targetDir); + } + + message("Creating a repository copy in " + targetDir); + RepositoryCopier.copy(source, target); + message("The repository has been successfully copied."); + } + + private void prepareServerLog(File log) + throws IOException { + System.setProperty( + "jackrabbit.log", new File(log, "jackrabbit.log").getPath()); + System.setProperty( + "jetty.log", new File(log, "jetty.log").getPath()); + + if (command.hasOption("debug")) { + System.setProperty("log.level", "DEBUG"); + } else { + System.setProperty("log.level", "INFO"); + } + + System.setProperty( + "derby.stream.error.file", + new File(log, "derby.log").getPath()); + } + + private void prepareAccessLog(File log) { + NCSARequestLog ncsa = new NCSARequestLog( + new File(log, "access.log.yyyy_mm_dd").getPath()); + ncsa.setFilenameDateFormat("yyyy-MM-dd"); + accessLog.setRequestLog(ncsa); + } + + private void prepareWebapp(File file, File repository, File tmp) { + webapp.setContextPath("/"); + webapp.setWar(file.getPath()); + webapp.setExtractWAR(false); + webapp.setTempDirectory(tmp); + + ServletHolder servlet = + new ServletHolder(JackrabbitRepositoryServlet.class); + servlet.setInitOrder(1); + servlet.setInitParameter("repository.home", repository.getPath()); + String conf = command.getOptionValue("conf"); + if (conf != null) { + servlet.setInitParameter("repository.config", conf); + } + webapp.addServlet(servlet, "/repository.properties"); + } + + private void prepareConnector() { + String port = command.getOptionValue("port", "8080"); + connector.setPort(Integer.parseInt(port)); + String host = command.getOptionValue("host"); + if (host != null) { + connector.setHost(host); + } + } + + private void prepareShutdown() { + Runtime.getRuntime().addShutdownHook(new Thread() { + public void run() { + try { + message("Shutting down the server..."); + server.stop(); + server.join(); + message("-------------------------------"); + message("Goodbye from Apache Jackrabbit!"); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + + private void message(String message) { + if (!command.hasOption("quiet")) { + System.out.println(message); + } + } + + private void copyToOutput(String resource) throws IOException { + InputStream stream = Main.class.getResourceAsStream(resource); + try { + IOUtils.copy(stream, System.out); + } finally { + stream.close(); + } + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/AbstractParameter.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/AbstractParameter.java new file mode 100644 index 00000000000..4e023ebec1c --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/AbstractParameter.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.standalone.cli; + +import java.util.ResourceBundle; + +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Command Line parameter superclass + */ +public abstract class AbstractParameter implements Cloneable { + + /** Resource bundle */ + protected ResourceBundle bundle = CommandHelper.getBundle(); + + /** name */ + private String name; + + /** long name */ + private String longName; + + /** description */ + private String description; + + /** command or a context attribute */ + private String contextKey; + + /** value */ + private String value; + + /** + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * @return the localized description + */ + public abstract String getLocalizedDescription(); + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param description + * The description to set + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * @param name + * the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value + * The value to set + */ + public void setValue(String value) { + this.value = value; + } + + /** + * @return the longName + */ + public String getLongName() { + return longName; + } + + /** + * @param longName + * The longName to set + */ + public void setLongName(String longName) { + this.longName = longName; + } + + /** + * @param param + * the AbstractParameter to clone + */ + protected void clone(AbstractParameter param) { + param.contextKey = this.contextKey; + param.description = this.description; + param.longName = this.longName; + param.name = this.name; + param.value = this.value; + } + + /** + * @return the commandAttribute. if the context key is unset it returns the + * parameter name + */ + public String getContextKey() { + if (contextKey == null) { + return this.name; + } + return contextKey; + } + + /** + * @param commandAttribute + * The commandAttribute to set + */ + public void setContextKey(String commandAttribute) { + this.contextKey = commandAttribute; + } + + /** + * @return true if this parameter is mandatory + */ + public abstract boolean isRequired(); + + /** + * @return argumentlocalized name + */ + public abstract String getLocalizedArgName(); + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Argument.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Argument.java new file mode 100644 index 00000000000..ab23574fcc8 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Argument.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.standalone.cli; + +/** + * Command line argument + */ +public class Argument extends Option { + /** position of the argument */ + private int position; + + /** + * @return the position + */ + public int getPosition() { + return position; + } + + /** + * sets the argument position + * @param position + * the position + */ + public void setPosition(int position) { + this.position = position; + } + + /** + * {@inheritDoc} + * @return a clone + */ + public Object clone() { + Argument arg = new Argument(); + arg.position = this.position; + this.clone(arg); + return arg; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandException.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandException.java new file mode 100644 index 00000000000..257e1290e36 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandException.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.standalone.cli; + +import java.text.MessageFormat; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +/** + * JCR command Exception + */ +public class CommandException extends Exception { + /** Resource bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** + * Exception arguments + */ + private Object[] arguments; + + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 3978426922931860275L; + + /** + * @param message + * the message + */ + public CommandException(String message) { + super(message); + } + + /** + * @param message + * the message + * @param arguments + * the arguments + */ + public CommandException(String message, Object[] arguments) { + super(message); + this.arguments = arguments; + } + + /** + * @param message + * the message + * @param cause + * the cause + */ + public CommandException(String message, Throwable cause) { + super(message, cause); + } + + /** + * @param message + * the message + * @param cause + * the cause + * @param arguments + * the arguments + */ + public CommandException(String message, Throwable cause, Object[] arguments) { + super(message, cause); + this.arguments = arguments; + } + + /** + * @return the localized message + */ + public String getLocalizedMessage() { + try { + if (this.arguments == null) { + return bundle.getString(this.getMessage()); + } else { + MessageFormat f = new MessageFormat(""); + f.applyPattern(bundle.getString(this.getMessage())); + return f.format(this.arguments); + } + } catch (MissingResourceException e) { + return this.getMessage(); + } + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandHelper.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandHelper.java new file mode 100644 index 00000000000..88b9c55d335 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandHelper.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli; + +import java.io.PrintWriter; +import java.util.Iterator; +import java.util.ResourceBundle; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.PropertyIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.chain.Context; +import org.apache.commons.collections.IteratorUtils; + +/** + * Utility class for getting and setting context attributes. + */ +public final class CommandHelper { + /** bundle */ + private static ResourceBundle bundle = ResourceBundle + .getBundle(CommandHelper.class.getPackage().getName() + ".resources"); + + /** Current node key */ + public static final String CURRENT_NODE_KEY = "jcr.current"; + + /** repository key */ + public static final String REPOSITORY_KEY = "jcr.repository"; + + /** session key */ + public static final String SESSION_KEY = "jcr.session"; + + /** output key */ + public static final String OUTPUT_KEY = "jcr.output"; + + /** address key */ + public static final String REPO_ADDRESS_KEY = "jcr.repo.address"; + + /** + * should never get called + */ + private CommandHelper() { + super(); + } + + /** + * Sets the current PrintWriter. + * @param ctx + * the Context + * @param out + * the PrintWriter + */ + public static void setOutput(Context ctx, PrintWriter out) { + if (out == null) { + ctx.remove(OUTPUT_KEY); + } else { + ctx.put(OUTPUT_KEY, out); + } + } + + /** + * Sets the current working Node. + * @param ctx + * the Context + * @param node + * the current working Node. + */ + public static void setCurrentNode(Context ctx, Node node) { + if (node == null) { + ctx.remove(CURRENT_NODE_KEY); + } else { + ctx.put(CURRENT_NODE_KEY, node); + } + } + + /** + * Sets the current working Repository + * @param ctx + * the Context + * @param repository + * the current working Repository + */ + public static void setRepository(Context ctx, Repository repository, String address) { + if (repository == null) { + ctx.remove(REPOSITORY_KEY); + ctx.remove(REPO_ADDRESS_KEY); + } else { + ctx.put(REPOSITORY_KEY, repository); + ctx.put(REPO_ADDRESS_KEY, address); + } + } + + /** + * Sets the current working Session + * @param ctx + * the Context + * @param session + * the current working Session + * @throws CommandException if there's an open working Session + */ + public static void setSession(Context ctx, Session session) throws CommandException { + if (session == null) { + ctx.remove(SESSION_KEY); + } else { + if (ctx.get(SESSION_KEY) != null) { + throw new CommandException("exception.already.logged.in"); + } + ctx.put(SESSION_KEY, session); + } + } + + /** + * Gets the current PrintWriter + * @param ctx + * the Context + * @return the current PrintWriter + */ + public static PrintWriter getOutput(Context ctx) { + PrintWriter out = (PrintWriter) ctx.get(OUTPUT_KEY); + if (out == null) { + out = new PrintWriter(System.out, true); + } + return out; + } + + /** + * Gets the current working Node + * @param ctx + * the Context + * @return the current working Node + * @throws CommandException + * if the current working Node can't be found. + */ + public static Node getCurrentNode(Context ctx) throws CommandException { + Node n = (Node) ctx.get(CURRENT_NODE_KEY); + if (n == null) { + throw new CommandException("exception.no.current.node"); + } + return n; + } + + /** + * Gets the current working Repository + * @param ctx + * the Context + * @return the current working Repository + * @throws CommandException + * if the current working Repository is unset. + */ + public static Repository getRepository(Context ctx) throws CommandException { + Repository r = (Repository) ctx.get(REPOSITORY_KEY); + if (r == null) { + throw new CommandException("exception.no.current.repository"); + } + return r; + } + + public static String getRepositoryAddress(Context ctx) throws CommandException { + String a = (String) ctx.get(REPO_ADDRESS_KEY); + if (a == null) { + throw new CommandException("exception.no.current.repository"); + } + return a; + } + + /** + * Gets the current working Session + * @param ctx + * the Context + * @return the current working Session + * @throws CommandException + * if the current working Session is unset. + */ + public static Session getSession(Context ctx) throws CommandException { + Session s = (Session) ctx.get(SESSION_KEY); + if (s == null) { + throw new CommandException("exception.no.current.session"); + } + return s; + } + + /** + * Gets the Node at the given path. + * @param ctx + * the Context + * @param path + * the path to the Node + * @return the Node at the given path + * @throws CommandException + * if the Node isn't found. + * @throws RepositoryException + * if the underlying repository throws a + * RepositoryException + */ + public static Node getNode(Context ctx, String path) + throws CommandException, RepositoryException { + try { + Item i = getItem(ctx, path); + if (!i.isNode()) { + throw new CommandException("exception.no.node.at", + new String[] { + path + }); + } + return (Node) i; + } catch (PathNotFoundException e) { + throw new CommandException("exception.no.node.at", new String[] { + path + }); + } + } + + /** + * Gets the Item at the given path.
        + * If the path is null it returns the current working Node. + * @param ctx + * the Context + * @param path + * the path to the Item + * @return the Item at the given path + * @throws CommandException + * if a Command internal error occurs. + * @throws PathNotFoundException + * if there's no Node at the given path. + * @throws RepositoryException + * if the underlying repository throws a + * RepositoryException + */ + public static Item getItem(Context ctx, String path) + throws CommandException, PathNotFoundException, RepositoryException { + Node current = getCurrentNode(ctx); + Item i = null; + + if (path == null) { + i = current; + } else if (path.equals("/")) { + i = current.getSession().getRootNode(); + } else if (path.startsWith("/")) { + i = current.getSession().getItem(path); + } else { + String newPath = current.getPath(); + // handle the root node + if (!newPath.endsWith("/")) { + newPath += "/"; + } + newPath += path; + i = current.getSession().getItem(newPath); + } + + return i; + } + + /** + * Checks Node existence. + * @param ctx + * the Context + * @param path + * the path to the Node + * @return true if the Node exists at the given path + * @throws CommandException + * if the current working Session is unset. + * @throws RepositoryException + * if the underlying repository throws a + * RepositoryException + */ + public static boolean hasNode(Context ctx, String path) + throws CommandException, RepositoryException { + Session s = getSession(ctx); + if (path.equals("/")) { + return true; + } else if (path.startsWith("/")) { + return s.getRootNode().hasNode(path.substring(1)); + } else { + Node current = (Node) ctx.get(CURRENT_NODE_KEY); + return current.hasNode(path); + } + } + + /** + * Gets the Node s under the given Node that + * match the given pattern. + * @param ctx + * the Context + * @param node + * the parent Node + * @param pattern + * the pattern + * @return an Iterator that contains the matching nodes + * @throws RepositoryException + * if the underlying repository throws a + * RepositoryException + */ + public static NodeIterator getNodes(Context ctx, Node node, String pattern) + throws RepositoryException { + if (pattern != null) { + return node.getNodes(pattern); + } else { + return node.getNodes(); + } + } + + /** + * Gets the Property s under the current working node for the + * given pattern + * @param ctx + * the Context + * @param node + * the parent Node + * @param pattern + * the pattern + * @return a PropertyIterator + * @throws RepositoryException + * if the underlying repository throws a + * RepositoryException + */ + public static PropertyIterator getProperties( + Context ctx, + Node node, + String pattern) throws RepositoryException { + if (pattern != null) { + return node.getProperties(pattern); + } else { + return node.getProperties(); + } + } + + /** + * @return the default ResourceBundle + */ + public static ResourceBundle getBundle() { + return bundle; + } + + /** + * Gets the Item s under the given Node that + * match the pattern + * @param ctx + * the Context + * @param node + * the parent Node + * @param pattern + * the pattern + * @return an Iterator with the Item s that + * match the given pattern. + * @throws RepositoryException + * if the underlying repository throws a + * RepositoryException + */ + public static Iterator getItems(Context ctx, Node node, String pattern) + throws RepositoryException { + return IteratorUtils.chainedIterator(getNodes(ctx, node, pattern), + getProperties(ctx, node, pattern)); + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandLine.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandLine.java new file mode 100644 index 00000000000..c1ccf57313d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandLine.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.standalone.cli; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.TreeMap; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.Predicate; +import org.apache.commons.collections.iterators.IteratorChain; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Command Line + */ +public class CommandLine implements Comparable, Cloneable { + /** Resource bundle */ + protected static ResourceBundle bundle = CommandHelper.getBundle(); + + /** Name */ + private String name; + + /** Description */ + private String description; + + /** Commons chain command implementation */ + private String impl; + + /** alias */ + private Collection alias = new ArrayList(); + + /** Options */ + private Map options = new TreeMap(); + + /** flags */ + private Map flags = new TreeMap(); + + /** arguments */ + private Map arguments = new TreeMap(); + + /** + * constructor + */ + public CommandLine() { + super(); + } + + /** + * @return required arguments + */ + public Collection getRequiredArguments() { + Predicate p = new Predicate() { + public boolean evaluate(Object o) { + Argument arg = (Argument) o; + return arg.isRequired(); + } + }; + return CollectionUtils.select(this.arguments.values(), p); + } + + /** + * @return required options + */ + public Collection getRequiredOptions() { + Predicate p = new Predicate() { + public boolean evaluate(Object o) { + Option opt = (Option) o; + return opt.isRequired(); + } + }; + return CollectionUtils.select(this.options.values(), p); + } + + /** + * @return the description. + */ + public String getDescription() { + return description; + } + + /** + * @return the localized description. + */ + public String getLocalizedDescription() { + if (description == null) { + return bundle.getString("cmd." + this.name); + } else { + return bundle.getString(this.description); + } + } + + /** + * @param description + * The description to set + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * @return the flags + */ + public Map getFlags() { + return flags; + } + + /** + * @param flags + * The flags to set + */ + public void setFlags(Map flags) { + this.flags = flags; + } + + /** + * @return the impl + */ + public String getImpl() { + return impl; + } + + /** + * @param impl + * The impl to set + */ + public void setImpl(String impl) { + this.impl = impl; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the options + */ + public Map getOptions() { + return options; + } + + /** + * @param options + * the Options to set + */ + public void setOptions(Map options) { + this.options = options; + } + + /** + * {@inheritDoc} + */ + public int compareTo(Object o) { + CommandLine cl = (CommandLine) o; + return name.compareTo(cl.name); + } + + /** + * @return all the parameters. i.e. args, options and flags. + */ + public Iterator getAllParameters() { + IteratorChain chain = new IteratorChain(); + chain.addIterator(getArguments().values().iterator()); + chain.addIterator(getOptions().values().iterator()); + chain.addIterator(getFlags().values().iterator()); + return chain; + } + + /** + * @return the required parameters. i.e. args, options and flags. + */ + public Iterator getRequiredParameters() { + IteratorChain chain = new IteratorChain(); + chain.addIterator(getRequiredArguments().iterator()); + chain.addIterator(getRequiredOptions().iterator()); + return chain; + } + + /** + * @param arg + * the Argument to add + */ + public void addArgument(Argument arg) { + if (arguments.containsKey(new Integer(arg.getPosition()))) { + throw new IllegalArgumentException( + "there's an argument in the position in command " + this.name); + } + // Put default values description and arg name i18n keys + if (arg.getArgName() == null) { + arg.setArgName("cmd." + this.getName() + "." + arg.getName()); + } + if (arg.getDescription() == null) { + arg.setDescription("cmd." + this.getName() + "." + arg.getName() + + ".desc"); + } + this.arguments.put(new Integer(arg.getPosition()), arg); + } + + /** + * @param opt the Option to add + */ + public void addOption(Option opt) { + // Put default values description and arg name i18n keys + if (opt.getArgName() == null) { + opt.setArgName("cmd." + this.getName() + "." + opt.getName()); + } + if (opt.getDescription() == null) { + opt.setDescription("cmd." + this.getName() + "." + opt.getName() + + ".desc"); + } + this.options.put(opt.getName(), opt); + } + + /** + * @param flag the Flag to add + */ + public void addFlag(Flag flag) { + // Put default values description and arg name i18n keys + if (flag.getDescription() == null) { + flag.setDescription("cmd." + this.getName() + "." + flag.getName() + + ".desc"); + } + this.flags.put(flag.getName(), flag); + } + + /** + * @return the arguments + */ + public Map getArguments() { + return arguments; + } + + /** + * @param arguments + * the arguments to set + */ + public void setArguments(Map arguments) { + this.arguments = arguments; + } + + /** + * @return the alias + */ + public Collection getAlias() { + return alias; + } + + /** + * @param alias + * the alias to set + */ + public void setAlias(Collection alias) { + this.alias = alias; + } + + /** + * @param alias the alias to add + */ + public void addAlias(String alias) { + this.alias.add(alias); + } + + /** + * {@inheritDoc} + */ + public Object clone() { + CommandLine cl = new CommandLine(); + cl.alias = this.alias; + // Arguments + Iterator iter = this.arguments.values().iterator(); + while (iter.hasNext()) { + Argument arg = (Argument) iter.next(); + cl.addArgument((Argument) arg.clone()); + } + cl.description = this.description; + // Flags + iter = this.flags.values().iterator(); + while (iter.hasNext()) { + Flag f = (Flag) iter.next(); + cl.addFlag((Flag) f.clone()); + } + cl.impl = this.impl; + cl.name = this.name; + // Flags + iter = this.options.values().iterator(); + while (iter.hasNext()) { + Option o = (Option) iter.next(); + cl.addOption((Option) o.clone()); + } + return cl; + } + + /** + * {@inheritDoc} + */ + public String toString() { + return "CommandLine-" + this.getName() + "(" + impl + ")"; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandLineFactory.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandLineFactory.java new file mode 100644 index 00000000000..581b62eed92 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/CommandLineFactory.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.TreeMap; + +import org.apache.commons.digester.Digester; +import org.apache.commons.digester.xmlrules.DigesterLoader; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; +import org.xml.sax.SAXException; + +/** + * Command line factory + */ +public class CommandLineFactory { + /** logger */ + private static Log log = LogFactory.getLog(CommandLineFactory.class); + + /** resource bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** file name */ + private static final String COMMAND_LINE_FILE = "command-line.xml"; + + /** rules file name */ + private static final String COMMAND_LINE_RULES_FILE = "command-line-rules.xml"; + + /** singleton */ + private static CommandLineFactory singleton; + + /** command cache */ + private Map cache = new TreeMap(); + + /** alias cache */ + private Map alias = new HashMap(); + + /** + * private constructor + */ + private CommandLineFactory() { + super(); + } + + /** + * @return singleton + */ + public static CommandLineFactory getInstance() { + if (singleton == null) { + try { + CommandLineFactory factory = new CommandLineFactory(); + factory.init(); + singleton = factory; + } catch (Exception e) { + log.error(bundle.getString("exception.unabletoinit"), e); + e.printStackTrace(); + } + } + return singleton; + } + + /** + * @return all registered Command s + */ + public Collection getCommandLines() { + List cls = new ArrayList(); + Iterator iter = cache.values().iterator(); + while (iter.hasNext()) { + CommandLine cl = (CommandLine) iter.next(); + cls.add(cl.clone()); + } + return cls; + } + + /** + * Get the Command for the given name + * @param name + * the Command name + * @return a new Command Line Instance for the given command name + * @throws JcrParserException + * if there's no Command for the given name + */ + public CommandLine getCommandLine(String name) throws JcrParserException { + log.debug("lookup command " + name); + // get Command line + CommandLine original = (CommandLine) cache.get(name); + + if (original == null) { + log.debug("lookup alias " + name); + original = (CommandLine) alias.get(name); + } + + if (original == null) { + log.warn("command not found " + name); + throw new JcrParserException("exception.no.command.for.name", + new String[] { + name + }); + } + + // Return a clone + return (CommandLine) original.clone(); + } + + /** + * parses the configuration file + * @throws ConfigurationException + * an Exception occurs while parsing + */ + private void init() throws ConfigurationException { + try { + // Configure Digester from XML ruleset + URL rulesFile = getClass().getResource(COMMAND_LINE_RULES_FILE); + URL clFile = getClass().getResource(COMMAND_LINE_FILE); + + // init digester + Digester digester = DigesterLoader.createDigester(rulesFile); + + // Push empty List onto Digester's Stack + List cls = new ArrayList(); + digester.push(cls); + + // Parse the XML document + InputStream input = clFile.openStream(); + digester.parse(input); + input.close(); + + // Add to cache + Iterator iter = cls.iterator(); + while (iter.hasNext()) { + CommandLine cl = (CommandLine) iter.next(); + cache.put(cl.getName(), cl); + // Add to alias cache + Iterator aliasIt = cl.getAlias().iterator(); + while (aliasIt.hasNext()) { + String aliasName = (String) aliasIt.next(); + if (alias.containsKey(aliasName)) { + throw new ConfigurationException( + "exception.alias.already.in.use", new String[] { + aliasName, cl.getName() + }); + } + alias.put(aliasName, cl); + } + } + } catch (IOException e) { + throw new ConfigurationException(e.getLocalizedMessage()); + } catch (SAXException e) { + throw new ConfigurationException(e.getLocalizedMessage()); + } + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ConfigurationException.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ConfigurationException.java new file mode 100644 index 00000000000..7ce996bfb37 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ConfigurationException.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.standalone.cli; + +import org.apache.jackrabbit.standalone.cli.CommandException; + +/** + * This Exception is thrown when there's an error in the command config file. + */ +public class ConfigurationException extends CommandException { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = 3257848770644621622L; + + /** + * @param message + * the message + */ + public ConfigurationException(String message) { + super(message); + } + + /** + * @param message + * the message + * @param arguments + * the arguments + */ + public ConfigurationException(String message, Object[] arguments) { + super(message, arguments); + } + + /** + * @param message + * the message + * @param cause + * the arguments + */ + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } + + /** + * @param message + * the message + * @param cause + * the cause + * @param arguments + * the arguments + */ + public ConfigurationException(String message, Throwable cause, + Object[] arguments) { + super(message, cause, arguments); + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Flag.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Flag.java new file mode 100644 index 00000000000..eee97cac2f1 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Flag.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.standalone.cli; + +/** + * A command line flag
        + * A flag is a parameter that has no other value that the option name. e.g. + * -[flag name]. + */ +public class Flag extends AbstractParameter { + /** true if flag is present in the user's input */ + private boolean present = false; + + /** + * @return true if the flag exists in the user's input + */ + public boolean isPresent() { + return present; + } + + /** + * @param present + * the present to set + */ + public void setPresent(boolean present) { + this.present = present; + } + + /** + * {@inheritDoc} + */ + public String getValue() { + return Boolean.toString(present); + } + + /** + * {@inheritDoc} + */ + public void setValue(String value) { + present = Boolean.getBoolean(value); + } + + /** + * {@inheritDoc} + */ + public Object clone() { + Flag f = new Flag(); + f.present = this.present; + this.clone(f); + return f; + } + + /** + * {@inheritDoc} + */ + public boolean isRequired() { + return false; + } + + /** + * {@inheritDoc} + */ + public String getLocalizedArgName() { + return ""; + } + + /** + * {@inheritDoc} + */ + public String getLocalizedDescription() { + if (this.getDescription() == null) { + return bundle.getString("param.flag." + this.getName() + ".desc"); + } else { + return bundle.getString(this.getDescription()); + } + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrClient.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrClient.java new file mode 100644 index 00000000000..79a5338748e --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrClient.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; + +import javax.jcr.InvalidItemStateException; +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import jline.ArgumentCompletor; +import jline.Completor; +import jline.ConsoleReader; +import jline.History; +import jline.SimpleCompletor; + +import org.apache.commons.chain.Context; +import org.apache.commons.chain.impl.ContextBase; +import org.apache.commons.cli.BasicParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.cli.Parser; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; +import org.apache.jackrabbit.util.ChildrenCollectorFilter; + +/** + * Command line interface client + */ +public class JcrClient { + /** logger */ + private static Log log = LogFactory.getLog(JcrClient.class); + + /** Resource bundle */ + private ResourceBundle bundle = CommandHelper.getBundle(); + + /** exit control variable */ + private boolean exit = false; + + /** Execution context */ + private Context ctx; + + /** run options */ + private Options options; + + /** + * Constructor + */ + JcrClient() { + super(); + ctx = new ContextBase(); + initOptions(); + initContext(); + } + + /** + * Constructor + * @param ctx + * the Context + */ + public JcrClient(Context ctx) { + super(); + this.ctx = ctx; + } + + /** + * @param args + * the arguments + */ + public static void main(String[] args) { + JcrClient client = new JcrClient(); + client.run(args); + } + + /** + * Run client + * @param args + * the arguments + */ + private void run(String[] args) { + try { + // parse arguments + Parser parser = new BasicParser(); + org.apache.commons.cli.CommandLine cl = parser.parse(options, args); + + // Set locale + this.setLocale(cl); + + // Welcome message + System.out.println(bundle.getString("word.welcome")); + + // check interactive mode + if (cl.hasOption("source")) { + this.runNonInteractive(cl); + } else { + this.runInteractive(); + } + } catch (Exception e) { + HelpFormatter hf = new HelpFormatter(); + hf.printHelp("jcrclient", options); + e.printStackTrace(); + return; + } + } + + /** + * jline ConsoleReader tab completor that completes on the children of the + * current jcr node (both nodes and properties). + * + * @author + * Alexander Klimetschek + * + */ + private class JcrChildrenCompletor implements Completor { + + public int complete(String buffer, int cursor, List clist) { + String start = (buffer == null) ? "" : buffer; + + Node node; + try { + node = CommandHelper.getNode(ctx, "."); + Collection items = new ArrayList(); + ChildrenCollectorFilter collector = new ChildrenCollectorFilter( + "*", items, true, true, 1); + collector.visit(node); + for (Object item : items) { + String can = ((Item) item).getName(); + if (can.startsWith(start)) { + clist.add(can); + } + } + + return 0; + } catch (CommandException e) { + e.printStackTrace(); + } catch (RepositoryException e) { + e.printStackTrace(); + } + + return -1; + } + + } + + /** + * Run in interactive mode + * @throws Exception + * if an Exception occurs + */ + public void runInteractive() throws Exception { + // built jline console reader with history + tab completion + ConsoleReader reader = new ConsoleReader(); + reader.setHistory(new History()); + reader.setUseHistory(true); + + // get all available commands for command tab completion + Collection commands = + CommandLineFactory.getInstance().getCommandLines(); + List commandNames = new ArrayList(); + for (CommandLine c : commands) { + commandNames.add(c.getName()); + for (Object alias : c.getAlias()) { + commandNames.add((String) alias); + } + } + commandNames.add("exit"); + commandNames.add("quit"); + + // first part is the command, then all arguments will get children tab completion + reader.addCompletor(new ArgumentCompletor( new Completor[] { + new SimpleCompletor(commandNames.toArray(new String[] {} )), + new JcrChildrenCompletor() + })); + + while (!exit) { + try { + String input = reader.readLine("[" + this.getPrompt() + "] > "); + if (input == null) { + input = "exit"; + } else { + input = input.trim(); + } + log.debug("running: " + input); + if (input.equals("exit") || input.equals("quit")) { // exit? + exit = true; + System.out.println("Good bye..."); + } else if (input.length() > 0) { + this.runCommand(input); + } + } catch (JcrParserException e) { + System.out.println(e.getLocalizedMessage()); + System.out.println(); + } catch (Exception e) { + handleException(reader, e); + } + } + } + + /** + * Run in non interactive mode + * @param cl + * the CommandLine + * @throws Exception + * if an Exception occurs while running the + * Command + */ + private void runNonInteractive(org.apache.commons.cli.CommandLine cl) throws Exception { + this.runCommand("source " + cl.getOptionValue("source")); + } + + /** + * Parses the input and runs the specified command + * @param input + * the user's input + * @throws Exception + * if an Exception occurs while running the + * Command + */ + void runCommand(String input) throws Exception { + if (input.startsWith("#") || input.length() == 0) { + return; + } + + // Process user input + JcrParser parser = new JcrParser(); + parser.parse(input); + + // populate ctx + parser.populateContext(ctx); + + // Execute command + long start = System.currentTimeMillis(); + parser.getCommand().execute(ctx); + long elapsed = System.currentTimeMillis() - start; + + // depopulate ctx + parser.depopulateContext(ctx); + + // Display elapsed timed + System.out.println(); + System.out.println(bundle.getString("phrase.elapsedtime") + ": " + + elapsed + " ms."); + System.out.println(); + } + + /** + * Handle the Exception.
        + * Shows a short message and prompt the user to show the entire stacktrace. + * @param ex + * the Exception to handle + */ + private void handleException(ConsoleReader cr, Exception ex) { + System.out.println(); + System.out.println(bundle.getString("exception.occurred")); + System.out.println(); + System.out.println(bundle.getString("exception") + ": " + + ex.getClass().getName()); + System.out.println(bundle.getString("word.message") + ": " + + ex.getLocalizedMessage()); + System.out.println(); + String prompt = bundle.getString("phrase.display.stacktrace") + + "? [y/n]"; + + String str = ""; + int tries = 0; + while (!str.equals("y") && !str.equals("n") && tries < 3) { + tries++; + + try { + str = cr.readLine(prompt); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (str.equals("y")) { + ex.printStackTrace(); + } + } + + /** + * Prompt message + * @return prompt the prompt message + * @throws RepositoryException + * if the current Repository throws a + * RepositoryException + */ + private String getPrompt() throws RepositoryException { + + try { + CommandHelper.getRepository(ctx); + } catch (CommandException e) { + return bundle.getString("phrase.not.connected"); + } + + boolean unsaved = false; + try { + unsaved = CommandHelper.getSession(ctx).hasPendingChanges(); + } catch (CommandException e) { + return bundle.getString("phrase.not.logged.in"); + } + + try { + Node n = CommandHelper.getCurrentNode(ctx); + // the current node might be Invalid + String path; + try { + path = n.getPath(); + } catch (InvalidItemStateException e) { + CommandHelper.setCurrentNode(ctx, CommandHelper.getSession(ctx) + .getRootNode()); + path = CommandHelper.getCurrentNode(ctx).getPath(); + } + if (unsaved) { + return path + "*"; + } else { + return path; + } + } catch (CommandException e) { + return bundle.getString("phrase.not.logged.in"); + } + + } + + /** + * Init allowed CommandLine options + */ + private void initOptions() { + options = new Options(); + options.addOption("lang", "code", true, "Language code"); + options.addOption("country", "code", true, "Country code"); + options.addOption("source", "path", true, + "Script for noninteractive mode"); + } + + /** + * Sets the default Locale for the given CommandLine + * @param cl + * the CLI CommandLine + * @throws ParseException + * if cl can't be parsed + */ + private void setLocale(org.apache.commons.cli.CommandLine cl) throws ParseException { + Locale locale = null; + if (cl.hasOption("lang") && cl.hasOption("country")) { + locale = new Locale(cl.getOptionValue("lang"), cl + .getOptionValue("country")); + } + if (cl.hasOption("lang") && !cl.hasOption("country")) { + locale = new Locale(cl.getOptionValue("lang")); + } + if (locale != null) { + Locale.setDefault(locale); + } + } + + /** + * Init context.
        + * Sets the Context Output to the console + */ + private void initContext() { + CommandHelper.setOutput(ctx, new PrintWriter(System.out, true)); + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrParser.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrParser.java new file mode 100644 index 00000000000..f0e8602578a --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrParser.java @@ -0,0 +1,293 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.chain.Catalog; +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.chain.config.ConfigParser; +import org.apache.commons.chain.impl.CatalogFactoryBase; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Input Parser + */ +public class JcrParser { + /** parser */ + private static Log log = LogFactory.getLog(JcrParser.class); + + static { + try { + ConfigParser parser = new ConfigParser(); + parser.parse(JcrParser.class.getResource("command.xml")); + } catch (Exception e) { + log.error("Failed to parse command.xml", e); + } + } + + /** catalog */ + private Catalog catalog = CatalogFactoryBase.getInstance().getCatalog(); + + /** Command */ + private Command cmd; + + /** Command Line */ + private CommandLine cl; + + /** + * Constructor + */ + public JcrParser() { + super(); + } + + /** + * Parse the user's input. + * @param input + * user's input + * @throws JcrParserException + * if the input is illegal + * @throws ConfigurationException + * if the mapped command can't be mapped to a Commons Chain Command + */ + public void parse(String input) throws JcrParserException, + ConfigurationException { + this.cl = null; + this.cmd = null; + + // Validate input + if (input == null || input.length() == 0) { + throw new JcrParserException("exception.parse.input.empty"); + } + + // Extract arguments + LinkedList args = this.getArguments(input); + + // The first arg is the command name + String cmdName = (String) args.getFirst(); + args.removeFirst(); + + // Get the command line descriptor + cl = CommandLineFactory.getInstance().getCommandLine(cmdName); + + // populate with the given params + populate(cl, args); + + // validate the command line + validate(cl); + + // Create Chain Command + String impl = cl.getImpl(); + if (impl == null) { + impl = cl.getName(); + } + cmd = catalog.getCommand(impl); + + if (cmd == null) { + throw new JcrParserException("no chain command for name " + impl); + } + + } + + /** + * Tokenize user's input + * @param input + * the user's input + * @return a List containing the arguments + */ + private LinkedList getArguments(String input) { + LinkedList args = new LinkedList(); + int length = input.length(); + + boolean insideSingleQuote = false; + boolean insideDoubleQuote = false; + int escape = -1; + + StringBuffer arg = new StringBuffer(); + + for (int i = 0; i < length; ++i) { + char c = input.charAt(i); + + // end of argument? + if ((!insideSingleQuote && !insideDoubleQuote && Character + .isWhitespace(c))) { + if (arg.toString().trim().length() > 0) { + args.add(arg.toString().trim()); + arg = new StringBuffer(); + } + continue; + } + + if (i == escape) { // escaped char + arg.append(c); + } else { // unescaped char + switch (c) { + case '\\': + escape = i + 1; + break; + case '"': + insideDoubleQuote = !insideDoubleQuote; + break; + case '\'': + insideSingleQuote = !insideSingleQuote; + break; + default: + arg.append(c); + break; + } + } + } + + if (arg.toString().trim().length() > 0) { + args.add(arg.toString()); + } + + return args; + } + + /** + * Populate the Context with the attributes needed by the + * Command + * @param ctx + * the Context + */ + public void populateContext(Context ctx) { + Iterator iter = cl.getAllParameters(); + while (iter.hasNext()) { + AbstractParameter param = (AbstractParameter) iter.next(); + log.debug("add ctx attr: " + param.getContextKey() + "=" + + param.getValue()); + ctx.put(param.getContextKey(), param.getValue()); + } + } + + /** + * Remove Context attribute specific to the parsed + * Command + * @param ctx + * the Context + */ + public void depopulateContext(Context ctx) { + Iterator iter = cl.getAllParameters(); + while (iter.hasNext()) { + AbstractParameter param = (AbstractParameter) iter.next(); + String ctxKey = param.getContextKey(); + log.debug("remove ctx attr: " + ctxKey + "=" + param.getValue()); + ctx.remove(ctxKey); + } + } + + /** + * @return the Command + */ + public Command getCommand() { + return cmd; + } + + /** + * Populate the CommandLine with the given parameters + * @param cl + * the CommandLine + * @param valList + * the arguments + * @throws JcrParserException + * if the user's input is illegal + */ + private void populate(CommandLine cl, List valList) + throws JcrParserException { + String[] values = (String[]) valList + .toArray(new String[valList.size()]); + + // Command Line parameters + Map options = cl.getOptions(); + Map flags = cl.getFlags(); + Map clArgs = cl.getArguments(); + + // Input arguments + List args = new ArrayList(); + + for (int i = 0; i < values.length; i++) { + String value = values[i]; + + if (value.startsWith("-")) { + // option + if (i + 1 < values.length && !values[i + 1].startsWith("-")) { + Option opt = (Option) options.get(value.substring(1)); + if (opt == null) { + throw new JcrParserException("exception.no.opt.for.name", + new String[] { + value.substring(1) + }); + } + opt.setValue(values[i + 1]); + i++; + } else { + // flag + Flag flag = (Flag) flags.get(value.substring(1)); + if (flag == null) { + throw new JcrParserException("exception.no.flag.for.name", + new String[] { + value + }); + } + flag.setPresent(true); + } + } else { + // collect arguments + args.add(value); + } + } + + // set arguments + String[] argValues = (String[]) args.toArray(new String[args.size()]); + for (int j = 0; j < argValues.length; j++) { + Argument arg = (Argument) clArgs.get(new Integer(j)); + if (arg == null) { + throw new JcrParserException("exception.more.arguments.than.expected"); + } + arg.setValue(argValues[j]); + } + + } + + /** + * Validate the CommandLine + * @param cl + * the CommandLine + * @throws JcrParserException + * if a required parameter is not present in the user's input + */ + private void validate(CommandLine cl) throws JcrParserException { + Iterator iter = cl.getRequiredParameters(); + while (iter.hasNext()) { + AbstractParameter param = (AbstractParameter) iter.next(); + if (param.getValue() == null) { + throw new JcrParserException("exception.missing.paramater", new String[] { + param.getName() + }); + } + } + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrParserException.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrParserException.java new file mode 100644 index 00000000000..d57056b0a35 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/JcrParserException.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.standalone.cli; + +import org.apache.jackrabbit.standalone.cli.CommandException; + +/** + * Exception thrown if any error occurs while parsing the user's input. + */ +public class JcrParserException extends CommandException { + + /** + * serialVersionUID + */ + private static final long serialVersionUID = 3761694498056713525L; + + /** + * @param message the message + * @param arguments the arguments + */ + public JcrParserException(String message, Object[] arguments) { + super(message, arguments); + } + + /** + * @param message the message + * @param cause the cause + * @param arguments the arguments + */ + public JcrParserException(String message, Throwable cause, + Object[] arguments) { + super(message, cause, arguments); + } + + /** + * @param message the message + */ + public JcrParserException(String message) { + super(message); + } + + /** + * @param message the message + * @param cause the cause + */ + public JcrParserException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Option.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Option.java new file mode 100644 index 00000000000..0413e7d4fc6 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/Option.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.standalone.cli; + +/** + * Command Line option.
        + * An option is a pair with the following pattern -[option name] [option value] + */ +public class Option extends AbstractParameter { + + /** argument name */ + private String argName; + + /** required */ + private boolean required = false; + + /** + * @return true if this Option is required + */ + public boolean isRequired() { + return required; + } + + /** + * @param required + * set required + */ + public void setRequired(boolean required) { + this.required = required; + } + + /** + * @return the argument name + */ + public String getArgName() { + return argName; + } + + /** + * @return the localized argument name + */ + public String getLocalizedArgName() { + return bundle.getString(this.getArgName()); + } + + /** + * @param argName + * the argument name to set + */ + public void setArgName(String argName) { + this.argName = argName; + } + + /** + * {@inheritDoc} + */ + public Object clone() { + Option o = new Option(); + this.clone(o); + return o; + } + + /** + * {@inheritDoc} + */ + protected void clone(Option opt) { + super.clone(opt); + opt.argName = this.argName; + opt.required = this.required; + + } + + /** + * {@inheritDoc} + */ + public String getLocalizedDescription() { + return bundle.getString(this.getDescription()); + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/SourceCommand.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/SourceCommand.java new file mode 100644 index 00000000000..898725c2f66 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/SourceCommand.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.standalone.cli; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.PrintWriter; +import java.util.ResourceBundle; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Executes a script from the given file + */ +public class SourceCommand implements Command { + /** Resource bundle */ + private ResourceBundle bundle = CommandHelper.getBundle(); + + /** file */ + private String fileKey = "file"; + + /** + * @return Returns the file. + */ + public String getFileKey() { + return fileKey; + } + + /** + * @param file + * The file to set. + */ + public void setFileKey(String file) { + this.fileKey = file; + } + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + File f = new File((String) ctx.get(this.fileKey)); + if (!f.exists()) { + throw new CommandException("exception.file.not.found", + new String[] { + f.getAbsolutePath() + }); + } + // client + JcrClient client = new JcrClient(ctx); + + BufferedReader in = new BufferedReader(new FileReader(f)); + PrintWriter out = CommandHelper.getOutput(ctx); + String line = null; + while ((line = in.readLine()) != null) { + out.println(bundle.getString("word.running") + ": " + line); + client.runCommand(line); + } + in.close(); + return false; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/AbstractCollect.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/AbstractCollect.java new file mode 100644 index 00000000000..b2d1504868f --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/AbstractCollect.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.standalone.cli.collect; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.jcr.Node; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; +import org.apache.jackrabbit.util.ChildrenCollectorFilter; + +/** + * Collect the children items from the given node and store them under the given + * key. + */ +public abstract class AbstractCollect implements Command { + /** logger */ + private static Log log = LogFactory.getLog(AbstractCollect.class); + + // ---------------------------- < keys > + + /** path key. Default value is "." */ + private String srcPathKey = "srcPath"; + + /** context attribute key for the depth attribute. */ + private String depthKey = "depth"; + + /** + * context attribute key for the name pattern attribute. Default value is + * "*" + */ + private String namePatternKey = "namePattern"; + + /** context attribute key for the destination attribute */ + private String destKey = "collected"; + + /** + * {@inheritDoc} + */ + public final boolean execute(Context ctx) throws Exception { + if (this.destKey == null || this.destKey.length() == 0) { + throw new IllegalStateException("target variable is not set"); + } + String relPath = (String) ctx.get(this.srcPathKey); + if (relPath == null) { + relPath = "."; + } + String namePattern = (String) ctx.get(this.namePatternKey); + if (namePattern == null || namePattern.length() == 0) { + namePattern = "*"; + } + + int depth = Integer.parseInt((String) ctx.get(this.depthKey)); + + Node node = CommandHelper.getNode(ctx, relPath); + if (log.isDebugEnabled()) { + log.debug("collecting nodes from " + node.getPath() + " depth=" + + depth + " pattern=" + namePattern + + " into target variable " + this.destKey); + } + + Collection items = new ArrayList(); + ChildrenCollectorFilter collector = new ChildrenCollectorFilter( + namePattern, items, isCollectNodes(), isCollectProperties(), depth); + collector.visit(node); + ctx.put(this.destKey, items.iterator()); + + return false; + } + + /** + * @return Returns the depthKey. + */ + public String getDepthKey() { + return depthKey; + } + + /** + * @param depthKey + * Set the context attribute key for the depth attribute + */ + public void setDepthKey(String depthKey) { + this.depthKey = depthKey; + } + + /** + * @return Returns the namePatternKey. + */ + public String getNamePatternKey() { + return namePatternKey; + } + + /** + * @param namePatternKey + * context attribute key for the name pattern attribute + */ + public void setNamePatternKey(String namePatternKey) { + this.namePatternKey = namePatternKey; + } + + /** + * Collect nodes flag + * @return true if Node s must be collected + */ + protected abstract boolean isCollectNodes(); + + /** + * Collect properties flag + * @return true if Property s must be collected + */ + protected abstract boolean isCollectProperties(); + + /** + * @return the destination key + */ + public String getDestKey() { + return destKey; + } + + /** + * Sets the destination key + * @param destKey + * the detination key + */ + public void setDestKey(String destKey) { + this.destKey = destKey; + } + + /** + * @return the source path key + */ + public String getSrcPathKey() { + return srcPathKey; + } + + /** + * Sets the source path key + * @param srcPathKey + * the source path key + */ + public void setSrcPathKey(String srcPathKey) { + this.srcPathKey = srcPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectItems.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectItems.java new file mode 100644 index 00000000000..de38653f1a8 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectItems.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.standalone.cli.collect; + +/** + * Collect children, either nodes or properties + */ +public class CollectItems extends AbstractCollect { + + /** + * {@inheritDoc} + */ + protected boolean isCollectNodes() { + return true; + } + + /** + * {@inheritDoc} + */ + protected boolean isCollectProperties() { + return true; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectNodes.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectNodes.java new file mode 100644 index 00000000000..359ff2212c6 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectNodes.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.standalone.cli.collect; + +/** + * Collect nodes only + */ +public class CollectNodes extends AbstractCollect { + + /** + * {@inheritDoc} + */ + protected boolean isCollectNodes() { + return true; + } + + /** + * {@inheritDoc} + */ + protected boolean isCollectProperties() { + return false; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectProperties.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectProperties.java new file mode 100644 index 00000000000..34eea4c0afc --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/collect/CollectProperties.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.standalone.cli.collect; + +/** + * Collect properties only + */ +public class CollectProperties extends AbstractCollect { + + /** + * {@inheritDoc} + */ + protected boolean isCollectNodes() { + return false; + } + + /** + * {@inheritDoc} + */ + protected boolean isCollectProperties() { + return true; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/AbstractSetProperty.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/AbstractSetProperty.java new file mode 100644 index 00000000000..a82d2b74042 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/AbstractSetProperty.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.standalone.cli.core; + +import org.apache.commons.chain.Command; + +/** + * SetProperty superclass. + */ +public abstract class AbstractSetProperty implements Command { + // ---------------------------- < keys > + + /** destination node path key */ + protected String parentPathKey = "parentPath"; + + /** Property name key */ + protected String nameKey = "name"; + + /** Propety type key */ + protected String typeKey = "type"; + + /** Property value key */ + protected String valueKey = "value"; + + /** + * @return Returns the valueKey. + */ + public String getValueKey() { + return valueKey; + } + + /** + * @param valueKey + * Set the context attribute key for the value attribute. + */ + public void setValueKey(String valueKey) { + this.valueKey = valueKey; + } + + /** + * @return the parent path key + */ + public String getParentPathKey() { + return parentPathKey; + } + + /** + * Sets the parent path key + * @param parentPathKey + * the parent path key + */ + public void setParentPathKey(String parentPathKey) { + this.parentPathKey = parentPathKey; + } + + /** + * @return the name key + */ + public String getNameKey() { + return nameKey; + } + + /** + * Sets the name key + * @param nameKey + * the name key + */ + public void setNameKey(String nameKey) { + this.nameKey = nameKey; + } + + /** + * @return the type key + */ + public String getTypeKey() { + return typeKey; + } + + /** + * Sets the type key + * @param typeKey + * the type key + */ + public void setTypeKey(String typeKey) { + this.typeKey = typeKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/AddNode.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/AddNode.java new file mode 100644 index 00000000000..0b4ed0709cf --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/AddNode.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.standalone.cli.core; + +import javax.jcr.Node; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Add a node to the current working Node + */ +public class AddNode implements Command { + /** logger */ + private static Log log = LogFactory.getLog(AddNode.class); + + // ---------------------------- < keys > + + /** Node type key */ + private String typeKey = "type"; + + /** Node name key */ + private String relPathKey = "relPath"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + Node node = CommandHelper.getCurrentNode(ctx); + String nodeType = (String) ctx.get(this.typeKey); + String name = (String) ctx.get(this.relPathKey); + + if (log.isDebugEnabled()) { + log.debug("adding node at " + node.getPath() + "/" + name); + } + + // If the new node name starts with / add it to the root node + if (name.startsWith("/")) { + node = CommandHelper.getSession(ctx).getRootNode(); + name = name.substring(1); + } + + if (nodeType == null) { + node.addNode(name); + } else { + node.addNode(name, nodeType); + } + return false; + } + + /** + * @return the nodeTypeKey. + */ + public String getTypeKey() { + return typeKey; + } + + /** + * @param nodeTypeKey + * Set the context attribute key for the node type attribute. + */ + public void setTypeKey(String nodeTypeKey) { + this.typeKey = nodeTypeKey; + } + + /** + * @return the relative path. + */ + public String getRelPathKey() { + return relPathKey; + } + + /** + * @param relPathKey + * the relative path key to set + */ + public void setRelPathKey(String relPathKey) { + this.relPathKey = relPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/ClearWorkspace.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/ClearWorkspace.java new file mode 100644 index 00000000000..48ed4bfc96a --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/ClearWorkspace.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.standalone.cli.core; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Remove all the content from the current working Workspace + */ +public class ClearWorkspace implements Command { + /** logger */ + private static Log log = LogFactory.getLog(ClearWorkspace.class); + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + Session s = CommandHelper.getSession(ctx); + + if (log.isDebugEnabled()) { + log.debug("removing all content from workspace " + + s.getWorkspace().getName()); + } + + // Set current node to root + CommandHelper.setCurrentNode(ctx, s.getRootNode()); + NodeIterator iter = s.getRootNode().getNodes(); + while (iter.hasNext()) { + Node n = (Node) iter.next(); + if (!n.getName().equals(JcrConstants.JCR_SYSTEM)) { + n.remove(); + } + } + PropertyIterator pIter = s.getRootNode().getProperties(); + while (pIter.hasNext()) { + Property p = pIter.nextProperty(); + if (!p.getName().equals(JcrConstants.JCR_PRIMARYTYPE)) { + p.remove(); + } + } + return false; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Clone.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Clone.java new file mode 100644 index 00000000000..c8efb3df64d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Clone.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.standalone.cli.core; + +import javax.jcr.Workspace; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Clone the given Node to another Workspace + */ +public class Clone implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Clone.class); + + // ---------------------------- < keys > + /** Source workspace */ + private String srcWorkspaceKey = "scrWorkspace"; + + /** target workspace */ + private String srcAbsPathKey = "srcAbsPath"; + + /** source node */ + private String destAbsPathKey = "destAbsPath"; + + /** target node */ + private String removeExistingKey = "removeExisting"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String srcWorkspace = (String) ctx.get(this.srcWorkspaceKey); + String srcAbsPath = (String) ctx.get(this.srcAbsPathKey); + String destAbsPath = (String) ctx.get(this.destAbsPathKey); + Boolean removeExisting = Boolean.valueOf((String) ctx + .get(this.removeExistingKey)); + + Workspace w = CommandHelper.getSession(ctx).getWorkspace(); + + if (log.isDebugEnabled()) { + log.debug("cloning node. from [" + srcWorkspace + ":" + srcAbsPath + + "] to [" + w.getName() + ":" + destAbsPath + "]"); + } + + w.clone(srcWorkspace, srcAbsPath, destAbsPath, removeExisting + .booleanValue()); + + return false; + } + + /** + * @return the destination absolute path key + */ + public String getDestAbsPathKey() { + return destAbsPathKey; + } + + /** + * Sets the destination absolute path key + * @param destAbsPathKey + * the destination absolute path key + */ + public void setDestAbsPathKey(String destAbsPathKey) { + this.destAbsPathKey = destAbsPathKey; + } + + /** + * @return the remove existing key option + */ + public String getRemoveExistingKey() { + return removeExistingKey; + } + + /** + * Set the remove existing key option + * @param removeExistingKey + * the remove existing key option + */ + public void setRemoveExistingKey(String removeExistingKey) { + this.removeExistingKey = removeExistingKey; + } + + /** + * @return the source absolute path key + */ + public String getSrcAbsPathKey() { + return srcAbsPathKey; + } + + /** + * Sets the source absolute path key + * @param srcAbsPathKey + * the source absolute path key + */ + public void setSrcAbsPathKey(String srcAbsPathKey) { + this.srcAbsPathKey = srcAbsPathKey; + } + + /** + * @return the source Workspace key + */ + public String getSrcWorkspaceKey() { + return srcWorkspaceKey; + } + + /** + * Sets the source Workspace key + * @param srcWorkspaceKey + * the source Workspace key + */ + public void setSrcWorkspaceKey(String srcWorkspaceKey) { + this.srcWorkspaceKey = srcWorkspaceKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Copy.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Copy.java new file mode 100644 index 00000000000..abf72806d0d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Copy.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.standalone.cli.core; + +import javax.jcr.Item; +import javax.jcr.Workspace; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Copy a Node.
        + * If the source Workspace is unset it will create a copy of the + * given Node from the current working Workspace.
        + * If the target path ends with '/' the source node will be copied as a child of + * the target node maintaining the name. + */ +public class Copy implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Copy.class); + + // ---------------------------- < keys > + /** + * Source workspace. + */ + private String srcWorkspaceKey = "srcWorkspace"; + + /** source path */ + private String srcAbsPathKey = "srcAbsPath"; + + /** destination path */ + private String destAbsPathKey = "destAbsPath"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String srcWorkspace = (String) ctx.get(this.srcWorkspaceKey); + String srcAbsPath = (String) ctx.get(this.srcAbsPathKey); + String destAbsPath = (String) ctx.get(this.destAbsPathKey); + + Workspace w = CommandHelper.getSession(ctx).getWorkspace(); + + if (srcWorkspace == null) { + srcWorkspace = w.getName(); + } + + if (log.isDebugEnabled()) { + log.debug("copying node from [" + srcWorkspace + ":" + srcAbsPath + + "] to [" + w.getName() + ":" + destAbsPath + "]"); + } + + if (destAbsPath.endsWith("/")) { + Item source = CommandHelper.getSession(ctx).getItem(srcAbsPath); + destAbsPath = destAbsPath + source.getName(); + } + + w.copy(srcWorkspace, srcAbsPath, destAbsPath); + + return false; + } + + /** + * @return the destination absolute path key + */ + public String getDestAbsPathKey() { + return destAbsPathKey; + } + + /** + * sets the destination absolute path key + * + * @param destAbsPathKey + * the destination absolute path key + */ + public void setDestAbsPathKey(String destAbsPathKey) { + this.destAbsPathKey = destAbsPathKey; + } + + /** + * @return the source absolute path key + */ + public String getSrcAbsPathKey() { + return srcAbsPathKey; + } + + /** + * Sets the source absolute path key + * + * @param srcAbsPathKey + * the source absolute path key + */ + public void setSrcAbsPathKey(String srcAbsPathKey) { + this.srcAbsPathKey = srcAbsPathKey; + } + + /** + * @return the source Workspace key + */ + public String getSrcWorkspaceKey() { + return srcWorkspaceKey; + } + + /** + * Sets the source Workspace key + * + * @param srcWorkspaceKey + * the source Workspace key + */ + public void setSrcWorkspaceKey(String srcWorkspaceKey) { + this.srcWorkspaceKey = srcWorkspaceKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/CurrentNode.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/CurrentNode.java new file mode 100644 index 00000000000..e84fb0ffc9a --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/CurrentNode.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.standalone.cli.core; + +import javax.jcr.Node; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Set the current working Node + */ +public class CurrentNode implements Command { + /** logger */ + private static Log log = LogFactory.getLog(CurrentNode.class); + + // ---------------------------- < keys > + + /** context attribute key for the path attribute */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String dest = (String) ctx.get(this.pathKey); + Node n = CommandHelper.getNode(ctx, dest); + if (log.isDebugEnabled()) { + log.debug("set current working node to " + n.getPath()); + } + CommandHelper.setCurrentNode(ctx, n); + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Login.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Login.java new file mode 100644 index 00000000000..07f72c4ba1b --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Login.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.standalone.cli.core; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import jline.ConsoleReader; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Login to the current working Repository + */ +public class Login implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Login.class); + + // ---------------------------- < keys > + /** user key */ + private String userKey = "user"; + + /** password key */ + private String passwordKey = "password"; + + /** workspace key */ + private String workspaceKey = "workspace"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String anon = "anonymous"; + + String user = (String) ctx.get(this.userKey); + String password = (String) ctx.get(this.passwordKey); + String workspace = (String) ctx.get(this.workspaceKey); + + if (user == null) { + user = anon; + } + + if (password == null || (password.equals(anon) && !user.equals(anon))) { + ConsoleReader reader = new ConsoleReader(); + password = reader.readLine("Password: ", (char) 0); + } + + if (log.isDebugEnabled()) { + log.debug("logging in as " + user); + } + + Session session = null; + Repository repo = CommandHelper.getRepository(ctx); + + Credentials credentials = new SimpleCredentials(user, password + .toCharArray()); + + if (workspace == null) { + session = repo.login(credentials); + } else { + session = repo.login(credentials, workspace); + } + CommandHelper.setSession(ctx, session); + CommandHelper.setCurrentNode(ctx, session.getRootNode()); + return false; + } + + /** + * @return the password key + */ + public String getPasswordKey() { + return passwordKey; + } + + /** + * @param passwordKey + * the password key to set + */ + public void setPasswordKey(String passwordKey) { + this.passwordKey = passwordKey; + } + + /** + * @return the user key. + */ + public String getUserKey() { + return userKey; + } + + /** + * @param userKey + * the user key to set + */ + public void setUserKey(String userKey) { + this.userKey = userKey; + } + + /** + * @return the Workspace. + */ + public String getWorkspaceKey() { + return workspaceKey; + } + + /** + * @param workspaceKey + * the Workspace key to set + */ + public void setWorkspaceKey(String workspaceKey) { + this.workspaceKey = workspaceKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Logout.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Logout.java new file mode 100644 index 00000000000..1c668ce7978 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Logout.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.standalone.cli.core; + +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Logout from the current working Repository + */ +public class Logout implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Logout.class); + + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + Session s = CommandHelper.getSession(ctx); + if (log.isDebugEnabled()) { + log.debug("logging out user " + s.getUserID()); + } + s.logout(); + CommandHelper.setCurrentNode(ctx, null); + CommandHelper.setSession(ctx, null); + return false; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Move.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Move.java new file mode 100644 index 00000000000..8b7ca9b277a --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Move.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.standalone.cli.core; + +import java.util.ResourceBundle; + +import javax.jcr.Item; +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Move a Node + */ +public class Move implements Command { + /** resource bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** logger */ + private static Log log = LogFactory.getLog(Move.class); + + // ---------------------------- < keys > + /** source path */ + private String srcAbsPathKey = "srcAbsPath"; + + /** destination path */ + private String destAbsPathKey = "destAbsPath"; + + /** persistent key */ + private String persistentKey = "persistent"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String srcAbsPath = (String) ctx.get(this.srcAbsPathKey); + String destAbsPath = (String) ctx.get(this.destAbsPathKey); + + boolean persistent = Boolean.valueOf( + (String) ctx.get(this.persistentKey)).booleanValue(); + + if (!srcAbsPath.startsWith("/") || !destAbsPath.startsWith("/")) { + throw new IllegalArgumentException(bundle + .getString("exception.illegalargument") + + ". " + + bundle.getString("exception.only.absolute.path") + + "."); + } + + Session s = CommandHelper.getSession(ctx); + + if (log.isDebugEnabled()) { + log.debug("moving node from " + srcAbsPath + " to " + destAbsPath); + } + + if (destAbsPath.endsWith("/")) { + Item source = s.getItem(srcAbsPath); + destAbsPath = destAbsPath + source.getName(); + } + + if (persistent) { + s.getWorkspace().move(srcAbsPath, destAbsPath); + } else { + s.move(srcAbsPath, destAbsPath); + } + + return false; + } + + /** + * @return the destintation absolute path key + */ + public String getDestAbsPathKey() { + return destAbsPathKey; + } + + /** + * @param destAbsPathKey + * the destintation absolute path key to set + */ + public void setDestAbsPathKey(String destAbsPathKey) { + this.destAbsPathKey = destAbsPathKey; + } + + /** + * @return the source absolute path key + */ + public String getSrcAbsPathKey() { + return srcAbsPathKey; + } + + /** + * @param srcAbsPathKey + * the source absolute path key to set + */ + public void setSrcAbsPathKey(String srcAbsPathKey) { + this.srcAbsPathKey = srcAbsPathKey; + } + + public String getPersistentKey() { + return persistentKey; + } + + public void setPersistentKey(String persistentKey) { + this.persistentKey = persistentKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/OrderBefore.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/OrderBefore.java new file mode 100644 index 00000000000..05e45e0b7a7 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/OrderBefore.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.standalone.cli.core; + +import javax.jcr.Node; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Set the order of the given Node + */ +public class OrderBefore implements Command { + /** logger */ + private static Log log = LogFactory.getLog(OrderBefore.class); + + // ---------------------------- < keys > + /** node path */ + private String parentPathKey = "parentPath"; + + /** source path */ + private String srcChildKey = "srcChild"; + + /** destination path */ + private String destChildKey = "destChild"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String parentPath = (String) ctx.get(this.parentPathKey); + Node n = CommandHelper.getNode(ctx, parentPath); + + String srcChildPath = (String) ctx.get(this.srcChildKey); + String destChildPath = (String) ctx.get(this.destChildKey); + + if (log.isDebugEnabled()) { + log + .debug("ordering before. from " + n.getPath() + "/" + + srcChildPath + " to " + n.getPath() + "/" + + destChildPath); + } + + n.orderBefore(srcChildPath, destChildPath); + + return false; + } + + /** + * @return the destination child key + */ + public String getDestChildKey() { + return destChildKey; + } + + /** + * @param destChildRelPathKey + * the destination child key to set + */ + public void setDestChildKey(String destChildRelPathKey) { + this.destChildKey = destChildRelPathKey; + } + + /** + * @return the source child key + */ + public String getSrcChildKey() { + return srcChildKey; + } + + /** + * @param srcChildRelPathKey + * the source child key to set + */ + public void setSrcChildKey(String srcChildRelPathKey) { + this.srcChildKey = srcChildRelPathKey; + } + + /** + * @return the parent path key + */ + public String getParentPathKey() { + return parentPathKey; + } + + /** + * @param parentPathKey + * the parent path key to set + */ + public void setParentPathKey(String parentPathKey) { + this.parentPathKey = parentPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/ReadValue.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/ReadValue.java new file mode 100644 index 00000000000..a01952dd7e8 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/ReadValue.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.standalone.cli.core; + +import javax.jcr.Property; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Read the Value of the given Property and store + * it under the given Context attribute. + */ +public class ReadValue implements Command { + /** logger */ + private static Log log = LogFactory.getLog(ReadValue.class); + + // ---------------------------- < keys > + + /** property path key */ + private String srcPathKey = "srcPath"; + + /** value index key */ + private String srcIndexKey = "srcIndex"; + + /** destination context attribute */ + private String destKey = "dest"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.srcPathKey); + int index = 1; + if (ctx.get(this.srcIndexKey) != null) { + index = Integer.valueOf(((String) ctx.get(this.srcIndexKey))) + .intValue(); + } + + String dest = (String) ctx.get(this.destKey); + + if (log.isDebugEnabled()) { + log.debug("reading value from " + path + "[" + index + "] to " + + dest); + } + + Property p = (Property) CommandHelper.getItem(ctx, path); + + if (p.getDefinition().isMultiple()) { + ctx.put(dest, p.getValues()[index].getString()); + } else { + ctx.put(dest, p.getValue().getString()); + } + return false; + } + + /** + * @return the destination key + */ + public String getDestKey() { + return destKey; + } + + /** + * @param destKey + * the destination key to set + */ + public void setDestKey(String destKey) { + this.destKey = destKey; + } + + /** + * @return the source index key + */ + public String getSrcIndexKey() { + return srcIndexKey; + } + + /** + * @param srcIndexKey + * the source index key to set + */ + public void setSrcIndexKey(String srcIndexKey) { + this.srcIndexKey = srcIndexKey; + } + + /** + * @return the source path key + */ + public String getSrcPathKey() { + return srcPathKey; + } + + /** + * @param srcPathKey + * the source path key to set + */ + public void setSrcPathKey(String srcPathKey) { + this.srcPathKey = srcPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Refresh.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Refresh.java new file mode 100644 index 00000000000..8e64dacbe09 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Refresh.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.standalone.cli.core; + +import javax.jcr.Item; +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Refresh the Item if specified or the Session to + * reflect the current saved state. + */ +public class Refresh implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Refresh.class); + + // ---------------------------- < keys > + /** keep changes key */ + private String keepChangesKey = "keepChanges"; + + /** path to the node to refresh key */ + private String pathKey = "path"; + + /** + * @return the keep changes key + */ + public String getKeepChangesKey() { + return keepChangesKey; + } + + /** + * @param keepChangesKey + * the keep changes key to set + */ + public void setKeepChangesKey(String keepChangesKey) { + this.keepChangesKey = keepChangesKey; + } + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + boolean keepChanges = Boolean.valueOf( + (String) ctx.get(this.keepChangesKey)).booleanValue(); + + String path = (String) ctx.get(this.pathKey); + + if (log.isDebugEnabled()) { + log.debug("refreshing. from node " + path); + } + + if (path == null) { + Session s = CommandHelper.getSession(ctx); + s.refresh(keepChanges); + } else { + Item i = CommandHelper.getItem(ctx, path); + i.refresh(keepChanges); + } + + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/RemoveItem.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/RemoveItem.java new file mode 100644 index 00000000000..da8fe553a6c --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/RemoveItem.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.standalone.cli.core; + +import javax.jcr.Item; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Remove the Item at the given path + */ +public class RemoveItem implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RemoveItem.class); + + // ---------------------------- < keys > + + /** context attribute key for the path attribute */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + if (log.isDebugEnabled()) { + log.debug("removing item from " + path); + } + Item item = CommandHelper.getItem(ctx, path); + if (item.isSame(CommandHelper.getCurrentNode(ctx)) + && item.getDepth() > 0) { + CommandHelper.setCurrentNode(ctx, item.getParent()); + } + item.remove(); + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/RemoveItems.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/RemoveItems.java new file mode 100644 index 00000000000..7a20b0f2852 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/RemoveItems.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.standalone.cli.core; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Item; +import javax.jcr.Node; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; +import org.apache.jackrabbit.util.ChildrenCollectorFilter; + +/** + * Remove any Item under the given Node that match + * the given name pattern. + */ +public class RemoveItems implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RemoveItems.class); + + // ---------------------------- < keys > + /** path key */ + private String pathKey = "path"; + + /** item pattern key */ + private String patternKey = "pattern"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String pattern = (String) ctx.get(this.patternKey); + String path = (String) ctx.get(this.pathKey); + + Node n = CommandHelper.getNode(ctx, path); + + if (log.isDebugEnabled()) { + log.debug("removing nodes from " + n.getPath() + + " that match pattern " + pattern); + } + + List children = new ArrayList(); + ChildrenCollectorFilter collector = new ChildrenCollectorFilter( + pattern, children, true, true, 1); + collector.visit(n); + + Iterator items = children.iterator(); + + while (items.hasNext()) { + Item item = (Item) items.next(); + if (item.isSame(CommandHelper.getCurrentNode(ctx)) + && item.getDepth() > 0) { + CommandHelper.setCurrentNode(ctx, item.getParent()); + } + item.remove(); + } + + return false; + } + + /** + * @return the pattern key + */ + public String getPatternKey() { + return patternKey; + } + + /** + * @param patternKey + * the pattern key to set + */ + public void setPatternKey(String patternKey) { + this.patternKey = patternKey; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Rename.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Rename.java new file mode 100644 index 00000000000..f13044ad800 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Rename.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.standalone.cli.core; + +import javax.jcr.Item; +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Rename a Node
        + * The persistent flag indicates whether to use the session or the workspace + * to perform the move command + */ +public class Rename implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Rename.class); + + // ---------------------------- < keys > + /** source path */ + private String srcPathKey = "srcPath"; + + /** destination path */ + private String destPathKey = "destPath"; + + /** persistent key */ + private String persistentKey = "persistent"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String from = (String) ctx.get(this.srcPathKey); + String to = (String) ctx.get(this.destPathKey); + + boolean persistent = Boolean.valueOf( + (String) ctx.get(this.persistentKey)).booleanValue(); + + if (log.isDebugEnabled()) { + log.debug("renaming node from " + from + " to " + to); + } + + Session s = CommandHelper.getSession(ctx); + Item itemFrom = CommandHelper.getItem(ctx, from); + if (itemFrom.getDepth() == 1) { + if (persistent) { + s.getWorkspace().move(itemFrom.getPath(), "/" + to); + } else { + s.move(itemFrom.getPath(), "/" + to); + } + } else { + if (persistent) { + s.getWorkspace().move(itemFrom.getPath(), + itemFrom.getParent().getPath() + "/" + to); + } else { + s.move(itemFrom.getPath(), itemFrom.getParent().getPath() + "/" + + to); + } + } + + return false; + } + + /** + * @return the destination path key + */ + public String getDestPathKey() { + return destPathKey; + } + + /** + * @param destPathKey + * the destination path key to set + */ + public void setDestPathKey(String destPathKey) { + this.destPathKey = destPathKey; + } + + /** + * @return the source path key + */ + public String getSrcPathKey() { + return srcPathKey; + } + + /** + * @param srcPathKey + * the source path key to set + */ + public void setSrcPathKey(String srcPathKey) { + this.srcPathKey = srcPathKey; + } + + public String getPersistentKey() { + return persistentKey; + } + + public void setPersistentKey(String persistentKey) { + this.persistentKey = persistentKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Save.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Save.java new file mode 100644 index 00000000000..288536f707e --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/Save.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.standalone.cli.core; + +import javax.jcr.Item; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Save the current working Item if specified, or the current + * working Session + */ +public class Save implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Save.class); + + // ---------------------------- < keys > + /** path key */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + + if (path == null) { + log.debug("saving session"); + CommandHelper.getSession(ctx).save(); + } else { + log.debug("saving node at " + path); + Item i = CommandHelper.getItem(ctx, path); + i.save(); + } + + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetBinaryProperty.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetBinaryProperty.java new file mode 100644 index 00000000000..fc3ab0617fe --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetBinaryProperty.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.standalone.cli.core; + +import java.io.InputStream; + +import javax.jcr.Node; + +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Set a binary Property + */ +public class SetBinaryProperty extends AbstractSetProperty { + /** logger */ + private static Log log = LogFactory.getLog(SetBinaryProperty.class); + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + InputStream value = (InputStream) ctx.get(this.valueKey); + String name = (String) ctx.get(this.nameKey); + String parent = (String) ctx.get(this.parentPathKey); + + Node node = CommandHelper.getNode(ctx, parent); + if (log.isDebugEnabled()) { + log.debug("setting property to node at " + node.getPath() + + ". property=" + name + " value=" + value); + } + + node.setProperty(name, value); + + return false; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetMultivalueProperty.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetMultivalueProperty.java new file mode 100644 index 00000000000..692281d31ea --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetMultivalueProperty.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.standalone.cli.core; + +import javax.jcr.Node; +import javax.jcr.PropertyType; + +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Set a multivalue Property to the current working + * Node.
        + * The default regular expression is ",". + */ +public class SetMultivalueProperty extends AbstractSetProperty { + /** logger */ + private static Log log = LogFactory.getLog(SetMultivalueProperty.class); + + // ---------------------------- < keys > + + /** regular expression key */ + private String regExpKey = "regExp"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String regExp = (String) ctx.get(this.regExpKey); + String value = (String) ctx.get(this.valueKey); + String name = (String) ctx.get(this.nameKey); + String type = (String) ctx.get(this.typeKey); + String parent = (String) ctx.get(this.parentPathKey); + + Node node = CommandHelper.getNode(ctx, parent); + + if (log.isDebugEnabled()) { + log.debug("setting multivalue property from node at " + + node.getPath() + ". regexp=" + regExp + " value=" + value + + " property=" + name); + } + + String[] values = value.split(regExp); + + if (type == null) { + node.setProperty(name, values); + } else { + node.setProperty(name, values, PropertyType.valueFromName(type)); + } + + return false; + } + + /** + * @return the regular expression key + */ + public String getRegExpKey() { + return regExpKey; + } + + /** + * @param regExpKey + * the regular expression key to set + */ + public void setRegExpKey(String regExpKey) { + this.regExpKey = regExpKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetProperty.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetProperty.java new file mode 100644 index 00000000000..17d10d3c3e4 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/core/SetProperty.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.standalone.cli.core; + +import javax.jcr.Node; +import javax.jcr.PropertyType; + +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Set a Property Value to the current working + * Node + */ +public class SetProperty extends AbstractSetProperty { + /** logger */ + private static Log log = LogFactory.getLog(SetProperty.class); + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String value = (String) ctx.get(this.valueKey); + String name = (String) ctx.get(this.nameKey); + String propertyType = (String) ctx.get(this.typeKey); + String parent = (String) ctx.get(this.parentPathKey); + + Node node = CommandHelper.getNode(ctx, parent); + if (log.isDebugEnabled()) { + log.debug("setting property to node at " + node.getPath() + + ". property=" + name + " value=" + value); + } + if (propertyType == null) { + node.setProperty(name, value); + } else { + node.setProperty(name, value, PropertyType + .valueFromName(propertyType)); + } + return false; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/ConnectToJNDIServer.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/ConnectToJNDIServer.java new file mode 100644 index 00000000000..d5c119042dd --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/ConnectToJNDIServer.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.standalone.cli.ext; + +import javax.jcr.Repository; +import javax.naming.InitialContext; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.rmi.client.ClientAdapterFactory; +import org.apache.jackrabbit.rmi.remote.RemoteRepository; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Connect to a JCR-RMI server + */ +public class ConnectToJNDIServer implements Command { + /** logger */ + private static Log log = LogFactory.getLog(ConnectToJNDIServer.class); + + // ---------------------------- < keys > + /** url key */ + private String urlKey = "url"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String url = (String) ctx.get(this.urlKey); + if (log.isDebugEnabled()) { + log.debug("connecting to jndi server at " + url); + } + InitialContext iCtx = new InitialContext(); + ClientAdapterFactory adapter = new ClientAdapterFactory(); + RemoteRepository remote = (RemoteRepository) iCtx.lookup(url); + Repository repo = adapter.getRepository(remote); + CommandHelper.setRepository(ctx, repo, "jndi " + url); + return false; + } + + /** + * @return the url key + */ + public String getUrlKey() { + return urlKey; + } + + /** + * @param urlKey + * the url key to set + */ + public void setUrlKey(String urlKey) { + this.urlKey = urlKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/ConnectToRmiServer.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/ConnectToRmiServer.java new file mode 100644 index 00000000000..b9c706cc75f --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/ConnectToRmiServer.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.standalone.cli.ext; + +import javax.jcr.Repository; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.rmi.client.ClientRepositoryFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Connect to a JCR-RMI server + */ +public class ConnectToRmiServer implements Command { + /** logger */ + private static Log log = LogFactory.getLog(ConnectToRmiServer.class); + + // ---------------------------- < keys > + /** url key */ + private String urlKey = "url"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String url = (String) ctx.get(this.urlKey); + if (log.isDebugEnabled()) { + log.debug("connecting to jcr-rmi server at " + url); + } + ClientRepositoryFactory factory = new ClientRepositoryFactory(); + Repository repository = factory.getRepository(url); + CommandHelper.setRepository(ctx, repository, "jcr-rmi " + url); + return false; + } + + /** + * @return the url key + */ + public String getUrlKey() { + return urlKey; + } + + /** + * @param urlKey + * the url key to set + */ + public void setUrlKey(String urlKey) { + this.urlKey = urlKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/CreateWorkspace.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/CreateWorkspace.java new file mode 100644 index 00000000000..a40c5512b00 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/CreateWorkspace.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.standalone.cli.ext; + +import java.util.ResourceBundle; + +import javax.jcr.Workspace; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.core.WorkspaceImpl; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Creates a Workspace.
        + * Note that this Command uses Jackrabbit specific API. + */ +public class CreateWorkspace implements Command { + /** bundle */ + private ResourceBundle bundle = CommandHelper.getBundle(); + + /** logger */ + private static Log log = LogFactory.getLog(CreateWorkspace.class); + + // ---------------------------- < keys > + /** workspace name key */ + private String nameKey = "name"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String name = (String) ctx.get(this.nameKey); + if (log.isDebugEnabled()) { + log.debug("creating workspace for name " + name); + } + Workspace w = CommandHelper.getSession(ctx).getWorkspace(); + if (!(w instanceof WorkspaceImpl)) { + throw new IllegalStateException(bundle + .getString("phrase.jackrabbit.command")); + } + + WorkspaceImpl jrw = (WorkspaceImpl) w; + jrw.createWorkspace(name); + return false; + } + + /** + * @return the name key + */ + public String getNameKey() { + return nameKey; + } + + /** + * @param nameKey + * the name key to set + */ + public void setNameKey(String nameKey) { + this.nameKey = nameKey; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StartJackrabbit.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StartJackrabbit.java new file mode 100644 index 00000000000..c9e7d15501d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StartJackrabbit.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.standalone.cli.ext; + +import javax.jcr.Repository; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Set a new Jackrabbit instance as the current working Repository + */ +public class StartJackrabbit implements Command { + /** logger */ + private static Log log = LogFactory.getLog(StartJackrabbit.class); + + /** config file */ + private String configKey = "config"; + + /** home folder */ + private String homeKey = "home"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String config = (String) ctx.get(this.configKey); + String home = (String) ctx.get(this.homeKey); + if (log.isDebugEnabled()) { + log + .debug("starting jackrabbit. config=" + config + " home=" + + home); + } + RepositoryConfig conf = RepositoryConfig.create(config, home); + Repository repo = RepositoryImpl.create(conf); + CommandHelper.setRepository(ctx, repo, "local " + home); + return false; + } + + /** + * @return the config key + */ + public String getConfigKey() { + return configKey; + } + + /** + * @param configKey + * the config key to set + */ + public void setConfigKey(String configKey) { + this.configKey = configKey; + } + + /** + * @return the home key + */ + public String getHomeKey() { + return homeKey; + } + + /** + * @param homeKey + * the home key to set + */ + public void setHomeKey(String homeKey) { + this.homeKey = homeKey; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StartJackrabbitSingleton.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StartJackrabbitSingleton.java new file mode 100644 index 00000000000..b34d344e534 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StartJackrabbitSingleton.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.standalone.cli.ext; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Repository; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + *

        + * Get a Jackrabbit instance and put it in the Context.
        + * This commands maintains a cache with already created instances.
        + * if there's no Repository for the given config and home settings a new + * instance will be created and cached. + *

        + */ +public class StartJackrabbitSingleton implements Command { + /** logger */ + private static Log log = LogFactory.getLog(StartJackrabbitSingleton.class); + + /** cache */ + private static Map cache = new HashMap(); + + /** config file */ + private String configKey = "config"; + + /** home folder */ + private String homeKey = "home"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String config = (String) ctx.get(this.configKey); + String home = (String) ctx.get(this.homeKey); + + if (log.isDebugEnabled()) { + log + .debug("starting jackrabbit. config=" + config + " home=" + + home); + } + + try { + synchronized (cache) { + String key = config + "@" + home; + Repository repo = (Repository) cache.get(key); + if (repo == null) { + String msg = "Starting Jakrabbit instance"; + log.info(msg); + RepositoryConfig conf = RepositoryConfig.create(config, + home); + repo = RepositoryImpl.create(conf); + cache.put(key, repo); + } + CommandHelper.setRepository(ctx, repo, "local singleton " + home); + } + } catch (Exception e) { + log.error("Unable to start jackrabbit", e); + throw e; + } + return false; + } + + /** + * @return the config key + */ + public String getConfigKey() { + return configKey; + } + + /** + * @param configKey + * the config key to set + */ + public void setConfigKey(String configKey) { + this.configKey = configKey; + } + + /** + * @return the home key + */ + public String getHomeKey() { + return homeKey; + } + + /** + * @param homeKey + * the home key to set + */ + public void setHomeKey(String homeKey) { + this.homeKey = homeKey; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StopJackrabbit.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StopJackrabbit.java new file mode 100644 index 00000000000..b41550eb2f9 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/ext/StopJackrabbit.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.standalone.cli.ext; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Stop Jackrabbit + */ +public class StopJackrabbit implements Command { + + /** logger */ + private static Log log = LogFactory.getLog(StopJackrabbit.class); + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + if (log.isDebugEnabled()) { + log.debug("stopping jackrabbit"); + } + RepositoryImpl repo = (RepositoryImpl) CommandHelper.getRepository(ctx); + if (repo == null) { + throw new IllegalStateException("No current working repository"); + } + repo.shutdown(); + CommandHelper.setRepository(ctx, null, null); + return false; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ExportFileSystem.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ExportFileSystem.java new file mode 100644 index 00000000000..7b83712c50d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ExportFileSystem.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli.fs; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Export a Node of type nt:file or nt:folder to the given file + * system path. + */ +public class ExportFileSystem implements Command { + /** logger */ + private static Log log = LogFactory.getLog(ExportFileSystem.class); + + // ---------------------------- < keys > + + /** Node path key */ + private String srcJcrPathKey = "srcJcrPath"; + + /** File system path key */ + private String destFsPathKey = "destFsPath"; + + /** Overwrite flag key */ + private String overwriteKey = "overwrite"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String from = (String) ctx.get(this.srcJcrPathKey); + String to = (String) ctx.get(this.destFsPathKey); + boolean overwrite = Boolean + .valueOf((String) ctx.get(this.overwriteKey)).booleanValue(); + + if (log.isDebugEnabled()) { + log.debug("exporting node at " + from + " to the filesystem (" + to + + ") overwrite=" + overwrite); + } + + Node node = CommandHelper.getNode(ctx, from); + + File f = new File(to); + + // check if the file exists + if (f.exists() && !overwrite) { + throw new CommandException("exception.file.exists", new String[] { + to + }); + } + + // export either a file or a folder + if (node.isNodeType("nt:file")) { + this.createFile(node, f); + } else if (node.isNodeType("nt:folder")) { + this.addFolder(node, f); + } else { + throw new CommandException("exception.not.file.or.folder", + new String[] { + node.getPrimaryNodeType().getName() + }); + } + + return false; + } + + /** + * Exports an nt:file to the file system + * @param node + * the Node + * @param file + * the File + * @throws IOException + * if an IOException occurs + * @throws CommandException + * if the File can't be created + * @throws ValueFormatException + * if a Value can't be retrieved + * @throws PathNotFoundException + * if the Node can't be found + * @throws RepositoryException + * if the current working Repository throws an + * Exception + */ + private void createFile(Node node, File file) throws IOException, + CommandException, ValueFormatException, PathNotFoundException, + RepositoryException { + + boolean created = file.createNewFile(); + if (!created) { + throw new CommandException("exception.file.not.created", + new String[] { + file.getPath() + }); + } + BufferedOutputStream out = new BufferedOutputStream( + new FileOutputStream(file)); + InputStream in = node.getNode("jcr:content").getProperty("jcr:data") + .getStream(); + + int c; + + while ((c = in.read()) != -1) { + out.write(c); + } + in.close(); + out.flush(); + out.close(); + } + + /** + * Exports a nt:folder and all its children to the file system + * @param node + * the Node + * @param file + * File + * @throws CommandException + * if the File can't be created + * @throws RepositoryException + * if the current working Repository throws an + * Exception + * @throws IOException + * if an IOException occurs + */ + private void addFolder(Node node, File file) throws CommandException, + RepositoryException, IOException { + boolean created = file.mkdir(); + + if (!created) { + throw new CommandException("exception.folder.not.created", + new String[] { + file.getPath() + }); + } + + NodeIterator iter = node.getNodes(); + while (iter.hasNext()) { + Node child = iter.nextNode(); + // File + if (child.isNodeType("nt:file")) { + File childFile = new File(file, child.getName()); + createFile(child, childFile); + } else if (child.isNodeType("nt:folder")) { + File childFolder = new File(file, child.getName()); + addFolder(child, childFolder); + } + } + } + + /** + * @return the overwrite key + */ + public String getOverwriteKey() { + return overwriteKey; + } + + /** + * @param overwriteKey + * the overwrite key to set + */ + public void setOverwriteKey(String overwriteKey) { + this.overwriteKey = overwriteKey; + } + + /** + * @return the source jcr path key + */ + public String getSrcJcrPathKey() { + return srcJcrPathKey; + } + + /** + * @param srcJcrPathKey + * the source jcr path key to set + */ + public void setSrcJcrPathKey(String srcJcrPathKey) { + this.srcJcrPathKey = srcJcrPathKey; + } + + /** + * @return the destination file system path key + */ + public String getDestFsPathKey() { + return destFsPathKey; + } + + /** + * @param toFsPathKey + * the destination file system path key to set + */ + public void setDestFsPathKey(String toFsPathKey) { + this.destFsPathKey = toFsPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ExportPropertyToFile.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ExportPropertyToFile.java new file mode 100644 index 00000000000..d4ae1ab41f2 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ExportPropertyToFile.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli.fs; + +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Exports a Property Value of the current working + * Node to the file system. + */ +public class ExportPropertyToFile implements Command { + /** logger */ + private static Log log = LogFactory.getLog(ExportPropertyToFile.class); + + // ---------------------------- < keys > + + /** property name */ + private String nameKey = "name"; + + /** value index */ + private String indexKey = "index"; + + /** target file */ + private String destFsPathKey = "destFsPath"; + + /** overwrite the target file if necessary */ + private String overwriteKey = "overwrite"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String name = (String) ctx.get(this.nameKey); + Integer index = (Integer) ctx.get(this.indexKey); + String to = (String) ctx.get(this.destFsPathKey); + + Node n = CommandHelper.getCurrentNode(ctx); + + if (log.isDebugEnabled()) { + log.debug("exporting property value from " + n.getPath() + "/" + + name + " to the filesystem: " + to); + } + + Property p = n.getProperty(name); + if (p.getDefinition().isMultiple()) { + exportValue(ctx, p.getValues()[index.intValue()], to); + } else { + exportValue(ctx, p.getValue(), to); + } + return false; + } + + /** + * Export th given value to a File + * @param ctx + * the Context + * @param value + * the Value + * @param to + * the target file system path + * @throws CommandException + * if the File already exists + * @throws IOException + * if an IOException occurs + * @throws RepositoryException + * if the current working Repository throws an + * Exception + */ + private void exportValue(Context ctx, Value value, String to) + throws CommandException, IOException, RepositoryException { + boolean overwrite = Boolean + .valueOf((String) ctx.get(this.overwriteKey)).booleanValue(); + + File file = new File(to); + + // Check if there's a file at the given target path + if (file.exists() && !overwrite) { + throw new CommandException("exception.file.exists", new String[] { + to + }); + } + + // If it doesn't exists create the file + if (!file.exists()) { + file.createNewFile(); + } + + if (value.getType() == PropertyType.BINARY) { + InputStream in = value.getStream(); + BufferedOutputStream out = new BufferedOutputStream( + new FileOutputStream(file)); + int c; + while ((c = in.read()) != -1) { + out.write(c); + } + in.close(); + out.flush(); + out.close(); + } else { + Reader in = new StringReader(value.getString()); + BufferedWriter out = new BufferedWriter(new FileWriter(file)); + int c; + while ((c = in.read()) != -1) { + out.write(c); + } + in.close(); + out.flush(); + out.close(); + } + } + + /** + * @return the index key + */ + public String getIndexKey() { + return indexKey; + } + + /** + * @param indexKey + * the index key to set + */ + public void setIndexKey(String indexKey) { + this.indexKey = indexKey; + } + + /** + * @return the name key + */ + public String getNameKey() { + return nameKey; + } + + /** + * @param nameKey + * the name key to set + */ + public void setNameKey(String nameKey) { + this.nameKey = nameKey; + } + + /** + * @return the overwrite key + */ + public String getOverwriteKey() { + return overwriteKey; + } + + /** + * @param overwriteKey + * the overwrite key to set + */ + public void setOverwriteKey(String overwriteKey) { + this.overwriteKey = overwriteKey; + } + + /** + * @return the destination file system path key + */ + public String getDestFsPathKey() { + return destFsPathKey; + } + + /** + * @param destFsPathKey + * the destination file system path key to set + */ + public void setDestFsPathKey(String destFsPathKey) { + this.destFsPathKey = destFsPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/FileToInputStream.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/FileToInputStream.java new file mode 100644 index 00000000000..3cc5ab4806d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/FileToInputStream.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.standalone.cli.fs; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Puts an java.io.InputStream in the context from an Fs path + * + */ +public class FileToInputStream implements Command { + + /** logger */ + private static Log log = LogFactory.getLog(FileToInputStream.class); + + // ---------------------------- < keys > + + /** file key */ + private String srcFsPathKey = "srcFsPath"; + + /** target context key */ + private String destKey = "dest"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String from = (String) ctx.get(this.srcFsPathKey); + File file = new File(from); + log.debug("putting " + file.getAbsolutePath() + " InputStream under " + + this.destKey); + InputStream is = new BufferedInputStream(new FileInputStream(file)); + ctx.put(this.destKey, is); + return false; + } + + public String getDestKey() { + return destKey; + } + + public void setDestKey(String destKey) { + this.destKey = destKey; + } + + public String getSrcFsPathKey() { + return srcFsPathKey; + } + + public void setSrcFsPathKey(String srcFsPathKey) { + this.srcFsPathKey = srcFsPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ImportFileSystem.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ImportFileSystem.java new file mode 100644 index 00000000000..6326b09b2c9 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/fs/ImportFileSystem.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli.fs; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Calendar; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.commons.JcrUtils; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; +import org.apache.tika.Tika; + +/** + * Import data from the file system.
        + * If the given path refers to a file it's imported to a Node of + * type nt:file under the current working Node.
        + * If the given path refers to a folder, the given folder and all the subtree is + * imported. + */ +public class ImportFileSystem implements Command { + /** logger */ + private static Log log = LogFactory.getLog(ImportFileSystem.class); + + /** Use Apache Tika to detect the media types of imported files */ + private static Tika tika = new Tika(); + + // ---------------------------- < keys > + + /** File system path key */ + private String srcFsPathKey = "srcFsPath"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String file = (String) ctx.get(this.srcFsPathKey); + Node parent = CommandHelper.getCurrentNode(ctx); + + if (log.isDebugEnabled()) { + log.debug("importing filesystem from " + file + " to node " + + parent); + } + + if (file == null) { + throw new CommandException("exception.fspath.is.null"); + } + + File f = new File(file); + + if (!f.exists()) { + throw new CommandException("exception.file.not.found", + new String[] { + file + }); + } + + if (f.isFile()) { + this.importFile(parent, f); + } else { + Node folder = parent.addNode(f.getName(), "nt:folder"); + this.importFolder(folder, f); + } + + return false; + } + + /** + * Imports a File. + * @param parentnode + * Parent Node + * @param file + * File to be imported + * @throws RepositoryException + * on Repository errors + * @throws IOException + * on io errors + */ + + private void importFile(Node parentnode, File file) + throws RepositoryException, IOException { + InputStream stream = new FileInputStream(file); + try { + Calendar date = Calendar.getInstance(); + date.setTimeInMillis(file.lastModified()); + JcrUtils.putFile( + parentnode, file.getName(), + tika.detect(file), stream, date); + } finally { + stream.close(); + } + } + + /** + * Import a Folder. + * @param parentnode + * Parent Node + * @param directory + * File system directory to traverse + * @throws RepositoryException + * on repository errors + * @throws IOException + * on io errors + */ + private void importFolder(Node parentnode, File directory) + throws RepositoryException, IOException { + File[] direntries = directory.listFiles(); + for (int i = 0; i < direntries.length; i++) { + File direntry = direntries[i]; + if (direntry.isDirectory()) { + Node childnode = + JcrUtils.getOrAddFolder(parentnode, direntry.getName()); + importFolder(childnode, direntry); + } else { + importFile(parentnode, direntry); + } + } + } + + /** + * @return the from key + */ + public String getSrcFsPathKey() { + return srcFsPathKey; + } + + /** + * @param fromKey + * the from key to set + */ + public void setSrcFsPathKey(String fromKey) { + this.srcFsPathKey = fromKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLs.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLs.java new file mode 100644 index 00000000000..2f0fc614e22 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLs.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.standalone.cli.info; + +import java.util.Iterator; +import java.util.ResourceBundle; + +import javax.jcr.NodeIterator; +import javax.jcr.PropertyIterator; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Ls superclass + */ +public abstract class AbstractLs implements Command { + /** bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** long width */ + protected int longWidth = 9; + + /** max items to list */ + private int defaultMaxItems = 100; + + /** max number of items */ + private String maxItemsKey = "maxItems"; + + /** show path flag */ + private boolean path; + + /** + * Print the footer + * @param ctx + * the Context + * @param iter + * the Iterator + */ + protected void printFooter(Context ctx, Iterator iter) { + CommandHelper.getOutput(ctx).println(); + CommandHelper.getOutput(ctx).println(bundle.getString("word.total")); + if (iter instanceof NodeIterator) { + printFooter(ctx, (NodeIterator) iter); + } else if (iter instanceof PropertyIterator) { + printFooter(ctx, (PropertyIterator) iter); + } + } + + /** + * Print footer + * @param ctx + * the Context + * @param iter + * the Iterator + */ + private void printFooter(Context ctx, NodeIterator iter) { + CommandHelper.getOutput(ctx).println( + iter.getSize() + " " + bundle.getString("word.nodes")); + } + + /** + * Print footer + * @param ctx + * the Context + * @param iter + * the Iterator + */ + private void printFooter(Context ctx, PropertyIterator iter) { + CommandHelper.getOutput(ctx).println( + iter.getSize() + " " + bundle.getString("word.properties")); + } + + /** + * @return the default max number of Items s to show + */ + public int getDefaultMaxItems() { + return defaultMaxItems; + } + + /** + * @param maxItems + * the default max number of Items s to set + */ + public void setDefaultMaxItems(int maxItems) { + this.defaultMaxItems = maxItems; + } + + /** + * @return the path + */ + public boolean isPath() { + return path; + } + + /** + * @param path + * the path to set + */ + public void setPath(boolean path) { + this.path = path; + } + + /** + * @return the max number of items key + */ + public String getMaxItemsKey() { + return maxItemsKey; + } + + /** + * @param maxItemsKey + * the max number of items key to set + */ + public void setMaxItemsKey(String maxItemsKey) { + this.maxItemsKey = maxItemsKey; + } + + /** + * @param ctx + * the Context + * @return the max number of Item s to show + */ + protected int getMaxItems(Context ctx) { + String maxItems = (String) ctx.get(this.maxItemsKey); + if (maxItems == null) { + maxItems = new Integer(this.defaultMaxItems).toString(); + } + return Integer.valueOf(maxItems).intValue(); + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsItems.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsItems.java new file mode 100644 index 00000000000..75a83c90441 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsItems.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.standalone.cli.info; + +import java.util.Iterator; +import java.util.ResourceBundle; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * List items superclass + */ +public abstract class AbstractLsItems extends AbstractLs { + /** bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** name width */ + private int nameWidth = 30; + + /** node type width */ + private int typeWidth = 15; + + /** + * @param ctx + * the Context + * @return Iterator containing the Items to list + * @throws CommandException + * if an errors occurs + * @throws RepositoryException + * if the current Repository throws a + * RepositoryException + */ + protected abstract Iterator getItems(Context ctx) throws CommandException, + RepositoryException; + + /** + * {@inheritDoc} + */ + public final boolean execute(Context ctx) throws Exception { + int nodes = 0; + int properties = 0; + + // header + int[] width = new int[] { + nameWidth, typeWidth, longWidth, longWidth, longWidth + }; + String[] header = new String[] { + bundle.getString("word.name"), bundle.getString("word.type"), + bundle.getString("word.node"), bundle.getString("word.new"), + bundle.getString("word.modified") + }; + + // print header + PrintHelper.printRow(ctx, width, header); + + // print separator + PrintHelper.printSeparatorRow(ctx, width, '-'); + + // nodes + Iterator iter = getItems(ctx); + + int index = 0; + + int maxItems = getMaxItems(ctx); + + // Print nodes + while (iter.hasNext() && index < maxItems) { + Item i = (Item) iter.next(); + + String type = null; + + // Show name or path + String name = null; + if (this.isPath()) { + name = i.getPath(); + } else { + name = i.getName(); + } + + if (i.isNode()) { + nodes++; + // name + Node n = (Node) i; + if (!isPath() && n.getIndex() > 1) { + name = n.getName() + "[" + n.getIndex() + "]"; + } + // type + type = n.getPrimaryNodeType().getName(); + } else { + properties++; + type = PropertyType.nameFromValue(((Property) i).getType()); + } + + PrintHelper.printRow(ctx, width, new String[] { + name, type, Boolean.toString(i.isNode()), + Boolean.valueOf(i.isNew()).toString(), + Boolean.valueOf(i.isModified()).toString() + }); + index++; + } + + // Footer + printFooter(ctx, iter); + + return false; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsNodes.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsNodes.java new file mode 100644 index 00000000000..d346896118f --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsNodes.java @@ -0,0 +1,783 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli.info; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.ResourceBundle; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * List nodes.
        + * The following attributes will be used in order to customize the output: + *
          + *
        • pathKey
        • + *
        • uuidKey
        • + *
        • mixinKey
        • + *
        • nodesSizeKey
        • + *
        • propertiesSizeKey
        • + *
        • referencesSizeKey
        • + *
        • versionableKey
        • + *
        • lockableKey
        • + *
        • referenceableKey
        • + *
        • lockedKey
        • + *
        • hasLockKey
        • + *
        • newKey
        • + *
        • modifiedKey
        • + *
        + */ +public abstract class AbstractLsNodes extends AbstractLs { + /** bundle */ + private static final ResourceBundle bundle = CommandHelper.getBundle(); + + /** show path */ + private String pathKey = "path"; + + /** show uuid */ + private String uuidKey = "uuid"; + + /** show mixin */ + private String mixinKey = "mixin"; + + /** show node size */ + private String nodesSizeKey = "nodeSize"; + + /** show properties size */ + private String propertiesSizeKey = "propertiesSize"; + + /** show references size */ + private String referencesSizeKey = "referencesSize"; + + /** show is versionable */ + private String versionableKey = "versionable"; + + /** show is lockable */ + private String lockableKey = "lockable"; + + /** show is referenceable */ + private String referenceableKey = "referenceable"; + + /** show is locked */ + private String lockedKey = "locked"; + + /** show has lock */ + private String hasLockKey = "hasLock"; + + /** show is new */ + private String newKey = "new"; + + /** show is modified */ + private String modifiedKey = "modified"; + + /** show lock tocken */ + private String lockTokenKey = "lockToken"; + + /** uuid width */ + private int uuidWidth = 36; + + /** path width */ + private int nameWidth = 30; + + /** node type width */ + private int nodeTypeWidth = 20; + + /** node type width */ + private int pathWidth = 40; + + /** referenceable width */ + private int mixinWidth = 30; + + /** + * {@inheritDoc} + */ + public final boolean execute(Context ctx) throws Exception { + OptionHolder oh = new OptionHolder(ctx); + + // Get children + Iterator iter = getNodes(ctx); + + // write header + writeHeader(ctx, oh); + + int index = 0; + + int maxItems = getMaxItems(ctx); + + // Write item + while (iter.hasNext() && index < maxItems) { + Node n = (Node) iter.next(); + writeItem(ctx, n, oh); + index++; + } + + // Write footer + printFooter(ctx, iter); + + return false; + } + + /** + * @param ctx + * the Context + * @return the Nodes to show + * @throws RepositoryException if the current working Repository throws a RepositoryException + * @throws CommandException + */ + protected abstract Iterator getNodes(Context ctx) throws CommandException, + RepositoryException; + + /** + * Write a node to the current output + * @param ctx + * the Context + * @param n + * the Node + * @throws RepositoryException + * @throws CommandException + */ + void writeItem(Context ctx, Node n, OptionHolder oh) + throws RepositoryException, CommandException { + // TODO do something with this long piece of code + Collection widths = new ArrayList(); + Collection texts = new ArrayList(); + + widths.add(new Integer(this.nameWidth)); + + String name = n.getName(); + if (n.getIndex() > 1) { + name += "[" + n.getIndex() + "]"; + } + texts.add(name); + + widths.add(new Integer(this.nodeTypeWidth)); + texts.add(n.getPrimaryNodeType().getName()); + + // uuid + if (oh.isUuid()) { + widths.add(new Integer(this.uuidWidth)); + if (n.isNodeType(JcrConstants.MIX_REFERENCEABLE)) { + texts.add(n.getUUID()); + } else { + texts.add(""); + } + } + + // is new + if (oh.isNew()) { + widths.add(new Integer(this.longWidth)); + texts.add(Boolean.toString(n.isNew())); + } + + // is new + if (oh.isModified()) { + widths.add(new Integer(this.longWidth)); + texts.add(Boolean.toString(n.isModified())); + } + + // mixin + if (oh.isMixin()) { + widths.add(new Integer(this.mixinWidth)); + Collection mixins = new ArrayList(); + // Assigned mixin types + NodeType[] assigned = n.getMixinNodeTypes(); + for (int i = 0; i < assigned.length; i++) { + mixins.add(assigned[i].getName()); + } + + // Inherited mixin types + NodeType[] nt = n.getPrimaryNodeType().getSupertypes(); + for (int i = 0; i < nt.length; i++) { + if (nt[i].isMixin()) { + mixins.add(nt[i].getName()); + } + } + texts.add(mixins); + } + + // node size + if (oh.isNodesSize()) { + widths.add(new Integer(this.longWidth)); + texts.add(Long.toString(n.getNodes().getSize())); + } + + // prop size + if (oh.isPropertiesSize()) { + widths.add(new Integer(this.longWidth)); + texts.add(Long.toString(n.getProperties().getSize())); + } + + // ref size + if (oh.isReferencesSize()) { + widths.add(new Integer(this.longWidth)); + texts.add(Long.toString(n.getReferences().getSize())); + } + + // is versionable + if (oh.isVersionable()) { + widths.add(new Integer(this.longWidth)); + texts.add(Boolean.toString(n + .isNodeType(JcrConstants.MIX_VERSIONABLE))); + } + + // is lockable + if (oh.isLockable()) { + widths.add(new Integer(this.longWidth)); + texts + .add(Boolean.toString(n.isNodeType(JcrConstants.MIX_LOCKABLE))); + } + + // is referenceable + if (oh.isReferenceable()) { + widths.add(new Integer(this.longWidth)); + texts.add(Boolean.toString(n + .isNodeType(JcrConstants.MIX_REFERENCEABLE))); + } + + // is locked + if (oh.isLocked()) { + widths.add(new Integer(this.longWidth)); + texts.add(Boolean.toString(n.isLocked())); + } + + // has lock + if (oh.isHasLock()) { + widths.add(new Integer(this.longWidth)); + texts.add(Boolean.toString(n.holdsLock())); + } + + // path + if (oh.isPath()) { + widths.add(new Integer(this.pathWidth)); + texts.add(n.getPath()); + } + + // lock token + if (oh.isLockToken()) { + widths.add(new Integer(this.nameWidth)); + if (n.isLocked()) { + texts.add(n.getLock().getLockToken()); + } else { + texts.add(""); + } + } + + PrintHelper.printRow(ctx, widths, texts); + } + + /** + * Prints the header + * @param ctx + * the Context + * @throws CommandException + */ + void writeHeader(Context ctx, OptionHolder oh) throws CommandException { + // TODO do something with this long piece of code + Collection widths = new ArrayList(); + Collection texts = new ArrayList(); + widths.add(new Integer(this.nameWidth)); + texts.add(bundle.getString("word.name")); + widths.add(new Integer(this.nodeTypeWidth)); + texts.add(bundle.getString("word.nodetype")); + + // uuid + if (oh.isUuid()) { + widths.add(new Integer(this.uuidWidth)); + texts.add("uuid"); + } + + // is new + if (oh.isNew()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.new")); + } + + // is new + if (oh.isModified()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.modified")); + } + + // mixin + if (oh.isMixin()) { + widths.add(new Integer(this.mixinWidth)); + texts.add("mixin"); + } + + // node size + if (oh.isNodesSize()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.nodes")); + } + + // prop size + if (oh.isPropertiesSize()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.properties")); + } + + // ref size + if (oh.isReferencesSize()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.references")); + } + + // is versionable + if (oh.isVersionable()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.versionable")); + } + + // is lockable + if (oh.isLockable()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.lockable")); + } + + // is referenceable + if (oh.isReferenceable()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.referenceable")); + } + + // is locked + if (oh.isLocked()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("word.locked")); + } + + // has lock + if (oh.isHasLock()) { + widths.add(new Integer(this.longWidth)); + texts.add(bundle.getString("phrase.haslock")); + } + + // path + if (oh.isPath()) { + widths.add(new Integer(this.pathWidth)); + texts.add(bundle.getString("word.path")); + } + + if (oh.isLockToken()) { + widths.add(new Integer(this.nameWidth)); + texts.add(bundle.getString("word.locktoken")); + } + + PrintHelper.printRow(ctx, widths, texts); + PrintHelper.printSeparatorRow(ctx, widths, '-'); + } + + /** + * option holder + */ + private class OptionHolder { + /** show path */ + private boolean path = false; + + /** show uuid */ + private boolean uuid = false; + + /** show mixin */ + private boolean mixin = false; + + /** show node size */ + private boolean nodesSize = false; + + /** show properties size */ + private boolean propertiesSize = false; + + /** show references size */ + private boolean referencesSize = false; + + /** show is versionable */ + private boolean versionable = false; + + /** show is lockable */ + private boolean lockable = false; + + /** show is referenceable */ + private boolean referenceable = false; + + /** show is locked */ + private boolean locked = false; + + /** show has lock */ + private boolean hasLock = false; + + /** show is new */ + private boolean new_ = false; + + /** show is modified */ + private boolean modified = false; + + /** lock tokeb */ + private boolean lockToken = false; + + /** context */ + private Context ctx; + + /** + * @param key + * the key the flag key + * @return the boolean value for the given key + */ + private boolean getFlag(String key) { + boolean flag = false; + if (ctx.containsKey(key)) { + flag = Boolean.valueOf((String) ctx.get(key)).booleanValue(); + } + return flag; + } + + /** + * Constructor + * @param ctx + * the Context + */ + public OptionHolder(Context ctx) { + super(); + this.ctx = ctx; + path = getFlag(pathKey); + uuid = getFlag(uuidKey); + mixin = getFlag(mixinKey); + nodesSize = getFlag(nodesSizeKey); + propertiesSize = getFlag(propertiesSizeKey); + referencesSize = getFlag(referencesSizeKey); + versionable = getFlag(versionableKey); + lockable = getFlag(lockableKey); + referenceable = getFlag(referenceableKey); + locked = getFlag(lockedKey); + hasLock = getFlag(hasLockKey); + new_ = getFlag(newKey); + modified = getFlag(modifiedKey); + lockToken = getFlag(lockTokenKey); + } + + /** + * @return the has lock + */ + public boolean isHasLock() { + return hasLock; + } + + /** + * @return Returns the lockable. + */ + public boolean isLockable() { + return lockable; + } + + /** + * @return Returns the locked. + */ + public boolean isLocked() { + return locked; + } + + /** + * @return Returns the mixin. + */ + public boolean isMixin() { + return mixin; + } + + /** + * @return Returns the modified. + */ + public boolean isModified() { + return modified; + } + + /** + * @return Returns the new_. + */ + public boolean isNew() { + return new_; + } + + /** + * @return Returns the nodesSize. + */ + public boolean isNodesSize() { + return nodesSize; + } + + /** + * @return Returns the propertiesSize. + */ + public boolean isPropertiesSize() { + return propertiesSize; + } + + /** + * @return Returns the referenceable. + */ + public boolean isReferenceable() { + return referenceable; + } + + /** + * @return Returns the referencesSize. + */ + public boolean isReferencesSize() { + return referencesSize; + } + + /** + * @return Returns the uuid. + */ + public boolean isUuid() { + return uuid; + } + + /** + * @return Returns the versionable. + */ + public boolean isVersionable() { + return versionable; + } + + /** + * @return Returns the path. + */ + public boolean isPath() { + return path; + } + + public boolean isLockToken() { + return lockToken; + } + } + + /** + * @return Returns the hasLockKey. + */ + public String getHasLockKey() { + return hasLockKey; + } + + /** + * @param hasLockKey + * The hasLockKey to set. + */ + public void setHasLockKey(String hasLockKey) { + this.hasLockKey = hasLockKey; + } + + /** + * @return Returns the lockableKey. + */ + public String getLockableKey() { + return lockableKey; + } + + /** + * @param lockableKey + * The lockableKey to set. + */ + public void setLockableKey(String lockableKey) { + this.lockableKey = lockableKey; + } + + /** + * @return Returns the lockedKey. + */ + public String getLockedKey() { + return lockedKey; + } + + /** + * @param lockedKey + * The lockedKey to set. + */ + public void setLockedKey(String lockedKey) { + this.lockedKey = lockedKey; + } + + /** + * @return Returns the mixinKey. + */ + public String getMixinKey() { + return mixinKey; + } + + /** + * @param mixinKey + * The mixinKey to set. + */ + public void setMixinKey(String mixinKey) { + this.mixinKey = mixinKey; + } + + /** + * @return Returns the modifiedKey. + */ + public String getModifiedKey() { + return modifiedKey; + } + + /** + * @param modifiedKey + * The modifiedKey to set. + */ + public void setModifiedKey(String modifiedKey) { + this.modifiedKey = modifiedKey; + } + + /** + * @return Returns the newKey. + */ + public String getNewKey() { + return newKey; + } + + /** + * @param newKey + * The new_Key to set. + */ + public void setNewKey(String newKey) { + this.newKey = newKey; + } + + /** + * @return Returns the nodesSizeKey. + */ + public String getNodesSizeKey() { + return nodesSizeKey; + } + + /** + * @param nodesSizeKey + * The nodesSizeKey to set. + */ + public void setNodesSizeKey(String nodesSizeKey) { + this.nodesSizeKey = nodesSizeKey; + } + + /** + * @return Returns the pathKey. + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * The pathKey to set. + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * @return Returns the propertiesSizeKey. + */ + public String getPropertiesSizeKey() { + return propertiesSizeKey; + } + + /** + * @param propertiesSizeKey + * The propertiesSizeKey to set. + */ + public void setPropertiesSizeKey(String propertiesSizeKey) { + this.propertiesSizeKey = propertiesSizeKey; + } + + /** + * @return Returns the referenceableKey. + */ + public String getReferenceableKey() { + return referenceableKey; + } + + /** + * @param referenceableKey + * The referenceableKey to set. + */ + public void setReferenceableKey(String referenceableKey) { + this.referenceableKey = referenceableKey; + } + + /** + * @return Returns the referencesSizeKey. + */ + public String getReferencesSizeKey() { + return referencesSizeKey; + } + + /** + * @param referencesSizeKey + * The referencesSizeKey to set. + */ + public void setReferencesSizeKey(String referencesSizeKey) { + this.referencesSizeKey = referencesSizeKey; + } + + /** + * @return Returns the uuidKey. + */ + public String getUuidKey() { + return uuidKey; + } + + /** + * @param uuidKey + * The uuidKey to set. + */ + public void setUuidKey(String uuidKey) { + this.uuidKey = uuidKey; + } + + /** + * @return Returns the versionableKey. + */ + public String getVersionableKey() { + return versionableKey; + } + + /** + * @param versionableKey + * The versionableKey to set. + */ + public void setVersionableKey(String versionableKey) { + this.versionableKey = versionableKey; + } + + /** + * @return the lock token key + */ + public String getLockTokenKey() { + return lockTokenKey; + } + + /** + * @param lockTokenKey + * the lock token to set + */ + public void setLockTokenKey(String lockTokenKey) { + this.lockTokenKey = lockTokenKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsProperties.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsProperties.java new file mode 100644 index 00000000000..e87a10a5a49 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/AbstractLsProperties.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli.info; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.ResourceBundle; + +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * List properties superclass + */ +public abstract class AbstractLsProperties extends AbstractLs { + + /** bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** length of length field */ + private static final int LENGTH_LENGTH = 8; + + /** + * {@inheritDoc} + */ + public final boolean execute(Context ctx) throws Exception { + int[] width = new int[] { + 30, longWidth, longWidth, LENGTH_LENGTH, 18 + }; + + String header[] = new String[] { + bundle.getString("word.name"), + bundle.getString("word.multiple"), + bundle.getString("word.type"), bundle.getString("word.length"), + bundle.getString("word.preview") + }; + + PrintHelper.printRow(ctx, width, header); + PrintHelper.printSeparatorRow(ctx, width, '-'); + + int index = 0; + Iterator iter = getProperties(ctx); + + int maxItems = getMaxItems(ctx); + + while (iter.hasNext() && index < maxItems) { + Property p = (Property) iter.next(); + + long length = 0; + + if (p.getDefinition().isMultiple()) { + long[] lengths = p.getLengths(); + for (int i = 0; i < lengths.length; i++) { + length += lengths[i]; + } + } else { + length = p.getLength(); + } + + String multiple = Boolean.toString(p.getDefinition().isMultiple()); + if (p.getDefinition().isMultiple()) { + multiple += "[" + p.getValues().length + "]"; + } + + Collection row = new ArrayList(); + row.add(p.getName()); + row.add(multiple); + row.add(PropertyType.nameFromValue(p.getType())); + row.add(Long.toString(length)); + // preview + if (p.getDefinition().isMultiple()) { + row.add(this.getMultiplePreview(p)); + } else { + row.add(this.getPreview(p)); + } + + PrintHelper.printRow(ctx, width, row); + index++; + } + + CommandHelper.getOutput(ctx).println(); + + // Write footer + printFooter(ctx, iter); + + return false; + } + + /** + * @param ctx + * the Context + * @return collected Property s to display + * @throws Exception + * if the Property s can't be retrieved + */ + protected abstract Iterator getProperties(Context ctx) throws Exception; + + /** + * @param property + * @return the first 50 characters of single value properties + * @throws RepositoryException + */ + private String getPreview(Property p) throws RepositoryException { + String value = p.getValue().getString(); + return value.substring(0, Math.min(value.length(), 50)); + } + + /** + * @param property + * @return a Collection in which element contains the first + * 50 characters of the Value's string + * representation + * @throws RepositoryException + * @throws ValueFormatException + */ + private Collection getMultiplePreview(Property p) + throws ValueFormatException, RepositoryException { + Collection c = new ArrayList(); + Value[] values = p.getValues(); + for (int i = 0; i < values.length; i++) { + try { + String value = values[i].getString(); + c.add(value.substring(0, Math.min(value.length(), 50))); + } catch (ValueFormatException e) { + c.add(bundle.getString("phrase.notavailable")); + } + } + return c; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Cat.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Cat.java new file mode 100644 index 00000000000..5a6685e30fd --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Cat.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.standalone.cli.info; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Display the content of a Property or a Node of + * type nt:file or nt:resource. + */ +public class Cat implements Command { + /** property name */ + private String pathKey = "path"; + + /** index. [optional] argument to display multivalue properties */ + private String indexKey = "index"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + Item item = CommandHelper.getItem(ctx, path); + if (item.isNode()) { + printNode(ctx, (Node) item); + } else { + printProperty(ctx, (Property) item); + } + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param path + * the path key to set + */ + public void setPathKey(String path) { + this.pathKey = path; + } + + /** + * @param ctx + * the Context + * @param n + * the Node + * @throws PathNotFoundException + * @throws CommandException + * @throws RepositoryException + * @throws IllegalStateException + * @throws IOException + */ + private void printNode(Context ctx, Node n) throws PathNotFoundException, + CommandException, RepositoryException, IllegalStateException, + IOException { + if (n.isNodeType("nt:file")) { + printValue(ctx, n.getNode("jcr:content").getProperty("jcr:data") + .getValue()); + } else if (n.isNodeType("nt:resource")) { + printValue(ctx, n.getProperty("jcr:data").getValue()); + } else { + throw new CommandException("exception.cat.unsupported.type", + new String[] { + n.getPrimaryNodeType().getName() + }); + } + } + + /** + * @param ctx + * the Context + * @param p + * the Property + * @throws CommandException + * @throws ValueFormatException + * @throws IllegalStateException + * @throws RepositoryException + * @throws IOException + */ + private void printProperty(Context ctx, Property p) + throws CommandException, ValueFormatException, + IllegalStateException, RepositoryException, IOException { + String indexStr = (String) ctx.get(this.indexKey); + int index = 0; + if (indexStr != null) { + index = Integer.parseInt(indexStr); + } + if (p.getDefinition().isMultiple()) { + printValue(ctx, p.getValues()[index]); + } else { + printValue(ctx, p.getValue()); + } + } + + /** + * Read the value + * @param ctx + * the Context + * @param value + * the Value + * @throws ValueFormatException + * @throws IllegalStateException + * @throws RepositoryException + * @throws IOException + */ + private void printValue(Context ctx, Value value) + throws ValueFormatException, IllegalStateException, + RepositoryException, IOException { + PrintWriter out = CommandHelper.getOutput(ctx); + out.println(); + BufferedReader in = new BufferedReader(new StringReader(value + .getString())); + String str = null; + while ((str = in.readLine()) != null) { + out.println(str); + } + } + + /** + * @return the index key + */ + public String getIndexKey() { + return indexKey; + } + + /** + * @param indexKey + * the index key to set + */ + public void setIndexKey(String indexKey) { + this.indexKey = indexKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Describe.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Describe.java new file mode 100644 index 00000000000..1b084fb84fb --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Describe.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.standalone.cli.info; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; + +/** + * Describes the given Node. + */ +public class Describe implements Command { + + /** + * {@inheritDoc} + */ + public boolean execute(Context arg0) throws Exception { + throw new UnsupportedOperationException(); + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Dump.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Dump.java new file mode 100644 index 00000000000..2fa64afaba2 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Dump.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.standalone.cli.info; + +import java.io.PrintWriter; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Dump stored data from the current working Node + */ +public class Dump implements Command { + /** root node to dump */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + PrintWriter out = CommandHelper.getOutput(ctx); + dump(out, CommandHelper.getNode(ctx, path)); + return false; + } + + /** + * Dumps the given Node to the given PrintWriter + * @param out + * the PrintWriter + * @param n + * the Node + * @throws RepositoryException + */ + public void dump(PrintWriter out, Node n) throws RepositoryException { + out.println(n.getPath()); + PropertyIterator pit = n.getProperties(); + while (pit.hasNext()) { + Property p = pit.nextProperty(); + out.print(p.getPath() + "="); + if (p.getDefinition().isMultiple()) { + Value[] values = p.getValues(); + for (int i = 0; i < values.length; i++) { + if (i > 0) + out.println(","); + out.println(values[i].getString()); + } + } else { + out.print(p.getString()); + } + out.println(); + } + NodeIterator nit = n.getNodes(); + while (nit.hasNext()) { + Node cn = nit.nextNode(); + dump(out, cn); + } + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Help.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Help.java new file mode 100644 index 00000000000..01bd470697c --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Help.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.standalone.cli.info; + +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Iterator; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.cli.HelpFormatter; +import org.apache.jackrabbit.standalone.cli.AbstractParameter; +import org.apache.jackrabbit.standalone.cli.Argument; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; +import org.apache.jackrabbit.standalone.cli.CommandLine; +import org.apache.jackrabbit.standalone.cli.CommandLineFactory; +import org.apache.jackrabbit.standalone.cli.Flag; +import org.apache.jackrabbit.standalone.cli.Option; + +/** + * Show available Commands. If a Command is + * specified it will show its description, usage and parameters. + */ +public class Help implements Command { + /** bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** Command factory */ + private CommandLineFactory factory = CommandLineFactory.getInstance(); + + /** Help formatter */ + private HelpFormatter hf = new HelpFormatter(); + + /** command key */ + private String commandKey = "command"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String command = (String) ctx.get(this.commandKey); + PrintWriter out = CommandHelper.getOutput(ctx); + out.println(); + if (command == null) { + helpAll(ctx); + } else { + helpCommand(ctx); + } + return false; + } + + /** + * Writes help for all the commands + * @param ctx + * the current working Context + * @throws CommandException + */ + private void helpAll(Context ctx) throws CommandException { + PrintWriter out = CommandHelper.getOutput(ctx); + Collection descriptors = factory.getCommandLines(); + Iterator iter = descriptors.iterator(); + + // Tab position + int tabPos = 20; + while (iter.hasNext()) { + CommandLine desc = (CommandLine) iter.next(); + if (desc.getName().length() > tabPos) { + tabPos = desc.getName().length() + 1; + } + } + + iter = descriptors.iterator(); + while (iter.hasNext()) { + CommandLine desc = (CommandLine) iter.next(); + StringBuffer buf = new StringBuffer(desc.getName()); + buf.setLength(tabPos); + for (int i = desc.getName().length(); i < buf.length(); i++) { + buf.setCharAt(i, ' '); + } + buf.append(desc.getLocalizedDescription()); + hf.printWrapped(out, 70, tabPos, buf.toString()); + } + } + + /** + * Writes detailed help for the given command + * @param ctx + * the current working Context + * @throws CommandException + */ + private void helpCommand(Context ctx) throws CommandException { + PrintWriter out = CommandHelper.getOutput(ctx); + + String cmdName = (String) ctx.get(this.commandKey); + + CommandLine desc = factory.getCommandLine(cmdName); + + out.println(getString(bundle, "word.description") + ": "); + out.println(desc.getLocalizedDescription()); + out.println(); + + // Usage + out.print(getString(bundle, "word.usage") + ":"); + out.print(desc.getName() + " "); + + // Arguments + Iterator iter = desc.getArguments().values().iterator(); + while (iter.hasNext()) { + Argument arg = (Argument) iter.next(); + out.print("<" + arg.getLocalizedArgName() + "> "); + } + + // Options + iter = desc.getOptions().values().iterator(); + while (iter.hasNext()) { + Option arg = (Option) iter.next(); + out.print("-" + arg.getName() + " <" + arg.getLocalizedArgName() + + "> "); + } + + // flags + iter = desc.getFlags().values().iterator(); + while (iter.hasNext()) { + Flag arg = (Flag) iter.next(); + out.print("-" + arg.getName() + " "); + } + out.println(); + + // Alias + if (desc.getAlias().size() > 0) { + out.print(getString(bundle, "word.alias") + ":"); + iter = desc.getAlias().iterator(); + while (iter.hasNext()) { + out.print((String) iter.next() + " "); + + } + out.println(); + } + out.println(); + + // Arguments details + if (desc.getArguments().size() > 0) { + out.println("<" + getString(bundle, "word.arguments") + ">"); + printParam(ctx, desc.getArguments().values()); + } + + // Options details + if (desc.getOptions().values().size() > 0) { + out.println(); + out.println("<" + getString(bundle, "word.options") + ">"); + printParam(ctx, desc.getOptions().values()); + } + + // flag details + if (desc.getFlags().values().size() > 0) { + out.println(); + out.println("<" + getString(bundle, "word.flags") + ">"); + printParam(ctx, desc.getFlags().values()); + } + + } + + /** + * @param ctx + * the current working Context + * @param params + * the parameters + */ + private void printParam(Context ctx, Collection params) { + int[] width = new int[] { + 10, 10, 10, 40 + }; + + String[] header = new String[] { + getString(bundle, "word.name"), + getString(bundle, "word.argument"), + getString(bundle, "word.required"), + getString(bundle, "word.description") + }; + + PrintHelper.printRow(ctx, width, header); + PrintHelper.printSeparatorRow(ctx, width, '-'); + + Iterator iter = params.iterator(); + while (iter.hasNext()) { + AbstractParameter p = (AbstractParameter) iter.next(); + String[] item = new String[] { + p.getName(), p.getLocalizedArgName(), + Boolean.toString(p.isRequired()), + p.getLocalizedDescription() + }; + PrintHelper.printRow(ctx, width, item); + } + + } + + /** + * @return the command key + */ + public String getCommandKey() { + return commandKey; + } + + /** + * @param commandKey + * the command key to set + */ + public void setCommandKey(String commandKey) { + this.commandKey = commandKey; + } + + private String getString(ResourceBundle bundle, String key) { + try { + return bundle.getString(key) ; + } catch (MissingResourceException e) { + return "not available"; + } + + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Info.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Info.java new file mode 100644 index 00000000000..9485511733d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/Info.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.standalone.cli.info; + +import java.io.PrintWriter; + +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +public class Info implements Command { + + public boolean execute(Context ctx) throws Exception { + PrintWriter out = CommandHelper.getOutput(ctx); + + out.println(); + + try { + CommandHelper.getRepository(ctx); + } catch (CommandException e) { + out.println("No connection to a repository."); + return false; + } + + out.println("Repository: " + CommandHelper.getRepositoryAddress(ctx)); + + Session session; + String currentPath; + try { + session = CommandHelper.getSession(ctx); + currentPath = CommandHelper.getCurrentNode(ctx).getPath(); + } catch (CommandException e) { + out.println("Not logged in / no session."); + return false; + } + + out.println("User : " + session.getUserID()); + out.println("Workspace : " + session.getWorkspace().getName()); + out.println("Node : " + currentPath); + + out.println(); + + if (session.isLive()) { + out.println("Session is live."); + } else { + out.println("Session is not live."); + } + + if (session.hasPendingChanges()) { + out.println("Session has pending changes."); + } else { + out.println("Session has no changes."); + } + + return false; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/JcrInfoCommandException.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/JcrInfoCommandException.java new file mode 100644 index 00000000000..a5b0ccb2ba1 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/JcrInfoCommandException.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.standalone.cli.info; + +import org.apache.jackrabbit.standalone.cli.CommandException; + +/** + * Exception thrown by Info Commands + */ +public class JcrInfoCommandException extends CommandException { + /** + * Comment for serialVersionUID + */ + private static final long serialVersionUID = 3257854259679866933L; + + /** + * @param message + * the message + */ + public JcrInfoCommandException(String message) { + super(message); + } + + /** + * @param message + * the message + * @param arguments + * the arguments + */ + public JcrInfoCommandException(String message, Object[] arguments) { + super(message, arguments); + } + + /** + * @param message + * the message + * @param cause + * the cause + */ + public JcrInfoCommandException(String message, Throwable cause) { + super(message, cause); + } + + /** + * @param message + * the message + * @param cause + * the cause + * @param arguments + * the arguments + */ + public JcrInfoCommandException(String message, Throwable cause, + Object[] arguments) { + super(message, cause, arguments); + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedItems.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedItems.java new file mode 100644 index 00000000000..59701213a75 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedItems.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.standalone.cli.info; + +import java.util.Iterator; + +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; + +/** + * Lists collected Items.
        + * This Command looks for an Iterator under the + * given Context variable and lists its Items. + */ +public class LsCollectedItems extends AbstractLsItems { + /** Context variable that holds the Iterator */ + private String fromKey = "collected"; + + /** + * @return the context variable + */ + public String getFromKey() { + return fromKey; + } + + /** + * Sets the context variable + * @param from + * from key to set + */ + public void setFromKey(String from) { + this.fromKey = from; + } + + /** + * {@inheritDoc} + */ + protected Iterator getItems(Context ctx) throws CommandException, + RepositoryException { + // Always show the path + this.setPath(true); + Object o = ctx.get(this.fromKey); + if (o == null || !(o instanceof Iterator)) { + throw new JcrInfoCommandException( + "illegalargument.no.iterator.under", new String[] { + fromKey + }); + } + return (Iterator) o; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedNodes.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedNodes.java new file mode 100644 index 00000000000..300d915918b --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedNodes.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.standalone.cli.info; + +import java.util.Iterator; + +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; + +/** + * Lists collected Nodes.
        + * This Command looks for an Iterator under the + * given Context variable and lists its Nodes. + */ +public class LsCollectedNodes extends AbstractLsNodes { + /** Context variable that holds the Iterator */ + private String fromKey = "collected"; + + /** + * @return the context variable + */ + public String getFromKey() { + return fromKey; + } + + /** + * Sets the Context variable + * @param contextVariable + * the context variable + */ + public void setFromKey(String contextVariable) { + this.fromKey = contextVariable; + } + + /** + * {@inheritDoc} + */ + protected Iterator getNodes(Context ctx) throws CommandException, + RepositoryException { + this.setPath(true); + Object o = ctx.get(this.fromKey); + if (o == null || !(o instanceof Iterator)) { + throw new JcrInfoCommandException( + "illegalargument.no.iterator.under", new String[] { + fromKey + }); + } + return (Iterator) o; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedProperties.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedProperties.java new file mode 100644 index 00000000000..bc1cd0bb1c3 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsCollectedProperties.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.standalone.cli.info; + +import java.util.Iterator; + +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; + +/** + * Lists collected Propertys.
        + * This Command looks for an Iterator under the + * given Context variable and lists its Propertys. + */ +public class LsCollectedProperties extends AbstractLsProperties { + /** Context variable that holds the Iterator */ + private String fromKey = "collected"; + + /** + * @return the from key + */ + public String getFromKey() { + return fromKey; + } + + /** + * @param fromKey + * from key to set + */ + public void setFromKey(String fromKey) { + this.fromKey = fromKey; + } + + /** + * {@inheritDoc} + */ + protected Iterator getProperties(Context ctx) throws CommandException, + RepositoryException { + // show the path + this.setPath(true); + Object o = ctx.get(fromKey); + if (o == null || !(o instanceof Iterator)) { + throw new JcrInfoCommandException( + "illegalargument.no.iterator.under", new String[] { + fromKey + }); + } + return (Iterator) o; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsItems.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsItems.java new file mode 100644 index 00000000000..da38500c80f --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsItems.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.standalone.cli.info; + +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * List items + *
          + *
        • name
        • + *
        • type
        • + *
        • isNode
        • + *
        • isNew
        • + *
        • isModified
        • + *
        + */ +public class LsItems extends AbstractLsItems { + + /** name pattern key */ + private String patternKey = "pattern"; + + /** + * @return the name pattern + */ + public String getPatternKey() { + return patternKey; + } + + /** + * @param pattern + * the pattern + */ + public void setPatternKey(String pattern) { + this.patternKey = pattern; + } + + /** + * {@inheritDoc} + */ + protected Iterator getItems(Context ctx) throws CommandException, + RepositoryException { + String pattern = (String) ctx.get(this.patternKey); + Node n = CommandHelper.getCurrentNode(ctx); + return CommandHelper.getItems(ctx, n, pattern); + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsNamespaces.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsNamespaces.java new file mode 100644 index 00000000000..5eeb763bb15 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsNamespaces.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.standalone.cli.info; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; + +/** + * Lists the registered namespaces. + */ +public class LsNamespaces implements Command { + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + throw new UnsupportedOperationException(); + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsNodes.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsNodes.java new file mode 100644 index 00000000000..b4925aee9f2 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsNodes.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.standalone.cli.info; + +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * List the Nodes under the current working Node + * that match the given pattern. + */ +public class LsNodes extends AbstractLsNodes { + + /** name pattern key */ + private String patternKey = "pattern"; + + /** + * {@inheritDoc} + */ + protected Iterator getNodes(Context ctx) throws CommandException, + RepositoryException { + String pattern = (String) ctx.get(this.patternKey); + Node n = CommandHelper.getCurrentNode(ctx); + return CommandHelper.getNodes(ctx, n, pattern); + } + + /** + * @return the pattern key + */ + public String getPatternKey() { + return patternKey; + } + + /** + * @param patternKey + * the pattern key to set + */ + public void setPatternKey(String patternKey) { + this.patternKey = patternKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsProperties.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsProperties.java new file mode 100644 index 00000000000..748ad2763a9 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsProperties.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.standalone.cli.info; + +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Property fields: + *
          + *
        • name
        • + *
        • multiple
        • + *
        • type
        • + *
        • length
        • + *
        + */ +public class LsProperties extends AbstractLsProperties { + + /** property name pattern key */ + private String patternKey = "pattern"; + + /** + * @return name pattern + */ + public String getPatternKey() { + return patternKey; + } + + /** + * Sets the name pattern + * @param pattern the pattern key + */ + public void setPatternKey(String pattern) { + this.patternKey = pattern; + } + + /** + * {@inheritDoc} + */ + protected Iterator getProperties(Context ctx) throws CommandException, + RepositoryException { + String pattern = (String) ctx.get(this.patternKey); + Node n = CommandHelper.getCurrentNode(ctx); + return CommandHelper.getProperties(ctx, n, pattern); + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsReferences.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsReferences.java new file mode 100644 index 00000000000..42c54745e36 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsReferences.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.standalone.cli.info; + +import java.util.ResourceBundle; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Displays references to the given Node + */ +public class LsReferences implements Command { + /** bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** path to the Node key */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + Node n = CommandHelper.getNode(ctx, path); + + // header + int[] width = new int[] { + 60 + }; + String[] header = new String[] { + bundle.getString("word.path") + }; + + // print header + PrintHelper.printRow(ctx, width, header); + + // print separator + PrintHelper.printSeparatorRow(ctx, width, '-'); + + PropertyIterator iter = n.getReferences(); + while (iter.hasNext()) { + Property p = iter.nextProperty(); + // print header + PrintHelper.printRow(ctx, width, new String[] { + p.getPath() + }); + } + + CommandHelper.getOutput(ctx).println(); + CommandHelper.getOutput(ctx).println( + iter.getSize() + " " + bundle.getString("word.references")); + + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param path + * the path key to set + */ + public void setPathKey(String path) { + this.pathKey = path; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsVersions.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsVersions.java new file mode 100644 index 00000000000..c18e2e9a3c6 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/LsVersions.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.standalone.cli.info; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.ResourceBundle; + +import javax.jcr.Node; +import javax.jcr.version.Version; +import javax.jcr.version.VersionIterator; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * List the Version s in the VersionHistory. + */ +public class LsVersions implements Command { + /** bundle */ + private static ResourceBundle bundle = CommandHelper.getBundle(); + + /** path to the node */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + Node n = CommandHelper.getNode(ctx, path); + + // header + int[] width = new int[] { + 20, 50 + }; + String[] header = new String[] { + bundle.getString("word.version"), + bundle.getString("word.labels") + }; + // print header + PrintHelper.printRow(ctx, width, header); + // print separator + PrintHelper.printSeparatorRow(ctx, width, '-'); + VersionIterator iter = n.getVersionHistory().getAllVersions(); + while (iter.hasNext()) { + Version v = iter.nextVersion(); + Collection row = new ArrayList(); + row.add(v.getName()); + row.add(Arrays.asList(n.getVersionHistory().getVersionLabels(v))); + PrintHelper.printRow(ctx, width, row); + } + + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/PrintHelper.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/PrintHelper.java new file mode 100644 index 00000000000..36a3c9c9031 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/info/PrintHelper.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli.info; + +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Iterator; + +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Utility class for printing + */ +final class PrintHelper { + + /** + * private constructor + */ + private PrintHelper() { + super(); + } + + /** + * @param ctx + * the Context + * @param width + * the columns width + * @param text + * the text + */ + public static void printRow(Context ctx, int[] width, String[] text) { + if (width.length != text.length) { + throw new IllegalArgumentException( + "width[] and text[] haven't the same length"); + } + + PrintWriter out = CommandHelper.getOutput(ctx); + + int rows = 1; + + // Calculate rows + for (int i = 0; i < text.length; i++) { + int textLength = text[i].length(); + if (textLength == 0) { + textLength = 1; + } + int columnWidth = width[i]; + int neededRows = (int) Math.ceil((double) textLength + / (double) columnWidth); + if (neededRows > rows) { + rows = neededRows; + } + } + + // Write table + for (int row = 0; row < rows; row++) { + for (int column = 0; column < width.length; column++) { + for (int pointer = 0; pointer < width[column]; pointer++) { + int pos = row * width[column] + pointer; + if (pos < text[column].length()) { + out.print(text[column].charAt(pos)); + } else { + out.print(' '); + } + } + out.print(' '); + } + out.println(); + } + } + + /** + * @param ctx + * the Context + * @param width + * the column width + * @param separator + * the separator chr + */ + public static void printSeparatorRow( + Context ctx, + int[] width, + char separator) { + PrintWriter out = CommandHelper.getOutput(ctx); + for (int i = 0; i < width.length; i++) { + for (int j = 0; j <= width[i]; j++) { + if (j < width[i]) { + out.print(separator); + } else { + out.print(' '); + } + } + } + out.println(); + } + + /** + * @param ctx + * the Context + * @param width + * the column width + * @param texts + * the texts + * @throws CommandException + */ + public static void printRow(Context ctx, int[] width, Collection texts) + throws CommandException { + String[] text = new String[width.length]; + Iterator iter = texts.iterator(); + int column = 0; + while (iter.hasNext()) { + Object o = iter.next(); + if (o == null) { + text[column] = ""; + } else if (o instanceof String) { + text[column] = (String) o; + } else if (o instanceof Collection) { + StringBuffer sb = new StringBuffer(); + Iterator i = ((Collection) o).iterator(); + while (i.hasNext()) { + String str = (String) i.next(); + int rows = (int) Math.ceil((double) str.length() + / (double) width[column]); + if (rows == 0) { + rows = 1; + } + sb.append(str); + for (int j = 0; j < rows * width[column] - str.length(); j++) { + sb.append(' '); + } + } + text[column] = sb.toString(); + } else { + throw new CommandException("exception.illegalargument"); + } + column++; + } + printRow(ctx, width, text); + } + + /** + * @param ctx + * the Context + * @param widths + * the column width + * @param texts + * the texts + * @throws CommandException + */ + public static void printRow(Context ctx, Collection widths, Collection texts) + throws CommandException { + printRow(ctx, convertWidth(widths), texts); + } + + /** + * @param widths + * the column width + * @return the column width + * @throws CommandException + */ + private static int[] convertWidth(Collection widths) + throws CommandException { + int[] width = new int[widths.size()]; + int index = 0; + Iterator iter = widths.iterator(); + while (iter.hasNext()) { + Integer i = (Integer) iter.next(); + width[index] = i.intValue(); + index++; + } + return width; + } + + /** + * @param ctx + * the Context + * @param widths + * the columns widths + * @param separator + * the separator char + * @throws CommandException + */ + public static void printSeparatorRow( + Context ctx, + Collection widths, + char separator) throws CommandException { + printSeparatorRow(ctx, convertWidth(widths), separator); + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/AddLockToken.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/AddLockToken.java new file mode 100644 index 00000000000..76bbf3819c6 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/AddLockToken.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.standalone.cli.lock; + +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Add the given Lock token to the current Session + */ +public class AddLockToken implements Command { + /** logger */ + private static Log log = LogFactory.getLog(AddLockToken.class); + + // ---------------------------- < keys > + /** + * token + */ + private String tokenKey = "token"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String token = (String) ctx.get(this.tokenKey); + if (log.isDebugEnabled()) { + log + .debug("Adding lock token " + token + + " to the current session."); + } + Session s = CommandHelper.getSession(ctx); + s.addLockToken(token); + return false; + } + + /** + * @return the token key + */ + public String getTokenKey() { + return tokenKey; + } + + /** + * @param tokenKey + * the token key to set + */ + public void setTokenKey(String tokenKey) { + this.tokenKey = tokenKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/Lock.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/Lock.java new file mode 100644 index 00000000000..7c73e294a1b --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/Lock.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.standalone.cli.lock; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Lock the given Node + */ +public class Lock implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Lock.class); + + // ---------------------------- < keys > + /** Node path key */ + private String pathKey = "path"; + + /** + * depth lock + */ + private String deepKey = "deep"; + + /** + * Session scoped lock
        + * Key that refers to a Boolean context variable + */ + private String sessionScopedKey = "sessionScoped"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + boolean deep = Boolean.valueOf((String) ctx.get(this.deepKey)) + .booleanValue(); + boolean sessionScoped = Boolean.valueOf( + (String) ctx.get(this.sessionScopedKey)).booleanValue(); + if (log.isDebugEnabled()) { + log.debug("locking node at " + path + " deep=" + deep + + " sessionScoped=" + sessionScoped); + } + CommandHelper.getNode(ctx, path).lock(deep, sessionScoped); + return false; + } + + /** + * @return deep key + */ + public String getDeepKey() { + return deepKey; + } + + /** + * @param deepKey + * deep key to set + */ + public void setDeepKey(String deepKey) { + this.deepKey = deepKey; + } + + /** + * @return the session scoped key + */ + public String getSessionScopedKey() { + return sessionScopedKey; + } + + /** + * @param sessionScopedKey + * the session scoped key to set + */ + public void setSessionScopedKey(String sessionScopedKey) { + this.sessionScopedKey = sessionScopedKey; + } + + /** + * @return the source path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param srcPathKey + * the source path key to set + */ + public void setPathKey(String srcPathKey) { + this.pathKey = srcPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/RefreshLock.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/RefreshLock.java new file mode 100644 index 00000000000..660788753b1 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/RefreshLock.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.standalone.cli.lock; + +import javax.jcr.Node; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Reset the Lock timer + */ +public class RefreshLock implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RefreshLock.class); + + // ---------------------------- < keys > + /** Node path key */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + if (log.isDebugEnabled()) { + log.debug("refreshing lock at " + path); + } + Node n = CommandHelper.getNode(ctx, path); + n.getLock().refresh(); + return false; + } + + /** + * @return the source path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param srcPathKey + * the source path key to set + */ + public void setPathKey(String srcPathKey) { + this.pathKey = srcPathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/RemoveLockToken.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/RemoveLockToken.java new file mode 100644 index 00000000000..ab7db8a591a --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/RemoveLockToken.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.standalone.cli.lock; + +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Remove the given Lock token to the current + * Session + */ +public class RemoveLockToken implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RemoveLockToken.class); + + // ---------------------------- < keys > + /** + * token key + */ + private String tokenKey = "token"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String token = (String) ctx.get(this.tokenKey); + if (log.isDebugEnabled()) { + log.debug("Removing lock token " + token + + " from the current session."); + } + Session s = CommandHelper.getSession(ctx); + s.removeLockToken(token); + return false; + } + + /** + * @return the token key + */ + public String getTokenKey() { + return tokenKey; + } + + /** + * @param tokenKey + * the token key to set + */ + public void setTokenKey(String tokenKey) { + this.tokenKey = tokenKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/Unlock.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/Unlock.java new file mode 100644 index 00000000000..1c7b1d4aa20 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/lock/Unlock.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.standalone.cli.lock; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Unlock the current working Node + */ +public class Unlock implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Unlock.class); + + // ---------------------------- < keys > + /** Node path key */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + if (log.isDebugEnabled()) { + log.debug("Unlocking node at " + path); + } + CommandHelper.getNode(ctx, path).unlock(); + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/mixin/AddMixin.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/mixin/AddMixin.java new file mode 100644 index 00000000000..987ec27a430 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/mixin/AddMixin.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.standalone.cli.mixin; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Add a mixin to the given Node + */ +public class AddMixin implements Command { + /** logger */ + private static Log log = LogFactory.getLog(AddMixin.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** mixin name */ + private String mixinKey = "mixin"; + + /** + * @return Returns the mixinNameKey. + */ + public String getMixinKey() { + return mixinKey; + } + + /** + * @param mixinNameKey + * The mixinNameKey to set. + */ + public void setMixinKey(String mixinNameKey) { + this.mixinKey = mixinNameKey; + } + + /** + * @return Returns the pathKey. + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * The pathKey to set. + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String mixin = (String) ctx.get(this.mixinKey); + if (log.isDebugEnabled()) { + log.debug("adding mixin " + mixin + " to node " + path); + } + CommandHelper.getNode(ctx, path).addMixin(mixin); + return false; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/mixin/RemoveMixin.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/mixin/RemoveMixin.java new file mode 100644 index 00000000000..636c61c2c79 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/mixin/RemoveMixin.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.standalone.cli.mixin; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Remove a mixin from the given Node + */ +public class RemoveMixin implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RemoveMixin.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** mixin name */ + private String mixinKey = "mixin"; + + /** + * @return the mixin name key + */ + public String getMixinKey() { + return mixinKey; + } + + /** + * @param mixinNameKey + * the mixin name key to set + */ + public void setMixinKey(String mixinNameKey) { + this.mixinKey = mixinNameKey; + } + + /** + * @return he path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String mixin = (String) ctx.get(this.mixinKey); + if (log.isDebugEnabled()) { + log.debug("removing mixin " + mixin + " from node " + path); + } + CommandHelper.getNode(ctx, path).removeMixin(mixin); + return false; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/RegisterNamespace.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/RegisterNamespace.java new file mode 100644 index 00000000000..627c82fb28f --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/RegisterNamespace.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.standalone.cli.namespace; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Register a namespace + */ +public class RegisterNamespace implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RegisterNamespace.class); + + // ---------------------------- < keys > + /** prefix key */ + private String prefixKey = "prefix"; + + /** uri key */ + private String uriKey = "uri"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String prefix = (String) ctx.get(this.prefixKey); + String uri = (String) ctx.get(this.uriKey); + if (log.isDebugEnabled()) { + log.debug("registering namespace uri=" + uri + " prefix=" + prefix); + } + CommandHelper.getSession(ctx).getWorkspace().getNamespaceRegistry() + .registerNamespace(prefix, uri); + return false; + } + + /** + * @return the prefix key. + */ + public String getPrefixKey() { + return prefixKey; + } + + /** + * @param prefixKey + * the prefix key to set + */ + public void setPrefixKey(String prefixKey) { + this.prefixKey = prefixKey; + } + + /** + * @return the uri key + */ + public String getUriKey() { + return uriKey; + } + + /** + * @param uriKey + * the uri key to set + */ + public void setUriKey(String uriKey) { + this.uriKey = uriKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/SetNamespacePrefix.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/SetNamespacePrefix.java new file mode 100644 index 00000000000..2f674eb677d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/SetNamespacePrefix.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.standalone.cli.namespace; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Sets a namespace prefix + */ +public class SetNamespacePrefix implements Command { + /** logger */ + private static Log log = LogFactory.getLog(SetNamespacePrefix.class); + + // ---------------------------- < keys > + /** prefix key */ + private String prefixKey = "prefix"; + + /** uri key */ + private String uriKey = "uri"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String prefix = (String) ctx.get(this.prefixKey); + String uri = (String) ctx.get(this.uriKey); + if (log.isDebugEnabled()) { + log.debug("setting namespace prefix uri=" + uri + " new prefix=" + prefix); + } + CommandHelper.getSession(ctx).setNamespacePrefix(prefix, uri); + return false; + } + + /** + * @return the prefix key. + */ + public String getPrefixKey() { + return prefixKey; + } + + /** + * @param prefixKey + * the prefix key to set + */ + public void setPrefixKey(String prefixKey) { + this.prefixKey = prefixKey; + } + + /** + * @return the uri key + */ + public String getUriKey() { + return uriKey; + } + + /** + * @param uriKey + * the uri key to set + */ + public void setUriKey(String uriKey) { + this.uriKey = uriKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/UnregisterNamespace.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/UnregisterNamespace.java new file mode 100644 index 00000000000..b59ee32b722 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/namespace/UnregisterNamespace.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.standalone.cli.namespace; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Unregister a namespace + */ +public class UnregisterNamespace implements Command { + /** logger */ + private static Log log = LogFactory.getLog(UnregisterNamespace.class); + + // ---------------------------- < keys > + /** prefix key */ + private String prefixKey = "prefix"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String prefix = (String) ctx.get(this.prefixKey); + if (log.isDebugEnabled()) { + log.debug("unregistering namespace with prefix=" + prefix); + } + CommandHelper.getSession(ctx).getWorkspace().getNamespaceRegistry() + .unregisterNamespace(prefix); + return false; + } + + /** + * @return the prefix key. + */ + public String getPrefixKey() { + return prefixKey; + } + + /** + * @param prefixKey + * the prefix key to set + */ + public void setPrefixKey(String prefixKey) { + this.prefixKey = prefixKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/nodetype/RegisterNodeType.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/nodetype/RegisterNodeType.java new file mode 100644 index 00000000000..92ca450c3a4 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/nodetype/RegisterNodeType.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.standalone.cli.nodetype; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Register node types via CND file + * + */ +public class RegisterNodeType implements Command { + + private static final Log log = LogFactory.getLog(RegisterNodeType.class); + + private final String srcFsPathKey = "srcFsPath"; + + /* + * (non-Javadoc) + * @see org.apache.commons.chain.Command#execute(org.apache.commons.chain.Context) + */ + public boolean execute(Context context) throws Exception { + String path = (String) context.get(srcFsPathKey); + + // Register the custom node types defined in the CND file + InputStream is = new FileInputStream(path); + + if (log.isDebugEnabled()) { + log.debug("Import CND from path " + path); + } + CndImporter.registerNodeTypes(new InputStreamReader(is), CommandHelper.getSession(context)); + return false; + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/AbstractQuery.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/AbstractQuery.java new file mode 100644 index 00000000000..63246e81fb8 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/AbstractQuery.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli.query; + +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Query the Repository through either SQL or XPATH language. + */ +public abstract class AbstractQuery implements Command { + + // ---------------------------- < keys > + + /** query statement key */ + private String statementKey = "statement"; + + /** destination key */ + private String destKey = "collected"; + + /** + * {@inheritDoc} + */ + public final boolean execute(Context ctx) throws Exception { + String statement = (String) ctx.get(this.statementKey); + Session session = CommandHelper.getSession(ctx); + Query query = session.getWorkspace().getQueryManager().createQuery( + statement, this.getLanguage()); + QueryResult result = query.execute(); + ctx.put(destKey, result.getNodes()); + return false; + } + + /** + * @return the query language + */ + protected abstract String getLanguage(); + + /** + * @return the statement key + */ + public String getStatementKey() { + return statementKey; + } + + /** + * @param statementKey + * the statement key to set + */ + public void setStatementKey(String statementKey) { + this.statementKey = statementKey; + } + + /** + * @return the destination key + */ + public String getDestKey() { + return destKey; + } + + /** + * @param toKey + * the destination key to set + */ + public void setDestKey(String toKey) { + this.destKey = toKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/SQLQuery.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/SQLQuery.java new file mode 100644 index 00000000000..dcbffd85324 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/SQLQuery.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.standalone.cli.query; + +import javax.jcr.query.Query; + +/** + * SQL Query + */ +public class SQLQuery extends AbstractQuery { + + /** + * {@inheritDoc} + */ + protected String getLanguage() { + return Query.SQL; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/XPathQuery.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/XPathQuery.java new file mode 100644 index 00000000000..f73554561ed --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/query/XPathQuery.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.standalone.cli.query; + +import javax.jcr.query.Query; + +/** + * XPath Query + */ +public class XPathQuery extends AbstractQuery { + + /** + * {@inheritDoc} + */ + protected String getLanguage() { + return Query.XPATH; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/AddVersionLabel.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/AddVersionLabel.java new file mode 100644 index 00000000000..f3f39fcbd2d --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/AddVersionLabel.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.standalone.cli.version; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Add a label to the given Version + */ +public class AddVersionLabel implements Command { + /** logger */ + private static Log log = LogFactory.getLog(AddVersionLabel.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** version name key */ + private String versionKey = "version"; + + /** version label key */ + private String labelKey = "label"; + + /** move label key */ + private String moveLabelKey = "moveLabel"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String versionName = (String) ctx.get(this.versionKey); + boolean moveLabel = Boolean + .valueOf((String) ctx.get(this.moveLabelKey)).booleanValue(); + String versionLabel = (String) ctx.get(this.labelKey); + if (log.isDebugEnabled()) { + log.debug("Add label " + versionLabel + " to version " + + versionName + " of node at " + path); + } + CommandHelper.getNode(ctx, path).getVersionHistory().addVersionLabel( + versionName, versionLabel, moveLabel); + return false; + } + + /** + * @return the move label key + */ + public String getMoveLabelKey() { + return moveLabelKey; + } + + /** + * @param moveLabelKey + * the move label key to set + */ + public void setMoveLabelKey(String moveLabelKey) { + this.moveLabelKey = moveLabelKey; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * @return the version label key + */ + public String getLabelKey() { + return labelKey; + } + + /** + * @param versionLabelKey + * the version label key to set + */ + public void setLabelKey(String versionLabelKey) { + this.labelKey = versionLabelKey; + } + + /** + * @return the version name key + */ + public String getVersionKey() { + return versionKey; + } + + /** + * @param versionNameKey + * the version name key to set + */ + public void setVersionKey(String versionNameKey) { + this.versionKey = versionNameKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Checkin.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Checkin.java new file mode 100644 index 00000000000..50fb378292c --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Checkin.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.standalone.cli.version; + +import javax.jcr.version.Version; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Check in the given versionable Node + */ +public class Checkin implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Checkin.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** target version number **/ + private String targetVersion = "version"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + if (log.isDebugEnabled()) { + log.debug("cheking in node at " + path); + } + Version v = CommandHelper.getNode(ctx, path).checkin(); + ctx.put(this.targetVersion, v.getName()) ; + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + public String getTargetVersion() { + return targetVersion; + } + + public void setTargetVersion(String targetVersion) { + this.targetVersion = targetVersion; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Checkout.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Checkout.java new file mode 100644 index 00000000000..0490da97860 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Checkout.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.standalone.cli.version; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Checkout the given versionable Node + */ +public class Checkout implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Checkout.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + if (log.isDebugEnabled()) { + log.debug("cheking out node at " + path); + } + CommandHelper.getNode(ctx, path).checkout(); + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Merge.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Merge.java new file mode 100644 index 00000000000..a86c724a62e --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Merge.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.standalone.cli.version; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Merge + */ +public class Merge implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Merge.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** source workspace key */ + private String srcWorkspaceKey = "srcWorkspace"; + + /** best effort key */ + private String bestEffortKey = "bestEffort"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String srcWorkspace = (String) ctx.get(this.srcWorkspaceKey); + boolean bestEffort = Boolean.valueOf( + (String) ctx.get(this.bestEffortKey)).booleanValue(); + if (log.isDebugEnabled()) { + log.debug("merging node at " + path + " from workspace " + + srcWorkspace + " besteffort=" + bestEffort); + } + CommandHelper.getNode(ctx, path).merge(srcWorkspace, bestEffort); + return false; + } + + /** + * @return the best effort key + */ + public String getBestEffortKey() { + return bestEffortKey; + } + + /** + * @param bestEffortKey + * the best effort key to set + */ + public void setBestEffortKey(String bestEffortKey) { + this.bestEffortKey = bestEffortKey; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * @return the source Workspace key + */ + public String getSrcWorkspaceKey() { + return srcWorkspaceKey; + } + + /** + * @param srcWorkspaceKey + * the source Workspace key to set + */ + public void setSrcWorkspaceKey(String srcWorkspaceKey) { + this.srcWorkspaceKey = srcWorkspaceKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersion.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersion.java new file mode 100644 index 00000000000..eb82ff77db0 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersion.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.standalone.cli.version; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Remove a Version from the VersionHistory + */ +public class RemoveVersion implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RemoveVersion.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** version label key */ + private String nameKey = "name"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String versionName = (String) ctx.get(this.nameKey); + if (log.isDebugEnabled()) { + log.debug("Remove version " + versionName + " from node " + path); + } + CommandHelper.getNode(ctx, path).getVersionHistory().removeVersion( + versionName); + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * @return the version name key + */ + public String getNameKey() { + return nameKey; + } + + /** + * @param versionNameKey + * the version name key to set + */ + public void setNameKey(String versionNameKey) { + this.nameKey = versionNameKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersionByLabel.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersionByLabel.java new file mode 100644 index 00000000000..c3512010765 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersionByLabel.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.standalone.cli.version; + +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Remove the Version from the VersionHistory that + * match the given label + */ +public class RemoveVersionByLabel implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RemoveVersionByLabel.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** version label key */ + private String labelKey = "label"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String label = (String) ctx.get(this.labelKey); + if (log.isDebugEnabled()) { + log.debug("Remove version with label " + label + " from node " + + path); + } + VersionHistory vh = CommandHelper.getNode(ctx, path) + .getVersionHistory(); + Version v = vh.getVersionByLabel(label); + vh.removeVersion(v.getName()); + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * @return the version name key + */ + public String getLabelKey() { + return labelKey; + } + + /** + * @param versionNameKey + * the version name key to set + */ + public void setLabelKey(String versionNameKey) { + this.labelKey = versionNameKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersionLabel.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersionLabel.java new file mode 100644 index 00000000000..fe19b224235 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RemoveVersionLabel.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.standalone.cli.version; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Remove a label from the VersionHistory + */ +public class RemoveVersionLabel implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RemoveVersionLabel.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** version label key */ + private String labelKey = "label"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String versionLabel = (String) ctx.get(this.labelKey); + if (log.isDebugEnabled()) { + log.debug("Remove label " + versionLabel + " from node " + path); + } + CommandHelper.getNode(ctx, path).getVersionHistory() + .removeVersionLabel(versionLabel); + return false; + } + + /** + * @return returns the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * @return the version label key. + */ + public String getLabelKey() { + return labelKey; + } + + /** + * @param versionLabelKey + * the version label key to set + */ + public void setLabelKey(String versionLabelKey) { + this.labelKey = versionLabelKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Restore.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Restore.java new file mode 100644 index 00000000000..d4ba59e58c0 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/Restore.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.standalone.cli.version; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Restore a Node to the state of the given Version + */ +public class Restore implements Command { + /** logger */ + private static Log log = LogFactory.getLog(Restore.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** version name key */ + private String versionKey = "version"; + + /** remove existing node key */ + private String removeExistingKey = "removeExisting"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String versionName = (String) ctx.get(this.versionKey); + boolean removeExisting = Boolean.valueOf( + (String) ctx.get(this.removeExistingKey)).booleanValue(); + if (log.isDebugEnabled()) { + log.debug("restoring node at " + path + " to version " + + versionName + " removeexisting=" + removeExisting); + } + CommandHelper.getNode(ctx, path).restore(versionName, removeExisting); + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * @return the remove existing key + */ + public String getRemoveExistingKey() { + return removeExistingKey; + } + + /** + * @param removeExistingKey + * the remove existing key to set + */ + public void setRemoveExistingKey(String removeExistingKey) { + this.removeExistingKey = removeExistingKey; + } + + /** + * @return the version name key + */ + public String getVersionKey() { + return versionKey; + } + + /** + * @param versionNameKey + * the version name key to set + */ + public void setVersionKey(String versionNameKey) { + this.versionKey = versionNameKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RestoreByLabel.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RestoreByLabel.java new file mode 100644 index 00000000000..cc2db1cd52a --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/version/RestoreByLabel.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.standalone.cli.version; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Restore a Node to the state of the Version with + * the specified label + */ +public class RestoreByLabel implements Command { + /** logger */ + private static Log log = LogFactory.getLog(RestoreByLabel.class); + + // ---------------------------- < keys > + /** node path */ + private String pathKey = "path"; + + /** version name key */ + private String labelKey = "label"; + + /** remove existing node key */ + private String removeExistingKey = "removeExisting"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + String path = (String) ctx.get(this.pathKey); + String versionLabel = (String) ctx.get(this.labelKey); + boolean removeExisting = Boolean.valueOf( + (String) ctx.get(this.removeExistingKey)).booleanValue(); + if (log.isDebugEnabled()) { + log.debug("restoring node at " + path + " to version label " + + versionLabel + " removeexisting=" + removeExisting); + } + CommandHelper.getNode(ctx, path).restoreByLabel(versionLabel, + removeExisting); + return false; + } + + /** + * @return the path key + */ + public String getPathKey() { + return pathKey; + } + + /** + * @param pathKey + * the path key to set + */ + public void setPathKey(String pathKey) { + this.pathKey = pathKey; + } + + /** + * @return the remove existing key + */ + public String getRemoveExistingKey() { + return removeExistingKey; + } + + /** + * @param removeExistingKey + * the remove existing key to set + */ + public void setRemoveExistingKey(String removeExistingKey) { + this.removeExistingKey = removeExistingKey; + } + + /** + * @return the version name key + */ + public String getLabelKey() { + return labelKey; + } + + /** + * @param versionNameKey + * the version name key to set + */ + public void setLabelKey(String versionNameKey) { + this.labelKey = versionNameKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/AbstractExportViewToFile.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/AbstractExportViewToFile.java new file mode 100644 index 00000000000..e223d9696d4 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/AbstractExportViewToFile.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.standalone.cli.xml; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.standalone.cli.CommandException; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Export the xml view to a file + */ +public abstract class AbstractExportViewToFile implements Command { + /** logger */ + private static Log log = LogFactory.getLog(AbstractExportViewToFile.class); + + // ---------------------------- < keys > + /** from literal */ + protected String srcAbsPathKey = "srcAbsPath"; + + /** target file key */ + protected String desFsPathKey = "desFsPath"; + + /** overwrite flag key */ + protected String overwriteKey = "overwrite"; + + /** skip binary flag key */ + protected String skipBinaryKey = "skipBinary"; + + /** no recurse flag key */ + protected String noRecurseKey = "noRecurse"; + + /** + * @return the OutputStream for the given file + * @throws CommandException + * @throws IOException + */ + protected OutputStream getOutputStream(Context ctx) + throws CommandException, IOException { + String to = (String) ctx.get(this.desFsPathKey); + boolean overwrite = Boolean + .valueOf((String) ctx.get(this.overwriteKey)).booleanValue(); + File f = new File(to); + + if (f.exists() && !overwrite) { + throw new CommandException("exception.file.exists", new String[] { + to + }); + } + + if (!f.exists()) { + f.createNewFile(); + } + + BufferedOutputStream out = new BufferedOutputStream( + new FileOutputStream(f)); + + return out; + } + + /** + * @return the no recurse key + */ + public String getNoRecurseKey() { + return noRecurseKey; + } + + /** + * @param noRecurseKey + * the no recurse key to set + */ + public void setNoRecurseKey(String noRecurseKey) { + this.noRecurseKey = noRecurseKey; + } + + /** + * @return the overwrite key + */ + public String getOverwriteKey() { + return overwriteKey; + } + + /** + * @param overwriteKey + * the overwrite key to set + */ + public void setOverwriteKey(String overwriteKey) { + this.overwriteKey = overwriteKey; + } + + /** + * @return the skip binary key + */ + public String getSkipBinaryKey() { + return skipBinaryKey; + } + + /** + * @param skipBinaryKey + * the skip binary key to set + */ + public void setSkipBinaryKey(String skipBinaryKey) { + this.skipBinaryKey = skipBinaryKey; + } + + /** + * @return the from key + */ + public String getSrcAbsPathKey() { + return srcAbsPathKey; + } + + /** + * @param fromKey + * the from key to set + */ + public void setSrcAbsPathKey(String fromKey) { + this.srcAbsPathKey = fromKey; + } + + /** + * @return the to key + */ + public String getDesFsPathKey() { + return desFsPathKey; + } + + /** + * @param toKey + * the to key to set + */ + public void setDesFsPathKey(String toKey) { + this.desFsPathKey = toKey; + } + + /** + * {@inheritDoc} + */ + public final boolean execute(Context ctx) throws Exception { + boolean skipBinary = Boolean.valueOf( + (String) ctx.get(this.skipBinaryKey)).booleanValue(); + boolean noRecurse = Boolean + .valueOf((String) ctx.get(this.noRecurseKey)).booleanValue(); + String fromStr = (String) ctx.get(this.srcAbsPathKey); + if (log.isDebugEnabled()) { + log.debug("exporting view from " + fromStr); + } + Node from = CommandHelper.getNode(ctx, fromStr); + OutputStream out = getOutputStream(ctx); + exportView(from, out, skipBinary, noRecurse); + out.close(); + return false; + } + + /** + * Export the view to the given OutputStream + * @param node + * the Node + * @param out + * the OutputStream + * @param skipBinary + * @param noRecurse + * @throws RepositoryException + * @throws IOException + * @throws PathNotFoundException + */ + protected abstract void exportView( + Node node, + OutputStream out, + boolean skipBinary, + boolean noRecurse) throws PathNotFoundException, IOException, + RepositoryException; +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ExportDocViewToFile.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ExportDocViewToFile.java new file mode 100644 index 00000000000..28f104a1ac7 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ExportDocViewToFile.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.standalone.cli.xml; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +/** + * Serialize the Node to the given file using the Document View + * Format + */ +public class ExportDocViewToFile extends AbstractExportViewToFile { + + /** + * {@inheritDoc} + */ + protected void exportView( + Node node, + OutputStream out, + boolean skipBinary, + boolean noRecurse) throws PathNotFoundException, IOException, + RepositoryException { + node.getSession().exportDocumentView(node.getPath(), out, skipBinary, + noRecurse); + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ExportSysViewToFile.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ExportSysViewToFile.java new file mode 100644 index 00000000000..a7449f3bb7a --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ExportSysViewToFile.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.standalone.cli.xml; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.jcr.Node; +import javax.jcr.PathNotFoundException; +import javax.jcr.RepositoryException; + +/** + * Serialize the Node to the given file using the System View + * Format + */ +public class ExportSysViewToFile extends AbstractExportViewToFile { + + /** + * {@inheritDoc} + */ + protected void exportView( + Node node, + OutputStream out, + boolean skipBinary, + boolean noRecurse) throws PathNotFoundException, IOException, + RepositoryException { + node.getSession().exportSystemView(node.getPath(), out, skipBinary, + noRecurse); + } + +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ImportXmlFromInputStream.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ImportXmlFromInputStream.java new file mode 100644 index 00000000000..e03e7a005c9 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/cli/xml/ImportXmlFromInputStream.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.standalone.cli.xml; + +import java.io.BufferedInputStream; +import java.io.InputStream; + +import javax.jcr.Node; +import javax.jcr.Session; + +import org.apache.commons.chain.Command; +import org.apache.commons.chain.Context; +import org.apache.jackrabbit.standalone.cli.CommandHelper; + +/** + * Import the xml view to the target Node + */ +public class ImportXmlFromInputStream implements Command { + + // ---------------------------- < keys > + + /** doc view file key */ + private String inputStreamKey = "inputStream"; + + /** flag that indicates whether to use the transient space or not */ + private String persistentKey = "persistent"; + + /** target node */ + private String destJcrPathKey = "destJcrPath"; + + /** uuid behaviour key */ + private String uuidBehaviourKey = "uuidBehaviour"; + + /** + * {@inheritDoc} + */ + public boolean execute(Context ctx) throws Exception { + InputStream is = (InputStream) ctx.get(this.inputStreamKey); + String dest = (String) ctx.get(this.destJcrPathKey); + String persistent = (String) ctx.get(this.persistentKey); + + int uuidBehaviour = Integer.valueOf( + (String) ctx.get(this.uuidBehaviourKey)).intValue(); + + BufferedInputStream bis = new BufferedInputStream(is); + Session s = CommandHelper.getSession(ctx); + Node n = CommandHelper.getNode(ctx, dest); + + if (persistent != null + && Boolean.valueOf(persistent).equals(Boolean.TRUE)) { + s.getWorkspace().importXML(n.getPath(), bis, uuidBehaviour); + } else { + s.importXML(n.getPath(), bis, uuidBehaviour); + } + + return false; + } + + /** + * @return the uuidBehaviourKey + */ + public String getUuidBehaviourKey() { + return uuidBehaviourKey; + } + + /** + * @param uuidBehaviourKey + * the uuidBehaviourKey to set + */ + public void setUuidBehaviourKey(String uuidBehaviourKey) { + this.uuidBehaviourKey = uuidBehaviourKey; + } + + /** + * @return the destination jcr path key + */ + public String getDestJcrPathKey() { + return destJcrPathKey; + } + + /** + * @param destJcrPathKey + * the destination jcr path key to set + */ + public void setDestJcrPathKey(String destJcrPathKey) { + this.destJcrPathKey = destJcrPathKey; + } + + /** + * @return the inputStreamKey + */ + public String getInputStreamKey() { + return inputStreamKey; + } + + /** + * @param inputStreamKey + * the inputStreamKey to set + */ + public void setInputStreamKey(String inputStreamKey) { + this.inputStreamKey = inputStreamKey; + } + + /** + * @return the persistentKey + */ + public String getPersistentKey() { + return persistentKey; + } + + /** + * @param persistentKey + * the persistentKey to set + */ + public void setPersistentKey(String persistentKey) { + this.persistentKey = persistentKey; + } +} diff --git a/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/package-info.java b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/package-info.java new file mode 100755 index 00000000000..127342c6054 --- /dev/null +++ b/jackrabbit-standalone/src/main/java/org/apache/jackrabbit/standalone/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.standalone; diff --git a/jackrabbit-standalone/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory b/jackrabbit-standalone/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory new file mode 100644 index 00000000000..4f182903870 --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/META-INF/services/javax.jcr.RepositoryFactory @@ -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. + +org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory +org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory +org.apache.jackrabbit.commons.JndiRepositoryFactory +org.apache.jackrabbit.core.RepositoryFactoryImpl +org.apache.jackrabbit.rmi.repository.RmiRepositoryFactory diff --git a/jackrabbit-standalone/src/main/resources/WEB-INF/web.xml b/jackrabbit-standalone/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000000..e00199c579c --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,210 @@ + + + + + + Apache Jackrabbit + + + + + + Repository + org.apache.jackrabbit.j2ee.RepositoryAccessServlet + + + repository.context.attribute.name + javax.jcr.Repository + + + 2 + + + + + + + Webdav + + The webdav servlet that connects HTTP request to the repository. + + org.apache.jackrabbit.j2ee.SimpleWebdavServlet + + resource-path-prefix + /repository + + defines the prefix for spooling resources out of the repository. + + + + resource-config + /WEB-INF/config.xml + + Defines various dav-resource configuration parameters. + + + + + 3 + + + + + + + JCRWebdavServer + + The servlet used to remote JCR calls over HTTP. + + org.apache.jackrabbit.j2ee.JcrRemotingServlet + + missing-auth-mapping + + + Defines how a missing authorization header should be handled. + 1) If this init-param is missing, a 401 response is generated. + This is suitable for clients (eg. WebDAV clients) for which + sending a proper authorization header is not possible if the + server never sent a 401. + 2) If this init-param is present with an empty value, + null-credentials are returned, thus forcing an null login + on the repository. + 3) If this init-param is present with the value 'guestcredentials' + java.jcr.GuestCredentials are used to login to the repository. + 4) If this init-param has a 'user:password' value, the respective + simple credentials are generated. + + + + + + resource-path-prefix + /server + + defines the prefix for spooling resources out of the repository. + + + + + + + 5 + + + + + + + RMI + org.apache.jackrabbit.servlet.remote.RemoteBindingServlet + + + + + + + Webdav + /repository/* + + + JCRWebdavServer + /server/* + + + RMI + /rmi + + + + + + + index.jsp + + + + org.apache.jackrabbit.j2ee.JcrApiNotFoundException + /error/classpath.jsp + + + javax.jcr.RepositoryException + /error/repository.jsp + + + diff --git a/jackrabbit-standalone/src/main/resources/logback-cli.xml b/jackrabbit-standalone/src/main/resources/logback-cli.xml new file mode 100644 index 00000000000..0a16d685d3b --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/logback-cli.xml @@ -0,0 +1,30 @@ + + + + + + + %level %msg%n + + + + + + + + diff --git a/jackrabbit-standalone/src/main/resources/logback.xml b/jackrabbit-standalone/src/main/resources/logback.xml new file mode 100644 index 00000000000..79fc3254cb7 --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/logback.xml @@ -0,0 +1,42 @@ + + + + + + ${jackrabbit.log} + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + ${jetty.log} + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + + + + + diff --git a/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command-line-rules.xml b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command-line-rules.xml new file mode 100644 index 00000000000..9c8879382e8 --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command-line-rules.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command-line.xml b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command-line.xml new file mode 100644 index 00000000000..c62fcc3046a --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command-line.xml @@ -0,0 +1,963 @@ + + + + + + + + + + + + + + + + + + + + ]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command.xml b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command.xml new file mode 100644 index 00000000000..959990e3fa4 --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/command.xml @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/digester-rules.dtd b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/digester-rules.dtd new file mode 100644 index 00000000000..31bae7cc548 --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/digester-rules.dtd @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/resources.properties b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/resources.properties new file mode 100644 index 00000000000..0034199f18a --- /dev/null +++ b/jackrabbit-standalone/src/main/resources/org/apache/jackrabbit/standalone/cli/resources.properties @@ -0,0 +1,279 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmd.addlocktoken = Add the given Lock token to the current Session +cmd.addlocktoken.token = lock token +cmd.addlocktoken.token.desc = the lock token +cmd.addmixin = Add a mixin to the given Node +cmd.addmixin.mixin = mixin name +cmd.addmixin.mixin.desc = a mixin node type name +cmd.addmixin.path = path +cmd.addmixin.path.desc = path to the given node +cmd.addnode = Add a node to the current working Node +cmd.addversionlabel = Add a label to the given Version +cmd.addversionlabel.label = label +cmd.addversionlabel.label.desc = label to add +cmd.addversionlabel.moveLabel = +cmd.addversionlabel.moveLabel.desc = move label +cmd.addversionlabel.version = version +cmd.addversionlabel.version.desc = the version name on which the label will be applied +cmd.cat = Display the content of a Property or a Node of type nt:file or nt:resource. +cmd.checkin = Check in the given versionable node +cmd.checkout = Checkout the given versionable Node +cmd.clearworkspace = Remove all the content from the current working Workspace +cmd.clone = Clone the given Node to another Workspace +cmd.clone.destAbsPath = absolute path +cmd.clone.destAbsPath.desc = absolute path to the destination node +cmd.clone.removeExisting = boolean +cmd.clone.removeExisting.desc = remove node if present at the destination path +cmd.clone.srcAbsPath = absolute path +cmd.clone.srcAbsPath.desc = source node +cmd.connect = Connect to a remote repository through jcr-rmi +cmd.connect.url = url +cmd.connect.url.desc = String with the url where a jcr-rmi server instance is running +cmd.copy = Copy a Node.\r\nIf the source Workspace is unset it will create a copy of the given Node from the current working Workspace. +cmd.copy.destAbsPath = path +cmd.copy.destAbsPath.desc = absolute path to the destination +cmd.copy.fromWorkspace = name +cmd.copy.fromWorkspace.desc = workspace name +cmd.copy.srcAbsPath = path +cmd.copy.srcAbsPath.desc = absolute path to the source node +cmd.createworkspace = Create a Workspace +cmd.createworkspace.name = name +cmd.createworkspace.name.desc = workspace name +cmd.currentnode = Set the current working Node +cmd.dump = Dump stored data from the current working Node +cmd.exportdocview = Serialize the Node to the given file using the Document View Format +cmd.exportfilesystem = Export a Node of type nt:file or nt:folder to the given file system path +cmd.exportproperty = Export a Property Value of the current working Node to the file system. +cmd.exportsysview = Serialize the Node to the given file using the System View Format +cmd.help = Show available Commands. If a Command is specified it will show its description, usage and parameters. +cmd.help.command = name +cmd.help.command.desc = name of the command to describe +cmd.info = Show status information, including repository, username, workspace and session status +cmd.importfilesystem = Import data from the file system. If the given path refers to a file it's imported to a Node of type nt:file under the current working Node. If the given path refers to a folder, the given folder and all the subtree is imported. +cmd.importxml = Imports the xml view from the given file to the current working Node +cmd.jndi = Connect to a repository through jndi +cmd.lock = Lock the given Node +cmd.lock.deep.desc = A deep lock applies to its holding node and all its descendants. +cmd.lock.session.desc = session scoped locks expire with the session +cmd.login = Login to the current working Repository +cmd.login.password.desc = password [default = anonymous] +cmd.login.user.desc = registered user name [default=anonymous] +cmd.login.workspace.desc = workspace +cmd.logout = Logout from the current working Repository +cmd.lscollect = List collected items under the current node +cmd.lscollectnodes = List collected nodes under the current working node +cmd.lscollectproperties = List collected properties under the current working node +cmd.lsitems = Lists nodes and properties under the current working node +cmd.lsnodes = List nodes under the current working node +cmd.lsnodes.hasLock = Show whether the current working node has a lock +cmd.lsnodes.l = Show whether the current working node is lockable +cmd.lsnodes.locked = Show whether the current working node is locked +cmd.lsnodes.locktoken = token +cmd.lsnodes.locktoken.desc = lock token +cmd.lsnodes.m = Show the full list o mixin that apply to the current working node +cmd.lsnodes.mod = Show whether the current working node is modified +cmd.lsnodes.new = Show whether the current working node is new +cmd.lsnodes.nsize = Show the number of nodes under the current working node +cmd.lsnodes.path = Show the full path to the node +cmd.lsnodes.psize = Show the number of properties under the current working node +cmd.lsnodes.r = Show whether the current working node is referenceable +cmd.lsnodes.rsize = Show the number of references that point to the current working node +cmd.lsnodes.uuid = uuid +cmd.lsnodes.v = Show whether the current working node is versionable +cmd.lsproperties = List properties under the current working node +cmd.lsreferences = List references to the given node +cmd.lsversions = list versions of the given node +cmd.merge = merge versions +cmd.merge.bestEffort.desc = merge best effort +cmd.move = Move a Node +cmd.move.from = source, absolute or relative path +cmd.move.to = destination, only absolute path +cmd.orderbefore = Set the order of the given Node +cmd.refresh = Refresh the Item if specified or the Session to reflect the current saved state +cmd.refresh.keepChanges = keep or discard the changes flag +cmd.refreshlock = Reset the Lock timer +cmd.registernamespace = Register a namespace +cmd.registernodetype = Register node types from a cnd file +cmd.removeitem = Remove the item at the given path +cmd.removeitems = Remove any Item under the given Node that match the given name pattern +cmd.removelocktoken = Remove the given Lock token to the current Session +cmd.removemixin = Remove a mixin from the given Node +cmd.removeversion = Remove a Version from the VersionHistory +cmd.removeversionbylabel = Remove the Version from the VersionHistory that match the given label +cmd.removeversionlabel = Remove a label from the VersionHistory +cmd.rename = Rename a Node +cmd.rename.from = source node +cmd.rename.to = destination node +cmd.restore = Restore a Node to the state of the given Version +cmd.restorebylabel = Restore a Node to the state of the Version with the specified label +cmd.save = Save the current working Node if specified, or the current working Session +cmd.setmultivalueproperty = Set a multivalue Property to the current working Node. The default regular expression is ",". +cmd.setmultivalueproperty.regExp = Regular expression used to split the value into tokens. +cmd.setnamespaceprefix = Sets a namespace prefix +cmd.setproperty = Set a Property Value to the current working Node +cmd.setproperty.name = property name +cmd.setproperty.type = property type. Default is String. +cmd.setproperty.value = property value +cmd.setpropertyfromfile = Set a Property Value with the content of the given file. +cmd.setpropertyfromfile.path = path to the file +cmd.source = Executes a script from the given file +cmd.sqlquery = SQL query +cmd.sqlquery.statement = SQL statement +cmd.sqlquery.target = target variables to store the nodes included in te query result +cmd.startjackrabbit = Start a Jackrabbit instance and set it as the current working Repository +cmd.startjackrabbit.config = Repository config file, usually called repository.xml +cmd.startjackrabbit.home = Repository location +cmd.stopjackrabbit = Stop Jackrabbit +cmd.unlock = Unlock the current working Node +cmd.xpathquery = XPath query +cmd.xpathquery.statement = statement + +common.encoding = Encoding +common.encoding.desc = see http://en.wikipedia.org/wiki/Character_encoding +common.exportview.from = Source node +common.exportview.noRecurse = if it's true it doesn't include the child nodes +common.exportview.skipBinary = if it's true the binary properties will be serialized as if they are empty +common.exportview.to = Target file where the xml view will be stored +common.fspath = path +common.fspath.desc = file system path +common.index = index of the value to show +common.index.desc = The indexing +common.jcrabspath = jcr absolute path +common.jcrabspath.desc = a path string representing the absolute path in the current workspace. +common.jcrname = jcr name +common.jcrname.desc = a valid jcr item name +common.jcrpath = jcr path +common.jcrpath.desc = a path string representing either a relative or absolute path in the current workspace. +common.jcrrelpath = jcr relative path +common.jcrrelpath.desc = a path string representing the relative path in the current workspace. +common.namepattern = pattern +common.namepattern.desc = name pattern +common.nodetype = node type +common.nodetype.desc = valid node type name +common.source = source +common.uuidBehaviour = int +common.uuidBehaviour.desc = The flag uuidBehavior. + +exception = exception + +exception.alias.already.in.use = Alias "{0}" already in use. +exception.already.logged.in = You are already logged in. Please logout. +exception.cat.unsupported.type = Unsupported type to display contents "{0}". +exception.file.exists = File already exists. {0}. +exception.file.not.created = The file was not created. {0}. +exception.file.not.found = File not found. {0}. +exception.folder.not.created = The folder was not created. {0}. +exception.fspath.is.null = The file system path is unset. +exception.illegalargument = illegal argument +exception.illegalargument.no.iterator.under = Illegal argument. There is no iterator under {0}. +exception.illegalargument.not.a.command = Illegal argument. "{0}" is not a command. +exception.illegalargument.null = Null arguments are illegal. +exception.jackrabbit.command = jackrabbit specific command +exception.missing.paramater = missing parameter "{0}" +exception.more.arguments.than.expected = more arguments than expected +exception.no.command.for.name = No Command for name "{0}". +exception.no.current.node = The current working node isn't set. Please start a jcr implementation before trying to get the current working node. +exception.no.current.repository = the current working repository is unset +exception.no.current.session = the current working session is unset +exception.no.flag.for.name = no flag for name "{0}" +exception.no.node.at = there is not a Node at "{0}". +exception.no.opt.for.name = no option for name "{0}" +exception.not.file.or.folder = The given node is not nt:file nor nt:folder. {0}. +exception.occurred = an exception occurred +exception.only.absolute.path = only absolute paths are allowed +exception.parse.input.empty = input is empty +exception.repository.not.in.context = The repository can't be found in the application context. You need to start a jcr implementation before trying to login. +exception.unabletoinit = Unable to init + +importxml.uuidBehaviour = uuid creation behaviour + +phrase.commandlist = command list +phrase.commandreference = command reference +phrase.display.stacktrace = display stack trace +phrase.elapsedtime = elapsed time +phrase.haslock = has lock +phrase.jackrabbit.command = jackrabbit specific command +phrase.keepchanges = keep changes +phrase.locktoken = lock token +phrase.maxitems = max number of items to show +phrase.not.available = not available +phrase.not.connected = not connected +phrase.not.logged.in = not logged in +phrase.regexp = regular expression +phrase.removeExisting = remove existing + +word.alias = alias +word.argument = argument +word.arguments = arguments +word.boolean = boolean +word.classname = class name +word.commandname = command name +word.commands = commands +word.default = default +word.depth = depth +word.description = description +word.destination = destination +word.elapsedtime = elapsed time +word.flag = flag +word.flags = flags +word.folder = folder +word.from = from +word.in = in +word.int = int +word.label = label +word.labels = labels +word.length = length +word.listed = listed +word.lockable = lockable +word.locked = locked +word.locktoken = lock token +word.message = message +word.mixin = mixin +word.modified = modified +word.multiple = multiple +word.name = name +word.new = new +word.node = node +word.nodes = nodes +word.nodetype = node type +word.options = options +word.overview = overview +word.overwrite = overwrite +word.password = password +word.path = path +word.pattern = pattern +word.prefix = prefix +word.preview = preview +word.properties = properties +word.property = property +word.referenceable = referenceable +word.references = references +word.required = required +word.running = running +word.savenode = Save the current working node +word.source = source +word.stmt = statement +word.total = total +word.type = type +word.uri = uri +word.usage = usage +word.user = user +word.value = value +word.version = version +word.versionable = versionable +word.versions = versions +word.welcome = welcome diff --git a/jackrabbit-vfs-ext/README.md b/jackrabbit-vfs-ext/README.md new file mode 100644 index 00000000000..4fdf2723378 --- /dev/null +++ b/jackrabbit-vfs-ext/README.md @@ -0,0 +1,110 @@ +#Welcome to Jackrabbit Commons VFS Extension + +This is the Commons-VFS Extension component of the Apache Jackrabbit project. + +## Build Instructions + +To build the latest SNAPSHOT versions of all the components +included here, run the following command with Maven 3: + + mvn clean install + +## Unit Test Instructions + +### Testing with the default local file system + +By default, the unit tests use the local file system as backend storage. +You can run the unit tests with the default temporary local file system like the following: + + mvn clean test + +### Testing with WebDAV file system + +You can run the unit tests with WebDAV backend file system like the following: + + mvn clean test -Dconfig=src/test/resources/vfs-webdav.properties + +*Tip*: You can install/run WsgiDAV server (http://wsgidav.readthedocs.io/en/latest/) like the following: + + wsgidav --host=0.0.0.0 --port=8888 --root=/tmp/davroot + +### Testing with SFTP file system + +You can run the unit tests with WebDAV backend file system like the following: + + mvn clean test -Dconfig=src/test/resources/vfs-sftp.properties + +## Configuration Instructions + +### With local file system + + + + + + + + + +### With WebDAV file system + + + + + + + + + +vfs2-datastore.properties: + +``` + baseFolderUri = webdav://tester:secret@localhost:8888/vfsds + # Properties to build org.apache.commons.vfs2.FileSystemOptions at runtime when resolving the base folder. + # Any properties, name of which is starting with 'fso.', are used to build FileSystemOptions + # after removing the 'fso.' prefix. See VFS2 documentation for the detail. + fso.http.maxTotalConnections = 200 + fso.http.maxConnectionsPerHost = 200 + fso.http.preemptiveAuth = false +``` + +### With SFTP file system + + + + + + + + + +vfs2-datastore.properties: + +``` + baseFolderUri = sftp://tester:secret@localhost/vfsds + # Properties to build org.apache.commons.vfs2.FileSystemOptions at runtime when resolving the base folder. + # Any properties, name of which is starting with 'fso.', are used to build FileSystemOptions + # after removing the 'fso.' prefix. See VFS2 documentation for the detail. +``` + +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/jackrabbit-vfs-ext/pom.xml b/jackrabbit-vfs-ext/pom.xml new file mode 100644 index 00000000000..13fcbe25e9a --- /dev/null +++ b/jackrabbit-vfs-ext/pom.xml @@ -0,0 +1,127 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-vfs-ext + Jackrabbit VFS Extension + Jackrabbit extenstion to Commons VFS + bundle + + + + + + + javax.jcr + jcr + + + org.osgi + org.osgi.annotation + provided + + + org.apache.commons + commons-vfs2 + 2.1 + + + commons-logging + commons-logging + + + + + org.apache.jackrabbit + jackrabbit-jcr-commons + ${project.version} + + + org.apache.jackrabbit + jackrabbit-data + ${project.version} + + + org.slf4j + slf4j-api + + + + org.apache.jackrabbit + jackrabbit-data + ${project.version} + test-jar + test + + + + com.jcraft + jsch + 0.1.53 + test + + + junit + junit + test + + + org.slf4j + slf4j-log4j12 + test + + + + + + maven-surefire-plugin + + + **/vfs/**/TestAll.java + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.jackrabbit.vfs.ext.ds + sun.io + + + + + org.apache.rat + apache-rat-plugin + + + .checkstyle + + + + + + diff --git a/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/LazyFileContentInputStream.java b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/LazyFileContentInputStream.java new file mode 100644 index 00000000000..c25015628fa --- /dev/null +++ b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/LazyFileContentInputStream.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.vfs.ext.ds; + +import java.io.IOException; + +import org.apache.commons.io.input.AutoCloseInputStream; +import org.apache.commons.vfs2.FileNotFoundException; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; + +/** + * This input stream delays opening the file content until the first byte is read, and + * closes and discards the underlying stream as soon as the end of input has + * been reached or when the stream is explicitly closed. + */ +public class LazyFileContentInputStream extends AutoCloseInputStream { + + /** + * The file object to read from. + */ + protected final FileObject fileObject; + + /** + * True if the input stream was opened. It is also set to true if the stream + * was closed without reading (to avoid opening the file content after the stream + * was closed). + */ + protected boolean opened; + + /** + * Creates a new LazyFileInputStream for the given file. If the + * file is unreadable, a FileSystemException is thrown. + * The file is not opened until the first byte is read from the stream. + * + * @param fileObject the file + * @throws org.apache.commons.vfs2.FileNotFoundException + * @throws org.apache.commons.vfs2.FileSystemException + */ + public LazyFileContentInputStream(FileObject fileObject) throws FileSystemException { + super(null); + + if (!fileObject.isReadable()) { + throw new FileNotFoundException(fileObject.getName().getFriendlyURI()); + } + + this.fileObject = fileObject; + } + + /** + * Open the stream if required. + * + * @throws java.io.IOException + */ + protected void open() throws IOException { + if (!opened) { + opened = true; + in = fileObject.getContent().getInputStream(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int read() throws IOException { + open(); + return super.read(); + } + + /** + * {@inheritDoc} + */ + @Override + public int available() throws IOException { + open(); + return super.available(); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + // make sure the file is not opened afterwards + opened = true; + + // only close the file if it was in fact opened + if (in != null) { + super.close(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void reset() throws IOException { + open(); + super.reset(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean markSupported() { + try { + open(); + } catch (IOException e) { + throw new IllegalStateException(e.toString()); + } + + return super.markSupported(); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void mark(int readlimit) { + try { + open(); + } catch (IOException e) { + throw new IllegalStateException(e.toString()); + } + + super.mark(readlimit); + } + + /** + * {@inheritDoc} + */ + @Override + public long skip(long n) throws IOException { + open(); + return super.skip(n); + } + + /** + * {@inheritDoc} + */ + @Override + public int read(byte[] b) throws IOException { + open(); + return super.read(b, 0, b.length); + } + + /** + * {@inheritDoc} + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + open(); + return super.read(b, off, len); + } +} diff --git a/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSBackend.java b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSBackend.java new file mode 100644 index 00000000000..3b2a7f92a1e --- /dev/null +++ b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSBackend.java @@ -0,0 +1,812 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.vfs.ext.ds; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.Timestamp; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; +import org.apache.commons.vfs2.FileType; +import org.apache.jackrabbit.core.data.AbstractBackend; +import org.apache.jackrabbit.core.data.AsyncTouchCallback; +import org.apache.jackrabbit.core.data.AsyncTouchResult; +import org.apache.jackrabbit.core.data.AsyncUploadCallback; +import org.apache.jackrabbit.core.data.AsyncUploadResult; +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A data store backend that stores data on VFS file system. + */ +public class VFSBackend extends AbstractBackend { + + /** + * Logger instance. + */ + private static final Logger LOG = LoggerFactory.getLogger(VFSBackend.class); + + /** + * The default pool size of asynchronous write pooling executor. + */ + static final int DEFAULT_ASYNC_WRITE_POOL_SIZE = 10; + + /** + * The maximum last modified time resolution of the file system. + */ + private static final int ACCESS_TIME_RESOLUTION = 2000; + + /** + * Touch file name suffix. + * When {@link #isTouchFilePreferred()} returns true, this backend creates a separate file named by + * the original file base name followed by this touch file name suffix. + * So, this backend can set the last modified time on the separate touch file instead of trying to do it + * on the original entry file. + * For example, WebDAV file system doesn't allow to modify the last modified time on a file. + */ + private static final String TOUCH_FILE_NAME_SUFFIX = ".touch"; + + /** + * VFS base folder object. + */ + private FileObject baseFolder; + + /** + * Whether or not a touch file is preferred to set/get the last modified timestamp for a file object + * instead of setting/getting the last modified timestamp directly from the file object. + */ + private boolean touchFilePreferred = true; + + public VFSBackend(FileObject baseFolder) { + this.baseFolder = baseFolder; + } + + /** + * {@inheritDoc} + */ + @Override + public void init(CachingDataStore store, String homeDir, String config) throws DataStoreException { + super.init(store, homeDir, config); + + // When it's local file system, no need to use a separate touch file. + if ("file".equals(baseFolder.getName().getScheme())) { + touchFilePreferred = false; + } + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream read(DataIdentifier identifier) throws DataStoreException { + FileObject fileObject = getExistingFileObject(identifier); + + if (fileObject == null) { + throw new DataStoreException("Could not find file object for: " + identifier); + } + + try { + return new LazyFileContentInputStream(fileObject); + } catch (FileSystemException e) { + throw new DataStoreException("Could not get input stream from object: " + identifier, e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public long getLength(DataIdentifier identifier) throws DataStoreException { + FileObject fileObject = getExistingFileObject(identifier); + + if (fileObject == null) { + throw new DataStoreException("Could not find file object for: " + identifier); + } + + try { + return fileObject.getContent().getSize(); + } catch (FileSystemException e) { + throw new DataStoreException("Could not get length from object: " + identifier, e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public long getLastModified(DataIdentifier identifier) throws DataStoreException { + FileObject fileObject = getExistingFileObject(identifier); + + if (fileObject == null) { + throw new DataStoreException("Could not find file object for: " + identifier); + } + + return getLastModifiedTime(fileObject); + } + + /** + * {@inheritDoc} + */ + @Override + public void write(DataIdentifier identifier, File file) throws DataStoreException { + write(identifier, file, false, null); + } + + /** + * {@inheritDoc} + */ + @Override + public void writeAsync(DataIdentifier identifier, File file, AsyncUploadCallback callback) + throws DataStoreException { + if (callback == null) { + throw new IllegalArgumentException("callback parameter cannot be null in asyncUpload"); + } + + getAsyncWriteExecutor().execute(new AsyncUploadJob(identifier, file, callback)); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator getAllIdentifiers() throws DataStoreException { + List identifiers = new LinkedList(); + + try { + for (FileObject fileObject : VFSUtils.getChildFolders(getBaseFolderObject())) { // skip top-level files + pushIdentifiersRecursively(identifiers, fileObject); + } + } catch (FileSystemException e) { + throw new DataStoreException("Object identifiers not resolved.", e); + } + + LOG.debug("Found " + identifiers.size() + " identifiers."); + + return identifiers.iterator(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean exists(DataIdentifier identifier, boolean touch) throws DataStoreException { + FileObject fileObject = getExistingFileObject(identifier); + + if (fileObject == null) { + return false; + } + + if (touch) { + touch(identifier, System.currentTimeMillis(), false, null); + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean exists(DataIdentifier identifier) throws DataStoreException { + return exists(identifier, false); + } + + /** + * {@inheritDoc} + */ + @Override + public void touch(DataIdentifier identifier, long minModifiedDate) throws DataStoreException { + touch(identifier, minModifiedDate, false, null); + } + + /** + * {@inheritDoc} + */ + @Override + public void touchAsync(DataIdentifier identifier, long minModifiedDate, AsyncTouchCallback callback) + throws DataStoreException { + if (callback == null) { + throw new IllegalArgumentException("callback parameter cannot be null in touchAsync"); + } + + getAsyncWriteExecutor().execute(new AsyncTouchJob(identifier, minModifiedDate, callback)); + } + + /** + * {@inheritDoc} + */ + @Override + public Set deleteAllOlderThan(long timestamp) throws DataStoreException { + Set deleteIdSet = new HashSet(30); + + try { + for (FileObject folderObject : VFSUtils.getChildFolders(getBaseFolderObject())) { + deleteOlderRecursive(deleteIdSet, folderObject, timestamp); + } + } catch (FileSystemException e) { + throw new DataStoreException("Object deletion aborted.", e); + } + + return deleteIdSet; + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteRecord(DataIdentifier identifier) throws DataStoreException { + FileObject fileObject = getExistingFileObject(identifier); + + if (fileObject != null) { + deleteRecordFileObject(fileObject); + deleteEmptyParentFolders(fileObject); + } + } + + /** + * Returns true if a touch file should be used to save/get the last modified time for a file object. + * True by default unless the {@link #getBaseFolderObject()} is representing a local file system folder (e.g, file://...). + *

        + * When returns true, this backend creates a separate file named by the original file base name followed + * by this touch file name suffix. So, this backend can set the last modified time on the separate touch file + * instead of trying to do it on the original entry file. + * For example, WebDAV file system doesn't allow to modify the last modified time on a file. + *

        + * @return true if a touch file should be used to save/get the last modified time for a file object + */ + public boolean isTouchFilePreferred() { + return touchFilePreferred; + } + + /** + * Sets whether or not a touch file should be used to save/get the last modified timestamp for a file object. + * @param touchFilePreferred whether or not a touch file should be used to save/get the last modified timestamp for a file object + */ + public void setTouchFilePreferred(boolean touchFilePreferred) { + this.touchFilePreferred = touchFilePreferred; + } + + /** + * Returns the VFS base folder object. + * @return the VFS base folder object + */ + protected FileObject getBaseFolderObject() { + return baseFolder; + } + + /** + * Returns a resolved identified file object. This method implements the pattern + * used to avoid problems with too many files in a single folder. + * + * @param identifier data identifier + * @return identified file object + * @throws DataStoreException if any file system exception occurs + */ + protected FileObject resolveFileObject(DataIdentifier identifier) throws DataStoreException { + try { + String relPath = resolveFileObjectRelPath(identifier); + return getBaseFolderObject().resolveFile(relPath); + } catch (FileSystemException e) { + throw new DataStoreException("File object not resolved: " + identifier, e); + } + } + + /** + * Returns a resolved relative file object path by the given entry identifier. + * @param identifier entry identifier + * @return a resolved relative file object path by the given entry identifier + */ + protected String resolveFileObjectRelPath(DataIdentifier identifier) { + String idString = identifier.toString(); + StringBuilder sb = new StringBuilder(80); + sb.append(idString.substring(0, 2)).append('/'); + sb.append(idString.substring(2, 4)).append('/'); + sb.append(idString.substring(4, 6)).append('/'); + sb.append(idString); + return sb.toString(); + } + + /** + * Returns the identified file object. If not existing, returns null. + * + * @param identifier data identifier + * @return identified file object + * @throws DataStoreException if any file system exception occurs + */ + protected FileObject getExistingFileObject(DataIdentifier identifier) throws DataStoreException { + String relPath = resolveFileObjectRelPath(identifier); + String [] segments = relPath.split("/"); + + FileObject tempFileObject = getBaseFolderObject(); + + try { + for (int i = 0; i < segments.length; i++) { + tempFileObject = tempFileObject.getChild(segments[i]); + + if (tempFileObject == null) { + return null; + } + } + + return tempFileObject; + } catch (FileSystemException e) { + throw new DataStoreException("File object not resolved: " + identifier, e); + } + } + + /** + * Returns true if the fileObject is used for touching purpose. + * + * @param fileObject file object + * @return true if the fileObject is used for touching purpose + */ + protected boolean isTouchFileObject(FileObject fileObject) { + if (fileObject.getName().getBaseName().endsWith(TOUCH_FILE_NAME_SUFFIX)) { + return true; + } + + return false; + } + + /** + * Returns the touch file for the fileObject. + * If there's no corresponding touch file existing, then returns null when {@code create} is false. + * When {@code create} is true, it creates a new touch file if no corresponding touch file exists. + * + * @param fileObject file object + * @param create create a touch file if not existing + * @return touch file object + * @throws DataStoreException if any file system exception occurs + */ + protected FileObject getTouchFileObject(FileObject fileObject, boolean create) throws DataStoreException { + try { + FileObject folderObject = fileObject.getParent(); + String touchFileName = fileObject.getName().getBaseName() + TOUCH_FILE_NAME_SUFFIX; + FileObject touchFileObject = folderObject.getChild(touchFileName); + + if (touchFileObject == null && create) { + touchFileObject = folderObject.resolveFile(touchFileName); + touchFileObject.createFile(); + touchFileObject = folderObject.getChild(touchFileName); + } + + return touchFileObject; + } catch (FileSystemException e) { + throw new DataStoreException("Touch file object not resolved: " + fileObject.getName().getFriendlyURI(), e); + } + } + + /** + * Returns the approximate number of threads that are actively executing asynchronous writing tasks. + * @return the approximate number of threads that are actively executing asynchronous writing tasks + */ + protected int getAsyncWriteExecutorActiveCount() { + Executor asyncExecutor = getAsyncWriteExecutor(); + + if (asyncExecutor != null && asyncExecutor instanceof ThreadPoolExecutor) { + return ((ThreadPoolExecutor) asyncExecutor).getActiveCount(); + } + + return 0; + } + + /** + * Copy the content of the local file ({@code srcFile}) to the record identified by the {@code identifier}. + * @param srcFile source local file + * @param identifier record identifier + * @throws IOException if any IO exception occurs + * @throws DataStoreException if any file system exception occurs + */ + private void copyFileContentToRecord(File srcFile, DataIdentifier identifier) throws IOException, DataStoreException { + String relPath = resolveFileObjectRelPath(identifier); + String [] segments = relPath.split("/"); + + InputStream input = null; + OutputStream output = null; + + try { + FileObject baseFolderObject = getBaseFolderObject(); + FileObject folderObject = null; + + for (int i = 0; i < segments.length - 1; i++) { + folderObject = VFSUtils.createChildFolder(baseFolderObject, segments[i]); + baseFolderObject = folderObject; + } + + FileObject destFileObject = VFSUtils.createChildFile(folderObject, segments[segments.length - 1]); + input = new FileInputStream(srcFile); + output = destFileObject.getContent().getOutputStream(); + IOUtils.copy(input, output); + } finally { + IOUtils.closeQuietly(output); + IOUtils.closeQuietly(input); + } + } + + /** + * Set the last modified time of a fileObject, if the fileObject is writable. + * @param fileObject the file object + * @throws DataStoreException if the fileObject is writable but modifying the date fails + */ + private void updateLastModifiedTime(FileObject fileObject) throws DataStoreException { + try { + if (isTouchFilePreferred()) { + getTouchFileObject(fileObject, true); + } else { + long time = System.currentTimeMillis() + ACCESS_TIME_RESOLUTION; + fileObject.getContent().setLastModifiedTime(time); + } + } catch (FileSystemException e) { + throw new DataStoreException("An IO Exception occurred while trying to set the last modified date: " + + fileObject.getName().getFriendlyURI(), e); + } + } + + /** + * Get the last modified time of a file object. + * @param fileObject the file object + * @return the last modified date + * @throws DataStoreException if reading fails + */ + private long getLastModifiedTime(FileObject fileObject) throws DataStoreException { + long lastModified = 0; + + try { + if (isTouchFilePreferred()) { + FileObject touchFile = getTouchFileObject(fileObject, false); + + if (touchFile != null) { + lastModified = touchFile.getContent().getLastModifiedTime(); + } else { + lastModified = fileObject.getContent().getLastModifiedTime(); + } + } else { + lastModified = fileObject.getContent().getLastModifiedTime(); + } + + if (lastModified == 0) { + throw new DataStoreException("Failed to read record modified date: " + fileObject.getName().getFriendlyURI()); + } + } catch (FileSystemException e) { + throw new DataStoreException("Failed to read record modified date: " + fileObject.getName().getFriendlyURI()); + } + + return lastModified; + } + + /** + * Scans {@code folderObject} and all the descendant folders to find record entries and push the record entry + * identifiers to {@code identifiers}. + * @param identifiers identifier list + * @param folderObject folder object + * @throws FileSystemException if any file system exception occurs + * @throws DataStoreException if any file system exception occurs + */ + private void pushIdentifiersRecursively(List identifiers, FileObject folderObject) + throws FileSystemException, DataStoreException { + FileType type; + + for (FileObject fileObject : VFSUtils.getChildFileOrFolders(folderObject)) { + type = fileObject.getType(); + + if (type == FileType.FOLDER) { + pushIdentifiersRecursively(identifiers, fileObject); + } else if (type == FileType.FILE) { + if (!isTouchFileObject(fileObject)) { + identifiers.add(new DataIdentifier(fileObject.getName().getBaseName())); + } + } + } + } + + /** + * Writes {@code file}'s content to the record entry identified by {@code identifier}. + * @param identifier record identifier + * @param file local file to copy from + * @param asyncUpload whether or not it should be done asynchronously + * @param callback asynchronous uploading callback instance + * @throws DataStoreException if any file system exception occurs + */ + private void write(DataIdentifier identifier, File file, boolean asyncUpload, AsyncUploadCallback callback) + throws DataStoreException { + AsyncUploadResult asyncUpRes = null; + + if (asyncUpload) { + asyncUpRes = new AsyncUploadResult(identifier, file); + } + + synchronized (this) { + FileObject fileObject = getExistingFileObject(identifier); + FileObject resolvedFileObject = resolveFileObject(identifier); + + try { + if (fileObject != null) { + updateLastModifiedTime(resolvedFileObject); + } else { + copyFileContentToRecord(file, identifier); + } + + if (asyncUpRes != null && callback != null) { + callback.onSuccess(asyncUpRes); + } + } catch (IOException e) { + DataStoreException e2 = new DataStoreException( + "Could not get output stream to object: " + resolvedFileObject.getName().getFriendlyURI(), e); + + if (asyncUpRes != null && callback != null) { + asyncUpRes.setException(e2); + callback.onFailure(asyncUpRes); + } + + throw e2; + } + } + } + + /** + * Touches the object entry file identified by {@code identifier}. + * @param identifier record identifier + * @param minModifiedDate minimum modified date time to be used in touching + * @param asyncTouch whether or not it should be done asynchronously + * @param callback asynchrounous touching callback instance + * @throws DataStoreException if any file system exception occurs + */ + private void touch(DataIdentifier identifier, long minModifiedDate, boolean asyncTouch, AsyncTouchCallback callback) + throws DataStoreException { + AsyncTouchResult asyncTouchRes = null; + + if (asyncTouch) { + asyncTouchRes = new AsyncTouchResult(identifier); + } + + try { + FileObject fileObject = getExistingFileObject(identifier); + + if (fileObject != null) { + if (minModifiedDate > 0 && minModifiedDate > getLastModifiedTime(fileObject)) { + updateLastModifiedTime(fileObject); + } + } else { + LOG.debug("File doesn't exist for the identifier: {}.", identifier); + } + } catch (DataStoreException e) { + if (asyncTouchRes != null) { + asyncTouchRes.setException(e); + } + + throw e; + } finally { + if (asyncTouchRes != null && callback != null) { + if (asyncTouchRes.getException() != null) { + callback.onFailure(asyncTouchRes); + } else { + callback.onSuccess(asyncTouchRes); + } + } + } + } + + /** + * Deletes record file object. + * @param fileObject file object to delete + * @return true if deleted + * @throws DataStoreException if any file system exception occurs + */ + private boolean deleteRecordFileObject(FileObject fileObject) throws DataStoreException { + if (isTouchFilePreferred()) { + try { + FileObject touchFile = getTouchFileObject(fileObject, false); + + if (touchFile != null) { + touchFile.delete(); + } + } catch (FileSystemException e) { + LOG.warn("Could not delete touch file for " + fileObject.getName().getFriendlyURI(), e); + } + } + + try { + return fileObject.delete(); + } catch (FileSystemException e) { + throw new DataStoreException("Could not delete record file at " + fileObject.getName().getFriendlyURI(), e); + } + } + + /** + * Deletes the parent folders of {@code fileObject} if a parent folder is empty. + * @param fileObject fileObject to start with + * @throws DataStoreException if any file system exception occurs + */ + private void deleteEmptyParentFolders(FileObject fileObject) throws DataStoreException { + try { + String baseFolderUri = getBaseFolderObject().getName().getFriendlyURI() + "/"; + FileObject parentFolder = fileObject.getParent(); + + // Only iterate & delete if parent folder of the blob file is + // child of the base directory and if it is empty + while (parentFolder.getName().getFriendlyURI().startsWith(baseFolderUri)) { + if (VFSUtils.hasAnyChildFileOrFolder(parentFolder)) { + break; + } + + boolean deleted = parentFolder.delete(); + LOG.debug("Deleted parent folder [{}] of file [{}]: {}", + new Object[] { parentFolder, fileObject.getName().getFriendlyURI(), deleted }); + parentFolder = parentFolder.getParent(); + } + } catch (IOException e) { + LOG.warn("Error in parents deletion for " + fileObject.getName().getFriendlyURI(), e); + } + } + + /** + * Deletes any descendant record files under {@code folderObject} if the record files are older than {@code timestamp}, + * and push all the deleted record identifiers into {@code deleteIdSet}. + * @param deleteIdSet set to store all the deleted record identifiers + * @param folderObject folder object to start with + * @param timestamp timestamp + * @throws FileSystemException if any file system exception occurs + * @throws DataStoreException if any file system exception occurs + */ + private void deleteOlderRecursive(Set deleteIdSet, FileObject folderObject, long timestamp) + throws FileSystemException, DataStoreException { + FileType type; + DataIdentifier identifier; + + for (FileObject fileObject : VFSUtils.getChildFileOrFolders(folderObject)) { + type = fileObject.getType(); + + if (type == FileType.FOLDER) { + deleteOlderRecursive(deleteIdSet, fileObject, timestamp); + + synchronized (this) { + if (!VFSUtils.hasAnyChildFileOrFolder(fileObject)) { + fileObject.delete(); + } + } + + } else if (type == FileType.FILE) { + long lastModified = getLastModifiedTime(fileObject); + + if (lastModified < timestamp) { + identifier = new DataIdentifier(fileObject.getName().getBaseName()); + + if (getDataStore().confirmDelete(identifier)) { + getDataStore().deleteFromCache(identifier); + + if (LOG.isInfoEnabled()) { + LOG.info("Deleting old file " + fileObject.getName().getFriendlyURI() + " modified: " + + new Timestamp(lastModified).toString() + " length: " + + fileObject.getContent().getSize()); + } + + if (deleteRecordFileObject(fileObject)) { + deleteIdSet.add(identifier); + } else { + LOG.warn("Failed to delete old file " + fileObject.getName().getFriendlyURI()); + } + } + } + } + } + } + + /** + * This class implements {@link Runnable} interface to copy {@link File} to VFS file object asynchronously. + */ + private class AsyncUploadJob implements Runnable { + + /** + * Record data identifier. + */ + private DataIdentifier identifier; + + /** + * Source file to upload. + */ + private File file; + + /** + * Callback to handle events on completion, failure or abortion. + */ + private AsyncUploadCallback callback; + + /** + * Constructs an asynchronous file uploading job. + * @param identifier record data identifier + * @param file source file to upload + * @param callback callback to handle events on completion, failure or abortion. + */ + public AsyncUploadJob(DataIdentifier identifier, File file, AsyncUploadCallback callback) { + super(); + this.identifier = identifier; + this.file = file; + this.callback = callback; + } + + /** + * Executes this job. + */ + public void run() { + try { + write(identifier, file, true, callback); + } catch (DataStoreException e) { + LOG.error("Could not upload [" + identifier + "], file[" + file + "]", e); + } + } + } + + /** + * This class implements {@link Runnable} interface to touch a VFS file object asynchronously. + */ + private class AsyncTouchJob implements Runnable { + + /** + * Record data identifier. + */ + private DataIdentifier identifier; + + /** + * Minimum modification time in milliseconds to be used in touching. + */ + private long minModifiedDate; + + /** + * Callback to handle events on completion, failure or abortion. + */ + private AsyncTouchCallback callback; + + /** + * Constructs an asynchronous record touching job. + * @param identifier record data identifier + * @param minModifiedDate minimum modification time in milliseconds to be used in touching + * @param callback callback to handle events on completion, failure or abortion + */ + public AsyncTouchJob(DataIdentifier identifier, long minModifiedDate, AsyncTouchCallback callback) { + super(); + this.identifier = identifier; + this.minModifiedDate = minModifiedDate; + this.callback = callback; + } + + /** + * Executes this job. + */ + public void run() { + try { + touch(identifier, minModifiedDate, true, callback); + } catch (DataStoreException e) { + LOG.error("Could not touch [" + identifier + "]", e); + } + } + } +} diff --git a/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSDataStore.java b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSDataStore.java new file mode 100644 index 00000000000..5e03d66ad28 --- /dev/null +++ b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSDataStore.java @@ -0,0 +1,438 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.vfs.ext.ds; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.Enumeration; +import java.util.Properties; + +import javax.jcr.RepositoryException; + +import org.apache.commons.vfs2.FileName; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; +import org.apache.commons.vfs2.FileSystemManager; +import org.apache.commons.vfs2.FileSystemOptions; +import org.apache.commons.vfs2.impl.DefaultFileSystemManager; +import org.apache.commons.vfs2.impl.StandardFileSystemManager; +import org.apache.commons.vfs2.util.DelegatingFileSystemOptionsBuilder; +import org.apache.jackrabbit.core.data.Backend; +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Commons VFS based data store. + */ +public class VFSDataStore extends CachingDataStore { + + static final String BASE_FOLDER_URI = "baseFolderUri"; + + static final String ASYNC_WRITE_POOL_SIZE = "asyncWritePoolSize"; + + static final String FILE_SYSTEM_MANAGER_CLASS_NAME = "fileSystemManagerClassName"; + + /** + * Logger instance. + */ + private static final Logger LOG = LoggerFactory.getLogger(VFSDataStore.class); + + /** + * Property key name for maximum backend connection. e.g. max total http connections. + */ + static final String PROP_MAX_TOTAL_CONNECTIONS = "maxTotalConnections"; + + /** + * Property key name for maximum backend connection. e.g. max http connections per host. + */ + static final String PROP_MAX_CONNECTIONS_PER_HOST = "maxConnectionsPerHost"; + + /** + * Default maximum backend connection. e.g. max http connection. + */ + static final int DEFAULT_MAX_CONNECTION = 200; + + /** + * Property key prefix for FielSystemOptions. + */ + private static final String FILE_SYSTEM_OPTIONS_PROP_PREFIX = "fso."; + + /** + * The class name of the VFS {@link FileSystemManager} instance used in this VFS data store. + * If this is not set, then {@link StandardFileSystemManager} is used by default. + */ + private String fileSystemManagerClassName; + + /** + * The VFS {@link FileSystemManager} instance used in this VFS data store. + * If {@link #fileSystemManagerClassName} is not set, then a {@link StandardFileSystemManager} instance is created by default. + */ + private FileSystemManager fileSystemManager; + + /** + * {@link FileSystemOptions} used when resolving the {@link #baseFolder}. + */ + private FileSystemOptions fileSystemOptions; + + /** + * Properties used when building a {@link FileSystemOptions} using {@link DelegatingFileSystemOptionsBuilder}. + */ + private Properties fileSystemOptionsProperties; + + /** + * The VFS base folder URI where all the entry files are maintained. + */ + private String baseFolderUri; + + /** + * The VFS base folder object where all the entry files are maintained. + */ + private FileObject baseFolder; + + /** + * The pool size of asynchronous write pooling executor. + */ + private int asyncWritePoolSize = VFSBackend.DEFAULT_ASYNC_WRITE_POOL_SIZE; + + @Override + public void init(String homeDir) throws RepositoryException { + overridePropertiesFromConfig(); + + if (baseFolderUri == null) { + throw new RepositoryException("VFS base folder URI must be set."); + } + + fileSystemManager = createFileSystemManager(); + + FileName baseFolderName = null; + + try { + baseFolderName = fileSystemManager.resolveURI(baseFolderUri); + + FileSystemOptions fso = getFileSystemOptions(); + + if (fso != null) { + baseFolder = fileSystemManager.resolveFile(baseFolderUri, fso); + } else { + baseFolder = fileSystemManager.resolveFile(baseFolderUri); + } + + baseFolder.createFolder(); + } catch (FileSystemException e) { + throw new RepositoryException("Could not initialize the VFS base folder at '" + + (baseFolderName == null ? "" : baseFolderName.getFriendlyURI()) + "'.", e); + } + + super.init(homeDir); + } + + @Override + public void close() throws DataStoreException { + VFSBackend backend = (VFSBackend) getBackend(); + + try { + // Let's wait for 5 minutes at max if there are still execution jobs in the async writing executor's queue. + int seconds = 0; + while (backend.getAsyncWriteExecutorActiveCount() > 0 && seconds++ < 300) { + Thread.sleep(1000); + } + } catch (InterruptedException e) { + LOG.warn("Interrupted while waiting for the async write executor to complete.", e); + } + + // Commenting out the following because the javadoc of FileSystemManager#closeFileSystem(FileSystem) + // says it is dangerous when singleton instance is being used, which is the case as VFSDataStore keeps + // single file system manager instance. + // Also, VFS seems to remove the related provider component on that invocation. +// if (fileSystemManager != null) { +// fileSystemManager.closeFileSystem(baseFolder.getFileSystem()); +// } + + super.close(); + } + + /** + * {@inheritDoc} + */ + @Override + protected Backend createBackend() { + VFSBackend backend = new VFSBackend(baseFolder); + backend.setAsyncWritePoolSize(getAsyncWritePoolSize()); + return backend; + } + + /** + * {@inheritDoc} + */ + @Override + protected String getMarkerFile() { + return "vfs.init.done"; + } + + /** + * Returns the class name of the VFS {@link FileSystemManager} instance used in this VFS data store. + * @return + */ + public String getFileSystemManagerClassName() { + return fileSystemManagerClassName; + } + + /** + * Sets the class name of the VFS {@link FileSystemManager} instance used in this VFS data store. + * If this is not set, then {@link StandardFileSystemManager} is used by default. + * @param fileSystemManagerClassName + */ + public void setFileSystemManagerClassName(String fileSystemManagerClassName) { + this.fileSystemManagerClassName = fileSystemManagerClassName; + } + + /** + * Returns the VFS {@link FileSystemManager} instance used in this VFS data store. + * @return the VFS {@link FileSystemManager} instance used in this VFS data store + */ + public FileSystemManager getFileSystemManager() { + return fileSystemManager; + } + + /** + * Returns {@link FileSystemOptions} instance used when resolving the {@link #baseFolder}. + * This may return null if {@link FileSystemOptions} instance was not injected or + * a {@link #fileSystemOptionsProperties} instance cannot be injected or created. + * Therefore, the caller should check whether or not this returns null. + * When returning null, the caller may not use a {@link FileSystemOptions} instance. + * @return {@link FileSystemOptions} instance used when resolving the {@link #baseFolder} + */ + public FileSystemOptions getFileSystemOptions() throws RepositoryException { + if (fileSystemOptions == null) { + fileSystemOptions = createFileSystemOptions(); + } + + return fileSystemOptions; + } + + /** + * Sets the {@link FileSystemOptions} instance used when resolving the {@link #baseFolder}. + * @param fileSystemOptions {@link FileSystemOptions} instance used when resolving the {@link #baseFolder} + */ + public void setFileSystemOptions(FileSystemOptions fileSystemOptions) { + this.fileSystemOptions = fileSystemOptions; + } + + /** + * Sets the properties used when building a {@link FileSystemOptions}, using {@link DelegatingFileSystemOptionsBuilder}. + * @param fileSystemOptionsProperties properties used when building a {@link FileSystemOptions} + */ + public void setFileSystemOptionsProperties(Properties fileSystemOptionsProperties) { + this.fileSystemOptionsProperties = fileSystemOptionsProperties; + } + + /** + * Sets the properties in a semi-colon delimited string used when building a {@link FileSystemOptions}, + * using {@link DelegatingFileSystemOptionsBuilder}. + * @param fileSystemOptionsPropertiesInString properties in String + */ + public void setFileSystemOptionsPropertiesInString(String fileSystemOptionsPropertiesInString) { + if (fileSystemOptionsPropertiesInString != null) { + try { + StringReader reader = new StringReader(fileSystemOptionsPropertiesInString); + Properties props = new Properties(); + props.load(reader); + fileSystemOptionsProperties = props; + } catch (IOException e) { + throw new IllegalArgumentException("Could not load file system options properties.", e); + } + } + } + + /** + * Sets the base VFS folder URI. + * @param baseFolderUri base VFS folder URI + */ + public void setBaseFolderUri(String baseFolderUri) { + this.baseFolderUri = baseFolderUri; + } + + /** + * Returns the pool size of the async write pool executor. + * @return the pool size of the async write pool executor + */ + public int getAsyncWritePoolSize() { + return asyncWritePoolSize; + } + + /** + * Sets the pool size of the async write pool executor. + * @param asyncWritePoolSize pool size of the async write pool executor + */ + public void setAsyncWritePoolSize(int asyncWritePoolSize) { + this.asyncWritePoolSize = asyncWritePoolSize; + } + + /** + * Creates a {@link FileSystemManager} instance. + * @return a {@link FileSystemManager} instance. + * @throws RepositoryException if an error occurs creating the manager. + */ + protected FileSystemManager createFileSystemManager() throws RepositoryException { + FileSystemManager fileSystemManager = null; + + try { + if (getFileSystemManagerClassName() == null) { + fileSystemManager = new StandardFileSystemManager(); + } else { + final Class mgrClass = Class.forName(getFileSystemManagerClassName()); + fileSystemManager = (FileSystemManager) mgrClass.newInstance(); + } + + if (fileSystemManager instanceof DefaultFileSystemManager) { + ((DefaultFileSystemManager) fileSystemManager).init(); + } + } catch (final FileSystemException e) { + throw new RepositoryException( + "Could not initialize file system manager of class: " + getFileSystemManagerClassName(), e); + } catch (final Exception e) { + throw new RepositoryException( + "Could not create file system manager of class: " + getFileSystemManagerClassName(), e); + } + + return fileSystemManager; + } + + /** + * Builds and returns {@link FileSystemOptions} instance which is used when resolving the {@link #baseFolder} + * during the initialization. + * If {@link #fileSystemOptionsProperties} is available, this scans all the property key names starting with {@link #FILE_SYSTEM_OPTIONS_PROP_PREFIX} + * and uses the rest of the key name after the {@link #FILE_SYSTEM_OPTIONS_PROP_PREFIX} as the combination of scheme and property name + * when building a {@link FileSystemOptions} using {@link DelegatingFileSystemOptionsBuilder}. + * @return {@link FileSystemOptions} instance which is used when resolving the {@link #baseFolder} during the initialization + * @throws RepositoryException if any file system exception occurs + */ + protected FileSystemOptions createFileSystemOptions() throws RepositoryException { + FileSystemOptions fso = null; + + if (fileSystemOptionsProperties != null) { + try { + fso = new FileSystemOptions(); + DelegatingFileSystemOptionsBuilder delegate = new DelegatingFileSystemOptionsBuilder(getFileSystemManager()); + + String key; + String schemeDotPropName; + String scheme; + String propName; + String value; + int offset; + + for (Enumeration e = fileSystemOptionsProperties.propertyNames(); e.hasMoreElements(); ) { + key = (String) e.nextElement(); + + if (key.startsWith(FILE_SYSTEM_OPTIONS_PROP_PREFIX)) { + value = fileSystemOptionsProperties.getProperty(key); + schemeDotPropName = key.substring(FILE_SYSTEM_OPTIONS_PROP_PREFIX.length()); + offset = schemeDotPropName.indexOf('.'); + + if (offset > 0) { + scheme = schemeDotPropName.substring(0, offset); + propName = schemeDotPropName.substring(offset + 1); + delegate.setConfigString(fso, scheme, propName, value); + } else { + LOG.warn("Ignoring an FileSystemOptions property in invalid format. Key: {}, Value: {}", key, value); + } + } + } + } catch (FileSystemException e) { + throw new RepositoryException("Could not create File System Options.", e); + } + } + + return fso; + } + + /** + * Returns properties used when building a {@link FileSystemOptions} instance by the properties + * during the initialization. + * @return properties used when building a {@link FileSystemOptions} instance by the properties during the initialization + */ + protected Properties getFileSystemOptionsProperties() { + return fileSystemOptionsProperties; + } + + private void overridePropertiesFromConfig() throws RepositoryException { + final String config = getConfig(); + + // If config param provided, then override properties from the config file. + if (config != null && !"".equals(config)) { + try { + final Properties props = readConfig(config); + + String propValue = props.getProperty(ASYNC_WRITE_POOL_SIZE); + if (propValue != null && !"".equals(propValue)) { + setAsyncWritePoolSize(Integer.parseInt(propValue)); + } + + propValue = props.getProperty(BASE_FOLDER_URI); + if (propValue != null && !"".equals(propValue)) { + setBaseFolderUri(propValue); + } + + propValue = props.getProperty(FILE_SYSTEM_MANAGER_CLASS_NAME); + if (propValue != null && !"".equals(propValue)) { + setFileSystemManagerClassName(propValue); + } + + final Properties fsoProps = new Properties(); + String propName; + for (Enumeration propNames = props.propertyNames(); propNames.hasMoreElements(); ) { + propName = (String) propNames.nextElement(); + if (propName.startsWith(FILE_SYSTEM_OPTIONS_PROP_PREFIX)) { + fsoProps.setProperty(propName, props.getProperty(propName)); + } + } + if (!fsoProps.isEmpty()) { + this.setFileSystemOptionsProperties(fsoProps); + } + } catch (IOException e) { + throw new RepositoryException("Configuration file doesn't exist at '" + config + "'."); + } + } + } + + private Properties readConfig(String fileName) throws IOException { + if (!new File(fileName).exists()) { + throw new IOException("Config file not found: " + fileName); + } + + Properties prop = new Properties(); + InputStream in = null; + + try { + in = new FileInputStream(fileName); + prop.load(in); + } finally { + if (in != null) { + in.close(); + } + } + + return prop; + } + +} diff --git a/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSUtils.java b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSUtils.java new file mode 100644 index 00000000000..cab4eecf0ea --- /dev/null +++ b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/VFSUtils.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.vfs.ext.ds; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; +import org.apache.commons.vfs2.FileType; +import org.apache.jackrabbit.core.data.DataStoreException; + +/** + * VFS Utilities. + */ +class VFSUtils { + + /** + * A set to filter out other files other than {@link FileType#FILE}. + */ + static Set FILE_ONLY_TYPES = Collections + .unmodifiableSet(new HashSet(Arrays.asList(FileType.FILE))); + + /** + * A set to filter out other files other than {@link FileType#FOLDER}. + */ + static Set FOLDER_ONLY_TYPES = Collections + .unmodifiableSet(new HashSet(Arrays.asList(FileType.FOLDER))); + + /** + * A set to filter out other files other than {@link FileType#FILE} or {@link FileType#FOLDER}. + */ + static Set FILE_OR_FOLDER_TYPES = Collections + .unmodifiableSet(new HashSet(Arrays.asList(FileType.FILE, FileType.FOLDER))); + + /** + * Creates a child folder by the {@code name} under the {@code baseFolder} and retrieves the created folder. + * @param baseFolder base folder object + * @param name child folder name + * @return a child folder by the {@code name} under the {@code baseFolder} and retrieves the created folder + * @throws DataStoreException if any file system exception occurs + */ + static FileObject createChildFolder(FileObject baseFolder, String name) throws DataStoreException { + FileObject childFolder = null; + + try { + childFolder = baseFolder.resolveFile(name); + + if (!childFolder.exists()) { + childFolder.createFolder(); + childFolder = baseFolder.getChild(childFolder.getName().getBaseName()); + } + } catch (FileSystemException e) { + throw new DataStoreException( + "Could not create a child folder, '" + name + "' under " + baseFolder.getName().getFriendlyURI(), + e); + } + + return childFolder; + } + + /** + * Creates a child file by the {@code name} under the {@code baseFolder} and retrieves the created file. + * @param baseFolder base folder object + * @param name child file name + * @return a child file by the {@code name} under the {@code baseFolder} and retrieves the created file + * @throws DataStoreException if any file system exception occurs + */ + static FileObject createChildFile(FileObject baseFolder, String name) throws DataStoreException { + FileObject childFile = null; + + try { + childFile = baseFolder.resolveFile(name); + + if (!childFile.exists()) { + childFile.createFile(); + childFile = baseFolder.getChild(childFile.getName().getBaseName()); + } + } catch (FileSystemException e) { + throw new DataStoreException( + "Could not create a child file, '" + name + "' under " + baseFolder.getName().getFriendlyURI(), + e); + } + + return childFile; + } + + /** + * Returns child files under {@code folderObject} after filtering out files other than {@link FileType#FILE}. + * @param folderObject folder object + * @return child files under {@code folderObject} after filtering out files other than {@link FileType#FILE} + * @throws DataStoreException if any file system exception occurs + */ + static List getChildFiles(FileObject folderObject) throws DataStoreException { + return getChildrenOfTypes(folderObject, FILE_ONLY_TYPES); + } + + /** + * Returns child folders under {@code folderObject} after filtering out files other than {@link FileType#FOLDER}. + * @param folderObject folder object + * @return child folders under {@code folderObject} after filtering out files other than {@link FileType#FOLDER} + * @throws DataStoreException if any file system exception occurs + */ + static List getChildFolders(FileObject folderObject) throws DataStoreException { + return getChildrenOfTypes(folderObject, FOLDER_ONLY_TYPES); + } + + /** + * Returns child file objects under {@code folderObject} after filtering out files other than {@link FileType#FILE} or {@link FileType#FOLDER}. + * @param folderObject folder object + * @return child file objects under {@code folderObject} after filtering out files other than {@link FileType#FILE} or {@link FileType#FOLDER} + * @throws DataStoreException if any file system exception occurs + */ + static List getChildFileOrFolders(FileObject folderObject) throws DataStoreException { + return getChildrenOfTypes(folderObject, FILE_OR_FOLDER_TYPES); + } + + /** + * Returns true if {@code folderObject} has any file or folder child objects. + * @param folderObject folder object + * @return true if {@code folderObject} has any file or folder child objects + * @throws DataStoreException if any file system exception occurs + */ + static boolean hasAnyChildFileOrFolder(FileObject folderObject) throws DataStoreException { + return !getChildFileOrFolders(folderObject).isEmpty(); + } + + private static List getChildrenOfTypes(FileObject folderObject, Set fileTypes) throws DataStoreException { + try { + String folderBaseName = folderObject.getName().getBaseName(); + FileObject [] children = folderObject.getChildren(); + List files = new ArrayList(children.length); + String childBaseName; + + for (int i = 0; i < children.length; i++) { + childBaseName = children[i].getName().getBaseName(); + FileType fileType = null; + + try { + fileType = children[i].getType(); + } catch (FileSystemException notDetermineTypeEx) { + if (folderBaseName.equals(childBaseName)) { + // Ignore this case. + // Some WebDAV server or VFS seems to include the folder itself as child as imaginary file type, + // and throw FileSystemException saying "Could not determine the type of file" in this case. + } else { + throw notDetermineTypeEx; + } + } + + if (fileType != null && fileTypes.contains(fileType)) { + files.add(children[i]); + } + } + + return files; + } catch (FileSystemException e) { + throw new DataStoreException( + "Could not find children under " + folderObject.getName().getFriendlyURI(), e); + } + } + + private VFSUtils() { + } + +} diff --git a/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/package-info.java b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/package-info.java new file mode 100755 index 00000000000..adf1b1edc7a --- /dev/null +++ b/jackrabbit-vfs-ext/src/main/java/org/apache/jackrabbit/vfs/ext/ds/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* see JCR-4060 */ +@org.osgi.annotation.versioning.Version("2.13.5") +package org.apache.jackrabbit.vfs.ext.ds; diff --git a/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/TestAll.java b/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/TestAll.java new file mode 100644 index 00000000000..1f98096403e --- /dev/null +++ b/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/TestAll.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.vfs.ext; + +import org.apache.jackrabbit.vfs.ext.ds.TestVFSDataStore; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all test cases for the this module. + */ +public class TestAll extends TestCase { + + /** + * TestAll suite that executes all tests inside this module. + */ + public static Test suite() { + TestSuite suite = new TestSuite("VFS tests"); + suite.addTestSuite(TestVFSDataStore.class); + return suite; + } +} diff --git a/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/ds/TestVFSDataStore.java b/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/ds/TestVFSDataStore.java new file mode 100644 index 00000000000..c8c89d2162a --- /dev/null +++ b/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/ds/TestVFSDataStore.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.vfs.ext.ds; + +import java.io.File; +import java.io.FileInputStream; +import java.io.StringReader; +import java.util.Properties; + +import javax.jcr.RepositoryException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemOptions; +import org.apache.commons.vfs2.provider.sftp.SftpFileSystemConfigBuilder; +import org.apache.jackrabbit.core.data.CachingDataStore; +import org.apache.jackrabbit.core.data.TestCaseBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; + +import junit.framework.Assert; + +/** + * Test {@link CachingDataStore} with VFSBackend with a VFS file system (local file system) by default. + *

        + * You can provide different properties to use other file system backend by passing vfs config file via system property. + * For e.g. -Dconfig=/opt/repository/vfs-webdav.properties. + *

        + *

        + * Sample VFS properties located at src/test/resources/vfs-*.properties + *

        + */ +public class TestVFSDataStore extends TestCaseBase { + + /** + * Logger instance. + */ + private static final Logger LOG = LoggerFactory.getLogger(TestVFSDataStore.class); + + // Example when http provider or its variants (https or webdav) FSO is used. + @SuppressWarnings("unused") + private static final String HTTP_FILE_SYSTEM_OPTIONS_PARAM_XML = + ""; + + private static final String SFTP_FILE_SYSTEM_OPTIONS_PARAM_XML = + ""; + + private String baseFolderUri; + + private VFSDataStore dataStore; + + private Properties configProps; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + String configProp = System.getProperty("config"); + + if (configProp != null && !"".equals(configProp)) { + Properties props = new Properties(); + File configFile = new File(configProp); + + if (configFile.isFile()) { + FileInputStream fis = null; + + try { + fis = new FileInputStream(configFile); + props.load(fis); + } finally { + IOUtils.closeQuietly(fis); + } + } else { + props = loadProperties(configProp); + } + + configProps = props; + } + } + + @Override + protected CachingDataStore createDataStore() throws RepositoryException { + dataStore = new VFSDataStore(); + Properties props = getConfigProps(); + baseFolderUri = props.getProperty(VFSDataStore.BASE_FOLDER_URI); + dataStore.setBaseFolderUri(baseFolderUri); + LOG.info("baseFolderUri [{}] set.", baseFolderUri); + String value = props.getProperty(VFSDataStore.ASYNC_WRITE_POOL_SIZE); + if (value != null) { + dataStore.setAsyncWritePoolSize(Integer.parseInt(value)); + LOG.info("asyncWritePoolSize [{}] set.", dataStore.getAsyncWritePoolSize()); + } + dataStore.setFileSystemOptionsProperties(props); + dataStore.setSecret("123456"); + dataStore.init(dataStoreDir); + return dataStore; + } + + @Override + protected void tearDown() { + LOG.info("cleaning vfsBaseFolderUri [{}]", baseFolderUri); + VFSBackend backend = (VFSBackend) dataStore.getBackend(); + FileObject vfsBaseFolder = backend.getBaseFolderObject(); + + try { + // Let's wait for 5 minutes at max if there are still execution jobs in the async writing executor's queue. + int seconds = 0; + while (backend.getAsyncWriteExecutorActiveCount() > 0 && seconds++ < 300) { + Thread.sleep(1000); + } + + VFSTestUtils.deleteAllDescendantFiles(vfsBaseFolder); + } catch (Exception e) { + LOG.error("Failed to clean base folder at '{}'.", vfsBaseFolder.getName().getFriendlyURI(), e); + } + + super.tearDown(); + } + + /** + * Test case to validate {@link VFSDataStore#setFileSystemOptionsPropertiesInString(String)}. + */ + public void testSetFileSystemOptionsPropertiesInString() throws Exception { + try { + long start = System.currentTimeMillis(); + LOG.info("Testcase: " + this.getClass().getName() + + "#setFileSystemOptionsPropertiesInString, testDir=" + dataStoreDir); + doSetFileSystemOptionsPropertiesInString(); + LOG.info("Testcase: " + this.getClass().getName() + + "#setFileSystemOptionsPropertiesInString finished, time taken = [" + + (System.currentTimeMillis() - start) + "]ms"); + } catch (Exception e) { + LOG.error("error:", e); + fail(e.getMessage()); + } + } + + /** + * Test {@link VFSDataStore#setFileSystemOptionsPropertiesInString(String)} and validate the internal properties. + */ + protected void doSetFileSystemOptionsPropertiesInString() throws Exception { + dataStore = new VFSDataStore(); + Properties props = getConfigProps(); + baseFolderUri = props.getProperty(VFSDataStore.BASE_FOLDER_URI); + dataStore.setBaseFolderUri(baseFolderUri); + LOG.info("baseFolderUri [{}] set.", baseFolderUri); + dataStore.setFileSystemOptionsProperties(props); + dataStore.setSecret("123456"); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new InputSource(new StringReader(SFTP_FILE_SYSTEM_OPTIONS_PARAM_XML))); + Element paramElem = document.getDocumentElement(); + String propsInString = paramElem.getAttribute("value"); + dataStore.setFileSystemOptionsPropertiesInString(propsInString); + final Properties internalProps = dataStore.getFileSystemOptionsProperties(); + Assert.assertEquals("/home/tester/.ssh/id_rsa", internalProps.getProperty("fso.sftp.identities")); + Assert.assertEquals("30000", internalProps.getProperty("fso.sftp.timeout")); + + dataStore.init(dataStoreDir); + + final FileSystemOptions fso = dataStore.getFileSystemOptions(); + final SftpFileSystemConfigBuilder configBuilder = SftpFileSystemConfigBuilder.getInstance(); + File [] identities = configBuilder.getIdentities(fso); + Assert.assertNotNull(identities); + Assert.assertEquals(1, identities.length); + Assert.assertEquals("/home/tester/.ssh/id_rsa", FilenameUtils.separatorsToUnix(identities[0].getPath())); + Assert.assertEquals(Integer.valueOf(30000), configBuilder.getTimeout(fso)); + + dataStore.close(); + } + + private Properties getConfigProps() { + if (configProps == null) { + Properties props = new Properties(); + String baseFolderUri = new File(new File(dataStoreDir), "vfsds").toURI().toString(); + props.setProperty(VFSDataStore.BASE_FOLDER_URI, baseFolderUri); + // By default (when testing with local file system), disable asynchronous writing to the backend. + props.setProperty(VFSDataStore.ASYNC_WRITE_POOL_SIZE, "0"); + configProps = props; + } + + return configProps; + } +} diff --git a/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/ds/VFSTestUtils.java b/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/ds/VFSTestUtils.java new file mode 100644 index 00000000000..8ffae651387 --- /dev/null +++ b/jackrabbit-vfs-ext/src/test/java/org/apache/jackrabbit/vfs/ext/ds/VFSTestUtils.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.vfs.ext.ds; + +import java.util.List; + +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; +import org.apache.commons.vfs2.FileType; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * VFS Test Utilities. + */ +class VFSTestUtils { + + /** + * Logger instance. + */ + private static final Logger LOG = LoggerFactory.getLogger(VFSTestUtils.class); + + /** + * Deletes all the descendant files under the folder. + * @param folderObject folder object + * @throws FileSystemException if any file system exception occurs + * @throws DataStoreException if any file system exception occurs + */ + static void deleteAllDescendantFiles(FileObject folderObject) throws FileSystemException, DataStoreException { + List children = VFSUtils.getChildFileOrFolders(folderObject); + FileType fileType; + + for (FileObject child : children) { + fileType = child.getType(); + + if (fileType == FileType.FILE) { + boolean deleted = child.delete(); + + if (!deleted) { + LOG.warn("File not deleted: {}", child.getName().getFriendlyURI()); + } + } else if (fileType == FileType.FOLDER) { + deleteAllDescendantFiles(child); + } + } + } + + private VFSTestUtils() { + } + +} diff --git a/jackrabbit-vfs-ext/src/test/resources/log4j.properties b/jackrabbit-vfs-ext/src/test/resources/log4j.properties new file mode 100644 index 00000000000..86450359ab6 --- /dev/null +++ b/jackrabbit-vfs-ext/src/test/resources/log4j.properties @@ -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. +# + +# this is the log4j configuration for the JCR API tests +log4j.rootLogger=INFO, file + +#log4j.logger.org.apache.jackrabbit.test=DEBUG + +# 'file' is set to be a FileAppender. +log4j.appender.file=org.apache.log4j.FileAppender +log4j.appender.file.File=target/debug.log + +# 'file' uses PatternLayout. +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m (%F, line %L)\n diff --git a/jackrabbit-vfs-ext/src/test/resources/vfs-sftp.properties b/jackrabbit-vfs-ext/src/test/resources/vfs-sftp.properties new file mode 100644 index 00000000000..e9fe3802fae --- /dev/null +++ b/jackrabbit-vfs-ext/src/test/resources/vfs-sftp.properties @@ -0,0 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# VFS base folder URI +baseFolderUri = sftp://tester:secret@localhost/vfsds diff --git a/jackrabbit-vfs-ext/src/test/resources/vfs-webdav.properties b/jackrabbit-vfs-ext/src/test/resources/vfs-webdav.properties new file mode 100644 index 00000000000..ccc27756f70 --- /dev/null +++ b/jackrabbit-vfs-ext/src/test/resources/vfs-webdav.properties @@ -0,0 +1,24 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# VFS base folder URI +#baseFolderUri = webdav://tester:secret@localhost:8888/vfsds +baseFolderUri = webdav://localhost:8888/vfsds + +# VFS File System Options +fso.http.maxTotalConnections = 200 +fso.http.maxConnectionsPerHost = 200 diff --git a/jackrabbit-webapp/README.txt b/jackrabbit-webapp/README.txt new file mode 100644 index 00000000000..c35e1bbd0a3 --- /dev/null +++ b/jackrabbit-webapp/README.txt @@ -0,0 +1,22 @@ +===================================== +Welcome to Jackrabbit Web Application +===================================== + +This is the Web Application component of the Apache Jackrabbit project. +This component provides servlets used to access a Jackrabbit repository: + + * RepositoryAccessServlet.java + * LoggingServlet.java + * RepositoryStartupServlet.java + +In addition, the project contains 2 different WebDAV servlets: + + * SimpleWebdavServlet.java + Adds WebDAV support (DAV 1,2) to your jackrabbit repository. + + * JCRWebdavServerServlet.java + A servlet used to remote JSR170 calls via WebDAV. + IMPORTANT: Please note, that this servlet is not intended to provide + common WebDAV support to the repository. Instead the primary goal is to + remote JSR170 calls. + For the corresponding client see -> jackrabbit-jcr2dav (work in progress). diff --git a/jackrabbit-webapp/pom.xml b/jackrabbit-webapp/pom.xml new file mode 100644 index 00000000000..8da469872a5 --- /dev/null +++ b/jackrabbit-webapp/pom.xml @@ -0,0 +1,236 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-webapp + war + Jackrabbit Web Application + Web application that hosts and serves a Jackrabbit content repository + + + 8.5.32 + + + + + javax.jcr + jcr + + + org.apache.jackrabbit + jackrabbit-core + ${project.version} + + + org.apache.tika + tika-parsers + + + commons-logging + commons-logging + + + + + org.slf4j + jcl-over-slf4j + + + org.apache.jackrabbit + jackrabbit-jcr-server + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-servlet + ${project.version} + + + org.apache.jackrabbit + jackrabbit-jcr-rmi + ${project.version} + + + ch.qos.logback + logback-classic + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + + junit + junit + test + + + org.apache.tomcat + tomcat-catalina + ${tomcat.version} + test + + + org.apache.tomcat + tomcat-coyote + ${tomcat.version} + test + + + org.apache.tomcat + tomcat-jasper + ${tomcat.version} + test + + + net.sourceforge.htmlunit + htmlunit + 2.8 + test + + + commons-logging + commons-logging + + + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + test + + + org.apache.james + apache-mime4j + 0.6 + test + + + org.apache.httpcomponents + httpcore + 4.4.10 + test + + + + + + + org.eclipse.jetty + maven-jetty-plugin + ${jetty.version} + + 10 + + + 8080 + 60000 + + + + + + + maven-antrun-plugin + + + package + + + + + + + run + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + attach-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/jackrabbit-webapp-${project.version}.jar + jar + + + + + + + + org.apache.rat + apache-rat-plugin + + + src/main/webapp/WEB-INF/log4j.dtd + + + + + + + + maven-failsafe-plugin + + + + derby.stream.error.file + target/derby.log + + + + **/BackwardsCompatibilityIT.java + + + + + + + + diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/AbstractConfig.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/AbstractConfig.java new file mode 100644 index 00000000000..4c2c4b2ef30 --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/AbstractConfig.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.j2ee; + +import org.apache.commons.collections.BeanMap; +import org.apache.jackrabbit.util.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Properties; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +/** + * Abstract configuration class that is based on a bean map. + */ +public abstract class AbstractConfig { + + /** + * default logger + */ + private static final Logger log = LoggerFactory.getLogger(AbstractConfig.class); + + protected boolean valid; + + private BeanMap map = new BeanMap(this); + + /** + * Initializes the configuration with values from the given properties + * @param props the configuration properties + */ + public void init(Properties props) throws ServletException { + Iterator iter = props.keySet().iterator(); + while (iter.hasNext()) { + String name = (String) iter.next(); + String mapName = toMapName(name, '.'); + try { + if (map.containsKey(mapName)) { + map.put(mapName, props.getProperty(name)); + } + } catch (Exception e) { + throw new ServletExceptionWithCause( + "Invalid configuration property: " + name, e); + } + } + } + + public void init(ServletConfig ctx) throws ServletException { + Enumeration names = ctx.getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + String mapName = toMapName(name, '-'); + try { + if (map.containsKey(mapName)) { + map.put(mapName, ctx.getInitParameter(name)); + } + } catch (Exception e) { + throw new ServletExceptionWithCause( + "Invalid servlet configuration option: " + name, e); + } + } + } + + public String toMapName(String name, char delim) { + StringBuffer ret = new StringBuffer(); + String[] elems = Text.explode(name, delim); + ret.append(elems[0]); + for (int i=1; i + * +-------------------+-------------------+ + * | Property Name | Init-Param Name | + * +-------------------+-------------------+ + * | repository.home | repository-home | + * | repository.config | repository-config | + * | repository.name | repository-name | + * +-------------------+-------------------+ + * + */ +public class BootstrapConfig extends AbstractConfig { + + private String repositoryHome; + + private String repositoryConfig; + + private String repositoryName; + + private JNDIConfig jndiConfig = new JNDIConfig(this); + + private RMIConfig rmiConfig = new RMIConfig(this); + + public void init(Properties props) throws ServletException { + super.init(props); + jndiConfig.init(props); + rmiConfig.init(props); + } + + public void init(ServletConfig ctx) throws ServletException { + super.init(ctx); + jndiConfig.init(ctx); + rmiConfig.init(ctx); + } + + public String getRepositoryHome() { + return repositoryHome; + } + + public void setRepositoryHome(String repositoryHome) { + this.repositoryHome = repositoryHome; + } + + public String getRepositoryConfig() { + return repositoryConfig; + } + + public void setRepositoryConfig(String repositoryConfig) { + this.repositoryConfig = repositoryConfig; + } + + public String getRepositoryName() { + return repositoryName; + } + + public void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + public JNDIConfig getJndiConfig() { + return jndiConfig; + } + + public RMIConfig getRmiConfig() { + return rmiConfig; + } + + public void validate() { + valid = repositoryName != null; + jndiConfig.validate(); + rmiConfig.validate(); + } + + + public void logInfos() { + super.logInfos(); + if (jndiConfig.isValid()) { + jndiConfig.logInfos(); + } + if (rmiConfig.isValid()) { + rmiConfig.logInfos(); + } + } +} \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/DerbyShutdown.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/DerbyShutdown.java new file mode 100644 index 00000000000..a1b0510cdfe --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/DerbyShutdown.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.j2ee; + +import java.lang.reflect.Method; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Enumeration; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * Servlet context listener that releases all remaining Derby resources + * when the web application is undeployed. The resources are released only + * if the Derby classes were loaded from within this webapp. + * + * @see JCR-1301 + */ +public class DerbyShutdown implements ServletContextListener { + + public void contextInitialized(ServletContextEvent event) { + } + + public void contextDestroyed(ServletContextEvent event) { + ClassLoader loader = DerbyShutdown.class.getClassLoader(); + + // Deregister all JDBC drivers loaded from this webapp + Enumeration drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + // Check if this driver comes from this webapp + if (driver.getClass().getClassLoader() == loader) { + try { + DriverManager.deregisterDriver(driver); + } catch (SQLException ignore) { + } + } + } + + // Explicitly tell Derby to release all remaining resources. + // Use reflection to avoid problems when the Derby is not used. + try { + Class monitorClass = + loader.loadClass("org.apache.derby.iapi.services.monitor.Monitor"); + if (monitorClass.getClassLoader() == loader) { + Method getMonitorMethod = + monitorClass.getMethod("getMonitor", new Class[0]); + Object monitor = + getMonitorMethod.invoke(null, new Object[0]); + if (monitor != null) { + Method shutdownMethod = + monitor.getClass().getMethod("shutdown", new Class[0]); + shutdownMethod.invoke(monitor, new Object[0]); + } + } + } catch (Exception ignore) { + } + } + +} \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/Installer.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/Installer.java new file mode 100644 index 00000000000..6a9104fd602 --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/Installer.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.j2ee; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +/** + * Provides very basic installation capabilities. + */ +public class Installer { + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(Installer.class); + + /** + * Return code for installation succeeded + */ + public static final int C_INSTALL_OK = 0; + + /** + * Return code for invalid input parameter + */ + public static final int C_INVALID_INPUT = 1; + + /** + * Return code for repository home already exists + */ + public static final int C_HOME_EXISTS = 2; + + /** + * Return code for repository home is missing + */ + public static final int C_HOME_MISSING = 3; + + /** + * Return code for repository config already exists + */ + public static final int C_CONFIG_EXISTS = 4; + + /** + * Return code for repository config is missing + */ + public static final int C_CONFIG_MISSING = 5; + + /** + * Return code for bootstrap config already exists + */ + public static final int C_BOOTSTRAP_EXISTS = 6; + + /** + * Return code for a general install error + */ + public static final int C_INSTALL_ERROR = 7; + + /** + * place to store the config file + */ + private final File bootstrapConfigFile; + + /** + * the servlet context + */ + private final ServletContext context; + + /** + * the place for the repository config template + * todo: to be configured + */ + private final String configTemplate = + "/org/apache/jackrabbit/core/repository.xml"; + + /** + * the place for the bootstrap properties template + * todo: to be configured + */ + private final String bootstrapTemplate = "/WEB-INF/templates/bootstrap.properties"; + + /** + * Creates a new installer + * @param bootstrapConfigFile the location for the config file + * @param context the servlet context for accessing resources + */ + public Installer(File bootstrapConfigFile, ServletContext context) { + this.bootstrapConfigFile = bootstrapConfigFile; + this.context = context; + } + + /** + * Handles the installation. + * + * @param req the servlet request with the input parameters + * @return the installation return code + * + * @throws ServletException if a servlet error occurs. + * @throws IOException if an I/O error occurs. + */ + public int installRepository(HttpServletRequest req) + throws ServletException, IOException { + String repHome = req.getParameter("repository_home"); + String repType = req.getParameter("repository_type"); + String repXml = req.getParameter("repository_xml"); + String mode = req.getParameter("mode"); + + if (repHome == null || mode == null) { + return C_INVALID_INPUT; + } + File home = new File(repHome); + + File config; + if ("oak".equals(repType)) { + config = null; + repXml = null; + } else if (repXml == null || repXml.length() == 0) { + config = new File(home, "repository.xml"); + repXml = config.getPath(); + } else { + config = new File(repXml); + } + + if ("new".equals(mode)) { + // Test internal folder repository existence and not home because home is already created + // by org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet + if (new File(home, "repository").exists()) { + log.error("Trying to install new repository home '{}' but it already contain a repository", repHome); + return C_HOME_EXISTS; + } + if (config != null && config.exists()) { + log.error("Trying to install new repository config '{}' but already exists", repXml); + return C_CONFIG_EXISTS; + } + log.info("Creating new repository home '{}'", repHome); + home.mkdirs(); + + if (config != null) { + // install repository xml for Jackrabbit Classic + try { + installRepositoryConfig(config); + } catch (IOException e) { + log.error("Error while installing new repository config '{}': {}", repXml, e.toString()); + return C_BOOTSTRAP_EXISTS; + } + } + } else { + if (!home.exists()) { + log.error("Trying to use existing repository home '{}' but does not exists", repHome); + return C_HOME_MISSING; + } + if (config != null && !config.exists()) { + log.error("Trying to use existing repository config '{}' but does not exists", repXml); + return C_CONFIG_MISSING; + } + } + // install bootstrap.properties + try { + installBootstrap(bootstrapConfigFile, repHome, repXml); + } catch (IOException e) { + log.error("Error while installing '{}': {}", bootstrapConfigFile.getPath(), e.toString()); + return C_INSTALL_ERROR; + } + return C_INSTALL_OK; + } + + /** + * Installs the repository config file from the template + * @param dest the destination location + * @throws IOException if an I/O error occurs. + */ + private void installRepositoryConfig(File dest) throws IOException { + log.info("Creating new repository config: {}", dest.getPath()); + InputStream in = context.getResourceAsStream(configTemplate); + if (in == null) { + in = getClass().getResourceAsStream(configTemplate); + } + OutputStream out = new FileOutputStream(dest); + byte[] buffer = new byte[8192]; + int read; + while ((read = in.read(buffer)) >= 0) { + out.write(buffer, 0, read); + } + in.close(); + out.close(); + } + + /** + * Installs the bootstrap config file from the template + * @param dest the destination location + * @param repHome the repository home location + * @param repXml the repository xml location + * @throws IOException if an I/O error occurs + */ + private void installBootstrap(File dest, String repHome, String repXml) + throws IOException { + log.info("Creating new bootstrap properties: {}", dest.getPath()); + InputStream in = context.getResourceAsStream(bootstrapTemplate); + Properties props = new Properties(); + props.load(in); + props.setProperty("repository.home", repHome); + if (repXml != null) { + props.setProperty("repository.config", repXml); + } + in.close(); + if (!dest.getParentFile().exists()) { + dest.getParentFile().mkdirs(); + } + OutputStream out = new FileOutputStream(dest); + props.store(out, "bootstrap properties for the repository startup servlet."); + out.close(); + } + +} diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JCRWebdavServerServlet.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JCRWebdavServerServlet.java new file mode 100644 index 00000000000..d68f6f34648 --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JCRWebdavServerServlet.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.j2ee; + +import javax.jcr.Repository; + +/** + * JCRWebdavServerServlet provides request/response handling for the + * JCRWebdavServer. + */ +public class JCRWebdavServerServlet extends + org.apache.jackrabbit.webdav.jcr.JCRWebdavServerServlet { + + /** + * Returns the repository available from the servlet context of this + * servlet. + */ + protected Repository getRepository() { + return RepositoryAccessServlet.getRepository(getServletContext()); + } + +} diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JNDIConfig.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JNDIConfig.java new file mode 100644 index 00000000000..46ddb330387 --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JNDIConfig.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.j2ee; + +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Properties; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +/** + * The JNDI config hold information about JNDI connection details. + * + * It supports the following properties and init parameters: + *
        + * +-------------------+--------------------+
        + * | Property Name     | Init-Param Name    |
        + * +-------------------+--------------------+
        + * | jndi.enable       | {provider spec.}   |
        + * | java.naming.*     | java.naming.*      |
        + * +-------------------+--------------------+
        + * 
        + */ +public class JNDIConfig extends AbstractConfig { + + private boolean jndiEnabled; + + private String jndiName; + + private final BootstrapConfig parentConfig; + + private Properties jndiEnv = new Properties(); + + + public JNDIConfig(BootstrapConfig parentConfig) { + this.parentConfig = parentConfig; + } + + + public String getJndiName() { + return jndiName; + } + + public void setJndiName(String jndiName) { + this.jndiName = jndiName; + } + + public boolean enabled() { + return jndiEnabled; + } + + public String getJndiEnabled() { + return String.valueOf(jndiEnabled); + } + + public void setJndiEnabled(String jndiEnabled) { + this.jndiEnabled = Boolean.valueOf(jndiEnabled).booleanValue(); + } + + public Properties getJndiEnv() { + return jndiEnv; + } + + public void init(Properties props) throws ServletException { + super.init(props); + // add all props whose name starts with 'java.namming.' to the env + Iterator iter = props.keySet().iterator(); + while (iter.hasNext()) { + String name = (String) iter.next(); + if (name.startsWith("java.naming.")) { + jndiEnv.put(name, props.getProperty(name)); + } + } + } + + public void init(ServletConfig ctx) throws ServletException { + super.init(ctx); + // add all params whose name starts with 'java.namming.' to the env + Enumeration names = ctx.getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + if (name.startsWith("java.naming.")) { + jndiEnv.put(name, ctx.getInitParameter(name)); + } + } + // enable jndi if url is specified + jndiEnabled = jndiEnv.containsKey("java.naming.provider.url"); + } + + + public void validate() { + if (jndiName == null) { + jndiName = parentConfig.getRepositoryName(); + } + valid = true; + } +} \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JcrApiNotFoundException.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JcrApiNotFoundException.java new file mode 100644 index 00000000000..e70db5b1a7c --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JcrApiNotFoundException.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.j2ee; + +/** + * Exception for signaling that the JCR API is not available. + */ +public class JcrApiNotFoundException extends ServletExceptionWithCause { + + /** + * Serial version UID + */ + private static final long serialVersionUID = -6439777923943394980L; + + /** + * Creates an exception to signal that the JCR API is not available. + * + * @param e the specific exception that indicates the lack of the JCR API + */ + public JcrApiNotFoundException(ClassNotFoundException e) { + super("JCR API (jcr-1.0.jar) not available in the classpath", e); + } + +} diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JcrRemotingServlet.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JcrRemotingServlet.java new file mode 100644 index 00000000000..a7d582d5f49 --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/JcrRemotingServlet.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.j2ee; + +import javax.jcr.Repository; + +/** + * JcrRemotingServlet... + */ +public class JcrRemotingServlet extends org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet { + + /** + * Returns the repository available from the servlet context of this + * servlet. + */ + protected Repository getRepository() { + return RepositoryAccessServlet.getRepository(getServletContext()); + } +} \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RMIConfig.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RMIConfig.java new file mode 100644 index 00000000000..6b77e6c6ef9 --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RMIConfig.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.j2ee; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.URISyntaxException; +import java.rmi.registry.Registry; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; + +/** + * The RMI config hold information about RMI connection details. + * + * It supports the following properties and init parameters: + *
        + * +-------------------+--------------------+
        + * | Property Name     | Init-Param Name    |
        + * +-------------------+--------------------+
        + * | rmi.enable        | {rmi-port sepc.}   |
        + * | rmi.host          | rmi-host           |
        + * | rmi.port          | rmi-port           |
        + * | rmi.name          | {repository name}  |
        + * | rmi.url           | rmi-url            |
        + * +-------------------+--------------------+
        + * 
        + */ +public class RMIConfig extends AbstractConfig { + + /** + * default logger + */ + private static final Logger log = LoggerFactory.getLogger(RMIConfig.class); + + private boolean rmiEnabled; + + private int rmiPort = -1; + + private String rmiHost; + + private String rmiName; + + private String rmiUri; + + private final BootstrapConfig parentConfig; + + + public RMIConfig(BootstrapConfig parentConfig) { + this.parentConfig = parentConfig; + } + + public void init(ServletConfig ctx) throws ServletException { + super.init(ctx); + // enable RMI if either port or url was defined + rmiEnabled = rmiPort >=0 || rmiUri != null; + } + + public String getRmiName() { + return rmiName; + } + + public void setRmiName(String rmiName) { + this.rmiName = rmiName; + } + + public boolean enabled() { + return rmiEnabled; + } + + public String getRmiEnabled() { + return String.valueOf(rmiEnabled); + } + + public void setRmiEnabled(String rmiEnabled) { + this.rmiEnabled = Boolean.valueOf(rmiEnabled).booleanValue(); + } + + public int rmiPort() { + return rmiPort; + } + + public String getRmiPort() { + return String.valueOf(rmiPort); + } + + public void setRmiPort(String rmiPort) { + this.rmiPort = Integer.decode(rmiPort).intValue(); + } + + public String getRmiHost() { + return rmiHost; + } + + public void setRmiHost(String rmiHost) { + this.rmiHost = rmiHost; + } + + public String getRmiUri() { + return rmiUri; + } + + public void setRmiUri(String rmiUri) { + this.rmiUri = rmiUri; + } + + public void validate() { + if (!rmiEnabled) { + return; + } + + if (rmiUri != null && rmiUri.length() > 0) { + // URI takes precedences, so check whether the configuration has to + // be set from the URI + try { + URI uri = new URI(rmiUri); + + // extract values from the URI, check later + rmiHost = uri.getHost(); + rmiPort = uri.getPort(); + rmiName = uri.getPath(); + + } catch (URISyntaxException e) { + log.warn("Cannot parse RMI URI '" + rmiUri + "'.", e); + rmiUri = null; // clear RMI URI use another one + rmiHost = null; // use default host, ignore rmi-host param + } + + // cut of leading slash from name if defined at all + if (rmiName != null && rmiName.startsWith("/")) { + rmiName = rmiName.substring(1); + } + } + + // check RMI port + if (rmiPort == -1 || rmiPort == 0) { + // accept -1 or 0 as a hint to use the default + rmiPort = Registry.REGISTRY_PORT; + } else if (rmiPort < -1 || rmiPort > 0xFFFF) { + // emit a warning if out of range, use defualt in this case + log.warn("Invalid port in rmi-port param " + rmiPort + ". using default port."); + rmiPort = Registry.REGISTRY_PORT; + } + + // check host - use an empty name if null (i.e. not configured) + if (rmiHost == null) { + rmiHost = ""; + } + + // check name - use repositoryName if empty or null + if (rmiName == null || rmiName.length() ==0) { + rmiName = parentConfig.getRepositoryName(); + } + + // reconstruct the rmiURI now because values might have been changed + rmiUri = "//" + rmiHost + ":" + rmiPort + "/" + rmiName; + valid = true; + } +} \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RepositoryAccessServlet.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RepositoryAccessServlet.java new file mode 100644 index 00000000000..f09575fdab2 --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RepositoryAccessServlet.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.j2ee; + +import org.apache.jackrabbit.rmi.client.ClientRepositoryFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.util.Properties; + +import javax.jcr.Repository; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; + +/** + * This Class implements a servlet that is used as unified mechanism to retrieve + * a jcr repository either through JNDI or RMI. + */ +public class RepositoryAccessServlet extends HttpServlet { + + /** + * default logger + */ + private static final Logger log = LoggerFactory.getLogger(RepositoryAccessServlet.class); + + /** + * initial param name for the bootstrap config location + */ + public final static String INIT_PARAM_BOOTSTRAP_CONFIG = "bootstrap-config"; + + /** + * Context parameter name for 'this' instance. + */ + private final static String CTX_PARAM_THIS = "repository.access.servlet"; + + /** + * Ugly hack to override the bootstrap file location in the test cases + */ + static String bootstrapOverride = null; + + /** + * the bootstrap config + */ + private BootstrapConfig config; + + /** + * the initialized initial context + */ + private InitialContext jndiContext; + + /** + * if this is set we try to get a Repository from the ServletContext + */ + private String repositoryContextAttributeName; + + /** + * the repository + */ + private Repository repository; + + /** + * Initializes the servlet.
        + * Please note that only one repository startup servlet may exist per + * webapp. it registers itself as context attribute and acts as singleton. + * + * @throws ServletException if a same servlet is already registered or of + * another initialization error occurs. + */ + public void init() throws ServletException { + // check if servlet is defined twice + if (getServletContext().getAttribute(CTX_PARAM_THIS) != null) { + throw new ServletException("Only one repository access servlet allowed per web-app."); + } + getServletContext().setAttribute(CTX_PARAM_THIS, this); + + repositoryContextAttributeName = getServletConfig().getInitParameter("repository.context.attribute.name"); + + log.info("RepositoryAccessServlet initialized."); + } + + /** + * Returns the instance of this servlet + * @param ctx the servlet context + * @return this servlet + */ + public static RepositoryAccessServlet getInstance(ServletContext ctx) { + final RepositoryAccessServlet instance = (RepositoryAccessServlet) ctx.getAttribute(CTX_PARAM_THIS); + if(instance==null) { + throw new IllegalStateException( + "No RepositoryAccessServlet instance in ServletContext, RepositoryAccessServlet servlet not initialized?" + ); + } + return instance; + } + + /** + * Returns the bootstrap config + * @return the bootstrap config + * @throws ServletException if the config is not valid + */ + private BootstrapConfig getConfig() throws ServletException { + if (config == null) { + // check if there is a loadable bootstrap config + Properties bootstrapProps = new Properties(); + String bstrp = bootstrapOverride; + if (bstrp == null) { + bstrp = getServletConfig().getInitParameter(INIT_PARAM_BOOTSTRAP_CONFIG); + } + if (bstrp != null) { + // check if it's a web-resource + InputStream in = getServletContext().getResourceAsStream(bstrp); + if (in == null) { + // check if it's a file + File file = new File(bstrp); + if (file.canRead()) { + try { + in = new FileInputStream(file); + } catch (FileNotFoundException e) { + throw new ServletExceptionWithCause( + "Bootstrap configuration not found: " + bstrp, e); + } + } + } + if (in != null) { + try { + bootstrapProps.load(in); + } catch (IOException e) { + throw new ServletExceptionWithCause( + "Bootstrap configuration failure: " + bstrp, e); + } finally { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + // read bootstrap config + BootstrapConfig tmpConfig = new BootstrapConfig(); + tmpConfig.init(getServletConfig()); + tmpConfig.init(bootstrapProps); + tmpConfig.validate(); + if (!tmpConfig.isValid()) { + throw new ServletException( + "Repository access configuration is not valid."); + } + tmpConfig.logInfos(); + config = tmpConfig; + } + return config; + } + + /** + * Returns the initial jndi context or null if the jndi access + * is not configured or erroous. + * @return the initial context or null + */ + private InitialContext getInitialContext() { + if (jndiContext == null && config.getJndiConfig().enabled()) { + // retrieve JNDI Context environment + try { + jndiContext = new InitialContext(config.getJndiConfig().getJndiEnv()); + } catch (NamingException e) { + log.error("Create initial context: " + e.toString()); + } + } + return jndiContext; + } + + /** + * Checks if the repository is available via JNDI and returns it. + * @return the repository or null + * @throws ServletException if this servlet is not properly configured. + */ + private Repository getRepositoryByJNDI() throws ServletException { + BootstrapConfig config = getConfig(); + if (!config.getJndiConfig().isValid() || !config.getJndiConfig().enabled()) { + return null; + } + // acquire via JNDI + String repositoryName = config.getRepositoryName(); + InitialContext ctx = getInitialContext(); + if (ctx == null) { + return null; + } + try { + Repository r = (Repository) ctx.lookup(repositoryName); + log.info("Acquired repository via JNDI."); + return r; + } catch (NamingException e) { + log.error("Error while retrieving repository using JNDI (name={})", repositoryName, e); + return null; + } + } + + /** + * Checks if the repository is available via RMI and returns it. + * @return the repository or null + * @throws ServletException if this servlet is not properly configured. + */ + private Repository getRepositoryByRMI() throws ServletException { + BootstrapConfig config = getConfig(); + if (!config.getRmiConfig().isValid() || !config.getRmiConfig().enabled()) { + return null; + } + + // acquire via RMI + String rmiURI = config.getRmiConfig().getRmiUri(); + if (rmiURI == null) { + return null; + } + log.info(" trying to retrieve repository using rmi. uri={}", rmiURI); + ClientFactoryDelegater cfd; + try { + Class clazz = Class.forName(getServerFactoryDelegaterClass()); + cfd = (ClientFactoryDelegater) clazz.newInstance(); + } catch (Throwable e) { + log.error("Unable to locate RMI ClientRepositoryFactory. Is jcr-rmi.jar missing?", e); + return null; + } + + try { + Repository r = cfd.getRepository(rmiURI); + log.info("Acquired repository via RMI."); + return r; + } catch (Exception e) { + log.error("Error while retrieving repository using RMI: {}", rmiURI, e); + return null; + } + } + + /** + * If our config said so, try to retrieve a Repository from the ServletContext + */ + protected Repository getRepositoryByContextAttribute() { + Repository result = null; + if(repositoryContextAttributeName!=null) { + result = (Repository)getServletContext().getAttribute(repositoryContextAttributeName); + + if(log.isDebugEnabled()) { + if(result!=null) { + log.debug("Got Repository from ServletContext attribute '{}'", repositoryContextAttributeName); + } else { + log.debug("ServletContext attribute '{}' does not provide a Repository", repositoryContextAttributeName); + } + } + } + return result; + } + + /** + * Return the fully qualified name of the class providing the client + * repository. The class whose name is returned must implement the + * {@link ClientFactoryDelegater} interface. + * + * @return the qfn of the factory class. + */ + protected String getServerFactoryDelegaterClass() { + return getClass().getName() + "$RMIClientFactoryDelegater"; + } + + /** + * Returns the JCR repository + * + * @return a JCR repository + * @throws IllegalStateException if the repository is not available in the context. + */ + public Repository getRepository() { + try { + if (repository == null) { + // try to get via context attribute + repository = getRepositoryByContextAttribute(); + } + if (repository == null) { + // try to retrieve via jndi + repository = getRepositoryByJNDI(); + } + if (repository == null) { + // try to get via rmi + repository = getRepositoryByRMI(); + } + if (repository == null) { + throw new ServletException("N/A"); + } + return repository; + } catch (ServletException e) { + throw new IllegalStateException( + "The repository is not available. Please check" + + " RepositoryAccessServlet configuration in web.xml.", e); + } + } + + /** + * Returns the JCR repository + * + * @param ctx the servlet context + * @return a JCR repository + * @throws IllegalStateException if the repository is not available in the context. + */ + public static Repository getRepository(ServletContext ctx) { + return getInstance(ctx).getRepository(); + } + + /** + * Returns the config that was used to bootstrap this servlet. + * @return the bootstrap config or null. + */ + public BootstrapConfig getBootstrapConfig() { + return config; + } + + /** + * optional class for RMI, will only be used, if RMI client is present + */ + protected static abstract class ClientFactoryDelegater { + + public abstract Repository getRepository(String uri) + throws RemoteException, MalformedURLException, NotBoundException; + } + + /** + * optional class for RMI, will only be used, if RMI server is present + */ + protected static class RMIClientFactoryDelegater extends ClientFactoryDelegater { + + // only used to enforce linking upon Class.forName() + static String FactoryClassName = ClientRepositoryFactory.class.getName(); + + public Repository getRepository(String uri) + throws MalformedURLException, NotBoundException, RemoteException { + System.setProperty("java.rmi.server.useCodebaseOnly", "true"); + return new ClientRepositoryFactory().getRepository(uri); + } + } +} + diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RepositoryStartupServlet.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RepositoryStartupServlet.java new file mode 100644 index 00000000000..fef961e058b --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/RepositoryStartupServlet.java @@ -0,0 +1,767 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.j2ee; + +import org.apache.jackrabbit.api.JackrabbitRepository; +import org.apache.jackrabbit.commons.repository.RepositoryFactory; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.rmi.server.RemoteAdapterFactory; +import org.apache.jackrabbit.rmi.server.ServerAdapterFactory; +import org.apache.jackrabbit.servlet.AbstractRepositoryServlet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.InputSource; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.UnknownHostException; +import java.rmi.AlreadyBoundException; +import java.rmi.Naming; +import java.rmi.NoSuchObjectException; +import java.rmi.Remote; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMIServerSocketFactory; +import java.rmi.server.UnicastRemoteObject; +import java.util.Properties; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * The RepositoryStartupServlet starts a jackrabbit repository and registers it + * to the JNDI environment and optional to the RMI registry. + *

        + * Registration with RMI + *

        + * Upon successfull creation of the repository in the {@link #init()} method, + * the repository is registered with an RMI registry if the web application is + * so configured. To register with RMI, the following web application + * init-params are considered: rmi-port designating + * the port on which the RMI registry is listening, rmi-host + * designating the interface on the local host on which the RMI registry is + * active, repository-name designating the name to which the + * repository is to be bound in the registry, and rmi-uri + * designating an RMI URI complete with host, optional port and name to which + * the object is bound. + *

        + * If the rmi-uri parameter is configured with a non-empty value, + * the rmi-port and rmi-host parameters are ignored. + * The repository-name parameter is only considered if a non-empty + * rmi-uri parameter is configured if the latter does not contain + * a name to which to bind the repository. + *

        + * This is the algorithm used to find out the host, port and name for RMI + * registration: + *

          + *
        1. If neither a rmi-uri nor a rmi-host nor a + * rmi-port parameter is configured, the repository is not + * registered with any RMI registry. + *
        2. If a non-empty rmi-uri parameter is configured extract the + * host name (or IP address), port number and name to bind to from the + * URI. If the URI is not valid, host defaults to 0.0.0.0 + * meaning all interfaces on the local host, port defaults to the RMI + * default port (1099) and the name defaults to the value + * of the repository-name parameter. + *
        3. If a non-empty rmi-uri is not configured, the host is taken + * from the rmi-host parameter, the port from the + * rmi-port parameter and the name to bind the repository to + * from the repository-name parameter. If the + * rmi-host parameter is empty or not configured, the host + * defaults to 0.0.0.0 meaning all interfaces on the local + * host. If the rmi-port parameter is empty, not configured, + * zero or a negative value, the default port for the RMI registry + * (1099) is used. + *
        + *

        + * After finding the host and port of the registry, the RMI registry itself + * is acquired. It is assumed, that host and port primarily designate an RMI + * registry, which should be active on the local host but has not been started + * yet. In this case, the LocateRegistry.createRegistry method is + * called to create a registry on the local host listening on the host and port + * configured. If creation fails, the LocateRegistry.getRegistry + * method is called to get a remote instance of the registry. Note, that + * getRegistry does not create an actual registry on the given + * host/port nor does it check, whether an RMI registry is active. + *

        + * When the registry has been retrieved, either by creation or by just creating + * a remote instance, the repository is bound to the configured name in the + * registry. + *

        + * Possible causes for registration failures include: + *

          + *
        • The web application is not configured to register with an RMI registry at + * all. + *
        • The registry is expected to be running on a remote host but does not. + *
        • The registry is expected to be running on the local host but cannot be + * accessed. Reasons include another application which does not act as an + * RMI registry is running on the configured port and thus blocks creation + * of a new RMI registry. + *
        • An object may already be bound to the same name as is configured to be + * used for the repository. + *
        + *

        + * Note: if a bootstrap-config init parameter is specified the + * servlet tries to read the respective resource, either as context resource or + * as file. The properties specified in this file override the init params + * specified in the web.xml. + *

        + *

        + * Setup Wizard Functionality
        + * When using the first time, the configuration can miss the relevant + * repository parameters in the web.xml. if so, it must contain a + * bootstrap-config parameter that refers to a property file. + * This file must exist for proper working. If not, the repository is not + * started.
        + * If the servlet is not configured correctly and accessed via http, it will + * provide a simple wizard for the first time configuration. It prompts for + * a new (or existing) repository home and will copy the templates of the + * repository.xml and bootstrap.properties to the respective location. + */ +public class RepositoryStartupServlet extends AbstractRepositoryServlet { + + /** + * the default logger + */ + private static final Logger log = LoggerFactory.getLogger(RepositoryStartupServlet.class); + + /** + * the context attribute name foe 'this' instance. + */ + private final static String CTX_PARAM_THIS = "repository.startup.servet"; + + /** + * initial param name for the bootstrap config location + */ + public final static String INIT_PARAM_BOOTSTRAP_CONFIG = "bootstrap-config"; + + /** + * Ugly hack to override the bootstrap file location in the test cases + */ + static String bootstrapOverride = null; + + /** + * the registered repository + */ + private Repository repository; + + /** + * the jndi context; created based on configuration + */ + private InitialContext jndiContext; + + private Registry rmiRegistry = null; + + /** + * Keeps a strong reference to the server side RMI repository instance to + * prevent the RMI distributed Garbage Collector from collecting the + * instance making the repository unaccessible though it should still be. + * This field is only set to a non-null value, if registration + * of the repository to an RMI registry succeeded in the + * {@link #registerRMI()} method. + * + * @see #registerRMI() + * @see #unregisterRMI() + */ + private Remote rmiRepository; + + /** + * the file to the bootstrap config + */ + private File bootstrapConfigFile; + + /** + * The bootstrap configuration + */ + private BootstrapConfig config; + + /** + * Initializes the servlet.
        + * Please note that only one repository startup servlet may exist per + * webapp. it registers itself as context attribute and acts as singleton. + * + * @throws ServletException if a same servlet is already registered or of + * another initialization error occurs. + */ + public void init() throws ServletException { + super.init(); + // check if servlet is defined twice + if (getServletContext().getAttribute(CTX_PARAM_THIS) != null) { + throw new ServletException("Only one repository startup servlet allowed per web-app."); + } + getServletContext().setAttribute(CTX_PARAM_THIS, this); + startup(); + } + + /** + * Returns an instance of this servlet. Please note, that only 1 + * repository startup servlet can exist per webapp. + * + * @param context the servlet context + * @return this servlet + */ + public static RepositoryStartupServlet getInstance(ServletContext context) { + return (RepositoryStartupServlet) context.getAttribute(CTX_PARAM_THIS); + } + + /** + * Configures and starts the repository. It registers it then to the + * RMI registry and bind is to the JNDI context if so configured. + * @throws ServletException if an error occurs. + */ + public void startup() throws ServletException { + if (repository != null) { + log.error("Startup: Repository already running."); + throw new ServletException("Repository already running."); + } + log.info("RepositoryStartupServlet initializing..."); + try { + if (configure()) { + initRepository(); + registerRMI(); + registerJNDI(); + } + log.info("RepositoryStartupServlet initialized."); + } catch (ServletException e) { + // shutdown repository + shutdownRepository(); + log.error("RepositoryStartupServlet initializing failed: " + e, e); + } + } + + /** + * Does a shutdown of the repository and deregisters it from the RMI + * registry and unbinds if from the JNDI context if so configured. + */ + public void shutdown() { + if (repository == null) { + log.info("Shutdown: Repository already stopped."); + } else { + log.info("RepositoryStartupServlet shutting down..."); + shutdownRepository(); + unregisterRMI(); + unregisterJNDI(); + log.info("RepositoryStartupServlet shut down."); + } + } + + /** + * Restarts the repository. + * @throws ServletException if an error occurs. + * @see #shutdown() + * @see #startup() + */ + public void restart() throws ServletException { + if (repository != null) { + shutdown(); + } + startup(); + } + + /** + * destroy the servlet + */ + public void destroy() { + super.destroy(); + shutdown(); + } + + /** + * Returns the started repository or null if not started + * yet. + * @return the JCR repository + */ + public Repository getRepository() { + return repository; + } + + /** + * Returns a repository factory that returns the repository if available + * or throws an exception if not. + * + * @return repository factory + */ + public RepositoryFactory getRepositoryFactory() { + return new RepositoryFactory() { + public Repository getRepository() throws RepositoryException { + Repository r = repository; + if (r != null) { + return repository; + } else { + throw new RepositoryException("Repository not available"); + } + } + }; + } + + /** + * Reads the configuration and initializes the {@link #config} field if + * successful. + * @throws ServletException if an error occurs. + */ + private boolean configure() throws ServletException { + // check if there is a loadable bootstrap config + Properties bootstrapProps = new Properties(); + String bstrp = bootstrapOverride; + if (bstrp == null) { + bstrp = getServletConfig().getInitParameter(INIT_PARAM_BOOTSTRAP_CONFIG); + } + if (bstrp != null) { + // check if it's a web-resource + InputStream in = getServletContext().getResourceAsStream(bstrp); + if (in == null) { + // check if it's a file + bootstrapConfigFile = new File(bstrp); + if (bootstrapConfigFile.canRead()) { + try { + in = new FileInputStream(bootstrapConfigFile); + } catch (FileNotFoundException e) { + throw new ServletExceptionWithCause( + "Bootstrap configuration not found: " + bstrp, e); + } + } + } + if (in != null) { + try { + bootstrapProps.load(in); + } catch (IOException e) { + throw new ServletException( + "Bootstrap configuration failure: " + bstrp, e); + } finally { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + // read bootstrap config + config = new BootstrapConfig(); + config.init(getServletConfig()); + config.init(bootstrapProps); + config.validate(); + if (!config.isValid() + || config.getRepositoryHome() == null) { + if (bstrp == null) { + log.error("Repository startup configuration is not valid."); + } else { + log.error("Repository startup configuration is not valid but a bootstrap config is specified."); + log.error("Either create the {} file or", bstrp); + log.error("use the '/config/index.jsp' for easy configuration."); + } + return false; + } else { + config.logInfos(); + return true; + } + } + + /** + * Creates a new Repository based on the configuration and initializes the + * {@link #repository} field if successful. + * + * @throws ServletException if an error occurs + */ + private void initRepository() throws ServletException { + // get repository config + File repHome; + try { + repHome = new File(config.getRepositoryHome()).getCanonicalFile(); + } catch (IOException e) { + throw new ServletExceptionWithCause( + "Repository configuration failure: " + config.getRepositoryHome(), e); + } + String repConfig = config.getRepositoryConfig(); + if (repConfig != null) { + InputStream in = getServletContext().getResourceAsStream(repConfig); + if (in == null) { + try { + in = new FileInputStream(new File(repConfig)); + } catch (FileNotFoundException e) { + // fallback to old config + try { + in = new FileInputStream(new File(repHome, repConfig)); + } catch (FileNotFoundException e1) { + throw new ServletExceptionWithCause( + "Repository configuration not found: " + repConfig, e); + } + } + } + + try { + repository = createRepository(new InputSource(in), repHome); + } catch (RepositoryException e) { + throw new ServletExceptionWithCause("Error while creating repository", e); + } + } else { + throw new ServletException("Missing configuration"); + } + } + + /** + * Shuts down the repository. If the repository is an instanceof + * {@link JackrabbitRepository} it's {@link JackrabbitRepository#shutdown()} + * method is called. in any case, the {@link #repository} field is + * nulled. + */ + private void shutdownRepository() { + if (repository instanceof JackrabbitRepository) { + ((JackrabbitRepository) repository).shutdown(); + } + repository = null; + } + + /** + * Creates the repository instance for the given config and homedir. + * Subclasses may override this method of providing own implementations of + * a {@link Repository}. + * + * @param is input source of the repository config + * @param homedir the repository home directory + * @return a new jcr repository. + * @throws RepositoryException if an error during creation occurs. + */ + protected Repository createRepository(InputSource is, File homedir) + throws RepositoryException { + RepositoryConfig config = RepositoryConfig.create(is, homedir.getAbsolutePath()); + return RepositoryImpl.create(config); + } + + /** + * Binds the repository to the JNDI context + * @throws ServletException if an error occurs. + */ + private void registerJNDI() throws ServletException { + JNDIConfig jc = config.getJndiConfig(); + if (jc.isValid() && jc.enabled()) { + try { + jndiContext = new InitialContext(jc.getJndiEnv()); + jndiContext.bind(jc.getJndiName(), repository); + log.info("Repository bound to JNDI with name: " + jc.getJndiName()); + } catch (NamingException e) { + throw new ServletExceptionWithCause( + "Unable to bind repository using JNDI: " + jc.getJndiName(), e); + } + } + } + + /** + * Unbinds the repository from the JNDI context. + */ + private void unregisterJNDI() { + if (jndiContext != null) { + try { + jndiContext.unbind(config.getJndiConfig().getJndiName()); + } catch (NamingException e) { + log("Error while unbinding repository from JNDI: " + e); + } + } + } + + /** + * Registers the repository to an RMI registry configured in the web + * application. See Registration with RMI in the + * class documentation for a description of the algorithms used to register + * the repository with an RMI registry. + * @throws ServletException if an error occurs. + */ + private void registerRMI() { + RMIConfig rc = config.getRmiConfig(); + if (!rc.isValid() || !rc.enabled()) { + return; + } + + // try to create remote repository + Remote remote; + try { + Class clazz = Class.forName(getRemoteFactoryDelegaterClass()); + RemoteFactoryDelegater rmf = (RemoteFactoryDelegater) clazz.newInstance(); + remote = rmf.createRemoteRepository(repository); + } catch (RemoteException e) { + log.warn("Unable to create RMI repository.", e); + return; + } catch (Throwable t) { + log.warn("Unable to create RMI repository." + + " The jcr-rmi jar might be missing.", t); + return; + } + + try { + System.setProperty("java.rmi.server.useCodebaseOnly", "true"); + Registry reg = null; + + // first try to create the registry, which will fail if another + // application is already running on the configured host/port + // or if the rmiHost is not local + try { + // find the server socket factory: use the default if the + // rmiHost is not configured + RMIServerSocketFactory sf; + if (rc.getRmiHost().length() > 0) { + log.debug("Creating RMIServerSocketFactory for host " + rc.getRmiHost()); + InetAddress hostAddress = InetAddress.getByName(rc.getRmiHost()); + sf = getRMIServerSocketFactory(hostAddress); + } else { + // have the RMI implementation decide which factory is the + // default actually + log.debug("Using default RMIServerSocketFactory"); + sf = null; + } + + // create a registry using the default client socket factory + // and the server socket factory retrieved above. This also + // binds to the server socket to the rmiHost:rmiPort. + reg = LocateRegistry.createRegistry(rc.rmiPort(), null, sf); + rmiRegistry = reg; + } catch (UnknownHostException uhe) { + // thrown if the rmiHost cannot be resolved into an IP-Address + // by getRMIServerSocketFactory + log.info("Cannot create Registry", uhe); + } catch (RemoteException e) { + // thrown by createRegistry if binding to the rmiHost:rmiPort + // fails, for example due to rmiHost being remote or another + // application already being bound to the port + log.info("Cannot create Registry", e); + } + + // if creation of the registry failed, we try to access an + // potentially active registry. We do not check yet, whether the + // registry is actually accessible. + if (reg == null) { + log.debug("Trying to access existing registry at " + rc.getRmiHost() + + ":" + rc.getRmiPort()); + try { + reg = LocateRegistry.getRegistry(rc.getRmiHost(), rc.rmiPort()); + } catch (RemoteException re) { + log.warn("Cannot create the reference to the registry at " + + rc.getRmiHost() + ":" + rc.getRmiPort(), re); + } + } + + // if we finally have a registry, register the repository with the + // rmiName + if (reg != null) { + log.debug("Registering repository as " + rc.getRmiName() + + " to registry " + reg); + reg.bind(rc.getRmiName(), remote); + + // when successfull, keep references + this.rmiRepository = remote; + log.info("Repository bound via RMI with name: " + rc.getRmiUri()); + } else { + log.info("RMI registry missing, cannot bind repository via RMI"); + } + } catch (RemoteException e) { + log.warn("Unable to bind repository via RMI: " + rc.getRmiUri(), e); + } catch (AlreadyBoundException e) { + log.warn("Unable to bind repository via RMI: " + rc.getRmiUri(), e); + } + } + + /** + * Unregisters the repository from the RMI registry, if it has previously + * been registered. + */ + private void unregisterRMI() { + if (rmiRepository != null) { + // Forcibly unexport the repository; + try { + UnicastRemoteObject.unexportObject(rmiRepository, true); + } catch (NoSuchObjectException e) { + log.warn("Odd, the RMI repository was not exported", e); + } + // drop strong reference to remote repository + rmiRepository = null; + + // unregister repository + try { + Naming.unbind(config.getRmiConfig().getRmiUri()); + } catch (Exception e) { + log("Error while unbinding repository from JNDI: " + e); + } + } + + if (rmiRegistry != null) { + try { + UnicastRemoteObject.unexportObject(rmiRegistry, true); + } catch (NoSuchObjectException e) { + log.warn("Odd, the RMI registry was not exported", e); + } + rmiRegistry = null; + } + } + + /** + * Returns the config that was used to bootstrap this servlet. + * @return the bootstrap config or null. + */ + public BootstrapConfig getBootstrapConfig() { + return config; + } + + /** + * Return the fully qualified name of the class providing the remote + * repository. The class whose name is returned must implement the + * {@link RemoteFactoryDelegater} interface. + *

        + * Subclasses may override this method for providing a name of a own + * implementation. + * + * @return getClass().getName() + "$RMIRemoteFactoryDelegater" + */ + protected String getRemoteFactoryDelegaterClass() { + return getClass().getName() + "$RMIRemoteFactoryDelegater"; + } + + /** + * Returns an RMIServerSocketFactory used to create the server + * socket for a locally created RMI registry. + *

        + * This implementation returns a new instance of a simple + * RMIServerSocketFactory which just creates instances of + * the java.net.ServerSocket class bound to the given + * hostAddress. Implementations may overwrite this method to + * provide factory instances, which provide more elaborate server socket + * creation, such as SSL server sockets. + * + * @param hostAddress The InetAddress instance representing the + * the interface on the local host to which the server sockets are + * bound. + * @return A new instance of a simple RMIServerSocketFactory + * creating java.net.ServerSocket instances bound to + * the rmiHost. + */ + protected RMIServerSocketFactory getRMIServerSocketFactory( + final InetAddress hostAddress) { + return new RMIServerSocketFactory() { + public ServerSocket createServerSocket(int port) throws IOException { + return new ServerSocket(port, -1, hostAddress); + } + }; + } + + /** + * optional class for RMI, will only be used, if RMI server is present + */ + protected static abstract class RemoteFactoryDelegater { + + public abstract Remote createRemoteRepository(Repository repository) + throws RemoteException; + } + + /** + * optional class for RMI, will only be used, if RMI server is present + */ + protected static class RMIRemoteFactoryDelegater extends RemoteFactoryDelegater { + + private static final RemoteAdapterFactory FACTORY = + new ServerAdapterFactory(); + + public Remote createRemoteRepository(Repository repository) + throws RemoteException { + return FACTORY.getRemoteRepository(repository); + } + + } + + //-------------------------------------------------< Installer Routines >--- + + /** + * {@inheritDoc} + */ + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (repository == null) { + redirect(req, resp, "/bootstrap/missing.jsp"); + } else { + redirect(req, resp, "/bootstrap/running.jsp"); + } + } + + /** + * {@inheritDoc} + */ + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + if (repository != null) { + redirect(req, resp, "/bootstrap/reconfigure.jsp"); + } else { + int rc = new Installer(bootstrapConfigFile, + getServletContext()).installRepository(req); + switch (rc) { + case Installer.C_INSTALL_OK: + // restart rep + restart(); + if (repository == null) { + redirect(req, resp, "/bootstrap/error.jsp"); + } else { + redirect(req, resp, "/bootstrap/success.jsp"); + } + break; + case Installer.C_INVALID_INPUT: + redirect(req, resp, "/bootstrap/missing.jsp"); + break; + case Installer.C_CONFIG_EXISTS: + case Installer.C_BOOTSTRAP_EXISTS: + case Installer.C_HOME_EXISTS: + redirect(req, resp, "/bootstrap/exists.jsp"); + break; + case Installer. C_HOME_MISSING: + case Installer.C_CONFIG_MISSING: + redirect(req, resp, "/bootstrap/notexists.jsp"); + break; + case Installer.C_INSTALL_ERROR: + redirect(req, resp, "/bootstrap/error.jsp"); + break; + } + } + } + + /** + * Helper function to send a redirect response respecting the context path. + * + * @param req the request + * @param resp the response + * @param loc the location for the redirect + * @throws IOException if an I/O error occurs. + */ + private void redirect(HttpServletRequest req, + HttpServletResponse resp, String loc) + throws IOException { + String cp = req.getContextPath(); + if (cp == null || cp.equals("/")) { + cp = ""; + } + resp.sendRedirect(cp + loc); + } +} + diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/ServletExceptionWithCause.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/ServletExceptionWithCause.java new file mode 100644 index 00000000000..faa797d9a8d --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/ServletExceptionWithCause.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.j2ee; + +import javax.servlet.ServletException; + +/** + * Utility class that links {@link ServletException} with support for + * the exception chaining mechanism in {@link Throwable}. + * + * @see JCR-1598 + */ +public class ServletExceptionWithCause extends ServletException { + + /** + * Serial version UID + */ + private static final long serialVersionUID = -7201954529718775444L; + + /** + * Creates a servlet exception with the given message and cause. + * + * @param message exception message + * @param cause cause of the exception + */ + public ServletExceptionWithCause(String message, Throwable cause) { + super(message, cause); + if (getCause() == null) { + initCause(cause); + } + } + +} diff --git a/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/SimpleWebdavServlet.java b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/SimpleWebdavServlet.java new file mode 100644 index 00000000000..f3e31e6622e --- /dev/null +++ b/jackrabbit-webapp/src/main/java/org/apache/jackrabbit/j2ee/SimpleWebdavServlet.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.j2ee; + +import javax.jcr.Repository; + +/** + * WebdavServlet provides webdav support (level 1 and 2 complient) for repository + * resources. + */ +public class SimpleWebdavServlet extends org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet { + + /** + * the jcr repository + */ + private Repository repository; + + /** + * Returns the Repository. If no repository has been set or + * created the repository initialized by RepositoryAccessServlet + * is returned. + * + * @return repository + * @see RepositoryAccessServlet#getRepository(ServletContext) + */ + public Repository getRepository() { + if (repository == null) { + repository = RepositoryAccessServlet.getRepository(getServletContext()); + } + return repository; + } + + /** + * Sets the Repository. + * + * @param repository + */ + public void setRepository(Repository repository) { + this.repository = repository; + } +} diff --git a/jackrabbit-webapp/src/main/resources/logback.xml b/jackrabbit-webapp/src/main/resources/logback.xml new file mode 100644 index 00000000000..f3742d41796 --- /dev/null +++ b/jackrabbit-webapp/src/main/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-webapp/src/main/webapp/META-INF/LICENSE b/jackrabbit-webapp/src/main/webapp/META-INF/LICENSE new file mode 100644 index 00000000000..82ea7256201 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/META-INF/LICENSE @@ -0,0 +1,1625 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +APACHE JACKRABBIT SUBCOMPONENTS + +Apache Jackrabbit includes parts with separate copyright notices and license +terms. Your use of these subcomponents is subject to the terms and conditions +of the following licenses: + +XPath parser (jackrabbit-spi-commons) + + XPath 2.0/XQuery 1.0 Parser: + http://www.w3.org/2002/11/xquery-xpath-applets/xgrammar.zip + + Copyright (C) 2002 World Wide Web Consortium, (Massachusetts Institute of + Technology, European Research Consortium for Informatics and Mathematics, + Keio University). All Rights Reserved. + + This work is distributed under the W3C(R) Software License in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + W3C(R) SOFTWARE NOTICE AND LICENSE + http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231 + + This work (and included software, documentation such as READMEs, or + other related items) is being provided by the copyright holders under + the following license. By obtaining, using and/or copying this work, + you (the licensee) agree that you have read, understood, and will comply + with the following terms and conditions. + + Permission to copy, modify, and distribute this software and its + documentation, with or without modification, for any purpose and + without fee or royalty is hereby granted, provided that you include + the following on ALL copies of the software and documentation or + portions thereof, including modifications: + + 1. The full text of this NOTICE in a location viewable to users + of the redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, + or terms and conditions. If none exist, the W3C Software Short + Notice should be included (hypertext is preferred, text is + permitted) within the body of any redistributed or derivative code. + + 3. Notice of any changes or modifications to the files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT + HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR + DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, + TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and + any associated documentation will at all times remain with + copyright holders. + +PDFBox libraries (pdfbox, jempbox, fontbox) + + Copyright (c) 2002-2007, www.pdfbox.org + Copyright (c) 2006-2007, www.jempbox.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of pdfbox; nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Adobe Font Metrics (AFM) for PDF Core 14 Fonts + + This file and the 14 PostScript(R) AFM files it accompanies may be used, + copied, and distributed for any purpose and without charge, with or without + modification, provided that all copyright notices are retained; that the + AFM files are not distributed without this file; that all modifications + to this file or any of the AFM files are prominently noted in the modified + file(s); and that this paragraph is not modified. Adobe Systems has no + responsibility or obligation to support the use of the AFM files. + +CMaps for PDF Fonts (http://www.adobe.com/devnet/font/#pcfi and +ftp://ftp.oreilly.com/pub/examples/nutshell/cjkv/adobe/) + + Copyright 1990-2001 Adobe Systems Incorporated. + All Rights Reserved. + + Patents Pending + + NOTICE: All information contained herein is the property + of Adobe Systems Incorporated. + + Permission is granted for redistribution of this file + provided this copyright notice is maintained intact and + that the contents of this file are not altered in any + way from its original form. + + PostScript and Display PostScript are trademarks of + Adobe Systems Incorporated which may be registered in + certain jurisdictions. + +Glyphlist (http://www.adobe.com/devnet/opentype/archives/glyph.html) + + Copyright (c) 1997,1998,2002,2007 Adobe Systems Incorporated + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this documentation file to use, copy, publish, distribute, + sublicense, and/or sell copies of the documentation, and to permit + others to do the same, provided that: + - No modification, editing or other alteration of this document is + allowed; and + - The above copyright notice and this permission notice shall be + included in all copies of the documentation. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this documentation file, to create their own derivative works + from the content of this document to use, copy, publish, distribute, + sublicense, and/or sell the derivative works, and to permit others to do + the same, provided that the derived work is not represented as being a + copy or version of this document. + + Adobe shall not be liable to any party for any loss of revenue or profit + or for indirect, incidental, special, consequential, or other similar + damages, whether based on tort (including without limitation negligence + or strict liability), contract or other legal or equitable grounds even + if Adobe has been advised or had reason to know of the possibility of + such damages. The Adobe materials are provided on an "AS IS" basis. + Adobe specifically disclaims all express, statutory, or implied + warranties relating to the Adobe materials, including but not limited to + those concerning merchantability or fitness for a particular purpose or + non-infringement of any third party rights regarding the Adobe + materials. + +The International Components for Unicode (http://site.icu-project.org/) + + Copyright (c) 1995-2009 International Business Machines Corporation + and others + + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, and/or sell copies of the Software, and to permit persons + to whom the Software is furnished to do so, provided that the above + copyright notice(s) and this permission notice appear in all copies + of the Software and that both the above copyright notice(s) and this + permission notice appear in supporting documentation. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE + BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, + OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + SOFTWARE. + + Except as contained in this notice, the name of a copyright holder shall + not be used in advertising or otherwise to promote the sale, use or other + dealings in this Software without prior written authorization of the + copyright holder. + +MIME type information from file-4.26.tar.gz (http://www.darwinsys.com/file/) + + Copyright (c) Ian F. Darwin 1986, 1987, 1989, 1990, 1991, 1992, 1994, 1995. + Software written by Ian F. Darwin and others; + maintained 1994- Christos Zoulas. + + This software is not subject to any export provision of the United States + Department of Commerce, and may be exported to any country or planet. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice immediately at the beginning of the file, without modification, + this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Metadata extractor library (metadata-extractor) + + This is public domain software - that is, you can do whatever you want + with it, and include it software that is licensed under the GNU or the + BSD license, or whatever other licence you choose, including proprietary + closed source licenses. I do ask that you leave this header in tact. + + If you make modifications to this code that you think would benefit the + wider community, please send me a copy and I'll post it on my site. + + If you make use of this code, I'd appreciate hearing about it. + metadata_extractor [at] drewnoakes [dot] com + Latest version of this software kept at + http://drewnoakes.com/ + +ASM bytecode manipulation library (asm) + + Copyright (c) 2000-2005 INRIA, France Telecom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +SLF4J libraries (slf4j-api, log4j-over-slf4j, jcl-over-slf4j) + + Copyright (c) 2004-2008 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Logback library (logback-core, logback-classic) + + Logback: the reliable, generic, fast and flexible logging framework. + Copyright (C) 1999-2009, QOS.ch. All rights reserved. + + This program and the accompanying materials are dual-licensed under + either the terms of the Eclipse Public License v1.0 as published by + the Eclipse Foundation + + or (per the licensee's choosing) + + under the terms of the GNU Lesser General Public License version 2.1 + as published by the Free Software Foundation. + +XML API library, org.w3c classes (xml-apis) + + DOM Java Language Binding: + http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/java-binding.html + + W3C IPR SOFTWARE NOTICE + Copyright (C) 2000 World Wide Web Consortium, (Massachusetts Institute of + Technology, Institut National de Recherche en Informatique et en + Automatique, Keio University). All Rights Reserved. + + The DOM bindings are published under the W3C Software Copyright Notice + and License. The software license requires "Notice of any changes or + modifications to the W3C files, including the date changes were made." + Consequently, modified versions of the DOM bindings must document that + they do not conform to the W3C standard; in the case of the IDL binding, + the pragma prefix can no longer be 'w3c.org'; in the case of the Java + binding, the package names can no longer be in the 'org.w3c' package. + + Note: The original version of the W3C Software Copyright Notice and + License could be found at + http://www.w3.org/Consortium/Legal/copyright-software-19980720 + + Copyright (C) 1994-2000 World Wide Web Consortium, (Massachusetts + Institute of Technology, Institut National de Recherche en Informatique + et en Automatique, Keio University). All Rights Reserved. + http://www.w3.org/Consortium/Legal/ + + This W3C work (including software, documents, or other related items) is + being provided by the copyright holders under the following license. By + obtaining, using and/or copying this work, you (the licensee) agree that + you have read, understood, and will comply with the following terms and + conditions: + + Permission to use, copy, and modify this software and its documentation, + with or without modification, for any purpose and without fee or royalty + is hereby granted, provided that you include the following on ALL copies + of the software and documentation or portions thereof, including + modifications, that you make: + + 1. The full text of this NOTICE in a location viewable to users of the + redistributed or derivative work. + + 2. Any pre-existing intellectual property disclaimers, notices, or + terms and conditions. If none exist, a short notice of the following + form (hypertext is preferred, text is permitted) should be used + within the body of any redistributed or derivative code: + "Copyright (C) [$date-of-software] World Wide Web Consortium, + (Massachusetts Institute of Technology, Institut National de + Recherche en Informatique et en Automatique, Keio University). + All Rights Reserved. http://www.w3.org/Consortium/Legal/" + + 3. Notice of any changes or modifications to the W3C files, including + the date changes were made. (We recommend you provide URIs to the + location from which the code is derived.) + + THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS + MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT + NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR + PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENTATION WILL NOT INFRINGE + ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. + + COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL + OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR + DOCUMENTATION. + + The name and trademarks of copyright holders may NOT be used in + advertising or publicity pertaining to the software without specific, + written prior permission. Title to copyright in this software and any + associated documentation will at all times remain with copyright holders. + +XML API library, org.xml.sax classes (xml-apis) + + SAX2 is Free! + + I hereby abandon any property rights to SAX 2.0 (the Simple API for + XML), and release all of the SAX 2.0 source code, compiled code, and + documentation contained in this distribution into the Public Domain. + SAX comes with NO WARRANTY or guarantee of fitness for any purpose. + + David Megginson, david@megginson.com + 2000-05-05 + +Concurrent library (concurrent-1.3.4.jar) + + http://g.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html + + All classes are released to the public domain and may be used for any + purpose whatsoever without permission or acknowledgment. Portions of + the CopyOnWriteArrayList and ConcurrentReaderHashMap classes are adapted + from Sun JDK source code. These are copyright of Sun Microsystems, Inc, + and are used with their kind permission, as described in this license: + + TECHNOLOGY LICENSE FROM SUN MICROSYSTEMS, INC. TO DOUG LEA + + Whereas Doug Lea desires to utlized certain Java Software technologies + in the util.concurrent technology; and Whereas Sun Microsystems, Inc. + ("Sun") desires that Doug Lea utilize certain Java Software technologies + in the util.concurrent technology; + + Therefore the parties agree as follows, effective May 31, 2002: + + "Java Software technologies" means + + classes/java/util/ArrayList.java, and + classes/java/util/HashMap.java. + + The Java Software technologies are Copyright (c) 1994-2000 Sun + Microsystems, Inc. All rights reserved. + + Sun hereby grants Doug Lea a non-exclusive, worldwide, non-transferrable + license to use, reproduce, create derivate works of, and distribute the + Java Software and derivative works thereof in source and binary forms + as part of a larger work, and to sublicense the right to use, reproduce + and distribute the Java Software and Doug Lea's derivative works as the + part of larger works through multiple tiers of sublicensees provided that + the following conditions are met: + + -Neither the name of or trademarks of Sun may be used to endorse or + promote products including or derived from the Java Software technology + without specific prior written permission; and + -Redistributions of source or binary code must contain the above + copyright notice, this notice and and the following disclaimers: + + This software is provided "AS IS," without a warranty of any kind. + ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, + INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN + MICROSYSTEMS, INC. AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES + SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING + THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN MICROSYSTEMS, INC. + OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR + DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, + HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF + THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN MICROSYSTEMS, INC. + HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + You acknowledge that Software is not designed,licensed or intended for + use in the design, construction, operation or maintenance of any nuclear + facility. + +Office Open XML schemas (ooxml-schemas-1.0.jar) + + The Office Open XML schema definitions used by Apache POI are + a part of the Office Open XML ECMA Specification (ECMA-376, [1]). + As defined in section 9.4 of the ECMA bylaws [2], this specification + is available to all interested parties without restriction: + + 9.4 All documents when approved shall be made available to + all interested parties without restriction. + + Furthermore, both Microsoft and Adobe have granted patent licenses + to this work [3,4,5]. + + [1] http://www.ecma-international.org/publications/standards/Ecma-376.htm + [2] http://www.ecma-international.org/memento/Ecmabylaws.htm + [3] http://www.microsoft.com/interop/osp/ + [4] http://www.ecma-international.org/publications/files/ECMA-ST/Ecma%20PATENT/ECMA-376%20Edition%201%20Microsoft%20Patent%20Declaration.pdf + [5] http://www.ecma-international.org/publications/files/ECMA-ST/Ecma%20PATENT/ga-2006-191.pdf + +DOM4J library (dom4j-1.6.1.jar) + + Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved. + + Redistribution and use of this software and associated documentation + ("Software"), with or without modification, are permitted provided + that the following conditions are met: + + 1. Redistributions of source code must retain copyright + statements and notices. Redistributions must also contain a + copy of this document. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + + 3. The name "DOM4J" must not be used to endorse or promote + products derived from this Software without prior written + permission of MetaStuff, Ltd. For written permission, + please contact dom4j-info@metastuff.com. + + 4. Products derived from this Software may not be called "DOM4J" + nor may "DOM4J" appear in their names without prior written + permission of MetaStuff, Ltd. DOM4J is a registered + trademark of MetaStuff, Ltd. + + 5. Due credit should be given to the DOM4J Project - + http://www.dom4j.org + + THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + +Unicode conversion code in Lucene Java (lucene-core) + + Copyright 2001-2004 Unicode, Inc. + + Disclaimer + + This source code is provided as is by Unicode, Inc. No claims are + made as to fitness for any particular purpose. No warranties of any + kind are expressed or implied. The recipient agrees to determine + applicability of information provided. If this file has been + purchased on magnetic or optical media from Unicode, Inc., the + sole remedy for any claim will be exchange of defective media + within 90 days of receipt. + + Limitations on Rights to Redistribute This Code + + Unicode, Inc. hereby grants the right to freely use the information + supplied in this file in the creation of products supporting the + Unicode Standard, and to make copies of this file in any form + for internal or external distribution as long as this notice + remains attached. + +Array utility code in Lucene Java (lucene-core) + + PSF LICENSE AGREEMENT FOR PYTHON 2.4 + ------------------------------------ + + 1. This LICENSE AGREEMENT is between the Python Software Foundation + ("PSF"), and the Individual or Organization ("Licensee") accessing and + otherwise using Python 2.4 software in source or binary form and its + associated documentation. + + 2. Subject to the terms and conditions of this License Agreement, PSF + hereby grants Licensee a nonexclusive, royalty-free, world-wide + license to reproduce, analyze, test, perform and/or display publicly, + prepare derivative works, distribute, and otherwise use Python 2.4 + alone or in any derivative version, provided, however, that PSF's + License Agreement and PSF's notice of copyright, i.e., "Copyright (c) + 2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved" + are retained in Python 2.4 alone or in any derivative version prepared + by Licensee. + + 3. In the event Licensee prepares a derivative work that is based on + or incorporates Python 2.4 or any part thereof, and wants to make + the derivative work available to others as provided herein, then + Licensee hereby agrees to include in any such work a brief summary of + the changes made to Python 2.4. + + 4. PSF is making Python 2.4 available to Licensee on an "AS IS" + basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.4 WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON + 2.4 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS + A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.4, + OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 7. Nothing in this License Agreement shall be deemed to create any + relationship of agency, partnership, or joint venture between PSF and + Licensee. This License Agreement does not grant permission to use PSF + trademarks or trade name in a trademark sense to endorse or promote + products or services of Licensee, or any third party. + + 8. By copying, installing or otherwise using Python 2.4, Licensee + agrees to be bound by the terms and conditions of this License + Agreement. + + BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 + ------------------------------------------- + + BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + + 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an + office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the + Individual or Organization ("Licensee") accessing and otherwise using + this software in source or binary form and its associated + documentation ("the Software"). + + 2. Subject to the terms and conditions of this BeOpen Python License + Agreement, BeOpen hereby grants Licensee a non-exclusive, + royalty-free, world-wide license to reproduce, analyze, test, perform + and/or display publicly, prepare derivative works, distribute, and + otherwise use the Software alone or in any derivative version, + provided, however, that the BeOpen Python License is retained in the + Software, alone or in any derivative version prepared by Licensee. + + 3. BeOpen is making the Software available to Licensee on an "AS IS" + basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE + SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS + AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY + DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 5. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 6. This License Agreement shall be governed by and interpreted in all + respects by the law of the State of California, excluding conflict of + law provisions. Nothing in this License Agreement shall be deemed to + create any relationship of agency, partnership, or joint venture + between BeOpen and Licensee. This License Agreement does not grant + permission to use BeOpen trademarks or trade names in a trademark + sense to endorse or promote products or services of Licensee, or any + third party. As an exception, the "BeOpen Python" logos available at + http://www.pythonlabs.com/logos.html may be used according to the + permissions granted on that web page. + + 7. By copying, installing or otherwise using the software, Licensee + agrees to be bound by the terms and conditions of this License + Agreement. + + CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 + --------------------------------------- + + 1. This LICENSE AGREEMENT is between the Corporation for National + Research Initiatives, having an office at 1895 Preston White Drive, + Reston, VA 20191 ("CNRI"), and the Individual or Organization + ("Licensee") accessing and otherwise using Python 1.6.1 software in + source or binary form and its associated documentation. + + 2. Subject to the terms and conditions of this License Agreement, CNRI + hereby grants Licensee a nonexclusive, royalty-free, world-wide + license to reproduce, analyze, test, perform and/or display publicly, + prepare derivative works, distribute, and otherwise use Python 1.6.1 + alone or in any derivative version, provided, however, that CNRI's + License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) + 1995-2001 Corporation for National Research Initiatives; All Rights + Reserved" are retained in Python 1.6.1 alone or in any derivative + version prepared by Licensee. Alternately, in lieu of CNRI's License + Agreement, Licensee may substitute the following text (omitting the + quotes): "Python 1.6.1 is made available subject to the terms and + conditions in CNRI's License Agreement. This Agreement together with + Python 1.6.1 may be located on the Internet using the following + unique, persistent identifier (known as a handle): 1895.22/1013. This + Agreement may also be obtained from a proxy server on the Internet + using the following URL: http://hdl.handle.net/1895.22/1013". + + 3. In the event Licensee prepares a derivative work that is based on + or incorporates Python 1.6.1 or any part thereof, and wants to make + the derivative work available to others as provided herein, then + Licensee hereby agrees to include in any such work a brief summary of + the changes made to Python 1.6.1. + + 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" + basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON + 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS + A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, + OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 7. This License Agreement shall be governed by the federal + intellectual property law of the United States, including without + limitation the federal copyright law, and, to the extent such + U.S. federal law does not apply, by the law of the Commonwealth of + Virginia, excluding Virginia's conflict of law provisions. + Notwithstanding the foregoing, with regard to derivative works based + on Python 1.6.1 that incorporate non-separable material that was + previously distributed under the GNU General Public License (GPL), the + law of the Commonwealth of Virginia shall govern this License + Agreement only as to issues arising under or with respect to + Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this + License Agreement shall be deemed to create any relationship of + agency, partnership, or joint venture between CNRI and Licensee. This + License Agreement does not grant permission to use CNRI trademarks or + trade name in a trademark sense to endorse or promote products or + services of Licensee, or any third party. + + 8. By clicking on the "ACCEPT" button where indicated, or by copying, + installing or otherwise using Python 1.6.1, Licensee agrees to be + bound by the terms and conditions of this License Agreement. + + ACCEPT + + + CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 + -------------------------------------------------- + + Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, + The Netherlands. All rights reserved. + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the name of Stichting Mathematisch + Centrum or CWI not be used in advertising or publicity pertaining to + distribution of the software without specific, written prior + permission. + + STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE + FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +AspectJ runtime library (aspectjrt) + + Eclipse Public License - v 1.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF + THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial code and + documentation distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + + i) changes to the Program, and + + ii) additions to the Program; + + where such changes and/or additions to the Program originate from and + are distributed by that particular Contributor. A Contribution + 'originates' from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include additions to the Program which: (i) are + separate modules of software distributed in conjunction with the + Program under their own license agreement, and (ii) are not derivative + works of the Program. + + "Contributor" means any person or entity that distributes the Program. + + "Licensed Patents " mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone or + when combined with the Program. + + "Program" means the Contributions distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement, + including all Contributors. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free copyright license to + reproduce, prepare derivative works of, publicly display, publicly + perform, distribute and sublicense the Contribution of such + Contributor, if any, and such derivative works, in source code and + object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby grants + Recipient a non-exclusive, worldwide, royalty-free patent license under + Licensed Patents to make, use, sell, offer to sell, import and + otherwise transfer the Contribution of such Contributor, if any, in + source code and object code form. This patent license shall apply to + the combination of the Contribution and the Program if, at the time + the Contribution is added by the Contributor, such addition of the + Contribution causes such combination to be covered by the Licensed + Patents. The patent license shall not apply to any other combinations + which include the Contribution. No hardware per se is licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. Each + Contributor disclaims any liability to Recipient for claims brought by + any other entity based on infringement of intellectual property rights + or otherwise. As a condition to exercising the rights and licenses + granted hereunder, each Recipient hereby assumes sole responsibility + to secure any other intellectual property rights needed, if any. For + example, if a third party patent license is required to allow Recipient + to distribute the Program, it is Recipient's responsibility to acquire + that license before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + + 3. REQUIREMENTS + + A Contributor may choose to distribute the Program in object code form + under its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + + i) effectively disclaims on behalf of all Contributors all warranties + and conditions, express and implied, including warranties or + conditions of title and non-infringement, and implied warranties + or conditions of merchantability and fitness for a particular + purpose; + + ii) effectively excludes on behalf of all Contributors all liability + for damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + + iii) states that any provisions which differ from this Agreement are + offered by that Contributor alone and not by any other party; and + + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a + reasonable manner on or through a medium customarily used for + software exchange. + + When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the + Program. + + Contributors may not remove or alter any copyright notices contained + within the Program. + + Each Contributor must identify itself as the originator of its + Contribution, if any, in a manner that reasonably allows subsequent + Recipients to identify the originator of the Contribution. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product offering + should do so in a manner which does not create potential liability for + other Contributors. Therefore, if a Contributor includes the Program in + a commercial product offering, such Contributor ("Commercial Contributor") + hereby agrees to defend and indemnify every other Contributor + ("Indemnified Contributor") against any losses, damages and costs + (collectively "Losses") arising from claims, lawsuits and other legal + actions brought by a third party against the Indemnified Contributor to + the extent caused by the acts or omissions of such Commercial Contributor + in connection with its distribution of the Program in a commercial + product offering. The obligations in this section do not apply to any + claims or Losses relating to any actual or alleged intellectual property + infringement. In order to qualify, an Indemnified Contributor must: + a) promptly notify the Commercial Contributor in writing of such claim, + and b) allow the Commercial Contributor to control, and cooperate with + the Commercial Contributor in, the defense and any related settlement + negotiations. The Indemnified Contributor may participate in any such + claim at its own expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance claims, + or offers warranties related to Product X, those performance claims and + warranties are such Commercial Contributor's responsibility alone. Under + this section, the Commercial Contributor would have to defend claims + against the other Contributors related to those performance claims and + warranties, and if a court requires any other Contributor to pay any + damages as a result, the Commercial Contributor must pay those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED + ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER + EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR + CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A + PARTICULAR PURPOSE. Each Recipient is solely responsible for determining + the appropriateness of using and distributing the Program and assumes all + risks associated with its exercise of rights under this Agreement , + including but not limited to the risks and costs of program errors, + compliance with applicable laws, damage to or loss of data, programs or + equipment, and unavailability or interruption of operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR + ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING + WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR + DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED + HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further action + by the parties hereto, such provision shall be reformed to the minimum + extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity (including + a cross-claim or counterclaim in a lawsuit) alleging that the Program + itself (excluding combinations of the Program with other software or + hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it fails + to comply with any of the material terms or conditions of this Agreement + and does not cure such failure in a reasonable period of time after + becoming aware of such noncompliance. If all Recipient's rights under + this Agreement terminate, Recipient agrees to cease use and distribution + of the Program as soon as reasonably practicable. However, Recipient's + obligations under this Agreement and any licenses granted by Recipient + relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and may + only be modified in the following manner. The Agreement Steward reserves + the right to publish new versions (including revisions) of this Agreement + from time to time. No one other than the Agreement Steward has the right + to modify this Agreement. The Eclipse Foundation is the initial Agreement + Steward. The Eclipse Foundation may assign the responsibility to serve as + the Agreement Steward to a suitable separate entity. Each new version of + the Agreement will be given a distinguishing version number. The Program + (including Contributions) may always be distributed subject to the version + of the Agreement under which it was received. In addition, after a new + version of the Agreement is published, Contributor may elect to distribute + the Program (including its Contributions) under the new version. Except as + expressly stated in Sections 2(a) and 2(b) above, Recipient receives no + rights or licenses to the intellectual property of any Contributor under + this Agreement, whether expressly, by implication, estoppel or otherwise. + All rights in the Program not expressly granted under this Agreement + are reserved. + + This Agreement is governed by the laws of the State of New York and the + intellectual property laws of the United States of America. No party to + this Agreement will bring a legal action under this Agreement more than + one year after the cause of action arose. Each party waives its rights to + a jury trial in any resulting litigation. + +juniversalchardet library (juniversalchardet) + + MOZILLA PUBLIC LICENSE + Version 1.1 + + --------------- + + 1. Definitions. + + 1.0.1. "Commercial Use" means distribution or otherwise making the + Covered Code available to a third party. + + 1.1. "Contributor" means each entity that creates or contributes to + the creation of Modifications. + + 1.2. "Contributor Version" means the combination of the Original + Code, prior Modifications used by a Contributor, and the Modifications + made by that particular Contributor. + + 1.3. "Covered Code" means the Original Code or Modifications or the + combination of the Original Code and Modifications, in each case + including portions thereof. + + 1.4. "Electronic Distribution Mechanism" means a mechanism generally + accepted in the software development community for the electronic + transfer of data. + + 1.5. "Executable" means Covered Code in any form other than Source + Code. + + 1.6. "Initial Developer" means the individual or entity identified + as the Initial Developer in the Source Code notice required by Exhibit + A. + + 1.7. "Larger Work" means a work which combines Covered Code or + portions thereof with code not governed by the terms of this License. + + 1.8. "License" means this document. + + 1.8.1. "Licensable" means having the right to grant, to the maximum + extent possible, whether at the time of the initial grant or + subsequently acquired, any and all of the rights conveyed herein. + + 1.9. "Modifications" means any addition to or deletion from the + substance or structure of either the Original Code or any previous + Modifications. When Covered Code is released as a series of files, a + Modification is: + A. Any addition to or deletion from the contents of a file + containing Original Code or previous Modifications. + + B. Any new file that contains any part of the Original Code or + previous Modifications. + + 1.10. "Original Code" means Source Code of computer software code + which is described in the Source Code notice required by Exhibit A as + Original Code, and which, at the time of its release under this + License is not already Covered Code governed by this License. + + 1.10.1. "Patent Claims" means any patent claim(s), now owned or + hereafter acquired, including without limitation, method, process, + and apparatus claims, in any patent Licensable by grantor. + + 1.11. "Source Code" means the preferred form of the Covered Code for + making modifications to it, including all modules it contains, plus + any associated interface definition files, scripts used to control + compilation and installation of an Executable, or source code + differential comparisons against either the Original Code or another + well known, available Covered Code of the Contributor's choice. The + Source Code can be in a compressed or archival form, provided the + appropriate decompression or de-archiving software is widely available + for no charge. + + 1.12. "You" (or "Your") means an individual or a legal entity + exercising rights under, and complying with all of the terms of, this + License or a future version of this License issued under Section 6.1. + For legal entities, "You" includes any entity which controls, is + controlled by, or is under common control with You. For purposes of + this definition, "control" means (a) the power, direct or indirect, + to cause the direction or management of such entity, whether by + contract or otherwise, or (b) ownership of more than fifty percent + (50%) of the outstanding shares or beneficial ownership of such + entity. + + 2. Source Code License. + + 2.1. The Initial Developer Grant. + The Initial Developer hereby grants You a world-wide, royalty-free, + non-exclusive license, subject to third party intellectual property + claims: + (a) under intellectual property rights (other than patent or + trademark) Licensable by Initial Developer to use, reproduce, + modify, display, perform, sublicense and distribute the Original + Code (or portions thereof) with or without Modifications, and/or + as part of a Larger Work; and + + (b) under Patents Claims infringed by the making, using or + selling of Original Code, to make, have made, use, practice, + sell, and offer for sale, and/or otherwise dispose of the + Original Code (or portions thereof). + + (c) the licenses granted in this Section 2.1(a) and (b) are + effective on the date Initial Developer first distributes + Original Code under the terms of this License. + + (d) Notwithstanding Section 2.1(b) above, no patent license is + granted: 1) for code that You delete from the Original Code; 2) + separate from the Original Code; or 3) for infringements caused + by: i) the modification of the Original Code or ii) the + combination of the Original Code with other software or devices. + + 2.2. Contributor Grant. + Subject to third party intellectual property claims, each Contributor + hereby grants You a world-wide, royalty-free, non-exclusive license + + (a) under intellectual property rights (other than patent or + trademark) Licensable by Contributor, to use, reproduce, modify, + display, perform, sublicense and distribute the Modifications + created by such Contributor (or portions thereof) either on an + unmodified basis, with other Modifications, as Covered Code + and/or as part of a Larger Work; and + + (b) under Patent Claims infringed by the making, using, or + selling of Modifications made by that Contributor either alone + and/or in combination with its Contributor Version (or portions + of such combination), to make, use, sell, offer for sale, have + made, and/or otherwise dispose of: 1) Modifications made by that + Contributor (or portions thereof); and 2) the combination of + Modifications made by that Contributor with its Contributor + Version (or portions of such combination). + + (c) the licenses granted in Sections 2.2(a) and 2.2(b) are + effective on the date Contributor first makes Commercial Use of + the Covered Code. + + (d) Notwithstanding Section 2.2(b) above, no patent license is + granted: 1) for any code that Contributor has deleted from the + Contributor Version; 2) separate from the Contributor Version; + 3) for infringements caused by: i) third party modifications of + Contributor Version or ii) the combination of Modifications made + by that Contributor with other software (except as part of the + Contributor Version) or other devices; or 4) under Patent Claims + infringed by Covered Code in the absence of Modifications made by + that Contributor. + + 3. Distribution Obligations. + + 3.1. Application of License. + The Modifications which You create or to which You contribute are + governed by the terms of this License, including without limitation + Section 2.2. The Source Code version of Covered Code may be + distributed only under the terms of this License or a future version + of this License released under Section 6.1, and You must include a + copy of this License with every copy of the Source Code You + distribute. You may not offer or impose any terms on any Source Code + version that alters or restricts the applicable version of this + License or the recipients' rights hereunder. However, You may include + an additional document offering the additional rights described in + Section 3.5. + + 3.2. Availability of Source Code. + Any Modification which You create or to which You contribute must be + made available in Source Code form under the terms of this License + either on the same media as an Executable version or via an accepted + Electronic Distribution Mechanism to anyone to whom you made an + Executable version available; and if made available via Electronic + Distribution Mechanism, must remain available for at least twelve (12) + months after the date it initially became available, or at least six + (6) months after a subsequent version of that particular Modification + has been made available to such recipients. You are responsible for + ensuring that the Source Code version remains available even if the + Electronic Distribution Mechanism is maintained by a third party. + + 3.3. Description of Modifications. + You must cause all Covered Code to which You contribute to contain a + file documenting the changes You made to create that Covered Code and + the date of any change. You must include a prominent statement that + the Modification is derived, directly or indirectly, from Original + Code provided by the Initial Developer and including the name of the + Initial Developer in (a) the Source Code, and (b) in any notice in an + Executable version or related documentation in which You describe the + origin or ownership of the Covered Code. + + 3.4. Intellectual Property Matters + (a) Third Party Claims. + If Contributor has knowledge that a license under a third party's + intellectual property rights is required to exercise the rights + granted by such Contributor under Sections 2.1 or 2.2, + Contributor must include a text file with the Source Code + distribution titled "LEGAL" which describes the claim and the + party making the claim in sufficient detail that a recipient will + know whom to contact. If Contributor obtains such knowledge after + the Modification is made available as described in Section 3.2, + Contributor shall promptly modify the LEGAL file in all copies + Contributor makes available thereafter and shall take other steps + (such as notifying appropriate mailing lists or newsgroups) + reasonably calculated to inform those who received the Covered + Code that new knowledge has been obtained. + + (b) Contributor APIs. + If Contributor's Modifications include an application programming + interface and Contributor has knowledge of patent licenses which + are reasonably necessary to implement that API, Contributor must + also include this information in the LEGAL file. + + (c) Representations. + Contributor represents that, except as disclosed pursuant to + Section 3.4(a) above, Contributor believes that Contributor's + Modifications are Contributor's original creation(s) and/or + Contributor has sufficient rights to grant the rights conveyed by + this License. + + 3.5. Required Notices. + You must duplicate the notice in Exhibit A in each file of the Source + Code. If it is not possible to put such notice in a particular Source + Code file due to its structure, then You must include such notice in a + location (such as a relevant directory) where a user would be likely + to look for such a notice. If You created one or more Modification(s) + You may add your name as a Contributor to the notice described in + Exhibit A. You must also duplicate this License in any documentation + for the Source Code where You describe recipients' rights or ownership + rights relating to Covered Code. You may choose to offer, and to + charge a fee for, warranty, support, indemnity or liability + obligations to one or more recipients of Covered Code. However, You + may do so only on Your own behalf, and not on behalf of the Initial + Developer or any Contributor. You must make it absolutely clear than + any such warranty, support, indemnity or liability obligation is + offered by You alone, and You hereby agree to indemnify the Initial + Developer and every Contributor for any liability incurred by the + Initial Developer or such Contributor as a result of warranty, + support, indemnity or liability terms You offer. + + 3.6. Distribution of Executable Versions. + You may distribute Covered Code in Executable form only if the + requirements of Section 3.1-3.5 have been met for that Covered Code, + and if You include a notice stating that the Source Code version of + the Covered Code is available under the terms of this License, + including a description of how and where You have fulfilled the + obligations of Section 3.2. The notice must be conspicuously included + in any notice in an Executable version, related documentation or + collateral in which You describe recipients' rights relating to the + Covered Code. You may distribute the Executable version of Covered + Code or ownership rights under a license of Your choice, which may + contain terms different from this License, provided that You are in + compliance with the terms of this License and that the license for the + Executable version does not attempt to limit or alter the recipient's + rights in the Source Code version from the rights set forth in this + License. If You distribute the Executable version under a different + license You must make it absolutely clear that any terms which differ + from this License are offered by You alone, not by the Initial + Developer or any Contributor. You hereby agree to indemnify the + Initial Developer and every Contributor for any liability incurred by + the Initial Developer or such Contributor as a result of any such + terms You offer. + + 3.7. Larger Works. + You may create a Larger Work by combining Covered Code with other code + not governed by the terms of this License and distribute the Larger + Work as a single product. In such a case, You must make sure the + requirements of this License are fulfilled for the Covered Code. + + 4. Inability to Comply Due to Statute or Regulation. + + If it is impossible for You to comply with any of the terms of this + License with respect to some or all of the Covered Code due to + statute, judicial order, or regulation then You must: (a) comply with + the terms of this License to the maximum extent possible; and (b) + describe the limitations and the code they affect. Such description + must be included in the LEGAL file described in Section 3.4 and must + be included with all distributions of the Source Code. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + + 5. Application of this License. + + This License applies to code to which the Initial Developer has + attached the notice in Exhibit A and to related Covered Code. + + 6. Versions of the License. + + 6.1. New Versions. + Netscape Communications Corporation ("Netscape") may publish revised + and/or new versions of the License from time to time. Each version + will be given a distinguishing version number. + + 6.2. Effect of New Versions. + Once Covered Code has been published under a particular version of the + License, You may always continue to use it under the terms of that + version. You may also choose to use such Covered Code under the terms + of any subsequent version of the License published by Netscape. No one + other than Netscape has the right to modify the terms applicable to + Covered Code created under this License. + + 6.3. Derivative Works. + If You create or use a modified version of this License (which you may + only do in order to apply it to code which is not already Covered Code + governed by this License), You must (a) rename Your license so that + the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", + "MPL", "NPL" or any confusingly similar phrase do not appear in your + license (except to note that your license differs from this License) + and (b) otherwise make it clear that Your version of the license + contains terms which differ from the Mozilla Public License and + Netscape Public License. (Filling in the name of the Initial + Developer, Original Code or Contributor in the notice described in + Exhibit A shall not of themselves be deemed to be modifications of + this License.) + + 7. DISCLAIMER OF WARRANTY. + + COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, + WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, + WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF + DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. + THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE + IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, + YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE + COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER + OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF + ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + + 8. TERMINATION. + + 8.1. This License and the rights granted hereunder will terminate + automatically if You fail to comply with terms herein and fail to cure + such breach within 30 days of becoming aware of the breach. All + sublicenses to the Covered Code which are properly granted shall + survive any termination of this License. Provisions which, by their + nature, must remain in effect beyond the termination of this License + shall survive. + + 8.2. If You initiate litigation by asserting a patent infringement + claim (excluding declatory judgment actions) against Initial Developer + or a Contributor (the Initial Developer or Contributor against whom + You file such action is referred to as "Participant") alleging that: + + (a) such Participant's Contributor Version directly or indirectly + infringes any patent, then any and all rights granted by such + Participant to You under Sections 2.1 and/or 2.2 of this License + shall, upon 60 days notice from Participant terminate prospectively, + unless if within 60 days after receipt of notice You either: (i) + agree in writing to pay Participant a mutually agreeable reasonable + royalty for Your past and future use of Modifications made by such + Participant, or (ii) withdraw Your litigation claim with respect to + the Contributor Version against such Participant. If within 60 days + of notice, a reasonable royalty and payment arrangement are not + mutually agreed upon in writing by the parties or the litigation claim + is not withdrawn, the rights granted by Participant to You under + Sections 2.1 and/or 2.2 automatically terminate at the expiration of + the 60 day notice period specified above. + + (b) any software, hardware, or device, other than such Participant's + Contributor Version, directly or indirectly infringes any patent, then + any rights granted to You by such Participant under Sections 2.1(b) + and 2.2(b) are revoked effective as of the date You first made, used, + sold, distributed, or had made, Modifications made by that + Participant. + + 8.3. If You assert a patent infringement claim against Participant + alleging that such Participant's Contributor Version directly or + indirectly infringes any patent where such claim is resolved (such as + by license or settlement) prior to the initiation of patent + infringement litigation, then the reasonable value of the licenses + granted by such Participant under Sections 2.1 or 2.2 shall be taken + into account in determining the amount or value of any payment or + license. + + 8.4. In the event of termination under Sections 8.1 or 8.2 above, + all end user license agreements (excluding distributors and resellers) + which have been validly granted by You or any distributor hereunder + prior to termination shall survive termination. + + 9. LIMITATION OF LIABILITY. + + UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT + (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL + DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, + OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR + ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY + CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, + WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER + COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN + INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF + LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY + RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW + PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE + EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO + THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + + 10. U.S. GOVERNMENT END USERS. + + The Covered Code is a "commercial item," as that term is defined in + 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer + software" and "commercial computer software documentation," as such + terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 + C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), + all U.S. Government End Users acquire Covered Code with only those + rights set forth herein. + + 11. MISCELLANEOUS. + + This License represents the complete agreement concerning subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. This License shall be governed by + California law provisions (except to the extent applicable law, if + any, provides otherwise), excluding its conflict-of-law provisions. + With respect to disputes in which at least one party is a citizen of, + or an entity chartered or registered to do business in the United + States of America, any litigation relating to this License shall be + subject to the jurisdiction of the Federal Courts of the Northern + District of California, with venue lying in Santa Clara County, + California, with the losing party responsible for costs, including + without limitation, court costs and reasonable attorneys' fees and + expenses. The application of the United Nations Convention on + Contracts for the International Sale of Goods is expressly excluded. + Any law or regulation which provides that the language of a contract + shall be construed against the drafter shall not apply to this + License. + + 12. RESPONSIBILITY FOR CLAIMS. + + As between Initial Developer and the Contributors, each party is + responsible for claims and damages arising, directly or indirectly, + out of its utilization of rights under this License and You agree to + work with Initial Developer and Contributors to distribute such + responsibility on an equitable basis. Nothing herein is intended or + shall be deemed to constitute any admission of liability. + + 13. MULTIPLE-LICENSED CODE. + + Initial Developer may designate portions of the Covered Code as + "Multiple-Licensed". "Multiple-Licensed" means that the Initial + Developer permits you to utilize portions of the Covered Code under + Your choice of the NPL or the alternative licenses, if any, specified + by the Initial Developer in the file described in Exhibit A. + + EXHIBIT A -Mozilla Public License. + + ``The contents of this file are subject to the Mozilla Public License + Version 1.1 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + http://www.mozilla.org/MPL/ + + Software distributed under the License is distributed on an "AS IS" + basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + License for the specific language governing rights and limitations + under the License. + + The Original Code is ______________________________________. + + The Initial Developer of the Original Code is ________________________. + Portions created by ______________________ are Copyright (C) ______ + _______________________. All Rights Reserved. + + Contributor(s): ______________________________________. + + Alternatively, the contents of this file may be used under the terms + of the _____ license (the "[___] License"), in which case the + provisions of [______] License are applicable instead of those + above. If you wish to allow use of your version of this file only + under the terms of the [____] License and not to allow others to use + your version of this file under the MPL, indicate your decision by + deleting the provisions above and replace them with the notice and + other provisions required by the [___] License. If you do not delete + the provisions above, a recipient may use your version of this file + under either the MPL or the [___] License." + + [NOTE: The text of this Exhibit A may differ slightly from the text of + the notices in the Source Code files of the Original Code. You should + use the text of this Exhibit A rather than the text found in the + Original Code Source Code for Your Modifications.] diff --git a/jackrabbit-webapp/src/main/webapp/META-INF/NOTICE b/jackrabbit-webapp/src/main/webapp/META-INF/NOTICE new file mode 100644 index 00000000000..362dbf5a9f7 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/META-INF/NOTICE @@ -0,0 +1,41 @@ +Apache Jackrabbit +Copyright 2010 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Based on source code originally developed by +Day Software (http://www.day.com/). + +This product includes software from the following contributions: + +Original BZip2 classes contributed by Keiron Liddle +, Aftex Software to the Apache Ant project + +Original Tar classes from contributors of the Apache Ant project + +Original Zip classes from contributors of the Apache Ant project + +Original CPIO classes contributed by Markus Kuss and the jRPM project +(jrpm.sourceforge.net) + +Portions of Derby were originally developed by International Business +Machines Corporation and are licensed to the Apache Software Foundation +under the "Software Grant and Corporate Contribution License Agreement", +informally known as the "Derby CLA". The following copyright notice(s) +were affixed to portions of the code with which this file is now or was +at one time distributed and are placed here unaltered. + + (C) Copyright 1997,2004 International Business Machines Corporation. + All rights reserved. + + (C) Copyright IBM Corp. 2003. + +The JDBC apis for small devices and JDBC3 (under java/stubs/jsr169 and +java/stubs/jdbc3) were produced by trimming sources supplied by the +Apache Harmony project. The following notice covers the Harmony sources: + + Portions of Harmony were originally developed by + Intel Corporation and are licensed to the Apache Software + Foundation under the "Software Grant and Corporate Contribution + License Agreement", informally known as the "Intel Harmony CLA". diff --git a/jackrabbit-webapp/src/main/webapp/WEB-INF/batchread.properties b/jackrabbit-webapp/src/main/webapp/WEB-INF/batchread.properties new file mode 100644 index 00000000000..16bd71f7ba9 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/WEB-INF/batchread.properties @@ -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. + +# This file contains an example batch read configuration used by +# JcrRemotingServlet according to the 'batchread-config' init-param. +# +# key = node type name +# value = desired depth for any node having the node type as primary type. +# +# - Use 'default' to set the default depth +# - Depth may be any int >= -1. +# - Depth -1 indicates infinite depth. + +default=5 +nt\:file=-1 \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/WEB-INF/config.xml b/jackrabbit-webapp/src/main/webapp/WEB-INF/config.xml new file mode 100644 index 00000000000..5a526f26f02 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/WEB-INF/config.xml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nt:file + nt:resource + + + + + + + + + + + + + rep + jcr + + + + + + diff --git a/jackrabbit-webapp/src/main/webapp/WEB-INF/protectedHandlers.properties b/jackrabbit-webapp/src/main/webapp/WEB-INF/protectedHandlers.properties new file mode 100644 index 00000000000..f51e6563dd8 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/WEB-INF/protectedHandlers.properties @@ -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. + +# ProtectedItemRemoveHandler implementation class +javax.jcr.tck.access.control.list.handler=org.apache.jackrabbit.server.remoting.davex.AclRemoveHandler diff --git a/jackrabbit-webapp/src/main/webapp/WEB-INF/templates/bootstrap.properties b/jackrabbit-webapp/src/main/webapp/WEB-INF/templates/bootstrap.properties new file mode 100644 index 00000000000..ca373785f89 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/WEB-INF/templates/bootstrap.properties @@ -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. + +# This is the template file for the 'bootstrap.properties' that will +# be placed in the repository home directory (or whatever is specified +# in the "bootstrap-config" init parameter. + +# Repository configuration settings (will be adjusted by installer) +repository.config=jackrabbit/repository/repository.xml +repository.home=jackrabbit/repository +repository.name=jackrabbit.repository + +# RMI Settings +rmi.enabled=true +rmi.port=0 +rmi.host=localhost +# If the URI is not specified, it's composed as follows: +#rmi.uri=//${rmi.host}:${rmi.port}/${repository.name} + +# JNDI Settings +# all properties starting with 'java.naming.' will go into the +# environment of the initial context +jndi.enabled=true +# if the name is not specified, it's initialized with the repository.name +#jndi.name=${repository.name} +java.naming.provider.url=http://www.apache.org/jackrabbit +java.naming.factory.initial=org.apache.jackrabbit.core.jndi.provider.DummyInitialContextFactory diff --git a/jackrabbit-webapp/src/main/webapp/WEB-INF/web.xml b/jackrabbit-webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..bfcf019d546 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,407 @@ + + + + + + Jackrabbit JCR Server + + + + + + org.apache.jackrabbit.j2ee.DerbyShutdown + + + + + + + + RepositoryStartup + + Repository servlet that starts the repository and registers it to JNDI ans RMI. + If you already have the repository registered in this appservers JNDI context, + or if its accessible via RMI, you do not need to use this servlet. + + org.apache.jackrabbit.j2ee.RepositoryStartupServlet + + + bootstrap-config + jackrabbit/bootstrap.properties + + Property file that hold the same initialization properties than + the init-params below. If a parameter is specified in both + places the one in the bootstrap-config wins. + + + + + + + + + + 2 + + + + + + + + Repository + + This servlet provides other servlets and jsps a common way to access + the repository. The repository can be accessed via JNDI, RMI or Webdav. + + org.apache.jackrabbit.j2ee.RepositoryAccessServlet + + + bootstrap-config + jackrabbit/bootstrap.properties + + Property file that hold the same initialization properties than + the init-params below. If a parameter is specified in both + places the one in the bootstrap-config wins. + + + + + + + + + + 3 + + + + + + + Webdav + + The webdav servlet that connects HTTP request to the repository. + + org.apache.jackrabbit.j2ee.SimpleWebdavServlet + + + resource-path-prefix + /repository + + defines the prefix for spooling resources out of the repository. + + + + + + + + resource-config + /WEB-INF/config.xml + + Defines various dav-resource configuration parameters. + + + + + 4 + + + + + + + JCRWebdavServer + + The servlet used to remote JCR calls over HTTP. + + org.apache.jackrabbit.j2ee.JcrRemotingServlet + + missing-auth-mapping + + + Defines how a missing authorization header should be handled. + 1) If this init-param is missing, a 401 response is generated. + This is suitable for clients (eg. webdav clients) for which + sending a proper authorization header is not possible if the + server never sent a 401. + 2) If this init-param is present with an empty value, + null-credentials are returned, thus forcing an null login + on the repository. + 3) If this init-param is present with the value 'guestcredentials' + java.jcr.GuestCredentials are used to login to the repository. + 4) If this init-param has a 'user:password' value, the respective + simple credentials are generated. + + + + + + resource-path-prefix + /server + + defines the prefix for spooling resources out of the repository. + + + + + + batchread-config + /WEB-INF/batchread.properties + JcrRemotingServlet: Optional mapping from node type names to default depth. + + + protectedhandlers-config + /WEB-INF/protectedHandlersConfig.xml + JcrRemotingServlet: Handlers for removing protected items. + + + + 5 + + + + + + + RMI + org.apache.jackrabbit.servlet.remote.RemoteBindingServlet + + + + + + + RepositoryStartup + /admin/* + + + Webdav + /repository/* + + + JCRWebdavServer + /server/* + + + RMI + /rmi + + + + + + + index.jsp + + + + org.apache.jackrabbit.j2ee.JcrApiNotFoundException + /error/classpath.jsp + + + javax.jcr.RepositoryException + /error/repository.jsp + + + diff --git a/jackrabbit-webapp/src/main/webapp/about.jsp b/jackrabbit-webapp/src/main/webapp/about.jsp new file mode 100644 index 00000000000..1fe9b9c6bf0 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/about.jsp @@ -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. +--%><%@page import="java.io.InputStream, + java.io.InputStreamReader, + java.io.Reader"%><%! + + /** + * Escapes and outputs the contents of a given (UTF-8) text resource. + * TODO: There should be an easier way to do this! + * + * @param path path of the resource to output + * @param out the JSP output writer + * @throws Exception if something goes wrong + */ + private void output(String path, JspWriter out) throws Exception { + InputStream input = getServletContext().getResourceAsStream(path); + try { + Reader reader = new InputStreamReader(input, "UTF-8"); + for (int ch = reader.read(); ch != -1; ch = reader.read()) { + if (ch == '<') { + out.write("<"); + } else if (ch == '>') { + out.write(">"); + } else if (ch == '&') { + out.write("&"); + } else { + out.write((char) ch); + } + } + } finally { + input.close(); + } + } + +%><% request.setAttribute("title", "About Apache Jackrabbit"); +%> +

        + Apache Jackrabbit is a fully + conforming implementation of the Content Repository for Java Technology API + (JCR). A content repository is a hierarchical content store with support for + structured and unstructured content, full text search, versioning, + transactions, observation, and more. Typical applications that use content + repositories include content management, document management, and records + management systems. +

        +

        + Version 1.0 of the JCR API was specified by the + Java Specification Request 170 + (JSR 170) and version 2.0 by the + Java Specification Request 283. +

        +

        + Apache Jackrabbit is a project of the + Apache Software Foundation. +

        +

        Copyright Notice

        +
        <% output("/META-INF/NOTICE", out); %>
        +

        License Information

        +
        <% output("/META-INF/LICENSE", out); %>
        + diff --git a/jackrabbit-webapp/src/main/webapp/bootstrap/error.jsp b/jackrabbit-webapp/src/main/webapp/bootstrap/error.jsp new file mode 100644 index 00000000000..5d42829567b --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/bootstrap/error.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Content Repository Error"); +%> +

        + Some error occurred during setup. See the log files for details. +

        +

        back

        + diff --git a/jackrabbit-webapp/src/main/webapp/bootstrap/exists.jsp b/jackrabbit-webapp/src/main/webapp/bootstrap/exists.jsp new file mode 100644 index 00000000000..e191282e641 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/bootstrap/exists.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Content Repository Exists"); +%> +

        The repository home directory or configuration already exists.

        +

        +You have chosen to create a new repository but the specified home +directory or the configuration file already exist. +

        +

        +Please specify a correct location or choose to reuse an existing repository. +

        +

        back

        + diff --git a/jackrabbit-webapp/src/main/webapp/bootstrap/missing.jsp b/jackrabbit-webapp/src/main/webapp/bootstrap/missing.jsp new file mode 100644 index 00000000000..068974eb0d0 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/bootstrap/missing.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Content Repository Setup"); +%> +

        + Your content repository is not properly configured yet. Please use + the forms below to setup the content repository. +

        +

        + Alternatively, you can directly modify the settings in the + WEB-INF/web.xml deployment descriptor and redeploy this + web application. +

        + +

        Create a new content repository

        +
        + +

        + Use this form to create a new content repository in the given directory. + The directory is created by this web application and should not already + exist. The repository is created using a default configuration file. +

        +

        + +

        +

        + Repository type: + + +

        +

        +
        + +

        Use an existing content repository

        +
        + +

        + Use this form to access an existing content repository in the given + directory. The repository configuration file should be available as + repository.xml within the given directory. +

        +

        + Note that the repository can not be concurrently accessed by multiple + applications. You must use WebDAV or RMI through this web application + if you want to access the repository remotely. Other web applications + running in the same servlet container can access the repository locally + using JNDI. +

        +

        + +

        +

        +
        + + diff --git a/jackrabbit-webapp/src/main/webapp/bootstrap/notexists.jsp b/jackrabbit-webapp/src/main/webapp/bootstrap/notexists.jsp new file mode 100644 index 00000000000..1324078b69b --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/bootstrap/notexists.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Content Repository Not Found"); +%> +

        The repository home directory or configuration do not exists.

        +

        +You have chosen to reuse an existing repository but the specified home +directory or the configuration file do not exist. +

        +

        +Please specify a correct location or choose to create a new repository. +

        +

        back

        + diff --git a/jackrabbit-webapp/src/main/webapp/bootstrap/reconfigure.jsp b/jackrabbit-webapp/src/main/webapp/bootstrap/reconfigure.jsp new file mode 100644 index 00000000000..d02fa7069c3 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/bootstrap/reconfigure.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Content Repository Already Running"); +%> +

        Your repository is already properly configured an running.

        +

        +Your changes were discarded. To reconfigure or reinstall the repository modify +the respective configuration files or remove them. +

        +

        home

        + diff --git a/jackrabbit-webapp/src/main/webapp/bootstrap/running.jsp b/jackrabbit-webapp/src/main/webapp/bootstrap/running.jsp new file mode 100644 index 00000000000..c6bcfb12dfd --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/bootstrap/running.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Content Repository Ready"); +%> +

        Your repository is properly configured an running.

        +

        +To reconfigure or reinstall the repository modify the respective configuration +files or remove them. +

        +

        home

        + diff --git a/jackrabbit-webapp/src/main/webapp/bootstrap/success.jsp b/jackrabbit-webapp/src/main/webapp/bootstrap/success.jsp new file mode 100644 index 00000000000..59c11208cc3 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/bootstrap/success.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Content Repository Ready"); +%> +

        Your repository is now properly configured an running.

        +

        +To reconfigure or reinstall the repository modify the respective configuration +files or remove them. +

        +

        home

        + diff --git a/jackrabbit-webapp/src/main/webapp/css/default.css b/jackrabbit-webapp/src/main/webapp/css/default.css new file mode 100644 index 00000000000..7d2ab802ce3 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/css/default.css @@ -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. + */ + +body { font-family: Verdana, Helvetica, Arial, sans-serif; + font-size: small; color: #000000; + background-color: #e0e0e0; + margin: 1em 100px 1em 100px; + padding: 0; } + +#page { background-color: white; } + +a:link { color: #667C00; } +a:visited { color: #7C7C7C; } + +img { border: 0; } + +/* BANNER */ + +#banner { padding: 0 0 113px 0; + border-top: 6px solid black; + border-bottom: 2px solid black; } +#banner p { margin: 0; } +#jcr { float: left; } +#asf { float: right; } +#banner img { margin: 0; + padding: 10px 0 0 0; } +#banner a { text-decoration: none; } + +/* NAVIGATION */ + +#navigation { padding: 18px 0; + width: 180px; + float: left; + font-size: x-small; } + +#navigation ul + { margin: 0; + padding: 1ex 0 1ex 1em; + list-style: none; + font-weight: bold; } + +#navigation ul ul + { font-weight: normal; } + +#navigation a + { text-decoration: none; } + +a.external { font-style: italic; } + +/* CONTENT */ + +#content { margin: 1em 1em 1em 180px; } + +#content p { line-height: 1.3em; } + +h1, h2, h3 { color: #869900; } + +pre { background: #E0E0E0; + padding: 1em; + border: 1px dotted black; + overflow: auto; } + +td { font-size: small; } + +pre.code { font-family: monospace; font-size: small; + padding: 25px; } + +/* FOOTER */ + +#footer { clear: both; + border-top: 1px solid #999; + font-size: x-small; } + +#footer p { margin: 0; + text-align: center; } + +#footer a { text-decoration: none; } diff --git a/jackrabbit-webapp/src/main/webapp/error/classpath.jsp b/jackrabbit-webapp/src/main/webapp/error/classpath.jsp new file mode 100644 index 00000000000..56a3a70cc7c --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/error/classpath.jsp @@ -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. +--%><% +request.setAttribute("title", "JCR API Not Found"); +%> +

        +The javax.jcr.Repository interface from the JCR API could not +be loaded. +

        +

        +To resolve this issue, you need to make the jcr-2.0.jar file +available in the shared classpath of the servlet container. The file is +available for download from the +JSR 283 web page. +

        + diff --git a/jackrabbit-webapp/src/main/webapp/error/repository.jsp b/jackrabbit-webapp/src/main/webapp/error/repository.jsp new file mode 100644 index 00000000000..0af01c4edde --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/error/repository.jsp @@ -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. +--%><%@ page isErrorPage="true" + import="org.apache.jackrabbit.util.Text, + java.io.StringWriter, + java.io.PrintWriter"%><% +request.setAttribute("title", "Repository Error"); +%> +

        + The content repository operation failed with the following + <%= exception.getClass().getSimpleName() %> error: +

        +
        <%= Text.encodeIllegalXMLCharacters(exception.getMessage()) %>
        +

        + See the + troubleshooting page + for ideas on how to resolve this issue. +

        + +

        Exception stack trace

        +

        + Below is the full exception stack trace associated with this error: +

        +<% +StringWriter buffer = new StringWriter(); +exception.printStackTrace(new PrintWriter(buffer)); +%> +
        <%= Text.encodeIllegalXMLCharacters(buffer.toString()) %>
        + diff --git a/jackrabbit-webapp/src/main/webapp/footer.jsp b/jackrabbit-webapp/src/main/webapp/footer.jsp new file mode 100644 index 00000000000..dc549f39d7b --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/footer.jsp @@ -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. +--%> +
        + + + + diff --git a/jackrabbit-webapp/src/main/webapp/header.jsp b/jackrabbit-webapp/src/main/webapp/header.jsp new file mode 100644 index 00000000000..bc4ce1d891b --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/header.jsp @@ -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. +--%> +<%@page import="org.apache.jackrabbit.util.Text"%> +<% +String title = + Text.encodeIllegalXMLCharacters(request.getAttribute("title").toString()); +String context = + Text.encodeIllegalXMLCharacters(request.getContextPath()); +%> + + + + <%= title %> + + + + +
        + + +
        +

        <%= title %>

        diff --git a/jackrabbit-webapp/src/main/webapp/images/0.gif b/jackrabbit-webapp/src/main/webapp/images/0.gif new file mode 100644 index 00000000000..decd155e3f7 Binary files /dev/null and b/jackrabbit-webapp/src/main/webapp/images/0.gif differ diff --git a/jackrabbit-webapp/src/main/webapp/images/asf-logo.gif b/jackrabbit-webapp/src/main/webapp/images/asf-logo.gif new file mode 100644 index 00000000000..909b2a7ab96 Binary files /dev/null and b/jackrabbit-webapp/src/main/webapp/images/asf-logo.gif differ diff --git a/jackrabbit-webapp/src/main/webapp/images/asf-logo.png b/jackrabbit-webapp/src/main/webapp/images/asf-logo.png new file mode 100644 index 00000000000..9bb1fdc4c57 Binary files /dev/null and b/jackrabbit-webapp/src/main/webapp/images/asf-logo.png differ diff --git a/jackrabbit-webapp/src/main/webapp/images/favicon.ico b/jackrabbit-webapp/src/main/webapp/images/favicon.ico new file mode 100644 index 00000000000..8bd07c47749 Binary files /dev/null and b/jackrabbit-webapp/src/main/webapp/images/favicon.ico differ diff --git a/jackrabbit-webapp/src/main/webapp/images/jackrabbit.png b/jackrabbit-webapp/src/main/webapp/images/jackrabbit.png new file mode 100644 index 00000000000..d64b90a2ab1 Binary files /dev/null and b/jackrabbit-webapp/src/main/webapp/images/jackrabbit.png differ diff --git a/jackrabbit-webapp/src/main/webapp/images/jackrabbitlogo.gif b/jackrabbit-webapp/src/main/webapp/images/jackrabbitlogo.gif new file mode 100644 index 00000000000..d18e485e40f Binary files /dev/null and b/jackrabbit-webapp/src/main/webapp/images/jackrabbitlogo.gif differ diff --git a/jackrabbit-webapp/src/main/webapp/images/jlogo.gif b/jackrabbit-webapp/src/main/webapp/images/jlogo.gif new file mode 100644 index 00000000000..9c67b032201 Binary files /dev/null and b/jackrabbit-webapp/src/main/webapp/images/jlogo.gif differ diff --git a/jackrabbit-webapp/src/main/webapp/index.jsp b/jackrabbit-webapp/src/main/webapp/index.jsp new file mode 100644 index 00000000000..8ee94a27cfb --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/index.jsp @@ -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. +--%><% +try { + Class.forName("javax.jcr.Repository"); +} catch (ClassNotFoundException e) { + throw new org.apache.jackrabbit.j2ee.JcrApiNotFoundException(e); +} +%> \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/local.jsp b/jackrabbit-webapp/src/main/webapp/local.jsp new file mode 100644 index 00000000000..4605e046b37 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/local.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Local Repository Access"); +%> +

        + The content repository within this web application can be accessed + locally by other web applications within the same servlet container. + Local access is much faster than remote access. +

        +

        + The content repository is made available both through JNDI and the + web application context. +

        + +

        Accessing the repository through JNDI

        +

        + By default the repository is only made available in a dummy JNDI directory + local to this web application. However, you can make the repository globally + available if your servlet container allows a web application to modify the + global JNDI directory or you are using some other JNDI directory that can + manage unserializable Java objects. +

        +

        + To bind the the repository to such a JNDI directory, you need to modify + the java.naming parameters in either the /WEB-INF/web.xml + deployment descriptor or the jackrabbit/bootstrap.properties file. You need + to redeploy this web application to activate the changes. +

        +

        + Use the following code to access a repository bound in a JNDI directory: +

        +
        +import javax.jcr.Repository;
        +import javax.naming.Context;
        +import javax.naming.InitialContext;
        +
        +Context context = new InitialContext(...);
        +Repository repository = (Repository) context.lookup(...);
        +
        + +

        Accessing the repository through servlet context

        +

        + This web application makes the repository available as the + javax.jcr.Repository attribute in the application context. + If your servlet container supports cross-context access, you can + access the repository directly using that attribute. +

        +

        + For example in Apache Tomcat + you can enable cross-context access by setting the crossContext + attribute to true in the <Context/> configuration. +

        +

        + Use the following code to access a repository through the servlet context: +

        +
        +import javax.jcr.Repository;
        +import javax.servlet.ServletContext;
        +
        +ServletContext context = ...; // context of your servlet
        +ServletContext jackrabbit =
        +    context.getContext("<%= Text.encodeIllegalXMLCharacters(request.getContextPath()) %>");
        +Repository repository = (Repository)
        +    context.getAttribute(Repository.class.getName()).
        +
        + +

        Using the jackrabbit-jcr-servlet component

        +

        + The jackrabbit-jcr-servlet component contains utility classes + for use within JCR web applications. With that component you can hide + both the above and the remote access options + from your code, and use just the following to access a repository: +

        +
        +import javax.jcr.Repository;
        +import org.apache.jackrabbit.servlet.ServletRepository;
        +
        +public class MyServlet extends HttpServlet {
        +
        +    private final Repository repository = new ServletRepository(this);
        +
        +    // ...
        +
        +}
        +
        +

        + See the jackrabbit-jcr-servlet documentation for more details. +

        + diff --git a/jackrabbit-webapp/src/main/webapp/remote.jsp b/jackrabbit-webapp/src/main/webapp/remote.jsp new file mode 100644 index 00000000000..1eedcfb1f51 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remote.jsp @@ -0,0 +1,106 @@ +<%@ page import="java.net.URI"%><%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Remote Repository Access"); + +URI uri = new URI(request.getRequestURL().toString()); +String base = + uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + + request.getContextPath(); +base = Text.encodeIllegalXMLCharacters(base); +%> +

        + The content repository within this web application is made available + to remote clients through + RMI + and the jackrabbit-jcr-rmi component. +

        +

        + The remote repository stub is available both in the RMI registry + (one is started automatically by this web application if not already running) + and as a direct HTTP download. The default URLs for accessing the remote + repository are: +

        +
          +
        • RMI registry: //localhost/jackrabbit.repository
        • +
        • HTTP download: <%= base %>/rmi
        • +
        +

        + Note that the above URLs are the defaults. You can disable or change them + by modifying the /WEB-INF/web.xml deployment descriptor. +

        + +

        Accessing the remote repository

        +

        + To access the remote content repository you need to use the + jackrabbit-jcr-rmi component in your application. If you use + Maven 2, you can declare the JCR and jackrabbit-jcr-rmi dependencies + like this: +

        +
        <dependency>
        +  <groupId>javax.jcr</groupId>
        +  <artifactId>jcr</artifactId>
        +  <version>1.0</version>
        +</dependency>
        +<dependency>
        +  <groupId>org.apache.jackrabbit</groupId>
        +  <artifactId>jackrabbit-jcr-rmi</artifactId>
        +  <version>1.4</version>
        +</dependency>
        +
        +

        + With that dependency in place, you can use either the RMI registry or + the direct HTTP download to access the repository. +

        +

        + The required code for accessing the repository using the RMI registry is: +

        +
        +import javax.jcr.Repository;
        +import org.apache.jackrabbit.rmi.repository.RMIRemoteRepository;
        +
        +Repository repository =
        +    new RMIRemoteRepository("//localhost/jackrabbit.repository");
        +
        +

        + The required code for accessing the repository using the RMI registry is: +

        +
        +import javax.jcr.Repository;
        +import org.apache.jackrabbit.rmi.repository.URLRemoteRepository;
        +
        +Repository repository =
        +    new URLRemoteRepository("<%= base %>/rmi");
        +
        +

        + See the JCR specification + and the + Repository + javadoc for details on what to do with the acquired Repository instance. +

        + +

        Remote access performance

        +

        + Note that the design goal of the current jackrabbit-jcr-rmi component + is correct and complete functionality instead of performance, so you should + not rely on remote access for performance-critical applications. +

        +

        + You may want to look at the Jackrabbit clustering feature for best + performance for concurrently accessing the repository on multiple separate + servers. +

        + diff --git a/jackrabbit-webapp/src/main/webapp/remoting/footer.jsp b/jackrabbit-webapp/src/main/webapp/remoting/footer.jsp new file mode 100644 index 00000000000..0412f344b35 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/footer.jsp @@ -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. +--%> + +
        + + diff --git a/jackrabbit-webapp/src/main/webapp/remoting/header.jsp b/jackrabbit-webapp/src/main/webapp/remoting/header.jsp new file mode 100644 index 00000000000..e4ece5bc49c --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/header.jsp @@ -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. +--%> +<%@page import="org.apache.jackrabbit.util.Text"%> +<% +String context = Text.encodeIllegalXMLCharacters(request.getContextPath()); +%> + + + + JCR Remoting Server + + + + +
        + + diff --git a/jackrabbit-webapp/src/main/webapp/remoting/index.jsp b/jackrabbit-webapp/src/main/webapp/remoting/index.jsp new file mode 100644 index 00000000000..582287c08f9 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/index.jsp @@ -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. +--%><% +%> +
        +

        JCR Remoting Server - Introduction

        +

        Ths section shortly overviews the batch read/write extensions + added to the JCR remoting feature. +

        +

        +

        Some principals are demonstrated in the corresponding example section.

        +
        + \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/remoting/json.js b/jackrabbit-webapp/src/main/webapp/remoting/json.js new file mode 100644 index 00000000000..24830ce823d --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/json.js @@ -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. +*/ + +function getXMLHttpRequest(url, method, headers, params) { + var xmlhttp = null; + if (window.XMLHttpRequest) { + // code for all new browsers + xmlhttp = new XMLHttpRequest(); + } else if (window.ActiveXObject) { + // code for IE + try { + xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + } + if (xmlhttp) { + if (!method) { + method = "GET"; + } + xmlhttp.open(method, url, false); + if (headers) { + for (var hdr in headers) { + xmlhttp.setRequestHeader(hdr, headers[hdr]); + } + } + xmlhttp.send(params); + return xmlhttp; + } else { + alert("Your browser does not support XMLHTTP."); + return null; + } +} + +var JsonFormatter = null; +(function() { + + JsonFormatter = new Object(); + JsonFormatter.clear = false; + + JsonFormatter.tree = function(jsonObj, baseHref) { + if (!jsonObj) { + return ""; + } + var indentionLevel = 0; + return JsonFormatter.objectTree("", jsonObj, indentionLevel, baseHref); + } + + JsonFormatter.format = function(jsonObj, clearSpecial) { + if (!jsonObj) { + return ""; + } + var indentionLevel = 0; + clear = clearSpecial; + return JsonFormatter.object(jsonObj, indentionLevel); + } + + JsonFormatter.addLineBreak = function(str) { + return str += "
        "; + } + + JsonFormatter.addIndention = function(str, indention, indStr) { + for (var i = 0; i < indention; i++) { + str += indStr; + } + return str; + } + + JsonFormatter.object = function(value, indentionLevel) { + if (value instanceof Array) { + return JsonFormatter.array(value, indentionLevel); + } + + var str = "{"; + str = JsonFormatter.addLineBreak(str); + indentionLevel++; + var delim = false; + + for (var i in value) { + var v = value[i]; + if (clear && i.charAt(0) == ':') { + // skip special prop. + // TODO: evaluate and add to display info. + } else { + var fnctn = JsonFormatter[typeof v]; + if (fnctn) { + v = fnctn(v, indentionLevel); + if (typeof v == 'string') { + if (delim) { + str += ","; + str = JsonFormatter.addLineBreak(str); + } + str = JsonFormatter.addIndention(str, indentionLevel, "\t"); + str += JsonFormatter.string(i) + ' : ' + v; + delim = true; + } + } + } + } + indentionLevel--; + str = JsonFormatter.addLineBreak(str); + str = JsonFormatter.addIndention(str, indentionLevel, "\t"); + str += "}"; + return str; + } + + JsonFormatter.array = function(value, indentionLevel) { + var str = "["; + var delim = false; + for (var i in value) { + var arrVal = value[i]; + var fnctn = JsonFormatter[typeof arrVal]; + if (fnctn) { + arrVal = fnctn(arrVal); + if (delim) { + str += ", "; + } + str += arrVal; + delim = true; + } + } + str += "]"; + return str; + } + + JsonFormatter.boolean = function(value, indentionLevel) { + return String(value); + } + + JsonFormatter.string = function(value, indentionLevel) { + return '"' + value + '"'; + + } + + JsonFormatter.number = function(value, indentionLevel) { + return String(value); + } + + JsonFormatter.extractPropertyType = function(key, value) { + if (key == "::NodeIteratorSize") { + return null; + } else if (key.charAt(0) == ':' && typeof value == 'string') { + return value; + } else { + return null; + } + } + + JsonFormatter.buildKey = function(key, propType, href) { + var keyStr = key; + if (propType) { + var href = "javascript:alert('PropertyType = " + propType + "');"; + keyStr = "" + key + ""; + } else if (key.charAt(0) == ':') { + // binary + var propname = key.substring(1, key.length); + var binHref = href + "/" + propname; + keyStr = "" + propname + ""; + } + return keyStr; + } + + JsonFormatter.objectTree = function(key, value, indentionLevel, href) { + var str = "+ " + key; // + node-name + if (href && href.charAt(href.length - 1) == '/') { + href += key; + } else { + href += "/" + key; + } + + indentionLevel++; + var propType; + var childSize; + var delim = false; + + for (var i in value) { + var v = value[i]; + var pt = JsonFormatter.extractPropertyType(i, v); + if (pt) { + propType = pt; + continue; + } else if (i == "::NodeIteratorSize") { + continue; + } + str = JsonFormatter.addLineBreak(str); + str = JsonFormatter.addIndention(str, indentionLevel, "  "); + if (v instanceof Array) { + // value array - propname + var key = JsonFormatter.buildKey(i, propType, href); + propType = null; + str += "- " + key + ' = ' + JsonFormatter.array(v, indentionLevel); + } else if (v instanceof Object) { + str += JsonFormatter.objectTree(i, v, indentionLevel, href); + } else { + // simple value - propname + var fnctn = JsonFormatter[typeof v]; + if (fnctn) { + v = fnctn(v, indentionLevel); + var key = JsonFormatter.buildKey(i, propType, href); + propType = null; + str += "- " + key + ' = ' + v; + } + } + } + indentionLevel--; + return str; + } +})(); diff --git a/jackrabbit-webapp/src/main/webapp/remoting/read.jsp b/jackrabbit-webapp/src/main/webapp/remoting/read.jsp new file mode 100644 index 00000000000..911afbc6fad --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/read.jsp @@ -0,0 +1,65 @@ +<%@ page import="java.net.URI" %> +<%@ page import="org.apache.jackrabbit.j2ee.JcrRemotingServlet" %> +<%@ page import="org.apache.jackrabbit.util.Text" %> +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><% + +URI uri = new URI(request.getRequestURL().toString()); +String href = + uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + + request.getContextPath() + + JcrRemotingServlet.getPathPrefix(pageContext.getServletContext()); +href = Text.encodeIllegalXMLCharacters(href); +href += "/default/jcr:root"; + +%> +
        +

        Read

        +

        Default Reading

        +

        Reading remotely from the repository generally follows the rules described in + JCR_Webdav_Protocol.zip. +

        +

        Batch Read

        +

        Batch read is triggered by adding a '.json' extension to the resource + href. Optionally the client may explicitely specify the desired batch + read depth by appending '.depth.json' extension. If no json extension + is present the GET request is processed by applied the default + remoting rules. +

        +

        The response to a batch read request contains a plain text representing + a JSON object. Its member either represent nodes or properties. +

          +
        • The name element of the Item path is added as key
        • +
        • The value of a Node entry is a JSON object.
        • +
        • The value of a Property entry is either a JSON array or a simple JSON value.
        • +
        +

        +

        In order to cope with property types that cannot be expressed with JSON + a couple of special rules are defined: +

          +
        • Binary properties: The key gets a leading ":", the value represents the + length of the property. In order to retrieve the binary value, the + client must follow the default rules (see above).
        • +
        • Date, Name, Path and Reference properties: The type information is passed with a separate JSON pair.
        • +
        • The value of a Property entry is either a JSON array or a simple JSON value.
        • +
        +

        + See Example: Batch Write for a demostration of + the batch read functionality. +

        +
        + \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/remoting/read_batch.jsp b/jackrabbit-webapp/src/main/webapp/remoting/read_batch.jsp new file mode 100644 index 00000000000..b1b83dffef6 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/read_batch.jsp @@ -0,0 +1,104 @@ +<%@ page import="java.net.URI" %> +<%@ page import="org.apache.jackrabbit.j2ee.JcrRemotingServlet" %> +<%@ page import="org.apache.jackrabbit.util.Text" %> +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><% + +URI uri = new URI(request.getRequestURL().toString()); +String href = + uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + + request.getContextPath() + + JcrRemotingServlet.getPathPrefix(pageContext.getServletContext()); +href = Text.encodeIllegalXMLCharacters(href); +href += "/default/jcr:root"; + +%> + + +
        +

        Examples: Batch Read

        +

        + Enter the path of an existing node and the desired depth. +

        + + + + + + + + + + + + + + +
        Node Path
        Depth
        Result type
        +

        +

        
        +
        + \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/remoting/write.jsp b/jackrabbit-webapp/src/main/webapp/remoting/write.jsp new file mode 100644 index 00000000000..36c04e7b8af --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/write.jsp @@ -0,0 +1,161 @@ +<%@ page import="java.net.URI" %> +<%@ page import="org.apache.jackrabbit.j2ee.JcrRemotingServlet" %> +<%@ page import="org.apache.jackrabbit.util.Text" %> +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><% + +URI uri = new URI(request.getRequestURL().toString()); +String href = + uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + + request.getContextPath() + + JcrRemotingServlet.getPathPrefix(pageContext.getServletContext()); +href = Text.encodeIllegalXMLCharacters(href); +href += "/default/jcr:root"; + +%> +
        +

        Write

        +

        Default Writing

        +

        Writing remotely to the repository generally follows the rules described in + JCR_Webdav_Protocol.zip. +

        +

        Batch Write

        +

        A set of transient modifications can in addition be sent by using the + extended batch write: A single POST request that contains a custom + :diff parameter describing the changes to be applied. + The expected format is described in the + JavaDoc. +

        + Some cases however can be easily demonstrated. The following examples can + be tested with the form provided at + Example: Batch Write. +

        +

        Examples

        +

        The following examples illustrate the basics of the diff format. It does + not cover the special treatment of properties with type Date, + Name, Path, Reference and Binary (see below).

        +

        Set properties

        +
        +^prop1  : "stringvalue"
        +^prop1  : "changedvalue"
        +^prop2  : true
        +^prop3  : 100.010
        +^prop4  : 1234567890
        +^prop5  : ["multi","valued","string prop"]
        +^.      : "change existing property at path."
        +^/abs/path/to/the/new/prop : "some value."
        +

        Add new nodes (optionally including child items)

        +
        ++node   : {"title" : "title property of the new node"}
        ++node2  : {"childN" : {}, "childN2" : {}}
        ++/abs/path/to/the/new/node : {"text" : "some text"}
        +

        Move or rename nodes

        +
        +>node   : rename
        +>rename : /moved/to/another/destination
        +

        Reorder nodes

        +
        +>childN : childN2#after
        +>childN : #first
        +>childN : #last
        +>childN : childN2#before
        +

        Remove items

        +
        +-prop4  :
        +-node2  :
        +-/moved/to/another/destination :
        +

        Dealing with Special Property Types

        +

        Property types that can not be covered unambigously, need some special + handling (see JavaDoc). This affects JCR properties being of type +

          +
        • Date,
        • +
        • Name,
        • +
        • Path,
        • +
        • Reference,
        • +
        • Binary.
        • +
        + In order to set properties of any of the types listed, the value part in the + :diff param must be left empty and a separate request parameter must be + included. Its name equals the corresponding key in the :diff, its value represents + the property value. In addition the desired property type must be specified + using the conversion defined with + JcrValueType#contentTypeFromType(int). +

        +

        Set a Date property

        +
        +POST /jackrabbit/server/default/jcr%3aroot/testNode HTTP/1.1
        +Content-Type: multipart/form-data; boundary=kTmAb2lkjCtxbMVFzHEplAJjHCUo5aQndaUu
        +
        +--kTmAb2lkjCtxbMVFzHEplAJjHCUo5aQndaUu
        +Content-Disposition: form-data; name="dateProp"
        +Content-Type: jcr-value/date
        +
        +2009-02-12T10:19:40.778+01:00         
        +--kTmAb2lkjCtxbMVFzHEplAJjHCUo5aQndaUu
        +Content-Disposition: form-data; name=":diff"
        +Content-Type: text/plain
        +
        +^dateProp :  
        +--kTmAb2lkjCtxbMVFzHEplAJjHCUo5aQndaUu--
        +    
        +

        Setting Binary, Name, Path or Reference + properties works accordingly. +

        + +

        Direct Content Editing

        +

        The functionality present with batch reading also enables very simplified + content editing using common HTML forms.

        +

        The :diff parameter is omitted altogether and each request parameter is + treated as property +

          +
        • param name : property name
        • +
        • param value : property value
        • +
        + whereas the form action indicates the path of the parent node. +

        +

        If no node exists at the specified path an attempt is made to create the + missing intermediate nodes. The primary node type of the new node is + either retrieved from the corresponding jcr:primaryType param or + automatically determined by the implementation.

        +

        Setting a property can be tested at + Example: Simplified Writing +

        +

        Examples

        +

        The following examples illustrate the simplified writing.

        +

        Set string property +

          +
        • Existing or non-existing node at /testnode
        • +
        • Set property 'propName' with value "any string value"
        • +
        +

        +
        +<form method="POST" action="<%= href %>/testnode">
        +    <input type="text" name="propName" value="any string value"/>
        +</form>
        +

        Add node with a defined node type and set a property

        +
          +
        • Non-existing node at /testnode/nonexisting
        • +
        • Define its primary type to be "nt:unstructured"
        • +
        • Set property 'propName' with value "any string value"
        • +
        +
        +<form method="POST" action="<%= href %>/nonexisting">
        +    <input type="text" name="jcr:primaryType" value="nt:unstructured"/>
        +    <input type="text" name="propName" value="any string value"/>
        +</form>
        +
        + \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/remoting/write_batch.jsp b/jackrabbit-webapp/src/main/webapp/remoting/write_batch.jsp new file mode 100644 index 00000000000..ee0c8348336 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/write_batch.jsp @@ -0,0 +1,84 @@ +<%@ page import="java.net.URI" %> +<%@ page import="org.apache.jackrabbit.j2ee.JcrRemotingServlet" %> +<%@ page import="org.apache.jackrabbit.util.Text" %> +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><% + +URI uri = new URI(request.getRequestURL().toString()); +String href = + uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + + request.getContextPath() + + JcrRemotingServlet.getPathPrefix(pageContext.getServletContext()); +href = Text.encodeIllegalXMLCharacters(href); +href += "/default/jcr:root"; + +%> + + +
        +

        Examples: Batch Write

        +

        + Enter the path of an existing node or property (depending on the desired + actions) and enter the :diff value. +

        +

        See the introduction to batched writing + for examples. +

        + + + + + + + + + + +
        Item Path
        Diff
        +

        +

        
        +
        + \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/remoting/write_simple.jsp b/jackrabbit-webapp/src/main/webapp/remoting/write_simple.jsp new file mode 100644 index 00000000000..f0a8f1f2978 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/remoting/write_simple.jsp @@ -0,0 +1,101 @@ +<%@ page import="java.net.URI" %> +<%@ page import="org.apache.jackrabbit.j2ee.JcrRemotingServlet" %> +<%@ page import="org.apache.jackrabbit.util.Text" %> +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><% + +URI uri = new URI(request.getRequestURL().toString()); +String href = + uri.getScheme() + "://" + uri.getHost() + ":" + uri.getPort() + + request.getContextPath() + + JcrRemotingServlet.getPathPrefix(pageContext.getServletContext()); +href = Text.encodeIllegalXMLCharacters(href); +href += "/default/jcr:root"; + +%> + + +
        +

        Examples: Simplified Writing

        +

        If the JCR node at the specified absolute path allows to set a properties + with name title or text, submitting the form below will + will set those properties to the given values.

        +

        If no JCR node exists at the specified absolute path, the missing + intermediate nodes will be created if an applicable node type for the + specified node name(s) can be determined.

        + + + + + + + + + + + + + + +
        Node Path
        Title
        Text
        +

        +

        
        +
        + \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/search.jsp b/jackrabbit-webapp/src/main/webapp/search.jsp new file mode 100644 index 00000000000..65739a71e59 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/search.jsp @@ -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. +--%><%@ page import="javax.jcr.Repository, + org.apache.jackrabbit.j2ee.RepositoryAccessServlet, + org.apache.jackrabbit.util.Text, + javax.jcr.Session, + javax.jcr.SimpleCredentials, + javax.jcr.query.Query, + javax.jcr.query.RowIterator, + java.text.NumberFormat, + javax.jcr.query.Row, + javax.jcr.Node, + java.net.URLEncoder, + java.text.SimpleDateFormat, + java.text.DateFormat, + java.util.List, + java.util.ArrayList, + java.util.Iterator, + javax.jcr.Value, + javax.jcr.RepositoryException"%> +<%@ page contentType="text/html;charset=UTF-8" %><% + Repository rep; + Session jcrSession; + try { + rep = RepositoryAccessServlet.getRepository(pageContext.getServletContext()); + jcrSession = rep.login(new SimpleCredentials("anonymous", "".toCharArray())); + } catch (Throwable e) { + %>Error while accessing the repository: <%= Text.encodeIllegalXMLCharacters(e.getMessage()) %>
        <% + %>Check the configuration or use the easy setup wizard.<% + return; + } + try { + String wspName = jcrSession.getWorkspace().getName(); + String q = request.getParameter("q"); + if (q == null) { + q = ""; + } else { + q = new String(q.getBytes("ISO-8859-1"), "UTF-8"); + } + if (request.getParameter("as_q") != null) { + q += " " + new String(request.getParameter("as_q").getBytes("ISO-8859-1"), "UTF-8"); + } + String executedIn = ""; + String queryTerms = ""; + String totalResults = ""; + long from = 0; + long to = 10; + long total = 0; + long maxPage = 0; + long minPage = 0; + long currentPageIndex = 0; + List indexes = new ArrayList(); + RowIterator rows = null; + String suggestedQuery = null; + if (q != null && q.length() > 0) { + String stmt; + if (q.startsWith("related:")) { + String path = q.substring("related:".length()); + path = path.replaceAll("'", "''"); + stmt = "//element(*, nt:file)[rep:similar(jcr:content, '" + path + "/jcr:content')]/rep:excerpt(.) order by @jcr:score descending"; + queryTerms = "similar to " + Text.encodeIllegalXMLCharacters(path) + ""; + } else { + queryTerms = "for " + Text.encodeIllegalXMLCharacters(q) + ""; + q = q.replaceAll("'", "''"); + stmt = "//element(*, nt:file)[jcr:contains(jcr:content, '" + q + "')]/rep:excerpt(.) order by @jcr:score descending"; + } + Query query = jcrSession.getWorkspace().getQueryManager().createQuery(stmt, Query.XPATH); + long time = System.currentTimeMillis(); + rows = query.execute().getRows(); + time = System.currentTimeMillis() - time; + NumberFormat nf = NumberFormat.getNumberInstance(); + nf.setMaximumFractionDigits(2); + nf.setMinimumFractionDigits(2); + executedIn = nf.format(((double) time) / 1000d); + nf.setMaximumFractionDigits(0); + totalResults = nf.format(rows.getSize()); + if (request.getParameter("start") != null) { + from = Long.parseLong(request.getParameter("start")); + try { + rows.skip(from); + } catch (Exception e) { + // make sure rows are consumed + while (rows.hasNext()) { + rows.nextRow(); + } + } + } + to = Math.min(from + 10, rows.getSize()); + + total = rows.getSize(); + maxPage = total / 10L; + if (total % 10L > 0) { + maxPage++; + } + currentPageIndex = from / 10L; + maxPage = Math.min(maxPage, currentPageIndex + 10); + minPage = Math.max(0, currentPageIndex - 10); + for (long i = minPage; i < maxPage; i++) { + indexes.add(new Long(i)); + } + + if (total < 10 && !q.startsWith("related:")) { + try { + Value v = jcrSession.getWorkspace().getQueryManager().createQuery( + "/jcr:root[rep:spellcheck('" + q + "')]/(rep:spellcheck())", + Query.XPATH).execute().getRows().nextRow().getValue("rep:spellcheck()"); + if (v != null) { + suggestedQuery = v.getString(); + } + } catch (RepositoryException e) { + // ignore + } + } + } +request.setAttribute("title", "Search workspace " + wspName); +%> + +
        +

        + +

        +

        +
        +<% if (rows != null && rows.getSize() == 0) { %> +<% if (suggestedQuery != null) { %> +

        Did you mean: + + <%= Text.encodeIllegalXMLCharacters(suggestedQuery) %> +
        +

        +<% } %> +

        Your search - <%= Text.encodeIllegalXMLCharacters(q) %> - did not match any documents. +

        Suggestions: +

          +
        • Make sure all words are spelled correctly.
        • +
        • Try different keywords.
        • +
        • Try more general keywords.
        • +
        • Try fewer keywords.
        • +
        + <% + } else if (rows != null) { + %> + + + +
        Results <%= from + 1 %> - <%= to %> of about <%= totalResults %> <%= queryTerms %>. (<%= executedIn %> seconds) 
        +<% if (suggestedQuery != null) { %> +

        + Did you mean: + + <%= Text.encodeIllegalXMLCharacters(suggestedQuery) %> +
        +

        +<% } %> +
        + <% + while (rows.hasNext() && rows.getPosition() < to) { + Row r = rows.nextRow(); + Node file = (Node) jcrSession.getItem(r.getValue("jcr:path").getString()); + Node resource = file.getNode("jcr:content"); + String size = ""; + if (resource.hasProperty("jcr:data")) { + double length = resource.getProperty("jcr:data").getLength(); + size = String.valueOf(Math.round(Math.ceil(length / 1000d))) + "k"; + } + DateFormat df = SimpleDateFormat.getDateInstance(SimpleDateFormat.LONG); + String lastModified = df.format(resource.getProperty("jcr:lastModified").getDate().getTime()); + %> +
        " class=l><%= Text.encodeIllegalXMLCharacters(file.getName()) %>
        + + + +
        <%= r.getValue("rep:excerpt(jcr:content)").getString() %> + <%= Text.encodeIllegalXMLCharacters(file.getPath()) %> - <%= size %> - <%= lastModified %> - ">Similar pages
        + <% + } // while + %> +
        + +
        + <% + if (indexes.size() > 1) { + %> +
        + + +
        Result Page:  + <% + if (currentPageIndex != ((Long) indexes.get(0)).longValue()) { + %>&start=<%= (currentPageIndex - 1) * 10 %>>Previous<% + } else { + %><% + } + for (Iterator it = indexes.iterator(); it.hasNext(); ) { + long pageIdx = ((Long) it.next()).longValue(); + if (pageIdx == currentPageIndex) { + %><%= pageIdx + 1 %><% + } else { + %>&start=<%= pageIdx * 10 %>><%= pageIdx + 1 %><% + } + } + if (currentPageIndex < (maxPage - 1)) { + %>&start=<%= (currentPageIndex + 1) * 10 %>>Next<% + } else { + %><% + } + %> +
        +
        + <% + } + %> + + +

        + + +

        /search.jsp> + +
        +
        + Search within results | Dissatisfied? Help us improve
        +
        +
        + + <% + } // if (rows != null) + + String tableClass = ""; + if (rows != null && rows.getSize() == 0) { + tableClass = " class=\"t n bt\""; + } + %> + +<% + } finally { + if (jcrSession != null) { + jcrSession.logout(); + } + } +%> \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/swr.jsp b/jackrabbit-webapp/src/main/webapp/swr.jsp new file mode 100644 index 00000000000..c5af6b73c7b --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/swr.jsp @@ -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. +--%><%@ page import="javax.jcr.Repository, + javax.jcr.Session, + org.apache.jackrabbit.j2ee.RepositoryAccessServlet, + org.apache.jackrabbit.util.Text, + javax.jcr.SimpleCredentials, + java.text.NumberFormat" +%><%@ page contentType="text/html;charset=UTF-8" %><% + Repository rep; + Session jcrSession; + try { + rep = RepositoryAccessServlet.getRepository(pageContext.getServletContext()); + jcrSession = rep.login(new SimpleCredentials("anonymous", "".toCharArray())); + } catch (Throwable e) { + %>Error while accessing the repository: <%= Text.encodeIllegalXMLCharacters(e.getMessage()) %>
        <% + %>Check the configuration or use the easy setup wizard.<% + return; + } + try { + String q = new String(request.getParameter("q").getBytes("ISO-8859-1"), "UTF-8"); + String swrnum = request.getParameter("swrnum"); + String numResults = null; + try { + numResults = NumberFormat.getNumberInstance().format(Long.parseLong(swrnum)); + } catch (NumberFormatException e) { + // ignore + } + if (q == null || numResults == null) { + return; + } + + request.setAttribute("title", "Search within results"); + %> +
        + + + +

        There were about <%= numResults %> results for <%= Text.encodeIllegalXMLCharacters(q) %>.
        + Use the search box below to search within these results.

        +
        +
        + + + +
        +
        +
        + +<% + } finally { + if (jcrSession != null) { + jcrSession.logout(); + } + } +%> \ No newline at end of file diff --git a/jackrabbit-webapp/src/main/webapp/troubleshooting.jsp b/jackrabbit-webapp/src/main/webapp/troubleshooting.jsp new file mode 100644 index 00000000000..4aec1f2d396 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/troubleshooting.jsp @@ -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. +--%><%@page import="org.apache.jackrabbit.util.Text, + java.io.StringWriter, + java.io.PrintWriter"%><% +request.setAttribute("title", "Troubleshooting"); +%> +

        + If you experience problems with the Jackrabbit JCR server, please + check the following: +

        +
          +
        1. + Did you encounter an exception? Copy the exception stack trace somewhere + so you don't loose it. The stack trace contains valuable information + for the Jackrabbit developers if you need to file a bug report for the + problem you encountered. +
        2. +
        3. + Is the repository up and running? Try browsing the + default workspace + to check if you can still see any content in the repository. You will + see an error message if the repository is not available. +
        4. +
        5. + What were you trying to do? Try to verify that your client code or + other manner of repository use is correct. Did it work before or are + you trying to do something new? +
        6. +
        7. + Are there any notable log entries? Check the log files for any related + warnings or errors. By default the Jackrabbit JCR Server writes log + entries to the standard output of the servlet container. You can customize + logging by editing the /WEB-INF/log4j.xml file and + redeploying this web application. +
        8. +
        +

        + If none of the above steps help you identify or resolve the problem, + you can contact the Jackrabbit users mailing list or report the problem + in the Jackrabbit issue tracker to get support from the Jackrabbit community. + When contacting the community, please include any relevant details related + to the above questions and the environment information shown at the end + of this page. +

        + +

        Jackrabbit mailing list

        +

        + The Jackrabbit user mailing list, users@jackrabbit.apache.org, is the + place to discuss any problems or other issues regarding the use of + Apache Jackrabbit (or JCR content repositories in general). +

        +

        + Feel free to subscribe the mailing list or browse the archives listed as + described in the + Jackrabbit mailing lists + page. +

        + +

        Jackrabbit issue tracker

        +

        + If you think you've identified a defect in Jackrabbit, you're welcome + to file a bug report in the + Jackrabbit issue tracker. + You can also use the issue tracker to request new features and other + improvements. +

        +

        + You need an account in the issue tracker to report new issues or to comment + on existing. Use the + registration form + if you don't already have an account. No account is needed browsing + and searching existing issues. +

        + +

        Environment information

        +

        + This instance of the Jackrabbit JCR Server is running in + a <%= Text.encodeIllegalXMLCharacters(application.getServerInfo()) %> servlet container + that supports the Java Servlet API version + <%= application.getMajorVersion() %>.<%= application.getMinorVersion() %>. +

        +

        + Details of the Java and operating system environment are included in + the system properties shown below: +

        +<% +StringWriter buffer = new StringWriter(); +System.getProperties().list(new PrintWriter(buffer)); +%> +
        <%= Text.encodeIllegalXMLCharacters(buffer.toString()) %>
        + diff --git a/jackrabbit-webapp/src/main/webapp/webdav-jcr.jsp b/jackrabbit-webapp/src/main/webapp/webdav-jcr.jsp new file mode 100644 index 00000000000..0222615448f --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/webdav-jcr.jsp @@ -0,0 +1,88 @@ +<%@ page import="org.apache.jackrabbit.j2ee.JCRWebdavServerServlet, + java.net.URI" +%><%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "JCR Remoting Server"); + +URI uri = new URI(request.getRequestURL().toString()); +int port = uri.getPort(); +String href = + uri.getScheme() + "://" + uri.getHost() + (port == -1 ? "" : (":" + port)) + + request.getContextPath() + + JCRWebdavServerServlet.getPathPrefix(pageContext.getServletContext()); +href = Text.encodeIllegalXMLCharacters(href); +String shref = href + "/default/jcr:root"; +%> +

        + The JCR Remoting Server provides an item-based WebDAV view to the + JCR repository, mapping the functionality provided by JSR 170 to the + WebDAV protocol in order to allow remote content repository access + via WebDAV. +

        +

        + See the draft document + JCR_Webdav_Protocol.zip + for more details regarding this remoting protocol. +

        +

        + Batch read and write as well as the missing functionality (cross workspace + copy and clone) has been addressed with a extension + to the remoting server. +

        + +

        Access the content repository

        +

        + Use the following URLs to access the content repository in your remoting client: +

        +
        +
        <%= href %>
        +
        to access all workspaces of your JCR repository
        +
        <%= shref %>
        +
        to access a single workspace (example with workspace named 'default')
        +
        + +

        Supported WebDAV functionality

        +

        + This implementation focuses on replicating all JCR features for remote + access instead of providing standard WebDAV functionality or compatibility + with existing WebDAV clients. +

        +

        + The following RFCs are used to implement the remoting functionality: +

        + + +

        JCR Remoting Client

        +

        + For the client counterpart of this WebDAV servlet please take a look at the + Jackrabbit SPI2DAV + module. +

        + +

        Configuration

        +
          +
        • Context Path: <%= Text.encodeIllegalXMLCharacters(request.getContextPath()) %>
        • +
        • Resource Path Prefix: <%= Text.encodeIllegalXMLCharacters(JCRWebdavServerServlet.getPathPrefix(pageContext.getServletContext())) %>
        • +
        • Workspace Name: optional (available workspaces are mapped as resources)
        • +
        • Additional servlet configuration: see /WEB-INF/web.xml
        • +
        + diff --git a/jackrabbit-webapp/src/main/webapp/webdav-remoting.jsp b/jackrabbit-webapp/src/main/webapp/webdav-remoting.jsp new file mode 100644 index 00000000000..7e2514d2349 --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/webdav-remoting.jsp @@ -0,0 +1,92 @@ +<%@ page import="org.apache.jackrabbit.j2ee.JCRWebdavServerServlet, + java.net.URI" +%><%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><%@page import="org.apache.jackrabbit.util.Text"%> +<%@ page import="org.apache.jackrabbit.j2ee.JcrRemotingServlet" %> +<% +request.setAttribute("title", "JCR Remoting Server with Batch Read/Write"); + +URI uri = new URI(request.getRequestURL().toString()); +int port = uri.getPort(); +String href = + uri.getScheme() + "://" + uri.getHost() + (port == -1 ? "" : (":" + port)) + + request.getContextPath() + + JCRWebdavServerServlet.getPathPrefix(pageContext.getServletContext()); +href = Text.encodeIllegalXMLCharacters(href); +String shref = href + "/default/jcr:root"; +%> +

        + The JCR Remoting Server provides an item-based WebDAV view to the + JCR repository, mapping the functionality provided by JSR 170 to the + WebDAV protocol in order to allow remote content repository access + via WebDAV. +

        +

        + This implementation variant adds batch read and write functionality to the initial + JCR Remoting Server. In addition it supports + copy across workspaces and clone. +

        + +

        Access the content repository

        +

        + Use the following URLs to access the content repository in the remoting client: +

        +
        +
        <%= href %>
        +
        to access all workspaces of your JCR repository
        +
        <%= shref %>
        +
        to access a single workspace (example with workspace named 'default')
        +
        + +

        Supported WebDAV functionality

        +

        + See JCR Remoting Server. +

        + +

        Batch Read

        +

        +Composes a JSON object for a node (and its child items) up to a explicitely +specified or configuration determined depth. +
        +See JavaDoc for details +or try the Examples. +

        + +

        Batch Write

        +

        +In contrast to the default JCR remoting this extended version allows to send +a block of modifications (SPI Batch) within a single POST request containing a +custom ":diff" parameter. +
        +See the JavaDoc for details +or try the Examples. +

        + +

        JCR Remoting Client

        +

        + For the client counterpart of this WebDAV servlet please take a look at the extended SPI2DAV + project. +

        + +

        Configuration

        +
          +
        • Context Path: <%= Text.encodeIllegalXMLCharacters(request.getContextPath()) %>
        • +
        • Resource Path Prefix: <%= Text.encodeIllegalXMLCharacters(JcrRemotingServlet.getPathPrefix(pageContext.getServletContext())) %>
        • +
        • Workspace Name: optional (available workspaces are mapped as resources)
        • +
        • Additional servlet configuration: see /WEB-INF/web.xml
        • +
        + diff --git a/jackrabbit-webapp/src/main/webapp/webdav-simple.jsp b/jackrabbit-webapp/src/main/webapp/webdav-simple.jsp new file mode 100644 index 00000000000..ab53775eb2f --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/webdav-simple.jsp @@ -0,0 +1,84 @@ +<%@ page import="org.apache.jackrabbit.j2ee.SimpleWebdavServlet, + java.net.URI" +%><%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><%@page import="org.apache.jackrabbit.util.Text"%><% +request.setAttribute("title", "Standard WebDAV Server"); + +URI uri = new URI(request.getRequestURL().toString()); +int port = uri.getPort(); +String href = + uri.getScheme() + "://" + uri.getHost() + (port == -1 ? "" : (":" + port)) + + request.getContextPath() + + SimpleWebdavServlet.getPathPrefix(pageContext.getServletContext()) + + "/default/"; +href = Text.encodeIllegalXMLCharacters(href); +%> + +

        + The default WebDAV server (aka: Simple Server) is a + DAV 1,2 and + DeltaV + compliant WebDAV server implementation. It offers a file-based view to + the JCR repository, suitable for everybody looking for standard WebDAV + functionality. Essentially, the contents of the underlying content + repository are exposed as a hierarchical collection of files and folders. +

        + +

        Access the content repository

        +

        + Use the following URL to access the content repository in your WebDAV client: +

        + +

        + The server asks for authentication and will accept credentials for the default + admin user: admin/admin. You can modify this security policy in + the repository configuration file. +

        +

        + To access other workspace than the default one, replace the last part of + the URL (/default/) with the name of another workspace. +

        +

        + You can also search the default workspace. +

        + +

        File system access

        +

        + Many operating systems, including Windows and Mac OS X, allow you to + "mount" a WebDAV server as a shared network disk. You can use the above + URL to make the default workspace available as such a network disk, after + which you can use normal file system tools to copy files and folders to + and from the content repository. +

        + +

        Supported WebDAV functionality

        + + +

        Configuration

        +
          +
        • Context path: <%= Text.encodeIllegalXMLCharacters(request.getContextPath()) %>
        • +
        • Resource path prefix: <%= Text.encodeIllegalXMLCharacters(SimpleWebdavServlet.getPathPrefix(pageContext.getServletContext())) %>
        • +
        • Servlet configuration: see /WEB-INF/web.xml
        • +
        • WebDAV specific resource configuration: see /WEB-INF/config.xml
        • +
        + diff --git a/jackrabbit-webapp/src/main/webapp/welcome.jsp b/jackrabbit-webapp/src/main/webapp/welcome.jsp new file mode 100644 index 00000000000..7ed1caa6e3d --- /dev/null +++ b/jackrabbit-webapp/src/main/webapp/welcome.jsp @@ -0,0 +1,60 @@ +<%@ page import="org.apache.jackrabbit.j2ee.RepositoryAccessServlet, + javax.jcr.Repository" +%><%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--%><% + +Repository rep; +try { + rep = RepositoryAccessServlet.getRepository(pageContext.getServletContext()); +} catch (Throwable e) { + %><% +} + +request.setAttribute("title", "Apache Jackrabbit JCR Server"); +%> +

        + Welcome to the Apache Jackrabbit JCR Server. This web application + contains a JCR content repository and makes it available to clients + through WebDAV and other means. +

        +

        + The following WebDAV view is provided for accessing the + content in the JCR content repository. +

        + +

        + In addition the JCR Server project provides means for JCR remoting over HTTP: +

        + +

        + Clients can also access the repository using the JCR API. Both local + and remote access is supported. +

        + +

        + For more information, including copyright and licensing details, visit the + About Apache Jackrabbit page. +

        + diff --git a/jackrabbit-webapp/src/test/java/org/apache/jackrabbit/j2ee/TomcatIT.java b/jackrabbit-webapp/src/test/java/org/apache/jackrabbit/j2ee/TomcatIT.java new file mode 100644 index 00000000000..81ecb5da741 --- /dev/null +++ b/jackrabbit-webapp/src/test/java/org/apache/jackrabbit/j2ee/TomcatIT.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.j2ee; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import junit.framework.TestCase; + +import org.apache.catalina.startup.Tomcat; +import org.apache.commons.io.FileUtils; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.google.common.io.Files; + +public class TomcatIT extends TestCase { + + static { + SLF4JBridgeHandler.install(); + } + + private URL url; + + private Tomcat tomcat; + + private WebClient client; + + protected void setUp() throws Exception { + File war = null; + for (File f : new File("target").listFiles()) { + if (f.isDirectory() && new File(f, "WEB-INF/web.xml").isFile()) { + war = f; + break; + } + } + assertNotNull(war); + rewriteWebXml(war); + + File bootstrap = new File("target", "bootstrap.properties"); + bootstrap.delete(); + + File baseDir = new File("target", "tomcat"); + FileUtils.deleteQuietly(baseDir); + + File repoDir = new File("target", "repository"); + FileUtils.deleteQuietly(repoDir); + + url = new URL("http://localhost:12856/"); + + tomcat = new Tomcat(); + tomcat.setSilent(true); + tomcat.setBaseDir(baseDir.getPath()); + tomcat.setHostname(url.getHost()); + tomcat.setPort(url.getPort()); + + tomcat.addWebapp("", war.getAbsolutePath()); + + tomcat.start(); + + client = new WebClient(); + } + + public void testTomcat() throws Exception { + HtmlPage page = client.getPage(url); + assertEquals("Content Repository Setup", page.getTitleText()); + + page = submitNewRepositoryForm(page); + assertEquals("Content Repository Ready", page.getTitleText()); + + page = page.getAnchorByText("home").click(); + assertEquals("Apache Jackrabbit JCR Server", page.getTitleText()); + } + + private HtmlPage submitNewRepositoryForm(HtmlPage page) throws IOException { + for (HtmlForm form : page.getForms()) { + for (HtmlInput mode : form.getInputsByName("mode")) { + if ("new".equals(mode.getValueAttribute())) { + for (HtmlInput home : form.getInputsByName("repository_home")) { + home.setValueAttribute("target/repository"); + for (HtmlElement submit : form.getElementsByAttribute("input", "type", "submit")) { + return submit.click(); + } + } + } + } + } + fail(); + return null; + } + + private void rewriteWebXml(File war) throws IOException { + File webXml = new File(war, new File("WEB-INF","web.xml").getPath()); + assertTrue(webXml.exists()); + List lines = Files.readLines(webXml, StandardCharsets.UTF_8); + BufferedWriter writer = Files.newWriter(webXml, StandardCharsets.UTF_8); + try { + for (String line : lines) { + line = line.replace("jackrabbit/bootstrap.properties", + "target/bootstrap.properties"); + writer.write(line); + writer.write(System.lineSeparator()); + } + } finally { + writer.close(); + } + } + + protected void tearDown() throws Exception { + client.closeAllWindows(); + + tomcat.stop(); + } + +} diff --git a/jackrabbit-webapp/src/test/resources/compatibility.zip b/jackrabbit-webapp/src/test/resources/compatibility.zip new file mode 100644 index 00000000000..8141883bb75 Binary files /dev/null and b/jackrabbit-webapp/src/test/resources/compatibility.zip differ diff --git a/jackrabbit-webapp/src/test/resources/logback-test.xml b/jackrabbit-webapp/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..15d98f9b73f --- /dev/null +++ b/jackrabbit-webapp/src/test/resources/logback-test.xml @@ -0,0 +1,31 @@ + + + + + + target/jcr.log + + %date{HH:mm:ss.SSS} %-5level %-40([%thread] %F:%L) %msg%n + + + + + + + + diff --git a/jackrabbit-webdav/README.txt b/jackrabbit-webdav/README.txt new file mode 100644 index 00000000000..dbc91e564fa --- /dev/null +++ b/jackrabbit-webdav/README.txt @@ -0,0 +1,81 @@ +==================================== +Welcome to Jackrabbit WebDAV Library +==================================== + +This is the WebDAV Library component of the Apache Jackrabbit project. +This component provides interfaces and common utility classes used for +building a WebDAV server or client. The following RFC have been integrated: + + * RFC 2518 (WebDAV - HTTP Extensions for Distributed Authoring) + * RFC 3253 (DeltaV - Versioning Extensions to WebDAV) + * RFC 3648 (Ordered Collections Protocol) + * RFC 3744 (Access Control Protocol) + * DAV Searching and Locating (DASL) + * Binding Extensions to Web Distributed Authoring and Versioning (WebDAV) (experimental) + +In addition this library defines (unspecified) + + * Observation + * Bundling multiple request with extensions to locking + +Common Questions +================ + +Q: Which WebDAV features are supported? +A: DAV 1, 2, DeltaV, Ordering, Access Control, Search, Bind + +Q: This this WebDAV library provide a full dav server? +A: This library only defines interfaces, utilities and common + classes used for a dav server/client. + A JCR specific implementation can be found in the 'jcr-server' + and the 'webapp' project. + +Q: How do a get a deployable Jackrabbit installation with WebDAV and + optional RMI support? +A: The 'webdav' project only serves as library. In order to access + a Jackrabbit repository please follow the instructions present + with the 'webapp' project. + +Q: Does the WebDAV library has dependency to JSR170 +A: No, the library can be used as generic webdav library in any + other project. There exists a dependency to the jackrabbit-commons + library for utility classes only. + +Things to do +============ + +------------------------------------------------------------------- +todo webdav/version +------------------------------------------------------------------- + +- review: compliance to deltaV +- reconsider feature-sets (see also JCR-394) +- CHECKOUT may contain request body (working-resource, activity, checkout-in-place) +- CHECKIN may contain request body (working-resource, checkout-in-place) +- VERSION-CONTROL may contain request body (workspace f.) +- BASELINE: creation of Baseline resources is not yet supported + (TODO within AbstractWebDAVServlet) + +------------------------------------------------------------------- +todo webdav/ordering +------------------------------------------------------------------- + +- respect Position header with creation of new collection members by + PUT, COPY, MKCOL requests + +------------------------------------------------------------------- +todo webdav/search +------------------------------------------------------------------- + +- SearchResource should extend DavResource +- Library misses support for the DAV:basicsearch + +------------------------------------------------------------------- +todo webdav/transaction +------------------------------------------------------------------- + +- review naming of the lock scopes. 'global','local' are not correct in + this context. +- j2ee explicitely requires any usertransaction to be completed + upon the end of the servletes service method. + general review necessary. diff --git a/jackrabbit-webdav/pom.xml b/jackrabbit-webdav/pom.xml new file mode 100644 index 00000000000..854d7d5ae2f --- /dev/null +++ b/jackrabbit-webdav/pom.xml @@ -0,0 +1,93 @@ + + + + + + 4.0.0 + + + + + + org.apache.jackrabbit + jackrabbit-parent + 2.17.5-SNAPSHOT + ../jackrabbit-parent/pom.xml + + jackrabbit-webdav + bundle + Jackrabbit WebDAV Library + Generic WebDAV Library + + + + + org.apache.felix + maven-bundle-plugin + true + + + maven-surefire-plugin + + + **/*TestAll.java + + once + ${test.opts} + + + + + + + + org.osgi + org.osgi.annotation + provided + + + org.slf4j + slf4j-api + + + javax.servlet + servlet-api + provided + + + org.apache.httpcomponents + httpclient + 4.5.6 + + + org.slf4j + jcl-over-slf4j + + + + junit + junit + test + + + ch.qos.logback + logback-classic + test + + + diff --git a/jackrabbit-webdav/src/main/appended-resources/META-INF/NOTICE b/jackrabbit-webdav/src/main/appended-resources/META-INF/NOTICE new file mode 100644 index 00000000000..45e7de5ff36 --- /dev/null +++ b/jackrabbit-webdav/src/main/appended-resources/META-INF/NOTICE @@ -0,0 +1,2 @@ +Based on source code originally developed by +Day Software (http://www.day.com/). diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/AbstractLocatorFactory.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/AbstractLocatorFactory.java new file mode 100644 index 00000000000..c6c681c928d --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/AbstractLocatorFactory.java @@ -0,0 +1,401 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav; + +import org.apache.jackrabbit.webdav.util.EncodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AbstractLocatorFactory is an implementation of the DavLocatorFactory + * interface that defines how a given uri is split to workspace path an + * resource path and how it's implementation of DavResourceLocator + * builds the href. In contrast, the conversion from repository path to + * resource path and vice versa is left to subclasses. + */ +public abstract class AbstractLocatorFactory implements DavLocatorFactory { + + private static Logger log = LoggerFactory.getLogger(AbstractLocatorFactory.class); + + private final String pathPrefix; + + /** + * Create a new factory + * + * @param pathPrefix Prefix, that needs to be removed in order to retrieve + * the path of the repository item from a given DavResourceLocator. + */ + public AbstractLocatorFactory(String pathPrefix) { + this.pathPrefix = pathPrefix; + } + + //--------------------------------------------------< DavLocatorFactory >--- + /** + * Create a new DavResourceLocator. Any leading prefix and + * path-prefix (as defined with the constructor) are removed from the + * given request handle. The same applies for trailing '/'. The remaining + * String is called the 'resource handle' and it's first segment is treated + * as workspace name. If resource handle (and therefore workspace name + * are missing, both values are set to null. + *

        + * Examples: + * + *

        +     * http://www.foo.bar/ (path prefix missing)
        +     * -> workspace path = null
        +     * -> resource path  = null
        +     * -> href           = http://www.foo.bar/pathPrefix/
        +     *
        +     * http://www.foo.bar/pathPrefix/
        +     * -> workspace path = null
        +     * -> resource path  = null
        +     * -> href           = http://www.foo.bar/pathPrefix/
        +     *
        +     * http://www.foo.bar/pathPrefix/wspName
        +     * -> workspace path = /wspName
        +     * -> resource path  = /wspName
        +     * -> href           = http://www.foo.bar/pathPrefix/wspName
        +     *
        +     * http://www.foo.bar/pathPrefix/wspName/anypath
        +     * -> workspace path = /wspName
        +     * -> resource path  = /wspName/anypath
        +     * -> href           = http://www.foo.bar/pathPrefix/wspName/anypath
        +     * 
        + * + * NOTE: If the given href is an absolute uri it must start with the + * specified prefix. + * + * @param prefix + * @param href + * @return a new DavResourceLocator + * @throws IllegalArgumentException if the given href is null + */ + public DavResourceLocator createResourceLocator(String prefix, String href) { + if (href == null) { + throw new IllegalArgumentException("Request handle must not be null."); + } + + // build prefix string and remove all prefixes from the given href. + StringBuffer b = new StringBuffer(""); + if (prefix != null && prefix.length() > 0) { + b.append(prefix); + if (href.startsWith(prefix)) { + href = href.substring(prefix.length()); + } + } + if (pathPrefix != null && pathPrefix.length() > 0) { + if (!b.toString().endsWith(pathPrefix)) { + b.append(pathPrefix); + } + if (href.startsWith(pathPrefix)) { + href = href.substring(pathPrefix.length()); + } + } + + // remove trailing "/" that is present with collections + if (href.endsWith("/")) { + href = href.substring(0, href.length() - 1); + } + + String resourcePath; + String workspacePath; + + // an empty requestHandle (after removal of the "/") signifies a request + // to the root that does not represent a repository item. + if ("".equals(href)) { + resourcePath = null; + workspacePath = null; + } else { + resourcePath = EncodeUtil.unescape(href); + // retrieve wspPath: look for the first slash ignoring the leading one + int pos = href.indexOf('/', 1); + if (pos == -1) { + // request to a 'workspace' resource + workspacePath = resourcePath; + } else { + // separate the workspace path from the resource path. + workspacePath = EncodeUtil.unescape(href.substring(0, pos)); + } + } + + return new DavResourceLocatorImpl(b.toString(), workspacePath, resourcePath, this); + } + + /** + * Like {@link #createResourceLocator(String, String)}, but by setting + * {@code forDestination} to {@code true} any special processing of URI + * suffixes can be disabled. + */ + public DavResourceLocator createResourceLocator(String prefix, String href, boolean forDestination) { + return createResourceLocator(prefix, href); + } + + /** + * Create a new DavResourceLocator from the specified prefix, + * workspace path and resource path, without modifying the specified Strings. + * Note, that it is expected that the resource path starts with the + * given workspace path unless both values are null. + * + * @param prefix + * @param workspacePath path or the workspace containing this resource or + * null. + * @param resourcePath Path of the resource or null. Any non + * null value must start with the specified workspace path. + * @return a new DavResourceLocator + * @see DavLocatorFactory#createResourceLocator(String, String, String) + */ + public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) { + return createResourceLocator(prefix, workspacePath, resourcePath, true); + } + + /** + * Create a new DavResourceLocator from the specified prefix, + * workspace path and resource path. If isResourcePath is set + * to false, the given 'resourcePath' is converted by calling + * {@link #getResourcePath(String, String)}. Otherwise the same restriction + * applies as for {@link #createResourceLocator(String, String, String)}. + * + * @param prefix + * @param workspacePath + * @param path + * @param isResourcePath + * @return + * @see DavLocatorFactory#createResourceLocator(String, String, String, boolean) + */ + public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) { + String resourcePath = (isResourcePath) ? path : getResourcePath(path, workspacePath); + return new DavResourceLocatorImpl(prefix, workspacePath, resourcePath, this); + } + + //-------------------------------------------------------------------------- + /** + * Subclasses must defined how the repository path is built from the given + * resource and workspace path. + * + * @param resourcePath + * @param wspPath + * @return + */ + protected abstract String getRepositoryPath(String resourcePath, String wspPath); + + /** + * Subclasses must defined how the resource path is built from the given + * repository and workspace path. + * + * @param repositoryPath + * @param wspPath + * @return + */ + protected abstract String getResourcePath(String repositoryPath, String wspPath); + + //--------------------------------------------------------< DavResource >--- + /** + * Private inner class DavResourceLocatorImpl implementing + * the DavResourceLocator interface. + */ + private class DavResourceLocatorImpl implements DavResourceLocator { + + private final String prefix; + private final String workspacePath; + private final String resourcePath; + private final AbstractLocatorFactory factory; + + private final String href; + + /** + * Create a new DavResourceLocatorImpl. + * + * @param prefix + * @param workspacePath + * @param resourcePath + */ + private DavResourceLocatorImpl(String prefix, String workspacePath, String resourcePath, AbstractLocatorFactory factory) { + + this.prefix = prefix; + this.workspacePath = workspacePath; + this.resourcePath = resourcePath; + this.factory = factory; + + StringBuffer buf = new StringBuffer(prefix); + // NOTE: no need to append the workspace path, since it must + // be part of the resource path. + if (resourcePath != null && resourcePath.length() > 0) { + // check if condition is really met + if (!resourcePath.startsWith(workspacePath)) { + throw new IllegalArgumentException("Resource path '" + resourcePath + "' does not start with workspace path '" + workspacePath + "'."); + } + buf.append(EncodeUtil.escapePath(resourcePath)); + } + int length = buf.length(); + if (length == 0 || (length > 0 && buf.charAt(length - 1) != '/')) { + buf.append("/"); + } + this.href = buf.toString(); + } + + /** + * Return the prefix used to build the href String. This includes the initial + * hrefPrefix as well a the path prefix. + * + * @return prefix String used to build the href. + */ + public String getPrefix() { + return prefix; + } + + /** + * Returns the resource path which always starts with the workspace + * path, if a workspace resource exists. For the top most resource + * (request handle '/'), null is returned. + * + * @return resource path or null + * @see org.apache.jackrabbit.webdav.DavResourceLocator#getResourcePath() + */ + public String getResourcePath() { + return resourcePath; + } + + /** + * Return the workspace path or null if this locator object + * represents the '/' request handle. + * + * @return workspace path or null + * @see org.apache.jackrabbit.webdav.DavResourceLocator#getWorkspacePath() + */ + public String getWorkspacePath() { + return workspacePath; + } + + /** + * Return the workspace name or null if this locator object + * represents the '/' request handle, which does not contain a workspace + * path. + * + * @return workspace name or null + * @see org.apache.jackrabbit.webdav.DavResourceLocator#getWorkspaceName() + */ + public String getWorkspaceName() { + if (workspacePath != null && workspacePath.length() > 0) { + return workspacePath.substring(1); + } + return null; + } + + /** + * Returns true if the specified locator object refers to a resource within + * the same workspace. + * + * @param locator + * @return true if the workspace name obtained from the given locator + * refers to the same workspace as the workspace name of this locator. + * @see DavResourceLocator#isSameWorkspace(org.apache.jackrabbit.webdav.DavResourceLocator) + */ + public boolean isSameWorkspace(DavResourceLocator locator) { + return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName()); + } + + /** + * Returns true if the specified string equals to this workspace name or + * if both names are null. + * + * @param workspaceName + * @return true if the workspace name is equal to this workspace name. + * @see DavResourceLocator#isSameWorkspace(String) + */ + public boolean isSameWorkspace(String workspaceName) { + String thisWspName = getWorkspaceName(); + return (thisWspName == null) ? workspaceName == null : thisWspName.equals(workspaceName); + } + + /** + * Returns an 'href' consisting of prefix and resource path (which starts + * with the workspace path). It assures a trailing '/' in case the href + * is used for collection. Note, that the resource path is + * {@link org.apache.jackrabbit.webdav.util.EncodeUtil#escapePath(String) escaped}. + * + * @param isCollection + * @return href String representing the text of the href element + * @see org.apache.jackrabbit.webdav.DavConstants#XML_HREF + * @see DavResourceLocator#getHref(boolean) + */ + public String getHref(boolean isCollection) { + return (isCollection) ? href : href.substring(0, href.length() - 1); + } + + /** + * Returns true if the 'workspacePath' field is null. + * + * @return true if the 'workspacePath' field is null. + * @see org.apache.jackrabbit.webdav.DavResourceLocator#isRootLocation() + */ + public boolean isRootLocation() { + return getWorkspacePath() == null; + } + + /** + * Return the factory that created this locator. + * + * @return factory + * @see org.apache.jackrabbit.webdav.DavResourceLocator#getFactory() + */ + public DavLocatorFactory getFactory() { + return factory; + } + + /** + * Uses {@link AbstractLocatorFactory#getRepositoryPath(String, String)} + * to build the repository path. + * + * @see DavResourceLocator#getRepositoryPath() + */ + public String getRepositoryPath() { + return factory.getRepositoryPath(getResourcePath(), getWorkspacePath()); + } + + /** + * Computes the hash code from the href, that is built from the prefix, + * the workspace name and the resource path all of them representing + * final instance fields. + * + * @return the hash code + */ + @Override + public int hashCode() { + return href.hashCode(); + } + + /** + * Returns true, if the given object is a DavResourceLocatorImpl + * with the same hash code. + * + * @param obj the object to compare to + * @return true if the 2 objects are equal; + * false otherwise + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof DavResourceLocatorImpl) { + DavResourceLocatorImpl other = (DavResourceLocatorImpl) obj; + return hashCode() == other.hashCode(); + } + return false; + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavCompliance.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavCompliance.java new file mode 100644 index 00000000000..de09b5b605d --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavCompliance.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.webdav; + +/** + * DavCompliance defines constants for the various compliance + * classes defined RFC 2518, RFC 4918 and it's extensions. + */ +public final class DavCompliance { + + /** + * Avoid instantiation + */ + private DavCompliance() {} + + // RFC 2518 + public static final String _1_ = "1"; + public static final String _2_ = "2"; + + // RFC 4918 + public static final String _3_ = "3"; + + // RFC 3253 + public static final String ACTIVITY = "activity"; + public static final String BASELINE = "baseline"; + public static final String CHECKOUT_IN_PLACE = "checkout-in-place"; + public static final String LABEL = "label"; + public static final String MERGE = "merge"; + public static final String UPDATE = "update"; + public static final String VERSION_CONTROL = "version-control"; + public static final String VERSION_CONTROLLED_COLLECTION = "version-controlled-collection"; + public static final String VERSION_HISTORY = "version-history"; + public static final String WORKING_RESOURCE = "working-resource"; + public static final String WORKSPACE = "workspace"; + + // RFC 3648 + public static final String ORDERED_COLLECTIONS = "ordered-collections"; + + // RFC 3744 + public static final String ACCESS_CONTROL = "access-control"; + + // RFC 5842 + public static final String BIND = "bind"; + + // no RFC + public static final String OBSERVATION = "observation"; + + public static String concatComplianceClasses(String[] complianceClasses) { + StringBuffer b = new StringBuffer(); + for (int i = 0; i < complianceClasses.length; i++) { + if (i > 0) { + b.append(","); + } + b.append(complianceClasses[i]); + } + return b.toString(); + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavConstants.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavConstants.java new file mode 100644 index 00000000000..a50b1cd2a54 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavConstants.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.webdav; + +import org.apache.jackrabbit.webdav.util.HttpDateFormat; +import org.apache.jackrabbit.webdav.xml.Namespace; + +import java.text.DateFormat; + +/** + * DavConstants provide constants for request and response + * headers, XML elements and property names defined by + * RFC 2518. In addition, + * common date formats (creation date and modification time) are included. + */ +public interface DavConstants { + + /** + * Default Namespace constant + */ + public static final Namespace NAMESPACE = Namespace.getNamespace("D", "DAV:"); + + //--------------------------------< Headers (Names and Value Constants) >--- + public static final String HEADER_DAV = "DAV"; + public static final String HEADER_DESTINATION = "Destination"; + public static final String HEADER_IF = "If"; + public static final String HEADER_AUTHORIZATION = "Authorization"; + public static final String HEADER_CONTENT_TYPE = "Content-Type"; + public static final String HEADER_CONTENT_LENGTH = "Content-Length"; + public static final String HEADER_CONTENT_LANGUAGE = "Content-Language"; + public static final String HEADER_ETAG = "ETag"; + public static final String HEADER_LAST_MODIFIED = "Last-Modified"; + + //--------------------------------------------------< Lock-Token Header >--- + public static final String HEADER_LOCK_TOKEN = "Lock-Token"; + public static final String OPAQUE_LOCK_TOKEN_PREFIX = "opaquelocktoken:"; + + //-----------------------------------------------------< Timeout Header >--- + public static final String HEADER_TIMEOUT = "Timeout"; + public static final String TIMEOUT_INFINITE = "Infinite"; + // RFC 2518: timeout value for TimeType "Second" MUST NOT be greater than 2^32-1 + public static final long INFINITE_TIMEOUT = Integer.MAX_VALUE; + public static final long UNDEFINED_TIMEOUT = Integer.MIN_VALUE; + + //---------------------------------------------------< Overwrite Header >--- + public static final String HEADER_OVERWRITE = "Overwrite"; + + //-------------------------------------------------------< Depth Header >--- + public static final String HEADER_DEPTH = "Depth"; + public static final String DEPTH_INFINITY_S = "infinity"; + public static final int DEPTH_INFINITY = Integer.MAX_VALUE; + public static final int DEPTH_0 = 0; + public static final int DEPTH_1 = 1; + + //---< XML Element, Attribute Names >--------------------------------------- + public static final String XML_ALLPROP = "allprop"; + public static final String XML_COLLECTION = "collection"; + public static final String XML_DST = "dst"; + public static final String XML_HREF = "href"; + public static final String XML_INCLUDE = "include"; + public static final String XML_KEEPALIVE = "keepalive"; + public static final String XML_LINK = "link"; + public static final String XML_MULTISTATUS = "multistatus"; + public static final String XML_OMIT = "omit"; + public static final String XML_PROP = "prop"; + public static final String XML_PROPERTYBEHAVIOR = "propertybehavior"; + public static final String XML_PROPERTYUPDATE = "propertyupdate"; + public static final String XML_PROPFIND = "propfind"; + public static final String XML_PROPNAME = "propname"; + public static final String XML_PROPSTAT = "propstat"; + public static final String XML_REMOVE = "remove"; + public static final String XML_RESPONSE = "response"; + public static final String XML_RESPONSEDESCRIPTION = "responsedescription"; + public static final String XML_SET = "set"; + public static final String XML_SOURCE = "source"; + public static final String XML_STATUS = "status"; + + //------------------------------------------------------------< locking >--- + public static final String XML_ACTIVELOCK = "activelock"; + public static final String XML_DEPTH = "depth"; + public static final String XML_LOCKTOKEN = "locktoken"; + public static final String XML_TIMEOUT = "timeout"; + public static final String XML_LOCKSCOPE = "lockscope"; + public static final String XML_EXCLUSIVE = "exclusive"; + public static final String XML_SHARED = "shared"; + public static final String XML_LOCKENTRY = "lockentry"; + public static final String XML_LOCKINFO = "lockinfo"; + public static final String XML_LOCKTYPE = "locktype"; + public static final String XML_WRITE = "write"; + public static final String XML_OWNER = "owner"; + /** + * The lockroot XML element + * @see RFC 4918 + */ + public static final String XML_LOCKROOT = "lockroot"; + + //-----------------------------------------------------< Property Names >--- + /* + * Webdav property names as defined by RFC 2518
        + * Note: Microsoft webdav clients as well as Webdrive request additional + * property (e.g. href, name, owner, isRootLocation, isCollection) within the + * default namespace, which are are ignored by this implementation, except + * for the 'isCollection' property, needed for XP built-in clients. + */ + public static final String PROPERTY_CREATIONDATE = "creationdate"; + public static final String PROPERTY_DISPLAYNAME = "displayname"; + public static final String PROPERTY_GETCONTENTLANGUAGE = "getcontentlanguage"; + public static final String PROPERTY_GETCONTENTLENGTH = "getcontentlength"; + public static final String PROPERTY_GETCONTENTTYPE = "getcontenttype"; + public static final String PROPERTY_GETETAG = "getetag"; + public static final String PROPERTY_GETLASTMODIFIED = "getlastmodified"; + public static final String PROPERTY_LOCKDISCOVERY = "lockdiscovery"; + public static final String PROPERTY_RESOURCETYPE = "resourcetype"; + public static final String PROPERTY_SOURCE = "source"; + public static final String PROPERTY_SUPPORTEDLOCK = "supportedlock"; + + //-------------------------------------------------< PropFind Constants >--- + public static final int PROPFIND_BY_PROPERTY = 0; + public static final int PROPFIND_ALL_PROP = 1; + public static final int PROPFIND_PROPERTY_NAMES = 2; + public static final int PROPFIND_ALL_PROP_INCLUDE = 3; // RFC 4918, Section 9.1 + + //----------------------------------------------< Date Format Constants >--- + /** + * Marker for undefined modification or creation time. + */ + public static long UNDEFINED_TIME = -1; + + /** + * modificationDate date format per RFC 1123.
        + * NOTE: Access to DateFormat isn't thread save. + */ + public static DateFormat modificationDateFormat = HttpDateFormat.modificationDateFormat(); + + /** + * Simple date format for the creation date ISO representation (partial).
        + * NOTE: Access to DateFormat isn't thread save. + */ + public static DateFormat creationDateFormat = HttpDateFormat.creationDateFormat(); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavException.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavException.java new file mode 100644 index 00000000000..624f4756afe --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavException.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.io.IOException; +import java.util.Properties; + +/** + * DavException extends the {@link Exception} class in order + * to simplify handling of exceptional situations occurring during processing + * of WebDAV requests and provides possibility to retrieve an Xml representation + * of the error. + */ +public class DavException extends Exception implements XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(DavException.class); + private static Properties statusPhrases = new Properties(); + static { + try { + statusPhrases.load(DavException.class.getResourceAsStream("statuscode.properties")); + } catch (IOException e) { + log.error("Failed to load status properties: " + e.getMessage()); + } + } + + public static final String XML_ERROR = "error"; + + private int errorCode = DavServletResponse.SC_INTERNAL_SERVER_ERROR; + private Element errorCondition; + + /** + * Create a new DavException. + * + * @param errorCode integer specifying any of the status codes defined by + * {@link DavServletResponse}. + * @param message Human readable error message. + * @see DavException#DavException(int, String, Throwable, Element) + */ + public DavException(int errorCode, String message) { + this(errorCode, message, null, null); + } + + /** + * Create a new DavException. + * + * @param errorCode integer specifying any of the status codes defined by + * {@link DavServletResponse}. + * @param cause Cause of this DavException + * @see DavException#DavException(int, String, Throwable, Element) + */ + public DavException(int errorCode, Throwable cause) { + this(errorCode, null, cause, null); + } + + /** + * Create a new DavException. + * + * @param errorCode integer specifying any of the status codes defined by + * {@link DavServletResponse}. + * @see DavException#DavException(int, String, Throwable, Element) + */ + public DavException(int errorCode) { + this(errorCode, statusPhrases.getProperty(String.valueOf(errorCode)), null, null); + } + + /** + * Create a new DavException. + * + * @param errorCode integer specifying any of the status codes defined by + * {@link DavServletResponse}. + * @param message Human readable error message. + * @param cause Cause of this DavException. + * @param errorCondition Xml element providing detailed information about + * the error. If the condition is not null, {@link #toXml(Document)} + */ + public DavException(int errorCode, String message, Throwable cause, Element errorCondition) { + super(message, cause); + this.errorCode = errorCode; + this.errorCondition = errorCondition; + log.debug("DavException: (" + errorCode + ") " + message); + } + + /** + * Return the error code attached to this DavException. + * + * @return errorCode + */ + public int getErrorCode() { + return errorCode; + } + + /** + * Return the status phrase corresponding to the error code attached to + * this DavException. + * + * @return status phrase corresponding to the error code. + * @see #getErrorCode() + */ + public String getStatusPhrase() { + return getStatusPhrase(errorCode); + } + + /** + * Returns the status phrase for the given error code. + * + * @param errorCode + * @return status phrase corresponding to the given error code. + */ + public static String getStatusPhrase(int errorCode) { + return statusPhrases.getProperty(errorCode + ""); + } + + /** + * @return true if a error condition has been specified, false otherwise. + */ + public boolean hasErrorCondition() { + return errorCondition != null; + } + + /** + * Returns a DAV:error element containing the error condition or + * null if no specific condition is available. See + * RFC 3253 + * Section 1.6 "Method Preconditions and Postconditions" for additional + * information. + * + * @param document + * @return A DAV:error element indicating the error cause or null. + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + if (hasErrorCondition()) { + Element error; + if (DomUtil.matches(errorCondition, XML_ERROR, DavConstants.NAMESPACE)) { + error = (Element) document.importNode(errorCondition, true); + } else { + error = DomUtil.createElement(document, XML_ERROR, DavConstants.NAMESPACE); + error.appendChild(document.importNode(errorCondition, true)); + } + return error; + } else { + return null; + } + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavLocatorFactory.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavLocatorFactory.java new file mode 100644 index 00000000000..db4ca0a9b25 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavLocatorFactory.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.webdav; + +/** + * DavLocatorFactory... + */ +public interface DavLocatorFactory { + + /** + * Create a new DavResourceLocator. + * + * @param prefix String consisting of [scheme:][//authority][path] where + * path defines the (imaginary) path to the {@link DavResourceLocator#isRootLocation root location}. + * @param href of the resource to be created. The given string may start with + * the 'prefix'. Please note, that in contrast to + * {@link DavLocatorFactory#createResourceLocator(String, String, String)} the + * href is expected to be URL encoded. + * @return a new resource locator. + */ + public DavResourceLocator createResourceLocator(String prefix, String href); + + /** + * Create a new DavResourceLocator. This methods corresponds to + * {@link DavLocatorFactory#createResourceLocator(String, String, String, boolean)} + * with the flag set to true. + * + * @param prefix String consisting of [scheme:][//authority][path] where + * path defines the path to the {@link DavResourceLocator#isRootLocation root location}. + * @param workspacePath the first segment of the URIs path indicating the + * workspace. The implementation may allow a empty String if workspaces + * are not supported. + * @param resourcePath the URL decoded resource path. + * @return a new resource locator. + */ + public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath); + + /** + * + * @param prefix String consisting of [scheme:][//authority][path] where + * path defines the path to the {@link DavResourceLocator#isRootLocation root location}. + * @param workspacePath the first segment of the URIs path indicating the + * workspace. The implementation may allow a empty String if workspaces + * are not supported. + * @param path the URL decoded path. + * @param isResourcePath If true this method returns the same as + * {@link DavLocatorFactory#createResourceLocator(String, String, String)}, + * otherwise the given path is treated as internal repository path. + * The implementation may choose to implement a conversion of the repository + * path to a valid resource path, e.g. (un)escaping of certain characters, due + * to incompatibility with the URI definition (or vice versa). Note that + * {@link DavResourceLocator#getRepositoryPath()} should in this case implement + * the reverse operation. + * @return a new resource locator. + * @see DavResourceLocator#getRepositoryPath() + */ + public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavMethods.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavMethods.java new file mode 100644 index 00000000000..1b1c8c40918 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavMethods.java @@ -0,0 +1,409 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav; + +import java.util.HashMap; +import java.util.Map; + +/** + * DavMethods defines constants for the WebDAV METHODS. + */ +public final class DavMethods { + + /** + * Avoid instantiation + */ + private DavMethods() {} + + /** + * A map of WebDAV METHODS + */ + private static Map methodMap = new HashMap(); + + /** + * An array of method codes that are affected by a Label header + * @see org.apache.jackrabbit.webdav.version.DeltaVConstants#HEADER_LABEL + */ + private static int[] labelMethods; + + /** + * An array of method codes defined by RFC 3253 (deltaV) + */ + private static int[] deltaVMethods; + + /** + * The webdav OPTIONS method and public constant + */ + public static final int DAV_OPTIONS = 1; + public static final String METHOD_OPTIONS = "OPTIONS"; + + /** + * The webdav GET method and public constant + */ + public static final int DAV_GET = DAV_OPTIONS + 1; + public static final String METHOD_GET = "GET"; + + /** + * The webdav HEAD method and public constant + */ + public static final int DAV_HEAD = DAV_GET + 1; + public static final String METHOD_HEAD = "HEAD"; + + + /** + * The webdav POST method and public constant + */ + public static final int DAV_POST = DAV_HEAD + 1; + public static final String METHOD_POST = "POST"; + + + /** The webdav DELETE method and public constant */ + public static final int DAV_DELETE = DAV_POST + 1; + public static final String METHOD_DELETE = "DELETE"; + + + /** The webdav PUT method and public constant */ + public static final int DAV_PUT = DAV_DELETE + 1; + public static final String METHOD_PUT = "PUT"; + + + /** + * The webdav PROPFIND method and public constant as defined by + * RFC 2518. + */ + public static final int DAV_PROPFIND = DAV_PUT + 1; + public static final String METHOD_PROPFIND = "PROPFIND"; + + + /** + * The webdav PROPPATCH method and public constant as defined by + * RFC 2518 + */ + public static final int DAV_PROPPATCH = DAV_PROPFIND + 1; + public static final String METHOD_PROPPATCH = "PROPPATCH"; + + + /** + * The webdav MKCOL (make collection) method and public constant as defined by + * RFC 2518 + */ + public static final int DAV_MKCOL = DAV_PROPPATCH + 1; + public static final String METHOD_MKCOL = "MKCOL"; + + + /** + * The webdav COPY method and public constant as defined by + * RFC 2518 + */ + public static final int DAV_COPY = DAV_MKCOL + 1; + public static final String METHOD_COPY = "COPY"; + + + /** + * The webdav MOVE method and public constant as defined by + * RFC 2518 + */ + public static final int DAV_MOVE = DAV_COPY + 1; + public static final String METHOD_MOVE = "MOVE"; + + + /** + * The webdav LOCK method and public constant as defined by + * RFC 2518 + */ + public static final int DAV_LOCK = DAV_MOVE + 1; + public static final String METHOD_LOCK = "LOCK"; + + + /** + * The webdav UNLOCK method and public constant as defined by + * RFC 2518 + */ + public static final int DAV_UNLOCK = DAV_LOCK + 1; + public static final String METHOD_UNLOCK = "UNLOCK"; + + + /** + * The webdav ORDERPATCH method and public constant + * defined by RFC 3648. + */ + public static final int DAV_ORDERPATCH = DAV_UNLOCK + 1; + public static final String METHOD_ORDERPATCH = "ORDERPATCH"; + + + /** + * The webdav SUBSCRIBE method and public constant.
        + * NOTE: This method is not defined by any of the Webdav RFCs + */ + public static final int DAV_SUBSCRIBE = DAV_ORDERPATCH + 1; + public static final String METHOD_SUBSCRIBE = "SUBSCRIBE"; + + + /** + * The webdav UNSUBSCRIBE method and public constant
        + * NOTE: This method is not defined by any of the Webdav RFCs + */ + public static final int DAV_UNSUBSCRIBE = DAV_SUBSCRIBE + 1; + public static final String METHOD_UNSUBSCRIBE = "UNSUBSCRIBE"; + + + /** + * The webdav POLL method and public constant
        + * NOTE: This method is not defined by any of the Webdav RFCs + */ + public static final int DAV_POLL = DAV_UNSUBSCRIBE + 1; + public static final String METHOD_POLL = "POLL"; + + + /** + * The webdav SEARCH method and public constant as defined by the + * Webdav Search internet draft. + */ + public static final int DAV_SEARCH = DAV_POLL + 1; + public static final String METHOD_SEARCH = "SEARCH"; + + + /** + * The webdav REPORT method and public constant defined by + * RFC 3253 + */ + public static final int DAV_REPORT = DAV_SEARCH + 1; + public static final String METHOD_REPORT = "REPORT"; + + + /** + * The webdav VERSION-CONTROL method and public constant defined by + * RFC 3253 + */ + public static final int DAV_VERSION_CONTROL = DAV_REPORT + 1; + public static final String METHOD_VERSION_CONTROL = "VERSION-CONTROL"; + + /** + * The webdav CHECKIN method and public constant defined by + * RFC 3253 + */ + public static final int DAV_CHECKIN = DAV_VERSION_CONTROL + 1; + public static final String METHOD_CHECKIN = "CHECKIN"; + + /** + * The webdav CHECKOUT method and public constant defined by + * RFC 3253 + */ + public static final int DAV_CHECKOUT = DAV_CHECKIN + 1; + public static final String METHOD_CHECKOUT = "CHECKOUT"; + + /** + * The webdav UNCHECKOUT method and public constant defined by + * RFC 3253 + */ + public static final int DAV_UNCHECKOUT = DAV_CHECKOUT + 1; + public static final String METHOD_UNCHECKOUT = "UNCHECKOUT"; + + /** + * The webdav LABEL method and public constant defined by + * RFC 3253 + */ + public static final int DAV_LABEL = DAV_UNCHECKOUT + 1; + public static final String METHOD_LABEL = "LABEL"; + + /** + * The webdav MERGE method and public constant defined by + * RFC 3253 + */ + public static final int DAV_MERGE = DAV_LABEL + 1; + public static final String METHOD_MERGE = "MERGE"; + + /** + * The webdav UPDATE method and public constant defined by + * RFC 3253 + */ + public static final int DAV_UPDATE = DAV_MERGE + 1; + public static final String METHOD_UPDATE = "UPDATE"; + + /** + * The webdav MKWORKSPACE method and public constant defined by + * RFC 3253 + */ + public static final int DAV_MKWORKSPACE = DAV_UPDATE + 1; + public static final String METHOD_MKWORKSPACE = "MKWORKSPACE"; + + /** + * The webdav BASELINE-CONTROL method and public constant defined by + * RFC 3253 + */ + public static final int DAV_BASELINE_CONTROL = DAV_MKWORKSPACE + 1; + public static final String METHOD_BASELINE_CONTROL = "BASELINE-CONTROL"; + + /** + * The webdav MKACTIVITY method and public constant defined by + * RFC 3253 + */ + public static final int DAV_MKACTIVITY = DAV_BASELINE_CONTROL + 1; + public static final String METHOD_MKACTIVITY = "MKACTIVITY"; + + /** + * The webdav ACL method and public constant defined by + * RFC 3744 + */ + public static final int DAV_ACL = DAV_MKACTIVITY + 1; + public static final String METHOD_ACL = "ACL"; + + /** + * The webdav REBIND method and public constant defined by + * the BIND specification + */ + public static final int DAV_REBIND = DAV_ACL + 1; + public static final String METHOD_REBIND = "REBIND"; + + /** + * The webdav UNBIND method and public constant defined by + * the BIND specification + */ + public static final int DAV_UNBIND = DAV_REBIND + 1; + public static final String METHOD_UNBIND = "UNBIND"; + + /** + * The webdav BIND method and public constant defined by + * the BIND specification + */ + public static final int DAV_BIND = DAV_UNBIND + 1; + public static final String METHOD_BIND = "BIND"; + + /** + * Returns webdav method type code, error result <= 0 + * Valid type codes > 0 + */ + public static int getMethodCode(String method) { + Integer code = methodMap.get(method.toUpperCase()); + if (code != null) { + return code; + } + return 0; + } + + /** + * Static initializer for methodTable map + */ + private static void addMethodCode(String method, int code) { + methodMap.put(method, code); + } + + /** + * Webdav Method table + */ + static { + addMethodCode(METHOD_OPTIONS, DAV_OPTIONS); + addMethodCode(METHOD_GET, DAV_GET); + addMethodCode(METHOD_HEAD, DAV_HEAD); + addMethodCode(METHOD_POST, DAV_POST); + addMethodCode(METHOD_PUT, DAV_PUT); + addMethodCode(METHOD_DELETE, DAV_DELETE); + addMethodCode(METHOD_PROPFIND, DAV_PROPFIND); + addMethodCode(METHOD_PROPPATCH, DAV_PROPPATCH); + addMethodCode(METHOD_MKCOL, DAV_MKCOL); + addMethodCode(METHOD_COPY, DAV_COPY); + addMethodCode(METHOD_MOVE, DAV_MOVE); + addMethodCode(METHOD_LOCK, DAV_LOCK); + addMethodCode(METHOD_UNLOCK, DAV_UNLOCK); + addMethodCode(METHOD_ORDERPATCH, DAV_ORDERPATCH); + addMethodCode(METHOD_SUBSCRIBE, DAV_SUBSCRIBE); + addMethodCode(METHOD_UNSUBSCRIBE, DAV_UNSUBSCRIBE); + addMethodCode(METHOD_POLL, DAV_POLL); + addMethodCode(METHOD_SEARCH, DAV_SEARCH); + addMethodCode(METHOD_REPORT, DAV_REPORT); + addMethodCode(METHOD_VERSION_CONTROL, DAV_VERSION_CONTROL); + addMethodCode(METHOD_CHECKIN, DAV_CHECKIN); + addMethodCode(METHOD_CHECKOUT, DAV_CHECKOUT); + addMethodCode(METHOD_UNCHECKOUT, DAV_UNCHECKOUT); + addMethodCode(METHOD_LABEL, DAV_LABEL); + addMethodCode(METHOD_MERGE, DAV_MERGE); + addMethodCode(METHOD_UPDATE, DAV_UPDATE); + addMethodCode(METHOD_MKWORKSPACE, DAV_MKWORKSPACE); + addMethodCode(METHOD_BASELINE_CONTROL, DAV_BASELINE_CONTROL); + addMethodCode(METHOD_MKACTIVITY, DAV_MKACTIVITY); + addMethodCode(METHOD_ACL, DAV_ACL); + addMethodCode(METHOD_REBIND, DAV_REBIND); + addMethodCode(METHOD_UNBIND, DAV_UNBIND); + addMethodCode(METHOD_BIND, DAV_BIND); + + labelMethods = new int[] { DAV_GET, DAV_HEAD, DAV_OPTIONS, DAV_PROPFIND, + DAV_LABEL, DAV_COPY }; + + deltaVMethods = new int[] { DAV_REPORT, DAV_VERSION_CONTROL, DAV_CHECKIN, + DAV_CHECKOUT, DAV_UNCHECKOUT, DAV_LABEL, + DAV_MERGE, DAV_UPDATE, DAV_MKWORKSPACE, + DAV_BASELINE_CONTROL, DAV_MKACTIVITY }; + } + + /** + * Returns true if the request is to create a resource. + * True for PUT, POST, MKCOL + * and MKWORKSPACE requests. + * + * @return true if request method is to create (or replace) a resource + */ + public static boolean isCreateRequest(DavServletRequest request) { + int methodCode = getMethodCode(request.getMethod()); + return ( methodCode == DAV_PUT || + methodCode == DAV_POST || + methodCode == DAV_MKCOL || + methodCode == DAV_MKWORKSPACE); + } + + /** + * Returns true if the request is to create a collection resource. + * True for MKCOL and MKWORKSPACE requests. + * + * @return true if request method is to create a new collection resource + */ + public static boolean isCreateCollectionRequest(DavServletRequest request) { + int methodCode = getMethodCode(request.getMethod()); + return (methodCode == DAV_MKCOL || methodCode == DAV_MKWORKSPACE); + } + + /** + * Returns true, if the specified method is affected by a Label header + * + * @param request + * @return + */ + public static boolean isMethodAffectedByLabel(DavServletRequest request) { + int code = getMethodCode(request.getMethod()); + for (int labelMethod : labelMethods) { + if (code == labelMethod) { + return true; + } + } + return false; + } + + /** + * Returns true, if the specified method is defined by RFC 3253 + * + * @param request + * @return true, if the specified method is defined by RFC 3253 + */ + public static boolean isDeltaVMethod(DavServletRequest request) { + int code = getMethodCode(request.getMethod()); + for (int deltaVMethod : deltaVMethods) { + if (code == deltaVMethod) { + return true; + } + } + return false; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResource.java new file mode 100644 index 00000000000..6e70a42069e --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResource.java @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav; + +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.PropEntry; + +import java.io.IOException; +import java.util.List; + +/** + * DavResource provides standard WebDAV functionality as specified + * by RFC 2518. + */ +public interface DavResource { + + /** + * String constant representing the WebDAV 1 and 2 method set. + */ + public static final String METHODS = "OPTIONS, GET, HEAD, TRACE, PROPFIND, PROPPATCH, MKCOL, COPY, PUT, DELETE, MOVE, LOCK, UNLOCK"; + + /** + * Returns a comma separated list of all compliance classes the given + * resource is fulfilling. + * + * @return compliance classes + */ + public String getComplianceClass(); + + /** + * Returns a comma separated list of all METHODS supported by the given + * resource. + * + * @return METHODS supported by this resource. + */ + public String getSupportedMethods(); + + /** + * Returns true if this webdav resource represents an existing repository item. + * + * @return true, if the resource represents an existing repository item. + */ + public boolean exists(); + + /** + * Returns true if this webdav resource has the resourcetype 'collection'. + * + * @return true if the resource represents a collection resource. + */ + public boolean isCollection(); + + /** + * Returns the display name of this resource. + * + * @return display name. + */ + public String getDisplayName(); + + /** + * Returns the {@link DavResourceLocator locator} object for this webdav resource, + * which encapsulates the information for building the complete 'href'. + * + * @return the locator for this resource. + * @see #getResourcePath() + * @see #getHref() + */ + public DavResourceLocator getLocator(); + + /** + * Returns the path of the hierarchy element defined by this DavResource. + * This method is a shortcut for DavResource.getLocator().getResourcePath(). + * + * @return path of the element defined by this DavResource. + */ + public String getResourcePath(); + + /** + * Returns the absolute href of this resource as returned in the + * multistatus response body. + * + * @return href + */ + public String getHref(); + + /** + * Return the time of the last modification or -1 if the modification time + * could not be retrieved. + * + * @return time of last modification or -1. + */ + public long getModificationTime(); + + /** + * Spools the resource properties and ev. content to the specified context + * (e.g. to respond to a 'GET' or 'HEAD' request). The context could e.g. + * wrap the servlet response. + * + * @param outputContext The output context. + * @throws IOException If an error occurs. + */ + public void spool(OutputContext outputContext) throws IOException; + + /** + * Returns an array of all {@link DavPropertyName property names} available + * on this resource. + * + * @return an array of property names. + */ + public DavPropertyName[] getPropertyNames(); + + /** + * Return the webdav property with the specified name. + * + * @param name name of the webdav property + * @return the {@link DavProperty} with the given name or null + * if the property does not exist. + */ + public DavProperty getProperty(DavPropertyName name); + + /** + * Returns all webdav properties present on this resource that will be + * return upon a {@link DavConstants#PROPFIND_ALL_PROP} request. The + * implementation may in addition expose other (protected or calculated) + * properties which should be marked accordingly (see also + * {@link org.apache.jackrabbit.webdav.property.DavProperty#isInvisibleInAllprop()}. + * + * @return a {@link DavPropertySet} containing at least all properties + * of this resource that are exposed in 'allprop' PROPFIND request. + */ + public DavPropertySet getProperties(); + + /** + * Add/Set the specified property on this resource. + * + * @param property + * @throws DavException if an error occurs + */ + public void setProperty(DavProperty property) throws DavException; + + /** + * Remove the specified property from this resource. + * + * @param propertyName + * @throws DavException if an error occurs + */ + public void removeProperty(DavPropertyName propertyName) throws DavException; + + /** + * Set/add and remove the specified properties from this resource. + * + * @param changeList list containing {@link DavPropertyName} objects (for + * properties to be removed) and {@link DavProperty} objects (for + * properties to be added/set). + * @return multistatus response listing the status resulting from + * setting and/or removing the specified properties, in order to allow a + * detailed multistatus response. + * @throws DavException if an error occurred. This may be the case if the + * general state of the resource prevents any properties to be set or removed + * (e.g. due to a lock). + */ + public MultiStatusResponse alterProperties(List changeList) throws DavException; + + /** + * Retrieve the resource this resource is internal member of. + * + * @return resource this resource is an internal member of. In case this resource + * is the root null is returned. + */ + public DavResource getCollection(); + + /** + * Add the given resource as an internal member to this resource. + * + * @param resource {@link DavResource} to be added as internal member. + * @param inputContext Context providing the properties and content for the + * internal member to be created or replaced. + * @throws DavException + */ + public void addMember(DavResource resource, InputContext inputContext) throws DavException; + + /** + * Returns an iterator over all internal members. + * + * @return a {@link DavResourceIterator} over all internal members. + */ + public DavResourceIterator getMembers(); + + /** + * Removes the specified member from this resource. + * + * @throws DavException + */ + public void removeMember(DavResource member) throws DavException; + + /** + * Move this DavResource to the given destination resource + * + * @param destination + * @throws DavException + */ + public void move(DavResource destination) throws DavException; + + /** + * Copy this DavResource to the given destination resource + * + * @param destination + * @param shallow + * @throws DavException + */ + public void copy(DavResource destination, boolean shallow) throws DavException; + + /** + * Returns true, if the this resource allows locking. NOTE, that this method + * does not define, whether a lock/unlock can be successfully executed. + * + * @return true, if this resource supports any locking. + * @param type + * @param scope + */ + public boolean isLockable(Type type, Scope scope); + + /** + * Returns true if a lock applies to this resource. This may be either a + * lock on this resource itself or a deep lock inherited from a collection + * above this resource.
        + * Note, that true is returned whenever a lock applies to that resource even + * if the lock is expired or not effective due to the fact that the request + * provides the proper lock token. + * + * @return true if a lock applies to this resource. + * @param type + */ + public boolean hasLock(Type type, Scope scope); + + /** + * Return the lock present on this webdav resource or null + * if the resource is either not locked or not lockable at all. Note, that + * a resource may have a lock that is inherited by a deep lock enforced on + * one of its 'parent' resources. + * + * @return lock information of this resource or null if this + * resource has no lock applying it. If an error occurs while retrieving the + * lock information null is returned as well. + * @param type + */ + public ActiveLock getLock(Type type, Scope scope) ; + + /** + * Returns an array of all locks applied to the given resource. + * + * @return array of locks. The array is empty if there are no locks applied + * to this resource. + */ + public ActiveLock[] getLocks(); + + /** + * Lock this webdav resource with the information retrieve from the request + * and return the resulting lockdiscovery object. + * + * @param reqLockInfo lock info as retrieved from the request. + * @return lockdiscovery object to be returned in the response. If the lock + * could not be obtained a DavException is thrown. + * @throws DavException if the lock could not be obtained. + */ + public ActiveLock lock(LockInfo reqLockInfo) throws DavException; + + /** + * Refresh an existing lock by resetting the timeout. + * + * @param reqLockInfo lock info as retrieved from the request. + * @param lockToken identifying the lock to be refreshed. + * @return lockdiscovery object to be returned in the response body. If the lock + * could not be refreshed a DavException is thrown. + * @throws DavException if the lock could not be refreshed. + */ + public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) throws DavException; + + /** + * Remove the lock identified by the included lock token from this resource. + * This method will return false if the unlocking did not succeed. + * + * @param lockToken identifying the lock to be removed. + * @throws DavException if the lock could not be removed. + */ + public void unlock(String lockToken) throws DavException; + + /** + * Add an external {@link LockManager} to this resource. This method may + * throw {@link UnsupportedOperationException} if the resource does handle + * locking itself. + * + * @param lockmgr + * @see LockManager + */ + public void addLockManager(LockManager lockmgr); + + /** + * Return the DavResourceFactory that created this resource. + * + * @return the factory that created this resource. + */ + public DavResourceFactory getFactory(); + + /** + * Retrieve the DavSession associated with this resource. + * + * @return session object associated with this resource. + */ + public DavSession getSession(); +} + diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceFactory.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceFactory.java new file mode 100644 index 00000000000..b015fe330d3 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceFactory.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.webdav; + +/** + * DavResourceFactory interface defines a single method for creating + * {@link DavResource} objects. + */ +public interface DavResourceFactory { + + /** + * Create a {@link DavResource} object from the given locator, request and response + * objects. + * + * @param locator locator of the resource + * @param request + * @param response + * @return a new DavResource object. + * @throws DavException + */ + public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException; + + /** + * Create a new {@link DavResource} object from the given locator and session. + * + * @param locator + * @param session + * @return a new DavResource object. + * @throws DavException + */ + public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException; +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceIterator.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceIterator.java new file mode 100644 index 00000000000..e625b28ad50 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceIterator.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.webdav; + +import java.util.Iterator; + +/** + * DavResourceIterator extends the Iterator interface. Additional + * METHODS allow to retrieve the next {@link DavResource} from the iterator + * and the iterators size. + */ +public interface DavResourceIterator extends Iterator { + + /** + * Returns the next {@link DavResource} in the iterator + * @return the next {@link DavResource} + */ + public DavResource nextResource(); + + /** + * Return the number of {@link DavResource}s in the iterator. + * @return number of elements in the iterator. + */ + public int size(); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceIteratorImpl.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceIteratorImpl.java new file mode 100644 index 00000000000..3f43b7a9a7e --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceIteratorImpl.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.webdav; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Iterator; +import java.util.List; +import java.util.Collections; + +/** + * DavResourceIteratorImpl implementation of the {@link DavResourceIterator} + * interface.
        + * NOTE: {@link #remove()} is not implemented. + */ +public class DavResourceIteratorImpl implements DavResourceIterator { + + private static Logger log = LoggerFactory.getLogger(DavResourceIteratorImpl.class); + + public static final DavResourceIterator EMPTY = new DavResourceIteratorImpl(Collections.emptyList()); + + private Iterator it; + private int size; + + /** + * Create a new DavResourceIterator from the specified list. + * @param list + */ + public DavResourceIteratorImpl(List list) { + it = list.iterator(); + size = list.size(); + } + + /** + * @see DavResourceIterator#hasNext() + */ + public boolean hasNext() { + return it.hasNext(); + } + + /** + * @see DavResourceIterator#next() + */ + public DavResource next() { + return it.next(); + } + + /** + * @see DavResourceIterator#nextResource() + */ + public DavResource nextResource() { + return next(); + } + + /** + * Returns the size of the initial list. + * + * @see DavResourceIterator#size() + */ + public int size() { + return size; + } + + /** + * @see DavResourceIterator#remove() + */ + public void remove() { + throw new UnsupportedOperationException("Remove not allowed with DavResourceIteratorImpl"); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceLocator.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceLocator.java new file mode 100644 index 00000000000..2a9b8227082 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavResourceLocator.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.webdav; + +/** + * DavResourceLocator... + */ +public interface DavResourceLocator { + + /** + * Return the prefix used to build the complete href of the resource as + * required for the {@link DavConstants#XML_HREF href Xml} element. + * This includes scheme and host information as well as constant prefixes. + * However, this must not include workspace prefix. + * + * @return prefix needed in order to build the href from a resource path. + * @see #getResourcePath() + */ + public String getPrefix(); + + /** + * Return the resource path. + * + * @return resource path + */ + public String getResourcePath(); + + /** + * Return the path of the workspace the resource identified by this + * locator is member of. + * + * @return path of the workspace + */ + public String getWorkspacePath(); + + /** + * Return the name of the workspace the resource identified by this + * locator is member of. + * + * @return workspace name + */ + public String getWorkspaceName(); + + /** + * Returns true if the specified locator refers to a resource within the + * same workspace. + * + * @param locator + * @return true if both paths are in the same workspace. + */ + public boolean isSameWorkspace(DavResourceLocator locator); + + /** + * Returns true if the specified workspace name equals to the workspace + * name defined with this locator. + * + * @param workspaceName + * @return true if workspace names are equal. + */ + public boolean isSameWorkspace(String workspaceName); + + /** + * Return the 'href' representation of this locator object. The implementation + * should perform an URL encoding of the resource path. + * + * @param isCollection + * @return 'href' representation of this path + * @see DavConstants#XML_HREF + * @see DavResource#getHref() + */ + public String getHref(boolean isCollection); + + /** + * Returns true if this DavResourceLocator represents the root + * locator that would be requested with 'hrefPrefix'+'pathPrefix' with or + * without a trailing '/'. + * + * @return true if this locator object belongs to the root resource. + */ + public boolean isRootLocation(); + + /** + * Return the locator factory that created this locator. + * + * @return the locator factory + */ + public DavLocatorFactory getFactory(); + + /** + * An implementation may choose to circumvent the incompatibility of a + * repository path with the URI path by applying an appropriate conversion. + * This utility method allows to retrieve this transformed repository path. + * By default this method should return the same as {@link #getResourcePath()} + * + * @return a repository compatible form if the resource path. + * @see DavLocatorFactory#createResourceLocator(String, String, String, boolean) + * that allows to build a valid DavResourceLocator from a given + * repository path. + */ + public String getRepositoryPath(); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavServletRequest.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavServletRequest.java new file mode 100644 index 00000000000..c6c0bd090fa --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavServletRequest.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.webdav; + +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.w3c.dom.Document; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * DavServletRequest extends the HttpServletRequest by Webdav + * specific METHODS. + */ +public interface DavServletRequest extends HttpServletRequest { + + /** + * Sets the DavSession to this request. + * + * @param session + */ + public void setDavSession(DavSession session); + + /** + * Returns the {@link DavSession} created for this request. + * + * @return session for this resource + */ + public DavSession getDavSession(); + + /** + * Return the locator of the requested {@link DavResource resource}. + * + * @return locator of the requested {@link DavResource resource}. + */ + public DavResourceLocator getRequestLocator(); + + /** + * Parse the {@link DavConstants#HEADER_DESTINATION Destination header} + * and return the locator of the corresponding {@link DavResource resource}. + * + * @return locator of the resource specified with the Destination header. + * @see DavConstants#HEADER_DESTINATION + */ + public DavResourceLocator getDestinationLocator() throws DavException; + + /** + * Returns true if the {@link DavConstants#HEADER_OVERWRITE Overwrite header} + * is set to 'T' thus instructing the server to overwrite the state of a + * non-null destination resource during a COPY or MOVE. A Overwrite header + * value of 'F' will return false. + * + * @return true if the Overwrite header is set to 'T', false if it is set + * to 'F'. + * @see DavConstants#HEADER_OVERWRITE + */ + public boolean isOverwrite(); + + /** + * Return the integer representation of the given {@link DavConstants#HEADER_DEPTH + * Depth header}. 'Infinity' is represented by {@link DavConstants#DEPTH_INFINITY}. + * + * @return integer representation of the {@link DavConstants#HEADER_DEPTH + * Depth header}. + * @see DavConstants#HEADER_DEPTH + */ + public int getDepth(); + + /** + * Returns the integer representation of the {@link DavConstants#HEADER_DEPTH + * Depth header} or the given defaultValue, if the Depth header is missing. + * + * @param defaultValue to be returned if no Depth header is present. + * @return integer representation of the {@link DavConstants#HEADER_DEPTH + * Depth header} or the given defaultValue. + * @see DavConstants#HEADER_DEPTH + */ + public int getDepth(int defaultValue); + + /** + * Returns the token present in the {@link DavConstants#HEADER_LOCK_TOKEN + * Lock-Token Header} or null if no such header is available.
        + * Note: The 'Lock-Token' header is sent with UNLOCK requests and with + * lock responses only. For any other request that may be affected by a lock + * the 'If' header field is responsible. + * + * @return the token present in the Lock-Token header. + * @see DavConstants#HEADER_LOCK_TOKEN + */ + public String getLockToken(); + + /** + * Return the timeout requested in the {@link DavConstants#HEADER_TIMEOUT + * Timeout header} as long. The representation of the + * 'Infinite' timeout is left to the implementation. + * + * @return long value representation of the Timeout header. + * @see DavConstants#HEADER_TIMEOUT + * @see DavConstants#TIMEOUT_INFINITE + */ + public long getTimeout(); + + /** + * Parse the Xml request body and return a {@link org.w3c.dom.Document}. + * + * @return Document representing the Xml request body or null + * if no request body is present. + * @throws DavException If the request body cannot be parsed into an Xml + * Document. + */ + public Document getRequestDocument() throws DavException; + + /** + * Return the type of PROPFIND request as indicated by the PROPFIND request + * body. + * + * @return type of PROPFIND request + * @see DavConstants#PROPFIND_ALL_PROP + * @see DavConstants#PROPFIND_BY_PROPERTY + * @see DavConstants#PROPFIND_PROPERTY_NAMES + * @see DavConstants#PROPFIND_ALL_PROP_INCLUDE + * @throws DavException If the propfind type could not be determined due to + * an invalid request body. + */ + public int getPropFindType() throws DavException; + + /** + * Return the set of properties the client requested with a PROPFIND request + * or an empty set if the type of PROPFIND request was {@link DavConstants#PROPFIND_ALL_PROP} + * or {@link DavConstants#PROPFIND_PROPERTY_NAMES}. + * + * @return set of properties the client requested with a PROPFIND request + * @throws DavException In case of invalid request body + */ + public DavPropertyNameSet getPropFindProperties() throws DavException; + + /** + * Return a {@link List} of property change operations. Each entry + * is either of type {@link DavPropertyName}, indicating a <remove> + * operation, or of type {@link DavProperty}, indicating a <set> + * operation. Note that ordering is significant here. + * + * @return {@link List} of property change operations + * @throws DavException In case of invalid request body + */ + public List getPropPatchChangeList() throws DavException; + + /** + * Return the parsed 'lockinfo' request body, the {@link DavConstants#HEADER_TIMEOUT + * Timeout header} and the {@link DavConstants#HEADER_DEPTH Depth header} + * of a LOCK request as LockInfo object. + * + * @return LockInfo object encapsulating the information + * present in the LOCK request. + * @see DavConstants#HEADER_TIMEOUT + * @see DavConstants#HEADER_DEPTH + * @see DavConstants#XML_LOCKINFO + * @throws DavException + */ + public LockInfo getLockInfo() throws DavException; + + /** + * Returns true, if the {@link DavConstants#HEADER_IF If header} present + * with the request matches the given resource. + * + * @param resource + * @return true, if the test is successful, false otherwise. + */ + public boolean matchesIfHeader(DavResource resource); + + /** + * Returns true, if the {@link DavConstants#HEADER_IF If header} present + * with the request matches to the given href, token and eTag. + * + * @param href + * @param token + * @param eTag + * @return true, if the test is successful, false otherwise. + */ + public boolean matchesIfHeader(String href, String token, String eTag); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavServletResponse.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavServletResponse.java new file mode 100644 index 00000000000..ee174f48684 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavServletResponse.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.webdav; + +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * WebdavResponse extends the HttpServletResponse by + * Webdav specific status codes and METHODS. + */ +public interface DavServletResponse extends HttpServletResponse { + + /** + * The 102 (Processing) status code is an interim response used to + * inform the client that the server has accepted the complete request, + * but has not yet completed it. + */ + int SC_PROCESSING = 102; + + /** + * Status code (207) indicating that the response requires + * providing status for multiple independent operations. + */ + int SC_MULTI_STATUS = 207; + + /** + * The 422 (Unprocessable Entity) status code means the server understands + * the content type of the request entity (hence a 415(Unsupported Media Type) + * status code is inappropriate), and the syntax of the request entity is + * correct (thus a 400 (Bad Request) status code is inappropriate) but was + * unable to process the contained instructions. For example, this error + * condition may occur if an XML request body contains well-formed (i.e., + * syntactically correct), but semantically erroneous XML instructions. + */ + int SC_UNPROCESSABLE_ENTITY = 422; + + /** + * Status code (423) indicating the destination resource of a + * method is locked, and either the request did not contain a + * valid Lock-Info header, or the Lock-Info header identifies + * a lock held by another principal. + */ + int SC_LOCKED = 423; + + /** + * Status code (424) indicating that the method could not be + * performed on the resource, because the requested action depended + * on another action which failed. + */ + int SC_FAILED_DEPENDENCY = 424; + + /** + * Status code (507) indicating that the resource does not have + * sufficient space to record the state of the resource after the + * execution of this method. + */ + int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 507; + + /** + * Send a response body given more detailed information about the error + * occurred. + * + * @param error + * @throws IOException + */ + public void sendError(DavException error) throws IOException; + + /** + * Send the multistatus response to the client. A multistatus response + * is returned in response to a successful PROPFIND and PROPPATCH request. + * In addition multistatus response is required response in case a COPY, + * MOVE, DELETE, LOCK or PROPPATCH request fails. + * + * @param multistatus + * @throws IOException + * @see #SC_MULTI_STATUS + */ + public void sendMultiStatus(MultiStatus multistatus) throws IOException; + + /** + * Send the lock response for a successful LOCK request, that was intended + * to refresh an existing lock. The locks array must contain at least + * a single element; the ActiveLock objects are then + * included in the lockdiscovery property of the response body as required + * by RFC 2518. + * + * @param locks + * @throws IOException + * @see DavConstants#PROPERTY_LOCKDISCOVERY + */ + public void sendRefreshLockResponse(ActiveLock[] locks) throws IOException; + + /** + * Generic method to return an Xml response body. + * + * @param serializable object that can be converted to the root Xml element + * of the document to be sent as response body. + * @param status Status code to be used with {@link #setStatus(int)}. + * @throws IOException + */ + public void sendXmlResponse(XmlSerializable serializable, int status) throws IOException; +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavSession.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavSession.java new file mode 100644 index 00000000000..90cc43e0a76 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavSession.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.webdav; + +/** + * DavSession allows to pass session information between request, + * response and resource(s). + */ +public interface DavSession { + + /** + * Adds a reference to this DavSession indicating that this + * session must not be discarded after completion of the current request. + * + * @param reference to be added. + */ + public void addReference(Object reference); + + /** + * Releasing a reference to this DavSession. If no more + * references are present, this session may be discarded. + * + * @param reference to be removed. + */ + public void removeReference(Object reference); + + /** + * Adds a lock token to this DavSession. + * + * @param token + */ + public void addLockToken(String token); + + /** + * Returns the lock tokens of this DavSession. + * + * @return + */ + public String[] getLockTokens(); + + /** + * Removes a lock token from this DavSession. + * + * @param token + */ + public void removeLockToken(String token); + +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavSessionProvider.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavSessionProvider.java new file mode 100644 index 00000000000..ff1f7062d87 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/DavSessionProvider.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.webdav; + +/** + * DavSessionProvider is an interface for components that + * can initiate and complete {@link DavSession}s. A provider is + * responsible for supplying references from a {@link WebdavRequest} + * to a {@link DavSession} when acquired and removing the references + * when released. + + */ +public interface DavSessionProvider { + + /** + * Acquires a DavSession. Upon success, the WebdavRequest will + * reference that session. + * + * A session will not be available if an exception is thrown. + * + * @param request + * @return true if the session was attached to the request; + * false otherwise. + * @throws DavException if a problem occurred while obtaining the session + */ + public boolean attachSession(WebdavRequest request) throws DavException; + + /** + * Releases the reference from the request to the session. + * + * @param request + */ + public void releaseSession(WebdavRequest request); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/MultiStatus.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/MultiStatus.java new file mode 100644 index 00000000000..79536b628aa --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/MultiStatus.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav; + +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * MultiStatus representing the content of a multistatus response body and + * allows to retrieve the Xml representation. + */ +public class MultiStatus implements DavConstants, XmlSerializable { + + /** + * Map collecting the responses for this multistatus, where every href must + * only occur one single time. + */ + private Map responses = new LinkedHashMap(); + + /** + * A general response description at the multistatus top level is used to + * provide a general message describing the overarching nature of the response. + * If this value is available an application may use it instead of + * presenting the individual response descriptions contained within the + * responses. + */ + private String responseDescription; + + /** + * Add response(s) to this multistatus, in order to build a multistatus for + * responding to a PROPFIND request. + * + * @param resource The resource to add property from + * @param propNameSet The requested property names of the PROPFIND request + * @param propFindType + * @param depth + */ + public void addResourceProperties(DavResource resource, DavPropertyNameSet propNameSet, + int propFindType, int depth) { + addResponse(new MultiStatusResponse(resource, propNameSet, propFindType)); + if (depth > 0 && resource.isCollection()) { + DavResourceIterator iter = resource.getMembers(); + while (iter.hasNext()) { + addResourceProperties(iter.nextResource(), propNameSet, propFindType, depth-1); + } + } + } + + /** + * Add response(s) to this multistatus, in order to build a multistatus e.g. + * in order to respond to a PROPFIND request. Please note, that in terms + * of PROPFIND, this method would correspond to a + * {@link DavConstants#PROPFIND_BY_PROPERTY} propfind type. + * + * @param resource The resource to add property from + * @param propNameSet The requested property names of the PROPFIND request + * @param depth + * @see #addResourceProperties(DavResource, DavPropertyNameSet, int, int) for + * the corresponding method that allows to specify the type. + */ + public void addResourceProperties(DavResource resource, DavPropertyNameSet propNameSet, + int depth) { + addResourceProperties(resource, propNameSet, PROPFIND_BY_PROPERTY, depth); + } + + /** + * Add response(s) to this multistatus, in order to build a multistatus + * as returned for COPY, MOVE, LOCK or DELETE requests resulting in an error + * with a resource other than the resource identified in the Request-URI. + * + * @param resource + * @param status + * @param depth + */ + public void addResourceStatus(DavResource resource, int status, int depth) { + addResponse(new MultiStatusResponse(resource.getHref(), status)); + if (depth > 0 && resource.isCollection()) { + DavResourceIterator iter = resource.getMembers(); + while (iter.hasNext()) { + addResourceStatus(iter.nextResource(), status, depth-1); + } + } + } + + /** + * Add a MultiStatusResponse element to this MultiStatus + *

        + * This method is synchronized to avoid the problem described in + * JCR-2755. + * + * @param response + */ + public synchronized void addResponse(MultiStatusResponse response) { + responses.put(response.getHref(), response); + } + + /** + * Returns the multistatus responses present as array. + *

        + * This method is synchronized to avoid the problem described in + * JCR-2755. + * + * @return array of all {@link MultiStatusResponse responses} present in this + * multistatus. + */ + public synchronized MultiStatusResponse[] getResponses() { + return responses.values().toArray(new MultiStatusResponse[responses.size()]); + } + + /** + * Set the response description. + * + * @param responseDescription + */ + public void setResponseDescription(String responseDescription) { + this.responseDescription = responseDescription; + } + + /** + * Returns the response description. + * + * @return responseDescription + */ + public String getResponseDescription() { + return responseDescription; + } + + /** + * Return the Xml representation of this MultiStatus. + * + * @return Xml document + * @param document + */ + public Element toXml(Document document) { + Element multistatus = DomUtil.createElement(document, XML_MULTISTATUS, NAMESPACE); + for (MultiStatusResponse resp : getResponses()) { + multistatus.appendChild(resp.toXml(document)); + } + if (responseDescription != null) { + Element respDesc = DomUtil.createElement(document, XML_RESPONSEDESCRIPTION, NAMESPACE, responseDescription); + multistatus.appendChild(respDesc); + } + return multistatus; + } + + /** + * Build a MultiStatus from the specified xml element. + * + * @param multistatusElement + * @return new MultiStatus instance. + * @throws IllegalArgumentException if the given document is null + * or does not provide the required element. + */ + public static MultiStatus createFromXml(Element multistatusElement) { + if (!DomUtil.matches(multistatusElement, XML_MULTISTATUS, NAMESPACE)) { + throw new IllegalArgumentException("DAV:multistatus element expected."); + } + + MultiStatus multistatus = new MultiStatus(); + + ElementIterator it = DomUtil.getChildren(multistatusElement, XML_RESPONSE, NAMESPACE); + while (it.hasNext()) { + Element respElem = it.nextElement(); + MultiStatusResponse response = MultiStatusResponse.createFromXml(respElem); + multistatus.addResponse(response); + } + + // optional response description on the multistatus element + multistatus.setResponseDescription(DomUtil.getChildText(multistatusElement, XML_RESPONSEDESCRIPTION, NAMESPACE)); + return multistatus; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/MultiStatusResponse.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/MultiStatusResponse.java new file mode 100644 index 00000000000..a0a5d0d7c86 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/MultiStatusResponse.java @@ -0,0 +1,486 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav; + +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.PropContainer; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * MultiStatusResponse represents the DAV:multistatus element defined + * by RFC 2518: + *

        + * <!ELEMENT response (href, ((href*, status)|(propstat+)), responsedescription?) >
        + * <!ELEMENT status (#PCDATA) >
        + * <!ELEMENT propstat (prop, status, responsedescription?) >
        + * <!ELEMENT responsedescription (#PCDATA) >
        + * <!ELEMENT prop ANY >
        + * 
        + */ +public class MultiStatusResponse implements XmlSerializable, DavConstants { + + private static final int TYPE_PROPSTAT = 0; + private static final int TYPE_HREFSTATUS = 1; + + /** + * The type of MultiStatusResponse + */ + private final int type; + + /** + * The content the 'href' element for this response + */ + private final String href; + + /** + * An optional response description. + */ + private final String responseDescription; + + /** + * Type of MultiStatus response: Href + Status + */ + private Status status; + + /** + * Type of MultiStatus response: PropStat Hashmap containing all status + */ + private HashMap statusMap = new HashMap(); + + private MultiStatusResponse(String href, String responseDescription, int type) { + if (!isValidHref(href)) { + throw new IllegalArgumentException("Invalid href ('" + href + "')"); + } + this.href = href; + this.responseDescription = responseDescription; + this.type = type; + } + + /** + * Constructs an WebDAV multistatus response + * + * @param href + * @param status + * @param responseDescription + */ + public MultiStatusResponse(String href, Status status, String responseDescription) { + this(href, responseDescription, TYPE_HREFSTATUS); + if (status == null) { + throw new IllegalArgumentException("Status must not be null in case of a multistatus reponse that consists of href + status only."); + } + this.status = status; + } + + /** + * Constructs an WebDAV multistatus response for a given resource. This + * would be used by COPY, MOVE, DELETE, LOCK that require a multistatus in + * case of error with a resource other than the resource identified in the + * Request-URI.
        + * The response description is set to null. + * + * @param href + * @param statusCode + */ + public MultiStatusResponse(String href, int statusCode) { + this(href, statusCode, null); + } + + /** + * Constructs an WebDAV multistatus response for a given resource. This + * would be used by COPY, MOVE, DELETE, LOCK that require a multistatus in + * case of error with a resource other than the resource identified in the + * Request-URI. + * + * @param href + * @param statusCode + * @param responseDescription + */ + public MultiStatusResponse(String href, int statusCode, String responseDescription) { + this(href, new Status(statusCode), responseDescription); + } + + /** + * Constructs an empty WebDAV multistatus response of type 'PropStat' + */ + public MultiStatusResponse(String href, String responseDescription) { + this(href, responseDescription, TYPE_PROPSTAT); + } + + /** + * Constructs a WebDAV multistatus response and retrieves the resource + * properties according to the given DavPropertyNameSet. + * + * @param resource + * @param propNameSet + */ + public MultiStatusResponse(DavResource resource, DavPropertyNameSet propNameSet) { + this(resource, propNameSet, PROPFIND_BY_PROPERTY); + } + + /** + * Constructs a WebDAV multistatus response and retrieves the resource + * properties according to the given DavPropertyNameSet. It + * adds all known property to the '200' set, while unknown properties are + * added to the '404' set. + *

        + * Note, that the set of property names is ignored in case of a {@link + * #PROPFIND_ALL_PROP} and {@link #PROPFIND_PROPERTY_NAMES} propFindType. + * + * @param resource The resource to retrieve the property from + * @param propNameSet The property name set as obtained from the request + * body. + * @param propFindType any of the following values: {@link + * #PROPFIND_ALL_PROP}, {@link #PROPFIND_BY_PROPERTY}, {@link + * #PROPFIND_PROPERTY_NAMES}, {@link #PROPFIND_ALL_PROP_INCLUDE} + */ + public MultiStatusResponse( + DavResource resource, DavPropertyNameSet propNameSet, + int propFindType) { + this(resource.getHref(), null, TYPE_PROPSTAT); + + if (propFindType == PROPFIND_PROPERTY_NAMES) { + // only property names requested + PropContainer status200 = getPropContainer(DavServletResponse.SC_OK, true); + for (DavPropertyName propName : resource.getPropertyNames()) { + status200.addContent(propName); + } + } else { + // all or a specified set of property and their values requested. + PropContainer status200 = getPropContainer(DavServletResponse.SC_OK, false); + + // Collection of missing property names for 404 responses + Set missing = new HashSet(propNameSet.getContent()); + + // Add requested properties or all non-protected properties, + // or non-protected properties plus requested properties (allprop/include) + if (propFindType == PROPFIND_BY_PROPERTY) { + // add explicitly requested properties (proptected or non-protected) + for (DavPropertyName propName : propNameSet) { + DavProperty prop = resource.getProperty(propName); + if (prop != null) { + status200.addContent(prop); + missing.remove(propName); + } + } + } else { + // add all non-protected properties + for (DavProperty property : resource.getProperties()) { + boolean allDeadPlusRfc4918LiveProperties = + propFindType == PROPFIND_ALL_PROP + || propFindType == PROPFIND_ALL_PROP_INCLUDE; + boolean wasRequested = missing.remove(property.getName()); + + if ((allDeadPlusRfc4918LiveProperties + && !property.isInvisibleInAllprop()) + || wasRequested) { + status200.addContent(property); + } + } + + // try if missing properties specified in the include section + // can be obtained using resource.getProperty + if (propFindType == PROPFIND_ALL_PROP_INCLUDE && !missing.isEmpty()) { + for (DavPropertyName propName : new HashSet(missing)) { + DavProperty prop = resource.getProperty(propName); + if (prop != null) { + status200.addContent(prop); + missing.remove(propName); + } + } + } + } + + if (!missing.isEmpty() && propFindType != PROPFIND_ALL_PROP) { + PropContainer status404 = getPropContainer(DavServletResponse.SC_NOT_FOUND, true); + for (DavPropertyName propName : missing) { + status404.addContent(propName); + } + } + } + } + + /** + * Returns the href + * + * @return href + * @see MultiStatusResponse#getHref() + */ + public String getHref() { + return href; + } + + /** + * @return responseDescription + * @see MultiStatusResponse#getResponseDescription() + */ + public String getResponseDescription() { + return responseDescription; + } + + /** + * Return an array listing all 'status' available is this response object. + * Note, that a the array contains a single element if this + * MultiStatusResponse defines an response consisting of + * href and status elements. + * + * @return + */ + public Status[] getStatus() { + Status[] sts; + if (type == TYPE_PROPSTAT) { + sts = new Status[statusMap.size()]; + Iterator iter = statusMap.keySet().iterator(); + for (int i = 0; iter.hasNext(); i++) { + Integer statusKey = iter.next(); + sts[i] = new Status(statusKey); + } + } else { + sts = new Status[] {status}; + } + return sts; + } + + /** + * @return {@code true} if the response is of type "propstat" (containing information about individual properties) + */ + public boolean isPropStat() { + return this.type == TYPE_PROPSTAT; + } + + /** + * @param document + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(org.w3c.dom.Document) + */ + public Element toXml(Document document) { + Element response = DomUtil.createElement(document, XML_RESPONSE, NAMESPACE); + // add '' + response.appendChild(DomUtil.hrefToXml(getHref(), document)); + if (type == TYPE_PROPSTAT) { + // add '' elements + for (Integer statusKey : statusMap.keySet()) { + Status st = new Status(statusKey); + PropContainer propCont = statusMap.get(statusKey); + if (!propCont.isEmpty()) { + Element propstat = DomUtil.createElement(document, XML_PROPSTAT, NAMESPACE); + propstat.appendChild(propCont.toXml(document)); + propstat.appendChild(st.toXml(document)); + response.appendChild(propstat); + } + } + } else { + // add a single '' element + // NOTE: a href+status response cannot be created with 'null' status + response.appendChild(status.toXml(document)); + } + // add the optional '' element + String description = getResponseDescription(); + if (description != null) { + Element desc = DomUtil.createElement(document, XML_RESPONSEDESCRIPTION, NAMESPACE); + DomUtil.setText(desc, description); + response.appendChild(desc); + } + return response; + } + //----------------------------------------------< type specific methods >--- + /** + * Adds a property to this response '200' propstat set. + * + * @param property the property to add + */ + public void add(DavProperty property) { + checkType(TYPE_PROPSTAT); + PropContainer status200 = getPropContainer(DavServletResponse.SC_OK, false); + status200.addContent(property); + } + + /** + * Adds a property name to this response '200' propstat set. + * + * @param propertyName the property name to add + */ + public void add(DavPropertyName propertyName) { + checkType(TYPE_PROPSTAT); + PropContainer status200 = getPropContainer(DavServletResponse.SC_OK, true); + status200.addContent(propertyName); + } + + /** + * Adds a property to this response + * + * @param property the property to add + * @param status the status of the response set to select + */ + public void add(DavProperty property, int status) { + checkType(TYPE_PROPSTAT); + PropContainer propCont = getPropContainer(status, false); + propCont.addContent(property); + } + + /** + * Adds a property name to this response + * + * @param propertyName the property name to add + * @param status the status of the response set to select + */ + public void add(DavPropertyName propertyName, int status) { + checkType(TYPE_PROPSTAT); + PropContainer propCont = getPropContainer(status, true); + propCont.addContent(propertyName); + } + + /** + * @param status + * @return + */ + private PropContainer getPropContainer(int status, boolean forNames) { + PropContainer propContainer = statusMap.get(status); + if (propContainer == null) { + if (forNames) { + propContainer = new DavPropertyNameSet(); + } else { + propContainer = new DavPropertySet(); + } + statusMap.put(status, propContainer); + } + return propContainer; + } + + private void checkType(int type) { + if (this.type != type) { + throw new IllegalStateException("The given MultiStatusResponse is not of the required type."); + } + } + + /** + * Get properties present in this response for the given status code. In + * case this MultiStatusResponse does not represent a 'propstat' response, + * always an empty {@link DavPropertySet} will be returned. + * + * @param status + * @return property set + */ + public DavPropertySet getProperties(int status) { + if (statusMap.containsKey(status)) { + PropContainer mapEntry = statusMap.get(status); + if (mapEntry != null && mapEntry instanceof DavPropertySet) { + return (DavPropertySet) mapEntry; + } + } + return new DavPropertySet(); + } + + /** + * Get property names present in this response for the given status code. In + * case this MultiStatusResponse does not represent a 'propstat' response, + * always an empty {@link DavPropertyNameSet} will be returned. + * + * @param status + * @return property names + */ + public DavPropertyNameSet getPropertyNames(int status) { + if (statusMap.containsKey(status)) { + PropContainer mapEntry = statusMap.get(status); + if (mapEntry != null) { + if (mapEntry instanceof DavPropertySet) { + DavPropertyNameSet set = new DavPropertyNameSet(); + for (DavPropertyName name : ((DavPropertySet) mapEntry).getPropertyNames()) { + set.add(name); + } + return set; + } else { + // is already a DavPropertyNameSet + return (DavPropertyNameSet) mapEntry; + } + } + } + return new DavPropertyNameSet(); + } + + /** + * Build a new response object from the given xml element. + * + * @param responseElement + * @return new MultiStatusResponse instance + * @throws IllegalArgumentException if the specified element is + * null or not a DAV:response element or if the mandatory + * DAV:href child is missing. + */ + public static MultiStatusResponse createFromXml(Element responseElement) { + if (!DomUtil.matches(responseElement, XML_RESPONSE, NAMESPACE)) { + throw new IllegalArgumentException("DAV:response element required."); + } + String href = DomUtil.getChildTextTrim(responseElement, XML_HREF, NAMESPACE); + if (href == null) { + throw new IllegalArgumentException("DAV:response element must contain a DAV:href element expected."); + } + String statusLine = DomUtil.getChildText(responseElement, XML_STATUS, NAMESPACE); + String responseDescription = DomUtil.getChildText(responseElement, XML_RESPONSEDESCRIPTION, NAMESPACE); + + MultiStatusResponse response; + if (statusLine != null) { + Status status = Status.parse(statusLine); + response = new MultiStatusResponse(href, status, responseDescription); + } else { + response = new MultiStatusResponse(href, responseDescription, TYPE_PROPSTAT); + // read propstat elements + ElementIterator it = DomUtil.getChildren(responseElement, XML_PROPSTAT, NAMESPACE); + while (it.hasNext()) { + Element propstat = it.nextElement(); + String propstatus = DomUtil.getChildText(propstat, XML_STATUS, NAMESPACE); + Element prop = DomUtil.getChildElement(propstat, XML_PROP, NAMESPACE); + if (propstatus != null && prop != null) { + int statusCode = Status.parse(propstatus).getStatusCode(); + ElementIterator propIt = DomUtil.getChildren(prop); + while (propIt.hasNext()) { + Element el = propIt.nextElement(); + /* + always build dav property from the given element, since + distinction between prop-names and properties not having + a value is not possible. + retrieval of the set of 'property names' is possible from + the given prop-set by calling DavPropertySet#getPropertyNameSet() + */ + DavProperty property = DefaultDavProperty.createFromXml(el); + response.add(property, statusCode); + } + } + } + } + return response; + } + + /** + * @param href + * @return false if the given href is null or empty string. + */ + private static boolean isValidHref(String href) { + return href != null && !"".equals(href); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/Status.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/Status.java new file mode 100644 index 00000000000..92bf1bffb2f --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/Status.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.webdav; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Status encapsulating the 'status' present in multistatus + * responses. + */ +public class Status implements DavConstants, XmlSerializable{ + + private static Logger log = LoggerFactory.getLogger(Status.class); + + private final String version; + private final int code; + private final String phrase; + + public Status(int code) { + version = "HTTP/1.1"; + this.code = code; + phrase = DavException.getStatusPhrase(code); + } + + public Status(String version, int code, String phrase) { + this.version = version; + this.code = code; + this.phrase = phrase; + } + + public int getStatusCode() { + return code; + } + + /** + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + String statusLine = version + " " + code + " " + phrase; + Element e = DomUtil.createElement(document, XML_STATUS, NAMESPACE); + DomUtil.setText(e, statusLine); + return e; + } + + /** + * Parse the given status line and return a new Status object. + * + * @param statusLine + * @return a new Status + */ + public static Status parse(String statusLine) { + if (statusLine == null) { + throw new IllegalArgumentException("Unable to parse status line from null xml element."); + } + Status status; + + // code copied from org.apache.commons.httpclient.StatusLine + int length = statusLine.length(); + int at = 0; + int start = 0; + try { + while (Character.isWhitespace(statusLine.charAt(at))) { + ++at; + ++start; + } + if (!"HTTP".equals(statusLine.substring(at, at += 4))) { + log.warn("Status-Line '" + statusLine + "' does not start with HTTP"); + } + //handle the HTTP-Version + at = statusLine.indexOf(' ', at); + if (at <= 0) { + log.warn("Unable to parse HTTP-Version from the status line: '" + statusLine + "'"); + } + String version = (statusLine.substring(start, at)).toUpperCase(); + //advance through spaces + while (statusLine.charAt(at) == ' ') { + at++; + } + //handle the Status-Code + int code; + int to = statusLine.indexOf(' ', at); + if (to < 0) { + to = length; + } + try { + code = Integer.parseInt(statusLine.substring(at, to)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Unable to parse status code from status line: '" + statusLine + "'"); + } + //handle the Reason-Phrase + String phrase = ""; + at = to + 1; + if (at < length) { + phrase = statusLine.substring(at).trim(); + } + + status = new Status(version, code, phrase); + + } catch (StringIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Status-Line '" + statusLine + "' is not valid"); + } + return status; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavRequest.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavRequest.java new file mode 100644 index 00000000000..09aa79f8896 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavRequest.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.webdav; + +import org.apache.jackrabbit.webdav.observation.ObservationDavServletRequest; +import org.apache.jackrabbit.webdav.ordering.OrderingDavServletRequest; +import org.apache.jackrabbit.webdav.transaction.TransactionDavServletRequest; +import org.apache.jackrabbit.webdav.version.DeltaVServletRequest; +import org.apache.jackrabbit.webdav.bind.BindServletRequest; + +/** + * The empty WebdavRequest interface collects the functionality + * defined by {@link org.apache.jackrabbit.webdav.DavServletRequest} encapsulating + * the core Webdav specification (RFC 2518) as well as the various extensions + * used for observation and transaction support, ordering of collections, search + * and versioning. + */ +public interface WebdavRequest extends DavServletRequest, + ObservationDavServletRequest, OrderingDavServletRequest, + TransactionDavServletRequest, DeltaVServletRequest, + BindServletRequest { +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavRequestImpl.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavRequestImpl.java new file mode 100644 index 00000000000..b10abe4fbfd --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavRequestImpl.java @@ -0,0 +1,1033 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.jackrabbit.webdav.bind.BindInfo; +import org.apache.jackrabbit.webdav.bind.RebindInfo; +import org.apache.jackrabbit.webdav.bind.UnbindInfo; +import org.apache.jackrabbit.webdav.header.CodedUrlHeader; +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.header.IfHeader; +import org.apache.jackrabbit.webdav.header.LabelHeader; +import org.apache.jackrabbit.webdav.header.OverwriteHeader; +import org.apache.jackrabbit.webdav.header.PollTimeoutHeader; +import org.apache.jackrabbit.webdav.header.TimeoutHeader; +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.observation.SubscriptionInfo; +import org.apache.jackrabbit.webdav.ordering.OrderPatch; +import org.apache.jackrabbit.webdav.ordering.OrderingConstants; +import org.apache.jackrabbit.webdav.ordering.Position; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.transaction.TransactionConstants; +import org.apache.jackrabbit.webdav.transaction.TransactionInfo; +import org.apache.jackrabbit.webdav.version.LabelInfo; +import org.apache.jackrabbit.webdav.version.MergeInfo; +import org.apache.jackrabbit.webdav.version.OptionsInfo; +import org.apache.jackrabbit.webdav.version.UpdateInfo; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * WebdavRequestImpl... + */ +public class WebdavRequestImpl implements WebdavRequest, DavConstants { + + private static Logger log = LoggerFactory.getLogger(WebdavRequestImpl.class); + + private final HttpServletRequest httpRequest; + private final DavLocatorFactory factory; + private final IfHeader ifHeader; + private final String hrefPrefix; + + private DavSession session; + + private int propfindType = PROPFIND_ALL_PROP; + private DavPropertyNameSet propfindProps; + private DavPropertySet proppatchSet; + private List proppatchList; + + /** + * Creates a new DavServletRequest with the given parameters. + */ + public WebdavRequestImpl(HttpServletRequest httpRequest, DavLocatorFactory factory) { + this(httpRequest, factory, true); + } + + /** + * Creates a new DavServletRequest with the given parameters. + * + * @param httpRequest + * @param factory + * @param createAbsoluteURI defines if we must create a absolute URI. if false a absolute path will be created + */ + public WebdavRequestImpl(HttpServletRequest httpRequest, DavLocatorFactory factory, boolean createAbsoluteURI) { + this.httpRequest = httpRequest; + this.factory = factory; + this.ifHeader = new IfHeader(httpRequest); + + String host = getHeader("Host"); + String scheme = getScheme(); + String uriPrefix = scheme + "://" + host + getContextPath(); + this.hrefPrefix = createAbsoluteURI ? uriPrefix : getContextPath(); + } + + /** + * Sets the session field and adds all lock tokens present with either the + * Lock-Token header or the If header to the given session object. + * + * @param session + * @see DavServletRequest#setDavSession(DavSession) + */ + public void setDavSession(DavSession session) { + this.session = session; + // set lock-tokens from header to the current session + if (session != null) { + String lt = getLockToken(); + if (lt != null) { + session.addLockToken(lt); + } + // add all token present in the the If header to the session as well. + Iterator it = ifHeader.getAllTokens(); + while (it.hasNext()) { + String ifHeaderToken = it.next(); + session.addLockToken(ifHeaderToken); + } + } + } + + /** + * @see DavServletRequest#getDavSession() + */ + public DavSession getDavSession() { + return session; + } + + /** + * Return a DavResourceLocator representing the request handle. + * + * @return locator of the requested resource + * @see DavServletRequest#getRequestLocator() + */ + public DavResourceLocator getRequestLocator() { + String path = getRequestURI(); + String ctx = getContextPath(); + if (path.startsWith(ctx)) { + path = path.substring(ctx.length()); + } + return factory.createResourceLocator(hrefPrefix, path); + } + + /** + * Parse the destination header field and return the path of the destination + * resource. + * + * @return path of the destination resource. + * @throws DavException + * @see #HEADER_DESTINATION + * @see DavServletRequest#getDestinationLocator + */ + public DavResourceLocator getDestinationLocator() throws DavException { + return getHrefLocator(httpRequest.getHeader(HEADER_DESTINATION), true); + } + + private DavResourceLocator getHrefLocator(String href, boolean forDestination) throws DavException { + String ref = href; + if (ref != null) { + //href should be a Simple-ref production as defined in RFC4918, so it is either an absolute URI + //or an absolute path + try { + URI uri = new URI(ref).normalize(); // normalize path (see JCR-3174) + String auth = uri.getAuthority(); + ref = uri.getRawPath(); + if (auth == null) { + //verify that href is an absolute path + if (ref.startsWith("//") || !ref.startsWith("/")) { + log.warn("expected absolute path but found " + ref); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else if (!auth.equals(httpRequest.getHeader("Host"))) { + //this looks like an unsupported cross-server operation, but of course a reverse-proxy + //might have rewritten the Host header. Since we can't find out, we have to reject anyway. + //Better use absolute paths in DAV:href elements! + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + } catch (URISyntaxException e) { + log.warn("malformed uri: " + href, e); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + // cut off the context path + String contextPath = httpRequest.getContextPath(); + if (ref.startsWith(contextPath)) { + ref = ref.substring(contextPath.length()); + } else { + //absolute path has to start with context path + throw new DavException(DavServletResponse.SC_FORBIDDEN); + } + } + if (factory instanceof AbstractLocatorFactory) { + return ((AbstractLocatorFactory)factory).createResourceLocator(hrefPrefix, ref, forDestination); + } + else { + return factory.createResourceLocator(hrefPrefix, ref); + } + } + + /** + * Parse a href and return the path of the resource. + * + * @return path of the resource identified by the href. + * @see org.apache.jackrabbit.webdav.bind.BindServletRequest#getHrefLocator + */ + public DavResourceLocator getHrefLocator(String href) throws DavException { + return getHrefLocator(href, false); + } + + /** + * Returns the path of the member resource of the request resource which is identified by the segment parameter. + * + * @return path of internal member resource. + */ + public DavResourceLocator getMemberLocator(String segment) { + String path = (this.getRequestLocator().getHref(true) + segment).substring(hrefPrefix.length()); + return factory.createResourceLocator(hrefPrefix, path); + } + + /** + * Return true if the overwrite header does not inhibit overwriting. + * + * @return true if the overwrite header requests 'overwriting' + * @see #HEADER_OVERWRITE + * @see DavServletRequest#isOverwrite() + */ + public boolean isOverwrite() { + return new OverwriteHeader(httpRequest).isOverwrite(); + } + + /** + * @see DavServletRequest#getDepth(int) + */ + public int getDepth(int defaultValue) { + return DepthHeader.parse(httpRequest, defaultValue).getDepth(); + } + + /** + * @see DavServletRequest#getDepth() + */ + public int getDepth() { + return getDepth(DEPTH_INFINITY); + } + + /** + * Parse the Timeout header and return a long representing the value. + * {@link #UNDEFINED_TIMEOUT} is used as default value if no header + * is available or if the parsing fails. + * + * @return milliseconds indicating length of the timeout. + * @see DavServletRequest#getTimeout() + * @see TimeoutHeader#parse(javax.servlet.http.HttpServletRequest, long) + */ + public long getTimeout() { + return TimeoutHeader.parse(httpRequest, UNDEFINED_TIMEOUT).getTimeout(); + } + + /** + * Retrieve the lock token from the 'Lock-Token' header. + * + * @return String representing the lock token sent in the Lock-Token header. + * @throws IllegalArgumentException If the value has not the correct format. + * @see #HEADER_LOCK_TOKEN + * @see DavServletRequest#getLockToken() + */ + public String getLockToken() { + return CodedUrlHeader.parse(httpRequest, HEADER_LOCK_TOKEN).getCodedUrl(); + } + + /** + * @see DavServletRequest#getRequestDocument() + */ + public Document getRequestDocument() throws DavException { + Document requestDocument = null; + /* + Don't attempt to parse the body if the content length header is 0. + NOTE: a value of -1 indicates that the length is unknown, thus we have + to parse the body. Note that http1.1 request using chunked transfer + coding will therefore not be detected here. + */ + if (httpRequest.getContentLength() == 0) { + return requestDocument; + } + // try to parse the request body + try { + InputStream in = httpRequest.getInputStream(); + if (in != null) { + // use a buffered input stream to find out whether there actually + // is a request body + InputStream bin = new BufferedInputStream(in); + bin.mark(1); + boolean isEmpty = -1 == bin.read(); + bin.reset(); + if (!isEmpty) { + requestDocument = DomUtil.parseDocument(bin); + } + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("Unable to build an XML Document from the request body: " + e.getMessage()); + } + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } catch (ParserConfigurationException e) { + if (log.isDebugEnabled()) { + log.debug("Unable to build an XML Document from the request body: " + e.getMessage()); + } + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } catch (SAXException e) { + if (log.isDebugEnabled()) { + log.debug("Unable to build an XML Document from the request body: " + e.getMessage()); + } + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + return requestDocument; + } + + /** + * Returns the type of PROPFIND as indicated by the request body. + * + * @return type of the PROPFIND request. Default value is {@link #PROPFIND_ALL_PROP allprops} + * @see DavServletRequest#getPropFindType() + */ + public int getPropFindType() throws DavException { + if (propfindProps == null) { + parsePropFindRequest(); + } + return propfindType; + } + + /** + * Returns the set of properties requested by the PROPFIND body or an + * empty set if the {@link #getPropFindType type} is either 'allprop' or + * 'propname'. + * + * @return set of properties requested by the PROPFIND body or an empty set. + * @see DavServletRequest#getPropFindProperties() + */ + public DavPropertyNameSet getPropFindProperties() throws DavException { + if (propfindProps == null) { + parsePropFindRequest(); + } + return propfindProps; + } + + /** + * Parse the propfind request body in order to determine the type of the propfind + * and the set of requested property. + * NOTE: An empty 'propfind' request body will be treated as request for all + * property according to the specification. + */ + private void parsePropFindRequest() throws DavException { + propfindProps = new DavPropertyNameSet(); + Document requestDocument = getRequestDocument(); + // propfind httpRequest with empty body >> retrieve all property + if (requestDocument == null) { + return; + } + + // propfind httpRequest with invalid body + Element root = requestDocument.getDocumentElement(); + if (!XML_PROPFIND.equals(root.getLocalName())) { + log.info("PropFind-Request has no tag."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "PropFind-Request has no tag."); + } + + DavPropertyNameSet include = null; + + ElementIterator it = DomUtil.getChildren(root); + int propfindTypeFound = 0; + + while (it.hasNext()) { + Element child = it.nextElement(); + String nodeName = child.getLocalName(); + if (NAMESPACE.getURI().equals(child.getNamespaceURI())) { + if (XML_PROP.equals(nodeName)) { + propfindType = PROPFIND_BY_PROPERTY; + propfindProps = new DavPropertyNameSet(child); + propfindTypeFound += 1; + } + else if (XML_PROPNAME.equals(nodeName)) { + propfindType = PROPFIND_PROPERTY_NAMES; + propfindTypeFound += 1; + } + else if (XML_ALLPROP.equals(nodeName)) { + propfindType = PROPFIND_ALL_PROP; + propfindTypeFound += 1; + } + else if (XML_INCLUDE.equals(nodeName)) { + include = new DavPropertyNameSet(); + ElementIterator pit = DomUtil.getChildren(child); + while (pit.hasNext()) { + include.add(DavPropertyName.createFromXml(pit.nextElement())); + } + } + } + } + + if (propfindTypeFound > 1) { + log.info("Multiple top-level propfind instructions"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Multiple top-level propfind instructions"); + } + + if (include != null) { + if (propfindType == PROPFIND_ALL_PROP) { + // special case: allprop with include extension + propfindType = PROPFIND_ALL_PROP_INCLUDE; + propfindProps = include; + } + else { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, " goes only with "); + + } + } + } + + /** + * Return a {@link List} of property change operations. Each entry + * is either of type {@link DavPropertyName}, indicating a <remove> + * operation, or of type {@link DavProperty}, indicating a <set> + * operation. Note that ordering is significant here. + * + * @return the list of change operations entries in the PROPPATCH request body + * @see DavServletRequest#getPropPatchChangeList() + */ + public List getPropPatchChangeList() throws DavException { + if (proppatchList == null) { + parsePropPatchRequest(); + } + return proppatchList; + } + + /** + * Parse the PROPPATCH request body. + */ + private void parsePropPatchRequest() throws DavException { + + proppatchSet = new DavPropertySet(); + proppatchList = new ArrayList(); + + Document requestDocument = getRequestDocument(); + + if (requestDocument == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid request body."); + } + + Element root = requestDocument.getDocumentElement(); + if (!DomUtil.matches(root, XML_PROPERTYUPDATE, NAMESPACE)) { + log.warn("PropPatch-Request has no tag."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "PropPatch-Request has no tag."); + } + + ElementIterator it = DomUtil.getChildren(root); + while (it.hasNext()) { + Element el = it.nextElement(); + if (DomUtil.matches(el, XML_SET, NAMESPACE)) { + Element propEl = DomUtil.getChildElement(el, XML_PROP, NAMESPACE); + if (propEl != null) { + ElementIterator properties = DomUtil.getChildren(propEl); + while (properties.hasNext()) { + DavProperty davProp = DefaultDavProperty.createFromXml(properties.nextElement()); + proppatchSet.add(davProp); + proppatchList.add(davProp); + } + } + } else if (DomUtil.matches(el, XML_REMOVE, NAMESPACE)) { + Element propEl = DomUtil.getChildElement(el, XML_PROP, NAMESPACE); + if (propEl != null) { + ElementIterator properties = DomUtil.getChildren(propEl); + while (properties.hasNext()) { + DavProperty davProp = DefaultDavProperty.createFromXml(properties.nextElement()); + proppatchSet.add(davProp); + proppatchList.add(davProp.getName()); + } + } + } else { + log.debug("Unknown element in DAV:propertyupdate: " + el.getNodeName()); + // unknown child elements are ignored + } + } + } + + /** + * {@link LockInfo} object encapsulating the information passed with a LOCK + * request if the LOCK request body was valid. If the request body is + * missing a 'refresh lock' request is assumed. The {@link LockInfo} + * then only provides timeout and isDeep property and returns true on + * {@link org.apache.jackrabbit.webdav.lock.LockInfo#isRefreshLock()} + * + * @return lock info object or null if an error occurred while + * parsing the request body. + * @throws DavException throws a 400 (Bad Request) DavException if a request + * body is present but does not start with a DAV:lockinfo element. Note however, + * that a non-existing request body is a valid request used to refresh + * an existing lock. + * @see DavServletRequest#getLockInfo() + */ + public LockInfo getLockInfo() throws DavException { + LockInfo lockInfo; + boolean isDeep = (getDepth(DEPTH_INFINITY) == DEPTH_INFINITY); + Document requestDocument = getRequestDocument(); + // check if XML request body is present. It SHOULD have one for + // 'create Lock' request and missing for a 'refresh Lock' request + if (requestDocument != null) { + Element root = requestDocument.getDocumentElement(); + if (root.getLocalName().equals(XML_LOCKINFO)) { + lockInfo = new LockInfo(root, getTimeout(), isDeep); + } else { + log.debug("Lock request body must start with a DAV:lockinfo element."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else { + lockInfo = new LockInfo(null, getTimeout(), isDeep); + } + return lockInfo; + } + + /** + * Test if the if header matches the given resource. The comparison is + * made with the {@link DavResource#getHref() + * resource href} and the token returned from an exclusive write lock present on + * the resource.
        + * NOTE: If either the If header or the resource is null or if + * the resource has not applied an exclusive write lock the preconditions are met. + * If in contrast the lock applied to the given resource returns a + * null lock token (e.g. for security reasons) or a lock token + * that does not match, the method will return false. + * + * @param resource Webdav resources being operated on + * @return true if the test is successful and the preconditions for the + * request processing are fulfilled. + * @see DavServletRequest#matchesIfHeader(DavResource) + * @see IfHeader#matches(String, String, String) + * @see DavResource#hasLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope) + * @see org.apache.jackrabbit.webdav.lock.ActiveLock#getToken() + */ + public boolean matchesIfHeader(DavResource resource) { + // no ifheader, no resource or no write lock on resource + // >> preconditions ok so far + if (!ifHeader.hasValue() || resource == null || !resource.hasLock(Type.WRITE, Scope.EXCLUSIVE)) { + return true; + } + + boolean isMatching = false; + String lockToken = resource.getLock(Type.WRITE, Scope.EXCLUSIVE).getToken(); + if (lockToken != null) { + isMatching = matchesIfHeader(resource.getHref(), lockToken, getStrongETag(resource)); + } // else: lockToken is null >> the if-header will not match. + + return isMatching; + } + + /** + * @see DavServletRequest#matchesIfHeader(String, String, String) + * @see IfHeader#matches(String, String, String) + */ + public boolean matchesIfHeader(String href, String token, String eTag) { + return ifHeader.matches(href, token, isStrongETag(eTag) ? eTag : ""); + } + + /** + * Returns the strong etag present on the given resource or empty string + * if either the resource does not provide any etag or if the etag is weak. + * + * @param resource + * @return strong etag or empty string. + */ + private String getStrongETag(DavResource resource) { + DavProperty prop = resource.getProperty(DavPropertyName.GETETAG); + if (prop != null && prop.getValue() != null) { + String etag = prop.getValue().toString(); + if (isStrongETag(etag)) { + return etag; + } + } + // no strong etag available + return ""; + } + + /** + * Returns true if the given string represents a strong etag. + * + * @param eTag + * @return true, if its a strong etag + */ + private static boolean isStrongETag(String eTag) { + return eTag != null && eTag.length() > 0 && !eTag.startsWith("W\\"); + } + + //-----------------------------< TransactionDavServletRequest Interface >--- + /** + * @see org.apache.jackrabbit.webdav.transaction.TransactionDavServletRequest#getTransactionId() + */ + public String getTransactionId() { + return CodedUrlHeader.parse(httpRequest, TransactionConstants.HEADER_TRANSACTIONID).getCodedUrl(); + } + + /** + * @see org.apache.jackrabbit.webdav.transaction.TransactionDavServletRequest#getTransactionInfo() + */ + public TransactionInfo getTransactionInfo() throws DavException { + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + return new TransactionInfo(requestDocument.getDocumentElement()); + } + return null; + } + + //-----------------------------< ObservationDavServletRequest Interface >--- + /** + * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletRequest#getSubscriptionId() + */ + public String getSubscriptionId() { + return CodedUrlHeader.parse(httpRequest, ObservationConstants.HEADER_SUBSCRIPTIONID).getCodedUrl(); + } + + /** + * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletRequest#getPollTimeout() + */ + public long getPollTimeout() { + return PollTimeoutHeader.parseHeader(httpRequest, 0).getTimeout(); + } + + /** + * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletRequest#getSubscriptionInfo() + */ + public SubscriptionInfo getSubscriptionInfo() throws DavException { + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + Element root = requestDocument.getDocumentElement(); + if (ObservationConstants.XML_SUBSCRIPTIONINFO.equals(root.getLocalName())) { + int depth = getDepth(DEPTH_0); + return new SubscriptionInfo(root, getTimeout(), depth == DEPTH_INFINITY); + } + } + return null; + } + + //--------------------------------< OrderingDavServletRequest Interface >--- + /** + * @see org.apache.jackrabbit.webdav.ordering.OrderingDavServletRequest#getOrderingType() + */ + public String getOrderingType() { + return getHeader(OrderingConstants.HEADER_ORDERING_TYPE); + } + + /** + * @see org.apache.jackrabbit.webdav.ordering.OrderingDavServletRequest#getPosition() + */ + public Position getPosition() { + String h = getHeader(OrderingConstants.HEADER_POSITION); + Position pos = null; + if (h != null) { + String[] typeNSegment = h.split("\\s"); + if (typeNSegment.length == 2) { + try { + pos = new Position(typeNSegment[0], typeNSegment[1]); + } catch (IllegalArgumentException e) { + log.error("Cannot parse Position header: " + e.getMessage()); + } + } + } + return pos; + } + + /** + * @return OrderPatch object representing the orderpatch request + * body or null if the + * @see org.apache.jackrabbit.webdav.ordering.OrderingDavServletRequest#getOrderPatch() + */ + public OrderPatch getOrderPatch() throws DavException { + OrderPatch op = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + Element root = requestDocument.getDocumentElement(); + op = OrderPatch.createFromXml(root); + } else { + log.error("Error while building xml document from ORDERPATH request body."); + } + return op; + } + + //-------------------------------------< DeltaVServletRequest interface >--- + /** + * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getLabel() + */ + public String getLabel() { + LabelHeader label = LabelHeader.parse(this); + if (label != null) { + return label.getLabel(); + } + return null; + } + + /** + * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getLabelInfo() + */ + public LabelInfo getLabelInfo() throws DavException { + LabelInfo lInfo = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + Element root = requestDocument.getDocumentElement(); + int depth = getDepth(DEPTH_0); + lInfo = new LabelInfo(root, depth); + } + return lInfo; + } + + /** + * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getMergeInfo() + */ + public MergeInfo getMergeInfo() throws DavException { + MergeInfo mInfo = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + mInfo = new MergeInfo(requestDocument.getDocumentElement()); + } + return mInfo; + } + + /** + * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getUpdateInfo() + */ + public UpdateInfo getUpdateInfo() throws DavException { + UpdateInfo uInfo = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + uInfo = new UpdateInfo(requestDocument.getDocumentElement()); + } + return uInfo; + } + + /** + * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getReportInfo() + */ + public ReportInfo getReportInfo() throws DavException { + ReportInfo rInfo = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + rInfo = new ReportInfo(requestDocument.getDocumentElement(), getDepth(DEPTH_0)); + } + return rInfo; + } + + /** + * @see org.apache.jackrabbit.webdav.version.DeltaVServletRequest#getOptionsInfo() + */ + public OptionsInfo getOptionsInfo() throws DavException { + OptionsInfo info = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + info = OptionsInfo.createFromXml(requestDocument.getDocumentElement()); + } + return info; + } + + /** + * @see org.apache.jackrabbit.webdav.bind.BindServletRequest#getRebindInfo() + */ + public RebindInfo getRebindInfo() throws DavException { + RebindInfo info = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + info = RebindInfo.createFromXml(requestDocument.getDocumentElement()); + } + return info; + } + + /** + * @see org.apache.jackrabbit.webdav.bind.BindServletRequest#getUnbindInfo() + */ + public UnbindInfo getUnbindInfo() throws DavException { + UnbindInfo info = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + info = UnbindInfo.createFromXml(requestDocument.getDocumentElement()); + } + return info; + } + + /** + * @see org.apache.jackrabbit.webdav.bind.BindServletRequest#getBindInfo() + */ + public BindInfo getBindInfo() throws DavException { + BindInfo info = null; + Document requestDocument = getRequestDocument(); + if (requestDocument != null) { + info = BindInfo.createFromXml(requestDocument.getDocumentElement()); + } + return info; + } + + //---------------------------------------< HttpServletRequest interface >--- + public String getAuthType() { + return httpRequest.getAuthType(); + } + + public Cookie[] getCookies() { + return httpRequest.getCookies(); + } + + public long getDateHeader(String s) { + return httpRequest.getDateHeader(s); + } + + public String getHeader(String s) { + return httpRequest.getHeader(s); + } + + public Enumeration getHeaders(String s) { + return httpRequest.getHeaders(s); + } + + public Enumeration getHeaderNames() { + return httpRequest.getHeaderNames(); + } + + public int getIntHeader(String s) { + return httpRequest.getIntHeader(s); + } + + public String getMethod() { + return httpRequest.getMethod(); + } + + public String getPathInfo() { + return httpRequest.getPathInfo(); + } + + public String getPathTranslated() { + return httpRequest.getPathTranslated(); + } + + public String getContextPath() { + return httpRequest.getContextPath(); + } + + public String getQueryString() { + return httpRequest.getQueryString(); + } + + public String getRemoteUser() { + return httpRequest.getRemoteUser(); + } + + public boolean isUserInRole(String s) { + return httpRequest.isUserInRole(s); + } + + public Principal getUserPrincipal() { + return httpRequest.getUserPrincipal(); + } + + public String getRequestedSessionId() { + return httpRequest.getRequestedSessionId(); + } + + public String getRequestURI() { + return httpRequest.getRequestURI(); + } + + public StringBuffer getRequestURL() { + return httpRequest.getRequestURL(); + } + + public String getServletPath() { + return httpRequest.getServletPath(); + } + + public HttpSession getSession(boolean b) { + return httpRequest.getSession(b); + } + + public HttpSession getSession() { + return httpRequest.getSession(); + } + + public boolean isRequestedSessionIdValid() { + return httpRequest.isRequestedSessionIdValid(); + } + + public boolean isRequestedSessionIdFromCookie() { + return httpRequest.isRequestedSessionIdFromCookie(); + } + + public boolean isRequestedSessionIdFromURL() { + return httpRequest.isRequestedSessionIdFromURL(); + } + + public boolean isRequestedSessionIdFromUrl() { + return httpRequest.isRequestedSessionIdFromUrl(); + } + + public Object getAttribute(String s) { + return httpRequest.getAttribute(s); + } + + public Enumeration getAttributeNames() { + return httpRequest.getAttributeNames(); + } + + public String getCharacterEncoding() { + return httpRequest.getCharacterEncoding(); + } + + public void setCharacterEncoding(String s) throws UnsupportedEncodingException { + httpRequest.setCharacterEncoding(s); + } + + public int getContentLength() { + return httpRequest.getContentLength(); + } + + public String getContentType() { + return httpRequest.getContentType(); + } + + public ServletInputStream getInputStream() throws IOException { + return httpRequest.getInputStream(); + } + + public String getParameter(String s) { + return httpRequest.getParameter(s); + } + + public Enumeration getParameterNames() { + return httpRequest.getParameterNames(); + } + + public String[] getParameterValues(String s) { + return httpRequest.getParameterValues(s); + } + + public Map getParameterMap() { + return httpRequest.getParameterMap(); + } + + public String getProtocol() { + return httpRequest.getProtocol(); + } + + public String getScheme() { + return httpRequest.getScheme(); + } + + public String getServerName() { + return httpRequest.getServerName(); + } + + public int getServerPort() { + return httpRequest.getServerPort(); + } + + public BufferedReader getReader() throws IOException { + return httpRequest.getReader(); + } + + public String getRemoteAddr() { + return httpRequest.getRemoteAddr(); + } + + public String getRemoteHost() { + return httpRequest.getRemoteHost(); + } + + public void setAttribute(String s, Object o) { + httpRequest.setAttribute(s, o); + } + + public void removeAttribute(String s) { + httpRequest.removeAttribute(s); + } + + public Locale getLocale() { + return httpRequest.getLocale(); + } + + public Enumeration getLocales() { + return httpRequest.getLocales(); + } + + public boolean isSecure() { + return httpRequest.isSecure(); + } + + public RequestDispatcher getRequestDispatcher(String s) { + return httpRequest.getRequestDispatcher(s); + } + + public String getRealPath(String s) { + return httpRequest.getRealPath(s); + } + + public int getRemotePort() { + return httpRequest.getRemotePort(); + } + + public String getLocalName() { + return httpRequest.getLocalName(); + } + + public String getLocalAddr() { + return httpRequest.getLocalAddr(); + } + + public int getLocalPort() { + return httpRequest.getLocalPort(); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavResponse.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavResponse.java new file mode 100644 index 00000000000..56749fb6aa7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavResponse.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.webdav; + +import org.apache.jackrabbit.webdav.observation.ObservationDavServletResponse; + +/** + * The empty WebdavResponse interface collects the functionality + * defined by {@link org.apache.jackrabbit.webdav.DavServletResponse} encapsulating + * for the core WebDAV specification (RFC 2518) as well as the various extensions + * used for observation and transaction support, ordering of collections, search + * and versioning. + */ +public interface WebdavResponse extends DavServletResponse, + ObservationDavServletResponse { +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavResponseImpl.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavResponseImpl.java new file mode 100644 index 00000000000..488aa428d51 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/WebdavResponseImpl.java @@ -0,0 +1,322 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav; + +import org.apache.jackrabbit.webdav.header.CodedUrlHeader; +import org.apache.jackrabbit.webdav.header.Header; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockDiscovery; +import org.apache.jackrabbit.webdav.observation.EventDiscovery; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.observation.Subscription; +import org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +/** + * WebdavResponseImpl implements the WebdavResponse interface. + */ +public class WebdavResponseImpl implements WebdavResponse { + + private static Logger log = LoggerFactory.getLogger(WebdavResponseImpl.class); + + private HttpServletResponse httpResponse; + + /** + * Create a new WebdavResponse + * + * @param httpResponse + */ + public WebdavResponseImpl(HttpServletResponse httpResponse) { + this(httpResponse, false); + } + + /** + * Create a new WebdavResponse + * + * @param httpResponse + * @param noCache + */ + public WebdavResponseImpl(HttpServletResponse httpResponse, boolean noCache) { + this.httpResponse = httpResponse; + if (noCache) { + /* set cache control headers */ + addHeader("Pragma", "No-cache"); // http1.0 + addHeader("Cache-Control", "no-cache"); // http1.1 + } + } + + /** + * If the specifid exception provides an error condition an Xml response body + * is sent providing more detailed information about the error. Otherwise only + * the error code and status phrase is sent back. + * + * @param exception + * @throws IOException + * @see DavServletResponse#sendError(org.apache.jackrabbit.webdav.DavException) + * @see #sendError(int, String) + * @see #sendXmlResponse(XmlSerializable, int) + */ + public void sendError(DavException exception) throws IOException { + if (!exception.hasErrorCondition()) { + httpResponse.sendError(exception.getErrorCode(), exception.getStatusPhrase()); + } else { + sendXmlResponse(exception, exception.getErrorCode()); + } + } + + /** + * Send a multistatus response. + * + * @param multistatus + * @throws IOException + * @see DavServletResponse#sendMultiStatus(org.apache.jackrabbit.webdav.MultiStatus) + */ + public void sendMultiStatus(MultiStatus multistatus) throws IOException { + sendXmlResponse(multistatus, SC_MULTI_STATUS); + } + + /** + * Send response body for a lock request that was intended to refresh one + * or several locks. + * + * @param locks + * @throws java.io.IOException + * @see DavServletResponse#sendRefreshLockResponse(org.apache.jackrabbit.webdav.lock.ActiveLock[]) + */ + public void sendRefreshLockResponse(ActiveLock[] locks) throws IOException { + DavPropertySet propSet = new DavPropertySet(); + propSet.add(new LockDiscovery(locks)); + sendXmlResponse(propSet, SC_OK); + } + + /** + * Send Xml response body. + * + * @param serializable + * @param status + * @throws IOException + * @see DavServletResponse#sendXmlResponse(XmlSerializable, int) + */ + public void sendXmlResponse(XmlSerializable serializable, int status) throws IOException { + httpResponse.setStatus(status); + + if (serializable != null) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + Document doc = DomUtil.createDocument(); + doc.appendChild(serializable.toXml(doc)); + + // JCR-2636: Need to use an explicit OutputStreamWriter + // instead of relying on the built-in UTF-8 serialization + // to avoid problems with surrogate pairs on Sun JRE 1.5. + Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8); + DomUtil.transformDocument(doc, writer); + writer.flush(); + + // TODO: Should this be application/xml? See JCR-1621 + httpResponse.setContentType("text/xml; charset=UTF-8"); + httpResponse.setContentLength(out.size()); + out.writeTo(httpResponse.getOutputStream()); + } catch (ParserConfigurationException e) { + log.error(e.getMessage()); + throw new IOException(e.getMessage()); + } catch (TransformerException e) { + log.error(e.getMessage()); + throw new IOException(e.getMessage()); + } catch (SAXException e) { + log.error(e.getMessage()); + throw new IOException(e.getMessage()); + } + } + } + + //----------------------------< ObservationDavServletResponse Interface >--- + /** + * + * @param subscription + * @throws IOException + * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletResponse#sendSubscriptionResponse(org.apache.jackrabbit.webdav.observation.Subscription) + */ + public void sendSubscriptionResponse(Subscription subscription) throws IOException { + String id = subscription.getSubscriptionId(); + if (id != null) { + Header h = new CodedUrlHeader(ObservationConstants.HEADER_SUBSCRIPTIONID, id); + httpResponse.setHeader(h.getHeaderName(), h.getHeaderValue()); + } + DavPropertySet propSet = new DavPropertySet(); + propSet.add(new SubscriptionDiscovery(subscription)); + sendXmlResponse(propSet, SC_OK); + } + + /** + * + * @param eventDiscovery + * @throws IOException + * @see org.apache.jackrabbit.webdav.observation.ObservationDavServletResponse#sendPollResponse(org.apache.jackrabbit.webdav.observation.EventDiscovery) + */ + public void sendPollResponse(EventDiscovery eventDiscovery) throws IOException { + sendXmlResponse(eventDiscovery, SC_OK); + } + + //--------------------------------------< HttpServletResponse interface >--- + public void addCookie(Cookie cookie) { + httpResponse.addCookie(cookie); + } + + public boolean containsHeader(String s) { + return httpResponse.containsHeader(s); + } + + public String encodeURL(String s) { + return httpResponse.encodeRedirectURL(s); + } + + public String encodeRedirectURL(String s) { + return httpResponse.encodeRedirectURL(s); + } + + public String encodeUrl(String s) { + return httpResponse.encodeUrl(s); + } + + public String encodeRedirectUrl(String s) { + return httpResponse.encodeRedirectURL(s); + } + + public void sendError(int i, String s) throws IOException { + httpResponse.sendError(i, s); + } + + public void sendError(int i) throws IOException { + httpResponse.sendError(i); + } + + public void sendRedirect(String s) throws IOException { + httpResponse.sendRedirect(s); + } + + public void setDateHeader(String s, long l) { + httpResponse.setDateHeader(s, l); + } + + public void addDateHeader(String s, long l) { + httpResponse.addDateHeader(s, l); + } + + public void setHeader(String s, String s1) { + httpResponse.setHeader(s, s1); + } + + public void addHeader(String s, String s1) { + httpResponse.addHeader(s, s1); + } + + public void setIntHeader(String s, int i) { + httpResponse.setIntHeader(s, i); + } + + public void addIntHeader(String s, int i) { + httpResponse.addIntHeader(s, i); + } + + public void setStatus(int i) { + httpResponse.setStatus(i); + } + + public void setStatus(int i, String s) { + httpResponse.setStatus(i, s); + } + + public String getCharacterEncoding() { + return httpResponse.getCharacterEncoding(); + } + + public ServletOutputStream getOutputStream() throws IOException { + return httpResponse.getOutputStream(); + } + + public PrintWriter getWriter() throws IOException { + return httpResponse.getWriter(); + } + + public void setContentLength(int i) { + httpResponse.setContentLength(i); + } + + public void setContentType(String s) { + httpResponse.setContentType(s); + } + + public void setBufferSize(int i) { + httpResponse.setBufferSize(i); + } + + public int getBufferSize() { + return httpResponse.getBufferSize(); + } + + public void flushBuffer() throws IOException { + httpResponse.flushBuffer(); + } + + public void resetBuffer() { + httpResponse.resetBuffer(); + } + + public boolean isCommitted() { + return httpResponse.isCommitted(); + } + + public void reset() { + httpResponse.reset(); + } + + public void setLocale(Locale locale) { + httpResponse.setLocale(locale); + } + + public Locale getLocale() { + return httpResponse.getLocale(); + } + + public String getContentType() { + return httpResponse.getContentType(); + } + + public void setCharacterEncoding(String charset) { + httpResponse.setCharacterEncoding(charset); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindConstants.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindConstants.java new file mode 100644 index 00000000000..e52f8c66845 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindConstants.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.webdav.bind; + +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.property.DavPropertyName; + +/** + * BindConstants provide constants for request and response + * headers, Xml elements and property names defined by + * the BIND specification. + */ +public interface BindConstants { + + /** + * The namespace + */ + public static final Namespace NAMESPACE = DavConstants.NAMESPACE; + + /** + * local names of XML elements used in the request bodies of the methods BIND, REBIND and UNBIND. + */ + public static final String XML_BIND = "bind"; + public static final String XML_REBIND = "rebind"; + public static final String XML_UNBIND = "unbind"; + public static final String XML_SEGMENT = "segment"; + public static final String XML_HREF = "href"; + public static final String XML_PARENT = "parent"; + + public static final String METHODS = "BIND, REBIND, UNBIND"; + + /* + * Webdav properties defined by the BIND specification. + */ + public static final DavPropertyName RESOURCEID = DavPropertyName.create("resource-id"); + public static final DavPropertyName PARENTSET = DavPropertyName.create("parent-set"); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindInfo.java new file mode 100644 index 00000000000..627640ebf51 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindInfo.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.webdav.bind; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Element; +import org.w3c.dom.Document; + +public class BindInfo implements XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(BindInfo.class); + + private String segment; + private String href; + + public BindInfo(String href, String segment) { + this.href = href; + this.segment = segment; + } + + public String getHref() { + return this.href; + } + + public String getSegment() { + return this.segment; + } + + /** + * Build an BindInfo object from the root element present + * in the request body. + * + * @param root the root element of the request body + * @return a BindInfo object containing segment and href + * @throws org.apache.jackrabbit.webdav.DavException if the BIND request is malformed + */ + public static BindInfo createFromXml(Element root) throws DavException { + if (!DomUtil.matches(root, BindConstants.XML_BIND, BindConstants.NAMESPACE)) { + log.warn("DAV:bind element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + String href = null; + String segment = null; + ElementIterator it = DomUtil.getChildren(root); + while (it.hasNext()) { + Element elt = it.nextElement(); + if (DomUtil.matches(elt, BindConstants.XML_SEGMENT, BindConstants.NAMESPACE)) { + if (segment == null) { + segment = DomUtil.getText(elt); + } else { + log.warn("unexpected multiple occurrence of DAV:segment element"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else if (DomUtil.matches(elt, BindConstants.XML_HREF, BindConstants.NAMESPACE)) { + if (href == null) { + href = DomUtil.getText(elt); + } else { + log.warn("unexpected multiple occurrence of DAV:href element"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else { + log.warn("unexpected element " + elt.getLocalName()); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } + if (href == null) { + log.warn("DAV:href element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + if (segment == null) { + log.warn("DAV:segment element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + return new BindInfo(href, segment); + } + + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(org.w3c.dom.Document) + */ + public Element toXml(Document document) { + Element bindElt = DomUtil.createElement(document, BindConstants.XML_BIND, BindConstants.NAMESPACE); + Element hrefElt = DomUtil.createElement(document, BindConstants.XML_HREF, BindConstants.NAMESPACE, this.href); + Element segElt = DomUtil.createElement(document, BindConstants.XML_SEGMENT, BindConstants.NAMESPACE, this.segment); + bindElt.appendChild(hrefElt); + bindElt.appendChild(segElt); + return bindElt; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindServletRequest.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindServletRequest.java new file mode 100644 index 00000000000..8d6c5168e9c --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindServletRequest.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.webdav.bind; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResourceLocator; + +/** + * BindServletRequest provides extension useful for functionality + * related to BIND specification. + */ +public interface BindServletRequest { + + /** + * Returns the {@link RebindInfo} present with the request + * + * @return {@link RebindInfo} object + * @throws org.apache.jackrabbit.webdav.DavException in case of an invalid or missing request body + */ + public RebindInfo getRebindInfo() throws DavException; + + /** + * Returns the {@link UnbindInfo} present with the request + * + * @return {@link UnbindInfo} object + * @throws org.apache.jackrabbit.webdav.DavException in case of an invalid or missing request body + */ + public UnbindInfo getUnbindInfo() throws DavException; + + /** + * Returns the {@link BindInfo} present with the request + * + * @return {@link BindInfo} object + * @throws org.apache.jackrabbit.webdav.DavException in case of an invalid or missing request body + */ + public BindInfo getBindInfo() throws DavException; + + /** + * Parses a href and returns the path of the resource. + * + * @return path of the resource identified by the href. + */ + public DavResourceLocator getHrefLocator(String href) throws DavException; + + /** + * Returns the path of the member resource of the request resource which is identified by the segment parameter. + * + * @return path of internal member resource. + */ + public DavResourceLocator getMemberLocator(String segment); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindableResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindableResource.java new file mode 100644 index 00000000000..86be8faa38a --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/BindableResource.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.webdav.bind; + +import java.util.Set; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; + +public interface BindableResource { + + /** + * Will add a new binding to the given collection referencing this resource. + * + * @param collection the collection to create the new binding in. + * @param newBinding the new binding + */ + public void bind(DavResource collection, DavResource newBinding) throws DavException; + + /** + * Will rebind the resource to the given collection. By definition, this is + * an atomic move operation. + * + * @param collection the collection to create the new binding in. + * @param newBinding the new binding + */ + public void rebind(DavResource collection, DavResource newBinding) throws DavException; + + /** + * Will retrieve a collection of parent elements of the bindable resource + * representing the parent set. + * + * @return newBinding the new binding + */ + public Set getParentElements(); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/ParentElement.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/ParentElement.java new file mode 100644 index 00000000000..3edd2ab1536 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/ParentElement.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.webdav.bind; + +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.w3c.dom.Element; +import org.w3c.dom.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ParentElement wraps en element of the parent set of a resource. A java.util.Set of + * ParentElement objects may serve as the value object of the ParentSet DavProperty. + */ +public class ParentElement implements XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(ParentElement.class); + + private final String href; + private final String segment; + + public ParentElement(String href, String segment) { + this.href = href; + this.segment = segment; + } + + public String getHref() { + return this.href; + } + + public String getSegment() { + return this.segment; + } + + /** + * Build an ParentElement object from an XML element DAV:parent + * + * @param root the DAV:parent element + * @return a ParentElement object + * @throws org.apache.jackrabbit.webdav.DavException if the DAV:parent element is malformed + */ + public static ParentElement createFromXml(Element root) throws DavException { + if (!DomUtil.matches(root, BindConstants.XML_PARENT, BindConstants.NAMESPACE)) { + log.warn("DAV:paret element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + String href = null; + String segment = null; + ElementIterator it = DomUtil.getChildren(root); + while (it.hasNext()) { + Element elt = it.nextElement(); + if (DomUtil.matches(elt, BindConstants.XML_SEGMENT, BindConstants.NAMESPACE)) { + if (segment == null) { + segment = DomUtil.getText(elt); + } else { + log.warn("unexpected multiple occurrence of DAV:segment element"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else if (DomUtil.matches(elt, BindConstants.XML_HREF, BindConstants.NAMESPACE)) { + if (href == null) { + href = DomUtil.getText(elt); + } else { + log.warn("unexpected multiple occurrence of DAV:href element"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else { + log.warn("unexpected element " + elt.getLocalName()); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } + if (href == null) { + log.warn("DAV:href element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + if (segment == null) { + log.warn("DAV:segment element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + return new ParentElement(href, segment); + } + + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(org.w3c.dom.Document) + */ + public Element toXml(Document document) { + Element parentElt = DomUtil.createElement(document, BindConstants.XML_PARENT, BindConstants.NAMESPACE); + Element hrefElt = DomUtil.createElement(document, BindConstants.XML_HREF, BindConstants.NAMESPACE, this.href); + Element segElt = DomUtil.createElement(document, BindConstants.XML_SEGMENT, BindConstants.NAMESPACE, this.segment); + parentElt.appendChild(hrefElt); + parentElt.appendChild(segElt); + return parentElt; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/ParentSet.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/ParentSet.java new file mode 100644 index 00000000000..01fae423087 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/ParentSet.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.webdav.bind; + +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; + +import java.util.Collection; + +/** + * ParentSet represents a DAV:parent-set property. + */ +public class ParentSet extends AbstractDavProperty> { + + private final Collection parents; + + /** + * Creates a new ParentSet from a collection of ParentElement objects. + * @param parents + */ + public ParentSet(Collection parents) { + super(BindConstants.PARENTSET, true); + this.parents = parents; + } + + /** + * @see org.apache.jackrabbit.webdav.property.AbstractDavProperty#getValue() + */ + public Collection getValue() { + return this.parents; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/RebindInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/RebindInfo.java new file mode 100644 index 00000000000..233b236a7a5 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/RebindInfo.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.webdav.bind; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Element; +import org.w3c.dom.Document; + +public class RebindInfo implements XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(RebindInfo.class); + + private String segment; + private String href; + + public RebindInfo(String href, String segment) { + this.href = href; + this.segment = segment; + } + + public String getHref() { + return this.href; + } + + public String getSegment() { + return this.segment; + } + + /** + * Build an RebindInfo object from the root element present + * in the request body. + * + * @param root the root element of the request body + * @return a RebindInfo object containing segment and href + * @throws org.apache.jackrabbit.webdav.DavException if the REBIND request is malformed + */ + public static RebindInfo createFromXml(Element root) throws DavException { + if (!DomUtil.matches(root, BindConstants.XML_REBIND, BindConstants.NAMESPACE)) { + log.warn("DAV:rebind element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + String href = null; + String segment = null; + ElementIterator it = DomUtil.getChildren(root); + while (it.hasNext()) { + Element elt = it.nextElement(); + if (DomUtil.matches(elt, BindConstants.XML_SEGMENT, BindConstants.NAMESPACE)) { + if (segment == null) { + segment = DomUtil.getText(elt); + } else { + log.warn("unexpected multiple occurrence of DAV:segment element"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else if (DomUtil.matches(elt, BindConstants.XML_HREF, BindConstants.NAMESPACE)) { + if (href == null) { + href = DomUtil.getText(elt); + } else { + log.warn("unexpected multiple occurrence of DAV:href element"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else { + log.warn("unexpected element " + elt.getLocalName()); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } + if (href == null) { + log.warn("DAV:href element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + if (segment == null) { + log.warn("DAV:segment element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + return new RebindInfo(href, segment); + } + + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(org.w3c.dom.Document) + */ + public Element toXml(Document document) { + Element rebindElt = DomUtil.createElement(document, BindConstants.XML_REBIND, BindConstants.NAMESPACE); + Element hrefElt = DomUtil.createElement(document, BindConstants.XML_HREF, BindConstants.NAMESPACE, this.href); + Element segElt = DomUtil.createElement(document, BindConstants.XML_SEGMENT, BindConstants.NAMESPACE, this.segment); + rebindElt.appendChild(hrefElt); + rebindElt.appendChild(segElt); + return rebindElt; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/UnbindInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/UnbindInfo.java new file mode 100644 index 00000000000..e3c7a3e2ded --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/UnbindInfo.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.webdav.bind; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Element; +import org.w3c.dom.Document; + +public class UnbindInfo implements XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(UnbindInfo.class); + + private String segment; + + private UnbindInfo() {} + + public UnbindInfo(String segment) { + this.segment = segment; + } + + public String getSegment() { + return this.segment; + } + + /** + * Build an UnbindInfo object from the root element present + * in the request body. + * + * @param root the root element of the request body + * @return a UnbindInfo object containing a segment identifier + * @throws org.apache.jackrabbit.webdav.DavException if the UNBIND request is malformed + */ + public static UnbindInfo createFromXml(Element root) throws DavException { + if (!DomUtil.matches(root, BindConstants.XML_UNBIND, BindConstants.NAMESPACE)) { + log.warn("DAV:unbind element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + String segment = null; + ElementIterator it = DomUtil.getChildren(root); + while (it.hasNext()) { + Element elt = it.nextElement(); + if (DomUtil.matches(elt, BindConstants.XML_SEGMENT, BindConstants.NAMESPACE)) { + if (segment == null) { + segment = DomUtil.getText(elt); + } else { + log.warn("unexpected multiple occurrence of DAV:segment element"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else { + log.warn("unexpected element " + elt.getLocalName()); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } + if (segment == null) { + log.warn("DAV:segment element expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + return new UnbindInfo(segment); + } + + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(org.w3c.dom.Document) + */ + public Element toXml(Document document) { + Element unbindElt = DomUtil.createElement(document, BindConstants.XML_UNBIND, BindConstants.NAMESPACE); + Element segElt = DomUtil.createElement(document, BindConstants.XML_SEGMENT, BindConstants.NAMESPACE, this.segment); + unbindElt.appendChild(segElt); + return unbindElt; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/package-info.java new file mode 100644 index 00000000000..05d41820c49 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/bind/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.bind; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/BaseDavRequest.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/BaseDavRequest.java new file mode 100644 index 00000000000..ca365b75f02 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/BaseDavRequest.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.webdav.client.methods; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.lock.LockDiscovery; +import org.apache.jackrabbit.webdav.observation.EventDiscovery; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.observation.Subscription; +import org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * Base class for HTTP request classes defined in this package. + */ +public abstract class BaseDavRequest extends HttpEntityEnclosingRequestBase { + + private static Logger log = LoggerFactory.getLogger(BaseDavRequest.class); + + public BaseDavRequest(URI uri) { + super(); + super.setURI(uri); + } + + /** + * Gets a {@link Document} representing the response body. + * @return document or {@code null} for null entity + * @throws IOException in case of I/O or XMP pasting problems + */ + public Document getResponseBodyAsDocument(HttpEntity entity) throws IOException { + + if (entity == null) { + return null; + } else { + // read response and try to build a xml document + InputStream in = entity.getContent(); + try { + return DomUtil.parseDocument(in); + } catch (ParserConfigurationException ex) { + throw new IOException("XML parser configuration error", ex); + } catch (SAXException ex) { + throw new IOException("XML parsing error", ex); + } finally { + in.close(); + } + } + } + + /** + * Return response body as {@link MultiStatus} object. + * @throws IllegalStateException when response does not represent a {@link MultiStatus} + * @throws DavException for failures in obtaining/parsing the response body + */ + public MultiStatus getResponseBodyAsMultiStatus(HttpResponse response) throws DavException { + try { + Document doc = getResponseBodyAsDocument(response.getEntity()); + if (doc == null) { + throw new DavException(response.getStatusLine().getStatusCode(), "no response body"); + } + return MultiStatus.createFromXml(doc.getDocumentElement()); + } catch (IOException ex) { + throw new DavException(response.getStatusLine().getStatusCode(), ex); + } + } + + /** + * Return response body as {@link LockDiscovery} object. + * @throws IllegalStateException when response does not represent a {@link LockDiscovery} + * @throws DavException for failures in obtaining/parsing the response body + */ + public LockDiscovery getResponseBodyAsLockDiscovery(HttpResponse response) throws DavException { + try { + Document doc = getResponseBodyAsDocument(response.getEntity()); + if (doc == null) { + throw new DavException(response.getStatusLine().getStatusCode(), "no response body"); + } + Element root = doc.getDocumentElement(); + + if (!DomUtil.matches(root, DavConstants.XML_PROP, DavConstants.NAMESPACE) + && DomUtil.hasChildElement(root, DavConstants.PROPERTY_LOCKDISCOVERY, DavConstants.NAMESPACE)) { + throw new DavException(response.getStatusLine().getStatusCode(), + "Missing DAV:prop response body in LOCK response."); + } + + Element lde = DomUtil.getChildElement(root, DavConstants.PROPERTY_LOCKDISCOVERY, DavConstants.NAMESPACE); + if (!DomUtil.hasChildElement(lde, DavConstants.XML_ACTIVELOCK, DavConstants.NAMESPACE)) { + throw new DavException(response.getStatusLine().getStatusCode(), + "The DAV:lockdiscovery must contain a least a single DAV:activelock in response to a successful LOCK request."); + } + + return LockDiscovery.createFromXml(lde); + } catch (IOException ex) { + throw new DavException(response.getStatusLine().getStatusCode(), ex); + } + } + + /** + * Return response body as {@link SubscriptionDiscovery} object. + * @throws IllegalStateException when response does not represent a {@link SubscriptionDiscovery} + * @throws DavException for failures in obtaining/parsing the response body + */ + public SubscriptionDiscovery getResponseBodyAsSubscriptionDiscovery(HttpResponse response) throws DavException { + try { + Document doc = getResponseBodyAsDocument(response.getEntity()); + if (doc == null) { + throw new DavException(response.getStatusLine().getStatusCode(), "no response body"); + } + Element root = doc.getDocumentElement(); + + if (!DomUtil.matches(root, DavConstants.XML_PROP, DavConstants.NAMESPACE) + && DomUtil.hasChildElement(root, ObservationConstants.SUBSCRIPTIONDISCOVERY.getName(), + ObservationConstants.SUBSCRIPTIONDISCOVERY.getNamespace())) { + throw new DavException(response.getStatusLine().getStatusCode(), + "Missing DAV:prop response body in SUBSCRIBE response."); + } + + Element sde = DomUtil.getChildElement(root, ObservationConstants.SUBSCRIPTIONDISCOVERY.getName(), + ObservationConstants.SUBSCRIPTIONDISCOVERY.getNamespace()); + SubscriptionDiscovery sd = SubscriptionDiscovery.createFromXml(sde); + if (((Subscription[]) sd.getValue()).length > 0) { + return sd; + } else { + throw new DavException(response.getStatusLine().getStatusCode(), + "Missing 'subscription' elements in SUBSCRIBE response body. At least a single subscription must be present if SUBSCRIBE was successful."); + } + } catch (IOException ex) { + throw new DavException(response.getStatusLine().getStatusCode(), ex); + } + } + + /** + * Return response body as {@link EventDiscovery} object. + * @throws IllegalStateException when response does not represent a {@link EventDiscovery} + * @throws DavException for failures in obtaining/parsing the response body + */ + public EventDiscovery getResponseBodyAsEventDiscovery(HttpResponse response) throws DavException { + try { + Document doc = getResponseBodyAsDocument(response.getEntity()); + if (doc == null) { + throw new DavException(response.getStatusLine().getStatusCode(), "no response body"); + } + return EventDiscovery.createFromXml(doc.getDocumentElement()); + } catch (IOException ex) { + throw new DavException(response.getStatusLine().getStatusCode(), ex); + } + } + + /** + * Check the response and throw when it is considered to represent a failure. + */ + public void checkSuccess(HttpResponse response) throws DavException { + if (!succeeded(response)) { + throw getResponseException(response); + } + } + + /** + * Obtain a {@link DavException} representing the response. + * @throws IllegalStateException when the response is considered to be successful + */ + public DavException getResponseException(HttpResponse response) { + if (succeeded(response)) { + String msg = "Cannot retrieve exception from successful response."; + log.warn(msg); + throw new IllegalStateException(msg); + } + + StatusLine st = response.getStatusLine(); + Element responseRoot = null; + try { + responseRoot = getResponseBodyAsDocument(response.getEntity()).getDocumentElement(); + } catch (IOException e) { + // non-parseable body -> use null element + } + + return new DavException(st.getStatusCode(), st.getReasonPhrase(), null, responseRoot); + } + + /** + * Check the provided {@link HttpResponse} for successful execution. The default implementation treats all + * 2xx status codes (RFC 7231, Section 6.3). + * Implementations can further restrict the accepted range of responses (or even check the response body). + */ + public boolean succeeded(HttpResponse response) { + int status = response.getStatusLine().getStatusCode(); + return status >= 200 && status <= 299; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpBind.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpBind.java new file mode 100644 index 00000000000..4c2161810f5 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpBind.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.bind.BindInfo; + +/** + * Represents an HTTP BIND request. + * + * @see RFC 5842, Section 4 + * @since 2.13.6 + */ +public class HttpBind extends BaseDavRequest { + + public HttpBind(URI uri, BindInfo info) throws IOException { + super(uri); + super.setEntity(XmlEntity.create(info)); + } + + public HttpBind(String uri, BindInfo info) throws IOException { + this(URI.create(uri), info); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_BIND; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK || statusCode == DavServletResponse.SC_CREATED; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCheckin.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCheckin.java new file mode 100644 index 00000000000..06b67aec070 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCheckin.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; + +/** + * Represents an HTTP CHECKIN request. + * + * @see RFC 3253, Section 4.4 + * @since 2.13.6 + */ +public class HttpCheckin extends BaseDavRequest { + + public HttpCheckin(URI uri) { + super(uri); + } + + public HttpCheckin(String uri) { + this(URI.create(uri)); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_CHECKIN; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_CREATED; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCheckout.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCheckout.java new file mode 100644 index 00000000000..a7f5e009bda --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCheckout.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; + +/** + * Represents an HTTP CHECKOUT request. + * + * @see RFC 3253, Section 4.3 + * @since 2.13.6 + */ +public class HttpCheckout extends BaseDavRequest { + + public HttpCheckout(URI uri) { + super(uri); + } + + public HttpCheckout(String uri) { + this(URI.create(uri)); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_CHECKOUT; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCopy.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCopy.java new file mode 100644 index 00000000000..4f5337b8732 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpCopy.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; + +/** + * Represents an HTTP COPY request. + * + * @see RFC 4918, Section 9.8 + * @since 2.13.6 + */ +public class HttpCopy extends BaseDavRequest { + + public HttpCopy(URI uri, URI dest, boolean overwrite, boolean shallow) { + super(uri); + super.setHeader(DavConstants.HEADER_DESTINATION, dest.toASCIIString()); + if (!overwrite) { + super.setHeader(DavConstants.HEADER_OVERWRITE, "F"); + } + if (shallow) { + super.setHeader("Depth", "0"); + } + } + + public HttpCopy(String uri, String dest, boolean overwrite, boolean shallow) { + this(URI.create(uri), URI.create(dest), overwrite, shallow); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_COPY; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_CREATED || statusCode == DavServletResponse.SC_NO_CONTENT; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpDelete.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpDelete.java new file mode 100644 index 00000000000..99c283bab96 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpDelete.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.jackrabbit.webdav.DavMethods; + +/** + * Represents an HTTP DELETE request. + * + * @see RFC 7231, Section 4.3.5 + * @since 2.13.6 + */ +public class HttpDelete extends BaseDavRequest { + + public HttpDelete(URI uri){ + super(uri); + } + + public HttpDelete(String uri) { + this(URI.create(uri)); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_DELETE; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpLabel.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpLabel.java new file mode 100644 index 00000000000..05c6f48ceea --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpLabel.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.version.LabelInfo; + +/** + * Represents an HTTP LABEL request. + * + * @see RFC 3253, Section 8.2 + * @since 2.13.6 + */ +public class HttpLabel extends BaseDavRequest { + + public HttpLabel(URI uri, LabelInfo labelInfo) throws IOException { + super(uri); + DepthHeader dh = new DepthHeader(labelInfo.getDepth()); + super.setHeader(dh.getHeaderName(), dh.getHeaderValue()); + super.setEntity(XmlEntity.create(labelInfo)); + } + + public HttpLabel(String uri, LabelInfo labelInfo) throws IOException { + this(URI.create(uri), labelInfo); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_LABEL; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpLock.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpLock.java new file mode 100644 index 00000000000..171776d17c5 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpLock.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.header.IfHeader; +import org.apache.jackrabbit.webdav.header.TimeoutHeader; +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Represents an HTTP LOCK request. + * + * @see RFC 4918, Section 9.10 + * @since 2.13.6 + */ +public class HttpLock extends BaseDavRequest { + + private static final Logger LOG = LoggerFactory.getLogger(HttpLock.class); + + private final boolean isRefresh; + + public HttpLock(URI uri, LockInfo lockInfo) throws IOException { + super(uri); + + TimeoutHeader th = new TimeoutHeader(lockInfo.getTimeout()); + super.setHeader(th.getHeaderName(), th.getHeaderValue()); + DepthHeader dh = new DepthHeader(lockInfo.isDeep()); + super.setHeader(dh.getHeaderName(), dh.getHeaderValue()); + + super.setEntity(XmlEntity.create(lockInfo)); + + isRefresh = false; + } + + public HttpLock(String uri, LockInfo lockInfo) throws IOException { + this(URI.create(uri), lockInfo); + } + + public HttpLock(URI uri, long timeout, String[] lockTokens) { + super(uri); + + TimeoutHeader th = new TimeoutHeader(timeout); + super.setHeader(th.getHeaderName(), th.getHeaderValue()); + IfHeader ifh = new IfHeader(lockTokens); + super.setHeader(ifh.getHeaderName(), ifh.getHeaderValue()); + isRefresh = true; + } + + public HttpLock(String uri, long timeout, String[] lockTokens) { + this(URI.create(uri), timeout, lockTokens); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_LOCK; + } + + public String getLockToken(HttpResponse response) { + Header[] ltHeader = response.getHeaders(DavConstants.HEADER_LOCK_TOKEN); + if (ltHeader == null || ltHeader.length == 0) { + return null; + } else if (ltHeader.length != 1) { + LOG.debug("Multiple 'Lock-Token' header fields in response for " + getURI() + ": " + Arrays.asList(ltHeader)); + return null; + } else { + String v = ltHeader[0].getValue().trim(); + if (!v.startsWith("<") || !v.endsWith(">")) { + LOG.debug("Invalid 'Lock-Token' header field in response for " + getURI() + ": " + Arrays.asList(ltHeader)); + return null; + } else { + return v.substring(1, v.length() - 1); + } + } + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + boolean lockTokenHeaderOk = isRefresh || null != getLockToken(response); + return lockTokenHeaderOk && (statusCode == DavServletResponse.SC_OK || statusCode == DavServletResponse.SC_CREATED); + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMerge.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMerge.java new file mode 100644 index 00000000000..b8db71dcff2 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMerge.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.version.MergeInfo; + +/** + * Represents an HTTP MERGE request. + * + * @see RFC 3253, Section 11.2 + * @since 2.13.6 + */ +public class HttpMerge extends BaseDavRequest { + + public HttpMerge(URI uri, MergeInfo mergeInfo) throws IOException { + super(uri); + super.setEntity(XmlEntity.create(mergeInfo)); + } + + public HttpMerge(String uri, MergeInfo mergeInfo) throws IOException { + this(URI.create(uri), mergeInfo); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_MERGE; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + // TODO: is this correct? + return statusCode == DavServletResponse.SC_MULTI_STATUS; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMkcol.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMkcol.java new file mode 100644 index 00000000000..5ecf6a237c1 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMkcol.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; + +/** + * Represents an HTTP MKCOL request. + * + * @see RFC 4918, Section 9.3 + * @since 2.13.6 + */ +public class HttpMkcol extends BaseDavRequest { + + public HttpMkcol(URI uri) { + super(uri); + } + + public HttpMkcol(String uri) { + this(URI.create(uri)); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_MKCOL; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_CREATED; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMkworkspace.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMkworkspace.java new file mode 100644 index 00000000000..1c14fe52ac3 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMkworkspace.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; + +/** + * Represents an HTTP MKWORKSPACE request. + * + * @see RFC 3253, Section 6.3 + * @since 2.13.6 + */ +public class HttpMkworkspace extends BaseDavRequest { + + public HttpMkworkspace(URI uri) { + super(uri); + } + + public HttpMkworkspace(String uri) { + this(URI.create(uri)); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_MKWORKSPACE; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_CREATED; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMove.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMove.java new file mode 100644 index 00000000000..2411e0891a8 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpMove.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; + +/** + * Represents an HTTP MOVE request. + * + * @see RFC 4918, Section 9.9 + * @since 2.13.6 + */ +public class HttpMove extends BaseDavRequest { + + public HttpMove(URI uri, URI dest, boolean overwrite) { + super(uri); + super.setHeader(DavConstants.HEADER_DESTINATION, dest.toASCIIString()); + if (!overwrite) { + super.setHeader(DavConstants.HEADER_OVERWRITE, "F"); + } + } + + public HttpMove(String uri, String dest, boolean overwrite) { + this(URI.create(uri), URI.create(dest), overwrite); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_MOVE; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_CREATED || statusCode == DavServletResponse.SC_NO_CONTENT; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpOptions.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpOptions.java new file mode 100644 index 00000000000..4986ee2b9a7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpOptions.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.webdav.client.methods; + +import java.net.URI; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.header.FieldValueParser; +import org.apache.jackrabbit.webdav.search.SearchConstants; + +/** + * Represents an HTTP OPTIONS request. + * + * @see RFC 7231, Section 4.3.7 + * @since 2.13.6 + */ +public class HttpOptions extends org.apache.http.client.methods.HttpOptions { + + public HttpOptions(URI uri) { + super(uri); + } + + public HttpOptions(String uri) { + super(URI.create(uri)); + } + + /** + * Compute the set of compliance classes returned in the "dav" header field + */ + public Set getDavComplianceClasses(HttpResponse response) { + Header[] headers = response.getHeaders(DavConstants.HEADER_DAV); + return parseTokenOrCodedUrlheaderField(headers, false); + } + + /** + * Compute set of search grammars returned in the "dasl" header field + */ + public Set getSearchGrammars(HttpResponse response) { + Header[] headers = response.getHeaders(SearchConstants.HEADER_DASL); + return parseTokenOrCodedUrlheaderField(headers, true); + } + + private Set parseTokenOrCodedUrlheaderField(Header[] headers, boolean removeBrackets) { + if (headers == null) { + return Collections.emptySet(); + } + else { + Set result = new HashSet(); + for (Header h : headers) { + for (String s : FieldValueParser.tokenizeList(h.getValue())) { + if (removeBrackets && s.startsWith("<") && s.endsWith(">")) { + s = s.substring(1, s.length() - 1); + } + result.add(s.trim()); + } + } + return Collections.unmodifiableSet(result); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpOrderpatch.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpOrderpatch.java new file mode 100644 index 00000000000..0e6f0fbf06e --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpOrderpatch.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.ordering.OrderPatch; + +/** + * Represents an HTTP ORDERPATCH request. + * + * @see RFC 3648, Section 5 + * @since 2.13.6 + */ +public class HttpOrderpatch extends BaseDavRequest { + + public HttpOrderpatch(URI uri, OrderPatch info) throws IOException { + super(uri); + super.setEntity(XmlEntity.create(info)); + } + + public HttpOrderpatch(String uri, OrderPatch info) throws IOException { + this(URI.create(uri), info); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_ORDERPATCH; + } + + @Override + public boolean succeeded(HttpResponse response) { + return response.getStatusLine().getStatusCode() == DavServletResponse.SC_OK; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpPoll.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpPoll.java new file mode 100644 index 00000000000..af7d620846b --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpPoll.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.PollTimeoutHeader; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; + +/** + * Represents an HTTP POLL request. + *

        + * Note that "POLL" is a custom HTTP extension, not defined in a standards paper. + * @since 2.13.6 + */ +public class HttpPoll extends BaseDavRequest { + + public HttpPoll(URI uri, String subscriptionId, long timeout) { + super(uri); + super.setHeader(ObservationConstants.HEADER_SUBSCRIPTIONID, subscriptionId); + if (timeout > 0) { + PollTimeoutHeader th = new PollTimeoutHeader(timeout); + super.setHeader(th.getHeaderName(), th.getHeaderValue()); + } + } + + public HttpPoll(String uri, String subscriptionId, long timeout) { + this(URI.create(uri), subscriptionId, timeout); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_POLL; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpPropfind.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpPropfind.java new file mode 100644 index 00000000000..e548db0697a --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpPropfind.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.PropfindInfo; + +/** + * Represents an HTTP PROPFIND request. + * + * @see RFC 4918, Section 9.1 + * @since 2.13.6 + */ +public class HttpPropfind extends BaseDavRequest { + + public HttpPropfind(URI uri, int propfindType, DavPropertyNameSet names, int depth) throws IOException { + super(uri); + + DepthHeader dh = new DepthHeader(depth); + super.setHeader(dh.getHeaderName(), dh.getHeaderValue()); + + PropfindInfo info = new PropfindInfo(propfindType, names); + super.setEntity(XmlEntity.create(info)); + } + + public HttpPropfind(URI uri, DavPropertyNameSet names, int depth) throws IOException { + this(uri, DavConstants.PROPFIND_BY_PROPERTY, names, depth); + } + + public HttpPropfind(URI uri, int propfindType, int depth) throws IOException { + this(uri, propfindType, new DavPropertyNameSet(), depth); + } + + public HttpPropfind(String uri, int propfindType, int depth) throws IOException { + this(URI.create(uri), propfindType, depth); + } + + public HttpPropfind(String uri, int propfindType, DavPropertyNameSet names, int depth) throws IOException { + this(URI.create(uri), propfindType, names, depth); + } + + public HttpPropfind(String uri, DavPropertyNameSet names, int depth) throws IOException { + this(URI.create(uri), names, depth); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_PROPFIND; + } + + @Override + public boolean succeeded(HttpResponse response) { + return response.getStatusLine().getStatusCode() == DavServletResponse.SC_MULTI_STATUS; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpProppatch.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpProppatch.java new file mode 100644 index 00000000000..2cd35f8f0f6 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpProppatch.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; +import java.util.List; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.property.ProppatchInfo; + +/** + * Represents an HTTP PROPPATCH request. + * + * @see RFC 4918, Section 9.2 + * @since 2.13.6 + */ +public class HttpProppatch extends BaseDavRequest { + + // private DavPropertyNameSet propertyNames; + + public HttpProppatch(URI uri, ProppatchInfo info) throws IOException { + super(uri); + super.setEntity(XmlEntity.create(info)); + // this.propertyNames = info.getAffectedProperties(); + } + + public HttpProppatch(URI uri, List changeList) throws IOException { + this(uri, new ProppatchInfo(changeList)); + } + + public HttpProppatch(URI uri, DavPropertySet setProperties, DavPropertyNameSet removeProperties) throws IOException { + this(uri, new ProppatchInfo(setProperties, removeProperties)); + } + + public HttpProppatch(String uri, List changeList) throws IOException { + this(URI.create(uri), new ProppatchInfo(changeList)); + } + + public HttpProppatch(String uri, DavPropertySet setProperties, DavPropertyNameSet removeProperties) throws IOException { + this(URI.create(uri), new ProppatchInfo(setProperties, removeProperties)); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_PROPPATCH; + } + + @Override + public boolean succeeded(HttpResponse response) { + return response.getStatusLine().getStatusCode() == DavServletResponse.SC_MULTI_STATUS; + + // disabled code that fails for current PROPPATCH behavior of Jackrabbit +// MultiStatusResponse responses[] = super.getResponseBodyAsMultiStatus(response).getResponses(); +// if (responses.length != 1) { +// throw new DavException(DavServletResponse.SC_MULTI_STATUS, +// "PROPPATCH failed: Expected exactly one multistatus response element, but got " + responses.length); +// } +// DavPropertyNameSet okSet = responses[0].getPropertyNames(DavServletResponse.SC_OK); +// if (okSet.isEmpty()) { +// throw new DavException(DavServletResponse.SC_MULTI_STATUS, +// "PROPPATCH failed: No 'OK' response found for resource " + responses[0].getHref()); +// } else { +// DavPropertyNameIterator it = propertyNames.iterator(); +// while (it.hasNext()) { +// DavPropertyName pn = it.nextPropertyName(); +// boolean success = okSet.remove(pn); +// if (!success) { +// throw new DavException(DavServletResponse.SC_MULTI_STATUS, +// "PROPPATCH failed: Property name not present in multistatus response: " + pn); +// } +// } +// } +// if (!okSet.isEmpty()) { +// StringBuilder b = new StringBuilder(); +// DavPropertyNameIterator it = okSet.iterator(); +// while (it.hasNext()) { +// b.append(it.nextPropertyName().toString()).append("; "); +// } +// throw new DavException(DavServletResponse.SC_MULTI_STATUS, +// "PROPPATCH failed: The following properties outside of the original request where set or removed: " + b.toString()); +// } +// return true; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpRebind.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpRebind.java new file mode 100644 index 00000000000..e72dea89008 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpRebind.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.bind.RebindInfo; + +/** + * Represents an HTTP REBIND request. + * + * @see RFC 5842, Section 6 + * @since 2.13.6 + */ +public class HttpRebind extends BaseDavRequest { + + public HttpRebind(URI uri, RebindInfo info) throws IOException { + super(uri); + super.setEntity(XmlEntity.create(info)); + } + + public HttpRebind(String uri, RebindInfo info) throws IOException { + this(URI.create(uri), info); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_REBIND; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK || statusCode == DavServletResponse.SC_CREATED; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpReport.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpReport.java new file mode 100644 index 00000000000..efb20b6caf7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpReport.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; + +/** + * Represents an HTTP REPORT request. + * + * @see RFC 3253, Section 3.6 + * @since 2.13.6 + */ +public class HttpReport extends BaseDavRequest { + + private final boolean isDeep; + + public HttpReport(URI uri, ReportInfo reportInfo) throws IOException { + super(uri); + DepthHeader dh = new DepthHeader(reportInfo.getDepth()); + isDeep = reportInfo.getDepth() > DavConstants.DEPTH_0; + super.setHeader(dh.getHeaderName(), dh.getHeaderValue()); + super.setEntity(XmlEntity.create(reportInfo)); + } + + public HttpReport(String uri, ReportInfo reportInfo) throws IOException { + this(URI.create(uri), reportInfo); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_REPORT; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + if (isDeep) { + return statusCode == DavServletResponse.SC_MULTI_STATUS; + } else { + return statusCode == DavServletResponse.SC_OK || statusCode == DavServletResponse.SC_MULTI_STATUS; + } + + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpSearch.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpSearch.java new file mode 100644 index 00000000000..f230f2dfbf1 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpSearch.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.search.SearchInfo; + +/** + * Represents an HTTP SEARCH request. + * + * @see RFC 5323, Section 2 + * @since 2.13.6 + */ +public class HttpSearch extends BaseDavRequest { + + public HttpSearch(URI uri, SearchInfo searchInfo) throws IOException { + super(uri); + super.setEntity(XmlEntity.create(searchInfo)); + } + + public HttpSearch(String uri, SearchInfo searchInfo) throws IOException { + this(URI.create(uri), searchInfo); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_SEARCH; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_MULTI_STATUS; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpSubscribe.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpSubscribe.java new file mode 100644 index 00000000000..6bdbb7adcdc --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpSubscribe.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.CodedUrlHeader; +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.header.TimeoutHeader; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; +import org.apache.jackrabbit.webdav.observation.SubscriptionInfo; + +/** + * Represents an HTTP SUBSCRIBE request. + *

        + * Note that "SUBSCRIBE" is a custom HTTP extension, not defined in a standards paper. + * @since 2.13.6 + */ +public class HttpSubscribe extends BaseDavRequest { + + public HttpSubscribe(URI uri, SubscriptionInfo info, String subscriptionId) throws IOException { + super(uri); + if (info == null) { + throw new IllegalArgumentException("SubscriptionInfo must not be null."); + } + // optional subscriptionId (only required to modify an existing + // subscription) + if (subscriptionId != null) { + CodedUrlHeader h = new CodedUrlHeader(ObservationConstants.HEADER_SUBSCRIPTIONID, subscriptionId); + super.setHeader(h.getHeaderName(), h.getHeaderValue()); + } + // optional timeout header + long to = info.getTimeOut(); + if (to != DavConstants.UNDEFINED_TIMEOUT) { + TimeoutHeader h = new TimeoutHeader(info.getTimeOut()); + super.setHeader(h.getHeaderName(), h.getHeaderValue()); + } + // always set depth header since value is boolean flag + DepthHeader dh = new DepthHeader(info.isDeep()); + super.setHeader(dh.getHeaderName(), dh.getHeaderValue()); + super.setEntity(XmlEntity.create(info)); + } + + public HttpSubscribe(String uri, SubscriptionInfo info, String subscriptionId) throws IOException { + this(URI.create(uri), info, subscriptionId); + } + + public String getSubscriptionId(HttpResponse response) { + org.apache.http.Header sbHeader = response.getFirstHeader(ObservationConstants.HEADER_SUBSCRIPTIONID); + if (sbHeader != null) { + CodedUrlHeader cuh = new CodedUrlHeader(ObservationConstants.HEADER_SUBSCRIPTIONID, sbHeader.getValue()); + return cuh.getCodedUrl(); + } + else { + return null; + } + } + + @Override + public String getMethod() { + return DavMethods.METHOD_SUBSCRIBE; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnbind.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnbind.java new file mode 100644 index 00000000000..10e7f4a5b50 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnbind.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.bind.UnbindInfo; + +/** + * Represents an HTTP UNBIND request. + * + * @see RFC 5842, Section 5 + * @since 2.13.6 + */ +public class HttpUnbind extends BaseDavRequest { + + public HttpUnbind(URI uri, UnbindInfo info) throws IOException { + super(uri); + super.setEntity(XmlEntity.create(info)); + } + + public HttpUnbind(String uri, UnbindInfo info) throws IOException { + this(URI.create(uri), info); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_UNBIND; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK || statusCode == DavServletResponse.SC_NO_CONTENT; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnlock.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnlock.java new file mode 100644 index 00000000000..aee34736539 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnlock.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.CodedUrlHeader; + +/** + * Represents an HTTP UNLOCK request. + * + * @see RFC 4918, Section 9.11 + * @since 2.13.6 + */ +public class HttpUnlock extends BaseDavRequest { + + public HttpUnlock(URI uri, String lockToken) { + super(uri); + CodedUrlHeader lth = new CodedUrlHeader(DavConstants.HEADER_LOCK_TOKEN, lockToken); + super.setHeader(lth.getHeaderName(), lth.getHeaderValue()); + } + + public HttpUnlock(String uri, String lockToken) { + this(URI.create(uri), lockToken); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_UNLOCK; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK || statusCode == DavServletResponse.SC_NO_CONTENT; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnsubscribe.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnsubscribe.java new file mode 100644 index 00000000000..522f17fce6e --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUnsubscribe.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.observation.ObservationConstants; + +/** + * Represents an HTTP UNSUBSCRIBE request. + *

        + * Note that "UNSUBSCRIBE" is a custom HTTP extension, not defined in a standards paper. + * @since 2.13.6 + */ +public class HttpUnsubscribe extends BaseDavRequest { + + public HttpUnsubscribe(URI uri, String subscriptionId) { + super(uri); + super.setHeader(ObservationConstants.HEADER_SUBSCRIPTIONID, subscriptionId); + } + + public HttpUnsubscribe(String uri, String subscriptionId) { + this(URI.create(uri), subscriptionId); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_UNSUBSCRIBE; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_NO_CONTENT; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUpdate.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUpdate.java new file mode 100644 index 00000000000..019a24fff62 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpUpdate.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.webdav.client.methods; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.version.UpdateInfo; + +/** + * Represents an HTTP UPDATE request. + * + * @see RFC 3253, Section 7.1 + * @since 2.13.6 + */ +public class HttpUpdate extends BaseDavRequest { + + public HttpUpdate(URI uri, UpdateInfo updateInfo) throws IOException { + super(uri); + super.setEntity(XmlEntity.create(updateInfo)); + } + + public HttpUpdate(String uri, UpdateInfo updateInfo) throws IOException { + this(URI.create(uri), updateInfo); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_UPDATE; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_MULTI_STATUS; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpVersionControl.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpVersionControl.java new file mode 100644 index 00000000000..e23a601bee7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/HttpVersionControl.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.webdav.client.methods; + +import java.net.URI; + +import org.apache.http.HttpResponse; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavServletResponse; + +/** + * Represents an HTTP VERSION-CONTROL request. + * + * @see RFC 3253, Section 3.5 + * @since 2.13.6 + */ +public class HttpVersionControl extends BaseDavRequest { + + public HttpVersionControl(URI uri) { + super(uri); + } + + public HttpVersionControl(String uri) { + this(URI.create(uri)); + } + + @Override + public String getMethod() { + return DavMethods.METHOD_VERSION_CONTROL; + } + + @Override + public boolean succeeded(HttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + return statusCode == DavServletResponse.SC_OK; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/XmlEntity.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/XmlEntity.java new file mode 100644 index 00000000000..859bbe40442 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/XmlEntity.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.webdav.client.methods; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +import org.apache.http.HttpEntity; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * Utility methods for creating request entities from {@link Document}s or {@link XmlSerializable}s. + */ +public class XmlEntity { + + private static Logger LOG = LoggerFactory.getLogger(XmlEntity.class); + + private static ContentType CT = ContentType.create("application/xml", "UTF-8"); + + public static HttpEntity create(Document doc) throws IOException { + try { + ByteArrayOutputStream xml = new ByteArrayOutputStream(); + DomUtil.transformDocument(doc, xml); + return new ByteArrayEntity(xml.toByteArray(), CT); + } catch (TransformerException ex) { + LOG.error(ex.getMessage()); + throw new IOException(ex); + } catch (SAXException ex) { + LOG.error(ex.getMessage()); + throw new IOException(ex); + } + } + + public static HttpEntity create(XmlSerializable payload) throws IOException { + try { + Document doc = DomUtil.createDocument(); + doc.appendChild(payload.toXml(doc)); + return create(doc); + } catch (ParserConfigurationException ex) { + LOG.error(ex.getMessage()); + throw new IOException(ex); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/package-info.java new file mode 100644 index 00000000000..b667d39c615 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/client/methods/package-info.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. + */ + +/** + * Provides classes for use with the Apache HttpClient, supporting WebDAV + * request methods. + *

        + * This version also contains classes for use with the obsolete "Commons + * HttpClient"; they have been marked "deprecated" and will be removed in the + * next major release. + * + * @see JCR-2406 + * @see https://hc.apache.org/httpcomponents-client-4.5.x/ + */ +@org.osgi.annotation.versioning.Version("2.0.0") +package org.apache.jackrabbit.webdav.client.methods; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/CodedUrlHeader.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/CodedUrlHeader.java new file mode 100644 index 00000000000..7b51bb367b7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/CodedUrlHeader.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.webdav.header; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; + +/** + * CodedUrlHeader... + */ +public class CodedUrlHeader implements Header { + + private static Logger log = LoggerFactory.getLogger(CodedUrlHeader.class); + + private final String headerName; + private final String headerValue; + + public CodedUrlHeader(String headerName, String headerValue) { + this.headerName = headerName; + if (headerValue != null && !(headerValue.startsWith("<") && headerValue.endsWith(">"))) { + headerValue = "<" + headerValue + ">"; + } + this.headerValue = headerValue; + } + + /** + * Return the name of the header + * + * @return header name + * @see Header#getHeaderName() + */ + public String getHeaderName() { + return headerName; + } + + /** + * Return the value of the header + * + * @return value + * @see Header#getHeaderValue() + */ + public String getHeaderValue() { + return headerValue; + } + + /** + * Returns the token present in the header value or null. + * If the header contained multiple tokens separated by ',' the first value + * is returned. + * + * @return token present in the CodedURL header or null if + * the header is not present. + * @see #getCodedUrls() + */ + public String getCodedUrl() { + String[] codedUrls = getCodedUrls(); + return (codedUrls != null) ? codedUrls[0] : null; + } + + /** + * Return an array of coded urls as present in the header value or null if + * no value is present. + * + * @return array of coded urls + */ + public String[] getCodedUrls() { + String[] codedUrls = null; + if (headerValue != null) { + String[] values = headerValue.split(","); + codedUrls = new String[values.length]; + for (int i = 0; i < values.length; i++) { + int p1 = values[i].indexOf('<'); + if (p1<0) { + throw new IllegalArgumentException("Invalid CodedURL header value:" + values[i]); + } + int p2 = values[i].indexOf('>', p1); + if (p2<0) { + throw new IllegalArgumentException("Invalid CodedURL header value:" + values[i]); + } + codedUrls[i] = values[i].substring(p1+1, p2); + } + } + return codedUrls; + } + + /** + * Retrieves the header with the given name and builds a new CodedUrlHeader. + * + * @param request + * @param headerName + * @return new CodedUrlHeader instance + */ + public static CodedUrlHeader parse(HttpServletRequest request, String headerName) { + String headerValue = request.getHeader(headerName); + return new CodedUrlHeader(headerName, headerValue); + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/DepthHeader.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/DepthHeader.java new file mode 100644 index 00000000000..b66ecca9e55 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/DepthHeader.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.webdav.header; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; + +/** + * DepthHeader... + */ +public class DepthHeader implements Header, DavConstants { + + private static Logger log = LoggerFactory.getLogger(DepthHeader.class); + + private final int depth; + + /** + * Create a new DepthHeader from the given integer. + * + * @param depth + */ + public DepthHeader(int depth) { + if (depth == DEPTH_0 || depth == DEPTH_1 || depth == DEPTH_INFINITY) { + this.depth = depth; + } else { + throw new IllegalArgumentException("Invalid depth: " + depth); + } + } + + /** + * Create a new DepthHeader with either value {@link #DEPTH_0 0} + * or {@link #DEPTH_INFINITY infinity}. + * + * @param isDeep + */ + public DepthHeader(boolean isDeep) { + this.depth = (isDeep) ? DEPTH_INFINITY : DEPTH_0; + } + + /** + * @return integer representation of the depth indicated by the given header. + */ + public int getDepth() { + return depth; + } + + /** + * Return {@link DavConstants#HEADER_DEPTH Depth} + * + * @return {@link DavConstants#HEADER_DEPTH Depth} + * @see DavConstants#HEADER_DEPTH + * @see Header#getHeaderName() + */ + public String getHeaderName() { + return DavConstants.HEADER_DEPTH; + } + + /** + * Returns the header value. + * + * @return header value + * @see Header#getHeaderValue() + */ + public String getHeaderValue() { + if (depth == DavConstants.DEPTH_0 || depth == DavConstants.DEPTH_1) { + return String.valueOf(depth); + } else { + return DavConstants.DEPTH_INFINITY_S; + } + } + + /** + * Retrieve the Depth header from the given request object and parse the + * value. If no header is present or the value is empty String, the + * defaultValue is used ot build a new DepthHeader instance. + * + * @param request + * @param defaultValue + * @return a new DepthHeader instance + */ + public static DepthHeader parse(HttpServletRequest request, int defaultValue) { + String headerValue = request.getHeader(HEADER_DEPTH); + if (headerValue == null || "".equals(headerValue)) { + return new DepthHeader(defaultValue); + } else { + return new DepthHeader(depthToInt(headerValue)); + } + } + + /** + * Convert the String depth value to an integer. + * + * @param depth + * @return integer representation of the given depth String + * @throws IllegalArgumentException if the String does not represent a valid + * depth. + */ + private static int depthToInt(String depth) { + int d; + if (depth.equalsIgnoreCase(DavConstants.DEPTH_INFINITY_S)) { + d = DavConstants.DEPTH_INFINITY; + } else if (depth.equals(DavConstants.DEPTH_0+"")) { + d = DavConstants.DEPTH_0; + } else if (depth.equals(DavConstants.DEPTH_1+"")) { + d = DavConstants.DEPTH_1; + } else { + throw new IllegalArgumentException("Invalid depth value: " + depth); + } + return d; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/FieldValueParser.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/FieldValueParser.java new file mode 100644 index 00000000000..6f305216883 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/FieldValueParser.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.webdav.header; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FieldValueParser { + + /** + * Tokenize lists of token and quoted-url + * @param list field value + */ + public static List tokenizeList(String list) { + + String[] split = list.split(","); + if (split.length == 1) { + return Collections.singletonList(split[0].trim()); + } else { + List result = new ArrayList(); + String inCodedUrl = null; + for (String t : split) { + String trimmed = t.trim(); + // handle quoted-url containing "," + if (trimmed.startsWith("<") && !trimmed.endsWith(">")) { + inCodedUrl = trimmed + ","; + } else if (inCodedUrl != null && trimmed.endsWith(">")) { + inCodedUrl += trimmed; + result.add(inCodedUrl); + inCodedUrl = null; + } else { + if (trimmed.length() != 0) { + result.add(trimmed); + } + } + } + return Collections.unmodifiableList(result); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/Header.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/Header.java new file mode 100644 index 00000000000..f61a07e90c0 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/Header.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.webdav.header; + +/** + * Header... + */ +public interface Header { + + public String getHeaderName(); + + public String getHeaderValue(); +} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/IfHeader.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/IfHeader.java similarity index 83% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/IfHeader.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/IfHeader.java index 90983473659..63d272865b6 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/IfHeader.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/IfHeader.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -13,18 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.jackrabbit.webdav; +package org.apache.jackrabbit.webdav.header; -import org.apache.log4j.Logger; +import org.apache.jackrabbit.webdav.DavConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Iterator; -import java.io.StringReader; -import java.io.IOException; -import java.io.Reader; +import java.util.List; /** * The IfHeader class represents the state lists defined @@ -37,7 +41,7 @@ Resource = Coded-URL List = "(" 1*(["Not"](State-etag | "[" entity-tag "]")) ")" State-etag = Coded-URL - Coded-URL = "<" absoluteURI ">" + Coded-URL = "<" absoluteURI ">" * *

        * Reformulating this specification into proper EBNF as specified by N. Wirth @@ -46,9 +50,9 @@ * within words which is considered significant. *

              If = "If:" ( Tagged | Untagged ).
        -     Tagged = { "<" Word ">" Untagged } .
        +     Tagged = { "<" Word ">" Untagged } .
              Untagged = { "(" IfList ")" } .
        -     IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
        +     IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
              Word = characters .
          * 
        *

        @@ -73,12 +77,17 @@ * * @author Felix Meschberger */ -public class IfHeader { +public class IfHeader implements Header { /** * default logger */ - private static final Logger log = Logger.getLogger(IfHeader.class); + private static final Logger log = LoggerFactory.getLogger(IfHeader.class); + + /** + * The string representation of the header value + */ + private final String headerValue; /** * The list of untagged state entries @@ -86,9 +95,30 @@ public class IfHeader { private final IfHeaderInterface ifHeader; /** - * The list of all tokens present in the If header. + * The list of all positive tokens present in the If header. */ - private List allTokens = new ArrayList(); + private List allTokens = new ArrayList(); + /** + * The list of all NOT tokens present in the If header. + */ + private List allNotTokens = new ArrayList(); + + /** + * Create a Untagged IfHeader if the given lock tokens. + * + * @param tokens + */ + public IfHeader(String[] tokens) { + allTokens.addAll(Arrays.asList(tokens)); + StringBuffer b = new StringBuffer(); + for (String token : tokens) { + b.append("(").append("<"); + b.append(token); + b.append(">").append(")"); + } + headerValue = b.toString(); + ifHeader = parse(); + } /** * Parses the If header and creates and internal representation @@ -97,45 +127,37 @@ public class IfHeader { * @param req The request object */ public IfHeader(HttpServletRequest req) { + headerValue = req.getHeader(DavConstants.HEADER_IF); + ifHeader = parse(); + } - String ifHeaderValue = req.getHeader(DavConstants.HEADER_IF); - if (ifHeaderValue != null && ifHeaderValue.length() > 0) { - - StringReader reader = null; - int firstChar = 0; - - try { - reader = new StringReader(ifHeaderValue); - - // get the first character to decide - expect '(' or '<' - try { - reader.mark(1); - firstChar = readWhiteSpace(reader); - reader.reset(); - } catch (IOException ignore) { - // may be thrown according to API but is only thrown by the - // StringReader class if the reader is already closed. - } - - if (firstChar == '(') { - ifHeader = parseUntagged(reader); - } else if (firstChar == '<') { - ifHeader = parseTagged(reader); - } else { - logIllegalState("If", firstChar, "(<", null); - ifHeader = null; - } + /** + * Return {@link DavConstants#HEADER_IF If} + * + * @return {@link DavConstants#HEADER_IF If} + * @see DavConstants#HEADER_IF + */ + public String getHeaderName() { + return DavConstants.HEADER_IF; + } - } finally { - if (reader != null) { - reader.close(); - } - } + /** + * Return the String representation of the If header present on + * the given request or null. + * + * @return If header value as String or null. + */ + public String getHeaderValue() { + return headerValue; + } - } else { - log.debug("IfHeader: No If header in request"); - ifHeader = null; - } + /** + * Returns true if an If header was present in the given request. False otherwise. + * + * @return true if an If header was present. + */ + public boolean hasValue() { + return ifHeader != null; } /** @@ -145,7 +167,7 @@ public IfHeader(HttpServletRequest req) { * If the If header is of untagged type, the untagged IfList * is matched against the token and etag given: A match of the token and * etag is found if at least one of the IfList entries match the - * token and etag tupel. + * token and etag tuple. * * @param tag The tag to identify the IfList to match the token * and etag against. @@ -169,13 +191,65 @@ public boolean matches(String tag, String token, String etag) { } /** - * - * @return + * @return an iterator over all tokens present in the if header, that were + * not denied by a leading NOT statement. */ - public Iterator getAllTokens() { + public Iterator getAllTokens() { return allTokens.iterator(); } + /** + * @return an iterator over all NOT tokens present in the if header, that + * were explicitly denied. + */ + public Iterator getAllNotTokens() { + return allNotTokens.iterator(); + } + + /** + * Parse the original header value and build the internal IfHeaderInterface + * object that is easy to query. + */ + private IfHeaderInterface parse() { + IfHeaderInterface ifHeader; + if (headerValue != null && headerValue.length() > 0) { + StringReader reader = null; + int firstChar = 0; + + try { + reader = new StringReader(headerValue); + // get the first character to decide - expect '(' or '<' + try { + reader.mark(1); + firstChar = readWhiteSpace(reader); + reader.reset(); + } catch (IOException ignore) { + // may be thrown according to API but is only thrown by the + // StringReader class if the reader is already closed. + } + + if (firstChar == '(') { + ifHeader = parseUntagged(reader); + } else if (firstChar == '<') { + ifHeader = parseTagged(reader); + } else { + logIllegalState("If", firstChar, "(<", null); + ifHeader = null; + } + + } finally { + if (reader != null) { + reader.close(); + } + } + + } else { + log.debug("IfHeader: No If header in request"); + ifHeader = null; + } + return ifHeader; + } + //---------- internal IF header parser ------------------------------------- /** * Parses a tagged type If header. This method implements the @@ -191,7 +265,7 @@ private IfHeaderMap parseTagged(StringReader reader) { IfHeaderMap map = new IfHeaderMap(); try { while (true) { - // read next non-whitespace + // read next non-white space int c = readWhiteSpace(reader); if (c < 0) { // end of input, no more entries @@ -233,7 +307,7 @@ private IfHeaderList parseUntagged(StringReader reader) { IfHeaderList list = new IfHeaderList(); try { while (true) { - // read next non-whitespace + // read next non white space reader.mark(1); int c = readWhiteSpace(reader); if (c < 0) { @@ -272,7 +346,7 @@ private IfHeaderList parseUntagged(StringReader reader) { * * @return The {@link IfList} for the input IfList. * - * @throws IOException if a problem occurrs during reading. + * @throws IOException if a problem occurs during reading. */ private IfList parseIfList(StringReader reader) throws IOException { IfList res = new IfList(); @@ -296,7 +370,7 @@ private IfList parseIfList(StringReader reader) throws IOException { // check whether t or T not = reader.read(); - if (not !='t' || not != 'T') { + if (not !='t' && not != 'T') { logIllegalState("IfList-Not", not, "t", null); break; } @@ -311,7 +385,11 @@ private IfList parseIfList(StringReader reader) throws IOException { if (word != null) { res.add(new IfListEntryToken(word, positive)); // also add the token to the list of all tokens - allTokens.add(word); + if (positive) { + allTokens.add(word); + } else { + allNotTokens.add(word); + } positive = true; } break; @@ -354,7 +432,7 @@ private IfList parseIfList(StringReader reader) throws IOException { * * @return The first non-whitespace character or -1 in case of EOF. * - * @throws IOException if a problem occurrs during reading. + * @throws IOException if a problem occurs during reading. */ private int readWhiteSpace(Reader reader) throws IOException { int c = reader.read(); @@ -366,20 +444,20 @@ private int readWhiteSpace(Reader reader) throws IOException { /** * Reads from the input until the end character is encountered and returns - * the string upto but not including this end character. If the end of input + * the string up to but not including this end character. If the end of input * is reached before reading the end character null is * returned. *

        * Note that this method does not support any escaping. * * @param reader The Reader to read from - * @param end The ending character limitting the word. + * @param end The ending character limiting the word. * - * @return The string read upto but not including the ending character or + * @return The string read up to but not including the ending character or * null if the end of input is reached before the ending * character has been read. * - * @throws IOException if a problem occurrs during reading. + * @throws IOException if a problem occurs during reading. */ private String readWord(Reader reader, char end) throws IOException { StringBuffer buf = new StringBuffer(); @@ -408,7 +486,7 @@ private String readWord(Reader reader, char end) throws IOException { * * @param state The name of the current parse state. This method logs this * name in the message. The intended value would probably be the - * name of the EBNF production during which the error occurrs. + * name of the EBNF production during which the error occurs. * @param effChar The effective character read. * @param expChar The list of characters acceptable in the current state. * @param reader The reader to be caught up to any of the expected @@ -422,7 +500,7 @@ private void logIllegalState(String state, int effChar, String expChar, String effString = (effChar < 0) ? "" : String.valueOf((char) effChar); // log the error - log.error("logIllegalState: Unexpected character '"+effString+" in state "+state+", expected any of "+expChar); + log.error("logIllegalState: Unexpected character '"+effString+"' in state "+state+", expected any of "+expChar); // catch up if a reader is given if (reader != null && effChar >= 0) { @@ -524,21 +602,22 @@ protected boolean match(String value) { */ protected abstract String getType(); - /** - * Returns the value of this entry. - * - * @return the value - */ - protected String getValue() { - return value; - } + /** + * Returns the value of this entry. + * + * @return the value + */ + protected String getValue() { + return value; + } /** - * Returns the String represenation of this entry. This method uses the + * Returns the String representation of this entry. This method uses the * {@link #getType} to build the string representation. * - * @return the String represenation of this entry. + * @return the String representation of this entry. */ + @Override public String toString() { if (stringValue == null) { stringValue = getType() + ": " + (positive?"":"!") + value; @@ -576,6 +655,7 @@ private static class IfListEntryToken extends IfListEntry { * @return true if the token matches the IfList * entry's token value. */ + @Override public boolean match(String token, String etag) { return super.match(token); } @@ -586,6 +666,7 @@ public boolean match(String token, String etag) { * * @return The fixed string Token as the type name. */ + @Override protected String getType() { return "Token"; } @@ -620,6 +701,7 @@ private static class IfListEntryEtag extends IfListEntry { * @return true if the etag matches the IfList * entry's etag value. */ + @Override public boolean match(String token, String etag) { return super.match(etag); } @@ -630,6 +712,7 @@ public boolean match(String token, String etag) { * * @return The fixed string ETag as the type name. */ + @Override protected String getType() { return "ETag"; } @@ -643,48 +726,20 @@ protected String getType() { * This class is a container for data contained in the If * production IfList *

        -         IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
        +         IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
              * 
        *

        */ - private static class IfList extends ArrayList { - - /** - * Throws an IllegalStateException because only - * {@link IfListEntry} objects are supported in this list. - * - * @param o The Object to add. - * @return true if successfull - * - * @throws IllegalStateException because only {@link IfListEntry} - * objects are supported in this list. - */ - public boolean add(Object o) { - throw new IllegalArgumentException("Only IfListEntry instances allowed"); - } - - /** - * Throws an IllegalStateException because only - * {@link IfListEntry} objects are supported in this list. - * - * @param index The position at which to add the object. - * @param element The Object to add. - * - * @throws IllegalStateException because only {@link IfListEntry} - * objects are supported in this list. - */ - public void add(int index, Object element) { - throw new IllegalArgumentException("Only IfListEntry instances allowed"); - } + private static class IfList extends ArrayList { /** * Adds the {@link IfListEntry} at the end of the list. * * @param entry The {@link IfListEntry} to add to the list * - * @return true (as per the general contract of - * Collection.add). + * @return true (as per the general contract of Collection.add). */ + @Override public boolean add(IfListEntry entry) { return super.add(entry); } @@ -698,6 +753,7 @@ public boolean add(IfListEntry entry) { * @throws IndexOutOfBoundsException if index is out of range * (index < 0 || index > size()). */ + @Override public void add(int index, IfListEntry entry) { super.add(index, entry); } @@ -710,15 +766,15 @@ public void add(int index, IfListEntry entry) { * @param token The token to compare. * @param etag The etag to compare. * - * @return true if all entries in the list matche the + * @return true if all entries in the list match the * given tag and token. */ public boolean match(String token, String etag) { log.debug("match: Trying to match token="+token+", etag="+etag); for (int i=0; i < size(); i++) { - IfListEntry ile = (IfListEntry) get(i); + IfListEntry ile = get(i); if (!ile.match(token, etag)) { - log.debug("match: Entry "+String.valueOf(i)+"-"+ile+" does not match"); + log.debug("match: Entry "+i+"-"+ile+" does not match"); return false; } } @@ -755,14 +811,14 @@ private static interface IfHeaderInterface { } /** - * The IfHeaderList clss implements the {@link IfHeaderInterface} + * The IfHeaderList class implements the {@link IfHeaderInterface} * interface to support untagged lists of {@link IfList}s. This class * implements the data container for the production : *

                  Untagged = { "(" IfList ")" } .
              * 
        */ - private static class IfHeaderList extends ArrayList implements IfHeaderInterface { + private static class IfHeaderList extends ArrayList implements IfHeaderInterface { /** * Matches a list of {@link IfList}s against the token and etag. If any of @@ -782,10 +838,9 @@ private static class IfHeaderList extends ArrayList implements IfHeaderInterface public boolean matches(String resource, String token, String etag) { log.debug("matches: Trying to match token="+token+", etag="+etag); - for (int i=0; i < size(); i++) { - IfList il = (IfList) get(i); + for (IfList il : this) { if (il.match(token, etag)) { - log.debug("matches: Found match with "+il); + log.debug("matches: Found match with " + il); return true; } } @@ -796,14 +851,14 @@ public boolean matches(String resource, String token, String etag) { } /** - * The IfHeaderMap clss implements the {@link IfHeaderInterface} + * The IfHeaderMap class implements the {@link IfHeaderInterface} * interface to support tagged lists of {@link IfList}s. This class * implements the data container for the production : *
        -         Tagged = { "<" Word ">" "(" IfList ")" } .
        +         Tagged = { "<" Word ">" "(" IfList ")" } .
              * 
        */ - private static class IfHeaderMap extends HashMap implements IfHeaderInterface { + private static class IfHeaderMap extends HashMap implements IfHeaderInterface { /** * Matches the token and etag for the given resource. If the resource is @@ -821,7 +876,7 @@ private static class IfHeaderMap extends HashMap implements IfHeaderInterface { public boolean matches(String resource, String token, String etag) { log.debug("matches: Trying to match resource="+resource+", token="+token+","+etag); - IfHeaderList list = (IfHeaderList) get(resource); + IfHeaderList list = get(resource); if (list == null) { log.debug("matches: No entry for tag "+resource+", assuming match"); return true; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/LabelHeader.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/LabelHeader.java new file mode 100644 index 00000000000..723df11ec34 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/LabelHeader.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.webdav.header; + +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.util.EncodeUtil; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * LabelHeader... + */ +public class LabelHeader implements Header { + + private static Logger log = LoggerFactory.getLogger(LabelHeader.class); + + private final String label; + + public LabelHeader(String label) { + if (label == null) { + throw new IllegalArgumentException("null is not a valid label."); + } + this.label = label; + } + + public String getLabel() { + return label; + } + + public String getHeaderName() { + return DeltaVConstants.HEADER_LABEL; + } + + public String getHeaderValue() { + return EncodeUtil.escape(label); + } + + public static LabelHeader parse(WebdavRequest request) { + String hv = request.getHeader(DeltaVConstants.HEADER_LABEL); + if (hv == null) { + return null; + } else { + return new LabelHeader(EncodeUtil.unescape(hv)); + } + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/OverwriteHeader.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/OverwriteHeader.java new file mode 100644 index 00000000000..fddb584350f --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/OverwriteHeader.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.webdav.header; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; + +/** + * OverwriteHeader... + */ +public class OverwriteHeader implements Header { + + private static Logger log = LoggerFactory.getLogger(OverwriteHeader.class); + + public static final String OVERWRITE_TRUE = "T"; + public static final String OVERWRITE_FALSE = "F"; + + /** + * Set 'doOverwrite' to true by default. See RFC 2518: + * "If the overwrite header is not included in a COPY or MOVE request then + * the resource MUST treat the request as if it has an overwrite header of + * value {@link #OVERWRITE_TRUE}". + */ + private final boolean doOverwrite; + + public OverwriteHeader(boolean doOverwrite) { + this.doOverwrite = doOverwrite; + } + + /** + * Create a new OverwriteHeader for the given request object. + * If the latter does not contain an "Overwrite" header field, the default + * applies, which is {@link #OVERWRITE_TRUE} according to RFC 2518. + * + * @param request + */ + public OverwriteHeader(HttpServletRequest request) { + String overwriteHeader = request.getHeader(DavConstants.HEADER_OVERWRITE); + if (overwriteHeader != null) { + doOverwrite = overwriteHeader.equalsIgnoreCase(OVERWRITE_TRUE); + } else { + // no Overwrite header -> default is 'true' + doOverwrite = true; + } + } + + public String getHeaderName() { + return DavConstants.HEADER_OVERWRITE; + } + + public String getHeaderValue() { + return (doOverwrite) ? OVERWRITE_TRUE : OVERWRITE_FALSE; + } + + public boolean isOverwrite() { + return doOverwrite; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/PollTimeoutHeader.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/PollTimeoutHeader.java new file mode 100644 index 00000000000..146d3567278 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/PollTimeoutHeader.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.webdav.header; + +import org.apache.jackrabbit.webdav.observation.ObservationConstants; + +import javax.servlet.http.HttpServletRequest; + +/** + * PollTimeoutHeader implements a timeout header for subscription + * polling. + */ +public class PollTimeoutHeader extends TimeoutHeader { + + public PollTimeoutHeader(long timeout) { + super(timeout); + } + + @Override + public String getHeaderName() { + return ObservationConstants.HEADER_POLL_TIMEOUT; + } + + /** + * Parses the request timeout header and converts it into a new + * PollTimeoutHeader object.
        The default value is used as + * fallback if the String is not parseable. + * + * @param request + * @param defaultValue + * @return a new PollTimeoutHeader object. + */ + public static PollTimeoutHeader parseHeader(HttpServletRequest request, long defaultValue) { + String timeoutStr = request.getHeader(ObservationConstants.HEADER_POLL_TIMEOUT); + long timeout = parse(timeoutStr, defaultValue); + return new PollTimeoutHeader(timeout); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/TimeoutHeader.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/TimeoutHeader.java new file mode 100644 index 00000000000..c1e72ae04f5 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/TimeoutHeader.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.header; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; + +/** + * TimeoutHeader... + */ +public class TimeoutHeader implements Header, DavConstants { + + private static Logger log = LoggerFactory.getLogger(TimeoutHeader.class); + + private final long timeout; + + public TimeoutHeader(long timeout) { + this.timeout = timeout; + } + + public String getHeaderName() { + return DavConstants.HEADER_TIMEOUT; + } + + public String getHeaderValue() { + if (timeout == INFINITE_TIMEOUT) { + return TIMEOUT_INFINITE; + } else { + return "Second-" + (timeout / 1000); + } + } + + public long getTimeout() { + return timeout; + } + + /** + * Parses the request timeout header and converts it into a new + * TimeoutHeader object.
        The default value is used as + * fallback if the String is not parseable. + * + * @param request + * @param defaultValue + * @return a new TimeoutHeader object. + */ + public static TimeoutHeader parse(HttpServletRequest request, long defaultValue) { + String timeoutStr = request.getHeader(HEADER_TIMEOUT); + long timeout = parse(timeoutStr, defaultValue); + return new TimeoutHeader(timeout); + } + + /** + * Parses the given timeout String and converts the timeout value + * into a long indicating the number of milliseconds until expiration time + * is reached.
        + * NOTE: If the timeout String equals to {@link #TIMEOUT_INFINITE 'infinite'} + * {@link Integer#MAX_VALUE} is returned. If the Sting is invalid or is in an + * invalid format that cannot be parsed, the default value is returned. + * + * @param timeoutStr + * @param defaultValue + * @return long representing the timeout present in the header or the default + * value if the header is missing or could not be parsed. + */ + public static long parse(String timeoutStr, long defaultValue) { + long timeout = defaultValue; + if (timeoutStr != null && timeoutStr.length() > 0) { + int secondsInd = timeoutStr.indexOf("Second-"); + if (secondsInd >= 0) { + secondsInd += 7; // read over "Second-" + int i = secondsInd; + while (i < timeoutStr.length() && Character.isDigit(timeoutStr.charAt(i))) { + i++; + } + try { + timeout = 1000L * Long.parseLong(timeoutStr.substring(secondsInd, i)); + } catch (NumberFormatException ignore) { + // ignore and return 'undefined' timeout + log.error("Invalid timeout format: " + timeoutStr); + } + } else if (timeoutStr.equalsIgnoreCase(TIMEOUT_INFINITE)) { + timeout = INFINITE_TIMEOUT; + } + } + return timeout; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/package-info.java new file mode 100644 index 00000000000..3c834230d9d --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/header/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.1.0") +package org.apache.jackrabbit.webdav.header; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/InputContext.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/InputContext.java new file mode 100644 index 00000000000..7faaf32fbc2 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/InputContext.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.webdav.io; + +import java.io.InputStream; + +/** + * InputContext... + */ +public interface InputContext { + + /** + * Return true, if there are any data to be imported (and not only properties) + * + * @return + */ + public boolean hasStream(); + + /** + * Returns the input stream of the resource to import. + * + * @return the input stream. + */ + public InputStream getInputStream(); + + /** + * Returns the modification time of the resource or the current time if + * the modification time has not been set. + * + * @return the modification time. + */ + public long getModificationTime(); + + /** + * Returns the content language or null + * + * @return contentLanguage + */ + public String getContentLanguage(); + + /** + * Returns the length of the data or -1 if the contentlength could not be + * determined. + * + * @return the content length + */ + public long getContentLength(); + + /** + * Return the content type or null + * + * @return + */ + public String getContentType(); + + /** + * Returns the value of the given property or null if this property does + * not exist. + * + * @param propertyName + * @return String property value or null + */ + public String getProperty(String propertyName); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/InputContextImpl.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/InputContextImpl.java new file mode 100644 index 00000000000..e5b9fb1db44 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/InputContextImpl.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.webdav.io; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import java.io.InputStream; +import java.util.Date; + +/** + * InputContextImpl class encapsulates the InputStream + * and some header values as present in the POST, PUT or MKCOL request. + */ +public class InputContextImpl implements InputContext { + + private static Logger log = LoggerFactory.getLogger(InputContextImpl.class); + + private final HttpServletRequest request; + private final InputStream in; + + public InputContextImpl(HttpServletRequest request, InputStream in) { + if (request == null) { + throw new IllegalArgumentException("DavResource and Request must not be null."); + } + + this.request = request; + this.in = in; + } + + public boolean hasStream() { + return in != null; + } + + /** + * Returns the input stream of the resource to import. + * + * @return the input stream. + */ + public InputStream getInputStream() { + return in; + } + + public long getModificationTime() { + return new Date().getTime(); + } + + /** + * Returns the content language or null. + * + * @return contentLanguage + */ + public String getContentLanguage() { + return request.getHeader(DavConstants.HEADER_CONTENT_LANGUAGE); + } + + /** + * @return content length or -1 when unknown + */ + public long getContentLength() { + String length = request.getHeader(DavConstants.HEADER_CONTENT_LENGTH); + if (length == null) { + // header not present + return -1; + } else { + try { + return Long.parseLong(length); + } catch (NumberFormatException ex) { + log.error("broken Content-Length header: " + length); + return -1; + } + } + } + + public String getContentType() { + return request.getHeader(DavConstants.HEADER_CONTENT_TYPE); + } + + public String getProperty(String propertyName) { + return request.getHeader(propertyName); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/OutputContext.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/OutputContext.java new file mode 100644 index 00000000000..a5a25eff5a5 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/OutputContext.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.webdav.io; + +import java.io.OutputStream; + +/** + * OutputContext... + */ +public interface OutputContext { + + /** + * Return true if the given export context can provide an output stream + */ + public boolean hasStream(); + + /** + * Return the output stream to be used for the export or null + * + * @return + */ + public OutputStream getOutputStream(); + + /** + * Sets the content language. + * + * @param contentLanguage + */ + public void setContentLanguage(String contentLanguage); + + /** + * Sets the length of the data. + * + * @param contentLength the content length + */ + public void setContentLength(long contentLength); + + /** + * Set the content type for the resource content + * + * @param contentType + */ + public void setContentType(String contentType); + + /** + * Sets the modification time of the resource + * + * @param modificationTime the modification time + */ + public void setModificationTime(long modificationTime); + + /** + * Sets the ETag of the resource. A successful export command + * may set this member. + * + * @param etag the ETag + */ + public void setETag(String etag); + + /** + * Allows to set additional properties that are not covered by an extra setter + * method. + * + * @param propertyName + * @param propertyValue + */ + public void setProperty(String propertyName, String propertyValue); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/OutputContextImpl.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/OutputContextImpl.java new file mode 100644 index 00000000000..94a9202819a --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/OutputContextImpl.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.webdav.io; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletResponse; +import java.io.OutputStream; + +/** + * OutputContextImpl... + */ +public class OutputContextImpl implements OutputContext { + + private static Logger log = LoggerFactory.getLogger(OutputContextImpl.class); + + private final HttpServletResponse response; + private final OutputStream out; + + public OutputContextImpl(HttpServletResponse response, OutputStream out) { + if (response == null) { + throw new IllegalArgumentException("Response must not be null."); + } + + this.response = response; + this.out = out; + } + + public boolean hasStream() { + return out != null; + } + + public OutputStream getOutputStream() { + return out; + } + + public void setContentLanguage(String contentLanguage) { + if (contentLanguage != null) { + response.setHeader(DavConstants.HEADER_CONTENT_LANGUAGE, contentLanguage); + } + } + + public void setContentLength(long contentLength) { + if (contentLength >= 0) { + if (contentLength <= Integer.MAX_VALUE) { + response.setContentLength((int) contentLength); + } else { + response.addHeader(DavConstants.HEADER_CONTENT_LENGTH, Long.toString(contentLength)); + } + } // else: negative content length -> ignore. + } + + public void setContentType(String contentType) { + if (contentType != null) { + response.setContentType(contentType); + } + } + + public void setModificationTime(long modificationTime) { + if (modificationTime >= 0) { + response.addDateHeader(DavConstants.HEADER_LAST_MODIFIED, modificationTime); + } + } + + public void setETag(String etag) { + if (etag != null) { + response.setHeader(DavConstants.HEADER_ETAG, etag); + } + } + + public void setProperty(String propertyName, String propertyValue) { + if (propertyName != null && propertyValue != null) { + response.setHeader(propertyName, propertyValue); + } + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/package-info.java new file mode 100644 index 00000000000..dfafee70b11 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/io/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.1") +package org.apache.jackrabbit.webdav.io; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/AbstractActiveLock.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/AbstractActiveLock.java new file mode 100644 index 00000000000..abd3ef266c2 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/AbstractActiveLock.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.webdav.lock; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * AbstractActiveLock... + */ +public abstract class AbstractActiveLock implements ActiveLock, DavConstants { + + private String lockroot; + + /** + * @see ActiveLock#getLockroot() + */ + public String getLockroot() { + return lockroot; + } + + /** + * @see ActiveLock#setLockroot(String) + */ + public void setLockroot(String lockroot) { + this.lockroot = lockroot; + } + + /** + * Returns the default Xml representation of the 'activelock' element + * as defined by RFC 4918. + * + * @return Xml representation + * @param document + */ + public Element toXml(Document document) { + Element activeLock = DomUtil.createElement(document, XML_ACTIVELOCK, NAMESPACE); + + // lockscope property + activeLock.appendChild(getScope().toXml(document)); + // locktype property + activeLock.appendChild(getType().toXml(document)); + // depth + activeLock.appendChild(DomUtil.depthToXml(isDeep(), document)); + // timeout + long timeout = getTimeout(); + if (!isExpired() && timeout != UNDEFINED_TIMEOUT) { + activeLock.appendChild(DomUtil.timeoutToXml(timeout, document)); + } + + // owner + if (getOwner() != null) { + DomUtil.addChildElement(activeLock, XML_OWNER, NAMESPACE, getOwner()); + } + + // locktoken + if (getToken() != null) { + Element lToken = DomUtil.addChildElement(activeLock, XML_LOCKTOKEN, NAMESPACE); + lToken.appendChild(DomUtil.hrefToXml(getToken(), document)); + } + + // lock root + if (getLockroot() != null) { + Element lroot = DomUtil.addChildElement(activeLock, XML_LOCKROOT, NAMESPACE); + lroot.appendChild(DomUtil.hrefToXml(getLockroot(), document)); + } + return activeLock; + } + +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/AbstractLockEntry.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/AbstractLockEntry.java new file mode 100644 index 00000000000..e1ca61c4ca8 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/AbstractLockEntry.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.webdav.lock; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * AbstractLockEntry provides the generic {@link org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml} method. + */ +public abstract class AbstractLockEntry implements LockEntry, DavConstants { + + private static Logger log = LoggerFactory.getLogger(AbstractLockEntry.class); + + /** + * Returns the Xml representation of this LockEntry. + * + * @return Xml representation + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Element entry = DomUtil.createElement(document, XML_LOCKENTRY, NAMESPACE); + entry.appendChild(getScope().toXml(document)); + entry.appendChild(getType().toXml(document)); + return entry; + } + +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/ActiveLock.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/ActiveLock.java new file mode 100644 index 00000000000..c0ec6f655e7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/ActiveLock.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.webdav.lock; + +import org.apache.jackrabbit.webdav.xml.XmlSerializable; + +/** + * ActiveLock encapsulates the lock information for a + * {@link org.apache.jackrabbit.webdav.DavResource}. + */ +public interface ActiveLock extends XmlSerializable { + + /** + * Return true, if the given token matches the lock token present in this + * lock thus indicating that any lock relevant operation should not fail + * due to a lock. + * + * @param lockToken to be checked + * @return true if the given lock token matches. + */ + public boolean isLockedByToken(String lockToken); + + /** + * Returns true, if this lock is already expired. + * + * @return true if the lock is expired + */ + public boolean isExpired(); + + /** + * Return the lock token. + * + * @return token string representing the lock token. + */ + public String getToken(); + + /** + * Return the name (or id) of the lock owner. + * + * @return name (or id) of the lock owner. + */ + public String getOwner(); + + /** + * Set the name (or id) of the lock owner + * + * @param owner + */ + public void setOwner(String owner); + + /** + * Return the number of milliseconds the lock will live until it is expired + * or -1 if the timeout is not available (or the client is not allowed + * to retrieve it). + * + * @return number of milliseconds. + */ + public long getTimeout(); + + /** + * Defines the number of milliseconds until the timeout is reached. + * + * @param timeout + */ + public void setTimeout(long timeout); + + /** + * Return true if the lock is deep. + * + * @return true if the lock is deep. + */ + public boolean isDeep(); + + /** + * Set the lock deepness. + * + * @param isDeep + */ + public void setIsDeep(boolean isDeep); + + /** + * Returns the lockroot. + * + * @return lockroot + * @see RFC 4918 + */ + public String getLockroot(); + + /** + * Set the lockroot. + * + * @param lockroot + * @see RFC 4918 + */ + public void setLockroot(String lockroot); + + /** + * Return the type of this lock. + * + * @return type + */ + public Type getType(); + + /** + * Return the scope of this lock. + * + * @return scope + */ + public Scope getScope(); +} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/DefaultActiveLock.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/DefaultActiveLock.java similarity index 75% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/DefaultActiveLock.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/DefaultActiveLock.java index 5c1ac941f62..d5710b6e6d6 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/DefaultActiveLock.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/DefaultActiveLock.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -15,7 +16,8 @@ */ package org.apache.jackrabbit.webdav.lock; -import org.apache.jackrabbit.core.util.uuid.UUID; +import java.util.UUID; + import org.apache.jackrabbit.webdav.DavConstants; /** @@ -26,11 +28,11 @@ * values are set:
          * - timeout is set to infinity.
          * - isDeep is set to true.
        - * 
        + * 
        */ public class DefaultActiveLock extends AbstractActiveLock { - private final String token = DavConstants.OPAQUE_LOCK_TOKEN_PREFIX + UUID.randomUUID().toString(); + private final String token = DavConstants.OPAQUE_LOCK_TOKEN_PREFIX + UUID.randomUUID(); private String owner; private boolean isDeep = true; // deep by default private long expirationTime = DavConstants.INFINITE_TIMEOUT; // never expires by default; @@ -50,7 +52,7 @@ public DefaultActiveLock() { public DefaultActiveLock(LockInfo lockInfo) { if (lockInfo != null) { if (!(Type.WRITE.equals(lockInfo.getType()) && Scope.EXCLUSIVE.equals(lockInfo.getScope()))) { - throw new IllegalArgumentException("Only 'exclusive write' lock is allowed scope/type pair."); + throw new IllegalArgumentException("Only 'exclusive write' lock is allowed scope/type pair."); } owner = lockInfo.getOwner(); isDeep = lockInfo.isDeep(); @@ -62,65 +64,65 @@ public DefaultActiveLock(LockInfo lockInfo) { * @see ActiveLock#isLockedByToken(String) */ public boolean isLockedByToken(String lockToken) { - return (token != null) && token.equals(lockToken); + return (token != null) && token.equals(lockToken); } /** * @see ActiveLock#isExpired() */ public boolean isExpired() { - return System.currentTimeMillis() > expirationTime; + return System.currentTimeMillis() > expirationTime; } /** * @see ActiveLock#getToken() */ public String getToken() { - return token; + return token; } /** * @see ActiveLock#getOwner() */ public String getOwner() { - return owner; + return owner; } /** * @see ActiveLock#setOwner(String) */ public void setOwner(String owner) { - this.owner = owner; + this.owner = owner; } /** * @see ActiveLock#getTimeout() */ public long getTimeout() { - return expirationTime - System.currentTimeMillis(); + return expirationTime - System.currentTimeMillis(); } /** * @see ActiveLock#setTimeout(long) */ public void setTimeout(long timeout) { - if (timeout > 0) { - expirationTime = System.currentTimeMillis() + timeout; - } + if (timeout > 0) { + expirationTime = System.currentTimeMillis() + timeout; + } } /** * @see ActiveLock#isDeep() */ public boolean isDeep() { - return isDeep; + return isDeep; } /** * @see ActiveLock#setIsDeep(boolean) */ public void setIsDeep(boolean isDeep) { - this.isDeep = isDeep; + this.isDeep = isDeep; } /** diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockDiscovery.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockDiscovery.java new file mode 100644 index 00000000000..6bd6960c784 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockDiscovery.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.webdav.lock; + +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.header.TimeoutHeader; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.List; + +/** + * The LockDiscovery class encapsulates the webdav lock discovery + * property that is sent in the request body (PROPFIND and LOCK) and received + * in a LOCK response body. + */ +public class LockDiscovery extends AbstractDavProperty> { + + /** + * Listing of existing locks applied to the resource this discovery was + * requested for. Each entry reveals, who has a lock, what type of lock he has, + * the timeout type and the time remaining on the timeout, and the lock-type. + * NOTE, that any of the information listed may be not available for the + * server is free to withhold any or all of this information. + */ + private List activeLocks = new ArrayList(); + + /** + * Creates a new empty LockDiscovery property + */ + public LockDiscovery() { + super(DavPropertyName.LOCKDISCOVERY, false); + } + + /** + * Create a new LockDiscovery property + * + * @param lock + */ + public LockDiscovery(ActiveLock lock) { + super(DavPropertyName.LOCKDISCOVERY, false); + addActiveLock(lock); + } + + /** + * Create a new LockDiscovery property + * + * @param locks + */ + public LockDiscovery(ActiveLock[] locks) { + super(DavPropertyName.LOCKDISCOVERY, false); + for (ActiveLock lock : locks) { + addActiveLock(lock); + } + } + + private void addActiveLock(ActiveLock lock) { + if (lock != null) { + activeLocks.add(lock); + } + } + + /** + * Returns the list of active locks. + * + * @return list of active locks + * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() + */ + public List getValue() { + return activeLocks; + } + + /** + * Creates a <lockdiscovery> element in response + * to a LOCK request or to the lockdiscovery property of a PROPFIND request.
        + * NOTE: if the {@link #activeLocks} list is empty an empty lockdiscovery + * property is created ( <lockdiscovery/>) + * @return A <lockdiscovery> element. + * @param document + */ + @Override + public Element toXml(Document document) { + Element lockdiscovery = getName().toXml(document); + for (ActiveLock lock : activeLocks) { + lockdiscovery.appendChild(lock.toXml(document)); + } + return lockdiscovery; + } + + //---------------------------------------------------< factory from xml >--- + /** + * Builds a new LockDiscovery object from the given xml element. + * + * @param lockDiscoveryElement + * @return + * @throws IllegalArgumentException if the given xml element is not a + * DAV:lockdiscovery element. + */ + public static LockDiscovery createFromXml(Element lockDiscoveryElement) { + if (!DomUtil.matches(lockDiscoveryElement, PROPERTY_LOCKDISCOVERY, NAMESPACE)) { + throw new IllegalArgumentException("DAV:lockdiscovery element expected."); + } + + List activeLocks = new ArrayList(); + ElementIterator it = DomUtil.getChildren(lockDiscoveryElement, XML_ACTIVELOCK, NAMESPACE); + while (it.hasNext()) { + Element al = it.nextElement(); + activeLocks.add(new ALockImpl(al)); + } + + return new LockDiscovery(activeLocks.toArray(new ActiveLock[activeLocks.size()])); + } + + //------< inner class >----------------------------------------------------- + /** + * Simple implementation of ActiveLock interface, that retrieves + * the values from the DAV:activelock XML element.
        + * Note, that all set-methods as well as {@link #isExpired()} are not + * implemented. + */ + private static class ALockImpl implements ActiveLock { + + private final Element alElement; + + private ALockImpl(Element alElement) { + if (!DomUtil.matches(alElement, XML_ACTIVELOCK, NAMESPACE)) { + throw new IllegalArgumentException("DAV:activelock element expected."); + } + this.alElement = alElement; + } + + public boolean isLockedByToken(String lockToken) { + String lt = getToken(); + if (lt == null) { + return false; + } else { + return lt.equals(lockToken); + } + } + + public boolean isExpired() { + throw new UnsupportedOperationException("Not implemented"); + } + + public String getToken() { + Element ltEl = DomUtil.getChildElement(alElement, XML_LOCKTOKEN, NAMESPACE); + if (ltEl != null) { + return DomUtil.getChildText(ltEl, XML_HREF, NAMESPACE); + } + return null; + } + + public String getOwner() { + String owner = null; + Element ow = DomUtil.getChildElement(alElement, XML_OWNER, NAMESPACE); + if (ow != null) { + if (DomUtil.hasChildElement(ow, XML_HREF, NAMESPACE)) { + owner = DomUtil.getChildTextTrim(ow, XML_HREF, NAMESPACE); + } else { + owner = DomUtil.getTextTrim(ow); + } + } + return owner; + } + + public void setOwner(String owner) { + throw new UnsupportedOperationException("Not implemented"); + } + + public long getTimeout() { + // get timeout string. if no DAV:timeout element is present, + // 't' will be 'null' and the undefined timeout will be returned. + String t = DomUtil.getChildTextTrim(alElement, XML_TIMEOUT, NAMESPACE); + return TimeoutHeader.parse(t, UNDEFINED_TIMEOUT); + } + + public void setTimeout(long timeout) { + throw new UnsupportedOperationException("Not implemented"); + } + + public boolean isDeep() { + String depth = DomUtil.getChildTextTrim(alElement, XML_DEPTH, NAMESPACE); + return DEPTH_INFINITY_S.equalsIgnoreCase(depth); + } + + public void setIsDeep(boolean isDeep) { + throw new UnsupportedOperationException("Not implemented"); + } + + public String getLockroot() { + Element root = DomUtil.getChildElement(alElement, XML_LOCKROOT, NAMESPACE); + if (root != null) { + return DomUtil.getChildTextTrim(root, XML_HREF, NAMESPACE); + } + // no lockroot element + return null; + } + + public void setLockroot(String lockroot) { + throw new UnsupportedOperationException("Not implemented"); + } + + public Type getType() { + return Type.createFromXml(DomUtil.getChildElement(alElement, XML_LOCKTYPE, NAMESPACE)); + } + + public Scope getScope() { + return Scope.createFromXml(DomUtil.getChildElement(alElement, XML_LOCKSCOPE, NAMESPACE)); + } + + public Element toXml(Document document) { + return (Element) document.importNode(alElement, true); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockEntry.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockEntry.java new file mode 100644 index 00000000000..3b295391152 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockEntry.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.webdav.lock; + +import org.apache.jackrabbit.webdav.xml.XmlSerializable; + +/** + * LockEntry... + */ +public interface LockEntry extends XmlSerializable { + + /** + * Returns the type of this lock entry + * + * @return type of this lock entry. + */ + public Type getType(); + + /** + * Returns the scope of this lock entry. + * + * @return scope of this lock entry. + */ + public Scope getScope(); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockInfo.java new file mode 100644 index 00000000000..30e7a5db8ea --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockInfo.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.webdav.lock; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * LockInfo is a simple utility class encapsulating the information + * passed with a LOCK request. It combines both the request body (which if present + * is required to by a 'lockinfo' Xml element) and the lock relevant request + * headers '{@link DavConstants#HEADER_TIMEOUT Timeout}' and + * '{@link DavConstants#HEADER_DEPTH Depth}'.
        + * Note that is class is not intended to perform any validation of the information + * given, since this left to those objects responsible for the lock creation + * on the requested resource. + */ +public class LockInfo implements DavConstants, XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(LockInfo.class); + + private Type type; + private Scope scope; + private String owner; + private boolean isDeep; + private long timeout; + + private boolean isRefreshLock; + + /** + * Create a new LockInfo used for refreshing an existing lock. + * + * @param timeout + */ + public LockInfo(long timeout) { + this.timeout = (timeout > 0) ? timeout : INFINITE_TIMEOUT; + this.isRefreshLock = true; + } + + /** + * Create a new LockInfo + * + * @param scope + * @param type + * @param owner + * @param timeout + * @param isDeep + */ + public LockInfo(Scope scope, Type type, String owner, long timeout, boolean isDeep) { + this.timeout = (timeout > 0) ? timeout : INFINITE_TIMEOUT; + this.isDeep = isDeep; + + if (scope == null || type == null) { + this.isRefreshLock = true; + } else { + this.scope = scope; + this.type = type; + this.owner = owner; + } + } + + /** + * Create a new LockInfo object from the given information. If + * liElement is null this lockinfo is assumed to + * be issued from a 'Refresh Lock' request. + * + * @param liElement 'lockinfo' element present in the request body of a LOCK request + * or null if the request was intended to refresh an existing lock. + * @param timeout Requested timespan until the lock should expire. A LOCK + * request MUST contain a '{@link DavConstants#HEADER_TIMEOUT Timeout}' + * according to RFC 2518. + * @param isDeep boolean value indicating whether the lock should be applied + * with depth infinity or only to the requested resource. + * @throws DavException if the liElement is not + * null but does not start with an 'lockinfo' element. + */ + public LockInfo(Element liElement, long timeout, boolean isDeep) throws DavException { + this.timeout = (timeout > 0) ? timeout : INFINITE_TIMEOUT; + this.isDeep = isDeep; + + if (liElement != null) { + if (!DomUtil.matches(liElement, XML_LOCKINFO, NAMESPACE)) { + log.warn("'DAV:lockinfo' element expected."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + + ElementIterator it = DomUtil.getChildren(liElement); + while (it.hasNext()) { + Element child = it.nextElement(); + String childName = child.getLocalName(); + if (XML_LOCKTYPE.equals(childName)) { + type = Type.createFromXml(child); + } else if (XML_LOCKSCOPE.equals(childName)) { + scope = Scope.createFromXml(child); + } else if (XML_OWNER.equals(childName)) { + // first try if 'owner' is inside a href element + owner = DomUtil.getChildTextTrim(child, XML_HREF, NAMESPACE); + if (owner==null) { + // otherwise: assume owner is a simple text element + owner = DomUtil.getTextTrim(child); + } + } + } + isRefreshLock = false; + } else { + isRefreshLock = true; + } + } + + /** + * Returns the lock type or null if no 'lockinfo' element was + * passed to the constructor or did not contain an 'type' element and the + * type has not been set otherwise. + * + * @return type or null + */ + public Type getType() { + return type; + } + + /** + * Set the lock type. + * + * @param type + */ + public void setType(Type type) { + this.type = type; + } + + /** + * Return the lock scope or null if no 'lockinfo' element was + * passed to the constructor or did not contain an 'scope' element and the + * scope has not been set otherwise. + * + * @return scope or null + */ + public Scope getScope() { + return scope; + } + + /** + * Set the lock scope. + * + * @param scope + */ + public void setScope(Scope scope) { + this.scope = scope; + } + + /** + * Return the owner indicated by the corresponding child element from the + * 'lockinfo' element or null if no 'lockinfo' element was + * passed to the constructor or did not contain an 'owner' element. + * + * @return owner or null + */ + public String getOwner() { + return owner; + } + + /** + * Returns true if the lock must be applied with depth infinity. + * + * @return true if a deep lock must be created. + */ + public boolean isDeep() { + return isDeep; + } + + /** + * Returns the time until the lock is requested to expire. + * + * @return time until the lock should expire. + */ + public long getTimeout() { + return timeout; + } + + /** + * Returns true if this LockInfo was created for a LOCK + * request intended to refresh an existing lock rather than creating a + * new one. + * + * @return true if the corresponding LOCK request was intended to refresh + * an existing lock. + */ + public boolean isRefreshLock() { + return isRefreshLock; + } + + /** + * Returns the xml representation of this lock info.
        + * NOTE however, that the depth and the timeout are not included + * in the xml. They will be passed to the server using the corresponding + * request headers. + * + * @param document + * @return xml representation of this lock info. + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + if (isRefreshLock) { + return null; + } else { + Element lockInfo = DomUtil.createElement(document, XML_LOCKINFO, NAMESPACE); + lockInfo.appendChild(scope.toXml(document)); + lockInfo.appendChild(type.toXml(document)); + if (owner != null) { + DomUtil.addChildElement(lockInfo, XML_OWNER, NAMESPACE, owner); + } + return lockInfo; + } + } + +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockManager.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockManager.java new file mode 100644 index 00000000000..298bbb0dce1 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/LockManager.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.lock; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; + +/** + * The LockManager interface. + */ +public interface LockManager { + + /** + * Create a new lock for the given {@link org.apache.jackrabbit.webdav.DavResource resource}. + * + * @param lockInfo + * @param resource + * @return + * @throws DavException + */ + public ActiveLock createLock(LockInfo lockInfo, DavResource resource) + throws DavException; + + /** + * Refresh the lock identified by the given lockToken and initially created + * on the specified resource. The update information can be retrieved from + * the lockInfo object passes. + * + * @param lockInfo + * @param lockToken + * @param resource + * @return + * @throws DavException + */ + public ActiveLock refreshLock(LockInfo lockInfo, String lockToken, DavResource resource) + throws DavException; + + /** + * Release the lock identified by the given lockToken and initially created + * on the specified resource. + * + * @param lockToken + * @param resource + * @throws DavException + */ + public void releaseLock(String lockToken, DavResource resource) + throws DavException; + + /** + * Retrieve the lock with the given type and scope that is applied to the + * given resource. The lock may be either initially created on this resource + * or might be inherited from an ancestor resource that hold a deep lock. + * If no such lock applies to the given resource null must be + * returned. + * + * @param type + * @param scope + * @param resource + * @return lock with the given type and scope applied to the resource or + * null if no lock applies. + */ + public ActiveLock getLock(Type type, Scope scope, DavResource resource); + + /** + * Returns true, if the the manager contains a lock for the given + * resource, that is hold by the specified token. + * + * @param lockToken + * @param resource + * @return true if the resource is locked by the specified token. + */ + public boolean hasLock(String lockToken, DavResource resource); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/Scope.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/Scope.java new file mode 100644 index 00000000000..e62b5c9c816 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/Scope.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.webdav.lock; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.Map; + +/** + * The Scope class abstracts the lock scope as defined by RFC 2518. + */ +public class Scope implements XmlSerializable { + + private static final Map scopes = new HashMap(); + + public static final Scope EXCLUSIVE = Scope.create(DavConstants.XML_EXCLUSIVE, DavConstants.NAMESPACE); + public static final Scope SHARED = Scope.create(DavConstants.XML_SHARED, DavConstants.NAMESPACE); + + private final String localName; + private final Namespace namespace; + + /** + * Private constructor + * + * @param localName + * @param namespace + */ + private Scope(String localName, Namespace namespace) { + this.localName = localName; + this.namespace = namespace; + } + + /** + * Return the Xml representation of the lock scope object as present in + * the LOCK request and response body and in the {@link LockDiscovery}. + * + * @return Xml representation + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element lockScope = DomUtil.createElement(document, DavConstants.XML_LOCKSCOPE, DavConstants.NAMESPACE); + DomUtil.addChildElement(lockScope, localName, namespace); + return lockScope; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + localName.hashCode(); + result = prime * result + namespace.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof Scope) { + Scope other = (Scope) obj; + return localName.equals(other.localName) && namespace.equals(other.namespace); + } else { + return false; + } + } + + /** + * Create a Scope object from the given Xml element. + * + * @param lockScope + * @return Scope object. + */ + public static Scope createFromXml(Element lockScope) { + if (lockScope != null && DavConstants.XML_LOCKSCOPE.equals(lockScope.getLocalName())) { + // we have the parent element and must retrieve the scope first + lockScope = DomUtil.getFirstChildElement(lockScope); + } + if (lockScope == null) { + throw new IllegalArgumentException("'null' is not a valid lock scope entry."); + } + Namespace namespace = Namespace.getNamespace(lockScope.getPrefix(), lockScope.getNamespaceURI()); + return create(lockScope.getLocalName(), namespace); + } + + /** + * Create a Scope object from the given name and namespace. + * + * @param localName + * @param namespace + * @return Scope object. + */ + public static Scope create(String localName, Namespace namespace) { + String key = DomUtil.getExpandedName(localName, namespace); + if (scopes.containsKey(key)) { + return scopes.get(key); + } else { + Scope scope = new Scope(localName, namespace); + scopes.put(key, scope); + return scope; + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/SimpleLockManager.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/SimpleLockManager.java new file mode 100644 index 00000000000..1fe2e320f56 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/SimpleLockManager.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.webdav.lock; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavServletResponse; + +import java.util.HashMap; +import java.util.Map; + +/** + * Simple manager for webdav locks.
        + */ +public class SimpleLockManager implements LockManager { + + /** map of locks */ + private Map locks = new HashMap(); + + /** + * + * @param lockToken + * @param resource + * @return + * @see LockManager#hasLock(String, org.apache.jackrabbit.webdav.DavResource) + */ + public boolean hasLock(String lockToken, DavResource resource) { + ActiveLock lock = locks.get(resource.getResourcePath()); + if (lock != null && lock.getToken().equals(lockToken)) { + return true; + } + return false; + } + + /** + * Returns the lock applying to the given resource or null if + * no lock can be found. + * + * @param type + * @param scope + * @param resource + * @return lock that applies to the given resource or null. + */ + public synchronized ActiveLock getLock(Type type, Scope scope, DavResource resource) { + if (!(Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope))) { + return null; + } + return getLock(resource.getResourcePath()); + } + + /** + * Looks for a valid lock at the given path or a deep lock present with + * a parent path. + * + * @param path + * @return + */ + private ActiveLock getLock(String path) { + ActiveLock lock = locks.get(path); + if (lock != null) { + // check if not expired + if (lock.isExpired()) { + lock = null; + } + } + if (lock == null) { + // check, if child of deep locked parent + if (!path.equals("/")) { + ActiveLock parentLock = getLock(getParentPath(path)); + if (parentLock != null && parentLock.isDeep()) { + lock = parentLock; + } + } + } + return lock; + } + + /** + * Adds the lock for the given resource, replacing any existing lock. + * + * @param lockInfo + * @param resource being the lock holder + */ + public synchronized ActiveLock createLock(LockInfo lockInfo, + DavResource resource) + throws DavException { + if (lockInfo == null || resource == null) { + throw new IllegalArgumentException("Neither lockInfo nor resource must be null."); + } + + String resourcePath = resource.getResourcePath(); + // test if there is already a lock present on this resource + ActiveLock lock = locks.get(resourcePath); + if (lock != null && lock.isExpired()) { + locks.remove(resourcePath); + lock = null; + } + if (lock != null) { + throw new DavException(DavServletResponse.SC_LOCKED, "Resource '" + resource.getResourcePath() + "' already holds a lock."); + } + // test if the new lock would conflict with any lock inherited from the + // collection or with a lock present on any member resource. + for (String key : locks.keySet()) { + // TODO: is check for lock on internal-member correct? + if (isDescendant(key, resourcePath)) { + ActiveLock l = locks.get(key); + if (l.isDeep() || (key.equals(getParentPath(resourcePath)) && !resource.isCollection())) { + throw new DavException(DavServletResponse.SC_LOCKED, "Resource '" + resource.getResourcePath() + "' already inherits a lock by its collection."); + } + } else if (isDescendant(resourcePath, key)) { + if (lockInfo.isDeep() || isInternalMember(resource, key)) { + throw new DavException(DavServletResponse.SC_CONFLICT, "Resource '" + resource.getResourcePath() + "' cannot be locked due to a lock present on the member resource '" + key + "'."); + } + + } + } + lock = new DefaultActiveLock(lockInfo); + locks.put(resource.getResourcePath(), lock); + return lock; + } + + /** + * + * @param lockInfo + * @param lockToken + * @param resource + * @return + * @throws DavException + * @see DavResource#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String) + */ + public ActiveLock refreshLock(LockInfo lockInfo, String lockToken, DavResource resource) + throws DavException { + ActiveLock lock = getLock(lockInfo.getType(), lockInfo.getScope(), resource); + if (lock == null) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } else if (!lock.getToken().equals(lockToken)) { + throw new DavException(DavServletResponse.SC_LOCKED); + } + lock.setTimeout(lockInfo.getTimeout()); + return lock; + } + + /** + * Remove the lock hold by the given resource. + * + * @param lockToken + * @param resource that is the lock holder + */ + public synchronized void releaseLock(String lockToken, DavResource resource) + throws DavException { + if (!locks.containsKey(resource.getResourcePath())) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + ActiveLock lock = locks.get(resource.getResourcePath()); + if (lock.getToken().equals(lockToken)) { + locks.remove(resource.getResourcePath()); + } else { + throw new DavException(DavServletResponse.SC_LOCKED); + } + } + + /** + * Return true, if the resource with the given memberPath is a internal + * non-collection member of the given resource, thus affected by a + * non-deep lock present on the resource. + * + * @param resource + * @param memberPath + * @return + */ + private static boolean isInternalMember(DavResource resource, String memberPath) { + if (resource.getResourcePath().equals(getParentPath(memberPath))) { + // find the member with the given path + DavResourceIterator it = resource.getMembers(); + while (it.hasNext()) { + DavResource member = it.nextResource(); + if (member.getResourcePath().equals(memberPath)) { + // return true if that member is not a collection + return !member.isCollection(); + } + } + } + return false; + } + + /** + * @param path Path to retrieve the parent path for. + * @return empty string if the specified path contains no '/', "/" if the + * last index of '/' is zero. Otherwise the last segment is cut off the + * specified path. + */ + private static String getParentPath(String path) { + int idx = path.lastIndexOf('/'); + switch (idx) { + case -1: + return ""; + case 0: + return "/"; + default: + return path.substring(0, idx); + } + } + + /** + * Determines if the descendant path is hierarchical a + * descendant of path. + * + * @param path the current path + * @param descendant the potential descendant + * @return true if the descendant is a descendant; + * false otherwise. + */ + private static boolean isDescendant(String path, String descendant) { + String pattern = path.endsWith("/") ? path : path + "/"; + return !pattern.equals(descendant) && descendant.startsWith(pattern); + } +} + diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/SupportedLock.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/SupportedLock.java similarity index 77% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/SupportedLock.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/SupportedLock.java index e2ed18dd5af..4ddf86d6160 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/lock/SupportedLock.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/SupportedLock.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -15,22 +16,24 @@ */ package org.apache.jackrabbit.webdav.lock; -import org.jdom.Element; -import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.w3c.dom.Document; +import org.w3c.dom.Element; import java.util.ArrayList; import java.util.Iterator; +import java.util.List; /** - * The SupportedLock class encapsulates the lock capabilties - * of a resource. It is mainly responsible for generating the <supportedlock> + * The SupportedLock class encapsulates the lock capabilities + * of a resource. It is mainly responsible for generating the <supportedlock> * property. */ -public class SupportedLock extends AbstractDavProperty { +public class SupportedLock extends AbstractDavProperty> { /** the list of lock entries */ - private final ArrayList entries = new ArrayList(); + private final List entries = new ArrayList(); /** * Creates a new empty SupportedLock property. @@ -73,9 +76,7 @@ public void addEntry(LockEntry entry) { * supported. */ public boolean isSupportedLock(Type type, Scope scope) { - Iterator it = entries.iterator(); - while (it.hasNext()) { - LockEntry le = (LockEntry) it.next(); + for (LockEntry le : entries) { if (le.getType().equals(type) && le.getScope().equals(scope)) { return true; } @@ -88,23 +89,23 @@ public boolean isSupportedLock(Type type, Scope scope) { * * @return an iterator over all supported locks */ - public Iterator getSupportedLocks() { + public Iterator getSupportedLocks() { return entries.iterator(); } /** - * Creates a JDOM element that represents the <supportedlock> tag. - * - * @return A JDOM element of this lock support. + * Creates an XML element that represents the <supportedlock> tag. + * + * @return An XML element of this lock support. + * @param document */ - public Element toXml() { - Element support = getName().toXml(); - Iterator iter = entries.iterator(); - while (iter.hasNext()) { - LockEntry le = (LockEntry) iter.next(); - support.addContent(le.toXml()); + @Override + public Element toXml(Document document) { + Element support = getName().toXml(document); + for (LockEntry le : entries) { + support.appendChild(le.toXml(document)); } - return support; + return support; } /** @@ -113,7 +114,7 @@ public Element toXml() { * @return list of supported lock. * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() */ - public Object getValue() { + public List getValue() { return entries; } diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/Type.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/Type.java new file mode 100644 index 00000000000..079b502018e --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/Type.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.webdav.lock; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.Map; + +/** + * The Type class encapsulates the lock type as defined by RFC 2518. + */ +public class Type implements XmlSerializable { + + private static Map types = new HashMap(); + + public static final Type WRITE = Type.create(DavConstants.XML_WRITE, DavConstants.NAMESPACE); + + private final String localName; + private final Namespace namespace; + + private int hashCode = -1; + + /** + * Private constructor. + * + * @param name + * @param namespace + */ + private Type(String name, Namespace namespace) { + this.localName = name; + this.namespace = namespace; + } + + /** + * Returns the Xml representation of this lock Type. + * + * @return Xml representation + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element lockType = DomUtil.createElement(document, DavConstants.XML_LOCKTYPE, DavConstants.NAMESPACE); + DomUtil.addChildElement(lockType, localName, namespace); + return lockType; + } + + @Override + public int hashCode() { + if (hashCode == -1) { + StringBuilder b = new StringBuilder(); + b.append("LockType : {").append(namespace).append("}").append(localName); + hashCode = b.toString().hashCode(); + } + return hashCode; + } + + /** + * Returns true if this Type is equal to the given one. + * + * @param obj + * @return + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof Type) { + Type other = (Type) obj; + return localName.equals(other.localName) && namespace.equals(other.namespace); + } + return false; + } + + /** + * Create a Type object from the given Xml element. + * + * @param lockType + * @return Type object. + */ + public static Type createFromXml(Element lockType) { + if (lockType != null && DavConstants.XML_LOCKTYPE.equals(lockType.getLocalName())) { + // we have the parent element and must retrieve the type first + lockType = DomUtil.getFirstChildElement(lockType); + } + if (lockType == null) { + throw new IllegalArgumentException("'null' is not valid lock type entry."); + } + Namespace namespace = Namespace.getNamespace(lockType.getPrefix(), lockType.getNamespaceURI()); + return create(lockType.getLocalName(), namespace); + } + + /** + * Create a Type object from the given localName and namespace. + * + * @param localName + * @param namespace + * @return Type object. + */ + public static Type create(String localName, Namespace namespace) { + String key = DomUtil.getExpandedName(localName, namespace); + if (types.containsKey(key)) { + return types.get(key); + } else { + Type type = new Type(localName, namespace); + types.put(key, type); + return type; + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/package-info.java new file mode 100644 index 00000000000..3d9c106ebaa --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/lock/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.1") +package org.apache.jackrabbit.webdav.lock; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/DefaultEventType.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/DefaultEventType.java new file mode 100644 index 00000000000..927abccaac8 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/DefaultEventType.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.webdav.observation; + +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.w3c.dom.Element; +import org.w3c.dom.Document; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; + +/** + * DefaultEventType defines a simple EventType implementation that + * only consists of a qualified event name consisting of namespace plus local + * name. + */ +public class DefaultEventType implements EventType { + + private static final Map eventTypes = new HashMap(); + + private final String localName; + private final Namespace namespace; + + /** + * Avoid instantiation of DefaultEventType. Since the amount + * of available (and registered) events is considered to be limited, the + * static {@link #create(String, Namespace) method is defined. + * + * @param localName + * @param namespace + */ + private DefaultEventType(String localName, Namespace namespace) { + this.localName = localName; + this.namespace = namespace; + } + + /** + * Factory method to create a new EventType. + * + * @param localName + * @param namespace + * @return + */ + public static EventType create(String localName, Namespace namespace) { + if (localName == null || "".equals(localName)) { + throw new IllegalArgumentException("null and '' are not valid local names of an event type."); + } + String key = DomUtil.getExpandedName(localName, namespace); + if (eventTypes.containsKey(key)) { + return eventTypes.get(key); + } else { + EventType type = new DefaultEventType(localName, namespace); + eventTypes.put(key, type); + return type; + } + } + + /** + * Factory method to create an array of new EventType for the + * specified localNames and the specified namespace. + * + * @param localNames + * @param namespace + * @return An array of event types. + */ + public static EventType[] create(String[] localNames, Namespace namespace) { + EventType[] types = new EventType[localNames.length]; + for (int i = 0; i < localNames.length; i++) { + types[i] = create(localNames[i], namespace); + } + return types; + } + + /** + * Retrieves one or multiple EventTypes from the 'eventtype' + * Xml element. While a subscription may register multiple types (thus + * the 'eventtype' contains multiple child elements), a single event may only + * refer to one single type. + * + * @param eventType + * @return + */ + public static EventType[] createFromXml(Element eventType) { + if (!DomUtil.matches(eventType, ObservationConstants.XML_EVENTTYPE, ObservationConstants.NAMESPACE)) { + throw new IllegalArgumentException("'eventtype' element expected which contains a least a single child element."); + } + + List etypes = new ArrayList(); + ElementIterator it = DomUtil.getChildren(eventType); + while (it.hasNext()) { + Element el = it.nextElement(); + etypes.add(create(el.getLocalName(), DomUtil.getNamespace(el))); + } + return etypes.toArray(new EventType[etypes.size()]); + } + + //----------------------------------------------------------< EventType >--- + /** + * @see EventType#getName() + */ + public String getName() { + return localName; + } + + /** + * @see EventType#getNamespace() + */ + public Namespace getNamespace() { + return namespace; + } + + //----------------------------------------------------< XmlSerializable >--- + /** + * Returns a single empty Xml element where namespace and local name of this + * event type define the elements name. + *
        +     * EventType.create("someevent", Namespace.getNamespace("F", "http://www.foo.bar/eventtypes"));
        +     *
        +     * returns the following element upon call of toXml:
        +     *
        +     * <F:someevent xmlns:F="http://www.foo.bar/eventtypes" />
        +     * 
        + * + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + return DomUtil.createElement(document, localName, namespace); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventBundle.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventBundle.java new file mode 100644 index 00000000000..5389dc18ede --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventBundle.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.webdav.observation; + +import org.apache.jackrabbit.webdav.xml.XmlSerializable; + +/** + * EventBundle defines an empty interface used to represent a bundle + * of events. + * + * @see EventDiscovery#addEventBundle(EventBundle) + */ +public interface EventBundle extends XmlSerializable { + +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventDiscovery.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventDiscovery.java new file mode 100644 index 00000000000..ac36136ca46 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventDiscovery.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.webdav.observation; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * EventDiscovery represents the request body of a successful + * POLL request. It reveals all events that occurred since the last POLL. The + * definition what events that particular subscription is interested in was + * specified with the initial SUBSCRIPTION that started the event listening. + */ +public class EventDiscovery implements ObservationConstants, XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(EventDiscovery.class); + + private final List bundles = new ArrayList(); + + /** + * Add the Xml representation of an single 'eventBundle' listing the + * events that resulted from a change in the server, filtered by the + * restrictions present in the corresponding subscription. + * + * @param eventBundle + * @see Subscription + */ + public void addEventBundle(EventBundle eventBundle) { + if (eventBundle != null) { + bundles.add(eventBundle); + } + } + + /** + * Returns an iterator over the {@link EventBundle event bundles} currently + * present on this discovery. + * + * @return iterator over event bundles present. + */ + public Iterator getEventBundles() { + return bundles.iterator(); + } + + /** + * Returns true, if this event discovery does not report any events (thus + * {@link #getEventBundles()} would return an empty iterator. + * + * @return true if {@link #getEventBundles()} would return an empty iterator, + * false otherwise. + */ + public boolean isEmpty() { + return bundles.isEmpty(); + } + + /** + * Returns the Xml representation of this EventDiscovery as + * being present in the POLL response body. + * + * @return Xml representation + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Element ed = DomUtil.createElement(document, XML_EVENTDISCOVERY, NAMESPACE); + for (EventBundle bundle : bundles) { + ed.appendChild(bundle.toXml(document)); + } + return ed; + } + + /** + * Build a EventDiscovery from the specified xml element. + * + * @param eventDiscoveryElement + * @return new EventDiscovery instance. + * @throws IllegalArgumentException if the given document is null + * or does not provide the required element. + */ + public static EventDiscovery createFromXml(Element eventDiscoveryElement) { + if (!DomUtil.matches(eventDiscoveryElement, XML_EVENTDISCOVERY, ObservationConstants.NAMESPACE)) { + throw new IllegalArgumentException( + "{" + ObservationConstants.NAMESPACE + "}" + XML_EVENTDISCOVERY + " element expected, but got: {" + + eventDiscoveryElement.getNamespaceURI() + "}" + eventDiscoveryElement.getLocalName()); + } + EventDiscovery eventDiscovery = new EventDiscovery(); + ElementIterator it = DomUtil.getChildren(eventDiscoveryElement, XML_EVENTBUNDLE, ObservationConstants.NAMESPACE); + while (it.hasNext()) { + final Element ebElement = it.nextElement(); + EventBundle eb = new EventBundle() { + public Element toXml(Document document) { + return (Element) document.importNode(ebElement, true); + } + }; + eventDiscovery.addEventBundle(eb); + } + return eventDiscovery; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventType.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventType.java new file mode 100644 index 00000000000..3813e4d1e34 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/EventType.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.webdav.observation; + +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; + +/** + * EventType... + */ +public interface EventType extends XmlSerializable { + + public String getName(); + + public Namespace getNamespace(); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/Filter.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/Filter.java new file mode 100644 index 00000000000..28635ca8b42 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/Filter.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.webdav.observation; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Filter... + */ +public class Filter implements XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(Filter.class); + + private final String filterName; + private final Namespace filterNamespace; + private final String filterValue; + + public Filter(String filterName, Namespace filterNamespace, String filterValue) { + if (filterName == null) { + throw new IllegalArgumentException("filterName must not be null."); + } + this.filterName = filterName; + this.filterNamespace = filterNamespace; + this.filterValue = filterValue; + } + + public Filter(Element filterElem) { + filterName = filterElem.getLocalName(); + filterNamespace = DomUtil.getNamespace(filterElem); + filterValue = DomUtil.getTextTrim(filterElem); + } + + public String getName() { + return filterName; + } + + public Namespace getNamespace() { + return filterNamespace; + } + + public String getValue() { + return filterValue; + } + + public boolean isMatchingFilter(String localName, Namespace namespace) { + boolean matchingNsp = (filterNamespace == null) ? namespace == null : filterNamespace.equals(namespace); + return filterName.equals(localName) && matchingNsp; + } + + public Element toXml(Document document) { + return DomUtil.createElement(document, filterName, filterNamespace, filterValue); + } + +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationConstants.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationConstants.java new file mode 100644 index 00000000000..f103c5d5482 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationConstants.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.webdav.observation; + +import javax.xml.namespace.QName; + +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * ObservationConstants interface provide constants for request + * and response headers, Xml elements and property names used for handling + * observation over WebDAV. There exists no public standard for this + * functionality. + */ +public interface ObservationConstants { + + /** + * The namespace + */ + public static final Namespace NAMESPACE = Namespace.getNamespace("dcr", "http://www.day.com/jcr/webdav/1.0"); + + //---< Headers >------------------------------------------------------------ + /** + * The SubscriptionId request header
        + */ + public static final String HEADER_SUBSCRIPTIONID = "SubscriptionId"; + + /** + * The PollTimeout request header. + */ + public static final String HEADER_POLL_TIMEOUT = "PollTimeout"; + + //---< XML Element, Attribute Names >--------------------------------------- + /** + * subscription Xml element
        + * Mandatory element inside the {@link #SUBSCRIPTIONDISCOVERY subscriptiondiscovery} + * property indicating the event listeners present for this session.
        + * NOTE, that this will not reveal any subscription made by another session. + */ + public static final String XML_SUBSCRIPTION = "subscription"; + + /** + * Xml elements + */ + public static final String XML_SUBSCRIPTIONINFO = "subscriptioninfo"; + + public static final String XML_EVENTTYPE = "eventtype"; + public static final String XML_NOLOCAL = "nolocal"; + public static final String XML_FILTER = "filter"; + public static final String XML_SUBSCRIPTIONID = "subscriptionid"; + public static final String XML_EVENTSWITHTYPES = "eventswithnodetypes"; + public static final String XML_EVENTSWITHLOCALFLAG = "eventswithlocalflag"; + public static final String XML_UUID = "uuid"; + public static final String XML_NODETYPE_NAME = "nodetype-name"; + + public static final String XML_EVENTDISCOVERY = "eventdiscovery"; + public static final String XML_EVENTBUNDLE = "eventbundle"; + public static final String XML_EVENT_TRANSACTION_ID = "transactionid"; + public static final String XML_EVENT_LOCAL = "local"; + public static final String XML_EVENT = "event"; + public static final String XML_EVENTUSERID = "eventuserid"; + public static final String XML_EVENTUSERDATA = "eventuserdata"; + public static final String XML_EVENTDATE = "eventdate"; + public static final String XML_EVENTIDENTIFIER = "eventidentifier"; + public static final String XML_EVENTINFO = "eventinfo"; + public static final String XML_EVENTPRIMARNODETYPE = "eventprimarynodetype"; + public static final String XML_EVENTMIXINNODETYPE = "eventmixinnodetype"; + + public static final QName N_EVENT = new QName(NAMESPACE.getURI(), XML_EVENT); + public static final QName N_EVENTBUNDLE = new QName(NAMESPACE.getURI(), XML_EVENTBUNDLE); + public static final QName N_EVENTDATE = new QName(NAMESPACE.getURI(), XML_EVENTDATE); + public static final QName N_EVENTDISCOVERY = new QName(NAMESPACE.getURI(), XML_EVENTDISCOVERY); + public static final QName N_EVENTINFO = new QName(NAMESPACE.getURI(), XML_EVENTINFO); + public static final QName N_EVENTMIXINNODETYPE = new QName(NAMESPACE.getURI(), XML_EVENTMIXINNODETYPE); + public static final QName N_EVENTPRIMARYNODETYPE = new QName(NAMESPACE.getURI(), XML_EVENTPRIMARNODETYPE); + public static final QName N_EVENTTYPE = new QName(NAMESPACE.getURI(), XML_EVENTTYPE); + public static final QName N_EVENTUSERDATA = new QName(NAMESPACE.getURI(), XML_EVENTUSERDATA); + public static final QName N_EVENTUSERID = new QName(NAMESPACE.getURI(), XML_EVENTUSERID); + + //---< Property Names >----------------------------------------------------- + /** + * The protected subscription discovery property is used to find out about + * existing subscriptions present on the specified resource. + */ + public static final DavPropertyName SUBSCRIPTIONDISCOVERY = DavPropertyName.create("subscriptiondiscovery", NAMESPACE); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletRequest.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletRequest.java new file mode 100644 index 00000000000..a3ec64850c0 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletRequest.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.webdav.observation; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletRequest; + +/** + * ObservationDavServletRequest provides extensions to the + * {@link DavServletRequest} interface used for dealing with observation. + */ +public interface ObservationDavServletRequest extends DavServletRequest { + + /** + * Return the {@link ObservationConstants#HEADER_SUBSCRIPTIONID SubscriptionId header} + * or null if no such header is present. + * + * @return the {@link ObservationConstants#HEADER_SUBSCRIPTIONID SubscriptionId header} + */ + public String getSubscriptionId(); + + /** + * Returns the {@link ObservationConstants#HEADER_POLL_TIMEOUT PollTimeout header} + * or 0 (zero) if no such header is present. + * + * @return milliseconds indicating length of the poll timeout. + */ + public long getPollTimeout(); + + /** + * Return a {@link SubscriptionInfo} object representing the subscription + * info present in the SUBSCRIBE request body or null if + * retrieving the subscription info fails. + * + * @return subscription info object encapsulating the SUBSCRIBE request body + * or null if the subscription info cannot be built. + * @throws DavException if an invalid request body was encountered. + */ + public SubscriptionInfo getSubscriptionInfo() throws DavException; +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletResponse.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletResponse.java new file mode 100644 index 00000000000..77d6de3fe0e --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationDavServletResponse.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.webdav.observation; + +import org.apache.jackrabbit.webdav.DavServletResponse; + +import java.io.IOException; + +/** + * ObservationDavServletResponse provides extensions to the + * {@link DavServletResponse} interface used for dealing with observation. + */ +public interface ObservationDavServletResponse extends DavServletResponse { + + /** + * Send the response to a successful SUBSCRIBE request. + * + * @param subscription that needs to be represented in the response body. + * @throws IOException + */ + public void sendSubscriptionResponse(Subscription subscription) throws IOException; + + /** + * Send the response to a successful POLL request. + * + * @param eventDiscovery {@link EventDiscovery} object to be returned in + * the response body. + * @throws IOException + */ + public void sendPollResponse(EventDiscovery eventDiscovery) throws IOException; +} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationResource.java similarity index 77% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationResource.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationResource.java index c1af6ba89c8..d27b7da7342 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/observation/ObservationResource.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/ObservationResource.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -17,7 +18,6 @@ import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; -import org.apache.jackrabbit.webdav.DavSession; /** * ObservationResource extends the {@link DavResource} interface by @@ -25,7 +25,6 @@ */ public interface ObservationResource extends DavResource { - public String COMPLIANCE_CLASS = "observation"; public String METHODS = "SUBSCRIBE, UNSUBSCRIBE, POLL"; /** @@ -35,13 +34,6 @@ public interface ObservationResource extends DavResource { */ public void init(SubscriptionManager subsMgr); - /** - * Retrieve the DavSession associated with this resource. - * - * @return session object associated with this resource. - */ - public DavSession getSession(); - /** * Subscribe this resource for event listening defined by the specified * subscription info. A subscriptionId may be specified in case an existing @@ -71,7 +63,10 @@ public interface ObservationResource extends DavResource { * * @param subscriptionId as present in the * {@link ObservationConstants#HEADER_SUBSCRIPTIONID SubscriptionId} header. + * @param timeout as present in the + * {@link ObservationConstants#HEADER_POLL_TIMEOUT} header or 0 (zero) if + * none is present. * @return EventDiscovery object */ - public EventDiscovery poll(String subscriptionId) throws DavException; + public EventDiscovery poll(String subscriptionId, long timeout) throws DavException; } \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/Subscription.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/Subscription.java new file mode 100644 index 00000000000..0c7c35abbe3 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/Subscription.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.webdav.observation; + +import org.apache.jackrabbit.webdav.xml.XmlSerializable; + +/** + * Subscription represents public representation of the event + * listener created (or modified) by a successful SUBSCRIBE request.
        + * Please note that this interface extends the XmlSerializable + * interface. The Xml representation of a Subscription is + * returned in the response to a successful SUBSCRIBE request as well + * as in a PROPFIND request. In both cases the subscription is packed into + * a {@link SubscriptionDiscovery} property object. + */ +public interface Subscription extends XmlSerializable { + + /** + * Returns the id of this subscription, that must be used for un-subscribing + * as well as for event discovery later on. + * + * @return subscriptionId + */ + public String getSubscriptionId(); + + /** + * @return whether events will be returned with node type information + */ + public boolean eventsProvideNodeTypeInformation(); + + /** + * @return whether events will be returned with the "noLocal" flag + */ + public boolean eventsProvideNoLocalFlag(); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionDiscovery.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionDiscovery.java new file mode 100644 index 00000000000..46750b2580d --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionDiscovery.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.webdav.observation; + +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.apache.jackrabbit.webdav.DavConstants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.ArrayList; + +/** + * SubscriptionDiscovery encapsulates the 'subscriptiondiscovery' + * property of a webdav resource. + */ +public class SubscriptionDiscovery extends AbstractDavProperty { + + private final Subscription[] subscriptions; + + /** + * Create a new SubscriptionDiscovery that lists the given + * subscriptions. + * + * @param subscriptions + */ + public SubscriptionDiscovery(Subscription[] subscriptions) { + super(ObservationConstants.SUBSCRIPTIONDISCOVERY, true); + if (subscriptions != null) { + this.subscriptions = subscriptions; + } else { + this.subscriptions = new Subscription[0]; + } + } + + /** + * Create a new SubscriptionDiscovery that contains a single + * subscription entry. + * + * @param subscription + */ + public SubscriptionDiscovery(Subscription subscription) { + super(ObservationConstants.SUBSCRIPTIONDISCOVERY, true); + if (subscription != null) { + this.subscriptions = new Subscription[]{subscription}; + } else { + this.subscriptions = new Subscription[0]; + } + } + + /** + * Returns an array of {@link Subscription}s. + * + * @return an array of {@link Subscription}s + * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() + */ + public Subscription[] getValue() { + return subscriptions; + } + + /** + * Returns the Xml representation of the subscription discovery. + * + * @return Xml representation + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + for (Subscription subscription : subscriptions) { + elem.appendChild(subscription.toXml(document)); + } + return elem; + } + + //-----------------------------------------------------< static Factory >--- + public static SubscriptionDiscovery createFromXml(Element sDiscoveryElement) { + if (!DomUtil.matches(sDiscoveryElement, ObservationConstants.SUBSCRIPTIONDISCOVERY.getName(), ObservationConstants.SUBSCRIPTIONDISCOVERY.getNamespace())) { + throw new IllegalArgumentException("'subscriptiondiscovery' element expected."); + } + + List subscriptions = new ArrayList(); + ElementIterator it = DomUtil.getChildren(sDiscoveryElement, ObservationConstants.XML_SUBSCRIPTION, ObservationConstants.NAMESPACE); + while (it.hasNext()) { + final Element sb = it.nextElement(); + // anonymous inner class: Subscription interface + Subscription s = new Subscription() { + /** + * @see Subscription#getSubscriptionId() + */ + public String getSubscriptionId() { + Element ltEl = DomUtil.getChildElement(sb, ObservationConstants.XML_SUBSCRIPTIONID, ObservationConstants.NAMESPACE); + if (ltEl != null) { + return DomUtil.getChildText(sb, DavConstants.XML_HREF, DavConstants.NAMESPACE); + } + return null; + } + + public boolean eventsProvideNodeTypeInformation() { + String t = DomUtil.getChildText(sb, ObservationConstants.XML_EVENTSWITHTYPES, ObservationConstants.NAMESPACE); + return t == null ? false : Boolean.parseBoolean(t); + } + + public boolean eventsProvideNoLocalFlag() { + String t = DomUtil.getChildText(sb, ObservationConstants.XML_EVENTSWITHLOCALFLAG, ObservationConstants.NAMESPACE); + return t == null ? false : Boolean.parseBoolean(t); + } + + /** + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + return (Element) document.importNode(sb, true); + } + }; + subscriptions.add(s); + } + + return new SubscriptionDiscovery(subscriptions.toArray(new Subscription[subscriptions.size()])); + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionInfo.java new file mode 100644 index 00000000000..4b4feb727a6 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionInfo.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.webdav.observation; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.TimeoutHeader; +import org.apache.jackrabbit.webdav.header.DepthHeader; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.List; + +/** + * SubscriptionInfo class encapsulates the subscription info + * that forms the request body of a SUBSCRIBE request.
        + * The following xml layout is defined for the subscription info: + *
        + * <!ELEMENT subscriptioninfo ( eventtype, nolocal?, filter? ) >
        + * <!ELEMENT eventtype ANY >
        + *
        + * ANY defines any sequence of elements where at least one defines a valid
        + * eventtype. Note that a single eventtype must not occur multiple times.
        +
        + * <!ELEMENT nolocal EMPTY >
        + * <!ELEMENT filter ANY >
        + *
        + * ANY: any sequence of elements identifying a filter for event listening but
        + * at least a single element.
        + * 
        + * @see ObservationConstants#XML_SUBSCRIPTIONINFO + */ +public class SubscriptionInfo implements ObservationConstants, XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(SubscriptionInfo.class); + + private final EventType[] eventTypes; + private final Filter[] filters; + private final boolean noLocal; + private final boolean isDeep; + private final long timeout; + + /** + * Create a new SubscriptionInfo + * + * @param eventTypes + * @param isDeep + * @param timeout + */ + public SubscriptionInfo(EventType[] eventTypes, boolean isDeep, long timeout) { + this(eventTypes, null, false, isDeep, timeout); + } + + /** + * Create a new SubscriptionInfo + * + * @param eventTypes + * @param filters + * @param noLocal + * @param isDeep + * @param timeout + */ + public SubscriptionInfo(EventType[] eventTypes, Filter[] filters, boolean noLocal, boolean isDeep, long timeout) { + if (eventTypes == null || eventTypes.length == 0) { + throw new IllegalArgumentException("'subscriptioninfo' must at least indicate a single event type."); + } + + this.eventTypes = eventTypes; + this.noLocal = noLocal; + + if (filters != null) { + this.filters = filters; + } else { + this.filters = new Filter[0]; + } + + this.isDeep = isDeep; + this.timeout = timeout; + } + + /** + * Create a new SubscriptionInfo from the given Xml element + * and from additional information that is transported within the request + * header: + *
          + *
        • {@link TimeoutHeader timeout},
        • + *
        • {@link DepthHeader isDeep}
        • + *
        + * @param reqInfo Xml element present in the request body. + * @param timeout as defined in the {@link org.apache.jackrabbit.webdav.DavConstants#HEADER_TIMEOUT timeout header}. + * @param isDeep as defined in the {@link org.apache.jackrabbit.webdav.DavConstants#HEADER_DEPTH depth header}. + * @throws IllegalArgumentException if the reqInfo element does not contain the mandatory elements. + */ + public SubscriptionInfo(Element reqInfo, long timeout, boolean isDeep) throws DavException { + if (!DomUtil.matches(reqInfo, XML_SUBSCRIPTIONINFO, NAMESPACE)) { + log.warn("Element with name 'subscriptioninfo' expected"); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + Element el = DomUtil.getChildElement(reqInfo, XML_EVENTTYPE, NAMESPACE); + if (el != null) { + eventTypes = DefaultEventType.createFromXml(el); + if (eventTypes.length == 0) { + log.warn("'subscriptioninfo' must at least indicate a single, valid event type."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } else { + log.warn("'subscriptioninfo' must contain an 'eventtype' child element."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + + List filters = new ArrayList(); + el = DomUtil.getChildElement(reqInfo, XML_FILTER, NAMESPACE); + if (el != null) { + ElementIterator it = DomUtil.getChildren(el); + while (it.hasNext()) { + Filter f = new Filter(it.nextElement()); + filters.add(f); + } + } + this.filters = filters.toArray(new Filter[filters.size()]); + + this.noLocal = DomUtil.hasChildElement(reqInfo, XML_NOLOCAL, NAMESPACE); + this.isDeep = isDeep; + this.timeout = timeout; + } + + /** + * Return array of event type names present in the subscription info. + * + * @return array of String defining the names of the events this subscription + * should listen to. + * + */ + public EventType[] getEventTypes() { + return eventTypes; + } + + /** + * Return all filters defined for this SubscriptionInfo + * + * @return all filters or an empty Filter array. + */ + public Filter[] getFilters() { + return filters; + } + + /** + * Return array of filters with the specified name. + * + * @param localName the filter elements must provide. + * @param namespace + * @return array containing the text of the filter elements with the given + * name. + */ + public Filter[] getFilters(String localName, Namespace namespace) { + List l = new ArrayList(); + for (Filter filter : filters) { + if (filter.isMatchingFilter(localName, namespace)) { + l.add(filter); + } + } + return l.toArray(new Filter[l.size()]); + } + + /** + * Returns true if the {@link #XML_NOLOCAL} element is present in this + * subscription info. + * + * @return if {@link #XML_NOLOCAL} element is present. + */ + public boolean isNoLocal() { + return noLocal; + } + + /** + * Returns true if the {@link org.apache.jackrabbit.webdav.DavConstants#HEADER_DEPTH + * depths header} defined a depth other than '0'. + * + * @return true if this subscription info was created with isDeep + * true. + */ + public boolean isDeep() { + return isDeep; + } + + /** + * Return the timeout as retrieved from the request. + * + * @return timeout. + */ + public long getTimeOut() { + return timeout; + } + + /** + * Xml representation of this SubscriptionInfo. + * + * @return Xml representation + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Element subscrInfo = DomUtil.createElement(document, XML_SUBSCRIPTIONINFO, NAMESPACE); + Element eventType = DomUtil.addChildElement(subscrInfo, XML_EVENTTYPE, NAMESPACE); + for (EventType et : eventTypes) { + eventType.appendChild(et.toXml(document)); + } + + if (filters.length > 0) { + Element filter = DomUtil.addChildElement(subscrInfo, XML_FILTER, NAMESPACE); + for (Filter f : filters) { + filter.appendChild(f.toXml(document)); + } + } + + if (noLocal) { + DomUtil.addChildElement(subscrInfo, XML_NOLOCAL, NAMESPACE); + } + return subscrInfo; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionManager.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionManager.java new file mode 100644 index 00000000000..30541a9a744 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/SubscriptionManager.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.webdav.observation; + +import org.apache.jackrabbit.webdav.DavException; + +/** + * SubscriptionManager interface. + */ +public interface SubscriptionManager { + + /** + * Retrieve the {@link org.apache.jackrabbit.webdav.observation.SubscriptionDiscovery} object for the given + * resource. Note, that the discovery object will be empty if there are + * no subscriptions present. + * + * @param resource + */ + public SubscriptionDiscovery getSubscriptionDiscovery(ObservationResource resource); + + /** + * Create a new Subscription or update an existing Subscription.. + * + * @param info + * @param subscriptionId + * @param resource + * @return Subscription that has been created or updated + * @throws DavException if the subscription fails + */ + public Subscription subscribe(SubscriptionInfo info, String subscriptionId, + ObservationResource resource) + throws DavException; + + /** + * Unsubscribe the Subscription with the given id. + * + * @param subscriptionId + * @param resource + * @throws DavException + */ + public void unsubscribe(String subscriptionId, ObservationResource resource) + throws DavException; + + /** + * Retrieve the list of events that occurred since the last poll. + * + * @param subscriptionId identifier for the subscription + * @param timeout the time in milliseconds to wait at most for events + * if none is present currently. + * @param resource + * @return + */ + public EventDiscovery poll(String subscriptionId, long timeout, ObservationResource resource) + throws DavException; +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/package-info.java new file mode 100644 index 00000000000..36dd57f82f8 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/observation/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.observation; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderPatch.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderPatch.java new file mode 100644 index 00000000000..e574e0e02dc --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderPatch.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.ordering; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.List; + +/** + * OrderPatch represents the mandatory request body of an + * ORDERPATCH request. RFC 3648 defines the following structure for it:
        + *
        + * <!ELEMENT orderpatch (ordering-type?, order-member*) >
        + * <!ELEMENT order-member (segment, position) >
        + * <!ELEMENT position (first | last | before | after) >
        + * <!ELEMENT segment (#PCDATA) >
        + * <!ELEMENT first EMPTY >
        + * <!ELEMENT last EMPTY >
        + * <!ELEMENT before segment >
        + * <!ELEMENT after segment >
        + * 
        + */ +public class OrderPatch implements OrderingConstants, XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(OrderPatch.class); + + private Member[] instructions; + private String orderingType; + + /** + * Create a new OrderPath object. + * + * @param orderingType + * @param instruction + */ + public OrderPatch(String orderingType, Member instruction) { + this(orderingType, new Member[] {instruction}); + } + + /** + * Create a new OrderPath object. + * + * @param orderingType + * @param instructions + */ + public OrderPatch(String orderingType, Member[] instructions) { + if (orderingType == null || instructions == null) { + throw new IllegalArgumentException("ordering type and instructions cannot be null."); + } + this.orderingType = orderingType; + this.instructions = instructions; + } + + /** + * Return the ordering type. + * + * @return ordering type + */ + public String getOrderingType() { + return orderingType; + } + + /** + * Return an array of {@link Member} objects defining the re-ordering + * instructions to be applied to the requested resource. + * + * @return ordering instructions. + */ + public Member[] getOrderInstructions() { + return instructions; + } + + //------------------------------------------< XmlSerializable interface >--- + /** + * + * @return + * @param document + */ + public Element toXml(Document document) { + Element orderPatch = DomUtil.createElement(document, XML_ORDERPATCH, NAMESPACE); + // add DAV:ordering-type below DAV:orderpatch + Element otype = DomUtil.addChildElement(orderPatch, XML_ORDERING_TYPE, NAMESPACE); + otype.appendChild(DomUtil.hrefToXml(orderingType, document)); + // add DAV:member elements below DAV:orderpatch + for (Member instruction : instructions) { + orderPatch.appendChild(instruction.toXml(document)); + } + return orderPatch; + } + + //------------------------------------------------------< static method >--- + /** + * Create a new OrderPath object. + * + * @param orderPatchElement + * @throws IllegalArgumentException if the specified Xml element was not valid. + */ + public static OrderPatch createFromXml(Element orderPatchElement) throws DavException { + if (!DomUtil.matches(orderPatchElement, XML_ORDERPATCH, NAMESPACE)) { + log.warn("ORDERPATH request body must start with an 'orderpatch' element."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + + // retrieve the href of the orderingtype element + String orderingType; + Element otype = DomUtil.getChildElement(orderPatchElement, XML_ORDERING_TYPE, NAMESPACE); + if (otype != null) { + orderingType = DomUtil.getChildText(otype, DavConstants.XML_HREF, DavConstants.NAMESPACE); + } else { + log.warn("ORDERPATH request body must contain an 'ordering-type' child element."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + + // set build the list of ordering instructions + List tmpList = new ArrayList(); + ElementIterator it = DomUtil.getChildren(orderPatchElement, XML_ORDER_MEMBER, NAMESPACE); + while (it.hasNext()) { + Element el = it.nextElement(); + try { + // retrieve text 'DAV:segment' child of this DAV:order-member element + String segment = DomUtil.getChildText(el, XML_SEGMENT, NAMESPACE); + // retrieve the 'DAV:position' child element + Position pos = Position.createFromXml(DomUtil.getChildElement(el, XML_POSITION, NAMESPACE)); + Member om = new Member(segment, pos); + tmpList.add(om); + } catch (IllegalArgumentException e) { + log.warn("Invalid element in 'orderpatch' request body: " + e.getMessage()); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } + Member[] instructions = tmpList.toArray(new Member[tmpList.size()]); + return new OrderPatch(orderingType, instructions); + } + + //-------------------------------------------------------------------------- + /** + * Internal class Member represents the 'Order-Member' children + * elements of an 'OrderPatch' request body present in the ORDERPATCH request. + */ + public static class Member implements XmlSerializable { + + private String memberHandle; + private Position position; + + /** + * Create a new Member object. + * + * @param memberHandle + * @param position + */ + public Member(String memberHandle, Position position) { + this.memberHandle = memberHandle; + this.position = position; + } + + /** + * Return the handle of the internal member to be reordered. + * + * @return handle of the internal member. + */ + public String getMemberHandle() { + return memberHandle; + } + + /** + * Return the position where the internal member identified by the + * member handle should be placed. + * + * @return position for the member after the request. + * @see #getMemberHandle() + */ + public Position getPosition() { + return position; + } + + //--------------------------------------< XmlSerializable interface >--- + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element memberElem = DomUtil.createElement(document, XML_ORDER_MEMBER, NAMESPACE); + DomUtil.addChildElement(memberElem, XML_SEGMENT, NAMESPACE, memberHandle); + memberElem.appendChild(position.toXml(document)); + return memberElem; + } + + } +} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingConstants.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingConstants.java similarity index 82% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingConstants.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingConstants.java index 1e0c89d2122..879ad2b61ec 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingConstants.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingConstants.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -15,8 +16,9 @@ */ package org.apache.jackrabbit.webdav.ordering; -import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.Namespace; /** * OrderingConstants provide constants for request and response @@ -25,6 +27,25 @@ */ public interface OrderingConstants { + /** + * The namespace + */ + public static final Namespace NAMESPACE = DavConstants.NAMESPACE; + + /** + * Constant representing the DAV:custom ordering type URI, which indicates + * that the collection is not ordered. + */ + public static final String ORDERING_TYPE_CUSTOM = "DAV:custom"; + + /** + * Constant representing the DAV:unordered ordering type URI, which indicates + * that the collection is to be ordered, but the semantics of the ordering + * is not being advertised. + */ + public static final String ORDERING_TYPE_UNORDERED = "DAV:unordered"; + + //---< Headers >------------------------------------------------------------ /** * The "Ordering-Type" request header. */ @@ -41,6 +62,7 @@ public interface OrderingConstants { */ public static final String HEADER_POSITION = "Position"; + //---< XML Element, Attribute Names >--------------------------------------- /** * Xml elements used for reordering internal members of a collection. */ @@ -55,19 +77,7 @@ public interface OrderingConstants { public static final String XML_BEFORE = "before"; public static final String XML_AFTER = "after"; - /** - * Constant representing the DAV:custom ordering type URI, which indicates - * that the collection is not ordered. - */ - public static final String ORDERING_TYPE_CUSTOM = "DAV:custom"; - - /** - * Constant representing the DAV:unordered ordering type URI, which indicates - * that the collection is to be ordered, but the semantics of the ordering - * is not being advertised. - */ - public static final String ORDERING_TYPE_UNORDERED = "DAV:unordered"; - + //---< XML Element, Attribute Names >--------------------------------------- /** * The DAV:ordering-type property indicates whether the collection is * ordered and, if so, uniquely identifies the semantics of the ordering. diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingDavServletRequest.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingDavServletRequest.java similarity index 76% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingDavServletRequest.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingDavServletRequest.java index 94bf80ad17f..a077850a6a3 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingDavServletRequest.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingDavServletRequest.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -15,6 +16,7 @@ */ package org.apache.jackrabbit.webdav.ordering; +import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavServletRequest; /** @@ -48,6 +50,6 @@ public interface OrderingDavServletRequest extends DavServletRequest { * * @return OrderPatch object encapsulating the request body. */ - public OrderPatch getOrderPatch(); + public OrderPatch getOrderPatch() throws DavException; } \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingResource.java similarity index 76% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingResource.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingResource.java index 32a80630af1..e57981206f9 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/ordering/OrderingResource.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingResource.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -15,8 +16,8 @@ */ package org.apache.jackrabbit.webdav.ordering; -import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; /** * OrderingResource extends the {@link DavResource} interface by @@ -25,7 +26,6 @@ */ public interface OrderingResource extends DavResource { - public String COMPLIANCE_CLASS = "ordered-collections"; public String METHODS = "ORDERPATCH"; /** diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingType.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingType.java new file mode 100644 index 00000000000..91f1fb7a50d --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/OrderingType.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.webdav.ordering; + +import org.apache.jackrabbit.webdav.property.HrefProperty; + +/** + * OrderingType represents the {@link #ORDERING_TYPE + * DAV:ordering-type} property as defined by + * RFC 3648. This property is + * protected cannot be set using PROPPATCH. Its value may only be set by + * including the Ordering-Type header with a MKCOL request or by submitting an + * ORDERPATCH request. + * + * @see org.apache.jackrabbit.webdav.property.DavProperty#isInvisibleInAllprop() + */ +public class OrderingType extends HrefProperty implements OrderingConstants { + + /** + * Creates a OrderingType with the default type (e.g. default + * value). The default value is specified to be {@link #ORDERING_TYPE_UNORDERED}. + */ + public OrderingType() { + this(null); + } + + /** + * Create an OrderingType with the given ordering.
        + * NOTE: the ordering-type property is defined to be protected. + * + * @param href + * @see org.apache.jackrabbit.webdav.property.DavProperty#isInvisibleInAllprop() + */ + public OrderingType(String href) { + // spec requires that the default value is 'DAV:unordered' + super(ORDERING_TYPE, (href != null) ? href : ORDERING_TYPE_UNORDERED, true); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/Position.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/Position.java new file mode 100644 index 00000000000..138a8874e0f --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/Position.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.ordering; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashSet; +import java.util.Set; + +/** + * Position encapsulates the position in ordering information + * contained in a Webdav request. This includes both the + * {@link OrderingConstants#HEADER_POSITION Position header} and the position + * Xml element present in the request body of an ORDERPATCH request. + * + * @see OrderingConstants#HEADER_POSITION + * @see OrderingConstants#XML_POSITION + * @see OrderPatch + */ +public class Position implements OrderingConstants, XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(Position.class); + + private static final Set VALID_TYPES = new HashSet(); + static { + VALID_TYPES.add(XML_FIRST); + VALID_TYPES.add(XML_LAST); + VALID_TYPES.add(XML_AFTER); + VALID_TYPES.add(XML_BEFORE); + } + + private final String type; + private final String segment; + + /** + * Create a new Position object with the specified type. + * Since any type except for {@link #XML_FIRST first} and {@link #XML_LAST last} + * must be combined with a segment, only the mentioned types are valid + * arguments. + * + * @param type {@link #XML_FIRST first} or {@link #XML_LAST last} + * @throws IllegalArgumentException if the given type is other than {@link #XML_FIRST} + * or {@link #XML_LAST}. + */ + public Position(String type) { + if (!VALID_TYPES.contains(type)) { + throw new IllegalArgumentException("Invalid type: " + type); + } + if (!(XML_FIRST.equals(type) || XML_LAST.equals(type))) { + throw new IllegalArgumentException("If type is other than 'first' or 'last' a segment must be specified"); + } + this.type = type; + this.segment = null; + } + + /** + * Create a new Position object with the specified type and + * segment. + * + * @param type + * @param segment + * @throws IllegalArgumentException if the specified type and segment do not + * form a valid pair. + */ + public Position(String type, String segment) { + if (!VALID_TYPES.contains(type)) { + throw new IllegalArgumentException("Invalid type: " + type); + } + if ((XML_AFTER.equals(type) || XML_BEFORE.equals(type)) && (segment == null || "".equals(segment))) { + throw new IllegalArgumentException("If type is other than 'first' or 'last' a segment must be specified"); + } + this.type = type; + this.segment = segment; + } + + /** + * Return the type of this Position object, which may be any + * of the following valid types: {@link #XML_FIRST first}, + * {@link #XML_LAST last}, {@link #XML_AFTER after}, {@link #XML_BEFORE before} + * + * @return type + */ + public String getType() { + return type; + } + + /** + * Returns the segment used to create this Position object or + * null if no segment is present with the type. + * + * @return segment or null + * @see #getType() + */ + public String getSegment() { + return segment; + } + + //------------------------------------------< XmlSerializable interface >--- + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Element positionElement = DomUtil.createElement(document, XML_POSITION, NAMESPACE); + Element typeElement = DomUtil.addChildElement(positionElement, type, NAMESPACE); + if (segment != null) { + DomUtil.addChildElement(typeElement, XML_SEGMENT, NAMESPACE, segment); + } + return positionElement; + } + + //-----------------------------------------------------< static methods >--- + /** + * Create a new Position object from the specified position + * element. The element must fulfill the following structure:
        + *
        +     * <!ELEMENT position (first | last | before | after) >
        +     * <!ELEMENT segment (#PCDATA) >
        +     * <!ELEMENT first EMPTY >
        +     * <!ELEMENT last EMPTY >
        +     * <!ELEMENT before segment >
        +     * <!ELEMENT after segment >
        +     * 
        + * + * @param positionElement Xml element defining the position. + * @throws IllegalArgumentException if the given Xml element is not valid. + */ + public static Position createFromXml(Element positionElement) { + if (!DomUtil.matches(positionElement, XML_POSITION, NAMESPACE)) { + throw new IllegalArgumentException("The 'DAV:position' element required."); + } + ElementIterator it = DomUtil.getChildren(positionElement); + if (it.hasNext()) { + Element el = it.nextElement(); + String type = el.getLocalName(); + // read the text of DAV:segment child element inside the type + String segmentText = DomUtil.getChildText(el, XML_SEGMENT, NAMESPACE); + // stop after the first iteration + return new Position(type, segmentText); + } else { + throw new IllegalArgumentException("The 'DAV:position' element required with exact one child indicating the type."); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/package-info.java new file mode 100644 index 00000000000..71a8aa42e25 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/ordering/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.ordering; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/package-info.java new file mode 100644 index 00000000000..1673fd1e149 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.2.0") +package org.apache.jackrabbit.webdav; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/AbstractDavProperty.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/AbstractDavProperty.java new file mode 100644 index 00000000000..45d3f9ddceb --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/AbstractDavProperty.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.webdav.property; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.util.Collection; + +/** + * AbstractDavProperty provides generic METHODS used by various + * implementations of the {@link DavProperty} interface. + */ +public abstract class AbstractDavProperty implements DavProperty { + + private static Logger log = LoggerFactory.getLogger(AbstractDavProperty.class); + + private final DavPropertyName name; + private final boolean isInvisibleInAllprop; + + /** + * Create a new AbstractDavProperty with the given {@link DavPropertyName} + * and a boolean flag indicating whether this property should be suppressed + * in PROPFIND/allprop responses. + */ + public AbstractDavProperty(DavPropertyName name, boolean isInvisibleInAllprop) { + this.name = name; + this.isInvisibleInAllprop = isInvisibleInAllprop; + } + + /** + * Computes the hash code using this property's name and value. + * + * @return the hash code + */ + @Override + public int hashCode() { + int hashCode = getName().hashCode(); + if (getValue() != null) { + hashCode += getValue().hashCode(); + } + return hashCode % Integer.MAX_VALUE; + } + + /** + * Checks if this property has the same {@link DavPropertyName name} + * and value as the given one. + * + * @param obj the object to compare to + * @return true if the 2 objects are equal; + * false otherwise + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof DavProperty) { + DavProperty prop = (DavProperty) obj; + boolean equalName = getName().equals(prop.getName()); + boolean equalValue = (getValue() == null) ? prop.getValue() == null : getValue().equals(prop.getValue()); + return equalName && equalValue; + } + return false; + } + + + /** + * Return a XML element representation of this property. The value of the + * property will be added as text or as child element. + *
        +     * new DavProperty("displayname", "WebDAV Directory").toXml
        +     * gives a element like:
        +     * <D:displayname>WebDAV Directory</D:displayname>
        +     *
        +     * new DavProperty("resourcetype", new Element("collection")).toXml
        +     * gives a element like:
        +     * <D:resourcetype><D:collection/></D:resourcetype>
        +     *
        +     * Element[] customVals = { new Element("bla", customNamespace), new Element("bli", customNamespace) };
        +     * new DavProperty("custom-property", customVals, customNamespace).toXml
        +     * gives an element like
        +     * <Z:custom-property>
        +     *    <Z:bla/>
        +     *    <Z:bli/>
        +     * </Z:custom-property>
        +     * 
        + * + * @return a XML element of this property + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Element elem = getName().toXml(document); + T value = getValue(); + // todo: improve.... + if (value != null) { + if (value instanceof XmlSerializable) { + elem.appendChild(((XmlSerializable)value).toXml(document)); + } else if (value instanceof Node) { + Node n = document.importNode((Node)value, true); + elem.appendChild(n); + } else if (value instanceof Node[]) { + for (int i = 0; i < ((Node[])value).length; i++) { + Node n = document.importNode(((Node[])value)[i], true); + elem.appendChild(n); + } + } else if (value instanceof Collection) { + for (Object entry : ((Collection) value)) { + if (entry instanceof XmlSerializable) { + elem.appendChild(((XmlSerializable) entry).toXml(document)); + } else if (entry instanceof Node) { + Node n = document.importNode((Node) entry, true); + elem.appendChild(n); + } else { + DomUtil.setText(elem, entry.toString()); + } + } + } else { + DomUtil.setText(elem, value.toString()); + } + } + return elem; + } + + /** + * Returns the name of this property. + * + * @return name + * @see DavProperty#getName() + */ + public DavPropertyName getName() { + return name; + } + + /** + * Return true if this property should be suppressed + * in a PROPFIND/{@link DavConstants#PROPFIND_ALL_PROP DAV:allprop} + * response. See RFC 4918, Section 9.1. + * + * @see org.apache.jackrabbit.webdav.property.DavProperty#isInvisibleInAllprop() + */ + public boolean isInvisibleInAllprop() { + return isInvisibleInAllprop; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavProperty.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavProperty.java new file mode 100644 index 00000000000..a3bf270ccb4 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavProperty.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.webdav.property; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; + +/** + * The Property class represents a Property of a WebDAV + * resource. The {@link Object#hashCode()} and {@link Object#equals(Object)} methods are + * overridden in a way, such that the name and value of the property are + * respected. This means, a property is equal to another if the names + * and values are equal.
        + * The XML representation of a DavProperty: + *
        + * new DavProperty("displayname", "WebDAV Directory").toXml
        + * gives a element like:
        + * <D:displayname>WebDAV Directory</D:displayname>
        + *
        + * new DavProperty("resourcetype", new Element("collection")).toXml
        + * gives a element like:
        + * <D:resourcetype><D:collection/></D:resourcetype>
        + *
        + * Element[] customVals = { new Element("bla", customNamespace), new Element("bli", customNamespace) };
        + * new DavProperty("custom-property", customVals, customNamespace).toXml
        + * gives an element like
        + * <Z:custom-property>
        + *    <Z:bla/>
        + *    <Z:bli/>
        + * </Z:custom-property>
        + * 
        + */ +public interface DavProperty extends XmlSerializable, DavConstants, PropEntry { + + /** + * Returns the name of this property + * + * @return the name of this property + */ + public DavPropertyName getName(); + + /** + * Returns the value of this property + * + * @return the value of this property + */ + public T getValue(); + + /** + * Return true if this property should be suppressed + * in a PROPFIND/{@link DavConstants#PROPFIND_ALL_PROP DAV:allprop} + * response. See RFC 4918, Section 9.1. + * + * @return true, if this property should be suppressed in a PROPFIND/allprop response + */ + public boolean isInvisibleInAllprop(); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyIterator.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyIterator.java new file mode 100644 index 00000000000..6bb45d50b79 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyIterator.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.webdav.property; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * The DavPropertyIterator extends the Iterator by + * a property specific next() method. + */ +public interface DavPropertyIterator extends Iterator> { + /** + * Returns the next Property. + * + * @return the next Property in the iteration. + * @throws java.util.NoSuchElementException if iteration has no more elements. + */ + public DavProperty nextProperty() throws NoSuchElementException; +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyName.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyName.java new file mode 100644 index 00000000000..93c43345a65 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyName.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.property; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.Map; + +/** + * The DavPropertyName class reflects a WebDAV property name. It + * holds together the local name of the property and its namespace. + */ +public class DavPropertyName implements DavConstants, XmlSerializable, PropEntry { + + /** internal 'cache' of created property names */ + private static final Map> cache = new HashMap>(); + + /* some standard webdav property (that have #PCDATA) */ + public static final DavPropertyName CREATIONDATE = DavPropertyName.create(PROPERTY_CREATIONDATE); + public static final DavPropertyName DISPLAYNAME = DavPropertyName.create(PROPERTY_DISPLAYNAME); + public static final DavPropertyName GETCONTENTLANGUAGE = DavPropertyName.create(PROPERTY_GETCONTENTLANGUAGE); + public static final DavPropertyName GETCONTENTLENGTH = DavPropertyName.create(PROPERTY_GETCONTENTLENGTH); + public static final DavPropertyName GETCONTENTTYPE = DavPropertyName.create(PROPERTY_GETCONTENTTYPE); + public static final DavPropertyName GETETAG = DavPropertyName.create(PROPERTY_GETETAG); + public static final DavPropertyName GETLASTMODIFIED = DavPropertyName.create(PROPERTY_GETLASTMODIFIED); + + /* some standard webdav property (that have other elements) */ + public static final DavPropertyName LOCKDISCOVERY = DavPropertyName.create(PROPERTY_LOCKDISCOVERY); + public static final DavPropertyName RESOURCETYPE = DavPropertyName.create(PROPERTY_RESOURCETYPE); + public static final DavPropertyName SOURCE = DavPropertyName.create(PROPERTY_SOURCE); + public static final DavPropertyName SUPPORTEDLOCK = DavPropertyName.create(PROPERTY_SUPPORTEDLOCK); + + /* property use by microsoft that are not specified in the RFC 2518 */ + public static final DavPropertyName ISCOLLECTION = DavPropertyName.create("iscollection"); + + /** the name of the property */ + private final String name; + + /** the namespace of the property */ + private final Namespace namespace; + + /** + * Creates a new DavPropertyName with the given name and + * Namespace. + * + * @param name The local name of the new property name + * @param namespace The namespace of the new property name + * + * @return The WebDAV property name + */ + public synchronized static DavPropertyName create(String name, Namespace namespace) { + + // get (or create) map for the given namespace + Map map = cache.get(namespace); + if (map == null) { + map = new HashMap(); + cache.put(namespace, map); + } + // get (or create) property name object + DavPropertyName ret = map.get(name); + if (ret == null) { + if (namespace.equals(NAMESPACE)) { + // ensure prefix for default 'DAV:' namespace + namespace = NAMESPACE; + } + ret = new DavPropertyName(name, namespace); + map.put(name, ret); + } + return ret; + } + + /** + * Creates a new DavPropertyName with the given local name + * and the default WebDAV {@link DavConstants#NAMESPACE namespace}. + * + * @param name The local name of the new property name + * + * @return The WebDAV property name + */ + public synchronized static DavPropertyName create(String name) { + return create(name, NAMESPACE); + } + + /** + * Create a new DavPropertyName with the name and namespace + * of the given Xml element. + * + * @param nameElement + * @return DavPropertyName instance + */ + public synchronized static DavPropertyName createFromXml(Element nameElement) { + if (nameElement == null) { + throw new IllegalArgumentException("Cannot build DavPropertyName from a 'null' element."); + } + String ns = nameElement.getNamespaceURI(); + if (ns == null) { + return create(nameElement.getLocalName(), Namespace.EMPTY_NAMESPACE); + } else { + return create(nameElement.getLocalName(), Namespace.getNamespace(nameElement.getPrefix(), ns)); + } + } + + /** + * Creates a new DavPropertyName with the given name and + * Namespace. + * + * @param name The local name of the new property name + * @param namespace The namespace of the new property name + */ + private DavPropertyName(String name, Namespace namespace) { + if (name == null || namespace == null) { + throw new IllegalArgumentException("Name and namespace must not be 'null' for a DavPropertyName."); + } + this.name = name; + this.namespace = namespace; + } + + /** + * Return the name of this DavPropertyName. + * + * @return name + */ + public String getName() { + return name; + } + + /** + * Return the namespace of this DavPropertyName. + * + * @return namespace + */ + public Namespace getNamespace() { + return namespace; + } + + /** + * Computes the hash code using this properties name and namespace. + * + * @return the hash code + */ + @Override + public int hashCode() { + return (name.hashCode() + namespace.hashCode()) % Integer.MAX_VALUE; + } + + /** + * Checks if this property has the same name and namespace as the + * given one. + * + * @param obj the object to compare to + * + * @return true if the 2 objects are equal; + * false otherwise + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof DavPropertyName) { + DavPropertyName propName = (DavPropertyName) obj; + return name.equals(propName.name) && namespace.equals(propName.namespace); + } + return false; + } + + /** + * Returns a string representation of this property suitable for debugging + * + * @return a human readable string representation + */ + @Override + public String toString() { + return DomUtil.getExpandedName(name, namespace); + } + + /** + * Creates a element with the name and namespace of this + * DavPropertyName. + * + * @return A element with the name and namespace of this + * DavPropertyName. + * @param document + */ + public Element toXml(Document document) { + return DomUtil.createElement(document, name, namespace); + } +} + diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyNameIterator.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyNameIterator.java new file mode 100644 index 00000000000..2b160c4d722 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyNameIterator.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.webdav.property; + +import java.util.Iterator; + +/** + * DavPropertyNameIterator... + */ +public interface DavPropertyNameIterator extends Iterator { + + public DavPropertyName nextPropertyName(); + +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyNameSet.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyNameSet.java new file mode 100644 index 00000000000..2541418ada4 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertyNameSet.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.property; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * DavPropertyNameSet represents a Set of {@link DavPropertyName} + * objects. + */ +public class DavPropertyNameSet extends PropContainer + implements Iterable { + + private static Logger log = LoggerFactory.getLogger(DavPropertyNameSet.class); + private final Set set = new HashSet(); + + /** + * Create a new empty set. + */ + public DavPropertyNameSet() { + } + + /** + * Create a new DavPropertyNameSet with the given initial values. + * + * @param initialSet + */ + public DavPropertyNameSet(DavPropertyNameSet initialSet) { + addAll(initialSet); + } + + /** + * Create a new DavPropertyNameSet from the given DAV:prop + * element. + * + * @param propElement + * @throws IllegalArgumentException if the specified element is null + * or is not a DAV:prop element. + */ + public DavPropertyNameSet(Element propElement) { + if (!DomUtil.matches(propElement, XML_PROP, NAMESPACE)) { + throw new IllegalArgumentException("'DAV:prop' element expected."); + } + + // fill the set + ElementIterator it = DomUtil.getChildren(propElement); + while (it.hasNext()) { + add(DavPropertyName.createFromXml(it.nextElement())); + } + } + + /** + * Adds the specified {@link DavPropertyName} object to this + * set if it is not already present. + * + * @param propertyName element to be added to this set. + * @return true if the set did not already contain the specified + * element. + */ + public boolean add(DavPropertyName propertyName) { + return set.add(propertyName); + } + + /** + * Creates a DavPropertyName from the given parameters and add it to this set. + * + * @param localName + * @param namespace + * @return true if the set did not already contain the specified + * property name. + */ + public boolean add(String localName, Namespace namespace) { + return set.add(DavPropertyName.create(localName, namespace)); + } + + /** + * Add the property names contained in the specified set to this set. + * + * @param propertyNames + * @return true if the set has been modified by this call. + */ + public boolean addAll(DavPropertyNameSet propertyNames) { + return set.addAll(propertyNames.set); + } + + /** + * Removes the specified {@link DavPropertyName} object from this set. + * + * @param propertyName + * @return true if the given property name could be removed. + * @see HashSet#remove(Object) + */ + public boolean remove(DavPropertyName propertyName) { + return set.remove(propertyName); + } + + /** + * @return Iterator over all DavPropertyNames contained in this + * set. + */ + public DavPropertyNameIterator iterator() { + return new PropertyNameIterator(); + } + + //------------------------------------------------------< PropContainer >--- + /** + * @see PropContainer#contains(DavPropertyName) + */ + @Override + public boolean contains(DavPropertyName name) { + return set.contains(name); + } + + /** + * @param contentEntry NOTE that an instance of DavPropertyName + * in order to successfully add the given entry. + * @return true if contentEntry is an instance of DavPropertyName + * that could be added to this set. False otherwise. + * @see PropContainer#addContent(Object) + */ + @Override + public boolean addContent(PropEntry contentEntry) { + if (contentEntry instanceof DavPropertyName) { + return add((DavPropertyName) contentEntry); + } + log.debug("DavPropertyName object expected. Found: " + contentEntry.getClass().toString()); + return false; + } + + /** + * @see PropContainer#isEmpty() + */ + @Override + public boolean isEmpty() { + return set.isEmpty(); + } + + /** + * @see PropContainer#getContentSize() + */ + @Override + public int getContentSize() { + return set.size(); + } + + /** + * @see PropContainer#getContent() + */ + @Override + public Collection getContent() { + return set; + } + + //--------------------------------------------------------< inner class >--- + private class PropertyNameIterator implements DavPropertyNameIterator { + + private Iterator iter; + + private PropertyNameIterator() { + this.iter = set.iterator(); + } + + public DavPropertyName nextPropertyName() { + return iter.next(); + } + + public void remove() { + iter.remove(); + } + + public boolean hasNext() { + return iter.hasNext(); + } + + public DavPropertyName next() { + return iter.next(); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertySet.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertySet.java new file mode 100644 index 00000000000..3f239b1c912 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DavPropertySet.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.property; + +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +/** + * The DavPropertySet class represents a set of WebDAV + * property. + */ +public class DavPropertySet extends PropContainer + implements Iterable> { + + private static Logger log = LoggerFactory.getLogger(DavPropertySet.class); + + /** + * the set of property + */ + private final Map> map = new HashMap>(); + + /** + * Adds a new property to this set. + * + * @param property The property to add + * + * @return The previously assigned property or null. + */ + public DavProperty add(DavProperty property) { + return map.put(property.getName(), property); + } + + /** + * + * @param pset Properties to add + */ + public void addAll(DavPropertySet pset) { + map.putAll(pset.map); + } + + /** + * Retrieves the property with the specified name and the + * default WebDAV {@link org.apache.jackrabbit.webdav.DavConstants#NAMESPACE namespace}. + * + * @param name The name of the property to retrieve + * + * @return The desired property or null + */ + public DavProperty get(String name) { + return get(DavPropertyName.create(name)); + } + + /** + * Retrieves the property with the specified name and + * namespace. + * + * @param name The name of the property to retrieve + * @param namespace The namespace of the property to retrieve + * + * @return The desired property or null + */ + public DavProperty get(String name, Namespace namespace) { + return get(DavPropertyName.create(name, namespace)); + } + + /** + * Retrieves the property with the specified name + * + * @param name The webdav property name of the property to retrieve + * + * @return The desired property or null + */ + public DavProperty get(DavPropertyName name) { + return map.get(name); + } + + + /** + * Removes the indicated property from this set. + * + * @param name The webdav property name to remove + * + * @return The removed property or null + */ + public DavProperty remove(DavPropertyName name) { + return map.remove(name); + } + + /** + * Removes the property with the specified name and the + * default WebDAV {@link org.apache.jackrabbit.webdav.DavConstants#NAMESPACE namespace}. + * + * @param name The name of the property to remove + * + * @return The removed property or null + */ + public DavProperty remove(String name) { + return remove(DavPropertyName.create(name)); + } + + /** + * Removes the property with the specified name and + * namespace from this set. + * + * @param name The name of the property to remove + * @param namespace The namespace of the property to remove + * + * @return The removed property or null + */ + public DavProperty remove(String name, Namespace namespace) { + return remove(DavPropertyName.create(name, namespace)); + } + + /** + * Returns an iterator over all property in this set. + * + * @return An iterator over {@link DavProperty}. + */ + public DavPropertyIterator iterator() { + return new PropIter(); + } + + /** + * Returns an iterator over all those property in this set, that have the + * indicated namespace. + * + * @param namespace The namespace of the property in the iteration. + * + * @return An iterator over {@link DavProperty}. + */ + public DavPropertyIterator iterator(Namespace namespace) { + return new PropIter(namespace); + } + + /** + * Return the names of all properties present in this set. + * + * @return array of {@link DavPropertyName property names} present in this set. + */ + public DavPropertyName[] getPropertyNames() { + return map.keySet().toArray(new DavPropertyName[map.keySet().size()]); + } + + //------------------------------------------------------< PropContainer >--- + /** + * Checks if this set contains the property with the specified name. + * + * @param name The name of the property + * @return true if this set contains the property; + * false otherwise. + * @see PropContainer#contains(DavPropertyName) + */ + @Override + public boolean contains(DavPropertyName name) { + return map.containsKey(name); + } + + /** + * @param contentEntry NOTE, that the given object must be an instance of + * DavProperty in order to be successfully added to this set. + * @return true if the specified object is an instance of DavProperty + * and false otherwise. + * @see PropContainer#addContent(PropEntry) + */ + @Override + public boolean addContent(PropEntry contentEntry) { + if (contentEntry instanceof DavProperty) { + add((DavProperty) contentEntry); + return true; + } + log.debug("DavProperty object expected. Found: " + contentEntry.getClass().toString()); + return false; + } + + /** + * @see PropContainer#isEmpty() + */ + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + /** + * @see PropContainer#getContentSize() + */ + @Override + public int getContentSize() { + return map.size(); + } + + /** + * @see PropContainer#getContent() + */ + @Override + public Collection getContent() { + return map.values(); + } + + //---------------------------------------------------------- Inner class --- + /** + * Implementation of a DavPropertyIterator that returns webdav property. + * Additionally, it can only return property with the given namespace. + */ + private class PropIter implements DavPropertyIterator { + + /** the namespace to match against */ + private final Namespace namespace; + + /** the internal iterator */ + private final Iterator> iterator; + + /** the next property to return */ + private DavProperty next; + + /** + * Creates a new property iterator. + */ + private PropIter() { + this(null); + } + + /** + * Creates a new iterator with the given namespace + * @param namespace The namespace to match against + */ + private PropIter(Namespace namespace) { + this.namespace = namespace; + iterator = map.values().iterator(); + seek(); + } + + /** + * @see DavPropertyIterator#nextProperty(); + */ + public DavProperty nextProperty() throws NoSuchElementException { + if (next==null) { + throw new NoSuchElementException(); + } + DavProperty ret = next; + seek(); + return ret; + } + + /** + * @see DavPropertyIterator#hasNext(); + */ + public boolean hasNext() { + return next!=null; + } + + /** + * @see DavPropertyIterator#next(); + */ + public DavProperty next() { + return nextProperty(); + } + + /** + * @see DavPropertyIterator#remove(); + */ + public void remove() { + throw new UnsupportedOperationException(); + } + + /** + * Seeks for the next valid property + */ + private void seek() { + while (iterator.hasNext()) { + next = iterator.next(); + if (namespace == null || namespace.equals(next.getName().getNamespace())) { + return; + } + } + next = null; + } + } +} + diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DefaultDavProperty.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DefaultDavProperty.java new file mode 100644 index 00000000000..3defacc97f7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/DefaultDavProperty.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.webdav.property; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.util.List; + +/** + * DefaultDavProperty... + */ +public class DefaultDavProperty extends AbstractDavProperty { + + private static Logger log = LoggerFactory.getLogger(DefaultDavProperty.class); + + /** + * the value of the property + */ + private final T value; + + /** + * Creates a new WebDAV property with the given namespace, name and value. + * If the property is intended to be protected the isProtected flag must + * be set to true. + * + * @param name the name of the property + * @param value the value of the property + * @param namespace the namespace of the property + * @param isInvisibleInAllprop A value of true, defines this property to be protected. + * It will not be returned in a {@link org.apache.jackrabbit.webdav.DavConstants#PROPFIND_ALL_PROP DAV:allprop} + * PROPFIND request and cannot be set/removed with a PROPPATCH request. + */ + public DefaultDavProperty(String name, T value, Namespace namespace, boolean isInvisibleInAllprop) { + super(DavPropertyName.create(name, namespace), isInvisibleInAllprop); + this.value = value; + } + + /** + * Creates a new non-protected WebDAV property with the given namespace, name + * and value. + * + * @param name the name of the property + * @param value the value of the property + * @param namespace the namespace of the property + */ + public DefaultDavProperty(String name, T value, Namespace namespace) { + this(name, value, namespace, false); + } + + /** + * Creates a new WebDAV property with the given DavPropertyName + * and value. If the property is meant to be protected the 'isProtected' + * flag must be set to true. + * + * @param name the name of the property + * @param value the value of the property + * @param isInvisibleInAllprop A value of true, defines this property to be protected. + * It will not be returned in a {@link org.apache.jackrabbit.webdav.DavConstants#PROPFIND_ALL_PROP DAV:allprop} + * PROPFIND request and cannot be set/removed with a PROPPATCH request. + */ + public DefaultDavProperty(DavPropertyName name, T value, boolean isInvisibleInAllprop) { + super(name, isInvisibleInAllprop); + this.value = value; + } + + /** + * Creates a new non- protected WebDAV property with the given + * DavPropertyName and value. + * + * @param name the name of the property + * @param value the value of the property + */ + public DefaultDavProperty(DavPropertyName name, T value) { + this(name, value, false); + } + + /** + * Returns the value of this property + * + * @return the value of this property + */ + public T getValue() { + return value; + } + + /** + * Create a new DefaultDavProperty instance from the given Xml + * element. Name and namespace of the element are building the {@link DavPropertyName}, + * while the element's content forms the property value. The following logic + * is applied: + *
        +     * - empty Element           -> null value
        +     * - single Text content     -> String value
        +     * - single non-Text content -> Element.getContent(0) is used as value
        +     * - other: List obtained from Element.getContent() is used as value
        +     * 
        + * + * @param propertyElement + * @return + */ + public static DefaultDavProperty createFromXml(Element propertyElement) { + if (propertyElement == null) { + throw new IllegalArgumentException("Cannot create a new DavProperty from a 'null' element."); + } + DavPropertyName name = DavPropertyName.createFromXml(propertyElement); + DefaultDavProperty prop; + + if (!DomUtil.hasContent(propertyElement)) { + prop = new DefaultDavProperty(name, null, false); + } else { + List c = DomUtil.getContent(propertyElement); + if (c.size() == 1) { + Node n = c.get(0); + if (n instanceof Element) { + prop = new DefaultDavProperty(name, (Element) n, false); + } else { + prop = new DefaultDavProperty(name, n.getNodeValue(), false); + } + } else /* size > 1 */ { + prop = new DefaultDavProperty>(name, c, false); + } + } + return prop; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/HrefProperty.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/HrefProperty.java new file mode 100644 index 00000000000..8e1864d4d90 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/HrefProperty.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.webdav.property; + +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * HrefProperty is an extension to the common {@link DavProperty}. + * The String representation of the property value is always displayed as text + * inside an extra 'href' element. If the value is a String array each array + * element is added as text to a separate 'href' element. + * + * @see org.apache.jackrabbit.webdav.DavConstants#XML_HREF + * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() + */ +public class HrefProperty extends AbstractDavProperty { + + private static Logger log = LoggerFactory.getLogger(HrefProperty.class); + + private final String[] value; + + /** + * Creates a new WebDAV property with the given DavPropertyName + * + * @param name the name of the property + * @param value the value of the property + * @param isInvisibleInAllprop A value of true, defines this property to be invisible in PROPFIND/allprop + * It will not be returned in a {@link org.apache.jackrabbit.webdav.DavConstants#PROPFIND_ALL_PROP DAV:allprop} + * PROPFIND request. + */ + public HrefProperty(DavPropertyName name, String value, boolean isInvisibleInAllprop) { + super(name, isInvisibleInAllprop); + this.value = new String[]{value}; + } + + /** + * Creates a new WebDAV property with the given DavPropertyName + * + * @param name the name of the property + * @param value the value of the property + * @param isInvisibleInAllprop A value of true, defines this property to be invisible in PROPFIND/allprop + * It will not be returned in a {@link org.apache.jackrabbit.webdav.DavConstants#PROPFIND_ALL_PROP DAV:allprop} + * PROPFIND request. + */ + public HrefProperty(DavPropertyName name, String[] value, boolean isInvisibleInAllprop) { + super(name, isInvisibleInAllprop); + this.value = value; + } + + /** + * Create a new HrefProperty from the specified property. + * Please note, that the property must have a List value + * object, consisting of {@link #XML_HREF href} Element entries. + * + * @param prop + */ + public HrefProperty(DavProperty prop) { + super(prop.getName(), prop.isInvisibleInAllprop()); + if (prop instanceof HrefProperty) { + // already an HrefProperty: no parsing required + this.value = ((HrefProperty)prop).value; + } else { + // assume property has be built from xml + ArrayList hrefList = new ArrayList(); + Object val = prop.getValue(); + if (val instanceof List) { + for (Object entry : ((List) val)) { + if (entry instanceof Element && XML_HREF.equals(((Element) entry).getLocalName())) { + String href = DomUtil.getText((Element) entry); + if (href != null) { + hrefList.add(href); + } else { + log.warn("Valid DAV:href element expected instead of " + entry.toString()); + } + } else { + log.warn("DAV: href element expected in the content of " + getName().toString()); + } + } + } else if (val instanceof Element && XML_HREF.equals(((Element)val).getLocalName())) { + String href = DomUtil.getTextTrim((Element)val); + if (href != null) { + hrefList.add(href); + } else { + log.warn("Valid DAV:href element expected instead of " + val.toString()); + } + } + value = hrefList.toArray(new String[hrefList.size()]); + } + } + + /** + * Returns an Xml element with the following form: + *
        +     * <Z:name>
        +     *    <DAV:href>value</DAV:href/>
        +     * </Z:name>
        +     * 
        + * where Z: represents the prefix of the namespace defined with the initial + * webdav property name. + * + * @return Xml representation + * @see org.apache.jackrabbit.webdav.xml.DomUtil#hrefToXml(String,org.w3c.dom.Document) + * @param document + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + String[] value = getValue(); + if (value != null) { + for (String href : value) { + elem.appendChild(DomUtil.hrefToXml(href, document)); + } + } + return elem; + } + + /** + * Returns an array of String. + * + * @return an array of String. + * @see DavProperty#getValue() + */ + public String[] getValue() { + return value; + } + + /** + * Return an list of String containing the text of those DAV:href elements + * that would be returned as child elements of this property on + * {@link org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document)} + * + * @return list of href String + */ + public List getHrefs() { + return (value != null) ? Arrays.asList(value) : new ArrayList(); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropContainer.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropContainer.java new file mode 100644 index 00000000000..a86e55fea36 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropContainer.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.webdav.property; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.Collection; + +/** + * PropContainer... + */ +public abstract class PropContainer implements XmlSerializable, DavConstants { + + private static Logger log = LoggerFactory.getLogger(PropContainer.class); + + /** + * Tries to add the specified object to the PropContainer and + * returns a boolean indicating whether the content could be added to the + * internal set/map. + * + * @param contentEntry + * @return true if the object could be added; false otherwise + * @deprecated Use {@link #addContent(PropEntry)} instead. + */ + public boolean addContent(Object contentEntry) { + if (contentEntry instanceof PropEntry) { + return addContent((PropEntry) contentEntry); + } else { + return false; + } + } + + /** + * Tries to add the specified entry to the PropContainer and + * returns a boolean indicating whether the content could be added to the + * internal set/map. + * + * @param contentEntry + * @return true if the object could be added; false otherwise + */ + public abstract boolean addContent(PropEntry contentEntry); + + /** + * Returns true if the PropContainer does not yet contain any content elements. + * + * @return true if this container is empty. + */ + public abstract boolean isEmpty(); + + /** + * Returns the number of property related content elements that are present + * in this PropContainer. + * + * @return number of content elements + */ + public abstract int getContentSize(); + + /** + * Returns the collection that contains all the content elements of this + * PropContainer. + * + * @return collection representing the contents of this PropContainer. + */ + public abstract Collection getContent(); + + /** + * Returns true if this PropContainer contains a content element + * that matches the given DavPropertyName. + * + * @param name + * @return true if any of the content elements (be it a DavProperty or a + * DavPropertyName only) matches the given name. + */ + public abstract boolean contains(DavPropertyName name); + + /** + * Returns the xml representation of a property related set with the + * following format: + *
        +     * <!ELEMENT prop (ANY) >
        +     * where ANY consists of a list of elements each reflecting the xml
        +     * representation of the entries returned by {@link #getContent()}.
        +     * 
        + * + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element prop = DomUtil.createElement(document, XML_PROP, NAMESPACE); + for (Object content : getContent()) { + if (content instanceof XmlSerializable) { + prop.appendChild(((XmlSerializable) content).toXml(document)); + } else { + log.debug("Unexpected content in PropContainer: should be XmlSerializable."); + } + } + return prop; + } + +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropEntry.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropEntry.java new file mode 100644 index 00000000000..7f3104cad9a --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropEntry.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.webdav.property; + +/** + * Marker interface used to flag the different types of entries that form + * part of a PROPPATCH request and define the possible entries for a + * PropContainer. + */ +public interface PropEntry { +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropfindInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropfindInfo.java new file mode 100755 index 00000000000..0d1854868e0 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/PropfindInfo.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.webdav.property; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +public class PropfindInfo implements XmlSerializable { + + private final int propfindType; + private final DavPropertyNameSet propNameSet; + + public PropfindInfo(int propfindType, DavPropertyNameSet propNameSet) { + this.propfindType = propfindType; + this.propNameSet = propNameSet; + } + + @Override + public Element toXml(Document document) { + Element propfind = DomUtil.createElement(document, DavConstants.XML_PROPFIND, DavConstants.NAMESPACE); + + // fill the propfind element + switch (propfindType) { + case DavConstants.PROPFIND_ALL_PROP: + propfind.appendChild(DomUtil.createElement(document, DavConstants.XML_ALLPROP, DavConstants.NAMESPACE)); + break; + + case DavConstants.PROPFIND_PROPERTY_NAMES: + propfind.appendChild(DomUtil.createElement(document, DavConstants.XML_PROPNAME, DavConstants.NAMESPACE)); + break; + + case DavConstants.PROPFIND_BY_PROPERTY: + if (propNameSet == null) { + // name set missing, ask for a property that is known to + // exist + Element prop = DomUtil.createElement(document, DavConstants.XML_PROP, DavConstants.NAMESPACE); + Element resourcetype = DomUtil.createElement(document, DavConstants.PROPERTY_RESOURCETYPE, + DavConstants.NAMESPACE); + prop.appendChild(resourcetype); + propfind.appendChild(prop); + } else { + propfind.appendChild(propNameSet.toXml(document)); + } + break; + + case DavConstants.PROPFIND_ALL_PROP_INCLUDE: + propfind.appendChild(DomUtil.createElement(document, DavConstants.XML_ALLPROP, DavConstants.NAMESPACE)); + if (propNameSet != null && !propNameSet.isEmpty()) { + Element include = DomUtil.createElement(document, DavConstants.XML_INCLUDE, DavConstants.NAMESPACE); + Element prop = propNameSet.toXml(document); + for (Node c = prop.getFirstChild(); c != null; c = c.getNextSibling()) { + // copy over the children of to + // element + include.appendChild(c.cloneNode(true)); + } + propfind.appendChild(include); + } + break; + + default: + throw new IllegalArgumentException("unknown propfind type"); + } + + return propfind; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/ProppatchInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/ProppatchInfo.java new file mode 100644 index 00000000000..b209a93d05b --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/ProppatchInfo.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.webdav.property; + +import java.util.List; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class ProppatchInfo implements XmlSerializable { + + private final List changeList; + private final DavPropertySet setProperties; + private final DavPropertyNameSet removeProperties; + + private final DavPropertyNameSet propertyNames = new DavPropertyNameSet(); + + public ProppatchInfo(List changeList) { + if (changeList == null || changeList.isEmpty()) { + throw new IllegalArgumentException("PROPPATCH cannot be executed without properties to be set or removed."); + } + this.changeList = changeList; + this.setProperties = null; + this.removeProperties = null; + for (PropEntry entry : changeList) { + if (entry instanceof DavPropertyName) { + // DAV:remove + this.propertyNames.add((DavPropertyName) entry); + } else if (entry instanceof DavProperty) { + // DAV:set + DavProperty setProperty = (DavProperty) entry; + this.propertyNames.add(setProperty.getName()); + } else { + throw new IllegalArgumentException("ChangeList may only contain DavPropertyName and DavProperty elements."); + } + } + } + + public ProppatchInfo(DavPropertySet setProperties, DavPropertyNameSet removeProperties) { + if (setProperties == null || removeProperties == null) { + throw new IllegalArgumentException("Neither setProperties nor removeProperties must be null."); + } + if (setProperties.isEmpty() && removeProperties.isEmpty()) { + throw new IllegalArgumentException("Either setProperties or removeProperties can be empty; not both of them."); + } + this.changeList = null; + this.setProperties = setProperties; + this.removeProperties = removeProperties; + this.propertyNames.addAll(removeProperties); + for (DavPropertyName setName : setProperties.getPropertyNames()) { + this.propertyNames.add(setName); + } + } + + public DavPropertyNameSet getAffectedProperties() { + if (this.propertyNames.isEmpty()) { + throw new IllegalStateException("must be called after toXml()"); + } + return this.propertyNames; + } + + @Override + public Element toXml(Document document) { + Element proppatch = DomUtil.createElement(document, DavConstants.XML_PROPERTYUPDATE, DavConstants.NAMESPACE); + + if (changeList != null) { + Element propElement = null; + boolean isSet = false; + for (Object entry : changeList) { + if (entry instanceof DavPropertyName) { + // DAV:remove + DavPropertyName removeName = (DavPropertyName) entry; + if (propElement == null || isSet) { + isSet = false; + propElement = getPropElement(proppatch, false); + } + propElement.appendChild(removeName.toXml(document)); + } else if (entry instanceof DavProperty) { + // DAV:set + DavProperty setProperty = (DavProperty) entry; + if (propElement == null || !isSet) { + isSet = true; + propElement = getPropElement(proppatch, true); + } + propElement.appendChild(setProperty.toXml(document)); + } else { + throw new IllegalArgumentException("ChangeList may only contain DavPropertyName and DavProperty elements."); + } + } + } else { + // DAV:set + if (!setProperties.isEmpty()) { + Element set = DomUtil.addChildElement(proppatch, DavConstants.XML_SET, DavConstants.NAMESPACE); + set.appendChild(setProperties.toXml(document)); + } + // DAV:remove + if (!removeProperties.isEmpty()) { + Element remove = DomUtil.addChildElement(proppatch, DavConstants.XML_REMOVE, DavConstants.NAMESPACE); + remove.appendChild(removeProperties.toXml(document)); + } + } + + return proppatch; + } + + private Element getPropElement(Element propUpdate, boolean isSet) { + Element updateEntry = DomUtil.addChildElement(propUpdate, isSet ? DavConstants.XML_SET : DavConstants.XML_REMOVE, + DavConstants.NAMESPACE); + return DomUtil.addChildElement(updateEntry, DavConstants.XML_PROP, DavConstants.NAMESPACE); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/ResourceType.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/ResourceType.java new file mode 100644 index 00000000000..5ce5304d2da --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/ResourceType.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.webdav.property; + +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The ResourceType class represents the webdav resource + * type property. The property may contain multiple resource type + * values. Predefined resource types are those defined by RFC2518 and RFC3253: + *
          + *
        • {@link #DEFAULT_RESOURCE the empty default resource type},
        • + *
        • '{@link #COLLECTION DAV:collection}',
        • + *
        • '{@link #VERSION_HISTORY DAV:version-history}',
        • + *
        • '{@link #ACTIVITY DAV:activity}',
        • + *
        • '{@link #BASELINE DAV:baseline}',
        • + *
        + */ +public class ResourceType extends AbstractDavProperty> { + + /** + * The default resource type + */ + public static final int DEFAULT_RESOURCE = 0; + + /** + * The collection resource type + */ + public static final int COLLECTION = DEFAULT_RESOURCE + 1; + + /** + * The version-history resource type + */ + public static final int VERSION_HISTORY = COLLECTION + 1; + + /** + * The activity resource type + */ + public static final int ACTIVITY = VERSION_HISTORY + 1; + + /** + * The baseline resource type + */ + public static final int BASELINE = ACTIVITY + 1; + + /** + * Array containing all possible resourcetype elements + */ + private static final List NAMES = new ArrayList(); + static { + NAMES.add(null); + NAMES.add(new TypeName(XML_COLLECTION, NAMESPACE)); + NAMES.add(new TypeName(DeltaVConstants.XML_VERSION_HISTORY, DeltaVConstants.NAMESPACE)); + NAMES.add(new TypeName(DeltaVConstants.XML_ACTIVITY, DeltaVConstants.NAMESPACE)); + NAMES.add(new TypeName(DeltaVConstants.XML_BASELINE, DeltaVConstants.NAMESPACE)); + } + + private final int[] resourceTypes; + + /** + * Create a single-valued resource type property + */ + public ResourceType(int resourceType) { + this(new int[] { resourceType }); + } + + /** + * Create a multi-valued resource type property + */ + public ResourceType(int[] resourceTypes) { + super(DavPropertyName.RESOURCETYPE, false); + for (int resourceType : resourceTypes) { + if (!isValidResourceType(resourceType)) { + throw new IllegalArgumentException("Invalid resource type '" + resourceType + "'."); + } + } + this.resourceTypes = resourceTypes; + } + + /** + * Returns a Set of resource types each implementing the XmlSerializable + * interface. + * + * @return a Set of resource types representing this property. + * @see DavProperty#getValue() + */ + public Set getValue() { + Set rTypes = new HashSet(); + for (int resourceType : resourceTypes) { + TypeName n = NAMES.get(resourceType); + if (n != null) { + rTypes.add(n); + } + } + return rTypes; + } + + /** + * Returns the resource types specified with the constructor. + * + * @return resourceTypes + */ + public int[] getResourceTypes() { + return resourceTypes; + } + + /** + * Returns true if the given integer defines a valid resource type. + * + * @param resourceType to be validated. + * @return true if this is one of the predefined resource types + */ + private static boolean isValidResourceType(int resourceType) { + if (resourceType < DEFAULT_RESOURCE || resourceType > NAMES.size()-1) { + return false; + } + return true; + } + + /** + * Register an additional resource type + * + * @param name + * @param namespace + * @return int to be used for creation of a new ResourceType property + * that contains this type. + * @throws IllegalArgumentException if the given element is null or + * if the registration fails for another reason. + */ + public static int registerResourceType(String name, Namespace namespace) { + if (name == null || namespace == null) { + throw new IllegalArgumentException("Cannot register a resourcetype"); + } + TypeName tn = new TypeName(name, namespace); + // avoid duplicate registrations + if (NAMES.contains(tn)) { + return NAMES.indexOf(tn); + } + // register new type + if (NAMES.add(tn)) { + return NAMES.size() - 1; + } else { + throw new IllegalArgumentException("Could not register resourcetype " + namespace.getPrefix() + name); + } + } + + //--------------------------------------------------------< inner class >--- + /** + * Private inner class used to register predefined and user defined resource + * types. + */ + private static class TypeName implements XmlSerializable { + + private final String localName; + private final Namespace namespace; + private final int hashCode; + + private TypeName(String localName, Namespace namespace) { + this.localName = localName; + this.namespace = namespace; + hashCode = DomUtil.getExpandedName(localName, namespace).hashCode(); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object o) { + if (o instanceof TypeName) { + return hashCode == ((TypeName)o).hashCode; + } + return false; + } + + public Element toXml(Document document) { + return DomUtil.createElement(document, localName, namespace); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/package-info.java new file mode 100644 index 00000000000..ddaa801c78e --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/property/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.property; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/QueryGrammerSet.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/QueryGrammerSet.java new file mode 100644 index 00000000000..b3f214fa170 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/QueryGrammerSet.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.webdav.search; + +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashSet; +import java.util.Set; + +/** + * QueryGrammerSet is a {@link DavProperty} that + * encapsulates the 'supported-query-grammer-set' as defined by the + * Webdav SEARCH internet draft. + */ +public class QueryGrammerSet extends AbstractDavProperty> implements SearchConstants { + + private final Set queryGrammers = new HashSet(); + + /** + * Create a new empty QueryGrammerSet. Supported query grammers + * may be added by calling {@link #addQueryLanguage(String, Namespace)}. + */ + public QueryGrammerSet() { + super(QUERY_GRAMMER_SET, true); + } + + /** + * Add another query queryGrammer to this set. + * + * @param grammerName + * @param namespace + */ + public void addQueryLanguage(String grammerName, Namespace namespace) { + queryGrammers.add(new Grammer(grammerName, namespace)); + } + + /** + * Return a String array containing the URIs of the query + * languages supported. + * + * @return names of the supported query languages + */ + public String[] getQueryLanguages() { + int size = queryGrammers.size(); + if (size > 0) { + String[] qLangStr = new String[size]; + Grammer[] grammers = queryGrammers.toArray(new Grammer[size]); + for (int i = 0; i < grammers.length; i++) { + qLangStr[i] = grammers[i].namespace.getURI() + grammers[i].localName; + } + return qLangStr; + } else { + return new String[0]; + } + } + + /** + * Return the Xml representation of this property according to the definition + * of the 'supported-query-grammer-set'. + * + * @return Xml representation + * @see SearchConstants#QUERY_GRAMMER_SET + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + for (Grammer qGrammer : queryGrammers) { + elem.appendChild(qGrammer.toXml(document)); + } + return elem; + } + + /** + * Returns the set of supported query grammers. + * + * @return list of supported query languages. + * @see org.apache.jackrabbit.webdav.property.DavProperty#getValue() + */ + public Set getValue() { + return queryGrammers; + } + + + private class Grammer implements XmlSerializable { + + private final String localName; + private final Namespace namespace; + private final int hashCode; + + Grammer(String localName, Namespace namespace) { + this.localName = localName; + this.namespace = namespace; + hashCode = DomUtil.getExpandedName(localName, namespace).hashCode(); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Grammer) { + return obj.hashCode() == hashCode(); + } + return false; + } + + /** + * @see XmlSerializable#toXml(org.w3c.dom.Document) + */ + public Element toXml(Document document) { + Element sqg = DomUtil.createElement(document, XML_QUERY_GRAMMAR, SearchConstants.NAMESPACE); + Element grammer = DomUtil.addChildElement(sqg, XML_GRAMMER, SearchConstants.NAMESPACE); + DomUtil.addChildElement(grammer, localName, namespace); + return sqg; + } + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchConstants.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchConstants.java new file mode 100644 index 00000000000..c07227edf74 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchConstants.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.webdav.search; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * SearchConstants interface provide constants for request + * and response headers, Xml elements and property names used for WebDAV + * search. + */ +public interface SearchConstants { + + /** + * Namespace definition.
        + * NOTE: For convenience reasons, the namespace is defined to be the default + * {@link DavConstants#NAMESPACE DAV:} namespace. This is not correct for the + * underlying specification is still in a draft state. See also the editorial + * note inside the + * Internet Draft WebDAV Search + * document. + */ + public static final Namespace NAMESPACE = DavConstants.NAMESPACE; + + /** + * Predefined basic query grammer. + */ + public static final String BASICSEARCH = NAMESPACE.getPrefix()+"basicsearch"; + + //---< Headers >------------------------------------------------------------ + /** + * The DASL response header specifying the query languages supported by + * the requested resource. + */ + public static final String HEADER_DASL = "DASL"; + + //---< XML Element, Attribute Names >--------------------------------------- + /** + * Xml element name for a single query grammar element inside + * the {@link #QUERY_GRAMMER_SET supported-query-grammer-set property}. + */ + public static final String XML_QUERY_GRAMMAR = "supported-query-grammar"; + + /** + * Name constant for the 'DAV:grammar' element, which is used inside the + * {@link #XML_QUERY_GRAMMAR} element. + */ + public static final String XML_GRAMMER = "grammar"; + + /** + * Xml element name for the required request body of a SEARCH request. + * + * @see SearchInfo + * @see SearchResource#search(SearchInfo) + */ + public static final String XML_SEARCHREQUEST = "searchrequest"; + + /** + * Optional Xml element name used in the SEARCH request body instead of {@link #XML_SEARCHREQUEST} + * in order to access a given query schema. + */ + public static final String XML_QUERY_SCHEMA_DISCOVERY = "query-schema-discovery"; + + //---< Property Names >----------------------------------------------------- + /** + * Property indicating the set of query languages the given resource is + * able deal with. The property has the following definition:
        + *
        +     * <!ELEMENT supported-query-grammar-set (supported-query-grammar*)>
        +     * <!ELEMENT supported-query-grammar grammar>
        +     * <!ELEMENT grammar ANY>
        +     * 
        + */ + public static final DavPropertyName QUERY_GRAMMER_SET = DavPropertyName.create("supported-query-grammar-set", NAMESPACE); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchInfo.java new file mode 100644 index 00000000000..cae113caaa2 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchInfo.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.search; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Attr; + +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; +import java.util.Set; +import java.util.HashSet; + +/** + * SearchInfo parses the 'searchrequest' element of a SEARCH + * request body and performs basic validation. Both query language and the + * query itself can be access from the resulting object.
        + * NOTE: The query is expected to be represented by the text contained in the + * Xml element specifying the query language, thus the 'basicsearch' defined + * by the Webdav Search Internet Draft is not supported by this implementation. + *

        + * + * Example of a valid 'searchrequest' body + *

        + * <d:searchrequest xmlns:d="DAV:" dcr:="http://www.day.com/jcr/webdav/1.0" >
        + *    <dcr:xpath>//sv:node[@sv:name='myapp:paragraph'][1]</dcr:xpath>
        + * </d:searchrequest>
        + * 
        + * + * Would return the following values: + *
        + *    getLanguageName() -> xpath
        + *    getQuery()        -> //sv:node[@sv:name='myapp:paragraph'][1]
        + * 
        + * + */ +public class SearchInfo implements SearchConstants, XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(SearchInfo.class); + + public static final long NRESULTS_UNDEFINED = -1; + public static final long OFFSET_UNDEFINED = -1; + + private static final String LIMIT = "limit"; + private static final String NRESULTS = "nresults"; + private static final String OFFSET = "offset"; + + /** + * Set of namespace uri String which are ignored in the search request. + */ + private static final Set IGNORED_NAMESPACES; + + static { + Set s = new HashSet(); + s.add(Namespace.XMLNS_NAMESPACE.getURI()); + s.add(Namespace.XML_NAMESPACE.getURI()); + s.add(DavConstants.NAMESPACE.getURI()); + IGNORED_NAMESPACES = Collections.unmodifiableSet(s); + } + + private final String language; + private final Namespace languageNamespace; + private final String query; + private final Map namespaces; + + private long nresults = NRESULTS_UNDEFINED; + private long offset = OFFSET_UNDEFINED; + + /** + * Create a new SearchInfo instance. + * + * @param language + * @param languageNamespace + * @param query + * @param namespaces the re-mapped namespaces. Key=prefix, value=uri. + */ + public SearchInfo(String language, Namespace languageNamespace, String query, + Map namespaces) { + this.language = language; + this.languageNamespace = languageNamespace; + this.query = query; + this.namespaces = Collections.unmodifiableMap(new HashMap(namespaces)); + } + + /** + * Create a new SearchInfo instance. + * + * @param language + * @param languageNamespace + * @param query + */ + public SearchInfo(String language, Namespace languageNamespace, String query) { + this(language, languageNamespace, query, Collections.emptyMap()); + } + + /** + * Returns the name of the query language to be used. + * + * @return name of the query language + */ + public String getLanguageName() { + return language; + } + + /** + * Returns the namespace of the language specified with the search request element. + * + * @return namespace of the requested language. + */ + public Namespace getLanguageNameSpace() { + return languageNamespace; + } + + /** + * Return the query string. + * + * @return query string + */ + public String getQuery() { + return query; + } + + /** + * Returns the namespaces that have been re-mapped by the user. + * + * @return map of namespace to prefix mappings. Key=prefix, value=uri. + */ + public Map getNamespaces() { + return namespaces; + } + + /** + * Returns the maximal number of search results that should be returned. + * + * @return the maximal number of search results that should be returned. + */ + public long getNumberResults() { + return nresults; + } + + /** + * Sets the maximal number of search results that should be returned. + * + * @param nresults The maximal number of search results + */ + public void setNumberResults(long nresults) { + this.nresults = nresults; + } + + /** + * Returns the desired offset in the total result set. + * + * @return the desired offset in the total result set. + */ + public long getOffset() { + return offset; + } + + /** + * Sets the desired offset in the total result set. + * + * @param offset The desired offset in the total result set. + */ + public void setOffset(long offset) { + this.offset = offset; + } + + /** + * Return the xml representation of this SearchInfo instance. + * + * @return xml representation + * @param document + */ + public Element toXml(Document document) { + Element sRequestElem = DomUtil.createElement(document, XML_SEARCHREQUEST, NAMESPACE); + for (String prefix : namespaces.keySet()) { + String uri = namespaces.get(prefix); + DomUtil.setNamespaceAttribute(sRequestElem, prefix, uri); + } + DomUtil.addChildElement(sRequestElem, language, languageNamespace, query); + if (nresults != NRESULTS_UNDEFINED|| offset != OFFSET_UNDEFINED) { + Element limitE = DomUtil.addChildElement(sRequestElem, LIMIT, NAMESPACE); + if (nresults != NRESULTS_UNDEFINED) { + DomUtil.addChildElement(limitE, NRESULTS, NAMESPACE, nresults + ""); + } + if (offset != OFFSET_UNDEFINED) { + // TODO define reasonable namespace... + DomUtil.addChildElement(limitE, OFFSET, Namespace.EMPTY_NAMESPACE, offset + ""); + } + } + return sRequestElem; + } + + /** + * Create a new SearchInfo from the specifying document + * retrieved from the request body. + * + * @param searchRequest + * @throws DavException if the root element's name is other than + * 'searchrequest' or if it does not contain a single child element specifying + * the query language to be used. + */ + public static SearchInfo createFromXml(Element searchRequest) throws DavException { + if (searchRequest == null || !XML_SEARCHREQUEST.equals(searchRequest.getLocalName())) { + log.warn("The root element must be 'searchrequest'."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + Element first = DomUtil.getFirstChildElement(searchRequest); + Attr[] nsAttributes = DomUtil.getNamespaceAttributes(searchRequest); + Map namespaces = new HashMap(); + for (Attr nsAttribute : nsAttributes) { + // filter out xmlns namespace and DAV namespace + if (!IGNORED_NAMESPACES.contains(nsAttribute.getValue())) { + namespaces.put(nsAttribute.getLocalName(), nsAttribute.getValue()); + } + } + SearchInfo sInfo; + if (first != null) { + sInfo = new SearchInfo(first.getLocalName(), DomUtil.getNamespace(first), DomUtil.getText(first), namespaces); + } else { + log.warn("A single child element is expected with the 'DAV:searchrequest'."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + + Element limit = DomUtil.getChildElement(searchRequest, LIMIT, NAMESPACE); + if (limit != null) { + // try to get the value DAV:nresults element + String nresults = DomUtil.getChildTextTrim(limit, NRESULTS, NAMESPACE); + if (nresults != null) { + try { + sInfo.setNumberResults(Long.valueOf(nresults)); + } catch (NumberFormatException e) { + log.error("DAV:nresults cannot be parsed into a long -> ignore."); + } + } + // try of an offset is defined within the DAV:limit element. + String offset = DomUtil.getChildTextTrim(limit, OFFSET, Namespace.EMPTY_NAMESPACE); + if (offset != null) { + try { + sInfo.setOffset(Long.valueOf(offset)); + } catch (NumberFormatException e) { + log.error("'offset' cannot be parsed into a long -> ignore."); + } + } + } + return sInfo; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchResource.java new file mode 100644 index 00000000000..e9af317e79e --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/SearchResource.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.webdav.search; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.MultiStatus; + +/** + * SearchResource defines METHODS required in order to handle + * a SEARCH request. + */ +public interface SearchResource { + + /** + * The 'SEARCH' method + */ + public String METHODS = "SEARCH"; + + + /** + * Returns the protected DAV:supported-method-set property which is defined + * mandatory by RTF 3253. This method call is a shortcut for + * DavResource.getProperty(SearchConstants.QUERY_GRAMMER_SET). + * + * @return the DAV:supported-query-grammer-set + * @see SearchConstants#QUERY_GRAMMER_SET + */ + public QueryGrammerSet getQueryGrammerSet(); + + /** + * Runs a search with the language and query defined in the {@link SearchInfo} + * object specified and returns a {@link MultiStatus} object listing the + * results. + * + * @param sInfo SearchInfo element encapsulating the SEARCH + * request body. + * @return MultiStatus object listing the results. + * @throws DavException + */ + public MultiStatus search(SearchInfo sInfo) throws DavException; +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/package-info.java new file mode 100644 index 00000000000..5f687980a8a --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/search/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.search; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclProperty.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclProperty.java new file mode 100644 index 00000000000..9dfed08f58c --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclProperty.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.webdav.security; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * AclProperty defines a protected property that specifies the list + * of access control entries (ACEs). The set of ACEs define the privileges granted + * or denied to principals on the resource a given property instance belongs to. + *
        + * RFC 3744 defines the following format for this property: + *
        + * <!ELEMENT acl (ace*) >
        + * <!ELEMENT ace ((principal | invert), (grant|deny), protected?, inherited?)>
        + * <!ELEMENT principal (href | all | authenticated | unauthenticated | property | self)>
        + * <!ELEMENT invert (principal)>
        + * <!ELEMENT grant (privilege+)>
        + * <!ELEMENT deny (privilege+)>
        + * <!ELEMENT protected EMPTY>
        + * <!ELEMENT inherited (href)>
        + * 
        + * + * @see Principal for details regarding DAV:principal + * @see Privilege for details regarding DAV:privilege + */ +public class AclProperty extends AbstractDavProperty> { + + private final List aces; + + /** + * Create a new AclProperty from the given ACEs. + * + * @see AclProperty#createGrantAce(Principal, Privilege[], boolean, boolean, AclResource) for a factory method to create a grant ACE. + * @see AclProperty#createDenyAce(Principal, Privilege[], boolean, boolean, AclResource) for a factory method to create a deny ACE. + */ + public AclProperty(Ace[] accessControlElements) { + this((accessControlElements == null) ? new ArrayList() : Arrays.asList(accessControlElements)); + } + + private AclProperty(List aces) { + super(SecurityConstants.ACL, true); + this.aces = aces; + } + + /** + * @return a List of Ace object. If this property defines no ACEs + * an empty list is returned. + * @see DavProperty#getValue() + */ + public List getValue() { + return aces; + } + + /** + * Build a new AclProperty object from the request body of the + * ACL method call. + * + * @param aclElement + * @return new AclProperty + * @throws DavException + */ + public static AclProperty createFromXml(Element aclElement) throws DavException { + if (!DomUtil.matches(aclElement, SecurityConstants.ACL.getName(), SecurityConstants.ACL.getNamespace())) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "ACL request requires a DAV:acl body."); + } + List aces = new ArrayList(); + ElementIterator it = DomUtil.getChildren(aclElement, Ace.XML_ACE, SecurityConstants.NAMESPACE); + while (it.hasNext()) { + Element aceElem = it.nextElement(); + aces.add(Ace.createFromXml(aceElem)); + } + return new AclProperty(aces); + } + + public static Ace createGrantAce(Principal principal, Privilege[] privileges, boolean invert, boolean isProtected, AclResource inheritedFrom) { + return new Ace(principal, invert, privileges, true, isProtected, inheritedFrom); + } + + public static Ace createDenyAce(Principal principal, Privilege[] privileges, boolean invert, boolean isProtected, AclResource inheritedFrom) { + return new Ace(principal, invert, privileges, false, isProtected, inheritedFrom); + } + + //--------------------------------------------------------< inner class >--- + /** + * Simple WebDAV ACE implementation + */ + public static class Ace implements XmlSerializable, SecurityConstants { + + private static final String XML_ACE = "ace"; + private static final String XML_INVERT = "invert"; + private static final String XML_GRANT = "grant"; + private static final String XML_DENY = "deny"; + private static final String XML_PROTECTED = "protected"; + private static final String XML_INHERITED = "inherited"; + + private final Principal principal; + private final boolean invert; + private final Privilege[] privileges; + private final boolean grant; + private final boolean isProtected; + private final String inheritedHref; + + /** + * Private constructor + * + * @param principal + * @param invert + * @param privileges + * @param grant + * @param isProtected + * @param inherited + */ + private Ace(Principal principal, boolean invert, Privilege[] privileges, + boolean grant, boolean isProtected, AclResource inherited) { + this(principal, invert, privileges, grant, isProtected, + ((inherited != null) ? inherited.getHref() : null)); + } + + /** + * Private constructor + * + * @param principal + * @param invert + * @param privileges + * @param grant + * @param isProtected + * @param inheritedHref + */ + private Ace(Principal principal, boolean invert, Privilege[] privileges, + boolean grant, boolean isProtected, String inheritedHref) { + if (principal == null) { + throw new IllegalArgumentException("Cannot create a new ACE with 'null' principal."); + } + if (privileges == null || privileges.length == 0) { + throw new IllegalArgumentException("Cannot create a new ACE: at least a single privilege must be specified."); + } + this.principal = principal; + this.invert = invert; + this.privileges = privileges; + this.grant = grant; + this.isProtected = isProtected; + this.inheritedHref = inheritedHref; + } + + public Principal getPrincipal() { + return principal; + } + + public boolean isInvert() { + return invert; + } + + public Privilege[] getPrivileges() { + return privileges; + } + + public boolean isGrant() { + return grant; + } + + public boolean isDeny() { + return !grant; + } + + public boolean isProtected() { + return isProtected; + } + + public String getInheritedHref() { + return inheritedHref; + } + + /** + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element ace = DomUtil.createElement(document, XML_ACE, SecurityConstants.NAMESPACE); + if (invert) { + Element inv = DomUtil.addChildElement(ace, XML_INVERT, SecurityConstants.NAMESPACE); + inv.appendChild(principal.toXml(document)); + } else { + ace.appendChild(principal.toXml(document)); + } + Element gd = DomUtil.addChildElement(ace, ((grant) ? XML_GRANT : XML_DENY), SecurityConstants.NAMESPACE); + for (Privilege privilege : privileges) { + gd.appendChild(privilege.toXml(document)); + } + if (isProtected) { + DomUtil.addChildElement(ace, XML_PROTECTED, SecurityConstants.NAMESPACE); + } + if (inheritedHref != null) { + Element inh = DomUtil.addChildElement(ace, XML_INHERITED, SecurityConstants.NAMESPACE); + inh.appendChild(DomUtil.hrefToXml(inheritedHref, document)); + } + return ace; + } + + private static Ace createFromXml(Element aceElement) throws DavException { + boolean invert = DomUtil.hasChildElement(aceElement, XML_INVERT, NAMESPACE); + Element pe; + if (invert) { + Element invertE = DomUtil.getChildElement(aceElement, XML_INVERT, NAMESPACE); + pe = DomUtil.getChildElement(invertE, Principal.XML_PRINCIPAL, NAMESPACE); + } else { + pe = DomUtil.getChildElement(aceElement, Principal.XML_PRINCIPAL, SecurityConstants.NAMESPACE); + } + Principal principal = Principal.createFromXml(pe); + + boolean grant = DomUtil.hasChildElement(aceElement, Ace.XML_GRANT, SecurityConstants.NAMESPACE); + Element gdElem; + if (grant) { + gdElem = DomUtil.getChildElement(aceElement, XML_GRANT, NAMESPACE); + } else { + gdElem = DomUtil.getChildElement(aceElement, XML_DENY, NAMESPACE); + } + List privilegeList = new ArrayList(); + ElementIterator privIt = DomUtil.getChildren(gdElem, Privilege.XML_PRIVILEGE, NAMESPACE); + while (privIt.hasNext()) { + Privilege pv = Privilege.getPrivilege(privIt.nextElement()); + privilegeList.add(pv); + } + Privilege[] privileges = privilegeList.toArray(new Privilege[privilegeList.size()]); + + boolean isProtected = DomUtil.hasChildElement(aceElement, XML_PROTECTED, NAMESPACE); + String inheritedHref = null; + if (DomUtil.hasChildElement(aceElement, XML_INHERITED, NAMESPACE)) { + Element inhE = DomUtil.getChildElement(aceElement, XML_INHERITED, NAMESPACE); + inheritedHref = DomUtil.getChildText(inhE, DavConstants.XML_HREF, DavConstants.NAMESPACE); + } + + return new Ace(principal, invert, privileges, grant, isProtected, inheritedHref); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclResource.java new file mode 100644 index 00000000000..3573752c5a2 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclResource.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.webdav.security; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.version.DeltaVResource; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; + +/** + * AclResource... + */ +public interface AclResource extends DavResource { + + /** + * The AclResource must support the ACL method and the REPORT method in order + * to retrieve various security related reports. + * + * @see DeltaVResource#METHODS + * @see org.apache.jackrabbit.webdav.DavResource#METHODS + */ + public String METHODS = "ACL, REPORT"; + + /** + * Modify the DAV:acl property of this resource object.
        + * Note: RFC 3744 limits modification of access control elements (ACEs) to + * elements that are neither inherited nor protected. + * + * @param aclProperty DAV:acl property listing the set of ACEs to be modified + * by this call. This may be a subset of all access control elements + * present on this resource object only. + * @throws DavException If the request fails. RFC 3744 defines a set of + * preconditions which must be met for a successful ACL request. + * If these conditions are violated, this method must fail with + * {@link DavServletResponse#SC_FORBIDDEN 403 (Forbidden)} or + * {@link DavServletResponse#SC_CONFLICT 409 (Conflict)} and should provide + * a detailed error condition in the response body. See + * RFC 3744 Section 8.1.1 + * (ACL Preconditions) for further details. + */ + public void alterAcl(AclProperty aclProperty) throws DavException; + + /** + * Same as {@link DeltaVResource#getReport(ReportInfo)}. + * + * @param reportInfo specifying the report details retrieved from the REPORT + * request. + * @return the requested report. + * @throws DavException in case an error occurred or if the specified + * ReportInfo is either not valid or cannot be run by this + * resource. + */ + public Report getReport(ReportInfo reportInfo) throws DavException; +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclRestrictionsProperty.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclRestrictionsProperty.java new file mode 100644 index 00000000000..5ce7b2609ba --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/AclRestrictionsProperty.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.security; + +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * AclRestrictionsProperty as defined by RFC 3744 Section 5.6. + * DAV:acl-restrictions: + *
        + * <!ELEMENT acl-restrictions (grant-only?, no-invert?, deny-before-grant?, required-principal?)>
        + * <!ELEMENT grant-only EMPTY>
        + * <!ELEMENT no-invert EMPTY>
        + * <!ELEMENT deny-before-grant EMPTY>
        + * <!ELEMENT required-principal (all? | authenticated? | unauthenticated? | self? | href* | property*)>
        + * 
        + * + * @see Principal + * @see AclProperty + */ +public class AclRestrictionsProperty extends AbstractDavProperty { + + // TODO: RFC 3744 defines a distinct structure for required-principal + // than for principal. Current 'normal' principal is used instead. + + private static final String XML_GRANT_ONLY = "grant-only"; + private static final String XML_NO_INVERT = "no-invert"; + private static final String XML_DENY_BEFORE_GRANT = "deny-before-grant"; + + private final boolean grantOnly; + private final boolean noInvert; + private final boolean denyBeforeGrant; + private final Principal requiredPrincipal; + + public AclRestrictionsProperty(boolean grantOnly, boolean noInvert, boolean denyBeforeGrant, Principal requiredPrincipal) { + super(SecurityConstants.ACL_RESTRICTIONS, true); + this.grantOnly = grantOnly; + this.noInvert = noInvert; + this.denyBeforeGrant = denyBeforeGrant; + this.requiredPrincipal = requiredPrincipal; + } + + /** + * Not implemented. + * + * @throws UnsupportedOperationException + */ + public Object getValue() { + throw new UnsupportedOperationException("Not implemented. Use the property specific methods instead."); + } + + /** + * @see DavProperty#toXml(Document) + */ + @Override + public Element toXml(Document document) { + Element elem = getName().toXml(document); + if (grantOnly) { + DomUtil.addChildElement(elem, XML_GRANT_ONLY, SecurityConstants.NAMESPACE); + } + if (noInvert) { + DomUtil.addChildElement(elem, XML_NO_INVERT, SecurityConstants.NAMESPACE); + } + if (denyBeforeGrant) { + DomUtil.addChildElement(elem, XML_DENY_BEFORE_GRANT, SecurityConstants.NAMESPACE); + } + if (requiredPrincipal != null) { + elem.appendChild(requiredPrincipal.toXml(document)); + } + return elem; + } + + public boolean isGrantOnly() { + return grantOnly; + } + + public boolean isNoInvert() { + return noInvert; + } + + public boolean isDenyBeforeGrant() { + return denyBeforeGrant; + } + + public Principal getRequiredPrincipal() { + // TODO: check of should be replaced by specific required-principal... + return requiredPrincipal; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/CurrentUserPrivilegeSetProperty.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/CurrentUserPrivilegeSetProperty.java new file mode 100644 index 00000000000..3e94833f888 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/CurrentUserPrivilegeSetProperty.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.webdav.security; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * CurrentUserPrivilegeSetProperty... + */ +public class CurrentUserPrivilegeSetProperty extends AbstractDavProperty> { + + private final Set privileges; + + /** + * Create a new instance of this property. + * + * @param privileges array privileges. + */ + public CurrentUserPrivilegeSetProperty(Privilege[] privileges) { + super(SecurityConstants.CURRENT_USER_PRIVILEGE_SET, true); + + this.privileges = new HashSet(); + for (Privilege privilege : privileges) { + if (privilege != null) { + this.privileges.add(privilege); + } + } + } + + /** + * Create a new CurrentUserPrivilegeSetProperty from a DavProperty + * as obtained from a MultiStatusResponse. + * + * @param xmlDavProperty + * @throws DavException + */ + public CurrentUserPrivilegeSetProperty(DavProperty xmlDavProperty) throws DavException { + super(xmlDavProperty.getName(), true); + if (!SecurityConstants.CURRENT_USER_PRIVILEGE_SET.equals(getName())) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "DAV:current-user-privilege-set expected."); + } + privileges = new HashSet(); + + // parse property value + Object value = xmlDavProperty.getValue(); + if (value != null) { + if (value instanceof Element) { + privileges.add(Privilege.getPrivilege((Element)value)); + } else if (value instanceof Collection) { + for (Object entry : ((Collection) value)) { + if (entry instanceof Element) { + privileges.add(Privilege.getPrivilege((Element) entry)); + } + } + } + } + } + + /** + * @return a Collection of Privilege objects use for xml serialization. + * @see DavProperty#getValue() + * @see AbstractDavProperty#toXml(Document) + */ + public Collection getValue() { + return privileges; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/Principal.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/Principal.java new file mode 100644 index 00000000000..64961494cbf --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/Principal.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.security; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.Map; + +/** + * Principal encapsulates the DAV:principal element which identifies + * the principal to which this ACE applies. + * RFC 3744 defines the following structure for this element: + *
        + * <!ELEMENT principal (href | all | authenticated | unauthenticated | property | self)>
        + * 
        + */ +public class Principal implements XmlSerializable, SecurityConstants { + + public static final String XML_PRINCIPAL = "principal"; + + private static final String XML_ALL = "all"; + private static final String XML_AUTHENTICATED = "authenticated"; + private static final String XML_UNAUTHENTICATED = "unauthenticated"; + private static final String XML_SELF = "self"; + private static final String XML_PROPERTY = "property"; + + private static final int TYPE_ALL = 0; + private static final int TYPE_AUTHENTICATED = 1; + private static final int TYPE_UNAUTHENTICATED = 2; + private static final int TYPE_SELF = 3; + private static final int TYPE_PROPERTY = 4; + private static final int TYPE_HREF = 5; + + private static final Principal ALL_PRINCIPAL = new Principal(TYPE_ALL); + private static final Principal AUTHENTICATED_PRINCIPAL = new Principal(TYPE_AUTHENTICATED); + private static final Principal UNAUTHENTICATED_PRINCIPAL = new Principal(TYPE_UNAUTHENTICATED); + private static final Principal SELF_PRINCIPAL = new Principal(TYPE_SELF); + + private static final Map PROP_PRINCIPALS = new HashMap(); + + private final int type; + private DavPropertyName propertyName; + private String href; + + private Principal(int type) { + this.type = type; + } + + private Principal(DavPropertyName propertyName) { + this.type = TYPE_PROPERTY; + this.propertyName = propertyName; + } + + private Principal(String href) { + this.type = TYPE_HREF; + this.href = href; + } + + /** + * @return href if this Principal is a href-typed principal, null + * otherwise. + */ + public String getHref() { + return href; + } + + /** + * @return propertyName if this Principal is a property-principal, + * null otherwise. + */ + public DavPropertyName getPropertyName() { + return propertyName; + } + + /** + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element pEl = DomUtil.createElement(document, XML_PRINCIPAL, NAMESPACE); + switch (type) { + case TYPE_ALL: + DomUtil.addChildElement(pEl, XML_ALL, NAMESPACE); + break; + case TYPE_AUTHENTICATED: + DomUtil.addChildElement(pEl, XML_AUTHENTICATED, NAMESPACE); + break; + case TYPE_UNAUTHENTICATED: + DomUtil.addChildElement(pEl, XML_UNAUTHENTICATED, NAMESPACE); + break; + case TYPE_SELF: + DomUtil.addChildElement(pEl, XML_SELF, NAMESPACE); + break; + case TYPE_PROPERTY: + Element prop = DomUtil.addChildElement(pEl, XML_PROPERTY, NAMESPACE); + prop.appendChild(propertyName.toXml(document)); + break; + case TYPE_HREF: + Element hrefEl = DomUtil.hrefToXml(href, document); + pEl.appendChild(hrefEl); + break; + // no default + } + return pEl; + } + + public static Principal getAllPrincipal() { + return ALL_PRINCIPAL; + } + + public static Principal getAuthenticatedPrincipal() { + return AUTHENTICATED_PRINCIPAL; + } + + public static Principal getUnauthenticatedPrincipal() { + return UNAUTHENTICATED_PRINCIPAL; + } + + public static Principal getSelfPrincipal() { + return SELF_PRINCIPAL; + } + + public static Principal getPropertyPrincipal(DavPropertyName propertyName) { + if (propertyName == null) { + throw new IllegalArgumentException("Property-Principal must contain a valid property name."); + } + // there is a limited amount of properties, that can be used + // for a Property-Principal + if (PROP_PRINCIPALS.containsKey(propertyName)) { + return PROP_PRINCIPALS.get(propertyName); + } else { + Principal p = new Principal(propertyName); + PROP_PRINCIPALS.put(propertyName, p); + return p; + } + } + + public static Principal getHrefPrincipal(String href) { + if (href == null) { + throw new IllegalArgumentException("Href-Principal must contain a valid href."); + } + return new Principal(href); + } + + public static Principal createFromXml(Element principalElement) throws DavException { + if (!DomUtil.matches(principalElement, XML_PRINCIPAL, NAMESPACE)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "DAV:principal element expected."); + } + if (DomUtil.hasChildElement(principalElement, XML_ALL, NAMESPACE)) { + return ALL_PRINCIPAL; + } else if (DomUtil.hasChildElement(principalElement, XML_SELF, NAMESPACE)) { + return SELF_PRINCIPAL; + } else if (DomUtil.hasChildElement(principalElement, XML_AUTHENTICATED, NAMESPACE)) { + return AUTHENTICATED_PRINCIPAL; + } else if (DomUtil.hasChildElement(principalElement, XML_UNAUTHENTICATED, NAMESPACE)) { + return UNAUTHENTICATED_PRINCIPAL; + } else if (DomUtil.hasChildElement(principalElement, DavConstants.XML_HREF, DavConstants.NAMESPACE)) { + String href = DomUtil.getChildText(principalElement, DavConstants.XML_HREF, DavConstants.NAMESPACE); + return getHrefPrincipal(href); + } else if (DomUtil.hasChildElement(principalElement, XML_PROPERTY, NAMESPACE)) { + Element propEl = DomUtil.getChildElement(principalElement, XML_PROPERTY, NAMESPACE); + DavPropertyName pn = DavPropertyName.createFromXml(DomUtil.getFirstChildElement(propEl)); + return getPropertyPrincipal(pn); + } else { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid structure inside DAV:principal element."); + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/Privilege.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/Privilege.java new file mode 100644 index 00000000000..d7514612af4 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/Privilege.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.webdav.security; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.Map; + +/** + * Privilege + */ +public class Privilege implements XmlSerializable { + + public static final String XML_PRIVILEGE = "privilege"; + + /** + * Map for registered privileges + */ + private static final Map REGISTERED_PRIVILEGES = new HashMap(); + + //-------------------------------------< Privileges defined by RFC 3744 >--- + /** + * The read privilege controls methods that return information about the + * state of the resource, including the resource's properties. Affected + * methods include GET and PROPFIND and OPTIONS. + * + * @see RFC 3744 Section 3.1. DAV:read Privilege + */ + public static final Privilege PRIVILEGE_READ = getPrivilege("read", SecurityConstants.NAMESPACE); + /** + * The write privilege controls methods that lock a resource or modify the + * content, dead properties, or (in the case of a collection) membership of + * the resource, such as PUT and PROPPATCH. + * + * @see RFC 3744 Section 3.2. DAV:write Privilege + */ + public static final Privilege PRIVILEGE_WRITE = getPrivilege("write", SecurityConstants.NAMESPACE); + /** + * The DAV:write-properties privilege controls methods that modify the dead + * properties of the resource, such as PROPPATCH. Whether this privilege may + * be used to control access to any live properties is determined by the + * implementation. + * + * @see RFC 3744 Section 3.3. DAV:write-properties Privilege + */ + public static final Privilege PRIVILEGE_WRITE_PROPERTIES = getPrivilege("write-properties", SecurityConstants.NAMESPACE); + /** + * The DAV:write-content privilege controls methods that modify the content + * of an existing resource, such as PUT. + * + * @see RFC 3744 Section 3.4. DAV:write-content Privilege + */ + public static final Privilege PRIVILEGE_WRITE_CONTENT = getPrivilege("write-content", SecurityConstants.NAMESPACE); + /** + * The DAV:unlock privilege controls the use of the UNLOCK method by a + * principal other than the lock owner (the principal that created a lock + * can always perform an UNLOCK). + * + * @see RFC 3744 Section 3.5. DAV:unlock Privilege + */ + public static final Privilege PRIVILEGE_UNLOCK = getPrivilege("unlock", SecurityConstants.NAMESPACE); + /** + * The DAV:read-acl privilege controls the use of PROPFIND to retrieve the + * DAV:acl property of the resource. + * + * @see RFC 3744 Section 3.6. DAV:read-acl Privilege + */ + public static final Privilege PRIVILEGE_READ_ACL = getPrivilege("read-acl", SecurityConstants.NAMESPACE); + /** + * The DAV:read-current-user-privilege-set privilege controls the use of + * PROPFIND to retrieve the DAV:current-user-privilege-set property of the + * resource. + * + * @see RFC 3744 Section 3.7. DAV:"read-current-user-privilege-set Privilege + */ + public static final Privilege PRIVILEGE_READ_CURRENT_USER_PRIVILEGE_SET = getPrivilege("read-current-user-privilege-set", SecurityConstants.NAMESPACE); + /** + * The DAV:write-acl privilege controls use of the ACL method to modify the + * DAV:acl property of the resource. + * + * @see RFC 3744 Section 3.8. DAV:write-acl Privilege + */ + public static final Privilege PRIVILEGE_WRITE_ACL = getPrivilege("write-acl", SecurityConstants.NAMESPACE); + /** + * The DAV:bind privilege allows a method to add a new member URL to the + * specified collection (for example via PUT or MKCOL). It is ignored for + * resources that are not collections. + * + * @see RFC 3744 Section 3.9. DAV:bind Privilege + */ + public static final Privilege PRIVILEGE_BIND = getPrivilege("bind", SecurityConstants.NAMESPACE); + /** + * The DAV:unbind privilege allows a method to remove a member URL from the + * specified collection (for example via DELETE or MOVE). It is ignored for + * resources that are not collections. + * + * @see RFC 3744 Section 3.10. DAV:unbind Privilege + */ + public static final Privilege PRIVILEGE_UNBIND = getPrivilege("unbind", SecurityConstants.NAMESPACE); + /** + * DAV:all is an aggregate privilege that contains the entire set of + * privileges that can be applied to the resource. + * + * @see RFC 3744 Section 3.11. DAV:all Privilege + */ + public static final Privilege PRIVILEGE_ALL = getPrivilege("all", SecurityConstants.NAMESPACE); + + private final String privilege; + private final Namespace namespace; + + /** + * Private constructor + * + * @param privilege + * @param namespace + */ + private Privilege(String privilege, Namespace namespace) { + this.privilege = privilege; + this.namespace = namespace; + } + + /** + * @return The local name of this Privilege. + */ + public String getName() { + return privilege; + } + + /** + * @return The namespace of this Privilege. + */ + public Namespace getNamespace() { + return namespace; + } + + /** + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element privEl = DomUtil.createElement(document, XML_PRIVILEGE, SecurityConstants.NAMESPACE); + DomUtil.addChildElement(privEl, privilege, namespace); + return privEl; + } + + /** + * Factory method to create/retrieve a Privilege. + * + * @param privilege + * @param namespace + * @return + */ + public static Privilege getPrivilege(String privilege, Namespace namespace) { + if (privilege == null) { + throw new IllegalArgumentException("'null' is not a valid privilege."); + } + if (namespace == null) { + namespace = Namespace.EMPTY_NAMESPACE; + } + String key = "{" + namespace.getURI() + "}" + privilege; + if (REGISTERED_PRIVILEGES.containsKey(key)) { + return REGISTERED_PRIVILEGES.get(key); + } else { + Privilege p = new Privilege(privilege, namespace); + REGISTERED_PRIVILEGES.put(key, p); + return p; + } + } + + /** + * Factory method to create/retrieve a Privilege from the given + * DAV:privilege element. + * + * @param privilege + * @return + */ + public static Privilege getPrivilege(Element privilege) throws DavException { + if (!DomUtil.matches(privilege, XML_PRIVILEGE, SecurityConstants.NAMESPACE)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "DAV:privilege element expected."); + } + Element el = DomUtil.getFirstChildElement(privilege); + return getPrivilege(el.getLocalName(), DomUtil.getNamespace(el)); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SecurityConstants.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SecurityConstants.java new file mode 100644 index 00000000000..5b52c5900aa --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SecurityConstants.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.webdav.security; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.property.ResourceType; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * SecurityConstants interface lists constants defined by + * RFC 3744 (WebDAV Access + * Control Protocol). + */ +public interface SecurityConstants { + + /** + * Default Namespace constant + */ + public static final Namespace NAMESPACE = DavConstants.NAMESPACE; + + /** + * Principal resources must define DAV:principal XML element in the value + * of the DAV:resourcetype property. + */ + public static int PRINCIPAL_RESOURCETYPE = ResourceType.registerResourceType("principal", NAMESPACE); + + //---< Property Names for Principal Resource >------------------------------ + /** + * Protected href property DAV:principal-URL for principal resources. + * @see HrefProperty + */ + public static final DavPropertyName PRINCIPAL_URL = DavPropertyName.create("principal-URL", NAMESPACE); + /** + * Protected href property DAV:alternate-URI-set for principal resources. + * @see HrefProperty + */ + public static final DavPropertyName ALTERNATE_URI_SET = DavPropertyName.create("alternate-URI-set", NAMESPACE); + /** + * Protected href property DAV:group-member-set for principal resources. + * @see HrefProperty + */ + public static final DavPropertyName GROUP_MEMBER_SET = DavPropertyName.create("group-member-set", NAMESPACE); + /** + * Protected href property DAV:group-membership for principal resources. + * @see HrefProperty + */ + public static final DavPropertyName GROUP_MEMBERSHIP = DavPropertyName.create("group-membership", NAMESPACE); + + //---< Property Names for DavResource >------------------------------------- + /** + * Protected href property DAV:owner + * @see HrefProperty + */ + public static final DavPropertyName OWNER = DavPropertyName.create("owner", NAMESPACE); + /** + * Protected href property DAV:group + * @see HrefProperty + */ + public static final DavPropertyName GROUP = DavPropertyName.create("group", NAMESPACE); + /** + * Protected property DAV:supported-privilege-set + * @see CurrentUserPrivilegeSetProperty + */ + public static final DavPropertyName SUPPORTED_PRIVILEGE_SET = DavPropertyName.create("supported-privilege-set", NAMESPACE); + /** + * Protected property DAV:current-user-privilege-set + * @see CurrentUserPrivilegeSetProperty + */ + public static final DavPropertyName CURRENT_USER_PRIVILEGE_SET = DavPropertyName.create("current-user-privilege-set", NAMESPACE); + /** + * Protected property DAV:acl. Note, that DAV:acl property may be altered + * with a ACL request. + * + * @see AclProperty + * @see DavMethods#METHOD_ACL + */ + public static final DavPropertyName ACL = DavPropertyName.create("acl", NAMESPACE); + /** + * Protected property DAV:acl-restrictions + * @see AclRestrictionsProperty + */ + public static final DavPropertyName ACL_RESTRICTIONS = DavPropertyName.create("acl-restrictions", NAMESPACE); + /** + * Protected href property DAV:inherited-acl-set + * @see HrefProperty + */ + public static final DavPropertyName INHERITED_ACL_SET = DavPropertyName.create("inherited-acl-set", NAMESPACE); + /** + * Protected href property DAV:principal-collection-set + * @see HrefProperty + */ + public static final DavPropertyName PRINCIPAL_COLLECTION_SET = DavPropertyName.create("principal-collection-set", NAMESPACE); +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SupportedPrivilege.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SupportedPrivilege.java new file mode 100644 index 00000000000..7c55c13258c --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SupportedPrivilege.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.webdav.security; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.ElementIterator; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * SupportedPrivilege... + */ +public class SupportedPrivilege implements XmlSerializable { + + private static final String XML_SUPPORTED_PRIVILEGE = "supported-privilege"; + private static final String XML_ABSTRACT = "abstract"; + private static final String XML_DESCRIPTION = "description"; + + private final Privilege privilege; + private final boolean isAbstract; + private final String description; + private final String descriptionLanguage; + private final SupportedPrivilege[] supportedPrivileges; + + /** + * + * @param privilege + * @param description + * @param descriptionLanguage + * @param isAbstract + * @param supportedPrivileges + */ + public SupportedPrivilege(Privilege privilege, String description, + String descriptionLanguage, boolean isAbstract, + SupportedPrivilege[] supportedPrivileges) { + if (privilege == null) { + throw new IllegalArgumentException("DAV:supported-privilege element must contain a single privilege."); + } + this.privilege = privilege; + this.description = description; + this.descriptionLanguage = descriptionLanguage; + this.isAbstract = isAbstract; + this.supportedPrivileges = supportedPrivileges; + } + + /** + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element spElem = DomUtil.createElement(document, XML_SUPPORTED_PRIVILEGE, SecurityConstants.NAMESPACE); + spElem.appendChild(privilege.toXml(document)); + if (isAbstract) { + DomUtil.addChildElement(spElem, XML_ABSTRACT, SecurityConstants.NAMESPACE); + } + if (description != null) { + Element desc = DomUtil.addChildElement(spElem, XML_DESCRIPTION, SecurityConstants.NAMESPACE, description); + if (descriptionLanguage != null) { + DomUtil.setAttribute(desc, "lang", Namespace.XML_NAMESPACE, descriptionLanguage); + } + } + if (supportedPrivileges != null) { + for (SupportedPrivilege supportedPrivilege : supportedPrivileges) { + spElem.appendChild(supportedPrivilege.toXml(document)); + } + } + return spElem; + } + + public Privilege getPrivilege() { + return privilege; + } + + public boolean isAbstract() { + return isAbstract; + } + + public SupportedPrivilege[] getSupportedPrivileges() { + return supportedPrivileges; + } + + /** + * Factory method to create/retrieve a SupportedPrivilege from the given + * DAV:privilege element. + * + * @param privilege + * @return + */ + static SupportedPrivilege getSupportedPrivilege(Element supportedPrivilege) throws DavException { + if (!DomUtil.matches(supportedPrivilege, XML_SUPPORTED_PRIVILEGE, SecurityConstants.NAMESPACE)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "DAV:supported-privilege element expected."); + } + boolean isAbstract = false; + Privilege privilege = null; + String description = null; + String descriptionLanguage = null; + List sp = new ArrayList(); + + ElementIterator children = DomUtil.getChildren(supportedPrivilege); + while (children.hasNext()) { + Element child = children.next(); + if (child.getLocalName().equals(XML_ABSTRACT)) { + isAbstract = true; + } else if (child.getLocalName().equals(Privilege.XML_PRIVILEGE)) { + privilege = Privilege.getPrivilege(child); + } else if (child.getLocalName().equals(XML_DESCRIPTION)) { + description = child.getLocalName(); + if (child.hasAttribute(descriptionLanguage)) { + descriptionLanguage = child.getAttribute(descriptionLanguage); + } + } else if (child.getLocalName().equals(XML_SUPPORTED_PRIVILEGE)) { + sp.add(getSupportedPrivilege(child)); + } + } + return new SupportedPrivilege(privilege, description, + descriptionLanguage, isAbstract, + sp.toArray(new SupportedPrivilege[sp.size()])); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SupportedPrivilegeSetProperty.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SupportedPrivilegeSetProperty.java new file mode 100644 index 00000000000..6223d2472ee --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/SupportedPrivilegeSetProperty.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.webdav.security; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.property.AbstractDavProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Collections; + +/** + * SupportedPrivilegeSetProperty defines the + * {@link SecurityConstants#SUPPORTED_PRIVILEGE_SET} property, used to identify + * the privileges defined for the resource. RFC 3744 defines the the following + * structure for this property: + *
        + * <!ELEMENT supported-privilege-set (supported-privilege*)>
        + * <!ELEMENT supported-privilege (privilege, abstract?, description, supported-privilege*)>
        + * <!ELEMENT privilege ANY>
        + * <!ELEMENT abstract EMPTY>
        + * <!ELEMENT description #PCDATA>
        + * 
        + * + * @see SupportedPrivilege + * @see Privilege + */ +public class SupportedPrivilegeSetProperty extends AbstractDavProperty> { + + private final SupportedPrivilege[] supportedPrivileges; + + /** + * Create a new SupportedPrivilegeSetProperty. + * + * @param supportedPrivileges + */ + public SupportedPrivilegeSetProperty(SupportedPrivilege[] supportedPrivileges) { + super(SecurityConstants.SUPPORTED_PRIVILEGE_SET, true); + this.supportedPrivileges = supportedPrivileges; + } + + public SupportedPrivilegeSetProperty(DavProperty p) throws DavException { + super(SecurityConstants.SUPPORTED_PRIVILEGE_SET, true); + if (!SecurityConstants.SUPPORTED_PRIVILEGE_SET.equals(getName())) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "DAV:supported-privilege-set expected."); + } + + List supportedPrivs = new ArrayList(); + + for (Object obj : Collections.singletonList(p.getValue())) { + if (obj instanceof Element) { + supportedPrivs.add(SupportedPrivilege.getSupportedPrivilege((Element) obj)); + } else if (obj instanceof Collection) { + for (Object entry : ((Collection) obj)) { + if (entry instanceof Element) { + supportedPrivs.add(SupportedPrivilege.getSupportedPrivilege((Element) entry)); + } + } + } + } + supportedPrivileges = supportedPrivs.toArray(new SupportedPrivilege[supportedPrivs.size()]); + } + + /** + * @return List of {@link SupportedPrivilege}s. + */ + public List getValue() { + List l; + if (supportedPrivileges == null) { + l = Collections.emptyList(); + } else { + l = Arrays.asList(supportedPrivileges); + } + return l; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/package-info.java new file mode 100644 index 00000000000..dec831d5b10 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.security; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/AbstractSecurityReport.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/AbstractSecurityReport.java new file mode 100644 index 00000000000..27f7a9df3f7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/AbstractSecurityReport.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.webdav.security.report; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * AbstractSecurityReport covers basic validation and utilities + * common to the majority of the reports defined within RFC 3744. + */ +public abstract class AbstractSecurityReport implements Report { + + protected MultiStatusResponse[] responses; + + /** + * Always returns true. + * + * @return true + */ + public boolean isMultiStatusReport() { + return true; + } + + /** + * Checks if the given resource and report info are not null, + * that the requested report type matches this implementation and that no + * other Depth header than 0 is present. + * + * @param resource + * @param info + * @throws DavException + */ + public void init(DavResource resource, ReportInfo info) throws DavException { + if (resource == null || info == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unable to run report: WebDAV Resource and ReportInfo must not be null."); + } + if (!getType().isRequestedReportType(info)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Expected report type: '" + getType().getReportName() + "', found: '" + info.getReportName() + ";" + "'."); + } + if (info.getDepth() > DavConstants.DEPTH_0) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid Depth header: " + info.getDepth()); + } + } + + /** + * @return DAV:multistatus element listing the matching resources. + * @see Report#toXml(Document) + */ + public Element toXml(Document document) { + MultiStatus ms = new MultiStatus(); + if (responses != null) { + for (MultiStatusResponse response : responses) { + ms.addResponse(response); + } + } + return ms.toXml(document); + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/AclPrincipalReport.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/AclPrincipalReport.java new file mode 100644 index 00000000000..64c56a4d055 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/AclPrincipalReport.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.webdav.security.report; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceLocator; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.security.AclProperty; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The AclPrincipalReport report returns the requested property set + * for all principals in the DAV:acl property, that are identified by http(s) + * URLs or by a DAV:property principal. + *

        + * The request body MUST be a DAV:acl-principal-prop-set XML element: + *

        + * <!ELEMENT acl-principal-prop-set ANY>
        + * ANY value: a sequence of one or more elements, with at most one
        + *            DAV:prop element.
        + * prop: see RFC 2518, Section 12.11
        + * 
        + * The response body MUST be a DAV:multistatus element containing a + * DAV:response element for each principal identified by a http(s) URL listed + * in a DAV:principal XML element of an ACE within the DAV:acl property of the + * resource this report is requested for. + */ +public class AclPrincipalReport extends AbstractSecurityReport { + + public static final String REPORT_NAME = "acl-principal-prop-set"; + + /** + * The report type + */ + public static final ReportType REPORT_TYPE = ReportType.register(REPORT_NAME, SecurityConstants.NAMESPACE, AclPrincipalReport.class); + + /** + * @see Report#getType() + */ + public ReportType getType() { + return REPORT_TYPE; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + super.init(resource, info); + // build the DAV:responses objects. + DavProperty acl = resource.getProperty(SecurityConstants.ACL); + if (!(acl instanceof AclProperty)) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "DAV:acl property expected."); + } + + DavResourceLocator loc = resource.getLocator(); + Map respMap = new HashMap(); + List list = (List) ((AclProperty)acl).getValue(); + for (AclProperty.Ace ace : list) { + String href = ace.getPrincipal().getHref(); + if (href == null || respMap.containsKey(href)) { + // ignore non-href principals and principals that have been listed before + continue; + } + // href-principal that has not been found before + DavResourceLocator princLocator = loc.getFactory().createResourceLocator(loc.getPrefix(), href); + DavResource principalResource = resource.getFactory().createResource(princLocator, resource.getSession()); + respMap.put(href, new MultiStatusResponse(principalResource, info.getPropertyNameSet())); + } + this.responses = respMap.values().toArray(new MultiStatusResponse[respMap.size()]); + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/PrincipalMatchReport.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/PrincipalMatchReport.java new file mode 100644 index 00000000000..3d86b92c64b --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/PrincipalMatchReport.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.webdav.security.report; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Element; + +/** + * PrincipalMatchReport can be request for any collection resources. + * The resulting report identifies member resources that either represent the + * requesting principal ("principal resources") or contain a specified property + * that matches the requesting principal in its value. For the first match + * the request body must contain a DAV:self element, for the latter a + * DAV:principal-property element which in turn specifies the property to + * be examined. + *

        + * The request body MUST be a DAV:principal-match XML element: + *

        + * <!ELEMENT principal-match ((principal-property | self), prop?)>
        + * <!ELEMENT principal-property ANY>
        + * ANY value: an element whose value identifies a property. The value of this
        + * property typically contains an href element referring to a principal.
        + * <!ELEMENT self EMPTY>
        + * prop: see RFC 2518, Section 12.11
        + * 
        + * The response body of a successful report must contain a DAV:multistatus + * element. Each matching member is present with a separate DAV:response element. + */ +public class PrincipalMatchReport extends AbstractSecurityReport { + + public static final String XML_PRINCIPAL_PROPERTY = "principal-property"; + public static final String XML_SELF = "self"; + + /** + * The report name + */ + public static final String REPORT_NAME = "principal-match"; + + /** + * The report type + */ + public static final ReportType REPORT_TYPE = ReportType.register(REPORT_NAME, SecurityConstants.NAMESPACE, PrincipalMatchReport.class); + + private DavPropertyName principalPropertyName; + + /** + * @see Report#getType() + */ + public ReportType getType() { + return REPORT_TYPE; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + super.init(resource, info); + if (info.containsContentElement(XML_PRINCIPAL_PROPERTY, SecurityConstants.NAMESPACE)) { + Element pp = info.getContentElement(XML_PRINCIPAL_PROPERTY, SecurityConstants.NAMESPACE); + principalPropertyName = DavPropertyName.createFromXml(DomUtil.getFirstChildElement(pp)); + } else if (info.containsContentElement(XML_SELF, SecurityConstants.NAMESPACE)) { + principalPropertyName = SecurityConstants.PRINCIPAL_URL; + } else { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "DAV:self or DAV:principal-property element required within report info."); + } + } + + //------------------------------------< implementation specific methods >--- + /** + * Retrieve the property name that indicates which property must be search + * for matching principals.
        + * Note, that the search result must be converted to {@link MultiStatusResponse}s + * that must be returned back to this report. + * + * @return property name which should be check for match with the current + * user. + * @see #setResponses(MultiStatusResponse[]) + */ + public DavPropertyName getPrincipalPropertyName() { + return principalPropertyName; + } + + /** + * Write the result(s) of the match back to the report. + * + * @param responses + */ + public void setResponses(MultiStatusResponse[] responses) { + this.responses = responses; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/PrincipalSearchReport.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/PrincipalSearchReport.java new file mode 100644 index 00000000000..3c2f7223cb1 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/PrincipalSearchReport.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.security.report; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.HrefProperty; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +import java.util.List; + +/** + * The PrincipalSearchReport performs a search for all principals + * that match the search criteria specified in the request. + *

        + * The following XML structure is required in the request body: + *

        + * <!ELEMENT principal-property-search ((property-search+), prop?, apply-to-principal-collection-set?) >
        + * <!ELEMENT property-search (prop, match) >
        + *  prop: see RFC 2518, Section 12.11
        + * <!ELEMENT match #PCDATA >
        + * <!ELEMENT apply-to-principal-collection-set #EMPTY >
        + * 
        + * DAV:property-search contains lists the properties to be + * searched inside the DAV:prop element and the query string inside the DAV:match + * element. Multiple DAV:property-search elements or multiple elements within the + * DAV:prop element will be interpreted with a logical AND. + *

        + * DAV:prop lists the property names to be reported in the + * response for each of the principle resources found. + *

        + * DAV:apply-to-principal-collection-set: Optional empty element. + * If present in the request body the search will be executed over all members + * of the collections that are listed as values in the DAV:principal-collection-set + * property present on the resource the report has been requested for. + * Otherwise the search is limited to all members (at any depth) of that resource + * itself. + *

        + * The response body must contain a single DAV:multistatus XML element. + */ +public class PrincipalSearchReport extends AbstractSecurityReport { + + private static Logger log = LoggerFactory.getLogger(PrincipalSearchReport.class); + + public static final String XML_APPLY_TO_PRINCIPAL_COLLECTION_SET = "apply-to-principal-collection-set"; + public static final String XML_PROPERTY_SEARCH = "property-search"; + public static final String XML_MATCH = "match"; + + /** + * The report name + */ + public static final String REPORT_NAME = "principal-property-search"; + + /** + * The report type + */ + public static final ReportType REPORT_TYPE = ReportType.register(REPORT_NAME, SecurityConstants.NAMESPACE, PrincipalSearchReport.class); + + private String[] searchRoots; + private SearchArgument[] searchArguments; + + /** + * @see Report#getType() + */ + public ReportType getType() { + return REPORT_TYPE; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + @Override + public void init(DavResource resource, ReportInfo info) throws DavException { + super.init(resource, info); + // make sure the request body contains all mandatory elements + if (!info.containsContentElement(XML_PROPERTY_SEARCH, SecurityConstants.NAMESPACE)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Request body must contain at least a single DAV:property-search element."); + } + List psElements = info.getContentElements(XML_PROPERTY_SEARCH, SecurityConstants.NAMESPACE); + searchArguments = new SearchArgument[psElements.size()]; + int i = 0; + for (Element psElement : psElements) { + searchArguments[i++] = new SearchArgument(psElement); + } + + if (info.containsContentElement(XML_APPLY_TO_PRINCIPAL_COLLECTION_SET, SecurityConstants.NAMESPACE)) { + HrefProperty p = new HrefProperty(resource.getProperty(SecurityConstants.PRINCIPAL_COLLECTION_SET)); + searchRoots = p.getHrefs().toArray(new String[0]); + } else { + searchRoots = new String[] {resource.getHref()}; + } + } + + //------------------------------------< implementation specific methods >--- + /** + * Retrieve the the locations where the search should be performed.
        + * Note, that the search result must be converted to {@link MultiStatusResponse}s + * that must be returned back to this report. + * + * @return href of collections that act as start for the search. + * @see #setResponses(MultiStatusResponse[]) + */ + public String[] getSearchRoots() { + return searchRoots; + } + + /** + * Retrieve the search arguments used to run the search for principals.
        + * Note, that the search result must be converted to {@link MultiStatusResponse}s + * that must be returned back to this report. + * + * @return array of SearchArgument used to run the principal + * search. + * @see #setResponses(MultiStatusResponse[]) + */ + public SearchArgument[] getSearchArguments() { + return searchArguments; + } + + /** + * Write the search result back to the report. + * + * @param responses + */ + public void setResponses(MultiStatusResponse[] responses) { + this.responses = responses; + } + + //--------------------------------< implementation specific inner class >--- + /** + * Inner utility class preparing the query arguments present in the + * DAV:property-search element(s). + */ + protected class SearchArgument { + + private final DavPropertyNameSet searchProps; + private final String searchString; + + private SearchArgument(Element propSearch) { + searchProps = new DavPropertyNameSet(DomUtil.getChildElement(propSearch, DavConstants.XML_PROP, DavConstants.NAMESPACE)); + searchString = DomUtil.getChildText(propSearch, XML_MATCH, SecurityConstants.NAMESPACE); + } + + /** + * @return property name set used to restrict the search to a limited + * amount of properties. + */ + protected DavPropertyNameSet getSearchProperties() { + return searchProps; + } + + /** + * @return query string as present in the DAV:match element. + */ + protected String getSearchString() { + return searchString; + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/SearchablePropertyReport.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/SearchablePropertyReport.java new file mode 100644 index 00000000000..5e6c21ed815 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/SearchablePropertyReport.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.security.report; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.Namespace; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.HashSet; +import java.util.Set; + +/** + * SearchablePropertyReport identifies those properties that may be + * searched using the {@link PrincipalSearchReport DAV:principal-property-search REPORT}. + * This report must be supported on all collections identified in the value of + * a DAV:principal-collection-set property. + *

        + * The request body MUST be an empty DAV:principal-search-property-set element. + *

        + * The response body MUST be a DAV:principal-search-property-set XML element + * with the following structure: + *

        + *  <!ELEMENT principal-search-property-set (principal-search-property*) >
        + *  <!ELEMENT principal-search-property (prop, description) >
        + *  prop: see RFC 2518, Section 12.11
        + *  <!ELEMENT description #PCDATA >
        + *  Human readable description. Note, that the language of the description must
        + *  be indicated by the xml:lang attribute.
        + * 
        + * + * Note that a DAV:principal-search-property XML element is required for each + * property that may be searched with the DAV:principal-property-search REPORT. + */ +public class SearchablePropertyReport implements Report { + + /** + * The report name + */ + public static final String REPORT_NAME = "principal-search-property-set"; + + /** + * The report type + */ + public static final ReportType REPORT_TYPE = ReportType.register(REPORT_NAME, SecurityConstants.NAMESPACE, SearchablePropertyReport.class); + + /** + * Constant used for the DAV:principal-search-property-set response element. + */ + public static final String XML_PRINCIPAL_SEARCH_PROPERTY_SET = "principal-search-property-set"; + + /** + * Set collecting the DAV:principal-search-property entries. + */ + private final Set searchPropertySet = new HashSet(); + + /** + * @see Report#getType() + */ + public ReportType getType() { + return REPORT_TYPE; + } + + /** + * @return false Status code of after a successful completion must be + * {@link DavServletResponse#SC_OK 200 (ok)}. + * @see Report#isMultiStatusReport() + */ + public boolean isMultiStatusReport() { + return false; + } + + /** + * @see Report#init(DavResource, ReportInfo) + */ + public void init(DavResource resource, ReportInfo info) throws DavException { + if (resource == null || info == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unable to run report: WebDAV Resource and ReportInfo must not be null."); + } + if (!getType().isRequestedReportType(info)) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Expected report type: '" + getType().getReportName() + "', found: '" + info.getReportName() + ";" + "'."); + } + if (info.getDepth() > DavConstants.DEPTH_0) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid Depth header: " + info.getDepth()); + } + } + + /** + * @see Report#toXml(Document) + */ + public Element toXml(Document document) { + Element rootElem = DomUtil.createElement(document, XML_PRINCIPAL_SEARCH_PROPERTY_SET, SecurityConstants.NAMESPACE); + for (PrincipalSearchProperty psp : searchPropertySet) { + rootElem.appendChild(psp.toXml(document)); + } + return rootElem; + } + + //-----------------------------------------------------< implementation >--- + /** + * Add a property name that should be listed in the DAV:principal-search-property-set. + * + * @param propName a property name that may be searched in the {@link PrincipalSearchReport}. + * @param description Human readable description of the searchable property. + * @param language defines in which language the description is written. + * @throws IllegalArgumentException if propName is null. + */ + public void addPrincipalSearchProperty(DavPropertyName propName, + String description, String language) { + searchPropertySet.add(new PrincipalSearchProperty(propName, description, language)); + } + + //--------------------------------------------------------< inner class >--- + /** + * Inner class encapsulating the DAV:principal-search-property + */ + private class PrincipalSearchProperty implements XmlSerializable { + + private static final String XML_PRINCIPAL_SEARCH_PROPERTY = "principal-search-property"; + private static final String XML_DESCRIPTION = "description"; + private static final String ATTR_LANG = "lang"; + + private final DavPropertyName propName; + private final String description; + private final String language; + private final int hashCode; + + private PrincipalSearchProperty(DavPropertyName propName, + String description, + String language) { + if (propName == null) { + throw new IllegalArgumentException("null is not a valid DavPropertyName for the DAV:principal-search-property."); + } + this.propName = propName; + this.description = description; + this.language = language; + hashCode = propName.hashCode(); + } + + /** + * @see XmlSerializable#toXml(Document) + */ + public Element toXml(Document document) { + Element psElem = DomUtil.createElement(document, XML_PRINCIPAL_SEARCH_PROPERTY, SecurityConstants.NAMESPACE); + // create property set from the single property name + DavPropertyNameSet pnSet = new DavPropertyNameSet(); + pnSet.add(propName); + psElem.appendChild(pnSet.toXml(document)); + // append description if present + if (description != null) { + Element desc = DomUtil.addChildElement(psElem, XML_DESCRIPTION, SecurityConstants.NAMESPACE, description); + if (language != null) { + DomUtil.setAttribute(desc, ATTR_LANG, Namespace.XML_NAMESPACE, language); + } + } + return psElem; + } + + //---------------------------------------------------------< Object >--- + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof PrincipalSearchProperty) { + PrincipalSearchProperty other = (PrincipalSearchProperty)obj; + // ignore the optional description/language + return hashCode == other.hashCode; + } + return false; + } + + @Override + public int hashCode() { + return hashCode; + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/package-info.java new file mode 100644 index 00000000000..bef4bb984e4 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/security/report/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.security.report; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/server/AbstractWebdavServlet.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/server/AbstractWebdavServlet.java new file mode 100644 index 00000000000..d230822d136 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/server/AbstractWebdavServlet.java @@ -0,0 +1,1430 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.server; + +import org.apache.jackrabbit.webdav.DavCompliance; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavLocatorFactory; +import org.apache.jackrabbit.webdav.DavMethods; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavServletRequest; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSessionProvider; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.WebdavRequestImpl; +import org.apache.jackrabbit.webdav.WebdavResponse; +import org.apache.jackrabbit.webdav.WebdavResponseImpl; +import org.apache.jackrabbit.webdav.bind.RebindInfo; +import org.apache.jackrabbit.webdav.bind.UnbindInfo; +import org.apache.jackrabbit.webdav.bind.BindableResource; +import org.apache.jackrabbit.webdav.bind.BindInfo; +import org.apache.jackrabbit.webdav.header.CodedUrlHeader; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.InputContextImpl; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.io.OutputContextImpl; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockDiscovery; +import org.apache.jackrabbit.webdav.lock.LockInfo; +import org.apache.jackrabbit.webdav.observation.EventDiscovery; +import org.apache.jackrabbit.webdav.observation.ObservationResource; +import org.apache.jackrabbit.webdav.observation.Subscription; +import org.apache.jackrabbit.webdav.observation.SubscriptionInfo; +import org.apache.jackrabbit.webdav.ordering.OrderPatch; +import org.apache.jackrabbit.webdav.ordering.OrderingResource; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.PropEntry; +import org.apache.jackrabbit.webdav.search.SearchConstants; +import org.apache.jackrabbit.webdav.search.SearchInfo; +import org.apache.jackrabbit.webdav.search.SearchResource; +import org.apache.jackrabbit.webdav.security.AclProperty; +import org.apache.jackrabbit.webdav.security.AclResource; +import org.apache.jackrabbit.webdav.transaction.TransactionInfo; +import org.apache.jackrabbit.webdav.transaction.TransactionResource; +import org.apache.jackrabbit.webdav.util.CSRFUtil; +import org.apache.jackrabbit.webdav.version.ActivityResource; +import org.apache.jackrabbit.webdav.version.DeltaVConstants; +import org.apache.jackrabbit.webdav.version.DeltaVResource; +import org.apache.jackrabbit.webdav.version.LabelInfo; +import org.apache.jackrabbit.webdav.version.MergeInfo; +import org.apache.jackrabbit.webdav.version.OptionsInfo; +import org.apache.jackrabbit.webdav.version.OptionsResponse; +import org.apache.jackrabbit.webdav.version.UpdateInfo; +import org.apache.jackrabbit.webdav.version.VersionControlledResource; +import org.apache.jackrabbit.webdav.version.VersionResource; +import org.apache.jackrabbit.webdav.version.VersionableResource; +import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +/** + * AbstractWebdavServlet + *

        + */ +abstract public class AbstractWebdavServlet extends HttpServlet implements DavConstants { + + // todo respect Position header + /** + * default logger + */ + private static Logger log = LoggerFactory.getLogger(AbstractWebdavServlet.class); + + /** the 'missing-auth-mapping' init parameter */ + public final static String INIT_PARAM_MISSING_AUTH_MAPPING = "missing-auth-mapping"; + + /** + * Name of the optional init parameter that defines the value of the + * 'WWW-Authenticate' header. + *

        + * If the parameter is omitted the default value + * {@link #DEFAULT_AUTHENTICATE_HEADER "Basic Realm=Jackrabbit Webdav Server"} + * is used. + * + * @see #getAuthenticateHeaderValue() + */ + public static final String INIT_PARAM_AUTHENTICATE_HEADER = "authenticate-header"; + + /** + * Default value for the 'WWW-Authenticate' header, that is set, if request + * results in a {@link DavServletResponse#SC_UNAUTHORIZED 401 (Unauthorized)} + * error. + * + * @see #getAuthenticateHeaderValue() + */ + public static final String DEFAULT_AUTHENTICATE_HEADER = "Basic realm=\"Jackrabbit Webdav Server\""; + + /** + * Name of the parameter that specifies the configuration of the CSRF protection. + * May contain a comma-separated list of allowed referrer hosts. + * If the parameter is omitted or left empty the behaviour is to only allow requests which have an empty referrer + * or a referrer host equal to the server host. + * If the parameter is set to 'disabled' no referrer checks will be performed at all. + */ + public static final String INIT_PARAM_CSRF_PROTECTION = "csrf-protection"; + + /** + * Name of the 'createAbsoluteURI' init parameter that defines whether hrefs + * should be created with a absolute URI or as absolute Path (ContextPath). + * The value should be 'true' or 'false'. The default value if not set is true. + */ + public final static String INIT_PARAM_CREATE_ABSOLUTE_URI = "createAbsoluteURI"; + + + /** + * Header value as specified in the {@link #INIT_PARAM_AUTHENTICATE_HEADER} parameter. + */ + private String authenticate_header; + + /** + * CSRF protection utility + */ + private CSRFUtil csrfUtil; + + /** + * Create per default absolute URI hrefs + */ + private boolean createAbsoluteURI = true; + + @Override + public void init() throws ServletException { + super.init(); + + // authenticate header + authenticate_header = getInitParameter(INIT_PARAM_AUTHENTICATE_HEADER); + if (authenticate_header == null) { + authenticate_header = DEFAULT_AUTHENTICATE_HEADER; + } + log.info(INIT_PARAM_AUTHENTICATE_HEADER + " = " + authenticate_header); + + // read csrf protection params + String csrfParam = getInitParameter(INIT_PARAM_CSRF_PROTECTION); + csrfUtil = new CSRFUtil(csrfParam); + log.info(INIT_PARAM_CSRF_PROTECTION + " = " + csrfParam); + + //create absolute URI hrefs.. + String param = getInitParameter(INIT_PARAM_CREATE_ABSOLUTE_URI); + if (param != null) { + createAbsoluteURI = Boolean.parseBoolean(param); + } + log.info(INIT_PARAM_CREATE_ABSOLUTE_URI + " = " + createAbsoluteURI); + } + + /** + * Checks if the precondition for this request and resource is valid. + * + * @param request + * @param resource + * @return + */ + abstract protected boolean isPreconditionValid(WebdavRequest request, DavResource resource); + + /** + * Returns the DavSessionProvider. + * + * @return the session provider + */ + abstract public DavSessionProvider getDavSessionProvider(); + + /** + * Returns the DavSessionProvider. + * + * @param davSessionProvider + */ + abstract public void setDavSessionProvider(DavSessionProvider davSessionProvider); + + /** + * Returns the DavLocatorFactory. + * + * @return the locator factory + */ + abstract public DavLocatorFactory getLocatorFactory(); + + /** + * Sets the DavLocatorFactory. + * + * @param locatorFactory + */ + abstract public void setLocatorFactory(DavLocatorFactory locatorFactory); + + /** + * Returns the DavResourceFactory. + * + * @return the resource factory + */ + abstract public DavResourceFactory getResourceFactory(); + + /** + * Sets the DavResourceFactory. + * + * @param resourceFactory + */ + abstract public void setResourceFactory(DavResourceFactory resourceFactory); + + /** + * Returns the value of the 'WWW-Authenticate' header, that is returned in + * case of 401 error: the value is retrireved from the corresponding init + * param or defaults to {@link #DEFAULT_AUTHENTICATE_HEADER}. + * + * @return corresponding init parameter or {@link #DEFAULT_AUTHENTICATE_HEADER}. + * @see #INIT_PARAM_AUTHENTICATE_HEADER + */ + public String getAuthenticateHeaderValue() { + return authenticate_header; + } + + /** + * Returns if a absolute URI should be created for hrefs. + * + * @return absolute URI hrefs + */ + protected boolean isCreateAbsoluteURI() { + return createAbsoluteURI; + } + + /** + * Service the given request. + * + * @param request + * @param response + * @throws ServletException + * @throws IOException + */ + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + WebdavRequest webdavRequest = new WebdavRequestImpl(request, getLocatorFactory(), isCreateAbsoluteURI()); + // DeltaV requires 'Cache-Control' header for all methods except 'VERSION-CONTROL' and 'REPORT'. + int methodCode = DavMethods.getMethodCode(request.getMethod()); + boolean noCache = DavMethods.isDeltaVMethod(webdavRequest) && !(DavMethods.DAV_VERSION_CONTROL == methodCode || DavMethods.DAV_REPORT == methodCode); + WebdavResponse webdavResponse = new WebdavResponseImpl(response, noCache); + try { + // make sure there is a authenticated user + if (!getDavSessionProvider().attachSession(webdavRequest)) { + return; + } + + // perform referrer host checks if CSRF protection is enabled + if (!csrfUtil.isValidRequest(webdavRequest)) { + webdavResponse.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + + // JCR-4165: reject any content-coding in request until we can + // support it (see JCR-4166) + List ces = getContentCodings(request); + if (!ces.isEmpty()) { + webdavResponse.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + webdavResponse.setHeader("Accept-Encoding", "identity"); + webdavResponse.setContentType("text/plain; charset=UTF-8"); + webdavResponse.getWriter().println("Content-Encodings not supported, but received: " + ces); + webdavResponse.getWriter().flush(); + } + + // check matching if=header for lock-token relevant operations + DavResource resource = getResourceFactory().createResource(webdavRequest.getRequestLocator(), webdavRequest, webdavResponse); + if (!isPreconditionValid(webdavRequest, resource)) { + webdavResponse.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); + return; + } + if (!execute(webdavRequest, webdavResponse, methodCode, resource)) { + super.service(request, response); + } + + } catch (DavException e) { + if (e.getErrorCode() == HttpServletResponse.SC_UNAUTHORIZED) { + sendUnauthorized(webdavRequest, webdavResponse, e); + } else { + webdavResponse.sendError(e); + } + } finally { + getDavSessionProvider().releaseSession(webdavRequest); + } + } + + /** + * Sets the "WWW-Authenticate" header and writes the appropriate error + * to the given webdav response. + * + * @param request The webdav request. + * @param response The webdav response. + * @param error The DavException that leads to the unauthorized response. + * @throws IOException + */ + protected void sendUnauthorized(WebdavRequest request, + WebdavResponse response, DavException error) throws IOException { + response.setHeader("WWW-Authenticate", getAuthenticateHeaderValue()); + if (error == null || error.getErrorCode() != HttpServletResponse.SC_UNAUTHORIZED) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } else { + response.sendError(error.getErrorCode(), error.getStatusPhrase()); + } + } + + /** + * Executes the respective method in the given webdav context + * + * @param request + * @param response + * @param method + * @param resource + * @throws ServletException + * @throws IOException + * @throws DavException + */ + protected boolean execute(WebdavRequest request, WebdavResponse response, + int method, DavResource resource) + throws ServletException, IOException, DavException { + + switch (method) { + case DavMethods.DAV_GET: + doGet(request, response, resource); + break; + case DavMethods.DAV_HEAD: + doHead(request, response, resource); + break; + case DavMethods.DAV_PROPFIND: + doPropFind(request, response, resource); + break; + case DavMethods.DAV_PROPPATCH: + doPropPatch(request, response, resource); + break; + case DavMethods.DAV_POST: + doPost(request, response, resource); + break; + case DavMethods.DAV_PUT: + doPut(request, response, resource); + break; + case DavMethods.DAV_DELETE: + doDelete(request, response, resource); + break; + case DavMethods.DAV_COPY: + doCopy(request, response, resource); + break; + case DavMethods.DAV_MOVE: + doMove(request, response, resource); + break; + case DavMethods.DAV_MKCOL: + doMkCol(request, response, resource); + break; + case DavMethods.DAV_OPTIONS: + doOptions(request, response, resource); + break; + case DavMethods.DAV_LOCK: + doLock(request, response, resource); + break; + case DavMethods.DAV_UNLOCK: + doUnlock(request, response, resource); + break; + case DavMethods.DAV_ORDERPATCH: + doOrderPatch(request, response, resource); + break; + case DavMethods.DAV_SUBSCRIBE: + doSubscribe(request, response, resource); + break; + case DavMethods.DAV_UNSUBSCRIBE: + doUnsubscribe(request, response, resource); + break; + case DavMethods.DAV_POLL: + doPoll(request, response, resource); + break; + case DavMethods.DAV_SEARCH: + doSearch(request, response, resource); + break; + case DavMethods.DAV_VERSION_CONTROL: + doVersionControl(request, response, resource); + break; + case DavMethods.DAV_LABEL: + doLabel(request, response, resource); + break; + case DavMethods.DAV_REPORT: + doReport(request, response, resource); + break; + case DavMethods.DAV_CHECKIN: + doCheckin(request, response, resource); + break; + case DavMethods.DAV_CHECKOUT: + doCheckout(request, response, resource); + break; + case DavMethods.DAV_UNCHECKOUT: + doUncheckout(request, response, resource); + break; + case DavMethods.DAV_MERGE: + doMerge(request, response, resource); + break; + case DavMethods.DAV_UPDATE: + doUpdate(request, response, resource); + break; + case DavMethods.DAV_MKWORKSPACE: + doMkWorkspace(request, response, resource); + break; + case DavMethods.DAV_MKACTIVITY: + doMkActivity(request, response, resource); + break; + case DavMethods.DAV_BASELINE_CONTROL: + doBaselineControl(request, response, resource); + break; + case DavMethods.DAV_ACL: + doAcl(request, response, resource); + break; + case DavMethods.DAV_REBIND: + doRebind(request, response, resource); + break; + case DavMethods.DAV_UNBIND: + doUnbind(request, response, resource); + break; + case DavMethods.DAV_BIND: + doBind(request, response, resource); + break; + default: + // any other method + return false; + } + return true; + } + + /** + * The OPTION method + * + * @param request + * @param response + * @param resource + */ + protected void doOptions(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + response.addHeader(DavConstants.HEADER_DAV, resource.getComplianceClass()); + response.addHeader("Allow", resource.getSupportedMethods()); + response.addHeader("MS-Author-Via", DavConstants.HEADER_DAV); + if (resource instanceof SearchResource) { + String[] langs = ((SearchResource) resource).getQueryGrammerSet().getQueryLanguages(); + for (String lang : langs) { + response.addHeader(SearchConstants.HEADER_DASL, "<" + lang + ">"); + } + } + // with DeltaV the OPTIONS request may contain a Xml body. + OptionsResponse oR = null; + OptionsInfo oInfo = request.getOptionsInfo(); + if (oInfo != null && resource instanceof DeltaVResource) { + oR = ((DeltaVResource) resource).getOptionResponse(oInfo); + } + if (oR == null) { + response.setStatus(DavServletResponse.SC_OK); + } else { + response.sendXmlResponse(oR, DavServletResponse.SC_OK); + } + } + + /** + * The HEAD method + * + * @param request + * @param response + * @param resource + * @throws IOException + */ + protected void doHead(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException { + spoolResource(request, response, resource, false); + } + + /** + * The GET method + * + * @param request + * @param response + * @param resource + * @throws IOException + */ + protected void doGet(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + spoolResource(request, response, resource, true); + } + + /** + * @param request + * @param response + * @param resource + * @param sendContent + * @throws IOException + */ + private void spoolResource(WebdavRequest request, WebdavResponse response, + DavResource resource, boolean sendContent) + throws IOException { + + if (!resource.exists()) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + long modSince = request.getDateHeader("If-Modified-Since"); + if (modSince > UNDEFINED_TIME) { + long modTime = resource.getModificationTime(); + // test if resource has been modified. note that formatted modification + // time lost the milli-second precision + if (modTime != UNDEFINED_TIME && (modTime / 1000 * 1000) <= modSince) { + // resource has not been modified since the time indicated in the + // 'If-Modified-Since' header. + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return; + } + } + + // spool resource properties and eventually resource content. + OutputStream out = (sendContent) ? response.getOutputStream() : null; + resource.spool(getOutputContext(response, out)); + response.flushBuffer(); + } + + /** + * The PROPFIND method + * + * @param request + * @param response + * @param resource + * @throws IOException + */ + protected void doPropFind(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + if (!resource.exists()) { + response.sendError(DavServletResponse.SC_NOT_FOUND); + return; + } + + int depth = request.getDepth(DEPTH_INFINITY); + DavPropertyNameSet requestProperties = request.getPropFindProperties(); + int propfindType = request.getPropFindType(); + + MultiStatus mstatus = new MultiStatus(); + mstatus.addResourceProperties(resource, requestProperties, propfindType, depth); + response.sendMultiStatus(mstatus); + } + + /** + * The PROPPATCH method + * + * @param request + * @param response + * @param resource + * @throws IOException + */ + protected void doPropPatch(WebdavRequest request, WebdavResponse response, + DavResource resource) + throws IOException, DavException { + + List changeList = request.getPropPatchChangeList(); + if (changeList.isEmpty()) { + response.sendError(DavServletResponse.SC_BAD_REQUEST); + return; + } + + MultiStatus ms = new MultiStatus(); + MultiStatusResponse msr = resource.alterProperties(changeList); + ms.addResponse(msr); + response.sendMultiStatus(ms); + } + + /** + * The POST method. Delegate to PUT + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doPost(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + /** + * The PUT method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doPut(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + DavResource parentResource = resource.getCollection(); + if (parentResource == null || !parentResource.exists()) { + // parent does not exist + response.sendError(DavServletResponse.SC_CONFLICT); + return; + } + + int status; + // test if resource already exists + if (resource.exists()) { + status = DavServletResponse.SC_NO_CONTENT; + } else { + status = DavServletResponse.SC_CREATED; + } + + parentResource.addMember(resource, getInputContext(request, request.getInputStream())); + response.setStatus(status); + } + + /** + * The MKCOL method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doMkCol(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + DavResource parentResource = resource.getCollection(); + if (parentResource == null || !parentResource.exists() || !parentResource.isCollection()) { + // parent does not exist or is not a collection + response.sendError(DavServletResponse.SC_CONFLICT); + return; + } + // shortcut: mkcol is only allowed on deleted/non-existing resources + if (resource.exists()) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + if (request.getContentLength() > 0 || request.getHeader("Transfer-Encoding") != null) { + parentResource.addMember(resource, getInputContext(request, request.getInputStream())); + } else { + parentResource.addMember(resource, getInputContext(request, null)); + } + response.setStatus(DavServletResponse.SC_CREATED); + } + + /** + * The DELETE method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doDelete(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + DavResource parent = resource.getCollection(); + if (parent != null) { + parent.removeMember(resource); + response.setStatus(DavServletResponse.SC_NO_CONTENT); + } else { + response.sendError(DavServletResponse.SC_FORBIDDEN, "Cannot remove the root resource."); + } + } + + /** + * The COPY method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doCopy(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + // only depth 0 and infinity is allowed + int depth = request.getDepth(DEPTH_INFINITY); + if (!(depth == DEPTH_0 || depth == DEPTH_INFINITY)) { + response.sendError(DavServletResponse.SC_BAD_REQUEST); + return; + } + + DavResource destResource = getResourceFactory().createResource(request.getDestinationLocator(), request, response); + int status = validateDestination(destResource, request, true); + if (status > DavServletResponse.SC_NO_CONTENT) { + response.sendError(status); + return; + } + + resource.copy(destResource, depth == DEPTH_0); + response.setStatus(status); + } + + /** + * The MOVE method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doMove(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + DavResource destResource = getResourceFactory().createResource(request.getDestinationLocator(), request, response); + int status = validateDestination(destResource, request, true); + if (status > DavServletResponse.SC_NO_CONTENT) { + response.sendError(status); + return; + } + + resource.move(destResource); + response.setStatus(status); + } + + /** + * The BIND method + * + * @param request + * @param response + * @param resource the collection resource to which a new member will be added + * @throws IOException + * @throws DavException + */ + protected void doBind(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + if (!resource.exists()) { + response.sendError(DavServletResponse.SC_NOT_FOUND); + } + BindInfo bindInfo = request.getBindInfo(); + DavResource oldBinding = getResourceFactory().createResource(request.getHrefLocator(bindInfo.getHref()), request, response); + if (!(oldBinding instanceof BindableResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + DavResource newBinding = getResourceFactory().createResource(request.getMemberLocator(bindInfo.getSegment()), request, response); + int status = validateDestination(newBinding, request, false); + if (status > DavServletResponse.SC_NO_CONTENT) { + response.sendError(status); + return; + } + ((BindableResource) oldBinding).bind(resource, newBinding); + response.setStatus(status); + } + + /** + * The REBIND method + * + * @param request + * @param response + * @param resource the collection resource to which a new member will be added + * @throws IOException + * @throws DavException + */ + protected void doRebind(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + if (!resource.exists()) { + response.sendError(DavServletResponse.SC_NOT_FOUND); + } + RebindInfo rebindInfo = request.getRebindInfo(); + DavResource oldBinding = getResourceFactory().createResource(request.getHrefLocator(rebindInfo.getHref()), request, response); + if (!(oldBinding instanceof BindableResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + DavResource newBinding = getResourceFactory().createResource(request.getMemberLocator(rebindInfo.getSegment()), request, response); + int status = validateDestination(newBinding, request, false); + if (status > DavServletResponse.SC_NO_CONTENT) { + response.sendError(status); + return; + } + ((BindableResource) oldBinding).rebind(resource, newBinding); + response.setStatus(status); + } + + /** + * The UNBIND method + * + * @param request + * @param response + * @param resource the collection resource from which a member will be removed + * @throws IOException + * @throws DavException + */ + protected void doUnbind(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + UnbindInfo unbindInfo = request.getUnbindInfo(); + DavResource srcResource = getResourceFactory().createResource(request.getMemberLocator(unbindInfo.getSegment()), request, response); + resource.removeMember(srcResource); + } + + /** + * Validate the given destination resource and return the proper status + * code: Any return value greater/equal than {@link DavServletResponse#SC_NO_CONTENT} + * indicates an error. + * + * @param destResource destination resource to be validated. + * @param request + * @param checkHeader flag indicating if the destination header must be present. + * @return status code indicating whether the destination is valid. + */ + protected int validateDestination(DavResource destResource, WebdavRequest request, boolean checkHeader) + throws DavException { + + if (checkHeader) { + String destHeader = request.getHeader(HEADER_DESTINATION); + if (destHeader == null || "".equals(destHeader)) { + return DavServletResponse.SC_BAD_REQUEST; + } + } + if (destResource.getLocator().equals(request.getRequestLocator())) { + return DavServletResponse.SC_FORBIDDEN; + } + + int status; + if (destResource.exists()) { + if (request.isOverwrite()) { + // matching if-header required for existing resources + if (!request.matchesIfHeader(destResource)) { + return DavServletResponse.SC_PRECONDITION_FAILED; + } else { + // overwrite existing resource + DavResource col; + try { + col = destResource.getCollection(); + } + catch (IllegalArgumentException ex) { + return DavServletResponse.SC_BAD_GATEWAY; + } + col.removeMember(destResource); + status = DavServletResponse.SC_NO_CONTENT; + } + } else { + // cannot copy/move to an existing item, if overwrite is not forced + return DavServletResponse.SC_PRECONDITION_FAILED; + } + } else { + // destination does not exist >> copy/move can be performed + status = DavServletResponse.SC_CREATED; + } + return status; + } + + /** + * The LOCK method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doLock(WebdavRequest request, WebdavResponse response, + DavResource resource) throws IOException, DavException { + + LockInfo lockInfo = request.getLockInfo(); + if (lockInfo.isRefreshLock()) { + // refresh any matching existing locks + ActiveLock[] activeLocks = resource.getLocks(); + List lList = new ArrayList(); + for (ActiveLock activeLock : activeLocks) { + // adjust lockinfo with type/scope retrieved from the lock. + lockInfo.setType(activeLock.getType()); + lockInfo.setScope(activeLock.getScope()); + + DavProperty etagProp = resource.getProperty(DavPropertyName.GETETAG); + String etag = etagProp != null ? String.valueOf(etagProp.getValue()) : ""; + if (request.matchesIfHeader(resource.getHref(), activeLock.getToken(), etag)) { + lList.add(resource.refreshLock(lockInfo, activeLock.getToken())); + } + } + if (lList.isEmpty()) { + throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); + } + ActiveLock[] refreshedLocks = lList.toArray(new ActiveLock[lList.size()]); + response.sendRefreshLockResponse(refreshedLocks); + } else { + int status = HttpServletResponse.SC_OK; + if (!resource.exists()) { + // lock-empty requires status code 201 (Created) + status = HttpServletResponse.SC_CREATED; + } + + // create a new lock + ActiveLock lock = resource.lock(lockInfo); + + CodedUrlHeader header = new CodedUrlHeader( + DavConstants.HEADER_LOCK_TOKEN, lock.getToken()); + response.setHeader(header.getHeaderName(), header.getHeaderValue()); + + DavPropertySet propSet = new DavPropertySet(); + propSet.add(new LockDiscovery(lock)); + response.sendXmlResponse(propSet, status); + } + } + + /** + * The UNLOCK method + * + * @param request + * @param response + * @param resource + * @throws DavException + */ + protected void doUnlock(WebdavRequest request, WebdavResponse response, + DavResource resource) throws DavException { + // get lock token from header + String lockToken = request.getLockToken(); + TransactionInfo tInfo = request.getTransactionInfo(); + if (tInfo != null) { + ((TransactionResource) resource).unlock(lockToken, tInfo); + } else { + resource.unlock(lockToken); + } + response.setStatus(DavServletResponse.SC_NO_CONTENT); + } + + /** + * The ORDERPATCH method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doOrderPatch(WebdavRequest request, + WebdavResponse response, + DavResource resource) + throws IOException, DavException { + + if (!(resource instanceof OrderingResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + OrderPatch op = request.getOrderPatch(); + if (op == null) { + response.sendError(DavServletResponse.SC_BAD_REQUEST); + return; + } + // perform reordering of internal members + ((OrderingResource) resource).orderMembers(op); + response.setStatus(DavServletResponse.SC_OK); + } + + /** + * The SUBSCRIBE method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doSubscribe(WebdavRequest request, + WebdavResponse response, + DavResource resource) + throws IOException, DavException { + + if (!(resource instanceof ObservationResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + SubscriptionInfo info = request.getSubscriptionInfo(); + if (info == null) { + response.sendError(DavServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + return; + } + Subscription subs = ((ObservationResource) resource).subscribe(info, request.getSubscriptionId()); + response.sendSubscriptionResponse(subs); + } + + /** + * The UNSUBSCRIBE method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doUnsubscribe(WebdavRequest request, + WebdavResponse response, + DavResource resource) + throws IOException, DavException { + + if (!(resource instanceof ObservationResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + ((ObservationResource) resource).unsubscribe(request.getSubscriptionId()); + response.setStatus(DavServletResponse.SC_NO_CONTENT); + } + + /** + * The POLL method + * + * @param request + * @param response + * @param resource + * @throws IOException + * @throws DavException + */ + protected void doPoll(WebdavRequest request, + WebdavResponse response, + DavResource resource) + throws IOException, DavException { + + if (!(resource instanceof ObservationResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + EventDiscovery ed = ((ObservationResource) resource).poll( + request.getSubscriptionId(), request.getPollTimeout()); + response.sendPollResponse(ed); + } + + /** + * The VERSION-CONTROL method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doVersionControl(WebdavRequest request, WebdavResponse response, + DavResource resource) + throws DavException, IOException { + if (!(resource instanceof VersionableResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + ((VersionableResource) resource).addVersionControl(); + } + + /** + * The LABEL method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doLabel(WebdavRequest request, WebdavResponse response, + DavResource resource) + throws DavException, IOException { + + LabelInfo labelInfo = request.getLabelInfo(); + if (resource instanceof VersionResource) { + ((VersionResource) resource).label(labelInfo); + } else if (resource instanceof VersionControlledResource) { + ((VersionControlledResource) resource).label(labelInfo); + } else { + // any other resource type that does not support a LABEL request + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + } + + /** + * The REPORT method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doReport(WebdavRequest request, WebdavResponse response, + DavResource resource) + throws DavException, IOException { + ReportInfo info = request.getReportInfo(); + Report report; + if (resource instanceof DeltaVResource) { + report = ((DeltaVResource) resource).getReport(info); + } else if (resource instanceof AclResource) { + report = ((AclResource) resource).getReport(info); + } else { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + int statusCode = (report.isMultiStatusReport()) ? DavServletResponse.SC_MULTI_STATUS : DavServletResponse.SC_OK; + response.sendXmlResponse(report, statusCode); + } + + /** + * The CHECKIN method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doCheckin(WebdavRequest request, WebdavResponse response, + DavResource resource) + throws DavException, IOException { + + if (!(resource instanceof VersionControlledResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + String versionHref = ((VersionControlledResource) resource).checkin(); + response.setHeader(DeltaVConstants.HEADER_LOCATION, versionHref); + response.setStatus(DavServletResponse.SC_CREATED); + } + + /** + * The CHECKOUT method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doCheckout(WebdavRequest request, WebdavResponse response, + DavResource resource) + throws DavException, IOException { + if (!(resource instanceof VersionControlledResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + ((VersionControlledResource) resource).checkout(); + } + + /** + * The UNCHECKOUT method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doUncheckout(WebdavRequest request, WebdavResponse response, + DavResource resource) + throws DavException, IOException { + if (!(resource instanceof VersionControlledResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + ((VersionControlledResource) resource).uncheckout(); + } + + /** + * The MERGE method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doMerge(WebdavRequest request, WebdavResponse response, + DavResource resource) throws DavException, IOException { + + if (!(resource instanceof VersionControlledResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + MergeInfo info = request.getMergeInfo(); + MultiStatus ms = ((VersionControlledResource) resource).merge(info); + response.sendMultiStatus(ms); + } + + /** + * The UPDATE method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doUpdate(WebdavRequest request, WebdavResponse response, + DavResource resource) throws DavException, IOException { + + if (!(resource instanceof VersionControlledResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + UpdateInfo info = request.getUpdateInfo(); + MultiStatus ms = ((VersionControlledResource) resource).update(info); + response.sendMultiStatus(ms); + } + + /** + * The MKWORKSPACE method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doMkWorkspace(WebdavRequest request, WebdavResponse response, + DavResource resource) throws DavException, IOException { + if (resource.exists()) { + AbstractWebdavServlet.log.warn("Cannot create a new workspace. Resource already exists."); + response.sendError(DavServletResponse.SC_FORBIDDEN); + return; + } + + DavResource parentResource = resource.getCollection(); + if (parentResource == null || !parentResource.exists() || !parentResource.isCollection()) { + // parent does not exist or is not a collection + response.sendError(DavServletResponse.SC_CONFLICT); + return; + } + if (!(parentResource instanceof DeltaVResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + ((DeltaVResource) parentResource).addWorkspace(resource); + response.setStatus(DavServletResponse.SC_CREATED); + } + + /** + * The MKACTIVITY method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doMkActivity(WebdavRequest request, WebdavResponse response, + DavResource resource) throws DavException, IOException { + if (resource.exists()) { + AbstractWebdavServlet.log.warn("Unable to create activity: A resource already exists at the request-URL " + request.getRequestURL()); + response.sendError(DavServletResponse.SC_FORBIDDEN); + return; + } + + DavResource parentResource = resource.getCollection(); + if (parentResource == null || !parentResource.exists() || !parentResource.isCollection()) { + // parent does not exist or is not a collection + response.sendError(DavServletResponse.SC_CONFLICT); + return; + } + // TODO: improve. see http://issues.apache.org/jira/browse/JCR-394 + if (!parentResource.getComplianceClass().contains(DavCompliance.ACTIVITY)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + if (!(resource instanceof ActivityResource)) { + AbstractWebdavServlet.log.error("Unable to create activity: ActivityResource expected"); + response.sendError(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + // try to add the new activity resource + parentResource.addMember(resource, getInputContext(request, request.getInputStream())); + + // Note: mandatory cache control header has already been set upon response creation. + response.setStatus(DavServletResponse.SC_CREATED); + } + + /** + * The BASELINECONTROL method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doBaselineControl(WebdavRequest request, WebdavResponse response, + DavResource resource) + throws DavException, IOException { + + if (!resource.exists()) { + AbstractWebdavServlet.log.warn("Unable to add baseline control. Resource does not exist " + resource.getHref()); + response.sendError(DavServletResponse.SC_NOT_FOUND); + return; + } + // TODO: improve. see http://issues.apache.org/jira/browse/JCR-394 + if (!(resource instanceof VersionControlledResource) || !resource.isCollection()) { + AbstractWebdavServlet.log.warn("BaselineControl is not supported by resource " + resource.getHref()); + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + + // TODO : missing method on VersionControlledResource + throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED); + /* + ((VersionControlledResource) resource).addBaselineControl(request.getRequestDocument()); + // Note: mandatory cache control header has already been set upon response creation. + response.setStatus(DavServletResponse.SC_OK); + */ + } + + /** + * The SEARCH method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doSearch(WebdavRequest request, WebdavResponse response, + DavResource resource) throws DavException, IOException { + + if (!(resource instanceof SearchResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + Document doc = request.getRequestDocument(); + if (doc != null) { + SearchInfo sR = SearchInfo.createFromXml(doc.getDocumentElement()); + response.sendMultiStatus(((SearchResource) resource).search(sR)); + } else { + // request without request body is valid if requested resource + // is a 'query' resource. + response.sendMultiStatus(((SearchResource) resource).search(null)); + } + } + + /** + * The ACL method + * + * @param request + * @param response + * @param resource + * @throws DavException + * @throws IOException + */ + protected void doAcl(WebdavRequest request, WebdavResponse response, + DavResource resource) throws DavException, IOException { + if (!(resource instanceof AclResource)) { + response.sendError(DavServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + Document doc = request.getRequestDocument(); + if (doc == null) { + throw new DavException(DavServletResponse.SC_BAD_REQUEST, "ACL request requires a DAV:acl body."); + } + AclProperty acl = AclProperty.createFromXml(doc.getDocumentElement()); + ((AclResource)resource).alterAcl(acl); + } + + /** + * Return a new InputContext used for adding resource members + * + * @param request + * @param in + * @return + * @see #spoolResource(WebdavRequest, WebdavResponse, DavResource, boolean) + */ + protected InputContext getInputContext(DavServletRequest request, InputStream in) { + return new InputContextImpl(request, in); + } + + /** + * Return a new OutputContext used for spooling resource properties and + * the resource content + * + * @param response + * @param out + * @return + * @see #doPut(WebdavRequest, WebdavResponse, DavResource) + * @see #doMkCol(WebdavRequest, WebdavResponse, DavResource) + */ + protected OutputContext getOutputContext(DavServletResponse response, OutputStream out) { + return new OutputContextImpl(response, out); + } + + private List getContentCodings(HttpServletRequest request) { + List result = Collections.emptyList(); + for (@SuppressWarnings("unchecked") + Enumeration ceh = request.getHeaders("Content-Encoding"); ceh.hasMoreElements();) { + for (String h : ceh.nextElement().split(",")) { + if (!h.trim().isEmpty()) { + if (result.isEmpty()) { + result = new ArrayList(); + } + result.add(h.trim()); + } + } + } + + return result; + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/server/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/server/package-info.java new file mode 100644 index 00000000000..2fc014fe59c --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/server/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.server; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionConstants.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionConstants.java new file mode 100644 index 00000000000..8730e21e160 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionConstants.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.webdav.transaction; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * TransactionConstants interface provide constants for request + * and response headers, Xml elements and property names used for handling + * transactions over WebDAV. There exists no public standard for this functionality. + * + * todo: 'local' and 'global' are not accurate terms in the given context > replace + */ +public interface TransactionConstants { + + /** + * Namespace for transaction related xml elements + */ + public static final Namespace NAMESPACE = Namespace.getNamespace("dcr", "http://www.day.com/jcr/webdav/1.0"); + + //---< Headers >------------------------------------------------------------ + /** + * TransactionId Header + */ + public static final String HEADER_TRANSACTIONID = "TransactionId"; + + //---< XML Element, Attribute Names >--------------------------------------- + /** + * transaction XML element
        + * Used as element inside the {@link DavConstants#XML_LOCKTYPE locktype} + * element. + * @see DavConstants#XML_LOCKTYPE + */ + public static final String XML_TRANSACTION = "transaction"; + + /** + * global XML element
        + * Used as element inside of the {@link DavConstants#XML_LOCKSCOPE lockscope} element. + * + * @see DavConstants#XML_LOCKSCOPE + */ + public static final String XML_GLOBAL = "global"; + + /** + * local XML element
        + * Used as element inside of the {@link DavConstants#XML_LOCKSCOPE lockscope} element. + * It indicates the transaction to be local (e.g. transient changes to + * a repository). + * + * @see DavConstants#XML_LOCKSCOPE + */ + public static final String XML_LOCAL = "local"; + + /** + * transactioninfo XML element
        + * Mandatory element of the UNLOCK request body, if the unlock request + * is intended to complete a transaction. + */ + public static final String XML_TRANSACTIONINFO = "transactioninfo"; + + /** + * transactionstatus XML element
        + * Mandatory element inside the {@link #XML_TRANSACTIONINFO transactioninfo} + * element indicating how the transaction should be completed. + * @see #XML_TRANSACTIONINFO + */ + public static final String XML_TRANSACTIONSTATUS = "transactionstatus"; + + /** + * commit XML element
        + * Used as element inside of the {@link #XML_TRANSACTIONSTATUS transactionstatus} + * element. It indicates a completion by committing the transaction. + * @see #XML_TRANSACTIONSTATUS + */ + public static final String XML_COMMIT = "commit"; + + /** + * rollback XML element
        + * Used as element inside of the {@link #XML_TRANSACTIONSTATUS transactionstatus} + * element. It indicates a completion by roll backing the transaction. + * @see #XML_TRANSACTIONSTATUS + */ + public static final String XML_ROLLBACK = "rollback"; + + //---< Lock Type, Lock Scope >---------------------------------------------- + /** + * "transaction" lock type constant. + * @see #XML_TRANSACTION + * @see Type#create(String, Namespace) + */ + public static final Type TRANSACTION = Type.create(XML_TRANSACTION, TransactionConstants.NAMESPACE); + + /** + * "local" lock scope constant. + * + * @see #XML_LOCAL + * @see Scope#create(String, Namespace) + */ + public static final Scope LOCAL = Scope.create(XML_LOCAL, TransactionConstants.NAMESPACE); + + /** + * "global" lock scope constant. + * + * @see #XML_GLOBAL + * @see Scope#create(String, Namespace) + */ + public static final Scope GLOBAL = Scope.create(XML_GLOBAL, TransactionConstants.NAMESPACE); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionDavServletRequest.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionDavServletRequest.java new file mode 100644 index 00000000000..1002e5b9094 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionDavServletRequest.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.webdav.transaction; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletRequest; + +/** + * TransactionDavServletRequest provides extensions to the + * {@link DavServletRequest} interface used for dealing with transaction lock + * requests. + */ +public interface TransactionDavServletRequest extends DavServletRequest { + + /** + * Retrieve the 'transactioninfo' request body that must be included with + * the UNLOCK request of a transaction lock. If the request body is does not + * provide the information required (either because it is missing or the + * Xml is not valid) null is returned. + * + * @return TransactionInfo object encapsulating the 'transactioninfo' + * Xml element present in the request body or null if no + * body is present or if it could not be parsed. + * @throws DavException if an invalid request body is present. + */ + public TransactionInfo getTransactionInfo() throws DavException; + + + /** + * Retrieve the transaction id from the + * {@link TransactionConstants#HEADER_TRANSACTIONID TransactionId header}. + * + * @return transaction id as present in the {@link TransactionConstants#HEADER_TRANSACTIONID TransactionId header} + * or null. + */ + public String getTransactionId(); +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionInfo.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionInfo.java new file mode 100644 index 00000000000..10d0d8d5a7a --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionInfo.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.webdav.transaction; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * TransactionInfo class encapsulates the information present + * in the {@link #XML_TRANSACTIONINFO} element that forms the request body of + * the UNLOCk request for a transaction lock. + * + * @see TransactionConstants#XML_TRANSACTIONINFO + * @see TransactionConstants#XML_TRANSACTION + */ +public class TransactionInfo implements TransactionConstants, XmlSerializable { + + private static Logger log = LoggerFactory.getLogger(TransactionInfo.class); + + private final boolean isCommit; + + /** + * Creates a TransactionInfo object + * + * @param isCommit + */ + public TransactionInfo(boolean isCommit) { + this.isCommit = isCommit; + } + + /** + * Creates a TransactionInfo object from the given 'transactionInfo' + * element. The 'transactionInfo' must have the following form: + *

        +     *
        +     *  <!ELEMENT transactioninfo (transactionstatus) >
        +     *  <!ELEMENT transactionstatus ( commit | rollback ) >
        +     *  <!ELEMENT commit EMPTY >
        +     *  <!ELEMENT rollback EMPTY >
        +     * 
        + * @param transactionInfo as present in the UNLOCK request body. + * @throws IllegalArgumentException if the given transactionInfo element + * is not valid. + */ + public TransactionInfo(Element transactionInfo) throws DavException { + if (transactionInfo == null || !XML_TRANSACTIONINFO.equals(transactionInfo.getLocalName())) { + log.warn("'transactionInfo' element expected."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + Element txStatus = DomUtil.getChildElement(transactionInfo, XML_TRANSACTIONSTATUS, NAMESPACE); + if (txStatus != null) { + // retrieve status: commit or rollback + isCommit = DomUtil.hasChildElement(txStatus, XML_COMMIT, NAMESPACE); + } else { + log.warn("transactionInfo must contain a single 'transactionstatus' element."); + throw new DavException(DavServletResponse.SC_BAD_REQUEST); + } + } + + /** + * Returns true, if this info requires a 'commit' action, false otherwise + * (i.e. 'rollback' is requested). + * + * @return true if a 'commit' element was present. false otherwise. + * @see #XML_COMMIT + * @see #XML_ROLLBACK + */ + public boolean isCommit() { + return isCommit; + } + + //------------------------------------------< XmlSerializable interface >--- + /** + * @see org.apache.jackrabbit.webdav.xml.XmlSerializable#toXml(Document) + * @param document + */ + public Element toXml(Document document) { + Element elem = DomUtil.createElement(document, XML_TRANSACTIONINFO, NAMESPACE); + Element st = DomUtil.addChildElement(elem, XML_TRANSACTIONSTATUS, NAMESPACE); + String lName = (isCommit) ? XML_COMMIT : XML_ROLLBACK; + DomUtil.addChildElement(st, lName, NAMESPACE); + return elem; + } + +} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionResource.java similarity index 77% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionResource.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionResource.java index 6d2f54ac7ad..49c89d9c646 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TransactionResource.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TransactionResource.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -17,7 +18,6 @@ import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; -import org.apache.jackrabbit.webdav.DavSession; /** * TransactionResource extends the {@link DavResource} interface by @@ -25,7 +25,6 @@ */ public interface TransactionResource extends DavResource { - public static final String COMPLIANCE_CLASS = ""; public static final String METHODS = ""; /** @@ -36,13 +35,6 @@ public interface TransactionResource extends DavResource { */ public void init(TxLockManager txMgr, String transactionId); - /** - * Retrieve the DavSession associated with this resource. - * - * @return session object associated with this resource. - */ - public DavSession getSession(); - /** * The TransactionId or null according to the value of the * corresponding request {@link TransactionConstants#HEADER_TRANSACTIONID header} diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxActiveLock.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxActiveLock.java similarity index 85% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxActiveLock.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxActiveLock.java index 40673ffc17d..e977ede0122 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/transaction/TxActiveLock.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxActiveLock.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -17,8 +18,8 @@ import org.apache.jackrabbit.webdav.lock.DefaultActiveLock; import org.apache.jackrabbit.webdav.lock.LockInfo; -import org.apache.jackrabbit.webdav.lock.Type; import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; /** * TxActiveLock represents the transaction lock present on a @@ -65,6 +66,7 @@ public TxActiveLock(LockInfo lockInfo) { * * @return true */ + @Override public boolean isDeep() { return true; } @@ -74,6 +76,7 @@ public boolean isDeep() { * * @return {@link #TRANSACTION} */ + @Override public Type getType() { return TRANSACTION; } @@ -83,7 +86,8 @@ public Type getType() { * * @return {@link #LOCAL} or {@link #GLOBAL} */ + @Override public Scope getScope() { return scope; } -} \ No newline at end of file +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxLockEntry.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxLockEntry.java new file mode 100644 index 00000000000..1525be10718 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxLockEntry.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.webdav.transaction; + +import org.apache.jackrabbit.webdav.lock.AbstractLockEntry; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TxLockEntry represents the lock entry objects allowed for + * a transaction lock. + */ +public final class TxLockEntry extends AbstractLockEntry implements TransactionConstants { + + private static Logger log = LoggerFactory.getLogger(TxLockEntry.class); + + private final Scope scope; + + /** + * Create a lock entry that identifies transaction lock. + * + * @param isLocal boolean value indicating whether this is a local or a global + * lock entry. + */ + public TxLockEntry(boolean isLocal) { + if (isLocal) { + scope = LOCAL; + } else { + scope = GLOBAL; + } + } + + /** + * Returns the {@link #TRANSACTION 'transaction'} lock type. + * + * @return always returns the 'transaction' type. + * @see org.apache.jackrabbit.webdav.lock.LockEntry#getType() + * @see #TRANSACTION + */ + public Type getType() { + return TRANSACTION; + } + + /** + * Returns either {@link #LOCAL local} or {@link #GLOBAL global} scope + * depending on the initial constructor value. + * + * @return returns 'global' or 'local' scope. + * @see org.apache.jackrabbit.webdav.lock.LockEntry#getScope() + * @see #GLOBAL + * @see #LOCAL + */ + public Scope getScope() { + return scope; + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxLockManager.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxLockManager.java new file mode 100644 index 00000000000..8b3b7049142 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/TxLockManager.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.webdav.transaction; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.lock.ActiveLock; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.apache.jackrabbit.webdav.lock.Scope; +import org.apache.jackrabbit.webdav.lock.Type; + +/** + * TxLockManager manages locks with locktype + * '{@link TransactionConstants#TRANSACTION dcr:transaction}'. + * + * todo: removing all expired locks + * todo: 'local' and 'global' are not accurate terms in the given context > replace + * todo: the usage of the 'global' transaction is not according to the JTA specification, + * which explicitly requires any transaction present on a servlet to be completed before + * the service method returns. Starting/completing transactions on the session object, + * which is possible with the jackrabbit implementation is a hack. + * todo: review of this transaction part is therefore required. Is there a use-case + * for those 'global' transactions at all... + */ +public interface TxLockManager extends LockManager { + + + /** + * Release the lock identified by the given lock token. + * + * @param lockInfo + * @param lockToken + * @param resource + * @throws org.apache.jackrabbit.webdav.DavException + */ + public void releaseLock(TransactionInfo lockInfo, String lockToken, + TransactionResource resource) throws DavException; + + + /** + * Return the lock applied to the given resource or null + * + * @param type + * @param scope + * @param resource + * @return lock applied to the given resource or null + * @see org.apache.jackrabbit.webdav.lock.LockManager#getLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope, org.apache.jackrabbit.webdav.DavResource) + */ + public ActiveLock getLock(Type type, Scope scope, TransactionResource resource); + + +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/package-info.java new file mode 100644 index 00000000000..fdbd51e358b --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/transaction/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.0") +package org.apache.jackrabbit.webdav.transaction; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/CSRFUtil.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/CSRFUtil.java new file mode 100644 index 00000000000..25b2ae5fd2c --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/CSRFUtil.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.webdav.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * CSRFUtil... + */ +public class CSRFUtil { + + /** + * Constant used to + */ + public static final String DISABLED = "disabled"; + + /** + * Request content types for CSRF checking, see JCR-3909, JCR-4002, and JCR-4009 + */ + public static final Set CONTENT_TYPES = Collections.unmodifiableSet(new HashSet( + Arrays.asList( + new String[] { + "application/x-www-form-urlencoded", + "multipart/form-data", + "text/plain" + } + ) + )); + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(CSRFUtil.class); + + /** + * Disable referrer based CSRF protection + */ + private final boolean disabled; + + /** + * Additional allowed referrer hosts for CSRF protection + */ + private final Set allowedReferrerHosts; + + /** + * Creates a new instance from the specified configuration, which defines + * the behaviour of the referrer based CSRF protection as follows: + *
          + *
        1. If config is null or empty string the default + * behaviour is to allow only requests with an empty referrer header or a + * referrer host equal to the server host
        2. + *
        3. A comma separated list of additional allowed referrer hosts which are + * valid in addition to default behaviour (see above).
        4. + *
        5. The value {@link #DISABLED} may be used to disable the referrer checking altogether
        6. + *
        + * + * @param config The configuration value which may be any of the following: + *
          + *
        • null or empty string for the default behaviour, which + * only allows requests with an empty referrer header or a + * referrer host equal to the server host
        • + *
        • A comma separated list of additional allowed referrer hosts which are + * valid in addition to default behaviour (see above).
        • + *
        • {@link #DISABLED} in order to disable the referrer checking altogether
        • + *
        + */ + public CSRFUtil(String config) { + if (config == null || config.length() == 0) { + disabled = false; + allowedReferrerHosts = Collections.emptySet(); + log.debug("CSRF protection disabled"); + } else { + if (DISABLED.equalsIgnoreCase(config.trim())) { + disabled = true; + allowedReferrerHosts = Collections.emptySet(); + } else { + disabled = false; + String[] allowed = config.split(","); + allowedReferrerHosts = new HashSet(allowed.length); + for (String entry : allowed) { + allowedReferrerHosts.add(entry.trim()); + } + } + log.debug("CSRF protection enabled, allowed referrers: " + allowedReferrerHosts); + } + } + + public boolean isValidRequest(HttpServletRequest request) { + + if (disabled) { + return true; + } else if (!"POST".equals(request.getMethod())) { + // protection only needed for POST + return true; + } else { + Enumeration cts = (Enumeration) request.getHeaders("Content-Type"); + String ct = null; + if (cts != null && cts.hasMoreElements()) { + String t = cts.nextElement(); + // prune parameters + int semicolon = t.indexOf(';'); + if (semicolon >= 0) { + t = t.substring(0, semicolon); + } + ct = t.trim().toLowerCase(Locale.ENGLISH); + } + if (cts != null && cts.hasMoreElements()) { + // reject if there are more header field instances + log.debug("request blocked because there were multiple content-type header fields"); + return false; + } + if (ct != null && !CONTENT_TYPES.contains(ct)) { + // type present and not in blacklist + return true; + } + + String refHeader = request.getHeader("Referer"); + // empty referrer headers are not allowed for POST + relevant + // content types (see JCR-3909) + if (refHeader == null) { + log.debug("POST with content type " + ct + " blocked due to missing referer header field"); + return false; + } + + try { + String host = new URI(refHeader).getHost(); + // test referrer-host equals server or + // if it is contained in the set of explicitly allowed host + // names + boolean ok = host == null || host.equals(request.getServerName()) || allowedReferrerHosts.contains(host); + if (!ok) { + log.debug("POST with content type " + ct + " blocked due to referer header field being: " + refHeader); + } + return ok; + } catch (URISyntaxException ex) { + // referrer malformed -> block access + log.debug("POST with content type " + ct + " blocked due to malformed referer header field: " + refHeader); + return false; + } + } + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/EncodeUtil.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/EncodeUtil.java new file mode 100644 index 00000000000..5cb73b5ad93 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/EncodeUtil.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.webdav.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.BitSet; + +/** + * EncodeUtil provides helper methods for URL encoding and decoding + * (copied from jcr-commons jackrabbit.util.Text). + * + * @see JCR-2897. + */ +public final class EncodeUtil { + + /** + * logger instance + */ + private static final Logger log = LoggerFactory.getLogger(EncodeUtil.class); + + /** + * hextable used for {@link #escape(String, char, boolean)} + */ + public static final char[] hexTable = "0123456789abcdef".toCharArray(); + + /** + * The list of characters that are not encoded by the escape() + * and unescape() METHODS. They contains the characters as + * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax': + *

        + *

        +     * unreserved  = alphanum | mark
        +     * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
        +     * 
        + */ + private static BitSet URISave; + + /** + * Same as {@link #URISave} but also contains the '/' + */ + private static BitSet URISaveEx; + + static { + URISave = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + URISave.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + URISave.set(i); + } + for (i = '0'; i <= '9'; i++) { + URISave.set(i); + } + URISave.set('-'); + URISave.set('_'); + URISave.set('.'); + URISave.set('!'); + URISave.set('~'); + URISave.set('*'); + URISave.set('\''); + URISave.set('('); + URISave.set(')'); + + URISaveEx = (BitSet) URISave.clone(); + URISaveEx.set('/'); + } + + /** + * Does a URL encoding of the string. The characters that + * don't need encoding are those defined 'unreserved' in section 2.3 of + * the 'URI generic syntax' RFC 2396. + * + * @param string the string to encode + * @return the escaped string + * @throws NullPointerException if string is null. + */ + public static String escape(String string) { + return escape(string, '%', false); + } + + /** + * Does a URL encoding of the path. The characters that + * don't need encoding are those defined 'unreserved' in section 2.3 of + * the 'URI generic syntax' RFC 2396. In contrast to the + * {@link #escape(String)} method, not the entire path string is escaped, + * but every individual part (i.e. the slashes are not escaped). + * + * @param path the path to encode + * @return the escaped path + * @throws NullPointerException if path is null. + */ + public static String escapePath(String path) { + return escape(path, '%', true); + } + + /** + * Does an URL encoding of the string using the + * escape character. The characters that don't need encoding + * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' + * RFC 2396, but without the escape character. If isPath is + * true, additionally the slash '/' is ignored, too. + * + * @param string the string to encode. + * @param escape the escape character. + * @param isPath if true, the string is treated as path + * @return the escaped string + * @throws NullPointerException if string is null. + */ + private static String escape(String string, char escape, boolean isPath) { + BitSet validChars = isPath ? URISaveEx : URISave; + byte[] bytes = string.getBytes(StandardCharsets.UTF_8); + StringBuffer out = new StringBuffer(bytes.length); + for (byte aByte : bytes) { + int c = aByte & 0xff; + if (validChars.get(c) && c != escape) { + out.append((char) c); + } else { + out.append(escape); + out.append(hexTable[(c >> 4) & 0x0f]); + out.append(hexTable[(c) & 0x0f]); + } + } + return out.toString(); + } + + /** + * Does a URL decoding of the string. Please note that in + * opposite to the {@link java.net.URLDecoder} it does not transform the + + * into spaces. + * + * @param string the string to decode + * @return the decoded string + * @throws NullPointerException if string is null. + * @throws ArrayIndexOutOfBoundsException if not enough character follow an + * escape character + * @throws IllegalArgumentException if the 2 characters following the escape + * character do not represent a hex-number. + */ + public static String unescape(String string) { + return unescape(string, '%'); + } + + /** + * Does a URL decoding of the string using the + * escape character. Please note that in opposite to the + * {@link java.net.URLDecoder} it does not transform the + into spaces. + * + * @param string the string to decode + * @param escape the escape character + * @return the decoded string + * @throws NullPointerException if string is null. + * @throws IllegalArgumentException if the 2 characters following the escape + * character do not represent a hex-number + * or if not enough characters follow an + * escape character + */ + private static String unescape(String string, char escape) { + byte[] utf8 = string.getBytes(StandardCharsets.UTF_8); + + // Check whether escape occurs at invalid position + if ((utf8.length >= 1 && utf8[utf8.length - 1] == escape) || + (utf8.length >= 2 && utf8[utf8.length - 2] == escape)) { + throw new IllegalArgumentException("Premature end of escape sequence at end of input"); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(utf8.length); + for (int k = 0; k < utf8.length; k++) { + byte b = utf8[k]; + if (b == escape) { + out.write((decodeDigit(utf8[++k]) << 4) + decodeDigit(utf8[++k])); + } + else { + out.write(b); + } + } + + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + + private static byte decodeDigit(byte b) { + if (b >= 0x30 && b <= 0x39) { + return (byte) (b - 0x30); + } + else if (b >= 0x41 && b <= 0x46) { + return (byte) (b - 0x37); + } + else if (b >= 0x61 && b <= 0x66) { + return (byte) (b - 0x57); + } + else { + throw new IllegalArgumentException("Escape sequence is not hexadecimal: " + (char)b); + } + } + + /** + * Private constructor: avoid instantiation. + */ + private EncodeUtil() { + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/HttpDateFormat.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/HttpDateFormat.java new file mode 100644 index 00000000000..45605753a06 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/HttpDateFormat.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.webdav.util; + +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; + +/** + * HttpDateFormat... + */ +public class HttpDateFormat extends SimpleDateFormat { + + private static final TimeZone GMT_TIMEZONE = TimeZone.getTimeZone("GMT"); + + /** + * Pattern for the modification date as defined by RFC 1123 + */ + public static final String MODIFICATION_DATE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + + /** + * Simple date format pattern for the creation date ISO representation (partial). + */ + public static final String CREATION_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + public HttpDateFormat(String pattern) { + super(pattern, Locale.ENGLISH); + super.setTimeZone(GMT_TIMEZONE); + } + + /** + * Creates a new HttpDateFormat using the + * {@link #MODIFICATION_DATE_PATTERN modifcation date pattern}. + * + * @return a new HttpDateFormat. + */ + public static HttpDateFormat modificationDateFormat() { + return new HttpDateFormat(MODIFICATION_DATE_PATTERN); + } + + /** + * Creates a new HttpDateFormat using the + * {@link #CREATION_DATE_PATTERN creation date pattern}. + * + * @return a new HttpDateFormat. + */ + public static HttpDateFormat creationDateFormat() { + return new HttpDateFormat(CREATION_DATE_PATTERN); + } +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/LinkHeaderFieldParser.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/LinkHeaderFieldParser.java new file mode 100644 index 00000000000..bc183959219 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/LinkHeaderFieldParser.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.webdav.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicHeaderValueParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple parser for HTTP Link header fields, as defined in RFC 5988. + */ +public class LinkHeaderFieldParser { + + /** + * the default logger + */ + private static Logger log = LoggerFactory.getLogger(LinkHeaderFieldParser.class); + + private final List relations; + + public LinkHeaderFieldParser(List fieldValues) { + List tmp = new ArrayList(); + if (fieldValues != null) { + for (String value : fieldValues) { + addFields(tmp, value); + } + } + relations = Collections.unmodifiableList(tmp); + } + + public LinkHeaderFieldParser(Enumeration en) { + if (en != null && en.hasMoreElements()) { + List tmp = new ArrayList(); + + while (en.hasMoreElements()) { + addFields(tmp, en.nextElement().toString()); + } + relations = Collections.unmodifiableList(tmp); + } else { + // optimize case of no Link headers + relations = Collections.emptyList(); + } + } + + public String getFirstTargetForRelation(String relationType) { + + for (LinkRelation lr : relations) { + + String relationNames = lr.getParameters().get("rel"); + if (relationNames != null) { + + // split rel value on whitespace + for (String rn : relationNames.toLowerCase(Locale.ENGLISH) + .split("\\s")) { + if (relationType.equals(rn)) { + return lr.getTarget(); + } + } + } + } + + return null; + } + + // A single header field instance can contain multiple, comma-separated + // fields. + private void addFields(List l, String fieldValue) { + + boolean insideAngleBrackets = false; + boolean insideDoubleQuotes = false; + + for (int i = 0; i < fieldValue.length(); i++) { + + char c = fieldValue.charAt(i); + + if (insideAngleBrackets) { + insideAngleBrackets = c != '>'; + } else if (insideDoubleQuotes) { + insideDoubleQuotes = c != '"'; + if (c == '\\' && i < fieldValue.length() - 1) { + // skip over next character + c = fieldValue.charAt(++i); + } + } else { + insideAngleBrackets = c == '<'; + insideDoubleQuotes = c == '"'; + + if (c == ',') { + String v = fieldValue.substring(0, i); + if (v.length() > 0) { + try { + l.add(new LinkRelation(v)); + } catch (Exception ex) { + log.warn("parse error in Link Header field value", + ex); + } + } + addFields(l, fieldValue.substring(i + 1)); + return; + } + } + } + + if (fieldValue.length() > 0) { + try { + l.add(new LinkRelation(fieldValue)); + } catch (Exception ex) { + log.warn("parse error in Link Header field value", ex); + } + } + } + + private static class LinkRelation { + + private static Pattern P = Pattern.compile("\\s*<(.*)>\\s*(.*)"); + + private String target; + private Map parameters; + + /** + * Parses a single link relation, consisting of and optional + * parameters. + * + * @param field + * field value + * @throws Exception + */ + public LinkRelation(String field) throws Exception { + + // find the link target using a regexp + Matcher m = P.matcher(field); + if (!m.matches()) { + throw new Exception("illegal Link header field value:" + field); + } + + target = m.group(1); + + // pass the remainder to the generic parameter parser + NameValuePair[] params = BasicHeaderValueParser.parseParameters(m.group(2), null); + + if (params.length == 0) { + parameters = Collections.emptyMap(); + } else if (params.length == 1) { + NameValuePair nvp = params[0]; + parameters = Collections.singletonMap(nvp.getName() + .toLowerCase(Locale.ENGLISH), nvp.getValue()); + } else { + parameters = new HashMap(); + for (NameValuePair p : params) { + if (null != parameters.put( + p.getName().toLowerCase(Locale.ENGLISH), + p.getValue())) { + throw new Exception("duplicate parameter + " + + p.getName() + " field ignored"); + } + } + } + } + + public String getTarget() { + return target; + } + + public Map getParameters() { + return parameters; + } + + public String toString() { + return target + " " + parameters; + } + } +} diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/package-info.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/package-info.java new file mode 100644 index 00000000000..f2bbb6100f7 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/util/package-info.java @@ -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. + */ +@org.osgi.annotation.versioning.Version("1.0.1") +package org.apache.jackrabbit.webdav.util; diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/ActivityResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/ActivityResource.java new file mode 100644 index 00000000000..4d84790c800 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/ActivityResource.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.webdav.version; + +import org.apache.jackrabbit.webdav.property.DavPropertyName; + +/** + * An activity is a resource that selects a set of versions that are on a single + * "line of descent", where a line of descent is a sequence of versions connected + * by successor relationships. If an activity selects versions from multiple + * version histories, the versions selected in each version history must be on a + * single line of descent. + *

        + * RFC 3253 defines the following required live properties for an Activity + * resource. + *

          + *
        • {@link #ACTIVITY_VERSION_SET DAV:activity-version-set}
        • + *
        • {@link #ACTIVITY_CHECKOUT_SET DAV:activity-checkout-set}
        • + *
        • {@link #SUBACTIVITY_SET DAV:subactivity-set}
        • + *
        • {@link #CURRENT_WORKSPACE_SET DAV:current-workspace-set}
        • + *
        • all DeltaV-compliant resource properties}.
        • + *
        • Note, that the {@link org.apache.jackrabbit.webdav.DavConstants#PROPERTY_RESOURCETYPE DAV:resourcetype} + * property returned by an Activity resource must be + * {@link org.apache.jackrabbit.webdav.property.ResourceType#ACTIVITY DAV:activity}
        • + *
        + *

        + * The Activity resource must support all methods defined for a + * {@link DeltaVResource DeltaV-compliant resource}. Since no additional methods + * are required for an activity this interface mainly acts as marker. + *

        + * Please refer to RFC 3253 + * Section 13 for a complete description of this resource type. + */ +public interface ActivityResource extends DeltaVResource { + + /** + * The computed DAV:activity-version-set property identifies each version + * whose DAV:activity-set property identifies this activity. Multiple + * versions of a single version history can be selected by an activity's + * DAV:activity-version-set property, but all DAV:activity-version-set + * versions from a given version history must be on a single line of descent + * from the root version of that version history. + *

        + * Note that the DAV:activity-version-set represents a + * {@link org.apache.jackrabbit.webdav.property.HrefProperty HrefProperty} + */ + public static final DavPropertyName ACTIVITY_VERSION_SET = DavPropertyName.create("activity-version-set", DeltaVConstants.NAMESPACE); + + /** + * The computed DAV:activity-checkout-set property identifies each + * checked-out resource whose DAV:activity-set identifies this activity. + *

        + * Note that the DAV:activity-checkout-set represents a + * {@link org.apache.jackrabbit.webdav.property.HrefProperty HrefProperty} + */ + public static final DavPropertyName ACTIVITY_CHECKOUT_SET = DavPropertyName.create("activity-checkout-set", DeltaVConstants.NAMESPACE); + + /** + * The DAV:subactivity-set property identifies each activity that forms a + * part of the logical change being captured by this activity. An activity + * behaves as if its DAV:activity-version-set is extended by the + * DAV:activity-version-set of each activity identified in the + * DAV:subactivity-set. In particular, the versions in this extended set + * MUST be on a single line of descent, and when an activity selects a version + * for merging, the latest version in this extended set is the one that will + * be merged. + *

        + * A server MAY reject attempts to modify the DAV:subactivity-set of an activity. + * + * Note that the DAV:subactivity-set represents a + * {@link org.apache.jackrabbit.webdav.property.HrefProperty HrefProperty} + */ + public static final DavPropertyName SUBACTIVITY_SET = DavPropertyName.create("subactivity-set", DeltaVConstants.NAMESPACE); + + /** + * The computed DAV:current-workspace-set property identifies identifies + * each workspace whose DAV:current-activity-set identifies this activity. + *

        + * Note that the DAV:current-workspace-set represents a + * {@link org.apache.jackrabbit.webdav.property.HrefProperty HrefProperty} + */ + public static final DavPropertyName CURRENT_WORKSPACE_SET = DavPropertyName.create("current-workspace-set", DeltaVConstants.NAMESPACE); + +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/BaselineResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/BaselineResource.java new file mode 100644 index 00000000000..b48d6f2d457 --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/BaselineResource.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.webdav.version; + +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.DavResource; +import org.apache.jackrabbit.webdav.DavException; + +/** + * BaselineResource represents the 'version' of a configuration + * which is represented by a 'version-controlled-configuration' (VCC) resource. + * Such as new versions are created by CHECKIN of a version-controlled + * resource, a new baseline is created, whenever the VCC resource, that + * represents a set of resources rather than a single resource, is checked-in. + *

        + * Since the baseline behaves like a VersionResource and only is + * defined to provide additional protected properties, this interface only adds + * a convenience method that allows to retrieve the baseline collection. + *

        + * Supported live properties: + *

        + * DAV:baseline-collection
        + * DAV:subbaseline-set
        + * all version properties.
        + * 
        + * + * Supported methods: + *
        + * all version methods.
        + * 
        + */ +public interface BaselineResource extends VersionResource { + + /** + * The protected DAV:baseline-collection property identifies a distinct + * collection that lists as members all version-controlled resources of + * the configuration this baseline belongs to (the baseline being one + * version of the corresponding vc-configuration-resource). In other words: + * each member in the list must correspond to a member of the baseline-controlled + * collection at the time this baseline (version) was created. + *

        + * + * Note that the DAV:baseline-collection represents a + * {@link org.apache.jackrabbit.webdav.property.HrefProperty HrefProperty} + */ + public static final DavPropertyName BASELINE_COLLECTION = DavPropertyName.create("baseline-collection", DeltaVConstants.NAMESPACE); + + /** + * The protected DAV:subbaseline-set property identifies a set of baseline + * resources. Note however, that the subbaselines of this resource are + * not only formed from the baseline resources listed in this property + * but also includes all subbaseline resources of the latter. + * + * Note that the DAV:subbaseline-set represents a + * {@link org.apache.jackrabbit.webdav.property.HrefProperty HrefProperty} + */ + public static final DavPropertyName SUBBASELINE_SET = DavPropertyName.create("subbaseline-set", DeltaVConstants.NAMESPACE); + + /** + * Return the resource that represents the baseline-collection of this + * baseline, which is identified the href present in the {@link #BASELINE_COLLECTION} + * property. + * + * @return baseline collection + */ + public DavResource getBaselineCollection() throws DavException; +} \ No newline at end of file diff --git a/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/DeltaVConstants.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/DeltaVConstants.java new file mode 100644 index 00000000000..8fc7e968b2a --- /dev/null +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/DeltaVConstants.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.webdav.version; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * DeltaVConstants defines the following headers and properties + * required for any resource that is compliant to + * RFC 3253:

        + * + * Headers: + *

        + * Label
        + * 
        + * + * Properties: + *
        + * DAV:comment
        + * DAV:creator-displayname
        + * DAV:supported-method-set
        + * DAV:supported-live-property-set
        + * DAV:supported-report-set
        + * 
        + * + * Some additional resource properties are defined by the various advanced + * version features: + *
        + * DAV:workspace (workspace feature)
        + * DAV:version-controlled-configuration (baseline)
        + * 
        + */ +public interface DeltaVConstants { + + /** + * The DAV: namespace. + */ + public static final Namespace NAMESPACE = DavConstants.NAMESPACE; + + //---< Headers >------------------------------------------------------------ + /** + * For certain METHODS, if the request-URL identifies a version-controlled + * resource, a label can be specified in a LabelInfo request header to cause the + * method to be applied to the version selected by that label.
        + * LabelInfo header MUST have no effect on a request whose request-URL does not + * identify a version-controlled resource. In particular, it MUST have no + * effect on a request whose request-URL identifies a version or a version + * history. + */ + public static final String HEADER_LABEL = "Label"; + + /** + * Location header as defined by + * RFC 2616. In the versioning + * context it is used to indicate the location of the new version created by a + * successful checkin in the response.

        + * From RFC 2616:
        + * The Location response-header field is used to redirect the recipient to a + * location other than the Request-URI for completion of the request or + * identification of a new resource.
        + * For 201 (Created) responses, the Location is that of the new resource + * which was created by the request. + */ + public static final String HEADER_LOCATION = "Location"; + + //---< Property Names >----------------------------------------------------- + /** + * The "DAV:comment" property is used to track a brief comment about a resource that is + * suitable for presentation to a user. The DAV:comment of a version can be + * used to indicate why that version was created. + */ + public static final DavPropertyName COMMENT = DavPropertyName.create("comment", NAMESPACE); + + /** + * The "DAV:creator-displayname" property contains a description of the creator of + * the resource that is suitable for presentation to a user. The + * DAV:creator-displayname of a version can be used to indicate who created + * that version. + */ + public static final DavPropertyName CREATOR_DISPLAYNAME = DavPropertyName.create("creator-displayname", NAMESPACE); + + /** + * Required protected live property for any resources being compliant with + * RFC 3253. Clients should classify a resource by examine the values of the + * DAV:supported-method-set and DAV:supported-live-property-set + * properties of that resource.
        + * Property structure: + *
        +     * <!ELEMENT supported-method-set (supported-method*)>
        +     * <!ELEMENT supported-method ANY>
        +     * <!ATTLIST supported-method name NMTOKEN #REQUIRED>
        +     * name value: a method name
        +     * 
        + * + * @see #SUPPORTED_LIVE_PROPERTY_SET + */ + public static final DavPropertyName SUPPORTED_METHOD_SET = DavPropertyName.create("supported-method-set", NAMESPACE); + + /** + * Required protected live property for any resources being compliant with + * RFC 3253. Clients should classify a resource by examine the values of the + * DAV:supported-method-set and DAV:supported-live-property-set + * properties of that resource.
        + * Property structure: + *
        +     * <!ELEMENT supported-live-property-set (supported-live-property*)>
        +     * <!ELEMENT supported-live-property name>
        +     * <!ELEMENT prop ANY>
        +     * ANY value: a property element type
        +     * 
        + * + * @see #SUPPORTED_METHOD_SET + */ + public static final DavPropertyName SUPPORTED_LIVE_PROPERTY_SET = DavPropertyName.create("supported-live-property-set", NAMESPACE); + + /** + * Protected "supported-report-set" property identifies the reports that are + * supported by the resource. + * + * @see #SUPPORTED_REPORT_SET + */ + public static final DavPropertyName SUPPORTED_REPORT_SET = DavPropertyName.create("supported-report-set", NAMESPACE); + + /** + * Protected "workspace" property indicating the workspace of a resource. + * This property is required for all resources if (but only if) the workspace + * feature is supported. + *

        + * Note that the DAV:activity-version-set represents a + * {@link org.apache.jackrabbit.webdav.property.HrefProperty HrefProperty}. + * It is defined to have the following format: + *

        +     * <!ELEMENT workspace (href)>
        +     * 
        + * + * @see WorkspaceResource + */ + public static final DavPropertyName WORKSPACE = DavPropertyName.create("workspace", NAMESPACE); + + /** + * The Baseline feature introduces the computed DAV:version-controlled-configuration + * property for all resources that are member of a version-controlled + * configuration. This may be the case if the resource is a collection under + * baseline control or is a member of a collection under baseline control. + *

        + * Note that the DAV:activity-version-set represents a + * {@link org.apache.jackrabbit.webdav.property.HrefProperty HrefProperty}. + * It is defined to have the following format: + *

        +     * <!ELEMENT version-controlled-configuration (href)>
        +     * 
        + */ + public static final DavPropertyName VERSION_CONTROLLED_CONFIGURATION = DavPropertyName.create("version-controlled-configuration", NAMESPACE); + + + //---< XML Element, Attribute Names >--------------------------------------- + /** + * Xml elements + */ + public static final String XML_ACTIVITY = "activity"; + public static final String XML_BASELINE = "baseline"; + + public static final String XML_SUPPORTED_METHOD = "supported-method"; + public static final String XML_VERSION_HISTORY = "version-history"; + public static final String XML_VERSION = "version"; + public static final String XML_WORKSPACE = "workspace"; + + // options + /** + * If the OPTIONS request contains a body, i must start with an DAV:options + * element. + * + * @see OptionsInfo + * @see #XML_VH_COLLECTION_SET + * @see #XML_WSP_COLLECTION_SET + * @see #XML_ACTIVITY_COLLECTION_SET + */ + public static final String XML_OPTIONS = "options"; + + /** + * If an XML response body for a successful request is included, it must be + * a DAV:options-response XML element. + * + * @see OptionsResponse + */ + public static final String XML_OPTIONS_RESPONSE = "options-response"; + + /** + * A DAV:version-history-collection-set element may be included in the OPTIONS + * request body to identify collections that may contain version history + * resources.
        + * The response body for a successful request must in consequence contain a + * DAV:version-history-collection-set element identifying collections that + * may contain version histories. An identified collection may be the root + * collection of a tree of collections, all of which may contain version + * histories. + * + *
        +     * <!ELEMENT version-history-collection-set (href*)>
        +     * 
        + */ + public static final String XML_VH_COLLECTION_SET = "version-history-collection-set"; + + /** + * A DAV:workspace-collection-set element may be included in the OPTIONS request + * body to identify collections that may contain workspace resources.
        + * The response body for a successful request must contain a + * DAV:workspace-collection-set element identifying collections that may + * contain workspaces. An identified collection may be the root collection + * of a tree of collections, all of which may contain workspaces. + * + *
        +     * <!ELEMENT workspace-collection-set (href*)>
        +     * 
        + */ + public static final String XML_WSP_COLLECTION_SET = "workspace-collection-set"; + + /** + * A DAV:workspace-collection-set element may be included in the OPTIONS request + * body to identify collections that may contain activity resources.
        + * The response body for a successful request must contain a + * DAV:workspace-collection-set element identifying collections that may + * contain activity resources. An identified collection may be the root collection + * of a tree of collections, all of which may contain activity resources. + * + *
        +     * <!ELEMENT activity-collection-set (href*)>
        +     * 
        + */ + public static final String XML_ACTIVITY_COLLECTION_SET = "activity-collection-set"; + + /** + * Name of Xml element contained in the {@link #SUPPORTED_REPORT_SET} property. + * + * @see #SUPPORTED_REPORT_SET + * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty + */ + public static final String XML_SUPPORTED_REPORT = "supported-report"; + + /** + * Name of Xml child elements of {@link #XML_SUPPORTED_REPORT}. + * + * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty + */ + public static final String XML_REPORT = "report"; + + /** + * Top element for the 'DAV:version-tree' report + */ + public static final String XML_VERSION_TREE = "version-tree"; + + /** + * Top element for the 'DAV:expand-property' report + */ + public static final String XML_EXPAND_PROPERTY = "expand-property"; + + /** + * 'DAV:property' element to be used inside the 'DAV:expand-property' element. + * + * @see #XML_EXPAND_PROPERTY + */ + public static final String XML_PROPERTY = "property"; + + /** + * 'DAV:name' attribute for the property element + * + * @see #XML_PROPERTY + */ + public static final String ATTR_NAME = "name"; + + /** + * 'DAV:namespace' attribute for the property element + * + * @see #XML_PROPERTY + */ + public static final String ATTR_NAMESPACE = "namespace"; + + /** + * Top element for the 'DAV:locate-by-history' report + */ + public static final String XML_LOCATE_BY_HISTORY = "locate-by-history"; + + /** + * 'DAV:version-history-set' to be used inside the 'DAV:locate-by-history' + * element + * + * @see #XML_LOCATE_BY_HISTORY + */ + public static final String XML_VERSION_HISTORY_SET = "version-history-set"; + + + /** + * Xml element representing the mandatory root element of a LABEL request + * body. + * + * @see #XML_LABEL_NAME + * @see #XML_LABEL_ADD + * @see #XML_LABEL_REMOVE + * @see #XML_LABEL_SET + * @see LabelInfo + */ + public static final String XML_LABEL = "label"; + public static final String XML_LABEL_NAME = "label-name"; + public static final String XML_LABEL_ADD = "add"; + public static final String XML_LABEL_REMOVE = "remove"; + public static final String XML_LABEL_SET = "set"; + + /** + * Xml element defining the top element in the UPDATE request body. RFC 3253 + * defines the following structure for the 'update' element. + *
        +     * <!ELEMENT update ANY>
        +     * ANY value: A sequence of elements with at most one DAV:version element
        +     * and at most one DAV:prop element.
        +     * <!ELEMENT version (href)>
        +     * prop: see RFC 2518, Section 12.11
        +     * 
        + */ + public static final String XML_UPDATE = "update"; + + // auto-version + /** + * Value for the DAV:auto-version property indicating that any modification + * (such as PUT/PROPPATCH) applied to a checked-in version-controlled + * resource will automatically be preceded by a checkout and followed by a + * checkin operation.
        + * See also RFC 3253 DAV:auto-version + */ + public static final String XML_CHECKOUT_CHECKIN = "checkout-checkin"; + /** + * Value for the DAV:auto-version property indicating that any modification + * (such as PUT/PROPPATCH) applied to a checked-in version-controlled + * resource will automatically be preceded by a checkout operation. + * If the resource is not write-locked, the request is automatically + * followed by a checkin operation.
        + * See also RFC 3253 DAV:auto-version + */ + public static final String XML_CHECKOUT_UNLOCK_CHECKIN = "checkout-unlocked-checkin"; + /** + * Value for the DAV:auto-version property indicating that any modification + * (such as PUT/PROPPATCH) applied to a checked-in version-controlled + * resource will automatically be preceded by a checkout operation.
        + * See also RFC 3253 DAV:auto-version + */ + public static final String XML_CHECKOUT = "checkout"; + /** + * Value for the DAV:auto-version property indicating that any modification + * (such as PUT/PROPPATCH) applied to a write-locked checked-in version-controlled + * resource will automatically be preceded by a checkout operation.
        + * See also RFC 3253 DAV:auto-version + */ + public static final String XML_LOCKED_CHECKIN = "locked-checkout"; + + // merge + public static final String XML_MERGE = "merge"; + public static final String XML_N0_AUTO_MERGE = "no-auto-merge"; + public static final String XML_N0_CHECKOUT = "no-checkout"; +} \ No newline at end of file diff --git a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVResource.java b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/DeltaVResource.java similarity index 77% rename from contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVResource.java rename to jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/DeltaVResource.java index 2a8dde0a8d0..a8a909de84e 100644 --- a/contrib/jcr-server/webdav/src/java/org/apache/jackrabbit/webdav/version/DeltaVResource.java +++ b/jackrabbit-webdav/src/main/java/org/apache/jackrabbit/webdav/version/DeltaVResource.java @@ -1,9 +1,10 @@ /* - * Copyright 2005 The Apache Software Foundation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -15,17 +16,16 @@ */ package org.apache.jackrabbit.webdav.version; -import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavException; -import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.property.DavPropertyName; -import org.apache.jackrabbit.webdav.version.report.ReportInfo; import org.apache.jackrabbit.webdav.version.report.Report; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; /** - * The DeltaVResource encapsultes the functionality common to all + * The DeltaVResource encapsulates the functionality common to all * DeltaV compliant resources. - *

        + *

        * RFC 3253 defines the following required properties: *

          *
        • {@link DeltaVConstants#COMMENT DAV:comment}
        • @@ -35,7 +35,7 @@ *
        • {@link DeltaVConstants#SUPPORTED_REPORT_SET DAV:supported-report-set}
        • *
        • all properties defined in WebDAV [RFC2518].
        • *
        - *

        + *

        * In addition a DeltaV compliant resource must support the following METHODS: *